Writing Exercise Builder (Any Language)

An agent skill that builds a kid-facing handwriting-practice canvas for ANY script — Hangul, Latin, Cyrillic, Japanese kana/kanji, Chinese, Arabic, numbers, and more. Kids trace letters, syllables, or whole words with real stroke-order guidance and per-stroke scoring, then prove recall in an optional listen-and-write quiz. You tell it the language and the targets; it authors the canvas, links it into a kid-callable skill, and delivers it as a task.

Adopt

Authoring helper that builds a kid-facing stroke-order WRITING-PRACTICE canvas for ANY language or script — Hangul, Latin, Cyrillic, Japanese kana/kanji, Chinese, Arabic, numbers, etc. Practice letters, syllables, or whole words with stroke-order tracing + a listen-and-write recall quiz, real per-stroke scoring, and gems. Use when a parent asks for a handwriting / tracing / "write the letters" activity in any language. Produces the canvas HTML, links it into a kid-callable skill, and delivers it as a task.

How it works

  1. 1Gather inputsConfirm the language/script, the target letters/syllables/words, the child + age, gems per completion, and whether to include trace mode, listen-and-write mode, or both.
  2. 2Get stroke dataFor Chinese/Japanese kanji, paste upstream HanziWriter {strokes,medians} data; for every other script, author centerline polylines in correct stroke order and convert with the built-in generator. Verify each glyph visually before shipping.
  3. 3Build the canvasAuthor the HTML: HanziWriter loaded via the canvas-CDN proxy, local charDataLoader, trace + listen-and-write modes, avatar signals, sprout.state resume, exactly one sprout.complete, and design-system buttons.
  4. 4Ship itcanvas.create (preview then commit), skill.write linking the canvas as kid-callable with an age range, checkpoint with the parent on gems/cadence/which child, then deliver via task.create or heartbeat.create.

Instructions

  1. You are building a kid-facing **writing-practice canvas** — stroke-order tracing plus an optional listen-and-write recall quiz — for {{input.language}}, practicing: {{input.targets}}. Works for ANY script. Follow this procedure exactly; it encodes hard-won constraints.
  2. ​
  3. ## 1. Gather inputs (ask the parent ONLY for what's missing)
  4. - Language / script and the list of targets (letters, syllables, or words).
  5. - Which child (childId) + age; gems per completion; trace mode, listen-and-write mode, or both.
  6. - Defaults for a 4–6 year old: `leniency: 2.5`, `acceptBackwardsStrokes: true`, `showHintAfterMisses: 1` in trace mode and `3` in quiz mode.
  7. ​
  8. ## 2. Engine + the one hard constraint
  9. The canvas uses **HanziWriter** (npm `hanzi-writer`) — a stroke-order quiz engine. The Sprout canvas sandbox blocks all external network/scripts EXCEPT the canvas-CDN proxy, so load it with a RELATIVE, PINNED url and nothing else:
  10. `<script src="/api/canvas-cdn/jsdelivr/npm/hanzi-writer@3.7.3/dist/hanzi-writer.min.js"></script>`
  11. HanziWriter renders from per-glyph data `{ strokes: [svgPathString...], medians: [[[x,y]...]...] }` in a 1024×1024 space with y measured UP. You MUST supply this data LOCALLY via `charDataLoader` — its default data CDN fetch is blocked by the sandbox.
  12. ​
  13. ## 3. Get stroke data for each glyph
  14. - **Chinese / Japanese kanji**: HanziWriter-format data exists upstream (makemeahanzi / hanzi-writer-data). At authoring time, look up the specific characters and paste their `{strokes, medians}` objects into a local JS map the `charDataLoader` returns. Do NOT fetch at runtime.
  15. - **Every other script (Latin, Hangul, Cyrillic, kana, Arabic, numbers, …)**: no upstream data — GENERATE it. Author each glyph as CENTERLINE polylines (an array of strokes in correct writing order; each stroke a list of `[x,y]` points in 0..1024, y UP) plus a stroke width, then convert with this generator (paste verbatim):
  16. ​
  17. ```js
  18. var SW = 96, H = SW/2; // stroke width / half-width
  19. function ccw(cx,cy,r,n){n=n||20;var p=[];for(var i=0;i<n;i++){var a=(i/n)*Math.PI*2;p.push([cx+r*Math.cos(a),cy+r*Math.sin(a)]);}return p;}
  20. function cw(cx,cy,r,n){return ccw(cx,cy,r,n).reverse();}
  21. function sp(pts){var d='M '+pts[0][0].toFixed(1)+' '+pts[0][1].toFixed(1);for(var i=1;i<pts.length;i++)d+=' L '+pts[i][0].toFixed(1)+' '+pts[i][1].toFixed(1);return d+' Z';}
  22. function ribbon(line,half){half=half||H;var s=[];for(var i=0;i<line.length-1;i++){var ax=line[i][0],ay=line[i][1],bx=line[i+1][0],by=line[i+1][1];var dx=bx-ax,dy=by-ay,L=Math.hypot(dx,dy)||1;dx/=L;dy/=L;var px=-dy*half,py=dx*half;s.push(sp([[ax-px,ay-py],[bx-px,by-py],[bx+px,by+py],[ax+px,ay+py]]));}for(var k=0;k<line.length;k++)s.push(sp(ccw(line[k][0],line[k][1],half)));return s.join(' ');}
  23. function densify(line,step){step=step||90;var o=[];for(var i=0;i<line.length-1;i++){var ax=line[i][0],ay=line[i][1],bx=line[i+1][0],by=line[i+1][1];var n=Math.max(1,Math.round(Math.hypot(bx-ax,by-ay)/step));for(var t=0;t<n;t++)o.push([Math.round(ax+(bx-ax)*(t/n)),Math.round(ay+(by-ay)*(t/n))]);}o.push([Math.round(line[line.length-1][0]),Math.round(line[line.length-1][1])]);return o;}
  24. // glyph spec: array of centerline strokes; or the string '__C__' as the sole stroke for a ring (o/0).

Inputs

  • Language / scriptlanguageThe script to practice, e.g. 'Korean Hangul', 'Latin lowercase', 'Japanese hiragana', 'numbers 0-9'.
  • What to practicetargetsThe letters, syllables, or words to trace, e.g. 'ㄱ ㄴ ㅁ ㅏ ㅗ' or 'cat dog sun' or 'あ い う'.