You have written <path d="..." /> before and let your eyes slide past the string. This
chapter is about that string, because morphing is entirely an operation on it.
Here is Lucide's hamburger menu:
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 5h16" />
<path d="M4 12h16" />
<path d="M4 19h16" />
</svg>Three separate <path> elements, one per bar. The d attribute is a sequence of drawing
commands for an imaginary pen:
M4 5 move the pen to x=4, y=5 without drawingh16 draw a horizontal line 16 units to the rightThe coordinate space comes from viewBox="0 0 24 24", so the icon is drawn on a 24 by 24
grid regardless of what size it renders at. Everything in this book happens on that grid.
The origin is the top-left and y grows downward. This trips up everyone who last saw a coordinate plane in a maths class, where y goes up. Here y=5 is the top bar and y=19 is the bottom one, which is why the three paths above read in the order they do. Get this backwards and every morph you author will be vertically mirrored and you will not immediately see why.
The alphabet is small. M move, L line, H and V for horizontal and vertical lines,
C cubic curve, Q quadratic, A arc, Z close. Two more exist and matter later: S
and T, the smooth forms of C and Q, which drop the first control point and infer it
by reflecting the previous one. Design tools emit them constantly, so anything reading real
icon data has to handle them. Lowercase means the coordinates are relative to wherever the
pen currently is, which is why h16 means "16 to the right of here" rather than "to
x=16".
Strip the letters and a path is a list of numbers. That is the entire reason morphing is possible at all: if two shapes are lists of numbers of the same length, you can walk from one to the other by averaging.
Here is the top bar of the menu becoming the first diagonal of the cross. Drag t and
watch the four numbers walk from one row to the other, and the shape do exactly the same
thing.
That is the entire mechanism. Feed the in-between numbers back into a path and you get a real shape. Do it sixty times a second and you have an animation. That is all a morph is.
Now the part that looks like waste and is not.
A cubic Bézier is a curve with four points: a start, an end, and two control points that
pull the line between them. C takes six numbers, the two controls and the endpoint.
A straight line needs two points. So why would anyone store a straight line as a cubic?
Because of this rule, which is the hinge the whole project turns on:
M4 5h16 and M7 7L17 17 are M,h and M,L. Different letters, so no interpolation.
Rewrite both as cubics and they become M,C and M,C. Same structure, same number count,
and now they interpolate. A straight line stored as a cubic just has its control points
sitting on the line, which draws identically and costs four numbers instead of two.
M 4 5 C 9.333 5 14.667 5 20 5 the same bar, as a cubicThe endpoint is unchanged, at x=20, because the bar still ends where it ended. The two
controls sit at one third and two thirds along it. That spacing is not arbitrary: it is the
one placement that leaves the curve's uniform, so the pen moves at a
constant rate from end to end. Any two points on the line draw the same picture, and a
lopsided pair would make the midpoint of the animation land somewhere other than the
midpoint of the bar. lineToCubic in lib/morph/core/geometry.ts is those two thirds.
So every shape gets normalised to M followed by some number of C commands, always, even
when nothing is curved. It is a normal form, the same idea as a database normal form: pay a
little redundancy so that comparison is trivial.
<path> has a dWorth stopping on, because it decides what this technique can touch at all.
d belongs to <path> and to nothing else. <rect>, <circle>, <ellipse>, <line>,
<polyline> and <polygon> are the other six primitives, and they carry their own geometry
in their own attributes: cx/cy/r, x/y/width/height, points. None of them has
a d to interpolate, so none of them can be morphed the way this book morphs things.
This is not hypothetical. Lucide ships 453 icons containing a <circle> or a <line>, and
circle itself is one <circle> element with no path data anywhere in it. Any of those has
to be converted to an equivalent <path> before it can enter a rig at all.
One detail worth knowing because it bites people. The A arc command cannot be
interpolated meaningfully, and the elliptical-arc parameterisation has flags in it that are
booleans rather than numbers. Averaging a boolean is not a thing.
Every serious morphing pipeline converts arcs into cubics before doing anything else. A
circle becomes four cubic segments, which approximates it to well under a tenth of a pixel
at icon sizes. If you see an A in a path you are about to morph, it is going to become
curves first.
A shape is a list of numbers with letters between them. Morphing is averaging those numbers. The letters have to match or the browser refuses. And to make the letters match, everything becomes a cubic.
Which leaves exactly one question, and it is the hard one: given two lists of numbers, which entry pairs with which?