Layers and keyframes

This page covers the comp as a web page, its three files, a layer with keyframes, easing, and the Kinetic counterpart of each motion design term.

Your comp is a web page

A video is a web page captured once per frame, thirty times a second at 30 fps. You write what the page looks like at second t, and the renderer saves the picture at every t from the first frame to the last. The comp's element takes a few data- attributes, and the rest is HTML, JavaScript, and CSS.

The comp is one element with a width, a height, a duration and a frame rate, and a layer is an element inside it, styled with CSS. A pre-comp with its own start and length is a clip, an element with data-start and data-duration, and its keyframes are a script's keyframes block, timed from the clip's start.

A frame depends only on its time, so scrubbing to any second shows what a render captures there.

The page

A comp is three files in one folder, index.html, style.css and scenes.js. index.html loads three scripts in order and holds the comp's element, which has no clips yet.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Launch</title>
    <link rel="stylesheet" href="style.css">
    <script defer src="https://cdn.jsdelivr.net/npm/wildflowerjs@^1.5.5/dist/wildflower.lite.min.js"></script>
    <script defer src="https://cdn.jsdelivr.net/npm/@wildflowerjs/kinetic@0.1/dist/kinetic.wf.js" data-preview></script>
    <script defer src="scenes.js"></script>
</head>
<body>
    <div id="stage" data-composition-id="main"         data-start="0" data-duration="6" data-width="1280" data-height="720" data-fps="30">
        <!-- clips go here -->
    </div>
</body>
</html>
  • WildflowerJS, the framework Kinetic builds on, whose lite build covers every example.
  • Kinetic, which sets the page's time and runs the render. data-preview on its script tag adds the preview bar and scales the stage down to fit the window. kinetic.wf.js is the development build, which prints a warning with a KN code when a scene breaks a rule. kinetic.wf.min.js is the same code with the checks left out, for the finished page.
  • Your motion. scenes.js holds the keyframes.

The comp's element carries these attributes.

  • data-composition-id is the comp's name.
  • data-start="0" and data-duration="6" are the comp's start and length in seconds.
  • data-width and data-height are its size in pixels.
  • data-fps is its frame rate, and without it the frame rate is 30.

