Turn typed text into handwriting. One HTML file, no build step, no server, no dependencies. Your text never leaves the browser.
Open app/index.html. That is the whole install.
Live: https://handtext.nerdapplabs.com/ · Tool: https://handtext.nerdapplabs.com/app/
- Eight handwriting fonts, size and line height
- Six ink colours; eight paper colours, white through legal yellow and manila
- Ruling: ruled lines, grid, small grid, dot grid, blank; optional margin line and spiral binding
- Page sizes: A4, Letter, square 1080, wide 1200x630
- Select any text and give it its own font, ink colour, highlighter, bold, italic, underline or strikethrough
- Draw hand-sketched boxes, ovals, circles, lines and arrows straight onto the page
- Pages flow automatically when the text overruns
- Export PDF or PNG for every page, or copy page one to the clipboard
A handwriting font alone reads as a font, because every e is identical and every baseline is perfectly flat. Every character gets its own slight rotation, its own vertical drift, its own gaps either side, and its own opacity, which reads as pen pressure.
Only the last of those is exposed, as Ink variation. The other three are fixed constants near the top of the script. They were sliders once; four knobs to describe one idea is worse than a good default, and nobody opens a tool wanting to choose a slant jitter value.
All of it runs through a seeded generator with a fixed seed, so a given piece of text always renders the same way. Re-export it tomorrow and you get the identical page.
Text is laid out on a <canvas>: words are measured and wrapped, then each character is drawn individually inside a save/restore with its own rotation, offset and alpha. Paper ruling is drawn underneath. Export is canvas.toBlob, so what you download is exactly what you see.
The editor is a contenteditable surface. Selecting text and choosing a font, ink or highlighter wraps the selection in a <span> carrying data-font, data-color or data-hl. At render time the DOM is walked into a flat list of characters, each one carrying the nearest ancestor's attributes. So styling survives editing, because the browser maintains it, and the canvas just reads whatever is there.
Highlighter is drawn as a pass behind the text: consecutive characters sharing a colour become one rectangle at 50% alpha, so overlapping strokes do not stack into a darker band. Underline and strikethrough work the same way but on top, each drawn as a single wobbly stroke across the whole run rather than one per character. Per-character strokes look like a dotted line, which is the giveaway.
Bold and italic go into the canvas font string, so they affect measurement as well as drawing. Get that wrong and styled text drifts out of its line.
Pick box, oval, circle, line or arrow, then drag on a page. Shapes belong to the page you drew them on and survive edits above them. Their positions are stored as fractions of the page rather than pixels, so changing page size or column count keeps them where you put them.
Nothing is drawn as a clean geometric primitive. Every stroke is laid down twice with a slight wobble, and ellipses are walked as a polygon with a jittered radius, which is what makes a circle read as drawn rather than computed. Each shape stores its own random seed, so it stays exactly the same shape on every re-render instead of squirming whenever you change a slider.
Click a shape with no tool armed to remove just that one; the cursor turns into a pointer when you are over one. undo steps back through every shape change, including a clear all shapes. Undo works on snapshots rather than by popping the shape list, which is what makes clearing recoverable — a pop-based undo cannot restore a clear, because after the clear there is nothing left to pop.
Text is laid out once into lines, then split by how many lines fit a column. Set one or two columns; a column fills top to bottom before the next one starts, and a full page flows onto a new sheet. Add text and pages appear; delete it and they disappear.
To decide where a column ends yourself, click insert break to drop a marker at the cursor. It pads the rest of the current column with blank lines, so the following text starts at the top of the next column, or the next page if you are on one column. A break already sitting on a boundary adds nothing.
PDF is written byte by byte, with no library. Each page is exported as a JPEG and embedded as a DCTDecode image XObject, with a hand-built xref table. That is about 40 lines and avoids a 300KB dependency for a file format we only need one corner of. Page boxes are converted from 96dpi pixels to 72dpi points, so A4 comes out as a true 595x842pt page.
Latin text is drawn one grapheme at a time, which is what produces the per-letter jitter. That approach cannot be used for scripts whose glyphs are shaped across several code points, because drawing the pieces separately detaches matras and pulls conjuncts apart.
So Devanagari, Bengali, Tamil, Thai, Arabic and their neighbours are drawn a whole word at a time. Shaping survives, and the jitter applies per word rather than per letter, which still reads as handwriting.
Of the handwriting fonts here only Kalam carries Devanagari letters. The others are Latin only, and the app says so when it sees Devanagari in text set in one of them.
Fonts load from Google Fonts, the only network call. Cache them locally if you want it fully offline. PDF pages are JPEG at quality 0.92, so this is not a vector PDF and the text is not selectable. Very long documents render every page eagerly, which gets slow past a few dozen.
It is one file per page and no build step, so the whole loop is: edit, refresh, done. Things worth adding:
- Drag a shape after placing it
- Freehand pen
- Vector PDF, so the text stays selectable
- Local font caching, which would make it work fully offline
MIT.