@weasel-js/labkit 1.8.0 → 1.8.1

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.
Files changed (189) hide show
  1. package/README.md +24 -0
  2. package/dist/_dts/{CanvasStackContext-C0fzFsQg.d.ts → CanvasStackContext-B05ZM04l.d.ts} +3 -1
  3. package/dist/_dts/{Stage-Cvwxdnfp.d.ts → Stage-CIJlbjrE.d.ts} +5 -3
  4. package/dist/_dts/{index-DRaswz91.d.ts → index-8geQNQet.d.ts} +83 -11
  5. package/dist/_dts/{index-DEHRoTXK.d.ts → index-DRi3V5sN.d.ts} +5 -3
  6. package/dist/_dts/{useTrialState-CS4TdWPW.d.ts → useTrialState-Ar0AFpCe.d.ts} +94 -87
  7. package/dist/canvas/index.d.ts +4 -4
  8. package/dist/canvas/index.js +2 -2
  9. package/dist/chrome/index.d.ts +2 -2
  10. package/dist/chrome/index.js +7 -7
  11. package/dist/{chunk-YX75W6EK.js → chunk-4RCVWX4M.js} +8 -8
  12. package/dist/chunk-4RCVWX4M.js.map +1 -0
  13. package/dist/{chunk-WIG6XHJ7.js → chunk-6IIKLXBZ.js} +13 -4
  14. package/dist/chunk-6IIKLXBZ.js.map +1 -0
  15. package/dist/{chunk-F4ACIK7Z.js → chunk-6Q6ACJ3H.js} +3 -3
  16. package/dist/{chunk-F4ACIK7Z.js.map → chunk-6Q6ACJ3H.js.map} +1 -1
  17. package/dist/{chunk-PMLZP7RT.js → chunk-7ALJOVGP.js} +5 -9
  18. package/dist/chunk-7ALJOVGP.js.map +1 -0
  19. package/dist/{chunk-XE5HJOPL.js → chunk-7FMZT2WL.js} +3 -3
  20. package/dist/{chunk-XE5HJOPL.js.map → chunk-7FMZT2WL.js.map} +1 -1
  21. package/dist/{chunk-XVUKDSYV.js → chunk-AVOILDN2.js} +2 -2
  22. package/dist/{chunk-XVUKDSYV.js.map → chunk-AVOILDN2.js.map} +1 -1
  23. package/dist/{chunk-2N7UBSS2.js → chunk-J5MDJEOZ.js} +3 -3
  24. package/dist/{chunk-2N7UBSS2.js.map → chunk-J5MDJEOZ.js.map} +1 -1
  25. package/dist/{chunk-NVKBL7ZE.js → chunk-KHB3KRRQ.js} +7 -7
  26. package/dist/chunk-KHB3KRRQ.js.map +1 -0
  27. package/dist/{chunk-XXACAPVU.js → chunk-NHIO2O3O.js} +31 -30
  28. package/dist/chunk-NHIO2O3O.js.map +1 -0
  29. package/dist/{chunk-EESJUSYR.js → chunk-QMAY74TP.js} +56 -5
  30. package/dist/chunk-QMAY74TP.js.map +1 -0
  31. package/dist/{chunk-YDE6XATS.js → chunk-RN7T7OBO.js} +44 -22
  32. package/dist/chunk-RN7T7OBO.js.map +1 -0
  33. package/dist/{chunk-3L4MLLQB.js → chunk-RXO5UEGT.js} +4 -11
  34. package/dist/chunk-RXO5UEGT.js.map +1 -0
  35. package/dist/{chunk-MOTDMMMN.js → chunk-SQX35X5V.js} +3 -3
  36. package/dist/{chunk-MOTDMMMN.js.map → chunk-SQX35X5V.js.map} +1 -1
  37. package/dist/{chunk-J5HJJDLF.js → chunk-T5OEUNCB.js} +7 -2
  38. package/dist/chunk-T5OEUNCB.js.map +1 -0
  39. package/dist/{chunk-RL2LOLNI.js → chunk-UX66USTN.js} +2 -2
  40. package/dist/chunk-UX66USTN.js.map +1 -0
  41. package/dist/{chunk-WURZTJGQ.js → chunk-V4SMGEYY.js} +4858 -4806
  42. package/dist/chunk-V4SMGEYY.js.map +1 -0
  43. package/dist/{chunk-E23Q6A3Y.js → chunk-VCK5T6EP.js} +3 -3
  44. package/dist/{chunk-E23Q6A3Y.js.map → chunk-VCK5T6EP.js.map} +1 -1
  45. package/dist/{chunk-T6DLGEJB.js → chunk-XWR2P6MR.js} +23 -6
  46. package/dist/chunk-XWR2P6MR.js.map +1 -0
  47. package/dist/{chunk-7KAETQSS.js → chunk-YZA73U4A.js} +7 -7
  48. package/dist/chunk-YZA73U4A.js.map +1 -0
  49. package/dist/config/index.d.ts +2 -2
  50. package/dist/config/index.js +2 -2
  51. package/dist/controls/index.js +5 -5
  52. package/dist/dragdrop/index.d.ts +1 -1
  53. package/dist/dragdrop/index.js +2 -2
  54. package/dist/index.d.ts +157 -15
  55. package/dist/index.js +893 -258
  56. package/dist/index.js.map +1 -1
  57. package/dist/layers/index.d.ts +1 -1
  58. package/dist/layers/index.js +3 -3
  59. package/dist/loupe/index.d.ts +175 -27
  60. package/dist/loupe/index.js +547 -141
  61. package/dist/loupe/index.js.map +1 -1
  62. package/dist/overview/index.js +5 -5
  63. package/dist/passthrough/weasel-ui.js +3 -3
  64. package/dist/primitives/index.d.ts +4 -19
  65. package/dist/primitives/index.js +7 -7
  66. package/dist/state/index.d.ts +5 -7
  67. package/dist/state/index.js +5 -5
  68. package/dist/styles.css +173 -38
  69. package/dist/surface/index.d.ts +20 -1
  70. package/dist/surface/index.js +2 -2
  71. package/dist/undo/index.d.ts +18 -7
  72. package/dist/undo/index.js +1 -1
  73. package/package.json +11 -11
  74. package/src/annotations/AnnotationOverlay.abandon.test.tsx +4 -0
  75. package/src/annotations/AnnotationOverlay.tsx +10 -1
  76. package/src/annotations/Annotations.less +2 -0
  77. package/src/annotations/Annotations.overlay.test.tsx +29 -0
  78. package/src/canvas/CanvasStack.test.tsx +35 -0
  79. package/src/canvas/CanvasStack.tsx +12 -1
  80. package/src/canvas/useLayerScheduler.ts +14 -0
  81. package/src/chrome/builtins.test.ts +14 -0
  82. package/src/chrome/builtins.tsx +17 -2
  83. package/src/chrome/regions/StatusRegion.tsx +20 -14
  84. package/src/chrome/regions/regions.test.tsx +28 -0
  85. package/src/chrome/types.ts +8 -0
  86. package/src/clock/clock.browser.test.less +5 -0
  87. package/src/clock/clock.browser.test.tsx +77 -0
  88. package/src/clock/clock.integration.test.tsx +95 -0
  89. package/src/clock/clockRegistry.test.ts +33 -0
  90. package/src/clock/clockRegistry.ts +46 -0
  91. package/src/clock/hooks.ts +44 -0
  92. package/src/clock/index.ts +2 -0
  93. package/src/clock/trialClock.test.ts +265 -0
  94. package/src/clock/trialClock.ts +245 -0
  95. package/src/clock/useClockLoop.ts +45 -0
  96. package/src/config/resolve.test.ts +12 -0
  97. package/src/config/resolve.ts +5 -0
  98. package/src/controls/ControlMatrix.tsx +56 -54
  99. package/src/index.ts +3 -2
  100. package/src/instrument/types.ts +21 -1
  101. package/src/lab/Lab.chrome.test.tsx +8 -1
  102. package/src/lab/Lab.presentation.test.tsx +113 -0
  103. package/src/lab/Lab.surface.test.tsx +20 -9
  104. package/src/lab/Lab.tsx +132 -257
  105. package/src/lab/LabLightbox.browser.test.less +17 -0
  106. package/src/lab/LabLightbox.browser.test.tsx +216 -0
  107. package/src/lab/LabShell.less +10 -3
  108. package/src/lab/LabSurface.tsx +129 -0
  109. package/src/lab/Workspace.stories.tsx +18 -0
  110. package/src/lab/Workspace.surface.test.tsx +4 -0
  111. package/src/lab/Workspace.test.tsx +37 -1
  112. package/src/lab/Workspace.tsx +34 -6
  113. package/src/lab/index.ts +2 -0
  114. package/src/lab/nebula.ts +30 -0
  115. package/src/lab/openLab.test.ts +81 -0
  116. package/src/lab/openLab.ts +119 -0
  117. package/src/lab/presentation.browser.test.less +5 -0
  118. package/src/lab/presentation.browser.test.tsx +119 -0
  119. package/src/lab/presentation.less +64 -0
  120. package/src/lab/presentation.tsx +47 -0
  121. package/src/lab/useFocusPick.ts +43 -0
  122. package/src/lightbox/Lightbox.browser.test.less +14 -0
  123. package/src/lightbox/Lightbox.browser.test.tsx +113 -0
  124. package/src/lightbox/Lightbox.less +128 -0
  125. package/src/lightbox/Lightbox.test.tsx +195 -0
  126. package/src/lightbox/Lightbox.tsx +205 -0
  127. package/src/lightbox/LightboxContext.ts +38 -0
  128. package/src/lightbox/LightboxLayers.tsx +40 -0
  129. package/src/lightbox/index.ts +8 -0
  130. package/src/lightbox/modal.ts +94 -0
  131. package/src/loupe/AGENTS.md +40 -2
  132. package/src/loupe/CanvasLoupe.tsx +5 -4
  133. package/src/loupe/DomLoupe.tsx +7 -5
  134. package/src/loupe/Loupe.less +15 -0
  135. package/src/loupe/LoupeBubble.tsx +35 -13
  136. package/src/loupe/SourceLoupe.browser.test.tsx +204 -0
  137. package/src/loupe/SourceLoupe.tsx +60 -0
  138. package/src/loupe/TrialLoupe.browser.test.tsx +342 -0
  139. package/src/loupe/TrialLoupe.tsx +126 -28
  140. package/src/loupe/canvasLens.ts +20 -17
  141. package/src/loupe/index.ts +26 -2
  142. package/src/loupe/sourceLens.test.ts +65 -0
  143. package/src/loupe/sourceLens.ts +97 -0
  144. package/src/loupe/types.test.ts +6 -0
  145. package/src/loupe/types.ts +109 -7
  146. package/src/loupe/useLoupe.test.tsx +188 -3
  147. package/src/loupe/useLoupe.ts +106 -11
  148. package/src/primitives/index.ts +2 -2
  149. package/src/specimen/Specimen.tsx +0 -7
  150. package/src/state/helpers.test.ts +4 -11
  151. package/src/state/helpers.ts +2 -8
  152. package/src/state/index.ts +0 -2
  153. package/src/state/openLabStore.test.ts +10 -8
  154. package/src/state/openLabStore.ts +1 -1
  155. package/src/state/store.test.ts +2 -2
  156. package/src/state/store.ts +15 -16
  157. package/src/state/types.ts +9 -9
  158. package/src/styles.less +2 -1
  159. package/src/surface/AGENTS.md +13 -0
  160. package/src/surface/index.ts +1 -0
  161. package/src/surface/useSurfaceTile.test.tsx +4 -0
  162. package/src/surface/useSurfaceTile.ts +17 -1
  163. package/src/surface/useTiledSurface.test.tsx +65 -0
  164. package/src/surface/useTiledSurface.ts +68 -1
  165. package/src/trial/Trial.stories.tsx +0 -1
  166. package/src/trial/Trial.test.tsx +40 -3
  167. package/src/trial/Trial.tsx +87 -43
  168. package/src/trial/TrialChrome.tsx +20 -2
  169. package/src/trial/trialOps.test.ts +1 -2
  170. package/src/trial/trialOps.ts +2 -3
  171. package/src/undo/stateOp.ts +21 -0
  172. package/src/undo/undoStack.ts +18 -6
  173. package/dist/chunk-3L4MLLQB.js.map +0 -1
  174. package/dist/chunk-7KAETQSS.js.map +0 -1
  175. package/dist/chunk-EESJUSYR.js.map +0 -1
  176. package/dist/chunk-J5HJJDLF.js.map +0 -1
  177. package/dist/chunk-NVKBL7ZE.js.map +0 -1
  178. package/dist/chunk-PMLZP7RT.js.map +0 -1
  179. package/dist/chunk-RL2LOLNI.js.map +0 -1
  180. package/dist/chunk-T6DLGEJB.js.map +0 -1
  181. package/dist/chunk-WIG6XHJ7.js.map +0 -1
  182. package/dist/chunk-WURZTJGQ.js.map +0 -1
  183. package/dist/chunk-XXACAPVU.js.map +0 -1
  184. package/dist/chunk-YDE6XATS.js.map +0 -1
  185. package/dist/chunk-YX75W6EK.js.map +0 -1
  186. package/src/primitives/StatusBar.less +0 -34
  187. package/src/primitives/StatusBar.stories.tsx +0 -24
  188. package/src/primitives/StatusBar.test.tsx +0 -25
  189. package/src/primitives/StatusBar.tsx +0 -28
