Type, Lottie and animated images

text() draws type with a weight per letter on a canvas, and lottie() and animatedImage() put Lottie animations and animated GIF, WebP and APNG files on Kinetic's clock.

text() draws from outlines the outline baker makes from a variable font. Without lottie() or animatedImage(), a Lottie animation or an animated image plays on the browser's own clock, and a render captures it wherever it had got to. A file from another origin needs CORS headers from its server.

Type with a weight per letter

text(canvas, { scene, font, lines }) draws lines of type on a canvas from a variable font's outlines, each letter with its own weight and slant. The outline baker makes the outlines from the font file, for the characters you choose. Kinetic places each letter from its advance and kerning at its own setting and draws it straight onto the canvas, so the page lays nothing out, no letter snaps to a whole pixel, and the browser builds no font weight while the clock plays.

const { scene, text } = wildflower.kinetic;

scene('title', {
    init() {
        text(this.element.querySelector('canvas'), { scene: this, font: window.WORK_SANS, lines: (m, t) => {
            m.line('KINETIC', { at: [640, 360], size: 96, align: 'center', valign: 'middle' },
                (i) => ({ wght: 500 + 400 * Math.sin(t * 2 - i * 0.3) ** 2 }));
        } });
    },
});
Each letter's weight follows the call, a wave along the word, and the readout is the K's. With spacing: 'fixed' the word keeps its width as the weights change. Click the stage to pause.

The example's outlines were baked with the variable name WORK_SANS (the baker's default is OUTLINES), and their file loads in a <script> before the scene's.

Lines and letters

  • The lines option is a function, lines(m, t), which text() needs. Kinetic calls it before each draw with the time: the scene's local time with scene: this, and the composition time otherwise (a layer's scene). m.line(string, options, letter) adds a line, and returns m for the next.
  • A line's options are at, the point [x, y] in px from the canvas's top left; size in px (16); align ('left', the default, 'center' or 'right'); valign ('baseline', the default, or 'middle', the middle of the capitals on y); tracking, in em between letters (0); color (black); and wght or slnt for the whole line.
  • spacing: 'natural', the default, places each letter by its advance and kerning at its own weight, so a heavier letter pushes the next along, as After Effects' per-character spacing does. spacing: 'fixed' keeps each letter in the slot of its advance at the font's defaults, centers it there and applies no kerning, so the line keeps its width as the weight changes.
  • letter(i, ch) returns that letter's own values, which replace the line's: wght, slnt, dx and dy in px, scale, rotate in degrees, opacity (1), blur in px and color. A scale or a rotation is about the letter's middle: half its advance across, and half the capitals' height up from the baseline. A letter at opacity 0 still takes its space.
  • On a font with no slant axis, slnt skews the letter by that many degrees, with the axis's sign: a negative slant leans right.
  • A kerning pair takes the left letter's setting, and a line's width is its advances and the tracking between its letters, with none after the last.

See Which to use for the other ways to blur.

Drawing and errors

  • The canvas is drawn at its shown size times the device pixel ratio, and at its CSS box in a render. Every value comes from the time, so the preview, check() and a render draw the same letters at the same time.
  • The handle's set({ font, lines }) swaps the outlines, the lines function or both, and dispose() stops the drawing; a text() made with scene: this is disposed with the scene.
  • Outlines that are not the baker's, no lines, the lines as a third argument, an unknown or bad option or letter value, and a character the outlines do not hold throw KN-100.

lottie(animation, options)

lottie(animation, options) hands an animation made by lottie-web to the clock, so it previews, checks and renders like the rest of the page.

The page loads lottie-web itself, as it loads three.js, and Kinetic does not include it.

const { scene, lottie } = wildflower.kinetic;

