Reactive, composable animation primitives for WPF and Avalonia. Extends ReactiveUI for Schedulers and Rx conventions.
AnimationRx exposes animations as IObservable<Unit> (for effects that complete) and value streams like IObservable<double> (for interpolated values). This makes animations easy to compose, sequence, run in parallel, cancel (dispose), and bind to reactive view-models.
- AnimationRx.Wpf: targets .NET Framework
4.6.2,4.7.2,4.8and.NET 8/9/10 (windows). - AnimationRx.Avalonia: targets
.NET 8/9/10.
Cancellation model: every animation is an
IObservable. Dispose the subscription returned bySubscribe()to cancel mid-flight.
NuGet:
- Package:
AnimationRx.Wpf
Package Manager:
Install-Package AnimationRx.Wpf
NuGet:
- Package:
AnimationRx.Avalonia
Package Manager:
Install-Package AnimationRx.Avalonia
using CP.AnimationRx;
using System.Reactive.Disposables;
// Fade in over 300ms using Sine ease
var d = someElement
.OpacityTo(300, 1.0, Ease.SineInOut)
.Subscribe();
// later => cancel if needed
// d.Dispose();using CP.AnimationRx;
// Fade out over 250ms
someVisual
.OpacityTo(250, 0.0, Ease.SineOut)
.Subscribe();Animations are driven by a Duration value stream, where:
Duration.Percentis always in[0..1]DurationPercentage(...)produces the progress streamEaseAnimation(...)reshapes the progress curveDistance(...)maps progress into a delta (e.g., pixels, degrees)
Easing is provided by:
Easeenum (selects common easings)Easesextension methods (e.g.,BackIn,SineOut) onIObservable<Duration>
Both libraries separate:
- time source (defaults to
RxSchedulers.TaskpoolScheduler) - UI updates (defaults to
RxSchedulers.MainThreadScheduler)
All built-in UI animations marshal reads/writes to the UI thread.
In the sections below:
rx<T>meansIObservable<T>- methods returning
rx<Unit>represent animations that complete
Namespace: CP.AnimationRx
Add these usings:
using CP.AnimationRx;
using System.Reactive;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;Emits an increasing tick at approximately the requested FPS.
var frames = Animations.AnimateFrame(60)
.Subscribe(i => Debug.WriteLine($"tick {i}"));Emits ticks on a fixed period.
var frames = Animations.AnimateFrame(TimeSpan.FromMilliseconds(33))
.Subscribe(i => Debug.WriteLine(i));Emits one tick per WPF CompositionTarget.Rendering (synchronized to the UI rendering loop).
var renderLoop = Animations.RenderFrames()
.Subscribe(_ =>
{
// do per-render work here
});Emits elapsed milliseconds since subscription (starting with 0.0).
var ms = Animations.MilliSecondsElapsed(RxSchedulers.TaskpoolScheduler)
.Subscribe(t => Debug.WriteLine($"elapsed {t:0}ms"));Produces progress from 0..1 over the duration, always emitting a final 1.0 and then completing.
Animations.DurationPercentage(500)
.Subscribe(d => Debug.WriteLine(d.Percent));Animations.DurationPercentage(rx<double> milliSeconds, IScheduler? scheduler = null) -> rx<Duration>
Same as above, but the duration can change dynamically.
var durationMs = Observable.Return(750.0);
Animations.DurationPercentage(durationMs)
.Subscribe(d => Debug.WriteLine(d.Percent));Maps raw [0..1] values to Duration.
Observable.Return(0.25)
.Concat(Observable.Return(0.5))
.Concat(Observable.Return(1.0))
.ToDuration()
.Subscribe(d => Debug.WriteLine(d.Percent));Back-pressure helper that delays each element by interval and concatenates (one “in flight” at a time).
var throttled = Observable.Range(1, 5)
.TakeOneEvery(TimeSpan.FromMilliseconds(200));
throttled.Subscribe(Console.WriteLine);Use Ease.None for linear or pick from:
BackIn, BackOut, BackInOut, BounceIn, BounceOut, BounceInOut, CircIn, CircOut, CircInOut, CubicIn, CubicOut, CubicInOut, ElasticIn, ElasticOut, ElasticInOut, ExpoIn, ExpoOut, ExpoInOut, QuadIn, QuadOut, QuadInOut, QuarticIn, QuarticOut, QuarticInOut, QuinticIn, QuinticOut, QuinticInOut, SineIn, SineOut, SineInOut.
Applies the selected ease to a duration stream.
Animations.DurationPercentage(400)
.EaseAnimation(Ease.ExpoOut)
.Subscribe(d => Debug.WriteLine(d.Percent));Each method below reshapes Duration.Percent:
BackIn(),BackOut(),BackInOut()BounceIn(),BounceOut(),BounceInOut()CircIn(),CircOut(),CircInOut()CubicIn(),CubicOut(),CubicInOut()ElasticIn(),ElasticOut(),ElasticInOut()ExpoIn(),ExpoOut(),ExpoInOut()QuadIn(),QuadOut(),QuadInOut()QuarticIn(),QuarticOut(),QuarticInOut()QuinticIn(),QuinticOut(),QuinticInOut()SineIn(),SineOut(),SineInOut()
Example of using a specific easing method directly:
Animations.DurationPercentage(300)
.SineInOut()
.Subscribe(d => Debug.WriteLine(d.Percent));Animations.AnimateValue(double ms, double from, double to, Ease ease = Ease.None, IScheduler? scheduler = null) -> rx<double>
Creates a numeric interpolation stream from from to to.
Animations.AnimateValue(600, from: 0, to: 100, Ease.SineInOut)
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(v => viewModel.Progress = v);Converts progress to progress.Percent * distance.
Animations.DurationPercentage(300)
.Distance(200) // 0..200
.Subscribe(px => Debug.WriteLine(px));Same as above, but distance is dynamic.
var distance = Observable.Return(150.0);
Animations.DurationPercentage(300)
.Distance(distance)
.Subscribe(px => Debug.WriteLine(px));Converts milliseconds to pixels at a constant velocity (px/s): velocity * ms / 1000.
Animations.MilliSecondsElapsed(RxSchedulers.TaskpoolScheduler)
.PixelsPerSecond(velocity: 200)
.Subscribe(px => Debug.WriteLine($"{px:0.0}px"));All methods below return rx<Unit>.
OpacityTo(this UIElement element, double ms, double to, Ease ease = Ease.None, IScheduler? scheduler = null)
Animates UIElement.Opacity to to.
myPanel
.OpacityTo(250, 0.0, Ease.SineOut)
.Concat(myPanel.OpacityTo(250, 1.0, Ease.SineIn))
.Subscribe();WidthTo(this FrameworkElement element, double ms, double to, Ease ease = Ease.None, IScheduler? scheduler = null)
Animates Width (falls back to ActualWidth if Width is NaN).
myControl.WidthTo(400, 320, Ease.ExpoOut).Subscribe();HeightTo(this FrameworkElement element, double ms, double to, Ease ease = Ease.None, IScheduler? scheduler = null)
Animates Height (falls back to ActualHeight if Height is NaN).
myControl.HeightTo(400, 80, Ease.ExpoOut).Subscribe();MarginTo(this FrameworkElement element, double ms, Thickness to, Ease ease = Ease.None, IScheduler? scheduler = null)
Animates Margin to to.
myControl.MarginTo(300, new Thickness(10), Ease.SineInOut).Subscribe();PaddingTo(this Control element, double ms, Thickness to, Ease ease = Ease.None, IScheduler? scheduler = null)
Animates Padding to to.
myButton.PaddingTo(300, new Thickness(24, 8, 24, 8), Ease.SineOut).Subscribe();CanvasLeftTo(this FrameworkElement element, double ms, double to, Ease ease = Ease.None, IScheduler? scheduler = null)
Animates the Canvas.Left attached property.
myItem.CanvasLeftTo(350, 120, Ease.ExpoOut).Subscribe();CanvasTopTo(this FrameworkElement element, double ms, double to, Ease ease = Ease.None, IScheduler? scheduler = null)
Animates the Canvas.Top attached property.
myItem.CanvasTopTo(350, 40, Ease.ExpoOut).Subscribe();BrushColorTo(this SolidColorBrush brush, double ms, Color to, Ease ease = Ease.None, IScheduler? scheduler = null)
Animates a SolidColorBrush.Color.
If the brush might be frozen, clone it first and assign a non-frozen brush instance to your element.
var brush = new SolidColorBrush(Colors.OrangeRed);
myBorder.Background = brush;
brush.BrushColorTo(500, Colors.SteelBlue, Ease.SineInOut).Subscribe();These helpers animate individual Thickness components per-frame.
LeftMarginMove(this FrameworkElement element, double ms, double position, Ease ease = Ease.None, IScheduler? scheduler = null)
Moves Margin.Left to an absolute position.
myControl.LeftMarginMove(400, 100, Ease.ExpoOut).Subscribe();RightMarginMove(this FrameworkElement element, rx<double> ms, rx<double> position, Ease ease = Ease.None, IScheduler? scheduler = null)
Dynamic duration + dynamic position.
var ms = Observable.Return(250.0);
var pos = Observable.Return(30.0);
myControl.RightMarginMove(ms, pos, Ease.SineInOut).Subscribe();Moves Margin.Top to position.
myControl.TopMarginMove(300, 12, Ease.SineOut).Subscribe();BottomMarginMove(this FrameworkElement element, double ms, double position, Ease ease = Ease.None, IScheduler? scheduler = null)
Moves Margin.Bottom to position.
myControl.BottomMarginMove(300, 12, Ease.SineOut).Subscribe();Observable overloads also exist for
LeftMarginMove,RightMarginMove,TopMarginMove,BottomMarginMovewherems,position, and/oreasecan beIObservable.
Transform helpers will add the required transform into RenderTransform using a TransformGroup if needed.
TranslateTransform(this FrameworkElement element, rx<double> ms, rx<Point> position, Ease xease = Ease.None, Ease yease = Ease.None)
Animates translate using dynamic duration and a dynamic target point.
var ms = Observable.Return(400.0);
var pos = Observable.Return(new Point(120, 40));
myControl.TranslateTransform(ms, pos, Ease.ExpoOut, Ease.SineIn).Subscribe();TranslateTransform(this FrameworkElement element, double ms, double xPosition, double yPosition, Ease xease = Ease.None, Ease yease = Ease.None, IScheduler? scheduler = null)
Animates translate to (xPosition,yPosition).
myControl.TranslateTransform(400, 120, 40, Ease.ExpoOut, Ease.SineIn).Subscribe();RotateTransform(this FrameworkElement element, double ms, double angle, Ease ease = Ease.None, IScheduler? scheduler = null)
Animates rotation by delta (angle - initialAngle internally).
myControl.RotateTransform(300, 45, Ease.SineOut).Subscribe();ScaleTransform(this FrameworkElement element, double ms, double scaleX, double scaleY, Ease easeX = Ease.None, Ease easeY = Ease.None, IScheduler? scheduler = null)
Animates scale by delta.
myControl.ScaleTransform(250, 1.2, 1.2, Ease.SineInOut, Ease.SineInOut).Subscribe();SkewTransform(this FrameworkElement element, double ms, double angleX, double angleY, Ease easeX = Ease.None, Ease easeY = Ease.None, IScheduler? scheduler = null)
Animates skew by delta.
myControl.SkewTransform(250, 10, 0, Ease.SineOut, Ease.SineOut).Subscribe();These are convenience helpers that animate to absolute targets:
TranslateTo(this FrameworkElement element, double ms, double toX, double toY, ...)ScaleTo(this FrameworkElement element, double ms, double toScaleX, double toScaleY, ...)RotateTo(this FrameworkElement element, double ms, double toAngle, ...)SkewTo(this FrameworkElement element, double ms, double toAngleX, double toAngleY, ...)
TranslateTo example:
myControl.TranslateTo(300, toX: 0, toY: 0, Ease.ExpoOut, Ease.ExpoOut).Subscribe();ShakeTranslate(this FrameworkElement element, double ms, double amplitude, int shakes = 6, Ease ease = Ease.None, IScheduler? scheduler = null)
Shakes horizontally using TranslateTransform and returns to the original position.
myControl.ShakeTranslate(600, amplitude: 12, shakes: 8, ease: Ease.SineOut).Subscribe();PulseOpacity(this UIElement element, double milliSecondsPerHalf, double low = 0.2, double high = 1.0, int pulses = 1, Ease ease = Ease.None, IScheduler? scheduler = null)
Pulses opacity between low and high for pulses cycles.
myControl.PulseOpacity(150, low: 0.3, high: 1.0, pulses: 3, ease: Ease.SineInOut).Subscribe();Concatenates animations in-order.
var anim = new[]
{
myControl.OpacityTo(150, 0.0),
myControl.OpacityTo(150, 1.0),
myControl.ScaleTransform(200, 1.1, 1.1, Ease.SineOut, Ease.SineOut),
myControl.ScaleTransform(200, 1.0, 1.0, Ease.SineIn, Ease.SineIn)
}.Sequence();
anim.Subscribe();Merges animations and completes when the last one finishes.
new[]
{
myControl.OpacityTo(250, 1.0, Ease.SineOut),
myControl.TranslateTransform(250, 120, 0, Ease.ExpoOut, Ease.ExpoOut)
}.Parallel().Subscribe();Repeats an animation. If count is null, repeats forever until disposed.
var pulseOnce = myControl.PulseOpacity(120, pulses: 1);
pulseOnce.RepeatAnimation(count: 5).Subscribe();
// pulseOnce.RepeatAnimation().Subscribe(); // infinite until disposedStagger(this IEnumerable<rx<Unit>> animations, TimeSpan staggerBy, IScheduler? scheduler = null) -> IEnumerable<rx<Unit>>
Delays each animation by an incremental amount.
var items = listBox.Items.Cast<FrameworkElement>();
var anims = items.Select(el => el.TranslateTransform(300, -40, 0, Ease.SineOut, Ease.SineOut)
.Concat(el.TranslateTransform(250, 0, 0, Ease.ExpoOut, Ease.ExpoOut)));
anims.Stagger(TimeSpan.FromMilliseconds(80))
.Parallel()
.Subscribe();Namespace: CP.AnimationRx
Add these usings:
using CP.AnimationRx;
using System.Reactive;
using System.Reactive.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;Frames on an interval (default uses taskpool scheduler).
Animations.AnimateFrame(60)
.Subscribe(i => Debug.WriteLine(i));Fixed-period ticker.
Animations.AnimateFrame(TimeSpan.FromMilliseconds(16))
.Subscribe(_ => { /* timer */ });Elapsed milliseconds since subscription.
Animations.MilliSecondsElapsed(RxSchedulers.TaskpoolScheduler)
.Subscribe(ms => Debug.WriteLine(ms));Progress [0..1] over a fixed duration.
Animations.DurationPercentage(500)
.EaseAnimation(Ease.SineInOut)
.Subscribe(d => Debug.WriteLine(d.Percent));Progress [0..1] over a dynamic duration.
var durationMs = Observable.Return(350.0);
Animations.DurationPercentage(durationMs)
.Subscribe(d => Debug.WriteLine(d.Percent));Delays each element by interval and sequences them.
Observable.Range(1, 3)
.TakeOneEvery(TimeSpan.FromMilliseconds(250))
.Subscribe(Console.WriteLine);Applies a selected ease.
Animations.DurationPercentage(300)
.EaseAnimation(Ease.ExpoOut)
.Subscribe(d => Debug.WriteLine(d.Percent));Maps raw [0..1] values to Duration.
Observable.Return(0.0)
.Concat(Observable.Return(1.0))
.ToDuration()
.Subscribe(d => Debug.WriteLine(d.Percent));Same list as WPF (BackIn, BounceOut, SineInOut, etc.).
Animations.DurationPercentage(300)
.BounceOut()
.Subscribe(d => Debug.WriteLine(d.Percent));Animations.AnimateValue(double ms, double from, double to, Ease ease = Ease.None, IScheduler? scheduler = null) -> rx<double>
Interpolates numeric values.
Animations.AnimateValue(400, 0, 100, Ease.SineInOut)
.Subscribe(v => Debug.WriteLine(v));Progress to delta.
Animations.DurationPercentage(200)
.Distance(50)
.Subscribe(v => Debug.WriteLine(v));Dynamic distance.
var distance = Observable.Return(180.0);
Animations.DurationPercentage(200)
.Distance(distance)
.Subscribe(v => Debug.WriteLine(v));All methods below return rx<Unit>.
OpacityTo(this Visual element, double ms, double to, Ease ease = Ease.None, IScheduler? scheduler = null)
Animates Visual.Opacity.
myVisual.OpacityTo(250, 0.0, Ease.SineOut).Subscribe();WidthTo(this Control element, double ms, double to, Ease ease = Ease.None, IScheduler? scheduler = null)
Animates Control.Width (falls back to Bounds.Width if Width is NaN).
myControl.WidthTo(300, 240, Ease.ExpoOut).Subscribe();HeightTo(this Control element, double ms, double to, Ease ease = Ease.None, IScheduler? scheduler = null)
Animates Control.Height.
myControl.HeightTo(300, 90, Ease.ExpoOut).Subscribe();MarginTo(this Control element, double ms, Thickness to, Ease ease = Ease.None, IScheduler? scheduler = null)
Animates Control.Margin.
myControl.MarginTo(300, new Thickness(12), Ease.SineInOut).Subscribe();PaddingTo(this TemplatedControl element, double ms, Thickness to, Ease ease = Ease.None, IScheduler? scheduler = null)
Animates TemplatedControl.Padding.
myTemplatedControl.PaddingTo(200, new Thickness(16, 8), Ease.SineOut).Subscribe();CanvasLeftTo(this Control element, double ms, double to, Ease ease = Ease.None, IScheduler? scheduler = null)
Animates the Canvas.Left attached property.
myControl.CanvasLeftTo(250, 120, Ease.ExpoOut).Subscribe();CanvasTopTo(this Control element, double ms, double to, Ease ease = Ease.None, IScheduler? scheduler = null)
Animates the Canvas.Top attached property.
myControl.CanvasTopTo(250, 40, Ease.ExpoOut).Subscribe();BrushColorTo(this SolidColorBrush brush, double ms, Color to, Ease ease = Ease.None, IScheduler? scheduler = null)
Animates a SolidColorBrush.Color.
var brush = new SolidColorBrush(Colors.OrangeRed);
myBorder.Background = brush;
brush.BrushColorTo(500, Colors.SteelBlue, Ease.SineInOut).Subscribe();ColorTo(this SolidColorBrush brush, double ms, Color to, Ease ease = Ease.None, IScheduler? scheduler = null)
Alias for BrushColorTo.
brush.ColorTo(500, Colors.LimeGreen, Ease.ExpoOut).Subscribe();Transform helpers will add the required transform to Visual.RenderTransform using a TransformGroup if needed.
TranslateTransform(this Visual element, double ms, double xPosition, double yPosition, Ease xease = Ease.None, Ease yease = Ease.None, IScheduler? scheduler = null)
Animates translation.
myVisual.TranslateTransform(400, 120, 40, Ease.ExpoOut, Ease.SineIn).Subscribe();Alias for TranslateTransform.
myVisual.TranslateTo(300, 0, 0, Ease.ExpoOut, Ease.ExpoOut).Subscribe();RotateTransform(this Visual element, double ms, double angle, Ease ease = Ease.None, IScheduler? scheduler = null)
Animates rotation to the target angle.
myVisual.RotateTransform(300, 45, Ease.SineOut).Subscribe();Alias for RotateTransform.
myVisual.RotateTo(300, 0, Ease.ExpoOut).Subscribe();ScaleTransform(this Visual element, double ms, double scaleX, double scaleY, Ease easeX = Ease.None, Ease easeY = Ease.None, IScheduler? scheduler = null)
Animates scale.
myVisual.ScaleTransform(250, 1.2, 1.2, Ease.SineInOut, Ease.SineInOut).Subscribe();Alias for ScaleTransform.
myVisual.ScaleTo(250, 1.0, 1.0, Ease.SineOut, Ease.SineOut).Subscribe();SkewTransform(this Visual element, double ms, double angleX, double angleY, Ease easeX = Ease.None, Ease easeY = Ease.None, IScheduler? scheduler = null)
Animates skew.
myVisual.SkewTransform(250, 10, 0, Ease.SineOut, Ease.SineOut).Subscribe();Alias for SkewTransform.
myVisual.SkewTo(250, 0, 0, Ease.ExpoOut, Ease.ExpoOut).Subscribe();ShakeTranslate(this Visual element, double ms, double amplitude, int shakes = 6, Ease ease = Ease.None, IScheduler? scheduler = null)
Shakes horizontally and returns to the original position.
myVisual.ShakeTranslate(600, amplitude: 12, shakes: 8, ease: Ease.SineOut).Subscribe();PulseOpacity(this Visual element, double milliSecondsPerHalf, double low = 0.2, double high = 1.0, int pulses = 1, Ease ease = Ease.None, IScheduler? scheduler = null)
Pulses opacity between low and high for pulses cycles.
myVisual.PulseOpacity(150, low: 0.3, high: 1.0, pulses: 3, ease: Ease.SineInOut).Subscribe();Sequences animations.
new[]
{
myVisual.OpacityTo(150, 0.0),
myVisual.OpacityTo(150, 1.0),
}.Sequence().Subscribe();Runs animations in parallel.
new[]
{
myVisual.OpacityTo(250, 1.0),
myVisual.TranslateTransform(250, 120, 0)
}.Parallel().Subscribe();Repeats an animation.
var pulse = myVisual.PulseOpacity(120, pulses: 1);
pulse.RepeatAnimation(5).Subscribe();DelayBetween(this IEnumerable<rx<Unit>> animations, TimeSpan delay, IScheduler? scheduler = null) -> rx<Unit>
Inserts a delay between each animation in a sequence.
new[]
{
myVisual.OpacityTo(100, 0.0),
myVisual.OpacityTo(100, 1.0),
}
.DelayBetween(TimeSpan.FromMilliseconds(200))
.Subscribe();Stagger(this IEnumerable<rx<Unit>> animations, TimeSpan staggerBy, IScheduler? scheduler = null) -> IEnumerable<rx<Unit>>
Delays each animation by an incremental stagger.
var anims = items.Select(v => v.OpacityTo(250, 1.0, Ease.SineOut));
anims.Stagger(TimeSpan.FromMilliseconds(60))
.Parallel()
.Subscribe();- Prefer composing animations with Rx (
Concat,Merge,Sequence,Parallel) rather than timers. - Always dispose subscriptions for:
- cancellation
- preventing leaks if element lifetime is shorter than the animation
- For smooth per-frame loops you can build a
dtstream fromAnimateFrame(...)(orRenderFrames()in WPF).
MIT License. © Chris Pulman
AnimationRx - Empowering Automation with Reactive Technology ⚡🏭