Player
Recording and playback for drawings. A drawing’s log is data, not raster: the canvas size, the starting background, and one record per committed mark. StrokeRecorder collects the log while drawing, and DrawingPlayer runs a full transport over one — play, pause, seek, step, rewind, finish — records the playback to a video, and serializes the log to and from a file.
public/lib/demo/strokeRecorder.js, public/lib/demo/drawingPlayer.js
The record
A record carries everything needed to rebuild one mark: toolId, the parameter values, widthPx, sens, colorA, colorB, colors, seed, and the drawn points with their pressures. Only drawn points are stored, so blank time costs nothing and a playback skips it by construction. The toolId names an entry in a registry, so the log stays valid as long as the ids do. A sharp turn splits a stroke gesture into several records (fills draw one record per gesture); the one the pen lifted after carries release: true. Records the clear’s initializer laid down carry initial: true.
StrokeRecorder
begin(background) starts a new take, as a clear does, storing the background spec (a color or a gradient description). add(record, points) appends one committed mark, copying the points as plain { x, y, pressure }; markRelease() marks the last record as a release. The recorder’s { background, records } is the log.
DrawingPlayer
The player owns no interface and no scene. It reaches the surface through callbacks given to the constructor, and everything they restore comes out of the log: feed(points, done) hands points to whatever cycle the host wires up, as if a pen produced them; applyRecord(record) restores one record’s tool and colors before its points; clear(background) resets the surface to the log’s background; resize(width, height) (optional) applies the log’s canvas size before the first clear; canvas is the surface the video capture streams from. DrawingTool composes one and exposes it as player; a page that only plays drawings uses the engine headless with PlaybackConfig rather than wiring these itself.
The transport
setData(data) receives a log, replacing what it had. play({ pointsPerFrame, strokeWaitMs, instantInitial, onDone }) animates from the current position, resting strokeWaitMs after each record marked release; with instantInitial (the default) records marked initial are placed instantly instead of animated; pause() holds, keeping what is drawn, and resume() continues. seek(index) jumps so that many records are drawn; next() and prev() are one-step seeks, rewind() returns to the cleared background, and finish() jumps to the end. position, length, playing, and recording report the state, and on(event, fn) subscribes to 'step', 'play', 'pause', 'end', 'record-start', and 'record-end'.
Seeking is rebuilt, not rewound: strokes are paint on a raster, and many read the canvas beneath them, so the frame at any position depends on every stroke before it. Moving backward clears to the log’s background and re-feeds from the start; moving forward feeds only the difference. Each pass is stroke-level (whole paths, no animation and no idle time), so finish stays cheap. To only show the final frame, skip the player entirely and use the image from the tool’s snapshot().
record({ filename, onDone }) plays from the start while capturing the canvas and saves the video, mp4 where the browser can encode it, webm otherwise.
Serialization
serializeDrawing({ size, background, records }) returns the log as JSON, under { version: 2, size, background, records }; size is the canvas in CSS pixels when recorded, and version 1 logs, which have none, still load. downloadDrawingZip(log, filename) saves it as a zip holding one JSON file, and readDrawingZip(file) reads it back from a zip or a bare JSON file. The zip codec loads on demand, so pages that never save or load pay nothing for it.