@@ -1,27 +1,266 @@
1
1
  import { LoupeSwitchContext } from '../chunk-2ES4W47N.js';
2
2
  import { centerOn, zoomAt } from '../chunk-ROGJWDQA.js';
3
3
  import { screenToWorld } from '../chunk-FG3EXZDV.js';
4
+ import { useSurfaceOptional } from '../chunk-6IIKLXBZ.js';
4
5
  import { CameraContext, CameraScope, fromCameraView } from '../chunk-VYVGVBGY.js';
5
6
  import { resolveFrame, DEFAULT_FRAME } from '../chunk-ULYYNWGP.js';
6
7
  import { CanvasStackContext } from '../chunk-ZQIHJXP4.js';
8
+ import '../chunk-2YILQ7Y5.js';
7
9
  import { useLatest, useVisibleRaf, useActionsRegistry, useGestureDispatcher, normalizeZoom } from '@weasel-js/core';
8
10
  import { useRef, useEffect, useContext, useReducer, useCallback, useMemo, useState } from 'react';
9
11
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
10
12
 
13
+ function loupeExtent(size) {
14
+ return typeof size === "number" ? { width: size, height: size } : size;
15
+ }
16
+ function loupeSourcePoint(p, rect, aim, factor) {
17
+ return {
18
+ x: aim.x + (p.x - (rect.x + rect.w / 2)) / factor,
19
+ y: aim.y + (p.y - (rect.y + rect.h / 2)) / factor
20
+ };
21
+ }
22
+ function placeBand({ band, factor, maxWidth, maxHeight }) {
23
+ let fit = factor;
24
+ if (band.w > 0) fit = Math.min(fit, maxWidth / band.w);
25
+ if (maxHeight !== void 0 && band.h > 0) fit = Math.min(fit, maxHeight / band.h);
26
+ const width = band.w * fit;
27
+ const height = band.h * fit;
28
+ const shows = { x: band.x + band.w / 2, y: band.y + band.h / 2 };
29
+ const within = (c, half, max) => Math.min(Math.max(c, half), Math.max(half, max - half));
30
+ return {
31
+ center: {
32
+ x: within(shows.x, width / 2, maxWidth),
33
+ y: maxHeight === void 0 ? shows.y : within(shows.y, height / 2, maxHeight)
34
+ },
35
+ shows,
36
+ width,
37
+ height,
38
+ factor: fit
39
+ };
40
+ }
41
+ function createLoupeModel(opts) {
42
+ const { surface } = opts;
43
+ let mode = opts.mode ?? "vector";
44
+ let factor = normalizeZoom(opts.factor ?? 8);
45
+ let aim = { x: 0, y: 0 };
46
+ let color = null;
47
+ let disposed = false;
48
+ let aimUnsampled = false;
49
+ const clamp = (n) => normalizeZoom(Math.min(
50
+ opts.maxFactor ?? Number.POSITIVE_INFINITY,
51
+ Math.max(opts.minFactor ?? Number.NEGATIVE_INFINITY, n)
52
+ ));
53
+ const sampleColor = () => {
54
+ const hex = surface.sample(aim);
55
+ if (hex === null || hex === color) return;
56
+ color = hex;
57
+ opts.onColorChange?.(hex);
58
+ };
59
+ const unsubscribeFrame = surface.subscribeFrame?.(() => {
60
+ if (!aimUnsampled) return;
61
+ aimUnsampled = false;
62
+ sampleColor();
63
+ });
64
+ const teardown = () => {
65
+ if (disposed) return;
66
+ disposed = true;
67
+ unsubscribeFrame?.();
68
+ opts.onDispose?.();
69
+ };
70
+ return {
71
+ get mode() {
72
+ return mode;
73
+ },
74
+ get factor() {
75
+ return factor;
76
+ },
77
+ get aim() {
78
+ return aim;
79
+ },
80
+ get color() {
81
+ return color;
82
+ },
83
+ setMode(next) {
84
+ if (disposed || next === mode) return;
85
+ mode = next;
86
+ surface.changed();
87
+ },
88
+ setFactor(next) {
89
+ if (disposed) return;
90
+ factor = clamp(next);
91
+ surface.changed();
92
+ },
93
+ aimAt(p) {
94
+ if (disposed) return;
95
+ if (surface.gone()) {
96
+ teardown();
97
+ return;
98
+ }
99
+ if (surface.hidden()) return;
100
+ if (surface.covers(p)) return;
101
+ aim = p;
102
+ if (unsubscribeFrame) aimUnsampled = true;
103
+ else sampleColor();
104
+ surface.changed();
105
+ },
106
+ pick(p) {
107
+ if (disposed) return null;
108
+ let at = aim;
109
+ if (p) {
110
+ const rect = surface.lens();
111
+ if (!rect) return null;
112
+ at = loupeSourcePoint(p, rect, rect.shows ?? aim, rect.factor ?? factor);
113
+ if (surface.covers(at)) return null;
114
+ }
115
+ const hex = surface.sample(at);
116
+ if (hex !== null) opts.onPick?.(hex);
117
+ return hex;
118
+ },
119
+ resample() {
120
+ if (disposed || surface.hidden()) return;
121
+ aimUnsampled = false;
122
+ sampleColor();
123
+ },
124
+ dispose: teardown
125
+ };
126
+ }
127
+ var MISSED_FRAMES_BEFORE_WARNING = 120;
128
+ function isWebGL(ctx) {
129
+ return "drawArrays" in ctx;
130
+ }
131
+ function isContext(target) {
132
+ return "canvas" in target && !("getContext" in target);
133
+ }
134
+ function makeCanvas(width, height) {
135
+ if (typeof OffscreenCanvas !== "undefined") return new OffscreenCanvas(width, height);
136
+ const c = document.createElement("canvas");
137
+ c.width = width;
138
+ c.height = height;
139
+ return c;
140
+ }
141
+ function context2d(canvas, willReadFrequently = false) {
142
+ return canvas.getContext("2d", { willReadFrequently });
143
+ }
144
+ function createCanvasSource(target, options = {}) {
145
+ const canvas = isContext(target) ? target.canvas : target;
146
+ const mustCapture = isContext(target) && isWebGL(target) && !target.getContextAttributes()?.preserveDrawingBuffer;
147
+ let captured = mustCapture;
148
+ let readers = 0;
149
+ let snapshot = null;
150
+ let current = false;
151
+ let missed = 0;
152
+ let warned = false;
153
+ let probe = null;
154
+ const listeners = /* @__PURE__ */ new Set();
155
+ const source = {
156
+ canvas,
157
+ get captured() {
158
+ return captured;
159
+ },
160
+ box: options.box,
161
+ capture() {
162
+ captured = true;
163
+ if (readers === 0) return;
164
+ const { width, height } = canvas;
165
+ if (width === 0 || height === 0) return;
166
+ if (!snapshot) snapshot = makeCanvas(width, height);
167
+ else if (snapshot.width !== width || snapshot.height !== height) {
168
+ snapshot.width = width;
169
+ snapshot.height = height;
170
+ }
171
+ const ctx = context2d(snapshot);
172
+ if (!ctx) return;
173
+ ctx.globalCompositeOperation = "copy";
174
+ ctx.drawImage(canvas, 0, 0);
175
+ current = true;
176
+ missed = 0;
177
+ for (const fn of listeners) fn();
178
+ },
179
+ frame() {
180
+ if (!captured) return canvas.width > 0 && canvas.height > 0 ? canvas : null;
181
+ if (current && snapshot) return snapshot;
182
+ if (!warned && ++missed >= MISSED_FRAMES_BEFORE_WARNING) {
183
+ warned = true;
184
+ console.warn(
185
+ "[weasel loupe] A WebGL canvas without preserveDrawingBuffer is being read, and nothing has called capture() on its source. Call source.capture() right after drawing each frame, or pass requestRedraw if the canvas draws on demand."
186
+ );
187
+ }
188
+ return null;
189
+ },
190
+ sample(p) {
191
+ const frame = source.frame();
192
+ if (!frame) return null;
193
+ const x = Math.floor(p.x);
194
+ const y = Math.floor(p.y);
195
+ if (x < 0 || y < 0 || x >= canvas.width || y >= canvas.height) return null;
196
+ if (!probe) {
197
+ const c = makeCanvas(1, 1);
198
+ probe = context2d(c, true);
199
+ if (!probe) return null;
200
+ }
201
+ probe.globalCompositeOperation = "copy";
202
+ probe.drawImage(frame, x, y, 1, 1, 0, 0, 1, 1);
203
+ const [r, g, b, a] = probe.getImageData(0, 0, 1, 1).data;
204
+ if (a === 0) return null;
205
+ return `#${[r, g, b].map((c) => c.toString(16).padStart(2, "0")).join("")}`;
206
+ },
207
+ retain() {
208
+ readers++;
209
+ if (readers === 1 && captured) {
210
+ current = false;
211
+ options.requestRedraw?.();
212
+ }
213
+ let held = true;
214
+ return () => {
215
+ if (!held) return;
216
+ held = false;
217
+ readers--;
218
+ };
219
+ },
220
+ subscribeFrame(fn) {
221
+ listeners.add(fn);
222
+ return () => {
223
+ listeners.delete(fn);
224
+ };
225
+ }
226
+ };
227
+ return source;
228
+ }
229
+ function sourceRegion(aim, factor, size, box, backing) {
230
+ const { width, height } = loupeExtent(size);
231
+ const spanX = width / factor;
232
+ const spanY = height / factor;
233
+ const kx = box.width > 0 ? backing.width / box.width : 1;
234
+ const ky = box.height > 0 ? backing.height / box.height : 1;
235
+ return {
236
+ sx: (aim.x - spanX / 2 - box.x) * kx,
237
+ sy: (aim.y - spanY / 2 - box.y) * ky,
238
+ sw: spanX * kx,
239
+ sh: spanY * ky
240
+ };
241
+ }
242
+ function sourcePixel(p, box, backing) {
243
+ const kx = box.width > 0 ? backing.width / box.width : 1;
244
+ const ky = box.height > 0 ? backing.height / box.height : 1;
245
+ return { x: (p.x - box.x) * kx, y: (p.y - box.y) * ky };
246
+ }
247
+
11
248
  // src/loupe/canvasLens.ts
