@promptctl/rich-js 0.24.0 → 0.25.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 (163) hide show
  1. package/README.md +7 -0
  2. package/THIRD-PARTY-NOTICES +29 -1
  3. package/dist/core/anchor.d.ts +5 -0
  4. package/dist/core/anchor.js +11 -4
  5. package/dist/core/animation.d.ts +243 -0
  6. package/dist/core/animation.js +375 -0
  7. package/dist/core/ansi.js +2 -2
  8. package/dist/core/box.d.ts +7 -7
  9. package/dist/core/cell-widths.d.ts +9 -0
  10. package/dist/core/cell-widths.js +140 -0
  11. package/dist/core/cells.d.ts +40 -24
  12. package/dist/core/cells.js +151 -66
  13. package/dist/core/clock.d.ts +2 -2
  14. package/dist/core/clock.js +6 -4
  15. package/dist/core/color.d.ts +27 -10
  16. package/dist/core/color.js +21 -29
  17. package/dist/core/console.d.ts +28 -17
  18. package/dist/core/console.js +48 -43
  19. package/dist/core/easing.d.ts +140 -10
  20. package/dist/core/easing.js +328 -50
  21. package/dist/core/emoji.d.ts +1 -1
  22. package/dist/core/emoji.js +1 -2
  23. package/dist/core/export-html.js +2 -1
  24. package/dist/core/export-lines.js +3 -3
  25. package/dist/core/highlighter.d.ts +9 -8
  26. package/dist/core/highlighter.js +3 -2
  27. package/dist/core/json.js +3 -2
  28. package/dist/core/markup.js +16 -12
  29. package/dist/core/measure.js +3 -3
  30. package/dist/core/memo.d.ts +11 -2
  31. package/dist/core/memo.js +16 -0
  32. package/dist/core/oklch.js +17 -6
  33. package/dist/core/osc8.d.ts +1 -1
  34. package/dist/core/osc8.js +4 -5
  35. package/dist/core/paint.d.ts +2 -1
  36. package/dist/core/paint.js +4 -3
  37. package/dist/core/pattern.d.ts +21 -0
  38. package/dist/core/pattern.js +21 -0
  39. package/dist/core/place.js +2 -1
  40. package/dist/core/pretty.js +11 -12
  41. package/dist/core/protocol.d.ts +136 -21
  42. package/dist/core/protocol.js +203 -16
  43. package/dist/core/render.d.ts +15 -2
  44. package/dist/core/render.js +27 -6
  45. package/dist/core/segment.d.ts +31 -6
  46. package/dist/core/segment.js +68 -53
  47. package/dist/core/spinnerData.d.ts +1 -1
  48. package/dist/core/state.d.ts +53 -0
  49. package/dist/core/state.js +82 -0
  50. package/dist/core/strip.d.ts +11 -13
  51. package/dist/core/strip.js +126 -113
  52. package/dist/core/style.d.ts +47 -14
  53. package/dist/core/style.js +74 -34
  54. package/dist/core/text.d.ts +5 -8
  55. package/dist/core/text.js +133 -133
  56. package/dist/core/timeline.d.ts +109 -0
  57. package/dist/core/timeline.js +180 -0
  58. package/dist/core/tween.d.ts +49 -0
  59. package/dist/core/tween.js +71 -0
  60. package/dist/core/view.d.ts +92 -0
  61. package/dist/core/view.js +83 -0
  62. package/dist/host/app.d.ts +49 -21
  63. package/dist/host/app.js +72 -38
  64. package/dist/host/input.js +3 -4
  65. package/dist/index.d.ts +30 -15
  66. package/dist/index.js +20 -6
  67. package/dist/node/traceback.js +4 -3
  68. package/dist/renderables/change.d.ts +42 -0
  69. package/dist/renderables/change.js +112 -0
  70. package/dist/renderables/columns.d.ts +1 -1
  71. package/dist/renderables/columns.js +3 -3
  72. package/dist/renderables/constrain.js +2 -1
  73. package/dist/renderables/effect.d.ts +155 -92
  74. package/dist/renderables/effect.js +203 -136
  75. package/dist/renderables/effects.d.ts +137 -24
  76. package/dist/renderables/effects.js +276 -113
  77. package/dist/renderables/embed.d.ts +1 -1
  78. package/dist/renderables/embed.js +18 -9
  79. package/dist/renderables/flexStrip.js +2 -1
  80. package/dist/renderables/frames.d.ts +45 -43
  81. package/dist/renderables/frames.js +57 -45
  82. package/dist/renderables/glyph-effect.d.ts +158 -0
  83. package/dist/renderables/glyph-effect.js +237 -0
  84. package/dist/renderables/glyph-effects.d.ts +147 -0
  85. package/dist/renderables/glyph-effects.js +283 -0
  86. package/dist/renderables/group.d.ts +1 -1
  87. package/dist/renderables/group.js +2 -2
  88. package/dist/renderables/html-entities.d.ts +8 -0
  89. package/dist/renderables/html-entities.js +386 -0
  90. package/dist/renderables/layout.d.ts +10 -9
  91. package/dist/renderables/layout.js +42 -35
  92. package/dist/renderables/lifecycle.d.ts +77 -0
  93. package/dist/renderables/lifecycle.js +132 -0
  94. package/dist/renderables/list-view.d.ts +2 -2
  95. package/dist/renderables/list-view.js +11 -8
  96. package/dist/renderables/live.d.ts +36 -21
  97. package/dist/renderables/live.js +89 -42
  98. package/dist/renderables/markdown-parse.d.ts +109 -0
  99. package/dist/renderables/markdown-parse.js +1427 -0
  100. package/dist/renderables/markdown-url.d.ts +19 -0
  101. package/dist/renderables/markdown-url.js +314 -0
  102. package/dist/renderables/markdown.d.ts +5 -7
  103. package/dist/renderables/markdown.js +356 -683
  104. package/dist/renderables/padding.d.ts +7 -20
  105. package/dist/renderables/padding.js +29 -50
  106. package/dist/renderables/panel.d.ts +30 -32
  107. package/dist/renderables/panel.js +65 -109
  108. package/dist/renderables/progress.d.ts +83 -32
  109. package/dist/renderables/progress.js +186 -90
  110. package/dist/renderables/progressBar.d.ts +22 -12
  111. package/dist/renderables/progressBar.js +33 -10
  112. package/dist/renderables/prompt.js +3 -2
  113. package/dist/renderables/python-space.d.ts +13 -0
  114. package/dist/renderables/python-space.js +25 -0
  115. package/dist/renderables/scroll.d.ts +50 -0
  116. package/dist/renderables/scroll.js +106 -0
  117. package/dist/renderables/spinner.d.ts +57 -24
  118. package/dist/renderables/spinner.js +101 -49
  119. package/dist/renderables/status.js +1 -1
  120. package/dist/renderables/syntax.d.ts +10 -9
  121. package/dist/renderables/syntax.js +5 -3
  122. package/dist/renderables/table.d.ts +48 -46
  123. package/dist/renderables/table.js +170 -208
  124. package/dist/renderables/traceback.d.ts +3 -3
  125. package/dist/renderables/traceback.js +12 -10
  126. package/dist/renderables/tree.d.ts +28 -25
  127. package/dist/renderables/tree.js +108 -58
  128. package/dist/renderables/viewport.d.ts +77 -38
  129. package/dist/renderables/viewport.js +156 -72
  130. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  131. package/dist/template-bindings/animation-funcs.js +132 -14
  132. package/dist/template-bindings/index.d.ts +5 -5
  133. package/dist/template-bindings/index.js +7 -6
  134. package/dist/themes/ramp.d.ts +62 -4
  135. package/dist/themes/ramp.js +82 -4
  136. package/dist/themes/registry.js +5 -7
  137. package/dist/widgets/button.js +3 -2
  138. package/dist/widgets/checkbox.js +4 -3
  139. package/dist/widgets/dropdown.d.ts +3 -1
  140. package/dist/widgets/dropdown.js +20 -15
  141. package/dist/widgets/event-router.js +4 -2
  142. package/dist/widgets/focus-manager.d.ts +23 -14
  143. package/dist/widgets/focus-manager.js +34 -33
  144. package/dist/widgets/hit.d.ts +6 -0
  145. package/dist/widgets/hit.js +8 -1
  146. package/dist/widgets/index.d.ts +1 -1
  147. package/dist/widgets/observable.d.ts +23 -0
  148. package/dist/widgets/observable.js +43 -0
  149. package/dist/widgets/slider.d.ts +1 -1
  150. package/dist/widgets/slider.js +12 -3
  151. package/dist/widgets/static-item.d.ts +1 -1
  152. package/dist/widgets/static-item.js +6 -7
  153. package/dist/widgets/text-input.d.ts +53 -47
  154. package/dist/widgets/text-input.js +456 -399
  155. package/dist/widgets/themed-widget.js +2 -1
  156. package/dist/widgets/toggle.js +4 -3
  157. package/dist/widgets/types.d.ts +10 -4
  158. package/dist/widgets/types.js +8 -2
  159. package/dist/widgets/widget-app.d.ts +32 -6
  160. package/dist/widgets/widget-app.js +74 -27
  161. package/dist/widgets/widget-base.d.ts +2 -2
  162. package/dist/widgets/widget-base.js +29 -21
  163. package/package.json +9 -5
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * The effect catalogue: shimmer, pulse, sparkle and wheel, which loop for as
3
- * long as they are drawn, and fade-in and dissolve-out, which run once and
4
- * are done. Each is an `Effect` for `Effected` (`./effect.ts`); the defaults
3
+ * long as they are drawn, fade-in and dissolve-out, which run once and are
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
6
  * in `EFFECT_CURVES` are the ones signed off by eye in the effects-feel demo
