@promptctl/rich-js 0.26.0 → 0.28.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 +1 -0
- package/THIRD-PARTY-NOTICES +42 -0
- package/dist/core/animation.d.ts +52 -15
- package/dist/core/animation.js +82 -37
- package/dist/core/ansi.d.ts +15 -8
- package/dist/core/ansi.js +106 -28
- package/dist/core/bidi-data.d.ts +14 -0
- package/dist/core/bidi-data.js +385 -0
- package/dist/core/bidi.d.ts +89 -0
- package/dist/core/bidi.js +615 -0
- package/dist/core/box.d.ts +9 -1
- package/dist/core/box.js +11 -0
- package/dist/core/cell-widths.d.ts +3 -2
- package/dist/core/cell-widths.js +93 -93
- package/dist/core/cells.d.ts +20 -0
- package/dist/core/cells.js +93 -5
- package/dist/core/color.d.ts +26 -18
- package/dist/core/color.js +37 -34
- package/dist/core/console.d.ts +26 -1
- package/dist/core/console.js +26 -6
- package/dist/core/control-codes.d.ts +61 -0
- package/dist/core/control-codes.js +157 -0
- package/dist/core/easing.js +6 -1
- package/dist/core/env.d.ts +6 -0
- package/dist/core/env.js +9 -1
- package/dist/core/export-html.js +4 -3
- package/dist/core/export-lines.d.ts +2 -0
- package/dist/core/export-lines.js +7 -3
- package/dist/core/export-svg.js +6 -5
- package/dist/core/json.js +9 -2
- package/dist/core/markup.js +4 -3
- package/dist/core/oklch.d.ts +2 -0
- package/dist/core/oklch.js +3 -3
- package/dist/core/orient.d.ts +68 -0
- package/dist/core/orient.js +224 -0
- package/dist/core/osc8.d.ts +12 -5
- package/dist/core/osc8.js +14 -7
- package/dist/core/pretty.js +22 -18
- package/dist/core/protocol.d.ts +53 -22
- package/dist/core/protocol.js +14 -6
- package/dist/core/render.d.ts +8 -3
- package/dist/core/render.js +19 -5
- package/dist/core/seal.d.ts +26 -0
- package/dist/core/seal.js +75 -0
- package/dist/core/segment.d.ts +9 -1
- package/dist/core/segment.js +11 -1
- package/dist/core/style.d.ts +29 -1
- package/dist/core/style.js +41 -5
- package/dist/core/text-order.d.ts +51 -0
- package/dist/core/text-order.js +50 -0
- package/dist/core/text.d.ts +10 -0
- package/dist/core/text.js +133 -87
- package/dist/core/timeline.js +6 -6
- package/dist/core/view.d.ts +23 -9
- package/dist/core/view.js +37 -12
- package/dist/core/wrap.d.ts +24 -6
- package/dist/core/wrap.js +82 -16
- package/dist/host/app.d.ts +48 -7
- package/dist/host/app.js +116 -11
- package/dist/host/index.d.ts +3 -2
- package/dist/host/index.js +1 -0
- package/dist/host/input.d.ts +35 -2
- package/dist/host/input.js +88 -4
- package/dist/host/terminal-colors.d.ts +18 -0
- package/dist/host/terminal-colors.js +46 -0
- package/dist/host/terminal-host.d.ts +2 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.js +2 -2
- package/dist/renderables/change.d.ts +1 -3
- package/dist/renderables/change.js +4 -6
- package/dist/renderables/effect.d.ts +61 -60
- package/dist/renderables/effect.js +91 -81
- package/dist/renderables/effects.d.ts +37 -8
- package/dist/renderables/effects.js +193 -55
- package/dist/renderables/frames.d.ts +1 -10
- package/dist/renderables/frames.js +1 -16
- package/dist/renderables/glyph-effect.d.ts +12 -13
- package/dist/renderables/glyph-effect.js +34 -32
- package/dist/renderables/glyph-effects.d.ts +27 -8
- package/dist/renderables/glyph-effects.js +75 -7
- package/dist/renderables/lifecycle.d.ts +40 -32
- package/dist/renderables/lifecycle.js +87 -48
- package/dist/renderables/live.d.ts +3 -3
- package/dist/renderables/live.js +4 -4
- package/dist/renderables/markdown.js +4 -2
- package/dist/renderables/panel.js +6 -4
- package/dist/renderables/progress.d.ts +11 -4
- package/dist/renderables/progress.js +29 -17
- package/dist/renderables/progressBar.d.ts +3 -3
- package/dist/renderables/progressBar.js +16 -13
- package/dist/renderables/syntax.js +7 -2
- package/dist/renderables/table.js +2 -1
- package/dist/template-bindings/animation-funcs.d.ts +49 -18
- package/dist/template-bindings/animation-funcs.js +115 -32
- package/dist/template-bindings/index.d.ts +7 -5
- package/dist/template-bindings/index.js +8 -5
- package/dist/template-bindings/palette-funcs.d.ts +1 -1
- package/dist/template-bindings/palette-funcs.js +1 -1
- package/dist/themes/buildPalette.d.ts +15 -39
- package/dist/themes/buildPalette.js +196 -67
- package/dist/themes/colorMath.d.ts +10 -4
- package/dist/themes/colorMath.js +33 -1
- package/dist/themes/data/index.js +1 -4
- package/dist/themes/data/textual-ansi.js +0 -143
- package/dist/themes/data/types.d.ts +3 -1
- package/dist/themes/palette.d.ts +22 -6
- package/dist/themes/palette.js +13 -21
- package/dist/themes/ramp.d.ts +1 -1
- package/dist/themes/ramp.js +1 -1
- package/dist/themes/registry.d.ts +7 -10
- package/dist/themes/registry.js +9 -30
- package/dist/themes/terminalThemes.d.ts +4 -10
- package/dist/themes/terminalThemes.js +31 -46
- package/dist/themes/transpose.d.ts +5 -10
- package/dist/themes/transpose.js +13 -24
- package/dist/widgets/button.d.ts +1 -1
- package/dist/widgets/button.js +6 -6
- package/dist/widgets/checkbox.js +4 -4
- package/dist/widgets/dropdown.d.ts +1 -1
- package/dist/widgets/dropdown.js +7 -5
- package/dist/widgets/event-router.js +2 -2
- package/dist/widgets/ink.d.ts +5 -4
- package/dist/widgets/ink.js +6 -9
- package/dist/widgets/text-input.d.ts +1 -1
- package/dist/widgets/text-input.js +41 -32
- package/dist/widgets/themed-widget.js +3 -3
- package/dist/widgets/toggle.d.ts +1 -1
- package/dist/widgets/toggle.js +5 -5
- package/dist/widgets/types.d.ts +2 -2
- package/dist/widgets/widget-app.d.ts +3 -3
- package/dist/widgets/widget-app.js +5 -5
- package/package.json +5 -4
|
@@ -51,44 +51,40 @@
|
|
|
51
51
|
* Positions are the child's own: row and column count from the top-left of
|
|
52
52
|
* what the child drew.
|
|
53
53
|
*/
|
|
54
|
-
import { Animation,
|
|
54
|
+
import { Animation, placedRender, shownAt } from "../core/animation.js";
|
|
55
55
|
import { cellLen, graphemes } from "../core/cells.js";
|
|
56
56
|
import { Measurement } from "../core/measure.js";
|
|
57
57
|
import { render } from "../core/protocol.js";
|
|
58
58
|
import { Segment } from "../core/segment.js";
|
|
59
59
|
import { finiteTime } from "../core/easing.js";
|
|
60
|
-
import {
|
|
61
|
-
import { cellSeed, walkGlyphs } from "./effect.js";
|
|
60
|
+
import { sanitized } from "../core/text.js";
|
|
61
|
+
import { cellSeed, holdOver, walkGlyphs } from "./effect.js";
|
|
62
62
|
import { pattern } from "../core/pattern.js";
|
|
63
63
|
/**
|
|
64
64
|
* A `GlyphEffected` at `t`, or an element through a glyph effect at a time an
|
|
65
65
|
* animation's `at` was handed: `renderable` with its cells' glyphs passed
|
|
66
|
-
* through `effect` at `t`,
|
|
67
|
-
* `
|
|
66
|
+
* through `effect` at `t`, telling the hold it drew in as a `Recolored` does
|
|
67
|
+
* (`RenderOptions.onMoved`, `./effect.ts`). [LAW:one-type-per-behavior]
|
|
68
68
|
*/
|
|
69
69
|
export class Reglyphed {
|
|
70
70
|
renderable;
|
|
71
71
|
effect;
|
|
72
|
-
|
|
72
|
+
_t;
|
|
73
73
|
key;
|
|
74
74
|
constructor(renderable, effect, options) {
|
|
75
75
|
this.renderable = renderable;
|
|
76
76
|
this.effect = effect;
|
|
77
|
-
this.
|
|
77
|
+
this._t = finiteTime("GlyphEffected", options.t);
|
|
78
78
|
this.key = options.key;
|
|
79
79
|
}
|
|
80
80
|
render(options) {
|
|
81
|
-
return this.moved(options).segments;
|
|
82
|
-
}
|
|
83
|
-
/** The frame the effect draws at `t`, and the hold it says that is in. */
|
|
84
|
-
moved(options) {
|
|
85
81
|
const segments = [...render(this.renderable, options)];
|
|
86
82
|
const { placed, cells } = placeGlyphs(segments, this.key);
|
|
87
83
|
const animation = this.effect({ cells, asciiOnly: options.asciiOnly === true });
|
|
88
|
-
const frame = shownAt(animation, this.
|
|
84
|
+
const frame = shownAt(animation, this._t, options);
|
|
89
85
|
const shown = showGlyphs(placed, frame.value);
|
|
90
|
-
|
|
91
|
-
return
|
|
86
|
+
options.onMoved?.(this._t, this.key, holdOver(cells.length > 0, frame, this._t));
|
|
87
|
+
return segments.map((segment, i) => (shown[i].every((text, j) => text === placed[i][j].glyph) ? segment : new Segment(shown[i].join(""), segment.style)));
|
|
92
88
|
}
|
|
93
89
|
measure(options) {
|
|
94
90
|
return Measurement.get(options, this.renderable);
|
|
@@ -96,9 +92,10 @@ export class Reglyphed {
|
|
|
96
92
|
}
|
|
97
93
|
/**
|
|
98
94
|
* `renderable` under a glyph effect over time, an `Animation<Renderable>`
|
|
99
|
-
* as an `Effected` is (`./effect.ts`): its
|
|
100
|
-
*
|
|
101
|
-
* output's `asciiOnly`,
|
|
95
|
+
* as an `Effected` is (`./effect.ts`): with nothing drawn its answer is `t`,
|
|
96
|
+
* and placed, on its own or as a part of any combinator, it says the glyph
|
|
97
|
+
* effect's exact hold at that time and the output's `asciiOnly`, which the
|
|
98
|
+
* frame's drawing told (`Moves`).
|
|
102
99
|
*/
|
|
103
100
|
export class GlyphEffected {
|
|
104
101
|
renderable;
|
|
@@ -113,11 +110,11 @@ export class GlyphEffected {
|
|
|
113
110
|
at(t) {
|
|
114
111
|
return new Reglyphed(this.renderable, this.effect, { t, key: this.key });
|
|
115
112
|
}
|
|
116
|
-
nextChange(t) {
|
|
117
|
-
return t;
|
|
113
|
+
nextChange(t, moves) {
|
|
114
|
+
return moves?.(t, this.key)?.nextChange ?? t;
|
|
118
115
|
}
|
|
119
|
-
lastChange(t) {
|
|
120
|
-
return t;
|
|
116
|
+
lastChange(t, moves) {
|
|
117
|
+
return moves?.(t, this.key)?.lastChange ?? t;
|
|
121
118
|
}
|
|
122
119
|
/** The element under the glyph effect with its motion taken out. */
|
|
123
120
|
reduced() {
|
|
@@ -126,8 +123,7 @@ export class GlyphEffected {
|
|
|
126
123
|
}
|
|
127
124
|
/** Drawn at the frame's time, its cells stamped with the glyph effect's exact hold there. */
|
|
128
125
|
render(options) {
|
|
129
|
-
|
|
130
|
-
return stampFrame(segments, hold);
|
|
126
|
+
return placedRender(this, options);
|
|
131
127
|
}
|
|
132
128
|
measure(options) {
|
|
133
129
|
return Measurement.get(options, this.renderable);
|
|
@@ -161,7 +157,7 @@ function showGlyphs(placed, frame) {
|
|
|
161
157
|
* its every style and span, each moved to where its glyphs now sit, and a
|
|
162
158
|
* frame that changes nothing hands the text back as it was. What it says of
|
|
163
159
|
* when it changes is the effect's, unstamped: whoever draws it at a time
|
|
164
|
-
* stamps it (`
|
|
160
|
+
* stamps it (`placed`), or marks it for the engine to (the template `glyphs`).
|
|
165
161
|
*/
|
|
166
162
|
export function reglyphText(text, effect, options) {
|
|
167
163
|
const { placed, cells } = placeGlyphs([new Segment(text.plain)], options.key);
|
|
@@ -193,7 +189,7 @@ function rewritten(text, glyphs, shown) {
|
|
|
193
189
|
result.stylize(span.style, moved(span.start), moved(span.end));
|
|
194
190
|
return result;
|
|
195
191
|
}
|
|
196
|
-
/** Line breaks and tabs, which `
|
|
192
|
+
/** Line breaks and tabs, which `sanitized` leaves for a text's own lines and which no cell may hold. */
|
|
197
193
|
const BREAKS = pattern(/[\n\t]/);
|
|
198
194
|
/**
|
|
199
195
|
* What `frame` shows in `cell`, held to the contract: as many cells wide as
|
|
@@ -205,7 +201,7 @@ function shows(frame, cell) {
|
|
|
205
201
|
const text = frame(cell);
|
|
206
202
|
if (text === cell.glyph)
|
|
207
203
|
return text;
|
|
208
|
-
if (cellLen(text) !== cellLen(cell.glyph) || BREAKS.test(text) ||
|
|
204
|
+
if (cellLen(text) !== cellLen(cell.glyph) || BREAKS.test(text) || sanitized(text) !== text) {
|
|
209
205
|
throw new RangeError(`a glyph effect turned ${JSON.stringify(cell.glyph)} at row ${cell.row}, column ${cell.col} into ${JSON.stringify(text)}: ` +
|
|
210
206
|
`it must be ${cellLen(cell.glyph)} cell(s) wide, with no line break or control character`);
|
|
211
207
|
}
|
|
@@ -218,18 +214,24 @@ function shows(frame, cell) {
|
|
|
218
214
|
* wide as its glyph is `shows`'s check; this is the half no one cell can
|
|
219
215
|
* see — a trailing zero-width joiner that fuses with the next emoji, or a
|
|
220
216
|
* leading combining mark that draws on the glyph before it, is right alone
|
|
221
|
-
* and wrong beside its neighbour. Only a
|
|
222
|
-
* that splits a cluster across its own segments is drawn
|
|
217
|
+
* and wrong beside its neighbour. Only a join the effect made is asked, seam
|
|
218
|
+
* by seam, so a child that splits a cluster across its own segments is drawn
|
|
219
|
+
* as it drew it, whatever becomes of the glyph its mark sits on.
|
|
223
220
|
*/
|
|
224
221
|
function standsApart(placed, shown) {
|
|
225
|
-
|
|
226
|
-
|
|
222
|
+
const fuse = (left, right) => graphemes(left + right).length < graphemes(left).length + graphemes(right).length;
|
|
223
|
+
// Whether the text at `i` fuses across its left seam and across its right.
|
|
224
|
+
const seams = (texts, i) => {
|
|
225
|
+
// A line break is no cell, and nothing joins across it.
|
|
226
|
+
const beside = (j) => (placed[j]?.cell === undefined ? "" : texts(j));
|
|
227
|
+
const [before, text, after] = [beside(i - 1), texts(i), beside(i + 1)];
|
|
228
|
+
return [fuse(before, text + after), fuse(before + text, after)];
|
|
229
|
+
};
|
|
227
230
|
shown.forEach((text, i) => {
|
|
228
231
|
const { glyph, cell } = placed[i];
|
|
229
232
|
if (cell === undefined || text === glyph)
|
|
230
233
|
return;
|
|
231
|
-
|
|
232
|
-
if (graphemes(before + text + after).length !== graphemes(before).length + graphemes(text).length + graphemes(after).length) {
|
|
234
|
+
if (seams((j) => shown[j], i).some((fused, seam) => fused && !seams((j) => placed[j].glyph, i)[seam])) {
|
|
233
235
|
throw new RangeError(`a glyph effect turned ${JSON.stringify(glyph)} at row ${cell.row}, column ${cell.col} into ${JSON.stringify(text)}, ` +
|
|
234
236
|
`which joins the cell beside it into one character`);
|
|
235
237
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The glyph effect catalogue: a typewriter and a scramble, which run once and
|
|
3
3
|
* are done, each with its way out for an element leaving (`backspace`,
|
|
4
|
-
* `scrambleOut`), and a wave of case changes, rain
|
|
5
|
-
* long as they are drawn. Each is a `GlyphEffect` for
|
|
6
|
-
* (`./glyph-effect.ts`), built as the colour catalogue's are
|
|
7
|
-
* (`./effects.ts`): one function each over the one shape,
|
|
8
|
-
*
|
|
4
|
+
* `scrambleOut`), and a wave of case changes, rain, stars and marching ants,
|
|
5
|
+
* which loop for as long as they are drawn. Each is a `GlyphEffect` for
|
|
6
|
+
* `GlyphEffected` (`./glyph-effect.ts`), built as the colour catalogue's are
|
|
7
|
+
* (`./effects.ts`): one function each over the one shape, at the defaults in
|
|
8
|
+
* `GLYPH_DEFAULTS`. `GLYPH_EFFECT_CATALOGUE` makes by name those a template's
|
|
9
|
+
* `glyphs` takes.
|
|
9
10
|
*
|
|
10
11
|
* Where a colour loop glides, a glyph cannot: a cell shows one glyph or
|
|
11
12
|
* another, so every loop here moves in steps, on a `beat`, and says when its
|
|
@@ -90,11 +91,25 @@ export interface Particles {
|
|
|
90
91
|
* row a step, and one that holds still draws a fresh field each life.
|
|
91
92
|
*/
|
|
92
93
|
export declare function particles(spec: Particles, z: number): GlyphEffect;
|
|
93
|
-
/**
|
|
94
|
+
/** How ants march: a step, in seconds, for the gaps to move one cell, and the most cells a gap may be from the next. */
|
|
95
|
+
export interface MarchingAnts {
|
|
96
|
+
readonly step: number;
|
|
97
|
+
readonly every: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Marching ants: gaps blanked round each box the element draws, spaced as
|
|
101
|
+
* evenly as the ring allows and never more than `every` cells apart, walking
|
|
102
|
+
* one cell clockwise a `step`. The whole pattern turns, so a ring that `every`
|
|
103
|
+
* does not divide closes with no seam at its corner. The ring is whatever the box
|
|
104
|
+
* is (`Box.frame`), at any width and beside a title. A ring may draw blanks and
|
|
105
|
+
* they keep the count, but an element with no ink on any ring never changes.
|
|
106
|
+
*/
|
|
107
|
+
export declare function marchingAnts({ step, every }: MarchingAnts): GlyphEffect;
|
|
108
|
+
/** The names a template's `glyphs` takes for an effect that loops. */
|
|
94
109
|
export type GlyphLoopName = "wave" | "rain" | "stars";
|
|
95
|
-
/** The names
|
|
110
|
+
/** The names a template's `glyphs` takes for an effect that runs once. */
|
|
96
111
|
export type GlyphTransitionName = "typewriter" | "scramble" | "backspace" | "scrambleOut";
|
|
97
|
-
/** Each
|
|
112
|
+
/** Each glyph effect's defaults, as first drawn in `examples/glyph-effects`. */
|
|
98
113
|
export declare const GLYPH_DEFAULTS: {
|
|
99
114
|
readonly typewriter: {
|
|
100
115
|
readonly seconds: 2;
|
|
@@ -132,6 +147,10 @@ export declare const GLYPH_DEFAULTS: {
|
|
|
132
147
|
readonly drift: 0;
|
|
133
148
|
readonly life: 1;
|
|
134
149
|
};
|
|
150
|
+
readonly march: {
|
|
151
|
+
readonly step: 0.15;
|
|
152
|
+
readonly every: 3;
|
|
153
|
+
};
|
|
135
154
|
};
|
|
136
155
|
/**
|
|
137
156
|
* Every glyph effect in the catalogue by name, each made the one way: for an
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The glyph effect catalogue: a typewriter and a scramble, which run once and
|
|
3
3
|
* are done, each with its way out for an element leaving (`backspace`,
|
|
4
|
-
* `scrambleOut`), and a wave of case changes, rain
|
|
5
|
-
* long as they are drawn. Each is a `GlyphEffect` for
|
|
6
|
-
* (`./glyph-effect.ts`), built as the colour catalogue's are
|
|
7
|
-
* (`./effects.ts`): one function each over the one shape,
|
|
8
|
-
*
|
|
4
|
+
* `scrambleOut`), and a wave of case changes, rain, stars and marching ants,
|
|
5
|
+
* which loop for as long as they are drawn. Each is a `GlyphEffect` for
|
|
6
|
+
* `GlyphEffected` (`./glyph-effect.ts`), built as the colour catalogue's are
|
|
7
|
+
* (`./effects.ts`): one function each over the one shape, at the defaults in
|
|
8
|
+
* `GLYPH_DEFAULTS`. `GLYPH_EFFECT_CATALOGUE` makes by name those a template's
|
|
9
|
+
* `glyphs` takes.
|
|
9
10
|
*
|
|
10
11
|
* Where a colour loop glides, a glyph cannot: a cell shows one glyph or
|
|
11
12
|
* another, so every loop here moves in steps, on a `beat`, and says when its
|
|
@@ -50,6 +51,10 @@ function text(cells) {
|
|
|
50
51
|
}
|
|
51
52
|
return { length: place.size, place };
|
|
52
53
|
}
|
|
54
|
+
/** `n` wrapped into [0, `size`), for a count a timeline may run backwards past 0. */
|
|
55
|
+
function wrapped(n, size) {
|
|
56
|
+
return ((n % size) + size) % size;
|
|
57
|
+
}
|
|
53
58
|
/** Of `n` cells, how many have arrived at eased progress `p`, an ease that overshoots held to the cells there are. */
|
|
54
59
|
function arrived(n, p) {
|
|
55
60
|
return Math.min(n, Math.max(0, Math.floor(n * p)));
|
|
@@ -200,7 +205,7 @@ export function caseWave({ step, width }, z) {
|
|
|
200
205
|
const start = Math.floor(hash(z, 2.3) * span);
|
|
201
206
|
return steps((s) => {
|
|
202
207
|
// The column just past the band's right edge; it enters at the left.
|
|
203
|
-
const lead = (
|
|
208
|
+
const lead = wrapped(s + start, span);
|
|
204
209
|
// A wide glyph is in the band if any of its columns is.
|
|
205
210
|
return (cell) => (cell.col < lead && cell.col + cellLen(cell.glyph) > lead - width ? swapCase(cell.glyph) : cell.glyph);
|
|
206
211
|
}, AS_WRITTEN);
|
|
@@ -250,7 +255,69 @@ export function particles(spec, z) {
|
|
|
250
255
|
}, AS_WRITTEN);
|
|
251
256
|
};
|
|
252
257
|
}
|
|
253
|
-
/**
|
|
258
|
+
/**
|
|
259
|
+
* Where each cell stands on the ring of the box frame that drew it, by
|
|
260
|
+
* `index`. A ring is the
|
|
261
|
+
* perimeter of the rectangle a frame's cells cover, read in the element's own
|
|
262
|
+
* rows and columns, so it is true wherever the box sits among them. A rule or
|
|
263
|
+
* divider inside that rectangle, as a table draws, is frame and no part of the
|
|
264
|
+
* ring, and a title set into the frame is no frame but keeps its places.
|
|
265
|
+
*/
|
|
266
|
+
function ringPlaces(cells) {
|
|
267
|
+
const frames = new Map();
|
|
268
|
+
for (const cell of cells) {
|
|
269
|
+
if (cell.boxFrame === undefined)
|
|
270
|
+
continue;
|
|
271
|
+
const ring = frames.get(cell.boxFrame);
|
|
272
|
+
if (ring === undefined)
|
|
273
|
+
frames.set(cell.boxFrame, [cell]);
|
|
274
|
+
else
|
|
275
|
+
ring.push(cell);
|
|
276
|
+
}
|
|
277
|
+
const places = new Map();
|
|
278
|
+
for (const ring of frames.values()) {
|
|
279
|
+
// Cells come in reading order, so the rows are the ring's two ends.
|
|
280
|
+
const [top, down] = [ring[0].row, ring[ring.length - 1].row - ring[0].row];
|
|
281
|
+
const left = ring.reduce((most, { col }) => Math.min(most, col), Infinity);
|
|
282
|
+
const across = ring.reduce((most, { col }) => Math.max(most, col), -Infinity) - left;
|
|
283
|
+
const size = Math.max(1, 2 * (across + down));
|
|
284
|
+
for (const cell of ring) {
|
|
285
|
+
const [y, x] = [cell.row - top, cell.col - left];
|
|
286
|
+
const place = y === 0 ? x : x === across ? across + y : y === down ? 2 * across + down - x : x === 0 ? 2 * (across + down) - y : undefined;
|
|
287
|
+
if (place !== undefined)
|
|
288
|
+
places.set(cell.index, { place, cells: size });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return places;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Marching ants: gaps blanked round each box the element draws, spaced as
|
|
295
|
+
* evenly as the ring allows and never more than `every` cells apart, walking
|
|
296
|
+
* one cell clockwise a `step`. The whole pattern turns, so a ring that `every`
|
|
297
|
+
* does not divide closes with no seam at its corner. The ring is whatever the box
|
|
298
|
+
* is (`Box.frame`), at any width and beside a title. A ring may draw blanks and
|
|
299
|
+
* they keep the count, but an element with no ink on any ring never changes.
|
|
300
|
+
*/
|
|
301
|
+
export function marchingAnts({ step, every }) {
|
|
302
|
+
positiveSeconds("marchingAnts step", step);
|
|
303
|
+
if (!(Number.isInteger(every) && every > 1))
|
|
304
|
+
throw new RangeError(`marchingAnts every must be a whole number of cells above 1, got ${every}`);
|
|
305
|
+
const steps = stepped(step);
|
|
306
|
+
return ({ cells }) => {
|
|
307
|
+
const places = ringPlaces(cells);
|
|
308
|
+
if (!cells.some((cell) => places.has(cell.index) && inked(cell.glyph)))
|
|
309
|
+
return Animation.constant(AS_WRITTEN);
|
|
310
|
+
return steps((s) => (cell) => {
|
|
311
|
+
const at = places.get(cell.index);
|
|
312
|
+
if (at === undefined)
|
|
313
|
+
return cell.glyph;
|
|
314
|
+
// Bresenham's line over the ring: of `cells` places, the `gaps` where a running count crosses a whole number.
|
|
315
|
+
const gaps = Math.ceil(at.cells / every);
|
|
316
|
+
return (wrapped(at.place - s, at.cells) * gaps) % at.cells < gaps ? blank(cell.glyph) : cell.glyph;
|
|
317
|
+
}, AS_WRITTEN);
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/** Each glyph effect's defaults, as first drawn in `examples/glyph-effects`. */
|
|
254
321
|
export const GLYPH_DEFAULTS = {
|
|
255
322
|
typewriter: { seconds: 2, ease: EASES.linear },
|
|
256
323
|
scramble: { seconds: 2.5, ease: EASES["ease-in"] },
|
|
@@ -259,6 +326,7 @@ export const GLYPH_DEFAULTS = {
|
|
|
259
326
|
wave: { step: 0.06, width: 3 },
|
|
260
327
|
rain: { glyphs: "│╎", ascii: "|:", density: 0.06, step: 0.08, drift: 1, life: Number.POSITIVE_INFINITY },
|
|
261
328
|
stars: { glyphs: "·+*", ascii: ".+*", density: 0.04, step: 1.5, drift: 0, life: 1 },
|
|
329
|
+
march: { step: 0.15, every: 3 },
|
|
262
330
|
};
|
|
263
331
|
/**
|
|
264
332
|
* Every glyph effect in the catalogue by name, each made the one way: for an
|
|
@@ -10,27 +10,31 @@
|
|
|
10
10
|
* part of the drawing asks whether it is leaving. Telling it is a new value
|
|
11
11
|
* (`leave`), as every `Timeline` operation is. [LAW:no-ambient-temporal-coupling]
|
|
12
12
|
*
|
|
13
|
-
* Each half is a `
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
13
|
+
* Each half is a `Half`: handed the element, an `Animation<Renderable>` of
|
|
14
|
+
* it on the half's own clock from 0, in the shape of the library's other
|
|
15
|
+
* one-shot from renderables to an animation (`Change`, `./change.ts`). One
|
|
16
|
+
* value carries the half's frames and its holds, so neither is kept in
|
|
17
|
+
* agreement with the other by hand, and nothing here measures progress
|
|
18
|
+
* itself: a half's schedule carried onto the host's clock by `Timeline.run`
|
|
19
|
+
* is that half's hold — every frame for a fade, once a hold for one on a
|
|
20
|
+
* beat — and the moment it says it never changes again (`Animation.done`)
|
|
21
|
+
* is where the arrival ends and where the element is gone. The frames carry
|
|
22
|
+
* no word of their own on when they change: placed, the lifecycle stamps
|
|
23
|
+
* them with its hold at the frame's time (`placedRender`). The exit is
|
|
24
|
+
* drawn over whatever the arrival draws, so an element told to leave while
|
|
25
|
+
* it is still arriving leaves from where it is, never shown whole first —
|
|
26
|
+
* though a colour exit is spread over the cells each frame shows, so a cell
|
|
27
|
+
* the arrival adds can move one already leaving back toward whole
|
|
28
|
+
* (`ownChange`, `./effects.ts`). [LAW:dataflow-not-control-flow]
|
|
26
29
|
*
|
|
27
30
|
* A half draws the element through a colour transition (`throughColors`, a
|
|
28
|
-
* `fadeIn`) or a glyph one (`throughGlyphs`, a `typewriter`): each
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* a
|
|
33
|
-
* `scrambleOut` — so the element has
|
|
31
|
+
* `fadeIn`) or a glyph one (`throughGlyphs`, a `typewriter`): each binds its
|
|
32
|
+
* transition to the one wrapper that draws it, so nothing here asks which kind
|
|
33
|
+
* a half is, and a line can type itself in and dissolve out. A half
|
|
34
|
+
* written by hand is any function of that shape — an arrival that slides the
|
|
35
|
+
* element in, which no effect draws. An exit is a half that ends with nothing
|
|
36
|
+
* drawn — `dissolveOut`, `backspace`, `scrambleOut` — so the element has
|
|
37
|
+
* already gone from sight when it is gone.
|
|
34
38
|
*
|
|
35
39
|
* Gone is a fact the host reads (`gone(t)`) to drop the element, and from then
|
|
36
40
|
* the lifecycle draws nothing and never changes, so a host still holding it
|
|
@@ -38,27 +42,31 @@
|
|
|
38
42
|
* backwards — it draws nothing too, but is not gone.
|
|
39
43
|
*/
|
|
40
44
|
import { Animation } from "../core/animation.js";
|
|
41
|
-
import type { TerminalTheme } from "../core/color.js";
|
|
42
45
|
import type { Renderable } from "../core/protocol.js";
|
|
43
|
-
import { Transition } from "./effects.js";
|
|
46
|
+
import type { Transition } from "./effects.js";
|
|
44
47
|
import type { GlyphTransition } from "./glyph-effects.js";
|
|
45
48
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
49
|
+
* One half of an element's lifecycle: handed the element and `key`, which
|
|
50
|
+
* names it for the half's seeds, the element as the half draws it from
|
|
51
|
+
* `t = 0`, the moment the half began, on its own clock. Its holds are the
|
|
52
|
+
* half's own, whatever element it is handed, and it is done from the first
|
|
53
|
+
* moment it says it never changes again (`Animation.done`): an arrival is
|
|
54
|
+
* the element from then, and an exit is gone. A half that never ends is
|
|
55
|
+
* refused when the lifecycle is made: an `Effected` or `GlyphEffected`
|
|
56
|
+
* handed in bare is one, since its own word is "now" at every time;
|
|
57
|
+
* `throughColors` and `throughGlyphs` lend one the transition's schedule.
|
|
50
58
|
*/
|
|
51
|
-
export type
|
|
52
|
-
/** `transition`'s colours drawn over an element
|
|
53
|
-
export declare function throughColors(transition: Transition
|
|
59
|
+
export type Half = (element: Renderable, key: string) => Animation<Renderable>;
|
|
60
|
+
/** `transition`'s colours drawn over an element, arriving from and leaving to the ground of the terminal the frame is drawn on. */
|
|
61
|
+
export declare function throughColors(transition: Transition): Half;
|
|
54
62
|
/** `transition`'s glyphs drawn over an element, at every depth, with no colour too. */
|
|
55
|
-
export declare function throughGlyphs(transition: GlyphTransition):
|
|
63
|
+
export declare function throughGlyphs(transition: GlyphTransition): Half;
|
|
56
64
|
/** How an element arrives and leaves, and what it is named for their seeds. */
|
|
57
65
|
export interface LifecycleOptions {
|
|
58
|
-
/** The half it arrives through — `throughColors(fadeIn(curve, z)
|
|
59
|
-
readonly enter:
|
|
66
|
+
/** The half it arrives through — `throughColors(fadeIn(curve, z))`. */
|
|
67
|
+
readonly enter: Half;
|
|
60
68
|
/** The half it leaves through — `throughGlyphs(scrambleOut(timing, z))`. */
|
|
61
|
-
readonly exit:
|
|
69
|
+
readonly exit: Half;
|
|
62
70
|
/** Names the element for both halves' seeds, as `Effected`'s and `GlyphEffected`'s key does. */
|
|
63
71
|
readonly key: string;
|
|
64
72
|
}
|
|
@@ -10,27 +10,31 @@
|
|
|
10
10
|
* part of the drawing asks whether it is leaving. Telling it is a new value
|
|
11
11
|
* (`leave`), as every `Timeline` operation is. [LAW:no-ambient-temporal-coupling]
|
|
12
12
|
*
|
|
13
|
-
* Each half is a `
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
13
|
+
* Each half is a `Half`: handed the element, an `Animation<Renderable>` of
|
|
14
|
+
* it on the half's own clock from 0, in the shape of the library's other
|
|
15
|
+
* one-shot from renderables to an animation (`Change`, `./change.ts`). One
|
|
16
|
+
* value carries the half's frames and its holds, so neither is kept in
|
|
17
|
+
* agreement with the other by hand, and nothing here measures progress
|
|
18
|
+
* itself: a half's schedule carried onto the host's clock by `Timeline.run`
|
|
19
|
+
* is that half's hold — every frame for a fade, once a hold for one on a
|
|
20
|
+
* beat — and the moment it says it never changes again (`Animation.done`)
|
|
21
|
+
* is where the arrival ends and where the element is gone. The frames carry
|
|
22
|
+
* no word of their own on when they change: placed, the lifecycle stamps
|
|
23
|
+
* them with its hold at the frame's time (`placedRender`). The exit is
|
|
24
|
+
* drawn over whatever the arrival draws, so an element told to leave while
|
|
25
|
+
* it is still arriving leaves from where it is, never shown whole first —
|
|
26
|
+
* though a colour exit is spread over the cells each frame shows, so a cell
|
|
27
|
+
* the arrival adds can move one already leaving back toward whole
|
|
28
|
+
* (`ownChange`, `./effects.ts`). [LAW:dataflow-not-control-flow]
|
|
26
29
|
*
|
|
27
30
|
* A half draws the element through a colour transition (`throughColors`, a
|
|
28
|
-
* `fadeIn`) or a glyph one (`throughGlyphs`, a `typewriter`): each
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* a
|
|
33
|
-
* `scrambleOut` — so the element has
|
|
31
|
+
* `fadeIn`) or a glyph one (`throughGlyphs`, a `typewriter`): each binds its
|
|
32
|
+
* transition to the one wrapper that draws it, so nothing here asks which kind
|
|
33
|
+
* a half is, and a line can type itself in and dissolve out. A half
|
|
34
|
+
* written by hand is any function of that shape — an arrival that slides the
|
|
35
|
+
* element in, which no effect draws. An exit is a half that ends with nothing
|
|
36
|
+
* drawn — `dissolveOut`, `backspace`, `scrambleOut` — so the element has
|
|
37
|
+
* already gone from sight when it is gone.
|
|
34
38
|
*
|
|
35
39
|
* Gone is a fact the host reads (`gone(t)`) to drop the element, and from then
|
|
36
40
|
* the lifecycle draws nothing and never changes, so a host still holding it
|
|
@@ -40,20 +44,31 @@
|
|
|
40
44
|
import { Animation } from "../core/animation.js";
|
|
41
45
|
import { firstReaching } from "../core/easing.js";
|
|
42
46
|
import { Timeline } from "../core/timeline.js";
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return lift(transition, (element, t, key) => new Recolored(element, transition.effect, { t, key, theme }));
|
|
47
|
+
import { Effected } from "./effect.js";
|
|
48
|
+
import { GlyphEffected } from "./glyph-effect.js";
|
|
49
|
+
/** `transition`'s colours drawn over an element, arriving from and leaving to the ground of the terminal the frame is drawn on. */
|
|
50
|
+
export function throughColors(transition) {
|
|
51
|
+
return (element, key) => scheduled(new Effected(element, transition.effect, { key }), transition.schedule);
|
|
49
52
|
}
|
|
50
53
|
/** `transition`'s glyphs drawn over an element, at every depth, with no colour too. */
|
|
51
54
|
export function throughGlyphs(transition) {
|
|
52
|
-
return
|
|
55
|
+
return (element, key) => scheduled(new GlyphEffected(element, transition.effect, { key }), transition.schedule);
|
|
53
56
|
}
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
/**
|
|
58
|
+
* `placed` — an element under a transition's effect — changing when
|
|
59
|
+
* `schedule`, the transition's own, says: its frames with the one word on
|
|
60
|
+
* when they change that the transition has and the wrapper lacks, whose own
|
|
61
|
+
* answer is "now" until it is drawn. [LAW:one-source-of-truth] Reduced,
|
|
62
|
+
* both are reduced together, as `onBeat` steps both: a one-shot's effect
|
|
63
|
+
* and its schedule each reduce to a cut at 0 (`transition`, `./effects.ts`).
|
|
64
|
+
*/
|
|
65
|
+
function scheduled(placed, schedule) {
|
|
66
|
+
return Animation.of({
|
|
67
|
+
at: (t) => placed.at(t),
|
|
68
|
+
nextChange: (t) => schedule.nextChange(t),
|
|
69
|
+
lastChange: (t) => schedule.lastChange(t),
|
|
70
|
+
reduced: () => scheduled(placed.reduced(), schedule.reduced()),
|
|
71
|
+
});
|
|
57
72
|
}
|
|
58
73
|
/** What an element draws before it entered and once it is gone: no cells, and no width. */
|
|
59
74
|
const NOTHING = { render: () => [], measure: () => ({ minimum: 0, maximum: 0 }) };
|
|
@@ -61,26 +76,50 @@ const NOTHING = { render: () => [], measure: () => ({ minimum: 0, maximum: 0 })
|
|
|
61
76
|
export function lifecycle(element, options, entered) {
|
|
62
77
|
if (!Number.isFinite(entered))
|
|
63
78
|
throw new RangeError(`lifecycle: entered must be a finite time, got ${entered}`);
|
|
64
|
-
|
|
79
|
+
const { enter, exit, key } = options;
|
|
80
|
+
const arrival = ending(enter(element, key), "enter", key);
|
|
81
|
+
const departure = ending(exit(element, key), "exit", key);
|
|
82
|
+
return life(element, options, { arrival, arrived: doneAt(arrival, entered), departure }, entered, Number.POSITIVE_INFINITY);
|
|
65
83
|
}
|
|
66
84
|
/**
|
|
67
|
-
* The first host moment `
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* [LAW:single-enforcer] Since `x - start` never falls as `x` rises, a host
|
|
71
|
-
* is at or past this moment exactly when `done` holds there.
|
|
85
|
+
* The first host moment `half`, begun at host time `start`, is done: its own
|
|
86
|
+
* answer read on host time, never a sum `start + seconds`, which can round
|
|
87
|
+
* to either side of it, as `Animation.sequence` carries its switch.
|
|
88
|
+
* [LAW:single-enforcer] Since `x - start` never falls as `x` rises, a host
|
|
89
|
+
* time is at or past this moment exactly when `done` holds there. Done is
|
|
90
|
+
* for good, so a half done as it begins — the element at once — is done at
|
|
91
|
+
* `start`, and from a start it is not done at the walk is upward only: no
|
|
92
|
+
* half is asked about a time before it began.
|
|
72
93
|
*/
|
|
73
|
-
function doneAt(
|
|
74
|
-
|
|
94
|
+
function doneAt(half, start) {
|
|
95
|
+
if (!Number.isFinite(start) || Animation.done(half, 0))
|
|
96
|
+
return start;
|
|
97
|
+
return firstReaching(start, (x) => Animation.done(half, x - start));
|
|
75
98
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
99
|
+
/**
|
|
100
|
+
* `half` over the element, asked where it ends on its own clock, or refused
|
|
101
|
+
* by its `role` and the element's `key` when it never does. [LAW:no-silent-failure]
|
|
102
|
+
* Kept, it would never arrive, or never be gone, and wake the host every frame
|
|
103
|
+
* for good; asked here, the refusal lands where the options were written and
|
|
104
|
+
* not at the first `leave`. A half on a beat that never ends is refused by the
|
|
105
|
+
* beat instead, where the walk asks it past what its holds resolve.
|
|
106
|
+
*/
|
|
107
|
+
function ending(half, role, key) {
|
|
108
|
+
if (doneAt(half, 0) === Number.POSITIVE_INFINITY) {
|
|
109
|
+
throw new RangeError(`lifecycle "${key}": the ${role} half never ends: its animation never says it has stopped changing (Animation.done). An Effected or GlyphEffected on its own does not; throughColors(transition) and throughGlyphs(transition) lend one the transition's schedule`);
|
|
110
|
+
}
|
|
111
|
+
return half;
|
|
112
|
+
}
|
|
113
|
+
function life(element, options, halves, entered, left) {
|
|
114
|
+
const { exit, key } = options;
|
|
115
|
+
const { arrival, arrived, departure } = halves;
|
|
116
|
+
// Where the exit is gone, and the exit's hold, are read off the exit over the element; the frame while
|
|
117
|
+
// leaving is the exit over what is shown, whose holds are the same by `Half`'s contract.
|
|
118
|
+
const goneAt = doneAt(departure, left);
|
|
119
|
+
// The arrival on the host's clock: when its frame next changes, and since when it has been what it is.
|
|
120
|
+
const arriving = Timeline.run(Timeline.play(entered), arrival);
|
|
82
121
|
// What the element draws by `t` if it is not leaving: arriving through `enter`, then itself.
|
|
83
|
-
const shown = (t) => (t < arrived ?
|
|
122
|
+
const shown = (t) => (t < arrived ? arrival.at(t - entered) : element);
|
|
84
123
|
const held = (schedule, t) => ({ last: schedule.lastChange(t), next: schedule.nextChange(t) });
|
|
85
124
|
// [LAW:one-source-of-truth] Each phase's frame and its hold, side by side, so the two cannot disagree.
|
|
86
125
|
// The frames say nothing of when they change — the hold does, stamped where the lifecycle is placed.
|
|
@@ -89,10 +128,10 @@ function life(element, options, entered, left) {
|
|
|
89
128
|
return { frame: NOTHING, last: Number.NEGATIVE_INFINITY, next: entered };
|
|
90
129
|
if (t >= goneAt)
|
|
91
130
|
return { frame: NOTHING, last: goneAt, next: Number.POSITIVE_INFINITY };
|
|
92
|
-
// Leaving
|
|
131
|
+
// Leaving is drawn over whatever arriving draws, so both schedules count; once arrived, the arrival's never changes again.
|
|
93
132
|
if (t >= left) {
|
|
94
|
-
const leaving = Animation.all([arriving, Timeline.run(Timeline.play(left),
|
|
95
|
-
return { frame: exit
|
|
133
|
+
const leaving = Animation.all([arriving, Timeline.run(Timeline.play(left), departure)]);
|
|
134
|
+
return { frame: exit(shown(t), key).at(t - left), ...held(leaving, t) };
|
|
96
135
|
}
|
|
97
136
|
if (t < arrived)
|
|
98
137
|
return { frame: shown(t), ...held(arriving, t) };
|
|
@@ -126,7 +165,7 @@ function life(element, options, entered, left) {
|
|
|
126
165
|
throw new RangeError(`lifecycle: already told to leave at ${left}, told again at ${at}`);
|
|
127
166
|
if (!(Number.isFinite(at) && at >= entered))
|
|
128
167
|
throw new RangeError(`lifecycle: leave must be a finite time from entered (${entered}) on, got ${at}`);
|
|
129
|
-
return life(element, options, entered, at);
|
|
168
|
+
return life(element, options, halves, entered, at);
|
|
130
169
|
},
|
|
131
170
|
});
|
|
132
171
|
}
|