@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.
Files changed (185) hide show
  1. package/README.md +7 -6
  2. package/THIRD-PARTY-NOTICES +42 -0
  3. package/dist/core/animation.d.ts +65 -46
  4. package/dist/core/animation.js +91 -67
  5. package/dist/core/ansi.d.ts +19 -12
  6. package/dist/core/ansi.js +114 -36
  7. package/dist/core/bidi-data.d.ts +14 -0
  8. package/dist/core/bidi-data.js +385 -0
  9. package/dist/core/bidi.d.ts +89 -0
  10. package/dist/core/bidi.js +615 -0
  11. package/dist/core/box.d.ts +9 -1
  12. package/dist/core/box.js +11 -0
  13. package/dist/core/cell-widths.d.ts +3 -2
  14. package/dist/core/cell-widths.js +93 -93
  15. package/dist/core/cells.d.ts +20 -0
  16. package/dist/core/cells.js +94 -6
  17. package/dist/core/clock.d.ts +1 -1
  18. package/dist/core/color.d.ts +56 -57
  19. package/dist/core/color.js +79 -81
  20. package/dist/core/console.d.ts +24 -7
  21. package/dist/core/console.js +27 -8
  22. package/dist/core/control-codes.d.ts +61 -0
  23. package/dist/core/control-codes.js +157 -0
  24. package/dist/core/easing.d.ts +40 -40
  25. package/dist/core/easing.js +40 -40
  26. package/dist/core/env.d.ts +6 -0
  27. package/dist/core/env.js +9 -1
  28. package/dist/core/export-html.js +5 -4
  29. package/dist/core/export-lines.d.ts +13 -11
  30. package/dist/core/export-lines.js +19 -15
  31. package/dist/core/export-svg.d.ts +3 -3
  32. package/dist/core/export-svg.js +10 -9
  33. package/dist/core/json.js +9 -2
  34. package/dist/core/markup.js +5 -4
  35. package/dist/core/memo.d.ts +1 -1
  36. package/dist/core/memo.js +1 -1
  37. package/dist/core/oklch.d.ts +14 -14
  38. package/dist/core/oklch.js +10 -10
  39. package/dist/core/orient.d.ts +68 -0
  40. package/dist/core/orient.js +224 -0
  41. package/dist/core/osc8.d.ts +12 -5
  42. package/dist/core/osc8.js +14 -7
  43. package/dist/core/paint.d.ts +9 -9
  44. package/dist/core/paint.js +3 -3
  45. package/dist/core/pretty.d.ts +1 -1
  46. package/dist/core/pretty.js +23 -19
  47. package/dist/core/protocol.d.ts +47 -26
  48. package/dist/core/protocol.js +31 -23
  49. package/dist/core/render.d.ts +19 -14
  50. package/dist/core/render.js +30 -17
  51. package/dist/core/seal.d.ts +26 -0
  52. package/dist/core/seal.js +75 -0
  53. package/dist/core/segment.d.ts +12 -4
  54. package/dist/core/segment.js +16 -6
  55. package/dist/core/strip.d.ts +83 -83
  56. package/dist/core/strip.js +70 -70
  57. package/dist/core/style.d.ts +46 -18
  58. package/dist/core/style.js +92 -56
  59. package/dist/core/text-order.d.ts +51 -0
  60. package/dist/core/text-order.js +50 -0
  61. package/dist/core/text.d.ts +12 -2
  62. package/dist/core/text.js +136 -90
  63. package/dist/core/timeline.d.ts +5 -5
  64. package/dist/core/timeline.js +11 -11
  65. package/dist/core/tween.d.ts +6 -6
  66. package/dist/core/tween.js +10 -10
  67. package/dist/core/view.d.ts +5 -5
  68. package/dist/core/view.js +15 -14
  69. package/dist/core/wrap.d.ts +24 -6
  70. package/dist/core/wrap.js +82 -16
  71. package/dist/host/app.d.ts +50 -9
  72. package/dist/host/app.js +120 -15
  73. package/dist/host/host-environment.d.ts +2 -2
  74. package/dist/host/host-environment.js +2 -2
  75. package/dist/host/index.d.ts +3 -2
  76. package/dist/host/index.js +1 -0
  77. package/dist/host/input.d.ts +42 -9
  78. package/dist/host/input.js +91 -7
  79. package/dist/host/terminal-colors.d.ts +18 -0
  80. package/dist/host/terminal-colors.js +46 -0
  81. package/dist/host/terminal-host.d.ts +4 -3
  82. package/dist/host/terminal-host.js +1 -1
  83. package/dist/index.d.ts +17 -16
  84. package/dist/index.js +11 -11
  85. package/dist/node/prompt.d.ts +1 -1
  86. package/dist/node/prompt.js +1 -1
  87. package/dist/node/terminal-host.d.ts +1 -1
  88. package/dist/node/traceback.js +2 -2
  89. package/dist/renderables/change.d.ts +1 -3
  90. package/dist/renderables/change.js +6 -8
  91. package/dist/renderables/effect.d.ts +88 -87
  92. package/dist/renderables/effect.js +124 -114
  93. package/dist/renderables/effects.d.ts +105 -76
  94. package/dist/renderables/effects.js +309 -176
  95. package/dist/renderables/embed.d.ts +1 -1
  96. package/dist/renderables/embed.js +2 -2
  97. package/dist/renderables/flexStrip.d.ts +7 -7
  98. package/dist/renderables/flexStrip.js +15 -15
  99. package/dist/renderables/frames.d.ts +7 -16
  100. package/dist/renderables/frames.js +12 -27
  101. package/dist/renderables/glyph-effect.d.ts +22 -23
  102. package/dist/renderables/glyph-effect.js +43 -41
  103. package/dist/renderables/glyph-effects.d.ts +33 -14
  104. package/dist/renderables/glyph-effects.js +90 -22
  105. package/dist/renderables/html-entities.d.ts +1 -1
  106. package/dist/renderables/html-entities.js +1 -1
  107. package/dist/renderables/lifecycle.d.ts +8 -10
  108. package/dist/renderables/lifecycle.js +17 -18
  109. package/dist/renderables/live.d.ts +2 -2
  110. package/dist/renderables/live.js +7 -7
  111. package/dist/renderables/markdown-parse.js +3 -3
  112. package/dist/renderables/markdown.js +4 -2
  113. package/dist/renderables/panel.js +7 -5
  114. package/dist/renderables/progress.d.ts +1 -1
  115. package/dist/renderables/progress.js +1 -1
  116. package/dist/renderables/progressBar.d.ts +3 -3
  117. package/dist/renderables/progressBar.js +21 -18
  118. package/dist/renderables/prompt.d.ts +1 -1
  119. package/dist/renderables/scroll.d.ts +1 -1
  120. package/dist/renderables/scroll.js +3 -3
  121. package/dist/renderables/spinner.d.ts +4 -4
  122. package/dist/renderables/spinner.js +10 -10
  123. package/dist/renderables/syntax.d.ts +2 -2
  124. package/dist/renderables/syntax.js +9 -4
  125. package/dist/renderables/table.js +5 -4
  126. package/dist/renderables/traceback.d.ts +1 -1
  127. package/dist/renderables/traceback.js +1 -1
  128. package/dist/renderables/tree.js +3 -3
  129. package/dist/template-bindings/animation-funcs.d.ts +60 -29
  130. package/dist/template-bindings/animation-funcs.js +126 -43
  131. package/dist/template-bindings/color-funcs.d.ts +14 -14
  132. package/dist/template-bindings/color-funcs.js +25 -25
  133. package/dist/template-bindings/index.d.ts +16 -14
  134. package/dist/template-bindings/index.js +17 -14
  135. package/dist/template-bindings/palette-funcs.d.ts +2 -2
  136. package/dist/template-bindings/palette-funcs.js +4 -4
  137. package/dist/template-bindings/style-funcs.d.ts +5 -5
  138. package/dist/template-bindings/style-funcs.js +16 -16
  139. package/dist/themes/{transpose.d.ts → adjust.d.ts} +23 -23
  140. package/dist/themes/{transpose.js → adjust.js} +28 -28
  141. package/dist/themes/buildPalette.d.ts +6 -5
  142. package/dist/themes/buildPalette.js +22 -22
  143. package/dist/themes/colorMath.d.ts +16 -16
  144. package/dist/themes/colorMath.js +32 -32
  145. package/dist/themes/data/ansi-tables.d.ts +2 -2
  146. package/dist/themes/data/ansi-tables.js +2 -2
  147. package/dist/themes/data/index.js +1 -4
  148. package/dist/themes/data/textual-ansi.js +0 -143
  149. package/dist/themes/data/types.d.ts +4 -4
  150. package/dist/themes/data/types.js +2 -2
  151. package/dist/themes/palette.d.ts +12 -12
  152. package/dist/themes/palette.js +16 -16
  153. package/dist/themes/ramp.d.ts +17 -17
  154. package/dist/themes/ramp.js +12 -12
  155. package/dist/themes/registry.d.ts +6 -8
  156. package/dist/themes/registry.js +6 -8
  157. package/dist/themes/terminalThemes.d.ts +4 -11
  158. package/dist/themes/terminalThemes.js +31 -47
  159. package/dist/widgets/button.d.ts +3 -3
  160. package/dist/widgets/button.js +8 -8
  161. package/dist/widgets/checkbox.d.ts +2 -2
  162. package/dist/widgets/checkbox.js +4 -4
  163. package/dist/widgets/dropdown.d.ts +3 -3
  164. package/dist/widgets/dropdown.js +7 -5
  165. package/dist/widgets/event-router.d.ts +6 -6
  166. package/dist/widgets/event-router.js +2 -2
  167. package/dist/widgets/focus-manager.d.ts +3 -3
  168. package/dist/widgets/focus-manager.js +3 -3
  169. package/dist/widgets/index.d.ts +2 -2
  170. package/dist/widgets/index.js +1 -1
  171. package/dist/widgets/ink.d.ts +3 -3
  172. package/dist/widgets/ink.js +4 -4
  173. package/dist/widgets/slider.d.ts +2 -2
  174. package/dist/widgets/static-item.d.ts +2 -2
  175. package/dist/widgets/text-input.d.ts +2 -2
  176. package/dist/widgets/text-input.js +5 -5
  177. package/dist/widgets/themed-widget.js +4 -4
  178. package/dist/widgets/toggle.d.ts +4 -4
  179. package/dist/widgets/toggle.js +6 -6
  180. package/dist/widgets/types.d.ts +6 -6
  181. package/dist/widgets/types.js +2 -2
  182. package/dist/widgets/widget-app.d.ts +10 -10
  183. package/dist/widgets/widget-app.js +8 -8
  184. package/dist/widgets/widget-base.d.ts +2 -2
  185. 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 colour ramp across an element and may move it. Each is an `Effect` for `Effected` (`./effect.ts`); the defaults
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 colours of daylight, fireflies, ink
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 colours on a beat, the news
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 colour. Of the list below it
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 colour can take while every cell it is drawn in
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
- * Colour moves are in OKLCH, where equal steps look equal. A curve's `ease`
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, Phase, beat } from "../core/easing.js";
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-colour cube colour changes in jumps between strengths, and no check of
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 colour written is one the depth holds exactly, so
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 colours `Effected` writes it in at `drawn`'s depth and theme.
91
- * A colour in a pair but not in `colors` takes none, and so does one
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
- * Colours are settled lightest first, each against every partner: one
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
- * colour of a cell takes the light first and its darker partner spends
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 colour left as it is always holds,
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, theme }, colors, touch) {
103
- // [LAW:one-source-of-truth] A slot shows a subject colour, by hex, only
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 colour no pair writes keeps no word legible, so nothing bounds its share:
111
- // a frame drawing a lit colour only under blanks and seams, or not at all,
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 colour, with the side it shows it on and the share its partner takes.
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 colours `Effected` writes it in, touched at each side's share.
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, theme);
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 colours, by hex, wherever a cell
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
- * colours a touched colour lands on a colour the slot can write, and the
148
- * contrast it spends is that colour's; on a sixteen-colour terminal nothing
149
- * moves (`stillOnSlots`). `colors` are the ones lit, by default every colour the element
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 colour the terminal supplies (`EffectCell.terminal`) is
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 colours an
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 = colors ?? drawn.colors;
163
+ const lit = litIn(drawn);
163
164
  const key = [drawnKey(drawn), "|", ...lit].join(" ");
