@merittdev/horus-lens 0.1.1 → 0.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/browser.cjs +1261 -194
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.cts +2 -2
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +2 -2
- package/dist/{chunk-UB7YYHI4.js → chunk-JXSK4JG4.js} +245 -27
- package/dist/chunk-JXSK4JG4.js.map +1 -0
- package/dist/{chunk-CRV65EEY.js → chunk-UT7E5LCG.js} +1016 -167
- package/dist/chunk-UT7E5LCG.js.map +1 -0
- package/dist/index.cjs +244 -26
- package/dist/index.cjs.map +1 -1
- package/dist/{index.d-DeLYS02n.d.ts → index.d-BtnbCqUx.d.ts} +26 -1
- package/dist/index.d-Cyvb07sp.d.cts +301 -0
- package/dist/index.d-Cyvb07sp.d.ts +301 -0
- package/dist/{index.d-sNoEEgRg.d.cts → index.d-QzZn4EMM.d.cts} +26 -1
- package/dist/index.d.cts +342 -37
- package/dist/index.d.ts +342 -37
- package/dist/index.js +1 -1
- package/dist/lens.min.js +163 -43
- package/dist/lens.min.js.map +1 -1
- package/dist/react.cjs +1261 -194
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/package.json +3 -3
- package/dist/chunk-CRV65EEY.js.map +0 -1
- package/dist/chunk-UB7YYHI4.js.map +0 -1
- package/dist/index.d-Bzgtl7-B.d.cts +0 -161
- package/dist/index.d-Bzgtl7-B.d.ts +0 -161
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
LENS_OVERLAY_HOST_ID,
|
|
3
3
|
LensClient
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-JXSK4JG4.js";
|
|
5
5
|
|
|
6
6
|
// ../browser/dist/index.js
|
|
7
7
|
var DEFAULT_PARAM_NAME = "lens";
|
|
@@ -124,177 +124,933 @@ function el(tag, props = {}, children = []) {
|
|
|
124
124
|
function clear(node) {
|
|
125
125
|
while (node.firstChild) node.removeChild(node.firstChild);
|
|
126
126
|
}
|
|
127
|
-
var
|
|
127
|
+
var THIN_MIN_DIST = 3;
|
|
128
|
+
var MAX_POINTS = 2e3;
|
|
129
|
+
var HIGHLIGHTER_WIDTH = 14;
|
|
130
|
+
var STEP_RADIUS = 12;
|
|
131
|
+
function distance(a, b) {
|
|
132
|
+
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
133
|
+
}
|
|
134
|
+
function shouldKeepPoint(last, candidate, minDist = THIN_MIN_DIST) {
|
|
135
|
+
return distance(last, candidate) >= minDist;
|
|
136
|
+
}
|
|
137
|
+
function perpendicularDistance(p, a, b) {
|
|
138
|
+
const dx = b.x - a.x;
|
|
139
|
+
const dy = b.y - a.y;
|
|
140
|
+
const len2 = dx * dx + dy * dy;
|
|
141
|
+
if (len2 === 0) return distance(p, a);
|
|
142
|
+
let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2;
|
|
143
|
+
t = Math.max(0, Math.min(1, t));
|
|
144
|
+
return distance(p, { x: a.x + t * dx, y: a.y + t * dy });
|
|
145
|
+
}
|
|
146
|
+
function simplifyPoints(points, tolerance = 1) {
|
|
147
|
+
if (points.length <= 2 || tolerance <= 0) return points.map((p) => ({ ...p }));
|
|
148
|
+
const first = points[0];
|
|
149
|
+
const last = points[points.length - 1];
|
|
150
|
+
let maxDist = 0;
|
|
151
|
+
let index = 0;
|
|
152
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
153
|
+
const d = perpendicularDistance(points[i], first, last);
|
|
154
|
+
if (d > maxDist) {
|
|
155
|
+
maxDist = d;
|
|
156
|
+
index = i;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (maxDist > tolerance) {
|
|
160
|
+
const left = simplifyPoints(points.slice(0, index + 1), tolerance);
|
|
161
|
+
const right = simplifyPoints(points.slice(index), tolerance);
|
|
162
|
+
return left.slice(0, -1).concat(right);
|
|
163
|
+
}
|
|
164
|
+
return [{ ...first }, { ...last }];
|
|
165
|
+
}
|
|
166
|
+
function arrowheadPolygon(x1, y1, x2, y2, size = 14) {
|
|
167
|
+
const angle = Math.atan2(y2 - y1, x2 - x1);
|
|
168
|
+
const spread = Math.PI / 7;
|
|
169
|
+
return [
|
|
170
|
+
{ x: x2, y: y2 },
|
|
171
|
+
{ x: x2 - size * Math.cos(angle - spread), y: y2 - size * Math.sin(angle - spread) },
|
|
172
|
+
{ x: x2 - size * Math.cos(angle + spread), y: y2 - size * Math.sin(angle + spread) }
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
function boundsOfAnnotation(a) {
|
|
176
|
+
switch (a.kind) {
|
|
177
|
+
case "rect":
|
|
178
|
+
case "ellipse":
|
|
179
|
+
case "blur": {
|
|
180
|
+
const x = Math.min(a.x, a.x + a.width);
|
|
181
|
+
const y = Math.min(a.y, a.y + a.height);
|
|
182
|
+
return { x, y, width: Math.abs(a.width), height: Math.abs(a.height) };
|
|
183
|
+
}
|
|
184
|
+
case "arrow":
|
|
185
|
+
case "line": {
|
|
186
|
+
const x = Math.min(a.x1, a.x2);
|
|
187
|
+
const y = Math.min(a.y1, a.y2);
|
|
188
|
+
return { x, y, width: Math.abs(a.x2 - a.x1), height: Math.abs(a.y2 - a.y1) };
|
|
189
|
+
}
|
|
190
|
+
case "pencil":
|
|
191
|
+
case "highlighter": {
|
|
192
|
+
if (a.points.length === 0) return null;
|
|
193
|
+
let minX = Infinity;
|
|
194
|
+
let minY = Infinity;
|
|
195
|
+
let maxX = -Infinity;
|
|
196
|
+
let maxY = -Infinity;
|
|
197
|
+
for (const p of a.points) {
|
|
198
|
+
minX = Math.min(minX, p.x);
|
|
199
|
+
minY = Math.min(minY, p.y);
|
|
200
|
+
maxX = Math.max(maxX, p.x);
|
|
201
|
+
maxY = Math.max(maxY, p.y);
|
|
202
|
+
}
|
|
203
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
204
|
+
}
|
|
205
|
+
case "text": {
|
|
206
|
+
const fontSize = a.fontSize ?? 16;
|
|
207
|
+
const width = Math.max(1, a.text.length * fontSize * 0.6);
|
|
208
|
+
return { x: a.x, y: a.y, width, height: fontSize * 1.2 };
|
|
209
|
+
}
|
|
210
|
+
case "step": {
|
|
211
|
+
return {
|
|
212
|
+
x: a.x - STEP_RADIUS,
|
|
213
|
+
y: a.y - STEP_RADIUS,
|
|
214
|
+
width: STEP_RADIUS * 2,
|
|
215
|
+
height: STEP_RADIUS * 2
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function unionBounds(list) {
|
|
221
|
+
if (list.length === 0) return null;
|
|
222
|
+
let minX = Infinity;
|
|
223
|
+
let minY = Infinity;
|
|
224
|
+
let maxX = -Infinity;
|
|
225
|
+
let maxY = -Infinity;
|
|
226
|
+
for (const b of list) {
|
|
227
|
+
minX = Math.min(minX, b.x);
|
|
228
|
+
minY = Math.min(minY, b.y);
|
|
229
|
+
maxX = Math.max(maxX, b.x + b.width);
|
|
230
|
+
maxY = Math.max(maxY, b.y + b.height);
|
|
231
|
+
}
|
|
232
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
233
|
+
}
|
|
234
|
+
function padAndClampBounds(b, padding, maxWidth, maxHeight) {
|
|
235
|
+
const x = Math.max(0, b.x - padding);
|
|
236
|
+
const y = Math.max(0, b.y - padding);
|
|
237
|
+
const right = Math.min(maxWidth, b.x + b.width + padding);
|
|
238
|
+
const bottom = Math.min(maxHeight, b.y + b.height + padding);
|
|
239
|
+
return { x, y, width: Math.max(0, right - x), height: Math.max(0, bottom - y) };
|
|
240
|
+
}
|
|
241
|
+
var svg = (inner) => `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${inner}</svg>`;
|
|
242
|
+
var iconEye = svg(
|
|
243
|
+
'<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>'
|
|
244
|
+
);
|
|
245
|
+
var iconCrosshair = svg(
|
|
246
|
+
'<circle cx="12" cy="12" r="8"/><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/>'
|
|
247
|
+
);
|
|
248
|
+
var iconClose = svg('<line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/>');
|
|
249
|
+
var iconMark = svg(
|
|
250
|
+
'<rect x="3" y="3" width="18" height="18" rx="2" stroke-dasharray="4 3"/>'
|
|
251
|
+
);
|
|
252
|
+
var iconCopy = svg(
|
|
253
|
+
'<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/>'
|
|
254
|
+
);
|
|
255
|
+
var iconCheck = svg('<polyline points="20 6 9 17 4 12"/>');
|
|
256
|
+
var iconSend = svg('<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>');
|
|
257
|
+
var iconRecord = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="12" cy="12" r="6" fill="#ef4444"/></svg>`;
|
|
258
|
+
var iconStop = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>`;
|
|
259
|
+
var iconRectTool = svg('<rect x="4" y="6" width="16" height="12" rx="1"/>');
|
|
260
|
+
var iconEllipseTool = svg('<ellipse cx="12" cy="12" rx="8" ry="6"/>');
|
|
261
|
+
var iconArrowTool = svg('<line x1="5" y1="19" x2="18" y2="6"/><polyline points="18 12 18 6 12 6"/>');
|
|
262
|
+
var iconLineTool = svg('<line x1="5" y1="19" x2="19" y2="5"/>');
|
|
263
|
+
var iconPencil = svg('<path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/>');
|
|
264
|
+
var iconHighlighter = svg(
|
|
265
|
+
'<path d="m9 11-6 6v3h9l3-3"/><path d="m22 12-4.6 4.6a1 1 0 0 1-1.4 0l-5.6-5.6a1 1 0 0 1 0-1.4L14 4"/>'
|
|
266
|
+
);
|
|
267
|
+
var iconText = svg(
|
|
268
|
+
'<polyline points="4 7 4 4 20 4 20 7"/><line x1="12" y1="4" x2="12" y2="20"/><line x1="9" y1="20" x2="15" y2="20"/>'
|
|
269
|
+
);
|
|
270
|
+
var iconStepTool = svg('<circle cx="12" cy="12" r="9"/><path d="M11.2 9 13 8v8"/>');
|
|
271
|
+
var iconBlur = svg('<path d="M12 3s6 6.4 6 10a6 6 0 0 1-12 0c0-3.6 6-10 6-10Z"/>');
|
|
272
|
+
var iconUndo = svg('<path d="M9 14 4 9l5-5"/><path d="M4 9h11a5 5 0 0 1 0 10h-1"/>');
|
|
273
|
+
var iconTrash = svg(
|
|
274
|
+
'<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>'
|
|
275
|
+
);
|
|
276
|
+
var iconAnnotate = iconPencil;
|
|
277
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
278
|
+
var FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
|
|
279
|
+
var MIN_DRAG = 6;
|
|
280
|
+
var TOOLS = [
|
|
281
|
+
{ tool: "rect", icon: iconRectTool, label: "Rectangle" },
|
|
282
|
+
{ tool: "ellipse", icon: iconEllipseTool, label: "Ellipse" },
|
|
283
|
+
{ tool: "arrow", icon: iconArrowTool, label: "Arrow" },
|
|
284
|
+
{ tool: "line", icon: iconLineTool, label: "Line" },
|
|
285
|
+
{ tool: "pencil", icon: iconPencil, label: "Pencil" },
|
|
286
|
+
{ tool: "highlighter", icon: iconHighlighter, label: "Highlighter" },
|
|
287
|
+
{ tool: "text", icon: iconText, label: "Text" },
|
|
288
|
+
{ tool: "step", icon: iconStepTool, label: "Step number" },
|
|
289
|
+
{ tool: "blur", icon: iconBlur, label: "Blur / redact" }
|
|
290
|
+
];
|
|
291
|
+
var COLORS = ["#ef4444", "#f59e0b", "#22c55e", "#3b82f6", "#8b5cf6", "#111827"];
|
|
292
|
+
var WIDTHS = [
|
|
293
|
+
{ label: "S", value: 2 },
|
|
294
|
+
{ label: "M", value: 3.5 },
|
|
295
|
+
{ label: "L", value: 6 }
|
|
296
|
+
];
|
|
297
|
+
var AnnotationEditor = class {
|
|
128
298
|
constructor(host) {
|
|
129
299
|
this.host = host;
|
|
130
300
|
}
|
|
131
301
|
host;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
dragging = false;
|
|
302
|
+
svg = null;
|
|
303
|
+
bg = null;
|
|
304
|
+
toolbar = null;
|
|
305
|
+
textInputEl = null;
|
|
306
|
+
textAnchor = null;
|
|
307
|
+
textColor = COLORS[0];
|
|
139
308
|
annotations = [];
|
|
140
|
-
|
|
309
|
+
markNodes = [];
|
|
310
|
+
drag = null;
|
|
311
|
+
active = false;
|
|
141
312
|
sessionBase = 0;
|
|
313
|
+
stepCounter = 0;
|
|
314
|
+
idSeq = 0;
|
|
142
315
|
onExit = null;
|
|
316
|
+
tool = "rect";
|
|
317
|
+
color = COLORS[0];
|
|
318
|
+
width = 3.5;
|
|
319
|
+
docW = 0;
|
|
320
|
+
docH = 0;
|
|
321
|
+
resizeObserver = null;
|
|
322
|
+
toolBtns = /* @__PURE__ */ new Map();
|
|
323
|
+
colorBtns = /* @__PURE__ */ new Map();
|
|
324
|
+
widthBtns = /* @__PURE__ */ new Map();
|
|
325
|
+
undoBtn = null;
|
|
326
|
+
clearBtn = null;
|
|
143
327
|
get isActive() {
|
|
144
328
|
return this.active;
|
|
145
329
|
}
|
|
146
330
|
get count() {
|
|
147
331
|
return this.annotations.length;
|
|
148
332
|
}
|
|
333
|
+
/** Deep-copied document-coord annotations per the schema contract. */
|
|
149
334
|
list() {
|
|
150
|
-
return this.annotations.map((a) =>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
335
|
+
return this.annotations.map((a) => {
|
|
336
|
+
const copy2 = { ...a };
|
|
337
|
+
if ("points" in copy2 && Array.isArray(copy2.points)) {
|
|
338
|
+
copy2.points = copy2.points.map((p) => ({ ...p }));
|
|
339
|
+
}
|
|
340
|
+
if (copy2.kind === "rect" && copy2.viewport) copy2.viewport = { ...copy2.viewport };
|
|
341
|
+
return copy2;
|
|
342
|
+
});
|
|
158
343
|
}
|
|
344
|
+
/** Remove every mark. Tears the layer down when not actively drawing. */
|
|
159
345
|
clear() {
|
|
346
|
+
for (const node of this.markNodes) node.remove();
|
|
347
|
+
this.markNodes = [];
|
|
160
348
|
this.annotations = [];
|
|
161
|
-
|
|
162
|
-
this.
|
|
349
|
+
this.stepCounter = 0;
|
|
350
|
+
if (!this.active) this.teardownLayer();
|
|
351
|
+
this.syncActionButtons();
|
|
163
352
|
}
|
|
164
353
|
start(onExit) {
|
|
165
354
|
if (this.active) return;
|
|
355
|
+
this.ensureLayer();
|
|
356
|
+
const svg2 = this.svg;
|
|
166
357
|
this.active = true;
|
|
167
358
|
this.onExit = onExit;
|
|
168
359
|
this.sessionBase = this.annotations.length;
|
|
169
|
-
this.
|
|
170
|
-
this.
|
|
171
|
-
this.
|
|
172
|
-
|
|
360
|
+
this.stepCounter = 0;
|
|
361
|
+
this.sizeLayer();
|
|
362
|
+
this.updateTransform();
|
|
363
|
+
svg2.style.pointerEvents = "auto";
|
|
364
|
+
svg2.style.touchAction = "none";
|
|
365
|
+
svg2.style.cursor = "crosshair";
|
|
366
|
+
svg2.addEventListener("pointerdown", this.onPointerDown);
|
|
367
|
+
this.buildToolbar();
|
|
368
|
+
window.addEventListener("keydown", this.onKey, true);
|
|
369
|
+
}
|
|
370
|
+
/** Done — keep this session's marks, exit draw mode. */
|
|
371
|
+
exit() {
|
|
372
|
+
if (!this.active) return;
|
|
373
|
+
this.active = false;
|
|
374
|
+
this.commitTextInput();
|
|
375
|
+
if (this.drag) {
|
|
376
|
+
this.drag.node.remove();
|
|
377
|
+
this.detachDragListeners();
|
|
378
|
+
this.drag = null;
|
|
379
|
+
}
|
|
380
|
+
const svg2 = this.svg;
|
|
381
|
+
if (svg2) {
|
|
382
|
+
svg2.removeEventListener("pointerdown", this.onPointerDown);
|
|
383
|
+
svg2.style.pointerEvents = "none";
|
|
384
|
+
svg2.style.touchAction = "";
|
|
385
|
+
svg2.style.cursor = "";
|
|
386
|
+
}
|
|
387
|
+
window.removeEventListener("keydown", this.onKey, true);
|
|
388
|
+
this.removeToolbar();
|
|
389
|
+
const cb = this.onExit;
|
|
390
|
+
this.onExit = null;
|
|
391
|
+
cb?.();
|
|
392
|
+
}
|
|
393
|
+
/** Cancel — discard marks drawn during this session, then exit. */
|
|
394
|
+
cancel() {
|
|
395
|
+
this.cancelTextInput();
|
|
396
|
+
const removed = this.markNodes.splice(this.sessionBase);
|
|
397
|
+
for (const node of removed) node.remove();
|
|
398
|
+
this.annotations.splice(this.sessionBase);
|
|
399
|
+
this.exit();
|
|
400
|
+
}
|
|
401
|
+
undo() {
|
|
402
|
+
const node = this.markNodes.pop();
|
|
403
|
+
if (!node) return;
|
|
404
|
+
node.remove();
|
|
405
|
+
this.annotations.pop();
|
|
406
|
+
this.syncActionButtons();
|
|
407
|
+
}
|
|
408
|
+
// --------------------------------------------------------------------------
|
|
409
|
+
// Composite / crop
|
|
410
|
+
// --------------------------------------------------------------------------
|
|
411
|
+
/**
|
|
412
|
+
* Complete standalone SVG of every NON-blur mark, plus blur regions returned
|
|
413
|
+
* separately. `null` when there are no marks.
|
|
414
|
+
*/
|
|
415
|
+
getComposite() {
|
|
416
|
+
if (this.annotations.length === 0 || !this.svg) return null;
|
|
417
|
+
this.sizeLayer();
|
|
418
|
+
const docWidth = this.docW;
|
|
419
|
+
const docHeight = this.docH;
|
|
420
|
+
const clone = this.svg.cloneNode(true);
|
|
421
|
+
clone.removeAttribute("style");
|
|
422
|
+
clone.removeAttribute("class");
|
|
423
|
+
clone.querySelectorAll("[data-lens-chrome],[data-lens-draft],[data-lens-blur]").forEach((n) => n.remove());
|
|
424
|
+
clone.querySelector("defs")?.remove();
|
|
425
|
+
clone.setAttribute("xmlns", SVG_NS);
|
|
426
|
+
clone.setAttribute("viewBox", `0 0 ${docWidth} ${docHeight}`);
|
|
427
|
+
clone.setAttribute("width", String(docWidth));
|
|
428
|
+
clone.setAttribute("height", String(docHeight));
|
|
429
|
+
const svgMarkup = new XMLSerializer().serializeToString(clone);
|
|
430
|
+
const blurRegions = this.annotations.filter((a) => a.kind === "blur").map((a) => ({ x: a.x, y: a.y, width: a.width, height: a.height }));
|
|
431
|
+
return { svgMarkup, docWidth, docHeight, blurRegions };
|
|
432
|
+
}
|
|
433
|
+
/** Union bbox of all marks + 24px padding, clamped to the document. */
|
|
434
|
+
getCropBounds() {
|
|
435
|
+
if (this.annotations.length === 0) return void 0;
|
|
436
|
+
const boxes = [];
|
|
437
|
+
for (let i = 0; i < this.annotations.length; i++) {
|
|
438
|
+
const a = this.annotations[i];
|
|
439
|
+
const b = a.kind === "text" ? this.textBounds(i) : boundsOfAnnotation(a);
|
|
440
|
+
if (b) boxes.push(b);
|
|
441
|
+
}
|
|
442
|
+
const u = unionBounds(boxes);
|
|
443
|
+
if (!u) return void 0;
|
|
444
|
+
this.sizeLayer();
|
|
445
|
+
return padAndClampBounds(u, 24, this.docW, this.docH);
|
|
446
|
+
}
|
|
447
|
+
/** Prefer a live getBBox() for text; fall back to the geometric estimate. */
|
|
448
|
+
textBounds(index) {
|
|
449
|
+
const node = this.markNodes[index];
|
|
450
|
+
const annotation = this.annotations[index];
|
|
451
|
+
if (node && typeof node.getBBox === "function") {
|
|
452
|
+
try {
|
|
453
|
+
const box = node.getBBox();
|
|
454
|
+
if (box.width > 0 && box.height > 0) {
|
|
455
|
+
return { x: box.x, y: box.y, width: box.width, height: box.height };
|
|
456
|
+
}
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return boundsOfAnnotation(annotation);
|
|
461
|
+
}
|
|
462
|
+
// --------------------------------------------------------------------------
|
|
463
|
+
// Layer lifecycle
|
|
464
|
+
// --------------------------------------------------------------------------
|
|
465
|
+
ensureLayer() {
|
|
466
|
+
if (this.svg) return;
|
|
467
|
+
const svg2 = document.createElementNS(SVG_NS, "svg");
|
|
468
|
+
svg2.setAttribute("class", "lens-annotate-svg");
|
|
469
|
+
svg2.setAttribute("xmlns", SVG_NS);
|
|
470
|
+
const defs = document.createElementNS(SVG_NS, "defs");
|
|
471
|
+
defs.setAttribute("data-lens-chrome", "1");
|
|
472
|
+
const pattern = document.createElementNS(SVG_NS, "pattern");
|
|
473
|
+
pattern.setAttribute("id", "lens-blur-hatch");
|
|
474
|
+
pattern.setAttribute("width", "8");
|
|
475
|
+
pattern.setAttribute("height", "8");
|
|
476
|
+
pattern.setAttribute("patternUnits", "userSpaceOnUse");
|
|
477
|
+
pattern.setAttribute("patternTransform", "rotate(45)");
|
|
478
|
+
const patternBase = document.createElementNS(SVG_NS, "rect");
|
|
479
|
+
patternBase.setAttribute("width", "8");
|
|
480
|
+
patternBase.setAttribute("height", "8");
|
|
481
|
+
patternBase.setAttribute("fill", "rgba(17,24,39,0.12)");
|
|
482
|
+
const patternLine = document.createElementNS(SVG_NS, "line");
|
|
483
|
+
patternLine.setAttribute("x1", "0");
|
|
484
|
+
patternLine.setAttribute("y1", "0");
|
|
485
|
+
patternLine.setAttribute("x2", "0");
|
|
486
|
+
patternLine.setAttribute("y2", "8");
|
|
487
|
+
patternLine.setAttribute("stroke", "rgba(17,24,39,0.55)");
|
|
488
|
+
patternLine.setAttribute("stroke-width", "4");
|
|
489
|
+
pattern.append(patternBase, patternLine);
|
|
490
|
+
defs.append(pattern);
|
|
491
|
+
svg2.append(defs);
|
|
492
|
+
const bg = document.createElementNS(SVG_NS, "rect");
|
|
493
|
+
bg.setAttribute("data-lens-chrome", "1");
|
|
494
|
+
bg.setAttribute("x", "0");
|
|
495
|
+
bg.setAttribute("y", "0");
|
|
496
|
+
bg.setAttribute("fill", "transparent");
|
|
497
|
+
bg.style.pointerEvents = "all";
|
|
498
|
+
svg2.append(bg);
|
|
499
|
+
this.svg = svg2;
|
|
500
|
+
this.bg = bg;
|
|
501
|
+
this.host.append(svg2);
|
|
502
|
+
window.addEventListener("scroll", this.onScroll, { passive: true, capture: true });
|
|
503
|
+
window.addEventListener("resize", this.onResize);
|
|
504
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
505
|
+
this.resizeObserver = new ResizeObserver(this.onResize);
|
|
506
|
+
this.resizeObserver.observe(document.documentElement);
|
|
507
|
+
}
|
|
508
|
+
this.sizeLayer();
|
|
509
|
+
this.updateTransform();
|
|
510
|
+
}
|
|
511
|
+
teardownLayer() {
|
|
512
|
+
window.removeEventListener("scroll", this.onScroll, true);
|
|
513
|
+
window.removeEventListener("resize", this.onResize);
|
|
514
|
+
this.resizeObserver?.disconnect();
|
|
515
|
+
this.resizeObserver = null;
|
|
516
|
+
this.svg?.remove();
|
|
517
|
+
this.svg = null;
|
|
518
|
+
this.bg = null;
|
|
519
|
+
}
|
|
520
|
+
docSize() {
|
|
521
|
+
const d = document.documentElement;
|
|
522
|
+
const b = document.body;
|
|
523
|
+
const width = Math.max(d.scrollWidth, b?.scrollWidth ?? 0, d.clientWidth);
|
|
524
|
+
const height = Math.max(d.scrollHeight, b?.scrollHeight ?? 0, d.clientHeight);
|
|
525
|
+
return { width, height };
|
|
526
|
+
}
|
|
527
|
+
sizeLayer() {
|
|
528
|
+
if (!this.svg) return;
|
|
529
|
+
const { width, height } = this.docSize();
|
|
530
|
+
this.docW = width;
|
|
531
|
+
this.docH = height;
|
|
532
|
+
this.svg.setAttribute("width", String(width));
|
|
533
|
+
this.svg.setAttribute("height", String(height));
|
|
534
|
+
this.svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
535
|
+
this.svg.style.width = `${width}px`;
|
|
536
|
+
this.svg.style.height = `${height}px`;
|
|
537
|
+
if (this.bg) {
|
|
538
|
+
this.bg.setAttribute("width", String(width));
|
|
539
|
+
this.bg.setAttribute("height", String(height));
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
updateTransform() {
|
|
543
|
+
if (!this.svg) return;
|
|
544
|
+
const scrollX = window.scrollX || window.pageXOffset || 0;
|
|
545
|
+
const scrollY = window.scrollY || window.pageYOffset || 0;
|
|
546
|
+
this.svg.style.left = `${-scrollX}px`;
|
|
547
|
+
this.svg.style.top = `${-scrollY}px`;
|
|
548
|
+
}
|
|
549
|
+
onScroll = () => this.updateTransform();
|
|
550
|
+
onResize = () => {
|
|
551
|
+
this.sizeLayer();
|
|
552
|
+
this.updateTransform();
|
|
553
|
+
};
|
|
554
|
+
// --------------------------------------------------------------------------
|
|
555
|
+
// Toolbar
|
|
556
|
+
// --------------------------------------------------------------------------
|
|
557
|
+
buildToolbar() {
|
|
558
|
+
this.toolBtns.clear();
|
|
559
|
+
this.colorBtns.clear();
|
|
560
|
+
this.widthBtns.clear();
|
|
561
|
+
const toolGroup = el("div", { class: "lens-tool-group" });
|
|
562
|
+
for (const { tool, icon, label } of TOOLS) {
|
|
563
|
+
const btn = el("button", {
|
|
564
|
+
class: "lens-tool-btn",
|
|
565
|
+
ariaLabel: label,
|
|
566
|
+
title: label,
|
|
567
|
+
html: icon,
|
|
568
|
+
attrs: { type: "button", "aria-pressed": String(tool === this.tool) },
|
|
569
|
+
on: { click: () => this.selectTool(tool) }
|
|
570
|
+
});
|
|
571
|
+
this.toolBtns.set(tool, btn);
|
|
572
|
+
toolGroup.append(btn);
|
|
573
|
+
}
|
|
574
|
+
const colorGroup = el("div", { class: "lens-tool-group lens-swatches" });
|
|
575
|
+
for (const c of COLORS) {
|
|
576
|
+
const btn = el("button", {
|
|
577
|
+
class: "lens-swatch",
|
|
578
|
+
ariaLabel: `Color ${c}`,
|
|
579
|
+
title: c,
|
|
580
|
+
attrs: { type: "button", "aria-pressed": String(c === this.color) },
|
|
581
|
+
style: { background: c },
|
|
582
|
+
on: { click: () => this.selectColor(c) }
|
|
583
|
+
});
|
|
584
|
+
this.colorBtns.set(c, btn);
|
|
585
|
+
colorGroup.append(btn);
|
|
586
|
+
}
|
|
587
|
+
const widthGroup = el("div", { class: "lens-tool-group" });
|
|
588
|
+
for (const { label, value } of WIDTHS) {
|
|
589
|
+
const btn = el("button", {
|
|
590
|
+
class: "lens-width-btn",
|
|
591
|
+
text: label,
|
|
592
|
+
ariaLabel: `Stroke width ${label}`,
|
|
593
|
+
title: `Stroke ${label} (${value}px)`,
|
|
594
|
+
attrs: { type: "button", "aria-pressed": String(value === this.width) },
|
|
595
|
+
on: { click: () => this.selectWidth(value) }
|
|
596
|
+
});
|
|
597
|
+
this.widthBtns.set(value, btn);
|
|
598
|
+
widthGroup.append(btn);
|
|
599
|
+
}
|
|
600
|
+
this.undoBtn = el("button", {
|
|
601
|
+
class: "lens-tool-btn",
|
|
602
|
+
ariaLabel: "Undo",
|
|
603
|
+
title: "Undo",
|
|
604
|
+
html: iconUndo,
|
|
605
|
+
attrs: { type: "button" },
|
|
606
|
+
on: { click: () => this.undo() }
|
|
607
|
+
});
|
|
608
|
+
this.clearBtn = el("button", {
|
|
609
|
+
class: "lens-tool-btn",
|
|
610
|
+
ariaLabel: "Clear all",
|
|
611
|
+
title: "Clear all",
|
|
612
|
+
html: iconTrash,
|
|
613
|
+
attrs: { type: "button" },
|
|
614
|
+
on: { click: () => this.clear() }
|
|
615
|
+
});
|
|
616
|
+
const editGroup = el("div", { class: "lens-tool-group" }, [this.undoBtn, this.clearBtn]);
|
|
617
|
+
const doneGroup = el("div", { class: "lens-tool-group" }, [
|
|
173
618
|
el("button", {
|
|
174
619
|
class: "lens-pick-chip__btn",
|
|
175
620
|
text: "Cancel",
|
|
621
|
+
attrs: { type: "button" },
|
|
176
622
|
on: { click: () => this.cancel() }
|
|
177
623
|
}),
|
|
178
624
|
el("button", {
|
|
179
625
|
class: "lens-pick-chip__btn lens-pick-chip__btn--primary",
|
|
180
626
|
text: "Done",
|
|
627
|
+
attrs: { type: "button" },
|
|
181
628
|
on: { click: () => this.exit() }
|
|
182
629
|
})
|
|
183
630
|
]);
|
|
184
|
-
this.
|
|
185
|
-
|
|
631
|
+
this.toolbar = el(
|
|
632
|
+
"div",
|
|
633
|
+
{ class: "lens-annotate-toolbar", attrs: { role: "toolbar", "aria-label": "Annotation tools" } },
|
|
634
|
+
[
|
|
635
|
+
toolGroup,
|
|
636
|
+
el("span", { class: "lens-tool-sep" }),
|
|
637
|
+
colorGroup,
|
|
638
|
+
el("span", { class: "lens-tool-sep" }),
|
|
639
|
+
widthGroup,
|
|
640
|
+
el("span", { class: "lens-tool-sep" }),
|
|
641
|
+
editGroup,
|
|
642
|
+
el("span", { class: "lens-tool-sep" }),
|
|
643
|
+
doneGroup
|
|
644
|
+
]
|
|
645
|
+
);
|
|
646
|
+
this.host.append(this.toolbar);
|
|
647
|
+
this.syncActionButtons();
|
|
648
|
+
}
|
|
649
|
+
removeToolbar() {
|
|
650
|
+
this.toolbar?.remove();
|
|
651
|
+
this.toolbar = null;
|
|
652
|
+
this.toolBtns.clear();
|
|
653
|
+
this.colorBtns.clear();
|
|
654
|
+
this.widthBtns.clear();
|
|
655
|
+
this.undoBtn = null;
|
|
656
|
+
this.clearBtn = null;
|
|
657
|
+
}
|
|
658
|
+
selectTool(tool) {
|
|
659
|
+
this.commitTextInput();
|
|
660
|
+
this.tool = tool;
|
|
661
|
+
for (const [t, btn] of this.toolBtns) {
|
|
662
|
+
const on = t === tool;
|
|
663
|
+
btn.classList.toggle("is-active", on);
|
|
664
|
+
btn.setAttribute("aria-pressed", String(on));
|
|
665
|
+
}
|
|
666
|
+
if (this.svg) this.svg.style.cursor = tool === "text" ? "text" : "crosshair";
|
|
667
|
+
}
|
|
668
|
+
selectColor(color) {
|
|
669
|
+
this.color = color;
|
|
670
|
+
for (const [c, btn] of this.colorBtns) {
|
|
671
|
+
const on = c === color;
|
|
672
|
+
btn.classList.toggle("is-active", on);
|
|
673
|
+
btn.setAttribute("aria-pressed", String(on));
|
|
674
|
+
}
|
|
186
675
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
676
|
+
selectWidth(value) {
|
|
677
|
+
this.width = value;
|
|
678
|
+
for (const [v, btn] of this.widthBtns) {
|
|
679
|
+
const on = v === value;
|
|
680
|
+
btn.classList.toggle("is-active", on);
|
|
681
|
+
btn.setAttribute("aria-pressed", String(on));
|
|
682
|
+
}
|
|
193
683
|
}
|
|
194
|
-
|
|
684
|
+
syncActionButtons() {
|
|
685
|
+
const empty = this.markNodes.length === 0;
|
|
686
|
+
if (this.undoBtn) this.undoBtn.disabled = empty;
|
|
687
|
+
if (this.clearBtn) this.clearBtn.disabled = empty;
|
|
688
|
+
}
|
|
689
|
+
// --------------------------------------------------------------------------
|
|
690
|
+
// Pointer / drawing
|
|
691
|
+
// --------------------------------------------------------------------------
|
|
692
|
+
toDoc(e) {
|
|
693
|
+
return {
|
|
694
|
+
x: e.clientX + (window.scrollX || window.pageXOffset || 0),
|
|
695
|
+
y: e.clientY + (window.scrollY || window.pageYOffset || 0)
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
onPointerDown = (e) => {
|
|
195
699
|
if (!this.active) return;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
this.draft = null;
|
|
700
|
+
if (e.button !== void 0 && e.button !== 0) return;
|
|
701
|
+
e.preventDefault();
|
|
702
|
+
const p = this.toDoc(e);
|
|
703
|
+
const tool = this.tool;
|
|
704
|
+
if (tool === "text") {
|
|
705
|
+
this.openTextInput(e.clientX, e.clientY, p);
|
|
706
|
+
return;
|
|
204
707
|
}
|
|
205
|
-
if (
|
|
206
|
-
this.
|
|
207
|
-
|
|
208
|
-
this.layer = null;
|
|
708
|
+
if (tool === "step") {
|
|
709
|
+
this.placeStep(p);
|
|
710
|
+
return;
|
|
209
711
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
712
|
+
const color = this.color;
|
|
713
|
+
const width = this.width;
|
|
714
|
+
let drag;
|
|
715
|
+
if (tool === "pencil" || tool === "highlighter") {
|
|
716
|
+
const poly = this.svgEl("polyline", {
|
|
717
|
+
"data-lens-draft": "1",
|
|
718
|
+
fill: "none",
|
|
719
|
+
stroke: color,
|
|
720
|
+
"stroke-width": tool === "highlighter" ? HIGHLIGHTER_WIDTH : width,
|
|
721
|
+
"stroke-linecap": "round",
|
|
722
|
+
"stroke-linejoin": "round",
|
|
723
|
+
...tool === "highlighter" ? { "stroke-opacity": "0.35" } : {},
|
|
724
|
+
points: `${p.x},${p.y}`
|
|
725
|
+
});
|
|
726
|
+
this.svg.append(poly);
|
|
727
|
+
drag = { tool, color, width, start: p, node: poly, points: [p], last: p };
|
|
728
|
+
} else if (tool === "arrow") {
|
|
729
|
+
const g = this.svgEl("g", { "data-lens-draft": "1" });
|
|
730
|
+
const line = this.svgEl("line", {
|
|
731
|
+
stroke: color,
|
|
732
|
+
"stroke-width": width,
|
|
733
|
+
"stroke-linecap": "round"
|
|
734
|
+
});
|
|
735
|
+
const head = this.svgEl("polygon", { fill: color });
|
|
736
|
+
g.append(line, head);
|
|
737
|
+
this.svg.append(g);
|
|
738
|
+
drag = { tool, color, width, start: p, node: g, line, head };
|
|
739
|
+
this.updateArrow(drag, p);
|
|
740
|
+
} else {
|
|
741
|
+
const node = this.createShapeNode(tool, color, width);
|
|
742
|
+
this.svg.append(node);
|
|
743
|
+
drag = { tool, color, width, start: p, node };
|
|
744
|
+
this.updateShape(drag, p);
|
|
213
745
|
}
|
|
214
|
-
|
|
215
|
-
this.
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
onDown = (e) => {
|
|
219
|
-
e.preventDefault();
|
|
220
|
-
this.dragging = true;
|
|
221
|
-
this.startX = e.clientX;
|
|
222
|
-
this.startY = e.clientY;
|
|
223
|
-
this.draft = el("div", { class: "lens-rect" });
|
|
224
|
-
this.host.append(this.draft);
|
|
225
|
-
this.position(this.draft, e.clientX, e.clientY);
|
|
226
|
-
window.addEventListener("pointermove", this.onMove, true);
|
|
227
|
-
window.addEventListener("pointerup", this.onUp, true);
|
|
746
|
+
this.drag = drag;
|
|
747
|
+
window.addEventListener("pointermove", this.onPointerMove, true);
|
|
748
|
+
window.addEventListener("pointerup", this.onPointerUp, true);
|
|
228
749
|
};
|
|
229
|
-
|
|
230
|
-
|
|
750
|
+
onPointerMove = (e) => {
|
|
751
|
+
const d = this.drag;
|
|
752
|
+
if (!d) return;
|
|
753
|
+
const p = this.toDoc(e);
|
|
754
|
+
if (d.tool === "pencil" || d.tool === "highlighter") {
|
|
755
|
+
const pts = d.points;
|
|
756
|
+
if (d.last && shouldKeepPoint(d.last, p) && pts.length < MAX_POINTS) {
|
|
757
|
+
pts.push(p);
|
|
758
|
+
d.last = p;
|
|
759
|
+
d.node.setAttribute("points", this.pointsAttr(pts));
|
|
760
|
+
}
|
|
761
|
+
} else if (d.tool === "arrow") {
|
|
762
|
+
this.updateArrow(d, p);
|
|
763
|
+
} else {
|
|
764
|
+
this.updateShape(d, p);
|
|
765
|
+
}
|
|
231
766
|
};
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
this.
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
767
|
+
onPointerUp = (e) => {
|
|
768
|
+
const d = this.drag;
|
|
769
|
+
this.detachDragListeners();
|
|
770
|
+
this.drag = null;
|
|
771
|
+
if (!d) return;
|
|
772
|
+
const p = this.toDoc(e);
|
|
773
|
+
let annotation = null;
|
|
774
|
+
if (d.tool === "pencil" || d.tool === "highlighter") {
|
|
775
|
+
let pts = d.points;
|
|
776
|
+
if (pts.length >= 2) pts = simplifyPoints(pts, 1);
|
|
777
|
+
if (pts.length < 2) {
|
|
778
|
+
d.node.remove();
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
d.node.setAttribute("points", this.pointsAttr(pts));
|
|
782
|
+
annotation = d.tool === "pencil" ? { kind: "pencil", points: pts, color: d.color, strokeWidth: d.width, id: this.nextId() } : {
|
|
783
|
+
kind: "highlighter",
|
|
784
|
+
points: pts,
|
|
785
|
+
color: d.color,
|
|
786
|
+
strokeWidth: HIGHLIGHTER_WIDTH,
|
|
787
|
+
id: this.nextId()
|
|
788
|
+
};
|
|
789
|
+
} else if (d.tool === "arrow" || d.tool === "line") {
|
|
790
|
+
if (distance(d.start, p) < MIN_DRAG) {
|
|
791
|
+
d.node.remove();
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
if (d.tool === "arrow") {
|
|
795
|
+
this.updateArrow(d, p);
|
|
796
|
+
annotation = {
|
|
797
|
+
kind: "arrow",
|
|
798
|
+
x1: d.start.x,
|
|
799
|
+
y1: d.start.y,
|
|
800
|
+
x2: p.x,
|
|
801
|
+
y2: p.y,
|
|
802
|
+
color: d.color,
|
|
803
|
+
strokeWidth: d.width,
|
|
804
|
+
id: this.nextId()
|
|
805
|
+
};
|
|
806
|
+
} else {
|
|
807
|
+
this.updateShape(d, p);
|
|
808
|
+
annotation = {
|
|
809
|
+
kind: "line",
|
|
810
|
+
x1: d.start.x,
|
|
811
|
+
y1: d.start.y,
|
|
812
|
+
x2: p.x,
|
|
813
|
+
y2: p.y,
|
|
814
|
+
color: d.color,
|
|
815
|
+
strokeWidth: d.width,
|
|
816
|
+
id: this.nextId()
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
} else {
|
|
820
|
+
const x = Math.min(d.start.x, p.x);
|
|
821
|
+
const y = Math.min(d.start.y, p.y);
|
|
822
|
+
const width = Math.abs(p.x - d.start.x);
|
|
823
|
+
const height = Math.abs(p.y - d.start.y);
|
|
824
|
+
if (width < MIN_DRAG || height < MIN_DRAG) {
|
|
825
|
+
d.node.remove();
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
this.updateShape(d, p);
|
|
829
|
+
if (d.tool === "rect") {
|
|
830
|
+
annotation = {
|
|
831
|
+
kind: "rect",
|
|
832
|
+
x,
|
|
833
|
+
y,
|
|
834
|
+
width,
|
|
835
|
+
height,
|
|
836
|
+
color: d.color,
|
|
837
|
+
strokeWidth: d.width,
|
|
838
|
+
id: this.nextId()
|
|
839
|
+
};
|
|
840
|
+
} else if (d.tool === "ellipse") {
|
|
841
|
+
annotation = {
|
|
842
|
+
kind: "ellipse",
|
|
843
|
+
x,
|
|
844
|
+
y,
|
|
845
|
+
width,
|
|
846
|
+
height,
|
|
847
|
+
color: d.color,
|
|
848
|
+
strokeWidth: d.width,
|
|
849
|
+
id: this.nextId()
|
|
850
|
+
};
|
|
851
|
+
} else {
|
|
852
|
+
annotation = { kind: "blur", x, y, width, height, id: this.nextId() };
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
d.node.removeAttribute("data-lens-draft");
|
|
856
|
+
this.commitMark(annotation, d.node);
|
|
857
|
+
};
|
|
858
|
+
detachDragListeners() {
|
|
859
|
+
window.removeEventListener("pointermove", this.onPointerMove, true);
|
|
860
|
+
window.removeEventListener("pointerup", this.onPointerUp, true);
|
|
861
|
+
}
|
|
862
|
+
commitMark(annotation, node) {
|
|
863
|
+
this.annotations.push(annotation);
|
|
864
|
+
this.markNodes.push(node);
|
|
865
|
+
this.syncActionButtons();
|
|
866
|
+
}
|
|
867
|
+
createShapeNode(tool, color, width) {
|
|
868
|
+
if (tool === "ellipse") {
|
|
869
|
+
return this.svgEl("ellipse", {
|
|
870
|
+
"data-lens-draft": "1",
|
|
871
|
+
fill: "none",
|
|
872
|
+
stroke: color,
|
|
873
|
+
"stroke-width": width
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
if (tool === "line") {
|
|
877
|
+
return this.svgEl("line", {
|
|
878
|
+
"data-lens-draft": "1",
|
|
879
|
+
stroke: color,
|
|
880
|
+
"stroke-width": width,
|
|
881
|
+
"stroke-linecap": "round"
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
if (tool === "blur") {
|
|
885
|
+
return this.svgEl("rect", {
|
|
886
|
+
"data-lens-draft": "1",
|
|
887
|
+
"data-lens-blur": "1",
|
|
888
|
+
fill: "url(#lens-blur-hatch)",
|
|
889
|
+
stroke: "#111827",
|
|
890
|
+
"stroke-width": 1,
|
|
891
|
+
"stroke-dasharray": "4 3"
|
|
892
|
+
});
|
|
243
893
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
height: `${height}px`
|
|
894
|
+
return this.svgEl("rect", {
|
|
895
|
+
"data-lens-draft": "1",
|
|
896
|
+
fill: "none",
|
|
897
|
+
stroke: color,
|
|
898
|
+
"stroke-width": width,
|
|
899
|
+
rx: 2
|
|
251
900
|
});
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
901
|
+
}
|
|
902
|
+
updateShape(d, cur) {
|
|
903
|
+
const x = Math.min(d.start.x, cur.x);
|
|
904
|
+
const y = Math.min(d.start.y, cur.y);
|
|
905
|
+
const w = Math.abs(cur.x - d.start.x);
|
|
906
|
+
const h = Math.abs(cur.y - d.start.y);
|
|
907
|
+
const node = d.node;
|
|
908
|
+
if (d.tool === "ellipse") {
|
|
909
|
+
node.setAttribute("cx", String(x + w / 2));
|
|
910
|
+
node.setAttribute("cy", String(y + h / 2));
|
|
911
|
+
node.setAttribute("rx", String(w / 2));
|
|
912
|
+
node.setAttribute("ry", String(h / 2));
|
|
913
|
+
} else if (d.tool === "line") {
|
|
914
|
+
node.setAttribute("x1", String(d.start.x));
|
|
915
|
+
node.setAttribute("y1", String(d.start.y));
|
|
916
|
+
node.setAttribute("x2", String(cur.x));
|
|
917
|
+
node.setAttribute("y2", String(cur.y));
|
|
918
|
+
} else {
|
|
919
|
+
node.setAttribute("x", String(x));
|
|
920
|
+
node.setAttribute("y", String(y));
|
|
921
|
+
node.setAttribute("width", String(w));
|
|
922
|
+
node.setAttribute("height", String(h));
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
updateArrow(d, cur) {
|
|
926
|
+
if (!d.line || !d.head) return;
|
|
927
|
+
d.line.setAttribute("x1", String(d.start.x));
|
|
928
|
+
d.line.setAttribute("y1", String(d.start.y));
|
|
929
|
+
d.line.setAttribute("x2", String(cur.x));
|
|
930
|
+
d.line.setAttribute("y2", String(cur.y));
|
|
931
|
+
const size = Math.max(12, d.width * 3);
|
|
932
|
+
const poly = arrowheadPolygon(d.start.x, d.start.y, cur.x, cur.y, size);
|
|
933
|
+
d.head.setAttribute("points", this.pointsAttr(poly));
|
|
934
|
+
}
|
|
935
|
+
// --------------------------------------------------------------------------
|
|
936
|
+
// Text tool
|
|
937
|
+
// --------------------------------------------------------------------------
|
|
938
|
+
openTextInput(clientX, clientY, docPoint) {
|
|
939
|
+
this.commitTextInput();
|
|
940
|
+
const input = el("input", {
|
|
941
|
+
class: "lens-text-input",
|
|
942
|
+
attrs: { type: "text", "aria-label": "Annotation text" },
|
|
943
|
+
style: { left: `${clientX}px`, top: `${clientY}px`, color: this.color }
|
|
261
944
|
});
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
945
|
+
input.addEventListener("blur", () => this.commitTextInput());
|
|
946
|
+
this.host.append(input);
|
|
947
|
+
this.textInputEl = input;
|
|
948
|
+
this.textAnchor = docPoint;
|
|
949
|
+
this.textColor = this.color;
|
|
950
|
+
input.focus();
|
|
951
|
+
}
|
|
952
|
+
commitTextInput() {
|
|
953
|
+
const input = this.textInputEl;
|
|
954
|
+
const anchor = this.textAnchor;
|
|
955
|
+
if (!input || !anchor) return;
|
|
956
|
+
const value = input.value.trim();
|
|
957
|
+
this.textInputEl = null;
|
|
958
|
+
this.textAnchor = null;
|
|
959
|
+
input.remove();
|
|
960
|
+
if (!value) return;
|
|
961
|
+
const node = this.svgEl("text", {
|
|
962
|
+
x: anchor.x,
|
|
963
|
+
y: anchor.y,
|
|
964
|
+
fill: this.textColor,
|
|
965
|
+
"font-size": 16,
|
|
966
|
+
"font-family": FONT,
|
|
967
|
+
"dominant-baseline": "hanging"
|
|
271
968
|
});
|
|
969
|
+
node.textContent = value;
|
|
970
|
+
this.svg.append(node);
|
|
971
|
+
this.commitMark(
|
|
972
|
+
{
|
|
973
|
+
kind: "text",
|
|
974
|
+
x: anchor.x,
|
|
975
|
+
y: anchor.y,
|
|
976
|
+
text: value,
|
|
977
|
+
fontSize: 16,
|
|
978
|
+
color: this.textColor,
|
|
979
|
+
id: this.nextId()
|
|
980
|
+
},
|
|
981
|
+
node
|
|
982
|
+
);
|
|
272
983
|
}
|
|
984
|
+
cancelTextInput() {
|
|
985
|
+
if (!this.textInputEl) return;
|
|
986
|
+
this.textInputEl.remove();
|
|
987
|
+
this.textInputEl = null;
|
|
988
|
+
this.textAnchor = null;
|
|
989
|
+
}
|
|
990
|
+
// --------------------------------------------------------------------------
|
|
991
|
+
// Step tool
|
|
992
|
+
// --------------------------------------------------------------------------
|
|
993
|
+
placeStep(p) {
|
|
994
|
+
const n = ++this.stepCounter;
|
|
995
|
+
const g = this.svgEl("g", {});
|
|
996
|
+
const circle = this.svgEl("circle", {
|
|
997
|
+
cx: p.x,
|
|
998
|
+
cy: p.y,
|
|
999
|
+
r: STEP_RADIUS,
|
|
1000
|
+
fill: this.color
|
|
1001
|
+
});
|
|
1002
|
+
const text = this.svgEl("text", {
|
|
1003
|
+
x: p.x,
|
|
1004
|
+
y: p.y,
|
|
1005
|
+
fill: "#ffffff",
|
|
1006
|
+
"font-size": 13,
|
|
1007
|
+
"font-weight": 700,
|
|
1008
|
+
"font-family": FONT,
|
|
1009
|
+
"text-anchor": "middle",
|
|
1010
|
+
"dominant-baseline": "central"
|
|
1011
|
+
});
|
|
1012
|
+
text.textContent = String(n);
|
|
1013
|
+
g.append(circle, text);
|
|
1014
|
+
this.svg.append(g);
|
|
1015
|
+
this.commitMark({ kind: "step", x: p.x, y: p.y, n, color: this.color, id: this.nextId() }, g);
|
|
1016
|
+
}
|
|
1017
|
+
// --------------------------------------------------------------------------
|
|
1018
|
+
// Keyboard
|
|
1019
|
+
// --------------------------------------------------------------------------
|
|
273
1020
|
onKey = (e) => {
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
1021
|
+
if (this.textInputEl) {
|
|
1022
|
+
if (e.key === "Escape") {
|
|
1023
|
+
e.preventDefault();
|
|
1024
|
+
e.stopPropagation();
|
|
1025
|
+
this.cancelTextInput();
|
|
1026
|
+
} else if (e.key === "Enter") {
|
|
1027
|
+
e.preventDefault();
|
|
1028
|
+
e.stopPropagation();
|
|
1029
|
+
this.commitTextInput();
|
|
1030
|
+
}
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (e.key === "Escape") {
|
|
1034
|
+
e.preventDefault();
|
|
1035
|
+
e.stopPropagation();
|
|
1036
|
+
this.exit();
|
|
1037
|
+
}
|
|
278
1038
|
};
|
|
1039
|
+
// --------------------------------------------------------------------------
|
|
1040
|
+
// Small helpers
|
|
1041
|
+
// --------------------------------------------------------------------------
|
|
1042
|
+
svgEl(tag, attrs) {
|
|
1043
|
+
const node = document.createElementNS(SVG_NS, tag);
|
|
1044
|
+
for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, String(v));
|
|
1045
|
+
return node;
|
|
1046
|
+
}
|
|
1047
|
+
pointsAttr(points) {
|
|
1048
|
+
return points.map((p) => `${p.x},${p.y}`).join(" ");
|
|
1049
|
+
}
|
|
1050
|
+
nextId() {
|
|
1051
|
+
return `lens-${++this.idSeq}`;
|
|
1052
|
+
}
|
|
279
1053
|
};
|
|
280
|
-
var svg = (inner) => `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${inner}</svg>`;
|
|
281
|
-
var iconEye = svg(
|
|
282
|
-
'<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>'
|
|
283
|
-
);
|
|
284
|
-
var iconCrosshair = svg(
|
|
285
|
-
'<circle cx="12" cy="12" r="8"/><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/>'
|
|
286
|
-
);
|
|
287
|
-
var iconClose = svg('<line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/>');
|
|
288
|
-
var iconMark = svg(
|
|
289
|
-
'<rect x="3" y="3" width="18" height="18" rx="2" stroke-dasharray="4 3"/>'
|
|
290
|
-
);
|
|
291
|
-
var iconCopy = svg(
|
|
292
|
-
'<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/>'
|
|
293
|
-
);
|
|
294
|
-
var iconCheck = svg('<polyline points="20 6 9 17 4 12"/>');
|
|
295
|
-
var iconSend = svg('<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>');
|
|
296
|
-
var iconRecord = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="12" cy="12" r="6" fill="#ef4444"/></svg>`;
|
|
297
|
-
var iconStop = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>`;
|
|
298
1054
|
var INCLUDE_ITEMS = [
|
|
299
1055
|
{ key: "replay", label: "Replay" },
|
|
300
1056
|
{ key: "console", label: "Console" },
|
|
@@ -451,7 +1207,7 @@ var Panel = class {
|
|
|
451
1207
|
buildActions() {
|
|
452
1208
|
const mark = el("button", {
|
|
453
1209
|
class: "lens-abtn lens-abtn--ghost lens-abtn--compact",
|
|
454
|
-
html:
|
|
1210
|
+
html: iconAnnotate + "<span>Annotate</span>",
|
|
455
1211
|
on: { click: () => this.cb.onMarkArea() }
|
|
456
1212
|
});
|
|
457
1213
|
const record = el("button", {
|
|
@@ -835,21 +1591,33 @@ var OVERLAY_CSS = (
|
|
|
835
1591
|
pointer-events: none;
|
|
836
1592
|
}
|
|
837
1593
|
|
|
838
|
-
/* ---------------- Annotation layer ---------------- */
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1594
|
+
/* ---------------- Annotation SVG layer ---------------- */
|
|
1595
|
+
/* One full-DOCUMENT SVG anchored to page content (translated by -scroll so
|
|
1596
|
+
marks survive scrolling). pointer-events flip to auto only while drawing. */
|
|
1597
|
+
.lens-annotate-svg {
|
|
1598
|
+
position: absolute;
|
|
1599
|
+
left: 0;
|
|
1600
|
+
top: 0;
|
|
842
1601
|
z-index: calc(var(--lens-z) - 1);
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
touch-action: none;
|
|
1602
|
+
pointer-events: none;
|
|
1603
|
+
overflow: visible;
|
|
846
1604
|
}
|
|
847
|
-
|
|
1605
|
+
|
|
1606
|
+
/* Floating text-entry input for the Text tool (viewport-positioned). */
|
|
1607
|
+
.lens-text-input {
|
|
848
1608
|
position: fixed;
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1609
|
+
z-index: var(--lens-z);
|
|
1610
|
+
min-width: 120px;
|
|
1611
|
+
padding: 2px 6px;
|
|
1612
|
+
border: 1px solid var(--lens-accent);
|
|
1613
|
+
border-radius: 4px;
|
|
1614
|
+
background: rgba(255, 255, 255, 0.96);
|
|
1615
|
+
color: #111827;
|
|
1616
|
+
font-family: var(--lens-font);
|
|
1617
|
+
font-size: 15px;
|
|
1618
|
+
line-height: 1.3;
|
|
1619
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
|
|
1620
|
+
outline: none;
|
|
853
1621
|
}
|
|
854
1622
|
|
|
855
1623
|
/* ---------------- Panel ---------------- */
|
|
@@ -1140,6 +1908,114 @@ var OVERLAY_CSS = (
|
|
|
1140
1908
|
}
|
|
1141
1909
|
.lens-annotate-chips { z-index: var(--lens-z); }
|
|
1142
1910
|
|
|
1911
|
+
/* ---------------- Annotation toolbar ---------------- */
|
|
1912
|
+
/* Same floating top-center anchor as the chips row; wraps on narrow screens. */
|
|
1913
|
+
.lens-annotate-toolbar {
|
|
1914
|
+
position: fixed;
|
|
1915
|
+
top: calc(16px + env(safe-area-inset-top));
|
|
1916
|
+
left: 50%;
|
|
1917
|
+
transform: translateX(-50%);
|
|
1918
|
+
z-index: var(--lens-z);
|
|
1919
|
+
display: inline-flex;
|
|
1920
|
+
flex-wrap: wrap;
|
|
1921
|
+
align-items: center;
|
|
1922
|
+
justify-content: center;
|
|
1923
|
+
gap: 6px;
|
|
1924
|
+
max-width: calc(100vw - 24px);
|
|
1925
|
+
padding: 6px 8px;
|
|
1926
|
+
border-radius: 14px;
|
|
1927
|
+
background: rgba(20, 20, 23, 0.92);
|
|
1928
|
+
-webkit-backdrop-filter: blur(12px) saturate(180%);
|
|
1929
|
+
backdrop-filter: blur(12px) saturate(180%);
|
|
1930
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
1931
|
+
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
|
|
1932
|
+
color: #f4f4f5;
|
|
1933
|
+
}
|
|
1934
|
+
.lens-tool-group {
|
|
1935
|
+
display: inline-flex;
|
|
1936
|
+
align-items: center;
|
|
1937
|
+
gap: 4px;
|
|
1938
|
+
flex-wrap: wrap;
|
|
1939
|
+
}
|
|
1940
|
+
.lens-tool-sep {
|
|
1941
|
+
width: 1px;
|
|
1942
|
+
align-self: stretch;
|
|
1943
|
+
min-height: 22px;
|
|
1944
|
+
background: rgba(255, 255, 255, 0.16);
|
|
1945
|
+
margin: 0 2px;
|
|
1946
|
+
}
|
|
1947
|
+
.lens-tool-btn {
|
|
1948
|
+
display: inline-flex;
|
|
1949
|
+
align-items: center;
|
|
1950
|
+
justify-content: center;
|
|
1951
|
+
width: 30px;
|
|
1952
|
+
height: 30px;
|
|
1953
|
+
padding: 0;
|
|
1954
|
+
border: 1px solid transparent;
|
|
1955
|
+
border-radius: 8px;
|
|
1956
|
+
background: transparent;
|
|
1957
|
+
color: #d4d4d8;
|
|
1958
|
+
cursor: pointer;
|
|
1959
|
+
transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
|
|
1960
|
+
}
|
|
1961
|
+
.lens-tool-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
|
|
1962
|
+
.lens-tool-btn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
|
|
1963
|
+
.lens-tool-btn.is-active {
|
|
1964
|
+
background: var(--lens-accent);
|
|
1965
|
+
border-color: var(--lens-accent);
|
|
1966
|
+
color: #fff;
|
|
1967
|
+
}
|
|
1968
|
+
.lens-tool-btn:disabled { opacity: 0.4; cursor: default; }
|
|
1969
|
+
.lens-tool-btn:disabled:hover { background: transparent; color: #d4d4d8; }
|
|
1970
|
+
|
|
1971
|
+
.lens-swatch {
|
|
1972
|
+
width: 22px;
|
|
1973
|
+
height: 22px;
|
|
1974
|
+
padding: 0;
|
|
1975
|
+
border-radius: 999px;
|
|
1976
|
+
border: 2px solid transparent;
|
|
1977
|
+
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25) inset;
|
|
1978
|
+
cursor: pointer;
|
|
1979
|
+
transition: transform 120ms ease, border-color 120ms ease;
|
|
1980
|
+
}
|
|
1981
|
+
.lens-swatch:hover { transform: scale(1.1); }
|
|
1982
|
+
.lens-swatch:focus-visible { outline: 2px solid #fff; outline-offset: 1px; }
|
|
1983
|
+
.lens-swatch.is-active {
|
|
1984
|
+
border-color: #fff;
|
|
1985
|
+
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.4);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
.lens-width-btn {
|
|
1989
|
+
display: inline-flex;
|
|
1990
|
+
align-items: center;
|
|
1991
|
+
justify-content: center;
|
|
1992
|
+
min-width: 26px;
|
|
1993
|
+
height: 30px;
|
|
1994
|
+
padding: 0 6px;
|
|
1995
|
+
border: 1px solid transparent;
|
|
1996
|
+
border-radius: 8px;
|
|
1997
|
+
background: transparent;
|
|
1998
|
+
color: #d4d4d8;
|
|
1999
|
+
font-family: var(--lens-font);
|
|
2000
|
+
font-size: 12px;
|
|
2001
|
+
font-weight: 700;
|
|
2002
|
+
cursor: pointer;
|
|
2003
|
+
transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
|
|
2004
|
+
}
|
|
2005
|
+
.lens-width-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
|
|
2006
|
+
.lens-width-btn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
|
|
2007
|
+
.lens-width-btn.is-active {
|
|
2008
|
+
background: var(--lens-accent);
|
|
2009
|
+
border-color: var(--lens-accent);
|
|
2010
|
+
color: #fff;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
/* Coarse pointers: bump the tool targets toward 40px. */
|
|
2014
|
+
@media (pointer: coarse) {
|
|
2015
|
+
.lens-tool-btn, .lens-width-btn { width: 38px; height: 38px; }
|
|
2016
|
+
.lens-swatch { width: 30px; height: 30px; }
|
|
2017
|
+
}
|
|
2018
|
+
|
|
1143
2019
|
/* Stacking inside the shadow layer: the panel, pill and floating chips always
|
|
1144
2020
|
sit above the annotation layer/rects and picker highlight, so marked areas
|
|
1145
2021
|
never paint over interactive UI. */
|
|
@@ -1265,7 +2141,7 @@ var Overlay = class {
|
|
|
1265
2141
|
this.layer = el("div", { class: "lens-layer" });
|
|
1266
2142
|
this.shadow.append(this.layer);
|
|
1267
2143
|
this.picker = new ElementPicker(this.layer, this.host);
|
|
1268
|
-
this.annotator = new
|
|
2144
|
+
this.annotator = new AnnotationEditor(this.layer);
|
|
1269
2145
|
this.layer.append(this.buildPill());
|
|
1270
2146
|
document.documentElement.append(this.host);
|
|
1271
2147
|
window.addEventListener("keydown", this.onGlobalKey, true);
|
|
@@ -1433,13 +2309,15 @@ var Overlay = class {
|
|
|
1433
2309
|
}
|
|
1434
2310
|
async submit(comment, include) {
|
|
1435
2311
|
const annotations = this.annotator.list();
|
|
1436
|
-
const
|
|
2312
|
+
const composite = this.annotator.getComposite() ?? void 0;
|
|
2313
|
+
const screenshotCrop = this.annotator.getCropBounds();
|
|
1437
2314
|
const result = await this.deps.client.submitReport({
|
|
1438
2315
|
comment: comment || void 0,
|
|
1439
2316
|
annotations,
|
|
1440
2317
|
target: this.target ?? void 0,
|
|
1441
2318
|
excludeFromScreenshot: this.host,
|
|
1442
2319
|
screenshotCrop,
|
|
2320
|
+
annotationComposite: composite,
|
|
1443
2321
|
include
|
|
1444
2322
|
});
|
|
1445
2323
|
this.target = null;
|
|
@@ -1448,35 +2326,6 @@ var Overlay = class {
|
|
|
1448
2326
|
this.deps.client.discardRecording();
|
|
1449
2327
|
return result;
|
|
1450
2328
|
}
|
|
1451
|
-
/**
|
|
1452
|
-
* Union the marked rects into a single bounding box in DOCUMENT coordinates,
|
|
1453
|
-
* or `undefined` if nothing is marked (→ full-page screenshot). Each rect is
|
|
1454
|
-
* fixed-positioned, so getBoundingClientRect() gives viewport coords; add the
|
|
1455
|
-
* page scroll offset to convert to document space.
|
|
1456
|
-
*/
|
|
1457
|
-
computeAnnotationCrop() {
|
|
1458
|
-
const rects = this.annotator.rects();
|
|
1459
|
-
if (rects.length === 0) return void 0;
|
|
1460
|
-
const scrollX = window.scrollX || window.pageXOffset || 0;
|
|
1461
|
-
const scrollY = window.scrollY || window.pageYOffset || 0;
|
|
1462
|
-
let minX = Infinity;
|
|
1463
|
-
let minY = Infinity;
|
|
1464
|
-
let maxX = -Infinity;
|
|
1465
|
-
let maxY = -Infinity;
|
|
1466
|
-
for (const rect of rects) {
|
|
1467
|
-
const r = rect.getBoundingClientRect();
|
|
1468
|
-
const left = r.left + scrollX;
|
|
1469
|
-
const top = r.top + scrollY;
|
|
1470
|
-
minX = Math.min(minX, left);
|
|
1471
|
-
minY = Math.min(minY, top);
|
|
1472
|
-
maxX = Math.max(maxX, left + r.width);
|
|
1473
|
-
maxY = Math.max(maxY, top + r.height);
|
|
1474
|
-
}
|
|
1475
|
-
const width = maxX - minX;
|
|
1476
|
-
const height = maxY - minY;
|
|
1477
|
-
if (!(width > 0) || !(height > 0)) return void 0;
|
|
1478
|
-
return { x: minX, y: minY, width, height };
|
|
1479
|
-
}
|
|
1480
2329
|
dismiss() {
|
|
1481
2330
|
this.deps.onDismiss();
|
|
1482
2331
|
this.destroy();
|
|
@@ -1679,4 +2528,4 @@ export {
|
|
|
1679
2528
|
boot,
|
|
1680
2529
|
autoBoot
|
|
1681
2530
|
};
|
|
1682
|
-
//# sourceMappingURL=chunk-
|
|
2531
|
+
//# sourceMappingURL=chunk-UT7E5LCG.js.map
|