@promptctl/rich-js 0.25.2 → 0.27.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 (49) hide show
  1. package/dist/core/animation.d.ts +26 -8
  2. package/dist/core/animation.js +32 -11
  3. package/dist/core/color.d.ts +20 -11
  4. package/dist/core/color.js +26 -21
  5. package/dist/core/console.d.ts +8 -0
  6. package/dist/core/console.js +2 -1
  7. package/dist/core/easing.js +6 -1
  8. package/dist/core/oklch.d.ts +2 -0
  9. package/dist/core/oklch.js +3 -3
  10. package/dist/core/protocol.d.ts +26 -16
  11. package/dist/core/render.d.ts +1 -1
  12. package/dist/core/render.js +1 -0
  13. package/dist/core/view.d.ts +22 -8
  14. package/dist/core/view.js +32 -8
  15. package/dist/host/app.d.ts +4 -4
  16. package/dist/host/app.js +5 -5
  17. package/dist/index.d.ts +6 -5
  18. package/dist/renderables/effect.d.ts +44 -19
  19. package/dist/renderables/effect.js +20 -13
  20. package/dist/renderables/effects.d.ts +15 -9
  21. package/dist/renderables/effects.js +166 -34
  22. package/dist/renderables/lifecycle.d.ts +38 -28
  23. package/dist/renderables/lifecycle.js +85 -45
  24. package/dist/renderables/live.d.ts +3 -3
  25. package/dist/renderables/live.js +4 -4
  26. package/dist/renderables/progress.d.ts +11 -4
  27. package/dist/renderables/progress.js +29 -17
  28. package/dist/themes/buildPalette.d.ts +14 -39
  29. package/dist/themes/buildPalette.js +196 -67
  30. package/dist/themes/colorMath.d.ts +10 -4
  31. package/dist/themes/colorMath.js +33 -1
  32. package/dist/themes/data/types.d.ts +3 -1
  33. package/dist/themes/palette.d.ts +22 -6
  34. package/dist/themes/palette.js +13 -21
  35. package/dist/themes/registry.d.ts +2 -3
  36. package/dist/themes/registry.js +7 -26
  37. package/dist/themes/terminalThemes.d.ts +6 -5
  38. package/dist/themes/terminalThemes.js +6 -5
  39. package/dist/themes/transpose.d.ts +5 -10
  40. package/dist/themes/transpose.js +13 -24
  41. package/dist/widgets/event-router.js +2 -2
  42. package/dist/widgets/ink.d.ts +5 -4
  43. package/dist/widgets/ink.js +6 -9
  44. package/dist/widgets/text-input.d.ts +1 -1
  45. package/dist/widgets/text-input.js +41 -32
  46. package/dist/widgets/types.d.ts +2 -2
  47. package/dist/widgets/widget-app.d.ts +3 -3
  48. package/dist/widgets/widget-app.js +5 -5
  49. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  export { cellLen, setCellSize, splitText, chopCells, cellFit, graphemes, asCellCol, asCodePoint, } from "./core/cells.js";
2
2
  export type { CellCol, CodeUnit, CodePoint } from "./core/cells.js";
3
- export { ColorRgba, ColorTable, ColorDepth, ColorSpec, ColorParseError, TerminalTheme, parseRgbHex, parseRgbaHex, parseHexColor, blendRgb, resolveColorSystem, detectColorSystem, resolveDestination, STANDARD_TABLE, EIGHT_BIT_TABLE, WINDOWS_TABLE, ANSI_COLOR_NAMES, } from "./core/color.js";
3
+ export { ColorRgba, ColorTable, ColorDepth, ColorSpec, ColorParseError, TerminalTheme, type TerminalColors, parseRgbHex, parseRgbaHex, parseHexColor, blendRgb, resolveColorSystem, detectColorSystem, resolveDestination, STANDARD_TABLE, EIGHT_BIT_TABLE, WINDOWS_TABLE, ANSI_COLOR_NAMES, } from "./core/color.js";
4
4
  export type { ColorSystemName, DetectColorOptions, Destination, DestinationOptions } from "./core/color.js";
5
5
  export { Oklch, IDENTITY, INVERT_LIGHTNESS, isIdentityKey, } from "./core/oklch.js";
6
6
  export type { ThemeKey, OklchWeights } from "./core/oklch.js";
7
7
  export { Palette } from "./themes/palette.js";
8
+ export type { AccentRole, BaseRole, PaletteRole, PaletteRoles, ShadedRole } from "./themes/palette.js";
8
9
  export { resolveColorRef, ColorRefError } from "./themes/colorRef.js";
9
10
  export { buildPalette } from "./themes/buildPalette.js";
10
- export type { BaseColors } from "./themes/buildPalette.js";
11
+ export type { BaseColors, StatedRoles } from "./themes/buildPalette.js";
11
12
  export { DEFAULT_TERMINAL_THEME, MONOKAI, SVG_EXPORT_THEME, NORD, GRUVBOX, DRACULA, TOKYO_NIGHT, FLEXOKI, CYBERPUNK, CATPPUCCIN_MOCHA, CATPPUCCIN_LATTE, CATPPUCCIN_FRAPPE, CATPPUCCIN_MACCHIATO, SOLARIZED_DARK, SOLARIZED_LIGHT, ROSE_PINE, ROSE_PINE_MOON, ROSE_PINE_DAWN, ATOM_ONE_DARK, ATOM_ONE_LIGHT, TEXTUAL_DARK, TEXTUAL_LIGHT, TEXTUAL_ANSI, } from "./themes/terminalThemes.js";
