When standard WPF easing functions are insufficient, you can use SpringSimulation to drive a per-frame spring animation.
To implement this:
- Obtain a
SpringMotionSpec from MotionSchemeContext to ensure your stiffness and damping ratios align with Material 3 guidelines. - Initialize a
SpringSimulation with the desired Stiffness and DampingRatio. - Use
SpringSimulation.UpdateValues within a rendering loop (such as CompositionTarget.Rendering) to calculate the next position and velocity based on elapsed time. - Apply the resulting value to your target property.
- Stop the animation when the value and velocity have converged (e.g., when the difference from the final position and the velocity are both below a small threshold).
// Example: Spring-driven translation on TranslateTransform.X
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Media;
using MaterialDesignThemes.Motion;
public partial class SpringSampleControl : UserControl, IDisposable
{
private readonly SpringAnimator _animator;
public SpringSampleControl()
{
InitializeComponent();
// 1. Get the Material 3 spring spec
var spec = MotionSchemeContext.Current.RememberDefaultSpatialSpec();
// 2. Initialize the animator with the spec
_animator = new SpringAnimator(
apply: value => Translate.X = value,
springSpec: spec);
Loaded += (_, _) => _animator.Start(from: -120, to: 0);
Unloaded += (_, _) => Dispose();
}
public void Dispose() => _animator.Dispose();
public TranslateTransform Translate { get; } = new();
private sealed class SpringAnimator : IDisposable
{
private readonly SpringSimulation _simulation;
private readonly Action<double> _apply;
private readonly Stopwatch _stopwatch = new();
private double _value;
private double _velocity;
private bool _isRunning;
public SpringAnimator(Action<double> apply, SpringMotionSpec springSpec)
{
_apply = apply;
_simulation = new SpringSimulation(finalPosition: 0f)
{
Stiffness = (float)springSpec.Stiffness,
DampingRatio = (float)springSpec.DampingRatio,
};
}
public void Start(double from, double to)
{
_simulation.FinalPosition = (float)to;
_value = from;
_velocity = 0;
_stopwatch.Restart();
if (_isRunning) return;
CompositionTarget.Rendering += OnRendering;
_isRunning = true;
}
private void OnRendering(object? sender, EventArgs e)
{
var elapsed = _stopwatch.Elapsed;
_stopwatch.Restart();
// 3. Update values per frame
var next = _simulation.UpdateValues((float)_value, (float)_velocity, elapsed);
_value = next.Value;
_velocity = next.Velocity;
_apply(_value);
// 4. Check for convergence to stop
if (Math.Abs(_value - _simulation.FinalPosition) < 0.5 &&
Math.Abs(_velocity) < 0.5)
{
Stop();
}
}
private void Stop()
{
if (!_isRunning) return;
CompositionTarget.Rendering -= OnRendering;
_isRunning = false;
}
public void Dispose() => Stop();
}
}