@promptctl/rich-js 0.18.1 → 0.20.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 (169) hide show
  1. package/README.md +11 -8
  2. package/dist/core/box.d.ts +3 -1
  3. package/dist/core/box.js +6 -13
  4. package/dist/core/cells.d.ts +61 -28
  5. package/dist/core/cells.js +171 -124
  6. package/dist/core/clock.d.ts +48 -0
  7. package/dist/core/clock.js +42 -0
  8. package/dist/core/color.d.ts +99 -18
  9. package/dist/core/color.js +334 -146
  10. package/dist/core/console.d.ts +38 -7
  11. package/dist/core/console.js +159 -90
  12. package/dist/core/easing.d.ts +86 -0
  13. package/dist/core/easing.js +171 -0
  14. package/dist/core/env.d.ts +16 -0
  15. package/dist/core/env.js +16 -0
  16. package/dist/core/export-html.js +1 -4
  17. package/dist/core/export-lines.d.ts +22 -13
  18. package/dist/core/export-lines.js +37 -19
  19. package/dist/core/export-svg.d.ts +49 -0
  20. package/dist/core/export-svg.js +213 -0
  21. package/dist/core/fnv1a.d.ts +10 -0
  22. package/dist/core/fnv1a.js +17 -0
  23. package/dist/core/highlighter.d.ts +2 -2
  24. package/dist/core/highlighter.js +113 -47
  25. package/dist/core/json.js +1 -1
  26. package/dist/core/markup.d.ts +28 -7
  27. package/dist/core/markup.js +235 -312
  28. package/dist/core/measure.d.ts +3 -3
  29. package/dist/core/measure.js +12 -7
  30. package/dist/core/memo.d.ts +19 -0
  31. package/dist/core/memo.js +34 -0
  32. package/dist/core/noise.d.ts +31 -0
  33. package/dist/core/noise.js +94 -0
  34. package/dist/core/osc8.d.ts +6 -6
  35. package/dist/core/osc8.js +9 -21
  36. package/dist/core/paint.d.ts +107 -0
  37. package/dist/core/paint.js +161 -0
  38. package/dist/core/place.d.ts +1 -1
  39. package/dist/core/place.js +4 -6
  40. package/dist/core/pretty.d.ts +126 -2
  41. package/dist/core/pretty.js +571 -105
  42. package/dist/core/protocol.d.ts +43 -3
  43. package/dist/core/protocol.js +23 -2
  44. package/dist/core/render.d.ts +33 -30
  45. package/dist/core/render.js +43 -83
  46. package/dist/core/segment.d.ts +6 -5
  47. package/dist/core/segment.js +11 -15
  48. package/dist/core/selection.d.ts +12 -0
  49. package/dist/core/selection.js +14 -0
  50. package/dist/core/strip.d.ts +116 -74
  51. package/dist/core/strip.js +266 -217
  52. package/dist/core/style.d.ts +27 -7
  53. package/dist/core/style.js +106 -41
  54. package/dist/core/text.d.ts +43 -16
  55. package/dist/core/text.js +180 -59
  56. package/dist/core/wrap.d.ts +5 -0
  57. package/dist/core/wrap.js +16 -3
  58. package/dist/host/app.d.ts +95 -17
  59. package/dist/host/app.js +201 -61
  60. package/dist/host/index.d.ts +4 -1
  61. package/dist/host/index.js +1 -0
  62. package/dist/host/input.d.ts +84 -0
  63. package/dist/host/input.js +498 -0
  64. package/dist/host/terminal-host.d.ts +15 -2
  65. package/dist/host/terminal-host.js +2 -0
  66. package/dist/index.d.ts +26 -14
  67. package/dist/index.js +17 -7
  68. package/dist/node/prompt.d.ts +13 -0
  69. package/dist/node/prompt.js +30 -9
  70. package/dist/node/save.d.ts +4 -3
  71. package/dist/node/save.js +6 -3
  72. package/dist/node/terminal-host.d.ts +10 -2
  73. package/dist/node/terminal-host.js +7 -0
  74. package/dist/node/traceback.d.ts +2 -1
  75. package/dist/node/traceback.js +3 -39
  76. package/dist/renderables/align.js +2 -9
  77. package/dist/renderables/columns.d.ts +45 -29
  78. package/dist/renderables/columns.js +142 -103
  79. package/dist/renderables/constrain.js +6 -12
  80. package/dist/renderables/effect.d.ts +113 -0
  81. package/dist/renderables/effect.js +182 -0
  82. package/dist/renderables/effects.d.ts +252 -0
  83. package/dist/renderables/effects.js +565 -0
  84. package/dist/renderables/embed.d.ts +94 -31
  85. package/dist/renderables/embed.js +141 -45
  86. package/dist/renderables/group.d.ts +6 -3
  87. package/dist/renderables/group.js +5 -0
  88. package/dist/renderables/layout.d.ts +6 -23
  89. package/dist/renderables/layout.js +24 -113
  90. package/dist/renderables/list-view.d.ts +42 -0
  91. package/dist/renderables/list-view.js +78 -0
  92. package/dist/renderables/live.d.ts +40 -16
  93. package/dist/renderables/live.js +96 -83
  94. package/dist/renderables/markdown.d.ts +16 -2
  95. package/dist/renderables/markdown.js +521 -118
  96. package/dist/renderables/padding.d.ts +22 -1
  97. package/dist/renderables/padding.js +30 -24
  98. package/dist/renderables/panel.d.ts +31 -33
  99. package/dist/renderables/panel.js +137 -141
  100. package/dist/renderables/progress.d.ts +110 -16
  101. package/dist/renderables/progress.js +212 -81
  102. package/dist/renderables/progressBar.d.ts +24 -4
  103. package/dist/renderables/progressBar.js +78 -23
  104. package/dist/renderables/prompt.d.ts +37 -15
  105. package/dist/renderables/prompt.js +125 -85
  106. package/dist/renderables/ratio.d.ts +123 -0
  107. package/dist/renderables/ratio.js +213 -0
  108. package/dist/renderables/rule.d.ts +3 -2
  109. package/dist/renderables/rule.js +50 -52
  110. package/dist/renderables/spinner.d.ts +28 -2
  111. package/dist/renderables/spinner.js +52 -19
  112. package/dist/renderables/status.d.ts +13 -7
  113. package/dist/renderables/status.js +15 -29
  114. package/dist/renderables/syntax.d.ts +37 -7
  115. package/dist/renderables/syntax.js +134 -83
  116. package/dist/renderables/table.d.ts +58 -20
  117. package/dist/renderables/table.js +373 -197
  118. package/dist/renderables/traceback.d.ts +5 -4
  119. package/dist/renderables/traceback.js +239 -68
  120. package/dist/renderables/tree.d.ts +10 -2
  121. package/dist/renderables/tree.js +61 -34
  122. package/dist/renderables/viewport.d.ts +30 -0
  123. package/dist/renderables/viewport.js +61 -7
  124. package/dist/template-bindings/color-funcs.d.ts +31 -9
  125. package/dist/template-bindings/color-funcs.js +40 -14
  126. package/dist/template-bindings/helpers.d.ts +4 -2
  127. package/dist/template-bindings/helpers.js +5 -20
  128. package/dist/template-bindings/index.d.ts +29 -17
  129. package/dist/template-bindings/index.js +36 -20
  130. package/dist/template-bindings/palette-funcs.d.ts +3 -1
  131. package/dist/template-bindings/palette-funcs.js +11 -7
  132. package/dist/template-bindings/style-funcs.js +4 -2
  133. package/dist/themes/buildPalette.d.ts +24 -3
  134. package/dist/themes/buildPalette.js +52 -8
  135. package/dist/themes/colorMath.d.ts +29 -17
  136. package/dist/themes/colorMath.js +74 -48
  137. package/dist/themes/colorRef.d.ts +2 -17
  138. package/dist/themes/colorRef.js +13 -31
  139. package/dist/themes/palette.d.ts +24 -0
  140. package/dist/themes/palette.js +46 -3
  141. package/dist/themes/ramp.d.ts +21 -34
  142. package/dist/themes/ramp.js +23 -42
  143. package/dist/themes/registry.js +39 -28
  144. package/dist/widgets/button.d.ts +2 -6
  145. package/dist/widgets/button.js +9 -40
  146. package/dist/widgets/checkbox.d.ts +2 -5
  147. package/dist/widgets/checkbox.js +9 -31
  148. package/dist/widgets/dropdown.d.ts +2 -5
  149. package/dist/widgets/dropdown.js +17 -51
  150. package/dist/widgets/event-router.d.ts +31 -40
  151. package/dist/widgets/event-router.js +52 -483
  152. package/dist/widgets/index.d.ts +2 -1
  153. package/dist/widgets/ink.d.ts +22 -0
  154. package/dist/widgets/ink.js +31 -0
  155. package/dist/widgets/slider.d.ts +2 -5
  156. package/dist/widgets/slider.js +11 -34
  157. package/dist/widgets/text-input.d.ts +19 -10
  158. package/dist/widgets/text-input.js +156 -147
  159. package/dist/widgets/themed-widget.d.ts +20 -0
  160. package/dist/widgets/themed-widget.js +89 -0
  161. package/dist/widgets/toggle.d.ts +2 -5
  162. package/dist/widgets/toggle.js +8 -31
  163. package/dist/widgets/types.d.ts +8 -25
  164. package/dist/widgets/types.js +8 -0
  165. package/dist/widgets/widget-app.d.ts +24 -7
  166. package/dist/widgets/widget-app.js +35 -18
  167. package/dist/widgets/widget-base.d.ts +2 -0
  168. package/dist/widgets/widget-base.js +9 -2
  169. package/package.json +31 -17
