SLOPSHOPPER

jungle-falls

A tall jungle pane: terraced waterfalls wind down into a lily pool, and animals drop by at random - toucans, parrots, a swinging monkey, a jaguar, a crocodile…

newpaneguardcommandtimer
v0.2.0MITupdated 2026-10-24Gnosis333/mod_gallery/mods/jungle-falls
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · jungle-falls
│ ┃ Jungle ✕ › fix the failing auth test and add an audit log call │ ┃ ♣ ~~~ ┃┃ ~~~ ♣ │ ┃ ▟▙ ┃┃┃ ▟▙ ⏺ Read(src/auth.ts) │ ┃ ♣ ▐ ┃┃┃ ▌ ♣ ⎿ Read 6 lines │ ┃ ▟▙ ┃┃┃┃ ▟▙ ⏺ Update(src/auth.ts) │ ┃ ≈≈≈≈≈≈ ◎ ◎ ≈≈≈≈≈≈ ⎿ Added 2 lines, removed 1 line │ ┃ Idle ⏺ Bash(bun test) │ ┃ The animation shows in the desktop app. ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /falls │ ⎿ jungle-falls: Opened the jungle pane. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Jungle
♣ ~~~ ┃┃ ~~~ ♣ ▟▙ ┃┃┃ ▟▙ ♣ ▐ ┃┃┃ ▌ ♣ ▟▙ ┃┃┃┃ ▟▙ ≈≈≈≈≈≈ ◎ ◎ ≈≈≈≈≈≈ Idle The animation shows in the desktop app.
README

Claude Code Mod Gallery

<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).

한국어 설명은 아래에 있습니다.

Requirements

  • Claude Code 2.1.287 or later. Mods are an early-access feature, so the API may change between versions.
  • The animations show in the desktop app. In a terminal you get a still text picture instead.

Install

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.

Language

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.

Mods

ModCommandWhat it is
night-train/trainA night-train window
beach-day/beachA top-down beach
snow-cabin/snowA snowy cabin, day and night
deep-sub/deepA deep-sea submersible
campfire/fireA campfire to stare at
jungle-falls/fallsA jungle waterfall with 24 kinds of animals
rain-window/rainA rainy window
moon-view/moonThe universe seen from the Moon
tower-cut/towerA building cross-section
desk-garden/gardenA desk plant and a workshop
reef-tank/reefAn aquarium
usage-bandautomaticContext, limits and cost on one line
haiku-warningautomaticA warning while a Haiku model is selected

Panels

night-train

<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.

beach-day

<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.

snow-cabin

<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.

deep-sub

<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.

campfire

<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.

jungle-falls

<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.

rain-window

<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.

moon-view

<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.

tower-cut

<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.

desk-garden

<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.

reef-tank

<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.


Prompt bands

usage-band

<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.

haiku-warning

<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.


Good to know

  • CPU: moving pictures redraw constantly, so several panels open at once add up. Open only what you use. They don't affect model response speed, only drawing, heat and battery. jungle-falls, snow-cabin and campfire are larger pictures and relatively heavier.
  • Layout: each mod lives in 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.
  • Languages: English (default) and Korean, chosen per mod with the 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.

License

MIT. Use, change and share freely.


한국어

<img src="images/hero-ko.gif" alt="왼쪽 Claude Code가 일하는 동안 오른쪽 어항 패널이 어두워지고 잠수함이 내려오는 모습" width="720">

Claude Code에 일을 맡기고, 일하는 동안엔 어항 보며 멍~ (왼쪽 대화창은 비슷하게 그린 화면이고, 어항은 실제 모드입니다.)

Claude Code 옆에 띄워 두는 그림 패널과 입력창 위 상태 띠를 모았습니다. 패널은 Claude가 쉬고 있을 때와 일하고 있을 때 모습이 달라집니다. Claude Code의 모드를 공부하면서 이것저것 만들어 본 결과물입니다. 위 영어 부분의 그림(왼쪽 쉬는 중, 오른쪽 작업 중)이 그대로 해당합니다.

요구 사항과 설치

  • Claude Code 2.1.287 이상(모드는 얼리 액세스 기능이라 버전마다 API가 바뀔 수 있습니다)
  • 그림 움직임은 데스크톱 앱에서 보입니다. 터미널에서는 글자 그림으로 나옵니다.
/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을 쓰세요.

모드 설명

  • night-train (/train): 야간열차 창가. 쉴 때는 STATION에 정차해 가로등이 켜져 있고, 작업 중에는 산·나무·전신주가 서로 다른 속도로 흘러가며 가끔 긴 터널을 지납니다. 하늘에는 철새떼가 날아가고, 턴이 끝나면 다시 역으로 들어옵니다.
  • beach-day (/beach): 위에서 내려다본 해변. 파라솔 아래 썬베드에 선글라스를 낀 사람들이 누워 있고, 바다에는 핑크 유니콘 튜브를 비롯한 여러 튜브가 떠 있습니다. 작업 중에는 제트스키, 바나나보트, 꽃게가 나옵니다.
  • snow-cabin (/snow): 눈이 소복소복 내리는 흰 숲과 설산, 통나무 오두막. 쉴 때는 낮, 작업 중에는 밤이 되어 창에 불이 켜지고 눈사람이 15초마다 하나씩 생깁니다. 흰 토끼, 흰 여우, 까치, 허스키가 찾아옵니다.
  • deep-sub (/deep): 어둠 속 노란 잠수정과 야광 해파리, 아귀, 덤보 문어, 풍선장어, 그리고 저 아래 희미하게 움직이는 정체불명의 거대한 것. 작업 중에는 탐조등과 소나가 켜지고 가끔 거대 오징어 그림자가 스칩니다.
  • campfire (/fire): 별이 가득한 밤 숲속 공터의 모닥불. 불꽃과 불티, 통나무 원, 꽃 여섯 종류, 여우·올빼미(머리를 돌립니다)·사슴·너구리·고슴도치. 작업 중에는 불이 커지고 끝나면 잦아듭니다.
  • jungle-falls (/falls): 계단식 폭포와 연꽃 연못, 동물 24종이 무작위로 방문. 쉴 때는 낮, 작업 중에는 밤. CPU를 가장 많이 씁니다.
  • rain-window (/rain): 비 오는 창가. 작업 중에는 폭풍, 번개가 치면 고양이가 눈을 뜨고 스탠드가 깜빡이며, 끝나면 무지개가 뜹니다.
  • moon-view (/moon): 달에서 본 지구. 통통 뛰는 우주인(작업 중에는 빙빙 돕니다), 무작위로 지나가는 우주 물체와 스타워즈풍 우주선.
  • tower-cut (/tower): 빌딩 단면. 턴이 끝날 때마다 한 층씩 올라가고 20층이면 준공 불꽃놀이.
  • desk-garden (/garden): 창가 화분(턴마다 꽃이 핍니다), 졸고 있는 고양이와 수조, Claude가 일하는 작업실.
  • reef-tank (/reef): 수족관. 작업 중에는 조명이 어두워지고 노란 잠수함이 내려오며, 턴이 끝날 때마다 진주가 쌓입니다.
  • usage-band: 입력창 위 한 줄로 컨텍스트, 5시간·7일 한도(초기화 시각 ↻ 포함), 비용을 보여줍니다.
  • haiku-warning: Haiku 모델이 선택돼 있으면 입력창 위에 빨간 경고를 띄웁니다.

알아 둘 것

  • 움직이는 그림은 화면을 계속 그리므로 패널을 여러 개 동시에 열면 CPU 부담이 합쳐집니다. 쓰는 것만 여세요. 모델 응답 속도와는 무관하고, 화면 그리기와 발열·배터리에만 영향이 있습니다. jungle-falls, snow-cabin, campfire는 그림이 커서 상대적으로 무겁습니다.
  • 글자(상태 줄, 그림 설명, 열기 답장)는 기본이 영어이고, language 옵션을 ko로 하면 한글입니다. 그림 속 글자는 STATION과 desk-garden의 말풍선 정도입니다(말풍선은 옵션을 따릅니다).
  • 라이선스는 MIT입니다. 누구나 자유롭게 쓰고, 고치고, 다시 배포해도 됩니다.

모두 Claude Code로 만들었습니다.

