Write a capital letter, then the next one on top of it. The widget separates the strokes into letters and shows the words the ink could be; tap one to accept it. Everything runs on the visitor's device.
One script tag and one element. The widget mounts itself, and mirrors the recognized text into whatever data-target points at (an input or textarea gets its value and an input event; anything else its text).
Change the character set or the list id and the snippet follows. Without data-set the box reads capital letters; data-set="lowercase|digits|punctuation|all" picks another set, data-alphabet="ABC…" exactly those characters. Without data-list the widget uses the default list: the 10,000 most common English words — folded onto the set, so a digits or punctuation box with no usable words runs free (every character priced alike, no word list).
The recognizer reads against the beginning of every word in the list as the writer goes, and prefers a word that is already complete when the ink is close enough. Order matters: put the most likely words first — rank is the frequency estimate. Any of the 75 characters may appear; a list is folded onto the character set the box reads (a capitals box uppercases it, a lowercase box lowercases it, the all-75 set keeps case and adds Capitalized and UPPER variants), and words with characters the set lacks are skipped. Duplicates are removed.
Ids and edit tokens are kept in this browser's storage only. The edit token is what lets you change a list later — keep it somewhere safe; it is never shown again by the server.
| id | name | words | saved | |
|---|---|---|---|---|
| none yet | ||||
From JavaScript at any time: widget.setWords(['RED', 'GREEN', …]) or widget.loadList('ol_…'); in the page: data-words="RED GREEN BLUE" or data-words-src="words.txt". The API behind this editor is POST /api/overlap/lists, GET /api/overlap/lists/:id, PUT /api/overlap/lists/:id (with the edit token).
const w = await MillirecOverlap.create('#box', {
list: 'ol_a1b2c3d4', // or words: ['APPLE', 'BANANA', …], or wordsSrc: 'words.txt'
target: '#answer', // an input to mirror the text into
onWord: ({ word, canvas }) => { … }, // a word was finished (done, a tap, or idleFinish)
})
w.getText() // finished words w.getLiveText() // plus the word being written
w.getCandidates() // [{word, score, complete, inList}], best first
w.getHandwriting() // a canvas: the current word's ink, letters side by side (scope: 'all' for everything)
w.finishWord() // best reading w.selectWord('WORK') // what a tap does: re-split the strokes to spell it
w.setWords([…]) // a new list w.undo() w.clearWord() w.clear() w.destroy()
Events on the element, all bubbling CustomEvents with a detail: millirec-overlap:ready, :stroke (after every stroke: text, live, current, candidates, canvas), :change, :word (word, letters, canvas, inList, selected), :select, :error.
| option / attribute | default | |
|---|---|---|
words · list · wordsSrc | default list | the word list, in that precedence |
target, targetMode | — / live | where the text goes; words = finished words only |
size | 240 | the box, CSS px |
layout | column | row puts the text, handwriting and candidates beside the box |
chips · history | 6 · 1 | candidates shown · previous words' handwriting kept beside the current |
idleFinish | 0 | seconds of pen-up after which the word finishes itself |
theme · hide · placeholder | auto | palette; parts to leave out (text handwriting words buttons); the empty-state text |
engine | — | recognizer knobs, e.g. {"commitMargin": 6, "oovPen": 8} |
Style it with custom properties on the element (--mo-ink --mo-bg --mo-line --mo-accent --mo-muted --mo-suspect --mo-chip --mo-radius) or the parts ::part(box), ::part(text), ::part(handwriting), ::part(words), ::part(chip), ::part(buttons).
Download millirec-overlap.zip — the widget, the matcher, the letter templates, the default word list and a demo page; serve the files from one directory over HTTP. It needs nothing from this server (hosted lists still work if you tell it where they live: window.MILLIREC_OVERLAP = { apiBase: '…' }).
| file | size | |
|---|---|---|
millirec-overlap.min.js | the widget: recognizer, matcher glue, UI | 44 KB (17 gzipped) |
millirec.wasm | the matcher | 29 KB (13) |
training.json | letter templates, full and partial | 87 KB (18) |
words.json / words.js | the default word list | 94 KB (37) |
Strokes arrive in writing order, so a letter is always a run of consecutive strokes. After every stroke the ink in the box is segmented by a dynamic program: each candidate run is scored against every letter by the point-cloud matcher — full letters, and for the run still being drawn also partial letters (the first strokes of a letter drawn with more), so an E in progress is a hypothesis rather than an I plus a bar — and each letter is priced by the word list. A letter is erased when the best reading beats the best reading that keeps its strokes together by a margin. Accepting a candidate runs the same program with the letters fixed, over every stroke of the word, and keeps the best split. Measured on held-out drill cells written into one box: 95% of words read right, five letters in six erased on the very next stroke. The full reference — every option, event, knob and the measurements — is docs/overlap.md.
Capital letters only, for now, and the templates come from one writer's hand. Most people's capitals are close enough; a very different letter formation will need more corrections, and per-writer templates are the natural next step.