Correspondence has to be decided somewhere. There are two somewheres, and picking one decides almost everything else about what you end up building.
Ship a solver. When a morph is requested, take the two shapes, work out the pairing, and animate. Roughly what a good one does:
Five nouns, and none of the work they name is visible in the finished animation. So here it is one step at a time, on the pair this book 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. Give every icon in a set the same fixed number of slots,
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.
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. Roughly 5% of arbitrary icon pairs produce a morph worth shipping. Something has to notice the other 95%, 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.