@promptctl/rich-js 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/THIRD-PARTY-NOTICES +42 -0
- package/dist/core/animation.d.ts +36 -17
- package/dist/core/animation.js +61 -37
- package/dist/core/ansi.d.ts +15 -8
- package/dist/core/ansi.js +106 -28
- package/dist/core/bidi-data.d.ts +14 -0
- package/dist/core/bidi-data.js +385 -0
- package/dist/core/bidi.d.ts +89 -0
- package/dist/core/bidi.js +615 -0
- package/dist/core/box.d.ts +9 -1
- package/dist/core/box.js +11 -0
- package/dist/core/cell-widths.d.ts +3 -2
- package/dist/core/cell-widths.js +93 -93
- package/dist/core/cells.d.ts +20 -0
- package/dist/core/cells.js +93 -5
- package/dist/core/color.d.ts +6 -7
- package/dist/core/color.js +11 -13
- package/dist/core/console.d.ts +18 -1
- package/dist/core/console.js +24 -5
- package/dist/core/control-codes.d.ts +61 -0
- package/dist/core/control-codes.js +157 -0
- package/dist/core/env.d.ts +6 -0
- package/dist/core/env.js +9 -1
- package/dist/core/export-html.js +4 -3
- package/dist/core/export-lines.d.ts +2 -0
- package/dist/core/export-lines.js +7 -3
- package/dist/core/export-svg.js +6 -5
- package/dist/core/json.js +9 -2
- package/dist/core/markup.js +4 -3
- package/dist/core/orient.d.ts +68 -0
- package/dist/core/orient.js +224 -0
- package/dist/core/osc8.d.ts +12 -5
- package/dist/core/osc8.js +14 -7
- package/dist/core/pretty.js +22 -18
- package/dist/core/protocol.d.ts +27 -6
- package/dist/core/protocol.js +14 -6
- package/dist/core/render.d.ts +8 -3
- package/dist/core/render.js +18 -5
- package/dist/core/seal.d.ts +26 -0
- package/dist/core/seal.js +75 -0
- package/dist/core/segment.d.ts +9 -1
- package/dist/core/segment.js +11 -1
- package/dist/core/style.d.ts +29 -1
- package/dist/core/style.js +41 -5
- package/dist/core/text-order.d.ts +51 -0
- package/dist/core/text-order.js +50 -0
- package/dist/core/text.d.ts +10 -0
- package/dist/core/text.js +133 -87
- package/dist/core/timeline.js +6 -6
- package/dist/core/view.d.ts +1 -1
- package/dist/core/view.js +5 -4
- package/dist/core/wrap.d.ts +24 -6
- package/dist/core/wrap.js +82 -16
- package/dist/host/app.d.ts +44 -3
- package/dist/host/app.js +111 -6
- package/dist/host/index.d.ts +3 -2
- package/dist/host/index.js +1 -0
- package/dist/host/input.d.ts +35 -2
- package/dist/host/input.js +88 -4
- package/dist/host/terminal-colors.d.ts +18 -0
- package/dist/host/terminal-colors.js +46 -0
- package/dist/host/terminal-host.d.ts +2 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +2 -2
- package/dist/renderables/change.d.ts +1 -3
- package/dist/renderables/change.js +4 -6
- package/dist/renderables/effect.d.ts +60 -59
- package/dist/renderables/effect.js +91 -81
- package/dist/renderables/effects.d.ts +37 -8
- package/dist/renderables/effects.js +185 -52
- package/dist/renderables/frames.d.ts +1 -10
- package/dist/renderables/frames.js +1 -16
- package/dist/renderables/glyph-effect.d.ts +12 -13
- package/dist/renderables/glyph-effect.js +34 -32
- package/dist/renderables/glyph-effects.d.ts +27 -8
- package/dist/renderables/glyph-effects.js +75 -7
- package/dist/renderables/lifecycle.d.ts +5 -7
- package/dist/renderables/lifecycle.js +6 -7
- package/dist/renderables/markdown.js +4 -2
- package/dist/renderables/panel.js +6 -4
- package/dist/renderables/progressBar.d.ts +3 -3
- package/dist/renderables/progressBar.js +16 -13
- package/dist/renderables/syntax.js +7 -2
- package/dist/renderables/table.js +2 -1
- package/dist/template-bindings/animation-funcs.d.ts +49 -18
- package/dist/template-bindings/animation-funcs.js +115 -32
- package/dist/template-bindings/index.d.ts +7 -5
- package/dist/template-bindings/index.js +8 -5
- package/dist/template-bindings/palette-funcs.d.ts +1 -1
- package/dist/template-bindings/palette-funcs.js +1 -1
- package/dist/themes/buildPalette.d.ts +3 -2
- package/dist/themes/buildPalette.js +2 -2
- package/dist/themes/data/index.js +1 -4
- package/dist/themes/data/textual-ansi.js +0 -143
- package/dist/themes/ramp.d.ts +1 -1
- package/dist/themes/ramp.js +1 -1
- package/dist/themes/registry.d.ts +6 -8
- package/dist/themes/registry.js +2 -4
- package/dist/themes/terminalThemes.d.ts +4 -11
- package/dist/themes/terminalThemes.js +31 -47
- package/dist/widgets/button.d.ts +1 -1
- package/dist/widgets/button.js +6 -6
- package/dist/widgets/checkbox.js +4 -4
- package/dist/widgets/dropdown.d.ts +1 -1
- package/dist/widgets/dropdown.js +7 -5
- package/dist/widgets/themed-widget.js +3 -3
- package/dist/widgets/toggle.d.ts +1 -1
- package/dist/widgets/toggle.js +5 -5
- package/package.json +5 -4
|
@@ -87,7 +87,7 @@ const SIDES = ["fg", "bg"];
|
|
|
87
87
|
* How much of `touch` each of `colors` (hex) can take: the largest share in
|
|
88
88
|
* [0, 1] at which every pair `drawn` shows it in, touched at the same
|
|
89
89
|
* strength, still reads at its resting contrast or `LEGIBLE`, whichever is
|
|
90
|
-
* lower, in the colours `Effected` writes it in at `drawn`'s depth and
|
|
90
|
+
* lower, in the colours `Effected` writes it in at `drawn`'s depth and terminal.
|
|
91
91
|
* A colour in a pair but not in `colors` takes none, and so does one
|
|
92
92
|
* the terminal supplies, whatever its hex, as in `onColors`. One of `colors`
|
|
93
93
|
* no pair writes has nothing to be measured against, and takes it all.
|
|
@@ -99,7 +99,7 @@ const SIDES = ["fg", "bg"];
|
|
|
99
99
|
* the fill room to glow — and a darker colour left as it is always holds,
|
|
100
100
|
* because its lighter partner was settled against exactly that.
|
|
101
101
|
*/
|
|
102
|
-
export function shares({ pairs, depth,
|
|
102
|
+
export function shares({ pairs, depth, terminal }, colors, touch) {
|
|
103
103
|
// [LAW:one-source-of-truth] A slot shows a subject colour, by hex, only
|
|
104
104
|
// where the element writes it: the reading `onColors` lights by.
|
|
105
105
|
const own = (pair, side) => (pair.terminal[side] ? undefined : pair[side].hex);
|
|
@@ -126,7 +126,7 @@ export function shares({ pairs, depth, theme }, colors, touch) {
|
|
|
126
126
|
const floor = floorOf(pair, depth);
|
|
127
127
|
const at = (side, w) => touchedAt(touch, pair[side], (side === mine ? share : theirs) * w);
|
|
128
128
|
return STRENGTHS.every((w) => {
|
|
129
|
-
const shown = Effected.shows(pair, { fg: at("fg", w), bg: at("bg", w) }, depth,
|
|
129
|
+
const shown = Effected.shows(pair, { fg: at("fg", w), bg: at("bg", w) }, depth, terminal);
|
|
130
130
|
return contrastRatio(shown.fg, shown.bg) >= floor;
|
|
131
131
|
});
|
|
132
132
|
});
|
|
@@ -146,10 +146,10 @@ export function shares({ pairs, depth, theme }, colors, touch) {
|
|
|
146
146
|
* (`Drawn`), touched as `Effected` writes them at the frame's depth — at 256
|
|
147
147
|
* colours a touched colour lands on a colour the slot can write, and the
|
|
148
148
|
* contrast it spends is that colour's; on a sixteen-colour terminal nothing
|
|
149
|
-
* moves (`
|
|
149
|
+
* moves (`stillWhereNothingMoves`). `colors` are the ones lit, by default every colour the element
|
|
150
150
|
* sets itself. A powerline strip's fill is the ground of its cells and the
|
|
151
151
|
* ink of the seam glyphs between them, so the arrow moves with the cell it
|
|
152
|
-
* points out of. A colour the terminal supplies (`
|
|
152
|
+
* points out of. A colour the terminal supplies (`Pair.terminal`) is
|
|
153
153
|
* not the element's, whatever its hex, and takes no share.
|
|
154
154
|
*
|
|
155
155
|
* Settling shares is costly, so it is done once for each set of colours an
|
|
@@ -158,12 +158,13 @@ export function shares({ pairs, depth, theme }, colors, touch) {
|
|
|
158
158
|
*/
|
|
159
159
|
export function onColors(loop, colors) {
|
|
160
160
|
const settled = new WeakMemo();
|
|
161
|
+
const litIn = (drawn) => colors ?? drawn.colors;
|
|
161
162
|
const settle = (drawn) => {
|
|
162
|
-
const lit =
|
|
163
|
+
const lit = litIn(drawn);
|
|
163
164
|
const key = [drawnKey(drawn), "|", ...lit].join(" ");
|
|
164
|
-
return settled.get(drawn.
|
|
165
|
+
return settled.get(drawn.terminal, () => new Memo()).get(key, () => shares(drawn, lit, loop.touch));
|
|
165
166
|
};
|
|
166
|
-
return
|
|
167
|
+
return stillWhereNothingMoves(SIDES, litIn, atShares(loop, settle));
|
|
167
168
|
}
|
|
168
169
|
/** What a frame draws, as a key: its depth and its pairs, which its `colors` and `fills` are read off. */
|
|
169
170
|
function drawnKey({ depth, pairs }) {
|
|
@@ -198,7 +199,7 @@ function pairKey({ fg, bg, terminal, reverse }) {
|
|
|
198
199
|
* body. The element is where its frame lists
|
|
199
200
|
* cells, as the fireflies read it, so a padded or confined element has the
|
|
200
201
|
* ramp across what it shows. On a sixteen-colour terminal nothing moves
|
|
201
|
-
* (`
|
|
202
|
+
* (`stillWhereNothingMoves`): a gradient there is jumps between slots, and the cells
|
|
202
203
|
* hold as drawn.
|
|
203
204
|
*/
|
|
204
205
|
export function gradient(ramp, colors) {
|
|
@@ -209,16 +210,16 @@ export function gradient(ramp, colors) {
|
|
|
209
210
|
// [LAW:one-source-of-truth] A colour named as a style spells it is lit
|
|
210
211
|
// wherever a cell shows it, so it is resolved as `Effected` resolves a
|
|
211
212
|
// cell's ink: drawn at the frame's depth, then read on its terminal.
|
|
212
|
-
const shown = (spec, { depth,
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
const shown = (spec, { depth, terminal }) => (Style.fromColor(spec).drawnColors(depth).color ?? ColorSpec.default()).getTruecolor(terminal, true).hex;
|
|
214
|
+
const litIn = (drawn) => (named === undefined ? drawn.colors : new Set(named.map((spec) => shown(spec, drawn))));
|
|
215
|
+
return stillWhereNothingMoves(["fg"], litIn, (drawn, lit) => {
|
|
215
216
|
const inkOn = legible(drawn);
|
|
216
217
|
const [left, right] = extent(drawn.cells().map((cell) => cell.col));
|
|
217
218
|
return Animation.map(ramp, (now) => {
|
|
218
219
|
const first = now.stops[0].at;
|
|
219
220
|
const span = now.stops[now.stops.length - 1].at - first;
|
|
220
221
|
const along = (col) => now.at(first + (span * (col - left + 0.5)) / (right - left + 1));
|
|
221
|
-
return (look,
|
|
222
|
+
return (look, { col, pair }) => (pair.terminal.fg || !lit.has(pair.fg.hex) ? look : { ...look, fg: inkOn(along(col), pair) });
|
|
222
223
|
});
|
|
223
224
|
});
|
|
224
225
|
}
|
|
@@ -245,17 +246,20 @@ export function atShares(loop, share) {
|
|
|
245
246
|
const field = loop.field(t, drawn);
|
|
246
247
|
return (look, cell) => {
|
|
247
248
|
const w = Math.round(clamp01(field(cell)) * LEVELS) / LEVELS;
|
|
248
|
-
|
|
249
|
-
|
|
249
|
+
// [LAW:one-source-of-truth] A share belongs to the colour the element drew; the touch falls on what is there now.
|
|
250
|
+
const moved = (side) => touchedAt(loop.touch, look[side], (cell.pair.terminal[side] ? 0 : (shared.get(cell.pair[side].hex) ?? 0)) * w);
|
|
251
|
+
return { ...look, fg: moved("fg"), bg: moved("bg") };
|
|
250
252
|
};
|
|
251
253
|
});
|
|
252
254
|
};
|
|
253
255
|
}
|
|
254
256
|
/**
|
|
255
|
-
* `loop` as drawn at the frame's depth
|
|
257
|
+
* `loop` as drawn at the frame's depth and on the colours it moves on `sides`
|
|
258
|
+
* (`litIn`): where the depth writes only the
|
|
256
259
|
* sixteen ANSI slots, or no colour, a move that shows at all is a jump to
|
|
257
260
|
* another slot, and a light on an element never jumps — so there it
|
|
258
|
-
* holds every cell still,
|
|
261
|
+
* holds every cell still, as it does where no cell that shows anything has
|
|
262
|
+
* one of those colours of its own there, and anywhere else it moves. [LAW:single-enforcer]
|
|
259
263
|
* `onColors`, `wheel` and `gradient` draw through this, so every loop in the catalogue
|
|
260
264
|
* meets the sixteen-colour terminal here. `atShares` does not: a progress
|
|
261
265
|
* bar's pulse is the news that work is going on, and flashes a slot as
|
|
@@ -263,8 +267,12 @@ export function atShares(loop, share) {
|
|
|
263
267
|
* A frame held still never changes, and says so, so a host drawing the
|
|
264
268
|
* loop on such a terminal draws it once.
|
|
265
269
|
*/
|
|
266
|
-
function
|
|
267
|
-
return (drawn) =>
|
|
270
|
+
function stillWhereNothingMoves(sides, litIn, loop) {
|
|
271
|
+
return (drawn) => {
|
|
272
|
+
const lit = litIn(drawn);
|
|
273
|
+
const shows = ({ pair }) => sides.some((side) => !pair.terminal[side] && lit.has(pair[side].hex));
|
|
274
|
+
return onlySlots(drawn.depth) || !drawn.cells().some(shows) ? Animation.constant((look) => look) : loop(drawn, lit);
|
|
275
|
+
};
|
|
268
276
|
}
|
|
269
277
|
/** Whether `depth` writes nothing between the slots it has: the sixteen, at either sixteen-colour depth, or none. */
|
|
270
278
|
function onlySlots(depth) {
|
|
@@ -618,7 +626,7 @@ export function sparkle(curve, glow, z) {
|
|
|
618
626
|
}
|
|
619
627
|
/**
|
|
620
628
|
* A cell mixed toward the ground of the terminal the frame is drawn on
|
|
621
|
-
* (`Drawn.
|
|
629
|
+
* (`Drawn.terminal`) by how hidden it is: its fill toward the ground, and its ink
|
|
622
630
|
* toward that fill, so an element with fills of its own — a powerline strip —
|
|
623
631
|
* arrives and leaves whole, fill and words together, never as bare coloured
|
|
624
632
|
* blocks. Invisible is ink and fill equal to the ground. `swing` scales the
|
|
@@ -627,7 +635,7 @@ export function sparkle(curve, glow, z) {
|
|
|
627
635
|
export function veiled(visibility, swing) {
|
|
628
636
|
return (drawn) => {
|
|
629
637
|
// [LAW:one-source-of-truth] The ground is the terminal `Effected` draws on, never a second one handed here.
|
|
630
|
-
const ground = Oklch.fromRgba(drawn.
|
|
638
|
+
const ground = Oklch.fromRgba(drawn.terminal.backgroundColor);
|
|
631
639
|
return Animation.continuous((t) => {
|
|
632
640
|
const shows = visibility(t, drawn);
|
|
633
641
|
return (look, cell) => {
|
|
@@ -953,7 +961,7 @@ function segmentsOf(drawn) {
|
|
|
953
961
|
* is drawn in at its floor (`floorOf`) — its resting contrast or WCAG AA,
|
|
954
962
|
* whichever is lower — as `shares` keeps the light's (`turnedBounds`). The
|
|
955
963
|
* turn only happens to the element's own colours (`Drawn.colors`), never the
|
|
956
|
-
* terminal's. On a sixteen-colour terminal nothing turns (`
|
|
964
|
+
* terminal's. On a sixteen-colour terminal nothing turns (`stillWhereNothingMoves`).
|
|
957
965
|
*/
|
|
958
966
|
export function wheel(curve, z) {
|
|
959
967
|
requireCurve(curve);
|
|
@@ -972,23 +980,23 @@ export function wheel(curve, z) {
|
|
|
972
980
|
const turned = (place, t) => (360 * t) / P + curve.swing * STRAY * ((stray(place, t) - stray(place, 0)) / 1.5);
|
|
973
981
|
// A turned colour's bounds rest on what a frame draws, never on `t`: settled once for each.
|
|
974
982
|
const settled = new WeakMemo();
|
|
975
|
-
return
|
|
976
|
-
const { colors, fills, depth,
|
|
983
|
+
return stillWhereNothingMoves(SIDES, ({ colors }) => colors, (drawn) => {
|
|
984
|
+
const { colors, fills, depth, terminal } = drawn;
|
|
977
985
|
// Whether a slot shows one of the element's colours: never the terminal's, whatever its hex.
|
|
978
|
-
const own = (
|
|
986
|
+
const own = (supplied, color) => !supplied && colors.has(color.hex);
|
|
979
987
|
// The colour an own colour is bounded with, under which its bounds are
|
|
980
988
|
// kept: itself if a fill or beside no own colour, else the ground beside
|
|
981
989
|
// it. Where it turns is `segmentsOf`'s, not this.
|
|
982
990
|
const boundWith = (color, other, theirs) => (fills.has(color.hex) || !theirs ? color.hex : other.hex);
|
|
983
991
|
// The bounds a colour turns within, by the colour it is bounded with: one bound for every place a frame writes it at.
|
|
984
992
|
const keyOf = (color, bound) => `${color.hex} ${bound}`;
|
|
985
|
-
const bounds = settled.get(
|
|
993
|
+
const bounds = settled.get(terminal, () => new Memo()).get(drawnKey(drawn), () => turnedBounds(drawn.pairs, depth, (pair, side) => {
|
|
986
994
|
const other = side === "fg" ? "bg" : "fg";
|
|
987
995
|
return own(pair.terminal[side], pair[side]) ? keyOf(pair[side], boundWith(pair[side], pair[other], own(pair.terminal[other], pair[other]))) : undefined;
|
|
988
996
|
}));
|
|
989
997
|
// 256 colours writes a turned colour as one it has an index for; truecolor
|
|
990
998
|
// as one of the two either side of its luminance.
|
|
991
|
-
const indexed = measuredIndexed(depth,
|
|
999
|
+
const indexed = measuredIndexed(depth, terminal);
|
|
992
1000
|
// Where each cell's colours turn, read off the frame's listing once.
|
|
993
1001
|
const placeOf = segmentsOf(drawn);
|
|
994
1002
|
return Animation.continuous((t) => {
|
|
@@ -997,20 +1005,26 @@ export function wheel(curve, z) {
|
|
|
997
1005
|
// key is the bounds' key with the place, so one entry never serves
|
|
998
1006
|
// two bounds.
|
|
999
1007
|
const written = new Map();
|
|
1000
|
-
|
|
1008
|
+
// The bounds are measured from the colour the element drew; a layer
|
|
1009
|
+
// beneath may have lit it, so they travel with it, and a lit colour
|
|
1010
|
+
// turns round its own luminance rather than snapping back unturned.
|
|
1011
|
+
const choose = (color, rested, bound, place) => {
|
|
1001
1012
|
const turn = hueTurned(color, turned(place, t));
|
|
1002
|
-
|
|
1013
|
+
const { lo, hi } = bounds.get(keyOf(rested, bound)) ?? UNBOUNDED;
|
|
1014
|
+
const lit = stop(color) - stop(rested);
|
|
1015
|
+
return nearestWithin(turn.looks, color, depth === ColorDepth.TRUECOLOR ? turn.either.map(measured) : indexed, { lo: lo + lit, hi: hi + lit });
|
|
1003
1016
|
};
|
|
1004
|
-
const write = (color, bound, place) => {
|
|
1005
|
-
const key = `${keyOf(
|
|
1006
|
-
const found = written.get(key) ?? choose(color, bound, place);
|
|
1017
|
+
const write = (color, rested, bound, place) => {
|
|
1018
|
+
const key = `${color.hex} ${keyOf(rested, bound)} ${place.row}:${place.centre}`;
|
|
1019
|
+
const found = written.get(key) ?? choose(color, rested, bound, place);
|
|
1007
1020
|
written.set(key, found);
|
|
1008
1021
|
return found;
|
|
1009
1022
|
};
|
|
1010
1023
|
return (shown, cell) => {
|
|
1011
|
-
const
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1024
|
+
const { pair } = cell;
|
|
1025
|
+
const mine = { fg: own(pair.terminal.fg, pair.fg), bg: own(pair.terminal.bg, pair.bg) };
|
|
1026
|
+
const turn = (side, other) => mine[side] ? write(shown[side], pair[side], boundWith(pair[side], pair[other], mine[other]), placeOf(cell, pair[side].hex)) : shown[side];
|
|
1027
|
+
return { ...shown, fg: turn("fg", "bg"), bg: turn("bg", "fg") };
|
|
1014
1028
|
};
|
|
1015
1029
|
});
|
|
1016
1030
|
});
|
|
@@ -1027,10 +1041,16 @@ export function wheel(curve, z) {
|
|
|
1027
1041
|
* off (`touchedAt`).
|
|
1028
1042
|
*/
|
|
1029
1043
|
function hueTurned(color, degrees) {
|
|
1030
|
-
|
|
1031
|
-
|
|
1044
|
+
return degrees === 0 ? { looks: color, either: [color] } : atLuminance(color, degrees, relativeLuminance(color));
|
|
1045
|
+
}
|
|
1046
|
+
/** `color` with its chroma taken out. */
|
|
1047
|
+
function greyOf(color) {
|
|
1048
|
+
const { l, h, alpha } = Oklch.fromRgba(color);
|
|
1049
|
+
return new Oklch(l, 0, h, alpha).toRgba();
|
|
1050
|
+
}
|
|
1051
|
+
/** `color` turned `degrees` round the hue circle and brought to `luminance`: the two 8-bit colours either side of it. */
|
|
1052
|
+
function atLuminance(color, degrees, luminance) {
|
|
1032
1053
|
const from = Oklch.fromRgba(color);
|
|
1033
|
-
const luminance = relativeLuminance(color);
|
|
1034
1054
|
const at = (l) => new Oklch(l, from.c, from.h + degrees, from.alpha).toRgba();
|
|
1035
1055
|
let [dim, bright] = [0, 1];
|
|
1036
1056
|
for (let i = 0; i < 24; i++) {
|
|
@@ -1090,10 +1110,10 @@ function turnedBounds(pairs, depth, turnedAs) {
|
|
|
1090
1110
|
return new Map([...bounds].map(([key, { lo, hi }]) => [key, hi - lo < ROUNDING_GAP ? STILL : { lo, hi }]));
|
|
1091
1111
|
}
|
|
1092
1112
|
const measured = (color) => ({ color, looks: Oklch.fromRgba(color), stop: stop(color) });
|
|
1093
|
-
/** `ColorSpec.indexed`'s colours, measured once for each
|
|
1113
|
+
/** `ColorSpec.indexed`'s colours, measured once for each terminal and depth. */
|
|
1094
1114
|
const measuredMemo = new WeakMemo();
|
|
1095
|
-
function measuredIndexed(depth,
|
|
1096
|
-
return measuredMemo.get(ColorSpec.indexed(depth,
|
|
1115
|
+
function measuredIndexed(depth, terminal) {
|
|
1116
|
+
return measuredMemo.get(ColorSpec.indexed(depth, terminal), (indexed) => indexed.map(([, color]) => measured(color)));
|
|
1097
1117
|
}
|
|
1098
1118
|
/**
|
|
1099
1119
|
* The colour that looks nearest `color` (OKLab's ΔE): `rest`, or one of
|
|
@@ -1115,6 +1135,120 @@ function nearestOf(color, offered) {
|
|
|
1115
1135
|
}
|
|
1116
1136
|
return best?.[0];
|
|
1117
1137
|
}
|
|
1138
|
+
/** What a stop's float arithmetic can cost: a pair at its floor sits exactly on its bound. */
|
|
1139
|
+
const MEASURE = 1e-9;
|
|
1140
|
+
/**
|
|
1141
|
+
* The stops a colour written at `depth` can land on: everything between black
|
|
1142
|
+
* and white in truecolor, and otherwise what the depth has an index for, so a
|
|
1143
|
+
* sixteen-colour terminal's reach is its slots.
|
|
1144
|
+
*/
|
|
1145
|
+
function reachOf(depth, terminal) {
|
|
1146
|
+
return depth === ColorDepth.TRUECOLOR ? [0, Math.log(21)] : extent(measuredIndexed(depth, terminal).map((candidate) => candidate.stop));
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Where each ground of `pairs` may land so that every ink drawn on it keeps
|
|
1150
|
+
* room to read on the side of it the ink rested on: a light ink caps its ground
|
|
1151
|
+
* at the floor below the lightest the depth writes, a dark one bars it from the
|
|
1152
|
+
* floor above the darkest. Where a ground rests is always within: a depth also
|
|
1153
|
+
* writes the terminal's own colour, which at sixteen colours lies past its
|
|
1154
|
+
* terminal's reach, and a ground no layer moved comes back as it was. These are
|
|
1155
|
+
* what the depth's reach leaves, so a ground any one effect moves is inside them.
|
|
1156
|
+
*/
|
|
1157
|
+
function groundBounds(pairs, depth, terminal) {
|
|
1158
|
+
const [bottom, top] = reachOf(depth, terminal);
|
|
1159
|
+
const bounds = new Map();
|
|
1160
|
+
for (const pair of pairs) {
|
|
1161
|
+
const [gap, rests] = [Math.log(floorOf(pair, depth)), stop(pair.bg)];
|
|
1162
|
+
const { lo, hi } = bounds.get(pair.bg.hex) ?? UNBOUNDED;
|
|
1163
|
+
bounds.set(pair.bg.hex, stop(pair.fg) >= rests ? { lo, hi: Math.min(hi, Math.max(rests, top - gap)) } : { lo: Math.max(lo, Math.min(rests, bottom + gap)), hi });
|
|
1164
|
+
}
|
|
1165
|
+
return bounds;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Several effects on one element, as one effect: each layer is handed the look
|
|
1169
|
+
* the one before it left and the same cell, so later layers are on top, and each
|
|
1170
|
+
* recognises a cell by what the element drew (`EffectCell.pair`), never by the
|
|
1171
|
+
* colour it is handed. A hue a layer turned onto another of the element's
|
|
1172
|
+
* colours is not mistaken for it, and turns once. It changes when the soonest
|
|
1173
|
+
* layer does, and with no layers it leaves every cell as drawn.
|
|
1174
|
+
*
|
|
1175
|
+
* Every word reads at its floor (`floorOf`), whatever the layers did and in
|
|
1176
|
+
* whatever order. Each loop proves that for colours as the element drew them,
|
|
1177
|
+
* and a colour another layer moved voids the proof: light lands at another
|
|
1178
|
+
* luminance on a turned hue. So once the layers have moved a cell, its ground
|
|
1179
|
+
* lands where its inks can still read (`groundBounds`), a function of the colour
|
|
1180
|
+
* alone so one fill stays one fill, and an ink that does not read on it slides
|
|
1181
|
+
* to the luminance that does, on the side of the ground it stands on while that
|
|
1182
|
+
* side has room. A layer that takes an ink across its ground crosses it here too:
|
|
1183
|
+
* nothing reads at the crossing, so the word jumps it. [LAW:single-enforcer]
|
|
1184
|
+
* Both are measured as `Effected` writes them, and both hand back what they were
|
|
1185
|
+
* given where it already holds, so a layer that proved its own floor is drawn as
|
|
1186
|
+
* it would be alone. A frame that takes a ground past its bounds is the one
|
|
1187
|
+
* exception, whatever its ink reads: a swap of ink and ground lands at the floor,
|
|
1188
|
+
* since a bound cannot ask what one cell's ink does without splitting a fill.
|
|
1189
|
+
* The floor outranks a colour being the terminal's: a terminal
|
|
1190
|
+
* ink is moved here where a ground under it left it unreadable. The rule is the
|
|
1191
|
+
* same for one layer as for five: a veil (`veiled`, or a transition's `effect`)
|
|
1192
|
+
* stops at the floor here, so a transition goes in an `Effected` around this
|
|
1193
|
+
* one, as a lifecycle places it.
|
|
1194
|
+
*/
|
|
1195
|
+
export function layered(...effects) {
|
|
1196
|
+
const settled = new WeakMemo();
|
|
1197
|
+
return (drawn) => {
|
|
1198
|
+
const { depth, terminal } = drawn;
|
|
1199
|
+
const grounds = settled.get(terminal, () => new Memo()).get(drawnKey(drawn), () => groundBounds(drawn.pairs, depth, terminal));
|
|
1200
|
+
const words = new Set(drawn.pairs.map(pairKey));
|
|
1201
|
+
const offered = depth === ColorDepth.TRUECOLOR ? undefined : measuredIndexed(depth, terminal);
|
|
1202
|
+
// `color` as itself if it is within the first of `bounds`, else the nearest
|
|
1203
|
+
// colour the depth writes there, else the same of the next; as itself where
|
|
1204
|
+
// none has any. In truecolor that is its hue at the bound's luminance, or a
|
|
1205
|
+
// grey there: a colour keeps its chroma only so near black or white, and a
|
|
1206
|
+
// bound may be at either. `rested` is always offered, since a depth also
|
|
1207
|
+
// writes the terminal's own colour, which it has no index for, and the
|
|
1208
|
+
// colour a pair rested in is the one a bound was measured from.
|
|
1209
|
+
const landing = (color, rested, ...bounds) => {
|
|
1210
|
+
const at = stop(color);
|
|
1211
|
+
for (const { lo, hi } of bounds) {
|
|
1212
|
+
const inside = (stop) => stop >= lo - MEASURE && stop <= hi + MEASURE;
|
|
1213
|
+
if (inside(at))
|
|
1214
|
+
return color;
|
|
1215
|
+
const aim = 0.05 * Math.exp(Math.min(hi, Math.max(lo, at))) - 0.05;
|
|
1216
|
+
const found = nearestOf(color, [measured(rested), ...(offered ?? [color, greyOf(color)].flatMap((from) => atLuminance(from, 0, aim).either.map(measured)))].filter(({ stop }) => inside(stop)));
|
|
1217
|
+
if (found !== undefined)
|
|
1218
|
+
return found.color;
|
|
1219
|
+
}
|
|
1220
|
+
return color;
|
|
1221
|
+
};
|
|
1222
|
+
const floored = (pair, moved) => {
|
|
1223
|
+
// [LAW:single-enforcer] Every measure below reads the colours as `Effected`
|
|
1224
|
+
// writes them: rounded to the depth, and a translucent one laid on what is
|
|
1225
|
+
// beneath it, so a colour that passes here cannot draw as one that fails.
|
|
1226
|
+
const ground = landing(Effected.shows(pair, moved, depth, terminal).bg, pair.bg, grounds.get(pair.bg.hex) ?? UNBOUNDED);
|
|
1227
|
+
const { fg } = Effected.shows(pair, { fg: moved.fg, bg: ground }, depth, terminal);
|
|
1228
|
+
const floor = floorOf(pair, depth);
|
|
1229
|
+
if (!words.has(pairKey(pair)) || contrastRatio(fg, ground) >= floor)
|
|
1230
|
+
return { fg, bg: ground };
|
|
1231
|
+
const gap = Math.log(floor);
|
|
1232
|
+
const [above, below] = [{ lo: stop(ground) + gap, hi: Infinity }, { lo: -Infinity, hi: stop(ground) - gap }];
|
|
1233
|
+
// The side it stands on, not the one it rested on: a painter's ink may
|
|
1234
|
+
// stand across the ground, and aiming it back would strobe it there and
|
|
1235
|
+
// back each time a move took it under the floor.
|
|
1236
|
+
const [near, far] = stop(fg) >= stop(ground) ? [above, below] : [below, above];
|
|
1237
|
+
return { fg: landing(fg, pair.fg, near, far), bg: ground };
|
|
1238
|
+
};
|
|
1239
|
+
return Animation.map(Animation.all(effects.map((effect) => effect(drawn))), (frames) => {
|
|
1240
|
+
// A colour pair lands once a frame, wherever it falls. [LAW:one-source-of-truth]
|
|
1241
|
+
const landed = new Map();
|
|
1242
|
+
return (look, cell) => {
|
|
1243
|
+
const moved = frames.reduce((under, frame) => frame(under, cell), look);
|
|
1244
|
+
const key = `${pairKey(cell.pair)} ${moved.fg.hex} ${moved.bg.hex}`;
|
|
1245
|
+
const colors = landed.get(key) ?? floored(cell.pair, moved);
|
|
1246
|
+
landed.set(key, colors);
|
|
1247
|
+
return { ...moved, ...colors };
|
|
1248
|
+
};
|
|
1249
|
+
});
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1118
1252
|
/**
|
|
1119
1253
|
* A signal: every cell's ink stepping through `steps`' colours on a beat,
|
|
1120
1254
|
* one colour a hold, looping, for the time it is handed — a gauge's
|
|
@@ -1128,7 +1262,7 @@ function nearestOf(color, offered) {
|
|
|
1128
1262
|
* (`ensureContrast`, the one slide a template's `readableOn` makes too);
|
|
1129
1263
|
* below truecolor, the nearest of the colours the depth has an index for, so on a
|
|
1130
1264
|
* sixteen-colour terminal it steps from slot to slot, the one effect here
|
|
1131
|
-
* that does (`
|
|
1265
|
+
* that does (`stillWhereNothingMoves`). A cell whose ground no offered colour reads
|
|
1132
1266
|
* on keeps the ink it rested in.
|
|
1133
1267
|
*
|
|
1134
1268
|
* The search is settled once for each ground and step — below truecolor,
|
|
@@ -1149,12 +1283,12 @@ export function steppedInk(steps) {
|
|
|
1149
1283
|
const inkOn = legible(drawn);
|
|
1150
1284
|
return Animation.map(rhythm, (i) => {
|
|
1151
1285
|
const step = inks[i];
|
|
1152
|
-
return (look,
|
|
1286
|
+
return (look, { pair }) => ({ ...look, fg: inkOn(step, pair) });
|
|
1153
1287
|
});
|
|
1154
1288
|
};
|
|
1155
1289
|
}
|
|
1156
1290
|
/**
|
|
1157
|
-
* `legibleInk` for the frame `Drawn` describes, of an ink on a cell's
|
|
1291
|
+
* `legibleInk` for the frame `Drawn` describes, of an ink on a cell's pair:
|
|
1158
1292
|
* settled once for each terminal, depth, pair and ink, so a frame drawing
|
|
1159
1293
|
* what an earlier one drew redoes no search. [LAW:single-enforcer] Every
|
|
1160
1294
|
* effect that writes an ink of the caller's choosing writes it through
|
|
@@ -1167,16 +1301,15 @@ export function steppedInk(steps) {
|
|
|
1167
1301
|
*/
|
|
1168
1302
|
function legibleInks() {
|
|
1169
1303
|
const settled = new WeakMemo();
|
|
1170
|
-
return ({ depth,
|
|
1171
|
-
const memo = settled.get(
|
|
1172
|
-
return (ink,
|
|
1173
|
-
const pair = { fg: look.fg, bg: look.bg, terminal: cell.terminal, reverse: cell.reverse };
|
|
1304
|
+
return ({ depth, terminal }) => {
|
|
1305
|
+
const memo = settled.get(terminal, () => ({ inks: new Memo(), readable: new Memo() }));
|
|
1306
|
+
return (ink, pair) => {
|
|
1174
1307
|
const key = `${depth} ${pairKey(pair)}`;
|
|
1175
|
-
const legibleOf = (offered) => legibleAmong(pair, offered, depth,
|
|
1308
|
+
const legibleOf = (offered) => legibleAmong(pair, offered, depth, terminal);
|
|
1176
1309
|
return memo.inks.get(`${key} ${ink.hex}`, () => {
|
|
1177
1310
|
const offered = depth === ColorDepth.TRUECOLOR
|
|
1178
1311
|
? legibleOf([ink, ensureContrast(ink, pair.bg, floorOf(pair, depth))].map(measured))
|
|
1179
|
-
: memo.readable.get(key, () => legibleOf(measuredIndexed(depth,
|
|
1312
|
+
: memo.readable.get(key, () => legibleOf(measuredIndexed(depth, terminal)));
|
|
1180
1313
|
return nearestOf(ink, offered)?.color ?? pair.fg;
|
|
1181
1314
|
});
|
|
1182
1315
|
};
|
|
@@ -1188,10 +1321,10 @@ function legibleInks() {
|
|
|
1188
1321
|
* way `shares` and `wheel` measure: the written colours, at the catalogue's
|
|
1189
1322
|
* floor. An ink none of them is nearest stays `pair`'s own.
|
|
1190
1323
|
*/
|
|
1191
|
-
function legibleAmong(pair, offered, depth,
|
|
1324
|
+
function legibleAmong(pair, offered, depth, terminal) {
|
|
1192
1325
|
const floor = floorOf(pair, depth);
|
|
1193
1326
|
return offered.filter(({ color }) => {
|
|
1194
|
-
const shown = Effected.shows(pair, { fg: color, bg: pair.bg }, depth,
|
|
1327
|
+
const shown = Effected.shows(pair, { fg: color, bg: pair.bg }, depth, terminal);
|
|
1195
1328
|
return contrastRatio(shown.fg, shown.bg) >= floor;
|
|
1196
1329
|
});
|
|
1197
1330
|
}
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
*/
|
|
47
47
|
import { Animation } from "../core/animation.js";
|
|
48
48
|
import { type Beat } from "../core/easing.js";
|
|
49
|
-
import type {
|
|
49
|
+
import type { RenderOptions } from "../core/protocol.js";
|
|
50
50
|
import type { Segment } from "../core/segment.js";
|
|
51
51
|
import { RichText } from "../core/text.js";
|
|
52
52
|
/** One frame: what it shows, and how long it is held, in seconds. */
|
|
@@ -86,12 +86,3 @@ export declare class FrameSequence implements Animation<RichText> {
|
|
|
86
86
|
maximum: number;
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
|
-
/**
|
|
90
|
-
* The text frame `animation` shows at host time `t` under `motion`
|
|
91
|
-
* (`shownAt`), copied and stamped with the hold it is in there: what the
|
|
92
|
-
* template `frames` and `glyphs` functions, which know the reader's
|
|
93
|
-
* preference before any render, splice into a line. The stamp is on the
|
|
94
|
-
* base style, which an empty text still draws as its one piece of no width,
|
|
95
|
-
* and on a copy, since an animation may hand back the one `RichText` it holds.
|
|
96
|
-
*/
|
|
97
|
-
export declare function textFrameAt(animation: Animation<RichText>, t: number, motion: Motion): RichText;
|
|
@@ -44,9 +44,8 @@
|
|
|
44
44
|
* binding has no console for, so the frame arrives read, as a `Group`'s
|
|
45
45
|
* children do.
|
|
46
46
|
*/
|
|
47
|
-
import { Animation,
|
|
47
|
+
import { Animation, placedRender } from "../core/animation.js";
|
|
48
48
|
import { beat } from "../core/easing.js";
|
|
49
|
-
import { layerStyle } from "../core/style.js";
|
|
50
49
|
import { setIntoLine } from "./embed.js";
|
|
51
50
|
/** A sequence of frames prepared to be drawn: built once, asked for the frame at a time. */
|
|
52
51
|
export class FrameSequence {
|
|
@@ -102,17 +101,3 @@ export class FrameSequence {
|
|
|
102
101
|
return { minimum: this.width, maximum: this.width };
|
|
103
102
|
}
|
|
104
103
|
}
|
|
105
|
-
/**
|
|
106
|
-
* The text frame `animation` shows at host time `t` under `motion`
|
|
107
|
-
* (`shownAt`), copied and stamped with the hold it is in there: what the
|
|
108
|
-
* template `frames` and `glyphs` functions, which know the reader's
|
|
109
|
-
* preference before any render, splice into a line. The stamp is on the
|
|
110
|
-
* base style, which an empty text still draws as its one piece of no width,
|
|
111
|
-
* and on a copy, since an animation may hand back the one `RichText` it holds.
|
|
112
|
-
*/
|
|
113
|
-
export function textFrameAt(animation, t, motion) {
|
|
114
|
-
const shown = shownAt(animation, t, motion);
|
|
115
|
-
const stamped = shown.value.copy();
|
|
116
|
-
stamped.style = layerStyle(stamped.style, stamp(holdOf(shown, t)));
|
|
117
|
-
return stamped;
|
|
118
|
-
}
|
|
@@ -51,11 +51,11 @@
|
|
|
51
51
|
* Positions are the child's own: row and column count from the top-left of
|
|
52
52
|
* what the child drew.
|
|
53
53
|
*/
|
|
54
|
-
import { Animation } from "../core/animation.js";
|
|
54
|
+
import { Animation, type Moves } from "../core/animation.js";
|
|
55
55
|
import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
|
|
56
56
|
import { Segment } from "../core/segment.js";
|
|
57
57
|
import { type RichText } from "../core/text.js";
|
|
58
|
-
import { type
|
|
58
|
+
import { type Placed } from "./effect.js";
|
|
59
59
|
/**
|
|
60
60
|
* A cell of the element a glyph effect wraps: the glyph it shows, where it
|
|
61
61
|
* sits and who drew it, its place in reading order among the cells handed
|
|
@@ -95,20 +95,18 @@ export interface GlyphEffectedOptions {
|
|
|
95
95
|
/**
|
|
96
96
|
* A `GlyphEffected` at `t`, or an element through a glyph effect at a time an
|
|
97
97
|
* animation's `at` was handed: `renderable` with its cells' glyphs passed
|
|
98
|
-
* through `effect` at `t`,
|
|
99
|
-
* `
|
|
98
|
+
* through `effect` at `t`, telling the hold it drew in as a `Recolored` does
|
|
99
|
+
* (`RenderOptions.onMoved`, `./effect.ts`). [LAW:one-type-per-behavior]
|
|
100
100
|
*/
|
|
101
101
|
export declare class Reglyphed implements Renderable, Measurable {
|
|
102
102
|
readonly renderable: Renderable;
|
|
103
103
|
readonly effect: GlyphEffect;
|
|
104
|
-
readonly
|
|
104
|
+
private readonly _t;
|
|
105
105
|
readonly key: string;
|
|
106
106
|
constructor(renderable: Renderable, effect: GlyphEffect, options: GlyphEffectedOptions & {
|
|
107
107
|
readonly t: number;
|
|
108
108
|
});
|
|
109
109
|
render(options: RenderOptions): Iterable<Segment>;
|
|
110
|
-
/** The frame the effect draws at `t`, and the hold it says that is in. */
|
|
111
|
-
moved(options: RenderOptions): Moved;
|
|
112
110
|
measure(options: RenderOptions): {
|
|
113
111
|
minimum: number;
|
|
114
112
|
maximum: number;
|
|
@@ -116,9 +114,10 @@ export declare class Reglyphed implements Renderable, Measurable {
|
|
|
116
114
|
}
|
|
117
115
|
/**
|
|
118
116
|
* `renderable` under a glyph effect over time, an `Animation<Renderable>`
|
|
119
|
-
* as an `Effected` is (`./effect.ts`): its
|
|
120
|
-
*
|
|
121
|
-
* output's `asciiOnly`,
|
|
117
|
+
* as an `Effected` is (`./effect.ts`): with nothing drawn its answer is `t`,
|
|
118
|
+
* and placed, on its own or as a part of any combinator, it says the glyph
|
|
119
|
+
* effect's exact hold at that time and the output's `asciiOnly`, which the
|
|
120
|
+
* frame's drawing told (`Moves`).
|
|
122
121
|
*/
|
|
123
122
|
export declare class GlyphEffected implements Animation<Renderable> {
|
|
124
123
|
readonly renderable: Renderable;
|
|
@@ -127,8 +126,8 @@ export declare class GlyphEffected implements Animation<Renderable> {
|
|
|
127
126
|
constructor(renderable: Renderable, effect: GlyphEffect, options: GlyphEffectedOptions);
|
|
128
127
|
/** The element through the glyph effect at `t`, unstamped. */
|
|
129
128
|
at(t: number): Reglyphed;
|
|
130
|
-
nextChange(t: number): number;
|
|
131
|
-
lastChange(t: number): number;
|
|
129
|
+
nextChange(t: number, moves?: Moves): number;
|
|
130
|
+
lastChange(t: number, moves?: Moves): number;
|
|
132
131
|
/** The element under the glyph effect with its motion taken out. */
|
|
133
132
|
reduced(): GlyphEffected;
|
|
134
133
|
/** Drawn at the frame's time, its cells stamped with the glyph effect's exact hold there. */
|
|
@@ -150,7 +149,7 @@ export declare class GlyphEffected implements Animation<Renderable> {
|
|
|
150
149
|
* its every style and span, each moved to where its glyphs now sit, and a
|
|
151
150
|
* frame that changes nothing hands the text back as it was. What it says of
|
|
152
151
|
* when it changes is the effect's, unstamped: whoever draws it at a time
|
|
153
|
-
* stamps it (`
|
|
152
|
+
* stamps it (`placed`), or marks it for the engine to (the template `glyphs`).
|
|
154
153
|
*/
|
|
155
154
|
export declare function reglyphText(text: RichText, effect: GlyphEffect, options: {
|
|
156
155
|
readonly key: string;
|