scene('logo', {
    init() {
        this.anim = window.lottie.loadAnimation({
            container: this.element.querySelector('.mark'),
            renderer: 'svg', loop: false, autoplay: false, path: 'logo.json',
        });
        lottie(this.anim, { scene: this, start: 0.5 });
    },
    destroy() { this.anim.destroy(); },
});
OptionDefaultMeaning
scenenoneThe scene's own component, this: the animation runs on the scene's local time and is handed back with the scene (a layer's scene). Without it, the composition time.
start0The time, in seconds, at which the animation is on its first frame. Before it, the animation holds that frame.
rate1The animation's seconds per second, above 0.
loopfalseAt the end, true starts the animation again and false holds its last frame.
The animation holds its first frame until start, plays at rate, then holds its last frame, or starts again with loop; the strip marks the frame. A click pauses. Animation by Abdul Latif on LottieFiles.

Frames

  • On every seek, once the animation has loaded, Kinetic calls goToAndStop(frame, true), with the frame (t - start) × rate × frameRate, the animation's own frame rate. t is the scene's local time with scene: this, and the composition time without it.
  • Past its end the animation holds frame totalFrames - 1, where lottie-web stops an animation that has played through, since frame totalFrames is past its layers and shows nothing.
  • Kinetic passes frames rather than milliseconds, so the animation's own setSpeed() and setDirection() leave its timing alone, and rate is its one speed.
  • Kinetic turns subframes on with setSubframe(true), so a seek between two of the animation's frames draws the motion between them.
  • The animation is put on the frame for the clock's time when lottie() is called and again when it loads, so it shows the right frame with the clock paused.
  • The 'svg' renderer draws into a render like the rest of the page, and the 'canvas' renderer through the render's copy of each canvas. check() compares the svg renderer's attributes and the canvas renderer's pixels as it compares any element.

Loading and failure

  • Readiness, check() and render() wait for each animation to load: isLoaded, or its DOMLoaded event. An animation exported in segments, whose data lottie-web loads in parts, is waited for until its first part has loaded, so export one file for a render.
  • The svg renderer adds an animation's images as SVG <image> elements after DOMLoaded, and readiness decodes every one under the composition root; the canvas renderer has them before DOMLoaded.
  • An animation that fires either failure event before it has loaded is KN-180 (Failure). The message gives the animation's name when loadAnimation() was given one, its file, and the scene it was given.
    • data_failed means a file that could not be loaded.
    • lottie-web's error means data lottie-web loaded and could not set up, and the message has : its data after the animation.
  • lottie-web 5.13 fires neither for a file that is not JSON at all, which throws an error of lottie-web's own in the console, and readiness then waits until the runtime's timeout.
  • Call lottie() in the same task as loadAnimation(): an event that fired before lottie() was called is missed, and lottie-web keeps nothing that says a load failed.
  • With the HyperFrames runtime on the page the runtime seeks the animations itself (Rendering through HyperFrames).

Handing an animation back

  • lottie() returns a handle whose dispose() hands the animation back, after which Kinetic no longer seeks it, listens to it or waits for it. The animation is handed back as well when it is destroyed, and when the scene it was given is removed.
  • lottie-web keeps every animation until its destroy(), so call that in the scene's destroy(), which also hands it back. lottie() again for the same animation replaces its options, and readiness that was waiting for it waits for the new registration.
  • Kinetic calls only goToAndStop(), setSubframe() when there is one, and addEventListener() for DOMLoaded, data_failed, error and destroy, and it reads isLoaded, totalFrames and frameRate, and for the message name, path and fileName. Any object with those works, and one with no isLoaded is taken as loaded.
  • An animation without goToAndStop() and addEventListener(), options that are not an object, an unknown option, a start that is not a finite number, a rate that is not above 0, and a loop that is not true or false throw KN-100.

animatedImage(src)

animatedImage(src) decodes a GIF, an animated WebP or an APNG with WebCodecs' ImageDecoder and returns a handle that draws the frame for a time into a canvas.

const { scene, animatedImage } = wildflower.kinetic;
const sticker = animatedImage('wave.gif');

scene('sticker', {
    seek(local) { sticker.draw(this.element.querySelector('canvas'), local); },
});
MemberMeaning
durationThe length of one play-through in seconds, 0 until the file is decoded.
framesThe number of frames, 0 until the file is decoded.
frameAt(t, { loop })The index of the frame on screen t seconds in.
draw(canvas, t, { loop })Clears the canvas and draws that frame over all of it, stretched to its width and height, whatever transform the canvas's context has. The context's alpha and composite mode apply as the page set them.

Frames and timing

  • animatedImage() fetches the file and decodes every frame once, keeping each as a bitmap for the life of the page. A second call with the same URL returns the same handle, and a decode that fails partway frees the frames it had made.
  • Each frame takes its width × height × 4 bytes, so 300 frames at 1920 × 1080 take about 2.5 GB: keep animated images small, and use footage for long or large clips.
  • Each frame lasts as long as the file says, and a frame of 10 ms or less lasts 100 ms, as browsers play one. The frame for t comes from those durations alone, so a seek shows the same frame however it was reached.
  • Before 0 the first frame shows. Past the end it plays as browsers play the file: again and again when the file loops for ever, and when the file repeats n times, n + 1 times in all, then holding its last frame. loop: true starts it again for ever, and loop: false holds the last frame after one play.
  • Give the canvas the image's shape, since draw() stretches the frame over it. Before the file is decoded, draw() draws nothing and frameAt() returns 0.

Loading and errors

  • Readiness, check() and render() wait for every animated image made so far, also one made at the top of a script outside any scene. One made after ready() has resolved is waited for by the next ready(), check() or render(). One asked for with animatedImage(src, { scene: this }) is waited for while that scene is on the page; one asked for without a scene, for good.
  • A file that cannot be fetched or decoded is KN-181 (Failure), and the message gives the file. A file on another origin needs CORS headers, and a page opened from a file cannot fetch at all, which the development build's message says.
  • In a browser without ImageDecoder, Safari at the time of writing, animatedImage() throws KN-100. A render needs Chrome anyway, and a preview in such a browser can show the file in a plain <img>, which plays on the browser's own clock. Make the handle only when 'ImageDecoder' in window, and otherwise have the scene's init() replace the canvas with an <img> of the same file.
  • A src that is empty or not a string, a draw() target that is not a canvas or already has a WebGL context, a t that is not a finite number, options that are not an object, an unknown option, a scene that is not a scene's component and a loop that is not true or false throw KN-100.