Interpolators
Interpolators are functions that compute intermediate values between two endpoints. They are the tweening engine behind Ripl's animation system: when you transition an element's radius from 50 to 100, an interpolator generates all the in-between values.
What can be tweened is not limited to numbers. Ripl interpolates dates, colors written as hex, rgb(), hsl(), hsv() or a CSS keyword, gradient strings, pattern paints, rotations in degrees or radians, border radii, dash patterns, and point arrays — the last of which morphs one polyline or polygon outline into another, optionally matching points by key so a curved renderer survives an add or remove. Every built-in element declares which interpolators its own state properties use; anything undeclared is chosen from the value's type, and either can be overridden per property.
NOTE
For the full API, see the Core API Reference.
Built-in Interpolators
Ripl ships with interpolators for common value types. A property an element declares an interpolator for uses it directly. Anything else is detected from the value, testing in this order and taking the first whose test function returns true: interpolateNumber, interpolateGradient, interpolatePattern (matching-type pattern(...) paints), interpolateColor (hex, rgb, rgba, hsl, hsv and CSS keywords), interpolateDate, interpolatePoints (arrays of [x, y] tuples), and interpolateNumbers (arrays of numbers, of any length). interpolateAny is the fallback, snapping at t > 0.5.
A further set is never detected and is reached for by declaration or by hand: interpolateRotation and interpolateTransformOrigin back the rotation and transformOrigin* properties, interpolateBorderRadius backs Rect's borderRadius (which may be a single radius or a four-corner tuple), interpolateImage cross-fades an Image element's source, and interpolateString, interpolatePath, interpolateWaypoint, interpolatePolygonPoint and interpolateCirclePoint are called directly.
How Interpolators Work
An interpolator factory takes two values (start and end) and returns a function that accepts a time value t (0 to 1) and returns the interpolated result:
import {
interpolateNumber,
} from '@ripl/web';
const interpolate = interpolateNumber(0, 100);
interpolate(0); // 0
interpolate(0.5); // 50
interpolate(1); // 100Number Interpolation
The simplest interpolator performs linear interpolation between two numbers:
const interpolate = interpolateNumber(10, 50);
interpolate(0.25); // 20
interpolate(0.75); // 40Color Interpolation
Interpolates between CSS color strings by parsing them to RGBA, interpolating each channel, and serializing back:
import {
interpolateColor,
} from '@ripl/web';
const interpolate = interpolateColor('#3a86ff', '#ff006e');
interpolate(0); // 'rgba(58, 134, 255, 1)'
interpolate(0.5); // 'rgba(157, 67, 162, 1)' (midpoint)
interpolate(1); // 'rgba(255, 0, 110, 1)'Both endpoints are parsed before interpolating, so a hex color, an rgb() color and a CSS named color such as red mix freely in either position. Anything parseColor cannot resolve (currentColor, say) falls back to a hard step at the halfway point.
Any Interpolation
The fallback interpolator for values that don't match any other type. It snaps to the target value at the halfway point:
import {
interpolateAny,
} from '@ripl/web';
const interpolate = interpolateAny('hello', 'world');
interpolate(0.3); // 'hello'
interpolate(0.7); // 'world'Automatic Interpolation
When you use element.interpolate() or renderer.transition(), Ripl selects the appropriate interpolator for each property:
await renderer.transition(circle, {
duration: 1000,
state: {
radius: 100, // uses interpolateNumber
fill: '#ff006e', // uses interpolateColor
},
});Circle declares radius as numeric, so no detection runs for it. fill is declared as a paint — a gradient, a pattern or a color, tried in that order — so the same property tweens correctly whichever form it holds:
await renderer.transition(rect, {
duration: 1000,
state: {
fill: 'linear-gradient(180deg, #ff006e, #fb5607)', // uses interpolateGradient
},
});Custom Interpolators
Inline Interpolator
The simplest way to use a custom interpolator is to pass a function directly in the transition state:
await renderer.transition(circle, {
duration: 1000,
state: {
// Custom function: t goes from 0 to 1
radius: t => 50 + Math.sin(t * Math.PI) * 50,
},
});InterpolatorFactory
For reusable interpolators, create an InterpolatorFactory: a function that takes start and end values and returns an interpolator:
import type {
InterpolatorFactory,
} from '@ripl/web';
const interpolateBoolean: InterpolatorFactory<boolean> = (a, b) => {
return t => t > 0.5 ? b : a;
};The interpolators option
Declare which factory a property uses with the interpolators option, either at construction or per transition. A transition-level entry wins over a construction-level one, which wins over the element type's own default:
const toggle = createRect({
x: 0,
y: 0,
width: 100,
height: 40,
interpolators: {
// 'active' is this element's own state, so nothing could have guessed it
active: interpolateBoolean,
},
});
await renderer.transition(toggle, {
duration: 1000,
state: { width: 200 },
interpolators: {
width: interpolateStepped,
},
});A property may declare several factories, tried in order. Each is asked, via its test function, whether it can handle the value; the first to claim it wins. That is how fill accepts a gradient, a pattern or a color under one property:
const swatch = createRect({
x: 0,
y: 0,
width: 100,
height: 40,
interpolators: {
fill: [interpolateGradient, interpolatePattern, interpolateColor],
},
});A factory with no test function is an unconditional choice and is used as-is. If every declared factory declines the value, the property snaps at t > 0.5 rather than falling back to detection — declaring a factory is a statement about what the property holds.
TIP
A custom element should declare its own state properties this way rather than relying on detection. See Custom Elements.
Keyframe Values
Transitions also support keyframe-style arrays for multi-step animations:
await renderer.transition(circle, {
duration: 1000,
state: {
// Implicit offsets (evenly spaced)
fill: ['#3a86ff', '#ff006e', '#8338ec'],
// Explicit offsets
radius: [
{
value: 80,
offset: 0.3,
},
{
value: 40,
offset: 0.7,
},
{
value: 100,
offset: 1.0,
},
],
},
});The Interpolation Pipeline
When a transition runs, here's what happens for each property:
- Read the current value from the element
- Select an interpolator: a function passed as the target value is used verbatim; otherwise the first factory to claim the value is taken from the transition's
interpolators, then the element's, then — for a property neither declares — the built-in detection order - On each frame, compute the eased time
t - Apply the interpolated value to the element
- The renderer re-renders the scene
This pipeline runs for every animated property simultaneously, producing smooth multi-property transitions.
Demos
Each demo below lets you scrub through interpolation time t (0→1) to see the interpolator in action.
Number
Linear interpolation between two numbers, the foundation of all other interpolators.
import {
interpolateNumber,
} from '@ripl/web';
const interp = interpolateNumber(20, 120);
circle.radius = interp(t);Color
Interpolates between CSS color strings by parsing to RGBA, interpolating each channel independently, and serializing back. Named colors are parsed like any other format, so interpolateColor('red', 'blue') tweens rather than stepping.
import {
interpolateColor,
} from '@ripl/web';
const interp = interpolateColor('#3a86ff', '#ff006e');
rect.fill = interp(t);Gradient
Transitions between two CSS gradient strings by interpolating their stop colors, offsets, and angles.
import {
interpolateGradient,
} from '@ripl/web';
const interp = interpolateGradient(
'linear-gradient(0deg, #3a86ff, #8338ec)',
'linear-gradient(180deg, #ff006e, #fb5607)'
);
rect.fill = interp(t);Pattern
Transitions between two pattern(...) paints that share a tile type by interpolating their foreground color, background color, and tile size.
import {
interpolatePattern,
} from '@ripl/web';
const interp = interpolatePattern(
'pattern(diagonal, #3a86ff, #eff6ff, 6)',
'pattern(diagonal, #ff006e, #fff0, 16)'
);
rect.fill = interp(t);Rotation
Interpolates between rotation values. It supports numbers (radians) and strings like "90deg" or "1.5rad".
import {
interpolateRotation,
} from '@ripl/web';
const interp = interpolateRotation('0deg', '360deg');
rect.rotation = interp(t);Path
Progressively reveals a polyline path from start to end as t advances from 0 to 1.
import {
getPolygonPoints, interpolatePath,
} from '@ripl/web';
const points = getPolygonPoints(6, cx, cy, radius, true);
const interp = interpolatePath(points);
polyline.points = interp(t);Point Interpolation & Shape Morphing
interpolatePoints transitions between two point arrays. When the arrays differ in length, the shorter set is automatically extrapolated: intermediate points are inserted along its edges so both arrays have equal length. This enables smooth morphing between any two polygon shapes.
import {
getPolygonPoints, interpolatePoints,
} from '@ripl/web';
const triangle = getPolygonPoints(3, cx, cy, radius);
const octagon = getPolygonPoints(8, cx, cy, radius);
const interp = interpolatePoints(triangle, octagon);
polygon.points = interp(t); // smoothly morphs between shapes