Animate

Morph and warp

A morph or warp track changes a path's whole outline. Each keyframe holds an outline as SVG path data, and Stew Engine blends from one to the next.

One shape into another

A morph track's keyframes each hold a whole outline: SVG path data, the same text as a path's d. The outlines don't have to match. Stew Engine makes them alike, so every number of one has a partner in the next, then blends each number along the ease of the keyframe it leaves.

Here a triangle turns into a flower, then a ring, then back. The triangle has three cubics and the flower six, so each side of the triangle is split in half. With points shown, the dot in the middle of the triangle is the ring's hole, waiting as a point.

0.0 / 4.8 s
Points
{
  "duration": 4.8,
  "tracks": [
    {
      "targetUid": "shape",
      "property": "morph",
      "keyframes": [
        { "time": 0.4, "value": "M 320 34 L 410 190 L 230 190 Z", "easing": "power2.inOut" },
        { "time": 1.2, "value": "M 305 102 C 269.2 13.8 370.8 13.8 335 102 C 393.5 26.9 444.3 114.9 350 128 C 444.3 141.1 393.5 229.1 335 154 C 370.8 242.2 269.2 242.2 305 154 C 246.5 229.1 195.7 141.1 290 128 C 195.7 114.9 246.5 26.9 305 102 Z" },
        { "time": 1.9, "value": "M 305 102 C 269.2 13.8 370.8 13.8 335 102 C 393.5 26.9 444.3 114.9 350 128 C 444.3 141.1 393.5 229.1 335 154 C 370.8 242.2 269.2 242.2 305 154 C 246.5 229.1 195.7 141.1 290 128 C 195.7 114.9 246.5 26.9 305 102 Z", "easing": "power2.inOut" },
        { "time": 2.7, "value": "M 232 128 A 88 88 0 0 1 408 128 A 88 88 0 0 1 232 128 Z M 282 128 A 38 38 0 0 1 358 128 A 38 38 0 0 1 282 128 Z" },
        { "time": 3.4, "value": "M 232 128 A 88 88 0 0 1 408 128 A 88 88 0 0 1 232 128 Z M 282 128 A 38 38 0 0 1 358 128 A 38 38 0 0 1 282 128 Z", "easing": "power2.inOut" },
        { "time": 4.2, "value": "M 320 34 L 410 190 L 230 190 Z" }
      ]
    }
  ]
}

How outlines are made alike

Every keyframe of a track ends up with the same subpaths, each with as many cubics. It happens once, when the clip is compiled, in four steps. A warp skips pairing by size and lining up.

Read
Each outline becomes all curves. Every command is read, absolute or relative: M L H V C S Q T A Z. A line becomes a cubic, a quadratic the cubic that draws the same curve, an arc one cubic per quarter turn or less.
Pair
A morph's subpaths pair up biggest with biggest, by the box their points take (a warp keeps the order they are drawn in). A keyframe short of one gets a stand-in: a point at the middle of that subpath in the nearest keyframe that has it, which grows out of nothing or shrinks into it.
Split
Each subpath is split until it has as many cubics as the most any keyframe has there: the longest cubic (measured round its control points) split at its middle, again and again (the first one on a tie).
Line up
A morph lines each keyframe up with the one before. A closed outline may start at any of its points and run either way, and takes the start and direction whose points travel least (on a tie, as drawn). An open one can only be turned around.

Warp: one outline in new poses

A warp track is one outline moved about: every keyframe has the same points, and each point goes to its own partner in the next pose. It is read and split like a morph, but its subpaths pair in the order they are drawn, not by size, and it is never lined up: that could swap its points around.

Here the jelly's body and its eyes are two warps. Switch them to morph: the body plays the same, but the left eye is the bigger one until it winks. A morph pairs biggest with biggest, so the eyes cross.

