# Silk > Silk is a drawing toy: strokes of light (or of ink) that keep moving after you draw them, mirrored and turned into > symmetrical patterns. This page can be driven by a script in its browser console, by a person or an agent: all of it > is one object, `globalThis.silk`. This file is its whole reference: `await silk.help()` returns it too, and > `await silk.help("stroke")` just one section. ## Draw links: the simplest way A drawing short enough for a link can be written as one, with no browser at all: `https://silk.art/#draw=` and then steps separated by `;`. Opening the link plays the steps on the page, live, and the person can watch, keep drawing, and export. Any assistant can write one; hand it over as a markdown link, `[Open your drawing](https://silk.art/#draw=...)`. This one draws a blue six-fold snowflake: ``` https://silk.art/#draw=clear;brush=wavy;turns=6;mirror=on;color=0;stroke=50,30,55,36,58,45,55,52;wait=1500 ``` - A step is `verb=value`, or a verb alone (`clear`), and `;` separates them. Nothing else: no spaces, quotes or brackets, nothing to encode. - Positions are **percentages of the square in the canvas's middle**, as wide as the screen's shorter side: `50,50` is the middle, `0,0` the square's top left, `100,100` its bottom right. A drawing keeps its shape on every screen: draw a circle and it is round on a phone and on a wide monitor alike, so do not adjust for the screen's shape. Beyond 0 to 100 reaches the rest of a screen's longer side. A stroke is its points' x,y pairs: `stroke=50,30,55,36,58,45`. Ten to forty points make a good stroke. - Names are **ids**, exactly as **Names** below lists them: `brush=wavy-glow`, `tiling=p4m`, `palette=silk`. - A color is a swatch's number (see **Names**) or six hex digits without a `#`: `color=3380ff`. - Begin with `clear`, so the link draws the same thing every time, and end with `wait=1500`, so the strands settle. - Every step is checked before any plays. A link with a mistake plays nothing, and the page says which step is wrong and the form it takes. - Keep it to a few thousand characters: a longer drawing is better drawn through the page's script (below). - In a browser, `silk.link(steps)` writes the link for steps (see **Steps**), and `await silk.run(steps)` plays them. ## In the page Paste this into the page's console to draw a snowflake and get it as an image: ```js await silk.ready; // the engine is up silk.clear(); silk.brush("wavy"); // the silk brush silk.symmetry({ turns: 6, mirror: true }); // six turns, each mirrored silk.color(0); // the palette's first color await silk.stroke([[0.50, 0.30], [0.55, 0.36], [0.58, 0.45], [0.55, 0.52]]); await silk.wait(1500); // the strands settle await silk.show(); // the drawing as an image over the page, to see or screenshot ``` ## Rules of thumb - **Positions are fractions of the square in the canvas's middle**, as wide as its shorter side: `[0.5, 0.5]` is the middle, `[0, 0]` the square's top left, `[1, 1]` its bottom right, and a drawing keeps its shape on any screen (beyond 0 to 1 reaches the rest of the longer side). The symmetry turns strokes about the middle. - **Strokes play in real time**, 16 ms a point unless a point says otherwise: `await` them. A silk stroke keeps moving for about 1.5 s after it lifts, so `await silk.wait(1500)` before taking an image. - **Nothing fails quietly.** A wrong name or value throws, and the message lists what is allowed: read it and retry. - **`silk.state()`** says what is set now: check it after a change. - **To show a person what you drew**, `await silk.show()`: the drawing as an image over the whole page, which you can screenshot and they can see; `silk.hide()` (or Esc, or its × button) takes it down. `silk.save()` downloads it. - **Draw with the methods; reach deeper with `silk.engine`.** The methods move the page's own controls, so the page shows what you set. `silk.engine(name, ...args)` calls the engine underneath directly: everything the engine can do, documented below, but the page's controls do not follow it. - Everything drawn is recorded on the drawing's **tape**: `silk.tape()` gives it as bytes, `silk.play(bytes)` plays it again, `silk.replay()` plays the drawing again from its start. ## Methods ### silk.ready A promise: resolves once the engine is up and the page has its brushes. `await silk.ready` first. ### silk.help(topic) The reference, as text (a promise: `await` it): all of it, or the section whose heading is `topic`, a method (`"stroke"`), a brush (`"Sumi"`), an engine export (`"set_white_point"`) or a part (`"Brushes"`, `"Engine"`, `"Examples"`). A topic it does not have throws with the topics it has. ### silk.run(steps) and silk.link(steps) `await silk.run(steps)` plays a list of steps (see **Steps**), in order, each awaited: the same list a draw link carries. A mistake throws before anything plays, naming the step and what would have been right. `silk.link(steps)` checks the steps the same way and returns the draw link that plays them, this page's address and `#draw=`; a step no link can say (`together`, or points with their own pressure or time) throws. ```js await silk.run([{ clear: true }, { brush: "sumi" }, { stroke: [[0.3, 0.5], [0.5, 0.45], [0.7, 0.55]] }, { show: true }]); ``` ### silk.state() What is set now, as an object: `brush`, `color` (the swatch's number), `colors` (the palette's swatches, CSS), `palette`, `symmetry` (`turns`, `mirror`, `spiral`), `tiling`, `size`, `layer`, `infinite` (a count of strokes kept, or false), `whitePoint`, `paper` (whether the drawing is on paper), `view` (`zoom`, `angle`, `x`, `y`), `canvas` (its size in CSS px), `canUndo`, `canRedo`, `playing` (whether a tape plays), `blank` (whether nothing is drawn). ### silk.brushes() and silk.brush(name) `silk.brushes()` lists the brushes by name. `silk.brush("sumi")` chooses one, by its id or its name exactly as **Names** lists it (`"wavy-glow"` or `"Wavy + Glow"`); `silk.brush()` names the one chosen. `"googly"` is a tool: its next press puts googly eyes on the drawing. The specs that measure the engine (`"heavy-ink"`, `"stress-500"`, ...) are chosen the same way, though the menu hides them. Choosing a brush on a blank drawing puts it on the brush's canvas (silk's black, or paper); on a drawing, any brush draws on the canvas as it is. See **Brushes** below for each brush's parameters. ### silk.set(name, value) A brush's parameter, by its name in **Brushes** below or by the label of the page's slider for it (`"Waviness"`, `"Swing"`): for the stroke being drawn now and the strokes to come. Called while a stroke plays (without awaiting the stroke), it changes the stroke as it draws: that is how to modulate one. ```js const s = silk.stroke(points); // not awaited: it plays on for (let i = 0; i <= 10; i++) { silk.set("noise_force_scale", i / 10); await silk.wait(80); } await s; ``` ### silk.palettes(), silk.palette(which), silk.colors(), silk.color(which) The palettes by name; one chosen by name or number. The chosen palette's swatches as CSS colors; one chosen by number or by its CSS. The last swatch is the eraser. On paper the swatches are ink's. ### silk.symmetry({ turns, mirror, spiral }) Any of the three: `turns` 1 to 9 (copies turned about the middle), `mirror` (each turn mirrored), `spiral` (each turn spiraling in). What is left out stays. ### silk.tilings() and silk.tiling(name) The wallpaper groups by name (`"none"`, `"p1"`, `"p4m"`, `"p6m"`, ...): the canvas repeated as one tile of a pattern, each copy of a stroke drawn in every tile. `"p1"` is the canvas alone, the default. ### silk.size(n) The brush's size: `0` small, `1` medium, `2` large. ### silk.stroke(points, options) A stroke through `points`, played in real time as pointer events through the page's own input: down at the first, a move at each after, up at the last. A point is `[x, y]` (fractions of the square in the canvas's middle, as **Rules of thumb** says) or `{ x, y, t, pressure, altitude, azimuth }` (`t` in ms from the stroke's start; `altitude` and `azimuth` a pen's tilt in radians). `options`: `pointerType` (`"mouse"`, the default, `"pen"` or `"touch"`), `pressure` (0.5), `id` (1: give two strokes different ids to draw them at once). ### silk.undo(), silk.redo(), silk.clear() As the page's buttons: the last stroke taken back, put back, and the drawing cleared (a clear can be undone until the next stroke). ### silk.home() and silk.view({ zoom, angle, x, y }) The view back at 1×, the whole drawing; and the camera set at once: `zoom`, `angle` in radians, pan `x` and `y` in CSS px. The view is not on the tape and not in an image unless the image asks for it. ### silk.png({ view, transparent, high }), silk.save() The drawing as a PNG data URL: the drawing alone at the canvas's size, or the view as it shows (`view`, the tiling's copies), on nothing (`transparent`), at high resolution (`high`). `silk.save()` downloads one, as the page's s key does. ### silk.show(options) and silk.hide() The drawing put up as an image over the whole page, for a person to see and for an agent to take a screenshot of: made as `silk.png` makes it, with the same options (`view` for the view with its tiling, `transparent`, `high`), on black, with a × button. Resolves once it shows, with the image as a data URL. `silk.hide()`, the × or Esc takes it down. ### silk.tape(), silk.play(bytes), silk.replay({ tight }), silk.saveTape() The drawing's tape as bytes (a `Uint8Array`); a tape played into the drawing at the pace it was drawn (false if the engine refuses it); the drawing played again from its start, as drawn or, with `tight`, without what was undone and with its pauses short; the tape downloaded, to drop on the page later. ### silk.wait(ms) Time passing, while the strokes move. `await` it. ### silk.paper() Whether the drawing is on paper (ink's canvas) rather than silk's black. ### silk.engine(name, ...args) Any of the engine's exports, by its name with or without `engine_`, called at once; returns what it returns. Where an export's first parameter is `time`, leave it out and the page's clock fills it in. Wrong arguments throw with the export's signature. The page's controls do not follow what it changes: a brush chosen here is not the menu's. Every export is in **Engine** below; `silk.help("set_layer_medium")` shows one. ```js silk.engine("set_white_point", 2.5); // the light rolled off at 2.5 (time filled in) silk.engine("set_layer_medium", 2, 0, false); // layer 2 laid over the canvas as ink is silk.engine("view"); // { zoom, angle, x, y } ``` ## Steps The steps `silk.run` plays, and a draw link's steps become: an object with one verb and its value, some verbs taking options beside it. Names are ids (**Names**), and positions **fractions** of the square in the canvas's middle (`0.5` the middle), where a link's are percentages. Every step is a method above, so its rules are that method's. - `{ "stroke": [[x, y], ...], "pen": true, "pressure": 0.5, "id": 1 }`: a stroke (`silk.stroke`); a point may be `{ "x", "y", "t", "pressure", "altitude", "azimuth" }`. `pen` draws with a pen's pressure; `id` tells strokes apart. - `{ "together": [step, step, ...] }`: steps at once (two strokes of two ids: two hands). - `{ "brush": "wavy" }`, `{ "color": 0 }` (a swatch's number or CSS), `{ "eraser": true }`, `{ "palette": "silk" }`, `{ "ground": 0 }` (the ground under the drawing, by number or CSS), `{ "symmetry": { "turns": 6, "mirror": true, "spiral": false } }`, `{ "tiling": "p4m" }`, `{ "size": 2 }`, `{ "layer": 1 }` (1 to 3), `{ "infinite": 3 }` (the strokes kept, or false), `{ "hdr": 5 }` (the white point, 1 to 20), `{ "crossing": true }`. - `{ "set": { "Waviness": 0.8 } }`: a brush's parameters (`silk.set`); between two strokes, or in a `together` beside one, to change it as it draws. - `{ "undo": true }`, `{ "redo": true }`, `{ "clear": true }`, `{ "wait": 1500 }` (ms), `{ "view": "home" }` or `{ "view": { "zoom": 0.5, "angle": 0.3 } }`, `{ "replay": "tight" }` or `"drawn"`, `{ "show": true }` (or png's options), `{ "hide": true }`. - `{ "engine": ["set_white_point", 2.5] }`: any export of the engine (`silk.engine`). ## Examples A pen stroke, pressing harder as it goes: ```js await silk.ready; const pts = Array.from({ length: 40 }, (_, i) => ({ x: 0.3 + i * 0.01, y: 0.5 + 0.1 * Math.sin(i / 6), pressure: i / 40 })); await silk.stroke(pts, { pointerType: "pen" }); ``` Ink on paper, then undone and drawn again: ```js silk.clear(); silk.brush("sumi"); await silk.stroke([[0.3, 0.5], [0.5, 0.45], [0.7, 0.55]]); silk.undo(); await silk.stroke([[0.3, 0.6], [0.5, 0.55], [0.7, 0.6]]); ``` A tiled pattern, as the view shows it: ```js silk.tiling("p4m"); silk.view({ zoom: 0.4 }); await silk.stroke([[0.4, 0.4], [0.6, 0.45], [0.5, 0.6]]); await silk.wait(1500); const url = await silk.png({ view: true }); ``` The drawing's tape, played again: ```js const bytes = silk.tape(); silk.clear(); silk.play(bytes); ``` ## Link verbs Every verb a draw link takes (`#draw=`, above), each with the form of its value; a verb shown alone takes none. `pen` and `pressure` change the strokes after them. (Made from client/src/draw-link.js, which reads the links.) - `clear`: clear - `undo`: undo - `redo`: redo - `show`: show - `hide`: hide - `eraser`: eraser - `home`: home - `brush`: brush=wavy - `palette`: palette=silk - `tiling`: tiling=p4m - `color`: color=0 (a swatch's number) or color=3380ff - `ground`: ground=0 or ground=0d1733 - `turns`: turns=6 (1 to 9) - `mirror`: mirror=on or mirror=off - `spiral`: spiral=on or spiral=off - `size`: size=2 (0, 1 or 2) - `layer`: layer=1 (1, 2 or 3) - `infinite`: infinite=3 (strokes kept, 1 to 7) or infinite=off - `hdr`: hdr=5 (1 to 20) - `crossing`: crossing=on or crossing=off - `wait`: wait=1500 (ms, up to 60000) - `pen`: pen=on or pen=off (the strokes after it drawn with a pen, or a mouse) - `pressure`: pressure=0.5 (0 to 1, the strokes after it) - `stroke`: stroke=50,30,55,36,58,45 (x,y pairs, percentages of the square in the canvas's middle, 50,50 its middle) - `set`: set=noise_force_scale:0.8 (a brush's parameter: its name, a colon, a number) - `view`: view=home, view=0.5 (a zoom) or view=0.5,30 (a zoom and a turn in degrees) - `replay`: replay=tight or replay=drawn - `engine`: engine=set_white_point,2.5 (an export's name, then its arguments: numbers, on or off) ## Names The names a link and the methods take, exactly as written here: a link takes the ids, the methods an id or the name. - **Brushes** (`brush=`): `wavy` (Wavy), `glow` (Glow), `wavy-glow` (Wavy + Glow), `sumi` (Sumi), `swoosh` (Swoosh), `swoosh-glow` (Swoosh + Glow), `sketchy` (Sketchy), `googly` (Googly). Hidden from the menu, for testing: `fireworks`, `stress-500`, `ellipse-stress`, `long-trails`, `many-copies`, `wide-silk`, `heavy-ink`. - **Tilings** (`tiling=`): `none`, `p1`, `p2`, `pm`, `pg`, `cm`, `pmm`, `pmg`, `pgg`, `cmm`, `p4`, `p4m`, `p4g`, `p3`, `p3m1`, `p31m`, `p6`, `p6m`. `none` draws on one canvas; the others repeat it across the plane (their wallpaper groups). - **Palettes** (`palette=`): `silk`, `dusk`, `ember`, `neon`, `nebula`, `aurora`, `abyss`, `moonlight`, `grove`, `bloom`, `solstice`. - **Colors** (`color=`), in the silk palette: `0` blue, `1` teal, `2` green, `3` yellow, `4` orange, `5` red, `6` pink, `7` purple, `8` light grey; `eraser` erases. Every palette has nine swatches, 0 to 8, in its own colors. - **Grounds** (`ground=`): `0` black, and `1` to `8` a dark ground of colors 0 to 7. ## Brushes Each brush of the menu, what it draws with, and the parameters `silk.set(name, value)` takes for it: the name, its type, its default and its range where it has one (made from the engine, `engine_brush_info`). The specs the menu hides (chosen by name all the same): Fireworks, Stress 500, Ellipse Stress, Long Trails, Many Copies, Wide Silk, Heavy Ink. ### Wavy (`wavy`) The silk brush, in the symmetry and the color chosen. (brush `silk`, on silk's canvas) - `brush_size` (f32, 0.75, 0.25 to 3): How big the strand's motion is: its simulation runs in units of this many of the stroke's pixels. - `chaos` (f32, 0, 0 to 2): How much each point is jittered as the strand is drawn, which also thins it. - `color_from` (vec4, [0, 0, 0, 0]): The color a stroke blends from as the hand moves slowly, in Oklab, with alpha. - `color_speed` (f32, 0, 0 to 5000): The hand's speed, in pixels a second, at which the blend arrives at `color_to`; 0 leaves `instance.color` alone. - `color_to` (vec4, [0, 0, 0, 0]): The color a stroke blends to as the hand moves fast, in Oklab, with alpha. - `dots.color` (vec4, [0.45, 0.45, 0.45, 1]): The preview ring's color when it shows fully, premultiplied. - `dots.radius` (f32, 4, 1 to 20): The preview ring's radius, in the pointer's pixels. - `dots.width` (f32, 1.5, 0.5 to 6): The preview ring's line width, in the pointer's pixels. - `friction` (f32, 0.975, 0.8 to 1): How much of its last pull toward its neighbors a point keeps as velocity: lower, and the strands settle sooner. - `generation` (u32, 0): A new value begins the strands again, from nothing. - `glow` (f32, 0, 0 to 1): How bright the haze about the newest points is, a share of the brush's color. - `glow_ms` (f32, 100, 10 to 1000): How long a point glows after it is made, in milliseconds. - `initial_velocity_decay` (f32, 0.98, 0.8 to 1): How much of that speed a point keeps from one step to the next. - `initial_velocity_force_scale` (f32, 0.3, 0 to 1): How much of the hand's own speed a new point sets out with. - `noise_angle_offset` (f32, 0, -3.2 to 3.2): An angle added to the way the noise pushes, in radians. - `noise_angle_scale` (f32, 15.708, 0 to 40): How far the noise turns the way it pushes, in radians over its range: higher, and the strands turn more often. - `noise_fallout` (f32, 0.65, 0 to 1): How much each octave of the noise counts, against the one before. - `noise_force_scale` (f32, 0.35, 0 to 1): How hard the noise pushes the strands: at 0 they run straight. - `noise_octaves` (f32, 8, 1 to 16): How many octaves of noise are summed (16 at most). - `noise_space_scale` (f32, 0.02, 0.001 to 0.1): How finely the noise varies across the page: larger, and the strands swirl tighter. - `noise_time_scale` (f32, 0.005, 0 to 0.05): How fast the noise changes as the stroke goes on. - `resting_distance` (f32, 0, 0 to 10): How far apart neighboring points come to rest, in the strand's units. - `rigidity` (f32, 0.2, 0 to 0.33): How hard neighboring points pull together; past 1 / (1 + 2 × friction), 0.339 at the default friction, a strand can blow up. - `scatter` (f32, 40, 0 to 200): How far the haze spreads about the young points, in the brush's units. - `self` (bytes, null): - `start_life` (f32, 150, 10 to 1024): How many steps a point lives, 480 a second: how long the trail is (1024 at most). - `strand` (f32, 1, 0 to 1): How bright the strand is, a share of the brush's color: 0 draws the haze alone. ### Glow (`glow`) Silk 2's glow: a haze of faint lines about the newest part of the stroke, in the symmetry and the color chosen. (brush `silk`, on silk's canvas) - `brush_size` (f32, 0.75, 0.25 to 3): How big the strand's motion is: its simulation runs in units of this many of the stroke's pixels. - `chaos` (f32, 0, 0 to 2): How much each point is jittered as the strand is drawn, which also thins it. - `color_from` (vec4, [0, 0, 0, 0]): The color a stroke blends from as the hand moves slowly, in Oklab, with alpha. - `color_speed` (f32, 0, 0 to 5000): The hand's speed, in pixels a second, at which the blend arrives at `color_to`; 0 leaves `instance.color` alone. - `color_to` (vec4, [0, 0, 0, 0]): The color a stroke blends to as the hand moves fast, in Oklab, with alpha. - `dots.color` (vec4, [0.45, 0.45, 0.45, 1]): The preview ring's color when it shows fully, premultiplied. - `dots.radius` (f32, 4, 1 to 20): The preview ring's radius, in the pointer's pixels. - `dots.width` (f32, 1.5, 0.5 to 6): The preview ring's line width, in the pointer's pixels. - `friction` (f32, 0.975, 0.8 to 1): How much of its last pull toward its neighbors a point keeps as velocity: lower, and the strands settle sooner. - `generation` (u32, 0): A new value begins the strands again, from nothing. - `glow` (f32, 0, 0 to 1): How bright the haze about the newest points is, a share of the brush's color. - `glow_ms` (f32, 100, 10 to 1000): How long a point glows after it is made, in milliseconds. - `initial_velocity_decay` (f32, 0.98, 0.8 to 1): How much of that speed a point keeps from one step to the next. - `initial_velocity_force_scale` (f32, 0.3, 0 to 1): How much of the hand's own speed a new point sets out with. - `noise_angle_offset` (f32, 0, -3.2 to 3.2): An angle added to the way the noise pushes, in radians. - `noise_angle_scale` (f32, 15.708, 0 to 40): How far the noise turns the way it pushes, in radians over its range: higher, and the strands turn more often. - `noise_fallout` (f32, 0.65, 0 to 1): How much each octave of the noise counts, against the one before. - `noise_force_scale` (f32, 0.35, 0 to 1): How hard the noise pushes the strands: at 0 they run straight. - `noise_octaves` (f32, 8, 1 to 16): How many octaves of noise are summed (16 at most). - `noise_space_scale` (f32, 0.02, 0.001 to 0.1): How finely the noise varies across the page: larger, and the strands swirl tighter. - `noise_time_scale` (f32, 0.005, 0 to 0.05): How fast the noise changes as the stroke goes on. - `resting_distance` (f32, 0, 0 to 10): How far apart neighboring points come to rest, in the strand's units. - `rigidity` (f32, 0.2, 0 to 0.33): How hard neighboring points pull together; past 1 / (1 + 2 × friction), 0.339 at the default friction, a strand can blow up. - `scatter` (f32, 40, 0 to 200): How far the haze spreads about the young points, in the brush's units. - `self` (bytes, null): - `start_life` (f32, 150, 10 to 1024): How many steps a point lives, 480 a second: how long the trail is (1024 at most). - `strand` (f32, 1, 0 to 1): How bright the strand is, a share of the brush's color: 0 draws the haze alone. ### Wavy + Glow (`wavy-glow`) Silk 2's wave and glow: the silk brush's strand with the glow's haze on it, in the symmetry and the color chosen. (brush `silk`, on silk's canvas) - `brush_size` (f32, 0.75, 0.25 to 3): How big the strand's motion is: its simulation runs in units of this many of the stroke's pixels. - `chaos` (f32, 0, 0 to 2): How much each point is jittered as the strand is drawn, which also thins it. - `color_from` (vec4, [0, 0, 0, 0]): The color a stroke blends from as the hand moves slowly, in Oklab, with alpha. - `color_speed` (f32, 0, 0 to 5000): The hand's speed, in pixels a second, at which the blend arrives at `color_to`; 0 leaves `instance.color` alone. - `color_to` (vec4, [0, 0, 0, 0]): The color a stroke blends to as the hand moves fast, in Oklab, with alpha. - `dots.color` (vec4, [0.45, 0.45, 0.45, 1]): The preview ring's color when it shows fully, premultiplied. - `dots.radius` (f32, 4, 1 to 20): The preview ring's radius, in the pointer's pixels. - `dots.width` (f32, 1.5, 0.5 to 6): The preview ring's line width, in the pointer's pixels. - `friction` (f32, 0.975, 0.8 to 1): How much of its last pull toward its neighbors a point keeps as velocity: lower, and the strands settle sooner. - `generation` (u32, 0): A new value begins the strands again, from nothing. - `glow` (f32, 0, 0 to 1): How bright the haze about the newest points is, a share of the brush's color. - `glow_ms` (f32, 100, 10 to 1000): How long a point glows after it is made, in milliseconds. - `initial_velocity_decay` (f32, 0.98, 0.8 to 1): How much of that speed a point keeps from one step to the next. - `initial_velocity_force_scale` (f32, 0.3, 0 to 1): How much of the hand's own speed a new point sets out with. - `noise_angle_offset` (f32, 0, -3.2 to 3.2): An angle added to the way the noise pushes, in radians. - `noise_angle_scale` (f32, 15.708, 0 to 40): How far the noise turns the way it pushes, in radians over its range: higher, and the strands turn more often. - `noise_fallout` (f32, 0.65, 0 to 1): How much each octave of the noise counts, against the one before. - `noise_force_scale` (f32, 0.35, 0 to 1): How hard the noise pushes the strands: at 0 they run straight. - `noise_octaves` (f32, 8, 1 to 16): How many octaves of noise are summed (16 at most). - `noise_space_scale` (f32, 0.02, 0.001 to 0.1): How finely the noise varies across the page: larger, and the strands swirl tighter. - `noise_time_scale` (f32, 0.005, 0 to 0.05): How fast the noise changes as the stroke goes on. - `resting_distance` (f32, 0, 0 to 10): How far apart neighboring points come to rest, in the strand's units. - `rigidity` (f32, 0.2, 0 to 0.33): How hard neighboring points pull together; past 1 / (1 + 2 × friction), 0.339 at the default friction, a strand can blow up. - `scatter` (f32, 40, 0 to 200): How far the haze spreads about the young points, in the brush's units. - `self` (bytes, null): - `start_life` (f32, 150, 10 to 1024): How many steps a point lives, 480 a second: how long the trail is (1024 at most). - `strand` (f32, 1, 0 to 1): How bright the strand is, a share of the brush's color: 0 draws the haze alone. ### Sumi (`sumi`) The ink brush, wet and made for blots, in the symmetry and the color chosen. (brush `ink`, on paper's canvas) - `clump` (f32, 0.6, 0.1 to 3): How wide a clump of hair is, a share of the radius: it holds its ink together and runs dry on its own. - `drag` (f32, 0.85, 0 to 1): How much of the tuft stays lying on the paper as the brush is pulled: 0 stretches it to its tip. - `edge` (f32, 0.9, 0 to 2): How much drier the stroke's sides are: more, and its edges feather. - `flutter` (f32, 0.9, 0 to 1): How much the pressure the tuft feels wavers along the stroke, as the paper catches the hair. - `flyaway` (f32, 0.45, 0 to 1): How far past the pressed body stray hairs reach, a share of its half width. - `friction` (f32, 0.6, 0 to 1): How much the paper holds the tip against the tuft's spring. - `gather_ms` (f32, 500, 1 to 500): How long splayed bristles take to gather again once the pressure is off. - `grain_depth` (f32, 0.25, 0 to 2): How deep the paper's tooth is to the driest ink. - `hair` (f32, 2.5, 0.3 to 6): How wide a strand of hair is across the stroke, which a clump nearly dry breaks along. - `hairs` (u32, 1, 0 to 1): 1 draws the hairs' texture; 0 lays solid ink. - `length` (f32, 80, 16 to 160): The tuft's length, root to tip, at rest. - `lift_ms` (f32, 90, 0 to 400): How long a released mouse's brush travels on as it lifts: harai and hane. - `load` (f32, 850, 100 to 8000): How far a clump of hair goes, on average, before half its ink is gone: how long a stroke stays wet. - `mouse_pressure` (f32, 0.8, 0.05 to 1): The pressure a mouse or finger stroke settles to. - `press` (f32, 0.9, 0.1 to 0.95): How far the root sinks at full pressure, a share of the length. - `press_in_px` (f32, 8, 0 to 200): A mouse or finger stroke presses in over this much of its travel. - `pressure_curve` (f32, 1.3, 0.3 to 3): A pen's pressure is raised to this. - `radius` (f32, 22, 2 to 40): The tuft's radius at the root. - `scale` (f32, 1, 0.25 to 2): A factor on every length: the tuft's, and how far its ink goes. - `self` (bytes, null): - `settle_ms` (f32, 60, 1 to 200): How long the hand takes to lower or raise the brush to where the pressure puts it. - `skip` (f32, 0.4, 0 to 2): How readily a dry, lightly pressed hair leaves the paper: the broom at a dry stroke's end. - `speed_dry` (f32, 0.3, 0 to 1): How much speed dries a stroke. - `speed_light` (f32, 0.5, 0 to 1): How much speed lightens a mouse or finger stroke. - `speed_stretch` (f32, 0.5, 0 to 1): How much of that a quick stroke loses, stretching thinner. - `spread` (f32, 1.5, 0 to 2): How far pressed bristles splay sideways, for each pixel they are pressed in. - `stamp_every` (u32, 1, 1 to 16): The fewest steps from one stamp to the next. - `stamp_px` (f32, 0.5, 0 to 4): How far some part of the footprint must come before it is stamped again; 0 stamps at every step. - `stiffness` (f32, 0.3, 0 to 1): Where the tuft bends: a stiff one stays straight near the root and bends near the paper. - `tilt` (f32, 0.8, 0 to 1): How much of the pen's lean the handle takes. - `tip_shape` (f32, 0.4, 0.2 to 2): How the tuft narrows to its tip: lower is blunter. - `tone` (f32, 1, 0 to 2): How dark the ink is, a factor on the copy's color. - `water` (f32, 1.3, 0 to 1.5): How much ink the brush picks up as it comes down: below 1 it starts dry. ### Swoosh (`swoosh`) A string on springs, swung by the pointer: it paints the sweep of every swing. (brush `swoosh`, on silk's canvas) - `aim` (vec2, [0, 0]): Where a launched string's target sets out from, as an offset from the lift. - `damping` (f32, 0.975, 0.9 to 0.995): The part of its velocity a point keeps from one step to the next: how long the string keeps swinging. - `dots.color` (vec4, [0.45, 0.45, 0.45, 1]): The preview ring's color when it shows fully, premultiplied. - `dots.radius` (f32, 4, 1 to 20): The preview ring's radius, in the pointer's pixels. - `dots.width` (f32, 1.5, 0.5 to 6): The preview ring's line width, in the pointer's pixels. - `dt` (f32, 0.1, 0.02 to 0.3): The string's time step, a tick's worth: larger, and it swings faster. - `flight` (u32, 0): 0 for a string the pointer carries (Swoosh); otherwise the ticks a launched one flies (Fireworks). - `gravity` (f32, 1, 0 to 3): How hard a launched string falls: what is added to its target's velocity downward each tick. - `input_stiffness` (f32, 0.9, 0 to 2): How hard the points pull toward the pointer, most at the end it holds. - `launch` (vec2, [0, 0]): The target's velocity as it sets out, in the stroke's pixels a tick. - `points` (u32, 7, 4 to 32): How many points the string has (4 to 32). - `rand_factor` (f32, 5, 0 to 20): How hard a moving point is jostled. - `relaxation` (f32, 0.01, 0 to 0.2): How much of the distance between two neighbors closes after each step. - `seed` (u32, 1234): The seed of the string's randomness, which the spec draws for each stroke. - `self` (bytes, null): - `spacing` (f32, 1.5, 0.5 to 8): The distance between the curves a step paints of the sweep, where it moved most, in the stroke's pixels. - `stick_stiffness` (f32, 1, 0 to 2): How hard each point pulls toward its neighbors. - `width` (f32, 1, 0.25 to 4): The curves' width, in the stroke's pixels. ### Swoosh + Glow (`swoosh-glow`) The swoosh brush's string with Silk 2's haze about the way the pointer went, in the symmetry and the color chosen. (brush `swoosh`, on silk's canvas) - `aim` (vec2, [0, 0]): Where a launched string's target sets out from, as an offset from the lift. - `damping` (f32, 0.975, 0.9 to 0.995): The part of its velocity a point keeps from one step to the next: how long the string keeps swinging. - `dots.color` (vec4, [0.45, 0.45, 0.45, 1]): The preview ring's color when it shows fully, premultiplied. - `dots.radius` (f32, 4, 1 to 20): The preview ring's radius, in the pointer's pixels. - `dots.width` (f32, 1.5, 0.5 to 6): The preview ring's line width, in the pointer's pixels. - `dt` (f32, 0.1, 0.02 to 0.3): The string's time step, a tick's worth: larger, and it swings faster. - `flight` (u32, 0): 0 for a string the pointer carries (Swoosh); otherwise the ticks a launched one flies (Fireworks). - `gravity` (f32, 1, 0 to 3): How hard a launched string falls: what is added to its target's velocity downward each tick. - `input_stiffness` (f32, 0.9, 0 to 2): How hard the points pull toward the pointer, most at the end it holds. - `launch` (vec2, [0, 0]): The target's velocity as it sets out, in the stroke's pixels a tick. - `points` (u32, 7, 4 to 32): How many points the string has (4 to 32). - `rand_factor` (f32, 5, 0 to 20): How hard a moving point is jostled. - `relaxation` (f32, 0.01, 0 to 0.2): How much of the distance between two neighbors closes after each step. - `seed` (u32, 1234): The seed of the string's randomness, which the spec draws for each stroke. - `self` (bytes, null): - `spacing` (f32, 1.5, 0.5 to 8): The distance between the curves a step paints of the sweep, where it moved most, in the stroke's pixels. - `stick_stiffness` (f32, 1, 0 to 2): How hard each point pulls toward its neighbors. - `width` (f32, 1, 0.25 to 4): The curves' width, in the stroke's pixels. ### Sketchy (`sketchy`) Harmony's sketchy brush: faint lines tie the pointer to where the stroke has been nearby. (brush `sketchy`, on silk's canvas) - `dots.color` (vec4, [0.45, 0.45, 0.45, 1]): The preview ring's color when it shows fully, premultiplied. - `dots.radius` (f32, 4, 1 to 20): The preview ring's radius, in the pointer's pixels. - `dots.width` (f32, 1.5, 0.5 to 6): The preview ring's line width, in the pointer's pixels. - `inset` (f32, 0.3, 0 to 0.49): The part of each line's length cut from either end. - `jitter` (f32, 2.5, 0 to 10): How far each end of a line strays, at most, in the stroke's pixels. - `passes` (u32, 2, 1 to 8): How many times each place nearby is considered at a move (8 at most). - `reach` (f32, 70, 20 to 200): How far back the web's lines reach, in the stroke's pixels. - `seed` (u32, 1234): The seed of the web's randomness, which the spec draws for each stroke. - `self` (bytes, null): - `spacing` (f32, 5, 1 to 20): How far the pointer moves between the stroke's places, in the stroke's pixels. - `width` (f32, 1, 0.25 to 4): The lines' width, in the stroke's pixels. ### Googly (`googly`) A googly eye, put down where the pointer lifts, whose pupil follows the pointer. (brush `googly`, a tool: used on any drawing) - `pupil_gap` (f32, 2, 0 to 8): How far short of the eyeball's edge the pupil stops, in the stroke's pixels. - `pupil_radius` (f32, 0.45, 0.1 to 0.9): The pupil's radius, a share of the eyeball's. - `radius` (f32, 13, 4 to 40): The eyeball's radius, in the stroke's pixels. - `rim` (f32, 1.5, 0 to 6): The width of the dark rim about the eyeball, in the stroke's pixels. - `self` (bytes, null): ## Engine Every export of the engine, for `silk.engine(name, ...args)`: its name, its parameters (a leading `time` is filled in when left out), what it returns, and what it does (made from engine/zig/engine.zig's doc comments). ### can_undo `can_undo() -> bool` Whether an undo would do anything now (never in infinite mode); `engine_undo` asks the same. ### can_redo `can_redo() -> bool` Whether a redo would do anything now (never in infinite mode); `engine_redo` asks the same. ### has_group `has_group(handle: u32) -> bool` Whether `handle` names a group that is alive: a stroke's in the drawing or among the previews, or a prop's. Any value may be asked about; one that names no group is false. A group exists from the tick its `add_group` reaches. A control's groups have no handle (ui.zig). ### infinite_mode_on `infinite_mode_on(time: f32) -> void` Infinite mode from `time`: what is drawn on the layers above 0 is drawn down onto layer 0, which blurs away, and cleared, the history goes, and strokes take turns on as many layers as `engine_set_infinite_layers` last chose (`constants.infinite_layers` unless it did). When a layer's turn comes round again, what it holds goes down onto layer 0 the same way. In infinite mode already, it begins again, with every stroke kept going down to fade. ### infinite_mode_off `infinite_mode_off(time: f32) -> void` Paint mode from `time`, every stroke on the layer `engine_set_layer` chose (1 unless it did); the history goes here too. ### set_infinite_layers `set_infinite_layers(count: u32) -> void` How many strokes infinite mode keeps before it lets them fade, one to a layer, from the next `engine_infinite_mode_on`: 1 to `constants.paint_layers`. Any other count is ignored with a warning. ### set_layer `set_layer(layer: u32) -> void` The layer the next stroke is drawn on in paint mode, 1 (the bottom) to `constants.paint_layers`: each layer composites over the ones below it. Read as a stroke begins, as its size is; a preview in progress moves to it. Any other layer is ignored with a warning. ### is_infinite_mode_enabled `is_infinite_mode_enabled() -> bool` Whether the drawing is in infinite mode as of the last command executed (a switch enqueued for a later time is not in effect yet). ### brush_info `brush_info() -> *const Bytes` The parameter tables of the brush types (`BrushDoc` above), as JSON; static. ### stroke `stroke() -> *const Words` The stroke being drawn, or about to be (plans/stroke-settings-plan.md): the stroke the pointer over the canvas makes, from the moment it is over the canvas (a mouse or a pen hovering, before it presses; a finger as it touches) until it lifts or leaves. As words: whether it is down, its symmetry (its turns, and 1 or 0 for whether they spiral and whether each is mirrored), and its groups, how many and then three words each: its handle (`engine_write`'s), its brush type (`engine_brush_info`'s) and how many brushes it has. None, no words, while the pointer is away. A group's brushes are its copies, numbered in the order the symmetry makes them, the stroke itself first: for each turn, for each spiral step, the copy and then its mirror image, so copy `mirror + M × (spiral + S × turn)`, M 2 when mirrored (else 1) and S 4 with the spiral (else 1). A group of more brushes than copies holds them that many times over, brush `i` copy `i` mod the copies (Fireworks' strings). A stroke's first group's handle names it: a new one is a new stroke. Settings written before it is down are its start, and cost its tape nothing. ### add_group `add_group(time: f32, brush_type: u32, num_brushes: u32) -> u32` A group of `num_brushes` brushes of `brush_type` (brush.BrushType), added at `time`; returns its handle. An unknown brush type panics. A group of no brushes is added with a warning: it has no state to tick, so it is not alive and goes at its first tick. A group of more than `constants.brushes_per_group_max` brushes, or one added when `constants.groups_max` groups are alive, is refused when its `add_group` executes, and counted for the frame's warning; its handle then names no group, so writes to it are ignored as writes to an ended group are, and `engine_has_group` is false for it. ### begin_stroke `begin_stroke(time: f32, handle: u32) -> void` A stroke begins at `time` on the layer of the group `handle` names, the first of its groups (plans/multibrush-plan.md): the drawing is snapshotted for undo (and in infinite mode the layer is faded out to layer 0 and cleared, and the next group gets the next layer). Every group activated after it is one of its groups, until the next stroke begins, so one undo takes them all back. A handle on a layer strokes are not drawn on is ignored with a warning. ### activate_group `activate_group(time: f32, handle: u32) -> void` Activates the group `handle` names at `time`: it begins to draw, its preview entering the drawing, and joins the stroke begun last (`engine_begin_stroke`). A handle that names no preview when the activation executes (a group that already draws, has ended, or never was) is ignored with a warning. ### copy_layer_to_layer `copy_layer_to_layer(time: f32, source_layer_index: u8, destination_layer_index: u8) -> void` Copies one layer over another at `time`: the texture only, so any layer a host names may be named, the wet, composite and clear-fade layers included. An index past them (`constants.host_layers`), or a source that is also the destination, is a copy WebGPU rejects, so it is ignored with a warning. ### draw_layer_to_layer `draw_layer_to_layer(time: f32, source_layer_index: u8, destination_layer_index: u8) -> void` Draws one layer of the drawing onto another at `time` (Document.drawLayerToLayer), which reads the source's dirty flag and white point: both indices must be below the drawing's layer count, `constants.drawing_layers` (the blur layer and the paint layers), and they must differ, since a pass cannot sample the layer it draws into, or the draw is ignored with a warning. ### write_room `write_room(bytes: u32) -> *const Bytes` Room for the values of a write (`engine_write`): `bytes` of engine memory, which a host fills with them and then calls `engine_write`, as it fills `engine_tape_load`'s with a tape. Valid until the next call into the engine. More than a write may hold (`constants.command_bytes_max`) is refused with a warning, and the room has no bytes. ### write `write(time: f32, handle: u32, offset: u32, start: u32, step: u32, end: u32, len: u32, count: u32) -> void` A write to a stroke's settings (plans/stroke-settings-plan.md), from the room `engine_write_room` gave: at `time`, into the parameter at byte `offset` of the brushes of the group `handle` names, from brush `start` by `step` below `end` (0: to the end of the group), `count` values of `len` bytes each. One value (`count` 1) goes into every brush the range reaches; more are one for each of them, in order, and must be as many (the write is ignored with a warning when they are not). A value is packed as its parameter's type says (`engine_brush_info`): an f32 or a u32, little-endian, or the f32s of a vector or of a matrix's columns in order. A write is ignored when its group does not exist (logged at info, since a spec's writes can outlive its group), and with a warning when its values do not fit inside one brush at `offset` (`engine_brush_info` has each brush type's size, as its `self` parameter), when `len`, `count` or `step` is 0, or when the room holds fewer than `len × count` bytes. A range that runs past the group's last brush stops there, however large `start` or `end` is. ### alloc `alloc(now_ms: f32, surface_format: u32) -> *Engine` `surface_format` is the texture format of the surface the driver presents to (gpu.TextureFormat: 1 for bgra8unorm, 2 for rgba8unorm; the canvas's preferred format in the browser), which the pipelines that draw into it must declare. A host no longer says how many layers it has: every layer is a texture of its own, which the engine makes the first frame the layer is used (until 2026-09-26 a host passed the counts of the two layer arrays, 10 and 10). An unknown surface format panics, and so does a `now_ms` that is not finite, from which no frame time is ever far enough along to tick. ### frame `frame(now_ms: f64) -> *const Stream` A frame at `now_ms`, a double as the browser's clock gives it (the simulation rounds it to its f32; the session's specs compute with the double): the previous frame's words and upload bytes are released, the ticks and the presentation run, and the frame's device stream (gpu.zig) is returned as words. The words, and the bytes their `write_buffer` ops point at, stay valid until the next call; the renderer keeps them, so nothing is copied. Frame times never run backwards (`requestAnimationFrame`'s do not): a time earlier than the last frame's, or not a number, is held at the last frame's with a warning, as `Sim.encodeCommand` holds a command that arrives out of order or late. Everything downstream then sees a clock that only moves forward: the tick count, the blur-fade and clear-fade timing, and the specs' frame time. ### pointer `pointer(id: u32, kind: u32, time: f64, x: f64, y: f64, pressure: f64, tilt_angle: f64, tilt_strength: f64, pointer_type: u32, buttons: u32) -> void` A pointer event of pointer `id` (PointerEvent.pointerId: a page forwards every pointer, and the engine decides which draw and which move the camera, input.zig; `kind` is session.Kind, `pointer_type` session.PointerType, `buttons` the buttons held as PointerEvent.buttons reports them, or 0): the stroke's lifecycle and the selected spec's writes follow from it. An unknown kind or pointer type panics. A position more than `constants.pointer_coordinate_max` from the canvas's center in either coordinate, or not a number, is held at the last event's with a warning, and the event goes on from there, so it still starts, moves or ends the stroke: far enough out, the curves' geometry would overflow to NaN. The pressure and tilt are used as they come: ones that are not finite make geometry that draws nothing sensible, but trap nowhere (probed on every spec). ### wheel `wheel(time: f64, x: f64, y: f64, dx: f64, dy: f64, mode: u32, modifiers: u32) -> void` The wheel (WheelEvent): `dy` zooms about the cursor at (`x`, `y`), canvas-centered CSS px as a pointer's are, by 2^(−dy · c) with `c` by `mode` (WheelEvent.deltaMode: 0 pixels, 1 lines, 2 pages), and ten times as fast for a trackpad's pinch (`modifiers` bit 1, `ctrlKey`); a plain scroll zooms too (plans/zoom-plan.md, section 3). `dx` does nothing. Ignored while a stroke is down. An unknown mode panics; a position or a delta that is not finite is ignored with a warning. ### gesture `gesture(phase: u32, time: f64, x: f64, y: f64, scale: f64, rotation: f64) -> void` Safari's pinch (GestureEvent): `phase` 0 begins it (gesturestart), 1 changes it (gesturechange) and 2 ends it (gestureend); `scale` and `rotation` (degrees) are the gesture's since it began, about the cursor at (`x`, `y`). Ignored while a finger is down (Safari sends these for a touch pinch as well as its pointers) or a stroke is. An unknown phase panics; values that are not finite, or a scale that is not positive, are ignored with a warning. ### set_tiling `set_tiling(time: f64, group: u32) -> void` The wallpaper group the canvas is one tile of, from `time` (plans/zoom-plan.md, section 7): tiling.zig's `Group`, 0 for none; p1 until one is set, which at 1× unpanned changes nothing. Outside the history, so a change shows the drawing under the new group at the next frame and nothing is redrawn. An unknown group is ignored with a warning. ### view_home `view_home(time: f64) -> void` Back to 1× unturned with the canvas in the middle of the view, eased (a key's). ### view `view() -> *const View` The camera, by address as the JSON documents are, refreshed by each call. ### set_view `set_view(zoom: f64, angle: f64, x: f64, y: f64) -> void` The camera (plans/zoom-plan.md, section 3): the view is the world scaled by `zoom`, turned by `angle` (radians, clockwise on screen, y being down), and moved by (`x`, `y`) CSS px. The identity (1, 0, 0, 0) is the canvas as it always was. A zoom is clamped to 1/8 to 32 (`constants.zoom_min`, `constants.zoom_max`); one that is not positive and finite, or an angle or a pan that is not finite, is ignored with a warning. Set while a stroke is down, it waits for the stroke to end, since the camera does not move under a pen. ### set_zoom_grid `set_zoom_grid(on: u32) -> void` Whether the picture shows the grid of the edges between the wallpaper's copies while the camera moves, fading in and out (`View.grid`; Yuri, 2026-09-25): nonzero for on, which it is not until a host asks. ### set_sliding `set_sliding(on: u32, time: f64) -> void` Whether a drag on the canvas slides the drawing under the tiling, on the torus its edges make, rather than drawing (input.zig's `setSliding`; the page's shift, held), nonzero; a slide under way is laid down at `time` when it stops. ### set_background `set_background(time: f32, r: f32, g: f32, b: f32) -> void` The color behind the drawing from `time`, red, green and blue from 0 to 1 as the surface shows them (the page's grounds; Yuri, 2026-09-26): the canvas's own, recolored under the drawing, which stays as it is, until a spec on another canvas is chosen. A component out of range, or not a number, is ignored with a warning, the color with it. ### field_room `field_room(width: u32, height: u32) -> *const Bytes` Room for the field's cells (field.zig): `width × height` of them in rows from the top, 12 bytes each, little-endian: a push (two f32s, x and y, in the brush's units a substep, the units of the noise's push, whose Wavy is 0.35; its direction the tile's) and a region (a u32, 0 for open). A host fills them and lays them over the canvas with `engine_set_field`; the field is off from this call until it does. More than `constants.field_cells_max` cells are refused with a warning, and the room has no bytes (the field is then off). ### set_field `set_field(x0: f32, y0: f32, cell_px: f32, drift: f32, comb: f32, stretch: f32, walls: u32, paint: u32) -> void` The field laid over the canvas from its room (`engine_field_room`), felt by the brushes from their next tick: its cells `cell_px` CSS px square, the first's top left corner at (`x0`, `y0`) in the tile, the canvas's CSS px about its center, y down. A silk curve's points are pushed `drift` times the cells' pushes a substep (1 as the cells say, 0 not at all), its segments turned toward the pushes' lines `comb` times their length of the way a substep (0 not at all), each combed segment laid `stretch` brush units long along the line (0 keeps its own length); and the regions are walls as `walls` says: 0, a stroke keeps to the region it began in (a coloring page's cells), or 1, each point to the region it is in (a picture's edges, which strands do not drift across, whatever the hand crosses); and a silk strand is laid as paint, over what is under it, where `paint` is nonzero, rather than added as light (a page composites its layers as paint to match: `engine_set_layer_medium`). A room of no cells, or a `cell_px` that is not positive, takes the field away; so does a `drift`, a `comb` or a `stretch` that is negative or not a number, or unknown `walls`, with a warning. The field is the page's, as the canvas's size is: no tape holds it. ### set_hand `set_hand(on: u32) -> void` Whether a drag on the canvas moves the view rather than drawing, nonzero (input.zig's `setHand`: the page's cmd, held): the hand over the tiled map. ### layer_texture `layer_texture(layer: u32) -> u32` The id of the texture that holds layer `layer` (constants.zig, "the layers": the drawing's layers, then the wet, composite and clear-fade layers, below `constants.host_layers`), as of the last frame, for a host that reads the layers back (the tests hash them). 0 for a layer that has no texture, since nothing has used it yet, which holds nothing, and for an index past the layers a host names, which is ignored with a warning. ### undo_reach `undo_reach() -> u32` The fewest undo levels that serve a replay of the session so far exactly as the engine's served it (undo.zig's `Undo.reach`): 0 for a session in which no undo, redo or dropped stroke restored a snapshot, and the engine's levels otherwise. A replay capped at that many (`engine_set_undo_levels`) draws the same, and one of a session that never undid keeps no history at all, so copies out no stroke's tiles for one (Yuri, 2026-09-26: "we also don't need undo for an export"; client/src/export.js). (Until the history was copy-on-write tiles, 2026-09-27, a level was a layer, and those a replay did not keep were layers it never made.) ### set_history_budget `set_history_budget(layers: u32) -> void` The undo history's memory from now on: as many strokes as fit in `layers` layers' worth of tiles, or with 0 as many as the levels allow (`engine_set_undo_levels`), whatever they changed. The default is `constants.history_budget_layers`. A replay keeps no budget of its own while its tape plays: it lets go what the tape says was let go (`forget`). ### history_stats `history_stats(which: u32) -> u32` What the undo history holds (history.zig): its entries (`which` 0), the tiles they keep (1), the kilobytes of the pool's pages (2), and the kilobytes of the pages the most slots it has given out at once since the drawing began lie on (any other): what a replay of the drawing's tape at this size makes of them (`Pool.peak`), where the pool's own may still hold a drawing let go. ### set_undo_levels `set_undo_levels(levels: u32) -> void` At most `levels` undo levels from now on, 0 (no history, and no tile copied out for one) to `constants.undo_levels`: for a replay, set before its first call to what its session reached (`engine_undo_reach`). The oldest snapshots go if the history holds more. Any other count is ignored with a warning. ### capture `capture(transparent: u32, view: u32) -> u32` The drawing as an image: the next frame presents, so that it composites the drawing into the picture, the texture labeled "Picture", which a host reads back once that frame has run (the page's s saves it as a PNG). The drawing alone, at the layers' size, without what the frame draws over it (the preview's rings, the sparkles, the grid, the controls): on the canvas's ground, or on nothing when `transparent` is nonzero. With `view` nonzero, the frame's surface is the view as it is on screen, the tiling's copies and all, and nothing over it either: no grid, no dimming, no rings, sparkles or controls, for a host to read the surface back (the page's export of the view). Returns where the canvas is in the picture: `picture_border` texels in from its top left. ### hold_grid `hold_grid(on: u32) -> void` Whether the host holds the grid up, nonzero: it fades in and stays while held, and fades out once let go (the page's shift or cmd, held to slide the drawing or to pan the view; Yuri, 2026-09-26), where the host shows the grid. ### show_grid `show_grid(on: u32) -> void` Whether the host shows the grid outright, nonzero: held up, fading in, whether or not the view has a seam to show, and, let go, fading out (the page's tiling picker, while it is open; Yuri, 2026-09-27: the grid "should persist while p1 settings are up"), where the host shows the grid. ### recenter `recenter(time: f32) -> void` The drawing slid back at `time` so that its center is the canvas's again (the page's recenter button), what the slides moved moving back with it (`recenter`). The stroke in progress, or the preview, is let go first, as a control over the canvas lets it go, so that it does not move with the drawing from under the pointer. ### remove_googly_eyes `remove_googly_eyes(time: f32) -> void` Every googly eye put down taken away, from `time` (the page's eyes button, pressed once eyes are down): it fades out. ### center `center() -> *const Center` ### move_center `move_center(time: f32, dx: f32, dy: f32) -> void` The drawing's center moved by (`dx`, `dy`) CSS px from `time`, on its torus, and nothing else: the strokes stay, and the next turn and mirror about it (`move_center`; the page's center handle behind ?handle=center). A move that is not finite is ignored with a warning. ### has_googly_eyes `has_googly_eyes() -> bool` Whether googly eyes have been put down on the drawing (engine/zig/googly.zig): the page's eyes button then takes them away rather than putting more down. ### off_center `off_center() -> bool` Whether the drawing's center is off the canvas's: a slide moved it, or one is under way over a drawing that has anything to move (Yuri, 2026-09-26: the page's recenter shows as the drag begins, and not over an empty canvas, which a slide leaves as it was), and `engine_recenter` would bring it back. ### set_symmetry `set_symmetry(rotations: u32, mirror: u32, spiral: u32) -> void` A brush's own symmetry, for the strokes begun from now by the brushes that take it (specs.zig's `Symmetry`, the page's symmetry row): `rotations` turns about the drawing's center, 1 to `Symmetry.rotations_max`, each mirrored when `mirror` is nonzero and spiraling in when `spiral` is. A count out of range is ignored with a warning. ### set_instance_color `set_instance_color(mapping: u32) -> void` How each copy of the drawing is colored by its place (sim.zig's `InstanceColor`: 0 not at all, 1 by the angle it is turned, 2 by whether it is mirrored, 3 each its own; the page's ?instancecolor=): the brush's copies as their strokes begin, the wallpaper's in the present at once. A mapping it does not know is ignored with a warning. ### set_color `set_color(r: f32, g: f32, b: f32) -> void` One color for the strokes begun from now, linear red, green and blue from 0 to 1 (the page's swatches), in place of the pair the color picker settles on (`Session.setColor`). A component out of range, or not a number, is ignored with a warning, the color with it. ### set_white_point `set_white_point(time: f32, white_point: f32) -> void` The white point of the paint layers from `time` (the page's HDR slider; `Document.setWhitePoint`): the light a layer holds past it is rolled off toward white on a canvas that tone-maps (silk's), 5 until set. At least 1, where nothing is rolled off and the light past 1 is cut; one lower, or not a number, is ignored with a warning. ### set_layer_medium `set_layer_medium(time: f32, layer: u32, blend: u32, tone_map: bool) -> void` How layer `layer` of the drawing composites onto the canvas from `time` (`Document.setLayerMedium`): `blend` (render.zig's `Blend`: 0 normal, laid over what is under it; 1 light, added; 2 erase; 3 max) and whether it is tone-mapped at its white point. A new canvas gives every layer its own medium again. A layer that is not the drawing's, or a blend there is none of, is ignored with a warning. ### canvas `canvas() -> u32` The canvas the drawing is on, by its medium: its index in `Canvas.named` (0 silk, 1 paper), as the menu names a brush's home canvas (`engine_brush_info`), or `std.math.maxInt(u32)` for a canvas of another; as the last frame left it. A page offers the brushes of the drawing's canvas by it, and the palette that suits it. ### set_eraser `set_eraser() -> void` The eraser for the strokes begun from now, in place of a color (the page's last swatch; `Session.setEraser`): any brush takes away what it draws over. The erasing pipelines are asked for now, so the first stroke does not wait for them. ### set_crossing `set_crossing(on: u32) -> void` Whether the strokes begun from now go on across their tiles' edges into the next tiles (crossing.zig), nonzero, as by default, or are drawn into the canvas as they go, cut at its edge (`Session.setCrossing`). ### select_spec `select_spec(index: u32) -> void` The spec the next stroke uses, an index into `engine_specs`. An index past the end of the menu is ignored with a warning, and the spec stays what it was. ### set_brush_scale `set_brush_scale(scale: f32) -> void` How big the next stroke is, as a multiple of its spec's own size (the page's small, medium and large): its stroke space's scale, so the stroke is drawn that much bigger, silk's strands, their wander and their thread alike, and ink's tuft (plans/zoom-plan.md, section 5). Read as a stroke begins, so a stroke under way keeps its size and a preview in progress shows the new one. A scale that is not positive and finite is ignored with a warning, and the scale stays what it was. ### set_brush_sizing `set_brush_sizing(sizing: u32) -> void` How the brush's size answers the zoom (session.zig's `BrushSizing`): 0 keeps its size on screen, 1 its size in the drawing (the default, the session's and the page's). Read as a stroke begins; a preview shows it at once. An unknown value is ignored with a warning. ### specs `specs() -> *const Bytes` The menu: `[{"name":..,"description":..,"brush":..,"canvas":..,"debug":..}, ...]`, as JSON; static. ### seed_random `seed_random(seed: u32) -> void` Seeds the specs' randomness (the stipple's scatter), so a recording reproduces. ### set_stress `set_stress(n: u32, life: f32, groups: u32, copies: u32) -> void` The stress specs' knobs (session.Stress). The specs clamp the counts to the engine's limits (`constants.brushes_per_group_max` bristles or copies, `constants.groups_max` groups); `life` is a bristle's `start_life`, which the silk brush clamps to `constants.curve_points_max` (a longer curve per bristle, up to about two seconds of stroke). ### set_canvas_size `set_canvas_size(canvas_width: f32, canvas_height: f32) -> void` The canvas's size in CSS pixels, the coordinates the pointer arrives in. A size that is not positive and finite would send the world-to-screen transform to infinity or flip it, so it is ignored with a warning and the last size stays. ### set_box `set_box(width: f32, height: f32) -> void` The box the host shows the canvas in, in CSS px: where a drawing of any size would be shown, scaled down to fit it where it is larger (`View.fit`), so that what a fade holds of a drawing no longer shown is drawn where that drawing would be shown now (plans/robust-fades-plan.md). The view's, like the camera: never on the tape. Until a host sets one, every drawing is shown at its own size. A box that is not positive and finite is ignored with a warning. ### blank `blank() -> bool` Whether the drawing is blank, which a host gives the size of its box as the frame begins (plans/robust-fades-plan.md): nothing shows, nothing is left to redo, no tape plays, and no undo waits for its size (`Sim.blank`). Once a clear has run, its drawing kept for an undo keeps its own size, and its fade is drawn where the drawing showed. ### set_layer_size `set_layer_size(layer_width: f32, layer_height: f32) -> void` The size of the layer textures in texels (the canvas size times the pixel ratio), applied at the next frame: the drawing shown is cropped or padded about its center, and what belongs to a drawing no longer shown (a fade, a clear's drawing kept for an undo) keeps its own size (plans/robust-fades-plan.md). A texture's size is a whole number of texels, at least 1 and within a u32: any other is ignored with a warning and the last size stays. The engine does not know the device's own limit on a texture's size, so a size past it fails in the driver when the textures are created (a question for round 2's limits). ### set_layer_window `set_layer_window(x0: f32, y0: f32, width: f32, height: f32) -> void` A window onto the layers, for the export in pieces (plans/tiled-export-plan.md): from the next frame the layers hold texels `x0` to `x0 + width` and `y0` to `y0 + height` of the layers `engine_set_layer_size` makes, their textures that size, and what is drawn into them lands where it would in the whole, less the window's origin; a layer drawn into another, and the picture, are the same window. A width and height of 0 take the window away, back to the whole layer. A window whose size is not a texture size, or whose origin is not a whole number of texels from 0, is ignored with a warning. ### tape `tape() -> *const Bytes` The recording tape (plans/tape-plan.md, 4.8): what the engine has run on the drawing since the first stroke's down, as bytes that are the replay. They stay valid only until the next call into the engine, so a host copies them at once (`new Uint8Array(memory.buffer, ptr, len).slice()`). Empty before the first down. ### tape_drawing `tape_drawing() -> *const Bytes` The tape of the drawing under way (plans/tape-plan.md, 4.9): a header, then the recording tape from the drawing's beginning, where it was last emptied for good, as a share or an export plays it; valid, as `engine_tape`'s bytes are, until the next call. Empty before the first down. ### drawing_uses `drawing_uses() -> u32` What the drawing under way has used that an export in pieces cannot follow past a piece's edge (`Sim.Uses`), as bits: 1 its size changed after it began, 2 infinite mode, 4 an ink stroke, 8 a slide, 16 a tiling whose tile is a portion of the canvas. ### sizes `sizes() -> *const @import("tape.zig").Sizes` The drawing's size as the next frame begins with it (tape.zig's `Sizes`, `Sim.nextSizes`): the canvas's in CSS px, and the layers' in texels as the host asked for them, which a tape played changes, and an undo that brings back a clear's drawing kept at its own size. A host follows it before each frame, so that its surface is the frame's size. Refreshed by each call. ### tape_ticks `tape_ticks() -> u32` The recording tape's ticks since it began, the tick its next record runs at; 0 before the first down. ### tape_load `tape_load(len: u32) -> *const Bytes` Room for a tape of `len` bytes, at most `constants.tape_bytes_max`, into which the host copies one as it copied it out (`engine_tape`), for `engine_tape_play` to play: its address and length. A tape loaded before and not played through goes. A length past the limit is refused with a warning, and the room is empty. ### tape_tighten `tape_tighten(pause_ticks: u32) -> u32` The loaded tape made tight in place (tape.zig's `tighten`): the strokes undone for good taken out with their undos, an undo and its redo, a clear undone, and the time each spanned, and each stretch in which the drawing was still cut to `pause_ticks`. Its length in bytes, which the loaded tape is from here; 0 for bytes that are not a tape this engine plays, left as they are for `engine_tape_play` to refuse with its reason. ### tape_play `tape_play(scale: f32) -> bool` The loaded tape checked (tape.zig's `check`) and played from its start at `scale`, its layers that many times the tape's (an export's; 1 for the drawing as it was): its first drawing's sizes applied at once, then its records, its first drawing's settings first, as the next frame or `engine_advance` runs its ticks. False, with a warning saying why, for bytes that are not a tape this engine plays, or a scale that is not positive and finite. ### played_ticks `played_ticks() -> u32` The loaded tape's tick the replay has come to, the one its next records run at: after `engine_advance(n)`, n ticks on, or fewer where a change of size is next, which the next frame begins with. ### advance `advance(ticks: u32) -> *const Stream` `ticks` ticks of the playing tape, as one frame whose device stream it returns as `engine_frame` does: a change of size the frame begins with, then each tick's records and every group's step, on the tape's own clock; then the records of the tick to come. At most `constants.ticks_per_advance_max` ticks run, and fewer where a change of size is next, which the next frame begins with (`engine_played_ticks` says how far it came); nothing is presented. Once the tape's records have all run, the ticks go on all the same, as a live engine's do with nothing to do, while strokes play out and layer 0 fades. ### tape_checks `tape_checks() -> *const TapeChecks` A loaded tape's checksums as they have played (`Playback.checks`; plans/tape-plan.md, 4.12): how many matched the replay's own state and how many did not, and the first that did not, its tick on the tape and its stroke's handle, or `TapeChecks.none` for its tick while every one has matched; and how many were not compared, met once someone drew over the tape as it played (plans/tape-plan.md, 4.9). ### playing `playing() -> bool` Whether a tape plays (`Sim.playing`): records of it still to run, or strokes of the drawing still simulating. ### state_hash `state_hash() -> u64` The drawing's state hashed (`Sim.stateHash`): the same in a replay as in the session it replays, at the same tick. ### dealloc `dealloc() -> void` Frees the engine and everything it holds. The pointer must be one `engine_alloc` returned and not yet freed, and no export may be called with it afterwards. ### clear `clear(time: f32) -> void` Clears the drawing at `time`, with a shower of sparkles: when anything has been drawn, the drawing's groups, layers and history go, and what was there fades out over a second. ### undo `undo(time: f32) -> void` Undoes the last stroke at `time`, when `engine_can_undo`; or brings back the drawing a clear kept, at its own size: where the drawing shown since has another, the undo waits for the next frame, which begins at the kept drawing's size (`engine_sizes` says it, for a host to follow first), and the commands after it wait with it. ### redo `redo(time: f32) -> void` Redoes the last stroke undone at `time`, when `engine_can_redo`. ### ui_open `ui_open(control: u32) -> void` Opens a control over the canvas, ending the stroke in progress. `control` is `ui.Control`; an unknown one panics. At once, with no time: what the control holds changes now, and ticks from the next frame (ui.zig). ### ui_close `ui_close(control: u32) -> void` Closes a control, at once as `engine_ui_open` opens one. `control` is `ui.Control`; an unknown one panics. ### ui_state `ui_state() -> *const UiView` What a page shows of the controls, held by the engine and refreshed by each call.