@weasel-js/hud 1.0.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +99 -62
- package/dist/index.js.map +1 -1
- package/dist/loupe/createLoupe.d.ts +15 -2
- package/dist/loupe/createLoupe.d.ts.map +1 -1
- package/dist/loupe/pick.d.ts +20 -0
- package/dist/loupe/pick.d.ts.map +1 -0
- package/dist/react/index.js +1 -1
- package/dist/{tool-Dta4BYKu.js → tool-BPkQR6_K.js} +105 -91
- package/dist/tool-BPkQR6_K.js.map +1 -0
- package/dist/widgets/image.d.ts +22 -0
- package/dist/widgets/image.d.ts.map +1 -1
- package/dist/widgets/window/window.d.ts +7 -0
- package/dist/widgets/window/window.d.ts.map +1 -1
- package/package.json +3 -3
- package/dist/tool-Dta4BYKu.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as e, c as t, i as n, l as r, n as i, o as a, r as o, s, t as c, u as l } from "./tool-
|
|
1
|
+
import { a as e, c as t, i as n, l as r, n as i, o as a, r as o, s, t as c, u as l } from "./tool-BPkQR6_K.js";
|
|
2
2
|
import { createViewportLayer as u, screenToWorld as d, viewToTransform as f } from "@weasel-js/core";
|
|
3
3
|
//#region src/loupe/innerView.ts
|
|
4
4
|
function p(e, t, n, r) {
|
|
@@ -13,9 +13,17 @@ function p(e, t, n, r) {
|
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
15
|
//#endregion
|
|
16
|
+
//#region src/loupe/pick.ts
|
|
17
|
+
function m(e, t, n, r) {
|
|
18
|
+
return {
|
|
19
|
+
x: n.x + (e.x - (t.x + t.w / 2)) / r,
|
|
20
|
+
y: n.y + (e.y - (t.y + t.h / 2)) / r
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
16
24
|
//#region src/loupe/readback.ts
|
|
17
|
-
function
|
|
18
|
-
let o = Math.round(n.x * r), s = Math.round(n.y * r), c =
|
|
25
|
+
function h(e, t, n, r, i, a) {
|
|
26
|
+
let o = Math.round(n.x * r), s = Math.round(n.y * r), c = g(o - Math.floor(i / 2), 0, Math.max(0, t.width - i)), l = g(s - Math.floor(a / 2), 0, Math.max(0, t.height - a)), u = t.height - l - a, d = new Uint8Array(i * a * 4);
|
|
19
27
|
e.readPixels(c, u, i, a, e.RGBA, e.UNSIGNED_BYTE, d);
|
|
20
28
|
let f = new Uint8ClampedArray(i * a * 4), p = i * 4;
|
|
21
29
|
for (let e = 0; e < a; e++) {
|
|
@@ -24,110 +32,138 @@ function m(e, t, n, r, i, a) {
|
|
|
24
32
|
}
|
|
25
33
|
return new ImageData(f, i, a);
|
|
26
34
|
}
|
|
27
|
-
function
|
|
35
|
+
function g(e, t, n) {
|
|
28
36
|
return e < t ? t : e > n ? n : e;
|
|
29
37
|
}
|
|
30
38
|
//#endregion
|
|
31
39
|
//#region src/loupe/createLoupe.ts
|
|
32
|
-
function
|
|
40
|
+
function _(e) {
|
|
33
41
|
let { hud: t, element: n, source: r, requestRedraw: i } = e, a = e.mode ?? "vector", o = e.factor ?? 8, s = {
|
|
34
42
|
x: 0,
|
|
35
43
|
y: 0
|
|
36
|
-
}, c = null, l = null,
|
|
44
|
+
}, c = null, l = null, g = !1, _ = !1, v = !1, y = e.bounds ?? {
|
|
37
45
|
x: 24,
|
|
38
46
|
y: 24,
|
|
39
47
|
w: 220,
|
|
40
48
|
h: 200
|
|
41
|
-
},
|
|
42
|
-
let
|
|
43
|
-
x:
|
|
44
|
-
y:
|
|
45
|
-
},
|
|
49
|
+
}, b = (e) => a === "vector" ? S(e) : C(e), x = (t) => e.background ?? t.tokens["--wzl-surface"], S = (e) => {
|
|
50
|
+
let t = d(s.x, s.y, f(e.view)), n = p({
|
|
51
|
+
x: t[0],
|
|
52
|
+
y: t[1]
|
|
53
|
+
}, e.view, e.rect, o);
|
|
46
54
|
return u({
|
|
47
|
-
id: `${
|
|
55
|
+
id: `${D.id}:lens`,
|
|
48
56
|
label: "Loupe content",
|
|
49
57
|
source: r,
|
|
50
|
-
view:
|
|
51
|
-
bounds: () =>
|
|
52
|
-
background: e
|
|
53
|
-
}).draw(
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
view: n,
|
|
59
|
+
bounds: () => e.rect,
|
|
60
|
+
background: x(e)
|
|
61
|
+
}).draw(e.data, e.view, e.dims);
|
|
62
|
+
}, C = (e) => {
|
|
63
|
+
let t = [{
|
|
64
|
+
kind: "path",
|
|
65
|
+
path: {
|
|
66
|
+
kind: "rect",
|
|
67
|
+
x: e.rect.x,
|
|
68
|
+
y: e.rect.y,
|
|
69
|
+
width: e.rect.w,
|
|
70
|
+
height: e.rect.h
|
|
71
|
+
},
|
|
72
|
+
fill: {
|
|
73
|
+
fill: "solid",
|
|
74
|
+
color: x(e)
|
|
75
|
+
}
|
|
76
|
+
}];
|
|
77
|
+
return l && t.push({
|
|
78
|
+
kind: "image",
|
|
79
|
+
image: l,
|
|
80
|
+
x: e.rect.x,
|
|
81
|
+
y: e.rect.y,
|
|
82
|
+
w: e.rect.w,
|
|
83
|
+
h: e.rect.h,
|
|
84
|
+
sampling: "nearest"
|
|
85
|
+
}), t;
|
|
86
|
+
}, w = () => {
|
|
87
|
+
if (v || a !== "pixel") return;
|
|
88
|
+
if (g) {
|
|
89
|
+
_ = !0;
|
|
66
90
|
return;
|
|
67
91
|
}
|
|
68
92
|
let e = n.getContext("webgl2");
|
|
69
93
|
if (!e) return;
|
|
70
94
|
let t = n.getBoundingClientRect();
|
|
71
95
|
if (t.width === 0) return;
|
|
72
|
-
let r = n.width / t.width, c =
|
|
96
|
+
let r = n.width / t.width, c = D.contentRect, u = Math.max(1, Math.round(c.w * r / o)), d = Math.max(1, Math.round(c.h * r / o)), f = h(e, {
|
|
73
97
|
width: n.width,
|
|
74
98
|
height: n.height
|
|
75
99
|
}, s, r, u, d);
|
|
76
|
-
|
|
77
|
-
if (
|
|
100
|
+
g = !0, _ = !1, createImageBitmap(f).then((e) => {
|
|
101
|
+
if (v) {
|
|
78
102
|
e.close();
|
|
79
103
|
return;
|
|
80
104
|
}
|
|
81
|
-
l?.close(), l = e,
|
|
105
|
+
l?.close(), l = e, g = !1, i(), _ && w();
|
|
82
106
|
}).catch(() => {
|
|
83
|
-
|
|
107
|
+
g = !1, _ && w();
|
|
84
108
|
});
|
|
85
|
-
},
|
|
109
|
+
}, T = (e) => {
|
|
86
110
|
let t = n.getContext("webgl2");
|
|
87
|
-
if (!t) return;
|
|
111
|
+
if (!t) return null;
|
|
88
112
|
let r = n.getBoundingClientRect();
|
|
89
|
-
if (r.width === 0) return;
|
|
90
|
-
let i = n.width / r.width, a =
|
|
113
|
+
if (r.width === 0) return null;
|
|
114
|
+
let i = n.width / r.width, a = h(t, {
|
|
91
115
|
width: n.width,
|
|
92
116
|
height: n.height
|
|
93
|
-
},
|
|
117
|
+
}, e, i, 1, 1);
|
|
118
|
+
return "#" + [
|
|
94
119
|
a.data[0],
|
|
95
120
|
a.data[1],
|
|
96
121
|
a.data[2]
|
|
97
122
|
].map((e) => e.toString(16).padStart(2, "0")).join("");
|
|
98
|
-
|
|
99
|
-
|
|
123
|
+
}, E = () => {
|
|
124
|
+
let t = T(s);
|
|
125
|
+
t === null || t === c || (c = t, e.onColorChange?.(t));
|
|
126
|
+
}, D = t.window({
|
|
100
127
|
id: "weasel-loupe",
|
|
101
|
-
x:
|
|
102
|
-
y:
|
|
103
|
-
w:
|
|
104
|
-
h:
|
|
128
|
+
x: y.x,
|
|
129
|
+
y: y.y,
|
|
130
|
+
w: y.w,
|
|
131
|
+
h: y.h,
|
|
105
132
|
title: e.title ?? "Loupe",
|
|
106
133
|
titlebar: e.titlebar,
|
|
107
|
-
content:
|
|
134
|
+
content: b,
|
|
108
135
|
onResize: () => {
|
|
109
|
-
|
|
136
|
+
w();
|
|
137
|
+
},
|
|
138
|
+
onContentClick: (e) => {
|
|
139
|
+
O(e);
|
|
110
140
|
},
|
|
111
141
|
...e.onClose ? { onClose: e.onClose } : {}
|
|
112
|
-
}),
|
|
142
|
+
}), O = (t) => {
|
|
143
|
+
if (v) return null;
|
|
144
|
+
let n = t ? m(t, D.contentRect, s, o) : s;
|
|
145
|
+
if (t && D.hitTest(n.x, n.y)) return null;
|
|
146
|
+
let r = T(n);
|
|
147
|
+
return r !== null && e.onPick?.(r), r;
|
|
148
|
+
}, k = (e) => {
|
|
113
149
|
let t = n.getBoundingClientRect();
|
|
114
|
-
|
|
150
|
+
A({
|
|
115
151
|
x: e.clientX - t.left,
|
|
116
152
|
y: e.clientY - t.top
|
|
117
153
|
});
|
|
118
|
-
},
|
|
119
|
-
if (!
|
|
120
|
-
if (
|
|
121
|
-
|
|
154
|
+
}, A = (e) => {
|
|
155
|
+
if (!v) {
|
|
156
|
+
if (D.disposed) {
|
|
157
|
+
j();
|
|
122
158
|
return;
|
|
123
159
|
}
|
|
124
|
-
|
|
160
|
+
D.hidden || D.hitTest(e.x, e.y) || (s = e, E(), a === "pixel" && w(), i());
|
|
125
161
|
}
|
|
126
|
-
},
|
|
127
|
-
|
|
162
|
+
}, j = () => {
|
|
163
|
+
v = !0, n.removeEventListener("pointermove", k), l?.close(), l = null;
|
|
128
164
|
};
|
|
129
|
-
return n.addEventListener("pointermove",
|
|
130
|
-
window:
|
|
165
|
+
return n.addEventListener("pointermove", k), {
|
|
166
|
+
window: D,
|
|
131
167
|
get mode() {
|
|
132
168
|
return a;
|
|
133
169
|
},
|
|
@@ -141,18 +177,19 @@ function g(e) {
|
|
|
141
177
|
return c;
|
|
142
178
|
},
|
|
143
179
|
setMode(e) {
|
|
144
|
-
e !== a && (a = e, a === "pixel" &&
|
|
180
|
+
e !== a && (a = e, a === "pixel" && w(), i());
|
|
145
181
|
},
|
|
146
182
|
setFactor(e) {
|
|
147
|
-
o = e, a === "pixel" &&
|
|
183
|
+
o = e, a === "pixel" && w(), i();
|
|
148
184
|
},
|
|
149
|
-
aimAt:
|
|
185
|
+
aimAt: A,
|
|
186
|
+
pick: O,
|
|
150
187
|
dispose() {
|
|
151
|
-
|
|
188
|
+
j(), D.dispose();
|
|
152
189
|
}
|
|
153
190
|
};
|
|
154
191
|
}
|
|
155
192
|
//#endregion
|
|
156
|
-
export { a as DEFAULT_FONT_FAMILY, n as DEFAULT_WIDGET_CLAIMS, r as DEFAULT_WINDOW_METRICS, c as HUD_AFFORDANCE_KIND, o as attachHud, e as claimsOf, t as createHud, i as createHudContribution,
|
|
193
|
+
export { a as DEFAULT_FONT_FAMILY, n as DEFAULT_WIDGET_CLAIMS, r as DEFAULT_WINDOW_METRICS, c as HUD_AFFORDANCE_KIND, o as attachHud, e as claimsOf, t as createHud, i as createHudContribution, _ as createLoupe, l as cursorForZone, p as loupeInnerView, s as registerDefaultFont };
|
|
157
194
|
|
|
158
195
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/loupe/innerView.ts","../src/loupe/readback.ts","../src/loupe/createLoupe.ts"],"sourcesContent":["import type { View } from '@weasel-js/core';\nimport type { WidgetBounds } from '../widget';\n\n/**\n * The inner `View` a loupe renders its content through: the outer view's\n * magnification times `factor`, positioned so `target` sits at the center of\n * a viewport the size of `rect`.\n */\nexport function loupeInnerView(\n target: { x: number; y: number },\n outer: View,\n rect: WidgetBounds,\n factor: number,\n): View {\n const scale = { x: outer.scale.x * factor, y: outer.scale.y * factor };\n return {\n x: target.x - rect.w / 2 / scale.x,\n y: target.y - rect.h / 2 / scale.y,\n scale,\n };\n}\n","/**\n * Read a `rw × rh` device-pixel region of the drawing buffer centered on\n * `pointer` (CSS px relative to the canvas), returned top-down.\n *\n * GL reports rows bottom-up; every consumer here wants top-down, so both the\n * read origin and the returned rows are flipped.\n */\nexport function readbackRegion(\n gl: WebGL2RenderingContext,\n buffer: { width: number; height: number },\n pointer: { x: number; y: number },\n dpr: number,\n rw: number,\n rh: number,\n): ImageData {\n const cx = Math.round(pointer.x * dpr);\n const cy = Math.round(pointer.y * dpr);\n const gx = clamp(cx - Math.floor(rw / 2), 0, Math.max(0, buffer.width - rw));\n const gyTop = clamp(cy - Math.floor(rh / 2), 0, Math.max(0, buffer.height - rh));\n const gy = buffer.height - gyTop - rh;\n\n const raw = new Uint8Array(rw * rh * 4);\n gl.readPixels(gx, gy, rw, rh, gl.RGBA, gl.UNSIGNED_BYTE, raw);\n\n const flipped = new Uint8ClampedArray(rw * rh * 4);\n const stride = rw * 4;\n for (let row = 0; row < rh; row++) {\n const src = (rh - 1 - row) * stride;\n flipped.set(raw.subarray(src, src + stride), row * stride);\n }\n return new ImageData(flipped, rw, rh);\n}\n\nfunction clamp(v: number, lo: number, hi: number): number {\n return v < lo ? lo : v > hi ? hi : v;\n}\n","import {\n createViewportLayer, screenToWorld, viewToTransform,\n type RenderLayer, type View,\n} from '@weasel-js/core';\nimport type { DrawCommand } from '@weasel-js/core/renderer';\nimport type { Hud } from '../hud';\nimport type { WindowWidget } from '../widgets/window/window';\nimport type { HudContentCtx, WidgetBounds } from '../widget';\nimport { loupeInnerView } from './innerView';\nimport { readbackRegion } from './readback';\n\n/** How a loupe magnifies. `'vector'` re-renders the source layers through a\n * zoomed-in view, so content stays sharp at any factor; `'pixel'` reads the\n * framebuffer back and blows up the actual pixels. */\nexport type LoupeMode = 'vector' | 'pixel';\n\n/** Options for `createLoupe`. */\nexport interface LoupeOptions {\n hud: Hud;\n /** The live canvas. Supplies the GL context for pixel mode and the pointer\n * feed for aiming. */\n element: HTMLCanvasElement;\n /** Layers re-rendered through the magnified inner view in vector mode. */\n source: RenderLayer<unknown>[];\n requestRedraw: () => void;\n mode?: LoupeMode;\n factor?: number;\n bounds?: WidgetBounds;\n title?: string;\n /** Draw the window's titlebar and close box. Default `true`. Pass `false`\n * for a bare lens; the interior then doubles as the move handle. */\n titlebar?: boolean;\n /** Called with the hex color under the aim point whenever it changes.\n * Sampled from the framebuffer in both modes — vector content is a\n * re-render whose edge colors differ from the screen's, so reading the\n * color off it would report something the user cannot see. */\n onColorChange?: (hex: string) => void;\n /** Fired by the window's close box. The window does not hide itself —\n * whoever owns the loupe's visibility decides what closing means. */\n onClose?: () => void;\n /** Opaque backdrop behind vector content, so the outer canvas does not\n * show through where the inner view is empty. */\n background?: string;\n}\n\n/** Control surface for a live loupe: where it is aimed, how it magnifies,\n * what color it is over, and the window it lives in. */\nexport interface LoupeHandle {\n readonly window: WindowWidget;\n readonly mode: LoupeMode;\n readonly factor: number;\n /** Last aimed point, in screen-space CSS px relative to the canvas. */\n readonly aim: { x: number; y: number };\n /** Hex color under the aim point, read off the framebuffer. `null` before\n * the first aim, or when the GL context is unavailable. */\n readonly color: string | null;\n setMode(mode: LoupeMode): void;\n setFactor(factor: number): void;\n /** Aim at a screen point. Ignored while the point is over the window —\n * that is the freeze rule that makes the borders reachable. */\n aimAt(p: { x: number; y: number }): void;\n dispose(): void;\n}\n\n/**\n * Build a magnifier: a HUD window whose interior shows the canvas around an\n * aim point, enlarged. Adds the window to the HUD and starts following the\n * pointer; `dispose` removes it again.\n */\nexport function createLoupe(opts: LoupeOptions): LoupeHandle {\n const { hud, element, source, requestRedraw } = opts;\n let mode: LoupeMode = opts.mode ?? 'vector';\n let factor = opts.factor ?? 8;\n let aim = { x: 0, y: 0 };\n let color: string | null = null;\n let pixels: ImageBitmap | null = null;\n let pixelsPending = false;\n let pixelsStale = false;\n let disposed = false;\n\n const b = opts.bounds ?? { x: 24, y: 24, w: 220, h: 200 };\n\n const content = (ctx: HudContentCtx): DrawCommand[] =>\n mode === 'vector' ? drawVector(ctx) : drawPixels(ctx);\n\n const drawVector = (ctx: HudContentCtx): DrawCommand[] => {\n const world = screenToWorld(aim.x, aim.y, viewToTransform(ctx.view));\n const inner: View = loupeInnerView(\n { x: world[0], y: world[1] }, ctx.view, ctx.rect, factor,\n );\n // A fresh lens per frame: CreateViewportLayerOpts.view is static, and the\n // inner view moves with the pointer.\n const lens = createViewportLayer<unknown>({\n id: `${win.id}:lens`,\n label: 'Loupe content',\n source,\n view: inner,\n bounds: () => ctx.rect,\n background: opts.background ?? ctx.tokens['--wzl-surface'],\n });\n return lens.draw(ctx.data, ctx.view, ctx.dims);\n };\n\n const drawPixels = (ctx: HudContentCtx): DrawCommand[] => {\n if (!pixels) return [];\n return [{\n kind: 'image',\n image: pixels,\n x: ctx.rect.x, y: ctx.rect.y, w: ctx.rect.w, h: ctx.rect.h,\n sampling: 'nearest',\n }];\n };\n\n const refreshPixels = () => {\n if (disposed || mode !== 'pixel') return;\n // A readback requested mid-flight is remembered rather than dropped: the\n // aim that arrives during a fast drag is the one the user ends on.\n if (pixelsPending) { pixelsStale = true; return; }\n const gl = element.getContext('webgl2');\n if (!gl) return;\n const cssRect = element.getBoundingClientRect();\n if (cssRect.width === 0) return;\n const dpr = element.width / cssRect.width;\n const rect = win.contentRect;\n const rw = Math.max(1, Math.round((rect.w * dpr) / factor));\n const rh = Math.max(1, Math.round((rect.h * dpr) / factor));\n const data = readbackRegion(\n gl, { width: element.width, height: element.height }, aim, dpr, rw, rh,\n );\n pixelsPending = true;\n pixelsStale = false;\n createImageBitmap(data)\n .then((bmp) => {\n if (disposed) { bmp.close(); return; }\n pixels?.close();\n pixels = bmp;\n pixelsPending = false;\n requestRedraw();\n if (pixelsStale) refreshPixels();\n })\n .catch(() => {\n pixelsPending = false;\n if (pixelsStale) refreshPixels();\n });\n };\n\n const sampleColor = () => {\n const gl = element.getContext('webgl2');\n if (!gl) return;\n const cssRect = element.getBoundingClientRect();\n if (cssRect.width === 0) return;\n const dpr = element.width / cssRect.width;\n const px = readbackRegion(\n gl, { width: element.width, height: element.height }, aim, dpr, 1, 1,\n );\n const hex = '#' + [px.data[0], px.data[1], px.data[2]]\n .map((c) => c.toString(16).padStart(2, '0')).join('');\n if (hex !== color) {\n color = hex;\n opts.onColorChange?.(hex);\n }\n };\n\n const win = hud.window({\n id: 'weasel-loupe',\n x: b.x, y: b.y, w: b.w, h: b.h,\n title: opts.title ?? 'Loupe',\n titlebar: opts.titlebar,\n content,\n onResize: () => { refreshPixels(); },\n ...(opts.onClose ? { onClose: opts.onClose } : {}),\n });\n\n const onPointerMove = (evt: PointerEvent) => {\n const r = element.getBoundingClientRect();\n handleAim({ x: evt.clientX - r.left, y: evt.clientY - r.top });\n };\n\n const handleAim = (p: { x: number; y: number }) => {\n if (disposed) return;\n // The window can be removed through the HUD, which disposes it without\n // going through `LoupeHandle.dispose`. Follow it down rather than keep\n // reading pixels back for a loupe nobody can see.\n if (win.disposed) { teardown(); return; }\n if (win.hidden) return;\n if (win.hitTest(p.x, p.y)) return; // freeze rule\n aim = p;\n sampleColor();\n if (mode === 'pixel') refreshPixels();\n requestRedraw();\n };\n\n const teardown = () => {\n disposed = true;\n element.removeEventListener('pointermove', onPointerMove);\n pixels?.close();\n pixels = null;\n };\n\n element.addEventListener('pointermove', onPointerMove);\n\n return {\n window: win,\n get mode() { return mode; },\n get factor() { return factor; },\n get aim() { return aim; },\n get color() { return color; },\n setMode(next) {\n if (next === mode) return;\n mode = next;\n if (mode === 'pixel') refreshPixels();\n requestRedraw();\n },\n setFactor(next) {\n factor = next;\n if (mode === 'pixel') refreshPixels();\n requestRedraw();\n },\n aimAt: handleAim,\n dispose() {\n teardown();\n win.dispose();\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/loupe/innerView.ts","../src/loupe/pick.ts","../src/loupe/readback.ts","../src/loupe/createLoupe.ts"],"sourcesContent":["import type { View } from '@weasel-js/core';\nimport type { WidgetBounds } from '../widget';\n\n/**\n * The inner `View` a loupe renders its content through: the outer view's\n * magnification times `factor`, positioned so `target` sits at the center of\n * a viewport the size of `rect`.\n */\nexport function loupeInnerView(\n target: { x: number; y: number },\n outer: View,\n rect: WidgetBounds,\n factor: number,\n): View {\n const scale = { x: outer.scale.x * factor, y: outer.scale.y * factor };\n return {\n x: target.x - rect.w / 2 / scale.x,\n y: target.y - rect.h / 2 / scale.y,\n scale,\n };\n}\n","import type { WidgetBounds } from '../widget';\n\n/**\n * Where on the outer canvas a point inside the lens is looking, in\n * screen-space CSS px.\n *\n * The inverse of {@link loupeInnerView}: the lens centers `aim` in `rect` and\n * magnifies by `factor`, and the outer view's own scale cancels out of the\n * round trip — so this needs no `View`.\n */\nexport function loupeSourcePoint(\n p: { x: number; y: number },\n rect: WidgetBounds,\n aim: { x: number; y: number },\n factor: number,\n): { x: number; y: number } {\n return {\n x: aim.x + (p.x - (rect.x + rect.w / 2)) / factor,\n y: aim.y + (p.y - (rect.y + rect.h / 2)) / factor,\n };\n}\n","/**\n * Read a `rw × rh` device-pixel region of the drawing buffer centered on\n * `pointer` (CSS px relative to the canvas), returned top-down.\n *\n * GL reports rows bottom-up; every consumer here wants top-down, so both the\n * read origin and the returned rows are flipped.\n */\nexport function readbackRegion(\n gl: WebGL2RenderingContext,\n buffer: { width: number; height: number },\n pointer: { x: number; y: number },\n dpr: number,\n rw: number,\n rh: number,\n): ImageData {\n const cx = Math.round(pointer.x * dpr);\n const cy = Math.round(pointer.y * dpr);\n const gx = clamp(cx - Math.floor(rw / 2), 0, Math.max(0, buffer.width - rw));\n const gyTop = clamp(cy - Math.floor(rh / 2), 0, Math.max(0, buffer.height - rh));\n const gy = buffer.height - gyTop - rh;\n\n const raw = new Uint8Array(rw * rh * 4);\n gl.readPixels(gx, gy, rw, rh, gl.RGBA, gl.UNSIGNED_BYTE, raw);\n\n const flipped = new Uint8ClampedArray(rw * rh * 4);\n const stride = rw * 4;\n for (let row = 0; row < rh; row++) {\n const src = (rh - 1 - row) * stride;\n flipped.set(raw.subarray(src, src + stride), row * stride);\n }\n return new ImageData(flipped, rw, rh);\n}\n\nfunction clamp(v: number, lo: number, hi: number): number {\n return v < lo ? lo : v > hi ? hi : v;\n}\n","import {\n createViewportLayer, screenToWorld, viewToTransform,\n type RenderLayer, type View,\n} from '@weasel-js/core';\nimport type { DrawCommand } from '@weasel-js/core/renderer';\nimport type { Hud } from '../hud';\nimport type { WindowWidget } from '../widgets/window/window';\nimport type { HudContentCtx, WidgetBounds } from '../widget';\nimport { loupeInnerView } from './innerView';\nimport { loupeSourcePoint } from './pick';\nimport { readbackRegion } from './readback';\n\n/** How a loupe magnifies. `'vector'` re-renders the source layers through a\n * zoomed-in view, so content stays sharp at any factor; `'pixel'` reads the\n * framebuffer back and blows up the actual pixels. */\nexport type LoupeMode = 'vector' | 'pixel';\n\n/** Options for `createLoupe`. */\nexport interface LoupeOptions {\n hud: Hud;\n /** The live canvas. Supplies the GL context for pixel mode and the pointer\n * feed for aiming. */\n element: HTMLCanvasElement;\n /** Layers re-rendered through the magnified inner view in vector mode. */\n source: RenderLayer<unknown>[];\n requestRedraw: () => void;\n mode?: LoupeMode;\n factor?: number;\n bounds?: WidgetBounds;\n title?: string;\n /** Draw the window's titlebar and close box. Default `true`. Pass `false`\n * for a bare lens; the interior then doubles as the move handle. */\n titlebar?: boolean;\n /** Called with the hex color under the aim point whenever it changes.\n * Sampled from the framebuffer in both modes — vector content is a\n * re-render whose edge colors differ from the screen's, so reading the\n * color off it would report something the user cannot see. */\n onColorChange?: (hex: string) => void;\n /** Fired when a click inside the lens picks a color — an eyedropper on the\n * magnified surface. Where the color goes is the consumer's: the loupe\n * reports the pick and nothing else. */\n onPick?: (hex: string) => void;\n /** Fired by the window's close box. The window does not hide itself —\n * whoever owns the loupe's visibility decides what closing means. */\n onClose?: () => void;\n /** Backdrop painted behind the magnified content in both modes, so the\n * outer canvas does not show through where the lens is empty. Defaults to\n * the theme's surface color. */\n background?: string;\n}\n\n/** Control surface for a live loupe: where it is aimed, how it magnifies,\n * what color it is over, and the window it lives in. */\nexport interface LoupeHandle {\n readonly window: WindowWidget;\n readonly mode: LoupeMode;\n readonly factor: number;\n /** Last aimed point, in screen-space CSS px relative to the canvas. */\n readonly aim: { x: number; y: number };\n /** Hex color under the aim point, read off the framebuffer. `null` before\n * the first aim, or when the GL context is unavailable. */\n readonly color: string | null;\n setMode(mode: LoupeMode): void;\n setFactor(factor: number): void;\n /** Aim at a screen point. Ignored while the point is over the window —\n * that is the freeze rule that makes the borders reachable. */\n aimAt(p: { x: number; y: number }): void;\n /** Sample the color the lens is showing at `p` — a screen point inside the\n * lens — and report it to `onPick`. Omit `p` to pick at the aim point.\n * Returns the hex, or `null` when the framebuffer cannot answer for that\n * point. Leaves the aim, and `color`, alone. */\n pick(p?: { x: number; y: number }): string | null;\n dispose(): void;\n}\n\n/**\n * Build a magnifier: a HUD window whose interior shows the canvas around an\n * aim point, enlarged. Adds the window to the HUD and starts following the\n * pointer; `dispose` removes it again.\n */\nexport function createLoupe(opts: LoupeOptions): LoupeHandle {\n const { hud, element, source, requestRedraw } = opts;\n let mode: LoupeMode = opts.mode ?? 'vector';\n let factor = opts.factor ?? 8;\n let aim = { x: 0, y: 0 };\n let color: string | null = null;\n let pixels: ImageBitmap | null = null;\n let pixelsPending = false;\n let pixelsStale = false;\n let disposed = false;\n\n const b = opts.bounds ?? { x: 24, y: 24, w: 220, h: 200 };\n\n const content = (ctx: HudContentCtx): DrawCommand[] =>\n mode === 'vector' ? drawVector(ctx) : drawPixels(ctx);\n\n const backdropColor = (ctx: HudContentCtx): string =>\n opts.background ?? ctx.tokens['--wzl-surface'];\n\n const drawVector = (ctx: HudContentCtx): DrawCommand[] => {\n const world = screenToWorld(aim.x, aim.y, viewToTransform(ctx.view));\n const inner: View = loupeInnerView(\n { x: world[0], y: world[1] }, ctx.view, ctx.rect, factor,\n );\n // A fresh lens per frame: CreateViewportLayerOpts.view is static, and the\n // inner view moves with the pointer.\n const lens = createViewportLayer<unknown>({\n id: `${win.id}:lens`,\n label: 'Loupe content',\n source,\n view: inner,\n bounds: () => ctx.rect,\n background: backdropColor(ctx),\n });\n return lens.draw(ctx.data, ctx.view, ctx.dims);\n };\n\n const drawPixels = (ctx: HudContentCtx): DrawCommand[] => {\n // The backdrop paints even with a bitmap in hand: a readback of a\n // transparent framebuffer is transparent, and before the first one\n // settles there is nothing at all, either of which leaves the lens a\n // hole onto the unmagnified canvas.\n const out: DrawCommand[] = [{\n kind: 'path',\n path: { kind: 'rect', x: ctx.rect.x, y: ctx.rect.y, width: ctx.rect.w, height: ctx.rect.h },\n fill: { fill: 'solid', color: backdropColor(ctx) },\n }];\n if (pixels) {\n out.push({\n kind: 'image',\n image: pixels,\n x: ctx.rect.x, y: ctx.rect.y, w: ctx.rect.w, h: ctx.rect.h,\n sampling: 'nearest',\n });\n }\n return out;\n };\n\n const refreshPixels = () => {\n if (disposed || mode !== 'pixel') return;\n // A readback requested mid-flight is remembered rather than dropped: the\n // aim that arrives during a fast drag is the one the user ends on.\n if (pixelsPending) { pixelsStale = true; return; }\n const gl = element.getContext('webgl2');\n if (!gl) return;\n const cssRect = element.getBoundingClientRect();\n if (cssRect.width === 0) return;\n const dpr = element.width / cssRect.width;\n const rect = win.contentRect;\n const rw = Math.max(1, Math.round((rect.w * dpr) / factor));\n const rh = Math.max(1, Math.round((rect.h * dpr) / factor));\n const data = readbackRegion(\n gl, { width: element.width, height: element.height }, aim, dpr, rw, rh,\n );\n pixelsPending = true;\n pixelsStale = false;\n createImageBitmap(data)\n .then((bmp) => {\n if (disposed) { bmp.close(); return; }\n pixels?.close();\n pixels = bmp;\n pixelsPending = false;\n requestRedraw();\n if (pixelsStale) refreshPixels();\n })\n .catch(() => {\n pixelsPending = false;\n if (pixelsStale) refreshPixels();\n });\n };\n\n const readHex = (at: { x: number; y: number }): string | null => {\n const gl = element.getContext('webgl2');\n if (!gl) return null;\n const cssRect = element.getBoundingClientRect();\n if (cssRect.width === 0) return null;\n const dpr = element.width / cssRect.width;\n const px = readbackRegion(\n gl, { width: element.width, height: element.height }, at, dpr, 1, 1,\n );\n return '#' + [px.data[0], px.data[1], px.data[2]]\n .map((c) => c.toString(16).padStart(2, '0')).join('');\n };\n\n const sampleColor = () => {\n const hex = readHex(aim);\n if (hex === null || hex === color) return;\n color = hex;\n opts.onColorChange?.(hex);\n };\n\n const win = hud.window({\n id: 'weasel-loupe',\n x: b.x, y: b.y, w: b.w, h: b.h,\n title: opts.title ?? 'Loupe',\n titlebar: opts.titlebar,\n content,\n onResize: () => { refreshPixels(); },\n onContentClick: (p) => { pick(p); },\n ...(opts.onClose ? { onClose: opts.onClose } : {}),\n });\n\n const pick = (p?: { x: number; y: number }): string | null => {\n if (disposed) return null;\n const src = p ? loupeSourcePoint(p, win.contentRect, aim, factor) : aim;\n // The window is in the framebuffer too. A lens point near enough to the\n // frame maps back under it, and reading there reports chrome as artwork.\n if (p && win.hitTest(src.x, src.y)) return null;\n const hex = readHex(src);\n if (hex !== null) opts.onPick?.(hex);\n return hex;\n };\n\n const onPointerMove = (evt: PointerEvent) => {\n const r = element.getBoundingClientRect();\n handleAim({ x: evt.clientX - r.left, y: evt.clientY - r.top });\n };\n\n const handleAim = (p: { x: number; y: number }) => {\n if (disposed) return;\n // The window can be removed through the HUD, which disposes it without\n // going through `LoupeHandle.dispose`. Follow it down rather than keep\n // reading pixels back for a loupe nobody can see.\n if (win.disposed) { teardown(); return; }\n if (win.hidden) return;\n if (win.hitTest(p.x, p.y)) return; // freeze rule\n aim = p;\n sampleColor();\n if (mode === 'pixel') refreshPixels();\n requestRedraw();\n };\n\n const teardown = () => {\n disposed = true;\n element.removeEventListener('pointermove', onPointerMove);\n pixels?.close();\n pixels = null;\n };\n\n element.addEventListener('pointermove', onPointerMove);\n\n return {\n window: win,\n get mode() { return mode; },\n get factor() { return factor; },\n get aim() { return aim; },\n get color() { return color; },\n setMode(next) {\n if (next === mode) return;\n mode = next;\n if (mode === 'pixel') refreshPixels();\n requestRedraw();\n },\n setFactor(next) {\n factor = next;\n if (mode === 'pixel') refreshPixels();\n requestRedraw();\n },\n aimAt: handleAim,\n pick,\n dispose() {\n teardown();\n win.dispose();\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -27,11 +27,16 @@ export interface LoupeOptions {
|
|
|
27
27
|
* re-render whose edge colors differ from the screen's, so reading the
|
|
28
28
|
* color off it would report something the user cannot see. */
|
|
29
29
|
onColorChange?: (hex: string) => void;
|
|
30
|
+
/** Fired when a click inside the lens picks a color — an eyedropper on the
|
|
31
|
+
* magnified surface. Where the color goes is the consumer's: the loupe
|
|
32
|
+
* reports the pick and nothing else. */
|
|
33
|
+
onPick?: (hex: string) => void;
|
|
30
34
|
/** Fired by the window's close box. The window does not hide itself —
|
|
31
35
|
* whoever owns the loupe's visibility decides what closing means. */
|
|
32
36
|
onClose?: () => void;
|
|
33
|
-
/**
|
|
34
|
-
* show through where the
|
|
37
|
+
/** Backdrop painted behind the magnified content in both modes, so the
|
|
38
|
+
* outer canvas does not show through where the lens is empty. Defaults to
|
|
39
|
+
* the theme's surface color. */
|
|
35
40
|
background?: string;
|
|
36
41
|
}
|
|
37
42
|
/** Control surface for a live loupe: where it is aimed, how it magnifies,
|
|
@@ -56,6 +61,14 @@ export interface LoupeHandle {
|
|
|
56
61
|
x: number;
|
|
57
62
|
y: number;
|
|
58
63
|
}): void;
|
|
64
|
+
/** Sample the color the lens is showing at `p` — a screen point inside the
|
|
65
|
+
* lens — and report it to `onPick`. Omit `p` to pick at the aim point.
|
|
66
|
+
* Returns the hex, or `null` when the framebuffer cannot answer for that
|
|
67
|
+
* point. Leaves the aim, and `color`, alone. */
|
|
68
|
+
pick(p?: {
|
|
69
|
+
x: number;
|
|
70
|
+
y: number;
|
|
71
|
+
}): string | null;
|
|
59
72
|
dispose(): void;
|
|
60
73
|
}
|
|
61
74
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createLoupe.d.ts","sourceRoot":"","sources":["../../src/loupe/createLoupe.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EACjB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAClC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAiB,YAAY,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"createLoupe.d.ts","sourceRoot":"","sources":["../../src/loupe/createLoupe.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EACjB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAClC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAiB,YAAY,EAAE,MAAM,WAAW,CAAC;AAK7D;;uDAEuD;AACvD,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE3C,iCAAiC;AACjC,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,GAAG,CAAC;IACT;2BACuB;IACvB,OAAO,EAAE,iBAAiB,CAAC;IAC3B,0EAA0E;IAC1E,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;IAC/B,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;yEACqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;mEAG+D;IAC/D,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC;;6CAEyC;IACzC,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B;0EACsE;IACtE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;qCAEiC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;yDACyD;AACzD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACvC;gEAC4D;IAC5D,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,OAAO,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IAC/B,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;oEACgE;IAChE,KAAK,CAAC,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzC;;;qDAGiD;IACjD,IAAI,CAAC,CAAC,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,GAAG,IAAI,CAAC;IAClD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,WAAW,CAyL3D"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { WidgetBounds } from '../widget';
|
|
2
|
+
/**
|
|
3
|
+
* Where on the outer canvas a point inside the lens is looking, in
|
|
4
|
+
* screen-space CSS px.
|
|
5
|
+
*
|
|
6
|
+
* The inverse of {@link loupeInnerView}: the lens centers `aim` in `rect` and
|
|
7
|
+
* magnifies by `factor`, and the outer view's own scale cancels out of the
|
|
8
|
+
* round trip — so this needs no `View`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function loupeSourcePoint(p: {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
}, rect: WidgetBounds, aim: {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
}, factor: number): {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=pick.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pick.d.ts","sourceRoot":"","sources":["../../src/loupe/pick.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,CAAC,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAC3B,IAAI,EAAE,YAAY,EAClB,GAAG,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAC7B,MAAM,EAAE,MAAM,GACb;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAK1B"}
|
package/dist/react/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as e, n as t, r as n } from "../tool-
|
|
1
|
+
import { c as e, n as t, r as n } from "../tool-BPkQR6_K.js";
|
|
2
2
|
import { resolveTheme as r, weaselTheme as i } from "@weasel-js/theme";
|
|
3
3
|
import { useEffect as a, useMemo as o, useState as s } from "react";
|
|
4
4
|
import { useThemeOptional as c } from "@weasel-js/theme/react";
|