Eases, interpolation and tracks

ease() and interpolate() turn a time into a value, and styleAt() reads tracks of numbers, colors, text or SVG path data into a style object.

The motion helpers are pure functions, so the same frame always gives the same value. A track is { at, to, ease, unit, round }, with at in the clip's seconds, and a length property such as width needs a unit. Every keyframe of a text or path track has the same text and commands in the same places, or the scene throws when it is defined (KN-120).

Motion

The motion helpers are pure functions on wildflower.kinetic that touch neither the DOM nor the framework, so the same frame always gives the same value. The one exception is smear(), which also reads the clock store's fps and shutter (Motion blur).

ease(name)

ease(name) returns an easing function of progress, 0 to 1, from one of five kinds of value.

  • The CSS set is linear, ease, ease-in, ease-out, ease-in-out, cubic-bezier(x1, y1, x2, y2) with x1 and x2 from 0 to 1, steps(n), and steps(n, start), which also takes end, jump-start and jump-end.
  • 'smooth' is the smoothstep. As the one ease of a number track with three or more keyframes it threads one curve through all of them, with the velocity continuous at each keyframe between and zero at the first and the last; in a per-segment array, or on a color or text track, it is the smoothstep of each segment.
  • GSAP's families are power1 to power4, sine, expo, circ, back, elastic and bounce, each with .in, .out or .inOut. A bare name is .out, as in GSAP.
  • 'spring' and { spring: { stiffness, damping, mass } }. A spring ease reads the seconds elapsed in its segment instead of the segment's progress.
  • A function of progress, which is returned as it is.

The interactive under interpolate() draws thirteen of them.

An unknown name throws (KN-121).

interpolate(t, at, to, options)

interpolate returns the value at t for keyframe times at, two or more increasing numbers, and values to, an array of the same length.

OptionDefaultSets
ease'linear'The ease of every segment, or an array of one per segment.
extrapolate'clamp'Both sides outside the keyframes, to a mode in the table below.
extrapolateLeft, extrapolateRightextrapolateOne side.
roundnonetrue for whole numbers, or a number of decimals.
space'rgb'How colors mix, 'rgb' or 'oklch' (Colors in oklch).
The ball is the call shown, and the curve beside it is the same call over the loop's two seconds, so past t = 1 it shows what extrapolate does. Click the stage to pause.

A malformed range throws (KN-120), and an ease array of the wrong length, or with an entry that is not an ease, throws (KN-121) with the entry's index in the message.

const { interpolate } = wildflower.kinetic;

