@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
@@ -51,44 +51,40 @@
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, frameTimeOf, holdOf, shownAt, stampFrame } from "../core/animation.js";
54
+ import { Animation, placedRender, shownAt } from "../core/animation.js";
55
55
  import { cellLen, graphemes } from "../core/cells.js";
56
56
  import { Measurement } from "../core/measure.js";
57
57
  import { render } from "../core/protocol.js";
58
58
  import { Segment } from "../core/segment.js";
59
59
  import { finiteTime } from "../core/easing.js";
60
- import { stripControlChars } from "../core/text.js";
61
- import { cellSeed, walkGlyphs } from "./effect.js";
60
+ import { sanitized } from "../core/text.js";
61
+ import { cellSeed, holdOver, walkGlyphs } from "./effect.js";
62
62
  import { pattern } from "../core/pattern.js";
63
63
  /**
64
64
  * A `GlyphEffected` at `t`, or an element through a glyph effect at a time an
65
65
  * animation's `at` was handed: `renderable` with its cells' glyphs passed
66
- * through `effect` at `t`, saying nothing of when that changes, as a
67
- * `Recolored` is (`./effect.ts`). [LAW:one-type-per-behavior]
66
+ * through `effect` at `t`, telling the hold it drew in as a `Recolored` does
67
+ * (`RenderOptions.onMoved`, `./effect.ts`). [LAW:one-type-per-behavior]
68
68
  */
