@promptctl/rich-js 0.25.0 → 0.25.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.
- package/dist/core/color.js +4 -5
- package/dist/core/strip.d.ts +85 -13
- package/dist/core/strip.js +92 -40
- package/dist/renderables/effect.d.ts +30 -13
- package/dist/renderables/effect.js +211 -61
- package/package.json +1 -1
package/dist/core/color.js
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* Terminal color representation, parsing, downgrading, and ANSI code generation.
|
|
3
3
|
*/
|
|
4
4
|
import { Memo, WeakMemo } from "./memo.js";
|
|
5
|
+
import { pattern } from "./pattern.js";
|
|
6
|
+
// Runtime-importing `Palette` from `themes/palette.ts` is safe: that module
|
|
7
|
+
// only `import type`s from `core/color.ts`, so there is no runtime cycle.
|
|
8
|
+
import { Palette } from "../themes/palette.js";
|
|
5
9
|
// --- ColorRgba ---
|
|
6
10
|
function hex2(byte) {
|
|
7
11
|
return byte.toString(16).padStart(2, "0");
|
|
@@ -950,11 +954,6 @@ export const WINDOWS_TABLE = new ColorTable(buildWindowsTable());
|
|
|
950
954
|
// terminal a colour's depth assumes, built from that depth's own sixteen with
|
|
951
955
|
// an empty Palette — sufficient for the lookups it serves, which never read
|
|
952
956
|
// `palette`.
|
|
953
|
-
//
|
|
954
|
-
// Runtime-importing `Palette` from `themes/palette.ts` is safe: that module
|
|
955
|
-
// only `import type`s from `core/color.ts`, so there is no runtime cycle.
|
|
956
|
-
import { Palette } from "../themes/palette.js";
|
|
957
|
-
import { pattern } from "./pattern.js";
|
|
958
957
|
const INTERNAL_DEFAULT_THEME = new TerminalTheme(new ColorRgba(0, 0, 0), new ColorRgba(255, 255, 255), STANDARD_TABLE, new Palette("default", true, new Map()));
|
|
959
958
|
/** The Windows console in its default Campbell scheme: a WINDOWS colour's terminal. */
|
|
960
959
|
const WINDOWS_CONSOLE_THEME = new TerminalTheme(WINDOWS_TABLE.get(0), WINDOWS_TABLE.get(7), WINDOWS_TABLE, new Palette("windows", true, new Map()));
|
package/dist/core/strip.d.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import type { Anchor } from "./anchor.js";
|
|
29
29
|
import { Segment } from "./segment.js";
|
|
30
30
|
import { Style } from "./style.js";
|
|
31
|
+
import { ColorDepth, ColorSpec } from "./color.js";
|
|
31
32
|
import { type Measurable, type Renderable, type RenderOptions } from "./protocol.js";
|
|
32
33
|
/**
|
|
33
34
|
* Items in a `Strip` expose their per-edge style so joiners can paint the
|
|
@@ -70,21 +71,11 @@ export interface Joiner<T extends StyledRenderable = StyledRenderable> {
|
|
|
70
71
|
* not draw at all. An effect reads it to leave a seam out of the words it
|
|
71
72
|
* keeps legible (`drawnOf`, `../renderables/effect.ts`).
|
|
72
73
|
*
|
|
73
|
-
* Inside it, every cell a `CellJoiner` draws
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* points into — so a cell has no single owner; each side has one.
|
|
74
|
+
* Inside it, every cell a `CellJoiner` draws is stamped with what it was
|
|
75
|
+
* resolved from (`JoinedCell`), so a cell whose neighbours an effect moves is
|
|
76
|
+
* resolved again from where they moved to.
|
|
77
77
|
*/
|
|
78
78
|
export declare const JOIN: object;
|
|
79
|
-
/**
|
|
80
|
-
* Which of a cell's two colours on screen are one of `owners`': both, for a
|
|
81
|
-
* cell one of them drew, itself or through an owner nested in its output; for
|
|
82
|
-
* a cell a `CellJoiner` drew, the sides shown in one of their colours.
|
|
83
|
-
*/
|
|
84
|
-
export declare function colorsOwnedBy(anchor: Anchor | undefined, owners: ReadonlySet<object>): {
|
|
85
|
-
readonly fg: boolean;
|
|
86
|
-
readonly bg: boolean;
|
|
87
|
-
};
|
|
88
79
|
/**
|
|
89
80
|
* [LAW:single-enforcer] What `joiner` draws at one join, every cell stamped
|
|
90
81
|
* as `JOIN`'s: the one way `Strip` and `FlexStrip` draw a join.
|
|
@@ -203,6 +194,86 @@ export interface JoinCaps {
|
|
|
203
194
|
* difference, because a seam is one cell wide.
|
|
204
195
|
*/
|
|
205
196
|
export declare const SEAM_MIN_DELTA_E = 0.04;
|
|
197
|
+
/** The parts of each neighbour's edge a join cell paints with. */
|
|
198
|
+
interface CellParts {
|
|
199
|
+
readonly left: ReadonlySet<"fg" | "bg">;
|
|
200
|
+
readonly right: ReadonlySet<"fg" | "bg">;
|
|
201
|
+
}
|
|
202
|
+
/** A neighbour a join cell paints with, and its edge as the cell was resolved from it. */
|
|
203
|
+
export interface JoinSide {
|
|
204
|
+
readonly owner: object;
|
|
205
|
+
readonly edge: Style;
|
|
206
|
+
}
|
|
207
|
+
interface Edges {
|
|
208
|
+
readonly left: Style | undefined;
|
|
209
|
+
readonly right: Style | undefined;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Where an effect moved one slot or both of a neighbour's edge cell to, each
|
|
213
|
+
* the spec the depth writes the slot in, a palette slot as that slot:
|
|
214
|
+
* `color` for its text, `bgcolor` for its ground. A slot left unset did not
|
|
215
|
+
* move.
|
|
216
|
+
*/
|
|
217
|
+
export interface EdgeMove {
|
|
218
|
+
readonly color?: ColorSpec;
|
|
219
|
+
readonly bgcolor?: ColorSpec;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* A cell a `CellJoiner` drew, as what it is: one function of its two
|
|
223
|
+
* neighbours' edges. It holds the cell's paints, the neighbour each side of
|
|
224
|
+
* them reads, and the edges and render it was resolved from, and it is stamped
|
|
225
|
+
* on the cell as the owner of its innermost anchor level.
|
|
226
|
+
*
|
|
227
|
+
* [LAW:one-source-of-truth] A seam's colours are its neighbours', so nothing
|
|
228
|
+
* paints them but this resolution. An effect paints a strip's items and never
|
|
229
|
+
* a join cell that `follows` them; once the items are painted, each such cell
|
|
230
|
+
* is handed what moved at the item cells beside it and resolved again through
|
|
231
|
+
* `cellStyle`, the path the join was drawn by, lift included
|
|
232
|
+
* (`../renderables/effect.ts`). A seam then follows its neighbours wherever an
|
|
233
|
+
* effect takes them, a mix of the two included, and an effect that paints
|
|
234
|
+
* both one colour leaves the seam's ink lifted off it. A cell painted from
|
|
235
|
+
* roles and the terminal's ground alone, as `plain`'s separator is, follows
|
|
236
|
+
* nobody, and an effect paints it as it paints any cell.
|
|
237
|
+
*/
|
|
238
|
+
export declare class JoinedCell {
|
|
239
|
+
private readonly cell;
|
|
240
|
+
private readonly parts;
|
|
241
|
+
private readonly neighbours;
|
|
242
|
+
private readonly edges;
|
|
243
|
+
private readonly options;
|
|
244
|
+
/** What the cell's paints read on each side: that neighbour and its edge as the cell was resolved from it, or `null` for a side they read nothing of. */
|
|
245
|
+
readonly reads: {
|
|
246
|
+
readonly left: JoinSide | null;
|
|
247
|
+
readonly right: JoinSide | null;
|
|
248
|
+
};
|
|
249
|
+
/** Whether the cell's paints read either neighbour, so it follows them under an effect. */
|
|
250
|
+
readonly follows: boolean;
|
|
251
|
+
/** The cell as drawn from its edges: the one resolution, at join time and after an effect alike. */
|
|
252
|
+
readonly style: Style;
|
|
253
|
+
constructor(cell: JoinCell, parts: CellParts, neighbours: {
|
|
254
|
+
readonly left: object | null;
|
|
255
|
+
readonly right: object | null;
|
|
256
|
+
}, edges: Edges, options: RenderOptions);
|
|
257
|
+
/**
|
|
258
|
+
* The cell resolved again once each side's edge has moved as `moves` says:
|
|
259
|
+
* its `colors`, as `cellStyle` draws them, and `style`, the cell's own, with
|
|
260
|
+
* the record of that stamped where this one was, so an effect drawn over
|
|
261
|
+
* this one moves on from these edges. Resolved, lift included, at `depth`,
|
|
262
|
+
* the depth the effect writes the frame at. None when nothing moved of what
|
|
263
|
+
* the cell paints with: a seam of arrows in two grounds is as it was drawn
|
|
264
|
+
* while an effect moves only the lettering beside it, and its lift is not
|
|
265
|
+
* walked again for a frame that cannot change it.
|
|
266
|
+
*/
|
|
267
|
+
rederived(moves: {
|
|
268
|
+
readonly left?: EdgeMove;
|
|
269
|
+
readonly right?: EdgeMove;
|
|
270
|
+
}, style: Style, depth: ColorDepth): {
|
|
271
|
+
readonly colors: Style;
|
|
272
|
+
readonly style: Style;
|
|
273
|
+
} | undefined;
|
|
274
|
+
}
|
|
275
|
+
/** The record a `CellJoiner` stamped on the cell `anchor` names, or none for a cell no `CellJoiner` drew. */
|
|
276
|
+
export declare function joinedCell(anchor: Anchor | undefined): JoinedCell | undefined;
|
|
206
277
|
/**
|
|
207
278
|
* The one joiner: what it draws is a `JoinSeam` between neighbours and a
|
|
208
279
|
* `JoinCaps` at the ends of each run, picked independently, so a new joiner is
|
|
@@ -308,3 +379,4 @@ export declare function joinerNamed(spec: JoinerSpec): CellJoiner;
|
|
|
308
379
|
* draws the caps the ramp is ended in.
|
|
309
380
|
*/
|
|
310
381
|
export declare function gradientSeam(steps?: number): JoinSeam;
|
|
382
|
+
export {};
|
package/dist/core/strip.js
CHANGED
|
@@ -39,41 +39,11 @@ import { drawable, getStyle, isAscii, render } from "./protocol.js";
|
|
|
39
39
|
* not draw at all. An effect reads it to leave a seam out of the words it
|
|
40
40
|
* keeps legible (`drawnOf`, `../renderables/effect.ts`).
|
|
41
41
|
*
|
|
42
|
-
* Inside it, every cell a `CellJoiner` draws
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* points into — so a cell has no single owner; each side has one.
|
|
42
|
+
* Inside it, every cell a `CellJoiner` draws is stamped with what it was
|
|
43
|
+
* resolved from (`JoinedCell`), so a cell whose neighbours an effect moves is
|
|
44
|
+
* resolved again from where they moved to.
|
|
46
45
|
*/
|
|
47
46
|
export const JOIN = Object.freeze({ name: "join" });
|
|
48
|
-
/**
|
|
49
|
-
* Whose colour each side of a join cell shows on screen, its ink and its
|
|
50
|
-
* ground: the neighbour whose edge a `Paint` names, or `null` for a role, a
|
|
51
|
-
* mix or the terminal's ground, which are no neighbour's.
|
|
52
|
-
*/
|
|
53
|
-
class ColorOwners {
|
|
54
|
-
fg;
|
|
55
|
-
bg;
|
|
56
|
-
constructor(fg, bg) {
|
|
57
|
-
this.fg = fg;
|
|
58
|
-
this.bg = bg;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Which of a cell's two colours on screen are one of `owners`': both, for a
|
|
63
|
-
* cell one of them drew, itself or through an owner nested in its output; for
|
|
64
|
-
* a cell a `CellJoiner` drew, the sides shown in one of their colours.
|
|
65
|
-
*/
|
|
66
|
-
export function colorsOwnedBy(anchor, owners) {
|
|
67
|
-
for (let level = anchor; level; level = level.inner) {
|
|
68
|
-
if (owners.has(level.owner))
|
|
69
|
-
return { fg: true, bg: true };
|
|
70
|
-
if (level.owner instanceof ColorOwners) {
|
|
71
|
-
const { fg, bg } = level.owner;
|
|
72
|
-
return { fg: fg !== null && owners.has(fg), bg: bg !== null && owners.has(bg) };
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return { fg: false, bg: false };
|
|
76
|
-
}
|
|
77
47
|
/**
|
|
78
48
|
* [LAW:single-enforcer] What `joiner` draws at one join, every cell stamped
|
|
79
49
|
* as `JOIN`'s: the one way `Strip` and `FlexStrip` draw a join.
|
|
@@ -262,9 +232,12 @@ function parseCaps(raw) {
|
|
|
262
232
|
function joinForms(seam, caps) {
|
|
263
233
|
return { seam: seam.seam.flatMap((cell) => ("cap" in cell ? caps[cell.cap] : [cell])), lead: caps.lead, tail: caps.tail };
|
|
264
234
|
}
|
|
235
|
+
// The parts of `side`'s edge `cell` paints with: its ground (`bg`), its text (`fg`).
|
|
236
|
+
const partsRead = (cell, side) => new Set([cell.fg, cell.bg].flatMap((color) => (color === "ground" ? [] : [...paintsIn(color)].flatMap((p) => ("edge" in p && p.edge === side ? [p.part] : [])))));
|
|
265
237
|
// Whether any of `cells` paints with `side`'s ground.
|
|
266
238
|
const readsGround = (cells, side) => cells.some((cell) => [cell.fg, cell.bg].some((color) => color !== "ground" && [...paintsIn(color)].some((p) => "edge" in p && p.edge === side && p.part === "bg")));
|
|
267
239
|
function formOf(shape) {
|
|
240
|
+
const cells = (part) => shape[part].map((cell) => ({ cell, parts: { left: partsRead(cell, "left"), right: partsRead(cell, "right") } }));
|
|
268
241
|
return {
|
|
269
242
|
shape,
|
|
270
243
|
reads: {
|
|
@@ -272,6 +245,7 @@ function formOf(shape) {
|
|
|
272
245
|
lead: readsGround(shape.lead, "right"),
|
|
273
246
|
tail: readsGround(shape.tail, "left"),
|
|
274
247
|
},
|
|
248
|
+
cells: { seam: cells("seam"), lead: cells("lead"), tail: cells("tail") },
|
|
275
249
|
};
|
|
276
250
|
}
|
|
277
251
|
// A bg with a real colour to paint. `undefined` (no bg) and the terminal
|
|
@@ -380,6 +354,9 @@ function resolvePaint(paint, edges, options) {
|
|
|
380
354
|
// A mix with no colour at one end has nothing to blend toward.
|
|
381
355
|
return a && b && ColorSpec.blend(a, b, t);
|
|
382
356
|
}
|
|
357
|
+
// [LAW:single-enforcer] The one resolution of a join cell's paints: at join
|
|
358
|
+
// time, and again whenever an effect moves the neighbours it reads
|
|
359
|
+
// (`JoinedCell`).
|
|
383
360
|
function cellStyle(cell, edges, options) {
|
|
384
361
|
// [LAW:dataflow-not-control-flow] "ground" is the absence of a colour on its
|
|
385
362
|
// side; as the fg it is reverse video over the bg colour, so the glyph's ink
|
|
@@ -391,6 +368,84 @@ function cellStyle(cell, edges, options) {
|
|
|
391
368
|
// the gap measured between them decides how far it moves, usually not at all.
|
|
392
369
|
return new Style({ color: standOff(resolvePaint(cell.fg, edges, options), bgcolor, options.colorSystem ?? ColorDepth.TRUECOLOR), bgcolor });
|
|
393
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* A cell a `CellJoiner` drew, as what it is: one function of its two
|
|
373
|
+
* neighbours' edges. It holds the cell's paints, the neighbour each side of
|
|
374
|
+
* them reads, and the edges and render it was resolved from, and it is stamped
|
|
375
|
+
* on the cell as the owner of its innermost anchor level.
|
|
376
|
+
*
|
|
377
|
+
* [LAW:one-source-of-truth] A seam's colours are its neighbours', so nothing
|
|
378
|
+
* paints them but this resolution. An effect paints a strip's items and never
|
|
379
|
+
* a join cell that `follows` them; once the items are painted, each such cell
|
|
380
|
+
* is handed what moved at the item cells beside it and resolved again through
|
|
381
|
+
* `cellStyle`, the path the join was drawn by, lift included
|
|
382
|
+
* (`../renderables/effect.ts`). A seam then follows its neighbours wherever an
|
|
383
|
+
* effect takes them, a mix of the two included, and an effect that paints
|
|
384
|
+
* both one colour leaves the seam's ink lifted off it. A cell painted from
|
|
385
|
+
* roles and the terminal's ground alone, as `plain`'s separator is, follows
|
|
386
|
+
* nobody, and an effect paints it as it paints any cell.
|
|
387
|
+
*/
|
|
388
|
+
export class JoinedCell {
|
|
389
|
+
cell;
|
|
390
|
+
parts;
|
|
391
|
+
neighbours;
|
|
392
|
+
edges;
|
|
393
|
+
options;
|
|
394
|
+
/** What the cell's paints read on each side: that neighbour and its edge as the cell was resolved from it, or `null` for a side they read nothing of. */
|
|
395
|
+
reads;
|
|
396
|
+
/** Whether the cell's paints read either neighbour, so it follows them under an effect. */
|
|
397
|
+
follows;
|
|
398
|
+
/** The cell as drawn from its edges: the one resolution, at join time and after an effect alike. */
|
|
399
|
+
style;
|
|
400
|
+
constructor(cell, parts, neighbours, edges, options) {
|
|
401
|
+
this.cell = cell;
|
|
402
|
+
this.parts = parts;
|
|
403
|
+
this.neighbours = neighbours;
|
|
404
|
+
this.edges = edges;
|
|
405
|
+
this.options = options;
|
|
406
|
+
const read = (side) => {
|
|
407
|
+
if (parts[side].size === 0)
|
|
408
|
+
return null;
|
|
409
|
+
const [owner, edge] = [neighbours[side], edges[side]];
|
|
410
|
+
// [LAW:no-silent-failure] Unreachable: a join draws a part only beside
|
|
411
|
+
// the neighbours its cells may name: a cap naming the one it is drawn
|
|
412
|
+
// without is refused when parsed (`parsePaint`).
|
|
413
|
+
if (owner === null || edge === undefined)
|
|
414
|
+
throw new Error(`a join cell paints with its ${side} neighbour's edge, and the join has no ${side} neighbour`);
|
|
415
|
+
return { owner, edge };
|
|
416
|
+
};
|
|
417
|
+
this.reads = { left: read("left"), right: read("right") };
|
|
418
|
+
this.follows = this.reads.left !== null || this.reads.right !== null;
|
|
419
|
+
this.style = cellStyle(cell, edges, options);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* The cell resolved again once each side's edge has moved as `moves` says:
|
|
423
|
+
* its `colors`, as `cellStyle` draws them, and `style`, the cell's own, with
|
|
424
|
+
* the record of that stamped where this one was, so an effect drawn over
|
|
425
|
+
* this one moves on from these edges. Resolved, lift included, at `depth`,
|
|
426
|
+
* the depth the effect writes the frame at. None when nothing moved of what
|
|
427
|
+
* the cell paints with: a seam of arrows in two grounds is as it was drawn
|
|
428
|
+
* while an effect moves only the lettering beside it, and its lift is not
|
|
429
|
+
* walked again for a frame that cannot change it.
|
|
430
|
+
*/
|
|
431
|
+
rederived(moves, style, depth) {
|
|
432
|
+
const reaches = (side) => (moves[side]?.color !== undefined && this.parts[side].has("fg")) || (moves[side]?.bgcolor !== undefined && this.parts[side].has("bg"));
|
|
433
|
+
if (!reaches("left") && !reaches("right"))
|
|
434
|
+
return undefined;
|
|
435
|
+
const moved = (edge, move) => move === undefined ? edge : new Style({ color: move.color ?? edge?.color, bgcolor: move.bgcolor ?? edge?.bgcolor });
|
|
436
|
+
const edges = { left: moved(this.edges.left, moves.left), right: moved(this.edges.right, moves.right) };
|
|
437
|
+
const next = new JoinedCell(this.cell, this.parts, this.neighbours, edges, { ...this.options, colorSystem: depth });
|
|
438
|
+
const restamp = (level) => ({ ...level, owner: level.owner === this ? next : level.owner, inner: level.inner && restamp(level.inner) });
|
|
439
|
+
return { colors: next.style, style: style.withAnchor(style.anchor && restamp(style.anchor)) };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/** The record a `CellJoiner` stamped on the cell `anchor` names, or none for a cell no `CellJoiner` drew. */
|
|
443
|
+
export function joinedCell(anchor) {
|
|
444
|
+
for (let level = anchor; level; level = level.inner)
|
|
445
|
+
if (level.owner instanceof JoinedCell)
|
|
446
|
+
return level.owner;
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
394
449
|
const shapeGlyphs = (shape) => PARTS.flatMap((part) => shape[part].flatMap(glyphsOf)).join("");
|
|
395
450
|
/**
|
|
396
451
|
* The one joiner: what it draws is a `JoinSeam` between neighbours and a
|
|
@@ -422,7 +477,7 @@ export class CellJoiner {
|
|
|
422
477
|
const forms = this._forms;
|
|
423
478
|
return {
|
|
424
479
|
*render(options) {
|
|
425
|
-
const {
|
|
480
|
+
const { cells, reads } = drawable(options, forms.glyph, forms.ascii, (form) => shapeGlyphs(form.shape));
|
|
426
481
|
const edges = { left: left?.edgeStyle("right", options), right: right?.edgeStyle("left", options) };
|
|
427
482
|
const leftBg = paintableBg(edges.left?.bgcolor);
|
|
428
483
|
const rightBg = paintableBg(edges.right?.bgcolor);
|
|
@@ -431,12 +486,9 @@ export class CellJoiner {
|
|
|
431
486
|
const leads = hasRight && (!reads.lead || rightBg !== undefined);
|
|
432
487
|
const tails = hasLeft && (!reads.tail || leftBg !== undefined);
|
|
433
488
|
const neighbours = { left, right };
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const styled = (part) => drawn[part].map((cell, at) => {
|
|
438
|
-
const owners = new ColorOwners(ownerOf(cell.fg), ownerOf(cell.bg));
|
|
439
|
-
return new Segment(pickGlyph(cell, part, index, at), cellStyle(cell, edges, options).withAnchor({ owner: owners, row: 0, col: 0, inner: undefined }));
|
|
489
|
+
const styled = (part) => cells[part].map(({ cell, parts }, at) => {
|
|
490
|
+
const joined = new JoinedCell(cell, parts, neighbours, edges, options);
|
|
491
|
+
return new Segment(pickGlyph(cell, part, index, at), joined.style.withAnchor({ owner: joined, row: 0, col: 0, inner: undefined }));
|
|
440
492
|
});
|
|
441
493
|
if (!hasLeft || !hasRight) {
|
|
442
494
|
if (leads || tails)
|
|
@@ -8,11 +8,16 @@
|
|
|
8
8
|
* (`EffectFrame`), and it says when that next changes as every
|
|
9
9
|
* animation does. Whatever holds for the whole frame is worked out once, in
|
|
10
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
|
|
11
|
+
* function `at(t)` returns, but for those a wrapper keeps back — `confined`
|
|
12
12
|
* hands over only the cells its owners drew, so an effect never counts on
|
|
13
|
-
* seeing them all
|
|
14
|
-
* are
|
|
15
|
-
*
|
|
13
|
+
* seeing them all — and for a strip's join cells painted from the items beside
|
|
14
|
+
* them, which are no one's to paint: a seam's colours are its neighbours'
|
|
15
|
+
* edges, so once the effect has painted every other cell, each such cell is
|
|
16
|
+
* resolved again from the cells beside it as the effect left them
|
|
17
|
+
* (`JoinedCell`, `../core/strip.ts`), the way the joiner drew it. Shimmer,
|
|
18
|
+
* pulse, fade, dissolve and a blinking attribute are all this one shape,
|
|
19
|
+
* differing by what the function does with its inputs
|
|
20
|
+
* [LAW:one-type-per-behavior]; two effects stack by nesting one
|
|
16
21
|
* wrapper in another, which is the same as calling one after the other but
|
|
17
22
|
* for what the outer one measures: the inner one's output.
|
|
18
23
|
*
|
|
@@ -149,9 +154,12 @@ export interface Drawn {
|
|
|
149
154
|
* sits and who drew it, listed before the effect is handed any: what
|
|
150
155
|
* an effect spread over the element — a transition, which has its last cell
|
|
151
156
|
* settle as it ends — is spread over. A blank cell on the terminal's ground
|
|
152
|
-
* that draws no line is handed but not listed, since nothing in it shows
|
|
153
|
-
*
|
|
154
|
-
*
|
|
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.
|
|
155
163
|
*/
|
|
156
164
|
cells(): readonly Placed[];
|
|
157
165
|
/** Every distinct ink and ground of a word cell. */
|
|
@@ -220,6 +228,16 @@ export declare class Recolored implements Renderable, Measurable {
|
|
|
220
228
|
/** The frame the effect draws at `t`, and the hold it says that is in. */
|
|
221
229
|
moved(options: RenderOptions): Moved;
|
|
222
230
|
private _recolored;
|
|
231
|
+
/** `segment`'s cells drawn in the looks the effect painted them. */
|
|
232
|
+
private _repainted;
|
|
233
|
+
/**
|
|
234
|
+
* A join cell's `segment` resolved again from the edges beside it, as the
|
|
235
|
+
* effect left them: each neighbour's nearest cell to the join on its row,
|
|
236
|
+
* its slots as the depth writes them. A side whose neighbour the effect moved
|
|
237
|
+
* nothing of keeps the edge the cell was drawn from, so with neither moved
|
|
238
|
+
* the cell is as it was drawn.
|
|
239
|
+
*/
|
|
240
|
+
private _rejoined;
|
|
223
241
|
measure(options: RenderOptions): {
|
|
224
242
|
minimum: number;
|
|
225
243
|
maximum: number;
|
|
@@ -297,13 +315,12 @@ export declare function walkGlyphs(segments: readonly Segment[]): (readonly Walk
|
|
|
297
315
|
*/
|
|
298
316
|
export declare function inked(text: string): boolean;
|
|
299
317
|
/**
|
|
300
|
-
* `effect` on the
|
|
301
|
-
* nested in its output, and every other
|
|
318
|
+
* `effect` on the cells any of `owners` drew, itself or through an owner
|
|
319
|
+
* nested in its output, and every other cell left as it is: one item of a
|
|
302
320
|
* strip whose items share a fill, the items of a row one other item stands
|
|
303
|
-
* apart from, one widget of a screen. A
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
* `../core/strip.ts`). A cell none of them drew is never handed to the
|
|
321
|
+
* apart from, one widget of a screen. A strip's join cell is no item's, so it
|
|
322
|
+
* is never handed; one painted from the items beside it follows them, the
|
|
323
|
+
* moved ones included (`Recolored`). A cell none of them drew is never handed to the
|
|
307
324
|
* effect, so confining one effect to many owners costs one call a cell, not
|
|
308
325
|
* one for each owner.
|
|
309
326
|
*/
|
|
@@ -8,11 +8,16 @@
|
|
|
8
8
|
* (`EffectFrame`), and it says when that next changes as every
|
|
9
9
|
* animation does. Whatever holds for the whole frame is worked out once, in
|
|
10
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
|
|
11
|
+
* function `at(t)` returns, but for those a wrapper keeps back — `confined`
|
|
12
12
|
* hands over only the cells its owners drew, so an effect never counts on
|
|
13
|
-
* seeing them all
|
|
14
|
-
* are
|
|
15
|
-
*
|
|
13
|
+
* seeing them all — and for a strip's join cells painted from the items beside
|
|
14
|
+
* them, which are no one's to paint: a seam's colours are its neighbours'
|
|
15
|
+
* edges, so once the effect has painted every other cell, each such cell is
|
|
16
|
+
* resolved again from the cells beside it as the effect left them
|
|
17
|
+
* (`JoinedCell`, `../core/strip.ts`), the way the joiner drew it. Shimmer,
|
|
18
|
+
* pulse, fade, dissolve and a blinking attribute are all this one shape,
|
|
19
|
+
* differing by what the function does with its inputs
|
|
20
|
+
* [LAW:one-type-per-behavior]; two effects stack by nesting one
|
|
16
21
|
* wrapper in another, which is the same as calling one after the other but
|
|
17
22
|
* for what the outer one measures: the inner one's output.
|
|
18
23
|
*
|
|
@@ -71,7 +76,7 @@ import { fnv1a } from "../core/fnv1a.js";
|
|
|
71
76
|
import { Measurement } from "../core/measure.js";
|
|
72
77
|
import { render } from "../core/protocol.js";
|
|
73
78
|
import { Segment } from "../core/segment.js";
|
|
74
|
-
import { JOIN,
|
|
79
|
+
import { JOIN, joinedCell } from "../core/strip.js";
|
|
75
80
|
import { ATTRIBUTE_NAMES, Style } from "../core/style.js";
|
|
76
81
|
import { finiteTime } from "../core/easing.js";
|
|
77
82
|
/** What drew nothing the effect can change: held forever, as far as the effect says. */
|
|
@@ -118,48 +123,154 @@ export class Recolored {
|
|
|
118
123
|
return { segments: this._recolored(walked, shown.value, depth), hold: holdOf(shown, this.t) };
|
|
119
124
|
}
|
|
120
125
|
*_recolored(walked, frame, depth) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const colors = onScreen(moved, pair.reverse);
|
|
139
|
-
const last = runs[runs.length - 1];
|
|
140
|
-
const writes = last !== undefined && sameColors(last.colors, colors) ? last.drawn : this.respec(wire, slots, colors, depth);
|
|
141
|
-
if (last !== undefined && writesAlike(last.drawn, writes) && sameAttributes(last.attributes, moved.attributes)) {
|
|
142
|
-
last.text += glyph;
|
|
143
|
-
last.cells += cellLen(glyph);
|
|
144
|
-
last.colors = colors;
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
runs.push({ text: glyph, cells: cellLen(glyph), colors, attributes: moved.attributes, drawn: writes });
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
if (runs.every((run) => writesAlike(run.drawn, wire) && sameAttributes(run.attributes, attributes))) {
|
|
126
|
+
// [LAW:one-source-of-truth] Every cell but a join cell that follows its
|
|
127
|
+
// neighbours is painted first, so that cell is resolved from the cells
|
|
128
|
+
// beside it as the effect left them, wherever in the frame they fall.
|
|
129
|
+
const drawings = walked.map(({ look, glyphs }) => {
|
|
130
|
+
if (look === undefined)
|
|
131
|
+
return undefined;
|
|
132
|
+
const joined = joinedCell(look.style.anchor);
|
|
133
|
+
if (joined?.follows === true)
|
|
134
|
+
return { joined };
|
|
135
|
+
const from = { fg: look.pair.fg, bg: look.pair.bg, attributes: look.attributes };
|
|
136
|
+
const { terminal, reverse } = look.pair;
|
|
137
|
+
return { painted: glyphs.map(({ cell }) => (cell === undefined ? from : returned(frame(from, { ...cell, seed: cellSeed(this.key, cell.row, cell.col), terminal, reverse })))) };
|
|
138
|
+
});
|
|
139
|
+
const beside = edgesBeside(walked, drawings);
|
|
140
|
+
for (const [i, { segment, look, glyphs }] of walked.entries()) {
|
|
141
|
+
const drawing = drawings[i];
|
|
142
|
+
if (look === undefined || drawing === undefined)
|
|
151
143
|
yield segment;
|
|
152
|
-
|
|
144
|
+
else if ("joined" in drawing)
|
|
145
|
+
yield this._rejoined(segment, look, drawing.joined, glyphs, beside, frame, depth);
|
|
146
|
+
else
|
|
147
|
+
yield* this._repainted(segment, look, glyphs, drawing.painted, depth);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** `segment`'s cells drawn in the looks the effect painted them. */
|
|
151
|
+
*_repainted(segment, look, glyphs, moved, depth) {
|
|
152
|
+
const { style: base, wire, slots, pair, attributes } = look;
|
|
153
|
+
// [LAW:one-source-of-truth] A run is cells the wire writes alike: each
|
|
154
|
+
// cell is drawn at the output depth first and merged on the colours that
|
|
155
|
+
// writes and on its attributes, so two truecolors landing on one index
|
|
156
|
+
// stay one run. A cell the effect gave its neighbour's look is drawn as
|
|
157
|
+
// that neighbour was.
|
|
158
|
+
const runs = [];
|
|
159
|
+
for (const [k, { glyph }] of glyphs.entries()) {
|
|
160
|
+
const now = moved[k];
|
|
161
|
+
// Back from the screen to the style's slots: the same swap undoes itself.
|
|
162
|
+
const colors = onScreen(now, pair.reverse);
|
|
163
|
+
const last = runs[runs.length - 1];
|
|
164
|
+
const writes = last !== undefined && sameColors(last.colors, colors) ? last.drawn : this.respec(wire, slots, colors, depth);
|
|
165
|
+
if (last !== undefined && writesAlike(last.drawn, writes) && sameAttributes(last.attributes, now.attributes)) {
|
|
166
|
+
last.text += glyph;
|
|
167
|
+
last.cells += cellLen(glyph);
|
|
168
|
+
last.colors = colors;
|
|
153
169
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const placed = base.shiftedBy(offset);
|
|
157
|
-
const shifted = sameAttributes(run.attributes, attributes) ? placed : placed.withAttributes(cellAttributes(run.attributes));
|
|
158
|
-
const style = writesAlike(run.drawn, wire) ? shifted : shifted.add(Style.fromColor(run.drawn.color, run.drawn.bgcolor));
|
|
159
|
-
yield new Segment(run.text, style);
|
|
160
|
-
offset += run.cells;
|
|
170
|
+
else {
|
|
171
|
+
runs.push({ text: glyph, cells: cellLen(glyph), colors, attributes: now.attributes, drawn: writes });
|
|
161
172
|
}
|
|
162
173
|
}
|
|
174
|
+
if (runs.every((run) => writesAlike(run.drawn, wire) && sameAttributes(run.attributes, attributes))) {
|
|
175
|
+
yield segment;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
let offset = 0;
|
|
179
|
+
for (const run of runs) {
|
|
180
|
+
const placed = base.shiftedBy(offset);
|
|
181
|
+
const shifted = sameAttributes(run.attributes, attributes) ? placed : placed.withAttributes(cellAttributes(run.attributes));
|
|
182
|
+
const style = writesAlike(run.drawn, wire) ? shifted : shifted.add(Style.fromColor(run.drawn.color, run.drawn.bgcolor));
|
|
183
|
+
yield new Segment(run.text, style);
|
|
184
|
+
offset += run.cells;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* A join cell's `segment` resolved again from the edges beside it, as the
|
|
189
|
+
* effect left them: each neighbour's nearest cell to the join on its row,
|
|
190
|
+
* its slots as the depth writes them. A side whose neighbour the effect moved
|
|
191
|
+
* nothing of keeps the edge the cell was drawn from, so with neither moved
|
|
192
|
+
* the cell is as it was drawn.
|
|
193
|
+
*/
|
|
194
|
+
_rejoined(segment, look, joined, glyphs, beside, frame, depth) {
|
|
195
|
+
const cells = glyphs.flatMap(({ cell }) => (cell === undefined ? [] : [cell]));
|
|
196
|
+
// A join cell that draws no cell shows nothing an effect could move.
|
|
197
|
+
const [at] = cells;
|
|
198
|
+
if (at === undefined)
|
|
199
|
+
return segment;
|
|
200
|
+
const { theme } = this;
|
|
201
|
+
// [LAW:one-source-of-truth] A moved edge is the spec the depth writes its
|
|
202
|
+
// slot in, the spec the joiner reads of an item drawn so: a palette slot
|
|
203
|
+
// stays that slot, which the lift tells apart by number, never an RGB read
|
|
204
|
+
// back through a palette the writer did not use. Only the terminal's own
|
|
205
|
+
// colour names none, and the terminal the effect draws on says what it is.
|
|
206
|
+
// Each such spec is kept, so the writer below knows it by identity: an
|
|
207
|
+
// RGB a blend or another colour lands on is not the terminal's own.
|
|
208
|
+
const terminals = new Set();
|
|
209
|
+
const edgeOf = (spec, foreground) => {
|
|
210
|
+
if (!spec?.isDefault)
|
|
211
|
+
return spec;
|
|
212
|
+
const own = ColorSpec.fromRgba(spec.getTruecolor(theme, foreground));
|
|
213
|
+
terminals.add(own);
|
|
214
|
+
return own;
|
|
215
|
+
};
|
|
216
|
+
const moved = (from, to) => {
|
|
217
|
+
const specs = movedSpecs(from, to, depth, theme);
|
|
218
|
+
return specs.fg === undefined && specs.bg === undefined ? undefined : { color: edgeOf(specs.fg, true), bgcolor: edgeOf(specs.bg, false) };
|
|
219
|
+
};
|
|
220
|
+
// [LAW:one-source-of-truth] A neighbour the frame shows no cell of beside
|
|
221
|
+
// the join (cut off by a narrower container, scrolled out of a `Viewport`,
|
|
222
|
+
// covered, or an item that draws nothing) is painted by the effect where
|
|
223
|
+
// the seam shows it: its edge, as the seam drew it, handed to the frame at
|
|
224
|
+
// the seam's own cell. An effect over the whole element moves it as it
|
|
225
|
+
// moves everything there; one confined to some owners leaves it, since no
|
|
226
|
+
// owner drew the seam. Where the frame is whole and every item draws, each
|
|
227
|
+
// neighbour a seam reads has a cell on its row, so only a partial frame
|
|
228
|
+
// comes here [LAW:no-silent-failure].
|
|
229
|
+
const unseen = (edge) => {
|
|
230
|
+
const wire = edge.drawnColors(depth);
|
|
231
|
+
const slots = { fg: (wire.color ?? DEFAULT).getTruecolor(theme, true), bg: (wire.bgcolor ?? DEFAULT).getTruecolor(theme, false) };
|
|
232
|
+
const terminal = { fg: wire.color?.isDefault ?? true, bg: wire.bgcolor?.isDefault ?? true };
|
|
233
|
+
return moved(slots, returned(frame({ ...slots, attributes: cellAttributes(edge) }, { ...at, seed: cellSeed(this.key, at.row, at.col), terminal, reverse: false })));
|
|
234
|
+
};
|
|
235
|
+
const moveOf = (side) => {
|
|
236
|
+
const read = joined.reads[side];
|
|
237
|
+
if (read === null)
|
|
238
|
+
return undefined;
|
|
239
|
+
const cell = beside(cells, side, read.owner);
|
|
240
|
+
return cell === undefined ? unseen(read.edge) : moved(cell.look.slots, onScreen(cell.moved, cell.look.pair.reverse));
|
|
241
|
+
};
|
|
242
|
+
const rederived = joined.rederived({ left: moveOf("left"), right: moveOf("right") }, look.style, depth);
|
|
243
|
+
if (rederived === undefined)
|
|
244
|
+
return segment;
|
|
245
|
+
const { colors, style } = rederived;
|
|
246
|
+
// [LAW:one-source-of-truth] Each slot is written as the depth writes what
|
|
247
|
+
// it resolved to, by the writer the lift measured it with
|
|
248
|
+
// (`Style.drawnColors`), so a lifted ink stands off its ground in the bytes
|
|
249
|
+
// as it did when measured. A slot resolved to the colour it was before
|
|
250
|
+
// keeps the spec it was written in, as a painted cell's unmoved slot does.
|
|
251
|
+
// The terminal's own colours, which stand in a moved edge as the spec of
|
|
252
|
+
// the RGB the effect's terminal gives them and reach a slot unchanged or
|
|
253
|
+
// not at all, are written by the writer that wrote them on the item
|
|
254
|
+
// beside, on that terminal, which writes its own ground below truecolor as
|
|
255
|
+
// `default`.
|
|
256
|
+
const was = joined.style;
|
|
257
|
+
const drawn = colors.drawnColors(depth);
|
|
258
|
+
const { wire, slots } = look;
|
|
259
|
+
const rounded = this.respec(wire, slots, { fg: colors.color?.getTruecolor(theme, true) ?? slots.fg, bg: colors.bgcolor?.getTruecolor(theme, false) ?? slots.bg }, depth);
|
|
260
|
+
const written = (now, before, wrote, drew, round) => {
|
|
261
|
+
if (sameSpec(now, before))
|
|
262
|
+
return wrote;
|
|
263
|
+
// [LAW:no-silent-failure] Unreachable: a move only fills an edge in, so
|
|
264
|
+
// a slot that resolved to a colour when drawn resolves to one after.
|
|
265
|
+
if (now === undefined)
|
|
266
|
+
throw new Error(`a seam cell resolved to ${before?.name ?? "nothing"} when drawn and to nothing after an effect moved its neighbours`);
|
|
267
|
+
return terminals.has(now) ? round : drew;
|
|
268
|
+
};
|
|
269
|
+
const writes = {
|
|
270
|
+
color: written(colors.color, was.color, wire.color, drawn.color, rounded.color),
|
|
271
|
+
bgcolor: written(colors.bgcolor, was.bgcolor, wire.bgcolor, drawn.bgcolor, rounded.bgcolor),
|
|
272
|
+
};
|
|
273
|
+
return new Segment(segment.text, writesAlike(writes, wire) ? style : style.add(Style.fromColor(writes.color, writes.bgcolor)));
|
|
163
274
|
}
|
|
164
275
|
measure(options) {
|
|
165
276
|
return Measurement.get(options, this.renderable);
|
|
@@ -293,6 +404,45 @@ function walk(looked) {
|
|
|
293
404
|
const placed = walkGlyphs(looked.map(({ segment }) => segment));
|
|
294
405
|
return looked.map((item, i) => ({ ...item, glyphs: placed[i] }));
|
|
295
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Where to find the cells beside a join in `walked`, painted as `painted` says.
|
|
409
|
+
* Read by owner, never by column alone: a `FlexStrip` can set a gap or a
|
|
410
|
+
* justified fill between a seam and the items it joins.
|
|
411
|
+
*/
|
|
412
|
+
function edgesBeside(walked, drawings) {
|
|
413
|
+
let rows;
|
|
414
|
+
const index = () => {
|
|
415
|
+
const byRow = new Map();
|
|
416
|
+
walked.forEach(({ look, glyphs }, i) => {
|
|
417
|
+
const drawing = drawings[i];
|
|
418
|
+
if (look === undefined || drawing === undefined || !("painted" in drawing))
|
|
419
|
+
return;
|
|
420
|
+
const moved = drawing.painted;
|
|
421
|
+
glyphs.forEach(({ cell }, k) => {
|
|
422
|
+
if (cell === undefined)
|
|
423
|
+
return;
|
|
424
|
+
const row = byRow.get(cell.row) ?? [];
|
|
425
|
+
row.push({ col: cell.col, anchor: cell.anchor, look, moved: moved[k] });
|
|
426
|
+
byRow.set(cell.row, row);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
return byRow;
|
|
430
|
+
};
|
|
431
|
+
return (cells, side, owner) => {
|
|
432
|
+
// Cells on a row are listed left to right, as the walk places them.
|
|
433
|
+
const row = (rows ??= index()).get(cells[0].row) ?? [];
|
|
434
|
+
const first = cells[0].col;
|
|
435
|
+
const last = cells[cells.length - 1].col;
|
|
436
|
+
if (side === "right")
|
|
437
|
+
return row.find((cell) => cell.col > last && drawnBy(cell.anchor, owner));
|
|
438
|
+
for (let k = row.length - 1; k >= 0; k--) {
|
|
439
|
+
const cell = row[k];
|
|
440
|
+
if (cell.col < first && drawnBy(cell.anchor, owner))
|
|
441
|
+
return cell;
|
|
442
|
+
}
|
|
443
|
+
return undefined;
|
|
444
|
+
};
|
|
445
|
+
}
|
|
296
446
|
/** Whether `style` draws in its ink on a blank glyph: a line under, through or over it, or a frame round it. */
|
|
297
447
|
function drawsLines(style) {
|
|
298
448
|
return [style.underline, style.underline2, style.strike, style.overline, style.frame, style.encircle].some((on) => on === true);
|
|
@@ -327,32 +477,28 @@ function measured(looked, walked, depth, theme) {
|
|
|
327
477
|
};
|
|
328
478
|
}
|
|
329
479
|
/**
|
|
330
|
-
* `effect` on the
|
|
331
|
-
* nested in its output, and every other
|
|
480
|
+
* `effect` on the cells any of `owners` drew, itself or through an owner
|
|
481
|
+
* nested in its output, and every other cell left as it is: one item of a
|
|
332
482
|
* strip whose items share a fill, the items of a row one other item stands
|
|
333
|
-
* apart from, one widget of a screen. A
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
* `../core/strip.ts`). A cell none of them drew is never handed to the
|
|
483
|
+
* apart from, one widget of a screen. A strip's join cell is no item's, so it
|
|
484
|
+
* is never handed; one painted from the items beside it follows them, the
|
|
485
|
+
* moved ones included (`Recolored`). A cell none of them drew is never handed to the
|
|
337
486
|
* effect, so confining one effect to many owners costs one call a cell, not
|
|
338
487
|
* one for each owner.
|
|
339
488
|
*/
|
|
340
489
|
export function confined(effect, ...owners) {
|
|
341
490
|
const confinedTo = new Set(owners);
|
|
342
|
-
// [LAW:single-enforcer] One reading of
|
|
343
|
-
const
|
|
344
|
-
|
|
491
|
+
// [LAW:single-enforcer] One reading of whose a cell is, for the cells listed and the cells handed alike.
|
|
492
|
+
const owned = (cell) => {
|
|
493
|
+
for (let level = cell.anchor; level; level = level.inner)
|
|
494
|
+
if (confinedTo.has(level.owner))
|
|
495
|
+
return true;
|
|
496
|
+
return false;
|
|
497
|
+
};
|
|
345
498
|
// The frame as the effect sees it: the owners' cells alone. [LAW:one-source-of-truth] One narrowing, handed to both stages.
|
|
346
|
-
const
|
|
499
|
+
const narrowed = (drawn) => ({ ...drawn, cells: () => drawn.cells().filter(owned) });
|
|
347
500
|
// The owners' cells change when the effect's frame does.
|
|
348
|
-
return (drawn) => Animation.map(effect(
|
|
349
|
-
const owns = ownedOf(cell);
|
|
350
|
-
if (!any(owns))
|
|
351
|
-
return look;
|
|
352
|
-
const moved = frame(look, cell);
|
|
353
|
-
// An attribute is drawn on the glyph, in its ink, so it moves with whoever owns the ink.
|
|
354
|
-
return { fg: owns.fg ? moved.fg : look.fg, bg: owns.bg ? moved.bg : look.bg, attributes: owns.fg ? moved.attributes : look.attributes };
|
|
355
|
-
});
|
|
501
|
+
return (drawn) => Animation.map(effect(narrowed(drawn)), (frame) => (look, cell) => (owned(cell) ? frame(look, cell) : look));
|
|
356
502
|
}
|
|
357
503
|
/**
|
|
358
504
|
* What `segments`, drawn at `depth` on `theme`, draw their words in, measured
|
|
@@ -424,6 +570,10 @@ export function cellAttributes(of) {
|
|
|
424
570
|
function sameAttributes(a, b) {
|
|
425
571
|
return a === b || MOVED.every((name) => a[name] === b[name]);
|
|
426
572
|
}
|
|
573
|
+
/** Whether two specs are one colour as written: the same kind, naming the same colour. */
|
|
574
|
+
function sameSpec(a, b) {
|
|
575
|
+
return a === b || (a !== undefined && b !== undefined && a.type === b.type && a.name === b.name);
|
|
576
|
+
}
|
|
427
577
|
function sameColors(a, b) {
|
|
428
578
|
return sameColor(a.fg, b.fg) && sameColor(a.bg, b.bg);
|
|
429
579
|
}
|