The stylesheet sizes the stage (the comp's element) and the layers.

html, body { margin: 0; background: #0a0b10; }

#stage { position: relative; overflow: hidden; background: #0b0f1a; }

.layer { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }

h1 { margin: 0; font: 800 120px system-ui, sans-serif; color: #fff; }

[data-composition-id]:not([data-kinetic-ready]) { visibility: hidden; }

The stage matches data-width and data-height, and a .layer fills it. Elements later in the markup paint over earlier ones.

Until Kinetic has set up the motion, the stage shows every clip at once with no keyframes applied. The visibility rule hides it until Kinetic sets data-kinetic-ready on the comp's element.

A layer with keyframes

A scene is the motion for one clip. The clip gives the scene's name in data-component, and wildflower.kinetic.scene() in scenes.js registers the scene under that name. This clip, inside the stage in index.html, starts half a second into the comp, runs four seconds, and holds one layer, the heading.

<div class="layer" data-component="card" data-start="0.5" data-duration="4">
    <h1 data-bind-style="heading">Launch</h1>
</div>

scenes.js becomes this file.

wildflower.kinetic.scene('card', {
    keyframes: {
        heading: {
            opacity:    { at: [0, 0.5, 3.5, 4], to: [0, 1, 1, 0] },
            translateY: { at: [0, 0.6], to: [40, 0] },
            color:      { at: [0, 2], to: ['#9d7bff', '#ffffff'] }
        }
    }
});

data-bind-style="heading" applies the heading entry of keyframes to the heading's style. An entry is a set of tracks, and a track is the keyframes of one property.

  • at gives the times in seconds from the clip's start, and to the value at each time.
  • A track needs two or more keys, the times have to increase, and each time has one value.
  • Before its first time a track holds its first value, and after its last time its last value.
  • A value repeated in to is a hold. With [0, 1, 1, 0] the heading fades in over half a second, stays at full opacity, and fades out over the clip's last half second.

translateX, translateY, rotate and scale fold into one transform, in the order translate, rotate, scale, with units px, px, deg and none unless unit changes one. A blur track becomes filter: blur(), in px, so a rack focus on a layer is one track.

wildflower.kinetic.scene('card', {
    keyframes: {
        heading: {
            translateX: { at: [0, 0.8], to: [-50, 0], unit: '%' },
            scale:      { at: [0, 0.8], to: [1.2, 1] },
            blur:       { at: [0, 0.6], to: [12, 0] }
        }
    }
});
// at 0 s: transform: translate(-50%, 0px) scale(1.2), filter: blur(12px)
The heading slides in, shrinks to size and comes into focus, with the transform and filter the tracks write under it, and a click on the stage pauses it.

See Which to use for the other ways to blur, and for fading a clip with no track.

  • perspective, translateZ, rotateX, rotateY, skewX, skewY, scaleX and scaleY fold in too, and Tracks and styleAt() gives the order of all twelve.
  • scale is one number for both axes.
  • Rotation and scale turn around the element's transform-origin, which CSS sets.

Any other property is a track of its own. A number track on a property such as width takes a unit, as in { at: [0, 1], to: [0, 480], unit: 'px' }. round: true rounds a track's values to whole numbers, and round set to a number rounds them to that many decimals.

A color track blends between the colors in to, written #rgb, #rrggbb, #rrggbbaa, rgb() or rgba(). It holds only colors and has no unit.

A track can also hold text with numbers in it, such as 'inset(0% 100% 0% 0%)' on clipPath or '"wght" 300' on fontVariationSettings. Every keyframe has the same text, and only its numbers and colors change (Tracks with text values has the rules).

Open index.html in a browser to see the clip. data-preview adds a bar at the bottom of the window with play and pause, a scrubber, one frame back, one frame forward and the time. The bar is left out of the rendered video.

<div id="stage" data-composition-id="main"     data-start="0" data-duration="6" data-width="1280" data-height="720" data-fps="30">
    <div class="layer" data-component="card" data-start="0.5" data-duration="4">
        <h1 data-bind-style="heading">Launch</h1>
    </div>
</div>
wildflower.kinetic.scene('card', {
    keyframes: {
        heading: {
            opacity:    { at: [0, 0.5, 3.5, 4], to: [0, 1, 1, 0] },
            translateY: { at: [0, 0.6], to: [40, 0] },
            color:      { at: [0, 2], to: ['#9d7bff', '#ffffff'] }
        }
    }
});
html, body { margin: 0; background: #0a0b10; }

#stage { position: relative; overflow: hidden; background: #0b0f1a; }

.layer { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }

h1 { margin: 0; font: 800 120px system-ui, sans-serif; color: #fff; }

[data-composition-id]:not([data-kinetic-ready]) { visibility: hidden; }
A layer with keyframes

Easing

A track's ease shapes the motion between each pair of its keys. A track with no ease moves in a straight line from key to key. Replace the scene with this one to give each property its own curve.

wildflower.kinetic.scene('card', {
    keyframes: {
        heading: {
            opacity:    { at: [0, 0.5, 3.5, 4], to: [0, 1, 1, 0], ease: 'ease-in-out' },
            translateY: { at: [0, 0.8], to: [60, 0], ease: 'cubic-bezier(0.16, 1, 0.3, 1)' },
            rotate:     { at: [0, 0.8], to: [-8, 0], ease: 'back.out' },
            scale:      { at: [0, 0.8], to: [0.9, 1], ease: { spring: { stiffness: 170, damping: 14, mass: 1 } } }
        }
    }
});

The CSS names are linear, ease, ease-in, ease-out and ease-in-out. cubic-bezier(x1, y1, x2, y2) takes the four numbers from a CSS curve editor, and the two x values stay between 0 and 1. steps(n) moves in n equal jumps, and steps(n, start) makes each jump at the start of its step.

The names borrowed from GSAP, a web animation library, are power1 to power4, sine, expo, circ, back, elastic and bounce. Each takes .in, .out or .inOut, and a bare name means .out, so 'back' is 'back.out'.

'smooth' is the smoothstep, which eases in and out. As the one ease of a number track with three or more keyframes it threads a single curve through all of them: the value keeps its speed through each keyframe between, starts from rest at the first and comes to rest at the last, and never goes past a keyframe's value on the way to the next. Where the keyframes turn back, or hold the same value, the curve comes to rest there too. A move through four points reads as one move, where an ease per segment would make it stop at each point.

'spring' simulates a spring, and { spring: { stiffness, damping, mass } } sets its three numbers.

  • stiffness (default 100) is how hard the spring pulls toward the end value, and a higher number is faster.
  • damping (default 10) is the friction, and a lower number overshoots further and swings back more times. At least 2 * Math.sqrt(stiffness * mass) settles with no overshoot, which at mass 1 is 20 for stiffness 100, about 28.3 for 200 and 40 for 400. Measured with spring(), damping 20 overshoots by 4.3% at stiffness 200 and 16.3% at 400.
  • mass (default 1) is the weight, and a higher number is slower and swings more.

'spring' alone uses all three defaults and settles in about a second. A spring runs on the seconds since its key, so the gap to the next key does not change its speed.

  • On a track's last two keys it keeps moving after the final time in at.
  • On any earlier pair the next key cuts it off, and a spring still moving jumps to that key. Development builds warn (KN-123) when the jump is more than 2% of the distance between the two keys, and the warning gives how long the spring needs.

ease can also be an array with one ease per pair of keys, so one property can spring in and ease out. This track springs up into place, holds, and accelerates away.

translateY: { at: [0, 1, 2.4, 3], to: [60, 0, 0, -60], ease: ['spring', 'linear', 'power3.in'] }
The heading springs up, holds and accelerates away, with the track drawn beside it and a guide at each key time, and a click on the stage pauses it.

The spring's segment here is 1 second long, which leaves the default spring within 0.3% of rest when the second key arrives.

<div id="stage" data-composition-id="main"     data-start="0" data-duration="6" data-width="1280" data-height="720" data-fps="30">
    <div class="layer" data-component="card" data-start="0.5" data-duration="4">
        <h1 data-bind-style="heading">Launch</h1>
    </div>
</div>
wildflower.kinetic.scene('card', {
    keyframes: {
        heading: {
            opacity:    { at: [0, 0.5, 3.5, 4], to: [0, 1, 1, 0], ease: 'ease-in-out' },
            translateY: { at: [0, 0.8], to: [60, 0], ease: 'cubic-bezier(0.16, 1, 0.3, 1)' },
            rotate:     { at: [0, 0.8], to: [-8, 0], ease: 'back.out' },
            scale:      { at: [0, 0.8], to: [0.9, 1], ease: { spring: { stiffness: 170, damping: 14, mass: 1 } } }
        }
    }
});
html, body { margin: 0; background: #0a0b10; }

#stage { position: relative; overflow: hidden; background: #0b0f1a; }

.layer { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }

h1 { margin: 0; font: 800 120px system-ui, sans-serif; color: #fff; }

[data-composition-id]:not([data-kinetic-ready]) { visibility: hidden; }
Easing

The words, side by side

Each term from a motion design tool has a counterpart in a Kinetic page.

Motion designKinetic
CompThe element with data-composition-id, sized by data-width and data-height, with a data-duration and a data-fps.
LayerAn element inside the comp, styled with CSS. Later elements paint over earlier ones.
Pre-compA clip, which is an element with data-start and data-duration.
KeyframeA time in at and its value in to, on a track in keyframes.
Easing curveease on a track, a CSS or GSAP name, cubic-bezier(...), steps(n) or a spring.
HoldA value repeated in to, such as [0, 1, 1, 0].
ExpressionA computed, which is a function that returns a value from the time.