@promptctl/rich-js 0.24.0 → 0.24.1

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 (95) hide show
  1. package/THIRD-PARTY-NOTICES +29 -1
  2. package/dist/core/animation.d.ts +78 -0
  3. package/dist/core/animation.js +107 -0
  4. package/dist/core/cell-widths.d.ts +9 -0
  5. package/dist/core/cell-widths.js +140 -0
  6. package/dist/core/cells.d.ts +17 -11
  7. package/dist/core/cells.js +100 -25
  8. package/dist/core/console.js +5 -5
  9. package/dist/core/easing.d.ts +110 -10
  10. package/dist/core/easing.js +269 -36
  11. package/dist/core/json.js +2 -1
  12. package/dist/core/measure.js +2 -2
  13. package/dist/core/place.js +2 -1
  14. package/dist/core/pretty.js +2 -2
  15. package/dist/core/protocol.d.ts +73 -19
  16. package/dist/core/protocol.js +56 -15
  17. package/dist/core/render.js +2 -1
  18. package/dist/core/segment.d.ts +18 -1
  19. package/dist/core/segment.js +45 -44
  20. package/dist/core/strip.d.ts +3 -2
  21. package/dist/core/strip.js +11 -7
  22. package/dist/core/style.js +17 -6
  23. package/dist/core/text.js +22 -14
  24. package/dist/core/timeline.d.ts +84 -0
  25. package/dist/core/timeline.js +144 -0
  26. package/dist/core/tween.d.ts +49 -0
  27. package/dist/core/tween.js +69 -0
  28. package/dist/host/app.js +2 -2
  29. package/dist/index.d.ts +10 -7
  30. package/dist/index.js +13 -4
  31. package/dist/renderables/align.d.ts +3 -2
  32. package/dist/renderables/align.js +3 -0
  33. package/dist/renderables/columns.d.ts +3 -2
  34. package/dist/renderables/columns.js +7 -2
  35. package/dist/renderables/constrain.d.ts +3 -2
  36. package/dist/renderables/constrain.js +5 -1
  37. package/dist/renderables/effect.d.ts +43 -58
  38. package/dist/renderables/effect.js +58 -64
  39. package/dist/renderables/effects.d.ts +16 -14
  40. package/dist/renderables/effects.js +64 -45
  41. package/dist/renderables/embed.js +3 -2
  42. package/dist/renderables/flexStrip.d.ts +3 -2
  43. package/dist/renderables/flexStrip.js +6 -1
  44. package/dist/renderables/frames.d.ts +40 -19
  45. package/dist/renderables/frames.js +48 -21
  46. package/dist/renderables/group.d.ts +3 -2
  47. package/dist/renderables/group.js +5 -2
  48. package/dist/renderables/html-entities.d.ts +8 -0
  49. package/dist/renderables/html-entities.js +386 -0
  50. package/dist/renderables/layout.d.ts +3 -2
  51. package/dist/renderables/layout.js +15 -2
  52. package/dist/renderables/list-view.d.ts +3 -2
  53. package/dist/renderables/list-view.js +12 -5
  54. package/dist/renderables/live.js +2 -2
  55. package/dist/renderables/markdown-parse.d.ts +109 -0
  56. package/dist/renderables/markdown-parse.js +1421 -0
  57. package/dist/renderables/markdown-url.d.ts +19 -0
  58. package/dist/renderables/markdown-url.js +314 -0
  59. package/dist/renderables/markdown.d.ts +5 -7
  60. package/dist/renderables/markdown.js +356 -683
  61. package/dist/renderables/padding.d.ts +8 -20
  62. package/dist/renderables/padding.js +32 -50
  63. package/dist/renderables/panel.d.ts +26 -26
  64. package/dist/renderables/panel.js +67 -103
  65. package/dist/renderables/progress.d.ts +4 -2
  66. package/dist/renderables/progress.js +11 -2
  67. package/dist/renderables/progressBar.js +2 -2
  68. package/dist/renderables/python-space.d.ts +13 -0
  69. package/dist/renderables/python-space.js +24 -0
  70. package/dist/renderables/spinner.d.ts +26 -14
  71. package/dist/renderables/spinner.js +49 -37
  72. package/dist/renderables/syntax.js +2 -1
  73. package/dist/renderables/table.d.ts +19 -11
  74. package/dist/renderables/table.js +140 -156
  75. package/dist/renderables/traceback.d.ts +1 -1
  76. package/dist/renderables/traceback.js +6 -5
  77. package/dist/renderables/tree.d.ts +6 -5
  78. package/dist/renderables/tree.js +11 -7
  79. package/dist/renderables/viewport.d.ts +13 -5
  80. package/dist/renderables/viewport.js +41 -20
  81. package/dist/template-bindings/animation-funcs.js +2 -2
  82. package/dist/template-bindings/index.js +2 -1
  83. package/dist/themes/ramp.d.ts +18 -3
  84. package/dist/themes/ramp.js +23 -2
  85. package/dist/widgets/focus-manager.d.ts +21 -14
  86. package/dist/widgets/focus-manager.js +28 -29
  87. package/dist/widgets/index.d.ts +1 -1
  88. package/dist/widgets/static-item.d.ts +4 -3
  89. package/dist/widgets/static-item.js +9 -7
  90. package/dist/widgets/text-input.d.ts +28 -18
  91. package/dist/widgets/text-input.js +144 -101
  92. package/dist/widgets/widget-app.d.ts +19 -3
  93. package/dist/widgets/widget-app.js +39 -21
  94. package/dist/widgets/widget-base.js +0 -3
  95. package/package.json +2 -5
