@promptctl/rich-js 0.24.1 → 0.25.1
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 +7 -0
- package/dist/core/anchor.d.ts +5 -0
- package/dist/core/anchor.js +11 -4
- package/dist/core/animation.d.ts +175 -10
- package/dist/core/animation.js +283 -15
- package/dist/core/ansi.js +2 -2
- package/dist/core/box.d.ts +7 -7
- package/dist/core/cells.d.ts +23 -13
- package/dist/core/cells.js +58 -48
- package/dist/core/clock.d.ts +2 -2
- package/dist/core/clock.js +6 -4
- package/dist/core/color.d.ts +27 -10
- package/dist/core/color.js +24 -33
- package/dist/core/console.d.ts +28 -17
- package/dist/core/console.js +44 -39
- package/dist/core/easing.d.ts +30 -0
- package/dist/core/easing.js +65 -20
- package/dist/core/emoji.d.ts +1 -1
- package/dist/core/emoji.js +1 -2
- package/dist/core/export-html.js +2 -1
- package/dist/core/export-lines.js +3 -3
- package/dist/core/highlighter.d.ts +9 -8
- package/dist/core/highlighter.js +3 -2
- package/dist/core/json.js +2 -2
- package/dist/core/markup.js +16 -12
- package/dist/core/measure.js +2 -2
- package/dist/core/memo.d.ts +11 -2
- package/dist/core/memo.js +16 -0
- package/dist/core/oklch.js +17 -6
- package/dist/core/osc8.d.ts +1 -1
- package/dist/core/osc8.js +4 -5
- package/dist/core/paint.d.ts +2 -1
- package/dist/core/paint.js +4 -3
- package/dist/core/pattern.d.ts +21 -0
- package/dist/core/pattern.js +21 -0
- package/dist/core/pretty.js +9 -10
- package/dist/core/protocol.d.ts +100 -39
- package/dist/core/protocol.js +167 -21
- package/dist/core/render.d.ts +15 -2
- package/dist/core/render.js +25 -5
- package/dist/core/segment.d.ts +13 -5
- package/dist/core/segment.js +23 -9
- package/dist/core/spinnerData.d.ts +1 -1
- package/dist/core/state.d.ts +53 -0
- package/dist/core/state.js +82 -0
- package/dist/core/strip.d.ts +98 -29
- package/dist/core/strip.js +211 -150
- package/dist/core/style.d.ts +47 -14
- package/dist/core/style.js +57 -28
- package/dist/core/text.d.ts +5 -8
- package/dist/core/text.js +116 -124
- package/dist/core/timeline.d.ts +30 -5
- package/dist/core/timeline.js +43 -7
- package/dist/core/tween.d.ts +1 -1
- package/dist/core/tween.js +5 -3
- package/dist/core/view.d.ts +92 -0
- package/dist/core/view.js +83 -0
- package/dist/host/app.d.ts +49 -21
- package/dist/host/app.js +71 -37
- package/dist/host/input.js +3 -4
- package/dist/index.d.ts +24 -12
- package/dist/index.js +9 -4
- package/dist/node/traceback.js +4 -3
- package/dist/renderables/align.d.ts +2 -3
- package/dist/renderables/align.js +0 -3
- package/dist/renderables/change.d.ts +42 -0
- package/dist/renderables/change.js +112 -0
- package/dist/renderables/columns.d.ts +3 -4
- package/dist/renderables/columns.js +2 -7
- package/dist/renderables/constrain.d.ts +2 -3
- package/dist/renderables/constrain.js +0 -3
- package/dist/renderables/effect.d.ts +158 -63
- package/dist/renderables/effect.js +378 -155
- package/dist/renderables/effects.d.ts +122 -11
- package/dist/renderables/effects.js +238 -94
- package/dist/renderables/embed.d.ts +1 -1
- package/dist/renderables/embed.js +16 -8
- package/dist/renderables/flexStrip.d.ts +2 -3
- package/dist/renderables/flexStrip.js +0 -4
- package/dist/renderables/frames.d.ts +35 -54
- package/dist/renderables/frames.js +43 -58
- package/dist/renderables/glyph-effect.d.ts +158 -0
- package/dist/renderables/glyph-effect.js +237 -0
- package/dist/renderables/glyph-effects.d.ts +147 -0
- package/dist/renderables/glyph-effects.js +283 -0
- package/dist/renderables/group.d.ts +3 -4
- package/dist/renderables/group.js +0 -3
- package/dist/renderables/layout.d.ts +12 -12
- package/dist/renderables/layout.js +40 -46
- package/dist/renderables/lifecycle.d.ts +77 -0
- package/dist/renderables/lifecycle.js +132 -0
- package/dist/renderables/list-view.d.ts +4 -5
- package/dist/renderables/list-view.js +9 -13
- package/dist/renderables/live.d.ts +36 -21
- package/dist/renderables/live.js +88 -41
- package/dist/renderables/markdown-parse.d.ts +4 -4
- package/dist/renderables/markdown-parse.js +52 -46
- package/dist/renderables/markdown-url.js +11 -11
- package/dist/renderables/padding.d.ts +4 -5
- package/dist/renderables/padding.js +0 -3
- package/dist/renderables/panel.d.ts +14 -16
- package/dist/renderables/panel.js +6 -14
- package/dist/renderables/progress.d.ts +83 -34
- package/dist/renderables/progress.js +182 -95
- package/dist/renderables/progressBar.d.ts +22 -12
- package/dist/renderables/progressBar.js +32 -9
- package/dist/renderables/prompt.js +3 -2
- package/dist/renderables/python-space.js +2 -1
- package/dist/renderables/scroll.d.ts +50 -0
- package/dist/renderables/scroll.js +106 -0
- package/dist/renderables/spinner.d.ts +35 -14
- package/dist/renderables/spinner.js +65 -25
- package/dist/renderables/status.js +1 -1
- package/dist/renderables/syntax.d.ts +10 -9
- package/dist/renderables/syntax.js +3 -2
- package/dist/renderables/table.d.ts +34 -40
- package/dist/renderables/table.js +41 -63
- package/dist/renderables/traceback.d.ts +2 -2
- package/dist/renderables/traceback.js +6 -5
- package/dist/renderables/tree.d.ts +27 -25
- package/dist/renderables/tree.js +104 -58
- package/dist/renderables/viewport.d.ts +75 -44
- package/dist/renderables/viewport.js +145 -82
- package/dist/template-bindings/animation-funcs.d.ts +78 -9
- package/dist/template-bindings/animation-funcs.js +132 -14
- package/dist/template-bindings/index.d.ts +5 -5
- package/dist/template-bindings/index.js +5 -5
- package/dist/themes/ramp.d.ts +46 -3
- package/dist/themes/ramp.js +61 -4
- package/dist/themes/registry.js +5 -7
- package/dist/widgets/button.js +3 -2
- package/dist/widgets/checkbox.js +4 -3
- package/dist/widgets/dropdown.d.ts +3 -1
- package/dist/widgets/dropdown.js +20 -15
- package/dist/widgets/event-router.js +4 -2
- package/dist/widgets/focus-manager.d.ts +2 -0
- package/dist/widgets/focus-manager.js +6 -4
- package/dist/widgets/hit.d.ts +6 -0
- package/dist/widgets/hit.js +8 -1
- package/dist/widgets/observable.d.ts +23 -0
- package/dist/widgets/observable.js +43 -0
- package/dist/widgets/slider.d.ts +1 -1
- package/dist/widgets/slider.js +12 -3
- package/dist/widgets/static-item.d.ts +2 -3
- package/dist/widgets/static-item.js +0 -3
- package/dist/widgets/text-input.d.ts +28 -32
- package/dist/widgets/text-input.js +360 -346
- package/dist/widgets/themed-widget.js +2 -1
- package/dist/widgets/toggle.js +4 -3
- package/dist/widgets/types.d.ts +10 -4
- package/dist/widgets/types.js +8 -2
- package/dist/widgets/widget-app.d.ts +14 -4
- package/dist/widgets/widget-app.js +36 -7
- package/dist/widgets/widget-base.d.ts +2 -2
- package/dist/widgets/widget-base.js +29 -18
- package/package.json +8 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GlyphEffected — a renderable whose finished cells' glyphs pass through a
|
|
3
|
+
* glyph effect at a moment `t`: the glyph half of `Effected` (`./effect.ts`),
|
|
4
|
+
* built to its contract.
|
|
5
|
+
*
|
|
6
|
+
* A glyph effect is the colour effect's two-stage shape with a glyph where
|
|
7
|
+
* the colours were: for the cells an element draws in a frame, an
|
|
8
|
+
* `Animation` (`../core/animation.ts`) of what is done to them. The first
|
|
9
|
+
* stage is handed every cell once a frame (`GlyphCell`), blanks included —
|
|
10
|
+
* rain and stars live there — and whether the output draws ASCII alone
|
|
11
|
+
* (`DrawnGlyphs`), and works out what holds for the whole frame:
|
|
12
|
+
* how many cells are typed, where a band is. The second, the function
|
|
13
|
+
* `at(t)` returns, maps each cell to the text it shows. A typewriter, a
|
|
14
|
+
* scramble, a wave of case changes and particles on blank cells are all this
|
|
15
|
+
* one shape (`./glyph-effects.ts`) [LAW:one-type-per-behavior].
|
|
16
|
+
*
|
|
17
|
+
* Colour and glyph are not two kinds of animation: each is an `Animation` of
|
|
18
|
+
* a per-cell map, and they differ only in what a cell is mapped to. The
|
|
19
|
+
* wrappers are two because the stages are handed different facts. A colour
|
|
20
|
+
* effect is settled against the depth, the terminal and the colours of the
|
|
21
|
+
* words (`Drawn`), and with no colour output there is nothing for it to
|
|
22
|
+
* change. A glyph effect needs none of that, and is written at every depth,
|
|
23
|
+
* with no colour too. Over one element the two compose by nesting:
|
|
24
|
+
* `new Effected(new GlyphEffected(child, glyphs, …), colours, …)` moves the
|
|
25
|
+
* colours of the glyphs the glyph effect left, since each wrapper measures
|
|
26
|
+
* what its child drew.
|
|
27
|
+
*
|
|
28
|
+
* A `GlyphEffected` is the element under its glyph effect over time, an
|
|
29
|
+
* `Animation<Renderable>` placed as an `Effected` is, its value at `t` the
|
|
30
|
+
* element through the effect at `t` (`Reglyphed`).
|
|
31
|
+
* [LAW:no-ambient-temporal-coupling] The time is the frame's, never a clock
|
|
32
|
+
* read, so the same time draws the same bytes at any frame rate.
|
|
33
|
+
*
|
|
34
|
+
* What a glyph effect may not do is the contract this module holds:
|
|
35
|
+
*
|
|
36
|
+
* - It changes what a cell shows, never which cells exist, where they are or
|
|
37
|
+
* who drew them. The text it returns for a cell is exactly as many cells
|
|
38
|
+
* wide (`cellLen`), carries no line break or control character, and does
|
|
39
|
+
* not join the cell beside it into one grapheme cluster, or the render
|
|
40
|
+
* throws naming the cell [LAW:single-enforcer]. So what sits beside the
|
|
41
|
+
* element never moves, and a wide glyph is changed whole — blanked as two
|
|
42
|
+
* spaces, scrambled as two one-cell glyphs — never cut.
|
|
43
|
+
* - A segment whose every glyph comes back as it was is yielded as it was,
|
|
44
|
+
* so the identity effect is byte-identical. A changed segment keeps its
|
|
45
|
+
* style whole, link, meta and anchor included; no cell has moved, so the
|
|
46
|
+
* anchor's offsets are still true and hit testing still finds the owner.
|
|
47
|
+
* - Placed as a `GlyphEffected`, every cell carries the hold the frame is in
|
|
48
|
+
* (`Style.hold`), under whatever the child said,
|
|
49
|
+
* as under `Effected`; drawn as a `Reglyphed`, a value, none.
|
|
50
|
+
*
|
|
51
|
+
* Positions are the child's own: row and column count from the top-left of
|
|
52
|
+
* what the child drew.
|
|
53
|
+
*/
|
|
54
|
+
import { Animation, frameTimeOf, holdOf, shownAt, stampFrame } from "../core/animation.js";
|
|
55
|
+
import { cellLen, graphemes } from "../core/cells.js";
|
|
56
|
+
import { Measurement } from "../core/measure.js";
|
|
57
|
+
import { render } from "../core/protocol.js";
|
|
58
|
+
import { Segment } from "../core/segment.js";
|
|
59
|
+
import { finiteTime } from "../core/easing.js";
|
|
60
|
+
import { stripControlChars } from "../core/text.js";
|
|
61
|
+
import { cellSeed, walkGlyphs } from "./effect.js";
|
|
62
|
+
import { pattern } from "../core/pattern.js";
|
|
63
|
+
/**
|
|
64
|
+
* A `GlyphEffected` at `t`, or an element through a glyph effect at a time an
|
|
65
|
+
* animation's `at` was handed: `renderable` with its cells' glyphs passed
|
|
66
|
+
* through `effect` at `t`, saying nothing of when that changes, as a
|
|
67
|
+
* `Recolored` is (`./effect.ts`). [LAW:one-type-per-behavior]
|
|
68
|
+
*/
|
|
69
|
+
export class Reglyphed {
|
|
70
|
+
renderable;
|
|
71
|
+
effect;
|
|
72
|
+
t;
|
|
73
|
+
key;
|
|
74
|
+
constructor(renderable, effect, options) {
|
|
75
|
+
this.renderable = renderable;
|
|
76
|
+
this.effect = effect;
|
|
77
|
+
this.t = finiteTime("GlyphEffected", options.t);
|
|
78
|
+
this.key = options.key;
|
|
79
|
+
}
|
|
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
|
+
const segments = [...render(this.renderable, options)];
|
|
86
|
+
const { placed, cells } = placeGlyphs(segments, this.key);
|
|
87
|
+
const animation = this.effect({ cells, asciiOnly: options.asciiOnly === true });
|
|
88
|
+
const frame = shownAt(animation, this.t, options);
|
|
89
|
+
const shown = showGlyphs(placed, frame.value);
|
|
90
|
+
const changed = segments.map((segment, i) => shown[i].every((text, j) => text === placed[i][j].glyph) ? segment : new Segment(shown[i].join(""), segment.style));
|
|
91
|
+
return { segments: changed, hold: holdOf(frame, this.t) };
|
|
92
|
+
}
|
|
93
|
+
measure(options) {
|
|
94
|
+
return Measurement.get(options, this.renderable);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* `renderable` under a glyph effect over time, an `Animation<Renderable>`
|
|
99
|
+
* as an `Effected` is (`./effect.ts`): its own answer is `t`, and placed,
|
|
100
|
+
* its cells carry the glyph effect's exact hold at the frame's time and the
|
|
101
|
+
* output's `asciiOnly`, under whatever the element already said.
|
|
102
|
+
*/
|
|
103
|
+
export class GlyphEffected {
|
|
104
|
+
renderable;
|
|
105
|
+
effect;
|
|
106
|
+
key;
|
|
107
|
+
constructor(renderable, effect, options) {
|
|
108
|
+
this.renderable = renderable;
|
|
109
|
+
this.effect = effect;
|
|
110
|
+
this.key = options.key;
|
|
111
|
+
}
|
|
112
|
+
/** The element through the glyph effect at `t`, unstamped. */
|
|
113
|
+
at(t) {
|
|
114
|
+
return new Reglyphed(this.renderable, this.effect, { t, key: this.key });
|
|
115
|
+
}
|
|
116
|
+
nextChange(t) {
|
|
117
|
+
return t;
|
|
118
|
+
}
|
|
119
|
+
lastChange(t) {
|
|
120
|
+
return t;
|
|
121
|
+
}
|
|
122
|
+
/** The element under the glyph effect with its motion taken out. */
|
|
123
|
+
reduced() {
|
|
124
|
+
const { effect } = this;
|
|
125
|
+
return new GlyphEffected(this.renderable, (drawn) => effect(drawn).reduced(), this);
|
|
126
|
+
}
|
|
127
|
+
/** Drawn at the frame's time, its cells stamped with the glyph effect's exact hold there. */
|
|
128
|
+
render(options) {
|
|
129
|
+
const { segments, hold } = this.at(frameTimeOf(options)).moved(options);
|
|
130
|
+
return stampFrame(segments, hold);
|
|
131
|
+
}
|
|
132
|
+
measure(options) {
|
|
133
|
+
return Measurement.get(options, this.renderable);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Each segment's glyphs, each with its cell as the effect is handed it, and
|
|
138
|
+
* every cell in reading order. [LAW:one-source-of-truth] One walk, one count
|
|
139
|
+
* of reading order, for a rendered frame and a text alike.
|
|
140
|
+
*/
|
|
141
|
+
function placeGlyphs(segments, key) {
|
|
142
|
+
let index = 0;
|
|
143
|
+
const placed = walkGlyphs(segments).map((glyphs) => glyphs.map(({ glyph, cell }) => ({ glyph, cell: cell && { ...cell, glyph, index: index++, seed: cellSeed(key, cell.row, cell.col) } })));
|
|
144
|
+
return { placed, cells: placed.flatMap((glyphs) => glyphs.flatMap(({ cell }) => (cell === undefined ? [] : [cell]))) };
|
|
145
|
+
}
|
|
146
|
+
/** What `frame` shows for each placed glyph, held to the contract (`shows`, `standsApart`). */
|
|
147
|
+
function showGlyphs(placed, frame) {
|
|
148
|
+
const shown = placed.map((glyphs) => glyphs.map(({ glyph, cell }) => (cell === undefined ? glyph : shows(frame, cell))));
|
|
149
|
+
standsApart(placed.flat(), shown.flat());
|
|
150
|
+
return shown;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* `text` with its glyphs passed through `effect`, before any render: the
|
|
154
|
+
* glyph half of a template fragment (`../template-bindings/animation-funcs.ts`),
|
|
155
|
+
* which is spliced into a line and drawn under a theme it does not know yet,
|
|
156
|
+
* so it cannot be rendered to cells first as `Reglyphed` renders its child.
|
|
157
|
+
* The cells are the text's own lines, at its own columns; the contract is
|
|
158
|
+
* the one `Reglyphed` is held to, through the same checks, over the text's
|
|
159
|
+
* own glyphs: a neighbour spliced beside it later, a combining mark leading
|
|
160
|
+
* the next fragment say, is outside what either check can see. The text keeps
|
|
161
|
+
* its every style and span, each moved to where its glyphs now sit, and a
|
|
162
|
+
* frame that changes nothing hands the text back as it was. What it says of
|
|
163
|
+
* when it changes is the effect's, unstamped: whoever draws it at a time
|
|
164
|
+
* stamps it (`textFrameAt`).
|
|
165
|
+
*/
|
|
166
|
+
export function reglyphText(text, effect, options) {
|
|
167
|
+
const { placed, cells } = placeGlyphs([new Segment(text.plain)], options.key);
|
|
168
|
+
return Animation.map(effect({ cells, asciiOnly: options.asciiOnly }), (frame) => rewritten(text, placed[0], showGlyphs(placed, frame)[0]));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* `text` with each of its `glyphs` written as `shown` says, every span moved
|
|
172
|
+
* with the glyphs it covers. An offset inside a glyph — a span edge between
|
|
173
|
+
* a letter and its mark — lands as far into the written glyph as it can.
|
|
174
|
+
*/
|
|
175
|
+
function rewritten(text, glyphs, shown) {
|
|
176
|
+
if (shown.every((s, i) => s === glyphs[i].glyph))
|
|
177
|
+
return text;
|
|
178
|
+
// Where each glyph starts, before and after, with one more entry for the end.
|
|
179
|
+
const from = [0];
|
|
180
|
+
const to = [0];
|
|
181
|
+
glyphs.forEach(({ glyph }, i) => {
|
|
182
|
+
from.push(from[i] + glyph.length);
|
|
183
|
+
to.push(to[i] + shown[i].length);
|
|
184
|
+
});
|
|
185
|
+
const moved = (offset) => {
|
|
186
|
+
let i = 0;
|
|
187
|
+
while (i + 1 < from.length && from[i + 1] <= offset)
|
|
188
|
+
i++;
|
|
189
|
+
return i === glyphs.length ? to[i] : to[i] + Math.min(offset - from[i], shown[i].length);
|
|
190
|
+
};
|
|
191
|
+
const result = text.blankCopy(shown.join(""));
|
|
192
|
+
for (const span of text.spans)
|
|
193
|
+
result.stylize(span.style, moved(span.start), moved(span.end));
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
/** Line breaks and tabs, which `stripControlChars` leaves for a text's own lines and which no cell may hold. */
|
|
197
|
+
const BREAKS = pattern(/[\n\t]/);
|
|
198
|
+
/**
|
|
199
|
+
* What `frame` shows in `cell`, held to the contract: as many cells wide as
|
|
200
|
+
* the glyph, with no line break or control character. [LAW:single-enforcer]
|
|
201
|
+
* The one place a glyph effect's output is checked; a breach throws, naming
|
|
202
|
+
* the cell and what it was turned into.
|
|
203
|
+
*/
|
|
204
|
+
function shows(frame, cell) {
|
|
205
|
+
const text = frame(cell);
|
|
206
|
+
if (text === cell.glyph)
|
|
207
|
+
return text;
|
|
208
|
+
if (cellLen(text) !== cellLen(cell.glyph) || BREAKS.test(text) || stripControlChars(text) !== text) {
|
|
209
|
+
throw new RangeError(`a glyph effect turned ${JSON.stringify(cell.glyph)} at row ${cell.row}, column ${cell.col} into ${JSON.stringify(text)}: ` +
|
|
210
|
+
`it must be ${cellLen(cell.glyph)} cell(s) wide, with no line break or control character`);
|
|
211
|
+
}
|
|
212
|
+
return text;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Every text `frame` changed, in `shown`, stands apart from the texts beside
|
|
216
|
+
* it on its line: joined to them, it splits into as many grapheme clusters as
|
|
217
|
+
* it and they do alone, or this throws naming the cell. Each text being as
|
|
218
|
+
* wide as its glyph is `shows`'s check; this is the half no one cell can
|
|
219
|
+
* see — a trailing zero-width joiner that fuses with the next emoji, or a
|
|
220
|
+
* leading combining mark that draws on the glyph before it, is right alone
|
|
221
|
+
* and wrong beside its neighbour. Only a changed text is asked, so a child
|
|
222
|
+
* that splits a cluster across its own segments is drawn as it drew it.
|
|
223
|
+
*/
|
|
224
|
+
function standsApart(placed, shown) {
|
|
225
|
+
// A line break is no cell, and nothing joins across it.
|
|
226
|
+
const beside = (j) => (placed[j]?.cell === undefined ? "" : shown[j]);
|
|
227
|
+
shown.forEach((text, i) => {
|
|
228
|
+
const { glyph, cell } = placed[i];
|
|
229
|
+
if (cell === undefined || text === glyph)
|
|
230
|
+
return;
|
|
231
|
+
const [before, after] = [beside(i - 1), beside(i + 1)];
|
|
232
|
+
if (graphemes(before + text + after).length !== graphemes(before).length + graphemes(text).length + graphemes(after).length) {
|
|
233
|
+
throw new RangeError(`a glyph effect turned ${JSON.stringify(glyph)} at row ${cell.row}, column ${cell.col} into ${JSON.stringify(text)}, ` +
|
|
234
|
+
`which joins the cell beside it into one character`);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The glyph effect catalogue: a typewriter and a scramble, which run once and
|
|
3
|
+
* are done, each with its way out for an element leaving (`backspace`,
|
|
4
|
+
* `scrambleOut`), and a wave of case changes, rain and stars, which loop for as
|
|
5
|
+
* long as they are drawn. Each is a `GlyphEffect` for `GlyphEffected`
|
|
6
|
+
* (`./glyph-effect.ts`), built as the colour catalogue's are
|
|
7
|
+
* (`./effects.ts`): one function each over the one shape, made by name, one
|
|
8
|
+
* way, at the defaults in `GLYPH_DEFAULTS` (`GLYPH_EFFECT_CATALOGUE`).
|
|
9
|
+
*
|
|
10
|
+
* Where a colour loop glides, a glyph cannot: a cell shows one glyph or
|
|
11
|
+
* another, so every loop here moves in steps, on a `beat`, and says when its
|
|
12
|
+
* next step is. The transitions are `Transition`s by the one rule the
|
|
13
|
+
* colour transitions follow (`transition`): they move every frame until
|
|
14
|
+
* their seconds are up, and never after.
|
|
15
|
+
*
|
|
16
|
+
* [LAW:no-ambient-temporal-coupling] No effect reads a clock, and what a cell
|
|
17
|
+
* shows at `t` is worked out from `t` alone, so a draw costs the same at
|
|
18
|
+
* `t = 0` and `t = 10⁶`, and a host drawing once a second and one drawing
|
|
19
|
+
* sixty times show the same cell at every `t` they share. What is random is
|
|
20
|
+
* `hash` (`../core/noise.ts`) of where a cell is and which step it is in.
|
|
21
|
+
*/
|
|
22
|
+
import { type Timing } from "../core/easing.js";
|
|
23
|
+
import { type EffectTarget, type Transition } from "./effects.js";
|
|
24
|
+
import type { GlyphEffect } from "./glyph-effect.js";
|
|
25
|
+
/** A glyph effect that runs once, and says when it is done. */
|
|
26
|
+
export type GlyphTransition = Transition<GlyphEffect>;
|
|
27
|
+
/**
|
|
28
|
+
* A typewriter: the element's text blank until it is typed, in reading
|
|
29
|
+
* order, the last cell at `timing.seconds`. The ease shapes the pace, so
|
|
30
|
+
* `ease-out` types fast and slows to the end. The spaces between words are
|
|
31
|
+
* typed too, as a typewriter's are; the margins a renderable pads its text
|
|
32
|
+
* with are not text, so no second is spent on them.
|
|
33
|
+
*/
|
|
34
|
+
export declare function typewriter(timing: Timing): GlyphTransition;
|
|
35
|
+
/**
|
|
36
|
+
* A scramble that resolves: a cell of the element's text shows glyphs drawn
|
|
37
|
+
* at random, a new one each flicker, until it resolves to its own, in reading
|
|
38
|
+
* order, the last at `timing.seconds`. A wide glyph scrambles as two one-cell
|
|
39
|
+
* glyphs. A blank stays blank, so the words keep their shape while their
|
|
40
|
+
* letters churn.
|
|
41
|
+
*/
|
|
42
|
+
export declare function scramble(timing: Timing, z: number): GlyphTransition;
|
|
43
|
+
/**
|
|
44
|
+
* A typewriter backwards, for an element leaving: its text whole, then erased
|
|
45
|
+
* from its last cell to its first, the first at `timing.seconds`, so the
|
|
46
|
+
* element ends blank and nothing is left to vanish when it goes.
|
|
47
|
+
*/
|
|
48
|
+
export declare function backspace(timing: Timing): GlyphTransition;
|
|
49
|
+
/**
|
|
50
|
+
* A scramble that decays, for an element leaving: its text whole, then each
|
|
51
|
+
* cell, in reading order, churning through random glyphs for a while before
|
|
52
|
+
* it is blank, the last blank at `timing.seconds`. A blank stays blank, as in
|
|
53
|
+
* `scramble`, so the words keep their shape until they are gone.
|
|
54
|
+
*/
|
|
55
|
+
export declare function scrambleOut(timing: Timing, z: number): GlyphTransition;
|
|
56
|
+
/** How a case wave moves: a step, in seconds, for the band to move one column, and how many columns wide it is. */
|
|
57
|
+
export interface CaseWave {
|
|
58
|
+
readonly step: number;
|
|
59
|
+
readonly width: number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A wave of case changes: a band `width` columns wide crosses the element
|
|
63
|
+
* left to right, one column a `step`, and every cell inside it shows its
|
|
64
|
+
* glyph's case swapped. It starts again once it has left the last column,
|
|
65
|
+
* so a pass takes the element's columns and the band's, a step each. `z`
|
|
66
|
+
* sets how far into its pass the band is at `t = 0`.
|
|
67
|
+
*/
|
|
68
|
+
export declare function caseWave({ step, width }: CaseWave, z: number): GlyphEffect;
|
|
69
|
+
/** Particles over the blank cells of an element: what they look like, how many, and how they move. */
|
|
70
|
+
export interface Particles {
|
|
71
|
+
/** The glyphs a particle may be, each one cell wide. */
|
|
72
|
+
readonly glyphs: string;
|
|
73
|
+
/** What it may be on an output that draws ASCII alone, each one cell wide. */
|
|
74
|
+
readonly ascii: string;
|
|
75
|
+
/** The share of blank cells holding a particle at any step, in [0, 1]. */
|
|
76
|
+
readonly density: number;
|
|
77
|
+
/** How long a step holds, in seconds. */
|
|
78
|
+
readonly step: number;
|
|
79
|
+
/** How many rows every particle falls each step, a whole number, since a glyph moves a cell at a time; 0 holds them where they are. */
|
|
80
|
+
readonly drift: number;
|
|
81
|
+
/** How many steps a particle lasts before the field is drawn afresh; `Infinity`, never. */
|
|
82
|
+
readonly life: number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Particles on the element's blank cells: rain falling, stars coming and
|
|
86
|
+
* going. A particle is never drawn over a glyph, so it passes behind the
|
|
87
|
+
* words. Which cells hold one is `hash` of the cell's column, where it was at
|
|
88
|
+
* the step's start (its row less what the field has fallen) and which life
|
|
89
|
+
* of the field this is, so a field that falls carries each particle down a
|
|
90
|
+
* row a step, and one that holds still draws a fresh field each life.
|
|
91
|
+
*/
|
|
92
|
+
export declare function particles(spec: Particles, z: number): GlyphEffect;
|
|
93
|
+
/** The names of the glyph effects that loop. */
|
|
94
|
+
export type GlyphLoopName = "wave" | "rain" | "stars";
|
|
95
|
+
/** The names of the glyph effects that run once. */
|
|
96
|
+
export type GlyphTransitionName = "typewriter" | "scramble" | "backspace" | "scrambleOut";
|
|
97
|
+
/** Each catalogue glyph effect's defaults, as first drawn in `examples/glyph-effects`. */
|
|
98
|
+
export declare const GLYPH_DEFAULTS: {
|
|
99
|
+
readonly typewriter: {
|
|
100
|
+
readonly seconds: 2;
|
|
101
|
+
readonly ease: import("../core/easing.js").Ease;
|
|
102
|
+
};
|
|
103
|
+
readonly scramble: {
|
|
104
|
+
readonly seconds: 2.5;
|
|
105
|
+
readonly ease: import("../core/easing.js").Ease;
|
|
106
|
+
};
|
|
107
|
+
readonly backspace: {
|
|
108
|
+
readonly seconds: 1.5;
|
|
109
|
+
readonly ease: import("../core/easing.js").Ease;
|
|
110
|
+
};
|
|
111
|
+
readonly scrambleOut: {
|
|
112
|
+
readonly seconds: 2.5;
|
|
113
|
+
readonly ease: import("../core/easing.js").Ease;
|
|
114
|
+
};
|
|
115
|
+
readonly wave: {
|
|
116
|
+
readonly step: 0.06;
|
|
117
|
+
readonly width: 3;
|
|
118
|
+
};
|
|
119
|
+
readonly rain: {
|
|
120
|
+
readonly glyphs: "│╎";
|
|
121
|
+
readonly ascii: "|:";
|
|
122
|
+
readonly density: 0.06;
|
|
123
|
+
readonly step: 0.08;
|
|
124
|
+
readonly drift: 1;
|
|
125
|
+
readonly life: number;
|
|
126
|
+
};
|
|
127
|
+
readonly stars: {
|
|
128
|
+
readonly glyphs: "·+*";
|
|
129
|
+
readonly ascii: ".+*";
|
|
130
|
+
readonly density: 0.04;
|
|
131
|
+
readonly step: 1.5;
|
|
132
|
+
readonly drift: 0;
|
|
133
|
+
readonly life: 1;
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Every glyph effect in the catalogue by name, each made the one way: for an
|
|
138
|
+
* element, at `z` in the noise so two elements under one effect do not move
|
|
139
|
+
* in lockstep, at its defaults. A transition takes another timing in place
|
|
140
|
+
* of its own. [LAW:types-are-the-program] Keyed by the two name types, as
|
|
141
|
+
* `EFFECT_CATALOGUE` is, so a picker walking a section's names offers an
|
|
142
|
+
* effect added here with no change of its own.
|
|
143
|
+
*/
|
|
144
|
+
export declare const GLYPH_EFFECT_CATALOGUE: {
|
|
145
|
+
readonly loops: Readonly<Record<GlyphLoopName, (element: EffectTarget) => GlyphEffect>>;
|
|
146
|
+
readonly transitions: Readonly<Record<GlyphTransitionName, (element: EffectTarget, timing?: Timing) => GlyphTransition>>;
|
|
147
|
+
};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The glyph effect catalogue: a typewriter and a scramble, which run once and
|
|
3
|
+
* are done, each with its way out for an element leaving (`backspace`,
|
|
4
|
+
* `scrambleOut`), and a wave of case changes, rain and stars, which loop for as
|
|
5
|
+
* long as they are drawn. Each is a `GlyphEffect` for `GlyphEffected`
|
|
6
|
+
* (`./glyph-effect.ts`), built as the colour catalogue's are
|
|
7
|
+
* (`./effects.ts`): one function each over the one shape, made by name, one
|
|
8
|
+
* way, at the defaults in `GLYPH_DEFAULTS` (`GLYPH_EFFECT_CATALOGUE`).
|
|
9
|
+
*
|
|
10
|
+
* Where a colour loop glides, a glyph cannot: a cell shows one glyph or
|
|
11
|
+
* another, so every loop here moves in steps, on a `beat`, and says when its
|
|
12
|
+
* next step is. The transitions are `Transition`s by the one rule the
|
|
13
|
+
* colour transitions follow (`transition`): they move every frame until
|
|
14
|
+
* their seconds are up, and never after.
|
|
15
|
+
*
|
|
16
|
+
* [LAW:no-ambient-temporal-coupling] No effect reads a clock, and what a cell
|
|
17
|
+
* shows at `t` is worked out from `t` alone, so a draw costs the same at
|
|
18
|
+
* `t = 0` and `t = 10⁶`, and a host drawing once a second and one drawing
|
|
19
|
+
* sixty times show the same cell at every `t` they share. What is random is
|
|
20
|
+
* `hash` (`../core/noise.ts`) of where a cell is and which step it is in.
|
|
21
|
+
*/
|
|
22
|
+
import { Animation } from "../core/animation.js";
|
|
23
|
+
import { cellLen, graphemes } from "../core/cells.js";
|
|
24
|
+
import { EASES, Phase, beat, positiveSeconds, settled } from "../core/easing.js";
|
|
25
|
+
import { hash } from "../core/noise.js";
|
|
26
|
+
import { drawable, isAscii } from "../core/protocol.js";
|
|
27
|
+
import { inked } from "./effect.js";
|
|
28
|
+
import { requireZ, transition } from "./effects.js";
|
|
29
|
+
/** `glyph` as blank: as many spaces as it is cells wide. */
|
|
30
|
+
function blank(glyph) {
|
|
31
|
+
return " ".repeat(cellLen(glyph));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The text among `cells`: each row from its first inked glyph to its last,
|
|
35
|
+
* the blanks between words in it and the margins round it not, as a
|
|
36
|
+
* typewriter types and not as a renderable pads. A glyph no cell wide — a
|
|
37
|
+
* byte-order mark, a mark a child split from its letter — takes no time and
|
|
38
|
+
* is no place in it. Each cell's place, in reading order, by `index`, and
|
|
39
|
+
* how many places there are.
|
|
40
|
+
*/
|
|
41
|
+
function text(cells) {
|
|
42
|
+
const ends = new Map();
|
|
43
|
+
for (const cell of cells.filter((c) => inked(c.glyph)))
|
|
44
|
+
ends.set(cell.row, { first: ends.get(cell.row)?.first ?? cell.index, last: cell.index });
|
|
45
|
+
const place = new Map();
|
|
46
|
+
for (const cell of cells) {
|
|
47
|
+
const row = ends.get(cell.row);
|
|
48
|
+
if (row !== undefined && cell.index >= row.first && cell.index <= row.last && cellLen(cell.glyph) > 0)
|
|
49
|
+
place.set(cell.index, place.size);
|
|
50
|
+
}
|
|
51
|
+
return { length: place.size, place };
|
|
52
|
+
}
|
|
53
|
+
/** Of `n` cells, how many have arrived at eased progress `p`, an ease that overshoots held to the cells there are. */
|
|
54
|
+
function arrived(n, p) {
|
|
55
|
+
return Math.min(n, Math.max(0, Math.floor(n * p)));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Steps of `seconds`, counted from 0 on the animation's own clock: for a
|
|
59
|
+
* `frame` of each step, the animation holding it through the step and
|
|
60
|
+
* changing at its end, and reduced, `still`. [LAW:one-source-of-truth] The step and its two ends
|
|
61
|
+
* are a `beat`'s, the one timing vocabulary, so a step boundary is the moment
|
|
62
|
+
* the change shows. The beat is made once, for every frame drawn on it.
|
|
63
|
+
*/
|
|
64
|
+
function stepped(seconds) {
|
|
65
|
+
const pace = beat([seconds]);
|
|
66
|
+
return (frame, still) => Animation.of({
|
|
67
|
+
at: (t) => frame(Math.round(pace.lastChange(t) / seconds)),
|
|
68
|
+
nextChange: (t) => pace.nextChange(t),
|
|
69
|
+
lastChange: (t) => pace.lastChange(t),
|
|
70
|
+
reduced: () => Animation.constant(still),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/** Every cell its own glyph: a glyph loop with its motion taken out — the text as written, no wave, no rain. */
|
|
74
|
+
const AS_WRITTEN = (cell) => cell.glyph;
|
|
75
|
+
/**
|
|
76
|
+
* A typewriter: the element's text blank until it is typed, in reading
|
|
77
|
+
* order, the last cell at `timing.seconds`. The ease shapes the pace, so
|
|
78
|
+
* `ease-out` types fast and slows to the end. The spaces between words are
|
|
79
|
+
* typed too, as a typewriter's are; the margins a renderable pads its text
|
|
80
|
+
* with are not text, so no second is spent on them.
|
|
81
|
+
*/
|
|
82
|
+
export function typewriter(timing) {
|
|
83
|
+
const phase = Phase.once(0, timing.seconds);
|
|
84
|
+
// Settled, so the last cell is typed at the seconds whatever ease was handed.
|
|
85
|
+
const ease = settled(timing.ease);
|
|
86
|
+
return transition(timing.seconds, ({ cells }) => {
|
|
87
|
+
const { length, place } = text(cells);
|
|
88
|
+
return Animation.continuous((t) => {
|
|
89
|
+
const typed = arrived(length, ease(phase(t)));
|
|
90
|
+
// A margin has no place, and is blank already.
|
|
91
|
+
return (cell) => ((place.get(cell.index) ?? -1) < typed ? cell.glyph : blank(cell.glyph));
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** What a scrambled cell shows: one-cell glyphs, none that reads as a blank. */
|
|
96
|
+
const SCRAMBLE_GLYPHS = [..."ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789#$%&*+<=>?@"];
|
|
97
|
+
/** How long a scrambled glyph holds before the next, in seconds. */
|
|
98
|
+
const FLICKER = 1 / 15;
|
|
99
|
+
/**
|
|
100
|
+
* A scramble that resolves: a cell of the element's text shows glyphs drawn
|
|
101
|
+
* at random, a new one each flicker, until it resolves to its own, in reading
|
|
102
|
+
* order, the last at `timing.seconds`. A wide glyph scrambles as two one-cell
|
|
103
|
+
* glyphs. A blank stays blank, so the words keep their shape while their
|
|
104
|
+
* letters churn.
|
|
105
|
+
*/
|
|
106
|
+
export function scramble(timing, z) {
|
|
107
|
+
requireZ("scramble", z);
|
|
108
|
+
const phase = Phase.once(0, timing.seconds);
|
|
109
|
+
// Settled, so the last cell resolves at the seconds whatever ease was handed.
|
|
110
|
+
const ease = settled(timing.ease);
|
|
111
|
+
const flicker = stepped(FLICKER)((step) => step, 0);
|
|
112
|
+
return transition(timing.seconds, ({ cells }) => {
|
|
113
|
+
const { length, place } = text(cells);
|
|
114
|
+
return Animation.continuous((t) => {
|
|
115
|
+
const resolved = arrived(length, ease(phase(t)));
|
|
116
|
+
const step = flicker.at(t);
|
|
117
|
+
return (cell) => (!inked(cell.glyph) || (place.get(cell.index) ?? -1) < resolved ? cell.glyph : churned(cell, z, step));
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** `cell` scrambled at flicker `step`: a random one-cell glyph for each cell it is wide. */
|
|
122
|
+
function churned(cell, z, step) {
|
|
123
|
+
const pick = (i) => SCRAMBLE_GLYPHS[Math.floor(hash(cell.seed * 7919 + i, z + step * 0.618) * SCRAMBLE_GLYPHS.length)];
|
|
124
|
+
return Array.from({ length: cellLen(cell.glyph) }, (_, i) => pick(i)).join("");
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A typewriter backwards, for an element leaving: its text whole, then erased
|
|
128
|
+
* from its last cell to its first, the first at `timing.seconds`, so the
|
|
129
|
+
* element ends blank and nothing is left to vanish when it goes.
|
|
130
|
+
*/
|
|
131
|
+
export function backspace(timing) {
|
|
132
|
+
const phase = Phase.once(0, timing.seconds);
|
|
133
|
+
// Settled, so the first cell is erased at the seconds whatever ease was handed.
|
|
134
|
+
const ease = settled(timing.ease);
|
|
135
|
+
return transition(timing.seconds, ({ cells }) => {
|
|
136
|
+
const { length, place } = text(cells);
|
|
137
|
+
return Animation.continuous((t) => {
|
|
138
|
+
const kept = length - arrived(length, ease(phase(t)));
|
|
139
|
+
// A margin has no place, and is blank already.
|
|
140
|
+
return (cell) => ((place.get(cell.index) ?? -1) < kept ? cell.glyph : blank(cell.glyph));
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/** Of a `scrambleOut`'s seconds, the share each cell churns for between its own glyph and its blank. */
|
|
145
|
+
const CHURN = 0.3;
|
|
146
|
+
/**
|
|
147
|
+
* A scramble that decays, for an element leaving: its text whole, then each
|
|
148
|
+
* cell, in reading order, churning through random glyphs for a while before
|
|
149
|
+
* it is blank, the last blank at `timing.seconds`. A blank stays blank, as in
|
|
150
|
+
* `scramble`, so the words keep their shape until they are gone.
|
|
151
|
+
*/
|
|
152
|
+
export function scrambleOut(timing, z) {
|
|
153
|
+
requireZ("scrambleOut", z);
|
|
154
|
+
const phase = Phase.once(0, timing.seconds);
|
|
155
|
+
// Settled, so the last cell is blank at the seconds whatever ease was handed.
|
|
156
|
+
const ease = settled(timing.ease);
|
|
157
|
+
const flicker = stepped(FLICKER)((step) => step, 0);
|
|
158
|
+
return transition(timing.seconds, ({ cells }) => {
|
|
159
|
+
const { length, place } = text(cells);
|
|
160
|
+
return Animation.continuous((t) => {
|
|
161
|
+
const p = ease(phase(t));
|
|
162
|
+
// Two fronts in reading order, a churn's share of the seconds apart: past the first a cell churns, past the second it is blank.
|
|
163
|
+
const churning = arrived(length, p / (1 - CHURN));
|
|
164
|
+
const gone = arrived(length, (p - CHURN) / (1 - CHURN));
|
|
165
|
+
const step = flicker.at(t);
|
|
166
|
+
return (cell) => {
|
|
167
|
+
// A margin or a glyph no cell wide has no place, and neither front reaches it.
|
|
168
|
+
const at = place.get(cell.index) ?? Number.POSITIVE_INFINITY;
|
|
169
|
+
if (at < gone)
|
|
170
|
+
return blank(cell.glyph);
|
|
171
|
+
return !inked(cell.glyph) || at >= churning ? cell.glyph : churned(cell, z, step);
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* `glyph` with its case swapped, or as it is if the swap would change how
|
|
178
|
+
* wide it is (`ß` upper-cases to `SS`), since a cell never changes width.
|
|
179
|
+
*/
|
|
180
|
+
function swapCase(glyph) {
|
|
181
|
+
const swapped = [...glyph].map((ch) => (ch === ch.toUpperCase() ? ch.toLowerCase() : ch.toUpperCase())).join("");
|
|
182
|
+
return cellLen(swapped) === cellLen(glyph) ? swapped : glyph;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* A wave of case changes: a band `width` columns wide crosses the element
|
|
186
|
+
* left to right, one column a `step`, and every cell inside it shows its
|
|
187
|
+
* glyph's case swapped. It starts again once it has left the last column,
|
|
188
|
+
* so a pass takes the element's columns and the band's, a step each. `z`
|
|
189
|
+
* sets how far into its pass the band is at `t = 0`.
|
|
190
|
+
*/
|
|
191
|
+
export function caseWave({ step, width }, z) {
|
|
192
|
+
positiveSeconds("caseWave step", step);
|
|
193
|
+
if (!(Number.isInteger(width) && width > 0))
|
|
194
|
+
throw new RangeError(`caseWave width must be a positive whole number of columns, got ${width}`);
|
|
195
|
+
requireZ("caseWave", z);
|
|
196
|
+
const steps = stepped(step);
|
|
197
|
+
return ({ cells }) => {
|
|
198
|
+
const columns = cells.reduce((most, cell) => Math.max(most, cell.col + cellLen(cell.glyph)), 0);
|
|
199
|
+
const span = columns + width;
|
|
200
|
+
const start = Math.floor(hash(z, 2.3) * span);
|
|
201
|
+
return steps((s) => {
|
|
202
|
+
// The column just past the band's right edge; it enters at the left.
|
|
203
|
+
const lead = (((s + start) % span) + span) % span;
|
|
204
|
+
// A wide glyph is in the band if any of its columns is.
|
|
205
|
+
return (cell) => (cell.col < lead && cell.col + cellLen(cell.glyph) > lead - width ? swapCase(cell.glyph) : cell.glyph);
|
|
206
|
+
}, AS_WRITTEN);
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Particles on the element's blank cells: rain falling, stars coming and
|
|
211
|
+
* going. A particle is never drawn over a glyph, so it passes behind the
|
|
212
|
+
* words. Which cells hold one is `hash` of the cell's column, where it was at
|
|
213
|
+
* the step's start (its row less what the field has fallen) and which life
|
|
214
|
+
* of the field this is, so a field that falls carries each particle down a
|
|
215
|
+
* row a step, and one that holds still draws a fresh field each life.
|
|
216
|
+
*/
|
|
217
|
+
export function particles(spec, z) {
|
|
218
|
+
const oneCell = (set) => {
|
|
219
|
+
const glyphs = graphemes(set);
|
|
220
|
+
if (glyphs.length === 0 || glyphs.some((glyph) => cellLen(glyph) !== 1))
|
|
221
|
+
throw new RangeError(`particles needs one-cell glyphs, got ${JSON.stringify(set)}`);
|
|
222
|
+
return glyphs;
|
|
223
|
+
};
|
|
224
|
+
const [glyphs, ascii] = [oneCell(spec.glyphs), oneCell(spec.ascii)];
|
|
225
|
+
if (!isAscii(spec.ascii))
|
|
226
|
+
throw new RangeError(`particles ascii must be ASCII, got ${JSON.stringify(spec.ascii)}`);
|
|
227
|
+
if (!(spec.density >= 0 && spec.density <= 1))
|
|
228
|
+
throw new RangeError(`particles density must be in [0, 1], got ${spec.density}`);
|
|
229
|
+
if (!Number.isInteger(spec.drift))
|
|
230
|
+
throw new RangeError(`particles drift must be a whole number of rows, got ${spec.drift}`);
|
|
231
|
+
if (!(spec.life > 0))
|
|
232
|
+
throw new RangeError(`particles life must be a positive number of steps, got ${spec.life}`);
|
|
233
|
+
positiveSeconds("particles step", spec.step);
|
|
234
|
+
requireZ("particles", z);
|
|
235
|
+
const steps = stepped(spec.step);
|
|
236
|
+
return ({ asciiOnly }) => {
|
|
237
|
+
const drawn = drawable({ asciiOnly }, glyphs, ascii, (set) => set.join(""));
|
|
238
|
+
return steps((s) => {
|
|
239
|
+
const life = Number.isFinite(spec.life) ? Math.floor(s / spec.life) : 0;
|
|
240
|
+
const fallen = spec.drift * s;
|
|
241
|
+
return (cell) => {
|
|
242
|
+
// A particle takes a blank cell, never ink, nor a glyph no cell wide.
|
|
243
|
+
if (inked(cell.glyph) || cellLen(cell.glyph) === 0)
|
|
244
|
+
return cell.glyph;
|
|
245
|
+
const from = cell.row - fallen;
|
|
246
|
+
const field = z + 1.37 * cell.col + 0.71 * life;
|
|
247
|
+
// A particle is one cell; a wide blank keeps the rest of its width blank.
|
|
248
|
+
return hash(from, field) < spec.density ? drawn[Math.floor(hash(from, field + 3.1) * drawn.length)] + blank(cell.glyph).slice(1) : cell.glyph;
|
|
249
|
+
};
|
|
250
|
+
}, AS_WRITTEN);
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/** Each catalogue glyph effect's defaults, as first drawn in `examples/glyph-effects`. */
|
|
254
|
+
export const GLYPH_DEFAULTS = {
|
|
255
|
+
typewriter: { seconds: 2, ease: EASES.linear },
|
|
256
|
+
scramble: { seconds: 2.5, ease: EASES["ease-in"] },
|
|
257
|
+
backspace: { seconds: 1.5, ease: EASES.linear },
|
|
258
|
+
scrambleOut: { seconds: 2.5, ease: EASES["ease-out"] },
|
|
259
|
+
wave: { step: 0.06, width: 3 },
|
|
260
|
+
rain: { glyphs: "│╎", ascii: "|:", density: 0.06, step: 0.08, drift: 1, life: Number.POSITIVE_INFINITY },
|
|
261
|
+
stars: { glyphs: "·+*", ascii: ".+*", density: 0.04, step: 1.5, drift: 0, life: 1 },
|
|
262
|
+
};
|
|
263
|
+
/**
|
|
264
|
+
* Every glyph effect in the catalogue by name, each made the one way: for an
|
|
265
|
+
* element, at `z` in the noise so two elements under one effect do not move
|
|
266
|
+
* in lockstep, at its defaults. A transition takes another timing in place
|
|
267
|
+
* of its own. [LAW:types-are-the-program] Keyed by the two name types, as
|
|
268
|
+
* `EFFECT_CATALOGUE` is, so a picker walking a section's names offers an
|
|
269
|
+
* effect added here with no change of its own.
|
|
270
|
+
*/
|
|
271
|
+
export const GLYPH_EFFECT_CATALOGUE = {
|
|
272
|
+
loops: {
|
|
273
|
+
wave: (element) => caseWave(GLYPH_DEFAULTS.wave, element.z),
|
|
274
|
+
rain: (element) => particles(GLYPH_DEFAULTS.rain, element.z),
|
|
275
|
+
stars: (element) => particles(GLYPH_DEFAULTS.stars, element.z),
|
|
276
|
+
},
|
|
277
|
+
transitions: {
|
|
278
|
+
typewriter: (_element, timing = GLYPH_DEFAULTS.typewriter) => typewriter(timing),
|
|
279
|
+
scramble: (element, timing = GLYPH_DEFAULTS.scramble) => scramble(timing, element.z),
|
|
280
|
+
backspace: (_element, timing = GLYPH_DEFAULTS.backspace) => backspace(timing),
|
|
281
|
+
scrambleOut: (element, timing = GLYPH_DEFAULTS.scrambleOut) => scrambleOut(timing, element.z),
|
|
282
|
+
},
|
|
283
|
+
};
|