@promptctl/rich-js 0.20.0 → 0.22.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/anchor.d.ts +5 -0
- package/dist/core/anchor.js +10 -0
- package/dist/core/color.d.ts +9 -0
- package/dist/core/color.js +23 -1
- package/dist/core/strip.d.ts +81 -29
- package/dist/core/strip.js +190 -66
- package/dist/core/style.js +1 -0
- package/dist/host/input.d.ts +5 -2
- package/dist/host/input.js +29 -10
- package/dist/index.d.ts +6 -6
- package/dist/index.js +3 -3
- package/dist/renderables/effect.d.ts +90 -17
- package/dist/renderables/effect.js +114 -43
- package/dist/renderables/effects.d.ts +93 -50
- package/dist/renderables/effects.js +215 -150
- package/dist/renderables/flexStrip.d.ts +1 -1
- package/dist/renderables/flexStrip.js +10 -9
- package/dist/renderables/progressBar.js +3 -2
- package/dist/widgets/hit.js +2 -4
- package/package.json +6 -2
package/dist/host/input.js
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Lone-ESC handling: a bare `\x1b` is ambiguous (escape key vs. start of a
|
|
18
18
|
* CSI sequence). When the buffer is drained but ends with a lone ESC, the
|
|
19
|
-
* parser defers emission via setTimeout(0).
|
|
20
|
-
*
|
|
19
|
+
* parser defers emission via setTimeout(0). A next chunk opening with `[` or
|
|
20
|
+
* `O` extends the sequence; any other next chunk emits the ESC as Escape
|
|
21
|
+
* first. `flush()` drains it synchronously.
|
|
21
22
|
*/
|
|
22
23
|
/**
|
|
23
24
|
* The one name a key goes by — what a keymap is keyed by and what
|
|
@@ -164,12 +165,26 @@ export class InputParser {
|
|
|
164
165
|
/** Feed a chunk of bytes (or a string of them) into the parser. */
|
|
165
166
|
feed(chunk) {
|
|
166
167
|
const next = typeof chunk === "string" ? UTF8_ENCODER.encode(chunk) : chunk;
|
|
167
|
-
|
|
168
|
+
// A held lone ESC waits only for the rest of a split CSI/SS3 introducer.
|
|
169
|
+
// Any other next chunk means Escape was its own key, even when the hold
|
|
170
|
+
// timer has not run yet: a busy worker can queue the next key ahead of it,
|
|
171
|
+
// and joining them would read Escape then G as Alt+G (rich-host-input-0c4f).
|
|
172
|
+
const escaped = this.holdingEscape() && next.length > 0 && next[0] !== LBRACKET && next[0] !== O_BYTE;
|
|
168
173
|
this.cancelEscape();
|
|
169
|
-
|
|
174
|
+
// The buffer is settled before the sink hears Escape, so a sink that
|
|
175
|
+
// resets the parser on it (an app stopping) leaves nothing to drain.
|
|
176
|
+
this.buffer = concatBytes(escaped ? EMPTY_BYTES : this.buffer, next);
|
|
177
|
+
try {
|
|
178
|
+
if (escaped)
|
|
179
|
+
this.sink.key(ESCAPE);
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
this.drain();
|
|
183
|
+
}
|
|
170
184
|
// [LAW:dataflow-not-control-flow] If a lone ESC remains, defer emission
|
|
171
|
-
// until the next macrotask so a follow-up chunk can extend it.
|
|
172
|
-
|
|
185
|
+
// until the next macrotask so a follow-up chunk can extend it. Only a lone
|
|
186
|
+
// one: a sequence split after `ESC [` waits for its rest, however late.
|
|
187
|
+
if (this.holdingEscape()) {
|
|
173
188
|
this.escTimer = setTimeout(() => {
|
|
174
189
|
this.escTimer = undefined;
|
|
175
190
|
this.flush();
|
|
@@ -179,9 +194,9 @@ export class InputParser {
|
|
|
179
194
|
/** Force any pending lone ESC out of the buffer. */
|
|
180
195
|
flush() {
|
|
181
196
|
this.cancelEscape();
|
|
182
|
-
if (this.
|
|
197
|
+
if (this.holdingEscape()) {
|
|
183
198
|
this.buffer = EMPTY_BYTES;
|
|
184
|
-
this.sink.key(
|
|
199
|
+
this.sink.key(ESCAPE);
|
|
185
200
|
}
|
|
186
201
|
}
|
|
187
202
|
/** Drop what is buffered and any pending lone ESC: the parser as constructed. */
|
|
@@ -189,6 +204,10 @@ export class InputParser {
|
|
|
189
204
|
this.cancelEscape();
|
|
190
205
|
this.buffer = EMPTY_BYTES;
|
|
191
206
|
}
|
|
207
|
+
/** The parser's one hold: a lone ESC that may yet open a sequence. */
|
|
208
|
+
holdingEscape() {
|
|
209
|
+
return this.buffer.length === 1 && this.buffer[0] === ESC;
|
|
210
|
+
}
|
|
192
211
|
cancelEscape() {
|
|
193
212
|
clearTimeout(this.escTimer);
|
|
194
213
|
this.escTimer = undefined;
|
|
@@ -294,7 +313,7 @@ export class InputParser {
|
|
|
294
313
|
return {
|
|
295
314
|
kind: "key",
|
|
296
315
|
bytes: 2,
|
|
297
|
-
event:
|
|
316
|
+
event: ESCAPE,
|
|
298
317
|
};
|
|
299
318
|
}
|
|
300
319
|
// ESC <named> / ESC <printable> → Alt-modified key.
|
|
@@ -347,7 +366,7 @@ export class InputParser {
|
|
|
347
366
|
return {
|
|
348
367
|
kind: "key",
|
|
349
368
|
bytes: 1,
|
|
350
|
-
event:
|
|
369
|
+
event: ESCAPE,
|
|
351
370
|
};
|
|
352
371
|
}
|
|
353
372
|
consumeSS3(buf) {
|
package/dist/index.d.ts
CHANGED
|
@@ -33,8 +33,8 @@ export type { Unsubscribe } from "./core/subscription.js";
|
|
|
33
33
|
export { Measurement, measureRenderables } from "./core/measure.js";
|
|
34
34
|
export { Span, RichText } from "./core/text.js";
|
|
35
35
|
export type { RichTextOptions } from "./core/text.js";
|
|
36
|
-
export { Strip, CellJoiner, SEAM_MIN_DELTA_E,
|
|
37
|
-
export type { StyledRenderable, Joiner, JoinShape, JoinCell, CellColor, Paint } from "./core/strip.js";
|
|
36
|
+
export { Strip, CellJoiner, SEAM_MIN_DELTA_E, JOINERS, joinerNamed, gradientJoiner, } from "./core/strip.js";
|
|
37
|
+
export type { StyledRenderable, Joiner, JoinContext, JoinShape, JoinCell, CellColor, Paint } from "./core/strip.js";
|
|
38
38
|
export { renderToString, segmentToString, segmentsToString, } from "./core/render.js";
|
|
39
39
|
export type { RenderToStringOptions } from "./core/render.js";
|
|
40
40
|
export { OSC8, osc8Sequences } from "./core/osc8.js";
|
|
@@ -55,10 +55,10 @@ export { Console } from "./core/console.js";
|
|
|
55
55
|
export type { ConsoleOptions, ConsoleSink, ConsoleStream, ConsoleEnvironment, LiveRegion, PrintOptions, } from "./core/console.js";
|
|
56
56
|
export type { Env } from "./core/env.js";
|
|
57
57
|
export { Constrain } from "./renderables/constrain.js";
|
|
58
|
-
export { Effected } from "./renderables/effect.js";
|
|
59
|
-
export type { CellColors, Effect, EffectCell, EffectedOptions } from "./renderables/effect.js";
|
|
60
|
-
export { EFFECT_CURVES, EFFECT_LIGHTS,
|
|
61
|
-
export type { Curve, Field, Loop,
|
|
58
|
+
export { Effected, confined, drawnOf } from "./renderables/effect.js";
|
|
59
|
+
export type { CellColors, Drawn, Effect, EffectCell, EffectedOptions, Pair } from "./renderables/effect.js";
|
|
60
|
+
export { EFFECT_CATALOGUE, EFFECT_CURVES, EFFECT_LIGHTS, dissolveOut, fadeIn, onColors, pulse, shimmer, sparkle, wheel, } from "./renderables/effects.js";
|
|
61
|
+
export type { Curve, EffectTarget, Field, Loop, LoopName, Touch, Transition, TransitionName } from "./renderables/effects.js";
|
|
62
62
|
export { Align } from "./renderables/align.js";
|
|
63
63
|
export type { Alignment } from "./core/place.js";
|
|
64
64
|
export { Padding } from "./renderables/padding.js";
|
package/dist/index.js
CHANGED
|
@@ -44,7 +44,7 @@ export { Measurement, measureRenderables } from "./core/measure.js";
|
|
|
44
44
|
// Text
|
|
45
45
|
export { Span, RichText } from "./core/text.js";
|
|
46
46
|
// Strip + Joiner
|
|
47
|
-
export { Strip, CellJoiner, SEAM_MIN_DELTA_E,
|
|
47
|
+
export { Strip, CellJoiner, SEAM_MIN_DELTA_E, JOINERS, joinerNamed, gradientJoiner, } from "./core/strip.js";
|
|
48
48
|
// renderToString — stateless one-shot emission
|
|
49
49
|
export { renderToString, segmentToString, segmentsToString, } from "./core/render.js";
|
|
50
50
|
// OSC 8 hyperlink wire grammar — for consumers that read rendered bytes back
|
|
@@ -69,8 +69,8 @@ export { SPINNERS, DEFAULT_SPINNER } from "./core/spinnerData.js";
|
|
|
69
69
|
export { Console } from "./core/console.js";
|
|
70
70
|
// Renderables
|
|
71
71
|
export { Constrain } from "./renderables/constrain.js";
|
|
72
|
-
export { Effected } from "./renderables/effect.js";
|
|
73
|
-
export { EFFECT_CURVES, EFFECT_LIGHTS,
|
|
72
|
+
export { Effected, confined, drawnOf } from "./renderables/effect.js";
|
|
73
|
+
export { EFFECT_CATALOGUE, EFFECT_CURVES, EFFECT_LIGHTS, dissolveOut, fadeIn, onColors, pulse, shimmer, sparkle, wheel, } from "./renderables/effects.js";
|
|
74
74
|
export { Align } from "./renderables/align.js";
|
|
75
75
|
export { Padding } from "./renderables/padding.js";
|
|
76
76
|
export { Viewport, ViewportPerKey, SCROLLBAR } from "./renderables/viewport.js";
|
|
@@ -2,11 +2,21 @@
|
|
|
2
2
|
* Effected — a renderable whose finished cells' colours pass through an
|
|
3
3
|
* effect at a moment `t`.
|
|
4
4
|
*
|
|
5
|
-
* An effect is one pure function: a
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* An effect is one pure function in two stages: a time and what the element
|
|
6
|
+
* draws in that frame in, a function of the frame out; and into that, a
|
|
7
|
+
* cell's colours and where the cell is, the cell's new colours out. Whatever
|
|
8
|
+
* holds for the whole frame is worked out in the first stage, once, and every
|
|
9
|
+
* cell of the frame is handed to the second. Shimmer, pulse, fade and
|
|
10
|
+
* dissolve are all this one shape, differing by what the function does with
|
|
11
|
+
* its inputs [LAW:one-type-per-behavior]; two effects stack by nesting one
|
|
12
|
+
* wrapper in another, which is the same as calling one after the other but
|
|
13
|
+
* for what the outer one measures: the inner one's output.
|
|
14
|
+
*
|
|
15
|
+
* What the element draws (`Drawn`) is measured here, in the render, off the
|
|
16
|
+
* same reading of each segment its cells are drawn from, and handed to the
|
|
17
|
+
* effect's first stage, once a frame. It is what an effect keeping words legible is
|
|
18
|
+
* settled against (`onColors`, `./effects.ts`), so no caller derives it a
|
|
19
|
+
* second time from a render of its own.
|
|
10
20
|
*
|
|
11
21
|
* [LAW:no-ambient-temporal-coupling] `t` is a constructor argument, not a
|
|
12
22
|
* clock read. Whoever draws frames builds the view for each frame's `t`, so
|
|
@@ -33,7 +43,8 @@
|
|
|
33
43
|
* Positions are the child's own: row and column count from the top-left of
|
|
34
44
|
* what the child drew, so a sweep crosses the element, not the screen.
|
|
35
45
|
*/
|
|
36
|
-
import { type
|
|
46
|
+
import { type Anchor } from "../core/anchor.js";
|
|
47
|
+
import { ColorDepth, type ColorRgba, type TerminalTheme } from "../core/color.js";
|
|
37
48
|
import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
|
|
38
49
|
import { Segment } from "../core/segment.js";
|
|
39
50
|
/**
|
|
@@ -65,9 +76,55 @@ export interface EffectCell {
|
|
|
65
76
|
readonly fg: boolean;
|
|
66
77
|
readonly bg: boolean;
|
|
67
78
|
};
|
|
79
|
+
/**
|
|
80
|
+
* Who drew the cell, and where it sits in their output, as
|
|
81
|
+
* `Segment.anchorAt` reads it off a frame: what tells one owner's cells
|
|
82
|
+
* from another's when their colours are alike (`confined`).
|
|
83
|
+
*/
|
|
84
|
+
readonly anchor: Anchor | undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A cell's ink and ground as the screen shows them, which of the two are the
|
|
88
|
+
* terminal's, and whether its style swaps them (`reverse`): what an `Effect`
|
|
89
|
+
* is handed for that cell.
|
|
90
|
+
*/
|
|
91
|
+
export interface Pair extends CellColors {
|
|
92
|
+
readonly terminal: EffectCell["terminal"];
|
|
93
|
+
/**
|
|
94
|
+
* Whether the ink is written in the style's ground slot and the ground in
|
|
95
|
+
* its ink slot. Below truecolor the slot decides what a moved colour can
|
|
96
|
+
* land on: a slot set to `default` draws the terminal's foreground in one
|
|
97
|
+
* and its background in the other.
|
|
98
|
+
*/
|
|
99
|
+
readonly reverse: boolean;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The colours the element an effect wraps draws its words in, in one frame,
|
|
103
|
+
* as the screen shows them: what an effect that must keep the words legible
|
|
104
|
+
* is measured against. A word cell is one whose glyph is not blank and that a
|
|
105
|
+
* strip's joiner did not draw (`JOIN`): a seam's ink is the fill beside it,
|
|
106
|
+
* not words.
|
|
107
|
+
*/
|
|
108
|
+
export interface Drawn {
|
|
109
|
+
/** Every distinct ink and ground of a word cell. */
|
|
110
|
+
readonly pairs: readonly Pair[];
|
|
111
|
+
/** The colours the element sets itself in its word cells, by hex: never the terminal's, whatever its hex. */
|
|
112
|
+
readonly colors: ReadonlySet<string>;
|
|
113
|
+
/** Of `colors`, the ones a word cell has for its ground. */
|
|
114
|
+
readonly fills: ReadonlySet<string>;
|
|
115
|
+
/** The depth the frame is written at: below truecolor a moved colour lands on a palette colour. */
|
|
116
|
+
readonly depth: ColorDepth;
|
|
117
|
+
/** The terminal the frame is drawn on. */
|
|
118
|
+
readonly theme: TerminalTheme;
|
|
68
119
|
}
|
|
69
|
-
/**
|
|
70
|
-
|
|
120
|
+
/**
|
|
121
|
+
* The frame at time `t`, in seconds, whose element draws `drawn`: each cell's
|
|
122
|
+
* colours in that frame. Return `colors` to leave a cell be.
|
|
123
|
+
* [LAW:types-are-the-program] Per-frame work is the outer stage, so it runs
|
|
124
|
+
* once a frame by the shape of the call, never by a cache asking whether this
|
|
125
|
+
* cell's frame is the last one's.
|
|
126
|
+
*/
|
|
127
|
+
export type Effect = (t: number, drawn: Drawn) => (colors: CellColors, cell: EffectCell) => CellColors;
|
|
71
128
|
export interface EffectedOptions {
|
|
72
129
|
/** The moment to draw, in seconds. */
|
|
73
130
|
readonly t: number;
|
|
@@ -95,19 +152,35 @@ export declare class Effected implements Renderable, Measurable {
|
|
|
95
152
|
minimum: number;
|
|
96
153
|
maximum: number;
|
|
97
154
|
};
|
|
155
|
+
/**
|
|
156
|
+
* The colours the screen shows for `pair` once it is drawn moved to `to`,
|
|
157
|
+
* both as the screen shows them, at `depth` on `theme`: what an effect
|
|
158
|
+
* moving a pair's colours leaves its words reading in.
|
|
159
|
+
* [LAW:one-source-of-truth] Read through `movedSpecs`, the one writer a
|
|
160
|
+
* moved slot is written with.
|
|
161
|
+
*/
|
|
162
|
+
static shows(pair: Pair, to: CellColors, depth: ColorDepth, theme: TerminalTheme): CellColors;
|
|
98
163
|
/**
|
|
99
164
|
* The specs that draw a cell's slot colours `to` where the segment drew
|
|
100
|
-
* `from` with `wire`:
|
|
101
|
-
*
|
|
102
|
-
* `SURFACE_BLACK`, the glyph on that ground, as the writer lays any
|
|
103
|
-
* translucent colour (`Style.drawnColors`) — then drawn in the colour nearest it at `depth`
|
|
104
|
-
* that the terminal shows (`ColorSpec.matchOn`). A move the depth cannot
|
|
105
|
-
* show is no move: where the written spec is as near the moved colour as
|
|
106
|
-
* that match, the slot keeps it, so below truecolor a colour moved a little
|
|
107
|
-
* off any colour it can write — a theme slot, the default colour, the cube —
|
|
108
|
-
* is still drawn as that one.
|
|
165
|
+
* `from` with `wire`: each moved slot in the spec `movedSpecs` writes it
|
|
166
|
+
* in, the rest as `wire` wrote them.
|
|
109
167
|
*/
|
|
110
168
|
private respec;
|
|
111
169
|
/** A cell's seed: FNV-1a of the key and the position, scaled into [0, 1). */
|
|
112
170
|
private seed;
|
|
113
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* `effect` on the colours `owner` drew, itself or through an owner nested in
|
|
174
|
+
* its output, and every other colour left as it is: one item of a strip whose
|
|
175
|
+
* items share a fill, one widget of a screen. A seam cell shows its two
|
|
176
|
+
* neighbours side by side, a powerline arrow in the fill it points out of
|
|
177
|
+
* over the fill it points into, so each side moves with its own item
|
|
178
|
+
* (`colorsOwnedBy`, `../core/strip.ts`).
|
|
179
|
+
*/
|
|
180
|
+
export declare function confined(effect: Effect, owner: object): Effect;
|
|
181
|
+
/**
|
|
182
|
+
* What `segments`, drawn at `depth` on `theme`, draw their words in, measured
|
|
183
|
+
* as `Effected` measures its child for every frame. Measure an element's
|
|
184
|
+
* output under an effect to read the contrast its words are left at.
|
|
185
|
+
*/
|
|
186
|
+
export declare function drawnOf(segments: Iterable<Segment>, depth: ColorDepth, theme: TerminalTheme): Drawn;
|
|
@@ -2,11 +2,21 @@
|
|
|
2
2
|
* Effected — a renderable whose finished cells' colours pass through an
|
|
3
3
|
* effect at a moment `t`.
|
|
4
4
|
*
|
|
5
|
-
* An effect is one pure function: a
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* An effect is one pure function in two stages: a time and what the element
|
|
6
|
+
* draws in that frame in, a function of the frame out; and into that, a
|
|
7
|
+
* cell's colours and where the cell is, the cell's new colours out. Whatever
|
|
8
|
+
* holds for the whole frame is worked out in the first stage, once, and every
|
|
9
|
+
* cell of the frame is handed to the second. Shimmer, pulse, fade and
|
|
10
|
+
* dissolve are all this one shape, differing by what the function does with
|
|
11
|
+
* its inputs [LAW:one-type-per-behavior]; two effects stack by nesting one
|
|
12
|
+
* wrapper in another, which is the same as calling one after the other but
|
|
13
|
+
* for what the outer one measures: the inner one's output.
|
|
14
|
+
*
|
|
15
|
+
* What the element draws (`Drawn`) is measured here, in the render, off the
|
|
16
|
+
* same reading of each segment its cells are drawn from, and handed to the
|
|
17
|
+
* effect's first stage, once a frame. It is what an effect keeping words legible is
|
|
18
|
+
* settled against (`onColors`, `./effects.ts`), so no caller derives it a
|
|
19
|
+
* second time from a render of its own.
|
|
10
20
|
*
|
|
11
21
|
* [LAW:no-ambient-temporal-coupling] `t` is a constructor argument, not a
|
|
12
22
|
* clock read. Whoever draws frames builds the view for each frame's `t`, so
|
|
@@ -33,11 +43,13 @@
|
|
|
33
43
|
* Positions are the child's own: row and column count from the top-left of
|
|
34
44
|
* what the child drew, so a sweep crosses the element, not the screen.
|
|
35
45
|
*/
|
|
46
|
+
import { drawnBy, shiftAnchor } from "../core/anchor.js";
|
|
36
47
|
import { cellLen, graphemes } from "../core/cells.js";
|
|
37
|
-
import { ColorDepth, ColorSpec, SURFACE_BLACK
|
|
48
|
+
import { ColorDepth, ColorSpec, SURFACE_BLACK } from "../core/color.js";
|
|
38
49
|
import { fnv1a } from "../core/fnv1a.js";
|
|
39
50
|
import { Measurement } from "../core/measure.js";
|
|
40
51
|
import { Segment } from "../core/segment.js";
|
|
52
|
+
import { JOIN, colorsOwnedBy } from "../core/strip.js";
|
|
41
53
|
import { Style } from "../core/style.js";
|
|
42
54
|
export class Effected {
|
|
43
55
|
renderable;
|
|
@@ -63,30 +75,27 @@ export class Effected {
|
|
|
63
75
|
}
|
|
64
76
|
// No depth named is no downgrade, as `drawnColors` reads it.
|
|
65
77
|
const depth = options.colorSystem ?? ColorDepth.TRUECOLOR;
|
|
78
|
+
// [LAW:one-source-of-truth] The frame is measured before any cell is
|
|
79
|
+
// handed to the effect, off the same looks its cells are drawn from.
|
|
80
|
+
const looked = looks(segments, depth, this.theme);
|
|
81
|
+
const frame = this.effect(this.t, measured(looked, depth, this.theme));
|
|
66
82
|
let row = 0;
|
|
67
83
|
let col = 0;
|
|
68
|
-
for (const segment of
|
|
69
|
-
if (
|
|
84
|
+
for (const { segment, look } of looked) {
|
|
85
|
+
if (look === undefined) {
|
|
70
86
|
yield segment;
|
|
71
87
|
continue;
|
|
72
88
|
}
|
|
73
|
-
const base
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// leaves to the terminal. A slot the effect leaves alone keeps the
|
|
77
|
-
// written spec, so it stays what the screen showed whatever moves
|
|
78
|
-
// beside it.
|
|
79
|
-
const wire = base.drawnColors(options.colorSystem ?? undefined);
|
|
80
|
-
const ink = (wire.color ?? DEFAULT).getTruecolor(this.theme, true);
|
|
81
|
-
const paper = (wire.bgcolor ?? DEFAULT).getTruecolor(this.theme, false);
|
|
82
|
-
const slots = { fg: ink, bg: paper };
|
|
83
|
-
const from = onScreen(slots, base);
|
|
84
|
-
const terminal = onScreen({ fg: wire.color?.isDefault ?? true, bg: wire.bgcolor?.isDefault ?? true }, base);
|
|
89
|
+
const { style: base, wire, slots, pair } = look;
|
|
90
|
+
const { terminal } = pair;
|
|
91
|
+
const from = { fg: pair.fg, bg: pair.bg };
|
|
85
92
|
// [LAW:one-source-of-truth] A run is cells the wire writes alike: each
|
|
86
93
|
// cell is drawn at the output depth first and merged on the colours that
|
|
87
94
|
// writes, so two truecolors landing on one index stay one run. A cell
|
|
88
95
|
// the effect gave its neighbour's colours is drawn as that neighbour was.
|
|
89
96
|
const runs = [];
|
|
97
|
+
// Cells into the segment, as `base.shiftedBy` counts them below.
|
|
98
|
+
let into = 0;
|
|
90
99
|
for (const glyph of graphemes(segment.text)) {
|
|
91
100
|
let colors = slots;
|
|
92
101
|
if (glyph === "\n") {
|
|
@@ -94,19 +103,21 @@ export class Effected {
|
|
|
94
103
|
col = 0;
|
|
95
104
|
}
|
|
96
105
|
else {
|
|
106
|
+
const anchor = base.anchor && shiftAnchor(base.anchor, into);
|
|
97
107
|
// Back from the screen to the style's slots: the same swap undoes itself.
|
|
98
|
-
colors = onScreen(
|
|
108
|
+
colors = onScreen(frame(from, { row, col, seed: this.seed(row, col), terminal, anchor }), pair.reverse);
|
|
99
109
|
col += cellLen(glyph);
|
|
100
110
|
}
|
|
111
|
+
into += cellLen(glyph);
|
|
101
112
|
const last = runs[runs.length - 1];
|
|
102
|
-
const
|
|
103
|
-
if (last !== undefined && writesAlike(last.drawn,
|
|
113
|
+
const writes = last !== undefined && sameColors(last.colors, colors) ? last.drawn : this.respec(wire, slots, colors, depth);
|
|
114
|
+
if (last !== undefined && writesAlike(last.drawn, writes)) {
|
|
104
115
|
last.text += glyph;
|
|
105
116
|
last.cells += cellLen(glyph);
|
|
106
117
|
last.colors = colors;
|
|
107
118
|
}
|
|
108
119
|
else {
|
|
109
|
-
runs.push({ text: glyph, cells: cellLen(glyph), colors, drawn });
|
|
120
|
+
runs.push({ text: glyph, cells: cellLen(glyph), colors, drawn: writes });
|
|
110
121
|
}
|
|
111
122
|
}
|
|
112
123
|
if (runs.every((run) => writesAlike(run.drawn, wire))) {
|
|
@@ -125,27 +136,26 @@ export class Effected {
|
|
|
125
136
|
measure(options) {
|
|
126
137
|
return Measurement.get(options, this.renderable);
|
|
127
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* The colours the screen shows for `pair` once it is drawn moved to `to`,
|
|
141
|
+
* both as the screen shows them, at `depth` on `theme`: what an effect
|
|
142
|
+
* moving a pair's colours leaves its words reading in.
|
|
143
|
+
* [LAW:one-source-of-truth] Read through `movedSpecs`, the one writer a
|
|
144
|
+
* moved slot is written with.
|
|
145
|
+
*/
|
|
146
|
+
static shows(pair, to, depth, theme) {
|
|
147
|
+
const from = onScreen(pair, pair.reverse);
|
|
148
|
+
const moved = movedSpecs(from, onScreen(to, pair.reverse), depth, theme);
|
|
149
|
+
return onScreen({ fg: moved.fg?.getTruecolor(theme, true) ?? from.fg, bg: moved.bg?.getTruecolor(theme, false) ?? from.bg }, pair.reverse);
|
|
150
|
+
}
|
|
128
151
|
/**
|
|
129
152
|
* The specs that draw a cell's slot colours `to` where the segment drew
|
|
130
|
-
* `from` with `wire`:
|
|
131
|
-
*
|
|
132
|
-
* `SURFACE_BLACK`, the glyph on that ground, as the writer lays any
|
|
133
|
-
* translucent colour (`Style.drawnColors`) — then drawn in the colour nearest it at `depth`
|
|
134
|
-
* that the terminal shows (`ColorSpec.matchOn`). A move the depth cannot
|
|
135
|
-
* show is no move: where the written spec is as near the moved colour as
|
|
136
|
-
* that match, the slot keeps it, so below truecolor a colour moved a little
|
|
137
|
-
* off any colour it can write — a theme slot, the default colour, the cube —
|
|
138
|
-
* is still drawn as that one.
|
|
153
|
+
* `from` with `wire`: each moved slot in the spec `movedSpecs` writes it
|
|
154
|
+
* in, the rest as `wire` wrote them.
|
|
139
155
|
*/
|
|
140
156
|
respec(wire, from, to, depth) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const glyph = to.fg.compositeOver(ground);
|
|
144
|
-
const drawn = (spec, was, now, foreground) => {
|
|
145
|
-
const match = ColorSpec.matchOn(now, depth, this.theme, foreground);
|
|
146
|
-
return rgbDistance(was, now) <= rgbDistance(match.getTruecolor(this.theme, foreground), now) ? spec : match;
|
|
147
|
-
};
|
|
148
|
-
return { color: drawn(wire.color, from.fg, glyph, true), bgcolor: drawn(wire.bgcolor, from.bg, ground, false) };
|
|
157
|
+
const moved = movedSpecs(from, to, depth, this.theme);
|
|
158
|
+
return { color: moved.fg ?? wire.color, bgcolor: moved.bg ?? wire.bgcolor };
|
|
149
159
|
}
|
|
150
160
|
/** A cell's seed: FNV-1a of the key and the position, scaled into [0, 1). */
|
|
151
161
|
seed(row, col) {
|
|
@@ -153,12 +163,73 @@ export class Effected {
|
|
|
153
163
|
}
|
|
154
164
|
}
|
|
155
165
|
const DEFAULT = ColorSpec.default();
|
|
166
|
+
/** Each of `segments` with how it is drawn at `depth` on `theme`; a control segment draws nothing. */
|
|
167
|
+
function looks(segments, depth, theme) {
|
|
168
|
+
return [...segments].map((segment) => {
|
|
169
|
+
if (segment.isControl)
|
|
170
|
+
return { segment, look: undefined };
|
|
171
|
+
const style = segment.style ?? Style.null();
|
|
172
|
+
const wire = style.drawnColors(depth);
|
|
173
|
+
const slots = { fg: (wire.color ?? DEFAULT).getTruecolor(theme, true), bg: (wire.bgcolor ?? DEFAULT).getTruecolor(theme, false) };
|
|
174
|
+
const reverse = style.reverse === true;
|
|
175
|
+
const terminal = onScreen({ fg: wire.color?.isDefault ?? true, bg: wire.bgcolor?.isDefault ?? true }, reverse);
|
|
176
|
+
return { segment, look: { style, wire, slots, pair: { ...onScreen(slots, reverse), terminal, reverse } } };
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/** What `looked` draws its words in: `Drawn`'s measure, taken once per frame. */
|
|
180
|
+
function measured(looked, depth, theme) {
|
|
181
|
+
const words = looked.flatMap(({ segment, look }) => (look !== undefined && segment.text.trim() !== "" && !drawnBy(look.style.anchor, JOIN) ? [look.pair] : []));
|
|
182
|
+
const pairs = [...new Map(words.map((pair) => [`${pair.fg.hex}/${pair.bg.hex}/${pair.terminal.fg}/${pair.terminal.bg}/${pair.reverse}`, pair])).values()];
|
|
183
|
+
// [LAW:one-source-of-truth] A slot the terminal fills is no colour of the element's, whatever its hex.
|
|
184
|
+
const own = (slot) => pairs.flatMap((pair) => (pair.terminal[slot] ? [] : [pair[slot].hex]));
|
|
185
|
+
return { pairs, colors: new Set([...own("fg"), ...own("bg")]), fills: new Set(own("bg")), depth, theme };
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* `effect` on the colours `owner` drew, itself or through an owner nested in
|
|
189
|
+
* its output, and every other colour left as it is: one item of a strip whose
|
|
190
|
+
* items share a fill, one widget of a screen. A seam cell shows its two
|
|
191
|
+
* neighbours side by side, a powerline arrow in the fill it points out of
|
|
192
|
+
* over the fill it points into, so each side moves with its own item
|
|
193
|
+
* (`colorsOwnedBy`, `../core/strip.ts`).
|
|
194
|
+
*/
|
|
195
|
+
export function confined(effect, owner) {
|
|
196
|
+
return (t, drawn) => {
|
|
197
|
+
const frame = effect(t, drawn);
|
|
198
|
+
return (colors, cell) => {
|
|
199
|
+
const owned = colorsOwnedBy(cell.anchor, owner);
|
|
200
|
+
const moved = frame(colors, cell);
|
|
201
|
+
return { fg: owned.fg ? moved.fg : colors.fg, bg: owned.bg ? moved.bg : colors.bg };
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* What `segments`, drawn at `depth` on `theme`, draw their words in, measured
|
|
207
|
+
* as `Effected` measures its child for every frame. Measure an element's
|
|
208
|
+
* output under an effect to read the contrast its words are left at.
|
|
209
|
+
*/
|
|
210
|
+
export function drawnOf(segments, depth, theme) {
|
|
211
|
+
return measured(looks(segments, depth, theme), depth, theme);
|
|
212
|
+
}
|
|
156
213
|
/**
|
|
157
214
|
* A style's colour slots as the screen shows them, or the reverse: `reverse`
|
|
158
215
|
* swaps glyph and ground, and swapping twice is where you started.
|
|
159
216
|
*/
|
|
160
|
-
function onScreen(slots,
|
|
161
|
-
return
|
|
217
|
+
function onScreen(slots, reverse) {
|
|
218
|
+
return reverse ? { fg: slots.bg, bg: slots.fg } : slots;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The specs a cell's slots are written in once their colours move from
|
|
222
|
+
* `from`, what the written specs draw, to `to`: each laid on what is beneath
|
|
223
|
+
* it — the ground on `SURFACE_BLACK`, the glyph on that ground, as the writer
|
|
224
|
+
* lays any translucent colour (`Style.drawnColors`) — then matched at `depth`
|
|
225
|
+
* (`ColorSpec.matchMove`). A slot the depth shows no move in has none, and
|
|
226
|
+
* keeps the spec it was written in.
|
|
227
|
+
*/
|
|
228
|
+
function movedSpecs(from, to, depth, theme) {
|
|
229
|
+
// [LAW:one-source-of-truth] The writer's surface, so one translucent colour draws one way in a frame.
|
|
230
|
+
const ground = to.bg.compositeOver(SURFACE_BLACK);
|
|
231
|
+
const glyph = to.fg.compositeOver(ground);
|
|
232
|
+
return { fg: ColorSpec.matchMove(from.fg, glyph, depth, theme, true), bg: ColorSpec.matchMove(from.bg, ground, depth, theme, false) };
|
|
162
233
|
}
|
|
163
234
|
/**
|
|
164
235
|
* Whether two cells' colours are written alike. A slot with no colour is not
|