@promptctl/rich-js 0.23.0 → 0.24.0
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/dist/core/clock.d.ts +9 -1
- package/dist/core/clock.js +22 -2
- package/dist/core/color.d.ts +10 -0
- package/dist/core/color.js +36 -1
- package/dist/core/easing.d.ts +31 -0
- package/dist/core/easing.js +106 -1
- package/dist/core/segment.d.ts +10 -0
- package/dist/core/segment.js +15 -0
- package/dist/core/strip.d.ts +104 -27
- package/dist/core/strip.js +231 -124
- package/dist/core/style.d.ts +18 -1
- package/dist/core/style.js +32 -3
- package/dist/core/text.js +4 -1
- package/dist/host/app.js +2 -4
- package/dist/host/index.d.ts +1 -1
- package/dist/host/input.d.ts +37 -13
- package/dist/host/input.js +146 -56
- package/dist/host/terminal-host.d.ts +16 -0
- package/dist/host/terminal-host.js +1 -0
- package/dist/index.d.ts +11 -9
- package/dist/index.js +5 -4
- package/dist/node/terminal-host.d.ts +8 -1
- package/dist/node/terminal-host.js +2 -0
- package/dist/renderables/effect.d.ts +78 -10
- package/dist/renderables/effect.js +143 -39
- package/dist/renderables/effects.d.ts +73 -26
- package/dist/renderables/effects.js +412 -92
- package/dist/renderables/embed.d.ts +10 -0
- package/dist/renderables/embed.js +15 -7
- package/dist/renderables/frames.d.ts +95 -0
- package/dist/renderables/frames.js +106 -0
- package/dist/renderables/live.d.ts +23 -7
- package/dist/renderables/live.js +68 -34
- package/dist/renderables/markdown.d.ts +5 -2
- package/dist/renderables/markdown.js +210 -19
- package/dist/renderables/progress.d.ts +1 -1
- package/dist/renderables/progress.js +3 -1
- package/dist/renderables/progressBar.js +3 -3
- package/dist/renderables/spinner.d.ts +62 -28
- package/dist/renderables/spinner.js +99 -52
- package/dist/renderables/status.d.ts +8 -3
- package/dist/renderables/status.js +8 -5
- package/dist/template-bindings/animation-funcs.d.ts +37 -0
- package/dist/template-bindings/animation-funcs.js +83 -0
- package/dist/template-bindings/color-funcs.d.ts +13 -0
- package/dist/template-bindings/color-funcs.js +1 -1
- package/dist/template-bindings/helpers.d.ts +6 -0
- package/dist/template-bindings/helpers.js +12 -3
- package/dist/template-bindings/index.d.ts +6 -2
- package/dist/template-bindings/index.js +6 -2
- package/package.json +1 -1
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
* a slow glow, sunlight moving on water, the colours of daylight, fireflies, ink
|
|
11
11
|
* blooming in water, mist lifting — because a motion the eye recognises
|
|
12
12
|
* reads as calm and alive, where a motion it has to learn reads as a widget.
|
|
13
|
-
*
|
|
13
|
+
* One effect here is not ambience but a signal, and keeps none of this on
|
|
14
|
+
* purpose: `steppedInk` steps its ink through colours on a beat, the news
|
|
15
|
+
* that something is at its limit, and jumps from slot to slot where the
|
|
16
|
+
* loops hold still. What makes the loops' motions familiar is what every
|
|
17
|
+
* curve of theirs keeps:
|
|
14
18
|
*
|
|
15
19
|
* - Nothing jumps. Every curve is continuous in time, built from smooth
|
|
16
20
|
* noise (`core/noise.ts`) and smooth waveforms, so at one frame a second a cell
|
|
@@ -42,12 +46,13 @@
|
|
|
42
46
|
* cards runs this file's own declarations as the program a visitor edits
|
|
43
47
|
* (`examples/effects-playground/programs.ts`), on the published library.
|
|
44
48
|
*/
|
|
45
|
-
import { ColorDepth, ColorRgba, contrastRatio } from "../core/color.js";
|
|
46
|
-
import { EASES, Phase } from "../core/easing.js";
|
|
49
|
+
import { ColorDepth, ColorRgba, ColorSpec, contrastRatio, relativeLuminance } from "../core/color.js";
|
|
50
|
+
import { EASES, Phase, beat } from "../core/easing.js";
|
|
47
51
|
import { fbm, hash, noise, smoothstep } from "../core/noise.js";
|
|
48
52
|
import { Memo } from "../core/memo.js";
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
53
|
+
import { Oklch } from "../core/oklch.js";
|
|
54
|
+
import { ensureContrast } from "../themes/colorMath.js";
|
|
55
|
+
import { Effected, nextChangeOf, withNextChange } from "./effect.js";
|
|
51
56
|
/** The contrast no touch takes a cell below, unless it rested below it: WCAG AA for body text. */
|
|
52
57
|
const LEGIBLE = 4.5;
|
|
53
58
|
/**
|
|
@@ -61,6 +66,16 @@ const LEVELS = 48;
|
|
|
61
66
|
const STRENGTHS = Array.from({ length: LEVELS }, (_, i) => (i + 1) / LEVELS);
|
|
62
67
|
/** What 8-bit rounding of an OKLCH round trip can cost a contrast ratio. */
|
|
63
68
|
const ROUNDING = 0.01;
|
|
69
|
+
/**
|
|
70
|
+
* The contrast no loop draws `pair` below at `depth`: its resting contrast or
|
|
71
|
+
* `LEGIBLE`, whichever is lower, less what truecolor's rounding can cost.
|
|
72
|
+
* Below truecolor every colour written is one the depth holds exactly, so
|
|
73
|
+
* there is nothing to forgive. [LAW:one-source-of-truth] The floor `shares`
|
|
74
|
+
* settles the light against and `wheel` writes its turn above.
|
|
75
|
+
*/
|
|
76
|
+
function floorOf(pair, depth) {
|
|
77
|
+
return Math.min(contrastRatio(pair.fg, pair.bg), LEGIBLE) - (depth === ColorDepth.TRUECOLOR ? ROUNDING : 0);
|
|
78
|
+
}
|
|
64
79
|
const SIDES = ["fg", "bg"];
|
|
65
80
|
/**
|
|
66
81
|
* How much of `touch` each of `colors` (hex) can take: the largest share in
|
|
@@ -86,8 +101,6 @@ export function shares({ pairs, depth, theme }, colors, touch) {
|
|
|
86
101
|
const hex = own(pair, side);
|
|
87
102
|
return hex === undefined ? [] : [[hex, pair[side]]];
|
|
88
103
|
})));
|
|
89
|
-
// Below truecolor every colour written is one the depth holds exactly, so only truecolor's rounding has a cost to forgive.
|
|
90
|
-
const slack = depth === ColorDepth.TRUECOLOR ? ROUNDING : 0;
|
|
91
104
|
// A colour no pair writes keeps no word legible, so nothing bounds its share:
|
|
92
105
|
// a frame drawing a lit colour only under blanks and seams, or not at all,
|
|
93
106
|
// is a frame like any other.
|
|
@@ -104,7 +117,7 @@ export function shares({ pairs, depth, theme }, colors, touch) {
|
|
|
104
117
|
});
|
|
105
118
|
// [LAW:one-source-of-truth] A pair reads in the colours `Effected` writes it in, touched at each side's share.
|
|
106
119
|
const holds = (share) => facing.every(([pair, mine, theirs]) => {
|
|
107
|
-
const floor =
|
|
120
|
+
const floor = floorOf(pair, depth);
|
|
108
121
|
const at = (side, w) => touchedAt(touch, pair[side], (side === mine ? share : theirs) * w);
|
|
109
122
|
return STRENGTHS.every((w) => {
|
|
110
123
|
const shown = Effected.shows(pair, { fg: at("fg", w), bg: at("bg", w) }, depth, theme);
|
|
@@ -124,9 +137,10 @@ export function shares({ pairs, depth, theme }, colors, touch) {
|
|
|
124
137
|
* A loop as an `Effect` on the element's own colours, by hex, wherever a cell
|
|
125
138
|
* shows them, each touched at its share of the cell's strength: the share
|
|
126
139
|
* `shares` settles for it against the pairs the element draws its words in
|
|
127
|
-
* (`Drawn`), touched as `Effected` writes them at the frame's depth —
|
|
128
|
-
*
|
|
129
|
-
* contrast it spends is that colour's
|
|
140
|
+
* (`Drawn`), touched as `Effected` writes them at the frame's depth — at 256
|
|
141
|
+
* colours a touched colour lands on a colour the slot can write, and the
|
|
142
|
+
* contrast it spends is that colour's; on a sixteen-colour terminal nothing
|
|
143
|
+
* moves (`stillOnSlots`). `colors` are the ones lit, by default every colour the element
|
|
130
144
|
* sets itself. A powerline strip's fill is the ground of its cells and the
|
|
131
145
|
* ink of the seam glyphs between them, so the arrow moves with the cell it
|
|
132
146
|
* points out of. A colour the terminal supplies (`EffectCell.terminal`) is
|
|
@@ -140,12 +154,27 @@ export function onColors(loop, colors) {
|
|
|
140
154
|
const settled = new WeakMap();
|
|
141
155
|
const settle = (drawn) => {
|
|
142
156
|
const lit = colors ?? drawn.colors;
|
|
143
|
-
const key = [drawn
|
|
157
|
+
const key = [drawnKey(drawn), "|", ...lit].join(" ");
|
|
144
158
|
const memo = settled.get(drawn.theme) ?? new Memo();
|
|
145
159
|
settled.set(drawn.theme, memo);
|
|
146
160
|
return memo.get(key, () => shares(drawn, lit, loop.touch));
|
|
147
161
|
};
|
|
148
|
-
return atShares(loop, settle);
|
|
162
|
+
return stillOnSlots(atShares(loop, settle));
|
|
163
|
+
}
|
|
164
|
+
/** What a frame draws, as a key: its depth and its pairs, which its `colors` and `fills` are read off. */
|
|
165
|
+
function drawnKey({ depth, pairs }) {
|
|
166
|
+
return [depth, ...pairs.map(pairKey)].join(" ");
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* A pair as a key: two pairs the screen shows alike, from slots alike, are
|
|
170
|
+
* one — the spelling `Drawn.pairs` is made distinct by (`./effect.ts`), and
|
|
171
|
+
* what every memo here over a frame's pairs is keyed on. Spelled here rather
|
|
172
|
+
* than imported: a playground card runs this file on the published library
|
|
173
|
+
* (header), and a name this file imports from `./effect.js` is one the
|
|
174
|
+
* package exports.
|
|
175
|
+
*/
|
|
176
|
+
function pairKey({ fg, bg, terminal, reverse }) {
|
|
177
|
+
return `${fg.hex}/${bg.hex}/${terminal.fg}/${terminal.bg}/${reverse}`;
|
|
149
178
|
}
|
|
150
179
|
/**
|
|
151
180
|
* A loop as an `Effect` on colours by hex, each touched at the share
|
|
@@ -155,7 +184,7 @@ export function onColors(loop, colors) {
|
|
|
155
184
|
export function atShares(loop, share) {
|
|
156
185
|
return (t, drawn) => {
|
|
157
186
|
const shared = share(drawn);
|
|
158
|
-
const field = loop.field(t);
|
|
187
|
+
const field = loop.field(t, drawn);
|
|
159
188
|
return ({ fg, bg }, cell) => {
|
|
160
189
|
const w = Math.round(clamp01(field(cell)) * LEVELS) / LEVELS;
|
|
161
190
|
const moved = (color, terminal) => touchedAt(loop.touch, color, (terminal ? 0 : (shared.get(color.hex) ?? 0)) * w);
|
|
@@ -163,6 +192,33 @@ export function atShares(loop, share) {
|
|
|
163
192
|
};
|
|
164
193
|
};
|
|
165
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* `loop` as drawn at the frame's depth: where the depth writes only the
|
|
197
|
+
* sixteen ANSI slots, or no colour, a move that shows at all is a jump to
|
|
198
|
+
* another slot, and a light on an element never jumps — so there it
|
|
199
|
+
* holds every cell still, and anywhere else it moves. [LAW:single-enforcer]
|
|
200
|
+
* `onColors` and `wheel` draw through this, so every loop in the catalogue
|
|
201
|
+
* meets the sixteen-colour terminal here. `atShares` does not: a progress
|
|
202
|
+
* bar's pulse is the news that work is going on, and flashes a slot as
|
|
203
|
+
* Rich's does; nor does `steppedInk`, a signal, which steps slot to slot.
|
|
204
|
+
* A frame held still never changes, and says so, so a host drawing the
|
|
205
|
+
* loop on such a terminal draws it once.
|
|
206
|
+
*/
|
|
207
|
+
function stillOnSlots(loop) {
|
|
208
|
+
return withNextChange((t, drawn) => (onlySlots(drawn.depth) ? (colors) => colors : loop(t, drawn)), (t, drawn) => (onlySlots(drawn.depth) ? Number.POSITIVE_INFINITY : nextChangeOf(loop, t, drawn)));
|
|
209
|
+
}
|
|
210
|
+
/** Whether `depth` writes nothing between the slots it has: the sixteen, at either sixteen-colour depth, or none. */
|
|
211
|
+
function onlySlots(depth) {
|
|
212
|
+
switch (depth) {
|
|
213
|
+
case ColorDepth.DEFAULT:
|
|
214
|
+
case ColorDepth.STANDARD:
|
|
215
|
+
case ColorDepth.WINDOWS:
|
|
216
|
+
return true;
|
|
217
|
+
case ColorDepth.EIGHT_BIT:
|
|
218
|
+
case ColorDepth.TRUECOLOR:
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
166
222
|
/**
|
|
167
223
|
* `color` under `touch` at strength `w`, and at strength 0 the colour itself,
|
|
168
224
|
* exactly: an OKLCH round trip can land a step off, and at 16 colours a step
|
|
@@ -313,8 +369,9 @@ const bump = (d) => (Math.abs(d) >= 1 ? 0 : (1 - d * d) ** 3);
|
|
|
313
369
|
/**
|
|
314
370
|
* Shimmer, as sunlight moving across water. Swing: how far into `light` the
|
|
315
371
|
* brightest glint goes, 0–1. A soft band of light, `width` columns either
|
|
316
|
-
* side of its centre, crosses
|
|
317
|
-
*
|
|
372
|
+
* side of its centre, crosses the element from the first column a frame
|
|
373
|
+
* lists (`Drawn.cells`) to the last, entering and leaving fully off it,
|
|
374
|
+
* `STRIDE` columns in about three quarters of a period. Its passes
|
|
318
375
|
* come the way light comes and goes on water as clouds move: while the sun
|
|
319
376
|
* is out they are bright and follow close on one another, one sometimes
|
|
320
377
|
* catching the last; under cloud they thin to faint ones with the row still
|
|
@@ -324,26 +381,26 @@ const bump = (d) => (Math.abs(d) >= 1 ? 0 : (1 - d * d) ** 3);
|
|
|
324
381
|
* on a slow current and re-forming as they go, so the glints dance while
|
|
325
382
|
* the band glides. Before the first pass enters, the row is untouched.
|
|
326
383
|
*/
|
|
327
|
-
export function shimmer(curve,
|
|
384
|
+
export function shimmer(curve, width, glow, z) {
|
|
328
385
|
requireCurve(curve);
|
|
329
386
|
const P = curve.seconds;
|
|
330
|
-
const across = span + 2 * width;
|
|
331
387
|
// Passes are events on slots of half a period. Whether slot `j` holds one,
|
|
332
388
|
// and how bright, is the sky's: slow noise over the slots, so sunny and
|
|
333
389
|
// cloudy spells each last a few periods, and a pass is likelier and
|
|
334
390
|
// brighter the clearer the sky.
|
|
335
391
|
const SLOT = P / 2;
|
|
336
392
|
const sky = (j) => smoothstep(-0.4, 0.4, noise(j / 7, 3.3, 5.5 + z));
|
|
337
|
-
// Pass `j` at `t
|
|
338
|
-
// the
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
393
|
+
// Pass `j` at `t`, over a crossing `across` columns long — the element's
|
|
394
|
+
// and the band's either side: how bright its band is and where its centre
|
|
395
|
+
// stands from the element's first column, off the element before it sets
|
|
396
|
+
// off and once it is done. It sets off somewhere in the first `SETS_OFF`
|
|
397
|
+
// of its slot and carries its centre `STRIDE` columns in `LEAST` to
|
|
398
|
+
// `LEAST + SPREAD` periods, give or take its drift, so a pass's centre moves
|
|
399
|
+
// at the same pace however long the crossing.
|
|
342
400
|
const SETS_OFF = 0.7;
|
|
343
401
|
const LEAST = 0.51;
|
|
344
402
|
const SPREAD = 0.4;
|
|
345
|
-
const
|
|
346
|
-
const pass = (j, t) => {
|
|
403
|
+
const pass = (j, t, across) => {
|
|
347
404
|
const clear = sky(j);
|
|
348
405
|
// Even under cloud about one slot in three holds a pass, a faint one, so a
|
|
349
406
|
// still spell lasts a period or so, the way a cloud's shadow does, and
|
|
@@ -353,29 +410,35 @@ export function shimmer(curve, span, width, glow, z) {
|
|
|
353
410
|
const crossing = ((P * across) / STRIDE) * (LEAST + SPREAD * hash(j, 5.5 + z));
|
|
354
411
|
const q = clamp01((t - (j + SETS_OFF * hash(j, 6.5 + z)) * SLOT) / crossing);
|
|
355
412
|
// It drifts ahead and behind on slow noise, by nothing at either edge of
|
|
356
|
-
// the
|
|
413
|
+
// the element and never enough to turn it back.
|
|
357
414
|
const centre = q * across - width + 0.06 * across * Math.sin(Math.PI * q) * noise(q, 2.7, j + z);
|
|
358
415
|
return { strength, centre };
|
|
359
416
|
};
|
|
360
|
-
// The passes on the
|
|
361
|
-
|
|
417
|
+
// The passes on the element at `t`, every one off it `up` slots after its
|
|
418
|
+
// own began; the first slot is 0, so before it there are none.
|
|
419
|
+
const passing = (t, across) => {
|
|
420
|
+
const up = SETS_OFF + ((LEAST + SPREAD) * P * across) / STRIDE / SLOT;
|
|
362
421
|
const last = Math.floor(t / SLOT);
|
|
363
422
|
const passes = [];
|
|
364
|
-
for (let j = Math.max(0, Math.ceil(last -
|
|
365
|
-
passes.push(pass(j, t));
|
|
423
|
+
for (let j = Math.max(0, Math.ceil(last - up)); j <= last; j++)
|
|
424
|
+
passes.push(pass(j, t, across));
|
|
366
425
|
return passes;
|
|
367
426
|
};
|
|
368
427
|
// A ripple's crest: a Gaussian ridge, 1 where the field crosses zero and in
|
|
369
428
|
// (0, 1] for any noise, smooth throughout, so a crest sliding through a cell
|
|
370
429
|
// lights it smoothly.
|
|
371
430
|
const ripple = (a, b, c) => Math.exp(-((noise(a, b, c) / 0.8) ** 2));
|
|
372
|
-
const field = (t) => {
|
|
373
|
-
|
|
431
|
+
const field = (t, drawn) => {
|
|
432
|
+
// [LAW:one-source-of-truth] The element is as wide as the cells its frame
|
|
433
|
+
// lists, never as a width its caller states: a padded, wrapped or
|
|
434
|
+
// confined element is crossed where it shows.
|
|
435
|
+
const [left, right] = extent(drawn.cells().map((cell) => cell.col));
|
|
436
|
+
const passes = passing(t, right - left + 1 + 2 * width);
|
|
374
437
|
return (cell) => {
|
|
375
438
|
// Where two passes overlap the brighter wins, so light never doubles up.
|
|
376
439
|
let lit = 0;
|
|
377
440
|
for (const { strength, centre } of passes)
|
|
378
|
-
lit = Math.max(lit, strength * bump((cell.col - centre) / width));
|
|
441
|
+
lit = Math.max(lit, strength * bump((cell.col - left - centre) / width));
|
|
379
442
|
const row = cell.row + z;
|
|
380
443
|
const current = 0.8 * noise(cell.col * 0.05, row * 0.3 + 1.3, t * 0.02);
|
|
381
444
|
const caustic = (ripple(cell.col * 0.21 + current, row * 0.9, t * 0.022) * ripple(cell.col * 0.33 + 9.1 - current, row * 0.7, t * 0.018 + 4.2)) **
|
|
@@ -387,8 +450,14 @@ export function shimmer(curve, span, width, glow, z) {
|
|
|
387
450
|
}
|
|
388
451
|
/**
|
|
389
452
|
* Sparkle, as fireflies. Swing: how far into the firefly's `light` a glow
|
|
390
|
-
* goes at its brightest, 0–1.
|
|
391
|
-
*
|
|
453
|
+
* goes at its brightest, 0–1. Fireflies keep to a lattice laid from the
|
|
454
|
+
* element's first column and top row as a frame lists its cells
|
|
455
|
+
* (`Drawn.cells`): one every `PER` columns along a row of the lattice, a row
|
|
456
|
+
* of the lattice every `ROWS` rows, each firefly near a home of its own.
|
|
457
|
+
* The lattice does not hang on how wide or deep the element is, so one that
|
|
458
|
+
* grows or shrinks at its last column or bottom row moves no firefly; one
|
|
459
|
+
* whose first column moves, as centred words that change length do, carries
|
|
460
|
+
* its fireflies with it. A firefly flashes — kindles,
|
|
392
461
|
* drifts a few cells glowing, slowing as it fades — then flies on in the
|
|
393
462
|
* dark and flashes again somewhere a few cells off, the way fireflies at
|
|
394
463
|
* dusk are seen: a short lit stroke, then gone, each time a little
|
|
@@ -399,53 +468,78 @@ export function shimmer(curve, span, width, glow, z) {
|
|
|
399
468
|
* turn: its brightest flash, rise and fall, takes `FLASH` of it, and a
|
|
400
469
|
* dimmer one less.
|
|
401
470
|
*/
|
|
402
|
-
export function sparkle(curve,
|
|
471
|
+
export function sparkle(curve, glow, z) {
|
|
403
472
|
requireCurve(curve);
|
|
404
|
-
|
|
473
|
+
// The columns a firefly has to itself along a row of the lattice, and the
|
|
474
|
+
// rows between two rows of it: more than a halo's reach, so a one-row
|
|
475
|
+
// element is lit by one row of fireflies.
|
|
476
|
+
const PER = 8;
|
|
477
|
+
const ROWS = 3;
|
|
405
478
|
const HALO = 5;
|
|
406
479
|
// How far from home a flash may begin, and how far the brightest drifts while lit.
|
|
407
480
|
const HOP = 5;
|
|
408
481
|
const GLIDE = 5;
|
|
409
482
|
const TURN = curve.seconds;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
483
|
+
// How far from its slot's middle a firefly's light can reach a cell: its
|
|
484
|
+
// home's jitter, a hop, a glide, its wander, and its halo; across rows,
|
|
485
|
+
// its wander and its halo, which counts a row as two columns.
|
|
486
|
+
const REACH = 0.7 * PER + HOP + GLIDE + 1.5 + HALO;
|
|
487
|
+
const REACH_ROWS = 0.6 + HALO / 2;
|
|
488
|
+
// The firefly of slot `i` along lattice row `k` at `t`, from the element's
|
|
489
|
+
// first column and top row: where it is, and how bright. Flash `n` is one
|
|
490
|
+
// event: whether it comes, how bright, when in its turn and where are its
|
|
491
|
+
// own. Between flashes the firefly is dark, so where it moves to is never
|
|
492
|
+
// seen moving.
|
|
493
|
+
const firefly = (i, k, t, left, top) => {
|
|
494
|
+
const id = i * 4.9 + k * 31.7 + 0.3 + z;
|
|
495
|
+
const home = i + 0.5 + 0.7 * noise(i * 1.7 + 0.2, 0.3 + k * 2.9, 0.5 + z);
|
|
413
496
|
// Out of step with every other firefly from the first moment.
|
|
414
|
-
|
|
415
|
-
}));
|
|
416
|
-
// Where every firefly is at `t`, and how bright.
|
|
417
|
-
const fliesAt = (t) => homes.map((fly) => {
|
|
418
|
-
// Flash `n` is one event: whether it comes, how bright, when in
|
|
419
|
-
// its turn and where are its own. Between flashes the firefly
|
|
420
|
-
// is dark, so where it moves to is never seen moving.
|
|
421
|
-
const u = t + fly.offset;
|
|
497
|
+
const u = t + hash(i + k * 1013, 0.7 + z) * TURN;
|
|
422
498
|
const n = Math.floor(u / TURN);
|
|
423
499
|
// A dark turn is a flash with no light: it keeps its length, so
|
|
424
500
|
// every flash is timed alike.
|
|
425
|
-
const lit = hash(n,
|
|
426
|
-
const
|
|
427
|
-
const at = hash(n,
|
|
501
|
+
const lit = hash(n, id + 1) < 0.15 ? 0 : 1;
|
|
502
|
+
const strength = 0.5 + 0.5 * hash(n, id + 2);
|
|
503
|
+
const at = hash(n, id + 3) * (1 - lasts(FLASH, strength));
|
|
428
504
|
const s = u / TURN - n - at;
|
|
429
505
|
// While it glows it drifts a few cells one way or the other, as
|
|
430
506
|
// fast as it is bright: quickest at its height, slowing as it
|
|
431
507
|
// fades — the lit stroke a firefly draws on the dusk. A brighter
|
|
432
508
|
// flash lasts longer and draws a longer stroke at the same pace.
|
|
433
509
|
return {
|
|
434
|
-
x:
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
510
|
+
x: left +
|
|
511
|
+
home * PER +
|
|
512
|
+
HOP * (2 * hash(n, id + 4) - 1) +
|
|
513
|
+
GLIDE * strength * (2 * hash(n, id + 5) - 1) * given(FLASH, s, strength) +
|
|
514
|
+
1.5 * noise(id, 0.1, t * 0.035),
|
|
515
|
+
y: top + k * ROWS + 0.6 * noise(id, 2.2, t * 0.035),
|
|
516
|
+
glow: lit * swell(FLASH, s, strength),
|
|
440
517
|
};
|
|
441
|
-
}
|
|
442
|
-
const field = (t) => {
|
|
443
|
-
|
|
518
|
+
};
|
|
519
|
+
const field = (t, drawn) => {
|
|
520
|
+
// [LAW:one-source-of-truth] The element is where its frame lists cells,
|
|
521
|
+
// never a width its caller states: a padded, wrapped or confined element
|
|
522
|
+
// has its fireflies over it. Its extent only bounds which slots are
|
|
523
|
+
// worked out; where each firefly is never depends on it.
|
|
524
|
+
const cells = drawn.cells();
|
|
525
|
+
const [left, right] = extent(cells.map((cell) => cell.col));
|
|
526
|
+
const [top, bottom] = extent(cells.map((cell) => cell.row));
|
|
527
|
+
const slot = (col) => (col - left) / PER - 0.5;
|
|
528
|
+
const band = (row) => (row - top) / ROWS;
|
|
529
|
+
const [i0, i1] = [Math.ceil(slot(left - REACH)), Math.floor(slot(right + REACH))];
|
|
530
|
+
const [k0, k1] = [Math.ceil(band(top - REACH_ROWS)), Math.floor(band(bottom + REACH_ROWS))];
|
|
531
|
+
const flies = Array.from({ length: k1 - k0 + 1 }, (_, k) => Array.from({ length: i1 - i0 + 1 }, (_, i) => firefly(i0 + i, k0 + k, t, left, top)));
|
|
444
532
|
return (cell) => {
|
|
445
|
-
//
|
|
533
|
+
// A cell is asked only of the fireflies whose light can reach it, and
|
|
534
|
+
// where two halos overlap the brighter wins, so light never doubles up.
|
|
446
535
|
let shine = 0;
|
|
447
|
-
|
|
448
|
-
|
|
536
|
+
const [ia, ib] = [Math.max(i0, Math.ceil(slot(cell.col - REACH))), Math.min(i1, Math.floor(slot(cell.col + REACH)))];
|
|
537
|
+
const [ka, kb] = [Math.max(k0, Math.ceil(band(cell.row - REACH_ROWS))), Math.min(k1, Math.floor(band(cell.row + REACH_ROWS)))];
|
|
538
|
+
for (let k = ka; k <= kb; k++)
|
|
539
|
+
for (let i = ia; i <= ib; i++) {
|
|
540
|
+
const fly = flies[k - k0][i - i0];
|
|
541
|
+
shine = Math.max(shine, fly.glow * bump(Math.hypot(cell.col - fly.x, 2 * (cell.row - fly.y)) / HALO));
|
|
542
|
+
}
|
|
449
543
|
return curve.swing * curve.ease(shine);
|
|
450
544
|
};
|
|
451
545
|
};
|
|
@@ -460,8 +554,9 @@ export function sparkle(curve, span, glow, z) {
|
|
|
460
554
|
* hiding — 1 reaches fully invisible. A cell wholly shown is returned as it was.
|
|
461
555
|
*/
|
|
462
556
|
export function veiled(visibility, swing) {
|
|
463
|
-
return (t,
|
|
464
|
-
const
|
|
557
|
+
return (t, drawn) => {
|
|
558
|
+
const { theme } = drawn;
|
|
559
|
+
const shows = visibility(t, drawn);
|
|
465
560
|
// [LAW:one-source-of-truth] The ground is the terminal `Effected` draws on, never a second one handed here.
|
|
466
561
|
const ground = Oklch.fromRgba(theme.backgroundColor);
|
|
467
562
|
return (colors, cell) => {
|
|
@@ -492,7 +587,8 @@ function order(z) {
|
|
|
492
587
|
};
|
|
493
588
|
}
|
|
494
589
|
function transition(curve, effect) {
|
|
495
|
-
|
|
590
|
+
const done = (t) => t >= curve.seconds;
|
|
591
|
+
return { effect: withNextChange(effect, (t) => (done(t) ? Number.POSITIVE_INFINITY : t)), done };
|
|
496
592
|
}
|
|
497
593
|
/**
|
|
498
594
|
* Of a transition's `seconds`, how long each cell's own change takes. Under
|
|
@@ -500,15 +596,51 @@ function transition(curve, effect) {
|
|
|
500
596
|
*/
|
|
501
597
|
const OWN = 0.45;
|
|
502
598
|
/**
|
|
503
|
-
* Each cell's
|
|
504
|
-
*
|
|
505
|
-
* the
|
|
599
|
+
* Each cell's own change in a transition, in a frame listing `cells`: a cell
|
|
600
|
+
* starts as far through the time the others leave it as its `place` falls
|
|
601
|
+
* between the element's first and last, so on any element, however little of
|
|
602
|
+
* the order its cells span, the first starts at 0 and the last settles at
|
|
603
|
+
* `seconds`. [LAW:one-source-of-truth] That is what lets `done` read
|
|
604
|
+
* `seconds` alone. An element whose cells all share one place has no order to
|
|
605
|
+
* stagger, and its change takes the whole of `seconds`.
|
|
606
|
+
*
|
|
607
|
+
* A cell's change is settled in the first frame that lists it and kept, as
|
|
608
|
+
* its place is, so an element whose cells change while it moves — a label
|
|
609
|
+
* that gains a column as it leaves — spreads only its new cells over the
|
|
610
|
+
* order it has then, and never sends a cell that has gone back to a later
|
|
611
|
+
* start. [LAW:no-ambient-temporal-coupling] The transition owns that record;
|
|
612
|
+
* it is why a transition is made for one element.
|
|
506
613
|
*/
|
|
507
614
|
function ownChange(curve, place) {
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
615
|
+
const changes = new Map();
|
|
616
|
+
const key = (cell) => `${cell.row}:${cell.col}`;
|
|
617
|
+
return (cells) => {
|
|
618
|
+
const [first, last] = extent(cells.map(place));
|
|
619
|
+
const stagger = (last > first ? 1 - OWN : 0) * curve.seconds;
|
|
620
|
+
// One phase a frame, shifted per cell, so no cell builds its own.
|
|
621
|
+
const change = Phase.once(0, curve.seconds - stagger);
|
|
622
|
+
// A share of the way from the first place to the last, held to the order for a cell the frame does not list.
|
|
623
|
+
const settled = (cell) => {
|
|
624
|
+
const along = last > first ? clamp01((place(cell) - first) / (last - first)) : 0;
|
|
625
|
+
return { along, progress: (t) => change(t - along * stagger) };
|
|
626
|
+
};
|
|
627
|
+
for (const cell of cells) {
|
|
628
|
+
const at = key(cell);
|
|
629
|
+
changes.set(at, changes.get(at) ?? settled(cell));
|
|
630
|
+
}
|
|
631
|
+
// A cell the frame hands but does not list shows nothing yet, so it is timed for this frame alone.
|
|
632
|
+
return (cell) => changes.get(key(cell)) ?? settled(cell);
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
/** The least and greatest of `values`; of none, a frame with no cell to place, both 0. */
|
|
636
|
+
function extent(values) {
|
|
637
|
+
let lo = values[0] ?? 0;
|
|
638
|
+
let hi = lo;
|
|
639
|
+
for (const v of values) {
|
|
640
|
+
lo = Math.min(lo, v);
|
|
641
|
+
hi = Math.max(hi, v);
|
|
642
|
+
}
|
|
643
|
+
return [lo, hi];
|
|
512
644
|
}
|
|
513
645
|
/**
|
|
514
646
|
* Fade-in, as ink blooming in water: patches of the element surface first
|
|
@@ -518,9 +650,9 @@ function ownChange(curve, place) {
|
|
|
518
650
|
export function fadeIn(curve, z) {
|
|
519
651
|
requireCurve(curve);
|
|
520
652
|
const risen = ownChange(curve, order(1.7 + z));
|
|
521
|
-
return transition(curve, veiled((t) => {
|
|
522
|
-
const
|
|
523
|
-
return (cell) => curve.ease(
|
|
653
|
+
return transition(curve, veiled((t, drawn) => {
|
|
654
|
+
const of = risen(drawn.cells());
|
|
655
|
+
return (cell) => curve.ease(of(cell).progress(t));
|
|
524
656
|
}, curve.swing));
|
|
525
657
|
}
|
|
526
658
|
/**
|
|
@@ -543,16 +675,16 @@ const DISSOLVE_SHAPE = { depth: 0.4, from: 0.5, to: 0.95, maskFrom: 0.35, maskTo
|
|
|
543
675
|
*/
|
|
544
676
|
export function dissolveOut(curve, z) {
|
|
545
677
|
requireCurve(curve);
|
|
546
|
-
const place = order(4.2 + z);
|
|
547
678
|
const eddy = order(9.3 + z);
|
|
548
|
-
const faded = ownChange(curve,
|
|
679
|
+
const faded = ownChange(curve, order(4.2 + z));
|
|
549
680
|
const { depth, from, to, maskFrom, maskTo, late } = DISSOLVE_SHAPE;
|
|
550
|
-
return transition(curve, veiled((t) => {
|
|
551
|
-
const
|
|
681
|
+
return transition(curve, veiled((t, drawn) => {
|
|
682
|
+
const of = faded(drawn.cells());
|
|
552
683
|
return (cell) => {
|
|
553
|
-
const
|
|
684
|
+
const { along, progress } = of(cell);
|
|
685
|
+
const gone = curve.ease(progress(t));
|
|
554
686
|
const q = clamp01((gone - from) / (to - from));
|
|
555
|
-
const back = depth * smoothstep(maskFrom, maskTo, eddy(cell)) * (1 - late + late *
|
|
687
|
+
const back = depth * smoothstep(maskFrom, maskTo, eddy(cell)) * (1 - late + late * along) * Math.sin(Math.PI * q) ** 2;
|
|
556
688
|
return 1 - gone + back;
|
|
557
689
|
};
|
|
558
690
|
}, curve.swing));
|
|
@@ -581,9 +713,13 @@ const STRAY = 180;
|
|
|
581
713
|
* together; on the terminal's ground, a run of text's ink is a segment of
|
|
582
714
|
* its own. A segment's stray is slow noise of its own, at `STRAY_PERIODS`,
|
|
583
715
|
* so no two keep step, and the row never shows one spread of hues twice.
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
716
|
+
* A colour keeps its luminance as it turns (`hueTurned`), so every pair of
|
|
717
|
+
* words reads at the contrast it rested at; at 256 colours a turned colour
|
|
718
|
+
* lands on a colour the depth writes, one that keeps every pair of words it
|
|
719
|
+
* is drawn in at its floor (`floorOf`) — its resting contrast or WCAG AA,
|
|
720
|
+
* whichever is lower — as `shares` keeps the light's (`turnedBounds`). The
|
|
721
|
+
* turn only happens to the element's own colours (`Drawn.colors`), never the
|
|
722
|
+
* terminal's. On a sixteen-colour terminal nothing turns (`stillOnSlots`).
|
|
587
723
|
*/
|
|
588
724
|
export function wheel(curve, z) {
|
|
589
725
|
requireCurve(curve);
|
|
@@ -597,13 +733,197 @@ export function wheel(curve, z) {
|
|
|
597
733
|
const s = seed(hex);
|
|
598
734
|
return (360 * t) / P + curve.swing * STRAY * ((stray(s, t) - stray(s, 0)) / 1.5);
|
|
599
735
|
};
|
|
600
|
-
|
|
736
|
+
// A turned colour's bounds rest on what a frame draws, never on `t`: settled once for each.
|
|
737
|
+
const settled = new WeakMap();
|
|
738
|
+
return stillOnSlots((t, drawn) => {
|
|
739
|
+
const { colors, fills, depth, theme } = drawn;
|
|
601
740
|
// Whether a slot shows one of the element's colours: never the terminal's, whatever its hex.
|
|
602
|
-
const own =
|
|
603
|
-
//
|
|
604
|
-
const
|
|
605
|
-
|
|
741
|
+
const own = (terminal, color) => !terminal && colors.has(color.hex);
|
|
742
|
+
// The segment an own colour turns with: itself if a fill or beside no own colour, else the one beside it.
|
|
743
|
+
const segmentOf = (color, other, theirs) => (fills.has(color.hex) || !theirs ? color.hex : other.hex);
|
|
744
|
+
// A colour as it turns with a segment: written one way wherever it is drawn in a frame.
|
|
745
|
+
const keyOf = (color, segment) => `${color.hex} ${segment}`;
|
|
746
|
+
const memo = settled.get(theme) ?? new Memo();
|
|
747
|
+
settled.set(theme, memo);
|
|
748
|
+
const bounds = memo.get(drawnKey(drawn), () => turnedBounds(drawn.pairs, depth, (pair, side) => {
|
|
749
|
+
const other = side === "fg" ? "bg" : "fg";
|
|
750
|
+
return own(pair.terminal[side], pair[side]) ? keyOf(pair[side], segmentOf(pair[side], pair[other], own(pair.terminal[other], pair[other]))) : undefined;
|
|
751
|
+
}));
|
|
752
|
+
// 256 colours writes a turned colour as one it has an index for; truecolor
|
|
753
|
+
// as one of the two either side of its luminance.
|
|
754
|
+
const indexed = measuredIndexed(depth, theme);
|
|
755
|
+
const written = new Map();
|
|
756
|
+
const choose = (color, segment) => {
|
|
757
|
+
const turn = hueTurned(color, turned(segment, t));
|
|
758
|
+
return nearestWithin(turn.looks, color, depth === ColorDepth.TRUECOLOR ? turn.either.map(measured) : indexed, bounds.get(keyOf(color, segment)) ?? UNBOUNDED);
|
|
759
|
+
};
|
|
760
|
+
const write = (color, segment) => {
|
|
761
|
+
const found = written.get(keyOf(color, segment)) ?? choose(color, segment);
|
|
762
|
+
written.set(keyOf(color, segment), found);
|
|
763
|
+
return found;
|
|
764
|
+
};
|
|
765
|
+
return (shown, cell) => {
|
|
766
|
+
const mine = { fg: own(cell.terminal.fg, shown.fg), bg: own(cell.terminal.bg, shown.bg) };
|
|
767
|
+
const turn = (color, ours, other, theirs) => (ours ? write(color, segmentOf(color, other, theirs)) : color);
|
|
768
|
+
return { fg: turn(shown.fg, mine.fg, shown.bg, mine.bg), bg: turn(shown.bg, mine.bg, shown.fg, mine.fg) };
|
|
769
|
+
};
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* `color` turned `degrees` round the hue circle at its own relative
|
|
774
|
+
* luminance, and at its own chroma where the gamut has it. WCAG contrast is
|
|
775
|
+
* a ratio of luminances, so a pair of colours each turned this way reads at
|
|
776
|
+
* the contrast it did; one turned at its OKLCH lightness instead does not,
|
|
777
|
+
* since equal lightness is not equal luminance across hues. The lightness
|
|
778
|
+
* is found by bisection, as `ensureContrastToward` finds one, and 8-bit
|
|
779
|
+
* rounding lands the luminance between two colours, so both are kept. At 0
|
|
780
|
+
* degrees the colour itself, exactly: an OKLCH round trip can land a step
|
|
781
|
+
* off (`touchedAt`).
|
|
782
|
+
*/
|
|
783
|
+
function hueTurned(color, degrees) {
|
|
784
|
+
if (degrees === 0)
|
|
785
|
+
return { looks: color, either: [color] };
|
|
786
|
+
const from = Oklch.fromRgba(color);
|
|
787
|
+
const luminance = relativeLuminance(color);
|
|
788
|
+
const at = (l) => new Oklch(l, from.c, from.h + degrees, from.alpha).toRgba();
|
|
789
|
+
let [dim, bright] = [0, 1];
|
|
790
|
+
for (let i = 0; i < 24; i++) {
|
|
791
|
+
const mid = (dim + bright) / 2;
|
|
792
|
+
[dim, bright] = relativeLuminance(at(mid)) < luminance ? [mid, bright] : [dim, mid];
|
|
793
|
+
}
|
|
794
|
+
return { looks: at(bright), either: [at(dim), at(bright)] };
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Where a colour sits on WCAG's contrast scale: the log of its contrast with
|
|
798
|
+
* black. Two colours' contrast ratio is `exp` of the gap between their
|
|
799
|
+
* stops, so a contrast to keep is a distance a colour may move.
|
|
800
|
+
*/
|
|
801
|
+
const stop = (color) => Math.log(contrastRatio(color, new ColorRgba(0, 0, 0)));
|
|
802
|
+
const UNBOUNDED = { lo: -Infinity, hi: Infinity };
|
|
803
|
+
/** Bounds that admit no turn: the colour is written as it rests. */
|
|
804
|
+
const STILL = { lo: Infinity, hi: -Infinity };
|
|
805
|
+
/**
|
|
806
|
+
* The widest 8-bit rounding parts `hueTurned`'s two colours, in stops. They
|
|
807
|
+
* are one point of the hue rounded two ways, so they differ by at most a
|
|
808
|
+
* step in each channel, and a channel of a colour that dark or light can
|
|
809
|
+
* take a step only so large: worst near a luminance of 0.05, at 3.03% of
|
|
810
|
+
* contrast. Bounds at least this wide admit one of the two whatever the
|
|
811
|
+
* hue; narrower, a colour would turn at some hues and snap back to rest at
|
|
812
|
+
* the next, so it holds still.
|
|
813
|
+
*/
|
|
814
|
+
const ROUNDING_GAP = Math.log(1.031);
|
|
815
|
+
/**
|
|
816
|
+
* How far each turned colour's luminance may land from the one it rests at,
|
|
817
|
+
* keyed as `turnedAs` keys each side of a pair — none for a side that holds
|
|
818
|
+
* still — so that every pair of `pairs` reads at its floor (`floorOf`) at
|
|
819
|
+
* `depth` however each of its colours lands within its bounds. Bounds
|
|
820
|
+
* narrower than `ROUNDING_GAP` are `STILL`.
|
|
821
|
+
*
|
|
822
|
+
* A pair's contrast to spare above its floor, as a gap between stops, is
|
|
823
|
+
* shared out evenly between its sides that turn: the lighter may land that
|
|
824
|
+
* much darker, the darker that much lighter, and never further. So a colour
|
|
825
|
+
* written once for every pair it is drawn in is bounded by each of them at
|
|
826
|
+
* once, and a frame writes it one way wherever it falls. A pair whose floor
|
|
827
|
+
* is 1:1 holds however it is drawn, and bounds nothing.
|
|
828
|
+
*/
|
|
829
|
+
function turnedBounds(pairs, depth, turnedAs) {
|
|
830
|
+
const bounds = new Map();
|
|
831
|
+
for (const pair of pairs.filter((pair) => floorOf(pair, depth) > 1)) {
|
|
832
|
+
const spare = Math.log(contrastRatio(pair.fg, pair.bg)) - Math.log(floorOf(pair, depth));
|
|
833
|
+
const share = spare / SIDES.filter((side) => turnedAs(pair, side) !== undefined).length;
|
|
834
|
+
for (const side of SIDES) {
|
|
835
|
+
const key = turnedAs(pair, side);
|
|
836
|
+
if (key === undefined)
|
|
837
|
+
continue;
|
|
838
|
+
const [mine, theirs] = [stop(pair[side]), stop(pair[side === "fg" ? "bg" : "fg"])];
|
|
839
|
+
const { lo, hi } = bounds.get(key) ?? UNBOUNDED;
|
|
840
|
+
bounds.set(key, mine >= theirs ? { lo: Math.max(lo, mine - share), hi } : { lo, hi: Math.min(hi, mine + share) });
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return new Map([...bounds].map(([key, { lo, hi }]) => [key, hi - lo < ROUNDING_GAP ? STILL : { lo, hi }]));
|
|
844
|
+
}
|
|
845
|
+
const measured = (color) => ({ color, looks: Oklch.fromRgba(color), stop: stop(color) });
|
|
846
|
+
/** `ColorSpec.indexed`'s colours, measured once for each theme and depth. */
|
|
847
|
+
const measuredMemo = new WeakMap();
|
|
848
|
+
function measuredIndexed(depth, theme) {
|
|
849
|
+
const indexed = ColorSpec.indexed(depth, theme);
|
|
850
|
+
const listed = measuredMemo.get(indexed) ?? indexed.map(([, color]) => measured(color));
|
|
851
|
+
measuredMemo.set(indexed, listed);
|
|
852
|
+
return listed;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* The colour that looks nearest `color` (OKLab's ΔE): `rest`, or one of
|
|
856
|
+
* `candidates` whose luminance is within `bounds`. `rest` is what the bounds
|
|
857
|
+
* are measured from, so it is always offered; ties go to it, then to the
|
|
858
|
+
* earlier.
|
|
859
|
+
*/
|
|
860
|
+
function nearestWithin(color, rest, candidates, { lo, hi }) {
|
|
861
|
+
return nearestOf(color, [measured(rest), ...candidates.filter(({ stop }) => stop >= lo && stop <= hi)]).color;
|
|
862
|
+
}
|
|
863
|
+
/** Of `offered`, the one that looks nearest `color` (OKLab's ΔE), ties to the earlier; none when nothing is offered. */
|
|
864
|
+
function nearestOf(color, offered) {
|
|
865
|
+
const looks = Oklch.fromRgba(color);
|
|
866
|
+
let best;
|
|
867
|
+
for (const candidate of offered) {
|
|
868
|
+
const apart = looks.deltaE(candidate.looks);
|
|
869
|
+
if (best === undefined || apart < best[1])
|
|
870
|
+
best = [candidate, apart];
|
|
871
|
+
}
|
|
872
|
+
return best?.[0];
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* A signal: every cell's ink stepping through `steps`' colours on a beat,
|
|
876
|
+
* one colour a hold, looping, for the time it is handed — a gauge's
|
|
877
|
+
* chevrons in bright changing colours. The ground is left as the cell draws
|
|
878
|
+
* it, and the words stay legible on it: each step's ink is written as the
|
|
879
|
+
* colour nearest the step's (OKLab's ΔE) that the depth draws and that
|
|
880
|
+
* reads on the cell's ground, as `Effected` writes it, at the cell's floor
|
|
881
|
+
* (`floorOf`) — its resting contrast or `LEGIBLE`, whichever is lower, the
|
|
882
|
+
* floor every loop here keeps. In truecolor that is the step itself, or its
|
|
883
|
+
* hue and chroma at the lightness nearest it that reaches the floor
|
|
884
|
+
* (`ensureContrast`, the one slide a template's `readableOn` makes too);
|
|
885
|
+
* below truecolor, the nearest of the colours the depth has an index for, so on a
|
|
886
|
+
* sixteen-colour terminal it steps from slot to slot, the one effect here
|
|
887
|
+
* that does (`stillOnSlots`). A cell whose ground no offered colour reads
|
|
888
|
+
* on keeps the ink it rested in.
|
|
889
|
+
*
|
|
890
|
+
* The search is settled once for each ink, ground and step, so a frame
|
|
891
|
+
* inside a step redoes none, and nor does a later pass of the beat: at sixty
|
|
892
|
+
* frames a second a one-second step is sixty identical frames, and the
|
|
893
|
+
* frame says it changes next when the step ends (`Beat.end`).
|
|
894
|
+
* [LAW:no-ambient-temporal-coupling] The beat is `core/easing.ts`'s, and
|
|
895
|
+
* nothing here reads a clock.
|
|
896
|
+
*/
|
|
897
|
+
export function steppedInk(steps) {
|
|
898
|
+
const rhythm = beat(steps.map((step) => step.seconds));
|
|
899
|
+
// A copy, as `beat` keeps of the holds: what the signal plays stays what
|
|
900
|
+
// `steps` said whatever the caller does to its array.
|
|
901
|
+
const inks = steps.map((step) => step.color);
|
|
902
|
+
const settled = new WeakMap();
|
|
903
|
+
return withNextChange((t, { depth, theme }) => {
|
|
904
|
+
const step = inks[rhythm.at(t)];
|
|
905
|
+
const memo = settled.get(theme) ?? new Memo();
|
|
906
|
+
settled.set(theme, memo);
|
|
907
|
+
return (colors, cell) => {
|
|
908
|
+
const pair = { ...colors, terminal: cell.terminal, reverse: cell.reverse };
|
|
909
|
+
return { fg: memo.get(`${depth} ${pairKey(pair)} ${step.hex}`, () => legibleInk(step, pair, depth, theme)), bg: colors.bg };
|
|
910
|
+
};
|
|
911
|
+
}, (t) => rhythm.end(t));
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* The colour nearest `step` the depth writes that reads on `pair`'s ground
|
|
915
|
+
* at `pair`'s floor, as `Effected` writes it (`shows`), or `pair`'s own ink
|
|
916
|
+
* when none does. [LAW:one-source-of-truth] Measured the way `shares` and
|
|
917
|
+
* `wheel` measure: the written colours, at the catalogue's floor.
|
|
918
|
+
*/
|
|
919
|
+
function legibleInk(step, pair, depth, theme) {
|
|
920
|
+
const floor = floorOf(pair, depth);
|
|
921
|
+
const offered = depth === ColorDepth.TRUECOLOR ? [step, ensureContrast(step, pair.bg, floor)].map(measured) : measuredIndexed(depth, theme);
|
|
922
|
+
const reads = ({ color }) => {
|
|
923
|
+
const shown = Effected.shows(pair, { fg: color, bg: pair.bg }, depth, theme);
|
|
924
|
+
return contrastRatio(shown.fg, shown.bg) >= floor;
|
|
606
925
|
};
|
|
926
|
+
return nearestOf(step, offered.filter(reads))?.color ?? pair.fg;
|
|
607
927
|
}
|
|
608
928
|
/**
|
|
609
929
|
* Every effect in the catalogue by its name in `EFFECT_CURVES`, each made the
|
|
@@ -618,9 +938,9 @@ export function wheel(curve, z) {
|
|
|
618
938
|
*/
|
|
619
939
|
export const EFFECT_CATALOGUE = {
|
|
620
940
|
loops: {
|
|
621
|
-
shimmer: (element, curve = EFFECT_CURVES.shimmer) => onColors(shimmer(curve,
|
|
941
|
+
shimmer: (element, curve = EFFECT_CURVES.shimmer) => onColors(shimmer(curve, SHIMMER_WIDTH, EFFECT_LIGHTS.sun, element.z)),
|
|
622
942
|
pulse: (element, curve = EFFECT_CURVES.pulse) => onColors(pulse(curve, EFFECT_LIGHTS.sun, element.z)),
|
|
623
|
-
sparkle: (element, curve = EFFECT_CURVES.sparkle) => onColors(sparkle(curve,
|
|
943
|
+
sparkle: (element, curve = EFFECT_CURVES.sparkle) => onColors(sparkle(curve, EFFECT_LIGHTS.firefly, element.z)),
|
|
624
944
|
wheel: (element, curve = EFFECT_CURVES.wheel) => wheel(curve, element.z),
|
|
625
945
|
},
|
|
626
946
|
transitions: {
|