@promptctl/rich-js 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/core/color.d.ts +1 -0
- package/dist/core/color.js +3 -2
- package/dist/core/noise.d.ts +31 -0
- package/dist/core/noise.js +94 -0
- package/dist/core/paint.js +13 -2
- package/dist/core/pretty.d.ts +23 -2
- package/dist/core/pretty.js +54 -18
- package/dist/core/protocol.d.ts +2 -0
- package/dist/core/protocol.js +3 -2
- package/dist/core/selection.d.ts +12 -0
- package/dist/core/selection.js +14 -0
- package/dist/core/strip.d.ts +103 -72
- package/dist/core/strip.js +240 -212
- package/dist/core/style.js +9 -7
- package/dist/host/app.d.ts +68 -2
- package/dist/host/app.js +125 -15
- package/dist/host/index.d.ts +3 -1
- package/dist/host/index.js +1 -0
- package/dist/host/input.d.ts +84 -0
- package/dist/host/input.js +498 -0
- package/dist/host/terminal-host.d.ts +12 -0
- package/dist/host/terminal-host.js +2 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +6 -3
- package/dist/node/terminal-host.d.ts +7 -0
- package/dist/node/terminal-host.js +7 -0
- package/dist/renderables/effect.d.ts +23 -8
- package/dist/renderables/effect.js +48 -26
- package/dist/renderables/effects.d.ts +252 -0
- package/dist/renderables/effects.js +565 -0
- package/dist/renderables/embed.d.ts +11 -5
- package/dist/renderables/embed.js +13 -6
- package/dist/renderables/list-view.d.ts +42 -0
- package/dist/renderables/list-view.js +78 -0
- package/dist/renderables/progress.d.ts +6 -0
- package/dist/renderables/progress.js +11 -4
- package/dist/renderables/progressBar.d.ts +22 -2
- package/dist/renderables/progressBar.js +51 -7
- package/dist/renderables/rule.d.ts +2 -2
- package/dist/renderables/rule.js +12 -10
- package/dist/renderables/table.js +5 -7
- package/dist/renderables/viewport.d.ts +30 -0
- package/dist/renderables/viewport.js +59 -3
- package/dist/widgets/dropdown.js +3 -3
- package/dist/widgets/event-router.d.ts +31 -40
- package/dist/widgets/event-router.js +52 -483
- package/dist/widgets/index.d.ts +2 -1
- package/dist/widgets/types.d.ts +8 -25
- package/dist/widgets/types.js +8 -0
- package/dist/widgets/widget-app.d.ts +22 -7
- package/dist/widgets/widget-app.js +22 -12
- package/package.json +22 -18
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The effect catalogue: shimmer, pulse, sparkle and wheel, which loop for as
|
|
3
|
+
* long as they are drawn, and fade-in and dissolve-out, which run once and
|
|
4
|
+
* are done. Each is an `Effect` for `Effected` (`./effect.ts`); the defaults
|
|
5
|
+
* in `EFFECT_CURVES` are the ones signed off by eye in the effects-feel demo
|
|
6
|
+
* (`npm run effects-feel`), at 30 fps and at one frame a second.
|
|
7
|
+
*
|
|
8
|
+
* Each effect is something a first-time observer already knows from life —
|
|
9
|
+
* a slow glow, sunlight moving on water, the colours of daylight, fireflies, ink
|
|
10
|
+
* blooming in water, mist lifting — because a motion the eye recognises
|
|
11
|
+
* reads as calm and alive, where a motion it has to learn reads as a widget.
|
|
12
|
+
* What makes those motions familiar is what every curve here keeps:
|
|
13
|
+
*
|
|
14
|
+
* - Nothing jumps. Every curve is continuous in time, built from smooth
|
|
15
|
+
* noise (`core/noise.ts`) and smooth waveforms, so at one frame a second a cell
|
|
16
|
+
* moves a little and at thirty it glides.
|
|
17
|
+
* - Nothing repeats exactly. Rhythms are perturbed by slow noise, and each
|
|
18
|
+
* breath, firefly's flash and pass of light is an event of its own, with
|
|
19
|
+
* its own strength and moment, the way no breath or gust is the last one
|
|
20
|
+
* again. A weaker event is a shorter one, so its strength costs nothing
|
|
21
|
+
* in smoothness.
|
|
22
|
+
* - Neighbours move together. Variation is spatially coherent — patches,
|
|
23
|
+
* filaments, fronts — never per-cell static.
|
|
24
|
+
* - Light only ever lights, and never takes the words away. Ink and the
|
|
25
|
+
* fill under it both move toward the light and never darken, each by its
|
|
26
|
+
* own share: the share a colour can take while every cell it is drawn in
|
|
27
|
+
* still reads at its resting contrast or WCAG AA, whichever is lower. A
|
|
28
|
+
* cell with contrast to spare glows brightly; one with none barely moves.
|
|
29
|
+
* - Elements are apart. Each sits at its own `z` in the noise, so two
|
|
30
|
+
* elements under one effect do not move in lockstep.
|
|
31
|
+
*
|
|
32
|
+
* Colour moves are in OKLCH, where equal steps look equal. A curve's `ease`
|
|
33
|
+
* maps its intensity in [0, 1] — how strongly the effect acts on a cell at a
|
|
34
|
+
* moment — onto its swing, for the loops and the transitions alike.
|
|
35
|
+
*
|
|
36
|
+
* [LAW:no-ambient-temporal-coupling] No curve reads a clock; `t` is the
|
|
37
|
+
* effect's argument, so the frame owner decides when to sample.
|
|
38
|
+
*
|
|
39
|
+
* Every import below names something the package exports, but for `noise`,
|
|
40
|
+
* which the docs' effects playground page supplies itself: each of its
|
|
41
|
+
* cards runs this file's own declarations as the program a visitor edits
|
|
42
|
+
* (`examples/effects-playground/programs.ts`), on the published library.
|
|
43
|
+
*/
|
|
44
|
+
import { ColorRgba, contrastRatio } from "../core/color.js";
|
|
45
|
+
import { EASES, Phase } from "../core/easing.js";
|
|
46
|
+
import { fbm, hash, noise, smoothstep } from "../core/noise.js";
|
|
47
|
+
import { IDENTITY, Oklch } from "../core/oklch.js";
|
|
48
|
+
/** The contrast no touch takes a cell below, unless it rested below it: WCAG AA for body text. */
|
|
49
|
+
const LEGIBLE = 4.5;
|
|
50
|
+
/**
|
|
51
|
+
* The strengths a cell is drawn at: a field is snapped to one of these, and
|
|
52
|
+
* a share is checked at every one, so every strength drawn was checked. A
|
|
53
|
+
* 256-colour cube colour changes in jumps between strengths, and no check of
|
|
54
|
+
* a continuous strength between two of them could see every jump. A step is
|
|
55
|
+
* a 48th of a swing, well under a just-noticeable difference.
|
|
56
|
+
*/
|
|
57
|
+
const LEVELS = 48;
|
|
58
|
+
const STRENGTHS = Array.from({ length: LEVELS }, (_, i) => (i + 1) / LEVELS);
|
|
59
|
+
/** What 8-bit rounding of an OKLCH round trip can cost a contrast ratio. */
|
|
60
|
+
const ROUNDING = 0.01;
|
|
61
|
+
/**
|
|
62
|
+
* How much of `touch` each of `colors` (hex) can take: the largest share in
|
|
63
|
+
* [0, 1] at which every pair in `pairs` that shows it, touched at the same
|
|
64
|
+
* strength, still reads at its resting contrast or `LEGIBLE`, whichever is
|
|
65
|
+
* lower. A colour in a pair but not in `colors` takes none, and so does one
|
|
66
|
+
* the terminal supplies, whatever its hex, as in `onColors`. One of `colors`
|
|
67
|
+
* no pair writes has nothing to be measured against, and throws.
|
|
68
|
+
*
|
|
69
|
+
* Colours are settled lightest first, each against every partner: one
|
|
70
|
+
* already settled at its share, one not yet settled as it is. So the lighter
|
|
71
|
+
* colour of a cell takes the light first and its darker partner spends
|
|
72
|
+
* whatever contrast is left — pale lettering lifting off a dark fill gives
|
|
73
|
+
* the fill room to glow — and a darker colour left as it is always holds,
|
|
74
|
+
* because its lighter partner was settled against exactly that.
|
|
75
|
+
*/
|
|
76
|
+
export function shares(pairs, colors, touch) {
|
|
77
|
+
// [LAW:one-source-of-truth] A slot shows a subject colour, by hex, only
|
|
78
|
+
// where the element writes it: the reading `onColors` lights by.
|
|
79
|
+
const sides = pairs.map(({ fg, bg, terminal }) => [
|
|
80
|
+
{ color: fg, own: terminal.fg ? undefined : fg.hex },
|
|
81
|
+
{ color: bg, own: terminal.bg ? undefined : bg.hex },
|
|
82
|
+
]);
|
|
83
|
+
const rgba = new Map(sides.flat().flatMap(({ color, own }) => (own === undefined ? [] : [[own, color]])));
|
|
84
|
+
const unpaired = [...colors].filter((hex) => !rgba.has(hex));
|
|
85
|
+
if (unpaired.length > 0)
|
|
86
|
+
throw new RangeError(`shares: ${unpaired.join(", ")} written in no pair, so nothing to keep legible against`);
|
|
87
|
+
const lightness = (hex) => Oklch.fromRgba(rgba.get(hex)).l;
|
|
88
|
+
const settled = new Map();
|
|
89
|
+
for (const hex of [...colors].sort((a, b) => lightness(b) - lightness(a))) {
|
|
90
|
+
const partners = sides.flatMap(([fg, bg]) => {
|
|
91
|
+
const partner = fg.own === hex ? bg : bg.own === hex ? fg : undefined;
|
|
92
|
+
return partner === undefined ? [] : [[partner.color, partner.own === undefined ? 0 : (settled.get(partner.own) ?? 0)]];
|
|
93
|
+
});
|
|
94
|
+
const color = rgba.get(hex);
|
|
95
|
+
const holds = (share) => partners.every(([partner, theirs]) => {
|
|
96
|
+
const floor = Math.min(contrastRatio(color, partner), LEGIBLE);
|
|
97
|
+
return STRENGTHS.every((w) => contrastRatio(touchedAt(touch, color, share * w), touchedAt(touch, partner, theirs * w)) >= floor - ROUNDING);
|
|
98
|
+
});
|
|
99
|
+
let [lo, hi] = holds(1) ? [1, 1] : [0, 1];
|
|
100
|
+
for (let i = 0; i < 20 && hi - lo > 1e-3; i++) {
|
|
101
|
+
const mid = (lo + hi) / 2;
|
|
102
|
+
[lo, hi] = holds(mid) ? [mid, hi] : [lo, mid];
|
|
103
|
+
}
|
|
104
|
+
settled.set(hex, lo);
|
|
105
|
+
}
|
|
106
|
+
return settled;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* A loop as an `Effect` on a subject's own colours, by hex, wherever a cell
|
|
110
|
+
* shows them, each touched at its share of the cell's strength. A powerline
|
|
111
|
+
* strip's fill is the ground of its cells and the ink of the seam glyphs
|
|
112
|
+
* between them, so the arrow moves with the cell it points out of. A colour
|
|
113
|
+
* the terminal supplies (`EffectCell.terminal`) is not the subject's, whatever
|
|
114
|
+
* its hex, and takes no share.
|
|
115
|
+
*/
|
|
116
|
+
export function onColors(share, loop) {
|
|
117
|
+
return ({ fg, bg }, cell, t) => {
|
|
118
|
+
const w = Math.round(clamp01(loop.field(cell, t)) * LEVELS) / LEVELS;
|
|
119
|
+
const moved = (color, terminal) => touchedAt(loop.touch, color, (terminal ? 0 : (share.get(color.hex) ?? 0)) * w);
|
|
120
|
+
return { fg: moved(fg, cell.terminal.fg), bg: moved(bg, cell.terminal.bg) };
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* `color` under `touch` at strength `w`, and at strength 0 the colour itself,
|
|
125
|
+
* exactly: an OKLCH round trip can land a step off, and at 16 colours a step
|
|
126
|
+
* off is another slot.
|
|
127
|
+
*/
|
|
128
|
+
function touchedAt(touch, color, w) {
|
|
129
|
+
return w === 0 ? color : touch(color, w);
|
|
130
|
+
}
|
|
131
|
+
/** Throws, naming the field, unless `curve` has a positive, finite `seconds` and a `swing` in [0, 1]. */
|
|
132
|
+
function requireCurve(curve) {
|
|
133
|
+
if (!(Number.isFinite(curve.seconds) && curve.seconds > 0))
|
|
134
|
+
throw new RangeError(`curve: seconds must be a positive number of seconds, got ${curve.seconds}`);
|
|
135
|
+
if (!(curve.swing >= 0 && curve.swing <= 1))
|
|
136
|
+
throw new RangeError(`curve: swing must be in [0, 1], got ${curve.swing}`);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Each effect's curve as it was signed off. Every loop's period is a prime
|
|
140
|
+
* number of seconds, so no two loops come back into step within a sitting.
|
|
141
|
+
* The periods are long on purpose: at one frame a second a loop's colour
|
|
142
|
+
* moves by no more than a just-noticeable step from one frame to the next.
|
|
143
|
+
*/
|
|
144
|
+
export const EFFECT_CURVES = {
|
|
145
|
+
shimmer: { seconds: 109, ease: EASES.linear, swing: 0.9 },
|
|
146
|
+
pulse: { seconds: 43, ease: EASES.linear, swing: 0.85 },
|
|
147
|
+
sparkle: { seconds: 139, ease: EASES.linear, swing: 0.95 },
|
|
148
|
+
wheel: { seconds: 907, ease: EASES.linear, swing: 1 },
|
|
149
|
+
fade: { seconds: 20, ease: EASES["ease-in-out"], swing: 1 },
|
|
150
|
+
dissolve: { seconds: 30, ease: EASES["ease-in-out"], swing: 1 },
|
|
151
|
+
};
|
|
152
|
+
/** The lights the loops were signed off casting, warm as sunlight and fireflies are. */
|
|
153
|
+
export const EFFECT_LIGHTS = {
|
|
154
|
+
sun: new ColorRgba(255, 228, 176),
|
|
155
|
+
firefly: new ColorRgba(222, 245, 140),
|
|
156
|
+
};
|
|
157
|
+
/** How far a shimmer's light reaches either side of its centre, in columns, as signed off. */
|
|
158
|
+
export const SHIMMER_WIDTH = 24;
|
|
159
|
+
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
160
|
+
/**
|
|
161
|
+
* `w` of the way from `from` to `to` along a straight line in OKLab. Light
|
|
162
|
+
* falling on a colour, or a colour thinning into its ground, moves straight
|
|
163
|
+
* toward the other colour; `Oklch.mix` goes round the hue circle instead, so
|
|
164
|
+
* blue lit by gold would pass through cyan on the way.
|
|
165
|
+
*/
|
|
166
|
+
function blend(from, to, w) {
|
|
167
|
+
if (!(w >= 0 && w <= 1))
|
|
168
|
+
throw new RangeError(`blend: w must be in [0, 1]; got ${w}`);
|
|
169
|
+
const rad = Math.PI / 180;
|
|
170
|
+
const a = (1 - w) * from.c * Math.cos(from.h * rad) + w * to.c * Math.cos(to.h * rad);
|
|
171
|
+
const b = (1 - w) * from.c * Math.sin(from.h * rad) + w * to.c * Math.sin(to.h * rad);
|
|
172
|
+
return new Oklch((1 - w) * from.l + w * to.l, Math.hypot(a, b), Math.atan2(b, a) / rad, from.alpha);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* A colour under `glow`: the one way every light here falls. It moves `w` of
|
|
176
|
+
* the way toward the light's colour, and never below its own lightness —
|
|
177
|
+
* light can warm a colour and lift it, never darken it. A colour already
|
|
178
|
+
* lighter than the light only warms, and white, which has no warmer shade at
|
|
179
|
+
* its lightness, stays white.
|
|
180
|
+
*/
|
|
181
|
+
export function light(glow) {
|
|
182
|
+
const to = Oklch.fromRgba(glow);
|
|
183
|
+
return (color, w) => {
|
|
184
|
+
const from = Oklch.fromRgba(color);
|
|
185
|
+
return blend(from, new Oklch(Math.max(from.l, to.l), to.c, to.h, to.alpha), w).toRgba();
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** How long a swell `k` strong lasts, in turns. */
|
|
189
|
+
const lasts = (shape, k) => k * shape[shape.length - 1][0];
|
|
190
|
+
/**
|
|
191
|
+
* A swell — a breath, a firefly's glow — `k` strong, in [0, 1], `s` after it
|
|
192
|
+
* began: a raised cosine from each knot to the next, 0 before and after, zero
|
|
193
|
+
* slope at every knot. A weaker swell is a shorter one in proportion, so it
|
|
194
|
+
* climbs no more steeply than the strongest: however strong, its steepest
|
|
195
|
+
* moment is the same, and only the strongest need be checked against the
|
|
196
|
+
* step a frame may take. A swell of strength 0 is no swell at all: 0
|
|
197
|
+
* throughout, its own start included.
|
|
198
|
+
*/
|
|
199
|
+
function swell(shape, s, k) {
|
|
200
|
+
const p = s / k;
|
|
201
|
+
const to = shape.findIndex(([turn]) => p < turn);
|
|
202
|
+
if (!(p > 0) || to < 0)
|
|
203
|
+
return 0;
|
|
204
|
+
const [t0, l0] = to === 0 ? [0, 0] : shape[to - 1];
|
|
205
|
+
const [t1, l1] = shape[to];
|
|
206
|
+
return k * (l0 + ((l1 - l0) * (1 - Math.cos((Math.PI * (p - t0)) / (t1 - t0)))) / 2);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* How much of a swell's light has been given by `s`, in [0, 1]: `swell`'s own
|
|
210
|
+
* running integral over its whole, by the midpoint rule, so the two cannot
|
|
211
|
+
* disagree. Paced by it, a motion is quickest at the swell's height and still
|
|
212
|
+
* at both its ends. `k` is above 0.
|
|
213
|
+
*/
|
|
214
|
+
function given(shape, s, k) {
|
|
215
|
+
const whole = lasts(shape, k);
|
|
216
|
+
const area = (to) => {
|
|
217
|
+
let sum = 0;
|
|
218
|
+
for (let i = 0; i < 24; i++)
|
|
219
|
+
sum += swell(shape, ((i + 0.5) / 24) * to, k);
|
|
220
|
+
return sum * to;
|
|
221
|
+
};
|
|
222
|
+
return area(Math.min(Math.max(s, 0), whole)) / area(whole);
|
|
223
|
+
}
|
|
224
|
+
/** A firefly's brightest flash, in turns: it kindles quicker than it fades, and the rest of its turn is dark. */
|
|
225
|
+
const FLASH = [[0.24, 1], [0.6, 0]];
|
|
226
|
+
/**
|
|
227
|
+
* Pulse, as a gentle glow on one element of a screen: the colours handed to
|
|
228
|
+
* it, an element's own, warm into `light` and settle back, over and over, the
|
|
229
|
+
* way an indicator breathes while something is waiting. Swing: how far into
|
|
230
|
+
* the light it goes at the top of a beat, 0–1. The element glows as one —
|
|
231
|
+
* every cell it is drawn in at once — and the colours not handed to it, the
|
|
232
|
+
* rest of the screen, stay as they are, so what pulses is what was chosen.
|
|
233
|
+
* Each element keeps a time of its own, a little quicker or slower than
|
|
234
|
+
* another's by `z`, and no beat is quite the last: the rhythm wanders ahead
|
|
235
|
+
* and behind its metronome and each beat goes a little deeper or shallower
|
|
236
|
+
* than the one before. A beat starts and ends in rest, so `t = 0` draws the
|
|
237
|
+
* element untouched.
|
|
238
|
+
*/
|
|
239
|
+
export function pulse(curve, glow, z) {
|
|
240
|
+
requireCurve(curve);
|
|
241
|
+
// How much quicker or slower than `curve.seconds` this element beats.
|
|
242
|
+
const rate = 0.9 + 0.2 * hash(z, 3.1);
|
|
243
|
+
// How far, in beats, the rhythm wanders ahead and behind its metronome. Noise's
|
|
244
|
+
// slope stays within ±2.3 over its 3-beat length, so the wander never turns
|
|
245
|
+
// the beat back.
|
|
246
|
+
const WANDER = 0.1;
|
|
247
|
+
const wander = (t) => WANDER * noise(t / (3 * curve.seconds), 0.5, 0.5 + z);
|
|
248
|
+
const start = wander(0);
|
|
249
|
+
const field = (_cell, t) => {
|
|
250
|
+
const beats = (rate * t) / curve.seconds + wander(t) - start;
|
|
251
|
+
const n = Math.floor(beats);
|
|
252
|
+
// Each beat's own depth, 70% to the whole of the swing; it changes where
|
|
253
|
+
// the beat is at rest, so nothing jumps.
|
|
254
|
+
const depth = 0.7 + 0.3 * smoothstep(-0.5, 0.5, noise(n * 0.37, 2.1, z));
|
|
255
|
+
const beat = 0.5 - 0.5 * Math.cos(2 * Math.PI * (beats - n));
|
|
256
|
+
return curve.swing * curve.ease(depth * beat);
|
|
257
|
+
};
|
|
258
|
+
return { touch: light(glow), field };
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* How many columns a passing light crosses in its period: its pace.
|
|
262
|
+
* Sunlight moves at a speed, not in a time set by how long the row is,
|
|
263
|
+
* so a wider element is crossed for longer, never faster, and no cell's colour
|
|
264
|
+
* changes quicker on it from one frame to the next.
|
|
265
|
+
*/
|
|
266
|
+
const STRIDE = 80;
|
|
267
|
+
/** A soft, compact bump: 1 at `d = 0`, 0 from `|d| ≥ 1`, smooth throughout. */
|
|
268
|
+
const bump = (d) => (Math.abs(d) >= 1 ? 0 : (1 - d * d) ** 3);
|
|
269
|
+
/**
|
|
270
|
+
* Shimmer, as sunlight moving across water. Swing: how far into `light` the
|
|
271
|
+
* brightest glint goes, 0–1. A soft band of light, `width` columns either
|
|
272
|
+
* side of its centre, crosses columns 0 to `span`, entering and leaving
|
|
273
|
+
* fully off the row, `STRIDE` columns in about three quarters of a period. Its passes
|
|
274
|
+
* come the way light comes and goes on water as clouds move: while the sun
|
|
275
|
+
* is out they are bright and follow close on one another, one sometimes
|
|
276
|
+
* catching the last; under cloud they thin to faint ones with the row still
|
|
277
|
+
* for a while between, and the light is never gone for long. Each crosses at its own unhurried pace, drifting a little ahead
|
|
278
|
+
* and behind as it goes. Inside a band the light is a faint glow broken
|
|
279
|
+
* into a glitter path — bright filaments where two ripples cross — carried
|
|
280
|
+
* on a slow current and re-forming as they go, so the glints dance while
|
|
281
|
+
* the band glides. Before the first pass enters, the row is untouched.
|
|
282
|
+
*/
|
|
283
|
+
export function shimmer(curve, span, width, glow, z) {
|
|
284
|
+
requireCurve(curve);
|
|
285
|
+
const P = curve.seconds;
|
|
286
|
+
const across = span + 2 * width;
|
|
287
|
+
// Passes are events on slots of half a period. Whether slot `j` holds one,
|
|
288
|
+
// and how bright, is the sky's: slow noise over the slots, so sunny and
|
|
289
|
+
// cloudy spells each last a few periods, and a pass is likelier and
|
|
290
|
+
// brighter the clearer the sky.
|
|
291
|
+
const SLOT = P / 2;
|
|
292
|
+
const sky = (j) => smoothstep(-0.4, 0.4, noise(j / 7, 3.3, 5.5 + z));
|
|
293
|
+
// Pass `j` at `t`: how bright its band is and where its centre stands, off
|
|
294
|
+
// the row before it sets off and once it is done. It sets off somewhere in
|
|
295
|
+
// the first `SETS_OFF` of its slot and carries its centre `STRIDE` columns
|
|
296
|
+
// in `LEAST` to `LEAST + SPREAD` periods, give or take its drift, and every
|
|
297
|
+
// one is off the row `UP` slots after its own began.
|
|
298
|
+
const SETS_OFF = 0.7;
|
|
299
|
+
const LEAST = 0.51;
|
|
300
|
+
const SPREAD = 0.4;
|
|
301
|
+
const UP = SETS_OFF + ((LEAST + SPREAD) * P * across) / STRIDE / SLOT;
|
|
302
|
+
const pass = (j, t) => {
|
|
303
|
+
const clear = sky(j);
|
|
304
|
+
// Even under cloud about one slot in three holds a pass, a faint one, so a
|
|
305
|
+
// still spell lasts a period or so, the way a cloud's shadow does, and
|
|
306
|
+
// never long enough to read as the light gone out.
|
|
307
|
+
const present = hash(j, 7.5 + z) < 0.35 + 0.6 * clear ? 1 : 0;
|
|
308
|
+
const strength = present * (0.3 + 0.7 * clear * (0.6 + 0.4 * hash(j, 8.5 + z)));
|
|
309
|
+
const crossing = ((P * across) / STRIDE) * (LEAST + SPREAD * hash(j, 5.5 + z));
|
|
310
|
+
const q = clamp01((t - (j + SETS_OFF * hash(j, 6.5 + z)) * SLOT) / crossing);
|
|
311
|
+
// It drifts ahead and behind on slow noise, by nothing at either edge of
|
|
312
|
+
// the row and never enough to turn it back.
|
|
313
|
+
const centre = q * across - width + 0.06 * across * Math.sin(Math.PI * q) * noise(q, 2.7, j + z);
|
|
314
|
+
return { strength, centre };
|
|
315
|
+
};
|
|
316
|
+
// The passes on the row at one moment: a frame asks for the same moment
|
|
317
|
+
// once per cell, so they are worked out once per moment.
|
|
318
|
+
let passing = { t: Number.NaN, passes: [] };
|
|
319
|
+
// Where two passes overlap the brighter wins, so light never doubles up;
|
|
320
|
+
// the first slot is 0, so before it nothing is lit.
|
|
321
|
+
const band = (col, t) => {
|
|
322
|
+
if (passing.t !== t) {
|
|
323
|
+
const last = Math.floor(t / SLOT);
|
|
324
|
+
const passes = [];
|
|
325
|
+
for (let j = Math.max(0, Math.ceil(last - UP)); j <= last; j++)
|
|
326
|
+
passes.push(pass(j, t));
|
|
327
|
+
passing = { t, passes };
|
|
328
|
+
}
|
|
329
|
+
let lit = 0;
|
|
330
|
+
for (const { strength, centre } of passing.passes)
|
|
331
|
+
lit = Math.max(lit, strength * bump((col - centre) / width));
|
|
332
|
+
return lit;
|
|
333
|
+
};
|
|
334
|
+
// A ripple's crest: a Gaussian ridge, 1 where the field crosses zero and in
|
|
335
|
+
// (0, 1] for any noise, smooth throughout, so a crest sliding through a cell
|
|
336
|
+
// lights it smoothly.
|
|
337
|
+
const ripple = (a, b, c) => Math.exp(-((noise(a, b, c) / 0.8) ** 2));
|
|
338
|
+
const field = (cell, t) => {
|
|
339
|
+
const lit = band(cell.col, t);
|
|
340
|
+
const row = cell.row + z;
|
|
341
|
+
const current = 0.8 * noise(cell.col * 0.05, row * 0.3 + 1.3, t * 0.02);
|
|
342
|
+
const caustic = (ripple(cell.col * 0.21 + current, row * 0.9, t * 0.022) * ripple(cell.col * 0.33 + 9.1 - current, row * 0.7, t * 0.018 + 4.2)) **
|
|
343
|
+
2;
|
|
344
|
+
return curve.swing * curve.ease(lit * (0.1 + 0.6 * caustic));
|
|
345
|
+
};
|
|
346
|
+
return { touch: light(glow), field };
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Sparkle, as fireflies. Swing: how far into the firefly's `light` a glow
|
|
350
|
+
* goes at its brightest, 0–1. About one firefly to every eight columns of
|
|
351
|
+
* `span`, each keeping near a home of its own. A firefly flashes — kindles,
|
|
352
|
+
* drifts a few cells glowing, slowing as it fades — then flies on in the
|
|
353
|
+
* dark and flashes again somewhere a few cells off, the way fireflies at
|
|
354
|
+
* dusk are seen: a short lit stroke, then gone, each time a little
|
|
355
|
+
* elsewhere. Each keeps its own irregular time, now and then letting a turn
|
|
356
|
+
* pass dark, so most moments a few are lit, at different brightnesses. Its
|
|
357
|
+
* light is a soft halo several cells wide, brightest where it is, so as it
|
|
358
|
+
* drifts the glow slides between cells rather than hopping. `seconds` is a firefly's
|
|
359
|
+
* turn: its brightest flash, rise and fall, takes `FLASH` of it, and a
|
|
360
|
+
* dimmer one less.
|
|
361
|
+
*/
|
|
362
|
+
export function sparkle(curve, span, glow, z) {
|
|
363
|
+
requireCurve(curve);
|
|
364
|
+
const count = Math.max(3, Math.round(span / 8));
|
|
365
|
+
const HALO = 5;
|
|
366
|
+
// How far from home a flash may begin, and how far the brightest drifts while lit.
|
|
367
|
+
const HOP = 5;
|
|
368
|
+
const GLIDE = 5;
|
|
369
|
+
const TURN = curve.seconds;
|
|
370
|
+
const homes = Array.from({ length: count }, (_, i) => ({
|
|
371
|
+
home: (i + 0.5 + 0.7 * noise(i * 1.7 + 0.2, 0.3, 0.5 + z)) * (span / count),
|
|
372
|
+
z: i * 4.9 + 0.3 + z,
|
|
373
|
+
// Out of step with every other firefly from the first moment.
|
|
374
|
+
offset: hash(i, 0.7 + z) * TURN,
|
|
375
|
+
}));
|
|
376
|
+
// Where every firefly is and how bright at one moment: a frame asks for
|
|
377
|
+
// the same moment once per cell, so it is worked out once per moment.
|
|
378
|
+
let flown = { t: Number.NaN, flies: [] };
|
|
379
|
+
const fliesAt = (t) => {
|
|
380
|
+
if (flown.t !== t) {
|
|
381
|
+
flown = {
|
|
382
|
+
t,
|
|
383
|
+
flies: homes.map((fly) => {
|
|
384
|
+
// Flash `n` is one event: whether it comes, how bright, when in
|
|
385
|
+
// its turn and where are its own. Between flashes the firefly
|
|
386
|
+
// is dark, so where it moves to is never seen moving.
|
|
387
|
+
const u = t + fly.offset;
|
|
388
|
+
const n = Math.floor(u / TURN);
|
|
389
|
+
// A dark turn is a flash with no light: it keeps its length, so
|
|
390
|
+
// every flash is timed alike.
|
|
391
|
+
const lit = hash(n, fly.z + 1) < 0.15 ? 0 : 1;
|
|
392
|
+
const k = 0.5 + 0.5 * hash(n, fly.z + 2);
|
|
393
|
+
const at = hash(n, fly.z + 3) * (1 - lasts(FLASH, k));
|
|
394
|
+
const s = u / TURN - n - at;
|
|
395
|
+
// While it glows it drifts a few cells one way or the other, as
|
|
396
|
+
// fast as it is bright: quickest at its height, slowing as it
|
|
397
|
+
// fades — the lit stroke a firefly draws on the dusk. A brighter
|
|
398
|
+
// flash lasts longer and draws a longer stroke at the same pace.
|
|
399
|
+
return {
|
|
400
|
+
x: fly.home +
|
|
401
|
+
HOP * (2 * hash(n, fly.z + 4) - 1) +
|
|
402
|
+
GLIDE * k * (2 * hash(n, fly.z + 5) - 1) * given(FLASH, s, k) +
|
|
403
|
+
1.5 * noise(fly.z, 0.1, t * 0.035),
|
|
404
|
+
y: 0.6 * noise(fly.z, 2.2, t * 0.035),
|
|
405
|
+
glow: lit * swell(FLASH, s, k),
|
|
406
|
+
};
|
|
407
|
+
}),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
return flown.flies;
|
|
411
|
+
};
|
|
412
|
+
const field = (cell, t) => {
|
|
413
|
+
// Where two halos overlap the brighter wins, so light never doubles up.
|
|
414
|
+
let shine = 0;
|
|
415
|
+
for (const fly of fliesAt(t))
|
|
416
|
+
shine = Math.max(shine, fly.glow * bump(Math.hypot(cell.col - fly.x, 2 * (cell.row - fly.y)) / HALO));
|
|
417
|
+
return curve.swing * curve.ease(shine);
|
|
418
|
+
};
|
|
419
|
+
return { touch: light(glow), field };
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* A cell mixed toward the terminal's `ground` by how hidden it is: its fill
|
|
423
|
+
* toward the ground, and its ink toward that fill, so an element with fills
|
|
424
|
+
* of its own — a powerline strip — arrives and leaves whole, fill and words
|
|
425
|
+
* together, never as bare coloured blocks. Invisible is ink and fill equal
|
|
426
|
+
* to the ground. `swing` scales the hiding — 1 reaches fully invisible. A
|
|
427
|
+
* cell wholly shown is returned as it was.
|
|
428
|
+
*/
|
|
429
|
+
export function veiled(visibility, swing, ground) {
|
|
430
|
+
const to = Oklch.fromRgba(ground);
|
|
431
|
+
return (colors, cell, t) => {
|
|
432
|
+
const hidden = swing * (1 - clamp01(visibility(cell, t)));
|
|
433
|
+
if (hidden === 0)
|
|
434
|
+
return colors;
|
|
435
|
+
const bg = blend(Oklch.fromRgba(colors.bg), to, hidden);
|
|
436
|
+
return { fg: blend(Oklch.fromRgba(colors.fg), bg, hidden).toRgba(), bg: bg.toRgba() };
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Where each cell falls in a transition's order, in [0, 1]: smooth noise over
|
|
441
|
+
* the element, so neighbouring cells arrive or leave together, in patches
|
|
442
|
+
* with soft edges, as ink spreads or mist thins. `z` places the element in
|
|
443
|
+
* the noise. A cell's place never changes, so it is worked out once.
|
|
444
|
+
*/
|
|
445
|
+
function order(z) {
|
|
446
|
+
const places = new Map();
|
|
447
|
+
return (cell) => {
|
|
448
|
+
const at = `${cell.row}:${cell.col}`;
|
|
449
|
+
// The noise is read through slower noise, so the patches reach out in
|
|
450
|
+
// curling tendrils rather than sitting as round blots.
|
|
451
|
+
const place = places.get(at) ??
|
|
452
|
+
clamp01(0.5 + 1.1 * fbm(cell.col * 0.09 + 1.4 * noise(cell.col * 0.035, cell.row * 0.2 + 2.6, z + 0.5), cell.row * 0.45, z, 3));
|
|
453
|
+
places.set(at, place);
|
|
454
|
+
return place;
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function transition(curve, start, effect) {
|
|
458
|
+
const end = start + curve.seconds;
|
|
459
|
+
return { effect, done: (t) => t >= end };
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Of a transition's `seconds`, how long each cell's own change takes. Under
|
|
463
|
+
* 1, so the last cell to start is done by the transition's end.
|
|
464
|
+
*/
|
|
465
|
+
const OWN = 0.45;
|
|
466
|
+
/**
|
|
467
|
+
* Each cell's progress through its own change in a transition starting at
|
|
468
|
+
* `start`, in [0, 1]: a cell `place` along the order starts that share of the
|
|
469
|
+
* way through the time the others leave it, and takes `OWN` of `seconds`.
|
|
470
|
+
*/
|
|
471
|
+
function ownChange(curve, start, place) {
|
|
472
|
+
const own = curve.seconds * OWN;
|
|
473
|
+
// One phase, shifted per cell, so no cell builds its own each frame.
|
|
474
|
+
const change = Phase.once(start, own);
|
|
475
|
+
return (cell, t) => change(t - place(cell) * (curve.seconds - own));
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Fade-in starting at `start`, as ink blooming in water: patches of the
|
|
479
|
+
* element surface first and the rest follows outward from them, each cell
|
|
480
|
+
* rising smoothly over its own part of the duration. Whole at
|
|
481
|
+
* `start + seconds`.
|
|
482
|
+
*/
|
|
483
|
+
export function fadeIn(curve, start, z, ground) {
|
|
484
|
+
requireCurve(curve);
|
|
485
|
+
const risen = ownChange(curve, start, order(1.7 + z));
|
|
486
|
+
return transition(curve, start, veiled((cell, t) => curve.ease(risen(cell, t)), curve.swing, ground));
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* The rebound of a dissolve, every constant that makes it feel as it does —
|
|
490
|
+
* patches of what is left rising back before they go: `depth` (the most, as a
|
|
491
|
+
* share of whole, a cell rises again), `from` and `to` (where in its own
|
|
492
|
+
* fade, as a share of it gone, the rising begins and is done), `maskFrom` and
|
|
493
|
+
* `maskTo` (which cells rebound: those whose eddy noise is past `maskFrom`,
|
|
494
|
+
* all of them past `maskTo`) and `late` (how much likelier the later a cell
|
|
495
|
+
* goes: 0 is no more likely, 1 is nothing for the first to go).
|
|
496
|
+
*/
|
|
497
|
+
const DISSOLVE_SHAPE = { depth: 0.4, from: 0.5, to: 0.95, maskFrom: 0.35, maskTo: 0.75, late: 0.6 };
|
|
498
|
+
/**
|
|
499
|
+
* Dissolve-out starting at `start`, as mist lifting: the element thins in
|
|
500
|
+
* drifting patches, each cell fading smoothly over its own part of the
|
|
501
|
+
* duration, until nothing is left at `start + seconds`. Mist does not lift
|
|
502
|
+
* in one breath: towards the end, patches of what is left — the later a cell
|
|
503
|
+
* goes, the likelier — rise back part way, as the last of it eddies, before
|
|
504
|
+
* they thin away for good.
|
|
505
|
+
*/
|
|
506
|
+
export function dissolveOut(curve, start, z, ground) {
|
|
507
|
+
requireCurve(curve);
|
|
508
|
+
const place = order(4.2 + z);
|
|
509
|
+
const eddy = order(9.3 + z);
|
|
510
|
+
const faded = ownChange(curve, start, place);
|
|
511
|
+
const { depth, from, to, maskFrom, maskTo, late } = DISSOLVE_SHAPE;
|
|
512
|
+
return transition(curve, start, veiled((cell, t) => {
|
|
513
|
+
const gone = curve.ease(faded(cell, t));
|
|
514
|
+
const q = clamp01((gone - from) / (to - from));
|
|
515
|
+
const back = depth * smoothstep(maskFrom, maskTo, eddy(cell)) * (1 - late + late * place(cell)) * Math.sin(Math.PI * q) ** 2;
|
|
516
|
+
return 1 - gone + back;
|
|
517
|
+
}, curve.swing, ground));
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* The periods, in seconds, of the two noises a segment's stray from the
|
|
521
|
+
* wheel's shared turn rides on. Primes, as every loop's default period is,
|
|
522
|
+
* and none a multiple of another's: a session's loops never come back round
|
|
523
|
+
* to a state they have shown, within any sitting — the first return of the
|
|
524
|
+
* wheel alone is after the product of its three periods.
|
|
525
|
+
*/
|
|
526
|
+
const STRAY_PERIODS = [211, 337];
|
|
527
|
+
/** How far a segment may stray from the shared turn at swing 1, in degrees: half a turn — fully apart. */
|
|
528
|
+
const STRAY = 180;
|
|
529
|
+
/**
|
|
530
|
+
* Wheel, as the colour of daylight going round: every hue turning the whole
|
|
531
|
+
* way round once a period — far too slowly to be seen moving, so a glance
|
|
532
|
+
* sees a steady palette and an hour's absence a different one — each
|
|
533
|
+
* segment of the element at its own pace. Swing: how far the segments stray
|
|
534
|
+
* from one another, 0–1, 1 being half a turn apart. A segment is a fill of
|
|
535
|
+
* the element's own — one of `fills`, the colours it draws as a ground — and
|
|
536
|
+
* it turns at its own pace wherever it is drawn: as the ground of its cells,
|
|
537
|
+
* and as the ink of the seam glyph pointing out of it into the next, so a
|
|
538
|
+
* powerline cap keeps the colour of the cell it caps. An ink that is no fill
|
|
539
|
+
* turns with the ground it sits on, so a cell's words and fill turn
|
|
540
|
+
* together; on the terminal's ground, a run of text's ink is a segment of
|
|
541
|
+
* its own. A segment's stray is slow noise of its own, at `STRAY_PERIODS`,
|
|
542
|
+
* so no two keep step, and the row never shows one spread of hues twice.
|
|
543
|
+
* Lightness and chroma are kept, so the words read as they did; the turn
|
|
544
|
+
* only happens to `colors`, the element's own, never the terminal's.
|
|
545
|
+
*/
|
|
546
|
+
export function wheel(curve, colors, fills, z) {
|
|
547
|
+
requireCurve(curve);
|
|
548
|
+
const P = curve.seconds;
|
|
549
|
+
// A segment's seed, from the colour it is.
|
|
550
|
+
const seed = (hex) => hash(Number.parseInt(hex.slice(1), 16) * 1e-4, 17.3 + z);
|
|
551
|
+
const stray = (s, t) => noise(t / STRAY_PERIODS[0], 5.9 + 97 * s, z) + 0.5 * noise(t / STRAY_PERIODS[1], 2.3 + 89 * s, z + 7.1);
|
|
552
|
+
// Measured from where it stood at the start, as pulse's wander is, so the
|
|
553
|
+
// first frame turns nothing and the second has turned only a frame's worth.
|
|
554
|
+
const turned = (hex, t) => {
|
|
555
|
+
const s = seed(hex);
|
|
556
|
+
return (360 * t) / P + curve.swing * STRAY * ((stray(s, t) - stray(s, 0)) / 1.5);
|
|
557
|
+
};
|
|
558
|
+
return (drawn, cell, t) => {
|
|
559
|
+
// Whether a slot shows one of the element's colours: never the terminal's, whatever its hex.
|
|
560
|
+
const own = { fg: !cell.terminal.fg && colors.has(drawn.fg.hex), bg: !cell.terminal.bg && colors.has(drawn.bg.hex) };
|
|
561
|
+
// An own colour turns with its segment: itself if a fill or beside no own colour, else the one beside it.
|
|
562
|
+
const turn = (color, mine, other, theirs) => !mine ? color : Oklch.fromRgba(color).applyKey({ ...IDENTITY, hueShift: turned(fills.has(color.hex) || !theirs ? color.hex : other.hex, t) }).toRgba();
|
|
563
|
+
return { fg: turn(drawn.fg, own.fg, drawn.bg, own.bg), bg: turn(drawn.bg, own.bg, drawn.fg, own.fg) };
|
|
564
|
+
};
|
|
565
|
+
}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
|
|
25
25
|
import { Segment } from "../core/segment.js";
|
|
26
|
-
import type { Style } from "../core/style.js";
|
|
26
|
+
import type { Style, TextStyle } from "../core/style.js";
|
|
27
27
|
import { RichText } from "../core/text.js";
|
|
28
28
|
/**
|
|
29
29
|
* Caller content as text an embedding site lays out. A string is the only kind
|
|
@@ -39,9 +39,12 @@ export declare class EmbeddedText implements Renderable, Measurable {
|
|
|
39
39
|
* The text this content draws under `options`. A string reads with `base` as
|
|
40
40
|
* its style, as Rich's `render_str(style=)` gives one; text handed over
|
|
41
41
|
* keeps its own, since the caller already said how it looks — so a table's
|
|
42
|
-
* `Text` title draws without the title style, as Rich's does.
|
|
42
|
+
* `Text` title draws without the title style, as Rich's does. A highlighted
|
|
43
|
+
* string keeps only its spans: Rich's `render_str` highlights a fresh `Text`
|
|
44
|
+
* of the plain string and copies the spans onto it, so `base` is lost there,
|
|
45
|
+
* and a title Rich reads with highlighting on draws without it.
|
|
43
46
|
*/
|
|
44
|
-
text(options: RenderOptions, base?:
|
|
47
|
+
text(options: RenderOptions, base?: TextStyle): RichText;
|
|
45
48
|
render(options: RenderOptions): Iterable<Segment>;
|
|
46
49
|
/**
|
|
47
50
|
* Measured unhighlighted, as Rich's `Measurement.get` reads a string with
|
|
@@ -73,8 +76,11 @@ export declare function embed(content: unknown): Renderable & Partial<Measurable
|
|
|
73
76
|
export declare class InlineLabel {
|
|
74
77
|
private readonly _content;
|
|
75
78
|
constructor(content: string | RichText);
|
|
76
|
-
/**
|
|
77
|
-
|
|
79
|
+
/**
|
|
80
|
+
* The label drawn under `options`: one line, its own overflow kept for the
|
|
81
|
+
* caller's cut. A string reads with `base` as its style, as `EmbeddedText.text` reads one.
|
|
82
|
+
*/
|
|
83
|
+
text(options: RenderOptions, base?: TextStyle): RichText;
|
|
78
84
|
}
|
|
79
85
|
/**
|
|
80
86
|
* A label cut to `width` cells as Rich's `Text.truncate` cuts one: its plain
|
|
@@ -52,7 +52,10 @@ export class EmbeddedText {
|
|
|
52
52
|
* The text this content draws under `options`. A string reads with `base` as
|
|
53
53
|
* its style, as Rich's `render_str(style=)` gives one; text handed over
|
|
54
54
|
* keeps its own, since the caller already said how it looks — so a table's
|
|
55
|
-
* `Text` title draws without the title style, as Rich's does.
|
|
55
|
+
* `Text` title draws without the title style, as Rich's does. A highlighted
|
|
56
|
+
* string keeps only its spans: Rich's `render_str` highlights a fresh `Text`
|
|
57
|
+
* of the plain string and copies the spans onto it, so `base` is lost there,
|
|
58
|
+
* and a title Rich reads with highlighting on draws without it.
|
|
56
59
|
*/
|
|
57
60
|
text(options, base) {
|
|
58
61
|
if (typeof this._source !== "string")
|
|
@@ -61,9 +64,10 @@ export class EmbeddedText {
|
|
|
61
64
|
const reading = this._readings.get(markup) ?? readStr(this._source, markup);
|
|
62
65
|
this._readings.set(markup, reading);
|
|
63
66
|
const text = reading.copy();
|
|
64
|
-
|
|
67
|
+
const highlighter = activeHighlighter(options);
|
|
68
|
+
if (base !== undefined && highlighter === undefined)
|
|
65
69
|
text.style = base;
|
|
66
|
-
|
|
70
|
+
highlighter?.highlight(text);
|
|
67
71
|
return text;
|
|
68
72
|
}
|
|
69
73
|
render(options) {
|
|
@@ -105,9 +109,12 @@ export class InlineLabel {
|
|
|
105
109
|
constructor(content) {
|
|
106
110
|
this._content = new EmbeddedText(content);
|
|
107
111
|
}
|
|
108
|
-
/**
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
/**
|
|
113
|
+
* The label drawn under `options`: one line, its own overflow kept for the
|
|
114
|
+
* caller's cut. A string reads with `base` as its style, as `EmbeddedText.text` reads one.
|
|
115
|
+
*/
|
|
116
|
+
text(options, base) {
|
|
117
|
+
const text = this._content.text(options, base);
|
|
111
118
|
// A fragment of the line, so its own end is not drawn, as Rich's `_title` clears it.
|
|
112
119
|
text.end = "";
|
|
113
120
|
text.plain = text.plain.replaceAll("\n", " ");
|