@@ -0,0 +1,565 @@
1
+ /**
2
+ * The effect catalogue: shimmer, pulse, sparkle and wheel, which loop for as
3
+ * long as they are drawn, and fade-in and dissolve-out, which run once and
4
+ * are done. Each is an `Effect` for `Effected` (`./effect.ts`); the defaults
5
+ * in `EFFECT_CURVES` are the ones signed off by eye in the effects-feel demo
6
+ * (`npm run effects-feel`), at 30 fps and at one frame a second.
7
+ *
8
+ * Each effect is something a first-time observer already knows from life —
9
+ * a slow glow, sunlight moving on water, the colours of daylight, fireflies, ink
10
+ * blooming in water, mist lifting — because a motion the eye recognises
11
+ * reads as calm and alive, where a motion it has to learn reads as a widget.
12
+ * What makes those motions familiar is what every curve here keeps:
13
+ *
14
+ * - Nothing jumps. Every curve is continuous in time, built from smooth
15
+ * noise (`core/noise.ts`) and smooth waveforms, so at one frame a second a cell
16
+ * moves a little and at thirty it glides.
17
+ * - Nothing repeats exactly. Rhythms are perturbed by slow noise, and each
18
+ * breath, firefly's flash and pass of light is an event of its own, with
19
+ * its own strength and moment, the way no breath or gust is the last one
20
+ * again. A weaker event is a shorter one, so its strength costs nothing
21
+ * in smoothness.
22
+ * - Neighbours move together. Variation is spatially coherent — patches,
23
+ * filaments, fronts — never per-cell static.
24
+ * - Light only ever lights, and never takes the words away. Ink and the
25
+ * fill under it both move toward the light and never darken, each by its
26
+ * own share: the share a colour can take while every cell it is drawn in
27
+ * still reads at its resting contrast or WCAG AA, whichever is lower. A
28
+ * cell with contrast to spare glows brightly; one with none barely moves.
29
+ * - Elements are apart. Each sits at its own `z` in the noise, so two
30
+ * elements under one effect do not move in lockstep.
31
+ *
32
+ * Colour moves are in OKLCH, where equal steps look equal. A curve's `ease`
33
+ * maps its intensity in [0, 1] — how strongly the effect acts on a cell at a
34
+ * moment — onto its swing, for the loops and the transitions alike.
35
+ *
36
+ * [LAW:no-ambient-temporal-coupling] No curve reads a clock; `t` is the
37
+ * effect's argument, so the frame owner decides when to sample.
38
+ *
39
+ * Every import below names something the package exports, but for `noise`,
40
+ * which the docs' effects playground page supplies itself: each of its
41
+ * cards runs this file's own declarations as the program a visitor edits
42
+ * (`examples/effects-playground/programs.ts`), on the published library.
43
+ */
44
+ import { ColorRgba, contrastRatio } from "../core/color.js";
45
+ import { EASES, Phase } from "../core/easing.js";
46
+ import { fbm, hash, noise, smoothstep } from "../core/noise.js";
47
+ import { IDENTITY, Oklch } from "../core/oklch.js";
48
+ /** The contrast no touch takes a cell below, unless it rested below it: WCAG AA for body text. */
49
+ const LEGIBLE = 4.5;
50
+ /**
51
+ * The strengths a cell is drawn at: a field is snapped to one of these, and
52
+ * a share is checked at every one, so every strength drawn was checked. A
53
+ * 256-colour cube colour changes in jumps between strengths, and no check of
54
+ * a continuous strength between two of them could see every jump. A step is
55
+ * a 48th of a swing, well under a just-noticeable difference.
56
+ */
57
+ const LEVELS = 48;
58
+ const STRENGTHS = Array.from({ length: LEVELS }, (_, i) => (i + 1) / LEVELS);
59
+ /** What 8-bit rounding of an OKLCH round trip can cost a contrast ratio. */
60
+ const ROUNDING = 0.01;
61
+ /**
62
+ * How much of `touch` each of `colors` (hex) can take: the largest share in
63
+ * [0, 1] at which every pair in `pairs` that shows it, touched at the same
64
+ * strength, still reads at its resting contrast or `LEGIBLE`, whichever is
65
+ * lower. A colour in a pair but not in `colors` takes none, and so does one
66
+ * the terminal supplies, whatever its hex, as in `onColors`. One of `colors`
67
+ * no pair writes has nothing to be measured against, and throws.
68
+ *
69
+ * Colours are settled lightest first, each against every partner: one
70
+ * already settled at its share, one not yet settled as it is. So the lighter
71
+ * colour of a cell takes the light first and its darker partner spends
72
+ * whatever contrast is left — pale lettering lifting off a dark fill gives
73
+ * the fill room to glow — and a darker colour left as it is always holds,
74
+ * because its lighter partner was settled against exactly that.
75
+ */
76
+ export function shares(pairs, colors, touch) {
77
+ // [LAW:one-source-of-truth] A slot shows a subject colour, by hex, only
78
+ // where the element writes it: the reading `onColors` lights by.
79
+ const sides = pairs.map(({ fg, bg, terminal }) => [
80
+ { color: fg, own: terminal.fg ? undefined : fg.hex },
81
+ { color: bg, own: terminal.bg ? undefined : bg.hex },
82
+ ]);
83
+ const rgba = new Map(sides.flat().flatMap(({ color, own }) => (own === undefined ? [] : [[own, color]])));
84
+ const unpaired = [...colors].filter((hex) => !rgba.has(hex));
85
+ if (unpaired.length > 0)
86
+ throw new RangeError(`shares: ${unpaired.join(", ")} written in no pair, so nothing to keep legible against`);
87
+ const lightness = (hex) => Oklch.fromRgba(rgba.get(hex)).l;
88
+ const settled = new Map();
89
+ for (const hex of [...colors].sort((a, b) => lightness(b) - lightness(a))) {
90
+ const partners = sides.flatMap(([fg, bg]) => {
91
+ const partner = fg.own === hex ? bg : bg.own === hex ? fg : undefined;
92
+ return partner === undefined ? [] : [[partner.color, partner.own === undefined ? 0 : (settled.get(partner.own) ?? 0)]];
93
+ });
94
+ const color = rgba.get(hex);
95
+ const holds = (share) => partners.every(([partner, theirs]) => {
96
+ const floor = Math.min(contrastRatio(color, partner), LEGIBLE);
97
+ return STRENGTHS.every((w) => contrastRatio(touchedAt(touch, color, share * w), touchedAt(touch, partner, theirs * w)) >= floor - ROUNDING);
98
+ });
99
+ let [lo, hi] = holds(1) ? [1, 1] : [0, 1];
100
+ for (let i = 0; i < 20 && hi - lo > 1e-3; i++) {
101
+ const mid = (lo + hi) / 2;
102
+ [lo, hi] = holds(mid) ? [mid, hi] : [lo, mid];
103
+ }
104
+ settled.set(hex, lo);
105
+ }
106
+ return settled;
107
+ }
108
+ /**
109
+ * A loop as an `Effect` on a subject's own colours, by hex, wherever a cell
110
+ * shows them, each touched at its share of the cell's strength. A powerline
111
+ * strip's fill is the ground of its cells and the ink of the seam glyphs
112
+ * between them, so the arrow moves with the cell it points out of. A colour
113
+ * the terminal supplies (`EffectCell.terminal`) is not the subject's, whatever
114
+ * its hex, and takes no share.
115
+ */
116
+ export function onColors(share, loop) {
117
+ return ({ fg, bg }, cell, t) => {
118
+ const w = Math.round(clamp01(loop.field(cell, t)) * LEVELS) / LEVELS;
119
+ const moved = (color, terminal) => touchedAt(loop.touch, color, (terminal ? 0 : (share.get(color.hex) ?? 0)) * w);
120
+ return { fg: moved(fg, cell.terminal.fg), bg: moved(bg, cell.terminal.bg) };
121
+ };
122
+ }
123
+ /**
124
+ * `color` under `touch` at strength `w`, and at strength 0 the colour itself,
125
+ * exactly: an OKLCH round trip can land a step off, and at 16 colours a step
126
+ * off is another slot.
127
+ */
128
+ function touchedAt(touch, color, w) {
129
+ return w === 0 ? color : touch(color, w);
130
+ }
131
+ /** Throws, naming the field, unless `curve` has a positive, finite `seconds` and a `swing` in [0, 1]. */
132
+ function requireCurve(curve) {
133
+ if (!(Number.isFinite(curve.seconds) && curve.seconds > 0))
134
+ throw new RangeError(`curve: seconds must be a positive number of seconds, got ${curve.seconds}`);
135
+ if (!(curve.swing >= 0 && curve.swing <= 1))
136
+ throw new RangeError(`curve: swing must be in [0, 1], got ${curve.swing}`);
137
+ }
138
+ /**
139
+ * Each effect's curve as it was signed off. Every loop's period is a prime
140
+ * number of seconds, so no two loops come back into step within a sitting.
141
+ * The periods are long on purpose: at one frame a second a loop's colour
142
+ * moves by no more than a just-noticeable step from one frame to the next.
143
+ */
144
+ export const EFFECT_CURVES = {
145
+ shimmer: { seconds: 109, ease: EASES.linear, swing: 0.9 },
146
+ pulse: { seconds: 43, ease: EASES.linear, swing: 0.85 },
147
+ sparkle: { seconds: 139, ease: EASES.linear, swing: 0.95 },
148
+ wheel: { seconds: 907, ease: EASES.linear, swing: 1 },
149
+ fade: { seconds: 20, ease: EASES["ease-in-out"], swing: 1 },
150
+ dissolve: { seconds: 30, ease: EASES["ease-in-out"], swing: 1 },
151
+ };
152
+ /** The lights the loops were signed off casting, warm as sunlight and fireflies are. */
153
+ export const EFFECT_LIGHTS = {
154
+ sun: new ColorRgba(255, 228, 176),
155
+ firefly: new ColorRgba(222, 245, 140),
156
+ };
157
+ /** How far a shimmer's light reaches either side of its centre, in columns, as signed off. */
158
+ export const SHIMMER_WIDTH = 24;
159
+ const clamp01 = (x) => Math.min(1, Math.max(0, x));
160
+ /**
161
+ * `w` of the way from `from` to `to` along a straight line in OKLab. Light
162
+ * falling on a colour, or a colour thinning into its ground, moves straight
163
+ * toward the other colour; `Oklch.mix` goes round the hue circle instead, so
164
+ * blue lit by gold would pass through cyan on the way.
165
+ */
166
+ function blend(from, to, w) {
167
+ if (!(w >= 0 && w <= 1))
168
+ throw new RangeError(`blend: w must be in [0, 1]; got ${w}`);
169
+ const rad = Math.PI / 180;
170
+ const a = (1 - w) * from.c * Math.cos(from.h * rad) + w * to.c * Math.cos(to.h * rad);
171
+ const b = (1 - w) * from.c * Math.sin(from.h * rad) + w * to.c * Math.sin(to.h * rad);
172
+ return new Oklch((1 - w) * from.l + w * to.l, Math.hypot(a, b), Math.atan2(b, a) / rad, from.alpha);
173
+ }
174
+ /**
175
+ * A colour under `glow`: the one way every light here falls. It moves `w` of
176
+ * the way toward the light's colour, and never below its own lightness —
177
+ * light can warm a colour and lift it, never darken it. A colour already
178
+ * lighter than the light only warms, and white, which has no warmer shade at
179
+ * its lightness, stays white.
180
+ */
181
+ export function light(glow) {
182
+ const to = Oklch.fromRgba(glow);
183
+ return (color, w) => {
184
+ const from = Oklch.fromRgba(color);
185
+ return blend(from, new Oklch(Math.max(from.l, to.l), to.c, to.h, to.alpha), w).toRgba();
186
+ };
187
+ }
188
+ /** How long a swell `k` strong lasts, in turns. */
189
+ const lasts = (shape, k) => k * shape[shape.length - 1][0];
190
+ /**
191
+ * A swell — a breath, a firefly's glow — `k` strong, in [0, 1], `s` after it
192
+ * began: a raised cosine from each knot to the next, 0 before and after, zero
193
+ * slope at every knot. A weaker swell is a shorter one in proportion, so it
194
+ * climbs no more steeply than the strongest: however strong, its steepest
195
+ * moment is the same, and only the strongest need be checked against the
196
+ * step a frame may take. A swell of strength 0 is no swell at all: 0
197
+ * throughout, its own start included.
198
+ */
199
+ function swell(shape, s, k) {
200
+ const p = s / k;
201
+ const to = shape.findIndex(([turn]) => p < turn);
202
+ if (!(p > 0) || to < 0)
203
+ return 0;
204
+ const [t0, l0] = to === 0 ? [0, 0] : shape[to - 1];
205
+ const [t1, l1] = shape[to];
206
+ return k * (l0 + ((l1 - l0) * (1 - Math.cos((Math.PI * (p - t0)) / (t1 - t0)))) / 2);
207
+ }
208
+ /**
209
+ * How much of a swell's light has been given by `s`, in [0, 1]: `swell`'s own
210
+ * running integral over its whole, by the midpoint rule, so the two cannot
211
+ * disagree. Paced by it, a motion is quickest at the swell's height and still
212
+ * at both its ends. `k` is above 0.
213
+ */
214
+ function given(shape, s, k) {
215
+ const whole = lasts(shape, k);
216
+ const area = (to) => {
217
+ let sum = 0;
218
+ for (let i = 0; i < 24; i++)
219
+ sum += swell(shape, ((i + 0.5) / 24) * to, k);
220
+ return sum * to;
221
+ };
222
+ return area(Math.min(Math.max(s, 0), whole)) / area(whole);
223
+ }
224
+ /** A firefly's brightest flash, in turns: it kindles quicker than it fades, and the rest of its turn is dark. */
225
+ const FLASH = [[0.24, 1], [0.6, 0]];
226
+ /**
227
+ * Pulse, as a gentle glow on one element of a screen: the colours handed to
228
+ * it, an element's own, warm into `light` and settle back, over and over, the
229
+ * way an indicator breathes while something is waiting. Swing: how far into
230
+ * the light it goes at the top of a beat, 0–1. The element glows as one —
231
+ * every cell it is drawn in at once — and the colours not handed to it, the
232
+ * rest of the screen, stay as they are, so what pulses is what was chosen.
233
+ * Each element keeps a time of its own, a little quicker or slower than
234
+ * another's by `z`, and no beat is quite the last: the rhythm wanders ahead
235
+ * and behind its metronome and each beat goes a little deeper or shallower
236
+ * than the one before. A beat starts and ends in rest, so `t = 0` draws the
237
+ * element untouched.
238
+ */
239
+ export function pulse(curve, glow, z) {
240
+ requireCurve(curve);
241
+ // How much quicker or slower than `curve.seconds` this element beats.
242
+ const rate = 0.9 + 0.2 * hash(z, 3.1);
243
+ // How far, in beats, the rhythm wanders ahead and behind its metronome. Noise's
244
+ // slope stays within ±2.3 over its 3-beat length, so the wander never turns
245
+ // the beat back.
246
+ const WANDER = 0.1;
247
+ const wander = (t) => WANDER * noise(t / (3 * curve.seconds), 0.5, 0.5 + z);
248
+ const start = wander(0);
249
+ const field = (_cell, t) => {
250
+ const beats = (rate * t) / curve.seconds + wander(t) - start;
251
+ const n = Math.floor(beats);
252
+ // Each beat's own depth, 70% to the whole of the swing; it changes where
253
+ // the beat is at rest, so nothing jumps.
254
+ const depth = 0.7 + 0.3 * smoothstep(-0.5, 0.5, noise(n * 0.37, 2.1, z));
255
+ const beat = 0.5 - 0.5 * Math.cos(2 * Math.PI * (beats - n));
256
+ return curve.swing * curve.ease(depth * beat);
257
+ };
258
+ return { touch: light(glow), field };
259
+ }
260
+ /**
261
+ * How many columns a passing light crosses in its period: its pace.
262
+ * Sunlight moves at a speed, not in a time set by how long the row is,
263
+ * so a wider element is crossed for longer, never faster, and no cell's colour
264
+ * changes quicker on it from one frame to the next.
265
+ */
266
+ const STRIDE = 80;
267
+ /** A soft, compact bump: 1 at `d = 0`, 0 from `|d| ≥ 1`, smooth throughout. */
268
+ const bump = (d) => (Math.abs(d) >= 1 ? 0 : (1 - d * d) ** 3);
269
+ /**
270
+ * Shimmer, as sunlight moving across water. Swing: how far into `light` the
271
+ * brightest glint goes, 0–1. A soft band of light, `width` columns either
272
+ * side of its centre, crosses columns 0 to `span`, entering and leaving
273
+ * fully off the row, `STRIDE` columns in about three quarters of a period. Its passes
274
+ * come the way light comes and goes on water as clouds move: while the sun
275
+ * is out they are bright and follow close on one another, one sometimes
276
+ * catching the last; under cloud they thin to faint ones with the row still
277
+ * for a while between, and the light is never gone for long. Each crosses at its own unhurried pace, drifting a little ahead
278
+ * and behind as it goes. Inside a band the light is a faint glow broken
279
+ * into a glitter path — bright filaments where two ripples cross — carried
280
+ * on a slow current and re-forming as they go, so the glints dance while
281
+ * the band glides. Before the first pass enters, the row is untouched.
282
+ */
283
+ export function shimmer(curve, span, width, glow, z) {
284
+ requireCurve(curve);
285
+ const P = curve.seconds;
286
+ const across = span + 2 * width;
287
+ // Passes are events on slots of half a period. Whether slot `j` holds one,
288
+ // and how bright, is the sky's: slow noise over the slots, so sunny and
289
+ // cloudy spells each last a few periods, and a pass is likelier and
290
+ // brighter the clearer the sky.
291
+ const SLOT = P / 2;
292
+ const sky = (j) => smoothstep(-0.4, 0.4, noise(j / 7, 3.3, 5.5 + z));
293
+ // Pass `j` at `t`: how bright its band is and where its centre stands, off
294
+ // the row before it sets off and once it is done. It sets off somewhere in
295
+ // the first `SETS_OFF` of its slot and carries its centre `STRIDE` columns
296
+ // in `LEAST` to `LEAST + SPREAD` periods, give or take its drift, and every
297
+ // one is off the row `UP` slots after its own began.
298
+ const SETS_OFF = 0.7;
299
+ const LEAST = 0.51;
300
+ const SPREAD = 0.4;
301
+ const UP = SETS_OFF + ((LEAST + SPREAD) * P * across) / STRIDE / SLOT;
302
+ const pass = (j, t) => {
303
+ const clear = sky(j);
304
+ // Even under cloud about one slot in three holds a pass, a faint one, so a
305
+ // still spell lasts a period or so, the way a cloud's shadow does, and
306
+ // never long enough to read as the light gone out.
307
+ const present = hash(j, 7.5 + z) < 0.35 + 0.6 * clear ? 1 : 0;
308
+ const strength = present * (0.3 + 0.7 * clear * (0.6 + 0.4 * hash(j, 8.5 + z)));
309
+ const crossing = ((P * across) / STRIDE) * (LEAST + SPREAD * hash(j, 5.5 + z));
310
+ const q = clamp01((t - (j + SETS_OFF * hash(j, 6.5 + z)) * SLOT) / crossing);
311
+ // It drifts ahead and behind on slow noise, by nothing at either edge of
312
+ // the row and never enough to turn it back.
313
+ const centre = q * across - width + 0.06 * across * Math.sin(Math.PI * q) * noise(q, 2.7, j + z);
314
+ return { strength, centre };
315
+ };
316
+ // The passes on the row at one moment: a frame asks for the same moment
317
+ // once per cell, so they are worked out once per moment.
318
+ let passing = { t: Number.NaN, passes: [] };
319
+ // Where two passes overlap the brighter wins, so light never doubles up;
320
+ // the first slot is 0, so before it nothing is lit.
321
+ const band = (col, t) => {
322
+ if (passing.t !== t) {
323
+ const last = Math.floor(t / SLOT);
324
+ const passes = [];
325
+ for (let j = Math.max(0, Math.ceil(last - UP)); j <= last; j++)
326
+ passes.push(pass(j, t));
327
+ passing = { t, passes };
328
+ }
329
+ let lit = 0;
330
+ for (const { strength, centre } of passing.passes)
331
+ lit = Math.max(lit, strength * bump((col - centre) / width));
332
+ return lit;
333
+ };
334
+ // A ripple's crest: a Gaussian ridge, 1 where the field crosses zero and in
335
+ // (0, 1] for any noise, smooth throughout, so a crest sliding through a cell
336
+ // lights it smoothly.
337
+ const ripple = (a, b, c) => Math.exp(-((noise(a, b, c) / 0.8) ** 2));
338
+ const field = (cell, t) => {
339
+ const lit = band(cell.col, t);
340
+ const row = cell.row + z;
341
+ const current = 0.8 * noise(cell.col * 0.05, row * 0.3 + 1.3, t * 0.02);
342
+ const caustic = (ripple(cell.col * 0.21 + current, row * 0.9, t * 0.022) * ripple(cell.col * 0.33 + 9.1 - current, row * 0.7, t * 0.018 + 4.2)) **
343
+ 2;
344
+ return curve.swing * curve.ease(lit * (0.1 + 0.6 * caustic));
345
+ };
346
+ return { touch: light(glow), field };
347
+ }
348
+ /**
349
+ * Sparkle, as fireflies. Swing: how far into the firefly's `light` a glow
350
+ * goes at its brightest, 0–1. About one firefly to every eight columns of
351
+ * `span`, each keeping near a home of its own. A firefly flashes — kindles,
352
+ * drifts a few cells glowing, slowing as it fades — then flies on in the
353
+ * dark and flashes again somewhere a few cells off, the way fireflies at
354
+ * dusk are seen: a short lit stroke, then gone, each time a little
355
+ * elsewhere. Each keeps its own irregular time, now and then letting a turn
356
+ * pass dark, so most moments a few are lit, at different brightnesses. Its
357
+ * light is a soft halo several cells wide, brightest where it is, so as it
358
+ * drifts the glow slides between cells rather than hopping. `seconds` is a firefly's
359
+ * turn: its brightest flash, rise and fall, takes `FLASH` of it, and a
360
+ * dimmer one less.
361
+ */
362
+ export function sparkle(curve, span, glow, z) {
363
+ requireCurve(curve);
364
+ const count = Math.max(3, Math.round(span / 8));
365
+ const HALO = 5;
366
+ // How far from home a flash may begin, and how far the brightest drifts while lit.
367
+ const HOP = 5;
368
+ const GLIDE = 5;
369
+ const TURN = curve.seconds;
370
+ const homes = Array.from({ length: count }, (_, i) => ({
371
+ home: (i + 0.5 + 0.7 * noise(i * 1.7 + 0.2, 0.3, 0.5 + z)) * (span / count),
372
+ z: i * 4.9 + 0.3 + z,
373
+ // Out of step with every other firefly from the first moment.
374
+ offset: hash(i, 0.7 + z) * TURN,
375
+ }));
376
+ // Where every firefly is and how bright at one moment: a frame asks for
377
+ // the same moment once per cell, so it is worked out once per moment.
378
+ let flown = { t: Number.NaN, flies: [] };
379
+ const fliesAt = (t) => {
380
+ if (flown.t !== t) {
381
+ flown = {
382
+ t,
383
+ flies: homes.map((fly) => {
384
+ // Flash `n` is one event: whether it comes, how bright, when in
385
+ // its turn and where are its own. Between flashes the firefly
386
+ // is dark, so where it moves to is never seen moving.
387
+ const u = t + fly.offset;
388
+ const n = Math.floor(u / TURN);
389
+ // A dark turn is a flash with no light: it keeps its length, so
390
+ // every flash is timed alike.
391
+ const lit = hash(n, fly.z + 1) < 0.15 ? 0 : 1;
392
+ const k = 0.5 + 0.5 * hash(n, fly.z + 2);
393
+ const at = hash(n, fly.z + 3) * (1 - lasts(FLASH, k));
394
+ const s = u / TURN - n - at;
395
+ // While it glows it drifts a few cells one way or the other, as
396
+ // fast as it is bright: quickest at its height, slowing as it
397
+ // fades — the lit stroke a firefly draws on the dusk. A brighter
398
+ // flash lasts longer and draws a longer stroke at the same pace.
399
+ return {
400
+ x: fly.home +
401
+ HOP * (2 * hash(n, fly.z + 4) - 1) +
402
+ GLIDE * k * (2 * hash(n, fly.z + 5) - 1) * given(FLASH, s, k) +
403
+ 1.5 * noise(fly.z, 0.1, t * 0.035),
404
+ y: 0.6 * noise(fly.z, 2.2, t * 0.035),
405
+ glow: lit * swell(FLASH, s, k),
406
+ };
407
+ }),
408
+ };
409
+ }
410
+ return flown.flies;
411
+ };
412
+ const field = (cell, t) => {
413
+ // Where two halos overlap the brighter wins, so light never doubles up.
414
+ let shine = 0;
415
+ for (const fly of fliesAt(t))
416
+ shine = Math.max(shine, fly.glow * bump(Math.hypot(cell.col - fly.x, 2 * (cell.row - fly.y)) / HALO));
417
+ return curve.swing * curve.ease(shine);
418
+ };
419
+ return { touch: light(glow), field };
420
+ }
421
+ /**
422
+ * A cell mixed toward the terminal's `ground` by how hidden it is: its fill
423
+ * toward the ground, and its ink toward that fill, so an element with fills
424
+ * of its own — a powerline strip — arrives and leaves whole, fill and words
425
+ * together, never as bare coloured blocks. Invisible is ink and fill equal
426
+ * to the ground. `swing` scales the hiding — 1 reaches fully invisible. A
427
+ * cell wholly shown is returned as it was.
428
+ */
429
+ export function veiled(visibility, swing, ground) {
430
+ const to = Oklch.fromRgba(ground);
431
+ return (colors, cell, t) => {
432
+ const hidden = swing * (1 - clamp01(visibility(cell, t)));
433
+ if (hidden === 0)
434
+ return colors;
435
+ const bg = blend(Oklch.fromRgba(colors.bg), to, hidden);
436
+ return { fg: blend(Oklch.fromRgba(colors.fg), bg, hidden).toRgba(), bg: bg.toRgba() };
437
+ };
438
+ }
439
+ /**
440
+ * Where each cell falls in a transition's order, in [0, 1]: smooth noise over
441
+ * the element, so neighbouring cells arrive or leave together, in patches
442
+ * with soft edges, as ink spreads or mist thins. `z` places the element in
443
+ * the noise. A cell's place never changes, so it is worked out once.
444
+ */
445
+ function order(z) {
446
+ const places = new Map();
447
+ return (cell) => {
448
+ const at = `${cell.row}:${cell.col}`;
449
+ // The noise is read through slower noise, so the patches reach out in
450
+ // curling tendrils rather than sitting as round blots.
451
+ const place = places.get(at) ??
452
+ clamp01(0.5 + 1.1 * fbm(cell.col * 0.09 + 1.4 * noise(cell.col * 0.035, cell.row * 0.2 + 2.6, z + 0.5), cell.row * 0.45, z, 3));
453
+ places.set(at, place);
454
+ return place;
455
+ };
456
+ }
457
+ function transition(curve, start, effect) {
458
+ const end = start + curve.seconds;
459
+ return { effect, done: (t) => t >= end };
460
+ }
461
+ /**
462
+ * Of a transition's `seconds`, how long each cell's own change takes. Under
463
+ * 1, so the last cell to start is done by the transition's end.
464
+ */
465
+ const OWN = 0.45;
466
+ /**
467
+ * Each cell's progress through its own change in a transition starting at
468
+ * `start`, in [0, 1]: a cell `place` along the order starts that share of the
469
+ * way through the time the others leave it, and takes `OWN` of `seconds`.
470
+ */
471
+ function ownChange(curve, start, place) {
472
+ const own = curve.seconds * OWN;
473
+ // One phase, shifted per cell, so no cell builds its own each frame.
474
+ const change = Phase.once(start, own);
475
+ return (cell, t) => change(t - place(cell) * (curve.seconds - own));
476
+ }
477
+ /**
478
+ * Fade-in starting at `start`, as ink blooming in water: patches of the
479
+ * element surface first and the rest follows outward from them, each cell
480
+ * rising smoothly over its own part of the duration. Whole at
481
+ * `start + seconds`.
482
+ */
483
+ export function fadeIn(curve, start, z, ground) {
484
+ requireCurve(curve);
485
+ const risen = ownChange(curve, start, order(1.7 + z));
486
+ return transition(curve, start, veiled((cell, t) => curve.ease(risen(cell, t)), curve.swing, ground));
487
+ }
488
+ /**
489
+ * The rebound of a dissolve, every constant that makes it feel as it does —
490
+ * patches of what is left rising back before they go: `depth` (the most, as a
491
+ * share of whole, a cell rises again), `from` and `to` (where in its own
492
+ * fade, as a share of it gone, the rising begins and is done), `maskFrom` and
493
+ * `maskTo` (which cells rebound: those whose eddy noise is past `maskFrom`,
494
+ * all of them past `maskTo`) and `late` (how much likelier the later a cell
495
+ * goes: 0 is no more likely, 1 is nothing for the first to go).
496
+ */
497
+ const DISSOLVE_SHAPE = { depth: 0.4, from: 0.5, to: 0.95, maskFrom: 0.35, maskTo: 0.75, late: 0.6 };
498
+ /**
499
+ * Dissolve-out starting at `start`, as mist lifting: the element thins in
500
+ * drifting patches, each cell fading smoothly over its own part of the
501
+ * duration, until nothing is left at `start + seconds`. Mist does not lift
502
+ * in one breath: towards the end, patches of what is left — the later a cell
503
+ * goes, the likelier — rise back part way, as the last of it eddies, before
504
+ * they thin away for good.
505
+ */
506
+ export function dissolveOut(curve, start, z, ground) {
507
+ requireCurve(curve);
508
+ const place = order(4.2 + z);
509
+ const eddy = order(9.3 + z);
510
+ const faded = ownChange(curve, start, place);
511
+ const { depth, from, to, maskFrom, maskTo, late } = DISSOLVE_SHAPE;
512
+ return transition(curve, start, veiled((cell, t) => {
513
+ const gone = curve.ease(faded(cell, t));
514
+ const q = clamp01((gone - from) / (to - from));
515
+ const back = depth * smoothstep(maskFrom, maskTo, eddy(cell)) * (1 - late + late * place(cell)) * Math.sin(Math.PI * q) ** 2;
516
+ return 1 - gone + back;
517
+ }, curve.swing, ground));
518
+ }
519
+ /**
520
+ * The periods, in seconds, of the two noises a segment's stray from the
521
+ * wheel's shared turn rides on. Primes, as every loop's default period is,
522
+ * and none a multiple of another's: a session's loops never come back round
523
+ * to a state they have shown, within any sitting — the first return of the
524
+ * wheel alone is after the product of its three periods.
525
+ */
526
+ const STRAY_PERIODS = [211, 337];
527
+ /** How far a segment may stray from the shared turn at swing 1, in degrees: half a turn — fully apart. */
528
+ const STRAY = 180;
529
+ /**
530
+ * Wheel, as the colour of daylight going round: every hue turning the whole
531
+ * way round once a period — far too slowly to be seen moving, so a glance
532
+ * sees a steady palette and an hour's absence a different one — each
533
+ * segment of the element at its own pace. Swing: how far the segments stray
534
+ * from one another, 0–1, 1 being half a turn apart. A segment is a fill of
535
+ * the element's own — one of `fills`, the colours it draws as a ground — and
536
+ * it turns at its own pace wherever it is drawn: as the ground of its cells,
537
+ * and as the ink of the seam glyph pointing out of it into the next, so a
538
+ * powerline cap keeps the colour of the cell it caps. An ink that is no fill
539
+ * turns with the ground it sits on, so a cell's words and fill turn
540
+ * together; on the terminal's ground, a run of text's ink is a segment of
541
+ * its own. A segment's stray is slow noise of its own, at `STRAY_PERIODS`,
542
+ * so no two keep step, and the row never shows one spread of hues twice.
543
+ * Lightness and chroma are kept, so the words read as they did; the turn
544
+ * only happens to `colors`, the element's own, never the terminal's.
545
+ */
546
+ export function wheel(curve, colors, fills, z) {
547
+ requireCurve(curve);
548
+ const P = curve.seconds;
549
+ // A segment's seed, from the colour it is.
550
+ const seed = (hex) => hash(Number.parseInt(hex.slice(1), 16) * 1e-4, 17.3 + z);
551
+ const stray = (s, t) => noise(t / STRAY_PERIODS[0], 5.9 + 97 * s, z) + 0.5 * noise(t / STRAY_PERIODS[1], 2.3 + 89 * s, z + 7.1);
552
+ // Measured from where it stood at the start, as pulse's wander is, so the
553
+ // first frame turns nothing and the second has turned only a frame's worth.
554
+ const turned = (hex, t) => {
555
+ const s = seed(hex);
556
+ return (360 * t) / P + curve.swing * STRAY * ((stray(s, t) - stray(s, 0)) / 1.5);
557
+ };
558
+ return (drawn, cell, t) => {
559
+ // Whether a slot shows one of the element's colours: never the terminal's, whatever its hex.
560
+ const own = { fg: !cell.terminal.fg && colors.has(drawn.fg.hex), bg: !cell.terminal.bg && colors.has(drawn.bg.hex) };
561
+ // An own colour turns with its segment: itself if a fill or beside no own colour, else the one beside it.
562
+ const turn = (color, mine, other, theirs) => !mine ? color : Oklch.fromRgba(color).applyKey({ ...IDENTITY, hueShift: turned(fills.has(color.hex) || !theirs ? color.hex : other.hex, t) }).toRgba();
563
+ return { fg: turn(drawn.fg, own.fg, drawn.bg, own.bg), bg: turn(drawn.bg, own.bg, drawn.fg, own.fg) };
564
+ };
565
+ }