@promptctl/rich-js 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/anchor.d.ts +5 -0
- package/dist/core/anchor.js +10 -0
- package/dist/core/color.d.ts +9 -0
- package/dist/core/color.js +23 -1
- package/dist/core/strip.d.ts +81 -29
- package/dist/core/strip.js +190 -66
- package/dist/core/style.js +1 -0
- package/dist/host/input.d.ts +5 -2
- package/dist/host/input.js +29 -10
- package/dist/index.d.ts +6 -6
- package/dist/index.js +3 -3
- package/dist/renderables/effect.d.ts +90 -17
- package/dist/renderables/effect.js +114 -43
- package/dist/renderables/effects.d.ts +93 -50
- package/dist/renderables/effects.js +215 -150
- package/dist/renderables/flexStrip.d.ts +1 -1
- package/dist/renderables/flexStrip.js +10 -9
- package/dist/renderables/progressBar.js +3 -2
- package/dist/widgets/hit.js +2 -4
- package/package.json +6 -2
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* long as they are drawn, and fade-in and dissolve-out, which run once and
|
|
4
4
|
* are done. Each is an `Effect` for `Effected` (`./effect.ts`); the defaults
|
|
5
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
|
|
6
|
+
* (`npm run effects-feel`), at 30 fps and at one frame a second, and
|
|
7
|
+
* `EFFECT_CATALOGUE` makes every one of them at those, by name, one way.
|
|
7
8
|
*
|
|
8
9
|
* Each effect is something a first-time observer already knows from life —
|
|
9
10
|
* a slow glow, sunlight moving on water, the colours of daylight, fireflies, ink
|
|
@@ -36,15 +37,17 @@
|
|
|
36
37
|
* [LAW:no-ambient-temporal-coupling] No curve reads a clock; `t` is the
|
|
37
38
|
* effect's argument, so the frame owner decides when to sample.
|
|
38
39
|
*
|
|
39
|
-
* Every import below names something the package exports, but for `noise
|
|
40
|
-
* which the docs' effects playground page supplies itself: each of its
|
|
40
|
+
* Every import below names something the package exports, but for `noise`
|
|
41
|
+
* and `Memo`, which the docs' effects playground page supplies itself: each of its
|
|
41
42
|
* cards runs this file's own declarations as the program a visitor edits
|
|
42
43
|
* (`examples/effects-playground/programs.ts`), on the published library.
|
|
43
44
|
*/
|
|
44
|
-
import { ColorRgba, contrastRatio } from "../core/color.js";
|
|
45
|
+
import { ColorDepth, ColorRgba, contrastRatio } from "../core/color.js";
|
|
45
46
|
import { EASES, Phase } from "../core/easing.js";
|
|
46
47
|
import { fbm, hash, noise, smoothstep } from "../core/noise.js";
|
|
48
|
+
import { Memo } from "../core/memo.js";
|
|
47
49
|
import { IDENTITY, Oklch } from "../core/oklch.js";
|
|
50
|
+
import { Effected } from "./effect.js";
|
|
48
51
|
/** The contrast no touch takes a cell below, unless it rested below it: WCAG AA for body text. */
|
|
49
52
|
const LEGIBLE = 4.5;
|
|
50
53
|
/**
|
|
@@ -58,13 +61,15 @@ const LEVELS = 48;
|
|
|
58
61
|
const STRENGTHS = Array.from({ length: LEVELS }, (_, i) => (i + 1) / LEVELS);
|
|
59
62
|
/** What 8-bit rounding of an OKLCH round trip can cost a contrast ratio. */
|
|
60
63
|
const ROUNDING = 0.01;
|
|
64
|
+
const SIDES = ["fg", "bg"];
|
|
61
65
|
/**
|
|
62
66
|
* How much of `touch` each of `colors` (hex) can take: the largest share in
|
|
63
|
-
* [0, 1] at which every pair
|
|
67
|
+
* [0, 1] at which every pair `drawn` shows it in, touched at the same
|
|
64
68
|
* strength, still reads at its resting contrast or `LEGIBLE`, whichever is
|
|
65
|
-
* lower
|
|
69
|
+
* lower, in the colours `Effected` writes it in at `drawn`'s depth and theme.
|
|
70
|
+
* A colour in a pair but not in `colors` takes none, and so does one
|
|
66
71
|
* the terminal supplies, whatever its hex, as in `onColors`. One of `colors`
|
|
67
|
-
* no pair writes has nothing to be measured against, and
|
|
72
|
+
* no pair writes has nothing to be measured against, and takes it all.
|
|
68
73
|
*
|
|
69
74
|
* Colours are settled lightest first, each against every partner: one
|
|
70
75
|
* already settled at its share, one not yet settled as it is. So the lighter
|
|
@@ -73,28 +78,38 @@ const ROUNDING = 0.01;
|
|
|
73
78
|
* the fill room to glow — and a darker colour left as it is always holds,
|
|
74
79
|
* because its lighter partner was settled against exactly that.
|
|
75
80
|
*/
|
|
76
|
-
export function shares(pairs, colors, touch) {
|
|
81
|
+
export function shares({ pairs, depth, theme }, colors, touch) {
|
|
77
82
|
// [LAW:one-source-of-truth] A slot shows a subject colour, by hex, only
|
|
78
83
|
// where the element writes it: the reading `onColors` lights by.
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
const own = (pair, side) => (pair.terminal[side] ? undefined : pair[side].hex);
|
|
85
|
+
const rgba = new Map(pairs.flatMap((pair) => SIDES.flatMap((side) => {
|
|
86
|
+
const hex = own(pair, side);
|
|
87
|
+
return hex === undefined ? [] : [[hex, pair[side]]];
|
|
88
|
+
})));
|
|
89
|
+
// Below truecolor every colour written is one the depth holds exactly, so only truecolor's rounding has a cost to forgive.
|
|
90
|
+
const slack = depth === ColorDepth.TRUECOLOR ? ROUNDING : 0;
|
|
91
|
+
// A colour no pair writes keeps no word legible, so nothing bounds its share:
|
|
92
|
+
// a frame drawing a lit colour only under blanks and seams, or not at all,
|
|
93
|
+
// is a frame like any other.
|
|
94
|
+
const settled = new Map([...colors].filter((hex) => !rgba.has(hex)).map((hex) => [hex, 1]));
|
|
87
95
|
const lightness = (hex) => Oklch.fromRgba(rgba.get(hex)).l;
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
96
|
+
for (const hex of [...colors].filter((hex) => rgba.has(hex)).sort((a, b) => lightness(b) - lightness(a))) {
|
|
97
|
+
// Each pair showing the colour, with the side it shows it on and the share its partner takes.
|
|
98
|
+
const facing = pairs.flatMap((pair) => {
|
|
99
|
+
const mine = SIDES.find((side) => own(pair, side) === hex);
|
|
100
|
+
if (mine === undefined)
|
|
101
|
+
return [];
|
|
102
|
+
const partner = own(pair, mine === "fg" ? "bg" : "fg");
|
|
103
|
+
return [[pair, mine, partner === undefined ? 0 : (settled.get(partner) ?? 0)]];
|
|
93
104
|
});
|
|
94
|
-
|
|
95
|
-
const holds = (share) =>
|
|
96
|
-
const floor = Math.min(contrastRatio(
|
|
97
|
-
|
|
105
|
+
// [LAW:one-source-of-truth] A pair reads in the colours `Effected` writes it in, touched at each side's share.
|
|
106
|
+
const holds = (share) => facing.every(([pair, mine, theirs]) => {
|
|
107
|
+
const floor = Math.min(contrastRatio(pair.fg, pair.bg), LEGIBLE) - slack;
|
|
108
|
+
const at = (side, w) => touchedAt(touch, pair[side], (side === mine ? share : theirs) * w);
|
|
109
|
+
return STRENGTHS.every((w) => {
|
|
110
|
+
const shown = Effected.shows(pair, { fg: at("fg", w), bg: at("bg", w) }, depth, theme);
|
|
111
|
+
return contrastRatio(shown.fg, shown.bg) >= floor;
|
|
112
|
+
});
|
|
98
113
|
});
|
|
99
114
|
let [lo, hi] = holds(1) ? [1, 1] : [0, 1];
|
|
100
115
|
for (let i = 0; i < 20 && hi - lo > 1e-3; i++) {
|
|
@@ -106,18 +121,46 @@ export function shares(pairs, colors, touch) {
|
|
|
106
121
|
return settled;
|
|
107
122
|
}
|
|
108
123
|
/**
|
|
109
|
-
* A loop as an `Effect` on
|
|
110
|
-
* shows them, each touched at its share of the cell's strength
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
124
|
+
* A loop as an `Effect` on the element's own colours, by hex, wherever a cell
|
|
125
|
+
* shows them, each touched at its share of the cell's strength: the share
|
|
126
|
+
* `shares` settles for it against the pairs the element draws its words in
|
|
127
|
+
* (`Drawn`), touched as `Effected` writes them at the frame's depth — below
|
|
128
|
+
* truecolor a touched colour lands on a colour the slot can write, and the
|
|
129
|
+
* contrast it spends is that colour's. `colors` are the ones lit, by default every colour the element
|
|
130
|
+
* sets itself. A powerline strip's fill is the ground of its cells and the
|
|
131
|
+
* ink of the seam glyphs between them, so the arrow moves with the cell it
|
|
132
|
+
* points out of. A colour the terminal supplies (`EffectCell.terminal`) is
|
|
133
|
+
* not the element's, whatever its hex, and takes no share.
|
|
134
|
+
*
|
|
135
|
+
* Settling shares is costly, so it is done once for each set of colours an
|
|
136
|
+
* element draws: make the effect once and draw it in every frame, and a frame
|
|
137
|
+
* drawing what an earlier one drew costs no settling.
|
|
138
|
+
*/
|
|
139
|
+
export function onColors(loop, colors) {
|
|
140
|
+
const settled = new WeakMap();
|
|
141
|
+
const settle = (drawn) => {
|
|
142
|
+
const lit = colors ?? drawn.colors;
|
|
143
|
+
const key = [drawn.depth, ...drawn.pairs.map(({ fg, bg, terminal, reverse }) => `${fg.hex}/${bg.hex}/${terminal.fg}/${terminal.bg}/${reverse}`), "|", ...lit].join(" ");
|
|
144
|
+
const memo = settled.get(drawn.theme) ?? new Memo();
|
|
145
|
+
settled.set(drawn.theme, memo);
|
|
146
|
+
return memo.get(key, () => shares(drawn, lit, loop.touch));
|
|
147
|
+
};
|
|
148
|
+
return atShares(loop, settle);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* A loop as an `Effect` on colours by hex, each touched at the share
|
|
152
|
+
* `share` gives it of the cell's strength in a frame drawing what `Drawn`
|
|
153
|
+
* says; a colour it gives none, and one the terminal supplies, stays as it is.
|
|
115
154
|
*/
|
|
116
|
-
export function
|
|
117
|
-
return (
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
return { fg
|
|
155
|
+
export function atShares(loop, share) {
|
|
156
|
+
return (t, drawn) => {
|
|
157
|
+
const shared = share(drawn);
|
|
158
|
+
const field = loop.field(t);
|
|
159
|
+
return ({ fg, bg }, cell) => {
|
|
160
|
+
const w = Math.round(clamp01(field(cell)) * LEVELS) / LEVELS;
|
|
161
|
+
const moved = (color, terminal) => touchedAt(loop.touch, color, (terminal ? 0 : (shared.get(color.hex) ?? 0)) * w);
|
|
162
|
+
return { fg: moved(fg, cell.terminal.fg), bg: moved(bg, cell.terminal.bg) };
|
|
163
|
+
};
|
|
121
164
|
};
|
|
122
165
|
}
|
|
123
166
|
/**
|
|
@@ -246,14 +289,15 @@ export function pulse(curve, glow, z) {
|
|
|
246
289
|
const WANDER = 0.1;
|
|
247
290
|
const wander = (t) => WANDER * noise(t / (3 * curve.seconds), 0.5, 0.5 + z);
|
|
248
291
|
const start = wander(0);
|
|
249
|
-
const field = (
|
|
292
|
+
const field = (t) => {
|
|
250
293
|
const beats = (rate * t) / curve.seconds + wander(t) - start;
|
|
251
294
|
const n = Math.floor(beats);
|
|
252
295
|
// Each beat's own depth, 70% to the whole of the swing; it changes where
|
|
253
296
|
// the beat is at rest, so nothing jumps.
|
|
254
297
|
const depth = 0.7 + 0.3 * smoothstep(-0.5, 0.5, noise(n * 0.37, 2.1, z));
|
|
255
298
|
const beat = 0.5 - 0.5 * Math.cos(2 * Math.PI * (beats - n));
|
|
256
|
-
|
|
299
|
+
const level = curve.swing * curve.ease(depth * beat);
|
|
300
|
+
return () => level;
|
|
257
301
|
};
|
|
258
302
|
return { touch: light(glow), field };
|
|
259
303
|
}
|
|
@@ -313,35 +357,31 @@ export function shimmer(curve, span, width, glow, z) {
|
|
|
313
357
|
const centre = q * across - width + 0.06 * across * Math.sin(Math.PI * q) * noise(q, 2.7, j + z);
|
|
314
358
|
return { strength, centre };
|
|
315
359
|
};
|
|
316
|
-
// The passes on the row at
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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;
|
|
360
|
+
// The passes on the row at `t`; the first slot is 0, so before it there are none.
|
|
361
|
+
const passing = (t) => {
|
|
362
|
+
const last = Math.floor(t / SLOT);
|
|
363
|
+
const passes = [];
|
|
364
|
+
for (let j = Math.max(0, Math.ceil(last - UP)); j <= last; j++)
|
|
365
|
+
passes.push(pass(j, t));
|
|
366
|
+
return passes;
|
|
333
367
|
};
|
|
334
368
|
// A ripple's crest: a Gaussian ridge, 1 where the field crosses zero and in
|
|
335
369
|
// (0, 1] for any noise, smooth throughout, so a crest sliding through a cell
|
|
336
370
|
// lights it smoothly.
|
|
337
371
|
const ripple = (a, b, c) => Math.exp(-((noise(a, b, c) / 0.8) ** 2));
|
|
338
|
-
const field = (
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
372
|
+
const field = (t) => {
|
|
373
|
+
const passes = passing(t);
|
|
374
|
+
return (cell) => {
|
|
375
|
+
// Where two passes overlap the brighter wins, so light never doubles up.
|
|
376
|
+
let lit = 0;
|
|
377
|
+
for (const { strength, centre } of passes)
|
|
378
|
+
lit = Math.max(lit, strength * bump((cell.col - centre) / width));
|
|
379
|
+
const row = cell.row + z;
|
|
380
|
+
const current = 0.8 * noise(cell.col * 0.05, row * 0.3 + 1.3, t * 0.02);
|
|
381
|
+
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)) **
|
|
382
|
+
2;
|
|
383
|
+
return curve.swing * curve.ease(lit * (0.1 + 0.6 * caustic));
|
|
384
|
+
};
|
|
345
385
|
};
|
|
346
386
|
return { touch: light(glow), field };
|
|
347
387
|
}
|
|
@@ -373,67 +413,64 @@ export function sparkle(curve, span, glow, z) {
|
|
|
373
413
|
// Out of step with every other firefly from the first moment.
|
|
374
414
|
offset: hash(i, 0.7 + z) * TURN,
|
|
375
415
|
}));
|
|
376
|
-
// Where every firefly is and how bright
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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);
|
|
416
|
+
// Where every firefly is at `t`, and how bright.
|
|
417
|
+
const fliesAt = (t) => homes.map((fly) => {
|
|
418
|
+
// Flash `n` is one event: whether it comes, how bright, when in
|
|
419
|
+
// its turn and where are its own. Between flashes the firefly
|
|
420
|
+
// is dark, so where it moves to is never seen moving.
|
|
421
|
+
const u = t + fly.offset;
|
|
422
|
+
const n = Math.floor(u / TURN);
|
|
423
|
+
// A dark turn is a flash with no light: it keeps its length, so
|
|
424
|
+
// every flash is timed alike.
|
|
425
|
+
const lit = hash(n, fly.z + 1) < 0.15 ? 0 : 1;
|
|
426
|
+
const k = 0.5 + 0.5 * hash(n, fly.z + 2);
|
|
427
|
+
const at = hash(n, fly.z + 3) * (1 - lasts(FLASH, k));
|
|
428
|
+
const s = u / TURN - n - at;
|
|
429
|
+
// While it glows it drifts a few cells one way or the other, as
|
|
430
|
+
// fast as it is bright: quickest at its height, slowing as it
|
|
431
|
+
// fades — the lit stroke a firefly draws on the dusk. A brighter
|
|
432
|
+
// flash lasts longer and draws a longer stroke at the same pace.
|
|
433
|
+
return {
|
|
434
|
+
x: fly.home +
|
|
435
|
+
HOP * (2 * hash(n, fly.z + 4) - 1) +
|
|
436
|
+
GLIDE * k * (2 * hash(n, fly.z + 5) - 1) * given(FLASH, s, k) +
|
|
437
|
+
1.5 * noise(fly.z, 0.1, t * 0.035),
|
|
438
|
+
y: 0.6 * noise(fly.z, 2.2, t * 0.035),
|
|
439
|
+
glow: lit * swell(FLASH, s, k),
|
|
440
|
+
};
|
|
441
|
+
});
|
|
442
|
+
const field = (t) => {
|
|
443
|
+
const flies = fliesAt(t);
|
|
444
|
+
return (cell) => {
|
|
445
|
+
// Where two halos overlap the brighter wins, so light never doubles up.
|
|
446
|
+
let shine = 0;
|
|
447
|
+
for (const fly of flies)
|
|
448
|
+
shine = Math.max(shine, fly.glow * bump(Math.hypot(cell.col - fly.x, 2 * (cell.row - fly.y)) / HALO));
|
|
449
|
+
return curve.swing * curve.ease(shine);
|
|
450
|
+
};
|
|
418
451
|
};
|
|
419
452
|
return { touch: light(glow), field };
|
|
420
453
|
}
|
|
421
454
|
/**
|
|
422
|
-
* A cell mixed toward the
|
|
423
|
-
*
|
|
424
|
-
* of its own — a powerline strip —
|
|
425
|
-
*
|
|
426
|
-
* to the ground. `swing` scales the
|
|
427
|
-
* cell wholly shown is returned as it was.
|
|
455
|
+
* A cell mixed toward the ground of the terminal the frame is drawn on
|
|
456
|
+
* (`Drawn.theme`) by how hidden it is: its fill toward the ground, and its ink
|
|
457
|
+
* toward that fill, so an element with fills of its own — a powerline strip —
|
|
458
|
+
* arrives and leaves whole, fill and words together, never as bare coloured
|
|
459
|
+
* blocks. Invisible is ink and fill equal to the ground. `swing` scales the
|
|
460
|
+
* hiding — 1 reaches fully invisible. A cell wholly shown is returned as it was.
|
|
428
461
|
*/
|
|
429
|
-
export function veiled(visibility, swing
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
462
|
+
export function veiled(visibility, swing) {
|
|
463
|
+
return (t, { theme }) => {
|
|
464
|
+
const shows = visibility(t);
|
|
465
|
+
// [LAW:one-source-of-truth] The ground is the terminal `Effected` draws on, never a second one handed here.
|
|
466
|
+
const ground = Oklch.fromRgba(theme.backgroundColor);
|
|
467
|
+
return (colors, cell) => {
|
|
468
|
+
const hidden = swing * (1 - clamp01(shows(cell)));
|
|
469
|
+
if (hidden === 0)
|
|
470
|
+
return colors;
|
|
471
|
+
const bg = blend(Oklch.fromRgba(colors.bg), ground, hidden);
|
|
472
|
+
return { fg: blend(Oklch.fromRgba(colors.fg), bg, hidden).toRgba(), bg: bg.toRgba() };
|
|
473
|
+
};
|
|
437
474
|
};
|
|
438
475
|
}
|
|
439
476
|
/**
|
|
@@ -454,9 +491,8 @@ function order(z) {
|
|
|
454
491
|
return place;
|
|
455
492
|
};
|
|
456
493
|
}
|
|
457
|
-
function transition(curve,
|
|
458
|
-
|
|
459
|
-
return { effect, done: (t) => t >= end };
|
|
494
|
+
function transition(curve, effect) {
|
|
495
|
+
return { effect, done: (t) => t >= curve.seconds };
|
|
460
496
|
}
|
|
461
497
|
/**
|
|
462
498
|
* Of a transition's `seconds`, how long each cell's own change takes. Under
|
|
@@ -464,26 +500,28 @@ function transition(curve, start, effect) {
|
|
|
464
500
|
*/
|
|
465
501
|
const OWN = 0.45;
|
|
466
502
|
/**
|
|
467
|
-
* Each cell's progress through its own change in a transition
|
|
468
|
-
*
|
|
469
|
-
*
|
|
503
|
+
* Each cell's progress through its own change in a transition, in [0, 1]: a
|
|
504
|
+
* cell `place` along the order starts that share of the way through the time
|
|
505
|
+
* the others leave it, and takes `OWN` of `seconds`.
|
|
470
506
|
*/
|
|
471
|
-
function ownChange(curve,
|
|
507
|
+
function ownChange(curve, place) {
|
|
472
508
|
const own = curve.seconds * OWN;
|
|
473
509
|
// One phase, shifted per cell, so no cell builds its own each frame.
|
|
474
|
-
const change = Phase.once(
|
|
475
|
-
return (
|
|
510
|
+
const change = Phase.once(0, own);
|
|
511
|
+
return (t) => (cell) => change(t - place(cell) * (curve.seconds - own));
|
|
476
512
|
}
|
|
477
513
|
/**
|
|
478
|
-
* Fade-in
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
* `start + seconds`.
|
|
514
|
+
* Fade-in, as ink blooming in water: patches of the element surface first
|
|
515
|
+
* and the rest follows outward from them, each cell rising smoothly over its
|
|
516
|
+
* own part of the duration. Whole at `seconds`.
|
|
482
517
|
*/
|
|
483
|
-
export function fadeIn(curve,
|
|
518
|
+
export function fadeIn(curve, z) {
|
|
484
519
|
requireCurve(curve);
|
|
485
|
-
const risen = ownChange(curve,
|
|
486
|
-
return transition(curve,
|
|
520
|
+
const risen = ownChange(curve, order(1.7 + z));
|
|
521
|
+
return transition(curve, veiled((t) => {
|
|
522
|
+
const at = risen(t);
|
|
523
|
+
return (cell) => curve.ease(at(cell));
|
|
524
|
+
}, curve.swing));
|
|
487
525
|
}
|
|
488
526
|
/**
|
|
489
527
|
* The rebound of a dissolve, every constant that makes it feel as it does —
|
|
@@ -496,25 +534,28 @@ export function fadeIn(curve, start, z, ground) {
|
|
|
496
534
|
*/
|
|
497
535
|
const DISSOLVE_SHAPE = { depth: 0.4, from: 0.5, to: 0.95, maskFrom: 0.35, maskTo: 0.75, late: 0.6 };
|
|
498
536
|
/**
|
|
499
|
-
* Dissolve-out
|
|
500
|
-
*
|
|
501
|
-
*
|
|
537
|
+
* Dissolve-out, as mist lifting: the element thins in drifting patches, each
|
|
538
|
+
* cell fading smoothly over its own part of the duration, until nothing is
|
|
539
|
+
* left at `seconds`. Mist does not lift
|
|
502
540
|
* in one breath: towards the end, patches of what is left — the later a cell
|
|
503
541
|
* goes, the likelier — rise back part way, as the last of it eddies, before
|
|
504
542
|
* they thin away for good.
|
|
505
543
|
*/
|
|
506
|
-
export function dissolveOut(curve,
|
|
544
|
+
export function dissolveOut(curve, z) {
|
|
507
545
|
requireCurve(curve);
|
|
508
546
|
const place = order(4.2 + z);
|
|
509
547
|
const eddy = order(9.3 + z);
|
|
510
|
-
const faded = ownChange(curve,
|
|
548
|
+
const faded = ownChange(curve, place);
|
|
511
549
|
const { depth, from, to, maskFrom, maskTo, late } = DISSOLVE_SHAPE;
|
|
512
|
-
return transition(curve,
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
550
|
+
return transition(curve, veiled((t) => {
|
|
551
|
+
const at = faded(t);
|
|
552
|
+
return (cell) => {
|
|
553
|
+
const gone = curve.ease(at(cell));
|
|
554
|
+
const q = clamp01((gone - from) / (to - from));
|
|
555
|
+
const back = depth * smoothstep(maskFrom, maskTo, eddy(cell)) * (1 - late + late * place(cell)) * Math.sin(Math.PI * q) ** 2;
|
|
556
|
+
return 1 - gone + back;
|
|
557
|
+
};
|
|
558
|
+
}, curve.swing));
|
|
518
559
|
}
|
|
519
560
|
/**
|
|
520
561
|
* The periods, in seconds, of the two noises a segment's stray from the
|
|
@@ -541,9 +582,10 @@ const STRAY = 180;
|
|
|
541
582
|
* its own. A segment's stray is slow noise of its own, at `STRAY_PERIODS`,
|
|
542
583
|
* so no two keep step, and the row never shows one spread of hues twice.
|
|
543
584
|
* Lightness and chroma are kept, so the words read as they did; the turn
|
|
544
|
-
* only happens to
|
|
585
|
+
* only happens to the element's own colours (`Drawn.colors`), never the
|
|
586
|
+
* terminal's.
|
|
545
587
|
*/
|
|
546
|
-
export function wheel(curve,
|
|
588
|
+
export function wheel(curve, z) {
|
|
547
589
|
requireCurve(curve);
|
|
548
590
|
const P = curve.seconds;
|
|
549
591
|
// A segment's seed, from the colour it is.
|
|
@@ -555,11 +597,34 @@ export function wheel(curve, colors, fills, z) {
|
|
|
555
597
|
const s = seed(hex);
|
|
556
598
|
return (360 * t) / P + curve.swing * STRAY * ((stray(s, t) - stray(s, 0)) / 1.5);
|
|
557
599
|
};
|
|
558
|
-
return (
|
|
600
|
+
return (t, { colors, fills }) => (shown, cell) => {
|
|
559
601
|
// 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(
|
|
602
|
+
const own = { fg: !cell.terminal.fg && colors.has(shown.fg.hex), bg: !cell.terminal.bg && colors.has(shown.bg.hex) };
|
|
561
603
|
// An own colour turns with its segment: itself if a fill or beside no own colour, else the one beside it.
|
|
562
604
|
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(
|
|
605
|
+
return { fg: turn(shown.fg, own.fg, shown.bg, own.bg), bg: turn(shown.bg, own.bg, shown.fg, own.fg) };
|
|
564
606
|
};
|
|
565
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* Every effect in the catalogue by its name in `EFFECT_CURVES`, each made the
|
|
610
|
+
* one way: for an element, at its signed-off curve unless handed another,
|
|
611
|
+
* with the light and the width it was signed off with. A loop comes lit on
|
|
612
|
+
* the element's own colours (`onColors`); a transition comes as itself, to
|
|
613
|
+
* be told when it is done. [LAW:types-are-the-program] Keyed by the two
|
|
614
|
+
* name types, which `EFFECT_CURVES` is held to as well: a curve signed off
|
|
615
|
+
* does not compile until it is named a loop or a transition, and then not
|
|
616
|
+
* until it has an entry here making one. A picker walking a section's names
|
|
617
|
+
* offers an effect added here with no change of its own.
|
|
618
|
+
*/
|
|
619
|
+
export const EFFECT_CATALOGUE = {
|
|
620
|
+
loops: {
|
|
621
|
+
shimmer: (element, curve = EFFECT_CURVES.shimmer) => onColors(shimmer(curve, element.span, SHIMMER_WIDTH, EFFECT_LIGHTS.sun, element.z)),
|
|
622
|
+
pulse: (element, curve = EFFECT_CURVES.pulse) => onColors(pulse(curve, EFFECT_LIGHTS.sun, element.z)),
|
|
623
|
+
sparkle: (element, curve = EFFECT_CURVES.sparkle) => onColors(sparkle(curve, element.span, EFFECT_LIGHTS.firefly, element.z)),
|
|
624
|
+
wheel: (element, curve = EFFECT_CURVES.wheel) => wheel(curve, element.z),
|
|
625
|
+
},
|
|
626
|
+
transitions: {
|
|
627
|
+
fade: (element, curve = EFFECT_CURVES.fade) => fadeIn(curve, element.z),
|
|
628
|
+
dissolve: (element, curve = EFFECT_CURVES.dissolve) => dissolveOut(curve, element.z),
|
|
629
|
+
},
|
|
630
|
+
};
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { Segment } from "../core/segment.js";
|
|
19
19
|
import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
|
|
20
|
-
import type
|
|
20
|
+
import { type Joiner, type StyledRenderable } from "../core/strip.js";
|
|
21
21
|
export type FlexAlign = "left" | "center" | "right" | "justify";
|
|
22
22
|
export interface FlexStripOptions<T extends StyledRenderable> {
|
|
23
23
|
/** Joiner used between items within a line, and at line endpoints. */
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* FlexStrip reuses it verbatim. There is no second concept of "join."
|
|
17
17
|
*/
|
|
18
18
|
import { Segment } from "../core/segment.js";
|
|
19
|
+
import { drawItem, drawJoin } from "../core/strip.js";
|
|
19
20
|
function renderToBlock(r, options) {
|
|
20
21
|
const segments = [...r.render(options)];
|
|
21
22
|
let width = 0;
|
|
@@ -63,10 +64,10 @@ export class FlexStrip {
|
|
|
63
64
|
const joiner = this.joiner;
|
|
64
65
|
// Pre-render every item and every joiner form we might need. Doing this
|
|
65
66
|
// up-front turns packing into a pure data computation over widths.
|
|
66
|
-
const itemBlocks = items.map((it) => renderToBlock(it, options));
|
|
67
|
-
const startCap = (i) => joiner ? renderToBlock(joiner
|
|
68
|
-
const endCap = (i) => joiner ? renderToBlock(joiner
|
|
69
|
-
const midJoin = (i, j) => joiner ? renderToBlock(joiner
|
|
67
|
+
const itemBlocks = items.map((it) => renderToBlock(drawItem(it), options));
|
|
68
|
+
const startCap = (i) => joiner ? renderToBlock(drawJoin(joiner, null, items[i], { index: i }), options) : EMPTY_BLOCK;
|
|
69
|
+
const endCap = (i) => joiner ? renderToBlock(drawJoin(joiner, items[i], null, { index: i + 1 }), options) : EMPTY_BLOCK;
|
|
70
|
+
const midJoin = (i, j) => joiner ? renderToBlock(drawJoin(joiner, items[i], items[j], { index: j }), options) : EMPTY_BLOCK;
|
|
70
71
|
const lines = [];
|
|
71
72
|
let line = null;
|
|
72
73
|
const gapBlock = gap > 0
|
|
@@ -177,23 +178,23 @@ export class FlexStrip {
|
|
|
177
178
|
for (let i = 0; i < items.length; i++) {
|
|
178
179
|
const itemWidth = renderToBlock(items[i], options).width;
|
|
179
180
|
const sc = this.joiner
|
|
180
|
-
? renderToBlock(this.joiner
|
|
181
|
+
? renderToBlock(drawJoin(this.joiner, null, items[i], { index: i }), options).width
|
|
181
182
|
: 0;
|
|
182
183
|
const ec = this.joiner
|
|
183
|
-
? renderToBlock(this.joiner
|
|
184
|
+
? renderToBlock(drawJoin(this.joiner, items[i], null, { index: i + 1 }), options).width
|
|
184
185
|
: 0;
|
|
185
186
|
minimum = Math.max(minimum, itemWidth + sc + ec);
|
|
186
187
|
maximum += itemWidth;
|
|
187
188
|
if (i > 0) {
|
|
188
189
|
maximum += this.gap * 2;
|
|
189
190
|
if (this.joiner) {
|
|
190
|
-
maximum += renderToBlock(this.joiner
|
|
191
|
+
maximum += renderToBlock(drawJoin(this.joiner, items[i - 1], items[i], { index: i }), options).width;
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
195
|
if (this.joiner) {
|
|
195
|
-
maximum += renderToBlock(this.joiner
|
|
196
|
-
maximum += renderToBlock(this.joiner
|
|
196
|
+
maximum += renderToBlock(drawJoin(this.joiner, null, items[0], { index: 0 }), options).width;
|
|
197
|
+
maximum += renderToBlock(drawJoin(this.joiner, items[items.length - 1], null, { index: items.length }), options).width;
|
|
197
198
|
}
|
|
198
199
|
return { minimum, maximum };
|
|
199
200
|
}
|
|
@@ -7,7 +7,7 @@ import { Segment } from "../core/segment.js";
|
|
|
7
7
|
import { Style } from "../core/style.js";
|
|
8
8
|
import { drawable, getStyle } from "../core/protocol.js";
|
|
9
9
|
import { Effected } from "./effect.js";
|
|
10
|
-
import { EFFECT_CURVES,
|
|
10
|
+
import { EFFECT_CURVES, atShares, shimmer } from "./effects.js";
|
|
11
11
|
const BAR = "━";
|
|
12
12
|
const ASCII_BAR = "-";
|
|
13
13
|
const HALF_BAR_RIGHT = "╸";
|
|
@@ -70,7 +70,8 @@ export class ProgressBar {
|
|
|
70
70
|
const ink = back.drawnColors(depth).color ?? ColorSpec.default();
|
|
71
71
|
const glow = getStyle(options, "bar.pulse").drawnColors(depth).color ?? ColorSpec.default();
|
|
72
72
|
const loop = shimmer(PULSE_CURVE, width, PULSE_REACH, glow.getTruecolor(theme, true), 0);
|
|
73
|
-
|
|
73
|
+
const whole = new Map([[ink.getTruecolor(theme, true).hex, 1]]);
|
|
74
|
+
return atShares(loop, () => whole);
|
|
74
75
|
}
|
|
75
76
|
/** Rich 9d8f9a3 `__rich_console__`: the fill is counted in half cells, and a total of zero is a bar already full. */
|
|
76
77
|
completeHalves(width) {
|
package/dist/widgets/hit.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* answer "what is under the pointer", so the answer cannot disagree with what
|
|
12
12
|
* the user sees. Z-order is paint order — whoever painted a cell last owns it.
|
|
13
13
|
*/
|
|
14
|
+
import { drawnBy } from "../core/anchor.js";
|
|
14
15
|
import { Segment } from "../core/segment.js";
|
|
15
16
|
import { isScrollable } from "../core/protocol.js";
|
|
16
17
|
import { WidgetBase } from "./widget-base.js";
|
|
@@ -72,10 +73,7 @@ export function originOf(frame, widget) {
|
|
|
72
73
|
* or through a widget nested in its output.
|
|
73
74
|
*/
|
|
74
75
|
export function drew(frame, widget, x, y) {
|
|
75
|
-
|
|
76
|
-
if (a.owner === widget)
|
|
77
|
-
return true;
|
|
78
|
-
return false;
|
|
76
|
+
return drawnBy(Segment.anchorAt(frame, x, y), widget);
|
|
79
77
|
}
|
|
80
78
|
/**
|
|
81
79
|
* Where `widget`'s footprint sits in `frame`: the cell its row 0, column 0
|