Animated State
States that move towards a target over time, how fast and along what curve, and how to animate any type.
Animated states
An animated state moves toward its target instead of jumping to it, and everything bound to it glides along without knowing it's animated. Hover this card to see a color and a scale do that:

var color = RememberAnimated(idle, 250.ms, AnimationCurve.EaseOutCubic);
var scale = RememberAnimated(1f, 300.ms, AnimationCurve.EaseOutBack);
new Background {
ContentTransform = {
slocalScale = scale.Map(x => Vector3.one * x)
},
sColor = color,
Do = x => x.WithPointerEvents(
onEnter: _ => {
color.TargetValue = hovered;
scale.TargetValue = 1.15f;
},
onLeave: _ => {
color.TargetValue = idle;
scale.TargetValue = 1f;
}
)
}Move off the card while it's still growing, and it shrinks back from where it got to, not from full size. That's
because setting TargetValue starts the animation from the current value, so stopping one halfway always looks smooth.
While it moves, the state updates its bindings every frame like any other state, and setting the same target twice does
nothing.
A few more members help around the start and end of an animation:
SetValueImmediatejumps to a value without animating.IsFinishedtells whether the value has reached its target.OnStartruns when an animation starts from rest, andOnFinishwhen it arrives.Progressis how far the current animation has got after the curve, usually from 0 to 1.TimingandCurvecan be changed after the state is created.
An animated state needs to move every frame, so RememberAnimated attaches it to your component as a
module. Because of that, it only runs while the component is active and stops
when the component is destroyed, with no code of yours. The one thing to watch is that the state belongs to the
component that called RememberAnimated, so if that component isn't on screen, the animation never moves.
Timing and curves
Timing decides how the value moves toward its target, and there are two types of it:
A duration, written 400.ms or 1.5f.sec, reaches the target in exactly that time along the curve, and every new
target restarts the clock. That suits things that happen once, like a hover, a panel opening or a toast leaving. Driven
by something that changes all the time, like a dragged slider, it restarts on every change and never quite catches up,
which looks like lag.
A factor, written 8.fact, closes part of the remaining distance every frame, so the value is fast when far from
the target and slow when close, and the factor sets the pace at any frame rate. A new target just sends the value
somewhere else from wherever it is, so a factor suits values that change all the time, like a scroll position or
anything following a slider. Drag the slider below and compare the two:

var timed = RememberAnimated(0f, 400.ms, AnimationCurve.EaseInOutCubic);
var factored = RememberAnimated(0f, 8.fact);
target.AddCallback(x => {
timed.TargetValue = x;
factored.TargetValue = x;
});While you drag, the factor dot stays close behind the slider, and the duration dot keeps starting over and lags. That restart is also where the curve, the optional third argument, comes in, since it decides how a duration spreads its progress over time. Leave it out with a factor, because a curve would start its easing over on every new target. Every dot below gets the same target and duration, so only the curve differs:

(string, AnimationCurve)[] curves = [
("Linear", AnimationCurve.Linear),
("EaseInQuad", AnimationCurve.EaseInQuad),
("EaseOutCubic", AnimationCurve.EaseOutCubic),
("EaseInOutSine", AnimationCurve.EaseInOutSine),
("EaseOutBack", AnimationCurve.EaseOutBack)
];
foreach (var (name, curve) in curves) {
var dot = RememberAnimated(0f, 1000.ms, curve);
tracks.Children.Add(CreateTrack(
name: name,
progress: dot,
labelWidth: 22f
));
}Press Play, and the dots arrive together but get there differently. Linear keeps a steady pace, while EaseOutBack
overshoots the end and settles back. These are only a few of the curves on AnimationCurve, which also has the easing
families you may know from CSS, like Quad, Cubic, Sine and Back.
For a curve of your own, subclass AnimationCurve and override Evaluate, which turns how much of the time has passed
into how far the value has got. This one bounces off the bottom like a dropped ball:

class BounceCurve : AnimationCurve {
// Takes how much of the time has passed and returns how far the
// value has got, both from 0 to 1
public override float Evaluate(float x) {
const float n = 7.5625f;
const float d = 2.75f;
if (x < 1f / d) {
return n * x * x;
}
if (x < 2f / d) {
x -= 1.5f / d;
return n * x * x + 0.75f;
}
if (x < 2.5f / d) {
x -= 2.25f / d;
return n * x * x + 0.9375f;
}
x -= 2.625f / d;
return n * x * x + 0.984375f;
}
}
var fall = RememberAnimated(0f, 1200.ms, new BounceCurve());Each drop falls, hits the bottom and bounces a few times before it rests. The curve is the only new part, since the
state, the timing and the binding work as before, and SetValueImmediate only puts the ball back at the top first. A
curve shaped like a CSS cubic-bezier doesn't need a class at all, because BezierCurve takes its four numbers.
Interpolators
An interpolator decides how an animated state blends between two values. RememberAnimated already has one for
float, Vector2, Vector3 and Color, and for any other type you pass your own, so anything you can blend can be
animated. Your own needs two methods:
Lerpreturns the value at a given point between the start and the target. That point can go below 0 or past 1 on curves that overshoot, so use Unity's unclamped lerps unless you want to cut the overshoot off.Equalstells whether two values are the same, so the state can skip a target it already has.
The usual reason to write one is color, since the default blends in RGB and turns a muddy gray between opposite hues. Press Swap to compare it with one that goes around the hue circle instead:

class HsvInterpolator : ValueInterpolator<HsvInterpolator, Color> {
public override Color Lerp(Color from, Color to, float t) {
Color.RGBToHSV(from, out var h1, out var s1, out var v1);
Color.RGBToHSV(to, out var h2, out var s2, out var v2);
// Wrapped into half a turn either way, so the hue takes the
// short way round
var hueDelta = Wrap(h2 - h1 + 0.5f) - 0.5f;
return Color.HSVToRGB(
Wrap(h1 + hueDelta * t),
Mathf.LerpUnclamped(s1, s2, t),
Mathf.LerpUnclamped(v1, v2, t)
);
}
public override bool Equals(Color x, Color y) => x == y;
// Keeps a hue between 0 and 1
private static float Wrap(float x) => x - Mathf.Floor(x);
}
var color = RememberAnimated(Color.red, HsvInterpolator.Instance, 600.ms);Halfway through, the RGB swatch turns gray, while the HSV one stays bright. The hue difference is wrapped into half a
turn either way, so the hue always takes the shorter way around. Deriving from ValueInterpolator gives you a shared
Instance, so only Lerp and Equals are left to write. The same approach works for a Quaternion, a YogaVector
for an animated size, or a struct of your own.