Animate

Paint and shape

What a track can change besides a shape's transform or its outline: how see-through it is, its colors, its stroke, its size and place, its gradient and its effects. Each is written straight onto your SVG, as an inline style or an attribute.

Color and opacity

opacity, fill and stroke are written as inline styles on every element the target is bound to. Colors blend straight in RGB: red, green, blue and alpha, each on its own.

A color can be hex (3, 4, 6 or 8 digits), rgb(), rgba(), hsl(), hsla(), transparent or one of the 148 CSS color names. The engine can't read currentColor or a CSS variable: a keyframe it can't read is left out, and program.skipped says so.

fill and stroke also take none. A color keyed to none turns off as soon as it leaves the key before: it never fades. Out of none, the next color comes in from transparent black.

To fade a paint out, key it to transparent: that is transparent black, so the color darkens toward black as it fades. To fade with no darkening, key the same color at alpha 0, like #00ff7b00. Pick the friend's second key below and compare.

1.6 / 3.2 s
The fill's second key
{
  "duration": 3.2,
  "tracks": [
    {
      "targetUid": "blob",
      "property": "fill",
      "keyframes": [
        { "time": 0, "value": "#00ff7b" },
        { "time": 0.8, "value": "#fd0ddd" },
        { "time": 1.6, "value": "#8b5cf6" },
        { "time": 2.4, "value": "#00ff7b" }
      ]
    },
    {
      "targetUid": "star",
      "property": "strokeWidth",
      "keyframes": [
        { "time": 0, "value": 2 },
        { "time": 0.8, "value": 9 },
        { "time": 1.6, "value": 2 },
        { "time": 2.4, "value": 9 },
        { "time": 3.2, "value": 2 }
      ]
    },
    {
      "targetUid": "bubble",
      "property": "r",
      "keyframes": [
        { "time": 0.2, "value": 18, "easing": "back.out(2)" },
        { "time": 1, "value": 44 },
        { "time": 2.2, "value": 44, "easing": "power2.in" },
        { "time": 2.9, "value": 18 }
      ]
    },
    {
      "targetUid": "line",
      "property": "draw",
      "keyframes": [
        { "time": 0.2, "value": 0, "easing": "power2.inOut" },
        { "time": 1.4, "value": 1 }
      ]
    },
    {
      "targetUid": "line",
      "property": "opacity",
      "keyframes": [
        { "time": 2.5, "value": 1, "easing": "power1.in" },
        { "time": 3.1, "value": 0 }
      ]
    },
    {
      "targetUid": "ground",
      "property": "dashOffset",
      "keyframes": [
        { "time": 0, "value": 0, "easing": "none" },
        { "time": 3.2, "value": -66 }
      ]
    }
  ]
}

Strokes and the self-draw

strokeWidth is a plain number in user units, written as the stroke-width style with px added. A string like 4px can't be read, so that keyframe is left out. The star's outline above swells with it.

draw is how much of the stroke shows, from 0 to 1. The engine measures the stroke once, when the drawer is made, on the first element the target is bound to. It sets one dash that long, then moves only where the dash starts. The pink line above draws itself this way.

dashOffset writes the stroke-dashoffset attribute: where your own dash pattern starts along the stroke. Keyed at an even pace, the dashes march, like the ground above. draw writes inline styles, which win over that attribute, so give an element one or the other.

Shape attributes

cx, cy, r, width and height are written as the attributes of the same names, as plain numbers in the element's own units: a circle's middle and radius, a rect's size. The bubble above grows with r.

Whole outlines (morph, warp) have their own page.

Gradients

A fillGradient keyframe holds a whole gradient, as a JSON string: its type (linear or radial), a linear one's angle in degrees, and its stops, each an offset, a color and an opacity.

Stops pair by index, once sorted by offset: each one's offset, color and opacity blend toward the same stop in the next keyframe. Every keyframe needs the first one's type and number of stops. One that differs, or can't be read, holds the gradient before it, and program.skipped says so. If the earliest keyframe isn't a gradient it can read, the track plays nothing.

The engine doesn't paint the shape: it drives a gradient you made, the first element the target is bound to. Bind it to the <linearGradient> or <radialGradient> the shape is filled with, with a <stop> for each stop (the art.ts tab below). The engine never swaps that element, so radial keyframes need a <radialGradient>.

Offsets are written as attributes, colors and opacities as inline styles. A linear one's direction is written as x1, y1, x2, y2, through the middle of the shape's box, so leave its gradientUnits at the default. The angle blends like any number: 0 to 360 is one full turn, and the dial reads it back every frame. Switch to radial: the stops still blend, with no angle to turn.

angle 0°
0.0 / 3.2 s
type
// each keyframe holds a whole gradient, as a JSON string
const gradients = [
  {
    type: "linear",
    angle: 0,
    stops: [
      { offset: 0, color: "#00ff7b" },
      { offset: 0.5, color: "#00d4ff" },
      { offset: 1, color: "#8b5cf6" },
    ],
  },
  {
    type: "linear",
    angle: 180,
    stops: [
      { offset: 0, color: "#fd0ddd" },
      { offset: 0.3, color: "#ff8f00" },
      { offset: 1, color: "#00d4ff" },
    ],
  },
  {
    type: "linear",
    angle: 360,
    stops: [
      { offset: 0, color: "#00ff7b" },
      { offset: 0.5, color: "#00d4ff" },
      { offset: 1, color: "#8b5cf6" },
    ],
  },
];
const times = [0, 1.4, 2.8];

