perf(canvas): smooth reveal stream pacing
This commit is contained in:
parent
8bbe0768f3
commit
54295ce840
|
|
@ -17,13 +17,13 @@ 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_680;
|
||||
pub const REVEAL_DURATION_MS: u64 = 1_920;
|
||||
/// Delay between the first generated nodes in one applied batch.
|
||||
pub const REVEAL_STAGGER_MS: u64 = 48;
|
||||
pub const REVEAL_STAGGER_MS: u64 = 40;
|
||||
/// Minimum delay before descendants of a newly revealed container begin
|
||||
/// their own entrances. This leaves the parent opening beat readable
|
||||
/// without making nested content feel stalled.
|
||||
pub const REVEAL_CHILD_RUNWAY_MS: u64 = 80;
|
||||
pub const REVEAL_CHILD_RUNWAY_MS: u64 = 72;
|
||||
/// 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.
|
||||
|
|
@ -32,12 +32,12 @@ pub const REVEAL_DEPTH_STAGGER_MS: u64 = 0;
|
|||
/// 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.04;
|
||||
const REVEAL_FULL_STAGGER_SIBLINGS: u64 = 18;
|
||||
const REVEAL_FULL_STAGGER_SIBLINGS: u64 = 24;
|
||||
const REVEAL_MID_STAGGER_SIBLINGS: u64 = 72;
|
||||
const REVEAL_COMPRESSED_STAGGER_MS: u64 = 32;
|
||||
const REVEAL_TAIL_STAGGER_MS: u64 = 24;
|
||||
const REVEAL_MAX_NEW_STARTS_PER_SNAPSHOT: usize = 1;
|
||||
const REVEAL_BURST_RECOVERY_STAGGER_MS: u64 = REVEAL_STAGGER_MS;
|
||||
const REVEAL_BURST_RECOVERY_STAGGER_MS: u64 = 32;
|
||||
const CLOCK_REBASE_THRESHOLD_MS: u64 = 60_000;
|
||||
const REVEAL_FRAME_MS: u64 = 16;
|
||||
|
||||
|
|
@ -334,11 +334,18 @@ fn smooth_overdue_reveal_burst(r: &mut AgentIndicators, now_ms: u64) {
|
|||
}
|
||||
let mut newly_due_seen = 0;
|
||||
let mut reschedule_tail = false;
|
||||
let mut next_slot = now_ms.saturating_add(REVEAL_BURST_RECOVERY_STAGGER_MS);
|
||||
let mut next_slot = now_ms;
|
||||
for (id, original_start) in ordered {
|
||||
if original_start > prev_ms && original_start <= now_ms {
|
||||
newly_due_seen += 1;
|
||||
if newly_due_seen > REVEAL_MAX_NEW_STARTS_PER_SNAPSHOT {
|
||||
if newly_due_seen <= REVEAL_MAX_NEW_STARTS_PER_SNAPSHOT {
|
||||
if let Some(started_at) = r.reveals.get_mut(&id) {
|
||||
*started_at = next_slot;
|
||||
}
|
||||
next_slot = next_slot.saturating_add(REVEAL_BURST_RECOVERY_STAGGER_MS);
|
||||
reschedule_tail = true;
|
||||
continue;
|
||||
} else {
|
||||
reschedule_tail = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -452,7 +459,7 @@ mod tests {
|
|||
"reveal should stay active long enough to read as a smooth entrance"
|
||||
);
|
||||
assert!(
|
||||
snapshot_at(2_800).reveals.is_empty(),
|
||||
snapshot_at(3_000).reveals.is_empty(),
|
||||
"expired reveal should be pruned"
|
||||
);
|
||||
assert!(!is_active(), "expired reveal should not keep animating");
|
||||
|
|
@ -570,6 +577,34 @@ mod tests {
|
|||
end_if_epoch(epoch);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_replays_first_overdue_reveal_instead_of_jumping_mid_animation() {
|
||||
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let epoch = begin();
|
||||
for i in 0..6 {
|
||||
add_reveal(
|
||||
epoch,
|
||||
&format!("n{i}"),
|
||||
1_000 + i as u64 * REVEAL_STAGGER_MS,
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(snapshot_at(1_000).reveals.get("n0"), Some(&1_000));
|
||||
let snap = snapshot_at(1_220);
|
||||
|
||||
assert_eq!(
|
||||
snap.reveals.get("n1"),
|
||||
Some(&1_220),
|
||||
"the first overdue node should restart at the current frame instead of appearing partway through"
|
||||
);
|
||||
assert_eq!(
|
||||
snap.reveals.get("n2"),
|
||||
Some(&(1_220 + REVEAL_BURST_RECOVERY_STAGGER_MS)),
|
||||
"overdue siblings should recover one-by-one on a tighter cadence"
|
||||
);
|
||||
end_if_epoch(epoch);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_recovery_preserves_stream_order_after_frame_gap() {
|
||||
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
|
|
|||
|
|
@ -205,11 +205,11 @@ fn paint_reveal_sweep(
|
|||
let tail = (1.0 - t * t).clamp(0.0, 1.0);
|
||||
let radius = 8.0_f32.min(animated.size.y / 2.0);
|
||||
cx.backend
|
||||
.fill_round_rect(animated, radius, REVEAL_ACCENT.with_alpha(0.075 * tail));
|
||||
.fill_round_rect(animated, radius, REVEAL_ACCENT.with_alpha(0.060 * tail));
|
||||
cx.backend.stroke_round_rect(
|
||||
animated,
|
||||
radius,
|
||||
REVEAL_ACCENT.with_alpha(0.42 * tail),
|
||||
REVEAL_ACCENT.with_alpha(0.34 * tail),
|
||||
1.25,
|
||||
);
|
||||
let sweep_w = (animated.size.x * 0.18).clamp(16.0, 64.0);
|
||||
|
|
@ -223,14 +223,14 @@ fn paint_reveal_sweep(
|
|||
cx.backend.save();
|
||||
cx.backend.clip_rect(animated);
|
||||
cx.backend
|
||||
.fill_round_rect(sweep, sweep_w / 2.0, Color::WHITE.with_alpha(0.24 * tail));
|
||||
.fill_round_rect(sweep, sweep_w / 2.0, Color::WHITE.with_alpha(0.18 * tail));
|
||||
cx.backend.restore();
|
||||
}
|
||||
|
||||
fn lifted_scaled_rect(rect: Rect, ease: f32) -> Rect {
|
||||
let settle = 1.0 - ease;
|
||||
let scale = 0.970 + ease * 0.030;
|
||||
let lift = settle * 8.0;
|
||||
let scale = 0.960 + ease * 0.040;
|
||||
let lift = settle * 11.0;
|
||||
let w = rect.size.x * scale;
|
||||
let h = rect.size.y * scale;
|
||||
Rect {
|
||||
|
|
@ -457,7 +457,7 @@ mod tests {
|
|||
&roots,
|
||||
Point2D::new(100.0, 50.0),
|
||||
2.0,
|
||||
2_800,
|
||||
3_000,
|
||||
);
|
||||
assert!(expired_backend.round_fills.is_empty());
|
||||
assert!(expired_backend.round_strokes.is_empty());
|
||||
|
|
|
|||
|
|
@ -669,8 +669,8 @@ fn push_reveal_transform(cx: &mut PaintCx<'_>, rect: Rect, phase: RevealPhase) -
|
|||
return false;
|
||||
}
|
||||
let settle = 1.0 - phase.ease;
|
||||
let lift = 9.0 * settle * (1.0 - phase.t * 0.12);
|
||||
let scale = 0.968 + 0.032 * phase.ease;
|
||||
let lift = 11.0 * settle * (1.0 - phase.t * 0.10);
|
||||
let scale = 0.960 + 0.040 * phase.ease;
|
||||
let pivot = Point2D::new(
|
||||
rect.origin.x + rect.size.x / 2.0,
|
||||
rect.origin.y + rect.size.y / 2.0,
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ fn reveal_schedule_gives_new_container_children_an_entrance_runway() {
|
|||
});
|
||||
|
||||
assert!(
|
||||
starts[0] - status >= 80,
|
||||
starts[0] - status >= op_editor_core::agent_indicators::REVEAL_CHILD_RUNWAY_MS,
|
||||
"children of a new container need their own runway instead of starting inside the parent's first beat"
|
||||
);
|
||||
assert!(
|
||||
|
|
|
|||
Loading…
Reference in a new issue