Source 2 files
hooks/register.tsx 1024 lines
1import { atom, read, update } from 'claude-code'
2import type { ElementTable, Register } from 'claude-code'
3
4import type { Falls } from '../types'
5
6const PANE = 'jungle-falls'
7const STRINGS = {
8  en: {
9    title: 'Jungle',
10    alt: 'A terraced waterfall winds down through the jungle into a lily pool, and animals drop by at random.',
11    working: (clock: string, tools: number) => `Working ${clock} · tools ${tools}`,
12    idle: 'Idle',
13    opened: 'Opened the jungle pane.',
14    note: 'The animation shows in the desktop app.',
15  },
16  ko: {
17    title: '정글',
18    alt: '정글 속 계단식 폭포가 굽이쳐 내려와 연꽃 연못으로 떨어지고, 동물들이 무작위로 찾아옵니다.',
19    working: (clock: string, tools: number) => `작업 중 ${clock} · 도구 ${tools}회`,
20    idle: '쉬는 중',
21    opened: '정글 폭포를 열었습니다.',
22    note: '그림 움직임은 데스크톱 앱에서 보입니다.',
23  },
24}
25
26type Words = (typeof STRINGS)['en']
27
28const KINDS = 24 // kinds of animal; 13 to 17 swim
29const SLOTS = 1 // each kind visits at its own times
30const HORIZON = 800 // seconds of visits laid out in each picture
31const isUnder = (kind: number) => kind >= 13 && kind <= 17 // these swim under the pool's lily pads
32const LIFE = 14 // seconds a visitor stays
33const LIMB_KINDS = [2, 3, 7, 9, 10, 19, 20, 21, 22, 23] // these use a limb; two never share one at once
34
35// ---scene-start
36type Pt = [number, number]
37type Slot = { id: number; kind: number; seed: number }
38
39const W = 400
40const H = 1660 // the pool is twice as deep as it was, so the picture is taller
41const TOP = 262 // where the cliff begins
42const POOL_Y = 1340
43
44const n = (v: number) => String(Math.round(v * 10) / 10)
45const EASE = '.45 0 .55 1'
46const pick = <T,>(list: T[], s: number): T => list[((s % list.length) + list.length) % list.length]
47const rnd = (s: number) => {
48  const v = Math.sin(s * 127.1 + 311.7) * 43758.5453
49
50  return v - Math.floor(v)
51}
52
53const sway = (deg: number, cx: number, cy: number, dur: number, begin: number) =>
54  `<animateTransform attributeName="transform" type="rotate" values="${-deg} ${cx} ${cy};${deg} ${cx} ${cy};${-deg} ${cx} ${cy}" keyTimes="0;.5;1" calcMode="spline" keySplines="${EASE};${EASE}" dur="${dur}s" begin="${n(-begin)}s" repeatCount="indefinite"/>`
55
56// ---- leaves and ferns --------------------------------------------------------------------------
57const GREENS = ['#1f6b45', '#2f8f55', '#43a863', '#175a3a', '#5cbf72', '#2a7d4c']
58
59const leaf = (x: number, y: number, rot: number, len: number, color: string) =>
60  `<g transform="translate(${n(x)} ${n(y)}) rotate(${n(rot)})"><path d="M0 0 C${n(len * 0.3)} ${n(-len * 0.34)} ${n(len * 0.75)} ${n(-len * 0.3)} ${n(len)} 0 C${n(len * 0.75)} ${n(len * 0.3)} ${n(len * 0.3)} ${n(len * 0.34)} 0 0Z" fill="${color}"/>` +
61  `<path d="M0 0 L${n(len * 0.92)} 0" stroke="#fff" stroke-opacity=".25" stroke-width="1.2"/></g>`
62
63const fern = (x: number, y: number, k: number, size = 1) =>
64  `<g>${sway(2.4, x, y, 4 + (k % 3), k)}` +
65  [-68, -34, 0, 34, 68]
66    .map((a, j) => leaf(x, y, a - 90 + (j % 2) * 4, (28 + (j % 3) * 8) * size, pick(GREENS, k + j)))
67    .join('') +
68  '</g>'
69
70// ---- the sky, the far mountains and the canopy line --------------------------------------------
71const SKYLINE =
72  `<rect x="0" y="0" width="${W}" height="${TOP + 60}" fill="url(#sky)"/>` +
73  '<circle cx="318" cy="96" r="70" fill="#fff4c0" opacity=".35"/><circle cx="318" cy="96" r="34" fill="#fff0a8" opacity=".85"/>' +
74  `<path d="M0 ${TOP - 40} L50 150 L96 ${TOP - 70} L160 120 L226 ${TOP - 60} L290 150 L350 ${TOP - 80} L${W} 160 V${TOP + 60} H0Z" fill="#8fbfb4" opacity=".75"/>` +
75  `<path d="M0 ${TOP - 20} L70 ${TOP - 100} L140 ${TOP - 30} L210 ${TOP - 110} L290 ${TOP - 34} L350 ${TOP - 96} L${W} ${TOP - 40} V${TOP + 60} H0Z" fill="#6fa59c" opacity=".85"/>` +
76  `<rect x="0" y="${TOP - 60}" width="${W}" height="70" fill="url(#haze)"/>`
77
78const CANOPY = [0, 1].map(layer =>
79  Array.from({ length: 22 }, (_, k) => {
80    const x = k * 19 - 8 + layer * 9
81    const y = TOP - 6 - layer * 22 + rnd(k + layer * 40) * 14
82
83    return `<circle cx="${n(x)}" cy="${n(y)}" r="${n(18 + rnd(k + layer * 9) * 14)}" fill="${layer ? '#2a7050' : '#1f5a40'}"/>`
84  }).join(''),
85).join('')
86
87// ---- the cliff -------------------------------------------------------------------------------------
88const ROCKS = Array.from({ length: 28 }, (_, k) => {
89  const x = rnd(k) * 440 - 20
90  const y = TOP + 10 + rnd(k + 50) * (POOL_Y - TOP - 20)
91  const rx = 20 + rnd(k + 100) * 36
92  const ry = 9 + rnd(k + 150) * 18
93
94  return (
95    `<ellipse cx="${n(x)}" cy="${n(y)}" rx="${n(rx)}" ry="${n(ry)}" fill="${pick(['#4b6b4f', '#3f5f48', '#587a58', '#33503f'], k)}"/>` +
96    `<ellipse cx="${n(x - rx * 0.1)}" cy="${n(y - ry * 0.55)}" rx="${n(rx * 0.7)}" ry="${n(ry * 0.4)}" fill="#7ec27a" opacity=".38"/>`
97  )
98}).join('')
99
100const WALL = `<rect x="0" y="${TOP - 4}" width="${W}" height="${POOL_Y - TOP + 8}" fill="url(#wall)"/>` + ROCKS
101
102// ---- the terraces and the water that winds through them --------------------------------------------
103const BASINS: [number, number, number][] = [
104  [262, 330, 62],
105  [176, 560, 70],
106  [246, 790, 68],
107  [150, 1030, 74],
108]
109// from the lip of one terrace to the next: x1, y1, x2, y2, width at the top, width at the foot
110const FALLS: [number, number, number, number, number, number][] = [
111  [248, 340, 192, 556, 26, 40],
112  [192, 570, 232, 786, 28, 42],
113  [232, 800, 166, 1026, 28, 44],
114  [164, 1040, 198, 1346, 36, 70],
115]
116
117const ctrl = (x1: number, y1: number, x2: number, y2: number) => {
118  const dy = y2 - y1
119
120  return { a: [x1 + (x2 - x1) * 0.35, y1 + dy * 0.15], b: [x2 - (x2 - x1) * 0.08, y1 + dy * 0.7] }
121}
122
123const ribbon = (x1: number, y1: number, x2: number, y2: number, w1: number, w2: number) => {
124  const { a, b } = ctrl(x1, y1, x2, y2)
125  const wm = (w1 + w2) / 2
126
127  return (
128    `M${n(x1 - w1 / 2)} ${y1} C${n(a[0] - wm / 2)} ${n(a[1])} ${n(b[0] - w2 / 2)} ${n(b[1])} ${n(x2 - w2 / 2)} ${y2} L${n(x2 + w2 / 2)} ${y2} ` +
129    `C${n(b[0] + w2 / 2)} ${n(b[1])} ${n(a[0] + wm / 2)} ${n(a[1])} ${n(x1 + w1 / 2)} ${y1}Z`
130  )
131}
132
133const centre = (x1: number, y1: number, x2: number, y2: number, dx: number) => {
134  const { a, b } = ctrl(x1, y1, x2, y2)
135
136  return `M${n(x1 + dx)} ${y1} C${n(a[0] + dx)} ${n(a[1])} ${n(b[0] + dx)} ${n(b[1])} ${n(x2 + dx)} ${y2}`
137}
138
139const falls = (fast: boolean) =>
140  FALLS.map(([x1, y1, x2, y2, w1, w2], k) => {
141    const spread = [-0.28, 0, 0.28]
142      .map((f, j) => {
143        const w = w1 + (w2 - w1) * 0.5
144
145        return (
146          `<path d="${centre(x1, y1, x2, y2, f * w)}" fill="none" stroke="#fff" stroke-width="${n(2.2 + (j % 2) * 1.4)}" stroke-linecap="round" stroke-dasharray="${12 + j * 5} ${24 + (j % 2) * 12}" opacity=".7">` +
147          `<animate attributeName="stroke-dashoffset" values="0;-${36 + j * 5 + (j % 2) * 12}" dur="${n((fast ? 0.7 : 1.25) + k * 0.12 + j * 0.1)}s" repeatCount="indefinite"/></path>`
148        )
149      })
150      .join('')
151
152    return (
153      `<path d="${ribbon(x1, y1, x2, y2, w1, w2)}" fill="url(#fallw)"/>` +
154      `<path d="${ribbon(x1, y1, x2, y2, w1 * 0.7, w2 * 0.7)}" fill="#fff" opacity=".16"/>` +
155      spread
156    )
157  }).join('') +
158  // the little stream that feeds the top terrace
159  `<path d="${ribbon(254, TOP + 4, 262, 326, 14, 36)}" fill="url(#fallw)"/>`
160
161const basin = (cx: number, cy: number, rx: number, k: number) =>
162  `<ellipse cx="${cx}" cy="${cy + 20}" rx="${rx + 14}" ry="24" fill="#2f4a38"/>` +
163  `<ellipse cx="${cx}" cy="${cy + 6}" rx="${rx + 10}" ry="17" fill="#5b7a58"/>` +
164  `<ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="12" fill="url(#basinw)"/>` +
165  [0, 1].map(j => `<ellipse cx="${cx + (j ? 18 : -22)}" cy="${cy + 1}" rx="14" ry="3.6" fill="none" stroke="#fff" stroke-width="1.2" opacity="0"><animate attributeName="opacity" values="0;.7;0" dur="2.6s" begin="${n(-k * 0.9 - j * 1.3)}s" repeatCount="indefinite"/><animateTransform attributeName="transform" type="scale" values="1 1;1.5 1.4" additive="sum" dur="2.6s" begin="${n(-k * 0.9 - j * 1.3)}s" repeatCount="indefinite"/></ellipse>`).join('') +
166  `<path d="M${cx - rx - 8} ${cy + 8} Q${cx} ${cy + 36} ${cx + rx + 8} ${cy + 8}" fill="none" stroke="#7d9a72" stroke-width="5" stroke-linecap="round"/>`
167
168const mist = (x: number, y: number, k: number, fast: boolean) =>
169  [0, 1, 2]
170    .slice(0, fast ? 3 : 2)
171    .map(
172      j =>
173        `<ellipse cx="${n(x + (j - 1) * 16)}" cy="${y - 4}" rx="${n(20 + j * 4)}" ry="${n(9 + j * 2)}" fill="#fff" opacity=".4"><animate attributeName="opacity" values=".12;.5;.12" dur="${n(2.2 + j * 0.5)}s" begin="${n(-k - j * 0.7)}s" repeatCount="indefinite"/>` +
174        `<animateTransform attributeName="transform" type="translate" values="0 0;${(j % 2 ? 1 : -1) * 5} -14;0 0" dur="${n(3 + j * 0.5)}s" begin="${n(-k - j * 0.4)}s" repeatCount="indefinite"/></ellipse>`,
175    )
176    .join('')
177
178// ---- salmon leaping up the falls, and out of the pool ---------------------------------------------------
179const salmon = () =>
180  '<g><animateTransform attributeName="transform" type="rotate" values="-16 0 0;16 0 0;-16 0 0" dur=".28s" repeatCount="indefinite"/>' +
181  '<path d="M-9 0 Q0 -5 8 0 Q0 5 -9 0Z" fill="#ee7f5a"/><path d="M-9 0 L-15 -4 L-14 0 L-15 4Z" fill="#d9603c"/><path d="M-2 -3 Q2 -7 4 -3" fill="#d9603c"/>' +
182  '<path d="M-6 1.2 Q0 3.4 6 1.2" stroke="#ffd0b8" fill="none" stroke-width="1.2"/><circle cx="5" cy="-.8" r="1" fill="#1c2430"/></g>'
183
184// fall, sideways offset (a share of the width), seconds per round, delay
185const LEAP: [number, number, number, number][] = [
186  [0, -0.18, 10.5, 0],
187  [1, 0.2, 12.5, 4],
188  [2, -0.1, 9.5, 2],
189  [3, -0.2, 13, 0],
190  [3, 0.22, 13, 6.5],
191]
192
193const leapers = () =>
194  LEAP.map(([k, f, dur, delay]) => {
195    const [x1, y1, x2, y2, w1, w2] = FALLS[k]
196    const { a, b: c } = ctrl(x1, y1, x2, y2)
197    const dx = f * ((w1 + w2) / 2)
198    const path = `M${n(x2 + dx)} ${y2} C${n(c[0] + dx)} ${n(c[1])} ${n(a[0] + dx)} ${n(a[1])} ${n(x1 + dx)} ${y1}`
199
200    return (
201      '<g data-fish="1" opacity="0">' +
202      `<animate attributeName="opacity" values="0;1;1;0;0" keyTimes="0;.02;.2;.23;1" dur="${dur}s" begin="${n(-delay)}s" repeatCount="indefinite"/>` +
203      `<animateMotion path="${path}" rotate="auto" dur="${dur}s" begin="${n(-delay)}s" repeatCount="indefinite" calcMode="linear" keyPoints="0;1;1" keyTimes="0;.22;1"/>${salmon()}</g>`
204    )
205  }).join('')
206
207const jumper = (x: number, y: number, delay: number, dur: number) =>
208  `<g data-fish="1" opacity="0"><animate attributeName="opacity" values="0;1;1;0;0" keyTimes="0;.01;.1;.11;1" dur="${dur}s" begin="${n(-delay)}s" repeatCount="indefinite"/>` +
209  `<animateMotion path="M${x} ${y} Q${x + 16} ${y - 52} ${x + 34} ${y}" rotate="auto" dur="${dur}s" begin="${n(-delay)}s" repeatCount="indefinite" calcMode="linear" keyPoints="0;1;1" keyTimes="0;.1;1"/>${salmon()}</g>` +
210  `<ellipse cx="${x + 34}" cy="${y}" rx="10" ry="3" fill="none" stroke="#fff" stroke-width="1.2" opacity="0"><animate attributeName="opacity" values="0;0;.8;0;0" keyTimes="0;.09;.12;.2;1" dur="${dur}s" begin="${n(-delay)}s" repeatCount="indefinite"/></ellipse>`
211
212// ---- the pool, its banks, pads and lotus ---------------------------------------------------------
213const PADS: Pt[] = [
214  [96, 1396],
215  [146, 1432],
216  [222, 1460],
217  [258, 1408],
218  [306, 1444],
219  [70, 1510],
220  [182, 1534],
221  [268, 1508],
222  [330, 1566],
223  [124, 1590],
224  [236, 1612],
225  [340, 1500],
226]
227
228const pad = ([x, y]: Pt, k: number) =>
229  `<g><animateTransform attributeName="transform" type="translate" values="0 0;0 1.6;0 0" dur="${n(3.4 + k * 0.5)}s" begin="${n(-k)}s" repeatCount="indefinite"/>` +
230  `<path d="M${x} ${y} L${x + 15} ${y - 4} A16 6.6 0 1 1 ${x + 11} ${y + 4.6}Z" fill="#3f9f58" transform="translate(${-x + x} 0)"/>` +
231  `<ellipse cx="${x}" cy="${y}" rx="15" ry="5.6" fill="none" stroke="#2a7d46" stroke-width="1"/></g>`
232
233const lotus = (x: number, y: number) =>
234  `<g transform="translate(${x} ${y})">` +
235  [-40, -20, 0, 20, 40].map(a => `<path d="M0 0 Q-5 -10 0 -16 Q5 -10 0 0Z" fill="#f08ab8" transform="rotate(${a})"/>`).join('') +
236  '<circle cy="-2" r="3" fill="#ffd84d"/></g>'
237
238const POOL_BASE_SHAPE = `<path d="M24 ${POOL_Y - 4} H376 V1560 Q376 1654 200 1654 Q24 1654 24 1560Z" fill="url(#pool)"/>`
239
240
241// the bed of the pool: stones, waving weeds and light moving on the water
242const POOL_DEPTH =
243  Array.from({ length: 9 }, (_, k) => `<ellipse cx="${n(50 + rnd(k + 400) * 300)}" cy="${n(1620 + rnd(k + 410) * 26)}" rx="${n(14 + rnd(k + 420) * 22)}" ry="${n(6 + rnd(k + 430) * 7)}" fill="${pick(['#0a3a46', '#0d4452', '#15555f'], k)}"/>`).join('') +
244  Array.from({ length: 11 }, (_, k) => {
245    const x = 40 + k * 31 + rnd(k + 440) * 14
246    const ht = 44 + rnd(k + 450) * 60
247
248    return `<g>${sway(5, x, 1650, 3 + (k % 4) * 0.6, k * 0.8)}<path d="M${n(x)} 1650 q${n(rnd(k) * 10 - 5)} ${n(-ht * 0.5)} ${n(rnd(k + 9) * 12 - 6)} ${n(-ht)}" fill="none" stroke="${pick(['#1f8f5a', '#2aa56a', '#157a4a'], k)}" stroke-width="${n(3 + (k % 3))}" stroke-linecap="round"/></g>`
249  }).join('') +
250  Array.from({ length: 9 }, (_, k) => `<ellipse cx="${n(50 + rnd(k + 460) * 300)}" cy="${n(1380 + rnd(k + 470) * 240)}" rx="${n(18 + rnd(k) * 20)}" ry="${n(4 + rnd(k + 5) * 5)}" fill="#bff6ec" opacity="0"><animate attributeName="opacity" values="0;.22;0" dur="${n(4 + rnd(k + 8) * 4)}s" begin="${n(-rnd(k + 3) * 6)}s" repeatCount="indefinite"/></ellipse>`).join('')
251
252const POOL_BASE = POOL_BASE_SHAPE + POOL_DEPTH
253
254const POOL_TOP =
255  `<path d="M24 ${POOL_Y} q16 -6 33 0 t33 0 t33 0 t33 0 t33 0 t33 0 t33 0 t33 0 t33 0 t33 0 V${POOL_Y - 6} H24Z" fill="#a8efe6" opacity=".8"><animateTransform attributeName="transform" type="translate" values="0 0;-66 0" dur="4.2s" repeatCount="indefinite"/></path>` +
256  PADS.map((p, k) => pad(p, k)).join('') +
257  lotus(120, 1408) +
258  lotus(278, 1424) +
259  lotus(190, 1522) +
260  lotus(84, 1562) +
261  lotus(300, 1596) +
262  jumper(236, 1372, 0, 9) +
263  jumper(96, 1398, 4.5, 11) +
264  // something watching from the water
265  '<g transform="translate(330 1362)"><animateTransform attributeName="transform" type="translate" values="330 1362;330 1366;330 1362" dur="3.4s" repeatCount="indefinite"/>' +
266  '<circle cx="0" cy="0" r="3.8" fill="#4f7a3a"/><circle cx="-1" cy="-1" r="1.6" fill="#f2c94c"/><circle cx="-12" cy="1" r="3.2" fill="#4f7a3a"/><circle cx="-13" cy="0" r="1.4" fill="#f2c94c"/></g>'
267
268const BANKS =
269  `<path d="M0 ${POOL_Y - 10} Q30 ${POOL_Y - 22} 44 ${POOL_Y - 4} V${H} H0Z" fill="#4d3b2c"/><path d="M${W} ${POOL_Y - 10} Q370 ${POOL_Y - 22} 356 ${POOL_Y - 4} V${H} H${W}Z" fill="#4d3b2c"/>` +
270  `<path d="M0 ${POOL_Y - 10} Q30 ${POOL_Y - 22} 44 ${POOL_Y - 4}" fill="none" stroke="#5cbf72" stroke-width="9" stroke-linecap="round"/><path d="M${W} ${POOL_Y - 10} Q370 ${POOL_Y - 22} 356 ${POOL_Y - 4}" fill="none" stroke="#5cbf72" stroke-width="9" stroke-linecap="round"/>` +
271  Array.from({ length: 14 }, (_, k) => `<ellipse cx="${n(rnd(k + 70) * 40)}" cy="${n(1400 + k * 20)}" rx="${n(9 + rnd(k) * 8)}" ry="5" fill="#6b5640"/><ellipse cx="${n(W - rnd(k + 80) * 40)}" cy="${n(1410 + k * 18)}" rx="${n(9 + rnd(k + 3) * 8)}" ry="5" fill="#6b5640"/>`).join('')
272
273// reeds and big ferns on the banks and the terrace rims
274const GROWTH =
275  [22].map((x, k) => fern(x, POOL_Y - 8, k + 3, 1.3)).join('') +
276  [378].map((x, k) => fern(x, POOL_Y - 8, k + 7, 1.3)).join('') +
277  BASINS.map(([cx, cy, rx], k) => fern(cx - rx - 2, cy + 6, k + 11) + fern(cx + rx + 2, cy + 6, k + 21)).join('') +
278  Array.from({ length: 9 }, (_, k) => `<path d="M${n(46 + k * 36)} ${POOL_Y} q${n(rnd(k) * 8 - 4)} -26 ${n(rnd(k + 9) * 12 - 6)} -${n(28 + rnd(k + 4) * 14)}" stroke="#3f9a55" stroke-width="2.4" fill="none" stroke-linecap="round"/>`).join('')
279
280// ---- limbs for animals to walk and swing from, and vines ----------------------------------------------
281const LIMBS = [
282  { x0: 280, x1: 400, y: 900, side: 1 },
283  { x0: 0, x1: 118, y: 1110, side: -1 },
284  { x0: 0, x1: 120, y: 675, side: -1 },
285] as const
286
287const limb = (l: (typeof LIMBS)[number], k: number) => {
288  const tipx = l.side === -1 ? l.x1 : l.x0
289  const rootx = l.side === -1 ? l.x0 : l.x1
290
291  return (
292    `<path d="M${rootx} ${l.y + 8} Q${n((rootx + tipx) / 2)} ${l.y - 4} ${tipx} ${l.y}" fill="none" stroke="#3b2a1a" stroke-width="15" stroke-linecap="round"/>` +
293    `<path d="M${rootx} ${l.y + 6} Q${n((rootx + tipx) / 2)} ${l.y - 6} ${tipx} ${l.y - 2}" fill="none" stroke="#7a5532" stroke-width="10" stroke-linecap="round"/>` +
294    `<path d="M${rootx} ${l.y + 3} Q${n((rootx + tipx) / 2)} ${l.y - 9} ${tipx} ${l.y - 5}" fill="none" stroke="#9a7048" stroke-width="2.6" stroke-linecap="round" opacity=".6"/>` +
295    `<g>${sway(1.6, tipx, l.y, 5 + k, k)}${[-70, -35, 0, 35].map((a, j) => leaf(tipx, l.y - 2, (l.side === -1 ? a : 180 - a), 34 + j * 3, pick(GREENS, k + j))).join('')}</g>`
296  )
297}
298
299const vine = (x: number, len: number, dur: number, begin: number) =>
300  `<g>${sway(2.4, x, TOP + 6, dur, begin)}<path d="M${x} ${TOP + 6} q-7 ${n(len * 0.3)} 0 ${n(len * 0.5)} t0 ${n(len * 0.5)}" fill="none" stroke="#1f6b3d" stroke-width="3" stroke-linecap="round"/>` +
301  `<circle cx="${x}" cy="${TOP + 6 + len}" r="3.2" fill="#5cbf72"/><circle cx="${x - 3}" cy="${n(TOP + 6 + len * 0.55)}" r="2.4" fill="#43a863"/></g>`
302
303const VINES = [34, 84, 154, 318, 366].map((x, k) => vine(x, 150 + (k % 3) * 120, 4.5 + k * 0.4, k)).join('')
304
305// the near leaves that frame the picture
306const FRAME = [
307  [-14, 420, -14, 120],
308  [-14, 820, 8, 130],
309  [-14, 1230, -6, 118],
310  [414, 560, 192, 126],
311  [414, 1010, 172, 132],
312  [414, 1280, 190, 120],
313]
314  .map(
315    ([x, y, rot, len], k) =>
316      `<g>${sway(2, x, y, 5 + (k % 3), k * 1.3)}${leaf(x, y, rot, len, '#124d32')}${leaf(x, y, rot + (x < 0 ? 30 : -30), len * 0.8, '#1a6b43')}${leaf(x, y, rot - (x < 0 ? 28 : -28), len * 0.7, '#0f4029')}</g>`,
317  )
318  .join('')
319
320const RAYS = [
321  [300, 380, 150, 70, 8],
322  [236, 296, 40, -50, 11],
323  [350, 400, 260, 220, 13],
324]
325  .map(
326    ([a, b, c, d, dur], k) =>
327      `<polygon points="${a},0 ${b},0 ${c},1000 ${d},1000" fill="#fff6c8" opacity=".08"><animate attributeName="opacity" values=".04;.16;.04" dur="${dur}s" begin="${-k * 3}s" repeatCount="indefinite"/></polygon>`,
328  )
329  .join('')
330
331// Resting is day; while Claude works it is night: a dark veil, the moon, stars and fireflies fade in over 1.4 s.
332const fade = (a0: number, a1: number, inner: string, max = 1) => {
333  const v0 = Math.round(a0 * max * 1000) / 1000
334  const v1 = Math.round(a1 * max * 1000) / 1000
335  if (a0 === a1) {
336    return a1 === 0 ? '' : `<g opacity="${v1}">${inner}</g>`
337  }
338
339  return `<g opacity="${v0}"><animate data-once="1" attributeName="opacity" values="${v0};${v1}" dur="${n(1.4 * Math.abs(a1 - a0))}s" fill="freeze"/>${inner}</g>`
340}
341
342const NIGHT_SKY =
343  '<circle cx="88" cy="112" r="40" fill="#cfd8ff" opacity=".14"/><circle cx="88" cy="112" r="21" fill="#f4f1da"/><circle cx="81" cy="106" r="4" fill="#d6d2b6"/><circle cx="95" cy="119" r="3" fill="#d6d2b6"/>' +
344  Array.from({ length: 14 }, (_, k) => `<circle cx="${n(rnd(k * 2) * W)}" cy="${n(rnd(k * 2 + 1) * (TOP - 50))}" r="${n(0.7 + rnd(k + 90))}" fill="#fff" opacity=".7"><animate attributeName="opacity" values=".25;.9;.25" dur="${n(2 + rnd(k + 5) * 4)}s" begin="${n(-rnd(k + 9) * 6)}s" repeatCount="indefinite"/></circle>`).join('')
345
346const FIREFLIES = Array.from({ length: 10 }, (_, k) => {
347  const x = 20 + rnd(k + 200) * 360
348  const y = TOP + 40 + rnd(k + 210) * (H - 60 - TOP - 80)
349
350  return `<circle cx="${n(x)}" cy="${n(y)}" r="2.2" fill="#e8ff7a"><animate attributeName="opacity" values=".1;1;.2;1;.1" dur="${n(3 + rnd(k) * 4)}s" begin="${n(-rnd(k + 4) * 6)}s" repeatCount="indefinite"/>` +
351    `<animateMotion path="M0 0 C${n(10 + rnd(k) * 16)} -14 ${n(-12 - rnd(k + 3) * 14)} -10 0 0Z" dur="${n(6 + rnd(k + 8) * 5)}s" repeatCount="indefinite"/></circle>`
352}).join('')
353
354const nightLayer = (a0: number, a1: number) =>
355  fade(a0, a1, `<rect x="0" y="0" width="${W}" height="${H}" fill="#06133a"/>`, 0.55) + fade(a0, a1, NIGHT_SKY + FIREFLIES)
356
357// ---- visitors: each is drawn for LIFE seconds from its own start, then dropped ---------------------------
358const flap = (draw: string, dur: number) =>
359  `<g><animateTransform attributeName="transform" type="scale" values="1 1;1 -.4;1 1" dur="${dur}s" repeatCount="indefinite"/>${draw}</g>`
360
361// Crosses the picture left to right (or back), facing the way it goes.
362const flyer = (draw: string, y: number, dir: 1 | -1, dur: number, begin: string) => {
363  const path = dir === 1 ? `M-50 ${n(y)} C100 ${n(y - 70)} 260 ${n(y + 60)} 450 ${n(y - 30)}` : `M450 ${n(y)} C300 ${n(y - 70)} 140 ${n(y + 60)} -50 ${n(y - 30)}`
364
365  return `<g><animateMotion path="${path}" dur="${dur}s" begin="${begin}" fill="freeze"/><g transform="scale(${dir} 1)">${draw}</g></g>`
366}
367
368const toucan = () =>
369  flap('<path d="M-4 0 Q-10 -16 -18 -10 Q-10 -2 -4 0Z" fill="#2a3342"/>', 0.46) +
370  '<ellipse rx="14" ry="6" fill="#1c2430"/><ellipse cx="9" cy="1.4" rx="5" ry="3.8" fill="#fff"/><circle cx="15" cy="-2" r="5" fill="#1c2430"/>' +
371  '<path d="M18 -5 q16 -2 19 4 q-9 6 -19 2Z" fill="#f4a21f"/><path d="M20 -2 q10 -1 16 1" stroke="#d9531e" fill="none"/><circle cx="17" cy="-3.2" r="1.2" fill="#fff"/>' +
372  '<path d="M-12 -1 l-13 3 l13 2Z" fill="#1c2430"/><rect x="-26" y="1" width="3" height="3" fill="#e24a4a"/>'
373
374const parrot = (c: string, wing: string) =>
375  flap(`<path d="M-3 0 Q-8 -14 -16 -9 Q-9 -2 -3 0Z" fill="${wing}"/>`, 0.4) +
376  `<ellipse rx="10" ry="4.4" fill="${c}"/><circle cx="11" cy="-2" r="4.2" fill="${c}"/><ellipse cx="12" cy="-2" rx="2.2" ry="2.8" fill="#fff"/><circle cx="12.4" cy="-2.2" r="1" fill="#1c2430"/>` +
377  '<path d="M14 -4 q5 0 5 4 q-3 0 -5 -2Z" fill="#3a3a3a"/>' +
378  `<path d="M-9 0 L-32 6 L-28 1Z" fill="${wing}"/><path d="M-9 0 L-24 3" stroke="#f2c94c" stroke-width="2"/>`
379
380const monkeyHang = () =>
381  '<path d="M0 0 L-3 13 M0 0 L3 13" stroke="#8b5a2b" stroke-width="3.2" stroke-linecap="round"/>' +
382  '<path d="M2 32 q14 6 8 18 q-4 6 -10 2" fill="none" stroke="#7a4a22" stroke-width="3.4" stroke-linecap="round"/>' +
383  '<ellipse cx="0" cy="24" rx="6.5" ry="10" fill="#8b5a2b"/><ellipse cx="0" cy="25" rx="3.6" ry="6" fill="#e8c9a0"/>' +
384  '<path d="M-3 32 l-2 10 M3 32 l2 10" stroke="#8b5a2b" stroke-width="3" stroke-linecap="round"/>' +
385  '<circle cx="0" cy="12" r="6" fill="#8b5a2b"/><ellipse cx="0" cy="13.2" rx="3.8" ry="3.2" fill="#e8c9a0"/><circle cx="-1.8" cy="11.4" r=".9" fill="#1c2430"/><circle cx="1.8" cy="11.4" r=".9" fill="#1c2430"/><circle cx="-6" cy="11" r="2.2" fill="#8b5a2b"/><circle cx="6" cy="11" r="2.2" fill="#8b5a2b"/>'
386
387const legs = (x: number, k: number) =>
388  `<g><animateTransform attributeName="transform" type="rotate" values="${k ? -24 : 24} ${x} -8;${k ? 24 : -24} ${x} -8;${k ? -24 : 24} ${x} -8" dur=".8s" repeatCount="indefinite"/><rect x="${x - 2.4}" y="-9" width="5" height="12" rx="2.4" fill="#e8a63c"/></g>`
389
390const jaguar = () =>
391  `<g>${sway(8, -26, -16, 2.2, 0)}<path d="M-26 -16 q-14 -6 -12 -20 q2 -8 8 -4" fill="none" stroke="#e8a63c" stroke-width="5" stroke-linecap="round"/></g>` +
392  legs(-20, 0) +
393  legs(18, 0) +
394  '<rect x="-28" y="-24" width="50" height="17" rx="8.5" fill="#e8a63c"/><rect x="-28" y="-12" width="50" height="5" rx="2.5" fill="#f4d89a" opacity=".6"/>' +
395  legs(-10, 1) +
396  legs(26, 1) +
397  [[-20, -18], [-10, -20], [0, -17], [8, -20], [-16, -12], [-4, -11], [12, -14]].map(([x, y]) => `<circle cx="${x}" cy="${y}" r="2.2" fill="#3b2a1a"/>`).join('') +
398  '<circle cx="28" cy="-22" r="9" fill="#e8a63c"/><circle cx="23" cy="-30" r="3" fill="#e8a63c"/><circle cx="32" cy="-30" r="3" fill="#e8a63c"/><ellipse cx="34" cy="-19" rx="5" ry="3.6" fill="#f4d89a"/>' +
399  '<circle cx="30" cy="-24" r="1.6" fill="#1c2430"/><circle cx="37" cy="-20" r="1.2" fill="#222"/><circle cx="26" cy="-27" r="1.5" fill="#3b2a1a"/>'
400
401const croc = (jaw: number) =>
402  '<g><animateTransform attributeName="transform" type="rotate" values="-4 -30 0;4 -30 0;-4 -30 0" dur="2.6s" repeatCount="indefinite"/><path d="M-30 -3 L-72 3 L-30 6Z" fill="#46703a"/></g>' +
403  '<ellipse rx="34" ry="8.5" fill="#4f7a3a"/><ellipse cx="2" cy="4" rx="28" ry="4" fill="#cfd9a0" opacity=".7"/>' +
404  '<path d="M-22 -7 l3 -4 l3 4 l3 -4 l3 4 l3 -4 l3 4 l3 -4 l3 4 l3 -4 l3 4" fill="#35592b"/>' +
405  '<rect x="26" y="-6" width="32" height="8" rx="3.5" fill="#4f7a3a"/><circle cx="55" cy="-6.4" r="1.2" fill="#223"/>' +
406  `<g><animateTransform attributeName="transform" type="rotate" values="0 28 3;0 28 3;24 28 3;24 28 3;0 28 3;0 28 3" keyTimes="0;.35;.4;.46;.52;1" dur="${LIFE}s" begin="${n(jaw)}s" repeatCount="indefinite"/><rect x="28" y="2" width="29" height="5" rx="2.4" fill="#3f6630"/><path d="M32 2 l2 -2.6 l2 2.6 M40 2 l2 -2.6 l2 2.6 M48 2 l2 -2.6 l2 2.6" fill="#fff"/></g>` +
407  '<ellipse cx="27" cy="-8" rx="4.4" ry="3.2" fill="#4f7a3a"/><circle cx="28" cy="-8.6" r="2" fill="#f2c94c"/><rect x="27.4" y="-10" width="1.2" height="3" fill="#1c2430"/>'
408
409const frog = () =>
410  '<ellipse cx="0" cy="-5" rx="7" ry="5" fill="#5fbf4a"/><circle cx="-3.5" cy="-10" r="2.4" fill="#5fbf4a"/><circle cx="3.5" cy="-10" r="2.4" fill="#5fbf4a"/>' +
411  '<circle cx="-3.5" cy="-10.4" r="1" fill="#1c2430"/><circle cx="3.5" cy="-10.4" r="1" fill="#1c2430"/><path d="M-6 -2 q-5 2 -3 4 M6 -2 q5 2 3 4" stroke="#3f8f33" stroke-width="2" fill="none" stroke-linecap="round"/>'
412
413const butterfly = (c: string) =>
414  `<g><animateTransform attributeName="transform" type="scale" values="1 1;.25 1;1 1" dur=".32s" repeatCount="indefinite"/><ellipse cx="-3.4" cy="0" rx="3.6" ry="4.6" fill="${c}"/><ellipse cx="3.4" cy="0" rx="3.6" ry="4.6" fill="${c}"/><circle cx="-3.4" cy="-1" r="1" fill="#fff" opacity=".7"/><circle cx="3.4" cy="-1" r="1" fill="#fff" opacity=".7"/></g><rect x="-.5" y="-4" width="1" height="8" fill="#222"/>`
415
416const sloth = () =>
417  '<path d="M-7 6 L-7 -2 M7 6 L7 -2" stroke="#6b5a42" stroke-width="3.4" stroke-linecap="round"/><path d="M-9 -1 l2 2 M-7 -2 l0 3 M-5 -1 l-2 2" stroke="#e8e0cc" stroke-width="1" stroke-linecap="round"/>' +
418  '<ellipse cx="0" cy="14" rx="16" ry="7.4" fill="#8a7457"/><path d="M-12 12 q4 3 8 0 M0 12 q4 3 8 0" stroke="#6b5a42" stroke-width="1.2" fill="none"/>' +
419  '<circle cx="18" cy="14" r="6.4" fill="#e6d9b8"/><path d="M13.6 12.4 q4 -3 8 0 l-1 3 q-3 -1 -6 0Z" fill="#3b2a1a" opacity=".85"/><circle cx="17" cy="13.6" r="1" fill="#fff"/><circle cx="21" cy="13.6" r="1" fill="#fff"/><path d="M16.4 18 q2 1.6 4 0" stroke="#6b5a42" fill="none" stroke-linecap="round"/>' +
420  '<path d="M-7 20 L-8 28 M7 20 L8 28" stroke="#6b5a42" stroke-width="3" stroke-linecap="round"/>'
421
422const bat = () =>
423  flap('<path d="M0 0 L-13 -8 Q-9 -2 -13 4Z M0 0 L13 -8 Q9 -2 13 4Z" fill="#2a2433"/>', 0.22) +
424  '<ellipse rx="3" ry="4.4" fill="#2a2433"/><path d="M-2.4 -3.4 l-1 -3 l2.4 1.4 M2.4 -3.4 l1 -3 l-2.4 1.4" fill="#2a2433"/><circle cx="-1.1" cy="-1" r=".6" fill="#f2c94c"/><circle cx="1.1" cy="-1" r=".6" fill="#f2c94c"/>'
425
426const chameleon = () =>
427  '<path d="M-9 -7 q-11 6 -15 -2 q-3 -7 4 -6 q5 1 3 5" fill="none" stroke="#6fbf3a" stroke-width="3.2" stroke-linecap="round"><animate attributeName="stroke" values="#6fbf3a;#e8a63c;#6fbf3a" dur="6s" repeatCount="indefinite"/></path>' +
428  '<path d="M-5 -2 l-3 5 M5 -2 l3 5 M-1 -2 l-1 5 M9 -2 l2 5" stroke="#5e9a30" stroke-width="2" stroke-linecap="round"/>' +
429  '<ellipse cx="0" cy="-8" rx="11" ry="7.4" fill="#6fbf3a"><animate attributeName="fill" values="#6fbf3a;#e8a63c;#6fbf3a" dur="6s" repeatCount="indefinite"/></ellipse>' +
430  '<path d="M-6 -14 l3 -3 l3 3 l3 -3 l3 3" fill="none" stroke="#4f8a28" stroke-width="1.6" stroke-linecap="round"/>' +
431  '<ellipse cx="12" cy="-9" rx="6" ry="4.6" fill="#6fbf3a"><animate attributeName="fill" values="#6fbf3a;#e8a63c;#6fbf3a" dur="6s" repeatCount="indefinite"/></ellipse>' +
432  '<circle cx="12" cy="-12.4" r="3" fill="#8bd24f"/><circle cx="12.8" cy="-12.6" r="1.3" fill="#1c2430"/>' +
433  '<path d="M17 -8 L19 -8" stroke="#e24a4a" stroke-width="1.6" stroke-linecap="round"><animate attributeName="d" values="M17 -8 L19 -8;M17 -8 L19 -8;M17 -8 L34 -8;M17 -8 L19 -8;M17 -8 L19 -8" keyTimes="0;.5;.55;.6;1" dur="7s" repeatCount="indefinite"/></path>'
434
435const snake = () =>
436  '<path d="M0 0 q9 12 0 24 t0 24 t0 24 t0 24" fill="none" stroke="#4fae64" stroke-width="6.4" stroke-linecap="round"/>' +
437  '<path d="M0 0 q9 12 0 24 t0 24 t0 24 t0 24" fill="none" stroke="#a8e08a" stroke-width="1.6" stroke-dasharray="3 6" stroke-linecap="round"/>' +
438  '<ellipse cx="0" cy="98" rx="4.4" ry="5.6" fill="#3f9a55"/><circle cx="-2" cy="97" r="1" fill="#f2c94c"/><circle cx="2" cy="97" r="1" fill="#f2c94c"/>' +
439  '<path d="M0 103 v7 l-2 3 M0 110 l2 3" stroke="#e24a4a" stroke-width="1" fill="none"><animate attributeName="opacity" values="1;0;1;1" keyTimes="0;.2;.4;1" dur="1.6s" repeatCount="indefinite"/></path>'
440
441const hummingbird = () =>
442  flap('<ellipse cx="-2" cy="-2" rx="8" ry="2.4" fill="#cfe9f2" opacity=".65" transform="rotate(-28)"/>', 0.1) +
443  '<ellipse rx="5.4" ry="2.8" fill="#1f9d8a"/><circle cx="6" cy="-1" r="2.6" fill="#2fbfa0"/><path d="M8 -1 L20 -1.8" stroke="#333" stroke-width="1"/><circle cx="6.6" cy="-1.4" r=".7" fill="#111"/>' +
444  '<path d="M-5 0 L-11 3 L-11 -2Z" fill="#157a6a"/><circle cx="5.4" cy="1.4" r="1.4" fill="#e23b3b"/>'
445
446const heron = (begin: string) =>
447  '<path d="M-3 -44 V0 M3 -44 V0" stroke="#c9a56b" stroke-width="2" stroke-linecap="round"/>' +
448  '<ellipse cx="0" cy="0" rx="14" ry="3" fill="none" stroke="#fff" stroke-width="1.2" opacity=".6"><animate attributeName="rx" values="10;22;10" dur="3s" repeatCount="indefinite"/></ellipse>' +
449  '<ellipse cx="-2" cy="-52" rx="17" ry="8.6" fill="#8fa8c2" transform="rotate(-8 0 -52)"/><path d="M-16 -50 L-30 -46 L-16 -46Z" fill="#6f88a2"/><path d="M-8 -54 q10 -6 18 -2" stroke="#cfd9e6" stroke-width="2" fill="none"/>' +
450  `<g><animateTransform attributeName="transform" type="rotate" values="0 12 -56;0 12 -56;44 12 -56;44 12 -56;0 12 -56;0 12 -56" keyTimes="0;.3;.37;.45;.52;1" dur="${LIFE}s" begin="${begin}" fill="freeze"/>` +
451  '<path d="M12 -56 q9 -14 -2 -28 q-4 -6 4 -12" fill="none" stroke="#e8eef4" stroke-width="5" stroke-linecap="round"/>' +
452  '<circle cx="14" cy="-98" r="4.6" fill="#e8eef4"/><path d="M12 -100 l-9 -2" stroke="#1c2430" stroke-width="2" stroke-linecap="round"/><path d="M18 -98 L38 -96" stroke="#f2c94c" stroke-width="3" stroke-linecap="round"/><circle cx="15.6" cy="-99" r="1" fill="#1c2430"/></g>'
453
454const turtle = () =>
455  '<ellipse cx="9" cy="8" rx="8" ry="3.4" fill="#7da854"><animateTransform attributeName="transform" type="rotate" values="-20 9 8;20 9 8;-20 9 8" dur="1.8s" repeatCount="indefinite"/></ellipse>' +
456  '<ellipse cx="-10" cy="7" rx="6" ry="2.8" fill="#7da854"><animateTransform attributeName="transform" type="rotate" values="20 -10 7;-20 -10 7;20 -10 7" dur="1.8s" repeatCount="indefinite"/></ellipse>' +
457  '<path d="M-17 2 L-24 4 L-17 5Z" fill="#7da854"/><ellipse cx="20" cy="-1" rx="6.4" ry="4.2" fill="#9ac06a"/><circle cx="22" cy="-2.4" r="1.1" fill="#1c2430"/>' +
458  '<ellipse cx="0" cy="0" rx="18" ry="11" fill="#5b8a3c"/><ellipse cx="0" cy="0" rx="18" ry="11" fill="none" stroke="#3e6a28" stroke-width="1.6"/>' +
459  '<path d="M-8 -6 l6 -3 l6 3 l0 7 l-6 3 l-6 -3Z M6 -7 l6 3 M-14 2 l6 3" fill="none" stroke="#3e6a28" stroke-width="1.4"/>'
460
461const neon = () =>
462  '<g><animateTransform attributeName="transform" type="rotate" values="-8 -6 0;8 -6 0;-8 -6 0" dur=".34s" repeatCount="indefinite"/><path d="M-6 0 L-12 -4 L-12 4Z" fill="#cfd9e8"/></g>' +
463  '<ellipse rx="8" ry="3.3" fill="#d9e3f0"/><path d="M-7 -.6 L8 -.6" stroke="#2fc2ff" stroke-width="1.8"/><path d="M-6 1.2 Q0 3.4 8 1.2 L7 3 Q0 4 -6 2.6Z" fill="#e5483a"/><circle cx="5.4" cy="-1" r="1" fill="#1c2430"/>'
464
465const piranha = () =>
466  '<g><animateTransform attributeName="transform" type="rotate" values="-10 -8 0;10 -8 0;-10 -8 0" dur=".3s" repeatCount="indefinite"/><path d="M-8 0 L-16 -5 L-16 5Z" fill="#4f5e6a"/></g>' +
467  '<ellipse rx="11" ry="6.6" fill="#6b7a86"/><path d="M-8 2 Q0 8 10 3 Q0 5 -8 2Z" fill="#d9534f"/><path d="M-3 -7 Q2 -12 6 -6Z" fill="#4f5e6a"/>' +
468  '<path d="M7 1 l2 3 l2 -3 l2 3 l1 -3" fill="#fff" stroke="#fff" stroke-width=".6"/><circle cx="6" cy="-2" r="1.5" fill="#f2c94c"/><circle cx="6.3" cy="-2" r=".7" fill="#111"/>'
469
470const otter = () =>
471  '<path d="M-16 0 Q-28 2 -36 -3 Q-30 4 -16 3Z" fill="#6b4a2a"/><ellipse cx="0" cy="0" rx="17" ry="5.6" fill="#7a5532"/><ellipse cx="2" cy="2.6" rx="12" ry="2.6" fill="#c9a578"/>' +
472  '<circle cx="17" cy="-1" r="5.4" fill="#7a5532"/><ellipse cx="21" cy="1" rx="3.2" ry="2.4" fill="#e8d4b0"/><circle cx="21.6" cy="0" r=".9" fill="#111"/><circle cx="18" cy="-3" r="1" fill="#111"/><circle cx="14" cy="-5.4" r="1.6" fill="#7a5532"/>' +
473  '<path d="M22 2 l8 -1 M22 3 l8 2" stroke="#fff" stroke-width=".6"/>'
474
475const ray = () =>
476  '<path d="M-8 0 L-46 3" stroke="#5d7690" stroke-width="2" stroke-linecap="round"/>' +
477  '<g><animateTransform attributeName="transform" type="scale" values="1 1;1 .72;1 1" dur="1.8s" repeatCount="indefinite"/>' +
478  '<path d="M26 0 Q10 -17 -12 -20 Q-6 0 -12 20 Q10 17 26 0Z" fill="#6d8aa5"/><path d="M26 0 Q10 -9 -4 -10 Q-1 0 -4 10 Q10 9 26 0Z" fill="#8fa9c0" opacity=".6"/>' +
479  '<circle cx="-1" cy="-6" r="1.8" fill="#4f6a85"/><circle cx="-8" cy="8" r="1.4" fill="#4f6a85"/></g>' +
480  '<circle cx="17" cy="-3" r="1.2" fill="#111"/><circle cx="17" cy="3" r="1.2" fill="#111"/>'
481
482const dragonfly = () =>
483  flap(
484    '<ellipse cx="2" cy="-3" rx="11" ry="3" fill="#cfe9f2" opacity=".6" transform="rotate(-16 2 -3)"/><ellipse cx="2" cy="3" rx="11" ry="3" fill="#cfe9f2" opacity=".6" transform="rotate(16 2 3)"/>',
485    0.09,
486  ) +
487  '<rect x="-16" y="-1.3" width="28" height="2.6" rx="1.3" fill="#2f6fd0"/><path d="M-14 -1.3 v2.6 M-9 -1.3 v2.6 M-4 -1.3 v2.6" stroke="#fff" stroke-width=".8" opacity=".6"/>' +
488  '<circle cx="14" cy="0" r="3.2" fill="#6fd0a0"/><circle cx="15.4" cy="-.8" r="1" fill="#111"/>'
489
490const ant = () =>
491  '<path d="M-3 -1 l-2 3 M0 -1 l0 3 M3 -1 l2 3" stroke="#3b1f12" stroke-width=".8"/>' +
492  '<circle cx="-4" cy="-2" r="2" fill="#5a2a1a"/><circle cx="0" cy="-2.4" r="1.5" fill="#5a2a1a"/><circle cx="4" cy="-2.6" r="2.2" fill="#5a2a1a"/>' +
493  '<path d="M-1 -5 l4 -6 l4 6Z" fill="#5cbf72"><animateTransform attributeName="transform" type="rotate" values="-6 2 -5;6 2 -5;-6 2 -5" dur="1s" repeatCount="indefinite"/></path>'
494
495const kingfisher = () =>
496  '<path d="M-6 -1 L-13 3 L-6 3Z" fill="#2f7fd0"/><ellipse cx="0" cy="0" rx="6.4" ry="4.6" fill="#2f9fd0"/><ellipse cx="0" cy="2" rx="5" ry="2.8" fill="#f08a2f"/>' +
497  '<circle cx="5.6" cy="-3.6" r="3.6" fill="#2f9fd0"/><path d="M8.6 -4 L17 -3.4 L8.6 -2Z" fill="#222"/><circle cx="6.4" cy="-4.2" r=".9" fill="#fff"/><path d="M-4 -2 q4 -5 8 0" fill="#1f7fb0"/>'
498
499const owl = () =>
500  '<ellipse cx="0" cy="-12" rx="9" ry="12" fill="#8a6a4a"/><path d="M-6 -12 q3 4 6 0 q3 4 6 0 M-5 -6 q3 4 5 0 q3 4 5 0" fill="none" stroke="#e8d4b0" stroke-width="1.3"/>' +
501  '<path d="M-6 0 L-6 2 M-3 0 L-3 2 M3 0 L3 2 M6 0 L6 2" stroke="#e8a63c" stroke-width="1.6"/>' +
502  '<g><animateTransform attributeName="transform" type="scale" values="1 1;1 1;.15 1;.15 1;1 1;1 1" keyTimes="0;.3;.34;.5;.54;1" dur="7s" repeatCount="indefinite"/>' +
503  '<circle cx="0" cy="-26" r="8.4" fill="#9a7a56"/><path d="M-7 -32 l-2 -6 l5 3 M7 -32 l2 -6 l-5 3" fill="#9a7a56"/>' +
504  '<circle cx="-3.6" cy="-26" r="3.6" fill="#fff"/><circle cx="3.6" cy="-26" r="3.6" fill="#fff"/><circle cx="-3.6" cy="-26" r="1.8" fill="#f2c94c"/><circle cx="3.6" cy="-26" r="1.8" fill="#f2c94c"/>' +
505  '<circle cx="-3.6" cy="-26" r=".9" fill="#111"/><circle cx="3.6" cy="-26" r=".9" fill="#111"/><path d="M0 -24 l-1.6 3 l3.2 0Z" fill="#f08a2f"/></g>'
506
507const lg = (x: number, k: number, color: string) =>
508  `<g><animateTransform attributeName="transform" type="rotate" values="${k ? -24 : 24} ${x} -8;${k ? 24 : -24} ${x} -8;${k ? -24 : 24} ${x} -8" dur=".9s" repeatCount="indefinite"/><rect x="${x - 2.2}" y="-9" width="4.4" height="11" rx="2.2" fill="${color}"/></g>`
509
510const iguana = () =>
511  '<path d="M-14 -7 q-18 -2 -34 4 q-6 3 -12 2" fill="none" stroke="#5fae4a" stroke-width="5" stroke-linecap="round"/><path d="M-26 -5 l0 -3 M-36 -3 l0 -3 M-46 -1 l0 -3" stroke="#3f7a30" stroke-width="2"/>' +
512  lg(-8, 0, '#5fae4a') + lg(12, 0, '#5fae4a') +
513  '<ellipse cx="0" cy="-8" rx="17" ry="6.6" fill="#6fbf55"/><path d="M-12 -14 l2 -4 l2 4 l2 -4 l2 4 l2 -4 l2 4 l2 -4 l2 4 l2 -4 l2 4" fill="#3f7a30"/>' +
514  lg(-14, 1, '#5fae4a') + lg(8, 1, '#5fae4a') +
515  '<ellipse cx="20" cy="-9" rx="7.4" ry="5" fill="#7ccf62"/><circle cx="21" cy="-11.4" r="1.3" fill="#1c2430"/><path d="M18 -4 q3 7 6 0Z" fill="#e8a63c"/><circle cx="23" cy="-8.4" r=".8" fill="#3f7a30"/>'
516
517const coati = () =>
518  '<path d="M-17 -10 Q-34 -14 -30 -32 Q-27 -42 -20 -36" fill="none" stroke="#8a5a3a" stroke-width="6" stroke-linecap="round"/>' +
519  '<path d="M-24 -14 l5 -4 M-28 -22 l6 -2 M-29 -30 l6 0 M-25 -37 l5 3" stroke="#2a1a10" stroke-width="2.4" stroke-linecap="round"/>' +
520  lg(-12, 0, '#6b4528') + lg(10, 0, '#6b4528') +
521  '<ellipse cx="0" cy="-12" rx="18" ry="6.6" fill="#8a5a3a"/><ellipse cx="0" cy="-9" rx="14" ry="3" fill="#c9a578" opacity=".7"/>' +
522  lg(-8, 1, '#6b4528') + lg(14, 1, '#6b4528') +
523  '<ellipse cx="22" cy="-13" rx="7" ry="5" fill="#8a5a3a"/><path d="M26 -15 L36 -11 L26 -9Z" fill="#a8764c"/><circle cx="36" cy="-11" r="1.8" fill="#fff"/><circle cx="23" cy="-15" r="1.3" fill="#111"/><circle cx="20" cy="-18" r="2" fill="#6b4528"/>'
524
525const wrap = (kind: number, begin: string, body: string) =>
526  `<g data-visitor="${kind}" display="none"><animate attributeName="display" values="inline;inline" calcMode="discrete" dur="${LIFE}s" begin="${begin}"/>` +
527  `<g opacity="0"><animate attributeName="opacity" values="0;1;1;0" keyTimes="0;.05;.93;1" dur="${LIFE}s" begin="${begin}" fill="freeze"/>${body}</g></g>`
528
529const limbsInView = (y0: number, view: number) => LIMBS.filter(l => l.y > y0 + 50 && l.y < y0 + view - 20)
530
531const visitorSvg = (v: Slot, starts: number[], y0: number, view: number, limbAt = -1) => {
532  // every time it is due to appear, shifted by `lag` seconds
533  const B = (lag = 0) => starts.map(t => `${n(t + lag)}s`).join(';')
534  const r = (k: number) => rnd(v.seed + k)
535  const dir: 1 | -1 = r(1) < 0.5 ? 1 : -1
536  const yFly = y0 + 70 + r(2) * Math.max(80, view * 0.55)
537  // a limb inside the view, if there is one
538  const seen = limbsInView(y0, view)
539  const spot = seen.length ? seen[limbAt >= 0 ? limbAt % seen.length : Math.floor(r(3) * seen.length) % seen.length] : undefined
540  const dur = LIFE - 1
541
542  switch (v.kind) {
543    case 0:
544      return wrap(0, B(), flyer(toucan(), yFly, dir, dur, B()))
545    case 1:
546      return wrap(
547        1,
548        B(),
549        flyer(parrot('#e23b3b', '#2f7fd0'), yFly, dir, dur, B()) + flyer(parrot('#f2c94c', '#2fa58a'), yFly + 22, dir, dur, B(-0.7)),
550      )
551    case 2: {
552      const ax = spot ? (spot.side === -1 ? spot.x1 : spot.x0) : 40 + r(4) * 320
553      const ay = spot ? spot.y : y0 + 20
554      const L = 90 + r(5) * 40
555
556      return wrap(
557        2,
558        B(),
559        `<g transform="translate(${n(ax)} ${n(ay)})"><g><animateTransform attributeName="transform" type="rotate" values="-42 0 0;42 0 0;-42 0 0" keyTimes="0;.5;1" calcMode="spline" keySplines="${EASE};${EASE}" dur="4.4s" begin="${B()}" repeatCount="indefinite"/>` +
560          `<path d="M0 0 V${n(L)}" stroke="#2a7d3d" stroke-width="3"/><g transform="translate(0 ${n(L)})">${monkeyHang()}</g></g></g>`,
561      )
562    }
563    case 3: {
564      const l = spot ?? LIMBS[2]
565      const a = l.x0 + 6
566      const z = l.x1 - 6
567      const [s, e, f] = dir === 1 ? [a, z, 1] : [z, a, -1]
568      const mid = s + (e - s) * 0.55
569
570      return wrap(
571        3,
572        B(),
573        `<g><animateTransform attributeName="transform" type="translate" values="${n(s)} ${l.y - 6};${n(mid)} ${l.y - 6};${n(mid)} ${l.y - 6};${n(e)} ${l.y - 6}" keyTimes="0;.45;.66;1" dur="${dur}s" begin="${B()}" fill="freeze"/><g transform="scale(${f} 1)">${jaguar()}</g></g>`,
574      )
575    }
576    case 4: {
577      const x0 = 100 + r(6) * 70
578      const x1 = x0 + 90 + r(7) * 40
579      const [s, e, f] = dir === 1 ? [x0, x1, 1] : [x1, x0, -1]
580      const id = `cc${v.id}`
581
582      return wrap(
583        4,
584        B(),
585        `<clipPath id="${id}"><rect x="-90" y="-28" width="200" height="28"/></clipPath>` +
586          `<g><animateTransform attributeName="transform" type="translate" values="${n(s)} 1382;${n(e)} 1386" dur="${dur}s" begin="${B()}" fill="freeze"/>` +
587          `<g clip-path="url(#${id})"><g transform="scale(${1.3 * f} 1.3)">${croc(3)}</g></g>` +
588          '<ellipse cx="0" cy="1" rx="52" ry="5" fill="none" stroke="#fff" stroke-width="1.2" opacity=".5"/></g>',
589      )
590    }
591    case 5: {
592      const order = dir === 1 ? [0, 1, 2, 3, 4] : [4, 3, 2, 1, 0]
593      const pts = order.slice(0, 4).map(i => PADS[i])
594      let path = `M${pts[0][0]} ${pts[0][1] - 4}`
595      for (let i = 1; i < pts.length; i++) {
596        const [px, py] = pts[i - 1]
597        const [qx, qy] = pts[i]
598        path += ` Q${n((px + qx) / 2)} ${n(Math.min(py, qy) - 38)} ${qx} ${qy - 4}`
599      }
600
601      return wrap(5, B(), `<g><animateMotion path="${path}" dur="${dur - 2}s" begin="${B()}" fill="freeze" calcMode="spline" keyPoints="0;1" keyTimes="0;1" keySplines=".4 0 .6 1"/>${frog()}</g>`)
602    }
603    case 6:
604      return wrap(
605        6,
606        B(),
607        [0, 1, 2, 3, 4]
608          .map(k => {
609            const y = yFly + k * 16 - 30
610            const path = `M-20 ${n(y)} C80 ${n(y - 60)} 160 ${n(y + 60)} 250 ${n(y - 20)} S380 ${n(y + 40)} 430 ${n(y - 30)}`
611
612            return `<g><animateMotion path="${path}" dur="${12 + k}s" begin="${B(- k * 0.5)}" fill="freeze"/>${butterfly(pick(['#3f9be0', '#f08a2f', '#c05fd0', '#f2c94c', '#e24a8d'], k + v.seed))}</g>`
613          })
614          .join(''),
615      )
616    case 8:
617      return wrap(
618        8,
619        B(),
620        [0, 1, 2, 3, 4]
621          .map(k => {
622            const y = yFly + k * 12 - 24
623            const path = `M-30 ${n(y)} C60 ${n(y - 90)} 120 ${n(y + 90)} 200 ${n(y - 30)} S330 ${n(y + 70)} 430 ${n(y - 20)}`
624
625            return `<g><animateMotion path="${path}" dur="${10 + (k % 3)}s" begin="${B(- k * 0.45)}" fill="freeze"/>${bat()}</g>`
626          })
627          .join(''),
628      )
629    case 9: {
630      const l = spot ?? LIMBS[0]
631      const s0 = l.x0 + 24 + r(6) * 30
632
633      return wrap(
634        9,
635        B(),
636        `<g><animateTransform attributeName="transform" type="translate" values="${n(s0)} ${l.y - 8};${n(s0 + 50 * dir)} ${l.y - 8}" dur="${dur}s" begin="${B()}" fill="freeze"/><g transform="scale(${dir} 1)">${chameleon()}</g></g>`,
637      )
638    }
639    case 10: {
640      const ax = spot ? (spot.side === -1 ? spot.x1 : spot.x0) : 40 + r(4) * 320
641      const ay = spot ? spot.y : y0 + 20
642
643      return wrap(
644        10,
645        B(),
646        `<g transform="translate(${n(ax)} ${n(ay)})"><g><animateTransform attributeName="transform" type="rotate" values="-10 0 0;10 0 0;-10 0 0" keyTimes="0;.5;1" calcMode="spline" keySplines="${EASE};${EASE}" dur="3.6s" begin="${B()}" repeatCount="indefinite"/>${snake()}</g></g>`,
647      )
648    }
649    case 11: {
650      const pt = (k: number): Pt => [60 + r(k) * 280, y0 + 110 + r(k + 1) * Math.max(60, view * 0.55)]
651      const [p1, p2, p3] = [pt(20), pt(22), pt(24)]
652      const val = (p: Pt) => `${n(p[0])} ${n(p[1])}`
653
654      return wrap(
655        11,
656        B(),
657        `<g><animateTransform attributeName="transform" type="translate" values="-30 ${n(p1[1])};${val(p1)};${val(p1)};${val(p2)};${val(p2)};${val(p3)};${val(p3)};440 ${n(p3[1] - 30)}" keyTimes="0;.12;.28;.34;.5;.56;.72;1" dur="${dur}s" begin="${B()}" fill="freeze"/>` +
658          `<g><animateTransform attributeName="transform" type="translate" values="0 0;0 -3;0 0;0 2;0 0" dur=".7s" repeatCount="indefinite"/>${hummingbird()}</g></g>`,
659      )
660    }
661    case 12: {
662      const hx = 100 + r(8) * 200
663
664      return wrap(
665        12,
666        B(),
667        `<g transform="translate(${n(hx)} 1394)"><g><animateTransform attributeName="transform" type="translate" values="0 0;${dir * 18} 0" dur="${dur}s" begin="${B()}" fill="freeze"/><g transform="scale(${dir} 1)">${heron(B())}</g></g></g>`,
668      )
669    }
670    case 13: {
671      const y = 1400 + r(9) * 200
672      const [s, e, f] = dir === 1 ? [50, 340, 1] : [340, 50, -1]
673
674      return wrap(13, B(), `<g><animateTransform attributeName="transform" type="translate" values="${s} ${n(y)};${e} ${n(y + 8)}" dur="${dur}s" begin="${B()}" fill="freeze"/><g transform="scale(${f} 1)">${turtle()}</g></g>`)
675    }
676    case 14:
677      return wrap(
678        14,
679        B(),
680        Array.from({ length: 9 }, (_, k) => {
681          const y = 1400 + r(10) * 160 + (k % 3) * 12
682          const [s, e] = dir === 1 ? [40, 360] : [360, 40]
683          const path = `M${s} ${n(y)} C${n(s + (e - s) * 0.3)} ${n(y - 26 + k * 3)} ${n(s + (e - s) * 0.6)} ${n(y + 24 - k * 3)} ${e} ${n(y - 6)}`
684
685          return `<g><animateMotion path="${path}" dur="${dur - 1}s" begin="${B(- (k % 4) * 0.35 - Math.floor(k / 4) * 0.2)}" fill="freeze"/><g transform="scale(${dir} 1)">${neon()}</g></g>`
686        }).join(''),
687      )
688    case 15:
689      return wrap(
690        15,
691        B(),
692        Array.from({ length: 4 }, (_, k) => {
693          const y = 1410 + r(11) * 150 + k * 14
694          const [s, e] = dir === 1 ? [40, 360] : [360, 40]
695          const path = `M${s} ${n(y)} L${e} ${n(y + 10)}`
696
697          return `<g><animateMotion path="${path}" dur="${dur - 1}s" begin="${B(- k * 0.5)}" fill="freeze" calcMode="linear" keyPoints="0;.3;.3;.62;.62;1" keyTimes="0;.15;.3;.55;.7;1"/><g transform="scale(${dir} 1)">${piranha()}</g></g>`
698        }).join(''),
699      )
700    case 16: {
701      const [s, e] = dir === 1 ? [40, 360] : [360, 40]
702      const mid = (s + e) / 2
703      const path = `M${s} 1384 Q${n((s + mid) / 2)} 1352 ${n(mid)} 1384 T${e} 1384`
704
705      return wrap(16, B(), `<g><animateMotion path="${path}" rotate="auto" dur="${dur}s" begin="${B()}" fill="freeze"/><g transform="scale(1 ${dir})">${otter()}</g></g>`)
706    }
707    case 17: {
708      const y = 1480 + r(12) * 150
709      const [s, e, f] = dir === 1 ? [50, 340, 1] : [340, 50, -1]
710
711      return wrap(17, B(), `<g><animateTransform attributeName="transform" type="translate" values="${s} ${y};${e} ${y - 10}" dur="${dur}s" begin="${B()}" fill="freeze"/><g transform="scale(${f} 1)">${ray()}</g></g>`)
712    }
713    case 18: {
714      const pt = (k: number): Pt => [50 + r(k) * 300, Math.max(y0 + 60, 1230 + r(k + 1) * 150)]
715      const [p1, p2, p3, p4] = [pt(30), pt(32), pt(34), pt(36)]
716      const val = (p: Pt) => `${n(p[0])} ${n(p[1])}`
717
718      return wrap(
719        18,
720        B(),
721        `<g><animateTransform attributeName="transform" type="translate" values="-30 ${n(p1[1])};${val(p1)};${val(p1)};${val(p2)};${val(p2)};${val(p3)};${val(p3)};${val(p4)};440 ${n(p4[1])}" keyTimes="0;.1;.2;.26;.4;.46;.6;.66;1" dur="${dur}s" begin="${B()}" fill="freeze"/>` +
722          `<g><animateTransform attributeName="transform" type="translate" values="0 0;0 -2;0 0;0 2;0 0" dur=".5s" repeatCount="indefinite"/>${dragonfly()}</g></g>`,
723      )
724    }
725    case 19: {
726      const l = spot ?? LIMBS[1]
727      const [s0, e0, f] = dir === 1 ? [l.x0 + 10, l.x1 - 10, 1] : [l.x1 - 10, l.x0 + 10, -1]
728
729      return wrap(
730        19,
731        B(),
732        Array.from({ length: 6 }, (_, k) => `<g><animateTransform attributeName="transform" type="translate" values="${s0} ${l.y - 7};${e0} ${l.y - 7}" dur="${dur}s" begin="${B(- k * 0.8)}" fill="freeze"/><g transform="scale(${f} 1)">${ant()}</g></g>`).join(''),
733      )
734    }
735    case 20: {
736      const l = spot ?? LIMBS[1]
737      const f = l.side === -1 ? 1 : -1
738      const tipx = l.side === -1 ? l.x1 : l.x0
739      const tx = tipx - f * 14
740      const py = l.y - 12
741      const dx = Math.min(350, Math.max(50, tx + f * 40))
742      const path = `M${n(tx)} ${n(py)} C${n(tx + f * 20)} ${n(py + 120)} ${n(dx - f * 10)} ${n(1352 - 260)} ${n(dx)} 1352 C${n(dx + f * 8)} ${n(1352 - 100)} ${n(tx + f * 30)} ${n(py + 80)} ${n(tx)} ${n(py)}`
743
744      return wrap(
745        20,
746        B(),
747        `<g><animateMotion path="${path}" dur="${dur}s" begin="${B()}" fill="freeze" calcMode="linear" keyPoints="0;0;.5;.5;1;1" keyTimes="0;.22;.45;.55;.8;1"/><g transform="scale(${f} 1)">${kingfisher()}</g></g>` +
748          `<ellipse cx="${n(dx)}" cy="1352" rx="4" ry="1.6" fill="none" stroke="#fff" stroke-width="1.4" opacity="0"><animate attributeName="opacity" values="0;0;.9;0;0" keyTimes="0;.45;.5;.66;1" dur="${dur}s" begin="${B()}" fill="freeze"/><animateTransform attributeName="transform" type="translate" values="0 0;0 0;0 0" dur="${dur}s" begin="${B()}"/><animate attributeName="rx" values="4;4;4;22;22" keyTimes="0;.45;.5;.66;1" dur="${dur}s" begin="${B()}" fill="freeze"/></ellipse>`,
749      )
750    }
751    case 21: {
752      const l = spot ?? LIMBS[0]
753      const f = l.side === -1 ? 1 : -1
754      const tipx = l.side === -1 ? l.x1 : l.x0
755
756      return wrap(21, B(), `<g transform="translate(${n(tipx - f * 24)} ${l.y - 8})"><g transform="scale(${f} 1)">${owl()}</g></g>`)
757    }
758    case 22: {
759      const l = spot ?? LIMBS[0]
760      const s0 = l.x0 + 40 + r(6) * 20
761
762      return wrap(
763        22,
764        B(),
765        `<g><animateTransform attributeName="transform" type="translate" values="${n(s0)} ${l.y - 6};${n(s0 + 60 * dir)} ${l.y - 6}" dur="${dur}s" begin="${B()}" fill="freeze"/><g transform="scale(${dir} 1)">${iguana()}</g></g>`,
766      )
767    }
768    case 23: {
769      const l = spot ?? LIMBS[0]
770      const a = l.x0 + 30
771      const z = l.x1 - 6
772      const [s0, e0, f] = dir === 1 ? [a, z, 1] : [z, a, -1]
773      const mid = s0 + (e0 - s0) * 0.5
774
775      return wrap(
776        23,
777        B(),
778        `<g><animateTransform attributeName="transform" type="translate" values="${n(s0)} ${l.y - 6};${n(mid)} ${l.y - 6};${n(mid)} ${l.y - 6};${n(e0)} ${l.y - 6}" keyTimes="0;.4;.6;1" dur="${dur}s" begin="${B()}" fill="freeze"/><g transform="scale(${f} 1)">${coati()}</g></g>`,
779      )
780    }
781    default: {
782      const l = spot ?? LIMBS[1]
783      const near = l.side === -1 ? l.x1 - 12 : l.x0 + 12
784      const far = l.side === -1 ? l.x1 - 78 : l.x0 + 78
785
786      return wrap(
787        7,
788        B(),
789        `<g><animateTransform attributeName="transform" type="translate" values="${n(near)} ${l.y + 4};${n(far)} ${l.y + 4}" dur="${dur}s" begin="${B()}" fill="freeze"/>` +
790          `<g><animateTransform attributeName="transform" type="rotate" values="-3 0 0;3 0 0;-3 0 0" dur="4.5s" repeatCount="indefinite"/>${sloth()}</g></g>`,
791      )
792    }
793  }
794}
795
796// ---- the whole picture ---------------------------------------------------------------------------------
797// Looping motion shifts back by the time already elapsed, so redrawing the picture never makes it jump.
798// Tags marked data-once (the sky darkening for work) are left alone and play from the start.
799const retime = (svg: string, elapsed: number) =>
800  svg.replace(/<(animate|animateTransform|animateMotion)\b[^>]*>/g, tag => {
801    if (tag.includes('data-once')) {
802      return tag
803    }
804    const found = / begin="(-?[\d.]+)s"/.exec(tag)
805    const shifted = ` begin="${n((found ? parseFloat(found[1]) : 0) - elapsed)}s"`
806
807    return found ? tag.replace(found[0], shifted) : tag.replace(/^<(\w+)/, `<$1${shifted}`)
808  })
809
810// How far the night has come: [from, to]. A rebuilt picture carries on from where the fade already is.
811const nightSpan = (w: Falls, now: number): [number, number] => {
812  const since = (now - w.startedAt) / 1000
813  const sinceEnd = (now - (w.endedAt ?? 0)) / 1000
814  if (w.working) {
815    return [Math.min(1, since / 1.4), 1]
816  }
817
818  return w.endedAt > 0 && sinceEnd < 1.4 ? [1 - sinceEnd / 1.4, 0] : [0, 0]
819}
820
821const buildScene = (w: Falls, elapsed: number, vh: number, now: number) => {
822  const view = Math.min(H, Math.max(vh, 240))
823  const y0 = H - view
824  const [a0, a1] = nightSpan(w, now)
825
826  const defs =
827    '<defs>' +
828    '<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#8fd3e6"/><stop offset="1" stop-color="#f6efc4"/></linearGradient>' +
829    '<linearGradient id="haze" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#e8f6ef" stop-opacity="0"/><stop offset="1" stop-color="#e8f6ef" stop-opacity=".75"/></linearGradient>' +
830    '<linearGradient id="wall" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#5f8a5c"/><stop offset=".55" stop-color="#456a4a"/><stop offset="1" stop-color="#2c4a3a"/></linearGradient>' +
831    '<linearGradient id="fallw" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#e6fbfb"/><stop offset=".5" stop-color="#8fe0e0"/><stop offset="1" stop-color="#3fb8c8"/></linearGradient>' +
832    '<linearGradient id="basinw" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#7fe6dc"/><stop offset="1" stop-color="#2aa0a8"/></linearGradient>' +
833    '<linearGradient id="pool" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#46d0bd"/><stop offset=".5" stop-color="#1f8f94"/><stop offset="1" stop-color="#0b4b57"/></linearGradient>' +
834    '</defs>'
835
836  const backA =
837    SKYLINE +
838    CANOPY +
839    WALL +
840    VINES +
841    LIMBS.map((l, k) => limb(l, k)).join('') +
842    BASINS.map(([cx, cy, rx], k) => basin(cx, cy, rx, k)).join('') +
843    falls(w.working) +
844    // where each fall leaves its terrace: a lip of foam
845    FALLS.map(([x1, y1, , , w1]) => `<ellipse cx="${x1}" cy="${y1}" rx="${n(w1 / 2 + 4)}" ry="4" fill="#fff" opacity=".7"/>`).join('') +
846    leapers() +
847    FALLS.map(([, , x2, y2], k) => mist(x2, y2, k, w.working)).join('') +
848    BANKS +
849    POOL_BASE
850  const backB = POOL_TOP + mist(198, POOL_Y, 5, w.working) + mist(176, POOL_Y, 6, w.working) + GROWTH
851
852  // Every kind visits at random times for the next 25 minutes. The picture is not redrawn when one arrives,
853  // so nothing flickers; one animal is due at once, so the picture is never empty.
854  const slots: { v: Slot; starts: number[]; limbAt: number }[] = []
855  const nLimbs = Math.max(1, limbsInView(y0, view).length)
856  for (let kind = 0; kind < KINDS; kind++) {
857    for (let k = 0; k < SLOTS; k++) {
858      const starts: number[] = []
859      for (let t = Math.random() * 110; t < HORIZON; t += 80 + Math.random() * 50) starts.push(t)
860      const limbAt = LIMB_KINDS.includes(kind) ? Math.floor(Math.random() * nLimbs) : -1
861      slots.push({ v: { id: kind * SLOTS + k, kind, seed: Math.floor(Math.random() * 1_000_000) }, starts, limbAt })
862    }
863  }
864  slots[Math.floor(Math.random() * slots.length)].starts.unshift(0.4)
865
866  // A limb holds one visitor at a time: walk through every visit in order and drop any that would
867  // overlap an earlier one on the same limb.
868  const freeAt: number[] = Array(nLimbs).fill(-1)
869  slots
870    .flatMap((x, i) => (x.limbAt >= 0 ? x.starts.map(t => ({ i, t })) : []))
871    .sort((a, b) => a.t - b.t)
872    .forEach(({ i, t }) => {
873      const x = slots[i]
874      if (t < freeAt[x.limbAt]) {
875        x.starts = x.starts.filter(s => s !== t)
876      } else {
877        freeAt[x.limbAt] = t + LIFE + 1
878      }
879    })
880  const drawn = slots
881    .filter(x => x.starts.length > 0)
882    .map(x => ({ kind: x.v.kind, svg: visitorSvg(x.v, x.starts, y0, view, x.limbAt) }))
883  const under = drawn.filter(x => isUnder(x.kind)).map(x => x.svg).join('')
884  const air = drawn.filter(x => !isUnder(x.kind)).map(x => x.svg).join('')
885
886  const front = RAYS + FRAME + nightLayer(a0, a1)
887
888  return (
889    `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 ${n(y0)} ${W} ${n(view)}" width="${W}" height="${n(view)}" preserveAspectRatio="xMidYMax slice">${defs}` +
890    `<rect x="0" y="${n(y0)}" width="${W}" height="${n(view)}" fill="#12241c"/>` +
891    retime(backA, elapsed) +
892    under +
893    retime(backB, elapsed) +
894    air +
895    retime(front, elapsed) +
896    '</svg>'
897  )
898}
899// ---scene-end
900
901const TERMINAL_ART: [string, string][] = [
902  ['  ♣  ~~~  ┃┃  ~~~  ♣', '#43a863'],
903  ['     ▟▙  ┃┃┃  ▟▙', '#8a929a'],
904  ['  ♣   ▐  ┃┃┃  ▌   ♣', '#8fd8e4'],
905  ['     ▟▙ ┃┃┃┃ ▟▙', '#8a929a'],
906  ['  ≈≈≈≈≈≈ ◎ ◎ ≈≈≈≈≈≈', '#46d0bd'],
907]
908
909const clock = (ms: number) => {
910  const s = Math.max(0, Math.floor(ms / 1000))
911
912  return `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`
913}
914
915const label = (w: Falls, now: number, T: Words) => (w.working ? T.working(clock(now - w.startedAt), w.tools) : T.idle)
916
917const jungle = atom(
918  { plugin: 'jungle-falls', key: 'calm' } as const,
919  { working: false, prev: false, tools: 0, startedAt: 0, endedAt: 0, tick: 0 } as Falls,
920)
921
922// The picture is built again only when the state it draws changes; the same text keeps it running.
923let t0 = 0
924let cache = { key: '', svg: '' }
925
926export const register: Register = (on, options) => {
927  // `language` is a field of the plugin's userConfig: 'en' (default) or 'ko'
928  const T: Words = STRINGS[options.language === 'ko' ? 'ko' : 'en']
929
930  on('session.start', async ($, e, next) => {
931    t0 = await $.clock.now()
932    await $.command.register({
933      name: 'falls',
934      description: 'Open the jungle waterfall pane',
935    })
936    // Wakes the pane each second while Claude works, for the stopwatch in its top line.
937    $.clock.every(1000, () => {
938      void read($, jungle)
939        .then(r => (r.working ? update($, jungle, v => ({ ...v, tick: v.tick + 1 })) : undefined))
940        .catch(() => {})
941    })
942
943    return next(e)
944  })
945
946  on('command.run', { command: 'falls' }, async $ => {
947    await $.ui.open({ id: PANE, title: T.title })
948
949    return { text: T.opened }
950  })
951
952  // The main loop's turns only: a subagent's run is not Claude at work.
953  on('turn.start', async ($, e, next) => {
954    if (e.agentId === undefined) {
955      const now = await $.clock.now()
956      await update($, jungle, v => ({ ...v, prev: false, working: true, tools: 0, startedAt: now }))
957    }
958
959    return next(e)
960  }).catch(($, e, next) => next(e))
961
962  // Each finished turn draws the picture afresh, with an animal due straight away.
963  on('turn.complete', async ($, e, next) => {
964    if (e.agentId === undefined) {
965      const now = await $.clock.now()
966      await update($, jungle, v => ({ ...v, prev: true, working: false, endedAt: now }))
967    }
968
969    return next(e)
970  }).catch(($, e, next) => next(e))
971
972  on('tool.call', async ($, e, next) => {
973    await update($, jungle, v => ({ ...v, tools: v.tools + 1 }))
974
975    return next(e)
976  }).catch(($, e, next) => next(e))
977
978  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
979    const els = $.ui.resolve(e)
980    const w = await read($, jungle)
981    const now = await $.clock.now()
982    if (t0 === 0) t0 = now
983    const cols = e.props?.bodyColumns ?? 40
984    const rows = e.props?.scroll?.bodyRows ?? 20
985    const line = label(w, now, T)
986
987    if (e.surface !== 'terminal') {
988      const { Box, Text, Svg } = els as ElementTable<'desktop'>
989      // The pane's px: its cells over the code font's advance (about 7.8 px) and line height
990      // (about 19.5 px). Short of the whole picture, the top of the jungle is cut, never the pool.
991      const width = Math.max(160, Math.round(cols * 7.8))
992      const full = Math.round((width * H) / W)
993      const tall = e.props?.scroll?.bodyRows ? Math.round((rows - 1) * 19.5) : full
994      const height = Math.min(full, tall)
995      const [n0, n1] = nightSpan(w, now)
996      const key = `${w.working}|${n0 > 0 && n0 < 1 ? 'f' : n0}|${n1}|${w.startedAt}|${w.endedAt}|${height}|${width}`
997      if (cache.key !== key) cache = { key, svg: buildScene(w, (now - t0) / 1000, (height * W) / width, now) }
998
999      return (
1000        <Box flexDirection="column" justifyContent="flex-end" minHeight={rows}>
1001          <Box justifyContent="center">
1002            <Text bold={w.working} dimColor={!w.working}>
1003              {line}
1004            </Text>
1005          </Box>
1006          <Svg source={cache.svg} alt={T.alt} width={width} height={height} isInteractive />
1007        </Box>
1008      )
1009    }
1010
1011    const { Box, Text } = els as ElementTable<'terminal'>
1012
1013    return (
1014      <Box flexDirection="column">
1015        {TERMINAL_ART.map(([row, color]) => (
1016          <Text color={color}>{row}</Text>
1017        ))}
1018        <Text>{line}</Text>
1019        <Text dimColor>{T.note}</Text>
1020      </Box>
1021    )
1022  })
1023}
1024
types/index.d.ts 15 lines
1export type Falls = {
2  working: boolean
3  prev: boolean
4  tools: number
5  startedAt: number
6  endedAt: number
7  tick: number
8}
9
10declare module 'claude-code' {
11  interface PluginState {
12    'jungle-falls': { calm: Falls }
13  }
14}
15