@@ -2,24 +2,26 @@
2
2
  * Effected — a renderable whose finished cells' colours pass through an
3
3
  * effect at a moment `t`.
4
4
  *
5
- * An effect is one pure function in two stages: a time and what the element
6
- * draws in that frame in, a function of the frame out; and into that, a
7
- * cell's colours and where the cell is, the cell's new colours out. Whatever
8
- * holds for the whole frame is worked out in the first stage, once, and every
9
- * cell of the frame is handed to the second, but for those a wrapper keeps
10
- * back: `confined` hands over only the cells its owners drew, so a second
11
- * stage never counts on seeing them all. Shimmer, pulse, fade and
12
- * dissolve are all this one shape, differing by what the function does with
13
- * its inputs [LAW:one-type-per-behavior]; two effects stack by nesting one
14
- * wrapper in another, which is the same as calling one after the other but
15
- * for what the outer one measures: the inner one's output.
5
+ * An effect is, for what an element draws in a frame, an `Animation`
6
+ * (`core/animation.ts`) of what is done to its cells: its value at `t` is a
7
+ * function from a cell's colours and where the cell is to the cell's new
8
+ * colours (`EffectFrame`), and it says when that next changes as every
9
+ * animation does. Whatever holds for the whole frame is worked out once, in
10
+ * the effect and its `at(t)`, and every cell of the frame is handed to the
11
+ * function `at(t)` returns, but for those a wrapper keeps back: `confined`
12
+ * hands over only the cells its owners drew, so an effect never counts on
13
+ * seeing them all. Shimmer, pulse, fade and dissolve are all this one shape,
14
+ * differing by what the function does with its inputs
15
+ * [LAW:one-type-per-behavior]; two effects stack by nesting one wrapper in
16
+ * another, which is the same as calling one after the other but for what the
17
+ * outer one measures: the inner one's output.
16
18
  *
17
- * What the element draws (`Drawn`) — the cells the second stage will be
18
- * handed, and the colours of its words — is measured here, in the render, off
19
- * the same reading of each segment its cells are drawn from, and handed to the
20
- * effect's first stage, once a frame. It is what an effect keeping words legible is
21
- * settled against (`onColors`, `./effects.ts`), so no caller derives it a
22
- * second time from a render of its own.
19
+ * What the element draws (`Drawn`) — the cells the effect will be handed,
20
+ * and the colours of its words — is measured here, in the render, off the
21
+ * same reading of each segment its cells are drawn from, and handed to the
22
+ * effect once a frame. It is what an effect keeping words legible is settled
23
+ * against (`onColors`, `./effects.ts`), so no caller derives it a second time
24
+ * from a render of its own.
23
25
  *
24
26
  * [LAW:no-ambient-temporal-coupling] `t` is a constructor argument, not a
25
27
  * clock read. Whoever draws frames builds the view for each frame's `t`, so
@@ -49,9 +51,10 @@
49
51
  * what the child drew, so a sweep crosses the element, not the screen.
50
52
  */
51
53
  import { type Anchor } from "../core/anchor.js";
54
+ import { Animation } from "../core/animation.js";
52
55
  import { ColorDepth, type ColorRgba, type TerminalTheme } from "../core/color.js";
53
56
  import type { Beat } from "../core/easing.js";
54
- import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
57
+ import type { Composite, Measurable, Renderable, RenderOptions } from "../core/protocol.js";
55
58
  import { Segment } from "../core/segment.js";
56
59
  /**
57
60
  * A cell's foreground and background as they appear on screen — the glyph's
@@ -119,7 +122,7 @@ export interface Drawn {
119
122
  /**
120
123
  * Every cell the element shows something in this frame — a glyph, a
121
124
  * ground that is not the terminal's, or a line its style draws — where it
122
- * sits and who drew it, listed before the second stage is handed any: what
125
+ * sits and who drew it, listed before the effect is handed any: what
123
126
  * an effect spread over the element — a transition, which has its last cell
124
127
  * settle as it ends — is spread over. A blank cell on the terminal's ground
125
128
  * that draws no line is handed but not listed, since nothing in it shows.
@@ -138,42 +141,23 @@ export interface Drawn {
138
141
  /** The terminal the frame is drawn on. */
139
142
  readonly theme: TerminalTheme;
140
143
  }
