@promptctl/rich-js 0.24.1 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/README.md +7 -0
  2. package/dist/core/anchor.d.ts +5 -0
  3. package/dist/core/anchor.js +11 -4
  4. package/dist/core/animation.d.ts +175 -10
  5. package/dist/core/animation.js +283 -15
  6. package/dist/core/ansi.js +2 -2
  7. package/dist/core/box.d.ts +7 -7
  8. package/dist/core/cells.d.ts +23 -13
  9. package/dist/core/cells.js +58 -48
  10. package/dist/core/clock.d.ts +2 -2
  11. package/dist/core/clock.js +6 -4
  12. package/dist/core/color.d.ts +27 -10
  13. package/dist/core/color.js +21 -29
  14. package/dist/core/console.d.ts +28 -17
  15. package/dist/core/console.js +44 -39
  16. package/dist/core/easing.d.ts +30 -0
  17. package/dist/core/easing.js +65 -20
  18. package/dist/core/emoji.d.ts +1 -1
  19. package/dist/core/emoji.js +1 -2
  20. package/dist/core/export-html.js +2 -1
  21. package/dist/core/export-lines.js +3 -3
  22. package/dist/core/highlighter.d.ts +9 -8
  23. package/dist/core/highlighter.js +3 -2
  24. package/dist/core/json.js +2 -2
  25. package/dist/core/markup.js +16 -12
  26. package/dist/core/measure.js +2 -2
  27. package/dist/core/memo.d.ts +11 -2
  28. package/dist/core/memo.js +16 -0
  29. package/dist/core/oklch.js +17 -6
  30. package/dist/core/osc8.d.ts +1 -1
  31. package/dist/core/osc8.js +4 -5
  32. package/dist/core/paint.d.ts +2 -1
  33. package/dist/core/paint.js +4 -3
  34. package/dist/core/pattern.d.ts +21 -0
  35. package/dist/core/pattern.js +21 -0
  36. package/dist/core/pretty.js +9 -10
  37. package/dist/core/protocol.d.ts +100 -39
  38. package/dist/core/protocol.js +167 -21
  39. package/dist/core/render.d.ts +15 -2
  40. package/dist/core/render.js +25 -5
  41. package/dist/core/segment.d.ts +13 -5
  42. package/dist/core/segment.js +23 -9
  43. package/dist/core/spinnerData.d.ts +1 -1
  44. package/dist/core/state.d.ts +53 -0
  45. package/dist/core/state.js +82 -0
  46. package/dist/core/strip.d.ts +13 -16
  47. package/dist/core/strip.js +119 -110
  48. package/dist/core/style.d.ts +47 -14
  49. package/dist/core/style.js +57 -28
  50. package/dist/core/text.d.ts +5 -8
  51. package/dist/core/text.js +116 -124
  52. package/dist/core/timeline.d.ts +30 -5
  53. package/dist/core/timeline.js +43 -7
  54. package/dist/core/tween.d.ts +1 -1
  55. package/dist/core/tween.js +5 -3
  56. package/dist/core/view.d.ts +92 -0
  57. package/dist/core/view.js +83 -0
  58. package/dist/host/app.d.ts +49 -21
  59. package/dist/host/app.js +71 -37
  60. package/dist/host/input.js +3 -4
  61. package/dist/index.d.ts +24 -12
  62. package/dist/index.js +9 -4
  63. package/dist/node/traceback.js +4 -3
  64. package/dist/renderables/align.d.ts +2 -3
  65. package/dist/renderables/align.js +0 -3
  66. package/dist/renderables/change.d.ts +42 -0
  67. package/dist/renderables/change.js +112 -0
  68. package/dist/renderables/columns.d.ts +3 -4
  69. package/dist/renderables/columns.js +2 -7
  70. package/dist/renderables/constrain.d.ts +2 -3
  71. package/dist/renderables/constrain.js +0 -3
  72. package/dist/renderables/effect.d.ts +132 -54
  73. package/dist/renderables/effect.js +188 -115
  74. package/dist/renderables/effects.d.ts +122 -11
  75. package/dist/renderables/effects.js +238 -94
  76. package/dist/renderables/embed.d.ts +1 -1
  77. package/dist/renderables/embed.js +16 -8
  78. package/dist/renderables/flexStrip.d.ts +2 -3
  79. package/dist/renderables/flexStrip.js +0 -4
  80. package/dist/renderables/frames.d.ts +35 -54
  81. package/dist/renderables/frames.js +43 -58
  82. package/dist/renderables/glyph-effect.d.ts +158 -0
  83. package/dist/renderables/glyph-effect.js +237 -0
  84. package/dist/renderables/glyph-effects.d.ts +147 -0
  85. package/dist/renderables/glyph-effects.js +283 -0
  86. package/dist/renderables/group.d.ts +3 -4
  87. package/dist/renderables/group.js +0 -3
  88. package/dist/renderables/layout.d.ts +12 -12
  89. package/dist/renderables/layout.js +40 -46
  90. package/dist/renderables/lifecycle.d.ts +77 -0
  91. package/dist/renderables/lifecycle.js +132 -0
  92. package/dist/renderables/list-view.d.ts +4 -5
  93. package/dist/renderables/list-view.js +9 -13
  94. package/dist/renderables/live.d.ts +36 -21
  95. package/dist/renderables/live.js +88 -41
  96. package/dist/renderables/markdown-parse.d.ts +4 -4
  97. package/dist/renderables/markdown-parse.js +52 -46
  98. package/dist/renderables/markdown-url.js +11 -11
  99. package/dist/renderables/padding.d.ts +4 -5
  100. package/dist/renderables/padding.js +0 -3
  101. package/dist/renderables/panel.d.ts +14 -16
  102. package/dist/renderables/panel.js +6 -14
  103. package/dist/renderables/progress.d.ts +83 -34
  104. package/dist/renderables/progress.js +182 -95
  105. package/dist/renderables/progressBar.d.ts +22 -12
  106. package/dist/renderables/progressBar.js +32 -9
  107. package/dist/renderables/prompt.js +3 -2
  108. package/dist/renderables/python-space.js +2 -1
  109. package/dist/renderables/scroll.d.ts +50 -0
  110. package/dist/renderables/scroll.js +106 -0
  111. package/dist/renderables/spinner.d.ts +35 -14
  112. package/dist/renderables/spinner.js +65 -25
  113. package/dist/renderables/status.js +1 -1
  114. package/dist/renderables/syntax.d.ts +10 -9
  115. package/dist/renderables/syntax.js +3 -2
  116. package/dist/renderables/table.d.ts +34 -40
  117. package/dist/renderables/table.js +41 -63
  118. package/dist/renderables/traceback.d.ts +2 -2
  119. package/dist/renderables/traceback.js +6 -5
  120. package/dist/renderables/tree.d.ts +27 -25
  121. package/dist/renderables/tree.js +104 -58
  122. package/dist/renderables/viewport.d.ts +75 -44
  123. package/dist/renderables/viewport.js +145 -82
  124. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  125. package/dist/template-bindings/animation-funcs.js +132 -14
  126. package/dist/template-bindings/index.d.ts +5 -5
  127. package/dist/template-bindings/index.js +5 -5
  128. package/dist/themes/ramp.d.ts +46 -3
  129. package/dist/themes/ramp.js +61 -4
  130. package/dist/themes/registry.js +5 -7
  131. package/dist/widgets/button.js +3 -2
  132. package/dist/widgets/checkbox.js +4 -3
  133. package/dist/widgets/dropdown.d.ts +3 -1
  134. package/dist/widgets/dropdown.js +20 -15
  135. package/dist/widgets/event-router.js +4 -2
  136. package/dist/widgets/focus-manager.d.ts +2 -0
  137. package/dist/widgets/focus-manager.js +6 -4
  138. package/dist/widgets/hit.d.ts +6 -0
  139. package/dist/widgets/hit.js +8 -1
  140. package/dist/widgets/observable.d.ts +23 -0
  141. package/dist/widgets/observable.js +43 -0
  142. package/dist/widgets/slider.d.ts +1 -1
  143. package/dist/widgets/slider.js +12 -3
  144. package/dist/widgets/static-item.d.ts +2 -3
  145. package/dist/widgets/static-item.js +0 -3
  146. package/dist/widgets/text-input.d.ts +28 -32
  147. package/dist/widgets/text-input.js +360 -346
  148. package/dist/widgets/themed-widget.js +2 -1
  149. package/dist/widgets/toggle.js +4 -3
  150. package/dist/widgets/types.d.ts +10 -4
  151. package/dist/widgets/types.js +8 -2
  152. package/dist/widgets/widget-app.d.ts +14 -4
  153. package/dist/widgets/widget-app.js +36 -7
  154. package/dist/widgets/widget-base.d.ts +2 -2
  155. package/dist/widgets/widget-base.js +29 -18
  156. package/package.json +8 -1
