Animated cartoons of what Claude is doing, drawn under the spinner while it works

Little animated cartoons of what Claude is doing, drawn under the spinner in Claude Code while it works. Clawd, the Claude Code mascot, stars in every scene, and the scene follows the work: tests become a castle siege, a bug hunt becomes a safari, a long build becomes a rocket on the pad.

An independent project, not affiliated with or endorsed by Anthropic.
On default settings, about 1 to 3% on top of what Claude Code already uses.
Most scenes come from a library of 138 ready-made scenes that ship with the plugin and cost nothing. A model is asked to draw a fresh scene only when you start a task or something fails, and at most once a minute. Each of those costs about 2 to 4 cents at API prices.
The range depends on how you work. Long tasks land near 1%, because one fresh scene covers minutes of Claude's work. Rapid short back-and-forth lands near 3%, because nearly every message starts a task. These figures assume Claude Code itself runs on Opus. On a cheaper model the share is larger.
The requests use your Claude Code login and nothing else. On a subscription they count toward your plan's usage limits like any other Claude use. On an API key they're billed to that key. Nothing is spent while Claude is idle or the cartoons are hidden.
The "Scenes" setting picks where scenes come from:
| Scenes | Extra on top of Claude Code |
|---|---|
| Ready-made only | Nothing. No requests are made. |
| Mix (default) | ~1–3% |
| Fresh only, a new scene every 15 seconds | ~5–10% |
| Fresh only, as fast as possible | ~15–25% |
/toons settings shows what the cartoons have actually cost you, next to what Claude's own work costs, once there's enough to measure.
You need Claude Code with plugin hook modules, an early-access feature. This was built on version 2.1.287.
git clone https://github.com/achimala/claude-toons ~/src/claude-toons
claude --plugin-dir ~/src/claude-toons
Give Claude any task. The cartoons appear under the spinner while it works.
env block of ~/.claude/settings.json with its full path, then restart Claude Code: {
"env": {
"CLAUDE_CODE_PLUGIN_DIRS": "/Users/you/src/claude-toons"
}
}
To update, run git pull in the folder. A running session picks up the change on its own. To uninstall, remove the folder from CLAUDE_CODE_PLUGIN_DIRS and delete it.
Nothing shows up? Run claude --debug and look for a toons line. If it says hook modules are turned off, your Claude Code doesn't have the feature yet. Cartoons only appear while Claude is working, and only in the terminal.
/toons shows or hides the cartoons, even mid-task. Hidden means no scenes are requested at all. The choice is remembered across sessions./toons settings opens the settings and the cost figures. The same settings are in /config under "Toons".| Setting | What it does |
|---|---|
| Scenes | "Ready-made only" is free. "Mix", the default, adds fresh scenes for news. "Fresh only" draws every scene new, at several times the cost. |
| Scene styles | A mix, or only 3D, pixel art or text art, or everything but 3D. |
| Director model | The model that draws fresh scenes. Sonnet by default. Haiku is cheaper, Opus is more inventive. |
| New scene | How often fresh scenes may be requested, in "fresh only". |
| Director thinks | Lets the model think before each fresh scene. Off by default, since thinking costs more. |
MIT. See LICENSE.
hooks/register.tsx 737 lines1import type { EngineInterface, Register } from 'claude-code'
2
3import {
4 GAP_MS,
5 MODELS,
6 MODEL_NAMES,
7 PACES,
8 addTokens,
9 costOf,
10 emptyStats,
11 isModel,
12 isPace,
13 money,
14 spendKey,
15 summarizeCost,
16 type Model,
17 type Pace,
18 type Spend,
19 type Stats,
20 type Window,
21} from './cost'
22import { createDealer, deal, isInteresting, isStockDue, phaseOf, whatOf, type Dealer, type Phase } from './library'
23import { STYLE_SETS, URL, createThread, isStyleSet, type Narration, type StyleSet } from './narrator'
24import { SCENES } from './scenes'
25import { cleanScript, stage, type Script } from './script'
26
27const PLUGIN = 'toons'
28const ROWS = 9
29// How long the band takes to rise to its full height when the spinner shows.
30const GROW_MS = 700
31// How long the log may stay quiet mid-turn before a "still going" line.
32const QUIET_MS = 20_000
33// How long a scene plays before a quiet stretch asks for its next beat. A
34// scene animates on its own, so until then quiet costs nothing: no request
35// is made just to hear that the scene should keep playing.
36const BEAT_MS = 90_000
37// With the stock of ready-made scenes on, the director is asked only for news
38// worth a scene of its own (the task, a failure, the turn's end, a broken
39// scene), and no more often than this; everything else deals a stock scene.
40const LIVE_MS = 60_000
41
42// Where scenes come from: ready-made ones only (free, nothing is requested),
43// ready-made with fresh ones for news (the default), or fresh ones only.
44const SOURCES = ['mix', 'ready-made only', 'fresh only'] as const
45type Source = (typeof SOURCES)[number]
46// Settings saved before there were three: on was the mix, off fresh only.
47const sourceOf = (v: unknown): Source => (v === 'off' ? 'fresh only' : SOURCES.includes(v as Source) ? (v as Source) : 'mix')
48// The settings pane, and the slash command that toggles the cartoons or opens it.
49const PANE = 'toons'
50const COMMAND = 'toons'
51
52// The buddy waking up, until its first scene arrives.
53const FIRST = cleanScript({
54 background: { effect: 'plasma', palette: ['#5a2416', '#b4532f', '#d97757', '#f5c4a8'], speed: 0.6, intensity: 0.3 },
55 actors: [{ kind: 'clawd', frames: [], fps: 0, x: 'max(w*0.45, w - t*14)', y: '3', color: '#d97757', say: '', sayAt: 0 }],
56 particles: [{ glyphs: '·∘°', count: 12, x: 'rand(k)*w', y: 'mod(rand(k+3)*h - t*(0.5+rand(k+5)), h)', color: '#d97757' }],
57}) as Script
58
59// The arguments worth narrating, per tool, cut to a line.
60const ARGS = ['command', 'file_path', 'pattern', 'path', 'url', 'query', 'description', 'subagent_type'] as const
61
62function describe(input: Record<string, unknown>) {
63 const parts = ARGS.map(key => input[key])
64 .filter((v): v is string => typeof v === 'string' && v.length > 0)
65 .map(v => v.replace(/\s+/g, ' ').slice(0, 100))
66
67 return parts.length > 0 ? ` \`${parts.join(' | ')}\`` : ''
68}
69
70const clamp01 = (v: number) => Math.min(1, Math.max(0, v))
71
72// Stats as stored, made safe to add to.
73function cleanStats(raw: unknown): Stats {
74 const stats = emptyStats()
75 if (typeof raw !== 'object' || raw === null) return stats
76 const r = raw as { buddy?: Record<string, unknown>; main?: unknown }
77 const spend = (v: unknown): Spend | undefined => {
78 const s = v as Partial<Spend> | null
79 if (!s || typeof s !== 'object' || typeof s.usd !== 'number' || typeof s.scenes !== 'number' || typeof s.activeMs !== 'number') return undefined
80
81 return { usd: s.usd, scenes: s.scenes, activeMs: s.activeMs }
82 }
83 for (const [key, v] of Object.entries(r.buddy ?? {})) {
84 const s = spend(v)
85 if (s) stats.buddy[key] = s
86 }
87 stats.main = spend(r.main) ?? stats.main
88
89 return stats
90}
91
92// Everything the buddy keeps for the session.
93type Buddy = {
94 model: Model
95 pace: Pace
96 thread: ReturnType<typeof createThread>
97 // The log of what happened since the last scene, and when the turn began.
98 pending: string[]
99 startedAt: number
100 lastCall: number
101 loggedAt: number
102 isTurn: boolean
103 isAsking: boolean
104 isAnimating: boolean
105 // The last trouble toasted, so the same again is not.
106 error?: string
107 // The scene playing and when it arrived, and the one dissolving out of
108 // the picture and when that one arrived (its clock keeps running).
109 scene: Script
110 sceneAt: number
111 previous?: Script
112 previousAt: number
113 // Whether stock scenes are dealt, what Claude is doing by the log and the
114 // thing it last touched (to label a stock scene), the phase the scene
115 // playing was dealt for, when the director was last asked, the concepts
116 // of recent stock scenes, and how many were dealt this session.
117 isStock: boolean
118 // Ready-made only: the director is never asked, and nothing is spent.
119 isFree: boolean
120 source: Source
121 dealer: Dealer
122 doing: Phase
123 what?: string
124 scenePhase?: Phase
125 // Whether the scene playing is the director's, and the tools Claude has
126 // started this turn (thinking after the first is part of the work in hand).
127 isLiveScene: boolean
128 turnTools: number
129 liveAt: number
130 sessionStock: number
131 // The spinner being drawn on, its width, when it showed, and the rows the
132 // band was last drawn with.
133 spinner?: string
134 cols: number
135 shownAt: number
136 drawnRows: number
137 // Whether the session's login refused these requests: no more are made.
138 isRefused: boolean
139 // Tool calls running now with when each began, and the spinner's phase.
140 running: Map<number, { what: string; at: number }>
141 callCount: number
142 phase: string
143 phaseAt: number
144 // No request before this time: a rate limit's or a failure's wait, and how
145 // many requests in a row have failed.
146 calmUntil: number
147 failures: number
148 // Whether the cartoons show (kept across sessions), and what they and
149 // Claude's own work have spent, for the settings pane's estimates.
150 isShown: boolean
151 stats: Stats
152 sessionUsd: number
153 sessionScenes: number
154 isSubscription: boolean
155 windows: Window[]
156 // The last scene code error reported to the narrator, for the settings pane.
157 lastTrouble?: string
158 // The turn's start and the session's cost then, and when working time with
159 // the cartoons showing was last added up.
160 turnAt: number
161 turnCostAt: number
162 accruedAt: number
163}
164
165function createBuddy(model: Model, pace: Pace, isThinking: boolean, styles: StyleSet, source: Source): Buddy {
166 return {
167 source,
168 isStock: source !== 'fresh only',
169 isFree: source === 'ready-made only',
170 dealer: createDealer(STYLE_SETS[styles]),
171 doing: 'thinking',
172 isLiveScene: false,
173 turnTools: 0,
174 liveAt: 0,
175 sessionStock: 0,
176 model,
177 pace,
178 thread: createThread(model, { isThinking, styles }),
179 pending: [],
180 startedAt: 0,
181 lastCall: 0,
182 loggedAt: 0,
183 isTurn: false,
184 isAsking: false,
185 isAnimating: false,
186 scene: FIRST,
187 sceneAt: 0,
188 previousAt: 0,
189 cols: 60,
190 shownAt: 0,
191 drawnRows: 1,
192 isRefused: false,
193 running: new Map(),
194 callCount: 0,
195 phase: '',
196 phaseAt: 0,
197 calmUntil: 0,
198 failures: 0,
199 isShown: true,
200 stats: emptyStats(),
201 sessionUsd: 0,
202 sessionScenes: 0,
203 isSubscription: false,
204 windows: [],
205 turnAt: 0,
206 turnCostAt: 0,
207 accruedAt: 0,
208 }
209}
210
211const rowsAt = (b: Buddy, at: number) => {
212 const grown = clamp01((at - b.shownAt) / GROW_MS)
213
214 return Math.max(1, Math.ceil(ROWS * (1 - Math.pow(1 - grown, 3))))
215}
216
217const frameAt = (b: Buddy, at: number, rows: number) =>
218 stage({
219 cols: b.cols,
220 rows,
221 t: at / 1000,
222 script: b.scene,
223 previous: b.previous,
224 since: at - b.sceneAt,
225 previousSince: at - b.previousAt,
226 reveal: clamp01((at - b.shownAt) / (GROW_MS * 1.6)),
227 })
228
229const stamp = (b: Buddy, at: number) => `+${Math.round((at - b.startedAt) / 1000)}s`
230
231// Logs a line for the next scene; while the cartoons are hidden nothing is
232// logged, so nothing is sent.
233function note(b: Buddy, at: number, line: string) {
234 if (!b.isShown) return
235 b.pending.push(`${stamp(b, at)} ${line}`)
236 b.loggedAt = at
237 const doing = phaseOf(line)
238 // Thinking between tool calls is part of the work in hand, not a new
239 // phase; only thinking before the turn's first tool is.
240 if (doing && !(doing === 'thinking' && b.turnTools > 0)) b.doing = doing
241 const what = whatOf(line)
242 if (what) b.what = what
243}
244
245// The scene playing gives way to another.
246function show(b: Buddy, script: Script, at: number) {
247 b.previous = b.scene
248 b.previousAt = b.sceneAt
249 b.scene = script
250 b.sceneAt = at
251 b.scenePhase = b.doing
252 b.error = undefined
253}
254
255// A stock scene for what Claude is doing, if there is one it can draw.
256function dealStock(b: Buddy, at: number) {
257 const got = deal(b.dealer, b.doing, b.what)
258 const script = got && cleanScript(got.raw)
259 if (!got || !script) return false
260 // A stock scene that breaks is not the director's to fix.
261 if (script.code) script.code.isReported = true
262 b.sessionStock += 1
263 show(b, script, at)
264 b.isLiveScene = false
265
266 return true
267}
268
269const bucket = (b: Buddy) => (b.stats.buddy[spendKey(b.model, b.pace)] ??= { usd: 0, scenes: 0, activeMs: 0 })
270
271// Adds up Claude's working time with the cartoons showing.
272function accrue(b: Buddy, now: number) {
273 if (b.isTurn && b.isShown && b.accruedAt > 0) bucket(b).activeMs += Math.max(0, now - b.accruedAt)
274 b.accruedAt = now
275}
276
277// One scene request, on the session's own credential and nothing else: on a
278// subscription it counts toward the plan's limits, never a separate bill. If
279// the login refuses (no credential, 401, 403), the cartoons stop asking for
280// the rest of the session rather than looking for another way to pay.
281async function requestScene($: EngineInterface, b: Buddy): Promise<Narration> {
282 const auth = await $.session.authorize()
283 let response: { ok: boolean; status: number; text: string; headers: Record<string, string> } | undefined
284 if (auth) {
285 response = await $.http.fetch(URL, { method: 'POST', auth: auth.handle, ...b.thread.request(auth.kind) })
286 if (b.thread.isFallbackRefused(response.status, response.text)) {
287 response = await $.http.fetch(URL, { method: 'POST', auth: auth.handle, ...b.thread.request(auth.kind) })
288 }
289 }
290 if (!response || response.status === 401 || response.status === 403) {
291 b.thread.abandon()
292 b.isRefused = true
293
294 return { error: "this session's login can't make the requests the cartoons need, so they are off for this session" }
295 }
296 if (response.ok) {
297 b.failures = 0
298
299 return b.thread.accept(response.text)
300 }
301 b.pending.unshift(b.thread.abandon().replace(/^\[strip [^\]]*\]\n/, ''))
302 // A request refused as malformed would be refused again with the same
303 // history: the conversation begins again.
304 if (response.status === 400) b.thread.reset()
305 // A rate limit waits as long as it asks; any other failure waits a little
306 // longer each time it repeats.
307 b.failures += 1
308 const wait = response.status === 429 ? Number(response.headers['retry-after'] ?? 20) : Math.min(120, 5 * 2 ** (b.failures - 1))
309 b.calmUntil = (await $.clock.now()) + Math.max(5, Math.min(120, wait || 20)) * 1000
310
311 return { error: `API ${response.status}: ${response.text.replace(/\s+/g, ' ').slice(0, 140)}` }
312}
313
314// Asks for a scene whenever there is news, at most once per the pace's gap,
315// while a turn runs and the cartoons show.
316function ask($: EngineInterface, b: Buddy) {
317 if (b.isAsking) return
318 b.isAsking = true
319 const gapMs = GAP_MS[b.pace]
320 void (async () => {
321 while (b.isTurn && b.isShown && !b.isRefused) {
322 const now = await $.clock.now()
323 // A quiet stretch mid-turn is news too, once the scene has run its
324 // course (or its code broke): what is still going on, for its next
325 // beat. Before that the scene keeps playing on its own, unasked.
326 const quiet = Math.max(QUIET_MS, gapMs * 2)
327 const isSpent = Boolean(b.scene.code?.error) || now - b.sceneAt >= BEAT_MS
328 if (b.pending.length === 0 && now - b.loggedAt >= quiet && now - b.lastCall >= quiet) {
329 const oldest = [...b.running.values()].sort((x, y) => x.at - y.at)[0]
330 if (!isSpent) b.loggedAt = now
331 else if (b.isStock && !b.scene.code?.error && dealStock(b, now)) {
332 b.loggedAt = now
333 $.ui.invalidate('ui.render')
334 } else if (oldest) note(b, now, `still running ${oldest.what} (${Math.round((now - oldest.at) / 1000)}s so far)`)
335 else if (b.phase) note(b, now, `still ${b.phase} (${Math.round((now - b.phaseAt) / 1000)}s so far)`)
336 else b.loggedAt = now
337 }
338 // With the stock on, the pace does not apply: stock scenes are dealt by
339 // isStockDue, and the director is asked at most once per LIVE_MS.
340 if (b.pending.length > 0 && now - b.lastCall >= (b.isStock ? 0 : gapMs)) {
341 // A scene whose code broke is news for the narrator, once.
342 const broken = b.scene.code?.error && !b.scene.code.isReported ? b.scene.code : undefined
343 // With the stock on, routine news deals a stock scene once the scene
344 // playing is due for replacing, and the log waits for the director's
345 // next turn: news worth it, once LIVE_MS have passed since its last.
346 const isNews = Boolean(broken) || b.pending.some(isInteresting)
347 // The waking-up scene gives way at once.
348 const isDue = isStockDue({ age: b.scene === FIRST ? Infinity : now - b.sceneAt, isBroken: Boolean(b.scene.code?.error), isLive: b.isLiveScene, isNewPhase: b.doing !== b.scenePhase })
349 // The stock has nothing for this phase in the styles allowed: the
350 // director draws it, at the same spacing.
351 const isUnstocked = b.isStock && isDue && !SCENES.some(s => s.phase === b.doing && b.dealer.styles.includes(s.style))
352 const isLive = !b.isStock || (!b.isFree && (isNews || isUnstocked) && now - b.liveAt >= LIVE_MS)
353 if (!isLive || now < b.calmUntil) {
354 if (b.isStock && isDue && dealStock(b, now)) $.ui.invalidate('ui.render')
355 // The log keeps its task line and its latest lines.
356 if (b.pending.length > 60) b.pending = [...b.pending.filter(line => line.startsWith('[task] ')).slice(-1), ...b.pending.slice(-50)]
357 await $.clock.sleep(400)
358 continue
359 }
360 b.lastCall = now
361 b.liveAt = now
362 if (broken) {
363 broken.isReported = true
364 b.lastTrouble = broken.error
365 }
366 const activity = [...(broken ? [`[your last scene's code stopped: ${broken.error?.slice(0, 200)}]`] : []), ...b.pending].join('\n')
367 b.pending = []
368 b.thread.ask(`[strip ${b.cols}x${ROWS}]\n${activity}`)
369 const told = await requestScene($, b)
370 if (told.spent) {
371 const usd = costOf(b.model, told.spent)
372 const spend = bucket(b)
373 spend.usd += usd
374 addTokens(spend, told.spent)
375 b.sessionUsd += usd
376 spend.scenes += 1
377 b.sessionScenes += 1
378 // The working time so far goes with the money, so the saved rate holds
379 // if the session ends mid-turn.
380 accrue(b, await $.clock.now())
381 await $.store.set('stats', b.stats).catch(() => {})
382 }
383 if (told.script) {
384 show(b, told.script, await $.clock.now())
385 b.isLiveScene = true
386 } else {
387 if (told.error && told.error !== b.error) $.ui.toast(`toons: ${told.error}`)
388 b.error = told.error
389 }
390 $.ui.invalidate('ui.render')
391 }
392 await $.clock.sleep(400)
393 }
394 b.isAsking = false
395 })().catch(() => {
396 // The module unloaded mid-wait (a reload): the next load asks afresh.
397 b.isAsking = false
398 })
399}
400
401// Shows or hides the cartoons; hidden, nothing is asked for or drawn.
402async function toggle($: EngineInterface, b: Buddy, show: boolean) {
403 const now = await $.clock.now()
404 accrue(b, now)
405 b.isShown = show
406 await $.store.set('isShown', show).catch(() => {})
407 if (show && b.isTurn) {
408 // The narrator missed what happened while hidden: it starts fresh.
409 note(b, now, '[the developer switched the cartoons on]')
410 ask($, b)
411 }
412 if (!show) b.pending = []
413 $.ui.invalidate('ui.render')
414}
415
416// Reads the session's cost and its plan's windows, if it has any.
417async function readPlan($: EngineInterface, b: Buddy) {
418 const usage = await $.session.usage()
419 b.isSubscription = usage.rateLimits.length > 0
420 b.windows = usage.rateLimits
421
422 return usage.cost?.usd ?? 0
423}
424
425// Changes one of the plugin's settings as /config would; the engine then
426// reloads the plugin with it. The row's key is the plugin's name (or its
427// name@inline when loaded from a folder), a dot, and the field.
428async function setOption($: EngineInterface, field: string, value: string) {
429 try {
430 // The row is found by its label rather than the plugin's name, which the
431 // engine may still hold from an older manifest until it restarts.
432 const rows = await $.config.list()
433 const row = rows.find(r => r.key.endsWith(`.${field}`) && (r.key.startsWith(`${PLUGIN}.`) || r.label.startsWith('Toons:')))
434 if (!row) throw new Error('no such setting; restart Claude Code if the plugin was renamed')
435 const { deny } = await $.config.set({ key: row.key, value })
436 if (deny) throw new Error(deny)
437 } catch (e) {
438 $.ui.toast(`toons: could not change ${field}: ${e instanceof Error ? e.message : String(e)}`)
439 }
440}
441
442// Paints the scene at about 20 frames a second while the spinner shows, each
443// frame once the last one landed.
444function animate($: EngineInterface, b: Buddy) {
445 if (b.isAnimating) return
446 b.isAnimating = true
447 void (async () => {
448 while (b.spinner && b.isShown) {
449 const at = await $.clock.now()
450 // While it grows, each new height is a redraw; frames keep painting at
451 // the height drawn until it lands.
452 if (rowsAt(b, at) !== b.drawnRows) $.ui.invalidate('ui.render')
453 const frame = frameAt(b, at, b.drawnRows)
454 await $.ui.blit({ requestId: b.spinner, key: 'oracle', cells: frame, columns: b.cols, rows: b.drawnRows }).catch(() => {})
455 await $.clock.sleep(50)
456 }
457 b.isAnimating = false
458 })().catch(() => {
459 b.isAnimating = false
460 })
461}
462
463export const register: Register = (on, options) => {
464 const settings = options as Record<string, unknown>
465 const b = createBuddy(
466 isModel(settings.model) ? settings.model : 'claude-sonnet-5-5',
467 isPace(settings.pace) ? settings.pace : 'every 15 seconds',
468 settings.thinking === 'on',
469 isStyleSet(settings.styles) ? settings.styles : 'mix',
470 sourceOf(settings.library),
471 )
472
473 on('session.start', async ($, e, next) => {
474 b.isShown = (await $.store.get('isShown')) !== false
475 b.stats = cleanStats(await $.store.get('stats'))
476 await $.command.register({
477 name: COMMAND,
478 description: 'Show or hide the cartoons under the spinner; "/toons settings" for the model, pace and cost',
479 argumentHint: '[on|off|settings]',
480 immediate: true,
481 })
482
483 return next(e)
484 })
485
486 on('command.run', { command: COMMAND }, async ($, e) => {
487 const arg = e.args.trim().toLowerCase()
488 if (arg === 'settings') {
489 await $.ui.open({ id: PANE, title: 'Toons', focus: true, closeOnEscape: true, rows: 22 })
490
491 return { text: 'Toons settings opened.' }
492 }
493 await toggle($, b, arg === 'on' ? true : arg === 'off' ? false : !b.isShown)
494
495 return {
496 text: b.isShown
497 ? `Cartoons on (${MODEL_NAMES[b.model]}, a new scene ${b.pace}). /toons settings shows what they cost.`
498 : 'Cartoons off: no scenes are requested or drawn until you turn them back on with /toons.',
499 }
500 })
501
502 on('prompt.submit', async ($, e, next) => {
503 const now = await $.clock.now()
504 b.startedAt = now
505 if (b.isShown) b.pending.push(`[task] ${e.text.replace(/\s+/g, ' ').slice(0, 400)}`)
506 b.loggedAt = now
507 b.phase = ''
508 b.running.clear()
509 b.turnTools = 0
510 b.isTurn = true
511 b.turnAt = now
512 b.accruedAt = now
513 b.turnCostAt = await readPlan($, b)
514 if (b.isShown) ask($, b)
515
516 return next(e)
517 })
518
519 on('tool.call', async ($, e, next) => {
520 const what = `${e.tool}${describe(e as unknown as Record<string, unknown>)}`
521 const id = (b.callCount += 1)
522 if (b.isTurn) b.turnTools += 1
523 const began = await $.clock.now()
524 if (b.isTurn) {
525 b.running.set(id, { what, at: began })
526 note(b, began, `-> started ${what}`)
527 }
528 let ran: Awaited<ReturnType<typeof next>>
529 try {
530 ran = await next(e)
531 } finally {
532 b.running.delete(id)
533 }
534 if (b.isTurn) {
535 const at = await $.clock.now()
536 const took = Math.round((at - began) / 1000)
537 const how = ran.deny !== undefined ? 'denied' : ran.isError ? `failed: ${(ran.text ?? '').replace(/\s+/g, ' ').slice(0, 120)}` : 'done'
538 note(b, at, `<- ${how} after ${took}s: ${what}`)
539 }
540
541 return ran
542 })
543
544 on('turn.complete', async ($, e, next) => {
545 const now = await $.clock.now()
546 accrue(b, now)
547 b.isTurn = false
548 b.spinner = undefined
549 if (b.isShown) b.pending.push('[turn finished]')
550 // What Claude's own work cost this turn, against its working time: the
551 // yardstick the settings pane measures the cartoons by.
552 if (b.turnAt > 0) {
553 const cost = await readPlan($, b)
554 b.stats.main.usd += Math.max(0, cost - b.turnCostAt)
555 b.stats.main.activeMs += now - b.turnAt
556 b.stats.main.scenes += 1
557 b.turnAt = 0
558 await $.store.set('stats', b.stats).catch(() => {})
559 }
560
561 return next(e)
562 })
563
564 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
565 await readPlan($, b)
566 const cost = summarizeCost({ stats: b.stats, model: b.model, pace: b.pace, windows: b.windows })
567 if (e.surface === 'mobile') {
568 const { Text } = $.ui.resolve(e)
569
570 return <Text>Cartoons cost about {money(cost.hour.usd)} per hour of Claude working. Change settings in the terminal.</Text>
571 }
572 const { Box, Text, Select } = $.ui.resolve(e)
573 // A label and its value on one line, the labels in one column.
574 const row = (label: string, value: string, note?: string) => (
575 <Box flexDirection="row">
576 <Box width={18}>
577 <Text dimColor>{label}</Text>
578 </Box>
579 <Text>{value}</Text>
580 {note && <Text dimColor>{` ${note}`}</Text>}
581 </Box>
582 )
583 const heading = (text: string) => (
584 <Box marginTop={1}>
585 <Text bold color="#d97757">
586 {text}
587 </Text>
588 </Box>
589 )
590 const cheapest = Math.min(...cost.models.map(m => m.usd))
591
592 return (
593 <Box flexDirection="column" paddingX={1}>
594 <Select
595 key="shown"
596 label="Cartoons "
597 options={[{ value: 'shown' }, { value: 'hidden' }]}
598 value={b.isShown ? 'shown' : 'hidden'}
599 autoFocus
600 onSelect={(value: string) => void toggle($, b, value === 'shown')}
601 />
602 <Select
603 key="model"
604 label="Director model "
605 options={MODELS.map(m => ({ value: m, label: MODEL_NAMES[m] }))}
606 value={b.model}
607 onSelect={(value: string) => void setOption($, 'model', value)}
608 />
609 <Select
610 key="pace"
611 label="New scene "
612 options={PACES.map(p => ({ value: p }))}
613 value={b.pace}
614 onSelect={(value: string) => void setOption($, 'pace', value)}
615 />
616 <Select
617 key="thinking"
618 label="Director thinks "
619 options={[{ value: 'off', label: 'off (cheaper)' }, { value: 'on' }]}
620 value={b.thread.isThinking ? 'on' : 'off'}
621 onSelect={(value: string) => void setOption($, 'thinking', value)}
622 />
623 <Select
624 key="library"
625 label="Scenes "
626 options={[
627 { value: 'ready-made only', label: 'ready-made only: free, no tokens used' },
628 { value: 'mix', label: 'mix: ready-made, fresh ones for news (~1-3% extra)' },
629 { value: 'fresh only', label: 'fresh only: every scene drawn new, uses far more tokens' },
630 ]}
631 value={b.source}
632 onSelect={(value: string) => void setOption($, 'library', value)}
633 />
634 <Select
635 key="styles"
636 label="Scene styles "
637 options={Object.keys(STYLE_SETS).map(value => ({ value, label: value === 'mix' ? 'mix (3D, pixel art, text art)' : value }))}
638 value={b.thread.styles}
639 onSelect={(value: string) => void setOption($, 'styles', value)}
640 />
641 <Text dimColor>Toggle any time with /toons, even while Claude works</Text>
642
643 {b.isFree ? (
644 <Box flexDirection="column">
645 {heading('Cost')}
646 {row('Cartoons', 'free', 'ready-made scenes only: no requests, no tokens')}
647 </Box>
648 ) : (
649 <Box flexDirection="column">
650 {heading('Per hour of Claude working')}
651 {cost.claude ? row("Claude's own work", `~${money(cost.claude.usd)}`, 'measured') : row("Claude's own work", 'not measured yet', 'after ~10 min of Claude working')}
652 {row(
653 'Cartoons',
654 `~${money(cost.hour.usd)}`,
655 `~${Math.round(cost.hour.scenes)} scenes, ${cost.hour.measuredMinutes !== undefined ? `measured over ${cost.hour.measuredMinutes} min` : 'estimate'}`,
656 )}
657 {cost.claude &&
658 row('One scene', money(cost.hour.usd / Math.max(1, cost.hour.scenes)), `costs about ${cost.claude.sceneSeconds < 1 ? 'a second' : `${Math.round(cost.claude.sceneSeconds)} seconds`} of Claude's own work`)}
659 {cost.perScene &&
660 row(
661 'Per scene',
662 money(cost.perScene.usd),
663 `${Math.round(cost.perScene.outputShare * 100)}% is the scene itself (~${Math.round(cost.perScene.output)} tokens); the rest is the cached history (~${Math.round(cost.perScene.cacheRead / 1000)}k tokens, mostly cache reads)`,
664 )}
665
666 {heading('Each model at this pace')}
667 {cost.models.map(m => (
668 <Box flexDirection="row">
669 <Box width={18}>
670 <Text bold={m.model === b.model} dimColor={m.model !== b.model}>
671 {m.model === b.model ? `> ${m.name}` : ` ${m.name}`}
672 </Text>
673 </Box>
674 <Text bold={m.model === b.model} dimColor={m.model !== b.model}>{`~${money(m.usd)}/hr`}</Text>
675 <Text dimColor>{` ~${Math.round(m.scenes)} scenes${m.usd === cheapest ? ', cheapest' : ''}`}</Text>
676 </Box>
677 ))}
678 </Box>
679 )}
680
681 {heading('Usage')}
682 {cost.windows.length > 0 && row('Your plan', cost.windows.map(w => `${w.label} ${w.percent}%`).join(' · '))}
683 {b.isFree
684 ? row('This session', `${b.sessionStock} ready-made scenes`, 'free')
685 : row('This session', `${b.sessionScenes} scenes · ${money(b.sessionUsd)}`, b.sessionStock > 0 ? `and ${b.sessionStock} ready-made, free` : undefined)}
686 {b.lastTrouble && row('Last scene error', b.lastTrouble.slice(0, 70), 'sent back to the director to fix')}
687
688 <Box marginTop={1}>
689 <Text dimColor>
690 {b.isSubscription
691 ? "On a subscription neither line is charged separately: both count toward the same usage limits, each in proportion to its cost."
692 : "Billed to this session's API key."}{' '}
693 Hidden or idle: nothing.
694 </Text>
695 </Box>
696 </Box>
697 )
698 })
699
700 on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
701 if (e.surface !== 'terminal') return next(e)
702 const spinnerLine = await next(e)
703 if (!b.isShown) {
704 b.spinner = undefined
705
706 return spinnerLine
707 }
708 const { Box, Raster } = $.ui.resolve(e)
709 // The spinner knows when Claude turns to thinking or to writing its reply.
710 const nextPhase = e.props.mode === 'thinking' ? 'thinking' : e.props.mode === 'responding' ? 'writing the reply' : ''
711 if (b.isTurn && nextPhase && nextPhase !== b.phase) {
712 const at = await $.clock.now()
713 b.phase = nextPhase
714 b.phaseAt = at
715 note(b, at, `[Claude is ${nextPhase}]`)
716 } else if (!nextPhase) {
717 b.phase = ''
718 }
719 const now = await $.clock.now()
720 if (b.spinner !== e.requestId) b.shownAt = now
721 b.spinner = e.requestId
722 // A little narrower than the screen, so the spinner's indent never clips it.
723 b.cols = Math.max(20, Math.min(220, (e.viewport?.columns ?? 80) - 6))
724 b.drawnRows = rowsAt(b, now)
725 const cells = frameAt(b, now, b.drawnRows)
726 if (b.sceneAt === 0) b.sceneAt = now
727 animate($, b)
728
729 return (
730 <Box flexDirection="column">
731 {spinnerLine}
732 <Raster key="oracle" columns={b.cols} rows={b.drawnRows} cells={cells} />
733 </Box>
734 )
735 })
736}
737hooks/cost.ts 147 lines1// What the cartoons cost: API prices per model, what a scene typically
2// spends, and an hour of Claude working with cartoons on, put next to what
3// Claude's own work costs so the number means something on a subscription.
4
5export const MODELS = ['claude-haiku-4-5', 'claude-sonnet-5-5', 'claude-opus-5-5'] as const
6export type Model = (typeof MODELS)[number]
7
8export const MODEL_NAMES: Record<Model, string> = {
9 'claude-haiku-4-5': 'Haiku 4.5',
10 'claude-sonnet-5-5': 'Sonnet 5.5',
11 'claude-opus-5-5': 'Opus 5.5',
12}
13
14export const PACES = ['as fast as possible', 'every 15 seconds', 'every 30 seconds', 'every minute'] as const
15export type Pace = (typeof PACES)[number]
16
17// The least time between two scene requests, per pace.
18export const GAP_MS: Record<Pace, number> = {
19 'as fast as possible': 1500,
20 'every 15 seconds': 15_000,
21 'every 30 seconds': 30_000,
22 'every minute': 60_000,
23}
24
25export const isModel = (v: unknown): v is Model => MODELS.includes(v as Model)
26export const isPace = (v: unknown): v is Pace => PACES.includes(v as Pace)
27
28export type CallUsage = { input: number; output: number; cacheRead: number; cacheWrite: number }
29
30// US dollars per million tokens, at Anthropic's API list prices.
31const PRICES: Record<Model, CallUsage> = {
32 'claude-haiku-4-5': { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
33 'claude-sonnet-5-5': { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
34 'claude-opus-5-5': { input: 4, output: 20, cacheRead: 0.2, cacheWrite: 5 },
35}
36
37export const costOf = (model: Model, u: CallUsage) => {
38 const p = PRICES[model]
39
40 return (u.input * p.input + u.output * p.output + u.cacheRead * p.cacheRead + u.cacheWrite * p.cacheWrite) / 1e6
41}
42
43// Seconds a scene takes to come back, per model, Sonnet's measured at about
44// 9 (a request waits for the last one, so "as fast as possible" is bounded by
45// it). A faster model draws more scenes an hour at that pace, so Haiku's
46// hourly cost there lands near Sonnet's despite costing half per scene.
47const LATENCY_S: Record<Model, number> = { 'claude-haiku-4-5': 5, 'claude-sonnet-5-5': 9, 'claude-opus-5-5': 14 }
48
49// A typical scene's tokens on Sonnet 5.5 (measured at about 1.7 cents a
50// scene): the cached conversation read back, what each exchange adds to it,
51// and the scene itself. The scene's own tokens are most of the cost.
52const TYPICAL: CallUsage = { input: 300, output: 1100, cacheRead: 30_000, cacheWrite: 1500 }
53
54export const scenesPerHour = (model: Model, pace: Pace) => 3600 / Math.max(GAP_MS[pace] / 1000, LATENCY_S[model])
55
56// What one model and pace has spent, and over how much of Claude's working
57// time with the cartoons showing.
58export type Spend = { usd: number; scenes: number; activeMs: number; tokens?: CallUsage }
59
60export const addTokens = (spend: Spend, u: CallUsage) => {
61 const t = (spend.tokens ??= { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 })
62 t.input += u.input
63 t.output += u.output
64 t.cacheRead += u.cacheRead
65 t.cacheWrite += u.cacheWrite
66}
67export type Stats = { buddy: Record<string, Spend>; main: Spend }
68
69export const emptyStats = (): Stats => ({ buddy: {}, main: { usd: 0, scenes: 0, activeMs: 0 } })
70export const spendKey = (model: Model, pace: Pace) => `${model}|${pace}`
71
72// Measured figures count once they cover this much working time.
73const ENOUGH_MS = 10 * 60_000
74
75// Dollars per hour of Claude working, measured where there is enough of it.
76const hourly = (spend: Spend | undefined) => (spend && spend.activeMs >= ENOUGH_MS ? spend.usd / (spend.activeMs / 3_600_000) : undefined)
77
78export function estimate(stats: Stats, model: Model, pace: Pace) {
79 const measured = hourly(stats.buddy[spendKey(model, pace)])
80 const spend = stats.buddy[spendKey(model, pace)]
81
82 return {
83 usd: measured ?? scenesPerHour(model, pace) * costOf(model, TYPICAL),
84 scenes: measured !== undefined && spend ? spend.scenes / (spend.activeMs / 3_600_000) : scenesPerHour(model, pace),
85 measuredMinutes: measured !== undefined && spend ? Math.round(spend.activeMs / 60_000) : undefined,
86 }
87}
88
89// Dollars as a person reads them: cents under a dollar, then fewer digits.
90export const money = (usd: number) => (usd < 0.995 ? `$${usd.toFixed(2)}` : `$${usd.toFixed(usd < 9.95 ? 2 : 0)}`)
91
92export type Window = { kind: string; percentUsed: number }
93
94export type CostSummary = {
95 // An hour of Claude working with the cartoons on, at this model and pace.
96 hour: { usd: number; scenes: number; measuredMinutes?: number }
97 // Claude's own work, once measured: what an hour of it costs, and how many
98 // seconds of it cost as much as one scene, so the two read in one unit.
99 claude?: { usd: number; sceneSeconds: number }
100 // What a scene spends, from the tokens measured so far at this model (any
101 // pace), with the share of its cost that is the scene itself (output).
102 perScene?: { usd: number; output: number; cacheRead: number; outputShare: number }
103 // An hour at this pace on each model.
104 models: { model: Model; name: string; usd: number; scenes: number }[]
105 // The plan's windows, where the session has any.
106 windows: { label: string; percent: number }[]
107}
108
109export function summarizeCost(args: { stats: Stats; model: Model; pace: Pace; windows: Window[] }): CostSummary {
110 const { stats, model, pace, windows } = args
111 const hour = estimate(stats, model, pace)
112 const claudeUsd = hourly(stats.main)
113 const share = claudeUsd ? hour.usd / claudeUsd : undefined
114 // Every scene on this model, whatever the pace.
115 const mine = Object.entries(stats.buddy)
116 .filter(([key, s]) => key.startsWith(`${model}|`) && s.tokens)
117 .map(([, s]) => s)
118 const scenes = mine.reduce((n, s) => n + s.scenes, 0)
119 const tokens = mine.reduce(
120 (sum, s) => ({
121 input: sum.input + (s.tokens?.input ?? 0),
122 output: sum.output + (s.tokens?.output ?? 0),
123 cacheRead: sum.cacheRead + (s.tokens?.cacheRead ?? 0),
124 cacheWrite: sum.cacheWrite + (s.tokens?.cacheWrite ?? 0),
125 }),
126 { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
127 )
128 const usd = costOf(model, tokens)
129 const outputUsd = costOf(model, { ...tokens, input: 0, cacheRead: 0, cacheWrite: 0 })
130
131 return {
132 hour,
133 perScene:
134 scenes >= 5
135 ? { usd: usd / scenes, output: tokens.output / scenes, cacheRead: tokens.cacheRead / scenes, outputShare: usd > 0 ? outputUsd / usd : 0 }
136 : undefined,
137 claude:
138 claudeUsd && share !== undefined
139 ? { usd: claudeUsd, sceneSeconds: (hour.usd / Math.max(1, hour.scenes)) / (claudeUsd / 3600) }
140 : undefined,
141 models: MODELS.map(m => ({ model: m, name: MODEL_NAMES[m], usd: estimate(stats, m, pace).usd, scenes: estimate(stats, m, pace).scenes })),
142 windows: windows
143 .filter(w => w.kind === 'five_hour' || w.kind === 'seven_day')
144 .map(w => ({ label: w.kind === 'five_hour' ? '5-hour' : 'weekly', percent: w.percentUsed })),
145 }
146}
147hooks/library.ts 127 lines1// A library of ready-made scenes, one for each kind of thing Claude does,
2// dealt for free while the work is routine, so the director is asked only
3// when something worth a new picture happens.
4
5import { SCENES, type Stock } from './scenes'
6
7export const PHASES = ['thinking', 'searching', 'reading', 'editing', 'testing', 'building', 'running', 'git', 'web', 'agents', 'writing'] as const
8export type Phase = (typeof PHASES)[number]
9export const isPhase = (v: unknown): v is Phase => PHASES.includes(v as Phase)
10
11// What a scene of each phase is about, for whoever generates one.
12export const ABOUT: Record<Phase, string> = {
13 thinking: 'Claude is thinking: working out what to do next, weighing options, puzzling over a problem',
14 searching: 'Claude is searching the codebase: grepping for a pattern, globbing for files, hunting for where something is defined',
15 reading: 'Claude is reading code: going through files to understand how they work',
16 editing: 'Claude is editing code: writing and changing files, making the fix',
17 testing: 'Claude is running the tests and waiting to see if they pass',
18 building: 'Claude is building or installing: a compile, a bundle, a package install that takes a while',
19 running: 'Claude is running a command in the shell and waiting for it',
20 git: 'Claude is working with git: committing, branching, looking at history or a diff',
21 web: 'Claude is fetching a web page or searching the web for an answer',
22 agents: 'Claude has sent out subagents to work on parts of the task in parallel and is waiting for them',
23 writing: 'Claude is writing its reply to the developer, the work done',
24}
25
26// The phase a line of the log belongs to, if it names one: a tool starting,
27// or the spinner turning to thinking or writing.
28export function phaseOf(line: string): Phase | undefined {
29 if (/\[Claude is thinking\]/.test(line)) return 'thinking'
30 if (/\[Claude is writing the reply\]/.test(line)) return 'writing'
31 const m = /-> started (\w+)(?: `([^`]*)`)?/.exec(line)
32 if (!m) return undefined
33 const tool = m[1] ?? ''
34 const args = m[2] ?? ''
35 if (/^(Read|NotebookRead)$/.test(tool)) return 'reading'
36 if (/^(Grep|Glob|LS|Explore)$/.test(tool)) return 'searching'
37 if (/^(Edit|Write|MultiEdit|NotebookEdit)$/.test(tool)) return 'editing'
38 if (/^(WebFetch|WebSearch)$/.test(tool) || /^mcp__/.test(tool)) return 'web'
39 if (/^(Agent|Task|Workflow)$/.test(tool)) return 'agents'
40 if (tool === 'Bash') {
41 if (/\b(test|tests|jest|vitest|pytest|mocha|spec|rspec|go test|cargo test)\b/.test(args)) return 'testing'
42 if (/\b(build|tsc|make|cargo (build|check)|compile|webpack|vite|bundle|install|npm ci|yarn|pnpm|pip|poetry|gradle|mvn|xcodebuild)\b/.test(args)) return 'building'
43 if (/\b(git|gh)\b/.test(args)) return 'git'
44
45 return 'running'
46 }
47
48 return undefined
49}
50
51// How long a scene keeps the strip before a stock scene may replace it: at
52// least DWELL_MS (a fresh scene from the director, LIVE_DWELL_MS, since it
53// was paid for and is about this moment), then when the work turns to a new
54// phase, or after STOCK_MS in any case. A scene whose code broke goes at once.
55export const DWELL_MS = 20_000
56export const LIVE_DWELL_MS = 45_000
57export const STOCK_MS = 60_000
58
59export function isStockDue(scene: { age: number; isBroken: boolean; isLive: boolean; isNewPhase: boolean }) {
60 if (scene.isBroken) return true
61 if (scene.age < (scene.isLive ? LIVE_DWELL_MS : DWELL_MS)) return false
62
63 return scene.isNewPhase || scene.age >= STOCK_MS
64}
65
66// Whether a line of the log is news worth a scene of its own from the
67// director: the task, a failure, the end of the turn, a scene that broke.
68export const isInteresting = (line: string) => /^\[task\] |^\+\d+s <- (failed|denied)|^\[turn finished\]|^\[your last scene's code/.test(line)
69
70// The thing Claude last touched, short enough to label a prop: a file's
71// name, or the head of a command.
72export function whatOf(line: string): string | undefined {
73 const m = /-> started \w+ `([^`|]*)/.exec(line)
74 if (!m) return undefined
75 const arg = m[1]?.trim() ?? ''
76 if (!arg) return undefined
77 const name = /^[\w./-]+$/.test(arg) && arg.includes('/') ? arg.slice(arg.lastIndexOf('/') + 1) : arg.split(' ').slice(0, 2).join(' ')
78
79 return name.replace(/["\\]/g, '').slice(0, 20) || undefined
80}
81
82export const STYLES = ['3D', 'pixel art', 'text art'] as const
83export type Style = (typeof STYLES)[number]
84
85// How a scene is really drawn, by what its code calls: the style it was
86// dealt is a request the model may not have honored.
87export function styleOf(scene: unknown): Style {
88 const code = JSON.stringify(scene)
89 if (/\b(camera|mesh3d|clawd3d|line3d)\(/.test(code)) return '3D'
90 if (/\bpixels?\(/.test(code)) return 'pixel art'
91
92 return 'text art'
93}
94
95// The stock, dealt like cards: each phase has a shuffled deck of its scenes
96// in the styles allowed, and a scene comes around again only once the rest
97// of its deck has been dealt.
98export type Dealer = { styles: readonly string[]; decks: Map<Phase, Stock[]> }
99
100export const createDealer = (styles: readonly string[]): Dealer => ({ styles, decks: new Map() })
101
102// A scene for a phase, as the raw object the director would have answered,
103// with "{what}" in it filled in. Nothing when the stock has none for the
104// phase in the styles allowed.
105export function deal(dealer: Dealer, phase: Phase, what: string | undefined, random: () => number = Math.random): { concept: string; raw: unknown } | undefined {
106 let deck = dealer.decks.get(phase)
107 if (!deck || deck.length === 0) {
108 deck = SCENES.filter(s => s.phase === phase && dealer.styles.includes(s.style)).sort(() => random() - 0.5)
109 dealer.decks.set(phase, deck)
110 }
111 const pick = deck.pop()
112 if (!pick) return undefined
113 const filled = JSON.stringify(pick.scene).split('{what}').join(JSON.stringify(what ?? 'the code').slice(1, -1))
114
115 return { concept: pick.concept, raw: JSON.parse(filled) }
116}
117
118// The brief a generator adds to the director's prompt for one stock scene.
119export function brief(phase: Phase, world: string, cols: number) {
120 return [
121 `[strip ${cols}x9]`,
122 `[task] (unknown: this is a ready-made scene)`,
123 `This scene goes into a library of ready-made scenes, dealt whenever ${ABOUT[phase]}. No more news will come while it plays, for a minute or two, so it has to tell its own story: something happens, and keeps happening, in this world, about this kind of work. Clawd has a voice here; what it says and when is yours. The real file or command is not known: write {what} wherever a label or a line would name it, and it is filled in when the scene is dealt (up to 20 characters).`,
124 `[world: ${world}]`,
125 ].join('\n')
126}
127hooks/narrator.ts 461 lines1// The buddy's one conversation with Sonnet: append-only for the session, so
2// each request reads the earlier ones from the prompt cache. Every update is a
3// user message holding what the main agent did since the last; every reply is
4// kept exactly as returned, thinking blocks included.
5
6import type { CallUsage, Model } from './cost'
7import { EFFECTS } from './effects'
8import { cleanScript, type Script } from './script'
9
10
11// A request on the session's own subscription login opens its system prompt
12// as Claude Code's own requests do.
13const IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude."
14
15// The director's prompt, built from only what the styles allowed need: a
16// model never sees the parts of the drawing language it may not use, so it
17// cannot reach for them.
18const HEAD = `You direct a tiny animated cartoon that plays inside Claude Code's spinner. Claude Code is a terminal app where Claude, an AI coding agent, works on a developer's code. While Claude works, a strip of the terminal under the "thinking" spinner is yours: 9 rows tall and as wide as the terminal. Every few seconds you get a log of what Claude just did and answer with a new scene for the developer to watch while they wait.
19
20Each user message starts with the strip's size, "[strip 120x9]" (columns x rows), then the log of what happened since your last scene, each line stamped with seconds since the task began:
21- "[task] ..." is what the developer asked for.
22- "-> started Bash \`npm test\`" means Claude just launched a tool; "<- done after 12s: Bash \`npm test\`" or "<- failed: 3 tests failing ... after 12s: Bash \`npm test\`" means it finished.
23- "[Claude is thinking]" or "[Claude is writing the reply]" means Claude turned to thinking or to writing its answer.
24- "still running Bash \`npm test\` (25s so far)" or "still thinking (15s so far)" means nothing new has happened for a while and the scene playing has run its course. Answer the next beat of the same scene: keep its world, set and cast and move the action along (the countdown reaches 1, the crane lifts the next block, night falls on the siege), never start over.
25- "[turn finished]" means Claude stopped.
26- "[your last scene's code stopped: ...]" means the code you wrote hit an error or ran out of time, and the error. Fix that mistake from now on.
27- "[recent scenes: ...]" lists the concepts of your last few scenes. Do not repeat any of them, or anything close.
28- "[world: ...]" is the setting this scene must live in (deep sea, wild west, cooking show...). Translate the real work into that world with wit: in a cooking show a failing test is a fallen soufflé, in the wild west a bug is an outlaw on a wanted poster. Fresh worlds keep the cartoon from going stale, so commit to it.`
29const STYLE_LINES: Record<string, string> = {
30 '3D': '"3D" means the scene is a 3D world drawn with camera, mesh3d, clawd3d and friends (a 2D Clawd or labels may sit on top)',
31 'pixel art': '"pixel art" means the set and props are drawn with pixels() on the pixel grid, in several colors, Clawd dressed with pixel art at its anchors',
32 'text art': '"text art" means sprites and text',
33}
34const DRAWING = `You see the whole session in this one conversation.
35
36Tell the story visually. The picture carries the meaning: what Claude is doing, how it is going, what just broke or got fixed. There is no caption, and nothing should read like a status line. Text belongs in the picture only as labels on things, with the real names from the log: the file name on a crate or a book spine, the test name on a banner, the command on a little terminal.
37
38What makes a scene good:
39- Show what the work means, not its mechanics. "Claude reads a file" is how; the story is why and how it is going. A bug hunt is a safari stalking a beetle, a stakeout, a bomb squad, a mole-whacking arcade; tracing a config is following a river to its source; a refactor is moving house. A long run of reads and searches is one investigation with an arc (setting out, a lead, a dead end, closing in, the reveal), and each new scene is its next chapter in a new place, never the same shot again.
40- Banned, because it is the scene you reach for by default and the developer is tired of it: Clawd walking past or around files, documents, papers, scrolls, books or folders, with or without a magnifying glass. Files may appear only as labels on things that belong to the world (a crate, a fish, a planet, a wanted poster).
41- Vary Clawd's role and the shot, not just the props: Clawd rides, pilots, swims, digs, cooks, conducts, hides, sleeps, is chased, is tiny in a huge set, watches from a corner while the world does the work, or is mid-leap. Walking left to right is one option of many and rarely the best.
42- Some starting points, not a menu: searching is sonar sweeping, a metal detector on a beach, a sniffer dog; editing is a crane placing blocks, a tailor stitching, a surgeon; running tests is a racetrack, an exam hall, a castle siege where failures are bugs storming the walls; builds and installs are a factory conveyor or a rocket on the pad; git is trains on branching tracks; web requests are a fishing boat or a carrier pigeon; subagents are helpers hatching; waiting on a long command is a campfire at night; success is a parade, fireworks, a podium.
43- Clawd, the Claude Code mascot, stands in for Claude and is in nearly every scene: it carries the file, drives the crane, flees the bugs, climbs the podium. It may also make an entrance late, peek in from the edge, or be inside something (a submarine porthole, a cockpit). In code, Clawd can be dressed, recolored, scaled and posed (see clawd below), so give it a costume and a role that fit the world: a helmet on the construction site, a cape for the hero, a chef's hat on the cooking show, green when seasick, two Clawds of different colors as rivals. Add Clawd as an actor with kind "clawd" and frames [] (the stage draws Clawd itself in pixel art, 14 columns wide and 4 rows tall, in its own orange). Clawd animates on its own: it bobs gently and looks the way it goes whenever its x changes, and blinks when it stands still. So give Clawd motion through x and y, and put its props beside it as separate sprite actors. Every other actor has kind "sprite".
44- Everything around Clawd changes when the work changes: for new work, never reuse the metaphor, the set, or the supporting characters of any of your last five scenes. (A "still ..." beat is the exception: it is the next beat of the scene that is playing.)
45- Play with the frame itself: big set pieces that fill the strip, a skyline, a parallax background scrolling slower than the foreground, a split screen, a giant thing only partly in view, a tiny world seen from far away, the whole scene scrolling past like a camera pan, a silhouette against a sunset. The strip is wide; use it.
46- Motion with intent, timed like a cartoon. You are free to choose the pace the moment calls for: a slow stakeout, a frantic chase, a rocket blasting off, an explosion, a pratfall, a leap, a long fall, a zoom across the strip. Use anticipation and payoff: a beat of stillness, then the action; things ease in and out (smoothstep) rather than starting and stopping dead. A scene can be a little story of several beats: with "show" things appear, vanish, get swapped (the closed chest becomes the open one at t=3, the bug pops when the hammer lands), and with "frame" a sprite's animation follows the action rather than looping. The one thing to avoid is aimless noise: jitter, constant wobble, everything moving at once with no focus.
47- A composed set, not a figure on an empty strip: still set pieces (fps 0) such as shelves, buildings, servers, trees, a terminal window, a track, waves, a horizon line, drawn with box drawing (─│┌┐└┘├┤┬┴┼═║╔╗╚╝), blocks (█▓▒░▀▄▌▐), braille (⣿⣶⣤⡇) and ASCII; then characters and props moving and interacting in front of them.
48- Clean layout. Place things on purpose with the strip size in hand: assign every element its own column range before writing its x. Actors are solid: the space inside each line of a sprite, from its first character to its last, hides whatever is behind it, and later actors are drawn in front. List set pieces first and characters after. No character standing on a sign, no text running into a box, no two sprites sharing cells unless one is deliberately passing in front of the other.
49- Color with intent: each sprite is one color, so split a multicolored thing into several actors (you have up to 20). All colors must be clearly visible on a dark background.
50- These are guides, not a cage. Surprise the developer: an unexpected angle, a sight gag, a callback to an earlier scene, a tiny running subplot in the corner.
51
52Speech: Clawd can say one short line to the developer in a speech bubble. Make it fun to read: jokes, puns and a bit of personality are welcome. But it must be about what is actually happening and make sense at a glance to someone who has not read the log: name the real thing (the file, the test, the error) and say something about it a person would get immediately. "3 tests vs. one undefined password. undefined is winning." works; "the scrolls whisper of absence" does not. No riddles, nothing cryptic, nothing that only makes sense if you already know the metaphor. Skip speech when there is nothing worth saying: leave say empty.
53
54You have two ways to draw, and can mix them in one scene:
551. Declarative: actors, particles and a background, each moved by a math expression of time (below). Quick for simple staging.
562. Code: a real program in the "code" field, for anything the declarative parts cannot do: physics and simulations (gravity, bouncing, flocks, fluids, cellular automata, growing plants, sand piling up), transformations (morphing one shape into another, zooming, ripples and distortion, a scrolling world, a camera pan, text that shatters or assembles), per-cell shaders (fire, water, clouds, plasma painted with background colors), procedural sets, characters that react to each other, state that builds up over time. Reach for code whenever the idea is more than things sliding around: it is how the best scenes are made.
57
58The code language is JavaScript, interpreted: let/const/var, functions and arrow functions (closures), if/else, switch, for, for-of, for-in, while, do-while, break/continue/return, ternaries, template literals, arrays and plain objects (with destructuring and ...spread), the usual operators, Math.*, Array.from, Object.keys/values/entries, and array methods (push pop shift unshift slice splice concat indexOf includes join reverse fill map filter forEach some every find findIndex reduce sort flat at) and string methods (slice substring split repeat padStart padEnd toUpperCase toLowerCase trim replace includes startsWith endsWith charAt charCodeAt at). No classes, no regex, no "this", no async. The top level runs once when the scene starts: set up state there. Then define function frame(t, dt), called about 20 times a second with t (seconds since the scene started) and dt (seconds since the last frame); it redraws the whole picture each time on a fresh strip, and its variables at the top level persist between frames. The globals w and h are the strip's columns and rows, and t and dt are also globals.
59Drawing (x is the column, y the row, 0,0 top-left; anything off the strip is clipped; colors are "#rrggbb" or numbers from rgb/hsl/mix):
60- put(x, y, ch, color, bg?) one character; with bg it also paints the cell's background, so put(x, y, ' ', null, bg) paints a solid colored cell, and ▀▄ with a color and a bg give two pixels per cell
61- text(x, y, str, color, bg?) a line of text
62- sprite(x, y, art, color, bg?) multi-line art (lines split on \\n), solid inside its outline
63- fill(x, y, w, h, ch, color, bg?), line(x0, y0, x1, y1, ch, color), circle(cx, cy, r, ch, color) (r in rows; drawn twice as wide so it looks round), disc(cx, cy, r, ch, color, bg?)
64- clawd(x, y, options) draws Clawd, 14 wide and 4 rows tall at scale 1, x, y its top-left (y may end in .5 to sit half a row lower). Options: pose ("stand", "walk", "jump", "sit"), stride (0 to 3, the walk frame: use floor(x/1.5)%4 so its feet match its travel), facing (-1 left, 0 ahead, 1 right), eyes ("open", "closed", "wide"), look ({x, y} in pixels, a glance), blink (true/false), color (any color: Clawd can be painted, sunburnt, frozen blue, camouflaged), eyeColor, scale (1 to 4; 2 is 28 wide and 8 rows tall), arms ("up", "out" or "down" for both, or {left, right}: change it over time to wave, point, dig, cheer, carry; the left and right anchors follow the claw tips, so a prop drawn at c.right moves with the claw). The short form clawd(x, y, facing, stride, blink, color) still works. It returns where things attach: {x, py, row, w, h, top, left, right, feet, eyes}, each anchor {x, py, row} with py the pixel row (for pixel and pixels) and row the text row (for text, sprite and say), so a hat goes at pixels(c.top.x - 2, c.top.py - 3, ...), a prop in its hand at c.right, a shadow under c.feet, and a bubble at say(line, c.top.x, c.row). Dress Clawd for the scene: a hard hat, a crown, a snorkel, a cape, sunglasses over c.eyes, a lantern at c.left, and recolor it when the story calls for it.`
65const PIXELS = `- pixel(x, py, color) and pixels(x, py, art, palette) draw at pixel resolution: pixel rows are twice as fine as text rows (py 0 to 2h-1; pixel row py is in text row floor(py/2)), the same grid Clawd is drawn on, so pixel art sits flush with it. In pixels(), art is lines of characters and palette maps each character to a color ({"#": "#fc0", "o": "#000"}); "." and " " are transparent. Pixels and text can mix in one cell. Use this for anything that should look drawn rather than typed: props, creatures, vehicles, a whole pixel-art set.`
66const MID = `- say(text, x, y) a speech bubble pointing at x, y (Clawd's head is about x+7, y)
67- rgb(r, g, b), hsl(hue 0-360, sat 0-1, light 0-1), mix(c1, c2, k) make colors
68- clamp(v, lo, hi), lerp(a, b, k), smoothstep(a, b, v), fract(v), mod(a, b), rand(n) (a fixed random number per n), noise(x) and noise2(x, y) (smooth noise in [0,1)), and Math.random()`
69const WORLD3D = `3D: the strip can also be a window into a world. Surfaces are lit smoothly (diffuse plus a highlight) and drawn as solid cells in their lit color with block-glyph edges, clean and flat; a mesh given ascii: true is drawn instead as glyphs as dense as its brightness (" .,:;-=+*#%@"), the classic terminal look, good for a planet, a donut, a curved hero object, not for floors and walls. Give surfaces mid-to-light colors (dark ones vanish into the fog), and keep the camera and light steady or moving smoothly. camera(ex, ey, ez, tx, ty, tz, fov) sets the eye, what it looks at, and the angle it sees across (default eye 0,2.5,9 looking at 0,1,0, fov 90: at the origin that shows about 20 units across and 3 units tall, so the world is wide and low, like the strip; build things about 1 to 2 units tall and spread them left to right, and move the camera to fly, orbit or dolly). light(dx, dy, dz, ambient) is the direction toward the light; fog(near, far, color) fades things with distance (default 8 to 40 into black: that is the depth cue, keep it). Shapes: box(w, h, d) (centered), sphere(r, segments), cylinder(r, h, segments) and cone(r, h, segments) (standing on y 0), plane(w, d) (flat on the ground), or your own {verts: [x,y,z,...], faces: [[i,j,k,...], ...]}. mesh3d(mesh, {x, y, z, rx, ry, rz, scale, color, wire, unlit, ascii}) draws one, lit smoothly; wire draws its edges; unlit skips the lighting; ascii uses the glyph ramp. line3d(x0,y0,z0, x1,y1,z1, color) and point3d(x, y, z, color) for rails, rain, stars. clawd3d(x, y, z, options) puts Clawd standing at a world point as a solid lit like any mesh, its eyes flat on its face, sized by distance (options as clawd, plus size: its height in units, 0.9 by default: about as tall as a crate, a third of the strip at the camera's target; it is never drawn taller than most of the strip, and one whose feet would fall below the strip stands on its bottom edge instead), so it can walk down a road into the distance; it returns the same anchors, or null behind the camera. project(x, y, z) gives {x, y, px, py, depth, scale} for placing text, a bubble or 2D art at a world point. Everything 3D in a frame shares one depth buffer, so later draws go behind nearer ones. The camera, light and fog keep between frames. Good 3D scenes: a road or rails vanishing to a point with things passing, a planet with a moon orbiting, a city of boxes at night, a tunnel flying through, a chessboard, a spinning gear, a crane lifting a crate, an orbiting camera around one hero object. Keep it to a few dozen shapes; big flat-shaded shapes read better than detail at this size. Mix freely with 2D: a 3D set behind a 2D Clawd, or text labels placed with project().`
70const CODING = `Write the code compactly: no comments, no blank lines, short names, nothing decorative, and keep it under about 60 lines; every token of it is paid for. The code draws over the background effect and particles and under the actors. Keep each frame light: a few thousand steps is fine (a loop over every cell of the strip with a little math each is fine), heavy nested loops are not, and a frame that runs too long or throws stops the code for the rest of the scene. When the code draws everything, leave actors and particles empty and set the background's intensity to 0. When the code draws Clawd and its speech, do not also add a "clawd" actor. "" for no code.
71
72A code example, for the shape of it (do not copy the idea):
73let drops = Array.from({length: 40}, (_, i) => ({x: rand(i) * w, y: rand(i + 50) * h, v: 4 + rand(i + 99) * 6}))
74let cx = -14
75function frame(t, dt) {
76 for (let x = 0; x < w; x++) put(x, h - 1, x % 7 == 0 ? '┴' : '─', '#5a6b7c')
77 for (const d of drops) {
78 d.y += d.v * dt
79 if (d.y > h - 1) { d.y = 0; d.x = rand(t + d.x) * w }
80 put(d.x, d.y, '│', '#6fa8dc')
81 }
82 cx = Math.min(w / 2 - 7, cx + 5 * dt)
83 const walking = cx < w / 2 - 7
84 clawd(cx, h - 5, walking ? 1 : 0, walking ? Math.floor(cx / 1.25) % 4 : -1, !walking && fract(t * 0.4) < 0.05)
85 sprite(cx + 2, h - 8, ' ▄███▄ \\n▀▀▀█▀▀▀\\n │ ', '#e06c75')
86 if (!walking) say('npm test in the rain. 3 failing, umbrella holding.', cx + 7, h - 8)
87}
88`
89const EXAMPLE3D = `A 3D example, for the shape of it (do not copy the idea): a road into the distance with crates passing and Clawd on it.
90const crate = box(1.2, 1.2, 1.2)
91const post = cylinder(0.08, 2, 6)
92const crates = Array.from({length: 6}, (_, i) => ({x: i % 2 ? 3 : -3, z: -4 - i * 6, spin: rand(i) * 3}))
93function frame(t, dt) {
94 camera(0, 2.2, 8 - t * 1.5, 0, 1, -30, 90)
95 fog(10, 45, "#000")
96 for (let i = -2; i < 12; i++) {
97 const z = -i * 6 + mod(t * 1.5, 6)
98 mesh3d(post, {x: -5, z, color: "#8a8"})
99 mesh3d(post, {x: 5, z, color: "#8a8"})
100 line3d(-5, 2, z, 5, 2, z, "#575")
101 }
102 line3d(-5, 0, 10, -5, 0, -80, "#aaa")
103 line3d(5, 0, 10, 5, 0, -80, "#aaa")
104 let nearest = null
105 for (const c of crates) {
106 c.z += dt * 2.5
107 if (c.z > 10) c.z -= 40
108 mesh3d(crate, {x: c.x, y: 0.6, z: c.z, ry: c.spin + t, color: "#e0a060"})
109 if (c.z < 6 && (!nearest || c.z > nearest.z)) nearest = c
110 }
111 // One label, on the nearest crate, placed with project().
112 const p = nearest && project(nearest.x, 1.5, nearest.z)
113 if (p) text(p.x - 3, p.y - 1, "auth.ts", "#ffd")
114 const me = clawd3d(0, 0, -1 - t * 1.5, {facing: 0, stride: Math.floor(t * 6) % 4})
115 if (me && t > 1) say("Six crates, one road, zero tests passing yet.", me.top.x, me.row)
116}`
117const TAIL = `
118Reply with one scene as JSON matching the schema:
119- concept: one short line naming the scene's world, metaphor and shot, e.g. "wild west: Clawd as sheriff nailing a wanted poster for auth.ts to a saloon wall". Decide it first.
120- code: the scene's program, or "".
121- actors: up to 20, each with kind ("clawd" or "sprite"), frames (for a sprite: 1 to 12 frames, each a string with lines separated by \\n, at most 9 lines and 80 characters wide, every frame the same size; for Clawd: []), fps (0 for a still), frame (an expression picking the frame index, wrapped to the frame count, or "" to cycle by fps), show (an expression: the actor is drawn only while it is above 0, or "" for always), x and y (expressions for the top-left cell; columns 0 to w-1, rows 0 to h-1, and anything off the strip is simply clipped), color (hex; Clawd's is "#d97757"), say ("" for nothing, else at most 60 characters), and sayAt (seconds into the scene when it starts talking).
122- particles: up to 6 swarms (rain, sparks, dust, bytes, bugs, confetti, bubbles, zzz, snow, stars, smoke, leaves, fish, birds, anything) with glyphs (characters to cycle, up to 16), count (up to 120), x and y expressions where k is the particle's index from 0 to n-1, and a color. Particles are drawn behind actors.
123- background: a faint ambient effect behind everything: effect (plasma, fire, starfield, rain, tunnel, lava, glitch, aurora, waves, pulse, fireworks), palette (3 to 5 hex colors, all visible on near-black), speed (0.3 to 2.5), intensity (0 to 1, keep it low when the set is busy).
124
125Single-width characters only: ASCII, Latin-1, box drawing, blocks, braille, arrows (←↑→↓↖↗↘↙⇐⇒), geometric shapes (■□▪▲△▼▽◆◇○●◐◑◢◣◤◥), and ★☆♥♦♣♠♪♫☺☻✓✗✦✧. No emoji.
126
127Expressions are small math formulas. Variables: t (seconds since this scene started), w and h (the strip's width and height), k and n (a particle's index and count, or an actor's index and the actor count). Operators: + - * / % ^ and parentheses. Functions: sin cos abs min max floor ceil round sqrt pow exp sign mod(a,b) fract(x) clamp(x,lo,hi) lerp(a,b,k) tan tri(x) (0 to 1 and back over each unit) rand(x) (a fixed random number in [0,1) per x) noise(x) (smooth wandering value in [0,1)) step(edge,x) between(x,lo,hi) (1 while lo <= x < hi) smoothstep(a,b,x) (eases 0 to 1 as x goes from a to b) atan2(y,x) hypot(a,b), and pi. Anything else evaluates to 0.
128
129Motions that work:
130- walk across and wrap: x = "mod(t*5, w+20) - 20"
131- pace back and forth: x = "10 + tri(t*0.05)*(w-30)"
132- walk to a spot and stop: x = "min(40, 5 + t*5)"
133- walk in from the right and stop: x = "max(w*0.6, w - t*5)"
134- follow another actor with a gap: reuse its x expression minus the gap
135- drop in and land: y = "min(4, -6 + t*10)"
136- falling particles: x = "rand(k)*w", y = "mod(t*(3+rand(k+9)*4) + rand(k+3)*h, h)"
137- orbit: x = "40 + cos(t*0.8 + k*2*pi/n)*12", y = "3 + sin(t*0.8 + k*2*pi/n)*2"
138- burst of sparks: x = "30 + cos(k*2.4)*fract(t)*14", y = "3 + sin(k*2.4)*fract(t)*3"
139- ease from one spot to another between t=1 and t=3: x = "lerp(5, 60, smoothstep(1, 3, t))"
140- jump: y = "4 - 3*sin(pi*clamp(t-2, 0, 1))"
141- appear at t=2.5 and leave at t=6: show = "between(t, 2.5, 6)"
142- explosion frames once at t=3, then hold the last: frame = "clamp(floor((t-3)*8), 0, 4)", show = "step(3, t)"
143- blink on and off: show = "step(0.5, fract(t*2))"
144- parallax pan: far hills x = "-mod(t*2, 60)", near trees x = "-mod(t*6, 60)" (draw them wider than the strip so the wrap is unseen)
145- wander like a fish or a fly: x = "20 + noise(t*0.5 + k)*40", y = "1 + noise(t*0.7 + k*3)*6"
146
147Keep it charming, take creative risks, and make every scene look different from the last.`
148
149export function systemFor(styles: readonly string[]) {
150 const allowed = [...new Set(styles)]
151 const isOne = allowed.length === 1
152 const has = (style: string) => allowed.includes(style)
153 // With one style there is nothing to deal: every scene is drawn in it.
154 const style = isOne
155 ? `Every scene is drawn as ${allowed[0]}: ${STYLE_LINES[allowed[0] ?? '']?.replace(/^"[^"]+" means /, '')}.`
156 : `- "[style: ...]" is how this scene is drawn, and it is not optional: ${allowed.map(s => STYLE_LINES[s]).join('; ')}. Each has its own look, and the developer wants to see all of them over a session.`
157
158 return [
159 HEAD,
160 style,
161 DRAWING,
162 ...(has('pixel art') || has('3D') ? [PIXELS] : []),
163 MID,
164 ...(has('3D') ? [WORLD3D] : []),
165 CODING,
166 ...(has('3D') ? [EXAMPLE3D, ''] : []),
167 TAIL,
168 ].join('\n')
169}
170
171// Settings dealt one per new scene, so a run of similar work (a dozen reads in
172// a bug hunt) still plays out across very different worlds.
173export const WORLDS = [
174 'deep sea', 'outer space', 'wild west', 'medieval castle', 'jungle safari', 'cooking show',
175 'bank heist', 'sports broadcast', 'film noir city at night', 'fairy tale forest', 'weather report',
176 'nature documentary', 'cyberpunk street', 'circus', 'haunted house', 'pirate ship', 'farm',
177 'construction site', 'orchestra concert', 'retro arcade game', 'ski slope', 'mad science lab',
178 'train station', 'volcano island', 'arctic expedition', 'desert caravan', 'theater stage',
179 'vegetable garden', 'airport runway', 'dinosaur era', 'ant colony up close', 'rush hour traffic',
180 'mountain climb', 'submarine', 'carnival midway', 'beehive', 'lighthouse in a storm', 'ancient tomb',
181 'skate park', 'fishing pond', 'mail room', 'rocket launch pad', 'museum after hours', 'bakery',
182 'bowling alley', 'snow globe', 'underground mine', 'race track pit stop', 'ufo abduction',
183 'zen rock garden',
184]
185
186// How many past concepts each new request lists.
187const RECENT = 6
188
189// The drawing styles dealt with the world, per the "styles" setting; the
190// prompt holds only these styles' drawing calls, and a style is named in
191// each request only when there is more than one to choose from.
192export const STYLE_SETS = {
193 mix: ['3D', '3D', 'pixel art', 'text art'],
194 '3D': ['3D'],
195 'pixel art': ['pixel art'],
196 'text art': ['text art'],
197 'no 3D': ['pixel art', 'text art'],
198} as const
199export type StyleSet = keyof typeof STYLE_SETS
200export const isStyleSet = (v: unknown): v is StyleSet => typeof v === 'string' && v in STYLE_SETS
201
202// The thread is cut back to KEEP messages once it reaches MOST, so a long
203// session never outgrows the context window; cutting in one go, not a little
204// each time, keeps the cached prefix good between cuts.
205const MOST = 60
206const KEEP = 30
207
208// Older scenes are kept only as their concept line: the last FULL replies
209// stay whole (a "still" beat is the next beat of the latest, and a callback
210// may reach one back), and once BATCH more have piled up behind them they are
211// collapsed together, so the cached prefix is rebuilt once per batch rather
212// than on every request. Scenes are not repeated thanks to the concept lines,
213// not the whole replies, so few need keeping.
214const FULL = 2
215const BATCH = 6
216
217// A line of the log that only says something is still going.
218const isQuiet = (line: string) => /^\+\d+s still /.test(line)
219
220export const SCHEMA = {
221 type: 'object',
222 properties: {
223 concept: { type: 'string' },
224 code: { type: 'string' },
225 actors: {
226 type: 'array',
227 items: {
228 type: 'object',
229 properties: {
230 kind: { type: 'string', enum: ['clawd', 'sprite'] },
231 frames: { type: 'array', items: { type: 'string' } },
232 fps: { type: 'number' },
233 frame: { type: 'string' },
234 show: { type: 'string' },
235 x: { type: 'string' },
236 y: { type: 'string' },
237 color: { type: 'string' },
238 say: { type: 'string' },
239 sayAt: { type: 'number' },
240 },
241 required: ['kind', 'frames', 'fps', 'frame', 'show', 'x', 'y', 'color', 'say', 'sayAt'],
242 additionalProperties: false,
243 },
244 },
245 particles: {
246 type: 'array',
247 items: {
248 type: 'object',
249 properties: {
250 glyphs: { type: 'string' },
251 count: { type: 'number' },
252 x: { type: 'string' },
253 y: { type: 'string' },
254 color: { type: 'string' },
255 },
256 required: ['glyphs', 'count', 'x', 'y', 'color'],
257 additionalProperties: false,
258 },
259 },
260 background: {
261 type: 'object',
262 properties: {
263 effect: { type: 'string', enum: [...EFFECTS] },
264 palette: { type: 'array', items: { type: 'string' } },
265 speed: { type: 'number' },
266 intensity: { type: 'number' },
267 },
268 required: ['effect', 'palette', 'speed', 'intensity'],
269 additionalProperties: false,
270 },
271 },
272 required: ['concept', 'code', 'actors', 'particles', 'background'],
273 additionalProperties: false,
274}
275
276type Block = Record<string, unknown>
277type Message = { role: 'user' | 'assistant'; content: string | Block[] }
278
279// A scene, or why there is none, and what the call spent when one was made.
280export type Narration = { script?: Script; error?: string; spent?: CallUsage }
281
282export const URL = 'https://api.anthropic.com/v1/messages'
283
284// The thread itself: the hooks send what it builds and hand back what came.
285export function createThread(model: Model, options: { isThinking?: boolean; styles?: StyleSet } = {}) {
286 const styles: readonly string[] = STYLE_SETS[options.styles ?? 'mix']
287 const system = systemFor(styles)
288 const messages: Message[] = []
289 // Off once the API refuses the server-side fallback option.
290 let canFallBack = true
291 // The concepts of the scenes so far, and the worlds not yet dealt.
292 const concepts: string[] = []
293 // The developer's latest request.
294 let task = ''
295 let deck: string[] = []
296 const deal = (random: () => number) => {
297 if (deck.length === 0) deck = [...WORLDS].sort(() => random() - 0.5)
298
299 return deck.pop() as string
300 }
301
302 // Opens the next exchange with what happened since the last scene; new work
303 // also gets the recent concepts to steer clear of and a fresh world.
304 const ask = (activityIn: string, random: () => number = Math.random) => {
305 let activity = activityIn
306 const lines = activity.split('\n').filter(line => !line.startsWith('[strip '))
307 task = lines.find(line => line.startsWith('[task] ')) ?? task
308 // A scene whose code broke has no next beat: a quiet beat after it asks
309 // for a new scene.
310 const isBroken = lines.some(line => line.startsWith("[your last scene's code"))
311 if (messages.length >= MOST) {
312 const dropped = messages.slice(0, messages.length - KEEP).filter(m => m.role === 'assistant').length
313 messages.splice(0, messages.length - KEEP)
314 collapsed = Math.max(0, collapsed - dropped)
315 stripThinking()
316 }
317 // The task the developer gave may have been cut, or the thread begun
318 // again: the first message says it again.
319 const first = messages[0]
320 if (first && typeof first.content === 'string' && task && !first.content.includes(task)) first.content = `${task}\n${first.content}`
321 else if (!first && task && !lines.includes(task)) activity = `${task}\n${activity}`
322 const recent = concepts.slice(-RECENT)
323 const steer = lines.length > 0 && !isBroken && lines.every(isQuiet)
324 ? [`[playing: ${recent[recent.length - 1] ?? 'the first scene'}; it has run its course: answer its next beat]`]
325 : [...(recent.length > 0 ? [`[recent scenes: ${recent.join(' / ')}]`] : []), `[world: ${deal(random)}]`, ...(new Set(styles).size > 1 ? [`[style: ${styles[Math.floor(random() * styles.length)]}]`] : [])]
326 messages.push({ role: 'user', content: [activity, ...steer].join('\n') })
327 }
328
329 // The request for the open exchange, for a credential of `kind`.
330 const request = (kind: 'bearer' | 'api-key') => {
331 const betas = [kind === 'bearer' ? 'oauth-2025-04-20' : '', canFallBack ? 'server-side-fallback-2026-07-01' : '']
332 const body = {
333 model,
334 // Sonnet can skip thinking, which is billed as output; Opus cannot, and
335 // Haiku does not think unless asked.
336 ...(model === 'claude-sonnet-5-5' && !options.isThinking ? { thinking: { type: 'between_tools' } } : {}),
337 max_tokens: 16000,
338 system: [
339 ...(kind === 'bearer' ? [{ type: 'text', text: IDENTITY }] : []),
340 { type: 'text', text: system, cache_control: { type: 'ephemeral' } },
341 ],
342 cache_control: { type: 'ephemeral' },
343 // Haiku 4.5 takes no effort setting.
344 output_config: {
345 ...(model === 'claude-haiku-4-5' ? {} : { effort: 'low' }),
346 format: { type: 'json_schema', schema: SCHEMA },
347 },
348 messages,
349 ...(canFallBack ? { fallbacks: 'default' } : {}),
350 }
351
352 return {
353 headers: {
354 'content-type': 'application/json',
355 'anthropic-version': '2023-06-01',
356 'anthropic-beta': betas.filter(Boolean).join(','),
357 },
358 body: JSON.stringify(body),
359 }
360 }
361
362 // True when the API refused the fallback option: send the request again.
363 const isFallbackRefused = (status: number, text: string) => {
364 if (status !== 400 || !canFallBack || !text.includes('fallback')) return false
365 canFallBack = false
366
367 return true
368 }
369
370 // How many replies from the start have been collapsed to their concept.
371 let collapsed = 0
372
373 // A thinking block is bound to the history it was made in: once that is
374 // edited (a cut, a collapse), replaying one is refused, so every edit
375 // drops them all. The model reads nothing from past thinking anyway.
376 const isThought = (block: Block) => block.type === 'thinking' || block.type === 'redacted_thinking'
377 const stripThinking = () => {
378 for (const m of messages) {
379 if (m.role === 'assistant' && Array.isArray(m.content) && m.content.some(isThought)) m.content = m.content.filter(block => !isThought(block))
380 }
381 }
382
383 // Collapses the oldest whole replies to their concept lines, a batch at a
384 // time.
385 const collapse = () => {
386 const replies = messages.filter(m => m.role === 'assistant')
387 const whole = replies.length - collapsed
388 if (whole < FULL + BATCH) return
389 const batch = replies.slice(collapsed, replies.length - FULL)
390 for (const m of batch) {
391 const concept = conceptOf(m)
392 m.content = [{ type: 'text', text: JSON.stringify({ concept, note: 'an earlier scene, kept only as its concept' }) }]
393 }
394 collapsed += batch.length
395 stripThinking()
396 }
397
398 // Begins the conversation again, after a request the API would not take
399 // however it is retried: the next exchange says the task again.
400 const reset = () => {
401 messages.length = 0
402 collapsed = 0
403 }
404
405 // A reply's concept line, from its JSON.
406 const conceptOf = (m: Message) => {
407 if (typeof m.content === 'string') return ''
408 const text = m.content.find(block => block.type === 'text')?.text
409 try {
410 const raw = JSON.parse(typeof text === 'string' ? text : '') as { concept?: unknown }
411
412 return typeof raw.concept === 'string' ? raw.concept.slice(0, 140) : ''
413 } catch {
414 return ''
415 }
416 }
417
418 // Closes the exchange unanswered: its activity goes with the next one.
419 const abandon = () => {
420 const open = messages.pop()
421
422 const content = typeof open?.content === 'string' ? open.content : ''
423
424 return content.split('\n').filter(line => !/^\[(recent scenes|world|style|playing): /.test(line)).join('\n')
425 }
426
427 // Closes the exchange with the reply, kept whole, and reads its scene.
428 const accept = (text: string): Narration => {
429 let reply: { content: Block[]; stop_reason: string; usage?: Record<string, number> }
430 try {
431 reply = JSON.parse(text) as typeof reply
432 if (!Array.isArray(reply.content)) throw new Error('no content')
433 } catch {
434 abandon()
435
436 return { error: 'the API answered something unreadable' }
437 }
438 messages.push({ role: 'assistant', content: reply.content })
439 collapse()
440 const spent: CallUsage = {
441 input: reply.usage?.input_tokens ?? 0,
442 output: reply.usage?.output_tokens ?? 0,
443 cacheRead: reply.usage?.cache_read_input_tokens ?? 0,
444 cacheWrite: reply.usage?.cache_creation_input_tokens ?? 0,
445 }
446 if (reply.stop_reason === 'refusal') return { error: 'the buddy declined to draw that one', spent }
447 const answer = reply.content.find(block => block.type === 'text')?.text
448 try {
449 const raw = JSON.parse(typeof answer === 'string' ? answer : '') as { concept?: unknown }
450 if (typeof raw?.concept === 'string' && raw.concept.trim()) concepts.push(raw.concept.trim().slice(0, 140))
451 const script = cleanScript(raw)
452
453 return script ? { script, spent } : { error: 'the buddy answered a scene it cannot draw', spent }
454 } catch {
455 return { error: 'the buddy answered something other than a scene', spent }
456 }
457 }
458
459 return { ask, request, isFallbackRefused, abandon, accept, reset, isThinking: Boolean(options.isThinking), styles: options.styles ?? 'mix' }
460}
461hooks/scenes.ts 148 lines1// The stock of ready-made scenes, written by scripts/library.ts. Do not edit
2// by hand: run the script to add scenes.
3
4import type { Phase } from './library'
5
6export type Stock = { phase: Phase; world: string; style: string; concept: string; source: string; scene: unknown }
7
8export const SCENES: Stock[] = [
9 {"phase":"testing","world":"pirate ship","style":"pixel art","concept":"rush hour traffic: Clawd as a traffic cop at a toll gate waving test-cars through, green ones pass, red ones get a ticket","source":"claude-opus-5-5","scene":{"concept":"rush hour traffic: Clawd as a traffic cop at a toll gate waving test-cars through, green ones pass, red ones get a ticket","code":"let cars=[],P=0,F=0,nx=0;const G=66;\nconst art=\"..######..\\n.#ww#ww#..\\n##########\\n.oo....oo.\";\nfunction frame(t,dt){for(let x=0;x<w;x++){const b=3+Math.floor(noise(x*0.3)*6);for(let p=b;p<10;p++)pixel(x,p,(x+p)%7==0&&p>b+1?'#ffd36b':'#2a3550');}\nfill(0,5,w,1,'▀','#777','#333');fill(0,6,w,3,' ',null,'#333');for(let x=0;x<w;x+=6)put(x-Math.floor(t*0)%6,8,'═','#ccc','#333');\nif(t>=nx){cars.push({x:-12,f:rand(cars.length*7.3)<0.25,s:0,c:hsl(rand(cars.length)*360,0.7,0.6)});nx=t+1.6+rand(t)*1.5;}\nlet near=null;for(let i=0;i<cars.length;i++){const c=cars[i];let lim=i?cars[i-1].x-13:999;if(c.s<1&&c.x+10>=G-1&&c.x<G){c.s+=dt;if(c.s>=1){c.f?F++:P++;}near=c;}else{c.x=Math.min(c.x+14*dt,lim);}\nif(c.x+10>=G-3&&c.x<G)near=c;pixels(Math.floor(c.x),12,art,{'#':c.c,w:'#bfe8ff',o:'#111'});if(c.s>=1&&c.f&&c.x<G+40)text(Math.floor(c.x)+3,5,'✗',\"#ff5555\");if(c.s>=1&&!c.f&&c.x<G+30)text(Math.floor(c.x)+3,5,'✓','#5f5');}\ncars=cars.filter(c=>c.x<w+5);const open=near&&near.s>=1;\nfill(G+1,2,6,4,' ',null,'#556');text(G+1,2,'TOLL',\"#fff\",'#556');put(G+3,3,'●',near&&near.s<1?'#fc3':open?(near.f?'#f44':'#4f4'):'#444','#556');\nif(open)line(G,1,G,5,'│','#fff');else text(G-8,5,'▬▬▬▬▬▬▬▬','#f55');\nconst cc=clawd(G+10,1,{facing:-1,arms:open?{left:'up',right:'down'}:'out',blink:fract(t*0.3)<0.05});pixels(cc.top.x-3,cc.top.py-2,'..###..\\n#######',{'#':'#3366ff'});\ntext(G+27,1,'{what}','#ffd36b');text(G+27,2,'✓ '+P,'#5f5');text(G+27,3,'✗ '+F,'#f66');\nconst lines=['Rush hour on {what}. Keep it moving!','Green light, you pass. Next!','That one failed. Pull over, buddy.','Bumper to bumper assertions.'];const k=Math.floor(t/8)%4;if(fract(t/8)<0.55)say(lines[k],cc.top.x,cc.row);}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#1a2340","#2a3550","#3a4560"],"speed":0.5,"intensity":0}}},
10 {"phase":"searching","world":"bank heist","style":"3D","concept":"deep sea: Clawd in a lit submarine sweeping sonar across a sunken vault field, hunting the one chest labeled {what}","source":"claude-opus-5-5","scene":{"concept":"deep sea: Clawd in a lit submarine sweeping sonar across a sunken vault field, hunting the one chest labeled {what}","code":"const rock=box(1.6,0.8,1.4),chest=box(1,0.7,0.7),sub=sphere(0.7,10),fin=cone(0.3,0.6,6);const ch=Array.from({length:9},(_,i)=>({x:-14+i*3.5,z:-3-rand(i)*6}));const hit=6;function frame(t,dt){const L=20;const ph=mod(t,L);camera(0,3,9,0,0.8,-4,90);fog(6,30,'#012');light(0.3,1,0.4,0.35);mesh3d(plane(60,40),{y:0,z:-8,color:'#3a6a7a'});for(let i=0;i<9;i++){const c=ch[i];mesh3d(rock,{x:c.x+1.2,y:0.4,z:c.z-1.5,ry:i,color:'#6f8890'});const f=i==hit&&ph>12;mesh3d(chest,{x:c.x,y:0.35,z:c.z,color:f?'#ffd060':'#a07040'});}const sx=lerp(-14,ch[hit].x,smoothstep(0,12,ph))+Math.sin(t*0.7)*0.3;const sy=2.6+Math.sin(t*1.3)*0.15;mesh3d(sub,{x:sx,y:sy,z:-2,scale:1,color:'#e8c050'});mesh3d(fin,{x:sx-0.9,y:sy-0.3,z:-2,rz:1.57,color:'#d0a040'});const p=project(sx,sy,-2);if(p){const r=fract(t*0.6)*30;for(let a=0;a<40;a++){const an=a/40*6.28;const x=p.x+Math.cos(an)*r,y=p.y+Math.sin(an)*r*0.25+3;put(x,y,'·','#5ff0d0');}clawd(p.x-4,p.y-1.5,{scale:1,facing:1,eyes:ph>12?'wide':'open',color:'#d97757'});if(ph>12){const q=project(ch[hit].x,1,ch[hit].z);if(q){text(q.x-4,q.y-1,'{what}','#ffe9a0');}say('Found {what}! Down here the whole time.',p.x+3,p.row||p.y-2);}else if(ph>3&&ph<9)say('Pinging every crate for {what}...',p.x+3,p.y-2);}for(let i=0;i<14;i++){const by=mod(-t*1.5-rand(i)*9,10);put(rand(i+7)*w,by,'o','#8fd8ff');}}","actors":[],"particles":[],"background":{"effect":"waves","palette":["#023","#035","#047"],"speed":0.5,"intensity":0}}},
11 {"phase":"reading","world":"deep sea","style":"3D","concept":"theater stage in the deep sea: Clawd as a diver-actor reads {what} aloud under a spotlight on a sunken stage while fish audience drifts and ","source":"claude-opus-5-5","scene":{"concept":"theater stage in the deep sea: Clawd as a diver-actor reads {what} aloud under a spotlight on a sunken stage while fish audience drifts and the curtain rises","code":"const flr=box(16,0.3,5),cur=box(8,3.2,0.2),col=cylinder(0.25,3,8),fish=sphere(0.25,6),scr=box(2.4,1.4,0.1);\nconst F=Array.from({length:7},(_,i)=>({x:-9+i*3,z:4+rand(i)*2,p:rand(i+5)*6}));\nconst B=Array.from({length:30},(_,i)=>({x:rand(i)*24-12,y:rand(i+40)*4,z:rand(i+80)*-6}));\nfunction frame(t,dt){\nconst a=t*0.08;camera(Math.sin(a)*3,2.4,9,0,1,0,80);light(0.3,1,0.6,0.35);fog(8,30,'#021826');\nmesh3d(flr,{y:-0.15,color:'#b98a5a'});\nconst o=smoothstep(0,4,t)*5;\nmesh3d(cur,{x:-4-o,y:0.2,z:-1.5,color:'#c0405a'});mesh3d(cur,{x:4+o,y:0.2,z:-1.5,color:'#c0405a'});\nmesh3d(col,{x:-7.5,z:-2,color:'#9ab'});mesh3d(col,{x:7.5,z:-2,color:'#9ab'});\nmesh3d(scr,{x:0,y:2,z:-2.2,rx:0.05,color:'#e8e0c0'});\nconst sp=project(0,2.3,-2.1);if(sp)text(sp.x-Math.floor('{what}'.length/2),sp.y,'{what}','#203040');\nconst ph=Math.floor(t/8)%4;const cx=Math.sin(t*0.4)*2.5;\nconst c=clawd3d(cx,0,0.5,{facing:Math.cos(t*0.4)>0?1:-1,stride:Math.floor(t*4)%4,arms:ph==1?'up':ph==2?{left:'down',right:'out'}:'out'});\nfor(const f of F){const y=0.6+Math.sin(t+f.p)*0.2;mesh3d(fish,{x:f.x+Math.sin(t*0.3+f.p)*0.5,y,z:f.z,color:hsl(30+f.p*40,0.7,0.6)});}\nfor(const b of B){b.y+=dt*0.5;if(b.y>4)b.y=0;point3d(b.x+Math.sin(t+b.y)*0.2,b.y,b.z,'#8fd8ff');}\nif(c&&t>4){const L=['Act I: {what}, line by line.','Plot twist: it imports half the ocean.','Aha! So that is who calls this function.','Encore! Reading {what} once more.'];say(L[ph],c.top.x,c.row);}\n}","actors":[],"particles":[],"background":{"effect":"waves","palette":["#03304a","#064a6a","#0a6a8a"],"speed":0.4,"intensity":0}}},
12 {"phase":"thinking","world":"sports broadcast","style":"text art","concept":"race track pit stop: Clawd as crew chief in a headset, mulling strategy while the {what} car gets tires swapped and rivals scream past","source":"claude-opus-5-5","scene":{"concept":"race track pit stop: Clawd as crew chief in a headset, mulling strategy while the {what} car gets tires swapped and rivals scream past","code":"const L=['Box box box! New tires on {what}, then we decide.','Undercut or overcut? Thinking it through...','Fresh rubber. Plan for {what} almost locked in.','Go go go! I have a strategy now. Mostly.'];\nfunction car(x,y,c,lab){text(x,y,' ▄▄███▄▄▄▄▄ ',c);text(x,y+1,'▐█▀▀▀▀▀▀▀▀▀▀▀██▶',c);if(lab)text(x+3,y+1,lab.slice(0,10),'#fff',c)}\nfunction frame(t,dt){for(let x=0;x<w;x++){put(x,0,'·▪•'[(x*7+Math.floor(t*3))%3],hsl((x*37)%360,.6,.6));put(x,3,x%4<2?'▀':'▀',x%8<4?'#e33':'#eee');put(x,8,'═','#777')}\nfor(let i=0;i<3;i++){const cx=mod(t*60+i*55,w+60)-30;text(cx,1,'▄▄██▄▄▶',['#4af','#fd3','#5e5'][i]);text(cx+1,2,'○ ○','#aaa')}\nconst p=mod(t,12),cy=Math.floor(t/12);let x=p<2?lerp(-30,40,smoothstep(0,2,p)):p<8?40:lerp(40,w+40,smoothstep(8,10,p));\ntext(38,4,'┌─PIT─┐','#888');car(x,5,'#d97757','{what}');\nconst st=p>=2&&p<8;const wo=st&&p>3&&p<6;text(x+1,7,wo?' ':'(◎)','#ccc');text(x+12,7,wo?' ':'(◎)','#ccc');\nif(st){const k=Math.floor(t*6)%2;text(x-4,6,k?'╪':'┼','#fd3');text(x+18,6,k?'╪':'┼','#fd3');if(wo){text(x-9,7,'(◎)','#666');text(x+20,7,'(◎)','#666')}}\nconst cl=clawd(78,4.5,{facing:-1,arms:st?{left:'out',right:'down'}:'up',blink:fract(t*.3)<.05});\ntext(cl.top.x-3,cl.top.row-1>=0?cl.top.row:4,'','#fff');put(cl.left.x-1,cl.left.row,'▮','#ccc');\nsprite(96,4,'┌──────────┐\\n│ LAP '+String(12+cy).padStart(2,'0')+' │\\n│ P'+(3-Math.min(2,cy))+' +'+(p<8?'0.4':'0.1')+'s │\\n└────┬─────┘','#5e5');text(101,8,'┴','#5e5');\nif(p>1.5&&p<11)say(L[cy%4],cl.top.x,cl.row)}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#222","#333","#2a2a2a"],"speed":0.5,"intensity":0}}},
13 {"phase":"editing","world":"bowling alley","style":"3D","concept":"weather report: Clawd the TV meteorologist points at a 3D forecast map as edits blow the storm away from {what}","source":"claude-opus-5-5","scene":{"concept":"weather report: Clawd the TV meteorologist points at a 3D forecast map as edits blow the storm away from {what}","code":"const gr=plane(34,12),cl=sphere(1,8),sn=sphere(1,10),bx=box(1,1,1);const cs=[[-6,1.4],[-3,0.8],[0,1.8],[3,1],[6,1.3]];\nfunction frame(t,dt){camera(2,4,9,2,0,-2,80);light(0.3,1,0.6,0.35);fog(8,40,'#000');const k=fract(t/24),s=1-smoothstep(0.35,0.75,k);mesh3d(gr,{z:-2,color:'#5a9a6a'});cs.forEach((c,i)=>mesh3d(bx,{x:c[0]+2,y:c[1]/2,z:-2,scale:1,color:i==2?'#e8c070':'#9ab0c8',ry:0.3}));const cx=2+Math.sin(t*0.6)*1.2+(1-s)*9;if(s>0.05){for(let j=0;j<4;j++)mesh3d(cl,{x:cx+j*0.9-1.3,y:3+Math.sin(j*2)*0.2,z:-2,scale:s*(0.8+j%2*0.3),color:'#b8bcc8'});for(let i=0;i<12;i++){const rx=cx-1.6+i*0.3,ry=mod(3-t*4-i*0.7,3);line3d(rx,ry,-2,rx,ry-0.4,-2,'#7fb8ff')}}mesh3d(sn,{x:9,y:lerp(-1.5,3.5,1-s),z:-5,scale:1.1,color:'#ffd84a',unlit:true});const p=project(2,2.3,-2);if(p)text(p.x-5,p.y,'{what}','#fff');text(w-22,0,s>0.5?'STORM WARNING':'CLEAR SKIES',s>0.5?'#ff6b6b':'#ffd84a');const temp=Math.round(lerp(12,27,1-s));text(w-22,1,temp+'° and editing','#cde');const c=clawd(3,4.5,{facing:1,arms:{left:'down',right:'out'},blink:fract(t*0.3)<0.04});line(c.right.x,c.right.row,c.right.x+8,c.right.row-2,'-','#ddd');pixels(c.top.x-3,c.top.py-2,'.######.\\n########','#'.length?{'#':'#335'}:{});if(k<0.3)say('Heavy bugs over {what}. Patching now.',c.top.x,c.row);else if(k>0.8)say('Forecast for {what}: sunny, zero errors.',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#203050","#304870","#405890"],"speed":0.4,"intensity":0}}},
14 {"phase":"running","world":"vegetable garden","style":"3D","concept":"cyberpunk rooftop garden: Clawd waters neon vegetables in a 3D street of glowing towers while {what} runs, a carrot rising slowly like a pro","source":"claude-opus-5-5","scene":{"concept":"cyberpunk rooftop garden: Clawd waters neon vegetables in a 3D street of glowing towers while {what} runs, a carrot rising slowly like a progress bar","code":"const bx=box(1,1,1),cy=cylinder(0.25,1,8),sp=sphere(0.5,8),cn=cone(0.3,1,8);\nconst tw=Array.from({length:14},(_,i)=>({x:(i%2?1:-1)*(7+rand(i)*4),z:-4-i*4,h:3+rand(i+9)*6,c:i%3?'#7a6cff':'#40d0e0'}));\nfunction frame(t,dt){camera(Math.sin(t*0.1)*2,2.4,8,0,1,-6,90);light(0.4,1,0.6,0.35);fog(8,45,'#05010a');\nfor(const b of tw)mesh3d(bx,{x:b.x,y:b.h/2,z:b.z,scale:1,color:b.c,rx:0,ry:0,rz:0});\nfor(const b of tw){const p=project(b.x,b.h,b.z);if(p&&b.z>-20)text(p.x-1,p.y,'▓▓',b.c);}\nfor(let i=0;i<6;i++){mesh3d(box(1.6,0.4,1),{x:-5+i*2,y:0.2,z:0,color:'#8a5a3a'});}\nconst g=Math.min(1,t/60);\nfor(let i=0;i<6;i++){const s=0.3+Math.min(1,g*1.4+rand(i)*0.3)*0.9;const x=-5+i*2;\nif(i%2)mesh3d(sp,{x,y:0.4+s*0.4,z:0,scale:s,color:i%4==1?'#ff4fa0':'#c060ff'});else mesh3d(cn,{x,y:0.4,z:0,scale:s,color:'#ff9a30'});\nmesh3d(cn,{x,y:0.4+s,z:0,scale:s*0.5,color:'#50ff90'});}\nconst cx=-6+((t*0.8)%13);const c=clawd3d(cx,0,1.5,{facing:1,stride:Math.floor(t*6)%4,arms:{left:'down',right:'out'}});\nif(c){for(let k=0;k<4;k++){const f=fract(t*2+k/4);put(c.right.x+1+k,c.right.row+Math.floor(f*3),'.','#60e0ff');}\nconst lab=project(0,1.8,0);if(lab)text(lab.x-4,lab.y,'{what}','#ff4fa0');\nconst m=Math.floor(t/12)%4;const L=['Watering {what}. Code grows slower than kale.','Neon radishes ripening. {what} still chugging.','Pro tip: compiles taste best organic.','Almost harvest. Carrots ahead of {what}.'];\nif(fract(t/12)<0.7)say(L[m],c.top.x,c.row);}\nfor(let i=0;i<20;i++){const x=(rand(i)*w+t*(4+rand(i+5)*6))%w;put(x,Math.floor(rand(i+3)*3),'-','#ff4fa0');}}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#3040a0","#6050c0","#40a0d0"],"speed":1,"intensity":0.3}}},
15 {"phase":"building","world":"train station","style":"pixel art","concept":"snow globe: a tiny train station inside a snow globe where a freight train hauls {what} round and round; Clawd shakes the globe whenever the","source":"claude-opus-5-5","scene":{"concept":"snow globe: a tiny train station inside a snow globe where a freight train hauls {what} round and round; Clawd shakes the globe whenever the build drags","code":"const cx=Math.floor(w/2),rx=34,ry=8;let sn=Array.from({length:70},(_,i)=>({x:cx-30+rand(i)*60,y:rand(i+9)*12,v:3}));const ins=(x,y)=>{const a=(x-cx)/rx,b=(y-8)/ry;return a*a+b*b<0.92};const lines=['Building {what}... one lap at a time.','Shake it! Compilers love a blizzard.','Still loading cargo. The snow is not helping.','Next stop: a finished {what}. Probably.'];function frame(t,dt){const T=mod(t,12),sh=T<1.2,ox=sh?Math.round(Math.sin(t*35)*1.5):0;for(let x=cx-28;x<cx+28;x++){pixel(x+ox,13,'#eef');pixel(x+ox,14,'#cde');}for(let x=cx-27;x<cx+27;x+=2)pixel(x+ox,12,'#888');pixels(cx+8+ox,4,' rrrrrrrr \nrrrrrrrrrr\n wwwwwwww \n wdwwwwdw \n wdwwbbdw \n wwwwbbww \n wwwwbbww ',{r:'#c33',w:'#ec9',d:'#69c',b:'#753'});text(cx+9+ox,2,'{what}'.slice(0,8),'#fe8');const tx=cx-30+mod(t*7,60);const tr=' kk \nyyyy gggg gg\nyyyy gggg gg\n o o o o o';for(let i=0;i<tr.length;i++){}pixels(Math.round(tx)+ox,7,' s \n s \nyyyyy gggg gggg\nyyyyy gggg gggg\n o o o o o o',{y:'#fc3',g:'#4b7',o:'#444',s:'#bbb'});for(const s of sn){if(sh)s.v=-8-rand(s.x)*14;s.v=lerp(s.v,3,dt*1.2);s.y+=s.v*dt;s.x+=Math.sin(t+s.y)*dt*2;if(s.y>12){s.y=1;s.x=cx-30+Math.random()*60}if(s.y<1)s.y=1;if(ins(s.x,s.y))pixel(Math.round(s.x)+ox,Math.round(s.y),'#fff')}for(let a=0;a<6.28;a+=0.02){const y=8+Math.sin(a)*ry;if(y<15.5)pixel(Math.round(cx+ox+Math.cos(a)*rx),Math.round(y),'#8bf')}fill(cx-26+ox,8,52,1,'▄','#a63','#000');text(cx-4+ox,8,'{what}'.slice(0,8),'#fd9','#a63');const c=clawd(cx-rx-17,4.5,{facing:1,arms:sh?'up':{left:'down',right:'out'},blink:fract(t*0.3)<0.04,eyes:sh?'wide':'open'});pixels(c.top.x-3,c.top.py-3,' rrrr \nwwwwww',{r:'#c33',w:'#fff'});if(T>2&&T<9)say(lines[Math.floor(t/12)%4],c.top.x,c.row);}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#335","#557","#88a"],"speed":0.4,"intensity":0.2}}},
16 {"phase":"git","world":"desert caravan","style":"3D","concept":"museum after hours: Clawd as night guard patrolling a 3D gallery of commit exhibits on pedestals, the newest exhibit {what} rising into plac","source":"claude-opus-5-5","scene":{"concept":"museum after hours: Clawd as night guard patrolling a 3D gallery of commit exhibits on pedestals, the newest exhibit {what} rising into place","code":"const ped=box(1.2,1.2,1.2),gem=sphere(0.45,10),fl=plane(80,14),col=cylinder(0.25,3.5,8);\nconst hs=['a1f3c9','7be204','c0ffee','3d9a1b','e45f70','91bd2c','f00d42'];\nfunction frame(t,dt){const N=hs.length,end=N*4;const cx=Math.min(t*0.45,end-1);\ncamera(cx,2.4,7,cx+1,1,-2,80);fog(6,30,'#000');light(0.3,1,0.6,0.35);\nmesh3d(fl,{x:20,y:0,z:-3,color:'#8a7a66'});\nfor(let i=0;i<=N;i++){const x=i*4;mesh3d(col,{x:x+2,z:-5,color:'#d8d0c0'});\nconst last=i==N;const r=last?smoothstep(end-6,end-2,cx):1;const y=last?-1.2+r*1.2:0;\nmesh3d(ped,{x,y:y+0.6,z:-2,color:last?'#e8c070':'#c8c8d0'});\nmesh3d(gem,{x,y:y+1.7,z:-2,ry:t,color:last?'#ffd060':mix('#80c0ff','#c090ff',i/N),ascii:last});\nconst p=project(x,y+2.5,-2);const lb=last?'{what}':hs[i];if(p&&r>0.5)text(p.x-lb.length/2,p.y,lb,last?'#ffe080':'#ccd');}\nconst wk=cx<end-1;const me=clawd3d(cx+1,0,0.5,{facing:wk?1:0,stride:wk?Math.floor(t*5)%4:0,arms:wk?'down':{left:'down',right:'up'},color:'#d97757'});\nif(me){for(let k=0;k<6;k++)put(me.right.x+1+k,me.right.row-1+Math.floor(k/3),'░',mix('#fff6a0','#554',k/6));\nconst q=fract(t/12);let s='';if(q>0.1&&q<0.35)s='Night shift: guarding every commit since the first.';\nif(!wk)s='New exhibit: {what}. Nobody touch the history.';if(s)say(s,me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#778"],"speed":0.3,"intensity":0}}},
17 {"phase":"agents","world":"haunted house","style":"3D","concept":"rocket launch pad: Clawd as flight director at a row of four pads, sending subagent rockets up one after another, each refilled and launched","source":"claude-opus-5-5","scene":{"concept":"rocket launch pad: Clawd as flight director at a row of four pads, sending subagent rockets up one after another, each refilled and launched again","code":"const bd=cylinder(0.3,1.6,10),ns=cone(0.3,0.6,10),pd=box(1.8,0.2,1.8),tw=box(0.25,3,0.25),gr=plane(40,24);\nconst cols=['#e8e8f0','#9fd0ff','#ffd27a','#b8f0a0'],L=['Four agents fueled. Liftoff on {what} in 3...','Agent two is up. Nobody touch the big red button.','They report back when they land. I wait. I sip.','Another one cleared the tower. Parallel is nice.'];\nfunction frame(t,dt){camera(0,2.6,9,0,1.6,0,90);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nmesh3d(gr,{y:0,z:-2,color:'#556070'});let up=false;\nfor(let i=0;i<4;i++){const x=-6+i*4,z=-2,ph=mod(t+i*3,12);\nmesh3d(pd,{x,y:0.1,z,color:'#8a8f99'});mesh3d(tw,{x:x+1.1,y:1.5,z,color:'#c06040'});\nconst y=ph<4?0.2:0.2+Math.pow(ph-4,2)*0.6;if(ph>=4&&ph<6)up=true;\nif(y<25){mesh3d(bd,{x,y,z,color:cols[i]});mesh3d(ns,{x,y:y+1.6,z,color:'#e05050'});\nif(ph>3.4)for(let k=0;k<6;k++)point3d(x+(Math.random()-0.5)*0.4,y-0.2-Math.random()*1.2,z,k%2?'#ffb030':'#ff5020');}\nconst p=project(x,3.2,z);if(p&&ph<4)text(p.x-1,p.y,'T-'+Math.ceil(4-ph),'#ffe070');\nif(p&&i==0)text(p.x-4,p.y+3,'{what}','#ffffff');}\nconst c=clawd3d(0,0,3,{facing:0,arms:up?'up':'down',blink:fract(t*0.3)<0.04});\nif(c)say(L[Math.floor(t/7)%4],c.top.x,c.row);}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#ffffff","#9fb8ff","#ffe0a0"],"speed":0.4,"intensity":0.3}}},
18 {"phase":"writing","world":"fairy tale forest","style":"3D","concept":"orchestra concert: Clawd conducts a 3D orchestra through the grand finale of the reply, {what} on the marquee","source":"claude-opus-5-5","scene":{"concept":"orchestra concert: Clawd conducts a 3D orchestra through the grand finale of the reply, {what} on the marquee","code":"const fl=plane(26,12),bd=box(28,5,.3),body=cylinder(.32,.9,8),head=sphere(.24,8),horn=cone(.18,.6,8),st=box(.5,.05,.4);\nconst P=Array.from({length:11},(_,i)=>{const a=(i/10-.5)*2.4;return{x:Math.sin(a)*7,z:-3-Math.cos(a)*3,c:hsl(i*33,.5,.65)}});\nconst L=['Final movement: {what}. Strings, gently...','Brass! Give me the summary in fortissimo!','Woodwinds, softer, it is only a code block.','And now... the closing paragraph. Hold it...','Bravo! Encore? No? Fine, I will keep typing.'];\nfunction frame(t,dt){camera(Math.sin(t*.12)*3,2.4,8.5,0,1,-3,80);light(.3,1,.7,.35);fog(8,30,'#000');\nmesh3d(bd,{y:2.5,z:-8,color:'#a03a4a'});mesh3d(fl,{z:-3,color:'#b07a40'});\nconst beat=Math.abs(Math.sin(t*3.2));\nP.forEach((p,i)=>{const b=Math.max(0,Math.sin(t*3.2+i*.6))*.12;mesh3d(body,{x:p.x,z:p.z,color:p.c});mesh3d(head,{x:p.x,y:1.15+b,z:p.z,color:'#f0d0b0'});mesh3d(st,{x:p.x,y:.9,z:p.z+.6,color:'#ddd'});if(i%3==1)mesh3d(horn,{x:p.x+.3,y:1,z:p.z+.3,rx:1.4+b,color:'#ffd060'});});\nconst m=project(0,4.2,-7.8);if(m)text(Math.round(m.x-6),Math.max(0,m.y),'♪ '+'{what}'+' ♪','#ffe8a0');\nconst up=fract(t*1.6)<.5;const c=clawd3d(0,0,2.5,{facing:0,arms:up?{left:'out',right:'up'}:{left:'up',right:'out'},size:1});\nif(c){text(c.right.x+1,c.right.row-1,up?'/':'─','#fff');}\nfor(let k=0;k<8;k++){const q=fract(t*.3+k/8);put(fract(k*.37+t*.02)*w,h-1-q*h,'♪♫'[k%2],hsl(40+k*30,.7,.7));}\nif(c&&fract(t/7)<.75)say(L[Math.floor(t/7)%L.length],c.top.x,c.row);}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#3a1020","#602030","#804050"],"speed":0.6,"intensity":0}}},
19 {"phase":"web","world":"ufo abduction","style":"3D","concept":"ufo abduction: Clawd pilots a saucer over a moonlit farm, tractor-beaming up a crate labeled {what} again and again","source":"claude-opus-5-5","scene":{"concept":"ufo abduction: Clawd pilots a saucer over a moonlit farm, tractor-beaming up a crate labeled {what} again and again","code":"const disk=sphere(1,12),dome=sphere(0.6,10),cr=box(0.8,0.8,0.8),barn=box(2,1.6,1.6),roof=cone(1.4,0.8,4),fence=box(0.1,0.5,0.1);\nconst st=Array.from({length:40},(_,i)=>[rand(i)*40-20,2+rand(i+9)*5,-20]);\nconst lines=['Beaming up {what}. Hold still, little crate.','Got it! The internet never sees us coming.','Another one for the mothership archive.','Tractor beam: the original download button.'];\nfunction frame(t,dt){camera(0,2,14,0,2,0,90);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nfor(const s of st)point3d(s[0],s[1],s[2],'#ccd');\nmesh3d(plane(60,30),{y:0,color:'#4a7a44'});\nmesh3d(barn,{x:-8,y:0.8,z:-3,color:'#c05050'});mesh3d(roof,{x:-8,y:1.6,z:-3,ry:0.78,color:'#8a3a3a'});\nfor(let i=0;i<8;i++)mesh3d(fence,{x:4+i*1.2,y:0.25,z:-2,color:'#b9a27a'});\nconst c=Math.floor(t/10),p=t%10;\nconst ux=p<3?lerp(-16,0,smoothstep(0,3,p)):p<7?0:lerp(0,16,smoothstep(7,10,p)),uy=3.7+Math.sin(t*2)*0.1;\nconst k=smoothstep(3.5,6.5,p),cy=0.4+k*2.6;\nif(p>3&&p<7){for(let a=0;a<8;a++){const an=a/8*6.28+t;line3d(ux,uy-0.2,0,Math.cos(an)*1.2,0,Math.sin(an)*1.2,'#9fffb0')}}\nif(p<6.6){mesh3d(cr,{x:0,y:cy,z:0,ry:k*4,rx:k*2,color:'#e0a060'});const q=project(0,cy+0.6,0);if(q&&p<3.5)text(q.x-5,q.y-1,'{what}','#ffd')}\nmesh3d(disk,{x:ux,y:uy,z:0,scale:1,color:'#b8c4d0',sx:1});\nmesh3d(disk,{x:ux,y:uy,z:0,ry:t,scale:1,color:'#b8c4d0'});\nconst me=clawd3d(ux,uy+0.5,0.3,{size:0.6,facing:0,arms:p>3&&p<7?'up':'down'});\nmesh3d(dome,{x:ux,y:uy+0.6,z:0,wire:true,color:'#8fe8ff'});\nfor(let a=0;a<6;a++){const an=a/6*6.28+t*3;point3d(ux+Math.cos(an)*1.6,uy-0.1,Math.sin(an)*0.6,a%2?'#ffe066':'#ff6b9a')}\nif(me&&p>1.5&&p<8.5)say(lines[c%4],me.top.x,me.row)}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#668","#aac"],"speed":0.4,"intensity":0}}},
20 {"phase":"testing","world":"mountain climb","style":"3D","concept":"fishing pond 3D: Clawd on a dock waiting for bites while {what} runs, each passing test a leaping fish, the odd failure an old boot","source":"claude-opus-5-5","scene":{"concept":"fishing pond 3D: Clawd on a dock waiting for bites while {what} runs, each passing test a leaping fish, the odd failure an old boot","code":"const pl=plane(40,30),dk=box(4,0.3,2.5),tr=cone(1.2,3,8),tk=cylinder(0.2,1,6),bb=sphere(0.18,8),fs=sphere(0.35,8),bt=box(0.5,0.4,0.3),bk=cylinder(0.4,0.5,8),sg=box(1.8,0.8,0.1),ps=cylinder(0.06,1.5,5);\nfunction frame(t,dt){camera(0,2.6,9,0,0.8,-2,80);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nmesh3d(pl,{y:0,z:-6,color:'#3f7fb0'});\nfor(let i=0;i<6;i++){const x=-10+i*4.2,z=-13-rand(i)*4;mesh3d(tk,{x,z,color:'#8a6a4a'});mesh3d(tr,{x,y:1,z,color:i%2?'#5fb06a':'#7cc47a'});}\nmesh3d(dk,{x:-6,y:0.3,z:2,color:'#c09060'});mesh3d(ps,{x:6,z:0,color:'#b08a60'});mesh3d(sg,{x:6,y:1.6,z:0,color:'#e8d8a8'});\nconst sp=project(6,1.6,0);if(sp)text(sp.x-5,sp.y,'{what}'.slice(0,12),'#ffe');\nmesh3d(bk,{x:-4.6,y:0.45,z:2.6,color:'#9aa6b2'});\nconst c=mod(t,10),n=floor(t/10),boot=n%4==3;let by=0.05+sin(t*2)*0.04;if(c>5&&c<7)by=0.05-abs(sin(t*18))*0.15;\nconst bx=2,bz=-2;\nconst me=clawd3d(-6.5,0.45,2,{facing:1,arms:{left:'down',right:c>7&&c<8.5?'up':'out'},eyes:c>5&&c<7?'wide':'open'});\nconst tip=project(-3.5,2.4,1),bp=project(bx,by+0.15,bz);if(tip&&bp&&!(c>7&&c<8.5))line(tip.x,tip.y,bp.x,bp.y,'.','#ddd');\nif(me&&tip)line(me.right.x,me.right.row,tip.x,tip.y,'/','#a87a4a');\nif(!(c>7&&c<8.5))mesh3d(bb,{x:bx,y:by,z:bz,color:'#ff5050'});\nif(c>7&&c<8.5){const k=(c-7)/1.5,x=lerp(bx,-4.6,k),y=Math.sin(Math.PI*k)*2.5+0.6,z=lerp(bz,2.6,k);if(boot)mesh3d(bt,{x,y,z,rz:t*6,color:'#a07850'});else mesh3d(fs,{x,y,z,color:'#f0c040',scale:1});if(tip){const fp=project(x,y,z);if(fp)line(tip.x,tip.y,fp.x,fp.y,'.','#ddd');}}\nconst f=n-floor((n+1)/4);for(let i=0;i<Math.min(f,5);i++)mesh3d(sphere(0.15,6),{x:-4.8+i*0.12,y:1,z:2.6,color:'#f0c040'});\nif(c>5&&c<6.8)for(let i=0;i<5;i++){const p=project(bx+Math.cos(i*1.3+t)*0.6*(c-5),0.05,bz+Math.sin(i*1.3)*0.3);if(p)put(p.x,p.y,'~','#bfe8ff');}\nif(me){let s='';if(c<4&&c>1)s=n==0?'Line cast into {what}. Now we wait.':'Patience. The tests bite when they bite.';else if(c>5&&c<7)s='Nibble! Something in {what} is moving!';else if(c>7.5&&c<10)s=boot?'An old boot. One failing test, recast.':'Caught a passing test! Into the bucket.';if(s)say(s,me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#203a5a","#2e5a7a","#4a7a9a"],"speed":0.4,"intensity":0}}},
21 {"phase":"thinking","world":"nature documentary","style":"3D","concept":"carnival midway at night: Clawd riding a slowly turning 3D ferris wheel, pondering {what} from the top while booth lights blink below","source":"claude-opus-5-5","scene":{"concept":"carnival midway at night: Clawd riding a slowly turning 3D ferris wheel, pondering {what} from the top while booth lights blink below","code":"const cab=box(0.9,0.7,0.7),hub=cylinder(0.3,0.4,10),booth=box(1.6,1.4,1.2),roof=cone(1.1,0.8,8),post=cylinder(0.06,4,6);\nconst lines=['Thinking about {what}. Best view is at the top.','Round and round, like my ideas on {what}.','Ooh, the top! I can almost see the answer.','One more lap and I will have it figured out.'];\nfunction frame(t,dt){camera(Math.sin(t*0.1)*2,2.6,10,0,2.2,0,90);light(0.4,0.8,0.6,0.35);fog(10,40,'#05030a');\nconst cx=0,cy=4.2,R=3.2,a=t*0.25;\nfor(let i=0;i<8;i++){const b=a+i*Math.PI/4,x=cx+Math.cos(b)*R,y=cy+Math.sin(b)*R;line3d(cx,cy,0,x,y,0,'#ffd27a');const b2=b+Math.PI/4;line3d(x,y,0,cx+Math.cos(b2)*R,cy+Math.sin(b2)*R,0,'#ff8fc0');if(i)mesh3d(cab,{x,y:y-0.6,z:0.3,color:hsl(i*45,0.7,0.6)});}\nmesh3d(hub,{x:cx,y:cy-0.2,z:0,rx:1.57,color:'#ccc'});line3d(-2,0,0,cx,cy,0,'#aab');line3d(2,0,0,cx,cy,0,'#aab');\nconst me={x:cx+Math.cos(a)*R,y:cy+Math.sin(a)*R};mesh3d(cab,{x:me.x,y:me.y-0.6,z:0.3,color:'#ffcf5a'});\nconst c=clawd3d(me.x,me.y-0.5,0.75,{size:0.6,facing:0,blink:fract(t*0.3)<0.05,arms:Math.sin(a)>0.9?'up':'down'});\nfor(let j=0;j<4;j++){const bx=-9+j*(j<2?3:3)+(j>1?10:0);mesh3d(booth,{x:bx,y:0.7,z:2,color:j%2?'#e06666':'#f4f4f4'});mesh3d(roof,{x:bx,y:1.4,z:2,color:j%2?'#f4f4f4':'#e06666'});for(let k=0;k<5;k++){const p=project(bx-0.7+k*0.35,2.3,2);if(p)put(p.x,p.y,'●',fract(t*2+k*0.2+j*0.5)<0.5?'#ffe066':'#ff6fa8');}}\nconst lp=project(6,1.0,2.6);if(lp)text(lp.x-4,lp.y,'{what}','#fff');\nif(c){const s=Math.sin(a);const i=Math.floor(t/7)%lines.length;if(fract(t/7)<0.75)say(lines[i],c.top.x,c.row);}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#668","#aab"],"speed":0.4,"intensity":0.25}}},
22 {"phase":"thinking","world":"desert caravan","style":"3D","concept":"mail room in 3D: Clawd sorting thought-parcels on a conveyor, weighing which pigeonhole {what} belongs in","source":"claude-opus-5-5","scene":{"concept":"mail room in 3D: Clawd sorting thought-parcels on a conveyor, weighing which pigeonhole {what} belongs in","code":"const bx=box(0.9,0.7,0.9),sh=box(0.9,0.9,0.1),cv=box(14,0.2,1.6);\nfunction frame(t,dt){camera(Math.sin(t*0.15)*1.5,2.6,8,0,1,0,90);light(0.4,1,0.6,0.35);fog(8,35,'#000');\nmesh3d(cv,{x:0,y:0.4,z:0,color:'#8890a0'});\nfor(let i=0;i<8;i++){const x=mod(t*1.2+i*2,16)-8;mesh3d(bx,{x,y:0.85,z:0,ry:i,color:i%3?'#d8a868':'#c89858'});}\nfor(let c=0;c<9;c++)for(let r=0;r<3;r++){const x=-8+c*2,y=1+r*1.05,z=-5;const lit=floor(t*0.7)%27==c*3+r;mesh3d(sh,{x,y,z,color:lit?'#ffe080':'#a07850'});}\nconst k=fract(t/6),ph=floor(t/6);const hx=lerp(2,-6+(ph*5)%13,smoothstep(0.4,0.9,k)),hy=lerp(1.4,3,smoothstep(0.4,0.9,k)),hz=lerp(0.8,-4.6,smoothstep(0.4,0.9,k));\nconst me=clawd3d(2.5,0,1.6,{facing:k<0.4?0:-1,arms:k<0.4?'out':'up'});\nmesh3d(bx,{x:hx,y:hy,z:hz,scale:0.7,ry:t,color:'#f0c070'});\nconst p=project(hx,hy+0.6,hz);if(p)text(p.x-4,p.y,'{what}','#fff');\nif(me){const L=['Which pigeonhole does {what} go in?','Not this one. Hmm. Maybe that one.','Return to sender: me, five seconds ago.','Sorting thoughts, one parcel at a time.'];say(L[ph%4],me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#221a10","#332818","#443320"],"speed":0.4,"intensity":0}}},
23 {"phase":"thinking","world":"ancient tomb","style":"3D","concept":"film noir city at night: Clawd as trench-coat detective under a streetlamp on a rainy 3D street, puzzling over a case labeled {what} while t","source":"claude-opus-5-5","scene":{"concept":"film noir city at night: Clawd as trench-coat detective under a streetlamp on a rainy 3D street, puzzling over a case labeled {what} while the camera slowly dollies past dark buildings","code":"const bld=box(1,1,1);const lamp=cylinder(0.06,3,6);const head=sphere(0.25,8);const B=Array.from({length:14},(_,i)=>({x:(i%2?1:-1)*(5+rand(i)*2),z:-i*4+4,hh:2+rand(i+9)*5,c:mix('#8890a0','#a0a8b8',rand(i+3))}));const R=Array.from({length:60},(_,i)=>({x:(rand(i)-0.5)*16,y:rand(i+70)*6,z:-rand(i+40)*20+4}));const lines=['Case file: {what}. Something here does not add up.','The trail goes cold right about... here.','Every clue points back to {what}.','Wait. What if it was the config all along?','Got it. Time to make some moves.'];function frame(t,dt){const a=t*0.05;camera(Math.sin(a)*3,2.4,8,0,1,-4,85);light(-0.4,0.8,0.5,0.25);fog(6,34,'#000');for(const b of B){const z=b.z+mod(t*0.4,4)-2;mesh3d(bld,{x:b.x,y:b.hh/2,z:b.z,rx:0,ry:0,scale:1,color:b.c});mesh3d(box(2,b.hh,2),{x:b.x,y:b.hh/2,z:b.z,color:b.c});for(let k=0;k<3;k++){if(rand(k+b.z*7+floor(t*0.3))>0.6){const p=project(b.x+(b.x>0?-1.01:1.01),1+k*1.3,b.z);if(p)put(p.x,p.y,'▪','#ffd27a')}}}line3d(-3,0,8,-3,0,-50,'#556');line3d(3,0,8,3,0,-50,'#556');mesh3d(lamp,{x:2.2,z:-1,color:'#b0b0b0'});mesh3d(head,{x:2.2,y:3,z:-1,color:'#fff0b0',unlit:true});for(const r of R){r.y-=dt*9;if(r.y<0)r.y=6;const p=project(r.x,r.y,r.z);if(p)put(p.x,p.y,'╎','#6a7a9a')}const think=fract(t/6)<0.5;const c=clawd3d(0.6,0,-1,{facing:0,size:1.2,color:'#b06a50',arms:think?{left:'down',right:'up'}:'down',eyes:fract(t*0.3)<0.04?'closed':'open'});if(c){pixels(c.top.x-6,c.top.py-3,'....######....\\n..##########..\\n##############','#'==='#'?{'#':'#5a4a3a'}:{});const s=project(0.6,1.6,0.2);const i=floor(t/9)%lines.length;if(fract(t/9)<0.75)say(lines[i],c.top.x,c.row);const q=project(-0.6,0.9,-0.6);if(q)text(q.x-4,q.y,'{what}','#ffe9a8');if(think&&c.right)put(c.right.x+1,c.right.row-1,fract(t)<0.5?'?':'¿','#ffd27a')}}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#334","#445","#556"],"speed":1,"intensity":0}}},
24 {"phase":"thinking","world":"ufo abduction","style":"pixel art","concept":"jungle safari: pith-helmeted Clawd crouches in tall grass with binoculars, stalking a beetle labeled {what} that keeps peeking out and ducki","source":"claude-opus-5-5","scene":{"concept":"jungle safari: pith-helmeted Clawd crouches in tall grass with binoculars, stalking a beetle labeled {what} that keeps peeking out and ducking away","code":"const P={g:'#3c8a3a',G:'#6cc04a',b:'#7a5230',k:'#2a1a0a',y:'#e8c860',r:'#c0402a',w:'#ffffff'};\nconst lines=['Something in {what} just rustled.','Hold still... it thinks I can not see it.','There! No. That was a leaf.','Patience. Bugs always come back for a snack.','Got a lead on {what}. Quietly now.'];\nfunction frame(t,dt){for(let x=0;x<w;x++){for(let py=0;py<4;py++){const n=noise2(x*0.15,py*0.5+t*0.05);if(n>0.35+py*0.08)pixel(x,py,n>0.6?P.G:P.g)}}\nfor(const tx of [8,30,88,108]){fill(tx,1,3,h-1,' ',null,'#5a3a20');put(tx+3,2+(tx%3),'~','#4a9a3a')}\nfor(let x=0;x<w;x+=9)line(x+4,0,x+4,1+Math.floor(noise(x+t*0.3)*3),'|','#3a7a2a');\nconst cy=t%12,bx=72+Math.sin(t*0.4)*14;const up=cy>3&&cy<6||cy>8.5&&cy<10;\nconst c=clawd(46,h-5,{pose:'stand',facing:bx>53?1:-1,blink:fract(t*0.3)<0.04,eyes:up?'wide':'open',arms:{left:'down',right:'up'}});\npixels(c.top.x-5,c.top.py-3,'..yyyyyy..\\n.yyyyyyyy.\\nyyyyyyyyyy\\n..kkkkkk..',P);\npixels(c.right.x-1,c.right.py-2,'kk.kk\\nkk.kk',P);\nif(up){const by=h*2-6-Math.min(1,(cy%3))*2;pixels(bx,by,'.k.k.\\nrrrrr\\nrwrwr\\n.rrr.',P);text(bx-2,Math.floor(by/2)-1,'{what}','#ffd27a')}\nfor(let x=0;x<w;x++){const s=Math.sin(x*1.3+t*1.5)*0.6;pixel(x,h*2-3+Math.round(s),P.G);pixel(x,h*2-2,P.g);pixel(x,h*2-1,P.g)}\nconst i=Math.floor(t/7)%lines.length;if(fract(t/7)<0.7)say(lines[i],c.top.x,c.row)}","actors":[],"particles":[{"glyphs":"·*","count":8,"x":"20+noise(t*0.3+k)*80","y":"2+noise(t*0.4+k*3)*4","color":"#e8f070"}],"background":{"effect":"aurora","palette":["#0a3a1a","#1a5a2a","#2a6a3a"],"speed":0.3,"intensity":0.2}}},
25 {"phase":"thinking","world":"medieval castle","style":"3D","concept":"medieval train station: Clawd as a knight-stationmaster on a stone platform, pondering which track to send the royal steam engine down while","source":"claude-opus-5-5","scene":{"concept":"medieval train station: Clawd as a knight-stationmaster on a stone platform, pondering which track to send the royal steam engine down while trains arrive from the misty distance","code":"const plat=box(4,0.6,30),pil=cylinder(0.25,2.6,8),tow=box(1.4,3,1.4),cap=cone(1,1,8),eng=box(1.6,1.4,3),chim=cylinder(0.25,0.7,8),car=box(1.6,1.2,3);\nfunction frame(t,dt){camera(6+Math.sin(t*0.1)*1.5,2.6,8,0,1,-12,90);light(0.4,1,0.6,0.35);fog(10,50,'#000');\nmesh3d(plat,{x:-2.5,y:0.3,z:-10,color:'#a89c88'});\nfor(let i=0;i<6;i++){const z=4-i*6;mesh3d(pil,{x:-4,y:0.6,z,color:'#c8bca8'});mesh3d(pil,{x:-1,y:0.6,z,color:'#c8bca8'});line3d(-4,3.2,z,-1,3.2,z,'#998870');}\nmesh3d(tow,{x:-7,y:1.5,z:-20,color:'#b0a490'});mesh3d(cap,{x:-7,y:3,z:-20,color:'#c05050'});mesh3d(tow,{x:9,y:1.5,z:-26,color:'#b0a490'});mesh3d(cap,{x:9,y:3,z:-26,color:'#5070c0'});\nfor(const r of [0.6,2.4,4.4,6.2])line3d(r,0,10,r,0,-60,'#8a8a9a');\nfor(let z=8;z>-60;z-=2){line3d(0.3,0,z,2.7,0,z,'#6a5040');line3d(4.1,0,z,6.5,0,z,'#6a5040');}\nconst cyc=mod(t,22),tz=-60+smoothstep(0,9,cyc)*55+smoothstep(14,22,cyc)*60,tx=cyc<11?1.5:5.3;\nmesh3d(eng,{x:tx,y:0.9,z:tz,color:'#d0b040'});mesh3d(chim,{x:tx,y:1.6,z:tz-1,color:'#806040'});\nfor(let c=1;c<3;c++)mesh3d(car,{x:tx,y:0.8,z:tz+c*3.3,color:c%2?'#c06a50':'#6a9a70'});\nfor(let s=0;s<5;s++){const p=project(tx,2.4+s*0.4+fract(t)*0.4,tz-1-s*0.3);if(p)put(p.x+s,p.y,s%2?'○':'°','#ddd');}\nconst lp=project(tx,1.7,tz+0.5);if(lp&&tz>-30)text(lp.x-5,lp.y,'{what}','#ffe');\nconst sw=project(3.4,0.2,0);if(sw)text(sw.x-1,sw.y,cyc<11?'◄╱':'╲►','#fc6');\nconst me=clawd3d(-2.5,0.6,1,{facing:1,arms:cyc>9&&cyc<12?'up':{left:'down',right:'out'},size:1});\nif(me){pixels(me.top.x-3,me.top.py-3,'.ggggg.\\nggggggg\\n.g...g.','{\"g\":\"#c0c8d0\"}'.length?{g:'#c0c8d0'}:{});\nconst L=cyc<5?'Which track for {what}? Hmm, left feels royal.':cyc<10?'Thinking... the north line or the east line.':cyc<13?'Throw the lever! East it is!':cyc<18?'Train departs on time. Chivalry lives.':'Next one is due. More thinking, sire.';say(L,me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#203040","#405070","#604060"],"speed":0.4,"intensity":0}}},
26 {"phase":"thinking","world":"pirate ship","style":"pixel art","concept":"pirate ship meets train station: Clawd as pirate captain at the wheel of a ship-train steaming along rails over the sea, scanning with a spy","source":"claude-opus-5-5","scene":{"concept":"pirate ship meets train station: Clawd as pirate captain at the wheel of a ship-train steaming along rails over the sea, scanning with a spyglass for the answer","code":"function frame(t,dt){for(let y=0;y<h;y++)for(let x=0;x<w;x++){if(y<6){put(x,y,' ',null,mix('#1a2a4a','#3a4a7a',y/6))}else put(x,y,'~',y==6?'#7ab':'#48a',y==6?'#2a5a8a':'#1d4470')}for(let x=0;x<w;x++)put(x,6,(x+Math.floor(t*8))%4==0?'┼':'═','#a98')\nfor(let i=0;i<5;i++){const sx=mod(i*31-t*3,w+20)-10;pixels(sx,2+i%2,'..##..\\n.####.\\n######',{'#':'#cdd'})}\nconst bx=30;pixels(bx,6,'..oooooooooooooooooooooooooooooooooo..\\n.oooooooooooooooooooooooooooooooooooo.\\n..o..o.....o..o.....o..o.....o..o.....',{o:'#8a5a2a'});fill(bx+2,4,34,1,'▄','#6b3f1a');fill(bx+18,0,1,4,'│','#cba');pixels(bx+13,0,'wwwwwwwwwww\\nwwwwwwwwwww\\nwwwwwwwwwww\\nwwwwwwwwwww\\nwwwwwwwwwww',{w:'#eee'});text(bx+14,1,'{what}'.slice(0,9),'#222','#eee');pixels(bx+30,0,'.k.\\nkkk\\nkkk\\nkkk\\nkkk\\nkkk',{k:'#333'});for(let i=0;i<4;i++){const p=fract(t*0.7+i/4);put(bx+31-p*25,-p*2,'○',mix('#ddd','#556',p))}\nconst c=clawd(bx+2,1,{facing:1,arms:{left:'down',right:'up'},look:{x:2,y:-1}});pixels(c.top.x-4,c.top.py-3,'..kkkk..\\nkkkkkkkk\\n.kwkkwk.',{k:'#222',w:'#fff'});pixels(c.right.x,c.right.py-1,'yyyyyy',{y:'#da4'});const st=Math.floor(t/12)%4;const L=['Full steam ahead on {what}!','Spyglass says: answer at next stop.','Arr, rails or waves, we keep thinking.','Land ho! Er... platform ho!'];if(fract(t/12)<0.7)say(L[st],c.top.x,c.row)\nconst px=w-mod(t*6,w+40)+20;pixels(px,8,'gggggggggggggg',{g:'#998'});pixels(px+5,4,'.p.\\nppp\\n.p.\\n.p.',{p:'#cc8'});text(px+2,3,'STATION','#ffd')}","actors":[],"particles":[],"background":{"effect":"waves","palette":["#1a2a4a","#2a4a7a","#3a6a9a"],"speed":0.5,"intensity":0}}},
27 {"phase":"thinking","world":"construction site","style":"text art","concept":"snow globe construction site: Clawd in a hard hat inside a glass globe, thinking while snow falls, a tiny crane slowly stacking blocks label","source":"claude-opus-5-5","scene":{"concept":"snow globe construction site: Clawd in a hard hat inside a glass globe, thinking while snow falls, a tiny crane slowly stacking blocks labeled {what}; every so often the globe gets shaken and the snow swirls up","code":"let fl=Array.from({length:70},(_,i)=>({x:rand(i),y:rand(i+40)*9,v:0.6+rand(i+80)}))\nfunction frame(t,dt){const cx=w/2,R=28;const sh=mod(t,24)<1.2;const off=sh?Math.sin(t*40)*1.5:0;for(let y=0;y<8;y++){const dy=(y-4.5)/4.6;const hw=Math.sqrt(Math.max(0,1-dy*dy))*R;put(cx-hw+off,y,'(','#9fc8e8');put(cx+hw+off,y,')','#9fc8e8')}text(cx-R*0.85+off,7,'▄'.repeat(Math.floor(R*1.7)),'#dfe8f0');fill(cx-24,8,48,1,'█','#7a4a2a');text(cx-4,8,'{what}'.slice(0,8),'#ffd27a','#7a4a2a');const bx=cx+6+off;line(bx,1,bx,6,'║','#f0c040');line(bx,1,bx+12,1,'═','#f0c040');const p=fract(t/8);const hx=bx+lerp(11,3,smoothstep(0.2,0.6,p));const hy=lerp(2,5,smoothstep(0.6,0.9,p))*(p<0.9?1:0)+(p>=0.9?2:0);const n=Math.min(4,Math.floor(t/8));line(hx,2,hx,hy,'│','#aaa');if(p<0.9)text(hx-1,hy+1,'[■]','#e08040');for(let i=0;i<n;i++)text(bx+2,6-i,'[■]','#e08040');for(const f of fl){f.y+=(sh?-6:f.v)*dt;if(f.y>7)f.y=0.5;if(f.y<0)f.y=6.5;const x=cx+(f.x-0.5)*2*R*Math.sqrt(Math.max(0,1-((f.y-4.5)/4.6)**2))*0.9+off;put(x+Math.sin(t+f.x*9),f.y,sh?'*':'·','#ffffff')}const c=clawd(cx-20+off,3,{facing:1,eyes:sh?'wide':'open',blink:fract(t*0.3)<0.04,arms:sh?'up':{left:'down',right:'out'}});text(c.top.x-3,c.top.row-1,'▄███▄','#ffcc22');const m=mod(t,24);if(m>2&&m<7)say('Thinking about {what}. One block at a time.',c.top.x,c.row-1);else if(m>13&&m<18)say('Who keeps shaking the site?!',c.top.x,c.row-1)}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334466","#556688","#8899bb"],"speed":0.3,"intensity":0.2}}},
28 {"phase":"thinking","world":"weather report","style":"3D","concept":"arctic expedition: Clawd in a parka plants flags across a 3D ice field, pondering beside an igloo under aurora while a weather balloon label","source":"claude-opus-5-5","scene":{"concept":"arctic expedition: Clawd in a parka plants flags across a 3D ice field, pondering beside an igloo under aurora while a weather balloon labeled {what} drifts up","code":"const ice=box(1.6,1,1.6),ig=sphere(1.2,10),fl=cylinder(0.04,1.4,5),bg=cone(0.5,1.6,6);const flags=[];let nx=2;\nfunction frame(t,dt){camera(Math.sin(t*0.1)*2,2.6,9,0,1,0,90);light(0.4,0.8,0.5,0.35);fog(8,40,'#04101a');for(let i=0;i<w;i+=3){const a=Math.sin(i*0.05+t*0.6)*0.5+0.5;put(i,0,'░',hsl(140+a*60,0.7,0.5));put(i+1,1,'▒',hsl(160+a*80,0.6,0.35));}\nfor(let i=-4;i<5;i++)for(let j=0;j<3;j++)mesh3d(ice,{x:i*3+(j%2)*1.5,y:-0.5,z:-j*4,color:'#cfe8f5'});mesh3d(ig,{x:-6,y:0,z:-2,color:'#e8f4ff'});mesh3d(box(0.6,0.7,0.6),{x:-6,y:0.35,z:-0.8,color:'#335'});\nif(t>nx&&flags.length<6){flags.push(-2+flags.length*1.8);nx+=7;}const cyc=t%7,px=flags.length?flags[flags.length-1]:-2;const cx=lerp(px-1.8,px-0.6,smoothstep(0,3,cyc));\nfor(const f of flags){mesh3d(fl,{x:f,y:0,z:0.5,color:'#999'});const p=project(f,1.4,0.5);if(p)text(p.x,p.y,'▶',f==flags[flags.length-1]?'#ff5050':'#ffcc33');}\nconst by=1+t*0.04;mesh3d(sphere(0.35,8),{x:5,y:by+1.6,z:-3,color:'#ff8866'});line3d(5,by+1.25,-3,5,by,-3,'#ccc');const bp=project(5,by+2.3,-3);if(bp)text(bp.x-4,bp.y,'{what}','#fff');\nconst c=clawd3d(cx,0,0.5,{facing:cyc<3?1:0,stride:cyc<3?Math.floor(t*6)%4:0,arms:cyc>3?{left:'down',right:'up'}:'down',eyes:cyc>5?'closed':'open'});\nif(c){pixels(c.top.x-4,c.top.py-2,'.wwwwwwww.\\nwbbbbbbbbw',{w:'#fff',b:'#3a6fd8'});if(cyc>3.5)say(['Flag here: {what} is solid ice.','Hmm, thin ice past this line.','Mapping {what} one step at a time.','Brr. Thinking keeps me warm.'][flags.length%4],c.top.x,c.row);}\nfor(let k=0;k<30;k++)put(mod(rand(k)*w+t*3,w),mod(rand(k+5)*h+t*(1+rand(k)*2),h),'*','#eef');}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#1fae7a","#3fd0c0","#6a5acd"],"speed":0.5,"intensity":0}}},
29 {"phase":"searching","world":"ant colony up close","style":"3D","concept":"desert caravan in 3D: Clawd rides a camel train across dunes toward an oasis marked {what}, scouting tracks in the sand","source":"claude-opus-5-5","scene":{"concept":"desert caravan in 3D: Clawd rides a camel train across dunes toward an oasis marked {what}, scouting tracks in the sand","code":"const dune=sphere(3,10),cam=box(1.4,0.8,0.5),leg=cylinder(0.08,0.7,4),neck=box(0.25,0.8,0.25),palm=cylinder(0.12,2,5),leaf=cone(1,0.5,6),pool=cylinder(1.6,0.05,12);\nfunction frame(t,dt){camera(0,2.6,9,0,1,-6,90);light(0.4,0.8,0.5,0.35);fog(10,45,'#1a0f08');\nfor(let x=0;x<w;x++)for(let y=0;y<3;y++)put(x,y,' ',null,mix('#2a1640','#a0502a',y/3));\ndisc(w-18,1,1,' ',null,'#ffd27a');\nfor(let i=0;i<8;i++){const x=mod(i*7-t*1.2,56)-28;mesh3d(dune,{x,y:-2.4,z:-14-(i%3)*5,color:'#d9a860'})}\nconst ox=mod(30-t*1.2,70)-30;mesh3d(pool,{x:ox,z:-12,color:'#5ab8e0'});mesh3d(palm,{x:ox+1.5,z:-12.5,color:'#b08050'});mesh3d(leaf,{x:ox+1.5,y:2,z:-12.5,color:'#6fc060'});\nconst p=project(ox,2.6,-12);if(p)text(p.x-5,p.y,'{what}','#fff4c0');\nlet me=null;for(let k=0;k<3;k++){const x=-6+k*3.2,b=Math.abs(Math.sin(t*4+k))*0.15;mesh3d(cam,{x,y:1+b,z:0,color:'#c89060'});mesh3d(neck,{x:x+0.8,y:1.6+b,z:0,rz:-0.3,color:'#c89060'});for(const d of[-0.5,0.5]){mesh3d(leg,{x:x+d+Math.sin(t*6+k+d)*0.1,y:0.3,z:0.15,color:'#a87048'})}\nif(k==2)me=clawd3d(x,1.4+b,0,{size:0.7,facing:1,arms:fract(t/8)>0.6?{left:'down',right:'up'}:'down'})}\nfor(let i=0;i<14;i++){const tx=mod(i*1.5+t*1.2,20)-10;mesh3d(box(0.3,0.02,0.15),{x:tx,z:1.5,color:'#8a5a30'})}\nif(me){const s=fract(t/16);if(s>0.15&&s<0.45)say('Tracks lead to {what}. The grep is warm.',me.top.x,me.row);else if(s>0.6&&s<0.9)say('Mirage or match? Only one way to find out.',me.top.x,me.row)}}","actors":[],"particles":[],"background":{"effect":"waves","palette":["#3a2010","#6a3a18","#a0602a"],"speed":0.5,"intensity":0}}},
30 {"phase":"searching","world":"medieval castle","style":"3D","concept":"skate park at dusk: Clawd grinds a row of ramps, kickflipping over each crate to check its label, hunting for {what}","source":"claude-opus-5-5","scene":{"concept":"skate park at dusk: Clawd grinds a row of ramps, kickflipping over each crate to check its label, hunting for {what}","code":"const pl=plane(34,12),rb=box(3,0.6,2.4),cr=box(0.8,0.8,0.8),bd=box(1.1,0.08,0.45),pole=cylinder(0.06,3,6),rx=[-9,-3,3,9];\nfunction frame(t,dt){camera(0,3.2,10,0,1,0,90);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nmesh3d(pl,{y:0,z:-1,color:'#8a8f9a'});for(const x of [-14,14])mesh3d(pole,{x,z:-4,color:'#cc8'});\nconst c=Math.floor(t/12)%4,ph=mod(t,12);\nfor(let i=0;i<4;i++){mesh3d(rb,{x:rx[i],y:0.2,z:-2,rz:i%2?0.25:-0.25,color:'#d0b080'});const hit=i==c&&ph>8;mesh3d(cr,{x:rx[i],y:0.9+(hit?0.3*Math.abs(Math.sin(t*6)):0),z:-3,ry:t*0.5+i,color:hit?'#ffd23f':'#b07850'});const p=project(rx[i],1.9,-3);if(p)text(p.x-4,p.y,i==c?'{what}':['utils/','lib/','src/','test/'][i],hit?'#ffe066':'#aab');}\nconst tx=ph<8?Math.sin(t*0.9)*11:lerp(Math.sin(t*0.9)*11,rx[c],smoothstep(8,9,ph)),jy=ph<8?Math.max(0,Math.sin(t*3))*0.9:0;\nmesh3d(bd,{x:tx,y:jy+0.1,z:0.5,rz:jy>0.3?Math.sin(t*12)*0.6:0,color:'#e05a5a'});\nconst me=clawd3d(tx,jy+0.15,0.5,{facing:Math.cos(t*0.9)>0?1:-1,arms:jy>0.3?'up':'out',size:0.9});\nif(me){if(ph>8.5)say('Landed it! {what} was right under this ramp.',me.top.x,me.row);else if(ph>1&&ph<4)say('Ollie over every crate till I find {what}.',me.top.x,me.row);}\nif(ph>8)for(let k=0;k<12;k++){const p=project(rx[c]+Math.cos(k*2.4+t)*1.5,1.2+Math.sin(k*1.7+t*2)*0.8,-3);if(p)put(p.x,p.y,'✦',hsl(k*30,0.9,0.7));}}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#3a2a5a","#6a3a7a","#c86a4a"],"speed":0.5,"intensity":0.2}}},
31 {"phase":"searching","world":"arctic expedition","style":"3D","concept":"vegetable garden 3D: Clawd in a straw hat pulls up carrots row by row hunting the one labeled {what}","source":"claude-opus-5-5","scene":{"concept":"vegetable garden 3D: Clawd in a straw hat pulls up carrots row by row hunting the one labeled {what}","code":"const bed=box(1.6,0.3,1.2),leaf=cone(0.25,0.7,5),root=cone(0.18,0.8,6),post=cylinder(0.06,1.2,5);\nconst rows=[];for(let i=0;i<8;i++)rows.push({x:-9+i*2.6,z:-1});\nfunction frame(t,dt){camera(0,2.6,8,0,0.8,-2,90);light(0.4,1,0.6,0.35);fog(8,40,'#000');\nmesh3d(plane(40,30),{y:0,z:-6,color:'#7a5a3a'});\nfor(let j=0;j<5;j++)mesh3d(post,{x:-12+j*6,z:-8,color:'#c9a070'});line3d(-12,1,-8,12,1,-8,'#c9a070');\nconst cyc=4,k=Math.floor(t/cyc)%8,p=fract(t/cyc);const tgt=(Math.floor(t/cyc)%3==2)?k:-1;\nlet cx=-9;\nfor(let i=0;i<8;i++){const r=rows[i];mesh3d(bed,{x:r.x,y:0.15,z:r.z,color:'#8b6a48'});\nlet up=0;if(i<k)up=-1;if(i==k)up=smoothstep(0.35,0.6,p)*1.4;\nif(up>=0){const y=0.3+up;mesh3d(leaf,{x:r.x,y:y,z:r.z,color:'#6fd36f'});if(up>0.1)mesh3d(root,{x:r.x,y:y,z:r.z,rx:Math.PI,color:i==tgt?'#ffd24a':'#f08a30'});}\nelse mesh3d(root,{x:r.x,y:0.4,z:r.z+0.8,rz:1.5,color:'#f08a30'});}\ncx=lerp(k==0?-11:rows[Math.max(0,k-1)].x,rows[k].x-1.2,smoothstep(0,0.3,p));\nconst c=clawd3d(cx,0,0,{facing:p<0.3?1:0,stride:Math.floor(t*6)%4,arms:p>0.35&&p<0.7?'up':'down',size:1});\nif(c){pixels(c.top.x-5,c.top.py-3,'..yyyy..\\nyyyyyyyyyy\\n',{y:'#e8d27a'});}\nconst lp=project(rows[k].x,2.3,rows[k].z);if(lp&&p>0.5)text(lp.x-4,lp.y,i0(tgt==k),tgt==k?'#ffd24a':'#ccc');\nif(c&&p>0.6){say(tgt==k?'Found {what}! Golden root!':'Just a carrot. Next row.',c.top.x,c.row);}}\nfunction i0(b){return b?'{what}':'nope'}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#203a20","#2f5a3a","#4a7a4a"],"speed":0.4,"intensity":0}}},
32 {"phase":"searching","world":"race track pit stop","style":"3D","concept":"pirate ship: Clawd in a crow's nest on a 3D galleon sweeping a spyglass over a sea of buoys, hunting the one labeled {what}","source":"claude-opus-5-5","scene":{"concept":"pirate ship: Clawd in a crow's nest on a 3D galleon sweeping a spyglass over a sea of buoys, hunting the one labeled {what}","code":"const hull=box(6,1.2,2),mast=cylinder(0.12,4,6),sail=box(0.1,1.8,2.4),nest=cylinder(0.5,0.4,8),buoy=cone(0.35,0.9,6);\nconst bs=Array.from({length:9},(_,i)=>({x:-14+i*3.5+rand(i)*2,z:-6-rand(i+9)*14}));\nconst tgt=6;\nfunction frame(t,dt){\nconst ang=Math.sin(t*0.25)*0.5;\ncamera(Math.sin(t*0.1)*3,3,10,0,1.5,-6,90);light(0.4,0.8,0.5,0.35);fog(8,40,'#001018');\nfor(let x=-24;x<=24;x+=2)for(let z=-28;z<=6;z+=3){const y=Math.sin(x*0.5+t*1.5+z*0.3)*0.12;line3d(x,y,z,x+1.6,y,z,'#2a6a9a');}\nconst roll=Math.sin(t*0.9)*0.05,by=Math.sin(t*0.9)*0.1;\nmesh3d(hull,{x:-5,y:0.3+by,z:2,rz:roll,color:'#a8743c'});\nmesh3d(mast,{x:-5,y:0.9+by,z:2,color:'#c9a070'});\nmesh3d(sail,{x:-5,y:2.6+by,z:2,ry:1.57,color:'#eee6cc'});\nmesh3d(nest,{x:-5,y:4.3+by,z:2,color:'#8a5a30'});\nlet found=t>40;\nbs.forEach((b,i)=>{const hit=found&&i==tgt;const s=hit?1+0.2*Math.sin(t*8):1;mesh3d(buoy,{x:b.x,y:Math.sin(t+i)*0.15,z:b.z,scale:s,color:hit?'#ffd23c':(i%2?'#d05050':'#e0e0e0')});});\nconst me=clawd3d(-5,4.6+by,2,{size:0.8,facing:1,arms:{left:'down',right:'out'}});\nif(me){const sx=me.right.x;text(sx,me.right.row,'═◎','#e8c060');\nconst ax=Math.round(w/2+ang*60);line(sx+2,me.right.row,ax,5,'·',found?'#ffd23c':'#88aacc');\npixels(me.top.x-3,me.top.py-3,'.KKKKK.\\nKKwKwKK','{\"K\":\"#222\",\"w\":\"#fff\"}'.length?{K:'#222',w:'#fff'}:{});\nconst lines=['Sweepin\\' the seas for {what}...','Nothin\\' but bilge in this grep.','Every buoy\\'s a decoy, arr.','LAND HO! {what} off the port bow!'];\nconst li=found?3:Math.floor(t/12)%3;if(fract(t/12)<0.6||found)say(lines[li],me.top.x,me.row);}\nconst tb=bs[tgt],p=project(tb.x,1.4,tb.z);if(p&&found)text(p.x-4,p.y-1,'{what}','#ffd23c');\nif(found)for(let i=0;i<12;i++)put(w*0.6+Math.cos(i+t*3)*(t%2)*14,4+Math.sin(i*2.1)*(t%2)*3,'✦',hsl(i*30,0.8,0.65));}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#335577","#6688aa","#aaccee"],"speed":0.3,"intensity":0.3}}},
33 {"phase":"searching","world":"ancient tomb","style":"3D","concept":"lighthouse in a storm: Clawd as keeper atop a 3D lighthouse, the rotating beam sweeping the dark sea searching for a drifting crate labeled ","source":"claude-opus-5-5","scene":{"concept":"lighthouse in a storm: Clawd as keeper atop a 3D lighthouse, the rotating beam sweeping the dark sea searching for a drifting crate labeled {what}","code":"const tw=cylinder(0.9,4,10),top=cylinder(1.1,0.8,10),cap=cone(1.2,0.8,10),rk=box(3,0.6,2),cr=box(0.7,0.7,0.7);const sea=plane(60,40);function frame(t,dt){camera(0,3,11,0,2.2,0,90);light(0.3,1,0.6,0.35);fog(8,40,'#000');mesh3d(sea,{y:-0.1,z:-8,color:'#3a5a7a'});for(let i=0;i<14;i++){const x=-14+i*2.2,y=0.05+Math.sin(t*2+i)*0.15;line3d(x,y,-2-i%3*3,x+1.2,y,-2-i%3*3,'#9cd')}mesh3d(rk,{x:-3,y:0.3,z:0,color:'#8a8a80'});mesh3d(tw,{x:-3,y:0.5,z:0,color:'#e8e0d0'});mesh3d(top,{x:-3,y:4.5,z:0,color:'#c04040'});mesh3d(cap,{x:-3,y:5.3,z:0,color:'#c04040'});const a=t*0.9,lx=-3,ly=5,lz=0;for(let k=0;k<3;k++){const aa=a+k*0.04;line3d(lx,ly,lz,lx+Math.cos(aa)*18,0.5,lz-Math.abs(Math.sin(aa))*14-2,'#ffeb99')}const cxz=6+Math.sin(t*0.3)*3,cy=0.3+Math.sin(t*1.7)*0.2;const found=Math.cos(a%(2*Math.PI))>0.8&&Math.sin(a%(2*Math.PI))<0;mesh3d(cr,{x:cxz,y:cy,z:-6,ry:t*0.5,rz:Math.sin(t)*0.3,color:found?'#ffd070':'#a07040'});const p=project(cxz,1.2,-6);if(p)text(p.x-4,p.y-1,'{what}',found?'#fff6a0':'#c8a070');const me=clawd3d(-3,4.5,1.2,{size:0.7,facing:1,arms:found?'up':{left:'down',right:'out'}});if(me){const s=fract(t/14);if(s<0.3)say('Sweeping the dark for {what}...',me.top.x,me.row);else if(found)say('THERE! {what}, dead ahead!',me.top.x,me.row)}if(fract(t*0.13)<0.03){for(let x=0;x<w;x+=1)put(x,0,' ',null,'#334');line(70,0,64,4,'/','#fff')}}","actors":[],"particles":[{"glyphs":"/","count":80,"x":"mod(rand(k)*w - t*12, w)","y":"mod(t*(10+rand(k+9)*5)+rand(k+3)*h,h)","color":"#7a9cc0"}],"background":{"effect":"rain","palette":["#223344","#334455","#445566"],"speed":1.5,"intensity":0.2}}},
34 {"phase":"searching","world":"fairy tale forest","style":"3D","concept":"construction site in a fairy-tale forest, 3D: Clawd in a hard hat drives an excavator down a forest road, digging test pits one by one until","source":"claude-opus-5-5","scene":{"concept":"construction site in a fairy-tale forest, 3D: Clawd in a hard hat drives an excavator down a forest road, digging test pits one by one until the last pit strikes a glowing crate labeled {what}","code":"const tr=cone(0.9,2.6,7),tk=cylinder(0.15,0.8,6),cab=box(1.2,0.9,1),trk=box(1.6,0.35,1.2),arm=box(0.15,0.15,1.6),pit=cylinder(0.6,0.05,10),cr=box(0.8,0.8,0.8),cn=cone(0.25,0.6,6);\nconst trees=Array.from({length:16},(_,i)=>({x:(i%2?1:-1)*(5+rand(i)*4),z:-i*3+4}));\nfunction frame(t,dt){const P=14,cyc=t%P,seg=Math.floor(t/P),X=-6+cyc*0.9,dig=fract(cyc/2.8),dz=0;\ncamera(0,3,9,0,0.8,-4,90);light(0.4,1,0.6,0.35);fog(8,40,'#0b1410');\nmesh3d(plane(40,40),{y:0,z:-10,color:'#5a7a45'});\nfor(const q of trees){mesh3d(tk,{x:q.x,z:q.z,color:'#8a6a4a'});mesh3d(tr,{x:q.x,y:0.7,z:q.z,color:'#4fa86a'});}\nfor(let i=0;i<5;i++){const px=-6+i*2.8*0.9*1.0+1.5;if(px<X){mesh3d(pit,{x:px,z:1,color:'#7a5a3a'});}}\nfor(let i=-1;i<2;i+=2)mesh3d(cn,{x:i*7,z:1.5,color:'#ff9a40'});\nconst found=cyc>11.5;\nif(found){mesh3d(cr,{x:6.5,y:0.4+Math.sin(t*3)*0.1,z:1,ry:t,color:'#ffd060'});const p=project(6.5,1.3,1);if(p)text(p.x-4,p.y-1,'{what}','#fff2a0');}\nmesh3d(trk,{x:X,y:0.2,z:-1,color:'#777'});mesh3d(cab,{x:X,y:0.85,z:-1,color:'#f2c230'});\nconst a=Math.sin(dig*Math.PI)*0.7;mesh3d(arm,{x:X+0.8,y:1+a*0.5,z:-0.2,rx:0.6-a,color:'#e0a020'});\nconst c=clawd3d(X-0.2,1.3,-1,{size:0.6,facing:1});\nif(c){pixels(c.top.x-3,c.top.py-2,'..yyyy..\\nyyyyyyyy','{\"y\":\"#ffd23a\"}'.length?{y:'#ffd23a'}:{});\nif(found)say('Struck gold: {what} was buried right here!',c.top.x,c.row);else if(cyc>1&&cyc<5)say('Not under this oak. Next pit!',c.top.x,c.row);else if(cyc>6&&cyc<9)say('Digging for {what}... the forest hides it well.',c.top.x,c.row);}\nfor(let i=0;i<3;i++){const sx=X+1.5+Math.cos(i*2+t*4)*0.6,sy=0.3+fract(t*1.5+i/3)*0.8;point3d(sx,sy,0.2,'#c8a070');}\nfor(let i=0;i<6;i++){const p=project(-8+noise(t*0.3+i)*16,1.5+noise(t*0.4+i*5)*1.5,-3);if(p)put(p.x,p.y,'*','#bfffd0');}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#3a5a4a","#6a8a7a","#a0c0b0"],"speed":0.3,"intensity":0}}},
35 {"phase":"reading","world":"underground mine","style":"text art","concept":"bakery: Clawd as pastry inspector slicing a giant layered cake labeled {what} to study each layer inside","source":"claude-opus-5-5","scene":{"concept":"bakery: Clawd as pastry inspector slicing a giant layered cake labeled {what} to study each layer inside","code":"const L=[['sponge','#f3d27a'],['jam','#e0566b'],['cream','#fff3e0'],['sponge','#f3d27a'],['ganache','#a8653a']];\nfunction frame(t,dt){\nfor(let x=0;x<w;x++)put(x,h-1,'═','#8a6a4a');\ntext(2,1,'┌──────┐', '#c0c0c0');text(2,2,'│ OVEN │','#c0c0c0');text(2,3,'│▒▒▒▒▒▒│',mix('#a04020','#ffa040',0.5+0.5*Math.sin(t*2)));text(2,4,'│▒▒▒▒▒▒│','#ff8030');text(2,5,'└──────┘','#c0c0c0');\nconst cx=62,cw=46;\nconst cut=Math.min(cw-2,Math.floor(t*0.8)%(cw));\nfor(let i=0;i<5;i++){const y=2+i;for(let x=0;x<cw;x++){const open=Math.abs(x-cut)<2;if(open&&x==cut-1){text(cx+x,y,'▌',L[i][1]);continue}if(open)continue;put(cx+x,y,i==2?'░':i==1?'▒':'█',L[i][1])}}\ntext(cx,1,'~'.repeat(cw),'#fff3e0');put(cx+20,0,'♥','#e0566b');\ntext(cx+4,7,'{what}','#ffd27a');\nconst li=Math.floor(t/4)%5;text(cx+cw+2,2+li,'← '+L[li][0],'#9fe0a0');\nconst kx=cx+cut-15;const c=clawd(Math.min(kx,cx-16),3.5,1,-1,fract(t*0.3)<0.04);\ntext(c.top.x-2,c.row-1,'▄█▄','#ffffff');\ntext(c.right.x+1,c.right.row,'━━╾','#d0d0d0');\nconst S=['Reading {what} one layer at a time.','Jam layer calls the cream layer. Interesting.','Every import is another tier of sponge.','Who baked this? Taking notes.'];\nif(fract(t/7)<0.6)say(S[Math.floor(t/7)%4],c.top.x,c.row)}","actors":[],"particles":[{"glyphs":"·∙.","count":20,"x":"60+rand(k)*50","y":"mod(-t*0.5+rand(k+3)*9,9)","color":"#e8d8b0"}],"background":{"effect":"pulse","palette":["#3a2418","#4a3020","#2a1a10"],"speed":0.4,"intensity":0.2}}},
36 {"phase":"searching","world":"underground mine","style":"3D","concept":"desert caravan: Clawd riding the lead camel past rolling dunes, chasing an oasis labeled {what} that first turns out to be a mirage","source":"claude-opus-5-5","scene":{"concept":"desert caravan: Clawd riding the lead camel past rolling dunes, chasing an oasis labeled {what} that first turns out to be a mirage","code":"const dune=cone(3,1.4,8),body=box(1.4,0.6,0.5),leg=box(0.15,0.8,0.15),hump=sphere(0.35,6),head=box(0.3,0.3,0.3),trunk=cylinder(0.12,2.2,6),leaf=cone(1,0.4,6),pool=cylinder(1.6,0.05,10)\nfunction frame(t,dt){camera(-5,2.8,6,1,1,-4,80);light(0.4,1,0.6,0.35);fog(12,45,'#2a1408')\nfor(let i=0;i<14;i++){const x=mod(i*7-t*2,98)-49;mesh3d(dune,{x,z:-6-rand(i)*22,scale:0.8+rand(i+5)*0.6,color:'#e0b070'})}\nfor(let i=0;i<3;i++){const cx=-i*2.4,b=Math.abs(Math.sin(t*3+i))*0.12;mesh3d(body,{x:cx,y:1.1+b,color:'#c89060'});mesh3d(hump,{x:cx,y:1.45+b,color:'#b07a50'});mesh3d(head,{x:cx+0.9,y:1.6+b,color:'#c89060'});for(const d of[-0.5,0.5])mesh3d(leg,{x:cx+d,y:0.4,rz:Math.sin(t*6+i+d)*0.3,color:'#a87048'})}\nconst b0=Math.abs(Math.sin(t*3))*0.12,me=clawd3d(-0.1,1.55+b0,0,{size:0.7,facing:1,stride:-1,arms:mod(t,20)>13?'up':'down'})\nconst p=mod(t,20),cyc=Math.floor(t/20)%2,ox=26-p*1.6,real=cyc==1||p<11\nif(real&&ox>-20){const sh=cyc==0&&p>8?(Math.sin(t*20)>0?'#80c0ff':'#ffe0a0'):null;mesh3d(pool,{x:ox,z:-7,color:sh||'#60a8e0'});mesh3d(trunk,{x:ox,z:-7,color:sh||'#a07040'});mesh3d(leaf,{x:ox,y:2.1,z:-7,color:sh||'#60c060'});const q=project(ox,3,-7);if(q)text(q.x-5,q.y-1,'{what}','#fff2c0')}\nif(me){const s=cyc==0?(p<8?'Oasis {what} dead ahead. Water, finally!':p<13?'It is... shimmering. Bad sign.':'Mirage. Wrong match, keep grepping.'):(p<12?'Second lead on {what}. Fingers crossed.':'Real palms! Found where it lives.');say(s,me.top.x,me.row)}}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#ff9a4a","#ffcf70","#c06040"],"speed":0.4,"intensity":0}}},
37 {"phase":"reading","world":"circus","style":"3D","concept":"ufo abduction over a circus tent at night: Clawd's flying saucer beams up crates labeled {what} one by one to read them, 3D","source":"claude-opus-5-5","scene":{"concept":"ufo abduction over a circus tent at night: Clawd's flying saucer beams up crates labeled {what} one by one to read them, 3D","code":"const tent=cone(3,2.5,10),pole=cylinder(0.06,3.4,5),saucer=sphere(1,12),dome=sphere(0.5,10),crate=box(0.7,0.7,0.7),ground=plane(40,20);\nfunction frame(t,dt){camera(0,2.2,9,0,1.6,0,80);light(0.3,1,0.5,0.35);fog(8,35,'#000');\nfor(let i=0;i<40;i++)point3d(rand(i)*40-20,3+rand(i+7)*5,-12,'#aab');\nmesh3d(ground,{y:0,z:-3,color:'#4a6a3a'});\nmesh3d(tent,{x:-5,z:-3,color:'#e05a5a'});mesh3d(pole,{x:-5,y:2.4,z:-3,color:'#ddd'});\nmesh3d(tent,{x:6,z:-4,scale:0.7,color:'#e0c050'});\nconst sx=Math.sin(t*0.3)*1.5,sy=3.3+Math.sin(t*1.3)*0.1;\nconst cyc=t%6,k=Math.floor(t/6);\nconst lift=smoothstep(1,4.5,cyc);\nif(cyc>0.8&&cyc<5){for(let i=0;i<14;i++){const a=i/14*6.28,r=0.25+(1-(i%2))*0.4;line3d(sx,sy-0.2,0,sx+Math.cos(a)*1.1,0,Math.sin(a)*0.6,i%2?'#9fe8a0':'#cfffb0');}}\nconst cy=lerp(0.35,sy-0.5,lift);\nif(cyc<5)mesh3d(crate,{x:sx*lift,y:cy,z:0,ry:t*1.5*lift,color:'#d9a066'});\nfor(let j=0;j<3;j++)mesh3d(crate,{x:2.5+j*0.8,y:0.35,z:0.5,color:'#c08850'});\nmesh3d(saucer,{x:sx,y:sy,z:0,scale:1,color:'#b0c0d0'});\nmesh3d(dome,{x:sx,y:sy+0.5,z:0,color:'#90e0ff'});\nfor(let i=0;i<6;i++){const a=t*2+i;const p=project(sx+Math.cos(a)*1.6,sy-0.1,Math.sin(a)*0.5);if(p)put(p.x,p.y,'●',hsl((i*60+t*90)%360,0.9,0.6));}\nconst p=project(sx*lift,cy+0.6,0);if(p&&cyc<5)text(p.x-4,p.y,'{what}','#ffe9a0');\nconst c=project(sx,sy+0.6,0);if(c){clawd(c.x-7,c.y-3,{scale:1,arms:'up',look:{x:0,y:2}});}\nconst lines=['Beam it up. {what} goes in for study.','Probing {what}... fascinating logic.','Next specimen: more {what}!'];\nif(c&&cyc>1.5&&cyc<4.5)say(lines[k%3],c.x,c.y-3);}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#99a"],"speed":0.3,"intensity":0}}},
38 {"phase":"reading","world":"mail room","style":"text art","concept":"ufo abduction, text art: a saucer hovers over a moonlit field beaming crates labeled {what} up one by one to study them, while Clawd in an a","source":"claude-opus-5-5","scene":{"concept":"ufo abduction, text art: a saucer hovers over a moonlit field beaming crates labeled {what} up one by one to study them, while Clawd in an antenna headband steers it from the ground","code":"const cx=[8,30,52,74];function frame(t,dt){for(let i=0;i<40;i++)put(rand(i)*w,rand(i+7)*4,fract(t*0.3+rand(i+3))<0.1?'+':'.','#8899cc');for(let x=0;x<w;x++)put(x,8,x%5?'▒':'░','#3a6b3a');const P=8,ph=mod(t,P),i=Math.floor(t/P),j=i%4,pj=(i+3)%4;const ux=lerp(cx[pj],cx[j],smoothstep(0,2,ph))-2;for(let k=0;k<4;k++){if(k==j&&ph>3)continue;sprite(cx[k],6,'┌───┐\\n└───┘','#c9a26b');}if(ph>2&&ph<6.5){for(let y=3;y<8;y++)for(let x=-y+3;x<=y-3+4;x++)if(fract(x*0.37+y*0.21+t*2)<0.5)put(ux+4+x-2+1,y,'░','#7fffb0');}if(ph>3&&ph<6.2){const cy=6-smoothstep(3,6,ph)*3.5;sprite(cx[j],cy,'┌───┐\\n└───┘','#ffd27a');text(cx[j]-1,cy-1,'{what}','#ffffff');}else if(ph<=3)text(cx[j]-1,5,'{what}','#ffffff');sprite(ux,0,' .-===-. \\n(=o=o=o=)\\n \\'-----\\' ','#b0c4ff');for(let k=0;k<3;k++)put(ux+2+k*2,1,'o',fract(t*3+k/3)<0.5?'#ff6bd6':'#6bffe0');if(ph>6.2&&ph<8)text(ux+10,1,'scan '+'▮'.repeat(Math.floor((ph-6.2)*4)),'#7fffb0');const c=clawd(w-22,4,{facing:-1,arms:ph>2&&ph<6.5?'up':'down'});put(c.top.x-2,c.top.row-1,'╲','#9fff9f');put(c.top.x+2,c.top.row-1,'╱','#9fff9f');put(c.top.x-3,c.top.row-1,'●','#ff6bd6');put(c.top.x+3,c.top.row-1,'●','#ff6bd6');if(mod(t,24)>3&&mod(t,24)<9)say('Take me to your {what}. I want to read it.',c.top.x,c.row-1);else if(mod(t,24)>14&&mod(t,24)<20)say('Probing {what}. Painless, just a few functions.',c.top.x,c.row-1);}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#557","#99b"],"speed":0.4,"intensity":0}}},
39 {"phase":"reading","world":"medieval castle","style":"3D","concept":"construction site 3D: hard-hatted Clawd inspects a swinging crate labeled {what} as a crane hauls it past a steel skeleton, reading every be","source":"claude-opus-5-5","scene":{"concept":"construction site 3D: hard-hatted Clawd inspects a swinging crate labeled {what} as a crane hauls it past a steel skeleton, reading every beam","code":"const col=box(0.25,3,0.25),bm=box(3,0.2,0.25),cr=box(1,0.8,0.8),tw=box(0.35,3.6,0.35),jb=box(11,0.25,0.25),gr=plane(40,16);\nconst L=['Hoisting {what} up for a closer look.','Load-bearing function spotted. Do not remove.','Checking every rivet in {what}.','Ah, so THAT holds up the second floor.'];\nfunction frame(t,dt){camera(Math.sin(t*0.1)*1.5,2.4,9,0,1.4,0,90);fog(10,40,'#000');light(0.5,1,0.6,0.35);\nmesh3d(gr,{y:0,color:'#8a7a60'});\nfor(const x of [-9,-6,-3])mesh3d(col,{x,y:1.5,z:-1,color:'#d0a070'});\nfor(let f=1;f<4;f++)for(const x of [-7.5,-4.5]){if(f==3&&x==-4.5&&fract(t*0.05)<0.5)continue;mesh3d(bm,{x,y:f,z:-1,color:'#c08850'});}\nmesh3d(tw,{x:6,y:1.8,z:-1,color:'#f0c040'});mesh3d(jb,{x:3,y:3.6,z:-1,color:'#f0c040'});\nconst hx=3+Math.sin(t*0.3)*4.5,hy=2+Math.sin(t*0.7)*0.4;\nline3d(hx,3.5,-1,hx,hy+0.4,-1,'#ccc');mesh3d(cr,{x:hx,y:hy,z:-1,ry:Math.sin(t*0.9)*0.4,color:'#e09060'});\nconst p=project(hx,hy,-1);if(p)text(Math.round(p.x-5),p.y,'{what}','#fff8d0');\nconst cx=-1+Math.sin(t*0.3)*2.5;const c=clawd3d(cx,0,2,{facing:Math.cos(t*0.3)>0?1:-1,stride:Math.floor(t*5)%4,arms:fract(t*0.2)<0.5?'up':'down'});\nif(c){pixels(c.top.x-4,c.top.py-3,' yyyyyy \\nyyyyyyyy\\nYYYYYYYYYY',{y:'#ffd23f',Y:'#e0a800'});if(fract(t/8)<0.7)say(L[Math.floor(t/8)%4],c.top.x,c.row);}\n}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#778"],"speed":0.3,"intensity":0}}},
40 {"phase":"reading","world":"weather report","style":"3D","concept":"weather report: Clawd as TV weatherman pointing at a slowly orbiting 3D map where a cloud front labeled {what} rolls in, rains, then clears ","source":"claude-opus-5-5","scene":{"concept":"weather report: Clawd as TV weatherman pointing at a slowly orbiting 3D map where a cloud front labeled {what} rolls in, rains, then clears to sunshine","code":"const ground=plane(16,7),cl=sphere(0.7,10),sun=sphere(0.8,12),hill=cone(0.9,1.2,8),tw=cylinder(0.15,1.6,6);\nconst cs=Array.from({length:5},(_,i)=>({o:i*1.3,z:-1.5+rand(i)*3,s:0.8+rand(i+7)*0.6}));\nfunction frame(t,dt){const a=Math.sin(t*0.15)*0.4;camera(Math.sin(a)*10,4.5,Math.cos(a)*10,1,0,0,70);light(0.4,1,0.5,0.35);fog(8,30,'#000');\nfor(let x=0;x<w;x++)put(x,h-1,'▀','#335','#112');\nmesh3d(ground,{color:'#6a9'});mesh3d(hill,{x:-4,z:-1,color:'#9b7'});mesh3d(hill,{x:4.5,z:1,color:'#9b7',scale:0.8});mesh3d(tw,{x:1,z:0.5,color:'#ccd'});\nconst ph=mod(t,40),k=smoothstep(28,36,ph);const fx=lerp(-9,1.5,smoothstep(0,10,ph))+k*12;\nconst sx=5.5,sh=smoothstep(26,34,ph);mesh3d(sun,{x:sx,y:1.5+sh*1.5,z:-2,color:'#fd5',unlit:true});\nfor(const c of cs){const x=fx+c.o-2.6;mesh3d(cl,{x,y:2.2,z:c.z,scale:c.s,color:k>0.5?'#eef':'#aab'});\nif(ph>10&&ph<30)for(let j=0;j<3;j++){const yy=2-fract(t*1.5+j*0.33+c.o)*2;line3d(x+j*0.3-0.3,yy,c.z,x+j*0.3-0.4,yy-0.4,c.z,'#7af')}}\nconst p=project(fx,3.4,0);if(p)text(p.x-6,Math.max(0,p.y),'{what} front','#fff');\nfill(0,0,19,h-1,' ',null,'#1a2238');text(1,0,'CLAWD WX 7',ph>10&&ph<30?'#7af':'#fd5');\nconst arm=Math.sin(t*2)>0?'out':'up';const c=clawd(3,3,{facing:1,arms:{left:'down',right:arm}});\nconst msg=ph<10?'High-pressure {what} moving in. Reading it closely.':ph<20?'Heavy logic showers over {what}. Bring a debugger.':ph<30?'Scattered callbacks, 80% chance of insight.':'Clearing up! {what} finally makes sense.';\nif(mod(ph,10)>1)say(msg,c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#223355","#334477","#225566"],"speed":0.4,"intensity":0}}},
41 {"phase":"reading","world":"bowling alley","style":"3D","concept":"submarine: Clawd rides atop a sub sweeping sonar pings across the seabed, closing in on a sunken wreck stamped {what}","source":"claude-opus-5-5","scene":{"concept":"submarine: Clawd rides atop a sub sweeping sonar pings across the seabed, closing in on a sunken wreck stamped {what}","code":"const hull=cylinder(0.7,3,12),tw=box(0.7,0.7,0.9),rk=sphere(1,6),wr=box(4,1.2,1.6),ms=cylinder(0.08,2.6,5),fl=plane(80,80)\nconst rocks=Array.from({length:7},(_,i)=>({x:-12+i*4+rand(i)*2,z:-2-rand(i+9)*10,s:0.4+rand(i+5)*0.6}))\nconst lines=['Diving into {what}. Lights on.','Ping... something big down there.','Every function has a shipwreck story.','Ah, so THAT is where the data goes.','Mapping the hull, one call at a time.']\nfunction frame(t,dt){fill(0,0,w,h,' ',null,'#021626')\ncamera(Math.sin(t*0.1)*1.5,2.6,9,1,1,-2,90)\nfog(6,32,'#021626')\nlight(0.3,1,0.5,0.35)\nmesh3d(fl,{y:0,color:'#c9b48a'})\nfor(const r of rocks)mesh3d(rk,{x:r.x,y:0,z:r.z,scale:r.s,color:'#8a9a8a'})\nmesh3d(wr,{x:6,y:0.5,z:-4,ry:0.3,rz:0.12,color:'#b08a66'})\nmesh3d(ms,{x:6,y:1,z:-4,rz:0.45,color:'#9a7a5a'})\nconst sx=lerp(-9,1.5,smoothstep(0,25,t)),sy=2+Math.sin(t*0.8)*0.15\nmesh3d(hull,{x:sx+1.5,y:sy,z:0,rz:Math.PI/2,color:'#e8c84a'})\nmesh3d(tw,{x:sx,y:sy+0.9,z:0,color:'#d9b83a'})\nconst me=clawd3d(sx,sy+1.25,0,{size:0.6,facing:1,arms:fract(t*0.2)<0.3?'out':'down'})\nconst pr=fract(t*0.35)*12\nfor(let i=0;i<28;i++){const a=i/28*6.283;point3d(sx+1.6+Math.cos(a)*pr,sy,Math.sin(a)*pr,mix('#7fffd4','#021626',pr/12))}\nfor(let i=0;i<6;i++)point3d(sx-1.8-rand(i)*0.5,sy+fract(t*0.5+rand(i+3))*3,0,'#bfefff')\nconst hit=Math.abs(pr-5)<1.2\nline3d(sx+3,sy,0,6,0.8,-4,'#fff3a0')\nconst p=project(6,2,-4)\nif(p)text(p.x-4,p.y-1,'{what}',hit?'#ffffff':'#ffd27f')\nif(me&&t>2)say(lines[Math.floor((t-2)/9)%lines.length],me.top.x,me.row)}","actors":[],"particles":[],"background":{"effect":"waves","palette":["#0a3a5a","#0f5a7a","#1a7a9a"],"speed":0.5,"intensity":0}}},
42 {"phase":"reading","world":"fairy tale forest","style":"3D","concept":"race track pit stop: Clawd as pit chief reading the engine of a 3D race car labeled {what}, lap after lap of cars streaking past","source":"claude-opus-5-5","scene":{"concept":"race track pit stop: Clawd as pit chief reading the engine of a 3D race car labeled {what}, lap after lap of cars streaking past","code":"const car=box(3,0.6,1.4),cab=box(1.2,0.5,1),wh=cylinder(0.35,0.3,10),crate=box(0.8,0.8,0.8),post=cylinder(0.1,2.5,6);\nfunction frame(t,dt){camera(Math.sin(t*0.15)*2,2.6,9,0,0.8,0,80);light(0.4,1,0.6,0.35);fog(10,45,'#000');\nfor(let i=-6;i<=6;i++){line3d(i*4,0,-3,i*4+2,0,-3,'#ddd');}\nline3d(-30,0,-1.5,30,0,-1.5,'#888');line3d(-30,0,-8,30,0,-8,'#888');\nfor(let i=0;i<3;i++){const x=mod(t*30+i*25,90)-45;mesh3d(car,{x,y:0.4,z:-5,color:['#e55','#5ae','#ec4'][i]});mesh3d(cab,{x:x-0.3,y:0.9,z:-5,color:'#ddd'});}\nconst lift=0.3+0.15*smoothstep(1,2,t);mesh3d(car,{x:-2,y:lift,z:1,color:'#d43'});mesh3d(cab,{x:-2.4,y:lift+0.5,z:1,color:'#eee'});\nfor(const[a,b]of[[-3,0.3],[-1,0.3],[-3,1.7],[-1,1.7]])mesh3d(wh,{x:-2+a+2-1,y:lift-0.3,z:b,rx:1.57,ry:0,rz:0,color:'#666'});\nconst hood=Math.sin(Math.min(1,smoothstep(2,3,t))*1.2);mesh3d(box(1,0.06,1.2),{x:-0.8,y:lift+0.35+hood*0.4,z:1,rz:-hood,color:'#f86'});\nfor(let i=0;i<3;i++)mesh3d(crate,{x:5+i*1.1,y:0.4,z:0,color:'#c94'});mesh3d(post,{x:8,z:-1,color:'#aaa'});\nconst p=project(-2,lift+0.3,1.8);if(p)text(p.x-4,p.y,'{what}','#fff');\nconst cx=0.6+Math.sin(t*0.6)*0.3;const c=clawd3d(cx,0,1.8,{facing:-1,arms:fract(t*0.5)<0.5?'out':{left:'up',right:'down'},size:1});\nif(c){pixels(c.top.x-3,c.top.py-2,'.######.\\n########','#ccc'==0?{}:{'#':'#3af'});\nconst L=['Popping the hood on {what}...','Ooh, the gears mesh here.','Tracing that fuel line back.','Every lap I understand more.'];say(L[Math.floor(t/7)%4],c.top.x,c.row);}\nconst fl=Math.floor(t*4)%2;text(w-10,0,fl?'▓░▓░▓░':'░▓░▓░▓','#eee');}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#222","#333","#422"],"speed":0.5,"intensity":0}}},
43 {"phase":"reading","world":"fishing pond","style":"pixel art","concept":"bowling alley: Clawd bowls frame after frame down a lane named {what}, each strike knocking over pins and adding a mark to a growing scorebo","source":"claude-opus-5-5","scene":{"concept":"bowling alley: Clawd bowls frame after frame down a lane named {what}, each strike knocking over pins and adding a mark to a growing scoreboard of understood lines","code":"const P=12;let pins=[];for(let r=0;r<4;r++)for(let j=0;j<=r;j++)pins.push({r,j});\nconst ball=[' ## ','####','####',' ## '],pin=['.w.','www','.r.','www','www'];\nfunction frame(t,dt){const cy=Math.floor(t/P),ph=t%P,L=20,R=w-14;\nfor(let x=0;x<w;x++){put(x,8,'▀','#c8914a','#7a4f24');put(x,5,'─','#4a3a2a');}\nfor(let x=L;x<w;x+=8)put(x,7,'▲','#e8c070');\ntext(Math.floor(w/2)-12,0,'╔═ LANE: {what} ═╗','#9fd0ff');\nlet sc='';for(let i=0;i<10;i++)sc+=i<cy?'[X]':(i==cy&&ph>4.2?'[X]':'[ ]');text(Math.floor(w/2)-15,1,sc,'#ffd27a');\nconst roll=smoothstep(1.5,4.2,ph),bx=lerp(L,R-6,roll),hit=ph>4.2,set=ph>8;\nconst px=R,base=10;\nfor(const p of pins){let x=px+p.r*3,y=base-p.j*2-1+p.r;let dx=0,dy=0;if(hit&&!set){const k=Math.min(1,(ph-4.2)*2);dx=(p.r+1)*3*k*(rand(p.r*7+p.j+cy)-0.3);dy=-4*k*rand(p.j+p.r*3)+k*3}\nif(!set||ph>9.5)pixels(Math.round(x+dx),Math.round(y+dy),pin.join('\\n'),{w:'#f4f4f4',r:'#e04848'});}\nif(set){const d=ph<9.5?smoothstep(8,9,ph)*6:6-smoothstep(9.5,11,ph)*6;fill(R-2,0,12,Math.floor(d/2)+1,'▓','#6a6a7a');}\nif(ph>1.2&&!hit)pixels(Math.round(bx),11,ball.join('\\n'),{'#':hsl(220+cy*40,0.7,0.6)});\nconst wind=ph<1.5?smoothstep(0.3,1.5,ph):0;const c=clawd(4-wind*2+(ph<1.8&&ph>1.4?3:0),4,{pose:'stand',facing:1,arms:ph<1.5?{left:'down',right:wind>0.5?'down':'out'}:{left:'down',right:'out'},eyes:hit&&ph<5.5?'wide':'open'});\nif(ph<1.3)pixels(c.right.x,c.right.py-1,ball.join('\\n'),{'#':hsl(220+cy*40,0.7,0.6)});\nconst lines=['Lining up on {what}. Reading the lane first.','Strike! That function finally makes sense.','Pinsetter resets. Next block of {what}...','Another one down. The logic is clicking.'];\nif(ph>4.5&&ph<7.8)say(lines[cy%4],c.top.x,c.row);}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#2a1a3a","#3a2050","#1a2a4a"],"speed":0.5,"intensity":0.2}}},
44 {"phase":"editing","world":"zen rock garden","style":"3D","concept":"ant colony up close, 3D: a column of ants hauls code crumbs into a growing anthill labeled {what} while foreman Clawd directs the build","source":"claude-opus-5-5","scene":{"concept":"ant colony up close, 3D: a column of ants hauls code crumbs into a growing anthill labeled {what} while foreman Clawd directs the build","code":"const hill=cone(2.2,1.6,10),body=sphere(0.18,6),crumb=box(0.3,0.25,0.3),peb=sphere(0.4,6);\nconst ants=Array.from({length:9},(_,i)=>({o:i/9}));\nfunction frame(t,dt){camera(Math.sin(t*0.1)*1.5,1.4,7,0,0.6,-2,80);light(0.4,1,0.6,0.35);fog(8,35,'#000');\nmesh3d(plane(40,30),{z:-6,color:'#a08060'});\nfor(let i=0;i<7;i++)mesh3d(peb,{x:-9+i*3.2+rand(i)*1.5,y:0.2,z:-5-rand(i+5)*6,color:'#b0a898'});\nconst g=1+Math.min(t,90)*0.006;mesh3d(hill,{x:-3,z:-2,scale:g,color:'#c89a5a'});\nmesh3d(sphere(0.35,8),{x:-3,y:1.6*g,z:-1.8,color:'#3a2a1a',unlit:true});\nfor(const a of ants){const k=fract(a.o+t*0.07),x=lerp(9,-2.4,k),z=-1+Math.sin(k*9)*0.4,b=Math.abs(Math.sin(t*12+a.o*20))*0.05;\nmesh3d(body,{x,y:0.18+b,z,color:'#c0503a'});mesh3d(body,{x:x-0.3,y:0.2+b,z,scale:0.8,color:'#d0604a'});mesh3d(body,{x:x+0.32,y:0.18+b,z,scale:1.1,color:'#c0503a'});\nmesh3d(crumb,{x,y:0.5+b,z,ry:t+a.o*5,color:'#f0d070'});}\nconst p=project(-3,0.9*g,-1);if(p)text(p.x-4,p.y,'{what}','#fff4c0');\nconst c=clawd3d(1.5,0,0.8,{facing:1,arms:fract(t*0.5)<0.5?{left:'up',right:'out'}:'out',size:0.9});\nif(c){pixels(c.top.x-3,c.top.py-3,'.yyyyy.\\nyyyyyyy\\nooooooo','#'===''?{}:{y:'#ffd23a',o:'#e0a000'});\nif(t>2&&t<9)say('Every crumb a line of {what}. Hup, hup!',c.top.x,c.row);\nif(t>20&&t<28)say('Tunnel holds. Next ant, next edit.',c.top.x,c.row);\nif(t>45&&t<53)say('{what} is getting tall. Ants unionizing.',c.top.x,c.row);}}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#3a2a1a","#4a3520","#2a1e10"],"speed":0.5,"intensity":0}}},
45 {"phase":"editing","world":"cyberpunk street","style":"pixel art","concept":"race track pit stop: Clawd in a helmet as pit crew, swapping a worn tire on the {what} car before it screams back out, lap after lap","source":"claude-opus-5-5","scene":{"concept":"race track pit stop: Clawd in a helmet as pit crew, swapping a worn tire on the {what} car before it screams back out, lap after lap","code":"const car=['..........rrrrrrr...........','.........rrbbbbbrr........rr','rrrrrrrrrrrrrrrrrrrrrrrrrrrr','rryyyyyyyyyyyyyyyyyyyyyyyyrr','rrrrrrrrrrrrrrrrrrrrrrrrrrrr'];const pal={r:'#e04040',b:'#80c8ff',y:'#ffd040'};const tire=['.kkkk.','kkggkk','kkggkk','.kkkk.'];function frame(t,dt){const u=t%10,lap=Math.floor(t/10)+1;for(let x=0;x<w;x++){put(x,0,'▓',hsl((x*37)%360,0.5,0.55));put(x,1,x%2?'o':'∩',hsl((x*53+t*40)%360,0.6,0.6));put(x,2,'═','#888');put(x,8,(x+Math.floor(t*0))%8<4?'▀':' ','#ddd','#333')}fill(0,3,w,5,' ',null,'#1a1a22');for(let x=0;x<w;x+=12)text(x,3,'│ PIT │','#555');sprite(100,4,'┌──────┐\\n│LAP '+String(lap).padStart(2)+'│\\n└──┬───┘\\n │', '#ffd040');let cx=u<2?lerp(-40,60,smoothstep(0,2,u)):u<7.5?60:lerp(60,160,smoothstep(7.5,9,u));pixels(cx,10,car.join('\\n'),pal);text(cx+9,6,'{what}'.slice(0,16),'#201000','#ffd040');const fx=cx+21,rx=cx+1;pixels(fx,14,tire.join('\\n'),{k:'#222',g:'#aaa'});let ox=rx,oc='#777';if(u>=3&&u<7.5){ox=rx-(u-3)*14;oc='#666'}if(!(u>=4.5&&u<5.5))pixels(ox,14,tire.join('\\n'),{k:u>=3&&u<7.5?'#555':'#222',g:oc});if(u>=5.5&&u<7.5||u<3||u>=7.5)if(u>=5.5&&u<7.5)pixels(rx,14,tire.join('\\n'),{k:'#222',g:'#ff0'});const px=u<2?40:u<7.5?44:40;const c=clawd(px,4,{facing:1,arms:u>3&&u<6?(Math.floor(u*6)%2?'out':'down'):(u>6&&u<7.5?'up':'down'),stride:-1,blink:fract(t*0.3)<0.05});pixels(c.top.x-4,c.top.py-3,'..wwwww..\\n.wwrrrww.\\nwwwwwwwww',{w:'#f0f0f0',r:'#e04040'});if(u>3&&u<6)for(let i=0;i<3;i++)put(px+15+i,5+Math.floor(rand(i+Math.floor(t*10))*2),'*','#ffa040');if(u>6&&u<7.5)say(lap%2?'Fresh rubber on {what}. GO GO GO!':'Patched {what} in 4.2 seconds. New record!',c.top.x,c.row);if(u>7.5)for(let i=0;i<6;i++)put(cx-2-i*3,6+(i%2),'~','#999')}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#331111","#442222","#333333"],"speed":0.5,"intensity":0}}},
46 {"phase":"editing","world":"wild west","style":"pixel art","concept":"train station: Clawd as rail mechanic riding atop a stop-and-go freight line, welding each rusty {what} car green before it rolls out","source":"claude-opus-5-5","scene":{"concept":"train station: Clawd as rail mechanic riding atop a stop-and-go freight line, welding each rusty {what} car green before it rolls out","code":"const P=8,S=26,L=['Next car: {what}. Hold still, this will sting.','Rust out, patch in. {what} is good to go.','All aboard the {what} fix express!']\nfunction frame(t){const n=Math.floor(t/P),ph=t%P,m=smoothstep(5.5,8,ph);for(let x=0;x<w;x++){put(x,8,x%3?'═':'╪','#b8865a');}for(let k=0;k<5;k++){const lx=mod(k*31-t*0,w);put(lx,0,'●','#ffd060');put(lx,1,'│','#889');put(lx,2,'│','#889');put(lx,3,'│','#889');}text(2,1,'PLATFORM 9','#9cf');for(let i=n-2;i<=n+4;i++){const x=Math.round(w/2-12+(i-n)*S-S*m),f=i<n||(i==n&&ph>4.5),c=f?'#4fb87a':'#b5653d',d=f?'#2f7a50':'#7a3e22';fill(x,4,24,3,' ',null,c);fill(x,4,24,1,'▀',d,c);const lb='{what}';text(x+12-Math.floor(lb.length/2),5,lb,'#fff',c);text(x+2,7,'(●)', '#ccc');text(x+19,7,'(●)','#ccc');text(x+24,6,'══','#999');}const cx=w/2-7,wd=ph>1.5&&ph<4.5;const c=clawd(cx,0,{facing:0,arms:wd?(Math.floor(t*6)%2?'out':'down'):'down',eyes:wd?'closed':'open'});pixels(c.top.x-3,c.top.py-2,'.yyyyy.\\nyyyyyyy',{y:'#ffcc33'});if(wd){for(let s=0;s<6;s++)put(c.right.x+1+Math.random()*4,3+Math.random()*1.5,'*·✦'[s%3],s%2?'#ffee88':'#ff9933');put(c.right.x,c.right.row,'╲','#ccc');}if(ph>4.6&&ph<7.5)say(L[n%3],c.top.x,c.row);}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#1a2a4a","#2a3a6a","#3a2a5a"],"speed":0.4,"intensity":0}}},
47 {"phase":"editing","world":"race track pit stop","style":"3D","concept":"dinosaur era, 3D: a stegosaurus thunders into a stone-age pit stop and Clawd hot-swaps its cracked back plate (stamped {what}) for a fresh o","source":"claude-opus-5-5","scene":{"concept":"dinosaur era, 3D: a stegosaurus thunders into a stone-age pit stop and Clawd hot-swaps its cracked back plate (stamped {what}) for a fresh one before it roars off, lap after lap","code":"const B=box(4,1.4,1.6),L=cylinder(0.3,1.1,6),P=box(0.12,0.9,0.8),N=box(1.6,0.5,0.5),Hd=box(0.9,0.6,0.6),T=box(2.6,0.4,0.4),G=plane(70,30),V=cone(3,5,8),S=box(1.2,0.6,1.2);\nconst ss=(a,b,v)=>smoothstep(a,b,v);\nfunction frame(t,dt){camera(Math.sin(t*0.1)*1.5,3,9,0,1.3,0,80);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nmesh3d(G,{color:'#8a7650'});mesh3d(V,{x:-12,z:-20,color:'#b0654a'});mesh3d(V,{x:14,z:-24,scale:0.7,color:'#9a6a5a'});\nmesh3d(S,{x:-5,y:0.3,z:2,color:'#a9a9a0'});mesh3d(S,{x:5,y:0.3,z:2,color:'#a9a9a0'});\nconst c=t%12,lap=Math.floor(t/12);const dx=c<9?lerp(-26,0,ss(0,3,c)):lerp(0,26,ss(9,12,c));\nconst col='#7fa86a';mesh3d(B,{x:dx,y:1.7,color:col});\nfor(const a of [-1.3,1.3])for(const z of [-0.5,0.5])mesh3d(L,{x:dx+a,z,color:'#6a9258'});\nmesh3d(N,{x:dx+2.5,y:2.4,rz:0.6,color:col});mesh3d(Hd,{x:dx+3.3,y:3,color:'#8fb87a'});mesh3d(T,{x:dx-3.2,y:1.5,rz:0.3,color:col});\nfor(let i=0;i<5;i++){let y=3,cl='#d8b070',x=dx-1.6+i*0.8;if(i==2){if(c<4.5){y=3+ss(3,4.5,c)*5;x+=ss(3,4.5,c)*-4;cl='#d06a5a'}else{y=3+(1-ss(5,7,c))*5;cl='#9be07a'}}mesh3d(P,{x,y,color:cl});if(i==2){const p=project(x,y+0.8,0);if(p)text(p.x-3,p.y-1,'{what}',c<4.5?'#ffb0a0':'#d0ffc0')}}\nconst sw=c>2.8&&c<8;const me=clawd3d(0.6,0,2.4,{facing:sw?0:(dx<0?-1:1),arms:sw?'up':'down',eyes:c>7&&c<9?'wide':'open',size:1});\nif(me){if(c>3&&c<5)say('Old {what} plate: cracked. Swapping it!',me.top.x,me.row);else if(c>7&&c<9.5)say(lap%2?'Patched in 4 seconds. Pit record!':'Fresh plate, bolted on. GO GO GO!',me.top.x,me.row)}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#553","#664","#775"],"speed":0.3,"intensity":0}}},
48 {"phase":"editing","world":"bakery","style":"3D","concept":"circus big top in 3D: Clawd as tightrope walker crossing the high wire, dropping each edited block of {what} onto a growing acrobat stack in","source":"claude-opus-5-5","scene":{"concept":"circus big top in 3D: Clawd as tightrope walker crossing the high wire, dropping each edited block of {what} onto a growing acrobat stack in the ring","code":"const bx=box(0.9,0.6,0.9),pole=cylinder(0.1,3,6),ring=cylinder(5,0.2,24),sb=box(0.5,0.35,0.5);\nconst lines=['Steady... one edit to {what} on the wire.','Drop! Lands right on the stack.','No net. Just tests.','Hold my claws, adding another change.'];\nfunction frame(t,dt){camera(Math.sin(t*0.2)*2,2.2,10,0,1.4,0,90);light(0.4,1,0.6,0.3);fog(10,40,'#000');\nconst P=6,c=Math.floor(t/P),ph=fract(t/P),n=c%4;\nmesh3d(ring,{color:'#d04848'});mesh3d(pole,{x:-6,color:'#eeeeee'});mesh3d(pole,{x:6,color:'#eeeeee'});line3d(-6,2.6,0,6,2.6,0,'#ffdd88');\nfor(let i=0;i<n;i++)mesh3d(bx,{y:0.5+i*0.6,ry:i*0.3,color:hsl(40+i*90,0.7,0.6)});\nconst cx=lerp(-5.5,5.5,ph);\nconst me=clawd3d(cx,2.6,0,{facing:1,stride:Math.floor(t*6)%4,size:0.6,arms:ph<0.5?'up':'out'});\nconst tg=0.5+n*0.6,k=smoothstep(0.5,0.65,ph),bxx=ph<0.5?cx:0,by=ph<0.5?3.5:lerp(3.5,tg,k*k);\nif(n<3)mesh3d(sb,{x:bxx,y:by,ry:t,color:hsl(40+n*90,0.7,0.65)});\nconst p=project(bxx,by+0.4,0);if(p&&n<3)text(p.x-4,p.y,'{what}','#ffffcc');\nif(me&&ph>0.1&&ph<0.45)say(lines[c%4],me.top.x,me.row);}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#401020","#602030","#803040"],"speed":0.5,"intensity":0}}},
49 {"phase":"editing","world":"pirate ship","style":"3D","concept":"outer space pirate ship: Clawd as space-pirate shipwright welding new hull plates onto a 3D galleon drifting past a ringed planet","source":"claude-opus-5-5","scene":{"concept":"outer space pirate ship: Clawd as space-pirate shipwright welding new hull plates onto a 3D galleon drifting past a ringed planet","code":"const hull=box(6,1,1.6),mast=cylinder(0.08,2.4,6),sail=box(1.6,1.2,0.05),plate=box(0.7,0.5,0.1),pl=sphere(3,16),ring=cylinder(4.6,0.02,24);\nlet n=0;\nfunction frame(t,dt){\ncamera(Math.sin(t*0.1)*2,2.2,9,0,1,0,80);light(0.5,1,0.6,0.35);fog(10,50,'#000');\nfor(let i=0;i<70;i++)put(Math.floor(rand(i)*w),Math.floor(rand(i+9)*h),rand(i+3)<0.3?'✦':'.',rand(i+5)<0.5?'#99a':'#668');\nmesh3d(pl,{x:-10,y:1,z:-26,color:'#c98ad8',ascii:true,ry:t*0.2});\nmesh3d(ring,{x:-10,y:1,z:-26,rx:0.4,rz:0.2,color:'#e8c070',wire:true});\nconst by=Math.sin(t*0.8)*0.15;\nmesh3d(hull,{x:1,y:0.2+by,z:0,color:'#b08050'});\nfor(const mx of [-0.5,2])mesh3d(mast,{x:mx,y:0.7+by,z:0,color:'#ddc'});\nfor(const mx of [-0.5,2])mesh3d(sail,{x:mx,y:2+by,z:0.1,color:'#eee',ry:Math.sin(t)*0.1});\nn=Math.min(6,Math.floor(t/4));\nfor(let i=0;i<6;i++){const done=i<n;const k=done?1:(i==n?fract(t/4):0);if(i>n)continue;\nmesh3d(plate,{x:-1.5+i*0.9,y:0.25+by+(1-k)*2.5,z:0.85,color:done?'#9ad0ff':mix('#ffd060','#9ad0ff',k)});}\nconst p=project(1,1.4+by,0.9);if(p)text(p.x-5,p.y-3,'☠ {what}','#ffcc66');\nconst cx=-1.5+Math.min(n,5)*0.9;\nconst c=clawd3d(cx,1.3+by,1.4,{facing:0,arms:fract(t)<0.5?'up':'out',size:0.8});\nif(c){if(fract(t*2)<0.5){put(c.right.x+1,c.right.row,'*','#fff6a0');put(c.right.x+2,c.right.row+1,'·','#ffb040');}\npixels(c.top.x-3,c.top.py-3,'..###..\\n.#####.\\n#######','{\"#\":\"#222\"}');\nconst s=['Patching {what} plank by plank. Arr!','Hull breach sealed. Space is very drafty.','No bugs aboard. Well, fewer bugs.'];\nif(t>2)say(s[Math.floor(t/9)%3],c.top.x,c.row);}\n}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#99a"],"speed":0.4,"intensity":0}}},
50 {"phase":"editing","world":"ant colony up close","style":"3D","concept":"pirate ship in 3D: Clawd as ship's carpenter patching the hull mid-voyage, planks labeled {what} flying into place while the ship rolls","source":"claude-opus-5-5","scene":{"concept":"pirate ship in 3D: Clawd as ship's carpenter patching the hull mid-voyage, planks labeled {what} flying into place while the ship rolls","code":"const hull=box(9,1.4,3),deck=box(9.4,0.15,3.2),mast=cylinder(0.12,4.5,6),sail=box(3,2,0.08),plank=box(1.4,0.18,0.5),crate=box(0.8,0.8,0.8);\nfunction frame(t,dt){camera(0,2.6,9,0,1.2,0,80);light(0.4,1,0.6,0.35);fog(9,40,'#000');\nfor(let i=0;i<7;i++){const z=4-i*5;for(let x=-24;x<24;x+=2){const a=Math.sin(x*0.5+t*2+i)*0.15,b=Math.sin(x*0.5+1+t*2+i)*0.15;line3d(x,a,z,x+2,b,z,i%2?'#3a7ac0':'#5aa0e0')}}\nconst y=0.2+Math.sin(t*1.1)*0.15,r=Math.sin(t*1.1)*0.04;\nmesh3d(hull,{y:y+0.2,rz:r,color:'#a0683a'});mesh3d(deck,{y:y+0.95,rz:r,color:'#d8a060'});mesh3d(mast,{x:-1,y:y+1,color:'#c08850'});mesh3d(sail,{x:-1,y:y+3.4,ry:Math.sin(t*0.7)*0.2,color:'#f0e8d0'});\nfor(let i=0;i<3;i++)mesh3d(crate,{x:3+i*0.9,y:y+1.4,color:'#e0b070'});\nconst cyc=4,k=Math.floor(t/cyc),p=fract(t/cyc),n=Math.min(k,6);\nfor(let i=0;i<n;i++)mesh3d(plank,{x:-3.6+i*1.45,y:y+1.15,z:1.7,color:'#f0c080'});\nconst tx=-3.6+n*1.45,e=smoothstep(0,0.5,p),px=lerp(3,tx,e),py=y+1.4+Math.sin(Math.PI*e)*1.5;\nif(k<7||1){mesh3d(plank,{x:k<7?px:3,y:k<7?py:y+1.4,z:1.7,ry:(1-e)*3,color:'#ffd090'});const q=project(k<7?px:3,(k<7?py:y+1.4)+0.4,1.7);if(q)text(q.x-5,q.y,'{what}','#ffe8a0')}\nconst hit=p>0.55&&fract(t*3)<0.5;const c=clawd3d(Math.min(tx+1.2,4),y+1,1.5,{facing:-1,arms:hit?{left:'up',right:'down'}:'out',size:1});\nif(p>0.55&&fract(t*3)<0.2){const s=project(tx,y+1.3,1.7);if(s){put(s.x-1,s.y,'*','#ff0');put(s.x+1,s.y-1,'✦','#fc6')}}\nif(c&&p>0.6&&p<0.95)say(k%2?'Patching {what}. No leaks on my watch!':'Another plank, another fix. Hold steady!',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#99a"],"speed":0.3,"intensity":0}}},
51 {"phase":"testing","world":"volcano island","style":"pixel art","concept":"theater stage: opening night of {what}, Clawd as Hamlet in a roving spotlight while the curtain rises and falls and the audience waits for t","source":"claude-opus-5-5","scene":{"concept":"theater stage: opening night of {what}, Clawd as Hamlet in a roving spotlight while the curtain rises and falls and the audience waits for the verdict","code":"const P=14;function frame(t){const c=t%P,op=smoothstep(0,2.5,c)*(1-smoothstep(P-2,P,c));fill(0,0,w,h,' ',null,'#1c1024');fill(0,6,w,1,'▀','#a0682f','#4a2c12');for(let x=4;x<w;x+=10)put(x,6,'◆','#ffd860','#4a2c12');const cx=w/2-7+Math.sin(t*0.45)*18;for(let y=1;y<6;y++){const r=3+y*1.6;for(let x=Math.floor(cx+7-r);x<cx+7+r;x++)put(x,y,' ',null,mix('#1c1024','#5a4a30',0.4+y*0.08))}const a=clawd(cx,2,{facing:Math.cos(t*0.45)>0?1:-1,arms:{left:'down',right:c%4<2?'up':'out'},blink:fract(t*0.3)<0.04});pixels(a.top.x-3,a.top.py-3,'..yyyyy..\\n.yyyyyyy.\\nkkkkkkkkk','{\"y\":\"#2a2a40\",\"k\":\"#e05050\"}'.length?{y:'#6a5acd',k:'#ffd860'}:{});pixels(a.right.x,a.right.py-4,'.www.\\nwkwkw\\nwwwww\\n.w.w.',{w:'#eeeedd',k:'#333'});const hw=Math.floor(w/2*(1-op*0.88));fill(0,0,hw,6,'│','#d04a60','#8e1a30');fill(w-hw,0,hw,6,'│','#d04a60','#8e1a30');fill(0,0,w,1,'▼','#ffd27a','#6e1022');const lb='~ {what} ~';text(Math.floor(w/2-lb.length/2),0,lb,'#fff2b0','#6e1022');const clap=c>8&&c<11.5;for(let i=0;i<w;i+=5){const b=clap&&Math.sin(t*14+i)>0?1:0;put(i+2,7-b,'●','#7a6a9a');fill(i,8,5,1,'▀','#5a4a7a')}if(clap)for(let k=0;k<14;k++){const f=fract(t*0.8+rand(k));text(Math.floor(rand(k+3)*w),Math.floor(7-f*5),k%2?'♥':'✓',k%2?'#ff7090':'#7ae08a')}if(c>3&&c<7.5)say('To pass, or not to pass: {what} takes the stage.',a.top.x,a.row);else if(c>8.5&&c<11.5)say('Hold for applause... critics still reading the logs.',a.top.x,a.row)}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#2a1030","#3a1840","#201028"],"speed":0.5,"intensity":0}}},
52 {"phase":"testing","world":"train station","style":"3D","concept":"theater stage: a train-station play where Clawd, as stationmaster, waits on the platform while test trains roll on stage carrying pass/fail ","source":"claude-opus-5-5","scene":{"concept":"theater stage: a train-station play where Clawd, as stationmaster, waits on the platform while test trains roll on stage carrying pass/fail cars","code":"const fl=box(16,0.3,6),cur=box(2,4.5,0.3),val=box(18,0.8,0.3),bd=box(16,4,0.2),eng=box(2.2,1.1,1),car=box(2,0.9,1),lamp=cylinder(0.06,2.2,6)\nfunction frame(t){camera(0,2.2,8,0,1.1,0,80);light(0.3,1,0.7,0.35);fog(10,40,'#000')\nmesh3d(bd,{y:2,z:-3,color:'#7090b0'});mesh3d(fl,{y:-0.15,color:'#b08858'})\nline3d(-8,0.05,-1.6,8,0.05,-1.6,'#ccc');line3d(-8,0.05,-0.8,8,0.05,-0.8,'#ccc')\nmesh3d(lamp,{x:-4,z:0.8,color:'#999'});mesh3d(lamp,{x:4,z:0.8,color:'#999'})\nconst s=fract(t/8),r=floor(t/8),x=s<0.4?lerp(-15,0,smoothstep(0,0.4,s)):s<0.65?0:lerp(0,15,smoothstep(0.65,1,s)),dw=s>0.42&&s<0.65,bad=r%3==1\nmesh3d(eng,{x:x+2.4,y:0.65,z:-1.2,color:'#e0c060'})\nfor(let i=0;i<3;i++){mesh3d(car,{x:x-i*2.3,y:0.55,z:-1.2,color:'#90c8e8'});const p=project(x-i*2.3,1.4,-1.2);if(dw&&p)text(p.x,p.y,bad&&i==1?'✗':'✓',bad&&i==1?'#ff5050':'#60ff80')}\nconst q=project(x+2.4,1.5,-1.2);if(q)text(q.x-4,q.y-1,'{what}','#fff')\nmesh3d(cur,{x:-7.5,y:0,z:1.2,color:'#c03040'});mesh3d(cur,{x:7.5,y:0,z:1.2,color:'#c03040'});mesh3d(val,{y:3.6,z:1.2,color:'#c03040'})\nconst c=clawd3d(0,0,2.2,{facing:0,arms:dw?'up':'down',eyes:dw&&bad?'wide':'open',size:0.8})\nif(c){const ln=s<0.4?'Now arriving: {what}. Mind the gap.':dw?(bad?'One car derailed! Hold the curtain!':'All cars green. Applause, please!'):'Next train in eight seconds...';say(ln,c.top.x,c.row)}}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#302030","#402838","#203040"],"speed":0.5,"intensity":0}}},
53 {"phase":"testing","world":"mad science lab","style":"pixel art","concept":"mad science lab after hours in a museum: Clawd in goggles throws the lever, lightning zaps a jar labeled {what}, tests bubble green one by o","source":"claude-opus-5-5","scene":{"concept":"mad science lab after hours in a museum: Clawd in goggles throws the lever, lightning zaps a jar labeled {what}, tests bubble green one by one","code":"const N=8;let res=Array.from({length:N},(_,i)=>rand(i*7)<0.85);\nfunction frame(t,dt){\nfill(0,8,w,1,'▀','#5a4a6a');\nfor(let x=0;x<w;x+=30){pixels(x+2,0,'##.##\\n#....#\\n#....#\\n######','#'==='#'?{'#':'#8a6a3a'}:{});}\nconst cyc=mod(t,12),z=cyc>2&&cyc<2.6;\nconst jx=60;pixels(jx,2,'..####..\\n.#....#.\\n#......#\\n#......#\\n#......#\\n#......#\\n.######.\\n..#..#..',{'#':'#9ab'});\nconst lv=Math.floor(clamp((cyc-2)*3,0,N));\nfor(let i=0;i<N;i++){const y=13-Math.floor(i*1.1);const k=i<lv;pixel(jx+3+(i%2),y,k?(res[i]?'#4f4':'#f44'):'#335');}\ntext(jx-1,8,'{what}'.slice(0,12),'#ffd');\nline(jx+4,0,jx+4,0,'┬','#aaa');\nif(z){for(let i=0;i<5;i++)put(jx+2+Math.floor(rand(t*50+i)*6),Math.floor(rand(t*30+i)*1.5),'ϟ'.length?'⚡'.length==1?'*':'*':'*','#ff6');for(let y=0;y<2;y++)put(jx+4+(y%2?1:-1),y,'/','#ffef55');}\nconst lever=cyc>1.6&&cyc<6;\nconst lx=40;text(lx,4,'┌──┐','#888');text(lx,5,'│'+(lever?'▼ ':'▲ ')+'│','#ccc');text(lx,6,'└──┘','#888');\nconst c=clawd(24,4,{facing:1,arms:lever?{right:'down',left:'up'}:{right:'up',left:'down'},blink:fract(t*0.3)<0.04});\npixels(c.eyes.x-1,c.eyes.py,'gggggggggg',{g:'#4cf'});\nconst g=lv>=N&&cyc>5;if(g)for(let i=0;i<10;i++)put(jx+1+Math.floor(rand(i+Math.floor(t*4))*8),Math.floor(rand(i+3+Math.floor(t*4))*2),'○','#7f7');\nconst fails=res.filter(r=>!r).length;\nif(cyc>5.5&&cyc<11)say(fails?'IT LIVES! mostly. '+fails+' tests twitching in {what}.':'IT LIVES! {what} passed every jolt!',c.top.x,c.row);\nif(cyc>11.8)res=res.map((r,i)=>r||rand(t+i)<0.6);\nconst gx=w-20+Math.sin(t*0.3)*8;pixels(gx,10,'.##.\\n####\\n#..#',{'#':'#ddc'});pixel(gx+1,11,'#000');\n}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#201030","#302048","#183028"],"speed":0.5,"intensity":0.2}}},
54 {"phase":"testing","world":"sports broadcast","style":"text art","concept":"sports broadcast x mountain climb: live TV coverage of Clawd scaling the {what} north face, checkpoint flags for passing tests, a slip at th","source":"claude-opus-5-5","scene":{"concept":"sports broadcast x mountain climb: live TV coverage of Clawd scaling the {what} north face, checkpoint flags for passing tests, a slip at the crux, summit flag","code":"const P=38;function sy(x){return x<20?8:x<=95?8-(x-20)*4/75:4+(x-95)*4/23}\nfunction pr(tt){if(tt<14)return tt/14*0.6;if(tt<17)return 0.6-smoothstep(14,15.5,tt)*0.15;if(tt<30)return 0.45+(tt-17)/13*0.55;return 1}\nfunction frame(t,dt){const tt=t%P,p=pr(tt);\nfor(let x=20;x<119;x++){const top=Math.ceil(sy(x));for(let y=top;y<9;y++){const sn=y<6&&x>78&&x<108;put(x,y,sn?'▒':(x+y)%5?'▓':'░',sn?'#e8f0ff':'#7d8590')}}\nsprite(92,0,' ▲ \\n ▟█▙ \\n▟███▙','#e8f0ff');\nfor(const c of [0.25,0.5,0.75]){const fx=20+75*c,ok=p>c+0.03;put(fx,Math.ceil(sy(fx))-1,ok?'▶':'▷',ok?'#5fd75f':'#888')}\nconst cx=20+75*p-7,stride=Math.floor(cx/1.5)%4,mv=tt<30&&!(tt>16&&tt<17);\nconst slip=tt>=14&&tt<16;\nconst c=clawd(cx,sy(cx+7)-4,{pose:tt>=30?'jump':'walk',stride:mv?stride:-1,facing:slip?-1:1,eyes:slip?'wide':'open',arms:tt>=30?'up':'out'});\nsprite(c.top.x-2,c.top.row,'▄█▄','#ffd23f');\nline(c.right.x,c.right.row,c.right.x+3,c.right.row+1,'─','#c8a060');\nif(tt>=30){sprite(97,0,'│▀▀\\n│ ','#ff5f5f');for(let i=0;i<14;i++)put(96+Math.cos(i*2.3)*fract(tt)*14,2+Math.sin(i*2.3)*fract(tt)*3,'✦',hsl(i*40,0.9,0.65))}\nfill(0,0,19,3,' ',null,'#1a1a3a');text(1,0,'● LIVE',fract(t)<0.6?'#ff4040':'#802020');text(1,1,'{what}','#ffffff');text(1,2,'ALT '+String(Math.round(800+p*4000)).padStart(4)+'m','#ffd23f');\nsprite(1,5,'┌──────────┐\\n│ ◉ ◉ ┬ │\\n└──────────┘','#6fa8dc');text(3,8,'COMMENTARY','#6fa8dc');\nlet s='';if(tt>1&&tt<6)s='Live from {what}: base camp to summit!';else if(tt>8&&tt<13)s='First checkpoints green. Ropes holding.';else if(tt>=14&&tt<19)s='Whoa! One test slipped. Re-roping!';else if(tt>21&&tt<27)s='Retry looks clean. Air is thin up here.';else if(tt>=30&&tt<37)s='SUMMIT! Every test planted a flag!';\nif(s)say(s,c.top.x,c.row)}","actors":[],"particles":[{"glyphs":"·*","count":20,"x":"rand(k)*w","y":"mod(t*(1+rand(k+9)) + rand(k+3)*h, h)","color":"#c0d0e8"}],"background":{"effect":"aurora","palette":["#203060","#305080","#406090"],"speed":0.4,"intensity":0.2}}},
55 {"phase":"testing","world":"arctic expedition","style":"pixel art","concept":"snow globe arctic: tiny Clawd in a parka inside a snow globe, waiting by a flag labeled {what} while snow settles; each settled flake is a p","source":"claude-opus-5-5","scene":{"concept":"snow globe arctic: tiny Clawd in a parka inside a snow globe, waiting by a flag labeled {what} while snow settles; each settled flake is a passing test","code":"let fl=Array.from({length:50},(_,i)=>({x:rand(i)*44,y:rand(i+7)*16,v:1+rand(i+3)*2}));let piled=0;\nfunction frame(t,dt){const cx=Math.floor(w/2),R=22;\nfor(let py=0;py<18;py++)for(let x=cx-R;x<=cx+R;x++){const dx=(x-cx)/R,dy=(py-9)/9;const d=dx*dx+dy*dy;if(d<1&&d>0.85)pixel(x,py,'#9fd8ff');}\nfor(let x=cx-18;x<=cx+18;x++){const hh=Math.min(3,1+Math.floor(piled/40+noise(x*0.3)*1.5));for(let k=0;k<hh;k++)pixel(x,15-k,'#eef6ff');}\npixels(cx-16,16,'..................................\\n.##################################.','{\"#\":\"#8a5a3a\"}');\nfill(cx-17,8,35,1,'▀','#6b3f22');\nconst sh=t%14<1.2;const ox=sh?Math.round(Math.sin(t*40)*2):0;\nfor(const f of fl){f.y+=f.v*dt*(sh?-6:1);f.x+=Math.sin(t+f.y)*dt*2;if(f.y>14){f.y=0;f.x=rand(t*9+f.x)*44;piled++;}if(f.y<0)f.y=14;const px=cx-21+f.x+ox,dx=(px-cx)/R,dy=(f.y-9)/9;if(dx*dx+dy*dy<0.8)pixel(Math.round(px),Math.round(f.y),'#ffffff');}\npixels(cx+8+ox,4,'#....\\n#ooo.\\n#ooo.\\n#....\\n#\\n#\\n#\\n#\\n#','{\"#\":\"#c0c0c0\",\"o\":\"#e05050\"}');text(cx+10+ox,1,'{what}','#ffd27a');\nconst c=clawd(cx-9+ox,3.5,{pose:sh?'jump':'stand',facing:sh?0:1,eyes:sh?'wide':'open',color:'#d97757',arms:sh?'up':'down',blink:fract(t*0.3)<0.04});\npixels(c.top.x-4,c.top.py-2,'..wwwwww..\\n.wbbbbbbw.\\nwbbbbbbbbw','{\"w\":\"#ffffff\",\"b\":\"#4a7fd0\"}');\nconst lines=['Snug in here. Tests running on {what}.','Whoa! Who shook the globe?!','Each flake that lands is a passing test.','Snowdrift rising. Good sign.'];const i=sh?1:[0,2,3][Math.floor(t/5)%3];if(t%5>1.5||sh)say(lines[i],c.top.x+20,c.row);\ntext(cx-R-14,7,'❄ expedition','#7ab8e0');for(let x=0;x<w;x++)if(Math.abs(x-cx)>R+1)put(x,8,'▁','#3a5a7a');}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#1a4a6a","#2a8a7a","#5a6ad0"],"speed":0.4,"intensity":0.3}}},
56 {"phase":"testing","world":"theater stage","style":"3D","concept":"arctic expedition on a theater stage: Clawd in a parka waits by an ice-fishing hole while test flags pop up green across a 3D snowfield unde","source":"claude-opus-5-5","scene":{"concept":"arctic expedition on a theater stage: Clawd in a parka waits by an ice-fishing hole while test flags pop up green across a 3D snowfield under spotlights","code":"const ice=plane(40,14),flag=cylinder(0.05,1.4,5),ban=box(0.6,0.35,0.05),berg=cone(1.6,2.4,6),hole=cylinder(0.7,0.05,12),cur=box(1.5,4,0.3);\nconst N=8;\nfunction frame(t,dt){\nconst a=Math.sin(t*0.1)*0.4;\ncamera(Math.sin(a)*9,2.6,Math.cos(a)*9,0,0.8,0,90);\nlight(0.3,1,0.5,0.35);fog(10,40,'#05080f');\nmesh3d(ice,{color:'#cfe6f5'});\nmesh3d(berg,{x:-7,z:-5,color:'#a8d4ef'});mesh3d(berg,{x:7.5,z:-6,scale:1.3,color:'#b8ddf2'});\nfor(let i=0;i<6;i++){mesh3d(cur,{x:-10+i*0.4,y:2,z:-7+i,color:'#b03040'});mesh3d(cur,{x:10-i*0.4,y:2,z:-7+i,color:'#b03040'});}\nmesh3d(hole,{x:1.5,z:1,color:'#1a3a6a',unlit:true});\nconst cyc=t%20;\nfor(let i=0;i<N;i++){const fx=-5+i*1.4,fz=-2.5;const up=cyc>1+i*2;if(!up)continue;\nconst bad=rand(i+Math.floor(t/20)*9)<0.12;\nmesh3d(flag,{x:fx,z:fz,color:'#ddd'});mesh3d(ban,{x:fx+0.3,y:1.25,z:fz,color:bad?'#e05050':'#50d070'});}\nconst bob=Math.sin(t*3)*0.05;\nline3d(1.2,1.1,1,1.5,0.05+bob,1,'#ffffff');\nconst c=clawd3d(0,0,1,{facing:0,arms:{left:'down',right:'out'},blink:fract(t*0.3)<0.04,color:'#d97757'});\nif(c){pixels(c.top.x-4,c.top.py-2,'..wwwwwwww..\\nwwwwwwwwwwww','{\"w\":\"#f4f4f4\"}'.length?{w:'#f4f4f4'}:{});}\nconst p=project(-5,0.2,-2.5);if(p)text(p.x-1,p.y,'{what}','#ffe9a0');\nfor(let i=0;i<30;i++){put(mod(rand(i)*w+t*2,w),mod(rand(i+5)*h+t*(1+rand(i+3)),h),'*','#eef6ff');}\nif(c){if(cyc>3&&cyc<8)say('Ice fishing for passing tests in {what}.',c.top.x,c.row);else if(cyc>12&&cyc<17)say('Green flags so far. Toes: frozen.',c.top.x,c.row);}\n}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#40e0a0","#3080ff","#a060ff"],"speed":0.5,"intensity":0.3}}},
57 {"phase":"testing","world":"bakery","style":"pixel art","concept":"construction site, pixel art: Clawd in a hard hat directs a tower crane stacking test blocks for {what}; every third block comes up red and ","source":"claude-opus-5-5","scene":{"concept":"construction site, pixel art: Clawd in a hard hat directs a tower crane stacking test blocks for {what}; every third block comes up red and gets fixed","code":"function frame(t,dt){const H=2*h;for(let x=0;x<w;x++){pixel(x,H-1,'#7a5a3a');pixel(x,H-2,'#9a7a4a')}for(let y=4;y<H-2;y++){pixel(20,y,'#f0c020');pixel(23,y,'#f0c020');if(y%2)pixel(21+(y%4>1?1:0),y,'#c09010')}for(let x=12;x<72;x++){pixel(x,3,'#f0c020');if(x%3==0)pixel(x,4,'#c09010')}fill(12,1,6,1,' ',null,'#888');pixels(19,0,'######\\n#oooo#','{\"#\":\"#f0c020\",\"o\":\"#6cf\"}');const c=Math.floor(t/6),p=fract(t/6),n=c%5,red=c%3==2;for(let i=0;i<n;i++){fill(56,h-2-i,10,1,'▒','#4c8', '#2a6')}text(57,h-2-Math.max(0,n-1)-(n?1:0),'','#fff');for(let i=0;i<3;i++)fill(26+i*2,h-2,9,1,'▓','#a76','#643');let bx,by;const ty=h-2-n;if(p<0.15){bx=28;by=lerp(h-3,2,p/0.15)}else if(p<0.55){bx=lerp(28,56,smoothstep(0.15,0.55,p));by=2}else if(p<0.8){bx=56;by=lerp(2,ty,smoothstep(0.55,0.8,p))}else{bx=56;by=ty}const fix=red&&p>0.85;const col=red&&!fix?'#e44':'#4c8';const sh=red&&p>0.8&&!fix?Math.round(Math.sin(t*30)):0;line(bx+5,2,bx+5,by-1,'│','#ccc');fill(bx+sh,by,10,1,'▒',col,red&&!fix?'#822':'#2a6');text(bx+1+sh,by,'{what}'.slice(0,8),'#fff');const cx=p>0.8&&red?lerp(84,68,smoothstep(0.8,0.85,p)):84;const k=clawd(cx,h-5.5,{facing:-1,arms:p<0.8?{left:'up',right:'out'}:'up',stride:Math.floor(cx/1.5)%4});pixels(k.top.x-4,k.top.py-2,'..####..\\n########','{\"#\":\"#fd2\"}');if(fix)put(k.left.x-1,k.left.row,'✓','#4f8');const L=['Easy now... {what} going up.','Red block! One test failing.','Patched. Tower grows!','Floor by floor, test by test.'];const s=red?(fix?L[2]:p>0.75?L[1]:''):(p>0.3&&p<0.7?L[c%2?3:0]:'');if(s)say(s,k.top.x,k.row);fill(96,h-3,18,2,'▓','#555','#333');text(98,h-3,'SITE: TESTS','#fd2');for(let i=0;i<9;i++)put(96+i*2,h-1,i%2?'▲':'▲',i%2?'#f80':'#fff')}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#203048","#304860","#405870"],"speed":0.3,"intensity":0}}},
58 {"phase":"testing","world":"ant colony up close","style":"pixel art","concept":"lighthouse in a storm: keeper Clawd at the rocks while the sweeping beam guides test ships past the reef, each one lit up and checked as it ","source":"claude-opus-5-5","scene":{"concept":"lighthouse in a storm: keeper Clawd at the rocks while the sweeping beam guides test ships past the reef, each one lit up and checked as it passes","code":"let sh=Array.from({length:4},(_,i)=>({x:-12-i*24,ok:0}));let n=0;\nconst LH='..rr..\\n.yyyy.\\n.rrrr.\\nwwwwww\\nrrrrrr\\nwwwwww\\nrrrrrr\\nwwwwww\\nrrrrrr\\nwwwwww\\nrrrrrr\\nkkkkkkk';\nconst B='...m....\\n..sm....\\n.ssm....\\nhhhhhhhh\\n.hhhhhh.';\nfunction frame(t,dt){const fl=fract(t*0.13)<0.03;\nif(fl)fill(0,0,w,h,' ',null,'#2a2a44');\nfor(let x=0;x<w;x++){const y=7+Math.sin(x*0.3+t*2)*0.6;put(x,Math.round(y),'~','#4a7fc0');put(x,8,'≈','#2e5a99');}\nfor(let x=66;x<104;x++)put(x,8,'▓','#6b6560');fill(84,7,20,1,'▓','#6b6560');\nconst lx=w>110?92:w-24;pixels(lx,2,LH,{r:'#e04848',w:'#eeeeee',y:'#ffe066',k:'#555555'});\nconst a=Math.sin(t*0.9)*0.5+Math.PI;const bx=lx+3,by=1.6;\nfor(let r=2;r<90;r++){const px=bx+Math.cos(a)*r,py=by-Math.sin(a)*r*0.12+r*0.035;put(px,py,r%2?'░':'▒','#ffe98a');}\nconst beamX=bx+Math.cos(a)*60;\nfor(const s of sh){s.x+=dt*5;if(s.x>66){s.x-=96;s.ok=0;}if(Math.abs(s.x+4-beamX)<4&&!s.ok){s.ok=1;n++;}\nconst bob=Math.sin(t*2+s.x)*1;pixels(s.x,8+Math.round(bob),B,{m:'#a07040',s:'#f0f0f0',h:s.ok?'#7ad67a':'#c08850'});\nif(s.ok)text(s.x+3,3+Math.round(bob/2),'✓','#7ad67a');}\ntext(Math.max(0,sh.reduce((m,s)=>s.x>m.x?s:m).x-4),2,'{what}','#ffffff');\nconst c=clawd(70,3.5,{facing:-1,arms:{left:'up',right:'down'},blink:fract(t*0.3)<0.05});\npixels(c.top.x-3,c.top.py-2,'yyyyyy\\n.yyyy.',{y:'#ffd23f'});\npixels(c.left.x-1,c.left.py-2,'.k.\\nkok\\n.k.',{k:'#888',o:'#ffcc33'});\nconst q=['Steady... {what} sailing in.','Lit '+n+' ships so far. No wrecks!','Rough seas, green hulls.'];if(fract(t/24)<0.6)say(q[Math.floor(t/8)%3],c.top.x,c.row);}","actors":[],"particles":[{"glyphs":"/'","count":70,"x":"mod(rand(k)*w - t*8, w)","y":"mod(t*(9+rand(k+9)*5)+rand(k+3)*h, h)","color":"#7f9fcf"}],"background":{"effect":"rain","palette":["#1b2a44","#2c3e66","#41588a"],"speed":1.2,"intensity":0.25}}},
59 {"phase":"building","world":"mail room","style":"3D","concept":"zen rock garden: Clawd rakes sand rings around stones as packages of {what} settle into place, mail-room style, one by one onto stone pedest","source":"claude-opus-5-5","scene":{"concept":"zen rock garden: Clawd rakes sand rings around stones as packages of {what} settle into place, mail-room style, one by one onto stone pedestals","code":"const stone=sphere(0.7,10),ped=cylinder(0.9,0.3,10),pkg=box(0.8,0.6,0.8),gnd=plane(40,20);\nconst N=5,xs=[-7,-3.5,0,3.5,7];\nfunction frame(t,dt){\ncamera(Math.sin(t*0.05)*2,3,9,0,0.6,0,80);light(0.4,1,0.6,0.35);fog(8,35,'#000');\nmesh3d(gnd,{color:'#c8bfa0'});\nfor(let r=1;r<5;r++)for(let i=0;i<N;i++){const a=t*0.3;for(let k=0;k<16;k++){const q=k/16*6.283;point3d(xs[i]+Math.cos(q)*r*0.5,0.02,-2+Math.sin(q)*r*0.4,'#a89e80')}}\nconst cyc=4,done=Math.floor(t/cyc),ph=fract(t/cyc);\nfor(let i=0;i<N;i++){mesh3d(ped,{x:xs[i],z:-2,color:'#8c8c84'});\nconst slot=i<=done%(N+1)-1;\nif(slot)mesh3d(pkg,{x:xs[i],y:0.6,z:-2,color:'#d8a060'});\nelse if(i==done%(N+1)){const y=lerp(5,0.6,smoothstep(0,0.8,ph));mesh3d(pkg,{x:xs[i],y,z:-2,ry:(1-ph)*3,color:'#e8b070'});const p=project(xs[i],y+0.7,-2);if(p)text(p.x-4,p.y,'{what}'.slice(0,10),'#ffe')}\nelse mesh3d(stone,{x:xs[i],y:0.5,z:-2,scale:0.6,color:'#9a9a90'})}\nconst cx=Math.sin(t*0.4)*6;\nconst c=clawd3d(cx,0,1.5,{facing:Math.cos(t*0.4)>0?1:-1,stride:Math.floor(t*4)%4,arms:'down'});\nline3d(cx,0.4,1.5,cx-0.6,0,2.2,'#b08850');\nif(c){const m=Math.floor(t/12)%3;say(['Raking {what}. Patience is a dependency.','One package settles. Then another.','The build is complete when it is complete.'][m],c.top.x,c.row)}\n}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#2a4a3a","#3a5a4a","#4a6a5a"],"speed":0.3,"intensity":0}}},
60 {"phase":"building","world":"arctic expedition","style":"3D","concept":"arctic submarine: Clawd's sub surfaces through ice, loads {what} crates from a crane onto deck, dives and resurfaces in a loop","source":"claude-opus-5-5","scene":{"concept":"arctic submarine: Clawd's sub surfaces through ice, loads {what} crates from a crane onto deck, dives and resurfaces in a loop","code":"const hull=sphere(1,12),tow=box(0.8,0.9,1.2),crate=box(0.6,0.6,0.6),ice=box(3,0.3,2.5),berg=cone(1.6,2.5,6);let n=0;function frame(t,dt){camera(0,2.6,9,0,0.8,0,90);light(0.4,1,0.6,0.35);fog(10,40,'#001018');const c=t%16,u=smoothstep(0,3,c)*(1-smoothstep(13,16,c)),sy=lerp(-2.2,0,u);if(c<0.05)n=0;for(let i=0;i<7;i++){const x=-12+i*4+(i%2)*0.7;if(Math.abs(x)>2.5)mesh3d(ice,{x,y:-0.15,z:-1-(i%3),color:'#cfe8f5'})}mesh3d(berg,{x:-8,z:-9,color:'#b8dcef'});mesh3d(berg,{x:9,z:-12,scale:1.4,color:'#a9d0e8'});for(let x=-14;x<=14;x+=2)line3d(x,0,3,x+1,0,3,'#3a7ab0');mesh3d(hull,{y:sy,rz:0,scale:1,color:'#e8b04a',rx:0,ry:0,});mesh3d(hull,{x:-1.3,y:sy,scale:0.8,color:'#e8b04a'});mesh3d(hull,{x:1.3,y:sy,scale:0.8,color:'#e8b04a'});mesh3d(tow,{x:0,y:sy+1.1,color:'#d09a3a'});const me=clawd3d(0,sy+1.55,0,{size:0.6,arms:c>5&&c<11?'up':'down'});const k=c-4,ph=fract(k/2);if(c>4&&c<12){const cx=lerp(5,0.9,smoothstep(0,0.6,ph)),cy=lerp(3,sy+0.9,smoothstep(0.4,0.9,ph));line3d(5,3.5,0,cx,3.5,0,'#999');line3d(cx,3.5,0,cx,cy+0.3,0,'#bbb');mesh3d(crate,{x:cx,y:cy,color:'#c0703a'});const p=project(cx,cy+0.5,0);if(p)text(p.x-4,p.y,'{what}','#fff2c0');if(ph>0.9&&n<Math.floor(k/2)+1)n++}for(let i=0;i<Math.min(n,4);i++)mesh3d(crate,{x:-1.8+i*0.5,y:sy+0.95,z:0.3,color:'#c0703a'});line3d(5,0,0,5,3.6,0,'#ccc');if(me&&c>4&&c<12)say(n<2?'Loading {what} aboard. Ice fishing for deps!':'Hold full soon. Then we dive!',me.top.x,me.row);else if(me&&c>12)say('Diving! Back up with the next batch.',me.top.x,me.row)}","actors":[],"particles":[{"glyphs":"*·.","count":40,"x":"rand(k)*w","y":"mod(t*(1+rand(k+5))+rand(k+2)*h,h)","color":"#e8f4ff"}],"background":{"effect":"aurora","palette":["#1a6b5a","#2fa37a","#4a7bd0"],"speed":0.4,"intensity":0.3}}},
61 {"phase":"building","world":"orchestra concert","style":"pixel art","concept":"bank heist: masked Clawd cracks a spinning vault dial while gold bars stamped {what} fly into a growing loot pile, ducking a guard's sweepin","source":"claude-opus-5-5","scene":{"concept":"bank heist: masked Clawd cracks a spinning vault dial while gold bars stamped {what} fly into a growing loot pile, ducking a guard's sweeping flashlight","code":"function frame(t,dt){fill(0,0,w,h,' ',null,'#15151f');for(let y=0;y<8;y++)for(let x=(y%2)*4;x<w;x+=8)put(x,y,'│','#2a2a3a');fill(0,8,w,1,'▀','#5a5a6a');const vx=w-22;disc(vx,4,4,' ',null,'#5c6274');circle(vx,4,4,'●','#a0a8b8');const a=t*0.7+Math.sin(t*3)*0.2;for(let i=0;i<3;i++){const b=a+i*2.094;line(vx,4,vx+Math.cos(b)*7,4+Math.sin(b)*3.4,'▪','#dde')}put(vx,4,'◆','#fc3');fill(vx+10,0,2,8,'█','#3a3f4c');const per=2.5,k=Math.floor(t/per)%20,p=fract(t/per);for(let i=0;i<k;i++)pixels(6+(i%5)*5,15-Math.floor(i/5)*2,'yyyy',{y:i%2?'#fc3':'#e8b020'});text(7,1,'{what}','#ffcc66');text(7,2,'$$ LOOT $$','#8a7a3a');const tx=6+(k%5)*5,ty=15-Math.floor(k/5)*2;const bx=lerp(vx-6,tx,p),by=lerp(10,ty,p)-Math.sin(p*Math.PI)*10;pixels(bx,by,'yyyy',{y:'#ffe066'});const gm=mod(t,14),guard=gm>10;if(guard){const s=lerp(-30,w,smoothstep(10,14,gm));for(let y=0;y<8;y++)for(let x=0;x<16;x++)put(s+x+y,y,' ',null,'#3a3820')}const cx=vx-34;const c=clawd(cx,4,{facing:guard?0:1,eyes:guard?'wide':'open',arms:guard?'down':{left:'down',right:'out'},blink:!guard&&fract(t*0.3)<0.04});pixels(c.top.x-6,c.eyes.py-1,'kkkkkkkkkkkkk',{k:'#0a0a0a'});pixels(c.right.x,c.right.py-1,'ss\\n s\\n s',{s:'#9ab'});const lines=['Every compiled byte of {what}, straight into the bag.','Tumblers clicking. {what} is almost cracked.','Shh. The compiler hears everything.','Another bar of {what}. This heist is going swimmingly.'];if(guard)say('Freeze! Pretend to be a progress bar.',c.top.x,c.row);else if(mod(t,14)<6)say(lines[Math.floor(t/14)%4],c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#202030","#2a2a40","#302a20"],"speed":0.5,"intensity":0}}},
62 {"phase":"building","world":"sports broadcast","style":"3D","concept":"lighthouse in a storm: Clawd keeper on the rocks guides a cargo ship labeled {what} into harbor through rain, beam sweeping, 3D","source":"claude-opus-5-5","scene":{"concept":"lighthouse in a storm: Clawd keeper on the rocks guides a cargo ship labeled {what} into harbor through rain, beam sweeping, 3D","code":"const tw=cylinder(0.9,4,12),cap=cone(1.1,0.8,12),lamp=sphere(0.5,8),rock=sphere(1.6,6),hull=box(2.4,0.6,1),cab=box(0.8,0.6,0.7),mast=cylinder(0.05,1.2,4);\nconst rn=Array.from({length:90},(_,i)=>({x:rand(i)*w,y:rand(i+40)*h,v:12+rand(i+7)*8}));\nfunction frame(t,dt){\ncamera(0,2.4,9,0,1.2,0,90);light(-0.4,0.8,0.6,0.35);fog(8,40,'#05080c');\nconst fl=fract(t/7)<0.04;\nfor(let z=-30;z<6;z+=3)for(let x=-18;x<18;x+=2){const y=Math.sin(x*0.6+t*2+z*0.4)*0.15;line3d(x,y,z,x+1.6,Math.sin((x+1.6)*0.6+t*2+z*0.4)*0.15,z,fl?'#bde':'#3a6a8a')}\nmesh3d(rock,{x:6,y:-0.9,z:-1,color:'#7a7468'});\nmesh3d(tw,{x:6,y:0.4,z:-1,color:'#e8e2d8'});\nmesh3d(box(1.85,0.5,1.85),{x:6,y:2.6,z:-1,color:'#c94a3a'});\nmesh3d(lamp,{x:6,y:4.7,z:-1,color:'#ffe680',unlit:true});\nmesh3d(cap,{x:6,y:5.1,z:-1,color:'#c94a3a'});\nconst a=t*1.3;\nfor(let i=0;i<3;i++){const s=a+i*0.04;line3d(6,4.7,-1,6+Math.cos(s)*22,4.2,-1+Math.sin(s)*22,'#fff4a8')}\nconst k=smoothstep(0,80,t%90),sx=lerp(-16,-1,k),sz=lerp(-24,-1,k),bob=Math.sin(t*1.7)*0.2,rl=Math.sin(t*1.3)*0.12;\nmesh3d(hull,{x:sx,y:0.2+bob,z:sz,rz:rl,color:'#d0a050'});\nmesh3d(cab,{x:sx+0.5,y:0.75+bob,z:sz,rz:rl,color:'#e0e0e0'});\nmesh3d(mast,{x:sx-0.5,y:0.5+bob,z:sz,color:'#ccc'});\nconst p=project(sx,1.6+bob,sz);if(p)text(p.x-5,p.y-1,'{what}','#ffd27a');\nfor(const r of rn){r.y+=r.v*dt;r.x-=r.v*dt*0.3;if(r.y>h){r.y=0;r.x=rand(t+r.x)*w*1.2}put(r.x,r.y,'/','#6c8cb0')}\nif(fl)for(let x=0;x<w;x+=1)if(rand(x+floor(t))<0.04)put(x,0,'▼','#ffffff');\nconst c=clawd3d(3.4,0,0.5,{facing:-1,arms:{left:'up',right:'down'},size:0.9});\nif(c){pixels(c.left.x-1,c.left.py-3,'.y.\\nyYy\\n.y.',{y:'#ffd040',Y:'#fff8c0'});\nconst ph=t%90,l=ph<20?'Hold steady, {what}. Follow the light!':ph<45?'Big wave! Cargo still strapped down.':ph<75?'Almost to the dock. Keep compiling!':'{what} has landed. Lighthouse: 1, storm: 0';\nif(fract(ph/20)<0.6)say(l,c.top.x,c.row)}\n}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#1a2a3a","#2a4058","#3a5070"],"speed":1.2,"intensity":0}}},
63 {"phase":"building","world":"haunted house","style":"3D","concept":"nature documentary: Clawd crouched in the grass narrating as a termite colony hauls crumbs to raise a growing mound labeled {what}, slow 3D ","source":"claude-opus-5-5","scene":{"concept":"nature documentary: Clawd crouched in the grass narrating as a termite colony hauls crumbs to raise a growing mound labeled {what}, slow 3D orbit","code":"const md=cone(1.3,1,12),ant=sphere(0.13,5),cr=box(0.15,0.15,0.15),gr=plane(50,24),bl=cone(0.06,0.7,4);\nconst ants=Array.from({length:14},(_,i)=>({a:i*2.39,o:rand(i)}));\nconst gs=Array.from({length:30},(_,i)=>({x:(rand(i+5)-0.5)*24,z:(rand(i+40)-0.5)*10}));\nconst L=['Here we observe the wild {what}, building.','Each worker carries one crumb. Patience.','The mound grows. Nobody knows why it takes so long.','Shh. A dependency is being installed.','Remarkable. It has eaten another megabyte.','Nature is beautiful. Also slow.'];\nfunction frame(t,dt){\ncamera(Math.sin(t*0.06)*5,2.3,8,0,0.9,0,80);light(0.5,1,0.4,0.35);fog(8,32,'#000');\nmesh3d(gr,{color:'#6f9a4c'});\nfor(const g of gs)mesh3d(bl,{x:g.x,z:g.z,color:'#9ccc5a'});\nconst H=0.7+2*smoothstep(0,100,t)+0.05*Math.sin(t);\nmesh3d(md,{scale:H,color:'#c8925a'});\nfor(const a of ants){const p=fract(t*0.12+a.o),r=7*(1-p)+1.3*H*0.8,x=Math.cos(a.a)*r,z=Math.sin(a.a)*r*0.6,y=p>0.9?(p-0.9)*10*H*0.7:0;\nmesh3d(ant,{x,y:y+0.13,z,color:'#e0dccc'});mesh3d(cr,{x,y:y+0.35,z,ry:t,color:'#ffd27a'});}\nconst q=project(0,H+0.4,0);if(q)text(q.x-5,q.y-1,'{what}','#fff4c0');\nconst c=clawd3d(-6,0,3,{facing:1,pose:'sit',size:0.8,blink:fract(t*0.3)<0.05});\nif(c){pixels(c.eyes.x-3,c.eyes.py-1,'##.##\\n##.##',{'#':'#444'});if(t>2)say(L[Math.floor((t-2)/11)%L.length],c.top.x,c.row);}\n}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#2a4a3a","#3c6a4a","#5a7a3a"],"speed":0.4,"intensity":0}}},
64 {"phase":"building","world":"medieval castle","style":"pixel art","concept":"dinosaur era: Clawd rides a brontosaurus whose long neck works as a crane, hauling stone blocks from a quarry to build the {what} tower whil","source":"claude-opus-5-5","scene":{"concept":"dinosaur era: Clawd rides a brontosaurus whose long neck works as a crane, hauling stone blocks from a quarry to build the {what} tower while a volcano smokes behind","code":"function frame(t,dt){const G=16;for(let x=0;x<w;x++){pixel(x,G,'#6a8a3a');pixel(x,G+1,'#4a5a2a')}for(let i=0;i<14;i++){const y=4+i;for(let x=100-i*1.4;x<100+i*1.4;x++)pixel(x,y,i<2?'#ff6a2a':'#8a5a4a')}for(let k=0;k<6;k++){const f=fract(t*0.3+k/6);put(99+Math.sin(f*6+k)*3,1-f*2+1,'○','#aaa')}for(let k=0;k<3;k++)pixels(4+k*5,G-2-(k%2)*0,'####\\n####',{'#':'#b0a898'});const P=4,c=Math.floor(t/P),p=fract(t/P),n=c%8,s=p<0.5?smoothstep(0,0.5,p):1-smoothstep(0.5,1,p),tx=66,top=G-n*2;for(let i=0;i<n;i++)pixels(tx+(i%2),G-2-i*2,'######\\n######',{'#':i%2?'#c8c0a8':'#a8a090'});const hx=lerp(8,tx+2,s),hy=lerp(G-5,top-6,s)-6*Math.sin(Math.PI*s);for(let a=0;a<=1;a+=0.03){const x=lerp(40,hx,a),y=lerp(10,hy,a)-Math.sin(a*Math.PI)*3;pixel(x,y,'#5fb05f');pixel(x,y+1,'#5fb05f')}pixels(hx-1,hy-1,'###o\\n#####',{'#':'#5fb05f','o':'#000'});const carry=p<0.5&&p>0.05;if(carry){line(0,0,0,0,'','#000');pixels(hx-1,hy+2,'####\\n####',{'#':'#c8c0a8'})}for(let y=10;y<15;y++){const r=Math.floor(9*Math.sqrt(1-((y-12)/3)**2));for(let x=30-r;x<30+r;x++)pixel(x,y,'#5fb05f')}for(let k=0;k<4;k++){const lx=23+k*4;pixel(lx,15,'#4a904a');pixel(lx+1,15,'#4a904a')}for(let a=0;a<1;a+=0.08)pixel(21-a*8,12+a*3,'#5fb05f');const cl=clawd(23,1.5,{arms:{left:'down',right:s>0.1&&s<0.9?'up':'out'},facing:1,blink:fract(t*0.3)<0.04});pixels(cl.right.x,cl.right.py-3,'o\\n#\\n#\\no',{'o':'#eee','#':'#ddd'});pixels(cl.top.x-5,cl.top.py-1,'.#.#.#.#.\\n#########',{'#':'#c89040','.':'#c89040'});text(tx+9,8,'{what}','#ffd27a');text(tx+8,7,'▲','#ffd27a');const lines=['Bronto-crane, block '+(n+1)+' of {what}!','Hand-laid, hand-compiled. Well, neck-laid.','No cranes for 65 million years. Improvising.','{what} tower rising, one dino-load at a time.'];if(p<0.6)say(lines[c%4],cl.top.x,cl.row)}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#3a2a5a","#7a3a3a","#c86a3a"],"speed":0.4,"intensity":0.25}}},
65 {"phase":"building","world":"museum after hours","style":"text art","concept":"museum after hours meets construction: Clawd as night-shift builder assembling a dinosaur skeleton exhibit bone by bone with a ceiling crane","source":"claude-opus-5-5","scene":{"concept":"museum after hours meets construction: Clawd as night-shift builder assembling a dinosaur skeleton exhibit bone by bone with a ceiling crane, flashlight guard passing","code":"let bones=[' ▄▄','▄███▄','═╦═╦═','║ ║ ║','╱╲','◢◣']\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,h-1,x%10==0?'┴':'═','#8a7a6a');put(x,0,x%6==0?'┬':'─','#6a6a8a')}\nfor(let i=0;i<4;i++){const px=8+i*30;sprite(px,1,'╔═══╗\\n║░░░║\\n║░░░║\\n╚═══╝','#5a6a9a')}\nconst cy=t%12,n=Math.min(8,Math.floor(t/12)+3)\nconst bx=60,by=h-2\nsprite(bx-12,by,'▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀','#a08868')\nconst parts=[[0,-1,'══════════'],[10,-2,'╦╦╦╦'],[-4,-1,'═══╗'],[14,-3,'▄▀▀▄'],[14,-2,'▀▄▄▀'],[2,-2,'╦═╦═╦═╦'],[3,0,'║'],[9,0,'║']]\nfor(let i=0;i<n;i++){const p=parts[i];text(bx+p[0],by+p[1]-0,p[2],'#e8dcc0')}\nconst tx=bx+parts[n%8][0],ph=smoothstep(0,5,cy),dn=smoothstep(5,9,cy)\nconst hx=lerp(105,tx,ph),hy=lerp(1,by+parts[n%8][1]-1,dn)\ntext(0,0,'','#000')\nline(hx,1,hx,hy,'│','#c8c8c8')\nput(hx,1,'▼','#f5c542')\nif(cy<9)text(hx,hy+1,parts[n%8][2],'#e8dcc0')\ntext(bx-2,by+1>h-1?h-1:by+1,'','#fff')\ntext(bx+18,h-4,'['+'{what}'+']','#f5c542')\nconst gx=mod(t*4,w+40)-20\nsprite(gx,h-4,' o \\n/█>','#7aa0d8')\nfor(let k=1;k<8;k++)put(gx+3+k,h-3,'░','#ffee88')\nconst c=clawd(100,h-5,{facing:-1,arms:{left:'up',right:'down'},blink:fract(t*0.3)<0.05})\ntext(c.top.x-2,c.top.row-1,'▄██▄','#f5c542')\nif(t%20<5)say(t<20?'Building {what}, one bone at a time.':'Shh. The guard thinks it\\'s a real dinosaur.',c.top.x,c.row)\n}","actors":[],"particles":[{"glyphs":"·","count":15,"x":"rand(k)*w","y":"1+mod(t*0.5+rand(k+4)*7,7)","color":"#8a8070"}],"background":{"effect":"pulse","palette":["#101830","#182040","#202850"],"speed":0.3,"intensity":0.15}}},
66 {"phase":"building","world":"skate park","style":"pixel art","concept":"skate park at night, noir-lit: Clawd skates a halfpipe carrying build crates labeled {what}, dropping them into a bin that fills up","source":"claude-opus-5-5","scene":{"concept":"skate park at night, noir-lit: Clawd skates a halfpipe carrying build crates labeled {what}, dropping them into a bin that fills up","code":"let cr=0;function frame(t,dt){const H=2*h;for(let x=0;x<w;x+=1){const n=noise(x*0.07)*4;for(let y=0;y<H;y++){if(y>H-6-n&&x%9<6&&y<H-4)pixel(x,y,'#2a3348')}}for(let x=0;x<w;x+=11)pixel(x,3+(x%3),'#ffe9a8');pixels(w-8,2,'.oo.\\noooo\\n.oo.','{\"o\":\"#f4e7b0\"}'.length?{o:'#f4e7b0'}:{});const L=20,R=w-34;for(let x=L;x<=R;x++){const u=(x-L)/(R-L);const y=Math.round(H-3-Math.pow(Math.abs(u*2-1),3)*10);pixel(x,y,'#9ad4ff');for(let yy=y+1;yy<H;yy++)pixel(x,yy,'#3d4a66')}const p=t*0.9;const u=0.5+0.5*Math.sin(p);const cx=L+u*(R-L);const cy=H-3-Math.pow(Math.abs(u*2-1),3)*10;const f=Math.cos(p)>0?1:-1;const c=clawd(cx-7,(cy-10)/2,{pose:'stand',facing:f,arms:'out'});pixels(Math.round(cx-6),Math.round(cy-2),'############\\n.o........o.',{'#':'#ff6b9a','o':'#ddd'});const bx=w-24;pixels(bx,H-9,'#........#\\n#........#\\n#........#\\n#........#\\n##########',{'#':'#7ae0a0'});const ph=Math.floor(p/(2*Math.PI));cr=Math.min(4,ph);for(let i=0;i<cr;i++)pixels(bx+1+(i%2)*4,H-6-Math.floor(i/2)*2,'####\\n####',{'#':'#e0a060'});if(Math.sin(p)>0.8)pixels(c.right.x,c.right.py-2,'###\\n###',{'#':'#e0a060'});text(bx-1,h-1,'{what}'.slice(0,20),'#ffe9a8');if(t>3&&t<9)say('Kickflipping {what} into the bin, one crate per run.',c.top.x,c.row);if(t>40&&t<48)say('Bin full soon. Still landing every trick.',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#334","#446","#557"],"speed":0.6,"intensity":0.3}}},
67 {"phase":"building","world":"retro arcade game","style":"3D","concept":"ancient tomb as 3D arcade: Clawd lowers sandstone blocks onto a growing pyramid labelled {what} while torches flicker, score ticking up","source":"claude-opus-5-5","scene":{"concept":"ancient tomb as 3D arcade: Clawd lowers sandstone blocks onto a growing pyramid labelled {what} while torches flicker, score ticking up","code":"const blk=box(1.6,0.8,1.6);const col=cylinder(0.3,3,8);let placed=0;const total=15;const lay=[];for(let r=0;r<5;r++)for(let i=0;i<5-r;i++)lay.push({x:(i-(4-r)/2)*1.7,y:0.4+r*0.85});function frame(t,dt){camera(Math.sin(t*0.15)*2,3,10,0,1.5,0,80);light(0.4,0.8,0.5,0.35);fog(8,35,'#000');mesh3d(plane(40,20),{y:0,z:-3,color:'#a08050'});for(const s of[-1,1]){mesh3d(col,{x:s*8,z:-2,color:'#c9a86a'});const fl=0.6+0.4*noise(t*6+s);const p=project(s*8,3.3,-2);if(p){put(p.x,p.y,'▲',mix('#ff6020','#ffe060',fl));put(p.x,p.y+1,'█','#6a4a2a')}}const cyc=t%2.2,done=Math.floor(t/2.2)%total;for(let i=0;i<done;i++){const b=lay[i];mesh3d(blk,{x:b.x,y:b.y,color:i%2?'#d8b878':'#c8a060'})}const b=lay[done];const k=smoothstep(0,1.6,cyc);const by=lerp(4.2,b.y,k);mesh3d(blk,{x:b.x,y:by,color:'#f0d090'});line3d(b.x,by+0.4,0,b.x,5,0,'#998866');const c=clawd3d(b.x,4.4,0,{facing:0,arms:'down',size:0.8});const tp=project(0,4.6,0);if(tp)text(tp.x-Math.floor('{what}'.length/2),0,'{what}','#ffe08a');text(1,0,'SCORE '+String(Math.floor(t*37)*10).padStart(6,'0'),'#7cf');text(w-10,0,'1UP ♥♥♥','#f77');if(done==total-1&&cyc>1.6)text(w/2-5,h-1,'LEVEL UP!','#ff0');if(c&&t%30>2&&t%30<7)say('Stacking {what} one block at a time.',c.top.x,c.row);else if(c&&t%30>16&&t%30<21)say('Pharaohs waited centuries. You get a progress bar.',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#887"],"speed":0.4,"intensity":0}}},
68 {"phase":"running","world":"fairy tale forest","style":"3D","concept":"fairy tale forest launch pad: Clawd as a gnome mission controller watching a toadstool rocket labeled {what} count down and lift off among 3","source":"claude-opus-5-5","scene":{"concept":"fairy tale forest launch pad: Clawd as a gnome mission controller watching a toadstool rocket labeled {what} count down and lift off among 3D pine trees","code":"const tr=cone(0.9,2.4,8),tk=cylinder(0.15,0.6,6),body=cylinder(0.45,2.2,10),cap=sphere(0.9,10),pad=cylinder(1.6,0.2,12);\nconst trees=Array.from({length:14},(_,i)=>({x:(i%7-3)*3.4+(rand(i)-0.5),z:-3-Math.floor(i/7)*4-rand(i+9)*2}));\nfunction frame(t,dt){const c=mod(t,24),L=smoothstep(10,20,c),ry=c>10?Math.pow(c-10,2)*0.12:0;camera(0,2.2+L*1.5,9,0,1+L*2,-2,90);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nmesh3d(plane(40,30),{z:-6,color:'#4a7a4a'});for(const q of trees){mesh3d(tk,{x:q.x,z:q.z,color:'#9a6a40'});mesh3d(tr,{x:q.x,y:0.6,z:q.z,color:'#5fae6a'})}\nmesh3d(pad,{x:2,z:-1,color:'#b0a090'});const sh=c<10&&c>7?Math.sin(t*60)*0.04:0;mesh3d(body,{x:2+sh,y:0.2+ry,z:-1,color:'#f4ead0'});mesh3d(cap,{x:2+sh,y:2.4+ry,z:-1,scale:1,color:'#e05050'});\nconst p=project(2,1.3+ry,-1);if(p)text(p.x-5,p.y,'{what}'.slice(0,12),'#523');\nif(c>9){for(let i=0;i<14;i++){const f=fract(t*3+i*0.13);point3d(2+(rand(i)-0.5)*f*1.5,0.2+ry-f*1.5,-1,i%2?'#ffd060':'#ff8040')}}\nfor(let i=0;i<6;i++)point3d(-6+i*2.4+Math.sin(t+i),1.5+Math.sin(t*1.7+i)*0.5,0,'#ffffaa');\nconst me=clawd3d(-3,0,1,{facing:1,arms:c>10?'up':{left:'down',right:'out'},size:1});if(me){pixels(me.top.x-3,me.top.py-5,'...r...\\n..rrr..\\n.rrrrr.\\nrrrrrrr','{\"r\":\"#c03030\"}'.length?{r:'#c03030'}:{});\nconst n=Math.ceil(10-c);if(c<10)say(n>0?`{what} countdown: ${n}... wish upon a star.`:'Ignition!',me.top.x,me.row);else if(c<18)say('Up she goes! The mushroom has left the forest.',me.top.x,me.row);else say('Waiting for {what} to land somewhere magical.',me.top.x,me.row)}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#ffffcc","#aaccff","#ffd0f0"],"speed":0.5,"intensity":0.3}}},
69 {"phase":"running","world":"airport runway","style":"pixel art","concept":"underground mine: hard-hatted Clawd pushes a minecart labeled {what} down a lantern-lit tunnel as gems pile up in the cart","source":"claude-opus-5-5","scene":{"concept":"underground mine: hard-hatted Clawd pushes a minecart labeled {what} down a lantern-lit tunnel as gems pile up in the cart","code":"let ore=[];function frame(t,dt){const s=t*6;for(let y=0;y<h;y++)for(let x=0;x<w;x++){const n=noise2((x+s*0.5)*0.15,y*0.4);const tun=y>=2&&y<=7;let c=tun?mix('#1a1410','#2a2018',n):mix('#3a2a1e','#6a4a30',n);const d=Math.abs(x-60)/60;if(tun)c=mix(c,'#000',d*0.6);put(x,y,' ',null,c);if(!tun&&rand(Math.floor(x+s*0.5)*7+y)>0.97)put(x,y,'◆',hsl((x*37)%360,0.8,0.65),c)}for(let i=-1;i<6;i++){const bx=Math.round(i*28-mod(s,28));fill(bx,2,2,6,'█','#7a5230');fill(bx-1,1,4,1,'▀','#9a6a3a');put(bx+3,3,'●','#ffd060');}for(let x=0;x<w;x++)put(x,8,mod(x+Math.floor(s),4)==0?'╪':'═','#888');const c=clawd(48,4,{pose:'walk',stride:Math.floor(s/1.5)%4,facing:1,arms:'out'});pixels(c.top.x-4,c.top.py-3,'..yyyy..\n.yyyyyy.\nyyyyyyyy','#'.length?{y:'#f0c020'}:{});pixels(c.top.x-1,c.top.py-3,'w','#'.length?{w:'#fff8c0'}:{});const cx=63;pixels(cx,9,'bbbbbbbbbbbbbbbb\n.bggggggggggggb.\n.bggggggggggggb.\n.bbbbbbbbbbbbbb.\n..oo........oo..','#'.length?{b:'#8a8a9a',g:'#5a5a6a',o:'#333'}:{});text(cx+3,5,'{what}'.slice(0,10),'#ffe080','#5a5a6a');const f=Math.min(12,Math.floor(t/4));const pal=['#5ef','#f5a','#fd4','#6f6'];for(let i=0;i<f;i++)pixel(cx+2+(i*5)%12,8-Math.floor(i/6),pal[i%4]);if(fract(t/4)<0.15)put(cx+8+Math.floor(rand(Math.floor(t/4))*4),3,'✦','#fff');const L=['Every shovel of {what} is a gem. Probably.','Still digging. The tunnel goes deeper than the docs.','Cart is filling up! Output looks shiny.','Mind the beams. And the exit code.'];if(t>2)say(L[Math.floor(t/12)%4],c.top.x,c.row);}","actors":[],"particles":[{"glyphs":".·","count":20,"x":"rand(k)*w","y":"2+mod(t*(1+rand(k+5))+rand(k+3)*6,6)","color":"#a08060"}],"background":{"effect":"plasma","palette":["#2a1a10","#3a2818","#4a3020"],"speed":0.3,"intensity":0}}},
70 {"phase":"running","world":"ant colony up close","style":"3D","concept":"airport runway: Clawd as ground marshal waving wands while the {what} jet taxis, roars down the runway and lifts off, again and again","source":"claude-opus-5-5","scene":{"concept":"airport runway: Clawd as ground marshal waving wands while the {what} jet taxis, roars down the runway and lifts off, again and again","code":"const fus=box(4,0.7,0.7),wing=box(1.2,0.12,4.4),tail=box(0.6,1,0.12),strip=plane(60,5),lt=box(0.15,0.15,0.15),tw=box(1.2,3,1.2),cab=box(1.8,0.8,1.8);\nconst lines=['{what} cleared for takeoff. Wheels up!','Ground crew to tower: {what} is rolling.','Another one airborne. I love this job.','Fasten seatbelts, {what} is going up.'];\nfunction frame(t,dt){camera(0,2.6,11,0,1,0,90);light(0.4,1,0.6,0.35);fog(10,45,'#000');\nmesh3d(strip,{y:0,color:'#7a7f8a'});\nfor(let i=-15;i<16;i++){mesh3d(box(1.2,0.02,0.15),{x:i*2+mod(0,2),y:0.02,color:'#eee'});const on=fract(t*0.8-i*0.05)<0.3;mesh3d(lt,{x:i*2,y:0.1,z:2.6,color:on?'#ffd23f':'#806a20',unlit:true});mesh3d(lt,{x:i*2,y:0.1,z:-2.6,color:on?'#ffd23f':'#806a20',unlit:true});}\nmesh3d(tw,{x:10,y:1.5,z:-8,color:'#b8c4d6'});mesh3d(cab,{x:10,y:3.4,z:-8,color:'#6fc3ff'});\nconst P=16,c=Math.floor(t/P),p=mod(t,P);let x,y=0.6,rz=0;\nif(p<5)x=lerp(-28,-12,smoothstep(0,5,p));else if(p<8)x=-12;else{const q=p-8;x=-12+q*q*1.2;y=0.6+Math.max(0,x-2)*0.22;rz=Math.min(0.25,Math.max(0,x-2)*0.05);}\nconst o={x,y,z:0,rz,color:'#e8ecf2'};mesh3d(fus,o);mesh3d(wing,{...o,x:x-0.2,color:'#c9d3e0'});mesh3d(tail,{...o,x:x-1.8,y:y+0.6+rz*1.8,color:'#d97757'});\nconst pp=project(x,y+1.4,0);if(pp)text(pp.x-4,pp.y,'{what}','#ffe08a');\nif(p>8&&p<14)for(let k=0;k<6;k++){const e=project(x-2.2-k*0.6,y-0.1-rz*2,0);if(e)put(e.x,e.y,k<2?'▒':'░',k<2?'#ffb347':'#888');}\nconst wave=p<8?Math.floor(t*2)%2:0;const me=clawd3d(-3,0,4,{facing:0,arms:wave?{left:'up',right:'out'}:{left:'out',right:'up'},size:0.9});\nif(me){put(me.left.x,me.left.row-1,'▮','#ff5a36');put(me.right.x,me.right.row-1,'▮','#ff5a36');pixels(me.top.x-3,me.top.py-2,'..####..\\n########','{\"#\":\"#ffd23f\"}');if(p>8.5&&p<13)say(lines[c%lines.length],me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#557","#99b"],"speed":0.4,"intensity":0.3}}},
71 {"phase":"running","world":"fishing pond","style":"text art","concept":"mad science lab: goggled Clawd at the console while Tesla coils arc and a huge flask labeled {what} slowly fills and froths over","source":"claude-opus-5-5","scene":{"concept":"mad science lab: goggled Clawd at the console while Tesla coils arc and a huge flask labeled {what} slowly fills and froths over","code":"function frame(t,dt){fill(0,8,w,1,'═','#667');const coil=' (O) \\n ║ \\n ═╬═ \\n ║ \\n ═╬═ \\n ▐█▌ ';sprite(6,2,coil,'#c8a');sprite(30,2,coil,'#c8a');const zap=fract(t/3)<0.3;if(zap){for(let x=11;x<33;x++){const y=2+Math.round(Math.sin(x*1.7+t*40));put(x,y,Math.random()<0.5?'/':'\\\\','#aef')}put(8,2,'✦','#fff');put(32,2,'✦','#fff')}const c=clawd(42,4,{facing:1,arms:zap?'up':{left:'down',right:'out'},eyes:zap?'wide':'open'});text(c.eyes.x-1,c.eyes.row,'◎-◎','#8ff');text(43,3,'▄▄▄','#ddd');for(let x=58;x<81;x++)put(x,6,'═','#889');const bx=58+mod(t*8,23);put(Math.floor(bx),6,'●','#7f7');sprite(80,2,' ┌──┐ \\n │ │ \\n ╱ ╲ \\n ╱ ╲ \\n│ │\\n│ │\\n╰────────╯','#9cf');const R={3:[84,85],4:[83,86],5:[82,87],6:[81,88],7:[81,88]};const p=fract(t/15),lvl=p*5.5;for(let r=7;r>=3;r--){if(7-r<lvl){const[a,b]=R[r];for(let x=a;x<=b;x++)put(x,r,(x+r+Math.floor(t*3))%5?'▓':'o',hsl(110+20*Math.sin(t+x),0.8,0.5))}}if(p>0.85){for(let i=0;i<6;i++)put(83+((i*3+Math.floor(t*6))%5),1-((i+Math.floor(t*5))%2),'░','#7f7')}text(92,5,'◄ {what}','#fd6');fill(96,7,10,1,'▀','#556');text(97,6,'▯▯▯','#f6a');const s=mod(t,15);if(s>2&&s<6)say('Feeding {what} to the reactor. It lives!',c.top.x,c.row);if(s>11&&s<14.5)say('Almost done... or almost exploding.',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#203040","#302050","#204030"],"speed":0.6,"intensity":0.2}}},
72 {"phase":"running","world":"orchestra concert","style":"3D","concept":"orchestra concert on a construction site: Clawd in a hard hat conducts tower cranes that lift girder-notes onto a rising building score whil","source":"claude-opus-5-5","scene":{"concept":"orchestra concert on a construction site: Clawd in a hard hat conducts tower cranes that lift girder-notes onto a rising building score while {what} runs","code":"const bx=box(1,1,1),cy=cylinder(0.12,5,6),gd=box(2.2,0.25,0.4);let fl=0;\nfunction frame(t,dt){camera(Math.sin(t*0.1)*2,2.6,9,0,1.6,0,90);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nmesh3d(plane(40,20),{y:0,color:'#7a6a50'});\nconst n=Math.floor(t/4),p=fract(t/4);\nfor(let i=0;i<Math.min(n,12);i++){const c=i%4,r=Math.floor(i/4);mesh3d(bx,{x:-1.5+c,y:0.5+r,z:-2,color:['#e8b04a','#d0d4dc','#c98a5a','#a8c0e0'][c]})}\nfor(const s of [-1,1]){const cx=s*6;mesh3d(cy,{x:cx,z:-2,color:'#f0c040'});line3d(cx,5,-2,cx-s*5,5,-2,'#f0c040');line3d(cx,5,-2,cx+s*2,5,-2,'#f0c040')}\nconst i=n%12,c=i%4,r=Math.floor(i/4),s=n%2?1:-1,tx=-1.5+c,ty=0.5+Math.min(n,12)/4|0;\nconst k=Math.min(1,p*1.6),e=k*k*(3-2*k),gx=lerp(s*9,tx,e),gy=lerp(1,4.6,Math.sin(Math.PI*Math.min(1,e*1.3))*0.6)+ (e>0.9?lerp(4.6,0.5+r,(e-0.9)*10):0)*0;\nconst yy=e<0.7?lerp(0.3,4.2,e/0.7):lerp(4.2,0.5+(Math.min(n,12)/4|0),(e-0.7)/0.3);line3d(gx,5,-2,gx,yy+0.2,-2,'#ccc');mesh3d(gd,{x:gx,y:yy,z:-2,ry:Math.sin(t*2)*0.2,color:'#e06a4a'});\nconst q=project(gx,yy+0.5,-2);if(q)text(q.x-1,q.y-1,'♪','#ffe');\nconst arm=Math.sin(t*3)>0?'up':'out';const m=clawd3d(3.5,0,2,{facing:-1,arms:{left:arm,right:Math.sin(t*3+1.5)>0?'up':'down'}});\nif(m){pixels(m.top.x-4,m.top.py-3,'..yyyy..\\nyyyyyyyy','{\"y\":\"#ffd23a\"}'.length?{y:'#ffd23a'}:{});const lb=project(-3.5,0.3,1);if(lb)text(lb.x,lb.y,'[ '+'{what}'+' ]','#9fe');\nconst L=['Allegro, cranes! {what} in 4/4.','Brass section: more rivets.','Floor '+(Math.min(n,12)/4|0)+' and a key change.','Still building. Still bowing.'];if(t>2)say(L[Math.floor(t/9)%4],m.top.x,m.row)}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#889"],"speed":0.3,"intensity":0.2}}},
73 {"phase":"running","world":"desert caravan","style":"3D","concept":"zen rock garden: Clawd as a monk slowly raking ripples around a stone labeled {what} while the command runs, 3D orbiting garden","source":"claude-opus-5-5","scene":{"concept":"zen rock garden: Clawd as a monk slowly raking ripples around a stone labeled {what} while the command runs, 3D orbiting garden","code":"const rock=sphere(1,10),stone=sphere(0.6,8),lan=box(0.5,1.4,0.5),cap=cone(0.7,0.5,6);\nfunction frame(t,dt){\nconst a=t*0.08;\ncamera(Math.sin(a)*9,3,Math.cos(a)*9,0,0.5,0,80);\nlight(0.4,1,0.5,0.35);fog(8,30,'#000');\nfor(let r=1.5;r<9;r+=0.7){let p=null;const s=Math.min(48,Math.floor(t*2)+10);for(let i=0;i<=s;i++){const q=i/48*Math.PI*2;const x=Math.cos(q)*r,z=Math.sin(q)*r*0.8;if(p)line3d(p[0],0,p[1],x,0,z,'#c8b890');p=[x,z];}}\nmesh3d(rock,{x:0,y:0.6,z:0,scale:1,color:'#9a9488'});\nmesh3d(stone,{x:4,y:0.3,z:-2,color:'#8a8478'});\nmesh3d(stone,{x:-5,y:0.25,z:1.5,scale:0.7,color:'#a09a8a'});\nmesh3d(lan,{x:-3,y:0.7,z:-4,color:'#b0a898'});mesh3d(cap,{x:-3,y:1.4,z:-4,color:'#c0b8a8'});\nconst ang=t*0.25,cx=Math.cos(ang)*3.2,cz=Math.sin(ang)*2.6;\nconst c=clawd3d(cx,0,cz,{stride:Math.floor(t*3)%4,facing:Math.sin(ang+a)>0?-1:1,size:0.8});\nconst p=project(0,1.9,0);if(p)text(p.x-4,p.y,'{what}','#ffe9b0');\nif(c){text(c.left.x-2,c.left.row,'⊥','#d0b070');const m=t%30;\nif(m>4&&m<11)say('Raking circles around {what}. Patience.',c.top.x,c.row);\nelse if(m>17&&m<24)say('The command finishes when it finishes.',c.top.x,c.row);}\nconst lf=fract(t*0.07);put(10+lf*90,lf*8,'✦','#f0a0b0');\n}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#2a3a2a","#3a4a3a","#4a5a4a"],"speed":0.3,"intensity":0}}},
74 {"phase":"running","world":"bank heist","style":"pixel art","concept":"dinosaur-era bank heist: Clawd in a striped mask cracks a stone-age vault while a brontosaurus slowly drags it open, labeled {what}","source":"claude-opus-5-5","scene":{"concept":"dinosaur-era bank heist: Clawd in a striped mask cracks a stone-age vault while a brontosaurus slowly drags it open, labeled {what}","code":"let gold=[];function frame(t,dt){for(let x=0;x<w;x++){for(let py=0;py<18;py++){pixel(x,py,py>15?'#6b4a2a':py<10?mix('#2a1f4a','#7a3a5a',py/10):'#3a2a3a')}}pixels(4,4,'..##.....\\n.####....\\n######...\\n..##.....\\n..##.....\\n..##.....',{'#':'#3f8a3a'});pixels(108,6,'.###.\\n#####\\n..#..\\n..#..',{'#':'#3f8a3a'});pixels(10,3,'....###\\n...####\\n.....##',{'#':'#ff9a3a'});const vx=60;for(let py=4;py<16;py++)for(let x=vx;x<vx+26;x++)pixel(x,py,(x+py)%5==0?'#8a8a8a':'#a8a8a8');const o=smoothstep(2,30,mod(t,40));const dx=Math.round(o*16);for(let py=6;py<16;py++)for(let x=vx+4;x<vx+22;x++)pixel(x,py,'#2a1a10');for(let py=6;py<16;py++)for(let x=vx+4+dx;x<vx+22+dx;x++)pixel(x,py,'#7a7a7a');circle(vx+13+dx,5.5,1,'●','#ddd');text(vx+5+dx,4,'{what}'.slice(0,12),'#ffd');line(vx+22+dx,5,96,5,'─','#c9a');const bx=96+dx*0.3;pixels(Math.round(bx),2,'.........####\\n........##o##\\n........###..\\n.......###...\\n#############.\\n###############\\n###############\\n.##..##..##..#\\n.##..##..##..#',{'#':'#7aa85a','o':'#000'});if(o>0.3&&Math.random()<o*0.4)gold.push({x:vx+8+Math.random()*10,y:12,vx:-4-Math.random()*6,vy:-8});for(const g of gold){g.vy+=20*dt;g.x+=g.vx*dt;g.y+=g.vy*dt;if(g.y>15){g.y=15;g.vx=0;g.vy=0}pixel(Math.round(g.x),Math.round(g.y),'#ffd23a')}if(gold.length>150)gold.splice(0,1);const cx=lerp(-14,36,smoothstep(0,3,t));const c=clawd(cx,4,{pose:cx<36?'walk':'stand',stride:Math.floor(cx/1.5)%4,facing:1,arms:o>0.5?'up':'out'});fill(c.eyes.x-6,c.eyes.row,13,1,'▀','#222');pixels(c.right.x,c.right.py-2,'.##.\\n####\\n####',{'#':'#c9b07a'});const ph=Math.floor(t/12)%4;const L=['Tiny arms? I brought a bronto. Cracking {what}.','Vault door: limestone. Patience: also limestone.','Gold coming out. {what} still churning.','Meteor-proof vault. Not bronto-proof.'];if(t>3)say(L[ph],c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#3a2a5a","#7a3a5a","#c96a3a"],"speed":0.5,"intensity":0}}},
75 {"phase":"running","world":"wild west","style":"text art","concept":"submarine: Clawd pilots a little sub through the deep, sonar pinging into the dark while {what} runs below the waterline","source":"claude-opus-5-5","scene":{"concept":"submarine: Clawd pilots a little sub through the deep, sonar pinging into the dark while {what} runs below the waterline","code":"let bub=Array.from({length:25},(_,i)=>({x:rand(i)*w,y:rand(i+40)*h,v:0.8+rand(i+9)*1.5}));const lines=['Diving on {what}. Hold your breath.','Sonar says: still running. Very deep.','Ping... ping... nothing broke yet.','Depth gauge climbing. Output soon?','A fish just asked if {what} is done.'];function frame(t,dt){for(let y=0;y<h;y++)for(let x=0;x<w;x+=1)if((x+y*7)%23==0)put(x,y,'.','#1d4a66');for(let x=0;x<w;x++){const n=noise(x*0.12+t*0.6);put(x,h-1,n>0.6?'▲':n>0.4?'▄':'_','#4a7a5a');if(rand(floor(x+t*0.6*8))>0.93)put(x,h-2,'}','#3f9a5f')}for(const b of bub){b.y-=b.v*dt;if(b.y<0){b.y=h-1;b.x=rand(t+b.x)*w}put(b.x,b.y,b.v>1.6?'o':'°','#7fc8e8')}const sx=24,bo=Math.sin(t*1.2)*0.4,sy=Math.round(1+bo);const hc='#e8c34a';fill(sx+4,sy+1,44,5,'█',hc);text(sx+2,sy+2,'▐█',hc);text(sx+2,sy+4,'▐█',hc);text(sx+48,sy+2,'█▌',hc);text(sx+48,sy+4,'█▌',hc);text(sx+3,sy+3,'█',hc);text(sx+49,sy+3,'█',hc);text(sx+4,sy+6,'▀'.repeat(44),hc);text(sx+30,sy-1,'┌┐',hc);text(sx+30,sy,'██',hc);text(sx+32,sy-1,'═○','#cfd8dc');const pr='|/-\\\\'[floor(t*10)%4];text(sx,sy+3,pr+'═','#cfd8dc');fill(sx+15,sy+1,18,4,' ',null,'#0b2b3c');const c=clawd(sx+17,sy+1,{facing:1,blink:fract(t*0.3)<0.04,arms:fract(t*0.25)<0.5?'down':{left:'down',right:'out'}});text(sx+36,sy+2,'◐','#6fd3ff');text(sx+40,sy+2,'◐','#6fd3ff');text(sx+36,sy+4,'{what}','#5a3a10',hc);const g=Math.min(1,t/90);text(sx+6,sy+2,'DEPTH','#5a3a10',hc);text(sx+6,sy+3,'▓'.repeat(1+floor(g*6)).padEnd(7,'░'),'#8a2a10',hc);for(let i=0;i<3;i++){const f=fract(t*0.4+i/3);const px=sx+52+f*60;if(px<w)for(let r=-1;r<=1;r++)put(px+Math.abs(r),sy+3+r*2,r?(r<0?'/':'\\\\'):')',mix('#6fffd0','#0b2b3c',f))}const fx=w-mod(t*4,w+20);text(fx,6,'<°)))><','#ff9a6a');const li=floor(t/9)%lines.length;if(fract(t/9)<0.7)say(lines[li],c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"waves","palette":["#06243a","#0a3550","#0e4a6a"],"speed":0.5,"intensity":0.3}}},
76 {"phase":"git","world":"race track pit stop","style":"3D","concept":"bakery: Clawd the baker stacks a layer cake one commit-layer at a time, each tier dropping onto the stand, the newest labeled {what}","source":"claude-opus-5-5","scene":{"concept":"bakery: Clawd the baker stacks a layer cake one commit-layer at a time, each tier dropping onto the stand, the newest labeled {what}","code":"const L=cylinder(1.4,0.35,20),P=cylinder(1.9,0.08,20),C=box(16,0.5,4),cols=['#f4c2c2','#f0e0b0','#d8a070','#f8f0f0','#e8a8c8','#b8e0a8'];\nfunction frame(t,dt){camera(0,2.6,8,0,1.3,0,80);light(0.4,1,0.6,0.35);fog(8,40,'#000');\nmesh3d(C,{x:0,y:-0.25,z:0,color:'#b08860'});mesh3d(P,{x:1.5,y:0,z:0,color:'#dddddd'});\nconst cyc=t%28,n=Math.floor(cyc/4),k=fract(cyc/4);\nfor(let i=0;i<n&&i<6;i++)mesh3d(L,{x:1.5,y:0.08+i*0.35,z:0,color:cols[i]});\nlet top=0.08+n*0.35;\nif(n<6){const y=lerp(4,top,smoothstep(0.1,0.5,k));mesh3d(L,{x:1.5,y,z:0,color:cols[n],ry:t});const p=project(3.2,y+0.2,0);if(p)text(p.x+1,p.y,'{what}','#ffe9a0');}\nelse{const p=project(1.5,top+0.6,0);if(p){text(p.x,p.y,'★','#ffd84a');}}\nconst land=n<6&&k>0.5&&k<0.75;\nconst c=clawd3d(-3.2,0,0.5,{facing:1,arms:land?'up':'out',size:1.2});\nif(c){pixels(c.top.x-3,c.top.py-5,' ..... \\n.......\\n.......\\n ##### ','{\".\":\"#ffffff\",\"#\":\"#dddddd\"}'.length?{'.':'#ffffff','#':'#dddddd'}:{});\nconst s=n>=6?'Seven layers of history. Nobody rebase the cake.':n>=3?'Layer '+(n+1)+': {what}. Frosted, signed, committed.':'Fresh commit out of the oven: {what}';say(s,c.top.x,c.row);}}\n","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#3a2418","#4a2e20","#5a3828"],"speed":0.4,"intensity":0}}},
77 {"phase":"git","world":"train station","style":"pixel art","concept":"train station: Clawd as conductor on a platform as commit-cars couple onto a train that departs down a branching track","source":"claude-opus-5-5","scene":{"concept":"train station: Clawd as conductor on a platform as commit-cars couple onto a train that departs down a branching track","code":"let cars=[];let st=0;\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,0,' ',null,'#1a1f33');}\nfor(let i=0;i<8;i++)pixels(4+i*16,0,'..##..\\n.####.','{}'.length?{'#':'#2c3550'}:{});\nfill(0,7,w,1,'▀','#8a6d4a','#3a2a1a');\nfor(let x=0;x<w;x+=3)put(x,8,'╧','#776');\nline(70,8,w,6,'═','#556');\nfill(0,6,44,1,'▄','#9aa','#000');\ntext(2,1,'PLATFORM main',\"#ffd166\");\nconst cyc=mod(t,14);\nconst n=Math.min(4,Math.floor(cyc/2));\nconst dep=cyc>10?Math.pow(cyc-10,2)*5:0;\nconst bx=48+dep;\nconst P={'r':'#e06c75','y':'#ffd166','k':'#222','w':'#cde','g':'#5c9'};\npixels(bx,8,'..yy........\\nrrrrrrrrrrr.\\nrwwrrwwrrrrr\\nrrrrrrrrrrrr\\nkk..kk..kk..',P);\nif(dep<1)for(let p=0;p<3;p++){const a=fract(t*0.7+p/3);put(bx+2+a*4,3-a*3,'○','#aab');}\nfor(let i=0;i<n;i++){const cx=bx+13+i*13-(i==n-1&&cyc<10?Math.max(0,8-(cyc%2)*16):0);pixels(cx,9,'gggggggggggg\\ngwwgwwgwwggg\\ngggggggggggg\\nkk......kk..',P);text(cx+1,5,i==0?'{what}'.slice(0,11):'commit'+(i+1),'#fff');}\nconst c=clawd(20,2.5,{facing:1,arms:cyc>9&&cyc<11?{left:'down',right:'up'}:'down',blink:fract(t*0.3)<0.05});\npixels(c.top.x-4,c.top.py-2,'.kkkkkk.\\nkkkkkkkkk',{k:'#24305e'});\nif(cyc>9&&cyc<11)put(c.right.x,c.right.row-1,'⚑','#e06c75');\nif(cyc<3)say('All aboard! {what} is boarding.',c.top.x,c.row);\nelse if(cyc>9&&cyc<12)say('Committed! Next stop: HEAD.',c.top.x,c.row);\nelse if(cyc>12)say('Clear the track, new branch incoming.',c.top.x,c.row);\n}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#99a"],"speed":0.3,"intensity":0}}},
78 {"phase":"git","world":"ski slope","style":"3D","concept":"airport runway in 3D: commits take off as planes from a branching taxiway while Clawd waves them out as ground marshal","source":"claude-opus-5-5","scene":{"concept":"airport runway in 3D: commits take off as planes from a branching taxiway while Clawd waves them out as ground marshal","code":"const fus=box(0.8,0.7,3),wg=box(4,0.12,0.9),tl=box(0.12,0.8,0.6),twr=cylinder(0.4,3,8),cab=box(1.4,0.8,1.4);\nconst lines=['Cleared for takeoff: {what}','Another commit wheels-up!','Branch traffic heavy today.','Tower to main: {what} inbound.'];\nfunction frame(t,dt){camera(0,2.6,10,0,1,-20,85);fog(10,50,'#000');light(0.4,1,0.5,0.35);\nmesh3d(plane(8,120),{z:-40,color:'#7c8794'});\nfor(let i=0;i<14;i++){const z=6-i*6;line3d(0,0.02,z,0,0.02,z-2.5,'#fff');point3d(-4,0.1,z,'#ffd166');point3d(4,0.1,z,'#ffd166');}\nline3d(4,0.02,6,14,0.02,-30,'#7fd1b9');line3d(6,0.02,6,16,0.02,-30,'#7fd1b9');\nmesh3d(twr,{x:-10,z:-14,color:'#c9d1d9'});mesh3d(cab,{x:-10,y:3.3,z:-14,color:'#8ecae6'});\nconst cy=Math.floor(t/7),p=mod(t,7);const z=8-Math.pow(p,1.8)*2.4,y=0.45+Math.max(0,-z-6)*0.18;const rx=z<-6?-0.18:0;\nmesh3d(fus,{y,z,rx,color:'#e8eef5'});mesh3d(wg,{y,z:z+0.2,rx,color:'#d97757'});mesh3d(tl,{y:y+0.6,z:z+1.3,rx,color:'#d97757'});\nconst q=project(0,y+1.2,z);if(q&&q.depth>0)text(q.x-6,q.y,'{what}'.slice(0,20),'#ffe08a');\nconst bz=-20+mod(t*3,30);mesh3d(fus,{x:9+(-20-bz)*-0.0,z:bz,y:0.45,ry:-0.27,scale:0.7,color:'#a8dadc'});\nconst me=clawd3d(-5.5,0,4,{facing:1,arms:fract(t*1.5)<0.5?'up':'out'});\nif(me){pixels(me.left.x-1,me.left.py-3,'o\\no\\no','{\"o\":\"#ff9f1c\"}'==''?{}:{o:'#ff9f1c'});pixels(me.right.x,me.right.py-3,'o\\no\\no',{o:'#ff9f1c'});pixels(me.top.x-3,me.top.py-2,'.yyyy.\\nyyyyyy',{y:'#ffd23f'});if(p>1&&p<5.5)say(lines[cy%4],me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#99a"],"speed":0.4,"intensity":0}}},
79 {"phase":"git","world":"submarine","style":"3D","concept":"vegetable garden 3D: git as a garden, Clawd grafting a new branch onto a commit-vine, planting commit seeds along rows that fork into branch","source":"claude-opus-5-5","scene":{"concept":"vegetable garden 3D: git as a garden, Clawd grafting a new branch onto a commit-vine, planting commit seeds along rows that fork into branches","code":"const seed=sphere(0.25,8),stem=cylinder(0.06,1,6),leaf=cone(0.35,0.6,6),bed=box(26,0.3,1.4);\nlet planted=0;\nfunction frame(t,dt){\ncamera(Math.sin(t*0.1)*2,3.2,9,0,0.6,-3,80);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nmesh3d(bed,{x:0,y:-0.15,z:0,color:'#a07050'});\nmesh3d(bed,{x:4,y:-0.15,z:-5,color:'#8a6040'});\nline3d(-12,0.05,0,12,0.05,0,'#6c4');\nconst bx=-2;line3d(bx,0.05,0,bx+3,0.05,-5,'#6c4');line3d(bx+3,0.05,-5,16,0.05,-5,'#6c4');\nconst cyc=mod(t,30),n=Math.floor(cyc/3);\nfor(let i=0;i<8;i++){const x=-11+i*1.3;if(x>bx)break;const g=1;mesh3d(stem,{x,z:0,scale:g,color:'#7d5'});mesh3d(leaf,{x,y:0.8,z:0,color:'#5c4'});mesh3d(seed,{x,y:1.2,z:0,color:'#e55'});}\nfor(let i=0;i<n;i++){const x=bx+3+i*1.4;const g=clamp((cyc-i*3)/2,0.1,1);mesh3d(stem,{x,z:-5,scale:g,color:'#7d5'});mesh3d(leaf,{x,y:0.8*g,z:-5,scale:g,color:'#5c4'});if(g>0.9)mesh3d(seed,{x,y:1.2,z:-5,color:'#fb3'});}\nconst cx=bx+3+n*1.4-0.7,ph=fract(cyc/3);\nconst me=clawd3d(cx-1+ph,0,-4,{facing:1,stride:Math.floor(t*6)%4,arms:ph>0.6?'down':'out'});\nconst p=project(-11,1.6,0);if(p)text(p.x,p.y,'main','#9e8');\nconst q=project(bx+3,1.6,-5);if(q)text(q.x,q.y,'{what}','#fd6');\nif(me){if(cyc<6)say('New branch sprouting off main. Watering {what}.',me.top.x,me.row);else if(cyc>21&&cyc<27)say('Every commit a carrot. Harvest = merge.',me.top.x,me.row);}\n}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#234","#253","#345"],"speed":0.3,"intensity":0}}},
80 {"phase":"git","world":"weather report","style":"3D","concept":"circus big top in 3D: Clawd tightrope-walks the main branch high above the ring, a side rope forks off and merges back, and every step leave","source":"claude-opus-5-5","scene":{"concept":"circus big top in 3D: Clawd tightrope-walks the main branch high above the ring, a side rope forks off and merges back, and every step leaves a glowing commit bead on the wire","code":"const fl=cylinder(9,0.1,24),pl=cylinder(0.12,3,6),bl=sphere(0.22,8);\nconst bz=u=>-2.2*Math.sin(Math.PI*u);\nfunction frame(t){camera(Math.sin(t*0.2)*3,3.2,9,0,2.6,-2,90);fog(9,40,'#000');light(0.3,1,0.6,0.35);\nmesh3d(fl,{z:-3,color:'#c04848'});\nfor(let i=0;i<=16;i++){const a=i/16*Math.PI;line3d(0,9,-5,Math.cos(a)*13,0,-4-Math.sin(a)*9,i%2?'#e05050':'#eeeeee')}\nmesh3d(pl,{x:-8,color:'#dddddd'});mesh3d(pl,{x:8,color:'#dddddd'});\nline3d(-8,3,0,8,3,0,'#ffe080');\nfor(let i=0;i<8;i++){const u0=i/8,u1=(i+1)/8;line3d(-4+u0*8,3,bz(u0),-4+u1*8,3,bz(u1),'#70d0ff')}\nconst cy=mod(t,22),p=clamp(cy*1.0-8,-8,8);\nfor(let x=-7;x<=7;x+=2)if(p>x)mesh3d(bl,{x,y:3.25,z:0,color:'#ffd040'});\nfor(let k=1;k<4;k++){const u=k/4;if(cy>6+k*1.5)mesh3d(bl,{x:-4+u*8,y:3.25,z:bz(u),color:'#60e0ff'})}\nconst c=clawd3d(p,3,0,{stride:p<8?Math.floor(t*6)%4:-1,facing:p<8?1:0,arms:p<8?'out':'up',size:0.9});\nconst b=project(0,6.5,-5);if(b)text(b.x-6,b.y,'★ '+'{what}'+' ★','#ffee99');\nif(c){const s=cy<5?'Ladies and gentlemen: {what}, no safety net!':cy<12?'Feature branch swinging out... and back!':cy<18?'Merge caught! Every commit stays on the wire.':'Ta-da! Clean history, take a bow.';say(s,c.top.x,c.row)}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#ffe080","#ff8080","#80d0ff"],"speed":0.5,"intensity":0}}},
81 {"phase":"web","world":"train station","style":"pixel art","concept":"submarine meets train station: Clawd pilots a little pixel sub along a seabed rail line, stopping at an underwater station to collect a mess","source":"claude-opus-5-5","scene":{"concept":"submarine meets train station: Clawd pilots a little pixel sub along a seabed rail line, stopping at an underwater station to collect a message capsule labeled {what} from a pneumatic tube","code":"const P={'#':'#f2c14e','o':'#9fe3ff','d':'#b07a2a','p':'#888'};\nconst sub=['....##.....','....##.....','.#########.','###oo#oo####','############','.##########.'];\nlet bub=Array.from({length:25},(_,i)=>({x:rand(i)*w,y:rand(i+9)*h*2,v:2+rand(i+5)*3}));\nfunction frame(t,dt){\nfor(let y=0;y<h;y++)for(let x=0;x<w;x+=1){const k=y/h;put(x,y,' ',null,mix('#0a2a4a','#04121f',k));}\nfor(let x=0;x<w;x++){put(x,h-1,x%4==0?'┴':'═','#7a8a99');}\nfor(let x=0;x<w;x+=9)pixels(x,2*h-5,'.#.\\n###',{'#':'#2f6b3a'});\nconst sx=Math.floor(w*0.62);fill(sx,1,22,6,' ',null,'#1c3a55');sprite(sx,0,'╔════════════════════╗','#9ab');for(let y=1;y<7;y++){put(sx,y,'║','#9ab');put(sx+21,y,'║','#9ab');}\ntext(sx+3,1,'STATION: WEB DEPTHS','#ffd27a');line(sx+16,2,sx+16,6,'┃','#bbb');\nconst cyc=mod(t,16),go=smoothstep(0,5,cyc),leave=smoothstep(10,15,cyc);\nconst x=lerp(-16,sx+2,go)+leave*(w-sx+20);\nconst cy=cyc>3&&cyc<11?lerp(2,5,smoothstep(3,6,cyc))-(cyc>8?lerp(0,3,smoothstep(8,10,cyc)):0):-3;\nif(cy>-3){pixels((sx+15)*1,cy*2,'.dd.\\ndddd\\n.dd.',P);text(sx+11,cy,'{what}'.slice(0,20),'#fff');}\nfor(const b of bub){b.y-=b.v*dt;if(b.y<0){b.y=h*2;b.x=rand(t+b.x)*w}pixel(b.x,b.y,'#7fc8ff');}\nconst yy=2*h-9+Math.sin(t*2)*0.6;pixels(x-2,yy-2,sub.join('\\n'),P);\nconst c=clawd(x,h-5,{facing:cyc<10?1:1,stride:Math.floor(x/1.5)%4,scale:1});\npixels(c.top.x-3,c.top.py-3,'.ooo.\\nooooo',{o:'#9fe3ff'});\nif(cyc>5&&cyc<10)say(cyc<8?'Next stop: {what}. Mind the gap, and the squid.':'Capsule aboard! Surfacing with answers.',c.top.x,c.row);\n}","actors":[],"particles":[],"background":{"effect":"waves","palette":["#0a3a5a","#105070","#1a6a8a"],"speed":0.5,"intensity":0}}},
82 {"phase":"git","world":"circus","style":"pixel art","concept":"volcano island: lava flows branch and merge like git history while Clawd in a lei sets glowing commit-stones along them","source":"claude-opus-5-5","scene":{"concept":"volcano island: lava flows branch and merge like git history while Clawd in a lei sets glowing commit-stones along them","code":"const C=[[56,14],[66,10],[72,14],[76,10],[86,10],[88,14],[100,14],[112,14]];\nfunction frame(t,dt){for(let y=0;y<18;y++)for(let x=0;x<w;x+=1){if(y<16)continue;pixel(x,y,y==16&&(x+Math.floor(t*4))%6<2?'#9ce':'#2a5a9a')}\nfor(let y=3;y<16;y++){const hw=(y-3)*2.2;for(let x=Math.floor(28-hw);x<=28+hw;x++)pixel(x,y,y<5?'#f60':(noise2(x*.3,y*.3)>.6?'#7a4a2a':'#5a3a22'))}\nfor(let x=50;x<w;x++)pixel(x,15,'#3a2a1a');\nfor(let i=0;i<9;i++){const p=fract(t*.5+i/9);pixel(28+Math.sin(i*2)*p*12,3-p*3+p*p*2,mix('#ff0','#f40',p))}\nfor(let x=28;x<w;x++){const y=x<50?Math.floor(5+(x-28)*.42):14;pixel(x,y,mix('#f80','#f30',noise(x*.2-t)))}\nfor(let x=60;x<=94;x++){const y=x<64?14-(x-60):x>90?10+(x-90):10;pixel(x,y,mix('#fc0','#f60',noise(x*.2+t)))}\nconst n=Math.floor(t/2.2)%(C.length+2);for(let i=0;i<Math.min(n,C.length);i++){const[a,b]=C[i];pixels(a-1,b-1,'.#.\\n###\\n.#.',{'#':i==n-1?'#fff':'#ffd060'})}\nconst tg=C[Math.min(Math.max(n-1,0),C.length-1)];const cx=tg[0]-7,cy=(tg[1]==10?2:5)-4+1;\nconst c=clawd(cx,Math.max(cy,0),{facing:1,stride:Math.floor(t*6)%4,arms:fract(t/2.2)<.3?'up':'down'});\npixels(c.top.x-4,c.top.py+4,'o.y.o.y.o',{o:'#f6a',y:'#ff6'});\ntext(4,1,'┌'+'─'.repeat(22)+'┐','#c96');text(4,2,'│ '+'{what}'.padEnd(20)+' │','#ffd');text(4,3,'└'+'─'.repeat(22)+'┘','#c96');\nif(n>=C.length)say('Branch merged back to main. Lava cooled, history set in stone.',c.top.x,c.row);else if(n==2)say('Feature branch erupting off main. Keep it flowing!',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#331a33","#4a2244","#552211"],"speed":0.4,"intensity":0}}},
83 {"phase":"web","world":"zen rock garden","style":"3D","concept":"skate park 3D: Clawd skates a halfpipe, each run fetching a glowing parcel labeled {what} from a far ramp while a zen raked-sand pit sits be","source":"claude-opus-5-5","scene":{"concept":"skate park 3D: Clawd skates a halfpipe, each run fetching a glowing parcel labeled {what} from a far ramp while a zen raked-sand pit sits beside","code":"const rp=box(1,0.3,3),pk=box(0.5,0.5,0.5),stone=sphere(0.5,8),rail=cylinder(0.05,1,6);let got=0;function frame(t,dt){camera(Math.sin(t*0.1)*2,3,9,0,1,0,90);light(0.4,1,0.6,0.35);fog(10,40,'#000');for(let i=-9;i<=9;i++){const y=0.08*i*i/9;mesh3d(rp,{x:i,y:y,z:0,rz:Math.atan(0.18*i/9*9/9)*1.6,color:'#9ab0c8'})}for(let z=-6;z>-14;z-=1)line3d(-8,0.01,z,8,0.01,z,'#c8b890');mesh3d(stone,{x:-5,y:0.3,z:-9,color:'#a0a090'});mesh3d(stone,{x:4,y:0.4,z:-11,scale:1.4,color:'#b0a898'});mesh3d(rail,{x:0,y:0,z:-3,color:'#ccc'});const s=Math.sin(t*1.2),x=s*8,y=0.08*x*x/9+0.15;const cyc=Math.floor((t*1.2+Math.PI/2)/Math.PI);const hold=cyc%2==1;mesh3d(pk,{x:9.5,y:1.6,z:0,ry:t,color:hold?'#555':'#ffcc55'});const c=clawd3d(x,y,0,{facing:Math.cos(t*1.2)>0?1:-1,arms:'out',stride:0});if(c){if(hold){const p=project(x+0.6,y+1,0);if(p)text(p.x,p.y,'■',' #ffcc55'.trim());}if(mod(t,12)<4)say(hold?'Got {what}! Back for more.':'Dropping in for {what}...',c.top.x,c.row)}const q=project(9.5,2.3,0);if(q&&!hold)text(q.x-4,q.y,'{what}','#ffe9a0')}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#557","#99a"],"speed":0.4,"intensity":0}}},
84 {"phase":"web","world":"outer space","style":"pixel art","concept":"carnival midway: Clawd works a claw-crane game, fishing a prize labeled {what} out of the pile and into the chute, again and again","source":"claude-opus-5-5","scene":{"concept":"carnival midway: Clawd works a claw-crane game, fishing a prize labeled {what} out of the pile and into the chute, again and again","code":"const P=[[50,'#e55'],[58,'#5be'],[66,'#fd4'],[74,'#6d6'],[54,'#d6e'],[70,'#f94']];let got=0;\nfunction frame(t,dt){const T=t%8;for(let x=0;x<w;x++)put(x,0,x%4<2?'▼':'▽',hsl((x*20+t*90)%360,.8,.6));\nfor(let x=38;x<84;x++){put(x,1,'═','#c9a');put(x,8,'═','#c9a')}for(let y=1;y<9;y++){put(37,y,'║','#c9a');put(84,y,'║','#c9a')}\nfor(let i=0;i<14;i++){put(38+i*3.3,1,'●',hsl((i*40+t*200)%360,.9,.6))}\nfill(39,7,3,1,'▓','#777');text(39,2,'OUT','#fff');\nfor(let i=0;i<P.length;i++){if(i==2&&T>3.5)continue;pixels(P[i][0],13+(i%2),'.##.\\n####\\n.##.',{'#':P[i][1]})}\nlet cx=50,cy=3;if(T<2)cx=lerp(50,66,smoothstep(0,2,T));else if(T<5.5){cx=66;cy=T<3.5?lerp(3,10,smoothstep(2,3.5,T)):T<4?10:lerp(10,3,smoothstep(4,5.5,T))}else{cx=lerp(66,40,smoothstep(5.5,7,T));cy=3}\nline(cx+2,2,cx+2,cy/2,'│','#aaa');const op=T>3.4&&T<7?0:1;pixels(cx,cy,op?'#..#\\n#..#':'.##.\\n#..#',{'#':'#ddd'});\nif(T>3.5){let py=T<7?cy+3:cy+3+(T-7)*14;if(py<16){pixels(cx+0,py,'.##.\\n####\\n.##.',{'#':'#fd4'});text(cx-3,Math.floor(py/2)+2,'{what}','#ffe')}}\nfor(let x=86;x<w;x++)put(x,8,'▀','#634');fill(88,6,4,2,'█','#555');put(89,5,'●',T<2||T>5.5?'#e44':'#4e4');\nconst c=clawd(93,4,{facing:-1,arms:T<7?{left:'out',right:'down'}:'up',eyes:T>7?'wide':'open',blink:fract(t*.3)<.04});\npixels(c.top.x-3,c.top.py-2,'.#####.\\n#######',{'#':'#e33'});\nif(t%16>7&&t%16<11)say(T>7?'Ding! Fetched {what}, one token well spent.':'Steady... grabbing {what} off the web.',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#311","#413","#224"],"speed":0.6,"intensity":0}}},
85 {"phase":"git","world":"construction site","style":"pixel art","concept":"nature documentary: Clawd hides in a bush with binoculars as commit-birds lay eggs along a branching git tree, the feature branch arcing off","source":"claude-opus-5-5","scene":{"concept":"nature documentary: Clawd hides in a bush with binoculars as commit-birds lay eggs along a branching git tree, the feature branch arcing off and merging back into main","code":"const N=[[6,10],[16,10],[26,10],[40,4],[50,4],[36,10],[60,4],[48,10],[58,10],[80,10]];const L=['Shh. A rare commit nests on {what}.','The feature branch returns to main. Majestic.','Every egg a commit. Every one signed.','No merge conflicts this season. Beautiful.'];const P=2.4;function frame(t,dt){for(let x=0;x<w;x++){put(x,8,'▀','#3c7a3a','#244a22');if(rand(x)<0.3)put(x,7,rand(x+5)<0.5?',':'\"','#5fa856')}const B='#8a5a3a';for(let x=0;x<88;x++){pixel(x,10,B);pixel(x,11,'#6b432b')}for(let i=0;i<6;i++){pixel(30+i,10-i,B);pixel(66+i,4+i,B)}for(let x=36;x<66;x++)pixel(x,4,B);for(let i=0;i<40;i++){const x=Math.floor(rand(i)*86),y=Math.floor(rand(i+40)*3);pixel(x,y+(x>34&&x<66?1:7),i%2?'#4f9a48':'#72c060')}text(42,1,'{what}','#e8d48a');text(2,6,'main','#c9a27a');const C=N.length+2,c=Math.floor(t/P)%C,k=fract(t/P);for(let j=0;j<Math.min(c,N.length);j++){const[ex,ey]=N[j];pixels(ex-1,ey-2,'.o.\\nooo',{o:j==c-1?'#ffd166':'#f2efe6'})}if(c<N.length){const[nx,ny]=N[c],s=smoothstep(0,0.75,k),bx=lerp(118,nx-1,s),by=lerp(0,ny-5,s)+(s<1?Math.sin(t*12)*0.6:0);pixels(bx,by,Math.floor(t*8)%2?'#.#\\n.#.':'...\\n###',{'#':'#7ab8e8'})}const cl=clawd(98,4,{facing:-1,arms:{left:'up',right:'down'},blink:fract(t*0.3)<0.04});pixels(cl.left.x-3,cl.left.py-1,'##.##\\n#o#o#',{'#':'#333','o':'#9fd3ff'});pixels(94,13,'..gg.gGg..gg..gg..\\n.gGggGgggGgggGggg.\\ngggGgggggGgggggGgg',{g:'#3f8c3a',G:'#5fb050'});if(t>2)say(L[Math.floor((t-2)/9)%L.length],cl.top.x,cl.row)}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#1d3a4a","#2c5a4a","#3a6a5a"],"speed":0.4,"intensity":0.2}}},
86 {"phase":"web","world":"carnival midway","style":"3D","concept":"zen rock garden: Clawd rakes calm ripples toward a far boulder (the web), and an answer-pebble labeled {what} rolls back across the sand","source":"claude-opus-5-5","scene":{"concept":"zen rock garden: Clawd rakes calm ripples toward a far boulder (the web), and an answer-pebble labeled {what} rolls back across the sand","code":"const sand=plane(40,30),rk=sphere(1,10),pb=sphere(0.25,8);\nfunction ring(cx,cz,r,c){for(let i=0;i<24;i++){const a=i/24*6.283,b=(i+1)/24*6.283;line3d(cx+Math.cos(a)*r*1.4,0.02,cz+Math.sin(a)*r,cx+Math.cos(b)*r*1.4,0.02,cz+Math.sin(b)*r,c)}}\nfunction frame(t,dt){camera(Math.sin(t*0.1)*1.5,3,9,0,0.5,-2,80);light(0.4,1,0.6,0.35);fog(10,40,'#000');mesh3d(sand,{y:0,z:-4,color:'#d8cfb0'});\nfor(let z=-14;z<6;z+=1.2)line3d(-16,0.01,z,16,0.01,z,'#b5a985');\nfor(let j=0;j<4;j++)ring(5,-5,1.6+mod(t*0.8+j*1.3,5.2),'#8f8466');\nmesh3d(rk,{x:5,y:0.7,z:-5,scale:1.3,color:'#a0a098'});mesh3d(rk,{x:-1,y:0.4,z:-10,scale:0.8,color:'#8f9a88'});mesh3d(rk,{x:9,y:0.3,z:-1,scale:0.5,color:'#9a9488'});\nconst p=mod(t,10),k=smoothstep(4,8,p);\nif(p>3.5){const x=lerp(5,-3.6,k),z=lerp(-3.4,2.4,k),y=0.25+Math.abs(Math.sin(k*9))*0.4*(1-k);mesh3d(pb,{x,y,z,color:'#e8e2d0'});const q=project(x,y+0.6,z);if(q)text(q.x-5,q.y,'{what}','#fff7d6')}\nconst me=clawd3d(-5,0,2,{facing:1,arms:p>8?'up':'out',stride:p<3?Math.floor(t*4)%4:-1});\nline3d(-4.4,0.6,2,-3.2,0,2.6,'#8a5a30');line3d(-3.4,0,2.3,-3,0,2.9,'#8a5a30');\nif(me){if(p<3)say('One slow rake toward the web...',me.top.x,me.row);else if(p>8)say('{what} rolled right back to me.',me.top.x,me.row)}}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#2a3a4a","#3a5a5a","#4a4a6a"],"speed":0.3,"intensity":0}}},
87 {"phase":"web","world":"circus","style":"text art","concept":"beehive: Clawd as a worker bee flying out from the honeycomb to far meadow flowers, sipping nectar labeled {what} and hauling it home","source":"claude-opus-5-5","scene":{"concept":"beehive: Clawd as a worker bee flying out from the honeycomb to far meadow flowers, sipping nectar labeled {what} and hauling it home","code":"const fx=[0.45,0.65,0.85].map(k=>Math.floor(w*k));const fc=['#ff6fa8','#c58cff','#ffd34d'];let jars=0,last=-1;\nfunction frame(t,dt){const P=14,cyc=Math.floor(t/P),p=mod(t,P),i=cyc%3;if(cyc!=last){last=cyc;if(cyc>0)jars++;}\nfor(let x=0;x<w;x++)put(x,h-1,x%5==0?'\"':'_','#5a9a3a');\nconst hv=[' ╱⬡╲ ','╱⬡⬡⬡╲','⬡⬡⬡⬡⬡'];const hive=[' ▄███▄ ',' ▐▓▒▓▒▓▌ ','▐▒▓▒▓▒▓▒▌','▐▓▒▓▒▓▒▓▌','▐▒▓ ● ▓▒▌','▐▓▒▓▒▓▒▓▌',' ▀▀▀▀▀▀▀ '];sprite(3,1,hive.join('\\n'),'#e8a33a');text(4,8,'HONEY x'+jars,'#ffe08a');\nfor(let k=0;k<3;k++){const x=fx[k],sw=Math.round(Math.sin(t*1.5+k)*0.6);sprite(x+sw,h-5,' \\\\|/ \\n-(@)-\\n /|\\\\ ',fc[k]);for(let y=h-2;y>h-4;y--)put(x+2,y,'│','#5fbf4a');if(k==i)text(x+2-5,h-6,'{what}'.padStart(5),'#ffffff');}\nconst hx=12,tx=fx[i]-5,ty=h-8;let x,y;if(p<5){const e=smoothstep(0,5,p);x=lerp(hx,tx,e);y=2+Math.sin(e*Math.PI)*-1.5+e*(ty-2);}else if(p<9){x=tx;y=ty+Math.sin(p*3)*0.3;}else{const e=smoothstep(9,13.5,p);x=lerp(tx,hx,e);y=lerp(ty,2,e)-Math.sin(e*Math.PI)*1.5;}\ny=clamp(y,0,4);const dir=p<5?1:p<9?0:-1;const c=clawd(x,y,{facing:dir,color:'#e8b43a',arms:p>5&&p<9?'out':'down'});const wg=Math.floor(t*12)%2?'⌒ ⌒':'v v';text(c.top.x-1,Math.max(0,c.row-1),wg,'#cfefff');for(let s=1;s<13;s+=4)put(x+s,y+2,'▒','#3a2a10');\nif(p>9)put(c.left.x-1,c.left.row,'●','#ffcf40');\nif(p>5.5&&p<8.8)say(i==2?'Sweet! {what} has the good nectar.':'Sipping {what}... bit fizzy.',c.top.x,c.row);else if(p<3&&cyc==0)say('Off to the web meadow for {what}!',c.top.x,c.row);}","actors":[],"particles":[{"glyphs":"·*","count":8,"x":"8+noise(t*0.4+k)*20","y":"1+noise(t*0.6+k*3)*6","color":"#ffd34d"}],"background":{"effect":"aurora","palette":["#2a4a6a","#3a6a5a","#6a5a2a"],"speed":0.4,"intensity":0.2}}},
88 {"phase":"web","world":"desert caravan","style":"3D","concept":"deep sea: Clawd in a 3D bathysphere lowering a fishing line through glowing water to hook a lantern-fish carrying {what} back up","source":"claude-opus-5-5","scene":{"concept":"deep sea: Clawd in a 3D bathysphere lowering a fishing line through glowing water to hook a lantern-fish carrying {what} back up","code":"const sub=sphere(1,10),rock=cone(1.4,2,6),fish=sphere(0.35,6),kelp=cylinder(0.06,2.5,4);\nconst rocks=Array.from({length:10},(_,i)=>({x:-18+i*4+rand(i)*2,z:-4-rand(i+5)*10,s:0.6+rand(i+9)}));\nconst bub=Array.from({length:25},(_,i)=>({x:(rand(i)-0.5)*20,y:rand(i+3)*4,z:-rand(i+7)*8}));\nfunction frame(t,dt){camera(Math.sin(t*0.1)*2,2,9,0,1,0,90);light(0.3,1,0.5,0.35);fog(6,30,'#021428');\nfor(let x=0;x<w;x++)for(let y=0;y<h;y++)put(x,y,' ',null,mix('#03284a','#010812',y/h));\nmesh3d(plane(60,30),{y:-0.5,z:-5,color:'#7a6a4a'});\nfor(const r of rocks)mesh3d(rock,{x:r.x,y:-0.5,z:r.z,scale:r.s,color:'#8a8f9a'});\nfor(let i=0;i<8;i++)mesh3d(kelp,{x:-14+i*4,y:-0.5,z:-2-i%3,rz:Math.sin(t+i)*0.2,color:'#5fbf7a'});\nconst c=(t%20)/20,sy=3.2+Math.sin(t*0.8)*0.15;mesh3d(sub,{x:-3,y:sy,z:0,color:'#e8c070'});\nconst me=clawd3d(-3,sy-0.5,0.9,{size:0.7,facing:1,blink:fract(t*0.3)<0.05});\nconst ly=c<0.4?lerp(sy-0.2,0,c/0.4):c<0.6?0:lerp(0,sy-0.2,(c-0.6)/0.4);\nline3d(-2,sy,0.5,3,ly,0.5,'#ddd');\nconst hooked=c>=0.5;const fx=hooked?3:3+Math.cos(t*0.7)*6,fy=hooked?ly-0.3:0.6+Math.sin(t)*0.3;\nmesh3d(fish,{x:fx,y:fy,z:0.5,color:'#ff8fb0'});point3d(fx+0.4,fy+0.5,0.5,'#ffff80');\nconst p=project(fx,fy-0.6,0.5);if(p)text(p.x-5,p.y,'{what}','#ffe');\nfor(const b of bub){b.y+=dt*0.8;if(b.y>5)b.y=-0.5;point3d(b.x+Math.sin(t+b.z)*0.2,b.y,b.z,'#9fdfff');}\nif(me){const s=c<0.4?'Dropping a line for {what}...':c<0.6?'Nibble! Something with answers.':'Reeling it up. Fresh from the web!';say(s,me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"waves","palette":["#03284a","#0a4a7a","#1a6aa0"],"speed":0.5,"intensity":0}}},
89 {"phase":"web","world":"submarine","style":"3D","concept":"ancient tomb (3D): explorer Clawd treks down a pillared crypt to snatch the glowing {what} relic from its pedestal, then flees a rolling bou","source":"claude-opus-5-5","scene":{"concept":"ancient tomb (3D): explorer Clawd treks down a pillared crypt to snatch the glowing {what} relic from its pedestal, then flees a rolling boulder back toward the camera","code":"const pil=cylinder(0.4,3,8),ped=box(1,1,1),gem=sphere(0.35,8),bld=sphere(1.3,10),fl=plane(9,60),lin=box(9,0.4,0.6);\nfunction frame(t){const p=t%16;camera(0,2.6,7,0,1,-10,80);light(0.3,1,0.6,0.35);fog(6,32,'#000');mesh3d(fl,{z:-20,color:'#a08860'});\nfor(let i=0;i<8;i++){const z=2-i*4;mesh3d(pil,{x:-3.5,z,color:'#c8b080'});mesh3d(pil,{x:3.5,z,color:'#c8b080'});mesh3d(lin,{y:3.2,z,color:'#b09a70'});const q=project(-3,2.2,z+0.3);if(q)put(q.x,q.y,'♦',fract(t*3+i*0.37)<0.5?'#ffb030':'#ff6020')}\nmesh3d(ped,{y:0.5,z:-16,color:'#b8a890'});\nlet cz,arms='down',held=p>=10.5;if(p<10)cz=4-p*1.9;else if(p<11.5){cz=-15;arms='up'}else{cz=-15+(p-11.5)*5;arms='up'}\nconst gy=held?1.35:1.4,gz=held?cz:-16;mesh3d(gem,{y:gy,z:gz,color:'#ffe060',ry:t});\nif(p>12){const bz=-19+(p-12)*5;mesh3d(bld,{y:1.3,z:bz,rx:-bz,color:'#9a8a78'})}\nconst c=clawd3d(0,0,cz,{pose:'walk',facing:0,stride:Math.floor(t*(p>11.5?10:6))%4,arms});\nconst g=project(0,gy+0.6,gz);if(g&&!held)text(g.x-5,g.y,'{what}','#ffe9a0');\nif(c){if(p>2&&p<6)say('Fetching {what} from the deep archive...',c.top.x,c.row);else if(p>10.5&&p<12.2)say('Got {what}! ...why is the floor rumbling?',c.top.x,c.row);else if(p>12.4&&p<15)say('Response received! RUN!',c.top.x,c.row)}}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#3a2a10","#5a4020","#7a5a30"],"speed":0.5,"intensity":0}}},
90 {"phase":"agents","world":"bank heist","style":"3D","concept":"haunted house heist: Clawd waits in a moonlit hallway while ghost-helper subagents drift off through doors, returning with loot labeled {wha","source":"claude-opus-5-5","scene":{"concept":"haunted house heist: Clawd waits in a moonlit hallway while ghost-helper subagents drift off through doors, returning with loot labeled {what}","code":"const fl=plane(30,8),dr=box(1.2,2.2,0.2),bag=sphere(0.35,8),moon=sphere(1.2,12);\nconst gh=Array.from({length:4},(_,i)=>({x:-6+i*4,ph:rand(i)*6}));\nfunction frame(t,dt){camera(Math.sin(t*0.1)*2,2.2,8,0,1,-2,90);light(0.3,0.8,0.5,0.35);fog(8,30,'#000');\nmesh3d(fl,{y:0,z:-2,color:'#6a5a7a'});mesh3d(box(30,4,0.3),{y:2,z:-5,color:'#7a6a90'});\nmesh3d(moon,{x:8,y:5,z:-20,color:'#eeeedd',unlit:true});\nfor(const g of gh){mesh3d(dr,{x:g.x,y:1.1,z:-4.8,color:'#a07850'});\nconst c=fract(t*0.08+g.ph/6),k=c<0.5?c*2:2-c*2,z=-4.5+ (1-k)*4,y=1.2+Math.sin(t*2+g.ph)*0.2;\nmesh3d(sphere(0.5,10),{x:g.x,y:y,z:z,color:'#ddeeff',ascii:true});\nif(c>0.5){mesh3d(bag,{x:g.x+0.5,y:y-0.4,z:z,color:'#e0c050'});const p=project(g.x+0.5,y-1,z);if(p)text(p.x-3,p.y,'{what}','#ffd27a');}\nconst e=project(g.x,y+0.1,z);if(e)text(e.x-1,e.y,'o o','#222');}\nconst me=clawd3d(0,0,1.5,{facing:0,eyes:fract(t*0.3)<0.05?'closed':'wide',arms:Math.sin(t)>0.7?'up':'down'});\nif(me){const m=t%24;if(m>2&&m<8)say('Sent my ghosts through every door for {what}.',me.top.x,me.row);else if(m>13&&m<19)say('Boo-ty incoming. Still waiting on the slow ghost.',me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#2a2050","#403070","#205050"],"speed":0.4,"intensity":0}}},
91 {"phase":"agents","world":"deep sea","style":"text art","concept":"film noir city at night: Clawd as trench-coat detective under a streetlamp, sending three shadowy informants into the rainy alleys and colle","source":"claude-opus-5-5","scene":{"concept":"film noir city at night: Clawd as trench-coat detective under a streetlamp, sending three shadowy informants into the rainy alleys and collecting their tips on {what}","code":"const B=Array.from({length:16},(_,i)=>({x:30+i*6+Math.floor(rand(i)*3),hh:3+Math.floor(rand(i+7)*5),ww:4+Math.floor(rand(i+3)*3)}));\nconst L=['Three tails on {what}. Nobody sleeps tonight.','Word on the street: {what} has a past.','Keep digging, kid. Every alley talks.','The rain never stops. Neither do my agents.'];\nfunction frame(t,dt){for(const b of B){for(let y=h-1-b.hh;y<h-1;y++)for(let x=0;x<b.ww;x++){const lit=rand(b.x*31+x*7+y*13+Math.floor(t*0.3+b.x)*0.0)>0.7&&(x+y)%2==0;put(b.x+x,y,lit?'▪':'█',lit?'#e8c860':'#1e2433');}}\nfor(let x=0;x<w;x++)put(x,h-1,x%9==0?'┴':'─','#55606e');\ntext(2,0,'┌─────────┐','#c0485a');text(2,1,'│ HOTEL ★ │','#ff5a7a');text(2,2,'└─────────┘','#c0485a');\nfor(let y=1;y<h-1;y++)put(22,y,'│','#8a8f99');text(20,0,'╒═╕','#8a8f99');put(21,1,'▼','#ffe9a0');\nfor(let y=2;y<h-1;y++)put(21+(y%2),y,'░','#6a6040');\nfor(let i=0;i<60;i++){const x=Math.floor(rand(i)*w),y=mod(t*(10+rand(i+9)*5)+rand(i+3)*h,h);put(x-Math.floor(y*0.3),y,'/','#4a6a9a');}\nfor(let i=0;i<3;i++){const p=mod(t+i*5,15)/15;const out=p<0.5;const d=Math.sin(Math.PI*p);const x=26+d*(w-46)*(0.6+i*0.2);const back=!out;const c=clawd(x,h-5,{facing:out?1:-1,stride:Math.floor(x/1.5)%4,color:['#6a7080','#7a6a80','#6a8078'][i]});text(c.top.x-3,c.top.row-1,'▄███▄','#3a3f4a');if(back&&p<0.95)text(c.left.x-2,c.left.row,'{what}','#f0e6c0');}\nconst c=clawd(6,h-5,{facing:1,arms:{left:'down',right:mod(t,15)<1?'up':'down'},eyes:'open'});text(c.top.x-4,c.top.row-1,'▄▄███▄▄','#5a4a3a');text(c.x,c.row+2,'▓▓▓▓▓▓▓▓▓▓▓▓▓▓','#6b5a42');\nif(mod(t,4)<0.6)put(c.right.x+1,c.right.row-1,'~','#999');put(c.right.x,c.right.row,'═','#ddd');\nconst k=Math.floor(t/7)%L.length;if(mod(t,7)>1.5)say(L[k],c.top.x,c.row);}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#203048","#2a3a5a","#344868"],"speed":1,"intensity":0.2}}},
92 {"phase":"agents","world":"retro arcade game","style":"3D","concept":"bowling alley in 3D: Clawd bowls while helper Clawds in other lanes roll in parallel, pins labeled {what} crash and reset","source":"claude-opus-5-5","scene":{"concept":"bowling alley in 3D: Clawd bowls while helper Clawds in other lanes roll in parallel, pins labeled {what} crash and reset","code":"const pin=cylinder(0.12,0.7,8),ball=sphere(0.35,10),lane=box(2.2,0.1,30);const L=[-6,-2,2,6],cols=['#d97757','#6fa8dc','#8fd18f','#e0a0e0'];function frame(t,dt){camera(0,2.6,9,0,0.5,-12,80);light(0.3,1,0.6,0.35);fog(10,40,'#000');for(let i=0;i<4;i++){const x=L[i];mesh3d(lane,{x,y:-0.05,z:-8,color:'#c8a070'});line3d(x-1.2,0.05,7,x-1.2,0.05,-23,'#555');const per=4+i*0.7,ph=mod(t+i*1.3,per),k=ph/per,bz=lerp(4,-20,smoothstep(0.1,0.7,k)),hit=bz<-18.5;if(k<0.75)mesh3d(ball,{x:x+Math.sin(k*6+i)*0.3,y:0.35,z:bz,rx:-t*8,color:cols[i]});for(let r=0;r<4;r++)for(let c=0;c<=r;c++){const px=x+(c-r/2)*0.35,pz=-19-r*0.35,s=hit&&k<0.95?1:0,j=r*4+c;mesh3d(pin,{x:px+s*(rand(j+i)-0.5)*2*(k-0.7)*8,y:s*Math.max(0,(k-0.7)*3*rand(j)),z:pz,rz:s*(rand(j*3)-0.5)*3,color:'#f4f4f0'});}if(i>0)clawd3d(x,0,5.5,{size:0.6,color:cols[i],arms:k<0.15?'out':'down',facing:0});const p=project(x,1.4,-19);if(p)text(p.x-4,p.y,i==1?'{what}':'',cols[i]);}const me=clawd3d(-6,0,5.5,{size:0.7,arms:mod(t,4)<0.5?'up':'out',facing:0});const s=Math.floor(t/4);if(me&&mod(t,8)>4)say(s%2?'Every lane rolling at once. Strike on {what}!':'My helpers bowl, I keep score.',me.top.x,me.row);text(w-16,0,'FRAME '+(s%10+1)+' X', '#ffd866');}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#331144","#552266","#223366"],"speed":0.5,"intensity":0.15}}},
93 {"phase":"agents","world":"farm","style":"3D","concept":"beehive farm 3D: Clawd beekeeper waits by a hive as worker bees fly out to flower fields and return with nectar, honey jar filling","source":"claude-opus-5-5","scene":{"concept":"beehive farm 3D: Clawd beekeeper waits by a hive as worker bees fly out to flower fields and return with nectar, honey jar filling","code":"const hive=cylinder(1,1.8,8),roof=cone(1.2,0.8,8),jar=cylinder(0.5,1,10),fl=sphere(0.35,6),st=cylinder(0.05,0.8,4),gr=plane(60,30);const fls=[[-8,-6],[-5,-10],[7,-7],[10,-11],[-11,-12],[4,-13]];let honey=0;function frame(t,dt){camera(Math.sin(t*0.1)*2,2.6,9,0,1,-3,90);light(0.4,1,0.6,0.35);fog(10,40,'#000');mesh3d(gr,{y:0,z:-5,color:'#6a9a4a'});mesh3d(hive,{x:0,z:-3,color:'#e8c070'});mesh3d(roof,{x:0,y:1.8,z:-3,color:'#c89050'});for(let i=0;i<fls.length;i++){const[f,z]=fls[i];mesh3d(st,{x:f,z,color:'#5c5'});mesh3d(fl,{x:f,y:0.9,z,color:['#f7a','#fd5','#aaf'][i%3]})}honey=Math.min(1,honey+dt*0.012);mesh3d(jar,{x:3,z:-1,scale:1,color:'#cce'});mesh3d(cylinder(0.48,Math.max(0.05,honey),10),{x:3,y:0,z:-1,color:'#f0a020'});const pj=project(3,1.4,-1);if(pj)text(pj.x-5,pj.y-1,'{what}','#ffd');for(let b=0;b<12;b++){const[f,z]=fls[b%6];const k=0.5-0.5*Math.cos((t*0.25+rand(b))*Math.PI*2);const x=lerp(0,f,k),zz=lerp(-3,z,k),y=1.2+Math.sin(k*3.14)*1.5+Math.sin(t*9+b)*0.1;const p=project(x,y,zz);if(p)put(p.x,p.y,k>0.5?'●':'*',k>0.5?'#fd3':'#fa0')}const c=clawd3d(-3,0,-1,{facing:1,arms:Math.sin(t)>0.6?'up':'down'});if(c){pixels(c.top.x-4,c.top.py-3,'.wwwwwww.\nwwwwwwwww','#eee'.length?{w:'#eee'}:{});const m=Math.floor(t/12)%4;const L=['Twelve workers out on {what}. I just hold the smoker.','Bees: parallel. Me: patiently sequential.','Honey jar filling. Each drop is a finished subtask.','Buzz buzz. That is agent for \"still on it.\"'];if(t%12>2)say(L[m],c.top.x,c.row)}}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#335","#446","#553"],"speed":0.3,"intensity":0}}},
94 {"phase":"agents","world":"construction site","style":"3D","concept":"snow globe construction site: Clawd foreman in a snow globe watches helper crews raise a tower crate by crate while snow swirls","source":"claude-opus-5-5","scene":{"concept":"snow globe construction site: Clawd foreman in a snow globe watches helper crews raise a tower crate by crate while snow swirls","code":"const bx=box(1,0.6,1),pole=cylinder(0.1,3.2,6),base=cylinder(3.4,0.4,16),dome=sphere(3.6,14);\nconst crews=[-2.2,0,2.2];\nconst sn=Array.from({length:60},(_,i)=>({a:rand(i)*6.28,r:rand(i+7)*3.2,y:rand(i+3)*4}));\nfunction frame(t,dt){\nconst a=t*0.15;camera(Math.sin(a)*8,2.6,Math.cos(a)*8,0,1.3,0,80);light(0.5,1,0.6,0.35);fog(8,30,'#000');\nmesh3d(base,{y:-0.4,color:'#b08060'});\nmesh3d(dome,{y:1.4,color:'#9cf',wire:true});\ncrews.forEach((x,i)=>{const cyc=8,ph=t+i*2.6,n=Math.floor(ph/cyc),f=fract(ph/cyc);const st=Math.min(5,n+1);for(let k=0;k<st;k++)mesh3d(bx,{x,y:0.3+k*0.6,z:-1,color:hsl(30+i*90,0.6,0.6)});\nmesh3d(pole,{x:x+0.8,z:-1,color:'#fc4'});line3d(x+0.8,3.2,-1,x-0.2,3.2,-1,'#fc4');\nconst ty=0.3+st*0.6,cy=lerp(3,ty,smoothstep(0.2,0.8,f));line3d(x,3.2,-1,x,cy+0.3,-1,'#ccc');mesh3d(bx,{x,y:cy,z:-1,color:hsl(30+i*90,0.6,0.75)});\nconst p=project(x,cy+0.6,-1);if(p&&i==1)text(p.x-5,p.y,'{what}','#fff');});\nfor(const s of sn){s.y-=dt*0.5;s.a+=dt*0.3;if(s.y<0)s.y=4;point3d(Math.cos(s.a)*s.r,s.y,Math.sin(s.a)*s.r,'#fff');}\nconst me=clawd3d(0,0,1.4,{facing:0,arms:fract(t/8)>0.85?'up':'down',size:0.8});\nif(me){pixels(me.top.x-3,me.top.py-3,'.yyyyy.\\nyyyyyyy','#fd3'.length?{y:'#fd3'}:{});\nconst L=['Three crews, one {what}. I just hold the clipboard.','Somebody shook the globe. Work continues.','Another floor up. Helpers never sleep.'];if(t>2)say(L[Math.floor(t/10)%3],me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#335","#668","#aac"],"speed":0.3,"intensity":0.2}}},
95 {"phase":"agents","world":"ski slope","style":"3D","concept":"weather report: Clawd as TV weather presenter pointing at a 3D map where four subagent storm fronts spiral in toward a {what} tower","source":"claude-opus-5-5","scene":{"concept":"weather report: Clawd as TV weather presenter pointing at a 3D map where four subagent storm fronts spiral in toward a {what} tower","code":"const pl=plane(18,9),cl=sphere(0.5,8),tw=box(0.6,1.8,0.6),sun=sphere(0.4,8)\nlet cs=Array.from({length:4},(_,i)=>({a:i*1.57,r:4+i*0.8})),done=0,fl=0\nconst L=['Four agent fronts moving in on {what}.','Heavy scattered parallel work today.','Front landed! Clear skies over {what}.','Expect more patches by afternoon.']\nfunction frame(t,dt){camera(4+Math.sin(t*0.15)*1.5,4,7,4,0.5,-2,75);fog(8,30,'#000');light(0.4,1,0.6,0.35)\nmesh3d(pl,{x:4,z:-2,color:'#5f9a5a'});mesh3d(tw,{x:4,y:0,z:-2,color:'#c8d0e0'})\nconst tp=project(4,2.1,-2);if(tp)text(tp.x-5,tp.y-1,'{what}','#ffe08a')\ncs.forEach((c,i)=>{c.r-=dt*0.35;c.a+=dt*0.25;if(c.r<0.9){c.r=6;done++;fl=1}\nconst x=4+Math.cos(c.a)*c.r,z=-2+Math.sin(c.a)*c.r*0.6\nmesh3d(cl,{x,y:1.6,z,scale:1.4,color:'#e8eef8'});mesh3d(cl,{x:x+0.5,y:1.5,z,scale:1,color:'#cfd8e8'})\nconst p=project(x,2.5,z);if(p)text(p.x-3,p.y,'agent'+(i+1),hsl(200+i*40,0.7,0.7))})\nfl=Math.max(0,fl-dt*1.5);if(fl>0){mesh3d(sun,{x:4,y:2.6,z:-2,scale:1+fl,color:'#ffd040',unlit:true});if(tp)text(tp.x+3,tp.y-1,'\\u2713'+done,'#8f8')}\nfill(0,0,30,9,' ',null,'#101828');text(2,0,'WEATHER 7','#7cc8ff');text(22,8,'LIVE','#ff6060')\nconst c=clawd(4,4,{facing:1,arms:{left:'down',right:Math.sin(t*2)>0?'up':'out'},blink:fract(t*0.3)<0.04})\npixels(c.eyes.x-4,c.eyes.py+3,'..rr..\\n...r..',{r:'#d03040'})\nsay(L[Math.floor(t/6)%4],c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#778"],"speed":0.3,"intensity":0}}},
96 {"phase":"agents","world":"circus","style":"pixel art","concept":"cyberpunk street, pixel art: Clawd as a rain-soaked fixer under a neon {what} sign, dispatching courier drones that zip off into the megacit","source":"claude-opus-5-5","scene":{"concept":"cyberpunk street, pixel art: Clawd as a rain-soaked fixer under a neon {what} sign, dispatching courier drones that zip off into the megacity and return with data packets","code":"const B=Array.from({length:30},(_,i)=>({x:i*5,hh:3+Math.floor(rand(i)*12),c:rand(i+7)<0.5?'#2a2350':'#1d3048'}));\nconst R=Array.from({length:50},(_,i)=>({x:rand(i+3)*w,y:rand(i+9)*h,v:8+rand(i)*6}));\nconst D=[0,1,2].map(i=>({o:i*4,c:['#ff4fd8','#3ef0ff','#ffe14a'][i],yy:2+i*2}));\nconst L=['Three drones out on {what}. Fixer waits.','Runners are fast. Rain is faster.','Packet back! Two more in the wind.','Night shift: I dispatch, they deliver.'];\nfunction frame(t,dt){\nfor(const b of B){const top=2*h-2-b.hh;fill(b.x,Math.floor(top/2),5,h,' ',null,b.c);for(let py=top+2;py<2*h-3;py+=3)for(let k=1;k<4;k+=2)if(rand(b.x*7+py*3+k)<0.4)pixel(b.x+k,py,rand(py+b.x)<0.5?'#ff6ad5':'#5ff');}\nfill(0,h-1,w,1,'▀','#555a70','#151522');\nfor(let x=0;x<w;x+=9)put(x,h-1,'▄','#ff4fd8','#151522');\nfill(8,0,26,1,' ',null,'#401040');text(9,0,'NEON//'+'{what}'.slice(0,18),fract(t*0.7)<0.05?'#602060':'#ff7af0','#401040');\nfill(12,3,22,1,'▀','#6a3fa0');\nfor(const r of R){r.y+=r.v*dt;if(r.y>h-1){r.y=0;r.x=Math.random()*w;}put(r.x,r.y,'╎','#4a6fa8');}\nconst c=clawd(18,h-5,{facing:1,arms:fract(t/4)<0.15?{left:'down',right:'up'}:'down',blink:fract(t*0.3)<0.04,look:{x:2,y:0}});\npixels(c.eyes.x-1,c.eyes.py-1,'##########',{'#':'#3ef0ff'});\nfor(const d of D){const p=fract((t+d.o)/12),out=p<0.5,k=out?smoothstep(0,0.5,p):1-smoothstep(0.5,1,p);const x=35+k*(w-30),y=d.yy*2+Math.sin(t*3+d.o)*1;pixels(x,y,'.##.\\n####\\n#..#',{'#':d.c});if(!out)pixels(x+1,y+3,'oo',{'o':'#fff'});if(k<0.98)line(x+4,Math.floor(y/2),x+4+Math.min(6,k*20),Math.floor(y/2),'·',d.c);}\nconst li=Math.floor(t/7)%L.length;if(fract(t/7)<0.7)say(L[li],c.top.x,c.row);\n}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#203060","#402060","#305070"],"speed":1,"intensity":0}}},
97 {"phase":"writing","world":"mad science lab","style":"3D","concept":"lighthouse in a storm: Clawd as keeper atop a 3D lighthouse, beam sweeping the sea, guiding the {what} ship safely to harbor as the reply is","source":"claude-opus-5-5","scene":{"concept":"lighthouse in a storm: Clawd as keeper atop a 3D lighthouse, beam sweeping the sea, guiding the {what} ship safely to harbor as the reply is written","code":"const tw=cylinder(0.9,4,10),cap=cone(1.1,0.8,10),lamp=cylinder(0.6,0.6,8),rock=sphere(1.6,8),hull=box(1.6,0.5,0.6),mast=box(0.08,1.2,0.08)\nfunction frame(t,dt){camera(0,3,11,0,2.5,0,90);fog(8,40,'#000');light(0.5,1,0.6,0.35)\nfor(let x=0;x<w;x++){const y=h-1-Math.round(noise2(x*0.08,t*0.6)*1.5);for(let yy=y;yy<h;yy++)put(x,yy,yy==y?'~':'≈',yy==y?'#9cd':'#3a6a9a')}\nfor(let i=0;i<40;i++){const rx=mod(rand(i)*w+t*14+i*3,w),ry=mod(rand(i+7)*h+t*18,h);put(rx,ry,'/','#7890b0')}\nif(fract(t*0.13)<0.03)fill(0,0,w,2,' ',null,'#334')\nmesh3d(rock,{x:-4,y:-1,z:0,color:'#889'});mesh3d(tw,{x:-4,y:0,z:0,color:'#e8e0d8'});mesh3d(box(1.85,0.5,1.85),{x:-4,y:1.6,z:0,color:'#c33'});mesh3d(lamp,{x:-4,y:4,z:0,color:'#ffd860',unlit:true});mesh3d(cap,{x:-4,y:4.6,z:0,color:'#c33'})\nconst a=t*0.9,p=project(-4,4.3,0);if(p){const dx=Math.cos(a),len=Math.abs(dx)*70;for(let i=3;i<len;i++){const x=p.x+Math.sign(dx)*i,y=p.y+i*0.03*Math.sin(a*2);put(x,y,'═',mix('#fff6a0','#665520',i/len))}}\nconst k=clamp(t/50,0,1),sx=lerp(9,-1,k),bob=Math.sin(t*1.7)*0.2;mesh3d(hull,{x:sx,y:0.1+bob,z:2,rz:bob*0.5,color:'#b07040'});mesh3d(mast,{x:sx,y:0.9+bob,z:2,color:'#ddd'});mesh3d(box(0.6,0.6,0.05),{x:sx+0.3,y:1.1+bob,z:2,color:'#eee'})\nconst q=project(sx,2,2);if(q)text(q.x-4,q.y-1,'{what}','#ffd')\nconst c=clawd(p?p.x+5:40,p?p.y-1:1,{facing:-1,arms:{left:'out',right:Math.sin(t*3)>0?'up':'out'},scale:1});pixels(c.top.x-3,c.top.py-3,'.yyyyy.\\nyyyyyyy','{\"y\":\"#ffcc33\"}'.length?{y:'#ffcc33'}:{})\nconst L=['Keep that light turning, {what} is almost home!','Storm says no. Beam says {what}, this way.','Writing up the logbook while the ship docks.'];if(t>2)say(L[Math.floor(t/12)%3],c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#334466","#556688","#223344"],"speed":1.5,"intensity":0.2}}},
98 {"phase":"writing","world":"wild west","style":"text art","concept":"wild west mail room: Clawd as Pony Express clerk sealing the reply letter, stamping it, and handing it to a galloping courier at sunset","source":"claude-opus-5-5","scene":{"concept":"wild west mail room: Clawd as Pony Express clerk sealing the reply letter, stamping it, and handing it to a galloping courier at sunset","code":"let p=0\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,8,x%6==0?'┴':'═','#8a6a4a')}\nfor(let y=0;y<8;y++)for(let x=0;x<w;x+=3){if(noise2(x*0.05,y*0.3+t*0.05)>0.8)put(x,y,'.',mix('#ff9a50','#7a3a6a',y/8))}\ndisc(w-18,7,3,' ',null,'#c8552a')\nsprite(2,1,'╔══════════════╗\\n║ PONY EXPRESS ║\\n╠══╦══╦══╦══╦══╣\\n║▒▒║▓▓║▒▒║▓▓║▒▒║\\n║▓▓║▒▒║▓▓║▒▒║▓▓║\\n║▒▒║▓▓║▒▒║▓▓║▒▒║\\n╚══╩══╩══╩══╩══╝','#c08850')\nsprite(22,4,'┌──────────────────┐\\n│ │\\n└┬────────────────┬┘\\n │ │','#a07040')\nconst c=t%14\nconst cx=26\nconst cl=clawd(cx,0.5,{facing:1,arms:c<4&&fract(t*2)<0.5?'up':'out'})\nsprite(cl.top.x-3,0,' ▄▄▄ \\n▀▀▀▀▀','#7a5030')\nconst sealed=c>4\nif(c<9)sprite(44,4,sealed?'┌─✦──┐\\n│ ★ │':'┌────┐\\n│\\\\ /│','#f4e6c0')\nif(c<9)text(43,6,'{what}'.slice(0,20),'#ffd27a')\nlet hx=lerp(w+10,-40,smoothstep(8,13,c))\nconst g=Math.floor(t*8)%2\nsprite(hx,3,g?' ▄▄ \\n ▄███████▄▄ \\n▀ █▀ █▀ \\n ▀ ▀ ':' ▄▄ \\n ▄███████▄▄ \\n █▀ ▀█ \\n ▀ ▀ ','#b07a50')\nsprite(hx+3,1,' ▄█▄\\n █▓█','#e0c090')\nif(c>9.5&&c<13)sprite(hx+1,3,'✉','#f4e6c0')\nfor(let i=0;i<5;i++)put(hx+12+i*2,7-((t*5+i)%2),'░','#9a7a5a')\nif(c>1&&c<4)say('Sealing up the reply on {what}...',cx+7,0)\nelse if(c>5&&c<8)say('Stamped! Rider\\'s due any minute.',cx+7,0)\nelse if(c>9&&c<12)say('Ride fast, partner!',cx+7,0)\n}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#5a2a3a","#a0502a","#d08040"],"speed":0.4,"intensity":0}}},
99 {"phase":"writing","world":"mad science lab","style":"text art","concept":"ufo abduction over a mad science lab: Clawd beams the finished {what} up into a saucer while a lab tesla coil sparks below","source":"claude-opus-5-5","scene":{"concept":"ufo abduction over a mad science lab: Clawd beams the finished {what} up into a saucer while a lab tesla coil sparks below","code":"let c0=0\nfunction frame(t,dt){\nfor(let x=0;x<w;x++)put(x,h-1,x%9==0?'┴':'─','#5a6b7c')\nfor(let i=0;i<30;i++)put(Math.floor(rand(i)*w),Math.floor(rand(i+7)*3),fract(t*0.5+rand(i+3))<0.1?'✦':'.','#8899cc')\nsprite(4,h-5,' ┌┴┐ \n │≈│ \n │≈│ \n═╧═╧═','#7fd1b9')\nif(fract(t*1.3)<0.3){line(7,h-6,10+rand(Math.floor(t*9))*6,h-8,'~','#bfe8ff')}\nconst sx=lerp(-30,w*0.5-12,smoothstep(0,4,t))+(t>4?Math.sin(t*0.6)*6:0)\nsprite(sx,0,' ▄▄████▄▄ \\n◢████◆██◆██◆████◣','#9fe870')\nconst on=t>4.5\nconst cx=sx+2\nif(on){for(let y=2;y<h-1;y++){const s=(y-2)*0.6;text(Math.round(cx+1-s),y,'░'.repeat(Math.round(12+s*2)),y%2?'#e8ff9a':'#c8f070')}}\nconst p=fract((t-5)/6)\nconst cy=on&&t>5?lerp(h-6,2,smoothstep(0,0.8,p)):h-6\nconst crx=w*0.75-14\nconst ky=h-5\nclawd(crx,ky,{facing:-1,arms:on?'up':'out',eyes:on?'wide':'open'})\nif(on&&t>5&&p<0.85){text(Math.round(cx+3),Math.round(cy),'[{what}]','#ffd479')}\nif(!on||t<=5){text(Math.round(cx+3),h-2,'[{what}]','#ffd479')}\nif(t>2&&t<5)say('Ship it? They are literally shipping it.',crx+7,ky)\nif(t>7)say('Beam up {what}. Tell the mothership: tests pass.',crx+7,ky)\n}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#99a"],"speed":0.4,"intensity":0.2}}},
100 {"phase":"writing","world":"museum after hours","style":"3D","concept":"construction site at dusk: Clawd in a hard hat foreman-waving as a 3D crane stacks the final blocks of the {what} tower, topping out with a ","source":"claude-opus-5-5","scene":{"concept":"construction site at dusk: Clawd in a hard hat foreman-waving as a 3D crane stacks the final blocks of the {what} tower, topping out with a flag","code":"const bk=box(1.6,0.5,1.2),mast=cylinder(0.15,5,6),gr=plane(40,14),fl=box(0.05,0.6,0.05);\nfunction frame(t,dt){camera(0,3,12,0,2,0,70);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nmesh3d(gr,{color:'#7a6a50'});mesh3d(mast,{x:7,color:'#e8c040'});line3d(7,5,0,-2,5,0,'#e8c040');line3d(7,5,0,9,4.6,0,'#e8c040');line3d(7,5.6,0,-2,5,0,'#c9a030');line3d(7,5.6,0,9,4.6,0,'#c9a030');mesh3d(box(0.8,0.6,0.6),{x:9,y:4.4,color:'#999'});\nconst cy=t%32,n=Math.floor(cy/4),p=(cy%4)/4;\nfor(let i=0;i<Math.min(n,7);i++)mesh3d(bk,{x:0,y:0.25+i*0.5,color:i%2?'#d0d4dc':'#b8c4d8'});\nlet bx=6,by=0.25,top=0.25+n*0.5;\nif(n<7){if(p<0.3)by=lerp(0.25,4.3,smoothstep(0,0.3,p));else if(p<0.65){by=4.3;bx=lerp(6,0,smoothstep(0.3,0.65,p))}else{bx=0;by=lerp(4.3,top,smoothstep(0.65,1,p))}\nline3d(bx,5,0,bx,by+0.25,0,'#ddd');mesh3d(bk,{x:bx,y:by,ry:Math.sin(t*2)*0.15*(p>0.3&&p<0.65),color:'#f0a050'});mesh3d(bk,{x:6,y:0.25,z:1.5,color:'#f0a050'})}\nelse{mesh3d(fl,{x:0,y:3.8,color:'#fff'});const f=project(0.3,4.0,0);if(f)text(f.x,f.y,'▶',hsl(t*90%360,0.8,0.6))}\nconst lp=project(0,Math.min(n,7)*0.5-0.1,0.7);if(lp&&n>0)text(lp.x-5,lp.y,'{what}'.slice(0,20),'#222');\nconst c=clawd3d(-4.5,0,1,{facing:1,arms:n>=7?(Math.floor(t*3)%2?'up':'out'):{left:'down',right:p>0.6?'up':'out'},size:1.1});\nif(c){pixels(c.top.x-4,c.top.py-3,'..yyyyy..\\n.yyyyyyy.\\nyyyyyyyyy','{\"y\":\"#ffd02a\"}'.length?{y:'#ffd02a'}:{});\nconst L=n>=7?'Topped out! {what} is built, flag and all.':n<2?'Laying the foundation for {what}...':n<5?'Easy... a little left... perfect.':'One more floor and {what} is done.';say(L,c.top.x,c.row-1)}}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#3a2a5a","#7a3a5a","#c06040"],"speed":0.4,"intensity":0.2}}},
101 {"phase":"writing","world":"museum after hours","style":"3D","concept":"museum heist after hours: Clawd lowered on a rope into a 3D gallery, laser beams sweeping, to swap {what} onto the pedestal as the finished ","source":"claude-opus-5-5","scene":{"concept":"museum heist after hours: Clawd lowered on a rope into a 3D gallery, laser beams sweeping, to swap {what} onto the pedestal as the finished exhibit","code":"const ped=box(1.4,1.2,1.4),gem=sphere(0.45,10),col=cylinder(0.35,3.2,8),fl=plane(40,20),fr=box(2.2,1.4,0.1);\nfunction frame(t,dt){camera(Math.sin(t*0.15)*3,2.6,9,0,1.2,0,85);light(0.4,1,0.6,0.35);fog(8,32,'#000');\nmesh3d(fl,{y:0,color:'#7a6a90'});\nfor(let i=-3;i<=3;i++){if(i==0)continue;mesh3d(col,{x:i*3.2,z:-3,color:'#c8c0b0'});}\nfor(let i=-2;i<=2;i+=4)mesh3d(fr,{x:i*2.4,y:2.2,z:-4,color:'#d4a84a'});\nmesh3d(ped,{y:0.6,color:'#e0dcd0'});\nconst cyc=t%24,low=smoothstep(2,6,cyc)-smoothstep(16,20,cyc);const cy=4.2-low*2.4;\nline3d(0,6,0.6,0,cy+0.9,0.6,'#bbb');\nconst placed=cyc>9&&cyc<22;\nmesh3d(gem,{y:placed?1.7:cy+0.2,z:placed?0:0.6,ry:t,color:'#6fe0ff',ascii:true});\nfor(let k=0;k<3;k++){const a=t*0.7+k*2.1,x=Math.sin(a)*7;line3d(x,0.2+k*0.5,3,-x,0.6+k*0.4,-3,'#ff3344');}\nconst me=clawd3d(0,cy,0.6,{arms:low>0.9?'down':'up',color:'#d97757',eyes:cyc>9&&cyc<14?'wide':'open'});\nconst p=project(0,1.3,0.75);if(p)text(p.x-5,p.y,'{what}'.slice(0,12),'#ffd27a');\nif(me){me.top&&pixels(me.top.x-3,me.top.py-2,'#######\\n#.o#o.#','#222'.length?{'#':'#222','o':'#fff'}:{});\nif(cyc>10&&cyc<16)say('{what} is on the pedestal. Nobody touch it.',me.top.x,me.row);else if(cyc>19)say('Lasers dodged. Exhibit installed. Smooth.',me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#201030","#301040","#402050"],"speed":0.5,"intensity":0}}},
102 {"phase":"writing","world":"haunted house","style":"text art","concept":"dinosaur era: Clawd chisels the final reply onto a stone tablet while a long-necked dino peers over a volcano skyline","source":"claude-opus-5-5","scene":{"concept":"dinosaur era: Clawd chisels the final reply onto a stone tablet while a long-necked dino peers over a volcano skyline","code":"function frame(t,dt){for(let x=0;x<w;x++){put(x,8,x%5?'▀':'▄','#7a5a3a');}sprite(70,2,' ▄▄▄\\n ▟███▙\\n ▟█████▙\\n ▟███████▙\\n▟█████████▙','#8a4a3a');let e=fract(t*0.4);for(let i=0;i<5;i++)put(74+i+Math.sin(t*3+i)*2,1-e*3+i*0.3,'*','#ff9a3a');const nx=95+Math.sin(t*0.3)*3;sprite(nx,0,' ▄▄▖\\n ▐▀▀ \\n ▐ \\n ▐ \\n▄██▄▄▄\\n██████▄\\n▐▌ ▐▌ ','#7ec27e');fill(30,3,22,5,' ',null,'#6a6a72');text(31,3,'┌'+'─'.repeat(18)+'┐','#c8c8d0','#6a6a72');const n=Math.min(17,Math.floor(t*1.2));text(32,4,'{what}'.slice(0,n),'#fff6c0','#6a6a72');let lines=Math.floor(t/4)%3+1;for(let l=0;l<lines&&l<2;l++)text(32,5+l,'≈≈ ≈≈≈ ≈≈'.slice(0,Math.min(9,Math.floor(t*2)%12)),'#e0e0e8','#6a6a72');text(31,7,'└'+'─'.repeat(18)+'┘','#c8c8d0','#6a6a72');const hit=fract(t*2)<0.3;const c=clawd(12,4,{facing:1,arms:{left:'down',right:hit?'down':'up'}});text(c.right.x,c.right.row-1,hit?'⚒':' ⚒','#d0b080');if(hit)put(29,4+Math.floor(rand(Math.floor(t*2))*3),'✦','#ffe080');if(fract(t/12)<0.4)say('Carving the reply in stone. {what}, now fossil-proof.',c.top.x,c.row);else if(fract(t/12)>0.6&&fract(t/12)<0.85)say('Typing was invented later. Stay tuned.',c.top.x,c.row);}","actors":[],"particles":[{"glyphs":"·.","count":12,"x":"rand(k)*w","y":"mod(t*0.5+rand(k+3)*8,8)","color":"#a08060"}],"background":{"effect":"fire","palette":["#3a1a10","#6a2a10","#a04a20"],"speed":0.5,"intensity":0.15}}},
103 {"phase":"writing","world":"snow globe","style":"3D","concept":"underground mine inside a snow globe: Clawd in a hard hat pushes a minecart down glowing rails, hauling the finished {what} gem to the surfa","source":"claude-opus-5-5","scene":{"concept":"underground mine inside a snow globe: Clawd in a hard hat pushes a minecart down glowing rails, hauling the finished {what} gem to the surface while snow drifts down","code":"const cart=box(1.4,0.8,1),gem=sphere(0.35,6),beam=box(0.25,3,0.25),top=box(5,0.25,0.4),rock=sphere(1,5);\nconst fl=Array.from({length:50},(_,i)=>({x:rand(i)*20-10,y:rand(i+7)*4,z:-rand(i+3)*14}));\nfunction frame(t,dt){camera(0,2,8,0,1,-10,90);fog(6,30,'#000');light(0.3,1,0.6,0.35);\nconst s=t*2;for(let i=0;i<8;i++){const z=-i*5+mod(s,5)+2;mesh3d(beam,{x:-2.5,y:1.5,z,color:'#b08050'});mesh3d(beam,{x:2.5,y:1.5,z,color:'#b08050'});mesh3d(top,{x:0,y:3,z,color:'#a07040'});point3d(0,2.7,z,'#ffd070');}\nfor(let i=0;i<6;i++){mesh3d(rock,{x:i%2?-5:5,y:0.5,z:-i*6+mod(s,6),scale:1.5,color:'#8a7a6a'});}\nline3d(-0.6,0,10,-0.6,0,-40,'#ccc');line3d(0.6,0,10,0.6,0,-40,'#ccc');\nfor(let i=0;i<20;i++){const z=-i*2+mod(s,2)+4;line3d(-0.9,0,z,0.9,0,z,'#7a5a3a');}\nconst bob=Math.sin(t*8)*0.03;mesh3d(cart,{x:0,y:0.5+bob,z:1,color:'#9aa4b0'});\nmesh3d(gem,{x:0,y:1.1+bob,z:1,ry:t*2,color:hsl(mod(t*40,360),0.7,0.65)});\nconst p=project(0,1.7,1);if(p)text(p.x-5,p.y-1,'{what}','#fff4c0');\nconst c=clawd3d(0,0,3.2,{facing:0,stride:Math.floor(t*6)%4,arms:'out',size:0.9});\nif(c){pixels(c.top.x-3,c.top.py-3,'.yyyyy.\\nyyyyyyy\\n.y#yyy.',{y:'#ffcc33','#':'#fff'});\nconst m=mod(t,24);if(m>2&&m<8)say('Hauling {what} up. Mined clean!',c.top.x,c.row);else if(m>13&&m<19)say('Snowing in a mine? Must be a snow globe.',c.top.x,c.row);}\nfor(const f of fl){f.y-=dt*0.6;if(f.y<0)f.y=4;const q=project(f.x,f.y,f.z);if(q)put(q.x,q.y,'*','#eef6ff');}\nfor(let x=0;x<w;x++){const e=Math.abs(x-w/2)/(w/2);if(e>0.92)put(x,0,'░','#aaccff');}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#778"],"speed":0.3,"intensity":0}}},
104 {"phase":"writing","world":"desert caravan","style":"3D","concept":"airport runway: Clawd as ground marshaller waving off a 3D jet carrying the {what} reply, taking off again and again into the dusk","source":"claude-opus-5-5","scene":{"concept":"airport runway: Clawd as ground marshaller waving off a 3D jet carrying the {what} reply, taking off again and again into the dusk","code":"const fus=box(1.1,1.1,6),wg=box(7,0.2,1.6),tl=box(0.2,1.4,1.2),rw=plane(8,140),tw=box(1,4,1),cab=box(2,1,2);\nconst L=['Reply to {what}: cleared for takeoff!','Wheels up. Words on board.','Next flight boarding: more {what} notes.','Smooth climb. No turbulence in the diff.'];\nfunction frame(t,dt){camera(0,2.2,9,0,1.2,-20,90);fog(10,70,'#120a1a');light(0.3,1,0.6,0.35);\nconst T=mod(t,14),n=Math.floor(t/14);\nmesh3d(rw,{x:0,y:0,z:-60,color:'#8a8a94'});\nfor(let i=0;i<24;i++){const z=6-i*6;line3d(0,0.02,z,0,0.02,z-3,'#ffffff');point3d(-4.2,0.1,z,i%2?'#ffd060':'#60c0ff');point3d(4.2,0.1,z,i%2?'#ffd060':'#60c0ff');}\nmesh3d(tw,{x:9,y:2,z:-25,color:'#b0b8c8'});mesh3d(cab,{x:9,y:4.5,z:-25,color:'#80d0e0'});\nconst a=smoothstep(1.5,10,T),z=4-a*a*90,y=Math.max(0,(-z-18)*0.25),p=-Math.min(0.25,Math.max(0,(-z-18)*0.02));\nif(T<11){mesh3d(fus,{x:0,y:0.9+y,z,rx:p,color:'#e8eef4'});mesh3d(wg,{x:0,y:0.9+y,z:z+0.3,rx:p,color:'#c8d0dc'});mesh3d(tl,{x:0,y:1.9+y,z:z+2.6,rx:p,color:'#e07a50'});\nconst q=project(0,1.8+y,z);if(q&&z>-30)text(q.x-4,q.y-1,'{what}','#ffe8a0');}\nconst up=fract(t*1.2)<0.5;const c=clawd3d(-5.5,0,3,{facing:1,arms:T<2?'out':(up?'up':'out')});\nif(c){put(c.left.x,c.left.row,'●','#ff5030');put(c.right.x,c.right.row,'●','#ff5030');\nif(T>0.5&&T<6)say(L[n%4],c.top.x,c.row);}}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#3a1a4a","#7a3050","#d0704a","#2a3060"],"speed":0.4,"intensity":0.3}}},
105 {"phase":"thinking","world":"retro arcade game","style":"text art","concept":"underground mine: Clawd in a hard hat chips at a rock face by lantern light, ore cart rolls in, a gem labelled {what} finally pops out","source":"claude-opus-5-5","scene":{"concept":"underground mine: Clawd in a hard hat chips at a rock face by lantern light, ore cart rolls in, a gem labelled {what} finally pops out","code":"let gem=0\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,0,x%5?'▀':'▓','#6b5a48');put(x,h-1,x%3?'═':'╪','#8a7a66')}\nfor(let y=1;y<h-1;y++)for(let x=0;x<12;x++){const n=noise2(x*0.5,y*0.7);put(w-12+x,y,n>0.6?'▓':n>0.4?'▒':'░','#9a8070')}\nfor(let i=0;i<4;i++){const bx=10+i*24;sprite(bx,1,'╥\\n║\\n║\\n║\\n║\\n║\\n║','#a07a50');text(bx-2,1,'═════','#a07a50')}\nconst cyc=mod(t,12),sw=Math.sin(t*6)>0.3\nconst cx=w-30\nconst c=clawd(cx,h-5,{facing:1,arms:sw?'up':{left:'down',right:'out'},pose:'stand'})\ntext(c.top.x-3,c.top.row-1,'▄███▄','#f2c84b');put(c.top.x,c.top.row-1,'●','#fff6a0')\nconst px=c.right.x,pr=c.right.row\ntext(px,sw?pr-1:pr,sw?'┐':'─╼','#cfd6dc')\nif(!sw&&fract(t*3)<0.3)for(let i=0;i<3;i++)put(w-13+rand(i+t)*2,pr-1+rand(i*7+t)*3,'*','#ffd27a')\nconst lx=8+Math.sin(t*0.7)*2;text(lx,2,'╤','#aaa');text(lx-1,3,'[◊]','#ffcf60')\nconst k=smoothstep(0,4,cyc);const kx=lerp(-20,cx-24,k)\nsprite(kx,h-4,'\\\\▓▓▓▓▓▓/\\n └o──o┘','#b08060')\nif(cyc>7){const g=smoothstep(7,9,cyc);const gx=lerp(w-13,kx+3,g),gy=h-4-Math.sin(g*3.14)*3;put(gx,gy,'◆','#5ff0ff');if(cyc>7.5&&cyc<11)text(gx-4,gy-1,'{what}','#9ff')}\nif(cyc>1&&cyc<6.5)say('Still chipping at {what}. The rock is stubborn.',c.top.x,c.row)\nelse if(cyc>9)say('Found it! A gem of an idea.',c.top.x,c.row)\n}","actors":[],"particles":[{"glyphs":".·'","count":20,"x":"w-14+rand(k)*6","y":"mod(t*(2+rand(k+3)*3)+rand(k+5)*7,7)+1","color":"#7a6a5a"}],"background":{"effect":"pulse","palette":["#2a1d12","#3a2a18","#4a3820"],"speed":0.4,"intensity":0.15}}},
106 {"phase":"searching","world":"construction site","style":"3D","concept":"race track pit stop meets construction site: Clawd in a hard hat drives a 3D track crane sweeping a searchlight over stacked crates in the p","source":"claude-opus-5-5","scene":{"concept":"race track pit stop meets construction site: Clawd in a hard hat drives a 3D track crane sweeping a searchlight over stacked crates in the pit lane, hunting for {what}","code":"const crate=box(1.2,1,1.2),post=cylinder(0.12,3,6),cone1=cone(0.25,0.5,6);\nconst cr=Array.from({length:14},(_,i)=>({x:-12+i*2,z:-2-(i%3)*1.6,c:i%3}));\nconst cols=['#e0a060','#c9b37a','#d98f5a'];\nfunction frame(t,dt){\ncamera(Math.sin(t*0.1)*3,3,9,0,0.8,-2,90);fog(8,38,'#000');light(0.4,1,0.6,0.35);\nfor(let i=-6;i<7;i++){mesh3d(cone1,{x:i*3,z:2,color:i%2?'#ff8c3a':'#eee'});}\nline3d(-20,0,1,20,0,1,'#aaa');line3d(-20,0,-7,20,0,-7,'#888');\nfor(let i=-20;i<20;i+=2)line3d(i,0.01,1,i+1,0.01,1,'#e33');\nconst sweep=Math.sin(t*0.35)*13,cyc=fract(t/16),tg=5;\nfor(let i=0;i<cr.length;i++){const c=cr[i];const hit=i==tg&&cyc>0.55;const lift=hit?smoothstep(0.6,0.75,cyc)*1.5:0;\nmesh3d(crate,{x:c.x,y:0.5+lift,z:c.z,color:hit?'#ffd75e':cols[c.c]});}\nmesh3d(post,{x:-13,z:-1,color:'#f2c14e'});line3d(-13,3,-1,sweep,3,-1,'#f2c14e');\nconst sx=cyc>0.55?cr[tg].x:sweep;line3d(sx,3,-1,sx,1.2,cr[tg].z,'#ffe08a');\nconst p=project(sx,0.2,-2);if(p){for(let k=-3;k<=3;k++)put(p.x+k,p.y,'░','#ffe08a');}\nconst q=project(cr[tg].x,2.4,cr[tg].z);if(q&&cyc>0.6)text(q.x-4,q.y,'{what}','#fff');\nconst me=clawd3d(-11,0,0.5,{facing:1,arms:cyc>0.6?'up':'out',size:1.1});\nif(me){pixels(me.top.x-4,me.top.py-3,' ######\\n########','{\"#\":\"#ffd400\"}'.length?{'#':'#ffd400'}:{});\nif(cyc>0.6)say('Found {what}! Hoisting it to the pit.',me.top.x,me.row);else if(cyc>0.1&&cyc<0.4)say('Sweeping every crate for {what}...',me.top.x,me.row);}\n}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#778"],"speed":0.3,"intensity":0}}},
107 {"phase":"searching","world":"cooking show","style":"3D","concept":"race track pit stop in 3D: Clawd in pit lane scans tire stacks one by one hunting the right set while cars scream past on the track","source":"claude-opus-5-5","scene":{"concept":"race track pit stop in 3D: Clawd in pit lane scans tire stacks one by one hunting the right set while cars scream past on the track","code":"const tire=cylinder(0.45,0.3,12),car=box(1.8,0.5,0.9),cab=box(0.8,0.35,0.7),post=box(0.15,1.6,0.15);\nconst sx=[-8,-5,-2,1,4,7];\nfunction frame(t,dt){camera(0,3,8,0,0.8,-2,80);light(0.4,1,0.6,0.35);fog(10,40,'#000');\nfor(let z of [-4,-8])line3d(-30,0,z,30,0,z,'#bbb');line3d(-30,0,-2.5,30,0,-2.5,'#e44');\nfor(let i=-6;i<7;i++)mesh3d(post,{x:i*4,y:0.8,z:-9,color:'#99a'});line3d(-26,1.6,-9,26,1.6,-9,'#ccd');\nfor(let c=0;c<3;c++){const x=mod(t*22+c*20,60)-30,z=c%2?-5:-7,col=['#e55','#5ae','#ec5'][c];mesh3d(car,{x,y:0.3,z,color:col});mesh3d(cab,{x:x-0.2,y:0.7,z,color:'#eee'});}\nconst cy=mod(t,13),i=Math.min(5,Math.floor(cy/2)),found=cy>=11;\nfor(let k=0;k<6;k++){const hit=found&&k==5;for(let j=0;j<4;j++)mesh3d(tire,{x:sx[k],y:j*0.32,z:0,color:hit?'#7e7':(k<i||(k==i&&cy%2>1.2)?'#866':'#888')});}\nconst seg=Math.min(cy,11),fi=Math.floor(seg/2),fr=fract(seg/2),cx=lerp(sx[Math.min(fi,5)],sx[Math.min(fi+1,5)],smoothstep(0,0.5,fr))+0.9;\nconst me=clawd3d(cx,0,1,{facing:fr<0.5&&!found?1:0,stride:fr<0.5?Math.floor(t*8)%4:-1,arms:found?'up':{left:'down',right:'out'},size:0.8});\nconst p=project(sx[5],1.6,0);if(p)text(p.x-5,p.y-1,'{what}',found?'#7f7':'#ccc');\nif(me){if(!found&&fr>0.5)put(me.right.x+1,me.right.row,'≈','#6cf');\nif(found)say('Found {what}! Fastest pit stop in the codebase.',me.top.x,me.row);else if(cy<3)say('Box box! Hunting for {what}...',me.top.x,me.row);else if(cy>6&&cy<9)say('Wrong compound. Next stack!',me.top.x,me.row);}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#778"],"speed":0.5,"intensity":0}}},
108 {"phase":"writing","world":"underground mine","style":"3D","concept":"ski slope: Clawd slaloms down a snowy run through red and blue gates toward a FINISH banner for {what}, delivering the reply at speed","source":"claude-opus-5-5","scene":{"concept":"ski slope: Clawd slaloms down a snowy run through red and blue gates toward a FINISH banner for {what}, delivering the reply at speed","code":"const tree=cone(0.9,2.2,6),trunk=cylinder(0.15,0.5,5),pole=cylinder(0.06,1.3,4),post=cylinder(0.1,2.6,5),gnd=plane(60,220);\nconst L=['Carving the reply for {what}. Clean lines only.','Gate after gate, every point covered.','No wipeouts on {what} today.','Tucking in for the final stretch!'];\nfunction frame(t,dt){const z=-t*5,px=Math.sin(z*0.39)*3;camera(px*0.5,3,z+8,px*0.4,0.4,z-12,90);fog(10,50,'#000');light(0.4,1,0.5,0.35);\nmesh3d(gnd,{x:0,y:0,z:z-90,color:'#dce8f6'});\nfor(let i=-1;i<14;i++){const tz=Math.floor(z/5)*5-i*5;for(const s of [-1,1]){const tx=s*(8+rand(tz*s+3)*4);mesh3d(trunk,{x:tx,z:tz,color:'#a07850'});mesh3d(tree,{x:tx,y:0.5,z:tz,color:'#5fb070'});}}\nfor(let j=0;j<6;j++){const gz=Math.floor(z/8)*8-j*8,gx=Math.sin(gz*0.39)*3,c=mod(Math.floor(gz/8),2)?'#ff5a5a':'#5a8cff';mesh3d(pole,{x:gx-1.3,z:gz,color:c});mesh3d(pole,{x:gx+1.3,z:gz,color:c});line3d(gx-1.3,1.2,gz,gx+1.3,1.2,gz,c);}\nconst fz=Math.floor(z/100)*100-60;mesh3d(post,{x:-5,z:fz,color:'#ffcc55'});mesh3d(post,{x:5,z:fz,color:'#ffcc55'});line3d(-5,2.5,fz,5,2.5,fz,'#ffcc55');line3d(-5,2.2,fz,5,2.2,fz,'#ffcc55');const p=project(0,2.4,fz);if(p&&p.depth>0)text(p.x-8,p.y,'FINISH: {what}','#ffe066');\nline3d(px-0.35,0.02,z+0.6,px-0.35,0.02,z-0.6,'#ff8844');line3d(px+0.35,0.02,z+0.6,px+0.35,0.02,z-0.6,'#ff8844');\nconst d=Math.cos(z*0.39),me=clawd3d(px,0,z,{facing:d<-0.2?1:d>0.2?-1:0,arms:'out',pose:'stand'});\nfor(let k=0;k<8;k++){const sp=project(px+(rand(k+Math.floor(t*6))-0.5)*1.5,0.1,z+0.8+rand(k)*0.6);if(sp)put(sp.x,sp.y,'*','#ffffff');}\nconst li=Math.floor(t/7)%L.length;if(me&&fract(t/7)<0.7)say(L[li],me.top.x,me.row);}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#ffffff","#aaccff","#ccddee"],"speed":0.5,"intensity":0}}},
109 {"phase":"reading","world":"museum after hours","style":"3D","concept":"ant colony up close, 3D: Clawd crouches beside a winding ant trail, studying the workers as they haul crumbs of {what} back from the anthill","source":"claude-opus-5-5","scene":{"concept":"ant colony up close, 3D: Clawd crouches beside a winding ant trail, studying the workers as they haul crumbs of {what} back from the anthill","code":"const sp=sphere(0.16,6),cr=box(0.32,0.28,0.32),hill=cone(5,3.5,12),gr=plane(80,80),pb=sphere(0.5,6),gs=cone(0.08,1.2,4);\nconst ants=Array.from({length:16},(_,i)=>({z:-38+i*2.8,c:i%3==0}));\nconst peb=Array.from({length:8},(_,i)=>({x:(rand(i)-0.5)*16,z:-rand(i+7)*30,s:0.5+rand(i+3)}));\nconst grass=Array.from({length:14},(_,i)=>({x:(rand(i+20)<0.5?-1:1)*(5+rand(i+30)*6),z:-rand(i+40)*30}));\nconst L=['Following the trail to see where {what} goes.','Every ant carries one piece of {what}. Neat.','Ah, so THAT function feeds the queen.','The whole colony runs on {what}. Mapping it.'];\nfunction frame(t,dt){camera(4+Math.sin(t*0.12)*2,1.7,9,0,0.4,-8,80);light(0.4,1,0.6,0.35);fog(8,42,'#000');\nmesh3d(gr,{color:'#9a7a50'});mesh3d(hill,{x:-3,z:-32,color:'#c09a60'});\nfor(const p of peb)mesh3d(pb,{x:p.x,y:0.1,z:p.z,scale:p.s,color:'#a8a090'});\nfor(const g of grass)mesh3d(gs,{x:g.x,z:g.z,rz:Math.sin(t+g.x)*0.1,color:'#7cc060'});\nlet near=null;for(const a of ants){a.z+=dt*1.3;if(a.z>8)a.z-=44.8;const x=Math.sin(a.z*0.25)*3,b=Math.sin(t*12+a.z)*0.03;\nfor(let k=0;k<3;k++)mesh3d(sp,{x,y:0.18+b,z:a.z+k*0.3-0.3,scale:k==1?0.8:1,color:'#d8573c'});\nif(a.c){mesh3d(cr,{x,y:0.48+b,z:a.z,ry:a.z,color:'#f4e4a8'});if(a.z<5&&(!near||a.z>near.z))near={x,z:a.z};}}\nconst p=near&&project(near.x,0.9,near.z);if(p&&p.depth<20)text(p.x-4,p.y,'{what}','#fff6c0');\nconst wx=4+Math.sin(t*0.2)*0.6;const me=clawd3d(wx,0,1.5,{facing:-1,stride:-1,look:{x:-2,y:1},arms:fract(t/8)<0.5?'down':{left:'out',right:'down'},blink:fract(t*0.3)<0.04});\nif(me&&fract(t/8)>0.1)say(L[Math.floor(t/8)%4],me.top.x,me.row);}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#3a2a1a","#5a4028","#2a3a20"],"speed":0.3,"intensity":0}}},
110 {"phase":"running","world":"volcano island","style":"3D","concept":"submarine off a volcano island: Clawd peers from a 3D sub's porthole as the {what} command runs, an undersea vent rumbling and bubbles risin","source":"claude-opus-5-5","scene":{"concept":"submarine off a volcano island: Clawd peers from a 3D sub's porthole as the {what} command runs, an undersea vent rumbling and bubbles rising while depth ticks down","code":"const hull=sphere(1,14),tower=box(0.8,0.7,1),fin=box(0.1,0.8,0.6),rock=cone(2.5,3,8),vent=cone(1.2,1.6,8),bub=sphere(0.12,5);\nconst bs=Array.from({length:14},(_,i)=>({x:rand(i)*2-1,y:rand(i+9)*5,v:0.6+rand(i+3)}));\nfunction frame(t,dt){camera(Math.sin(t*0.1)*2,2,9,0,1,0,80);light(0.3,1,0.5,0.35);fog(6,30,'#021220');\nfor(let x=0;x<w;x++)for(let y=0;y<h;y++){const n=noise2(x*0.05+t*0.2,y*0.3);if(n>0.72)put(x,y,'~',mix('#0a3050','#1a6080',n));}\nmesh3d(plane(60,30),{y:-0.5,color:'#7a6a50'});\nmesh3d(rock,{x:-8,y:-0.5,z:-6,color:'#6a5a5a'});mesh3d(rock,{x:9,y:-0.5,z:-8,scale:1.4,color:'#5a5050'});\nmesh3d(vent,{x:5,y:-0.5,z:-2,color:'#8a5040'});\nconst glow=0.5+0.5*Math.sin(t*3);const vp=project(5,1.1,-2);if(vp)text(vp.x-1,vp.y,'▲',mix('#ff4020','#ffcc40',glow));\nfor(const b of bs){b.y+=b.v*dt;if(b.y>6)b.y=0;mesh3d(bub,{x:5+b.x*0.6+Math.sin(t+b.y)*0.3,y:1+b.y,z:-2,color:'#aee'});}\nconst sx=-3+Math.sin(t*0.2)*1.5,sy=1.4+Math.sin(t*0.8)*0.15;\nmesh3d(hull,{x:sx,y:sy,z:0,scale:1,sx:1,color:'#e8c040'});mesh3d(hull,{x:sx+1.4,y:sy,z:0,scale:0.8,color:'#e8c040'});mesh3d(hull,{x:sx-1.4,y:sy,z:0,scale:0.75,color:'#e8c040'});\nmesh3d(tower,{x:sx+0.3,y:sy+0.8,z:0,color:'#d0a830'});mesh3d(fin,{x:sx-2.2,y:sy,z:0,ry:t*6,color:'#c09020'});\nconst p=project(sx+0.2,sy+0.1,1.1);if(p){const c=clawd(p.x-7,p.y-2,{facing:1,eyes:fract(t*0.3)<0.04?'closed':'wide',look:{x:2,y:0}});\nconst lp=project(sx+0.3,sy+1.6,0);if(lp)text(lp.x-Math.floor('{what}'.length/2),lp.y-1,'{what}','#ffe');\nconst m=Math.floor(t/12)%4;const L=['Diving deep into {what}. Pressure: holding.','Vent rumbling. That is just the CPU fans.','Sonar pings, no output yet. Patience, crew.','Still sinking toward the result. Bubbles say soon.'];if(fract(t/12)>0.25)say(L[m],c.top.x,c.row);}\nconst d=Math.floor(t*3);text(w-14,h-1,'DEPTH '+String(120+d).padStart(4,'0')+'m','#6cf');}","actors":[],"particles":[],"background":{"effect":"waves","palette":["#03283d","#064060","#0a5070"],"speed":0.5,"intensity":0}}},
111 {"phase":"editing","world":"desert caravan","style":"3D","concept":"ancient tomb: Clawd the stonemason rebuilding a crumbling hieroglyph wall block by block, torches flickering, in a 3D burial chamber","source":"claude-opus-5-5","scene":{"concept":"ancient tomb: Clawd the stonemason rebuilding a crumbling hieroglyph wall block by block, torches flickering, in a 3D burial chamber","code":"const blk=box(0.95,0.55,0.6),pil=cylinder(0.35,3,8),fl=plane(30,30),tor=cone(0.15,0.4,6);\nfunction frame(t,dt){camera(Math.sin(t*0.15)*1.5,2.2,6,0,1,-6,70);light(0.4,0.8,0.6,0.35);fog(6,30,'#000');\nmesh3d(fl,{y:0,z:-6,color:'#b89a6a'});\nfor(const sx of[-6,6])for(let z=-2;z>-14;z-=4){mesh3d(pil,{x:sx,z,color:'#c9a96e'});mesh3d(tor,{x:sx*0.88,y:2,z,color:'#f6a23a',unlit:true});point3d(sx*0.88,2.5+Math.random()*0.15,z,Math.random()<0.5?'#ffd25a':'#ff7a2a');}\nconst s=mod(t,40),cur=Math.min(23,Math.floor(s/1.5));let cx=0;\nfor(let i=0;i<24;i++){const c=i%8,r=Math.floor(i/8),x=-3.5+c,y=0.3+r*0.58;const k=smoothstep(i*1.5,i*1.5+1,s);if(i==cur)cx=x;if(k<=0)continue;mesh3d(blk,{x,y:lerp(4,y,k),z:-6,ry:(1-k)*1.5,color:(c+r)%3==0?'#e0c48a':'#d4b27a'});}\nif(s>36){const p=project(0,2.2,-6);if(p)text(p.x-8,p.y,'✦ SEALED ✦','#ffe08a');}\nconst p=project(0,2.1,-5.6);if(p&&cur>18)text(p.x-6,p.y,'{what}','#5a2a10');\nconst me=clawd3d(cx,0,-4.3,{facing:0,arms:fract(t*1.5)<0.3?'up':'out',stride:-1});\nif(me){pixels(me.top.x-3,me.top.py-3,'.yyyyy.\\nyyyyyyy','{\"y\":\"#e8d070\"}'.length?{y:'#e8d070'}:{});\nif(fract(t*1.5)<0.25)text(me.right.x+1,me.right.row-1,'*','#fff6c0');\nif(s>2&&s<8)say('Restoring {what}, one stone at a time.',me.top.x,me.row);\nelse if(s>20&&s<26)say('Careful. The curse is in the indentation.',me.top.x,me.row);\nelse if(s>36)say('Wall sealed. Pharaoh would approve.',me.top.x,me.row);}}","actors":[],"particles":[{"glyphs":"·.","count":25,"x":"rand(k)*w","y":"mod(t*(0.3+rand(k+5)*0.5)+rand(k+2)*h,h)","color":"#8a7650"}],"background":{"effect":"fire","palette":["#2a1408","#4a2410","#6a3a18"],"speed":0.4,"intensity":0.15}}},
112 {"phase":"editing","world":"dinosaur era","style":"pixel art","concept":"desert caravan: Clawd rides a camel across scrolling dunes, packing saddlebags while signposts for {what} roll past","source":"claude-opus-5-5","scene":{"concept":"desert caravan: Clawd rides a camel across scrolling dunes, packing saddlebags while signposts for {what} roll past","code":"const cam=['......cc........','.....cccc.......','.....cc..hhh....','....cc..hhhhh...','...cc.hhhhhhhhh.','...ccccccccccccc','....cccccccccccc'].map(s=>s.split('').reverse().join(''));\nconst lg=[['.c.c.....c.c....','.c.c.....c.c....','.c.c.....c.c....'],['..c.c...c...c...','.c...c...c.c....','c.....c.c...c...']];\nconst P={c:'#c8955a',h:'#a8703a',b:'#7a4a2a',s:'#e8d070',k:'#5a3a1a'};\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){const f=Math.floor(4+noise2(x*0.03+t*0.05,1)*4);for(let p=f;p<10;p++)pixel(x,p,'#b07a48');const n=Math.floor(11+noise2(x*0.06+t*0.25,5)*4);for(let p=n;p<18;p++)pixel(x,p,p==n?'#f0c878':'#d8a860');}\npixels(100,1,'.sss.\\nsssss\\nsssss\\n.sss.',{s:'#ffd84a'});\nconst sx=mod(-t*7,140)+120;pixels(Math.floor(sx),8,'k\\nk\\nk\\nk\\nk\\nk\\nk',P);text(Math.floor(sx)-4,3,'[{what}]','#ffe9a0','#5a3a1a');\nconst cx=50,bob=Math.floor(t*4)%2;\npixels(cx,4+bob,cam.join('\\n'),P);pixels(cx,11,lg[Math.floor(t*4)%2].join('\\n'),P);\nconst nb=Math.floor(t/6)%5;for(let i=0;i<Math.min(nb,3);i++)pixels(cx+2+i*4,8+bob,'bbb\\nbsb\\nbbb',P);\nconst c=clawd(cx+2,0+bob*0.5,{pose:'sit',facing:1,arms:fract(t/6)<0.2?'up':'down'});\npixels(c.top.x-3,c.top.py-2,'..kkk..\\nsssssss',P);\nconst q=Math.floor(t/9)%4;const L=['Packing {what} for the long haul.','One more edit in the saddlebag.','Camel says the diff is heavy.','Oasis = tests. Keep riding.'];if(fract(t/9)<0.6)say(L[q],c.top.x+8,0);\nfor(let i=0;i<6;i++){const x=mod(i*23-t*12,w);pixel(Math.floor(x),16+i%2,'#f5dca0');}\n}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#5a3060","#a05040","#e09050"],"speed":0.3,"intensity":0}}},
113 {"phase":"agents","world":"snow globe","style":"3D","concept":"rush hour traffic: Clawd as a traffic cop on the shoulder waving parallel subagent cars down a 3D highway under a {what} gantry sign","source":"claude-opus-5-5","scene":{"concept":"rush hour traffic: Clawd as a traffic cop on the shoulder waving parallel subagent cars down a 3D highway under a {what} gantry sign","code":"const car=box(1.4,0.8,2.2),cab=box(1.1,0.5,1.1),post=cylinder(0.12,3.2,6),sign=box(9,0.9,0.2),cone1=cone(0.25,0.6,6);\nconst lanes=[-4,-1.4,1.4,4];\nconst cs=Array.from({length:10},(_,i)=>({l:lanes[i%4],z:-rand(i)*60,v:5+rand(i+7)*6,c:hsl(rand(i+3)*360,0.6,0.62)}));\nfunction frame(t,dt){camera(0,3.2,9,0,1,-25,90);fog(10,55,'#000');light(0.4,1,0.6,0.35);\nfor(let i=0;i<5;i++){const x=-5.4+i*2.7;line3d(x,0,10,x,0,-80,i%4?'#777':'#bbb')}\nfor(let i=0;i<14;i++){const z=6-i*6+mod(t*8,6);line3d(-2.7,0.01,z,-2.7,0.01,z-2,'#ee8');line3d(0,0.01,z,0,0.01,z-2,'#ee8');line3d(2.7,0.01,z,2.7,0.01,z-2,'#ee8')}\nmesh3d(post,{x:-5.8,z:-12,color:'#999'});mesh3d(post,{x:5.8,z:-12,color:'#999'});mesh3d(sign,{y:3.4,z:-12,color:'#3a9a5a'});\nconst sp=project(0,3.4,-12);if(sp)text(Math.round(sp.x-7),sp.y,'{what} -> EXIT 4','#fff');\nfor(const c of cs){c.z-=c.v*dt*(0.6+0.4*Math.sin(t*0.7+c.l));if(c.z<-70){c.z=6;c.c=hsl(Math.random()*360,0.6,0.62)}\nmesh3d(car,{x:c.l,y:0.4,z:c.z,color:c.c});mesh3d(cab,{x:c.l,y:1.05,z:c.z+0.2,color:'#cde'})}\nfor(let i=0;i<4;i++)mesh3d(cone1,{x:-6.2,z:3-i*3,color:'#f80'});\nconst wave=Math.floor(t*2)%2;const me=clawd3d(-7.5,0,3,{facing:1,size:1.1,arms:wave?{left:'down',right:'up'}:{left:'down',right:'out'}});\nif(me){const L=['Go go go! Every lane gets a piece of {what}.','Four helpers, four lanes, zero honking.','Rush hour, but make it parallel.','Waiting for my helpers to come back with answers.'];say(L[Math.floor(t/7)%4],me.top.x,me.row)}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#335","#557","#99b"],"speed":0.3,"intensity":0}}},
114 {"phase":"web","world":"jungle safari","style":"3D","concept":"mad science lab: Clawd throws the switch as a tesla coil yanks web pages out of the sky, bottling them as glowing packets in a flask labeled","source":"claude-opus-5-5","scene":{"concept":"mad science lab: Clawd throws the switch as a tesla coil yanks web pages out of the sky, bottling them as glowing packets in a flask labeled {what}","code":"const bench=box(12,0.8,2.4),pole=cylinder(0.25,2.6,8),orb=sphere(0.7,12),fl=cylinder(0.6,1.6,12),pk=box(0.35,0.35,0.35),fz=plane(40,14),lv=cone(0.3,0.6,8);\nfunction frame(t,dt){\ncamera(Math.sin(t*0.15)*3,2.6,9,0,1.2,0,90);light(0.3,1,0.6,0.3);fog(10,40,'#000');\nmesh3d(fz,{color:'#556677'});\nmesh3d(bench,{y:0.4,color:'#b08860'});\nmesh3d(pole,{x:5,y:0.8,color:'#aab0c0'});\nmesh3d(orb,{x:5,y:3.6,color:'#c8d8ff'});\nconst zap=fract(t*0.7)<0.5;\nif(zap){for(let j=0;j<3;j++){let x=5,y=3.6;for(let s=0;s<4;s++){const nx=x+(Math.random()-0.5)*1.4,ny=y+0.7;line3d(x,y,0,nx,ny,-0.3,'#aef4ff');x=nx;y=ny}}\nlet x=5,y=3.6;for(let s=1;s<=6;s++){const nx=lerp(5,-4,s/6),ny=lerp(3.6,2.6,s/6)+(Math.random()-0.5)*0.6;line3d(x,y,0,nx,ny,0,'#ffef80');x=nx;y=ny}}\nfor(let i=0;i<4;i++){const p=mod(t*1.6+i*1.3,5.2);if(p<3.4)mesh3d(pk,{x:-4,y:5.4-p*1.3,ry:t*3+i,color:hsl(90+i*40,0.8,0.65),unlit:true})}\nconst lvl=0.2+mod(t*0.04,1)*1.2;mesh3d(box(1,lvl,1),{x:-4,y:0.8+lvl/2,color:'#7cff8a'});\nmesh3d(fl,{x:-4,y:0.8,color:'#9fdcc0',wire:true});\nmesh3d(lv,{x:2,y:0.8,color:'#ff6060'});\nconst q=project(-4,0.6,1.2);if(q)text(q.x-4,q.y,'{what}','#ffffa0');\nconst up=fract(t*0.35)<0.5;\nconst c=clawd3d(0.2,0.8,0.6,{facing:0,arms:up?'up':{left:'down',right:'out'},eyes:zap?'wide':'open',color:'#d97757'});\nconst L=['Fetching {what}... IT\\'S ALIVE!','Catch it! The internet is pouring in!','Bottling {what}, one packet at a time.','More voltage! The page is still loading!'];\nif(c&&fract(t/6)<0.75)say(L[Math.floor(t/6)%4],c.top.x,c.row);\n}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#203040","#304060","#405070"],"speed":0.5,"intensity":0}}},
115 {"phase":"git","world":"outer space","style":"pixel art","concept":"lighthouse in a storm: Clawd the keeper guides commit-ships through the squall to dock in a row, each a link in the harbor's history","source":"claude-opus-5-5","scene":{"concept":"lighthouse in a storm: Clawd the keeper guides commit-ships through the squall to dock in a row, each a link in the harbor's history","code":"const S=[];for(let i=0;i<40;i++)S.push(i);\nconst H=['a1f3e9','7c02bd','e5d41a','09bb7f','3f6c22','d8e110'];\nconst tw=' .rr. \\n.yyyy.\\n.yyyy.\\n gggg \\n wwww \\n rrrr \\n wwww \\n rrrr \\n wwww \\n rrrr \\n wwwwww\\nwwwwwww\\nkkkkkkkk';\nconst tp={r:'#d04040',y:'#ffe066',g:'#555',w:'#eeeeee',k:'#666677','.':null};\nconst bt='....mm....\\n....m.....\\nbbbbbbbbbb\\n.bbbbbbbb.';\nfunction frame(t,dt){const fl=fract(t/9)<0.03;if(fl)fill(0,0,w,h-2,' ',null,'#2a2f45');\nfor(const k of S){const x=mod(rand(k)*w-t*14,w),y=mod(rand(k+5)*9+t*16,7);put(x,y,'/','#7a9cc6')}\nconst a=Math.sin(t*0.9);for(let s=1;s<70;s++){const x=12+s,y=2-a*s*0.04;if(y>=0&&y<6&&a>-0.2)put(x,y,'░','#fff3a0')}\npixels(6,1,tw,tp);for(let x=0;x<w;x++){put(x,h-1,'≈','#3a6ea5');put(x,h-2,mod(x+Math.floor(t*4),6)<2?'~':' ','#5a8fd0')}\nfor(let x=36;x<w;x++)put(x,7,x%4?'═':'╦','#a07850');\nconst n=Math.floor(t/6)+1;for(let i=0;i<n;i++){const sl=40+(i%6)*13,bx=Math.max(sl,w-(t-i*6)*10);if(i%6!=(n-1)%6||i==n-1){pixels(bx,10,bt,{m:'#ccc',b:i==n-1?'#e0a060':'#b07040'});text(bx+1,4,i==n-1?'{what}'.slice(0,12):H[i%6],i==n-1?'#ffe066':'#9fb')}}\nconst c=clawd(16,3.5,{facing:1,arms:{left:'down',right:'out'},color:'#d97757'});pixels(c.top.x-4,c.top.py-2,'..yyyyy..\\nyyyyyyyyy','',{y:'#ffd23f'});pixels(c.top.x-4,c.top.py-2,'..yyyyy..\\nyyyyyyyyy',{y:'#ffd23f'});\nconst L=['Steady now. {what} incoming!','Docked. Another commit in the log.','Storm or not, history stays tidy.','Beam on. No commit left at sea.'];say(L[Math.floor(t/6)%4],c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#1b2440","#2a3a60","#3a5080"],"speed":1.2,"intensity":0}}},
116 {"phase":"git","world":"skate park","style":"3D","concept":"train station meets skate park: 3D branching rails where commit-cars leave a station, Clawd skates the main line grinding rails as trains sp","source":"claude-opus-5-5","scene":{"concept":"train station meets skate park: 3D branching rails where commit-cars leave a station, Clawd skates the main line grinding rails as trains split onto branches","code":"const car=box(1.6,0.9,1),plat=box(30,0.4,3),post=cylinder(0.1,2.2,6),ramp=box(2,0.3,1.4);\nfunction frame(t,dt){camera(0,3,9,0,0.8,-6,90);light(0.4,1,0.6,0.35);fog(10,42,'#000');\nmesh3d(plat,{x:0,y:0.2,z:2.5,color:'#9aa4b8'});\nfor(let i=-3;i<=3;i++)mesh3d(post,{x:i*4,z:3.5,color:'#c8a060'});\nfor(let s=-1;s<=1;s+=2){line3d(s*0.6,0,6,s*0.6,0,-60,'#aaa');}\nfor(let z=0;z>-60;z-=1.5)line3d(-0.9,0,z,0.9,0,z,'#554');\nconst bz=-8;line3d(0.6,0,bz,7,0,bz-25,'#8cf');line3d(-0.6,0,bz,5.8,0,bz-25,'#8cf');\nline3d(-0.6,0,bz-4,-7,0,bz-29,'#f8c');line3d(0.6,0,bz-4,-5.8,0,bz-29,'#f8c');\nconst cyc=mod(t,8),tr=cyc*4;const br=mod(floor(t/8),2)?1:-1;\nfor(let i=0;i<3;i++){let z=2-tr-i*1.9,x=0;if(z<bz)x=br*(bz-z)*0.26;mesh3d(car,{x,y:0.55,z,ry:z<bz?-br*0.25:0,color:i?'#e0b060':'#7fd4a0'});if(i==0){const p=project(x,1.4,z);if(p&&cyc<6)text(p.x-3,p.y,'{what}','#fff');}}\nmesh3d(ramp,{x:-4,y:0.15,z:0.5,rz:0.3,color:'#d0d0e0'});\nconst sx=-9+mod(t*3,18),air=Math.max(0,Math.sin(Math.max(0,sx+4.5)*1.2))*(sx>-4.5&&sx<-1.9?1:0);\nconst c=clawd3d(sx,0.4+air*1.2,1.2,{facing:1,stride:-1,arms:air>0.1?'up':'out',size:0.8});\nif(c){pixels(c.feet.x-6,c.feet.py,'#############\\n.o.........o.',{'#':'#c33','o':'#ddd'});\nconst lines=['kickflip onto main. {what} incoming!','new branch? grind it.','commit hash, nollie, merge.','HEAD is wherever I land.'];if(fract(t/8)<0.6)say(lines[floor(t/8)%4],c.top.x,c.row);}}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#778"],"speed":0.4,"intensity":0.2}}},
117 {"phase":"web","world":"film noir city at night","style":"text art","concept":"cooking show: chef Clawd stirs a pot while ingredient crates labeled {what} zip in on a kitchen zipline from the web outside","source":"claude-opus-5-5","scene":{"concept":"cooking show: chef Clawd stirs a pot while ingredient crates labeled {what} zip in on a kitchen zipline from the web outside","code":"const P={'#':'#b0b8c0','d':'#6a7480','o':'#ff8030','y':'#ffd040'};\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,7,'▀','#c08850','#7a5030');put(x,8,x%10==0?'│':' ',null,'#5a3820');}\nfor(let x=0;x<w;x+=6)put(x,0,'▄','#3a5a6a');\nfill(96,1,22,5,' ',null,'#123050');text(97,1,'~ THE WEB ~','#8fd0ff','#123050');for(let i=0;i<6;i++)put(98+i*3,3+(i%2),'✦','#ffe080','#123050');\nline(60,1,w,1,'─','#909090');\nconst c=fract(t/8),k=t/8|0,lx=lerp(w,64,smoothstep(0,0.55,c)),fall=smoothstep(0.55,0.7,c),ly=1+fall*4;\nif(c<0.7){fill(lx-4,ly,9,1,' ',null,'#a06a30');text(lx-4,ly,('{what}').slice(0,9),'#fff','#a06a30');if(c<0.55)put(lx,ly-1,'┴','#ccc');}\npixels(58,10,'..dddddddddd..\\n.d##########d.\\n.d##########d.\\n..d########d..',P);\nconst f=c>0.7?1:0.3;for(let i=0;i<3;i++)put(60+i*4,5+Math.sin(t*3+i)*0.5-fract(t*0.7+i*0.3)*3,'~',mix('#555555','#ffffff',f));\nfor(let i=0;i<7;i++)put(60+i*1.5,7,(t*6+i)%2<1?'▲':'^',i%2?'#ff5020':'#ffb030');\nconst st=c>0.7&&c<0.95,ar=st?(Math.sin(t*10)>0?'up':'out'):'down';\nconst m=clawd(42,3,{facing:1,arms:{left:'down',right:ar},blink:fract(t*0.3)<0.04});\npixels(m.top.x-4,m.top.py-5,'.wwwww.\\nwwwwwww\\n.wwwww.\\n.wwwww.',{w:'#ffffff'});\nline(m.right.x,m.right.row,m.right.x+6,m.right.row+1,'/','#d0a060');\nconst L=['Ordering fresh {what} straight from the web!','Here it comes, still warm from the server.','Into the pot! A pinch of HTML, a dash of JSON.','Another crate! This recipe needs more sources.'];\nif(c>0.1&&c<0.5)say(L[(k*2)%4],m.top.x,m.row);else if(c>0.72&&c<0.97)say(L[(k*2+1)%4]+'',m.top.x,m.row);\n}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#3a2a20","#4a3020","#2a2018"],"speed":0.5,"intensity":0}}},
118 {"phase":"agents","world":"mad science lab","style":"pixel art","concept":"bowling alley, pixel art: Clawd as alley manager cheering while three subagent bowlers roll balls down parallel lanes, knocking pins for {wh","source":"claude-opus-5-5","scene":{"concept":"bowling alley, pixel art: Clawd as alley manager cheering while three subagent bowlers roll balls down parallel lanes, knocking pins for {what}","code":"const L=[{p:3.1,c:'#5af',o:0},{p:3.7,c:'#e5a',o:1.2},{p:2.9,c:'#7e6',o:2}];\nconst lines=['Three lanes, three helpers, all bowling {what}.','Strike! That pin was a bug in {what}.','I just keep score. Delegation is my sport.'];\nfunction frame(t,dt){\nfor(let i=0;i<3;i++){const L0=L[i],y0=i*3,ph=mod(t+L0.o,L0.p),s=clamp(ph/(L0.p*0.6),0,1),g=floor((t+L0.o)/L0.p);\nfor(let x=18;x<w-1;x++){put(x,y0,x%9==0?'·':' ','#c96',x<104?'#6b4422':'#4a2e18');put(x,y0+1,' ',null,x<104?'#7a5230':'#4a2e18');}\nfor(let x=18;x<w-1;x++)put(x,y0+2,'▔','#333');\ntext(19,y0,'{what}',mix(L0.c,'#000',0.4),'#6b4422');\nconst bx=30+s*76,py=y0*2+1;\nif(s<1)pixels(floor(bx),py,'oo\\noo',{o:L0.c});\nconst k=Math.max(0,ph-L0.p*0.6);\nfor(let j=0;j<3;j++){const px=107+j*4+k*(8+j*6),pp=py-k*(6-j*2)+k*k*6;pixels(floor(px),floor(pp),'w\\nr',{w:'#fff',r:'#e33'});}\nif(k>0&&k<0.6)text(100,y0,'STRIKE','#fd4');\ntext(w-6,y0+1,('x'+g).padStart(4),'#fd4','#4a2e18');}\nconst cheer=fract(t/3)<0.3;\nconst c=clawd(2,3,{facing:1,arms:cheer?'up':'down',pose:'stand'});\npixels(c.top.x-3,c.top.py-2,'bbbbbb\\n.bbbb.',{b:'#c22'});\nconst q=floor(t/10)%3;if(fract(t/10)<0.45)say(lines[q],c.top.x,c.row);}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#311","#213","#123"],"speed":0.5,"intensity":0}}},
119 {"phase":"reading","world":"film noir city at night","style":"pixel art","concept":"noir pond at night: fedora-wearing detective Clawd fishes a rain-slick city pond under a streetlamp, reeling up suspect fish tagged {what} f","source":"claude-opus-5-5","scene":{"concept":"noir pond at night: fedora-wearing detective Clawd fishes a rain-slick city pond under a streetlamp, reeling up suspect fish tagged {what} for questioning","code":"const fishA=['..oooo.ooo','.oooo#oo..','oooooooooo','.oooooooo.','..oooo.ooo'];\nconst hat=['..kkkk..','..kkkk..','bbbbbbbb','kkkkkkkk'];\nconst lines=['Pond full of suspects. Hooked {what}.','Talk, fish. What does {what} really do?','It calls three helpers. Classic alibi.','Throwing it back. Story checks out.'];\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){const b=floor(x/7),hh=3+floor(rand(b)*8);for(let py=12-hh;py<13;py++)pixel(x,py,'#2a2e44');if(rand(b*7+x)<.25&&rand(floor(t*.3)+b)<.7)pixel(x,13-hh+2+floor(rand(x)*hh*.6),'#e8c35a');}\nfor(let py=0;py<4;py++)for(let x=0;x<8;x++)if((x-3.5)**2/4+(py-1.5)**2<4)pixel(100+x,py+1,'#e6e2c8');\nfor(let x=0;x<w;x++)for(let py=14;py<18;py++){const s=noise2(x*.15+t*.5,py*.7);pixel(x,py,s>.7?'#4a6a9a':s>.5?'#24385a':'#152238');}\nfor(let py=4;py<14;py++)pixel(8,py,'#667');pixels(6,2,['ooooo','.yyy.'],{o:'#889',y:'#ffe08a'});\nfor(let x=3;x<14;x++)if(rand(x)>.3)pixel(x,14+floor(rand(x+5)*2),'#ffe08a');\nfill(14,7,28,1,'▀','#7a5235');for(let x=16;x<42;x+=8)put(x,8,'█','#5a3a25');\nfor(let i=0;i<40;i++){const rx=floor(mod(rand(i)*w-t*6,w)),ry=mod(t*14+rand(i+3)*18,18);pixel(rx,floor(ry),'#5d6d8a');}\nconst p=t%8,cy=floor(t/8);\nconst c=clawd(22,3,{pose:'sit',facing:1,arms:{left:'down',right:p>4&&p<6?'up':'out'},blink:fract(t*.3)<.05});\npixels(c.top.x-4,c.top.py-3,hat,{k:'#3c3c4c',b:'#a33'});\nconst tx=58,ty=p>4&&p<7?1:3;line(c.right.x,c.right.row,tx,ty,'·','#a98');\nlet by=7;if(p>3&&p<4)by=7+(fract(t*6)<.5?0:0);\nif(p<4){const bob=p>3?(fract(t*5)<.5?15:16):14+round(sin(t*2)*.5);line(tx,ty,70,floor(bob/2),'│','#bbb');pixels(70,bob-1,['r','w'],{r:'#e44',w:'#eee'});}\nelse{const k=smoothstep(4,5.5,p),fy=floor(lerp(16,4,k)),fx=67;line(tx,ty,fx+4,floor(fy/2),'│','#bbb');if(p<7.3){pixels(fx,fy,fishA,{o:p>6&&p<7?'#e8a04a':'#9ab8c8','#':'#111'});if(p>5.5)text(fx-3,floor(fy/2)-1,'{what}','#ffe08a');}else for(let i=0;i<6;i++)pixel(fx+2+i*1,15-floor(rand(i+cy)*3),'#cde');}\nif(p>1&&p<7.5)say(lines[cy%4],c.top.x,c.row);\n}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#223","#334466","#445577"],"speed":1,"intensity":0}}},
120 {"phase":"writing","world":"volcano island","style":"pixel art","concept":"volcano island at night: a UFO beams up the finished {what} crate from the beach while Clawd in a lei waves it off, volcano puffing behind","source":"claude-opus-5-5","scene":{"concept":"volcano island at night: a UFO beams up the finished {what} crate from the beach while Clawd in a lei waves it off, volcano puffing behind","code":"function frame(t,dt){for(let x=0;x<w;x++){put(x,h-1,'▀','#c9a46a','#8a6a3a');}for(let x=0;x<w;x++){const y=6+Math.floor(1.5+Math.sin(x*0.3+t*2));if(x<30)put(x,7,'~',x%2?'#4aa8d8':'#2f7fb0');}const vx=82;for(let r=0;r<7;r++){const hw=3+r*3;fill(vx-hw,2+r,hw*2,1,'█',r<1?'#e05030':'#6a4a3a');}for(let i=0;i<6;i++){const k=fract(t*0.4+i/6);put(vx-1+Math.sin(i*3+t)*k*4,1.5-k*2+1,'░',mix('#ff8040','#776655',k));}pixels(104,6,'..g..\n.ggg.\nggggg\n..b..\n..b..\n..b..',{g:'#3fbf5f',b:'#a07040'});const ux=lerp(-20,44,smoothstep(0,5,t))+Math.sin(t*0.7)*3;pixels(ux,0,'....cccc....\n..cccccccc..\nmmmmmmmmmmmm\n.yy.yy.yy.yy',{c:'#9fe8ff',m:'#b0b8c8',y:Math.floor(t*4)%2?'#ffe060':'#ff60c0'});const on=t>5;if(on){for(let y=2;y<h-1;y++)text(ux+2,y,'░░░░░░░░',y%2?'#7fffaa':'#4fdf8a');}const lift=on?clamp((t-6)*0.8,0,1):0,cy=lerp(5,1.5,fract(lift>=1?(t-7.25)*0.15:0)*0+lift);if(fract((t-7)*0.15)<0.9||t<7)pixels(ux+3,cy*2,'oooooo\noDDDDo\noDDDDo\noooooo',{o:'#c08040',D:'#e0b070'});if(on&&lift<1)text(ux+1,cy-1,'{what}','#ffe');const c=clawd(60,4,{facing:-1,arms:on?{left:Math.floor(t*3)%2?'up':'out',right:'down'}:'down',blink:fract(t*0.3)<0.05});pixels(c.top.x-4,c.top.py+5,'p.y.p.y.p',{p:'#ff70b0',y:'#ffe050'});if(t>2&&t<5)say('Ride for {what}? Neat.',c.top.x,c.row);if(t>8)say('{what} beamed up. Aloha, little crate!',c.top.x,c.row);}","actors":[],"particles":[{"glyphs":"·✧","count":25,"x":"rand(k)*w","y":"rand(k+5)*3","color":"#aabbff"}],"background":{"effect":"starfield","palette":["#334","#556","#99a"],"speed":0.4,"intensity":0.3}}},
121 {"phase":"writing","world":"weather report","style":"pixel art","concept":"fishing pond weather report: Clawd in a rain hat fishes from a dock as the forecast clears from rain to sun, reeling in a fish labeled {what","source":"claude-opus-5-5","scene":{"concept":"fishing pond weather report: Clawd in a rain hat fishes from a dock as the forecast clears from rain to sun, reeling in a fish labeled {what}","code":"let ph=0\nfunction frame(t,dt){\nconst cyc=fract(t/24),sun=smoothstep(0.3,0.6,cyc)\nfor(let y=0;y<2*h;y++){const c=y<10?mix('#26304a','#3a78b8',sun):mix('#1c4a6a','#2a6a8a',sun);for(let x=0;x<w;x+=1)if(y>=10||y%2==0)pixel(x,y,y>=10&&((x+Math.floor(t*4)+y*3)%17==0)?'#8fd0ee':c)}\nfor(let x=0;x<w;x++)pixel(x,10,'#6ab0d0')\nconst sx=w-18,sy=lerp(18,2,sun);pixels(sx,sy,' yyy \nyyyyy\nyyyyy\n yyy ',{y:'#ffd34a'})\nconst cl=1-sun;for(let i=0;i<3;i++){const cx=20+i*30+Math.sin(t*0.2+i)*4;pixels(cx,1,' ggg gg \nggggggggg\n ggggggg ',{g:mix('#9aa4b4','#e8eef4',sun)});if(cl>0.2)for(let k=0;k<4;k++){const ry=4+mod(t*12+k*3+i*5,10);pixel(cx+1+k*2,ry,'#9cc8ff')}}\nfor(let x=0;x<34;x++){pixel(x,12,'#8a5a32');pixel(x,13,'#6a4022')}for(let x=3;x<34;x+=8)for(let y=14;y<18;y++)pixel(x,y,'#5a3418')\nconst c=clawd(12,2,{facing:1,pose:'sit',arms:{left:'down',right:'out'},blink:fract(t*0.3)<0.04})\npixels(c.top.x-5,c.top.py-2,' yyyyy \nyyyyyyyyyy',{y:'#f0c020'})\nconst rx=c.right.x,ry=c.right.py,tx=60,bite=fract(t/8)>0.6,dip=bite?Math.sin(t*20):0,ty=11+dip\nline(rx,c.right.row,rx+6,c.right.row-2,'/','#c8a070')\nfor(let i=0;i<=20;i++){const k=i/20;pixel(Math.round(lerp(rx+6,tx,k)),Math.round(lerp(ry-4,ty,k)+Math.sin(k*3.14)*-2),'#dddddd')}\npixels(tx-1,ty,'r\\nw',{r:'#ff4040',w:'#ffffff'})\nconst jt=fract(t/8)*8-6.5;if(jt>0&&jt<1.5){const fx=tx+jt*6,fy=12-Math.sin(jt*2.1)*8;pixels(fx,fy,' ooo \\nooooo>\\n ooo ',{o:'#ff9a3a','>':'#ff9a3a'});text(fx-2,Math.floor(fy/2)-1,'{what}','#ffe0a0')}\nfor(let i=0;i<5;i++){const lx=80+i*7;pixels(lx,15,'gg\\ngg',{g:'#3a8a3a'});pixel(lx+1,13-(i%2),'#5aba4a')}\nconst msg=sun<0.5?'Forecast: drizzle over {what}. Fish still biting.':'Skies cleared on {what}. Reeling it in!';if(fract(t/12)<0.6)say(msg,c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#334","#557","#88a"],"speed":1,"intensity":0}}},
122 {"phase":"agents","world":"ancient tomb","style":"pixel art","concept":"snow globe: three tiny helper crabs each build a snowman labelled with their part of {what} inside a globe, while Clawd waits outside and gi","source":"claude-opus-5-5","scene":{"concept":"snow globe: three tiny helper crabs each build a snowman labelled with their part of {what} inside a globe, while Clawd waits outside and gives it an impatient shake now and then","code":"const cx=72,R=24,cy=7;let fl=Array.from({length:70},(_,i)=>({x:cx-R+rand(i)*2*R,y:rand(i+70)*12,v:1+rand(i+5)}));\nfunction hw(py){const d=(py-cy)/7.5;return d*d>=1?0:R*Math.sqrt(1-d*d)}\nfunction frame(t,dt){const m=mod(t,16),sh=m>13&&m<14.5,o=sh?Math.round(Math.sin(t*40)*1.5):0;\nfor(let py=0;py<14;py++){const k=hw(py);pixel(Math.round(cx-k)+o,py,'#9fd3ff');pixel(Math.round(cx+k)+o,py,'#9fd3ff');if(py>=12)for(let x=Math.ceil(cx-k)+1;x<cx+k;x++)pixel(x+o,py,'#eef6ff')}\nfill(cx-20+o,7,41,2,'▓','#8a5a3a');text(cx-Math.floor(('{what}').length/2)+o,8,'{what}','#ffd27a','#5a3520');\nfor(let i=0;i<3;i++){const sx=cx-14+i*14,n=Math.min(6,1+Math.floor((t+i*4)/9));for(let j=0;j<n;j++){const ww=j<3?2:1;for(let q=-ww;q<=ww;q++)pixel(sx+q+o,11-j,'#ffffff')}if(n>=6){pixel(sx+o,5,'#ff8c42');pixel(sx-1+o,6,'#222');}\nconst hx=Math.round(sx+4+Math.sin(t*1.3+i*2)*2);pixels(hx+o,10,'a.a\\naaa',{a:'#d97757'});if(fract(t*0.7+i/3)<0.15)pixel(hx+1+o,9,'#fff')}\nfor(const f of fl){if(sh)f.v=-6*rand(f.x+t);f.v=Math.min(1.5,f.v+dt*4);f.y+=f.v*dt;f.x+=Math.sin(t+f.y)*dt*(sh?8:1);if(f.y>11.5||f.y<0.5){f.y=f.y<0.5?0.6:1;f.x=cx-R+rand(t+f.x)*2*R}if(Math.abs(f.x-cx)<hw(f.y)-1)pixel(Math.round(f.x)+o,Math.round(f.y),'#dfefff')}\nconst c=clawd(30,4.5,{facing:1,arms:sh?'up':'down',stride:-1,blink:fract(t*0.3)<0.05,eyes:sh?'wide':'open'});\npixels(c.top.x-3,c.top.py-2,'.rrr.\\nwwwww',{r:'#e04848',w:'#fff'});\nfill(0,8,40,1,'▀','#cfe3f5');\nconst L=['Three helpers, one globe, all on {what}.','Shaking it does NOT make subagents faster.','...okay maybe a little.','Snowman #3 has a nose. {what} is nearly done.'];\nconst li=Math.floor(t/8)%4;if(fract(t/8)<0.7)say(L[li],c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334466","#556688","#8899bb"],"speed":0.3,"intensity":0.25}}},
123 {"phase":"web","world":"sports broadcast","style":"pixel art","concept":"ant colony as sports broadcast: Clawd the commentator in a booth while a scout ant races out across the soil to fetch a crumb labeled {what}","source":"claude-opus-5-5","scene":{"concept":"ant colony as sports broadcast: Clawd the commentator in a booth while a scout ant races out across the soil to fetch a crumb labeled {what} and haul it back to the nest, scoreboard ticking","code":"const G=h*2;function frame(t,dt){for(let y=0;y<h;y++)for(let x=0;x<w;x++){const s=y>=6?(rand(x*7+y)<0.15?'#4a3020':'#3a2416'):(y==5?'#2f5a2a':null);if(s)put(x,y,' ',null,s)}for(let x=0;x<w;x+=3)put(x,5,rand(x)<0.5?'\"':\"'\",'#6fbf5a');pixels(2,10,'..####..\\n.#oooo#.\\n#oooooo#\\n########','{\"#\":\"#8a5a34\",\"o\":\"#1a0e08\"}');text(1,8,'NEST','#d8b080');const cy=t%30,P=cy/30,out=P<0.5,k=out?smoothstep(0,0.45,P):1-smoothstep(0.5,0.95,P),ax=12+k*(w-40);const f=Math.floor(t*8)%2;pixels(ax,10,f?'.o.o.o\\nOOoOO.':'o.o.o.\\nOOoOO.','{\"o\":\"#1a0e08\",\"O\":\"#2a1a10\"}');const cx=out?w-26:ax+(f?-3:-4);if(out||P<0.96){pixels(cx,9,'.##.\\n####','{\"#\":\"#f0d890\"}');text(cx-2,7,'{what}'.slice(0,12),'#ffe9a0')}const bx=w-22;fill(bx,0,22,5,' ',null,'#202a3a');text(bx+1,0,'LIVE ●','#ff5050');const sc=Math.floor(t/30);text(bx+9,0,'CRUMBS '+sc,'#ffd23f');const c=clawd(bx+4,1,{facing:-1,arms:{left:'up',right:'down'},blink:fract(t*0.3)<0.04,look:{x:-2,y:1}});pixels(c.top.x-3,c.top.py-1,'#.....#','{\"#\":\"#9aa\"}');const lines=['And the scout is OFF toward {what}!','Lead at the crumb! She grabs it!','Hauling it home, crowd on its feet!','Back in the nest! Fetch complete!'];say(lines[Math.floor(P*4)],c.top.x,c.row)}","actors":[],"particles":[{"glyphs":"·","count":15,"x":"rand(k)*w*0.7","y":"7+rand(k+5)*2","color":"#7a5a3a"}],"background":{"effect":"pulse","palette":["#203040","#304050","#203828"],"speed":0.4,"intensity":0}}},
124 {"phase":"writing","world":"orchestra concert","style":"pixel art","concept":"cooking show meets orchestra: Clawd as chef-conductor plating the final dish while pots play notes, the finished reply served under a cloche","source":"claude-opus-5-5","scene":{"concept":"cooking show meets orchestra: Clawd as chef-conductor plating the final dish while pots play notes, the finished reply served under a cloche","code":"const W=w,H=h;\nfunction frame(t,dt){\nfor(let x=0;x<W;x++){pixel(x,2*H-3,'#7a5a3a');pixel(x,2*H-2,'#5a3f28');pixel(x,2*H-1,'#5a3f28');}\nfor(let x=0;x<W;x+=8)pixels(x,0,'..##..\\n.#..#.',{'#':'#c9a227'});\nconst pots=[[10,'#4fa3e0'],[24,'#e06c9f'],[38,'#7fd36a']];\nfor(let i=0;i<pots.length;i++){const [px,c]=pots[i];const b=Math.sin(t*4+i*2)>0.6;pixels(px,8,'.########.\\n##########\\n.########.\\n.########.\\n..######..',{'#':c});pixels(px+2,6+(b?-1:0),'.oooo.',{'o':'#ddd'});if(b)text(px+4,1,'♪','#ffe066');}\nconst cx=56,c=clawd(cx,H-6,{pose:'stand',facing:-1,arms:{left:Math.sin(t*3)>0?'up':'out',right:'out'}});\npixels(c.top.x-3,c.top.py-5,'.#.#.#.\\n#######\\n.#####.',{'#':'#ffffff'});\nconst l=c.left;line(l.x,l.row,l.x-3,l.row-2+Math.round(Math.sin(t*3)),'/','#eee');\nconst k=smoothstep(10,14,fract(t/20)*20),dx=88;\npixels(dx,12,'##################\\n.################.',{'#':'#cfd6de'});\nif(k<1){pixels(dx+2,4-Math.round(k*6),'.....######.....\\n...##########...\\n..############..\\n.##############.\\n################',{'#':'#b8c2cc'});}\nelse{pixels(dx+4,7,'..######..\\n.##****##.\\n##########',{'#':'#d97757','*':'#ffe066'});text(dx+1,H-1,'{what}','#ffe066');for(let i=0;i<5;i++)put(dx+3+i*3,1+Math.round(Math.sin(t*5+i)),'✦','#ffd166');}\nconst ph=fract(t/20);\nsay(ph<0.45?'Pots in tune, sauce in tempo. Plating {what}.':ph<0.7?'And... the big reveal!':'Bon appetit! Fresh from the code kitchen.',c.top.x,c.row);\n}","actors":[],"particles":[],"background":{"effect":"aurora","palette":["#3a2a4a","#4a3a6a","#2a3a5a"],"speed":0.4,"intensity":0.2}}},
125 {"phase":"web","world":"mad science lab","style":"pixel art","concept":"ski slope: Clawd skis down from a mountaintop lab with a {what} flag, catching air off a mogul each run","source":"claude-opus-5-5","scene":{"concept":"ski slope: Clawd skis down from a mountaintop lab with a {what} flag, catching air off a mogul each run","code":"const tr=[40,72,104];\nfunction frame(t){\nfor(let x=0;x<w;x++){const s=Math.floor(3+x*0.125);for(let p=s;p<2*h;p++)pixel(x,p,p==s?'#ffffff':(p+x)%5?'#cfe3f0':'#a9c8de');}\nfor(const a of tr){const s=Math.floor(3+a*0.125);pixels(a-2,s-7,'..g..\\n.ggg.\\n.ggg.\\nggggg\\nggggg\\n..b..\\n..b..',{g:'#3c9a5a',b:'#8a5a3a'});}\npixels(1,0,'..aaaaa..\\n.acccccа.\\naacccccaa\\nrrrrrrrrr',{a:'#9aa',c:'#6cf',r:'#c55','а':'#9aa'});\nif(fract(t*1.5)<0.5)pixel(5,0,'#ff0');\nconst P=9,p=fract(t/P),cx=p*135-12,sp=3+(cx+7)*0.125;\nconst mx=60,air=Math.max(0,1-Math.abs(cx-mx)/12)*4*(cx>mx-12&&cx<mx+12?1:0);\nconst ms=Math.floor(3+mx*0.125);pixels(mx-3,ms-2,'..www..\\nwwwwwww',{w:'#ffffff'});\nconst c=clawd(cx,(Math.round(sp-8-air*1.5))/2,{facing:1,pose:air>0.5?'jump':'stand',arms:air>0.5?'up':'out',color:'#d97757'});\nfor(let i=-2;i<16;i++)pixel(cx+i,c.feet.py+1+Math.floor(i*0.125),'#e33');\npixels(c.top.x-3,c.top.py-2,'.bbbbb.\\nyyyyyyy',{b:'#36c',y:'#fd3'});\nline(c.left.x,c.left.row,c.left.x,c.left.row-3,'│','#ccc');text(c.left.x+1,c.left.row-3,'{what}','#ff6');\nfor(let i=0;i<10;i++){const sx=cx-3-i*2;put(sx,Math.floor((3+sx*0.125)/2)-0.5+rand(i+Math.floor(t*8))*1,'·','#eef');}\nif(p>0.35&&p<0.6)say('Fetching {what} at 80 mph downhill!',c.top.x,c.row);\nif(p>0.05&&p<0.2)say('Lab says the page is at the bottom. Wheee!',c.top.x+8,c.row+2);\n}","actors":[],"particles":[{"glyphs":"*·.","count":40,"x":"mod(rand(k)*w - t*3,w)","y":"mod(t*(1+rand(k+9)*2)+rand(k+3)*h,h)","color":"#e8f4ff"}],"background":{"effect":"aurora","palette":["#2a4a8a","#3a6aa0","#5a4a9a"],"speed":0.4,"intensity":0.3}}},
126 {"phase":"thinking","world":"zen rock garden","style":"pixel art","concept":"lighthouse in a storm: Clawd as keeper atop a pixel-art lighthouse, beam sweeping the dark sea for {what} on a tossing boat","source":"claude-opus-5-5","scene":{"concept":"lighthouse in a storm: Clawd as keeper atop a pixel-art lighthouse, beam sweeping the dark sea for {what} on a tossing boat","code":"const drops=Array.from({length:50},(_,i)=>({x:rand(i)*w,y:rand(i+9)*h*2}));\nfunction frame(t,dt){\nfor(const d of drops){d.y+=dt*30;d.x-=dt*8;if(d.y>h*2){d.y=0;d.x=rand(t+d.x)*w+20}pixel(d.x,d.y,'#6a8cc0')}\nconst fl=fract(t*0.13)<0.02;if(fl)fill(0,0,w,3,' ',null,'#2a3048');\nif(fl||fract(t*0.13+0.02)<0.01)line(70,0,74,4,'/','#fff6a0');\nfor(let x=0;x<w;x++){const y=13+Math.sin(x*0.25+t*2)*1.5+Math.sin(x*0.07-t)*1;for(let p=Math.floor(y);p<18;p++)pixel(x,p,p<y+1?'#cfe8ff':'#2d5a8a')}\nconst lx=12;pixels(lx,4,'..rrrr..\\n.wwwwww.\\n.rrrrrr.\\n..wwww..\\n..rrrr..\\n..wwww..\\n..rrrr..\\n.wwwwww.\\n.rrrrrr.\\nggggggggg\\nggggggggg\\nggggggggg\\nggggggggg\\nggggggggg',{r:'#e05050',w:'#eeeeee',g:'#7a7a80'});\nconst a=Math.sin(t*0.6),bx=Math.max(0,60+a*55),ex=Math.max(0,1+lx+4);\nfor(let i=0;i<40;i++){const k=i/40,x=lerp(lx+18,bx,k),y=lerp(2,10+a,k);put(x,y/2,'░','#fff3a0')}\nconst c=clawd(lx-3,-1.5,{facing:a>0?1:-1,arms:{left:'down',right:'out'},eyes:fl?'wide':'open'});\npixels(c.top.x-3,c.top.py-2,'.yyyyy.\\nyyyyyyy',{y:'#f0d040'});\nconst bx2=70+Math.sin(t*0.2)*20,by=12+Math.sin(bx2*0.25+t*2)*1.5;\npixels(bx2,by-5,'...m....\\n...m....\\nbbbbbbbb\\n.bbbbbb.',{m:'#ddd',b:'#c08040'});\nconst hit=Math.abs(bx-bx2-4)<10;if(hit)text(bx2-2,Math.floor(by/2)-3,'{what}','#fff6a0');\nconst s=fract(t/16);say(s<0.3?'Somewhere out there is {what}. Sweeping...':s<0.6?(hit?'Got you in the beam, {what}!':'Lost it in the swell.'):s<0.8?'Thunder means think harder.':'Keep the light turning.',c.top.x,c.row);\n}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#1a2238","#24304e","#3a4870"],"speed":1.2,"intensity":0.2}}},
127 {"phase":"reading","world":"cyberpunk street","style":"pixel art","concept":"beehive: Clawd in a beekeeper veil reading the waggle dances of bees on a honeycomb wall to learn where {what} came from","source":"claude-opus-5-5","scene":{"concept":"beehive: Clawd in a beekeeper veil reading the waggle dances of bees on a honeycomb wall to learn where {what} came from","code":"const bees=Array.from({length:7},(_,i)=>({p:rand(i)*6,c:20+i*14}));\nfunction frame(t,dt){for(let y=0;y<h;y++)for(let x=0;x<w;x++){const o=(Math.floor(x/4)+y)%2;put(x,y,o?'⬡'.length?'▒':'▒':'░',o?'#7a5a10':'#5a4208');}\nfor(let i=0;i<6;i++){const cx=60+i*9,cy=1+(i%2)*2;pixels(cx,cy*2,'.####.\\n######\\n######\\n.####.',{'#':i<=Math.floor(t/4)%7?'#ffc83a':'#8a6a20'});}\ntext(62,8,'{what}','#fff2b0');\nfor(const b of bees){const a=t*3+b.p,bx=b.c+Math.sin(a)*4+(Math.floor(t)%2?Math.sin(a*2)*2:0),by=4+Math.sin(a*2)*3;pixels(Math.floor(bx),Math.floor(by),'.w.\\nyky\\n.y.',{w:'#e8f4ff',y:'#ffd23a',k:'#222'});}\nconst cx=6+Math.sin(t*0.3)*3;const c=clawd(cx,4.5,{facing:1,arms:{left:'down',right:fract(t*0.25)<0.5?'out':'up'}});\npixels(c.top.x-5,c.top.py-3,'..wwwwwwwww..\\nwwwwwwwwwwwww',{w:'#f4f4e8'});\nfill(c.x,c.eyes.row,14,1,'░','#cfd8c0');\npixels(c.right.x,c.right.py-2,'.s.\\nsss\\n.s.',{s:'#aaa'});\nconst L=['Each waggle points to a line in {what}.','The bees know where this function lives.','Figure-eight means: follow the import.','Honey cells fill as I understand it.'];\nif(t>2)say(L[Math.floor((t-2)/7)%4],c.top.x,c.row);}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#3a2a05","#5a4208","#7a5a10"],"speed":0.4,"intensity":0}}},
128 {"phase":"editing","world":"outer space","style":"pixel art","concept":"airport runway: Clawd as ground crew patching a jet labeled {what} on the tarmac, then waving it off to takeoff, next jet taxis in","source":"claude-opus-5-5","scene":{"concept":"airport runway: Clawd as ground crew patching a jet labeled {what} on the tarmac, then waving it off to takeoff, next jet taxis in","code":"const L='{what}'.padEnd(20).slice(0,20);const P=[' __ ','| \\____________________________ ','|== '+L+' ◘◘◘◘ \\__ ',' \\______________________________=_>',' ‾o‾ ‾o‾ '];let n=0;function frame(t,dt){const c=t%12;n=Math.floor(t/12);for(let x=0;x<w;x++){put(x,8,'═','#667');if(x%8<4)put(x,7,x%16<4?'─':' ','#cc4')}sprite(2,1,'┌─┐\\n│▓│\\n│ │\\n│ │\\n│ │\\n│ │','#8ab');put(3,0,fract(t)<.5?'●':'○','#f44');let px,py=2;if(c<3)px=lerp(-40,30,Math.sin(c/3*Math.PI/2));else if(c<8.5)px=30;else{const k=c-8.5;px=30+k*k*9;py=2-Math.max(0,k-1.2)*1.6}sprite(px,py,P.join('\\n'),'#dde');text(px+5,py+2,L,'#fc6');let cx=72,arms='up',st=-1;if(c>3&&c<8){cx=lerp(72,px+18,smoothstep(3,4,c));arms=fract(c*3)<.5?'up':'out';st=Math.floor(cx/1.5)%4}else if(c>=8&&c<8.5)cx=72;const g=clawd(cx,4,{facing:c>3&&c<4?-1:0,stride:st,arms});pixels(g.top.x-3,g.top.py-2,'.oooooo.\\noooooooo','{\"o\":\"#fa0\"}'.length?{o:'#fa0'}:{});if(c>4&&c<8){for(let i=0;i<5;i++)put(cx+6+rand(i+Math.floor(t*10))*8,3-rand(i*3+Math.floor(t*10))*2,'*✦'[i%2],'#ff8')}if(c>=8.5){put(g.left.x-1,g.left.row,'/','#f84');put(g.right.x+1,g.right.row,'\\\\','#f84')}if(c>4.5&&c<7.5)say(n%2?'Patching {what} mid-taxi. Do not tell the FAA.':'Tightening {what}. Seatbelts, everyone.',g.top.x,g.row);if(c>9&&c<11.5)say('Cleared for takeoff! Next edit taxiing in.',g.top.x,g.row)}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334","#556","#99a"],"speed":0.3,"intensity":0.2}}},
129 {"phase":"editing","world":"submarine","style":"text art","concept":"underground mine: Clawd in a hard hat chiseling a glowing ore vein labeled {what}, filling a mine cart that rolls off down the rails","source":"claude-opus-5-5","scene":{"concept":"underground mine: Clawd in a hard hat chiseling a glowing ore vein labeled {what}, filling a mine cart that rolls off down the rails","code":"let sp=[];\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,0,x%9==0?'┬':'▀','#6b5a4a');put(x,h-1,x%4==0?'╪':'═','#8a7a6a');}\nfor(let x=0;x<w;x+=23){for(let y=1;y<h-1;y++)put(x,y,'║','#9a6a3a');text(x-1,1,'═══','#9a6a3a');}\nfor(let i=0;i<60;i++){let x=(i*37)%w,y=1+(i*13)%7;put(x,y,'░','#4a3e34');}\nconst vx=70;sprite(vx,2,'▓▒◆▒▓▒\\n▒◆▓◆▒◆\\n▓▒◆▓◆▒\\n▒▓▒◆▒▓','#5fd0e0');\ntext(vx,1,'{what}','#ffe08a');\nconst p=fract(t/8),hit=fract(t*1.5)<0.2;\nconst c=clawd(52,h-5,{facing:1,arms:hit?{right:'up'}:{right:'out'}});\nsprite(c.top.x-3,c.row-1,'▄███▄','#ffcc33');put(c.top.x+1,c.row-1,'●','#fff8b0');\nsprite(c.right.x,c.right.row-(hit?1:0),hit?'⌐':'─╼','#c0c0c0');\nif(hit&&Math.random()<0.6)sp.push({x:vx,y:4,vx:-3-Math.random()*6,vy:-3,l:1});\nfor(const s of sp){s.x+=s.vx*dt;s.y+=s.vy*dt;s.vy+=12*dt;s.l-=dt;if(s.l>0)put(s.x,s.y,'✦','#9ff0ff');}\nsp=sp.filter(s=>s.l>0);\nconst cx=p<0.6?30:30-smoothstep(0.6,1,p)*60;\nconst fillN=Math.floor(clamp(p/0.6,0,1)*5);\nsprite(cx,h-4,' '+'◆'.repeat(fillN).padEnd(5,' ')+' \\n╲█████╱\\n ◎ ◎ ','#a08060');\nif(fillN)text(cx+1,h-4,'◆'.repeat(fillN),'#5fd0e0');\nif(p>0.2&&p<0.55)say('Chipping out the bad bits of {what}. Cart\\'s filling up!',c.top.x,c.row-1);\nif(p>0.62&&p<0.9)say('Load one rolling. More fix in this vein.',c.top.x,c.row-1);\n}","actors":[],"particles":[{"glyphs":"·.","count":20,"x":"rand(k)*w","y":"1+mod(t*0.6+rand(k+5)*7,7)","color":"#7a6a5a"}],"background":{"effect":"pulse","palette":["#1a1410","#2a2018","#3a2a1a"],"speed":0.4,"intensity":0.2}}},
130 {"phase":"building","world":"mountain climb","style":"text art","concept":"skate park: Clawd carving endless halfpipe runs while a graffiti build-progress mural fills in on the wall behind","source":"claude-opus-5-5","scene":{"concept":"skate park: Clawd carving endless halfpipe runs while a graffiti build-progress mural fills in on the wall behind","code":"let tags=0\nfunction frame(t,dt){\nconst L=8,R=w-9,P=22;\nlet pr=Math.min(1,t/90);\ntext(Math.floor(w/2)-14,0,'╔══ BUILDING '+'{what}'.padEnd(20)+'═╗','#7aa2f7');\nconst bw=30,bx=Math.floor(w/2)-15;\nfor(let i=0;i<bw;i++)put(bx+i,1,i<pr*bw?'▓':'░',i<pr*bw?hsl(i*12+t*40,0.8,0.6):'#445');\nfor(let x=0;x<w;x++){let r=0;if(x<L+P)r=Math.pow(Math.max(0,(L+P-x)/P),2)*5;else if(x>R-P)r=Math.pow(Math.max(0,(x-R+P)/P),2)*5;\nconst y=Math.round(8-r);if(x>=L-2&&x<=R+2){put(x,y,r>0.5?(x<w/2?'╲':'╱'):'═','#c0c0c0');for(let yy=y+1;yy<h;yy++)put(x,yy,'░','#556')}}\nconst s=(1-Math.cos(t*1.1))/2,cx=L+2+s*(R-L-18),mid=cx+7;\nlet r=0;if(mid<L+P)r=Math.pow((L+P-mid)/P,2)*5;else if(mid>R-P)r=Math.pow((mid-R+P)/P,2)*5;\nconst air=(s<0.04||s>0.96)?1.5:0,top=Math.max(-1,7-4-r-air);\nconst dir=Math.sin(t*1.1)>0?1:-1;\nconst c=clawd(cx,top,{facing:dir,arms:r>2?'up':'out',eyes:air>0?'wide':'open'});\ntext(cx+3,top+4,'▀o══o▀','#e0af68');\nsprite(c.top.x-3,c.top.row-1,'▄▄▄▄▄▄','#f7768e');\nif(air>0)text(cx+16*(dir<0?0:1)-2,top,'✦','#ffd700');\nconst lines=['{what} is compiling. Time for a few laps.','Kickflip per module. Still going.','Bundler says almost. Bundler lies.','Halfpipe count: lots. Build: not yet.'];\nif(fract(t/12)<0.5)say(lines[Math.floor(t/12)%4],c.top.x,c.row);\n}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#223","#334","#2a2a44"],"speed":0.5,"intensity":0}}},
131 {"phase":"agents","world":"outer space","style":"text art","concept":"ancient tomb: Clawd as head archaeologist sends little torch-bearing helper Clawds down three tomb shafts, waiting by the sarcophagus labele","source":"claude-opus-5-5","scene":{"concept":"ancient tomb: Clawd as head archaeologist sends little torch-bearing helper Clawds down three tomb shafts, waiting by the sarcophagus labeled {what} as relics come back up","code":"const sh=[22,52,82];let th=[0,0,0];function frame(t,dt){for(let x=0;x<w;x++){put(x,0,x%6==0?'╦':'═','#a08850');put(x,h-1,x%5==0?'┴':'▀','#7a6440')}for(let i=0;i<w;i+=11)text(i+2,1,'𓀀'.length>1?'◇ ▲ ○':'◇','#6a5a3a');for(const s of sh){fill(s,2,8,6,' ',null,'#1a1208');text(s,2,'╔══════╗','#b09060');for(let y=3;y<8;y++){put(s,y,'║','#b09060');put(s+7,y,'║','#b09060')}}for(let i=0;i<3;i++){const p=fract(t*0.12+i*0.33),d=p<0.5?p*2:2-p*2,yy=3+d*3.5,s=sh[i];text(s+2,Math.floor(yy),'(o)','#ffcf6a');put(s+3,Math.floor(yy)-1,'*',fract(t*5+i)<0.5?'#ff9a30':'#ffe080');if(p>0.5)put(s+5,Math.floor(yy),'◆',hsl(i*120+40,0.8,0.6));if(p>0.97&&th[i]<t-2){th[i]=t}if(t-th[i]<1.2&&th[i]>0)text(s+1,1,'✦ relic!','#ffe060')}sprite(100,4,'┌──────────┐\\n│ ☥ ☥ ☥ │\\n└──────────┘','#d0b070');text(101,3,'{what}'.slice(0,12),'#ffd890');const c=clawd(2,h-5,{facing:1,arms:fract(t*0.3)<0.15?'up':'down'});text(c.top.x-3,c.top.row-1,'▄███▄','#c8a060');const m=Math.floor(t/12)%3;const L=['Three helpers, three shafts, one {what}.','Nobody touch the cursed tile. Again.','Relics in. Curses out. Mostly.'];if(fract(t/12)<0.6)say(L[m],c.top.x,c.row)}","actors":[],"particles":[{"glyphs":"·.","count":25,"x":"rand(k)*w","y":"2+mod(t*(0.5+rand(k+3))+rand(k+7)*6,6)","color":"#8a7650"}],"background":{"effect":"fire","palette":["#2a1a08","#4a2c10","#6a4018"],"speed":0.4,"intensity":0.15}}},
132 {"phase":"searching","world":"haunted house","style":"text art","concept":"haunted train station: Clawd as night porter with a lantern searching a ghostly platform; ghost trains pass, one finally stops carrying {wha","source":"claude-opus-5-5","scene":{"concept":"haunted train station: Clawd as night porter with a lantern searching a ghostly platform; ghost trains pass, one finally stops carrying {what}","code":"let gs=[0,1,2,3].map(i=>({x:rand(i)*120,y:1+rand(i+5)*3,p:rand(i+9)*6}))\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,h-1,x%6==0?'┴':'═','#776655');put(x,h-3,'─','#554466')}\nfor(let i=0;i<w;i+=30){sprite(i+2,0,'┌────┐\\n│ ◷ │\\n└─┬──┘','#8877aa')}\nconst cyc=mod(t,20),stop=cyc>8&&cyc<15\nlet tx=stop?30:(cyc<=8?lerp(w+5,30,smoothstep(3,8,cyc)):lerp(30,-80,smoothstep(15,19,cyc)))\nconst car='╔══════════════╗\\n║ ○ ○ ○ ○ ○ ║\\n╚═◎════════◎═╝'\nfor(let k=0;k<3;k++)sprite(tx+k*18,h-6,car,mix('#88ccbb','#335555',0.5+0.5*Math.sin(t*3+k)))\nif(stop)text(tx+20,h-7,'[ {what} ]','#ffdd66')\nfor(const g of gs){g.x-=dt*3;if(g.x<-5)g.x=w+5;text(g.x,g.y+Math.sin(t+g.p)*0.6,'(o_o)','#ccccff')}\nconst cx=stop?lerp(4,14,smoothstep(9,11,cyc)):4+Math.sin(t*0.5)*2\nconst c=clawd(cx,h-7,{facing:1,arms:{left:'down',right:'out'},stride:Math.floor(cx/1.5)%4})\ntext(c.right.x,c.right.row,'☼','#ffcc33')\nif(cyc>9&&cyc<14)say('Last train carries {what}. Found it!',c.top.x,c.row)\nelse if(cyc<6)say('Searching the platforms for {what}...',c.top.x,c.row)\n}","actors":[],"particles":[{"glyphs":"·˙","count":20,"x":"rand(k)*w","y":"mod(t*0.5+rand(k+3)*h,h)","color":"#665588"}],"background":{"effect":"aurora","palette":["#221133","#332255","#224444"],"speed":0.4,"intensity":0.2}}},
133 {"phase":"searching","world":"farm","style":"text art","concept":"weather report on a farm: Clawd as TV weatherman pointing at a radar map sweeping fields for {what}, a storm front of matches rolling in","source":"claude-opus-5-5","scene":{"concept":"weather report on a farm: Clawd as TV weatherman pointing at a radar map sweeping fields for {what}, a storm front of matches rolling in","code":"let tk=0\nfunction frame(t,dt){\nconst cx=78,cy=4\nfor(let y=0;y<h;y++)for(let x=0;x<64;x++){const f=(x>>3)+(y>>1);put(x,y,(f%3==0)?'░':(f%3==1?'▒':' '),f%2?'#4a8a3a':'#7a6a2a')}\ntext(2,0,'FARM COUNTY DOPPLER','#ffffff')\nconst a=t*1.2\nfor(let r=0;r<16;r++){const x=32+Math.cos(a)*r*2,y=cy+Math.sin(a)*r*0.5;put(x,y,'•','#6cf')}\nfor(let i=0;i<6;i++){const sx=10+i*9+Math.sin(t*0.3+i)*3,sy=2+(i%3)*2;const hit=Math.abs(mod(a,6.283)-mod(Math.atan2((sy-cy)*2,(sx-32)/2),6.283))<0.4;text(sx,sy,hit?'▓▓▓':'░░░',hit?'#ff5':'#5af')}\nconst fx=mod(t*3,70)-6\ntext(fx,6,'≈≈☁☁≈≈','#ccd')\ntext(2,8,'grep front: {what}','#ffd060')\nfor(let y=0;y<h;y++)put(64,y,'║','#888')\nconst ph=Math.floor(t/4)%3\nconst c=clawd(cx,4,{facing:-1,arms:{left:'up',right:'down'}})\nsprite(cx-6,c.left.row,'━━━━─','#ccc')\nsprite(cx+2,c.top.row-1,'▄▄▄▄▄▄▄▄▄','#335')\nsprite(cx+5,c.eyes.row+2,'▼','#e33')\nconst lines=['Scattered matches over the barn!','Heavy grep showers near {what}.','100% chance of finding it.']\nsay(lines[ph],cx+7,c.row)\nfor(let x=96;x<w;x++){put(x,7,'▄','#6a4')}\ntext(98,4,'/▔▔\\\\','#c55');text(98,5,'|▢▢|','#c55');text(98,6,'|_▯|','#c55')\nconst cow=104+Math.sin(t*0.4)*6;text(cow,5,'(__)','#fff');text(cow,6,'/||\\\\','#eee')\n}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#224","#336","#448"],"speed":1,"intensity":0}}},
134 {"phase":"testing","world":"fishing pond","style":"text art","concept":"arctic ice fishing: Clawd in a parka sits by an ice hole, each bite on the line is a test result pulled up as a fish, tally grows on a snowb","source":"claude-opus-5-5","scene":{"concept":"arctic ice fishing: Clawd in a parka sits by an ice hole, each bite on the line is a test result pulled up as a fish, tally grows on a snowbank sign","code":"let fish=[],tally=0,next=3,state=0,st=0;\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,6,'▀','#dfefff');put(x,7,'░','#9fc4e0');put(x,8,'▒','#6f9cc0')}\nsprite(4,1,' /\\\\ \\n / \\\\ \\n /____\\\\ \\n │ ▄▄ │','#cfe6ff');\nsprite(w-30,2,'┌──────────────────┐\\n│{what}'.padEnd(20)+'\\n└──────────────────┘','#ffd27a');\ntext(w-27,3,'catch: '+tally+' ✓','#9fe0a0');\nconst hx=52;text(hx-2,6,'( )','#3a7bd5');put(hx+1,6,'~','#7ab8ff');\nconst c=clawd(30,2,{pose:'sit',facing:1,arms:{left:'down',right:'up'},eyes:state==1?'wide':'open'});\nsprite(c.top.x-3,c.top.row-1,'▄▄▄▄▄▄','#e05050');\nconst tip=c.right;let ty=6;\nif(t>next&&state==0){state=1;st=t}\nif(state==1){ty=6+Math.sin((t-st)*20)*0.6;if(t-st>1){state=2;st=t}}\nif(state==2){const k=smoothstep(0,0.8,t-st);ty=lerp(6,0,k);if(k>=1){fish.push({x:hx,y:0,vx:4+Math.random()*3,vy:-3,ok:Math.random()<0.85});tally++;state=0;next=t+3+Math.random()*3}}\nline(tip.x,tip.row,hx+1,ty,'·','#ccc');\nif(state==2)text(hx,ty,'><>','#ff9a6a');\nfor(const f of fish){f.vy+=12*dt;f.x+=f.vx*dt;f.y+=f.vy*dt;if(f.y>5){f.y=5;f.vx=0;f.vy=0}text(f.x,f.y,f.ok?'><>':'>✗>',f.ok?'#7fd0ff':'#ff6060')}\nif(fish.length>12)fish.shift();\nif(t>2&&t<7)say('Line in {what}. Bite = pass. Hope.',c.top.x,c.row);\nelse if(t>14&&t<19)say('Frozen paws, warm tests. Keep biting!',c.top.x,c.row);\n}","actors":[],"particles":[{"glyphs":"*·.","count":40,"x":"mod(rand(k)*w+t*2,w)","y":"mod(t*(1+rand(k+9)*1.5)+rand(k+3)*6,6)","color":"#ffffff"}],"background":{"effect":"aurora","palette":["#2fbf8f","#3a7bd5","#8f5fd0"],"speed":0.5,"intensity":0.35}}},
135 {"phase":"testing","world":"underground mine","style":"text art","concept":"film noir city at night: Clawd as a fedora detective under a rain-soaked streetlamp, running a police lineup where each test steps up and is","source":"claude-opus-5-5","scene":{"concept":"film noir city at night: Clawd as a fedora detective under a rain-soaked streetlamp, running a police lineup where each test steps up and is cleared or marked guilty","code":"let R=Array.from({length:70},(_,i)=>({x:rand(i)*w,y:rand(i+7)*h,v:9+rand(i+3)*6}))\nfunction frame(t,dt){\nfor(let x=0;x<72;x++){const b=Math.floor(x/8),bh=2+Math.floor(rand(b)*5);for(let y=h-1-bh;y<h-1;y++){const win=x%8>1&&x%8<6&&y%2==0&&rand(b*31+y*7+x)>0.55;put(x,y,win?'▪':'█',win?'#e8c860':'#34405e')}}\nfor(let x=0;x<w;x++)put(x,h-1,'▁','#667')\nconst lx=40;put(lx,1,'╤','#aab');for(let y=2;y<h-1;y++)put(lx,y,'│','#889');for(let y=2;y<h-1;y++){const s=y-1;for(let d=-s;d<=s;d++)if(d)put(lx+d,y,'░','#6a6040')}\nconst L=78,Rx=116;for(let y=0;y<h-1;y++){put(L,y,'║','#8899aa');put(Rx,y,'║','#8899aa')}for(let y=1;y<h-1;y+=2)for(let x=L+1;x<Rx;x++)put(x,y,'─','#3f4a5a');text(L+2,0,'LINEUP: {what}','#c9d4e0')\nconst i=Math.floor(t/7),p=t%7,bad=rand(i*3+1)<0.22,sx=p<1.5?lerp(Rx+2,95,smoothstep(0,1.5,p)):p<5.5?95:lerp(95,L-8,smoothstep(5.5,7,p));\nif(p>1.4&&p<1.7)fill(L+1,1,Rx-L-1,h-2,' ',null,'#d8d8c0')\nsprite(sx,h-5,' ▄█▄ \\n ███ \\n█████\\n █ █ ',p>3&&bad?'#e05555':'#b8bcc8');text(sx,h-6,'#'+(i+1),'#9ab')\nif(p>3&&p<5.8)text(sx-2,1,bad?'✗ GUILTY':'✓ CLEAR',bad?'#ff6060':'#70e090')\nconst c=clawd(lx-7,h-5,{facing:1,look:{x:2,y:0},arms:p>3&&p<4?{left:'down',right:'out'}:'down',color:'#d97757'});\nfill(c.top.x-5,c.top.row-1,11,1,'▄','#555a66');text(c.top.x-3,c.top.row-2,'▄▄▄▄▄▄▄','#555a66')\nput(c.right.x+1,c.right.row,'~','#ccc')\nconst ln=t<7?'Stakeout on {what}. Every test takes the lineup.':(p>3?(bad?'That one has no alibi. Booking it.':'Alibi checks out. Next suspect.'):'Step into the light, pal.');say(ln,c.top.x,c.row-1)\nfor(const d of R){d.y+=d.v*dt;if(d.y>h){d.y=0;d.x=rand(t*13+d.x)*w}put(d.x,d.y,'╱','#5a7aa0')}}","actors":[],"particles":[],"background":{"effect":"rain","palette":["#223355","#334466","#445577"],"speed":1,"intensity":0}}},
136 {"phase":"searching","world":"bowling alley","style":"text art","concept":"ant colony up close: Clawd as a tiny ant-scout tunneling through an underground cross-section, ants carrying crumbs labeled {what} through b","source":"claude-opus-5-5","scene":{"concept":"ant colony up close: Clawd as a tiny ant-scout tunneling through an underground cross-section, ants carrying crumbs labeled {what} through branching tunnels while Clawd follows scent trails","code":"let ants=Array.from({length:9},(_,i)=>({p:rand(i)*200,s:3+rand(i+7)*4,r:1+(i%3)*3}))\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){for(let y=0;y<h;y++){const n=noise2(x*0.15,y*0.4);put(x,y,n>0.6?'░':' ','#5a4030','#1a110b')}}\nfor(let x=0;x<w;x++){put(x,0,x%5==0?'ψ':'▀','#5c9a3c');put(x,2,' ',null,'#2b1d12');put(x,5,' ',null,'#2b1d12');put(x,8,' ',null,'#2b1d12')}\nfor(const c of [20,55,90]){for(let y=1;y<9;y++)put(c,y,'║','#7a5a3a','#2b1d12')}\ntext(96,5,'◆ {what}','#ffd27f','#2b1d12')\nconst k=smoothstep(0,40,t%50);const cx=lerp(-14,80,k);const row=t%50<20?1.5:4.5\nfor(const a of ants){a.p+=a.s*dt;const x=mod(a.p,w+10)-5;const y=a.r>=7?8:a.r>=4?5:2;put(x,y,'ж','#c94a3a','#2b1d12');put(x+1,y,'•','#c94a3a','#2b1d12');if(a.r==1)put(x+2,y,'▪','#ffd27f','#2b1d12')}\nfor(let i=0;i<12;i++){const px=cx-2-i*2;if(px>0)put(px,row+2.5,'·','#b0e070')}\nconst c=clawd(cx,row-1,{pose:'walk',facing:1,stride:Math.floor(cx/1.5)%4,scale:1})\nput(c.top.x-2,c.top.row-1,'\\\\','#d97757');put(c.top.x+2,c.top.row-1,'/','#d97757')\nconst m=t%50;if(m>5&&m<12)say('Sniffing every tunnel for {what}...',c.top.x,c.row);else if(m>28&&m<36)say('Scent is strong. The crumb trail never lies.',c.top.x,c.row);else if(m>42)say('Found {what}! The colony delivers.',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"plasma","palette":["#2b1d12","#3a2818","#4a3420"],"speed":0.3,"intensity":0}}},
137 {"phase":"searching","world":"submarine","style":"pixel art","concept":"ancient tomb: Clawd as explorer with a torch slides along a hieroglyph wall, torchlight revealing glyphs until one matches {what} and a hidd","source":"claude-opus-5-5","scene":{"concept":"ancient tomb: Clawd as explorer with a torch slides along a hieroglyph wall, torchlight revealing glyphs until one matches {what} and a hidden door grinds open","code":"let gl='☥◇○△♣▲◆●□♠♦★✦◐◑▼';let tgt=w-30;function frame(t,dt){const T=t%24;for(let y=0;y<h;y++)for(let x=0;x<w;x++){put(x,y,' ',null,y==h-1?'#3a2a18':'#1c140c')}for(let x=0;x<w;x+=6)text(x,0,'▀▀▀▀▀▀','#5a4428');const cx=lerp(2,tgt-16,smoothstep(0,14,T));const tx=cx+15;for(let x=0;x<w;x+=4){for(let y=1;y<4;y++){const d=Math.abs(x-tx);const k=clamp(1-d/18,0,1);if(k>0.05)put(x+1,y,gl[Math.floor(rand(x*7+y)*16)],mix('#2a1e10','#e8c070',k))}}const found=T>14;const dx=tgt+2;const open=smoothstep(15,18,T);fill(dx,1,10,7,' ',null,'#0a0604');const sh=Math.floor(open*7);fill(dx,1+sh,10,7-sh,'▓',found?'#c8a050':'#5a4428');if(!found||open<0.5)text(dx-2,0,'{what}'.slice(0,20),found?'#ffd75f':'#6a5030');if(found&&open>0.6){for(let i=0;i<6;i++)put(dx+1+Math.floor(rand(i+Math.floor(t*4))*8),2+Math.floor(rand(i+9)*4),'✦','#ffd75f')}const walk=T<14;const c=clawd(cx,h-5,{pose:walk?'walk':'stand',stride:Math.floor(cx/1.5)%4,facing:1,arms:{left:'down',right:'up'},eyes:found?'wide':'open'});pixels(c.top.x-4,c.top.py-2,'..####..\\n########','{\"#\":\"#b08850\"}'.length?{'#':'#b08850'}:{});const f=Math.floor(t*8)%2;pixels(c.right.x,c.right.py-4,f?'.o.\\nooo\\n.#.\\n.#.':'o..\\n.oo\\n.#.\\n.#.',{o:'#ff9a30','#':'#7a5030'});for(let i=0;i<4;i++)put(c.right.x+1+Math.sin(t*3+i)*1,c.right.row-2-((t*2+i*0.5)%1.5),'·','#ffcc66');if(T>2&&T<7)say('Reading walls for {what}... glyph by glyph.',c.top.x,c.row);if(T>15&&T<22)say('Found it! {what} was behind this door.',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"plasma","palette":["#1a1008","#2a1a0c","#3a2410"],"speed":0.3,"intensity":0}}},
138 {"phase":"searching","world":"ski slope","style":"pixel art","concept":"carnival midway: Clawd at a shooting-gallery booth, popping tin ducks on a conveyor until one labeled {what} gets hit","source":"claude-opus-5-5","scene":{"concept":"carnival midway: Clawd at a shooting-gallery booth, popping tin ducks on a conveyor until one labeled {what} gets hit","code":"let sc=0,hit=-1;\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,0,x%4<2?'▀':' ',x%8<4?'#e05555':'#f0f0f0');}\nfill(20,1,80,1,'─','#c9a227');fill(20,5,80,1,'═','#c9a227');\nfor(let i=0;i<8;i++){const dx=20+mod(i*11+t*6,80);const id=Math.floor((i*11+t*6)/80)*8+i;const tg=id%5==3;const dn=mod(t,4)>3.2&&Math.abs(dx-60)<4;\nif(dn){put(dx,4,'▁','#888');if(tg)hit=id;}else{pixels(dx,6,'.yy.\\nyyyyy\\n.yyy.',{y:tg?'#ffd84a':'#7fd6ff'});put(dx+1,3,'·','#000');}\nif(tg)text(dx-2,2,'{what}'.slice(0,8),'#ffd84a');}\nfor(let x=0;x<20;x++)put(x,8,'▓','#7a3b2a');for(let x=100;x<w;x++)put(x,8,'▓','#7a3b2a');\ntext(102,1,'SCORE '+Math.floor(t/4),'#9fef7f');\nconst c=clawd(52,5,{facing:0,arms:{left:'down',right:'up'},look:{x:2,y:-1}});\nsprite(c.right.x,c.right.row-1,'═╗','#aaa');\nif(mod(t,4)>3&&mod(t,4)<3.3)put(60,3,'✦','#fff');\nif(t>4&&mod(t,12)<4)say('grep shooting gallery: aiming for {what}',c.top.x,c.row);}","actors":[],"particles":[{"glyphs":"✦✧·","count":15,"x":"rand(k)*w","y":"mod(t*2+rand(k+3)*9,9)","color":"#ff9ff3"}],"background":{"effect":"pulse","palette":["#2a1040","#401a50","#1a2050"],"speed":0.6,"intensity":0.25}}},
139 {"phase":"running","world":"ancient tomb","style":"pixel art","concept":"ancient tomb staged as a theater play: Clawd as explorer waits by a sarcophagus labeled {what} while curtains, torches and a slowly opening ","source":"claude-opus-5-5","scene":{"concept":"ancient tomb staged as a theater play: Clawd as explorer waits by a sarcophagus labeled {what} while curtains, torches and a slowly opening lid play out","code":"function frame(t,dt){for(let x=0;x<w;x++){put(x,0,'▼',x%2?'#8b1e2b':'#a8323e');put(x,h-1,'▀','#6b4a2b')}let cw=Math.floor(lerp(w/2,4,smoothstep(0,3,t)));fill(0,1,cw,h-2,'║','#a8323e');fill(w-cw,1,cw,h-2,'║','#a8323e');if(cw>10)return;for(let i=0;i<3;i++){let cx=14+i*40;sprite(cx,1,'╔══╗\\n║◆◆║\\n║ ║\\n╚══╝','#c9a227');let f=noise(t*3+i);put(cx+1+(f>0.5?1:0),0,f>0.5?'^':'*','#ffb030')}let sx=60;sprite(sx,4,'╔════════════╗\\n║ ▲ ☥ ▲ ║\\n╚════════════╝','#d8b25a');text(sx+2,5,'{what}'.slice(0,10),'#ffe08a');let p=fract(t/20),lid=smoothstep(0.3,0.9,p);sprite(sx+Math.round(lid*10),3,'▄▄▄▄▄▄▄▄▄▄▄▄▄▄','#a88a3a');if(lid>0.5){for(let k=0;k<6;k++)put(sx+3+k*2,2-((t*2+k)%2),'░','#bfe')}let cx=lerp(20,42,smoothstep(3,6,t));let c=clawd(cx,4,{pose:t<6?'walk':'stand',stride:Math.floor(cx/1.5)%4,facing:1,arms:lid>0.6?'up':{left:'down',right:'out'},eyes:lid>0.6?'wide':'open'});pixels(c.top.x-4,c.top.py-2,'..####..\\n########',{'#':'#c2a060'});put(c.right.x+1,c.right.row,'¥','#ffb030');let m=t%20;if(m>6&&m<11)say('Act I: {what} runs. The tomb waits.',c.top.x,c.row);else if(m>13&&m<18)say('The lid moves! ...or is it still loading?',c.top.x,c.row)}","actors":[],"particles":[{"glyphs":"·.","count":20,"x":"rand(k)*w","y":"1+mod(t*0.5+rand(k+3)*7,7)","color":"#7a6a50"}],"background":{"effect":"pulse","palette":["#2a1a10","#3a2414","#4a2e18"],"speed":0.4,"intensity":0.2}}},
140 {"phase":"searching","world":"jungle safari","style":"pixel art","concept":"dinosaur-era jungle safari: Clawd in a pith helmet rides a long-necked brontosaurus through ferns, sniffing out giant {what} footprints that","source":"claude-opus-5-5","scene":{"concept":"dinosaur-era jungle safari: Clawd in a pith helmet rides a long-necked brontosaurus through ferns, sniffing out giant {what} footprints that lead to a hidden egg nest","code":"let ft=Array.from({length:30},(_,i)=>({x:i*9+4,s:rand(i)}))\nfunction frame(t,dt){\nconst sc=t*4;\nfor(let x=0;x<w;x++){const hh=Math.floor(noise((x+sc*0.3)*0.05)*3);for(let y=0;y<hh;y++)put(x,4-y,'▓','#2f5a3a');put(x,8,'▀','#6b4f2a','#3a2a18')}\nput(w-12,0,'●','#ffb347');\nfor(const f of ft){const fx=((f.x-sc)%270+270)%270-10;text(fx,7,'ψ','#c9a36b')}\nfor(let i=0;i<14;i++){const fx=((i*23-sc*1.6)%(w+30)+w+30)%(w+30)-15;sprite(fx,5,' \\|/ \n\\\\|//','#4caf50')}\nconst bx=28,by=3,st=Math.floor(t*4)%2;\npixels(bx,by*2+2,'......................#####\\n....................###o###\\n..................####.....\\n....############.####......\\n..##################.......\\n.#################.........\\n##################.........\\n.##..##....##..##..........\\n'+(st?'.##...##..##...##..........':'..##.##....##.##...........'),{'#':'#7fb069','o':'#111'});\nconst c=clawd(bx+9,by-1.5,{facing:1,arms:t%8<4?'down':{left:'down',right:'out'},scale:1});\npixels(c.top.x-4,c.top.py-3,'..####..\\n########',{'#':'#e8d8a8'});\nif(t%8>=4)text(c.right.x+1,c.right.row,'→','#ffd166');\nconst nx=w-30-((t*3)%60);if(t%20>12){pixels(nx+20,12,'.o.o.\\nooooo',{o:'#f2e6c9'});text(nx+16,5,'{what}','#ffd166')}\nconst lines=['Fresh tracks. {what} came through here.','These prints are huge. Big definition ahead.','Grep-osaurus, follow that scent!','A nest! Found where it hatched.'];\nconst k=Math.floor(t/5)%4;if(t%5<3.5)say(lines[k],c.top.x,c.row);\n}","actors":[],"particles":[{"glyphs":"v^","count":5,"x":"mod(rand(k)*w - t*(2+k), w)","y":"1+sin(t+k)*0.8","color":"#aab7c4"}],"background":{"effect":"aurora","palette":["#3a2a5a","#7a3a4a","#c86a3a"],"speed":0.4,"intensity":0.3}}},
141 {"phase":"reading","world":"farm","style":"pixel art","concept":"ski slope farm: Clawd skis down a snowy hillside past a barn, reading tracks in the snow that spell out {what}, a cow on a chairlift watches","source":"claude-opus-5-5","scene":{"concept":"ski slope farm: Clawd skis down a snowy hillside past a barn, reading tracks in the snow that spell out {what}, a cow on a chairlift watches","code":"let sx=0;\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){let g=Math.floor(2+x*6/w);for(let y=g;y<h;y++)put(x,y,' ',null,y==g?'#dfe8f0':'#b8c8d8');}\nfor(let i=0;i<5;i++){let px=8+i*26,py=Math.floor(2+px*6/w)-2;sprite(px,py,' ▲ \n▲▲▲\n │ ','#3c8a4a');}\nlet bx=w-22,by=1;pixels(bx,by*2,'....RRRR....\\n..RRRRRRRR..\\nRRRRRRRRRRRR\\nRWWRRRRRRWWR\\nRWWRRDDRRWWR\\nRRRRRDDRRRRR',{R:'#c0392b',W:'#f0f0f0',D:'#5a2a1a'});\nline(0,0,w-1,0,'─','#888');\nlet cx=mod(t*6,w+40)-20;sprite(Math.floor(cx),0,'│\\n▙▄▟','#aaa');text(Math.floor(cx)+1,1,'☻','#fff');\nlet p=fract(t/14),x=lerp(-10,w-30,smoothstep(0,1,p)),y=lerp(-1,3.5,p);\nfor(let k=1;k<30;k++){let tx=x-k*2;if(tx>0)put(tx,Math.floor(lerp(-1,3.5,(tx+10)/(w+20)))+4,'=','#8aa0b8');}\nlet c=clawd(x,y,{pose:'jump',facing:1,arms:'out'});\nline(Math.floor(x)-2,Math.floor(y)+4,Math.floor(x)+15,Math.floor(y)+4,'━','#3366ff');\npixels(c.top.x-3,c.top.py-3,'.YYYY.\\nYYYYYY',{Y:'#ffcc33'});\ntext(4,h-1,'tracks: {what}','#335577');\nif(p>0.35&&p<0.75)say('Reading {what} at speed. Every line a mogul.',c.top.x,c.row);}\n","actors":[],"particles":[{"glyphs":"*·","count":40,"x":"rand(k)*w","y":"mod(t*(1+rand(k+9)*2)+rand(k+3)*h,h)","color":"#ffffff"}],"background":{"effect":"aurora","palette":["#223355","#334477","#446688"],"speed":0.5,"intensity":0.2}}},
142 {"phase":"editing","world":"cooking show","style":"text art","concept":"dinosaur-era cooking show: caveman-chef Clawd grills a giant bone-in steak labeled {what} on a volcano stove while a T-rex judge watches and","source":"claude-opus-5-5","scene":{"concept":"dinosaur-era cooking show: caveman-chef Clawd grills a giant bone-in steak labeled {what} on a volcano stove while a T-rex judge watches and a tiny pterodactyl steals garnish","code":"let T=0\nfunction frame(t,dt){\nfor(let x=0;x<w;x++)put(x,8,x%9==0?'┴':'▀','#6b4f2a')\nsprite(2,1,' ▄▄▄ \\n ▟███▙ \\n ▟█████▙ \\n▟███████▙','#7a5a3a')\nfor(let i=0;i<4;i++){let a=fract(t*0.7+i/4);put(5+Math.round(Math.sin(t*3+i)),Math.round(1-a*2),'░▒'[i%2],mix('#ff7a2a','#666666',a))}\nconst cx=44;\nsprite(cx-2,6,'╔══════════════════╗\\n╚═▲▲═▲▲═▲▲═▲▲═▲▲══╝','#a0a0a0')\nconst p=fract(t/8),fl=p>0.45&&p<0.6;\nconst sy=fl?5-2.5*Math.sin(Math.PI*(p-0.45)/0.15):5;\nconst cook=Math.min(1,p*1.5);\ntext(cx+2,Math.round(sy),'(▓▓{what}▓▓)=○',mix('#e06070','#a0522d',cook))\nfor(let i=0;i<5;i++)put(cx+1+i*3,4-Math.floor(fract(t*1.5+i*0.3)*3),'~','#cccccc')\nconst c=clawd(cx-18,4,{pose:'stand',facing:1,arms:fl?'up':{left:'down',right:'out'}})\nsprite(c.top.x-3,c.top.row-1,'▄█▀█▄','#e8e8e8')\ntext(c.right.x,c.right.row,'═╕','#c8b088')\nsprite(84,0,' ▄████▄\\n █ ●▀▀█▄▄\\n ███▄▄▄▄▀\\n ▟██▌\\n ▟███▙▀\\n▟█████\\n██▀ ▀█','#5fa35a')\nconst sc=Math.floor(t/8)%3;\nif(p>0.7)sprite(92,4,'┌──┐\\n│'+(8+sc)+' │\\n└──┘','#ffd75e')\nconst px=w+10-mod(t*12,w+30);\nsprite(px,1+Math.round(Math.sin(t*4)),'\\\\▼/','#c08060')\nconst lines=['Searing {what} at 600°, volcano heat.','Flip! Even crust on every function.','Judge Rex says: more salt, fewer bugs.'];\nsay(lines[Math.floor(t/8)%3],c.top.x,c.row)\n}","actors":[],"particles":[],"background":{"effect":"fire","palette":["#3a1a0a","#7a2a0a","#c0501a"],"speed":0.6,"intensity":0.15}}},
143 {"phase":"git","world":"haunted house","style":"text art","concept":"circus big top: Clawd as tightrope walker crossing between branch platforms while commits juggle above, a trapeze merge at the end","source":"claude-opus-5-5","scene":{"concept":"circus big top: Clawd as tightrope walker crossing between branch platforms while commits juggle above, a trapeze merge at the end","code":"function frame(t,dt){for(let x=0;x<w;x++){const s=Math.floor(x/6)%2;put(x,0,'▀',s?'#e05555':'#f0e0d0');}put(2,1,'╔═══╗','#c0a060');text(3,2,'main','#ffd866');put(w-14,1,'╔═════════╗','#c0a060');text(w-13,2,'{what}'.slice(0,9),'#7fd6ff');for(let x=2;x<w-3;x++)put(x,5,'─','#b0b0c0');put(4,6,'│','#806040');put(4,7,'│','#806040');put(w-6,6,'│','#806040');put(w-6,7,'│','#806040');for(let x=0;x<w;x++)put(x,8,'▒','#704020');const p=fract(t/16),x=lerp(6,w-22,smoothstep(0.05,0.85,p));const mv=p>0.05&&p<0.85;const c=clawd(x,1.5,{pose:mv?'walk':'stand',stride:Math.floor(x/1.5)%4,facing:mv?1:0,arms:p>0.88?'up':'out',color:'#d97757'});text(c.left.x-5,c.left.row,'━━━','#ffd866');text(c.right.x+2,c.right.row,'━━━','#ffd866');for(let i=0;i<3;i++){const a=t*4+i*2.1;put(c.top.x+Math.cos(a)*5,c.row-1-Math.abs(Math.sin(a))*0.9,'●',['#ff6b6b','#7fd6ff','#a6e22e'][i]);}if(p>0.88){for(let i=0;i<14;i++)put(rand(i+Math.floor(t))*w,1+rand(i*7)*6,'✦',hsl(i*40,0.8,0.65));}const sy=p<0.3?'Commit juggling on a tightrope. No net.':p<0.88?'Staging {what}... steady... steady...':'Ta-da! Committed. Hold your applause.';if(p>0.08)say(sy,c.top.x,c.row);text(Math.floor(w/2)-6,7,'○ ○ ○ ○ ○ ○','#888899');}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#401020","#602030","#302050"],"speed":0.5,"intensity":0.2}}},
144 {"phase":"git","world":"fishing pond","style":"text art","concept":"haunted house: Clawd as ghost-hunter lantern-bearer watching commit ghosts float out of a creaky family-tree portrait (git log) and drift al","source":"claude-opus-5-5","scene":{"concept":"haunted house: Clawd as ghost-hunter lantern-bearer watching commit ghosts float out of a creaky family-tree portrait (git log) and drift along branching corridors","code":"let gs=Array.from({length:6},(_,i)=>({p:-i*0.18,b:i%2}))\nfunction frame(t,dt){\nconst W='#4a3b5c',G='#cfe8ff';\nfor(let x=0;x<w;x++){put(x,8,x%9==0?'┴':'═',W);}\nsprite(4,0,'╔════════════╗\\n║ ▄▀▀▄ ▄▀▀▄ ║\\n║ █○○█ █○○█ ║\\n║ ▀▀ ▀▀ ║\\n║ git log ║\\n╚════════════╝','#8a6d3b');\nconst y0=3;\nline(19,y0,w-4,y0,'─','#6b5a7a');\nline(50,y0,62,6,'╲','#6b5a7a');line(62,6,w-30,6,'─','#6b5a7a');line(w-30,6,w-24,y0,'╱','#6b5a7a');\nfor(let x=26;x<w-6;x+=14)put(x,y0,'●','#e0b050');\nfor(const g of gs){g.p+=dt*0.05;if(g.p>1)g.p-=1.1;if(g.p<0)continue;\nlet gx=19+g.p*(w-30),gy=y0-2+Math.sin(t*3+g.p*20)*0.5;\nif(g.b&&gx>55&&gx<w-28)gy=5+Math.sin(t*3)*0.4;\nsprite(gx,gy,' ▄▄ \\n▐°°▌\\n ▀▀▘',mix(G,'#ffffff',0.5+0.5*Math.sin(t*4+g.p*9)));}\nconst cx=Math.min(w*0.4,-14+t*6),wk=cx<w*0.4;\nconst c=clawd(cx,4,{pose:wk?'walk':'stand',stride:Math.floor(cx/1.5)%4,facing:wk?1:-1,arms:{left:'down',right:'out'},eyes:fract(t/7)<0.15?'wide':'open'});\ntext(c.right.x,c.right.row-1,'♦','#ffd166');\nfor(let i=0;i<5;i++)put(c.right.x+2+i*2,c.right.row,'·','#ffd16688');\nif(!wk&&fract(t/16)<0.5)say(fract(t/32)<0.5?'Every commit in {what} haunts this house forever.':'That branch ghost? Never merged. Still wandering.',c.top.x,c.row);\n}","actors":[],"particles":[{"glyphs":"·˙","count":20,"x":"rand(k)*w","y":"noise(t*0.3+k)*7","color":"#7a6a9a"}],"background":{"effect":"aurora","palette":["#1a1030","#2a1a40","#3a2a55"],"speed":0.4,"intensity":0.25}}},
145 {"phase":"running","world":"rocket launch pad","style":"text art","concept":"volcano island launch pad: Clawd in a hard hat watches a rocket labeled {what} on a pad beside a smoking volcano, countdown, liftoff, then w","source":"claude-opus-5-5","scene":{"concept":"volcano island launch pad: Clawd in a hard hat watches a rocket labeled {what} on a pad beside a smoking volcano, countdown, liftoff, then waits for the next","code":"let L=6;function frame(t,dt){const g=h-1;for(let x=0;x<w;x++)put(x,g,x%5==0?'┴':'─','#7a6a5a');const vx=w-34;sprite(vx,2,' ▄██▄ \n ▄██▓▓██▄ \n ▄███▓▓▓▓███▄ \n▄██████████████▄\n████████████████\n▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀','#6b4a3a');for(let i=0;i<6;i++){const s=fract(t*0.3+i/6);put(vx+7+Math.sin(i*3+t)*3*s,1-s*3+2,'░','#999999')}fill(vx+7,2,2,1,'▓','#ff6a2a');for(let x=0;x<w;x+=1){if(x<vx-4||x>vx+18){const y=g-1;if(noise(x*0.3+t)>0.7)put(x,y,'~','#3aa0d0')}}const cyc=mod(t,22),px=44;sprite(px-2,g-1,'╔═══════╗','#aaaaaa');sprite(px+9,1,'│\n├\n│\n├\n│\n├\n│','#888888');let lift=cyc>10?Math.pow(cyc-10,2)*1.2:0;const ry=g-8-lift;if(cyc<16){sprite(px+1,ry,' ▲ \n█ █\n█ █\n█ █\n███\n▀█▀','#e8e8f0');text(px+5,ry+2,'{what}','#ffd060');if(cyc>9.5)for(let i=0;i<8;i++){put(px+1+i%3,ry+6+Math.floor(rand(i+Math.floor(t*12))*3),'▓▒░'[i%3],i%2?'#ff9030':'#ffe060')}}if(cyc>=16&&cyc<18)for(let i=0;i<20;i++){const a=i*2.4,r=(cyc-16)*8;put(px+2+Math.cos(a)*r*2,1+Math.sin(a)*r*0.5,'*',hsl(i*40,0.9,0.6))}if(cyc>=18){sprite(px+1,g-7+Math.max(0,(20-cyc))*-3,' ▲ \n█ █\n█ █\n█ █\n███\n▀█▀','#e8e8f0')}const c=clawd(14,g-4,{facing:1,arms:cyc>10&&cyc<16?'up':'down',eyes:cyc>10&&cyc<14?'wide':'open',blink:fract(t*0.3)<0.04});sprite(c.top.x-3,c.top.row-1,'▄███▄','#ffcc00');if(cyc>3&&cyc<10)text(31,g-6,'T-'+Math.max(1,Math.ceil(10-cyc)),'#ff5050');if(cyc<3)say('{what} fueled. volcano says hi.',c.top.x,c.row);else if(cyc>11&&cyc<15)say('liftoff! go {what}, go!',c.top.x,c.row);else if(cyc>18)say('next one on the pad...',c.top.x,c.row)}","actors":[],"particles":[],"background":{"effect":"starfield","palette":["#334466","#556688","#8899bb"],"speed":0.4,"intensity":0.3}}},
146 {"phase":"agents","world":"bowling alley","style":"text art","concept":"race track pit stop: Clawd as crew chief waits while helper mini-Clawd pit crews service a car labeled {what}, cars lapping past in the back","source":"claude-opus-5-5","scene":{"concept":"race track pit stop: Clawd as crew chief waits while helper mini-Clawd pit crews service a car labeled {what}, cars lapping past in the background","code":"let cars=[0,1,2].map(i=>({x:rand(i)*w,v:30+rand(i+5)*20,c:hsl(rand(i+9)*360,0.7,0.6)}))\nfunction frame(t,dt){\nfor(let x=0;x<w;x++){put(x,1,x%4<2?'▀':' ','#e0e0e0');put(x,3,'─','#555566');put(x,8,'▀','#777788')}\nfor(const c of cars){c.x+=c.v*dt;if(c.x>w+10)c.x=-12;text(c.x,2,'▄█▀█▄▶',c.c)}\nconst px=50;text(px,7,'═'.repeat(30),'#ffd75f');text(px+2,4,'PIT 1','#ffd75f')\nconst ph=fract(t/12),dn=ph>0.8;const cx=dn?px+6+(ph-0.8)*400:px+6\nsprite(cx,5,' ▄▄██████▄▄▄ \n▐█({o})█▀█({o})▌',dn?'#5fd7ff':'#e06c75')\ntext(cx+3,4,'{what}'.slice(0,8),'#ffffff')\nconst helpers=[[px-2,'#8fd18f'],[px+20,'#b19cd9'],[px+9,'#f0c674']]\nhelpers.forEach(([hx,col],i)=>{if(dn)return;const b=Math.sin(t*12+i*2)>0;clawd(hx,4.5+(i==2?-0.5:0)+(b?0:0.5),{scale:1,color:col,arms:b?'up':'down',facing:i==1?-1:1});if(b)put(hx+(i==1?-1:14),6,'*','#ffffff')})\nconst c=clawd(10,4,{facing:1,arms:dn?'up':{left:'down',right:'out'},eyes:dn?'wide':'open'})\npixels&&0;text(c.top.x-3,c.row-1,'▄███▄','#333333')\ntext(c.right.x+1,c.right.row,dn?'⚑':'▭','#e0e0e0')\nconst m=Math.floor(t/12)%3\nconst lines=['Pit crew on {what}. Four subagents, four tires.','Helpers still wrenching. Box box, no rush.','Out of the pits! Next car in for {what}...']\nif(fract(t/12)>0.1&&fract(t/12)<0.6)say(lines[m],c.top.x,c.row)\nelse if(dn)say('GO GO GO!',c.top.x,c.row)\n}","actors":[],"particles":[],"background":{"effect":"pulse","palette":["#202030","#303045","#252535"],"speed":0.5,"intensity":0}}},
147]
148hooks/script.ts 1292 lines1// Scenes the buddy scripts: actors with frames of ASCII art, moved by small
2// math expressions of time, with speech, particle swarms and a faint backdrop.
3// The expressions run in an interpreter of their own that knows arithmetic and
4// a handful of functions, so a script can move things and nothing else.
5
6import {
7 CLEAR,
8 cleanBackground,
9 cleanText,
10 clamp,
11 dim,
12 encode,
13 hash,
14 mix,
15 paintBackground,
16 parseHex,
17 type Background,
18 type Rgb,
19} from './effects'
20import { ARMS, CLAWD_H, CLAWD_ORANGE, CLAWD_W, LOOK, clawdAnchors, clawdMesh, clawdPixels, clawdPoints, type Anchors, type Arm, type ClawdLook, type Eyes, type Pose } from './clawd'
21import { ScriptError, obj, parseProgram, type Program } from './lang'
22import {
23 DEFAULT_CAMERA,
24 DEFAULT_FOG,
25 DEFAULT_LIGHT,
26 MOST_TRIANGLES,
27 SX,
28 SY,
29 box,
30 claimPixel,
31 composite,
32 createFrame,
33 cylinder,
34 drawLine,
35 drawMesh,
36 drawPoint,
37 plane,
38 project,
39 resolve,
40 sphere,
41 type Camera,
42 type Fog,
43 type Frame,
44 type Light,
45 type Mesh,
46 type Transform,
47 type View,
48} from './render3d'
49
50type Env = { t: number; w: number; h: number; k: number; n: number }
51type Expr = (env: Env) => number
52
53const fract = (v: number) => v - Math.floor(v)
54const mod = (a: number, b: number) => (b === 0 ? 0 : ((a % b) + b) % b)
55
56const FUNCTIONS: Record<string, (...args: number[]) => number> = {
57 sin: Math.sin,
58 cos: Math.cos,
59 tan: Math.tan,
60 abs: Math.abs,
61 min: (...v) => Math.min(...v),
62 max: (...v) => Math.max(...v),
63 floor: Math.floor,
64 ceil: Math.ceil,
65 round: Math.round,
66 sqrt: v => Math.sqrt(Math.max(0, v)),
67 pow: Math.pow,
68 exp: Math.exp,
69 sign: Math.sign,
70 mod,
71 fract,
72 clamp: (v, lo = 0, hi = 1) => Math.min(hi, Math.max(lo, v)),
73 lerp: (a, b, k) => a + (b - a) * k,
74 // 0 up to 1 and back down over each unit of x.
75 tri: v => 1 - Math.abs(2 * fract(v) - 1),
76 // A steady pseudo-random number in [0, 1) for each x.
77 rand: v => hash(v, 17),
78 step: (edge, v) => (v >= edge ? 1 : 0),
79 // 1 while lo <= v < hi.
80 between: (v, lo, hi) => (v >= lo && v < hi ? 1 : 0),
81 // 0 below a, 1 above b, eased in between.
82 smoothstep: (a, b, v) => {
83 const k = Math.min(1, Math.max(0, b === a ? (v >= b ? 1 : 0) : (v - a) / (b - a)))
84
85 return k * k * (3 - 2 * k)
86 },
87 atan2: Math.atan2,
88 hypot: (...v) => Math.hypot(...v),
89 // Smooth wandering noise in [0, 1) along x.
90 noise: v => {
91 const i = Math.floor(v)
92 const k = fract(v)
93
94 return hash(i, 17) + (hash(i + 1, 17) - hash(i, 17)) * k * k * (3 - 2 * k)
95 },
96}
97
98const VARIABLES = new Set(['t', 'w', 'h', 'k', 'n'])
99
100// Compiles an expression to a function of the scene's variables; anything it
101// cannot read compiles to a constant 0 rather than failing the scene.
102export function compile(source: string): Expr {
103 const tokens = source.slice(0, 240).match(/\d+\.?\d*|\.\d+|[a-z_]+|[-+*/%^(),]/gi) ?? []
104 let at = 0
105 const peek = () => tokens[at]
106 const take = () => tokens[at++]
107 type Node = Expr
108
109 function primary(): Node {
110 const token = take()
111 if (token === undefined) throw new Error('end')
112 if (token === '(') {
113 const inner = sum()
114 if (take() !== ')') throw new Error(')')
115
116 return inner
117 }
118 if (token === '-') {
119 const inner = power()
120
121 return env => -inner(env)
122 }
123 if (token === '+') return power()
124 if (/^[\d.]/.test(token)) {
125 const value = Number(token)
126
127 return () => value
128 }
129 const name = token.toLowerCase()
130 if (name === 'pi') return () => Math.PI
131 if (VARIABLES.has(name)) return env => env[name as keyof Env]
132 const fn = FUNCTIONS[name]
133 if (!fn || take() !== '(') throw new Error(name)
134 const args: Node[] = []
135 if (peek() !== ')') {
136 args.push(sum())
137 while (peek() === ',') {
138 take()
139 args.push(sum())
140 }
141 }
142 if (take() !== ')') throw new Error(')')
143
144 return env => fn(...args.map(arg => arg(env)))
145 }
146 function power(): Node {
147 const base = primary()
148 if (peek() !== '^') return base
149 take()
150 const exponent = power()
151
152 return env => Math.pow(base(env), exponent(env))
153 }
154 function product(): Node {
155 let left = power()
156 while (peek() === '*' || peek() === '/' || peek() === '%') {
157 const op = take()
158 const right = power()
159 const l = left
160 left = op === '*' ? env => l(env) * right(env) : op === '/' ? env => l(env) / right(env) : env => mod(l(env), right(env))
161 }
162
163 return left
164 }
165 function sum(): Node {
166 let left = product()
167 while (peek() === '+' || peek() === '-') {
168 const op = take()
169 const right = product()
170 const l = left
171 left = op === '+' ? env => l(env) + right(env) : env => l(env) - right(env)
172 }
173
174 return left
175 }
176 try {
177 const expr = sum()
178 if (at !== tokens.length) return () => 0
179
180 return env => {
181 const v = expr(env)
182
183 return Number.isFinite(v) ? v : 0
184 }
185 } catch {
186 return () => 0
187 }
188}
189
190export type Actor = {
191 // A sprite of the script's own frames, or Clawd, whom the stage draws.
192 kind: 'sprite' | 'clawd'
193 frames: string[][]
194 fps: number
195 x: Expr
196 y: Expr
197 color: string
198 say: string
199 // When the speech shows: seconds into the scene.
200 sayAt: number
201 // Drawn only while this is above 0; always when absent.
202 show?: Expr
203 // Which frame shows (wrapped to the frame count); by fps when absent.
204 frame?: Expr
205}
206
207export type Swarm = { glyphs: string[]; count: number; x: Expr; y: Expr; color: string }
208
209// A scene's own code: a program with a frame(t, dt) function that draws on
210// the strip every frame, its state kept between frames.
211export type Code = {
212 program?: Program
213 isStarted: boolean
214 lastT: number
215 // Why the code stopped (or never parsed), for the narrator to hear.
216 error?: string
217 // Whether the narrator has heard about the error.
218 isReported?: boolean
219 // When each line the code has Clawd say first showed, for the typing.
220 said: Map<string, number>
221 // Where each line's bubble sat last, so it does not hop about.
222 bubblesAt: Map<string, { x: number; y: number }>
223 // The 3D settings, kept between frames once the code sets them.
224 cam?: Camera
225 light?: Light
226 fog?: Fog
227}
228
229export type Script = {
230 background: Background
231 actors: Actor[]
232 swarms: Swarm[]
233 code?: Code
234}
235
236// The fuel the code's setup and each frame may burn: statements, loop turns
237// and calls. A frame over budget stops the code, not the strip.
238const SETUP_FUEL = 1_000_000
239const FRAME_FUEL = 150_000
240// Fuel bounds the code's own steps; this bounds the drawing it asks for.
241const FRAME_MS = 80
242// The setup may build a world; it gets longer, once.
243const SETUP_MS = 500
244
245function cleanCode(raw: unknown): Code | undefined {
246 if (typeof raw !== 'string' || !raw.trim()) return undefined
247 const code: Code = { isStarted: false, lastT: 0, said: new Map(), bubblesAt: new Map() }
248 try {
249 code.program = parseProgram(raw.slice(0, 20_000))
250 } catch (e) {
251 code.error = e instanceof Error ? e.message : String(e)
252 }
253
254 return code
255}
256
257// For the quarter blocks U+2596 to U+259F, which halves (1 top, 2 bottom)
258// hold ink: ▖▗▘▙▚▛▜▝▞▟.
259const QUARTER_HALVES = [2, 2, 1, 3, 3, 3, 3, 1, 3, 3]
260
261// Characters a sprite may hold: what one terminal cell draws at one width.
262const DRAWABLE = /[\x21-\x7e¡-ÿ←-⇿─-◿⠀-⣿★☆♥♦♣♠♪♫☺☻✓✗✦✧]/
263
264const glyphsOf = (line: string) => [...line].map(c => (c === ' ' || DRAWABLE.test(c) ? c : ' '))
265
266const isColor = (c: unknown): c is string => typeof c === 'string' && /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.test(c.trim())
267
268export function cleanScript(raw: unknown): Script | undefined {
269 if (typeof raw !== 'object' || raw === null) return undefined
270 const s = raw as Record<string, unknown>
271 const background = cleanBackground(s.background)
272 if (!background) return undefined
273 const actors: Actor[] = (Array.isArray(s.actors) ? s.actors : []).slice(0, 20).flatMap(a => {
274 if (typeof a !== 'object' || a === null) return []
275 const o = a as Record<string, unknown>
276 const kind = o.kind === 'clawd' ? 'clawd' : 'sprite'
277 const frames = (Array.isArray(o.frames) ? o.frames : [])
278 .filter((f): f is string => typeof f === 'string')
279 .slice(0, 12)
280 .map(f => f.split('\n').slice(0, 9).map(line => glyphsOf(line.slice(0, 80)).join('')))
281 if (kind === 'sprite' && frames.length === 0) return []
282
283 return [
284 {
285 kind,
286 frames: kind === 'clawd' ? [CLAWD_BOX] : frames,
287 fps: clamp(typeof o.fps === 'number' ? o.fps : 4, 0, 20),
288 x: compile(typeof o.x === 'string' ? o.x : '0'),
289 y: compile(typeof o.y === 'string' ? o.y : '0'),
290 color: isColor(o.color) ? o.color : '#d97757',
291 say: cleanText(typeof o.say === 'string' ? o.say : '', 70),
292 sayAt: clamp(typeof o.sayAt === 'number' ? o.sayAt : 0.5, 0, 30),
293 show: typeof o.show === 'string' && o.show.trim() ? compile(o.show) : undefined,
294 frame: typeof o.frame === 'string' && o.frame.trim() ? compile(o.frame) : undefined,
295 },
296 ]
297 })
298 const swarms: Swarm[] = (Array.isArray(s.particles) ? s.particles : []).slice(0, 6).flatMap(p => {
299 if (typeof p !== 'object' || p === null) return []
300 const o = p as Record<string, unknown>
301 const glyphs = glyphsOf(typeof o.glyphs === 'string' ? o.glyphs : '*').filter(c => c !== ' ').slice(0, 16)
302
303 return [
304 {
305 glyphs: glyphs.length > 0 ? glyphs : ['*'],
306 count: Math.round(clamp(typeof o.count === 'number' ? o.count : 10, 0, 120)),
307 x: compile(typeof o.x === 'string' ? o.x : 'rand(k)*w'),
308 y: compile(typeof o.y === 'string' ? o.y : 'rand(k+7)*h'),
309 color: isColor(o.color) ? o.color : '#ffffff',
310 },
311 ]
312 })
313
314 return { background, actors, swarms, code: cleanCode(s.code) }
315}
316
317// For layout Clawd fills a 14 by 4 cell box (its 8 pixel rows).
318const CLAWD_BOX = Array.from({ length: CLAWD_H / 2 }, () => '█'.repeat(CLAWD_W))
319
320// What the scene's code draws through: set while its frame runs.
321type Canvas = {
322 cols: number
323 rows: number
324 code: Code
325 put: (col: number, row: number, char: string, color: Rgb, back?: Rgb) => void
326 clear: (col: number, row: number) => void
327 // Marks a fill as under way: its cells do not count as drawn on.
328 fill: (on: boolean) => void
329 // One pixel: a column and a pixel row (two to a cell).
330 pixel: (col: number, py: number, color: Rgb) => void
331 // Clawd with its top-left at a column and a pixel row; where things attach.
332 clawd: (x: number, py: number, look: ClawdLook) => Anchors
333 // One pixel in the 3D world: drawn where nothing nearer has been.
334 pixel3d: (col: number, py: number, color: Rgb, depth: number) => void
335 // The cells a Clawd drawn some other way covers, for its speech.
336 mark: (box: Box) => void
337 say: (text: string, x: number, y: number, color: Rgb) => void
338 // The frame's 3D world, made on first use; drawing in it is noted.
339 scene3d: (isDrawing?: boolean) => { frame: Frame; view: View; light: Light; fog: Fog }
340}
341let canvas: Canvas | undefined
342
343// A color from the code: 0xRRGGBB, "#rrggbb" or "#rgb". A string is parsed
344// once and remembered, since a fill names the same one for every cell.
345const COLORS = new Map<string, Rgb>()
346const colorOf = (c: unknown, fallback: Rgb = [255, 255, 255]): Rgb => {
347 if (typeof c === 'number' && Number.isFinite(c)) return [(c >> 16) & 255, (c >> 8) & 255, c & 255]
348 if (typeof c !== 'string') return fallback
349 let rgb = COLORS.get(c)
350 if (!rgb) {
351 if (!isColor(c)) return fallback
352 if (COLORS.size >= 512) COLORS.clear()
353 rgb = parseHex(c)
354 COLORS.set(c, rgb)
355 }
356
357 return [rgb[0], rgb[1], rgb[2]]
358}
359// Text from the code: a plain object (which has no toString in the sandbox)
360// or an array reads as it would in JavaScript rather than throwing.
361const strOf = (v: unknown): string =>
362 v === null || v === undefined ? '' : typeof v === 'object' ? (Array.isArray(v) ? v.slice(0, 400).map(strOf).join(',') : '[object Object]') : String(v)
363const packRgb = (r: number, g: number, b: number) =>
364 (clamp(Math.round(r), 0, 255) << 16) | (clamp(Math.round(g), 0, 255) << 8) | clamp(Math.round(b), 0, 255)
365const num = (v: unknown, fallback = 0) => (typeof v === 'number' && Number.isFinite(v) ? v : fallback)
366// One drawable character, or undefined.
367const charOf = (c: unknown) => {
368 const cp = strOf(c).codePointAt(0)
369 const first = cp === undefined ? undefined : String.fromCodePoint(cp)
370
371 return first === ' ' || (first && DRAWABLE.test(first)) ? first : undefined
372}
373
374// The functions the scene's code draws with.
375const API = {
376 put: (x: unknown, y: unknown, ch: unknown, color?: unknown, bg?: unknown) => {
377 const char = charOf(ch)
378 if (char) canvas?.put(Math.round(num(x)), Math.round(num(y)), char, colorOf(color), bg === undefined ? undefined : colorOf(bg))
379 },
380 text: (x: unknown, y: unknown, str: unknown, color?: unknown, bg?: unknown) => {
381 const col = Math.round(num(x))
382 const row = Math.round(num(y))
383 ;[...strOf(str)].slice(0, 400).forEach((c, i) => API.put(col + i, row, charOf(c) ?? ' ', color, bg))
384 },
385 // Multi-line art, solid inside its outline like a sprite actor.
386 sprite: (x: unknown, y: unknown, art: unknown, color?: unknown, bg?: unknown) => {
387 const col = Math.round(num(x))
388 const row = Math.round(num(y))
389 strOf(art).split('\n').slice(0, 40).forEach((line, dy) => {
390 const chars = [...line.slice(0, 300)]
391 const start = chars.findIndex(c => c !== ' ')
392 if (start < 0) return
393 const end = Math.min(chars.length, line.trimEnd().length)
394 for (let dx = start; dx < end; dx++) {
395 const c = charOf(chars[dx]) ?? ' '
396 if (c === ' ' && bg === undefined) canvas?.clear(col + dx, row + dy)
397 else API.put(col + dx, row + dy, c, color, bg)
398 }
399 })
400 },
401 fill: (x: unknown, y: unknown, w: unknown, h: unknown, ch?: unknown, color?: unknown, bg?: unknown) => {
402 const x0 = Math.round(num(x))
403 const y0 = Math.round(num(y))
404 canvas?.fill(true)
405 for (let row = y0; row < y0 + Math.min(60, Math.round(num(h))); row++) {
406 for (let col = x0; col < x0 + Math.min(400, Math.round(num(w))); col++) API.put(col, row, ch ?? ' ', color, bg)
407 }
408 canvas?.fill(false)
409 },
410 line: (x0: unknown, y0: unknown, x1: unknown, y1: unknown, ch?: unknown, color?: unknown) => {
411 const ax = num(x0)
412 const ay = num(y0)
413 const steps = Math.min(800, Math.ceil(Math.max(Math.abs(num(x1) - ax), Math.abs(num(y1) - ay))) || 0)
414 for (let i = 0; i <= steps; i++) {
415 const k = steps === 0 ? 0 : i / steps
416 API.put(ax + (num(x1) - ax) * k, ay + (num(y1) - ay) * k, ch ?? '*', color)
417 }
418 },
419 // A circle of radius r rows, twice as wide in columns so it looks round.
420 circle: (cx: unknown, cy: unknown, r: unknown, ch?: unknown, color?: unknown) => {
421 const radius = Math.min(100, Math.abs(num(r)))
422 const steps = Math.max(8, Math.ceil(radius * 12))
423 for (let i = 0; i < steps; i++) {
424 const a = (i / steps) * Math.PI * 2
425 API.put(num(cx) + Math.cos(a) * radius * 2, num(cy) + Math.sin(a) * radius, ch ?? 'o', color)
426 }
427 },
428 disc: (cx: unknown, cy: unknown, r: unknown, ch?: unknown, color?: unknown, bg?: unknown) => {
429 const radius = Math.min(60, Math.abs(num(r)))
430 for (let dy = -Math.ceil(radius); dy <= Math.ceil(radius); dy++) {
431 for (let dx = -Math.ceil(radius * 2); dx <= Math.ceil(radius * 2); dx++) {
432 if ((dx / 2) ** 2 + dy ** 2 <= radius ** 2) API.put(Math.round(num(cx)) + dx, Math.round(num(cy)) + dy, ch ?? '█', color, bg)
433 }
434 }
435 },
436 // Clawd at x, y (top-left; y in rows, halves allowed), with either the
437 // short form (facing, stride, blink, color) or an options object.
438 clawd: (x: unknown, y: unknown, facing?: unknown, stride?: unknown, blink?: unknown, color?: unknown) => {
439 const look = lookOf(facing, stride, blink, color)
440
441 return anchorsOut(canvas?.clawd(Math.round(num(x)), Math.round(num(y) * 2), look), Math.round(num(x)), Math.round(num(y) * 2))
442 },
443 pixel: (x: unknown, py: unknown, color?: unknown) => {
444 canvas?.pixel(Math.round(num(x)), Math.round(num(py)), colorOf(color))
445 },
446 // Pixel art: lines of characters, each mapped to a color by the palette
447 // ("." and " " are transparent; an unmapped character is white).
448 pixels: (x: unknown, py: unknown, art: unknown, palette?: unknown) => {
449 const col = Math.round(num(x))
450 const row = Math.round(num(py))
451 const map = typeof palette === 'object' && palette !== null ? (palette as Record<string, unknown>) : {}
452 strOf(art).split('\n').slice(0, 80).forEach((line, dy) => {
453 ;[...line.slice(0, 300)].forEach((c, dx) => {
454 if (c === ' ' || c === '.') return
455 canvas?.pixel(col + dx, row + dy, colorOf(map[c]))
456 })
457 })
458 },
459 // The 3D world: a camera (eye, target, the angle it sees across in degrees),
460 // one light (the direction toward it, and ambient 0 to 1), and fog that
461 // fades things out between two depths. Each keeps until set again.
462 camera: (ex: unknown, ey: unknown, ez: unknown, tx?: unknown, ty?: unknown, tz?: unknown, fov?: unknown) => {
463 if (canvas) canvas.code.cam = { eye: [num(ex), num(ey, 2.5), num(ez, 9)], target: [num(tx), num(ty, 1), num(tz)], fov: num(fov, 90) }
464 },
465 light: (dx: unknown, dy: unknown, dz: unknown, ambient?: unknown) => {
466 if (canvas) canvas.code.light = { dir: [num(dx, 0.4), num(dy, 1), num(dz, 0.6)], ambient: clamp(num(ambient, 0.35)) }
467 },
468 fog: (near: unknown, far: unknown, color?: unknown) => {
469 if (canvas) canvas.code.fog = { near: num(near, 8), far: Math.max(num(near, 8) + 0.01, num(far, 40)), color: colorOf(color, [0, 0, 0]) }
470 },
471 box: (w: unknown, h: unknown, d: unknown) => meshOut('box', [num(w, 1), num(h, 1), num(d, 1)], (a, b, c) => box(a, b, c)),
472 sphere: (r: unknown, segments?: unknown) => meshOut('sphere', [num(r, 1), num(segments, 8)], (a, b) => sphere(a, b)),
473 cylinder: (r: unknown, h: unknown, segments?: unknown) => meshOut('cylinder', [num(r, 1), num(h, 1), num(segments, 8)], (a, b, c) => cylinder(a, a, b, c)),
474 cone: (r: unknown, h: unknown, segments?: unknown) => meshOut('cone', [num(r, 1), num(h, 1), num(segments, 8)], (a, b, c) => cylinder(a, 0, b, c)),
475 plane: (w: unknown, d: unknown) => meshOut('plane', [num(w, 1), num(d, 1)], (a, b) => plane(a, b)),
476 // Draws a mesh placed by its options: x, y, z; rx, ry, rz (radians);
477 // scale (one number or [sx, sy, sz]); color; wire (edges only); unlit.
478 mesh3d: (mesh: unknown, options?: unknown) => {
479 if (!canvas) return
480 const m = meshIn(mesh)
481 if (!m) return
482 const o = (typeof options === 'object' && options !== null ? options : {}) as Record<string, unknown>
483 const sc = Array.isArray(o.scale) ? o.scale.map(v => num(v, 1)) : [num(o.scale, 1), num(o.scale, 1), num(o.scale, 1)]
484 const t: Transform = {
485 at: [num(o.x), num(o.y), num(o.z)],
486 rot: [num(o.rx), num(o.ry), num(o.rz)],
487 scale: [sc[0] ?? 1, sc[1] ?? 1, sc[2] ?? 1],
488 }
489 const { frame, view, light, fog } = canvas.scene3d()
490 drawMesh(frame, view, light, fog, m, t, { color: colorOf(o.color), wire: Boolean(o.wire), unlit: Boolean(o.unlit), ascii: Boolean(o.ascii) })
491 },
492 line3d: (x0: unknown, y0: unknown, z0: unknown, x1: unknown, y1: unknown, z1: unknown, color?: unknown) => {
493 if (!canvas) return
494 const { frame, view, fog } = canvas.scene3d()
495 drawLine(frame, view, fog, [num(x0), num(y0), num(z0)], [num(x1), num(y1), num(z1)], colorOf(color))
496 },
497 point3d: (x: unknown, y: unknown, z: unknown, color?: unknown) => {
498 if (!canvas) return
499 const { frame, view, fog } = canvas.scene3d()
500 drawPoint(frame, view, fog, [num(x), num(y), num(z)], colorOf(color))
501 },
502 // Where a world point lands: x and y in cells, px and py in pixels, its
503 // depth, and the pixels one world unit covers there; null behind the camera.
504 project: (x: unknown, y: unknown, z: unknown) => {
505 if (!canvas) return null
506 const { view } = canvas.scene3d(false)
507 const p = project(view, [num(x), num(y), num(z)])
508
509 return p
510 ? obj({ x: Math.round(p.x / SX), y: Math.round(p.y / SY), px: Math.round(p.x / SX), py: Math.round(p.y / (SY / 2)), depth: p.depth, scale: view.f / p.depth / SX })
511 : null
512 },
513 // Clawd standing at a world point (its feet there), facing the camera: a
514 // lit solid sized by distance (`size` is its height in world units, 0.9 by
515 // default), its eyes flat pixel art on its face. Takes the same options as
516 // clawd and returns the same anchors, or null when it is behind the camera.
517 clawd3d: (x: unknown, y: unknown, z: unknown, options?: unknown) => {
518 if (!canvas) return null
519 const { frame, view, light, fog } = canvas.scene3d()
520 const at: [number, number, number] = [num(x), num(y), num(z)]
521 const p = project(view, at)
522 if (!p) return null
523 const o = (typeof options === 'object' && options !== null ? options : {}) as Record<string, unknown>
524 const look = lookOf(o)
525 // Pixel rows per world unit at its depth; its height is capped so it is
526 // never taller than most of the strip: Clawd is small in a big world.
527 const perUnit = view.f / p.depth / (SY / 2)
528 const size = Math.min(num(o.size, 0.9), (canvas.rows * 2 * 0.7) / perUnit)
529 // World units per sprite pixel, and strip pixels per sprite pixel.
530 const u = size / CLAWD_H
531 const s = (size * perUnit) / CLAWD_H
532 // Its feet at the point, unless that puts them below the strip: a Clawd
533 // near the camera stands on the bottom edge rather than out of view.
534 const below = p.y / (SY / 2) - canvas.rows * 2
535 if (below > 0) at[1] += below / perUnit
536 // It faces the camera.
537 const ry = Math.atan2(view.eye[0] - at[0], view.eye[2] - at[2])
538 const sin = Math.sin(ry)
539 const cos = Math.cos(ry)
540 const place = (q: { x: number; y: number; z: number }) => {
541 const w = project(view, [at[0] + (q.x * cos + q.z * sin) * u, at[1] + q.y * u, at[2] + (q.z * cos - q.x * sin) * u])
542
543 return w ? { x: w.x / SX, py: w.y / (SY / 2), depth: w.depth } : undefined
544 }
545 drawMesh(frame, view, light, fog, clawdMesh(look), { at, rot: [0, ry, 0], scale: [u, u, u] }, { color: look.color, wire: false, unlit: false, ascii: false })
546 const points = clawdPoints(look)
547 const feet = place(points.feet)
548 if (!feet) return null
549 // The eyes: whole pixels at a size that steps with distance, so they
550 // never smear with perspective, and none on a Clawd too small for them.
551 // Both sit on one row, from the point between them: projected apart, a
552 // Clawd off the camera's axis under a high camera gets one eye lower
553 // than the other, which reads as lopsided rather than as a turned face.
554 const ew = Math.max(1, Math.round(s))
555 const [one, two] = points.eyes
556 const between = one && two ? place({ x: (one.x + two.x) / 2, y: (one.y + two.y) / 2, z: (one.z + two.z) / 2 }) : undefined
557 if (s * CLAWD_H >= 3 && between) {
558 for (const eye of points.eyes) {
559 const e = place(eye)
560 if (!e) continue
561 const wide = eye.kind === 'open' ? ew : 2 * ew
562 const col0 = Math.round(e.x - wide / 2)
563 const row0 = 2 * Math.round((between.py - ew) / 2)
564 const from = eye.kind === 'shut' ? row0 + ew : row0
565 for (let prow = from; prow < row0 + 2 * ew; prow++) for (let col = col0; col < col0 + wide; col++) canvas.pixel3d(col, prow, look.eyeColor, e.depth * 0.98)
566 }
567 }
568 const w = Math.round(CLAWD_W * s)
569 const h = Math.round(CLAWD_H * s)
570 const left = Math.round(feet.x - w / 2)
571 const top = Math.round(feet.py - h)
572 canvas.mark({ x0: left - 1, y0: Math.floor(top / 2), x1: left + w, y1: Math.floor((top + h - 1) / 2) })
573 const anchor = (q: { x: number; y: number; z: number }) => {
574 const e = place(q) ?? feet
575 const py = Math.round(e.py)
576
577 return obj({ x: Math.round(e.x), py, row: Math.floor(py / 2) })
578 }
579
580 return obj({
581 x: left,
582 py: top,
583 row: Math.floor(top / 2),
584 w,
585 h,
586 top: anchor(points.top),
587 left: anchor(points.left),
588 right: anchor(points.right),
589 feet: anchor(points.feet),
590 eyes: obj({ ...anchor(points.eyeSpan), w: Math.round(points.eyeSpan.w * s) }),
591 })
592 },
593 // A speech bubble pointing at x, y.
594 say: (text: unknown, x: unknown, y: unknown, color?: unknown) => {
595 const line = cleanText(strOf(text), 70)
596 if (line) canvas?.say(line, Math.round(num(x)), Math.round(num(y)), colorOf(color, [217, 119, 87]))
597 },
598 rgb: (r: unknown, g: unknown, b: unknown) => packRgb(num(r), num(g), num(b)),
599 // Hue in degrees, saturation and lightness 0 to 1.
600 hsl: (hue: unknown, sat: unknown, light: unknown) => {
601 const hh = (((num(hue) % 360) + 360) % 360) / 60
602 const ss = clamp(num(sat))
603 const ll = clamp(num(light))
604 const c = (1 - Math.abs(2 * ll - 1)) * ss
605 const x = c * (1 - Math.abs((hh % 2) - 1))
606 const [r, g, b] = hh < 1 ? [c, x, 0] : hh < 2 ? [x, c, 0] : hh < 3 ? [0, c, x] : hh < 4 ? [0, x, c] : hh < 5 ? [x, 0, c] : [c, 0, x]
607 const m = ll - c / 2
608
609 return packRgb((r + m) * 255, (g + m) * 255, (b + m) * 255)
610 },
611 mix: (a: unknown, b: unknown, k: unknown) => {
612 const [r, g, bb] = mix(colorOf(a), colorOf(b), clamp(num(k)))
613
614 return packRgb(r, g, bb)
615 },
616}
617
618// Clawd's look from a call's arguments: an options object, or the short
619// form's facing, stride, blink and color.
620function lookOf(facing: unknown, stride?: unknown, blink?: unknown, color?: unknown): ClawdLook {
621 if (typeof facing === 'object' && facing !== null) {
622 const o = facing as Record<string, unknown>
623 const poses: Pose[] = ['stand', 'walk', 'jump', 'sit']
624 const eyes: Eyes[] = ['open', 'closed', 'wide']
625 const strideIn = num(o.stride, -1)
626 const pose = poses.includes(o.pose as Pose) ? (o.pose as Pose) : strideIn >= 0 ? 'walk' : 'stand'
627 const lookIn = (typeof o.look === 'object' && o.look !== null ? o.look : {}) as Record<string, unknown>
628 // arms: one pose for both, or {left, right}.
629 const armOf = (v: unknown, fallback: Arm): Arm => (ARMS.includes(v as Arm) ? (v as Arm) : fallback)
630 const armsIn = o.arms
631 const arms =
632 typeof armsIn === 'object' && armsIn !== null
633 ? { left: armOf((armsIn as Record<string, unknown>).left, 'up'), right: armOf((armsIn as Record<string, unknown>).right, 'up') }
634 : { left: armOf(armsIn, 'up'), right: armOf(armsIn, 'up') }
635
636 return {
637 pose,
638 arms,
639 stride: Math.round(Math.max(0, strideIn)),
640 facing: Math.sign(num(o.facing)),
641 eyes: eyes.includes(o.eyes as Eyes) ? (o.eyes as Eyes) : 'open',
642 look: { x: num(lookIn.x, num(o.lookX)), y: num(lookIn.y, num(o.lookY)) },
643 isBlinking: Boolean(o.blink),
644 color: colorOf(o.color, CLAWD_ORANGE),
645 eyeColor: colorOf(o.eyeColor, LOOK.eyeColor),
646 scale: clamp(Math.round(num(o.scale, 1)), 1, 4),
647 }
648 }
649 const strideIn = Math.round(num(stride, -1))
650
651 return {
652 ...LOOK,
653 pose: strideIn >= 0 ? 'walk' : 'stand',
654 stride: Math.max(0, strideIn),
655 facing: Math.sign(num(facing)),
656 isBlinking: Boolean(blink),
657 color: colorOf(color, CLAWD_ORANGE),
658 }
659}
660
661// Anchors as the code reads them: absolute pixel positions.
662function anchorsOut(a: Anchors | undefined, left: number, top: number) {
663 if (!a) return null
664 const at = (p: { x: number; y: number }) => obj({ x: left + p.x, py: top + p.y, row: Math.floor((top + p.y) / 2) })
665
666 return obj({
667 x: left,
668 py: top,
669 row: Math.floor(top / 2),
670 w: a.w,
671 h: a.h,
672 top: at(a.top),
673 left: at(a.left),
674 right: at(a.right),
675 feet: at(a.feet),
676 eyes: obj({ ...at(a.eyes), w: a.eyes.w }),
677 })
678}
679
680// A primitive the code asked for, the same object for the same arguments:
681// code that builds sphere(1, 48) inside frame() (the natural way to write
682// it) would otherwise read and prepare a fresh mesh every frame. Its arrays
683// are frozen, so a change to one cannot leave its prepared normals stale.
684const PRIMITIVES = new Map<string, ReturnType<typeof obj>>()
685function meshOut(kind: string, args: number[], make: (...a: number[]) => Mesh) {
686 const key = `${kind}|${args.join(',')}`
687 let out = PRIMITIVES.get(key)
688 if (!out) {
689 if (PRIMITIVES.size >= 64) PRIMITIVES.clear()
690 const m = make(...args)
691 Object.freeze(m.verts)
692 Object.freeze(m.faces)
693 out = obj({ verts: m.verts, faces: m.faces })
694 meshesIn.set(out, { verts: m.verts, faces: m.faces, mesh: m })
695 PRIMITIVES.set(key, out)
696 }
697
698 return out
699}
700
701// A mesh as the code gives it: verts flat, faces flat triangles or lists of
702// indices (polygons, fanned into triangles). A mesh built once and drawn
703// every frame is read once, so its normals (kept per Mesh) are kept too.
704const meshesIn = new WeakMap<object, { verts: unknown; faces: unknown; mesh: Mesh }>()
705function meshIn(raw: unknown): Mesh | undefined {
706 if (typeof raw !== 'object' || raw === null) return undefined
707 const o = raw as Record<string, unknown>
708 if (!Array.isArray(o.verts) || !Array.isArray(o.faces)) return undefined
709 const known = meshesIn.get(raw)
710 if (known && known.verts === o.verts && known.faces === o.faces) return known.mesh
711 const mesh = readMesh(o)
712 meshesIn.set(raw, { verts: o.verts, faces: o.faces, mesh })
713
714 return mesh
715}
716
717function readMesh(o: Record<string, unknown>): Mesh {
718 if (!Array.isArray(o.verts) || !Array.isArray(o.faces)) return { verts: [], faces: [] }
719 const verts = o.verts.slice(0, 60_000).map(v => num(v))
720 const faces: number[] = []
721 const count = Math.floor(verts.length / 3)
722 const index = (v: unknown) => {
723 const i = Math.round(num(v, -1))
724
725 return i >= 0 && i < count ? i : -1
726 }
727 if (o.faces.every(f => Array.isArray(f))) {
728 // Polygons fan into triangles, up to the most one frame draws anyway.
729 for (const poly of o.faces.slice(0, 20_000) as unknown[][]) {
730 if (faces.length >= 3 * MOST_TRIANGLES) break
731 const ids = poly.slice(0, 3 * MOST_TRIANGLES).map(index)
732 if (ids.some(i => i < 0)) continue
733 for (let i = 1; i + 1 < ids.length && faces.length < 3 * MOST_TRIANGLES; i++) faces.push(ids[0]!, ids[i]!, ids[i + 1]!)
734 }
735 } else {
736 const ids = o.faces.slice(0, 60_000).map(index)
737 for (let i = 0; i + 2 < ids.length; i += 3) if (ids[i]! >= 0 && ids[i + 1]! >= 0 && ids[i + 2]! >= 0) faces.push(ids[i]!, ids[i + 1]!, ids[i + 2]!)
738 }
739
740 return { verts, faces }
741}
742
743type Bubble = { lines: string[]; x: number; y: number; wide: number; tall: number; base: Rgb; tail?: { col: number; row: number; char: string } }
744
745// A bubble's text as typed out so far, and its size.
746function typedBubble(text: string, age: number, cols: number) {
747 const all = wrap(text, Math.max(10, Math.min(30, cols - 6)))
748 let left = Math.floor((age * 1000) / BUBBLE_MS)
749 const lines = all.map(line => {
750 const part = line.slice(0, Math.max(0, left))
751 left -= line.length + 1
752
753 return part
754 })
755
756 return { lines, wide: Math.max(...all.map(line => line.length)) + 4, tall: all.length + 2 }
757}
758
759// Where a bubble goes: the clearest spot near its speaker, scored by how
760// many drawn cells it would cover and how far it strays from the speaker's
761// box; the last spot keeps its place unless a clearly better one opens, so
762// the bubble does not hop about. The tail notches the border on the side
763// that faces the speaker.
764function placeBubble(args: { speaker: Box; wide: number; tall: number; cols: number; rows: number; covered: Uint8Array; last?: { x: number; y: number } }) {
765 const { speaker, wide, tall, cols, rows, covered, last } = args
766 const bottom = rows - 1
767 const cx = (speaker.x0 + speaker.x1 + 1) / 2
768 const cost = (x: number, y: number) => {
769 let blocked = 0
770 for (let row = y; row < y + tall; row++) {
771 for (let col = x; col < x + wide; col++) if (covered[row * cols + col]) blocked += 1
772 }
773 const gap = y + tall <= speaker.y0 ? speaker.y0 - (y + tall) : y > speaker.y1 ? y - speaker.y1 - 1 : 0
774 const side = x + wide <= speaker.x0 ? speaker.x0 - (x + wide) : x > speaker.x1 ? x - speaker.x1 - 1 : 0
775
776 return blocked * 10 + Math.abs(x + wide / 2 - cx) * 0.08 + gap * 0.6 + side * 0.15
777 }
778 let best = { x: Math.max(0, Math.min(cols - wide, Math.round(cx - wide / 2))), y: Math.max(0, speaker.y0 - tall) }
779 let bestCost = Infinity
780 const lo = Math.max(0, Math.round(cx) - 50)
781 const hi = Math.min(cols - wide, Math.round(cx) + 50)
782 for (let y = 0; y + tall - 1 <= bottom; y++) {
783 // Every other column, and both bounds.
784 for (let x = lo; x <= hi + 1; x += 2) {
785 const xx = Math.min(x, hi)
786 const c = cost(xx, y)
787 if (c < bestCost) {
788 bestCost = c
789 best = { x: xx, y }
790 }
791 }
792 }
793 if (last && last.x >= 0 && last.x <= cols - wide && last.y >= 0 && last.y + tall - 1 <= bottom && cost(last.x, last.y) <= bestCost + 3) best = last
794 const { x, y } = best
795 const tailCol = Math.max(x + 2, Math.min(x + wide - 3, Math.round(cx)))
796 const tailRow = Math.max(y + 1, Math.min(y + tall - 2, Math.round((speaker.y0 + speaker.y1) / 2)))
797 const tail =
798 y + tall - 1 < speaker.y0 ? { col: tailCol, row: y + tall - 1, char: '┬' }
799 : y > speaker.y1 ? { col: tailCol, row: y, char: '┴' }
800 : x + wide - 1 < speaker.x0 ? { col: x + wide - 1, row: tailRow, char: '├' }
801 : x > speaker.x1 ? { col: x, row: tailRow, char: '┤' }
802 : undefined
803 // The bubble's cells count as drawn for the next bubble.
804 for (let row = y; row < y + tall; row++) for (let col = x; col < x + wide; col++) covered[row * cols + col] = 1
805
806 return { x, y, tail }
807}
808
809export type Stage = {
810 cols: number
811 rows: number
812 // The buddy's clock, in seconds: the backdrop's.
813 t: number
814 script: Script
815 previous?: Script
816 // Milliseconds since this script arrived (its actors' clock), and since
817 // the previous one did, so its code keeps its own time while it dissolves.
818 since: number
819 previousSince?: number
820 reveal: number
821}
822
823const BUBBLE_MS = 32
824// Where each speaker's bubble sat last frame.
825const lastBubble = new WeakMap<Actor, { x: number; y: number }>()
826const OLD_MS = 500
827
828// Words wrapped to lines no wider than `width`.
829function wrap(text: string, width: number) {
830 const lines: string[] = []
831 let line = ''
832 for (const word of text.split(' ')) {
833 const next = line ? `${line} ${word}` : word
834 if (next.length <= width) {
835 line = next
836 } else {
837 if (line) lines.push(line)
838 // A word too long for a line is broken across lines.
839 let rest = word
840 while (rest.length > width) {
841 lines.push(rest.slice(0, width))
842 rest = rest.slice(width)
843 }
844 line = rest
845 }
846 }
847 if (line) lines.push(line)
848
849 return lines
850}
851
852type Box = { x0: number; y0: number; x1: number; y1: number }
853type Placed = { actor: Actor; lines: string[]; x: number; y: number; width: number; alpha: number; look?: ClawdLook }
854
855// Draws one frame of a script: backdrop, swarms, actors, then speech on top,
856// with the backdrop cleared around everything that reads.
857export function stage(frame: Stage): string {
858 const { cols, rows, t, since, reveal } = frame
859 // A scene whose code broke with nothing else to show keeps the last scene
860 // on until the narrator's fixed one arrives.
861 const isBlank = (s: Script) => Boolean(s.code?.error) && s.actors.length === 0 && s.swarms.length === 0
862 const script = isBlank(frame.script) && frame.previous && !isBlank(frame.previous) ? frame.previous : frame.script
863 const previous = script === frame.script ? frame.previous : undefined
864 const words = new Uint32Array(cols * rows * 3)
865 paintBackground(words, {
866 cols,
867 rows,
868 t,
869 background: script.background,
870 previous: previous?.background,
871 fade: clamp(since / 1500),
872 reveal,
873 strength: 0.4,
874 })
875 // Rows above the band's risen front stay empty while it grows in.
876 const shown = (row: number) => row >= Math.floor(rows * (1 - clamp(reveal * 1.3)))
877 const clear = (box: Box) => {
878 for (let row = Math.max(0, box.y0); row <= Math.min(rows - 1, box.y1); row++) {
879 for (let col = Math.max(0, box.x0); col <= Math.min(cols - 1, box.x1); col++) {
880 if (gate && !gate(col, row)) continue
881 const at = (row * cols + col) * 3
882 words[at] = 0x20
883 words[at + 1] = CLEAR
884 words[at + 2] = CLEAR
885 }
886 }
887 }
888 const pack = (c: Rgb) => (Math.round(c[0]) << 16) | (Math.round(c[1]) << 8) | Math.round(c[2])
889 // While scenes dissolve, which cells this pass may touch.
890 let gate: ((col: number, row: number) => boolean) | undefined
891 const put = (col: number, row: number, char: string, color: Rgb, back?: Rgb) => {
892 if (col < 0 || col >= cols || row < 0 || row >= rows || !shown(row) || (gate && !gate(col, row))) return
893 const at = (row * cols + col) * 3
894 words[at] = char.codePointAt(0) ?? 0x20
895 words[at + 1] = pack(color)
896 words[at + 2] = back ? pack(back) : CLEAR
897 }
898
899 // Where every actor stands this frame, the outgoing scene's flickering out.
900 const placed: Placed[] = []
901 const scenes: [Script, number, number][] =
902 previous && since < OLD_MS ? [[previous, since / 1000 + 30, 1 - since / OLD_MS], [script, since / 1000, 1]] : [[script, since / 1000, 1]]
903 for (const [play, sceneT, alpha] of scenes) {
904 play.actors.forEach((actor, i) => {
905 const env = { t: sceneT, w: cols, h: rows, k: i, n: play.actors.length }
906 if (actor.show && actor.show(env) <= 0) return
907 const index = actor.frame ? Math.floor(actor.frame(env)) : Math.floor(sceneT * actor.fps)
908 const lines = actor.frames[mod(index, actor.frames.length)] ?? []
909 const width = Math.max(1, ...lines.map(line => line.trimEnd().length))
910 const x = actor.x(env)
911 let look: ClawdLook | undefined
912 if (actor.kind === 'clawd') {
913 // Clawd steps as far as it has walked, one frame each 1.5 columns, so
914 // its feet never slide; standing, it blinks now and then.
915 const dx = x - actor.x({ ...env, t: sceneT - 0.12 })
916 const isWalking = Math.abs(dx) >= 0.2
917 look = {
918 ...LOOK,
919 pose: isWalking ? 'walk' : 'stand',
920 stride: isWalking ? mod(Math.floor(x / 1.5), 4) : 0,
921 facing: isWalking ? Math.sign(dx) : 0,
922 isBlinking: !isWalking && hash(Math.floor(sceneT * 0.5), i) > 0.6 && fract(sceneT * 0.5) < 0.06,
923 color: parseHex(actor.color),
924 }
925 }
926 placed.push({ actor, lines, x: Math.round(x), y: Math.round(actor.y(env)), width, alpha, look })
927 })
928 }
929
930 // Each actor is solid inside its outline: on every line, the cells from its
931 // first drawn character to its last hide whatever lies behind them.
932 const spans = (one: Placed) =>
933 one.lines.map(line => {
934 const start = line.search(/\S/)
935
936 return start < 0 ? undefined : { x0: one.x + start, x1: one.x + line.trimEnd().length - 1 }
937 })
938 const covered = new Uint8Array(cols * rows)
939 for (const one of placed) {
940 spans(one).forEach((span, dy) => {
941 const row = one.y + dy
942 if (!span || row < 0 || row >= rows) return
943 for (let col = Math.max(0, span.x0); col <= Math.min(cols - 1, span.x1); col++) covered[row * cols + col] = 1
944 })
945 }
946
947 // Speech: a bubble in the clearest spot near its speaker.
948 const sceneT = since / 1000
949 const bubbles: Bubble[] = []
950 const saidNow = new Set<string>()
951 for (const one of placed) {
952 const { actor } = one
953 if (one.alpha < 1 || !actor.say || sceneT < actor.sayAt) continue
954 const typed = Math.floor(((sceneT - actor.sayAt) * 1000) / BUBBLE_MS)
955 const all = wrap(actor.say, Math.max(10, Math.min(30, cols - 6)))
956 let left = typed
957 const lines = all.map(line => {
958 const part = line.slice(0, Math.max(0, left))
959 left -= line.length + 1
960
961 return part
962 })
963 // The text plus a border and a space of padding either side. A bubble
964 // the strip cannot hold whole (while the band grows in) waits.
965 const wide = Math.max(...all.map(line => line.length)) + 4
966 const tall = all.length + 2
967 if (tall > rows || wide > cols) continue
968 const bottom = rows - 1
969 const outline = spans(one)
970 const drawn = outline.flatMap((span, dy) => (span ? [{ ...span, row: one.y + dy }] : []))
971 // An actor with nothing drawn (a blank narrator) speaks from its box.
972 const speaker: Box =
973 drawn.length > 0
974 ? {
975 x0: Math.min(...drawn.map(d => d.x0)),
976 x1: Math.max(...drawn.map(d => d.x1)),
977 y0: Math.min(...drawn.map(d => d.row)),
978 y1: Math.max(...drawn.map(d => d.row)),
979 }
980 : { x0: one.x, x1: one.x + Math.max(0, ...one.lines.map(l => l.length - 1)), y0: one.y, y1: one.y + one.lines.length - 1 }
981 const { x, y, tail } = placeBubble({ speaker, wide, tall, cols, rows, covered, last: lastBubble.get(actor) })
982 lastBubble.set(actor, { x, y })
983 bubbles.push({ lines, x, y, wide, tall, base: parseHex(actor.color), tail })
984 }
985
986 // Room to read: the backdrop gives way around the actors.
987 for (const one of placed) {
988 spans(one).forEach((span, dy) => span && clear({ x0: span.x0 - 1, y0: one.y + dy, x1: span.x1 + 1, y1: one.y + dy }))
989 }
990
991 // Swarms first, behind everything.
992 for (const [play, playT, alpha] of scenes) {
993 const env: Env = { t: playT, w: cols, h: rows, k: 0, n: 0 }
994 for (const swarm of play.swarms) {
995 const color = parseHex(swarm.color)
996 for (let k = 0; k < swarm.count; k++) {
997 if (alpha < 1 && hash(k, Math.floor(since / 60)) > alpha) continue
998 const at = { ...env, k, n: swarm.count }
999 const col = Math.round(swarm.x(at))
1000 const row = Math.round(swarm.y(at))
1001 put(col, row, swarm.glyphs[k % swarm.glyphs.length] ?? '*', color)
1002 }
1003 }
1004 }
1005 // One pixel, two to a cell: the cell keeps whatever its other half holds
1006 // (a backdrop pixel, an earlier pixel) and shows the new one in its half.
1007 const plotPixel = (col: number, py: number, color: Rgb) => {
1008 const row = Math.floor(py / 2)
1009 if (col < 0 || col >= cols || row < 0 || row >= rows || !shown(row) || (gate && !gate(col, row))) return
1010 const at = (row * cols + col) * 3
1011 const ch = words[at]
1012 const fg = words[at + 1]!
1013 const bg = words[at + 2]!
1014 let top: number | undefined
1015 let bottom: number | undefined
1016 if (ch === 0x2580) {
1017 top = fg
1018 bottom = bg === CLEAR ? undefined : bg
1019 } else if (ch === 0x2584) {
1020 bottom = fg
1021 top = bg === CLEAR ? undefined : bg
1022 } else if (ch === 0x2588) {
1023 top = bottom = fg
1024 } else if (ch === 0x20 && bg !== CLEAR) {
1025 top = bottom = bg
1026 } else if (ch !== undefined && ch >= 0x2596 && ch <= 0x259f) {
1027 // A quarter-block edge from the 3D world: each half counts as the
1028 // glyph's color where the glyph has anything in it.
1029 const q = QUARTER_HALVES[ch - 0x2596] ?? 0
1030 if (q & 1) top = fg
1031 if (q & 2) bottom = fg
1032 }
1033 if (py % 2 === 0) top = pack(color)
1034 else bottom = pack(color)
1035 if (top !== undefined) {
1036 words[at] = 0x2580
1037 words[at + 1] = top
1038 words[at + 2] = bottom ?? CLEAR
1039 } else {
1040 words[at] = 0x2584
1041 words[at + 1] = bottom ?? CLEAR
1042 words[at + 2] = CLEAR
1043 }
1044 }
1045
1046 // The cells the code has drawn characters and sprites on this frame (not
1047 // fills), and the last Clawd it drew: what its speech bubbles stay off.
1048 const inked = new Uint8Array(cols * rows)
1049 let isFilling = false
1050 const ink = (col: number, row: number) => {
1051 if (!isFilling && col >= 0 && col < cols && row >= 0 && row < rows) inked[row * cols + col] = 1
1052 }
1053 let lastClawd: Box | undefined
1054 const mark = (box: Box) => {
1055 lastClawd = box
1056 for (let row = Math.max(0, box.y0); row <= Math.min(rows - 1, box.y1); row++) for (let col = Math.max(0, box.x0); col <= Math.min(cols - 1, box.x1); col++) ink(col, row)
1057 }
1058 // The cells speech bubbles have taken this frame.
1059 const bubbled = new Uint8Array(cols * rows)
1060
1061 // The frame's 3D samples, made when the code first draws in 3D, and drawn
1062 // as glyphs before any 2D drawing that follows, so that sits on top.
1063 let frame3d: Frame | undefined
1064 let isDirty3d = false
1065 const frameOf = () => (frame3d ??= Object.assign(createFrame(cols, rows), { tick: () => spend() }))
1066 const flush3d = () => {
1067 if (frame3d && isDirty3d) composite(frame3d, put)
1068 isDirty3d = false
1069 }
1070 // A frame that draws for too long (a fill the size of the world, thousands
1071 // of meshes) stops the scene the way a runaway loop does.
1072 const deadline = performance.now() + FRAME_MS
1073 let calls = 0
1074 const spend = () => {
1075 if ((calls += 1) % 256 === 0 && performance.now() > deadline) throw new ScriptError(`a frame took more than ${FRAME_MS}ms to draw`)
1076 }
1077
1078 // Clawd at a column and a pixel row, some of its pixels held back while it
1079 // fades in.
1080 const paintClawd = (x: number, py: number, look: ClawdLook, isHidden?: (px: { x: number; y: number }) => boolean) => {
1081 for (const px of clawdPixels(look)) {
1082 if (isHidden?.(px)) continue
1083 plotPixel(x + px.x, py + px.y, px.c)
1084 }
1085
1086 return clawdAnchors(look)
1087 }
1088
1089 // A scene's code draws over the backdrop and swarms, under the actors:
1090 // the outgoing scene's for a moment, dissolving cell by cell into the new
1091 // one's, then the new one's. Each cell flips once, at its own moment.
1092 const runCode = (play: Script, playT: number, isOutgoing: boolean) => {
1093 const code = play.code
1094 if (!code?.program || code.error) return
1095 frame3d = undefined
1096 isDirty3d = false
1097 const dt = code.isStarted ? clamp(playT - code.lastT, 0, 0.25) : 0
1098 code.lastT = playT
1099 const globals = { t: playT, dt, w: cols, h: rows }
1100 let scene3d: { frame: Frame; view: View; light: Light; fog: Fog } | undefined
1101 canvas = {
1102 cols,
1103 rows,
1104 code,
1105 put: (col, row, char, color, back) => {
1106 spend()
1107 flush3d()
1108 put(col, row, char, color, back)
1109 ink(col, row)
1110 },
1111 fill: (on: boolean) => {
1112 isFilling = on
1113 },
1114 clear: (col, row) => {
1115 spend()
1116 flush3d()
1117 clear({ x0: col, y0: row, x1: col, y1: row })
1118 },
1119 pixel: (col, py, color) => {
1120 spend()
1121 flush3d()
1122 plotPixel(col, py, color)
1123 ink(col, Math.floor(py / 2))
1124 },
1125 clawd: (x, py, look) => {
1126 spend()
1127 flush3d()
1128 const a = paintClawd(x, py, look)
1129 mark({ x0: x - 1, y0: Math.floor(py / 2), x1: x + a.w, y1: Math.floor((py + a.h - 1) / 2) })
1130
1131 return a
1132 },
1133 pixel3d: (col, py, color, depth) => {
1134 spend()
1135 flush3d()
1136 if (!claimPixel(frameOf(), col, py, depth)) return
1137 plotPixel(col, py, color)
1138 ink(col, Math.floor(py / 2))
1139 },
1140 mark,
1141 scene3d: (isDrawing = true) => {
1142 if (isDrawing) {
1143 spend()
1144 isDirty3d = true
1145 }
1146 if (!scene3d) {
1147 scene3d = {
1148 frame: frameOf(),
1149 view: resolve(code.cam ?? DEFAULT_CAMERA, cols * SX, rows * SY),
1150 light: code.light ?? DEFAULT_LIGHT,
1151 fog: code.fog ?? DEFAULT_FOG,
1152 }
1153 }
1154
1155 return scene3d
1156 },
1157 say: (text, x, y, color) => {
1158 spend()
1159 if (isOutgoing) return
1160 // The same line twice in a frame is one bubble.
1161 if (saidNow.has(text)) return
1162 saidNow.add(text)
1163 flush3d()
1164 // A new line types out; one that only changed from a line already
1165 // typed (a live count, a timer) shows whole rather than restarting.
1166 let first = code.said.get(text)
1167 if (first === undefined) {
1168 const isTyped = (line: string, at: number) => (playT - at) * 1000 >= line.length * BUBBLE_MS
1169 first = [...code.said].some(([line, at]) => isTyped(line, at)) ? -Infinity : playT
1170 if (code.said.size >= 32) code.said.clear()
1171 code.said.set(text, first)
1172 }
1173 const { lines, wide, tall } = typedBubble(text, playT - first, cols)
1174 if (wide > cols || tall > rows) return
1175 // The speaker is the Clawd last drawn when the point is on or beside
1176 // it; otherwise the point itself.
1177 const near = lastClawd && x >= lastClawd.x0 - 2 && x <= lastClawd.x1 + 2 && y >= lastClawd.y0 - 1 && y <= lastClawd.y1 + 1
1178 const speaker: Box = near && lastClawd ? lastClawd : { x0: x, y0: y, x1: x, y1: y }
1179 // The bubble keeps clear of what is drawn in runs (Clawd, labels,
1180 // props) and of earlier bubbles, not of a scattering of rain, snow
1181 // or sparks: a drawn cell counts when the cells on both sides of it,
1182 // or above and below it, are drawn too.
1183 const covered = new Uint8Array(cols * rows)
1184 const drawn = (col: number, row: number) => col >= 0 && col < cols && row >= 0 && row < rows && inked[row * cols + col] === 1
1185 for (let row = 0; row < rows; row++) {
1186 for (let col = 0; col < cols; col++) {
1187 const i = row * cols + col
1188 if (bubbled[i] || (inked[i] && ((drawn(col - 1, row) && drawn(col + 1, row)) || (drawn(col, row - 1) && drawn(col, row + 1))))) covered[i] = 1
1189 }
1190 }
1191 const placed3 = placeBubble({ speaker, wide, tall, cols, rows, covered, last: code.bubblesAt.get(text) })
1192 bubbled.set(covered)
1193 if (code.bubblesAt.size >= 32) code.bubblesAt.clear()
1194 code.bubblesAt.set(text, { x: placed3.x, y: placed3.y })
1195 bubbles.push({ lines, x: placed3.x, y: placed3.y, wide, tall, base: color, tail: placed3.tail })
1196 },
1197 }
1198 try {
1199 if (!code.isStarted) {
1200 code.isStarted = truehooks/effects.ts 267 lines1// Procedural effects for the spinner: each one a field from a pixel to a
2// value in [0, 1], mapped through the scene's palette and packed into a
3// Raster's cells (upper half blocks, two pixels a row).
4
5export const EFFECTS = [
6 'plasma', 'fire', 'starfield', 'rain', 'tunnel', 'lava', 'glitch', 'aurora', 'waves', 'pulse', 'fireworks',
7] as const
8export type Effect = (typeof EFFECTS)[number]
9
10// The ambient layer behind a scene: one effect, faint and see-through.
11export type Background = {
12 effect: Effect
13 palette: string[]
14 speed: number
15 intensity: number
16}
17
18type Field = (x: number, y: number, t: number, w: number, h: number) => number
19
20const fract = (v: number) => v - Math.floor(v)
21const clamp = (v: number, lo = 0, hi = 1) => Math.min(hi, Math.max(lo, v))
22const hash = (x: number, y: number) => fract(Math.sin(x * 12.9898 + y * 78.233) * 43758.5453)
23
24function noise(x: number, y: number) {
25 const ix = Math.floor(x)
26 const iy = Math.floor(y)
27 const fx = x - ix
28 const fy = y - iy
29 const sx = fx * fx * (3 - 2 * fx)
30 const sy = fy * fy * (3 - 2 * fy)
31 const a = hash(ix, iy)
32 const b = hash(ix + 1, iy)
33 const c = hash(ix, iy + 1)
34 const d = hash(ix + 1, iy + 1)
35
36 return a + (b - a) * sx + (c - a) * sy + (a - b - c + d) * sx * sy
37}
38
39// Cells are about twice as tall as wide, so x distances count half.
40const polar = (x: number, y: number, w: number, h: number) => {
41 const dx = (x - w / 2) * 0.5
42 const dy = y - h / 2
43
44 return { r: Math.hypot(dx, dy), a: Math.atan2(dy, dx) }
45}
46
47const FIELDS: Record<Effect, Field> = {
48 plasma: (x, y, t) =>
49 (Math.sin(x * 0.08 + t) + Math.sin(y * 0.3 + t * 1.3) + Math.sin((x + y) * 0.05 + t * 0.7) +
50 Math.sin(Math.hypot(x * 0.5, y) * 0.2 - t)) / 8 + 0.5,
51 fire: (x, y, t, _w, h) => clamp(noise(x * 0.15, y * 0.45 + t * 3) * 1.5 * Math.pow(y / h, 1.3) + noise(x * 0.4, t * 5) * 0.15 * (y / h)),
52 starfield: (x, y, t, w, h) => {
53 const { r, a } = polar(x, y, w, h)
54 const ray = Math.floor((a + Math.PI) * 24)
55 const z = fract(hash(ray, 3) + t * 0.25)
56 const reach = Math.hypot(w * 0.25, h * 0.5)
57
58 return clamp(Math.exp(-Math.abs(r - z * reach) * 1.2) * z * 1.6)
59 },
60 rain: (x, y, t, _w, h) => {
61 const head = fract(hash(x, 2) + t * (0.3 + hash(x, 1) * 0.5)) * (h + 12)
62 const d = head - y
63
64 return d >= 0 && d < 10 ? 1 - d / 10 : hash(x, Math.floor(y + t * 4)) * 0.08
65 },
66 tunnel: (x, y, t, w, h) => {
67 const { r, a } = polar(x, y, w, h)
68
69 return 0.5 + 0.5 * Math.sin(30 / (r + 2) + t * 4) * Math.cos(a * 3 + t)
70 },
71 lava: (x, y, t, w, h) => {
72 let sum = 0
73 for (let i = 0; i < 6; i++) {
74 const bx = (0.5 + 0.45 * Math.sin(t * (0.3 + i * 0.11) + i * 2.1)) * w
75 const by = (0.5 + 0.45 * Math.cos(t * (0.4 + i * 0.07) + i * 1.3)) * h
76 sum += 6 / (Math.hypot((x - bx) * 0.5, y - by) + 0.5)
77 }
78
79 return clamp((sum - 2.2) / 2)
80 },
81 glitch: (x, y, t) => {
82 const frame = Math.floor(t * 9)
83 const shift = hash(Math.floor(y / 2), frame) > 0.75 ? Math.floor(hash(y, frame) * 24) : 0
84 const block = hash(Math.floor((x + shift) / 5), Math.floor(y / 3) + frame * 7)
85
86 return block > 0.6 ? block : 0.5 + 0.5 * Math.sin((x + shift) * 0.1 + t * 2) * 0.4
87 },
88 aurora: (x, y, t, _w, h) => {
89 const center = h * (0.45 + 0.25 * Math.sin(x * 0.04 + t * 0.6) + 0.12 * Math.sin(x * 0.11 - t * 1.4))
90 const band = (y - center) / (2.5 + 3 * noise(x * 0.05, t * 0.5))
91
92 return clamp(Math.exp(-band * band) * (0.6 + 0.4 * noise(x * 0.2, t * 2)))
93 },
94 waves: (x, y, t, _w, h) => {
95 const surface = h * (0.5 + 0.3 * Math.sin(x * 0.09 - t * 2) * Math.sin(x * 0.023 + t * 0.7))
96
97 return y > surface ? clamp(0.55 + 0.45 * Math.sin(x * 0.2 + y * 0.6 - t * 3)) : 0.1 * noise(x * 0.1, y * 0.3 + t)
98 },
99 pulse: (x, y, t, w, h) => {
100 const { r } = polar(x, y, w, h)
101 const ring = fract(t * 0.6) * Math.hypot(w * 0.25, h * 0.5)
102
103 return clamp(Math.exp(-Math.abs(r - ring) * 0.6) + 0.15 * Math.sin(r * 0.5 - t * 3))
104 },
105 fireworks: (x, y, t, w, h) => {
106 let v = 0
107 for (let k = 0; k < 4; k++) {
108 const cycle = Math.floor(t / 1.6 + k * 0.37)
109 const age = fract(t / 1.6 + k * 0.37)
110 const cx = hash(k, cycle) * w
111 const cy = (0.2 + hash(cycle, k) * 0.6) * h
112 const r = Math.hypot((x - cx) * 0.5, y - cy)
113 const sparkle = hash(Math.floor(Math.atan2(y - cy, x - cx) * 6), cycle) > 0.4 ? 1 : 0.3
114 v += Math.exp(-Math.abs(r - age * 14) * 1.3) * (1 - age) * sparkle
115 }
116
117 return clamp(v)
118 },
119}
120
121function parseHex(hex: string): [number, number, number] {
122 const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim())
123 if (!m || m[1] === undefined) return [128, 128, 128]
124 const s = m[1].length === 3 ? [...m[1]].map(c => c + c).join('') : m[1]
125
126 return [parseInt(s.slice(0, 2), 16), parseInt(s.slice(2, 4), 16), parseInt(s.slice(4, 6), 16)]
127}
128
129type Rgb = [number, number, number]
130
131function shade(stops: Rgb[], v: number): Rgb {
132 const p = clamp(v) * (stops.length - 1)
133 const i = Math.min(stops.length - 2, Math.floor(p))
134 const k = p - i
135 const a = stops[i] ?? [0, 0, 0]
136 const b = stops[i + 1] ?? a
137
138 return [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k, a[2] + (b[2] - a[2]) * k]
139}
140
141const pack = ([r, g, b]: Rgb) => (Math.round(r) << 16) | (Math.round(g) << 8) | Math.round(b)
142const mix = (a: Rgb, b: Rgb, k: number): Rgb => [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k, a[2] + (b[2] - a[2]) * k]
143const dim = (c: Rgb, k: number): Rgb => [c[0] * k, c[1] * k, c[2] * k]
144
145const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
146
147function base64(bytes: Uint8Array) {
148 let out = ''
149 let i = 0
150 for (; i + 2 < bytes.length; i += 3) {
151 const v = ((bytes[i] ?? 0) << 16) | ((bytes[i + 1] ?? 0) << 8) | (bytes[i + 2] ?? 0)
152 out += (B64[(v >> 18) & 63] ?? '') + (B64[(v >> 12) & 63] ?? '') + (B64[(v >> 6) & 63] ?? '') + (B64[v & 63] ?? '')
153 }
154 if (i < bytes.length) {
155 const v = ((bytes[i] ?? 0) << 16) | ((bytes[i + 1] ?? 0) << 8)
156 out += (B64[(v >> 18) & 63] ?? '') + (B64[(v >> 12) & 63] ?? '')
157 out += i + 1 < bytes.length ? (B64[(v >> 6) & 63] ?? '') + '=' : '=='
158 }
159
160 return out
161}
162
163// The terminal's own color: what shows through wherever nothing is drawn.
164export const CLEAR = 0x01000000
165
166// A 4x4 ordered dither: a pixel of opacity a shows where a beats its cell, so
167// a fade reads as a thinning pattern rather than a color against a background
168// the oracle cannot know.
169const BAYER = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5].map(v => (v + 0.5) / 16)
170const bayer = (x: number, y: number) => BAYER[(y & 3) * 4 + (x & 3)] ?? 0.5
171
172export const smooth = (lo: number, hi: number, v: number) => {
173 const k = clamp((v - lo) / (hi - lo))
174
175 return k * k * (3 - 2 * k)
176}
177
178export type Paint = {
179 cols: number
180 rows: number
181 t: number
182 background: Background
183 // The background being faded out, and how far the fade has gone (0 to 1).
184 previous?: Background
185 fade: number
186 // How far the band has risen into view, 0 to 1, from the bottom up.
187 reveal: number
188 // How strongly the backdrop shows, 0 to 1: kept low behind a scene.
189 strength: number
190}
191
192// Paints the backdrop into `words` (a Raster's cells): half blocks where the
193// effect is dense enough, the terminal's own color everywhere else, thinning
194// toward the band's edges and its rising front.
195export function paintBackground(words: Uint32Array, paint: Paint) {
196 const { cols, rows, t, background, previous, fade, reveal, strength } = paint
197 const w = cols
198 const h = rows * 2
199 const stops = background.palette.map(parseHex)
200 const oldStops = previous ? previous.palette.map(parseHex) : stops
201 const pixel = (x: number, y: number): { c: Rgb; a: number } => {
202 const v = FIELDS[background.effect](x, y, t * background.speed, w, h)
203 let c = shade(stops, 0.15 + 0.85 * v)
204 let a = smooth(0.2, 0.75, v) * (0.55 + 0.45 * background.intensity)
205 if (previous && fade < 1) {
206 const pv = FIELDS[previous.effect](x, y, t * previous.speed, w, h)
207 c = mix(shade(oldStops, 0.15 + 0.85 * pv), c, fade)
208 a = a * fade + smooth(0.2, 0.75, pv) * (0.55 + 0.45 * previous.intensity) * (1 - fade)
209 }
210 const edge = smooth(0, w * 0.14, Math.min(x, w - 1 - x)) * smooth(0, 2.5, Math.min(y + 0.5, h - 0.5 - y))
211 const rise = (h - 1 - y) / Math.max(1, h - 1)
212 const front = reveal * 1.35 - 0.3 * noise(x * 0.18, t * 1.5)
213 a *= strength * edge * smooth(0, 0.25, front - rise)
214
215 return { c, a }
216 }
217 for (let row = 0; row < rows; row++) {
218 for (let col = 0; col < cols; col++) {
219 const at = (row * cols + col) * 3
220 const top = pixel(col, row * 2)
221 const bottom = pixel(col, row * 2 + 1)
222 const isTop = top.a > bayer(col, row * 2)
223 const isBottom = bottom.a > bayer(col, row * 2 + 1)
224 words[at] = isTop || !isBottom ? (isTop ? 0x2580 : 0x20) : 0x2584
225 words[at + 1] = isTop ? pack(top.c) : isBottom ? pack(bottom.c) : CLEAR
226 words[at + 2] = isTop && isBottom ? pack(bottom.c) : CLEAR
227 }
228 }
229}
230
231export { parseHex, shade, mix, dim, hash, clamp, type Rgb }
232
233export function encode(words: Uint32Array) {
234 return base64(new Uint8Array(words.buffer))
235}
236
237export function cleanBackground(raw: unknown): Background | undefined {
238 if (typeof raw !== 'object' || raw === null) return undefined
239 const s = raw as Record<string, unknown>
240 const effect = EFFECTS.find(name => name === s.effect)
241 if (!effect) return undefined
242 const palette = Array.isArray(s.palette)
243 ? s.palette.filter((c): c is string => typeof c === 'string' && /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.test(c.trim())).slice(0, 6)
244 : []
245 if (palette.length < 2) return undefined
246
247 return {
248 effect,
249 palette,
250 speed: clamp(typeof s.speed === 'number' ? s.speed : 1, 0.1, 4),
251 intensity: clamp(typeof s.intensity === 'number' ? s.intensity : 0.6),
252 }
253}
254
255// Text the Raster can draw: one printable, single-width character a cell.
256export function cleanText(text: string, most: number) {
257 return text
258 .replace(/[‘’]/g, "'")
259 .replace(/[“”]/g, '"')
260 .replace(/[–—]/g, '-')
261 .replace(/…/g, '...')
262 .replace(/[^\x20-\x7e]/g, '')
263 .replace(/\s+/g, ' ')
264 .trim()
265 .slice(0, most)
266}
267hooks/clawd.ts 264 lines1// Clawd, the mascot, in pixel art: a rounded orange block with two eyes and
2// four little feet, 14 pixels wide and 8 tall at scale 1, which is 4 rows of
3// cells (two pixels to a cell). Scenes draw it with a pose, a facing, eyes,
4// a color and a scale, and get back where its head, sides and feet are, so
5// they can hang a hat on it or put a prop in its hand.
6
7import type { Rgb } from './effects'
8import type { Mesh } from './render3d'
9
10export const CLAWD_W = 14
11export const CLAWD_H = 8
12
13// The body: B is the body, E an eye (its resting place, 1 wide and 2 tall).
14// The claws are drawn per pose beside it.
15const BODY = [
16 '..BBBBBBBBBB..',
17 '..BBBBBBBBBB..',
18 '.BBBEBBBBBEBB.',
19 '.BBBEBBBBBEBB.',
20 '.BBBBBBBBBBBB.',
21 '..BBBBBBBBBB..',
22]
23
24export type Arm = 'up' | 'out' | 'down'
25export const ARMS: Arm[] = ['up', 'out', 'down']
26
27// A claw's pixels for each pose, for the left side (x from the body's left
28// edge at 1, so 0 is just outside it); the right side mirrors them. Each
29// ends at its tip, where things are held.
30const CLAWS: Record<Arm, { pixels: [number, number][]; tip: [number, number] }> = {
31 up: { pixels: [[0, 1], [0, 2]], tip: [0, 0] },
32 out: { pixels: [[-1, 2], [0, 2]], tip: [-2, 2] },
33 down: { pixels: [[0, 3], [0, 4]], tip: [0, 5] },
34}
35const BODY_H = BODY.length
36// The feet: two pairs under the body, each foot a pixel wide and two tall.
37const FEET = [3, 5, 8, 10]
38const EYES = [4, 9]
39const EYE_ROW = 2
40
41export const CLAWD_ORANGE: Rgb = [217, 119, 87]
42const EYE_DARK: Rgb = [42, 22, 16]
43
44export type Pose = 'stand' | 'walk' | 'jump' | 'sit'
45export type Eyes = 'open' | 'closed' | 'wide'
46
47export type ClawdLook = {
48 pose: Pose
49 // Which walk frame (0 to 3) while walking: a step cycle, so a walker's
50 // feet match its travel when this comes from its x.
51 stride: number
52 // -1 left, 0 ahead, 1 right: where the eyes look, and which way the feet
53 // swing.
54 facing: number
55 eyes: Eyes
56 // The eyes shifted from where they rest, in pixels: a glance.
57 look: { x: number; y: number }
58 isBlinking: boolean
59 color: Rgb
60 eyeColor: Rgb
61 // Whole pixels per pixel: 1 is 14x8, 2 is 28x16.
62 scale: number
63 // Where each claw is held.
64 arms: { left: Arm; right: Arm }
65}
66
67export const LOOK: ClawdLook = {
68 pose: 'stand',
69 stride: 0,
70 facing: 0,
71 eyes: 'open',
72 look: { x: 0, y: 0 },
73 isBlinking: false,
74 color: CLAWD_ORANGE,
75 eyeColor: EYE_DARK,
76 scale: 1,
77 arms: { left: 'up', right: 'up' },
78}
79
80export type Pixel = { x: number; y: number; c: Rgb }
81
82// Where things attach, in pixels from the sprite's top-left: above the head,
83// at either side, at the feet, and at the eyes.
84export type Anchors = {
85 w: number
86 h: number
87 top: { x: number; y: number }
88 left: { x: number; y: number }
89 right: { x: number; y: number }
90 feet: { x: number; y: number }
91 eyes: { x: number; y: number; w: number }
92}
93
94// The sprite's pixels at scale 1, body first and eyes last so a body pixel
95// never covers an eye that glanced aside.
96function pixelsAtOne(look: ClawdLook): Pixel[] {
97 const body: Pixel[] = []
98 const eyes: Pixel[] = []
99 // Sitting, the body rests on the ground where the feet would be.
100 const drop = look.pose === 'sit' ? CLAWD_H - BODY_H : 0
101 BODY.forEach((row, y) => {
102 for (let x = 0; x < row.length; x++) if (row[x] !== '.') body.push({ x, y: y + drop, c: look.color })
103 })
104 for (const [px, py] of CLAWS[look.arms.left].pixels) body.push({ x: px, y: py + drop, c: look.color })
105 for (const [px, py] of CLAWS[look.arms.right].pixels) body.push({ x: CLAWD_W - 1 - px, y: py + drop, c: look.color })
106 const ex = Math.max(-1, Math.min(1, Math.round(look.look.x))) || look.facing
107 const ey = Math.max(-1, Math.min(1, Math.round(look.look.y)))
108 for (const x0 of EYES) {
109 const x = x0 + ex
110 const y = EYE_ROW + ey + drop
111 if (look.eyes === 'closed' || look.isBlinking) {
112 // A line where the eye's lower pixel was.
113 eyes.push({ x, y: y + 1, c: look.eyeColor }, { x: x + (x0 === EYES[0] ? -1 : 1), y: y + 1, c: look.eyeColor })
114 } else if (look.eyes === 'wide') {
115 for (let dx = -1; dx <= 0; dx++) for (let dy = 0; dy <= 1; dy++) eyes.push({ x: x + dx, y: y + dy, c: look.eyeColor })
116 } else {
117 eyes.push({ x, y, c: look.eyeColor }, { x, y: y + 1, c: look.eyeColor })
118 }
119 }
120 const feet: Pixel[] = []
121 if (look.pose !== 'sit') {
122 const top = BODY_H
123 const ground = CLAWD_H - 1
124 const dir = look.facing || 1
125 FEET.forEach((x0, i) => {
126 // The left pair and the right pair swing against each other.
127 const isFront = i < 2
128 let swing = 0
129 let isLifted = false
130 if (look.pose === 'walk') {
131 // A four-beat step: the pairs swing past each other, and the pair
132 // mid-swing is off the ground.
133 const beat = ((Math.round(look.stride) % 4) + 4) % 4
134 if (beat === 0) swing = isFront ? 1 : -1
135 else if (beat === 2) swing = isFront ? -1 : 1
136 else isLifted = beat === 1 ? !isFront : isFront
137 } else if (look.pose === 'jump') {
138 isLifted = true
139 }
140 const x = x0 + swing * dir
141 for (let y = top; y <= (isLifted ? ground - 1 : ground); y++) feet.push({ x, y, c: look.color })
142 })
143 }
144 // A claw held out reaches a pixel past the box on either side.
145 const inside = (p: Pixel) => p.x >= -1 && p.x <= CLAWD_W && p.y >= 0 && p.y < CLAWD_H
146
147 return [...body, ...feet, ...eyes].filter(inside)
148}
149
150// The sprite's pixels at its scale, each pixel of the small sprite drawn as
151// a block of scale by scale.
152export function clawdPixels(look: ClawdLook): Pixel[] {
153 const scale = Math.max(1, Math.min(4, Math.round(look.scale)))
154 const small = pixelsAtOne(look)
155 if (scale === 1) return small
156 const out: Pixel[] = []
157 for (const p of small) {
158 for (let dy = 0; dy < scale; dy++) for (let dx = 0; dx < scale; dx++) out.push({ x: p.x * scale + dx, y: p.y * scale + dy, c: p.c })
159 }
160
161 return out
162}
163
164export function clawdAnchors(look: ClawdLook): Anchors {
165 const s = Math.max(1, Math.min(4, Math.round(look.scale)))
166 const drop = look.pose === 'sit' ? CLAWD_H - BODY_H : 0
167 // The claws' tips, where things are held.
168 const leftTip = CLAWS[look.arms.left].tip
169 const rightTip = CLAWS[look.arms.right].tip
170
171 return {
172 w: CLAWD_W * s,
173 h: CLAWD_H * s,
174 top: { x: Math.floor((CLAWD_W / 2) * s), y: drop * s - 1 },
175 left: { x: leftTip[0] * s - (s - 1), y: (leftTip[1] + drop) * s },
176 right: { x: (CLAWD_W - 1 - rightTip[0]) * s + (s - 1), y: (rightTip[1] + drop) * s },
177 feet: { x: Math.floor((CLAWD_W / 2) * s), y: CLAWD_H * s },
178 eyes: { x: EYES[0]! * s, y: (EYE_ROW + drop) * s, w: (EYES[1]! - EYES[0]! + 1) * s },
179 }
180}
181
182// Clawd as a solid for 3D scenes, in the sprite's pixel units: x across its
183// middle, y up from its feet, z toward the viewer. The body is the sprite's
184// outline as a block as deep as it is tall, with feet at its corners and the
185// claws beside it. The eyes are not part of it: a scene draws them flat on
186// its face, where clawdPoints puts them.
187const meshes = new Map<string, Mesh>()
188const BODY_DEPTH = 3
189
190export function clawdMesh(look: ClawdLook): Mesh {
191 const drop = look.pose === 'sit' ? CLAWD_H - BODY_H : 0
192 const beat = look.pose === 'walk' ? ((Math.round(look.stride) % 4) + 4) % 4 : -1
193 const key = `${look.pose}|${beat}|${look.arms.left}|${look.arms.right}`
194 const done = meshes.get(key)
195 if (done) return done
196 const verts: number[] = []
197 const faces: number[] = []
198 const vert = (x: number, y: number, z: number) => {
199 verts.push(x, y, z)
200
201 return verts.length / 3 - 1
202 }
203 const poly = (ids: number[]) => {
204 for (let i = 1; i + 1 < ids.length; i++) faces.push(ids[0]!, ids[i]!, ids[i + 1]!)
205 }
206 const block = (x0: number, x1: number, y0: number, y1: number, z0: number, z1: number) => {
207 const c = [vert(x0, y0, z0), vert(x1, y0, z0), vert(x1, y1, z0), vert(x0, y1, z0), vert(x0, y0, z1), vert(x1, y0, z1), vert(x1, y1, z1), vert(x0, y1, z1)]
208 for (const q of [[0, 1, 2, 3], [5, 4, 7, 6], [4, 0, 3, 7], [1, 5, 6, 2], [3, 2, 6, 7], [4, 5, 1, 0]]) poly(q.map(i => c[i]!))
209 }
210 // The body: 12 wide, 10 at its top and bottom rows, as a chamfered prism.
211 const bottom = CLAWD_H - BODY_H - drop
212 const top = CLAWD_H - drop
213 const outline: [number, number][] = [[-5, bottom], [5, bottom], [6, bottom + 1], [6, top - 1], [5, top], [-5, top], [-6, top - 1], [-6, bottom + 1]]
214 const front = outline.map(([x, y]) => vert(x, y, BODY_DEPTH))
215 const back = outline.map(([x, y]) => vert(x, y, -BODY_DEPTH))
216 poly(front)
217 poly([...back].reverse())
218 for (let i = 0; i < outline.length; i++) {
219 const j = (i + 1) % outline.length
220 poly([front[i]!, back[i]!, back[j]!, front[j]!])
221 }
222 // Feet at the corners; walking lifts one diagonal pair, then the other.
223 if (look.pose !== 'sit') {
224 for (const [fx, fz, diagonal] of [[-2.5, 1.5, 0], [2.5, -1.5, 0], [-2.5, -1.5, 1], [2.5, 1.5, 1]] as const) {
225 const isLifted = look.pose === 'jump' || (beat === 1 && diagonal === 0) || (beat === 3 && diagonal === 1)
226 block(fx - 0.5, fx + 0.5, isLifted ? 1 : 0, bottom, fz - 0.5, fz + 0.5)
227 }
228 }
229 // The claws, each pixel a block, mirrored for the right side.
230 for (const [side, arm] of [[-1, look.arms.left], [1, look.arms.right]] as const) {
231 for (const [px, py] of CLAWS[arm].pixels) {
232 const col = side < 0 ? px : CLAWD_W - 1 - px
233 block(col - CLAWD_W / 2, col - CLAWD_W / 2 + 1, CLAWD_H - py - 1 - drop, CLAWD_H - py - drop, -0.5, 0.5)
234 }
235 }
236 const mesh = { verts, faces }
237 meshes.set(key, mesh)
238
239 return mesh
240}
241
242// Where things attach on the solid, in its pixel units (z toward the viewer):
243// above the head, the claw tips, under the feet, and the eyes on its face,
244// each eye's center with how it looks (open, wide, or shut as a line), and
245// the span the two eyes cover.
246export function clawdPoints(look: ClawdLook) {
247 const drop = look.pose === 'sit' ? CLAWD_H - BODY_H : 0
248 const ex = Math.max(-1, Math.min(1, Math.round(look.look.x))) || look.facing
249 const ey = Math.max(-1, Math.min(1, Math.round(look.look.y)))
250 const half = CLAWD_W / 2
251 const tipAt = (side: number, tip: [number, number]) => ({ x: (side < 0 ? tip[0] : CLAWD_W - 1 - tip[0]) + 0.5 - half + (side < 0 ? -0.5 : 0.5), y: CLAWD_H - tip[1] - 0.5 - drop, z: 0 })
252 const eyeY = CLAWD_H - EYE_ROW - ey - 1 - drop
253 const shut = look.eyes === 'closed' || look.isBlinking
254
255 return {
256 top: { x: 0, y: CLAWD_H - drop, z: 0 },
257 left: tipAt(-1, CLAWS[look.arms.left].tip),
258 right: tipAt(1, CLAWS[look.arms.right].tip),
259 feet: { x: 0, y: 0, z: 0 },
260 eyes: EYES.map(col => ({ x: col + ex + 0.5 - half, y: eyeY, z: BODY_DEPTH, kind: shut ? ('shut' as const) : look.eyes === 'wide' ? ('wide' as const) : ('open' as const) })),
261 eyeSpan: { x: EYES[0]! + ex - half, y: eyeY, z: BODY_DEPTH, w: EYES[1]! - EYES[0]! + 1 },
262 }
263}
264hooks/lang.ts 1698 lines1// A small JavaScript-like language for scenes the buddy writes as code. The
2// module's environment has no eval, and the code comes from a model reading
3// the developer's repo, so it runs here, in an interpreter of its own: it sees
4// only its own values and the functions handed to it, and every step spends
5// fuel, so a runaway loop stops the scene rather than the terminal.
6//
7// What it knows: let/const/var, functions and arrow functions (closures),
8// if/else, for, for-of, for-in, while, break/continue/return, numbers,
9// strings and template literals, arrays, plain objects, destructuring, the
10// usual operators, Math, and a whitelist of array and string methods.
11
12// A value of the language: a number, string, boolean, null, undefined, array,
13// plain object, or function.
14// eslint-disable-next-line @typescript-eslint/no-explicit-any
15type V = any
16
17export class ScriptError extends Error {}
18
19// ---------------------------------------------------------------- tokens
20
21type Tok =
22 | { k: 'num'; v: number; at: number }
23 | { k: 'str'; v: string; at: number }
24 | { k: 'tpl'; strs: string[]; exprs: string[]; at: number }
25 | { k: 'id' | 'op' | 'eof'; v: string; at: number }
26
27const OPS = [
28 '>>>=', '===', '!==', '**=', '>>>', '<<=', '>>=', '...', '=>', '==', '!=', '<=', '>=', '&&', '||', '??', '?.', '++', '--',
29 '+=', '-=', '*=', '/=', '%=', '**', '<<', '>>', '&=', '|=', '^=', '&&=', '||=', '??=',
30].sort((a, b) => b.length - a.length)
31const SINGLE = '{}()[];,.?:+-*/%<>=!&|^~'
32
33function lex(src: string): Tok[] {
34 const toks: Tok[] = []
35 let i = 0
36 const fail = (what: string) => {
37 throw new ScriptError(`${what} at line ${src.slice(0, i).split('\n').length}`)
38 }
39 while (i < src.length) {
40 const c = src[i] as string
41 if (/\s/.test(c)) {
42 i++
43 } else if (src.startsWith('//', i)) {
44 while (i < src.length && src[i] !== '\n') i++
45 } else if (src.startsWith('/*', i)) {
46 const end = src.indexOf('*/', i + 2)
47 i = end < 0 ? src.length : end + 2
48 } else if (/[0-9]/.test(c) || (c === '.' && /[0-9]/.test(src[i + 1] ?? ''))) {
49 const m = /^(0x[0-9a-f_]+|0b[01_]+|(\d[\d_]*\.?[\d_]*|\.\d[\d_]*)(e[+-]?\d+)?)/i.exec(src.slice(i))
50 const text = m?.[0] ?? c
51 toks.push({ k: 'num', v: Number(text.replace(/_/g, '')), at: i })
52 i += text.length
53 } else if (/[A-Za-z_$]/.test(c)) {
54 const m = /^[A-Za-z_$][A-Za-z0-9_$]*/.exec(src.slice(i))
55 const text = m?.[0] ?? c
56 toks.push({ k: 'id', v: text, at: i })
57 i += text.length
58 } else if (c === '"' || c === "'") {
59 const at = i
60 let v = ''
61 i++
62 while (i < src.length && src[i] !== c) {
63 if (src[i] === '\\') {
64 const [ch, next] = escapeAt(src, i + 1)
65 v += ch
66 i = next
67 } else {
68 v += src[i]
69 i++
70 }
71 }
72 if (i >= src.length) fail('unclosed string')
73 i++
74 toks.push({ k: 'str', v, at })
75 } else if (c === '`') {
76 const at = i
77 const strs: string[] = []
78 const exprs: string[] = []
79 let v = ''
80 i++
81 while (i < src.length && src[i] !== '`') {
82 if (src[i] === '\\') {
83 const [ch, next] = escapeAt(src, i + 1)
84 v += ch
85 i = next
86 } else if (src.startsWith('${', i)) {
87 strs.push(v)
88 v = ''
89 const start = i + 2
90 i = closeOf(src, start)
91 exprs.push(src.slice(start, i))
92 i++
93 } else {
94 v += src[i]
95 i++
96 }
97 }
98 if (i >= src.length) fail('unclosed template')
99 i++
100 strs.push(v)
101 toks.push({ k: 'tpl', strs, exprs, at })
102 } else {
103 // "?." before a digit is a ternary and a number: t > 0 ? .5 : 1
104 const op = OPS.find(o => src.startsWith(o, i) && !(o === '?.' && /\d/.test(src[i + 2] ?? ''))) ?? (SINGLE.includes(c) ? c : undefined)
105 if (!op) fail(`unexpected "${c}"`)
106 toks.push({ k: 'op', v: op as string, at: i })
107 i += (op as string).length
108 }
109 }
110 toks.push({ k: 'eof', v: '', at: src.length })
111
112 return toks
113}
114
115const SIMPLE_ESCAPES: Record<string, string> = { n: '\n', t: '\t', r: '\r', b: '\b', f: '\f', v: '\v', '0': '\0' }
116
117// The character an escape at i (just after its backslash) stands for, and
118// where the escape ends.
119function escapeAt(src: string, i: number): [string, number] {
120 const c = src[i] ?? ''
121 const simple = SIMPLE_ESCAPES[c]
122 if (simple !== undefined) return [simple, i + 1]
123 if (c === 'x' || c === 'u') {
124 const m = (c === 'x' ? /^[0-9a-f]{2}/i : /^([0-9a-f]{4}|\{[0-9a-f]{1,6}\})/i).exec(src.slice(i + 1, i + 9))
125 const code = m ? parseInt(m[0].replace(/[{}]/g, ''), 16) : NaN
126 if (m && code <= 0x10ffff) return [String.fromCodePoint(code), i + 1 + m[0].length]
127 }
128
129 return [c, i + 1]
130}
131
132// From just after a template's "${", the index of the "}" that closes it,
133// stepping over strings and nested templates so their braces do not count.
134function closeOf(src: string, i: number): number {
135 let depth = 1
136 while (i < src.length) {
137 const c = src[i]
138 if (c === '"' || c === "'" || c === '`') {
139 i = skipString(src, i)
140 continue
141 }
142 if (c === '{') depth++
143 else if (c === '}' && --depth === 0) return i
144 i++
145 }
146
147 return i
148}
149
150// The index just past the string literal that opens at i.
151function skipString(src: string, i: number): number {
152 const q = src[i]
153 i++
154 while (i < src.length && src[i] !== q) {
155 if (src[i] === '\\') i += 2
156 else if (q === '`' && src.startsWith('${', i)) i = closeOf(src, i + 2) + 1
157 else i++
158 }
159
160 return i + 1
161}
162
163// ---------------------------------------------------------------- syntax
164
165type Node = { t: string; [key: string]: V }
166
167const ASSIGN = new Set(['=', '+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '>>>=', '&=', '|=', '^=', '&&=', '||=', '??='])
168const BINARY: Record<string, number> = {
169 '??': 1, '||': 2, '&&': 3, '|': 4, '^': 5, '&': 6,
170 '==': 7, '!=': 7, '===': 7, '!==': 7,
171 '<': 8, '>': 8, '<=': 8, '>=': 8, in: 8,
172 '<<': 9, '>>': 9, '>>>': 9,
173 '+': 10, '-': 10, '*': 11, '/': 11, '%': 11, '**': 12,
174}
175
176function parse(src: string): Node[] {
177 const toks = lex(src)
178 let p = 0
179 const peek = (o = 0) => toks[Math.min(p + o, toks.length - 1)] as Tok
180 const fail = (what: string): never => {
181 throw new ScriptError(`${what} at line ${src.slice(0, peek().at).split('\n').length}`)
182 }
183 const is = (v: string, o = 0) => {
184 const tok = peek(o)
185
186 return (tok.k === 'op' || tok.k === 'id') && tok.v === v
187 }
188 const eat = (v: string) => {
189 if (!is(v)) fail(`expected "${v}" but found "${'v' in peek() ? String((peek() as { v: unknown }).v) : peek().k}"`)
190 p++
191 }
192 const maybe = (v: string) => (is(v) ? (p++, true) : false)
193 const name = () => {
194 const tok = peek()
195 if (tok.k !== 'id') fail('expected a name')
196 p++
197
198 return (tok as { v: string }).v
199 }
200 const semi = () => {
201 maybe(';')
202 }
203
204 function program(): Node[] {
205 const body: Node[] = []
206 while (peek().k !== 'eof') body.push(statement())
207
208 return body
209 }
210
211 function block(): Node[] {
212 eat('{')
213 const body: Node[] = []
214 while (!is('}')) {
215 if (peek().k === 'eof') fail('missing "}"')
216 body.push(statement())
217 }
218 eat('}')
219
220 return body
221 }
222
223 function pattern(): Node {
224 let target: Node
225 if (maybe('[')) {
226 const items: (Node | null)[] = []
227 while (!is(']')) {
228 if (is(',')) items.push(null)
229 else items.push(restOr(pattern))
230 if (!is(']')) eat(',')
231 }
232 eat(']')
233 target = { t: 'arrPat', items }
234 } else if (maybe('{')) {
235 const props: [string, Node][] = []
236 while (!is('}')) {
237 if (maybe('...')) {
238 props.push(['...', { t: 'rest', target: pattern() }])
239 } else {
240 const key = name()
241 props.push([key, maybe(':') ? pattern() : withDefault({ t: 'id', name: key })])
242 }
243 if (!is('}')) eat(',')
244 }
245 eat('}')
246 target = { t: 'objPat', props }
247 } else {
248 target = { t: 'id', name: name() }
249 }
250
251 return withDefault(target)
252 }
253 function withDefault(target: Node): Node {
254 return maybe('=') ? { t: 'default', target, def: assign() } : target
255 }
256 // A "...rest" item in a list pattern, or an ordinary one.
257 function restOr(item: () => Node): Node {
258 return maybe('...') ? { t: 'rest', target: pattern() } : item()
259 }
260
261 // An array or object literal on the left of "=" is a pattern: [a, b] = [b, a].
262 function toPattern(e: Node): Node {
263 switch (e.t) {
264 case 'id':
265 case 'member':
266 return e
267 case 'spread':
268 return { t: 'rest', target: toPattern(e.e) }
269 case 'assign':
270 if (e.op === '=') return { t: 'default', target: toPattern(e.target), def: e.value }
271 break
272 case 'arr':
273 return { t: 'arrPat', items: (e.items as Node[]).map(toPattern) }
274 case 'obj':
275 return {
276 t: 'objPat',
277 props: (e.props as [Node | string, Node][]).map(([k, v]) => (typeof k === 'string' ? [k, k === '...' ? { t: 'rest', target: toPattern(v) } : toPattern(v)] : fail('cannot assign to that'))),
278 }
279 }
280
281 return fail('cannot assign to that')
282 }
283
284 function declaration(): Node {
285 const kind = name()
286 const decls: [Node, Node | undefined][] = []
287 do {
288 const target = pattern()
289 decls.push(target.t === 'default' ? [target.target, target.def] : [target, undefined])
290 } while (maybe(','))
291
292 return { t: 'decl', kind, decls }
293 }
294
295 function params(): Node[] {
296 eat('(')
297 const list: Node[] = []
298 while (!is(')')) {
299 list.push(restOr(pattern))
300 if (!is(')')) eat(',')
301 }
302 eat(')')
303
304 return list
305 }
306
307 function statement(): Node {
308 const tok = peek()
309 if (is('{')) return { t: 'block', body: block() }
310 if (is(';')) {
311 p++
312
313 return { t: 'empty' }
314 }
315 if (tok.k === 'id') {
316 switch (tok.v) {
317 case 'let':
318 case 'const':
319 case 'var': {
320 const d = declaration()
321 semi()
322
323 return d
324 }
325 case 'function': {
326 p++
327 const fnName = name()
328
329 return { t: 'fnDecl', name: fnName, fn: { t: 'fn', name: fnName, params: params(), body: block() } }
330 }
331 case 'if': {
332 p++
333 eat('(')
334 const test = expression()
335 eat(')')
336 const then = statement()
337
338 return { t: 'if', test, then, otherwise: maybe('else') ? statement() : undefined }
339 }
340 case 'while': {
341 p++
342 eat('(')
343 const test = expression()
344 eat(')')
345
346 return { t: 'while', test, body: statement() }
347 }
348 case 'do': {
349 p++
350 const body = statement()
351 eat('while')
352 eat('(')
353 const test = expression()
354 eat(')')
355 semi()
356
357 return { t: 'doWhile', test, body }
358 }
359 case 'for': {
360 p++
361 eat('(')
362 const isDecl = is('let') || is('const') || is('var')
363 if (isDecl && (is('of', 2) || is('in', 2) || peek(1).k === 'op')) {
364 // for (const x of xs), for (const [a, b] of xs), for (const k in o)
365 const save = p
366 const kind = name()
367 const target = pattern()
368 if (is('of') || is('in')) {
369 const each = name()
370 const iter = expression()
371 eat(')')
372
373 return { t: each === 'of' ? 'forOf' : 'forIn', kind, target, iter, body: statement() }
374 }
375 p = save
376 }
377 const init = is(';') ? undefined : isDecl ? declaration() : { t: 'expr', e: expression() }
378 eat(';')
379 const test = is(';') ? undefined : expression()
380 eat(';')
381 const update = is(')') ? undefined : expression()
382 eat(')')
383
384 return { t: 'for', init, test, update, body: statement() }
385 }
386 case 'switch': {
387 p++
388 eat('(')
389 const disc = expression()
390 eat(')')
391 eat('{')
392 const cases: { test?: Node; body: Node[] }[] = []
393 while (!is('}')) {
394 let test: Node | undefined
395 if (maybe('case')) test = expression()
396 else eat('default')
397 eat(':')
398 const body: Node[] = []
399 while (!is('case') && !is('default') && !is('}')) {
400 if (peek().k === 'eof') fail('missing "}"')
401 body.push(statement())
402 }
403 cases.push({ test, body })
404 }
405 eat('}')
406
407 return { t: 'switch', disc, cases }
408 }
409 case 'return': {
410 p++
411 const value = is(';') || is('}') ? undefined : expression()
412 semi()
413
414 return { t: 'return', value }
415 }
416 case 'break':
417 case 'continue':
418 p++
419 semi()
420
421 return { t: tok.v }
422 }
423 }
424 const e = expression()
425 semi()
426
427 return { t: 'expr', e }
428 }
429
430 function expression(): Node {
431 let e = assign()
432 while (maybe(',')) e = { t: 'seq', a: e, b: assign() }
433
434 return e
435 }
436
437 // An arrow function starts here: x =>, () =>, (a, b) =>, ([a, b]) =>.
438 function isArrow(): boolean {
439 if (peek().k === 'id' && is('=>', 1)) return true
440 if (!is('(')) return false
441 let depth = 0
442 for (let o = 0; ; o++) {
443 const tok = peek(o)
444 if (tok.k === 'eof') return false
445 if (tok.k === 'op' && ['(', '[', '{'].includes(tok.v)) depth++
446 if (tok.k === 'op' && [')', ']', '}'].includes(tok.v)) depth--
447 if (depth === 0) return is('=>', o + 1)
448 }
449 }
450
451 function assign(): Node {
452 if (isArrow()) {
453 const list = peek().k === 'id' ? [{ t: 'id', name: name() }] : params()
454 eat('=>')
455 if (is('{')) return { t: 'fn', arrow: true, params: list, body: block() }
456
457 return { t: 'fn', arrow: true, params: list, body: [{ t: 'return', value: assign() }] }
458 }
459 const left = conditional()
460 const tok = peek()
461 if (tok.k === 'op' && ASSIGN.has(tok.v)) {
462 const target = tok.v === '=' && (left.t === 'arr' || left.t === 'obj') ? toPattern(left) : left
463 if (!['id', 'member', 'arrPat', 'objPat'].includes(target.t)) fail('cannot assign to that')
464 p++
465
466 return { t: 'assign', op: tok.v, target, value: assign() }
467 }
468
469 return left
470 }
471
472 function conditional(): Node {
473 const test = binary(1)
474 if (!maybe('?')) return test
475 const a = assign()
476 eat(':')
477
478 return { t: 'cond', test, a, b: assign() }
479 }
480
481 function binary(min: number): Node {
482 let left = unary()
483 for (;;) {
484 const tok = peek()
485 const op = tok.k === 'op' || (tok.k === 'id' && tok.v === 'in') ? tok.v : undefined
486 const prec = op === undefined ? undefined : BINARY[op]
487 if (op === undefined || prec === undefined || prec < min) return left
488 p++
489 // ** binds to the right; the rest to the left.
490 const right = binary(op === '**' ? prec : prec + 1)
491 left = { t: ['&&', '||', '??'].includes(op) ? 'logic' : 'bin', op, l: left, r: right }
492 }
493 }
494
495 function unary(): Node {
496 const tok = peek()
497 if (tok.k === 'op' && ['-', '+', '!', '~'].includes(tok.v)) {
498 p++
499
500 return { t: 'unary', op: tok.v, arg: unary() }
501 }
502 if (tok.k === 'op' && (tok.v === '++' || tok.v === '--')) {
503 p++
504
505 return { t: 'update', op: tok.v, prefix: true, target: unary() }
506 }
507 if (tok.k === 'id' && tok.v === 'typeof') {
508 p++
509
510 return { t: 'typeof', arg: unary() }
511 }
512 const e = postfix()
513 const after = peek()
514 if (after.k === 'op' && (after.v === '++' || after.v === '--')) {
515 p++
516
517 return { t: 'update', op: after.v, prefix: false, target: e }
518 }
519
520 return e
521 }
522
523 function args(): Node[] {
524 eat('(')
525 const list: Node[] = []
526 while (!is(')')) {
527 list.push(maybe('...') ? { t: 'spread', e: assign() } : assign())
528 if (!is(')')) eat(',')
529 }
530 eat(')')
531
532 return list
533 }
534
535 function postfix(): Node {
536 let e: Node
537 if (maybe('new')) {
538 const callee = primary()
539 e = { t: 'call', callee, args: is('(') ? args() : [] }
540 } else {
541 e = primary()
542 }
543 // Past a "?.", the whole rest of the chain is optional: o?.a.b() is
544 // undefined when o is, as in JavaScript.
545 let optional = false
546 for (;;) {
547 if (maybe('?.')) {
548 optional = true
549 if (is('(')) {
550 e = { t: 'call', callee: e, args: args(), optional }
551 continue
552 }
553 if (!is('[')) {
554 e = { t: 'member', obj: e, prop: { t: 'lit', v: name() }, optional }
555 continue
556 }
557 }
558 if (maybe('.')) e = { t: 'member', obj: e, prop: { t: 'lit', v: name() }, optional }
559 else if (maybe('[')) {
560 const prop = expression()
561 eat(']')
562 e = { t: 'member', obj: e, prop, optional }
563 } else if (is('(')) e = { t: 'call', callee: e, args: args(), optional }
564 else return e
565 }
566 }
567
568 function primary(): Node {
569 const tok = peek()
570 p++
571 if (tok.k === 'num') return { t: 'lit', v: tok.v }
572 if (tok.k === 'str') return { t: 'lit', v: tok.v }
573 if (tok.k === 'tpl') return { t: 'tpl', strs: tok.strs, exprs: tok.exprs.map(source => parseExpression(source)) }
574 if (tok.k === 'id') {
575 if (tok.v === 'true') return { t: 'lit', v: true }
576 if (tok.v === 'false') return { t: 'lit', v: false }
577 if (tok.v === 'null') return { t: 'lit', v: null }
578 if (tok.v === 'undefined') return { t: 'lit', v: undefined }
579 if (tok.v === 'function') {
580 const fnName = peek().k === 'id' ? name() : undefined
581
582 // A function expression's name is its own to use inside; a
583 // declaration's is the enclosing scope's.
584 return { t: 'fn', name: fnName, self: fnName !== undefined, params: params(), body: block() }
585 }
586
587 return { t: 'id', name: tok.v }
588 }
589 if (tok.k === 'op' && tok.v === '(') {
590 const e = expression()
591 eat(')')
592
593 return e
594 }
595 if (tok.k === 'op' && tok.v === '[') {
596 const items: Node[] = []
597 while (!is(']')) {
598 items.push(maybe('...') ? { t: 'spread', e: assign() } : assign())
599 if (!is(']')) eat(',')
600 }
601 eat(']')
602
603 return { t: 'arr', items }
604 }
605 if (tok.k === 'op' && tok.v === '{') {
606 const props: [Node | string, Node][] = []
607 while (!is('}')) {
608 if (maybe('...')) {
609 props.push(['...', assign()])
610 } else {
611 const key = peek()
612 let k: Node | string
613 if (maybe('[')) {
614 k = expression()
615 eat(']')
616 } else {
617 p++
618 k = key.k === 'num' || key.k === 'str' || key.k === 'id' ? String((key as { v: unknown }).v) : fail('bad property name')
619 }
620 if (typeof k === 'string' && (is(',') || is('}'))) props.push([k, { t: 'id', name: k }])
621 else if (typeof k === 'string' && is('(')) props.push([k, { t: 'fn', name: k, params: params(), body: block() }])
622 else {
623 eat(':')
624 props.push([k, assign()])
625 }
626 }
627 if (!is('}')) eat(',')
628 }
629 eat('}')
630
631 return { t: 'obj', props }
632 }
633
634 return fail(`unexpected "${(tok as { v?: unknown }).v ?? (tok as { k: string }).k}"`)
635 }
636
637 function parseExpression(source: string): Node {
638 const [first] = parse(`(${source});`)
639
640 return (first as Node).e as Node
641 }
642
643 return program()
644}
645
646// ---------------------------------------------------------------- running
647
648// The fuel left for the current run; every statement, loop turn and call
649// spends one.
650let fuel = 0
651// When the run must be over by the clock: what a host method does on one
652// unit of fuel (copying a huge object, reading a long string as a number)
653// is caught here when fuel alone would not catch it.
654let deadline = Infinity
655const late = () => {
656 if (performance.now() > deadline) throw new ScriptError('the scene code took too long to run')
657}
658const burn = () => {
659 if (--fuel < 0) throw new ScriptError('the scene code took too long to run')
660 if ((fuel & 255) === 0) late()
661}
662// Work over n elements inside one host method spends fuel too, so a loop of
663// slices over a huge array cannot run for seconds on one unit each.
664const cost = (n: number) => {
665 fuel -= Math.max(0, Math.min(n, 2 ** 31 - 1)) >> 5
666 if (fuel < 0) throw new ScriptError('the scene code took too long to run')
667 if (n >= 1024) late()
668}
669// The keys of an object about to be copied or walked, paid for.
670const keysOf = (v: Record<string, V>) => sized(Object.keys(v))
671
672// The largest array the code may build, and the longest string.
673const MAX_LENGTH = 100_000
674
675class Scope {
676 vars = new Map<string, V>()
677 consts?: Set<string>
678 // A function's own scope, where its var declarations land.
679 constructor(readonly parent?: Scope, readonly isFunction = false) {}
680 declare(name: string, value: V, isConst = false) {
681 this.vars.set(name, value)
682 if (isConst) (this.consts ??= new Set()).add(name)
683 else this.consts?.delete(name)
684 }
685 lookup(name: string): V {
686 for (let s: Scope | undefined = this; s; s = s.parent) if (s.vars.has(name)) return s.vars.get(name)
687 throw new ScriptError(`${name} is not defined`)
688 }
689 assign(name: string, value: V) {
690 for (let s: Scope | undefined = this; s; s = s.parent) {
691 if (s.vars.has(name)) {
692 if (s.consts?.has(name)) throw new ScriptError(`${name} is a const`)
693 s.vars.set(name, value)
694
695 return
696 }
697 }
698 throw new ScriptError(`${name} is not defined`)
699 }
700}
701
702// A function the code made.
703class Fn {
704 constructor(
705 readonly params: Node[],
706 readonly body: Exec,
707 readonly scope: Scope,
708 readonly name?: string,
709 readonly self = false,
710 // An arrow function keeps the `this` of where it was written.
711 readonly isArrow = false,
712 ) {}
713}
714
715type Eval = (s: Scope) => V
716type Signal = undefined | 'break' | 'continue' | { value: V }
717type Exec = (s: Scope) => Signal
718
719export const obj = (entries: Record<string, V> = {}) => Object.assign(Object.create(null) as Record<string, V>, entries)
720const isObj = (v: V): v is Record<string, V> => typeof v === 'object' && v !== null && !Array.isArray(v) && Object.getPrototypeOf(v) === null
721const describe = (v: V) => (v === null ? 'null' : v === undefined ? 'undefined' : typeof v === 'string' ? `"${v.slice(0, 20)}"` : Array.isArray(v) ? 'an array' : typeof v)
722
723// What a value reads as in a string: objects have no prototype here, so the
724// host's String would throw on them.
725const str = (v: V, depth = 0): string => {
726 if (Array.isArray(v)) return depth > 8 ? '' : joinAll(v, ',', depth + 1)
727 if (isObj(v)) return '[object Object]'
728 if (v instanceof Fn || typeof v === 'function') return `function ${v.name ?? ''}() {}`
729
730 return String(v)
731}
732const joinAll = (a: V[], sep: string, depth = 0): string => {
733 cost(a.length)
734 let out = ''
735 for (let i = 0; i < a.length; i++) {
736 if (i > 0) out += sep
737 const v = a[i]
738 if (v !== null && v !== undefined) out += str(v, depth)
739 checkLength(out.length)
740 }
741
742 return out
743}
744
745// `self` is what `this` means inside: the holder of a method call.
746export function call(f: V, args: V[], self?: V): V {
747 burn()
748 if (f instanceof Fn) {
749 const scope = new Scope(f.scope, true)
750 if (f.self) scope.declare(f.name as string, f)
751 if (!f.isArrow) scope.declare('this', self)
752 bindList(f.params, args, scope, 'let')
753 scope.declare('arguments', args)
754 const signal = f.body(scope)
755
756 return typeof signal === 'object' ? signal.value : undefined
757 }
758 if (typeof f === 'function') return (f as (...a: V[]) => V)(...args)
759 throw new ScriptError(`${describe(f)} is not a function`)
760}
761
762const checkLength = (n: number) => {
763 if (n > MAX_LENGTH) throw new ScriptError('an array or string grew too big')
764
765 return n
766}
767
768// The length of the array a method over a would build or walk.
769const sized = (a: V[]) => (cost(a.length), a)
770
771const ARRAY: Record<string, (a: V[], ...args: V[]) => V> = {
772 push: (a, ...v) => (checkLength(a.length + v.length), a.push(...v)),
773 pop: a => a.pop(),
774 shift: a => sized(a).shift(),
775 unshift: (a, ...v) => (checkLength(a.length + v.length), sized(a).unshift(...v)),
776 slice: (a, s, e) => sized(a).slice(s, e),
777 splice: (a, s, n, ...v) => (s === undefined ? [] : (checkLength(a.length + v.length), sized(a).splice(s, n === undefined ? a.length : n, ...v))),
778 concat: (a, ...v) => {
779 let n = a.length
780 for (const x of v) n += Array.isArray(x) ? x.length : 1
781 cost(checkLength(n))
782
783 return a.concat(...v)
784 },
785 indexOf: (a, v, from) => sized(a).indexOf(v, from),
786 lastIndexOf: (a, v, from) => (from === undefined ? sized(a).lastIndexOf(v) : sized(a).lastIndexOf(v, from)),
787 includes: (a, v, from) => sized(a).includes(v, from),
788 join: (a, sep) => joinAll(a, sep === undefined ? ',' : str(sep)),
789 reverse: a => sized(a).reverse(),
790 fill: (a, v, s, e) => sized(a).fill(v, s, e),
791 at: (a, i) => a.at(i),
792 map: (a, f) => a.map((v, i) => call(f, [v, i, a])),
793 filter: (a, f) => a.filter((v, i) => call(f, [v, i, a])),
794 forEach: (a, f) => void a.forEach((v, i) => call(f, [v, i, a])),
795 some: (a, f) => a.some((v, i) => call(f, [v, i, a])),
796 every: (a, f) => a.every((v, i) => call(f, [v, i, a])),
797 find: (a, f) => a.find((v, i) => call(f, [v, i, a])),
798 findIndex: (a, f) => a.findIndex((v, i) => call(f, [v, i, a])),
799 reduce: (a, f, ...init) =>
800 init.length > 0 ? a.reduce((acc, v, i) => call(f, [acc, v, i, a]), init[0]) : a.reduce((acc, v, i) => call(f, [acc, v, i, a])),
801 sort: (a, f) => {
802 cost(a.length * Math.ceil(Math.log2(a.length + 1)))
803
804 const byString = (x: V, y: V) => {
805 const sx = str(x)
806 const sy = str(y)
807
808 return sx < sy ? -1 : sx > sy ? 1 : 0
809 }
810
811 return a.sort(f === undefined ? byString : (x, y) => Number(call(f, [x, y])) || 0)
812 },
813 flat: a => {
814 let n = 0
815 for (const x of a) n += Array.isArray(x) ? x.length : 1
816 cost(checkLength(n))
817
818 return a.flat()
819 },
820}
821
822const text = (s: string) => (cost(s.length), s)
823const padTo = (n: V) => (cost(checkLength(Number(n) || 0)), n)
824
825const STRING: Record<string, (s: string, ...args: V[]) => V> = {
826 slice: (s, a, b) => text(s).slice(a, b),
827 substring: (s, a, b) => text(s).substring(a, b),
828 charAt: (s, i) => s.charAt(i),
829 charCodeAt: (s, i) => s.charCodeAt(i),
830 codePointAt: (s, i) => s.codePointAt(i),
831 indexOf: (s, v, from) => text(s).indexOf(str(v), from),
832 lastIndexOf: (s, v, from) => text(s).lastIndexOf(str(v), from),
833 includes: (s, v, from) => text(s).includes(str(v), from),
834 startsWith: (s, v, from) => s.startsWith(str(v), from),
835 endsWith: (s, v, end) => s.endsWith(str(v), end),
836 split: (s, sep, limit) => text(s).split(sep === undefined ? (undefined as unknown as string) : str(sep), limit),
837 repeat: (s, n) => {
838 const times = Math.max(0, Math.floor(Number(n) || 0))
839 cost(checkLength(s.length * times))
840
841 return s.repeat(times)
842 },
843 padStart: (s, n, f) => s.padStart(padTo(n), f === undefined ? undefined : str(f)),
844 padEnd: (s, n, f) => s.padEnd(padTo(n), f === undefined ? undefined : str(f)),
845 toUpperCase: s => text(s).toUpperCase(),
846 toLowerCase: s => text(s).toLowerCase(),
847 trim: s => text(s).trim(),
848 replace: (s, a, b) => {
849 const out = text(s).replace(str(a), str(b))
850 checkLength(out.length)
851
852 return out
853 },
854 replaceAll: (s, a, b) => {
855 const parts = text(s).split(str(a))
856 const glue = str(b)
857 checkLength(s.length + (parts.length - 1) * glue.length)
858
859 return parts.join(glue)
860 },
861 at: (s, i) => s.at(i),
862}
863
864const own = <T>(table: Record<string, T>, key: V): T | undefined =>
865 Object.prototype.hasOwnProperty.call(table, str(key)) ? table[str(key)] : undefined
866
867// Names hung on a library function, as Array.from hangs on Array.
868const STATICS = new WeakMap<object, Record<string, V>>()
869
870function get(target: V, key: V): V {
871 if (target === null || target === undefined) throw new ScriptError(`cannot read "${str(key)}" of ${target}`)
872 if (Array.isArray(target)) {
873 if (typeof key === 'number') return target[key]
874 if (key === 'length') return target.length
875 const m = own(ARRAY, key)
876 if (m) return (...a: V[]) => m(target, ...a)
877 if (/^\d+$/.test(str(key))) return target[Number(key)]
878
879 return undefined
880 }
881 if (typeof target === 'string') {
882 if (typeof key === 'number' || /^\d+$/.test(str(key))) return target[Number(key)]
883 if (key === 'length') return target.length
884 const m = own(STRING, key)
885
886 return m ? (...a: V[]) => m(target, ...a) : undefined
887 }
888 if (typeof target === 'number') {
889 if (key === 'toFixed') return (d: V) => target.toFixed(Math.max(0, Math.min(20, Number(d) || 0)))
890
891 return undefined
892 }
893 if (isObj(target)) return own(target, key)
894 if (typeof target === 'function') {
895 const statics = STATICS.get(target)
896
897 return statics ? own(statics, key) : undefined
898 }
899
900 return undefined
901}
902
903function set(target: V, key: V, value: V): V {
904 if (Array.isArray(target)) {
905 if (key === 'length') {
906 target.length = checkLength(Math.max(0, Math.floor(Number(value) || 0)))
907
908 return value
909 }
910 const i = Number(key)
911 if (!Number.isInteger(i) || i < 0) throw new ScriptError(`bad array index ${str(key)}`)
912 checkLength(i + 1)
913 target[i] = value
914
915 return value
916 }
917 if (isObj(target)) {
918 if (Object.isFrozen(target)) throw new ScriptError(`cannot set "${str(key)}": built-in objects are read-only`)
919 target[str(key)] = value
920
921 return value
922 }
923 throw new ScriptError(`cannot set "${str(key)}" on ${describe(target)}`)
924}
925
926// An operand read as a number or compared loosely: a long string costs its
927// length to scan, and a plain object (which has no toString here) or an array
928// reads as the string it would in JavaScript, instead of throwing.
929const operand = (v: V): V => {
930 if (typeof v === 'string') {
931 cost(v.length)
932
933 return v
934 }
935
936 return isObj(v) || Array.isArray(v) ? str(v) : v
937}
938
939function binop(op: string, a: V, b: V): V {
940 if (op === 'in') return isObj(b) ? Object.prototype.hasOwnProperty.call(b, str(a)) : Array.isArray(b) ? Number(a) in b : false
941 if (op === '==' || op === '!=') {
942 // Two objects are the same only when they are one object.
943 const same = isPrimitive(a) || isPrimitive(b) ? operand(a) == operand(b) : a === b // eslint-disable-line eqeqeq
944
945 return op === '==' ? same : !same
946 }
947 if (op !== '+' && op !== '===' && op !== '!==') {
948 a = operand(a)
949 b = operand(b)
950 }
951 switch (op) {
952 case '+': {
953 if (typeof a === 'number' && typeof b === 'number') return a + b
954 const v = isPrimitive(a) && isPrimitive(b) ? a + b : str(a) + str(b)
955 if (typeof v === 'string') checkLength(v.length)
956
957 return v
958 }
959 case '-': return a - b
960 case '*': return a * b
961 case '/': return a / b
962 case '%': return a % b
963 case '**': return a ** b
964 case '===': return a === b
965 case '!==': return a !== b
966 case '<': return a < b
967 case '>': return a > b
968 case '<=': return a <= b
969 case '>=': return a >= b
970 case '&': return a & b
971 case '|': return a | b
972 case '^': return a ^ b
973 case '<<': return a << b
974 case '>>': return a >> b
975 case '>>>': return a >>> b
976 }
977 throw new ScriptError(`unknown operator ${op}`)
978}
979
980const isPrimitive = (v: V) => v === null || (typeof v !== 'object' && typeof v !== 'function')
981
982// Binds a pattern (a name, [a, b], {x, y}, with defaults and ...rest) to a
983// value: kind is let, const or var to declare, or "assign" to assign.
984function bind(pattern: Node, value: V, scope: Scope, kind: string) {
985 switch (pattern.t) {
986 case 'id': {
987 if (kind === 'assign') return scope.assign(pattern.name, value)
988 let target = scope
989 if (kind === 'var') while (!target.isFunction && target.parent) target = target.parent
990 target.declare(pattern.name, value, kind === 'const')
991
992 return
993 }
994 case 'member':
995 set(evalNode(pattern.obj)(scope), evalNode(pattern.prop)(scope), value)
996
997 return
998 case 'default':
999 bind(pattern.target, value === undefined ? evalNode(pattern.def)(scope) : value, scope, kind)
1000
1001 return
1002 case 'arrPat':
1003 bindList(pattern.items, value, scope, kind)
1004
1005 return
1006 case 'objPat': {
1007 const taken = new Set<string>()
1008 for (const [key, target] of pattern.props as [string, Node][]) {
1009 if (key !== '...') {
1010 taken.add(key)
1011 bind(target, get(value, key), scope, kind)
1012 continue
1013 }
1014 const rest = obj()
1015 if (isObj(value)) for (const k of keysOf(value)) if (!taken.has(k)) rest[k] = value[k]
1016 bind(target.target, rest, scope, kind)
1017 }
1018
1019 return
1020 }
1021 }
1022 throw new ScriptError('cannot assign to that')
1023}
1024
1025// Binds the items of a list pattern (or parameters) to a value's elements.
1026function bindList(items: (Node | null)[], value: V, scope: Scope, kind: string) {
1027 for (let i = 0; i < items.length; i++) {
1028 const item = items[i]
1029 if (!item) continue
1030 if (item.t === 'rest') {
1031 const list = Array.isArray(value) ? value : toList(value)
1032 cost(list.length)
1033 bind(item.target, list.slice(i), scope, kind)
1034
1035 return
1036 }
1037 bind(item, get(value, i), scope, kind)
1038 }
1039}
1040
1041// Calls visit on node and everything under it, stopping at functions unless
1042// told to go in.
1043function walk(node: V, visit: (n: Node) => void, intoFns: boolean) {
1044 if (Array.isArray(node)) {
1045 for (const n of node) walk(n, visit, intoFns)
1046
1047 return
1048 }
1049 if (node === null || typeof node !== 'object') return
1050 if (typeof node.t === 'string') {
1051 visit(node)
1052 if (node.t === 'fn' && !intoFns) return
1053 }
1054 for (const k in node) if (k !== 't') walk(node[k], visit, intoFns)
1055}
1056
1057// The names a pattern binds.
1058function patternNames(p: Node, out: string[]) {
1059 switch (p.t) {
1060 case 'id':
1061 out.push(p.name)
1062 break
1063 case 'default':
1064 case 'rest':
1065 patternNames(p.target, out)
1066 break
1067 case 'arrPat':
1068 for (const item of p.items as (Node | null)[]) if (item) patternNames(item, out)
1069 break
1070 case 'objPat':
1071 for (const [, target] of p.props as [string, Node][]) patternNames(target, out)
1072 }
1073}
1074
1075// The var names declared anywhere in a function body, which exist throughout it.
1076function varNames(body: Node[]): string[] {
1077 const out: string[] = []
1078 walk(body, n => {
1079 if (n.t === 'decl' && n.kind === 'var') for (const [target] of n.decls as [Node, Node | undefined][]) patternNames(target, out)
1080 if ((n.t === 'forOf' || n.t === 'forIn') && n.kind === 'var') patternNames(n.target, out)
1081 }, false)
1082
1083 return out
1084}
1085
1086const hasFn = (node: V): boolean => {
1087 let found = false
1088 walk(node, n => {
1089 if (n.t === 'fn') found = true
1090 }, true)
1091
1092 return found
1093}
1094
1095// Compiled closures, cached per node so a function body compiles once.
1096const compiled = new WeakMap<Node, Eval | Exec>()
1097function evalNode(node: Node): Eval {
1098 let f = compiled.get(node) as Eval | undefined
1099 if (!f) {
1100 f = compileExpr(node)
1101 compiled.set(node, f)
1102 }
1103
1104 return f
1105}
1106function execNode(node: Node): Exec {
1107 let f = compiled.get(node) as Exec | undefined
1108 if (!f) {
1109 f = compileStmt(node)
1110 compiled.set(node, f)
1111 }
1112
1113 return f
1114}
1115
1116function compileExpr(node: Node): Eval {
1117 switch (node.t) {
1118 case 'lit': {
1119 const v = node.v
1120
1121 return () => v
1122 }
1123 case 'tpl': {
1124 const strs = node.strs as string[]
1125 const exprs = (node.exprs as Node[]).map(evalNode)
1126
1127 return s => {
1128 let out = strs[0] ?? ''
1129 exprs.forEach((e, i) => {
1130 out += str(e(s)) + (strs[i + 1] ?? '')
1131 checkLength(out.length)
1132 })
1133
1134 return out
1135 }
1136 }
1137 case 'id': {
1138 const id = node.name as string
1139
1140 return s => s.lookup(id)
1141 }
1142 case 'arr': {
1143 const items = (node.items as Node[]).map(item => (item.t === 'spread' ? { spread: evalNode(item.e) } : { one: evalNode(item) }))
1144
1145 return s => {
1146 const out: V[] = []
1147 for (const item of items) {
1148 if (item.spread) out.push(...toList(item.spread(s)))
1149 else out.push(item.one(s))
1150 }
1151
1152 return out
1153 }
1154 }
1155 case 'obj': {
1156 const props = (node.props as [Node | string, Node][]).map(([k, v]) => ({ key: typeof k === 'string' ? k : evalNode(k), value: evalNode(v) }))
1157
1158 return s => {
1159 const out = obj()
1160 for (const { key, value } of props) {
1161 if (key === '...') {
1162 const v = value(s)
1163 if (isObj(v)) {
1164 keysOf(v)
1165 Object.assign(out, v)
1166 }
1167 }
1168 else out[typeof key === 'string' ? key : String(key(s))] = value(s)
1169 }
1170
1171 return out
1172 }
1173 }
1174 case 'fn': {
1175 const params = node.params as Node[]
1176 const body = compileBody(node.body as Node[], true)
1177 const name = node.name as string | undefined
1178 const self = node.self === true
1179
1180 return s => new Fn(params, body, s, name, self, node.arrow === true)
1181 }
1182 case 'seq': {
1183 const a = evalNode(node.a)
1184 const b = evalNode(node.b)
1185
1186 return s => (a(s), b(s))
1187 }
1188 case 'unary': {
1189 const arg = evalNode(node.arg)
1190 switch (node.op) {
1191 case '-': return s => -operand(arg(s))
1192 case '+': return s => +operand(arg(s))
1193 case '!': return s => !arg(s)
1194 default: return s => ~operand(arg(s))
1195 }
1196 }
1197 case 'typeof': {
1198 const arg = evalNode(node.arg)
1199 // typeof of a name never declared is "undefined"; any other error stands.
1200 const isName = (node.arg as Node).t === 'id'hooks/render3d.ts 765 lines1// A small 3D renderer for scene code. The world is sampled at a finer grid
2// than the cells (2 across and 4 down per cell, so the samples are square),
3// each sample is lit with a smoothly interpolated normal, and each cell is
4// then drawn from its samples: a cell a surface covers whole is a solid cell
5// in the lit color, a cell covered in part is the block glyph that matches
6// which quarters are covered (so edges come out anti-aliased), and a mesh
7// drawn `ascii` is instead the glyph whose density matches its brightness,
8// the classic terminal look, which suits curved things.
9
10import type { Rgb } from './effects'
11
12export type Vec3 = [number, number, number]
13
14// Triangles over shared vertices: verts is x, y, z per vertex; faces is
15// three vertex indices per triangle.
16export type Mesh = { verts: number[]; faces: number[] }
17
18// `fov` is the angle the strip shows across, in degrees (see NOMINAL_COLS).
19export type Camera = { eye: Vec3; target: Vec3; fov: number }
20export type Light = { dir: Vec3; ambient: number }
21export type Fog = { near: number; far: number; color: Rgb }
22
23export type Transform = {
24 at: Vec3
25 // Rotations in radians, applied about x, then y, then z.
26 rot: Vec3
27 scale: Vec3
28}
29
30export type Style = { color: Rgb; wire: boolean; unlit: boolean; ascii?: boolean }
31
32export const DEFAULT_CAMERA: Camera = { eye: [0, 2.5, 9], target: [0, 1, 0], fov: 90 }
33export const DEFAULT_LIGHT: Light = { dir: [0.4, 1, 0.6], ambient: 0.25 }
34export const DEFAULT_FOG: Fog = { near: 8, far: 40, color: [0, 0, 0] }
35
36// The strip is wide and only 9 rows tall, so the field of view is the
37// angle across it, measured over this many columns whatever the terminal's
38// width: things stay the same size on a wider terminal, which shows more.
39const NOMINAL_COLS = 120
40
41// Samples per cell, across and down.
42export const SX = 2
43export const SY = 4
44
45// Nothing nearer than this draws: the near plane, where geometry that
46// crosses behind the camera is cut.
47const NEAR = 0.05
48
49// The most triangles one frame draws, over every mesh.
50export const MOST_TRIANGLES = 20_000
51
52// A frame's samples: depth, the lit color, and what owns each (nothing, a
53// surface, or a sprite drawn in cells that the glyphs must leave alone).
54export type Frame = {
55 cols: number
56 rows: number
57 w: number
58 h: number
59 depth: Float32Array
60 color: Float32Array
61 owner: Uint8Array
62 // Cells with surface samples drawn since the last composite.
63 dirty: Uint8Array
64 triangles: number
65 // Called for every triangle or edge drawn, so the caller can keep time.
66 tick?: () => void
67}
68
69export const createFrame = (cols: number, rows: number): Frame => {
70 const w = cols * SX
71 const h = rows * SY
72
73 return {
74 cols,
75 rows,
76 w,
77 h,
78 depth: new Float32Array(w * h).fill(Infinity),
79 color: new Float32Array(w * h * 3),
80 owner: new Uint8Array(w * h),
81 dirty: new Uint8Array(cols * rows),
82 triangles: 0,
83 }
84}
85
86const SURFACE = 1
87const SPRITE = 2
88const ASCII = 3
89const isSurface = (owner: number) => owner === SURFACE || owner === ASCII
90
91const sub = (a: Vec3, b: Vec3): Vec3 => [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
92const dot = (a: Vec3, b: Vec3) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
93const cross = (a: Vec3, b: Vec3): Vec3 => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
94const norm = (a: Vec3): Vec3 => {
95 const l = Math.hypot(a[0], a[1], a[2]) || 1
96
97 return [a[0] / l, a[1] / l, a[2] / l]
98}
99const clamp01 = (v: number) => Math.min(1, Math.max(0, v))
100const smooth = (lo: number, hi: number, v: number) => {
101 const k = clamp01(hi === lo ? (v >= hi ? 1 : 0) : (v - lo) / (hi - lo))
102
103 return k * k * (3 - 2 * k)
104}
105
106// ---------------------------------------------------------------- camera
107
108export type View = { eye: Vec3; right: Vec3; up: Vec3; forward: Vec3; f: number; w: number; h: number }
109
110export function resolve(cam: Camera, w: number, h: number): View {
111 const forward = norm(sub(cam.target, cam.eye))
112 const worldUp: Vec3 = Math.abs(forward[1]) > 0.99 ? [0, 0, -1] : [0, 1, 0]
113 const right = norm(cross(forward, worldUp))
114 const up = cross(right, forward)
115 const fov = (Math.max(10, Math.min(170, cam.fov)) * Math.PI) / 180
116 // The focal length in samples: pixels over the nominal width, times the
117 // samples per column.
118 const f = ((NOMINAL_COLS / 2) * SX) / Math.tan(fov / 2)
119
120 return { eye: cam.eye, right, up, forward, f, w, h }
121}
122
123// A point in the camera's frame: across, up, and its depth ahead.
124type ViewPoint = { r: number; u: number; d: number }
125
126const toView = (view: View, p: Vec3): ViewPoint => {
127 const d = sub(p, view.eye)
128
129 return { r: dot(d, view.right), u: dot(d, view.up), d: dot(d, view.forward) }
130}
131
132// On the sample grid; `x` and `y` in samples.
133export type Projected = { x: number; y: number; depth: number }
134
135const toScreen = (view: View, v: ViewPoint): Projected => ({
136 x: (v.r * view.f) / v.d + view.w / 2,
137 y: view.h / 2 - (v.u * view.f) / v.d,
138 depth: v.d,
139})
140
141// A world point on the sample grid, or undefined behind the camera.
142export function project(view: View, p: Vec3): Projected | undefined {
143 const v = toView(view, p)
144
145 return v.d < NEAR ? undefined : toScreen(view, v)
146}
147
148// The part of a polygon in front of the near plane (Sutherland-Hodgman
149// against one plane), each vertex carrying its attributes along.
150type Vertex = { v: ViewPoint; p: Vec3; n: Vec3 }
151
152function clipNear(poly: Vertex[]): Vertex[] {
153 if (poly.every(x => x.v.d >= NEAR)) return poly
154 const out: Vertex[] = []
155 const mixv = (a: Vec3, b: Vec3, k: number): Vec3 => [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k, a[2] + (b[2] - a[2]) * k]
156 for (let i = 0; i < poly.length; i++) {
157 const a = poly[i]!
158 const b = poly[(i + 1) % poly.length]!
159 const aIn = a.v.d >= NEAR
160 const bIn = b.v.d >= NEAR
161 if (aIn) out.push(a)
162 if (aIn !== bIn) {
163 const k = (NEAR - a.v.d) / (b.v.d - a.v.d)
164 out.push({
165 v: { r: a.v.r + (b.v.r - a.v.r) * k, u: a.v.u + (b.v.u - a.v.u) * k, d: NEAR },
166 p: mixv(a.p, b.p, k),
167 n: mixv(a.n, b.n, k),
168 })
169 }
170 }
171
172 return out
173}
174
175// ---------------------------------------------------------------- meshes
176
177export function transformPoint(t: Transform, p: Vec3): Vec3 {
178 return placed(t, rotation(t.rot), p)
179}
180
181const placed = (t: Transform, m: Mat3, p: Vec3): Vec3 => {
182 const [x, y, z] = apply(m, [p[0] * t.scale[0], p[1] * t.scale[1], p[2] * t.scale[2]])
183
184 return [x + t.at[0], y + t.at[1], z + t.at[2]]
185}
186
187// A rotation as a row-major 3x3 matrix, built once per mesh rather than
188// six trig calls per point.
189type Mat3 = [number, number, number, number, number, number, number, number, number]
190
191// Rz * Ry * Rx, so x applies first.
192const rotation = (rot: Vec3): Mat3 => {
193 const [cx, sx, cy, sy, cz, sz] = [Math.cos(rot[0]), Math.sin(rot[0]), Math.cos(rot[1]), Math.sin(rot[1]), Math.cos(rot[2]), Math.sin(rot[2])]
194
195 return [cz * cy, cz * sy * sx - sz * cx, cz * sy * cx + sz * sx, sz * cy, sz * sy * sx + cz * cx, sz * sy * cx - cz * sx, -sy, cy * sx, cy * cx]
196}
197
198const apply = (m: Mat3, p: Vec3): Vec3 => [
199 m[0] * p[0] + m[1] * p[1] + m[2] * p[2],
200 m[3] * p[0] + m[4] * p[1] + m[5] * p[2],
201 m[6] * p[0] + m[7] * p[1] + m[8] * p[2],
202]
203
204// A normal under a transform: the inverse of the scale (a squashed sphere's
205// normals lean out, not along the squash), then the rotation.
206const transformNormal = (t: Transform, m: Mat3, n: Vec3): Vec3 =>
207 apply(m, norm([n[0] / (t.scale[0] || 1), n[1] / (t.scale[1] || 1), n[2] / (t.scale[2] || 1)]))
208
209// A mesh with a normal for each corner of each face: faces meeting at a
210// gentle angle share smoothed normals (a sphere looks round), faces meeting
211// at a crease keep their own (a cube stays sharp). And its edges to draw as
212// wire, each once: two vertex indices per edge, without the seams inside
213// flat polygons (the diagonal of a triangulated quad).
214type Prepared = { faceNormals: Vec3[]; cornerNormals: Vec3[]; edges: number[] }
215const prepared = new WeakMap<Mesh, Prepared>()
216const CREASE = Math.cos((50 * Math.PI) / 180)
217const FLAT = 0.999
218// The most faces around one vertex that smoothing considers: a real mesh has
219// a handful; a mesh of thousands of faces on three vertices would otherwise
220// cost the square of its size to prepare.
221const MOST_AROUND = 24
222
223export function prepare(mesh: Mesh): Prepared {
224 const done = prepared.get(mesh)
225 if (done) return done
226 const { verts, faces } = mesh
227 const at = (i: number): Vec3 => [verts[i * 3] ?? 0, verts[i * 3 + 1] ?? 0, verts[i * 3 + 2] ?? 0]
228 const count = Math.floor(faces.length / 3)
229 const faceNormals: Vec3[] = []
230 const around = new Map<number, number[]>()
231 for (let f = 0; f < count; f++) {
232 const [a, b, c] = [faces[f * 3]!, faces[f * 3 + 1]!, faces[f * 3 + 2]!]
233 faceNormals.push(norm(cross(sub(at(b), at(a)), sub(at(c), at(a)))))
234 for (const i of [a, b, c]) {
235 const list = around.get(i) ?? []
236 if (list.length < MOST_AROUND) list.push(f)
237 around.set(i, list)
238 }
239 }
240 const cornerNormals: Vec3[] = []
241 for (let f = 0; f < count; f++) {
242 const mine = faceNormals[f]!
243 for (let k = 0; k < 3; k++) {
244 const i = faces[f * 3 + k]!
245 const sum: Vec3 = [0, 0, 0]
246 for (const g of around.get(i) ?? []) {
247 const other = faceNormals[g]!
248 if (dot(mine, other) >= CREASE) {
249 sum[0] += other[0]
250 sum[1] += other[1]
251 sum[2] += other[2]
252 }
253 }
254 cornerNormals.push(norm(sum))
255 }
256 }
257 // Edges keyed by their two indices as one number (the index count is
258 // bounded, so this is exact), each with the faces along it.
259 const span = Math.floor(verts.length / 3) + 1
260 const edgeFaces = new Map<number, number[]>()
261 for (let f = 0; f < count; f++) {
262 for (let k = 0; k < 3; k++) {
263 const a = faces[f * 3 + k]!
264 const b = faces[f * 3 + ((k + 1) % 3)]!
265 const key = Math.min(a, b) * span + Math.max(a, b)
266 const along = edgeFaces.get(key)
267 if (along) along.push(f)
268 else edgeFaces.set(key, [f])
269 }
270 }
271 const edges: number[] = []
272 for (const [key, along] of edgeFaces) {
273 if (along.length === 2 && dot(faceNormals[along[0]!]!, faceNormals[along[1]!]!) > FLAT) continue
274 edges.push(Math.floor(key / span), key % span)
275 }
276 const result = { faceNormals, cornerNormals, edges }
277 prepared.set(mesh, result)
278
279 return result
280}
281
282// ---------------------------------------------------------------- drawing
283
284const fogged = (c: Rgb, fog: Fog, depth: number): Rgb => {
285 const k = smooth(fog.near, fog.far, depth)
286
287 return [c[0] + (fog.color[0] - c[0]) * k, c[1] + (fog.color[1] - c[1]) * k, c[2] + (fog.color[2] - c[2]) * k]
288}
289
290function plot(frame: Frame, x: number, y: number, depth: number, c: Rgb, owner = SURFACE) {
291 const at = y * frame.w + x
292 frame.depth[at] = depth
293 frame.owner[at] = owner
294 frame.color[at * 3] = c[0]
295 frame.color[at * 3 + 1] = c[1]
296 frame.color[at * 3 + 2] = c[2]
297 frame.dirty[Math.floor(y / SY) * frame.cols + Math.floor(x / SX)] = 1
298}
299
300// Lights a point of a surface: ambient, diffuse by the normal against the
301// light, and a small highlight where the light reflects toward the eye.
302function lit(color: Rgb, n: Vec3, p: Vec3, view: View, l: Vec3, ambient: number): Rgb {
303 const toEye = norm(sub(view.eye, p))
304 // Two-sided: a face seen from behind shades as if it faced the eye.
305 const nn: Vec3 = dot(n, toEye) < 0 ? [-n[0], -n[1], -n[2]] : n
306 const diffuse = Math.max(0, dot(nn, l))
307 const half = norm([l[0] + toEye[0], l[1] + toEye[1], l[2] + toEye[2]])
308 const spec = Math.pow(Math.max(0, dot(nn, half)), 24) * 0.35
309 const k = ambient + (1 - ambient) * diffuse
310
311 return [Math.min(255, color[0] * k + 255 * spec), Math.min(255, color[1] * k + 255 * spec), Math.min(255, color[2] * k + 255 * spec)]
312}
313
314// Fills one triangle on the sample grid, interpolating depth (linear in
315// 1/depth), world position and normal, lighting each sample.
316function fillTriangle(frame: Frame, view: View, fog: Fog, l: Vec3, ambient: number, tri: [Vertex, Vertex, Vertex], style: Style) {
317 const [A, B, C] = tri
318 const a = toScreen(view, A.v)
319 const b = toScreen(view, B.v)
320 const c = toScreen(view, C.v)
321 const area = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
322 // A triangle too thin to show, or one with a corner at infinity (its
323 // weights would be NaN and own every sample).
324 if (!(Math.abs(area) >= 1e-9) || !Number.isFinite(a.x + a.y + b.x + b.y + c.x + c.y)) return
325 const x0 = Math.max(0, Math.floor(Math.min(a.x, b.x, c.x)))
326 const x1 = Math.min(frame.w - 1, Math.ceil(Math.max(a.x, b.x, c.x)))
327 const y0 = Math.max(0, Math.floor(Math.min(a.y, b.y, c.y)))
328 const y1 = Math.min(frame.h - 1, Math.ceil(Math.max(a.y, b.y, c.y)))
329 if (x1 < x0 || y1 < y0) return
330 const ia = 1 / a.depth
331 const ib = 1 / b.depth
332 const ic = 1 / c.depth
333 for (let y = y0; y <= y1; y++) {
334 const py = y + 0.5
335 for (let x = x0; x <= x1; x++) {
336 const px = x + 0.5
337 let wa = ((b.x - px) * (c.y - py) - (b.y - py) * (c.x - px)) / area
338 let wb = ((c.x - px) * (a.y - py) - (c.y - py) * (a.x - px)) / area
339 let wc = 1 - wa - wb
340 if (wa < -1e-6 || wb < -1e-6 || wc < -1e-6) continue
341 const depth = 1 / (wa * ia + wb * ib + wc * ic)
342 const at = y * frame.w + x
343 if (depth >= frame.depth[at]!) continue
344 // Perspective-correct weights for the attributes.
345 wa *= ia * depth
346 wb *= ib * depth
347 wc *= ic * depth
348 let color = style.color
349 if (!style.unlit) {
350 const n = norm([
351 wa * A.n[0] + wb * B.n[0] + wc * C.n[0],
352 wa * A.n[1] + wb * B.n[1] + wc * C.n[1],
353 wa * A.n[2] + wb * B.n[2] + wc * C.n[2],
354 ])
355 const p: Vec3 = [
356 wa * A.p[0] + wb * B.p[0] + wc * C.p[0],
357 wa * A.p[1] + wb * B.p[1] + wc * C.p[1],
358 wa * A.p[2] + wb * B.p[2] + wc * C.p[2],
359 ]
360 color = lit(color, n, p, view, l, ambient)
361 }
362 plot(frame, x, y, depth, fogged(color, fog, depth), style.ascii ? ASCII : SURFACE)
363 }
364 }
365}
366
367// A line between two world points, a cell's width thick, cut at the near
368// plane and depth tested along its length.
369export function drawLine(frame: Frame, view: View, fog: Fog, p0: Vec3, p1: Vec3, color: Rgb) {
370 const zero: Vec3 = [0, 0, 0]
371 const ends = clipNear([
372 { v: toView(view, p0), p: p0, n: zero },
373 { v: toView(view, p1), p: p1, n: zero },
374 ])
375 if (ends.length < 2) return
376 const a = toScreen(view, ends[0]!.v)
377 const b = toScreen(view, ends[1]!.v)
378 // Only the part on screen is stepped, a sample at a time, with the step
379 // still a share of the whole segment so depth follows it correctly: an
380 // end cut at the near plane can project miles off-screen, and spreading
381 // a fixed count of steps over that leaves gaps in the part that shows.
382 const span = clipSpan(a.x, a.y, b.x, b.y, frame.w, frame.h)
383 if (!span) return
384 const [k0, k1] = span
385 const steps = Math.min(frame.w + frame.h, Math.ceil(Math.max(Math.abs(b.x - a.x), Math.abs(b.y - a.y)) * (k1 - k0)) || 1)
386 for (let i = 0; i <= steps; i++) {
387 const k = k0 + ((k1 - k0) * i) / steps
388 const x = Math.floor(a.x + (b.x - a.x) * k)
389 const y = Math.floor(a.y + (b.y - a.y) * k)
390 const depth = 1 / ((1 - k) / a.depth + k / b.depth)
391 // A little nearer than the surface it lies on, so edges show on faces.
392 dab(frame, x, y, depth - 0.03, fogged(color, fog, depth))
393 }
394}
395
396// The share of a segment inside the frame (Liang-Barsky), as the parameters
397// where it enters and leaves, or undefined when it misses. A sample of
398// margin keeps the dabs that straddle the frame's edge.
399function clipSpan(x0: number, y0: number, x1: number, y1: number, w: number, h: number): [number, number] | undefined {
400 let k0 = 0
401 let k1 = 1
402 for (const [p, dp, hi] of [[x0, x1 - x0, w], [y0, y1 - y0, h]] as const) {
403 if (dp === 0) {
404 if (p < -1 || p > hi) return undefined
405 continue
406 }
407 const ka = (-1 - p) / dp
408 const kb = (hi - p) / dp
409 k0 = Math.max(k0, Math.min(ka, kb))
410 k1 = Math.min(k1, Math.max(ka, kb))
411 }
412
413 return k0 <= k1 ? [k0, k1] : undefined
414}
415
416export function drawPoint(frame: Frame, view: View, fog: Fog, p: Vec3, color: Rgb) {
417 const a = project(view, p)
418 if (!a) return
419 dab(frame, Math.floor(a.x), Math.floor(a.y), a.depth, fogged(color, fog, a.depth))
420}
421
422// A 2 by 2 sample dab: what a line or a point needs to read as a glyph.
423function dab(frame: Frame, x: number, y: number, depth: number, color: Rgb) {
424 for (let dy = 0; dy < 2; dy++) {
425 for (let dx = 0; dx < 2; dx++) {
426 const sx = x + dx
427 const sy = y + dy
428 if (sx < 0 || sx >= frame.w || sy < 0 || sy >= frame.h) continue
429 const at = sy * frame.w + sx
430 if (depth >= frame.depth[at]!) continue
431 plot(frame, sx, sy, depth, color)
432 }
433 }
434}
435
436// Draws a mesh placed by a transform: filled and lit, or as wire edges.
437export function drawMesh(frame: Frame, view: View, light: Light, fog: Fog, mesh: Mesh, t: Transform, style: Style) {
438 const { verts, faces } = mesh
439 const { cornerNormals, edges } = prepare(mesh)
440 const m = rotation(t.rot)
441 const n = Math.floor(verts.length / 3)
442 const world: Vec3[] = []
443 const eyed: ViewPoint[] = []
444 for (let i = 0; i < n; i++) {
445 const p = placed(t, m, [verts[i * 3]!, verts[i * 3 + 1]!, verts[i * 3 + 2]!])
446 world.push(p)
447 eyed.push(toView(view, p))
448 }
449 if (style.wire) {
450 // Each edge once; an edge counts against the triangle budget.
451 for (let e = 0; e + 1 < edges.length && frame.triangles < MOST_TRIANGLES; e += 2) {
452 const a = world[edges[e]!]
453 const b = world[edges[e + 1]!]
454 if (!a || !b) continue
455 frame.triangles += 1
456 frame.tick?.()
457 drawLine(frame, view, fog, a, b, style.color)
458 }
459
460 return
461 }
462 const l = norm(light.dir)
463 const faceCount = Math.floor(faces.length / 3)
464 for (let f = 0; f < faceCount && frame.triangles < MOST_TRIANGLES; f++) {
465 const ids = [faces[f * 3]!, faces[f * 3 + 1]!, faces[f * 3 + 2]!]
466 const corners: Vertex[] = []
467 for (let k = 0; k < 3; k++) {
468 const v = eyed[ids[k]!]
469 const p = world[ids[k]!]
470 if (!v || !p) break
471 corners.push({ v, p, n: transformNormal(t, m, cornerNormals[f * 3 + k] ?? [0, 1, 0]) })
472 }
473 if (corners.length < 3) continue
474 if (corners.every(c => c.v.d < NEAR)) continue
475 frame.triangles += 1
476 frame.tick?.()
477 const poly = clipNear(corners)
478 for (let j = 1; j + 1 < poly.length; j++) fillTriangle(frame, view, fog, l, light.ambient, [poly[0]!, poly[j]!, poly[j + 1]!], style)
479 }
480}
481
482// Claims a sprite's pixel (a column and a pixel row, two to a cell) at a
483// depth: true when it is nearer than what is there, in which case the
484// glyphs leave that pixel to the sprite.
485export function claimPixel(frame: Frame, col: number, py: number, depth: number) {
486 const x0 = col * SX
487 const y0 = py * (SY / 2)
488 if (x0 < 0 || x0 >= frame.w || y0 < 0 || y0 >= frame.h) return false
489 let nearest = Infinity
490 for (let dy = 0; dy < SY / 2; dy++) for (let dx = 0; dx < SX; dx++) nearest = Math.min(nearest, frame.depth[(y0 + dy) * frame.w + x0 + dx]!)
491 if (depth >= nearest) return false
492 for (let dy = 0; dy < SY / 2; dy++) {
493 for (let dx = 0; dx < SX; dx++) {
494 const at = (y0 + dy) * frame.w + x0 + dx
495 frame.depth[at] = depth
496 frame.owner[at] = SPRITE
497 }
498 }
499
500 return true
501}
502
503// ---------------------------------------------------------------- glyphs
504
505// Glyphs by brightness, sparse to dense, for `ascii` meshes.
506const RAMP = [...' .,:;-=+*#%@']
507
508// The block glyph for each set of covered quarters: bit 1 top-left, 2
509// top-right, 4 bottom-left, 8 bottom-right.
510const QUARTERS = [' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█']
511
512// How far a sample's depth must jump from a neighbor's to count as an edge
513// (as a share of its depth), and how much the farther side darkens there:
514// a dark rim on the far side of every overlap, so shapes read as separate.
515const EDGE = 0.06
516const RIM = 0.45
517
518// Two groups closer than this (squared distance over the channels: about
519// 20 levels each) are one color: the cell stays solid rather than split.
520const ALIKE = 20 * 20 * 3
521
522// Draws the cells drawn on since the last composite. Each cell's four
523// quarters (two samples each) are split into two groups by color, and the
524// cell is the quarter-block glyph of that split with a color for each
525// group, so a cell can hold an edge, or a gradient, with two colors.
526export function composite(frame: Frame, put: (col: number, row: number, char: string, color: Rgb, back?: Rgb) => void) {
527 const per = SX * SY
528 const half = SY / 2
529 for (let row = 0; row < frame.rows; row++) {
530 for (let col = 0; col < frame.cols; col++) {
531 const cell = row * frame.cols + col
532 if (!frame.dirty[cell]) continue
533 frame.dirty[cell] = 0
534 // Per quarter: samples covered, their summed color; and the cell's.
535 const qCount = [0, 0, 0, 0]
536 const qSum: Rgb[] = [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]
537 let covered = 0
538 let sprite = 0
539 let ascii = 0
540 const sum: Rgb = [0, 0, 0]
541 for (let dy = 0; dy < SY; dy++) {
542 for (let dx = 0; dx < SX; dx++) {
543 const x = col * SX + dx
544 const y = row * SY + dy
545 const at = y * frame.w + x
546 const owner = frame.owner[at]!
547 if (owner === SPRITE) sprite += 1
548 if (!isSurface(owner)) continue
549 if (owner === ASCII) ascii += 1
550 const c = rimmed(frame, x, y, at)
551 const q = (dy < half ? 0 : 2) + (dx < SX / 2 ? 0 : 1)
552 qCount[q] = (qCount[q] ?? 0) + 1
553 qSum[q]![0] += c[0]
554 qSum[q]![1] += c[1]
555 qSum[q]![2] += c[2]
556 covered += 1
557 sum[0] += c[0]
558 sum[1] += c[1]
559 sum[2] += c[2]
560 }
561 }
562 // A cell a sprite holds is the sprite's, unless a surface drawn since
563 // covers more of it (something passing in front of Clawd).
564 if (covered === 0 || sprite >= covered) continue
565 const mean: Rgb = [sum[0] / covered, sum[1] / covered, sum[2] / covered]
566 const coverage = covered / per
567 if (ascii > 0) {
568 // The classic look: a glyph as dense as the brightness, thinned by
569 // coverage, in ink that keeps the hue, over a dark tint when solid.
570 // Mid-tones lean toward the denser glyphs (a gamma of 0.6).
571 const brightness = (0.299 * mean[0] + 0.587 * mean[1] + 0.114 * mean[2]) / 255
572 const level = Math.min(RAMP.length - 1, Math.round(Math.pow(brightness, 0.6) * coverage * (RAMP.length - 1)))
573 const back: Rgb | undefined = coverage >= 0.5 ? [mean[0] * 0.18, mean[1] * 0.18, mean[2] * 0.18] : undefined
574 put(col, row, RAMP[Math.max(1, level)] ?? '.', lift(mean), back)
575 continue
576 }
577 const qMean: (Rgb | undefined)[] = qCount.map((n, q) => (n > 0 ? [qSum[q]![0] / n, qSum[q]![1] / n, qSum[q]![2] / n] : undefined))
578 const onMask = qMean.reduce((m, c, q) => (c ? m | (1 << q) : m), 0)
579 // The split of the covered quarters into two color groups that keeps
580 // each group most alike; the uncovered quarters always sit in the
581 // second group, which draws as the background.
582 let best = onMask
583 let bestCost = Infinity
584 for (let mask = 1; mask <= onMask; mask++) {
585 if ((mask & onMask) !== mask) continue
586 const a = groupMean(qMean, mask)
587 const b = groupMean(qMean, onMask & ~mask)
588 let cost = 0
589 for (let q = 0; q < 4; q++) {
590 const c = qMean[q]
591 if (!c) continue
592 const g = mask & (1 << q) ? a : b
593 cost += g ? dist(c, g) : 0
594 }
595 if (cost < bestCost - 1e-9) {
596 bestCost = cost
597 best = mask
598 }
599 }
600 const fg = groupMean(qMean, best) ?? mean
601 const rest = onMask & ~best
602 const bg = groupMean(qMean, rest)
603 if (onMask === 15 && (best === 15 || (bg && dist(fg, bg) < ALIKE))) {
604 // Four quarters of much the same color: a solid cell.
605 put(col, row, ' ', mean, mean)
606 } else if (best === 15) {
607 // Four quarters of one color: a solid cell.
608 put(col, row, ' ', fg, fg)
609 } else if (bg && bitCount(rest) > bitCount(~onMask & 15)) {
610 // The second group covers more than the uncovered part: two colors.
611 put(col, row, QUARTERS[best] ?? '█', fg, bg)
612 } else {
613 put(col, row, QUARTERS[best] ?? '█', fg)
614 }
615 }
616 }
617}
618
619// A sample's color, darkened where its depth jumps away from a neighbor's
620// (the far side of an overlap). Depth is compared as 1/depth, which runs
621// straight across a flat surface on screen, so only a kink counts, not a
622// slope: a floor receding steadily toward the horizon gets no rim, a nearer
623// shape in front of it does. Along each axis the kink is how far this
624// sample sits beyond where its neighbors' trend puts it; at a surface's
625// border the trend comes from the two samples on the one side it has, so a
626// limb curving away still rims where a flat floor's far edge does not.
627function rimmed(frame: Frame, x: number, y: number, at: number): Rgb {
628 const c: Rgb = [frame.color[at * 3]!, frame.color[at * 3 + 1]!, frame.color[at * 3 + 2]!]
629 const i = 1 / frame.depth[at]!
630 // 1/depth of the surface sample k steps along an axis, if there is one.
631 const along = (dx: number, dy: number, k: number) => {
632 const sx = x + dx * k
633 const sy = y + dy * k
634 if (sx < 0 || sx >= frame.w || sy < 0 || sy >= frame.h) return undefined
635 const n = sy * frame.w + sx
636
637 return isSurface(frame.owner[n]!) ? 1 / frame.depth[n]! : undefined
638 }
639 const kink = (dx: number, dy: number) => {
640 const a = along(dx, dy, 1)
641 const b = along(dx, dy, -1)
642 if (a !== undefined && b !== undefined) return a + b - 2 * i
643 const side = a !== undefined ? 1 : b !== undefined ? -1 : 0
644 if (side === 0) return 0
645 const near = along(dx, dy, side)!
646 const beyond = along(dx, dy, 2 * side)
647 // Two samples wide there is no trend to go by: the jump alone decides.
648 if (beyond === undefined) return near - i > EDGE * near ? Infinity : 0
649
650 return 2 * near - beyond - i
651 }
652 const isRim = kink(1, 0) > EDGE * i || kink(0, 1) > EDGE * i
653
654 return isRim ? [c[0] * RIM, c[1] * RIM, c[2] * RIM] : c
655}
656
657const bitCount = (m: number) => ((m & 1) + ((m >> 1) & 1) + ((m >> 2) & 1) + ((m >> 3) & 1))
658
659const dist = (a: Rgb, b: Rgb) => (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2
660
661function groupMean(q: (Rgb | undefined)[], mask: number): Rgb | undefined {
662 const sum: Rgb = [0, 0, 0]
663 let n = 0
664 for (let i = 0; i < 4; i++) {
665 const c = q[i]
666 if (!(mask & (1 << i)) || !c) continue
667 sum[0] += c[0]
668 sum[1] += c[1]
669 sum[2] += c[2]
670 n += 1
671 }
672
673 return n > 0 ? [sum[0] / n, sum[1] / n, sum[2] / n] : undefined
674}
675
676// Ink that stays visible: the color brightened toward its hue when dark.
677function lift(c: Rgb): Rgb {
678 const max = Math.max(c[0], c[1], c[2], 1)
679 const k = Math.max(1, 90 / max)
680
681 return [Math.min(255, c[0] * k), Math.min(255, c[1] * k), Math.min(255, c[2] * k)]
682}
683
684// ---------------------------------------------------------------- shapes
685
686const quad = (faces: number[], a: number, b: number, c: number, d: number) => faces.push(a, b, c, a, c, d)
687
688// A box centered on the origin.
689export function box(w: number, h: number, d: number): Mesh {
690 const x = w / 2
691 const y = h / 2
692 const z = d / 2
693 const verts = [-x, -y, -z, x, -y, -z, x, y, -z, -x, y, -z, -x, -y, z, x, -y, z, x, y, z, -x, y, z]
694 const faces: number[] = []
695 quad(faces, 0, 1, 2, 3)
696 quad(faces, 5, 4, 7, 6)
697 quad(faces, 4, 0, 3, 7)
698 quad(faces, 1, 5, 6, 2)
699 quad(faces, 3, 2, 6, 7)
700 quad(faces, 4, 5, 1, 0)
701
702 return { verts, faces }
703}
704
705// A sphere of latitude rings and longitude segments.
706export function sphere(r: number, segments = 12): Mesh {
707 const seg = Math.max(3, Math.min(48, Math.round(segments)))
708 const rings = Math.max(2, Math.round(seg / 2))
709 const verts: number[] = []
710 const faces: number[] = []
711 for (let i = 0; i <= rings; i++) {
712 const phi = (i / rings) * Math.PI
713 for (let j = 0; j < seg; j++) {
714 const theta = (j / seg) * Math.PI * 2
715 verts.push(r * Math.sin(phi) * Math.cos(theta), r * Math.cos(phi), r * Math.sin(phi) * Math.sin(theta))
716 }
717 }
718 for (let i = 0; i < rings; i++) {
719 for (let j = 0; j < seg; j++) {
720 const a = i * seg + j
721 const b = i * seg + ((j + 1) % seg)
722 const c = (i + 1) * seg + ((j + 1) % seg)
723 const d = (i + 1) * seg + j
724 if (i > 0) faces.push(a, b, c)
725 if (i < rings - 1) faces.push(a, c, d)
726 }
727 }
728
729 return { verts, faces }
730}
731
732// A cylinder (or a cone when the top radius is 0) standing on the origin,
733// its base at y 0 and its top at y h.
734export function cylinder(rBottom: number, rTop: number, h: number, segments = 12): Mesh {
735 const seg = Math.max(3, Math.min(48, Math.round(segments)))
736 const verts: number[] = []
737 const faces: number[] = []
738 for (let j = 0; j < seg; j++) {
739 const theta = (j / seg) * Math.PI * 2
740 verts.push(rBottom * Math.cos(theta), 0, rBottom * Math.sin(theta))
741 verts.push(rTop * Math.cos(theta), h, rTop * Math.sin(theta))
742 }
743 // The centers of the two caps.
744 const cb = seg * 2
745 const ct = seg * 2 + 1
746 verts.push(0, 0, 0, 0, h, 0)
747 for (let j = 0; j < seg; j++) {
748 const a = j * 2
749 const b = ((j + 1) % seg) * 2
750 quad(faces, a, b, b + 1, a + 1)
751 faces.push(cb, b, a)
752 if (rTop > 0) faces.push(ct, a + 1, b + 1)
753 }
754
755 return { verts, faces }
756}
757
758// A flat rectangle on the ground (the x-z plane), centered on the origin.
759export function plane(w: number, d: number): Mesh {
760 const x = w / 2
761 const z = d / 2
762
763 return { verts: [-x, 0, -z, x, 0, -z, x, 0, z, -x, 0, z], faces: [0, 1, 2, 0, 2, 3] }
764}
765