Type text and render it onto a lined notebook page — either in a handwriting font of your choice, or as AI-synthesized cursive that draws your actual words stroke by stroke using an Alex Graves recurrent neural network — then view the page as a 3D crumpled sheet of paper lit with Three.js.
Live demo: https://handgen.onrender.com
Hosted on Render's free tier, so the first request after idle takes ~30 s to wake the server. Once it's up, generation is immediate.
The UI was redesigned from the original prototype into a futuristic, mobile-responsive "handwriting studio" — a dark glassmorphism theme with neon cyan/violet accents — generated with Google Stitch (via its MCP server) and then hand-integrated so all the original functionality kept working. The Django app now serves this single, unified UI.
| Original prototype | Redesigned (Stitch) |
|---|---|
![]() |
![]() |
| Original on mobile | Redesigned on mobile |
|---|---|
![]() |
![]() |
The Stitch design concept the integration was based on is saved at
screenshots/stitch-mockup.png.
- AI handwriting synthesis ("Use AI"). Renders your typed text as genuine, flowing cursive using a pretrained Alex Graves handwriting-synthesis network — run entirely in your browser via onnxruntime-web, so the demo needs no GPU and no per-request server compute. Text is split into short chunks, each laid out along the ruled lines and wrapped to fill the page. A Bias slider controls neatness (higher = steadier, more legible strokes). Progress shows in an off-canvas status pill so the paper stays clean.
- Handwriting fonts. Eight Google Fonts pre-loaded (Pacifico, Caveat, Architects Daughter, Cookie, Covered By Your Grace, Gochi Hand, Great Vibes, Homemade Apple).
- Upload your own font. Drop in a
.ttfor.otfand the text re-renders in it. The font stays local to your browser — nothing is uploaded. - Adjustable font size and skew. Sliders for both.
- Notebook-paper canvas. Red header and margin rules, evenly-spaced blue horizontal rules, gray paper background, and a "Date://___" stamp.
- 3D crumpled paper view. Click Render 3D to map the 2D notebook canvas onto a 99×99 plane in Three.js, with vertex displacement from a mix of Perlin, Simplex, and Worley noise to simulate folds and wrinkles. Orbit by dragging inside the 3D view; press Close or Esc to exit.
- Responsive — works on desktop and mobile.
The AI mode implements the handwriting model from Alex Graves' Generating Sequences With Recurrent Neural Networks (2013, arXiv:1308.0850). The paper describes two related networks, and this project contains both:
- Prediction network — learns the dynamics of a pen. Given the strokes so far, it predicts a probability distribution over the next pen move. It produces convincing handwriting-like motion but does not spell out specific text.
- Synthesis network — adds a learned attention window that slides along the input characters, so the strokes actually transcribe the requested words. This is what powers the "Use AI" button.
Handwriting is a sequence of tiny pen moves (Δx, Δy, pen_up). Predicting the single next point with squared error fails — after any stroke the next move is genuinely ambiguous, and the average of the options is a dead blur. So instead the network, at every timestep, outputs the parameters of a mixture of 2-D Gaussians plus a pen-lift probability:
e— probability the pen lifts (sigmoid),π₁…π_M— mixture weights (softmax, sum to 1),μ, σ, ρper component — each Gaussian's centre, spread, and correlation.
It's trained to maximise the likelihood of the true next move (negative-log-likelihood loss). To draw, you sample a move from this distribution, feed it back in, and repeat — the pen draws itself. Graves' bias b ≥ 0 (the UI's Bias slider) shrinks every σ → σ·e^(−b) and sharpens the mixture choice, trading variety for neatness.
docs-free and serverless on the client side: onnx_synthesis.js drives a pretrained synthesis model (synthesis_network_52.onnx) step-by-step through onnxruntime-web:
- 3 stacked LSTM layers (400 units each),
- a soft attention window of 10 Gaussian components whose centre
κonly ever moves forward along the one-hot character string (φ(t,u) = Σ αᵏ exp(−βᵏ(κᵏ−u)²)), telling the net which character it is currently drawing, - a 20-component bivariate mixture-density output + pen-lift, sampled each step (Box–Muller + Cholesky for the correlated Gaussian).
The model and inference recipe are adapted from the MIT-licensed pytorch-handwriting-synthesis-toolkit by Evgenii Dolotov (the only change is a built-in bivariate sampler replacing the multivariate-normal dependency). Layout and two legibility tricks live in CanvasScript.js: a trailing drift-trim removes the stray slant the model leaves after a word, and a "sacrificial successor" trick — append a throwaway glyph, then cut the strokes at the exact step the attention window hands off to it — keeps the final letter of every chunk complete.
HandGen/HandWriter/handwriting_rnn/ is a pure-NumPy implementation of the prediction network — no PyTorch, no TensorFlow. The LSTM forward/backward pass, the mixture-density layer, the NLL loss, the hand-derived backpropagation, an Adam optimiser, and gradient clipping are all written out explicitly, and the analytic gradients are confirmed against finite differences to ~1e-10 by gradcheck.py. It's a 1-LSTM-layer (100 hidden units), 20-mixture model trained on procedurally-generated strokes. The goal was to understand every equation in the paper by building it, not importing it. Full write-up: handwriting_rnn/README.md.
| Engine | Where | What it does |
|---|---|---|
| Pretrained synthesis (ONNX) | in the browser | Primary. Writes your actual typed text. |
| Pretrained synthesis (PyTorch) | server /writer/api/generate |
Fallback if the browser path is unavailable and PyTorch is installed. |
| From-scratch NumPy prediction | server /writer/api/generate |
Last-resort fallback (handwriting-like motion, doesn't transcribe). |
The server endpoint (views.py → generate) is only used as a fallback; on the live demo the in-browser ONNX path is what runs, so the model executes on your machine and the server just serves static files.
- Frontend: vanilla JS, jQuery, Tailwind CSS (CDN), Three.js r84, noisejs.
- AI inference: onnxruntime-web (WASM) in the browser, running a pretrained Graves synthesis model. Uses multi-threaded WASM when the page is cross-origin isolated, single-threaded otherwise (always functional).
- Design: Google Stitch generated the glassmorphism design system ("Digital Ink Synthesis").
- Backend: Django 3.1 (Python 3.7+), serving the UI and an
/api/generatefallback endpoint. Deployed on Render viarender.yamlwith gunicorn + WhiteNoise;SECRET_KEY/DEBUGread from environment variables.
- Text → 2D canvas (fonts). On Write,
CanvasScript.jspaints the gray paper, red margin/header rules and blue lines, then renders each line in the chosen font withctx.fillText. - Text → 2D canvas (AI). With Use AI on, the text is chunked and each chunk is generated stroke-by-stroke by the synthesis network, then scaled to the ruled-line height, placed on the next line, and wrapped — drawing real connected cursive.
- 2D canvas → 3D texture. On Render 3D, the canvas becomes a
THREE.CanvasTextureon aMeshPhongMaterialover aPlaneGeometry(5, 5, 99, 99). - Procedural crumpling. Each of the plane's 10 000 vertices gets a Z offset from a Worley fold-distance term + low-frequency Perlin + mid-frequency Simplex + five octaves of Simplex (Brownian) detail.
- Camera + lighting. A
PerspectiveCamera, anAmbientLight, a whitePointLight, andOrbitControlsscoped to the renderer canvas.
pip install "Django>=3.1,<3.2"
cd HandGen
python manage.py migrate # creates the local SQLite db
python manage.py runserver
# then visit http://127.0.0.1:8000/The "Use AI" button runs in the browser, so it works locally with no extra setup. (Multi-threaded WASM only engages when the page is cross-origin isolated; locally it falls back to single-threaded — slower but fully functional.)
.
├── HandGen/ # Django project
│ ├── manage.py
│ ├── HandGen/ # project config (settings, urls, wsgi)
│ └── HandWriter/ # the single Django app
│ ├── views.py # UI view + /api/generate fallback endpoint
│ ├── templates/HandWriter/base.html# the unified (redesigned) UI
│ ├── static/assets/ # JS engine + AI model:
│ │ ├── CanvasScript.js # 2D canvas + AI layout engine
│ │ ├── onnx_synthesis.js # in-browser Graves synthesis (onnxruntime-web)
│ │ ├── synthesis_network_52.onnx # pretrained synthesis weights
│ │ ├── three.js / OrbitControls.js / logic.js / main.css
│ ├── handwriting_rnn/ # from-scratch NumPy Graves prediction net (gradient-checked)
│ └── synthesis/ # vendored pretrained PyTorch model (server fallback)
│
├── screenshots/ # before/after UI images used by this README
├── render.yaml # Render.com Blueprint (gunicorn + WhiteNoise)
├── .mcp.json # Stitch MCP server config (remote HTTP endpoint)
├── refresh-stitch-token.ps1 # refreshes the hourly Stitch access token
└── README.md
- AI synthesis is best-effort. The pretrained model occasionally renders an uncommon word as a scribble or a slightly malformed glyph; the layout retries obvious collapses and trims drift, but it isn't perfect on every word.
- Style slider is not wired — only the Bias slider currently affects AI output.
- Skew slider value is read into a JS variable but never applied to the canvas transform.
SECRET_KEYhas a public dev fallback inHandGen/HandGen/settings.py. Production readsDJANGO_SECRET_KEYfrom the environment (Render injects a generated one); the committed fallback must never be used for a real deployment.
- Wire the Style slider to a learned style/primer for the synthesis network, and the Skew slider to
ctx.transform. - Voronoi crease network for sharper, more paper-like folds.
- PNG export of the 3D rendered view.
- More paper presets: graph, dotted, parchment, legal-pad yellow.
- Migrate Three.js off r84.
The futuristic UI was generated through Google Stitch's MCP server. To drive Stitch yourself:
- Have a Google Cloud project with the Stitch API enabled, and
gcloudinstalled and authenticated (gcloud auth login). .mcp.jsonpoints your MCP client at the official remote endpointhttps://stitch.googleapis.com/mcpusing a bearer token from theSTITCH_TOKENenv var.- The token expires hourly — run
./refresh-stitch-token.ps1to refresh it, then restart your MCP client.
No credentials are committed; the service-account key and token are gitignored / env-only.
Not yet specified. Until a license is added, default copyright applies — please do not redistribute without permission.



