perf(canvas): smooth ai reveal streaming

This commit is contained in:
Kayshen-X 2026-06-15 00:57:12 +08:00
parent 55c2775130
commit da4bfb1a7e
4 changed files with 43 additions and 21 deletions

View file

@ -17,19 +17,20 @@ use std::collections::{HashMap, HashSet};
use std::sync::{LazyLock, Mutex};
/// Duration of the short generated-node entrance animation.
pub const REVEAL_DURATION_MS: u64 = 1_360;
pub const REVEAL_DURATION_MS: u64 = 1_520;
/// Delay between the first generated nodes in one applied batch.
pub const REVEAL_STAGGER_MS: u64 = 24;
/// Extra delay for nested generated nodes. Keeps child content trailing
/// its parent without making the stream feel sluggish.
pub const REVEAL_DEPTH_STAGGER_MS: u64 = 6;
pub const REVEAL_STAGGER_MS: u64 = 18;
/// Extra delay for nested generated nodes. Visual traversal order
/// already places children after parents; keeping this at zero avoids
/// depth changes compressing adjacent stream slots into the same frame.
pub const REVEAL_DEPTH_STAGGER_MS: u64 = 0;
/// Parent reveals suppress child transforms only during their opening
/// beat. Once the parent has begun settling, delayed children animate
/// independently so streamed content does not pop in abruptly.
pub const REVEAL_CHILD_SUPPRESS_FRACTION: f32 = 0.11;
const REVEAL_FULL_STAGGER_SIBLINGS: u64 = 24;
const REVEAL_MID_STAGGER_SIBLINGS: u64 = 48;
const REVEAL_COMPRESSED_STAGGER_MS: u64 = 20;
pub const REVEAL_CHILD_SUPPRESS_FRACTION: f32 = 0.04;
const REVEAL_FULL_STAGGER_SIBLINGS: u64 = 36;
const REVEAL_MID_STAGGER_SIBLINGS: u64 = 72;
const REVEAL_COMPRESSED_STAGGER_MS: u64 = REVEAL_FRAME_MS;
const REVEAL_TAIL_STAGGER_MS: u64 = REVEAL_FRAME_MS;
const CLOCK_REBASE_THRESHOLD_MS: u64 = 60_000;
const REVEAL_FRAME_MS: u64 = 16;
@ -384,7 +385,11 @@ mod tests {
assert!(is_active(), "reveal should keep the paint loop active");
assert_eq!(snapshot_at(1_250).reveals.get("n7"), Some(&1_000));
assert!(
snapshot_at(2_400).reveals.is_empty(),
snapshot_at(2_400).reveals.get("n7").is_some(),
"reveal should stay active long enough to read as a smooth entrance"
);
assert!(
snapshot_at(2_600).reveals.is_empty(),
"expired reveal should be pruned"
);
assert!(!is_active(), "expired reveal should not keep animating");
@ -416,8 +421,8 @@ mod tests {
let snap = snapshot_at(1_040);
assert_eq!(snap.reveals.get("external-a"), Some(&1_104));
assert_eq!(snap.reveals.get("external-b"), Some(&1_128));
assert_eq!(snap.reveals.get("external-a"), Some(&1_098));
assert_eq!(snap.reveals.get("external-b"), Some(&1_116));
clear();
}
@ -440,6 +445,10 @@ mod tests {
offsets[1] - offsets[0] <= 32,
"the first visible nodes should start quickly"
);
assert!(
offsets[20] - offsets[0] <= 380,
"the first screenful should not wait behind a slow reveal queue"
);
assert!(
offsets
.windows(2)

View file

@ -390,7 +390,7 @@ mod tests {
&roots,
Point2D::new(100.0, 50.0),
2.0,
2_400,
2_600,
);
assert!(expired_backend.round_fills.is_empty());
assert!(expired_backend.round_strokes.is_empty());
@ -437,7 +437,7 @@ mod tests {
}
#[test]
fn parent_reveal_suppresses_nested_child_sweep() {
fn opening_parent_reveal_suppresses_nested_child_sweep() {
let _guard = crate::agent_indicator_test_support::lock();
let epoch = op_editor_core::agent_indicators::begin();
op_editor_core::agent_indicators::add_reveal(epoch, "parent", 1_000);
@ -453,12 +453,12 @@ mod tests {
let mut cx = PaintCx {
backend: &mut backend,
};
paint_agent_frame_indicators(&mut cx, &roots, Point2D::new(100.0, 50.0), 2.0, 1_120);
paint_agent_frame_indicators(&mut cx, &roots, Point2D::new(100.0, 50.0), 2.0, 1_040);
assert_eq!(
backend.round_strokes.len(),
1,
"nested child reveal should not add another sweep while parent is active"
"nested child reveal should not add another sweep during the parent's opening beat"
);
op_editor_core::agent_indicators::end_if_epoch(epoch);
}

View file

@ -119,15 +119,15 @@ fn active_reveal_wraps_node_paint_in_transform() {
}
#[test]
fn active_parent_reveal_prevents_nested_child_transform() {
fn opening_parent_reveal_prevents_nested_child_transform() {
let frame = frame_with_child();
let reveals = HashMap::from([("f".to_string(), 1_000), ("c".to_string(), 1_040)]);
let backend = paint_with_reveals(&frame, &reveals, 1_120);
let backend = paint_with_reveals(&frame, &reveals, 1_040);
assert_eq!(
backend.scales, 1,
"child reveal should not stack another transform while its parent is easing"
"child reveal should not stack another transform during the parent's opening beat"
);
assert!(
backend.ops.contains(&"fill(10,10)".to_string()),
@ -135,6 +135,19 @@ fn active_parent_reveal_prevents_nested_child_transform() {
);
}
#[test]
fn child_reveal_gets_own_transform_after_parent_opening_beat() {
let frame = frame_with_child();
let reveals = HashMap::from([("f".to_string(), 1_000), ("c".to_string(), 1_080)]);
let backend = paint_with_reveals(&frame, &reveals, 1_120);
assert_eq!(
backend.scales, 2,
"parent reveal should not swallow the child's own streamed entrance after the opening beat"
);
}
#[test]
fn delayed_child_reveal_keeps_its_own_transform_after_parent_settles() {
let frame = frame_with_child();

View file

@ -40,11 +40,11 @@ fn reveal_schedule_streams_large_subtrees_without_long_tail() {
let section = *snapshot.reveals.get("section").expect("section reveal");
let last_label = *snapshot.reveals.get("label-19").expect("last label reveal");
assert!(
last_label - section >= 420,
last_label - section >= 300,
"large nested subtrees should stay visibly streamed instead of arriving in one burst"
);
assert!(
last_label - section <= 650,
last_label - section <= 430,
"large nested subtrees should avoid a slow reveal queue"
);
assert!(