Clawd acts out the spinner's word above it while Claude works: cooking for Sautéing, dancing for Vibing, pacing for Pondering, 11 acts across all 179 words.

A Claude Code mod: while Claude works, Clawd acts out the spinner word above the spinner line.
Claude Code picks a random spinner word each turn. actFor() in hooks/acts.ts matches it against stem lists (saut|whisk|brew… → cook, ponder|mull|ruminat… → think, …). The first match wins.
/plugin marketplace add Saiharsharudra03/clawd-spinner
/plugin install clawd-spinner@clawd-spinner
Or from a clone: claude --plugin-dir ./clawd-spinner.
Terminal only. Needs a window at least 54 columns wide; narrower, the stock spinner shows.
claude plugin validate .
claude plugin test .
/plugin-types .claude-plugin/types regenerates the editor typings (git-ignored).
Spinner word list: shanraisshan/claude-code-best-practice (reports/claude-spinner-verbs-and-tips.md). Clawd is Anthropic's mascot; this is an unofficial fan mod.
MIT licensed.
hooks/register.tsx 76 lines1import type { EngineInterface, Register } from 'claude-code'
2
3import * as A from './acts'
4
5const FRAME_MS = 83 // ~12 frames a second
6const EDGE = 4 // columns kept clear at the right edge
7
8type $ = EngineInterface
9
10/** The spinner row while a turn runs. Module state: a reload starts the act over, which is fine. */
11const spin = {
12 turnAt: 0, word: '', working: false, last: '', blit: true,
13 mount: null as { requestId: string; columns: number } | null,
14}
15
16async function paint($: $) {
17 const m = spin.mount
18 if (!m || !spin.working) return
19 const now = await $.clock.now()
20 const cells = A.encode(A.frame({ word: spin.word, t: now - spin.turnAt }, m.columns))
21 if (cells === spin.last) return
22 spin.last = cells
23 if (!spin.blit) {
24 // This row refused blits: redraw it instead (the engine folds these to its own rate).
25 $.ui.invalidate('ui.render')
26 return
27 }
28 const r = await $.ui.blit({ requestId: m.requestId, key: 'act', cells })
29 if ('deny' in r && r.deny) {
30 spin.blit = false
31 $.ui.invalidate('ui.render')
32 }
33}
34
35export const register: Register = on => {
36 on('session.start', async ($, e, next) => {
37 if (e.isInteractive) $.clock.every(FRAME_MS, () => void paint($))
38 return next(e)
39 })
40
41 on('prompt.submit', async ($, e, next) => {
42 spin.turnAt = await $.clock.now()
43 spin.working = true
44 return next(e)
45 })
46
47 on('turn.complete', async ($, e, next) => {
48 const done = await next(e)
49 if (!e.agentId) [spin.working, spin.mount] = [false, null]
50 return done
51 })
52
53 // Clawd acts out the word, and under him the spinner's line: the word and the turn's time.
54 on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
55 const columns = (e.viewport?.columns ?? 0) - EDGE
56 if (e.surface !== 'terminal' || columns < 50) {
57 spin.mount = null
58 return next(e)
59 }
60 const { Raster, Box, Text } = $.ui.resolve(e)
61 const now = await $.clock.now()
62 if (!spin.working) [spin.working, spin.turnAt] = [true, now] // a reload mid-turn
63 if (spin.mount?.requestId !== e.requestId) spin.blit = true
64 spin.word = e.props.word
65 spin.mount = { requestId: e.requestId, columns }
66 const cells = A.encode(A.frame({ word: spin.word, t: now - spin.turnAt }, columns))
67 spin.last = cells
68 return (
69 <Box flexDirection="column">
70 <Raster key="act" columns={columns} rows={A.ROWS} cells={cells} />
71 <Text><Text color="#d97757">✻ {e.props.message ?? e.props.word}{e.props.suffix}</Text><Text dimColor> ({A.elapsed(now - spin.turnAt)})</Text></Text>
72 </Box>
73 )
74 })
75}
76hooks/acts.ts 440 lines1// Clawd acts out the spinner's word ("Sautéing", "Vibing", "Pondering"...), in the spinner's row.
2// Pure: frame() turns (the word, time, width) into Raster cells. Each cell is two pixels stacked (▀).
3
4export type Act = 'cook' | 'think' | 'dance' | 'build' | 'compute' | 'stroll' | 'herd' | 'weather' | 'garden' | 'space' | 'magic'
5
6export type Moment = {
7 word: string // the spinner's word, as drawn
8 t: number // ms since the turn began
9}
10
11export const ROWS = 8
12export const PH = ROWS * 2
13const DEF = 0x01000000 // the terminal's own colour: transparent here
14const GROUND = PH - 1
15
16// ------------------------------------------------------------------ which act
17
18// Stems of Claude Code's 179 spinner words, grouped by what Clawd can act out. Anything else: magic.
19const STEMS: [Act, RegExp][] = [
20 ['cook', /bak|blanch|brew|bunn|caramel|concoct|cook|drizzl|ferment|flamb|frost|garnish|infus|marinat|percolat|proof|saut|smoosh|stew|temper|whisk|zest/],
21 ['think', /cerebrat|cogitat|consider|contemplat|deciph|deliberat|determin|elucidat|envision|ideat|imagin|infer|mull|musing|philosoph|ponder|pontificat|puzzl|ruminat|think|perus|bloviat|befuddl|flummox|combobulat|newspaper/],
22 ['dance', /bebop|boogie|choreograph|frolic|groov|jitterbug|moonwalk|sock-hop|vib|razzle|razzmatazz|hullaballoo|tomfool|topsy|wibbl|undulat|noodl|flibbertigibbet|harmoniz|compos|orchestrat|honk|improvis|twist/],
23 ['build', /architect|bootstrap|craft|creat|forg|form|generat|sketch|doodl|embellish|tinker|accomplish|action|actualiz|^doing|effect|working|finagl|fiddle|boondoggl|gitif/],
24 ['compute', /calculat|churn|comput|crunch|hash|process|reticulat|spinn|whirr|(?<!photo)synthes|quantum|ioniz|nucleat|crystal|coalesc/],
25 ['herd', /herd|wrangl|muster/],
26 ['stroll', /dilly|gallivant|lollygag|meander|mosey|perambulat|putter|scamper|schlep|scurr|waddl|wander|zigzag|gallop|slither|pounc|spelunk/],
27 ['weather', /billow|evaporat|gust|mist|nebuliz|precipitat|thunder|cascad|flow|ebb|whirlpool|swirl|unfurl|osmos|sublimat/],
28 ['garden', /cultivat|germinat|photosynth|pollinat|propagat|sprout|nest|roost|incubat|^hatch|symbiot|burrow|flutter/],
29 ['space', /beam|catapult|hyperspac|levitat|orbit|swoop|warp/],
30]
31
32export function actFor(word: string): Act {
33 const w = word.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')
34 return STEMS.find(([, re]) => re.test(w))?.[0] ?? 'magic'
35}
36
37// ------------------------------------------------------------------ pixels
38
39type Canvas = { W: number; px: Uint32Array }
40type Cell = [number, number, number]
41
42const canvas = (W: number): Canvas => ({ W, px: new Uint32Array(W * PH).fill(DEF) })
43
44function put(c: Canvas, x: number, y: number, colour: number) {
45 x = Math.round(x)
46 y = Math.round(y)
47 if (x >= 0 && x < c.W && y >= 0 && y < PH) c.px[y * c.W + x] = colour
48}
49
50function rect(c: Canvas, x: number, y: number, w: number, h: number, colour: number) {
51 for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) put(c, x + i, y + j, colour)
52}
53
54function sprite(c: Canvas, art: string[], pal: Record<string, number>, x: number, y: number, flip = false) {
55 const w = Math.max(...art.map(r => r.length))
56 art.forEach((row, j) => [...row].forEach((ch, i) => ch !== '.' && pal[ch] !== undefined && put(c, x + (flip ? w - 1 - i : i), y + j, pal[ch]!)))
57}
58
59/** A stable random number in [0, 1) for the same inputs. */
60export function rnd(a: number, b = 0): number {
61 let h = Math.imul(a ^ 0x9e3779b9, 0x85ebca6b) ^ Math.imul(b + 0x632be5ab, 0xc2b2ae35)
62 h = Math.imul(h ^ (h >>> 15), 0x27d4eb2f)
63 return ((h ^ (h >>> 13)) >>> 0) / 4294967296
64}
65
66const mix = (a: number, b: number, k: number) => {
67 k = Math.min(1, Math.max(0, k))
68 const ch = (s: number) => Math.round(((a >> s) & 255) * (1 - k) + ((b >> s) & 255) * k) << s
69 return ch(16) | ch(8) | ch(0)
70}
71
72const wave = (t: number, period: number, phase = 0) => (Math.sin((t / period) * Math.PI * 2 + phase) + 1) / 2
73
74// ------------------------------------------------------------------ Clawd
75
76const CLAWD = 0xd97757
77const SPARKS = [0xff4b3e, 0xffd54f, 0xdfe7ff, 0xff9e3d]
78const CLAWD_ART = [
79 '..CCCCCCCCCC..',
80 '..CCECCCCECC..',
81 '..CCECCCCECC..',
82 '..CCCCCCCCCC..',
83 '..CCCCCCCCCC..',
84]
85const LEGS = ['..C.C....C.C..', '...C.C..C.C...']
86const CLAWD_TOP = GROUND - CLAWD_ART.length - 1
87const START = 3
88
89type Pose = 'stand' | 'cast' | 'walk' | 'pan' | 'hammer' | 'crank' | 'dance' | 'carry' | 'water' | 'umbrella' | 'float'
90type Hands = { hand: [number, number]; tip: [number, number] | null }
91
92/** Clawd (no hat here) at x, facing right (1) or left (-1), lifted `lift` pixels. */
93function clawd(c: Canvas, t: number, pose: Pose, x: number, facing: 1 | -1 = 1, lift = 0): Hands {
94 const f = Math.floor(t / 83)
95 const blink = f % 47 < 2
96 const ox = Math.round(x)
97 const hop = pose === 'dance' ? -(Math.floor(t / 300) % 2) : 0
98 const breathe = (pose === 'stand' || pose === 'cast') && Math.floor(t / 700) % 2 ? 1 : 0
99 const y = CLAWD_TOP + hop + breathe - lift
100 const at = (i: number) => (facing > 0 ? ox + i : ox + 13 - i)
101 const P = (i: number, yy: number, colour: number) => put(c, at(i), yy, colour)
102 const shade = mix(CLAWD, 0x000000, 0.18)
103 const pal: Record<string, number> = { C: CLAWD, E: blink ? CLAWD : 0x1a1410 }
104 CLAWD_ART.forEach((row, j) => [...row].forEach((ch, i) => ch !== '.' && pal[ch] !== undefined && P(i, y + j, pal[ch]!)))
105 for (let i = 2; i < 12; i++) P(i, y + 4, shade)
106 const stepping = pose === 'walk' || pose === 'dance' || pose === 'carry' || pose === 'umbrella'
107 ;[...LEGS[stepping ? (f >> 1) % 2 : 0]!].forEach((ch, i) => ch === 'C' && P(i, CLAWD_TOP + CLAWD_ART.length + hop - lift, shade))
108 const armY = y + 2
109 const beat = Math.floor(t / 300) % 2
110 const left: [number, number][] = pose === 'dance' && beat === 0 ? [[1, armY - 1], [0, armY - 2]] : [[1, armY], [0, armY]]
111 let right: [number, number][] = [[12, armY], [13, armY]]
112 if (pose === 'cast' || (pose === 'dance' && beat === 1) || pose === 'umbrella') right = [[12, armY - 1], [13, armY - 2]]
113 if (pose === 'hammer') right = t % 700 < 450 ? [[12, armY - 1], [13, armY - 2]] : [[12, armY], [13, armY + 1]]
114 if (pose === 'crank') right = [[12, armY], [13 + Math.round(Math.cos(t / 250)), armY + Math.round(Math.sin(t / 250))]]
115 if (pose === 'pan' || pose === 'water' || pose === 'carry') right = [[12, armY], [13, armY], [14, armY]]
116 for (const [i, yy] of [...left, ...right]) P(i, yy, CLAWD)
117 const [hi, hy] = right[right.length - 1]!
118 const hand: [number, number] = [at(hi), hy]
119 if (pose !== 'stand' && pose !== 'cast') return { hand, tip: null }
120 const wood = 0x7a4a24
121 P(hi + 1, hy - 1, wood); P(hi + 2, hy - 2, wood)
122 const tip: [number, number] = [at(hi + 3), hy - 3]
123 if (pose === 'cast' || f % 14 < 7) put(c, tip[0], tip[1], SPARKS[f % SPARKS.length]!)
124 return { hand, tip }
125}
126
127// ------------------------------------------------------------------ walking routes
128
129const WALK = 0.016 // pixels per ms
130
131type Stop = { x: number; stay: number; pose: Pose }
132type Where = { x: number; facing: 1 | -1; pose: Pose; stop: number; still: boolean; stayed: number }
133
134/** Clawd's place on a looping route: stands at each stop for `stay` ms, walks between them. */
135function along(stops: Stop[], ms: number, walkPose: Pose = 'walk'): Where {
136 const legs = stops.map((s, i) => {
137 const next = stops[(i + 1) % stops.length]!
138 return { s, next, walk: Math.abs(next.x - s.x) / WALK }
139 })
140 const total = legs.reduce((sum, l) => sum + l.s.stay + l.walk, 0) || 1
141 let t = ms % total
142 for (const [i, l] of legs.entries()) {
143 if (t < l.s.stay) return { x: l.s.x, facing: 1, pose: l.s.pose, stop: i, still: true, stayed: t }
144 t -= l.s.stay
145 if (t < l.walk) return { x: l.s.x + (l.next.x - l.s.x) * (t / l.walk), facing: l.next.x >= l.s.x ? 1 : -1, pose: walkPose, stop: i, still: false, stayed: 0 }
146 t -= l.walk
147 }
148 return { x: stops[0]!.x, facing: 1, pose: stops[0]!.pose, stop: 0, still: true, stayed: 0 }
149}
150
151const spot = (W: number, k: number) => Math.round(Math.max(24, Math.min(W - 24, W * k)))
152
153function ground(c: Canvas, colour = 0x2a2d36) {
154 for (let x = 0; x < c.W; x++) put(c, x, GROUND, colour)
155}
156
157// ------------------------------------------------------------------ the acts
158
159type Glyph = { x: number; row: number; ch: string; fg: number }
160type Draw = (c: Canvas, m: Moment, glyphs: Glyph[]) => void
161
162const cook: Draw = (c, m) => {
163 const fire = spot(c.W, 0.35)
164 const table = spot(c.W, 0.62)
165 // campfire: logs and flames
166 rect(c, fire - 3, GROUND - 1, 7, 1, 0x5a3a22)
167 for (let i = -2; i <= 2; i++) {
168 const h = 1 + Math.floor(rnd(i + 5, Math.floor(m.t / 110)) * 3)
169 for (let k = 0; k < h; k++) put(c, fire + i, GROUND - 2 - k, k === h - 1 ? 0xffd54f : 0xff8c2a)
170 }
171 // prep table with a cutting board and veg
172 rect(c, table - 4, GROUND - 5, 9, 1, 0x7a4a24); put(c, table - 3, GROUND - 4, 0x7a4a24); put(c, table + 3, GROUND - 4, 0x7a4a24)
173 for (let k = 0; k < 3; k++) rect(c, table - 3 + k * 2, GROUND - 4, 1, 4, 0x7a4a24)
174 put(c, table - 1, GROUND - 6, 0x6aa84f); put(c, table + 1, GROUND - 6, 0xd9534f)
175 const at = along([{ x: fire - 19, stay: 3200, pose: 'pan' }, { x: table - 19, stay: 1600, pose: 'hammer' }], m.t)
176 const { hand } = clawd(c, m.t, at.pose, at.x, at.facing)
177 if (at.pose === 'pan' && at.still) {
178 rect(c, hand[0] + 1, hand[1], 5, 1, 0x3a3a42) // the pan over the fire
179 const flip = (m.t % 1400) / 1400
180 if (flip < 0.6) {
181 const fx = hand[0] + 3
182 const fy = hand[1] - Math.sin((flip / 0.6) * Math.PI) * 6
183 put(c, fx, fy, 0xfff3d6); put(c, fx + 1, fy, 0xffd54f)
184 } else { put(c, hand[0] + 3, hand[1] - 1, 0xfff3d6); put(c, hand[0] + 4, hand[1] - 1, 0xffd54f) }
185 for (let k = 0; k < 3; k++) { // steam
186 const life = ((m.t / 120) + k * 5) % 8
187 put(c, hand[0] + 2 + k, hand[1] - 2 - life, mix(0x9aa0b0, 0x1a1c24, life / 8))
188 }
189 }
190 if (at.pose === 'hammer' && at.still && m.t % 700 > 450) put(c, table + (rnd(Math.floor(m.t / 700)) > 0.5 ? 1 : -1), GROUND - 7, 0x6aa84f)
191 ground(c)
192}
193
194const think: Draw = (c, m, glyphs) => {
195 const at = along([{ x: START, stay: 1800, pose: 'stand' }, { x: spot(c.W, 0.45), stay: 1800, pose: 'stand' }], m.t)
196 clawd(c, m.t, at.pose, at.x, at.facing)
197 const hx = Math.round(at.x) + (at.facing > 0 ? 13 : 0)
198 const dir = at.facing
199 put(c, hx + dir * 2, CLAWD_TOP + 1, 0xc8ccd8)
200 put(c, hx + dir * 4, CLAWD_TOP - 1, 0xc8ccd8)
201 // the thought: a cloud, and now and then a lit bulb
202 const cx = hx + dir * 6 - (dir < 0 ? 8 : 0)
203 const eureka = m.t % 6000 > 4800
204 if (eureka) {
205 sprite(c, ['.yyy.', 'yyyyy', 'yyyyy', '.yyy.', '.ggg.'], { y: 0xffe066, g: 0x9aa0b0 }, cx + 2, 0)
206 for (const [dx, dy] of [[-1, 1], [7, 1], [3, -1]]) if (m.t % 300 < 150) put(c, cx + 2 + dx!, dy!, 0xfff6c8)
207 } else {
208 sprite(c, ['.ccccc.', 'ccccccc', '.ccccc.'], { c: 0x5e6478 }, cx, 0)
209 const dots = Math.floor(m.t / 400) % 4
210 for (let k = 0; k < dots; k++) put(c, cx + 2 + k * 2, 1, 0xe8e4c8)
211 }
212 ground(c)
213 void glyphs
214}
215
216const dance: Draw = (c, m, glyphs) => {
217 const moon = /moonwalk/i.test(m.word)
218 const at = along([{ x: START, stay: 900, pose: 'dance' }, { x: spot(c.W, 0.5), stay: 900, pose: 'dance' }], m.t, 'dance')
219 // moonwalking: facing away from where he glides
220 clawd(c, m.t, 'dance', at.x, moon ? (at.facing > 0 ? -1 : 1) : at.facing)
221 // the disco floor under him
222 const beat = Math.floor(m.t / 300)
223 for (let x = 0; x < c.W; x++) put(c, x, GROUND, Math.abs(x - at.x - 7) < 14 ? [0xd94f8a, 0x4fa3d9, 0xd9c84f, 0x6ad94f][(Math.floor(x / 3) + beat) % 4]! : 0x2a2d36)
224 // notes float up from him
225 for (let k = 0; k < 3; k++) {
226 const life = ((m.t / 700) + k / 3) % 1
227 glyphs.push({ x: Math.round(at.x) + 15 + k * 3 + Math.round(life * 4), row: Math.max(0, 3 - Math.floor(life * 4)), ch: k % 2 ? '♫' : '♪', fg: [0xd94f8a, 0x4fa3d9, 0xd9c84f][k]! })
228 }
229}
230
231const build: Draw = (c, m) => {
232 const anvil = spot(c.W, 0.35)
233 const forge = spot(c.W, 0.65)
234 sprite(c, ['aaaaaaa', '.aaaaa.', '..aaa..', '.aaaaa.'], { a: 0x55555f }, anvil - 3, GROUND - 4)
235 const hot = mix(0xff4b3e, 0xffd54f, wave(m.t, 600))
236 rect(c, anvil - 1, GROUND - 5, 4, 1, hot)
237 // the forge: a brick hearth with a glow
238 sprite(c, ['bbbbbbb', 'b.....b', 'b.....b', 'bbbbbbb'], { b: 0x7a3b2e }, forge - 3, GROUND - 4)
239 for (let i = -2; i <= 2; i++) if (rnd(i + 9, Math.floor(m.t / 120)) > 0.3) put(c, forge + i, GROUND - 2 - Math.floor(rnd(i, Math.floor(m.t / 120)) * 2), rnd(i + 2, Math.floor(m.t / 120)) > 0.5 ? 0xff8c2a : 0xffd54f)
240 const at = along([{ x: anvil - 17, stay: 3500, pose: 'hammer' }, { x: forge - 19, stay: 1200, pose: 'stand' }], m.t, 'carry')
241 const { hand } = clawd(c, m.t, at.pose, at.x, at.facing)
242 if (at.pose === 'hammer') {
243 const down = m.t % 700 >= 450
244 rect(c, hand[0] + 1, hand[1] - (down ? 0 : 2), 2, 2, 0x8a919b) // hammer head
245 if (down) for (let s = 0; s < 6; s++) {
246 const k = ((m.t % 700) - 450) / 250
247 put(c, anvil + Math.cos(s * 1.1) * k * 5, GROUND - 6 - Math.abs(Math.sin(s * 1.1)) * k * 4, s % 2 ? 0xffd54f : 0xff8c2a)
248 }
249 }
250 if (at.pose === 'carry') rect(c, hand[0] + (at.facing > 0 ? 1 : -3), hand[1] - 1, 3, 1, hot)
251 ground(c)
252}
253
254function gear(c: Canvas, cx: number, cy: number, r: number, angle: number, colour: number) {
255 for (let a = 0; a < 24; a++) put(c, cx + Math.cos((a / 24) * Math.PI * 2) * r, cy + Math.sin((a / 24) * Math.PI * 2) * r, colour)
256 for (let k = 0; k < 6; k++) {
257 const a = angle + (k / 6) * Math.PI * 2
258 put(c, cx + Math.cos(a) * (r + 1), cy + Math.sin(a) * (r + 1), colour)
259 }
260 put(c, cx, cy, mix(colour, 0xffffff, 0.4))
261}
262
263const compute: Draw = (c, m, glyphs) => {
264 const g = spot(c.W, 0.35)
265 const cab = spot(c.W, 0.65)
266 gear(c, g, 9, 4, m.t / 400, 0x8a919b)
267 gear(c, g + 8, 5, 2, -m.t / 200, 0xb08d57)
268 // a cabinet of blinking lights
269 rect(c, cab - 3, GROUND - 9, 7, 9, 0x2e3440)
270 for (let j = 0; j < 4; j++) for (let i = 0; i < 3; i++) if (rnd(i + j * 3, Math.floor(m.t / 250)) > 0.4) put(c, cab - 2 + i * 2, GROUND - 8 + j * 2, [0x6ad94f, 0xd9c84f, 0x4fa3d9][(i + j) % 3]!)
271 const at = along([{ x: g - 19, stay: 3200, pose: 'crank' }, { x: cab - 19, stay: 1400, pose: 'stand' }], m.t)
272 clawd(c, m.t, at.pose, at.x, at.facing)
273 for (let k = 0; k < 3; k++) {
274 const life = ((m.t / 900) + k / 3) % 1
275 glyphs.push({ x: g - 2 + k * 3, row: Math.max(0, 3 - Math.floor(life * 4)), ch: rnd(k, Math.floor(m.t / 900)) > 0.5 ? '1' : '0', fg: mix(0x6ad94f, 0x1a1c24, life * 0.7) })
276 }
277 ground(c)
278}
279
280/** Walks the whole strip and wraps; quicker for scurrying, scampering and galloping. */
281const stroll: Draw = (c, m) => {
282 const fast = /scurr|scamper|gallop|pounc|zigzag/i.test(m.word) ? 2.2 : 1
283 const span = c.W + 18
284 const x = ((m.t * WALK * fast) % span) - 16
285 const lift = /gallop/i.test(m.word) ? Math.floor(m.t / 200) % 2 : 0
286 clawd(c, m.t, 'walk', x, 1, lift)
287 for (let k = 0; k < c.W; k += 9) put(c, k + Math.floor(rnd(k, 1) * 5), GROUND - 1, 0x3d5a3a) // grass tufts
288 ground(c, 0x2d4a2b)
289}
290
291const herd: Draw = (c, m) => {
292 const span = c.W + 60
293 const x = ((m.t * WALK) % span) - 16
294 clawd(c, m.t, 'walk', x)
295 for (let k = 0; k < 3; k++) { // sheep trot ahead of him
296 const sx = x + 22 + k * 9
297 const step = Math.floor(m.t / 160 + k) % 2
298 sprite(c, ['.www.', 'wwwwk', '.wwww', step ? '.l.l.' : 'l...l'], { w: 0xeeeeee, k: 0x2a2a30, l: 0x2a2a30 }, sx, GROUND - 4)
299 }
300 ground(c, 0x2d4a2b)
301}
302
303const weather: Draw = (c, m) => {
304 const at = along([{ x: START, stay: 1500, pose: 'umbrella' }, { x: spot(c.W, 0.55), stay: 1500, pose: 'umbrella' }], m.t, 'umbrella')
305 clawd(c, m.t, at.pose, at.x, at.facing)
306 const ux = Math.round(at.x) + 7
307 // umbrella over the hat, and the cloud that follows him
308 rect(c, ux - 6, CLAWD_TOP - 2, 13, 1, 0xd94f4f); rect(c, ux - 4, CLAWD_TOP - 3, 9, 1, 0xd94f4f)
309 for (let j = CLAWD_TOP - 1; j < CLAWD_TOP + 7; j++) put(c, ux + 6, j, 0x5a3a22)
310 const gusty = /gust|billow|swirl|whirl|unfurl/i.test(m.word)
311 const storm = /thunder/i.test(m.word)
312 const cloud = storm ? 0x3a3d4a : 0x6b7086
313 if (!gusty) {
314 rect(c, ux - 7, 0, 15, 1, cloud); rect(c, ux - 5, -1, 9, 1, cloud)
315 for (let d = 0; d < 7; d++) {
316 const fall = ((m.t / 60) + d * 7) % (CLAWD_TOP - 1)
317 const rx = ux - 7 + d * 2
318 if (Math.abs(rx - ux) > 6 || fall < CLAWD_TOP - 4) put(c, rx, 1 + fall, 0x7fa6d6)
319 }
320 for (let x = 0; x < c.W; x++) if (Math.abs(x - ux) > 7 && rnd(x, Math.floor(m.t / 70)) > 0.97) put(c, x, rnd(x, 3) * (GROUND - 1), 0x5c7ea8)
321 if (storm && m.t % 3000 < 160) {
322 let lx = ux + 14
323 for (let y = 0; y < GROUND; y++) { put(c, lx, y, 0xfff6c8); lx += (y % 3) - 1 }
324 }
325 } else {
326 for (let k = 0; k < 6; k++) { // wind streaks
327 const wx = (m.t / 25 + k * 37) % (c.W + 20) - 10
328 rect(c, wx, 2 + k * 2, 5, 1, mix(0x9aa0b0, 0x1a1c24, 0.4))
329 }
330 }
331 ground(c)
332}
333
334const garden: Draw = (c, m) => {
335 const beds = [0.3, 0.45, 0.6].map(k => spot(c.W, k))
336 for (const [i, bx] of beds.entries()) { // plants grow, bloom, start over
337 const grow = ((m.t / 4000) + i * 0.33) % 1
338 const h = 1 + Math.floor(grow * 6)
339 for (let k = 0; k < h; k++) put(c, bx, GROUND - 1 - k, 0x4f9a3a)
340 if (h > 2) { put(c, bx - 1, GROUND - 3, 0x6ab84f); put(c, bx + 1, GROUND - 4, 0x6ab84f) }
341 if (h > 5) sprite(c, ['.p.', 'pyp', '.p.'], { p: [0xd94f8a, 0xd9c84f, 0x9a6ad9][i]!, y: 0xffe066 }, bx - 1, GROUND - 1 - h - 2)
342 }
343 if (/nest|roost|incubat|hatch/i.test(m.word)) { // a nest whose egg wobbles and hatches
344 const nx = spot(c.W, 0.75)
345 rect(c, nx - 3, GROUND - 2, 7, 2, 0x7a5a3a)
346 const crack = m.t % 5000 > 3600
347 if (crack) sprite(c, ['.y.', 'yyy'], { y: 0xffe066 }, nx - 1, GROUND - 5)
348 else rect(c, nx - 1 + (Math.floor(m.t / 200) % 2), GROUND - 4, 2, 2, 0xf3efe0)
349 }
350 const stops = beds.map(bx => ({ x: bx - 18, stay: 1300, pose: 'water' as Pose }))
351 const at = along(stops, m.t)
352 const { hand } = clawd(c, m.t, at.pose, at.x, at.facing)
353 if (at.pose === 'water' && at.still) {
354 rect(c, hand[0] + 1, hand[1] - 1, 3, 2, 0x7f8fa6) // the watering can
355 for (let d = 0; d < 3; d++) put(c, hand[0] + 4, hand[1] + 1 + ((m.t / 80 + d * 2) % 4), 0x7fa6d6)
356 }
357 // a butterfly
358 const bx = spot(c.W, 0.5) + Math.sin(m.t / 900) * 18
359 const by = 4 + Math.sin(m.t / 300) * 2
360 const flap = Math.floor(m.t / 120) % 2
361 put(c, bx, by, 0x2a2a30); put(c, bx - 1, by - flap, 0xffb22e); put(c, bx + 1, by - flap, 0xffb22e)
362 ground(c, 0x4a3a2a)
363}
364
365const space: Draw = (c, m, glyphs) => {
366 const streak = /hyperspac|warp/i.test(m.word)
367 for (let k = 0; k < c.W / 4; k++) { // stars, streaking in hyperspace
368 const sx = streak ? (rnd(k, 1) * c.W - (m.t / 8) * (0.5 + rnd(k, 2))) % c.W : rnd(k, 1) * c.W
369 const x = sx < 0 ? sx + c.W : sx
370 const y = rnd(k, 3) * GROUND
371 put(c, x, y, 0xdfe7ff)
372 if (streak) for (let tail = 1; tail < 4; tail++) put(c, x + tail, y, mix(0xdfe7ff, 0x1a1c24, tail / 4))
373 }
374 const at = along([{ x: START, stay: 1600, pose: 'stand' }, { x: spot(c.W, 0.5), stay: 1600, pose: 'stand' }], m.t, 'float')
375 const lift = 2 + Math.round(wave(m.t, 1600) * 2)
376 clawd(c, m.t, at.still ? 'cast' : 'float', at.x, at.facing, lift)
377 // a little planet circles him
378 const a = m.t / 500
379 const px = at.x + 7 + Math.cos(a) * 11
380 const py = 8 + Math.sin(a) * 4
381 rect(c, px - 1, py - 1, 3, 3, 0x6a8fd9); put(c, px - 2, py, 0xd9c84f); put(c, px + 2, py, 0xd9c84f)
382 void glyphs
383}
384
385const magic: Draw = (c, m) => {
386 const target = spot(c.W, 0.4)
387 const at = along([{ x: START, stay: 2600, pose: 'cast' }, { x: target - 26, stay: 2600, pose: 'cast' }], m.t)
388 const { tip } = clawd(c, m.t, at.pose, at.x, at.facing)
389 const cx = Math.round(at.x) + 26
390 if (tip && at.still) {
391 for (let k = 0; k < 10; k++) { // a swirl of sparkles
392 const a = m.t / 250 + k * 0.63
393 const r = 1 + ((k + m.t / 200) % 10) * 0.45
394 put(c, cx + Math.cos(a) * r * 1.4, 8 + Math.sin(a) * r * 0.7, SPARKS[k % SPARKS.length]!)
395 }
396 // a top hat; the rabbit pops out every few seconds
397 rect(c, cx - 3, GROUND - 1, 7, 1, 0x4a4a5a); rect(c, cx - 2, GROUND - 4, 5, 3, 0x4a4a5a); rect(c, cx - 2, GROUND - 2, 5, 1, 0xae0001)
398 if (at.stayed > 1400) sprite(c, ['w.w', 'w.w', 'www', 'wkw'], { w: 0xf4f4f4, k: 0xe88aa0 }, cx - 1, GROUND - 8)
399 }
400 ground(c)
401}
402
403const ACTS: Record<Act, Draw> = { cook, think, dance, build, compute, stroll, herd, weather, garden, space, magic }
404
405// ------------------------------------------------------------------ cells
406
407/** The frame's cells, packed for a Raster: columns * ROWS triplets of [codePoint, fg, bg]. */
408export function frame(m: Moment, columns: number): Uint32Array {
409 const c = canvas(columns)
410 const glyphs: Glyph[] = []
411 ACTS[actFor(m.word)](c, m, glyphs)
412 const cells: Cell[] = []
413 for (let r = 0; r < ROWS; r++) {
414 for (let x = 0; x < columns; x++) {
415 const top = c.px[2 * r * columns + x]!
416 const bot = c.px[(2 * r + 1) * columns + x]!
417 if (top === DEF && bot === DEF) cells.push([0x20, DEF, DEF])
418 else if (top === bot) cells.push([0x2588, top, DEF])
419 else if (top === DEF) cells.push([0x2584, bot, DEF])
420 else cells.push([0x2580, top, bot])
421 }
422 }
423 for (const g of glyphs) {
424 const i = g.row * columns + g.x
425 if (g.x >= 0 && g.x < columns && g.row >= 0 && g.row < ROWS && cells[i]![0] === 0x20) cells[i] = [g.ch.codePointAt(0)!, g.fg, DEF]
426 }
427 const out = new Uint32Array(cells.length * 3)
428 cells.forEach(([cp, fg, bg], i) => out.set([cp, fg, bg], i * 3))
429 return out
430}
431
432/** Base64 of the cells, as RasterProps.cells wants them. */
433export const encode = (cells: Uint32Array): string => new Uint8Array(cells.buffer).toBase64()
434
435/** "12s", "1m 4s": the turn's time, as the spinner shows it. */
436export const elapsed = (ms: number) => {
437 const s = Math.max(0, Math.floor(ms / 1000))
438 return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`
439}
440