Handwriting input: one box, letters written over each other

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.

characters:
loading…
Try a stylus or a finger on a tablet; a mouse works too. Press done when a word is finished, or tap one of the suggested words. undo takes back a stroke.

Embed it

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

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

Lists saved from this browser

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.

idnamewordssaved
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).

JavaScript

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 / attributedefault
words · list · wordsSrcdefault listthe word list, in that precedence
target, targetMode— / livewhere the text goes; words = finished words only
size240the box, CSS px
layoutcolumnrow puts the text, handwriting and candidates beside the box
chips · history6 · 1candidates shown · previous words' handwriting kept beside the current
idleFinish0seconds of pen-up after which the word finishes itself
theme · hide · placeholderautopalette; 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).

Self-host it

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: '…' }).

filesize
millirec-overlap.min.jsthe widget: recognizer, matcher glue, UI44 KB (17 gzipped)
millirec.wasmthe matcher29 KB (13)
training.jsonletter templates, full and partial87 KB (18)
words.json / words.jsthe default word list94 KB (37)

How it works, briefly

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.