@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 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-Dta4BYKu.js";
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 m(e, t, n, r, i, a) {
18
- let o = Math.round(n.x * r), s = Math.round(n.y * r), c = h(o - Math.floor(i / 2), 0, Math.max(0, t.width - i)), l = h(s - Math.floor(a / 2), 0, Math.max(0, t.height - a)), u = t.height - l - a, d = new Uint8Array(i * a * 4);
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 h(e, t, n) {
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 g(e) {
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, h = !1, g = !1, _ = !1, v = e.bounds ?? {
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
- }, y = (e) => a === "vector" ? b(e) : x(e), b = (t) => {
42
- let n = d(s.x, s.y, f(t.view)), i = p({
43
- x: n[0],
44
- y: n[1]
45
- }, t.view, t.rect, o);
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: `${w.id}:lens`,
55
+ id: `${D.id}:lens`,
48
56
  label: "Loupe content",
49
57
  source: r,
50
- view: i,
51
- bounds: () => t.rect,
52
- background: e.background ?? t.tokens["--wzl-surface"]
53
- }).draw(t.data, t.view, t.dims);
54
- }, x = (e) => l ? [{
55
- kind: "image",
56
- image: l,
57
- x: e.rect.x,
58
- y: e.rect.y,
59
- w: e.rect.w,
60
- h: e.rect.h,
61
- sampling: "nearest"
62
- }] : [], S = () => {
63
- if (_ || a !== "pixel") return;
64
- if (h) {
65
- g = !0;
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 = w.contentRect, u = Math.max(1, Math.round(c.w * r / o)), d = Math.max(1, Math.round(c.h * r / o)), f = m(e, {
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
- h = !0, g = !1, createImageBitmap(f).then((e) => {
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, h = !1, i(), g && S();
105
+ l?.close(), l = e, g = !1, i(), _ && w();
82
106
  }).catch(() => {
83
- h = !1, g && S();
107
+ g = !1, _ && w();
84
108
  });
85
- }, C = () => {
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 = m(t, {
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
- }, s, i, 1, 1), o = "#" + [
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
- o !== c && (c = o, e.onColorChange?.(o));
99
- }, w = t.window({
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: v.x,
102
- y: v.y,
103
- w: v.w,
104
- h: v.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: y,
134
+ content: b,
108
135
  onResize: () => {
109
- S();
136
+ w();
137
+ },
138
+ onContentClick: (e) => {
139
+ O(e);
110
140
  },
111
141
  ...e.onClose ? { onClose: e.onClose } : {}
112
- }), T = (e) => {
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
- E({
150
+ A({
115
151
  x: e.clientX - t.left,
116
152
  y: e.clientY - t.top
117
153
  });
118
- }, E = (e) => {
119
- if (!_) {
120
- if (w.disposed) {
121
- D();
154
+ }, A = (e) => {
155
+ if (!v) {
156
+ if (D.disposed) {
157
+ j();
122
158
  return;
123
159
  }
124
- w.hidden || w.hitTest(e.x, e.y) || (s = e, C(), a === "pixel" && S(), i());
160
+ D.hidden || D.hitTest(e.x, e.y) || (s = e, E(), a === "pixel" && w(), i());
125
161
  }
126
- }, D = () => {
127
- _ = !0, n.removeEventListener("pointermove", T), l?.close(), l = null;
162
+ }, j = () => {
163
+ v = !0, n.removeEventListener("pointermove", k), l?.close(), l = null;
128
164
  };
129
- return n.addEventListener("pointermove", T), {
130
- window: w,
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" && S(), i());
180
+ e !== a && (a = e, a === "pixel" && w(), i());
145
181
  },
146
182
  setFactor(e) {
147
- o = e, a === "pixel" && S(), i();
183
+ o = e, a === "pixel" && w(), i();
148
184
  },
149
- aimAt: E,
185
+ aimAt: A,
186
+ pick: O,
150
187
  dispose() {
151
- D(), w.dispose();
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, g as createLoupe, l as cursorForZone, p as loupeInnerView, s as registerDefaultFont };
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":";;;AAQA,SAAgB,EACd,GACA,GACA,GACA,GACM;CACN,IAAM,IAAQ;EAAE,GAAG,EAAM,MAAM,IAAI;EAAQ,GAAG,EAAM,MAAM,IAAI;EAAQ;AACtE,QAAO;EACL,GAAG,EAAO,IAAI,EAAK,IAAI,IAAI,EAAM;EACjC,GAAG,EAAO,IAAI,EAAK,IAAI,IAAI,EAAM;EACjC;EACD;;;;ACTH,SAAgB,EACd,GACA,GACA,GACA,GAC0B;AAC1B,QAAO;EACL,GAAG,EAAI,KAAK,EAAE,KAAK,EAAK,IAAI,EAAK,IAAI,MAAM;EAC3C,GAAG,EAAI,KAAK,EAAE,KAAK,EAAK,IAAI,EAAK,IAAI,MAAM;EAC5C;;;;ACZH,SAAgB,EACd,GACA,GACA,GACA,GACA,GACA,GACW;CACX,IAAM,IAAK,KAAK,MAAM,EAAQ,IAAI,EAAI,EAChC,IAAK,KAAK,MAAM,EAAQ,IAAI,EAAI,EAChC,IAAK,EAAM,IAAK,KAAK,MAAM,IAAK,EAAE,EAAE,GAAG,KAAK,IAAI,GAAG,EAAO,QAAQ,EAAG,CAAC,EACtE,IAAQ,EAAM,IAAK,KAAK,MAAM,IAAK,EAAE,EAAE,GAAG,KAAK,IAAI,GAAG,EAAO,SAAS,EAAG,CAAC,EAC1E,IAAK,EAAO,SAAS,IAAQ,GAE7B,IAAM,IAAI,WAAW,IAAK,IAAK,EAAE;AACvC,GAAG,WAAW,GAAI,GAAI,GAAI,GAAI,EAAG,MAAM,EAAG,eAAe,EAAI;CAE7D,IAAM,IAAU,IAAI,kBAAkB,IAAK,IAAK,EAAE,EAC5C,IAAS,IAAK;AACpB,MAAK,IAAI,IAAM,GAAG,IAAM,GAAI,KAAO;EACjC,IAAM,KAAO,IAAK,IAAI,KAAO;AAC7B,IAAQ,IAAI,EAAI,SAAS,GAAK,IAAM,EAAO,EAAE,IAAM,EAAO;;AAE5D,QAAO,IAAI,UAAU,GAAS,GAAI,EAAG;;AAGvC,SAAS,EAAM,GAAW,GAAY,GAAoB;AACxD,QAAO,IAAI,IAAK,IAAK,IAAI,IAAK,IAAK;;;;AC8CrC,SAAgB,EAAY,GAAiC;CAC3D,IAAM,EAAE,QAAK,YAAS,WAAQ,qBAAkB,GAC5C,IAAkB,EAAK,QAAQ,UAC/B,IAAS,EAAK,UAAU,GACxB,IAAM;EAAE,GAAG;EAAG,GAAG;EAAG,EACpB,IAAuB,MACvB,IAA6B,MAC7B,IAAgB,IAChB,IAAc,IACd,IAAW,IAET,IAAI,EAAK,UAAU;EAAE,GAAG;EAAI,GAAG;EAAI,GAAG;EAAK,GAAG;EAAK,EAEnD,KAAW,MACf,MAAS,WAAW,EAAW,EAAI,GAAG,EAAW,EAAI,EAEjD,KAAiB,MACrB,EAAK,cAAc,EAAI,OAAO,kBAE1B,KAAc,MAAsC;EACxD,IAAM,IAAQ,EAAc,EAAI,GAAG,EAAI,GAAG,EAAgB,EAAI,KAAK,CAAC,EAC9D,IAAc,EAClB;GAAE,GAAG,EAAM;GAAI,GAAG,EAAM;GAAI,EAAE,EAAI,MAAM,EAAI,MAAM,EACnD;AAWD,SARa,EAA6B;GACxC,IAAI,GAAG,EAAI,GAAG;GACd,OAAO;GACP;GACA,MAAM;GACN,cAAc,EAAI;GAClB,YAAY,EAAc,EAAI;GAC/B,CACM,CAAK,KAAK,EAAI,MAAM,EAAI,MAAM,EAAI,KAAK;IAG1C,KAAc,MAAsC;EAKxD,IAAM,IAAqB,CAAC;GAC1B,MAAM;GACN,MAAM;IAAE,MAAM;IAAQ,GAAG,EAAI,KAAK;IAAG,GAAG,EAAI,KAAK;IAAG,OAAO,EAAI,KAAK;IAAG,QAAQ,EAAI,KAAK;IAAG;GAC3F,MAAM;IAAE,MAAM;IAAS,OAAO,EAAc,EAAI;IAAE;GACnD,CAAC;AASF,SARI,KACF,EAAI,KAAK;GACP,MAAM;GACN,OAAO;GACP,GAAG,EAAI,KAAK;GAAG,GAAG,EAAI,KAAK;GAAG,GAAG,EAAI,KAAK;GAAG,GAAG,EAAI,KAAK;GACzD,UAAU;GACX,CAAC,EAEG;IAGH,UAAsB;AAC1B,MAAI,KAAY,MAAS,QAAS;AAGlC,MAAI,GAAe;AAAE,OAAc;AAAM;;EACzC,IAAM,IAAK,EAAQ,WAAW,SAAS;AACvC,MAAI,CAAC,EAAI;EACT,IAAM,IAAU,EAAQ,uBAAuB;AAC/C,MAAI,EAAQ,UAAU,EAAG;EACzB,IAAM,IAAM,EAAQ,QAAQ,EAAQ,OAC9B,IAAO,EAAI,aACX,IAAK,KAAK,IAAI,GAAG,KAAK,MAAO,EAAK,IAAI,IAAO,EAAO,CAAC,EACrD,IAAK,KAAK,IAAI,GAAG,KAAK,MAAO,EAAK,IAAI,IAAO,EAAO,CAAC,EACrD,IAAO,EACX,GAAI;GAAE,OAAO,EAAQ;GAAO,QAAQ,EAAQ;GAAQ,EAAE,GAAK,GAAK,GAAI,EACrE;AAGD,EAFA,IAAgB,IAChB,IAAc,IACd,kBAAkB,EAAK,CACpB,MAAM,MAAQ;AACb,OAAI,GAAU;AAAE,MAAI,OAAO;AAAE;;AAK7B,GAJA,GAAQ,OAAO,EACf,IAAS,GACT,IAAgB,IAChB,GAAe,EACX,KAAa,GAAe;IAChC,CACD,YAAY;AAEX,GADA,IAAgB,IACZ,KAAa,GAAe;IAChC;IAGA,KAAW,MAAgD;EAC/D,IAAM,IAAK,EAAQ,WAAW,SAAS;AACvC,MAAI,CAAC,EAAI,QAAO;EAChB,IAAM,IAAU,EAAQ,uBAAuB;AAC/C,MAAI,EAAQ,UAAU,EAAG,QAAO;EAChC,IAAM,IAAM,EAAQ,QAAQ,EAAQ,OAC9B,IAAK,EACT,GAAI;GAAE,OAAO,EAAQ;GAAO,QAAQ,EAAQ;GAAQ,EAAE,GAAI,GAAK,GAAG,EACnE;AACD,SAAO,MAAM;GAAC,EAAG,KAAK;GAAI,EAAG,KAAK;GAAI,EAAG,KAAK;GAAG,CAC9C,KAAK,MAAM,EAAE,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,KAAK,GAAG;IAGnD,UAAoB;EACxB,IAAM,IAAM,EAAQ,EAAI;AACpB,QAAQ,QAAQ,MAAQ,MAC5B,IAAQ,GACR,EAAK,gBAAgB,EAAI;IAGrB,IAAM,EAAI,OAAO;EACrB,IAAI;EACJ,GAAG,EAAE;EAAG,GAAG,EAAE;EAAG,GAAG,EAAE;EAAG,GAAG,EAAE;EAC7B,OAAO,EAAK,SAAS;EACrB,UAAU,EAAK;EACf;EACA,gBAAgB;AAAE,MAAe;;EACjC,iBAAiB,MAAM;AAAE,KAAK,EAAE;;EAChC,GAAI,EAAK,UAAU,EAAE,SAAS,EAAK,SAAS,GAAG,EAAE;EAClD,CAAC,EAEI,KAAQ,MAAgD;AAC5D,MAAI,EAAU,QAAO;EACrB,IAAM,IAAM,IAAI,EAAiB,GAAG,EAAI,aAAa,GAAK,EAAO,GAAG;AAGpE,MAAI,KAAK,EAAI,QAAQ,EAAI,GAAG,EAAI,EAAE,CAAE,QAAO;EAC3C,IAAM,IAAM,EAAQ,EAAI;AAExB,SADI,MAAQ,QAAM,EAAK,SAAS,EAAI,EAC7B;IAGH,KAAiB,MAAsB;EAC3C,IAAM,IAAI,EAAQ,uBAAuB;AACzC,IAAU;GAAE,GAAG,EAAI,UAAU,EAAE;GAAM,GAAG,EAAI,UAAU,EAAE;GAAK,CAAC;IAG1D,KAAa,MAAgC;AAC7C,UAIJ;OAAI,EAAI,UAAU;AAAE,OAAU;AAAE;;AAC5B,KAAI,UACJ,EAAI,QAAQ,EAAE,GAAG,EAAE,EAAE,KACzB,IAAM,GACN,GAAa,EACT,MAAS,WAAS,GAAe,EACrC,GAAe;;IAGX,UAAiB;AAIrB,EAHA,IAAW,IACX,EAAQ,oBAAoB,eAAe,EAAc,EACzD,GAAQ,OAAO,EACf,IAAS;;AAKX,QAFA,EAAQ,iBAAiB,eAAe,EAAc,EAE/C;EACL,QAAQ;EACR,IAAI,OAAO;AAAE,UAAO;;EACpB,IAAI,SAAS;AAAE,UAAO;;EACtB,IAAI,MAAM;AAAE,UAAO;;EACnB,IAAI,QAAQ;AAAE,UAAO;;EACrB,QAAQ,GAAM;AACR,SAAS,MACb,IAAO,GACH,MAAS,WAAS,GAAe,EACrC,GAAe;;EAEjB,UAAU,GAAM;AAGd,GAFA,IAAS,GACL,MAAS,WAAS,GAAe,EACrC,GAAe;;EAEjB,OAAO;EACP;EACA,UAAU;AAER,GADA,GAAU,EACV,EAAI,SAAS;;EAEhB"}
@@ -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
- /** Opaque backdrop behind vector content, so the outer canvas does not
34
- * show through where the inner view is empty. */
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;AAI7D;;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;0EACsE;IACtE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;sDACkD;IAClD,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,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,WAAW,CA2J3D"}
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"}
@@ -1,4 +1,4 @@
1
- import { c as e, n as t, r as n } from "../tool-Dta4BYKu.js";
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";