-
Notifications
You must be signed in to change notification settings - Fork 0
UI Animation and Transitions
Cursorial.UI's animation layer drives property values over time — fading an Opacity, sliding a render
offset, pulsing a color. It sits on top of the pure Animation engine (IAnimation<T>:
elapsed → value) and adds the orchestration WPF/Avalonia authors expect: a frame-aligned scheduler,
imperative BeginAnimation, reusable storyboards you ignite from styles, and implicit transitions that
fade a property whenever its base value changes. Reach for it whenever a value should move to its new
state instead of snapping.
If you've used WPF, the vocabulary maps cleanly: BeginAnimation/Storyboard/AnimationTrack (≈ WPF's
Timeline), BeginStoryboard/StopStoryboard triggers, and Avalonia-style implicit Transitions.
You don't tick anything yourself. UIApplication's frame loop owns a thread-ambient AnimationScheduler
and a FrameClock that is frozen once per frame — every animation sampled in a frame sees the exact
same timestamp, so a storyboard's parallel tracks stay in lockstep. An idle UI (no running animations,
no pending input) parks and burns no CPU; the scheduler keeps the loop awake exactly while animations are
live.
That means the animation API is only meaningful inside a running UIApplication. Begin/stop are UI-thread
operations (they assert thread affinity).
The simplest entry point is the BeginAnimation extension on any UIObject. You hand it a styled
property and an IAnimation<T> (the two-point DoubleAnimation/ColorAnimation/Int32Animation, a
keyframe animation, or your own); it returns an AnimationHandle.
using Cursorial.Animation; // DoubleAnimation, Easings
using Cursorial.UI; // BeginAnimation extension, AnimationStartOptions
// Fade a panel from invisible to fully opaque over 250 ms.
AnimationHandle handle = panel.BeginAnimation(
UIElement.OpacityProperty,
new DoubleAnimation(from: 0.0, to: 1.0, duration: TimeSpan.FromMilliseconds(250), Easings.CubicOut));The animation writes at Animation priority — above local values, styles, and bindings — so it visibly
wins for as long as it runs. Beginning a new animation on the same (target, property) retires the
running one first (no fight). AnimationStartOptions tunes the run:
-
BeginTime— a stagger before the animation starts touching the property. -
Fill—FillBehavior.HoldEnd(default; the end value holds at Animation priority, costing nothing per frame) orFillBehavior.Stop(retract at the end so the underlying base value resurfaces). -
Handoff— how a re-begin combines with a still-running run (SnapshotAndReplaceis the only policy).
panel.BeginAnimation(UIElement.OpacityProperty,
new DoubleAnimation(1.0, 0.0, TimeSpan.FromMilliseconds(180)),
new AnimationStartOptions(BeginTime: TimeSpan.FromMilliseconds(50), Fill: FillBehavior.Stop));AnimationHandle is your control surface for the live run:
-
Stop()— retract immediately; the base value resurfaces. (NoCompleted.) -
Pause()/Resume()— hold the value; resume from where it paused, with no time jump. -
Seek(TimeSpan offset)— jump to a point on the animation's own timeline (works while paused). -
SkipToEnd()— snap to the end value and complete (throws for a perpetual animation). -
Completed— an event raised at most once, on the UI thread, when a finite animation reaches its end naturally. It does not fire onStop(), on detach, or on shutdown. -
State— the lifecycle (Delayed→Running→Holding/Completed/Stopped, orPaused).
To stop without a handle, target.StopAnimation(property) retracts whatever is running on that property.
A Storyboard is a reusable, declarative bundle of AnimationTracks that play in parallel against a
common frozen clock. Each track targets one property; the storyboard is a description you can share —
beginning it creates a fresh per-igniter instance, so two elements (or two rules) using the same storyboard
resource never interfere.
var slideIn = new Storyboard();
// Slide a render offset in, easing to rest.
slideIn.Children.Add(new DoubleTrack
{
TargetProperty = UIElement.RenderOffsetColumnProperty,
To = 0.0, // From unset ⇒ snapshots the current value at start
Duration = TimeSpan.FromMilliseconds(500),
Easing = Easings.CubicOut,
});
// ...and fade it in, as part of the SAME group.
slideIn.Children.Add(new DoubleTrack
{
TargetProperty = UIElement.OpacityProperty,
From = 0.0,
To = 1.0,
Duration = TimeSpan.FromMilliseconds(400),
});
StoryboardHandle h = slideIn.Begin(card); // begin on a scope elementTrack types are sealed leaves per value type: DoubleTrack, Int32Track, ColorTrack, BrushTrack,
RectTrack, SizeTrack, MarginsTrack. Each is an AnimationTrack<T> and exposes:
-
TargetProperty— required; aStyledProperty<T>whose type matches the track'sT. -
TargetName— optional; resolves a named descendant in the begin scope (template parts included). Null means the scope element itself. -
From/To— the two-point form.FromisOptional<T>: leave it unset to snapshot the property's current value when the track starts (so a hover-in/hover-out pair picks up wherever the value is now).Tois required for the two-point form. -
DurationandEasing(null ⇒ linear). -
BeginTime— a per-track stagger inside the storyboard. -
Repeat(aRepeatBehavior—Once,RepeatBehavior.Count(n), orRepeatBehavior.Forever) andAutoReverse(play forward then backward each iteration). -
Fill—HoldEnd(default) orStop. - Escape hatches:
Keyframes(anIList<Keyframe<T>>) or a code-builtSourceanimation, each mutually exclusive with the two-point form.
A storyboard seals on first Begin (or when a referencing Style seals) — validation runs then, and
mutating Children afterwards throws. StoryboardHandle mirrors AnimationHandle:
Stop/Pause/Resume/Seek/SkipToEnd/Completed/IsCompleted. Storyboard.Stop(scope) stops the
imperatively-keyed instance.
Styles carry edge actions that fire when a rule activates (Style.Enter) or deactivates (Style.Exit)
— e.g. when a :pointerover rule starts or stops matching. BeginStoryboard and StopStoryboard are the
two animation edge actions. They are do/undo: a BeginStoryboard in Enter begins; the same action
placed in Exit (or a matching StopStoryboard) stops.
var pulse = new Storyboard();
pulse.Children.Add(new DoubleTrack
{
TargetProperty = UIElement.OpacityProperty,
From = 1.0,
To = 0.35,
Duration = TimeSpan.FromMilliseconds(550),
Easing = Easings.SineInOut,
Repeat = RepeatBehavior.Forever,
AutoReverse = true,
});
var hover = new Style(Selectors.Class("alert")); // a :pointerover rule, say
hover.Enter.Add(new BeginStoryboard { Storyboard = pulse }); // hover-in ⇒ begin the perpetual pulse
hover.Exit.Add(new StopStoryboard { Storyboard = pulse }); // hover-out ⇒ stop it; base Opacity resurfacesEdge-ignited begins are no-throw: an unresolvable target/property routes to AnimationDiagnostics and
the failing track is skipped, so siblings still play. (Imperative Storyboard.Begin throws instead — you
asked for it directly.) Each (action, element) pair gets its own instance, so the same storyboard
resource shared across many cards never tangles.
A Transition fades a property automatically whenever its base value changes — a style flip, a local
set, a binding update. You don't begin it; you attach it once and it watches. This is Avalonia's implicit-
animation model: declare "animate Opacity over 220 ms" and any change to Opacity fades instead of snapping.
Transition.SetTransitions(card, new TransitionCollection
{
new DoubleTransition(UIElement.OpacityProperty)
{
Duration = TimeSpan.FromMilliseconds(220),
Easing = Easings.CubicInOut,
},
});Now a rule like .card:pointerover { Opacity: 1.0 } over a resting Opacity: 0.6 produces a smooth fade
in on hover-enter and back out on leave — the transition turns each base change into a short
Animation-priority run from the old value to the new (and from the live interpolated value if a fade is
already in flight, so rapid hover in/out reverses cleanly rather than jumping).
Transition leaves match the track types: DoubleTransition, Int32Transition, ColorTransition,
BrushTransition, MarginsTransition. Each takes the StyledProperty<T> it animates and exposes
Duration (default 150 ms), Delay, and Easing (default linear). Attach them as a
TransitionCollection through the attached Transition.TransitionsProperty (SetTransitions /
GetTransitions) — it's style-settable, so a theme can declare hover fades centrally. The collection
seals when armed.
One rule worth knowing: the initial application of a value doesn't transition. A transition fades changes, not the element's first appearance — a freshly-shown control snaps to its resting look and fades only on subsequent changes.
AnimationScheduler.AnimationsEnabled is the global motion switch (default true). Set it to false and:
- new finite animations snap straight to their end value;
- new perpetual animations are born stopped;
- a live
true → falseflip collapses everything in flight at the next frame; transitions stop igniting.
AnimationScheduler.Current.AnimationsEnabled = false; // honor a "reduce motion" preferenceBecause transitions consult this flag before each ignite, flipping it off makes every implicit fade an instant swap with no further code.
-
Animation — the pure, time-free engine underneath:
IAnimation<T>, easings, keyframes, and the interpolator registry. -
Styling & themes — selectors, pseudo-classes, and the
Style.Enter/Exitedges that ignite storyboards. -
Input & focus — where
:pointerover/:focusstate changes originate. -
Controls — controls like the indeterminate
ProgressBarwhose looks ride these primitives.
Cursorial.Core
Cursorial.Rendering
Drawing & Animation
Cursorial.UI
- Overview
- Layout & panels
- Controls
- Styling & themes
- Data binding
- Input & focus
- Windowing
- Animation & transitions
Declarative