@promptctl/rich-js 0.19.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/README.md +3 -2
- package/dist/core/anchor.d.ts +5 -0
- package/dist/core/anchor.js +10 -0
- package/dist/core/color.d.ts +1 -0
- package/dist/core/color.js +14 -3
- package/dist/core/noise.d.ts +31 -0
- package/dist/core/noise.js +94 -0
- package/dist/core/paint.js +13 -2
- package/dist/core/pretty.d.ts +23 -2
- package/dist/core/pretty.js +54 -18
- package/dist/core/protocol.d.ts +2 -0
- package/dist/core/protocol.js +3 -2
- package/dist/core/selection.d.ts +12 -0
- package/dist/core/selection.js +14 -0
- package/dist/core/strip.d.ts +159 -76
- package/dist/core/strip.js +367 -215
- package/dist/core/style.js +10 -7
- package/dist/host/app.d.ts +68 -2
- package/dist/host/app.js +125 -15
- package/dist/host/index.d.ts +3 -1
- package/dist/host/index.js +1 -0
- package/dist/host/input.d.ts +87 -0
- package/dist/host/input.js +517 -0
- package/dist/host/terminal-host.d.ts +12 -0
- package/dist/host/terminal-host.js +2 -0
- package/dist/index.d.ts +11 -6
- package/dist/index.js +7 -4
- package/dist/node/terminal-host.d.ts +7 -0
- package/dist/node/terminal-host.js +7 -0
- package/dist/renderables/effect.d.ts +89 -16
- package/dist/renderables/effect.js +115 -42
- package/dist/renderables/effects.d.ts +260 -0
- package/dist/renderables/effects.js +596 -0
- package/dist/renderables/embed.d.ts +11 -5
- package/dist/renderables/embed.js +13 -6
- package/dist/renderables/flexStrip.d.ts +1 -1
- package/dist/renderables/flexStrip.js +10 -9
- package/dist/renderables/list-view.d.ts +42 -0
- package/dist/renderables/list-view.js +78 -0
- package/dist/renderables/progress.d.ts +6 -0
- package/dist/renderables/progress.js +11 -4
- package/dist/renderables/progressBar.d.ts +22 -2
- package/dist/renderables/progressBar.js +52 -7
- package/dist/renderables/rule.d.ts +2 -2
- package/dist/renderables/rule.js +12 -10
- package/dist/renderables/table.js +5 -7
- package/dist/renderables/viewport.d.ts +30 -0
- package/dist/renderables/viewport.js +59 -3
- package/dist/widgets/dropdown.js +3 -3
- package/dist/widgets/event-router.d.ts +31 -40
- package/dist/widgets/event-router.js +52 -483
- package/dist/widgets/hit.js +2 -4
- package/dist/widgets/index.d.ts +2 -1
- package/dist/widgets/types.d.ts +8 -25
- package/dist/widgets/types.js +8 -0
- package/dist/widgets/widget-app.d.ts +22 -7
- package/dist/widgets/widget-app.js +22 -12
- package/package.json +26 -18
|
@@ -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
|
|
@@ -22,8 +29,9 @@
|
|
|
22
29
|
* cell (`Style.shiftedBy`), so hit testing still finds the owner.
|
|
23
30
|
* - A cell whose colours come back unchanged keeps its original `ColorSpec`,
|
|
24
31
|
* and a segment whose every cell is unchanged is yielded as it was, so the
|
|
25
|
-
* identity effect is byte-identical. Neighbouring cells
|
|
26
|
-
* stay one segment: a segment is cut only where the
|
|
32
|
+
* identity effect is byte-identical. Neighbouring cells the output depth
|
|
33
|
+
* writes alike stay one segment: a segment is cut only where the written
|
|
34
|
+
* colours change, so two colours that land on one index are one run.
|
|
27
35
|
* - New colours are built with `ColorSpec.fromRgba`, never parsed from a
|
|
28
36
|
* string, so no frame adds an entry to a parse cache.
|
|
29
37
|
* - With no colour output (`colorSystem: null`) there is nothing for an
|
|
@@ -32,7 +40,8 @@
|
|
|
32
40
|
* Positions are the child's own: row and column count from the top-left of
|
|
33
41
|
* what the child drew, so a sweep crosses the element, not the screen.
|
|
34
42
|
*/
|
|
35
|
-
import { type
|
|
43
|
+
import { type Anchor } from "../core/anchor.js";
|
|
44
|
+
import { ColorDepth, type ColorRgba, type TerminalTheme } from "../core/color.js";
|
|
36
45
|
import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
|
|
37
46
|
import { Segment } from "../core/segment.js";
|
|
38
47
|
/**
|
|
@@ -45,17 +54,63 @@ export interface CellColors {
|
|
|
45
54
|
readonly bg: ColorRgba;
|
|
46
55
|
}
|
|
47
56
|
/**
|
|
48
|
-
* Where a cell is in the output of the renderable an effect wraps,
|
|
49
|
-
*
|
|
50
|
-
*
|
|
57
|
+
* Where a cell is in the output of the renderable an effect wraps, its seed:
|
|
58
|
+
* a number in [0, 1) that is the same for that cell on every frame, for an
|
|
59
|
+
* effect that varies cell by cell (a per-cell offset in time, a sparkle) —
|
|
60
|
+
* and which of its on-screen colours are the terminal's.
|
|
51
61
|
*/
|
|
52
62
|
export interface EffectCell {
|
|
53
63
|
readonly row: number;
|
|
54
64
|
readonly col: number;
|
|
55
65
|
readonly seed: number;
|
|
66
|
+
/**
|
|
67
|
+
* Whether the glyph's colour and the colour behind it, as the screen shows
|
|
68
|
+
* them, are the terminal's own: a slot the element's style leaves unset or
|
|
69
|
+
* sets to `default`. Such a colour is resolved through the theme, so its
|
|
70
|
+
* hex can equal one the element wrote; this is what tells them apart.
|
|
71
|
+
*/
|
|
72
|
+
readonly terminal: {
|
|
73
|
+
readonly fg: boolean;
|
|
74
|
+
readonly bg: boolean;
|
|
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;
|
|
56
82
|
}
|
|
57
|
-
/**
|
|
58
|
-
|
|
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;
|
|
108
|
+
}
|
|
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;
|
|
59
114
|
export interface EffectedOptions {
|
|
60
115
|
/** The moment to draw, in seconds. */
|
|
61
116
|
readonly t: number;
|
|
@@ -84,15 +139,33 @@ export declare class Effected implements Renderable, Measurable {
|
|
|
84
139
|
maximum: number;
|
|
85
140
|
};
|
|
86
141
|
/**
|
|
87
|
-
* The
|
|
142
|
+
* The specs that draw a cell's slot colours `to` where the segment drew
|
|
88
143
|
* `from` with `wire`: a slot the effect left alone keeps its written spec,
|
|
89
144
|
* and a moved one is laid on what is beneath it — the ground on
|
|
90
145
|
* `SURFACE_BLACK`, the glyph on that ground, as the writer lays any
|
|
91
146
|
* translucent colour (`Style.drawnColors`) — then drawn in the colour nearest it at `depth`
|
|
92
|
-
* that the terminal shows (`ColorSpec.matchOn`)
|
|
93
|
-
*
|
|
147
|
+
* that the terminal shows (`ColorSpec.matchOn`). A move the depth cannot
|
|
148
|
+
* show is no move: where the written spec is as near the moved colour as
|
|
149
|
+
* that match, the slot keeps it, so below truecolor a colour moved a little
|
|
150
|
+
* off any colour it can write — a theme slot, the default colour, the cube —
|
|
151
|
+
* is still drawn as that one.
|
|
94
152
|
*/
|
|
95
153
|
private respec;
|
|
96
154
|
/** A cell's seed: FNV-1a of the key and the position, scaled into [0, 1). */
|
|
97
155
|
private seed;
|
|
98
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
|
|
@@ -22,8 +29,9 @@
|
|
|
22
29
|
* cell (`Style.shiftedBy`), so hit testing still finds the owner.
|
|
23
30
|
* - A cell whose colours come back unchanged keeps its original `ColorSpec`,
|
|
24
31
|
* and a segment whose every cell is unchanged is yielded as it was, so the
|
|
25
|
-
* identity effect is byte-identical. Neighbouring cells
|
|
26
|
-
* stay one segment: a segment is cut only where the
|
|
32
|
+
* identity effect is byte-identical. Neighbouring cells the output depth
|
|
33
|
+
* writes alike stay one segment: a segment is cut only where the written
|
|
34
|
+
* colours change, so two colours that land on one index are one run.
|
|
27
35
|
* - New colours are built with `ColorSpec.fromRgba`, never parsed from a
|
|
28
36
|
* string, so no frame adds an entry to a parse cache.
|
|
29
37
|
* - With no colour output (`colorSystem: null`) there is nothing for an
|
|
@@ -32,11 +40,13 @@
|
|
|
32
40
|
* Positions are the child's own: row and column count from the top-left of
|
|
33
41
|
* what the child drew, so a sweep crosses the element, not the screen.
|
|
34
42
|
*/
|
|
43
|
+
import { drawnBy, shiftAnchor } from "../core/anchor.js";
|
|
35
44
|
import { cellLen, graphemes } from "../core/cells.js";
|
|
36
|
-
import { ColorDepth, ColorSpec, SURFACE_BLACK } from "../core/color.js";
|
|
45
|
+
import { ColorDepth, ColorSpec, SURFACE_BLACK, rgbDistance } from "../core/color.js";
|
|
37
46
|
import { fnv1a } from "../core/fnv1a.js";
|
|
38
47
|
import { Measurement } from "../core/measure.js";
|
|
39
48
|
import { Segment } from "../core/segment.js";
|
|
49
|
+
import { JOIN, colorsOwnedBy } from "../core/strip.js";
|
|
40
50
|
import { Style } from "../core/style.js";
|
|
41
51
|
export class Effected {
|
|
42
52
|
renderable;
|
|
@@ -62,56 +72,59 @@ export class Effected {
|
|
|
62
72
|
}
|
|
63
73
|
// No depth named is no downgrade, as `drawnColors` reads it.
|
|
64
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);
|
|
65
79
|
let row = 0;
|
|
66
80
|
let col = 0;
|
|
67
|
-
for (const segment of
|
|
68
|
-
if (
|
|
81
|
+
for (const { segment, look } of looked) {
|
|
82
|
+
if (look === undefined) {
|
|
69
83
|
yield segment;
|
|
70
84
|
continue;
|
|
71
85
|
}
|
|
72
|
-
const base
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
const ink = (wire.color ?? DEFAULT).getTruecolor(this.theme, true);
|
|
80
|
-
const paper = (wire.bgcolor ?? DEFAULT).getTruecolor(this.theme, false);
|
|
81
|
-
const from = onScreen({ fg: ink, bg: paper }, base);
|
|
82
|
-
// Each grapheme's colours, merged into runs of equal colours as they
|
|
83
|
-
// come; `undefined` is a run left as the segment drew it.
|
|
86
|
+
const { style: base, wire, slots, pair } = look;
|
|
87
|
+
const { terminal } = pair;
|
|
88
|
+
const from = { fg: pair.fg, bg: pair.bg };
|
|
89
|
+
// [LAW:one-source-of-truth] A run is cells the wire writes alike: each
|
|
90
|
+
// cell is drawn at the output depth first and merged on the colours that
|
|
91
|
+
// writes, so two truecolors landing on one index stay one run. A cell
|
|
92
|
+
// the effect gave its neighbour's colours is drawn as that neighbour was.
|
|
84
93
|
const runs = [];
|
|
94
|
+
// Cells into the segment, as `base.shiftedBy` counts them below.
|
|
95
|
+
let into = 0;
|
|
85
96
|
for (const glyph of graphemes(segment.text)) {
|
|
86
|
-
let colors;
|
|
97
|
+
let colors = slots;
|
|
87
98
|
if (glyph === "\n") {
|
|
88
99
|
row++;
|
|
89
100
|
col = 0;
|
|
90
101
|
}
|
|
91
102
|
else {
|
|
92
|
-
const
|
|
93
|
-
|
|
103
|
+
const anchor = base.anchor && shiftAnchor(base.anchor, into);
|
|
104
|
+
// Back from the screen to the style's slots: the same swap undoes itself.
|
|
105
|
+
colors = onScreen(this.effect(from, { row, col, seed: this.seed(row, col), terminal, anchor }, this.t, drawn), base);
|
|
94
106
|
col += cellLen(glyph);
|
|
95
107
|
}
|
|
108
|
+
into += cellLen(glyph);
|
|
96
109
|
const last = runs[runs.length - 1];
|
|
97
|
-
|
|
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)) {
|
|
98
112
|
last.text += glyph;
|
|
99
113
|
last.cells += cellLen(glyph);
|
|
114
|
+
last.colors = colors;
|
|
100
115
|
}
|
|
101
116
|
else {
|
|
102
|
-
runs.push({ text: glyph, cells: cellLen(glyph), colors });
|
|
117
|
+
runs.push({ text: glyph, cells: cellLen(glyph), colors, drawn: writes });
|
|
103
118
|
}
|
|
104
119
|
}
|
|
105
|
-
if (runs.every((run) => run.
|
|
120
|
+
if (runs.every((run) => writesAlike(run.drawn, wire))) {
|
|
106
121
|
yield segment;
|
|
107
122
|
continue;
|
|
108
123
|
}
|
|
109
124
|
let offset = 0;
|
|
110
125
|
for (const run of runs) {
|
|
111
126
|
const shifted = base.shiftedBy(offset);
|
|
112
|
-
|
|
113
|
-
const slots = run.colors === undefined ? undefined : onScreen(run.colors, base);
|
|
114
|
-
const style = slots === undefined ? shifted : shifted.add(this.respec(wire, { fg: ink, bg: paper }, slots, depth));
|
|
127
|
+
const style = writesAlike(run.drawn, wire) ? shifted : shifted.add(Style.fromColor(run.drawn.color, run.drawn.bgcolor));
|
|
115
128
|
yield new Segment(run.text, style);
|
|
116
129
|
offset += run.cells;
|
|
117
130
|
}
|
|
@@ -121,20 +134,26 @@ export class Effected {
|
|
|
121
134
|
return Measurement.get(options, this.renderable);
|
|
122
135
|
}
|
|
123
136
|
/**
|
|
124
|
-
* The
|
|
137
|
+
* The specs that draw a cell's slot colours `to` where the segment drew
|
|
125
138
|
* `from` with `wire`: a slot the effect left alone keeps its written spec,
|
|
126
139
|
* and a moved one is laid on what is beneath it — the ground on
|
|
127
140
|
* `SURFACE_BLACK`, the glyph on that ground, as the writer lays any
|
|
128
141
|
* translucent colour (`Style.drawnColors`) — then drawn in the colour nearest it at `depth`
|
|
129
|
-
* that the terminal shows (`ColorSpec.matchOn`)
|
|
130
|
-
*
|
|
142
|
+
* that the terminal shows (`ColorSpec.matchOn`). A move the depth cannot
|
|
143
|
+
* show is no move: where the written spec is as near the moved colour as
|
|
144
|
+
* that match, the slot keeps it, so below truecolor a colour moved a little
|
|
145
|
+
* off any colour it can write — a theme slot, the default colour, the cube —
|
|
146
|
+
* is still drawn as that one.
|
|
131
147
|
*/
|
|
132
148
|
respec(wire, from, to, depth) {
|
|
133
149
|
// [LAW:one-source-of-truth] The writer's surface, so one translucent colour draws one way in a frame.
|
|
134
150
|
const ground = to.bg.compositeOver(SURFACE_BLACK);
|
|
135
151
|
const glyph = to.fg.compositeOver(ground);
|
|
136
|
-
const drawn = (spec, was, now, foreground) =>
|
|
137
|
-
|
|
152
|
+
const drawn = (spec, was, now, foreground) => {
|
|
153
|
+
const match = ColorSpec.matchOn(now, depth, this.theme, foreground);
|
|
154
|
+
return rgbDistance(was, now) <= rgbDistance(match.getTruecolor(this.theme, foreground), now) ? spec : match;
|
|
155
|
+
};
|
|
156
|
+
return { color: drawn(wire.color, from.fg, glyph, true), bgcolor: drawn(wire.bgcolor, from.bg, ground, false) };
|
|
138
157
|
}
|
|
139
158
|
/** A cell's seed: FNV-1a of the key and the position, scaled into [0, 1). */
|
|
140
159
|
seed(row, col) {
|
|
@@ -142,19 +161,73 @@ export class Effected {
|
|
|
142
161
|
}
|
|
143
162
|
}
|
|
144
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
|
+
}
|
|
145
207
|
/**
|
|
146
208
|
* A style's colour slots as the screen shows them, or the reverse: `reverse`
|
|
147
209
|
* swaps glyph and ground, and swapping twice is where you started.
|
|
148
210
|
*/
|
|
149
|
-
function onScreen(
|
|
150
|
-
return style.reverse === true ? { fg:
|
|
211
|
+
function onScreen(slots, style) {
|
|
212
|
+
return style.reverse === true ? { fg: slots.bg, bg: slots.fg } : slots;
|
|
151
213
|
}
|
|
152
|
-
|
|
153
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Whether two cells' colours are written alike. A slot with no colour is not
|
|
216
|
+
* the default colour: it writes nothing, so a style a container lays beneath
|
|
217
|
+
* (`Segment.applyStyle`) still shows through it, where `39`/`49` would not.
|
|
218
|
+
*/
|
|
219
|
+
function writesAlike(a, b) {
|
|
220
|
+
return slotWritesAlike(a.color, b.color, true) && slotWritesAlike(a.bgcolor, b.bgcolor, false);
|
|
221
|
+
}
|
|
222
|
+
function slotWritesAlike(a, b, foreground) {
|
|
223
|
+
return a === b || written(a, foreground) === written(b, foreground);
|
|
224
|
+
}
|
|
225
|
+
function written(spec, foreground) {
|
|
226
|
+
return spec?.getAnsiCodes(foreground).join(";") ?? "";
|
|
154
227
|
}
|
|
155
228
|
function sameColors(a, b) {
|
|
156
229
|
return sameColor(a.fg, b.fg) && sameColor(a.bg, b.bg);
|
|
157
230
|
}
|
|
158
|
-
function
|
|
159
|
-
return a ===
|
|
231
|
+
function sameColor(a, b) {
|
|
232
|
+
return a.red === b.red && a.green === b.green && a.blue === b.blue && a.alpha === b.alpha;
|
|
160
233
|
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The effect catalogue: shimmer, pulse, sparkle and wheel, which loop for as
|
|
3
|
+
* long as they are drawn, and fade-in and dissolve-out, which run once and
|
|
4
|
+
* are done. Each is an `Effect` for `Effected` (`./effect.ts`); the defaults
|
|
5
|
+
* in `EFFECT_CURVES` are the ones signed off by eye in the effects-feel demo
|
|
6
|
+
* (`npm run effects-feel`), at 30 fps and at one frame a second.
|
|
7
|
+
*
|
|
8
|
+
* Each effect is something a first-time observer already knows from life —
|
|
9
|
+
* a slow glow, sunlight moving on water, the colours of daylight, fireflies, ink
|
|
10
|
+
* blooming in water, mist lifting — because a motion the eye recognises
|
|
11
|
+
* reads as calm and alive, where a motion it has to learn reads as a widget.
|
|
12
|
+
* What makes those motions familiar is what every curve here keeps:
|
|
13
|
+
*
|
|
14
|
+
* - Nothing jumps. Every curve is continuous in time, built from smooth
|
|
15
|
+
* noise (`core/noise.ts`) and smooth waveforms, so at one frame a second a cell
|
|
16
|
+
* moves a little and at thirty it glides.
|
|
17
|
+
* - Nothing repeats exactly. Rhythms are perturbed by slow noise, and each
|
|
18
|
+
* breath, firefly's flash and pass of light is an event of its own, with
|
|
19
|
+
* its own strength and moment, the way no breath or gust is the last one
|
|
20
|
+
* again. A weaker event is a shorter one, so its strength costs nothing
|
|
21
|
+
* in smoothness.
|
|
22
|
+
* - Neighbours move together. Variation is spatially coherent — patches,
|
|
23
|
+
* filaments, fronts — never per-cell static.
|
|
24
|
+
* - Light only ever lights, and never takes the words away. Ink and the
|
|
25
|
+
* fill under it both move toward the light and never darken, each by its
|
|
26
|
+
* own share: the share a colour can take while every cell it is drawn in
|
|
27
|
+
* still reads at its resting contrast or WCAG AA, whichever is lower. A
|
|
28
|
+
* cell with contrast to spare glows brightly; one with none barely moves.
|
|
29
|
+
* - Elements are apart. Each sits at its own `z` in the noise, so two
|
|
30
|
+
* elements under one effect do not move in lockstep.
|
|
31
|
+
*
|
|
32
|
+
* Colour moves are in OKLCH, where equal steps look equal. A curve's `ease`
|
|
33
|
+
* maps its intensity in [0, 1] — how strongly the effect acts on a cell at a
|
|
34
|
+
* moment — onto its swing, for the loops and the transitions alike.
|
|
35
|
+
*
|
|
36
|
+
* [LAW:no-ambient-temporal-coupling] No curve reads a clock; `t` is the
|
|
37
|
+
* effect's argument, so the frame owner decides when to sample.
|
|
38
|
+
*
|
|
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
|
+
* cards runs this file's own declarations as the program a visitor edits
|
|
42
|
+
* (`examples/effects-playground/programs.ts`), on the published library.
|
|
43
|
+
*/
|
|
44
|
+
import { ColorRgba } from "../core/color.js";
|
|
45
|
+
import { type Ease } from "../core/easing.js";
|
|
46
|
+
import type { Drawn, Effect, EffectCell, Pair } from "./effect.js";
|
|
47
|
+
/** What a loop does to one colour at strength `w` in [0, 1]: at 0, nothing. */
|
|
48
|
+
export type Touch = (color: ColorRgba, w: number) => ColorRgba;
|
|
49
|
+
/** How strongly a loop acts on a cell at a moment, in [0, 1]; `onColors` holds a field outside it to it. */
|
|
50
|
+
export type Field = (cell: EffectCell, t: number) => number;
|
|
51
|
+
/** A looping effect: what it does to a colour, and where and when, how strongly. */
|
|
52
|
+
export interface Loop {
|
|
53
|
+
readonly touch: Touch;
|
|
54
|
+
readonly field: Field;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* How much of `touch` each of `colors` (hex) can take: the largest share in
|
|
58
|
+
* [0, 1] at which every pair in `pairs` that shows it, touched at the same
|
|
59
|
+
* strength, still reads at its resting contrast or `LEGIBLE`, whichever is
|
|
60
|
+
* lower. A colour in a pair but not in `colors` takes none, and so does one
|
|
61
|
+
* the terminal supplies, whatever its hex, as in `onColors`. One of `colors`
|
|
62
|
+
* no pair writes has nothing to be measured against, and takes it all.
|
|
63
|
+
*
|
|
64
|
+
* Colours are settled lightest first, each against every partner: one
|
|
65
|
+
* already settled at its share, one not yet settled as it is. So the lighter
|
|
66
|
+
* colour of a cell takes the light first and its darker partner spends
|
|
67
|
+
* whatever contrast is left — pale lettering lifting off a dark fill gives
|
|
68
|
+
* the fill room to glow — and a darker colour left as it is always holds,
|
|
69
|
+
* because its lighter partner was settled against exactly that.
|
|
70
|
+
*/
|
|
71
|
+
export declare function shares(pairs: readonly Pair[], colors: ReadonlySet<string>, touch: Touch): Map<string, number>;
|
|
72
|
+
/**
|
|
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.
|
|
93
|
+
*/
|
|
94
|
+
export declare function atShares(loop: Loop, share: (drawn: Drawn) => ReadonlyMap<string, number>): Effect;
|
|
95
|
+
/** How the curves below are tuned: one period (or duration), ease and swing. */
|
|
96
|
+
export interface Curve {
|
|
97
|
+
/** Seconds: a looping effect's period, a transition's duration. */
|
|
98
|
+
readonly seconds: number;
|
|
99
|
+
/** Intensity in [0, 1] to the fraction of `swing` applied. One that overshoots, as a CSS `cubic-bezier` may, saturates: an effect never acts past its swing. */
|
|
100
|
+
readonly ease: Ease;
|
|
101
|
+
/** How far the move goes at full intensity, in [0, 1]; what 1 means is the effect's. */
|
|
102
|
+
readonly swing: number;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Each effect's curve as it was signed off. Every loop's period is a prime
|
|
106
|
+
* number of seconds, so no two loops come back into step within a sitting.
|
|
107
|
+
* The periods are long on purpose: at one frame a second a loop's colour
|
|
108
|
+
* moves by no more than a just-noticeable step from one frame to the next.
|
|
109
|
+
*/
|
|
110
|
+
export declare const EFFECT_CURVES: {
|
|
111
|
+
readonly shimmer: {
|
|
112
|
+
readonly seconds: 109;
|
|
113
|
+
readonly ease: Ease;
|
|
114
|
+
readonly swing: 0.9;
|
|
115
|
+
};
|
|
116
|
+
readonly pulse: {
|
|
117
|
+
readonly seconds: 43;
|
|
118
|
+
readonly ease: Ease;
|
|
119
|
+
readonly swing: 0.85;
|
|
120
|
+
};
|
|
121
|
+
readonly sparkle: {
|
|
122
|
+
readonly seconds: 139;
|
|
123
|
+
readonly ease: Ease;
|
|
124
|
+
readonly swing: 0.95;
|
|
125
|
+
};
|
|
126
|
+
readonly wheel: {
|
|
127
|
+
readonly seconds: 907;
|
|
128
|
+
readonly ease: Ease;
|
|
129
|
+
readonly swing: 1;
|
|
130
|
+
};
|
|
131
|
+
readonly fade: {
|
|
132
|
+
readonly seconds: 20;
|
|
133
|
+
readonly ease: Ease;
|
|
134
|
+
readonly swing: 1;
|
|
135
|
+
};
|
|
136
|
+
readonly dissolve: {
|
|
137
|
+
readonly seconds: 30;
|
|
138
|
+
readonly ease: Ease;
|
|
139
|
+
readonly swing: 1;
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
/** The lights the loops were signed off casting, warm as sunlight and fireflies are. */
|
|
143
|
+
export declare const EFFECT_LIGHTS: {
|
|
144
|
+
readonly sun: ColorRgba;
|
|
145
|
+
readonly firefly: ColorRgba;
|
|
146
|
+
};
|
|
147
|
+
/** How far a shimmer's light reaches either side of its centre, in columns, as signed off. */
|
|
148
|
+
export declare const SHIMMER_WIDTH = 24;
|
|
149
|
+
/**
|
|
150
|
+
* A colour under `glow`: the one way every light here falls. It moves `w` of
|
|
151
|
+
* the way toward the light's colour, and never below its own lightness —
|
|
152
|
+
* light can warm a colour and lift it, never darken it. A colour already
|
|
153
|
+
* lighter than the light only warms, and white, which has no warmer shade at
|
|
154
|
+
* its lightness, stays white.
|
|
155
|
+
*/
|
|
156
|
+
export declare function light(glow: ColorRgba): Touch;
|
|
157
|
+
/**
|
|
158
|
+
* Pulse, as a gentle glow on one element of a screen: the colours handed to
|
|
159
|
+
* it, an element's own, warm into `light` and settle back, over and over, the
|
|
160
|
+
* way an indicator breathes while something is waiting. Swing: how far into
|
|
161
|
+
* the light it goes at the top of a beat, 0–1. The element glows as one —
|
|
162
|
+
* every cell it is drawn in at once — and the colours not handed to it, the
|
|
163
|
+
* rest of the screen, stay as they are, so what pulses is what was chosen.
|
|
164
|
+
* Each element keeps a time of its own, a little quicker or slower than
|
|
165
|
+
* another's by `z`, and no beat is quite the last: the rhythm wanders ahead
|
|
166
|
+
* and behind its metronome and each beat goes a little deeper or shallower
|
|
167
|
+
* than the one before. A beat starts and ends in rest, so `t = 0` draws the
|
|
168
|
+
* element untouched.
|
|
169
|
+
*/
|
|
170
|
+
export declare function pulse(curve: Curve, glow: ColorRgba, z: number): Loop;
|
|
171
|
+
/**
|
|
172
|
+
* Shimmer, as sunlight moving across water. Swing: how far into `light` the
|
|
173
|
+
* brightest glint goes, 0–1. A soft band of light, `width` columns either
|
|
174
|
+
* side of its centre, crosses columns 0 to `span`, entering and leaving
|
|
175
|
+
* fully off the row, `STRIDE` columns in about three quarters of a period. Its passes
|
|
176
|
+
* come the way light comes and goes on water as clouds move: while the sun
|
|
177
|
+
* is out they are bright and follow close on one another, one sometimes
|
|
178
|
+
* catching the last; under cloud they thin to faint ones with the row still
|
|
179
|
+
* for a while between, and the light is never gone for long. Each crosses at its own unhurried pace, drifting a little ahead
|
|
180
|
+
* and behind as it goes. Inside a band the light is a faint glow broken
|
|
181
|
+
* into a glitter path — bright filaments where two ripples cross — carried
|
|
182
|
+
* on a slow current and re-forming as they go, so the glints dance while
|
|
183
|
+
* the band glides. Before the first pass enters, the row is untouched.
|
|
184
|
+
*/
|
|
185
|
+
export declare function shimmer(curve: Curve, span: number, width: number, glow: ColorRgba, z: number): Loop;
|
|
186
|
+
/**
|
|
187
|
+
* Sparkle, as fireflies. Swing: how far into the firefly's `light` a glow
|
|
188
|
+
* goes at its brightest, 0–1. About one firefly to every eight columns of
|
|
189
|
+
* `span`, each keeping near a home of its own. A firefly flashes — kindles,
|
|
190
|
+
* drifts a few cells glowing, slowing as it fades — then flies on in the
|
|
191
|
+
* dark and flashes again somewhere a few cells off, the way fireflies at
|
|
192
|
+
* dusk are seen: a short lit stroke, then gone, each time a little
|
|
193
|
+
* elsewhere. Each keeps its own irregular time, now and then letting a turn
|
|
194
|
+
* pass dark, so most moments a few are lit, at different brightnesses. Its
|
|
195
|
+
* light is a soft halo several cells wide, brightest where it is, so as it
|
|
196
|
+
* drifts the glow slides between cells rather than hopping. `seconds` is a firefly's
|
|
197
|
+
* turn: its brightest flash, rise and fall, takes `FLASH` of it, and a
|
|
198
|
+
* dimmer one less.
|
|
199
|
+
*/
|
|
200
|
+
export declare function sparkle(curve: Curve, span: number, glow: ColorRgba, z: number): Loop;
|
|
201
|
+
/**
|
|
202
|
+
* How much of a cell shows at `t`: 1 is the cell as drawn, 0 is the cell gone
|
|
203
|
+
* to the terminal's ground. Read per cell, so cells can arrive or leave apart. One
|
|
204
|
+
* outside [0, 1] is held to it.
|
|
205
|
+
*/
|
|
206
|
+
export type Visibility = (cell: EffectCell, t: number) => number;
|
|
207
|
+
/**
|
|
208
|
+
* A cell mixed toward the terminal's `ground` by how hidden it is: its fill
|
|
209
|
+
* toward the ground, and its ink toward that fill, so an element with fills
|
|
210
|
+
* of its own — a powerline strip — arrives and leaves whole, fill and words
|
|
211
|
+
* together, never as bare coloured blocks. Invisible is ink and fill equal
|
|
212
|
+
* to the ground. `swing` scales the hiding — 1 reaches fully invisible. A
|
|
213
|
+
* cell wholly shown is returned as it was.
|
|
214
|
+
*/
|
|
215
|
+
export declare function veiled(visibility: Visibility, swing: number, ground: ColorRgba): Effect;
|
|
216
|
+
/**
|
|
217
|
+
* An effect that runs once: a fade-in or a dissolve-out from its start time
|
|
218
|
+
* over its curve's `seconds`. `done(t)` is true from the moment every cell
|
|
219
|
+
* has arrived or gone, so the caller knows when to stop drawing an element
|
|
220
|
+
* that is leaving, or to drop the effect from one that has arrived.
|
|
221
|
+
*/
|
|
222
|
+
export interface Transition {
|
|
223
|
+
readonly effect: Effect;
|
|
224
|
+
done(t: number): boolean;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Fade-in starting at `start`, as ink blooming in water: patches of the
|
|
228
|
+
* element surface first and the rest follows outward from them, each cell
|
|
229
|
+
* rising smoothly over its own part of the duration. Whole at
|
|
230
|
+
* `start + seconds`.
|
|
231
|
+
*/
|
|
232
|
+
export declare function fadeIn(curve: Curve, start: number, z: number, ground: ColorRgba): Transition;
|
|
233
|
+
/**
|
|
234
|
+
* Dissolve-out starting at `start`, as mist lifting: the element thins in
|
|
235
|
+
* drifting patches, each cell fading smoothly over its own part of the
|
|
236
|
+
* duration, until nothing is left at `start + seconds`. Mist does not lift
|
|
237
|
+
* in one breath: towards the end, patches of what is left — the later a cell
|
|
238
|
+
* goes, the likelier — rise back part way, as the last of it eddies, before
|
|
239
|
+
* they thin away for good.
|
|
240
|
+
*/
|
|
241
|
+
export declare function dissolveOut(curve: Curve, start: number, z: number, ground: ColorRgba): Transition;
|
|
242
|
+
/**
|
|
243
|
+
* Wheel, as the colour of daylight going round: every hue turning the whole
|
|
244
|
+
* way round once a period — far too slowly to be seen moving, so a glance
|
|
245
|
+
* sees a steady palette and an hour's absence a different one — each
|
|
246
|
+
* segment of the element at its own pace. Swing: how far the segments stray
|
|
247
|
+
* from one another, 0–1, 1 being half a turn apart. A segment is a fill of
|
|
248
|
+
* the element's own — one of `fills`, the colours it draws as a ground — and
|
|
249
|
+
* it turns at its own pace wherever it is drawn: as the ground of its cells,
|
|
250
|
+
* and as the ink of the seam glyph pointing out of it into the next, so a
|
|
251
|
+
* powerline cap keeps the colour of the cell it caps. An ink that is no fill
|
|
252
|
+
* turns with the ground it sits on, so a cell's words and fill turn
|
|
253
|
+
* together; on the terminal's ground, a run of text's ink is a segment of
|
|
254
|
+
* its own. A segment's stray is slow noise of its own, at `STRAY_PERIODS`,
|
|
255
|
+
* so no two keep step, and the row never shows one spread of hues twice.
|
|
256
|
+
* Lightness and chroma are kept, so the words read as they did; the turn
|
|
257
|
+
* only happens to the element's own colours (`Drawn.colors`), never the
|
|
258
|
+
* terminal's.
|
|
259
|
+
*/
|
|
260
|
+
export declare function wheel(curve: Curve, z: number): Effect;
|