Serialization & History
Two day-one features that most canvas libraries bolt on later: scenes round-trip through plain JSON, and every mutation can be a reversible command on a History stack.
Save & load
Section titled “Save & load”stringify walks the stage to a JSON document; parse rebuilds it. Add shapes, save, clear, then
load to restore:
import { Circle, type Layer, Rect, SceneSerializer } from '@veyrajs/core'import { button, createStage, cycle, disposeStage, readout, toolbar } from './_kit'
// SceneSerializer round-trips the scene through plain JSON: `stringify` walks the stage to a// document, `parse` rebuilds it. Add shapes, Save, Clear, then Load to restore the saved state.export function init(host: HTMLElement): () => void { const stage = createStage(host) let layer = stage.createLayer() const serializer = new SceneSerializer() let saved: string | null = null let colorIndex = 0
layer.add( new Rect({ x: 60, y: 60, width: 130, height: 80, fill: cycle[0] }), new Circle({ x: 330, y: 120, radius: 48, fill: cycle[1] }), )
const bar = toolbar(host) const load = button('Load', () => { if (!saved) return serializer.parse(stage, saved) layer = stage.children[0] as Layer }) load.disabled = true bar.append( button('+ Shape', () => { const color = cycle[colorIndex++ % cycle.length] if (Math.random() < 0.5) { const w = 60 + Math.random() * 110 layer.add( new Rect({ x: 30 + Math.random() * (stage.width - w - 60), y: 30 + Math.random() * (stage.height - 130), width: w, height: 50 + Math.random() * 60, fill: color, }), ) } else { const r = 26 + Math.random() * 38 layer.add( new Circle({ x: 50 + Math.random() * (stage.width - 100), y: 50 + Math.random() * (stage.height - 100), radius: r, fill: color, }), ) } }), button('Save', () => { saved = serializer.stringify(stage) out.textContent = `saved ${new Blob([saved]).size} bytes` load.disabled = false }), button('Clear', () => layer.removeChildren()), load, ) const out = readout(bar, 'nothing saved')
return () => disposeStage(stage)}Undo & redo
Section titled “Undo & redo”Wrap a change in a command and run it through History to make it reversible:
import { AddNodeCommand, History, Rect } from '@veyrajs/core'import { button, createStage, cycle, disposeStage, readout, toolbar } from './_kit'
// Every meaningful change can be a reversible Command. Wrapping `layer.add` in an AddNodeCommand// and running it through History makes each square individually undoable and redoable.export function init(host: HTMLElement): () => void { const stage = createStage(host) const layer = stage.createLayer() const history = new History() let i = 0
const bar = toolbar(host) const undo = button('Undo', () => history.undo()) const redo = button('Redo', () => history.redo()) undo.disabled = true redo.disabled = true bar.append( button('Add square', () => { const n = i++ history.run( new AddNodeCommand( layer, new Rect({ x: 30 + (n % 8) * 72, y: 40 + Math.floor(n / 8) * 72, width: 58, height: 58, fill: cycle[n % cycle.length], }), ), ) }), undo, redo, ) const hint = document.createElement('span') hint.className = 'veyrajs-demo__hint' hint.textContent = 'Each add is a reversible command' bar.append(hint) const out = readout(bar, '')
const sync = (): void => { undo.disabled = !history.canUndo redo.disabled = !history.canRedo out.textContent = `undo ${history.canUndo ? '●' : '○'} · redo ${history.canRedo ? '●' : '○'}` } const off = history.onChange(sync) sync() return () => { off() disposeStage(stage) }}