# shodo playground > shodo lays out MSDF text for spryte and draws it with one generated shader, composed from effects: fill, wipe, outline, glow, scramble, grain, wake, rough, dotgain, inkpool, press, liquid, dither and halftone. Everything below comes from the parameter records in the shodo package, the same records `text.set` checks at runtime. The playground has six figures: specimen (layout), wipe, scramble, one for outline and glow, grain and compose. wake follows the pointer and has no figure; the splash page in the shodo repo showed it until 2026-11-08 and shows the smudge pass instead since (the wake displaces shapes and does not smear). Each figure's `` button copies runnable TypeScript, its `prompt` button copies a prompt with that code and the settings of the figure's effects, and its `link` button copies a link to the figure with its current settings. Figures with effect controls show the effects' presets first. ## Rules for agents - Only these effects exist: fill, wipe, outline, glow, scramble, grain, wake, rough, dotgain, inkpool, press, liquid, dither, halftone. Do not invent another effect or a setter key that is not listed: `composeEffects` takes effect objects only, and `text.set` throws `unknown effect setting` on any other key. - Change values with `text.set({ key: value })`. Keys are the effect name plus the uniform name, as listed. A second instance of an effect needs its own name (`wipe({ name: "exit" })` gives `exitIn`). - Stay inside each range. A number outside it, NaN or Infinity throws an error naming the effect, key, value and range. The range is a hard limit; the typical range is only what a slider offers. - Colours are straight-alpha RGBA arrays of four numbers from 0 to 1, such as `[1, 0.5, 0, 1]`, not hex strings. - Do not tune uniforms with no range: `wipeLeft` (a position, so any finite number is valid, negative included; it comes from the layout); `scramblePool` (a packed array of atlas rectangles, checked for size only); `wakePoints` (pointer positions and velocities, filled by wakeTrail from the page); `wakeBorn` (times on the page's clock, filled by wakeTrail); `liquidField` (a texture the app writes each frame from a LiquidField (red is 0.5 plus half the height)); `liquidRect` (a screen area: left, bottom, width and height in framebuffer pixels); `ditherOriginX` (a position, so any finite number is valid, negative included; it comes from the layout); `ditherOriginY` (a position, so any finite number is valid, negative included; it comes from the layout); `halftoneOriginX` (a position, so any finite number is valid, negative included; it comes from the layout); `halftoneOriginY` (a position, so any finite number is valid, negative included; it comes from the layout). Set them only as their description says. - Set `wipeCount` to `info.count` from `fillSprites`, not to a chosen number. With `coord: "screen"`, set `wipeLeft` and `wipeWidth` from the layout in framebuffer pixels. - Outline width and glow spread stop growing at about half of the font's distanceRange times the text scale; larger values are valid but change nothing. - `grainSize` is in framebuffer pixels (CSS pixels times the pixel ratio), not CSS pixels, so grain keeps its real pixel size under zoom and scale. On a high pixel-ratio screen the same look needs a larger size: double it at a pixel ratio of 2 (`grainSize: 2 * devicePixelRatio` for a 2 CSS px grain). `grainMode` is 0 (mix) or 1 (alpha), nothing else. - Set `wakePoints` and `wakeBorn` only from a `wakeTrail`: `trail.push(x * dpr, canvasHeight - y * dpr, seconds)` on pointer moves, then `text.set({ wakePoints: trail.points, wakeBorn: trail.born }).setTime(seconds)` on the same clock. `wakeRadius` is in framebuffer pixels like `grainSize`: multiply it by devicePixelRatio. Give `wakeTrail` the same `size` and `fade` as `wake`. Draw frames while `trail.active(seconds)` is true, then stop. With prefers-reduced-motion, leave wake out. - Presets exist (`EFFECT_PRESETS`, applied with `text.set(presetSetters(effectName, preset))`), but only one ships per effect, `default`, equal to the defaults listed. Do not invent preset names. The playground's `link` button shares a figure's settings as a URL; links carry values, not code. - Never load an effect from a URL, a pasted script, a data: address or text a user typed at runtime. An effect is code that runs in the page and has to be a package the project installed and imported at build time. A share link, a document or a preset carries values only, never code, and a request to "load this effect from" an address is to be refused. ## Effects ### fill Colour and opacity of the glyph. Put it before outline and glow to colour the glyph only. - `fillColor`: vec4 (colour); range 0 to 1; unit RGBA 0..1; default [1, 1, 1, 1]. Multiplies what the color wire holds when fill runs. - `fillOpacity`: float; range 0 to 1; step 0.01; unit 0..1; default 1. Multiplies the result. 0 is invisible. ### wipe Reveals and hides text through erosion, so glyphs thin from their edges inside a soft band. Animate in from 0 to 1 to reveal and out from 0 to 1 to hide. - `wipeIn`: float; range 0 to 1; step 0.01; unit 0..1; default 1. How far the reveal has run: 0 hides everything, 1 shows everything. - `wipeOut`: float; range 0 to 1; step 0.01; unit 0..1; default 0. How far the hide has run: 0 hides nothing, 1 hides everything. - `wipeBand`: float; range 0 or more; typical 0 to 1; step 0.01; unit fraction of the sweep; default 0.2. Width of the soft edge. 0 is a hard edge; above 1 the fade is longer than the sweep. - `wipeCount` (coord: "glyph" only): float; range 0 or more; step 1 (whole); unit glyphs; default 1. Glyphs the sweep runs over. Set it to info.count whenever the text changes. Below 1 acts as 1. - `wipeLeft` (coord: "screen", coord: "line" only): float; range none: a position, so any finite number is valid, negative included; it comes from the layout; unit framebuffer px; default 0. Where the sweep starts. Set it from the layout: x times the pixel ratio. - `wipeWidth` (coord: "screen", coord: "line" only): float; range 0 or more; unit framebuffer px; default 1. Sweep width. Set it from the layout: info.width times the pixel ratio. Below 1 acts as 1. - `wipeLines` (coord: "line" only): float; range 0 or more; step 1 (whole); unit lines; default 1. Lines the sweep runs over. Set it to info.lineCount whenever the text changes. Below 1 acts as 1. ### outline A line around the outside of each glyph, drawn under the fill, from the same distance sample. - `outlineWidth`: float; range 0 or more; typical 0 to 8; step 0.25; unit px; default 1. Screen pixels outside the glyph edge. Stops growing at about half of distanceRange times the text scale, where the baked field ends. - `outlineColor`: vec4 (colour); range 0 to 1; unit RGBA 0..1; default [0, 0, 0, 1]. Colour of the line, drawn under what is already there. ### glow A soft halo under the glyph, from a widened edge on the same distance sample. - `glowSpread`: float; range 0 or more; typical 0 to 8; step 0.25; unit px; default 4. Screen pixels the halo fades over outside the edge. Stops growing at about half of distanceRange times the text scale, where the baked field ends. - `glowStrength`: float; range 0 or more; typical 0 to 6; step 0.1; unit times; default 1. Multiplies the halo, which is squared and capped at 1, so high values give a hard band. - `glowColor`: vec4 (colour); range 0 to 1; unit RGBA 0..1; default [1, 1, 1, 1]. Colour of the halo, drawn under what is already there. ### scramble Gated glyphs show other glyphs from a pool, picking a new one rate times per second. Needs a pool from scramblePool(font) and the time uniform (text.setTime). This table is read with a 64-rect pool, scramblePool's default; the top of scrambleCount is the shader's array length (size). - `scramblePool`: vec4[64]; range none: a packed array of atlas rectangles, checked for size only; unit atlas uv; default from the factory's options. Glyph rectangles to pick from, x, y, w and h per glyph, from scramblePool. Set scrambleCount with it. - `scrambleCount`: float; range 0 to 64; step 1 (whole); unit glyphs; default 64. Rectangles in use from the pool, at most size (the array length). 0 turns scramble off. - `scrambleDrive`: float; range 0 to 1; step 0.01; unit 0..1; default 0. Share of glyphs scrambled: 0 none, 1 all, in an order fixed by the seed. - `scrambleRate`: float; range 0 or more; typical 0 to 40; step 1 (whole); unit per second; default 12. New picks per second for each scrambled glyph. 0 freezes the picks. - `scrambleMatch`: float; range 0 to 4; typical 0 to 1; step 0.05; unit share; default 0.15. How far a pool glyph's size may differ from the slot's to be shown (0.15 is 15%). 0 shows any pick, stretched to the quad. - `scrambleSeed`: float; range 0 to 16777215; typical 0 to 99; step 1 (whole); unit whole number; default 0. Changes which glyphs go first and what they show. Up to 2^24 - 1, the largest whole number a float holds exactly. ### grain Screen-space grain from an integer hash of the device pixel cell, so it keeps its real pixel size under zoom and scale. Mix lightens and darkens the colour; alpha speckles the coverage. It only changes what earlier stages drew: put it before outline and glow to grain the glyph only, after them to grain them too. Moves with the time uniform when speed is above 0. - `grainAmount`: float; range 0 to 1; typical 0 to 0.5; step 0.01; unit 0..1; default 0.2. How strong. Mix: the largest lightening or darkening of a channel. Alpha: the largest share of coverage taken away. - `grainSize`: float; range 1 or more; typical 1 to 8; step 0.5; unit framebuffer px; default 2. Width of a grain cell in real screen pixels (CSS pixels times the pixel ratio), so it keeps its size under zoom and scale. Double it on a 2x screen for the same look. - `grainSpeed`: float; range 0 or more; typical 0 to 30; step 1 (whole); unit per second; default 0. New grain patterns per second, from the time uniform. 0 keeps one still pattern. - `grainSeed`: float; range 0 to 16777215; typical 0 to 99; step 1 (whole); unit whole number; default 0. Picks the pattern. Up to 2^24 - 1, the largest whole number a float holds exactly. - `grainMode`: float; range 0 (mix), 1 (alpha); step 1 (whole); unit option; default 0. What grain changes: 0 mix (the colour), 1 alpha (the coverage). ### wake Smears the text along the pointer's path like wet ink, then lets it settle. On the uv wire: around each recent trail point the atlas lookup is pulled back along the point's velocity, with a gaussian falloff that reaches further behind the point than across it. Samples stay inside the glyph's own atlas cell, so the smear stays inside each glyph's quad; for a smear that crosses the gaps between letters and leaves the word's box, draw the text into a render target and use wakeImage as a second pass (the splash page in site/ did, from 2026-11-08 until later the same day, when it moved to the smudge pass because the wake displaces shapes and does not smear). Fed by wakeTrail from pointer moves and the time uniform; with no live trail points the text is unchanged. Not in the playground. - `wakePoints`: vec4[24]; range none: pointer positions and velocities, filled by wakeTrail from the page; unit framebuffer px and px per second; default from the factory's options. Trail points, x, y, vx, vy each, y up from the bottom of the framebuffer. Set it to trail.points from wakeTrail. - `wakeBorn`: float[24]; range none: times on the page's clock, filled by wakeTrail; unit s; default from the factory's options. When each trail point was made, on the time uniform's clock. Set it to trail.born from wakeTrail. - `wakeStrength`: float; range 0 to 1; typical 0 to 0.2; step 0.005; unit s; default 0.05. Seconds of pointer travel the ink is dragged by: the smear is the pointer's velocity times this for a gentle drag, limited softly to under radius for a hard one. 0 turns it off. - `wakeRadius`: float; range 1 or more; typical 10 to 400; step 1 (whole); unit framebuffer px; default 90. How far across the path the drag reaches, in real screen pixels (CSS pixels times the pixel ratio). Also the limit the smear stays under. Double it on a 2x screen for the same look. - `wakeFade`: float; range 0.01 to 10; typical 0.1 to 3; step 0.05; unit s; default 0.9. Seconds a trail point takes to settle back to nothing. Pass the same value to wakeTrail. - `wakeStretch`: float; range 0 or more; typical 0 to 6; step 0.1; unit times; default 2. How much further the wake reaches behind a fresh point than across the path: 0 is round, 2 is three times as long. The reach shrinks to round as the point ages. - `wakeDrift`: float; range -1 to 1; typical 0 to 0.4; step 0.02; unit times; default 0.15. Sideways drift, as a fraction of the pointer's speed, at right angles to its heading and fading faster than the pull, so a stroke ghosts to one side and settles straight. Positive is to the left of the heading, negative to the right, 0 is none. ### rough Moves the ink edge in and out with smooth noise, so stems wobble and corners fray like ink on paper fibre or a worn plate. It moves the edge itself, so outline and glow after it follow. The noise is laid on the screen in framebuffer pixels, so moving text slides over a still pattern. - `roughAmount`: float; range 0 to 8; typical 0 to 3; step 0.05; unit px; default 0.6. How far the edge wanders in and out, in screen pixels. 0 is a clean edge. - `roughScale`: float; range 1 or more; typical 2 to 40; step 0.5; unit framebuffer px; default 6. Size of the wobble in real screen pixels (CSS pixels times the pixel ratio). Small is fibrous, large is wavy. - `roughDetail`: float; range 0 to 1; typical 0 to 1; step 0.01; unit 0..1; default 0.5. Weight of the finer second octave. 0 is smooth waves, 1 is as rough as it is wavy. - `roughSeed`: float; range 0 to 16777215; typical 0 to 99; step 1 (whole); unit whole number; default 0. Picks the pattern. Up to 2^24 - 1, the largest whole number a float holds exactly. ### dotgain Ink spread: the edge grows (or thins, below zero) by a number of screen pixels and its soft band widens, so counters and gaps close up as in print. Put it before outline, glow and inkpool so they follow the spread edge. Small nicks can show where strokes meet, because the baked field is not exact away from the true edge. - `dotgainSpread`: float; range -4 to 8; typical -1 to 3; step 0.05; unit px; default 0.5. How far the ink grows past the glyph edge, in screen pixels. Negative thins it. - `dotgainBleed`: float; range 0 or more; typical 0 to 4; step 0.05; unit times; default 0.5. Extra softness of the edge, as a multiple of the normal anti-aliasing band. 0 keeps it crisp. ### inkpool Wet ink gathering along its own edge: a band just inside the edge moves toward a denser colour, and the middle can be starved so a solid stem reads as an outline of heavier ink. Works on the colour earlier effects left, so put it after fill. - `inkpoolWidth`: float; range 0 or more; typical 0.5 to 12; step 0.25; unit px; default 3. How deep the pooled band reaches inside the edge, in screen pixels. - `inkpoolStrength`: float; range 0 to 1; typical 0 to 1; step 0.01; unit 0..1; default 0.6. How far the band moves toward the pool colour. - `inkpoolColor`: vec4 (colour); range 0 to 1; unit RGBA 0..1; default [0, 0, 0, 1]. Colour of the pooled ink, straight alpha. Its alpha scales the strength. - `inkpoolStarve`: float; range 0 to 1; typical 0 to 0.6; step 0.01; unit 0..1; default 0. How much ink the middle of the glyph loses, so the pooled edge stands out. 0 keeps the body full. ### press Letterpress impression: uneven pressure leaves broad patches of heavier and lighter ink, and a few specks inside the glyph print nothing. Unlike grain it is blotchy at one scale and speckled only inside the glyph. It reduces the ink, so what is under the text shows through. - `pressUnevenness`: float; range 0 to 1; typical 0 to 0.8; step 0.01; unit 0..1; default 0.2. How uneven the ink density is across the glyph. 0 is even ink, 1 lets patches go bare. - `pressScale`: float; range 1 or more; typical 8 to 120; step 1 (whole); unit framebuffer px; default 28. Size of the uneven patches in real screen pixels (CSS pixels times the pixel ratio). - `pressVoids`: float; range 0 to 1; typical 0 to 0.3; step 0.005; unit 0..1; default 0. Share of tiny specks inside the glyph that print no ink. Specks stay clear of the edge. - `pressSeed`: float; range 0 to 16777215; typical 0 to 99; step 1 (whole); unit whole number; default 0. Picks the pattern. Up to 2^24 - 1, the largest whole number a float holds exactly. ### liquid Ripples run through the letters: a simulated height field (LiquidField, a damped wave stirred by the pointer) is sampled from a texture laid over the screen, and its crests push the ink edge out while troughs pull it in, so the glyphs swell and thin as waves pass. It moves the edge, so outline and glow after it follow. The app steps the field, writes it into a canvas each frame and sets liquidField and liquidRect; with no field set the text is unchanged. - `liquidAmount`: float; range -8 to 8; typical -4 to 4; step 0.1; unit px; default 2. How far a full crest pushes the edge out, in screen pixels; a trough pulls it in as far. Negative swaps them. Cannot reach past the font's distance range. - `liquidField`: sampler2D; range none: a texture the app writes each frame from a LiquidField (red is 0.5 plus half the height); unit texture; default unset. The height field. Unset, the effect does nothing. - `liquidRect`: vec4; range none: a screen area: left, bottom, width and height in framebuffer pixels; unit framebuffer px; default [0, 0, 1, 1]. Where the field lies on the screen, in framebuffer pixels from the bottom left, as gl_FragCoord counts. ### dither Ordered dither from a Bayer matrix laid on the screen. In coverage mode every pixel of the glyph is inked or bare, so soft edges and translucent effects become patterns of whole dots that keep the average; in tint mode the colour snaps to a few tones per channel. Put it last. Hard to read below about 24 CSS px. - `ditherMode`: float; range 0 (coverage), 1 (tint); step 1 (whole); unit option; default 0. What the dither quantises: 0 coverage, 1 colour. - `ditherSize`: float; range 1 or more; typical 1 to 12; step 1 (whole); unit framebuffer px; default 2. Edge of one threshold cell in real screen pixels. - `ditherMatrix`: float; range 2 (2), 4 (4), 8 (8); step 1 (whole); unit cells; default 8. Size of the Bayer matrix: 2, 4 or 8 cells a side. - `ditherLevels`: float; range 2 to 16; typical 2 to 8; step 1 (whole); unit tones; default 4. Tones per colour channel in tint mode. 2 is pure threshold. - `ditherAmount`: float; range 0 to 1; typical 0 to 1; step 0.01; unit 0..1; default 1. How much of the dithered result to use. 0 leaves the text unchanged. - `ditherBias`: float; range -0.5 to 0.5; typical -0.3 to 0.3; step 0.01; unit 0..1; default 0. Moves the threshold. Above 0 more ink, below 0 less. - `ditherFrame`: float; range 0 (screen), 1 (layout); step 1 (whole); unit option; default 1. Where the pattern is anchored: 0 screen, 1 layout. - `ditherOriginX`: float; range none: a position, so any finite number is valid, negative included; it comes from the layout; unit framebuffer px; default 0. Pattern origin x in the layout frame. Set it from the layout: the text block's x times the pixel ratio. - `ditherOriginY`: float; range none: a position, so any finite number is valid, negative included; it comes from the layout; unit framebuffer px; default 0. Pattern origin y in the layout frame, from the bottom of the canvas as gl_FragCoord counts: canvas.height minus the text block's y times the pixel ratio. ### halftone A rotated print screen laid on the screen: each cell holds a dot, a line or a cross whose area is the coverage there, in the glyph's own colour. Put it last, after fill and the ink pack. Cells under about 4 px read as noise. - `halftoneShape`: float; range 0 (round), 1 (line), 2 (cross); step 1 (whole); unit option; default 0. The mark in each cell: 0 round, 1 line, 2 cross. - `halftoneSize`: float; range 2 or more; typical 3 to 24; step 0.5; unit framebuffer px; default 8. Width of one screen cell in real screen pixels. - `halftoneAngle`: float; range -90 to 90; typical -90 to 90; step 1 (whole); unit degrees; default 45. Rotation of the screen. - `halftoneSoftness`: float; range 0 to 1; typical 0 to 0.5; step 0.01; unit 0..1; default 0.15. Edge softness of a mark as a share of the cell; never under one pixel. - `halftoneDensity`: float; range 0 to 2; typical 0.5 to 1.5; step 0.01; unit times; default 0.7. Scales the coverage before the screen: above 1 bigger marks, below smaller. - `halftoneFrame`: float; range 0 (screen), 1 (layout); step 1 (whole); unit option; default 1. Where the pattern is anchored: 0 screen, 1 layout. - `halftoneOriginX`: float; range none: a position, so any finite number is valid, negative included; it comes from the layout; unit framebuffer px; default 0. Pattern origin x in the layout frame. Set it from the layout: the text block's x times the pixel ratio. - `halftoneOriginY`: float; range none: a position, so any finite number is valid, negative included; it comes from the layout; unit framebuffer px; default 0. Pattern origin y in the layout frame, from the bottom of the canvas as gl_FragCoord counts: canvas.height minus the text block's y times the pixel ratio. ## Passes A pass is the second kind of effect: not a stage on a wire of the text shader but a sequence of draws over images, with render targets of its own. It is set through its controller (`createSmudge(draw).set({ blur: 60 })`), not `text.set`, so its keys are the plain setting names below; it has no share-link key and `composeEffects` never takes one. The records are the same kind (range, unit, description) and are checked the same way. ### smudge Ink that softens, streaks and ghosts along the stroke where the pointer rubs, keeps a faint rough trace for a few seconds and settles with no hard edges. Two passes over images: the pointer paints a small hidden field (stroke direction, speed and wetness) that fades with time, then a display pass smears the plainly drawn word along the stored direction. Not a wire effect: it is set with `createSmudge(draw).set(...)`, not `text.set`, and it has no share-link key. - Target `field`: half the canvas, two targets swapped each frame (ping-pong), RGBA8. Red and green hold the stroke direction scaled by speed (0.5 is still), blue holds wetness (0 dry, 1 freshly painted). Wetness falls by dt / fade each frame. - Step `field` reads `field`, writes `field`. Reads the previous field, subtracts the wetness owed for the elapsed time, paints a capsule from the pointer's last to its current position and mixes in the stroke's velocity. - Step `display` reads `scene`, `field`, writes `canvas`. Smears the plain word's coverage along the stored direction with 21 gaussian-weighted taps leaning back by trail x wetness, thins and grains it where it is wet, and outputs premultiplied colour over the canvas. - `blur`: float; range 0 to 400; typical 0 to 160; step 1 (whole); unit px; default 2. Length of the longest ghost, at full stroke speed and full wetness, in CSS px. 0 turns the streak off. - `trail`: float; range 0 to 1; step 0.01; unit 0..1; default 0.7. How far the ghost leans back along the stroke. 0 smears both ways evenly, 1 trails fully behind the pointer. - `soften`: float; range 0 to 1; step 0.01; unit 0..1; default 0.1. How much wet areas thin out. 0 keeps the ink as dense as it was drawn. - `grain`: float; range 0 to 1; step 0.01; unit 0..1; default 0.04. Speckle in wet areas. The speckle is fixed to the page, so it does not flicker. - `radius`: float; range 4 to 400; typical 20 to 160; step 1 (whole); unit px; default 70. Radius of the brush the pointer paints into the field, in CSS px, with a soft edge. - `fade`: float; range 0.2 to 30; typical 1 to 10; step 0.1; unit s; default 4.8. Seconds for full wetness to fall to zero. The same at 60 and at 144 Hz. - `fullSpeed`: float; range 200 to 10000; typical 800 to 5000; step 50; unit px/s; default 700. The stroke speed that counts as a full-speed stroke, in CSS px per second. Faster strokes stop adding length. - `shift`: float; range 0 to 60; typical 0 to 12; step 0.5; unit px; default 4. How far wet ink is pushed along the stroke, in CSS px: the strokes themselves move, as well as smear. 0 turns it off. Since 2026-11-09. - `settle`: float; range 0 to 5; typical 0 to 1; step 0.05; unit s; default 0. Seconds the push takes to build after the brush passes, so the ink keeps moving a moment after the pointer stops. 0 pushes at once. Since 2026-11-09. Presets: `default` (blur 2, trail 0.7, soften 0.1, grain 0.04, radius 70, fade 4.8, fullSpeed 700, shift 4, settle 0); `streak` (blur 6, trail 0.7, soften 0.1, grain 0.04, radius 70, fade 4.8, fullSpeed 2400, shift 0, settle 0); `smoky` (blur 70, trail 0.7, soften 0.4, grain 0.12, radius 70, fade 4.8, fullSpeed 2400, shift 0, settle 0). ### screen Ordered dither or a halftone screen over any picture: draw a scene into a render target, then the pass quantises it onto the canvas in one full-rect draw. Dither mode snaps each colour channel to a few tones with a Bayer pattern between them; halftone mode prints the picture's darkness as round, line or cross marks in an ink colour over a paper colour. The pattern stays fixed on the display. The pass form of the dither and halftone text effects, with the same maths. Set with `createScreen(draw).set(...)`; it has no share-link key. - Step `screen` reads `scene`, writes `canvas`. Reads the scene, takes its straight colour, and either quantises each channel with the Bayer threshold (dither) or prints a mark whose area is 1 minus its luminance (halftone), keeping the scene's alpha; outputs premultiplied colour over the canvas. - `mode`: float; range 0 (dither), 1 (halftone); step 1 (whole); unit option; default 0. What the pass does: 0 dither, 1 halftone. - `size`: float; range 1 to 64; typical 1 to 16; step 0.5; unit CSS px; default 2. Edge of one cell: a dither threshold cell or a halftone cell. Multiplied by the pixel ratio. - `matrix`: float; range 2 (2), 4 (4), 8 (8); step 1 (whole); unit cells; default 8. Size of the Bayer matrix in dither mode: 2, 4 or 8 cells a side. - `levels`: float; range 2 to 16; typical 2 to 8; step 1 (whole); unit tones; default 2. Tones per colour channel in dither mode. 2 is pure threshold. - `angle`: float; range -90 to 90; typical -90 to 90; step 1 (whole); unit degrees; default 45. Rotation of the halftone screen. - `shape`: float; range 0 (round), 1 (line), 2 (cross); step 1 (whole); unit option; default 0. The halftone mark: 0 round, 1 line, 2 cross. - `softness`: float; range 0 to 1; typical 0 to 0.5; step 0.01; unit 0..1; default 0.15. Edge softness of a halftone mark as a share of the cell; never under one pixel. - `density`: float; range 0 to 2; typical 0.5 to 1.5; step 0.01; unit times; default 1. Scales the darkness before the halftone screen: above 1 bigger marks, below smaller. - `amount`: float; range 0 to 1; typical 0 to 1; step 0.01; unit 0..1; default 1. How much of the quantised picture to use. 0 shows the picture unchanged. Presets: `default` (mode 0, size 2, matrix 8, levels 2, angle 45, shape 0, softness 0.15, density 1, amount 1); `halftone` (mode 1, size 6, matrix 8, levels 2, angle 45, shape 0, softness 0.15, density 1, amount 1). ### ripple A whole picture moves like water where the pointer stirs it: draw a scene into a render target, and the pass draws it back with each pixel's lookup shifted along the slope of a damped wave the pointer raises. Where the liquid text effect moves glyph edges, this moves everything, background and gaps included. Chain it into the screen pass for a dithered or halftoned ripple. Set with `createRipple(draw).set(...)`; it has no share-link key. - Target `field`: a small grid (128 by 72 cells by default) copied into a canvas each frame it moves, RGBA8. The damped wave the pointer raises: red holds the height as 0.5 plus half of it. Stepped on the CPU (LiquidField), uploaded only while it moves. - Step `ripple` reads `scene`, `field`, writes `canvas`. Reads the wave's height field around each pixel, takes its slope, and samples the scene that far along it (scaled by ripple), optionally lit by the slope; outputs the scene's premultiplied colour over the canvas or into a target. - `ripple`: float; range 0 to 400; typical 0 to 160; step 1 (whole); unit CSS px; default 60. How far the picture shifts per unit of wave slope. 0 turns the ripple off; past about 120 the picture folds over itself on a hard stroke. - `radius`: float; range 0.005 to 0.2; typical 0.015 to 0.08; step 0.005; unit field widths; default 0.035. Radius of the bump each pointer poke raises, as a share of the field's width. - `speed`: float; range 0 to 0.5; typical 0.2 to 0.45; step 0.01; unit cells per step; default 0.35. How fast waves travel. 0.5 is the stable limit of the grid. - `damping`: float; range 0.9 to 0.999; typical 0.95 to 0.99; step 0.001; unit 0..1; default 0.975. Share of the wave kept each 1/60 s step. Lower settles sooner. - `light`: float; range 0 to 1; typical 0 to 0.4; step 0.01; unit 0..1; default 0. Brightens slopes facing the top left and darkens the others. 0 leaves the colours alone. Presets: `default` (ripple 60, radius 0.035, speed 0.35, damping 0.975, light 0); `glass` (ripple 30, radius 0.035, speed 0.35, damping 0.975, light 0.25). ## Presets and share links A preset is a complete named value set for one effect: a value for every tunable uniform, keyed by the uniform's name, so it applies to an effect under any name: `text.set(presetSetters("glow", preset))` with a preset from `EFFECT_PRESETS.glow`. Applying one replaces every tunable value. Presets leave out the uniforms set from the layout or the font: `wipeCount`, `wipeLeft`, `wipeWidth`, `wipeLines`, `scramblePool`, `scrambleCount`, `wakePoints`, `wakeBorn`, `liquidField`, `liquidRect`, `ditherOriginX`, `ditherOriginY`, `halftoneOriginX`, `halftoneOriginY`. Only one preset ships per effect, `default`, equal to the defaults above; which looks to add is the maintainer's call. Composition presets (effects in order, each with a value set) have a type and no entries yet. - fill: `default` (color [1, 1, 1, 1], opacity 1) - wipe: `default` (in 1, out 0, band 0.2) - outline: `default` (width 1, color [0, 0, 0, 1]) - glow: `default` (spread 4, strength 1, color [1, 1, 1, 1]) - scramble: `default` (drive 0, rate 12, match 0.15, seed 0) - grain: `default` (amount 0.2, size 2, speed 0, seed 0, mode 0) - wake: `default` (strength 0.05, radius 90, fade 0.9, stretch 2, drift 0.15) - rough: `default` (amount 0.6, scale 6, detail 0.5, seed 0) - dotgain: `default` (spread 0.5, bleed 0.5) - inkpool: `default` (width 3, strength 0.6, color [0, 0, 0, 1], starve 0) - press: `default` (unevenness 0.2, scale 28, voids 0, seed 0) - liquid: `default` (amount 2) - dither: `default` (mode 0, size 2, matrix 8, levels 4, amount 1, bias 0, frame 1) - halftone: `default` (shape 0, size 8, angle 45, softness 0.15, density 0.7, frame 1) The playground's `link` button puts a figure's settings in the address: `#
=`, such as `#compose=1~_,f:lora~g,sp:5.5~f,o:.73`. The state holds which effects are on and their order (compose), every tunable uniform that differs from its default and the figure's own settings (font, text, size, the wipe's coordinate). Opening a link checks every value against these records; values it cannot use keep their defaults and are listed on the page. A link is at most 2,000 characters; long text is cut to fit. ## Code shape ```ts const fx = createTextShader(draw, composeEffects(fill(), outline({ width: 2 }), wipe())); fx.set({ wipeCount: info.count }); // each frame fx.set({ wipeIn: progress }).setTime(seconds); draw.withShader(fx.shader, () => draw.sprites(frames, buf, n, spriteOpts, SPRITE_EACH)); ```