A playground for learning Chinese characters through their parts, their stories and their sounds. It started as a remake of the Hanly ad in this folder: 3,500 characters on a sunflower, learned parts-first, with a running "% of written Chinese" counter.
npm install
npm run dev # http://localhost:5317
npm run data # rebuild public/data/*.json (downloads sources into data/raw/ on first run)| View | Idea |
|---|---|
| Garden 园 | The video, live. Every character is a seed on a sunflower spiral, placed in learning order. Watch the path grows it from 0 to 3,500 characters with arcs from each character to its parts. My garden shows what you know. Type a part (氵, 木, 口…) to light up every character that contains it. |
| Learn 学 | Your next character on the path as a story card. Each part flashes its own colour as the character assembles, then the glyph settles into plain ink (hover or click to pull it apart again). The card shows what the parts mean, the real origin, a Roots strip of how the character looked on Shang oracle bones, Zhou bronzes, Warring States silk and Qin seals (with a "watch it evolve" time-lapse), your memory hook, stroke order and two writing drills. Spaced reviews work in both directions: see it and recall it, or read the meaning and write it from memory. |
| Forge 合 | Little Alchemy for characters. Combine parts (日 + 月) to discover what they make (明 bright). Discoveries join your inventory, so you can chain them: 木 + 目 → 相, then 相 + 心 → 想. |
| Sound families 声 | About two thirds of common characters are meaning part + sound part. Each family is drawn as a flower around its sound part (青 qīng → 清 请 情 晴 睛 精), coloured by how faithfully the sound survives, with a "guess the sound" quiz. |
| Read 读 | Paste any text to see what you can read, and the handful of characters that would unlock most of it. One click queues them, with their parts, at the front of your path. |
| Idioms 典 | Forty famous chengyu (画蛇添足, 守株待兔, 塞翁失马…), each a whole story in four characters: word-by-word tiles marked known/new, the meaning, a four-panel story strip, the story retold, the original classical line X-rayed for the characters you know (with a translation), a modern example sentence, and one click to queue its new characters. Story quiz shows a strip and asks which idiom it is. |
Progress (known characters, reviews, your stories and AI suggestions) is saved in the browser's localStorage for http://localhost:5317, which is why the port is fixed in vite.config.ts.
scripts/build-data.mjs builds a graph of the 3,500 most frequent characters plus the ~300 building blocks they are made of, then orders it with a greedy ratio rule. It repeatedly picks the character whose missing pieces (itself plus any parts you don't know yet) add the most text coverage per item learned, and learns those pieces parts-first. Parts always come before the characters built from them, so every new character is made of things you already know:
一 不 人 大 以 丨 冂 口 日 白 丿 勹 丶 勺 的 疋 是 个 中 二 月 有 …
Pure frequency order reaches 50% coverage after 152 characters. Parts-first reaches it after 195 characters plus 39 building blocks, and in exchange no character ever arrives as an unexplained shape. Run node scripts/build-data.mjs --report to print the curves.
The ✨ Suggest a story button asks a model on OpenRouter for a vivid scene built from the character's parts, plus a sound hook. It runs through a tiny endpoint in the Vite dev server (server/story-plugin.ts), which builds the prompt from the local character data, so your key never reaches the browser. To turn it on, open .env.local (ignored by git) and paste your key from openrouter.ai/keys:
OPENROUTER_API_KEY=sk-or-...Save the file and the dev server restarts by itself. If .env.local is missing, copy .env.example.
The default model is anthropic/claude-opus-5.5 (a suggestion costs well under a cent). To use another, uncomment the OPENROUTER_MODEL= line in .env.local. Any model works; ones that list structured outputs on openrouter.ai/models return the cleanest results.
Keeping the key safe. The key is read only by the dev server (Node) and sent only to OpenRouter; it never appears in code sent to the browser or in npm run build output. The endpoint refuses requests from other websites open in your browser, and Vite rejects forged hosts. Three habits keep it that way:
- Never rename the variable with a
VITE_prefix: Vite ships everyVITE_variable to the browser. - Don't start the dev server with
--host, which would let anyone on your network spend your credits. - Set a credit limit on the key at openrouter.ai/keys.
Deploying to Vercel. api/story.ts runs as a Vercel Function (the dev server uses the same code through server/story-plugin.ts). Add OPENROUTER_API_KEY (and optionally OPENROUTER_MODEL) under the project's Settings → Environment Variables, then redeploy. On a public URL anyone can call the endpoint with a script, so a credit limit on the key matters even more there; Vercel's Deployment Protection can also keep the whole site private.
Suggestions are cached per character in your browser. Writing or editing the story yourself works better than reading one, so treat suggestions as a starting point.
- Make Me a Hanzi: decompositions, etymology hints, pinyin, definitions and stroke-to-part matches (see its COPYING file for licences).
- Jun Da's Modern Chinese character frequency list: how much of written Chinese each character covers.
- Hanzi Writer and its stroke data (loaded from jsDelivr) for stroke animations and writing quizzes.
- Ancient forms: Wikimedia Commons Ancient Chinese Characters Project, after Richard Sears (hanziyuan.net); public domain.
scripts/build-origins.mjsindexes the files (one PetScan request) and the app loads the SVGs straight from upload.wikimedia.org. Simplified characters are looked up under their traditional form via OpenCC (Apache-2.0). - Short English labels for function words and building blocks are hand-written in
scripts/glosses.mjs. - Idioms (
src/lib/idioms.ts): the classical quotes come from public-domain texts (Zhuangzi, Han Feizi, Shiji, Zhan Guo Ce…); retellings, story strips, translations and example sentences are original. Pinyin was checked against chinese-xinhua (MIT).
Ancient-form images load from upload.wikimedia.org. To keep a local copy instead (about 4,300 small SVGs, fetched slowly and politely), run node scripts/build-origins.mjs --download.
Check each source's terms before redistributing the generated data.
docs/research.md surveys existing methods (Heisig, Chineasy, Mandarin Blueprint, Outlier, Dong Chinese, HanziHero, Hanly…), the learning-science evidence and the open data sources, and ends with a prioritised feature list. The short version, and how it shaped the app:
- Explaining a character's parts when you first meet it, and learning characters that share a part together, both help. That's why the path goes parts-first and the Families view exists.
- Animated origins beat static ones, so the Roots strip has a time-lapse.
- Colour coding left on permanently can make plain characters harder to recognise later. Part colours therefore appear briefly or on demand, and the glyph rests in plain ink.
- Mnemonics you make yourself stick better than ones you're given, and every mnemonic fades without spaced retrieval. Hence the editable memory hook (AI drafts are labelled as invented) and spaced reviews that test recall and handwriting.