tweakit 0.6.0 → 0.7.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/AGENTS.md +5 -5
- package/README.md +3 -3
- package/dist/tweaks/auto.js +1 -1
- package/dist/tweaks/auto.js.map +1 -1
- package/dist/tweaks/bezier-S2BOXQBM.js +2 -0
- package/dist/tweaks/bezier-S2BOXQBM.js.map +6 -0
- package/dist/tweaks/chunk-3MZAAMC2.js +2 -0
- package/dist/tweaks/chunk-3MZAAMC2.js.map +6 -0
- package/dist/tweaks/chunk-ETB35KFV.js +2 -0
- package/dist/tweaks/chunk-ETB35KFV.js.map +6 -0
- package/dist/tweaks/chunk-YB5GKWZI.js +2 -0
- package/dist/tweaks/chunk-YB5GKWZI.js.map +6 -0
- package/dist/tweaks/clip-A3VI53BK.js +2 -0
- package/dist/tweaks/clip-A3VI53BK.js.map +6 -0
- package/dist/tweaks/color-UGYMAF3W.js +2 -0
- package/dist/tweaks/core.js +1 -1
- package/dist/tweaks/core.js.map +3 -3
- package/dist/tweaks/fold-GY47HIQA.js +2 -0
- package/dist/tweaks/fold-GY47HIQA.js.map +6 -0
- package/dist/tweaks/gradient-BTX25WSH.js +2 -0
- package/dist/tweaks/gradient-BTX25WSH.js.map +6 -0
- package/dist/tweaks/{heavy-CLD7UAVI.js → heavy-NPFIGHRM.js} +2 -2
- package/dist/tweaks/image-HTQQ4WX6.js +4 -0
- package/dist/tweaks/image-HTQQ4WX6.js.map +6 -0
- package/dist/tweaks/{image-store-WRKY2NPU.js → image-store-QD5SMZXG.js} +1 -1
- package/dist/tweaks/image-store-QD5SMZXG.js.map +6 -0
- package/dist/tweaks/interval-BHPI4LRN.js +2 -0
- package/dist/tweaks/interval-BHPI4LRN.js.map +6 -0
- package/dist/tweaks/markup-QM53TQJZ.js +2 -0
- package/dist/tweaks/markup-QM53TQJZ.js.map +6 -0
- package/dist/tweaks/monitor-722WIYGW.js +3 -0
- package/dist/tweaks/monitor-722WIYGW.js.map +6 -0
- package/dist/tweaks/motion-HMCZK6HJ.js +2 -0
- package/dist/tweaks/motion-HMCZK6HJ.js.map +6 -0
- package/dist/tweaks/plot-D3NQ6XNN.js +3 -0
- package/dist/tweaks/plot-D3NQ6XNN.js.map +6 -0
- package/dist/tweaks/point-XDXKMDXH.js +2 -0
- package/dist/tweaks/point-XDXKMDXH.js.map +6 -0
- package/dist/tweaks/presets-EUWNSCUW.js +2 -0
- package/dist/tweaks/presets-EUWNSCUW.js.map +6 -0
- package/dist/tweaks/shadow-AM7VSRNS.js +2 -0
- package/dist/tweaks/shadow-AM7VSRNS.js.map +6 -0
- package/dist/tweaks/shadow-root-RYUV3AGS.js +3 -0
- package/dist/tweaks/shadow-root-RYUV3AGS.js.map +6 -0
- package/dist/tweaks/sides-JWIURDA4.js +2 -0
- package/dist/tweaks/sides-JWIURDA4.js.map +6 -0
- package/dist/tweaks/spring-BF5GBTPM.js +2 -0
- package/dist/tweaks/spring-BF5GBTPM.js.map +6 -0
- package/dist/tweaks/tabs-PRVUUZXZ.js +2 -0
- package/dist/tweaks/tabs-PRVUUZXZ.js.map +6 -0
- package/dist/tweaks.css +1 -1
- package/dist/tweaks.js +5 -4
- package/dist/tweaks.js.map +3 -3
- package/dist/types/tweaks/enhance.d.ts +3 -2
- package/dist/types/tweaks/types.d.ts +278 -72
- package/package.json +5 -2
- package/src/tweaks/auto.ts +9 -5
- package/src/tweaks/clip.ts +84 -22
- package/src/tweaks/controls/basic.ts +463 -139
- package/src/tweaks/controls/bezier.ts +152 -47
- package/src/tweaks/controls/color.ts +610 -161
- package/src/tweaks/controls/gradient.ts +256 -77
- package/src/tweaks/controls/image.ts +388 -132
- package/src/tweaks/controls/interval.ts +126 -41
- package/src/tweaks/controls/monitor.ts +142 -36
- package/src/tweaks/controls/motion.ts +231 -56
- package/src/tweaks/controls/plot.ts +321 -101
- package/src/tweaks/controls/point.ts +134 -45
- package/src/tweaks/controls/shadow.ts +103 -30
- package/src/tweaks/controls/sides.ts +150 -40
- package/src/tweaks/controls/spring.ts +220 -60
- package/src/tweaks/controls/tabs.ts +39 -18
- package/src/tweaks/core.ts +1 -1
- package/src/tweaks/easing.ts +45 -20
- package/src/tweaks/enhance.ts +21 -6
- package/src/tweaks/feedback.ts +133 -34
- package/src/tweaks/fold.ts +22 -10
- package/src/tweaks/heavy.ts +461 -137
- package/src/tweaks/icons.ts +12 -5
- package/src/tweaks/image-store.ts +131 -43
- package/src/tweaks/lazy.ts +36 -19
- package/src/tweaks/markup.ts +255 -92
- package/src/tweaks/panel.ts +1262 -438
- package/src/tweaks/presets.ts +85 -0
- package/src/tweaks/schema.ts +213 -51
- package/src/tweaks/shadow-root.ts +178 -0
- package/src/tweaks/shared.ts +802 -190
- package/src/tweaks/standalone.ts +120 -30
- package/src/tweaks/types.ts +644 -143
- package/src/tweaks/undo.ts +104 -0
- package/src/tweaks.css +556 -550
- package/src/wide-gamut.ts +473 -125
- package/dist/tweaks/bezier-JFEZMCLP.js +0 -2
- package/dist/tweaks/bezier-JFEZMCLP.js.map +0 -6
- package/dist/tweaks/chunk-6UO4DHBN.js +0 -2
- package/dist/tweaks/chunk-6UO4DHBN.js.map +0 -6
- package/dist/tweaks/chunk-DIQMLBAS.js +0 -2
- package/dist/tweaks/chunk-DIQMLBAS.js.map +0 -6
- package/dist/tweaks/chunk-XE6OUJBG.js +0 -2
- package/dist/tweaks/chunk-XE6OUJBG.js.map +0 -6
- package/dist/tweaks/clip-WW7JYO4X.js +0 -2
- package/dist/tweaks/clip-WW7JYO4X.js.map +0 -6
- package/dist/tweaks/color-YMMFURP6.js +0 -2
- package/dist/tweaks/fold-U6YRZLCJ.js +0 -2
- package/dist/tweaks/fold-U6YRZLCJ.js.map +0 -6
- package/dist/tweaks/gradient-JER7PW4D.js +0 -2
- package/dist/tweaks/gradient-JER7PW4D.js.map +0 -6
- package/dist/tweaks/image-H6QSLGO3.js +0 -4
- package/dist/tweaks/image-H6QSLGO3.js.map +0 -6
- package/dist/tweaks/image-store-WRKY2NPU.js.map +0 -6
- package/dist/tweaks/interval-TEUHQGZN.js +0 -2
- package/dist/tweaks/interval-TEUHQGZN.js.map +0 -6
- package/dist/tweaks/markup-NIBJD6E2.js +0 -2
- package/dist/tweaks/markup-NIBJD6E2.js.map +0 -6
- package/dist/tweaks/monitor-3BY7YCGX.js +0 -3
- package/dist/tweaks/monitor-3BY7YCGX.js.map +0 -6
- package/dist/tweaks/motion-FQPUBKKM.js +0 -2
- package/dist/tweaks/motion-FQPUBKKM.js.map +0 -6
- package/dist/tweaks/plot-2HBNBZSG.js +0 -3
- package/dist/tweaks/plot-2HBNBZSG.js.map +0 -6
- package/dist/tweaks/point-VV6XV3I6.js +0 -2
- package/dist/tweaks/point-VV6XV3I6.js.map +0 -6
- package/dist/tweaks/shadow-5E4E55I2.js +0 -2
- package/dist/tweaks/shadow-5E4E55I2.js.map +0 -6
- package/dist/tweaks/sides-P3STEVYY.js +0 -2
- package/dist/tweaks/sides-P3STEVYY.js.map +0 -6
- package/dist/tweaks/spring-HUVSXNVN.js +0 -2
- package/dist/tweaks/spring-HUVSXNVN.js.map +0 -6
- package/dist/tweaks/tabs-76FW7LQ7.js +0 -2
- package/dist/tweaks/tabs-76FW7LQ7.js.map +0 -6
- /package/dist/tweaks/{color-YMMFURP6.js.map → color-UGYMAF3W.js.map} +0 -0
- /package/dist/tweaks/{heavy-CLD7UAVI.js.map → heavy-NPFIGHRM.js.map} +0 -0
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
// ── Interval / range slider — dual-handle [lo,hi]. Lazy.
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
el,
|
|
4
|
+
txt,
|
|
5
|
+
clamp,
|
|
6
|
+
words,
|
|
7
|
+
roundToStep,
|
|
8
|
+
stepPrecision,
|
|
9
|
+
gridEnds,
|
|
10
|
+
normalizeRange,
|
|
11
|
+
rangeStep,
|
|
12
|
+
overlapsText,
|
|
13
|
+
wireHoverClass,
|
|
14
|
+
onReady,
|
|
15
|
+
onLive,
|
|
16
|
+
registerControl,
|
|
17
|
+
rubberBand,
|
|
18
|
+
rubberRelease,
|
|
19
|
+
} from "../shared.js";
|
|
3
20
|
import { dragGesture } from "../heavy.js";
|
|
4
21
|
import type { OnChange } from "../shared.js";
|
|
5
22
|
import type { Meta } from "../schema.js";
|
|
@@ -7,45 +24,70 @@ import type { Control } from "../types.js";
|
|
|
7
24
|
|
|
8
25
|
function createInterval(meta: Meta, onChange: OnChange): Control {
|
|
9
26
|
const label = meta.label;
|
|
10
|
-
const t0 = +(meta.value && meta.value[0]),
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
27
|
+
const t0 = +(meta.value && meta.value[0]),
|
|
28
|
+
t1 = +(meta.value && meta.value[1]);
|
|
29
|
+
const lo0 = Number.isFinite(t0) ? t0 : 0,
|
|
30
|
+
hi0 = Number.isFinite(t1) ? t1 : 1;
|
|
31
|
+
// normalizeRange, the slider's guard: an absent or non-finite bound (data-min="abc") derives from the values, each
|
|
32
|
+
// end widening it, for the shorthand and the verbose form alike: [20, 80] spans 0–240, not 20–80 (which pinned both
|
|
33
|
+
// handles).
|
|
14
34
|
let { min, max, step } = normalizeRange(meta.min, meta.max, meta.step, [lo0, hi0], label);
|
|
15
35
|
[min, max] = gridEnds(min, max, step); // the slider's rule: no value past an off-grid bound
|
|
16
36
|
const decimals = stepPrecision(step);
|
|
17
37
|
const q = (v: number) => roundToStep(v, min, step);
|
|
18
|
-
let lo = 0,
|
|
19
|
-
|
|
38
|
+
let lo = 0,
|
|
39
|
+
hi = 0;
|
|
40
|
+
// ordered, on the grid, in bounds
|
|
41
|
+
const put = (a: number, b: number) => {
|
|
42
|
+
lo = clamp(q(Math.min(a, b)), min, max);
|
|
43
|
+
hi = clamp(q(Math.max(a, b)), min, max);
|
|
44
|
+
};
|
|
20
45
|
// Missing tuple entries fall back to the bounds: value:[5] gives [5, max], not [5, NaN].
|
|
21
46
|
put(Number.isFinite(t0) ? t0 : min, Number.isFinite(t1) ? t1 : max);
|
|
22
47
|
|
|
23
|
-
const wrap = el("div", "
|
|
24
|
-
const track = el("div", "
|
|
25
|
-
const fill = el("div", "
|
|
26
|
-
const hLo = el("div", "
|
|
27
|
-
|
|
28
|
-
const
|
|
48
|
+
const wrap = el("div", "twk-slider-wrap");
|
|
49
|
+
const track = el("div", "twk-slider twk-interval");
|
|
50
|
+
const fill = el("div", "twk-slider-fill");
|
|
51
|
+
const hLo = el("div", "twk-slider-handle"),
|
|
52
|
+
hHi = el("div", "twk-slider-handle");
|
|
53
|
+
const labelEl = txt("span", "twk-slider-label", label);
|
|
54
|
+
const valueEl = el("span", "twk-slider-value");
|
|
29
55
|
track.append(fill, hLo, hHi, labelEl, valueEl);
|
|
30
56
|
wrap.append(track);
|
|
31
57
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
58
|
+
const tr = words();
|
|
59
|
+
(
|
|
60
|
+
[
|
|
61
|
+
["intervalMin", "{label} minimum", hLo],
|
|
62
|
+
["intervalMax", "{label} maximum", hHi],
|
|
63
|
+
] as const
|
|
64
|
+
).forEach(([k, d, h]) => {
|
|
65
|
+
h.tabIndex = 0;
|
|
66
|
+
h.setAttribute("role", "slider");
|
|
67
|
+
h.setAttribute("aria-label", tr(k, d, { label }));
|
|
68
|
+
h.setAttribute("aria-valuemin", String(min));
|
|
69
|
+
h.setAttribute("aria-valuemax", String(max));
|
|
35
70
|
});
|
|
36
71
|
|
|
37
|
-
const pctOf = (v: number) => ((v - min) / (
|
|
38
|
-
|
|
72
|
+
const pctOf = (v: number) => ((v - min) / (max - min || 1)) * 100;
|
|
73
|
+
// stay inset at the extremes, like the slider handle
|
|
74
|
+
const handleLeft = (pct: number) => `clamp(5px, calc(${pct}% - 1.5px), calc(100% - 9px))`;
|
|
39
75
|
const render = () => {
|
|
40
|
-
const a = pctOf(lo),
|
|
41
|
-
|
|
42
|
-
|
|
76
|
+
const a = pctOf(lo),
|
|
77
|
+
b = pctOf(hi);
|
|
78
|
+
fill.style.left = a + "%";
|
|
79
|
+
fill.style.width = Math.max(0, b - a) + "%";
|
|
80
|
+
hLo.style.left = handleLeft(a);
|
|
81
|
+
hHi.style.left = handleLeft(b);
|
|
43
82
|
valueEl.textContent = `${lo.toFixed(decimals)} – ${hi.toFixed(decimals)}`;
|
|
44
|
-
hLo.setAttribute("aria-valuenow", String(lo));
|
|
45
|
-
|
|
83
|
+
hLo.setAttribute("aria-valuenow", String(lo));
|
|
84
|
+
hHi.setAttribute("aria-valuenow", String(hi));
|
|
85
|
+
// Value-dodge (overlapsText), per handle: one sliding under the label or the readout yields so the text stays
|
|
86
|
+
// legible.
|
|
46
87
|
const trackW = wrap.offsetWidth;
|
|
47
88
|
if (trackW) {
|
|
48
|
-
// The label's room is what the dual readout leaves, measured: 12px label inset + 8px gap + the readout's right
|
|
89
|
+
// The label's room is what the dual readout leaves, measured: 12px label inset + 8px gap + the readout's right
|
|
90
|
+
// inset.
|
|
49
91
|
labelEl.style.maxWidth = Math.max(0, trackW - valueEl.offsetWidth - 12 - 8 - 10) + "px";
|
|
50
92
|
const dodges = (pct: number) => overlapsText(labelEl, valueEl, (pct / 100) * trackW - 1.5, 3);
|
|
51
93
|
hLo.classList.toggle("is-dodge", dodges(pctOf(lo)));
|
|
@@ -55,50 +97,93 @@ function createInterval(meta: Meta, onChange: OnChange): Control {
|
|
|
55
97
|
render();
|
|
56
98
|
|
|
57
99
|
const emit = () => onChange([lo, hi]);
|
|
58
|
-
const setLo = (v: number) => {
|
|
59
|
-
|
|
100
|
+
const setLo = (v: number) => {
|
|
101
|
+
lo = clamp(Math.min(q(v), hi), min, max);
|
|
102
|
+
render();
|
|
103
|
+
};
|
|
104
|
+
const setHi = (v: number) => {
|
|
105
|
+
hi = clamp(Math.max(q(v), lo), min, max);
|
|
106
|
+
render();
|
|
107
|
+
};
|
|
60
108
|
|
|
61
|
-
let rect: DOMRect | null = null,
|
|
109
|
+
let rect: DOMRect | null = null,
|
|
110
|
+
scale = 1,
|
|
111
|
+
active: "lo" | "hi" | null = null;
|
|
62
112
|
// Divide out any ancestor CSS transform (rect is visual px, offsetWidth layout px), as the single slider does.
|
|
63
|
-
const valFromX = (x: number) => {
|
|
113
|
+
const valFromX = (x: number) => {
|
|
114
|
+
const native = wrap.offsetWidth || rect.width;
|
|
115
|
+
const p = clamp((x - rect.left) / scale / native, 0, 1);
|
|
116
|
+
return clamp(min + p * (max - min), min, max);
|
|
117
|
+
};
|
|
64
118
|
const grab = (x: number) => {
|
|
65
119
|
const v = valFromX(x);
|
|
66
|
-
if (!active) active = v < lo ? "lo" : v > hi ? "hi" :
|
|
67
|
-
active === "lo" ? setLo(v) : setHi(v);
|
|
120
|
+
if (!active) active = v < lo ? "lo" : v > hi ? "hi" : Math.abs(v - lo) <= Math.abs(v - hi) ? "lo" : "hi";
|
|
121
|
+
active === "lo" ? setLo(v) : setHi(v);
|
|
122
|
+
emit();
|
|
68
123
|
};
|
|
69
124
|
dragGesture(track, {
|
|
70
125
|
onDown: (e) => {
|
|
71
126
|
e.preventDefault();
|
|
72
|
-
rect = wrap.getBoundingClientRect();
|
|
127
|
+
rect = wrap.getBoundingClientRect();
|
|
128
|
+
scale = rect.width / (wrap.offsetWidth || rect.width);
|
|
73
129
|
track.style.transition = ""; // a re-press mid spring-home stretches 1:1
|
|
74
|
-
track.classList.add("is-active", "is-dragging");
|
|
130
|
+
track.classList.add("is-active", "is-dragging");
|
|
131
|
+
active = null;
|
|
75
132
|
grab(e.clientX);
|
|
76
|
-
|
|
133
|
+
// preventDefault killed click-to-focus; focus without a ring
|
|
134
|
+
(active === "lo" ? hLo : hHi).focus({ focusVisible: false });
|
|
135
|
+
},
|
|
136
|
+
onMove: (e) => {
|
|
137
|
+
grab(e.clientX);
|
|
138
|
+
rubberBand(track, rect, e.clientX);
|
|
139
|
+
},
|
|
140
|
+
onEnd: () => {
|
|
141
|
+
rubberRelease(track);
|
|
142
|
+
rect = null;
|
|
143
|
+
active = null;
|
|
144
|
+
track.classList.remove("is-active", "is-dragging");
|
|
77
145
|
},
|
|
78
|
-
onMove: (e) => { grab(e.clientX); rubberBand(track, rect, e.clientX); },
|
|
79
|
-
onEnd: () => { rubberRelease(track); rect = null; active = null; track.classList.remove("is-active", "is-dragging"); },
|
|
80
146
|
});
|
|
81
147
|
wireHoverClass(track, render); // re-run the dodge with the real track width on hover
|
|
82
|
-
// Recompute once layout and fonts settle (a web-font swap shifts the widths the dodge measures) and on track-width
|
|
148
|
+
// Recompute once layout and fonts settle (a web-font swap shifts the widths the dodge measures) and on track-width
|
|
149
|
+
// change.
|
|
83
150
|
onReady(render);
|
|
84
|
-
|
|
151
|
+
// twk-reflow: a tab page revealing this re-measures it
|
|
152
|
+
onLive(
|
|
153
|
+
track,
|
|
154
|
+
[
|
|
155
|
+
[window, "resize"],
|
|
156
|
+
[window, "twk-reflow"],
|
|
157
|
+
],
|
|
158
|
+
render,
|
|
159
|
+
);
|
|
85
160
|
|
|
86
161
|
// Home/End snap a handle to its neighbor-or-limit, so the two can't cross.
|
|
87
162
|
const page = (max - min) / 10 || step * 10; // PageUp/Down: a tenth of the range, as the slider jumps
|
|
88
163
|
const onKey = (which: "lo" | "hi") => (e: KeyboardEvent) => {
|
|
89
164
|
const nv = which === "lo" ? rangeStep(e, lo, step, min, hi, page) : rangeStep(e, hi, step, lo, max, page);
|
|
90
165
|
if (nv == null) return;
|
|
91
|
-
e.preventDefault();
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
which === "lo" ? setLo(nv) : setHi(nv);
|
|
168
|
+
emit();
|
|
92
169
|
};
|
|
93
|
-
hLo.addEventListener("keydown", onKey("lo"));
|
|
170
|
+
hLo.addEventListener("keydown", onKey("lo"));
|
|
171
|
+
hHi.addEventListener("keydown", onKey("hi"));
|
|
94
172
|
|
|
95
173
|
return {
|
|
96
174
|
el: wrap,
|
|
97
|
-
set: (v: unknown) => {
|
|
175
|
+
set: (v: unknown) => {
|
|
176
|
+
if (Array.isArray(v)) {
|
|
177
|
+
const a = +v[0],
|
|
178
|
+
b = +v[1];
|
|
179
|
+
if (!Number.isFinite(a) || !Number.isFinite(b)) return;
|
|
180
|
+
put(a, b);
|
|
181
|
+
render();
|
|
182
|
+
}
|
|
183
|
+
},
|
|
98
184
|
get: () => [lo, hi],
|
|
99
185
|
range: [min, max],
|
|
100
186
|
};
|
|
101
187
|
}
|
|
102
188
|
|
|
103
189
|
registerControl("interval", createInterval);
|
|
104
|
-
|
|
@@ -1,105 +1,211 @@
|
|
|
1
1
|
// ── Monitor + FPS graph — live sparkline / readout. Lazy; registers both types.
|
|
2
|
-
import { el, txt, clamp, blade, json, registerControl } from "../shared.js";
|
|
2
|
+
import { el, txt, clamp, blade, json, registerControl, words } from "../shared.js";
|
|
3
3
|
import { fitCanvas, accentColor } from "../heavy.js";
|
|
4
4
|
import type { Meta } from "../schema.js";
|
|
5
5
|
|
|
6
6
|
// NaN means no sample yet: the pen lifts rather than drawing a false zero.
|
|
7
|
-
const strokeSeries = (
|
|
7
|
+
const strokeSeries = (
|
|
8
|
+
ctx: CanvasRenderingContext2D,
|
|
9
|
+
node: Element,
|
|
10
|
+
w: number,
|
|
11
|
+
h: number,
|
|
12
|
+
samples: number[],
|
|
13
|
+
start: number,
|
|
14
|
+
frac: (s: number) => number,
|
|
15
|
+
) => {
|
|
8
16
|
const N = samples.length;
|
|
9
17
|
ctx.clearRect(0, 0, w, h);
|
|
10
18
|
ctx.strokeStyle = accentColor(node);
|
|
11
|
-
ctx.lineWidth = 1.5;
|
|
19
|
+
ctx.lineWidth = 1.5;
|
|
20
|
+
ctx.lineJoin = "round";
|
|
21
|
+
ctx.beginPath();
|
|
12
22
|
let pen = false;
|
|
13
23
|
for (let k = 0; k < N; k++) {
|
|
14
24
|
const s = samples[(start + k) % N];
|
|
15
25
|
if (Number.isNaN(s)) continue;
|
|
16
|
-
const x = (k / (N - 1)) * w,
|
|
17
|
-
|
|
26
|
+
const x = (k / (N - 1)) * w,
|
|
27
|
+
y = h - clamp(frac(s), 0, 1) * h;
|
|
28
|
+
pen ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), (pen = true));
|
|
18
29
|
}
|
|
19
30
|
ctx.stroke();
|
|
20
31
|
};
|
|
21
32
|
|
|
22
33
|
// Manual, not onLive: these blades idle through the built-but-not-mounted window, which onLive would read as gone.
|
|
23
34
|
const watchReflow = (fn: () => void) => {
|
|
24
|
-
for (const t of ["resize", "
|
|
25
|
-
return () => {
|
|
35
|
+
for (const t of ["resize", "twk-reflow"]) window.addEventListener(t, fn);
|
|
36
|
+
return () => {
|
|
37
|
+
for (const t of ["resize", "twk-reflow"]) window.removeEventListener(t, fn);
|
|
38
|
+
};
|
|
26
39
|
};
|
|
27
40
|
|
|
28
41
|
// ── Monitor — a number as a sparkline or a readout, a string as a buffer of the last few values; read on an interval
|
|
29
42
|
// or, `perFrame` (the FPS graph), once per animation frame. ──
|
|
30
43
|
function createMonitor(meta: Meta, perFrame = false) {
|
|
31
44
|
const get: (now?: number) => unknown = typeof meta.get === "function" ? meta.get : () => meta.value;
|
|
32
|
-
|
|
33
|
-
|
|
45
|
+
// NaN would make setInterval a 0 ms busy-poll
|
|
46
|
+
const interval = Math.max(30, Number.isFinite(+meta.interval) ? +meta.interval : 200);
|
|
47
|
+
let probe: unknown;
|
|
48
|
+
try {
|
|
49
|
+
probe = get();
|
|
50
|
+
} catch {}
|
|
34
51
|
const isNum = typeof probe === "number";
|
|
35
52
|
const graph = meta.view === "graph" || (isNum && meta.graph !== false && meta.view !== "text" && meta.rows == null);
|
|
36
53
|
|
|
37
|
-
const wrap = el("div", "
|
|
38
|
-
const val = txt("span", "
|
|
39
|
-
wrap.append(txt("span", "
|
|
54
|
+
const wrap = el("div", "twk-fps twk-monitor");
|
|
55
|
+
const val = txt("span", "twk-fps-val", "—");
|
|
56
|
+
wrap.append(txt("span", "twk-fps-label", meta.label ?? words()("monitor", "Monitor")), val);
|
|
40
57
|
|
|
41
|
-
let timer = 0,
|
|
58
|
+
let timer = 0,
|
|
59
|
+
raf = 0,
|
|
60
|
+
unwatch = () => {},
|
|
61
|
+
wasConnected = false;
|
|
42
62
|
// toFixed throws past 100; a missing value reads as a dash.
|
|
43
63
|
const decimals = Number.isFinite(+meta.decimals) ? Math.min(20, Math.max(0, Math.floor(+meta.decimals))) : 2;
|
|
44
|
-
const fmt = (v: unknown) =>
|
|
64
|
+
const fmt = (v: unknown) =>
|
|
65
|
+
v == null
|
|
66
|
+
? "—"
|
|
67
|
+
: typeof v === "number"
|
|
68
|
+
? Number.isInteger(v) || !Number.isFinite(v)
|
|
69
|
+
? String(v)
|
|
70
|
+
: v.toFixed(decimals)
|
|
71
|
+
: typeof v === "object"
|
|
72
|
+
? json(v)
|
|
73
|
+
: String(v);
|
|
45
74
|
// Also the blade's `destroy`: a panel torn down before it ever connected can never self-stop below.
|
|
46
|
-
const stop = () => {
|
|
75
|
+
const stop = () => {
|
|
76
|
+
if (timer) clearInterval(timer);
|
|
77
|
+
timer = 0;
|
|
78
|
+
if (raf) cancelAnimationFrame(raf);
|
|
79
|
+
raf = 0;
|
|
80
|
+
unwatch();
|
|
81
|
+
};
|
|
47
82
|
// Never-mounted-yet idles the tick; only a mount-then-remove, or destroy(), stops it. The next frame is booked before
|
|
48
83
|
// the read, so a stop() inside the tick cancels it.
|
|
49
84
|
const poll = (fn: (v: unknown) => void) => {
|
|
50
85
|
const tick = (now?: number) => {
|
|
51
86
|
if (perFrame) raf = requestAnimationFrame(tick);
|
|
52
|
-
if (!wrap.isConnected) {
|
|
87
|
+
if (!wrap.isConnected) {
|
|
88
|
+
if (wasConnected) stop();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
53
91
|
wasConnected = true;
|
|
54
|
-
let v: unknown;
|
|
92
|
+
let v: unknown;
|
|
93
|
+
try {
|
|
94
|
+
v = get(now);
|
|
95
|
+
} catch {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
55
98
|
fn(v);
|
|
56
99
|
};
|
|
57
|
-
if (perFrame) raf = requestAnimationFrame(tick);
|
|
100
|
+
if (perFrame) raf = requestAnimationFrame(tick);
|
|
101
|
+
else timer = setInterval(tick, interval);
|
|
58
102
|
};
|
|
59
103
|
|
|
60
104
|
if (!graph && meta.rows) {
|
|
61
105
|
val.remove();
|
|
62
106
|
// A negative or NaN rows spun the trim loop forever.
|
|
63
107
|
const rows = Math.max(1, Math.floor(+meta.rows) || 1);
|
|
64
|
-
const buf = el("pre", "
|
|
108
|
+
const buf = el("pre", "twk-monitor-buffer");
|
|
109
|
+
buf.style.setProperty("--twk-monitor-rows", rows);
|
|
65
110
|
wrap.append(buf);
|
|
66
111
|
const lines: string[] = [];
|
|
67
|
-
poll((v) => {
|
|
112
|
+
poll((v) => {
|
|
113
|
+
lines.push(fmt(v));
|
|
114
|
+
while (lines.length > rows) lines.shift();
|
|
115
|
+
buf.textContent = lines.join("\n");
|
|
116
|
+
});
|
|
117
|
+
return blade(wrap, stop);
|
|
118
|
+
}
|
|
119
|
+
const readout = (v: unknown) => {
|
|
120
|
+
val.textContent = fmt(v);
|
|
121
|
+
};
|
|
122
|
+
if (!graph) {
|
|
123
|
+
poll(readout);
|
|
68
124
|
return blade(wrap, stop);
|
|
69
125
|
}
|
|
70
|
-
const readout = (v: unknown) => { val.textContent = fmt(v); };
|
|
71
|
-
if (!graph) { poll(readout); return blade(wrap, stop); }
|
|
72
126
|
|
|
73
|
-
const canvas = document.createElement("canvas");
|
|
127
|
+
const canvas = document.createElement("canvas");
|
|
128
|
+
canvas.className = "twk-fps-canvas";
|
|
74
129
|
wrap.append(canvas);
|
|
75
130
|
const ctx = canvas.getContext("2d");
|
|
76
131
|
// No 2D context (a headless DOM, a blocked canvas) degrades to the text readout.
|
|
77
|
-
if (!ctx) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
132
|
+
if (!ctx) {
|
|
133
|
+
poll(readout);
|
|
134
|
+
return blade(wrap, stop);
|
|
135
|
+
}
|
|
136
|
+
const N = 80,
|
|
137
|
+
samples: number[] = new Array(N).fill(NaN);
|
|
138
|
+
let idx = 0,
|
|
139
|
+
w = 0,
|
|
140
|
+
h = 0;
|
|
141
|
+
const onResize = () => {
|
|
142
|
+
[w, h] = fitCanvas(canvas, ctx, 2);
|
|
143
|
+
};
|
|
81
144
|
unwatch = watchReflow(onResize);
|
|
82
145
|
const draw = () => {
|
|
83
146
|
if (!w) onResize(); // measured 0 while detached: fit on the first live draw
|
|
84
147
|
if (!w) return;
|
|
85
|
-
let lo = meta.min,
|
|
148
|
+
let lo = meta.min,
|
|
149
|
+
hi = meta.max;
|
|
86
150
|
if (lo == null || hi == null) {
|
|
87
|
-
let mn = Infinity,
|
|
88
|
-
|
|
89
|
-
|
|
151
|
+
let mn = Infinity,
|
|
152
|
+
mx = -Infinity;
|
|
153
|
+
// an Infinity sample would blank the auto-range
|
|
154
|
+
for (const s of samples)
|
|
155
|
+
if (Number.isFinite(s)) {
|
|
156
|
+
if (s < mn) mn = s;
|
|
157
|
+
if (s > mx) mx = s;
|
|
158
|
+
}
|
|
159
|
+
if (mn === Infinity) {
|
|
160
|
+
mn = 0;
|
|
161
|
+
mx = 1;
|
|
162
|
+
} else if (mn === mx) {
|
|
163
|
+
mn -= 0.5;
|
|
164
|
+
mx += 0.5;
|
|
165
|
+
}
|
|
90
166
|
const pad = (mx - mn) * 0.1;
|
|
91
|
-
if (lo == null) lo = mn - pad;
|
|
167
|
+
if (lo == null) lo = mn - pad;
|
|
168
|
+
if (hi == null) hi = mx + pad;
|
|
92
169
|
}
|
|
93
|
-
const span =
|
|
170
|
+
const span = hi - lo || 1;
|
|
94
171
|
strokeSeries(ctx, wrap, w, h, samples, idx, (s) => (s - lo) / span);
|
|
95
172
|
};
|
|
96
|
-
|
|
173
|
+
// a non-finite sample shows in the readout, not the graph
|
|
174
|
+
poll((v) => {
|
|
175
|
+
readout(v);
|
|
176
|
+
if (typeof v === "number" && Number.isFinite(v)) {
|
|
177
|
+
samples[idx] = v;
|
|
178
|
+
idx = (idx + 1) % N;
|
|
179
|
+
draw();
|
|
180
|
+
}
|
|
181
|
+
});
|
|
97
182
|
return blade(wrap, stop);
|
|
98
183
|
}
|
|
99
184
|
|
|
100
185
|
// ── FPS graph — each frame's timestamp becomes the rate since the last (none on the first or for the probe), 0–120. ──
|
|
101
|
-
const frameRate = () => {
|
|
102
|
-
|
|
186
|
+
const frameRate = () => {
|
|
187
|
+
let last = 0;
|
|
188
|
+
return (now?: number) => {
|
|
189
|
+
if (!(now! > 0)) return undefined;
|
|
190
|
+
const fps = last ? 1000 / (now! - last) : undefined;
|
|
191
|
+
last = now!;
|
|
192
|
+
return fps;
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
// ??: an explicit "" label renders none
|
|
196
|
+
const createFps = (meta: Meta) =>
|
|
197
|
+
createMonitor(
|
|
198
|
+
{
|
|
199
|
+
...meta,
|
|
200
|
+
label: meta.label ?? words()("fps", "FPS"),
|
|
201
|
+
get: frameRate(),
|
|
202
|
+
view: "graph",
|
|
203
|
+
min: 0,
|
|
204
|
+
max: 120,
|
|
205
|
+
decimals: 0,
|
|
206
|
+
},
|
|
207
|
+
true,
|
|
208
|
+
);
|
|
103
209
|
|
|
104
210
|
registerControl("fpsgraph", createFps);
|
|
105
211
|
registerControl("monitor", (meta: Meta) => createMonitor(meta)); // the registry passes (meta, onChange), not perFrame
|