Skip to content
kerfjsGitHub

Art

Twenty-eight pieces drawn with Canvas and DitherField, in veneer's own inks. They follow your theme and the dial, three at most move at a time, and the rest wait as stills.

Each piece changes with the dial:

Dither fields

Tones and a few inks. The picture is whatever the threshold leaves behind.

Veneer Beam

Tense threads run down the loom and catch the light where they cross a slow beam. Drag through them to pluck.

Code
tsx
import { DitherField } from '@kerf-js/veneer'import { noise, type ArtProps, type Meta } from './shared'import code from './veneer-beam.tsx?raw'export const meta: Meta = {  name: 'Veneer Beam',  description:    'Tense threads run down the loom and catch the light where they cross a slow beam. Drag through them to pluck.',  kind: 'field',  code,  wide: true,}const { sin, exp, PI, floor } = Mathconst SIZE = 4096,  WAVE = Float32Array.from({ length: SIZE }, (_, i) => sin((i / SIZE) * PI * 2)),  wave = (v: number) => WAVE[(v * (SIZE / (PI * 2)) + SIZE * 64) & (SIZE - 1)]!const K = 66,  TENSION = Array.from({ length: 512 }, (_, i) => 0.4 + 0.6 * noise(i * 0.61, 2.1) ** 1.2)// Each row shares its beam, so that is worked out once a row, not once a cell.let row = -1,  stamp = -1,  beam = 0,  width = 0function threads(x: number, y: number, t: number) {  if (y !== row || t !== stamp) {    row = y    stamp = t    beam = 1.3 + 0.7 * sin(t * 0.13) + 0.3 * sin(y * 1.6 + t * 0.21)    width = 0.9 + 0.35 * sin(y * 2.4 - t * 0.17)  }  const phase = x * K + 2.4 * wave(2 * y + x * 1.3 + t * 0.5) + 0.8 * wave(5 * y - x * 2 - t * 0.3),    s = wave(phase),    s2 = s * s  const d = x - beam,    light = exp(-d * d * width)  return s2 * s2 * TENSION[floor(phase / PI) & 511]! * (0.42 + 0.68 * light) + 0.34 * light}export default function VeneerBeam({ live = true }: ArtProps) {  return (    <DitherField      fill      kind={threads}      ramp="neutral"      spark="accent"      pointer="pull"      cell={3}      gain={1.3}      seed={3}      speed={live ? 1 : 0}      label="Vertical threads lit by a slow beam of light"    />  )}

Tide Table

Five horizons, each a little closer and heavier than the last. Good for the seam between two sections.

Code
tsx
import { DitherField } from '@kerf-js/veneer'import { smooth, type ArtProps, type Meta } from './shared'import code from './tide-table.tsx?raw'export const meta: Meta = {  name: 'Tide Table',  description:    'Five horizons, each a little closer and heavier than the last. Good for the seam between two sections.',  kind: 'field',  code,  wide: true,}const { sin } = Math,  BANDS = 6,  SLOTS = 320,  SPAN = 4const crests = new Float32Array(BANDS * SLOTS)let stamp = NaN// Every cell in a column shares its crests, so they go in a table once per frame and the cells only compare against it.function tabulate(t: number) {  stamp = t  for (let i = 0; i < BANDS; i++)    for (let n = 0; n < SLOTS; n++) {      const x = (n / SLOTS) * SPAN      crests[i * SLOTS + n] =        0.14 +        i * 0.145 +        sin(x * (1.1 + i * 0.55) + t * (0.18 + i * 0.05) + i * 2.1) * (0.016 + i * 0.008) +        sin(x * (3.1 + i * 0.9) - t * 0.11 + i) * 0.008    }}function horizons(x: number, y: number, t: number) {  if (t !== stamp) tabulate(t)  const n = ((x / SPAN) * SLOTS) | 0  let tone = 0  for (let i = 0; i < BANDS; i++) {    const under = y - crests[i * SLOTS + n]!    if (under > 0)      tone =        (0.66 + i * 0.07) * (0.62 + 0.38 * smooth(0.12, 0, under)) + 0.16 * smooth(0.012, 0, under)  }  return tone}export default function TideTable({ live = true }: ArtProps) {  return (    <DitherField      fill      kind={horizons}      ramp={[        'none',        '--veneer-color-accent-soft',        '--veneer-color-accent-border',        '--veneer-color-accent-text',      ]}      pattern="blue"      fade="top"      cell={3}      gain={1.25}      seed={9}      speed={live ? 1 : 0}      label="Five stacked horizons, thinning toward the top"    />  )}

Moiré Loom

Two gratings, a few degrees apart, and everything else is the eye. Click to drop a ring through them.

Code
tsx
import { DitherField } from '@kerf-js/veneer'import { type ArtProps, type Meta } from './shared'import code from './moire-loom.tsx?raw'export const meta: Meta = {  name: 'Moiré Loom',  description:    'Two gratings, a few degrees apart, and everything else is the eye. Click to drop a ring through them.',  kind: 'field',  code,}const { sin, cos } = Mathfunction beats(x: number, y: number, t: number) {  const a = 0.06 + sin(t * 0.07) * 0.03,    c = cos(a),    s = sin(a),    k = 118  const lift = sin(x * 1.1 + t * 0.1) * 0.5 + sin(y * 1.7 - t * 0.08) * 0.5  const one = 0.5 + 0.5 * sin(y * k + lift),    two = 0.5 + 0.5 * sin((x * s + y * c) * k * 1.04 - lift + t * 0.12)  return Math.sqrt(one * two) * 0.96}export default function MoireLoom({ live = true }: ArtProps) {  return (    <DitherField      fill      kind={beats}      spark="accent"      ramp="neutral"      spread={0.5}      pointer="shockwave"      cell={2}      gain={1.2}      seed={21}      speed={live ? 1 : 0}      label="Two line gratings beating against each other"    />  )}

Turing Cloth

Two chemicals feed and kill each other, and a labyrinth grows out of it, the way a leopard gets its spots. Click to seed.