const clip = {
  duration: 3.2,
  tracks: [{
    targetUid: "sweep",
    property: "fillGradient",
    keyframes: gradients.map((g, i) => ({ time: times[i], value: JSON.stringify(g) })),
  }],
};

Effects

Effects are numbers, and one color (shadowColor), on the filter primitives that draw them. The engine doesn't build the filter or pick its primitives: you bind each effect target to the primitive it writes. Here the friend's filter is one <feDropShadow> carrying the uid shadow, and the star's is one <feGaussianBlur> carrying focus.

Each property writes one attribute or one style, whatever it is bound to. Under the stage is what the engine wrote on each primitive, read back every frame. The art.ts tab shows where each uid sits.

0.0 / 2.4 s
  • feDropShadowdy stdDeviation flood-opacity flood-color
  • feGaussianBlurstdDeviation
{
  "duration": 2.4,
  "tracks": [
    {
      "targetUid": "floater",
      "property": "y",
      "keyframes": [
        { "time": 0, "value": 0, "easing": "sine.inOut" },
        { "time": 1.2, "value": -20, "easing": "sine.inOut" },
        { "time": 2.4, "value": 0 }
      ]
    },
    {
      "targetUid": "shadow",
      "property": "shadowY",
      "keyframes": [
        { "time": 0, "value": 8, "easing": "sine.inOut" },
        { "time": 1.2, "value": 28, "easing": "sine.inOut" },
        { "time": 2.4, "value": 8 }
      ]
    },
    {
      "targetUid": "shadow",
      "property": "shadowBlur",
      "keyframes": [
        { "time": 0, "value": 2, "easing": "sine.inOut" },
        { "time": 1.2, "value": 9, "easing": "sine.inOut" },
        { "time": 2.4, "value": 2 }
      ]
    },
    {
      "targetUid": "shadow",
      "property": "shadowOpacity",
      "keyframes": [
        { "time": 0, "value": 0.55, "easing": "sine.inOut" },
        { "time": 1.2, "value": 0.2, "easing": "sine.inOut" },
        { "time": 2.4, "value": 0.55 }
      ]
    },
    {
      "targetUid": "shadow",
      "property": "shadowColor",
      "keyframes": [
        { "time": 0, "value": "#8b5cf6", "easing": "sine.inOut" },
        { "time": 1.2, "value": "#fd0ddd", "easing": "sine.inOut" },
        { "time": 2.4, "value": "#8b5cf6" }
      ]
    },
    {
      "targetUid": "focus",
      "property": "blur",
      "keyframes": [
        { "time": 0.2, "value": 0, "easing": "power2.inOut" },
        { "time": 1.2, "value": 6, "easing": "power2.inOut" },
        { "time": 2.2, "value": 0 }
      ]
    }
  ]
}
What each effect writes
blurnumber
The stdDeviation attribute: how far a <feGaussianBlur> spreads.
shadowXnumber
The dx attribute: how far the shadow sits to the right.
shadowYnumber
The dy attribute: how far it sits down.
shadowBlurnumber
The stdDeviation attribute: how soft it is.
shadowOpacitynumber
The flood-opacity inline style: how strong it is.
shadowColorstring
The flood-color inline style. Any color fill takes, except none.
glassRefractionnumber
The scale attribute: how far a <feDisplacementMap> bends the image.
glassFrostnumber
The stdDeviation attribute: how frosted the glass is.
glassTintnumber
The slope attribute of a transfer function, like <feFuncA>.
seednumber
The seed attribute of a <feTurbulence> noise.

Reference

Paint and shape: what each writes
opacitynumber
The opacity inline style.
fill, strokestring
The fill and stroke inline styles: rgb(), rgba(), or none.
strokeWidthnumber
The stroke-width inline style, in user units (the engine adds px).
drawnumber
0 to 1 of the stroke: the stroke-dasharray (the measured length, set once) and stroke-dashoffset inline styles.
dashOffsetnumber
The stroke-dashoffset attribute.
cx, cy, rnumber
The attributes of the same names.
width, heightnumber
The attributes of the same names.
fillGradientstring
A whole gradient, as JSON. On the bound gradient: each stop's offset attribute, its stop-color and stop-opacity styles, and a linear one's x1, y1, x2, y2.
A fillGradient keyframe's gradient
type"linear" | "radial"required
Every keyframe needs the first one's.
anglenumberdefault 90
Linear only, in degrees: 0 runs left to right, 90 down.
stops{ offset, color, opacity }[]required
At least one. Every keyframe needs the first one's number of stops.
Each stop
offsetnumberrequired
Where the stop sits, 0 to 1.
colorstringrequired
Any color fill takes, except none.
opacitynumberdefault 1
0 to 1.
NextMotion paths