Files
math-tasks/README.md
T
2026-09-21 00:11:55 +07:00

12 KiB
Raw Blame History

Math Tasks for kids

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.

This repository is the authoring system behind them: a template pipeline, a set of in-browser visual editors, and a deterministic HTML → PDF renderer.

Worksheets

One page from each document — the full PDFs are in output/pdf/.

Collecting Asteroids Cargo Filling Asteroid Splitting
Match asteroids to ships by weight sum Fill a hold to exactly 10 Split N into two unequal parts
Space Route Space Exploration Space Coloring
Route a hex graph under arithmetic constraints Problem cards over a hero scene Full-page line art
More documents of the same types
collecting-asteroids-2 collecting-asteroids-3 collecting-asteroids-4
cargo-filling-2 space-worksheet2 space-worksheet3

How it works

.template.html  +  .data.json   →   generate.mjs   →   .output.html   →   generate-pdf.mjs   →   PDF
   (authored)      (editor)          (per type)        (+ page PNGs)        (Puppeteer)

Two inputs, one output, no hidden state:

  • .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.

Why the output is static HTML

Templates contain no runtime scripting. Nothing computes, generates or lays out content when the page loads. A worksheet must look identical in the browser, in the screenshot and in the PDF, so everything is resolved before render time — otherwise the print artifact and the preview quietly drift apart.


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:

_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 POSTs /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

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/.

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.