Code
tsx
import { DitherField } from '@kerf-js/veneer'import { rng, clamp, type ArtProps, type Meta } from './shared'import code from './turing-cloth.tsx?raw'export const meta: Meta = {  name: 'Turing Cloth',  description:    'Two chemicals feed and kill each other, and a labyrinth grows out of it, the way a leopard gets its spots. Click to seed.',  kind: 'field',  code,}const W = 150,  H = 94,  FEED = 0.037,  KILL = 0.06function sampleCloth(b: Float32Array, at: { x: number; y: number }, aspect: number) {  const { x, y } = at  const fx = (x / aspect) * (W - 1),    fy = y * (H - 1),    ix = fx | 0,    iy = fy | 0,    kx = fx - ix,    ky = fy - iy  const i = iy * W + ix,    j = i + 1,    m = iy < H - 1 ? i + W : i,    n = m + 1  return clamp(    ((b[i]! * (1 - kx) + b[j]! * kx) * (1 - ky) + (b[m]! * (1 - kx) + b[n]! * kx) * ky) * 2.9,  )}type Chemicals = {  a: Float32Array  b: Float32Array  a2: Float32Array  b2: Float32Array}function step(grid: Chemicals, n: number) {  let { a, b, a2, b2 } = grid  for (let s = 0; s < n; s++) {    for (let y = 0, i = 0; y < H; y++) {      const up = (y === 0 ? H - 1 : y - 1) * W,        down = (y === H - 1 ? 0 : y + 1) * W,        here = y * W      for (let x = 0; x < W; x++, i++) {        const l = x === 0 ? W - 1 : x - 1,          r = x === W - 1 ? 0 : x + 1        const A = a[i]!,          B = b[i]!,          abb = A * B * B        a2[i] =          A +          (a[here + l]! + a[here + r]! + a[up + x]! + a[down + x]! - 4 * A) * 0.2 -          abb +          FEED * (1 - A)        b2[i] =          B +          (b[here + l]! + b[here + r]! + b[up + x]! + b[down + x]! - 4 * B) * 0.1 +          abb -          (KILL + FEED) * B      }    }    ;[a, a2] = [a2, a]    ;[b, b2] = [b2, b]  }  grid.a = a  grid.b = b  grid.a2 = a2  grid.b2 = b2}export default function TuringCloth({ live = true }: ArtProps) {  const grid: Chemicals = {    a: new Float32Array(W * H).fill(1),    b: new Float32Array(W * H),    a2: new Float32Array(W * H),    b2: new Float32Array(W * H),  }  let warm = false,    stamp = -1  const drop = (cx: number, cy: number, r: number) => {    for (let y = -r; y <= r; y++)      for (let x = -r; x <= r; x++)        if (x * x + y * y <= r * r) {          const i = ((cy + y + H) % H) * W + ((cx + x + W) % W)          grid.a[i] = 0.5          grid.b[i] = 0.5        }  }  const seed = () => {    const r = rng(11)    for (let i = 0; i < 46; i++) drop((r() * W) | 0, (r() * H) | 0, 2 + ((r() * 3) | 0))    step(grid, 1000)    warm = true  }  // A field is a pure function of place and time, so a new `t` is what advances the reaction.  let aspect = 1.6  const cloth = (x: number, y: number, t: number) => {    if (!warm) seed()    if (t !== stamp) {      stamp = t      step(grid, 2)    }    if (x > aspect) aspect = x    return sampleCloth(grid.b, { x, y }, aspect)  }  const press = (e: PointerEvent) => {    const r = (e.currentTarget as HTMLElement).getBoundingClientRect()    drop((((e.clientX - r.left) / r.width) * W) | 0, (((e.clientY - r.top) / r.height) * H) | 0, 3)  }  return (    <DitherField      fill      kind={cloth}      cell={3}      gain={1.45}      fps={30}      seed={1}      speed={live ? 1 : 0}      onPointerDown={press}      label="A labyrinth grown by a reaction and diffusion"    />  )}

Survey

A contour map of country that doesn't exist. Bring the pointer close and a new hill comes up under it.

Code
tsx
import { DitherField } from '@kerf-js/veneer'import { clamp, fbm, smooth, type ArtProps, type Meta } from './shared'import code from './survey.tsx?raw'export const meta: Meta = {  name: 'Survey',  description:    "A contour map of country that doesn't exist. Bring the pointer close and a new hill comes up under it.",  kind: 'field',  code,}const W = 96,  H = 60,  RINGS = 15export default function Survey({ live = true }: ArtProps) {  const hills = new Float32Array(W * H)  let stamp = NaN,    px = -1,    py = -1,    lift = 0,    aspect = 1.6  function survey(t: number) {    stamp = t    lift = px < 0 ? Math.max(0, lift - 0.015) : Math.min(1, lift + 0.03)    for (let y = 0, i = 0; y < H; y++)      for (let x = 0; x < W; x++, i++) {        const u = (x / (W - 1)) * aspect * 1.5,          v = (y / (H - 1)) * 1.5        let h = fbm(u + t * 0.008, v - t * 0.005, 4)        h = (h - 0.28) * 1.75        if (lift) {          const dx = x / (W - 1) - px,            dy = y / (H - 1) - py          h += 0.32 * lift * Math.exp(-(dx * dx * 2.5 + dy * dy) * 18)        }        hills[i] = h      }  }  const map = (x: number, y: number, t: number) => {    if (x > aspect) aspect = x    if (t !== stamp) survey(t)    const fx = clamp(x / aspect) * (W - 1.001),      fy = y * (H - 1.001),      ix = fx | 0,      iy = fy | 0,      kx = fx - ix,      ky = fy - iy,      i = iy * W + ix    const h =      (hills[i]! * (1 - kx) + hills[i + 1]! * kx) * (1 - ky) +      (hills[i + W]! * (1 - kx) + hills[i + W + 1]! * kx) * ky    // distance to the nearest contour, divided by the slope, so a line keeps its weight on steep ground and on flat    const sx = Math.abs(hills[i + 1]! - hills[i]!) * (W - 1),      sy = Math.abs(hills[i + W]! - hills[i]!) * (H - 1),      slope = Math.hypot(sx, sy) * RINGS + 0.4    const band = h * RINGS,      f = band - Math.floor(band),      line = smooth(1.5, 0.7, (Math.min(f, 1 - f) / slope) * 300)    const index = (Math.floor(band + 0.5) + 400) % 5 === 0 ? 1 : 0.62    return Math.max(line * index, 0.24 * smooth(0.5, 1, h))  }  const place = (e: PointerEvent) => {    const r = (e.currentTarget as HTMLElement).getBoundingClientRect()    px = (e.clientX - r.left) / r.width    py = (e.clientY - r.top) / r.height  }  return (    <DitherField      fill      kind={map}      ramp={[        'none',        '--veneer-color-border-strong',        '--veneer-color-text-subtle',        '--veneer-color-accent-text',      ]}      cell={2}      spread={0.4}      gain={1.6}      seed={40}      speed={live ? 1 : 0}      onPointerMove={place}      onPointerLeave={() => {        px = -1      }}      label="A contour map of invented hills"    />  )}

Woven Word

A word pulled out of the cloth from left to right, with a flash where each thread lands. It stays real text for readers and search.

Code
tsx
import { DitherGlyph } from '@kerf-js/veneer'import { type ArtProps, type Meta } from './shared'import code from './woven-word.tsx?raw'export const meta: Meta = {  name: 'Woven Word',  description:    'A word pulled out of the cloth from left to right, with a flash where each thread lands. It stays real text for readers and search.',  kind: 'field',  code,  wide: true,}export default function WovenWord({ live = true }: ArtProps) {  return (    <DitherGlyph      text="kerf"      reveal={1.4}      pointer="ripple"      spark="neutral"      cell={3}      fit={0.8}      weight={500}      speed={live ? 1 : 0}      ratio="auto"      style="position:absolute;inset:0;block-size:100%"    />  )}

Darkroom

One picture through every algorithm veneer has. Drag the divider to see what the dither kept.

Code
tsx
import { Dither, Segmented, type Algorithm } from '@kerf-js/veneer'import { state } from '@kerf-js/core'import { fbm, clamp, mix, smooth, type ArtProps, type Meta } from './shared'import code from './darkroom.tsx?raw'export const meta: Meta = {  name: 'Darkroom',  description:    'One picture through every algorithm veneer has. Drag the divider to see what the dither kept.',  kind: 'field',  code,  wide: true,  rest: true,}const ALGORITHMS = [  { value: 'atkinson', label: 'Atkinson' },  { value: 'floyd', label: 'Floyd' },  { value: 'bayer8', label: 'Bayer' },  { value: 'blue', label: 'Blue' },  { value: 'halftone', label: 'Halftone' },]const CELLS = [  { value: '2', label: '2' },  { value: '3', label: '3' },  { value: '5', label: '5' },]// There is no photograph in the repo, so the print is a dusk painted once here: sky, sun, three ridges, mist.function dusk() {  const W = 640,    H = 300,    c = document.createElement('canvas')  c.width = W  c.height = H  const g = c.getContext('2d')!,    px = g.createImageData(W, H),    sky = [      [0.98, 0.72, 0.5],      [0.72, 0.42, 0.52],      [0.16, 0.16, 0.34],    ]  for (let y = 0, i = 0; y < H; y++)    for (let x = 0; x < W; x++, i += 4) {      const v = y / H,        u = x / W,        up = clamp((0.72 - v) / 0.72),        k = up * 2,        a = sky[k < 1 ? 0 : 1]!,        b = sky[k < 1 ? 1 : 2]!,        m = k < 1 ? k : k - 1      const cloud = smooth(0.52, 0.8, fbm(u * 5 + 3, v * 16, 5)) * 0.5      let r = mix(a[0]!, b[0]!, m) + cloud * 0.25,        gr = mix(a[1]!, b[1]!, m) + cloud * 0.16,        bl = mix(a[2]!, b[2]!, m) + cloud * 0.12      const d = Math.hypot((u - 0.66) * 2.13, v - 0.52),        sun = smooth(0.1, 0.092, d),        glow = Math.exp(-d * d * 7) * 0.6      r += glow + sun      gr += glow * 0.7 + sun * 0.9      bl += glow * 0.35 + sun * 0.7      for (let n = 0; n < 3; n++) {        const top =          0.58 + n * 0.12 + (fbm(u * (3 + n * 1.5) + n * 7, n * 3.3, 5) - 0.5) * (0.42 - n * 0.06)        if (v > top) {          const haze = 0.42 - n * 0.2 + (v - top) * -0.1,            s = 0.1 - n * 0.025          r = mix(s, 0.86, haze)          gr = mix(s, 0.56, haze)          bl = mix(s + 0.04, 0.55, haze)        }      }      px.data[i] = r * 255      px.data[i + 1] = gr * 255      px.data[i + 2] = bl * 255      px.data[i + 3] = 255    }  g.putImageData(px, 0, 0)  return c.toDataURL('image/png')}export default function Darkroom(_: ArtProps) {  const split = state(52),    algorithm = state('atkinson'),    cell = state('3'),    print = state<string | undefined>(typeof document === 'undefined' ? undefined : dusk())  // oxlint-disable-next-line no-unassigned-vars -- the compiler assigns it through ref  let host!: HTMLDivElement  const slide = (e: PointerEvent) => {    if (!e.buttons && e.type !== 'pointerdown') return    if ((e.target as Element).closest('[data-art="controls"]')) return    const r = host.getBoundingClientRect()    split.value = clamp((e.clientX - r.left) / r.width, 0.02, 0.98) * 100  }  return (    <div      data-art="pierce"      data-art-darkroom=""      ref={host}      style={`--split:${split.value}%`}      onPointerDown={slide}      onPointerMove={slide}    >      <img src={print.value} alt="" />      <div data-part="print">        {print.value && (          <Dither            src={print.value}            alt="A dusk over three ridges"            algorithm={algorithm.value as Algorithm}            cell={+cell.value}            ramp="neutral"            inks={4}            fill          />        )}      </div>      <i data-part="divider"></i>      <div data-art="controls">        <Segmented          label="Algorithm"          value={algorithm.value}          onChange={(v: string) => {            algorithm.value = v          }}          options={ALGORITHMS}        />        <Segmented          label="Cell size"          value={cell.value}          onChange={(v: string) => {            cell.value = v          }}          options={CELLS}        />      </div>    </div>  )}

Ramp Wall

Every ramp through every pattern, as plates. Click one and its props are on your clipboard.

Code
tsx
import { Dither, type Algorithm, type Ramp } from '@kerf-js/veneer'import { state } from '@kerf-js/core'import { type ArtProps, type Meta } from './shared'import code from './ramp-wall.tsx?raw'export const meta: Meta = {  name: 'Ramp Wall',  description:    'Every ramp through every pattern, as plates. Click one and its props are on your clipboard.',  kind: 'field',  code,  wide: true,  rest: true,}const RAMPS = [  'accent',  'neutral',  'positive',  'caution',  'critical',  'info',  'highlight',] as const satisfies readonly Ramp[]const PATTERNS: Algorithm[] = ['bayer2', 'bayer4', 'bayer8', 'blue', 'halftone']export default function RampWall(_: ArtProps) {  const copied = state('')  const copy = (prop: string) => {    void navigator.clipboard?.writeText(prop)    copied.value = `${prop} copied`  }  return (    <div data-art="pierce" data-art-wall="">      {RAMPS.map((ramp) =>        PATTERNS.map((algorithm) => {          const prop = `<Dither ramp="${ramp}" algorithm="${algorithm}" />`          return (            <button              key={`${ramp}-${algorithm}`}              type="button"              aria-label={`${ramp}, ${algorithm}`}              onClick={() => copy(prop)}            >              <Dither                ramp={ramp}                algorithm={algorithm}                cell={3}                inks={4}                gradient={{ angle: 100 }}                fill              />            </button>          )        }),      )}      <p data-art="readout" role="status">        {copied.value}      </p>    </div>  )}

Generative

Rules, a seed and hairlines. Each of these could be printed and hung.

Shuttle

One stroke crosses the veneers and leaves one woven row behind it. It never rewinds. Hover to hurry it along.

Code
tsx
import { Canvas, type CanvasFrame as Frame } from '@kerf-js/veneer'import { state } from '@kerf-js/core'import { inks, type ArtProps, type Meta } from './shared'import code from './shuttle.tsx?raw'export const meta: Meta = {  name: 'Shuttle',  description:    'One stroke crosses the veneers and leaves one woven row behind it. It never rewinds. Hover to hurry it along.',  kind: 'generative',  code,  wide: true,}const PITCH = 5,  ROW = 4const colours = inks({  veneer: '--veneer-color-border-strong',  soft: '--veneer-color-accent-border',  ink: '--veneer-color-accent-text',  deep: '--veneer-color-text',  sweep: '--veneer-color-sweep',  ground: '--veneer-color-page',})/** Which of the kerf's colours row `r` takes: broad bands, like a blanket's stripes. */const band = (r: number) => {  const k = (r % 64) / 64  return k < 0.44 ? 0 : k < 0.5 ? 2 : k < 0.72 ? 1 : k < 0.75 ? 2 : k < 0.94 ? 0 : 1}/** One finished row: a 2/2 twill, so the kerf lies over two veneers and under two, stepping one along each row. */function weave(  ctx: CanvasRenderingContext2D,  c: ReturnType<typeof colours>,  { w, r, y }: { w: number; r: number; y: number },) {  const count = Math.ceil(w / PITCH)  ctx.fillStyle = c.veneer  ctx.fillRect(0, y, w, ROW)  ctx.fillStyle = [c.soft, c.ink, c.deep][band(r)]!  for (let i = 0; i < count; i++) if ((i + r) & 2) ctx.fillRect(i * PITCH, y, PITCH - 0.5, ROW)}function veneers(  ctx: CanvasRenderingContext2D,  c: ReturnType<typeof colours>,  { w, h }: { w: number; h: number },) {  ctx.fillStyle = c.veneer  for (let x = PITCH / 2; x < w; x += PITCH) ctx.fillRect(x - 0.5, 0, 1, h)}function shuttleTail(  ctx: CanvasRenderingContext2D,  c: ReturnType<typeof colours>,  { row, across, w, n }: { row: number; across: number; w: number; n: number },) {  const y = Math.min(row, n - 1) * ROW + 4,    right = row & 1,    x = (right ? 1 - across : across) * w,    from = right ? x : 0,    to = right ? w : x  // the row in progress, laid under the shuttle's tail  ctx.fillStyle = [c.soft, c.ink, c.deep][band(row)]!  for (let i = Math.floor(from / PITCH); i * PITCH < to; i++)    if ((i + row) & 2) ctx.fillRect(i * PITCH, y, PITCH - 0.5, ROW)  const tail = right ? x + 90 : x - 90,    wash = ctx.createLinearGradient(tail, 0, x, 0)  wash.addColorStop(0, 'transparent')  wash.addColorStop(1, c.sweep)  ctx.fillStyle = wash  ctx.fillRect(Math.min(tail, x), y + 1, 90, 2)  ctx.fillStyle = c.sweep  ctx.fillRect(x - 11, y - 1, 22, 6)}export default function Shuttle({ live = true }: ArtProps) {  let cloth: HTMLCanvasElement | undefined,    g: CanvasRenderingContext2D,    row = 0,    across = 0,    w = 0,    h = 0,    dpr = 1  const hurry = state(false)  const rows = () => Math.floor((h - 8) / ROW)  function rebuild(c: ReturnType<typeof colours>) {    cloth ??= document.createElement('canvas')    cloth.width = w * dpr    cloth.height = h * dpr    g = cloth.getContext('2d')!    g.setTransform(dpr, 0, 0, dpr, 0, 0)    const n = rows()    for (let i = 0; i < Math.min(row, n); i++)      weave(g, c, { w, r: row - 1 - i, y: (Math.min(row, n) - 1 - i) * ROW + 4 })  }  const draw = ({ ctx, width, height, dpr: scale, dt, tier, still }: Frame) => {    const c = colours(ctx.canvas)    if (width !== w || height !== h || !cloth || scale !== dpr) {      w = width      h = height      dpr = scale      if (!row) {        row = Math.floor(rows() * 0.8)        across = 0.58      }      rebuild(c)    }    if (!still) across += dt * (hurry.value ? 2.4 : 0.34 + tier.speed * 0.4)    while (across >= 1) {      across -= 1      const n = rows()      if (row >= n) {        g.globalCompositeOperation = 'copy'        g.setTransform(1, 0, 0, 1, 0, -ROW * dpr)        g.drawImage(cloth!, 0, 0)        g.setTransform(dpr, 0, 0, dpr, 0, 0)        g.globalCompositeOperation = 'source-over'      }      weave(g, c, { w, r: row, y: Math.min(row, n - 1) * ROW + 4 })      row++    }    ctx.clearRect(0, 0, w, h)    veneers(ctx, c, { w, h })    ctx.drawImage(cloth!, 0, 0, w, h)    const n = rows()    shuttleTail(ctx, c, { row, across, w, n })  }  return (    <Canvas      draw={draw}      animate={live}      onTheme={() => {        colours.reset()        cloth = undefined      }}      onPointerEnter={() => {        hurry.value = true      }}      onPointerLeave={() => {        hurry.value = false      }}      label="A shuttle weaving a striped twill, one row at a time"    />  )}

Curl Threads

Six hundred strands combed around eddies you can't see. Move the pointer in and it becomes one more.

Code
tsx
import { Canvas, type CanvasFrame as Frame } from '@kerf-js/veneer'import { fbm, inks, rng, type ArtProps, type Meta } from './shared'import code from './curl-threads.tsx?raw'export const meta: Meta = {  name: 'Curl Threads',  description:    "Six hundred strands combed around eddies you can't see. Move the pointer in and it becomes one more.",  kind: 'generative',  code,}const COUNT = 600,  STEP = 1.1const colours = inks({ line: '--veneer-color-text-subtle', accent: '--veneer-color-accent-text' })type ThreadFlow = {  x: Float32Array  y: Float32Array  age: Float32Array  potential: (x: number, y: number) => number  respawn: (i: number) => void  width: () => number  height: () => number}type Walk = { from: number; to: number; px: number; py: number; over: boolean }function walk(ctx: CanvasRenderingContext2D, o: Walk, flow: ThreadFlow) {  const { from, to, px, py, over } = o  const { x, y, age, potential, respawn } = flow  ctx.beginPath()  for (let i = from; i < to; i++) {    const e = 2.5,      n1 = potential(x[i]!, y[i]! + e),      n2 = potential(x[i]!, y[i]! - e),      n3 = potential(x[i]! + e, y[i]!),      n4 = potential(x[i]! - e, y[i]!)    let vx = (n1 - n2) * 130,      vy = (n4 - n3) * 130    if (over) {      const dx = x[i]! - px,        dy = y[i]! - py,        d = dx * dx + dy * dy + 900,        k = 2400 / d      vx += -dy * k * 0.05      vy += dx * k * 0.05    }    const nx = x[i]! + vx * STEP,      ny = y[i]! + vy * STEP    if (--age[i]! < 0 || nx < -8 || ny < -8 || nx > flow.width() + 8 || ny > flow.height() + 8) {      respawn(i)      continue    }    ctx.moveTo(x[i]!, y[i]!)    ctx.lineTo(nx, ny)    x[i] = nx    y[i] = ny  }  ctx.stroke()}type Pull = { px: number; py: number; over: boolean; flow: ThreadFlow }function strands(ctx: CanvasRenderingContext2D, c: ReturnType<typeof colours>, pull: Pull) {  const { px, py, over, flow } = pull  ctx.lineWidth = 0.7  ctx.strokeStyle = c.line  walk(ctx, { from: 0, to: COUNT - 60, px, py, over }, flow)  ctx.lineWidth = 1  ctx.strokeStyle = c.accent  walk(ctx, { from: COUNT - 60, to: COUNT, px, py, over }, flow)}function particles() {  return {    x: new Float32Array(COUNT),    y: new Float32Array(COUNT),    age: new Float32Array(COUNT),    random: rng(4),  }}export default function CurlThreads({ live = true }: ArtProps) {  const { x, y, age, random } = particles()  let w = 0,    h = 0,    drift = 0,    prepared = false  const respawn = (i: number) => {    x[i] = random() * w    y[i] = random() * h    age[i] = 90 + random() * 200  }  const potential = (px: number, py: number) =>    fbm(px * 0.0052 + drift, py * 0.0052 - drift * 0.5, 2)  const flow = { x, y, age, potential, respawn, width: () => w, height: () => h }  const draw = ({ ctx, width, height, still, tier, pointer, dt }: Frame) => {    const c = colours(ctx.canvas)    if (width !== w || height !== h || !prepared) {      w = width      h = height      for (let i = 0; i < COUNT; i++) respawn(i)      ctx.clearRect(0, 0, w, h)      ctx.globalAlpha = 0.85      for (let n = 0; n < 46; n++) strands(ctx, c, { px: 0, py: 0, over: false, flow })      ctx.globalAlpha = 1      prepared = true    }    if (still) return    drift += dt * 0.012 * tier.speed    ctx.globalCompositeOperation = 'destination-out'    ctx.fillStyle = 'rgb(0 0 0 / 0.022)'    ctx.fillRect(0, 0, w, h)    ctx.globalCompositeOperation = 'source-over'    ctx.globalAlpha = 0.85    strands(ctx, c, { px: pointer.x, py: pointer.y, over: pointer.in, flow })    ctx.globalAlpha = 1  }  return (    <Canvas      draw={draw}      animate={live}      fps={30}      pointer      onTheme={() => {        colours.reset()        prepared = false      }}      label="Long strands flowing around invisible eddies"    />  )}

Truchet Cloth

A single square, two arcs, turned or not. One tile at a time the knot rewrites itself. Hover to turn the nearest.

Code
tsx
import { Canvas, type CanvasFrame as Frame } from '@kerf-js/veneer'import { inks, rng, smooth, type ArtProps, type Meta } from './shared'import code from './truchet-cloth.tsx?raw'export const meta: Meta = {  name: 'Truchet Cloth',  description:    'A single square, two arcs, turned or not. One tile at a time the knot rewrites itself. Hover to turn the nearest.',  kind: 'generative',  code,}const SIZE = 30,  TWEEN = 0.5,  { PI } = Mathconst colours = inks({  line: '--veneer-color-text-subtle',  ground: '--veneer-color-accent-soft',  accent: '--veneer-color-sweep',  soft: '--veneer-color-page',})function tile(ctx: CanvasRenderingContext2D, i: number, { r, cols }: { r: number; cols: number }) {  const x = (i % cols) * SIZE + SIZE / 2,    y = Math.floor(i / cols) * SIZE + SIZE / 2,    c = Math.cos(r),    s = Math.sin(r),    h = SIZE / 2  // the arcs' centres, turned by hand: a path is cheaper than a transform per tile  const ax = x - h * c + h * s,    ay = y - h * s - h * c,    bx = x + h * c - h * s,    by = y + h * s + h * c  ctx.moveTo(ax + h * c, ay + h * s)  ctx.arc(ax, ay, h, r, r + PI / 2)  ctx.moveTo(bx - h * c, by - h * s)  ctx.arc(bx, by, h, r + PI, r + PI * 1.5)}type Ribbon = { width: number; ink: string; ground: string; turns: Float32Array; cols: number }function ribbon(ctx: CanvasRenderingContext2D, list: number[], o: Ribbon) {  const { width, ink, ground, turns, cols } = o  ctx.lineCap = 'butt'  for (const [wide, colour] of [    [width, ink],    [width - 2, ground],  ] as const) {    ctx.beginPath()    for (const i of list) tile(ctx, i, { r: turns[i]!, cols })    ctx.lineWidth = wide    ctx.strokeStyle = colour    ctx.stroke()  }}export default function TruchetCloth({ live = true }: ArtProps) {  let cols = 0,    rows = 0,    turns = new Float32Array(0),    targets = new Uint8Array(0),    active = -1,    phase = 0,    wait = 0,    last = -1  const random = rng(3)  const all: number[] = []  const draw = ({ ctx, width, height, dt, still, pointer, tier }: Frame) => {    const c = colours(ctx.canvas),      nc = Math.ceil(width / SIZE),      nr = Math.ceil(height / SIZE)    if (nc !== cols || nr !== rows) {      cols = nc      rows = nr      turns = new Float32Array(cols * rows)      targets = new Uint8Array(cols * rows)      all.length = 0      for (let i = 0; i < turns.length; i++) {        targets[i] = random() < 0.5 ? 0 : 1        turns[i] = targets[i]! * (PI / 2)        all.push(i)      }    }    const over = pointer.in      ? Math.floor(pointer.y / SIZE) * cols + Math.floor(pointer.x / SIZE)      : -1    if (active < 0 && over >= 0 && over !== last && over < turns.length) {      active = last = over      phase = 0    }    if (!still && active < 0 && (wait -= dt * tier.speed) < 0) {      active = (random() * turns.length) | 0      phase = 0      wait = 0.14    }    if (active >= 0) {      phase += dt / TWEEN      if (phase >= 1) {        targets[active]! ^= 1        turns[active] = targets[active]! * (PI / 2)        active = -1      } else turns[active] = targets[active]! * (PI / 2) + smooth(0, 1, phase) * (PI / 2)    }    ctx.clearRect(0, 0, width, height)    ribbon(ctx, all, { width: 12, ink: c.line, ground: c.ground, turns, cols })    if (active >= 0) {      ctx.fillStyle = c.soft      ctx.fillRect((active % cols) * SIZE, Math.floor(active / cols) * SIZE, SIZE, SIZE)      ribbon(ctx, [active], { width: 12, ink: c.accent, ground: c.ground, turns, cols })    }    return active >= 0  }  return (    <Canvas      draw={draw}      animate={live}      fps={30}      pointer      onTheme={colours.reset}      label="A knot of ribbons that rewrites itself tile by tile"    />  )}

Bobbin

Every dot sits 137.5° past the last, the angle sunflowers use, and the disc breathes. Scroll and it turns with you.

Code
tsx
import { Canvas, type CanvasFrame as Frame } from '@kerf-js/veneer'import { onCleanup, onMount } from '@kerf-js/core'import { inks, smooth, type ArtProps, type Meta } from './shared'import code from './bobbin.tsx?raw'export const meta: Meta = {  name: 'Bobbin',  description:    'Every dot sits 137.5° past the last, the angle sunflowers use, and the disc breathes. Scroll and it turns with you.',  kind: 'generative',  code,}const COUNT = 1700,  GOLDEN = (137.508 * Math.PI) / 180const colours = inks({  near: '--veneer-color-text',  mid: '--veneer-color-accent-text',  far: '--veneer-color-accent-border',})export default function Bobbin({ live = true }: ArtProps) {  let scroll = 0  onMount(() => {    const on = () => {      scroll = scrollY    }    addEventListener('scroll', on, { passive: true })    onCleanup(() => removeEventListener('scroll', on))  })  const draw = ({ ctx, width, height, time, tier }: Frame) => {    const c = colours(ctx.canvas),      cx = width / 2,      cy = height / 2,      unit = (Math.min(width, height) * 0.49) / Math.sqrt(COUNT),      t = 4 + time * (0.25 + tier.speed * 0.35) + scroll * 0.0025    ctx.clearRect(0, 0, width, height)    for (let band = 0; band < 3; band++) {      ctx.fillStyle = [c.far, c.mid, c.near][band]!      ctx.beginPath()      for (let n = band; n < COUNT; n += 3) {        const a = n * GOLDEN + t * 0.06,          r = unit * Math.sqrt(n),          wave = Math.sin(n * 0.02 + t) * 0.5 + 0.5,          breathe = Math.sin(Math.sqrt(n) * 0.55 - t * 1.3) * 0.5 + 0.5        const size = 0.5 + (wave * 0.4 + breathe * 0.6) ** 1.8 * unit * 0.6 * smooth(0, 90, n + 18)        const x = cx + Math.cos(a) * r,          y = cy + Math.sin(a) * r        ctx.moveTo(x + size, y)        ctx.arc(x, y, size, 0, 6.2832)      }      ctx.fill()    }  }  return (    <Canvas      draw={draw}      animate={live}      onTheme={colours.reset}      label="A phyllotaxis disc of dots that swells and settles"    />  )}

Long Thread

One unbroken Hilbert curve, 4,096 points long, that visits every square of the card and never crosses itself. It lays itself down again each time you come back.

Code
tsx
import { Canvas, type CanvasFrame as Frame } from '@kerf-js/veneer'import { state, watch } from '@kerf-js/core'import { inks, type ArtProps, type Meta } from './shared'import code from './long-thread.tsx?raw'export const meta: Meta = {  name: 'Long Thread',  description:    'One unbroken Hilbert curve, 4,096 points long, that visits every square of the card and never crosses itself. It lays itself down again each time you come back.',  kind: 'generative',  code,}const ORDER = 6,  SIDE = 1 << ORDER,  TOTAL = SIDE * SIDE,  RATE = 760const colours = inks({  line: '--veneer-color-text-subtle',  head: '--veneer-color-sweep',  tail: '--veneer-color-accent-text',})/** The classic index-to-point walk, by rotating quadrants. */function point(d: number) {  let rx: number,    ry: number,    x = 0,    y = 0,    t = d  for (let s = 1; s < SIDE; s *= 2) {    rx = 1 & (t / 2)    ry = 1 & (t ^ rx)    if (ry === 0) {      if (rx === 1) {        x = s - 1 - x        y = s - 1 - y      }      ;[x, y] = [y, x]    }    x += s * rx    y += s * ry    t = (t / 4) | 0  }  return [x, y] as const}const points = Array.from({ length: TOTAL }, (_, d) => point(d))export default function LongThread({ live = true }: ArtProps) {  const running = state(true)  let shown = 0,    sheet: HTMLCanvasElement | undefined,    sizedFor = ''  watch(    () => live,    (on) => {      if (on) {        shown = 0        sizedFor = ''        running.value = true      }    },  )  const place = (width: number, height: number) => {    const m = 16,      ux = (width - m * 2) / (SIDE - 1),      uy = (height - m * 2) / (SIDE - 1)    return (d: number) => [m + points[d]![0] * ux, m + points[d]![1] * uy] as const  }  const draw = ({ ctx, width, height, dpr, dt, still }: Frame) => {    const c = colours(ctx.canvas),      at = place(width, height),      key = `${width}x${height}x${dpr}`    if (key !== sizedFor) {      sizedFor = key      sheet = document.createElement('canvas')      sheet.width = width * dpr      sheet.height = height * dpr      sheet.getContext('2d')!.setTransform(dpr, 0, 0, dpr, 0, 0)      if (shown) shown = 0    }    const g = sheet!.getContext('2d')!,      target = still ? TOTAL : Math.min(TOTAL, shown + Math.max(1, dt * RATE))    g.strokeStyle = c.line    g.lineWidth = 1    g.lineJoin = 'round'    g.beginPath()    for (let d = Math.max(1, Math.floor(shown)); d < target; d++) {      const [ax, ay] = at(d - 1),        [bx, by] = at(d)      g.moveTo(ax, ay)      g.lineTo(bx, by)    }    g.stroke()    shown = target    ctx.clearRect(0, 0, width, height)    ctx.drawImage(sheet!, 0, 0, width, height)    if (shown < TOTAL) {      ctx.lineWidth = 1.5      ctx.lineJoin = 'round'      ctx.strokeStyle = c.tail      ctx.beginPath()      const end = Math.floor(shown),        from = Math.max(0, end - 40)      for (let d = from; d < end; d++) {        const [x, y] = at(d)        d === from ? ctx.moveTo(x, y) : ctx.lineTo(x, y)      }      ctx.stroke()      const [hx, hy] = at(end)      ctx.fillStyle = c.head      ctx.fillRect(hx - 2.5, hy - 2.5, 5, 5)    } else if (running.value) running.value = false  }  return (    <Canvas      draw={draw}      animate={live && running.value}      onTheme={() => {        colours.reset()        sizedFor = ''      }}      label="A single Hilbert curve filling the card"    />  )}

Selvedge Plan

A floor plan with nothing to do: hairlines, seeded ratios, a few cells in colour. Click to re-split, and the walls spring to their new places.

Code
tsx
import { Canvas, type CanvasFrame as Frame } from '@kerf-js/veneer'import { inks, rng, type ArtProps, type Meta } from './shared'import code from './selvedge-plan.tsx?raw'export const meta: Meta = {  name: 'Selvedge Plan',  description:    'A floor plan with nothing to do: hairlines, seeded ratios, a few cells in colour. Click to re-split, and the walls spring to their new places.',  kind: 'generative',  code,  rest: true,}const DEPTH = 5const colours = inks({  line: '--veneer-color-text-subtle',  soft: '--veneer-color-accent-soft',  mid: '--veneer-color-accent-border',  solid: '--veneer-color-accent-solid',  ground: '--veneer-color-page',})// One fixed binary tree. Re-splitting only moves its ratios, so every wall has somewhere to spring from.const NODES = (1 << (DEPTH + 1)) - 1export default function SelvedgePlan(_: ArtProps) {  const at = new Float32Array(NODES),    to = new Float32Array(NODES),    speed = new Float32Array(NODES),    tone = new Uint8Array(NODES)  const walls = { at, to, speed }  let seed = 5  function roll() {    const r = rng(seed++ * 7919)    for (let i = 0; i < NODES; i++) {      to[i] = 0.3 + r() * 0.4      tone[i] = r() < 0.09 ? 1 : r() < 0.04 ? 2 : 0    }    tone[12 + (seed % 16)] = 3  }  roll()  at.set(to)  const draw = ({ ctx, width, height, tier, dt }: Frame) => {    const c = colours(ctx.canvas),      m = 18,      lines: number[] = []    const moving = moveWalls(walls, dt)    ctx.clearRect(0, 0, width, height)    const plan = { at, tone, ctx, c, gain: tier.gain, lines }    room(plan, { n: 0, x: m, y: m, w: width - m * 2, h: height - m * 2, depth: 0 })    ctx.beginPath()    for (let i = 0; i < lines.length; i += 4)      stroke(ctx, { x: lines[i]!, y: lines[i + 1]!, w: lines[i + 2]!, h: lines[i + 3]! })    ctx.rect(m, m, width - m * 2, height - m * 2)    ctx.strokeStyle = c.line    ctx.lineWidth = 1    ctx.stroke()    return moving && tier.reach > 0  }  return (    <Canvas      draw={draw}      pointer      onPointerDown={roll}      onTheme={colours.reset}      label="Hairline rectangles split by seeded ratios"    />  )}type Rect = { x: number; y: number; w: number; h: number }const stroke = (ctx: CanvasRenderingContext2D, { x, y, w, h }: Rect) => {  ctx.moveTo(x, y)  ctx.lineTo(x + w, y + h)}function moveWalls(walls: { at: Float32Array; to: Float32Array; speed: Float32Array }, dt: number) {  const { at, to, speed } = walls  let moving = false  for (let i = 0; i < NODES; i++) {    const d = to[i]! - at[i]!    speed[i] = (speed[i]! + d * dt * 90) * Math.exp(-dt * 11)    at[i]! += speed[i]! * dt * 14    if (Math.abs(d) > 0.0008 || Math.abs(speed[i]!) > 0.0008) moving = true    else at[i] = to[i]!  }  return moving}type Plan = {  at: Float32Array  tone: Uint8Array  ctx: CanvasRenderingContext2D  c: ReturnType<typeof colours>  gain: number  lines: number[]}function room(plan: Plan, cell: Rect & { n: number; depth: number }) {  const { at, tone, lines } = plan  const { n, x, y, w, h, depth } = cell  if (depth === DEPTH || (w < 34 && h < 34)) {    fillRoom(plan, tone[n]!, cell)    return  }  const r = at[n]!  if (w > h * 0.8) {    const a = w * r    lines.push(x + a, y, 0, h)    room(plan, { n: n * 2 + 1, x, y, w: a, h, depth: depth + 1 })    room(plan, { n: n * 2 + 2, x: x + a, y, w: w - a, h, depth: depth + 1 })  } else {    const a = h * r    lines.push(x, y + a, w, 0)    room(plan, { n: n * 2 + 1, x, y, w, h: a, depth: depth + 1 })    room(plan, { n: n * 2 + 2, x, y: y + a, w, h: h - a, depth: depth + 1 })  }}function fillRoom({ ctx, c, gain }: Plan, t: number, { x, y, w, h }: Rect) {  if (t === 3 && gain < 0.9) return  if (t) {    ctx.fillStyle = t === 1 ? c.soft : t === 2 ? c.mid : c.solid    ctx.fillRect(x, y, w, h)  }}

Ridgeline

Forty ridges of noise, drawn back to front so each one hides what stands behind it. The pointer lifts the land under it.

Code
tsx
import { Canvas, type CanvasFrame as Frame } from '@kerf-js/veneer'import { fbm, inks, smooth, type ArtProps, type Meta } from './shared'import code from './ridgeline.tsx?raw'export const meta: Meta = {  name: 'Ridgeline',  description:    'Forty ridges of noise, drawn back to front so each one hides what stands behind it. The pointer lifts the land under it.',  kind: 'generative',  code,  wide: true,}const LINES = 40,  POINTS = 150const colours = inks({  line: '--veneer-color-text',  ground: '--veneer-color-page',  accent: '--veneer-color-accent-text',})export default function Ridgeline({ live = true }: ArtProps) {  let px = 0,    lift = 0  const ys = new Float32Array(POINTS + 1)  const draw = ({ ctx, width, height, time, tier, pointer, dt }: Frame) => {    const c = colours(ctx.canvas),      inset = width * 0.11,      span = width - inset * 2,      gap = (height * 0.74) / LINES,      t = 30 + time * tier.speed * 0.05,      relief = height * 0.34    lift += ((pointer.in ? 1 : 0) - lift) * Math.min(1, dt * 5)    if (pointer.in) px += (pointer.x - px) * Math.min(1, dt * 9)    ctx.clearRect(0, 0, width, height)    ctx.lineWidth = 1    ctx.lineJoin = 'round'    for (let i = 0; i < LINES; i++) {      const base = height * 0.2 + i * gap      for (let n = 0; n <= POINTS; n++) {        const u = n / POINTS,          x = inset + u * span,          centre = smooth(0, 0.42, u) * smooth(1, 0.58, u),          peak = centre * centre        const h = fbm(u * 5.5 + 3, i * 0.13 + t, 3),          lifted = lift            ? Math.exp(-(((x - px) / 70) ** 2)) *              lift *              smooth(0, 1, 1 - Math.abs(pointer.y - base) / 180)            : 0        ys[n] = base - (0.03 + 1.1 * (h * h) * peak + 0.12 * peak * h) * relief * (1 + lifted * 0.9)      }      ctx.beginPath()      ctx.moveTo(inset, ys[0]!)      for (let n = 1; n <= POINTS; n++) ctx.lineTo(inset + (n / POINTS) * span, ys[n]!)      ctx.lineTo(inset + span, base + 40)      ctx.lineTo(inset, base + 40)      ctx.closePath()      ctx.fillStyle = c.ground      ctx.fill()      ctx.beginPath()      ctx.moveTo(inset, ys[0]!)      for (let n = 1; n <= POINTS; n++) ctx.lineTo(inset + (n / POINTS) * span, ys[n]!)      ctx.strokeStyle = i === 27 ? c.accent : c.line      ctx.stroke()    }    return lift > 0.01 && lift < 0.99  }  return (    <Canvas      draw={draw}      animate={live}      fps={30}      pointer      onTheme={colours.reset}      label="Forty ridges of a hidden-line landscape"    />  )}

Draft

A real weaving drawdown. Threading along the top, tie-up in the corner, treadling down the side, and the cloth they make is whatever those three arrays say. This is how weavers wrote it down.

Code
tsx
import { Canvas, type CanvasFrame as Frame } from '@kerf-js/veneer'import { palette } from '@kerf-js/veneer-core/dither'import { rng, type ArtProps, type Meta } from './shared'import code from './draft.tsx?raw'export const meta: Meta = {  name: 'Draft',  description:    'A real weaving drawdown. Threading along the top, tie-up in the corner, treadling down the side, and the cloth they make is whatever those three arrays say. This is how weavers wrote it down.',  kind: 'generative',  code,  wide: true,}const SHAFTS = 8,  PITCH = 3,  RULE = 2const SEED = 8/** A point twill threading: up and down the shafts, with a longer run every so often, which is what makes the diamonds. */function threading(n: number) {  const out = new Uint8Array(n),    r = rng(SEED)  let at = 0,    dir = 1,    run = 3 + ((r() * 6) | 0)  for (let i = 0; i < n; i++) {    out[i] = at    if (--run < 0) {      dir = -dir      run = 3 + ((r() * 6) | 0)    }    at = (at + dir + SHAFTS) % SHAFTS  }  return out}// 2/2/1/3 twill on eight shafts: each treadle lifts a run of them.const TIE = Array.from({ length: SHAFTS }, (_, t) =>  Array.from({ length: SHAFTS }, (__, s) => {    const k = (s - t + SHAFTS) % SHAFTS    return k < 2 || k === 3 ? 1 : 0  }),)export default function Draft({ live = true }: ArtProps) {  let image: ImageData | undefined,    out = new Uint32Array(0),    inksNow: Uint32Array | undefined,    thread = new Uint8Array(0),    cols = 0  const treadle = (y: number) => {    const k = ((y % 52) + 52) % 52,      up = k < 26 ? k : 51 - k    return (up + (y >> 6)) % SHAFTS  }  const draw = ({ ctx, width, height, time, tier, still }: Frame) => {    if (!inksNow)      inksNow = palette(ctx.canvas.parentElement!, [        '--veneer-color-page',        '--veneer-color-accent-text',        '--veneer-color-accent-soft',        '--veneer-color-border-strong',        '--veneer-color-text',      ])    const [ground, veneer, kerf, rule, mark] = inksNow as unknown as number[]    if (!image || image.width !== width || image.height !== height) {      image = new ImageData(width, height)      out = new Uint32Array(image.data.buffer)      cols = Math.ceil(width / PITCH)      thread = threading(cols)    }    out.fill(ground!)    const margin = SHAFTS * PITCH + RULE * 2,      right = width - margin,      shift = Math.floor(still ? 24 : 24 + time * (2 + tier.speed * 6))    const span = Math.floor(right / PITCH)    const set = (x: number, y: number, ink: number) => {      for (let b = 0; b < PITCH - 1; b++)        for (let a = 0; a < PITCH - 1; a++) out[(y + b) * width + x + a] = ink    }    // threading    for (let x = 0; x < span; x++) set(x * PITCH, (SHAFTS - 1 - thread[x]!) * PITCH + RULE, mark!)    // tie-up    for (let t = 0; t < SHAFTS; t++)      for (let s = 0; s < SHAFTS; s++)        if (TIE[t]![s]) set(right + RULE + t * PITCH, (SHAFTS - 1 - s) * PITCH + RULE, mark!)    const top = margin,      rows = Math.floor((height - top) / PITCH)    for (let y = 0; y < rows; y++) {      const row = y + shift,        tr = treadle(row),        tie = TIE[tr]!      // treadling, and the cloth: a veneer thread on top wherever its shaft is tied to this row's treadle      set(right + RULE + tr * PITCH, top + y * PITCH, mark!)      for (let x = 0; x < span; x++) {        const lifted = tie[thread[x]!]        const ink = lifted ? veneer! : kerf!,          px = x * PITCH,          py = top + y * PITCH        for (let b = 0; b < PITCH; b++)          for (let a = 0; a < PITCH; a++) out[(py + b) * width + px + a] = ink      }    }    for (let x = 0; x < width; x++) out[(margin - 1) * width + x] = rule!    for (let y = 0; y < height; y++) out[y * width + right] = rule!    ctx.putImageData(image, 0, 0)  }  return (    <Canvas      draw={draw}      animate={live}      cell={2}      fps={12}      onTheme={() => {        inksNow = undefined      }}      label="A weaving drawdown scrolling past, with its threading, tie-up and treadling"    />  )}

Shaders on whole cells

Per-pixel work, at a resolution small enough to stay cheap and big enough to read as ink.

Plasma Ink

Four sines added together, the oldest trick in the demoscene, printed in four inks through an ordered screen. The pointer shifts the phase.

Code
tsx
import { Canvas, type CanvasFrame as Frame } from '@kerf-js/veneer'import { palette, quantize, tile } from '@kerf-js/veneer-core/dither'import { type ArtProps, type Meta } from './shared'import code from './plasma-ink.tsx?raw'export const meta: Meta = {  name: 'Plasma Ink',  description:    'Four sines added together, the oldest trick in the demoscene, printed in four inks through an ordered screen. The pointer shifts the phase.',  kind: 'shader',  code,}const SIZE = 1024,  WAVE = Float32Array.from({ length: SIZE }, (_, i) => Math.sin((i / SIZE) * Math.PI * 2)),  sine = (v: number) => WAVE[(v * (SIZE / (Math.PI * 2))) & (SIZE - 1)]!const SCREEN = tile('bayer8')export default function PlasmaInk({ live = true }: ArtProps) {  let tones = new Float32Array(0),    out = new Uint32Array(0),    image: ImageData | undefined,    cells: Uint32Array | undefined,    shift = 0  const draw = ({ ctx, width, height, time, tier, pointer, dt }: Frame) => {    cells ??= palette(ctx.canvas.parentElement!, [      '--veneer-color-page',      '--veneer-color-accent-soft',      '--veneer-color-accent-border',      '--veneer-color-accent-solid',      '--veneer-color-text',    ])    if (!image || image.width !== width || image.height !== height) {      image = new ImageData(width, height)      out = new Uint32Array(image.data.buffer)      tones = new Float32Array(width * height)    }    if (pointer.in) shift += ((pointer.x / width - 0.5) * 6 - shift) * Math.min(1, dt * 3)    const t = 11 + time * (0.2 + tier.speed * 0.5),      k = 96 / height,      cx = width * (0.5 + 0.3 * Math.sin(t * 0.3)),      cy = height * (0.5 + 0.3 * Math.cos(t * 0.23))    for (let y = 0, i = 0; y < height; y++) {      const v = y * k,        b = sine(v * 0.13 - t * 0.7 + 200),        dy = y - cy      for (let x = 0; x < width; x++, i++) {        const u = x * k,          dx = x - cx        const sum =          sine(u * 0.11 + t + shift + 200) +          b +          sine((u + v) * 0.08 + t * 0.5 + 200) +          sine(Math.sqrt(dx * dx + dy * dy) * k * 0.15 - t + 200)        tones[i] = sum * 0.125 + 0.5      }    }    if (quantize(tones, out, { cols: width, rows: height }, { tile: SCREEN, palette: cells }))      ctx.putImageData(image, 0, 0)  }  return (    <Canvas      draw={draw}      animate={live}      cell={3}      pointer      onTheme={() => {        cells = undefined        image = undefined      }}      label="An interference plasma in four dithered inks"    />  )}

Bobbin SDF

A ray-marched ring of wound thread, 32 steps a pixel, shaded in nothing but dither inks. Drag to turn it. Turns by itself only at vivid.

Code
tsx
import { Canvas, type CanvasFrame as Frame } from '@kerf-js/veneer'import { palette, quantize, tile } from '@kerf-js/veneer-core/dither'import { type ArtProps, type Meta } from './shared'import code from './bobbin-sdf.tsx?raw'export const meta: Meta = {  name: 'Bobbin SDF',  description:    'A ray-marched ring of wound thread, 32 steps a pixel, shaded in nothing but dither inks. Drag to turn it. Turns by itself only at vivid.',  kind: 'shader',  code,}const { sin, cos, sqrt, abs, atan2 } = Mathconst R = 1,  r = 0.46,  BOUND = R + r + 0.02,  STEPS = 32,  EYE = 3.4const SCREEN = tile('bayer4')type Ray = { px: number; py: number; dw: number; ca: number; sa: number; cb: number; sb: number }function shade({ px, py, dw, ca, sa, cb, sb }: Ray) {  // a ray through this pixel, in the ring's own frame  const ox = 0,    oy = 0,    oz = -EYE,    len = sqrt(px * px + py * py + dw * dw),    dx0 = px / len,    dy0 = py / len,    dz0 = dw / len  const dy1 = dy0 * ca - dz0 * sa,    dz1 = dy0 * sa + dz0 * ca,    oy1 = oy * ca - oz * sa,    oz1 = oy * sa + oz * ca  const dx = dx0 * cb + dz1 * sb,    dz = -dx0 * sb + dz1 * cb,    x0 = ox * cb + oz1 * sb,    z0 = -ox * sb + oz1 * cb,    y0 = oy1,    dy = dy1  const b = x0 * dx + y0 * dy + z0 * dz,    disc = b * b - (x0 * x0 + y0 * y0 + z0 * z0 - BOUND * BOUND)  if (disc < 0) return 0  let t = -b - sqrt(disc)  const far = -b + sqrt(disc)  for (let i = 0; i < STEPS && t < far; i++) {    const x = x0 + dx * t,      y = y0 + dy * t,      z = z0 + dz * t,      q = sqrt(x * x + z * z) - R,      d = sqrt(q * q + y * y) - r    if (d < 0.003) {      const major = atan2(z, x),        minor = atan2(y, q),        n = sqrt(x * x + z * z) || 1      const nx = ((x / n) * q) / r,        ny = y / r,        nz = ((z / n) * q) / r      const light = abs(nx * 0.46 + ny * 0.79 + nz * -0.4),        lit = nx * 0.46 + ny * 0.79 + nz * -0.4      const wind = sin(major * 62 + minor * 1.4),        thread = 0.8 + 0.2 * wind,        gloss = lit > 0 ? lit * lit * lit * 0.36 * (0.7 + 0.3 * wind) : 0      return Math.min(1, (0.1 + (lit > 0 ? lit : light * 0.08) * 0.68) * thread + gloss)    }    t += d  }  return 0}type BobbinState = {  tones: Float32Array  out: Uint32Array  image: ImageData | undefined  cells: Uint32Array | undefined  spin: number  tilt: number  dragging: boolean  lx: number  ly: number  parity: number  painted: boolean}function drawBobbin(state: BobbinState, { ctx, width, height, dt, tier, still, pointer }: Frame) {  state.cells ??= palette(ctx.canvas.parentElement!, [    '--veneer-color-page',    '--veneer-color-accent-soft',    '--veneer-color-accent-border',    '--veneer-color-accent-text',    '--veneer-color-text',  ])  let fresh = false  if (!state.image || state.image.width !== width || state.image.height !== height) {    state.image = new ImageData(width, height)    state.out = new Uint32Array(state.image.data.buffer)    state.tones = new Float32Array(width * height)    fresh = true  }  if (pointer.down) {    if (state.dragging) {      state.spin -= (pointer.x - state.lx) * 0.02      state.tilt += (pointer.y - state.ly) * 0.02    }    state.dragging = true    state.lx = pointer.x    state.ly = pointer.y  } else state.dragging = false  const turning = !still && tier.speed >= 0.9  if (!turning && !state.dragging && state.painted && !fresh) return  if (turning) state.spin += dt * 0.35  state.painted = true  const ca = cos(state.tilt),    sa = sin(state.tilt),    cb = cos(state.spin),    sb = sin(state.spin),    scale = 2.5 / height,    cx = width / 2,    cy = height / 2  // half the rows each frame, in turn: the eye averages them and the budget holds  const all = fresh || !turning  for (let y = all ? 0 : state.parity; y < height; y += all ? 1 : 2)    for (let x = 0, i = y * width; x < width; x++, i++)      state.tones[i] = shade({        px: (x - cx) * scale,        py: (y - cy) * scale,        dw: 2.6,        ca,        sa,        cb,        sb,      })  state.parity ^= 1  quantize(    state.tones,    state.out,    { cols: width, rows: height },    { tile: SCREEN, palette: state.cells },  )  ctx.putImageData(state.image, 0, 0)}export default function BobbinSdf({ live = true }: ArtProps) {  const state: BobbinState = {    tones: new Float32Array(0),    out: new Uint32Array(0),    image: undefined,    cells: undefined,    spin: 0.7,    tilt: 1.02,    dragging: false,    lx: 0,    ly: 0,    parity: 0,    painted: false,  }  const draw = (frame: Frame) => drawBobbin(state, frame)  return (    <Canvas      draw={draw}      animate={live}      cell={3}      fps={20}      pointer      onTheme={() => {        state.cells = undefined        state.painted = false      }}      label="A torus of wound thread, ray-marched and dithered"    />  )}

Caustics

Light on the floor of a pool. The water folds it into bright threads, and they never quite repeat.

Code
tsx
import { DitherField } from '@kerf-js/veneer'import { type ArtProps, type Meta } from './shared'import code from './caustics.tsx?raw'export const meta: Meta = {  name: 'Caustics',  description:    'Light on the floor of a pool. The water folds it into bright threads, and they never quite repeat.',  kind: 'shader',  code,  wide: true,}const { abs, PI } = Mathconst SIZE = 4096,  WAVE = Float32Array.from({ length: SIZE }, (_, i) => Math.sin((i / SIZE) * PI * 2)),  PART = SIZE / (PI * 2)const sin = (v: number) => WAVE[(v * PART + SIZE * 512) & (SIZE - 1)]!// Where a veneered wave crosses zero there is a thin bright line. Two sheets of those cross and knot into a net.function sheet(x: number, y: number, t: number) {  const u = x * 5.2 + 1.5 * sin(y * 3.1 + t * 0.3),    w = y * 5.6 + 1.5 * sin(x * 2.7 - t * 0.27),    v = (sin(u + t * 0.2) + sin(w - t * 0.17)) * 0.5,    k = 1 - abs(v),    k2 = k * k,    k4 = k2 * k2  return k4 * k4}function pool(x: number, y: number, t: number) {  const a = sheet(x, y, t),    b = sheet(y * 1.3 + 4, x * 1.3 - 2, t * 0.8 + 9)  return Math.min(1, a * 0.72 + b * 0.5 + a * b * 1.2)}export default function Caustics({ live = true }: ArtProps) {  return (    <DitherField      fill      kind={pool}      pattern="blue"      ramp={[        'none',        '--veneer-color-info-soft',        '--veneer-color-info-border',        '--veneer-color-info-text',      ]}      cell={3}      gain={1.5}      seed={6}      speed={live ? 1 : 0}      label="Caustic light on the floor of a pool"    />  )}

Bad Signal

A word on a failing tube. The rows tear in bursts and the rest barely holds. Good for a 404. Hover and it steadies.

Code
tsx
import { Canvas, type CanvasFrame as Frame } from '@kerf-js/veneer'import { palette, quantize, rasterMask, tile, type Mask } from '@kerf-js/veneer-core/dither'import { smooth, noise, type ArtProps, type Meta } from './shared'import code from './bad-signal.tsx?raw'export const meta: Meta = {  name: 'Bad Signal',  description:    'A word on a failing tube. The rows tear in bursts and the rest barely holds. Good for a 404. Hover and it steadies.',  kind: 'shader',  code,}const SCREEN = tile('bayer4')export default function BadSignal({ live = true }: ArtProps) {  let tones = new Float32Array(0),    out = new Uint32Array(0),    image: ImageData | undefined,    cells: Uint32Array | undefined,    torn: number,    mask: Mask | undefined,    calm = 0  const draw = ({ ctx, width, height, time, dt, pointer, still }: Frame) => {    const host = ctx.canvas.parentElement!    if (!cells) {      cells = palette(host, [        'none',        '--veneer-color-border-strong',        '--veneer-color-text-subtle',        '--veneer-color-text',      ])      torn = palette(host, ['--veneer-color-critical-text'])[0]!    }    if (!image || image.width !== width || image.height !== height) {      image = new ImageData(width, height)      out = new Uint32Array(image.data.buffer)      tones = new Float32Array(width * height)      mask = rasterMask(        { text: '404', weight: 600, fit: 0.72 },        width,        height,        getComputedStyle(host).fontFamily || 'sans-serif',      )    }    calm += ((pointer.in ? 1 : 0) - calm) * Math.min(1, dt * 6)    const t = still ? 3.4 : time,      wild = 1 - calm,      shape = mask!.shape,      halo = mask!.halo,      burst = smooth(0.62, 0.8, noise(t * 1.4, 4)) * wild,      roll = (t * 12) % height    for (let y = 0, i = 0; y < height; y++) {      const band = noise(y * 0.09, t * 3) - 0.5,        jag = noise(y * 0.7, t * 13) - 0.5      const off = Math.round(          (band * 34 * burst + jag * (6 + 22 * burst)) * wild +            Math.sin(y * 0.18 + t * 2) * 1.4 * wild,        ),        scan = y & 1 ? 0.8 : 1      const glow = 1 - Math.abs(((y - roll + height) % height) / height - 0.5) * 0.5      for (let x = 0; x < width; x++, i++) {        const sx = x - off,          at = sx < 0 || sx >= width ? -1 : y * width + sx        const s = at < 0 ? 0 : shape[at]!,          h = at < 0 ? 0 : halo[at]!        tones[i] = (s * 0.96 + h * 0.14) * scan * glow      }    }    quantize(tones, out, { cols: width, rows: height }, { tile: SCREEN, palette: cells })    if (burst > 0.2)      for (let y = 0; y < height; y++)        if (noise(y * 0.3, t * 9) > 0.66)          for (let x = 0, i = y * width; x < width; x++, i++) if (out[i]) out[i] = torn    ctx.putImageData(image, 0, 0)  }  return (    <Canvas      draw={draw}      animate={live}      cell={3}      fps={24}      pointer      onTheme={() => {        cells = undefined        image = undefined      }}      label="The number 404 on a failing screen"    />  )}

Data as cloth

Figures you could read in a table, and can: each piece carries one for screen readers.

Commit Weave

A year of activity as a strip of cloth: fifty-two veneers, seven kerfs, and each thread stands as tall as that day was busy. Hover a day. The year here is a seeded sample.

Code
tsx
import { Canvas, type CanvasFrame as Frame } from '@kerf-js/veneer'import { state } from '@kerf-js/core'import { inks, mix, rng, type ArtProps, type Meta } from './shared'import code from './commit-weave.tsx?raw'export const meta: Meta = {  name: 'Commit Weave',  description:    'A year of activity as a strip of cloth: fifty-two veneers, seven kerfs, and each thread stands as tall as that day was busy. Hover a day. The year here is a seeded sample.',  kind: 'data',  code,  wide: true,  rest: true,}const WEEKS = 52,  DAYS = 7,  NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']const colours = inks({  rule: '--veneer-color-border',  a: '--veneer-color-accent-soft',  b: '--veneer-color-accent-border',  c: '--veneer-color-accent-text',  d: '--veneer-color-text',  hot: '--veneer-color-sweep',})/** Sample activity with a real year's shape: quiet weekends, a summer lull, three bursts before releases. */const year = (() => {  const r = rng(52),    out: number[] = []  for (let w = 0; w < WEEKS; w++)    for (let d = 0; d < DAYS; d++) {      const weekday = d === 0 || d === 6 ? 0.18 : 1,        season = 0.55 + 0.45 * Math.cos(((w - 4) / WEEKS) * Math.PI * 2) ** 2,        burst = [11, 27, 44].some((b) => Math.abs(w - b) < 2) ? 2.3 : 1      out.push(Math.round(r() ** 1.7 * 14 * weekday * season * burst))    }  return out})()const peak = Math.max(...year),  total = year.reduce((a, b) => a + b, 0)export default function CommitWeave(_props: ArtProps) {  const found = state('')  const draw = ({ ctx, width, height, pointer }: Frame) => {    const c = colours(ctx.canvas),      mx = 18,      my = 26,      cw = (width - mx * 2) / WEEKS,      ch = (height - my * 2) / DAYS    ctx.clearRect(0, 0, width, height)    // the kerfs go first, so every veneer lies over them    ctx.fillStyle = c.rule    for (let d = 0; d < DAYS; d++) ctx.fillRect(mx, my + (d + 0.5) * ch - 1, width - mx * 2, 2)    const ramp = [c.a, c.b, c.c, c.d]    const hx = pointer.in ? Math.floor((pointer.x - mx) / cw) : -1,      hy = pointer.in ? Math.floor((pointer.y - my) / ch) : -1    for (let w = 0; w < WEEKS; w++)      for (let d = 0; d < DAYS; d++) {        const n = year[w * DAYS + d]!,          k = n / peak,          tall = mix(0.3, 1.16, Math.sqrt(k)) * ch,          cx = mx + (w + 0.5) * cw,          cy = my + (d + 0.5) * ch,          thick = cw * 0.84        ctx.fillStyle = ramp[Math.min(3, Math.floor(Math.sqrt(k) * 4))]!        ctx.fillRect(cx - thick / 2, cy - tall / 2, thick, tall)        if ((w + d) % 2 && n < peak * 0.5) {          ctx.fillStyle = c.rule          ctx.fillRect(cx - thick / 2, cy - 1, thick, 2)        }        if (w === hx && d === hy) {          ctx.strokeStyle = c.hot          ctx.lineWidth = 2          ctx.strokeRect(cx - thick / 2 - 2, cy - tall / 2 - 2, thick + 4, tall + 4)        }      }    const hit = hx >= 0 && hx < WEEKS && hy >= 0 && hy < DAYS    found.value = hit ? `week ${hx + 1}, ${NAMES[hy]}: ${year[hx * DAYS + hy]} commits` : ''  }  return (    <>      <Canvas        draw={draw}        pointer        onTheme={colours.reset}        label={`A year of activity as woven cloth, ${total} commits in all`}      />      <p data-art="readout">{found.value}</p>      <table data-art="table">        <caption>Commits by week, sample data</caption>        <thead>          <tr>            <th scope="col">Week</th>            {NAMES.map((n) => (              <th key={n} scope="col">                {n}              </th>            ))}          </tr>        </thead>        <tbody>          {Array.from({ length: WEEKS }, (_, w) => (            <tr key={w}>              <th scope="row">{w + 1}</th>              {NAMES.map((n, d) => (                <td key={n}>{year[w * DAYS + d]}</td>              ))}            </tr>          ))}        </tbody>      </table>    </>  )}

Latency Rain

Each request falls as far as it took. The p50 and p95 sit as rules across the rain, and only an error gets a colour. Brush a window to read it. The requests here are a seeded sample.

Code
tsx
import { Canvas, type CanvasFrame as Frame } from '@kerf-js/veneer'import { state } from '@kerf-js/core'import { clamp, inks, rng, type ArtProps, type Meta } from './shared'import code from './latency-rain.tsx?raw'export const meta: Meta = {  name: 'Latency Rain',  description:    'Each request falls as far as it took. The p50 and p95 sit as rules across the rain, and only an error gets a colour. Brush a window to read it. The requests here are a seeded sample.',  kind: 'data',  code,  wide: true,}const COUNT = 900,  MAX = 1000,  LIFE = 9const colours = inks({  drop: '--veneer-color-data-1',  fine: '--veneer-color-text-faint',  err: '--veneer-color-critical-text',  rule: '--veneer-color-text-subtle',  band: '--veneer-color-accent-soft',  ground: '--veneer-color-page',})// log-normal around 80 ms, with the long tail every real service has, and one bad patchconst r = rng(404)const requests = Array.from({ length: COUNT }, () => {  const at = r(),    normal = Math.sqrt(-2 * Math.log(r() || 0.001)) * Math.cos(6.283 * r()),    bad = at > 0.62 && at < 0.7  const ms = Math.min(MAX, Math.exp(4.4 + normal * 0.7 + (bad ? 0.9 : 0)))  return { at, ms, error: r() < (bad ? 0.14 : 0.004), phase: r() }}).sort((a, b) => a.at - b.at)const percentile = (list: number[], q: number) => {  const s = [...list].sort((a, b) => a - b)  return s[Math.floor((s.length - 1) * q)] ?? 0}const whole = requests.map((q) => q.ms),  p50 = percentile(whole, 0.5),  p95 = percentile(whole, 0.95),  fails = requests.filter((q) => q.error).lengthexport default function LatencyRain({ live = true }: ArtProps) {  const found = state('')  const draw = rainDrawing(found)  return (    <>      <Canvas        draw={draw}        animate={live}        fps={30}        pointer        style="touch-action:pan-y;cursor:crosshair"        onTheme={colours.reset}        label="Requests falling as streaks, length by latency"      />      <p data-art="readout">{found.value}</p>      <table data-art="table">        <caption>Latency summary, sample data</caption>        <thead>          <tr>            <th scope="col">Measure</th>            <th scope="col">Value</th>          </tr>        </thead>        <tbody>          <tr>            <th scope="row">Requests</th>            <td>{COUNT}</td>          </tr>          <tr>            <th scope="row">p50</th>            <td>{Math.round(p50)} ms</td>          </tr>          <tr>            <th scope="row">p95</th>            <td>{Math.round(p95)} ms</td>          </tr>          <tr>            <th scope="row">Errors</th>            <td>{fails}</td>          </tr>        </tbody>      </table>    </>  )}type Rain = {  clock: number  span: number  top: number  mx: number  inner: number  scale: (ms: number) => number}function drawRequests(ctx: CanvasRenderingContext2D, c: ReturnType<typeof colours>, rain: Rain) {  const { clock, span, top, mx, inner, scale } = rain  for (let pass = 0; pass < 3; pass++) {    ctx.fillStyle = pass === 2 ? c.err : pass ? c.drop : c.fine    ctx.beginPath()    for (let i = 0; i < COUNT; i++) {      const q = requests[i]!      if (        +q.error * 2 !== pass &&        !(pass === 1 && !q.error && q.ms > p50) &&        !(pass === 0 && !q.error && q.ms <= p50)      )        continue      if (pass === 2 && !q.error) continue      if (pass < 2 && q.error) continue      const age = (clock / LIFE + q.phase) % 1,        fall = Math.min(1, age * 3.2),        reach = scale(q.ms) * span,        tail = Math.min(reach, 12 + reach * 0.42)      const y = top + reach * fall,        x = mx + q.at * inner      ctx.rect(x, y - tail * fall, pass === 2 ? 2 : 1, tail * fall)    }    ctx.fill()  }}function rainDrawing(found: ReturnType<typeof state<string>>) {  let from = -1,    to = -1,    wasDown = false  const scale = (ms: number) => Math.log(1 + ms) / Math.log(1 + MAX)  return ({ ctx, width, height, time, still, pointer }: Frame) => {    const c = colours(ctx.canvas),      mx = 18,      top = 14,      bottom = height - 14,      span = bottom - top,      inner = width - mx * 2,      clock = still ? 6.7 : time    if (pointer.down) {      const u = clamp((pointer.x - mx) / inner)      if (!wasDown) from = u      to = u      const a = Math.min(from, to),        b = Math.max(from, to),        list = requests.filter((q) => q.at >= a && q.at <= b).map((q) => q.ms)      if (list.length > 2)        found.value = `${list.length} requests · p50 ${Math.round(percentile(list, 0.5))} ms · p95 ${Math.round(percentile(list, 0.95))} ms`    }    wasDown = pointer.down    ctx.clearRect(0, 0, width, height)    if (from >= 0) {      ctx.fillStyle = c.band      ctx.fillRect(mx + Math.min(from, to) * inner, top, Math.abs(to - from) * inner, span)    }    drawRequests(ctx, c, { clock, span, top, mx, inner, scale })    ctx.fillStyle = c.rule    ctx.font = '11px var(--veneer-font-mono, monospace)'    for (const [name, value] of [      ['p50', p50],      ['p95', p95],    ] as const) {      const y = Math.round(top + scale(value) * span)      const text = `${name} ${Math.round(value)} ms`      ctx.fillRect(mx, y, inner, 1)      ctx.fillStyle = c.ground      ctx.fillRect(mx, y - 17, ctx.measureText(text).width + 8, 16)      ctx.fillStyle = c.rule      ctx.fillText(text, mx + 4, y - 5)    }  }}

Bundle Tartan

veneer's fourteen largest components, gzipped, set as a tartan: a stripe as wide as its bytes, crossed with itself. Hover to name a stripe.

Code
tsx
import { Canvas, type CanvasFrame as Frame } from '@kerf-js/veneer'import { palette } from '@kerf-js/veneer-core/dither'import { state } from '@kerf-js/core'import { type ArtProps, type Meta } from './shared'import code from './bundle-tartan.tsx?raw'export const meta: Meta = {  name: 'Bundle Tartan',  description:    "veneer's fourteen largest components, gzipped, set as a tartan: a stripe as wide as its bytes, crossed with itself. Hover to name a stripe.",  kind: 'data',  code,  wide: true,  rest: true,}// gzip -c of each source file, 2026-11-01const MODULES: [string, number][] = [  ['DataGrid', 9516],  ['Table', 8949],  ['VirtualList', 5702],  ['Shell', 5557],  ['CommandPalette', 5504],  ['RichSelect', 5118],  ['RichTextEditor', 4521],  ['TimeField', 4283],  ['DateField', 3821],  ['MonthPicker', 3629],  ['Calendar', 3461],  ['ScrollGrid', 3254],  ['Chart', 2943],  ['RecurrenceField', 2924],]const total = MODULES.reduce((n, m) => n + m[1], 0),  SETT_INKS = [    '--veneer-color-text',    '--veneer-color-accent-solid',    '--veneer-color-accent-soft',    '--veneer-color-caution-solid',    '--veneer-color-text-subtle',    '--veneer-color-page',  ]/** A sett reflects at each end, as a real tartan's does. */const SETT = [...MODULES.keys(), ...[...MODULES.keys()].reverse()]export default function BundleTartan(_: ArtProps) {  const found = state('')  let inksNow: Uint32Array | undefined,    map = new Int16Array(0),    image: ImageData | undefined,    out = new Uint32Array(0)  function lay(cols: number) {    map = new Int16Array(cols)    let x = 0    for (let turn = 0; x < cols; turn++)      for (const m of SETT) {        const wide = Math.max(2, Math.round((MODULES[m]![1] / total) * 150))        for (let i = 0; i < wide && x < cols; i++, x++) map[x] = m      }  }  const draw = ({ ctx, width, height, pointer }: Frame) => {    inksNow ??= palette(ctx.canvas.parentElement!, SETT_INKS)    if (!image || image.width !== width || image.height !== height) {      image = new ImageData(width, height)      out = new Uint32Array(image.data.buffer)      lay(Math.max(width, height))      // 2/2 twill: veneer and kerf take turns on a diagonal, which is why tartan's crossings read as a third colour      for (let y = 0, i = 0; y < height; y++)        for (let x = 0; x < width; x++, i++)          out[i] =            inksNow[((x + y) & 3) < 2 ? map[x]! % SETT_INKS.length : map[y]! % SETT_INKS.length]!    }    ctx.putImageData(image, 0, 0)    const over = pointer.in ? map[Math.min(width - 1, Math.floor(pointer.x))]! : -1    if (over >= 0) {      found.value = `${MODULES[over]![0]}: ${(MODULES[over]![1] / 1000).toFixed(1)} kB`      ctx.fillStyle = 'rgb(255 255 255 / 0.55)'      for (let x = 0; x < width; x++) if (map[x] !== over) ctx.fillRect(x, 0, 1, height)    } else found.value = ''  }  return (    <>      <Canvas        draw={draw}        cell={2}        pointer        onTheme={() => {          inksNow = undefined          image = undefined        }}        label={`A tartan of veneer's fourteen largest components, ${(total / 1000).toFixed(0)} kB gzipped`}      />      <p data-art="readout">{found.value}</p>      <table data-art="table">        <caption>Gzipped size of veneer's largest components</caption>        <thead>          <tr>            <th scope="col">Component</th>            <th scope="col">Bytes</th>          </tr>        </thead>        <tbody>          {MODULES.map(([name, bytes]) => (            <tr key={name}>              <th scope="row">{name}</th>              <td>{bytes}</td>            </tr>          ))}        </tbody>      </table>    </>  )}

Token Spectrum

Every colour veneer has, plotted by lightness and chroma, live. Turn the dial, flip the theme or move the seed, and watch the whole system lean together.

Code
tsx
import { Canvas, Slider, type CanvasFrame as Frame } from '@kerf-js/veneer'import { palette } from '@kerf-js/veneer-core/dither'import { state } from '@kerf-js/core'import { inks, type ArtProps, type Meta } from './shared'import code from './token-spectrum.tsx?raw'export const meta: Meta = {  name: 'Token Spectrum',  description:    'Every colour veneer has, plotted by lightness and chroma, live. Turn the dial, flip the theme or move the seed, and watch the whole system lean together.',  kind: 'data',  code,  wide: true,  rest: true,}const FAMILIES = ['accent', 'positive', 'caution', 'critical', 'info', 'highlight', 'neutral'],  STEPS = ['soft', 'border', 'text', 'solid', 'solid-hover']const NAMES = [  ...FAMILIES.flatMap((f) => STEPS.map((s) => `${f}-${s}`)),  'page',  'surface',  'surface-sunken',  'surface-raised',  'surface-hover',  'text',  'text-subtle',  'text-faint',  'border',  'border-strong',  'border-control',  'focus',  'sweep',  'data-1',  'data-2',  'data-3',  'data-4',  'data-5',  'data-6',  'data-7',  'data-8',]const TOKENS = NAMES.map((n) => `--veneer-color-${n}`),  COUNT = NAMES.length,  FAMILY = STEPS.lengthconst colours = inks({  rule: '--veneer-color-border',  axis: '--veneer-color-text-faint',  label: '--veneer-color-text-subtle',  hot: '--veneer-color-text',})const MAX_C = 0.3const linear = (v: number) => {  const c = v / 255  return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4}/** Packed RGBA to OKLCH lightness and chroma: the plane veneer's tokens are designed on. */function oklch(rgba: number) {  const r = linear(rgba & 255),    g = linear((rgba >> 8) & 255),    b = linear((rgba >> 16) & 255)  const l = Math.cbrt(0.4122 * r + 0.5363 * g + 0.0514 * b),    m = Math.cbrt(0.2119 * r + 0.6807 * g + 0.1074 * b),    s = Math.cbrt(0.0883 * r + 0.2817 * g + 0.63 * b)  const L = 0.2104 * l + 0.7936 * m - 0.004 * s,    A = 1.9779 * l - 2.4286 * m + 0.4506 * s,    B = 0.0259 * l + 0.7828 * m - 0.8087 * s  return [L, Math.hypot(A, B)] as const}const css = (v: number) => `rgb(${v & 255} ${(v >> 8) & 255} ${(v >> 16) & 255})`type Spectrum = {  width: number  height: number  across: (chroma: number) => number  up: (light: number) => number  fills: string[]  cs: Float32Array  ls: Float32Array  pointer: Frame['pointer']}function paintSpectrum(  ctx: CanvasRenderingContext2D,  c: ReturnType<typeof colours>,  spectrum: Spectrum,) {  const { width, height, across, up, fills, cs, ls, pointer } = spectrum  const mx = 42,    my = 22,    w = width - mx * 2,    h = height - my * 2 - 12  ctx.clearRect(0, 0, width, height)  ctx.font = '11px var(--veneer-font-mono, monospace)'  ctx.fillStyle = c.rule  for (let v = 0; v <= 1.001; v += 0.25) ctx.fillRect(mx, Math.round(up(v)), w, 1)  for (let v = 0; v <= MAX_C; v += 0.1) ctx.fillRect(Math.round(across(v)), my, 1, h)  ctx.fillStyle = c.axis  ctx.fillText('L', 16, my + 10)  ctx.fillText('chroma →', mx + w - 54, my + h + 16)  // a family's steps are joined, so a ramp reads as a stroke across the plane  ctx.lineWidth = 1  for (let f = 0; f < FAMILIES.length; f++) {    const base = f * FAMILY    ctx.strokeStyle = fills[base + 3]!    ctx.beginPath()    for (let s = 0; s < 4; s++) {      const i = base + s      s ? ctx.lineTo(across(cs[i]!), up(ls[i]!)) : ctx.moveTo(across(cs[i]!), up(ls[i]!))    }    ctx.stroke()  }  let near = -1,    best = 196  for (let i = 0; i < COUNT; i++) {    const x = across(cs[i]!),      y = up(ls[i]!)    ctx.fillStyle = fills[i]!    ctx.beginPath()    ctx.arc(x, y, 5, 0, 6.2832)    ctx.fill()    ctx.strokeStyle = c.rule    ctx.stroke()    if (pointer.in) {      const d = (x - pointer.x) ** 2 + (y - pointer.y) ** 2      if (d < best) {        best = d        near = i      }    }  }  return near}type Targets = { ls: Float32Array; cs: Float32Array; tl: Float32Array; tc: Float32Array }function settleSpectrum({ ls, cs, tl, tc }: Targets, dt: number) {  let settling = false  const k = Math.min(1, (dt || 0.016) * 7)  for (let i = 0; i < COUNT; i++) {    ls[i]! += (tl[i]! - ls[i]!) * k    cs[i]! += (tc[i]! - cs[i]!) * k    if (Math.abs(tl[i]! - ls[i]!) + Math.abs(tc[i]! - cs[i]!) > 0.0015) settling = true  }  return settling}export default function TokenSpectrum(_: ArtProps) {  const found = state(''),    hue = state(152),    moving = state(false)  const ls = new Float32Array(COUNT),    cs = new Float32Array(COUNT),    tl = new Float32Array(COUNT),    tc = new Float32Array(COUNT)  let fills: string[] = [],    stale = true,    first = true  const reread = (el: Element) => {    const packed = palette(el, TOKENS)    fills = []    for (let i = 0; i < COUNT; i++) {      const [l, c] = oklch(packed[i]!)      tl[i] = l      tc[i] = c      fills.push(css(packed[i]!))    }    if (first) {      ls.set(tl)      cs.set(tc)      first = false    }    stale = false  }  const draw = ({ ctx, width, height, dt, pointer }: Frame) => {    if (stale) reread(ctx.canvas.parentElement!)    const c = colours(ctx.canvas),      mx = 42,      my = 22,      w = width - mx * 2,      h = height - my * 2 - 12,      across = (chroma: number) => mx + (chroma / MAX_C) * w,      up = (light: number) => my + (1 - light) * h    const settling = settleSpectrum({ ls, cs, tl, tc }, dt)    const near = paintSpectrum(ctx, c, { width, height, across, up, fills, cs, ls, pointer })    if (near >= 0) {      ctx.strokeStyle = c.hot      ctx.lineWidth = 2      ctx.beginPath()      ctx.arc(across(cs[near]!), up(ls[near]!), 8, 0, 6.2832)      ctx.stroke()    }    found.value =      near >= 0 ? `${NAMES[near]}  L ${ls[near]!.toFixed(3)}  C ${cs[near]!.toFixed(3)}` : ''    if (!settling && moving.value) moving.value = false    return settling  }  const turn = (v: number) => {    hue.value = v    stale = true    moving.value = true  }  return (    <div data-art="pierce" data-veneer="" style={`--veneer-seed-accent-h:${hue.value}`}>      <Canvas        draw={draw}        animate={moving.value}        pointer        onTheme={() => {          colours.reset()          stale = true        }}        label="Every veneer colour token on the lightness and chroma plane"      />      <div data-art="controls" data-art-end="">        <div data-art-seed="">          <Slider label="Seed hue" value={hue.value} min={0} max={360} onChange={turn} />        </div>      </div>      <p data-art="readout">{found.value}</p>    </div>  )}

Socket Tide

A day of open connections around a clock, rising with one timezone and falling with the next. Point at an hour to read it. The traffic here is a seeded sample: the site has no public socket to read yet.

Code
tsx
import { Canvas, type CanvasFrame as Frame } from '@kerf-js/veneer'import { state } from '@kerf-js/core'import { inks, rng, type ArtProps, type Meta } from './shared'import code from './socket-tide.tsx?raw'export const meta: Meta = {  name: 'Socket Tide',  description:    'A day of open connections around a clock, rising with one timezone and falling with the next. Point at an hour to read it. The traffic here is a seeded sample: the site has no public socket to read yet.',  kind: 'data',  code,}const SLOTS = 288,  TAU = Math.PI * 2const colours = inks({  rule: '--veneer-color-border',  low: '--veneer-color-accent-border',  high: '--veneer-color-accent-text',  hand: '--veneer-color-sweep',  label: '--veneer-color-text-faint',})const day = (() => {  const r = rng(24),    out = new Float32Array(SLOTS)  let noise = 0  for (let i = 0; i < SLOTS; i++) {    const h = (i / SLOTS) * 24,      gauss = (at: number, wide: number) => Math.exp(-((((h - at + 36) % 24) - 12) ** 2) / wide)    noise = noise * 0.8 + (r() - 0.5) * 0.09    out[i] = 0.24 + 0.72 * gauss(14, 10) + 0.5 * gauss(21, 7) + 0.26 * gauss(7, 5) + noise  }  return out})()const scale = 1800,  count = (i: number) => Math.round(day[i]! * scale)type Ring = { cx: number; cy: number; inner: number; span: number }function tideRings(ctx: CanvasRenderingContext2D, c: ReturnType<typeof colours>, ring: Ring) {  const { cx, cy, inner, span } = ring  ctx.lineWidth = 1  ctx.strokeStyle = c.rule  ctx.beginPath()  for (const k of [0, 0.5, 1]) {    ctx.moveTo(cx + inner + span * k, cy)    ctx.arc(cx, cy, inner + span * k, 0, TAU)  }  ctx.stroke()}function tideHours(ctx: CanvasRenderingContext2D, c: ReturnType<typeof colours>, ring: Ring) {  const { cx, cy, inner } = ring  ctx.fillStyle = c.label  ctx.font = '11px var(--veneer-font-mono, monospace)'  ctx.textAlign = 'center'  for (let h = 0; h < 24; h += 6) {    const a = (h / 24) * TAU - Math.PI / 2    ctx.fillText(      String(h).padStart(2, '0'),      cx + Math.cos(a) * inner * 0.74,      cy + Math.sin(a) * inner * 0.74 + 4,    )  }  ctx.textAlign = 'start'}export default function SocketTide({ live = true }: ArtProps) {  const found = state('')  const draw = ({ ctx, width, height, time, still, pointer }: Frame) => {    const c = colours(ctx.canvas),      cx = width / 2,      cy = height / 2,      outer = Math.min(width, height) * 0.47,      inner = outer * 0.36,      span = outer - inner,      t = still ? 2 : time    const ring = { cx, cy, inner, span }    const dx = pointer.x - cx,      dy = pointer.y - cy,      at =        pointer.in && dx * dx + dy * dy > 400          ? Math.floor((((Math.atan2(dy, dx) + Math.PI / 2 + TAU) % TAU) / TAU) * SLOTS)          : -1    ctx.clearRect(0, 0, width, height)    tideRings(ctx, c, ring)    for (let pass = 0; pass < 2; pass++) {      ctx.strokeStyle = pass ? c.high : c.low      ctx.lineWidth = pass ? 1.4 : 1      ctx.beginPath()      for (let i = pass ? 0 : 1; i < SLOTS; i += 2) {        const a = (i / SLOTS) * TAU - Math.PI / 2,          swell = Math.sin(a * 3 + t * 0.7) * 0.012 + Math.sin(a * 11 - t * 1.1) * 0.006,          r = inner + span * Math.min(1, day[i]! + swell),          cos = Math.cos(a),          sin = Math.sin(a)        ctx.moveTo(cx + cos * inner, cy + sin * inner)        ctx.lineTo(cx + cos * r, cy + sin * r)      }      ctx.stroke()    }    tideHours(ctx, c, ring)    const hand = at >= 0 ? at : (0.4 * SLOTS + t * 0.5) % SLOTS,      a = (hand / SLOTS) * TAU - Math.PI / 2    ctx.strokeStyle = c.hand    ctx.lineWidth = 2    ctx.beginPath()    ctx.moveTo(cx + Math.cos(a) * inner, cy + Math.sin(a) * inner)    ctx.lineTo(cx + Math.cos(a) * (outer + 2), cy + Math.sin(a) * (outer + 2))    ctx.stroke()    found.value =      at >= 0        ? `${String(Math.floor(at / 12)).padStart(2, '0')}:${String((at % 12) * 5).padStart(2, '0')}  ${count(at).toLocaleString('en')} open`        : ''  }  return (    <>      <Canvas        draw={draw}        animate={live}        fps={30}        pointer        onTheme={colours.reset}        label="A day of socket connections around a clock"      />      <p data-art="readout">{found.value}</p>      <table data-art="table">        <caption>Open connections by hour, sample data</caption>        <thead>          <tr>            <th scope="col">Hour</th>            <th scope="col">Connections</th>          </tr>        </thead>        <tbody>          {Array.from({ length: 24 }, (_, h) => (            <tr key={h}>              <th scope="row">{h}:00</th>              <td>{count(h * 12)}</td>            </tr>          ))}        </tbody>      </table>    </>  )}

For your hand

Nothing happens until you touch them, and they cost nothing until you do.

Pluck

Sixty strings, each a one-dimensional wave equation. Drag across them. When they settle the canvas stops, so at rest it costs nothing.

Code
tsx
import { Canvas, type CanvasFrame as Frame } from '@kerf-js/veneer'import { inks, type ArtProps, type Meta } from './shared'import code from './pluck.tsx?raw'export const meta: Meta = {  name: 'Pluck',  description:    'Sixty strings, each a one-dimensional wave equation. Drag across them. When they settle the canvas stops, so at rest it costs nothing.',  kind: 'pointer',  code,  wide: true,  rest: true,}const STRINGS = 60,  NODES = 44,  WAVE = 0.3,  DAMP = 0.996const colours = inks({  line: '--veneer-color-border-control',  hot: '--veneer-color-accent-text',  spark: '--veneer-color-sweep',})export default function Pluck(_: ArtProps) {  const u = new Float32Array(STRINGS * NODES),    v = new Float32Array(STRINGS * NODES)  let lx = -1,    due = 0,    ready = false  const draw = ({ ctx, width, height, dt, tier, pointer }: Frame) => {    const c = colours(ctx.canvas),      gap = width / (STRINGS + 1),      my = 14,      cell = (height - my * 2) / (NODES - 1)    if (!ready) {      chord(u, v)      ready = true    }    if (tier.reach > 0 && pointer.in) {      if (lx >= 0 && pointer.x !== lx) {        const a = Math.floor(Math.min(lx, pointer.x) / gap),          b = Math.floor(Math.max(lx, pointer.x) / gap),          dir = pointer.x > lx ? 1 : -1        for (let s = Math.max(0, a); s <= Math.min(STRINGS - 1, b); s++)          if (s + 1 > Math.min(lx, pointer.x) / gap && s + 1 <= Math.max(lx, pointer.x) / gap)            bendString(u, s, {              k: Math.max(0.1, Math.min(0.9, (pointer.y - my) / (height - my * 2))),              size: dir * (pointer.down ? 22 : 13),            })      }      lx = pointer.x    } else lx = -1    let peak = 0    if (tier.reach > 0) {      due = Math.min(due + dt, 0.05)      for (; due > 0.004; due -= 0.004) peak = stepStrings(u, v)    }    ctx.clearRect(0, 0, width, height)    paintStrings(ctx, c, { u, gap, my, cell })    return tier.reach > 0 && peak > 0.05  }  return (    <Canvas      draw={draw}      pointer      style="touch-action:pan-y"      onTheme={colours.reset}      label="Sixty strings that ring when dragged across"    />  )}/** Bends string `s` into a soft triangle peaking at `k` along it, the shape a finger leaves. */function bendString(u: Float32Array, s: number, { k, size }: { k: number; size: number }) {  const at = s * NODES  for (let n = 1; n < NODES - 1; n++) {    const f = n / (NODES - 1)    u[at + n] = size * (f < k ? f / k : (1 - f) / (1 - k))  }}function stepStrings(u: Float32Array, v: Float32Array) {  let peak = 0  for (let s = 0; s < STRINGS; s++) {    const at = s * NODES    for (let n = 1; n < NODES - 1; n++) {      const i = at + n      v[i] = (v[i]! + (u[i - 1]! + u[i + 1]! - 2 * u[i]!) * WAVE) * DAMP    }  }  for (let i = 0; i < u.length; i++) {    u[i]! += v[i]!    const a = Math.abs(u[i]!) + Math.abs(v[i]!)    if (a > peak) peak = a  }  return peak}type Strings = { u: Float32Array; gap: number; my: number; cell: number }function paintStrings(ctx: CanvasRenderingContext2D, c: ReturnType<typeof colours>, o: Strings) {  const { u, gap, my, cell } = o  for (let pass = 0; pass < 2; pass++) {    ctx.beginPath()    for (let s = 0; s < STRINGS; s++) {      const at = s * NODES,        x = (s + 1) * gap      let energy = 0      for (let n = 1; n < NODES - 1; n += 4) energy += Math.abs(u[at + n]!)      if (energy > 6 !== !!pass) continue      ctx.moveTo(x + u[at]!, my)      for (let n = 1; n < NODES; n++) ctx.lineTo(x + u[at + n]!, my + n * cell)    }    ctx.strokeStyle = pass ? c.hot : c.line    ctx.lineWidth = pass ? 1.25 : 1    ctx.stroke()  }}// A still is a chord caught mid-ring: the first run of it, frozen.function chord(u: Float32Array, v: Float32Array) {  for (let s = 6; s < STRINGS - 6; s += 1)    bendString(u, s, {      k: 0.3 + 0.4 * (s / STRINGS),      size: 26 * Math.sin((s / STRINGS) * Math.PI) ** 3 * Math.sin(s * 0.35) ** 2,    })  for (let i = 0; i < 9; i++) stepStrings(u, v)}

Filings

Iron filings around two poles. Bring the pointer in as a third, and click to flip it.

Code
tsx
import { Canvas, type CanvasFrame as Frame } from '@kerf-js/veneer'import { inks, type ArtProps, type Meta } from './shared'import code from './filings.tsx?raw'export const meta: Meta = {  name: 'Filings',  description:    'Iron filings around two poles. Bring the pointer in as a third, and click to flip it.',  kind: 'pointer',  code,  rest: true,}const PITCH = 11,  LENGTH = 9const colours = inks({  near: '--veneer-color-text',  mid: '--veneer-color-text-subtle',  far: '--veneer-color-text-faint',})export default function Filings(_: ArtProps) {  let cols = 0,    rows = 0,    angle = new Float32Array(0),    polarity = 1  const draw = ({ ctx, width, height, dt, pointer, tier }: Frame) => {    const c = colours(ctx.canvas),      nc = Math.ceil(width / PITCH),      nr = Math.ceil(height / PITCH),      fresh = nc !== cols || nr !== rows    if (fresh) {      cols = nc      rows = nr      angle = new Float32Array(cols * rows)    }    const ax = width * 0.33,      bx = width * 0.67,      mid = height / 2,      poles = [        [ax, mid, 1],        [bx, mid, -1],        ...(pointer.in ? [[pointer.x, pointer.y, 2.2 * polarity]] : []),      ] as number[][]    const k = fresh || !dt ? 1 : Math.min(1, dt * (tier.reach > 0 ? 12 : 60)),      buckets: number[][] = [[], [], []]    let moving = false    for (let y = 0, i = 0; y < rows; y++)      for (let x = 0; x < cols; x++, i++) {        const px = (x + 0.5) * PITCH,          py = (y + 0.5) * PITCH        let fx = 0,          fy = 0        for (const [qx, qy, q] of poles) {          const dx = px - qx!,            dy = py - qy!,            d = dx * dx + dy * dy + 80,            s = (q! * 30000) / d          fx += (dx / Math.sqrt(d)) * s          fy += (dy / Math.sqrt(d)) * s        }        const want = Math.atan2(fy, fx),          turn = ((want - angle[i]! + Math.PI * 3) % (Math.PI * 2)) - Math.PI        if (Math.abs(turn) > 0.01) moving = true        const now = (angle[i]! += turn * k),          power = Math.hypot(fx, fy),          tick = LENGTH * Math.min(1, 0.5 + power * 0.1),          cos = (Math.cos(now) * tick) / 2,          sin = (Math.sin(now) * tick) / 2        buckets[power > 7 ? 0 : power > 1.6 ? 1 : 2]!.push(px - cos, py - sin, px + cos, py + sin)      }    ctx.clearRect(0, 0, width, height)    ctx.lineCap = 'round'    ;[c.near, c.mid, c.far].forEach((ink, n) => {      const list = buckets[n]!      ctx.strokeStyle = ink      ctx.lineWidth = n ? 1 : 1.3      ctx.beginPath()      for (let i = 0; i < list.length; i += 4) {        ctx.moveTo(list[i]!, list[i + 1]!)        ctx.lineTo(list[i + 2]!, list[i + 3]!)      }      ctx.stroke()    })    return moving  }  return (    <Canvas      draw={draw}      pointer      onPointerDown={() => {        polarity = -polarity      }}      onTheme={colours.reset}      label="Iron filings lined up between two magnetic poles"    />  )}

Sand Table

A raked garden. Draw through it and the sand piles up along your groove, then slowly finds its way back.

Code
tsx
import { DitherField } from '@kerf-js/veneer'import { state } from '@kerf-js/core'import { clamp, type ArtProps, type Meta } from './shared'import code from './sand-table.tsx?raw'export const meta: Meta = {  name: 'Sand Table',  description:    'A raked garden. Draw through it and the sand piles up along your groove, then slowly finds its way back.',  kind: 'pointer',  code,  rest: true,}const W = 170,  H = 106,  STONES = [    [0.3, 0.42, 0.11],    [0.72, 0.62, 0.075],    [0.56, 0.2, 0.04],  ] as constexport default function SandTable(_: ArtProps) {  const rake = new Float32Array(W * H),    sand = new Float32Array(W * H),    rock = new Uint8Array(W * H),    shade = new Float32Array(W * H)  const busy = state(false)  let aspect = 1.6,    stamp = NaN,    built = false,    lx = -1,    ly = -1,    restless = 0  const garden = (x: number, y: number, t: number) => {    if (!built) {      buildGarden({ rake, sand, rock, shade })      built = true    }    if (x > aspect) aspect = x    if (t !== stamp) {      stamp = t      if (settle(rake, sand) < 40 && --restless < 0) busy.value = false      relight(rock, sand, shade)    }    return shade[Math.min(H - 1, (y * H) | 0) * W + Math.min(W - 1, ((x / aspect) * W) | 0)]!  }  const move = (e: PointerEvent) => {    const r = (e.currentTarget as HTMLElement).getBoundingClientRect(),      x = (((e.clientX - r.left) / r.width) * W) | 0,      y = (((e.clientY - r.top) / r.height) * H) | 0,      steps = lx < 0 ? 1 : Math.max(1, Math.hypot(x - lx, y - ly) | 0)    for (let n = 1; n <= steps; n++)      plough(        {          x: lx < 0 ? x : Math.round(lx + ((x - lx) * n) / steps),          y: lx < 0 ? y : Math.round(ly + ((y - ly) * n) / steps),        },        rock,        sand,      )    lx = x    ly = y    restless = 30    busy.value = true  }  return (    <DitherField      fill      kind={garden}      ramp="neutral"      pattern="blue"      cell={3}      spread={0.9}      gain={1.25}      seed={2}      speed={busy.value ? 1 : 0}      onPointerMove={move}      onPointerLeave={() => {        lx = -1      }}      style="touch-action:none"      label="A raked sand garden around three stones"    />  )}function relight(rock: Uint8Array, sand: Float32Array, shade: Float32Array) {  for (let y = 1; y < H - 1; y++)    for (let x = 1, i = y * W + 1; x < W - 1; x++, i++)      shade[i] = rock[i]        ? 1        : clamp(0.44 + (sand[i - 1]! - sand[i + 1]! + sand[i - W]! - sand[i + W]!) * 0.26)}function settle(rake: Float32Array, sand: Float32Array) {  let moved = 0  for (let y = 1; y < H - 1; y++)    for (let x = 1, i = y * W + 1; x < W - 1; x++, i++) {      const d = rake[i]! - sand[i]!      if (Math.abs(d) < 0.004) continue      moved++      sand[i]! +=        d * 0.012 +        (sand[i - 1]! + sand[i + 1]! + sand[i - W]! + sand[i + W]! - 4 * sand[i]!) * 0.05    }  return moved}function plough({ x: px, y: py }: { x: number; y: number }, rock: Uint8Array, sand: Float32Array) {  for (let dy = -5; dy <= 5; dy++)    for (let dx = -5; dx <= 5; dx++) {      const X = px + dx,        Y = py + dy,        d = Math.hypot(dx, dy)      if (X < 1 || Y < 1 || X >= W - 1 || Y >= H - 1 || d > 5) continue      const i = Y * W + X      if (!rock[i]) sand[i] = d < 2.6 ? -1 : d < 4.4 ? Math.max(sand[i]!, 0.9) : sand[i]!    }}type Garden = { rake: Float32Array; sand: Float32Array; rock: Uint8Array; shade: Float32Array }function buildGarden({ rake, sand, rock, shade }: Garden) {  for (let y = 0, i = 0; y < H; y++)    for (let x = 0; x < W; x++, i++) {      const u = (x / W) * 1.6,        v = y / H      let near = 9,        stone = -1      STONES.forEach(([sx, sy, r], n) => {        const d = Math.hypot(u - sx * 1.6, v - sy) - r        if (d < near) {          near = d          stone = n        }      })      // rings round each stone give way to straight rows, as a rake does      const rows = Math.sin(v * 2 * Math.PI * 11),        rings = Math.sin(near * 2 * Math.PI * 11 + stone),        k = clamp((near - 0.2) / 0.09)      rake[i] = sand[i] = (rings * (1 - k) + rows * k) * 0.5      rock[i] = near < 0 ? 1 : 0    }  relight(rock, sand, shade)}