@promptctl/rich-js 0.27.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 (110) hide show
  1. package/README.md +1 -0
  2. package/THIRD-PARTY-NOTICES +42 -0
  3. package/dist/core/animation.d.ts +36 -17
  4. package/dist/core/animation.js +61 -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 +6 -7
  18. package/dist/core/color.js +11 -13
  19. package/dist/core/console.d.ts +18 -1
  20. package/dist/core/console.js +24 -5
  21. package/dist/core/control-codes.d.ts +61 -0
  22. package/dist/core/control-codes.js +157 -0
  23. package/dist/core/env.d.ts +6 -0
  24. package/dist/core/env.js +9 -1
  25. package/dist/core/export-html.js +4 -3
  26. package/dist/core/export-lines.d.ts +2 -0
  27. package/dist/core/export-lines.js +7 -3
  28. package/dist/core/export-svg.js +6 -5
  29. package/dist/core/json.js +9 -2
  30. package/dist/core/markup.js +4 -3
  31. package/dist/core/orient.d.ts +68 -0
  32. package/dist/core/orient.js +224 -0
  33. package/dist/core/osc8.d.ts +12 -5
  34. package/dist/core/osc8.js +14 -7
  35. package/dist/core/pretty.js +22 -18
  36. package/dist/core/protocol.d.ts +27 -6
  37. package/dist/core/protocol.js +14 -6
  38. package/dist/core/render.d.ts +8 -3
  39. package/dist/core/render.js +18 -5
  40. package/dist/core/seal.d.ts +26 -0
  41. package/dist/core/seal.js +75 -0
  42. package/dist/core/segment.d.ts +9 -1
  43. package/dist/core/segment.js +11 -1
  44. package/dist/core/style.d.ts +29 -1
  45. package/dist/core/style.js +41 -5
  46. package/dist/core/text-order.d.ts +51 -0
  47. package/dist/core/text-order.js +50 -0
  48. package/dist/core/text.d.ts +10 -0
  49. package/dist/core/text.js +133 -87
  50. package/dist/core/timeline.js +6 -6
  51. package/dist/core/view.d.ts +1 -1
  52. package/dist/core/view.js +5 -4
  53. package/dist/core/wrap.d.ts +24 -6
  54. package/dist/core/wrap.js +82 -16
  55. package/dist/host/app.d.ts +44 -3
  56. package/dist/host/app.js +111 -6
  57. package/dist/host/index.d.ts +3 -2
  58. package/dist/host/index.js +1 -0
  59. package/dist/host/input.d.ts +35 -2
  60. package/dist/host/input.js +88 -4
  61. package/dist/host/terminal-colors.d.ts +18 -0
  62. package/dist/host/terminal-colors.js +46 -0
  63. package/dist/host/terminal-host.d.ts +2 -1
  64. package/dist/index.d.ts +5 -4
  65. package/dist/index.js +2 -2
  66. package/dist/renderables/change.d.ts +1 -3
  67. package/dist/renderables/change.js +4 -6
  68. package/dist/renderables/effect.d.ts +60 -59
  69. package/dist/renderables/effect.js +91 -81
  70. package/dist/renderables/effects.d.ts +37 -8
  71. package/dist/renderables/effects.js +185 -52
  72. package/dist/renderables/frames.d.ts +1 -10
  73. package/dist/renderables/frames.js +1 -16
  74. package/dist/renderables/glyph-effect.d.ts +12 -13
  75. package/dist/renderables/glyph-effect.js +34 -32
  76. package/dist/renderables/glyph-effects.d.ts +27 -8
  77. package/dist/renderables/glyph-effects.js +75 -7
  78. package/dist/renderables/lifecycle.d.ts +5 -7
  79. package/dist/renderables/lifecycle.js +6 -7
  80. package/dist/renderables/markdown.js +4 -2
  81. package/dist/renderables/panel.js +6 -4
  82. package/dist/renderables/progressBar.d.ts +3 -3
  83. package/dist/renderables/progressBar.js +16 -13
  84. package/dist/renderables/syntax.js +7 -2
  85. package/dist/renderables/table.js +2 -1
  86. package/dist/template-bindings/animation-funcs.d.ts +49 -18
  87. package/dist/template-bindings/animation-funcs.js +115 -32
  88. package/dist/template-bindings/index.d.ts +7 -5
  89. package/dist/template-bindings/index.js +8 -5
  90. package/dist/template-bindings/palette-funcs.d.ts +1 -1
  91. package/dist/template-bindings/palette-funcs.js +1 -1
  92. package/dist/themes/buildPalette.d.ts +3 -2
  93. package/dist/themes/buildPalette.js +2 -2
  94. package/dist/themes/data/index.js +1 -4
  95. package/dist/themes/data/textual-ansi.js +0 -143
  96. package/dist/themes/ramp.d.ts +1 -1
  97. package/dist/themes/ramp.js +1 -1
  98. package/dist/themes/registry.d.ts +6 -8
  99. package/dist/themes/registry.js +2 -4
  100. package/dist/themes/terminalThemes.d.ts +4 -11
  101. package/dist/themes/terminalThemes.js +31 -47
  102. package/dist/widgets/button.d.ts +1 -1
  103. package/dist/widgets/button.js +6 -6
  104. package/dist/widgets/checkbox.js +4 -4
  105. package/dist/widgets/dropdown.d.ts +1 -1
  106. package/dist/widgets/dropdown.js +7 -5
  107. package/dist/widgets/themed-widget.js +3 -3
  108. package/dist/widgets/toggle.d.ts +1 -1
  109. package/dist/widgets/toggle.js +5 -5
  110. 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) => {
@@ -953,7 +961,7 @@ function segmentsOf(drawn) {
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
963
  * turn only happens to the element's own colours (`Drawn.colors`), never the
956
- * terminal's. On a sixteen-colour terminal nothing turns (`stillOnSlots`).
964
+ * terminal's. On a sixteen-colour terminal nothing turns (`stillWhereNothingMoves`).
957
965
  */
958
966
  export function wheel(curve, z) {
959
967
  requireCurve(curve);
@@ -972,23 +980,23 @@ export function wheel(curve, z) {
972
980
  const turned = (place, t) => (360 * t) / P + curve.swing * STRAY * ((stray(place, t) - stray(place, 0)) / 1.5);
973
981
  // A turned colour'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;
983
+ return stillWhereNothingMoves(SIDES, ({ colors }) => colors, (drawn) => {
984
+ const { colors, fills, depth, terminal } = drawn;
977
985
  // 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);
986
+ const own = (supplied, color) => !supplied && colors.has(color.hex);
979
987
  // The colour an own colour is bounded with, under which its bounds are
980
988
  // kept: itself if a fill or beside no own colour, 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
991
  // The bounds a colour turns within, by the colour 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
997
  // 256 colours writes a turned colour 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);
999
+ const indexed = measuredIndexed(depth, terminal);
992
1000
  // Where each cell's colours turn, read off the frame's listing once.
993
1001
  const placeOf = segmentsOf(drawn);
994
1002
  return Animation.continuous((t) => {
@@ -997,20 +1005,26 @@ export function wheel(curve, z) {
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 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) => {
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
  });
@@ -1027,10 +1041,16 @@ export function wheel(curve, z) {
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 colours 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++) {
@@ -1090,10 +1110,10 @@ 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 colours, 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
1119
  * The colour that looks nearest `color` (OKLab's ΔE): `rest`, or one of
@@ -1115,6 +1135,120 @@ 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 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
+ }
1118
1252
  /**
1119
1253
  * A signal: every cell's ink stepping through `steps`' colours on a beat,
1120
1254
  * one colour a hold, looping, for the time it is handed — a gauge's
@@ -1128,7 +1262,7 @@ function nearestOf(color, offered) {
1128
1262
  * (`ensureContrast`, the one slide a template's `readableOn` makes too);
1129
1263
  * below truecolor, the nearest of the colours the depth has an index for, so on a
1130
1264
  * 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
1265
+ * that does (`stillWhereNothingMoves`). A cell whose ground no offered colour 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,
@@ -1149,12 +1283,12 @@ 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
@@ -1167,16 +1301,15 @@ export function steppedInk(steps) {
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
  };
@@ -1188,10 +1321,10 @@ function legibleInks() {
1188
1321
  * way `shares` and `wheel` measure: the written colours, 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
  }
@@ -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;