@promptctl/rich-js 0.27.0 → 0.29.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/README.md +7 -6
- package/THIRD-PARTY-NOTICES +42 -0
- package/dist/core/animation.d.ts +65 -46
- package/dist/core/animation.js +91 -67
- package/dist/core/ansi.d.ts +19 -12
- package/dist/core/ansi.js +114 -36
- package/dist/core/bidi-data.d.ts +14 -0
- package/dist/core/bidi-data.js +385 -0
- package/dist/core/bidi.d.ts +89 -0
- package/dist/core/bidi.js +615 -0
- package/dist/core/box.d.ts +9 -1
- package/dist/core/box.js +11 -0
- package/dist/core/cell-widths.d.ts +3 -2
- package/dist/core/cell-widths.js +93 -93
- package/dist/core/cells.d.ts +20 -0
- package/dist/core/cells.js +94 -6
- package/dist/core/clock.d.ts +1 -1
- package/dist/core/color.d.ts +56 -57
- package/dist/core/color.js +79 -81
- package/dist/core/console.d.ts +24 -7
- package/dist/core/console.js +27 -8
- package/dist/core/control-codes.d.ts +61 -0
- package/dist/core/control-codes.js +157 -0
- package/dist/core/easing.d.ts +40 -40
- package/dist/core/easing.js +40 -40
- package/dist/core/env.d.ts +6 -0
- package/dist/core/env.js +9 -1
- package/dist/core/export-html.js +5 -4
- package/dist/core/export-lines.d.ts +13 -11
- package/dist/core/export-lines.js +19 -15
- package/dist/core/export-svg.d.ts +3 -3
- package/dist/core/export-svg.js +10 -9
- package/dist/core/json.js +9 -2
- package/dist/core/markup.js +5 -4
- package/dist/core/memo.d.ts +1 -1
- package/dist/core/memo.js +1 -1
- package/dist/core/oklch.d.ts +14 -14
- package/dist/core/oklch.js +10 -10
- package/dist/core/orient.d.ts +68 -0
- package/dist/core/orient.js +224 -0
- package/dist/core/osc8.d.ts +12 -5
- package/dist/core/osc8.js +14 -7
- package/dist/core/paint.d.ts +9 -9
- package/dist/core/paint.js +3 -3
- package/dist/core/pretty.d.ts +1 -1
- package/dist/core/pretty.js +23 -19
- package/dist/core/protocol.d.ts +47 -26
- package/dist/core/protocol.js +31 -23
- package/dist/core/render.d.ts +19 -14
- package/dist/core/render.js +30 -17
- package/dist/core/seal.d.ts +26 -0
- package/dist/core/seal.js +75 -0
- package/dist/core/segment.d.ts +12 -4
- package/dist/core/segment.js +16 -6
- package/dist/core/strip.d.ts +83 -83
- package/dist/core/strip.js +70 -70
- package/dist/core/style.d.ts +46 -18
- package/dist/core/style.js +92 -56
- package/dist/core/text-order.d.ts +51 -0
- package/dist/core/text-order.js +50 -0
- package/dist/core/text.d.ts +12 -2
- package/dist/core/text.js +136 -90
- package/dist/core/timeline.d.ts +5 -5
- package/dist/core/timeline.js +11 -11
- package/dist/core/tween.d.ts +6 -6
- package/dist/core/tween.js +10 -10
- package/dist/core/view.d.ts +5 -5
- package/dist/core/view.js +15 -14
- package/dist/core/wrap.d.ts +24 -6
- package/dist/core/wrap.js +82 -16
- package/dist/host/app.d.ts +50 -9
- package/dist/host/app.js +120 -15
- package/dist/host/host-environment.d.ts +2 -2
- package/dist/host/host-environment.js +2 -2
- package/dist/host/index.d.ts +3 -2
- package/dist/host/index.js +1 -0
- package/dist/host/input.d.ts +42 -9
- package/dist/host/input.js +91 -7
- package/dist/host/terminal-colors.d.ts +18 -0
- package/dist/host/terminal-colors.js +46 -0
- package/dist/host/terminal-host.d.ts +4 -3
- package/dist/host/terminal-host.js +1 -1
- package/dist/index.d.ts +17 -16
- package/dist/index.js +11 -11
- package/dist/node/prompt.d.ts +1 -1
- package/dist/node/prompt.js +1 -1
- package/dist/node/terminal-host.d.ts +1 -1
- package/dist/node/traceback.js +2 -2
- package/dist/renderables/change.d.ts +1 -3
- package/dist/renderables/change.js +6 -8
- package/dist/renderables/effect.d.ts +88 -87
- package/dist/renderables/effect.js +124 -114
- package/dist/renderables/effects.d.ts +105 -76
- package/dist/renderables/effects.js +309 -176
- package/dist/renderables/embed.d.ts +1 -1
- package/dist/renderables/embed.js +2 -2
- package/dist/renderables/flexStrip.d.ts +7 -7
- package/dist/renderables/flexStrip.js +15 -15
- package/dist/renderables/frames.d.ts +7 -16
- package/dist/renderables/frames.js +12 -27
- package/dist/renderables/glyph-effect.d.ts +22 -23
- package/dist/renderables/glyph-effect.js +43 -41
- package/dist/renderables/glyph-effects.d.ts +33 -14
- package/dist/renderables/glyph-effects.js +90 -22
- package/dist/renderables/html-entities.d.ts +1 -1
- package/dist/renderables/html-entities.js +1 -1
- package/dist/renderables/lifecycle.d.ts +8 -10
- package/dist/renderables/lifecycle.js +17 -18
- package/dist/renderables/live.d.ts +2 -2
- package/dist/renderables/live.js +7 -7
- package/dist/renderables/markdown-parse.js +3 -3
- package/dist/renderables/markdown.js +4 -2
- package/dist/renderables/panel.js +7 -5
- package/dist/renderables/progress.d.ts +1 -1
- package/dist/renderables/progress.js +1 -1
- package/dist/renderables/progressBar.d.ts +3 -3
- package/dist/renderables/progressBar.js +21 -18
- package/dist/renderables/prompt.d.ts +1 -1
- package/dist/renderables/scroll.d.ts +1 -1
- package/dist/renderables/scroll.js +3 -3
- package/dist/renderables/spinner.d.ts +4 -4
- package/dist/renderables/spinner.js +10 -10
- package/dist/renderables/syntax.d.ts +2 -2
- package/dist/renderables/syntax.js +9 -4
- package/dist/renderables/table.js +5 -4
- package/dist/renderables/traceback.d.ts +1 -1
- package/dist/renderables/traceback.js +1 -1
- package/dist/renderables/tree.js +3 -3
- package/dist/template-bindings/animation-funcs.d.ts +60 -29
- package/dist/template-bindings/animation-funcs.js +126 -43
- package/dist/template-bindings/color-funcs.d.ts +14 -14
- package/dist/template-bindings/color-funcs.js +25 -25
- package/dist/template-bindings/index.d.ts +16 -14
- package/dist/template-bindings/index.js +17 -14
- package/dist/template-bindings/palette-funcs.d.ts +2 -2
- package/dist/template-bindings/palette-funcs.js +4 -4
- package/dist/template-bindings/style-funcs.d.ts +5 -5
- package/dist/template-bindings/style-funcs.js +16 -16
- package/dist/themes/{transpose.d.ts → adjust.d.ts} +23 -23
- package/dist/themes/{transpose.js → adjust.js} +28 -28
- package/dist/themes/buildPalette.d.ts +6 -5
- package/dist/themes/buildPalette.js +22 -22
- package/dist/themes/colorMath.d.ts +16 -16
- package/dist/themes/colorMath.js +32 -32
- package/dist/themes/data/ansi-tables.d.ts +2 -2
- package/dist/themes/data/ansi-tables.js +2 -2
- package/dist/themes/data/index.js +1 -4
- package/dist/themes/data/textual-ansi.js +0 -143
- package/dist/themes/data/types.d.ts +4 -4
- package/dist/themes/data/types.js +2 -2
- package/dist/themes/palette.d.ts +12 -12
- package/dist/themes/palette.js +16 -16
- package/dist/themes/ramp.d.ts +17 -17
- package/dist/themes/ramp.js +12 -12
- package/dist/themes/registry.d.ts +6 -8
- package/dist/themes/registry.js +6 -8
- package/dist/themes/terminalThemes.d.ts +4 -11
- package/dist/themes/terminalThemes.js +31 -47
- package/dist/widgets/button.d.ts +3 -3
- package/dist/widgets/button.js +8 -8
- package/dist/widgets/checkbox.d.ts +2 -2
- package/dist/widgets/checkbox.js +4 -4
- package/dist/widgets/dropdown.d.ts +3 -3
- package/dist/widgets/dropdown.js +7 -5
- package/dist/widgets/event-router.d.ts +6 -6
- package/dist/widgets/event-router.js +2 -2
- package/dist/widgets/focus-manager.d.ts +3 -3
- package/dist/widgets/focus-manager.js +3 -3
- package/dist/widgets/index.d.ts +2 -2
- package/dist/widgets/index.js +1 -1
- package/dist/widgets/ink.d.ts +3 -3
- package/dist/widgets/ink.js +4 -4
- package/dist/widgets/slider.d.ts +2 -2
- package/dist/widgets/static-item.d.ts +2 -2
- package/dist/widgets/text-input.d.ts +2 -2
- package/dist/widgets/text-input.js +5 -5
- package/dist/widgets/themed-widget.js +4 -4
- package/dist/widgets/toggle.d.ts +4 -4
- package/dist/widgets/toggle.js +6 -6
- package/dist/widgets/types.d.ts +6 -6
- package/dist/widgets/types.js +2 -2
- package/dist/widgets/widget-app.d.ts +10 -10
- package/dist/widgets/widget-app.js +8 -8
- package/dist/widgets/widget-base.d.ts +2 -2
- package/package.json +5 -4
|
@@ -2,21 +2,21 @@
|
|
|
2
2
|
* The effect catalogue: shimmer, pulse, sparkle and wheel, which loop for as
|
|
3
3
|
* long as they are drawn, fade-in and dissolve-out, which run once and are
|
|
4
4
|
* done, a blink and a bold pulse, which move an attribute on a beat, and a
|
|
5
|
-
* gradient, which lays a
|
|
5
|
+
* gradient, which lays a color ramp across an element and may move it. Each is an `Effect` for `Effected` (`./effect.ts`); the defaults
|
|
6
6
|
* in `EFFECT_CURVES` are the ones signed off by eye in the effects-feel demo
|
|
7
7
|
* (`npm run effects-feel`), at 30 fps and at one frame a second, and
|
|
8
8
|
* `EFFECT_CATALOGUE` makes every one of them at those, by name, one way.
|
|
9
9
|
*
|
|
10
10
|
* Each effect is something a first-time observer already knows from life —
|
|
11
|
-
* a slow glow, sunlight moving on water, the
|
|
11
|
+
* a slow glow, sunlight moving on water, the colors of daylight, fireflies, ink
|
|
12
12
|
* blooming in water, mist lifting — because a motion the eye recognises
|
|
13
13
|
* reads as calm and alive, where a motion it has to learn reads as a widget.
|
|
14
14
|
* One effect here is not ambience but a signal, and keeps none of this on
|
|
15
|
-
* purpose: `steppedInk` steps its ink through
|
|
15
|
+
* purpose: `steppedInk` steps its ink through colors on a beat, the news
|
|
16
16
|
* that something is at its limit, and jumps from slot to slot where the
|
|
17
17
|
* loops hold still. Nor is `gradient` a light: it paints an ink rather than
|
|
18
18
|
* lighting it, so it may darken one, and a ramp scrolled across it jumps
|
|
19
|
-
* where the ramp wraps unless its ends are one
|
|
19
|
+
* where the ramp wraps unless its ends are one color. Of the list below it
|
|
20
20
|
* keeps the words. What makes the loops' motions familiar is what every
|
|
21
21
|
* curve of theirs keeps:
|
|
22
22
|
*
|
|
@@ -32,13 +32,13 @@
|
|
|
32
32
|
* filaments, fronts — never per-cell static.
|
|
33
33
|
* - Light only ever lights, and never takes the words away. Ink and the
|
|
34
34
|
* fill under it both move toward the light and never darken, each by its
|
|
35
|
-
* own share: the share a
|
|
35
|
+
* own share: the share a color can take while every cell it is drawn in
|
|
36
36
|
* still reads at its resting contrast or WCAG AA, whichever is lower. A
|
|
37
37
|
* cell with contrast to spare glows brightly; one with none barely moves.
|
|
38
38
|
* - Elements are apart. Each sits at its own `z` in the noise, so two
|
|
39
39
|
* elements under one effect do not move in lockstep.
|
|
40
40
|
*
|
|
41
|
-
*
|
|
41
|
+
* Color moves are in OKLCH, where equal steps look equal. A curve's `ease`
|
|
42
42
|
* maps its intensity in [0, 1] — how strongly the effect acts on a cell at a
|
|
43
43
|
* moment — onto its swing, for the loops and the transitions alike.
|
|
44
44
|
*
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
*/
|
|
53
53
|
import { Animation } from "../core/animation.js";
|
|
54
54
|
import { ColorDepth, ColorRgba, ColorSpec, contrastRatio, relativeLuminance } from "../core/color.js";
|
|
55
|
-
import { EASES,
|
|
55
|
+
import { EASES, WrapMode, stepSequence } from "../core/easing.js";
|
|
56
56
|
import { fbm, hash, noise, smoothstep } from "../core/noise.js";
|
|
57
57
|
import { Memo, WeakMemo } from "../core/memo.js";
|
|
58
58
|
import { Oklch } from "../core/oklch.js";
|
|
@@ -64,7 +64,7 @@ const LEGIBLE = 4.5;
|
|
|
64
64
|
/**
|
|
65
65
|
* The strengths a cell is drawn at: a field is snapped to one of these, and
|
|
66
66
|
* a share is checked at every one, so every strength drawn was checked. A
|
|
67
|
-
* 256-
|
|
67
|
+
* 256-color cube color changes in jumps between strengths, and no check of
|
|
68
68
|
* a continuous strength between two of them could see every jump. A step is
|
|
69
69
|
* a 48th of a swing, well under a just-noticeable difference.
|
|
70
70
|
*/
|
|
@@ -75,7 +75,7 @@ const ROUNDING = 0.01;
|
|
|
75
75
|
/**
|
|
76
76
|
* The contrast no loop draws `pair` below at `depth`: its resting contrast or
|
|
77
77
|
* `LEGIBLE`, whichever is lower, less what truecolor's rounding can cost.
|
|
78
|
-
* Below truecolor every
|
|
78
|
+
* Below truecolor every color written is one the depth holds exactly, so
|
|
79
79
|
* there is nothing to forgive. [LAW:one-source-of-truth] The floor `shares`
|
|
80
80
|
* settles the light against and `wheel` writes its turn above.
|
|
81
81
|
*/
|
|
@@ -87,33 +87,33 @@ const SIDES = ["fg", "bg"];
|
|
|
87
87
|
* How much of `touch` each of `colors` (hex) can take: the largest share in
|
|
88
88
|
* [0, 1] at which every pair `drawn` shows it in, touched at the same
|
|
89
89
|
* strength, still reads at its resting contrast or `LEGIBLE`, whichever is
|
|
90
|
-
* lower, in the
|
|
91
|
-
* A
|
|
90
|
+
* lower, in the colors `Effected` writes it in at `drawn`'s depth and terminal.
|
|
91
|
+
* A color in a pair but not in `colors` takes none, and so does one
|
|
92
92
|
* the terminal supplies, whatever its hex, as in `onColors`. One of `colors`
|
|
93
93
|
* no pair writes has nothing to be measured against, and takes it all.
|
|
94
94
|
*
|
|
95
|
-
*
|
|
95
|
+
* Colors are settled lightest first, each against every partner: one
|
|
96
96
|
* already settled at its share, one not yet settled as it is. So the lighter
|
|
97
|
-
*
|
|
97
|
+
* color of a cell takes the light first and its darker partner spends
|
|
98
98
|
* whatever contrast is left — pale lettering lifting off a dark fill gives
|
|
99
|
-
* the fill room to glow — and a darker
|
|
99
|
+
* the fill room to glow — and a darker color left as it is always holds,
|
|
100
100
|
* because its lighter partner was settled against exactly that.
|
|
101
101
|
*/
|
|
102
|
-
export function shares({ pairs, depth,
|
|
103
|
-
// [LAW:one-source-of-truth] A slot shows a subject
|
|
102
|
+
export function shares({ pairs, depth, terminal }, colors, touch) {
|
|
103
|
+
// [LAW:one-source-of-truth] A slot shows a subject color, by hex, only
|
|
104
104
|
// where the element writes it: the reading `onColors` lights by.
|
|
105
105
|
const own = (pair, side) => (pair.terminal[side] ? undefined : pair[side].hex);
|
|
106
106
|
const rgba = new Map(pairs.flatMap((pair) => SIDES.flatMap((side) => {
|
|
107
107
|
const hex = own(pair, side);
|
|
108
108
|
return hex === undefined ? [] : [[hex, pair[side]]];
|
|
109
109
|
})));
|
|
110
|
-
// A
|
|
111
|
-
// a frame drawing a lit
|
|
110
|
+
// A color no pair writes keeps no word legible, so nothing bounds its share:
|
|
111
|
+
// a frame drawing a lit color only under blanks and seams, or not at all,
|
|
112
112
|
// is a frame like any other.
|
|
113
113
|
const settled = new Map([...colors].filter((hex) => !rgba.has(hex)).map((hex) => [hex, 1]));
|
|
114
114
|
const lightness = (hex) => Oklch.fromRgba(rgba.get(hex)).l;
|
|
115
115
|
for (const hex of [...colors].filter((hex) => rgba.has(hex)).sort((a, b) => lightness(b) - lightness(a))) {
|
|
116
|
-
// Each pair showing the
|
|
116
|
+
// Each pair showing the color, with the side it shows it on and the share its partner takes.
|
|
117
117
|
const facing = pairs.flatMap((pair) => {
|
|
118
118
|
const mine = SIDES.find((side) => own(pair, side) === hex);
|
|
119
119
|
if (mine === undefined)
|
|
@@ -121,12 +121,12 @@ export function shares({ pairs, depth, theme }, colors, touch) {
|
|
|
121
121
|
const partner = own(pair, mine === "fg" ? "bg" : "fg");
|
|
122
122
|
return [[pair, mine, partner === undefined ? 0 : (settled.get(partner) ?? 0)]];
|
|
123
123
|
});
|
|
124
|
-
// [LAW:one-source-of-truth] A pair reads in the
|
|
124
|
+
// [LAW:one-source-of-truth] A pair reads in the colors `Effected` writes it in, touched at each side's share.
|
|
125
125
|
const holds = (share) => facing.every(([pair, mine, theirs]) => {
|
|
126
126
|
const floor = floorOf(pair, depth);
|
|
127
127
|
const at = (side, w) => touchedAt(touch, pair[side], (side === mine ? share : theirs) * w);
|
|
128
128
|
return STRENGTHS.every((w) => {
|
|
129
|
-
const shown = Effected.shows(pair, { fg: at("fg", w), bg: at("bg", w) }, depth,
|
|
129
|
+
const shown = Effected.shows(pair, { fg: at("fg", w), bg: at("bg", w) }, depth, terminal);
|
|
130
130
|
return contrastRatio(shown.fg, shown.bg) >= floor;
|
|
131
131
|
});
|
|
132
132
|
});
|
|
@@ -140,30 +140,31 @@ export function shares({ pairs, depth, theme }, colors, touch) {
|
|
|
140
140
|
return settled;
|
|
141
141
|
}
|
|
142
142
|
/**
|
|
143
|
-
* A loop as an `Effect` on the element's own
|
|
143
|
+
* A loop as an `Effect` on the element's own colors, by hex, wherever a cell
|
|
144
144
|
* shows them, each touched at its share of the cell's strength: the share
|
|
145
145
|
* `shares` settles for it against the pairs the element draws its words in
|
|
146
146
|
* (`Drawn`), touched as `Effected` writes them at the frame's depth — at 256
|
|
147
|
-
*
|
|
148
|
-
* contrast it spends is that
|
|
149
|
-
* moves (`
|
|
147
|
+
* colors a touched color lands on a color the slot can write, and the
|
|
148
|
+
* contrast it spends is that color's; on a sixteen-color terminal nothing
|
|
149
|
+
* moves (`staticWhereUnchanged`). `colors` are the ones lit, by default every color the element
|
|
150
150
|
* sets itself. A powerline strip's fill is the ground of its cells and the
|
|
151
151
|
* ink of the seam glyphs between them, so the arrow moves with the cell it
|
|
152
|
-
* points out of. A
|
|
152
|
+
* points out of. A color the terminal supplies (`Pair.terminal`) is
|
|
153
153
|
* not the element's, whatever its hex, and takes no share.
|
|
154
154
|
*
|
|
155
|
-
* Settling shares is costly, so it is done once for each set of
|
|
155
|
+
* Settling shares is costly, so it is done once for each set of colors an
|
|
156
156
|
* element draws: make the effect once and draw it in every frame, and a frame
|
|
157
157
|
* drawing what an earlier one drew costs no settling.
|
|
158
158
|
*/
|
|
159
159
|
export function onColors(loop, colors) {
|
|
160
160
|
const settled = new WeakMemo();
|
|
161
|
+
const litIn = (drawn) => colors ?? drawn.colors;
|
|
161
162
|
const settle = (drawn) => {
|
|
162
|
-
const lit =
|
|
163
|
+
const lit = litIn(drawn);
|
|
163
164
|
const key = [drawnKey(drawn), "|", ...lit].join(" ");
|
|
164
|
-
return settled.get(drawn.
|
|
165
|
+
return settled.get(drawn.terminal, () => new Memo()).get(key, () => shares(drawn, lit, loop.touch));
|
|
165
166
|
};
|
|
166
|
-
return
|
|
167
|
+
return staticWhereUnchanged(SIDES, litIn, atShares(loop, settle));
|
|
167
168
|
}
|
|
168
169
|
/** What a frame draws, as a key: its depth and its pairs, which its `colors` and `fills` are read off. */
|
|
169
170
|
function drawnKey({ depth, pairs }) {
|
|
@@ -181,10 +182,10 @@ function pairKey({ fg, bg, terminal, reverse }) {
|
|
|
181
182
|
return `${fg.hex}/${bg.hex}/${terminal.fg}/${terminal.bg}/${reverse}`;
|
|
182
183
|
}
|
|
183
184
|
/**
|
|
184
|
-
* A ramp laid across the element: each lit cell's ink is the
|
|
185
|
+
* A ramp laid across the element: each lit cell's ink is the color `ramp`
|
|
185
186
|
* shows at `t` at the cell's column, the element's leftmost cell reading
|
|
186
187
|
* the ramp's first stop and its rightmost its last, each at its cell's
|
|
187
|
-
* centre — or, where that
|
|
188
|
+
* centre — or, where that color would not read on the cell's ground, the
|
|
188
189
|
* nearest that does (`legibleInks`, as `steppedInk` writes its steps). A
|
|
189
190
|
* ramp that moves (`ColorRamp.moving`) scrolls along the element and turns
|
|
190
191
|
* its hues, and the frame changes when the ramp does — every frame for a
|
|
@@ -192,51 +193,51 @@ function pairKey({ fg, bg, terminal, reverse }) {
|
|
|
192
193
|
* `Animation.constant(ramp)`. Lit cells are those whose ink is one of
|
|
193
194
|
* `colors`, named as a style names them (`"#a6e3a1"`, `"bright_green"`),
|
|
194
195
|
* opaque, and matched as the screen shows them at the frame's depth — so
|
|
195
|
-
* below truecolor every ink that lands on the same palette
|
|
196
|
-
* by default every
|
|
197
|
-
* rule's line by its
|
|
196
|
+
* below truecolor every ink that lands on the same palette color is lit —
|
|
197
|
+
* by default every color the element sets itself — never an ink the terminal supplies: a
|
|
198
|
+
* rule's line by its color and not its title, a panel's border and not its
|
|
198
199
|
* body. The element is where its frame lists
|
|
199
200
|
* cells, as the fireflies read it, so a padded or confined element has the
|
|
200
|
-
* ramp across what it shows. On a sixteen-
|
|
201
|
-
* (`
|
|
201
|
+
* ramp across what it shows. On a sixteen-color terminal nothing moves
|
|
202
|
+
* (`staticWhereUnchanged`): a gradient there is jumps between slots, and the cells
|
|
202
203
|
* hold as drawn.
|
|
203
204
|
*/
|
|
204
205
|
export function gradient(ramp, colors) {
|
|
205
206
|
const legible = legibleInks();
|
|
206
|
-
// [LAW:parse-dont-validate] Parsed at the call, so a misspelt
|
|
207
|
+
// [LAW:parse-dont-validate] Parsed at the call, so a misspelt color fails
|
|
207
208
|
// where it was written and not on some later frame.
|
|
208
209
|
const named = colors?.map(opaqueInk);
|
|
209
|
-
// [LAW:one-source-of-truth] A
|
|
210
|
+
// [LAW:one-source-of-truth] A color named as a style spells it is lit
|
|
210
211
|
// wherever a cell shows it, so it is resolved as `Effected` resolves a
|
|
211
212
|
// cell's ink: drawn at the frame's depth, then read on its terminal.
|
|
212
|
-
const shown = (spec, { depth,
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
const shown = (spec, { depth, terminal }) => (Style.fromColor(spec).drawnColors(depth).color ?? ColorSpec.default()).getTruecolor(terminal, true).hex;
|
|
214
|
+
const litIn = (drawn) => (named === undefined ? drawn.colors : new Set(named.map((spec) => shown(spec, drawn))));
|
|
215
|
+
return staticWhereUnchanged(["fg"], litIn, (drawn, lit) => {
|
|
215
216
|
const inkOn = legible(drawn);
|
|
216
217
|
const [left, right] = extent(drawn.cells().map((cell) => cell.col));
|
|
217
218
|
return Animation.map(ramp, (now) => {
|
|
218
219
|
const first = now.stops[0].at;
|
|
219
220
|
const span = now.stops[now.stops.length - 1].at - first;
|
|
220
221
|
const along = (col) => now.at(first + (span * (col - left + 0.5)) / (right - left + 1));
|
|
221
|
-
return (look,
|
|
222
|
+
return (look, { col, pair }) => (pair.terminal.fg || !lit.has(pair.fg.hex) ? look : { ...look, fg: inkOn(along(col), pair) });
|
|
222
223
|
});
|
|
223
224
|
});
|
|
224
225
|
}
|
|
225
226
|
/**
|
|
226
227
|
* `spec` parsed as an ink `gradient` lights. Opaque only: a translucent ink
|
|
227
228
|
* is drawn over the ground of each cell it is in, so it shows as no one
|
|
228
|
-
*
|
|
229
|
+
* color to match, and naming one is refused here.
|
|
229
230
|
*/
|
|
230
231
|
function opaqueInk(spec) {
|
|
231
232
|
const parsed = ColorSpec.parse(spec);
|
|
232
233
|
if (parsed.value !== undefined && parsed.value.alpha < 1)
|
|
233
|
-
throw new RangeError(`gradient lights opaque
|
|
234
|
+
throw new RangeError(`gradient lights opaque colors, and ${spec} is translucent`);
|
|
234
235
|
return parsed;
|
|
235
236
|
}
|
|
236
237
|
/**
|
|
237
|
-
* A loop as an `Effect` on
|
|
238
|
+
* A loop as an `Effect` on colors by hex, each touched at the share
|
|
238
239
|
* `share` gives it of the cell's strength in a frame drawing what `Drawn`
|
|
239
|
-
* says; a
|
|
240
|
+
* says; a color it gives none, and one the terminal supplies, stays as it is.
|
|
240
241
|
*/
|
|
241
242
|
export function atShares(loop, share) {
|
|
242
243
|
return (drawn) => {
|
|
@@ -245,28 +246,35 @@ export function atShares(loop, share) {
|
|
|
245
246
|
const field = loop.field(t, drawn);
|
|
246
247
|
return (look, cell) => {
|
|
247
248
|
const w = Math.round(clamp01(field(cell)) * LEVELS) / LEVELS;
|
|
248
|
-
|
|
249
|
-
|
|
249
|
+
// [LAW:one-source-of-truth] A share belongs to the color the element drew; the touch falls on what is there now.
|
|
250
|
+
const moved = (side) => touchedAt(loop.touch, look[side], (cell.pair.terminal[side] ? 0 : (shared.get(cell.pair[side].hex) ?? 0)) * w);
|
|
251
|
+
return { ...look, fg: moved("fg"), bg: moved("bg") };
|
|
250
252
|
};
|
|
251
253
|
});
|
|
252
254
|
};
|
|
253
255
|
}
|
|
254
256
|
/**
|
|
255
|
-
* `loop` as drawn at the frame's depth
|
|
256
|
-
*
|
|
257
|
+
* `loop` as drawn at the frame's depth and on the colors it moves on `sides`
|
|
258
|
+
* (`litIn`): where the depth writes only the
|
|
259
|
+
* sixteen ANSI slots, or no color, a move that shows at all is a jump to
|
|
257
260
|
* another slot, and a light on an element never jumps — so there it
|
|
258
|
-
* holds every cell still,
|
|
261
|
+
* holds every cell still, as it does where no cell that shows anything has
|
|
262
|
+
* one of those colors of its own there, and anywhere else it moves. [LAW:single-enforcer]
|
|
259
263
|
* `onColors`, `wheel` and `gradient` draw through this, so every loop in the catalogue
|
|
260
|
-
* meets the sixteen-
|
|
264
|
+
* meets the sixteen-color terminal here. `atShares` does not: a progress
|
|
261
265
|
* bar's pulse is the news that work is going on, and flashes a slot as
|
|
262
266
|
* Rich's does; nor does `steppedInk`, a signal, which steps slot to slot.
|
|
263
267
|
* A frame held still never changes, and says so, so a host drawing the
|
|
264
268
|
* loop on such a terminal draws it once.
|
|
265
269
|
*/
|
|
266
|
-
function
|
|
267
|
-
return (drawn) =>
|
|
270
|
+
function staticWhereUnchanged(sides, litIn, loop) {
|
|
271
|
+
return (drawn) => {
|
|
272
|
+
const lit = litIn(drawn);
|
|
273
|
+
const shows = ({ pair }) => sides.some((side) => !pair.terminal[side] && lit.has(pair[side].hex));
|
|
274
|
+
return onlySlots(drawn.depth) || !drawn.cells().some(shows) ? Animation.constant((look) => look) : loop(drawn, lit);
|
|
275
|
+
};
|
|
268
276
|
}
|
|
269
|
-
/** Whether `depth` writes nothing between the slots it has: the sixteen, at either sixteen-
|
|
277
|
+
/** Whether `depth` writes nothing between the slots it has: the sixteen, at either sixteen-color depth, or none. */
|
|
270
278
|
function onlySlots(depth) {
|
|
271
279
|
switch (depth) {
|
|
272
280
|
case ColorDepth.DEFAULT:
|
|
@@ -279,8 +287,8 @@ function onlySlots(depth) {
|
|
|
279
287
|
}
|
|
280
288
|
}
|
|
281
289
|
/**
|
|
282
|
-
* `color` under `touch` at strength `w`, and at strength 0 the
|
|
283
|
-
* exactly: an OKLCH round trip can land a step off, and at 16
|
|
290
|
+
* `color` under `touch` at strength `w`, and at strength 0 the color itself,
|
|
291
|
+
* exactly: an OKLCH round trip can land a step off, and at 16 colors a step
|
|
284
292
|
* off is another slot.
|
|
285
293
|
*/
|
|
286
294
|
function touchedAt(touch, color, w) {
|
|
@@ -305,7 +313,7 @@ export function requireZ(what, z) {
|
|
|
305
313
|
/**
|
|
306
314
|
* Each effect's curve as it was signed off. Every loop's period is a prime
|
|
307
315
|
* number of seconds, so no two loops come back into step within a sitting.
|
|
308
|
-
* The periods are long on purpose: at one frame a second a loop's
|
|
316
|
+
* The periods are long on purpose: at one frame a second a loop's color
|
|
309
317
|
* moves by no more than a just-noticeable step from one frame to the next.
|
|
310
318
|
*/
|
|
311
319
|
export const EFFECT_CURVES = {
|
|
@@ -326,8 +334,8 @@ export const SHIMMER_WIDTH = 24;
|
|
|
326
334
|
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
327
335
|
/**
|
|
328
336
|
* `w` of the way from `from` to `to` along a straight line in OKLab. Light
|
|
329
|
-
* falling on a
|
|
330
|
-
* toward the other
|
|
337
|
+
* falling on a color, or a color thinning into its ground, moves straight
|
|
338
|
+
* toward the other color; `Oklch.mix` goes round the hue circle instead, so
|
|
331
339
|
* blue lit by gold would pass through cyan on the way.
|
|
332
340
|
*/
|
|
333
341
|
function blend(from, to, w) {
|
|
@@ -339,9 +347,9 @@ function blend(from, to, w) {
|
|
|
339
347
|
return new Oklch((1 - w) * from.l + w * to.l, Math.hypot(a, b), Math.atan2(b, a) / rad, from.alpha);
|
|
340
348
|
}
|
|
341
349
|
/**
|
|
342
|
-
* A
|
|
343
|
-
* the way toward the light's
|
|
344
|
-
* light can warm a
|
|
350
|
+
* A color under `glow`: the one way every light here falls. It moves `w` of
|
|
351
|
+
* the way toward the light's color, and never below its own lightness —
|
|
352
|
+
* light can warm a color and lift it, never darken it. A color already
|
|
345
353
|
* lighter than the light only warms, and white, which has no warmer shade at
|
|
346
354
|
* its lightness, stays white.
|
|
347
355
|
*/
|
|
@@ -391,11 +399,11 @@ function given(shape, s, k) {
|
|
|
391
399
|
/** A firefly's brightest flash, in turns: it kindles quicker than it fades, and the rest of its turn is dark. */
|
|
392
400
|
const FLASH = [[0.24, 1], [0.6, 0]];
|
|
393
401
|
/**
|
|
394
|
-
* Pulse, as a gentle glow on one element of a screen: the
|
|
402
|
+
* Pulse, as a gentle glow on one element of a screen: the colors handed to
|
|
395
403
|
* it, an element's own, warm into `light` and settle back, over and over, the
|
|
396
404
|
* way an indicator breathes while something is waiting. Swing: how far into
|
|
397
405
|
* the light it goes at the top of a beat, 0–1. The element glows as one —
|
|
398
|
-
* every cell it is drawn in at once — and the
|
|
406
|
+
* every cell it is drawn in at once — and the colors not handed to it, the
|
|
399
407
|
* rest of the screen, stay as they are, so what pulses is what was chosen.
|
|
400
408
|
* Each element keeps a time of its own, a little quicker or slower than
|
|
401
409
|
* another's by `z`, and no beat is quite the last: the rhythm wanders ahead
|
|
@@ -420,8 +428,8 @@ export function pulse(curve, glow, z) {
|
|
|
420
428
|
// Each beat's own depth, 70% to the whole of the swing; it changes where
|
|
421
429
|
// the beat is at rest, so nothing jumps.
|
|
422
430
|
const depth = 0.7 + 0.3 * smoothstep(-0.5, 0.5, noise(n * 0.37, 2.1, z));
|
|
423
|
-
const
|
|
424
|
-
const level = curve.swing * curve.ease(depth *
|
|
431
|
+
const stepSequence = 0.5 - 0.5 * Math.cos(2 * Math.PI * (beats - n));
|
|
432
|
+
const level = curve.swing * curve.ease(depth * stepSequence);
|
|
425
433
|
return () => level;
|
|
426
434
|
};
|
|
427
435
|
return { touch: light(glow), field };
|
|
@@ -429,7 +437,7 @@ export function pulse(curve, glow, z) {
|
|
|
429
437
|
/**
|
|
430
438
|
* How many columns a passing light crosses in its period: its pace.
|
|
431
439
|
* Sunlight moves at a speed, not in a time set by how long the row is,
|
|
432
|
-
* so a wider element is crossed for longer, never faster, and no cell's
|
|
440
|
+
* so a wider element is crossed for longer, never faster, and no cell's color
|
|
433
441
|
* changes quicker on it from one frame to the next.
|
|
434
442
|
*/
|
|
435
443
|
const STRIDE = 80;
|
|
@@ -618,16 +626,16 @@ export function sparkle(curve, glow, z) {
|
|
|
618
626
|
}
|
|
619
627
|
/**
|
|
620
628
|
* A cell mixed toward the ground of the terminal the frame is drawn on
|
|
621
|
-
* (`Drawn.
|
|
629
|
+
* (`Drawn.terminal`) by how hidden it is: its fill toward the ground, and its ink
|
|
622
630
|
* toward that fill, so an element with fills of its own — a powerline strip —
|
|
623
|
-
* arrives and leaves whole, fill and words together, never as bare
|
|
631
|
+
* arrives and leaves whole, fill and words together, never as bare colored
|
|
624
632
|
* blocks. Invisible is ink and fill equal to the ground. `swing` scales the
|
|
625
633
|
* hiding — 1 reaches fully invisible. A cell wholly shown is returned as it was.
|
|
626
634
|
*/
|
|
627
|
-
export function
|
|
635
|
+
export function mask(visibility, swing) {
|
|
628
636
|
return (drawn) => {
|
|
629
637
|
// [LAW:one-source-of-truth] The ground is the terminal `Effected` draws on, never a second one handed here.
|
|
630
|
-
const ground = Oklch.fromRgba(drawn.
|
|
638
|
+
const ground = Oklch.fromRgba(drawn.terminal.backgroundColor);
|
|
631
639
|
return Animation.continuous((t) => {
|
|
632
640
|
const shows = visibility(t, drawn);
|
|
633
641
|
return (look, cell) => {
|
|
@@ -661,8 +669,8 @@ function order(z) {
|
|
|
661
669
|
* `schedule` is when its frame next changes on that clock, whatever the
|
|
662
670
|
* element draws: what a caller drawing the transition inside an animation —
|
|
663
671
|
* a lifecycle — says its frame does, since the frame it hands over carries
|
|
664
|
-
* no word of its own (`
|
|
665
|
-
* every frame, or on each hold of a beat (`
|
|
672
|
+
* no word of its own (`stampValidityFrame`, `../core/animation.ts`). Until done it changes
|
|
673
|
+
* every frame, or on each hold of a beat (`sampleAndHold`), and from then never.
|
|
666
674
|
* [LAW:one-source-of-truth] `done` is that schedule read as a fact — done
|
|
667
675
|
* from the first moment it never changes again — so no transition carries
|
|
668
676
|
* two answers to when it is over.
|
|
@@ -681,7 +689,7 @@ export class Transition {
|
|
|
681
689
|
/**
|
|
682
690
|
* `effect` run once over `seconds` as a `Transition`: what it draws until it
|
|
683
691
|
* is done, then held. [LAW:single-enforcer] The one rule for when a
|
|
684
|
-
* transition is done and what it answers after, for a
|
|
692
|
+
* transition is done and what it answers after, for a color effect and a
|
|
685
693
|
* glyph effect (`./glyph-effects.ts`) alike.
|
|
686
694
|
*/
|
|
687
695
|
export function transition(seconds, effect) {
|
|
@@ -696,21 +704,21 @@ export function transition(seconds, effect) {
|
|
|
696
704
|
const moving = effect(drawn);
|
|
697
705
|
return Animation.of({
|
|
698
706
|
at: (t) => moving.at(t),
|
|
699
|
-
|
|
707
|
+
validUntil: (t) => (ran.done(t) ? Number.POSITIVE_INFINITY : moving.validUntil(t)),
|
|
700
708
|
// Done, the frame settled when the transition did; until then it is the effect's.
|
|
701
|
-
|
|
709
|
+
validFrom: (t) => (ran.done(t) ? schedule.validFrom(t) : moving.validFrom(t)),
|
|
702
710
|
// A one-shot, reduced, cuts from its start to its end, whatever the effect it runs would hold as a loop.
|
|
703
711
|
reduced: () => Animation.cut(moving.at(0), moving.at(seconds)),
|
|
704
712
|
});
|
|
705
713
|
}, schedule);
|
|
706
714
|
return ran;
|
|
707
715
|
}
|
|
708
|
-
export function
|
|
716
|
+
export function sampleAndHold(stepped, stepSequence) {
|
|
709
717
|
// [LAW:types-are-the-program] An effect is a function and a transition an object: the representation is the discriminator.
|
|
710
718
|
if (typeof stepped === "function")
|
|
711
|
-
return (drawn) => Animation.
|
|
719
|
+
return (drawn) => Animation.sampleAndHold(stepped(drawn), stepSequence);
|
|
712
720
|
const { effect, schedule } = stepped;
|
|
713
|
-
return new Transition((drawn) => Animation.
|
|
721
|
+
return new Transition((drawn) => Animation.sampleAndHold(effect(drawn), stepSequence), Animation.sampleAndHold(schedule, stepSequence));
|
|
714
722
|
}
|
|
715
723
|
/**
|
|
716
724
|
* Of a transition's `seconds`, how long each cell's own change takes. Under
|
|
@@ -741,7 +749,7 @@ function ownChange(curve, place) {
|
|
|
741
749
|
const [first, last] = extent(cells.map(place));
|
|
742
750
|
const stagger = (last > first ? 1 - OWN : 0) * curve.seconds;
|
|
743
751
|
// One phase a frame, shifted per cell, so no cell builds its own.
|
|
744
|
-
const change =
|
|
752
|
+
const change = WrapMode.once(0, curve.seconds - stagger);
|
|
745
753
|
// A share of the way from the first place to the last, held to the order for a cell the frame does not list.
|
|
746
754
|
return (cell) => {
|
|
747
755
|
const along = last > first ? clamp01((place(cell) - first) / (last - first)) : 0;
|
|
@@ -768,7 +776,7 @@ export function fadeIn(curve, z) {
|
|
|
768
776
|
requireCurve(curve);
|
|
769
777
|
requireZ("fadeIn", z);
|
|
770
778
|
const risen = ownChange(curve, order(1.7 + z));
|
|
771
|
-
return transition(curve.seconds,
|
|
779
|
+
return transition(curve.seconds, mask((t, drawn) => {
|
|
772
780
|
const of = risen(t, drawn.cells());
|
|
773
781
|
return (cell) => curve.ease(of(cell).progress);
|
|
774
782
|
}, curve.swing));
|
|
@@ -797,7 +805,7 @@ export function dissolveOut(curve, z) {
|
|
|
797
805
|
const eddy = order(9.3 + z);
|
|
798
806
|
const faded = ownChange(curve, order(4.2 + z));
|
|
799
807
|
const { depth, from, to, maskFrom, maskTo, late } = DISSOLVE_SHAPE;
|
|
800
|
-
return transition(curve.seconds,
|
|
808
|
+
return transition(curve.seconds, mask((t, drawn) => {
|
|
801
809
|
const of = faded(t, drawn.cells());
|
|
802
810
|
return (cell) => {
|
|
803
811
|
const { along, progress } = of(cell);
|
|
@@ -829,22 +837,22 @@ const STRAY_SPREAD = 24;
|
|
|
829
837
|
/** `cell` as a place of its own, as wide as its glyph: where no region covers it. */
|
|
830
838
|
const ownPlace = (cell, width) => ({ row: cell.row, centre: cell.col + width / 2 });
|
|
831
839
|
/**
|
|
832
|
-
* The segments `drawn` draws, as where each
|
|
840
|
+
* The segments `drawn` draws, as where each color of each cell turns: a
|
|
833
841
|
* segment is a region of adjacent cells — beside or above one another — of
|
|
834
|
-
* one
|
|
842
|
+
* one color of the element's own, each cell carrying it as its ground or,
|
|
835
843
|
* on the terminal's ground, as its ink, read off `Drawn.all` once a frame.
|
|
836
844
|
* So a panel's ground is one segment whatever words lie on it, a blank
|
|
837
845
|
* carrying the ink keeps a styled paragraph one segment across its spaces
|
|
838
|
-
* and lines, a caption in a bar's
|
|
846
|
+
* and lines, a caption in a bar's color written under the bar turns with
|
|
839
847
|
* it, and a blank in the terminal's ink splits a segment. A seam a joiner
|
|
840
|
-
* drew is in none, since its
|
|
841
|
-
* of a seam cell turns with the region of that
|
|
848
|
+
* drew is in none, since its colors are its neighbours' edges: each color
|
|
849
|
+
* of a seam cell turns with the region of that color past the seam on
|
|
842
850
|
* either side — a two-cell rail reads past its other cell — so a powerline
|
|
843
|
-
* cap keeps the
|
|
851
|
+
* cap keeps the color of the cell it caps, a custom joiner's handed cap
|
|
844
852
|
* included, and with the cell's own place where none is beside it — a
|
|
845
853
|
* neighbour's edge the frame does not show, handed at the seam's cell. Any
|
|
846
|
-
* other cell's
|
|
847
|
-
* neighbour's fill
|
|
854
|
+
* other cell's colors turn with the region covering it: a letter in a
|
|
855
|
+
* neighbour's fill color is a word, not a cap. A cell the frame lists
|
|
848
856
|
* nowhere, which a hand-built `Drawn` hands, is one column wide. The place
|
|
849
857
|
* is the region's centre column from the element's left edge, with its
|
|
850
858
|
* centre row: a region that grows by a column moves its centre by half of
|
|
@@ -856,10 +864,10 @@ function segmentsOf(drawn) {
|
|
|
856
864
|
const { colors } = drawn;
|
|
857
865
|
const at = (row, col) => `${row}:${col}`;
|
|
858
866
|
const listed = new Map();
|
|
859
|
-
// Every column a seam cell covers: what a seam's
|
|
867
|
+
// Every column a seam cell covers: what a seam's colors look past to the region beside the seam.
|
|
860
868
|
const seams = new Set();
|
|
861
|
-
// The
|
|
862
|
-
const
|
|
869
|
+
// The color a cell is a region of: its own ground, else on the terminal's ground its own ink, else none.
|
|
870
|
+
const regionColor = (cell) => {
|
|
863
871
|
if (cell.seam)
|
|
864
872
|
return undefined;
|
|
865
873
|
const own = (terminal, hex) => (!terminal && colors.has(hex) ? hex : undefined);
|
|
@@ -870,7 +878,7 @@ function segmentsOf(drawn) {
|
|
|
870
878
|
const index = new Map();
|
|
871
879
|
for (const cell of drawn.all()) {
|
|
872
880
|
listed.set(at(cell.row, cell.col), cell);
|
|
873
|
-
const hex =
|
|
881
|
+
const hex = regionColor(cell);
|
|
874
882
|
for (let col = cell.col; col < cell.col + cell.width; col++) {
|
|
875
883
|
if (cell.seam)
|
|
876
884
|
seams.add(at(cell.row, col));
|
|
@@ -880,7 +888,7 @@ function segmentsOf(drawn) {
|
|
|
880
888
|
covered.push({ row: cell.row, col, hex });
|
|
881
889
|
}
|
|
882
890
|
}
|
|
883
|
-
// Union-find over the covered columns: two of one
|
|
891
|
+
// Union-find over the covered columns: two of one color beside or above one another are one region.
|
|
884
892
|
const parent = covered.map((_, i) => i);
|
|
885
893
|
const find = (i) => {
|
|
886
894
|
while (parent[i] !== i) {
|
|
@@ -912,7 +920,7 @@ function segmentsOf(drawn) {
|
|
|
912
920
|
return i === undefined ? undefined : regions.get(find(i));
|
|
913
921
|
};
|
|
914
922
|
const placeOf = (region) => ({ row: (region.top + region.bottom) / 2, centre: (region.left + region.right + 1) / 2 });
|
|
915
|
-
// The region past the seam from `col` in `step`'s direction: what a seam's
|
|
923
|
+
// The region past the seam from `col` in `step`'s direction: what a seam's color turns with.
|
|
916
924
|
const pastSeam = (row, col, step) => {
|
|
917
925
|
let past = col;
|
|
918
926
|
while (seams.has(at(row, past)))
|
|
@@ -928,17 +936,17 @@ function segmentsOf(drawn) {
|
|
|
928
936
|
};
|
|
929
937
|
}
|
|
930
938
|
/**
|
|
931
|
-
*
|
|
939
|
+
* WheelEvent, as the color of daylight going round: every hue turning the whole
|
|
932
940
|
* way round once a period — far too slowly to be seen moving, so a glance
|
|
933
941
|
* sees a steady palette and an hour's absence a different one — each
|
|
934
942
|
* segment of the element at its own pace. Swing: how far the segments stray
|
|
935
943
|
* from one another, 0–1, 1 being half a turn apart. A segment is a region
|
|
936
|
-
* of adjacent cells of one
|
|
944
|
+
* of adjacent cells of one color of the element's own, carried as their
|
|
937
945
|
* ground or, on the terminal's ground, as their ink (`segmentsOf`), and it
|
|
938
|
-
* paces by where it sits, not by its
|
|
939
|
-
* regions of one fill at two places turn apart, and each
|
|
940
|
-
* a joiner drew between them turns with the region of that
|
|
941
|
-
* it, so a powerline cap keeps the
|
|
946
|
+
* paces by where it sits, not by its color: two
|
|
947
|
+
* regions of one fill at two places turn apart, and each color of the seam
|
|
948
|
+
* a joiner drew between them turns with the region of that color beside
|
|
949
|
+
* it, so a powerline cap keeps the color of the cell it caps. An ink that
|
|
942
950
|
* is no fill turns with the ground it sits on, so a cell's words and fill
|
|
943
951
|
* turn together. A segment's stray is slow noise at its place, at
|
|
944
952
|
* `STRAY_PERIODS` in time and `STRAY_SPREAD` across the element, so
|
|
@@ -947,13 +955,13 @@ function segmentsOf(drawn) {
|
|
|
947
955
|
* column moves its hue a little, and those it pushes along as little. Only the
|
|
948
956
|
* stray reads the place: a place in the shared turn would jump a growing
|
|
949
957
|
* run by `360·t·Δ/P`, without bound in `t`.
|
|
950
|
-
* A
|
|
951
|
-
* words reads at the contrast it rested at; at 256
|
|
952
|
-
* lands on a
|
|
958
|
+
* A color keeps its luminance as it turns (`hueTurned`), so every pair of
|
|
959
|
+
* words reads at the contrast it rested at; at 256 colors a turned color
|
|
960
|
+
* lands on a color the depth writes, one that keeps every pair of words it
|
|
953
961
|
* is drawn in at its floor (`floorOf`) — its resting contrast or WCAG AA,
|
|
954
962
|
* whichever is lower — as `shares` keeps the light's (`turnedBounds`). The
|
|
955
|
-
* turn only happens to the element's own
|
|
956
|
-
* terminal's. On a sixteen-
|
|
963
|
+
* turn only happens to the element's own colors (`Drawn.colors`), never the
|
|
964
|
+
* terminal's. On a sixteen-color terminal nothing turns (`staticWhereUnchanged`).
|
|
957
965
|
*/
|
|
958
966
|
export function wheel(curve, z) {
|
|
959
967
|
requireCurve(curve);
|
|
@@ -970,47 +978,53 @@ export function wheel(curve, z) {
|
|
|
970
978
|
// Measured from where it stood at the start, as pulse's wander is, so the
|
|
971
979
|
// first frame turns nothing and the second has turned only a frame's worth.
|
|
972
980
|
const turned = (place, t) => (360 * t) / P + curve.swing * STRAY * ((stray(place, t) - stray(place, 0)) / 1.5);
|
|
973
|
-
// A turned
|
|
981
|
+
// A turned color's bounds rest on what a frame draws, never on `t`: settled once for each.
|
|
974
982
|
const settled = new WeakMemo();
|
|
975
|
-
return
|
|
976
|
-
const { colors, fills, depth,
|
|
977
|
-
// Whether a slot shows one of the element's
|
|
978
|
-
const own = (
|
|
979
|
-
// The
|
|
980
|
-
// kept: itself if a fill or beside no own
|
|
983
|
+
return staticWhereUnchanged(SIDES, ({ colors }) => colors, (drawn) => {
|
|
984
|
+
const { colors, fills, depth, terminal } = drawn;
|
|
985
|
+
// Whether a slot shows one of the element's colors: never the terminal's, whatever its hex.
|
|
986
|
+
const own = (supplied, color) => !supplied && colors.has(color.hex);
|
|
987
|
+
// The color an own color is bounded with, under which its bounds are
|
|
988
|
+
// kept: itself if a fill or beside no own color, else the ground beside
|
|
981
989
|
// it. Where it turns is `segmentsOf`'s, not this.
|
|
982
990
|
const boundWith = (color, other, theirs) => (fills.has(color.hex) || !theirs ? color.hex : other.hex);
|
|
983
|
-
// The bounds a
|
|
991
|
+
// The bounds a color turns within, by the color it is bounded with: one bound for every place a frame writes it at.
|
|
984
992
|
const keyOf = (color, bound) => `${color.hex} ${bound}`;
|
|
985
|
-
const bounds = settled.get(
|
|
993
|
+
const bounds = settled.get(terminal, () => new Memo()).get(drawnKey(drawn), () => turnedBounds(drawn.pairs, depth, (pair, side) => {
|
|
986
994
|
const other = side === "fg" ? "bg" : "fg";
|
|
987
995
|
return own(pair.terminal[side], pair[side]) ? keyOf(pair[side], boundWith(pair[side], pair[other], own(pair.terminal[other], pair[other]))) : undefined;
|
|
988
996
|
}));
|
|
989
|
-
// 256
|
|
997
|
+
// 256 colors writes a turned color as one it has an index for; truecolor
|
|
990
998
|
// as one of the two either side of its luminance.
|
|
991
|
-
const indexed = measuredIndexed(depth,
|
|
992
|
-
// Where each cell's
|
|
999
|
+
const indexed = measuredIndexed(depth, terminal);
|
|
1000
|
+
// Where each cell's colors turn, read off the frame's listing once.
|
|
993
1001
|
const placeOf = segmentsOf(drawn);
|
|
994
1002
|
return Animation.continuous((t) => {
|
|
995
|
-
// [LAW:one-source-of-truth] A
|
|
1003
|
+
// [LAW:one-source-of-truth] A color turned at a place within a
|
|
996
1004
|
// segment's bounds is written once a frame, wherever it falls: the
|
|
997
1005
|
// key is the bounds' key with the place, so one entry never serves
|
|
998
1006
|
// two bounds.
|
|
999
1007
|
const written = new Map();
|
|
1000
|
-
|
|
1008
|
+
// The bounds are measured from the color the element drew; a layer
|
|
1009
|
+
// beneath may have lit it, so they travel with it, and a lit color
|
|
1010
|
+
// turns round its own luminance rather than snapping back unturned.
|
|
1011
|
+
const choose = (color, rested, bound, place) => {
|
|
1001
1012
|
const turn = hueTurned(color, turned(place, t));
|
|
1002
|
-
|
|
1013
|
+
const { lo, hi } = bounds.get(keyOf(rested, bound)) ?? UNBOUNDED;
|
|
1014
|
+
const lit = stop(color) - stop(rested);
|
|
1015
|
+
return nearestWithin(turn.looks, color, depth === ColorDepth.TRUECOLOR ? turn.either.map(measured) : indexed, { lo: lo + lit, hi: hi + lit });
|
|
1003
1016
|
};
|
|
1004
|
-
const write = (color, bound, place) => {
|
|
1005
|
-
const key = `${keyOf(
|
|
1006
|
-
const found = written.get(key) ?? choose(color, bound, place);
|
|
1017
|
+
const write = (color, rested, bound, place) => {
|
|
1018
|
+
const key = `${color.hex} ${keyOf(rested, bound)} ${place.row}:${place.centre}`;
|
|
1019
|
+
const found = written.get(key) ?? choose(color, rested, bound, place);
|
|
1007
1020
|
written.set(key, found);
|
|
1008
1021
|
return found;
|
|
1009
1022
|
};
|
|
1010
1023
|
return (shown, cell) => {
|
|
1011
|
-
const
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1024
|
+
const { pair } = cell;
|
|
1025
|
+
const mine = { fg: own(pair.terminal.fg, pair.fg), bg: own(pair.terminal.bg, pair.bg) };
|
|
1026
|
+
const turn = (side, other) => mine[side] ? write(shown[side], pair[side], boundWith(pair[side], pair[other], mine[other]), placeOf(cell, pair[side].hex)) : shown[side];
|
|
1027
|
+
return { ...shown, fg: turn("fg", "bg"), bg: turn("bg", "fg") };
|
|
1014
1028
|
};
|
|
1015
1029
|
});
|
|
1016
1030
|
});
|
|
@@ -1018,19 +1032,25 @@ export function wheel(curve, z) {
|
|
|
1018
1032
|
/**
|
|
1019
1033
|
* `color` turned `degrees` round the hue circle at its own relative
|
|
1020
1034
|
* luminance, and at its own chroma where the gamut has it. WCAG contrast is
|
|
1021
|
-
* a ratio of luminances, so a pair of
|
|
1035
|
+
* a ratio of luminances, so a pair of colors each turned this way reads at
|
|
1022
1036
|
* the contrast it did; one turned at its OKLCH lightness instead does not,
|
|
1023
1037
|
* since equal lightness is not equal luminance across hues. The lightness
|
|
1024
1038
|
* is found by bisection, as `ensureContrastToward` finds one, and 8-bit
|
|
1025
|
-
* rounding lands the luminance between two
|
|
1026
|
-
* degrees the
|
|
1039
|
+
* rounding lands the luminance between two colors, so both are kept. At 0
|
|
1040
|
+
* degrees the color itself, exactly: an OKLCH round trip can land a step
|
|
1027
1041
|
* off (`touchedAt`).
|
|
1028
1042
|
*/
|
|
1029
1043
|
function hueTurned(color, degrees) {
|
|
1030
|
-
|
|
1031
|
-
|
|
1044
|
+
return degrees === 0 ? { looks: color, either: [color] } : atLuminance(color, degrees, relativeLuminance(color));
|
|
1045
|
+
}
|
|
1046
|
+
/** `color` with its chroma taken out. */
|
|
1047
|
+
function greyOf(color) {
|
|
1048
|
+
const { l, h, alpha } = Oklch.fromRgba(color);
|
|
1049
|
+
return new Oklch(l, 0, h, alpha).toRgba();
|
|
1050
|
+
}
|
|
1051
|
+
/** `color` turned `degrees` round the hue circle and brought to `luminance`: the two 8-bit colors either side of it. */
|
|
1052
|
+
function atLuminance(color, degrees, luminance) {
|
|
1032
1053
|
const from = Oklch.fromRgba(color);
|
|
1033
|
-
const luminance = relativeLuminance(color);
|
|
1034
1054
|
const at = (l) => new Oklch(l, from.c, from.h + degrees, from.alpha).toRgba();
|
|
1035
1055
|
let [dim, bright] = [0, 1];
|
|
1036
1056
|
for (let i = 0; i < 24; i++) {
|
|
@@ -1040,34 +1060,34 @@ function hueTurned(color, degrees) {
|
|
|
1040
1060
|
return { looks: at(bright), either: [at(dim), at(bright)] };
|
|
1041
1061
|
}
|
|
1042
1062
|
/**
|
|
1043
|
-
* Where a
|
|
1044
|
-
* black. Two
|
|
1045
|
-
* stops, so a contrast to keep is a distance a
|
|
1063
|
+
* Where a color sits on WCAG's contrast scale: the log of its contrast with
|
|
1064
|
+
* black. Two colors' contrast ratio is `exp` of the gap between their
|
|
1065
|
+
* stops, so a contrast to keep is a distance a color may move.
|
|
1046
1066
|
*/
|
|
1047
1067
|
const stop = (color) => Math.log(contrastRatio(color, new ColorRgba(0, 0, 0)));
|
|
1048
1068
|
const UNBOUNDED = { lo: -Infinity, hi: Infinity };
|
|
1049
|
-
/** Bounds that admit no turn: the
|
|
1069
|
+
/** Bounds that admit no turn: the color is written as it rests. */
|
|
1050
1070
|
const STILL = { lo: Infinity, hi: -Infinity };
|
|
1051
1071
|
/**
|
|
1052
|
-
* The widest 8-bit rounding parts `hueTurned`'s two
|
|
1072
|
+
* The widest 8-bit rounding parts `hueTurned`'s two colors, in stops. They
|
|
1053
1073
|
* are one point of the hue rounded two ways, so they differ by at most a
|
|
1054
|
-
* step in each channel, and a channel of a
|
|
1074
|
+
* step in each channel, and a channel of a color that dark or light can
|
|
1055
1075
|
* take a step only so large: worst near a luminance of 0.05, at 3.03% of
|
|
1056
1076
|
* contrast. Bounds at least this wide admit one of the two whatever the
|
|
1057
|
-
* hue; narrower, a
|
|
1077
|
+
* hue; narrower, a color would turn at some hues and snap back to rest at
|
|
1058
1078
|
* the next, so it holds still.
|
|
1059
1079
|
*/
|
|
1060
1080
|
const ROUNDING_GAP = Math.log(1.031);
|
|
1061
1081
|
/**
|
|
1062
|
-
* How far each turned
|
|
1082
|
+
* How far each turned color's luminance may land from the one it rests at,
|
|
1063
1083
|
* keyed as `turnedAs` keys each side of a pair — none for a side that holds
|
|
1064
1084
|
* still — so that every pair of `pairs` reads at its floor (`floorOf`) at
|
|
1065
|
-
* `depth` however each of its
|
|
1085
|
+
* `depth` however each of its colors lands within its bounds. Bounds
|
|
1066
1086
|
* narrower than `ROUNDING_GAP` are `STILL`.
|
|
1067
1087
|
*
|
|
1068
1088
|
* A pair's contrast to spare above its floor, as a gap between stops, is
|
|
1069
1089
|
* shared out evenly between its sides that turn: the lighter may land that
|
|
1070
|
-
* much darker, the darker that much lighter, and never further. So a
|
|
1090
|
+
* much darker, the darker that much lighter, and never further. So a color
|
|
1071
1091
|
* bounded once for every pair it is drawn in is bounded by each of them at
|
|
1072
1092
|
* once, and every way a frame writes it — the wheel turns it at each place
|
|
1073
1093
|
* it falls — lands within them. A pair whose floor is 1:1 holds however it
|
|
@@ -1090,13 +1110,13 @@ function turnedBounds(pairs, depth, turnedAs) {
|
|
|
1090
1110
|
return new Map([...bounds].map(([key, { lo, hi }]) => [key, hi - lo < ROUNDING_GAP ? STILL : { lo, hi }]));
|
|
1091
1111
|
}
|
|
1092
1112
|
const measured = (color) => ({ color, looks: Oklch.fromRgba(color), stop: stop(color) });
|
|
1093
|
-
/** `ColorSpec.indexed`'s
|
|
1113
|
+
/** `ColorSpec.indexed`'s colors, measured once for each terminal and depth. */
|
|
1094
1114
|
const measuredMemo = new WeakMemo();
|
|
1095
|
-
function measuredIndexed(depth,
|
|
1096
|
-
return measuredMemo.get(ColorSpec.indexed(depth,
|
|
1115
|
+
function measuredIndexed(depth, terminal) {
|
|
1116
|
+
return measuredMemo.get(ColorSpec.indexed(depth, terminal), (indexed) => indexed.map(([, color]) => measured(color)));
|
|
1097
1117
|
}
|
|
1098
1118
|
/**
|
|
1099
|
-
* The
|
|
1119
|
+
* The color that looks nearest `color` (OKLab's ΔE): `rest`, or one of
|
|
1100
1120
|
* `candidates` whose luminance is within `bounds`. `rest` is what the bounds
|
|
1101
1121
|
* are measured from, so it is always offered; ties go to it, then to the
|
|
1102
1122
|
* earlier.
|
|
@@ -1115,24 +1135,138 @@ function nearestOf(color, offered) {
|
|
|
1115
1135
|
}
|
|
1116
1136
|
return best?.[0];
|
|
1117
1137
|
}
|
|
1138
|
+
/** What a stop's float arithmetic can cost: a pair at its floor sits exactly on its bound. */
|
|
1139
|
+
const MEASURE = 1e-9;
|
|
1140
|
+
/**
|
|
1141
|
+
* The stops a color written at `depth` can land on: everything between black
|
|
1142
|
+
* and white in truecolor, and otherwise what the depth has an index for, so a
|
|
1143
|
+
* sixteen-color terminal's reach is its slots.
|
|
1144
|
+
*/
|
|
1145
|
+
function reachOf(depth, terminal) {
|
|
1146
|
+
return depth === ColorDepth.TRUECOLOR ? [0, Math.log(21)] : extent(measuredIndexed(depth, terminal).map((candidate) => candidate.stop));
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Where each ground of `pairs` may land so that every ink drawn on it keeps
|
|
1150
|
+
* room to read on the side of it the ink rested on: a light ink caps its ground
|
|
1151
|
+
* at the floor below the lightest the depth writes, a dark one bars it from the
|
|
1152
|
+
* floor above the darkest. Where a ground rests is always within: a depth also
|
|
1153
|
+
* writes the terminal's own color, which at sixteen colors lies past its
|
|
1154
|
+
* terminal's reach, and a ground no layer moved comes back as it was. These are
|
|
1155
|
+
* what the depth's reach leaves, so a ground any one effect moves is inside them.
|
|
1156
|
+
*/
|
|
1157
|
+
function groundBounds(pairs, depth, terminal) {
|
|
1158
|
+
const [bottom, top] = reachOf(depth, terminal);
|
|
1159
|
+
const bounds = new Map();
|
|
1160
|
+
for (const pair of pairs) {
|
|
1161
|
+
const [gap, rests] = [Math.log(floorOf(pair, depth)), stop(pair.bg)];
|
|
1162
|
+
const { lo, hi } = bounds.get(pair.bg.hex) ?? UNBOUNDED;
|
|
1163
|
+
bounds.set(pair.bg.hex, stop(pair.fg) >= rests ? { lo, hi: Math.min(hi, Math.max(rests, top - gap)) } : { lo: Math.max(lo, Math.min(rests, bottom + gap)), hi });
|
|
1164
|
+
}
|
|
1165
|
+
return bounds;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Several effects on one element, as one effect: each layer is handed the look
|
|
1169
|
+
* the one before it left and the same cell, so later layers are on top, and each
|
|
1170
|
+
* recognises a cell by what the element drew (`EffectCell.pair`), never by the
|
|
1171
|
+
* color it is handed. A hue a layer turned onto another of the element's
|
|
1172
|
+
* colors is not mistaken for it, and turns once. It changes when the soonest
|
|
1173
|
+
* layer does, and with no layers it leaves every cell as drawn.
|
|
1174
|
+
*
|
|
1175
|
+
* Every word reads at its floor (`floorOf`), whatever the layers did and in
|
|
1176
|
+
* whatever order. Each loop proves that for colors as the element drew them,
|
|
1177
|
+
* and a color another layer moved voids the proof: light lands at another
|
|
1178
|
+
* luminance on a turned hue. So once the layers have moved a cell, its ground
|
|
1179
|
+
* lands where its inks can still read (`groundBounds`), a function of the color
|
|
1180
|
+
* alone so one fill stays one fill, and an ink that does not read on it slides
|
|
1181
|
+
* to the luminance that does, on the side of the ground it stands on while that
|
|
1182
|
+
* side has room. A layer that takes an ink across its ground crosses it here too:
|
|
1183
|
+
* nothing reads at the crossing, so the word jumps it. [LAW:single-enforcer]
|
|
1184
|
+
* Both are measured as `Effected` writes them, and both hand back what they were
|
|
1185
|
+
* given where it already holds, so a layer that proved its own floor is drawn as
|
|
1186
|
+
* it would be alone. A frame that takes a ground past its bounds is the one
|
|
1187
|
+
* exception, whatever its ink reads: a swap of ink and ground lands at the floor,
|
|
1188
|
+
* since a bound cannot ask what one cell's ink does without splitting a fill.
|
|
1189
|
+
* The floor outranks a color being the terminal's: a terminal
|
|
1190
|
+
* ink is moved here where a ground under it left it unreadable. The rule is the
|
|
1191
|
+
* same for one layer as for five: a veil (`veiled`, or a transition's `effect`)
|
|
1192
|
+
* stops at the floor here, so a transition goes in an `Effected` around this
|
|
1193
|
+
* one, as a lifecycle places it.
|
|
1194
|
+
*/
|
|
1195
|
+
export function layered(...effects) {
|
|
1196
|
+
const settled = new WeakMemo();
|
|
1197
|
+
return (drawn) => {
|
|
1198
|
+
const { depth, terminal } = drawn;
|
|
1199
|
+
const grounds = settled.get(terminal, () => new Memo()).get(drawnKey(drawn), () => groundBounds(drawn.pairs, depth, terminal));
|
|
1200
|
+
const words = new Set(drawn.pairs.map(pairKey));
|
|
1201
|
+
const offered = depth === ColorDepth.TRUECOLOR ? undefined : measuredIndexed(depth, terminal);
|
|
1202
|
+
// `color` as itself if it is within the first of `bounds`, else the nearest
|
|
1203
|
+
// color the depth writes there, else the same of the next; as itself where
|
|
1204
|
+
// none has any. In truecolor that is its hue at the bound's luminance, or a
|
|
1205
|
+
// grey there: a color keeps its chroma only so near black or white, and a
|
|
1206
|
+
// bound may be at either. `rested` is always offered, since a depth also
|
|
1207
|
+
// writes the terminal's own color, which it has no index for, and the
|
|
1208
|
+
// color a pair rested in is the one a bound was measured from.
|
|
1209
|
+
const landing = (color, rested, ...bounds) => {
|
|
1210
|
+
const at = stop(color);
|
|
1211
|
+
for (const { lo, hi } of bounds) {
|
|
1212
|
+
const inside = (stop) => stop >= lo - MEASURE && stop <= hi + MEASURE;
|
|
1213
|
+
if (inside(at))
|
|
1214
|
+
return color;
|
|
1215
|
+
const aim = 0.05 * Math.exp(Math.min(hi, Math.max(lo, at))) - 0.05;
|
|
1216
|
+
const found = nearestOf(color, [measured(rested), ...(offered ?? [color, greyOf(color)].flatMap((from) => atLuminance(from, 0, aim).either.map(measured)))].filter(({ stop }) => inside(stop)));
|
|
1217
|
+
if (found !== undefined)
|
|
1218
|
+
return found.color;
|
|
1219
|
+
}
|
|
1220
|
+
return color;
|
|
1221
|
+
};
|
|
1222
|
+
const floored = (pair, moved) => {
|
|
1223
|
+
// [LAW:single-enforcer] Every measure below reads the colors as `Effected`
|
|
1224
|
+
// writes them: rounded to the depth, and a translucent one laid on what is
|
|
1225
|
+
// beneath it, so a color that passes here cannot draw as one that fails.
|
|
1226
|
+
const ground = landing(Effected.shows(pair, moved, depth, terminal).bg, pair.bg, grounds.get(pair.bg.hex) ?? UNBOUNDED);
|
|
1227
|
+
const { fg } = Effected.shows(pair, { fg: moved.fg, bg: ground }, depth, terminal);
|
|
1228
|
+
const floor = floorOf(pair, depth);
|
|
1229
|
+
if (!words.has(pairKey(pair)) || contrastRatio(fg, ground) >= floor)
|
|
1230
|
+
return { fg, bg: ground };
|
|
1231
|
+
const gap = Math.log(floor);
|
|
1232
|
+
const [above, below] = [{ lo: stop(ground) + gap, hi: Infinity }, { lo: -Infinity, hi: stop(ground) - gap }];
|
|
1233
|
+
// The side it stands on, not the one it rested on: a painter's ink may
|
|
1234
|
+
// stand across the ground, and aiming it back would strobe it there and
|
|
1235
|
+
// back each time a move took it under the floor.
|
|
1236
|
+
const [near, far] = stop(fg) >= stop(ground) ? [above, below] : [below, above];
|
|
1237
|
+
return { fg: landing(fg, pair.fg, near, far), bg: ground };
|
|
1238
|
+
};
|
|
1239
|
+
return Animation.map(Animation.all(effects.map((effect) => effect(drawn))), (frames) => {
|
|
1240
|
+
// A color pair lands once a frame, wherever it falls. [LAW:one-source-of-truth]
|
|
1241
|
+
const landed = new Map();
|
|
1242
|
+
return (look, cell) => {
|
|
1243
|
+
const moved = frames.reduce((under, frame) => frame(under, cell), look);
|
|
1244
|
+
const key = `${pairKey(cell.pair)} ${moved.fg.hex} ${moved.bg.hex}`;
|
|
1245
|
+
const colors = landed.get(key) ?? floored(cell.pair, moved);
|
|
1246
|
+
landed.set(key, colors);
|
|
1247
|
+
return { ...moved, ...colors };
|
|
1248
|
+
};
|
|
1249
|
+
});
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1118
1252
|
/**
|
|
1119
|
-
* A signal: every cell's ink stepping through `steps`'
|
|
1120
|
-
* one
|
|
1121
|
-
* chevrons in bright changing
|
|
1253
|
+
* A signal: every cell's ink stepping through `steps`' colors on a beat,
|
|
1254
|
+
* one color a hold, looping, for the time it is handed — a gauge's
|
|
1255
|
+
* chevrons in bright changing colors. The ground is left as the cell draws
|
|
1122
1256
|
* it, and the words stay legible on it: each step's ink is written as the
|
|
1123
|
-
*
|
|
1257
|
+
* color nearest the step's (OKLab's ΔE) that the depth draws and that
|
|
1124
1258
|
* reads on the cell's ground, as `Effected` writes it, at the cell's floor
|
|
1125
1259
|
* (`floorOf`) — its resting contrast or `LEGIBLE`, whichever is lower, the
|
|
1126
1260
|
* floor every loop here keeps. In truecolor that is the step itself, or its
|
|
1127
1261
|
* hue and chroma at the lightness nearest it that reaches the floor
|
|
1128
|
-
* (`ensureContrast`, the one slide a template's `
|
|
1129
|
-
* below truecolor, the nearest of the
|
|
1130
|
-
* sixteen-
|
|
1131
|
-
* that does (`
|
|
1262
|
+
* (`ensureContrast`, the one slide a template's `ensureContrast` makes too);
|
|
1263
|
+
* below truecolor, the nearest of the colors the depth has an index for, so on a
|
|
1264
|
+
* sixteen-color terminal it steps from slot to slot, the one effect here
|
|
1265
|
+
* that does (`staticWhereUnchanged`). A cell whose ground no offered color reads
|
|
1132
1266
|
* on keeps the ink it rested in.
|
|
1133
1267
|
*
|
|
1134
1268
|
* The search is settled once for each ground and step — below truecolor,
|
|
1135
|
-
* which
|
|
1269
|
+
* which colors read on a ground once for that ground — so a frame
|
|
1136
1270
|
* inside a step redoes none, and nor does a later pass of the beat: at sixty
|
|
1137
1271
|
* frames a second a one-second step is sixty identical frames, and the
|
|
1138
1272
|
* frame changes when the beat does: the signal is the beat mapped to inks.
|
|
@@ -1140,7 +1274,7 @@ function nearestOf(color, offered) {
|
|
|
1140
1274
|
* nothing here reads a clock.
|
|
1141
1275
|
*/
|
|
1142
1276
|
export function steppedInk(steps) {
|
|
1143
|
-
const rhythm =
|
|
1277
|
+
const rhythm = stepSequence(steps.map((step) => step.seconds));
|
|
1144
1278
|
// A copy, as `beat` keeps of the holds: what the signal plays stays what
|
|
1145
1279
|
// `steps` said whatever the caller does to its array.
|
|
1146
1280
|
const inks = steps.map((step) => step.color);
|
|
@@ -1149,49 +1283,48 @@ export function steppedInk(steps) {
|
|
|
1149
1283
|
const inkOn = legible(drawn);
|
|
1150
1284
|
return Animation.map(rhythm, (i) => {
|
|
1151
1285
|
const step = inks[i];
|
|
1152
|
-
return (look,
|
|
1286
|
+
return (look, { pair }) => ({ ...look, fg: inkOn(step, pair) });
|
|
1153
1287
|
});
|
|
1154
1288
|
};
|
|
1155
1289
|
}
|
|
1156
1290
|
/**
|
|
1157
|
-
* `legibleInk` for the frame `Drawn` describes, of an ink on a cell's
|
|
1291
|
+
* `legibleInk` for the frame `Drawn` describes, of an ink on a cell's pair:
|
|
1158
1292
|
* settled once for each terminal, depth, pair and ink, so a frame drawing
|
|
1159
1293
|
* what an earlier one drew redoes no search. [LAW:single-enforcer] Every
|
|
1160
1294
|
* effect that writes an ink of the caller's choosing writes it through
|
|
1161
1295
|
* here — `steppedInk`'s steps and `gradient`'s ramp alike — so none of them
|
|
1162
|
-
* leaves a word unreadable. Below truecolor which of the depth's
|
|
1296
|
+
* leaves a word unreadable. Below truecolor which of the depth's colors
|
|
1163
1297
|
* read on a pair does not depend on the ink, so it is settled once for each
|
|
1164
1298
|
* pair: a ramp, whose every frame offers inks no frame offered before,
|
|
1165
|
-
* then costs a nearest-
|
|
1299
|
+
* then costs a nearest-color scan a cell, not a contrast test of the whole
|
|
1166
1300
|
* palette.
|
|
1167
1301
|
*/
|
|
1168
1302
|
function legibleInks() {
|
|
1169
1303
|
const settled = new WeakMemo();
|
|
1170
|
-
return ({ depth,
|
|
1171
|
-
const memo = settled.get(
|
|
1172
|
-
return (ink,
|
|
1173
|
-
const pair = { fg: look.fg, bg: look.bg, terminal: cell.terminal, reverse: cell.reverse };
|
|
1304
|
+
return ({ depth, terminal }) => {
|
|
1305
|
+
const memo = settled.get(terminal, () => ({ inks: new Memo(), readable: new Memo() }));
|
|
1306
|
+
return (ink, pair) => {
|
|
1174
1307
|
const key = `${depth} ${pairKey(pair)}`;
|
|
1175
|
-
const legibleOf = (offered) => legibleAmong(pair, offered, depth,
|
|
1308
|
+
const legibleOf = (offered) => legibleAmong(pair, offered, depth, terminal);
|
|
1176
1309
|
return memo.inks.get(`${key} ${ink.hex}`, () => {
|
|
1177
1310
|
const offered = depth === ColorDepth.TRUECOLOR
|
|
1178
1311
|
? legibleOf([ink, ensureContrast(ink, pair.bg, floorOf(pair, depth))].map(measured))
|
|
1179
|
-
: memo.readable.get(key, () => legibleOf(measuredIndexed(depth,
|
|
1312
|
+
: memo.readable.get(key, () => legibleOf(measuredIndexed(depth, terminal)));
|
|
1180
1313
|
return nearestOf(ink, offered)?.color ?? pair.fg;
|
|
1181
1314
|
});
|
|
1182
1315
|
};
|
|
1183
1316
|
};
|
|
1184
1317
|
}
|
|
1185
1318
|
/**
|
|
1186
|
-
* Of `offered`, the
|
|
1319
|
+
* Of `offered`, the colors that read on `pair`'s ground at `pair`'s floor,
|
|
1187
1320
|
* as `Effected` writes them (`shows`). [LAW:one-source-of-truth] Measured the
|
|
1188
|
-
* way `shares` and `wheel` measure: the written
|
|
1321
|
+
* way `shares` and `wheel` measure: the written colors, at the catalogue's
|
|
1189
1322
|
* floor. An ink none of them is nearest stays `pair`'s own.
|
|
1190
1323
|
*/
|
|
1191
|
-
function legibleAmong(pair, offered, depth,
|
|
1324
|
+
function legibleAmong(pair, offered, depth, terminal) {
|
|
1192
1325
|
const floor = floorOf(pair, depth);
|
|
1193
1326
|
return offered.filter(({ color }) => {
|
|
1194
|
-
const shown = Effected.shows(pair, { fg: color, bg: pair.bg }, depth,
|
|
1327
|
+
const shown = Effected.shows(pair, { fg: color, bg: pair.bg }, depth, terminal);
|
|
1195
1328
|
return contrastRatio(shown.fg, shown.bg) >= floor;
|
|
1196
1329
|
});
|
|
1197
1330
|
}
|
|
@@ -1203,7 +1336,7 @@ function legibleAmong(pair, offered, depth, theme) {
|
|
|
1203
1336
|
* (`ATTRIBUTE_DEFAULTS`) [LAW:one-type-per-behavior].
|
|
1204
1337
|
*/
|
|
1205
1338
|
export function blink({ attribute, off, on }) {
|
|
1206
|
-
const rhythm =
|
|
1339
|
+
const rhythm = stepSequence([off, on]);
|
|
1207
1340
|
const frames = [(look) => look, (look) => ({ ...look, attributes: { ...look.attributes, [attribute]: true } })];
|
|
1208
1341
|
return () => Animation.map(rhythm, (i) => frames[i]);
|
|
1209
1342
|
}
|
|
@@ -1220,7 +1353,7 @@ export const ATTRIBUTE_DEFAULTS = {
|
|
|
1220
1353
|
* Every effect in the catalogue by its name in `EFFECT_CURVES`, each made the
|
|
1221
1354
|
* one way: for an element, at its signed-off curve unless handed another,
|
|
1222
1355
|
* with the light and the width it was signed off with. A loop comes lit on
|
|
1223
|
-
* the element's own
|
|
1356
|
+
* the element's own colors (`onColors`); a transition comes as itself, to
|
|
1224
1357
|
* be told when it is done. [LAW:types-are-the-program] Keyed by the two
|
|
1225
1358
|
* name types, which `EFFECT_CURVES` is held to as well: a curve signed off
|
|
1226
1359
|
* does not compile until it is named a loop or a transition, and then not
|