SLOPSHOPPER

ascii-rest

Play ascii.rest's 209 animated ascii pieces above the prompt: /ascii [piece | list | stop]

newbandcommandtimer
v0.1.0MITupdated 2026-10-08DresvyanskiyDenis/claude-code-ascii-rest
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · ascii-rest
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /ascii ⎿ ascii-rest: Playing cherry-blossom above the prompt; /ascii stop hides it. | `---.___ `-.__ \/ '|' | `--.__ `-.__ - o / (_)(_) `-._ `--.___ / (@) \ ° `---.___/ cherry-blossom: a flowering cherry branch shedding petals on the wind ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
| `---.___ `-.__ \/ '|' | `--.__ `-.__ - o / (_)(_) `-._ `--.___ / (@) \ ° `---.___/ cherry-blossom: a flowering cherry branch shedding petals on the wind
README

ascii-rest for Claude Code

A Claude Code mod that plays the 209 animated pieces from ascii.rest in a band above the prompt, beside whatever else you have open.

Install

At the prompt of a Claude Code terminal session:

/plugin install ascii-rest --marketplace DresvyanskiyDenis/claude-code-ascii-rest

Answer y to add the marketplace, then pick a scope: the user scope makes it available in every session.

Use

CommandWhat it does
/asciiplays a random piece
/ascii night-coastplays a piece by name
/ascii listlists every piece by category
/ascii stophides the band

Terminals

Character pieces (campfire, doom-fire, radar, …) play in any terminal.

The 15 scenes (alpine-dawn, aurora-fjord, deep-reef, desert-night, earthrise, kyoto-dusk, lantern-lake, marine-drive, misty-forest, night-coast, ocean-sunset, storm-plains, taj-dawn, tokyo-rain, varanasi-ghats) are pictures made of dots. They play as images in terminals with kitty graphics Unicode placeholders (Ghostty, kitty), and as coloured half blocks everywhere else.

  • herdr draws the images but does not answer Claude Code's graphics query. Tell Claude Code to draw them anyway, for herdr panes only (in ~/.zshrc):
  [[ -n $HERDR_PANE_ID ]] && export CLAUDE_CODE_FORCE_TERMINAL_IMAGES=1
  • tmux passes no images through; scenes play as half blocks.

Built and tested on Claude Code 2.1.294.

Develop

claude --plugin-dir .          # run it from this folder
claude plugin test .           # tests/ascii.test.ts
claude plugin validate .

hooks/png.ts is a small PNG encoder: the plugin sandbox has no zlib, and Claude Code moves image bytes to the terminal at about 1 MB/s, so a scene frame goes as a 12–25 KB PNG rather than 700 KB of raw pixels.

Credits

The pieces are ascii.rest by bas3line (MIT), vendored unchanged under hooks/vendor/; hooks/painter.ts is adapted from its src/terminal.ts. See hooks/vendor/SOURCE.txt and hooks/vendor/LICENSE.

License

MIT