12
13
  export { getThemePalette, listThemePalettes } from "./themes/registry.js";
13
14
  export type { ThemeName } from "./themes/registry.js";
@@ -33,7 +34,7 @@ export { Box } from "./core/box.js";
33
34
  export type { EdgeChars, ContentChars, RowLevel, SubstituteOptions } from "./core/box.js";
34
35
  export { ASCII, ASCII2, ASCII_DOUBLE_HEAD, SQUARE, SQUARE_DOUBLE_HEAD, MINIMAL, MINIMAL_HEAVY_HEAD, MINIMAL_DOUBLE_HEAD, SIMPLE, SIMPLE_HEAD, SIMPLE_HEAVY, HORIZONTALS, ROUNDED, HEAVY, HEAVY_EDGE, HEAVY_HEAD, DOUBLE, DOUBLE_EDGE, MARKDOWN, } from "./core/box.js";
35
36
  export { render, isRenderable, isMeasurable, withCellWidth, withBoundedWidth, insetHeight, stackedHeight, regionRows, fitHeight, getStyle, } from "./core/protocol.js";
36
- export type { RenderOptions, OverflowMethod, Height, Renderable, Measurable, Presence, StyleErrorHandler, FrameHost, } from "./core/protocol.js";
37
+ export type { RenderOptions, OverflowMethod, Height, Renderable, Measurable, Presence, StyleErrorHandler, FrameHost, HostFrame, } from "./core/protocol.js";
37
38
  export type { Unsubscribe } from "./core/subscription.js";
38
39
  export { Measurement, measureRenderables } from "./core/measure.js";
39
40
  export { Span, RichText } from "./core/text.js";
