@promptctl/rich-js 0.26.0 → 0.28.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 (132) hide show
  1. package/README.md +1 -0
  2. package/THIRD-PARTY-NOTICES +42 -0
  3. package/dist/core/animation.d.ts +52 -15
  4. package/dist/core/animation.js +82 -37
  5. package/dist/core/ansi.d.ts +15 -8
  6. package/dist/core/ansi.js +106 -28
  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 +93 -5
  17. package/dist/core/color.d.ts +26 -18
  18. package/dist/core/color.js +37 -34
  19. package/dist/core/console.d.ts +26 -1
  20. package/dist/core/console.js +26 -6
  21. package/dist/core/control-codes.d.ts +61 -0
  22. package/dist/core/control-codes.js +157 -0
  23. package/dist/core/easing.js +6 -1
  24. package/dist/core/env.d.ts +6 -0
  25. package/dist/core/env.js +9 -1
  26. package/dist/core/export-html.js +4 -3
  27. package/dist/core/export-lines.d.ts +2 -0
  28. package/dist/core/export-lines.js +7 -3
  29. package/dist/core/export-svg.js +6 -5
  30. package/dist/core/json.js +9 -2
  31. package/dist/core/markup.js +4 -3
  32. package/dist/core/oklch.d.ts +2 -0
  33. package/dist/core/oklch.js +3 -3
  34. package/dist/core/orient.d.ts +68 -0
  35. package/dist/core/orient.js +224 -0
  36. package/dist/core/osc8.d.ts +12 -5
  37. package/dist/core/osc8.js +14 -7
  38. package/dist/core/pretty.js +22 -18
  39. package/dist/core/protocol.d.ts +53 -22
  40. package/dist/core/protocol.js +14 -6
  41. package/dist/core/render.d.ts +8 -3
  42. package/dist/core/render.js +19 -5
  43. package/dist/core/seal.d.ts +26 -0
  44. package/dist/core/seal.js +75 -0
  45. package/dist/core/segment.d.ts +9 -1
  46. package/dist/core/segment.js +11 -1
  47. package/dist/core/style.d.ts +29 -1
  48. package/dist/core/style.js +41 -5
  49. package/dist/core/text-order.d.ts +51 -0
  50. package/dist/core/text-order.js +50 -0
  51. package/dist/core/text.d.ts +10 -0
  52. package/dist/core/text.js +133 -87
  53. package/dist/core/timeline.js +6 -6
  54. package/dist/core/view.d.ts +23 -9
  55. package/dist/core/view.js +37 -12
  56. package/dist/core/wrap.d.ts +24 -6
  57. package/dist/core/wrap.js +82 -16
  58. package/dist/host/app.d.ts +48 -7
  59. package/dist/host/app.js +116 -11
  60. package/dist/host/index.d.ts +3 -2
  61. package/dist/host/index.js +1 -0
  62. package/dist/host/input.d.ts +35 -2
  63. package/dist/host/input.js +88 -4
  64. package/dist/host/terminal-colors.d.ts +18 -0
  65. package/dist/host/terminal-colors.js +46 -0
  66. package/dist/host/terminal-host.d.ts +2 -1
  67. package/dist/index.d.ts +10 -8
  68. package/dist/index.js +2 -2
  69. package/dist/renderables/change.d.ts +1 -3
  70. package/dist/renderables/change.js +4 -6
  71. package/dist/renderables/effect.d.ts +61 -60
  72. package/dist/renderables/effect.js +91 -81
  73. package/dist/renderables/effects.d.ts +37 -8
  74. package/dist/renderables/effects.js +193 -55
  75. package/dist/renderables/frames.d.ts +1 -10
  76. package/dist/renderables/frames.js +1 -16
  77. package/dist/renderables/glyph-effect.d.ts +12 -13
  78. package/dist/renderables/glyph-effect.js +34 -32
  79. package/dist/renderables/glyph-effects.d.ts +27 -8
  80. package/dist/renderables/glyph-effects.js +75 -7
  81. package/dist/renderables/lifecycle.d.ts +40 -32
  82. package/dist/renderables/lifecycle.js +87 -48
  83. package/dist/renderables/live.d.ts +3 -3
  84. package/dist/renderables/live.js +4 -4
  85. package/dist/renderables/markdown.js +4 -2
  86. package/dist/renderables/panel.js +6 -4
  87. package/dist/renderables/progress.d.ts +11 -4
  88. package/dist/renderables/progress.js +29 -17
  89. package/dist/renderables/progressBar.d.ts +3 -3
  90. package/dist/renderables/progressBar.js +16 -13
  91. package/dist/renderables/syntax.js +7 -2
  92. package/dist/renderables/table.js +2 -1
  93. package/dist/template-bindings/animation-funcs.d.ts +49 -18
  94. package/dist/template-bindings/animation-funcs.js +115 -32
  95. package/dist/template-bindings/index.d.ts +7 -5
  96. package/dist/template-bindings/index.js +8 -5
  97. package/dist/template-bindings/palette-funcs.d.ts +1 -1
  98. package/dist/template-bindings/palette-funcs.js +1 -1
  99. package/dist/themes/buildPalette.d.ts +15 -39
  100. package/dist/themes/buildPalette.js +196 -67
  101. package/dist/themes/colorMath.d.ts +10 -4
  102. package/dist/themes/colorMath.js +33 -1
  103. package/dist/themes/data/index.js +1 -4
  104. package/dist/themes/data/textual-ansi.js +0 -143
  105. package/dist/themes/data/types.d.ts +3 -1
  106. package/dist/themes/palette.d.ts +22 -6
  107. package/dist/themes/palette.js +13 -21
  108. package/dist/themes/ramp.d.ts +1 -1
  109. package/dist/themes/ramp.js +1 -1
  110. package/dist/themes/registry.d.ts +7 -10
  111. package/dist/themes/registry.js +9 -30
  112. package/dist/themes/terminalThemes.d.ts +4 -10
  113. package/dist/themes/terminalThemes.js +31 -46
  114. package/dist/themes/transpose.d.ts +5 -10
  115. package/dist/themes/transpose.js +13 -24
  116. package/dist/widgets/button.d.ts +1 -1
  117. package/dist/widgets/button.js +6 -6
  118. package/dist/widgets/checkbox.js +4 -4
  119. package/dist/widgets/dropdown.d.ts +1 -1
  120. package/dist/widgets/dropdown.js +7 -5
  121. package/dist/widgets/event-router.js +2 -2
  122. package/dist/widgets/ink.d.ts +5 -4
  123. package/dist/widgets/ink.js +6 -9
  124. package/dist/widgets/text-input.d.ts +1 -1
  125. package/dist/widgets/text-input.js +41 -32
  126. package/dist/widgets/themed-widget.js +3 -3
  127. package/dist/widgets/toggle.d.ts +1 -1
  128. package/dist/widgets/toggle.js +5 -5
  129. package/dist/widgets/types.d.ts +2 -2
  130. package/dist/widgets/widget-app.d.ts +3 -3
  131. package/dist/widgets/widget-app.js +5 -5
  132. package/package.json +5 -4
