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.
{
"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.
// 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.
- 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 }
]
}
]
}blurnumber- The
stdDeviationattribute: how far a<feGaussianBlur>spreads. shadowXnumber- The
dxattribute: how far the shadow sits to the right. shadowYnumber- The
dyattribute: how far it sits down. shadowBlurnumber- The
stdDeviationattribute: how soft it is. shadowOpacitynumber- The
flood-opacityinline style: how strong it is. shadowColorstring- The
flood-colorinline style. Any colorfilltakes, exceptnone. glassRefractionnumber- The
scaleattribute: how far a<feDisplacementMap>bends the image. glassFrostnumber- The
stdDeviationattribute: how frosted the glass is. glassTintnumber- The
slopeattribute of a transfer function, like<feFuncA>. seednumber- The
seedattribute of a<feTurbulence>noise.
Reference
opacitynumber- The
opacityinline style. fill, strokestring- The
fillandstrokeinline styles:rgb(),rgba(), ornone. strokeWidthnumber- The
stroke-widthinline style, in user units (the engine addspx). drawnumber- 0 to 1 of the stroke: the
stroke-dasharray(the measured length, set once) andstroke-dashoffsetinline styles. dashOffsetnumber- The
stroke-dashoffsetattribute. 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
offsetattribute, itsstop-colorandstop-opacitystyles, and a linear one'sx1,y1,x2,y2.
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.
offsetnumberrequired- Where the stop sits, 0 to 1.
colorstringrequired- Any color
filltakes, exceptnone. opacitynumberdefault 1- 0 to 1.