0.0 / 3.6 s
property
clip.json
{
  "duration": 3.6,
  "tracks": [
    {
      "targetUid": "jelly",
      "property": "warp",
      "keyframes": [
        { "time": 0.2, "value": "M 236 206 C 229.3 142.2 267.9 90 320 90 C 372.1 90 410.7 142.2 404 206 C 357.8 213 282.2 213 236 206 Z", "easing": "power2.out" },
        { "time": 0.5, "value": "M 214 206 C 205.5 159.8 254.3 122 320 122 C 385.7 122 434.5 159.8 426 206 C 367.7 213 272.3 213 214 206 Z", "easing": "power2.out" },
        { "time": 0.85, "value": "M 252 206 C 246.6 127.9 277.8 64 320 64 C 362.2 64 393.4 127.9 388 206 C 350.6 213 289.4 213 252 206 Z", "easing": "power2.in" },
        { "time": 1.15, "value": "M 220 206 C 212 156.5 258 116 320 116 C 382 116 428 156.5 420 206 C 365 213 275 213 220 206 Z", "easing": "elastic.out(1, 0.35)" },
        { "time": 2.2, "value": "M 236 206 C 229.3 142.2 267.9 90 320 90 C 372.1 90 410.7 142.2 404 206 C 357.8 213 282.2 213 236 206 Z" }
      ]
    },
    {
      "targetUid": "eyes",
      "property": "warp",
      "keyframes": [
        { "time": 0.2, "value": "M 285.8 136.4 A 9 12 0 0 1 303.8 136.4 A 9 12 0 0 1 285.8 136.4 Z M 337.2 136.4 A 8 11 0 0 1 353.2 136.4 A 8 11 0 0 1 337.2 136.4 Z", "easing": "power2.out" },
        { "time": 0.5, "value": "M 276.8 155.6 A 11.4 8.7 0 0 1 299.6 155.6 A 11.4 8.7 0 0 1 276.8 155.6 Z M 341.7 155.6 A 10.1 8 0 0 1 361.9 155.6 A 10.1 8 0 0 1 341.7 155.6 Z", "easing": "power2.out" },
        { "time": 0.85, "value": "M 292.3 120.8 A 7.3 14.7 0 0 1 306.9 120.8 A 7.3 14.7 0 0 1 292.3 120.8 Z M 333.9 120.8 A 6.5 13.5 0 0 1 346.9 120.8 A 6.5 13.5 0 0 1 333.9 120.8 Z", "easing": "power2.in" },
        { "time": 1.15, "value": "M 279.3 152 A 10.7 9.3 0 0 1 300.7 152 A 10.7 9.3 0 0 1 279.3 152 Z M 340.5 152 A 9.5 8.5 0 0 1 359.5 152 A 9.5 8.5 0 0 1 340.5 152 Z", "easing": "elastic.out(1, 0.35)" },
        { "time": 2.2, "value": "M 285.8 136.4 A 9 12 0 0 1 303.8 136.4 A 9 12 0 0 1 285.8 136.4 Z M 337.2 136.4 A 8 11 0 0 1 353.2 136.4 A 8 11 0 0 1 337.2 136.4 Z", "easing": "power2.inOut" },
        { "time": 2.5, "value": "M 283.8 136.4 A 11 1.8 0 0 1 305.8 136.4 A 11 1.8 0 0 1 283.8 136.4 Z M 337.2 136.4 A 8 11 0 0 1 353.2 136.4 A 8 11 0 0 1 337.2 136.4 Z" },
        { "time": 2.9, "value": "M 283.8 136.4 A 11 1.8 0 0 1 305.8 136.4 A 11 1.8 0 0 1 283.8 136.4 Z M 337.2 136.4 A 8 11 0 0 1 353.2 136.4 A 8 11 0 0 1 337.2 136.4 Z", "easing": "power2.inOut" },
        { "time": 3.2, "value": "M 285.8 136.4 A 9 12 0 0 1 303.8 136.4 A 9 12 0 0 1 285.8 136.4 Z M 337.2 136.4 A 8 11 0 0 1 353.2 136.4 A 8 11 0 0 1 337.2 136.4 Z" }
      ]
    }
  ]
}

What gets written

Whenever a number of it changes, the outline is written to the d attribute of the target's elements: a move, one C per cubic and Z where a subpath closes, numbers to three places. destroy() puts the old d back. In a drawer's bind, a warp is the morph channel too.

A keyframe that isn't path data is left out and noted in program.skipped, and the others still play. Path data that goes wrong part way draws what came before it, as a browser does. A morph and a warp on one target, or two of either: the one later in tracks plays.

Reference

The track
propertystringrequired
"morph" or "warp". morph: outlines that may differ, read, paired by size, split and lined up. warp: poses of one outline, read and split, kept point for point.
keyframes[].valuenumber | stringrequired
Here, path data: a whole outline, written to the path's d in its own units. A value that isn't path data is left out.
Path data in code
parsePath(d)Curves | null
Path data into all-curve form. null when it doesn't start with a move, or draws nothing.
printPath(curves)string
All-curve form back into path data: a move, one C per cubic, Z where a subpath closes, numbers to three places.
CurvesSubpath[]
A path's subpaths, in the order they are drawn.
Subpath{ points: Float64Array; closed: boolean }
points: the start, x then y, then six numbers per cubic: first control, second control, end. closed: it ends where it started (Z).
NextThe player