update readme
This commit is contained in:
@@ -1,35 +1,243 @@
|
|||||||
# FunPen
|
# FunPen
|
||||||
|
|
||||||
FunPen — themed, skill-targeted printable worksheets for children aged 6-10.
|
Print-ready A4 math worksheets for children aged 6–10, built as HTML and rendered to PDF.
|
||||||
|
|
||||||
## Форматы
|
Every worksheet is a themed scene — spaceships, asteroids, cargo bays — where solving a
|
||||||
|
problem does something in the story: you fill a cargo hold, you charge a battery, you plot
|
||||||
|
a route. The math is ordinary; the wrapping is what makes a child pick the page up.
|
||||||
|
|
||||||
- **PDF** — A4 листы, готовые к распечатке
|
This repository is the authoring system behind them: a template pipeline, a set of
|
||||||
- **HTML** — промежуточный формат для просмотра всех страниц
|
in-browser visual editors, and a deterministic HTML → PDF renderer.
|
||||||
- **JSON** — исходный файл с описанием задания
|
|
||||||
|
|
||||||
## Оформление
|
<!-- GALLERY -->
|
||||||
|
|
||||||
Задания оформлены с лаконичной но элегантной графикой. Каждая строка с примером сопровождается небольшой картинкой — ребёнок "собирает" ресурсы, решая примеры. Помимо мелких картинок используются крупные фоновые изображения.
|
---
|
||||||
|
|
||||||
Для генерации изображений используется сервис **Banatie**.
|
## How it works
|
||||||
|
|
||||||
## JSON-формат задания
|
```
|
||||||
|
.template.html + .data.json → generate.mjs → .output.html → generate-pdf.mjs → PDF
|
||||||
|
(authored) (editor) (per type) (+ page PNGs) (Puppeteer)
|
||||||
|
```
|
||||||
|
|
||||||
В JSON-файле указываются:
|
Two inputs, one output, no hidden state:
|
||||||
|
|
||||||
- **Формат задания (промпт)** — шаблон вида `A * X + B = `, где A, X, B — переменные с диапазонами значений (например A от 1 до 5, X от 1 до 3, B от 4 до 8)
|
- **`.template.html`** is the document — near-final static markup, A4-sized, authored by hand
|
||||||
- **Папка с мелкими картинками** — иконки для каждого примера
|
(with an agent's help) and readable on its own.
|
||||||
- **Пути к крупным картинкам** — фоновые изображения для страниц
|
- **`.data.json`** is a thin overlay produced by the visual editor: positions, rotations,
|
||||||
|
scales, values. **It may not exist** — and when it doesn't, the template *is* the output.
|
||||||
|
- **`generate.mjs`** applies the overlay to the template. It is per task type, deterministic,
|
||||||
|
and the only thing that writes `.output.html`.
|
||||||
|
- **`generate-pdf.mjs`** renders that HTML with Puppeteer: A4, `printBackground`, zero
|
||||||
|
margins, `preferCSSPageSize`.
|
||||||
|
|
||||||
## Рабочий процесс
|
The same pipeline serves both paths — a human dragging things around in the editor, and an
|
||||||
|
agent regenerating the document from source. There is no second code path for "manual" edits.
|
||||||
|
|
||||||
Взаимодействие через Claude Code:
|
### Why the output is static HTML
|
||||||
|
|
||||||
1. Генерация крупных фоновых картинок для задания через Banatie
|
Templates contain **no runtime scripting**. Nothing computes, generates or lays out content
|
||||||
2. Генерация набора мелких картинок для примеров через Banatie
|
when the page loads. A worksheet must look identical in the browser, in the screenshot and
|
||||||
3. Описание идеи задания → генерация JSON-файла
|
in the PDF, so everything is resolved before render time — otherwise the print artifact and
|
||||||
4. Ревью JSON-файла
|
the preview quietly drift apart.
|
||||||
5. Генерация HTML с готовым заданием (с указанием количества страниц)
|
|
||||||
6. Ревью HTML
|
---
|
||||||
7. Генерация PDF для печати
|
|
||||||
|
## Document anatomy
|
||||||
|
|
||||||
|
Each document is up to four files in `tasks/{type}/docs/`:
|
||||||
|
|
||||||
|
| File | Purpose | Written by |
|
||||||
|
|---|---|---|
|
||||||
|
| `{docId}.md` | Problem patterns, page notes, non-obvious details | authoring (human + agent) |
|
||||||
|
| `{docId}.template.html` | The document itself — static A4 markup | authoring (human + agent) |
|
||||||
|
| `{docId}.data.json` | Editor overlay: positions, values, transforms | the editor's save endpoint |
|
||||||
|
| `{docId}.output.html` | Template with overlay applied | `generate.mjs` (not committed) |
|
||||||
|
|
||||||
|
The `.md` file never drives generation. It is context — *why* this page looks the way it
|
||||||
|
does, which patterns repeat, what must not be broken next time.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task types
|
||||||
|
|
||||||
|
Seven types, fourteen documents. Each type owns its folder, its rules, its generator and —
|
||||||
|
where the layout is hand-tuned — its editor.
|
||||||
|
|
||||||
|
| Type | What the child does | Editor |
|
||||||
|
|---|---|---|
|
||||||
|
| `space-exploration` | Solves problem cards decorated with icons, over a hero scene | — |
|
||||||
|
| `collecting-asteroids` | Matches asteroids to cargo ships so weights sum to capacity | ✓ |
|
||||||
|
| `asteroid-splitting` | Splits an asteroid of weight N into two unequal parts | ✓ |
|
||||||
|
| `cargo-filling` | Splits an asteroid to fill a hold to exactly 10 — addition across ten | ✓ |
|
||||||
|
| `space-route` | Follows a hex-graph route under arithmetic constraints between nodes | ✓ |
|
||||||
|
| `crystal-battery` | Two characters take turns filling a 10-cell battery — complements of 10 | — |
|
||||||
|
| `space-coloring` | Colours a full-page line-art scene | — |
|
||||||
|
|
||||||
|
Every type folder carries its own `CLAUDE.md` with layout rules, asset conventions and
|
||||||
|
generation constraints. **Type-specific rules live there, never in the root file** — that
|
||||||
|
separation is what keeps one type's quirks from leaking into another.
|
||||||
|
|
||||||
|
```
|
||||||
|
tasks/{type}/
|
||||||
|
CLAUDE.md — rules for this type
|
||||||
|
editor.html — visual editor (if the type needs one)
|
||||||
|
scripts/ — generate.mjs + validators
|
||||||
|
docs/ — the documents
|
||||||
|
temp/ — diffs and page screenshots (not committed)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The editor
|
||||||
|
|
||||||
|
Four of the seven types have a visual editor at
|
||||||
|
`http://localhost:3300/tasks/{type}/editor.html?file={docId}`.
|
||||||
|
|
||||||
|
They exist because some things cannot be authored blind: whether an asteroid overlaps a
|
||||||
|
ship's nose, whether a row breathes, whether the page is balanced. Those are seen, not
|
||||||
|
specified.
|
||||||
|
|
||||||
|
All four share `src/editor/editor-core.js` — worksheet loading, page navigation, selection,
|
||||||
|
toolbar, toast, save, export. Type-specific logic (what is draggable, what a keypress means,
|
||||||
|
how state serialises) stays in each `editor.html`.
|
||||||
|
|
||||||
|
### Keyboard
|
||||||
|
|
||||||
|
Precision work is done from the keyboard, not the mouse. In `collecting-asteroids`:
|
||||||
|
|
||||||
|
| Key | Effect |
|
||||||
|
|---|---|
|
||||||
|
| `←` `→` `↑` `↓` | Move by 1 |
|
||||||
|
| `Shift` + arrows | Move by 5 |
|
||||||
|
| `[` `]` | Rotate (5°, or 1° with `Shift`) |
|
||||||
|
| `-` `+` | Change the asteroid's value — the sprite rescales to match |
|
||||||
|
| `,` `.` | Change the base scale for this asteroid type on this page |
|
||||||
|
|
||||||
|
The last one matters: it edits a *class* of objects, not one object. Tuning one asteroid is
|
||||||
|
fiddling; tuning every asteroid of that kind on the page is design.
|
||||||
|
|
||||||
|
### Millimetres, not pixels
|
||||||
|
|
||||||
|
The worksheet is a physical object. The editor establishes the scale from the rendered page:
|
||||||
|
|
||||||
|
```js
|
||||||
|
_mmToPx = pages[0].getBoundingClientRect().width / 210; // 210mm = A4 width
|
||||||
|
```
|
||||||
|
|
||||||
|
Positions are reasoned about in millimetres and rendered in pixels, so what is nudged on
|
||||||
|
screen is what comes out of the printer.
|
||||||
|
|
||||||
|
### Save
|
||||||
|
|
||||||
|
1. Editor `POST`s `/api/save-edits` with `{ taskType, docId, data }`
|
||||||
|
2. Server writes `docs/{docId}.data.json`
|
||||||
|
3. Server diffs it against the previous state → `temp/{docId}.diff.json`
|
||||||
|
4. Server runs `generate.mjs` → fresh `.output.html` + per-page PNGs
|
||||||
|
|
||||||
|
The diff is the point. After a session of dragging, the question "what actually changed?"
|
||||||
|
has a file for an answer instead of a guess — reviewable by a human, readable by an agent.
|
||||||
|
|
||||||
|
Two details that stop the editor from fighting its own tooling: live-reload is disconnected
|
||||||
|
inside editor pages (the page holds unsaved DOM state, a reload would discard it), and the
|
||||||
|
save endpoint de-duplicates rapid repeat POSTs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Creating a new document of an existing type
|
||||||
|
|
||||||
|
No script — this is deliberate. Documents differ in ways a generator cannot guess.
|
||||||
|
|
||||||
|
1. Copy the previous document's `.template.html` and `.md`, incrementing the number
|
||||||
|
2. Update the `.md` with the new problem patterns and page notes
|
||||||
|
3. Edit the template: new problems, different hero images, different footers
|
||||||
|
4. Add a card to the type's `index.html`
|
||||||
|
5. Run `generate.mjs`, then `generate-pdf.mjs`
|
||||||
|
|
||||||
|
`.data.json` starts absent and is created only if the document is opened in the editor.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Creating a new task type
|
||||||
|
|
||||||
|
1. `tasks/{type-name}/`
|
||||||
|
2. Pick a theme, record it in the type's `CLAUDE.md` as `**Theme:** {name}`
|
||||||
|
3. Write `CLAUDE.md` — layout rules, asset conventions, generation constraints
|
||||||
|
4. Write `scripts/generate.mjs` — template + data → output. **It must call `postGenerate()`**,
|
||||||
|
which produces the per-page screenshots the rest of the workflow depends on
|
||||||
|
5. Write `editor.html` against `src/editor/editor-core.js` if the layout needs hand-tuning.
|
||||||
|
`tasks/asteroid-splitting/editor.html` is the reference implementation — the smallest one
|
||||||
|
6. Add a card to `tasks/index.html` and create the type's own `index.html`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Worksheets fail visually — an overlap, a clipped footer, a number sitting off its asteroid —
|
||||||
|
and no assertion catches that. So every generation ends in a picture:
|
||||||
|
|
||||||
|
- `postGenerate()` runs `take-screenshots.mjs`, writing one PNG per page to `temp/`
|
||||||
|
- Stale screenshots are deleted first, so what is on disk always matches the current output
|
||||||
|
- Those PNGs are read back and checked; Chrome DevTools MCP covers interactive checks
|
||||||
|
- Editors are verified end to end — load, edit, save, regenerate — not just eyeballed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scripts are tools, not actors
|
||||||
|
|
||||||
|
Every script in this repository reads input, computes, and either prints JSON to stdout or
|
||||||
|
writes one specific file. Scripts do not chain, do not modify documents, and do not decide
|
||||||
|
anything.
|
||||||
|
|
||||||
|
```
|
||||||
|
run → node tasks/collecting-asteroids/scripts/verify.mjs --ships 10,10,14 --asteroids 3,2,1,4,5
|
||||||
|
stdout ← { "valid": false, "errors": ["ship 2 overflow by 3"] }
|
||||||
|
then → read the output, decide, edit the template
|
||||||
|
```
|
||||||
|
|
||||||
|
Four writers are allowed, and the list is closed: `generate.mjs` (template + data → output),
|
||||||
|
`generate-pdf.mjs` (the final artifact), `take-screenshots.mjs` (page PNGs), and the editor's
|
||||||
|
save endpoint. Everything else is read-only.
|
||||||
|
|
||||||
|
This is the boundary the project is built on. Generation, layout and PDF rendering are fully
|
||||||
|
deterministic and reproducible; judgement — what a worksheet should teach, how a page should
|
||||||
|
feel, whether an image is right — stays outside that line, where it can be reviewed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Themes and assets
|
||||||
|
|
||||||
|
Assets live in `assets/themes/{name}/`, each theme with its own `CLAUDE.md` listing available
|
||||||
|
images, palettes and usage rules. The space theme (`assets/themes/nms/`) carries the current
|
||||||
|
task types: spaceships, asteroids, planets, cargo bays, footer panoramas.
|
||||||
|
|
||||||
|
Images are generated through the Banatie API via a `/gen-image` skill, using existing images
|
||||||
|
from the same folder as references so a new asteroid looks like it belongs beside the old
|
||||||
|
ones. Newly generated files land in `raw/` and move up a level once processed — usually
|
||||||
|
after `remove-bg.mjs` flood-fills the white background away.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Running it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm dev # Tailwind watch + live-reload server on :3300
|
||||||
|
pnpm pdf -- tasks/{type}/docs/{docId}.output.html
|
||||||
|
```
|
||||||
|
|
||||||
|
Preview index: `http://localhost:3300/tasks/index.html` → type page → document.
|
||||||
|
Generated PDFs are committed under `output/pdf/`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build:css # build Tailwind once, minified
|
||||||
|
pnpm remove-bg -- <file|dir> # strip white background from generated PNGs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
Plain HTML and CSS for the documents, Tailwind for the utility layer, vanilla JavaScript for
|
||||||
|
the editors, Node for the scripts. Puppeteer renders the PDFs, sharp processes the images.
|
||||||
|
No framework, no build step for the worksheets themselves — a document is a file you can open.
|
||||||
|
|||||||
Reference in New Issue
Block a user