@@ -87,7 +87,7 @@ 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.
90
+ * lower, in the colours `Effected` writes it in at `drawn`'s depth and terminal.
91
91
  * A colour 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.
@@ -99,7 +99,7 @@ const SIDES = ["fg", "bg"];
99
99
  * the fill room to glow — and a darker colour 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) {
102
+ export function shares({ pairs, depth, terminal }, colors, touch) {
103
103
  // [LAW:one-source-of-truth] A slot shows a subject colour, 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);
@@ -126,7 +126,7 @@ export function shares({ pairs, depth, theme }, colors, touch) {
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
  });
@@ -146,10 +146,10 @@ export function shares({ pairs, depth, theme }, colors, touch) {
146
146
  * (`Drawn`), touched as `Effected` writes them at the frame's depth — at 256
147
147
  * colours a touched colour lands on a colour the slot can write, and the
148
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
149
+ * moves (`stillWhereNothingMoves`). `colors` are the ones lit, by default every colour 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 colour the terminal supplies (`Pair.terminal`) is
153
153
  * not the element's, whatever its hex, and takes no share.
154
154
  *
155
155
  * Settling shares is costly, so it is done once for each set of colours an
@@ -158,12 +158,13 @@ export function shares({ pairs, depth, theme }, colors, touch) {
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 stillWhereNothingMoves(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 }) {
@@ -198,7 +199,7 @@ function pairKey({ fg, bg, terminal, reverse }) {
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
201
  * 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
+ * (`stillWhereNothingMoves`): a gradient there is jumps between slots, and the cells
202
203
  * hold as drawn.
203
204
  */
204
205
  export function gradient(ramp, colors) {
@@ -209,16 +210,16 @@ export function gradient(ramp, colors) {
209
210
  // [LAW:one-source-of-truth] A colour 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 stillWhereNothingMoves(["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
  }
@@ -245,17 +246,20 @@ 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 colour 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
257
+ * `loop` as drawn at the frame's depth and on the colours it moves on `sides`
258
+ * (`litIn`): where the depth writes only the
256
259
  * sixteen ANSI slots, or no colour, 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 colours 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
264
  * meets the sixteen-colour 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
@@ -263,8 +267,12 @@ export function atShares(loop, share) {
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 stillWhereNothingMoves(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
277
  /** Whether `depth` writes nothing between the slots it has: the sixteen, at either sixteen-colour depth, or none. */
270
278
  function onlySlots(depth) {
@@ -618,7 +626,7 @@ 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
631
  * arrives and leaves whole, fill and words together, never as bare coloured
624
632
  * blocks. Invisible is ink and fill equal to the ground. `swing` scales the
@@ -627,7 +635,7 @@ export function sparkle(curve, glow, z) {
627
635
  export function veiled(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) => {
@@ -675,7 +683,7 @@ export class Transition {
675
683
  this.schedule = schedule;
676
684
  }
677
685
  done(t) {
678
- return this.schedule.nextChange(t) === Number.POSITIVE_INFINITY;
686
+ return Animation.done(this.schedule, t);
679
687
  }
680
688
  }
681
689
  /**
@@ -685,8 +693,13 @@ export class Transition {
685
693
  * glyph effect (`./glyph-effects.ts`) alike.
686
694
  */
687
695
  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));
696
+ // Moving every frame until `seconds`, then never: the sequence carries the switch. [LAW:one-source-of-truth]
697
+ // Reduced, a one-shot cuts from its start to its end at 0, and its schedule says so too, the one word on
698
+ // when the reduced frames change, so a half that lends this schedule to its frames tells the same hold.
699
+ const schedule = Animation.of({
700
+ ...Animation.sequence(Animation.continuous(() => undefined), seconds, Animation.constant(undefined)),
701
+ reduced: () => Animation.cut(undefined, undefined),
702
+ });
690
703
  const ran = new Transition((drawn) => {
691
704
  const moving = effect(drawn);
692
705
  return Animation.of({
@@ -948,7 +961,7 @@ function segmentsOf(drawn) {
948
961
  * is drawn in at its floor (`floorOf`) — its resting contrast or WCAG AA,
949
962
  * whichever is lower — as `shares` keeps the light's (`turnedBounds`). The
950
963
  * turn only happens to the element's own colours (`Drawn.colors`), never the
951
- * terminal's. On a sixteen-colour terminal nothing turns (`stillOnSlots`).
964
+ * terminal's. On a sixteen-colour terminal nothing turns (`stillWhereNothingMoves`).
952
965
  */
953
966
  export function wheel(curve, z) {
954
967
  requireCurve(curve);
@@ -967,23 +980,23 @@ export function wheel(curve, z) {
967
980
  const turned = (place, t) => (360 * t) / P + curve.swing * STRAY * ((stray(place, t) - stray(place, 0)) / 1.5);
968
981
  // A turned colour's bounds rest on what a frame draws, never on `t`: settled once for each.
969
982
  const settled = new WeakMemo();
970
- return stillOnSlots((drawn) => {
971
- const { colors, fills, depth, theme } = drawn;
983
+ return stillWhereNothingMoves(SIDES, ({ colors }) => colors, (drawn) => {
984
+ const { colors, fills, depth, terminal } = drawn;
972
985
  // Whether a slot shows one of the element's colours: never the terminal's, whatever its hex.
973
- const own = (terminal, color) => !terminal && colors.has(color.hex);
986
+ const own = (supplied, color) => !supplied && colors.has(color.hex);
974
987
  // The colour an own colour is bounded with, under which its bounds are
975
988
  // kept: itself if a fill or beside no own colour, else the ground beside
976
989
  // it. Where it turns is `segmentsOf`'s, not this.
977
990
  const boundWith = (color, other, theirs) => (fills.has(color.hex) || !theirs ? color.hex : other.hex);
978
991
  // The bounds a colour turns within, by the colour it is bounded with: one bound for every place a frame writes it at.
979
992
  const keyOf = (color, bound) => `${color.hex} ${bound}`;
980
- 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) => {
981
994
  const other = side === "fg" ? "bg" : "fg";
982
995
  return own(pair.terminal[side], pair[side]) ? keyOf(pair[side], boundWith(pair[side], pair[other], own(pair.terminal[other], pair[other]))) : undefined;
983
996
  }));
984
997
  // 256 colours writes a turned colour as one it has an index for; truecolor
985
998
  // as one of the two either side of its luminance.
986
- const indexed = measuredIndexed(depth, theme);
999
+ const indexed = measuredIndexed(depth, terminal);
987
1000
  // Where each cell's colours turn, read off the frame's listing once.
988
1001
  const placeOf = segmentsOf(drawn);
989
1002
  return Animation.continuous((t) => {
@@ -992,20 +1005,26 @@ export function wheel(curve, z) {
992
1005
  // key is the bounds' key with the place, so one entry never serves
993
1006
  // two bounds.
994
1007
  const written = new Map();
995
- const choose = (color, bound, place) => {
1008
+ // The bounds are measured from the colour the element drew; a layer
1009
+ // beneath may have lit it, so they travel with it, and a lit colour
1010
+ // turns round its own luminance rather than snapping back unturned.
1011
+ const choose = (color, rested, bound, place) => {
996
1012
  const turn = hueTurned(color, turned(place, t));
997
- 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 });
998
1016
  };
999
- const write = (color, bound, place) => {
1000
- const key = `${keyOf(color, bound)} ${place.row}:${place.centre}`;
1001
- 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);
1002
1020
  written.set(key, found);
1003
1021
  return found;
1004
1022
  };
1005
1023
  return (shown, cell) => {
1006
- const mine = { fg: own(cell.terminal.fg, shown.fg), bg: own(cell.terminal.bg, shown.bg) };
1007
- const turn = (color, ours, other, theirs) => ours ? write(color, boundWith(color, other, theirs), placeOf(cell, color.hex)) : color;
1008
- 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") };
1009
1028
  };
1010
1029
  });
1011
1030
  });
@@ -1022,10 +1041,16 @@ export function wheel(curve, z) {
1022
1041
  * off (`touchedAt`).
1023
1042
  */
1024
1043
  function hueTurned(color, degrees) {
1025
- if (degrees === 0)
1026
- 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 colours either side of it. */
1052
+ function atLuminance(color, degrees, luminance) {
1027
1053
  const from = Oklch.fromRgba(color);
1028
- const luminance = relativeLuminance(color);
1029
1054
  const at = (l) => new Oklch(l, from.c, from.h + degrees, from.alpha).toRgba();
1030
1055
  let [dim, bright] = [0, 1];
1031
1056
  for (let i = 0; i < 24; i++) {
@@ -1085,10 +1110,10 @@ function turnedBounds(pairs, depth, turnedAs) {
1085
1110
  return new Map([...bounds].map(([key, { lo, hi }]) => [key, hi - lo < ROUNDING_GAP ? STILL : { lo, hi }]));
1086
1111
  }
1087
1112
  const measured = (color) => ({ color, looks: Oklch.fromRgba(color), stop: stop(color) });
1088
- /** `ColorSpec.indexed`'s colours, measured once for each theme and depth. */
1113
+ /** `ColorSpec.indexed`'s colours, measured once for each terminal and depth. */
1089
1114
  const measuredMemo = new WeakMemo();
1090
- function measuredIndexed(depth, theme) {
1091
- 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)));
1092
1117
  }
1093
1118
  /**
1094
1119
  * The colour that looks nearest `color` (OKLab's ΔE): `rest`, or one of
@@ -1110,6 +1135,120 @@ function nearestOf(color, offered) {
1110
1135
  }
1111
1136
  return best?.[0];
1112
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 colour 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-colour 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 colour, which at sixteen colours 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
+ * colour it is handed. A hue a layer turned onto another of the element's
1172
+ * colours 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 colours as the element drew them,
1177
+ * and a colour 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 colour
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 colour 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
+ // colour 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 colour 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 colour, which it has no index for, and the
1208
+ // colour 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 colours as `Effected`
1224
+ // writes them: rounded to the depth, and a translucent one laid on what is
1225
+ // beneath it, so a colour 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 colour 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
+ }
1113
1252
  /**
1114
1253
  * A signal: every cell's ink stepping through `steps`' colours on a beat,
1115
1254
  * one colour a hold, looping, for the time it is handed — a gauge's
@@ -1123,7 +1262,7 @@ function nearestOf(color, offered) {
1123
1262
  * (`ensureContrast`, the one slide a template's `readableOn` makes too);
1124
1263
  * below truecolor, the nearest of the colours the depth has an index for, so on a
1125
1264
  * sixteen-colour terminal it steps from slot to slot, the one effect here
1126
- * that does (`stillOnSlots`). A cell whose ground no offered colour reads
1265
+ * that does (`stillWhereNothingMoves`). A cell whose ground no offered colour reads
1127
1266
  * on keeps the ink it rested in.
1128
1267
  *
1129
1268
  * The search is settled once for each ground and step — below truecolor,
@@ -1144,12 +1283,12 @@ export function steppedInk(steps) {
1144
1283
  const inkOn = legible(drawn);
1145
1284
  return Animation.map(rhythm, (i) => {
1146
1285
  const step = inks[i];
1147
- return (look, cell) => ({ ...look, fg: inkOn(step, look, cell) });
1286
+ return (look, { pair }) => ({ ...look, fg: inkOn(step, pair) });
1148
1287
  });
1149
1288
  };
1150
1289
  }
1151
1290
  /**
1152
- * `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:
1153
1292
  * settled once for each terminal, depth, pair and ink, so a frame drawing
1154
1293
  * what an earlier one drew redoes no search. [LAW:single-enforcer] Every
1155
1294
  * effect that writes an ink of the caller's choosing writes it through
@@ -1162,16 +1301,15 @@ export function steppedInk(steps) {
1162
1301
  */
1163
1302
  function legibleInks() {
1164
1303
  const settled = new WeakMemo();
1165
- return ({ depth, theme }) => {
1166
- const memo = settled.get(theme, () => ({ inks: new Memo(), readable: new Memo() }));
1167
- return (ink, look, cell) => {
1168
- 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) => {
1169
1307
  const key = `${depth} ${pairKey(pair)}`;
1170
- const legibleOf = (offered) => legibleAmong(pair, offered, depth, theme);
1308
+ const legibleOf = (offered) => legibleAmong(pair, offered, depth, terminal);
1171
1309
  return memo.inks.get(`${key} ${ink.hex}`, () => {
1172
1310
  const offered = depth === ColorDepth.TRUECOLOR
1173
1311
  ? legibleOf([ink, ensureContrast(ink, pair.bg, floorOf(pair, depth))].map(measured))
1174
- : memo.readable.get(key, () => legibleOf(measuredIndexed(depth, theme)));
1312
+ : memo.readable.get(key, () => legibleOf(measuredIndexed(depth, terminal)));
1175
1313
  return nearestOf(ink, offered)?.color ?? pair.fg;
1176
1314
  });
1177
1315
  };
@@ -1183,10 +1321,10 @@ function legibleInks() {
1183
1321
  * way `shares` and `wheel` measure: the written colours, at the catalogue's
1184
1322
  * floor. An ink none of them is nearest stays `pair`'s own.
1185
1323
  */
1186
- function legibleAmong(pair, offered, depth, theme) {
1324
+ function legibleAmong(pair, offered, depth, terminal) {
1187
1325
  const floor = floorOf(pair, depth);
1188
1326
  return offered.filter(({ color }) => {
1189
- 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);
1190
1328
  return contrastRatio(shown.fg, shown.bg) >= floor;
1191
1329
  });
1192
1330
  }
@@ -46,7 +46,7 @@
46
46
  */
47
47
  import { Animation } from "../core/animation.js";
48
48
  import { type Beat } from "../core/easing.js";
49
- import type { Motion, RenderOptions } from "../core/protocol.js";
49
+ import type { RenderOptions } from "../core/protocol.js";
50
50
  import type { Segment } from "../core/segment.js";
51
51
  import { RichText } from "../core/text.js";
52
52
  /** One frame: what it shows, and how long it is held, in seconds. */
@@ -86,12 +86,3 @@ export declare class FrameSequence implements Animation<RichText> {
86
86
  maximum: number;
87
87
  };
88
88
  }
89
- /**
90
- * The text frame `animation` shows at host time `t` under `motion`
91
- * (`shownAt`), copied and stamped with the hold it is in there: what the
92
- * template `frames` and `glyphs` functions, which know the reader's
93
- * preference before any render, splice into a line. The stamp is on the
94
- * base style, which an empty text still draws as its one piece of no width,
95
- * and on a copy, since an animation may hand back the one `RichText` it holds.
96
- */
97
- export declare function textFrameAt(animation: Animation<RichText>, t: number, motion: Motion): RichText;
@@ -44,9 +44,8 @@
44
44
  * binding has no console for, so the frame arrives read, as a `Group`'s
45
45
  * children do.
46
46
  */
47
- import { Animation, holdOf, placedRender, shownAt, stamp } from "../core/animation.js";
47
+ import { Animation, placedRender } from "../core/animation.js";
48
48
  import { beat } from "../core/easing.js";
49
- import { layerStyle } from "../core/style.js";
50
49
  import { setIntoLine } from "./embed.js";
51
50
  /** A sequence of frames prepared to be drawn: built once, asked for the frame at a time. */
52
51
  export class FrameSequence {
@@ -102,17 +101,3 @@ export class FrameSequence {
102
101
  return { minimum: this.width, maximum: this.width };
103
102
  }
104
103
  }
105
- /**
106
- * The text frame `animation` shows at host time `t` under `motion`
107
- * (`shownAt`), copied and stamped with the hold it is in there: what the
108
- * template `frames` and `glyphs` functions, which know the reader's
109
- * preference before any render, splice into a line. The stamp is on the
110
- * base style, which an empty text still draws as its one piece of no width,
111
- * and on a copy, since an animation may hand back the one `RichText` it holds.
112
- */
113
- export function textFrameAt(animation, t, motion) {
114
- const shown = shownAt(animation, t, motion);
115
- const stamped = shown.value.copy();
116
- stamped.style = layerStyle(stamped.style, stamp(holdOf(shown, t)));
117
- return stamped;
118
- }
@@ -51,11 +51,11 @@
51
51
  * Positions are the child's own: row and column count from the top-left of
52
52
  * what the child drew.
53
53
  */
54
- import { Animation } from "../core/animation.js";
54
+ import { Animation, type Moves } from "../core/animation.js";
55
55
  import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
56
56
  import { Segment } from "../core/segment.js";
57
57
  import { type RichText } from "../core/text.js";
58
- import { type Moved, type Placed } from "./effect.js";
58
+ import { type Placed } from "./effect.js";
59
59
  /**
60
60
  * A cell of the element a glyph effect wraps: the glyph it shows, where it
61
61
  * sits and who drew it, its place in reading order among the cells handed
@@ -95,20 +95,18 @@ export interface GlyphEffectedOptions {
95
95
  /**
96
96
  * A `GlyphEffected` at `t`, or an element through a glyph effect at a time an
97
97
  * animation's `at` was handed: `renderable` with its cells' glyphs passed
98
- * through `effect` at `t`, saying nothing of when that changes, as a
99
- * `Recolored` is (`./effect.ts`). [LAW:one-type-per-behavior]
98
+ * through `effect` at `t`, telling the hold it drew in as a `Recolored` does
99
+ * (`RenderOptions.onMoved`, `./effect.ts`). [LAW:one-type-per-behavior]
100
100
  */
101
101
  export declare class Reglyphed implements Renderable, Measurable {
102
102
  readonly renderable: Renderable;
103
103
  readonly effect: GlyphEffect;
104
- readonly t: number;
104
+ private readonly _t;
105
105
  readonly key: string;
106
106
  constructor(renderable: Renderable, effect: GlyphEffect, options: GlyphEffectedOptions & {
107
107
  readonly t: number;
108
108
  });
109
109
  render(options: RenderOptions): Iterable<Segment>;
110
- /** The frame the effect draws at `t`, and the hold it says that is in. */
111
- moved(options: RenderOptions): Moved;
112
110
  measure(options: RenderOptions): {
113
111
  minimum: number;
114
112
  maximum: number;
@@ -116,9 +114,10 @@ export declare class Reglyphed implements Renderable, Measurable {
116
114
  }
117
115
  /**
118
116
  * `renderable` under a glyph effect over time, an `Animation<Renderable>`
119
- * as an `Effected` is (`./effect.ts`): its own answer is `t`, and placed,
120
- * its cells carry the glyph effect's exact hold at the frame's time and the
121
- * output's `asciiOnly`, under whatever the element already said.
117
+ * as an `Effected` is (`./effect.ts`): with nothing drawn its answer is `t`,
118
+ * and placed, on its own or as a part of any combinator, it says the glyph
119
+ * effect's exact hold at that time and the output's `asciiOnly`, which the
120
+ * frame's drawing told (`Moves`).
122
121
  */
123
122
  export declare class GlyphEffected implements Animation<Renderable> {
124
123
  readonly renderable: Renderable;
@@ -127,8 +126,8 @@ export declare class GlyphEffected implements Animation<Renderable> {
127
126
  constructor(renderable: Renderable, effect: GlyphEffect, options: GlyphEffectedOptions);
128
127
  /** The element through the glyph effect at `t`, unstamped. */
129
128
  at(t: number): Reglyphed;
130
- nextChange(t: number): number;
131
- lastChange(t: number): number;
129
+ nextChange(t: number, moves?: Moves): number;
130
+ lastChange(t: number, moves?: Moves): number;
132
131
  /** The element under the glyph effect with its motion taken out. */
133
132
  reduced(): GlyphEffected;
134
133
  /** Drawn at the frame's time, its cells stamped with the glyph effect's exact hold there. */
@@ -150,7 +149,7 @@ export declare class GlyphEffected implements Animation<Renderable> {
150
149
  * its every style and span, each moved to where its glyphs now sit, and a
151
150
  * frame that changes nothing hands the text back as it was. What it says of
152
151
  * when it changes is the effect's, unstamped: whoever draws it at a time
153
- * stamps it (`textFrameAt`).
152
+ * stamps it (`placed`), or marks it for the engine to (the template `glyphs`).
154
153
  */
155
154
  export declare function reglyphText(text: RichText, effect: GlyphEffect, options: {
156
155
  readonly key: string;