has to be decided somewhere. There are two somewheres, and picking one decides almost everything else about what you end up building.
By the end of this page you should be able to read any morphing library's front page and say which of the two it picked, and what it gave up to be there. There is no third answer, and nothing gets both.
Ship a solver. When a morph is requested, take the two shapes, work out the pairing, and animate.
Two of the steps are the interesting ones. Matching decides which stroke becomes which, which is Part 2's problem arriving again with no new information to solve it. Aligning works out the rotation and scale between a matched pair. The other three are preparation and arithmetic. Here is the whole sequence, and the demo below runs it a step at a time:
None of that work is visible in the finished animation, which is most of why it is hard to reason about. So here it is one step at a time, on the pair this manual opened with. Every number below is computed in the demo rather than typed into it.
Every command becomes a cubic, so the two paths compare.
A straight stroke keeps its shape exactly when its two controls sit a third and two thirds of the way along it. That is the same line() helper Part 1 opened with, and it is why nothing here looks any different yet.
That is real engineering and it works on anything you hand it. The appeal is obvious: you import a library, you pass it two icons, and it animates. No build step, no configuration, nothing to learn.
The other option starts from an observation that sounds too simple to matter.
You already know both icons at build time. They are in your repo. A hamburger is not going to become a different shape between now and when a user clicks it.
So why is a phone recomputing a fixed answer, on every page load, forever?
Write the answer down instead. Divide every icon in a set into the same fixed number of
parts, each one a ,
one <path> per slot, and declare that slot 0 of any icon becomes slot 0 of any other.
Correspondence stops being a search and becomes a fact about how the icons are drawn.
menu slot 0: top bar slot 1: middle bar slot 2: bottom bar
cross slot 0: "\" slot 1: "/" slot 2: parkedThere is no matching step. There is no cost function. There is nothing to get wrong on someone else's device, because there is nothing left to decide.
Once correspondence is fixed ahead of time, every frame of the animation is knowable ahead of time. Which means it can be computed at build and emitted as CSS keyframes, and a menu-to-cross toggle ships zero bytes of JavaScript.
The step from "knowable" to "precomputable" is the one worth being sure of, because the rest
of the manual depends on it. A frame of a morph is a list of coordinates, each one a weighted
average of the same coordinate in the two icons. Once you know which coordinate pairs with
which, the only remaining input is progress, a number between 0 and 1. So a frame is a pure
function of progress, and a pure function over a bounded input can be evaluated in advance
and stored as a table. @keyframes is that table, and the browser's own interpolation reads
between the rows.
Nothing about that reasoning is specific to CSS. It is why the same compiled also drives SMIL, WAAPI and the JavaScript without being recomputed for any of them.
It also means a wrong pairing is fixable. When a solver picks something you dislike, there is nowhere to put the correction: it will re-derive the same answer on every device forever. When correspondence is data in your repo, you edit it and commit.
And slots have identity, so they can be choreographed individually. The middle bar can leave 60 milliseconds before the outer two rotate. A solver has one similarity per subpath and nothing to address.
The solver wins on effort, decisively. Install, pass two icons, done. It also handles pairs you cannot know in advance, which the other approach cannot do at all, ever.
Writing it down costs a build step. That is not a detail to engineer away, it is the shape of the trade: a build-time answer requires a build.
| Solve on the device | Write it down | |
|---|---|---|
| Setup | none | a build step |
| Any pair, decided at runtime | works | impossible |
| JavaScript shipped | the whole solver | none, or a small driver |
| Wrong pairing | permanent | edit and commit |
| Per-stroke choreography | no | yes |
| When it looks bad | you find out in production | you can find out at build |
That last row is the one people underrate, and it is the subject of Part 8. Measured over 300 arbitrary icon pairs, 7.7% produce a morph worth shipping. Something has to notice the rest, and only one of these two approaches has a moment at which to notice.
The second one, because it is the less documented of the two and because the consequences are more interesting.
That is a choice, not a verdict. If you need to morph a pair your code will not know until it runs, stop reading and go install a runtime solver. It is the right tool and no amount of build-time cleverness gets you there.
Everything from here is about what becomes possible once correspondence is something you write down. Part 4 is how you write it.
The fork every morphing library stands on, and what each side costs.