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