@promptctl/rich-js 0.25.1 → 0.26.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.
- package/dist/core/strip.d.ts +15 -5
- package/dist/core/strip.js +24 -19
- package/dist/index.d.ts +1 -1
- package/dist/renderables/effect.d.ts +37 -12
- package/dist/renderables/effect.js +20 -13
- package/dist/renderables/effects.d.ts +15 -9
- package/dist/renderables/effects.js +158 -31
- package/package.json +1 -1
package/dist/core/strip.d.ts
CHANGED
|
@@ -372,11 +372,21 @@ export type JoinerSpec = string | {
|
|
|
372
372
|
*/
|
|
373
373
|
export declare function joinerNamed(spec: JoinerSpec): CellJoiner;
|
|
374
374
|
/**
|
|
375
|
-
* A ramp of `steps` cells from the left ground to the right
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
375
|
+
* A ramp of `steps` cells from the left ground to the right, each cell filled
|
|
376
|
+
* with one colour: cell `i` is the colour `(i + 1/2) / steps` of the way
|
|
377
|
+
* across, so no cell is either neighbour's ground. A cell is a space whose ink
|
|
378
|
+
* is the terminal's ground, so its one colour is its fill, the same in ASCII.
|
|
379
|
+
* Beside a neighbour with no ground there is no colour to ramp toward, so that
|
|
380
|
+
* join draws the caps the ramp is ended in.
|
|
381
|
+
*
|
|
382
|
+
* [LAW:dataflow-not-control-flow] A ramp cell is fill alone: a space has no
|
|
383
|
+
* ink, and `"ground"` as its `fg` is the absence of a colour on that side
|
|
384
|
+
* (`cellStyle`). So the lift, the path every seam cell is drawn by, has no ink
|
|
385
|
+
* to hold off its ground, and the ramp is the blend, monotone however close
|
|
386
|
+
* its grounds or however many its steps. Two samples to a cell, ink over
|
|
387
|
+
* ground, cannot be: the lift holds every cell's two the floor apart, so
|
|
388
|
+
* `steps` such cells rise without turning back only between grounds `steps`
|
|
389
|
+
* floors apart.
|
|
380
390
|
*/
|
|
381
391
|
export declare function gradientSeam(steps?: number): JoinSeam;
|
|
382
392
|
export {};
|
package/dist/core/strip.js
CHANGED
|
@@ -627,26 +627,31 @@ function named(table, kind, kinds, name) {
|
|
|
627
627
|
throw new Error(`no ${kind} named ${JSON.stringify(name)}; the ${kinds} are ${Object.keys(table).join(", ")}`);
|
|
628
628
|
return table[name];
|
|
629
629
|
}
|
|
630
|
-
function gradient(steps, glyph) {
|
|
631
|
-
// Midpoint sampling across `2 * steps` half-cell positions: cell i takes
|
|
632
|
-
// samples 2i (its ink, the left half) and 2i+1 (its ground, the right half),
|
|
633
|
-
// so no sample equals either neighbour's ground.
|
|
634
|
-
const samples = 2 * steps;
|
|
635
|
-
return {
|
|
636
|
-
seam: Array.from({ length: steps }, (_, i) => ({
|
|
637
|
-
glyph,
|
|
638
|
-
fg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 0.5) / samples] },
|
|
639
|
-
bg: { mix: [LEFT_BG, RIGHT_BG, (2 * i + 1.5) / samples] },
|
|
640
|
-
})),
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
630
|
/**
|
|
644
|
-
* A ramp of `steps` cells from the left ground to the right
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
631
|
+
* A ramp of `steps` cells from the left ground to the right, each cell filled
|
|
632
|
+
* with one colour: cell `i` is the colour `(i + 1/2) / steps` of the way
|
|
633
|
+
* across, so no cell is either neighbour's ground. A cell is a space whose ink
|
|
634
|
+
* is the terminal's ground, so its one colour is its fill, the same in ASCII.
|
|
635
|
+
* Beside a neighbour with no ground there is no colour to ramp toward, so that
|
|
636
|
+
* join draws the caps the ramp is ended in.
|
|
637
|
+
*
|
|
638
|
+
* [LAW:dataflow-not-control-flow] A ramp cell is fill alone: a space has no
|
|
639
|
+
* ink, and `"ground"` as its `fg` is the absence of a colour on that side
|
|
640
|
+
* (`cellStyle`). So the lift, the path every seam cell is drawn by, has no ink
|
|
641
|
+
* to hold off its ground, and the ramp is the blend, monotone however close
|
|
642
|
+
* its grounds or however many its steps. Two samples to a cell, ink over
|
|
643
|
+
* ground, cannot be: the lift holds every cell's two the floor apart, so
|
|
644
|
+
* `steps` such cells rise without turning back only between grounds `steps`
|
|
645
|
+
* floors apart.
|
|
649
646
|
*/
|
|
650
647
|
export function gradientSeam(steps = 4) {
|
|
651
|
-
|
|
648
|
+
// [LAW:no-silent-failure] A count that is no whole number of cells would draw
|
|
649
|
+
// a lopsided ramp without a word, and no cells is no ramp: a seam that reads
|
|
650
|
+
// neither ground, joining a colourless neighbour where a ramp draws the caps.
|
|
651
|
+
if (!(Number.isInteger(steps) && steps > 0))
|
|
652
|
+
throw new RangeError(`gradientSeam steps must be a positive whole number of cells, got ${steps}`);
|
|
653
|
+
const ramp = {
|
|
654
|
+
seam: Array.from({ length: steps }, (_, i) => ({ glyph: " ", fg: GROUND, bg: { mix: [LEFT_BG, RIGHT_BG, (i + 1 / 2) / steps] } })),
|
|
655
|
+
};
|
|
656
|
+
return parseSeam(both(ramp, ramp));
|
|
652
657
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -61,7 +61,7 @@ export type { ConsoleOptions, ConsoleSink, ConsoleStream, ConsoleEnvironment, Li
|
|
|
61
61
|
export type { Env } from "./core/env.js";
|
|
62
62
|
export { Constrain } from "./renderables/constrain.js";
|
|
63
63
|
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";
|
|
64
|
+
export type { CellAttributes, CellColors, CellLook, Drawn, Effect, EffectCell, EffectedOptions, EffectFrame, ListedCell, Pair, Placed } from "./renderables/effect.js";
|
|
65
65
|
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
66
|
export type { AttributeLoopName, Blink, ColorStep, Curve, EffectTarget, Field, Loop, LoopName, Touch, Transition, TransitionName } from "./renderables/effects.js";
|
|
67
67
|
export { crossfade, wipe } from "./renderables/change.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
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* that
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
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
|
|
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. */
|
|
@@ -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
|
|
@@ -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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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 `
|
|
452
|
-
* are listed off `walked` the first time
|
|
453
|
-
* effect spread over the element
|
|
454
|
-
* words alone, a segment at a time, is
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
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
|
|
@@ -813,20 +813,135 @@ export function dissolveOut(curve, z) {
|
|
|
813
813
|
const STRAY_PERIODS = [211, 337];
|
|
814
814
|
/** How far a segment may stray from the shared turn at swing 1, in degrees: half a turn — fully apart. */
|
|
815
815
|
const STRAY = 180;
|
|
816
|
+
/**
|
|
817
|
+
* The columns between two places at which their strays no longer pace
|
|
818
|
+
* alike: the noise a stray rides on is sampled at the place over this many
|
|
819
|
+
* columns a unit, so a strip's items, a dozen columns apart, pace a little
|
|
820
|
+
* apart and segments a screen apart pace on their own. Rows are the same
|
|
821
|
+
* distance apart as columns.
|
|
822
|
+
*/
|
|
823
|
+
const STRAY_SPREAD = 24;
|
|
824
|
+
/** `cell` as a place of its own, as wide as its glyph: where no region covers it. */
|
|
825
|
+
const ownPlace = (cell, width) => ({ row: cell.row, centre: cell.col + width / 2 });
|
|
826
|
+
/**
|
|
827
|
+
* The segments `drawn` draws, as where each colour of each cell turns: a
|
|
828
|
+
* segment is a region of adjacent cells — beside or above one another — of
|
|
829
|
+
* one colour of the element's own, each cell carrying it as its ground or,
|
|
830
|
+
* on the terminal's ground, as its ink, read off `Drawn.all` once a frame.
|
|
831
|
+
* So a panel's ground is one segment whatever words lie on it, a blank
|
|
832
|
+
* carrying the ink keeps a styled paragraph one segment across its spaces
|
|
833
|
+
* and lines, a caption in a bar's colour written under the bar turns with
|
|
834
|
+
* it, and a blank in the terminal's ink splits a segment. A seam a joiner
|
|
835
|
+
* drew is in none, since its colours are its neighbours' edges: each colour
|
|
836
|
+
* of a seam cell turns with the region of that colour past the seam on
|
|
837
|
+
* either side — a two-cell rail reads past its other cell — so a powerline
|
|
838
|
+
* cap keeps the colour of the cell it caps, a custom joiner's handed cap
|
|
839
|
+
* included, and with the cell's own place where none is beside it — a
|
|
840
|
+
* neighbour's edge the frame does not show, handed at the seam's cell. Any
|
|
841
|
+
* other cell's colours turn with the region covering it: a letter in a
|
|
842
|
+
* neighbour's fill colour is a word, not a cap. A cell the frame lists
|
|
843
|
+
* nowhere, which a hand-built `Drawn` hands, is one column wide. The place
|
|
844
|
+
* is the region's centre column from the element's left edge, with its
|
|
845
|
+
* centre row: a region that grows by a column moves its centre by half of
|
|
846
|
+
* one, and those it pushes along by a column theirs by one, where a share
|
|
847
|
+
* of the element's extent would move every region's place with any
|
|
848
|
+
* region's growth.
|
|
849
|
+
*/
|
|
850
|
+
function segmentsOf(drawn) {
|
|
851
|
+
const { colors } = drawn;
|
|
852
|
+
const at = (row, col) => `${row}:${col}`;
|
|
853
|
+
const listed = new Map();
|
|
854
|
+
// Every column a seam cell covers: what a seam's colours look past to the region beside the seam.
|
|
855
|
+
const seams = new Set();
|
|
856
|
+
// The colour a cell is a region of: its own ground, else on the terminal's ground its own ink, else none.
|
|
857
|
+
const regionColour = (cell) => {
|
|
858
|
+
if (cell.seam)
|
|
859
|
+
return undefined;
|
|
860
|
+
const own = (terminal, hex) => (!terminal && colors.has(hex) ? hex : undefined);
|
|
861
|
+
return own(cell.pair.terminal.bg, cell.pair.bg.hex) ?? own(cell.pair.terminal.fg, cell.pair.fg.hex);
|
|
862
|
+
};
|
|
863
|
+
// Every column a region's cell covers, in walk order, indexed by where it is.
|
|
864
|
+
const covered = [];
|
|
865
|
+
const index = new Map();
|
|
866
|
+
for (const cell of drawn.all()) {
|
|
867
|
+
listed.set(at(cell.row, cell.col), cell);
|
|
868
|
+
const hex = regionColour(cell);
|
|
869
|
+
for (let col = cell.col; col < cell.col + cell.width; col++) {
|
|
870
|
+
if (cell.seam)
|
|
871
|
+
seams.add(at(cell.row, col));
|
|
872
|
+
if (hex === undefined)
|
|
873
|
+
continue;
|
|
874
|
+
index.set(at(cell.row, col), covered.length);
|
|
875
|
+
covered.push({ row: cell.row, col, hex });
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
// Union-find over the covered columns: two of one colour beside or above one another are one region.
|
|
879
|
+
const parent = covered.map((_, i) => i);
|
|
880
|
+
const find = (i) => {
|
|
881
|
+
while (parent[i] !== i) {
|
|
882
|
+
parent[i] = parent[parent[i]];
|
|
883
|
+
i = parent[i];
|
|
884
|
+
}
|
|
885
|
+
return i;
|
|
886
|
+
};
|
|
887
|
+
const joinWith = (i, row, col) => {
|
|
888
|
+
const j = index.get(at(row, col));
|
|
889
|
+
if (j !== undefined && covered[j].hex === covered[i].hex)
|
|
890
|
+
parent[find(i)] = find(j);
|
|
891
|
+
};
|
|
892
|
+
covered.forEach(({ row, col }, i) => {
|
|
893
|
+
joinWith(i, row, col - 1);
|
|
894
|
+
joinWith(i, row - 1, col);
|
|
895
|
+
});
|
|
896
|
+
const regions = new Map();
|
|
897
|
+
covered.forEach(({ row, col, hex }, i) => {
|
|
898
|
+
const root = find(i);
|
|
899
|
+
const region = regions.get(root);
|
|
900
|
+
if (region === undefined)
|
|
901
|
+
regions.set(root, { hex, top: row, bottom: row, left: col, right: col });
|
|
902
|
+
else
|
|
903
|
+
[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)];
|
|
904
|
+
});
|
|
905
|
+
const regionAt = (row, col) => {
|
|
906
|
+
const i = index.get(at(row, col));
|
|
907
|
+
return i === undefined ? undefined : regions.get(find(i));
|
|
908
|
+
};
|
|
909
|
+
const placeOf = (region) => ({ row: (region.top + region.bottom) / 2, centre: (region.left + region.right + 1) / 2 });
|
|
910
|
+
// The region past the seam from `col` in `step`'s direction: what a seam's colour turns with.
|
|
911
|
+
const pastSeam = (row, col, step) => {
|
|
912
|
+
let past = col;
|
|
913
|
+
while (seams.has(at(row, past)))
|
|
914
|
+
past += step;
|
|
915
|
+
return regionAt(row, past);
|
|
916
|
+
};
|
|
917
|
+
return (cell, hex) => {
|
|
918
|
+
const own = listed.get(at(cell.row, cell.col));
|
|
919
|
+
const width = own?.width ?? 1;
|
|
920
|
+
const beside = (own?.seam === true ? [pastSeam(cell.row, cell.col - 1, -1), pastSeam(cell.row, cell.col + width, 1)] : []).find((region) => region?.hex === hex);
|
|
921
|
+
const region = beside ?? regionAt(cell.row, cell.col);
|
|
922
|
+
return region === undefined ? ownPlace(cell, width) : placeOf(region);
|
|
923
|
+
};
|
|
924
|
+
}
|
|
816
925
|
/**
|
|
817
926
|
* Wheel, as the colour of daylight going round: every hue turning the whole
|
|
818
927
|
* way round once a period — far too slowly to be seen moving, so a glance
|
|
819
928
|
* sees a steady palette and an hour's absence a different one — each
|
|
820
929
|
* 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
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
*
|
|
930
|
+
* from one another, 0–1, 1 being half a turn apart. A segment is a region
|
|
931
|
+
* of adjacent cells of one colour of the element's own, carried as their
|
|
932
|
+
* ground or, on the terminal's ground, as their ink (`segmentsOf`), and it
|
|
933
|
+
* paces by where it sits, not by its colour: two
|
|
934
|
+
* regions of one fill at two places turn apart, and each colour of the seam
|
|
935
|
+
* a joiner drew between them turns with the region of that colour beside
|
|
936
|
+
* it, so a powerline cap keeps the colour of the cell it caps. An ink that
|
|
937
|
+
* is no fill turns with the ground it sits on, so a cell's words and fill
|
|
938
|
+
* turn together. A segment's stray is slow noise at its place, at
|
|
939
|
+
* `STRAY_PERIODS` in time and `STRAY_SPREAD` across the element, so
|
|
940
|
+
* neighbours pace a little apart and distant segments on their own, the
|
|
941
|
+
* row never shows one spread of hues twice, and a segment that grows by a
|
|
942
|
+
* column moves its hue a little, and those it pushes along as little. Only the
|
|
943
|
+
* stray reads the place: a place in the shared turn would jump a growing
|
|
944
|
+
* run by `360·t·Δ/P`, without bound in `t`.
|
|
830
945
|
* A colour keeps its luminance as it turns (`hueTurned`), so every pair of
|
|
831
946
|
* words reads at the contrast it rested at; at 256 colours a turned colour
|
|
832
947
|
* lands on a colour the depth writes, one that keeps every pair of words it
|
|
@@ -839,46 +954,57 @@ export function wheel(curve, z) {
|
|
|
839
954
|
requireCurve(curve);
|
|
840
955
|
requireZ("wheel", z);
|
|
841
956
|
const P = curve.seconds;
|
|
842
|
-
// A segment's
|
|
843
|
-
|
|
844
|
-
|
|
957
|
+
// A segment's stray at its place. The fractions of the `y` offsets, .9
|
|
958
|
+
// and .3, are ones no centre — a half-integer over `STRAY_SPREAD` columns
|
|
959
|
+
// — reaches, so no sample's `y` is whole: a sample with a whole `y` and
|
|
960
|
+
// `z` sits on a lattice line along `t`, where the noise is zero at every
|
|
961
|
+
// multiple of the period and every segment on it would return to the
|
|
962
|
+
// shared turn together. `z` needs no such offset; its two differ so the
|
|
963
|
+
// two noises are read from different fields.
|
|
964
|
+
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
965
|
// Measured from where it stood at the start, as pulse's wander is, so the
|
|
846
966
|
// first frame turns nothing and the second has turned only a frame's worth.
|
|
847
|
-
const turned = (
|
|
848
|
-
const s = seed(hex);
|
|
849
|
-
return (360 * t) / P + curve.swing * STRAY * ((stray(s, t) - stray(s, 0)) / 1.5);
|
|
850
|
-
};
|
|
967
|
+
const turned = (place, t) => (360 * t) / P + curve.swing * STRAY * ((stray(place, t) - stray(place, 0)) / 1.5);
|
|
851
968
|
// A turned colour's bounds rest on what a frame draws, never on `t`: settled once for each.
|
|
852
969
|
const settled = new WeakMemo();
|
|
853
970
|
return stillOnSlots((drawn) => {
|
|
854
971
|
const { colors, fills, depth, theme } = drawn;
|
|
855
972
|
// Whether a slot shows one of the element's colours: never the terminal's, whatever its hex.
|
|
856
973
|
const own = (terminal, color) => !terminal && colors.has(color.hex);
|
|
857
|
-
// The
|
|
858
|
-
|
|
859
|
-
//
|
|
860
|
-
const
|
|
974
|
+
// The colour an own colour is bounded with, under which its bounds are
|
|
975
|
+
// kept: itself if a fill or beside no own colour, else the ground beside
|
|
976
|
+
// it. Where it turns is `segmentsOf`'s, not this.
|
|
977
|
+
const boundWith = (color, other, theirs) => (fills.has(color.hex) || !theirs ? color.hex : other.hex);
|
|
978
|
+
// The bounds a colour turns within, by the colour it is bounded with: one bound for every place a frame writes it at.
|
|
979
|
+
const keyOf = (color, bound) => `${color.hex} ${bound}`;
|
|
861
980
|
const bounds = settled.get(theme, () => new Memo()).get(drawnKey(drawn), () => turnedBounds(drawn.pairs, depth, (pair, side) => {
|
|
862
981
|
const other = side === "fg" ? "bg" : "fg";
|
|
863
|
-
return own(pair.terminal[side], pair[side]) ? keyOf(pair[side],
|
|
982
|
+
return own(pair.terminal[side], pair[side]) ? keyOf(pair[side], boundWith(pair[side], pair[other], own(pair.terminal[other], pair[other]))) : undefined;
|
|
864
983
|
}));
|
|
865
984
|
// 256 colours writes a turned colour as one it has an index for; truecolor
|
|
866
985
|
// as one of the two either side of its luminance.
|
|
867
986
|
const indexed = measuredIndexed(depth, theme);
|
|
987
|
+
// Where each cell's colours turn, read off the frame's listing once.
|
|
988
|
+
const placeOf = segmentsOf(drawn);
|
|
868
989
|
return Animation.continuous((t) => {
|
|
990
|
+
// [LAW:one-source-of-truth] A colour turned at a place within a
|
|
991
|
+
// segment's bounds is written once a frame, wherever it falls: the
|
|
992
|
+
// key is the bounds' key with the place, so one entry never serves
|
|
993
|
+
// two bounds.
|
|
869
994
|
const written = new Map();
|
|
870
|
-
const choose = (color,
|
|
871
|
-
const turn = hueTurned(color, turned(
|
|
872
|
-
return nearestWithin(turn.looks, color, depth === ColorDepth.TRUECOLOR ? turn.either.map(measured) : indexed, bounds.get(keyOf(color,
|
|
995
|
+
const choose = (color, bound, place) => {
|
|
996
|
+
const turn = hueTurned(color, turned(place, t));
|
|
997
|
+
return nearestWithin(turn.looks, color, depth === ColorDepth.TRUECOLOR ? turn.either.map(measured) : indexed, bounds.get(keyOf(color, bound)) ?? UNBOUNDED);
|
|
873
998
|
};
|
|
874
|
-
const write = (color,
|
|
875
|
-
const
|
|
876
|
-
written.
|
|
999
|
+
const write = (color, bound, place) => {
|
|
1000
|
+
const key = `${keyOf(color, bound)} ${place.row}:${place.centre}`;
|
|
1001
|
+
const found = written.get(key) ?? choose(color, bound, place);
|
|
1002
|
+
written.set(key, found);
|
|
877
1003
|
return found;
|
|
878
1004
|
};
|
|
879
1005
|
return (shown, cell) => {
|
|
880
1006
|
const mine = { fg: own(cell.terminal.fg, shown.fg), bg: own(cell.terminal.bg, shown.bg) };
|
|
881
|
-
const turn = (color, ours, other, theirs) =>
|
|
1007
|
+
const turn = (color, ours, other, theirs) => ours ? write(color, boundWith(color, other, theirs), placeOf(cell, color.hex)) : color;
|
|
882
1008
|
return { ...shown, fg: turn(shown.fg, mine.fg, shown.bg, mine.bg), bg: turn(shown.bg, mine.bg, shown.fg, mine.fg) };
|
|
883
1009
|
};
|
|
884
1010
|
});
|
|
@@ -937,9 +1063,10 @@ const ROUNDING_GAP = Math.log(1.031);
|
|
|
937
1063
|
* A pair's contrast to spare above its floor, as a gap between stops, is
|
|
938
1064
|
* shared out evenly between its sides that turn: the lighter may land that
|
|
939
1065
|
* much darker, the darker that much lighter, and never further. So a colour
|
|
940
|
-
*
|
|
941
|
-
* once, and a frame writes it
|
|
942
|
-
* is 1:1 holds however it
|
|
1066
|
+
* bounded once for every pair it is drawn in is bounded by each of them at
|
|
1067
|
+
* once, and every way a frame writes it — the wheel turns it at each place
|
|
1068
|
+
* it falls — lands within them. A pair whose floor is 1:1 holds however it
|
|
1069
|
+
* is drawn, and bounds nothing.
|
|
943
1070
|
*/
|
|
944
1071
|
function turnedBounds(pairs, depth, turnedAs) {
|
|
945
1072
|
const bounds = new Map();
|