@promptctl/rich-js 0.24.0 → 0.25.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 +7 -0
- package/THIRD-PARTY-NOTICES +29 -1
- package/dist/core/anchor.d.ts +5 -0
- package/dist/core/anchor.js +11 -4
- package/dist/core/animation.d.ts +243 -0
- package/dist/core/animation.js +375 -0
- package/dist/core/ansi.js +2 -2
- package/dist/core/box.d.ts +7 -7
- package/dist/core/cell-widths.d.ts +9 -0
- package/dist/core/cell-widths.js +140 -0
- package/dist/core/cells.d.ts +40 -24
- package/dist/core/cells.js +151 -66
- package/dist/core/clock.d.ts +2 -2
- package/dist/core/clock.js +6 -4
- package/dist/core/color.d.ts +27 -10
- package/dist/core/color.js +21 -29
- package/dist/core/console.d.ts +28 -17
- package/dist/core/console.js +48 -43
- package/dist/core/easing.d.ts +140 -10
- package/dist/core/easing.js +328 -50
- package/dist/core/emoji.d.ts +1 -1
- package/dist/core/emoji.js +1 -2
- package/dist/core/export-html.js +2 -1
- package/dist/core/export-lines.js +3 -3
- package/dist/core/highlighter.d.ts +9 -8
- package/dist/core/highlighter.js +3 -2
- package/dist/core/json.js +3 -2
- package/dist/core/markup.js +16 -12
- package/dist/core/measure.js +3 -3
- package/dist/core/memo.d.ts +11 -2
- package/dist/core/memo.js +16 -0
- package/dist/core/oklch.js +17 -6
- package/dist/core/osc8.d.ts +1 -1
- package/dist/core/osc8.js +4 -5
- package/dist/core/paint.d.ts +2 -1
- package/dist/core/paint.js +4 -3
- package/dist/core/pattern.d.ts +21 -0
- package/dist/core/pattern.js +21 -0
- package/dist/core/place.js +2 -1
- package/dist/core/pretty.js +11 -12
- package/dist/core/protocol.d.ts +136 -21
- package/dist/core/protocol.js +203 -16
- package/dist/core/render.d.ts +15 -2
- package/dist/core/render.js +27 -6
- package/dist/core/segment.d.ts +31 -6
- package/dist/core/segment.js +68 -53
- package/dist/core/spinnerData.d.ts +1 -1
- package/dist/core/state.d.ts +53 -0
- package/dist/core/state.js +82 -0
- package/dist/core/strip.d.ts +11 -13
- package/dist/core/strip.js +126 -113
- package/dist/core/style.d.ts +47 -14
- package/dist/core/style.js +74 -34
- package/dist/core/text.d.ts +5 -8
- package/dist/core/text.js +133 -133
- package/dist/core/timeline.d.ts +109 -0
- package/dist/core/timeline.js +180 -0
- package/dist/core/tween.d.ts +49 -0
- package/dist/core/tween.js +71 -0
- package/dist/core/view.d.ts +92 -0
- package/dist/core/view.js +83 -0
- package/dist/host/app.d.ts +49 -21
- package/dist/host/app.js +72 -38
- package/dist/host/input.js +3 -4
- package/dist/index.d.ts +30 -15
- package/dist/index.js +20 -6
- package/dist/node/traceback.js +4 -3
- package/dist/renderables/change.d.ts +42 -0
- package/dist/renderables/change.js +112 -0
- package/dist/renderables/columns.d.ts +1 -1
- package/dist/renderables/columns.js +3 -3
- package/dist/renderables/constrain.js +2 -1
- package/dist/renderables/effect.d.ts +155 -92
- package/dist/renderables/effect.js +203 -136
- package/dist/renderables/effects.d.ts +137 -24
- package/dist/renderables/effects.js +276 -113
- package/dist/renderables/embed.d.ts +1 -1
- package/dist/renderables/embed.js +18 -9
- package/dist/renderables/flexStrip.js +2 -1
- package/dist/renderables/frames.d.ts +45 -43
- package/dist/renderables/frames.js +57 -45
- package/dist/renderables/glyph-effect.d.ts +158 -0
- package/dist/renderables/glyph-effect.js +237 -0
- package/dist/renderables/glyph-effects.d.ts +147 -0
- package/dist/renderables/glyph-effects.js +283 -0
- package/dist/renderables/group.d.ts +1 -1
- package/dist/renderables/group.js +2 -2
- package/dist/renderables/html-entities.d.ts +8 -0
- package/dist/renderables/html-entities.js +386 -0
- package/dist/renderables/layout.d.ts +10 -9
- package/dist/renderables/layout.js +42 -35
- package/dist/renderables/lifecycle.d.ts +77 -0
- package/dist/renderables/lifecycle.js +132 -0
- package/dist/renderables/list-view.d.ts +2 -2
- package/dist/renderables/list-view.js +11 -8
- package/dist/renderables/live.d.ts +36 -21
- package/dist/renderables/live.js +89 -42
- package/dist/renderables/markdown-parse.d.ts +109 -0
- package/dist/renderables/markdown-parse.js +1427 -0
- package/dist/renderables/markdown-url.d.ts +19 -0
- package/dist/renderables/markdown-url.js +314 -0
- package/dist/renderables/markdown.d.ts +5 -7
- package/dist/renderables/markdown.js +356 -683
- package/dist/renderables/padding.d.ts +7 -20
- package/dist/renderables/padding.js +29 -50
- package/dist/renderables/panel.d.ts +30 -32
- package/dist/renderables/panel.js +65 -109
- package/dist/renderables/progress.d.ts +83 -32
- package/dist/renderables/progress.js +186 -90
- package/dist/renderables/progressBar.d.ts +22 -12
- package/dist/renderables/progressBar.js +33 -10
- package/dist/renderables/prompt.js +3 -2
- package/dist/renderables/python-space.d.ts +13 -0
- package/dist/renderables/python-space.js +25 -0
- package/dist/renderables/scroll.d.ts +50 -0
- package/dist/renderables/scroll.js +106 -0
- package/dist/renderables/spinner.d.ts +57 -24
- package/dist/renderables/spinner.js +101 -49
- package/dist/renderables/status.js +1 -1
- package/dist/renderables/syntax.d.ts +10 -9
- package/dist/renderables/syntax.js +5 -3
- package/dist/renderables/table.d.ts +48 -46
- package/dist/renderables/table.js +170 -208
- package/dist/renderables/traceback.d.ts +3 -3
- package/dist/renderables/traceback.js +12 -10
- package/dist/renderables/tree.d.ts +28 -25
- package/dist/renderables/tree.js +108 -58
- package/dist/renderables/viewport.d.ts +77 -38
- package/dist/renderables/viewport.js +156 -72
- package/dist/template-bindings/animation-funcs.d.ts +78 -9
- package/dist/template-bindings/animation-funcs.js +132 -14
- package/dist/template-bindings/index.d.ts +5 -5
- package/dist/template-bindings/index.js +7 -6
- package/dist/themes/ramp.d.ts +62 -4
- package/dist/themes/ramp.js +82 -4
- package/dist/themes/registry.js +5 -7
- package/dist/widgets/button.js +3 -2
- package/dist/widgets/checkbox.js +4 -3
- package/dist/widgets/dropdown.d.ts +3 -1
- package/dist/widgets/dropdown.js +20 -15
- package/dist/widgets/event-router.js +4 -2
- package/dist/widgets/focus-manager.d.ts +23 -14
- package/dist/widgets/focus-manager.js +34 -33
- package/dist/widgets/hit.d.ts +6 -0
- package/dist/widgets/hit.js +8 -1
- package/dist/widgets/index.d.ts +1 -1
- package/dist/widgets/observable.d.ts +23 -0
- package/dist/widgets/observable.js +43 -0
- package/dist/widgets/slider.d.ts +1 -1
- package/dist/widgets/slider.js +12 -3
- package/dist/widgets/static-item.d.ts +1 -1
- package/dist/widgets/static-item.js +6 -7
- package/dist/widgets/text-input.d.ts +53 -47
- package/dist/widgets/text-input.js +456 -399
- package/dist/widgets/themed-widget.js +2 -1
- package/dist/widgets/toggle.js +4 -3
- package/dist/widgets/types.d.ts +10 -4
- package/dist/widgets/types.js +8 -2
- package/dist/widgets/widget-app.d.ts +32 -6
- package/dist/widgets/widget-app.js +74 -27
- package/dist/widgets/widget-base.d.ts +2 -2
- package/dist/widgets/widget-base.js +29 -21
- package/package.json +9 -5
|
@@ -1,30 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Effected — a renderable whose finished cells' colours
|
|
3
|
-
* effect at a moment `t`.
|
|
2
|
+
* Effected — a renderable whose finished cells' looks — colours and text
|
|
3
|
+
* attributes — pass through an effect at a moment `t`.
|
|
4
4
|
*
|
|
5
|
-
* An effect is
|
|
6
|
-
*
|
|
7
|
-
* cell's
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
5
|
+
* An effect is, for what an element draws in a frame, an `Animation`
|
|
6
|
+
* (`core/animation.ts`) of what is done to its cells: its value at `t` is a
|
|
7
|
+
* function from a cell's look and where the cell is to the cell's new look
|
|
8
|
+
* (`EffectFrame`), and it says when that next changes as every
|
|
9
|
+
* animation does. Whatever holds for the whole frame is worked out once, in
|
|
10
|
+
* the effect and its `at(t)`, and every cell of the frame is handed to the
|
|
11
|
+
* function `at(t)` returns, but for those a wrapper keeps back: `confined`
|
|
12
|
+
* hands over only the cells its owners drew, so an effect never counts on
|
|
13
|
+
* seeing them all. Shimmer, pulse, fade, dissolve and a blinking attribute
|
|
14
|
+
* are all this one shape, differing by what the function does with its
|
|
15
|
+
* inputs [LAW:one-type-per-behavior]; two effects stack by nesting one
|
|
14
16
|
* wrapper in another, which is the same as calling one after the other but
|
|
15
17
|
* for what the outer one measures: the inner one's output.
|
|
16
18
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* A cell's look is one value, its colours and its attributes together
|
|
20
|
+
* (`CellLook`), rather than an attribute stage beside the colour one, so
|
|
21
|
+
* everything that takes an `Effect` — `confined`, `onBeat`,
|
|
22
|
+
* `Animation.all`, nesting over a glyph effect — takes one that moves
|
|
23
|
+
* attributes with no second copy of itself. `reverse` is the one attribute
|
|
24
|
+
* a look leaves out (`CellAttributes`): it decides which of a style's slots
|
|
25
|
+
* a colour on screen is in, and a swap of the two is already a colour move.
|
|
23
26
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
27
|
+
* What the element draws (`Drawn`) — the cells the effect will be handed,
|
|
28
|
+
* and the colours of its words — is measured here, in the render, off the
|
|
29
|
+
* same reading of each segment its cells are drawn from, and handed to the
|
|
30
|
+
* effect once a frame. It is what an effect keeping words legible is settled
|
|
31
|
+
* against (`onColors`, `./effects.ts`), so no caller derives it a second time
|
|
32
|
+
* from a render of its own.
|
|
33
|
+
*
|
|
34
|
+
* An `Effected` is the element under its effect over time: an
|
|
35
|
+
* `Animation<Renderable>`, placed wherever a renderable goes and drawn at
|
|
36
|
+
* the time of the frame it is drawn in. Its value at `t` is the element
|
|
37
|
+
* through the effect at `t` (`Recolored`). [LAW:no-ambient-temporal-coupling]
|
|
38
|
+
* The time is the frame's, never a clock read, so the same time always draws
|
|
39
|
+
* the same bytes, at 30 frames a second or one every two seconds.
|
|
28
40
|
*
|
|
29
41
|
* What an effect may not do is the contract this module holds:
|
|
30
42
|
*
|
|
@@ -34,46 +46,44 @@
|
|
|
34
46
|
* segment's link and meta and carries its anchor shifted to the run's first
|
|
35
47
|
* cell (`Style.shiftedBy`), so hit testing still finds the owner.
|
|
36
48
|
* - A cell whose colours come back unchanged keeps its original `ColorSpec`,
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* the
|
|
49
|
+
* one whose attributes come back as its style set them keeps them, and a
|
|
50
|
+
* segment whose every cell is unchanged is yielded with its look as
|
|
51
|
+
* it was, so the identity effect is byte-identical; what an `Effected` gains is when
|
|
52
|
+
* the frame next changes (`Style.hold`), which writes no byte.
|
|
40
53
|
* Neighbouring cells the output depth
|
|
41
54
|
* writes alike stay one segment: a segment is cut only where the written
|
|
42
|
-
* colours change, so two colours that land on one index
|
|
55
|
+
* colours or the attributes change, so two colours that land on one index
|
|
56
|
+
* are one run.
|
|
43
57
|
* - New colours are built with `ColorSpec.fromRgba`, never parsed from a
|
|
44
58
|
* string, so no frame adds an entry to a parse cache.
|
|
45
|
-
* - With no colour output (`colorSystem: null`)
|
|
46
|
-
*
|
|
59
|
+
* - With no colour output (`colorSystem: null`) no style is written at all,
|
|
60
|
+
* attributes included, so there is nothing for an effect to change, and
|
|
61
|
+
* the child's segments pass through untouched.
|
|
47
62
|
*
|
|
48
63
|
* Positions are the child's own: row and column count from the top-left of
|
|
49
64
|
* what the child drew, so a sweep crosses the element, not the screen.
|
|
50
65
|
*/
|
|
51
66
|
import { drawnBy, shiftAnchor } from "../core/anchor.js";
|
|
67
|
+
import { Animation, frameTimeOf, holdOf, shownAt, stampFrame } from "../core/animation.js";
|
|
52
68
|
import { cellLen, graphemes } from "../core/cells.js";
|
|
53
69
|
import { ColorDepth, ColorSpec, SURFACE_BLACK } from "../core/color.js";
|
|
54
70
|
import { fnv1a } from "../core/fnv1a.js";
|
|
55
71
|
import { Measurement } from "../core/measure.js";
|
|
72
|
+
import { render } from "../core/protocol.js";
|
|
56
73
|
import { Segment } from "../core/segment.js";
|
|
57
74
|
import { JOIN, colorsOwnedBy } from "../core/strip.js";
|
|
58
|
-
import { Style } from "../core/style.js";
|
|
75
|
+
import { ATTRIBUTE_NAMES, Style } from "../core/style.js";
|
|
76
|
+
import { finiteTime } from "../core/easing.js";
|
|
77
|
+
/** What drew nothing the effect can change: held forever, as far as the effect says. */
|
|
78
|
+
const UNMOVED = { lastChange: Number.NEGATIVE_INFINITY, nextChange: Number.POSITIVE_INFINITY };
|
|
59
79
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
80
|
+
* An `Effected` at `t`, or an element through an effect at a time an
|
|
81
|
+
* animation's `at` was handed — a lifecycle's time since it entered: `renderable`
|
|
82
|
+
* with its cells' looks passed through `effect` at `t`, saying nothing of
|
|
83
|
+
* when that changes, since whatever placed the animation stamps its hold.
|
|
84
|
+
* [LAW:one-type-per-behavior]
|
|
63
85
|
*/
|
|
64
|
-
export
|
|
65
|
-
return effect.nextChange?.(t, drawn) ?? t;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* An effect drawing as `effect` does, with `nextChange` as its answer to when
|
|
69
|
-
* its frame next changes, in place of whatever `effect` answered; `effect`
|
|
70
|
-
* itself is left as it was. A composer whose answer depends on the inner
|
|
71
|
-
* effect's reads it with `nextChangeOf`, as `onBeat` and `confined` do.
|
|
72
|
-
*/
|
|
73
|
-
export function withNextChange(effect, nextChange) {
|
|
74
|
-
return Object.assign((t, drawn) => effect(t, drawn), { nextChange });
|
|
75
|
-
}
|
|
76
|
-
export class Effected {
|
|
86
|
+
export class Recolored {
|
|
77
87
|
renderable;
|
|
78
88
|
effect;
|
|
79
89
|
t;
|
|
@@ -82,35 +92,30 @@ export class Effected {
|
|
|
82
92
|
constructor(renderable, effect, options) {
|
|
83
93
|
this.renderable = renderable;
|
|
84
94
|
this.effect = effect;
|
|
85
|
-
|
|
86
|
-
throw new RangeError(`Effected needs a finite t, got ${options.t}`);
|
|
87
|
-
}
|
|
88
|
-
this.t = options.t;
|
|
95
|
+
this.t = finiteTime("Effected", options.t);
|
|
89
96
|
this.key = options.key;
|
|
90
97
|
this.theme = options.theme;
|
|
91
98
|
}
|
|
92
99
|
render(options) {
|
|
93
|
-
|
|
100
|
+
return this.moved(options).segments;
|
|
101
|
+
}
|
|
102
|
+
/** The frame the effect draws at `t`, and the hold it says that is in. */
|
|
103
|
+
moved(options) {
|
|
104
|
+
const segments = render(this.renderable, options);
|
|
94
105
|
// With no colour output nothing the effect does is written, so the child
|
|
95
106
|
// is what is shown, and it changes when the child says it does.
|
|
96
107
|
if (options.colorSystem === null)
|
|
97
|
-
return segments;
|
|
98
|
-
return this._moved(segments, options);
|
|
99
|
-
}
|
|
100
|
-
*_moved(segments, options) {
|
|
108
|
+
return { segments, hold: UNMOVED };
|
|
101
109
|
// No depth named is no downgrade, as `drawnColors` reads it.
|
|
102
110
|
const depth = options.colorSystem ?? ColorDepth.TRUECOLOR;
|
|
103
111
|
// [LAW:one-source-of-truth] The frame is measured before any cell is
|
|
104
112
|
// handed to the effect, off the same looks its cells are drawn from, and
|
|
105
|
-
// the one measure is what
|
|
106
|
-
// asked with.
|
|
113
|
+
// the one measure is what the effect's animation is made from.
|
|
107
114
|
const looked = looks(segments, depth, this.theme);
|
|
108
115
|
const walked = walk(looked);
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
// cells say so, under whatever the child already said (the sooner wins).
|
|
113
|
-
yield* Segment.applyStyle(this._recolored(walked, frame, depth), new Style({ nextChange: nextChangeOf(this.effect, this.t, drawn) }));
|
|
116
|
+
const animation = this.effect(measured(looked, () => walked, depth, this.theme));
|
|
117
|
+
const shown = shownAt(animation, this.t, options);
|
|
118
|
+
return { segments: this._recolored(walked, shown.value, depth), hold: holdOf(shown, this.t) };
|
|
114
119
|
}
|
|
115
120
|
*_recolored(walked, frame, depth) {
|
|
116
121
|
for (const { segment, look, glyphs } of walked) {
|
|
@@ -118,35 +123,38 @@ export class Effected {
|
|
|
118
123
|
yield segment;
|
|
119
124
|
continue;
|
|
120
125
|
}
|
|
121
|
-
const { style: base, wire, slots, pair } = look;
|
|
126
|
+
const { style: base, wire, slots, pair, attributes } = look;
|
|
122
127
|
const { terminal } = pair;
|
|
123
|
-
const from = { fg: pair.fg, bg: pair.bg };
|
|
128
|
+
const from = { fg: pair.fg, bg: pair.bg, attributes };
|
|
124
129
|
// [LAW:one-source-of-truth] A run is cells the wire writes alike: each
|
|
125
130
|
// cell is drawn at the output depth first and merged on the colours that
|
|
126
|
-
// writes, so two truecolors landing on one index
|
|
127
|
-
// the effect gave its neighbour's
|
|
131
|
+
// writes and on its attributes, so two truecolors landing on one index
|
|
132
|
+
// stay one run. A cell the effect gave its neighbour's look is drawn as
|
|
133
|
+
// that neighbour was.
|
|
128
134
|
const runs = [];
|
|
129
135
|
for (const { glyph, cell } of glyphs) {
|
|
136
|
+
const moved = cell === undefined ? from : returned(frame(from, { ...cell, seed: cellSeed(this.key, cell.row, cell.col), terminal, reverse: pair.reverse }));
|
|
130
137
|
// Back from the screen to the style's slots: the same swap undoes itself.
|
|
131
|
-
const colors =
|
|
138
|
+
const colors = onScreen(moved, pair.reverse);
|
|
132
139
|
const last = runs[runs.length - 1];
|
|
133
140
|
const writes = last !== undefined && sameColors(last.colors, colors) ? last.drawn : this.respec(wire, slots, colors, depth);
|
|
134
|
-
if (last !== undefined && writesAlike(last.drawn, writes)) {
|
|
141
|
+
if (last !== undefined && writesAlike(last.drawn, writes) && sameAttributes(last.attributes, moved.attributes)) {
|
|
135
142
|
last.text += glyph;
|
|
136
143
|
last.cells += cellLen(glyph);
|
|
137
144
|
last.colors = colors;
|
|
138
145
|
}
|
|
139
146
|
else {
|
|
140
|
-
runs.push({ text: glyph, cells: cellLen(glyph), colors, drawn: writes });
|
|
147
|
+
runs.push({ text: glyph, cells: cellLen(glyph), colors, attributes: moved.attributes, drawn: writes });
|
|
141
148
|
}
|
|
142
149
|
}
|
|
143
|
-
if (runs.every((run) => writesAlike(run.drawn, wire))) {
|
|
150
|
+
if (runs.every((run) => writesAlike(run.drawn, wire) && sameAttributes(run.attributes, attributes))) {
|
|
144
151
|
yield segment;
|
|
145
152
|
continue;
|
|
146
153
|
}
|
|
147
154
|
let offset = 0;
|
|
148
155
|
for (const run of runs) {
|
|
149
|
-
const
|
|
156
|
+
const placed = base.shiftedBy(offset);
|
|
157
|
+
const shifted = sameAttributes(run.attributes, attributes) ? placed : placed.withAttributes(cellAttributes(run.attributes));
|
|
150
158
|
const style = writesAlike(run.drawn, wire) ? shifted : shifted.add(Style.fromColor(run.drawn.color, run.drawn.bgcolor));
|
|
151
159
|
yield new Segment(run.text, style);
|
|
152
160
|
offset += run.cells;
|
|
@@ -156,6 +164,61 @@ export class Effected {
|
|
|
156
164
|
measure(options) {
|
|
157
165
|
return Measurement.get(options, this.renderable);
|
|
158
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* The specs that draw a cell's slot colours `to` where the segment drew
|
|
169
|
+
* `from` with `wire`: each moved slot in the spec `movedSpecs` writes it
|
|
170
|
+
* in, the rest as `wire` wrote them.
|
|
171
|
+
*/
|
|
172
|
+
respec(wire, from, to, depth) {
|
|
173
|
+
const moved = movedSpecs(from, to, depth, this.theme);
|
|
174
|
+
return { color: moved.fg ?? wire.color, bgcolor: moved.bg ?? wire.bgcolor };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* `renderable` under `effect` over time: an `Animation<Renderable>` whose
|
|
179
|
+
* value at `t` is the element through the effect at `t`, placed wherever a
|
|
180
|
+
* renderable goes. Its own answer to when it changes is `t`, at every time:
|
|
181
|
+
* an `Effect` is made from what the element draws, so with nothing drawn
|
|
182
|
+
* nothing can say more — early, never false. Placed, it says exactly: its
|
|
183
|
+
* cells carry the effect's own hold at the frame's time and the output's
|
|
184
|
+
* depth (`stampFrame`), under whatever the element already said, so at a
|
|
185
|
+
* depth that shows no motion a loop says it never changes there and the
|
|
186
|
+
* host sleeps.
|
|
187
|
+
*/
|
|
188
|
+
export class Effected {
|
|
189
|
+
renderable;
|
|
190
|
+
effect;
|
|
191
|
+
key;
|
|
192
|
+
theme;
|
|
193
|
+
constructor(renderable, effect, options) {
|
|
194
|
+
this.renderable = renderable;
|
|
195
|
+
this.effect = effect;
|
|
196
|
+
this.key = options.key;
|
|
197
|
+
this.theme = options.theme;
|
|
198
|
+
}
|
|
199
|
+
/** The element through the effect at `t`, unstamped. */
|
|
200
|
+
at(t) {
|
|
201
|
+
return new Recolored(this.renderable, this.effect, { t, key: this.key, theme: this.theme });
|
|
202
|
+
}
|
|
203
|
+
nextChange(t) {
|
|
204
|
+
return t;
|
|
205
|
+
}
|
|
206
|
+
lastChange(t) {
|
|
207
|
+
return t;
|
|
208
|
+
}
|
|
209
|
+
/** The element under the effect with its motion taken out. */
|
|
210
|
+
reduced() {
|
|
211
|
+
const { effect } = this;
|
|
212
|
+
return new Effected(this.renderable, (drawn) => effect(drawn).reduced(), this);
|
|
213
|
+
}
|
|
214
|
+
/** Drawn at the frame's time, its cells stamped with the effect's exact hold there. */
|
|
215
|
+
render(options) {
|
|
216
|
+
const { segments, hold } = this.at(frameTimeOf(options)).moved(options);
|
|
217
|
+
return stampFrame(segments, hold);
|
|
218
|
+
}
|
|
219
|
+
measure(options) {
|
|
220
|
+
return Measurement.get(options, this.renderable);
|
|
221
|
+
}
|
|
159
222
|
/**
|
|
160
223
|
* The colours the screen shows for `pair` once it is drawn moved to `to`,
|
|
161
224
|
* both as the screen shows them, at `depth` on `theme`: what an effect
|
|
@@ -168,19 +231,14 @@ export class Effected {
|
|
|
168
231
|
const moved = movedSpecs(from, onScreen(to, pair.reverse), depth, theme);
|
|
169
232
|
return onScreen({ fg: moved.fg?.getTruecolor(theme, true) ?? from.fg, bg: moved.bg?.getTruecolor(theme, false) ?? from.bg }, pair.reverse);
|
|
170
233
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
/** A cell's seed: FNV-1a of the key and the position, scaled into [0, 1). */
|
|
181
|
-
seed(row, col) {
|
|
182
|
-
return Number.parseInt(fnv1a(`${this.key}:${row}:${col}`), 16) / 0x1_0000_0000;
|
|
183
|
-
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* A cell's seed: FNV-1a of the element's key and the cell's position, scaled
|
|
237
|
+
* into [0, 1). [LAW:one-source-of-truth] One rule for a colour effect's seeds
|
|
238
|
+
* and a glyph effect's, so one element under both twinkles from one seed.
|
|
239
|
+
*/
|
|
240
|
+
export function cellSeed(key, row, col) {
|
|
241
|
+
return Number.parseInt(fnv1a(`${key}:${row}:${col}`), 16) / 0x1_0000_0000;
|
|
184
242
|
}
|
|
185
243
|
const DEFAULT = ColorSpec.default();
|
|
186
244
|
/** Each of `segments` with how it is drawn at `depth` on `theme`; a control segment draws nothing. */
|
|
@@ -193,34 +251,48 @@ function looks(segments, depth, theme) {
|
|
|
193
251
|
const slots = { fg: (wire.color ?? DEFAULT).getTruecolor(theme, true), bg: (wire.bgcolor ?? DEFAULT).getTruecolor(theme, false) };
|
|
194
252
|
const reverse = style.reverse === true;
|
|
195
253
|
const terminal = onScreen({ fg: wire.color?.isDefault ?? true, bg: wire.bgcolor?.isDefault ?? true }, reverse);
|
|
196
|
-
return { segment, look: { style, wire, slots, pair: { ...onScreen(slots, reverse), terminal, reverse } } };
|
|
254
|
+
return { segment, look: { style, wire, slots, pair: { ...onScreen(slots, reverse), terminal, reverse }, attributes: cellAttributes(style) } };
|
|
197
255
|
});
|
|
198
256
|
}
|
|
199
257
|
/**
|
|
200
|
-
* Each glyph of `
|
|
201
|
-
* top-left of the output, a wide glyph one cell at its first
|
|
202
|
-
* anchor shifted to that column as `Style.shiftedBy` shifts
|
|
203
|
-
*
|
|
204
|
-
*
|
|
258
|
+
* Each glyph of each of `segments`, where its cell sits: rows and columns
|
|
259
|
+
* from the top-left of the output, a wide glyph one cell at its first
|
|
260
|
+
* column, and its anchor shifted to that column as `Style.shiftedBy` shifts
|
|
261
|
+
* a run's. A control segment draws nothing and has none.
|
|
262
|
+
* [LAW:one-source-of-truth] The one walk: what `Drawn.cells` lists, what the
|
|
263
|
+
* render hands a colour effect, and what a glyph effect (`./glyph-effect.ts`)
|
|
264
|
+
* is handed are all read off it.
|
|
205
265
|
*/
|
|
206
|
-
function
|
|
266
|
+
export function walkGlyphs(segments) {
|
|
207
267
|
let row = 0;
|
|
208
268
|
let col = 0;
|
|
209
|
-
return
|
|
210
|
-
if (
|
|
211
|
-
return
|
|
212
|
-
const anchor =
|
|
269
|
+
return segments.map((segment) => {
|
|
270
|
+
if (segment.isControl)
|
|
271
|
+
return [];
|
|
272
|
+
const anchor = segment.style?.anchor;
|
|
213
273
|
// Cells into the segment, as `base.shiftedBy` counts them in the render.
|
|
214
274
|
let into = 0;
|
|
215
|
-
|
|
275
|
+
return [...graphemes(segment.text)].map((glyph) => {
|
|
216
276
|
const cell = glyph === "\n" ? undefined : { row, col, anchor: anchor && shiftAnchor(anchor, into) };
|
|
217
277
|
[row, col] = glyph === "\n" ? [row + 1, 0] : [row, col + cellLen(glyph)];
|
|
218
278
|
into += cellLen(glyph);
|
|
219
279
|
return { glyph, cell };
|
|
220
280
|
});
|
|
221
|
-
return { segment, look, glyphs };
|
|
222
281
|
});
|
|
223
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Whether `text` draws ink: anything but blanks. [LAW:one-source-of-truth]
|
|
285
|
+
* The one test, so the words a colour effect lights are the words a glyph
|
|
286
|
+
* effect (`./glyph-effects.ts`) scrambles and its particles pass behind.
|
|
287
|
+
*/
|
|
288
|
+
export function inked(text) {
|
|
289
|
+
return text.trim() !== "";
|
|
290
|
+
}
|
|
291
|
+
/** `looked` with each segment's glyphs placed, by the one walk. */
|
|
292
|
+
function walk(looked) {
|
|
293
|
+
const placed = walkGlyphs(looked.map(({ segment }) => segment));
|
|
294
|
+
return looked.map((item, i) => ({ ...item, glyphs: placed[i] }));
|
|
295
|
+
}
|
|
224
296
|
/** Whether `style` draws in its ink on a blank glyph: a line under, through or over it, or a frame round it. */
|
|
225
297
|
function drawsLines(style) {
|
|
226
298
|
return [style.underline, style.underline2, style.strike, style.overline, style.frame, style.encircle].some((on) => on === true);
|
|
@@ -239,9 +311,9 @@ function measured(looked, walked, depth, theme) {
|
|
|
239
311
|
return [];
|
|
240
312
|
// A blank glyph shows its ground, and any line its style draws in its ink.
|
|
241
313
|
const blankShows = look.pair.bg.hex !== ground || drawsLines(look.style);
|
|
242
|
-
return glyphs.flatMap(({ glyph, cell }) => (cell !== undefined && (glyph
|
|
314
|
+
return glyphs.flatMap(({ glyph, cell }) => (cell !== undefined && (inked(glyph) || blankShows) ? [cell] : []));
|
|
243
315
|
});
|
|
244
|
-
const words = looked.flatMap(({ segment, look }) => (look !== undefined && segment.text
|
|
316
|
+
const words = looked.flatMap(({ segment, look }) => (look !== undefined && inked(segment.text) && !drawnBy(look.style.anchor, JOIN) ? [look.pair] : []));
|
|
245
317
|
const pairs = [...new Map(words.map((pair) => [`${pair.fg.hex}/${pair.bg.hex}/${pair.terminal.fg}/${pair.terminal.bg}/${pair.reverse}`, pair])).values()];
|
|
246
318
|
// [LAW:one-source-of-truth] A slot the terminal fills is no colour of the element's, whatever its hex.
|
|
247
319
|
const own = (slot) => pairs.flatMap((pair) => (pair.terminal[slot] ? [] : [pair[slot].hex]));
|
|
@@ -254,35 +326,6 @@ function measured(looked, walked, depth, theme) {
|
|
|
254
326
|
theme,
|
|
255
327
|
};
|
|
256
328
|
}
|
|
257
|
-
/**
|
|
258
|
-
* `effect` sampled and held on `beat`: drawn at the moment the hold in
|
|
259
|
-
* progress began (`Beat.start`), so within a hold every frame is the frame
|
|
260
|
-
* at its start, and at each hold's start it moves on. A continuous effect
|
|
261
|
-
* given a short curve becomes one that steps — a `wheel` turning a sixth of
|
|
262
|
-
* a turn once a second — and at one redraw a second a hold shorter than a
|
|
263
|
-
* second is skipped, as frames are. [LAW:no-ambient-temporal-coupling] The
|
|
264
|
-
* held time is a function of `t`, so the same `t` draws the same bytes at
|
|
265
|
-
* any rate. The frame next changes at a hold's start, and only one the
|
|
266
|
-
* sampled effect has moved by: the first hold start at or after what it
|
|
267
|
-
* answers for the frame held now (`Beat.end`). An effect that moves every
|
|
268
|
-
* frame makes the beat's own answer, the end of this hold; one that is done
|
|
269
|
-
* and says never leaves the beat saying never too, since the hold after this
|
|
270
|
-
* one would draw the same frame. The answer is read off what the effect
|
|
271
|
-
* says, never off a comparison of frames: an effect that steps and is back
|
|
272
|
-
* on the same colour by the next hold still turns the beat there, an early
|
|
273
|
-
* answer the contract allows.
|
|
274
|
-
*/
|
|
275
|
-
export function onBeat(effect, beat) {
|
|
276
|
-
return withNextChange((t, drawn) => effect(beat.start(t), drawn), (t, drawn) => {
|
|
277
|
-
const moved = nextChangeOf(effect, beat.start(t), drawn);
|
|
278
|
-
if (moved === Number.POSITIVE_INFINITY)
|
|
279
|
-
return moved;
|
|
280
|
-
const end = beat.end(t);
|
|
281
|
-
// The hold boundaries after `t` are `end`, then each hold's end in turn;
|
|
282
|
-
// the frame turns at the first one the effect has moved by.
|
|
283
|
-
return moved <= end ? end : beat.start(moved) === moved ? moved : beat.end(moved);
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
329
|
/**
|
|
287
330
|
* `effect` on the colours any of `owners` drew, itself or through an owner
|
|
288
331
|
* nested in its output, and every other colour left as it is: one item of a
|
|
@@ -302,16 +345,14 @@ export function confined(effect, ...owners) {
|
|
|
302
345
|
// The frame as the effect sees it: the owners' cells alone. [LAW:one-source-of-truth] One narrowing, handed to both stages.
|
|
303
346
|
const owned = (drawn) => ({ ...drawn, cells: () => drawn.cells().filter((cell) => any(ownedOf(cell))) });
|
|
304
347
|
// The owners' cells change when the effect's frame does.
|
|
305
|
-
return
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
};
|
|
314
|
-
}, (t, drawn) => nextChangeOf(effect, t, owned(drawn)));
|
|
348
|
+
return (drawn) => Animation.map(effect(owned(drawn)), (frame) => (look, cell) => {
|
|
349
|
+
const owns = ownedOf(cell);
|
|
350
|
+
if (!any(owns))
|
|
351
|
+
return look;
|
|
352
|
+
const moved = frame(look, cell);
|
|
353
|
+
// An attribute is drawn on the glyph, in its ink, so it moves with whoever owns the ink.
|
|
354
|
+
return { fg: owns.fg ? moved.fg : look.fg, bg: owns.bg ? moved.bg : look.bg, attributes: owns.fg ? moved.attributes : look.attributes };
|
|
355
|
+
});
|
|
315
356
|
}
|
|
316
357
|
/**
|
|
317
358
|
* What `segments`, drawn at `depth` on `theme`, draw their words in, measured
|
|
@@ -357,6 +398,32 @@ function slotWritesAlike(a, b, foreground) {
|
|
|
357
398
|
function written(spec, foreground) {
|
|
358
399
|
return spec?.getAnsiCodes(foreground).join(";") ?? "";
|
|
359
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* The look a frame returned, refused at once when it carries no attributes.
|
|
403
|
+
* [LAW:no-silent-failure] A frame written before an effect moved attributes
|
|
404
|
+
* returns `{ fg, bg }`, and the effects playground runs a visitor's frame with
|
|
405
|
+
* its types stripped, not checked; unrefused, it fails as a TypeError in run
|
|
406
|
+
* merging that names neither the frame nor the contract.
|
|
407
|
+
*/
|
|
408
|
+
function returned(look) {
|
|
409
|
+
if (look.attributes === undefined) {
|
|
410
|
+
throw new TypeError("an effect frame returns the look it was handed with its changes, { ...look, fg, bg }: this one returned no attributes");
|
|
411
|
+
}
|
|
412
|
+
return look;
|
|
413
|
+
}
|
|
414
|
+
/** The attributes a look carries: `ATTRIBUTE_NAMES` but `reverse`. */
|
|
415
|
+
const MOVED = ATTRIBUTE_NAMES.filter((name) => name !== "reverse");
|
|
416
|
+
/**
|
|
417
|
+
* The attributes a look carries, read off anything that sets them — a
|
|
418
|
+
* `Style`, or a look a frame returned with more on it than `CellAttributes`
|
|
419
|
+
* names. [LAW:one-source-of-truth] The one place `reverse` is left out.
|
|
420
|
+
*/
|
|
421
|
+
export function cellAttributes(of) {
|
|
422
|
+
return Object.fromEntries(MOVED.map((name) => [name, of[name]]));
|
|
423
|
+
}
|
|
424
|
+
function sameAttributes(a, b) {
|
|
425
|
+
return a === b || MOVED.every((name) => a[name] === b[name]);
|
|
426
|
+
}
|
|
360
427
|
function sameColors(a, b) {
|
|
361
428
|
return sameColor(a.fg, b.fg) && sameColor(a.bg, b.bg);
|
|
362
429
|
}
|