koi-ascii 0.1.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  Three koi swimming in a pond, drawn with ASCII characters, as a background for
4
4
  your page. WebGL2, a day/night cycle, ripples and wakes that follow the
5
- pointer. Plain JavaScript, ESM only, TypeScript declarations included, no
5
+ pointer, palettes to match your site and an intensity to keep text on top
6
+ readable. Plain JavaScript, ESM only, TypeScript declarations included, no
6
7
  runtime dependencies.
7
8
 
8
9
  **Demo:** https://luckmorecat.github.io/koi-ascii/
@@ -48,6 +49,9 @@ const pond = createKoiPond(container, {
48
49
  interactive: 'window', // 'window' | 'canvas' | 'none'
49
50
  seed: 42, // same seed, same fish; omit for a random pond on every mount
50
51
  fontFamily: '"JetBrains Mono", ui-monospace, Menlo, Consolas, monospace',
52
+ palette: 'pond', // a preset name or a palette object; see "Palettes"
53
+ intensity: 1, // 0 background only … 1 full pond; see "Intensity"
54
+ sky: { moon: true }, // night-sky features; see "Night sky"
51
55
  background: '#06131a', // CSS colour before the first frame and on failure
52
56
  onError: (error) => console.warn(error.code),
53
57
  });
@@ -59,23 +63,37 @@ const pond = createKoiPond(container, {
59
63
  | `interactive` | `'window'` | See "Interaction". |
60
64
  | `seed` | random | Any number; the same seed gives the same fish. |
61
65
  | `fontFamily` | `'"JetBrains Mono", ui-monospace, Menlo, Consolas, monospace'` | The glyph font. See "Fonts". |
62
- | `background` | `'#06131a'` | Shown until the first frame and if the pond fails. |
66
+ | `palette` | `'pond'` | Can change later with `setPalette`. See "Palettes". |
67
+ | `intensity` | `1` | Can change later with `setIntensity`. See "Intensity". |
68
+ | `sky` | stars, fireflies and `darkNight` on; moon and sun off | Can change later with `setSky`. See "Night sky". |
69
+ | `background` | the palette's background | Shown until the first frame and if the pond fails. |
63
70
  | `onError` | logs to the console | See "Failure". |
64
71
 
65
- Only the time can change after creation. For anything else, `destroy()` the
66
- pond and create a new one.
72
+ The time, palette, intensity and sky can change after creation. For anything else,
73
+ `destroy()` the pond and create a new one.
74
+
75
+ Without `background`, the container shows the palette's background (and
76
+ follows `setPalette`), so a light palette never flashes dark before the first
77
+ frame. An explicit `background` only sets that colour; the pond itself is
78
+ always drawn on the palette's background.
67
79
 
68
80
  ### Handle
69
81
 
70
82
  | Method | What it does |
71
83
  | --------------- | ------------------------------------------------------------------------------------------------ |
72
84
  | `setTime(time)` | Switches the time of day live. `{ mode: 'loop' }` without a phase carries on from the current one. |
85
+ | `setPalette(palette)` | Recolours the pond from the next frame on; the fish carry on. |
86
+ | `setIntensity(intensity)` | Fades the pond towards its background from the next frame on. Animate it for a fade. |
87
+ | `setSky(sky)` | Turns night-sky features on or off from the next frame on, with the same values as the `sky` option; the fish carry on. |
73
88
  | `pause()` | Stops simulating and drawing. The last frame stays on screen. |
74
89
  | `resume()` | Carries on from where it paused, without jumping ahead. |
75
90
  | `destroy()` | Stops the pond, removes its canvas, listeners and observers and frees its WebGL context. Safe to call twice, and safe before the pond has started. |
76
91
 
77
92
  Exported types: `KoiPond`, `KoiPondOptions`, `KoiPondTime`,
78
- `KoiPondInteractive`, `KoiPondError`, `KoiPondErrorCode`.
93
+ `KoiPondInteractive`, `KoiPondError`, `KoiPondErrorCode`, `KoiPondPalette`,
94
+ `KoiPondPaletteName`, `KoiPondRampPalette`, `KoiPondRolesPalette`,
95
+ `KoiPondFilterPalette`, `KoiPondColor`, `KoiPondSky`, `KoiPondSkyFeatures`.
96
+ Exported value besides `createKoiPond`: `koiPalettes`.
79
97
 
80
98
  ## Time of day
81
99
 
@@ -97,6 +115,184 @@ pond.setTime({ mode: 'loop', cycle: 120 }); // a 2-minute day from here
97
115
  pond.setTime({ mode: 'clock' });
98
116
  ```
99
117
 
118
+ ## Night sky
119
+
120
+ `sky` picks what the night (and the day's sky) adds to the pond. Stars,
121
+ fireflies and a darker night are on by default; the moon and the sun are
122
+ opt-in:
123
+
124
+ ```js
125
+ createKoiPond(el, { sky: { moon: true } }); // the defaults plus the moon
126
+ ```
127
+
128
+ | Feature | Default | What it does |
129
+ | ----------- | ------- | ------------ |
130
+ | `darkNight` | on | At night, darker water and background and about half the floor caustics, so the night reads as night rather than a tinted day. Fades in and out through the blue hours; by day nothing changes. Dark palettes only: a light background stays light at night. |
131
+ | `stars` | on | Reflected stars at night: a sparse field of small points (`.` `'` `` ` `` `+`, a rare `*`) that twinkle slowly. Fixed to the screen, so they don't drift with the water, but a passing ripple makes them jump. They arrive after dusk and leave before dawn, and never cover a fish. On a light palette they are dark ink, and the motes thin out at night so the stars read. |
132
+ | `fireflies` | on | Fireflies at dusk: 14 points (`.` `+` `*` by brightness) that drift slowly and blink in short pulses, each a single character with a soft glow, drawn over the fish. They come out after the golden hour and are gone by full night; never at dawn. The same `seed` gives the same fireflies. On a light palette there is no glow, so they are the glyph alone. |
133
+ | `moon` | off | The moon's reflection at night: a full disc with darker maria (`.` `:` `o`, a rare `O`) and a drawn rim, dimmer than the fish so it reads as a reflection. It rises up the left side after dusk, crosses the top through the night and is gone before dawn, always clear of the middle of the pond. A ripple through it tears it into `=` and `\|` shards that settle back. Fish swim over it. Opt-in; see "Placement" below. |
134
+ | `sun` | off | The sun's reflection by day: a patch of glitter (`.` `+` `*`) that flashes cell by cell, densest at its centre, gold at noon and amber with a low sun, never brighter than the Ogon's glints. It follows the moon's path: up the left side after dawn, over the top at noon, gone near the top-right corner before dusk, always clear of the middle of the pond. Ripples spread it; fish swim over it. Opt-in, like the moon; it is up all day. See "Placement" below. |
135
+
136
+ - Omitted: the defaults above.
137
+ - `true`: every feature on. `false`: every feature off, which is the pond
138
+ exactly as it looked in 0.1.
139
+ - An object changes only the features it names: `{ moon: true }` adds the
140
+ moon to the defaults, `{ darkNight: false }` keeps the 0.1 night water.
141
+
142
+ Invalid values never throw: a feature that is not `true` or `false` keeps its
143
+ default, an unknown feature is ignored, and anything that is neither a
144
+ boolean nor an object gives the defaults, each with a
145
+ `console.warn('koi-ascii: …')`.
146
+
147
+ ```js
148
+ createKoiPond(el, { sky: false }); // the 0.1 pond
149
+ pond.setSky({ sun: true }); // defaults plus the sun, from the next frame
150
+ ```
151
+
152
+ **Placement.** The moon and the sun are the only large shapes the sky draws,
153
+ and you can't place them. Both keep clear of the middle 40 % of the pond (in
154
+ each axis), where a headline usually sits, and travel along the left side and
155
+ the top edge, ending near the top-right corner. If your content sits there,
156
+ leave them off. Stars and fireflies are single characters spread over the
157
+ whole pond.
158
+
159
+ The features follow the palette (on a light palette they are dark ink and
160
+ have no glow), the `intensity` (they fade with the rest of the pond) and
161
+ reduced motion (see "Good to know").
162
+
163
+ ## Palettes
164
+
165
+ A palette is a preset name or an object whose `mode` picks how it recolours
166
+ the pond:
167
+
168
+ - **`ramp`**: a gradient map. Each glyph's brightness picks a colour along
169
+ `colors` (2 to 5 evenly spaced stops, background first). Suits single-hue
170
+ looks; fish of the same brightness come out the same colour.
171
+ - **`roles`**: named colour slots. Each glyph takes the colour of what it
172
+ draws: `background`, `water` (deep water and ripples are derived from it),
173
+ `highlight` (glints, crests, eyes, stars, the sun's glitter) and, under `fish`, `pale` (white
174
+ scales), `ink` (black patches), `accent` (red markings) and `metallic`
175
+ (Ogon gold). The most control.
176
+ - **`filter`**: today's pond with a filter on top: `background`, `tint`
177
+ (multiplies, default `'#ffffff'`), `saturation` (0 grey … 1 original,
178
+ default 1) and `invert` (dark glyphs on light, hues kept). Keeps the
179
+ pond's own hues, but can't add new ones.
180
+
181
+ ### Presets
182
+
183
+ | Name | Mode | Look |
184
+ | ---------- | -------- | ---------------------------------------------------------- |
185
+ | `pond` | `filter` | The default: today's pond, `{ mode: 'filter' }` with nothing changed. |
186
+ | `mono` | `ramp` | Greys on near-black. |
187
+ | `phosphor` | `ramp` | Green phosphor terminal. |
188
+ | `amber` | `ramp` | Amber terminal. |
189
+ | `paper` | `roles` | Dark ink on off-white, vermilion accent, ochre metallic. Light. |
190
+ | `brand` | `roles` | Indigo, lavender and pink. |
191
+
192
+ ```js
193
+ createKoiPond(el, { palette: 'paper' });
194
+ pond.setPalette('phosphor');
195
+ ```
196
+
197
+ `koiPalettes` holds the preset definitions, frozen (nested objects too), for
198
+ copying and tweaking:
199
+
200
+ ```js
201
+ import { createKoiPond, koiPalettes } from 'koi-ascii';
202
+
203
+ createKoiPond(el, {
204
+ palette: { ...koiPalettes.paper, fish: { ...koiPalettes.paper.fish, accent: '#0a7' } },
205
+ });
206
+ ```
207
+
208
+ ### Your own palette
209
+
210
+ For your own brand colours, write a `roles` palette. The `brand` preset is
211
+ one; swap in your colours:
212
+
213
+ ```js
214
+ createKoiPond(el, {
215
+ palette: {
216
+ mode: 'roles',
217
+ background: '#14102a', // indigo
218
+ water: '#3b2f6e',
219
+ highlight: '#f1edff',
220
+ fish: { pale: '#c8c2ff', ink: '#5a4a9a', accent: '#ff5c8a', metallic: '#ffc2d6' },
221
+ },
222
+ });
223
+ ```
224
+
225
+ Only `background` is required. A missing `highlight` is white on a dark
226
+ background and near-black on a light one; missing `water`, `pale` and `ink`
227
+ are mixed from the background and the highlight; `accent` and `metallic`
228
+ default to the pond's red and gold.
229
+
230
+ ### Colours
231
+
232
+ Every colour is a CSS colour string, parsed by the browser: `'#0a7'`,
233
+ `'rgb(20 16 42)'`, `'hsl(250 45% 11%)'`, `'oklch(0.7 0.15 30)'`, `'teal'`
234
+ and so on. Alpha is ignored. `var()` is not supported: read the custom
235
+ property yourself (`getComputedStyle(document.documentElement).getPropertyValue('--brand')`)
236
+ and pass the value.
237
+
238
+ ### Invalid input
239
+
240
+ A palette never throws and never calls `onError`. Invalid input falls back
241
+ field by field, with a `console.warn` for each:
242
+
243
+ - An unknown preset name or `mode` uses `'pond'`.
244
+ - An invalid colour field is ignored: a `roles` slot is derived as above, a
245
+ filter `tint` is white, a filter `background` is the pond's own.
246
+ - A `roles` palette without a valid `background` uses `'#06131a'`.
247
+ - A `ramp` with fewer than 2 or more than 5 colours, or an invalid one, uses
248
+ `'pond'`.
249
+ - A filter `saturation` outside [0, 1] is clamped; a non-number is 1. A
250
+ non-boolean `invert` is false.
251
+
252
+ ### Light backgrounds and time of day
253
+
254
+ A background whose relative luminance is above 0.4 counts as light. A light
255
+ background stays light at night: the night shows only in the glyphs. The
256
+ glow halo is skipped on a light background.
257
+
258
+ On a dark palette the night is darker by default (the `darkNight` sky
259
+ feature, see "Night sky"): the water and a time-of-day graded background (`pond`, or a
260
+ `filter` with a dark `background`) at about ×0.7, and fewer caustics. A
261
+ `roles` background and a `ramp`'s first stop are never graded, so they keep
262
+ their colour; the water drawn on them still darkens. Turn it
263
+ off with `sky: { darkNight: false }`, or `sky: false` for the 0.1 look.
264
+
265
+ In `ramp` and `roles`, the time of day changes only brightness, so the hues
266
+ stay on your palette. `filter` (and so `pond`) keeps the full sky colours:
267
+ dusk warmth and night blue. A filter without `background` keeps the pond's
268
+ dark, time-of-day background, so `invert` needs a light `background` to read
269
+ as dark glyphs on light:
270
+
271
+ ```js
272
+ pond.setPalette({ mode: 'filter', background: '#f5f2ea', invert: true });
273
+ ```
274
+
275
+ ## Intensity
276
+
277
+ `intensity` mixes the pond with its background, linearly: 0 leaves only the
278
+ background, 1 is the full pond. For text on top, 0.2 to 0.5 keeps body text
279
+ readable with every palette (0.35 is a good start). Values outside [0, 1] are
280
+ clamped with a warning; a non-number is ignored with a warning (the option
281
+ then uses 1, the setter keeps the current value).
282
+
283
+ ```js
284
+ const pond = createKoiPond(el, { palette: 'paper', intensity: 0.35 });
285
+
286
+ // A one-second fade-out:
287
+ const start = performance.now();
288
+ requestAnimationFrame(function fade(now) {
289
+ const t = Math.min(1, (now - start) / 1000);
290
+ pond.setIntensity(0.35 * (1 - t));
291
+ if (t < 1) requestAnimationFrame(fade);
292
+ else pond.pause(); // a fully faded pond keeps rendering until paused
293
+ });
294
+ ```
295
+
100
296
  ## Interaction
