A web-based and Tauri-wrapped SVG editor focused entirely on drawing font
glyphs. One glyph is one document is one SVG, exported as u_xxxx.svg for
import into FontForge. It is a glyph drawing tool, not a font editor —
kerning, OTF compilation, and metrics editing are left to FontForge.
Detail lives in CLAUDE.md — the architectural invariants, the per-phase history, and the feature-by-feature status table. This README deliberately stays a summary and defers there rather than duplicating it (the duplication is what let this file go stale before).
Drawing — Illustrator-style bezier pen, freehand pencil (auto-simplified/smoothed), and primitives (rectangle / ellipse / line / polygon / triangle) with Shift-constrain and draw-from-centre. Scissors, knife and eraser for cutting paths. Node editing with lasso and marquee select, a transform box (Ctrl+T) with a draggable rotation pivot, align/distribute, and snapping to grid or to anchors/paths.
Strokes (the core idea) — you draw a path, then shape it with a non-destructive stroke: the centerline stays editable and the filled outline is derived at render/export. Uniform, broad-nib calligraphic, and swept-brush models; per-end caps (butt / round / rectangle / serif / drop) with A/B algorithm variants; bracketed serif feet and teardrop ink-pool terminals; width and nib-angle profiles drawn in a graph editor; plus experimental halftone and dash/dot/custom-SVG brushes. Presets are saved in a user-managed library. Path corners (round / chamfer / inverted) apply non-destructively too, and any stroke can be manually expanded to editable outlines.
Layers & booleans — Illustrator-style layers (lock, hide, reorder, colour-coded). A non-destructive Pathfinder between two layers: Union / Subtract / Intersect / Exclude, plus Blend (an A→B stepped morph). Curve-exact via Paper.js, computed at render/export time, so both operand layers stay fully editable. Destructive merge/flatten is available when you want to bake.
Colour — per-contour fill and stroke colour are independent, with opacity, two-stop linear gradients (including along-path for strokes), saved palettes, and imported-SVG colour. Decorative only: FontForge flattens it on import.
Glyphs & output — a code-point-sorted sidebar with live thumbnails, onion-skinning against other
glyphs, adjustable typography guides and per-glyph advance width, and a text-preview window. Export
every glyph as u_xxxx.svg (or just one) with a universal scale %, an optional flat-black silhouette
mode, and synthetic bold/italic. SVG import lands on a new layer.
Workspace — dark / light / paper themes with a custom accent, movable and resizable panels, a
command registry with fully rebindable shortcuts, right-click menus everywhere, per-glyph undo/redo,
and autosave plus a portable .glphdrft project file that moves between web and desktop.
Not yet implemented: dynamic alignment "smart guides", a custom cap designer, procedural/L-system brushes, per-node corner styles (per-path corners are shipped), and i18n. See CLAUDE.md → "Future seams" for how entangled each is and whether it's safe to build.
- React 18 + TypeScript (strict,
noUncheckedIndexedAccess,exactOptionalPropertyTypes) — UI and component layer - Zustand — shared app state. Undo/redo is per-glyph and hand-rolled (
src/state/history.ts, 200 steps), so Ctrl+Z only ever changes the glyph you're looking at - Paper.js — the live geometry engine behind the
GeometryServiceinterface: curve-exact booleans and stroke expansion. Stroke outlines are built in-house (PaperGeometryService), not by an offset library - fflate — in-browser zip for the bulk SVG export
- marked — renders the bundled Markdown in the Information modal
- LocalForage (web) / Tauri v2 FS plugin (desktop) behind a single
StorageServiceinterface — the Tauri adapter is always lazy-loaded so the web bundle never references@tauri-apps - Vite for dev/build; Vitest for the pure-engine unit tests (46 files, 433 tests); Tauri v2 for the desktop shell
Paper.js note: Paper.js is stable but hasn't had a major release since 2022. It sits entirely behind
src/engine/geometry/geometryEngine.ts— swapping it for another library is a one-line change in that file with no ripple into stores or UI.
Build target is ES2020, so any browser from early 2020 or later:
| Browser | Minimum version |
|---|---|
| Chrome / Edge | 80 |
| Firefox | 72 |
| Safari / iOS Safari | 13.1 |
The canvas renders as SVG — no WebGL, no GPU requirement. Works on mobile browsers and low-powered hardware. Data is stored in IndexedDB (supported everywhere since ~2014).
| OS | Minimum version | Notes |
|---|---|---|
| Windows | 10 (2015) | Requires WebView2, already bundled on Win 10/11. Win 7/8 not supported. |
| macOS | 10.13 High Sierra (2017) | |
| Linux | Ubuntu 20.04 / Fedora 32 / Arch | Needs WebKitGTK 4.0 |
CPU: x86_64 and aarch64 (Apple Silicon, ARM Linux) only. No 32-bit desktop builds.
Web build — works on any OS, just needs Node.js:
npm install
npm run build # produces dist/ — deploy to any static hostDesktop build (Tauri) — produces the native installers/binaries: .exe on Windows,
.deb + .AppImage + .rpm on Linux, .dmg + .app on macOS. The project folder is fully
portable (no hardcoded paths) — move it to the target OS, install the prerequisites once, then build.
⚠️ Tauri builds for the OS you run it on — there is no practical cross-compilation. A Windows.exemust be built on Windows, the Linux bundles on Linux, and the macOS bundles on macOS. To produce every artifact from a single push, use CI (see "All platforms at once" below).
1. One-time prerequisites per machine — Node.js + npm and Rust (via rustup)
on every OS, plus:
| OS | Also install |
|---|---|
| Windows | Microsoft C++ Build Tools (VS Build Tools or Visual Studio → "Desktop development with C++"). WebView2 is preinstalled on Win 10/11. NSIS (for the -setup.exe) is fetched by Tauri automatically. |
| macOS | Xcode Command Line Tools (xcode-select --install). |
| Linux (Debian/Ubuntu) | sudo apt install build-essential curl wget file libssl-dev libwebkit2gtk-4.1-dev libgtk-3-dev librsvg2-dev libayatana-appindicator3-dev — and rpm if you want the .rpm bundle. (Fedora: the dnf webkit2gtk4.1-devel/@development-tools groups; Arch: pacman webkit2gtk-4.1 base-devel. See the Tauri Linux prerequisites.) |
2. The Tauri CLI is already a devDependency (@tauri-apps/cli@^2), wired to the tauri script —
so npm install is all you need, then use npm run tauri <cmd> (or npx tauri <cmd>).
If you'd rather have it globally: cargo install tauri-cli --version "^2" (then cargo tauri <cmd>).
3. (Windows / macOS only) Generate the platform icon set — this repo ships only PNG icons, and the
Windows/macOS installers need an .ico/.icns. Run once (uses a square ≥1024 px source PNG):
npx tauri icon path/to/icon.png # writes icon.ico + icon.icns and updates tauri.conf.json4. Build:
npm install
npm run tauri build # bundles for THIS OS (runs the web build first)
# pick specific bundles, e.g. on Linux (omit rpm unless rpmbuild is installed — see Troubleshooting):
npm run tauri build -- --bundles deb,appimageWhere the files land — under src-tauri/target/release/bundle/ (raw binary at
src-tauri/target/release/glyph-draft[.exe]):
| OS | Artifacts |
|---|---|
| Windows | nsis/Glyph Draft_0.1.0_x64-setup.exe (installer .exe); msi/…msi if WiX is present |
| Linux | deb/glyph-draft_0.1.0_amd64.deb · appimage/glyph-draft_0.1.0_amd64.AppImage · rpm/glyph-draft-0.1.0-1.x86_64.rpm |
| macOS | dmg/…dmg · macos/Glyph Draft.app (universal) |
All platforms at once — use a GitHub Actions matrix with
tauri-apps/tauri-action on windows-latest,
ubuntu-latest, and macos-latest runners; it builds each OS's artifacts in parallel and attaches them
to a release. This is the only way to get the Windows .exe without a Windows machine.
could not compile 'brotli'/the trait 'alloc::Allocator<u8>' …(alloc-no-stdlib). A transitive dep (brotli, pulled by Tauri) was incompatible with a newer Rust. Fix: refresh the lockfile —cd src-tauri && cargo update— then rebuild (brotli≥ 8.0.4 compiles cleanly). Commit the updatedsrc-tauri/Cargo.lockso it doesn't recur.icon …/32x32.png is not RGBA. Tauri requires RGBA PNG icons (with an alpha channel); a plain RGB PNG fails the build. Fix: regenerate the icon set from a square source —npx tauri icon src-tauri/icons/icon.png(writes RGBA32x32/128x128/icon.png+icon.ico/icon.icnsand updatestauri.conf.json). Commit the regeneratedsrc-tauri/icons/.- Build compiles, then fails at the
.rpmstep.tauri.conf.jsonsetsbundle.targets: "all", but.rpmneedsrpmbuild. Eithersudo apt install rpm, or build only the formats you want:npx tauri build --bundles deb,appimage. failed to run linuxdeploy(AppImage step).linuxdeployis itself an AppImage and FUSE-mounts to run; in sandboxed/headless/CI shells (no/dev/fuseor restricted user namespaces) that fails even whenlibfuse2is installed. Fix: tell the AppImage tools to extract instead of mount —APPIMAGE_EXTRACT_AND_RUN=1 npx tauri build --bundles appimage(addNO_STRIP=1if the strip step also errors). The first run also downloadslinuxdeployfrom GitHub, so it needs network once.
npm install
npm run dev # web app on http://localhost:5173
npm run typecheck # tsc --noEmit (strict)
npm test # vitest run — pure-engine unit tests
npm run test:watch # vitest in watch mode
npm run build # production web build (typechecks first)
npm run preview # serve the built dist/ locally
# Desktop (requires the Rust toolchain + platform prerequisites —
# see "Building for each platform" above):
npm run tauri devThe full, annotated file tree is in CLAUDE.md → "Source Structure". This is the orientation-level view.
src/
engine/ # framework-free domain logic (unit-tested, no React/DOM)
viewport/transform.ts # the ONLY place the world<->screen Y-flip lives
snapping/snap.ts # pure snap-to-grid quantization (world units)
geometry/ # path/winding/bezier math, corners, blend, profiles
# GeometryService.ts: the swappable interface
# geometryEngine.ts: the single swap point
# PaperGeometryService.ts: live impl (Paper.js, curve-exact)
# PolygonGeometryService.ts: test-only impl (flattens curves)
state/ # Zustand stores (+ persistence lifecycle)
viewportStore.ts # zoom/pan/grid/theme — NOT undoable
documentStore.ts # glyphs/layers — plain data
history.ts # PER-GLYPH undo/redo (custom, not zundo)
editorStore.ts # live ephemeral session state — NOT undoable
storage/ # StorageService + Local/Tauri adapters + versioned file formats
features/
canvas/ # viewport, tool controller, Stroke/Color panels, fill pipeline
tools/ # pen, pencil, select, lasso, shapes, scissors/knife/eraser
layers/ # LayersPanel, Pathfinder UI, merge/flatten
glyphs/ # GlyphSidebar, thumbnails, glyph-set templates
clipboard/ # copy/cut/paste-in-place (layer-aware)
import/ export/ # SVG import; glyphToSvg + bulk export behind ExportService
project/ # portable .glphdrft export/import (web/desktop seam)
preview/ info/ settings/
commands/ # registry.ts — single source of actions + keybinds
components/ # shared controls, menus, error boundary
types/ constants/ styles/ utils/
src-tauri/ # Tauri v2 desktop shell
-
Coordinate system. World space is in font units, Y-up, baseline at
y = 0(descenders negative). Screen space is CSS px, Y-down. The single Y-flip is encoded only inengine/viewport/transform.ts; every other module goes through it. -
Split stores.
viewportStore(camera + UI) is separate fromdocumentStore(the glyph model) and is deliberately not part of the undo/redo history — so Ctrl+Z never undoes a pan or zoom. The document model is plain serializable data (no class instances), which is what makes snapshot history and cross-glyph paste-in-place reliable. History is per-glyph, so an undo can never silently revert a glyph you aren't looking at. -
Geometry is a service, not the data model. Paper.js never owns the scene graph — the canonical glyph stays plain Zustand state rendered to native SVG, and heavy vector math goes through
GeometryService. Swapping the engine is a one-line change inengine/geometry/geometryEngine.ts.
- Pan: scroll/trackpad, Space+drag, or middle-mouse drag
- Zoom: Ctrl/Cmd+scroll (or trackpad pinch) — zooms to cursor; Ctrl/Cmd+0 fit, Ctrl/Cmd+1 actual size, Ctrl/Cmd+2 zoom to selection
- Tools: V select · Q lasso · P pen · B pencil · C scissors · K knife · X eraser · M/E/L/G/T shapes
- Edit selection: Arrow keys nudge (Shift = ×10), Ctrl/Cmd+D duplicate, Ctrl/Cmd+T transform box; flip H/V and reverse path from the right-click menu
- View modes: Ctrl/Cmd+Shift+O toggles wireframe/outline; a "final" mode previews the exported look
- Grid, snap, onion skin, guides, themes: the View and Settings top-bar menus
- Every shortcut is rebindable in Settings → Keyboard shortcuts