live glyph editing that leaves the rest alone


npm ↗
GitHub ↗
TypeScriptopentype.jsReact + Vanilla JSNo server

Click any character and reshape its outline. glyphShaper writes back only the glyph you edited. The tables that hold the font’s kerning, ligatures, hinting programs and variable axes are copied untouched, so the rest of the font behaves as its designer made it. Two limits: an edit that adds or removes a point stops that one glyph following a variable font’s axes, and fonts with CFF outlines are rebuilt whole. The result loads as a dynamic @font-face override, and every instance of that character on the page re-renders. No server, no export, no page reload.

Interactive demo — edit a letter, then switch the write path

Loading…

Loading…

0%

How it works

Parse, edit, patch

glyphShaper uses opentype.js to read each glyph’s outline as path commands — moveTo, lineTo, quadraticCurveTo — and shows them as draggable control points. When you apply an edit to a TrueType font, only that glyph is re-encoded into the glyf table; GSUB, GPOS, the hinting programs and the variation tables are copied from the original file untouched. Fonts with CFF outlines are rebuilt with font.toArrayBuffer() instead, which drops those tables; the demo above lets you compare the two. In a variable font, an edit that adds or removes a point removes that glyph’s own variation data (it keeps its default shape at every axis setting); every other glyph still varies.

Dynamic @font-face injection

The regenerated buffer becomes a Blob URL. A <style> element is injected (or replaced) in <head> with a new @font-face rule pointing at that URL. The browser immediately re-renders any element using that font‑family — headings, body text, buttons — with the modified glyph outline.

Bezier control points

Glyph outlines use cubic and quadratic Bézier segments. Anchor points (filled circles) are on-curve endpoints. Handle points (outlined circles) are off-curve control handles that pull the curve without touching it. Moving handles reshapes the curve smoothly; moving anchors shifts the segment endpoint. Pointer capture keeps the drag working even when the cursor leaves the SVG area.

Ephemeral by design

Every edit lives entirely in the browser’s memory. The original font file is never written to disk. Refreshing the page resets everything. This makes glyphShaper ideal for live demos, design explorations, and teaching moments — low commitment, instant feedback, no pipeline.

Usage

TypeScript + React · Vanilla JS

Drop-in editor component

import { useGlyphFont, GlyphShaperEditor } from '@overpunch/glyphshaper'

const { font } = useGlyphFont('/fonts/MyFont.ttf')

// fontFamily must match the CSS font-family name applied to the child element
<GlyphShaperEditor font={font} fontFamily="MyFont" text="Heading">
  <h1 style={{ fontFamily: 'MyFont' }}>Heading</h1>
</GlyphShaperEditor>

Load from uploaded File

import { useGlyphFont } from '@overpunch/glyphshaper'

// Pass a File object from an <input type="file"> onChange handler
// Note: useGlyphFont supports TTF, OTF, and WOFF1 only.
// For WOFF2, use parseFont() directly with a woff2Decompressor callback.
const { font, loading, error } = useGlyphFont(file)

// Pass null to reset the hook to idle state
const { font } = useGlyphFont(null)

Low-level — vanilla JS

import {
  parseFont, getGlyphCommands, setGlyphCommands,
  fontToBlob, applyFontBlob,
} from '@overpunch/glyphshaper'

const res    = await fetch('/fonts/MyFont.ttf')
const buffer = await res.arrayBuffer()
const font   = await parseFont(buffer)

// Read glyph path commands
const cmds = getGlyphCommands(font, 'A')

// Modify — e.g. shift all anchor y values up by 50 units
const shifted = cmds.map(c =>
  c.type !== 'Z' ? { ...c, y: c.y + 50 } : c
)

// Write back and apply
setGlyphCommands(font, 'A', shifted)
const url = applyFontBlob('MyFont', fontToBlob(font))
// All elements using font-family: MyFont now show the shifted A

API

glyphShaper API
NameDescription
parseFont(buffer, decompressor?)Parse an ArrayBuffer (TTF, OTF, or WOFF1) into a GlyphFont handle. For WOFF2 input, pass a woff2Decompressor callback — the function throws without it.
getGlyphCommands(font, char)Return a deep copy of the path commands for a character.
setGlyphCommands(font, char, cmds)Write modified commands back into the font object (mutates in place).
fontToBlob(font)Serialise the font to a Blob (OTF binary).
applyFontBlob(family, blob, existingUrl?, options?)Inject a @font-face override; returns the Blob URL for later cleanup. Pass existingUrl to revoke the previous Blob URL and prevent memory leaks. options accepts fontWeight and fontStyle.
revokeFont(url)Revoke the Blob URL and remove the override style element.
commandsToPathD(cmds)Convert a PathCommand[] to an SVG path d string for use in a <path> element.
useGlyphFont(source)React hook — accepts a URL string, File, or null (resets to idle). Supports TTF, OTF, and WOFF1; for WOFF2 use parseFont() directly. Returns {font, loading, error}.
GlyphShaperEditorDrop-in React component — handles font loading, glyph state, and bezier editing in a single wrapper. Accepts font, fontFamily, and text.
GlyphSvgEditorLower-level React component exposing just the SVG bezier editor. Use when you want to manage font loading and state yourself.

Font format support

glyphShaper accepts TTF, OTF, WOFF1, and WOFF2. WOFF2 support requires passing a woff2Decompressor callback to parseFont() — the demo uses wawoff2 (a WASM brotli decoder) for this. The useGlyphFont hook does not accept a decompressor and therefore does not support WOFF2; use parseFont() directly if you need WOFF2.

no-code

Use it in Figma

The same effect, no build step — drop it straight into your design tool.

Figma · beta

Part of the Type Tools Figma plugin ↗ — Plugins → Development → Import plugin from manifest, run Type Tools, and pick this tool. Here it ports faithfully as a canvas operation.