You have written <path d="..." /> before and let your eyes slide past the string.
By the end of this page you will be able to read a string like that out loud, and then look at two of them and say whether a browser will animate between them or snap straight from one to the other. Nothing warns you about the snap, so that second skill is the one worth having. It is also why a straight bar in an icon is almost never stored as a straight line.
Here is the hamburger menu from Lucide, the icon set every example on these pages is taken from:
<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, and five attributes between them. A block of
text cannot show you what any of those five does, so here they are one at a time. Pick one and
the drawing changes to show what that attribute controls, and only that.
A 24 by 24 grid, whatever size it renders at.
Every number in every d is a coordinate on this grid. These three are the same markup, byte for byte, at three CSS sizes.
Three of the five are painting instructions and never change while a morph runs. The two that
matter are d and viewBox, and the rest of this page is about them.
d is a list of drawing commands for an imaginary pen. A letter names a command and the
numbers after it are that command's arguments. So M4 5h16, read left to right, says: move
the pen to x=4, y=5 without drawing anything, then draw a horizontal line 16 units to the
right. That one string is the whole top bar.
viewBox="0 0 24 24" is what makes those numbers mean anything. It declares a coordinate
space 24 units wide and 24 tall, so x=4 is four units in from the left edge of a grid that is
24 across, whatever size the icon ends up rendering at. Ship the same icon at 16 pixels and at
200 and d is identical both times.
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 curve, Q quadratic, A arc, Z close. A lowercase letter means the
same command measured from wherever the pen is, so h16 is "16 to the right of here"
rather than "go to x=16".
Two more commands exist and matter later. S and T are the smooth forms of C and Q:
they drop the first control point and infer it by reflecting the previous one, which keeps
a chain of curves continuous without restating the same numbers.
Design tools emit them constantly, so anything reading real icon data has to handle them even though no icon on these pages is authored with one.
Strip the letters out and numbers are all that is left. That is the entire reason morphing is possible: two shapes that are lists of numbers of the same length can be walked from one to the other by averaging, which is what it means to them.
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.
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 one rule:
M4 5h16 is a move then a horizontal line, M,h. M7 7L17 17 is a move then a line,
M,L. Different commands, so no animation, and no way to get one without changing the
strings.
Both are below. Replace them with two d strings of your own and this reads the commands out
of each, then says whether a browser would animate between them and what it would take if
not.
1 subpath, 1 cubic
1 subpath, 1 cubic
Not as written, but they will once both are cubics
The letters differ, so the browser refuses today. They resolve to the same number of segments across the same number of subpaths, so converting every command to C makes the structures match and nothing has to be invented. This is what the importer does first.
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, and that spacing is not arbitrary: it is the one placement that makes the pen travel at a constant rate from end to end.
Any two control points on the line draw the same picture, so the choice is invisible when the shape is at rest. It stops being invisible the moment the shape moves. A lopsided pair leaves the curve's uneven, so the halfway point of the animation lands somewhere other than the halfway point of the bar.
lineToCubic in lib/morph/core/geometry.ts is those two thirds.
That claim about the spacing is the one thing on this page you cannot check by reading, so
here it is to play with. The dots sit at evenly spaced values of t, which is what a browser
walks when it animates. Drag a handle and watch them bunch.
The controls are on the thirds, and the samples are evenly spaced.
Nine dots at evenly spaced values of t, which is exactly what a browser walks when it animates. On the thirds they land evenly spaced along the bar, so the pen covers equal ground in equal time.
drag a handle toward either endhalfway sample at 50.0%
So every shape gets normalised to M followed by some number of C commands, always, even
when nothing is curved. A straight line stored as a curve costs six numbers where two would
have done, and in exchange any two shapes can be compared entry by entry without asking what
kind of command each one holds.
<path> has a dThis decides which icon sets you can morph without doing work first.
d belongs to <path> and to nothing else. SVG has six other shape elements and each keeps
its geometry in attributes of its own, so none of them has a d to interpolate and none of
them can become a shape of a different kind.
The six are <rect>, <circle>, <ellipse>, <line>, <polyline> and <polygon>, and
their geometry lives in x/y/width/height, cx/cy/r, x1/y1/x2/y2 and
points. You could animate those attributes directly, and for a circle growing into a bigger
circle that is the right tool. What you cannot do is animate a circle into a line, because
there is no shared list of numbers to average: one has a centre and a radius, the other has
two endpoints.
This is not hypothetical. Lucide ships 496 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 at all.
If you see an A in a path you are about to morph, it is going to become curves before
anything else happens, because an arc cannot be averaged the way the other commands can.
A takes seven parameters and two of them are flags: large-arc and sweep pick which of
the four possible arcs between two points you meant. They are booleans, and averaging a
boolean is not a thing. Halfway between "the long way round" and "the short way round" is
not an arc, it is a type error the format has no way to express.
So every serious morphing pipeline converts arcs to cubics before doing anything else. A circle becomes four cubic segments, which at icon sizes approximates it to well under a tenth of a pixel.
So a shape is a list of numbers with letters between them, and you can read both halves now.
The letters say what kind of drawing each group of numbers is, and two paths animate only when
the letters agree, which is the whole reason everything gets converted to cubics first. Hand
two d strings to the checker above and you already know what it is going to tell you.
One question is left and it is the hard one. Two lists of numbers, the same length, the letters matching: which entry pairs with which? Nothing on this page can answer it, because the answer is not in the geometry. That is Part 2.
How to read a path by eye, and why straight lines get stored as curves.