164
- return settled.get(drawn.theme, () => new Memo()).get(key, () => shares(drawn, lit, loop.touch));
165
+ return settled.get(drawn.terminal, () => new Memo()).get(key, () => shares(drawn, lit, loop.touch));
165
166
  };
166
- return stillOnSlots(atShares(loop, settle));
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 colour `ramp`
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 colour would not read on the cell's ground, the
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 colour is lit —
196
- * by default every colour the element sets itself — never an ink the terminal supplies: a
197
- * rule's line by its colour and not its title, a panel's border and not 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-colour terminal nothing moves
201
- * (`stillOnSlots`): a gradient there is jumps between slots, and the cells
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 colour fails
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 colour named as a style spells it is lit
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, theme }) => (Style.fromColor(spec).drawnColors(depth).color ?? ColorSpec.default()).getTruecolor(theme, true).hex;
213
- return stillOnSlots((drawn) => {
214
- const lit = named === undefined ? drawn.colors : new Set(named.map((spec) => shown(spec, drawn)));
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, cell) => (cell.terminal.fg || !lit.has(look.fg.hex) ? look : { ...look, fg: inkOn(along(cell.col), look, cell) });
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
- * colour to match, and naming one is refused here.
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 colours, and ${spec} is translucent`);
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 colours by hex, each touched at the share
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 colour it gives none, and one the terminal supplies, stays as it is.
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
- const moved = (color, terminal) => touchedAt(loop.touch, color, (terminal ? 0 : (shared.get(color.hex) ?? 0)) * w);
249
- return { ...look, fg: moved(look.fg, cell.terminal.fg), bg: moved(look.bg, cell.terminal.bg) };
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: where the depth writes only the
256
- * sixteen ANSI slots, or no colour, a move that shows at all is a jump to
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, and anywhere else it moves. [LAW:single-enforcer]
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-colour terminal here. `atShares` does not: a progress
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 stillOnSlots(loop) {
267
- return (drawn) => (onlySlots(drawn.depth) ? Animation.constant((look) => look) : loop(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-colour depth, or none. */
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 colour itself,
283
- * exactly: an OKLCH round trip can land a step off, and at 16 colours a step
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 colour
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 colour, or a colour thinning into its ground, moves straight
330
- * toward the other colour; `Oklch.mix` goes round the hue circle instead, so
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 colour under `glow`: the one way every light here falls. It moves `w` of
343
- * the way toward the light's colour, and never below its own lightness —
344
- * light can warm a colour and lift it, never darken it. A colour already
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 colours handed to
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 colours not handed to it, 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 beat = 0.5 - 0.5 * Math.cos(2 * Math.PI * (beats - n));
424
- const level = curve.swing * curve.ease(depth * beat);
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 colour
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.theme`) by how hidden it is: its fill toward the ground, and its ink
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 coloured
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 veiled(visibility, swing) {
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.theme.backgroundColor);
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 (`stampFrame`, `../core/animation.ts`). Until done it changes
665
- * every frame, or on each hold of a beat (`onBeat`), and from then never.
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 colour effect and 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
- nextChange: (t) => (ran.done(t) ? Number.POSITIVE_INFINITY : moving.nextChange(t)),
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
- lastChange: (t) => (ran.done(t) ? schedule.lastChange(t) : moving.lastChange(t)),
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 onBeat(stepped, beat) {
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.onBeat(stepped(drawn), beat);
719
+ return (drawn) => Animation.sampleAndHold(stepped(drawn), stepSequence);
712
720
  const { effect, schedule } = stepped;
713
- return new Transition((drawn) => Animation.onBeat(effect(drawn), beat), Animation.onBeat(schedule, beat));
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 = Phase.once(0, curve.seconds - stagger);
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, veiled((t, drawn) => {
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, veiled((t, drawn) => {
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 colour of each cell turns: a
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 colour of the element's own, each cell carrying it as its ground or,
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 colour written under the bar turns with
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 colours are its neighbours' edges: each colour
841
- * of a seam cell turns with the region of that colour past the seam on
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 colour of the cell it caps, a custom joiner's handed cap
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 colours turn with the region covering it: a letter in a
847
- * neighbour's fill colour is a word, not a cap. A cell the frame lists
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 colours look past to the region beside the seam.
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 colour a cell is a region of: its own ground, else on the terminal's ground its own ink, else none.
862
- const regionColour = (cell) => {
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 = regionColour(cell);
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 colour beside or above one another are one region.
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 colour turns with.
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
- * Wheel, as the colour of daylight going round: every hue turning the whole
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 colour of the element's own, carried as their
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 colour: two
939
- * regions of one fill at two places turn apart, and each colour of the seam
940
- * a joiner drew between them turns with the region of that colour beside
941
- * it, so a powerline cap keeps the colour of the cell it caps. An ink that
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 colour keeps its luminance as it turns (`hueTurned`), so every pair of
951
- * words reads at the contrast it rested at; at 256 colours a turned colour
952
- * lands on a colour the depth writes, one that keeps every pair of words it
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 colours (`Drawn.colors`), never the
956
- * terminal's. On a sixteen-colour terminal nothing turns (`stillOnSlots`).
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 colour's bounds rest on what a frame draws, never on `t`: settled once for each.
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 stillOnSlots((drawn) => {
976
- const { colors, fills, depth, theme } = drawn;
977
- // Whether a slot shows one of the element's colours: never the terminal's, whatever its hex.
978
- const own = (terminal, color) => !terminal && colors.has(color.hex);
979
- // The colour an own colour is bounded with, under which its bounds are
980
- // kept: itself if a fill or beside no own colour, else the ground beside
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 colour turns within, by the colour it is bounded with: one bound for every place a frame writes it at.
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(theme, () => new Memo()).get(drawnKey(drawn), () => turnedBounds(drawn.pairs, depth, (pair, side) => {
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 colours writes a turned colour as one it has an index for; truecolor
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, theme);
992
- // Where each cell's colours turn, read off the frame's listing once.
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 colour turned at a place within 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
- const choose = (color, bound, place) => {
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
- return nearestWithin(turn.looks, color, depth === ColorDepth.TRUECOLOR ? turn.either.map(measured) : indexed, bounds.get(keyOf(color, bound)) ?? UNBOUNDED);
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(color, bound)} ${place.row}:${place.centre}`;
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 mine = { fg: own(cell.terminal.fg, shown.fg), bg: own(cell.terminal.bg, shown.bg) };
1012
- const turn = (color, ours, other, theirs) => ours ? write(color, boundWith(color, other, theirs), placeOf(cell, color.hex)) : color;
1013
- return { ...shown, fg: turn(shown.fg, mine.fg, shown.bg, mine.bg), bg: turn(shown.bg, mine.bg, shown.fg, mine.fg) };
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 colours each turned this way reads at
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 colours, so both are kept. At 0
1026
- * degrees the colour itself, exactly: an OKLCH round trip can land a step
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
- if (degrees === 0)
1031
- return { looks: color, either: [color] };
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 colour sits on WCAG's contrast scale: the log of its contrast with
1044
- * black. Two colours' contrast ratio is `exp` of the gap between their
1045
- * stops, so a contrast to keep is a distance a colour may move.
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 colour is written as it rests. */
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 colours, in stops. They
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 colour that dark or light can
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 colour would turn at some hues and snap back to rest at
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 colour's luminance may land from the one it rests at,
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 colours lands within its bounds. Bounds
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 colour
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 colours, measured once for each theme and depth. */
1113
+ /** `ColorSpec.indexed`'s colors, measured once for each terminal and depth. */
1094
1114
  const measuredMemo = new WeakMemo();
1095
- function measuredIndexed(depth, theme) {
1096
- return measuredMemo.get(ColorSpec.indexed(depth, theme), (indexed) => indexed.map(([, color]) => measured(color)));
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 colour that looks nearest `color` (OKLab's ΔE): `rest`, or one of
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`' colours on a beat,
1120
- * one colour a hold, looping, for the time it is handed — a gauge's
1121
- * chevrons in bright changing colours. The ground is left as the cell draws
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
- * colour nearest the step's (OKLab's ΔE) that the depth draws and that
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 `readableOn` makes too);
1129
- * below truecolor, the nearest of the colours the depth has an index for, so on a
1130
- * sixteen-colour terminal it steps from slot to slot, the one effect here
1131
- * that does (`stillOnSlots`). A cell whose ground no offered colour reads
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 colours read on a ground once for that ground — so a frame
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 = beat(steps.map((step) => step.seconds));
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, cell) => ({ ...look, fg: inkOn(step, look, cell) });
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 look:
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 colours
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-colour scan a cell, not a contrast test of the whole
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, theme }) => {
1171
- const memo = settled.get(theme, () => ({ inks: new Memo(), readable: new Memo() }));
1172
- return (ink, look, cell) => {
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, theme);
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, theme)));
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 colours that read on `pair`'s ground at `pair`'s floor,
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 colours, at the catalogue's
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, theme) {
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, theme);
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 = beat([off, on]);
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 colours (`onColors`); a transition comes as itself, to
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