69
69
  export class Reglyphed {
70
70
  renderable;
71
71
  effect;
72
- t;
72
+ _t;
73
73
  key;
74
74
  constructor(renderable, effect, options) {
75
75
  this.renderable = renderable;
76
76
  this.effect = effect;
77
- this.t = finiteTime("GlyphEffected", options.t);
77
+ this._t = finiteTime("GlyphEffected", options.t);
78
78
  this.key = options.key;
79
79
  }
80
80
  render(options) {
81
- return this.moved(options).segments;
82
- }
83
- /** The frame the effect draws at `t`, and the hold it says that is in. */
84
- moved(options) {
85
81
  const segments = [...render(this.renderable, options)];
86
82
  const { placed, cells } = placeGlyphs(segments, this.key);
87
83
  const animation = this.effect({ cells, asciiOnly: options.asciiOnly === true });
88
- const frame = shownAt(animation, this.t, options);
84
+ const frame = shownAt(animation, this._t, options);
89
85
  const shown = showGlyphs(placed, frame.value);
90
- const changed = segments.map((segment, i) => shown[i].every((text, j) => text === placed[i][j].glyph) ? segment : new Segment(shown[i].join(""), segment.style));
91
- return { segments: changed, hold: holdOf(frame, this.t) };
86
+ options.onMoved?.(this._t, this.key, holdOver(cells.length > 0, frame, this._t));
87
+ return segments.map((segment, i) => (shown[i].every((text, j) => text === placed[i][j].glyph) ? segment : new Segment(shown[i].join(""), segment.style)));
92
88
  }
93
89
  measure(options) {
94
90
  return Measurement.get(options, this.renderable);
@@ -96,9 +92,10 @@ export class Reglyphed {
96
92
  }
97
93
  /**
98
94
  * `renderable` under a glyph effect over time, an `Animation<Renderable>`
99
- * as an `Effected` is (`./effect.ts`): its own answer is `t`, and placed,
100
- * its cells carry the glyph effect's exact hold at the frame's time and the
101
- * output's `asciiOnly`, under whatever the element already said.
95
+ * as an `Effected` is (`./effect.ts`): with nothing drawn its answer is `t`,
96
+ * and placed, on its own or as a part of any combinator, it says the glyph
97
+ * effect's exact hold at that time and the output's `asciiOnly`, which the
98
+ * frame's drawing told (`Moves`).
102
99
  */
103
100
  export class GlyphEffected {
104
101
  renderable;
@@ -113,11 +110,11 @@ export class GlyphEffected {
113
110
  at(t) {
114
111
  return new Reglyphed(this.renderable, this.effect, { t, key: this.key });
115
112
  }
116
- nextChange(t) {
117
- return t;
113
+ nextChange(t, moves) {
114
+ return moves?.(t, this.key)?.nextChange ?? t;
118
115
  }
119
- lastChange(t) {
120
- return t;
116
+ lastChange(t, moves) {
117
+ return moves?.(t, this.key)?.lastChange ?? t;
121
118
  }
122
119
  /** The element under the glyph effect with its motion taken out. */
123
120
  reduced() {
@@ -126,8 +123,7 @@ export class GlyphEffected {
126
123
  }
127
124
  /** Drawn at the frame's time, its cells stamped with the glyph effect's exact hold there. */
128
125
  render(options) {
129
- const { segments, hold } = this.at(frameTimeOf(options)).moved(options);
130
- return stampFrame(segments, hold);
126
+ return placedRender(this, options);
131
127
  }
132
128
  measure(options) {
133
129
  return Measurement.get(options, this.renderable);
@@ -161,7 +157,7 @@ function showGlyphs(placed, frame) {
161
157
  * its every style and span, each moved to where its glyphs now sit, and a
162
158
  * frame that changes nothing hands the text back as it was. What it says of
163
159
  * when it changes is the effect's, unstamped: whoever draws it at a time
164
- * stamps it (`textFrameAt`).
160
+ * stamps it (`placed`), or marks it for the engine to (the template `glyphs`).
165
161
  */
166
162
  export function reglyphText(text, effect, options) {
167
163
  const { placed, cells } = placeGlyphs([new Segment(text.plain)], options.key);
@@ -193,7 +189,7 @@ function rewritten(text, glyphs, shown) {
193
189
  result.stylize(span.style, moved(span.start), moved(span.end));
194
190
  return result;
195
191
  }
196
- /** Line breaks and tabs, which `stripControlChars` leaves for a text's own lines and which no cell may hold. */
192
+ /** Line breaks and tabs, which `sanitized` leaves for a text's own lines and which no cell may hold. */
197
193
  const BREAKS = pattern(/[\n\t]/);
198
194
  /**
199
195
  * What `frame` shows in `cell`, held to the contract: as many cells wide as
@@ -205,7 +201,7 @@ function shows(frame, cell) {
205
201
  const text = frame(cell);
206
202
  if (text === cell.glyph)
207
203
  return text;
208
- if (cellLen(text) !== cellLen(cell.glyph) || BREAKS.test(text) || stripControlChars(text) !== text) {
204
+ if (cellLen(text) !== cellLen(cell.glyph) || BREAKS.test(text) || sanitized(text) !== text) {
209
205
  throw new RangeError(`a glyph effect turned ${JSON.stringify(cell.glyph)} at row ${cell.row}, column ${cell.col} into ${JSON.stringify(text)}: ` +
210
206
  `it must be ${cellLen(cell.glyph)} cell(s) wide, with no line break or control character`);
211
207
  }
@@ -218,18 +214,24 @@ function shows(frame, cell) {
218
214
  * wide as its glyph is `shows`'s check; this is the half no one cell can
219
215
  * see — a trailing zero-width joiner that fuses with the next emoji, or a
220
216
  * leading combining mark that draws on the glyph before it, is right alone
221
- * and wrong beside its neighbour. Only a changed text is asked, so a child
222
- * that splits a cluster across its own segments is drawn as it drew it.
217
+ * and wrong beside its neighbour. Only a join the effect made is asked, seam
218
+ * by seam, so a child that splits a cluster across its own segments is drawn
219
+ * as it drew it, whatever becomes of the glyph its mark sits on.
223
220
  */
224
221
  function standsApart(placed, shown) {
225
- // A line break is no cell, and nothing joins across it.
226
- const beside = (j) => (placed[j]?.cell === undefined ? "" : shown[j]);
222
+ const fuse = (left, right) => graphemes(left + right).length < graphemes(left).length + graphemes(right).length;
223
+ // Whether the text at `i` fuses across its left seam and across its right.
224
+ const seams = (texts, i) => {
225
+ // A line break is no cell, and nothing joins across it.
226
+ const beside = (j) => (placed[j]?.cell === undefined ? "" : texts(j));
227
+ const [before, text, after] = [beside(i - 1), texts(i), beside(i + 1)];
228
+ return [fuse(before, text + after), fuse(before + text, after)];
229
+ };
227
230
  shown.forEach((text, i) => {
228
231
  const { glyph, cell } = placed[i];
229
232
  if (cell === undefined || text === glyph)
230
233
  return;
231
- const [before, after] = [beside(i - 1), beside(i + 1)];
232
- if (graphemes(before + text + after).length !== graphemes(before).length + graphemes(text).length + graphemes(after).length) {
234
+ if (seams((j) => shown[j], i).some((fused, seam) => fused && !seams((j) => placed[j].glyph, i)[seam])) {
233
235
  throw new RangeError(`a glyph effect turned ${JSON.stringify(glyph)} at row ${cell.row}, column ${cell.col} into ${JSON.stringify(text)}, ` +
234
236
  `which joins the cell beside it into one character`);
235
237
  }
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * The glyph effect catalogue: a typewriter and a scramble, which run once and
3
3
  * are done, each with its way out for an element leaving (`backspace`,
4
- * `scrambleOut`), and a wave of case changes, rain and stars, which loop for as
5
- * long as they are drawn. Each is a `GlyphEffect` for `GlyphEffected`
6
- * (`./glyph-effect.ts`), built as the colour catalogue's are
7
- * (`./effects.ts`): one function each over the one shape, made by name, one
8
- * way, at the defaults in `GLYPH_DEFAULTS` (`GLYPH_EFFECT_CATALOGUE`).
4
+ * `scrambleOut`), and a wave of case changes, rain, stars and marching ants,
5
+ * which loop for as long as they are drawn. Each is a `GlyphEffect` for
6
+ * `GlyphEffected` (`./glyph-effect.ts`), built as the colour catalogue's are
7
+ * (`./effects.ts`): one function each over the one shape, at the defaults in
8
+ * `GLYPH_DEFAULTS`. `GLYPH_EFFECT_CATALOGUE` makes by name those a template's
9
+ * `glyphs` takes.
9
10
  *
10
11
  * Where a colour loop glides, a glyph cannot: a cell shows one glyph or
11
12
  * another, so every loop here moves in steps, on a `beat`, and says when its
@@ -90,11 +91,25 @@ export interface Particles {
90
91
  * row a step, and one that holds still draws a fresh field each life.
91
92
  */
92
93
  export declare function particles(spec: Particles, z: number): GlyphEffect;
93
- /** The names of the glyph effects that loop. */
94
+ /** How ants march: a step, in seconds, for the gaps to move one cell, and the most cells a gap may be from the next. */
95
+ export interface MarchingAnts {
96
+ readonly step: number;
97
+ readonly every: number;
98
+ }
99
+ /**
100
+ * Marching ants: gaps blanked round each box the element draws, spaced as
101
+ * evenly as the ring allows and never more than `every` cells apart, walking
102
+ * one cell clockwise a `step`. The whole pattern turns, so a ring that `every`
103
+ * does not divide closes with no seam at its corner. The ring is whatever the box
104
+ * is (`Box.frame`), at any width and beside a title. A ring may draw blanks and
105
+ * they keep the count, but an element with no ink on any ring never changes.
106
+ */
107
+ export declare function marchingAnts({ step, every }: MarchingAnts): GlyphEffect;
108
+ /** The names a template's `glyphs` takes for an effect that loops. */
94
109
  export type GlyphLoopName = "wave" | "rain" | "stars";
95
- /** The names of the glyph effects that run once. */
110
+ /** The names a template's `glyphs` takes for an effect that runs once. */
96
111
  export type GlyphTransitionName = "typewriter" | "scramble" | "backspace" | "scrambleOut";
97
- /** Each catalogue glyph effect's defaults, as first drawn in `examples/glyph-effects`. */
112
+ /** Each glyph effect's defaults, as first drawn in `examples/glyph-effects`. */
98
113
  export declare const GLYPH_DEFAULTS: {
99
114
  readonly typewriter: {
100
115
  readonly seconds: 2;
@@ -132,6 +147,10 @@ export declare const GLYPH_DEFAULTS: {
132
147
  readonly drift: 0;
133
148
  readonly life: 1;
134
149
  };
150
+ readonly march: {
151
+ readonly step: 0.15;
152
+ readonly every: 3;
153
+ };
135
154
  };
136
155
  /**
137
156
  * Every glyph effect in the catalogue by name, each made the one way: for an
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * The glyph effect catalogue: a typewriter and a scramble, which run once and
3
3
  * are done, each with its way out for an element leaving (`backspace`,
4
- * `scrambleOut`), and a wave of case changes, rain and stars, which loop for as
5
- * long as they are drawn. Each is a `GlyphEffect` for `GlyphEffected`
6
- * (`./glyph-effect.ts`), built as the colour catalogue's are
7
- * (`./effects.ts`): one function each over the one shape, made by name, one
8
- * way, at the defaults in `GLYPH_DEFAULTS` (`GLYPH_EFFECT_CATALOGUE`).
4
+ * `scrambleOut`), and a wave of case changes, rain, stars and marching ants,
5
+ * which loop for as long as they are drawn. Each is a `GlyphEffect` for
6
+ * `GlyphEffected` (`./glyph-effect.ts`), built as the colour catalogue's are
7
+ * (`./effects.ts`): one function each over the one shape, at the defaults in
8
+ * `GLYPH_DEFAULTS`. `GLYPH_EFFECT_CATALOGUE` makes by name those a template's
9
+ * `glyphs` takes.
9
10
  *
10
11
  * Where a colour loop glides, a glyph cannot: a cell shows one glyph or
11
12
  * another, so every loop here moves in steps, on a `beat`, and says when its
@@ -50,6 +51,10 @@ function text(cells) {
50
51
  }
51
52
  return { length: place.size, place };
52
53
  }
54
+ /** `n` wrapped into [0, `size`), for a count a timeline may run backwards past 0. */
55
+ function wrapped(n, size) {
56
+ return ((n % size) + size) % size;
57
+ }
53
58
  /** Of `n` cells, how many have arrived at eased progress `p`, an ease that overshoots held to the cells there are. */
54
59
  function arrived(n, p) {
55
60
  return Math.min(n, Math.max(0, Math.floor(n * p)));
@@ -200,7 +205,7 @@ export function caseWave({ step, width }, z) {
200
205
  const start = Math.floor(hash(z, 2.3) * span);
201
206
  return steps((s) => {
202
207
  // The column just past the band's right edge; it enters at the left.
203
- const lead = (((s + start) % span) + span) % span;
208
+ const lead = wrapped(s + start, span);
204
209
  // A wide glyph is in the band if any of its columns is.
205
210
  return (cell) => (cell.col < lead && cell.col + cellLen(cell.glyph) > lead - width ? swapCase(cell.glyph) : cell.glyph);
206
211
  }, AS_WRITTEN);
@@ -250,7 +255,69 @@ export function particles(spec, z) {
250
255
  }, AS_WRITTEN);
251
256
  };
252
257
  }
253
- /** Each catalogue glyph effect's defaults, as first drawn in `examples/glyph-effects`. */
258
+ /**
259
+ * Where each cell stands on the ring of the box frame that drew it, by
260
+ * `index`. A ring is the
261
+ * perimeter of the rectangle a frame's cells cover, read in the element's own
262
+ * rows and columns, so it is true wherever the box sits among them. A rule or
263
+ * divider inside that rectangle, as a table draws, is frame and no part of the
264
+ * ring, and a title set into the frame is no frame but keeps its places.
265
+ */
266
+ function ringPlaces(cells) {
267
+ const frames = new Map();
268
+ for (const cell of cells) {
269
+ if (cell.boxFrame === undefined)
270
+ continue;
271
+ const ring = frames.get(cell.boxFrame);
272
+ if (ring === undefined)
273
+ frames.set(cell.boxFrame, [cell]);
274
+ else
275
+ ring.push(cell);
276
+ }
277
+ const places = new Map();
278
+ for (const ring of frames.values()) {
279
+ // Cells come in reading order, so the rows are the ring's two ends.
280
+ const [top, down] = [ring[0].row, ring[ring.length - 1].row - ring[0].row];
281
+ const left = ring.reduce((most, { col }) => Math.min(most, col), Infinity);
282
+ const across = ring.reduce((most, { col }) => Math.max(most, col), -Infinity) - left;
283
+ const size = Math.max(1, 2 * (across + down));
284
+ for (const cell of ring) {
285
+ const [y, x] = [cell.row - top, cell.col - left];
286
+ const place = y === 0 ? x : x === across ? across + y : y === down ? 2 * across + down - x : x === 0 ? 2 * (across + down) - y : undefined;
287
+ if (place !== undefined)
288
+ places.set(cell.index, { place, cells: size });
289
+ }
290
+ }
291
+ return places;
292
+ }
293
+ /**
294
+ * Marching ants: gaps blanked round each box the element draws, spaced as
295
+ * evenly as the ring allows and never more than `every` cells apart, walking
296
+ * one cell clockwise a `step`. The whole pattern turns, so a ring that `every`
297
+ * does not divide closes with no seam at its corner. The ring is whatever the box
298
+ * is (`Box.frame`), at any width and beside a title. A ring may draw blanks and
299
+ * they keep the count, but an element with no ink on any ring never changes.
300
+ */
301
+ export function marchingAnts({ step, every }) {
302
+ positiveSeconds("marchingAnts step", step);
303
+ if (!(Number.isInteger(every) && every > 1))
304
+ throw new RangeError(`marchingAnts every must be a whole number of cells above 1, got ${every}`);
305
+ const steps = stepped(step);
306
+ return ({ cells }) => {
307
+ const places = ringPlaces(cells);
308
+ if (!cells.some((cell) => places.has(cell.index) && inked(cell.glyph)))
309
+ return Animation.constant(AS_WRITTEN);
310
+ return steps((s) => (cell) => {
311
+ const at = places.get(cell.index);
312
+ if (at === undefined)
313
+ return cell.glyph;
314
+ // Bresenham's line over the ring: of `cells` places, the `gaps` where a running count crosses a whole number.
315
+ const gaps = Math.ceil(at.cells / every);
316
+ return (wrapped(at.place - s, at.cells) * gaps) % at.cells < gaps ? blank(cell.glyph) : cell.glyph;
317
+ }, AS_WRITTEN);
318
+ };
319
+ }
320
+ /** Each glyph effect's defaults, as first drawn in `examples/glyph-effects`. */
254
321
  export const GLYPH_DEFAULTS = {
255
322
  typewriter: { seconds: 2, ease: EASES.linear },
256
323
  scramble: { seconds: 2.5, ease: EASES["ease-in"] },
@@ -259,6 +326,7 @@ export const GLYPH_DEFAULTS = {
259
326
  wave: { step: 0.06, width: 3 },
260
327
  rain: { glyphs: "│╎", ascii: "|:", density: 0.06, step: 0.08, drift: 1, life: Number.POSITIVE_INFINITY },
261
328
  stars: { glyphs: "·+*", ascii: ".+*", density: 0.04, step: 1.5, drift: 0, life: 1 },
329
+ march: { step: 0.15, every: 3 },
262
330
  };
263
331
  /**
264
332
  * Every glyph effect in the catalogue by name, each made the one way: for an
@@ -10,27 +10,31 @@
10
10
  * part of the drawing asks whether it is leaving. Telling it is a new value
11
11
  * (`leave`), as every `Timeline` operation is. [LAW:no-ambient-temporal-coupling]
12
12
  *
13
- * Each half is a `Transition` of a `Veil` on its own clock from 0, handed the time since
14
- * its moment, and its schedule carried onto the host's clock by
15
- * `Timeline.run` is that half's hold — every frame for a fade, once a hold
16
- * for one on a beat — so the transition's schedule, and `done` read off it,
17
- * stays the one rule for where a one-shot ends and when its frame changes,
18
- * and nothing here measures progress itself. The frames carry no word of
19
- * their own on when they change: placed, the lifecycle stamps them with its
20
- * hold at the frame's time (`placedRender`). The exit veils whatever the
21
- * arrival draws, so an element told to leave while it is still arriving leaves
22
- * from where it is, never shown whole first — though a colour exit is spread
23
- * over the cells each frame shows, so a cell the arrival adds can move one
24
- * already leaving back toward whole (`ownChange`, `./effects.ts`).
25
- * [LAW:dataflow-not-control-flow]
13
+ * Each half is a `Half`: handed the element, an `Animation<Renderable>` of
14
+ * it on the half's own clock from 0, in the shape of the library's other
15
+ * one-shot from renderables to an animation (`Change`, `./change.ts`). One
16
+ * value carries the half's frames and its holds, so neither is kept in
17
+ * agreement with the other by hand, and nothing here measures progress
18
+ * itself: a half's schedule carried onto the host's clock by `Timeline.run`
19
+ * is that half's hold — every frame for a fade, once a hold for one on a
20
+ * beat — and the moment it says it never changes again (`Animation.done`)
21
+ * is where the arrival ends and where the element is gone. The frames carry
22
+ * no word of their own on when they change: placed, the lifecycle stamps
23
+ * them with its hold at the frame's time (`placedRender`). The exit is
24
+ * drawn over whatever the arrival draws, so an element told to leave while
25
+ * it is still arriving leaves from where it is, never shown whole first —
26
+ * though a colour exit is spread over the cells each frame shows, so a cell
27
+ * the arrival adds can move one already leaving back toward whole
28
+ * (`ownChange`, `./effects.ts`). [LAW:dataflow-not-control-flow]
26
29
  *
27
30
  * A half draws the element through a colour transition (`throughColors`, a
28
- * `fadeIn`) or a glyph one (`throughGlyphs`, a `typewriter`): each lift binds
29
- * its transition to the one wrapper that draws it, and to what that wrapper
30
- * needs — a theme for colours, nothing for glyphs — so nothing here asks which
31
- * kind a half is, and a line can type itself in and dissolve out. An exit is
32
- * a transition that ends with nothing drawn — `dissolveOut`, `backspace`,
33
- * `scrambleOut` — so the element has already gone from sight when it is gone.
31
+ * `fadeIn`) or a glyph one (`throughGlyphs`, a `typewriter`): each binds its
32
+ * transition to the one wrapper that draws it, so nothing here asks which kind
33
+ * a half is, and a line can type itself in and dissolve out. A half
34
+ * written by hand is any function of that shape — an arrival that slides the
35
+ * element in, which no effect draws. An exit is a half that ends with nothing
36
+ * drawn — `dissolveOut`, `backspace`, `scrambleOut` — so the element has
37
+ * already gone from sight when it is gone.
34
38
  *
35
39
  * Gone is a fact the host reads (`gone(t)`) to drop the element, and from then
36
40
  * the lifecycle draws nothing and never changes, so a host still holding it
@@ -38,27 +42,31 @@
38
42
  * backwards — it draws nothing too, but is not gone.
39
43
  */
40
44
  import { Animation } from "../core/animation.js";
41
- import type { TerminalTheme } from "../core/color.js";
42
45
  import type { Renderable } from "../core/protocol.js";
43
- import { Transition } from "./effects.js";
46
+ import type { Transition } from "./effects.js";
44
47
  import type { GlyphTransition } from "./glyph-effects.js";
45
48
  /**
46
- * An element drawn through one half of its lifecycle at `t`, the seconds since
47
- * that half began, its effect's seeds named by `key`, saying nothing of when
48
- * that changes — the half's schedule does, as a `Recolored`'s and a
49
- * `Reglyphed`'s frame say nothing.
49
+ * One half of an element's lifecycle: handed the element and `key`, which
50
+ * names it for the half's seeds, the element as the half draws it from
51
+ * `t = 0`, the moment the half began, on its own clock. Its holds are the
52
+ * half's own, whatever element it is handed, and it is done from the first
53
+ * moment it says it never changes again (`Animation.done`): an arrival is
54
+ * the element from then, and an exit is gone. A half that never ends is
55
+ * refused when the lifecycle is made: an `Effected` or `GlyphEffected`
56
+ * handed in bare is one, since its own word is "now" at every time;
57
+ * `throughColors` and `throughGlyphs` lend one the transition's schedule.
50
58
  */
51
- export type Veil = (element: Renderable, t: number, key: string) => Renderable;
52
- /** `transition`'s colours drawn over an element on `theme`, whose ground it arrives from and leaves to. */
53
- export declare function throughColors(transition: Transition, theme: TerminalTheme): Transition<Veil>;
59
+ export type Half = (element: Renderable, key: string) => Animation<Renderable>;
60
+ /** `transition`'s colours drawn over an element, arriving from and leaving to the ground of the terminal the frame is drawn on. */
61
+ export declare function throughColors(transition: Transition): Half;
54
62
  /** `transition`'s glyphs drawn over an element, at every depth, with no colour too. */
55
- export declare function throughGlyphs(transition: GlyphTransition): Transition<Veil>;
63
+ export declare function throughGlyphs(transition: GlyphTransition): Half;
56
64
  /** How an element arrives and leaves, and what it is named for their seeds. */
57
65
  export interface LifecycleOptions {
58
- /** The half it arrives through — `throughColors(fadeIn(curve, z), theme)`. */
59
- readonly enter: Transition<Veil>;
66
+ /** The half it arrives through — `throughColors(fadeIn(curve, z))`. */
67
+ readonly enter: Half;
60
68
  /** The half it leaves through — `throughGlyphs(scrambleOut(timing, z))`. */
61
- readonly exit: Transition<Veil>;
69
+ readonly exit: Half;
62
70
  /** Names the element for both halves' seeds, as `Effected`'s and `GlyphEffected`'s key does. */
63
71
  readonly key: string;
64
72
  }
@@ -10,27 +10,31 @@
10
10
  * part of the drawing asks whether it is leaving. Telling it is a new value
11
11
  * (`leave`), as every `Timeline` operation is. [LAW:no-ambient-temporal-coupling]
12
12
  *
13
- * Each half is a `Transition` of a `Veil` on its own clock from 0, handed the time since
14
- * its moment, and its schedule carried onto the host's clock by
15
- * `Timeline.run` is that half's hold — every frame for a fade, once a hold
16
- * for one on a beat — so the transition's schedule, and `done` read off it,
17
- * stays the one rule for where a one-shot ends and when its frame changes,
18
- * and nothing here measures progress itself. The frames carry no word of
19
- * their own on when they change: placed, the lifecycle stamps them with its
20
- * hold at the frame's time (`placedRender`). The exit veils whatever the
21
- * arrival draws, so an element told to leave while it is still arriving leaves
22
- * from where it is, never shown whole first — though a colour exit is spread
23
- * over the cells each frame shows, so a cell the arrival adds can move one
24
- * already leaving back toward whole (`ownChange`, `./effects.ts`).
25
- * [LAW:dataflow-not-control-flow]
13
+ * Each half is a `Half`: handed the element, an `Animation<Renderable>` of
14
+ * it on the half's own clock from 0, in the shape of the library's other
15
+ * one-shot from renderables to an animation (`Change`, `./change.ts`). One
16
+ * value carries the half's frames and its holds, so neither is kept in
17
+ * agreement with the other by hand, and nothing here measures progress
18
+ * itself: a half's schedule carried onto the host's clock by `Timeline.run`
19
+ * is that half's hold — every frame for a fade, once a hold for one on a
20
+ * beat — and the moment it says it never changes again (`Animation.done`)
21
+ * is where the arrival ends and where the element is gone. The frames carry
22
+ * no word of their own on when they change: placed, the lifecycle stamps
23
+ * them with its hold at the frame's time (`placedRender`). The exit is
24
+ * drawn over whatever the arrival draws, so an element told to leave while
25
+ * it is still arriving leaves from where it is, never shown whole first —
26
+ * though a colour exit is spread over the cells each frame shows, so a cell
27
+ * the arrival adds can move one already leaving back toward whole
28
+ * (`ownChange`, `./effects.ts`). [LAW:dataflow-not-control-flow]
26
29
  *
27
30
  * A half draws the element through a colour transition (`throughColors`, a
28
- * `fadeIn`) or a glyph one (`throughGlyphs`, a `typewriter`): each lift binds
29
- * its transition to the one wrapper that draws it, and to what that wrapper
30
- * needs — a theme for colours, nothing for glyphs — so nothing here asks which
31
- * kind a half is, and a line can type itself in and dissolve out. An exit is
32
- * a transition that ends with nothing drawn — `dissolveOut`, `backspace`,
33
- * `scrambleOut` — so the element has already gone from sight when it is gone.
31
+ * `fadeIn`) or a glyph one (`throughGlyphs`, a `typewriter`): each binds its
32
+ * transition to the one wrapper that draws it, so nothing here asks which kind
33
+ * a half is, and a line can type itself in and dissolve out. A half
34
+ * written by hand is any function of that shape — an arrival that slides the
35
+ * element in, which no effect draws. An exit is a half that ends with nothing
36
+ * drawn — `dissolveOut`, `backspace`, `scrambleOut` — so the element has
37
+ * already gone from sight when it is gone.
34
38
  *
35
39
  * Gone is a fact the host reads (`gone(t)`) to drop the element, and from then
36
40
  * the lifecycle draws nothing and never changes, so a host still holding it
@@ -40,20 +44,31 @@
40
44
  import { Animation } from "../core/animation.js";
41
45
  import { firstReaching } from "../core/easing.js";
42
46
  import { Timeline } from "../core/timeline.js";
43
- import { Recolored } from "./effect.js";
44
- import { Transition } from "./effects.js";
45
- import { Reglyphed } from "./glyph-effect.js";
46
- /** `transition`'s colours drawn over an element on `theme`, whose ground it arrives from and leaves to. */
47
- export function throughColors(transition, theme) {
48
- return lift(transition, (element, t, key) => new Recolored(element, transition.effect, { t, key, theme }));
47
+ import { Effected } from "./effect.js";
48
+ import { GlyphEffected } from "./glyph-effect.js";
49
+ /** `transition`'s colours drawn over an element, arriving from and leaving to the ground of the terminal the frame is drawn on. */
50
+ export function throughColors(transition) {
51
+ return (element, key) => scheduled(new Effected(element, transition.effect, { key }), transition.schedule);
49
52
  }
50
53
  /** `transition`'s glyphs drawn over an element, at every depth, with no colour too. */
51
54
  export function throughGlyphs(transition) {
52
- return lift(transition, (element, t, key) => new Reglyphed(element, transition.effect, { t, key }));
55
+ return (element, key) => scheduled(new GlyphEffected(element, transition.effect, { key }), transition.schedule);
53
56
  }
54
- /** [LAW:one-source-of-truth] A lifted half keeps its transition's schedule, so `done` and every hold are the transition's own. */
55
- function lift(transition, veil) {
56
- return new Transition(veil, transition.schedule);
57
+ /**
58
+ * `placed` — an element under a transition's effect — changing when
59
+ * `schedule`, the transition's own, says: its frames with the one word on
60
+ * when they change that the transition has and the wrapper lacks, whose own
61
+ * answer is "now" until it is drawn. [LAW:one-source-of-truth] Reduced,
62
+ * both are reduced together, as `onBeat` steps both: a one-shot's effect
63
+ * and its schedule each reduce to a cut at 0 (`transition`, `./effects.ts`).
64
+ */
65
+ function scheduled(placed, schedule) {
66
+ return Animation.of({
67
+ at: (t) => placed.at(t),
68
+ nextChange: (t) => schedule.nextChange(t),
69
+ lastChange: (t) => schedule.lastChange(t),
70
+ reduced: () => scheduled(placed.reduced(), schedule.reduced()),
71
+ });
57
72
  }
58
73
  /** What an element draws before it entered and once it is gone: no cells, and no width. */
59
74
  const NOTHING = { render: () => [], measure: () => ({ minimum: 0, maximum: 0 }) };
@@ -61,26 +76,50 @@ const NOTHING = { render: () => [], measure: () => ({ minimum: 0, maximum: 0 })
61
76
  export function lifecycle(element, options, entered) {
62
77
  if (!Number.isFinite(entered))
63
78
  throw new RangeError(`lifecycle: entered must be a finite time, got ${entered}`);
64
- return life(element, options, entered, Number.POSITIVE_INFINITY);
79
+ const { enter, exit, key } = options;
80
+ const arrival = ending(enter(element, key), "enter", key);
81
+ const departure = ending(exit(element, key), "exit", key);
82
+ return life(element, options, { arrival, arrived: doneAt(arrival, entered), departure }, entered, Number.POSITIVE_INFINITY);
65
83
  }
66
84
  /**
67
- * The first host moment `transition`, begun at host time `start`, is done:
68
- * its own `done` read on host time, never a sum `start + seconds`, which can
69
- * round to either side of it, as `Animation.sequence` carries its switch.
70
- * [LAW:single-enforcer] Since `x - start` never falls as `x` rises, a host time
71
- * is at or past this moment exactly when `done` holds there.
85
+ * The first host moment `half`, begun at host time `start`, is done: its own
86
+ * answer read on host time, never a sum `start + seconds`, which can round
87
+ * to either side of it, as `Animation.sequence` carries its switch.
88
+ * [LAW:single-enforcer] Since `x - start` never falls as `x` rises, a host
89
+ * time is at or past this moment exactly when `done` holds there. Done is
90
+ * for good, so a half done as it begins — the element at once — is done at
91
+ * `start`, and from a start it is not done at the walk is upward only: no
92
+ * half is asked about a time before it began.
72
93
  */
73
- function doneAt(transition, start) {
74
- return Number.isFinite(start) ? firstReaching(start, (x) => transition.done(x - start)) : start;
94
+ function doneAt(half, start) {
95
+ if (!Number.isFinite(start) || Animation.done(half, 0))
96
+ return start;
97
+ return firstReaching(start, (x) => Animation.done(half, x - start));
75
98
  }
76
- function life(element, options, entered, left) {
77
- const { enter, exit, key } = options;
78
- const arrived = doneAt(enter, entered);
79
- const goneAt = doneAt(exit, left);
80
- // The arrival's schedule on the host's clock: when its frame next changes, and since when it has been what it is.
81
- const arriving = Timeline.run(Timeline.play(entered), enter.schedule);
99
+ /**
100
+ * `half` over the element, asked where it ends on its own clock, or refused
101
+ * by its `role` and the element's `key` when it never does. [LAW:no-silent-failure]
102
+ * Kept, it would never arrive, or never be gone, and wake the host every frame
103
+ * for good; asked here, the refusal lands where the options were written and
104
+ * not at the first `leave`. A half on a beat that never ends is refused by the
105
+ * beat instead, where the walk asks it past what its holds resolve.
106
+ */
107
+ function ending(half, role, key) {
108
+ if (doneAt(half, 0) === Number.POSITIVE_INFINITY) {
109
+ throw new RangeError(`lifecycle "${key}": the ${role} half never ends: its animation never says it has stopped changing (Animation.done). An Effected or GlyphEffected on its own does not; throughColors(transition) and throughGlyphs(transition) lend one the transition's schedule`);
110
+ }
111
+ return half;
112
+ }
113
+ function life(element, options, halves, entered, left) {
114
+ const { exit, key } = options;
115
+ const { arrival, arrived, departure } = halves;
116
+ // Where the exit is gone, and the exit's hold, are read off the exit over the element; the frame while
117
+ // leaving is the exit over what is shown, whose holds are the same by `Half`'s contract.
118
+ const goneAt = doneAt(departure, left);
119
+ // The arrival on the host's clock: when its frame next changes, and since when it has been what it is.
120
+ const arriving = Timeline.run(Timeline.play(entered), arrival);
82
121
  // What the element draws by `t` if it is not leaving: arriving through `enter`, then itself.
83
- const shown = (t) => (t < arrived ? enter.effect(element, t - entered, key) : element);
122
+ const shown = (t) => (t < arrived ? arrival.at(t - entered) : element);
84
123
  const held = (schedule, t) => ({ last: schedule.lastChange(t), next: schedule.nextChange(t) });
85
124
  // [LAW:one-source-of-truth] Each phase's frame and its hold, side by side, so the two cannot disagree.
86
125
  // The frames say nothing of when they change — the hold does, stamped where the lifecycle is placed.
@@ -89,10 +128,10 @@ function life(element, options, entered, left) {
89
128
  return { frame: NOTHING, last: Number.NEGATIVE_INFINITY, next: entered };
90
129
  if (t >= goneAt)
91
130
  return { frame: NOTHING, last: goneAt, next: Number.POSITIVE_INFINITY };
92
- // Leaving veils whatever arriving draws, so both schedules count; once arrived, the arrival's never changes again.
131
+ // Leaving is drawn over whatever arriving draws, so both schedules count; once arrived, the arrival's never changes again.
93
132
  if (t >= left) {
94
- const leaving = Animation.all([arriving, Timeline.run(Timeline.play(left), exit.schedule)]);
95
- return { frame: exit.effect(shown(t), t - left, key), ...held(leaving, t) };
133
+ const leaving = Animation.all([arriving, Timeline.run(Timeline.play(left), departure)]);
134
+ return { frame: exit(shown(t), key).at(t - left), ...held(leaving, t) };
96
135
  }
97
136
  if (t < arrived)
98
137
  return { frame: shown(t), ...held(arriving, t) };
@@ -126,7 +165,7 @@ function life(element, options, entered, left) {
126
165
  throw new RangeError(`lifecycle: already told to leave at ${left}, told again at ${at}`);
127
166
  if (!(Number.isFinite(at) && at >= entered))
128
167
  throw new RangeError(`lifecycle: leave must be a finite time from entered (${entered}) on, got ${at}`);
129
- return life(element, options, entered, at);
168
+ return life(element, options, halves, entered, at);
130
169
  },
131
170
  });
132
171
  }