A tall pane: a window room with cat and fish tank above, and Claude's workshop below that reacts to what Claude is doing.

<img src="images/hero-en.gif" alt="Claude Code on the left works through a task while the reef-tank pane on the right dims and a submarine comes down" width="720">
Hand Claude Code a task, and zone out at the fish while it works. (The chat window here is a mock-up; the tank is the real mod.)
Animated side panels for Claude Code that change when Claude is idle vs. working, plus two small prompt bands. I'm learning Claude Code mods by building things, and this is what came out so far.
Every scene is a hand-written SVG animation (no image files). The previews below are the real pictures, captured with the idle state on the left and the working state on the right (an 8-second loop).
한국어 설명은 아래에 있습니다.
This repository is a marketplace. Inside Claude Code:
/plugin marketplace add Gnosis333/mod_gallery
/plugin install night-train@mod-gallery
Or from a shell:
claude plugin marketplace add Gnosis333/mod_gallery
claude plugin install night-train@mod-gallery --scope user
Swap night-train for any mod below. Panels open only when you run their slash command (they don't open by themselves, to save CPU). After installing, run /reload-plugins or start a new session.
To try one mod without installing: claude --plugin-dir mods/night-train.
Labels (the status line, descriptions, replies) are English by default. Every mod also has a Korean (한국어) mode through its language option:
echo '{"language":"ko"}' | claude plugin configure night-train@mod-gallery --values-stdin
Then restart Claude Code. You can also run /plugin configure night-train@mod-gallery inside Claude Code, and claude plugin configure night-train@mod-gallery shows whether it is set. Use en to switch back.
| Mod | Command | What it is |
|---|---|---|
night-train | /train | A night-train window |
beach-day | /beach | A top-down beach |
snow-cabin | /snow | A snowy cabin, day and night |
deep-sub | /deep | A deep-sea submersible |
campfire | /fire | A campfire to stare at |
jungle-falls | /falls | A jungle waterfall with 24 kinds of animals |
rain-window | /rain | A rainy window |
moon-view | /moon | The universe seen from the Moon |
tower-cut | /tower | A building cross-section |
desk-garden | /garden | A desk plant and a workshop |
reef-tank | /reef | An aquarium |
usage-band | automatic | Context, limits and cost on one line |
haiku-warning | automatic | A warning while a Haiku model is selected |
<img src="images/night-train.gif" alt="night-train" width="460">
A tall train window. At rest the train waits at STATION under lit lamp posts; when work starts the station slides away and hills, trees and poles stream past at different speeds. Now and then it runs through a long tunnel, and flocks of migrating birds cross the sky. The steam from the cup blows sideways, and when the turn ends the train rolls back into the station.
<img src="images/beach-day.gif" alt="beach-day" width="460">
A beach seen from above. Gentle waves wash the shore, and people in sunglasses lie on sunbeds under parasols (bikinis, one-pieces and trunks). Out at sea swimmers float on all kinds of rings, a pink unicorn among them. While Claude works a jet ski and a banana boat go by, and crabs scuttle across the sand.
<img src="images/snow-cabin.gif" alt="snow-cabin" width="460">
Snow falls softly over a white forest, snowy peaks and a log cabin. It's daytime at rest; while Claude works night falls, the windows glow, the chimney smokes, and a snowman is built every 15 seconds. A white rabbit, a white fox, a magpie and a husky come by.
<img src="images/deep-sub.gif" alt="deep-sub" width="460">
A yellow submersible drifts through the dark among glowing jellyfish, an anglerfish, lanternfish, a dumbo octopus and a gulper eel, while something vast and unknowable stirs faintly far below. While Claude works the searchlight comes on, sonar pings, and now and then the shadow of a giant squid sweeps past.
<img src="images/campfire.gif" alt="campfire" width="460">
A campfire in a dense forest clearing under the stars. Flames lick, sparks crackle up, and a ring of logs and six kinds of flowers surround the fire. A fox, an owl (it swivels its head), a deer, a raccoon and a hedgehog drop by. The fire roars up while Claude works and dies back afterwards.
<img src="images/jungle-falls.gif" alt="jungle-falls" width="460">
A terraced waterfall winds down into a lily pool. 24 kinds of animals visit at random (birds, monkeys, a jaguar, a crocodile, frogs, otters and more) and fish leap up the falls. It's day at rest and night while Claude works. This is the heaviest mod on CPU.
<img src="images/rain-window.gif" alt="rain-window" width="460">
A rainy window: drops run down the glass and merge, city lights blur outside and a cat dozes on the sill. While Claude works a storm comes in, and when lightning strikes the cat opens its eyes and the desk lamp flickers. When the turn ends a rainbow comes out and fades away.
<img src="images/moon-view.gif" alt="moon-view" width="460">
The Earth seen from the Moon. The lander's "eyes" flash and an astronaut bounces about (and tumbles around while Claude works). Stars, supernovae, a black hole, rockets, satellites, UFOs, planets and a few Star Wars-like ships pass at random.
<img src="images/tower-cut.gif" alt="tower-cut" width="460">
A building cross-section. One floor goes up with every finished turn, and at 20 floors the tower tops out with fireworks. A new session starts over. While Claude works the crane moves and raises the frame of the next floor.
<img src="images/desk-garden.gif" alt="desk-garden" width="460">
Three pictures stacked: a flowerpot by a window (one more flower blooms with every finished turn), a dozing cat by a fish tank, and a workshop where Claude's character works. While Claude works the character types at the keyboard.
<img src="images/reef-tank.gif" alt="reef-tank" width="460">
A tall aquarium with fish, a seahorse and jellyfish. While Claude works the lights dim and a yellow submarine dives; every finished turn adds a pearl to the treasure chest.
<img src="images/usage-band.png" alt="usage-band" width="720">
One line above the prompt: context window fill, the 5-hour and 7-day plan limits, and session cost. It turns amber at 70% and brick red at 85%, and each limit shows when it resets (↻). The image is an illustration of the look.
<img src="images/haiku-warning.png" alt="haiku-warning" width="720">
Shows a red warning above the prompt while a Haiku model is selected, as a reminder to switch before complex work. The image is an illustration of the look.
jungle-falls, snow-cabin and campfire are larger pictures and relatively heavier.mods/<name>/ with .claude-plugin/plugin.json, hooks/register.tsx (the behaviour), a test file and a state type. The part of register.tsx between // ---scene-start and // ---scene-end is what draws the picture.language option (see Language). The pictures themselves contain no text, except a few English words such as STATION and the speech bubble in desk-garden.Built with Claude Code.
MIT. Use, change and share freely.
<img src="images/hero-ko.gif" alt="왼쪽 Claude Code가 일하는 동안 오른쪽 어항 패널이 어두워지고 잠수함이 내려오는 모습" width="720">
Claude Code에 일을 맡기고, 일하는 동안엔 어항 보며 멍~ (왼쪽 대화창은 비슷하게 그린 화면이고, 어항은 실제 모드입니다.)
Claude Code 옆에 띄워 두는 그림 패널과 입력창 위 상태 띠를 모았습니다. 패널은 Claude가 쉬고 있을 때와 일하고 있을 때 모습이 달라집니다. Claude Code의 모드를 공부하면서 이것저것 만들어 본 결과물입니다. 위 영어 부분의 그림(왼쪽 쉬는 중, 오른쪽 작업 중)이 그대로 해당합니다.
/plugin marketplace add Gnosis333/mod_gallery
/plugin install night-train@mod-gallery
night-train 자리에 위 표의 모드 이름을 넣으면 됩니다. 패널은 슬래시 명령으로 열 때만 열립니다(CPU를 아끼려고 자동으로 열지 않습니다). 설치 후 /reload-plugins를 입력하거나 새 세션을 여세요.
한글 라벨로 보려면 모드마다 language 옵션을 ko로 정하고 Claude Code를 다시 시작하세요(기본은 영어).
echo '{"language":"ko"}' | claude plugin configure night-train@mod-gallery --values-stdin
/plugin configure night-train@mod-gallery로도 정할 수 있습니다. 영어로 되돌리려면 en을 쓰세요.
/train): 야간열차 창가. 쉴 때는 STATION에 정차해 가로등이 켜져 있고, 작업 중에는 산·나무·전신주가 서로 다른 속도로 흘러가며 가끔 긴 터널을 지납니다. 하늘에는 철새떼가 날아가고, 턴이 끝나면 다시 역으로 들어옵니다./beach): 위에서 내려다본 해변. 파라솔 아래 썬베드에 선글라스를 낀 사람들이 누워 있고, 바다에는 핑크 유니콘 튜브를 비롯한 여러 튜브가 떠 있습니다. 작업 중에는 제트스키, 바나나보트, 꽃게가 나옵니다./snow): 눈이 소복소복 내리는 흰 숲과 설산, 통나무 오두막. 쉴 때는 낮, 작업 중에는 밤이 되어 창에 불이 켜지고 눈사람이 15초마다 하나씩 생깁니다. 흰 토끼, 흰 여우, 까치, 허스키가 찾아옵니다./deep): 어둠 속 노란 잠수정과 야광 해파리, 아귀, 덤보 문어, 풍선장어, 그리고 저 아래 희미하게 움직이는 정체불명의 거대한 것. 작업 중에는 탐조등과 소나가 켜지고 가끔 거대 오징어 그림자가 스칩니다./fire): 별이 가득한 밤 숲속 공터의 모닥불. 불꽃과 불티, 통나무 원, 꽃 여섯 종류, 여우·올빼미(머리를 돌립니다)·사슴·너구리·고슴도치. 작업 중에는 불이 커지고 끝나면 잦아듭니다./falls): 계단식 폭포와 연꽃 연못, 동물 24종이 무작위로 방문. 쉴 때는 낮, 작업 중에는 밤. CPU를 가장 많이 씁니다./rain): 비 오는 창가. 작업 중에는 폭풍, 번개가 치면 고양이가 눈을 뜨고 스탠드가 깜빡이며, 끝나면 무지개가 뜹니다./moon): 달에서 본 지구. 통통 뛰는 우주인(작업 중에는 빙빙 돕니다), 무작위로 지나가는 우주 물체와 스타워즈풍 우주선./tower): 빌딩 단면. 턴이 끝날 때마다 한 층씩 올라가고 20층이면 준공 불꽃놀이./garden): 창가 화분(턴마다 꽃이 핍니다), 졸고 있는 고양이와 수조, Claude가 일하는 작업실./reef): 수족관. 작업 중에는 조명이 어두워지고 노란 잠수함이 내려오며, 턴이 끝날 때마다 진주가 쌓입니다.↻ 포함), 비용을 보여줍니다.jungle-falls, snow-cabin, campfire는 그림이 커서 상대적으로 무겁습니다.language 옵션을 ko로 하면 한글입니다. 그림 속 글자는 STATION과 desk-garden의 말풍선 정도입니다(말풍선은 옵션을 따릅니다).모두 Claude Code로 만들었습니다.
hooks/register.tsx 784 lines1import { atom, read, update } from 'claude-code'
2import type { ElementTable, Register } from 'claude-code'
3
4import type { Work } from '../types'
5
6const PANE = 'desk-garden'
7const STRINGS = {
8 en: {
9 title: 'Desk garden',
10 alt: 'A flowerpot on a desk by a window: five flowers sway in the breeze while birds and insects pass outside.',
11 altWork: 'While Claude works the lamp comes on and the character types; each finished turn opens one more flower in the bed.',
12 bubble: { work: 'Working…', cheer: 'Done! A flower bloomed', rest: 'Idle' },
13 line: (blooms: number, slots: number, done: number, tools: number) => `Flowers ${blooms}/${slots} · turns done ${done} · tools ${tools}`,
14 on: '● Working',
15 off: '○ Idle',
16 opened: 'Opened the desk garden pane.',
17 note: 'The animation shows in the desktop app.',
18 },
19 ko: {
20 title: '책상 위 화분',
21 alt: '창가 책상 위 화분에 꽃 다섯 송이가 바람에 흔들리고, 창밖으로 새와 곤충이 지나갑니다.',
22 altWork: 'Claude가 일하는 동안 램프가 켜지고 캐릭터가 타이핑하며, 한 턴이 끝날 때마다 화단에 꽃이 한 송이씩 핍니다.',
23 bubble: { work: '작업 중…', cheer: '완료! 꽃이 피었어요', rest: '쉬는 중' },
24 line: (blooms: number, slots: number, done: number, tools: number) => `꽃 ${blooms}/${slots} · 완료 ${done}턴 · 도구 ${tools}회`,
25 on: '● 작업 중',
26 off: '○ 쉬는 중',
27 opened: '책상 위 화분을 열었습니다.',
28 note: '그림 움직임은 데스크톱 앱에서 보입니다.',
29 },
30}
31
32type Words = (typeof STRINGS)['en']
33
34// ---scene-start
35type Attrs = Record<string, string | number>
36
37const n = (v: number) => String(Math.round(v * 10) / 10)
38const fine = (v: number) => +v.toFixed(3)
39const EASE = '.45 0 .55 1'
40const keyed = (times: number[]) =>
41 `keyTimes="${times.join(';')}" calcMode="spline" keySplines="${times
42 .slice(1)
43 .map(() => EASE)
44 .join(';')}"`
45
46const rotateAbout = (
47 angles: number[],
48 cx: number,
49 cy: number,
50 dur: number,
51 begin: number,
52 times: number[],
53) =>
54 `<animateTransform attributeName="transform" type="rotate" values="${angles
55 .map(a => `${a} ${cx} ${cy}`)
56 .join(';')}" ${keyed(times)} dur="${dur}s" begin="${begin}s" repeatCount="indefinite"/>`
57
58// ---- flowers (drawn around 0,0) -------------------------------------------
59const ring = (count: number, rx: number, ry: number, dist: number, fill: string) =>
60 Array.from(
61 { length: count },
62 (_, i) =>
63 `<ellipse cx="0" cy="${-dist}" rx="${rx}" ry="${ry}" fill="${fill}" transform="rotate(${n(
64 (360 / count) * i,
65 )})"/>`,
66 ).join('')
67
68const pointed = (count: number, fill: string, scale: number, turn: number) =>
69 Array.from(
70 { length: count },
71 (_, i) =>
72 `<path d="M0 0 C-6 -6 -5 -15 0 -20 C5 -15 6 -6 0 0Z" fill="${fill}" transform="rotate(${n(
73 (360 / count) * i + turn,
74 )}) scale(${scale})"/>`,
75 ).join('')
76
77const FLOWERS = {
78 cosmos: () =>
79 ring(8, 5.2, 9, 9, '#e58fa6') +
80 ring(8, 1.8, 5, 11, '#f2b6c6') +
81 '<circle r="3.6" fill="#f0c94e"/>',
82 daisy: () =>
83 ring(14, 2.4, 8, 10, '#f3d35a') +
84 '<circle r="5.6" fill="#8a5a2b"/><circle r="2.4" fill="#6b4220"/>',
85 poppy: () =>
86 ring(5, 8.5, 8, 7.5, '#e2735a') +
87 ring(5, 5, 5, 6, '#ec9078') +
88 '<circle r="3.2" fill="#3a2a2a"/>',
89 violet: () =>
90 pointed(6, '#9b7fd1', 1, 0) +
91 pointed(6, '#b9a2e6', 0.6, 30) +
92 '<circle r="2.6" fill="#f4e6a6"/>',
93 tulip: () =>
94 '<path d="M-9 0 C-11 -12 -6 -19 -6 -23 L0 -15 L6 -23 C6 -19 11 -12 9 0 C5 5 -5 5 -9 0Z" fill="#f0a35e"/>' +
95 '<path d="M-3 4 C-8 -4 -3 -14 0 -15 C3 -14 8 -4 3 4Z" fill="#f7c58f"/>',
96}
97
98type Plant = {
99 bx: number
100 hx: number
101 hy: number
102 bend: number
103 flower: keyof typeof FLOWERS
104 leaves: [number, number, number][]
105}
106
107const BASE_Y = 218
108
109const PLANTS: Plant[] = [
110 { bx: 262, hx: 226, hy: 68, bend: -14, flower: 'cosmos', leaves: [[0.28, -1, 1.2], [0.55, 1, 1], [0.8, -1, 0.8]] },
111 { bx: 270, hx: 262, hy: 4, bend: 8, flower: 'daisy', leaves: [[0.3, 1, 1.3], [0.55, -1, 1.1], [0.8, 1, 0.8]] },
112 { bx: 280, hx: 304, hy: 38, bend: 12, flower: 'poppy', leaves: [[0.3, -1, 1.2], [0.58, 1, 1.2], [0.82, -1, 0.8]] },
113 { bx: 288, hx: 336, hy: 110, bend: 12, flower: 'violet', leaves: [[0.4, 1, 1.1], [0.7, -1, 0.9]] },
114 { bx: 266, hx: 252, hy: 150, bend: -4, flower: 'tulip', leaves: [[0.3, -1, 1.3], [0.6, 1, 1.1]] },
115]
116
117// A gust: still, a push, a few settling swings, still again.
118const GUST = [0, 0, 4.2, -3.2, 2.6, -1.6, 0.8, 0, 0]
119const GUST_T = [0, 0.4, 0.47, 0.54, 0.62, 0.7, 0.78, 0.86, 1]
120
121const plantSvg = (p: Plant, i: number) => {
122 const cx = (p.bx + p.hx) / 2 + p.bend
123 const cy = (BASE_Y + p.hy) / 2
124 const at = (t: number) => [
125 (1 - t) ** 2 * p.bx + 2 * (1 - t) * t * cx + t ** 2 * p.hx,
126 (1 - t) ** 2 * BASE_Y + 2 * (1 - t) * t * cy + t ** 2 * p.hy,
127 ]
128 const leaves = p.leaves
129 .map(([t, side, size]) => {
130 const [x, y] = at(t)
131
132 return `<path d="M0 0 C7 -9 18 -9 25 -2 C16 5 7 5 0 0Z" fill="#5f9457" transform="translate(${n(
133 x,
134 )} ${n(y)}) rotate(${side > 0 ? -22 : 202}) scale(${size})"/>`
135 })
136 .join('')
137
138 return (
139 `<g>${rotateAbout([-0.9, 0.9, -0.9], p.bx, BASE_Y, 6.5 + i * 0.9, -i * 1.3, [0, 0.5, 1])}` +
140 `<g>${rotateAbout(GUST, p.bx, BASE_Y, 19, -8 + i * 0.28, GUST_T)}` +
141 `<path d="M${p.bx} ${BASE_Y} Q${n(cx)} ${n(cy)} ${p.hx} ${p.hy}" fill="none" stroke="#4f7d4a" stroke-width="2.6" stroke-linecap="round"/>` +
142 leaves +
143 `<g transform="translate(${p.hx} ${p.hy}) scale(1.15)">${FLOWERS[p.flower]()}</g>` +
144 '</g></g>'
145 )
146}
147
148// ---- window -----------------------------------------------------------------
149const cloud = (y: number, s: number, dur: number, phase: number) =>
150 `<g transform="translate(-80 0)"><animateTransform attributeName="transform" type="translate" values="-80 0;210 0" dur="${dur}s" begin="${-dur * phase}s" repeatCount="indefinite"/>` +
151 `<g transform="translate(0 ${y}) scale(${s})" fill="#fff" opacity=".92">` +
152 '<ellipse cx="0" cy="0" rx="18" ry="7"/><ellipse cx="12" cy="-5" rx="12" ry="8"/><ellipse cx="-10" cy="-4" rx="10" ry="6"/></g></g>'
153
154const bird = (dx: number, dy: number, scale: number, phase: number) =>
155 `<g transform="translate(${dx} ${dy}) scale(${scale})">` +
156 '<path d="M-8 0 q4 -5 8 0 q4 -5 8 0" fill="none" stroke="#4a5058" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">' +
157 `<animate attributeName="d" values="M-8 0 q4 -5 8 0 q4 -5 8 0;M-8 0 q4 3 8 0 q4 3 8 0;M-8 0 q4 -5 8 0 q4 -5 8 0" dur=".75s" begin="${phase}s" repeatCount="indefinite"/></path></g>`
158
159// A flock crosses during the first `share` of its cycle and rests off-glass the rest.
160const flock = (
161 mates: [number, number][],
162 scale: number,
163 from: [number, number],
164 to: [number, number],
165 dur: number,
166 begin: number,
167 share: number,
168) =>
169 `<g transform="translate(${from[0]} ${from[1]})">` +
170 `<animateTransform attributeName="transform" type="translate" values="${from.join(' ')};${to.join(' ')};${to.join(' ')}" keyTimes="0;${share};1" dur="${dur}s" begin="${begin}s" repeatCount="indefinite"/>` +
171 mates.map(([dx, dy], i) => bird(dx, dy, scale, i * 0.11)).join('') +
172 '</g>'
173
174const V_RIGHT: [number, number][] = [[0, 0], [-9, 5], [-9, -5], [-18, 10], [-18, -10]]
175const V_LEFT: [number, number][] = [[0, 0], [8, 4], [8, -4], [16, 8]]
176
177const WINDOW =
178 '<rect x="18" y="36" width="156" height="200" rx="3" fill="#f6f1e6" stroke="#cfc3ac"/>' +
179 '<rect x="30" y="48" width="132" height="176" fill="url(#sky)"/>' +
180 '<g clip-path="url(#glass)">' +
181 '<circle cx="136" cy="84" r="26" fill="#fff6d0" opacity=".25"/><circle cx="136" cy="84" r="13" fill="#fff6d8" opacity=".85"/>' +
182 cloud(78, 1, 95, 0.35) +
183 cloud(118, 0.7, 140, 0.8) +
184 '<path d="M30 205 C60 188 80 196 110 192 C135 188 150 196 162 190 L162 224 L30 224Z" fill="#a9c8a0"/>' +
185 '<path d="M30 214 C70 200 100 210 130 206 C145 204 155 208 162 206 L162 224 L30 224Z" fill="#8fb88a"/>' +
186 flock(V_RIGHT, 1, [-30, 112], [195, 92], 42, 3, 0.3) +
187 flock(V_LEFT, 0.8, [200, 70], [-40, 86], 57, 31, 0.28) +
188 flock([[0, 0]], 1.3, [-20, 150], [190, 140], 71, 14, 0.22) +
189 '</g>' +
190 '<rect x="94" y="48" width="4" height="176" fill="#f6f1e6"/><rect x="30" y="133" width="132" height="4" fill="#f6f1e6"/>' +
191 '<rect x="30" y="48" width="132" height="176" fill="none" stroke="#cfc3ac" stroke-width="1.5"/>' +
192 '<rect x="10" y="232" width="172" height="10" rx="2" fill="#efe7d6" stroke="#cfc3ac"/>' +
193 '<rect x="14" y="242" width="164" height="3" fill="#000" opacity=".12"/>'
194
195// ---- insects (top view, heading +x, about 14 units long) ---------------------
196const wing = (px: number, py: number, rot: number, rx: number, ry: number, flip: number, fill = '#e9f1f5') =>
197 `<g transform="translate(${px} ${py})"><g><animateTransform attributeName="transform" type="rotate" values="${-flip * 10};${flip * 24};${-flip * 10}" dur=".14s" repeatCount="indefinite"/>` +
198 `<ellipse cx="0" cy="${flip * ry}" rx="${rx}" ry="${ry}" fill="${fill}" fill-opacity=".72" stroke="#9fb0ba" stroke-width=".4" transform="rotate(${flip * rot})"/></g></g>`
199
200const BEE = () =>
201 wing(0, -2.4, 25, 3, 5.4, -1) +
202 wing(0, 2.4, 25, 3, 5.4, 1) +
203 '<ellipse rx="6" ry="3.8" fill="#e9b824"/>' +
204 '<path d="M-1.8 -3.4 v6.8 M1.6 -3.4 v6.8" stroke="#2b2623" stroke-width="1.8"/>' +
205 '<circle cx="6.4" r="2.3" fill="#2b2623"/>'
206
207const BUTTERFLY = () =>
208 '<g transform="scale(1.25)">' +
209 '<g><animateTransform attributeName="transform" type="scale" values="1 1;1 .25;1 1" dur=".55s" repeatCount="indefinite"/>' +
210 '<g fill="#e98a35" stroke="#3b2a22" stroke-width=".6">' +
211 '<ellipse cx="1" cy="-6" rx="4.2" ry="6.4"/><ellipse cx="1" cy="6" rx="4.2" ry="6.4"/>' +
212 '<ellipse cx="-3.5" cy="-4.2" rx="3.4" ry="4.4" fill="#f0a653"/><ellipse cx="-3.5" cy="4.2" rx="3.4" ry="4.4" fill="#f0a653"/></g>' +
213 '<circle cx="0" cy="-8.4" r=".9" fill="#fff"/><circle cx="0" cy="8.4" r=".9" fill="#fff"/></g>' +
214 '<ellipse rx="4.2" ry="1.1" fill="#2f2724"/><circle cx="4.4" r="1.3" fill="#2f2724"/>' +
215 '<path d="M5 -.4 l3 -2 M5 .4 l3 2" stroke="#2f2724" stroke-width=".4"/></g>'
216
217const BEETLE = (fold: string) =>
218 `<g>${fold}${wing(-1, -3.2, 30, 3, 8, -1)}${wing(-1, 3.2, 30, 3, 8, 1)}</g>` +
219 '<ellipse rx="6.6" ry="4.6" fill="#4c8a46" stroke="#2b5a30" stroke-width=".8"/>' +
220 '<path d="M-6 0 H5" stroke="#2b5a30" stroke-width=".7"/>' +
221 '<ellipse cx="-1" cy="-2" rx="3" ry="1" fill="#fff" opacity=".28"/>' +
222 '<ellipse cx="7.2" rx="2.4" ry="2.2" fill="#2b2f2a"/>' +
223 '<path d="M8.5 -1 l2.6 -2 M8.5 1 l2.6 2" stroke="#2b2f2a" stroke-width=".5"/>'
224
225const LADYBUG = (fold: string) =>
226 `<g>${fold}${wing(0, -3.4, 30, 2.6, 7, -1)}${wing(0, 3.4, 30, 2.6, 7, 1)}</g>` +
227 '<ellipse rx="5.2" ry="5" fill="#d9453b" stroke="#6b1d18" stroke-width=".7"/>' +
228 '<path d="M-5 0 H4.6" stroke="#1d1d1d" stroke-width=".7"/>' +
229 '<g fill="#1d1d1d"><circle cx="-2" cy="-2.2" r=".95"/><circle cx="-2" cy="2.2" r=".95"/><circle cx="1.5" cy="-2.8" r=".9"/><circle cx="1.5" cy="2.8" r=".9"/></g>' +
230 '<ellipse cx="5.4" rx="2.2" ry="2.8" fill="#1d1d1d"/>'
231
232type Flyer = {
233 body: (fold: string) => string
234 path: string
235 dur: number
236 begin: number
237 times: number[]
238 points: number[]
239 turns: boolean
240 landed?: [number, number]
241}
242
243const flyer = (o: Flyer) => {
244 const end = o.times[o.times.length - 2]
245 const eps = fine(0.8 / o.dur)
246 const fold = o.landed
247 ? `<animate attributeName="opacity" values="1;1;0;0;1;1" keyTimes="0;${o.landed[0]};${fine(o.landed[0] + 0.004)};${o.landed[1]};${fine(o.landed[1] + 0.004)};1" dur="${o.dur}s" begin="${o.begin}s" repeatCount="indefinite"/>`
248 : ''
249
250 return (
251 '<g opacity="0">' +
252 `<animateMotion path="${o.path}" dur="${o.dur}s" begin="${o.begin}s" repeatCount="indefinite" calcMode="linear" keyPoints="${o.points.join(';')}" keyTimes="${o.times.join(';')}"${o.turns ? ' rotate="auto"' : ''}/>` +
253 `<animate attributeName="opacity" values="0;1;1;0;0" keyTimes="0;${eps};${fine(end - eps)};${end};1" dur="${o.dur}s" begin="${o.begin}s" repeatCount="indefinite"/>` +
254 o.body(fold) +
255 '</g>'
256 )
257}
258
259const VISITORS =
260 flyer({
261 body: BEE,
262 path: 'M118 96 C160 70 205 60 226 72 C240 80 250 50 262 14 C280 10 300 40 296 52 C280 90 215 110 190 100 C160 92 135 92 118 96',
263 dur: 67,
264 begin: 4,
265 times: [0, 0.22, 1],
266 points: [0, 1, 1],
267 turns: true,
268 }) +
269 flyer({
270 body: BUTTERFLY,
271 path: 'M100 150 C140 120 170 160 205 120 C235 80 215 40 245 36 C275 30 285 80 310 70 C335 60 325 130 295 150 C265 170 235 165 200 175 C165 185 130 170 100 150',
272 dur: 83,
273 begin: 12,
274 times: [0, 0.26, 1],
275 points: [0, 1, 1],
276 turns: true,
277 }) +
278 flyer({
279 body: BEETLE,
280 path: 'M110 130 C170 120 240 170 322 264 C240 170 170 120 110 130',
281 dur: 97,
282 begin: 25,
283 times: [0, 0.12, 0.2, 0.32, 1],
284 points: [0, 0.5, 0.5, 1, 1],
285 turns: true,
286 landed: [0.12, 0.2],
287 }) +
288 flyer({
289 body: LADYBUG,
290 path: 'M120 175 C170 190 240 150 298 213 C240 150 170 190 120 175',
291 dur: 113,
292 begin: 40,
293 times: [0, 0.1, 0.17, 0.27, 1],
294 points: [0, 0.5, 0.5, 1, 1],
295 turns: true,
296 landed: [0.1, 0.17],
297 })
298
299// ---- room -------------------------------------------------------------------
300const steam = (x: number, begin: number) =>
301 `<path d="M${x} 232 q-4 -6 0 -12 q4 -6 0 -12" fill="none" stroke="#b9ad98" stroke-width="1.6" stroke-linecap="round" opacity="0">` +
302 `<animate attributeName="opacity" values="0;.7;0" dur="4.5s" begin="${begin}s" repeatCount="indefinite"/>` +
303 `<animateTransform attributeName="transform" type="translate" values="0 0;0 -9" dur="4.5s" begin="${begin}s" repeatCount="indefinite"/></path>`
304
305const ROOM =
306 '<rect width="400" height="258" fill="url(#wall)"/>' +
307 WINDOW +
308 '<rect y="258" width="400" height="16" fill="#b58b61"/><rect y="258" width="400" height="2" fill="#cfa77c"/>' +
309 '<rect y="274" width="400" height="46" fill="#8e6844"/>' +
310 '<path d="M30 262 L165 262 L182 272 L47 272Z" fill="#fff6d8" opacity=".2"/>' +
311 // mug
312 '<rect x="338" y="238" width="28" height="30" rx="4" fill="#f4efe6" stroke="#cfc3ac"/>' +
313 '<path d="M366 245 h5 a6 6 0 0 1 0 14 ht-5" fill="none" stroke="#cfc3ac" stroke-width="3"/>' +
314 '<ellipse cx="352" cy="240" rx="12" ry="2.6" fill="#6b4a33"/>' +
315 steam(347, 0) +
316 steam(357, 2.2) +
317 // pot
318 '<ellipse cx="280" cy="268" rx="46" ry="5" fill="#000" opacity=".14"/>' +
319 PLANTS.map(plantSvg).join('') +
320 '<ellipse cx="275" cy="266" rx="36" ry="5" fill="#b4623f"/>' +
321 '<path d="M246 224 L304 224 L298 264 L252 264Z" fill="#c9714a"/>' +
322 '<path d="M246 224 L254 224 L259 264 L252 264Z" fill="#fff" opacity=".1"/>' +
323 '<rect x="241" y="212" width="68" height="13" rx="3" fill="#d27d55"/>' +
324 '<rect x="241" y="212" width="68" height="3" rx="1.5" fill="#fff" opacity=".18"/>' +
325 VISITORS
326
327// ---- building shell (shared by both halves) ---------------------------------
328const BRICK = '#6b5b52'
329const HALF_W = 400
330const HALF_H = 680
331const D1 = 90 // taller window room
332const T = 70 // taller fish tank
333const D3 = 160 // extra wall above the workshop
334
335const DEFS =
336 '<defs>' +
337 '<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#9ccbea"/><stop offset="1" stop-color="#e4f1f4"/></linearGradient>' +
338 '<linearGradient id="wall" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#efe6d4"/><stop offset="1" stop-color="#ddd1bb"/></linearGradient>' +
339 '<linearGradient id="wall2" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#e9dcc6"/><stop offset="1" stop-color="#d6c6aa"/></linearGradient>' +
340 '<linearGradient id="shop" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#d3dccd"/><stop offset="1" stop-color="#bccab8"/></linearGradient>' +
341 '<linearGradient id="water" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#bfe3ef"/><stop offset="1" stop-color="#7fb9cf"/></linearGradient>' +
342 '<clipPath id="glass"><rect x="30" y="48" width="132" height="176"/></clipPath>' +
343 `<clipPath id="roomclip"><rect x="6" y="24" width="388" height="${290 + D1}"/></clipPath>` +
344 '<clipPath id="screen"><rect x="78" y="223" width="124" height="82"/></clipPath>' +
345 '</defs>'
346
347const frame = (inner: string) =>
348 `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${HALF_W} ${HALF_H}" width="${HALF_W}" height="${HALF_H}">${DEFS}${inner}` +
349 `<rect x="0" y="0" width="6" height="${HALF_H}" fill="${BRICK}"/><rect x="394" y="0" width="6" height="${HALF_H}" fill="${BRICK}"/></svg>`
350
351// ---- upper half: the window room, the cat and the fish tank --------------------
352const WB = 452 + T
353const seaweed = (x: number, ht: number, begin: number) =>
354 `<g><animateTransform attributeName="transform" type="rotate" values="-5 ${x} ${WB};5 ${x} ${WB};-5 ${x} ${WB}" ${keyed([0, 0.5, 1])} dur="${4 + begin}s" begin="${-begin}s" repeatCount="indefinite"/>` +
355 `<path d="M${x} ${WB} q-7 ${-ht / 3} 0 ${-ht / 2} q7 ${-ht / 3} 0 ${-ht / 2}" fill="none" stroke="#4f9a63" stroke-width="3.2" stroke-linecap="round"/></g>`
356
357const bubble = (x: number, r: number, dur: number, begin: number) =>
358 `<circle cx="${x}" cy="${WB - 4}" r="${r}" fill="#fff" fill-opacity=".55" stroke="#fff" stroke-opacity=".8" stroke-width=".6" opacity="0">` +
359 `<animate attributeName="cy" values="${WB - 4};362" dur="${dur}s" begin="${begin}s" repeatCount="indefinite"/>` +
360 `<animate attributeName="opacity" values="0;.9;.9;0" keyTimes="0;.1;.8;1" dur="${dur}s" begin="${begin}s" repeatCount="indefinite"/></circle>`
361
362const fish = (
363 body: string,
364 fin: string,
365 from: [number, number],
366 to: [number, number],
367 lift: number,
368 dur: number,
369 begin: number,
370) => {
371 const out = `C${from[0] + (to[0] - from[0]) * 0.3} ${from[1] - lift} ${from[0] + (to[0] - from[0]) * 0.7} ${to[1] + lift} ${to[0]} ${to[1]}`
372 const back = `C${from[0] + (to[0] - from[0]) * 0.7} ${to[1] + lift} ${from[0] + (to[0] - from[0]) * 0.3} ${from[1] - lift} ${from[0]} ${from[1]}`
373
374 return (
375 '<g>' +
376 `<animateMotion path="M${from[0]} ${from[1]} ${out} ${back}" dur="${dur}s" begin="${-begin}s" repeatCount="indefinite" calcMode="spline" keyPoints="0;.5;1" keyTimes="0;.5;1" keySplines=".4 0 .6 1;.4 0 .6 1"/>` +
377 `<g><animateTransform attributeName="transform" type="scale" values="1 1;-1 1" keyTimes="0;.5" calcMode="discrete" dur="${dur}s" begin="${-begin}s" repeatCount="indefinite"/>` +
378 `<path d="M-8 0 L-17 -6 L-17 6Z" fill="${fin}"><animateTransform attributeName="transform" type="rotate" values="-10 -8 0;10 -8 0;-10 -8 0" dur=".5s" repeatCount="indefinite"/></path>` +
379 `<ellipse rx="10" ry="5.4" fill="${body}"/><path d="M-2 -5 q3 5 0 10" fill="none" stroke="#fff" stroke-opacity=".6" stroke-width="1.6"/>` +
380 '<circle cx="5.5" cy="-1.2" r="1.3" fill="#1d2a30"/></g></g>'
381 )
382}
383
384const CAT =
385 // rug
386 '<ellipse cx="112" cy="494" rx="86" ry="11" fill="#a8574a" opacity=".85"/><ellipse cx="112" cy="494" rx="70" ry="8" fill="none" stroke="#e7c9a0" stroke-width="1.4"/>' +
387 // tail
388 '<g><animateTransform attributeName="transform" type="rotate" values="-5 70 480;6 70 480;-5 70 480" keyTimes="0;.5;1" calcMode="spline" keySplines=".45 0 .55 1;.45 0 .55 1" dur="3.4s" repeatCount="indefinite"/>' +
389 '<path d="M70 482 C42 486 40 506 78 504 C108 502 130 498 140 494" fill="none" stroke="#d9a066" stroke-width="10" stroke-linecap="round"/>' +
390 '<path d="M52 497 q-6 4 -1 8" fill="none" stroke="#b9824a" stroke-width="3" stroke-linecap="round"/></g>' +
391 // body, breathing
392 '<g transform="translate(112 486)"><g><animateTransform attributeName="transform" type="scale" values="1 1;1 1.045;1 1" keyTimes="0;.5;1" calcMode="spline" keySplines=".45 0 .55 1;.45 0 .55 1" dur="3.2s" repeatCount="indefinite"/>' +
393 '<g transform="translate(-112 -486)"><g transform="translate(0 -8)"><ellipse cx="112" cy="480" rx="48" ry="24" fill="#d9a066"/>' +
394 '<path d="M92 460 q4 14 -2 28 M112 457 q4 16 0 32 M132 460 q3 14 -1 27" fill="none" stroke="#b9824a" stroke-width="3.4" stroke-linecap="round"/></g></g></g></g>' +
395 // head
396 '<g transform="translate(0 -4)"><ellipse cx="156" cy="480" rx="17" ry="14" fill="#d9a066"/>' +
397 '<path d="M143 470 L146 456 L154 466Z M169 470 L166 456 L158 466Z" fill="#d9a066"/><path d="M146 468 L147 460 L151 466Z M166 468 L165 460 L161 466Z" fill="#f0b9a0"/>' +
398 '<path d="M148 480 q4 3 8 0 M158 480 q4 3 8 0" fill="none" stroke="#3b2a22" stroke-width="1.5" stroke-linecap="round"/>' +
399 '<path d="M154 485 l3 3 l3 -3Z" fill="#e08a8a"/></g>' +
400 // sleeping z
401 '<g font-family="sans-serif" font-size="13" fill="#6f7f93">' +
402 [0, 1.6, 3.2]
403 .map(
404 b =>
405 `<text x="178" y="458" opacity="0">z<animate attributeName="opacity" values="0;.85;0" dur="4.8s" begin="${b}s" repeatCount="indefinite"/>` +
406 `<animateTransform attributeName="transform" type="translate" values="0 0;8 -16" dur="4.8s" begin="${b}s" repeatCount="indefinite"/></text>`,
407 )
408 .join('') +
409 '</g>'
410
411const TANK =
412 // stand
413 `<rect x="200" y="${476 + T}" width="172" height="8" rx="2" fill="#8e6844"/><rect x="208" y="${484 + T}" width="6" height="36" fill="#7a5a3c"/><rect x="358" y="${484 + T}" width="6" height="36" fill="#7a5a3c"/>` +
414 // water
415 `<rect x="206" y="350" width="160" height="${124 + T}" rx="4" fill="url(#water)"/>` +
416 `<rect x="206" y="${454 + T}" width="160" height="20" fill="#e6d3a3"/><ellipse cx="250" cy="${456 + T}" rx="14" ry="5" fill="#b9a985"/><ellipse cx="330" cy="${458 + T}" rx="10" ry="4" fill="#a9997a"/>` +
417 seaweed(228, 104, 0) +
418 seaweed(236, 72, 1.4) +
419 seaweed(344, 116, 0.7) +
420 seaweed(352, 80, 2.1) +
421 fish('#f08a3c', '#f6b46e', [238, 396], [334, 430], 12, 15, 0) +
422 fish('#4f8fd0', '#8dbbe6', [334, 478], [250, 498], 10, 11, 4) +
423 fish('#f0d04a', '#f6e486', [250, 372], [338, 462], 14, 19, 9) +
424 bubble(232, 2.4, 6.6, 0) +
425 bubble(236, 1.6, 8.4, 1.7) +
426 bubble(230, 2, 7.5, 3.1) +
427 // glass and lid
428 `<rect x="206" y="350" width="160" height="${124 + T}" rx="4" fill="none" stroke="#9fb7bd" stroke-width="3"/><rect x="202" y="344" width="168" height="7" rx="2" fill="#4a5058"/>` +
429 `<path d="M214 358 v${50 + T * 0.6}" stroke="#fff" stroke-opacity=".5" stroke-width="3" stroke-linecap="round"/>`
430
431const TOP =
432 frame(
433 '<rect width="400" height="24" fill="#7a5f55"/><rect y="20" width="400" height="4" fill="#5a463e"/>' +
434 '<rect x="28" y="0" width="22" height="16" fill="#5a463e"/>' +
435 `<rect x="6" y="24" width="388" height="${290 + D1}" fill="#efe6d4"/>` +
436 `<g clip-path="url(#roomclip)"><g transform="translate(0 ${24 + D1 - 30})">${ROOM}</g></g>` +
437 `<rect x="0" y="${314 + D1}" width="400" height="8" fill="${BRICK}"/>` +
438 `<g transform="translate(0 ${D1})">` +
439 `<rect x="6" y="322" width="388" height="${198 + T}" fill="url(#wall2)"/>` +
440 `<rect x="6" y="${470 + T}" width="388" height="50" fill="#b79a78"/><rect x="6" y="${466 + T}" width="388" height="5" fill="#efe6d4"/>` +
441 `<path d="M6 ${494 + T} H394 M6 ${518 + T} H394" stroke="#a38865" stroke-opacity=".6"/>` +
442 `<g transform="translate(0 ${T})">${CAT}</g>` +
443 TANK +
444 '</g>',
445 )
446
447// ---- lower half: Claude's workshop, driven by what Claude is doing --------------------
448type Look = { working: boolean; prev: boolean; done: number; fresh: boolean }
449
450const GARDEN_SLOTS = 12
451const BLOOM_KINDS = ['cosmos', 'daisy', 'poppy', 'violet', 'tulip'] as const
452
453const bloomsOf = (done: number) => (done === 0 ? 0 : ((done - 1) % GARDEN_SLOTS) + 1)
454
455const gardenSlot = (i: number, blooms: number, fresh: boolean) => {
456 const sx = 41 + 29.8 * i
457 const baseY = 458
458 if (i > blooms) return ''
459 if (i === blooms) {
460 return `<g transform="translate(${n(sx)} ${baseY})"><path d="M0 0 q-6 -10 -13 -8 q4 8 13 8Z M0 0 q6 -10 13 -8 q-4 8 -13 8Z" fill="#5f9457"/></g>`
461 }
462 const ht = 38 + ((i * 13) % 5) * 7
463 const lean = ((i * 7) % 5) - 2
464 const hx = sx + lean * 2
465 const hy = baseY - ht
466 const popped = fresh && i === blooms - 1
467 const kind = BLOOM_KINDS[i % BLOOM_KINDS.length]
468 const head = popped
469 ? `<g><animateTransform attributeName="transform" type="scale" values="0;1.25;1" keyTimes="0;.6;1" dur="1.4s" begin="0s" fill="freeze"/>${FLOWERS[kind]()}</g>`
470 : FLOWERS[kind]()
471
472 return (
473 `<g>${rotateAbout([-2.2, 2.2, -2.2], sx, baseY, 4 + (i % 4) * 0.7, -i * 0.6, [0, 0.5, 1])}` +
474 `<path d="M${n(sx)} ${baseY} Q${n(sx + lean * 3)} ${baseY - ht / 2} ${n(hx)} ${n(hy)}" fill="none" stroke="#4f7d4a" stroke-width="2.2" stroke-linecap="round"/>` +
475 `<path d="M0 0 C7 -9 18 -9 25 -2 C16 5 7 5 0 0Z" fill="#5f9457" transform="translate(${n(sx + lean * 1.4)} ${baseY - ht * 0.45}) rotate(-22) scale(.5)"/>` +
476 `<g transform="translate(${n(hx)} ${n(hy)}) scale(.7)">${head}</g></g>`
477 )
478}
479
480const CODE_COLORS = ['#7fb4d8', '#e0b36a', '#8fc08a', '#c58fd0', '#cfd6dc', '#7fb4d8', '#e0b36a']
481const CODE_WIDTHS = [74, 52, 96, 38, 84, 62, 46]
482const codeLines = (opacity: number) =>
483 Array.from(
484 { length: 14 },
485 (_, i) =>
486 `<rect x="${86 + (i % 3 === 1 ? 10 : 0)}" y="${229 + i * 8}" width="${CODE_WIDTHS[i % 7]}" height="4" rx="2" fill="${CODE_COLORS[i % 7]}" opacity="${opacity}"/>`,
487 ).join('')
488
489const sparkle = (x: number, y: number, begin: number) =>
490 `<path d="M${x} ${y - 6} L${x + 1.8} ${y - 1.8} L${x + 6} ${y} L${x + 1.8} ${y + 1.8} L${x} ${y + 6} L${x - 1.8} ${y + 1.8} L${x - 6} ${y} L${x - 1.8} ${y - 1.8}Z" fill="#ffd96a" opacity="0">` +
491 `<animate attributeName="opacity" values="0;1;0" dur="1.6s" begin="${begin}s" repeatCount="indefinite"/></path>`
492
493const workshop = (w: Look, bubble: { work: string; cheer: string; rest: string }) => {
494 const blooms = bloomsOf(w.done)
495 const mood = w.working ? 'work' : w.fresh ? 'cheer' : 'rest'
496 const lit = mood !== 'rest'
497 const dimFrom = w.prev ? 0 : 0.4
498 const dimTo = lit ? 0 : 0.4
499 const glowFrom = w.prev ? 1 : 0
500 const glowTo = lit ? 1 : 0
501
502 const label = bubble[mood]
503 const pillW = mood === 'cheer' ? 196 : 126
504 const dot = { work: '#5fae6b', cheer: '#e0b050', rest: '#9aa3a8' }[mood]
505
506 const eyes = {
507 work:
508 '<ellipse cx="282" cy="262" rx="3.2" ry="3.2" fill="#2b2623"><animate attributeName="ry" values="3.2;3.2;.4;3.2" keyTimes="0;.9;.95;1" dur="4s" repeatCount="indefinite"/></ellipse>' +
509 '<ellipse cx="302" cy="262" rx="3.2" ry="3.2" fill="#2b2623"><animate attributeName="ry" values="3.2;3.2;.4;3.2" keyTimes="0;.9;.95;1" dur="4s" repeatCount="indefinite"/></ellipse>' +
510 '<path d="M287 275 h10" stroke="#2b2623" stroke-width="2" stroke-linecap="round"/>',
511 cheer:
512 '<path d="M277 265 q5 -6 10 0 M297 265 q5 -6 10 0" fill="none" stroke="#2b2623" stroke-width="2.4" stroke-linecap="round"/>' +
513 '<path d="M283 274 q9 9 18 0Z" fill="#7a2f22"/>',
514 rest:
515 '<path d="M277 262 q5 4 10 0 M297 262 q5 4 10 0" fill="none" stroke="#2b2623" stroke-width="2.2" stroke-linecap="round"/>' +
516 '<ellipse cx="292" cy="276" rx="2.4" ry="2" fill="#7a2f22"/>',
517 }[mood]
518
519 const bulb = mood === 'rest' ? '#9aa3a8' : '#ffd27a'
520 const bulbPulse =
521 mood === 'work'
522 ? '<animate attributeName="r" values="5;7;5" dur="1.2s" repeatCount="indefinite"/>'
523 : ''
524
525 const arms = {
526 work:
527 '<g transform="translate(262 318)"><animateTransform attributeName="transform" type="rotate" values="0 0 0;7 0 0;0 0 0" dur=".35s" repeatCount="indefinite" additive="sum"/>' +
528 '<path d="M0 0 L-42 22" stroke="#d9784a" stroke-width="9" stroke-linecap="round"/><circle cx="-42" cy="22" r="6" fill="#e58a5c"/></g>' +
529 '<g transform="translate(322 320)"><path d="M0 0 L12 24" stroke="#d9784a" stroke-width="9" stroke-linecap="round"/></g>',
530 cheer:
531 '<g transform="translate(262 318)"><animateTransform attributeName="transform" type="rotate" values="0 0 0;-9 0 0;0 0 0" dur=".6s" repeatCount="indefinite" additive="sum"/>' +
532 '<path d="M0 0 L-26 -40" stroke="#d9784a" stroke-width="9" stroke-linecap="round"/><circle cx="-26" cy="-40" r="6" fill="#e58a5c"/></g>' +
533 '<g transform="translate(322 320)"><animateTransform attributeName="transform" type="rotate" values="0 0 0;9 0 0;0 0 0" dur=".6s" repeatCount="indefinite" additive="sum"/>' +
534 '<path d="M0 0 L26 -40" stroke="#d9784a" stroke-width="9" stroke-linecap="round"/><circle cx="26" cy="-40" r="6" fill="#e58a5c"/></g>',
535 rest:
536 '<g transform="translate(262 318)"><path d="M0 0 L-12 26" stroke="#d9784a" stroke-width="9" stroke-linecap="round"/></g>' +
537 '<g transform="translate(322 320)"><path d="M0 0 L12 24" stroke="#d9784a" stroke-width="9" stroke-linecap="round"/></g>',
538 }[mood]
539
540 const zzz =
541 mood === 'rest'
542 ? '<g font-family="sans-serif" font-size="13" fill="#51606e">' +
543 [0, 1.6, 3.2]
544 .map(
545 b =>
546 `<text x="322" y="246" opacity="0">z<animate attributeName="opacity" values="0;.85;0" dur="4.8s" begin="${b}s" repeatCount="indefinite"/>` +
547 `<animateTransform attributeName="transform" type="translate" values="0 0;8 -16" dur="4.8s" begin="${b}s" repeatCount="indefinite"/></text>`,
548 )
549 .join('') +
550 '</g>'
551 : ''
552
553 const sparkles = mood === 'cheer' ? sparkle(250, 232, 0) + sparkle(334, 226, 0.5) + sparkle(318, 196, 1) + sparkle(262, 200, 0.3) : ''
554
555 const screen =
556 mood === 'rest'
557 ? '<rect x="78" y="223" width="124" height="82" fill="#1b232b"/>' +
558 `<g clip-path="url(#screen)">${codeLines(0.22)}</g>` +
559 '<rect x="88" y="289" width="6" height="10" fill="#9fc3d6"><animate attributeName="opacity" values="1;1;0;0" keyTimes="0;.5;.5;1" dur="1.2s" repeatCount="indefinite"/></rect>'
560 : '<rect x="78" y="223" width="124" height="82" fill="#18252e"/>' +
561 `<g clip-path="url(#screen)"><g>${codeLines(0.95)}<animateTransform attributeName="transform" type="translate" values="0 0;0 -56" dur="${mood === 'work' ? 2.8 : 9}s" repeatCount="indefinite"/></g></g>`
562
563 const keys =
564 mood === 'work'
565 ? Array.from(
566 { length: 6 },
567 (_, i) =>
568 `<rect x="${134 + i * 16}" y="337" width="10" height="4" rx="1" fill="#fff" opacity="0"><animate attributeName="opacity" values="0;.9;0" dur=".5s" begin="${(i * 0.37) % 1.1}s" repeatCount="indefinite"/></rect>`,
569 ).join('')
570 : ''
571
572 const beam =
573 '<g>' +
574 `<animate attributeName="opacity" values="${glowFrom};${glowTo}" dur="1.2s" fill="freeze"/>` +
575 '<path d="M346 272 L262 340 L350 340Z" fill="#fff0b8" opacity=".32"/><circle cx="347" cy="272" r="13" fill="#fff3b0" opacity=".7"/></g>'
576
577 const pill =
578 `<rect x="${200 - pillW / 2}" y="22" width="${pillW}" height="28" rx="14" fill="#fff" opacity=".82"/>` +
579 `<circle cx="${200 - pillW / 2 + 18}" cy="36" r="5" fill="${dot}">${mood === 'work' ? '<animate attributeName="r" values="4;6;4" dur="1.2s" repeatCount="indefinite"/>' : ''}</circle>` +
580 `<text x="${200 - pillW / 2 + 32}" y="41" font-family="sans-serif" font-size="14" fill="#33404a">${label}</text>`
581
582 // string lights along the extra wall: lit while Claude works or has just finished
583 const WIRE = [
584 [6, 62, 103, 122, 200, 72],
585 [200, 72, 297, 122, 394, 62],
586 ]
587 const BULB_COLORS = ['#f26d6d', '#f2b84b', '#7bc77e', '#6bb5e8', '#c58fe0']
588 const bulbs = WIRE.flatMap(([x0, y0, cx, cy, x1, y1], arc) =>
589 [0.2, 0.45, 0.7, 0.92].map((t, i) => {
590 const x = (1 - t) ** 2 * x0 + 2 * (1 - t) * t * cx + t ** 2 * x1
591 const y = (1 - t) ** 2 * y0 + 2 * (1 - t) * t * cy + t ** 2 * y1 + 5
592 const c = BULB_COLORS[(arc * 4 + i) % BULB_COLORS.length]
593 const twinkle = mood === 'work' ? `<animate attributeName="opacity" values="1;.55;1" dur="${1.4 + (i % 3) * 0.4}s" begin="${i * 0.3}s" repeatCount="indefinite"/>` : ''
594
595 return (
596 `<circle cx="${n(x)}" cy="${n(y)}" r="10" fill="${c}" opacity="${lit ? 0.28 : 0}"/>` +
597 `<circle cx="${n(x)}" cy="${n(y)}" r="4.2" fill="${c}" opacity="${lit ? 1 : 0.45}">${twinkle}</circle>`
598 )
599 }),
600 ).join('')
601 const lights = `<path d="M6 62 Q103 122 200 72 Q297 122 394 62" fill="none" stroke="#6a6a62" stroke-width="1.4"/>${bulbs}`
602
603 const NOTES: [number, number, number, string][] = [
604 [146, 126, -5, '#f6e27a'],
605 [186, 122, 3, '#f4a6a6'],
606 [226, 128, -3, '#a6d8f0'],
607 [160, 166, 4, '#b8e0a0'],
608 [212, 168, -4, '#f6c88a'],
609 ]
610 const board =
611 '<rect x="132" y="108" width="136" height="112" rx="4" fill="#8e6844"/><rect x="138" y="114" width="124" height="100" fill="#c9a06a"/>' +
612 NOTES.map(
613 ([x, y, rot, c]) =>
614 `<g transform="translate(${x} ${y}) rotate(${rot})"><rect width="32" height="30" fill="${c}"/>` +
615 '<path d="M5 10 h20 M5 16 h16 M5 22 h18" stroke="#000" stroke-opacity=".28" stroke-width="1.6"/><circle cx="16" cy="3" r="2.4" fill="#c4423a"/></g>',
616 ).join('')
617
618 return frame(
619 `<g transform="translate(0 ${D3})">` +
620 `<rect x="6" y="${8 - D3}" width="388" height="${396 + D3}" fill="url(#shop)"/>` +
621 `<g transform="translate(0 ${-D3})">${lights}${board}</g>` +
622 // shelf with books and a small plant
623 '<rect x="24" y="152" width="126" height="6" rx="1" fill="#8e6844"/>' +
624 '<rect x="30" y="120" width="12" height="32" fill="#c4675a"/><rect x="44" y="126" width="10" height="26" fill="#5b86a6"/><rect x="56" y="116" width="13" height="36" fill="#d6b25a"/><rect x="71" y="128" width="9" height="24" fill="#6a9a73"/>' +
625 '<rect x="108" y="136" width="26" height="16" rx="2" fill="#c9714a"/><path d="M121 136 q-8 -10 -2 -16 M121 136 q0 -14 6 -18 M121 136 q8 -8 12 -12" fill="none" stroke="#5f9457" stroke-width="3" stroke-linecap="round"/>' +
626 // wall clock
627 '<circle cx="330" cy="118" r="26" fill="#fbf8f0" stroke="#8b6b4a" stroke-width="3"/>' +
628 '<path d="M330 96 v4 M330 136 v4 M308 118 h4 M348 118 h4" stroke="#8b6b4a" stroke-width="2"/>' +
629 '<path d="M330 118 v-14" stroke="#2f3338" stroke-width="2.6" stroke-linecap="round"><animateTransform attributeName="transform" type="rotate" values="0 330 118;360 330 118" dur="43200s" repeatCount="indefinite"/></path>' +
630 '<path d="M330 118 v-19" stroke="#2f3338" stroke-width="1.8" stroke-linecap="round"><animateTransform attributeName="transform" type="rotate" values="0 330 118;360 330 118" dur="3600s" repeatCount="indefinite"/></path>' +
631 '<path d="M330 118 v-21" stroke="#c4675a" stroke-width="1" stroke-linecap="round"><animateTransform attributeName="transform" type="rotate" values="0 330 118;360 330 118" dur="60s" repeatCount="indefinite"/></path>' +
632 // character, behind the desk
633 '<ellipse cx="292" cy="330" rx="38" ry="32" fill="#d9784a"/>' +
634 '<circle cx="292" cy="266" r="31" fill="#e58a5c"/>' +
635 '<path d="M292 235 V222" stroke="#8a5a3c" stroke-width="3" stroke-linecap="round"/>' +
636 `<circle cx="292" cy="217" r="5" fill="${bulb}">${bulbPulse}</circle>` +
637 '<circle cx="272" cy="272" r="4.5" fill="#f0a98a" opacity=".8"/><circle cx="312" cy="272" r="4.5" fill="#f0a98a" opacity=".8"/>' +
638 eyes +
639 zzz +
640 sparkles +
641 // desk
642 '<rect x="6" y="330" width="388" height="14" fill="#b58b61"/><rect x="6" y="330" width="388" height="2" fill="#cfa77c"/>' +
643 '<rect x="6" y="344" width="388" height="60" fill="#8e6844"/><path d="M110 352 v44 M290 352 v44" stroke="#7a5a3c" stroke-width="2"/>' +
644 // monitor
645 '<rect x="128" y="318" width="24" height="12" fill="#3a3f45"/><rect x="112" y="326" width="56" height="6" rx="2" fill="#3a3f45"/>' +
646 '<rect x="70" y="215" width="140" height="98" rx="6" fill="#2f3338"/>' +
647 screen +
648 // keyboard, mug
649 '<rect x="128" y="335" width="106" height="9" rx="2" fill="#4a5058"/>' +
650 keys +
651 '<rect x="244" y="318" width="22" height="22" rx="3" fill="#f4efe6" stroke="#cfc3ac"/><path d="M266 324 h4 a5 5 0 0 1 0 10 ht-4" fill="none" stroke="#cfc3ac" stroke-width="2.4"/>' +
652 // lamp
653 '<ellipse cx="368" cy="332" rx="17" ry="4" fill="#4a4f55"/><path d="M368 330 V294 L350 270" fill="none" stroke="#4a4f55" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>' +
654 '<path d="M336 276 L354 258 L366 270 L348 286Z" fill="#e8b84a"/>' +
655 arms +
656 // floor and planter
657 '<rect x="6" y="404" width="388" height="116" fill="#b79a78"/><rect x="6" y="400" width="388" height="6" fill="#efe6d4"/>' +
658 '<path d="M6 430 H394 M6 462 H394 M6 494 H394" stroke="#a38865" stroke-opacity=".55"/>' +
659 '<rect x="22" y="456" width="356" height="46" rx="4" fill="#8b5e3c"/><rect x="22" y="456" width="356" height="8" fill="#4a3426"/><rect x="22" y="470" width="356" height="2" fill="#fff" opacity=".1"/>' +
660 // idle dusk, fading between states
661 `<rect x="6" y="${8 - D3}" width="388" height="${512 + D3}" fill="#16213a" opacity="${dimTo}"><animate attributeName="opacity" values="${dimFrom};${dimTo}" dur="1.2s" fill="freeze"/></rect>` +
662 beam +
663 Array.from({ length: GARDEN_SLOTS }, (_, i) => gardenSlot(i, blooms, w.fresh && w.done > 0)).join('') +
664 '</g>' +
665 `<rect x="0" y="0" width="400" height="8" fill="${BRICK}"/>` +
666 pill,
667 )
668}
669
670const scene = { top: TOP, work: workshop }
671// ---scene-end
672
673const TERMINAL_ART = [
674 ' ✿',
675 ' ❀ │ ❁',
676 ' ╲ │ ╱ ✾',
677 ' ╲│ ╱',
678 ' ╭─┴─┴─╮',
679 ' ╰╮ ╭╯',
680 ' ╰───╯',
681]
682
683const glyphColor = (ch: string) =>
684 ({ '✿': '#f3d35a', '❀': '#e58fa6', '❁': '#e2735a', '✾': '#9b7fd1' })[ch] ??
685 ('╭╮╯╰─┴'.includes(ch) ? '#c9714a' : '#5f9457')
686
687const work = atom(
688 { plugin: 'desk-garden', key: 'work' } as const,
689 { working: false, prev: false, done: 0, fresh: false, tools: 0 } as Work,
690)
691
692export const register: Register = (on, options) => {
693 // `language` is a field of the plugin's userConfig: 'en' (default) or 'ko'
694 const T: Words = STRINGS[options.language === 'ko' ? 'ko' : 'en']
695
696 on('session.start', async ($, e, next) => {
697 await $.command.register({
698 name: 'garden',
699 description: 'Open the desk garden pane',
700 })
701
702 return next(e)
703 })
704
705 on('command.run', { command: 'garden' }, async $ => {
706 await $.ui.open({ id: PANE, title: T.title })
707
708 return { text: T.opened }
709 })
710
711 // The main loop's turns only: a subagent's run is not Claude "at the desk".
712 on('turn.start', async ($, e, next) => {
713 if (e.agentId === undefined) {
714 await update($, work, w => ({ ...w, prev: w.working || w.fresh, working: true, fresh: false }))
715 }
716
717 return next(e)
718 }).catch(($, e, next) => next(e))
719
720 on('turn.complete', async ($, e, next) => {
721 if (e.agentId === undefined) {
722 const isAnswer = e.reason === 'answer'
723 await update($, work, w => ({
724 ...w,
725 prev: w.working,
726 working: false,
727 fresh: isAnswer,
728 done: w.done + (isAnswer ? 1 : 0),
729 }))
730 }
731
732 return next(e)
733 }).catch(($, e, next) => next(e))
734
735 // Counted as text beneath the pictures: the pictures themselves are left alone.
736 on('tool.call', async ($, e, next) => {
737 await update($, work, w => ({ ...w, tools: w.tools + 1 }))
738
739 return next(e)
740 }).catch(($, e, next) => next(e))
741
742 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
743 const w = await read($, work)
744 const els = $.ui.resolve(e)
745 const cols = e.props?.bodyColumns ?? 40
746 const rows = e.props?.scroll?.bodyRows ?? 20
747 const line = T.line(bloomsOf(w.done), GARDEN_SLOTS, w.done, w.tools)
748 // The pane's width in px: its cells over the code font's advance (about 7.6 px).
749 const width = Math.max(160, Math.round(cols * 7.6))
750 const height = Math.round((width * HALF_H) / HALF_W)
751
752 if (e.surface !== 'terminal') {
753 const { Box, Text, Svg } = els as ElementTable<'desktop'>
754
755 return (
756 <Box flexDirection="column" justifyContent="flex-end" minHeight={rows}>
757 <Box justifyContent="center">
758 <Text dimColor>{line}</Text>
759 </Box>
760 <Svg source={scene.top} alt={T.alt} width={width} height={height} isInteractive />
761 <Svg source={scene.work(w, T.bubble)} alt={T.altWork} width={width} height={height} isInteractive />
762 </Box>
763 )
764 }
765
766 const { Box, Text } = els as ElementTable<'terminal'>
767
768 return (
769 <Box flexDirection="column">
770 {TERMINAL_ART.map(row => (
771 <Box>
772 {[...row].map(ch => (
773 <Text color={glyphColor(ch)}>{ch}</Text>
774 ))}
775 </Box>
776 ))}
777 <Text>{w.working ? T.on : T.off}</Text>
778 <Text dimColor>{line}</Text>
779 <Text dimColor>{T.note}</Text>
780 </Box>
781 )
782 })
783}
784types/index.d.ts 14 lines1export type Work = {
2 working: boolean
3 prev: boolean
4 done: number
5 fresh: boolean
6 tools: number
7}
8
9declare module 'claude-code' {
10 interface PluginState {
11 'desk-garden': { work: Work }
12 }
13}
14