Source 91 files
hooks/register.tsx 190 lines
1import { atom, read, update } from 'claude-code'
2import type { Register, Timer } from 'claude-code'
3
4import { dots } from './dots.ts'
5import { dark, painter } from './painter.ts'
6import * as pieces from './vendor/pieces/index.ts'
7import type { Frame, Piece } from './vendor/types.ts'
8
9const ART = 'art'
10// Raster's colour for "the terminal's own"
11const DEFAULT = 0x01000000
12// ponytail: 3 px a dot, 600 x 300 and a 12 to 25 KB PNG a frame for a 200 x 100 scene; raise it for a sharper picture if frames keep up
13const SCALE = 3
14
15const playing = atom({ plugin: 'ascii-rest', key: 'playing' } as const, null)
16
17type Cells = ReturnType<typeof painter>
18// A scene plays as an Image of its dots, any other piece as a Raster of its characters.
19type Art = { pic: ReturnType<typeof dots>; color: Uint8Array } | { cells: Cells }
20// Its own timer, so a tick left over from the last piece can only end itself.
21type Player = { name: string; frame: Frame; paper: boolean; fps: number; start: number; art: Art; w: number; h: number; tick?: Timer; busy: boolean; denied: number }
22
23// "nightCoast" -> "night-coast", the names ascii.rest's site and CLI use
24export const byName = new Map(
25  Object.entries(pieces).map(([key, piece]) => [key.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`), piece as Piece]),
26)
27
28/** The middle `w` x `h` of the painted cells, cropped or padded with the ground, as Raster's base64 `[codePoint, fg, bg]` u32 triplets. */
29export function encode(cells: Cells, w: number, h: number): string {
30  const words = new Uint32Array(w * h * 3)
31  const ground = cells.base < 0 ? DEFAULT : cells.base
32  const sx = (cells.cols - w) >> 1, sy = (cells.rows - h) >> 1
33  for (let y = 0, k = 0; y < h; y++)
34    for (let x = 0; x < w; x++, k += 3) {
35      const cx = sx + x, cy = sy + y
36      if (cx < 0 || cx >= cells.cols || cy < 0 || cy >= cells.rows) {
37        words[k] = 32
38        words[k + 1] = DEFAULT
39        words[k + 2] = ground
40        continue
41      }
42      const i = cy * cells.cols + cx
43      words[k] = cells.ch[i].charCodeAt(0)
44      words[k + 1] = cells.fg[i] < 0 ? DEFAULT : cells.fg[i]
45      words[k + 2] = cells.bg[i] < 0 ? DEFAULT : cells.bg[i]
46    }
47  return new Uint8Array(words.buffer).toBase64()
48}
49
50const list = () => {
51  const groups = new Map<string, string[]>()
52  for (const [name, { meta }] of byName) groups.set(meta.category, [...(groups.get(meta.category) ?? []), name])
53  return [...groups].map(([category, names]) => `${category}: ${names.join(', ')}`).join('\n')
54}
55
56export const register: Register = on => {
57  // Module variables: a reload starts them over, and the next draw of the band starts the piece again.
58  let player: Player | undefined
59  // A scene is a picture until the engine says this terminal draws none (herdr, tmux), then half blocks.
60  let pictures = true
61
62  // Time from the clock, not counted in ticks: a late tick skips a frame instead of slowing the piece down.
63  const paint = (p: Player, now: number) => {
64    const t = (now - p.start) / 1000
65    if ('pic' in p.art) p.art.pic.paint(p.frame(t, { paper: p.paper, color: p.art.color }), p.art.color)
66    else p.art.cells.paint(p.frame(t, { paper: p.paper, color: p.art.cells.color }))
67  }
68  const stop = () => {
69    player?.tick?.cancel()
70    player = undefined
71  }
72
73  on('session.start', async ($, e, next) => {
74    await $.command.register({
75      name: 'ascii',
76      description: 'Play an ascii.rest animation above the prompt: /ascii [piece | list | stop], random when empty',
77    })
78
79    return next(e)
80  })
81
82  on('command.run', { command: 'ascii' }, async ($, e) => {
83    const arg = e.args.trim().toLowerCase()
84    if (arg === 'list') return { text: list() }
85
86    stop()
87    if (arg === 'stop') {
88      await update($, playing, () => null)
89      return { text: 'Stopped.' }
90    }
91
92    const names = [...byName.keys()]
93    const name = arg || names[Math.floor(Math.random() * names.length)]
94    if (!byName.has(name)) return { text: `No piece "${name}". /ascii list shows all ${names.length}.` }
95
96    await update($, playing, () => name)
97    $.ui.invalidate('ui.render')
98
99    return { text: `Playing ${name} above the prompt; /ascii stop hides it.` }
100  })
101
102  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
103    const name = await read($, playing)
104    const piece = name === null ? undefined : byName.get(name)
105    if (name === null || !piece || e.props.hasSurvey) return next(e)
106    const elements = $.ui.resolve(e)
107    // Raster and Image are the terminal's alone
108    if (!('Raster' in elements)) return next(e)
109    const { Box, Image, Raster, Text } = elements
110
111    const { meta } = piece
112    if (player?.name !== name) {
113      const art: Art =
114        pictures && meta.cell === 1 && meta.palette && meta.ground
115          ? { pic: dots(meta, SCALE), color: new Uint8Array(meta.cols * meta.rows) }
116          : { cells: painter(meta, true) }
117      // A piece keeps its own ground and shades for it; anything else assumes a dark terminal.
118      const base = 'pic' in art ? art.pic.base : art.cells.base
119      const start = await $.clock.now()
120      player = { name, frame: piece.default({ ...meta.options }), paper: base >= 0 ? !dark(base) : false, fps: meta.fps, start, art, w: 0, h: 0, busy: false, denied: 0 }
121    }
122    const p = player
123    const art = p.art
124    // The band's width, at most half the screen (outside fullscreen maxRows is all of it), less the caption's row.
125    const W = Math.min(255, e.props.bodyColumns)
126    const H = Math.max(1, Math.min(e.props.maxRows, Math.floor((e.viewport?.rows ?? 48) / 2)) - 1)
127    if ('pic' in art) {
128      // a terminal cell is about twice as tall as it is wide
129      p.w = Math.max(1, Math.min(W, Math.floor((2 * H * meta.cols) / meta.rows)))
130      p.h = Math.max(1, Math.round((p.w * meta.rows) / meta.cols / 2))
131    } else {
132      art.cells.size(W, H)
133      // as wide as the piece, so the Box centres it rather than its ground filling the band either side
134      p.w = Math.min(W, art.cells.cols)
135      p.h = Math.min(art.cells.rows, H)
136    }
137    paint(p, await $.clock.now())
138
139    const site = e.requestId
140    if (p.fps > 0 && !p.tick) {
141      const tick = $.clock.every(Math.round(1000 / p.fps), async () => {
142        if (player !== p) return tick.cancel()
143        // The last frame still on its way: skip this tick rather than queue behind it.
144        if (p.busy) return
145        p.busy = true
146        try {
147          paint(p, await $.clock.now())
148          // the engine refuses with { deny }; a hook above refusing rejects
149          const { deny } = await $.ui
150            .blit(
151              'pic' in art
152                ? { requestId: site, key: ART, source: art.pic.source() }
153                : { requestId: site, key: ART, columns: p.w, rows: p.h, cells: encode(art.cells, p.w, p.h) },
154            )
155            .catch((err: unknown) => ({ deny: String(err) }))
156          // The one redraw from here: its tree holds no Image, so it cannot be refused this way again.
157          if ('pic' in art && deny?.includes('draws its alt')) {
158            pictures = false
159            stop()
160            return $.ui.invalidate('ui.render')
161          }
162          // A refusal or two is a resize landing; a second of them (the band hidden, an Image showing its alt) rests
163          // the piece until the band draws again. Never a redraw from here: one that is refused again loops.
164          p.denied = deny ? p.denied + 1 : 0
165          if (p.denied > p.fps) {
166            tick.cancel()
167            p.tick = undefined
168          }
169        } finally {
170          p.busy = false
171        }
172      })
173      p.tick = tick
174    }
175
176    return (
177      <Box flexDirection="column" alignItems="center">
178        {'pic' in art ? (
179          <Image key={ART} source={art.pic.source()} columns={p.w} rows={p.h} alt={name} />
180        ) : (
181          <Raster key={ART} columns={p.w} rows={p.h} cells={encode(art.cells, p.w, p.h)} />
182        )}
183        <Text dimColor wrap="truncate">
184          {name}: {meta.note}
185        </Text>
186      </Box>
187    )
188  })
189}
190
hooks/dots.ts 60 lines
1import { png } from './png.ts'
2import type { Meta } from './vendor/types.ts'
3
4// Each dot's diameter as a share of its square cell: the glyph's size in SF Mono (ui-monospace on a Mac), drawn at 1/0.6
5// of the cell as ascii.rest's canvas draws it, measured with Pillow. The knob to tune the look.
6const DOT: Record<string, number> = { '·': 0.22, '•': 0.67, '●': 1.03 }
7// samples a pixel takes each way, for a dot's soft edge
8const SAMPLES = 4
9
10/** A scene's frames as RGBA pixels, `scale` a cell: each dot a disc of its palette ink on the ground, as the site draws it. */
11export function dots({ cols, rows, palette = [], ground = '#000000' }: Meta, scale: number) {
12  const width = cols * scale, height = rows * scale
13  const rgba = new Uint8Array(width * height * 4)
14  const rgb = (hex: string) => parseInt(hex.slice(1, 7), 16)
15  const base = rgb(ground), inks = palette.map(rgb)
16  // the ground as one little-endian RGBA word
17  const fill = (0xff000000 | ((base & 255) << 16) | (base & 0xff00) | (base >> 16)) >>> 0
18  // how much of each of a cell's pixels a dot covers
19  const masks = new Map(
20    Object.entries(DOT).map(([c, d]) => {
21      const mask = new Float32Array(scale * scale), r = (d * scale) / 2, mid = scale / 2
22      for (let j = 0; j < scale; j++)
23        for (let i = 0; i < scale; i++) {
24          let n = 0
25          for (let v = 0; v < SAMPLES; v++)
26            for (let u = 0; u < SAMPLES; u++) if (Math.hypot(i + (u + 0.5) / SAMPLES - mid, j + (v + 0.5) / SAMPLES - mid) <= r) n++
27          mask[j * scale + i] = n / SAMPLES ** 2
28        }
29      return [c, mask]
30    }),
31  )
32
33  const paint = (text: string, color: Uint8Array) => {
34    new Uint32Array(rgba.buffer).fill(fill)
35    const lines = text.split('\n')
36    for (let y = 0; y < rows; y++)
37      for (let x = 0; x < cols; x++) {
38        const c = lines[y]?.[x]
39        if (c === undefined || c === ' ') continue
40        const mask = masks.get(c) ?? masks.get('•')!
41        const ink = inks[color[y * cols + x]] ?? inks[0] ?? 0xffffff
42        const r = ink >> 16, g = (ink >> 8) & 255, b = ink & 255
43        for (let j = 0; j < scale; j++)
44          for (let i = 0, k = ((y * scale + j) * width + x * scale) * 4; i < scale; i++, k += 4) {
45            const a = mask[j * scale + i]
46            if (a === 0) continue
47            rgba[k] += (r - rgba[k]) * a
48            rgba[k + 1] += (g - rgba[k + 1]) * a
49            rgba[k + 2] += (b - rgba[k + 2]) * a
50          }
51      }
52  }
53  // as a PNG, a 30th of the raw pixels; raw past 256 colours
54  const source = () => {
55    const file = png(rgba, width, height)
56    return file ? { png: file.toBase64() } : { rgba: rgba.toBase64(), width, height }
57  }
58  return { width, height, base, rgba, paint, source }
59}
60
hooks/painter.ts 172 lines
1/*
2 * painter() and its helpers, copied from src/terminal.ts of ascii.rest by @bas3line
3 * (https://github.com/bas3line/ascii, MIT, see vendor/LICENSE); only `export` added.
4 * terminal.ts itself cannot load here: it imports node:process and dynamic import().
5 */
6import type { Meta } from "./vendor/types.ts";
7
8// How much of a square cell each of a scene's dots inks, for mixing its colour into the ground.
9const INK: Record<string, number> = { " ": 0, "·": 0.15, "•": 0.45, "●": 0.9, "░": 0.25, "▒": 0.5, "▓": 0.75, "█": 1 };
10
11const int = (hex: string) => parseInt(hex.slice(1, 7), 16);
12const mix = (a: number, b: number, k: number) =>
13  [16, 8, 0].reduce((out, s) => out | (Math.round(((a >> s) & 255) + (((b >> s) & 255) - ((a >> s) & 255)) * k) << s), 0);
14export const dark = (c: number) => 0.2126 * ((c >> 16) & 255) + 0.7152 * ((c >> 8) & 255) + 0.0722 * (c & 255) < 128;
15
16// How a run of n cells is averaged onto m: for each of the m, the cells it covers and the share of it each one is.
17function spans(n: number, m: number) {
18  return Array.from({ length: m }, (_, i) => {
19    const a = (i * n) / m, b = ((i + 1) * n) / m, cover: [number, number][] = [];
20    for (let j = Math.floor(a); j < Math.ceil(b); j++) {
21      const w = Math.min(j + 1, b) - Math.max(j, a);
22      if (w > 0) cover.push([j, w / (b - a)]);
23    }
24    return cover;
25  });
26}
27
28/*
29 * The terminal's cells for a piece's frames: each cell's character, and its ink and ground as 0xrrggbb, or -1 for the
30 * terminal's own. A piece with character cells is drawn as it is, and cropped by play() if the terminal is smaller.
31 *
32 * A square-celled piece (cell: 1), a scene, is a picture of up to 320 by 120 cells, too big for most terminals, and its
33 * dots are a texture that a canvas draws a few pixels across: one dot a solid cell reads as noise. So it is shrunk to
34 * fit the terminal and drawn in tones. In colour, each of its cells is its ink mixed into the ground by how much its dot
35 * inks, softened by a 1 4 1 blur each way, then averaged down onto the size that fits: two of those rows in each
36 * terminal cell, as an upper half block whose ink is the top one and whose ground is the bottom. As text, each terminal
37 * cell keeps the heaviest character of the cells it covers, so a star or a thin line survives. size() sets the room.
38 */
39export function painter({ cols, rows, palette, ground, cell = 2 }: Meta, colour: boolean) {
40  const square = cell === 1;
41  const color = colour && palette ? new Uint8Array(cols * rows) : undefined;
42  const inks = palette?.map(int) ?? [];
43  const base = color && ground ? int(ground) : -1;
44  const shades = new Map<number, number>();
45  // A square cell's colour: the ground for a space, else its ink, mixed into the ground when there is one.
46  const shade = (c: string, i: number) => {
47    if (c === " ") return base;
48    const key = i * 65536 + c.charCodeAt(0);
49    let s = shades.get(key);
50    if (s === undefined) shades.set(key, (s = base < 0 ? (inks[i] ?? inks[0]) : mix(base, inks[i] ?? inks[0], INK[c] ?? 0.5)));
51    return s;
52  };
53  const weight = (c: string) => " ·•●".indexOf(c) + 1 || 2.5;
54  // Tones only make sense over a ground; a square piece in colour with none keeps one cell a half, as drawn.
55  const tones = square && color !== undefined && base >= 0;
56  const pic = tones ? new Float32Array(cols * rows * 3) : new Float32Array(0);
57  const soft = tones ? new Float32Array(cols * rows * 3) : new Float32Array(0);
58
59  const out = { cols, rows: square ? Math.ceil(rows / 2) : rows, color, base, ch: [] as string[], fg: new Int32Array(0), bg: new Int32Array(0) };
60  let across: [number, number][][] = [], down: [number, number][][] = [], half = rows;
61  let wide = new Float32Array(0), small = new Float32Array(0);
62  const room = (w: number, h: number) => {
63    out.cols = w;
64    out.rows = h;
65    out.ch = new Array<string>(w * h).fill(" ");
66    out.fg = new Int32Array(w * h).fill(-1);
67    out.bg = new Int32Array(w * h).fill(-1);
68  };
69  // The room it has: a piece with character cells keeps its size, and so does a square one in colour with no ground; a
70  // scene, or a square piece as text, takes the most of the terminal it can.
71  const size = (W: number, H: number) => {
72    if (!square || (color && !tones)) return void (out.ch.length || room(cols, square ? Math.ceil(rows / 2) : rows));
73    const s = Math.min(1, W / cols, (2 * H) / rows), w = Math.max(1, Math.round(cols * s));
74    half = Math.max(2, Math.round(rows * s));
75    across = spans(cols, w);
76    down = spans(rows, tones ? half : Math.ceil(half / 2));
77    wide = new Float32Array(w * rows * 3);
78    small = new Float32Array(w * half * 3);
79    room(w, Math.ceil(half / 2));
80  };
81
82  // pic, blurred 1 4 1 across into soft and then down back into pic, the edges weighed by what they have
83  const blur = () => {
84    for (let y = 0; y < rows; y++)
85      for (let x = 0; x < cols; x++)
86        for (let c = 0, i = (y * cols + x) * 3; c < 3; c++, i++) {
87          let sum = 4 * pic[i], n = 4;
88          if (x > 0) (sum += pic[i - 3]), n++;
89          if (x < cols - 1) (sum += pic[i + 3]), n++;
90          soft[i] = sum / n;
91        }
92    const stride = cols * 3;
93    for (let y = 0; y < rows; y++)
94      for (let i = y * stride; i < (y + 1) * stride; i++) {
95        let sum = 4 * soft[i], n = 4;
96        if (y > 0) (sum += soft[i - stride]), n++;
97        if (y < rows - 1) (sum += soft[i + stride]), n++;
98        pic[i] = sum / n;
99      }
100  };
101
102  const paint = (text: string) => {
103    const lines = text.split("\n");
104    const { ch, fg, bg } = out, w = out.cols;
105    if (tones) {
106      for (let y = 0; y < rows; y++)
107        for (let x = 0; x < cols; x++) {
108          const s = shade(lines[y]?.[x] ?? " ", color![y * cols + x]), i = (y * cols + x) * 3;
109          pic[i] = (s >> 16) & 255;
110          pic[i + 1] = (s >> 8) & 255;
111          pic[i + 2] = s & 255;
112        }
113      blur();
114      // averaged across, then down
115      for (let y = 0; y < rows; y++)
116        for (let x = 0; x < w; x++)
117          for (let c = 0; c < 3; c++) {
118            let sum = 0;
119            for (const [j, k] of across[x]) sum += pic[(y * cols + j) * 3 + c] * k;
120            wide[(y * w + x) * 3 + c] = sum;
121          }
122      for (let y = 0; y < half; y++)
123        for (let x = 0; x < w; x++)
124          for (let c = 0; c < 3; c++) {
125            let sum = 0;
126            for (const [j, k] of down[y]) sum += wide[(j * w + x) * 3 + c] * k;
127            small[(y * w + x) * 3 + c] = sum;
128          }
129      const at = (x: number, y: number) => {
130        if (y >= half) return base;
131        const i = (y * w + x) * 3;
132        return (Math.round(small[i]) << 16) | (Math.round(small[i + 1]) << 8) | Math.round(small[i + 2]);
133      };
134      for (let y = 0, k = 0; y < out.rows; y++)
135        for (let x = 0; x < w; x++, k++) {
136          const top = at(x, 2 * y), bottom = at(x, 2 * y + 1);
137          if (top === bottom) (ch[k] = " "), (fg[k] = -1), (bg[k] = top);
138          else (ch[k] = "▀"), (fg[k] = top), (bg[k] = bottom);
139        }
140    } else if (square && color) {
141      // No ground to mix into: one cell a half, as drawn, cropped by play() when it does not fit.
142      for (let y = 0, k = 0; y < out.rows; y++)
143        for (let x = 0; x < w; x++, k++) {
144          const top = shade(lines[2 * y]?.[x] ?? " ", color[2 * y * cols + x]);
145          const bottom = 2 * y + 1 < rows ? shade(lines[2 * y + 1]?.[x] ?? " ", color[(2 * y + 1) * cols + x]) : base;
146          // With no ground behind a half, the other half's block is drawn alone.
147          if (top === bottom) (ch[k] = " "), (fg[k] = -1), (bg[k] = top);
148          else if (top < 0) (ch[k] = "▄"), (fg[k] = bottom), (bg[k] = -1);
149          else (ch[k] = "▀"), (fg[k] = top), (bg[k] = bottom);
150        }
151    } else if (square) {
152      for (let y = 0, k = 0; y < out.rows; y++)
153        for (let x = 0; x < w; x++, k++) {
154          let best = " ";
155          for (const [j] of down[y]) for (const [i] of across[x]) {
156            const c = lines[j]?.[i] ?? " ";
157            if (weight(c) > weight(best)) best = c;
158          }
159          ch[k] = best;
160        }
161    } else {
162      for (let y = 0, k = 0; y < rows; y++)
163        for (let x = 0; x < cols; x++, k++) {
164          const c = (ch[k] = lines[y]?.[x] ?? " ");
165          fg[k] = color && c !== " " ? (inks[color[k]] ?? inks[0]) : -1;
166          bg[k] = base;
167        }
168    }
169  };
170  return Object.assign(out, { size, paint });
171}
172
hooks/vendor/pieces/index.ts 214 lines
1// ascii.rest by @bas3line (https://github.com/bas3line), MIT licensed.
2// Generated by scripts/gen.ts from the files in src/pieces. Run npm run gen after adding one.
3
4/** Every piece as a named export: import { donut, nightCoast } from "ascii.rest/pieces". */
5export * as almalinux from "./almalinux.ts";
6export * as alpineDawn from "./alpine-dawn.ts";
7export * as alpineLinux from "./alpine-linux.ts";
8export * as analogClock from "./analog-clock.ts";
9export * as apple from "./apple.ts";
10export * as aquarium from "./aquarium.ts";
11export * as archLinux from "./arch-linux.ts";
12export * as aurora from "./aurora.ts";
13export * as auroraFjord from "./aurora-fjord.ts";
14export * as barChart from "./bar-chart.ts";
15export * as bigText from "./big-text.ts";
16export * as blackHole from "./black-hole.ts";
17export * as bonsai from "./bonsai.ts";
18export * as bootLog from "./boot-log.ts";
19export * as bouncingBalls from "./bouncing-balls.ts";
20export * as boxFrames from "./box-frames.ts";
21export * as butterfly from "./butterfly.ts";
22export * as c from "./c.ts";
23export * as calendar from "./calendar.ts";
24export * as campfire from "./campfire.ts";
25export * as candle from "./candle.ts";
26export * as candlesticks from "./candlesticks.ts";
27export * as cat from "./cat.ts";
28export * as centos from "./centos.ts";
29export * as cherryBlossom from "./cherry-blossom.ts";
30export * as chladni from "./chladni.ts";
31export * as clojure from "./clojure.ts";
32export * as cloudflare from "./cloudflare.ts";
33export * as coderabbit from "./coderabbit.ts";
34export * as coffee from "./coffee.ts";
35export * as commandCode from "./command-code.ts";
36export * as contourMap from "./contour-map.ts";
37export * as cpp from "./cpp.ts";
38export * as cpuMeters from "./cpu-meters.ts";
39export * as csharp from "./csharp.ts";
40export * as css from "./css.ts";
41export * as cube from "./cube.ts";
42export * as dart from "./dart.ts";
43export * as databuddy from "./databuddy.ts";
44export * as debian from "./debian.ts";
45export * as deepReef from "./deep-reef.ts";
46export * as deepin from "./deepin.ts";
47export * as desertNight from "./desert-night.ts";
48export * as digitalClock from "./digital-clock.ts";
49export * as dissolve from "./dissolve.ts";
50export * as dividers from "./dividers.ts";
51export * as dnaHelix from "./dna-helix.ts";
52export * as donut from "./donut.ts";
53export * as doomFire from "./doom-fire.ts";
54export * as doublePendulum from "./double-pendulum.ts";
55export * as earth from "./earth.ts";
56export * as earthrise from "./earthrise.ts";
57export * as eclipse from "./eclipse.ts";
58export * as elementaryOs from "./elementary-os.ts";
59export * as elixir from "./elixir.ts";
60export * as elm from "./elm.ts";
61export * as endeavouros from "./endeavouros.ts";
62export * as epicycles from "./epicycles.ts";
63export * as equalizer from "./equalizer.ts";
64export * as erlang from "./erlang.ts";
65export * as fallingSand from "./falling-sand.ts";
66export * as fedora from "./fedora.ts";
67export * as fern from "./fern.ts";
68export * as ferrisWheel from "./ferris-wheel.ts";
69export * as fileTree from "./file-tree.ts";
70export * as fireflies from "./fireflies.ts";
71export * as fireworks from "./fireworks.ts";
72export * as flag from "./flag.ts";
73export * as flowField from "./flow-field.ts";
74export * as formControls from "./form-controls.ts";
75export * as fountain from "./fountain.ts";
76export * as fox from "./fox.ts";
77export * as fractalTree from "./fractal-tree.ts";
78export * as galaxy from "./galaxy.ts";
79export * as gauge from "./gauge.ts";
80export * as gentoo from "./gentoo.ts";
81export * as gliderGun from "./glider-gun.ts";
82export * as glitch from "./glitch.ts";
83export * as glxgears from "./glxgears.ts";
84export * as go from "./go.ts";
85export * as greptile from "./greptile.ts";
86export * as gyroscope from "./gyroscope.ts";
87export * as harmonograph from "./harmonograph.ts";
88export * as haskell from "./haskell.ts";
89export * as hawaMahal from "./hawa-mahal.ts";
90export * as heart from "./heart.ts";
91export * as heartbeat from "./heartbeat.ts";
92export * as heatmap from "./heatmap.ts";
93export * as helium from "./helium.ts";
94export * as hilbertCurve from "./hilbert-curve.ts";
95export * as hourglass from "./hourglass.ts";
96export * as html from "./html.ts";
97export * as icosahedron from "./icosahedron.ts";
98export * as java from "./java.ts";
99export * as javascript from "./javascript.ts";
100export * as jellyfish from "./jellyfish.ts";
101export * as julia from "./julia.ts";
102export * as juliaSet from "./julia-set.ts";
103export * as kaliLinux from "./kali-linux.ts";
104export * as kite from "./kite.ts";
105export * as kotlin from "./kotlin.ts";
106export * as kyotoDusk from "./kyoto-dusk.ts";
107export * as landscape from "./landscape.ts";
108export * as langtonsAnt from "./langtons-ant.ts";
109export * as lanternLake from "./lantern-lake.ts";
110export * as lavaLamp from "./lava-lamp.ts";
111export * as lighthouse from "./lighthouse.ts";
112export * as lightning from "./lightning.ts";
113export * as linuxMint from "./linux-mint.ts";
114export * as lorenz from "./lorenz.ts";
115export * as lua from "./lua.ts";
116export * as mandelbrot from "./mandelbrot.ts";
117export * as manjaro from "./manjaro.ts";
118export * as marineDrive from "./marine-drive.ts";
119export * as marquee from "./marquee.ts";
120export * as matrixRain from "./matrix-rain.ts";
121export * as maze from "./maze.ts";
122export * as mintlify from "./mintlify.ts";
123export * as mistyForest from "./misty-forest.ts";
124export * as mobiusStrip from "./mobius-strip.ts";
125export * as moonPhases from "./moon-phases.ts";
126export * as morse from "./morse.ts";
127export * as newtonsCradle from "./newtons-cradle.ts";
128export * as nightCoast from "./night-coast.ts";
129export * as nixos from "./nixos.ts";
130export * as notFound from "./not-found.ts";
131export * as ocaml from "./ocaml.ts";
132export * as oceanSunset from "./ocean-sunset.ts";
133export * as omarchy from "./omarchy.ts";
134export * as opensuse from "./opensuse.ts";
135export * as orchid from "./orchid.ts";
136export * as owl from "./owl.ts";
137export * as pendulumWave from "./pendulum-wave.ts";
138export * as perl from "./perl.ts";
139export * as php from "./php.ts";
140export * as planet from "./planet.ts";
141export * as planetscale from "./planetscale.ts";
142export * as plasma from "./plasma.ts";
143export * as playstation from "./playstation.ts";
144export * as pluckedString from "./plucked-string.ts";
145export * as polar from "./polar.ts";
146export * as pondRipples from "./pond-ripples.ts";
147export * as popOs from "./pop-os.ts";
148export * as progressBar from "./progress-bar.ts";
149export * as python from "./python.ts";
150export * as r from "./r.ts";
151export * as radar from "./radar.ts";
152export * as rain from "./rain.ts";
153export * as reactionDiffusion from "./reaction-diffusion.ts";
154export * as redHat from "./red-hat.ts";
155export * as rocket from "./rocket.ts";
156export * as rockyLinux from "./rocky-linux.ts";
157export * as rotozoomer from "./rotozoomer.ts";
158export * as ruby from "./ruby.ts";
159export * as rule30 from "./rule-30.ts";
160export * as ruledMountains from "./ruled-mountains.ts";
161export * as rust from "./rust.ts";
162export * as saptarishi from "./saptarishi.ts";
163export * as scala from "./scala.ts";
164export * as scramble from "./scramble.ts";
165export * as seaSwell from "./sea-swell.ts";
166export * as sierpinski from "./sierpinski.ts";
167export * as skeleton from "./skeleton.ts";
168export * as skyline from "./skyline.ts";
169export * as smoke from "./smoke.ts";
170export * as snake from "./snake.ts";
171export * as snowfall from "./snowfall.ts";
172export * as solarSystem from "./solar-system.ts";
173export * as sparkline from "./sparkline.ts";
174export * as sparks from "./sparks.ts";
175export * as spider from "./spider.ts";
176export * as spinners from "./spinners.ts";
177export * as splitFlap from "./split-flap.ts";
178export * as spring from "./spring.ts";
179export * as starfield from "./starfield.ts";
180export * as starlings from "./starlings.ts";
181export * as stormPlains from "./storm-plains.ts";
182export * as sundial from "./sundial.ts";
183export * as sunrise from "./sunrise.ts";
184export * as supabase from "./supabase.ts";
185export * as swift from "./swift.ts";
186export * as synthwave from "./synthwave.ts";
187export * as tajDawn from "./taj-dawn.ts";
188export * as terminal from "./terminal.ts";
189export * as tesseract from "./tesseract.ts";
190export * as threeBody from "./three-body.ts";
191export * as tokyoRain from "./tokyo-rain.ts";
192export * as torusKnot from "./torus-knot.ts";
193export * as train from "./train.ts";
194export * as tunnel from "./tunnel.ts";
195export * as tux from "./tux.ts";
196export * as tvStatic from "./tv-static.ts";
197export * as twistedRing from "./twisted-ring.ts";
198export * as typescript from "./typescript.ts";
199export * as typewriter from "./typewriter.ts";
200export * as ubuntu from "./ubuntu.ts";
201export * as uptimeBar from "./uptime-bar.ts";
202export * as varanasiGhats from "./varanasi-ghats.ts";
203export * as vercel from "./vercel.ts";
204export * as vinyl from "./vinyl.ts";
205export * as voidLinux from "./void-linux.ts";
206export * as voronoi from "./voronoi.ts";
207export * as waveInterference from "./wave-interference.ts";
208export * as waveText from "./wave-text.ts";
209export * as whale from "./whale.ts";
210export * as wind from "./wind.ts";
211export * as windmill from "./windmill.ts";
212export * as zig from "./zig.ts";
213export * as zorinOs from "./zorin-os.ts";
214
hooks/vendor/types.ts 68 lines
1/*
2 * The piece contract. A piece module exports `meta` and a default function:
3 * the function takes the options and returns `frame(t, env)`, which returns
4 * the picture at `t` seconds as one string of `rows` lines of `cols` characters.
5 */
6
7export type Category =
8  | "scenes"
9  | "shapes"
10  | "space"
11  | "physics"
12  | "nature"
13  | "creatures"
14  | "objects"
15  | "generative"
16  | "effects"
17  | "ui"
18  | "data"
19  | "type"
20  | "logos"
21  | "companies"
22  | "distros";
23
24export type Options = Record<string, unknown>;
25
26export interface Meta<O extends Options = Options> {
27  /** Lowercase display name: "newton's cradle". */
28  name: string;
29  category: Category;
30  /** One lowercase line, at most 72 characters, saying what you see. */
31  note: string;
32  /** Frame size in cells: every frame is exactly `rows` lines of `cols` characters. */
33  cols: number;
34  rows: number;
35  /** Frames a second; 0 for a still. */
36  fps: number;
37  /** Defaults for every option the default function takes. */
38  options?: O;
39  /** True if the picture depends on the real time or date. */
40  clock?: boolean;
41  /** Up to 64 colours as #rrggbb, indexed by `env.color`; such a piece draws on a canvas. */
42  palette?: readonly string[];
43  /** The colour behind a coloured piece. */
44  ground?: string;
45  /** Cell height in cell widths on a canvas: 2, the shape of a character, by default; 1 is square. */
46  cell?: 1 | 2;
47}
48
49export interface Env {
50  /** True when the text is dark on a light ground, so shaded pieces can flip their ramp. */
51  paper?: boolean;
52  /**
53   * For coloured pieces drawn in colour: one palette index a cell, row by row,
54   * written by the frame. Absent when the piece is drawn as text in one ink.
55   */
56  color?: Uint8Array;
57}
58
59/** The picture at `t` seconds of play time. */
60export type Frame = (t: number, env?: Env) => string;
61
62/** A piece module, as `import * as donut from ".../donut"` gives it. */
63export interface Piece<O extends Options = Options> {
64  meta: Meta<O>;
65  // A method, so a piece whose options are { text: string } still counts as a Piece.
66  default(options?: Partial<O>): Frame;
67}
68
hooks/png.ts 131 lines
1// An opaque RGBA picture as an indexed-colour PNG, deflated here: the sandbox has no zlib, and the engine moves a
2// picture to the terminal at about a megabyte a second, so a scene's raw 700 KB frame plays at one a second and a
3// 10 to 20 KB PNG of it at the piece's own rate.
4
5const CRC = new Uint32Array(256).map((_, n) => {
6  for (let k = 0; k < 8; k++) n = n & 1 ? 0xedb88320 ^ (n >>> 1) : n >>> 1
7  return n
8})
9const crc = (bytes: Uint8Array) => {
10  let c = ~0
11  for (const b of bytes) c = CRC[(c ^ b) & 255] ^ (c >>> 8)
12  return ~c >>> 0
13}
14
15// deflate's match lengths and distances: each code's base and its count of extra bits
16const LEN = [3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258]
17const LEN_EXTRA = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0]
18const DIST = [1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577]
19const DIST_EXTRA = [0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13]
20// how many earlier places with the same next three bytes a match looks at
21// ponytail: fixed Huffman codes, about 1.5x zlib's size here; dynamic codes if frames ever outgrow the engine's pace
22const CHAIN = 32
23
24/** `data` as one final deflate block of fixed Huffman codes, its repeats as LZ77 matches. */
25export function deflate(data: Uint8Array): Uint8Array {
26  const out = new Uint8Array(data.length + (data.length >> 3) + 16)
27  let pos = 0, acc = 0, bits = 0
28  const put = (value: number, count: number) => {
29    acc |= value << bits
30    for (bits += count; bits >= 8; bits -= 8) (out[pos++] = acc & 255), (acc >>>= 8)
31  }
32  // Huffman codes go most significant bit first
33  const code = (value: number, count: number) => {
34    let r = 0
35    for (let i = 0; i < count; i++) r = (r << 1) | ((value >> i) & 1)
36    put(r, count)
37  }
38  const symbol = (s: number) =>
39    s < 144 ? code(0x30 + s, 8) : s < 256 ? code(0x190 + s - 144, 9) : s < 280 ? code(s - 256, 7) : code(0xc0 + s - 280, 8)
40
41  const head = new Int32Array(1 << 15).fill(-1), prev = new Int32Array(data.length)
42  const hash = (i: number) => ((data[i] << 10) ^ (data[i + 1] << 5) ^ data[i + 2]) & 0x7fff
43  const insert = (i: number) => {
44    if (i + 2 >= data.length) return
45    const h = hash(i)
46    prev[i] = head[h]
47    head[h] = i
48  }
49
50  put(1, 1)
51  put(1, 2)
52  for (let i = 0; i < data.length; ) {
53    let best = 0, dist = 0
54    if (i + 2 < data.length)
55      for (let j = head[hash(i)], n = 0; j >= 0 && i - j <= 32768 && n < CHAIN; j = prev[j], n++) {
56        let l = 0
57        while (l < 258 && i + l < data.length && data[j + l] === data[i + l]) l++
58        if (l > best) (best = l), (dist = i - j)
59        if (l === 258) break
60      }
61    if (best < 3) {
62      symbol(data[i])
63      insert(i++)
64      continue
65    }
66    let c = LEN.length - 1
67    while (LEN[c] > best) c--
68    symbol(257 + c)
69    put(best - LEN[c], LEN_EXTRA[c])
70    let d = DIST.length - 1
71    while (DIST[d] > dist) d--
72    code(d, 5)
73    put(dist - DIST[d], DIST_EXTRA[d])
74    for (const end = i + best; i < end; ) insert(i++)
75  }
76  symbol(256)
77  if (bits > 0) out[pos++] = acc & 255
78  return out.subarray(0, pos)
79}
80
81/** The PNG of `width` x `height` opaque RGBA pixels, or undefined past 256 colours. */
82export function png(rgba: Uint8Array, width: number, height: number): Uint8Array | undefined {
83  const px = new Uint32Array(rgba.buffer, rgba.byteOffset, width * height)
84  const index = new Map<number, number>()
85  // each row a filter byte (0, none) and its pixels' palette indices
86  const lines = new Uint8Array((width + 1) * height)
87  let last = -1, at = 0
88  for (let y = 0, k = 0; y < height; y++)
89    for (let x = 0, o = y * (width + 1) + 1; x < width; x++, k++, o++) {
90      if (px[k] !== last) {
91        last = px[k]
92        let i = index.get(last)
93        if (i === undefined) {
94          if (index.size === 256) return undefined
95          index.set(last, (i = index.size))
96        }
97        at = i
98      }
99      lines[o] = at
100    }
101  const palette = new Uint8Array(index.size * 3)
102  for (const [c, i] of index) palette.set([c & 255, (c >> 8) & 255, (c >> 16) & 255], i * 3)
103
104  let a = 1, b = 0
105  for (const v of lines) (a = (a + v) % 65521), (b = (b + a) % 65521)
106  const packed = deflate(lines)
107  const zlib = new Uint8Array(packed.length + 6)
108  zlib.set([0x78, 0x01])
109  zlib.set(packed, 2)
110  new DataView(zlib.buffer).setUint32(packed.length + 2, ((b << 16) | a) >>> 0)
111
112  const header = new Uint8Array(13)
113  new DataView(header.buffer).setUint32(0, width)
114  new DataView(header.buffer).setUint32(4, height)
115  header.set([8, 3, 0, 0, 0], 8)
116  const chunks = [chunk('IHDR', header), chunk('PLTE', palette), chunk('IDAT', zlib), chunk('IEND', new Uint8Array(0))]
117  const file = new Uint8Array(8 + chunks.reduce((n, c) => n + c.length, 0))
118  file.set([137, 80, 78, 71, 13, 10, 26, 10])
119  chunks.reduce((o, c) => (file.set(c, o), o + c.length), 8)
120  return file
121}
122
123function chunk(type: string, body: Uint8Array) {
124  const out = new Uint8Array(body.length + 12), view = new DataView(out.buffer)
125  view.setUint32(0, body.length)
126  for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i)
127  out.set(body, 8)
128  view.setUint32(body.length + 8, crc(out.subarray(4, body.length + 8)))
129  return out
130}
131
hooks/vendor/pieces/almalinux.ts 185 lines
1/*
2 * almalinux: the AlmaLinux logo, five coloured figures in a ring, with a scan
3 * line running down it every few seconds that scrambles what it passes.
4 *
5 * Drawn from devicon's almalinux-original.svg (MIT): each cell holds the
6 * character whose shape best matches the logo's edge through it, and 8 where
7 * the logo is solid. On a canvas it takes the logo's colours, lifted on a dark
8 * page where they would sink into it; in a <pre> it is one ink, from devicon's
9 * almalinux-plain.svg (MIT). The logo is a trademark of its owner, shown here
10 * to name the distribution.
11 */
12import type { Frame, Meta } from "../types.ts";
13
14export interface AlmalinuxOptions {
15  [key: string]: unknown;
16  /** Seconds between scans; 0 keeps the logo still. */
17  scan: number;
18}
19
20export const meta = {
21  name: "almalinux",
22  category: "distros",
23  note: "five coloured figures in a ring, scanned now and then",
24  cols: 56,
25  rows: 28,
26  fps: 30,
27  options: { scan: 5 },
28  // The logo's 5 colours for a light page, then for a dark one; each as drawn, then twice lighter for the scan.
29  palette: [
30    "#ff4546", "#0068d8", "#85d82e", "#ffc911", "#22c1ff", "#ff8687",
31    "#599de6", "#b0e677", "#ffdc64", "#6fd7ff", "#ffc7c8", "#b3d2f3",
32    "#daf3c0", "#ffefb8", "#bdecff", "#ff4546", "#0068d8", "#85d82e",
33    "#ffc911", "#22c1ff", "#ff8687", "#599de6", "#b0e677", "#ffdc64",
34    "#6fd7ff", "#ffc7c8", "#b3d2f3", "#daf3c0", "#ffefb8", "#bdecff",
35  ],
36} satisfies Meta<AlmalinuxOptions>;
37
38// In a <pre>, in the page's own colour.
39const MONO = String.raw`
40
41            _pq88q_                       ,
42            8888888p _pq_       __p__  _p888q_
43            8888888Pq88888.   _888888pq8888888,
44            'Y8888(p88888"  _p88888888/8888888"
45           qpqqqpp888888'  _8888888888q/8888P"
46          q888888888888P   8888"   '88888qqqq,
47          d88888P         |888'     '888888888
48          '88888,         |88P       O8888888P
49           '8888q,        |88'              '
50         __, "8888q_,      88        ________
51       _p888,  "Y8888qq_,  "8    _p88888888888qq,
52       )88888q,   '"""YYYY' '  oPP""""""""Y888888q,
53   _ppqq/888888p                            O88888p
54  q888888p8888P          _p'  )q_           q88888P
55  88888888d888         _p8P    "8q_      __8888P"___
56  "888888\8888      __p88"      "88p   )888888\888888(
57    """'q88888qqqppp888P'        O88p   O8888P88888888
58        "888888888888P"          '888,  |8888b)888888P
59         '"88888PP""    _pq,     .888p  |8888P '"PPP"
60                   __pp88888q_,._p888P   ""'
61                   888888888888888888"
62                   '8888"""""8888888P
63                       _p8888p"888P"
64                      .8888888p
65                       8888888P
66                       'Y888P"
67
68`;
69
70// On a canvas, and the colour of each cell: an index into the logo's colours.
71const ART = String.raw`
72
73            _pq88q_                       ,
74            8888888p _pq_       __p__  _p888q_
75            8888888Pq88888.   _888888pq8888888,
76            'Y8888(p88888"  _p88888888/8888888"
77           qpqqqpp888888'  _8888888888q/8888P"
78          q888888888888P   8888"   '88888qqqq,
79          d88888P         |888'     '888888888
80          '88888,         |88P       O8888888P
81           '8888q,        |88'              '
82         __, "8888q_,      88        ________
83       _p888,  "Y8888qq_,  "8    _p88888888888qq,
84       )88888q,   '"""YYYY' '  oPP""""""""Y888888q,
85   _ppqq/888888p                            O88888p
86  q888888p8888P          _p'  )q_           q88888P
87  88888888d888         _p8P    "8q_      __8888P"___
88  "888888\8888      __p88"      "88p   )888888\888888(
89    """'q88888qqqppp888P'        O88p   O8888P88888888
90        "888888888888P"          '888,  |8888b)888888P
91         '"88888PP""    _pq,     .888p  |8888P '"PPP"
92                   __pp88888q_,._p888P   ""'
93                   888888888888888888"
94                   '8888"""""8888888P
95                       _p8888p"888P"
96                      .8888888p
97                       8888888P
98                       'Y888P"
99
100`;
101const INK = String.raw`
102
103            0000000                       3
104            00000000 0000       33333  3333333
105            000000000000000   33333333333333333
106            00000000000000  3333333333333333333
107           00000000000000  3333333333333333333
108          00000000000000   33333   33333333333
109          0000000         33333     3333333333
110          0000000         3333       333333333
111           0000000        3333              3
112         111 00000000      33        22222222
113       111111  0000000000  33    2222222222222222
114       11111111   000000000 3  22222222222222222222
115   1111111111111                            2222222
116  1111111111111          111  444           2222222
117  111111111111         1111    4444      22222222222
118  111111111111      111111      4444   222222222222222
119    111111111111111111111        4444   22222222222222
120        111111111111111          44444  22222222222222
121         11111111111    4444     44444  222222 222222
122                   4444444444444444444   222
123                   4444444444444444444
124                   444444444444444444
125                       4444444444444
126                      444444444
127                       44444444
128                       4444444
129
130`;
131
132const START = 0.5; // seconds before the first scan
133const PASS = 2; // seconds a scan takes, top to bottom
134const HEAD = 2; // rows the line scrambles
135const TRAIL = 5; // rows of afterglow behind it
136const NOISE = "#$%&*+<=>?@[]{}"; // what the line scrambles the logo into
137
138const lines = (art: string) => art.slice(1, -1).split("\n").map((line) => line.padEnd(meta.cols));
139
140function hash(x: number, y: number, k: number) {
141  let h = Math.imul(x, 73856093) ^ Math.imul(y, 19349663) ^ Math.imul(k, 83492791);
142  h = Math.imul(h ^ (h >>> 13), 1274126177);
143  return (h ^ (h >>> 16)) >>> 0;
144}
145
146export default function almalinux({ scan = meta.options.scan }: Partial<AlmalinuxOptions> = {}): Frame {
147  const { cols, rows, fps } = meta;
148  const mono = lines(MONO), art = lines(ART), ink = lines(INK);
149  const n = meta.palette.length / 6;
150  const every = scan > 0 ? Math.max(scan, PASS + 0.5) : 0;
151  // The line runs from the logo's first row to past its last, rather than the whole frame.
152  let first = rows, last = -1;
153  for (const pic of [mono, art])
154    pic.forEach((line, y) => {
155      if (line.trim()) (first = Math.min(first, y)), (last = Math.max(last, y));
156    });
157  const span = last - first + 1 + HEAD + TRAIL;
158
159  return (t, { paper = false, color } = {}) => {
160    const pic = color ? art : mono;
161    const since = t - START;
162    const at = every && since >= 0 ? first + (span * (since % every)) / PASS : -Infinity;
163    const tick = Math.floor((t * fps) / 2); // the noise changes every other frame
164    const out: string[] = [];
165    for (let y = 0; y < rows; y++) {
166      const d = at - y; // rows since the line reached this one
167      const level = d >= 0 && d < HEAD ? 2 : d >= HEAD && d < HEAD + TRAIL ? 1 : 0;
168      let line = "";
169      for (let x = 0; x < cols; x++) {
170        let ch = pic[y][x];
171        if (ch !== " ") {
172          if (level === 2) {
173            const h = hash(x, y, tick);
174            if (h % 4) ch = NOISE[(h >>> 2) % NOISE.length];
175          }
176          if (color) color[y * cols + x] = (paper ? 0 : 3 * n) + level * n + parseInt(ink[y][x], 36);
177        }
178        line += ch;
179      }
180      out.push(line);
181    }
182    return out.join("\n");
183  };
184}
185
hooks/vendor/pieces/alpine-dawn.ts 559 lines
1/*
2 * alpine dawn: jagged snow peaks catch the first pink light on their east
3 * faces while their flanks stay in blue shadow. Mist pools along the far shore
4 * and a still lake mirrors it all. The light warms toward gold as the sun
5 * clears the ridge, the mist drifts, and slow ripples cross the water.
6 *
7 * The range is a heightfield, raymarched once from a camera just above the
8 * water: each cell keeps its depth, height, sunlight (with cast shadows) and
9 * snow cover, so a frame only re-tints it. The lake looks up the picture above
10 * it along each cell's reflected ray. Every cell is then drawn as a halftone
11 * dot, ordered-dithered, in the palette colour nearest its hue.
12 */
13import type { Frame, Meta } from "../types.ts";
14
15export const meta = {
16  name: "alpine dawn",
17  category: "scenes",
18  note: "snow peaks catching first light above a still, misty mountain lake",
19  cols: 200,
20  rows: 100,
21  cell: 1,
22  fps: 15,
23  ground: "#090c18",
24  palette: [
25    "#0e1430", "#151d40", "#1d2752", "#263365", "#314179", "#3e508c", "#4f62a0", "#6577b3", "#8090c4", "#9eaad3", "#bec6e2",
26    "#4b3e6c", "#6a5482", "#8c6a92", "#b0829c", "#cf96a4",
27    "#e8a9a8", "#f5bcaa", "#ffd0b0", "#ffe2c2", "#fff1e0", "#fdfaf6",
28    "#ffc887", "#f7a965", "#f2a08f", "#e58a87", "#f8b59d", "#d97b7e",
29    "#7a4c4a", "#a5654f", "#523a4a",
30    "#1b2034", "#262c45", "#363c59",
31    "#0a1418", "#0f1f24", "#162a2f", "#203a3c",
32    "#a3a7c6", "#c6c3d8", "#e0d4dc",
33    "#5a4f7e", "#7b6c9c", "#9a8cb6", "#b8a8c8", "#d8bccb",
34  ],
35} satisfies Meta;
36
37const W = 200, H = 100;
38const K = 0.62; // tangent of half the field of view, across the width
39const HZ = 56.5; // eye level, in rows
40const CAM = 1.5; // camera height above the water
41const SHORE_Z = 46; // distance to the far shore
42const SHORE = 62; // first row of open water
43const SUN = [151, 50];
44const DOTS = " ·•●";
45const COVER = [0, 0.3, 0.6, 1];
46const BAYER = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5].map((v) => v / 16 - 0.47);
47
48function hash(x: number, y: number): number {
49  let h = Math.imul(x | 0, 374761393) + Math.imul(y | 0, 668265263);
50  h = Math.imul(h ^ (h >>> 13), 1274126177);
51  return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
52}
53
54function noise(x: number, y: number, period: number): number {
55  const xi = Math.floor(x), yi = Math.floor(y);
56  const fx = x - xi, fy = y - yi;
57  const u = fx * fx * (3 - 2 * fx), v = fy * fy * (3 - 2 * fy);
58  let x0 = xi, x1 = xi + 1;
59  if (period) {
60    x0 = ((xi % period) + period) % period;
61    x1 = (x0 + 1) % period;
62  }
63  const a = hash(x0, yi), b = hash(x1, yi), c = hash(x0, yi + 1), d = hash(x1, yi + 1);
64  return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;
65}
66
67function fbm(x: number, y: number, octaves: number, period: number): number {
68  let s = 0, n = 0, amp = 0.5, f = 1;
69  for (let i = 0; i < octaves; i++) {
70    s += amp * noise(x * f, y * f, period * f);
71    n += amp;
72    amp *= 0.5;
73    f *= 2;
74  }
75  return s / n;
76}
77
78// Sharp crests where plain noise crosses its middle: rock ribs and couloirs.
79function ridged(x: number, y: number, octaves: number): number {
80  let s = 0, n = 0, amp = 0.5, f = 1;
81  for (let i = 0; i < octaves; i++) {
82    const v = 1 - Math.abs(2 * noise(x * f + i * 17.3, y * f, 0) - 1);
83    s += amp * v * v;
84    n += amp;
85    amp *= 0.5;
86    f *= 2.1;
87  }
88  return s / n;
89}
90
91const clamp = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);
92const smooth = (a: number, b: number, v: number) => {
93  const k = clamp((v - a) / (b - a));
94  return k * k * (3 - 2 * k);
95};
96const mix = (a: number, b: number, k: number) => a + (b - a) * k;
97const hex = (s: string) => [1, 3, 5].map((i) => parseInt(s.slice(i, i + 2), 16) / 255);
98
99// Peaks as pyramids, each turned a little, placed by where their summits
100// should land on screen: [column, row, distance, spread, turn].
101const PEAKS = [
102  [66, 11, 140, 0.95, 0.3],
103  [38, 26, 115, 1.1, -0.15],
104  [116, 22, 175, 0.9, 0.2],
105  [92, 33, 150, 1.0, 0.1],
106  [96, 25, 340, 1.1, 0.4],
107  [180, 38, 200, 1.5, -0.1],
108  [8, 30, 160, 1.2, 0.25],
109].map(([sx, row, z, f, a]) => {
110  const h = CAM + ((HZ - row) / 100) * K * z - 1.5;
111  return [((sx - 100) / 100) * K * z, z, h, h * f, Math.cos(a), Math.sin(a)];
112});
113
114function terrain(x: number, z: number): number {
115  let h = 0;
116  for (const [px, pz, ph, pr, c, s] of PEAKS) {
117    const dx = x - px, dz = z - pz;
118    const rx = dx * c - dz * s, rz = dx * s + dz * c;
119    const v = ph * (1 - (Math.abs(rx) + Math.abs(rz)) / pr);
120    if (v > h) h = v;
121  }
122  const hills = (1 + 3 * fbm(x * 0.04, z * 0.04, 3, 0)) * smooth(SHORE_Z, SHORE_Z + 15, z);
123  if (hills > h) h = hills;
124  // crags, deeper on the high ground
125  h += ((ridged(x * 0.06, z * 0.06, 3) - 0.45) * 6 + (ridged(x * 0.2, z * 0.2, 2) - 0.45) * 1.6) * smooth(4, 22, h);
126  return h;
127}
128
129// Distance along a ray from height `oy` with slope `v` and spread `u` to the
130// terrain beyond the shore, or 0 when it reaches the sky.
131function march(u: number, v: number, oy: number): number {
132  let z = SHORE_Z, prev = z;
133  for (let i = 0; i < 260 && z < 520; i++) {
134    const gap = oy + v * z - terrain(u * z, z);
135    if (gap < 0) {
136      let a = prev, b = z;
137      for (let j = 0; j < 7; j++) {
138        const m = (a + b) / 2;
139        if (oy + v * m - terrain(u * m, m) < 0) b = m;
140        else a = m;
141      }
142      return b;
143    }
144    prev = z;
145    z += Math.max(0.35, gap * 0.45) + z * 0.002;
146  }
147  return 0;
148}
149
150export default function alpineDawn(): Frame {
151  const P = meta.palette.map(hex);
152  const N = W * H;
153  const out: string[] = new Array(N);
154
155  const lut = new Uint8Array(32768).fill(255);
156  const nearest = (r: number, g: number, b: number): number => {
157    const k = (Math.min(31, (r * 31.99) | 0) << 10) | (Math.min(31, (g * 31.99) | 0) << 5) | Math.min(31, (b * 31.99) | 0);
158    if (lut[k] !== 255) return lut[k];
159    let best = 0, bd = 1e9;
160    for (let i = 0; i < P.length; i++) {
161      const dr = P[i][0] - r, dg = P[i][1] - g, db = P[i][2] - b;
162      const d = 0.3 * dr * dr + 0.5 * dg * dg + 0.2 * db * db;
163      if (d < bd) (bd = d), (best = i);
164    }
165    return (lut[k] = best);
166  };
167
168  const L = (() => {
169    const v = [0.9, 0.3, 0.14];
170    const n = Math.hypot(...v);
171    return v.map((c) => c / n);
172  })();
173
174  // --- the range, raymarched once ------------------------------------------
175  const SR = SHORE; // rows above the open water
176  const depth = new Float32Array(SR * W);
177  const alt = new Float32Array(SR * W);
178  const sun = new Float32Array(SR * W);
179  const snow = new Float32Array(SR * W);
180  const up = new Float32Array(SR * W);
181  for (let r = 0; r < SR; r++) {
182    const v = ((HZ - (r + 0.5)) / 100) * K;
183    for (let x = 0; x < W; x++) {
184      const u = ((x + 0.5 - 100) / 100) * K;
185      const z = march(u, v, CAM);
186      const k = r * W + x;
187      if (!z) continue;
188      const px = u * z, py = CAM + v * z;
189      const e = 0.35;
190      const hx = (terrain(px + e, z) - terrain(px - e, z)) / (2 * e);
191      const hz = (terrain(px, z + e) - terrain(px, z - e)) / (2 * e);
192      const nl = Math.hypot(hx, 1, hz);
193      const nx = -hx / nl, ny = 1 / nl, nz = -hz / nl;
194      let lit = Math.max(0, nx * L[0] + ny * L[1] + nz * L[2]);
195      if (lit > 0) {
196        // cast shadow: walk toward the sun
197        for (let s = 0.8; s < 160; s += 0.6 + s * 0.04) {
198          const qx = px + L[0] * s, qy = py + L[1] * s + 0.15, qz = z + L[2] * s;
199          if (qz < SHORE_Z) break;
200          if (qy < terrain(qx, qz)) {
201            lit = 0;
202            break;
203          }
204        }
205      }
206      depth[k] = z;
207      alt[k] = py;
208      sun[k] = lit;
209      up[k] = ny;
210      const grain = fbm(px * 0.4, py * 0.25, 2, 0);
211      // rock shows through in couloirs running down the fall line
212      const gully = ridged(px * 0.22 + z * 0.05, py * 0.045, 2);
213      snow[k] = smooth(0.36, 0.52, ny + 0.3 * (grain - 0.5)) * smooth(5, 11, py + 5 * grain) * (1 - 0.75 * smooth(0.62, 0.85, gully));
214    }
215  }
216
217  // sunlit terrain with open sky directly above: the crest line
218  const rim = new Uint8Array(SR * W);
219  for (let k = W; k < SR * W; k++) rim[k] = depth[k] && !depth[k - W] && sun[k] > 0 ? 1 : 0;
220
221  // --- the far shore's treeline --------------------------------------------
222  const treeTop = new Float32Array(W);
223  for (let x = 0; x < W; x++) treeTop[x] = SHORE - 0.6 - 1.2 * fbm(x * 0.06, 2.3, 2, 0);
224  for (let tx = -2; tx < W + 2; tx += 1.6 + hash(tx * 9, 7) * 2.2) {
225    const tip = SHORE - 2.6 - hash(tx * 3, 8) * 4 - 1.5 * smooth(60, 0, tx);
226    const slope = 1.1 + hash(tx * 5, 9) * 0.5;
227    for (let x = Math.max(0, Math.floor(tx - 6)); x < Math.min(W, tx + 6); x++) {
228      treeTop[x] = Math.min(treeTop[x], tip + Math.abs(x + 0.5 - tx) * slope);
229    }
230  }
231
232  // --- the lake: where each cell's reflected ray lands in the picture above -
233  const LR = H - SHORE;
234  const src = new Float32Array(LR * W);
235  for (let r = SHORE; r < H; r++) {
236    const v = ((HZ - (r + 0.5)) / 100) * K;
237    for (let x = 0; x < W; x++) {
238      const u = ((x + 0.5 - 100) / 100) * K;
239      const z = march(u, -v, -CAM);
240      const vs = -v - (z ? (2 * CAM) / z : 0);
241      let row = HZ - (vs * 100) / K - 0.5;
242      const mirror = 2 * SHORE - 1 - r; // the treeline stands on the shore
243      if (mirror >= treeTop[x]) row = mirror;
244      src[(r - SHORE) * W + x] = row;
245    }
246  }
247
248  // --- the near pines and the bank they stand on ---------------------------
249  const fg = new Uint8Array(N);
250  const fgShade = new Float32Array(N);
251  const fgRim = new Float32Array(N);
252  const PINES = [
253    [9, 5, 1.15], [20, 38, 0.85], [32, 66, 0.5],
254    [192, 10, 1.15], [181, 40, 0.75], [204, 26, 1],
255  ];
256  for (let r = 0; r < H; r++) {
257    for (let x = 0; x < W; x++) {
258      const k = r * W + x;
259      const y = r + 0.5;
260      const bankL = 88 + 14 * smooth(0, 52, x) + 2 * fbm(x * 0.2, 1, 2, 0);
261      const bankR = 90 + 12 * smooth(W, W - 40, x) + 2 * fbm(x * 0.2, 4, 2, 0);
262      if (y > bankL || y > bankR) (fg[k] = 1), (fgShade[k] = 0.15 * hash(x, r));
263      for (const [px, tip, s] of PINES) {
264        const d = y - tip;
265        if (d < 0) continue;
266        const tier = 3.4 * s;
267        const f = d / tier - Math.floor(d / tier);
268        const hw = (0.4 + d * 0.2) * (0.5 + 0.5 * f) * (1 + 0.6 * (hash(r, Math.floor(px) * 7) - 0.5) * smooth(0, 8, d));
269        const dx = x + 0.5 - px;
270        if (Math.abs(dx) <= hw) {
271          fg[k] = 2;
272          // the outline catches the dawn sky: a warm rim on the side facing
273          // the sun, a dim cool one on the other, the inside stays black
274          const edge = hw - Math.abs(dx) < 1;
275          const sunward = px < SUN[0] ? dx > 0 : dx < 0;
276          fgShade[k] = edge ? (sunward ? 1 : 0.5) : 0.3 * hash(x * 3, r * 5);
277          // the rim is light from the sky behind, so it fades out below the shore
278          fgRim[k] = edge ? smooth(70, 44, y) * (0.75 + 0.25 * hash(x, r * 7)) : 0;
279        }
280      }
281    }
282  }
283
284  // --- mist, a wrapping sheet that drifts along the valley -----------------
285  const MW = 480, M0 = 36, MR = SHORE + 2 - M0;
286  const mist = new Float32Array(MW * MR);
287  for (let r = 0; r < MR; r++) {
288    for (let x = 0; x < MW; x++) {
289      const y = r + M0;
290      const q = fbm(x * 0.0125, y * 0.1, 2, MW * 0.0125);
291      mist[r * MW + x] = fbm(x * 0.025 + q * 1.4, y * 0.22 + q, 4, MW * 0.025);
292    }
293  }
294
295  // --- thin high cloud, streaked and lit from below by the sun -------------
296  const CW = 640, CR = 34;
297  const cloud = new Float32Array(CW * CR);
298  for (let r = 0; r < CR; r++) {
299    for (let x = 0; x < CW; x++) {
300      const y = r + 0.5;
301      const q = fbm(x * 0.0125, y * 0.12, 2, 8);
302      const c = fbm(x * 0.025 + q * 2, y * 0.2 + q * 0.8, 4, 16);
303      cloud[r * CW + x] = smooth(0.52, 0.7, c - 0.06 * Math.abs(y - 18) / 10) * smooth(5, 13, y) * smooth(33, 24, y);
304    }
305  }
306
307  // a faint large-scale unevenness, so the open sky and the deep water are
308  // never one flat halftone screen
309  const hz = new Float32Array(N);
310  for (let k = 0; k < N; k++) hz[k] = fbm((k % W) * 0.03, Math.floor(k / W) * 0.06, 3, 0);
311
312  // the sky's colour at a point, for the sky itself and as haze on the peaks
313  const skyR = new Float32Array(SR * W), skyG = new Float32Array(SR * W), skyB = new Float32Array(SR * W);
314  const glowA = new Float32Array(SR * W);
315  for (let r = 0; r < SR; r++) {
316    for (let x = 0; x < W; x++) {
317      const y = r + 0.5;
318      const v = clamp(y / 52);
319      const east = smooth(20, 190, x);
320      const dx = x + 0.5 - SUN[0], dy = (y - SUN[1]) * 2.2;
321      const ds = Math.sqrt(dx * dx + dy * dy);
322      const low = Math.pow(v, 1.9);
323      // indigo overhead, a soft unevenness in it, then a pale lilac and rose
324      // band behind the range, lighter than the mountains' shadowed flanks
325      const veil = (hz[r * W + x] - 0.5) * 0.1 * (1 - v);
326      let cr = 0.03 + veil + low * (0.56 + 0.2 * east);
327      let cg = 0.04 + veil + low * (0.48 + 0.02 * east);
328      let cb = 0.13 + veil * 1.6 + low * (0.62 - 0.12 * east);
329      skyR[r * W + x] = cr;
330      skyG[r * W + x] = cg;
331      skyB[r * W + x] = cb;
332      // the sun's glow, kept apart so it can breathe
333      glowA[r * W + x] = Math.exp(-ds / 6) * 0.65 + Math.exp(-ds / 15) * 0.2 + Math.exp(-ds / 50) * 0.1;
334    }
335  }
336
337  const AR = new Float32Array(SR * W), AG = new Float32Array(SR * W), AB = new Float32Array(SR * W);
338  const FR = new Float32Array(N), FG = new Float32Array(N), FB = new Float32Array(N);
339  const floor = new Float32Array(N);
340  const fade = new Float32Array(N).fill(1);
341  const wisp = new Float32Array(W);
342
343  return (t, { color } = {}) => {
344    const warm = 0.75 - 0.5 * Math.exp(-t / 60); // rose first light warming toward gold
345    const line = 6 + 4 * Math.exp(-t / 70); // the sunlit line creeps down the slopes
346    const drift = t * 1.1;
347    const pulse = 1 + 0.06 * Math.sin((t / 8) * Math.PI * 2); // the sun's glow breathes
348    for (let x = 0; x < W; x++) wisp[x] = noise((x + drift * 0.6) * 0.06, 3.7, 0);
349
350    // the warm light, from rose toward gold
351    const lr = 1, lg = mix(0.6, 0.8, warm), lb = mix(0.55, 0.4, warm);
352
353    for (let r = 0; r < SR; r++) {
354      const y = r + 0.5;
355      for (let x = 0; x < W; x++) {
356        const k = r * W + x;
357        const gl = glowA[k] * pulse;
358        const gg = 0.62 + 0.28 * smooth(0.15, 0.7, gl); // gold at the core, rose further out
359        let cr = skyR[k] + gl, cg = skyG[k] + gl * gg, cb = skyB[k] + gl * (gg - 0.22), fl = 0.21;
360        const z = depth[k];
361        if (z) {
362          const sn = snow[k];
363          const lit = sun[k] * smooth(line, line + 7, alt[k]);
364          const amb = (0.55 + 0.45 * up[k]) * (0.55 + 0.5 * smooth(4, 34, alt[k]));
365          // snow: deep blue in shadow, rose to gold in the sun, ending sharply
366          const sl = smooth(0.08, 0.24, lit);
367          // full on faces and summits glow gold, glancing light stays rose
368          const gold = clamp(0.6 * smooth(0.25, 0.8, lit) + 0.5 * smooth(14, 36, alt[k]));
369          const br = 0.78 + 0.3 * lit;
370          const sr = mix(0.13 * amb, lr * br, sl);
371          const sg = mix(0.17 * amb, mix(lg - 0.12, lg + 0.14, gold) * br, sl);
372          const sb = mix(0.36 * amb, mix(lb + 0.02, lb + 0.12, gold) * br, sl);
373          // rock: slate in shadow, warm umber in the sun
374          const rr = mix(0.06, 0.4, sl), rg = mix(0.07, 0.2, sl), rb = mix(0.14, 0.2, sl);
375          cr = mix(rr, sr, sn);
376          cg = mix(rg, sg, sn);
377          cb = mix(rb, sb, sn);
378          // forested foothills
379          const wood = smooth(9, 4, alt[k]) * smooth(110, 75, z);
380          cr = mix(cr, 0.07, wood);
381          cg = mix(cg, 0.09, wood);
382          cb = mix(cb, 0.18, wood);
383          // distance hazes toward the sky behind, and haze settles in the
384          // far valleys so each ridge stands clear of the one behind it
385          const fog = smooth(16, 3, alt[k]) * smooth(70, 150, z) * 0.6;
386          const haze = Math.max(fog, clamp(1 - Math.exp(-(z - SHORE_Z) / 260)) * 0.45);
387          cr = mix(cr, skyR[k] + gl, haze);
388          cg = mix(cg, skyG[k] + gl * gg, haze);
389          cb = mix(cb, skyB[k] + gl * (gg - 0.22), haze);
390          // the first light catches the crest itself in a bright line
391          if (rim[k] && sl > 0.3) {
392            const a = rim[k] * sl;
393            cr = mix(cr, 1, a);
394            cg = mix(cg, mix(0.89, 0.95, warm), a);
395            cb = mix(cb, mix(0.76, 0.88, warm), a);
396          }
397          // the shadowed range still carries a dim blue screen; the wooded
398          // foothills in front of it drop away to near black
399          fl = mix(mix(0.42, 0.3, wood), 0.04, sl);
400        } else {
401          // a few stars still out in the west
402          if (y < 34 && hash(x, r * 3 + 11) > 0.985) {
403            const tw = 0.6 + 0.4 * Math.sin(t * (1.5 + hash(x, r) * 3) + hash(r, x) * 6.28);
404            const s = tw * smooth(150, 40, x) * smooth(34, 6, y) * 0.75;
405            cr = Math.max(cr, s * 0.9);
406            cg = Math.max(cg, s * 0.92);
407            cb = Math.max(cb, s);
408          }
409          // high cloud, rose-gold toward the sun and mauve away from it
410          if (r < CR) {
411            const sx = x + t * 0.8, ix = Math.floor(sx), fx = sx - ix;
412            const c0 = cloud[r * CW + (ix % CW)], c1 = cloud[r * CW + ((ix + 1) % CW)];
413            const c = (c0 + (c1 - c0) * fx) * (0.35 + 0.65 * smooth(40, 150, x));
414            if (c > 0.01) {
415              const g = Math.exp(-Math.hypot(x + 0.5 - SUN[0], (y - SUN[1]) * 1.6) / 55);
416              const b = clamp(0.25 + 0.9 * g);
417              const kr = mix(0.32, 1, b), kg = mix(0.24, mix(0.62, 0.74, warm), b), kb = mix(0.4, 0.5, b);
418              cr = mix(cr, kr, c * 0.75);
419              cg = mix(cg, kg, c * 0.75);
420              cb = mix(cb, kb, c * 0.75);
421            }
422          }
423          // the sun, just clearing the ridge
424          const dx = x + 0.5 - SUN[0], dy = y - SUN[1];
425          const ds = Math.sqrt(dx * dx + dy * dy);
426          if (ds < 4.5) {
427            const a = smooth(4.5, 3.3, ds);
428            cr = mix(cr, 1, a);
429            cg = mix(cg, 0.96, a);
430            cb = mix(cb, 0.86, a);
431          }
432        }
433        // mist pooled in the valley behind the shore, lit rose toward the sun
434        const e = smooth(20, 170, x) * (0.6 + 0.4 * warm);
435        const near = Math.exp(-Math.abs(x + 0.5 - SUN[0]) / 22) * 0.3;
436        const mr = mix(0.48, 0.9, e) + near, mg = mix(0.46, 0.7, e) + near * 0.75, mb = mix(0.7, 0.7, e) + near * 0.5;
437        if (r >= M0) {
438          const m = mist[(r - M0) * MW + (Math.floor(x + drift) % MW)];
439          // a ragged top edge: the sheet heaves in long swells and small tufts
440          const edge = 5 * (m - 0.5) + 4 * (wisp[x] - 0.5);
441          const band = smooth(M0 + 14, SHORE - 3, y + edge);
442          const a = (0.3 + 0.7 * smooth(0.32, 0.64, m)) * band * 0.6;
443          cr = mix(cr, mr, a);
444          cg = mix(cg, mg, a);
445          cb = mix(cb, mb, a);
446          if (a > 0.05) fl = Math.max(fl, 0.2);
447        }
448        if (y >= treeTop[x]) {
449          // the far shore's pines, dark against the mist
450          const s = 0.4 + 0.6 * hash(x * 7, r * 3);
451          cr = 0.04 + 0.03 * s;
452          cg = 0.06 + 0.04 * s;
453          cb = 0.1 + 0.05 * s;
454          fl = 0;
455          // and low wisps drifting across their feet
456          const m = mist[(r - M0) * MW + (Math.floor(x * 0.7 + drift * 1.9 + 211) % MW)];
457          const a = smooth(0.45, 0.72, m) * smooth(treeTop[x] + 1, SHORE, y) * 0.6;
458          cr = mix(cr, mr, a);
459          cg = mix(cg, mg, a);
460          cb = mix(cb, mb, a);
461        }
462        AR[k] = cr;
463        AG[k] = cg;
464        AB[k] = cb;
465        FR[k] = cr;
466        FG[k] = cg;
467        FB[k] = cb;
468        floor[k] = fl;
469      }
470    }
471
472    // the lake: the picture above, shaken a little by slow ripples
473    for (let r = SHORE; r < H; r++) {
474      const y = r + 0.5;
475      const d = (y - SHORE) / LR;
476      for (let x = 0; x < W; x++) {
477        const k = r * W + x;
478        const w1 = noise(x * 0.045 + t * 0.06, y * 0.5 - t * 0.35, 0);
479        const w2 = noise(x * 0.12 - t * 0.1, y * 1.1 - t * 0.7, 0);
480        const sway = (w1 - 0.5) * (0.4 + 1.4 * d) + (w2 - 0.5) * 0.5;
481        const sx = Math.max(0, Math.min(W - 1, Math.round(x + sway)));
482        const sr = Math.max(0, Math.min(SR - 1, Math.round(src[(r - SHORE) * W + x] + (w2 - 0.5) * 0.6 * d)));
483        const sk = sr * W + sx;
484        const refl = 0.68 - 0.32 * d;
485        const w3 = noise(x * 0.03 + t * 0.04, y * 1.9 - t * 0.45, 0);
486        const lift = 1 + (w3 - 0.5) * (0.4 + 0.5 * d); // long, faint ripple lines
487        // the water gives back a little less colour than it was sent
488        const ar = AR[sk], ag = AG[sk], ab = AB[sk];
489        const grey = (ar + ag + ab) / 3;
490        const deep = (hz[k] - 0.5) * 0.1 * d; // slow unevenness in the dark water
491        let cr = 0.02 + deep + mix(grey, ar, 0.75) * refl * lift;
492        let cg = 0.035 + deep + mix(grey, ag, 0.75) * refl * lift;
493        let cb = 0.07 + deep * 1.6 + mix(grey, ab, 0.75) * refl * lift;
494        // the sun's road
495        const roadW = 1.5 + (y - SHORE) * 0.45;
496        const road = Math.exp(-(((x + 0.5 - SUN[0]) / roadW) ** 2));
497        const glint = smooth(0.55, 0.85, w2) * road * (0.5 + 0.5 * warm);
498        cr += glint;
499        cg += glint * 0.8;
500        cb += glint * 0.6;
501        FR[k] = cr;
502        FG[k] = cg;
503        FB[k] = cb;
504        floor[k] = 0.26;
505        fade[k] = smooth(H + 2, H - 22, y);
506        if (r === SHORE) {
507          // a dark seam where the shore meets the water
508          FR[k] = 0.06;
509          FG[k] = 0.12;
510          FB[k] = 0.14;
511          floor[k] = 0;
512        }
513      }
514    }
515
516    // the near pines and the bank, black against it all, rimmed on the sun side
517    for (let k = 0; k < N; k++) {
518      if (!fg[k]) continue;
519      const s = fgShade[k];
520      FR[k] = 0.02 + 0.05 * s;
521      FG[k] = 0.04 + 0.06 * s;
522      FB[k] = 0.05 + 0.06 * s;
523      floor[k] = 0;
524      const e = fgRim[k];
525      if (e > 0 && s === 1) {
526        // warm on the side facing the sun
527        FR[k] = mix(FR[k], 0.62, e), FG[k] = mix(FG[k], 0.4, e), FB[k] = mix(FB[k], 0.38, e);
528        floor[k] = 0.12 * e;
529      } else if (e > 0 && s === 0.5) {
530        // cool lilac from the sky on the other
531        FR[k] = mix(FR[k], 0.2, e), FG[k] = mix(FG[k], 0.22, e), FB[k] = mix(FB[k], 0.36, e);
532        floor[k] = 0.22 * e;
533      }
534      fade[k] = 1;
535    }
536
537    for (let r = 0; r < H; r++) {
538      for (let x = 0; x < W; x++) {
539        const k = r * W + x;
540        const cr = FR[k], cg = FG[k], cbl = FB[k], fl = floor[k];
541        const peak = Math.max(cr, cg, cbl, 1e-4);
542        const level = clamp(fl + (1 - fl) * Math.pow(peak, 1.1)) * fade[k];
543        const step = Math.max(0, Math.min(3, Math.round(level * 3 + BAYER[(r & 3) * 4 + (x & 3)])));
544        out[k] = DOTS[step];
545        if (color) {
546          const want = step ? Math.min(1, (level + 0.06) / COVER[step]) : 0;
547          // dim cells keep some of their darkness in the colour too, so the
548          // shadows sit back in deep blues rather than as a bright fine screen
549          const s = ((0.3 + 0.7 * want) * mix(0.5, 1, smooth(0.08, 0.5, peak))) / peak;
550          color[k] = nearest(clamp(cr * s), clamp(cg * s), clamp(cbl * s));
551        }
552      }
553    }
554    const lines: string[] = [];
555    for (let r = 0; r < H; r++) lines.push(out.slice(r * W, (r + 1) * W).join(""));
556    return lines.join("\n");
557  };
558}
559
hooks/vendor/pieces/alpine-linux.ts 181 lines
1/*
2 * alpine linux: the Alpine Linux logo, a mountain in a hexagon, with a scan
3 * line running down it every few seconds that scrambles what it passes.
4 *
5 * Drawn from Simple Icons' alpinelinux.svg (CC0): each cell holds the
6 * character whose shape best matches the logo's edge through it, and 8 where
7 * the logo is solid. On a canvas it takes the logo's colours, lifted on a dark
8 * page where they would sink into it; in a <pre> it is one ink, from the same
9 * drawing. The logo is a trademark of its owner, shown here to name the
10 * distribution.
11 */
12import type { Frame, Meta } from "../types.ts";
13
14export interface AlpineLinuxOptions {
15  [key: string]: unknown;
16  /** Seconds between scans; 0 keeps the logo still. */
17  scan: number;
18}
19
20export const meta = {
21  name: "alpine linux",
22  category: "distros",
23  note: "the mountain in a hexagon, scanned now and then",
24  cols: 64,
25  rows: 28,
26  fps: 30,
27  options: { scan: 5 },
28  // The logo's colour for a light page, then for a dark one; each as drawn, then twice lighter for the scan.
29  palette: [
30    "#0d597f", "#6293ac", "#b6cdd9", "#0f6692", "#639cb8", "#b7d1de",
31  ],
32} satisfies Meta<AlpineLinuxOptions>;
33
34// In a <pre>, in the page's own colour.
35const MONO = String.raw`
36
37                q888888888888888888888888888888p
38               p88888888888888888888888888888888q,
39             _p8888888888888888888888888888888888q,
40            _88888888888888888888888888888888888888,
41           _8888888888888888888888888888888888888888,
42          )888888888888888888888888888888888888888888(
43         q88888888888888888888888888888888888888888888p
44        p8888888888888888P'"888888888888888888888888888q
45      .p888888888888888P'    "8888888P "8888888888888888q,
46     _p88888888888888P'        "888P     "888888888888888q,
47    _88888888888888P'     _      "\,       "888888888888888,
48   _8888888888888P'     _p88_      "(,_      "88888888888888_
49  q888888888888P'     _p888888_      "8q_      "8888888888888p
50  "8888888888P'     _pP"88888888_      "8q_      "88888888888"
51   "8888888P'     _pP" |8888888888_      "8q_      "88888888"
52    "88888q_    _p8q_, |888888888888_    __p88_    __p88888"
53     '88888888q88888888888888888888888qq88888888qq88888888'
54      '8888888888888888888888888888888888888888888888888P'
55        O8888888888888888888888888888888888888888888888P
56         )88888888888888888888888888888888888888888888P
57          "888888888888888888888888888888888888888888"
58           "8888888888888888888888888888888888888888"
59            "88888888888888888888888888888888888888"
60             '88888888888888888888888888888888888P'
61               888888888888888888888888888888888P
62                Y888888888888888888888888888888P
63
64`;
65
66// On a canvas, and the colour of each cell: an index into the logo's colours.
67const ART = String.raw`
68
69                q888888888888888888888888888888p
70               p88888888888888888888888888888888q,
71             _p8888888888888888888888888888888888q,
72            _88888888888888888888888888888888888888,
73           _8888888888888888888888888888888888888888,
74          )888888888888888888888888888888888888888888(
75         q88888888888888888888888888888888888888888888p
76        p8888888888888888P'"888888888888888888888888888q
77      .p888888888888888P'    "8888888P "8888888888888888q,
78     _p88888888888888P'        "888P     "888888888888888q,
79    _88888888888888P'     _      "\,       "888888888888888,
80   _8888888888888P'     _p88_      "(,_      "88888888888888_
81  q888888888888P'     _p888888_      "8q_      "8888888888888p
82  "8888888888P'     _pP"88888888_      "8q_      "88888888888"
83   "8888888P'     _pP" |8888888888_      "8q_      "88888888"
84    "88888q_    _p8q_, |888888888888_    __p88_    __p88888"
85     '88888888q88888888888888888888888qq88888888qq88888888'
86      '8888888888888888888888888888888888888888888888888P'
87        O8888888888888888888888888888888888888888888888P
88         )88888888888888888888888888888888888888888888P
89          "888888888888888888888888888888888888888888"
90           "8888888888888888888888888888888888888888"
91            "88888888888888888888888888888888888888"
92             '88888888888888888888888888888888888P'
93               888888888888888888888888888888888P
94                Y888888888888888888888888888888P
95
96`;
97const INK = String.raw`
98
99                00000000000000000000000000000000
100               00000000000000000000000000000000000
101             00000000000000000000000000000000000000
102            0000000000000000000000000000000000000000
103           000000000000000000000000000000000000000000
104          00000000000000000000000000000000000000000000
105         0000000000000000000000000000000000000000000000
106        000000000000000000000000000000000000000000000000
107      0000000000000000000    000000000 0000000000000000000
108     000000000000000000        00000     000000000000000000
109    00000000000000000     0      000       00000000000000000
110   0000000000000000     00000      0000      0000000000000000
111  000000000000000     000000000      0000      000000000000000
112  0000000000000     0000000000000      0000      0000000000000
113   0000000000     0000 000000000000      0000      0000000000
114    00000000    000000 00000000000000    000000    000000000
115     000000000000000000000000000000000000000000000000000000
116      0000000000000000000000000000000000000000000000000000
117        000000000000000000000000000000000000000000000000
118         0000000000000000000000000000000000000000000000
119          00000000000000000000000000000000000000000000
120           000000000000000000000000000000000000000000
121            0000000000000000000000000000000000000000
122             00000000000000000000000000000000000000
123               0000000000000000000000000000000000
124                00000000000000000000000000000000
125
126`;
127
128const START = 0.5; // seconds before the first scan
129const PASS = 2; // seconds a scan takes, top to bottom
130const HEAD = 2; // rows the line scrambles
131const TRAIL = 5; // rows of afterglow behind it
132const NOISE = "#$%&*+<=>?@[]{}"; // what the line scrambles the logo into
133
134const lines = (art: string) => art.slice(1, -1).split("\n").map((line) => line.padEnd(meta.cols));
135
136function hash(x: number, y: number, k: number) {
137  let h = Math.imul(x, 73856093) ^ Math.imul(y, 19349663) ^ Math.imul(k, 83492791);
138  h = Math.imul(h ^ (h >>> 13), 1274126177);
139  return (h ^ (h >>> 16)) >>> 0;
140}
141
142export default function alpineLinux({ scan = meta.options.scan }: Partial<AlpineLinuxOptions> = {}): Frame {
143  const { cols, rows, fps } = meta;
144  const mono = lines(MONO), art = lines(ART), ink = lines(INK);
145  const n = meta.palette.length / 6;
146  const every = scan > 0 ? Math.max(scan, PASS + 0.5) : 0;
147  // The line runs from the logo's first row to past its last, rather than the whole frame.
148  let first = rows, last = -1;
149  for (const pic of [mono, art])
150    pic.forEach((line, y) => {
151      if (line.trim()) (first = Math.min(first, y)), (last = Math.max(last, y));
152    });
153  const span = last - first + 1 + HEAD + TRAIL;
154
155  return (t, { paper = false, color } = {}) => {
156    const pic = color ? art : mono;
157    const since = t - START;
158    const at = every && since >= 0 ? first + (span * (since % every)) / PASS : -Infinity;
159    const tick = Math.floor((t * fps) / 2); // the noise changes every other frame
160    const out: string[] = [];
161    for (let y = 0; y < rows; y++) {
162      const d = at - y; // rows since the line reached this one
163      const level = d >= 0 && d < HEAD ? 2 : d >= HEAD && d < HEAD + TRAIL ? 1 : 0;
164      let line = "";
165      for (let x = 0; x < cols; x++) {
166        let ch = pic[y][x];
167        if (ch !== " ") {
168          if (level === 2) {
169            const h = hash(x, y, tick);
170            if (h % 4) ch = NOISE[(h >>> 2) % NOISE.length];
171          }
172          if (color) color[y * cols + x] = (paper ? 0 : 3 * n) + level * n + parseInt(ink[y][x], 36);
173        }
174        line += ch;
175      }
176      out.push(line);
177    }
178    return out.join("\n");
179  };
180}
181
hooks/vendor/pieces/analog-clock.ts 159 lines
1/*
2 * analog-clock: a round wall clock keeping local time. A lit bezel, a tick
3 * for every minute and hour, and hour, minute and ticking second hands.
4 */
5import type { Frame, Meta } from "../types.ts";
6
7export interface AnalogClockOptions {
8  [key: string]: unknown;
9  numerals: boolean;
10  seconds: boolean;
11}
12
13export const meta = {
14  name: "analog clock",
15  category: "objects",
16  note: "a round clock face showing the local time, seconds ticking",
17  cols: 47,
18  rows: 23,
19  fps: 8,
20  options: { numerals: true, seconds: true },
21  clock: true,
22} satisfies Meta<AnalogClockOptions>;
23
24const RAMP = ".,-~:;=!*#$@";
25const LO = 3, HI = 10; // the stretch of the ramp the bezel uses, so no part of it fades out
26const R = 21; // the bezel's middle, in columns; the face is drawn in columns, y up
27const LM = Math.hypot(-0.5, 0.6, 0.62);
28const [LX, LY, LZ] = [-0.5 / LM, 0.6 / LM, 0.62 / LM];
29
30// How each hand is drawn: upright, rising and falling strokes, the run along a
31// row where it is shallow, the run where it lies flat in one row, and how many
32// columns a row it may cross before it is drawn as runs.
33interface Hand {
34  up: string;
35  rise: string;
36  fall: string;
37  run: string;
38  flat: string;
39  wide?: boolean;
40  span: number;
41}
42const HOUR: Hand = { up: "║", rise: "/", fall: "\\", run: "=", flat: "=", wide: true, span: 2.4 };
43const MINUTE: Hand = { up: "|", rise: "/", fall: "\\", run: "_", flat: "-", span: 1.3 };
44const SECOND: Hand = { up: "│", rise: "╱", fall: "╲", run: "─", flat: "─", span: 1.3 };
45
46export default function analogClock({ numerals = true, seconds = true }: Partial<AnalogClockOptions> = {}): Frame {
47  const { cols, rows } = meta;
48  const cx = cols / 2, cy = rows / 2;
49  const start = Date.now();
50  const g: string[][] = Array.from({ length: rows }, () => new Array(cols));
51  const set = (c: number, r: number, ch: string) => {
52    if (c >= 0 && c < cols && r >= 0 && r < rows) g[r][c] = ch;
53  };
54  const put = (x: number, y: number, ch: string) => set(Math.floor(cx + x), Math.floor(cy - y / 2), ch);
55
56  // A hand along clock angle a (radians from twelve, clockwise) from r0 to r1
57  // columns out, taken a row at a time. Steep, it is one stroke a row; shallow,
58  // a run along the row stepped up or down where it crosses into the next.
59  // Returns the cell it ends in.
60  const hand = (a: number, r0: number, r1: number, st: Hand) => {
61    let end: [number, number] | null = null;
62    const sx = Math.sin(a), sy = Math.cos(a);
63    const steep = Math.abs(sy) > 4 * Math.abs(sx); // within about 14 degrees of upright
64    const rise = sx * sy > 0;
65    const runs = 2 * Math.abs(sx) > st.span * Math.abs(sy); // columns crossed in a full row
66    const yAt = (s: number) => cy - (sy * s) / 2, xAt = (s: number) => cx + sx * s;
67    const ya = yAt(r0), yb = yAt(r1);
68    for (let r = Math.floor(Math.min(ya, yb)); r <= Math.floor(Math.max(ya, yb)); r++) {
69      let s0 = r0, s1 = r1;
70      if (Math.abs(sy) > 1e-6) {
71        const p = ((cy - r) * 2) / sy, q = ((cy - r - 1) * 2) / sy;
72        s0 = Math.max(r0, Math.min(p, q));
73        s1 = Math.min(r1, Math.max(p, q));
74        if (s1 - s0 < 1e-6) continue;
75      }
76      const xa = xAt(s0), xb = xAt(s1);
77      const lo = Math.min(xa, xb), hi = Math.max(xa, xb), mid = (lo + hi) / 2;
78      const last = s1 > r1 - 1e-6;
79      if (!runs) {
80        const c = Math.floor(mid);
81        set(c, r, steep ? st.up : rise ? st.rise : st.fall);
82        if (st.wide && !steep) set(mid - c < 0.5 ? c - 1 : c + 1, r, rise ? st.rise : st.fall);
83        if (last) end = [c, r];
84        continue;
85      }
86      // Which ends of this row's run meet the row above or below.
87      const edgeLo = (xa < xb ? s0 : s1) > r0 + 1e-6 && (xa < xb ? s0 : s1) < r1 - 1e-6;
88      const edgeHi = (xa < xb ? s1 : s0) > r0 + 1e-6 && (xa < xb ? s1 : s0) < r1 - 1e-6;
89      const ch = edgeLo || edgeHi ? st.run : st.flat;
90      const c0 = Math.floor(lo), c1 = Math.floor(hi - 1e-6);
91      for (let c = c0; c <= c1; c++) set(c, r, ch);
92      if (last) end = [Math.max(c0, Math.min(c1, Math.floor(xAt(r1)))), r];
93      if (st.wide) continue;
94      if (rise && edgeHi) set(c1, r, st.rise);
95      if (!rise && edgeLo) set(c0, r, st.fall);
96    }
97    return end;
98  };
99
100  // The bezel is a fixed picture: a torus cut across, lit from the upper left,
101  // sampled four times a cell. Shading only depends on paper, so keep both.
102  const bezel = [false, true].map((paper) => {
103    const b: (string | null)[][] = Array.from({ length: rows }, () => new Array(cols).fill(null));
104    for (let r = 0; r < rows; r++)
105      for (let c = 0; c < cols; c++) {
106        let sum = 0, hit = 0;
107        for (const [ox, oy] of [[0.25, 0.25], [0.75, 0.25], [0.25, 0.75], [0.75, 0.75]]) {
108          const x = c + ox - cx, y = (cy - (r + oy)) * 2;
109          const d = Math.hypot(x, y), s = (d - R) / 1.3;
110          if (Math.abs(s) >= 1) continue;
111          const z = Math.sqrt(1 - s * s);
112          sum += Math.max(0, ((s * x) / d) * LX + ((s * y) / d) * LY + z * LZ);
113          hit++;
114        }
115        if (hit < 2) continue;
116        const i = Math.round(LO + (sum / hit) * (HI - LO));
117        b[r][c] = RAMP[paper ? RAMP.length - 1 - i : i];
118      }
119    return b;
120  });
121
122  return (t, { paper = false } = {}) => {
123    // The wall clock, or play time from the start if that is ahead of it.
124    const now = new Date(Math.max(Date.now(), start + t * 1000));
125    const sec = now.getSeconds(), min = now.getMinutes() + sec / 60;
126    const hr = (now.getHours() % 12) + min / 60;
127    const face = bezel[paper ? 1 : 0];
128    for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) g[r][c] = face[r][c] ?? " ";
129
130    // A dot for each minute and a one-cell stroke for each hour.
131    for (let i = 0; i < 60; i++) {
132      const a = (i * Math.PI) / 30, sx = Math.sin(a), sy = Math.cos(a);
133      if (i % 5) put(sx * 18, sy * 18, "·");
134      else if (!numerals || i % 15) put(sx * 17.4, sy * 17.4, Math.abs(sy) > 0.9 ? "|" : Math.abs(sy) < 0.1 ? "-" : sx * sy > 0 ? "/" : "\\");
135    }
136    const ah = (hr * Math.PI) / 6, am = (min * Math.PI) / 30, as = (sec * Math.PI) / 30;
137    hand(ah, 1.5, 9.5, HOUR);
138    hand(am, 1.5, 15.5, MINUTE);
139    // The second hand: a fine line with a ring at its tip and a weight on its
140    // tail, a half block in whichever half of its cell the tail ends.
141    const tip = seconds && hand(as, 1.5, 14.8, SECOND);
142    if (seconds) {
143      const w = hand(as + Math.PI, 1.5, 4.4, SECOND), f = cy + Math.cos(as) * 2.2;
144      if (w) set(w[0], w[1], f - Math.floor(f) < 0.5 ? "▀" : "▄");
145    }
146    // The numerals sit over the hour and minute hands; the ring passes over them.
147    if (numerals) {
148      put(-1, 15.6, "1");
149      put(0, 15.6, "2");
150      put(15.2, 0, "3");
151      put(0, -15.6, "6");
152      put(-15.2, 0, "9");
153    }
154    if (tip) set(tip[0], tip[1], "o");
155    put(0, 0, "@");
156    return g.map((row) => row.join("")).join("\n");
157  };
158}
159
hooks/vendor/pieces/apple.ts 171 lines
1/*
2 * apple: the Apple logo, an apple with a bite out of it, with a glint crossing
3 * it every few seconds.
4 *
5 * Drawn from Simple Icons' apple.svg (CC0): each cell holds the character
6 * whose shape best matches the logo's edge through it, and 8 where the logo is
7 * solid. On a canvas it is black, and a light grey on a dark page, where black
8 * would sink into it; in a <pre> it is one ink, from the same drawing. The
9 * logo is a trademark of its owner, shown here to name the company.
10 */
11import type { Frame, Meta } from "../types.ts";
12
13export interface AppleOptions {
14  [key: string]: unknown;
15  /** Seconds between glints; 0 keeps the logo still. */
16  shine: number;
17}
18
19export const meta = {
20  name: "apple",
21  category: "companies",
22  note: "the apple with a bite out of it, glinting now and then",
23  cols: 46,
24  rows: 28,
25  fps: 30,
26  options: { shine: 5 },
27  // The logo's colour for a light page, then for a dark one; each as drawn, then twice lighter for the glint.
28  palette: [
29    "#000000", "#595959", "#b3b3b3", "#e8ebef", "#f0f2f5", "#f8f9fa",
30  ],
31} satisfies Meta<AppleOptions>;
32
33// In a <pre>, in the page's own colour.
34const MONO = String.raw`
35
36                            __pp8|
37                          _p88888'
38                        _p888888P
39                       _8888888P
40                       8888888"
41                      .8888"'
42          __ppqqq__,       __pppqqqq__
43       _p888888888888qqppp8888888888888q_
44     _88888888888888888888888888888888888q,
45    p88888888888888888888888888888888888P"
46   q88888888888888888888888888888888888"
47  |88888888888888888888888888888888888'
48  d8888888888888888888888888888888888|
49  88888888888888888888888888888888888'
50  88888888888888888888888888888888888,
51  88888888888888888888888888888888888p
52  d88888888888888888888888888888888888p
53  '888888888888888888888888888888888888q_
54   )8888888888888888888888888888888888888q__
55    888888888888888888888888888888888888888P
56    '8888888888888888888888888888888888888P
57     '88888888888888888888888888888888888P'
58       O88888888888888888888888888888888P
59        "888888888888888888888888888888"
60         'Y88888888888888888888888888P'
61           '"8888PP"'      '"Y8888P"'
62
63`;
64
65// On a canvas, and the colour of each cell: an index into the logo's colours.
66const ART = String.raw`
67
68                            __pp8|
69                          _p88888'
70                        _p888888P
71                       _8888888P
72                       8888888"
73                      .8888"'
74          __ppqqq__,       __pppqqqq__
75       _p888888888888qqppp8888888888888q_
76     _88888888888888888888888888888888888q,
77    p88888888888888888888888888888888888P"
78   q88888888888888888888888888888888888"
79  |88888888888888888888888888888888888'
80  d8888888888888888888888888888888888|
81  88888888888888888888888888888888888'
82  88888888888888888888888888888888888,
83  88888888888888888888888888888888888p
84  d88888888888888888888888888888888888p
85  '888888888888888888888888888888888888q_
86   )8888888888888888888888888888888888888q__
87    888888888888888888888888888888888888888P
88    '8888888888888888888888888888888888888P
89     '88888888888888888888888888888888888P'
90       O88888888888888888888888888888888P
91        "888888888888888888888888888888"
92         'Y88888888888888888888888888P'
93           '"8888PP"'      '"Y8888P"'
94
95`;
96const INK = String.raw`
97
98                            000000
99                          00000000
100                        000000000
101                       000000000
102                       00000000
103                      0000000
104          0000000000       00000000000
105       0000000000000000000000000000000000
106     00000000000000000000000000000000000000
107    00000000000000000000000000000000000000
108   0000000000000000000000000000000000000
109  0000000000000000000000000000000000000
110  000000000000000000000000000000000000
111  000000000000000000000000000000000000
112  000000000000000000000000000000000000
113  000000000000000000000000000000000000
114  0000000000000000000000000000000000000
115  000000000000000000000000000000000000000
116   00000000000000000000000000000000000000000
117    0000000000000000000000000000000000000000
118    000000000000000000000000000000000000000
119     00000000000000000000000000000000000000
120       0000000000000000000000000000000000
121        00000000000000000000000000000000
122         000000000000000000000000000000
123           0000000000      0000000000
124
125`;
126
127const START = 0.5; // seconds before the first glint
128const PASS = 2; // seconds a glint takes to cross
129const HALF = 5; // half its width, in cells
130const LEAN = 0.9; // cells it shifts left a row down, so it leans like a slash
131const SOLID = "8dbqpPYOo0"; // what the glint turns to slashes; thin edges keep their shape
132
133const lines = (art: string) => art.slice(1, -1).split("\n").map((line) => line.padEnd(meta.cols));
134
135export default function apple({ shine = meta.options.shine }: Partial<AppleOptions> = {}): Frame {
136  const { cols, rows } = meta;
137  const mono = lines(MONO), art = lines(ART), ink = lines(INK);
138  const n = meta.palette.length / 6;
139  const every = shine > 0 ? Math.max(shine, PASS + 0.5) : 0;
140  // The glint crosses the logo's ink, edge to edge, rather than the whole frame.
141  let lo = Infinity, hi = -Infinity;
142  for (const pic of [mono, art])
143    pic.forEach((line, y) => {
144      for (let x = 0; x < cols; x++) if (line[x] !== " ") (lo = Math.min(lo, x + LEAN * y)), (hi = Math.max(hi, x + LEAN * y));
145    });
146  const span = hi - lo + 2 * HALF;
147
148  return (t, { paper = false, color } = {}) => {
149    const pic = color ? art : mono;
150    const since = t - START;
151    const at = every && since >= 0 ? lo - HALF + (span * (since % every)) / PASS : -Infinity;
152    const out: string[] = [];
153    for (let y = 0; y < rows; y++) {
154      let line = "";
155      for (let x = 0; x < cols; x++) {
156        let ch = pic[y][x];
157        if (ch !== " ") {
158          const d = Math.abs(x + LEAN * y - at);
159          let k = d < HALF ? 1 - d / HALF : 0;
160          k = k * k * (3 - 2 * k);
161          if (k > 0.55 && SOLID.includes(ch)) ch = "/";
162          if (color) color[y * cols + x] = (paper ? 0 : 3 * n) + (k > 0.6 ? 2 : k > 0.25 ? 1 : 0) * n + parseInt(ink[y][x], 36);
163        }
164        line += ch;
165      }
166      out.push(line);
167    }
168    return out.join("\n");
169  };
170}
171
hooks/vendor/pieces/aquarium.ts 120 lines
1/*
2 * aquarium: a fish tank. Fish of a few kinds swim its length, drift between
3 * depths and turn about, bubbles stream up from a stone, and weed sways.
4 */
5import type { Frame, Meta } from "../types.ts";
6
7export const meta = {
8  name: "aquarium",
9  category: "creatures",
10  note: "fish swimming and turning in a tank, bubbles and weed",
11  cols: 60,
12  rows: 20,
13  fps: 15,
14} satisfies Meta;
15
16const LOOP = 48; // seconds; every period below divides it
17const FLIP: Record<string, string> = { "<": ">", ">": "<", "(": ")", ")": "(", "/": "\\", "\\": "/", "`": "'", "'": "`", "{": "}", "}": "{" };
18// Fish, facing right, with the face-on view shown while they turn, farthest
19// first: [art, face-on art, row, from column, to column, seconds there and
20// back, phase, rows it drifts up and down over the loop, drift phase].
21const FISH: [art: string[], turn: string[], row: number, from: number, to: number, period: number, phase: number, drift: number, dp: number][] = [
22  [["><>"], ["<o>"], 5.2, 6, 54, 12, 0.8, 1.5, 1],
23  [["><>"], ["<o>"], 6.6, 3, 51, 12, 0.775, 1.5, 1.1],
24  [["><>"], ["<o>"], 8, 8, 56, 12, 0.79, 1.5, 0.9],
25  [["><(((°>"], ["<(°o°)>"], 3.3, 3, 50, 24, 0.1, 1.2, 1],
26  [["><(°>"], ["(°o°)"], 15.6, 4, 40, 48, 0.55, 0.4, 3],
27  [["    /\\", " ><(  °>", "    \\/"], ["/^\\", "(°o°)", "\\v/"], 9.8, 10, 51, 24, 0.35, 0.9, 4],
28  [["   _.-._", "><(     °)", "   `-.-'"], [".-^-.", "(°o°)", "`-v-'"], 12.8, 2, 46, 16, 0.6, 0.9, 4],
29];
30// Clumps of weed, [root column, [height, lean] for each blade, phase].
31const WEEDS: [root: number, blades: [height: number, lean: number][], phase: number][] = [
32  [6, [[11, 0.6], [8, -1.6], [6, 2]], 0],
33  [13, [[6, 1.2], [4, -1.2]], 1.7],
34  [25, [[4, -1], [3, 1]], 0.9],
35  [37, [[9, 1.2], [6, -1.4], [4, 2.2]], 2.4],
36  [55, [[7, -1.2], [5, 0.8]], 0.4],
37];
38const STONE: [row: number, col: number, art: string][] = [[15, 43, " .--._"], [16, 42, "(  .  `)"]];
39const NOZZLE = 46; // where the bubbles leave the stone
40
41const hash = (a: number, b: number): number => {
42  let h = Math.imul(a, 0x27d4eb2d) ^ Math.imul(b, 0x165667b1);
43  h = Math.imul(h ^ (h >>> 15), 0x85ebca6b);
44  h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
45  return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
46};
47const mirror = (art: string[]): string[] => {
48  const w = Math.max(...art.map((l) => l.length));
49  return art.map((l) => [...l.padEnd(w)].reverse().map((c) => FLIP[c] || c).join(""));
50};
51
52export default function aquarium(): Frame {
53  const { cols, rows } = meta;
54  const TAU = Math.PI * 2;
55  const SURFACE = 1, FLOOR = 17;
56  const fish = FISH.map(([art, turn, row, from, to, period, phase, drift, dp]) => ({ art, back: mirror(art), turn, row, from, to, period, phase, drift, dp }));
57  // Gravel: a fine bed with coarser stones lying in it here and there.
58  const gravel = [0, 1].map((k) => Array.from({ length: cols - 2 }, (_, c) => {
59    const n = hash(c, k + 3);
60    return k ? (n < 0.14 ? "o" : n < 0.2 ? "O" : n < 0.27 ? "°" : n < 0.4 ? "." : " ") : n < 0.55 ? "." : n < 0.75 ? "," : "·";
61  }));
62
63  return (t) => {
64    const u = ((t % LOOP) + LOOP) % LOOP;
65    const g = Array.from({ length: rows }, () => new Array<string>(cols).fill(" "));
66    // Art is opaque from its first mark to its last on each line.
67    const put = (r: number, c: number, s: string) => {
68      if (r <= SURFACE || r >= rows - 1) return;
69      for (let i = Math.max(0, s.search(/\S/)); i < s.trimEnd().length; i++) if (c + i > 0 && c + i < cols - 1) g[r][c + i] = s[i];
70    };
71    // The tank: glass, rim, and the water's surface rippling under it.
72    g[0] = ["┌", ..."─".repeat(cols - 2), "┐"];
73    g[rows - 1] = ["└", ..."─".repeat(cols - 2), "┘"];
74    for (let r = 1; r < rows - 1; r++) g[r][0] = g[r][cols - 1] = "│";
75    for (let c = 1; c < cols - 1; c++) g[SURFACE][c] = Math.sin(c * 0.55 - (u * TAU) / 4) + 0.5 * Math.sin(c * 0.23 + (u * TAU) / 6) > 0.3 ? "~" : "-";
76    for (let k = 0; k < 2; k++) for (let c = 1; c < cols - 1; c++) g[FLOOR + k][c] = gravel[k][c - 1];
77
78    // Weed: blades that rise straight from the root and bend further toward
79    // the tip, the tip swinging two or three columns as the water moves.
80    const weed = ([c0, blades, ph]: (typeof WEEDS)[number]) => {
81      for (const [h, lean] of blades) {
82        let px = c0 + 0.5;
83        for (let k = 1; k <= h; k++) {
84          const f = k / h, sway = 1.4 * Math.sin((u * TAU) / 6 + ph - f * 0.7 + lean * 0.2);
85          const x = c0 + 0.5 + lean * f ** 1.5 + sway * f * f, d = x - px;
86          put(FLOOR - k, Math.floor(x), d > 0.5 ? "/" : d > 0.17 ? "(" : d < -0.5 ? "\\" : d < -0.17 ? ")" : "|");
87          px = x;
88        }
89      }
90    };
91    WEEDS.slice(0, -1).forEach(weed);
92    STONE.forEach(([r, c, s]) => put(r, c, s));
93
94    // Each fish swims there and back, easing into each turn, and drifts up
95    // and down over the loop; for a moment mid-turn it faces out of the glass.
96    for (const f of fish) {
97      const th = TAU * (u / f.period + f.phase);
98      const p = Math.asin(0.995 * Math.sin(th)) / (Math.PI / 2);
99      const x = Math.round(f.from + ((p + 1) / 2) * (f.to - f.from));
100      const y = Math.round(f.row + f.drift * Math.sin((TAU * u) / LOOP + f.dp) + 0.35 * Math.sin((TAU * u) / 6 + f.dp * 3));
101      const art = Math.abs(Math.cos(th)) < 0.09 ? f.turn : Math.cos(th) > 0 ? f.art : f.back;
102      const w = Math.max(...f.art.map((l) => l.length)), tw = Math.max(...art.map((l) => l.length));
103      art.forEach((line, i) => put(y + i - (art.length >> 1), x + ((w - tw) >> 1), line));
104    }
105
106    // Bubbles leave the stone in a stream, wobbling and swelling as they rise,
107    // and break at the surface.
108    for (let k = 0; k < 80; k++) {
109      const age = (u - k * (LOOP / 80) + LOOP) % LOOP;
110      const y = 14.6 - age * (2.6 + hash(k, 1));
111      if (y < SURFACE + 1) continue;
112      const x = NOZZLE + Math.round(0.7 * Math.sin(age * 3.5 + k) + (hash(k, 2) - 0.5));
113      const r = Math.floor(y);
114      if (g[r][x] === " ") g[r][x] = y > 11 ? "." : y > 6 ? "o" : "O";
115    }
116    weed(WEEDS[WEEDS.length - 1]); // one in front of the fish
117    return g.map((row) => row.join("")).join("\n");
118  };
119}
120