12
- function lensCamera(aim, outer, outerFrame, factor, diameter, worldSpec) {
13
- const size = { width: diameter, height: diameter };
14
- const frame = resolveFrame(worldSpec, size);
249
+ function lensCamera(aim, outer, outerFrame, factor, size, worldSpec) {
250
+ const extent = loupeExtent(size);
251
+ const frame = resolveFrame(worldSpec, extent);
15
252
  const world = screenToWorld(aim, outer, outerFrame);
16
- return { view: centerOn(world, outer.zoom * factor, size, frame), frame };
253
+ return { view: centerOn(world, outer.zoom * factor, extent, frame), frame };
17
254
  }
18
- function lensSourceRect(aim, factor, diameter, dpr) {
19
- const span = diameter / factor;
255
+ function lensSourceRect(aim, factor, size, dpr) {
256
+ const { width, height } = loupeExtent(size);
257
+ const spanX = width / factor;
258
+ const spanY = height / factor;
20
259
  return {
21
- sx: (aim.x - span / 2) * dpr,
22
- sy: (aim.y - span / 2) * dpr,
23
- sw: span * dpr,
24
- sh: span * dpr
260
+ sx: (aim.x - spanX / 2) * dpr,
261
+ sy: (aim.y - spanY / 2) * dpr,
262
+ sw: spanX * dpr,
263
+ sh: spanY * dpr
25
264
  };
26
265
  }
27
266
  function sampleStack(layers, canvases, p, dpr) {
@@ -43,9 +282,10 @@ function sampleStack(layers, canvases, p, dpr) {
43
282
  }
44
283
  function drawCanvasLens(ctx, opts) {
45
284
  const { aim, factor, diameter, dpr, mode, outer, layers, canvases } = opts;
285
+ const { width, height } = loupeExtent(diameter);
46
286
  ctx.save();
47
287
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
48
- ctx.clearRect(0, 0, diameter, diameter);
288
+ ctx.clearRect(0, 0, width, height);
49
289
  if (mode === "pixel") {
50
290
  const { sx, sy, sw, sh } = lensSourceRect(aim, factor, diameter, dpr);
51
291
  ctx.imageSmoothingEnabled = false;
@@ -53,7 +293,7 @@ function drawCanvasLens(ctx, opts) {
53
293
  if (!layer.visible) continue;
54
294
  const canvas = canvases.get(layer.id);
55
295
  if (!canvas || canvas.width === 0 || canvas.height === 0) continue;
56
- ctx.drawImage(canvas, sx, sy, sw, sh, 0, 0, diameter, diameter);
296
+ ctx.drawImage(canvas, sx, sy, sw, sh, 0, 0, width, height);
57
297
  }
58
298
  ctx.restore();
59
299
  return;
@@ -120,8 +360,8 @@ function CanvasLoupe({
120
360
  {
121
361
  ref: canvasRef,
122
362
  className: "lk-loupe__canvas",
123
- width: Math.max(1, Math.round(diameter * dpr)),
124
- height: Math.max(1, Math.round(diameter * dpr))
363
+ width: Math.max(1, Math.round(loupeExtent(diameter).width * dpr)),
364
+ height: Math.max(1, Math.round(loupeExtent(diameter).height * dpr))
125
365
  }
126
366
  );
127
367
  }
@@ -135,10 +375,11 @@ function DomLoupe({
135
375
  frame,
136
376
  render
137
377
  }) {
378
+ const { width, height } = loupeExtent(diameter);
138
379
  const style = {
139
380
  width: `${size.width}px`,
140
381
  height: `${size.height}px`,
141
- transform: `translate(${diameter / 2 - aim.x}px, ${diameter / 2 - aim.y}px)`
382
+ transform: `translate(${width / 2 - aim.x}px, ${height / 2 - aim.y}px)`
142
383
  };
143
384
  return /* @__PURE__ */ jsx("div", { className: "lk-loupe__stage", style, children: render({
144
385
  view: zoomAt(view, factor, aim, { frame }),
@@ -147,13 +388,31 @@ function DomLoupe({
147
388
  size
148
389
  }) });
149
390
  }
150
- function LoupeBubble({ aim, diameter, hostRef, children }) {
391
+ function LoupeBubble({
392
+ aim,
393
+ diameter,
394
+ shape = "circle",
395
+ hollow,
396
+ hostRef,
397
+ children
398
+ }) {
399
+ const { width, height } = loupeExtent(diameter);
400
+ const square = shape === "square";
151
401
  const style = {
152
- width: `${diameter}px`,
153
- height: `${diameter}px`,
154
- transform: `translate(${aim.x - diameter / 2}px, ${aim.y - diameter / 2}px)`
402
+ "--lk-loupe-width": `${width}px`,
403
+ "--lk-loupe-height": `${height}px`,
404
+ transform: `translate(${aim.x - width / 2}px, ${aim.y - height / 2}px)`
155
405
  };
156
- return /* @__PURE__ */ jsx("div", { ref: hostRef, className: "lk-loupe", style, "aria-hidden": "true", children });
406
+ return /* @__PURE__ */ jsx(
407
+ "div",
408
+ {
409
+ ref: hostRef,
410
+ className: ["lk-loupe", square && "lk-loupe--square", hollow && "lk-loupe--hollow"].filter(Boolean).join(" "),
411
+ style,
412
+ "aria-hidden": "true",
413
+ children
414
+ }
415
+ );
157
416
  }
158
417
 
159
418
  // src/loupe/loupeActions.ts
@@ -252,6 +511,95 @@ function LoupeGestures(props) {
252
511
  return /* @__PURE__ */ jsx(LoupeDispatch, { ...props, registry, ownDispatcher: camera === null });
253
512
  }
254
513
 
514
+ // src/loupe/sourceLens.ts
515
+ function isCanvasSource(t) {
516
+ return "capture" in t && "retain" in t;
517
+ }
518
+ var made = /* @__PURE__ */ new WeakMap();
519
+ function resolveLoupeSource(source) {
520
+ if (!source) return null;
521
+ const target = typeof source === "function" ? source() : source;
522
+ if (!target) return null;
523
+ if (isCanvasSource(target)) return target;
524
+ let s = made.get(target);
525
+ if (!s) {
526
+ s = createCanvasSource(target);
527
+ made.set(target, s);
528
+ }
529
+ return s;
530
+ }
531
+ function sourceBoxIn(source, host) {
532
+ const own = source.box?.();
533
+ if (own) return own;
534
+ const hostRect = host?.getBoundingClientRect();
535
+ const c = source.canvas;
536
+ if (typeof HTMLCanvasElement !== "undefined" && c instanceof HTMLCanvasElement && c.isConnected) {
537
+ const r = c.getBoundingClientRect();
538
+ if (r.width > 0 && r.height > 0) {
539
+ return {
540
+ x: r.left - (hostRect?.left ?? 0),
541
+ y: r.top - (hostRect?.top ?? 0),
542
+ width: r.width,
543
+ height: r.height
544
+ };
545
+ }
546
+ }
547
+ return { x: 0, y: 0, width: hostRect?.width ?? c.width, height: hostRect?.height ?? c.height };
548
+ }
549
+ function sampleSource(source, p, box) {
550
+ return source.sample(sourcePixel(p, box, source.canvas));
551
+ }
552
+ function drawSourceLens(ctx, opts) {
553
+ const { aim, factor, diameter, dpr, source, box } = opts;
554
+ const { width, height } = loupeExtent(diameter);
555
+ ctx.save();
556
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
557
+ ctx.clearRect(0, 0, width, height);
558
+ const frame = source.frame();
559
+ if (frame) {
560
+ const { sx, sy, sw, sh } = sourceRegion(aim, factor, diameter, box, source.canvas);
561
+ ctx.imageSmoothingEnabled = false;
562
+ ctx.drawImage(frame, sx, sy, sw, sh, 0, 0, width, height);
563
+ }
564
+ ctx.restore();
565
+ }
566
+ function SourceLoupe({ aim, factor, diameter, source, hostRef }) {
567
+ const canvasRef = useRef(null);
568
+ const dpr = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1;
569
+ const argsRef = useLatest({ aim, factor, diameter, dpr, source });
570
+ const loop = useVisibleRaf(
571
+ () => {
572
+ loop.request();
573
+ const ctx = canvasRef.current?.getContext("2d");
574
+ const a = argsRef.current;
575
+ const src = resolveLoupeSource(a.source);
576
+ if (!ctx || !src) return;
577
+ drawSourceLens(ctx, {
578
+ aim: a.aim,
579
+ factor: a.factor,
580
+ diameter: a.diameter,
581
+ dpr: a.dpr,
582
+ source: src,
583
+ box: sourceBoxIn(src, hostRef.current)
584
+ });
585
+ },
586
+ { target: canvasRef }
587
+ );
588
+ useEffect(() => {
589
+ loop.request();
590
+ return () => loop.cancel();
591
+ }, [loop]);
592
+ return /* @__PURE__ */ jsx(
593
+ "canvas",
594
+ {
595
+ ref: canvasRef,
596
+ className: "lk-loupe__canvas",
597
+ width: Math.max(1, Math.round(loupeExtent(diameter).width * dpr)),
598
+ height: Math.max(1, Math.round(loupeExtent(diameter).height * dpr))
599
+ }
600
+ );
601
+ }
602
+
255
603
  // src/loupe/types.ts
256
604
  var LOUPE_DEFAULTS = {
257
605
  factor: 6,
@@ -259,6 +607,8 @@ var LOUPE_DEFAULTS = {
259
607
  maxFactor: 32,
260
608
  mode: "vector",
261
609
  diameter: 200,
610
+ shape: "circle",
611
+ hollow: false,
262
612
  peekKey: "Alt"
263
613
  };
264
614
  function resolveLoupe(options = {}) {
@@ -266,14 +616,19 @@ function resolveLoupe(options = {}) {
266
616
  const maxFactor = options.maxFactor ?? LOUPE_DEFAULTS.maxFactor;
267
617
  return {
268
618
  render: options.render,
619
+ source: options.source,
269
620
  onColorChange: options.onColorChange,
270
621
  minFactor,
271
622
  maxFactor,
272
623
  factor: Math.min(maxFactor, Math.max(minFactor, options.factor ?? LOUPE_DEFAULTS.factor)),
273
624
  diameter: options.diameter ?? LOUPE_DEFAULTS.diameter,
625
+ shape: options.shape ?? LOUPE_DEFAULTS.shape,
626
+ place: options.place,
627
+ hollow: options.hollow ?? LOUPE_DEFAULTS.hollow,
628
+ onLens: options.onLens,
274
629
  peekKey: options.peekKey === void 0 ? LOUPE_DEFAULTS.peekKey : options.peekKey,
275
630
  // A DOM loupe has no framebuffer, so `pixel` would have nothing to enlarge.
276
- mode: options.render ? "vector" : options.mode ?? LOUPE_DEFAULTS.mode
631
+ mode: options.render ? "vector" : options.source ? "pixel" : options.mode ?? LOUPE_DEFAULTS.mode
277
632
  };
278
633
  }
279
634
  var ZERO = { width: 0, height: 0 };
@@ -296,99 +651,19 @@ function useHostSize(ref) {
296
651
  }, [ref]);
297
652
  return size;
298
653
  }
299
- function loupeSourcePoint(p, rect, aim, factor) {
300
- return {
301
- x: aim.x + (p.x - (rect.x + rect.w / 2)) / factor,
302
- y: aim.y + (p.y - (rect.y + rect.h / 2)) / factor
303
- };
304
- }
305
- function createLoupeModel(opts) {
306
- const { surface } = opts;
307
- let mode = opts.mode ?? "vector";
308
- let factor = normalizeZoom(opts.factor ?? 8);
309
- let aim = { x: 0, y: 0 };
310
- let color = null;
311
- let disposed = false;
312
- let aimUnsampled = false;
313
- const clamp = (n) => normalizeZoom(Math.min(
314
- opts.maxFactor ?? Number.POSITIVE_INFINITY,
315
- Math.max(opts.minFactor ?? Number.NEGATIVE_INFINITY, n)
316
- ));
317
- const sampleColor = () => {
318
- const hex = surface.sample(aim);
319
- if (hex === null || hex === color) return;
320
- color = hex;
321
- opts.onColorChange?.(hex);
322
- };
323
- const unsubscribeFrame = surface.subscribeFrame?.(() => {
324
- if (!aimUnsampled) return;
325
- aimUnsampled = false;
326
- sampleColor();
327
- });
328
- const teardown = () => {
329
- if (disposed) return;
330
- disposed = true;
331
- unsubscribeFrame?.();
332
- opts.onDispose?.();
333
- };
334
- return {
335
- get mode() {
336
- return mode;
337
- },
338
- get factor() {
339
- return factor;
340
- },
341
- get aim() {
342
- return aim;
343
- },
344
- get color() {
345
- return color;
346
- },
347
- setMode(next) {
348
- if (disposed || next === mode) return;
349
- mode = next;
350
- surface.changed();
351
- },
352
- setFactor(next) {
353
- if (disposed) return;
354
- factor = clamp(next);
355
- surface.changed();
356
- },
357
- aimAt(p) {
358
- if (disposed) return;
359
- if (surface.gone()) {
360
- teardown();
361
- return;
362
- }
363
- if (surface.hidden()) return;
364
- if (surface.covers(p)) return;
365
- aim = p;
366
- if (unsubscribeFrame) aimUnsampled = true;
367
- else sampleColor();
368
- surface.changed();
369
- },
370
- pick(p) {
371
- if (disposed) return null;
372
- let at = aim;
373
- if (p) {
374
- const rect = surface.lens();
375
- if (!rect) return null;
376
- at = loupeSourcePoint(p, rect, aim, factor);
377
- if (surface.covers(at)) return null;
378
- }
379
- const hex = surface.sample(at);
380
- if (hex !== null) opts.onPick?.(hex);
381
- return hex;
382
- },
383
- dispose: teardown
384
- };
385
- }
386
654
  function lensShown(over, enabled, peeking) {
387
655
  return over && (enabled || peeking);
388
656
  }
389
- function useLoupe({ options, hostRef, enabled, sample }) {
657
+ function useLoupe({
658
+ options,
659
+ hostRef,
660
+ enabled,
661
+ sample,
662
+ subscribeResample
663
+ }) {
390
664
  const [, bump] = useReducer((n) => n + 1, 0);
391
665
  const overRef = useRef(false);
666
+ const pointerRef = useRef(null);
392
667
  const peekingRef = useRef(false);
393
668
  const enabledRef = useLatest(enabled);
394
669
  const sampleRef = useLatest(sample);
@@ -407,9 +682,15 @@ function useLoupe({ options, hostRef, enabled, sample }) {
407
682
  onColorChange: (hex) => onColorChangeRef.current?.(hex),
408
683
  surface: {
409
684
  lens: () => {
410
- const d = diameterRef.current;
411
- const { x, y } = modelRef.current?.aim ?? { x: 0, y: 0 };
412
- return { x: x - d / 2, y: y - d / 2, w: d, h: d };
685
+ const { center, shows, width, height, factor } = lensRef.current;
686
+ return {
687
+ x: center.x - width / 2,
688
+ y: center.y - height / 2,
689
+ w: width,
690
+ h: height,
691
+ shows,
692
+ factor
693
+ };
413
694
  },
414
695
  // The lens follows the pointer and is painted over the host rather than
415
696
  // into it, so it is never part of the picture being magnified.
@@ -422,7 +703,6 @@ function useLoupe({ options, hostRef, enabled, sample }) {
422
703
  });
423
704
  }
424
705
  const model = modelRef.current;
425
- const diameterRef = useLatest(options.diameter);
426
706
  const goneRef = useRef(false);
427
707
  useEffect(() => {
428
708
  goneRef.current = false;
@@ -430,14 +710,24 @@ function useLoupe({ options, hostRef, enabled, sample }) {
430
710
  goneRef.current = true;
431
711
  };
432
712
  }, []);
713
+ const hostRefRef = useLatest(hostRef);
714
+ const aimAtPointer = useCallback(() => {
715
+ const host = hostRefRef.current.current;
716
+ const p = pointerRef.current;
717
+ if (!host || !p) return;
718
+ const rect = host.getBoundingClientRect();
719
+ modelRef.current?.aimAt({ x: p.clientX - rect.left, y: p.clientY - rect.top });
720
+ }, [hostRefRef]);
433
721
  useEffect(() => {
434
- if (!enabled && !peekingRef.current) bump();
435
- }, [enabled]);
722
+ if (enabled) aimAtPointer();
723
+ else if (!peekingRef.current) bump();
724
+ }, [enabled, aimAtPointer]);
436
725
  useEffect(() => {
437
726
  const host = hostRef.current;
438
727
  if (!host) return;
439
728
  const onPointerMove = (e) => {
440
729
  overRef.current = true;
730
+ pointerRef.current = { clientX: e.clientX, clientY: e.clientY };
441
731
  const rect = host.getBoundingClientRect();
442
732
  model.aimAt({ x: e.clientX - rect.left, y: e.clientY - rect.top });
443
733
  if (!shown()) return;
@@ -445,6 +735,7 @@ function useLoupe({ options, hostRef, enabled, sample }) {
445
735
  };
446
736
  const onPointerLeave = () => {
447
737
  overRef.current = false;
738
+ pointerRef.current = null;
448
739
  bump();
449
740
  };
450
741
  host.addEventListener("pointermove", onPointerMove);
@@ -454,6 +745,45 @@ function useLoupe({ options, hostRef, enabled, sample }) {
454
745
  host.removeEventListener("pointerleave", onPointerLeave);
455
746
  };
456
747
  }, [hostRef, model, shown]);
748
+ const visible = lensShown(overRef.current, enabled, peekingRef.current);
749
+ const placed = visible ? options.place?.({ aim: model.aim, factor: model.factor }) ?? null : null;
750
+ const lens = placed ? {
751
+ center: placed.center,
752
+ shows: placed.shows ?? placed.center,
753
+ width: placed.width,
754
+ height: placed.height,
755
+ factor: placed.factor ?? model.factor,
756
+ shape: options.shape
757
+ } : {
758
+ center: model.aim,
759
+ shows: model.aim,
760
+ width: options.diameter,
761
+ height: options.diameter,
762
+ factor: model.factor,
763
+ shape: options.shape
764
+ };
765
+ const lensRef = useLatest(lens);
766
+ const onLensRef = useLatest(options.onLens);
767
+ const reportedRef = useRef(JSON.stringify(null));
768
+ const report = useCallback(
769
+ (next) => {
770
+ const key = JSON.stringify(next);
771
+ if (key === reportedRef.current) return;
772
+ reportedRef.current = key;
773
+ onLensRef.current?.(next);
774
+ },
775
+ [onLensRef]
776
+ );
777
+ const shownLens = visible ? lens : null;
778
+ const shownKey = JSON.stringify(shownLens);
779
+ useEffect(() => {
780
+ report(JSON.parse(shownKey));
781
+ }, [shownKey, report]);
782
+ useEffect(() => () => report(null), [report]);
783
+ useEffect(() => {
784
+ if (!visible || !subscribeResample) return;
785
+ return subscribeResample(() => model.resample());
786
+ }, [visible, subscribeResample, model]);
457
787
  const inputRef = useRef(null);
458
788
  if (inputRef.current === null) {
459
789
  inputRef.current = {
@@ -461,6 +791,7 @@ function useLoupe({ options, hostRef, enabled, sample }) {
461
791
  setPeeking: (on) => {
462
792
  if (peekingRef.current === on) return;
463
793
  peekingRef.current = on;
794
+ if (on) aimAtPointer();
464
795
  bump();
465
796
  },
466
797
  magnifyBy: (deltaY) => {
@@ -470,14 +801,15 @@ function useLoupe({ options, hostRef, enabled, sample }) {
470
801
  };
471
802
  }
472
803
  return {
473
- // This render's `enabled`: `enabledRef` holds the last committed one.
474
- visible: lensShown(overRef.current, enabled, peekingRef.current),
804
+ visible,
475
805
  aim: model.aim,
476
806
  factor: model.factor,
807
+ lens,
477
808
  mode: model.mode,
478
809
  color: model.color,
479
810
  setMode: model.setMode,
480
811
  setFactor: model.setFactor,
812
+ resample: model.resample,
481
813
  pick: model.pick,
482
814
  input: inputRef.current
483
815
  };
@@ -489,31 +821,23 @@ function TrialLoupe({
489
821
  hostRef,
490
822
  children,
491
823
  render,
824
+ source,
492
825
  factor,
493
826
  minFactor,
494
827
  maxFactor,
495
828
  mode,
496
829
  diameter,
830
+ shape,
831
+ place,
832
+ hollow,
833
+ onLens,
497
834
  peekKey,
498
835
  onColorChange
499
836
  }) {
500
- const options = useMemo(
501
- () => resolveLoupe({
502
- render,
503
- factor,
504
- minFactor,
505
- maxFactor,
506
- mode,
507
- diameter,
508
- peekKey,
509
- onColorChange
510
- }),
511
- [render, factor, minFactor, maxFactor, mode, diameter, peekKey, onColorChange]
512
- );
513
837
  const stack = useContext(CanvasStackContext);
514
838
  const camera = useContext(CameraContext);
515
839
  const loupeSwitch = useContext(LoupeSwitchContext);
516
- const surface = options.render ? void 0 : stack?.surface;
840
+ const surface = render || source || hollow ? void 0 : stack?.surface;
517
841
  const ownHost = useRef(null);
518
842
  const cameraHost = useMemo(
519
843
  () => camera ? {
@@ -525,43 +849,125 @@ function TrialLoupe({
525
849
  );
526
850
  const host = surface?.element ?? hostRef ?? cameraHost ?? ownHost;
527
851
  const wraps = host === ownHost;
852
+ const hostLatest = useLatest(host);
853
+ const tiles = useSurfaceOptional();
854
+ const onLensLatest = useLatest(onLens);
855
+ const was = useRef(null);
856
+ const damage = hollow ? tiles : null;
857
+ const reportLens = useCallback(
858
+ (lens2) => {
859
+ onLensLatest.current?.(lens2);
860
+ if (!damage) return;
861
+ const at = hostLatest.current.current?.getBoundingClientRect();
862
+ const now = lens2 && at ? {
863
+ left: at.left + lens2.center.x - lens2.width / 2,
864
+ top: at.top + lens2.center.y - lens2.height / 2,
865
+ width: lens2.width,
866
+ height: lens2.height
867
+ } : null;
868
+ if (was.current) damage.invalidateBox(was.current);
869
+ if (now) damage.invalidateBox(now);
870
+ was.current = now;
871
+ },
872
+ [damage, onLensLatest, hostLatest]
873
+ );
874
+ const options = useMemo(
875
+ () => resolveLoupe({
876
+ render,
877
+ source,
878
+ factor,
879
+ minFactor,
880
+ maxFactor,
881
+ mode,
882
+ diameter,
883
+ shape,
884
+ place,
885
+ hollow,
886
+ onLens: onLens || damage ? reportLens : void 0,
887
+ peekKey,
888
+ onColorChange
889
+ }),
890
+ [
891
+ render,
892
+ source,
893
+ factor,
894
+ minFactor,
895
+ maxFactor,
896
+ mode,
897
+ diameter,
898
+ shape,
899
+ place,
900
+ hollow,
901
+ onLens,
902
+ damage,
903
+ reportLens,
904
+ peekKey,
905
+ onColorChange
906
+ ]
907
+ );
528
908
  const mount = enabled === void 0 ? loupeSwitch?.mount : void 0;
529
909
  useEffect(() => mount?.(), [mount]);
530
910
  const sample = useMemo(() => {
911
+ if (options.source) {
912
+ const input = options.source;
913
+ return (p) => {
914
+ const src = resolveLoupeSource(input);
915
+ return src ? sampleSource(src, p, sourceBoxIn(src, host.current)) : null;
916
+ };
917
+ }
531
918
  if (!surface) return void 0;
532
919
  return (p) => {
533
920
  const canvases = surface.canvases.current;
534
921
  return canvases ? sampleStack(surface.layers, canvases, p, surface.size.dpr) : null;
535
922
  };
536
- }, [surface]);
923
+ }, [surface, options.source, host]);
924
+ const resolved = resolveLoupeSource(options.source);
925
+ const subscribeResample = useMemo(
926
+ () => resolved ? (fn) => resolved.subscribeFrame(fn) : void 0,
927
+ [resolved]
928
+ );
537
929
  const loupe = useLoupe({
538
930
  options,
539
931
  hostRef: host,
540
932
  enabled: enabled ?? loupeSwitch?.on ?? false,
541
- sample
933
+ sample,
934
+ subscribeResample
542
935
  });
936
+ const reading = loupe.visible ? resolved : null;
937
+ useEffect(() => reading?.retain(), [reading]);
543
938
  const measured = useHostSize(host);
544
939
  const size = surface?.size ?? measured;
545
940
  const gestures = /* @__PURE__ */ jsx(CameraScope, { children: /* @__PURE__ */ jsx(LoupeGestures, { hostRef: host, input: loupe.input, peekKey: options.peekKey ?? null }) });
546
- const lens = loupe.visible ? /* @__PURE__ */ jsx(LoupeBubble, { aim: loupe.aim, diameter: options.diameter, children: options.render ? /* @__PURE__ */ jsx(
941
+ const { center, shows, width, height, factor: shownFactor, shape: lensShape } = loupe.lens;
942
+ const box = { width, height };
943
+ const lens = loupe.visible ? /* @__PURE__ */ jsx(LoupeBubble, { aim: center, diameter: box, shape: lensShape, hollow: options.hollow, children: options.hollow ? null : options.render ? /* @__PURE__ */ jsx(
547
944
  DomLoupe,
548
945
  {
549
- aim: loupe.aim,
550
- factor: loupe.factor,
946
+ aim: shows,
947
+ factor: shownFactor,
551
948
  mode: loupe.mode,
552
- diameter: options.diameter,
949
+ diameter: box,
553
950
  size,
554
951
  view: view ?? stack?.view ?? (camera ? fromCameraView(camera.view.get(), camera.frame) : IDENTITY),
555
952
  frame: stack?.frame ?? camera?.frame,
556
953
  render: options.render
557
954
  }
955
+ ) : options.source ? /* @__PURE__ */ jsx(
956
+ SourceLoupe,
957
+ {
958
+ aim: shows,
959
+ factor: shownFactor,
960
+ diameter: box,
961
+ source: options.source,
962
+ hostRef: host
963
+ }
558
964
  ) : surface && stack ? /* @__PURE__ */ jsx(
559
965
  CanvasLoupe,
560
966
  {
561
- aim: loupe.aim,
562
- factor: loupe.factor,
967
+ aim: shows,
968
+ factor: shownFactor,
563
969
  mode: loupe.mode,
564
- diameter: options.diameter,
970
+ diameter: box,
565
971
  surface,
566
972
  view: stack.view,
567
973
  frame: stack.frame,
@@ -582,6 +988,6 @@ function TrialLoupe({
582
988
  ] });
583
989
  }
584
990
 
585
- export { CanvasLoupe, DomLoupe, LOUPE_DEFAULTS, LOUPE_MAGNIFY_ID, LOUPE_PEEK_ID, LoupeBubble, LoupeGestures, TrialLoupe, createLoupeActions, drawCanvasLens, lensCamera, lensSourceRect, resolveLoupe, sampleStack, useLoupe };
991
+ export { CanvasLoupe, DomLoupe, LOUPE_DEFAULTS, LOUPE_MAGNIFY_ID, LOUPE_PEEK_ID, LoupeBubble, LoupeGestures, SourceLoupe, TrialLoupe, createCanvasSource, createLoupeActions, drawCanvasLens, drawSourceLens, lensCamera, lensSourceRect, loupeExtent, placeBand, resolveLoupe, resolveLoupeSource, sampleSource, sampleStack, sourceBoxIn, useLoupe };
586
992
  //# sourceMappingURL=index.js.map
587
993
  //# sourceMappingURL=index.js.map