@promptctl/rich-js 0.23.0 → 0.24.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/clock.d.ts +9 -1
- package/dist/core/clock.js +22 -2
- package/dist/core/color.d.ts +10 -0
- package/dist/core/color.js +36 -1
- package/dist/core/easing.d.ts +31 -0
- package/dist/core/easing.js +106 -1
- package/dist/core/segment.d.ts +10 -0
- package/dist/core/segment.js +15 -0
- package/dist/core/strip.d.ts +104 -27
- package/dist/core/strip.js +231 -124
- package/dist/core/style.d.ts +18 -1
- package/dist/core/style.js +32 -3
- package/dist/core/text.js +4 -1
- package/dist/host/app.js +2 -4
- package/dist/host/index.d.ts +1 -1
- package/dist/host/input.d.ts +37 -13
- package/dist/host/input.js +146 -56
- package/dist/host/terminal-host.d.ts +16 -0
- package/dist/host/terminal-host.js +1 -0
- package/dist/index.d.ts +11 -9
- package/dist/index.js +5 -4
- package/dist/node/terminal-host.d.ts +8 -1
- package/dist/node/terminal-host.js +2 -0
- package/dist/renderables/effect.d.ts +78 -10
- package/dist/renderables/effect.js +143 -39
- package/dist/renderables/effects.d.ts +73 -26
- package/dist/renderables/effects.js +412 -92
- package/dist/renderables/embed.d.ts +10 -0
- package/dist/renderables/embed.js +15 -7
- package/dist/renderables/frames.d.ts +95 -0
- package/dist/renderables/frames.js +106 -0
- package/dist/renderables/live.d.ts +23 -7
- package/dist/renderables/live.js +68 -34
- package/dist/renderables/markdown.d.ts +5 -2
- package/dist/renderables/markdown.js +210 -19
- package/dist/renderables/progress.d.ts +1 -1
- package/dist/renderables/progress.js +3 -1
- package/dist/renderables/progressBar.js +3 -3
- package/dist/renderables/spinner.d.ts +62 -28
- package/dist/renderables/spinner.js +99 -52
- package/dist/renderables/status.d.ts +8 -3
- package/dist/renderables/status.js +8 -5
- package/dist/template-bindings/animation-funcs.d.ts +37 -0
- package/dist/template-bindings/animation-funcs.js +83 -0
- package/dist/template-bindings/color-funcs.d.ts +13 -0
- package/dist/template-bindings/color-funcs.js +1 -1
- package/dist/template-bindings/helpers.d.ts +6 -0
- package/dist/template-bindings/helpers.js +12 -3
- package/dist/template-bindings/index.d.ts +6 -2
- package/dist/template-bindings/index.js +6 -2
- package/package.json +1 -1
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* wrapper in another, which is the same as calling one after the other but
|
|
15
15
|
* for what the outer one measures: the inner one's output.
|
|
16
16
|
*
|
|
17
|
-
* What the element draws (`Drawn`)
|
|
18
|
-
*
|
|
17
|
+
* What the element draws (`Drawn`) — the cells the second stage will be
|
|
18
|
+
* handed, and the colours of its words — is measured here, in the render, off
|
|
19
|
+
* the same reading of each segment its cells are drawn from, and handed to the
|
|
19
20
|
* effect's first stage, once a frame. It is what an effect keeping words legible is
|
|
20
21
|
* settled against (`onColors`, `./effects.ts`), so no caller derives it a
|
|
21
22
|
* second time from a render of its own.
|
|
@@ -33,8 +34,10 @@
|
|
|
33
34
|
* segment's link and meta and carries its anchor shifted to the run's first
|
|
34
35
|
* cell (`Style.shiftedBy`), so hit testing still finds the owner.
|
|
35
36
|
* - A cell whose colours come back unchanged keeps its original `ColorSpec`,
|
|
36
|
-
* and a segment whose every cell is unchanged is yielded
|
|
37
|
-
* identity effect is byte-identical
|
|
37
|
+
* and a segment whose every cell is unchanged is yielded with its look as
|
|
38
|
+
* it was, so the identity effect is byte-identical; what it gains is when
|
|
39
|
+
* the frame next changes (`Style.nextChange`), which writes no byte.
|
|
40
|
+
* Neighbouring cells the output depth
|
|
38
41
|
* writes alike stay one segment: a segment is cut only where the written
|
|
39
42
|
* colours change, so two colours that land on one index are one run.
|
|
40
43
|
* - New colours are built with `ColorSpec.fromRgba`, never parsed from a
|
|
@@ -47,6 +50,7 @@
|
|
|
47
50
|
*/
|
|
48
51
|
import { type Anchor } from "../core/anchor.js";
|
|
49
52
|
import { ColorDepth, type ColorRgba, type TerminalTheme } from "../core/color.js";
|
|
53
|
+
import type { Beat } from "../core/easing.js";
|
|
50
54
|
import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
|
|
51
55
|
import { Segment } from "../core/segment.js";
|
|
52
56
|
/**
|
|
@@ -78,6 +82,8 @@ export interface EffectCell {
|
|
|
78
82
|
readonly fg: boolean;
|
|
79
83
|
readonly bg: boolean;
|
|
80
84
|
};
|
|
85
|
+
/** Whether the cell's style swaps its two slots, as `Pair.reverse` says: with `colors` and `terminal`, the cell's `Pair`. */
|
|
86
|
+
readonly reverse: boolean;
|
|
81
87
|
/**
|
|
82
88
|
* Who drew the cell, and where it sits in their output, as
|
|
83
89
|
* `Segment.anchorAt` reads it off a frame: what tells one owner's cells
|
|
@@ -85,6 +91,8 @@ export interface EffectCell {
|
|
|
85
91
|
*/
|
|
86
92
|
readonly anchor: Anchor | undefined;
|
|
87
93
|
}
|
|
94
|
+
/** Where a cell sits and who drew it: an `EffectCell` before it has a seed or colours. */
|
|
95
|
+
export type Placed = Pick<EffectCell, "row" | "col" | "anchor">;
|
|
88
96
|
/**
|
|
89
97
|
* A cell's ink and ground as the screen shows them, which of the two are the
|
|
90
98
|
* terminal's, and whether its style swaps them (`reverse`): what an `Effect`
|
|
@@ -101,13 +109,24 @@ export interface Pair extends CellColors {
|
|
|
101
109
|
readonly reverse: boolean;
|
|
102
110
|
}
|
|
103
111
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* not words.
|
|
112
|
+
* What the element an effect wraps draws in one frame: the cells the effect
|
|
113
|
+
* is handed, and the colours the element draws its words in, as the screen
|
|
114
|
+
* shows them — what an effect that must keep the words legible is measured
|
|
115
|
+
* against. A word cell is one whose glyph is not blank and that a strip's
|
|
116
|
+
* joiner did not draw (`JOIN`): a seam's ink is the fill beside it, not words.
|
|
109
117
|
*/
|
|
110
118
|
export interface Drawn {
|
|
119
|
+
/**
|
|
120
|
+
* Every cell the element shows something in this frame — a glyph, a
|
|
121
|
+
* ground that is not the terminal's, or a line its style draws — where it
|
|
122
|
+
* sits and who drew it, listed before the second stage is handed any: what
|
|
123
|
+
* an effect spread over the element — a transition, which has its last cell
|
|
124
|
+
* settle as it ends — is spread over. A blank cell on the terminal's ground
|
|
125
|
+
* that draws no line is handed but not listed, since nothing in it shows.
|
|
126
|
+
* `confined` narrows it to the cells its owners drew. A call, so a frame
|
|
127
|
+
* whose effect does not ask lists none.
|
|
128
|
+
*/
|
|
129
|
+
cells(): readonly Placed[];
|
|
111
130
|
/** Every distinct ink and ground of a word cell. */
|
|
112
131
|
readonly pairs: readonly Pair[];
|
|
113
132
|
/** The colours the element sets itself in its word cells, by hex: never the terminal's, whatever its hex. */
|
|
@@ -125,8 +144,36 @@ export interface Drawn {
|
|
|
125
144
|
* [LAW:types-are-the-program] Per-frame work is the outer stage, so it runs
|
|
126
145
|
* once a frame by the shape of the call, never by a cache asking whether this
|
|
127
146
|
* cell's frame is the last one's.
|
|
147
|
+
*
|
|
148
|
+
* An effect that knows when its frame next changes says so in `nextChange`,
|
|
149
|
+
* asked with the same `t` and `drawn` its first stage is: a stepped one its
|
|
150
|
+
* next step's start, a transition `t` until it is done and `Infinity` after,
|
|
151
|
+
* a loop that holds every cell still at the frame's depth `Infinity` there.
|
|
152
|
+
* [LAW:types-are-the-program] The answer is a fact about the frame, so it is
|
|
153
|
+
* asked with everything the frame is drawn from; an answer asked with `t`
|
|
154
|
+
* alone could not say that a frame is still because of its depth. A plain
|
|
155
|
+
* function is an effect that moves every frame, and answers `t`
|
|
156
|
+
* (`nextChangeOf`): the one true answer for a frame nothing has promised to
|
|
157
|
+
* hold, so a loop needs no second shape.
|
|
128
158
|
*/
|
|
129
|
-
export
|
|
159
|
+
export interface Effect {
|
|
160
|
+
(t: number, drawn: Drawn): (colors: CellColors, cell: EffectCell) => CellColors;
|
|
161
|
+
/** When the frame drawn at `t` of `drawn` next changes, in seconds on `t`'s timeline, never before `t`; absent, every frame. */
|
|
162
|
+
readonly nextChange?: (t: number, drawn: Drawn) => number;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* When `effect`'s frame at `t` of `drawn` next changes: what it says, or `t`
|
|
166
|
+
* for an effect that says nothing. [LAW:single-enforcer] The one reading of
|
|
167
|
+
* an absent `nextChange`, so no caller holds its own default.
|
|
168
|
+
*/
|
|
169
|
+
export declare function nextChangeOf(effect: Effect, t: number, drawn: Drawn): number;
|
|
170
|
+
/**
|
|
171
|
+
* An effect drawing as `effect` does, with `nextChange` as its answer to when
|
|
172
|
+
* its frame next changes, in place of whatever `effect` answered; `effect`
|
|
173
|
+
* itself is left as it was. A composer whose answer depends on the inner
|
|
174
|
+
* effect's reads it with `nextChangeOf`, as `onBeat` and `confined` do.
|
|
175
|
+
*/
|
|
176
|
+
export declare function withNextChange(effect: Effect, nextChange: (t: number, drawn: Drawn) => number): Effect;
|
|
130
177
|
export interface EffectedOptions {
|
|
131
178
|
/** The moment to draw, in seconds. */
|
|
132
179
|
readonly t: number;
|
|
@@ -150,6 +197,8 @@ export declare class Effected implements Renderable, Measurable {
|
|
|
150
197
|
readonly theme: TerminalTheme;
|
|
151
198
|
constructor(renderable: Renderable, effect: Effect, options: EffectedOptions);
|
|
152
199
|
render(options: RenderOptions): Iterable<Segment>;
|
|
200
|
+
private _moved;
|
|
201
|
+
private _recolored;
|
|
153
202
|
measure(options: RenderOptions): {
|
|
154
203
|
minimum: number;
|
|
155
204
|
maximum: number;
|
|
@@ -171,6 +220,25 @@ export declare class Effected implements Renderable, Measurable {
|
|
|
171
220
|
/** A cell's seed: FNV-1a of the key and the position, scaled into [0, 1). */
|
|
172
221
|
private seed;
|
|
173
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* `effect` sampled and held on `beat`: drawn at the moment the hold in
|
|
225
|
+
* progress began (`Beat.start`), so within a hold every frame is the frame
|
|
226
|
+
* at its start, and at each hold's start it moves on. A continuous effect
|
|
227
|
+
* given a short curve becomes one that steps — a `wheel` turning a sixth of
|
|
228
|
+
* a turn once a second — and at one redraw a second a hold shorter than a
|
|
229
|
+
* second is skipped, as frames are. [LAW:no-ambient-temporal-coupling] The
|
|
230
|
+
* held time is a function of `t`, so the same `t` draws the same bytes at
|
|
231
|
+
* any rate. The frame next changes at a hold's start, and only one the
|
|
232
|
+
* sampled effect has moved by: the first hold start at or after what it
|
|
233
|
+
* answers for the frame held now (`Beat.end`). An effect that moves every
|
|
234
|
+
* frame makes the beat's own answer, the end of this hold; one that is done
|
|
235
|
+
* and says never leaves the beat saying never too, since the hold after this
|
|
236
|
+
* one would draw the same frame. The answer is read off what the effect
|
|
237
|
+
* says, never off a comparison of frames: an effect that steps and is back
|
|
238
|
+
* on the same colour by the next hold still turns the beat there, an early
|
|
239
|
+
* answer the contract allows.
|
|
240
|
+
*/
|
|
241
|
+
export declare function onBeat(effect: Effect, beat: Beat): Effect;
|
|
174
242
|
/**
|
|
175
243
|
* `effect` on the colours any of `owners` drew, itself or through an owner
|
|
176
244
|
* nested in its output, and every other colour left as it is: one item of a
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* wrapper in another, which is the same as calling one after the other but
|
|
15
15
|
* for what the outer one measures: the inner one's output.
|
|
16
16
|
*
|
|
17
|
-
* What the element draws (`Drawn`)
|
|
18
|
-
*
|
|
17
|
+
* What the element draws (`Drawn`) — the cells the second stage will be
|
|
18
|
+
* handed, and the colours of its words — is measured here, in the render, off
|
|
19
|
+
* the same reading of each segment its cells are drawn from, and handed to the
|
|
19
20
|
* effect's first stage, once a frame. It is what an effect keeping words legible is
|
|
20
21
|
* settled against (`onColors`, `./effects.ts`), so no caller derives it a
|
|
21
22
|
* second time from a render of its own.
|
|
@@ -33,8 +34,10 @@
|
|
|
33
34
|
* segment's link and meta and carries its anchor shifted to the run's first
|
|
34
35
|
* cell (`Style.shiftedBy`), so hit testing still finds the owner.
|
|
35
36
|
* - A cell whose colours come back unchanged keeps its original `ColorSpec`,
|
|
36
|
-
* and a segment whose every cell is unchanged is yielded
|
|
37
|
-
* identity effect is byte-identical
|
|
37
|
+
* and a segment whose every cell is unchanged is yielded with its look as
|
|
38
|
+
* it was, so the identity effect is byte-identical; what it gains is when
|
|
39
|
+
* the frame next changes (`Style.nextChange`), which writes no byte.
|
|
40
|
+
* Neighbouring cells the output depth
|
|
38
41
|
* writes alike stay one segment: a segment is cut only where the written
|
|
39
42
|
* colours change, so two colours that land on one index are one run.
|
|
40
43
|
* - New colours are built with `ColorSpec.fromRgba`, never parsed from a
|
|
@@ -53,6 +56,23 @@ import { Measurement } from "../core/measure.js";
|
|
|
53
56
|
import { Segment } from "../core/segment.js";
|
|
54
57
|
import { JOIN, colorsOwnedBy } from "../core/strip.js";
|
|
55
58
|
import { Style } from "../core/style.js";
|
|
59
|
+
/**
|
|
60
|
+
* When `effect`'s frame at `t` of `drawn` next changes: what it says, or `t`
|
|
61
|
+
* for an effect that says nothing. [LAW:single-enforcer] The one reading of
|
|
62
|
+
* an absent `nextChange`, so no caller holds its own default.
|
|
63
|
+
*/
|
|
64
|
+
export function nextChangeOf(effect, t, drawn) {
|
|
65
|
+
return effect.nextChange?.(t, drawn) ?? t;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* An effect drawing as `effect` does, with `nextChange` as its answer to when
|
|
69
|
+
* its frame next changes, in place of whatever `effect` answered; `effect`
|
|
70
|
+
* itself is left as it was. A composer whose answer depends on the inner
|
|
71
|
+
* effect's reads it with `nextChangeOf`, as `onBeat` and `confined` do.
|
|
72
|
+
*/
|
|
73
|
+
export function withNextChange(effect, nextChange) {
|
|
74
|
+
return Object.assign((t, drawn) => effect(t, drawn), { nextChange });
|
|
75
|
+
}
|
|
56
76
|
export class Effected {
|
|
57
77
|
renderable;
|
|
58
78
|
effect;
|
|
@@ -69,21 +89,31 @@ export class Effected {
|
|
|
69
89
|
this.key = options.key;
|
|
70
90
|
this.theme = options.theme;
|
|
71
91
|
}
|
|
72
|
-
|
|
92
|
+
render(options) {
|
|
73
93
|
const segments = this.renderable.render(options);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
94
|
+
// With no colour output nothing the effect does is written, so the child
|
|
95
|
+
// is what is shown, and it changes when the child says it does.
|
|
96
|
+
if (options.colorSystem === null)
|
|
97
|
+
return segments;
|
|
98
|
+
return this._moved(segments, options);
|
|
99
|
+
}
|
|
100
|
+
*_moved(segments, options) {
|
|
78
101
|
// No depth named is no downgrade, as `drawnColors` reads it.
|
|
79
102
|
const depth = options.colorSystem ?? ColorDepth.TRUECOLOR;
|
|
80
103
|
// [LAW:one-source-of-truth] The frame is measured before any cell is
|
|
81
|
-
// handed to the effect, off the same looks its cells are drawn from
|
|
104
|
+
// handed to the effect, off the same looks its cells are drawn from, and
|
|
105
|
+
// the one measure is what both stages and the effect's next change are
|
|
106
|
+
// asked with.
|
|
82
107
|
const looked = looks(segments, depth, this.theme);
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
108
|
+
const walked = walk(looked);
|
|
109
|
+
const drawn = measured(looked, () => walked, depth, this.theme);
|
|
110
|
+
const frame = this.effect(this.t, drawn);
|
|
111
|
+
// What the effect draws at `t` is drawn until its next change, and the
|
|
112
|
+
// cells say so, under whatever the child already said (the sooner wins).
|
|
113
|
+
yield* Segment.applyStyle(this._recolored(walked, frame, depth), new Style({ nextChange: nextChangeOf(this.effect, this.t, drawn) }));
|
|
114
|
+
}
|
|
115
|
+
*_recolored(walked, frame, depth) {
|
|
116
|
+
for (const { segment, look, glyphs } of walked) {
|
|
87
117
|
if (look === undefined) {
|
|
88
118
|
yield segment;
|
|
89
119
|
continue;
|
|
@@ -96,21 +126,9 @@ export class Effected {
|
|
|
96
126
|
// writes, so two truecolors landing on one index stay one run. A cell
|
|
97
127
|
// the effect gave its neighbour's colours is drawn as that neighbour was.
|
|
98
128
|
const runs = [];
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
let colors = slots;
|
|
103
|
-
if (glyph === "\n") {
|
|
104
|
-
row++;
|
|
105
|
-
col = 0;
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
const anchor = base.anchor && shiftAnchor(base.anchor, into);
|
|
109
|
-
// Back from the screen to the style's slots: the same swap undoes itself.
|
|
110
|
-
colors = onScreen(frame(from, { row, col, seed: this.seed(row, col), terminal, anchor }), pair.reverse);
|
|
111
|
-
col += cellLen(glyph);
|
|
112
|
-
}
|
|
113
|
-
into += cellLen(glyph);
|
|
129
|
+
for (const { glyph, cell } of glyphs) {
|
|
130
|
+
// Back from the screen to the style's slots: the same swap undoes itself.
|
|
131
|
+
const colors = cell === undefined ? slots : onScreen(frame(from, { ...cell, seed: this.seed(cell.row, cell.col), terminal, reverse: pair.reverse }), pair.reverse);
|
|
114
132
|
const last = runs[runs.length - 1];
|
|
115
133
|
const writes = last !== undefined && sameColors(last.colors, colors) ? last.drawn : this.respec(wire, slots, colors, depth);
|
|
116
134
|
if (last !== undefined && writesAlike(last.drawn, writes)) {
|
|
@@ -178,13 +196,92 @@ function looks(segments, depth, theme) {
|
|
|
178
196
|
return { segment, look: { style, wire, slots, pair: { ...onScreen(slots, reverse), terminal, reverse } } };
|
|
179
197
|
});
|
|
180
198
|
}
|
|
181
|
-
/**
|
|
182
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Each glyph of `looked`, where its cell sits: rows and columns from the
|
|
201
|
+
* top-left of the output, a wide glyph one cell at its first column, and its
|
|
202
|
+
* anchor shifted to that column as `Style.shiftedBy` shifts a run's.
|
|
203
|
+
* [LAW:one-source-of-truth] The one walk: what `Drawn.cells` lists and what
|
|
204
|
+
* the render hands the effect are read off it.
|
|
205
|
+
*/
|
|
206
|
+
function walk(looked) {
|
|
207
|
+
let row = 0;
|
|
208
|
+
let col = 0;
|
|
209
|
+
return looked.map(({ segment, look }) => {
|
|
210
|
+
if (look === undefined)
|
|
211
|
+
return { segment, look, glyphs: [] };
|
|
212
|
+
const anchor = look.style.anchor;
|
|
213
|
+
// Cells into the segment, as `base.shiftedBy` counts them in the render.
|
|
214
|
+
let into = 0;
|
|
215
|
+
const glyphs = [...graphemes(segment.text)].map((glyph) => {
|
|
216
|
+
const cell = glyph === "\n" ? undefined : { row, col, anchor: anchor && shiftAnchor(anchor, into) };
|
|
217
|
+
[row, col] = glyph === "\n" ? [row + 1, 0] : [row, col + cellLen(glyph)];
|
|
218
|
+
into += cellLen(glyph);
|
|
219
|
+
return { glyph, cell };
|
|
220
|
+
});
|
|
221
|
+
return { segment, look, glyphs };
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/** Whether `style` draws in its ink on a blank glyph: a line under, through or over it, or a frame round it. */
|
|
225
|
+
function drawsLines(style) {
|
|
226
|
+
return [style.underline, style.underline2, style.strike, style.overline, style.frame, style.encircle].some((on) => on === true);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* What `looked` draws: `Drawn`'s measure, taken once per frame. Its `cells`
|
|
230
|
+
* are listed off `walked` the first time they are asked for, since only an
|
|
231
|
+
* effect spread over the element asks, and a glyph walk is what measuring the
|
|
232
|
+
* words alone, a segment at a time, is spared (`drawnOf`).
|
|
233
|
+
*/
|
|
234
|
+
function measured(looked, walked, depth, theme) {
|
|
235
|
+
const ground = theme.backgroundColor.hex;
|
|
236
|
+
let listed;
|
|
237
|
+
const list = () => walked().flatMap(({ look, glyphs }) => {
|
|
238
|
+
if (look === undefined)
|
|
239
|
+
return [];
|
|
240
|
+
// A blank glyph shows its ground, and any line its style draws in its ink.
|
|
241
|
+
const blankShows = look.pair.bg.hex !== ground || drawsLines(look.style);
|
|
242
|
+
return glyphs.flatMap(({ glyph, cell }) => (cell !== undefined && (glyph.trim() !== "" || blankShows) ? [cell] : []));
|
|
243
|
+
});
|
|
183
244
|
const words = looked.flatMap(({ segment, look }) => (look !== undefined && segment.text.trim() !== "" && !drawnBy(look.style.anchor, JOIN) ? [look.pair] : []));
|
|
184
245
|
const pairs = [...new Map(words.map((pair) => [`${pair.fg.hex}/${pair.bg.hex}/${pair.terminal.fg}/${pair.terminal.bg}/${pair.reverse}`, pair])).values()];
|
|
185
246
|
// [LAW:one-source-of-truth] A slot the terminal fills is no colour of the element's, whatever its hex.
|
|
186
247
|
const own = (slot) => pairs.flatMap((pair) => (pair.terminal[slot] ? [] : [pair[slot].hex]));
|
|
187
|
-
return {
|
|
248
|
+
return {
|
|
249
|
+
cells: () => (listed ??= list()),
|
|
250
|
+
pairs,
|
|
251
|
+
colors: new Set([...own("fg"), ...own("bg")]),
|
|
252
|
+
fills: new Set(own("bg")),
|
|
253
|
+
depth,
|
|
254
|
+
theme,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* `effect` sampled and held on `beat`: drawn at the moment the hold in
|
|
259
|
+
* progress began (`Beat.start`), so within a hold every frame is the frame
|
|
260
|
+
* at its start, and at each hold's start it moves on. A continuous effect
|
|
261
|
+
* given a short curve becomes one that steps — a `wheel` turning a sixth of
|
|
262
|
+
* a turn once a second — and at one redraw a second a hold shorter than a
|
|
263
|
+
* second is skipped, as frames are. [LAW:no-ambient-temporal-coupling] The
|
|
264
|
+
* held time is a function of `t`, so the same `t` draws the same bytes at
|
|
265
|
+
* any rate. The frame next changes at a hold's start, and only one the
|
|
266
|
+
* sampled effect has moved by: the first hold start at or after what it
|
|
267
|
+
* answers for the frame held now (`Beat.end`). An effect that moves every
|
|
268
|
+
* frame makes the beat's own answer, the end of this hold; one that is done
|
|
269
|
+
* and says never leaves the beat saying never too, since the hold after this
|
|
270
|
+
* one would draw the same frame. The answer is read off what the effect
|
|
271
|
+
* says, never off a comparison of frames: an effect that steps and is back
|
|
272
|
+
* on the same colour by the next hold still turns the beat there, an early
|
|
273
|
+
* answer the contract allows.
|
|
274
|
+
*/
|
|
275
|
+
export function onBeat(effect, beat) {
|
|
276
|
+
return withNextChange((t, drawn) => effect(beat.start(t), drawn), (t, drawn) => {
|
|
277
|
+
const moved = nextChangeOf(effect, beat.start(t), drawn);
|
|
278
|
+
if (moved === Number.POSITIVE_INFINITY)
|
|
279
|
+
return moved;
|
|
280
|
+
const end = beat.end(t);
|
|
281
|
+
// The hold boundaries after `t` are `end`, then each hold's end in turn;
|
|
282
|
+
// the frame turns at the first one the effect has moved by.
|
|
283
|
+
return moved <= end ? end : beat.start(moved) === moved ? moved : beat.end(moved);
|
|
284
|
+
});
|
|
188
285
|
}
|
|
189
286
|
/**
|
|
190
287
|
* `effect` on the colours any of `owners` drew, itself or through an owner
|
|
@@ -199,16 +296,22 @@ function measured(looked, depth, theme) {
|
|
|
199
296
|
*/
|
|
200
297
|
export function confined(effect, ...owners) {
|
|
201
298
|
const confinedTo = new Set(owners);
|
|
202
|
-
|
|
203
|
-
|
|
299
|
+
// [LAW:single-enforcer] One reading of which slots are owned, for the cells listed and the cells handed alike.
|
|
300
|
+
const ownedOf = (cell) => colorsOwnedBy(cell.anchor, confinedTo);
|
|
301
|
+
const any = ({ fg, bg }) => fg || bg;
|
|
302
|
+
// The frame as the effect sees it: the owners' cells alone. [LAW:one-source-of-truth] One narrowing, handed to both stages.
|
|
303
|
+
const owned = (drawn) => ({ ...drawn, cells: () => drawn.cells().filter((cell) => any(ownedOf(cell))) });
|
|
304
|
+
// The owners' cells change when the effect's frame does.
|
|
305
|
+
return withNextChange((t, drawn) => {
|
|
306
|
+
const frame = effect(t, owned(drawn));
|
|
204
307
|
return (colors, cell) => {
|
|
205
|
-
const
|
|
206
|
-
if (!
|
|
308
|
+
const owns = ownedOf(cell);
|
|
309
|
+
if (!any(owns))
|
|
207
310
|
return colors;
|
|
208
311
|
const moved = frame(colors, cell);
|
|
209
|
-
return { fg:
|
|
312
|
+
return { fg: owns.fg ? moved.fg : colors.fg, bg: owns.bg ? moved.bg : colors.bg };
|
|
210
313
|
};
|
|
211
|
-
};
|
|
314
|
+
}, (t, drawn) => nextChangeOf(effect, t, owned(drawn)));
|
|
212
315
|
}
|
|
213
316
|
/**
|
|
214
317
|
* What `segments`, drawn at `depth` on `theme`, draw their words in, measured
|
|
@@ -216,7 +319,8 @@ export function confined(effect, ...owners) {
|
|
|
216
319
|
* output under an effect to read the contrast its words are left at.
|
|
217
320
|
*/
|
|
218
321
|
export function drawnOf(segments, depth, theme) {
|
|
219
|
-
|
|
322
|
+
const looked = looks(segments, depth, theme);
|
|
323
|
+
return measured(looked, () => walk(looked), depth, theme);
|
|
220
324
|
}
|
|
221
325
|
/**
|
|
222
326
|
* A style's colour slots as the screen shows them, or the reverse: `reverse`
|
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
* a slow glow, sunlight moving on water, the colours of daylight, fireflies, ink
|
|
11
11
|
* blooming in water, mist lifting — because a motion the eye recognises
|
|
12
12
|
* reads as calm and alive, where a motion it has to learn reads as a widget.
|
|
13
|
-
*
|
|
13
|
+
* One effect here is not ambience but a signal, and keeps none of this on
|
|
14
|
+
* purpose: `steppedInk` steps its ink through colours on a beat, the news
|
|
15
|
+
* that something is at its limit, and jumps from slot to slot where the
|
|
16
|
+
* loops hold still. What makes the loops' motions familiar is what every
|
|
17
|
+
* curve of theirs keeps:
|
|
14
18
|
*
|
|
15
19
|
* - Nothing jumps. Every curve is continuous in time, built from smooth
|
|
16
20
|
* noise (`core/noise.ts`) and smooth waveforms, so at one frame a second a cell
|
|
@@ -48,11 +52,12 @@ import { type Drawn, type Effect, type EffectCell } from "./effect.js";
|
|
|
48
52
|
/** What a loop does to one colour at strength `w` in [0, 1]: at 0, nothing. */
|
|
49
53
|
export type Touch = (color: ColorRgba, w: number) => ColorRgba;
|
|
50
54
|
/**
|
|
51
|
-
* How strongly a loop acts at a moment `t` on each cell
|
|
52
|
-
* holds a field outside it to it.
|
|
53
|
-
* whole moment is worked out once,
|
|
55
|
+
* How strongly a loop acts at a moment `t` on each cell of a frame drawing
|
|
56
|
+
* what `Drawn` says, in [0, 1]; `onColors` holds a field outside it to it.
|
|
57
|
+
* Staged as `Effect` is: what holds for the whole moment is worked out once,
|
|
58
|
+
* then every cell is asked.
|
|
54
59
|
*/
|
|
55
|
-
export type Field = (t: number) => (cell: EffectCell) => number;
|
|
60
|
+
export type Field = (t: number, drawn: Drawn) => (cell: EffectCell) => number;
|
|
56
61
|
/** A looping effect: what it does to a colour, and where and when, how strongly. */
|
|
57
62
|
export interface Loop {
|
|
58
63
|
readonly touch: Touch;
|
|
@@ -79,9 +84,10 @@ export declare function shares({ pairs, depth, theme }: Drawn, colors: ReadonlyS
|
|
|
79
84
|
* A loop as an `Effect` on the element's own colours, by hex, wherever a cell
|
|
80
85
|
* shows them, each touched at its share of the cell's strength: the share
|
|
81
86
|
* `shares` settles for it against the pairs the element draws its words in
|
|
82
|
-
* (`Drawn`), touched as `Effected` writes them at the frame's depth —
|
|
83
|
-
*
|
|
84
|
-
* contrast it spends is that colour's
|
|
87
|
+
* (`Drawn`), touched as `Effected` writes them at the frame's depth — at 256
|
|
88
|
+
* colours a touched colour lands on a colour the slot can write, and the
|
|
89
|
+
* contrast it spends is that colour's; on a sixteen-colour terminal nothing
|
|
90
|
+
* moves (`stillOnSlots`). `colors` are the ones lit, by default every colour the element
|
|
85
91
|
* sets itself. A powerline strip's fill is the ground of its cells and the
|
|
86
92
|
* ink of the seam glyphs between them, so the arrow moves with the cell it
|
|
87
93
|
* points out of. A colour the terminal supplies (`EffectCell.terminal`) is
|
|
@@ -177,8 +183,9 @@ export declare function pulse(curve: Curve, glow: ColorRgba, z: number): Loop;
|
|
|
177
183
|
/**
|
|
178
184
|
* Shimmer, as sunlight moving across water. Swing: how far into `light` the
|
|
179
185
|
* brightest glint goes, 0–1. A soft band of light, `width` columns either
|
|
180
|
-
* side of its centre, crosses
|
|
181
|
-
*
|
|
186
|
+
* side of its centre, crosses the element from the first column a frame
|
|
187
|
+
* lists (`Drawn.cells`) to the last, entering and leaving fully off it,
|
|
188
|
+
* `STRIDE` columns in about three quarters of a period. Its passes
|
|
182
189
|
* come the way light comes and goes on water as clouds move: while the sun
|
|
183
190
|
* is out they are bright and follow close on one another, one sometimes
|
|
184
191
|
* catching the last; under cloud they thin to faint ones with the row still
|
|
@@ -188,11 +195,17 @@ export declare function pulse(curve: Curve, glow: ColorRgba, z: number): Loop;
|
|
|
188
195
|
* on a slow current and re-forming as they go, so the glints dance while
|
|
189
196
|
* the band glides. Before the first pass enters, the row is untouched.
|
|
190
197
|
*/
|
|
191
|
-
export declare function shimmer(curve: Curve,
|
|
198
|
+
export declare function shimmer(curve: Curve, width: number, glow: ColorRgba, z: number): Loop;
|
|
192
199
|
/**
|
|
193
200
|
* Sparkle, as fireflies. Swing: how far into the firefly's `light` a glow
|
|
194
|
-
* goes at its brightest, 0–1.
|
|
195
|
-
*
|
|
201
|
+
* goes at its brightest, 0–1. Fireflies keep to a lattice laid from the
|
|
202
|
+
* element's first column and top row as a frame lists its cells
|
|
203
|
+
* (`Drawn.cells`): one every `PER` columns along a row of the lattice, a row
|
|
204
|
+
* of the lattice every `ROWS` rows, each firefly near a home of its own.
|
|
205
|
+
* The lattice does not hang on how wide or deep the element is, so one that
|
|
206
|
+
* grows or shrinks at its last column or bottom row moves no firefly; one
|
|
207
|
+
* whose first column moves, as centred words that change length do, carries
|
|
208
|
+
* its fireflies with it. A firefly flashes — kindles,
|
|
196
209
|
* drifts a few cells glowing, slowing as it fades — then flies on in the
|
|
197
210
|
* dark and flashes again somewhere a few cells off, the way fireflies at
|
|
198
211
|
* dusk are seen: a short lit stroke, then gone, each time a little
|
|
@@ -203,13 +216,14 @@ export declare function shimmer(curve: Curve, span: number, width: number, glow:
|
|
|
203
216
|
* turn: its brightest flash, rise and fall, takes `FLASH` of it, and a
|
|
204
217
|
* dimmer one less.
|
|
205
218
|
*/
|
|
206
|
-
export declare function sparkle(curve: Curve,
|
|
219
|
+
export declare function sparkle(curve: Curve, glow: ColorRgba, z: number): Loop;
|
|
207
220
|
/**
|
|
208
|
-
* How much of each cell shows at `t
|
|
209
|
-
* gone to the terminal's ground. Read per
|
|
210
|
-
* apart, and staged as `
|
|
221
|
+
* How much of each cell shows at `t` in a frame drawing what `Drawn` says: 1
|
|
222
|
+
* is the cell as drawn, 0 is the cell gone to the terminal's ground. Read per
|
|
223
|
+
* cell, so cells can arrive or leave apart, and staged as `Effect` is. One
|
|
224
|
+
* outside [0, 1] is held to it.
|
|
211
225
|
*/
|
|
212
|
-
export type Visibility = (t: number) => (cell: EffectCell) => number;
|
|
226
|
+
export type Visibility = (t: number, drawn: Drawn) => (cell: EffectCell) => number;
|
|
213
227
|
/**
|
|
214
228
|
* A cell mixed toward the ground of the terminal the frame is drawn on
|
|
215
229
|
* (`Drawn.theme`) by how hidden it is: its fill toward the ground, and its ink
|
|
@@ -225,6 +239,8 @@ export declare function veiled(visibility: Visibility, swing: number): Effect;
|
|
|
225
239
|
* draws it at the time since it began. `done(t)` is true from the moment
|
|
226
240
|
* every cell has arrived or gone, so the caller knows when to stop drawing an
|
|
227
241
|
* element that is leaving, or to drop the effect from one that has arrived.
|
|
242
|
+
* Until then the effect's frame changes every frame, and from then never:
|
|
243
|
+
* its `nextChange` is `done` read as a time. [LAW:one-source-of-truth]
|
|
228
244
|
*/
|
|
229
245
|
export interface Transition {
|
|
230
246
|
readonly effect: Effect;
|
|
@@ -259,19 +275,50 @@ export declare function dissolveOut(curve: Curve, z: number): Transition;
|
|
|
259
275
|
* together; on the terminal's ground, a run of text's ink is a segment of
|
|
260
276
|
* its own. A segment's stray is slow noise of its own, at `STRAY_PERIODS`,
|
|
261
277
|
* so no two keep step, and the row never shows one spread of hues twice.
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
278
|
+
* A colour keeps its luminance as it turns (`hueTurned`), so every pair of
|
|
279
|
+
* words reads at the contrast it rested at; at 256 colours a turned colour
|
|
280
|
+
* lands on a colour the depth writes, one that keeps every pair of words it
|
|
281
|
+
* is drawn in at its floor (`floorOf`) — its resting contrast or WCAG AA,
|
|
282
|
+
* whichever is lower — as `shares` keeps the light's (`turnedBounds`). The
|
|
283
|
+
* turn only happens to the element's own colours (`Drawn.colors`), never the
|
|
284
|
+
* terminal's. On a sixteen-colour terminal nothing turns (`stillOnSlots`).
|
|
265
285
|
*/
|
|
266
286
|
export declare function wheel(curve: Curve, z: number): Effect;
|
|
287
|
+
/** One step of a signal: the ink it draws, and how long it is held, in seconds. */
|
|
288
|
+
export interface ColorStep {
|
|
289
|
+
readonly color: ColorRgba;
|
|
290
|
+
readonly seconds: number;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* A signal: every cell's ink stepping through `steps`' colours on a beat,
|
|
294
|
+
* one colour a hold, looping, for the time it is handed — a gauge's
|
|
295
|
+
* chevrons in bright changing colours. The ground is left as the cell draws
|
|
296
|
+
* it, and the words stay legible on it: each step's ink is written as the
|
|
297
|
+
* colour nearest the step's (OKLab's ΔE) that the depth draws and that
|
|
298
|
+
* reads on the cell's ground, as `Effected` writes it, at the cell's floor
|
|
299
|
+
* (`floorOf`) — its resting contrast or `LEGIBLE`, whichever is lower, the
|
|
300
|
+
* floor every loop here keeps. In truecolor that is the step itself, or its
|
|
301
|
+
* hue and chroma at the lightness nearest it that reaches the floor
|
|
302
|
+
* (`ensureContrast`, the one slide a template's `readableOn` makes too);
|
|
303
|
+
* below truecolor, the nearest of the colours the depth has an index for, so on a
|
|
304
|
+
* sixteen-colour terminal it steps from slot to slot, the one effect here
|
|
305
|
+
* that does (`stillOnSlots`). A cell whose ground no offered colour reads
|
|
306
|
+
* on keeps the ink it rested in.
|
|
307
|
+
*
|
|
308
|
+
* The search is settled once for each ink, ground and step, so a frame
|
|
309
|
+
* inside a step redoes none, and nor does a later pass of the beat: at sixty
|
|
310
|
+
* frames a second a one-second step is sixty identical frames, and the
|
|
311
|
+
* frame says it changes next when the step ends (`Beat.end`).
|
|
312
|
+
* [LAW:no-ambient-temporal-coupling] The beat is `core/easing.ts`'s, and
|
|
313
|
+
* nothing here reads a clock.
|
|
314
|
+
*/
|
|
315
|
+
export declare function steppedInk(steps: readonly ColorStep[]): Effect;
|
|
267
316
|
/**
|
|
268
|
-
* What a catalogue effect is made for: the element it is drawn on,
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* lockstep.
|
|
317
|
+
* What a catalogue effect is made for: the element it is drawn on, at `z` in
|
|
318
|
+
* the noise, so two elements under one effect do not move in lockstep. How
|
|
319
|
+
* wide the element is, the effect reads off each frame (`Drawn.cells`).
|
|
272
320
|
*/
|
|
273
321
|
export interface EffectTarget {
|
|
274
|
-
readonly span: number;
|
|
275
322
|
readonly z: number;
|
|
276
323
|
}
|
|
277
324
|
/** The names of the effects that loop. */
|