Skip to content

API reference

Every function and type the package exports, entry by entry: the core, React and the DOM.

@cesarvenzac/morphv1.0.0

Three entries: the core, which runs anywhere, @cesarvenzac/morph/react and @cesarvenzac/morph/dom. The core touches neither React nor the DOM, its types included.

The core

morph(from, to, options?)

A morph between two sources. options is a way, "soft" (the default), "even", "index" or Steps, or { way?, extra? }.

function morph(from: Source, to: Source, options?: Method | Steps | Options): Morph;

type Morph = {
  slots: number; // parts in every frame, some empty at the ends
  viewBox: Box; // the canvas every frame is drawn on
  at(t: number): Part[]; // the frame at t, 0 to 1
  shapes(t: number): Shape[]; // the same, as anchors and handles
  points(t: number): Vec[][]; // each part's outline points
};

svg(markup)

A drawing from SVG markup, read without a DOM: What it morphs says what's read.

function svg(markup: string): Drawing;

text(font, value, options?)

A word or a line set in a font, each glyph a filled part.

function text(font: Font, value: string, options?: TextOptions): Drawing;

type TextOptions = { size?: number; color?: string };

motion(source, options?)

A drawing at rest on source that springs to the next when told: pure and immutable, each method returning a new motion.

function motion(source: Source, options?: MotionOptions): Motion;

type MotionOptions = Options & { spring?: Spring };
type Motion = {
  readonly morph: Morph;
  readonly t: number;
  readonly velocity: number; // in t a second
  readonly target: 0 | 1;
  readonly source: Source; // headed for, or resting on
  readonly settled: boolean;
  readonly viewBox: Box;
  toward(next: Source): Motion;
  step(seconds: number): Motion;
  jump(): Motion;
  frame(): Part[];
};

attributesOf(part, options?)

A part as a <path>'s attributes, for setAttribute or markup. An icon's part is strokeWidth wide (2 unless said) at a width of 1.

function attributesOf(part: Part, options?: PaintOptions): Attributes;

type PaintOptions = { strokeWidth?: number };
type Attributes = {
  d: string;
  fill: string;
  stroke: string;
  "fill-opacity"?: string;
  "fill-rule"?: "evenodd";
  "stroke-width"?: string;
  "stroke-linecap"?: "butt" | "round" | "square";
  "stroke-linejoin"?: "miter" | "round" | "bevel";
  opacity?: string;
};

svgOf(morph, t, options?)

The frame at t as an SVG document. color is what the current colour is; width and height give it a size, without which it fills what holds it.

function svgOf(m: Morph, t: number, options?: SvgOptions): string;

type SvgOptions = PaintOptions & {
  color?: string;
  width?: number;
  height?: number;
};

nodesData(nodes, closed, digits?)

Nodes back as path data, three decimals unless said: for drawing anchors and handles, or a frame from shapes.

function nodesData(nodes: Node[], closed: boolean, digits?: number): string;

EVENLY, SOFTLY, PRESS

Soft's steps all off and all on, and the spring that settles without overshoot, { stiffness: 260, damping: 32 }.

Types

type Source = Icon | Drawing;
type Icon = string | readonly string[] | readonly IconElement[];
type IconElement = readonly [tag: string, attributes: Record<string, string | number | undefined>];

type Drawing = { parts: DrawingPart[]; viewBox: Box; em?: number };
type DrawingPart = {
  kind: "fill" | "stroke";
  d: string;
  color?: string;
  opacity?: number;
  width?: number;
  rule?: "nonzero" | "evenodd";
  cap?: "butt" | "round" | "square";
  join?: "miter" | "round" | "bevel";
};
type Box = [x: number, y: number, width: number, height: number];

type Part = {
  d: string;
  width: number;
  fill?: number; // drawings: 0 a stroke, 1 a fill
  color?: string;
  opacity?: number;
  cap?: "butt" | "round" | "square";
  join?: "miter" | "round" | "bevel";
};
type Shape = { nodes: Node[]; closed: boolean; width: number; holes?: Contour[] };
type Contour = { nodes: Node[]; closed: boolean };
type Node = { at: Vec; in?: Vec; out?: Vec };
type Vec = [number, number];

type Method = "soft" | "even" | "index";
type Steps = { shape: boolean; whole: boolean; round: boolean; curves: boolean };
type Extra = "auto" | "split" | "gather";
type Options = { way?: Method | Steps; extra?: Extra };
type Spring = { stiffness: number; damping: number };

type Font = {
  unitsPerEm: number;
  ascender: number;
  descender: number;
  getPath(text: string, x: number, y: number, size: number): FontPath;
  getPaths?(text: string, x: number, y: number, size: number): FontPath[];
  getAdvanceWidth(text: string, size: number): number;
  charToGlyph?(char: string): FontGlyph;
  getKerningValue?(left: FontGlyph, right: FontGlyph): number;
};
type FontGlyph = {
  advanceWidth?: number;
  getPath(x: number, y: number, size: number): FontPath;
};
type FontPath = { toPathData(decimals?: number): string };

React

@cesarvenzac/morph/react: client components and hooks, React 18 or later.

<MorphIcon>

An icon, or any drawing, that springs to the next whenever icon changes. Any <svg> prop goes on its <svg>; it's hidden from assistive tech unless it's named.

type MorphIconProps = SVGProps<SVGSVGElement> & {
  icon: Source;
  size?: number | string; // 24
  strokeWidth?: number; // an icon's, 2
  spring?: Spring;
  way?: Method | Steps;
  extra?: Extra;
  reducedMotion?: boolean; // prefers-reduced-motion
};

useMorph(from, to, options?)

A morph kept across renders, made again only when a side or the options say something new: icons written inline are the same icons.

useMorphing(source, options?)

MorphIcon's motion without its <svg>: { parts, viewBox, motion }, springing to each new source.

<MorphFrame>

A morph drawn at t: morph, t, size and strokeWidth, any other prop on its <svg>.

The DOM

@cesarvenzac/morph/dom, for a page without a framework.

mount(svg, source, options?)

function mount(svg: Element, source: Source, options?: MountOptions): Mounted;

type MountOptions = MotionOptions & PaintOptions & { reducedMotion?: boolean };
type Mounted = {
  to(next: Source): void; // spring there
  set(next: Source): void; // show it at once
  readonly motion: Motion;
  stop(): void;
};

draw(svg, parts, viewBox?, options?)

Parts into an <svg>: a <path> each, made and taken away as their number changes, drawn after whatever else the <svg> holds, each with no attribute left over from the frame before.

Cost

Making a morph is the costly part: about 8 milliseconds for a pair of Lucide's icons (15 for nine pairs in ten), and about 100 for two words of a dozen letters, measured in Node on one core of a 2.1 GHz server. Do it once and keep the morph, not the sources. A frame then takes a quarter of a millisecond for icons, and 2 to 4 for words.