@zkmake/three-meter 0.1.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/LICENSE +21 -0
- package/README.md +116 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/mount-perf-hud-CduRNym5.js +1176 -0
- package/dist/mount-perf-hud-CduRNym5.js.map +1 -0
- package/dist/mount-perf-hud-Dw8KNQTt.d.ts +156 -0
- package/dist/performance-monitor-WGLWbxOZ.d.ts +111 -0
- package/dist/performance-monitor-qswCj9Nl.js +263 -0
- package/dist/performance-monitor-qswCj9Nl.js.map +1 -0
- package/dist/react.d.ts +39 -0
- package/dist/react.js +114 -0
- package/dist/react.js.map +1 -0
- package/dist/styles.css +300 -0
- package/dist/ui.d.ts +11 -0
- package/dist/ui.js +2 -0
- package/package.json +104 -0
|
@@ -0,0 +1,1176 @@
|
|
|
1
|
+
//#region src/ui/hud-settings.ts
|
|
2
|
+
const DEFAULT_STORAGE_KEY = "three-meter";
|
|
3
|
+
const DEFAULT_DIM = false;
|
|
4
|
+
const DEFAULT_NUMBERS = [
|
|
5
|
+
"fps",
|
|
6
|
+
"calls",
|
|
7
|
+
"cpu",
|
|
8
|
+
"gpu"
|
|
9
|
+
];
|
|
10
|
+
const DEFAULT_GRAPHS = [];
|
|
11
|
+
const TIMING_METRICS = /* @__PURE__ */ new Set([
|
|
12
|
+
"fps",
|
|
13
|
+
"cpu",
|
|
14
|
+
"gpu"
|
|
15
|
+
]);
|
|
16
|
+
const isTimingMetric = (value) => TIMING_METRICS.has(value);
|
|
17
|
+
var HudSettings = class {
|
|
18
|
+
storageKey;
|
|
19
|
+
defaults;
|
|
20
|
+
listeners = /* @__PURE__ */ new Set();
|
|
21
|
+
state;
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.storageKey = options.storageKey === void 0 ? DEFAULT_STORAGE_KEY : options.storageKey;
|
|
24
|
+
this.defaults = {
|
|
25
|
+
dim: options.defaults?.dim ?? DEFAULT_DIM,
|
|
26
|
+
graphs: options.defaults?.graphs ?? DEFAULT_GRAPHS,
|
|
27
|
+
numbers: options.defaults?.numbers ?? DEFAULT_NUMBERS
|
|
28
|
+
};
|
|
29
|
+
this.state = this.load();
|
|
30
|
+
}
|
|
31
|
+
get selection() {
|
|
32
|
+
return this.state;
|
|
33
|
+
}
|
|
34
|
+
get dim() {
|
|
35
|
+
return this.state.dim;
|
|
36
|
+
}
|
|
37
|
+
setDim(enabled) {
|
|
38
|
+
this.state.dim = enabled;
|
|
39
|
+
this.persistAndNotify();
|
|
40
|
+
}
|
|
41
|
+
isNumberEnabled(key) {
|
|
42
|
+
return this.state.numbers.has(key);
|
|
43
|
+
}
|
|
44
|
+
isGraphEnabled(metric) {
|
|
45
|
+
return this.state.graphs.has(metric);
|
|
46
|
+
}
|
|
47
|
+
setNumberEnabled(key, enabled) {
|
|
48
|
+
if (enabled) this.state.numbers.add(key);
|
|
49
|
+
else this.state.numbers.delete(key);
|
|
50
|
+
this.persistAndNotify();
|
|
51
|
+
}
|
|
52
|
+
setGraphEnabled(metric, enabled) {
|
|
53
|
+
if (enabled) this.state.graphs.add(metric);
|
|
54
|
+
else this.state.graphs.delete(metric);
|
|
55
|
+
this.persistAndNotify();
|
|
56
|
+
}
|
|
57
|
+
subscribe(listener) {
|
|
58
|
+
this.listeners.add(listener);
|
|
59
|
+
return () => {
|
|
60
|
+
this.listeners.delete(listener);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
load() {
|
|
64
|
+
const fallback = () => ({
|
|
65
|
+
dim: this.defaults.dim,
|
|
66
|
+
graphs: new Set(this.defaults.graphs),
|
|
67
|
+
numbers: new Set(this.defaults.numbers)
|
|
68
|
+
});
|
|
69
|
+
if (this.storageKey === null) return fallback();
|
|
70
|
+
try {
|
|
71
|
+
const raw = localStorage.getItem(this.storageKey);
|
|
72
|
+
if (!raw) return fallback();
|
|
73
|
+
const parsed = JSON.parse(raw);
|
|
74
|
+
return {
|
|
75
|
+
dim: parsed.dim ?? this.defaults.dim,
|
|
76
|
+
graphs: new Set((parsed.graphs ?? this.defaults.graphs).filter(isTimingMetric)),
|
|
77
|
+
numbers: new Set(parsed.numbers ?? this.defaults.numbers)
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
return fallback();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
persistAndNotify() {
|
|
84
|
+
if (this.storageKey !== null) try {
|
|
85
|
+
localStorage.setItem(this.storageKey, JSON.stringify({
|
|
86
|
+
dim: this.state.dim,
|
|
87
|
+
graphs: [...this.state.graphs],
|
|
88
|
+
numbers: [...this.state.numbers]
|
|
89
|
+
}));
|
|
90
|
+
} catch {}
|
|
91
|
+
for (const listener of this.listeners) listener();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/ui/dock-panel.ts
|
|
96
|
+
/**
|
|
97
|
+
* Dock a fixed-position panel to a screen edge. Dragging the handle snaps it
|
|
98
|
+
* to the nearest edge on drop; the edge and the offset along it persist under
|
|
99
|
+
* `<storageKey>:placement`. A stored box whose centre sits outside the current
|
|
100
|
+
* viewport is re-docked to whichever edge that box is closest to.
|
|
101
|
+
*/
|
|
102
|
+
const INSET = 10;
|
|
103
|
+
/** Room for the stacked control discs on the inward side of the panel. */
|
|
104
|
+
const SIDE_CLEARANCE = 26;
|
|
105
|
+
const EDGES = [
|
|
106
|
+
"left",
|
|
107
|
+
"right",
|
|
108
|
+
"top",
|
|
109
|
+
"bottom"
|
|
110
|
+
];
|
|
111
|
+
const viewportBox = () => ({
|
|
112
|
+
height: window.innerHeight,
|
|
113
|
+
left: 0,
|
|
114
|
+
top: 0,
|
|
115
|
+
width: window.innerWidth
|
|
116
|
+
});
|
|
117
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
118
|
+
const panelBox = (element) => {
|
|
119
|
+
const rect = element.getBoundingClientRect();
|
|
120
|
+
return {
|
|
121
|
+
height: rect.height || element.offsetHeight,
|
|
122
|
+
left: rect.left,
|
|
123
|
+
top: rect.top,
|
|
124
|
+
width: rect.width || element.offsetWidth
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
const clampAlong = (edge, along, view, panel) => {
|
|
128
|
+
if (edge === "left" || edge === "right") return clamp(along, INSET, Math.max(INSET, view.height - panel.height - INSET));
|
|
129
|
+
return clamp(along, INSET, Math.max(INSET, view.width - panel.width - INSET - SIDE_CLEARANCE));
|
|
130
|
+
};
|
|
131
|
+
const ghostBox = (placement, panel, view) => {
|
|
132
|
+
switch (placement.edge) {
|
|
133
|
+
case "left": return {
|
|
134
|
+
height: panel.height,
|
|
135
|
+
left: INSET,
|
|
136
|
+
top: placement.along,
|
|
137
|
+
width: panel.width
|
|
138
|
+
};
|
|
139
|
+
case "right": return {
|
|
140
|
+
height: panel.height,
|
|
141
|
+
left: view.width - INSET - panel.width,
|
|
142
|
+
top: placement.along,
|
|
143
|
+
width: panel.width
|
|
144
|
+
};
|
|
145
|
+
case "top": return {
|
|
146
|
+
height: panel.height,
|
|
147
|
+
left: placement.along,
|
|
148
|
+
top: INSET,
|
|
149
|
+
width: panel.width
|
|
150
|
+
};
|
|
151
|
+
case "bottom": return {
|
|
152
|
+
height: panel.height,
|
|
153
|
+
left: placement.along,
|
|
154
|
+
top: view.height - INSET - panel.height,
|
|
155
|
+
width: panel.width
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const centerOutside = (box, view) => {
|
|
160
|
+
const x = box.left + box.width / 2;
|
|
161
|
+
const y = box.top + box.height / 2;
|
|
162
|
+
return x < 0 || y < 0 || x > view.width || y > view.height;
|
|
163
|
+
};
|
|
164
|
+
const snapBoxToEdge = (box, view) => {
|
|
165
|
+
const distances = {
|
|
166
|
+
bottom: view.height - (box.top + box.height),
|
|
167
|
+
left: box.left,
|
|
168
|
+
right: view.width - (box.left + box.width),
|
|
169
|
+
top: box.top
|
|
170
|
+
};
|
|
171
|
+
let edge = "left";
|
|
172
|
+
let nearest = Number.POSITIVE_INFINITY;
|
|
173
|
+
for (const next of EDGES) if (distances[next] < nearest) {
|
|
174
|
+
nearest = distances[next];
|
|
175
|
+
edge = next;
|
|
176
|
+
}
|
|
177
|
+
const along = edge === "left" || edge === "right" ? box.top : box.left;
|
|
178
|
+
return {
|
|
179
|
+
along: Math.round(clampAlong(edge, along, view, box)),
|
|
180
|
+
edge
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
const initialPlacement = (preset, view, panel) => {
|
|
184
|
+
const { edge } = preset;
|
|
185
|
+
const vertical = edge === "left" || edge === "right";
|
|
186
|
+
const span = vertical ? view.height : view.width;
|
|
187
|
+
const size = vertical ? panel.height : panel.width;
|
|
188
|
+
const along = preset.align === "start" ? 0 : preset.align === "end" ? span - size : (span - size) / 2;
|
|
189
|
+
return {
|
|
190
|
+
along: Math.round(clampAlong(edge, along, view, panel)),
|
|
191
|
+
edge
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
const isEdge = (value) => typeof value === "string" && EDGES.includes(value);
|
|
195
|
+
const readPlacement = (key) => {
|
|
196
|
+
if (key === null) return null;
|
|
197
|
+
try {
|
|
198
|
+
const raw = localStorage.getItem(key);
|
|
199
|
+
if (!raw) return null;
|
|
200
|
+
const parsed = JSON.parse(raw);
|
|
201
|
+
if (typeof parsed.along !== "number" || !Number.isFinite(parsed.along) || !isEdge(parsed.edge)) return null;
|
|
202
|
+
return {
|
|
203
|
+
along: parsed.along,
|
|
204
|
+
edge: parsed.edge
|
|
205
|
+
};
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const writePlacement = (key, placement) => {
|
|
211
|
+
if (key === null) return;
|
|
212
|
+
try {
|
|
213
|
+
localStorage.setItem(key, JSON.stringify(placement));
|
|
214
|
+
} catch {}
|
|
215
|
+
};
|
|
216
|
+
const resolvePlacement = (stored, preset, panel, view) => {
|
|
217
|
+
if (!stored) return initialPlacement(preset, view, panel);
|
|
218
|
+
const ghost = ghostBox(stored, panel, view);
|
|
219
|
+
if (centerOutside(ghost, view)) return snapBoxToEdge(ghost, view);
|
|
220
|
+
return {
|
|
221
|
+
along: Math.round(clampAlong(stored.edge, stored.along, view, panel)),
|
|
222
|
+
edge: stored.edge
|
|
223
|
+
};
|
|
224
|
+
};
|
|
225
|
+
const paintPlacement = (element, placement) => {
|
|
226
|
+
element.dataset.edge = placement.edge;
|
|
227
|
+
element.style.left = "";
|
|
228
|
+
element.style.right = "";
|
|
229
|
+
element.style.top = "";
|
|
230
|
+
element.style.bottom = "";
|
|
231
|
+
element.style.transform = "";
|
|
232
|
+
switch (placement.edge) {
|
|
233
|
+
case "left":
|
|
234
|
+
element.style.left = `${INSET}px`;
|
|
235
|
+
element.style.top = `${placement.along}px`;
|
|
236
|
+
break;
|
|
237
|
+
case "right":
|
|
238
|
+
element.style.right = `${INSET}px`;
|
|
239
|
+
element.style.top = `${placement.along}px`;
|
|
240
|
+
break;
|
|
241
|
+
case "top":
|
|
242
|
+
element.style.top = `${INSET}px`;
|
|
243
|
+
element.style.left = `${placement.along}px`;
|
|
244
|
+
break;
|
|
245
|
+
case "bottom":
|
|
246
|
+
element.style.bottom = `${INSET}px`;
|
|
247
|
+
element.style.left = `${placement.along}px`;
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
/**
|
|
251
|
+
* Dock `element` from storage (or the left centre), re-dock on resize, and
|
|
252
|
+
* snap to the nearest edge when the handle drops.
|
|
253
|
+
*/
|
|
254
|
+
const dockPanel = (element, options = {}) => {
|
|
255
|
+
const baseKey = options.storageKey === void 0 ? DEFAULT_STORAGE_KEY : options.storageKey;
|
|
256
|
+
const key = baseKey === null ? null : `${baseKey}:placement`;
|
|
257
|
+
const handle = options.handle ?? null;
|
|
258
|
+
const preset = options.defaultPlacement ?? { edge: "left" };
|
|
259
|
+
const apply = () => {
|
|
260
|
+
const stored = readPlacement(key);
|
|
261
|
+
const view = viewportBox();
|
|
262
|
+
if (element.classList.contains("is-dragging")) return stored ?? initialPlacement(preset, view, panelBox(element));
|
|
263
|
+
const panel = panelBox(element);
|
|
264
|
+
if (panel.width === 0 || panel.height === 0) return stored ?? initialPlacement(preset, view, panel);
|
|
265
|
+
const next = resolvePlacement(stored, preset, panel, view);
|
|
266
|
+
paintPlacement(element, next);
|
|
267
|
+
if (!stored || stored.edge !== next.edge || stored.along !== next.along) writePlacement(key, next);
|
|
268
|
+
return next;
|
|
269
|
+
};
|
|
270
|
+
apply();
|
|
271
|
+
requestAnimationFrame(apply);
|
|
272
|
+
const onResize = () => apply();
|
|
273
|
+
window.addEventListener("resize", onResize);
|
|
274
|
+
const resizeObserver = new ResizeObserver(() => apply());
|
|
275
|
+
resizeObserver.observe(element);
|
|
276
|
+
const disposeBase = () => {
|
|
277
|
+
window.removeEventListener("resize", onResize);
|
|
278
|
+
resizeObserver.disconnect();
|
|
279
|
+
};
|
|
280
|
+
if (!handle) return {
|
|
281
|
+
dispose: disposeBase,
|
|
282
|
+
refresh: apply
|
|
283
|
+
};
|
|
284
|
+
let pointerId = null;
|
|
285
|
+
let originX = 0;
|
|
286
|
+
let originY = 0;
|
|
287
|
+
let grabX = 0;
|
|
288
|
+
let grabY = 0;
|
|
289
|
+
let dragging = false;
|
|
290
|
+
const finish = (event) => {
|
|
291
|
+
if (pointerId !== event.pointerId) return;
|
|
292
|
+
if (dragging) {
|
|
293
|
+
const next = snapBoxToEdge(panelBox(element), viewportBox());
|
|
294
|
+
paintPlacement(element, next);
|
|
295
|
+
writePlacement(key, next);
|
|
296
|
+
}
|
|
297
|
+
dragging = false;
|
|
298
|
+
pointerId = null;
|
|
299
|
+
element.classList.remove("is-dragging");
|
|
300
|
+
try {
|
|
301
|
+
handle.releasePointerCapture(event.pointerId);
|
|
302
|
+
} catch {}
|
|
303
|
+
};
|
|
304
|
+
const onPointerDown = (event) => {
|
|
305
|
+
if (event.button !== 0) return;
|
|
306
|
+
const box = panelBox(element);
|
|
307
|
+
pointerId = event.pointerId;
|
|
308
|
+
originX = event.clientX;
|
|
309
|
+
originY = event.clientY;
|
|
310
|
+
grabX = event.clientX - box.left;
|
|
311
|
+
grabY = event.clientY - box.top;
|
|
312
|
+
dragging = false;
|
|
313
|
+
handle.setPointerCapture(event.pointerId);
|
|
314
|
+
};
|
|
315
|
+
const onPointerMove = (event) => {
|
|
316
|
+
if (pointerId !== event.pointerId) return;
|
|
317
|
+
const dx = event.clientX - originX;
|
|
318
|
+
const dy = event.clientY - originY;
|
|
319
|
+
if (!dragging && dx * dx + dy * dy < 16) return;
|
|
320
|
+
dragging = true;
|
|
321
|
+
element.classList.add("is-dragging");
|
|
322
|
+
element.style.right = "";
|
|
323
|
+
element.style.bottom = "";
|
|
324
|
+
element.style.transform = "";
|
|
325
|
+
element.style.left = `${event.clientX - grabX}px`;
|
|
326
|
+
element.style.top = `${event.clientY - grabY}px`;
|
|
327
|
+
};
|
|
328
|
+
handle.addEventListener("pointerdown", onPointerDown);
|
|
329
|
+
handle.addEventListener("pointermove", onPointerMove);
|
|
330
|
+
handle.addEventListener("pointerup", finish);
|
|
331
|
+
handle.addEventListener("pointercancel", finish);
|
|
332
|
+
return {
|
|
333
|
+
dispose: () => {
|
|
334
|
+
disposeBase();
|
|
335
|
+
handle.removeEventListener("pointerdown", onPointerDown);
|
|
336
|
+
handle.removeEventListener("pointermove", onPointerMove);
|
|
337
|
+
handle.removeEventListener("pointerup", finish);
|
|
338
|
+
handle.removeEventListener("pointercancel", finish);
|
|
339
|
+
},
|
|
340
|
+
refresh: apply
|
|
341
|
+
};
|
|
342
|
+
};
|
|
343
|
+
//#endregion
|
|
344
|
+
//#region src/ui/icons.ts
|
|
345
|
+
const ICON_MARKUP = {
|
|
346
|
+
blend: "<circle cx=\"9\" cy=\"9\" r=\"7\"/><circle cx=\"15\" cy=\"15\" r=\"7\"/>",
|
|
347
|
+
grip: "<circle cx=\"9\" cy=\"12\" r=\"1\"/><circle cx=\"9\" cy=\"5\" r=\"1\"/><circle cx=\"9\" cy=\"19\" r=\"1\"/><circle cx=\"15\" cy=\"12\" r=\"1\"/><circle cx=\"15\" cy=\"5\" r=\"1\"/><circle cx=\"15\" cy=\"19\" r=\"1\"/>",
|
|
348
|
+
sliders: "<line x1=\"21\" x2=\"14\" y1=\"4\" y2=\"4\"/><line x1=\"10\" x2=\"3\" y1=\"4\" y2=\"4\"/><line x1=\"21\" x2=\"12\" y1=\"12\" y2=\"12\"/><line x1=\"8\" x2=\"3\" y1=\"12\" y2=\"12\"/><line x1=\"21\" x2=\"16\" y1=\"20\" y2=\"20\"/><line x1=\"12\" x2=\"3\" y1=\"20\" y2=\"20\"/><line x1=\"14\" x2=\"14\" y1=\"2\" y2=\"6\"/><line x1=\"8\" x2=\"8\" y1=\"10\" y2=\"14\"/><line x1=\"16\" x2=\"16\" y1=\"18\" y2=\"22\"/>"
|
|
349
|
+
};
|
|
350
|
+
const createIcon = (name, className) => {
|
|
351
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
352
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
353
|
+
svg.setAttribute("fill", "none");
|
|
354
|
+
svg.setAttribute("stroke", "currentColor");
|
|
355
|
+
svg.setAttribute("stroke-width", "2");
|
|
356
|
+
svg.setAttribute("stroke-linecap", "round");
|
|
357
|
+
svg.setAttribute("stroke-linejoin", "round");
|
|
358
|
+
svg.setAttribute("aria-hidden", "true");
|
|
359
|
+
svg.setAttribute("class", className);
|
|
360
|
+
svg.innerHTML = ICON_MARKUP[name];
|
|
361
|
+
return svg;
|
|
362
|
+
};
|
|
363
|
+
//#endregion
|
|
364
|
+
//#region src/ui/sparkline.ts
|
|
365
|
+
function drawSparkline(ctx, width, height, values, style) {
|
|
366
|
+
ctx.clearRect(0, 0, width, height);
|
|
367
|
+
if (values.length < 2) return;
|
|
368
|
+
let max = 0;
|
|
369
|
+
for (const value of values) if (value > max) max = value;
|
|
370
|
+
const range = max * 1.1 || 1;
|
|
371
|
+
const stepX = width / (values.length - 1);
|
|
372
|
+
const toY = (value) => height - value / range * height;
|
|
373
|
+
ctx.beginPath();
|
|
374
|
+
ctx.moveTo(0, toY(values[0]));
|
|
375
|
+
for (let index = 1; index < values.length; index += 1) ctx.lineTo(index * stepX, toY(values[index]));
|
|
376
|
+
ctx.lineWidth = 1;
|
|
377
|
+
ctx.strokeStyle = style.stroke;
|
|
378
|
+
ctx.stroke();
|
|
379
|
+
ctx.lineTo(width, height);
|
|
380
|
+
ctx.lineTo(0, height);
|
|
381
|
+
ctx.closePath();
|
|
382
|
+
ctx.fillStyle = style.fill;
|
|
383
|
+
ctx.fill();
|
|
384
|
+
}
|
|
385
|
+
//#endregion
|
|
386
|
+
//#region src/ui/performance-view.ts
|
|
387
|
+
const TIMINGS = [
|
|
388
|
+
{
|
|
389
|
+
format: (value) => Math.round(value).toString(),
|
|
390
|
+
label: "FPS",
|
|
391
|
+
metric: "fps",
|
|
392
|
+
style: {
|
|
393
|
+
fill: "rgba(74, 222, 128, 0.15)",
|
|
394
|
+
stroke: "#4ade80"
|
|
395
|
+
},
|
|
396
|
+
unit: ""
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
format: (value) => value.toFixed(1),
|
|
400
|
+
label: "CPU",
|
|
401
|
+
metric: "cpu",
|
|
402
|
+
style: {
|
|
403
|
+
fill: "rgba(96, 165, 250, 0.15)",
|
|
404
|
+
stroke: "#60a5fa"
|
|
405
|
+
},
|
|
406
|
+
unit: "ms"
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
format: (value) => value.toFixed(1),
|
|
410
|
+
label: "GPU",
|
|
411
|
+
metric: "gpu",
|
|
412
|
+
style: {
|
|
413
|
+
fill: "rgba(244, 114, 182, 0.15)",
|
|
414
|
+
stroke: "#f472b6"
|
|
415
|
+
},
|
|
416
|
+
unit: "ms"
|
|
417
|
+
}
|
|
418
|
+
];
|
|
419
|
+
const formatCount = (value) => value.toLocaleString("en-US");
|
|
420
|
+
const readTiming = (sample, metric) => {
|
|
421
|
+
switch (metric) {
|
|
422
|
+
case "fps": return sample.fps;
|
|
423
|
+
case "cpu": return sample.cpu;
|
|
424
|
+
case "gpu": return sample.gpu.ms;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
const isTimingAvailable = (sample, metric) => metric !== "gpu" || sample.gpu.available;
|
|
428
|
+
const NUMBERS = [
|
|
429
|
+
{
|
|
430
|
+
key: "fps",
|
|
431
|
+
label: "FPS",
|
|
432
|
+
read: (sample) => Math.round(sample.fps).toString()
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
key: "calls",
|
|
436
|
+
label: "Calls",
|
|
437
|
+
read: (sample) => formatCount(sample.render.calls)
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
key: "cpu",
|
|
441
|
+
label: "CPU",
|
|
442
|
+
read: (sample) => sample.cpu.toFixed(1)
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
key: "gpu",
|
|
446
|
+
label: "GPU",
|
|
447
|
+
read: (sample) => sample.gpu.available ? sample.gpu.ms.toFixed(1) : "—"
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
key: "triangles",
|
|
451
|
+
label: "Triangles",
|
|
452
|
+
read: (sample) => formatCount(sample.render.triangles)
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
key: "lines",
|
|
456
|
+
label: "Lines",
|
|
457
|
+
read: (sample) => formatCount(sample.render.lines)
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
key: "points",
|
|
461
|
+
label: "Points",
|
|
462
|
+
read: (sample) => formatCount(sample.render.points)
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
key: "passes",
|
|
466
|
+
label: "Render passes",
|
|
467
|
+
read: (sample) => formatCount(sample.render.passes)
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
key: "geometries",
|
|
471
|
+
label: "Geometries",
|
|
472
|
+
read: (sample) => formatCount(sample.resources.geometries)
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
key: "textures",
|
|
476
|
+
label: "Textures",
|
|
477
|
+
read: (sample) => formatCount(sample.resources.textures)
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
key: "shaders",
|
|
481
|
+
label: "Shaders",
|
|
482
|
+
read: (sample) => formatCount(sample.resources.programs)
|
|
483
|
+
}
|
|
484
|
+
];
|
|
485
|
+
/**
|
|
486
|
+
* Host-agnostic widget that paints {@link Sample}s. Mount {@link element}
|
|
487
|
+
* anywhere; the view drives its own throttled repaint. Compact is the floating
|
|
488
|
+
* HUD; full is the checkbox list that chooses what compact shows. Styles are
|
|
489
|
+
* not injected here. Call `injectStyles()` or use `mountPerfHud`.
|
|
490
|
+
*/
|
|
491
|
+
var PerformanceView = class {
|
|
492
|
+
element;
|
|
493
|
+
settings;
|
|
494
|
+
monitor;
|
|
495
|
+
minIntervalMs;
|
|
496
|
+
mode;
|
|
497
|
+
graphValueEls = /* @__PURE__ */ new Map();
|
|
498
|
+
graphCanvases = /* @__PURE__ */ new Map();
|
|
499
|
+
graphCheckboxes = /* @__PURE__ */ new Map();
|
|
500
|
+
statValueEls = /* @__PURE__ */ new Map();
|
|
501
|
+
statCheckboxes = /* @__PURE__ */ new Map();
|
|
502
|
+
dimCheckbox;
|
|
503
|
+
hudGraphsEl;
|
|
504
|
+
hudGridEl;
|
|
505
|
+
hudGraphs = /* @__PURE__ */ new Map();
|
|
506
|
+
hudNumberEls = /* @__PURE__ */ new Map();
|
|
507
|
+
rafId = null;
|
|
508
|
+
lastPaintAt = 0;
|
|
509
|
+
resizeObserver;
|
|
510
|
+
unsubscribe;
|
|
511
|
+
constructor(options) {
|
|
512
|
+
this.monitor = options.monitor;
|
|
513
|
+
this.settings = options.settings ?? new HudSettings();
|
|
514
|
+
this.mode = options.mode ?? "full";
|
|
515
|
+
this.minIntervalMs = 1e3 / (options.refreshHz ?? 10);
|
|
516
|
+
this.element = document.createElement("div");
|
|
517
|
+
this.element.className = "perf-monitor";
|
|
518
|
+
this.applyModeClass();
|
|
519
|
+
this.resizeObserver = new ResizeObserver(() => this.resizeCanvases());
|
|
520
|
+
this.build();
|
|
521
|
+
this.rebuildHud();
|
|
522
|
+
this.unsubscribe = this.settings.subscribe(() => this.onSelectionChanged());
|
|
523
|
+
}
|
|
524
|
+
start() {
|
|
525
|
+
if (this.rafId === null) this.rafId = requestAnimationFrame(this.frame);
|
|
526
|
+
}
|
|
527
|
+
stop() {
|
|
528
|
+
if (this.rafId !== null) {
|
|
529
|
+
cancelAnimationFrame(this.rafId);
|
|
530
|
+
this.rafId = null;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
setMode(mode) {
|
|
534
|
+
if (mode === this.mode) return;
|
|
535
|
+
this.mode = mode;
|
|
536
|
+
this.applyModeClass();
|
|
537
|
+
this.resizeCanvases();
|
|
538
|
+
}
|
|
539
|
+
getMode() {
|
|
540
|
+
return this.mode;
|
|
541
|
+
}
|
|
542
|
+
dispose() {
|
|
543
|
+
this.stop();
|
|
544
|
+
this.unsubscribe();
|
|
545
|
+
this.resizeObserver.disconnect();
|
|
546
|
+
this.element.remove();
|
|
547
|
+
}
|
|
548
|
+
frame = (now) => {
|
|
549
|
+
this.rafId = requestAnimationFrame(this.frame);
|
|
550
|
+
if (now - this.lastPaintAt < this.minIntervalMs) return;
|
|
551
|
+
this.lastPaintAt = now;
|
|
552
|
+
this.render();
|
|
553
|
+
};
|
|
554
|
+
render() {
|
|
555
|
+
const sample = this.monitor.getSample();
|
|
556
|
+
if (this.mode === "compact") {
|
|
557
|
+
this.renderHud(sample);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
for (const config of TIMINGS) {
|
|
561
|
+
const available = isTimingAvailable(sample, config.metric);
|
|
562
|
+
const valueEl = this.graphValueEls.get(config.metric);
|
|
563
|
+
valueEl.textContent = available ? this.formatTiming(config, readTiming(sample, config.metric)) : "unavailable";
|
|
564
|
+
const graph = this.graphCanvases.get(config.metric);
|
|
565
|
+
drawSparkline(graph.ctx, graph.width, graph.height, available ? this.monitor.getHistory(config.metric) : [], config.style);
|
|
566
|
+
}
|
|
567
|
+
for (const config of NUMBERS) this.statValueEls.get(config.key).textContent = config.read(sample);
|
|
568
|
+
}
|
|
569
|
+
renderHud(sample) {
|
|
570
|
+
for (const [key, element] of this.hudNumberEls) {
|
|
571
|
+
const config = NUMBERS.find((number) => number.key === key);
|
|
572
|
+
if (config) element.textContent = config.read(sample);
|
|
573
|
+
}
|
|
574
|
+
for (const [metric, graph] of this.hudGraphs) {
|
|
575
|
+
const config = TIMINGS.find((timing) => timing.metric === metric);
|
|
576
|
+
const available = isTimingAvailable(sample, metric);
|
|
577
|
+
graph.valueEl.textContent = available ? this.formatTiming(config, readTiming(sample, metric)) : "—";
|
|
578
|
+
drawSparkline(graph.ctx, graph.width, graph.height, available ? this.monitor.getHistory(metric) : [], config.style);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
formatTiming(config, value) {
|
|
582
|
+
return `${config.format(value)}${config.unit ? ` ${config.unit}` : ""}`;
|
|
583
|
+
}
|
|
584
|
+
build() {
|
|
585
|
+
const hud = document.createElement("div");
|
|
586
|
+
hud.className = "perf-monitor__hud";
|
|
587
|
+
this.hudGraphsEl = document.createElement("div");
|
|
588
|
+
this.hudGraphsEl.className = "perf-monitor__hud-graphs";
|
|
589
|
+
this.hudGridEl = document.createElement("div");
|
|
590
|
+
this.hudGridEl.className = "perf-monitor__hud-grid";
|
|
591
|
+
hud.append(this.hudGraphsEl, this.hudGridEl);
|
|
592
|
+
const options = document.createElement("div");
|
|
593
|
+
options.className = "perf-monitor__section perf-monitor__options";
|
|
594
|
+
const dimRow = document.createElement("label");
|
|
595
|
+
dimRow.className = "perf-monitor__row";
|
|
596
|
+
this.dimCheckbox = this.buildCheckbox(this.settings.dim, "Dim the performance panel when the pointer leaves", (on) => this.settings.setDim(on));
|
|
597
|
+
const dimLabel = document.createElement("span");
|
|
598
|
+
dimLabel.className = "perf-monitor__label";
|
|
599
|
+
dimLabel.textContent = "dim on leave";
|
|
600
|
+
dimRow.append(this.dimCheckbox, createIcon("blend", "perf-monitor__icon"), dimLabel);
|
|
601
|
+
options.append(dimRow);
|
|
602
|
+
const graphs = document.createElement("div");
|
|
603
|
+
graphs.className = "perf-monitor__graphs";
|
|
604
|
+
for (const config of TIMINGS) {
|
|
605
|
+
const graph = document.createElement("div");
|
|
606
|
+
graph.className = "perf-monitor__graph";
|
|
607
|
+
const canvas = document.createElement("canvas");
|
|
608
|
+
canvas.className = "perf-monitor__canvas";
|
|
609
|
+
const overlay = document.createElement("div");
|
|
610
|
+
overlay.className = "perf-monitor__graph-overlay";
|
|
611
|
+
const head = document.createElement("label");
|
|
612
|
+
head.className = "perf-monitor__graph-head";
|
|
613
|
+
const checkbox = this.buildCheckbox(this.settings.isGraphEnabled(config.metric), `Show ${config.label} graph in HUD`, (on) => this.settings.setGraphEnabled(config.metric, on));
|
|
614
|
+
this.graphCheckboxes.set(config.metric, checkbox);
|
|
615
|
+
const label = document.createElement("span");
|
|
616
|
+
label.className = "perf-monitor__graph-label";
|
|
617
|
+
label.textContent = config.label;
|
|
618
|
+
const value = document.createElement("span");
|
|
619
|
+
value.className = "perf-monitor__graph-value";
|
|
620
|
+
value.textContent = "—";
|
|
621
|
+
this.graphValueEls.set(config.metric, value);
|
|
622
|
+
head.append(checkbox, label);
|
|
623
|
+
overlay.append(head, value);
|
|
624
|
+
graph.append(canvas, overlay);
|
|
625
|
+
graphs.append(graph);
|
|
626
|
+
const ctx = canvas.getContext("2d");
|
|
627
|
+
if (ctx) {
|
|
628
|
+
this.graphCanvases.set(config.metric, {
|
|
629
|
+
canvas,
|
|
630
|
+
ctx,
|
|
631
|
+
height: 0,
|
|
632
|
+
width: 0
|
|
633
|
+
});
|
|
634
|
+
this.resizeObserver.observe(graph);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const stats = document.createElement("div");
|
|
638
|
+
stats.className = "perf-monitor__section";
|
|
639
|
+
for (const config of NUMBERS) {
|
|
640
|
+
const rowEl = document.createElement("label");
|
|
641
|
+
rowEl.className = "perf-monitor__row";
|
|
642
|
+
const checkbox = this.buildCheckbox(this.settings.isNumberEnabled(config.key), `Show ${config.label} in HUD`, (on) => this.settings.setNumberEnabled(config.key, on));
|
|
643
|
+
this.statCheckboxes.set(config.key, checkbox);
|
|
644
|
+
const label = document.createElement("span");
|
|
645
|
+
label.className = "perf-monitor__label";
|
|
646
|
+
label.textContent = config.label;
|
|
647
|
+
const value = document.createElement("span");
|
|
648
|
+
value.className = "perf-monitor__value";
|
|
649
|
+
value.textContent = "—";
|
|
650
|
+
this.statValueEls.set(config.key, value);
|
|
651
|
+
rowEl.append(checkbox, label, value);
|
|
652
|
+
stats.append(rowEl);
|
|
653
|
+
}
|
|
654
|
+
this.element.append(hud, options, graphs, stats);
|
|
655
|
+
}
|
|
656
|
+
rebuildHud() {
|
|
657
|
+
const { selection } = this.settings;
|
|
658
|
+
for (const graph of this.hudGraphs.values()) this.resizeObserver.unobserve(graph.canvas.parentElement ?? graph.canvas);
|
|
659
|
+
this.hudGraphs.clear();
|
|
660
|
+
this.hudNumberEls.clear();
|
|
661
|
+
this.hudGraphsEl.replaceChildren();
|
|
662
|
+
this.hudGridEl.replaceChildren();
|
|
663
|
+
for (const config of TIMINGS) {
|
|
664
|
+
if (!selection.graphs.has(config.metric)) continue;
|
|
665
|
+
const row = document.createElement("div");
|
|
666
|
+
row.className = "perf-monitor__hud-graph";
|
|
667
|
+
const canvas = document.createElement("canvas");
|
|
668
|
+
canvas.className = "perf-monitor__canvas";
|
|
669
|
+
const overlay = document.createElement("div");
|
|
670
|
+
overlay.className = "perf-monitor__graph-overlay";
|
|
671
|
+
const label = document.createElement("span");
|
|
672
|
+
label.className = "perf-monitor__graph-label";
|
|
673
|
+
label.textContent = config.label;
|
|
674
|
+
const value = document.createElement("span");
|
|
675
|
+
value.className = "perf-monitor__graph-value";
|
|
676
|
+
value.textContent = "—";
|
|
677
|
+
overlay.append(label, value);
|
|
678
|
+
row.append(canvas, overlay);
|
|
679
|
+
this.hudGraphsEl.append(row);
|
|
680
|
+
const ctx = canvas.getContext("2d");
|
|
681
|
+
if (ctx) {
|
|
682
|
+
this.hudGraphs.set(config.metric, {
|
|
683
|
+
canvas,
|
|
684
|
+
ctx,
|
|
685
|
+
height: 0,
|
|
686
|
+
valueEl: value,
|
|
687
|
+
width: 0
|
|
688
|
+
});
|
|
689
|
+
this.resizeObserver.observe(row);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
for (const config of NUMBERS) {
|
|
693
|
+
if (!selection.numbers.has(config.key)) continue;
|
|
694
|
+
const item = document.createElement("div");
|
|
695
|
+
item.className = "perf-monitor__hud-item";
|
|
696
|
+
const label = document.createElement("span");
|
|
697
|
+
label.className = "perf-monitor__hud-label";
|
|
698
|
+
label.textContent = config.label;
|
|
699
|
+
const value = document.createElement("span");
|
|
700
|
+
value.className = "perf-monitor__hud-value";
|
|
701
|
+
value.textContent = "—";
|
|
702
|
+
item.append(label, value);
|
|
703
|
+
this.hudGridEl.append(item);
|
|
704
|
+
this.hudNumberEls.set(config.key, value);
|
|
705
|
+
}
|
|
706
|
+
this.resizeCanvases();
|
|
707
|
+
}
|
|
708
|
+
onSelectionChanged() {
|
|
709
|
+
for (const [metric, checkbox] of this.graphCheckboxes) checkbox.checked = this.settings.isGraphEnabled(metric);
|
|
710
|
+
for (const [key, checkbox] of this.statCheckboxes) checkbox.checked = this.settings.isNumberEnabled(key);
|
|
711
|
+
this.dimCheckbox.checked = this.settings.dim;
|
|
712
|
+
this.rebuildHud();
|
|
713
|
+
}
|
|
714
|
+
buildCheckbox(checked, title, onChange) {
|
|
715
|
+
const checkbox = document.createElement("input");
|
|
716
|
+
checkbox.type = "checkbox";
|
|
717
|
+
checkbox.className = "perf-monitor__checkbox";
|
|
718
|
+
checkbox.checked = checked;
|
|
719
|
+
checkbox.title = title;
|
|
720
|
+
checkbox.setAttribute("aria-label", title);
|
|
721
|
+
checkbox.addEventListener("change", () => onChange(checkbox.checked));
|
|
722
|
+
return checkbox;
|
|
723
|
+
}
|
|
724
|
+
applyModeClass() {
|
|
725
|
+
this.element.classList.toggle("perf-monitor--compact", this.mode === "compact");
|
|
726
|
+
this.element.classList.toggle("perf-monitor--full", this.mode === "full");
|
|
727
|
+
}
|
|
728
|
+
resizeCanvases() {
|
|
729
|
+
const dpr = window.devicePixelRatio || 1;
|
|
730
|
+
const sizeAll = (graphs) => {
|
|
731
|
+
for (const graph of graphs) {
|
|
732
|
+
const { canvas, ctx } = graph;
|
|
733
|
+
const width = canvas.clientWidth;
|
|
734
|
+
const height = canvas.clientHeight;
|
|
735
|
+
if (width === 0 || height === 0) continue;
|
|
736
|
+
canvas.width = Math.round(width * dpr);
|
|
737
|
+
canvas.height = Math.round(height * dpr);
|
|
738
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
739
|
+
graph.width = width;
|
|
740
|
+
graph.height = height;
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
sizeAll(this.graphCanvases.values());
|
|
744
|
+
sizeAll(this.hudGraphs.values());
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
//#endregion
|
|
748
|
+
//#region src/ui/styles.ts
|
|
749
|
+
/**
|
|
750
|
+
* The HUD stylesheet as a string, so the package works without a bundler and
|
|
751
|
+
* injects itself once per document. `.perf-monitor` is the metrics card,
|
|
752
|
+
* `.perf-hud` the floating host around it. Both namespaced so a host page can
|
|
753
|
+
* override safely; the `--perf-*` custom properties are the theming surface.
|
|
754
|
+
*/
|
|
755
|
+
const STYLE_ATTRIBUTE = "data-three-meter";
|
|
756
|
+
const PERF_HUD_STYLES = `
|
|
757
|
+
.perf-monitor {
|
|
758
|
+
--perf-bg: #16181d;
|
|
759
|
+
--perf-fg: #e6e8eb;
|
|
760
|
+
--perf-muted: #8b909a;
|
|
761
|
+
--perf-row: rgba(255, 255, 255, 0.04);
|
|
762
|
+
|
|
763
|
+
display: flex;
|
|
764
|
+
flex-direction: column;
|
|
765
|
+
gap: 10px;
|
|
766
|
+
padding: 8px;
|
|
767
|
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
768
|
+
font-size: 11px;
|
|
769
|
+
line-height: 1.4;
|
|
770
|
+
color: var(--perf-fg);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.perf-monitor__graphs {
|
|
774
|
+
display: flex;
|
|
775
|
+
flex-direction: column;
|
|
776
|
+
gap: 6px;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.perf-monitor__graph {
|
|
780
|
+
position: relative;
|
|
781
|
+
height: 40px;
|
|
782
|
+
background: var(--perf-row);
|
|
783
|
+
border-radius: 4px;
|
|
784
|
+
overflow: hidden;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
.perf-monitor__canvas {
|
|
788
|
+
display: block;
|
|
789
|
+
width: 100%;
|
|
790
|
+
height: 100%;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.perf-monitor__graph-overlay {
|
|
794
|
+
position: absolute;
|
|
795
|
+
inset: 0;
|
|
796
|
+
display: flex;
|
|
797
|
+
align-items: center;
|
|
798
|
+
justify-content: space-between;
|
|
799
|
+
padding: 0 8px;
|
|
800
|
+
pointer-events: none;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
.perf-monitor__graph-head {
|
|
804
|
+
display: flex;
|
|
805
|
+
align-items: center;
|
|
806
|
+
gap: 6px;
|
|
807
|
+
cursor: pointer;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
.perf-monitor__graph-overlay .perf-monitor__graph-head {
|
|
811
|
+
pointer-events: auto;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
.perf-monitor__graph-label {
|
|
815
|
+
color: var(--perf-muted);
|
|
816
|
+
text-transform: uppercase;
|
|
817
|
+
letter-spacing: 0.06em;
|
|
818
|
+
font-size: 10px;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
.perf-monitor__graph-value {
|
|
822
|
+
min-width: 6ch;
|
|
823
|
+
font-weight: 600;
|
|
824
|
+
font-variant-numeric: tabular-nums lining-nums;
|
|
825
|
+
text-align: right;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
.perf-monitor__section {
|
|
829
|
+
display: flex;
|
|
830
|
+
flex-direction: column;
|
|
831
|
+
gap: 2px;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
.perf-monitor__row {
|
|
835
|
+
display: flex;
|
|
836
|
+
align-items: center;
|
|
837
|
+
gap: 6px;
|
|
838
|
+
padding: 2px 6px;
|
|
839
|
+
border-radius: 3px;
|
|
840
|
+
cursor: pointer;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.perf-monitor__row:nth-child(even) {
|
|
844
|
+
background: var(--perf-row);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.perf-monitor__label {
|
|
848
|
+
flex: 1;
|
|
849
|
+
color: var(--perf-muted);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
.perf-monitor__icon {
|
|
853
|
+
flex: none;
|
|
854
|
+
width: 12px;
|
|
855
|
+
height: 12px;
|
|
856
|
+
color: var(--perf-muted);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
.perf-monitor__options {
|
|
860
|
+
padding-bottom: 8px;
|
|
861
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.perf-monitor__checkbox {
|
|
865
|
+
flex: none;
|
|
866
|
+
margin: 0;
|
|
867
|
+
width: 12px;
|
|
868
|
+
height: 12px;
|
|
869
|
+
accent-color: #60a5fa;
|
|
870
|
+
cursor: pointer;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
.perf-monitor__value {
|
|
874
|
+
min-width: 7ch;
|
|
875
|
+
font-variant-numeric: tabular-nums lining-nums;
|
|
876
|
+
text-align: right;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
.perf-monitor__hud {
|
|
880
|
+
display: flex;
|
|
881
|
+
flex-direction: column;
|
|
882
|
+
gap: 6px;
|
|
883
|
+
padding: 2px;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
.perf-monitor__hud-graphs {
|
|
887
|
+
display: flex;
|
|
888
|
+
flex-direction: column;
|
|
889
|
+
gap: 4px;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.perf-monitor__hud-graphs:empty {
|
|
893
|
+
display: none;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.perf-monitor__hud-graph {
|
|
897
|
+
position: relative;
|
|
898
|
+
height: 28px;
|
|
899
|
+
background: var(--perf-row);
|
|
900
|
+
border-radius: 4px;
|
|
901
|
+
overflow: hidden;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
.perf-monitor__hud-grid {
|
|
905
|
+
display: grid;
|
|
906
|
+
grid-template-columns: auto 6ch auto 6ch;
|
|
907
|
+
align-items: baseline;
|
|
908
|
+
gap: 2px 8px;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.perf-monitor__hud-grid:empty {
|
|
912
|
+
display: none;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
.perf-monitor__hud-item {
|
|
916
|
+
display: contents;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
.perf-monitor__hud-label {
|
|
920
|
+
color: var(--perf-muted);
|
|
921
|
+
font-size: 10px;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
.perf-monitor__hud-value {
|
|
925
|
+
font-weight: 600;
|
|
926
|
+
font-variant-numeric: tabular-nums lining-nums;
|
|
927
|
+
text-align: right;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
.perf-monitor--full .perf-monitor__hud {
|
|
931
|
+
display: none;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
.perf-monitor--compact .perf-monitor__graphs,
|
|
935
|
+
.perf-monitor--compact .perf-monitor__section {
|
|
936
|
+
display: none;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
.perf-monitor--compact {
|
|
940
|
+
padding: 4px 8px;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
.perf-hud {
|
|
944
|
+
position: fixed;
|
|
945
|
+
z-index: 9999;
|
|
946
|
+
/* Safari resolves width:100% canvases against the viewport on a fixed
|
|
947
|
+
host with auto width. Shrink-wrap so expand stays a card, not the window. */
|
|
948
|
+
width: max-content;
|
|
949
|
+
max-width: min(20rem, calc(100vw - 48px));
|
|
950
|
+
overflow: visible;
|
|
951
|
+
background: transparent;
|
|
952
|
+
pointer-events: none;
|
|
953
|
+
user-select: none;
|
|
954
|
+
touch-action: none;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
.perf-hud--full > .perf-monitor {
|
|
958
|
+
width: 16rem;
|
|
959
|
+
max-width: 100%;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
.perf-hud > .perf-monitor {
|
|
963
|
+
position: relative;
|
|
964
|
+
z-index: 1;
|
|
965
|
+
max-height: 70vh;
|
|
966
|
+
overflow: auto;
|
|
967
|
+
background: rgba(22, 24, 29, 0.85);
|
|
968
|
+
border-radius: 6px;
|
|
969
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.32);
|
|
970
|
+
backdrop-filter: blur(2px);
|
|
971
|
+
pointer-events: auto;
|
|
972
|
+
transition: opacity 0.25s ease;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
.perf-hud--dim:not(.is-awake):not(.is-dragging) > .perf-monitor {
|
|
976
|
+
opacity: 0.32;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.perf-hud__hotspot {
|
|
980
|
+
position: absolute;
|
|
981
|
+
z-index: 0;
|
|
982
|
+
inset: -20px;
|
|
983
|
+
pointer-events: auto;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
.perf-hud:not([data-edge="right"]) .perf-hud__hotspot {
|
|
987
|
+
right: -46px;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
.perf-hud[data-edge="right"] .perf-hud__hotspot {
|
|
991
|
+
left: -46px;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
.perf-hud__tools {
|
|
995
|
+
position: absolute;
|
|
996
|
+
top: 0;
|
|
997
|
+
z-index: 2;
|
|
998
|
+
display: flex;
|
|
999
|
+
flex-direction: column;
|
|
1000
|
+
gap: 4px;
|
|
1001
|
+
opacity: 0;
|
|
1002
|
+
pointer-events: none;
|
|
1003
|
+
transition: opacity 0.15s ease;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
.perf-hud.is-awake .perf-hud__tools,
|
|
1007
|
+
.perf-hud.is-dragging .perf-hud__tools {
|
|
1008
|
+
opacity: 1;
|
|
1009
|
+
pointer-events: auto;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/* Inward side: right by default, left when docked to the right edge. */
|
|
1013
|
+
.perf-hud:not([data-edge="right"]) .perf-hud__tools {
|
|
1014
|
+
left: calc(100% + 6px);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
.perf-hud[data-edge="right"] .perf-hud__tools {
|
|
1018
|
+
right: calc(100% + 6px);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
.perf-hud__disc {
|
|
1022
|
+
display: flex;
|
|
1023
|
+
align-items: center;
|
|
1024
|
+
justify-content: center;
|
|
1025
|
+
width: 20px;
|
|
1026
|
+
height: 20px;
|
|
1027
|
+
padding: 0;
|
|
1028
|
+
border: 0;
|
|
1029
|
+
border-radius: 999px;
|
|
1030
|
+
background: rgba(22, 24, 29, 0.85);
|
|
1031
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.32);
|
|
1032
|
+
color: #c5c8ce;
|
|
1033
|
+
cursor: pointer;
|
|
1034
|
+
backdrop-filter: blur(2px);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.perf-hud__icon {
|
|
1038
|
+
width: 12px;
|
|
1039
|
+
height: 12px;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
.perf-hud__drag {
|
|
1043
|
+
cursor: grab;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
.perf-hud.is-dragging .perf-hud__drag {
|
|
1047
|
+
cursor: grabbing;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
.perf-hud__disc:hover,
|
|
1051
|
+
.perf-hud__disc:focus-visible,
|
|
1052
|
+
.perf-hud__disc[aria-pressed="true"] {
|
|
1053
|
+
color: #e6e8eb;
|
|
1054
|
+
outline: 1px solid rgba(96, 165, 250, 0.6);
|
|
1055
|
+
outline-offset: 1px;
|
|
1056
|
+
}
|
|
1057
|
+
`;
|
|
1058
|
+
/**
|
|
1059
|
+
* Append the stylesheet to `target`'s head, once. Call before mounting a view
|
|
1060
|
+
* by hand; {@link mountPerfHud} does it for you unless told not to.
|
|
1061
|
+
*/
|
|
1062
|
+
const injectStyles = (target = document) => {
|
|
1063
|
+
if (target.head.querySelector(`style[${STYLE_ATTRIBUTE}]`)) return;
|
|
1064
|
+
const style = target.createElement("style");
|
|
1065
|
+
style.setAttribute(STYLE_ATTRIBUTE, "");
|
|
1066
|
+
style.textContent = PERF_HUD_STYLES;
|
|
1067
|
+
target.head.append(style);
|
|
1068
|
+
};
|
|
1069
|
+
//#endregion
|
|
1070
|
+
//#region src/ui/mount-perf-hud.ts
|
|
1071
|
+
const TOUCH_LINGER_MS = 2500;
|
|
1072
|
+
const disc = (label, className, icon) => {
|
|
1073
|
+
const button = document.createElement("button");
|
|
1074
|
+
button.type = "button";
|
|
1075
|
+
button.className = `perf-hud__disc ${className}`.trim();
|
|
1076
|
+
button.setAttribute("aria-label", label);
|
|
1077
|
+
button.append(createIcon(icon, "perf-hud__icon"));
|
|
1078
|
+
return button;
|
|
1079
|
+
};
|
|
1080
|
+
const mountPerfHud = (monitor, options = {}) => {
|
|
1081
|
+
const storageKey = options.storageKey === void 0 ? DEFAULT_STORAGE_KEY : options.storageKey;
|
|
1082
|
+
const settings = options.settings ?? new HudSettings({ storageKey });
|
|
1083
|
+
const parent = options.parent ?? document.body;
|
|
1084
|
+
if (options.injectStyles ?? true) injectStyles(parent.ownerDocument);
|
|
1085
|
+
const host = document.createElement("div");
|
|
1086
|
+
host.className = "perf-hud";
|
|
1087
|
+
host.dataset.edge = "left";
|
|
1088
|
+
host.setAttribute("aria-label", options.label ?? "Performance");
|
|
1089
|
+
const hotspot = document.createElement("div");
|
|
1090
|
+
hotspot.className = "perf-hud__hotspot";
|
|
1091
|
+
hotspot.setAttribute("aria-hidden", "true");
|
|
1092
|
+
const tools = document.createElement("div");
|
|
1093
|
+
tools.className = "perf-hud__tools";
|
|
1094
|
+
const grip = disc("Drag performance panel", "perf-hud__drag", "grip");
|
|
1095
|
+
const toggle = disc("Toggle full performance metrics", "", "sliders");
|
|
1096
|
+
tools.append(grip, toggle);
|
|
1097
|
+
const view = new PerformanceView({
|
|
1098
|
+
mode: options.mode ?? "compact",
|
|
1099
|
+
monitor,
|
|
1100
|
+
refreshHz: options.refreshHz,
|
|
1101
|
+
settings
|
|
1102
|
+
});
|
|
1103
|
+
host.append(hotspot, tools, view.element);
|
|
1104
|
+
parent.append(host);
|
|
1105
|
+
const applyMode = () => {
|
|
1106
|
+
host.classList.toggle("perf-hud--full", view.getMode() === "full");
|
|
1107
|
+
};
|
|
1108
|
+
const applyDim = () => {
|
|
1109
|
+
host.classList.toggle("perf-hud--dim", settings.dim);
|
|
1110
|
+
};
|
|
1111
|
+
applyMode();
|
|
1112
|
+
applyDim();
|
|
1113
|
+
const unsubscribe = settings.subscribe(applyDim);
|
|
1114
|
+
const dock = dockPanel(host, {
|
|
1115
|
+
defaultPlacement: options.defaultPlacement,
|
|
1116
|
+
handle: grip,
|
|
1117
|
+
storageKey
|
|
1118
|
+
});
|
|
1119
|
+
const setMode = (mode) => {
|
|
1120
|
+
view.setMode(mode);
|
|
1121
|
+
applyMode();
|
|
1122
|
+
requestAnimationFrame(() => dock.refresh());
|
|
1123
|
+
};
|
|
1124
|
+
toggle.addEventListener("click", () => {
|
|
1125
|
+
setMode(view.getMode() === "compact" ? "full" : "compact");
|
|
1126
|
+
});
|
|
1127
|
+
let hideTimer = 0;
|
|
1128
|
+
const sleep = () => {
|
|
1129
|
+
if (host.classList.contains("is-dragging")) return;
|
|
1130
|
+
host.classList.remove("is-awake");
|
|
1131
|
+
};
|
|
1132
|
+
const wake = (linger) => {
|
|
1133
|
+
host.classList.add("is-awake");
|
|
1134
|
+
window.clearTimeout(hideTimer);
|
|
1135
|
+
if (linger) hideTimer = window.setTimeout(sleep, TOUCH_LINGER_MS);
|
|
1136
|
+
};
|
|
1137
|
+
const onEnter = () => {
|
|
1138
|
+
wake(false);
|
|
1139
|
+
};
|
|
1140
|
+
const onLeave = (event) => {
|
|
1141
|
+
if (host.contains(event.relatedTarget)) return;
|
|
1142
|
+
if (event.pointerType === "touch" || event.pointerType === "pen") {
|
|
1143
|
+
wake(true);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
sleep();
|
|
1147
|
+
};
|
|
1148
|
+
const onDown = (event) => {
|
|
1149
|
+
wake(event.pointerType !== "mouse");
|
|
1150
|
+
};
|
|
1151
|
+
host.addEventListener("pointerenter", onEnter);
|
|
1152
|
+
host.addEventListener("pointerleave", onLeave);
|
|
1153
|
+
host.addEventListener("pointerdown", onDown);
|
|
1154
|
+
view.start();
|
|
1155
|
+
return {
|
|
1156
|
+
dispose: () => {
|
|
1157
|
+
window.clearTimeout(hideTimer);
|
|
1158
|
+
host.removeEventListener("pointerenter", onEnter);
|
|
1159
|
+
host.removeEventListener("pointerleave", onLeave);
|
|
1160
|
+
host.removeEventListener("pointerdown", onDown);
|
|
1161
|
+
unsubscribe();
|
|
1162
|
+
dock.dispose();
|
|
1163
|
+
view.dispose();
|
|
1164
|
+
host.remove();
|
|
1165
|
+
},
|
|
1166
|
+
element: host,
|
|
1167
|
+
getMode: () => view.getMode(),
|
|
1168
|
+
setMode,
|
|
1169
|
+
settings,
|
|
1170
|
+
view
|
|
1171
|
+
};
|
|
1172
|
+
};
|
|
1173
|
+
//#endregion
|
|
1174
|
+
export { dockPanel as a, PerformanceView as i, PERF_HUD_STYLES as n, DEFAULT_STORAGE_KEY as o, injectStyles as r, HudSettings as s, mountPerfHud as t };
|
|
1175
|
+
|
|
1176
|
+
//# sourceMappingURL=mount-perf-hud-CduRNym5.js.map
|