Blockfall

Painted stone blocks fall into a gold-edged glass well floating in the dark. The CPU lists blocks as render instances, and the GPU eases them between cells, brightest on the falling piece. A ghost shows where the piece will land. A fixed-step clock controls drops, and coroutines clear rows. A warm spot light out of view and a cool sun shadow the blocks, and ambient occlusion darkens their creases. Cleared blocks flash and break into stone chips that tumble, land flat and shadow the floor, with a little dust. Embers drift up past the well. MSAA smooths the HDR render, with bloom.

Files, in reading order

  1. scene.tsCreates the scene: its resources, its settings and what each frame records.
  2. game.tsRuns game rules on the CPU.
  3. blocks.tsDraws carved solid blocks, their plinth and landing ghosts as rounded-box instances.
  4. shards.tsBreaks cleared blocks into painted stone chips and a little sandstone dust.
  5. controls.tsThe game's buttons, as ui.ts declares them.
  6. stage.tsThe well's light, a warm spot out of view, and the embers drifting past it.
  7. well.tsDraws the well's far glass walls with a faint grid of its cells and gold edges, so its limits read from any side without standing between the camera and the play.
  8. ui.ts

Kit modules ao, bindings, bloom, cameras, clock, coroutines, environment, finish, instances, lighting, materials, meshPool, owner, particles, passes, pipelines, shading, table, targets. Also @typegpu/color, typegpu.

scene.ts