101
297
 
102
298
  - `'window'` (default): passive pointer listeners on `window`, mapped into the
@@ -159,12 +355,145 @@ logged to the console. Show your own fallback from `onError` if you want one.
159
355
  hidden, and continues without a jump when it comes back. `pause()` and the
160
356
  automatic pause combine: either one keeps it paused.
161
357
  - While a pond is paused (by `pause()` or because it is off-screen), it draws
162
- nothing: a `setTime` or a resize of the container shows when it resumes.
163
- - With `prefers-reduced-motion: reduce`, the fish swim slower and the water
164
- flickers less.
358
+ nothing: a `setTime`, `setPalette`, `setIntensity`, `setSky` or a resize of the
359
+ container shows when it resumes.
360
+ - With `prefers-reduced-motion: reduce`, the fish swim slower, the water
361
+ flickers less, the stars hold still, the moon's reflection sways slower, the
362
+ sun's glitter flashes slower and the fireflies drift slower and glow softly
363
+ up and down instead of blinking.
165
364
  - On software WebGL (SwiftShader, llvmpipe and the like) the pond renders at
166
365
  a lower resolution to stay smooth.
167
366
 
367
+ ## Demo debug mode
368
+
369
+ The demo page (not the package) has debug tooling behind one query flag,
370
+ `?debug`, on every build including the hosted demo. Without it the page
371
+ loads none of the debug code: no global, no panel, no shortcuts.
372
+
373
+ ```
374
+ https://luckmorecat.github.io/koi-ascii/?debug
375
+ http://localhost:5173/?debug&seed=42&time=fixed&phase=0.8&view=raw
376
+ http://localhost:5173/?palette=paper&intensity=0.35
377
+ http://localhost:5173/?time=fixed&phase=0.95&sky=none
378
+ ```
379
+
380
+ ### URL params
381
+
382
+ The scene lives in the URL next to `?debug`. Changes made from the panel or
383
+ the API are written back (`history.replaceState`), so a reload or a shared
384
+ link shows the same scene. Defaults are left out of the URL.
385
+
386
+ | Param | Values | Notes |
387
+ | ------- | ------------------------- | ------------------------------------------------------------------------------ |
388
+ | `seed` | `0` … `4294967295` | Same seed, same fish. Omitted: random on every load. |
389
+ | `time` | `loop`, `fixed`, `clock` | Default `loop`. |
390
+ | `phase` | `0` … `1` | `fixed`: the phase shown. `loop`: where the day starts (default 0). A running loop never rewrites it; only an explicit set (slider, day-part tick, API) does. |
391
+ | `cycle` | `1` … `86400` | Loop length in seconds, default 600. |
392
+ | `view` | `final`, `raw` | `raw`: glyphs on the plain background, without lighting grade or glow. Needs `?debug`. |
393
+ | `glow` | `on`, `off` | The halo around bright cells. Needs `?debug`. |
394
+ | `palette` | `pond`, `mono`, `phosphor`, `amber`, `paper`, `brand` | A preset (custom palettes can't go in the URL). Default `pond`; anything else is `pond`. |
395
+ | `intensity` | `0` … `1` | Default 1. |
396
+ | `sky` | `all` (or `on`), `none` (or `off`), or the features that are on, comma-separated: `stars,fireflies,darkNight,moon,sun` | Default `stars,fireflies,darkNight` (left out of the URL); `none` is the 0.1 pond. Anything unreadable gives the defaults. |
397
+
398
+ `seed`, `time`, `phase`, `cycle`, `palette`, `intensity` and `sky` also work without
399
+ `?debug`. Pause and
400
+ viewport size are not kept in the URL. The older `?perf` param is gone, and
401
+ `sky` no longer takes `slider|loop`: an old `?sky=slider&phase=0.8` link now
402
+ loops from 0.8 with the default sky; use `?time=fixed&phase=0.8` to hold it.
403
+
404
+ ### Panel and shortcuts
405
+
406
+ `?debug` opens a panel (Perf, Time, Render, Scene, Capture) whose controls are
407
+ generated from the API's tools, so everything it does an agent can do too,
408
+ and it follows changes made through the API. The Render section has a preset
409
+ dropdown, an intensity slider and a toggle per sky feature. Shortcuts work only with
410
+ `?debug`, without modifier keys, and not while typing in a text or number box:
411
+
412
+ | Key | Does |
413
+ | ------- | -------------------- |
414
+ | `P` | show / hide the panel |
415
+ | `R` | toggle the raw view |
416
+ | `Space` | pause / resume |
417
+ | `.` | step one frame |
418
+
419
+ ### `window.koi` for agents and humans
420
+
421
+ With `?debug` the page defines `window.koi`, a self-describing tool registry
422
+ in the style of MCP, meant to be driven through browser JS evaluation (for
423
+ example Playwright's `page.evaluate`). The console prints a pointer to
424
+ `await koi.help()` on load.
425
+
426
+ ```js
427
+ await page.waitForFunction(() => window.koi); // the debug chunk loads on demand
428
+ await page.evaluate(() => koi.ready); // the pond is up
429
+ console.log(await page.evaluate(() => koi.help()));
430
+ ```
431
+
432
+ - `koi.tools()`: every tool as `{ name, description, inputSchema, annotations }`.
433
+ `inputSchema` is a JSON Schema subset: `type: 'object'` with `properties`,
434
+ an optional `required` list and `additionalProperties: false` (unknown
435
+ arguments are rejected). Each property has a `description` and is a
436
+ `number` / `integer` (optional `minimum`, `maximum`), a `boolean` or a
437
+ `string` with an `enum`, with an optional `default` (the value used when
438
+ the argument is left out). `annotations` holds:
439
+ `section` (the panel section), `readOnly` (the call changes nothing),
440
+ `live` (each argument applies on its own, so the panel applies controls
441
+ as they change), `reflects` (the `getState()` key that holds each
442
+ argument's current value) and `shortcut` (`{ key, does }`).
443
+ - `koi.call(name, args)`: always resolves, never rejects, with
444
+ `{ ok: true, result }` or `{ ok: false, error: { code, message } }`. Codes:
445
+ `unknown-tool`, `invalid-args` (the message names the path, e.g.
446
+ `args.phase: must be at most 1, got 2`), `not-ready`, `context-lost`,
447
+ `pond-failed`, `internal`. Results are plain JSON.
448
+ - `koi.<tool>(args)`: the same call, resolving to `result` and rejecting
449
+ with the error object.
450
+ - `koi.ready`: resolves once the current pond is up. It rejects with
451
+ `{ code, message }`: `pond-failed` if the pond can't start, `not-ready`
452
+ if a `reseed` replaced it before it was up. Each `reseed` gives a new
453
+ promise, so read `koi.ready` again after one.
454
+ - `koi.on('change', fn)` after any call that changes state, with
455
+ `{ tool, state }`; `koi.on('frame', fn)` per drawn frame, including frames
456
+ drawn by `step` (marked `stepped: true`, and left out of `getPerf`). Both return an
457
+ unsubscribe function; `koi.off(event, fn)` works too. The last write wins.
458
+ - `koi.help()`: every tool with its description, arguments and shortcut.
459
+
460
+ | Tool | Args | Does |
461
+ | ------------- | ----------------------------- | ---------------------------------------------------------------------- |
462
+ | `getState` | | The whole state: ready, paused, frame counter, time, render (view, glow, palette, intensity), sky (each feature), seed, viewport, canvas size. |
463
+ | `setTime` | `mode?`, `phase?`, `cycle?`, `hold?` | Time of day; `hold` keeps the phase until a call with `hold: false`. |
464
+ | `setDayPart` | `part` | Jumps to a lighting keyframe (`night`, `blue-hour-am`, … `noon` …). |
465
+ | `getPerf` | | fps, 1% low, frame / CPU / GPU ms (GPU `null` without the timer extension), canvas size, GPU string, software flag, seed. |
466
+ | `profile` | `frames?` (400) | GPU µs per pass (`scene`, `glow`, `ascii`, `fly`; `frame` is all of them), median and p90, from timer queries around each draw over `frames` redraws of the paused frame. `{ gpuTimer: false }` without the timer extension. |
467
+ | `setRender` | `view?`, `glow?` | `final` / `raw` view and the glow halo. The raw view ignores the palette and intensity. |
468
+ | `setPalette` | `palette` | Switches to a preset (`pond`, `mono`, `phosphor`, `amber`, `paper`, `brand`). |
469
+ | `setIntensity` | `intensity` | 0 background only … 1 full pond. |
470
+ | `setSky` | `stars?`, `fireflies?`, `darkNight?`, `moon?`, `sun?` | Turns sky features on or off; omitted ones keep their value. |
471
+ | `reseed` | `seed?` | Remounts the pond with a seed (random if omitted), as on a fresh load of the URL. |
472
+ | `getFish` | | Each fish's variety, position, heading, speed, depth, behaviour. |
473
+ | `pause`, `resume` | | Stops and restarts the frame loop (no catch-up after a pause). |
474
+ | `step` | `frames?` (1), `dt?` (1/60) | Pauses, then simulates and draws the frames synchronously, so it works in a background tab. |
475
+ | `capture` | | Redraws the current state and returns `{ dataUrl, width, height }` (PNG). |
476
+ | `setViewport` | `width?`, `height?`, `dpr?` | Pins the pond to a size in CSS px (and a pixel ratio); no args fills the window again. |
477
+ | `setPanel` | `visible?` | Shows or hides the panel; no args toggles. |
478
+
479
+ A reproducible frame, identical on every run on the same machine:
480
+
481
+ ```js
482
+ await koi.setViewport({ width: 800, height: 600, dpr: 1 });
483
+ await koi.setTime({ mode: 'fixed', phase: 0.5 });
484
+ await koi.reseed({ seed: 42 });
485
+ await koi.step({ frames: 120 });
486
+ const { dataUrl } = await koi.capture();
487
+ ```
488
+
489
+ Two tools measure speed. `getPerf` is throughput of the running loop:
490
+ requestAnimationFrame fps and frame ms, with GPU ms for the whole frame.
491
+ `profile` is GPU time per pass: it pauses, redraws the same frame with a
492
+ timer query around every draw and gives the median and p90 per pass, so it
493
+ shows which pass a change made slower. To compare a change with another
494
+ revision, `npm run perf -- '?debug' --base main` profiles both side by side
495
+ (options at the top of `scripts/perf.mjs`).
496
+
168
497
  ## License
169
498
 
170
499
  [MIT](./LICENSE)
package/dist/index.d.ts CHANGED
@@ -33,6 +33,73 @@ export interface KoiPondError extends Error {
33
33
  readonly name: 'KoiPondError';
34
34
  readonly code: KoiPondErrorCode;
35
35
  }
36
+ /** Any CSS colour, parsed by the browser. Alpha is ignored; `var()` is not supported. */
37
+ export type KoiPondColor = string;
38
+ export type KoiPondPaletteName = 'pond' | 'mono' | 'phosphor' | 'amber' | 'paper' | 'brand';
39
+ /** Gradient map: brightness picks a colour along `colors`, background first. */
40
+ export interface KoiPondRampPalette {
41
+ mode: 'ramp';
42
+ /** 2 to 5 stops, evenly spaced. */
43
+ colors: KoiPondColor[];
44
+ }
45
+ /** Named slots: each glyph takes the colour of what it draws. */
46
+ export interface KoiPondRolesPalette {
47
+ mode: 'roles';
48
+ background: KoiPondColor;
49
+ /** Deep water and ripples are derived from it. */
50
+ water?: KoiPondColor;
51
+ /** Glints, crests, eyes. */
52
+ highlight?: KoiPondColor;
53
+ fish?: {
54
+ /** White scales. */
55
+ pale?: KoiPondColor;
56
+ /** Black patches (Showa sumi). */
57
+ ink?: KoiPondColor;
58
+ /** Red markings (Kohaku/Showa hi). */
59
+ accent?: KoiPondColor;
60
+ /** Ogon gold. */
61
+ metallic?: KoiPondColor;
62
+ };
63
+ }
64
+ /** Colour filter over today's pond; keeps its hues. */
65
+ export interface KoiPondFilterPalette {
66
+ mode: 'filter';
67
+ background?: KoiPondColor;
68
+ /** Default `'#ffffff'`. */
69
+ tint?: KoiPondColor;
70
+ /** 0 grey … 1 original (default). */
71
+ saturation?: number;
72
+ /** Dark glyphs on light, hues kept. */
73
+ invert?: boolean;
74
+ }
75
+ /**
76
+ * A preset name or a palette object; `mode` picks how it recolours the pond.
77
+ * Invalid fields fall back one by one with a `console.warn` (never a throw);
78
+ * an unknown preset or mode falls back to `'pond'`.
79
+ */
80
+ export type KoiPondPalette = KoiPondPaletteName | KoiPondRampPalette | KoiPondRolesPalette | KoiPondFilterPalette;
81
+ /**
82
+ * Night-sky features, each on or off. Omitted fields keep their defaults:
83
+ * `stars`, `fireflies` and `darkNight` on, `moon` and `sun` off.
84
+ */
85
+ export interface KoiPondSkyFeatures {
86
+ /** Reflected stars at night. */
87
+ stars?: boolean;
88
+ /** Fireflies at dusk, over the fish. */
89
+ fireflies?: boolean;
90
+ /** Darker water, fewer caustics and a darker background at night, on dark palettes only. */
91
+ darkNight?: boolean;
92
+ /** The moon's reflection at night. */
93
+ moon?: boolean;
94
+ /** The sun's reflection on the water by day, as glitter. */
95
+ sun?: boolean;
96
+ }
97
+ /**
98
+ * The night sky. `true` turns every feature on, `false` every one off (the
99
+ * pond as it was in 0.1); an object changes only the features it names.
100
+ * Invalid values fall back one by one with a `console.warn` (never a throw).
101
+ */
102
+ export type KoiPondSky = boolean | KoiPondSkyFeatures;
36
103
  export interface KoiPondOptions {
37
104
  /** Default `{ mode: 'loop', cycle: 600, phase: 0 }`. */
38
105
  time?: KoiPondTime;
@@ -45,7 +112,16 @@ export interface KoiPondOptions {
45
112
  * face yourself. Default `'"JetBrains Mono", ui-monospace, Menlo, Consolas, monospace'`.
46
113
  */
47
114
  fontFamily?: string;
48
- /** CSS colour shown before the first frame and if the pond fails. Default `'#06131a'`. */
115
+ /** Default `'pond'`. */
116
+ palette?: KoiPondPalette;
117
+ /** 0 leaves only the background, 1 is the full pond (linear). Default 1. */
118
+ intensity?: number;
119
+ /** Default: stars, fireflies and the darker night on; moon and sun off. */
120
+ sky?: KoiPondSky;
121
+ /**
122
+ * CSS colour shown before the first frame and if the pond fails. Default:
123
+ * the palette's background (and it follows `setPalette`).
124
+ */
49
125
  background?: string;
50
126
  /** Called (asynchronously) when WebGL2 is missing or the renderer fails. */
51
127
  onError?: (error: KoiPondError) => void;
@@ -53,12 +129,39 @@ export interface KoiPondOptions {
53
129
  export interface KoiPond {
54
130
  /** Changes the time of day live. A loop without `phase` carries on from the current phase. */
55
131
  setTime(time: KoiPondTime): void;
132
+ /** Changes the palette from the next frame on; the fish carry on. */
133
+ setPalette(palette: KoiPondPalette): void;
134
+ /** Changes the intensity from the next frame on; animate it for a fade. */
135
+ setIntensity(intensity: number): void;
136
+ /** Changes the night-sky features from the next frame on, as the `sky` option would; the fish carry on. */
137
+ setSky(sky: KoiPondSky): void;
56
138
  /** Stops simulating and drawing until `resume()`; combines with the off-screen pause. */
57
139
  pause(): void;
58
140
  resume(): void;
59
141
  /** Stops the pond, removes its canvas and listeners and frees the WebGL context. Safe to call twice. */
60
142
  destroy(): void;
61
143
  }
144
+ /**
145
+ * A roles preset as frozen in `koiPalettes`, `fish` included. Still assignable
146
+ * to `KoiPondRolesPalette`. (A ramp's `colors` stays typed as a plain array so
147
+ * a preset can be passed as a palette, but it is frozen too.)
148
+ */
149
+ type FrozenRoles = Readonly<KoiPondRolesPalette> & {
150
+ readonly fish?: Readonly<NonNullable<KoiPondRolesPalette['fish']>>;
151
+ };
152
+ /**
153
+ * Frozen preset definitions, for copying and tweaking:
154
+ * `{ ...koiPalettes.paper, fish: { ...koiPalettes.paper.fish, accent: '#0a7' } }`.
155
+ * Each preset keeps its own palette type, so its fields can be spread.
156
+ */
157
+ export declare const koiPalettes: {
158
+ readonly pond: Readonly<KoiPondFilterPalette>;
159
+ readonly mono: Readonly<KoiPondRampPalette>;
160
+ readonly phosphor: Readonly<KoiPondRampPalette>;
161
+ readonly amber: Readonly<KoiPondRampPalette>;
162
+ readonly paper: FrozenRoles;
163
+ readonly brand: FrozenRoles;
164
+ };
62
165
  /**
63
166
  * Mounts a koi pond into `container`: appends a canvas that fills it (100% ×
64
167
  * 100%) and starts once the font is ready. Size and position the container
@@ -66,3 +169,4 @@ export interface KoiPond {
66
169
  * anywhere.
67
170
  */
68
171
  export declare function createKoiPond(container: HTMLElement, options?: KoiPondOptions | null): KoiPond;
172
+ export {};