Drawing Tool
The drawing tool as an engine with a public API. DrawingTool owns everything that ends up in the drawing (the state, the strokes, the palette, replay and recording) and touches no DOM but the canvas it renders into; a UI is a client of the API, forwarding pointer events and calling the control methods. The default UI, the MIDI adapter, and one config class per page complete the instrument, and a custom UI can replace all of them.
public/lib/demo/drawingTool/ — DrawingTool.js, DrawingToolConfig.js, ui.js, midi.js, index.js — with toolConfigs.js, toolRegistry.js, and markBuilder.js beside it.
Construction
new DrawingTool(canvas, config) builds the engine into a canvas. The config is a DrawingToolConfig: the tool set (tools, ids from the master registry), the initial palette and tool, the preview box, initializers (initializer ids a clear picks from at random, ['scatter'] by default, [] for bare paper; see the Initializers page), and the pointer trace. The engine’s constructor signature never changes; what a config can carry does, so a new setting never touches a construction site. Each page has its own config subclass in toolConfigs.js, constructed with no arguments (new DryMediaDemoConfig()), and PlaybackConfig configures the engine as a playback host.
setupDrawingTool(config, { root, square }) in index.js assembles the whole instrument in one call: the layout, the engine, the default UI, and MIDI. Every try-drawing demo is this one call with its page’s config.
The engine API
Pointer input: pointerDown({ x, y, pressure }), pointerMove, pointerUp, pointerCancel. Coordinates are CSS pixels relative to the canvas; pressure is 0 to 1, 0 meaning none. The engine attaches no listeners of its own, so a UI always forwards events explicitly, gating pressure to pens in its adapter.
Dial control: stepTool(steps) walks the trail of rolled tools, ten remembered on each side of the current one, each keeping its width, parameters, and preview; stepPalette(steps) walks a palette trail the same way, each new entry moving the key hue by about ten degrees and rolling a fresh scheme and seed, so stepping back retrieves the exact palette. A dial, a key, and a MIDI knob all reduce to these two calls; bucketing a continuous control into steps belongs to the caller. The third dial, width, needs no method of its own: it is plain parameter control, setParams({ width }), mapped from the dial position.
Direct control: selectTool(id) picks an exact tool, whose parameters roll once and then stick per tool. paramSpec lists the current tool’s adjustable parameters, with width and pressure as reserved keys ahead of the registry’s own, so a UI renders one set of controls from one spec; the width spec carries the tool’s canonical range, so the panel slider and the dial mapping follow it per tool. setParams({ key: value }) updates any of them, clamping the width to the range. setPalette({ hue, scheme, seed, count }) updates the palette config partially, rerollPalette() draws a new seed under the same config, and setColors({ colorA, colorB, colors }) overrides the colors until the next palette change. setAutoRandomize(on) and setPointerTrace(on) set the two toggles.
Canvas and data: clear({ background }) starts a fresh take: one of the config’s initializers, picked at random, rolls the background and the starting marks, and the marks record like anything drawn. An explicit background overrides the initializer’s. setGuideImage(image), setGuideOpacity(v), and setGuideVisible(on) manage the guide overlay; decoding the file stays with the UI. getDrawingData() returns the serialized log, setDrawingData(data) loads one, downloadDrawing() saves it zipped, and snapshot() resolves with a PNG of the drawing without the preview, trace, or guide.
Playback: replay(), stopReplay(), and recordVideo() are wrappers over the DrawingPlayer the engine composes and exposes as player, so a UI can drive the full transport (pause, seek, step) on the current drawing. The player is documented on the Player page.
State and events: state is a read-only snapshot (the tool id, the values with width and pressure included, the palette config, the colors, and the replay flags); registry is the tool list, for building a menu. on(event, fn) and off subscribe to 'tool', 'palette', 'stroke-start' / 'stroke-end', 'clear' (with the background), 'replay-start' / 'replay-end', 'record-start' / 'record-end', and 'resize'. Every mutation fires its event whatever its source, so a UI stays in sync by listening rather than by wrapping each call.
The default UI and MIDI
ui.js builds what the pages show: the settings panel on the right, the three floating dials (hue stepping the palette on every value change, tool bucketed into steps, width a position mapped into the current tool’s range, held across tool changes so the middle of the dial is the middle of any range), the overlay toggle, fullscreen and the canvas size, and the guide file picker. Every handler is an API call and every display update a subscription; nothing in the engine references it. midi.js maps control change 16 onto the palette step, 17 onto the width, and 18 onto the tool step, matching the dials’ order, driving the UI’s dials when they exist so the knobs and dials stay one control.
The tool registry
A registry is a list of entries, each { id, kind, params, make }. id names the tool in records, so it must stay stable; kind is 'stroke', 'blob', or 'shape' (a shape entry also carries contour(a, b, seed), building its closed contour from the gesture’s endpoints alone); params lists what the tool randomizes, each { key, min, max, step } for a range or { key, pick } for a choice; width: [min, max] is the tool’s canonical width range in pixels (2 to 64 when omitted, narrower for fine media like the pencil); pressure is the tool’s pressure swing as a ratio around the set width (2 when omitted; a pencil barely moves, a watercolor swings to 3); split overrides whether the tool’s gestures split at sharp turns (by kind, strokes do and fills do not); symmetry names a stroke symmetry (‘mirror’, ‘rotation’, ‘screen’) — the engine rolls it at the stroke’s start, shows the copies while the gesture grows, and lands them at release, each fed as its own stroke, with even odds of every copy taking its own resolved palette colors; make(values, ctx) returns a renderer built from the rolled values and the context.
ctx carries colorA (the palette’s key color), colorB, colors (every palette color), texture (the canvas, for tools that read the background), seed, start and end (the drawn chord’s world points), and tintLight (the main color lightened, for metals). toolRegistry is the master list of every tool on the site; pickTools(ids) returns a page’s subset, randomValues(entry) rolls an entry’s parameters, and toolLabel(entry) turns an id into a display name.
Preview gesture
The tool preview draws the current tool on a canned gesture whose shape follows the tool, so the preview reads the way the tool draws. previewPathOf(entry) names the category and an entry’s previewPath overrides it: line is a wiggling line for ordinary strokes; mass is a compact oval loop, so a fill’s outline reads as a rounded mass rather than a long band; span is the two endpoints of a short diagonal, for the endpoint shapes that take their form from start and end alone; radial is a line leaving the box center, so a rotational tool’s copies fan around it. The default follows the kind and symmetry (blobs take mass, endpoint shapes span, rotational strokes radial, everything else line). previewPath(category, center, size, shape) builds the gesture.
Mark building
makeMarkBuilder({ state, board }) returns a draw cycle build: it turns one piece’s smoothed path and raw points into a mark with the state’s current tool. A stroke tool gets a width tapered by arc length, scaled around the set width by pressure (middle pressure draws it as set, the swing per tool from its pressure range, through the dead-zone response) and clamped by the slope limit; a blob tool gets a contour from blobOutline with a radius scaled the same way by the average pressure; a shape tool gets its contour from its own contour on the piece’s endpoints, with neither width nor pressure applied. state.seedOverride, set while a replayed record drives the cycle, replaces the cycle’s seed so seeded looks reproduce.
applyRecordTo(state, record, registry) restores one record’s tool, parameters, and colors into a state ahead of feeding its points, carrying the record’s seed in seedOverride.