@@ -0,0 +1,237 @@
1
+ /**
2
+ * GlyphEffected — a renderable whose finished cells' glyphs pass through a
3
+ * glyph effect at a moment `t`: the glyph half of `Effected` (`./effect.ts`),
4
+ * built to its contract.
5
+ *
6
+ * A glyph effect is the colour effect's two-stage shape with a glyph where
7
+ * the colours were: for the cells an element draws in a frame, an
8
+ * `Animation` (`../core/animation.ts`) of what is done to them. The first
9
+ * stage is handed every cell once a frame (`GlyphCell`), blanks included —
10
+ * rain and stars live there — and whether the output draws ASCII alone
11
+ * (`DrawnGlyphs`), and works out what holds for the whole frame:
12
+ * how many cells are typed, where a band is. The second, the function
13
+ * `at(t)` returns, maps each cell to the text it shows. A typewriter, a
14
+ * scramble, a wave of case changes and particles on blank cells are all this
15
+ * one shape (`./glyph-effects.ts`) [LAW:one-type-per-behavior].
16
+ *
17
+ * Colour and glyph are not two kinds of animation: each is an `Animation` of
18
+ * a per-cell map, and they differ only in what a cell is mapped to. The
19
+ * wrappers are two because the stages are handed different facts. A colour
20
+ * effect is settled against the depth, the terminal and the colours of the
21
+ * words (`Drawn`), and with no colour output there is nothing for it to
22
+ * change. A glyph effect needs none of that, and is written at every depth,
23
+ * with no colour too. Over one element the two compose by nesting:
24
+ * `new Effected(new GlyphEffected(child, glyphs, …), colours, …)` moves the
25
+ * colours of the glyphs the glyph effect left, since each wrapper measures
26
+ * what its child drew.
27
+ *
28
+ * A `GlyphEffected` is the element under its glyph effect over time, an
29
+ * `Animation<Renderable>` placed as an `Effected` is, its value at `t` the
30
+ * element through the effect at `t` (`Reglyphed`).
31
+ * [LAW:no-ambient-temporal-coupling] The time is the frame's, never a clock
32
+ * read, so the same time draws the same bytes at any frame rate.
33
+ *
34
+ * What a glyph effect may not do is the contract this module holds:
35
+ *
36
+ * - It changes what a cell shows, never which cells exist, where they are or
37
+ * who drew them. The text it returns for a cell is exactly as many cells
38
+ * wide (`cellLen`), carries no line break or control character, and does
39
+ * not join the cell beside it into one grapheme cluster, or the render
40
+ * throws naming the cell [LAW:single-enforcer]. So what sits beside the
41
+ * element never moves, and a wide glyph is changed whole — blanked as two
42
+ * spaces, scrambled as two one-cell glyphs — never cut.
43
+ * - A segment whose every glyph comes back as it was is yielded as it was,
44
+ * so the identity effect is byte-identical. A changed segment keeps its
45
+ * style whole, link, meta and anchor included; no cell has moved, so the
46
+ * anchor's offsets are still true and hit testing still finds the owner.
47
+ * - Placed as a `GlyphEffected`, every cell carries the hold the frame is in
48
+ * (`Style.hold`), under whatever the child said,
49
+ * as under `Effected`; drawn as a `Reglyphed`, a value, none.
50
+ *
51
+ * Positions are the child's own: row and column count from the top-left of
52
+ * what the child drew.
53
+ */
54
+ import { Animation, frameTimeOf, holdOf, shownAt, stampFrame } from "../core/animation.js";
55
+ import { cellLen, graphemes } from "../core/cells.js";
56
+ import { Measurement } from "../core/measure.js";
57
+ import { render } from "../core/protocol.js";
58
+ import { Segment } from "../core/segment.js";
59
+ import { finiteTime } from "../core/easing.js";
60
+ import { stripControlChars } from "../core/text.js";
61
+ import { cellSeed, walkGlyphs } from "./effect.js";
62
+ import { pattern } from "../core/pattern.js";
63
+ /**
64
+ * A `GlyphEffected` at `t`, or an element through a glyph effect at a time an
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]
68
+ */
69
+ export class Reglyphed {
70
+ renderable;
71
+ effect;
72
+ t;
73
+ key;
74
+ constructor(renderable, effect, options) {
75
+ this.renderable = renderable;
76
+ this.effect = effect;
77
+ this.t = finiteTime("GlyphEffected", options.t);
78
+ this.key = options.key;
79
+ }
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
+ const segments = [...render(this.renderable, options)];
86
+ const { placed, cells } = placeGlyphs(segments, this.key);
87
+ const animation = this.effect({ cells, asciiOnly: options.asciiOnly === true });
88
+ const frame = shownAt(animation, this.t, options);
89
+ 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) };
92
+ }
93
+ measure(options) {
94
+ return Measurement.get(options, this.renderable);
95
+ }
96
+ }
97
+ /**
98
+ * `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.
102
+ */
103
+ export class GlyphEffected {
104
+ renderable;
105
+ effect;
106
+ key;
107
+ constructor(renderable, effect, options) {
108
+ this.renderable = renderable;
109
+ this.effect = effect;
110
+ this.key = options.key;
111
+ }
112
+ /** The element through the glyph effect at `t`, unstamped. */
113
+ at(t) {
114
+ return new Reglyphed(this.renderable, this.effect, { t, key: this.key });
115
+ }
116
+ nextChange(t) {
117
+ return t;
118
+ }
119
+ lastChange(t) {
120
+ return t;
121
+ }
122
+ /** The element under the glyph effect with its motion taken out. */
123
+ reduced() {
124
+ const { effect } = this;
125
+ return new GlyphEffected(this.renderable, (drawn) => effect(drawn).reduced(), this);
126
+ }
127
+ /** Drawn at the frame's time, its cells stamped with the glyph effect's exact hold there. */
128
+ render(options) {
129
+ const { segments, hold } = this.at(frameTimeOf(options)).moved(options);
130
+ return stampFrame(segments, hold);
131
+ }
132
+ measure(options) {
133
+ return Measurement.get(options, this.renderable);
134
+ }
135
+ }
136
+ /**
137
+ * Each segment's glyphs, each with its cell as the effect is handed it, and
138
+ * every cell in reading order. [LAW:one-source-of-truth] One walk, one count
139
+ * of reading order, for a rendered frame and a text alike.
140
+ */
141
+ function placeGlyphs(segments, key) {
142
+ let index = 0;
143
+ const placed = walkGlyphs(segments).map((glyphs) => glyphs.map(({ glyph, cell }) => ({ glyph, cell: cell && { ...cell, glyph, index: index++, seed: cellSeed(key, cell.row, cell.col) } })));
144
+ return { placed, cells: placed.flatMap((glyphs) => glyphs.flatMap(({ cell }) => (cell === undefined ? [] : [cell]))) };
145
+ }
146
+ /** What `frame` shows for each placed glyph, held to the contract (`shows`, `standsApart`). */
147
+ function showGlyphs(placed, frame) {
148
+ const shown = placed.map((glyphs) => glyphs.map(({ glyph, cell }) => (cell === undefined ? glyph : shows(frame, cell))));
149
+ standsApart(placed.flat(), shown.flat());
150
+ return shown;
151
+ }
152
+ /**
153
+ * `text` with its glyphs passed through `effect`, before any render: the
154
+ * glyph half of a template fragment (`../template-bindings/animation-funcs.ts`),
155
+ * which is spliced into a line and drawn under a theme it does not know yet,
156
+ * so it cannot be rendered to cells first as `Reglyphed` renders its child.
157
+ * The cells are the text's own lines, at its own columns; the contract is
158
+ * the one `Reglyphed` is held to, through the same checks, over the text's
159
+ * own glyphs: a neighbour spliced beside it later, a combining mark leading
160
+ * the next fragment say, is outside what either check can see. The text keeps
161
+ * its every style and span, each moved to where its glyphs now sit, and a
162
+ * frame that changes nothing hands the text back as it was. What it says of
163
+ * when it changes is the effect's, unstamped: whoever draws it at a time
164
+ * stamps it (`textFrameAt`).
165
+ */
166
+ export function reglyphText(text, effect, options) {
167
+ const { placed, cells } = placeGlyphs([new Segment(text.plain)], options.key);
168
+ return Animation.map(effect({ cells, asciiOnly: options.asciiOnly }), (frame) => rewritten(text, placed[0], showGlyphs(placed, frame)[0]));
169
+ }
170
+ /**
171
+ * `text` with each of its `glyphs` written as `shown` says, every span moved
172
+ * with the glyphs it covers. An offset inside a glyph — a span edge between
173
+ * a letter and its mark — lands as far into the written glyph as it can.
174
+ */
175
+ function rewritten(text, glyphs, shown) {
176
+ if (shown.every((s, i) => s === glyphs[i].glyph))
177
+ return text;
178
+ // Where each glyph starts, before and after, with one more entry for the end.
179
+ const from = [0];
180
+ const to = [0];
181
+ glyphs.forEach(({ glyph }, i) => {
182
+ from.push(from[i] + glyph.length);
183
+ to.push(to[i] + shown[i].length);
184
+ });
185
+ const moved = (offset) => {
186
+ let i = 0;
187
+ while (i + 1 < from.length && from[i + 1] <= offset)
188
+ i++;
189
+ return i === glyphs.length ? to[i] : to[i] + Math.min(offset - from[i], shown[i].length);
190
+ };
191
+ const result = text.blankCopy(shown.join(""));
192
+ for (const span of text.spans)
193
+ result.stylize(span.style, moved(span.start), moved(span.end));
194
+ return result;
195
+ }
196
+ /** Line breaks and tabs, which `stripControlChars` leaves for a text's own lines and which no cell may hold. */
197
+ const BREAKS = pattern(/[\n\t]/);
198
+ /**
199
+ * What `frame` shows in `cell`, held to the contract: as many cells wide as
200
+ * the glyph, with no line break or control character. [LAW:single-enforcer]
201
+ * The one place a glyph effect's output is checked; a breach throws, naming
202
+ * the cell and what it was turned into.
203
+ */
204
+ function shows(frame, cell) {
205
+ const text = frame(cell);
206
+ if (text === cell.glyph)
207
+ return text;
208
+ if (cellLen(text) !== cellLen(cell.glyph) || BREAKS.test(text) || stripControlChars(text) !== text) {
209
+ throw new RangeError(`a glyph effect turned ${JSON.stringify(cell.glyph)} at row ${cell.row}, column ${cell.col} into ${JSON.stringify(text)}: ` +
210
+ `it must be ${cellLen(cell.glyph)} cell(s) wide, with no line break or control character`);
211
+ }
212
+ return text;
213
+ }
214
+ /**
215
+ * Every text `frame` changed, in `shown`, stands apart from the texts beside
216
+ * it on its line: joined to them, it splits into as many grapheme clusters as
217
+ * it and they do alone, or this throws naming the cell. Each text being as
218
+ * wide as its glyph is `shows`'s check; this is the half no one cell can
219
+ * see — a trailing zero-width joiner that fuses with the next emoji, or a
220
+ * 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.
223
+ */
224
+ 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]);
227
+ shown.forEach((text, i) => {
228
+ const { glyph, cell } = placed[i];
229
+ if (cell === undefined || text === glyph)
230
+ 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) {
233
+ throw new RangeError(`a glyph effect turned ${JSON.stringify(glyph)} at row ${cell.row}, column ${cell.col} into ${JSON.stringify(text)}, ` +
234
+ `which joins the cell beside it into one character`);
235
+ }
236
+ });
237
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * The glyph effect catalogue: a typewriter and a scramble, which run once and
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`).
9
+ *
10
+ * Where a colour loop glides, a glyph cannot: a cell shows one glyph or
11
+ * another, so every loop here moves in steps, on a `beat`, and says when its
12
+ * next step is. The transitions are `Transition`s by the one rule the
13
+ * colour transitions follow (`transition`): they move every frame until
14
+ * their seconds are up, and never after.
15
+ *
16
+ * [LAW:no-ambient-temporal-coupling] No effect reads a clock, and what a cell
17
+ * shows at `t` is worked out from `t` alone, so a draw costs the same at
18
+ * `t = 0` and `t = 10⁶`, and a host drawing once a second and one drawing
19
+ * sixty times show the same cell at every `t` they share. What is random is
20
+ * `hash` (`../core/noise.ts`) of where a cell is and which step it is in.
21
+ */
22
+ import { type Timing } from "../core/easing.js";
23
+ import { type EffectTarget, type Transition } from "./effects.js";
24
+ import type { GlyphEffect } from "./glyph-effect.js";
25
+ /** A glyph effect that runs once, and says when it is done. */
26
+ export type GlyphTransition = Transition<GlyphEffect>;
27
+ /**
28
+ * A typewriter: the element's text blank until it is typed, in reading
29
+ * order, the last cell at `timing.seconds`. The ease shapes the pace, so
30
+ * `ease-out` types fast and slows to the end. The spaces between words are
31
+ * typed too, as a typewriter's are; the margins a renderable pads its text
32
+ * with are not text, so no second is spent on them.
33
+ */
34
+ export declare function typewriter(timing: Timing): GlyphTransition;
35
+ /**
36
+ * A scramble that resolves: a cell of the element's text shows glyphs drawn
37
+ * at random, a new one each flicker, until it resolves to its own, in reading
38
+ * order, the last at `timing.seconds`. A wide glyph scrambles as two one-cell
39
+ * glyphs. A blank stays blank, so the words keep their shape while their
40
+ * letters churn.
41
+ */
42
+ export declare function scramble(timing: Timing, z: number): GlyphTransition;
43
+ /**
44
+ * A typewriter backwards, for an element leaving: its text whole, then erased
45
+ * from its last cell to its first, the first at `timing.seconds`, so the
46
+ * element ends blank and nothing is left to vanish when it goes.
47
+ */
48
+ export declare function backspace(timing: Timing): GlyphTransition;
49
+ /**
50
+ * A scramble that decays, for an element leaving: its text whole, then each
51
+ * cell, in reading order, churning through random glyphs for a while before
52
+ * it is blank, the last blank at `timing.seconds`. A blank stays blank, as in
53
+ * `scramble`, so the words keep their shape until they are gone.
54
+ */
55
+ export declare function scrambleOut(timing: Timing, z: number): GlyphTransition;
56
+ /** How a case wave moves: a step, in seconds, for the band to move one column, and how many columns wide it is. */
57
+ export interface CaseWave {
58
+ readonly step: number;
59
+ readonly width: number;
60
+ }
61
+ /**
62
+ * A wave of case changes: a band `width` columns wide crosses the element
63
+ * left to right, one column a `step`, and every cell inside it shows its
64
+ * glyph's case swapped. It starts again once it has left the last column,
65
+ * so a pass takes the element's columns and the band's, a step each. `z`
66
+ * sets how far into its pass the band is at `t = 0`.
67
+ */
68
+ export declare function caseWave({ step, width }: CaseWave, z: number): GlyphEffect;
69
+ /** Particles over the blank cells of an element: what they look like, how many, and how they move. */
70
+ export interface Particles {
71
+ /** The glyphs a particle may be, each one cell wide. */
72
+ readonly glyphs: string;
73
+ /** What it may be on an output that draws ASCII alone, each one cell wide. */
74
+ readonly ascii: string;
75
+ /** The share of blank cells holding a particle at any step, in [0, 1]. */
76
+ readonly density: number;
77
+ /** How long a step holds, in seconds. */
78
+ readonly step: number;
79
+ /** How many rows every particle falls each step, a whole number, since a glyph moves a cell at a time; 0 holds them where they are. */
80
+ readonly drift: number;
81
+ /** How many steps a particle lasts before the field is drawn afresh; `Infinity`, never. */
82
+ readonly life: number;
83
+ }
84
+ /**
85
+ * Particles on the element's blank cells: rain falling, stars coming and
86
+ * going. A particle is never drawn over a glyph, so it passes behind the
87
+ * words. Which cells hold one is `hash` of the cell's column, where it was at
88
+ * the step's start (its row less what the field has fallen) and which life
89
+ * of the field this is, so a field that falls carries each particle down a
90
+ * row a step, and one that holds still draws a fresh field each life.
91
+ */
92
+ export declare function particles(spec: Particles, z: number): GlyphEffect;
93
+ /** The names of the glyph effects that loop. */
94
+ export type GlyphLoopName = "wave" | "rain" | "stars";
95
+ /** The names of the glyph effects that run once. */
96
+ export type GlyphTransitionName = "typewriter" | "scramble" | "backspace" | "scrambleOut";
97
+ /** Each catalogue glyph effect's defaults, as first drawn in `examples/glyph-effects`. */
98
+ export declare const GLYPH_DEFAULTS: {
99
+ readonly typewriter: {
100
+ readonly seconds: 2;
101
+ readonly ease: import("../core/easing.js").Ease;
102
+ };
103
+ readonly scramble: {
104
+ readonly seconds: 2.5;
105
+ readonly ease: import("../core/easing.js").Ease;
106
+ };
107
+ readonly backspace: {
108
+ readonly seconds: 1.5;
109
+ readonly ease: import("../core/easing.js").Ease;
110
+ };
111
+ readonly scrambleOut: {
112
+ readonly seconds: 2.5;
113
+ readonly ease: import("../core/easing.js").Ease;
114
+ };
115
+ readonly wave: {
116
+ readonly step: 0.06;
117
+ readonly width: 3;
118
+ };
119
+ readonly rain: {
120
+ readonly glyphs: "│╎";
121
+ readonly ascii: "|:";
122
+ readonly density: 0.06;
123
+ readonly step: 0.08;
124
+ readonly drift: 1;
125
+ readonly life: number;
126
+ };
127
+ readonly stars: {
128
+ readonly glyphs: "·+*";
129
+ readonly ascii: ".+*";
130
+ readonly density: 0.04;
131
+ readonly step: 1.5;
132
+ readonly drift: 0;
133
+ readonly life: 1;
134
+ };
135
+ };
136
+ /**
137
+ * Every glyph effect in the catalogue by name, each made the one way: for an
138
+ * element, at `z` in the noise so two elements under one effect do not move
139
+ * in lockstep, at its defaults. A transition takes another timing in place
140
+ * of its own. [LAW:types-are-the-program] Keyed by the two name types, as
141
+ * `EFFECT_CATALOGUE` is, so a picker walking a section's names offers an
142
+ * effect added here with no change of its own.
143
+ */
144
+ export declare const GLYPH_EFFECT_CATALOGUE: {
145
+ readonly loops: Readonly<Record<GlyphLoopName, (element: EffectTarget) => GlyphEffect>>;
146
+ readonly transitions: Readonly<Record<GlyphTransitionName, (element: EffectTarget, timing?: Timing) => GlyphTransition>>;
147
+ };
@@ -0,0 +1,283 @@
1
+ /**
2
+ * The glyph effect catalogue: a typewriter and a scramble, which run once and
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`).
9
+ *
10
+ * Where a colour loop glides, a glyph cannot: a cell shows one glyph or
11
+ * another, so every loop here moves in steps, on a `beat`, and says when its
12
+ * next step is. The transitions are `Transition`s by the one rule the
13
+ * colour transitions follow (`transition`): they move every frame until
14
+ * their seconds are up, and never after.
15
+ *
16
+ * [LAW:no-ambient-temporal-coupling] No effect reads a clock, and what a cell
17
+ * shows at `t` is worked out from `t` alone, so a draw costs the same at
18
+ * `t = 0` and `t = 10⁶`, and a host drawing once a second and one drawing
19
+ * sixty times show the same cell at every `t` they share. What is random is
20
+ * `hash` (`../core/noise.ts`) of where a cell is and which step it is in.
21
+ */
22
+ import { Animation } from "../core/animation.js";
23
+ import { cellLen, graphemes } from "../core/cells.js";
24
+ import { EASES, Phase, beat, positiveSeconds, settled } from "../core/easing.js";
25
+ import { hash } from "../core/noise.js";
26
+ import { drawable, isAscii } from "../core/protocol.js";
27
+ import { inked } from "./effect.js";
28
+ import { requireZ, transition } from "./effects.js";
29
+ /** `glyph` as blank: as many spaces as it is cells wide. */
30
+ function blank(glyph) {
31
+ return " ".repeat(cellLen(glyph));
32
+ }
33
+ /**
34
+ * The text among `cells`: each row from its first inked glyph to its last,
35
+ * the blanks between words in it and the margins round it not, as a
36
+ * typewriter types and not as a renderable pads. A glyph no cell wide — a
37
+ * byte-order mark, a mark a child split from its letter — takes no time and
38
+ * is no place in it. Each cell's place, in reading order, by `index`, and
39
+ * how many places there are.
40
+ */
41
+ function text(cells) {
42
+ const ends = new Map();
43
+ for (const cell of cells.filter((c) => inked(c.glyph)))
44
+ ends.set(cell.row, { first: ends.get(cell.row)?.first ?? cell.index, last: cell.index });
45
+ const place = new Map();
46
+ for (const cell of cells) {
47
+ const row = ends.get(cell.row);
48
+ if (row !== undefined && cell.index >= row.first && cell.index <= row.last && cellLen(cell.glyph) > 0)
49
+ place.set(cell.index, place.size);
50
+ }
51
+ return { length: place.size, place };
52
+ }
53
+ /** Of `n` cells, how many have arrived at eased progress `p`, an ease that overshoots held to the cells there are. */
54
+ function arrived(n, p) {
55
+ return Math.min(n, Math.max(0, Math.floor(n * p)));
56
+ }
57
+ /**
58
+ * Steps of `seconds`, counted from 0 on the animation's own clock: for a
59
+ * `frame` of each step, the animation holding it through the step and
60
+ * changing at its end, and reduced, `still`. [LAW:one-source-of-truth] The step and its two ends
61
+ * are a `beat`'s, the one timing vocabulary, so a step boundary is the moment
62
+ * the change shows. The beat is made once, for every frame drawn on it.
63
+ */
64
+ function stepped(seconds) {
65
+ const pace = beat([seconds]);
66
+ return (frame, still) => Animation.of({
67
+ at: (t) => frame(Math.round(pace.lastChange(t) / seconds)),
68
+ nextChange: (t) => pace.nextChange(t),
69
+ lastChange: (t) => pace.lastChange(t),
70
+ reduced: () => Animation.constant(still),
71
+ });
72
+ }
73
+ /** Every cell its own glyph: a glyph loop with its motion taken out — the text as written, no wave, no rain. */
74
+ const AS_WRITTEN = (cell) => cell.glyph;
75
+ /**
76
+ * A typewriter: the element's text blank until it is typed, in reading
77
+ * order, the last cell at `timing.seconds`. The ease shapes the pace, so
78
+ * `ease-out` types fast and slows to the end. The spaces between words are
79
+ * typed too, as a typewriter's are; the margins a renderable pads its text
80
+ * with are not text, so no second is spent on them.
81
+ */
82
+ export function typewriter(timing) {
83
+ const phase = Phase.once(0, timing.seconds);
84
+ // Settled, so the last cell is typed at the seconds whatever ease was handed.
85
+ const ease = settled(timing.ease);
86
+ return transition(timing.seconds, ({ cells }) => {
87
+ const { length, place } = text(cells);
88
+ return Animation.continuous((t) => {
89
+ const typed = arrived(length, ease(phase(t)));
90
+ // A margin has no place, and is blank already.
91
+ return (cell) => ((place.get(cell.index) ?? -1) < typed ? cell.glyph : blank(cell.glyph));
92
+ });
93
+ });
94
+ }
95
+ /** What a scrambled cell shows: one-cell glyphs, none that reads as a blank. */
96
+ const SCRAMBLE_GLYPHS = [..."ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789#$%&*+<=>?@"];
97
+ /** How long a scrambled glyph holds before the next, in seconds. */
98
+ const FLICKER = 1 / 15;
99
+ /**
100
+ * A scramble that resolves: a cell of the element's text shows glyphs drawn
101
+ * at random, a new one each flicker, until it resolves to its own, in reading
102
+ * order, the last at `timing.seconds`. A wide glyph scrambles as two one-cell
103
+ * glyphs. A blank stays blank, so the words keep their shape while their
104
+ * letters churn.
105
+ */
106
+ export function scramble(timing, z) {
107
+ requireZ("scramble", z);
108
+ const phase = Phase.once(0, timing.seconds);
109
+ // Settled, so the last cell resolves at the seconds whatever ease was handed.
110
+ const ease = settled(timing.ease);
111
+ const flicker = stepped(FLICKER)((step) => step, 0);
112
+ return transition(timing.seconds, ({ cells }) => {
113
+ const { length, place } = text(cells);
114
+ return Animation.continuous((t) => {
115
+ const resolved = arrived(length, ease(phase(t)));
116
+ const step = flicker.at(t);
117
+ return (cell) => (!inked(cell.glyph) || (place.get(cell.index) ?? -1) < resolved ? cell.glyph : churned(cell, z, step));
118
+ });
119
+ });
120
+ }
121
+ /** `cell` scrambled at flicker `step`: a random one-cell glyph for each cell it is wide. */
122
+ function churned(cell, z, step) {
123
+ const pick = (i) => SCRAMBLE_GLYPHS[Math.floor(hash(cell.seed * 7919 + i, z + step * 0.618) * SCRAMBLE_GLYPHS.length)];
124
+ return Array.from({ length: cellLen(cell.glyph) }, (_, i) => pick(i)).join("");
125
+ }
126
+ /**
127
+ * A typewriter backwards, for an element leaving: its text whole, then erased
128
+ * from its last cell to its first, the first at `timing.seconds`, so the
129
+ * element ends blank and nothing is left to vanish when it goes.
130
+ */
131
+ export function backspace(timing) {
132
+ const phase = Phase.once(0, timing.seconds);
133
+ // Settled, so the first cell is erased at the seconds whatever ease was handed.
134
+ const ease = settled(timing.ease);
135
+ return transition(timing.seconds, ({ cells }) => {
136
+ const { length, place } = text(cells);
137
+ return Animation.continuous((t) => {
138
+ const kept = length - arrived(length, ease(phase(t)));
139
+ // A margin has no place, and is blank already.
140
+ return (cell) => ((place.get(cell.index) ?? -1) < kept ? cell.glyph : blank(cell.glyph));
141
+ });
142
+ });
143
+ }
144
+ /** Of a `scrambleOut`'s seconds, the share each cell churns for between its own glyph and its blank. */
145
+ const CHURN = 0.3;
146
+ /**
147
+ * A scramble that decays, for an element leaving: its text whole, then each
148
+ * cell, in reading order, churning through random glyphs for a while before
149
+ * it is blank, the last blank at `timing.seconds`. A blank stays blank, as in
150
+ * `scramble`, so the words keep their shape until they are gone.
151
+ */
152
+ export function scrambleOut(timing, z) {
153
+ requireZ("scrambleOut", z);
154
+ const phase = Phase.once(0, timing.seconds);
155
+ // Settled, so the last cell is blank at the seconds whatever ease was handed.
156
+ const ease = settled(timing.ease);
157
+ const flicker = stepped(FLICKER)((step) => step, 0);
158
+ return transition(timing.seconds, ({ cells }) => {
159
+ const { length, place } = text(cells);
160
+ return Animation.continuous((t) => {
161
+ const p = ease(phase(t));
162
+ // Two fronts in reading order, a churn's share of the seconds apart: past the first a cell churns, past the second it is blank.
163
+ const churning = arrived(length, p / (1 - CHURN));
164
+ const gone = arrived(length, (p - CHURN) / (1 - CHURN));
165
+ const step = flicker.at(t);
166
+ return (cell) => {
167
+ // A margin or a glyph no cell wide has no place, and neither front reaches it.
168
+ const at = place.get(cell.index) ?? Number.POSITIVE_INFINITY;
169
+ if (at < gone)
170
+ return blank(cell.glyph);
171
+ return !inked(cell.glyph) || at >= churning ? cell.glyph : churned(cell, z, step);
172
+ };
173
+ });
174
+ });
175
+ }
176
+ /**
177
+ * `glyph` with its case swapped, or as it is if the swap would change how
178
+ * wide it is (`ß` upper-cases to `SS`), since a cell never changes width.
179
+ */
180
+ function swapCase(glyph) {
181
+ const swapped = [...glyph].map((ch) => (ch === ch.toUpperCase() ? ch.toLowerCase() : ch.toUpperCase())).join("");
182
+ return cellLen(swapped) === cellLen(glyph) ? swapped : glyph;
183
+ }
184
+ /**
185
+ * A wave of case changes: a band `width` columns wide crosses the element
186
+ * left to right, one column a `step`, and every cell inside it shows its
187
+ * glyph's case swapped. It starts again once it has left the last column,
188
+ * so a pass takes the element's columns and the band's, a step each. `z`
189
+ * sets how far into its pass the band is at `t = 0`.
190
+ */
191
+ export function caseWave({ step, width }, z) {
192
+ positiveSeconds("caseWave step", step);
193
+ if (!(Number.isInteger(width) && width > 0))
194
+ throw new RangeError(`caseWave width must be a positive whole number of columns, got ${width}`);
195
+ requireZ("caseWave", z);
196
+ const steps = stepped(step);
197
+ return ({ cells }) => {
198
+ const columns = cells.reduce((most, cell) => Math.max(most, cell.col + cellLen(cell.glyph)), 0);
199
+ const span = columns + width;
200
+ const start = Math.floor(hash(z, 2.3) * span);
201
+ return steps((s) => {
202
+ // The column just past the band's right edge; it enters at the left.
203
+ const lead = (((s + start) % span) + span) % span;
204
+ // A wide glyph is in the band if any of its columns is.
205
+ return (cell) => (cell.col < lead && cell.col + cellLen(cell.glyph) > lead - width ? swapCase(cell.glyph) : cell.glyph);
206
+ }, AS_WRITTEN);
207
+ };
208
+ }
209
+ /**
210
+ * Particles on the element's blank cells: rain falling, stars coming and
211
+ * going. A particle is never drawn over a glyph, so it passes behind the
212
+ * words. Which cells hold one is `hash` of the cell's column, where it was at
213
+ * the step's start (its row less what the field has fallen) and which life
214
+ * of the field this is, so a field that falls carries each particle down a
215
+ * row a step, and one that holds still draws a fresh field each life.
216
+ */
217
+ export function particles(spec, z) {
218
+ const oneCell = (set) => {
219
+ const glyphs = graphemes(set);
220
+ if (glyphs.length === 0 || glyphs.some((glyph) => cellLen(glyph) !== 1))
221
+ throw new RangeError(`particles needs one-cell glyphs, got ${JSON.stringify(set)}`);
222
+ return glyphs;
223
+ };
224
+ const [glyphs, ascii] = [oneCell(spec.glyphs), oneCell(spec.ascii)];
225
+ if (!isAscii(spec.ascii))
226
+ throw new RangeError(`particles ascii must be ASCII, got ${JSON.stringify(spec.ascii)}`);
227
+ if (!(spec.density >= 0 && spec.density <= 1))
228
+ throw new RangeError(`particles density must be in [0, 1], got ${spec.density}`);
229
+ if (!Number.isInteger(spec.drift))
230
+ throw new RangeError(`particles drift must be a whole number of rows, got ${spec.drift}`);
231
+ if (!(spec.life > 0))
232
+ throw new RangeError(`particles life must be a positive number of steps, got ${spec.life}`);
233
+ positiveSeconds("particles step", spec.step);
234
+ requireZ("particles", z);
235
+ const steps = stepped(spec.step);
236
+ return ({ asciiOnly }) => {
237
+ const drawn = drawable({ asciiOnly }, glyphs, ascii, (set) => set.join(""));
238
+ return steps((s) => {
239
+ const life = Number.isFinite(spec.life) ? Math.floor(s / spec.life) : 0;
240
+ const fallen = spec.drift * s;
241
+ return (cell) => {
242
+ // A particle takes a blank cell, never ink, nor a glyph no cell wide.
243
+ if (inked(cell.glyph) || cellLen(cell.glyph) === 0)
244
+ return cell.glyph;
245
+ const from = cell.row - fallen;
246
+ const field = z + 1.37 * cell.col + 0.71 * life;
247
+ // A particle is one cell; a wide blank keeps the rest of its width blank.
248
+ return hash(from, field) < spec.density ? drawn[Math.floor(hash(from, field + 3.1) * drawn.length)] + blank(cell.glyph).slice(1) : cell.glyph;
249
+ };
250
+ }, AS_WRITTEN);
251
+ };
252
+ }
253
+ /** Each catalogue glyph effect's defaults, as first drawn in `examples/glyph-effects`. */
254
+ export const GLYPH_DEFAULTS = {
255
+ typewriter: { seconds: 2, ease: EASES.linear },
256
+ scramble: { seconds: 2.5, ease: EASES["ease-in"] },
257
+ backspace: { seconds: 1.5, ease: EASES.linear },
258
+ scrambleOut: { seconds: 2.5, ease: EASES["ease-out"] },
259
+ wave: { step: 0.06, width: 3 },
260
+ rain: { glyphs: "│╎", ascii: "|:", density: 0.06, step: 0.08, drift: 1, life: Number.POSITIVE_INFINITY },
261
+ stars: { glyphs: "·+*", ascii: ".+*", density: 0.04, step: 1.5, drift: 0, life: 1 },
262
+ };
263
+ /**
264
+ * Every glyph effect in the catalogue by name, each made the one way: for an
265
+ * element, at `z` in the noise so two elements under one effect do not move
266
+ * in lockstep, at its defaults. A transition takes another timing in place
267
+ * of its own. [LAW:types-are-the-program] Keyed by the two name types, as
268
+ * `EFFECT_CATALOGUE` is, so a picker walking a section's names offers an
269
+ * effect added here with no change of its own.
270
+ */
271
+ export const GLYPH_EFFECT_CATALOGUE = {
272
+ loops: {
273
+ wave: (element) => caseWave(GLYPH_DEFAULTS.wave, element.z),
274
+ rain: (element) => particles(GLYPH_DEFAULTS.rain, element.z),
275
+ stars: (element) => particles(GLYPH_DEFAULTS.stars, element.z),
276
+ },
277
+ transitions: {
278
+ typewriter: (_element, timing = GLYPH_DEFAULTS.typewriter) => typewriter(timing),
279
+ scramble: (element, timing = GLYPH_DEFAULTS.scramble) => scramble(timing, element.z),
280
+ backspace: (_element, timing = GLYPH_DEFAULTS.backspace) => backspace(timing),
281
+ scrambleOut: (element, timing = GLYPH_DEFAULTS.scrambleOut) => scrambleOut(timing, element.z),
282
+ },
283
+ };