Skip to content

What it morphs

Icons, any SVG markup, words set in a font, or drawings made by hand: either side can be any of them, and the two needn't match.

@cesarvenzac/morphv1.0.0

Either side of a morph can be any of these, and the two needn't be the same kind: an icon can turn into a word, or a filled drawing into a stroked one.

Icons

An icon is drawn with strokes on a 24-unit grid, as Lucide's and Feather's are. It can be:

  • SVG path data: "M5 12h14M12 5v14".
  • A list of it, an entry a stroke or more: ["M5 12h14", "M12 5v14"].
  • Elements as Lucide writes its icons, [tag, attributes][]: anything from the lucide package as it is, and icons in the same shape from elsewhere. path, line, polyline, polygon, circle, ellipse and rect are read; any other element is an error.

Every element is taken as a stroke, so leave out any that isn't drawn, a bounding box for one. Two icons morph on their grid, and each part's width is a share of the icon's stroke width: 1 as drawn, narrowing to 0 as a stroke shrinks away.

From
To
ShufflePoints
Any two of Lucide's icons. Shuffle goes on from each icon it lands on to another.

Any SVG

svg(markup) reads SVG markup into a drawing, with no DOM, so it runs on a server as well as in a page:

import { morph, svg } from "@cesarvenzac/morph";

const heart = svg(`<svg viewBox="0 0 24 24">
  <path fill="#e5484d" d="M12 20.5c-.3 0-.6-.1-.8-.3…Z"/>
</svg>`);

const m = morph(heart, svg(starMarkup));

It reads path, rect, circle, ellipse, line, polyline and polygon, inside groups and nested svgs (placed at their x and y, their own viewBox not applied), their transforms applied, with their fill, stroke, stroke width, fill rule, caps, joins, opacity and currentColor, from attributes or inline styles. A shape with a fill and a stroke is two parts. Gradients and patterns are read as the current colour, and what isn't drawn as a path (text, images, use, clips, masks, filters, defs) is passed over.

Drawings
Drawings of our own, read from their markup: a colour mixing into another, strokes joining a fill, a hole closing, a stroke filling in.

Words

text(font, value) sets a word, or a line, in a font opentype.js has read. Each glyph is a filled part, its holes found as SVG fills them; the package reads no font files itself.

import opentype from "opentype.js";
import { morph, text } from "@cesarvenzac/morph";

const file = await fetch("/fonts/Inter-Regular.woff");
const font = opentype.parse(await file.arrayBuffer());

const m = morph(text(font, "Hello"), text(font, "World"));

size sets the size it's set at, in the drawing's units (24 unless said), and color its colour (the current colour unless said). opentype.js reads TrueType, OpenType and WOFF files, not WOFF2.

Any object shaped like opentype.js's fonts will do: unitsPerEm, ascender, descender, getPath and getAdvanceWidth, and getPaths for a part a glyph (without it, the line is one part). Where its shaping throws, as opentype.js 2.0 does on the features of some fonts, Inter's and DejaVu's among them, the line is set glyph by glyph instead, through charToGlyph and getKerningValue: no ligatures, the same letters.

Type any two words: Inter's letters, each with its holes.

Drawings by hand

A drawing is plain data. svg and text make one, and so can you:

import type { Drawing } from "@cesarvenzac/morph";

const badge: Drawing = {
  viewBox: [0, 0, 24, 24],
  parts: [
    { kind: "fill", d: "M4 4h16v16H4Z", color: "#0a84ff" },
    { kind: "stroke", d: "M8 12h8", width: 2, cap: "round" },
  ],
};

A part is a fill or a stroke, its path data in the drawing's units, with an optional color (any CSS colour), opacity, width (a stroke's), fill rule (nonzero or evenodd), cap and join. A text's em is its size: two drawings that both have one are taken as they are rather than fitted (below).

One canvas for two sides

Two icons are drawn on their 24-unit grid. Otherwise every frame is drawn on the first side's canvas, m.viewBox, the second fitted into it as SVG fits a viewBox: centred, as large as fits. Two drawings that both have an em, as words from text do, are taken as they are instead, centred on each other, on a canvas wide enough for both. An icon among drawings is stroked 2 wide, as Lucide draws it.

Inside, every drawing is scaled to a 24-unit grid, which the morph's distances are tuned for (how far apart its points sit, how round a corner gets halfway), and scaled back as it's drawn: a drawing's own size doesn't change how it morphs.