Skip to content

Motion

Navius.Wpf.Motion is a standalone package (Navius.Wpf.Motion.csproj, targeting net8.0-windows and net10.0-windows) carrying the pure C# SpringSolver over from the web Navius Motion engine unchanged, plus a small WPF-side execution surface for driving values with it: a keyframe baker for the zero-interruption tier and a per-frame ticker for the interruptible tier. It has no dependency on Navius.Wpf.Primitives; nothing in that project references it.

  • Spring (readonly record struct): a spring definition. Construct it from raw physics parameters (Spring.Physics(stiffness, damping, mass, initialVelocity)), from a settle duration and bounce (Spring.FromDuration(durationSeconds, bounce, initialVelocity)), or from a perceptual visual duration and bounce (Spring.FromVisualDuration(visualDurationSeconds, bounce, initialVelocity)). Four named presets ship as static properties: Spring.Default (visual duration 0.3s, bounce 0.2), Spring.Smooth (0.35s, bounce 0, no overshoot), Spring.Snappy (0.2s, bounce 0.1), and Spring.Bouncy (0.35s, bounce 0.45, visible overshoot). WithInitialVelocity(velocity) returns a copy with a different initial velocity, used when carrying momentum into a retarget.
  • SpringSolver: a stateless closed-form evaluator for one spring run from an origin to a target. Construct it with new SpringSolver(spring, origin, target), then call Position(t) and Velocity(t) (seconds in, value units and value units per second out) to sample the run at any time without stepping. IsAtRest(position, velocity) reports whether a sample has settled, and SettleDuration reports the real settle time in seconds (the resolved duration for a duration-built spring, or the time it takes to step to rest in 10ms increments, capped at 20 seconds, for a physics or visual-duration spring).
  • SpringKeyframeBaker: a static class with one method, Bake(spring, from, to, initialVelocity), returning a System.Windows.Media.Animation.DoubleAnimationUsingKeyFrames. This is the zero-interruption tier: the first keyframe sits at t = 0 with the from value, the last lands exactly on to at the solver’s settle duration, and sample spacing is adaptive (as dense as roughly 4ms while the spring moves fast, as sparse as roughly 66ms once it settles into its tail). Feed the result straight into BeginAnimation.
  • SpringTicker (IDisposable): the interruptible runtime tier, driven by CompositionTarget.Rendering. new SpringTicker(spring, from, to, onValue) starts running immediately and calls onValue once synchronously at construction, then once per frame with the current position. Retarget(newTarget) carries the current position and velocity into a new solve so motion stays continuous instead of snapping back to rest first. Value, Velocity, Target, and IsAtRest read the current state; Stop() detaches the rendering hook and Dispose() calls Stop().
  • MotionPreset / MotionPresets: a data table of nine named enter/exit presets (Fade, FadeUp, FadeDown, Zoom, Pop, SlideUp, SlideDown, SlideLeft, SlideRight), each pairing a hidden MotionVisualState (opacity plus an optional transform) with an enter spring and an exit spring. MotionPresets.Get(Preset) resolves the Preset enum to its definition. Every preset fades on both directions, exits with Spring.Snappy, and enters with a preset- specific spring (Smooth for the plain fades, Default for zoom and the slides, Bouncy for pop).
  • MicroPreset / MicroPresets: a data table of four named micro-interaction presets (Shake, Pulse, Shimmer, FocusGlow), each a keyframe list plus duration, easing, loop flag, and a reduced-motion behavior (MicroReduce.Collapse or MicroReduce.OpacityOnly).

The MotionPreset/MotionPresets/MicroPreset/MicroPresets/Preset types are carried over as the same data shapes that feed the web repo’s CSS generator and WAAPI runtime, so the preset table itself stays in agreement across platforms. Nothing in this package or in Navius.Wpf.Primitives currently consumes them to drive a WPF animation; they exist as ported data, not as a wired preset-to-animation pipeline on this platform yet.

A spring is defined either physically (stiffness, damping, mass, initial velocity) or perceptually (a duration or visual duration plus a bounce amount in [0, 1], where 0 is critically damped and 1 is maximum overshoot). FromDuration solves for the undamped frequency with a Newton-Raphson root find over the amplitude envelope so the requested settle duration is honored; FromVisualDuration uses a direct formula instead (stiffness = (2 * pi / (visualDuration * 1.2))^2) for the perceived duration, whose real settle time runs longer than the perceptual one. SpringSolver reads Spring.DampingRatio (damping / (2 * sqrt(stiffness * mass))) to pick among the three closed-form regimes: underdamped (< 1, oscillates), critically damped (== 1), and overdamped (> 1).

No primitive control in Navius.Wpf.Primitives references Navius.Wpf.Motion. The Toast family is explicit about this in its own source comment: NaviusToastViewport “must not reference Navius.Wpf.Motion, so there is no spring/keyframe engine involved” and instead drives its 150ms enter/exit with a plain DoubleAnimation. The Dialog family (and the Drawer family alongside it, per the Dialog manifest’s web-deltas) is the same: both use a fixed 150ms DoubleAnimation opacity fade and deliberately do not reference this package. The only consumer in the repository today is apps/Navius.Wpf.Gallery/Pages/GatePage.xaml.cs, a gallery demo page that opens a Popup and animates it in with SpringKeyframeBaker.Bake(Spring.Default, from: 10, to: 0) on a TranslateTransform.

This is the pattern GatePage.xaml.cs uses to slide and fade a popup into place with a spring, verified against its actual source:

using Navius.Wpf.Motion;
var translate = new TranslateTransform(0, 10);
PopupRoot.RenderTransform = translate;
translate.BeginAnimation(
TranslateTransform.YProperty,
SpringKeyframeBaker.Bake(Spring.Default, from: 10, to: 0));
PopupRoot.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(150)));

For an interruptible drag or hover-retarget instead of a one-shot open animation, replace the baker call with a SpringTicker:

var ticker = new SpringTicker(Spring.Default, from: 10, to: 0, value => translate.Y = value);
// later, mid-flight, without snapping back to rest first:
ticker.Retarget(newTarget: 40);

The test project tests/Navius.Wpf.Motion.Tests carries 29 test methods across five files: SpringTests.cs, SpringSolverTests.cs, SpringTickerTests.cs, SpringKeyframeBakerTests.cs, and MicroPresetDataTests.cs. The keyframe baker and ticker tests run on the STA thread ([StaFact]), since they exercise real WPF animation and dispatcher types. MicroPresetDataTests covers only the ported preset data table itself; the web repo’s equivalent test file also covers a WAAPI runtime (Motion.Shake()/MotionPrograms.Micro) that has no WPF counterpart and was not ported.