6
7
  * (`npm run effects-feel`), at 30 fps and at one frame a second, and
7
8
  * `EFFECT_CATALOGUE` makes every one of them at those, by name, one way.
@@ -13,7 +14,10 @@
13
14
  * One effect here is not ambience but a signal, and keeps none of this on
14
15
  * purpose: `steppedInk` steps its ink through colours on a beat, the news
15
16
  * that something is at its limit, and jumps from slot to slot where the
16
- * loops hold still. What makes the loops' motions familiar is what every
17
+ * loops hold still. Nor is `gradient` a light: it paints an ink rather than
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
20
+ * keeps the words. What makes the loops' motions familiar is what every
17
21
  * curve of theirs keeps:
18
22
  *
19
23
  * - Nothing jumps. Every curve is continuous in time, built from smooth
@@ -46,13 +50,15 @@
46
50
  * cards runs this file's own declarations as the program a visitor edits
47
51
  * (`examples/effects-playground/programs.ts`), on the published library.
48
52
  */
53
+ import { Animation } from "../core/animation.js";
49
54
  import { ColorDepth, ColorRgba, ColorSpec, contrastRatio, relativeLuminance } from "../core/color.js";
50
55
  import { EASES, Phase, beat } from "../core/easing.js";
51
56
  import { fbm, hash, noise, smoothstep } from "../core/noise.js";
52
- import { Memo } from "../core/memo.js";
57
+ import { Memo, WeakMemo } from "../core/memo.js";
53
58
  import { Oklch } from "../core/oklch.js";
