@promptctl/rich-js 0.20.0 → 0.21.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.js +11 -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 +66 -8
- package/dist/renderables/effect.js +74 -23
- package/dist/renderables/effects.d.ts +28 -20
- package/dist/renderables/effects.js +54 -23
- 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, SHIMMER_WIDTH, dissolveOut, fadeIn, onColors, pulse,
|
|
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_CURVES, EFFECT_LIGHTS, SHIMMER_WIDTH, dissolveOut, fadeIn, onColors, pulse, shimmer, sparkle, wheel, } from "./renderables/effects.js";
|
|
61
|
+
export type { Curve, Field, Loop, Touch, Transition } 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, SHIMMER_WIDTH, dissolveOut, fadeIn, onColors, pulse,
|
|
72
|
+
export { Effected, confined, drawnOf } from "./renderables/effect.js";
|
|
73
|
+
export { EFFECT_CURVES, EFFECT_LIGHTS, SHIMMER_WIDTH, 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,18 @@
|
|
|
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 cell's colours, where the cell is,
|
|
6
|
-
*
|
|
7
|
-
* all this one shape, differing by what
|
|
8
|
-
* [LAW:one-type-per-behavior]; two effects
|
|
9
|
-
* another, which is the same as calling one
|
|
5
|
+
* An effect is one pure function: a cell's colours, where the cell is, a time,
|
|
6
|
+
* and what the element draws in that frame in, the cell's new colours out.
|
|
7
|
+
* Shimmer, pulse, fade and dissolve are all this one shape, differing by what
|
|
8
|
+
* the function does with its inputs [LAW:one-type-per-behavior]; two effects
|
|
9
|
+
* stack by nesting one wrapper in another, which is the same as calling one
|
|
10
|
+
* after the other but for what the outer one measures: the inner one's output.
|
|
11
|
+
*
|
|
12
|
+
* What the element draws (`Drawn`) is measured here, in the render, off the
|
|
13
|
+
* same reading of each segment its cells are drawn from, and handed to every
|
|
14
|
+
* cell of the frame alike. It is what an effect keeping words legible is
|
|
15
|
+
* settled against (`onColors`, `./effects.ts`), so no caller derives it a
|
|
16
|
+
* second time from a render of its own.
|
|
10
17
|
*
|
|
11
18
|
* [LAW:no-ambient-temporal-coupling] `t` is a constructor argument, not a
|
|
12
19
|
* clock read. Whoever draws frames builds the view for each frame's `t`, so
|
|
@@ -33,7 +40,8 @@
|
|
|
33
40
|
* Positions are the child's own: row and column count from the top-left of
|
|
34
41
|
* what the child drew, so a sweep crosses the element, not the screen.
|
|
35
42
|
*/
|
|
36
|
-
import { type
|
|
43
|
+
import { type Anchor } from "../core/anchor.js";
|
|
44
|
+
import { ColorDepth, type ColorRgba, type TerminalTheme } from "../core/color.js";
|
|
37
45
|
import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
|
|
38
46
|
import { Segment } from "../core/segment.js";
|
|
39
47
|
/**
|
|
@@ -65,9 +73,44 @@ export interface EffectCell {
|
|
|
65
73
|
readonly fg: boolean;
|
|
66
74
|
readonly bg: boolean;
|
|
67
75
|
};
|
|
76
|
+
/**
|
|
77
|
+
* Who drew the cell, and where it sits in their output, as
|
|
78
|
+
* `Segment.anchorAt` reads it off a frame: what tells one owner's cells
|
|
79
|
+
* from another's when their colours are alike (`confined`).
|
|
80
|
+
*/
|
|
81
|
+
readonly anchor: Anchor | undefined;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A cell's ink and ground as the screen shows them, and which of the two are
|
|
85
|
+
* the terminal's: what an `Effect` is handed for that cell.
|
|
86
|
+
*/
|
|
87
|
+
export interface Pair extends CellColors {
|
|
88
|
+
readonly terminal: EffectCell["terminal"];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The colours the element an effect wraps draws its words in, in one frame,
|
|
92
|
+
* as the screen shows them: what an effect that must keep the words legible
|
|
93
|
+
* is measured against. A word cell is one whose glyph is not blank and that a
|
|
94
|
+
* strip's joiner did not draw (`JOIN`): a seam's ink is the fill beside it,
|
|
95
|
+
* not words.
|
|
96
|
+
*/
|
|
97
|
+
export interface Drawn {
|
|
98
|
+
/** Every distinct ink and ground of a word cell. */
|
|
99
|
+
readonly pairs: readonly Pair[];
|
|
100
|
+
/** The colours the element sets itself in its word cells, by hex: never the terminal's, whatever its hex. */
|
|
101
|
+
readonly colors: ReadonlySet<string>;
|
|
102
|
+
/** Of `colors`, the ones a word cell has for its ground. */
|
|
103
|
+
readonly fills: ReadonlySet<string>;
|
|
104
|
+
/** The depth the frame is written at: below truecolor a moved colour lands on a palette colour. */
|
|
105
|
+
readonly depth: ColorDepth;
|
|
106
|
+
/** The terminal the frame is drawn on. */
|
|
107
|
+
readonly theme: TerminalTheme;
|
|
68
108
|
}
|
|
69
|
-
/**
|
|
70
|
-
|
|
109
|
+
/**
|
|
110
|
+
* A cell's colours at time `t`, in seconds, in a frame whose element draws
|
|
111
|
+
* `drawn`. Return `colors` to leave it be.
|
|
112
|
+
*/
|
|
113
|
+
export type Effect = (colors: CellColors, cell: EffectCell, t: number, drawn: Drawn) => CellColors;
|
|
71
114
|
export interface EffectedOptions {
|
|
72
115
|
/** The moment to draw, in seconds. */
|
|
73
116
|
readonly t: number;
|
|
@@ -111,3 +154,18 @@ export declare class Effected implements Renderable, Measurable {
|
|
|
111
154
|
/** A cell's seed: FNV-1a of the key and the position, scaled into [0, 1). */
|
|
112
155
|
private seed;
|
|
113
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* `effect` on the colours `owner` drew, itself or through an owner nested in
|
|
159
|
+
* its output, and every other colour left as it is: one item of a strip whose
|
|
160
|
+
* items share a fill, one widget of a screen. A seam cell shows its two
|
|
161
|
+
* neighbours side by side, a powerline arrow in the fill it points out of
|
|
162
|
+
* over the fill it points into, so each side moves with its own item
|
|
163
|
+
* (`colorsOwnedBy`, `../core/strip.ts`).
|
|
164
|
+
*/
|
|
165
|
+
export declare function confined(effect: Effect, owner: object): Effect;
|
|
166
|
+
/**
|
|
167
|
+
* What `segments`, drawn at `depth` on `theme`, draw their words in, measured
|
|
168
|
+
* as `Effected` measures its child for every frame. Measure an element's
|
|
169
|
+
* output under an effect to read the contrast its words are left at.
|
|
170
|
+
*/
|
|
171
|
+
export declare function drawnOf(segments: Iterable<Segment>, depth: ColorDepth, theme: TerminalTheme): Drawn;
|
|
@@ -2,11 +2,18 @@
|
|
|
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 cell's colours, where the cell is,
|
|
6
|
-
*
|
|
7
|
-
* all this one shape, differing by what
|
|
8
|
-
* [LAW:one-type-per-behavior]; two effects
|
|
9
|
-
* another, which is the same as calling one
|
|
5
|
+
* An effect is one pure function: a cell's colours, where the cell is, a time,
|
|
6
|
+
* and what the element draws in that frame in, the cell's new colours out.
|
|
7
|
+
* Shimmer, pulse, fade and dissolve are all this one shape, differing by what
|
|
8
|
+
* the function does with its inputs [LAW:one-type-per-behavior]; two effects
|
|
9
|
+
* stack by nesting one wrapper in another, which is the same as calling one
|
|
10
|
+
* after the other but for what the outer one measures: the inner one's output.
|
|
11
|
+
*
|
|
12
|
+
* What the element draws (`Drawn`) is measured here, in the render, off the
|
|
13
|
+
* same reading of each segment its cells are drawn from, and handed to every
|
|
14
|
+
* cell of the frame alike. It is what an effect keeping words legible is
|
|
15
|
+
* settled against (`onColors`, `./effects.ts`), so no caller derives it a
|
|
16
|
+
* second time from a render of its own.
|
|
10
17
|
*
|
|
11
18
|
* [LAW:no-ambient-temporal-coupling] `t` is a constructor argument, not a
|
|
12
19
|
* clock read. Whoever draws frames builds the view for each frame's `t`, so
|
|
@@ -33,11 +40,13 @@
|
|
|
33
40
|
* Positions are the child's own: row and column count from the top-left of
|
|
34
41
|
* what the child drew, so a sweep crosses the element, not the screen.
|
|
35
42
|
*/
|
|
43
|
+
import { drawnBy, shiftAnchor } from "../core/anchor.js";
|
|
36
44
|
import { cellLen, graphemes } from "../core/cells.js";
|
|
37
45
|
import { ColorDepth, ColorSpec, SURFACE_BLACK, rgbDistance } from "../core/color.js";
|
|
38
46
|
import { fnv1a } from "../core/fnv1a.js";
|
|
39
47
|
import { Measurement } from "../core/measure.js";
|
|
40
48
|
import { Segment } from "../core/segment.js";
|
|
49
|
+
import { JOIN, colorsOwnedBy } from "../core/strip.js";
|
|
41
50
|
import { Style } from "../core/style.js";
|
|
42
51
|
export class Effected {
|
|
43
52
|
renderable;
|
|
@@ -63,30 +72,27 @@ export class Effected {
|
|
|
63
72
|
}
|
|
64
73
|
// No depth named is no downgrade, as `drawnColors` reads it.
|
|
65
74
|
const depth = options.colorSystem ?? ColorDepth.TRUECOLOR;
|
|
75
|
+
// [LAW:one-source-of-truth] The frame is measured before any cell is
|
|
76
|
+
// handed to the effect, off the same looks its cells are drawn from.
|
|
77
|
+
const looked = looks(segments, depth, this.theme);
|
|
78
|
+
const drawn = measured(looked, depth, this.theme);
|
|
66
79
|
let row = 0;
|
|
67
80
|
let col = 0;
|
|
68
|
-
for (const segment of
|
|
69
|
-
if (
|
|
81
|
+
for (const { segment, look } of looked) {
|
|
82
|
+
if (look === undefined) {
|
|
70
83
|
yield segment;
|
|
71
84
|
continue;
|
|
72
85
|
}
|
|
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);
|
|
86
|
+
const { style: base, wire, slots, pair } = look;
|
|
87
|
+
const { terminal } = pair;
|
|
88
|
+
const from = { fg: pair.fg, bg: pair.bg };
|
|
85
89
|
// [LAW:one-source-of-truth] A run is cells the wire writes alike: each
|
|
86
90
|
// cell is drawn at the output depth first and merged on the colours that
|
|
87
91
|
// writes, so two truecolors landing on one index stay one run. A cell
|
|
88
92
|
// the effect gave its neighbour's colours is drawn as that neighbour was.
|
|
89
93
|
const runs = [];
|
|
94
|
+
// Cells into the segment, as `base.shiftedBy` counts them below.
|
|
95
|
+
let into = 0;
|
|
90
96
|
for (const glyph of graphemes(segment.text)) {
|
|
91
97
|
let colors = slots;
|
|
92
98
|
if (glyph === "\n") {
|
|
@@ -94,19 +100,21 @@ export class Effected {
|
|
|
94
100
|
col = 0;
|
|
95
101
|
}
|
|
96
102
|
else {
|
|
103
|
+
const anchor = base.anchor && shiftAnchor(base.anchor, into);
|
|
97
104
|
// Back from the screen to the style's slots: the same swap undoes itself.
|
|
98
|
-
colors = onScreen(this.effect(from, { row, col, seed: this.seed(row, col), terminal }, this.t), base);
|
|
105
|
+
colors = onScreen(this.effect(from, { row, col, seed: this.seed(row, col), terminal, anchor }, this.t, drawn), base);
|
|
99
106
|
col += cellLen(glyph);
|
|
100
107
|
}
|
|
108
|
+
into += cellLen(glyph);
|
|
101
109
|
const last = runs[runs.length - 1];
|
|
102
|
-
const
|
|
103
|
-
if (last !== undefined && writesAlike(last.drawn,
|
|
110
|
+
const writes = last !== undefined && sameColors(last.colors, colors) ? last.drawn : this.respec(wire, slots, colors, depth);
|
|
111
|
+
if (last !== undefined && writesAlike(last.drawn, writes)) {
|
|
104
112
|
last.text += glyph;
|
|
105
113
|
last.cells += cellLen(glyph);
|
|
106
114
|
last.colors = colors;
|
|
107
115
|
}
|
|
108
116
|
else {
|
|
109
|
-
runs.push({ text: glyph, cells: cellLen(glyph), colors, drawn });
|
|
117
|
+
runs.push({ text: glyph, cells: cellLen(glyph), colors, drawn: writes });
|
|
110
118
|
}
|
|
111
119
|
}
|
|
112
120
|
if (runs.every((run) => writesAlike(run.drawn, wire))) {
|
|
@@ -153,6 +161,49 @@ export class Effected {
|
|
|
153
161
|
}
|
|
154
162
|
}
|
|
155
163
|
const DEFAULT = ColorSpec.default();
|
|
164
|
+
/** Each of `segments` with how it is drawn at `depth` on `theme`; a control segment draws nothing. */
|
|
165
|
+
function looks(segments, depth, theme) {
|
|
166
|
+
return [...segments].map((segment) => {
|
|
167
|
+
if (segment.isControl)
|
|
168
|
+
return { segment, look: undefined };
|
|
169
|
+
const style = segment.style ?? Style.null();
|
|
170
|
+
const wire = style.drawnColors(depth);
|
|
171
|
+
const slots = { fg: (wire.color ?? DEFAULT).getTruecolor(theme, true), bg: (wire.bgcolor ?? DEFAULT).getTruecolor(theme, false) };
|
|
172
|
+
const terminal = onScreen({ fg: wire.color?.isDefault ?? true, bg: wire.bgcolor?.isDefault ?? true }, style);
|
|
173
|
+
return { segment, look: { style, wire, slots, pair: { ...onScreen(slots, style), terminal } } };
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/** What `looked` draws its words in: `Drawn`'s measure, taken once per frame. */
|
|
177
|
+
function measured(looked, depth, theme) {
|
|
178
|
+
const words = looked.flatMap(({ segment, look }) => (look !== undefined && segment.text.trim() !== "" && !drawnBy(look.style.anchor, JOIN) ? [look.pair] : []));
|
|
179
|
+
const pairs = [...new Map(words.map((pair) => [`${pair.fg.hex}/${pair.bg.hex}/${pair.terminal.fg}/${pair.terminal.bg}`, pair])).values()];
|
|
180
|
+
// [LAW:one-source-of-truth] A slot the terminal fills is no colour of the element's, whatever its hex.
|
|
181
|
+
const own = (slot) => pairs.flatMap((pair) => (pair.terminal[slot] ? [] : [pair[slot].hex]));
|
|
182
|
+
return { pairs, colors: new Set([...own("fg"), ...own("bg")]), fills: new Set(own("bg")), depth, theme };
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* `effect` on the colours `owner` drew, itself or through an owner nested in
|
|
186
|
+
* its output, and every other colour left as it is: one item of a strip whose
|
|
187
|
+
* items share a fill, one widget of a screen. A seam cell shows its two
|
|
188
|
+
* neighbours side by side, a powerline arrow in the fill it points out of
|
|
189
|
+
* over the fill it points into, so each side moves with its own item
|
|
190
|
+
* (`colorsOwnedBy`, `../core/strip.ts`).
|
|
191
|
+
*/
|
|
192
|
+
export function confined(effect, owner) {
|
|
193
|
+
return (colors, cell, t, drawn) => {
|
|
194
|
+
const owned = colorsOwnedBy(cell.anchor, owner);
|
|
195
|
+
const moved = effect(colors, cell, t, drawn);
|
|
196
|
+
return { fg: owned.fg ? moved.fg : colors.fg, bg: owned.bg ? moved.bg : colors.bg };
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* What `segments`, drawn at `depth` on `theme`, draw their words in, measured
|
|
201
|
+
* as `Effected` measures its child for every frame. Measure an element's
|
|
202
|
+
* output under an effect to read the contrast its words are left at.
|
|
203
|
+
*/
|
|
204
|
+
export function drawnOf(segments, depth, theme) {
|
|
205
|
+
return measured(looks(segments, depth, theme), depth, theme);
|
|
206
|
+
}
|
|
156
207
|
/**
|
|
157
208
|
* A style's colour slots as the screen shows them, or the reverse: `reverse`
|
|
158
209
|
* swaps glyph and ground, and swapping twice is where you started.
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
* [LAW:no-ambient-temporal-coupling] No curve reads a clock; `t` is the
|
|
37
37
|
* effect's argument, so the frame owner decides when to sample.
|
|
38
38
|
*
|
|
39
|
-
* Every import below names something the package exports, but for `noise
|
|
40
|
-
* which the docs' effects playground page supplies itself: each of its
|
|
39
|
+
* Every import below names something the package exports, but for `noise`
|
|
40
|
+
* and `Memo`, which the docs' effects playground page supplies itself: each of its
|
|
41
41
|
* cards runs this file's own declarations as the program a visitor edits
|
|
42
42
|
* (`examples/effects-playground/programs.ts`), on the published library.
|
|
43
43
|
*/
|
|
44
44
|
import { ColorRgba } from "../core/color.js";
|
|
45
45
|
import { type Ease } from "../core/easing.js";
|
|
46
|
-
import type {
|
|
46
|
+
import type { Drawn, Effect, EffectCell, Pair } from "./effect.js";
|
|
47
47
|
/** What a loop does to one colour at strength `w` in [0, 1]: at 0, nothing. */
|
|
48
48
|
export type Touch = (color: ColorRgba, w: number) => ColorRgba;
|
|
49
49
|
/** How strongly a loop acts on a cell at a moment, in [0, 1]; `onColors` holds a field outside it to it. */
|
|
@@ -53,20 +53,13 @@ export interface Loop {
|
|
|
53
53
|
readonly touch: Touch;
|
|
54
54
|
readonly field: Field;
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
57
|
-
* A cell's ink and ground as the screen shows them, and which of the two are
|
|
58
|
-
* the terminal's: what an `Effect` is handed for that cell.
|
|
59
|
-
*/
|
|
60
|
-
export interface Pair extends CellColors {
|
|
61
|
-
readonly terminal: EffectCell["terminal"];
|
|
62
|
-
}
|
|
63
56
|
/**
|
|
64
57
|
* How much of `touch` each of `colors` (hex) can take: the largest share in
|
|
65
58
|
* [0, 1] at which every pair in `pairs` that shows it, touched at the same
|
|
66
59
|
* strength, still reads at its resting contrast or `LEGIBLE`, whichever is
|
|
67
60
|
* lower. A colour in a pair but not in `colors` takes none, and so does one
|
|
68
61
|
* the terminal supplies, whatever its hex, as in `onColors`. One of `colors`
|
|
69
|
-
* no pair writes has nothing to be measured against, and
|
|
62
|
+
* no pair writes has nothing to be measured against, and takes it all.
|
|
70
63
|
*
|
|
71
64
|
* Colours are settled lightest first, each against every partner: one
|
|
72
65
|
* already settled at its share, one not yet settled as it is. So the lighter
|
|
@@ -77,14 +70,28 @@ export interface Pair extends CellColors {
|
|
|
77
70
|
*/
|
|
78
71
|
export declare function shares(pairs: readonly Pair[], colors: ReadonlySet<string>, touch: Touch): Map<string, number>;
|
|
79
72
|
/**
|
|
80
|
-
* A loop as an `Effect` on
|
|
81
|
-
* shows them, each touched at its share of the cell's strength
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* the
|
|
85
|
-
*
|
|
73
|
+
* A loop as an `Effect` on the element's own colours, by hex, wherever a cell
|
|
74
|
+
* shows them, each touched at its share of the cell's strength: the share
|
|
75
|
+
* `shares` settles for it against the pairs the element draws its words in
|
|
76
|
+
* (`Drawn`), touched as the frame's depth writes them — at 256 colours a
|
|
77
|
+
* touched colour lands on the cube, and the contrast it spends is the cube
|
|
78
|
+
* colour's. `colors` are the ones lit, by default every colour the element
|
|
79
|
+
* sets itself. A powerline strip's fill is the ground of its cells and the
|
|
80
|
+
* ink of the seam glyphs between them, so the arrow moves with the cell it
|
|
81
|
+
* points out of. A colour the terminal supplies (`EffectCell.terminal`) is
|
|
82
|
+
* not the element's, whatever its hex, and takes no share.
|
|
83
|
+
*
|
|
84
|
+
* Settling shares is costly, so it is done once for each set of colours an
|
|
85
|
+
* element draws: make the effect once and draw it in every frame, and a frame
|
|
86
|
+
* drawing what an earlier one drew costs no settling.
|
|
87
|
+
*/
|
|
88
|
+
export declare function onColors(loop: Loop, colors?: ReadonlySet<string>): Effect;
|
|
89
|
+
/**
|
|
90
|
+
* A loop as an `Effect` on colours by hex, each touched at the share
|
|
91
|
+
* `share` gives it of the cell's strength in a frame drawing what `Drawn`
|
|
92
|
+
* says; a colour it gives none, and one the terminal supplies, stays as it is.
|
|
86
93
|
*/
|
|
87
|
-
export declare function
|
|
94
|
+
export declare function atShares(loop: Loop, share: (drawn: Drawn) => ReadonlyMap<string, number>): Effect;
|
|
88
95
|
/** How the curves below are tuned: one period (or duration), ease and swing. */
|
|
89
96
|
export interface Curve {
|
|
90
97
|
/** Seconds: a looping effect's period, a transition's duration. */
|
|
@@ -247,6 +254,7 @@ export declare function dissolveOut(curve: Curve, start: number, z: number, grou
|
|
|
247
254
|
* its own. A segment's stray is slow noise of its own, at `STRAY_PERIODS`,
|
|
248
255
|
* so no two keep step, and the row never shows one spread of hues twice.
|
|
249
256
|
* Lightness and chroma are kept, so the words read as they did; the turn
|
|
250
|
-
* only happens to
|
|
257
|
+
* only happens to the element's own colours (`Drawn.colors`), never the
|
|
258
|
+
* terminal's.
|
|
251
259
|
*/
|
|
252
|
-
export declare function wheel(curve: Curve,
|
|
260
|
+
export declare function wheel(curve: Curve, z: number): Effect;
|
|
@@ -36,14 +36,15 @@
|
|
|
36
36
|
* [LAW:no-ambient-temporal-coupling] No curve reads a clock; `t` is the
|
|
37
37
|
* effect's argument, so the frame owner decides when to sample.
|
|
38
38
|
*
|
|
39
|
-
* Every import below names something the package exports, but for `noise
|
|
40
|
-
* which the docs' effects playground page supplies itself: each of its
|
|
39
|
+
* Every import below names something the package exports, but for `noise`
|
|
40
|
+
* and `Memo`, which the docs' effects playground page supplies itself: each of its
|
|
41
41
|
* cards runs this file's own declarations as the program a visitor edits
|
|
42
42
|
* (`examples/effects-playground/programs.ts`), on the published library.
|
|
43
43
|
*/
|
|
44
|
-
import { ColorRgba, contrastRatio } from "../core/color.js";
|
|
44
|
+
import { ColorRgba, ColorSpec, contrastRatio } from "../core/color.js";
|
|
45
45
|
import { EASES, Phase } from "../core/easing.js";
|
|
46
46
|
import { fbm, hash, noise, smoothstep } from "../core/noise.js";
|
|
47
|
+
import { Memo } from "../core/memo.js";
|
|
47
48
|
import { IDENTITY, Oklch } from "../core/oklch.js";
|
|
48
49
|
/** The contrast no touch takes a cell below, unless it rested below it: WCAG AA for body text. */
|
|
49
50
|
const LEGIBLE = 4.5;
|
|
@@ -64,7 +65,7 @@ const ROUNDING = 0.01;
|
|
|
64
65
|
* strength, still reads at its resting contrast or `LEGIBLE`, whichever is
|
|
65
66
|
* lower. A colour in a pair but not in `colors` takes none, and so does one
|
|
66
67
|
* the terminal supplies, whatever its hex, as in `onColors`. One of `colors`
|
|
67
|
-
* no pair writes has nothing to be measured against, and
|
|
68
|
+
* no pair writes has nothing to be measured against, and takes it all.
|
|
68
69
|
*
|
|
69
70
|
* Colours are settled lightest first, each against every partner: one
|
|
70
71
|
* already settled at its share, one not yet settled as it is. So the lighter
|
|
@@ -81,12 +82,12 @@ export function shares(pairs, colors, touch) {
|
|
|
81
82
|
{ color: bg, own: terminal.bg ? undefined : bg.hex },
|
|
82
83
|
]);
|
|
83
84
|
const rgba = new Map(sides.flat().flatMap(({ color, own }) => (own === undefined ? [] : [[own, color]])));
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
// A colour no pair writes keeps no word legible, so nothing bounds its share:
|
|
86
|
+
// a frame drawing a lit colour only under blanks and seams, or not at all,
|
|
87
|
+
// is a frame like any other.
|
|
88
|
+
const settled = new Map([...colors].filter((hex) => !rgba.has(hex)).map((hex) => [hex, 1]));
|
|
87
89
|
const lightness = (hex) => Oklch.fromRgba(rgba.get(hex)).l;
|
|
88
|
-
const
|
|
89
|
-
for (const hex of [...colors].sort((a, b) => lightness(b) - lightness(a))) {
|
|
90
|
+
for (const hex of [...colors].filter((hex) => rgba.has(hex)).sort((a, b) => lightness(b) - lightness(a))) {
|
|
90
91
|
const partners = sides.flatMap(([fg, bg]) => {
|
|
91
92
|
const partner = fg.own === hex ? bg : bg.own === hex ? fg : undefined;
|
|
92
93
|
return partner === undefined ? [] : [[partner.color, partner.own === undefined ? 0 : (settled.get(partner.own) ?? 0)]];
|
|
@@ -106,17 +107,46 @@ export function shares(pairs, colors, touch) {
|
|
|
106
107
|
return settled;
|
|
107
108
|
}
|
|
108
109
|
/**
|
|
109
|
-
* A loop as an `Effect` on
|
|
110
|
-
* shows them, each touched at its share of the cell's strength
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* the
|
|
114
|
-
*
|
|
110
|
+
* A loop as an `Effect` on the element's own colours, by hex, wherever a cell
|
|
111
|
+
* shows them, each touched at its share of the cell's strength: the share
|
|
112
|
+
* `shares` settles for it against the pairs the element draws its words in
|
|
113
|
+
* (`Drawn`), touched as the frame's depth writes them — at 256 colours a
|
|
114
|
+
* touched colour lands on the cube, and the contrast it spends is the cube
|
|
115
|
+
* colour's. `colors` are the ones lit, by default every colour the element
|
|
116
|
+
* sets itself. A powerline strip's fill is the ground of its cells and the
|
|
117
|
+
* ink of the seam glyphs between them, so the arrow moves with the cell it
|
|
118
|
+
* points out of. A colour the terminal supplies (`EffectCell.terminal`) is
|
|
119
|
+
* not the element's, whatever its hex, and takes no share.
|
|
120
|
+
*
|
|
121
|
+
* Settling shares is costly, so it is done once for each set of colours an
|
|
122
|
+
* element draws: make the effect once and draw it in every frame, and a frame
|
|
123
|
+
* drawing what an earlier one drew costs no settling.
|
|
124
|
+
*/
|
|
125
|
+
export function onColors(loop, colors) {
|
|
126
|
+
const settled = new WeakMap();
|
|
127
|
+
const settle = (drawn) => {
|
|
128
|
+
const lit = colors ?? drawn.colors;
|
|
129
|
+
const key = [drawn.depth, ...drawn.pairs.map(({ fg, bg, terminal }) => `${fg.hex}/${bg.hex}/${terminal.fg}/${terminal.bg}`), "|", ...lit].join(" ");
|
|
130
|
+
const memo = settled.get(drawn.theme) ?? new Memo();
|
|
131
|
+
settled.set(drawn.theme, memo);
|
|
132
|
+
const asWritten = (color, w) => ColorSpec.fromRgba(loop.touch(color, w)).downgrade(drawn.depth).getTruecolor(drawn.theme);
|
|
133
|
+
return memo.get(key, () => shares(drawn.pairs, lit, asWritten));
|
|
134
|
+
};
|
|
135
|
+
return atShares(loop, settle);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* A loop as an `Effect` on colours by hex, each touched at the share
|
|
139
|
+
* `share` gives it of the cell's strength in a frame drawing what `Drawn`
|
|
140
|
+
* says; a colour it gives none, and one the terminal supplies, stays as it is.
|
|
115
141
|
*/
|
|
116
|
-
export function
|
|
117
|
-
|
|
142
|
+
export function atShares(loop, share) {
|
|
143
|
+
// Every cell of a frame is handed the same `Drawn`, so its shares are looked up once a frame.
|
|
144
|
+
let frame = { drawn: undefined, share: new Map() };
|
|
145
|
+
return ({ fg, bg }, cell, t, drawn) => {
|
|
146
|
+
if (frame.drawn !== drawn)
|
|
147
|
+
frame = { drawn, share: share(drawn) };
|
|
118
148
|
const w = Math.round(clamp01(loop.field(cell, t)) * LEVELS) / LEVELS;
|
|
119
|
-
const moved = (color, terminal) => touchedAt(loop.touch, color, (terminal ? 0 : (share.get(color.hex) ?? 0)) * w);
|
|
149
|
+
const moved = (color, terminal) => touchedAt(loop.touch, color, (terminal ? 0 : (frame.share.get(color.hex) ?? 0)) * w);
|
|
120
150
|
return { fg: moved(fg, cell.terminal.fg), bg: moved(bg, cell.terminal.bg) };
|
|
121
151
|
};
|
|
122
152
|
}
|
|
@@ -541,9 +571,10 @@ const STRAY = 180;
|
|
|
541
571
|
* its own. A segment's stray is slow noise of its own, at `STRAY_PERIODS`,
|
|
542
572
|
* so no two keep step, and the row never shows one spread of hues twice.
|
|
543
573
|
* Lightness and chroma are kept, so the words read as they did; the turn
|
|
544
|
-
* only happens to
|
|
574
|
+
* only happens to the element's own colours (`Drawn.colors`), never the
|
|
575
|
+
* terminal's.
|
|
545
576
|
*/
|
|
546
|
-
export function wheel(curve,
|
|
577
|
+
export function wheel(curve, z) {
|
|
547
578
|
requireCurve(curve);
|
|
548
579
|
const P = curve.seconds;
|
|
549
580
|
// A segment's seed, from the colour it is.
|
|
@@ -555,11 +586,11 @@ export function wheel(curve, colors, fills, z) {
|
|
|
555
586
|
const s = seed(hex);
|
|
556
587
|
return (360 * t) / P + curve.swing * STRAY * ((stray(s, t) - stray(s, 0)) / 1.5);
|
|
557
588
|
};
|
|
558
|
-
return (
|
|
589
|
+
return (shown, cell, t, { colors, fills }) => {
|
|
559
590
|
// Whether a slot shows one of the element's colours: never the terminal's, whatever its hex.
|
|
560
|
-
const own = { fg: !cell.terminal.fg && colors.has(
|
|
591
|
+
const own = { fg: !cell.terminal.fg && colors.has(shown.fg.hex), bg: !cell.terminal.bg && colors.has(shown.bg.hex) };
|
|
561
592
|
// An own colour turns with its segment: itself if a fill or beside no own colour, else the one beside it.
|
|
562
593
|
const turn = (color, mine, other, theirs) => !mine ? color : Oklch.fromRgba(color).applyKey({ ...IDENTITY, hueShift: turned(fills.has(color.hex) || !theirs ? color.hex : other.hex, t) }).toRgba();
|
|
563
|
-
return { fg: turn(
|
|
594
|
+
return { fg: turn(shown.fg, own.fg, shown.bg, own.bg), bg: turn(shown.bg, own.bg, shown.fg, own.fg) };
|
|
564
595
|
};
|
|
565
596
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { Segment } from "../core/segment.js";
|
|
19
19
|
import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
|
|
20
|
-
import type
|
|
20
|
+
import { type Joiner, type StyledRenderable } from "../core/strip.js";
|
|
21
21
|
export type FlexAlign = "left" | "center" | "right" | "justify";
|
|
22
22
|
export interface FlexStripOptions<T extends StyledRenderable> {
|
|
23
23
|
/** Joiner used between items within a line, and at line endpoints. */
|