@merittdev/horus-lens 0.1.0 → 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 +1290 -142
- 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-MTQ3IO4U.js → chunk-JXSK4JG4.js} +273 -12
- package/dist/chunk-JXSK4JG4.js.map +1 -0
- package/dist/{chunk-A6SNBIRS.js → chunk-UT7E5LCG.js} +1017 -130
- package/dist/chunk-UT7E5LCG.js.map +1 -0
- package/dist/index.cjs +272 -11
- package/dist/index.cjs.map +1 -1
- package/dist/{index.d-BA540Fae.d.ts → index.d-BtnbCqUx.d.ts} +39 -1
- package/dist/index.d-Cyvb07sp.d.cts +301 -0
- package/dist/index.d-Cyvb07sp.d.ts +301 -0
- package/dist/{index.d-DOCLVJGS.d.cts → index.d-QzZn4EMM.d.cts} +39 -1
- package/dist/index.d.cts +355 -37
- package/dist/index.d.ts +355 -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 +1290 -142
- 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-A6SNBIRS.js.map +0 -1
- package/dist/chunk-MTQ3IO4U.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,170 +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) =>
|
|
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
|
+
});
|
|
151
343
|
}
|
|
344
|
+
/** Remove every mark. Tears the layer down when not actively drawing. */
|
|
152
345
|
clear() {
|
|
346
|
+
for (const node of this.markNodes) node.remove();
|
|
347
|
+
this.markNodes = [];
|
|
153
348
|
this.annotations = [];
|
|
154
|
-
|
|
155
|
-
this.
|
|
349
|
+
this.stepCounter = 0;
|
|
350
|
+
if (!this.active) this.teardownLayer();
|
|
351
|
+
this.syncActionButtons();
|
|
156
352
|
}
|
|
157
353
|
start(onExit) {
|
|
158
354
|
if (this.active) return;
|
|
355
|
+
this.ensureLayer();
|
|
356
|
+
const svg2 = this.svg;
|
|
159
357
|
this.active = true;
|
|
160
358
|
this.onExit = onExit;
|
|
161
359
|
this.sessionBase = this.annotations.length;
|
|
162
|
-
this.
|
|
163
|
-
this.
|
|
164
|
-
this.
|
|
165
|
-
|
|
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" }, [
|
|
166
618
|
el("button", {
|
|
167
619
|
class: "lens-pick-chip__btn",
|
|
168
620
|
text: "Cancel",
|
|
621
|
+
attrs: { type: "button" },
|
|
169
622
|
on: { click: () => this.cancel() }
|
|
170
623
|
}),
|
|
171
624
|
el("button", {
|
|
172
625
|
class: "lens-pick-chip__btn lens-pick-chip__btn--primary",
|
|
173
626
|
text: "Done",
|
|
627
|
+
attrs: { type: "button" },
|
|
174
628
|
on: { click: () => this.exit() }
|
|
175
629
|
})
|
|
176
630
|
]);
|
|
177
|
-
this.
|
|
178
|
-
|
|
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
|
+
}
|
|
179
675
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
}
|
|
186
683
|
}
|
|
187
|
-
|
|
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) => {
|
|
188
699
|
if (!this.active) return;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
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;
|
|
197
707
|
}
|
|
198
|
-
if (
|
|
199
|
-
this.
|
|
200
|
-
|
|
201
|
-
this.layer = null;
|
|
708
|
+
if (tool === "step") {
|
|
709
|
+
this.placeStep(p);
|
|
710
|
+
return;
|
|
202
711
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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);
|
|
206
745
|
}
|
|
207
|
-
|
|
208
|
-
this.
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
onDown = (e) => {
|
|
212
|
-
e.preventDefault();
|
|
213
|
-
this.dragging = true;
|
|
214
|
-
this.startX = e.clientX;
|
|
215
|
-
this.startY = e.clientY;
|
|
216
|
-
this.draft = el("div", { class: "lens-rect" });
|
|
217
|
-
this.host.append(this.draft);
|
|
218
|
-
this.position(this.draft, e.clientX, e.clientY);
|
|
219
|
-
window.addEventListener("pointermove", this.onMove, true);
|
|
220
|
-
window.addEventListener("pointerup", this.onUp, true);
|
|
746
|
+
this.drag = drag;
|
|
747
|
+
window.addEventListener("pointermove", this.onPointerMove, true);
|
|
748
|
+
window.addEventListener("pointerup", this.onPointerUp, true);
|
|
221
749
|
};
|
|
222
|
-
|
|
223
|
-
|
|
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
|
+
}
|
|
224
766
|
};
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
this.
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
+
});
|
|
236
883
|
}
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
+
});
|
|
893
|
+
}
|
|
894
|
+
return this.svgEl("rect", {
|
|
895
|
+
"data-lens-draft": "1",
|
|
896
|
+
fill: "none",
|
|
897
|
+
stroke: color,
|
|
898
|
+
"stroke-width": width,
|
|
899
|
+
rx: 2
|
|
244
900
|
});
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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 }
|
|
254
944
|
});
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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"
|
|
264
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
|
+
);
|
|
265
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
|
+
// --------------------------------------------------------------------------
|
|
266
1020
|
onKey = (e) => {
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
+
}
|
|
271
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
|
+
}
|
|
272
1053
|
};
|
|
273
|
-
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>`;
|
|
274
|
-
var iconEye = svg(
|
|
275
|
-
'<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"/>'
|
|
276
|
-
);
|
|
277
|
-
var iconCrosshair = svg(
|
|
278
|
-
'<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"/>'
|
|
279
|
-
);
|
|
280
|
-
var iconClose = svg('<line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/>');
|
|
281
|
-
var iconMark = svg(
|
|
282
|
-
'<rect x="3" y="3" width="18" height="18" rx="2" stroke-dasharray="4 3"/>'
|
|
283
|
-
);
|
|
284
|
-
var iconCopy = svg(
|
|
285
|
-
'<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/>'
|
|
286
|
-
);
|
|
287
|
-
var iconCheck = svg('<polyline points="20 6 9 17 4 12"/>');
|
|
288
|
-
var iconSend = svg('<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>');
|
|
289
|
-
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>`;
|
|
290
|
-
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>`;
|
|
291
1054
|
var INCLUDE_ITEMS = [
|
|
292
1055
|
{ key: "replay", label: "Replay" },
|
|
293
1056
|
{ key: "console", label: "Console" },
|
|
@@ -444,7 +1207,7 @@ var Panel = class {
|
|
|
444
1207
|
buildActions() {
|
|
445
1208
|
const mark = el("button", {
|
|
446
1209
|
class: "lens-abtn lens-abtn--ghost lens-abtn--compact",
|
|
447
|
-
html:
|
|
1210
|
+
html: iconAnnotate + "<span>Annotate</span>",
|
|
448
1211
|
on: { click: () => this.cb.onMarkArea() }
|
|
449
1212
|
});
|
|
450
1213
|
const record = el("button", {
|
|
@@ -828,21 +1591,33 @@ var OVERLAY_CSS = (
|
|
|
828
1591
|
pointer-events: none;
|
|
829
1592
|
}
|
|
830
1593
|
|
|
831
|
-
/* ---------------- Annotation layer ---------------- */
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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;
|
|
835
1601
|
z-index: calc(var(--lens-z) - 1);
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
touch-action: none;
|
|
1602
|
+
pointer-events: none;
|
|
1603
|
+
overflow: visible;
|
|
839
1604
|
}
|
|
840
|
-
|
|
1605
|
+
|
|
1606
|
+
/* Floating text-entry input for the Text tool (viewport-positioned). */
|
|
1607
|
+
.lens-text-input {
|
|
841
1608
|
position: fixed;
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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;
|
|
846
1621
|
}
|
|
847
1622
|
|
|
848
1623
|
/* ---------------- Panel ---------------- */
|
|
@@ -1133,6 +1908,114 @@ var OVERLAY_CSS = (
|
|
|
1133
1908
|
}
|
|
1134
1909
|
.lens-annotate-chips { z-index: var(--lens-z); }
|
|
1135
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
|
+
|
|
1136
2019
|
/* Stacking inside the shadow layer: the panel, pill and floating chips always
|
|
1137
2020
|
sit above the annotation layer/rects and picker highlight, so marked areas
|
|
1138
2021
|
never paint over interactive UI. */
|
|
@@ -1258,7 +2141,7 @@ var Overlay = class {
|
|
|
1258
2141
|
this.layer = el("div", { class: "lens-layer" });
|
|
1259
2142
|
this.shadow.append(this.layer);
|
|
1260
2143
|
this.picker = new ElementPicker(this.layer, this.host);
|
|
1261
|
-
this.annotator = new
|
|
2144
|
+
this.annotator = new AnnotationEditor(this.layer);
|
|
1262
2145
|
this.layer.append(this.buildPill());
|
|
1263
2146
|
document.documentElement.append(this.host);
|
|
1264
2147
|
window.addEventListener("keydown", this.onGlobalKey, true);
|
|
@@ -1426,11 +2309,15 @@ var Overlay = class {
|
|
|
1426
2309
|
}
|
|
1427
2310
|
async submit(comment, include) {
|
|
1428
2311
|
const annotations = this.annotator.list();
|
|
2312
|
+
const composite = this.annotator.getComposite() ?? void 0;
|
|
2313
|
+
const screenshotCrop = this.annotator.getCropBounds();
|
|
1429
2314
|
const result = await this.deps.client.submitReport({
|
|
1430
2315
|
comment: comment || void 0,
|
|
1431
2316
|
annotations,
|
|
1432
2317
|
target: this.target ?? void 0,
|
|
1433
2318
|
excludeFromScreenshot: this.host,
|
|
2319
|
+
screenshotCrop,
|
|
2320
|
+
annotationComposite: composite,
|
|
1434
2321
|
include
|
|
1435
2322
|
});
|
|
1436
2323
|
this.target = null;
|
|
@@ -1641,4 +2528,4 @@ export {
|
|
|
1641
2528
|
boot,
|
|
1642
2529
|
autoBoot
|
|
1643
2530
|
};
|
|
1644
|
-
//# sourceMappingURL=chunk-
|
|
2531
|
+
//# sourceMappingURL=chunk-UT7E5LCG.js.map
|