Animation
Retained time-based transitions, keyframes, and application-defined motion.
Transitions in immediate mode
Call the same channel each visible pass. Its first appearance uses the target
immediately; subsequent target changes animate from its currently sampled value.
transition_from supplies an initial value when the first appearance should move.
use std::time::Duration;
use zaxis::{Color, Easing, TweenOptions};
let target = if enabled { Color::WHITE } else { Color::gray(100) };
let color = ui.transition("indicator-color", target,
TweenOptions::new(Duration::from_millis(160)).easing(Easing::QuadOut)).value;Ui scopes these IDs like widgets. Context takes an explicit Id; use
widget_id.with("color"), widget_id.with("angle"), etc. for separate properties.
Use ui.animation_id(source) to obtain a channel ID for pause/cancel controls.
Repeated calls with the same target never restart, even after completion or
cancellation. A new target restarts a paused/cancelled transition from its frozen
value. Changed options apply on the next target change. Avoid NaN value targets:
transition identity follows the type's PartialEq implementation.
Explicit tracks
animate(id, factory) creates a track once. Completed and cancelled channels keep
their value and status, but release the track. The factory runs again only after
the channel disappeared for a complete UI pass or an explicit restart.
use std::time::Duration;
use zaxis::{Easing, Repeat, Tween};
let motion = ui.animate("pulse", ||
Tween::new(0.5_f32, 1.0, Duration::from_millis(300))
.delay(Duration::from_millis(100))
.easing(Easing::SineInOut)
.repeat(Repeat::Count(3))
.auto_reverse(true));
if motion.just_completed {
// One completion event for the entire track, rather than for each cycle.
}| Rule | Behavior |
|---|---|
| Start | Clock starts at the creating/restarting UI pass; the first sample is elapsed zero |
| Delay | Initial value remains visible; one future deadline, before the first cycle only |
| Duration | Time of a forward leg; elapsed time, never a frame counter |
| Repeat | Once, Count(n) total cycles including the first, or Forever; count zero panics |
| Auto-reverse | Each cycle has forward and backward legs; finishes at the exact initial value |
| Cycle boundary | Repeated forward-only tracks reset to their start; reverse tracks return continuously |
| Zero duration | Completes at the end of delay, even with Forever; reduced motion skips the delay |
| Completion | At or beyond total duration; exact endpoint clone, Completed, no next deadline |
| Pause | Samples at this pass's time, freezes value and elapsed time including remaining delay |
| Resume | Continues frozen elapsed time; paused wall time is excluded; completed/cancelled tracks stay stopped |
| Cancel | Samples and freezes the current value, discards the track; no completion event |
| Restart | Replaces the track, resets elapsed time, pause/cancel state and pending completion; may jump to its new start |
| Finish | Applies Animation::finish() exactly, even when paused, and emits completion once |
Use pause_animation, resume_animation, cancel_animation, finish_animation,
restart_animation and sample_animation on Context. Controls are idempotent;
their boolean return indicates whether the channel exists. Apply them during a
UI pass; outside a pass they use the latest frame_time. Restart must be followed
by an animate/sample call on the same pass to mark the channel visible.
Animated exposes value, status, running(), completed() and
just_completed. The completion flag is consumed by the first read, including
multiple reads within a pass. Cancelling is distinct from completing.
Rate, direction and scrubbing
A running or paused channel can change how its track time advances:
| Call | Effect |
|---|---|
set_animation_rate(id, rate) | Speed multiplier (2.0 is twice as fast, negative plays backwards). Position is kept; zero or non-finite panics, use pause to stop |
reverse_animation(id) | Flips the sign of the rate from the current pose |
seek_animation(id, elapsed) | Jumps to track time. A paused channel shows the pose and stays paused; past the end it completes |
animation_elapsed, animation_rate, animation_duration | Read back track time, rate and exact length, e.g. to drive a scrubber |
Playing backwards completes at the start pose (not finish()), with one completion
event. Completed and cancelled channels hold no track and cannot be seeked or
reversed; restart them, which also resets the rate to 1.0. Wake-ups of sleeping
stages are converted to wall time, so a half-speed Delay wakes at twice the
remaining track time without polling.
MotionStyle::time_scale multiplies the speed of every channel (0.25 is slow
motion for debugging). Changing it mid-flight does not jump; non-positive or
non-finite values mean 1.0. It scales animation channels only, not cursor
blink, scrolling or other timers.
Testing tracks
Tracks are pure functions of elapsed time. zaxis::animation::testing checks
an application track at several frame rates without a window:
assert_exact_finish (not complete at zero, exact finish() at every rate,
exact end after a long suspension, repeatable samples), assert_monotonic and
frames. For retained channels, drive Context::run_at with explicit instants.
Keyframes and curves
use std::time::Duration;
use zaxis::{Easing, Keyframe, Keyframes};
let motion = ui.animate("sequence", || Keyframes::new([
Keyframe::new(Duration::ZERO, 0.0_f32),
Keyframe::new(Duration::from_millis(100), 8.0).easing(Easing::QuadOut),
Keyframe::new(Duration::from_millis(280), 0.0).easing(Easing::QuintOut),
]));The easing on each point controls the segment arriving at that point. Exact point times return the original value. At least two strictly increasing points starting at zero are required; invalid input panics at construction. Keyframes support delay, repeat and auto-reverse with the same cycle rules as tweens.
Built-in curves are Linear and the In/Out/InOut forms of Quad, Cubic,
Quint, Sine, Expo, Circ, Back, Elastic and Bounce. In and InOut
are mirrors of each family's Out curve, so In(t) + Out(1 - t) = 1 and InOut
passes exactly through 0.5. Back and Elastic overshoot [0, 1] by design.
Circ has a vertical tangent at one end.
Easing::cubic_bezier(x1, y1, x2, y2) is the CSS timing function; x1 and x2
must lie in [0, 1] (time stays monotonic) while y1 and y2 may overshoot.
Easing::ease() and Easing::ease_in_out() are the CSS presets.
Easing::steps(n) makes n equal jumps at the end of each interval.
Easing::custom takes a reusable Fn(f32) -> f32 + Send + Sync + 'static, which
can capture curve data. Input is clamped to [0, 1], output must be finite and may
overshoot. Endpoints stay exact for every curve.
Paths
Path::new(start) builds a 2D path with line_to, quad_to and cubic_to;
curves are flattened once into a polyline with cumulative arc length, so
point_at(fraction) moves at constant speed however the control points are
placed. angle_at(fraction) is the direction of travel in radians,
pose_at returns both as a PathPose, length() is the total, and points() is
the polyline for drawing it.
PathFollow::new(path, duration) is an Animation<PathPose> with the tween
builders easing, delay, repeat and auto_reverse; it completes exactly on
the pose at the end. For a draw-on stroke animate a fraction and paint
path.trimmed(0.0, fraction), which returns the polyline between two fractions
(corners included).
Interpolation and external motion
Interpolate supports f32, f64, Vec2, Color, Border, CornerRadius,
Shadow, Rect, Padding, Transform, Gradient, arrays, pairs and triples of
supported types, plus Option<T> and Vec<T>. Some to Some and equal-length
vectors interpolate element-wise; appearing, disappearing or a changed length is
discrete (the from state is kept until the end). Color RGB interpolates in
linear-light sRGB, alpha linearly in straight-alpha space. Results are
encoded to sRGB and rounded to bytes. For example, black to white at 50% is
Color::gray(188). Gradient direction is discrete and changes at its endpoint.
Numeric interpolation allows custom-curve overshoot.
Oklab(color) is a wrapper that blends in the perceptual Oklab space: black to
white at 50% is mid grey (about 99) rather than 188, and saturated pairs such as
blue to yellow keep their lightness instead of dipping through grey. Use
Tween<Oklab> or transition(.., Oklab(target), ..) and read .0 for the color.
Endpoints are exact in both spaces.
Structs whose fields are all supported implement the traits with a macro instead of by hand:
#[derive(Clone, Debug, PartialEq)]
struct Pose { offset: Vec2, angle: f32 }
zaxis::impl_interpolate!(Pose { offset, angle });
zaxis::impl_spring_value!(Pose { offset, angle }); // for springs and DecayApplication value types implement Interpolate: Clone + PartialEq + 'static.
Application effects implement Animation<T> with two methods:
use std::time::Duration;
use zaxis::{Animation, AnimationSample};
#[derive(Clone)]
struct Dial { angle: f32 }
struct Wobble { target: f32 }
impl Animation<Dial> for Wobble {
fn sample(&self, elapsed: Duration) -> AnimationSample<Dial> {
if elapsed >= Duration::from_secs(1) {
return AnimationSample::completed(self.finish());
}
let t = elapsed.as_secs_f32();
AnimationSample::running(Dial {
angle: self.target * (1.0 - (-8.0 * t).exp() * (14.0 * t).cos()),
})
}
fn finish(&self) -> Dial { Dial { angle: self.target } }
}sample is a pure function of elapsed time and returns value, completion and
scheduling information. It must handle arbitrary elapsed times and large gaps.
finish supplies the exact state for explicit finish and reduced motion.
AnimationSample::after(value, delay) lets procedural timers sleep until their
next meaningful change. Procedural::new(final_value, closure) is a closure
adapter. Animation<T> only requires T: Clone + 'static in the context; a custom
track need not implement interpolation or equality. No unsafe, closed effect
enum, plugin registration, or worker thread is involved.
The custom_animation example defines an external Pose, its
Interpolate and SpringValue implementations and a custom easing curve. It
uses the built-in analytic spring, including physical velocity on retarget,
pause, resume, cancellation and completion events. Wobble above is an arbitrary
external time function; it does not model spring velocity.
Style and built-in components
Style::motion contains reduced_motion, hover, expand, page and
frame_interval. Defaults follow Rayfield's motion: hover/colors/reveal/chevron
use 160 ms Quad Out, directional pages use 280 ms Quint Out. The sampling interval
defaults to 16 ms for Immediate and timer-driven hosts. The built-in Vsync runner
requests each next presented frame during continuous motion, without a second
timer throttle. Duration and
easing can be overridden per API call. Built-in component presets should use
Repeat::Once without auto-reverse, so their final value represents their state.
Button, Checkbox, Slider and ColorPicker share resolved hover transitions for fill, gradient, border, shadow and text. TextEdit animates its background; its cursor uses a sleeping procedural timer through the same engine. Slider values, pointer capture, text selection and window drag/resize remain immediate. ColorPicker's inline editor reveals under a matching clip and disables interaction as soon as closing starts. Its real line chevron turns through half a rotation.
Pair tab_bar with ui.tab_pages(source, selected_index, size, build) for page
slides. This fixed-size container enters from the right for higher indices and
from the left for lower indices. It completes the current slide and retains only
the latest pending selection. During a slide, both visible pages can be built and
their controls are disabled. Keep external side effects out of page build callbacks.
Hidden pages are not built. Paint and hit regions use the same translated layout;
the container clip remains fixed.
Set style.motion.reduced_motion = true to immediately finish decorative motion,
including delayed or paused tracks. Infinite decorative tracks use their declared
final state. Essential timers can opt out with
animate_with(..., AnimationOptions { decorative: false }, ...). Cursor blinking
uses that option. Reduced motion never discards the final UI state.
Host scheduling and lifecycle
Only channels sampled during the current visible UI pass request deadlines.
Ui helpers check their clip; built-in controls also check their own rectangle.
For custom drawings, call animation APIs only in the visible region that needs
them. Paused, completed, cancelled and disappeared channels have no deadlines.
Manual request_repaint_after timers remain independent. An early native redraw
does not postpone a delayed track's start. No animation call invalidates the whole
context: changed paint descriptions rebuild affected elements, and completed
geometry is reused normally.
Runner uses WaitUntil for deadlines and Wait in idle. Occluded, minimized,
zero-size and suspended windows do not wake for animation. On restoration,
elapsed time catches up directly; time does not automatically pause. Hidden tabs
stop building their channels and remove them; reappearing transitions snap to
their current target. Explicit animate starts a new visible lifetime.
Custom hosts use Context::run_at(now, build), frame_time() and
needs_repaint_at(now) with the same monotonic Instant clock. Vsync hosts can
also use wants_animation_frame() after presenting; it excludes sleeping timers
and stopped/hidden channels. One time is fixed
for all animation/timer computations in a pass. Backwards timestamps clamp to
the previous pass. Pass future deadlines to your event loop, and suppress wakeups
while the viewport is hidden. Normal Context::run samples Instant::now() once.
Do not mix a simulated clock with real-time needs_repaint().
Momentum and velocity-preserving transitions
Decay::new(from, velocity) is a fling: velocity (units per second) falls as
e^(-friction * t) and the value glides toward a rest position it only
approaches. It is analytic like the spring, so sampling cost is constant after any
suspension, and it knows its destination up front:
use std::time::Duration;
use zaxis::{Decay, DecayOptions};
let fling = Decay::with_options(position, release_velocity,
DecayOptions::half_life(Duration::from_millis(350)));
let rest = fling.rest(); // from + velocity / friction
ui.context().restart_animation(id, fling);Friction is per second (DecayOptions::friction(4.0) by default); half_life
is the time for the speed to halve. The track completes once the remaining
distance is under distance_threshold (0.01 by default), returns the exact rest
position and reports an exact duration(). Because rest() is known, clamp or
snap it before starting; to hit a chosen spot pass velocity = (spot - from) * friction. Values need SpringValue (f32, f64, Vec2 or your own type).
transition restarts its easing curve from zero speed on every retarget, which
shows as a hitch when the target changes repeatedly mid-flight. transition_smooth
(on Context and Ui, for SpringValue + Interpolate types) keeps the displayed
velocity: the retargeted leg is a cubic Hermite curve of length options.duration
that starts at the current value with the current speed and comes to rest at the
new target. It ignores the easing curve for that leg (the speed is measured from
the running track, including rate and time_scale); from rest, and for the first
leg, the options apply unchanged. Equal targets never restart, and the first call
snaps, exactly like transition. The leg can overshoot slightly when the old
motion points away from the new target, which is the continuous behavior.
Physical spring
use zaxis::{vec2, SpringOptions, SpringState};
let motion = ui.spring_transition("panel-position", vec2(120.0, 0.0),
ui.style().motion.spring);
let position = motion.value.value;
let velocity = motion.value.velocity; // logical pixels per second
// Only the first appearance uses this initial state.
let motion = ui.spring_transition_from("preview", SpringState {
value: 0.0_f32, velocity: 12.0,
}, 100.0, SpringOptions::frequency(3.0, 1.0));Context takes explicit IDs for the same methods. Spring::new(from, target)
and Spring::with_options(...).velocity(initial_velocity) are ordinary open
Animation<SpringState<T>> tracks; use all existing Context controls with them.
SpringValue adds only zero, addition, subtraction, scalar multiplication and
norm. Built-in implementations support f32, f64, and Vec2; application
types can implement it independently of Interpolate.
The differential equation is mass * acceleration + damping * velocity + stiffness * displacement = 0. For frequency f in Hz and damping ratio r,
omega = 2*pi*f, stiffness = mass*omega^2, damping = 2*mass*omega*r.
frequency uses mass one. Ratios below one oscillate, one is critically damped,
and above one are overdamped. Default: 3 Hz, critical damping, no bounce.
The analytic solution has constant sampling cost at 30/60/144 Hz or after hours
of suspension. No missed-frame integration loop runs.
Mass and thresholds must be positive, stiffness/damping nonnegative, all parameters finite; invalid parameters or overflowing coefficients panic at construction/retarget. Stiffness zero snaps to the exact target and zero velocity. Zero damping keeps oscillating unless initially within both rest thresholds; finish/cancel or stop building it to end that track. Thresholds default to 0.001 distance units and 0.001 units/second. Once both are satisfied, the channel returns the exact target, zero velocity, one completion event and no deadline.
Instead of frequency and ratio, SpringOptions::duration_bounce(duration, bounce)
takes the period of one undamped oscillation and a bounce in (-1, 1): 0 is
critically damped, positive values overshoot (0.15 to 0.4 feel springy),
negative values are overdamped. It is frequency(1 / duration, 1 - bounce) for
non-negative bounce and frequency(1 / duration, 1 / (1 + bounce)) otherwise.
Retarget first samples position and velocity at the pass time. The new spring starts at that state. Equal targets never restart; option changes apply on the next target change. First appearance without an initial state snaps silently. Pause freezes position, velocity and elapsed time; resume excludes paused wall time. Cancel freezes the sampled state and discards its track, with no completion. A new target resumes from that frozen state; restart replaces the state and may jump. Explicit finish also emits completion once. All controls use one pass clock.
Enter, Exit, and measured disclosure
use zaxis::{vec2, Presence};
Presence::fade().offset(vec2(0.0, ui.style().motion.slide_distance))
.scaling(0.98).show(ui, "tool-panel", visible, |ui| {
ui.label("Tool output");
ui.button("Copy");
});
ui.reveal("details", open, |ui| {
ui.label(&details); // actual height, including wrapping
});ui.presence(source, visible, closure) is the fade shortcut. Standalone presets
are Presence::fade(), ::slide(offset) and ::scale(scale); combine them with
fading, offset, scaling, pivot, and motion. Initially hidden content is
never built and never runs Exit. Enter starts from the hidden pose. Exit keeps
calling the current pass's closure until opacity/pose reaches its hidden endpoint;
then it stops building. No borrowed closure or application subtree is retained.
Changing visibility during either transition reverses from its sampled pose.
exit_layout(true) (default) preserves the measured allocation during Exit;
false releases it immediately while painting the outgoing contents as an overlay.
Both choices disable outgoing input immediately, clear focus/capture at pass end,
and ignore already queued widget activation. interactive_enter(true) is the
default: the transformed hit regions follow the displayed content. Disable it
to wait until Enter completes. Reusing the source in an entirely different area
requires an appropriate stable parent push_id.
Scale uses a relative pivot in measured bounds (center by default). Layout and
text measurement stay at logical size. Cached base meshes are transformed; glyphs and
curves rasterize/tessellate at displayed resolution, using quarter-step scale buckets
and a bounded resolution cap. A changing raster bucket rebuilds glyph geometry while
retaining its shaped layout; motion within a bucket reuses it. Beyond the cap, text
uses texture filtering. Parent clipping stays fixed, child clips follow geometry.
Opacity multiplies every primitive's alpha, including text, borders, shadows,
gradients and the blur mask. It never edits shared Style. Blur remains opt-in.
Popup anchors follow their transform; panels fit the screen viewport independently
and their full-viewport blockers stay fixed.
Ui::visual exposes the same axis-aligned mapping and subtree opacity directly.
Widget Response.rect remains a logical layout bound; after the helper returns,
Ui::displayed_rect(&response) or Context::visual_rect(id, rect) obtains its
visual bound. Input coordinates are mapped back for sliders and text selection.
Reveal measures arbitrary content once per pass and animates its actual height
under a clip. Resizes and rapid toggles retarget from the current displayed height.
Closing content loses input immediately; at zero height it is neither built nor
allocated. Parent neighbors and ScrollArea content size use the animated height.
reveal_with overrides Style.motion.expand. The internal ColorPicker uses this
same measured placement/clip path. Presence and reveal accept finite forward
tweens only (no repeat or auto-reverse). When an entire area disappears for a
pass, its state/channels/deadlines are pruned. Reappearance starts fresh: hidden
stays hidden; visible Presence enters; a newly opened Reveal starts from zero.
Opt-in reorder and selection marker
use zaxis::{Id, Reorder, Layout};
let mut bounds = Vec::new();
Reorder::new("results").show(ui, model.iter().map(|item| item.id), |rows| {
for item in visible_items {
let response = rows.item(item.id, |ui| ui.button(&item.label));
bounds.push((item.id, rows.ui().displayed_rect(&response)));
}
});
ui.selection_indicator("selected", selected_id, &bounds, Layout::Vertical);Each row executes once and obtains its final measured allocation before its
visual offset is applied. Later rows use final layout, not animated positions.
Stable model IDs, never labels or current indexes, own the retained motion.
Reordering during movement samples the displayed position before retargeting.
New rows snap to their final allocation; deleted model IDs are cleaned at pass
end; reinsertion after deletion starts fresh. Changed row sizes shift subsequent
targets. Duplicate model/build IDs are reported as an IdCollision diagnostic.
Always supply full model membership, including rows outside the virtual range. Build only visible rows and reserve skipped layout using your existing virtual list. Membership preserves hidden row identity; hidden tracks are sampled without requesting frames. A row omitted from building for a pass snaps to the current layout when it returns, avoiding movement from an obsolete viewport position. Scroll offsets are normalized out: ordinary viewport movement never retargets every row. The container itself disappearing releases its motion state. There is no drag-and-drop or owned list model.
Paint and hits use one visual offset. The final model order is paint order; later rows win overlaps in both drawing and hit testing. No duplicate interactive instances are created. Reorder is explicit; streaming text, editor state and ordinary layout changes outside this container stay immediate.
SelectionIndicator takes stable selected ID plus current actual bounds; use
displayed_rect for reordered/transformed controls. Selected model value and
keyboard focus change immediately. Only the decorative marker retargets, including
width/height changes, rapid switches and scrolling. Missing selection bounds
remove its channel immediately. For per-use settings use SelectionIndicator::new
with orientation, motion, color and thickness, then show.
Busy indicators and change highlight
ui.skeleton(height) (or Skeleton::new(height).width(w).corner_radius(r).period(p)) is
a loading placeholder: a muted rounded block with a soft highlight sweeping across
it. It is decorative and takes no input. Under reduced motion, when clipped or
hidden, it is a static block and requests no frames.
use zaxis::{Loader, Progress, ProgressState};
ui.add(Loader::new().active(working).size(24.0).stroke(2.0));
ui.add(Progress::new(match actual_progress {
Some(value) => ProgressState::Determinate(value),
None => ProgressState::Indeterminate { active: working },
}));
ui.pulse("busy-label", working, |ui| { ui.label("Agent is working"); });
ui.highlight("file-change", revision, rect, current_base_fill, accent);Loader uses the current Lucide loader geometry:
eight strokes with round caps in a 24x24 viewbox. Full Lucide ISC and original
Feather MIT notices are bundled in assets/LUCIDE-LICENSE. No SVG parser, browser,
network request or image runtime is needed. Cached geometry rotates around its
own center with a linear phase and constant speed. Numeric angle wraps modulo
one turn, but the displayed pose is continuous. Builders set size, color, stroke,
period and active state; defaults come from motion and theme. It registers no
hit region. Rotation never changes allocation. While active it is a busy progress
indicator to screen readers, named by Loader::label("Loading messages"); the
rotation never changes what they are told. An inactive loader is not announced.
ProgressState::Determinate applies the application's value immediately;
Complete fills the track and stops animation. Indeterminate has no percentage.
Screen readers get the same state: a percentage for determinate progress, "busy"
for active indeterminate work, never the animated segment. Name the bar with
.accessible_label(..).
Inactive/reduced-motion indeterminate shows a fixed segment as a busy symbol.
Pulse quietly varies opacity between 0.65 and one; text never moves. Override
with pulse_with(..., Pulse::new(period).minimum(...), ...). Rotation and
Pulse also implement the public Animation trait for direct composition.
All convenience cycles require an external active flag. Inactive or omitted
cycles release their channels, visible clipping suppresses deadlines, and reduced
motion draws a static symbol. Loader/Progress restart at phase zero after a
clipped/hidden/inactive lifetime. Pulse keeps time while built but clipped, without
waking the host; omission or inactive resets it. Window occlusion pauses host
redraw, not monotonic time; restoration samples the current phase directly.
Returning from reduced motion starts convenience cycles fresh. Direct animate
tracks retain the standard completed-channel rule and need explicit restart.
Highlight compares an explicit revision: first appearance establishes a baseline,
each later change triggers once. It smoothly attacks from the current accent
amount to one (hover timing), then returns to zero (highlight timing). A repeat
event restarts that attack from the displayed amount without a jump. Base colors
are supplied fresh each pass, so theme/hover changes never restore an old fill.
highlight_with overrides release timing per use. Reduced motion keeps a subtle
static accent after an event until the area is removed, with no animation deadline.
Composition
use std::time::Duration;
use zaxis::{Sequence, Parallel, Tween, Delay, Stagger, Id};
let track = Sequence::new(0.0_f32)
.then(Tween::new(0.0, 1.0, Duration::from_millis(120)))
.delay(1.0, Duration::from_millis(250))
.then(Tween::new(1.0, 0.0, Duration::from_millis(180)));
let group = Parallel::new().with(track).with(Delay::new(0.0, Duration::from_secs(1)));
let mut stagger = Stagger::new(Duration::from_millis(25))
.max_delay(Duration::from_millis(240));
stagger.insert(Id::new("task-a"), Duration::ZERO,
Tween::new(0.0_f32, 1.0, Duration::from_millis(120)));These are ordinary Animation implementations, sampled by the existing Context
Slot. Pause/resume/cancel/finish/restart and single-consumer completion events
apply to the entire composition. No additional scheduler or script language.
Sequence consumes residual elapsed time and skips completed known-duration stages
after long gaps. Exact stage boundaries return exact finished values. Zero stages
are skipped; empty Sequence finishes at its explicit initial value. A child that
finishes before an explicit bound holds its endpoint until that bound, sleeping.
Delay returns Wake::After(remaining) and never spins continuous frames.
Animation::duration() defaults to None, preserving external implementations.
Tween/Keyframes/Delay/compositions supply known lengths. An unbounded or unspecified
Sequence child blocks subsequent stages; if it reports completion while its length
is unknown, an intermediate stage holds without continuous frames. Use
then_for(duration, custom_track) to define an exact stage boundary, including
for a spring or infinite child. Explicit finish always applies the final child's
finish value. Parallel returns children in insertion order and completes after
all children. Empty Parallel completes immediately; an infinite child prevents
normal completion. Stopped children do not add wakeups to the running group.
Parallel::at(offset, track) starts a child offset after the group begins. Until
then it holds its initial value and schedules one wake-up for the offset, never
continuous frames; with(track) is at(Duration::ZERO, track). duration()
counts the offsets. Sequence::mark("name") names the current end of a sequence
(every earlier stage needs a known length, otherwise it panics) and
Sequence::time_of("name") reads it back, to place other tracks or to
seek_animation to a named moment. Read marks before moving the sequence into a
channel.
Stagger returns (Id, value) pairs. Insert with composition elapsed time; existing
IDs keep their original track/schedule. Reorder changes output order only. Delete
only on model deletion, never viewport omission; reinserting a removed ID schedules
a new child. Each insertion adds step * current child count, capped at 240 ms by
default (configurable), to its insertion time. Pause elapsed time before passing
new insertion times. Stage storage belongs to this open composition; callers can
update it before moving it into Context, or use a custom Animation wrapper for
live model changes. Ordinary Context restart resets the composition clock.
The animation gallery continuously previews all built-in tracks and effects in the unchanged grey theme. Existing hover/expand/page durations remain untouched: new presence and reorder defaults are 240/200 ms, displacement 8 px. All motion is opt-in; typing, selection, cursor input, dragging/resizing and streaming tokens remain immediate.
Dock motion
Dock uses retained spring channels keyed by stable panel IDs. Layout first computes final SplitPane rectangles; existing panels travel from their displayed rectangle to those targets. Retargeting preserves current velocity. The default critical damping prevents overshoot from rest; a reversal while moving preserves momentum instead of jumping to zero velocity. An activated tab inherits its group's motion. A focus handoff rebases the offset velocity against both panels' velocities, preserving the ring's velocity in the viewport.
Panel contents build at the final size, then translate and clip into the displayed rectangle. This avoids repeated text wrapping/layout at intermediate sizes and keeps text at its normal scale. It is not a bitmap cache: visible viewers still run once each pass and geometry/text caches supply reuse. A closing panel replays ordinary paints only, with no interaction or semantics, during a restrained 100 ms fade and small shrink. Material passes and nested deferred scroll paints are not frozen into exit replays. New regions reveal close to their destination.
One focus ring spans docked and floating panels. Its base is the displayed panel rectangle; a spring animates the remaining offset on a focus change. Radius and color also use spring channels. Stroke edges and width snap to physical pixels. The inactive color fades more slowly (2 Hz) after viewport/child focus loss. Dragging changes only the preview spring until a drop commits the layout.
Snappy (default, 6 Hz) and Smooth (4.5 Hz) use critical damping, mass 1 and
0.2 px / 0.8 px/s settle thresholds. Off and reduced motion apply target values
immediately. Settled channels keep no repaint deadline. The optional rotating
contour requests frames only while enabled, focused and motion is permitted;
turning it off, losing focus or reducing motion releases its track. Removing a
Dock retires its transient geometry, input, animation and owned floating layers.