59
+ import { Style } from "../core/style.js";
54
60
  import { ensureContrast } from "../themes/colorMath.js";
55
- import { Effected, nextChangeOf, withNextChange } from "./effect.js";
61
+ import { Effected } from "./effect.js";
56
62
  /** The contrast no touch takes a cell below, unless it rested below it: WCAG AA for body text. */
57
63
  const LEGIBLE = 4.5;
58
64
  /**
@@ -151,13 +157,11 @@ export function shares({ pairs, depth, theme }, colors, touch) {
151
157
  * drawing what an earlier one drew costs no settling.
152
158
  */
153
159
  export function onColors(loop, colors) {
154
- const settled = new WeakMap();
160
+ const settled = new WeakMemo();
155
161
  const settle = (drawn) => {
156
162
  const lit = colors ?? drawn.colors;
157
163
  const key = [drawnKey(drawn), "|", ...lit].join(" ");
158
- const memo = settled.get(drawn.theme) ?? new Memo();
159
- settled.set(drawn.theme, memo);
160
- return memo.get(key, () => shares(drawn, lit, loop.touch));
164
+ return settled.get(drawn.theme, () => new Memo()).get(key, () => shares(drawn, lit, loop.touch));
161
165
  };
162
166
  return stillOnSlots(atShares(loop, settle));
163
167
  }
@@ -176,20 +180,75 @@ function drawnKey({ depth, pairs }) {
176
180
  function pairKey({ fg, bg, terminal, reverse }) {
177
181
  return `${fg.hex}/${bg.hex}/${terminal.fg}/${terminal.bg}/${reverse}`;
178
182
  }
183
+ /**
184
+ * A ramp laid across the element: each lit cell's ink is the colour `ramp`
185
+ * shows at `t` at the cell's column, the element's leftmost cell reading
186
+ * 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
+ * nearest that does (`legibleInks`, as `steppedInk` writes its steps). A
189
+ * ramp that moves (`ColorRamp.moving`) scrolls along the element and turns
190
+ * its hues, and the frame changes when the ramp does — every frame for a
191
+ * loop, never again once a tween settles; a still one is
192
+ * `Animation.constant(ramp)`. Lit cells are those whose ink is one of
193
+ * `colors`, named as a style names them (`"#a6e3a1"`, `"bright_green"`),
194
+ * 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
198
+ * body. The element is where its frame lists
199
+ * 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
202
+ * hold as drawn.
203
+ */
204
+ export function gradient(ramp, colors) {
205
+ const legible = legibleInks();
206
+ // [LAW:parse-dont-validate] Parsed at the call, so a misspelt colour fails
207
+ // where it was written and not on some later frame.
208
+ const named = colors?.map(opaqueInk);
209
+ // [LAW:one-source-of-truth] A colour named as a style spells it is lit
210
+ // wherever a cell shows it, so it is resolved as `Effected` resolves a
211
+ // 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)));
215
+ const inkOn = legible(drawn);
216
+ const [left, right] = extent(drawn.cells().map((cell) => cell.col));
217
+ return Animation.map(ramp, (now) => {
218
+ const first = now.stops[0].at;
219
+ const span = now.stops[now.stops.length - 1].at - first;
220
+ 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
+ });
223
+ });
224
+ }
225
+ /**
226
+ * `spec` parsed as an ink `gradient` lights. Opaque only: a translucent ink
227
+ * 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
+ */
230
+ function opaqueInk(spec) {
231
+ const parsed = ColorSpec.parse(spec);
232
+ if (parsed.value !== undefined && parsed.value.alpha < 1)
233
+ throw new RangeError(`gradient lights opaque colours, and ${spec} is translucent`);
234
+ return parsed;
235
+ }
179
236
  /**
180
237
  * A loop as an `Effect` on colours by hex, each touched at the share
181
238
  * `share` gives it of the cell's strength in a frame drawing what `Drawn`
182
239
  * says; a colour it gives none, and one the terminal supplies, stays as it is.
183
240
  */
184
241
  export function atShares(loop, share) {
185
- return (t, drawn) => {
242
+ return (drawn) => {
186
243
  const shared = share(drawn);
187
- const field = loop.field(t, drawn);
188
- return ({ fg, bg }, cell) => {
189
- const w = Math.round(clamp01(field(cell)) * LEVELS) / LEVELS;
190
- const moved = (color, terminal) => touchedAt(loop.touch, color, (terminal ? 0 : (shared.get(color.hex) ?? 0)) * w);
191
- return { fg: moved(fg, cell.terminal.fg), bg: moved(bg, cell.terminal.bg) };
192
- };
244
+ return Animation.continuous((t) => {
245
+ const field = loop.field(t, drawn);
246
+ return (look, cell) => {
247
+ 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) };
250
+ };
251
+ });
193
252
  };
194
253
  }
195
254
  /**
@@ -197,7 +256,7 @@ export function atShares(loop, share) {
197
256
  * sixteen ANSI slots, or no colour, a move that shows at all is a jump to
198
257
  * another slot, and a light on an element never jumps — so there it
199
258
  * holds every cell still, and anywhere else it moves. [LAW:single-enforcer]
200
- * `onColors` and `wheel` draw through this, so every loop in the catalogue
259
+ * `onColors`, `wheel` and `gradient` draw through this, so every loop in the catalogue
201
260
  * meets the sixteen-colour terminal here. `atShares` does not: a progress
202
261
  * bar's pulse is the news that work is going on, and flashes a slot as
203
262
  * Rich's does; nor does `steppedInk`, a signal, which steps slot to slot.
@@ -205,7 +264,7 @@ export function atShares(loop, share) {
205
264
  * loop on such a terminal draws it once.
206
265
  */
