A web app that shapes an Arabic word or phrase with HarfBuzz, draws it in a calligraphic font and animates it being written — then exports SVG, PNG and video.
Pen mode writing بسم الله الرحمن الرحيم in Amiri, gold on navy
A single-page app called Midad (مداد): type an Arabic word or short phrase, see it rendered in a calligraphic font, and watch it being written by hand from right to left. The brief made correct Arabic shaping non-negotiable, naming three test cases: the lam-alef لا, the word الله, and the phrase بسم الله الرحمن الرحيم. It also asked for three animation modes, a bilingual right-to-left interface, exports, a debug view, unit tests and a README, built in stages: static render, then Sweep, Trace, Pen, then export.
Build "Midad" (مداد) — a web app that renders an Arabic word or short phrase in a calligraphic font and animates it being written by hand, right to left, then exports it.
## Stack
- Vite + TypeScript, no UI framework (vanilla + small modules). Single-page app.
- harfbuzzjs (HarfBuzz WASM) for Arabic shaping and glyph outlines. Do NOT use opentype.js for shaping — its Arabic support is incomplete.
- Render everything as SVG.
## Fonts
- Load fonts from a local /public/fonts directory, driven by a fonts.json manifest: { id, label, labelAr, file, style: "kufic" | "naskh" | "ruqaa" | "thuluth" | "diwani" }.
- Ship with free OFL fonts: Reem Kufi (Kufic), Amiri (Naskh), Aref Ruqaa (Ruqaa). Include their OFL license files.
- Thuluth and Diwani slots exist in the manifest but are empty; I'll drop in licensed font files later. The UI should hide styles with no font file.
## Pipeline
1. Shape the input with HarfBuzz (direction RTL, script Arab, language ar) to get glyph IDs, advances, and offsets. Ligatures and contextual forms must be correct (test لا, الله, بسم الله الرحمن الرحيم).
2. Extract each glyph's outline as an SVG path, positioned per the shaping output. Auto-scale to fit the canvas.
3. Classify glyphs into "bodies" vs "marks" (dots, harakat, shadda). Use the font's GDEF mark class where available; fall back to a size/position heuristic for separate small contours sitting above or below the baseline band.
## Animation modes (user-selectable)
- **Sweep**: each glyph body is revealed by a mask sweeping right to left, one glyph after another with slight overlap. Marks drop in last with a short fade and scale-in.
- **Trace**: animate the outline with stroke-dashoffset, then fill. Sequence glyphs right to left, marks last.
- **Pen** (the hero mode): for each glyph, rasterize to an offscreen canvas, skeletonize with Zhang-Suen thinning, trace the skeleton into ordered polylines (start from the rightmost endpoint, prefer continuing in the same direction at junctions), smooth them, then animate a thick round-capped stroke along the polylines as an SVG mask revealing the real filled glyph. If skeletonization produces garbage for a glyph (too many branches), fall back to Sweep for that glyph only.
- Speed control (total duration 1–8s) and an "ink" easing that slows slightly at stroke starts.
## UI
- Bilingual Arabic/English, RTL by default, with a language toggle.
- Big centered canvas; a text input (dir="rtl"); a style picker showing each font's name rendered in itself; a mode picker; a speed slider; Replay; ink color and paper color pickers (presets: black on cream, gold on deep navy, white on black).
- Clean, minimal, generous whitespace. Light and dark themes.
- Keyboard: Enter to replay.
## Export
- SVG (final frame, outlined paths, no font dependency).
- PNG at 1x/2x/4x.
- Animated export: WebM via MediaRecorder on a canvas mirror of the animation. Add MP4 only if the browser supports it; don't add ffmpeg.wasm.
## Quality bar
- Correct shaping is non-negotiable. Add a /debug route that shows glyph bounding boxes, body/mark classification, and the skeleton overlay for the current word.
- Vitest unit tests for: shaping output order (RTL), mark classification on a few known words, and the skeleton-to-polyline ordering.
- README covering how to add a font (drop the file in /public/fonts, add a manifest entry) and the font-licensing note.
Build it incrementally: shaping + static render first, then Sweep, then Trace, then Pen, then export. Show me the working app after each stage.The three modes on the same word, الرحمن:
Sweep, Trace and Pen, top to bottom
The three bundled fonts writing مداد in Pen mode, each with one of the three colour presets:
Reem Kufi, Amiri and Aref Ruqaa, top to bottom
The interface is Arabic-first and right to left, with an English toggle, light and dark themes, a style picker that shows each font's name written in that font, a speed slider (1–8 s), colour presets and pickers, and export buttons for SVG, PNG (1×/2×/4×), WebM and — in this Chrome — MP4.
The controls, in Arabic, with the new dots setting
A /debug page shows how the app sees each word: glyph boxes in writing order, which pieces are letter bodies and which are dots or harakat (red: declared by the font, amber: detected by size and position), and the pen paths with their start points and directions.
Debug view of the bismillah in Amiri
The pen order is a judgement call. Taking "start at the rightmost end" literally made the slanted alef of لا start at its foot and go up, because the foot sits a little further right; the start was biased toward the top so both strokes are written downward, as a scribe would:
Pen paths for lam-alef: both strokes start at the top
In the first version, every dot and haraka was held back until the entire phrase had been written. After trying it, the request was:
just one thing, the dots have to appear either after the word, not the whole sentence, and the things like that, or as the letter goes, does that make sense, commit also before making any changes
The working version was committed first, and then both behaviours were added as a setting, Dots & harakat (النقاط والحركات): after each word (the default) or with each letter.
The same phrase, في بيتنا شجرة زيتون, three seconds in — the original build on the left, the new one on the right:
The full before/after, recorded from the original commit and the new one:
Before (top) and after (bottom)
And the two new options against each other, in Pen mode:
Dots after each word (top) vs with each letter (bottom)
Recording the clip above with Pen mode on في بيتنا شجرة زيتون crashed the app: two Amiri letters and one Aref Ruqaa letter in that phrase made the pen-path step fail. None of the earlier test words had hit it. It was reproduced with a unit test built from the exact letter shape, fixed, and then checked again on 378 letters across all three fonts (a pangram, a fully vowelled line, ligatures and Persian letters) with no crashes. The clips in this report were recorded after the fix.