144
+ /** What an effect does to the cells of one frame: each cell's colours and where it is in, its colours out. Return `colors` to leave a cell be. */
145
+ export type EffectFrame = (colors: CellColors, cell: EffectCell) => CellColors;
141
146
  /**
142
- * The frame at time `t`, in seconds, whose element draws `drawn`: each cell's
143
- * colours in that frame. Return `colors` to leave a cell be.
144
- * [LAW:types-are-the-program] Per-frame work is the outer stage, so it runs
145
- * once a frame by the shape of the call, never by a cache asking whether this
146
- * cell's frame is the last one's.
147
+ * For an element drawing `drawn`, the animation of what is done to its cells.
148
+ * [LAW:types-are-the-program] Per-frame work is `at(t)`, so it runs once a
149
+ * frame by the shape of the call, never by a cache asking whether this cell's
150
+ * frame is the last one's.
147
151
  *
148
- * An effect that knows when its frame next changes says so in `nextChange`,
149
- * asked with the same `t` and `drawn` its first stage is: a stepped one its
150
- * next step's start, a transition `t` until it is done and `Infinity` after,
151
- * a loop that holds every cell still at the frame's depth `Infinity` there.
152
- * [LAW:types-are-the-program] The answer is a fact about the frame, so it is
153
- * asked with everything the frame is drawn from; an answer asked with `t`
154
- * alone could not say that a frame is still because of its depth. A plain
155
- * function is an effect that moves every frame, and answers `t`
156
- * (`nextChangeOf`): the one true answer for a frame nothing has promised to
157
- * hold, so a loop needs no second shape.
152
+ * Its `nextChange` is when the frame next changes: a stepped effect its next
153
+ * step's start, a transition `t` until it is done and `Infinity` after, a
154
+ * loop that holds every cell still at the frame's depth `Infinity` there, and
155
+ * a loop that moves `t` (`Animation.continuous`). [LAW:types-are-the-program]
156
+ * The answer is a fact about the frame, so the animation is made from
157
+ * everything the frame is drawn from; one made from `t` alone could not say
158
+ * that a frame is still because of its depth.
158
159
  */
