Nine parts of theory. Here is the order you would actually write it in, and where the working version of each piece already is.
Everything under lib/morph in this repository is there to be read and copied. It is
deliberately not published as an installable package: the point is that you understand it
well enough to write your own, not that you add one more dependency you cannot see inside.
1. A parser. Take a d string and produce a list of subpaths, each a list of cubic
segments. This is the least glamorous part and the one most likely to have a bug in it.
Test it against a real icon set rather than examples you wrote. Lucide alone exercises the
H shorthand, an implicit lineto after a moveto (X is "M18 6 6 18"), a relative moveto
followed by an implicit relative lineto, negative numbers acting as their own separator
("m21 21-4.34-4.34"), and numbers with no leading zero (".56.56" in Heart).
One trap worth knowing before you hit it: arc flags cannot be tokenised generically.
a1 1 0 011 1 means flags 0 and 1 followed by the point 1,1, and a normal number
scanner reads 011 as eleven. That forces a hand-written scanner rather than
tokenise-then-parse.
2. A rig format and a validator. Slots, per-slot segment counts, and the icons. Write the validator early. It catches slot count mismatches, arity violations and parking cycles at author time, which is much cheaper than finding them by watching an animation.
3. An interpolator. Walk two icons' numbers, produce a frame at t. Twenty lines, and
the thing every other piece is measured against.
4. A serialiser. Frame to d string. The only subtlety is rounding, and it matters
more than it sounds: emit too many decimals and the output triples in size, too few and the
shape visibly steps.
5. The rest is optional. A compiler that bakes frames to CSS keyframes, a driver for reverse and interruption, an importer that fits arbitrary icons into a rig. Each is worth building and none is needed for a working morph.
| Piece | File |
|---|---|
| Parser and arc handling | lib/morph/compile/parse.ts |
| Rig format and validation | lib/morph/core/types.ts, core/geometry.ts |
| Interpolator | lib/morph/core/interpolate.ts |
| Serialiser | lib/morph/core/serialize.ts |
| Procrustes alignment | lib/morph/compile/align.ts |
| Polar sampling | lib/morph/compile/polar.ts |
| CSS emitter | lib/morph/compile/css.ts |
| Importer and confidence | lib/morph/compile/import.ts |
| Runtime driver | lib/morph/runtime/driver.ts |
| React binding | lib/morph/react/index.ts |
Three pieces were copied from other people's projects rather than worked out from scratch: the alignment maths, the conversion from arcs to cubics, and the spring.
All three came from projects released under the MIT licence, which permits copying on one
condition, that you keep the original author's copyright notice with the copy. So a file
called THIRD-PARTY-NOTICES.md sits at the root of this repository listing which pieces
came from whom. If you copy code out of here into your own project, take that file with
you, or update your own copy of it to say where the code came from. That is the whole of
the obligation, and it is the same one that applies to nearly every dependency you already
use.
Do not build a runtime solver. Good ones exist, they are MIT, and being 95% as good is being visibly worse. If you need arbitrary pairs at runtime, use one.
Do not build the studio before the compiler. A visual editor for correspondence sounds like the fun part and is worthless until there is something to correct.
Do not chase fill support. Everything here assumes stroke icons with an open path. Filled shapes bring winding rules and holes, which is a different problem wearing the same hat.
If you remember nothing else: correspondence is the problem, interpolation is arithmetic.
Every hard decision in this book was about which stroke becomes which stroke. Once that is settled, animating between two shapes is averaging numbers, and the browser will do most of it for you.
The rest was working out where to put the answer.