import {  ao,  bindings,  bloom,  cameras,  clock,  coroutines,  finish,  lighting,  owner,  passes,  pipelines,  shading,  targets,  type Frame,  type Surface,} from '@typegpu/kit';import { d, tgpu, type TgpuRoot } from 'typegpu';import { CELLS, createBlockfall, DEPTH, HEIGHT, PIECE, WIDTH, type Score } from './game.ts';import { createBlocks } from './blocks.ts';import { createShards } from './shards.ts';import { createControls } from './controls.ts';import { backdrop, createStage, cube, STAGE } from './stage.ts';import { createWell } from './well.ts';const SAMPLES = 4;const HDR = 'rgba16float';/** Well centre and shadow centre. */const MIDDLE = [WIDTH / 2, HEIGHT / 2, DEPTH / 2] as const;/** Seconds the glow takes to settle after blocks break, and its added intensity. */const SWELL = { seconds: 0.7, intensity: 0.3 };/** Camera target a little below the middle, toward the block pile. */const TARGET = [WIDTH / 2, HEIGHT / 2 - 0.4, DEPTH / 2] as const;/** How far occluders darken sky light, in cells: about a block, for the creases between them. */const CREASE = 0.8;/** Darkens the sky light in creases, between blocks and where they meet the floor. */const occludedSky = (material: shading.Material, point: shading.Point) => {  'use gpu';  return shading.hemisphere(material, point) * ao.at(point.position);};export interface SceneOptions {  /** Receives the score at startup and after each clear. */  readonly onScore?: (score: Score) => void;}/** * Draws 3D Blockfall in a glass well floating in the dark, under a warm key and a cool fill, with * MSAA, ambient occlusion, stone chips, bloom and a final pass. Its `ui` binds the buttons `ui.ts` * declares to `game` and `view`. */export async function createScene(root: TgpuRoot, surface: Surface, options: SceneOptions = {}) {  const own = owner();  const screen = own(    targets.create(root, surface, {      color: { format: HDR, samples: SAMPLES },      depth: { format: 'depth24plus', samples: SAMPLES },      hdr: { format: HDR, sampled: true },    }),  );  const view = cameras.orbit(root, {    size: surface,    target: TARGET,    distance: 21,    pitch: 0.4,    yaw: 0,    pitchRange: [0.15, 1.45],    distanceRange: [12, 32],    far: 200,  });  const time = root.createUniform(d.f32);  const effects = clock.fixedStep({ step: 1 / 60, maxSteps: 4 });  const blocks = own(    createBlocks(root, {      time,      capacity: CELLS + PIECE,      ghosts: PIECE,      plinth: {        at: [WIDTH / 2, -0.32, DEPTH / 2],        size: [WIDTH + 1, 0.6, DEPTH + 1],        color: STAGE.plinth,      },      grid: [WIDTH, DEPTH],      scenery: [cube],    }),  );  const stage = own(createStage(root, { camera: view, step: effects.step }));  /** Half-resolution depth the opaque drawings draw first, for the occlusion to read. */  const early = own(    targets.create(root, surface, {      depth: { format: 'depth32float', scale: 0.5, sampled: true },    }),  );  const prepass = passes    .create(root, early, { depth: passes.depth }, { label: 'prepass' })    .pipe(view.inject());  const occlusion = own(    ao.create(root, view, { depth: () => early.textures.depth, settings: { radius: CREASE } }),  );  const light = own(    lighting.create(root, {      settings: STAGE.settings,      shading: { ambient: tgpu.fn(occludedSky).pipe(occlusion.inject()) },      shadow: { size: 2048, extent: 16, center: MIDDLE, filter: 9 },      points: { table: stage.lamps },      pointShadow: { count: 1, size: 1024, filter: 9 },    }),  );  light.pointShadow.cast([stage.key]);  const shared = bindings.share(root, { ao: occlusion.part });  const scene = passes    .create(      root,      screen,      { color: passes.color, depth: passes.depth },      { resolve: { color: 'hdr' }, shared },    )    .pipe(view.inject())    .beneath(light.inject());  const shards = own(    createShards(root, {      camera: view,      step: effects.step,      time,      pool: blocks.pool,      chip: blocks.sceneryMeshes[0]!,    }),  );  // The backdrop fills what the blocks leave. The well, ghosts, embers and dust blend over them  const drawn = [    scene.add(blocks.solid),    scene.add(shards.chips),    scene.add(backdrop),    scene.add(createWell([WIDTH, HEIGHT, DEPTH])),    scene.add(blocks.ghosts),    scene.add(stage.embers.drawing),    ...shards.drawings.map((drawing) => scene.add(drawing)),  ];  const shapes = [prepass.add(blocks.solid), prepass.add(shards.chips)];  const casters = [light.shadow.add(blocks.solid), light.shadow.add(shards.chips)];  const lit = [light.pointShadow.add(blocks.solid), light.pointShadow.add(shards.chips)];  const stirred = () => !blocks.still(now) || shards.flying(now);  const glow = own(    bloom.create(root, { source: () => screen.textures.hdr, settings: STAGE.bloom }),  );  const final = own(    finish.create(root, {      format: surface.format,      source: () => screen.textures.hdr,      glow,      settings: STAGE.finish,    }),  );  await pipelines.compile([    scene,    prepass,    occlusion,    light.shadow,    light.pointShadow,    glow,    final,  ]);  let now = 0;  let brokeAt = -Infinity;  let swelling = false;  let score = '';  const scheduler = own(coroutines.create());  const game = createBlockfall({    blocks,    scheduler,    onScore(state) {      score = `${state.score}\nlines ${state.lines} · level ${state.level}`;      options.onScore?.(state);    },    onBreak(x, y, z, kind) {      shards.break(x, y, z, kind, now);      brokeAt = now;    },  });  /** Swells the glow as blocks break, settling over `SWELL.seconds`. */  const swell = () => {    const left = Math.max(0, 1 - (now - brokeAt) / SWELL.seconds);    if (left === 0 && !swelling) return;    final.settings.patch({      glowIntensity: finish.defaults.glowIntensity + SWELL.intensity * left * left,    });    swelling = left > 0;  };  return {    view,    ui: { score: () => score, controls: createControls(game, view) },    game,    frame(frame: Frame) {      time.write(frame.time);      game.update(frame);      now = frame.time;      scheduler.run(frame);      swell();      view.update(frame.dt);      screen.fit();      early.fit();      blocks.update(frame);      shards.update();      const compute = frame.encoder.beginComputePass();      for (let steps = effects.advance(frame.dt); steps > 0; steps--) {        shards.step(compute);        stage.embers.step(compute);      }      shards.spawn(compute);      stage.embers.spawn(compute);      shards.compact(compute, effects.ahead);      stage.embers.compact(compute, effects.ahead);      compute.end();      prepass.record(frame, shapes);      occlusion.record(frame);      // Shadows hold while the blocks do, and the scenery never moves      light.shadow.record(frame, casters, { redraw: stirred });      light.pointShadow.record(frame, lit, { redraw: stirred });      scene.record(frame, drawn);      glow.record(frame);      final.record(frame, surface.view(frame));    },    destroy: own.destroy,  };}

game.ts

import { clock, coroutines, type Frame, type Coroutines } from '@typegpu/kit';import { CLEAR_TIME, type BlockId, type Blocks } from './blocks.ts';export const WIDTH = 6;export const DEPTH = 6;export const HEIGHT = 12;/** Cells per well layer and total cells. */export const LAYER = WIDTH * DEPTH;export const CELLS = LAYER * HEIGHT;export const PIECE = 4;type Cell = [x: number, y: number, z: number];/** Spawn origin at the well centre, two cells below the top. */const SPAWN: Cell = [WIDTH / 2 - 1, HEIGHT - 2, DEPTH / 2 - 1];/** Each piece has `PIECE` cubes. The last two require three dimensions. */// oxfmt-ignoreconst SHAPES: readonly (readonly Cell[])[] = [  [[-1, 0, 0], [0, 0, 0], [1, 0, 0], [2, 0, 0]],  [[0, 0, 0], [1, 0, 0], [0, 0, 1], [1, 0, 1]],  [[-1, 0, 0], [0, 0, 0], [1, 0, 0], [0, 0, 1]],  [[-1, 0, 0], [0, 0, 0], [1, 0, 0], [1, 0, 1]],  [[-1, 0, 1], [0, 0, 1], [0, 0, 0], [1, 0, 0]],  [[0, 0, 0], [1, 0, 0], [0, 0, 1], [0, 1, 0]],  [[0, 0, 0], [1, 0, 0], [1, 0, 1], [1, 1, 1]],];/** Offsets tried, in order, when a rotation collides. */// oxfmt-ignoreconst KICKS: readonly Cell[] = [[0, 0, 0], [1, 0, 0], [-1, 0, 0], [0, 0, 1], [0, 0, -1], [0, 1, 0]];/** Score by cleared-layer count, from zero to four, multiplied by level. */const POINTS = [0, 100, 300, 600, 1000];/** Layers to clear for each level. */const LINES_PER_LEVEL = 6;/** Initial drop interval in seconds, multiplier per level and minimum interval. */const DROP = { first: 0.8, keep: 0.85, least: 0.1 };/** Delay in seconds between game-over layer flashes, ordered top down. */const CASCADE = 0.04;/** Seconds between drops at `level`. */const dropStep = (level: number) => Math.max(DROP.least, DROP.first * DROP.keep ** (level - 1));/** Linear grid index for `x, y, z`, ordered by layer from the bottom. */const at = (x: number, y: number, z: number) => y * LAYER + z * WIDTH + x;/** Whether column `x, z` lies inside the well's walls. */const inside = (x: number, z: number) => x >= 0 && x < WIDTH && z >= 0 && z < DEPTH;/** Rotates a cell a quarter turn around `axis` (0 x, 1 y, 2 z), counter-clockwise for turn 1. */function rotate([x, y, z]: Cell, axis: number, turn: number): Cell {  if (axis === 0) return [x, -turn * z, turn * y];  if (axis === 1) return [turn * z, y, -turn * x];  return [-turn * y, turn * x, z];}export interface Score {  readonly score: number;  readonly lines: number;  readonly level: number;}export interface BlockfallOptions {  readonly blocks: Blocks;  /** Runs layer clears and the game-over cascade. */  readonly scheduler: Coroutines;  /** Receives the score at startup and after each clear or game over. */  readonly onScore: (score: Score) => void;  /** Receives each cleared block's cell and kind as it breaks. Default none. */  readonly onBreak?: (x: number, y: number, z: number, kind: number) => void;}/** Runs game rules on the CPU. */export function createBlockfall(options: BlockfallOptions) {  const { blocks, scheduler, onScore, onBreak } = options;  const grid: (BlockId | undefined)[] = Array.from({ length: CELLS });  /** The block ids of layer `y`, from the bottom. */  const layer = (y: number) => grid.slice(y * LAYER, (y + 1) * LAYER);  /** The layers with no gaps, from the bottom. */  const fullLayers = () =>    Array.from({ length: HEIGHT }, (_, y) => y).filter((y) =>      layer(y).every((id) => id !== undefined),    );  /** Whether `cell` lies inside the walls, on or above the floor, and holds no block. */  const free = ([x, y, z]: Cell) =>    inside(x, z) && y >= 0 && (y >= HEIGHT || grid[at(x, y, z)] === undefined);  /** The falling piece: its kind, its cells around its origin, and their blocks. */  const piece = { kind: 0, cells: [] as Cell[], origin: [0, 0, 0] as Cell, ids: [] as BlockId[] };  const state = { score: 0, lines: 0, level: 1 };  /** Drops the piece a cell a step, at the level's rate. */  const gravity = clock.fixedStep({ step: dropStep(1), maxSteps: 1 });  // Until the game spawns the first piece  gravity.paused = true;  const startLevel = (level: number) => {    state.level = level;    gravity.step = dropStep(level);    gravity.reset();  };  /** `cell` of a piece at `origin`, in the well. */  const world = ([x, y, z]: Cell, [ox, oy, oz] = piece.origin): Cell => [x + ox, y + oy, z + oz];  const fits = (cells: readonly Cell[], origin: Cell) =>    cells.every((cell) => free(world(cell, origin)));  const landing = (): Cell => {    const [x, y, z] = piece.origin;    let floor = y;    while (fits(piece.cells, [x, floor - 1, z])) floor -= 1;    return [x, floor, z];  };  /** Eases piece blocks toward their target cells and ghosts toward landing cells. */  const show = () => {    const below = landing();    piece.cells.forEach((cell, i) => {      blocks.move(piece.ids[i]!, world(cell));      blocks.ghost(i, piece.kind, world(cell, below));    });  };  /** Tries cells and origin, easing blocks to the target if they fit. */  const attempt = (cells: Cell[], origin: Cell) => {    if (!fits(cells, origin)) return false;    piece.cells = cells;    piece.origin = origin;    show();    return true;  };  /** Starts a new piece above the well. Returns false when it has no room, ending the game. */  const spawn = () => {    piece.kind = Math.floor(Math.random() * SHAPES.length);    piece.cells = SHAPES[piece.kind]!.map((cell): Cell => [...cell]);    piece.origin = [...SPAWN];    if (!fits(piece.cells, piece.origin)) return false;    const above: Cell = [piece.origin[0], HEIGHT + 1, piece.origin[2]];    piece.ids = piece.cells.map((cell) => blocks.add(piece.kind, world(cell, above)));    for (const id of piece.ids) blocks.fall(id, true);    show();    gravity.paused = false;    return true;  };  /** Sets the piece into the grid where it is and stops its fall. */  const lock = () => {    piece.cells.forEach((cell, i) => {      const [x, y, z] = world(cell);      grid[at(x, y, z)] = piece.ids[i]!;      blocks.fall(piece.ids[i]!, false);    });    gravity.paused = true;  };  const playing = () => !gravity.paused;  /** Resumes once `lock` stops the falling piece. */  const landed = coroutines.wait({ ready: () => !playing() });  const flashed = coroutines.seconds(CLEAR_TIME);  const cascaded = coroutines.seconds(CASCADE);  /** Flashes and removes full layers, then drops blocks above them. */  function* clear(layers: readonly number[]) {    for (const y of layers) {      for (const id of layer(y)) blocks.flash(id!);    }    yield* flashed;    let removed = 0;    for (let y = 0; y < HEIGHT; y++) {      const cleared = layers.includes(y);      for (let z = 0; z < DEPTH; z++) {        for (let x = 0; x < WIDTH; x++) {          const id = grid[at(x, y, z)];          if (id === undefined) continue;          grid[at(x, y, z)] = undefined;          if (cleared) {            onBreak?.(x, y, z, blocks.kindOf(id));            blocks.remove(id);          } else {            grid[at(x, y - removed, z)] = id;            blocks.move(id, [x, y - removed, z]);          }        }      }      if (cleared) removed += 1;    }    state.lines += layers.length;    state.score += POINTS[layers.length]! * state.level;    const level = 1 + Math.floor(state.lines / LINES_PER_LEVEL);    if (level !== state.level) startLevel(level);    onScore(state);  }  /** Flashes the well layer by layer from the top, then empties it and resets the score. */  function* gameOver() {    for (let y = HEIGHT - 1; y >= 0; y--) {      for (const id of layer(y)) if (id !== undefined) blocks.flash(id);      yield* cascaded;    }    yield* flashed;    for (const id of grid) if (id !== undefined) blocks.remove(id);    grid.fill(undefined);    Object.assign(state, { score: 0, lines: 0 });    startLevel(1);    onScore(state);  }  /** Plays game after game: drops pieces and clears the layers they fill until one has no room. */  function* play() {    for (;;) {      while (spawn()) {        yield* landed;        const full = fullLayers();        if (full.length > 0) yield* clear(full);      }      yield* gameOver();    }  }  scheduler.start(play());  onScore(state);  return {    /** Advances the drop clock once per frame. Each drop moves the piece one cell. */    update(frame: Frame) {      for (let steps = gravity.advance(frame.dt); steps > 0; steps--) {        const [x, y, z] = piece.origin;        if (!attempt(piece.cells, [x, y - 1, z])) lock();      }    },    move(dx: number, dz: number) {      if (!playing()) return;      const [x, y, z] = piece.origin;      attempt(piece.cells, [x + dx, y, z + dz]);    },    /** A quarter turn around `axis` (0 x, 1 y, 2 z); `turn` is 1 or -1. */    rotate(axis: number, turn: number) {      if (!playing()) return;      const cells = piece.cells.map((cell) => rotate(cell, axis, turn));      KICKS.some((kick) => attempt(cells, world(kick)));    },    drop() {      if (!playing()) return;      attempt(piece.cells, landing());      lock();    },  };}

blocks.ts

import { instances, materials, meshPool, shading, type Frame, type Inject } from '@typegpu/kit';import { meshes } from '@typegpu/kit/geometry';import { srgbToLinear } from '@typegpu/color';import { d, std, tgpu, type TgpuRoot, type TgpuUniform } from 'typegpu';/** Seconds a move takes on screen. */const MOVE_TIME = 0.08;/** Seconds a cleared block flashes before it goes. */export const CLEAR_TIME = 0.3;/** Each kind's pigment, linear: vermilion, amber, saffron, jade, turquoise, lapis and amethyst. */export const PALETTE = [  d.vec3f(0.85, 0.22, 0.14),  d.vec3f(0.95, 0.5, 0.12),  d.vec3f(0.95, 0.78, 0.2),  d.vec3f(0.18, 0.62, 0.36),  d.vec3f(0.08, 0.6, 0.7),  d.vec3f(0.2, 0.32, 0.85),  d.vec3f(0.55, 0.25, 0.8),].map((color) => srgbToLinear(color));/** Edge length slightly below cell size, leaving gaps between blocks. */const SIZE = 0.94;/** The radius of a block's bevel, in cells. */const BEVEL = 0.035;/** Seconds after blocks start to change until they hold still: a flash, plus a frame or two. */const SETTLE = Math.max(MOVE_TIME, CLEAR_TIME) + 0.05;/** Final flash brightness exceeds white so cleared blocks bloom. */const FLASH = 6;/** A ghost's opacity, which also tells its material from a block's. */const GHOST_OPACITY = 0.3;/** Initial ghost positions below the well, hidden until a piece moves them. */const HIDDEN: Cell = [0, -2, 0];/** Frame-clock time in seconds. */const clock = tgpu.accessor(d.f32);/** * Movement progress from 0 to 1 after `since` seconds. The CPU also uses it to resume motion * from the visible position. */const eased = (since: number) => {  'use gpu';  return std.smoothstep(0, 1, since / MOVE_TIME);};/** Flash progress from 0 to 1. */const flashed = (clearedAt: number) => {  'use gpu';  if (clearedAt < 0) return d.f32(0);  return std.saturate((clock.$ - clearedAt) / CLEAR_TIME);};/** How much of its colour a block gives off, settled and falling, so the piece in play stands out. */const LIFT = { settled: 0, falling: 0.08 };/** * The paint: its roughness and how far it varies from block to block, how much pale stone mutes * the pigment, how much lighter or darker a block's coat may be, and how far the chamfers wear * through to the stone. */const PAINT = { roughness: 0.8, spread: 0.12, chalk: 0.08, shade: 0.14, worn: 0.5 };/** The pale limestone the blocks are cut from. */export const LIMESTONE = srgbToLinear(d.vec3f(0.76, 0.71, 0.62));/** Chalky pigment of `color`, a shade lighter or darker by `seed`, from 0 to 1. */export const pigmentOf = (color: d.v3f, seed: number) => {  'use gpu';  return std.mix(color, LIMESTONE, PAINT.chalk) * (1 + (seed - 0.5) * PAINT.shade);};/** The paint's roughness, varied by `seed`, from 0 to 1. */export const roughnessOf = (seed: number) => {  'use gpu';  return PAINT.roughness + (seed - 0.5) * PAINT.spread;};/** * The brass inlaid in the plinth: its colour, how strongly it shows, its glow, and its line * half-width in cells. */const BRASS = {  color: srgbToLinear(d.vec3f(0.85, 0.66, 0.4)),  strength: 0.8,  glow: 0.08,  width: 0.012,};/** The fewest pixels across a line is drawn; a line any thinner fades instead. */const LINE_PIXELS = 1.5;/** How many pixels either side of a line's edge its coverage softens over. */const LINE_SOFTNESS = 0.75;/** A ghost's fill, as a share of its paint, and the glow of its outline. */const GHOST = { fill: 0.5, outline: 2.4 };/** * How much of a line of half-width `width` covers a point `distance` from its middle, where a * `pixel` spans that many of the same units, as Ben Golus's pristine grid draws it. Its edges * soften over `LINE_SOFTNESS` pixels either side, and a line thinner than `LINE_PIXELS` is drawn * that wide and fades by how much thinner it is. * * A bright line on a dark floor blends in linear light, where a pixel half covered looks much * more than half as bright. Squaring the coverage evens that out, so a shallow line keeps one * weight as it crosses rows of pixels instead of swelling where it straddles two. */const line = (distance: number, width: number, pixel: number) => {  'use gpu';  const half = width / pixel;  const drawn = std.max(half, LINE_PIXELS / 2);  const away = std.abs(distance) / pixel;  const covered = 1 - std.smoothstep(drawn - LINE_SOFTNESS, drawn + LINE_SOFTNESS, away);  return covered * covered * (half / drawn);};/** How much of a line covers a point `over` past its end, softened as `line` softens its edges. */const capped = (over: number, width: number, pixel: number) => {  'use gpu';  const drawn = std.max(width / pixel, LINE_PIXELS / 2);  return 1 - std.smoothstep(drawn - LINE_SOFTNESS, drawn + LINE_SOFTNESS, over / pixel);};/** * Painted limestone. Eases from `origin` to the target over `MOVE_TIME`, starting at `movedAt`, * and shrinks over `CLEAR_TIME` from `clearedAt`, negative before clearing, by the frame clock. * The paint wears through on the chamfers, and the piece in play gives off a little of its colour. * A ghost is a faint fill with a glowing outline. A floor, whose `grid` holds the well's half * width and depth, is dark polished stone with the well's cells inlaid in brass. */const Block = instances.define('Block', {  host: {    origin: d.vec3f,    movedAt: d.f32,    clearedAt: d.f32,    falling: d.f32,    grid: d.vec2f,    seed: d.f32,  },  deform: (vertex, block) => {    'use gpu';    const at = std.mix(block.origin, block.position, eased(clock.$ - block.movedAt));    const cleared = flashed(block.clearedAt);    // Divide world displacement by scale because deformation uses local block units    return vertex.position * (1 - cleared * cleared) + (at - block.position) / block.scale;  },  carry: (vertexIndex) => {    'use gpu';    return d.vec4f(meshPool.positionAt(vertexIndex), 0);  },  material: (point, block, base, carried) => {    'use gpu';    const p = point.position;    const n = std.abs(point.normal);    // Derivatives run before any branch, in uniform control flow. They come from positions,    // since the distances below jump between lines and where faces meet    const across = d.vec2f(      std.length(d.vec2f(std.dpdx(p.x), std.dpdy(p.x))),      std.length(d.vec2f(std.dpdx(p.z), std.dpdy(p.z))),    );    const pixel = std.max(across, d.vec2f(1e-5));    const span = std.max(std.length(std.fwidth(carried.xyz)) * 0.7, 1e-5);    // Each line of the grid is a segment across the well, ending in a soft cap at its border    const rel = p.xz - block.position.xz;    const nearest = std.round(std.clamp(rel, block.grid * -1, block.grid));    const off = rel - nearest;    const over = std.max(std.abs(rel) - block.grid, d.vec2f(0));    const grid = std.max(      line(off.x, BRASS.width, pixel.x) * capped(over.y, BRASS.width, pixel.y),      line(off.y, BRASS.width, pixel.y) * capped(over.x, BRASS.width, pixel.x),    );    const local = std.abs(carried.xyz);    const face = std.select(      std.select(local.xy, local.xz, n.y > n.z),      local.zy,      n.x > n.y && n.x > n.z,    );    const outline = line(std.max(face.x, face.y) - 0.5 + 0.035, 0.03, span);    if (block.grid.x > 0) {      const gold = std.select(d.f32(0), grid * BRASS.strength, point.normal.y > 0.5);      return shading.Material({        color: std.mix(base.color, BRASS.color, gold),        roughness: std.mix(base.roughness, 0.3, gold),        emission: BRASS.color * (BRASS.glow * gold),        metallic: gold,        occlusion: 1,      });    }    // Chalky pigment, a shade lighter or darker on each block. The chamfer is where the normal    // leaves its face's axis, since blocks never turn, and there the paint wears to the stone    const pigment = pigmentOf(base.color, block.seed);    const chamfer = std.smoothstep(0.04, 0.25, 1 - std.max(std.max(n.x, n.y), n.z));    const color = std.mix(pigment, LIMESTONE, chamfer * PAINT.worn);    if (block.opacity < 0.99) {      const lit = materials.matte(color * GHOST.fill);      lit.emission = base.color * (GHOST.outline * outline);      return lit;    }    const lit = materials.glossy(color, roughnessOf(block.seed));    const lift = std.mix(LIFT.settled, LIFT.falling, block.falling);    const flash = FLASH * flashed(block.clearedAt);    lit.emission = base.color * (lift + flash * 0.4) + d.vec3f(flash * 0.2);    return lit;  },});export type Cell = readonly [x: number, y: number, z: number];type Triple = [number, number, number];type BlockSet = instances.Instances<typeof Block>;/** Block handle. Invalid after removal. */export type BlockId = instances.Id<'Block'>;/** CPU row state for resuming motion from the visible position. */interface Row {  readonly set: BlockSet;  readonly id: BlockId;  kind: number;  /** Origin, target and last requested cell centres. Motion starts at `movedAt`. */  readonly origin: Triple;  readonly to: Triple;  readonly goal: Triple;  movedAt: number;  /** Flash start time. -1 means inactive, and NaN requests a start on the next update. */  clearedAt: number;}export interface BlocksOptions {  /** Maximum solid-block count. */  readonly capacity: number;  /** One ghost block per piece cube. */  readonly ghosts: number;  /** Frame-clock time in seconds. */  readonly time: TgpuUniform<d.F32>;  /** A slab drawn with the solid blocks, by its centre and size. */  readonly plinth: { readonly at: Cell; readonly size: Cell; readonly color: d.v3f };  /** The well's width and depth, which the plinth's grid covers, centred on the plinth. */  readonly grid: readonly [width: number, depth: number];  /** The scenery's meshes, added to the same pool. */  readonly scenery?: readonly meshes.Geometry[];}/** * Draws carved solid blocks, their plinth and landing ghosts as rounded-box instances. The GPU * eases blocks between cells. Row writes occur only when motion or a flash starts. */export function createBlocks(root: TgpuRoot, options: BlocksOptions) {  const { capacity } = options;  const shape = meshes.roundedBox({ radius: BEVEL });  const [width, height, depth] = options.plinth.size;  const slab = meshes.roundedBox({ width, height, depth, radius: 0.12 });  const scenery = options.scenery ?? [];  const pool = meshPool.create(root, meshPool.roomFor([shape, slab, ...scenery]));  const cube = pool.add(shape);  const plinth = pool.add(slab);  const sceneryMeshes = scenery.map((mesh) => pool.add(mesh));  const solid = instances.create(root, pool, { kind: Block, capacity: capacity + 1 });  const ghosts = instances.create(root, pool, { kind: Block, capacity: options.ghosts });  const timed: Inject = (cfg) => cfg.with(clock, options.time);  const solidRenderer = solid.renderer().pipe(timed);  const ghostRenderer = ghosts.renderer({ blend: 'translucent' }).pipe(timed);  const rows = new Map<BlockId, Row>();  const starting = new Set<Row>();  /** When solid blocks last started to change, and whether one came or went since `update`. */  let stirredAt = -Infinity;  let stirred = false;  const written = {    position: [0, 0, 0] as Triple,    origin: [0, 0, 0] as Triple,    movedAt: 0,    clearedAt: -1,  };  const write = (row: Row) => {    written.position = row.to;    written.origin = row.origin;    written.movedAt = row.movedAt;    written.clearedAt = row.clearedAt;    row.set.write(row.id, written);  };  let placed = 0;  const placeBlock = (set: BlockSet, kind: number, cell: Cell, opacity = 1): Row => {    const centre: Triple = [cell[0] + 0.5, cell[1] + 0.5, cell[2] + 0.5];    const id = set.add(cube, {      position: centre,      scale: SIZE,      ...materials.matte(PALETTE[kind]!),      // Golden-ratio steps spread the coats' shades evenly      seed: (placed++ * 0.618034) % 1,      opacity,      origin: centre,      movedAt: 0,      clearedAt: -1,    });    return {      set,      id,      kind,      origin: [...centre],      to: [...centre],      goal: [...centre],      movedAt: 0,      clearedAt: -1,    };  };  /** Sets the row's goal to `cell`, so the next `update` starts it easing there. */  const ease = (row: Row, cell: Cell) => {    for (let axis = 0; axis < 3; axis++) row.goal[axis] = cell[axis]! + 0.5;    starting.add(row);  };  /** Starts easing the row from its visible position to its goal at `time`. */  const restart = (row: Row, time: number) => {    const moved = eased(time - row.movedAt);    for (let axis = 0; axis < 3; axis++) {      row.origin[axis] = row.origin[axis]! + (row.to[axis]! - row.origin[axis]!) * moved;      row.to[axis] = row.goal[axis]!;    }    row.movedAt = time;  };  /** Whether the requested cell differs from the current motion target. */  const redirected = (row: Row) =>    row.goal[0] !== row.to[0] || row.goal[1] !== row.to[1] || row.goal[2] !== row.to[2];  const at = [...options.plinth.at] as Triple;  solid.add(plinth, {    position: at,    ...materials.glossy(options.plinth.color, 0.4),    origin: at,    movedAt: 0,    clearedAt: -1,    grid: [options.grid[0] / 2, options.grid[1] / 2],  });  const ghostRows = Array.from({ length: options.ghosts }, () =>    placeBlock(ghosts, 0, HIDDEN, GHOST_OPACITY),  );  const destroy = () => {    solid.destroy();    ghosts.destroy();    pool.destroy();  };  return {    /** Adds a block of `kind` at `cell` without easing. */    add(kind: number, cell: Cell) {      const row = placeBlock(solid, kind, cell);      rows.set(row.id, row);      stirred = true;      return row.id;    },    /** Eases a block from its visible position to `cell`. */    move(id: BlockId, cell: Cell) {      ease(rows.get(id)!, cell);    },    /** Starts a clear flash lasting `CLEAR_TIME` seconds. */    flash(id: BlockId) {      const row = rows.get(id)!;      row.clearedAt = Number.NaN;      starting.add(row);    },    kindOf: (id: BlockId) => rows.get(id)!.kind,    /** Marks a block as part of the falling piece, which shines brightest. */    fall(id: BlockId, falling: boolean) {      solid.write(id, { falling: falling ? 1 : 0 });    },    remove(id: BlockId) {      stirred = true;      starting.delete(rows.get(id)!);      rows.delete(id);      solid.remove(id);    },    /** Eases ghost `i` to `cell`, in the colour of `kind`. */    ghost(i: number, kind: number, cell: Cell) {      const row = ghostRows[i]!;      if (row.kind !== kind) ghosts.write(row.id, { color: PALETTE[kind]! });      row.kind = kind;      ease(row, cell);    },    /**     * Whether no solid block has moved, flashed, come or gone since a little over `CLEAR_TIME`     * before `time`, so shadows drawn since then still hold.     */    still: (time: number) => time - stirredAt > SETTLE,    /** Writes rows starting motion or flashes, then flushes solid and ghost instances. */    update(frame: Frame) {      if (stirred || starting.size > 0) stirredAt = frame.time;      stirred = false;      for (const row of starting) {        if (redirected(row)) restart(row, frame.time);        if (Number.isNaN(row.clearedAt)) row.clearedAt = frame.time;        write(row);      }      starting.clear();      solid.flush();      ghosts.flush();    },    pool,    sceneryMeshes,    solid: solidRenderer,    /** Add ghosts after solid blocks so they blend over them. */    ghosts: ghostRenderer,    destroy,    [Symbol.dispose]: destroy,  };}export type Blocks = ReturnType<typeof createBlocks>;

shards.ts

import {  instances,  materials,  meshPool,  particles,  type CameraBase,  type Inject,} from '@typegpu/kit';import { d, std, tgpu, type TgpuComputePass, type TgpuRoot, type TgpuUniform } from 'typegpu';import { LIMESTONE, PALETTE, pigmentOf, roughnessOf } from './blocks.ts';import { DEPTH, LAYER, PIECE, WIDTH } from './game.ts';/** Chips a block breaks into. */const CHIPS = 4;/** Downward acceleration of a chip, in cells per second squared. */const GRAVITY = 14;/** How far a chip keeps from the well's walls, in cells. */const MARGIN = 0.12;/** A chip's size range along its three axes, in cells. */const CHIP = { small: [0.2, 0.08, 0.16], large: [0.5, 0.22, 0.42] } as const;/** A chip's life range in seconds; it shrinks away over the last `FADE` of it. */const LIFE = [0.9, 1.4] as const;const FADE = 0.35;/** Seconds a landed chip takes to tip onto its nearest face. */const TIP = 0.1;/** The sandstone dust a block leaves where it stood. */const SAND = [0.3, 0.26, 0.22, 0.2] as const;/** Frame-clock time in seconds. */const clock = tgpu.accessor(d.f32);/** The unit quaternion turning by `angle` about the unit `axis`. */const turn = (axis: d.v3f, angle: number) => {  'use gpu';  return d.vec4f(axis * std.sin(angle / 2), std.cos(angle / 2));};/** The rotation `a` after `b`, of unit quaternions. */const after = (a: d.v4f, b: d.v4f) => {  'use gpu';  return d.vec4f(    b.xyz * a.w + a.xyz * b.w + std.cross(a.xyz, b.xyz),    a.w * b.w - std.dot(a.xyz, b.xyz),  );};/** The heights of the unit axes `rotation` turns: how far each points up. */const heightsOf = (rotation: d.v4f) => {  'use gpu';  const q = rotation;  return d.vec3f(    2 * (q.x * q.y + q.w * q.z),    1 - 2 * (q.x * q.x + q.z * q.z),    2 * (q.y * q.z - q.w * q.x),  );};/** Seconds until something thrown up at `rise` lands `drop` below where it started. */const landing = (drop: number, rise: number) => {  'use gpu';  const below = std.max(drop, 0);  return (rise + std.sqrt(rise * rise + 2 * GRAVITY * below)) / GRAVITY;};/** How far a box of size `scale`, turned by `rotation`, reaches below its centre. */const reachOf = (rotation: d.v4f, scale: d.v3f) => {  'use gpu';  return std.dot(std.abs(heightsOf(rotation)), scale) * 0.5;};/** * The least turn after `rotation` that stands the axis nearest the vertical upright, so the box * lies on a face, turned about the vertical as it was. */const flattened = (rotation: d.v4f) => {  'use gpu';  const height = std.abs(heightsOf(rotation));  const axis = std.select(    std.select(d.vec3f(0, 0, 1), d.vec3f(0, 1, 0), height.y >= height.z),    d.vec3f(1, 0, 0),    height.x >= height.y && height.x >= height.z,  );  const tilted = instances.rotate(rotation, axis);  const upright = d.vec3f(0, std.sign(tilted.y), 0);  const level = std.normalize(d.vec4f(std.cross(tilted, upright), 1 + std.dot(tilted, upright)));  return after(level, rotation);};/** * A chip of a broken block: painted on the block's outer faces, raw limestone where it broke * away. It flies from `position` at `velocity`, spinning about `spin.xyz` at `spin.w` radians a * second, lands on the floor at `rest`, tips onto a face over `TIP` seconds, and shrinks away by * `bornAt + life`. */const Chip = instances.define('Chip', {  host: { velocity: d.vec3f, spin: d.vec4f, bornAt: d.f32, life: d.f32, rest: d.f32, seed: d.f32 },  place: (chip) => {    'use gpu';    const age = clock.$ - chip.bornAt;    const rise = chip.velocity.y;    const phase = chip.seed * 6.283;    // It lands about when its lowest corner meets the floor: first guessed from its smallest    // half extent, then from how it is turned then    const smallest = std.min(std.min(chip.scale.x, chip.scale.y), chip.scale.z) * 0.5;    const guess = landing(chip.position.y - chip.rest - smallest, rise);    const guessed = turn(chip.spin.xyz, phase + chip.spin.w * guess);    const landed = landing(chip.position.y - chip.rest - reachOf(guessed, chip.scale), rise);    const flying = std.min(age, landed);    const touched = turn(chip.spin.xyz, phase + chip.spin.w * landed);    const tipped = std.smoothstep(0, TIP, age - landed);    const rotation = std.normalize(std.mix(touched, flattened(touched), tipped));    const turning = std.select(      rotation,      turn(chip.spin.xyz, phase + chip.spin.w * flying),      age < landed,    );    const size = std.select(std.saturate((chip.life - age) / (chip.life * FADE)), 0, age < 0);    // It rests on its lowest corner as it tips and shrinks away    const resting = chip.rest + reachOf(turning, chip.scale) * size;    const fallen = chip.position.y + rise * flying - 0.5 * GRAVITY * flying * flying;    return instances.Placement({      rotation: turning,      position: d.vec3f(        std.clamp(chip.position.x + chip.velocity.x * flying, MARGIN, WIDTH - MARGIN),        std.select(std.max(fallen, resting), resting, age >= landed),        std.clamp(chip.position.z + chip.velocity.z * flying, MARGIN, DEPTH - MARGIN),      ),      scale: chip.scale * size,    });  },  visible: (chip) => {    'use gpu';    const age = clock.$ - chip.bornAt;    return age >= 0 && age < chip.life;  },  carry: (vertexIndex) => {    'use gpu';    return d.vec4f(meshPool.vertexAt(vertexIndex).normal, 0);  },  material: (_point, chip, base, carried) => {    'use gpu';    const broken = carried.y < -0.5 || carried.x < -0.5;    const color = std.select(pigmentOf(base.color, chip.seed), LIMESTONE * 0.8, broken);    return materials.glossy(color, roughnessOf(chip.seed));  },});const random = (low: number, high: number) => low + Math.random() * (high - low);/** Lays a dust patch flat on the floor, turned and stretched by its seed. */const lying = (dust: particles.Particle, corner: d.v2f) => {  'use gpu';  const angle = dust.seed * 6.283;  const along = d.vec3f(std.cos(angle), 0, std.sin(angle));  const across = d.vec3f(-along.z, 0, along.x);  const stretch = 1.2 + std.fract(dust.seed * 7.31) * 0.6;  return dust.position + (along * (corner.x * stretch) + across * corner.y) * (dust.size * 0.5);};/** Shades a dust patch with a ragged edge that spreads and thins with its age. */const ragged = (dust: particles.Particle, local: d.v2f) => {  'use gpu';  const aged = dust.age / dust.life;  const angle = std.atan2(local.y, local.x);  const edge =    1 + 0.25 * std.sin(angle * 3 + dust.seed * 40) + 0.12 * std.sin(angle * 5 + dust.seed * 17);  const reach = std.length(local) / (edge * (0.6 + 0.4 * std.sqrt(aged)));  const body = std.saturate(1 - reach);  const alpha = dust.color.w * body * body * (1 - aged) * std.smoothstep(0, 0.1, aged);  return d.vec4f(dust.color.xyz * alpha, alpha);};export interface ShardsOptions {  /** The length of a step in seconds; see `particles.Options.step`. */  readonly step: number;  readonly camera: CameraBase;  /** Frame-clock time in seconds. */  readonly time: TgpuUniform<d.F32>;  /** The pool holding `chip`. */  readonly pool: meshPool.MeshPool;  /** A unit box, which a chip stretches. */  readonly chip: meshPool.Mesh;}/** Breaks cleared blocks into painted stone chips and a little sandstone dust. */export function createShards(root: TgpuRoot, options: ShardsOptions) {  const { camera, step } = options;  // One piece can clear at most one layer per cube, and all cleared blocks break together  const capacity = CHIPS * PIECE * LAYER;  const chips = instances.create(root, options.pool, { kind: Chip, capacity });  const timed: Inject = (cfg) => cfg.with(clock, options.time);  const ids = Array.from({ length: capacity }, () =>    chips.add(options.chip, { ...materials.matte(PALETTE[0]!), bornAt: -1e6, life: 1 }),  );  let next = 0;  let brokeAt = -Infinity;  const dust = particles.create(root, camera, {    step,    capacity: PIECE * LAYER,    blend: 'over',    bursts: PIECE * LAYER,    place: lying,    shade: ragged,    settings: { gravity: [0, 0, 0], drag: 3 },  });  const chip = {    position: [0, 0, 0] as [number, number, number],    velocity: [0, 0, 0] as [number, number, number],    spin: [0, 0, 0, 0] as [number, number, number, number],    scale: [0, 0, 0] as [number, number, number],    color: PALETTE[0]!,    bornAt: 0,    life: 0,    rest: 0,    seed: 0,  };  const destroy = () => {    chips.destroy();    dust.destroy();  };  return {    chips: chips.renderer().pipe(timed),    /** Whether any chip is still moving or shrinking at `time`, so shadows must follow it. */    flying: (time: number) => time - brokeAt < LIFE[1],    drawings: [dust.drawing],    /** Breaks the block at cell `x, y, z`, of kind `kind`, at `time`. */    break(x: number, y: number, z: number, kind: number, time: number) {      chip.color = PALETTE[kind]!;      chip.bornAt = time;      brokeAt = time;      chip.rest = y;      for (let i = 0; i < CHIPS; i++) {        const out = [random(-0.35, 0.35), random(-0.35, 0.35), random(-0.35, 0.35)];        for (let axis = 0; axis < 3; axis++) {          chip.position[axis] = [x, y, z][axis]! + 0.5 + out[axis]!;          chip.scale[axis] = random(CHIP.small[axis]!, CHIP.large[axis]!);        }        chip.velocity[0] = out[0]! * 4 + random(-0.6, 0.6);        chip.velocity[1] = random(2, 4.5);        chip.velocity[2] = out[2]! * 4 + random(-0.6, 0.6);        const axis = [random(-1, 1), random(-1, 1), random(-1, 1)];        const length = Math.hypot(...axis) || 1;        chip.spin = [axis[0]! / length, axis[1]! / length, axis[2]! / length, random(4, 11)];        chip.life = random(LIFE[0], LIFE[1]);        chip.seed = Math.random();        chips.write(ids[next]!, chip);        next = (next + 1) % capacity;      }      dust.burst({        count: 1,        position: [x + 0.5, y + 0.1, z + 0.5],        area: [0.4, 0, 0.4],        scatter: 0.4,        size: 1.1,        life: 1.5,        variance: 0.4,        color: SAND,      });    },    /** Uploads the chips broken since the last frame. */    update() {      chips.flush();    },    step(pass: TgpuComputePass) {      dust.step(pass);    },    spawn(pass: TgpuComputePass) {      dust.spawn(pass);    },    compact(pass: TgpuComputePass, ahead: number) {      dust.compact(pass, ahead);    },    initAsync: () => dust.initAsync(),    destroy,    [Symbol.dispose]: destroy,  };}

controls.ts

import type { Tap } from '../ui.ts';import type { createBlockfall } from './game.ts';const QUARTER = Math.PI / 2;type Direction = readonly [x: number, z: number];type Blockfall = ReturnType<typeof createBlockfall>;/** Rotation axis and turn for tipping a piece along a grid direction. */const axisAlong = ([x, z]: Direction): [axis: number, turn: number] => (x !== 0 ? [0, x] : [2, z]);/** * The game's buttons, as `ui.ts` declares them. Moves follow the camera's goal, so they turn with a * camera turn at once. */export function createControls(game: Blockfall, view: { readonly goal: { yaw: number } }) {  const along = (act: (away: Direction, right: Direction) => void): Tap => ({    press() {      const quarter = Math.round(view.goal.yaw / QUARTER);      const sin = Math.round(Math.sin(quarter * QUARTER));      const cos = Math.round(Math.cos(quarter * QUARTER));      act([-sin, -cos], [cos, -sin]);    },  });  const turn = (by: number): Tap => ({ press: () => (view.goal.yaw += by * QUARTER) });  return {    away: along((away) => game.move(...away)),    near: along(([x, z]) => game.move(-x, -z)),    left: along((_, [x, z]) => game.move(-x, -z)),    right: along((_, right) => game.move(...right)),    spin: { press: () => game.rotate(1, 1) },    roll: along((_, right) => game.rotate(...axisAlong(right))),    tip: along((away) => game.rotate(...axisAlong(away))),    drop: { press: () => game.drop() },    turnLeft: turn(-1),    turnRight: turn(1),  };}

stage.ts

import { environment, lighting, particles, table, type CameraBase } from '@typegpu/kit';import { meshes } from '@typegpu/kit/geometry';import { srgbToLinear } from '@typegpu/color';import { d, std, type TgpuRoot } from 'typegpu';type Vec3 = [number, number, number];export const cube = meshes.box();export const STAGE = {  settings: {    // A cool fill from high behind the well, its rim light and shadow    sunDirection: [0.35, 0.9, -0.75],    sunIntensity: 1.2,    sunColor: [0.55, 0.65, 1],    skyTop: [0.05, 0.065, 0.14],    skyBottom: [0.03, 0.03, 0.045],    skyIntensity: 1,    skyReflection: 1,    fogDensity: 0,  },  plinth: srgbToLinear(d.vec3f(0.09, 0.085, 0.11)),  finish: { exposure: 1.45, vignette: 0.3 },  bloom: { threshold: 1.4 },} as const;/** Gradient colours, from looking straight down to above the horizon. */const DEEP = srgbToLinear(d.vec3f(0.05, 0.06, 0.08));const HIGH = srgbToLinear(d.vec3f(0.13, 0.15, 0.2));/** The dark gradient the well floats in. */const gradient = (direction: d.v3f) => {  'use gpu';  return std.mix(DEEP, HIGH, std.smoothstep(-0.9, 0.5, direction.y));};export const backdrop = environment.background(gradient);/** The warm key, from high at the front left, out of view, and where it aims, low in the well. */const KEY = {  position: [-9, 15, 14] as Vec3,  aim: [3, 3.5, 3] as Vec3,  color: [1, 0.68, 0.42] as Vec3,  intensity: 320,  range: 45,  cone: lighting.cone(0.55, 0.85),};/** Embers drifting up through the space around the well, from fires out of view below. */const EMBERS = {  position: [3, -3, 3],  area: [7, 4, 7],  velocity: [0, 1.1, 0],  scatter: 0.35,  color: [3, 1.1, 0.25, 1],  size: 0.05,  life: 14,  variance: 0.5,} as const;export interface StageOptions {  readonly camera: CameraBase;  /** The length of a step in seconds; see `particles.Options.step`. */  readonly step: number;}/** The well's light, a warm spot out of view, and the embers drifting past it. */export function createStage(root: TgpuRoot, options: StageOptions) {  const lamps = table.create(root, lighting.SpotLight, {    capacity: 1,    defaults: { ...lighting.WHITE_SPOT },  });  const toward = KEY.aim.map((at, axis) => at - KEY.position[axis]!);  const length = Math.hypot(...toward);  const key = lamps.add({    position: KEY.position,    aim: d.vec3f(...(toward.map((v) => v / length) as Vec3)),    cone: KEY.cone,    color: KEY.color,    intensity: KEY.intensity,    range: KEY.range,  });  lamps.flush();  const embers = particles.create(root, options.camera, {    capacity: 128,    step: options.step,    shade: particles.glowingDisc,    label: 'embers',    settings: { gravity: [0, 0, 0], drag: 0 },  });  // The space starts with embers already risen through it, then more drift up from below  embers.burst({ ...EMBERS, count: 48, position: [3, 5, 3], area: [7, 8, 7], aged: true });  embers.stream(4, { ...EMBERS });  const destroy = () => {    lamps.destroy();    embers.destroy();  };  return { lamps, key, embers, destroy };}

well.ts

import { cameras, passes } from '@typegpu/kit';import { meshes } from '@typegpu/kit/geometry';import { srgbToLinear } from '@typegpu/color';import { d, std } from 'typegpu';/** The gold of the well's edges and cell lines, a little brighter than white so the edges glow. */const GOLD = srgbToLinear(d.vec3f(1, 0.78, 0.48)).mul(1.4);/** The faint cool tint of the glass walls. */const GLASS = srgbToLinear(d.vec3f(0.45, 0.55, 0.75));/** The walls' fill, their cell lines and their edges, as opacities. */const OPACITY = { walls: 0.045, lines: 0.11, edges: 0.75 } as const;/** Cell line and edge widths in pixels. */const STROKE = { lines: 1.1, edges: 2.2 } as const;/** The fewest world units a pixel spans in the grid's distances. */const SMALLEST_PIXEL = 1e-6;const cube = meshes.unindexed(meshes.box());const Corner = d.struct({ position: d.vec4f, world: d.vec3f, normal: d.vec3f });const Seen = d.struct({ color: d.vec4f });/** * Draws the well's far glass walls with a faint grid of its cells and gold edges, so its limits * read from any side without standing between the camera and the play. The plinth carries the * floor's grid. Add it after the solid blocks: it blends over them and writes no depth. */export function createWell(  size: readonly [width: number, height: number, depth: number],): passes.Drawing<typeof Corner, d.Infer<typeof Seen>> {  const extent = d.vec3f(...size);  return {    Vertex: Corner,    at: (vertexIndex) => {      'use gpu';      const corner = cube.vertexAt(vertexIndex);      const world = (corner.position + 0.5) * extent;      return Corner({        position: cameras.current.$.viewProjection * d.vec4f(world, 1),        world,        normal: corner.normal,      });    },    surfaceOf: (corner) => {      'use gpu';      const world = corner.world;      // World units per pixel. The axis along the normal is constant across a face, so the      // smallest pixel keeps it from dividing by zero      const pixel = std.max(std.fwidth(world), d.vec3f(SMALLEST_PIXEL));      const across = 1 - std.abs(corner.normal);      // Pixel distance to the cell lines and to the face's edges, along the face's two axes      const cells = std.abs(std.fract(world + 0.5) - 0.5) / pixel;      const reach = across * STROKE.lines - cells;      // The plinth's brass border is the walls' foot, so neither a cell line nor an edge doubles it      const foot = std.select(reach.y, d.f32(-1), world.y < 0.5);      const lines = std.saturate(std.max(std.max(reach.x, foot), reach.z));      // Interpolation can land a hair outside a wall, so distances to the sides start at zero      const sides = std.min(world, extent - world);      const inside = std.max(d.vec3f(sides.x, extent.y - world.y, sides.z), d.vec3f(0));      const rim = across * STROKE.edges - inside / pixel;      const edges = std.saturate(std.max(std.max(rim.x, rim.y), rim.z));      // The plinth carries the floor and the open top has no glass, and the walls fade upward so the pile carries the light      const wall = std.select(d.f32(1), d.f32(0), std.abs(corner.normal.y) > 0.5);      const fade = std.mix(1, 0.35, std.saturate(world.y / extent.y));      const glow = std.max(lines * OPACITY.lines * fade, edges * OPACITY.edges);      const alpha = std.saturate(OPACITY.walls * fade + glow) * wall;      const tint = std.mix(GLASS, GOLD, glow / std.max(alpha, 1e-4));      return Seen({ color: d.vec4f(tint * alpha, alpha) });    },    color: passes.shaded,    translucent: true,    depth: { depthWriteEnabled: false, depthCompare: 'less' },    // Front-face culling shows the inside of the far walls    primitive: { cullMode: 'front' },    draw: (pass) => pass.draw(cube.vertexCount),  };}

ui.ts

import { buttons, text } from '../ui.ts';export const ui = {  score: text({ corner: 'top-right' }),  // Dragging the board orbits it, so the pads leave out the turns  controls: buttons(    {      away: { label: '▲', name: 'away', keys: ['ArrowUp'] },      near: { label: '▼', name: 'near', keys: ['ArrowDown'] },      left: { label: '◀', name: 'left', keys: ['ArrowLeft'] },      right: { label: '▶', name: 'right', keys: ['ArrowRight'] },      spin: { label: 'spin', keys: ['KeyZ'] },      roll: { label: 'roll', keys: ['KeyX'] },      tip: { label: 'tip', keys: ['KeyC'] },      drop: { label: 'drop', keys: ['Space'] },      turnLeft: { label: 'Q', name: 'turn left', keys: ['KeyQ'] },      turnRight: { label: 'E', name: 'turn right', keys: ['KeyE'] },    },    {      left: [        [null, 'away', null],        ['left', 'near', 'right'],      ],      right: [['spin', 'roll', 'tip'], ['drop']],    },  ),};