207
266
  function stillOnSlots(loop) {
208
- return withNextChange((t, drawn) => (onlySlots(drawn.depth) ? (colors) => colors : loop(t, drawn)), (t, drawn) => (onlySlots(drawn.depth) ? Number.POSITIVE_INFINITY : nextChangeOf(loop, t, drawn)));
267
+ return (drawn) => (onlySlots(drawn.depth) ? Animation.constant((look) => look) : loop(drawn));
209
268
  }
210
269
  /** Whether `depth` writes nothing between the slots it has: the sixteen, at either sixteen-colour depth, or none. */
211
270
  function onlySlots(depth) {
@@ -228,12 +287,21 @@ function touchedAt(touch, color, w) {
228
287
  return w === 0 ? color : touch(color, w);
229
288
  }
230
289
  /** Throws, naming the field, unless `curve` has a positive, finite `seconds` and a `swing` in [0, 1]. */
231
- function requireCurve(curve) {
290
+ export function requireCurve(curve) {
232
291
  if (!(Number.isFinite(curve.seconds) && curve.seconds > 0))
233
292
  throw new RangeError(`curve: seconds must be a positive number of seconds, got ${curve.seconds}`);
234
293
  if (!(curve.swing >= 0 && curve.swing <= 1))
235
294
  throw new RangeError(`curve: swing must be in [0, 1], got ${curve.swing}`);
236
295
  }
296
+ /**
297
+ * Throws, naming `what`, unless `z` is a finite place in the noise. Noise at
298
+ * a place that is not a number is not a number, which no effect could tell
299
+ * from a dim cell or an empty sky. [LAW:no-silent-failure]
300
+ */
301
+ export function requireZ(what, z) {
302
+ if (!Number.isFinite(z))
303
+ throw new RangeError(`${what} z must be a finite place in the noise, got ${z}`);
304
+ }
237
305
  /**
238
306
  * Each effect's curve as it was signed off. Every loop's period is a prime
239
307
  * number of seconds, so no two loops come back into step within a sitting.
@@ -337,6 +405,7 @@ const FLASH = [[0.24, 1], [0.6, 0]];
337
405
  */
338
406
  export function pulse(curve, glow, z) {
339
407
  requireCurve(curve);
408
+ requireZ("pulse", z);
340
409
  // How much quicker or slower than `curve.seconds` this element beats.
341
410
  const rate = 0.9 + 0.2 * hash(z, 3.1);
342
411
  // How far, in beats, the rhythm wanders ahead and behind its metronome. Noise's
@@ -383,6 +452,7 @@ const bump = (d) => (Math.abs(d) >= 1 ? 0 : (1 - d * d) ** 3);
383
452
  */
384
453
  export function shimmer(curve, width, glow, z) {
385
454
  requireCurve(curve);
455
+ requireZ("shimmer", z);
386
456
  const P = curve.seconds;
387
457
  // Passes are events on slots of half a period. Whether slot `j` holds one,
388
458
  // and how bright, is the sky's: slow noise over the slots, so sunny and
@@ -470,6 +540,7 @@ export function shimmer(curve, width, glow, z) {
470
540
  */
471
541
  export function sparkle(curve, glow, z) {
472
542
  requireCurve(curve);
543
+ requireZ("sparkle", z);
473
544
  // The columns a firefly has to itself along a row of the lattice, and the
474
545
  // rows between two rows of it: more than a halo's reach, so a one-row
475
546
  // element is lit by one row of fireflies.
@@ -554,41 +625,87 @@ export function sparkle(curve, glow, z) {
554
625
  * hiding — 1 reaches fully invisible. A cell wholly shown is returned as it was.
555
626
  */
556
627
  export function veiled(visibility, swing) {
557
- return (t, drawn) => {
558
- const { theme } = drawn;
559
- const shows = visibility(t, drawn);
628
+ return (drawn) => {
560
629
  // [LAW:one-source-of-truth] The ground is the terminal `Effected` draws on, never a second one handed here.
561
- const ground = Oklch.fromRgba(theme.backgroundColor);
562
- return (colors, cell) => {
563
- const hidden = swing * (1 - clamp01(shows(cell)));
564
- if (hidden === 0)
565
- return colors;
566
- const bg = blend(Oklch.fromRgba(colors.bg), ground, hidden);
567
- return { fg: blend(Oklch.fromRgba(colors.fg), bg, hidden).toRgba(), bg: bg.toRgba() };
568
- };
630
+ const ground = Oklch.fromRgba(drawn.theme.backgroundColor);
631
+ return Animation.continuous((t) => {
632
+ const shows = visibility(t, drawn);
633
+ return (look, cell) => {
634
+ const hidden = swing * (1 - clamp01(shows(cell)));
635
+ if (hidden === 0)
636
+ return look;
637
+ const bg = blend(Oklch.fromRgba(look.bg), ground, hidden);
638
+ return { ...look, fg: blend(Oklch.fromRgba(look.fg), bg, hidden).toRgba(), bg: bg.toRgba() };
639
+ };
640
+ });
569
641
  };
570
642
  }
571
643
  /**
572
644
  * Where each cell falls in a transition's order, in [0, 1]: smooth noise over
573
645
  * the element, so neighbouring cells arrive or leave together, in patches
574
646
  * with soft edges, as ink spreads or mist thins. `z` places the element in
575
- * the noise. A cell's place never changes, so it is worked out once.
647
+ * the noise.
576
648
  */
577
649
  function order(z) {
578
- const places = new Map();
579
- return (cell) => {
580
- const at = `${cell.row}:${cell.col}`;
581
- // The noise is read through slower noise, so the patches reach out in
582
- // curling tendrils rather than sitting as round blots.
583
- const place = places.get(at) ??
584
- clamp01(0.5 + 1.1 * fbm(cell.col * 0.09 + 1.4 * noise(cell.col * 0.035, cell.row * 0.2 + 2.6, z + 0.5), cell.row * 0.45, z, 3));
585
- places.set(at, place);
586
- return place;
587
- };
650
+ // The noise is read through slower noise, so the patches reach out in
651
+ // curling tendrils rather than sitting as round blots.
652
+ return (cell) => clamp01(0.5 + 1.1 * fbm(cell.col * 0.09 + 1.4 * noise(cell.col * 0.035, cell.row * 0.2 + 2.6, z + 0.5), cell.row * 0.45, z, 3));
588
653
  }
589
- function transition(curve, effect) {
590
- const done = (t) => t >= curve.seconds;
591
- return { effect: withNextChange(effect, (t) => (done(t) ? Number.POSITIVE_INFINITY : t)), done };
654
+ /**
655
+ * An effect that runs once: a fade-in or a dissolve-out over its curve's
656
+ * `seconds` from `t = 0`, its own clock as every effect's is — the caller
657
+ * draws it at the time since it began. `done(t)` is true from the moment
658
+ * every cell has arrived or gone, so the caller knows when to stop drawing an
659
+ * element that is leaving, or to drop the effect from one that has arrived.
660
+ *
661
+ * `schedule` is when its frame next changes on that clock, whatever the
662
+ * element draws: what a caller drawing the transition inside an animation —
663
+ * 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.
666
+ * [LAW:one-source-of-truth] `done` is that schedule read as a fact — done
667
+ * from the first moment it never changes again — so no transition carries
668
+ * two answers to when it is over.
669
+ */
670
+ export class Transition {
671
+ effect;
672
+ schedule;
673
+ constructor(effect, schedule) {
674
+ this.effect = effect;
675
+ this.schedule = schedule;
676
+ }
677
+ done(t) {
678
+ return this.schedule.nextChange(t) === Number.POSITIVE_INFINITY;
679
+ }
680
+ }
681
+ /**
682
+ * `effect` run once over `seconds` as a `Transition`: what it draws until it
683
+ * 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
685
+ * glyph effect (`./glyph-effects.ts`) alike.
686
+ */
687
+ export function transition(seconds, effect) {
688
+ // Moving every frame until `seconds`, then never: the sequence carries the switch.
689
+ const schedule = Animation.sequence(Animation.continuous(() => undefined), seconds, Animation.constant(undefined));
690
+ const ran = new Transition((drawn) => {
691
+ const moving = effect(drawn);
692
+ return Animation.of({
693
+ at: (t) => moving.at(t),
694
+ nextChange: (t) => (ran.done(t) ? Number.POSITIVE_INFINITY : moving.nextChange(t)),
695
+ // Done, the frame settled when the transition did; until then it is the effect's.
696
+ lastChange: (t) => (ran.done(t) ? schedule.lastChange(t) : moving.lastChange(t)),
697
+ // A one-shot, reduced, cuts from its start to its end, whatever the effect it runs would hold as a loop.
698
+ reduced: () => Animation.cut(moving.at(0), moving.at(seconds)),
699
+ });
700
+ }, schedule);
701
+ return ran;
702
+ }
703
+ export function onBeat(stepped, beat) {
704
+ // [LAW:types-are-the-program] An effect is a function and a transition an object: the representation is the discriminator.
705
+ if (typeof stepped === "function")
706
+ return (drawn) => Animation.onBeat(stepped(drawn), beat);
707
+ const { effect, schedule } = stepped;
708
+ return new Transition((drawn) => Animation.onBeat(effect(drawn), beat), Animation.onBeat(schedule, beat));
592
709
  }
593
710
  /**
594
711
  * Of a transition's `seconds`, how long each cell's own change takes. Under
@@ -596,7 +713,7 @@ function transition(curve, effect) {
596
713
  */
597
714
  const OWN = 0.45;
598
715
  /**
599
- * Each cell's own change in a transition, in a frame listing `cells`: a cell
716
+ * Each cell's own change in a transition, in the frame at `t` listing `cells`: a cell
600
717
  * starts as far through the time the others leave it as its `place` falls
601
718
  * between the element's first and last, so on any element, however little of
602
719
  * the order its cells span, the first starts at 0 and the last settles at
@@ -604,32 +721,27 @@ const OWN = 0.45;
604
721
  * `seconds` alone. An element whose cells all share one place has no order to
605
722
  * stagger, and its change takes the whole of `seconds`.
606
723
  *
607
- * A cell's change is settled in the first frame that lists it and kept, as
608
- * its place is, so an element whose cells change while it moves — a label
609
- * that gains a column as it leaves — spreads only its new cells over the
610
- * order it has then, and never sends a cell that has gone back to a later
611
- * start. [LAW:no-ambient-temporal-coupling] The transition owns that record;
612
- * it is why a transition is made for one element.
724
+ * [LAW:no-ambient-temporal-coupling] Worked out from the frame alone, so
725
+ * the same `t` over the same cells draws the same bytes whatever was drawn
726
+ * before — after a seek, in reverse, at any rate — and one transition is
727
+ * safe on any number of elements. An element that gains cells as it moves is
728
+ * spread over the order it has in each frame, so a cell already moving may
729
+ * move its start — back to unmoved, if a new cell comes first. Nothing
730
+ * steadier is there to spread over: the order's whole range would start a
731
+ * small element late and end it early, and the cells a frame hands reach
732
+ * across a wide frame's blank margin.
613
733
  */
614
734
  function ownChange(curve, place) {
615
- const changes = new Map();
616
- const key = (cell) => `${cell.row}:${cell.col}`;
617
- return (cells) => {
735
+ return (t, cells) => {
618
736
  const [first, last] = extent(cells.map(place));
619
737
  const stagger = (last > first ? 1 - OWN : 0) * curve.seconds;
620
738
  // One phase a frame, shifted per cell, so no cell builds its own.
621
739
  const change = Phase.once(0, curve.seconds - stagger);
622
740
  // A share of the way from the first place to the last, held to the order for a cell the frame does not list.
623
- const settled = (cell) => {
741
+ return (cell) => {
624
742
  const along = last > first ? clamp01((place(cell) - first) / (last - first)) : 0;
625
- return { along, progress: (t) => change(t - along * stagger) };
743
+ return { along, progress: change(t - along * stagger) };
626
744
  };
627
- for (const cell of cells) {
628
- const at = key(cell);
629
- changes.set(at, changes.get(at) ?? settled(cell));
630
- }
631
- // A cell the frame hands but does not list shows nothing yet, so it is timed for this frame alone.
632
- return (cell) => changes.get(key(cell)) ?? settled(cell);
633
745
  };
634
746
  }
635
747
  /** The least and greatest of `values`; of none, a frame with no cell to place, both 0. */
@@ -649,10 +761,11 @@ function extent(values) {
649
761
  */
650
762
  export function fadeIn(curve, z) {
651
763
  requireCurve(curve);
764
+ requireZ("fadeIn", z);
652
765
  const risen = ownChange(curve, order(1.7 + z));
653
- return transition(curve, veiled((t, drawn) => {
654
- const of = risen(drawn.cells());
655
- return (cell) => curve.ease(of(cell).progress(t));
766
+ return transition(curve.seconds, veiled((t, drawn) => {
767
+ const of = risen(t, drawn.cells());
768
+ return (cell) => curve.ease(of(cell).progress);
656
769
  }, curve.swing));
657
770
  }
658
771
  /**
@@ -675,14 +788,15 @@ const DISSOLVE_SHAPE = { depth: 0.4, from: 0.5, to: 0.95, maskFrom: 0.35, maskTo
675
788
  */
676
789
  export function dissolveOut(curve, z) {
677
790
  requireCurve(curve);
791
+ requireZ("dissolveOut", z);
678
792
  const eddy = order(9.3 + z);
679
793
  const faded = ownChange(curve, order(4.2 + z));
680
794
  const { depth, from, to, maskFrom, maskTo, late } = DISSOLVE_SHAPE;
681
- return transition(curve, veiled((t, drawn) => {
682
- const of = faded(drawn.cells());
795
+ return transition(curve.seconds, veiled((t, drawn) => {
796
+ const of = faded(t, drawn.cells());
683
797
  return (cell) => {
684
798
  const { along, progress } = of(cell);
685
- const gone = curve.ease(progress(t));
799
+ const gone = curve.ease(progress);
686
800
  const q = clamp01((gone - from) / (to - from));
687
801
  const back = depth * smoothstep(maskFrom, maskTo, eddy(cell)) * (1 - late + late * along) * Math.sin(Math.PI * q) ** 2;
688
802
  return 1 - gone + back;
@@ -723,6 +837,7 @@ const STRAY = 180;
723
837
  */
724
838
  export function wheel(curve, z) {
725
839
  requireCurve(curve);
840
+ requireZ("wheel", z);
726
841
  const P = curve.seconds;
727
842
  // A segment's seed, from the colour it is.
728
843
  const seed = (hex) => hash(Number.parseInt(hex.slice(1), 16) * 1e-4, 17.3 + z);
@@ -734,8 +849,8 @@ export function wheel(curve, z) {
734
849
  return (360 * t) / P + curve.swing * STRAY * ((stray(s, t) - stray(s, 0)) / 1.5);
735
850
  };
736
851
  // A turned colour's bounds rest on what a frame draws, never on `t`: settled once for each.
737
- const settled = new WeakMap();
738
- return stillOnSlots((t, drawn) => {
852
+ const settled = new WeakMemo();
853
+ return stillOnSlots((drawn) => {
739
854
  const { colors, fills, depth, theme } = drawn;
740
855
  // Whether a slot shows one of the element's colours: never the terminal's, whatever its hex.
741
856
  const own = (terminal, color) => !terminal && colors.has(color.hex);
@@ -743,30 +858,30 @@ export function wheel(curve, z) {
743
858
  const segmentOf = (color, other, theirs) => (fills.has(color.hex) || !theirs ? color.hex : other.hex);
744
859
  // A colour as it turns with a segment: written one way wherever it is drawn in a frame.
745
860
  const keyOf = (color, segment) => `${color.hex} ${segment}`;
746
- const memo = settled.get(theme) ?? new Memo();
747
- settled.set(theme, memo);
748
- const bounds = memo.get(drawnKey(drawn), () => turnedBounds(drawn.pairs, depth, (pair, side) => {
861
+ const bounds = settled.get(theme, () => new Memo()).get(drawnKey(drawn), () => turnedBounds(drawn.pairs, depth, (pair, side) => {
749
862
  const other = side === "fg" ? "bg" : "fg";
750
863
  return own(pair.terminal[side], pair[side]) ? keyOf(pair[side], segmentOf(pair[side], pair[other], own(pair.terminal[other], pair[other]))) : undefined;
751
864
  }));
752
865
  // 256 colours writes a turned colour as one it has an index for; truecolor
753
866
  // as one of the two either side of its luminance.
754
867
  const indexed = measuredIndexed(depth, theme);
755
- const written = new Map();
756
- const choose = (color, segment) => {
757
- const turn = hueTurned(color, turned(segment, t));
758
- return nearestWithin(turn.looks, color, depth === ColorDepth.TRUECOLOR ? turn.either.map(measured) : indexed, bounds.get(keyOf(color, segment)) ?? UNBOUNDED);
759
- };
760
- const write = (color, segment) => {
761
- const found = written.get(keyOf(color, segment)) ?? choose(color, segment);
762
- written.set(keyOf(color, segment), found);
763
- return found;
764
- };
765
- return (shown, cell) => {
766
- const mine = { fg: own(cell.terminal.fg, shown.fg), bg: own(cell.terminal.bg, shown.bg) };
767
- const turn = (color, ours, other, theirs) => (ours ? write(color, segmentOf(color, other, theirs)) : color);
768
- return { fg: turn(shown.fg, mine.fg, shown.bg, mine.bg), bg: turn(shown.bg, mine.bg, shown.fg, mine.fg) };
769
- };
868
+ return Animation.continuous((t) => {
869
+ const written = new Map();
870
+ const choose = (color, segment) => {
871
+ const turn = hueTurned(color, turned(segment, t));
872
+ return nearestWithin(turn.looks, color, depth === ColorDepth.TRUECOLOR ? turn.either.map(measured) : indexed, bounds.get(keyOf(color, segment)) ?? UNBOUNDED);
873
+ };
874
+ const write = (color, segment) => {
875
+ const found = written.get(keyOf(color, segment)) ?? choose(color, segment);
876
+ written.set(keyOf(color, segment), found);
877
+ return found;
878
+ };
879
+ return (shown, cell) => {
880
+ const mine = { fg: own(cell.terminal.fg, shown.fg), bg: own(cell.terminal.bg, shown.bg) };
881
+ const turn = (color, ours, other, theirs) => (ours ? write(color, segmentOf(color, other, theirs)) : color);
882
+ return { ...shown, fg: turn(shown.fg, mine.fg, shown.bg, mine.bg), bg: turn(shown.bg, mine.bg, shown.fg, mine.fg) };
883
+ };
884
+ });
770
885
  });
771
886
  }
772
887
  /**
@@ -844,12 +959,9 @@ function turnedBounds(pairs, depth, turnedAs) {
844
959
  }
845
960
  const measured = (color) => ({ color, looks: Oklch.fromRgba(color), stop: stop(color) });
846
961
  /** `ColorSpec.indexed`'s colours, measured once for each theme and depth. */
847
- const measuredMemo = new WeakMap();
962
+ const measuredMemo = new WeakMemo();
848
963
  function measuredIndexed(depth, theme) {
849
- const indexed = ColorSpec.indexed(depth, theme);
850
- const listed = measuredMemo.get(indexed) ?? indexed.map(([, color]) => measured(color));
851
- measuredMemo.set(indexed, listed);
852
- return listed;
964
+ return measuredMemo.get(ColorSpec.indexed(depth, theme), (indexed) => indexed.map(([, color]) => measured(color)));
853
965
  }
854
966
  /**
855
967
  * The colour that looks nearest `color` (OKLab's ΔE): `rest`, or one of
@@ -887,10 +999,11 @@ function nearestOf(color, offered) {
887
999
  * that does (`stillOnSlots`). A cell whose ground no offered colour reads
888
1000
  * on keeps the ink it rested in.
889
1001
  *
890
- * The search is settled once for each ink, ground and step, so a frame
1002
+ * The search is settled once for each ground and step — below truecolor,
1003
+ * which colours read on a ground once for that ground — so a frame
891
1004
  * inside a step redoes none, and nor does a later pass of the beat: at sixty
892
1005
  * frames a second a one-second step is sixty identical frames, and the
893
- * frame says it changes next when the step ends (`Beat.end`).
1006
+ * frame changes when the beat does: the signal is the beat mapped to inks.
894
1007
  * [LAW:no-ambient-temporal-coupling] The beat is `core/easing.ts`'s, and
895
1008
  * nothing here reads a clock.
896
1009
  */
@@ -899,32 +1012,78 @@ export function steppedInk(steps) {
899
1012
  // A copy, as `beat` keeps of the holds: what the signal plays stays what
900
1013
  // `steps` said whatever the caller does to its array.
901
1014
  const inks = steps.map((step) => step.color);
902
- const settled = new WeakMap();
903
- return withNextChange((t, { depth, theme }) => {
904
- const step = inks[rhythm.at(t)];
905
- const memo = settled.get(theme) ?? new Memo();
906
- settled.set(theme, memo);
907
- return (colors, cell) => {
908
- const pair = { ...colors, terminal: cell.terminal, reverse: cell.reverse };
909
- return { fg: memo.get(`${depth} ${pairKey(pair)} ${step.hex}`, () => legibleInk(step, pair, depth, theme)), bg: colors.bg };
1015
+ const legible = legibleInks();
1016
+ return (drawn) => {
1017
+ const inkOn = legible(drawn);
1018
+ return Animation.map(rhythm, (i) => {
1019
+ const step = inks[i];
1020
+ return (look, cell) => ({ ...look, fg: inkOn(step, look, cell) });
1021
+ });
1022
+ };
1023
+ }
1024
+ /**
1025
+ * `legibleInk` for the frame `Drawn` describes, of an ink on a cell's look:
1026
+ * settled once for each terminal, depth, pair and ink, so a frame drawing
1027
+ * what an earlier one drew redoes no search. [LAW:single-enforcer] Every
1028
+ * effect that writes an ink of the caller's choosing writes it through
1029
+ * here — `steppedInk`'s steps and `gradient`'s ramp alike — so none of them
1030
+ * leaves a word unreadable. Below truecolor which of the depth's colours
1031
+ * read on a pair does not depend on the ink, so it is settled once for each
1032
+ * pair: a ramp, whose every frame offers inks no frame offered before,
1033
+ * then costs a nearest-colour scan a cell, not a contrast test of the whole
1034
+ * palette.
1035
+ */
1036
+ function legibleInks() {
1037
+ const settled = new WeakMemo();
1038
+ return ({ depth, theme }) => {
1039
+ const memo = settled.get(theme, () => ({ inks: new Memo(), readable: new Memo() }));
1040
+ return (ink, look, cell) => {
1041
+ const pair = { fg: look.fg, bg: look.bg, terminal: cell.terminal, reverse: cell.reverse };
1042
+ const key = `${depth} ${pairKey(pair)}`;
1043
+ const legibleOf = (offered) => legibleAmong(pair, offered, depth, theme);
1044
+ return memo.inks.get(`${key} ${ink.hex}`, () => {
1045
+ const offered = depth === ColorDepth.TRUECOLOR
1046
+ ? legibleOf([ink, ensureContrast(ink, pair.bg, floorOf(pair, depth))].map(measured))
1047
+ : memo.readable.get(key, () => legibleOf(measuredIndexed(depth, theme)));
1048
+ return nearestOf(ink, offered)?.color ?? pair.fg;
1049
+ });
910
1050
  };
911
- }, (t) => rhythm.end(t));
1051
+ };
912
1052
  }
913
1053
  /**
914
- * The colour nearest `step` the depth writes that reads on `pair`'s ground
915
- * at `pair`'s floor, as `Effected` writes it (`shows`), or `pair`'s own ink
916
- * when none does. [LAW:one-source-of-truth] Measured the way `shares` and
917
- * `wheel` measure: the written colours, at the catalogue's floor.
1054
+ * Of `offered`, the colours that read on `pair`'s ground at `pair`'s floor,
1055
+ * as `Effected` writes them (`shows`). [LAW:one-source-of-truth] Measured the
1056
+ * way `shares` and `wheel` measure: the written colours, at the catalogue's
1057
+ * floor. An ink none of them is nearest stays `pair`'s own.
918
1058
  */
919
- function legibleInk(step, pair, depth, theme) {
1059
+ function legibleAmong(pair, offered, depth, theme) {
920
1060
  const floor = floorOf(pair, depth);
921
- const offered = depth === ColorDepth.TRUECOLOR ? [step, ensureContrast(step, pair.bg, floor)].map(measured) : measuredIndexed(depth, theme);
922
- const reads = ({ color }) => {
1061
+ return offered.filter(({ color }) => {
923
1062
  const shown = Effected.shows(pair, { fg: color, bg: pair.bg }, depth, theme);
924
1063
  return contrastRatio(shown.fg, shown.bg) >= floor;
925
- };
926
- return nearestOf(step, offered.filter(reads))?.color ?? pair.fg;
1064
+ });
1065
+ }
1066
+ /**
1067
+ * `spec.attribute` blinking: off, the cell as drawn, so that hold's frame is
1068
+ * the identity and writes the element's own bytes; on, the attribute set.
1069
+ * It changes when its beat does, at a hold's end, and never between.
1070
+ * A blink and a bold pulse are this one function at two specs
1071
+ * (`ATTRIBUTE_DEFAULTS`) [LAW:one-type-per-behavior].
1072
+ */
1073
+ export function blink({ attribute, off, on }) {
1074
+ const rhythm = beat([off, on]);
1075
+ const frames = [(look) => look, (look) => ({ ...look, attributes: { ...look.attributes, [attribute]: true } })];
1076
+ return () => Animation.map(rhythm, (i) => frames[i]);
927
1077
  }
1078
+ /**
1079
+ * Each attribute effect's spec: a blink hides the words for half of each
1080
+ * second, as a terminal's own blink does, and a bold pulse holds them bold
1081
+ * for the last three tenths of each second.
1082
+ */
1083
+ export const ATTRIBUTE_DEFAULTS = {
1084
+ blink: { attribute: "conceal", off: 0.5, on: 0.5 },
1085
+ boldPulse: { attribute: "bold", off: 0.7, on: 0.3 },
1086
+ };
928
1087
  /**
929
1088
  * Every effect in the catalogue by its name in `EFFECT_CURVES`, each made the
930
1089
  * one way: for an element, at its signed-off curve unless handed another,
@@ -947,4 +1106,8 @@ export const EFFECT_CATALOGUE = {
947
1106
  fade: (element, curve = EFFECT_CURVES.fade) => fadeIn(curve, element.z),
948
1107
  dissolve: (element, curve = EFFECT_CURVES.dissolve) => dissolveOut(curve, element.z),
949
1108
  },
1109
+ attributes: {
1110
+ blink: () => blink(ATTRIBUTE_DEFAULTS.blink),
1111
+ boldPulse: () => blink(ATTRIBUTE_DEFAULTS.boldPulse),
1112
+ },
950
1113
  };
@@ -89,7 +89,7 @@ export declare class InlineLabel {
89
89
  * measures it, so a cut to a border and the width a label asks for count
90
90
  * the cells it will draw. [LAW:single-enforcer] The one reading of "set into
91
91
  * a line", for a label read through the crossing above and for text a
92
- * caller hands over already read (`Frames`).
92
+ * caller hands over already read (`FrameSequence`).
93
93
  */
94
94
  export declare function setIntoLine(text: RichText): RichText;
95
95
  /**