interpolate(0.5, [0, 1], [0, 1], { ease: 'power1.in' });   // 0.25
interpolate(0.123456, [0, 1], [0, 100], { round: 2 });     // 12.35
interpolate(0.5, [0, 1], ['#000', '#fff']);                // 'rgb(128, 128, 128)'
  • to holds all numbers, all colors (#rgb, #rrggbb, #rrggbbaa, rgb() and rgba()), or all text with numbers in it (Tracks with text values).
  • Colors interpolate per channel and come back as an rgb() or rgba() string.
  • An ease array of ['spring', 'power3.in'] on three keyframes springs in and eases out.
  • An ease array is read when first seen, so new eases take a new array, and an edit in place is not seen.
  • A spring reads the seconds elapsed in its segment, and on the last segment runs on past the last key.
ModeOutside the keyframes
'clamp'Holds the end values (the default).
'extend'Continues the end segments.
'identity'Returns t itself.
'wrap'Starts the keyframes again from the first, so past the last key they repeat.
'mirror'Plays the keyframes backward and then forward again in turn.

A time a hair under a whole number of cycles counts as that cycle's start, and an infinite t on an 'extend', 'wrap' or 'mirror' side gives that side's end value. Remotion's default is 'extend'.

Tracks and styleAt(t, tracks)

A track is { at, to, ease, unit, round }, with at in the clip's seconds and to numbers, colors or text. Its extrapolate options, and its ease, one or one per segment, work as in interpolate. unit is appended to a number, such as 'px', '%' or 'deg', and a color or text track has none. interpolate(t, track) with one track returns that track's value at t.

const width = { at: [0, 1], to: [100, 200], unit: 'px', round: true };

wildflower.kinetic.interpolate(0.5, width);   // '150px'
The bar's width is the track's value at the live time, a string in whole pixels, and a click on the stage pauses it.
  • A length property such as width, top, fontSize, borderRadius or offsetDistance needs a unit, because the browser ignores a number with no unit there. Development builds warn once per track that has none (KN-122).
  • lineHeight is warned about too, since a bare number there is a multiple of the font size, and unit: '' keeps the multiple and ends the warning.
  • A spring segment that the next keyframe reaches while the spring is more than 2% of the keyframes' distance from rest jumps to the keyframe, and development builds warn once per track (KN-123). With 'wrap' on the track's right side, the same holds for the last segment, where the keyframes start again.

styleAt() and the transform keys

styleAt(t, tracks) reads an object of tracks at t seconds and returns a style object for that moment. To stagger the rows of a list, pass each row a time shifted by its stagger() delay.

  • The twelve keys in the table combine into transform, and blur becomes filter: blur(), in px.
  • These keys take numbers, and a color or text track on one of them throws (KN-120).
  • Every other key is a style property of its own, and a text track there writes its string.
KeyDefault unitWritten as
perspectivepxperspective()
translateX, translateYpxtranslate(X, Y)
translateZpxtranslateZ()
rotateXdegrotateX()
rotateYdegrotateY()
rotatedegrotate()
skewXdegskewX()
skewYdegskewY()
scalenonescale()
scaleXnonescaleX()
scaleYnonescaleY()

The functions are written in table order, each only when its key is present, and a track's unit replaces the default. CSS applies a transform list left to right in the element's own coordinates, so the element gets its depth, moves, turns in 3D, turns in the plane, shears, and then takes its size.

wildflower.kinetic.styleAt(0.5, {
    scale:      { at: [0, 1], to: [1, 2] },
    rotate:     { at: [0, 1], to: [0, 90] },
    translateY: { at: [0, 1], to: [40, 0] }
});
// { transform: 'translate(0px, 20px) rotate(45deg) scale(1.5)' }, in the table's order
Move t to change the card's style, and the transform under it keeps the table's order whatever order the tracks are given in.
  • perspective() comes first because it acts only on the functions after it.
  • translate(X, Y) is written when either axis is present, with the other as 0px, and translateZ() follows it as a function of its own.
  • rotate turns the element in the screen's plane, about the z axis.
  • A transform track beside any of the twelve keys throws (KN-120), since both write transform.

Numbers beside tracks

A key can also be a plain number, which is that key's value at every time, with its default unit. Use one for a value your own code works out, such as a pulse on the beat or a size from the layout, and styleAt() writes it into the transform in the table's order, beside the tracks. A scene's keyframes take numbers the same way.

disc() { return wildflower.kinetic.styleAt(this.local, { rotate: SPIN, scale: 1 + 0.14 * this.pulse }); }
// { transform: 'rotate(90deg) scale(1.07)' }, with SPIN a track and the scale a number
The rotate track turns the card, and the two numbers on the sliders hold its size and its height at every time.
  • A number that is not finite throws (KN-120), and so does a string, since a string is no track.
  • With only plain numbers, nothing depends on the time, so styleAt() never reads it and any t does: styleAt(0, { translateX: 40, scale: 1.5 }).
  • A number for a length outside the transform, such as width: 120, gets no unit, so the browser ignores it, and development builds warn once (KN-122). Set the length in CSS, or give it a track with a unit.

A turn about x or y shows depth only under perspective, from the perspective key on the element or the CSS perspective property on its parent. Without either, rotateY(60deg) draws the element flat at half its width. The perspective key writes the perspective() function, never the CSS property, so set a parent's perspective in its stylesheet.

const rise = {
    opacity:    { at: [0, 0.3], to: [0, 1] },
    translateY: { at: [0, 0.4], to: [24, 0], ease: 'power2' }
};

wildflower.kinetic.scene('menu', {
    state: { items: [{ id: 1, label: 'Start' }, { id: 2, label: 'Options' }, { id: 3, label: 'Quit' }] },
    computed: {
        rows() {
            const { styleAt, stagger } = wildflower.kinetic;
            return this.items.map((item, i) => ({
                key: item.id,
                label: item.label,
                style: styleAt(this.local - stagger(i, this.items.length), rise)
            }));
        }
    }
});
<ol data-list="rows" data-key="key">
    <template><li data-bind="label" data-bind-style="style"></li></template>
</ol>
The three rows share one set of tracks and rise in turn, each at a time shifted by its stagger() delay, and a click on the stage pauses them.

Tracks with text values

A track's to can hold strings with numbers in them, for a value with function syntax or several numbers, such as clipPath, filter, boxShadow, backgroundPosition, maskPosition, fontVariationSettings or a gradient's stops. A track whose strings are all colors is a color track, and any other track of strings is a text track.

keyframes: {
    panel: {
        clipPath: { at: [0, 0.6], to: ['inset(0% 100% 0% 0%)', 'inset(0% 0% 0% 0%)'], ease: 'power3.out' },
        filter:   { at: [0, 1], to: ['drop-shadow(0px 0px 0px rgba(0, 0, 0, 0))', 'drop-shadow(0px 12px 24px rgba(0, 0, 0, 0.45))'] }
    },
    word: { fontVariationSettings: { at: [0, 2], to: ['"wght" 300', '"wght" 800'] } }
}
The panel wipes in as its shadow lands and the word gains weight, with only the numbers in each string moving, and a click on the stage pauses them.

Each keyframe is read left to right into numbers, colors and the text between them. At each time every number and every color moves by the segment's eased progress, and the text between them stays as it is.

PartRead as
NumberThe CSS number token, with an optional sign, decimal part and exponent.
ColorAny form a color track takes.
TextUnits, words, quoted strings and url(), and a number right after a letter, a digit or #, such as the 1 in h1.
  • Every keyframe has the same text, character for character, with a number or a color in the same places. Units are text, so 0px against 10% does not match, and neither does 0 against 0%.
  • A track whose keyframes differ throws when the scene is defined (KN-120), and the message gives the keyframe. The development build's message also gives the two keyframes and the first place where they differ.
  • A number is written with at most four decimals, and trailing zeros, a trailing decimal point and the sign of -0 are dropped. round applies first. A color is written as a color track writes it, as rgb() or rgba().
  • ease, extrapolate, round and space work as they do on a number or color track, and 'extend' continues every number and color of the end segment. A spring on the last segment runs on past the last key.
  • 'identity' throws (KN-120), since it returns t, and so does unit, since the units are in the text.
  • A text track on transform writes the whole transform, and none of the twelve transform keys can sit beside it. A text track on filter cannot sit beside a blur track.

A few values behave in ways worth knowing before a track moves them.

  • inset(top right bottom left) cuts that much off each side, so a right inset falling from 100% to 0% uncovers the element from the left, and a left inset rising to 100% covers it again from the same side.
  • A percentage radius in circle(R% at X% Y%) is measured against the box's diagonal divided by the square root of 2, which puts each corner 70.7% from the center, so 75% uncovers the whole box. A track that shrinks the radius while it moves the center closes the circle onto that point.
  • drop-shadow() draws the shadow from what the element draws, rounded corners and transparent parts included, and the rgba() color inside it mixes per channel, alpha included.
  • One fontVariationSettings string can move several axes, such as '"wght" 200, "wdth" 75'. A self-hosted variable font declares the ranges of its axes in @font-face, such as font-weight: 100 1000; font-stretch: 25% 151% for Roboto Flex, and readiness waits for the font before frame 0.

Each keyframe is split once per track, not on every seek. interpolate() takes text values by the same rules and returns the string, and mix() takes two.

Path data in a track

SVG path data in a text track is read by the SVG 2 path grammar instead of the rules above.

  • A value that is all path data starts with M or m, and a value that starts with M and does not parse as path data is ordinary text.
  • The inside of path("…") or path('…') anywhere in a value, as in clipPath, offsetPath or the CSS d property, is path data too, and the path(" and the closing quote stay text.
keyframes: {
    blob: { at: [0, 2], to: ['M0 0C40-30 80-30 120 0Z', 'M0 0C40 30 80 30 120 0Z'], ease: 'sine.inOut' },
    photo: { clipPath: { at: [0, 1], to: ['path("M0 0H200V120H0Z")', 'path("M40 20H160V100H40Z")'] } }
}
// <path data-bind-attr="{ d: blob }"></path>
The blob flexes with the d it is written as under it while the photo's path() clip closes to the inner box, and a click on the stage pauses them.
  • Each command letter is text, and its arguments are numbers, split as the grammar splits them. A minus sign or a second decimal point starts a new number, and a number can follow a letter with no space, so M190-30C130 reads as M, 190, -30, C, 130.
  • The two flags of an arc, its 4th and 5th arguments, are text, so they never take a value between 0 and 1.
  • As on any text track, keyframes need the same command letters in the same places and the same arc flags. A path that differs throws when the scene is defined (KN-120), and the development build's message ends with "the paths have different commands; wildflower.kinetic.matchPaths() makes any two paths morphable". matchPaths() converts paths with different commands to the same ones.
  • The path is written with one space after each command letter and between its numbers, so M190-30C130 comes out as M 190 -30 C 130, and each number has at most four decimals. Each arc flag is written as the 0 or 1 it was given. Commas and extra spaces in a keyframe are not kept.