159
- export interface Effect {
160
- (t: number, drawn: Drawn): (colors: CellColors, cell: EffectCell) => CellColors;
161
- /** When the frame drawn at `t` of `drawn` next changes, in seconds on `t`'s timeline, never before `t`; absent, every frame. */
162
- readonly nextChange?: (t: number, drawn: Drawn) => number;
163
- }
164
- /**
165
- * When `effect`'s frame at `t` of `drawn` next changes: what it says, or `t`
166
- * for an effect that says nothing. [LAW:single-enforcer] The one reading of
167
- * an absent `nextChange`, so no caller holds its own default.
168
- */
169
- export declare function nextChangeOf(effect: Effect, t: number, drawn: Drawn): number;
170
- /**
171
- * An effect drawing as `effect` does, with `nextChange` as its answer to when
172
- * its frame next changes, in place of whatever `effect` answered; `effect`
173
- * itself is left as it was. A composer whose answer depends on the inner
174
- * effect's reads it with `nextChangeOf`, as `onBeat` and `confined` do.
175
- */
176
- export declare function withNextChange(effect: Effect, nextChange: (t: number, drawn: Drawn) => number): Effect;
160
+ export type Effect = (drawn: Drawn) => Animation<EffectFrame>;
177
161
  export interface EffectedOptions {
178
162
  /** The moment to draw, in seconds. */
179
163
  readonly t: number;
@@ -189,7 +173,7 @@ export interface EffectedOptions {
189
173
  */
190
174
  readonly theme: TerminalTheme;
191
175
  }
192
- export declare class Effected implements Renderable, Measurable {
176
+ export declare class Effected implements Renderable, Measurable, Composite {
193
177
  readonly renderable: Renderable;
194
178
  readonly effect: Effect;
195
179
  readonly t: number;
@@ -197,6 +181,7 @@ export declare class Effected implements Renderable, Measurable {
197
181
  readonly theme: TerminalTheme;
198
182
  constructor(renderable: Renderable, effect: Effect, options: EffectedOptions);
199
183
  render(options: RenderOptions): Iterable<Segment>;
184
+ contents(): Iterable<Renderable>;
200
185
  private _moved;
201
186
  private _recolored;
202
187
  measure(options: RenderOptions): {
@@ -222,7 +207,7 @@ export declare class Effected implements Renderable, Measurable {
222
207
  }
223
208
  /**
224
209
  * `effect` sampled and held on `beat`: drawn at the moment the hold in
225
- * progress began (`Beat.start`), so within a hold every frame is the frame
210
+ * progress began (`Beat.lastChange`), so within a hold every frame is the frame
226
211
  * at its start, and at each hold's start it moves on. A continuous effect
227
212
  * given a short curve becomes one that steps — a `wheel` turning a sixth of
228
213
  * a turn once a second — and at one redraw a second a hold shorter than a
@@ -230,10 +215,10 @@ export declare class Effected implements Renderable, Measurable {
230
215
  * held time is a function of `t`, so the same `t` draws the same bytes at
231
216
  * any rate. The frame next changes at a hold's start, and only one the
232
217
  * sampled effect has moved by: the first hold start at or after what it
233
- * answers for the frame held now (`Beat.end`). An effect that moves every
234
- * frame makes the beat's own answer, the end of this hold; one that is done
235
- * and says never leaves the beat saying never too, since the hold after this
236
- * one would draw the same frame. The answer is read off what the effect
218
+ * answers for the frame held now (`Beat.nextChange`). An effect that moves
219
+ * every frame makes the beat's own answer, the end of this hold; one that is
220
+ * done and says never leaves the beat saying never too, since the hold after
221
+ * this one would draw the same frame. The answer is read off what the effect
237
222
  * says, never off a comparison of frames: an effect that steps and is back
238
223
  * on the same colour by the next hold still turns the beat there, an early
239
224
  * answer the contract allows.
@@ -2,24 +2,26 @@
2
2
  * Effected — a renderable whose finished cells' colours pass through an
3
3
  * effect at a moment `t`.
4
4
  *
5
- * An effect is one pure function in two stages: a time and what the element
6
- * draws in that frame in, a function of the frame out; and into that, a
7
- * cell's colours and where the cell is, the cell's new colours out. Whatever
8
- * holds for the whole frame is worked out in the first stage, once, and every
9
- * cell of the frame is handed to the second, but for those a wrapper keeps
10
- * back: `confined` hands over only the cells its owners drew, so a second
11
- * stage never counts on seeing them all. Shimmer, pulse, fade and
12
- * dissolve are all this one shape, differing by what the function does with
13
- * its inputs [LAW:one-type-per-behavior]; two effects stack by nesting one
14
- * wrapper in another, which is the same as calling one after the other but
15
- * for what the outer one measures: the inner one's output.
5
+ * An effect is, for what an element draws in a frame, an `Animation`
6
+ * (`core/animation.ts`) of what is done to its cells: its value at `t` is a
7
+ * function from a cell's colours and where the cell is to the cell's new
8
+ * colours (`EffectFrame`), and it says when that next changes as every
9
+ * animation does. Whatever holds for the whole frame is worked out once, in
10
+ * the effect and its `at(t)`, and every cell of the frame is handed to the
11
+ * function `at(t)` returns, but for those a wrapper keeps back: `confined`
12
+ * hands over only the cells its owners drew, so an effect never counts on
13
+ * seeing them all. Shimmer, pulse, fade and dissolve are all this one shape,
14
+ * differing by what the function does with its inputs
15
+ * [LAW:one-type-per-behavior]; two effects stack by nesting one wrapper in
16
+ * another, which is the same as calling one after the other but for what the
17
+ * outer one measures: the inner one's output.
16
18
  *
17
- * What the element draws (`Drawn`) — the cells the second stage will be
18
- * handed, and the colours of its words — is measured here, in the render, off
19
- * the same reading of each segment its cells are drawn from, and handed to the
20
- * effect's first stage, once a frame. It is what an effect keeping words legible is
21
- * settled against (`onColors`, `./effects.ts`), so no caller derives it a
22
- * second time from a render of its own.
19
+ * What the element draws (`Drawn`) — the cells the effect will be handed,
20
+ * and the colours of its words — is measured here, in the render, off the
21
+ * same reading of each segment its cells are drawn from, and handed to the
22
+ * effect once a frame. It is what an effect keeping words legible is settled
23
+ * against (`onColors`, `./effects.ts`), so no caller derives it a second time
24
+ * from a render of its own.
23
25
  *
24
26
  * [LAW:no-ambient-temporal-coupling] `t` is a constructor argument, not a
25
27
  * clock read. Whoever draws frames builds the view for each frame's `t`, so
@@ -49,30 +51,15 @@
49
51
  * what the child drew, so a sweep crosses the element, not the screen.
50
52
  */
51
53
  import { drawnBy, shiftAnchor } from "../core/anchor.js";
54
+ import { Animation } from "../core/animation.js";
52
55
  import { cellLen, graphemes } from "../core/cells.js";
53
56
  import { ColorDepth, ColorSpec, SURFACE_BLACK } from "../core/color.js";
54
57
  import { fnv1a } from "../core/fnv1a.js";
55
58
  import { Measurement } from "../core/measure.js";
59
+ import { render } from "../core/protocol.js";
56
60
  import { Segment } from "../core/segment.js";
57
61
  import { JOIN, colorsOwnedBy } from "../core/strip.js";
58
62
  import { Style } from "../core/style.js";
59
- /**
60
- * When `effect`'s frame at `t` of `drawn` next changes: what it says, or `t`
61
- * for an effect that says nothing. [LAW:single-enforcer] The one reading of
62
- * an absent `nextChange`, so no caller holds its own default.
63
- */
64
- export function nextChangeOf(effect, t, drawn) {
65
- return effect.nextChange?.(t, drawn) ?? t;
66
- }
67
- /**
68
- * An effect drawing as `effect` does, with `nextChange` as its answer to when
69
- * its frame next changes, in place of whatever `effect` answered; `effect`
70
- * itself is left as it was. A composer whose answer depends on the inner
71
- * effect's reads it with `nextChangeOf`, as `onBeat` and `confined` do.
72
- */
73
- export function withNextChange(effect, nextChange) {
74
- return Object.assign((t, drawn) => effect(t, drawn), { nextChange });
75
- }
76
63
  export class Effected {
77
64
  renderable;
78
65
  effect;
@@ -90,27 +77,28 @@ export class Effected {
90
77
  this.theme = options.theme;
91
78
  }
92
79
  render(options) {
93
- const segments = this.renderable.render(options);
80
+ const segments = render(this.renderable, options);
94
81
  // With no colour output nothing the effect does is written, so the child
95
82
  // is what is shown, and it changes when the child says it does.
96
83
  if (options.colorSystem === null)
97
84
  return segments;
98
85
  return this._moved(segments, options);
99
86
  }
87
+ *contents() {
88
+ yield this.renderable;
89
+ }
100
90
  *_moved(segments, options) {
101
91
  // No depth named is no downgrade, as `drawnColors` reads it.
102
92
  const depth = options.colorSystem ?? ColorDepth.TRUECOLOR;
103
93
  // [LAW:one-source-of-truth] The frame is measured before any cell is
104
94
  // handed to the effect, off the same looks its cells are drawn from, and
105
- // the one measure is what both stages and the effect's next change are
106
- // asked with.
95
+ // the one measure is what the effect's animation is made from.
107
96
  const looked = looks(segments, depth, this.theme);
108
97
  const walked = walk(looked);
109
- const drawn = measured(looked, () => walked, depth, this.theme);
110
- const frame = this.effect(this.t, drawn);
98
+ const animation = this.effect(measured(looked, () => walked, depth, this.theme));
111
99
  // What the effect draws at `t` is drawn until its next change, and the
112
100
  // cells say so, under whatever the child already said (the sooner wins).
113
- yield* Segment.applyStyle(this._recolored(walked, frame, depth), new Style({ nextChange: nextChangeOf(this.effect, this.t, drawn) }));
101
+ yield* Segment.applyStyle(this._recolored(walked, animation.at(this.t), depth), new Style({ nextChange: animation.nextChange(this.t) }));
114
102
  }
115
103
  *_recolored(walked, frame, depth) {
116
104
  for (const { segment, look, glyphs } of walked) {
@@ -256,7 +244,7 @@ function measured(looked, walked, depth, theme) {
256
244
  }
257
245
  /**
258
246
  * `effect` sampled and held on `beat`: drawn at the moment the hold in
259
- * progress began (`Beat.start`), so within a hold every frame is the frame
247
+ * progress began (`Beat.lastChange`), so within a hold every frame is the frame
260
248
  * at its start, and at each hold's start it moves on. A continuous effect
261
249
  * given a short curve becomes one that steps — a `wheel` turning a sixth of
262
250
  * a turn once a second — and at one redraw a second a hold shorter than a
@@ -264,24 +252,33 @@ function measured(looked, walked, depth, theme) {
264
252
  * held time is a function of `t`, so the same `t` draws the same bytes at
265
253
  * any rate. The frame next changes at a hold's start, and only one the
266
254
  * sampled effect has moved by: the first hold start at or after what it
267
- * answers for the frame held now (`Beat.end`). An effect that moves every
268
- * frame makes the beat's own answer, the end of this hold; one that is done
269
- * and says never leaves the beat saying never too, since the hold after this
270
- * one would draw the same frame. The answer is read off what the effect
255
+ * answers for the frame held now (`Beat.nextChange`). An effect that moves
256
+ * every frame makes the beat's own answer, the end of this hold; one that is
257
+ * done and says never leaves the beat saying never too, since the hold after
258
+ * this one would draw the same frame. The answer is read off what the effect
271
259
  * says, never off a comparison of frames: an effect that steps and is back
272
260
  * on the same colour by the next hold still turns the beat there, an early
273
261
  * answer the contract allows.
274
262
  */
275
263
  export function onBeat(effect, beat) {
276
- return withNextChange((t, drawn) => effect(beat.start(t), drawn), (t, drawn) => {
277
- const moved = nextChangeOf(effect, beat.start(t), drawn);
278
- if (moved === Number.POSITIVE_INFINITY)
279
- return moved;
280
- const end = beat.end(t);
281
- // The hold boundaries after `t` are `end`, then each hold's end in turn;
282
- // the frame turns at the first one the effect has moved by.
283
- return moved <= end ? end : beat.start(moved) === moved ? moved : beat.end(moved);
284
- });
264
+ return (drawn) => {
265
+ const sampled = effect(drawn);
266
+ return {
267
+ at: (t) => sampled.at(beat.lastChange(t)),
268
+ nextChange(t) {
269
+ const moved = sampled.nextChange(beat.lastChange(t));
270
+ if (moved === Number.POSITIVE_INFINITY)
271
+ return moved;
272
+ const end = beat.nextChange(t);
273
+ // The hold boundaries after `t` are `end`, then each hold's end in turn;
274
+ // the frame turns at the first one the effect has moved by.
275
+ return moved <= end ? end : beat.lastChange(moved) === moved ? moved : beat.nextChange(moved);
276
+ },
277
+ // The frame held now began at this hold's start at the latest: an early
278
+ // answer where the effect had not moved by it, which the contract allows.
279
+ lastChange: (t) => beat.lastChange(t),
280
+ };
281
+ };
285
282
  }
286
283
  /**
287
284
  * `effect` on the colours any of `owners` drew, itself or through an owner
@@ -302,16 +299,13 @@ export function confined(effect, ...owners) {
302
299
  // The frame as the effect sees it: the owners' cells alone. [LAW:one-source-of-truth] One narrowing, handed to both stages.
303
300
  const owned = (drawn) => ({ ...drawn, cells: () => drawn.cells().filter((cell) => any(ownedOf(cell))) });
304
301
  // The owners' cells change when the effect's frame does.
305
- return withNextChange((t, drawn) => {
306
- const frame = effect(t, owned(drawn));
307
- return (colors, cell) => {
308
- const owns = ownedOf(cell);
309
- if (!any(owns))
310
- return colors;
311
- const moved = frame(colors, cell);
312
- return { fg: owns.fg ? moved.fg : colors.fg, bg: owns.bg ? moved.bg : colors.bg };
313
- };
314
- }, (t, drawn) => nextChangeOf(effect, t, owned(drawn)));
302
+ return (drawn) => Animation.map(effect(owned(drawn)), (frame) => (colors, cell) => {
303
+ const owns = ownedOf(cell);
304
+ if (!any(owns))
305
+ return colors;
306
+ const moved = frame(colors, cell);
307
+ return { fg: owns.fg ? moved.fg : colors.fg, bg: owns.bg ? moved.bg : colors.bg };
308
+ });
315
309
  }
316
310
  /**
317
311
  * What `segments`, drawn at `depth` on `theme`, draw their words in, measured
@@ -47,7 +47,7 @@
47
47
  * (`examples/effects-playground/programs.ts`), on the published library.
48
48
  */
49
49
  import { ColorRgba } from "../core/color.js";
50
- import { type Ease } from "../core/easing.js";
50
+ import { type Timing } from "../core/easing.js";
51
51
  import { type Drawn, type Effect, type EffectCell } from "./effect.js";
52
52
  /** What a loop does to one colour at strength `w` in [0, 1]: at 0, nothing. */
53
53
  export type Touch = (color: ColorRgba, w: number) => ColorRgba;
@@ -104,12 +104,14 @@ export declare function onColors(loop: Loop, colors?: ReadonlySet<string>): Effe
104
104
  * says; a colour it gives none, and one the terminal supplies, stays as it is.
105
105
  */
106
106
  export declare function atShares(loop: Loop, share: (drawn: Drawn) => ReadonlyMap<string, number>): Effect;
107
- /** How the curves below are tuned: one period (or duration), ease and swing. */
108
- export interface Curve {
109
- /** Seconds: a looping effect's period, a transition's duration. */
110
- readonly seconds: number;
111
- /** Intensity in [0, 1] to the fraction of `swing` applied. One that overshoots, as a CSS `cubic-bezier` may, saturates: an effect never acts past its swing. */
112
- readonly ease: Ease;
107
+ /**
108
+ * How the curves below are tuned: a `Timing` — seconds, a looping effect's
109
+ * period or a transition's duration, and an ease, intensity in [0, 1] to
110
+ * the fraction of `swing` applied; one that overshoots, as a CSS
111
+ * `cubic-bezier` may, saturates, since an effect never acts past its swing —
112
+ * and the swing itself.
113
+ */
114
+ export interface Curve extends Timing {
113
115
  /** How far the move goes at full intensity, in [0, 1]; what 1 means is the effect's. */
114
116
  readonly swing: number;
115
117
  }
@@ -122,32 +124,32 @@ export interface Curve {
122
124
  export declare const EFFECT_CURVES: {
123
125
  readonly shimmer: {
124
126
  readonly seconds: 109;
125
- readonly ease: Ease;
127
+ readonly ease: import("../core/easing.js").Ease;
126
128
  readonly swing: 0.9;
127
129
  };
128
130
  readonly pulse: {
129
131
  readonly seconds: 43;
130
- readonly ease: Ease;
132
+ readonly ease: import("../core/easing.js").Ease;
131
133
  readonly swing: 0.85;
132
134
  };
133
135
  readonly sparkle: {
134
136
  readonly seconds: 139;
135
- readonly ease: Ease;
137
+ readonly ease: import("../core/easing.js").Ease;
136
138
  readonly swing: 0.95;
137
139
  };
138
140
  readonly wheel: {
139
141
  readonly seconds: 907;
140
- readonly ease: Ease;
142
+ readonly ease: import("../core/easing.js").Ease;
141
143
  readonly swing: 1;
142
144
  };
143
145
  readonly fade: {
144
146
  readonly seconds: 20;
145
- readonly ease: Ease;
147
+ readonly ease: import("../core/easing.js").Ease;
146
148
  readonly swing: 1;
147
149
  };
148
150
  readonly dissolve: {
149
151
  readonly seconds: 30;
150
- readonly ease: Ease;
152
+ readonly ease: import("../core/easing.js").Ease;
151
153
  readonly swing: 1;
152
154
  };
153
155
  };
@@ -308,7 +310,7 @@ export interface ColorStep {
308
310
  * The search is settled once for each ink, ground and step, so a frame
309
311
  * inside a step redoes none, and nor does a later pass of the beat: at sixty
310
312
  * frames a second a one-second step is sixty identical frames, and the
311
- * frame says it changes next when the step ends (`Beat.end`).
313
+ * frame changes when the beat does: the signal is the beat mapped to inks.
312
314
  * [LAW:no-ambient-temporal-coupling] The beat is `core/easing.ts`'s, and
313
315
  * nothing here reads a clock.
314
316
  */
@@ -46,13 +46,14 @@
46
46
  * cards runs this file's own declarations as the program a visitor edits
47
47
  * (`examples/effects-playground/programs.ts`), on the published library.
48
48
  */
49
+ import { Animation } from "../core/animation.js";
49
50
  import { ColorDepth, ColorRgba, ColorSpec, contrastRatio, relativeLuminance } from "../core/color.js";
50
51
  import { EASES, Phase, beat } from "../core/easing.js";
51
52
  import { fbm, hash, noise, smoothstep } from "../core/noise.js";
52
53
  import { Memo } from "../core/memo.js";
53
54
  import { Oklch } from "../core/oklch.js";
54
55
  import { ensureContrast } from "../themes/colorMath.js";
55
- import { Effected, nextChangeOf, withNextChange } from "./effect.js";
56
+ import { Effected } from "./effect.js";
56
57
  /** The contrast no touch takes a cell below, unless it rested below it: WCAG AA for body text. */
57
58
  const LEGIBLE = 4.5;
58
59
  /**
@@ -182,14 +183,16 @@ function pairKey({ fg, bg, terminal, reverse }) {
182
183
  * says; a colour it gives none, and one the terminal supplies, stays as it is.
183
184
  */
184
185
  export function atShares(loop, share) {
185
- return (t, drawn) => {
186
+ return (drawn) => {
186
187
  const shared = share(drawn);
187
- const field = loop.field(t, drawn);
188
- return ({ fg, bg }, cell) => {
189
- const w = Math.round(clamp01(field(cell)) * LEVELS) / LEVELS;
190
- const moved = (color, terminal) => touchedAt(loop.touch, color, (terminal ? 0 : (shared.get(color.hex) ?? 0)) * w);
191
- return { fg: moved(fg, cell.terminal.fg), bg: moved(bg, cell.terminal.bg) };
192
- };
188
+ return Animation.continuous((t) => {
189
+ const field = loop.field(t, drawn);
190
+ return ({ fg, bg }, cell) => {
191
+ const w = Math.round(clamp01(field(cell)) * LEVELS) / LEVELS;
192
+ const moved = (color, terminal) => touchedAt(loop.touch, color, (terminal ? 0 : (shared.get(color.hex) ?? 0)) * w);
193
+ return { fg: moved(fg, cell.terminal.fg), bg: moved(bg, cell.terminal.bg) };
194
+ };
195
+ });
193
196
  };
194
197
  }
195
198
  /**
@@ -205,7 +208,7 @@ export function atShares(loop, share) {
205
208
  * loop on such a terminal draws it once.
206
209
  */
207
210
  function stillOnSlots(loop) {
208
- return withNextChange((t, drawn) => (onlySlots(drawn.depth) ? (colors) => colors : loop(t, drawn)), (t, drawn) => (onlySlots(drawn.depth) ? Number.POSITIVE_INFINITY : nextChangeOf(loop, t, drawn)));
211
+ return (drawn) => (onlySlots(drawn.depth) ? Animation.constant((colors) => colors) : loop(drawn));
209
212
  }
210
213
  /** Whether `depth` writes nothing between the slots it has: the sixteen, at either sixteen-colour depth, or none. */
211
214
  function onlySlots(depth) {
@@ -554,18 +557,19 @@ export function sparkle(curve, glow, z) {
554
557
  * hiding — 1 reaches fully invisible. A cell wholly shown is returned as it was.
555
558
  */
556
559
  export function veiled(visibility, swing) {
557
- return (t, drawn) => {
558
- const { theme } = drawn;
559
- const shows = visibility(t, drawn);
560
+ return (drawn) => {
560
561
  // [LAW:one-source-of-truth] The ground is the terminal `Effected` draws on, never a second one handed here.
561
- const ground = Oklch.fromRgba(theme.backgroundColor);
562
- return (colors, cell) => {
563
- const hidden = swing * (1 - clamp01(shows(cell)));
564
- if (hidden === 0)
565
- return colors;
566
- const bg = blend(Oklch.fromRgba(colors.bg), ground, hidden);
567
- return { fg: blend(Oklch.fromRgba(colors.fg), bg, hidden).toRgba(), bg: bg.toRgba() };
568
- };
562
+ const ground = Oklch.fromRgba(drawn.theme.backgroundColor);
563
+ return Animation.continuous((t) => {
564
+ const shows = visibility(t, drawn);
565
+ return (colors, cell) => {
566
+ const hidden = swing * (1 - clamp01(shows(cell)));
567
+ if (hidden === 0)
568
+ return colors;
569
+ const bg = blend(Oklch.fromRgba(colors.bg), ground, hidden);
570
+ return { fg: blend(Oklch.fromRgba(colors.fg), bg, hidden).toRgba(), bg: bg.toRgba() };
571
+ };
572
+ });
569
573
  };
570
574
  }
571
575
  /**
@@ -588,7 +592,18 @@ function order(z) {
588
592
  }
589
593
  function transition(curve, effect) {
590
594
  const done = (t) => t >= curve.seconds;
591
- return { effect: withNextChange(effect, (t) => (done(t) ? Number.POSITIVE_INFINITY : t)), done };
595
+ return {
596
+ effect: (drawn) => {
597
+ const moving = effect(drawn);
598
+ return {
599
+ at: (t) => moving.at(t),
600
+ nextChange: (t) => (done(t) ? Number.POSITIVE_INFINITY : moving.nextChange(t)),
601
+ // Done, the frame settled when the transition did; until then it is the effect's.
602
+ lastChange: (t) => (done(t) ? curve.seconds : moving.lastChange(t)),
603
+ };
604
+ },
605
+ done,
606
+ };
592
607
  }
593
608
  /**
594
609
  * Of a transition's `seconds`, how long each cell's own change takes. Under
@@ -735,7 +750,7 @@ export function wheel(curve, z) {
735
750
  };
736
751
  // A turned colour's bounds rest on what a frame draws, never on `t`: settled once for each.
737
752
  const settled = new WeakMap();
738
- return stillOnSlots((t, drawn) => {
753
+ return stillOnSlots((drawn) => {
739
754
  const { colors, fills, depth, theme } = drawn;
740
755
  // Whether a slot shows one of the element's colours: never the terminal's, whatever its hex.
741
756
  const own = (terminal, color) => !terminal && colors.has(color.hex);
@@ -752,21 +767,23 @@ export function wheel(curve, z) {
752
767
  // 256 colours writes a turned colour as one it has an index for; truecolor
753
768
  // as one of the two either side of its luminance.
754
769
  const indexed = measuredIndexed(depth, theme);
755
- const written = new Map();
756
- const choose = (color, segment) => {
757
- const turn = hueTurned(color, turned(segment, t));
758
- return nearestWithin(turn.looks, color, depth === ColorDepth.TRUECOLOR ? turn.either.map(measured) : indexed, bounds.get(keyOf(color, segment)) ?? UNBOUNDED);
759
- };
760
- const write = (color, segment) => {
761
- const found = written.get(keyOf(color, segment)) ?? choose(color, segment);
762
- written.set(keyOf(color, segment), found);
763
- return found;
764
- };
765
- return (shown, cell) => {
766
- const mine = { fg: own(cell.terminal.fg, shown.fg), bg: own(cell.terminal.bg, shown.bg) };
767
- const turn = (color, ours, other, theirs) => (ours ? write(color, segmentOf(color, other, theirs)) : color);
768
- return { fg: turn(shown.fg, mine.fg, shown.bg, mine.bg), bg: turn(shown.bg, mine.bg, shown.fg, mine.fg) };
769
- };
770
+ return Animation.continuous((t) => {
771
+ const written = new Map();
772
+ const choose = (color, segment) => {
773
+ const turn = hueTurned(color, turned(segment, t));
774
+ return nearestWithin(turn.looks, color, depth === ColorDepth.TRUECOLOR ? turn.either.map(measured) : indexed, bounds.get(keyOf(color, segment)) ?? UNBOUNDED);
775
+ };
776
+ const write = (color, segment) => {
777
+ const found = written.get(keyOf(color, segment)) ?? choose(color, segment);
778
+ written.set(keyOf(color, segment), found);
779
+ return found;
780
+ };
781
+ return (shown, cell) => {
782
+ const mine = { fg: own(cell.terminal.fg, shown.fg), bg: own(cell.terminal.bg, shown.bg) };
783
+ const turn = (color, ours, other, theirs) => (ours ? write(color, segmentOf(color, other, theirs)) : color);
784
+ return { fg: turn(shown.fg, mine.fg, shown.bg, mine.bg), bg: turn(shown.bg, mine.bg, shown.fg, mine.fg) };
785
+ };
786
+ });
770
787
  });
771
788
  }
772
789
  /**
@@ -890,7 +907,7 @@ function nearestOf(color, offered) {
890
907
  * The search is settled once for each ink, ground and step, so a frame
891
908
  * inside a step redoes none, and nor does a later pass of the beat: at sixty
892
909
  * frames a second a one-second step is sixty identical frames, and the
893
- * frame says it changes next when the step ends (`Beat.end`).
910
+ * frame changes when the beat does: the signal is the beat mapped to inks.
894
911
  * [LAW:no-ambient-temporal-coupling] The beat is `core/easing.ts`'s, and
895
912
  * nothing here reads a clock.
896
913
  */
@@ -900,15 +917,17 @@ export function steppedInk(steps) {
900
917
  // `steps` said whatever the caller does to its array.
901
918
  const inks = steps.map((step) => step.color);
902
919
  const settled = new WeakMap();
903
- return withNextChange((t, { depth, theme }) => {
904
- const step = inks[rhythm.at(t)];
920
+ return ({ depth, theme }) => {
905
921
  const memo = settled.get(theme) ?? new Memo();
906
922
  settled.set(theme, memo);
907
- return (colors, cell) => {
908
- const pair = { ...colors, terminal: cell.terminal, reverse: cell.reverse };
909
- return { fg: memo.get(`${depth} ${pairKey(pair)} ${step.hex}`, () => legibleInk(step, pair, depth, theme)), bg: colors.bg };
910
- };
911
- }, (t) => rhythm.end(t));
923
+ return Animation.map(rhythm, (i) => {
924
+ const step = inks[i];
925
+ return (colors, cell) => {
926
+ const pair = { ...colors, terminal: cell.terminal, reverse: cell.reverse };
927
+ return { fg: memo.get(`${depth} ${pairKey(pair)} ${step.hex}`, () => legibleInk(step, pair, depth, theme)), bg: colors.bg };
928
+ };
929
+ });
930
+ };
912
931
  }
913
932
  /**
914
933
  * The colour nearest `step` the depth writes that reads on `pair`'s ground