@@ -61,13 +62,13 @@ export type { ConsoleOptions, ConsoleSink, ConsoleStream, ConsoleEnvironment, Li
61
62
  export type { Env } from "./core/env.js";
62
63
  export { Constrain } from "./renderables/constrain.js";
63
64
  export { Effected, confined, drawnOf } from "./renderables/effect.js";
64
- export type { CellAttributes, CellColors, CellLook, Drawn, Effect, EffectCell, EffectedOptions, EffectFrame, Pair, Placed } from "./renderables/effect.js";
65
+ export type { CellAttributes, CellColors, CellLook, Drawn, Effect, EffectCell, EffectedOptions, EffectFrame, ListedCell, Pair, Placed } from "./renderables/effect.js";
65
66
  export { ATTRIBUTE_DEFAULTS, EFFECT_CATALOGUE, EFFECT_CURVES, EFFECT_LIGHTS, blink, dissolveOut, fadeIn, gradient, onBeat, onColors, pulse, shimmer, sparkle, steppedInk, wheel, } from "./renderables/effects.js";
66
67
  export type { AttributeLoopName, Blink, ColorStep, Curve, EffectTarget, Field, Loop, LoopName, Touch, Transition, TransitionName } from "./renderables/effects.js";
67
68
  export { crossfade, wipe } from "./renderables/change.js";
68
69
  export type { Between, Change } from "./renderables/change.js";
69
70
  export { lifecycle, throughColors, throughGlyphs } from "./renderables/lifecycle.js";
70
- export type { Lifecycle, LifecycleOptions, Veil } from "./renderables/lifecycle.js";
71
+ export type { Half, Lifecycle, LifecycleOptions } from "./renderables/lifecycle.js";
71
72
  export { scroll } from "./renderables/scroll.js";
72
73
  export type { ScrollOptions } from "./renderables/scroll.js";
73
74
  export { GlyphEffected } from "./renderables/glyph-effect.js";
@@ -71,7 +71,7 @@
71
71
  import { type Anchor } from "../core/anchor.js";
72
72
  import { Animation } from "../core/animation.js";
73
73
  import type { Hold } from "../core/style.js";
74
- import { ColorDepth, type ColorRgba, type TerminalTheme } from "../core/color.js";
74
+ import { ColorDepth, type ColorRgba, type TerminalColors } from "../core/color.js";
75
75
  import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
76
76
  import { Segment } from "../core/segment.js";
77
77
  import { type Attributes } from "../core/style.js";
@@ -125,6 +125,20 @@ export interface EffectCell {
125
125
  }
126
126
  /** Where a cell sits and who drew it: an `EffectCell` before it has a seed or colours. */
127
127
  export type Placed = Pick<EffectCell, "row" | "col" | "anchor">;
128
+ /**
129
+ * A cell as `Drawn.all` lists it: where it sits and who drew it, the
130
+ * columns its glyph takes, its ink and ground as the screen shows them, and
131
+ * whether a strip's joiner drew it — a seam, no item's, whose colours are its
132
+ * neighbours' edges. Each is read off the one look and the one walk the cell
133
+ * is later handed with, so an effect that groups cells into the runs they
134
+ * are drawn in — `wheel`'s segments — reads what the frame hands, never a
135
+ * second measure. [LAW:one-source-of-truth]
136
+ */
137
+ export interface ListedCell extends Placed {
138
+ readonly width: number;
139
+ readonly pair: Pair;
140
+ readonly seam: boolean;
141
+ }
128
142
  /**
129
143
  * A cell's ink and ground as the screen shows them, which of the two are the
130
144
  * terminal's, and whether its style swaps them (`reverse`): what an `Effect`
@@ -149,19 +163,28 @@ export interface Pair extends CellColors {
149
163
  */
150
164
  export interface Drawn {
151
165
  /**
152
- * Every cell the element shows something in this frame — a glyph, a
153
- * ground that is not the terminal's, or a line its style draws — where it
154
- * sits and who drew it, listed before the effect is handed any: what
155
- * an effect spread over the element — a transition, which has its last cell
156
- * settle as it ends — is spread over. A blank cell on the terminal's ground
157
- * that draws no line is handed but not listed, since nothing in it shows,
158
- * and a strip's join cell painted from the items beside it is listed but
159
- * never handed its own look, since it follows them: it is handed only a
160
- * neighbour's edge the frame does not show, to be painted where the seam
161
- * shows it. `confined` narrows it to the cells its owners drew. A call, so a
162
- * frame whose effect does not ask lists none.
166
+ * Every cell the element draws this frame, shown or not — where it sits
167
+ * and who drew it, what it shows and how wide it is (`ListedCell`) — in
168
+ * the order it draws them, listed before the effect is handed any. A blank
169
+ * cell on the terminal's ground that draws no line is here: the effect is
170
+ * handed it, and its ink, unseen, is the ink of the words beside it, so an
171
+ * effect that turns the element's runs each at their own pace — `wheel` —
172
+ * reads the runs off this and a styled sentence is one run across its
173
+ * spaces. A strip's join cell painted from the items beside it is here,
174
+ * as a `seam`, but never handed its own look, since it follows them: it is
175
+ * handed only a neighbour's edge the frame does not show, to be painted
176
+ * where the seam shows it. `confined` narrows it to the cells its owners
177
+ * drew. A call, so a frame whose effect does not ask lists none.
178
+ */
179
+ all(): readonly ListedCell[];
180
+ /**
181
+ * Of `all`, every cell the element shows something in — a glyph, a ground
182
+ * that is not the terminal's, or a line its style draws: what an effect
183
+ * spread over the element — a transition, which has its last cell settle
184
+ * as it ends — is spread over. A blank cell on the terminal's ground that
185
+ * draws no line is handed but not here, since nothing in it shows.
163
186
  */
164
- cells(): readonly Placed[];
187
+ cells(): readonly ListedCell[];
165
188
  /** Every distinct ink and ground of a word cell. */
166
189
  readonly pairs: readonly Pair[];
167
190
  /** The colours the element sets itself in its word cells, by hex: never the terminal's, whatever its hex. */
@@ -171,7 +194,7 @@ export interface Drawn {
171
194
  /** The depth the frame is written at: below truecolor a moved colour lands on a palette colour. */
172
195
  readonly depth: ColorDepth;
173
196
  /** The terminal the frame is drawn on. */
174
- readonly theme: TerminalTheme;
197
+ readonly theme: TerminalColors;
175
198
  }
176
199
  /** What an effect does to the cells of one frame: each cell's look and where it is in, its look out. Return `look` to leave a cell be. */
177
200
  export type EffectFrame = (look: CellLook, cell: EffectCell) => CellLook;
@@ -201,7 +224,7 @@ export interface EffectedOptions {
201
224
  * drawn in, and the sixteen an ANSI colour names. The terminal's own palette
202
225
  * is never queried, so the caller says what it is.
203
226
  */
204
- readonly theme: TerminalTheme;
227
+ readonly theme: TerminalColors;
205
228
  }
206
229
  /** A frame an effect drew at a moment, and the hold the effect says it is in there. */
207
230
  export interface Moved {
@@ -220,7 +243,7 @@ export declare class Recolored implements Renderable, Measurable {
220
243
  readonly effect: Effect;
221
244
  readonly t: number;
222
245
  readonly key: string;
223
- readonly theme: TerminalTheme;
246
+ readonly theme: TerminalColors;
224
247
  constructor(renderable: Renderable, effect: Effect, options: EffectedOptions & {
225
248
  readonly t: number;
226
249
  });
@@ -264,7 +287,7 @@ export declare class Effected implements Animation<Renderable> {
264
287
  readonly renderable: Renderable;
265
288
  readonly effect: Effect;
266
289
  readonly key: string;
267
- readonly theme: TerminalTheme;
290
+ readonly theme: TerminalColors;
268
291
  constructor(renderable: Renderable, effect: Effect, options: EffectedOptions);
269
292
  /** The element through the effect at `t`, unstamped. */
270
293
  at(t: number): Recolored;
@@ -285,7 +308,7 @@ export declare class Effected implements Animation<Renderable> {
285
308
  * [LAW:one-source-of-truth] Read through `movedSpecs`, the one writer a
286
309
  * moved slot is written with.
287
310
  */
288
- static shows(pair: Pair, to: CellColors, depth: ColorDepth, theme: TerminalTheme): CellColors;
311
+ static shows(pair: Pair, to: CellColors, depth: ColorDepth, theme: TerminalColors): CellColors;
289
312
  }
290
313
  /**
291
314
  * A cell's seed: FNV-1a of the element's key and the cell's position, scaled
@@ -297,6 +320,8 @@ export declare function cellSeed(key: string, row: number, col: number): number;
297
320
  export interface WalkedGlyph {
298
321
  readonly glyph: string;
299
322
  readonly cell: Placed | undefined;
323
+ /** The columns the glyph takes: the one measure the walk steps by, so a listed cell's width is the walk's. [LAW:one-source-of-truth] */
324
+ readonly width: number;
300
325
  }
301
326
  /**
302
327
  * Each glyph of each of `segments`, where its cell sits: rows and columns
@@ -330,7 +355,7 @@ export declare function confined(effect: Effect, ...owners: readonly object[]):
330
355
  * as `Effected` measures its child for every frame. Measure an element's
331
356
  * output under an effect to read the contrast its words are left at.
332
357
  */
333
- export declare function drawnOf(segments: Iterable<Segment>, depth: ColorDepth, theme: TerminalTheme): Drawn;
358
+ export declare function drawnOf(segments: Iterable<Segment>, depth: ColorDepth, theme: TerminalColors): Drawn;
334
359
  /**
335
360
  * The attributes a look carries, read off anything that sets them — a
336
361
  * `Style`, or a look a frame returned with more on it than `CellAttributes`
@@ -362,7 +362,7 @@ function looks(segments, depth, theme) {
362
362
  const slots = { fg: (wire.color ?? DEFAULT).getTruecolor(theme, true), bg: (wire.bgcolor ?? DEFAULT).getTruecolor(theme, false) };
363
363
  const reverse = style.reverse === true;
364
364
  const terminal = onScreen({ fg: wire.color?.isDefault ?? true, bg: wire.bgcolor?.isDefault ?? true }, reverse);
365
- return { segment, look: { style, wire, slots, pair: { ...onScreen(slots, reverse), terminal, reverse }, attributes: cellAttributes(style) } };
365
+ return { segment, look: { style, wire, slots, pair: { ...onScreen(slots, reverse), terminal, reverse }, attributes: cellAttributes(style), seam: drawnBy(style.anchor, JOIN) } };
366
366
  });
367
367
  }
368
368
  /**
@@ -385,9 +385,10 @@ export function walkGlyphs(segments) {
385
385
  let into = 0;
386
386
  return [...graphemes(segment.text)].map((glyph) => {
387
387
  const cell = glyph === "\n" ? undefined : { row, col, anchor: anchor && shiftAnchor(anchor, into) };
388
- [row, col] = glyph === "\n" ? [row + 1, 0] : [row, col + cellLen(glyph)];
389
- into += cellLen(glyph);
390
- return { glyph, cell };
388
+ const width = cellLen(glyph);
389
+ [row, col] = glyph === "\n" ? [row + 1, 0] : [row, col + width];
390
+ into += width;
391
+ return { glyph, cell, width };
391
392
  });
392
393
  });
393
394
  }
@@ -448,27 +449,33 @@ function drawsLines(style) {
448
449
  return [style.underline, style.underline2, style.strike, style.overline, style.frame, style.encircle].some((on) => on === true);
449
450
  }
450
451
  /**
451
- * What `looked` draws: `Drawn`'s measure, taken once per frame. Its `cells`
452
- * are listed off `walked` the first time they are asked for, since only an
453
- * effect spread over the element asks, and a glyph walk is what measuring the
454
- * words alone, a segment at a time, is spared (`drawnOf`).
452
+ * What `looked` draws: `Drawn`'s measure, taken once per frame. Its `all`
453
+ * and `cells` are listed off `walked` the first time either is asked for,
454
+ * since only an effect spread over the element or turning its runs asks, and
455
+ * a glyph walk is what measuring the words alone, a segment at a time, is
456
+ * spared (`drawnOf`).
455
457
  */
456
458
  function measured(looked, walked, depth, theme) {
457
459
  const ground = theme.backgroundColor.hex;
458
- let listed;
460
+ // [LAW:one-source-of-truth] One listing, each cell with whether it shows; `all` and `cells` are read off it.
461
+ let everything;
459
462
  const list = () => walked().flatMap(({ look, glyphs }) => {
460
463
  if (look === undefined)
461
464
  return [];
462
465
  // A blank glyph shows its ground, and any line its style draws in its ink.
463
466
  const blankShows = look.pair.bg.hex !== ground || drawsLines(look.style);
464
- return glyphs.flatMap(({ glyph, cell }) => (cell !== undefined && (inked(glyph) || blankShows) ? [cell] : []));
467
+ return glyphs.flatMap(({ glyph, cell, width }) => (cell === undefined ? [] : [{ cell: { ...cell, width, pair: look.pair, seam: look.seam }, shows: inked(glyph) || blankShows }]));
465
468
  });
466
- const words = looked.flatMap(({ segment, look }) => (look !== undefined && inked(segment.text) && !drawnBy(look.style.anchor, JOIN) ? [look.pair] : []));
469
+ const listed = () => (everything ??= list());
470
+ let all;
471
+ let shown;
472
+ const words = looked.flatMap(({ segment, look }) => (look !== undefined && inked(segment.text) && !look.seam ? [look.pair] : []));
467
473
  const pairs = [...new Map(words.map((pair) => [`${pair.fg.hex}/${pair.bg.hex}/${pair.terminal.fg}/${pair.terminal.bg}/${pair.reverse}`, pair])).values()];
468
474
  // [LAW:one-source-of-truth] A slot the terminal fills is no colour of the element's, whatever its hex.
469
475
  const own = (slot) => pairs.flatMap((pair) => (pair.terminal[slot] ? [] : [pair[slot].hex]));
470
476
  return {
471
- cells: () => (listed ??= list()),
477
+ all: () => (all ??= listed().map(({ cell }) => cell)),
478
+ cells: () => (shown ??= listed().flatMap(({ cell, shows }) => (shows ? [cell] : []))),
472
479
  pairs,
473
480
  colors: new Set([...own("fg"), ...own("bg")]),
474
481
  fills: new Set(own("bg")),
@@ -496,7 +503,7 @@ export function confined(effect, ...owners) {
496
503
  return false;
497
504
  };
498
505
  // The frame as the effect sees it: the owners' cells alone. [LAW:one-source-of-truth] One narrowing, handed to both stages.
499
- const narrowed = (drawn) => ({ ...drawn, cells: () => drawn.cells().filter(owned) });
506
+ const narrowed = (drawn) => ({ ...drawn, all: () => drawn.all().filter(owned), cells: () => drawn.cells().filter(owned) });
500
507
  // The owners' cells change when the effect's frame does.
501
508
  return (drawn) => Animation.map(effect(narrowed(drawn)), (frame) => (look, cell) => (owned(cell) ? frame(look, cell) : look));
502
509
  }
@@ -339,15 +339,21 @@ export declare function dissolveOut(curve: Curve, z: number): Transition;
339
339
  * way round once a period — far too slowly to be seen moving, so a glance
340
340
  * sees a steady palette and an hour's absence a different one — each
341
341
  * segment of the element at its own pace. Swing: how far the segments stray
342
- * from one another, 0–1, 1 being half a turn apart. A segment is a fill of
343
- * the element's own — one of `fills`, the colours it draws as a ground — and
344
- * it turns at its own pace wherever it is drawn: as the ground of its cells,
345
- * and as the ink of the seam glyph pointing out of it into the next, so a
346
- * powerline cap keeps the colour of the cell it caps. An ink that is no fill
347
- * turns with the ground it sits on, so a cell's words and fill turn
348
- * together; on the terminal's ground, a run of text's ink is a segment of
349
- * its own. A segment's stray is slow noise of its own, at `STRAY_PERIODS`,
350
- * so no two keep step, and the row never shows one spread of hues twice.
342
+ * from one another, 0–1, 1 being half a turn apart. A segment is a region
343
+ * of adjacent cells of one colour of the element's own, carried as their
344
+ * ground or, on the terminal's ground, as their ink (`segmentsOf`), and it
345
+ * paces by where it sits, not by its colour: two
346
+ * regions of one fill at two places turn apart, and each colour of the seam
347
+ * a joiner drew between them turns with the region of that colour beside
348
+ * it, so a powerline cap keeps the colour of the cell it caps. An ink that
349
+ * is no fill turns with the ground it sits on, so a cell's words and fill
350
+ * turn together. A segment's stray is slow noise at its place, at
351
+ * `STRAY_PERIODS` in time and `STRAY_SPREAD` across the element, so
352
+ * neighbours pace a little apart and distant segments on their own, the
353
+ * row never shows one spread of hues twice, and a segment that grows by a
354
+ * column moves its hue a little, and those it pushes along as little. Only the
355
+ * stray reads the place: a place in the shared turn would jump a growing
356
+ * run by `360·t·Δ/P`, without bound in `t`.
351
357
  * A colour keeps its luminance as it turns (`hueTurned`), so every pair of
352
358
  * words reads at the contrast it rested at; at 256 colours a turned colour
353
359
  * lands on a colour the depth writes, one that keeps every pair of words it
@@ -675,7 +675,7 @@ export class Transition {
675
675
  this.schedule = schedule;
676
676
  }
677
677
  done(t) {
678
- return this.schedule.nextChange(t) === Number.POSITIVE_INFINITY;
678
+ return Animation.done(this.schedule, t);
679
679
  }
680
680
  }
681
681
  /**
@@ -685,8 +685,13 @@ export class Transition {
685
685
  * glyph effect (`./glyph-effects.ts`) alike.
686
686
  */
687
687
  export function transition(seconds, effect) {
688
- // Moving every frame until `seconds`, then never: the sequence carries the switch.
689
- const schedule = Animation.sequence(Animation.continuous(() => undefined), seconds, Animation.constant(undefined));
688
+ // Moving every frame until `seconds`, then never: the sequence carries the switch. [LAW:one-source-of-truth]
689
+ // Reduced, a one-shot cuts from its start to its end at 0, and its schedule says so too, the one word on
690
+ // when the reduced frames change, so a half that lends this schedule to its frames tells the same hold.
691
+ const schedule = Animation.of({
692
+ ...Animation.sequence(Animation.continuous(() => undefined), seconds, Animation.constant(undefined)),
693
+ reduced: () => Animation.cut(undefined, undefined),
694
+ });
690
695
  const ran = new Transition((drawn) => {
691
696
  const moving = effect(drawn);
692
697
  return Animation.of({
@@ -813,20 +818,135 @@ export function dissolveOut(curve, z) {
813
818
  const STRAY_PERIODS = [211, 337];
814
819
  /** How far a segment may stray from the shared turn at swing 1, in degrees: half a turn — fully apart. */
815
820
  const STRAY = 180;
821
+ /**
822
+ * The columns between two places at which their strays no longer pace
823
+ * alike: the noise a stray rides on is sampled at the place over this many
824
+ * columns a unit, so a strip's items, a dozen columns apart, pace a little
825
+ * apart and segments a screen apart pace on their own. Rows are the same
826
+ * distance apart as columns.
827
+ */
828
+ const STRAY_SPREAD = 24;
829
+ /** `cell` as a place of its own, as wide as its glyph: where no region covers it. */
830
+ const ownPlace = (cell, width) => ({ row: cell.row, centre: cell.col + width / 2 });
831
+ /**
832
+ * The segments `drawn` draws, as where each colour of each cell turns: a
833
+ * segment is a region of adjacent cells — beside or above one another — of
834
+ * one colour of the element's own, each cell carrying it as its ground or,
835
+ * on the terminal's ground, as its ink, read off `Drawn.all` once a frame.
836
+ * So a panel's ground is one segment whatever words lie on it, a blank
837
+ * carrying the ink keeps a styled paragraph one segment across its spaces
838
+ * and lines, a caption in a bar's colour written under the bar turns with
839
+ * it, and a blank in the terminal's ink splits a segment. A seam a joiner
840
+ * drew is in none, since its colours are its neighbours' edges: each colour
841
+ * of a seam cell turns with the region of that colour past the seam on
842
+ * either side — a two-cell rail reads past its other cell — so a powerline
843
+ * cap keeps the colour of the cell it caps, a custom joiner's handed cap
844
+ * included, and with the cell's own place where none is beside it — a
845
+ * neighbour's edge the frame does not show, handed at the seam's cell. Any
846
+ * other cell's colours turn with the region covering it: a letter in a
847
+ * neighbour's fill colour is a word, not a cap. A cell the frame lists
848
+ * nowhere, which a hand-built `Drawn` hands, is one column wide. The place
849
+ * is the region's centre column from the element's left edge, with its
850
+ * centre row: a region that grows by a column moves its centre by half of
851
+ * one, and those it pushes along by a column theirs by one, where a share
852
+ * of the element's extent would move every region's place with any
853
+ * region's growth.
854
+ */
855
+ function segmentsOf(drawn) {
856
+ const { colors } = drawn;
857
+ const at = (row, col) => `${row}:${col}`;
858
+ const listed = new Map();
859
+ // Every column a seam cell covers: what a seam's colours look past to the region beside the seam.
860
+ const seams = new Set();
861
+ // The colour a cell is a region of: its own ground, else on the terminal's ground its own ink, else none.
862
+ const regionColour = (cell) => {
863
+ if (cell.seam)
864
+ return undefined;
865
+ const own = (terminal, hex) => (!terminal && colors.has(hex) ? hex : undefined);
866
+ return own(cell.pair.terminal.bg, cell.pair.bg.hex) ?? own(cell.pair.terminal.fg, cell.pair.fg.hex);
867
+ };
868
+ // Every column a region's cell covers, in walk order, indexed by where it is.
869
+ const covered = [];
870
+ const index = new Map();
871
+ for (const cell of drawn.all()) {
872
+ listed.set(at(cell.row, cell.col), cell);
873
+ const hex = regionColour(cell);
874
+ for (let col = cell.col; col < cell.col + cell.width; col++) {
875
+ if (cell.seam)
876
+ seams.add(at(cell.row, col));
877
+ if (hex === undefined)
878
+ continue;
879
+ index.set(at(cell.row, col), covered.length);
880
+ covered.push({ row: cell.row, col, hex });
881
+ }
882
+ }
883
+ // Union-find over the covered columns: two of one colour beside or above one another are one region.
884
+ const parent = covered.map((_, i) => i);
885
+ const find = (i) => {
886
+ while (parent[i] !== i) {
887
+ parent[i] = parent[parent[i]];
888
+ i = parent[i];
889
+ }
890
+ return i;
891
+ };
892
+ const joinWith = (i, row, col) => {
893
+ const j = index.get(at(row, col));
894
+ if (j !== undefined && covered[j].hex === covered[i].hex)
895
+ parent[find(i)] = find(j);
896
+ };
897
+ covered.forEach(({ row, col }, i) => {
898
+ joinWith(i, row, col - 1);
899
+ joinWith(i, row - 1, col);
900
+ });
901
+ const regions = new Map();
902
+ covered.forEach(({ row, col, hex }, i) => {
903
+ const root = find(i);
904
+ const region = regions.get(root);
905
+ if (region === undefined)
906
+ regions.set(root, { hex, top: row, bottom: row, left: col, right: col });
907
+ else
908
+ [region.top, region.bottom, region.left, region.right] = [Math.min(region.top, row), Math.max(region.bottom, row), Math.min(region.left, col), Math.max(region.right, col)];
909
+ });
910
+ const regionAt = (row, col) => {
911
+ const i = index.get(at(row, col));
912
+ return i === undefined ? undefined : regions.get(find(i));
913
+ };
914
+ const placeOf = (region) => ({ row: (region.top + region.bottom) / 2, centre: (region.left + region.right + 1) / 2 });
915
+ // The region past the seam from `col` in `step`'s direction: what a seam's colour turns with.
916
+ const pastSeam = (row, col, step) => {
917
+ let past = col;
918
+ while (seams.has(at(row, past)))
919
+ past += step;
920
+ return regionAt(row, past);
921
+ };
922
+ return (cell, hex) => {
923
+ const own = listed.get(at(cell.row, cell.col));
924
+ const width = own?.width ?? 1;
925
+ const beside = (own?.seam === true ? [pastSeam(cell.row, cell.col - 1, -1), pastSeam(cell.row, cell.col + width, 1)] : []).find((region) => region?.hex === hex);
926
+ const region = beside ?? regionAt(cell.row, cell.col);
927
+ return region === undefined ? ownPlace(cell, width) : placeOf(region);
928
+ };
929
+ }
816
930
  /**
817
931
  * Wheel, as the colour of daylight going round: every hue turning the whole
818
932
  * way round once a period — far too slowly to be seen moving, so a glance
819
933
  * sees a steady palette and an hour's absence a different one — each
820
934
  * segment of the element at its own pace. Swing: how far the segments stray
821
- * from one another, 0–1, 1 being half a turn apart. A segment is a fill of
822
- * the element's own — one of `fills`, the colours it draws as a ground — and
823
- * it turns at its own pace wherever it is drawn: as the ground of its cells,
824
- * and as the ink of the seam glyph pointing out of it into the next, so a
825
- * powerline cap keeps the colour of the cell it caps. An ink that is no fill
826
- * turns with the ground it sits on, so a cell's words and fill turn
827
- * together; on the terminal's ground, a run of text's ink is a segment of
828
- * its own. A segment's stray is slow noise of its own, at `STRAY_PERIODS`,
829
- * so no two keep step, and the row never shows one spread of hues twice.
935
+ * from one another, 0–1, 1 being half a turn apart. A segment is a region
936
+ * of adjacent cells of one colour of the element's own, carried as their
937
+ * ground or, on the terminal's ground, as their ink (`segmentsOf`), and it
938
+ * paces by where it sits, not by its colour: two
939
+ * regions of one fill at two places turn apart, and each colour of the seam
940
+ * a joiner drew between them turns with the region of that colour beside
941
+ * it, so a powerline cap keeps the colour of the cell it caps. An ink that
942
+ * is no fill turns with the ground it sits on, so a cell's words and fill
943
+ * turn together. A segment's stray is slow noise at its place, at
944
+ * `STRAY_PERIODS` in time and `STRAY_SPREAD` across the element, so
945
+ * neighbours pace a little apart and distant segments on their own, the
946
+ * row never shows one spread of hues twice, and a segment that grows by a
947
+ * column moves its hue a little, and those it pushes along as little. Only the
948
+ * stray reads the place: a place in the shared turn would jump a growing
949
+ * run by `360·t·Δ/P`, without bound in `t`.
830
950
  * A colour keeps its luminance as it turns (`hueTurned`), so every pair of
831
951
  * words reads at the contrast it rested at; at 256 colours a turned colour
832
952
  * lands on a colour the depth writes, one that keeps every pair of words it
@@ -839,46 +959,57 @@ export function wheel(curve, z) {
839
959
  requireCurve(curve);
840
960
  requireZ("wheel", z);
841
961
  const P = curve.seconds;
842
- // A segment's seed, from the colour it is.
843
- const seed = (hex) => hash(Number.parseInt(hex.slice(1), 16) * 1e-4, 17.3 + z);
844
- 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);
962
+ // A segment's stray at its place. The fractions of the `y` offsets, .9
963
+ // and .3, are ones no centre — a half-integer over `STRAY_SPREAD` columns
964
+ // — reaches, so no sample's `y` is whole: a sample with a whole `y` and
965
+ // `z` sits on a lattice line along `t`, where the noise is zero at every
966
+ // multiple of the period and every segment on it would return to the
967
+ // shared turn together. `z` needs no such offset; its two differ so the
968
+ // two noises are read from different fields.
969
+ const stray = ({ row, centre }, t) => noise(t / STRAY_PERIODS[0], centre / STRAY_SPREAD + 5.9, z + row / STRAY_SPREAD + 2.3) + 0.5 * noise(t / STRAY_PERIODS[1], centre / STRAY_SPREAD + 97.3, z + row / STRAY_SPREAD + 7.1);
845
970
  // Measured from where it stood at the start, as pulse's wander is, so the
846
971
  // first frame turns nothing and the second has turned only a frame's worth.
847
- const turned = (hex, t) => {
848
- const s = seed(hex);
849
- return (360 * t) / P + curve.swing * STRAY * ((stray(s, t) - stray(s, 0)) / 1.5);
850
- };
972
+ const turned = (place, t) => (360 * t) / P + curve.swing * STRAY * ((stray(place, t) - stray(place, 0)) / 1.5);
851
973
  // A turned colour's bounds rest on what a frame draws, never on `t`: settled once for each.
852
974
  const settled = new WeakMemo();
853
975
  return stillOnSlots((drawn) => {
854
976
  const { colors, fills, depth, theme } = drawn;
855
977
  // Whether a slot shows one of the element's colours: never the terminal's, whatever its hex.
856
978
  const own = (terminal, color) => !terminal && colors.has(color.hex);
857
- // The segment an own colour turns with: itself if a fill or beside no own colour, else the one beside it.
858
- const segmentOf = (color, other, theirs) => (fills.has(color.hex) || !theirs ? color.hex : other.hex);
859
- // A colour as it turns with a segment: written one way wherever it is drawn in a frame.
860
- const keyOf = (color, segment) => `${color.hex} ${segment}`;
979
+ // The colour an own colour is bounded with, under which its bounds are
980
+ // kept: itself if a fill or beside no own colour, else the ground beside
981
+ // it. Where it turns is `segmentsOf`'s, not this.
982
+ const boundWith = (color, other, theirs) => (fills.has(color.hex) || !theirs ? color.hex : other.hex);
983
+ // The bounds a colour turns within, by the colour it is bounded with: one bound for every place a frame writes it at.
984
+ const keyOf = (color, bound) => `${color.hex} ${bound}`;
861
985
  const bounds = settled.get(theme, () => new Memo()).get(drawnKey(drawn), () => turnedBounds(drawn.pairs, depth, (pair, side) => {
862
986
  const other = side === "fg" ? "bg" : "fg";
863
- return own(pair.terminal[side], pair[side]) ? keyOf(pair[side], segmentOf(pair[side], pair[other], own(pair.terminal[other], pair[other]))) : undefined;
987
+ return own(pair.terminal[side], pair[side]) ? keyOf(pair[side], boundWith(pair[side], pair[other], own(pair.terminal[other], pair[other]))) : undefined;
864
988
  }));
865
989
  // 256 colours writes a turned colour as one it has an index for; truecolor
866
990
  // as one of the two either side of its luminance.
867
991
  const indexed = measuredIndexed(depth, theme);
992
+ // Where each cell's colours turn, read off the frame's listing once.
993
+ const placeOf = segmentsOf(drawn);
868
994
  return Animation.continuous((t) => {
995
+ // [LAW:one-source-of-truth] A colour turned at a place within a
996
+ // segment's bounds is written once a frame, wherever it falls: the
997
+ // key is the bounds' key with the place, so one entry never serves
998
+ // two bounds.
869
999
  const written = new Map();
870
- const choose = (color, segment) => {
871
- const turn = hueTurned(color, turned(segment, t));
872
- return nearestWithin(turn.looks, color, depth === ColorDepth.TRUECOLOR ? turn.either.map(measured) : indexed, bounds.get(keyOf(color, segment)) ?? UNBOUNDED);
1000
+ const choose = (color, bound, place) => {
1001
+ const turn = hueTurned(color, turned(place, t));
1002
+ return nearestWithin(turn.looks, color, depth === ColorDepth.TRUECOLOR ? turn.either.map(measured) : indexed, bounds.get(keyOf(color, bound)) ?? UNBOUNDED);
873
1003
  };
874
- const write = (color, segment) => {
875
- const found = written.get(keyOf(color, segment)) ?? choose(color, segment);
876
- written.set(keyOf(color, segment), found);
1004
+ const write = (color, bound, place) => {
1005
+ const key = `${keyOf(color, bound)} ${place.row}:${place.centre}`;
1006
+ const found = written.get(key) ?? choose(color, bound, place);
1007
+ written.set(key, found);
877
1008
  return found;
878
1009
  };
879
1010
  return (shown, cell) => {
880
1011
  const mine = { fg: own(cell.terminal.fg, shown.fg), bg: own(cell.terminal.bg, shown.bg) };
881
- const turn = (color, ours, other, theirs) => (ours ? write(color, segmentOf(color, other, theirs)) : color);
1012
+ const turn = (color, ours, other, theirs) => ours ? write(color, boundWith(color, other, theirs), placeOf(cell, color.hex)) : color;
882
1013
  return { ...shown, fg: turn(shown.fg, mine.fg, shown.bg, mine.bg), bg: turn(shown.bg, mine.bg, shown.fg, mine.fg) };
883
1014
  };
884
1015
  });
@@ -937,9 +1068,10 @@ const ROUNDING_GAP = Math.log(1.031);
937
1068
  * A pair's contrast to spare above its floor, as a gap between stops, is
938
1069
  * shared out evenly between its sides that turn: the lighter may land that
939
1070
  * much darker, the darker that much lighter, and never further. So a colour
940
- * written once for every pair it is drawn in is bounded by each of them at
941
- * once, and a frame writes it one way wherever it falls. A pair whose floor
942
- * is 1:1 holds however it is drawn, and bounds nothing.
1071
+ * bounded once for every pair it is drawn in is bounded by each of them at
1072
+ * once, and every way a frame writes it — the wheel turns it at each place
1073
+ * it falls — lands within them. A pair whose floor is 1:1 holds however it
1074
+ * is drawn, and bounds nothing.
943
1075
  */
944
1076
  function turnedBounds(pairs, depth, turnedAs) {
945
1077
  const bounds = new Map();