@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
package/dist/react.cjs
CHANGED
|
@@ -28893,15 +28893,86 @@ var TargetElementSchema = external_exports.object({
|
|
|
28893
28893
|
boundingRect: external_exports.object({ x: external_exports.number(), y: external_exports.number(), width: external_exports.number(), height: external_exports.number() }).optional(),
|
|
28894
28894
|
reactComponents: external_exports.array(external_exports.string()).optional()
|
|
28895
28895
|
});
|
|
28896
|
-
var
|
|
28897
|
-
|
|
28898
|
-
|
|
28899
|
-
|
|
28900
|
-
|
|
28901
|
-
|
|
28902
|
-
|
|
28903
|
-
|
|
28904
|
-
|
|
28896
|
+
var annotationCommon = {
|
|
28897
|
+
id: external_exports.string().optional(),
|
|
28898
|
+
/** Any CSS color string. */
|
|
28899
|
+
color: external_exports.string().optional(),
|
|
28900
|
+
strokeWidth: external_exports.number().optional()
|
|
28901
|
+
};
|
|
28902
|
+
var AnnotationPointSchema = external_exports.object({ x: external_exports.number(), y: external_exports.number() });
|
|
28903
|
+
var AnnotationSchema = external_exports.discriminatedUnion("kind", [
|
|
28904
|
+
external_exports.object({
|
|
28905
|
+
...annotationCommon,
|
|
28906
|
+
kind: external_exports.literal("rect"),
|
|
28907
|
+
x: external_exports.number(),
|
|
28908
|
+
y: external_exports.number(),
|
|
28909
|
+
width: external_exports.number(),
|
|
28910
|
+
height: external_exports.number(),
|
|
28911
|
+
/**
|
|
28912
|
+
* OPTIONAL, kept for back-compat with 0.1.x clients that emit
|
|
28913
|
+
* viewport-anchored rects. New clients emit document-pixel rects with no
|
|
28914
|
+
* viewport.
|
|
28915
|
+
*/
|
|
28916
|
+
viewport: external_exports.object({ width: external_exports.number(), height: external_exports.number() }).optional()
|
|
28917
|
+
}),
|
|
28918
|
+
external_exports.object({
|
|
28919
|
+
...annotationCommon,
|
|
28920
|
+
kind: external_exports.literal("ellipse"),
|
|
28921
|
+
x: external_exports.number(),
|
|
28922
|
+
y: external_exports.number(),
|
|
28923
|
+
width: external_exports.number(),
|
|
28924
|
+
height: external_exports.number()
|
|
28925
|
+
}),
|
|
28926
|
+
external_exports.object({
|
|
28927
|
+
...annotationCommon,
|
|
28928
|
+
kind: external_exports.literal("blur"),
|
|
28929
|
+
x: external_exports.number(),
|
|
28930
|
+
y: external_exports.number(),
|
|
28931
|
+
width: external_exports.number(),
|
|
28932
|
+
height: external_exports.number()
|
|
28933
|
+
}),
|
|
28934
|
+
external_exports.object({
|
|
28935
|
+
...annotationCommon,
|
|
28936
|
+
kind: external_exports.literal("arrow"),
|
|
28937
|
+
x1: external_exports.number(),
|
|
28938
|
+
y1: external_exports.number(),
|
|
28939
|
+
x2: external_exports.number(),
|
|
28940
|
+
y2: external_exports.number()
|
|
28941
|
+
}),
|
|
28942
|
+
external_exports.object({
|
|
28943
|
+
...annotationCommon,
|
|
28944
|
+
kind: external_exports.literal("line"),
|
|
28945
|
+
x1: external_exports.number(),
|
|
28946
|
+
y1: external_exports.number(),
|
|
28947
|
+
x2: external_exports.number(),
|
|
28948
|
+
y2: external_exports.number()
|
|
28949
|
+
}),
|
|
28950
|
+
external_exports.object({
|
|
28951
|
+
...annotationCommon,
|
|
28952
|
+
kind: external_exports.literal("pencil"),
|
|
28953
|
+
points: external_exports.array(AnnotationPointSchema).min(2)
|
|
28954
|
+
}),
|
|
28955
|
+
external_exports.object({
|
|
28956
|
+
...annotationCommon,
|
|
28957
|
+
kind: external_exports.literal("highlighter"),
|
|
28958
|
+
points: external_exports.array(AnnotationPointSchema).min(2)
|
|
28959
|
+
}),
|
|
28960
|
+
external_exports.object({
|
|
28961
|
+
...annotationCommon,
|
|
28962
|
+
kind: external_exports.literal("text"),
|
|
28963
|
+
x: external_exports.number(),
|
|
28964
|
+
y: external_exports.number(),
|
|
28965
|
+
text: external_exports.string(),
|
|
28966
|
+
fontSize: external_exports.number().optional()
|
|
28967
|
+
}),
|
|
28968
|
+
external_exports.object({
|
|
28969
|
+
...annotationCommon,
|
|
28970
|
+
kind: external_exports.literal("step"),
|
|
28971
|
+
x: external_exports.number(),
|
|
28972
|
+
y: external_exports.number(),
|
|
28973
|
+
n: external_exports.number()
|
|
28974
|
+
})
|
|
28975
|
+
]);
|
|
28905
28976
|
var AssetKindSchema = external_exports.enum(["replay", "console", "network", "dom", "screenshot", "attachment"]);
|
|
28906
28977
|
var AssetRefSchema = external_exports.object({
|
|
28907
28978
|
kind: AssetKindSchema,
|
|
@@ -29354,6 +29425,11 @@ var ReplayRecorder = class {
|
|
|
29354
29425
|
emit: (event, isCheckout) => this.onEmit(event, isCheckout),
|
|
29355
29426
|
checkoutEveryNms: this.windowMs,
|
|
29356
29427
|
recordCanvas: false,
|
|
29428
|
+
// Inline same-origin + CORS images as data URIs at snapshot time so the
|
|
29429
|
+
// dashboard replay paints on a different origin (tainted cross-origin
|
|
29430
|
+
// images fall back to their URL, which is fine for public CDNs).
|
|
29431
|
+
inlineImages: true,
|
|
29432
|
+
collectFonts: true,
|
|
29357
29433
|
// Keep the Lens overlay itself out of every recording.
|
|
29358
29434
|
blockSelector: `#${LENS_OVERLAY_HOST_ID}`
|
|
29359
29435
|
});
|
|
@@ -29460,7 +29536,20 @@ var ReplayRecorder = class {
|
|
|
29460
29536
|
console.warn("[lens] rrweb recording unavailable; replay disabled", err);
|
|
29461
29537
|
}
|
|
29462
29538
|
};
|
|
29463
|
-
|
|
29539
|
+
var SVG_RASTERIZE_TIMEOUT_MS = 3e3;
|
|
29540
|
+
function computeCropRect(crop, scale, rasterWidth, rasterHeight) {
|
|
29541
|
+
let sx = Math.round(crop.x * scale);
|
|
29542
|
+
let sy = Math.round(crop.y * scale);
|
|
29543
|
+
let sw = Math.round(crop.width * scale);
|
|
29544
|
+
let sh = Math.round(crop.height * scale);
|
|
29545
|
+
sx = Math.max(0, Math.min(sx, rasterWidth));
|
|
29546
|
+
sy = Math.max(0, Math.min(sy, rasterHeight));
|
|
29547
|
+
sw = Math.min(sw, rasterWidth - sx);
|
|
29548
|
+
sh = Math.min(sh, rasterHeight - sy);
|
|
29549
|
+
if (sw <= 0 || sh <= 0) return null;
|
|
29550
|
+
return { sx, sy, sw, sh };
|
|
29551
|
+
}
|
|
29552
|
+
async function captureScreenshot(excludeHost, crop, composite) {
|
|
29464
29553
|
if (typeof document === "undefined" || typeof window === "undefined") return null;
|
|
29465
29554
|
try {
|
|
29466
29555
|
const viewportPixels = window.innerWidth * window.innerHeight || 1;
|
|
@@ -29470,6 +29559,15 @@ async function captureScreenshot(excludeHost) {
|
|
|
29470
29559
|
scale,
|
|
29471
29560
|
type: "image/jpeg",
|
|
29472
29561
|
quality: 0.82,
|
|
29562
|
+
// JPEG has no alpha — transparent page regions (and any capture box
|
|
29563
|
+
// extending past short content) would otherwise encode as BLACK.
|
|
29564
|
+
backgroundColor: "#ffffff",
|
|
29565
|
+
// When compositing annotations, force the capture box to the editor's
|
|
29566
|
+
// document measurement (scrollHeight-based). modern-screenshot otherwise
|
|
29567
|
+
// sizes from documentElement's bounding box, which on pages SHORTER than
|
|
29568
|
+
// the viewport is the content height only — marks drawn in the lower
|
|
29569
|
+
// viewport band would fall off the raster and the crop would clamp short.
|
|
29570
|
+
...composite ? { width: composite.docWidth, height: composite.docHeight } : {},
|
|
29473
29571
|
// Exclude the overlay from the capture CLONE — never hide the live node.
|
|
29474
29572
|
filter: (node2) => node2 !== excludeHost,
|
|
29475
29573
|
// Per-resource fetch budget: a single slow cross-origin image must not
|
|
@@ -29494,12 +29592,171 @@ async function captureScreenshot(excludeHost) {
|
|
|
29494
29592
|
);
|
|
29495
29593
|
return null;
|
|
29496
29594
|
}
|
|
29595
|
+
if (composite) {
|
|
29596
|
+
const baked = await compositeScreenshot(blob2, composite, scale, crop);
|
|
29597
|
+
if (baked) return baked;
|
|
29598
|
+
if (composite.blurRegions.length > 0) {
|
|
29599
|
+
console.warn(
|
|
29600
|
+
"[lens] screenshot compositing failed with active redactions; dropping screenshot to avoid leaking un-blurred content"
|
|
29601
|
+
);
|
|
29602
|
+
return null;
|
|
29603
|
+
}
|
|
29604
|
+
}
|
|
29605
|
+
if (crop) {
|
|
29606
|
+
const cropped = await cropBlob(blob2, crop, scale);
|
|
29607
|
+
return cropped ?? blob2;
|
|
29608
|
+
}
|
|
29497
29609
|
return blob2;
|
|
29498
29610
|
} catch (err) {
|
|
29499
29611
|
console.warn("[lens] screenshot capture failed:", err);
|
|
29500
29612
|
return null;
|
|
29501
29613
|
}
|
|
29502
29614
|
}
|
|
29615
|
+
async function compositeScreenshot(blob2, composite, scale, crop) {
|
|
29616
|
+
try {
|
|
29617
|
+
if (typeof createImageBitmap !== "function" || typeof document === "undefined") return null;
|
|
29618
|
+
const bitmap = await createImageBitmap(blob2);
|
|
29619
|
+
const canvas = document.createElement("canvas");
|
|
29620
|
+
canvas.width = bitmap.width;
|
|
29621
|
+
canvas.height = bitmap.height;
|
|
29622
|
+
const ctx = canvas.getContext("2d");
|
|
29623
|
+
if (!ctx) {
|
|
29624
|
+
bitmap.close?.();
|
|
29625
|
+
return null;
|
|
29626
|
+
}
|
|
29627
|
+
ctx.fillStyle = "#ffffff";
|
|
29628
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
29629
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
29630
|
+
bitmap.close?.();
|
|
29631
|
+
applyBlurRegions(ctx, canvas, composite.blurRegions, scale);
|
|
29632
|
+
await drawSvgOverlay(ctx, composite, scale);
|
|
29633
|
+
if (crop) {
|
|
29634
|
+
const rect = computeCropRect(crop, scale, canvas.width, canvas.height);
|
|
29635
|
+
if (rect) {
|
|
29636
|
+
const out = document.createElement("canvas");
|
|
29637
|
+
out.width = rect.sw;
|
|
29638
|
+
out.height = rect.sh;
|
|
29639
|
+
const octx = out.getContext("2d");
|
|
29640
|
+
if (octx) {
|
|
29641
|
+
octx.drawImage(canvas, rect.sx, rect.sy, rect.sw, rect.sh, 0, 0, rect.sw, rect.sh);
|
|
29642
|
+
return await canvasToJpegBlob(out);
|
|
29643
|
+
}
|
|
29644
|
+
}
|
|
29645
|
+
}
|
|
29646
|
+
return await canvasToJpegBlob(canvas);
|
|
29647
|
+
} catch (err) {
|
|
29648
|
+
console.warn("[lens] screenshot compositing failed:", err);
|
|
29649
|
+
return null;
|
|
29650
|
+
}
|
|
29651
|
+
}
|
|
29652
|
+
function applyBlurRegions(ctx, canvas, regions, scale) {
|
|
29653
|
+
if (regions.length === 0) return;
|
|
29654
|
+
const filterSupported = typeof ctx.filter === "string";
|
|
29655
|
+
const blurPx = Math.max(8, 12 * scale);
|
|
29656
|
+
const pad = Math.ceil(blurPx);
|
|
29657
|
+
for (const region of regions) {
|
|
29658
|
+
const x = Math.max(0, Math.min(Math.round(region.x * scale), canvas.width));
|
|
29659
|
+
const y = Math.max(0, Math.min(Math.round(region.y * scale), canvas.height));
|
|
29660
|
+
const w = Math.min(Math.round(region.width * scale), canvas.width - x);
|
|
29661
|
+
const h = Math.min(Math.round(region.height * scale), canvas.height - y);
|
|
29662
|
+
if (w <= 0 || h <= 0) continue;
|
|
29663
|
+
if (!filterSupported) {
|
|
29664
|
+
ctx.fillStyle = "rgba(127,127,127,1)";
|
|
29665
|
+
ctx.fillRect(x, y, w, h);
|
|
29666
|
+
continue;
|
|
29667
|
+
}
|
|
29668
|
+
const px = Math.max(0, x - pad);
|
|
29669
|
+
const py = Math.max(0, y - pad);
|
|
29670
|
+
const pw = Math.min(canvas.width, x + w + pad) - px;
|
|
29671
|
+
const ph = Math.min(canvas.height, y + h + pad) - py;
|
|
29672
|
+
if (pw <= 0 || ph <= 0) continue;
|
|
29673
|
+
const temp = document.createElement("canvas");
|
|
29674
|
+
temp.width = pw;
|
|
29675
|
+
temp.height = ph;
|
|
29676
|
+
const tctx = temp.getContext("2d");
|
|
29677
|
+
if (!tctx) {
|
|
29678
|
+
ctx.fillStyle = "rgba(127,127,127,1)";
|
|
29679
|
+
ctx.fillRect(x, y, w, h);
|
|
29680
|
+
continue;
|
|
29681
|
+
}
|
|
29682
|
+
tctx.filter = `blur(${blurPx}px)`;
|
|
29683
|
+
tctx.drawImage(canvas, px, py, pw, ph, 0, 0, pw, ph);
|
|
29684
|
+
tctx.filter = "none";
|
|
29685
|
+
ctx.save();
|
|
29686
|
+
ctx.beginPath();
|
|
29687
|
+
ctx.rect(x, y, w, h);
|
|
29688
|
+
ctx.clip();
|
|
29689
|
+
ctx.drawImage(temp, px, py);
|
|
29690
|
+
ctx.restore();
|
|
29691
|
+
}
|
|
29692
|
+
}
|
|
29693
|
+
async function drawSvgOverlay(ctx, composite, scale) {
|
|
29694
|
+
const { svgMarkup, docWidth, docHeight } = composite;
|
|
29695
|
+
if (!svgMarkup) return;
|
|
29696
|
+
const img = await loadSvgImage(svgMarkup);
|
|
29697
|
+
if (!img) return;
|
|
29698
|
+
try {
|
|
29699
|
+
ctx.drawImage(img, 0, 0, docWidth * scale, docHeight * scale);
|
|
29700
|
+
} catch (err) {
|
|
29701
|
+
console.warn("[lens] annotation overlay draw failed:", err);
|
|
29702
|
+
}
|
|
29703
|
+
}
|
|
29704
|
+
function loadSvgImage(svg2) {
|
|
29705
|
+
return new Promise((resolve) => {
|
|
29706
|
+
if (typeof Image !== "function") {
|
|
29707
|
+
resolve(null);
|
|
29708
|
+
return;
|
|
29709
|
+
}
|
|
29710
|
+
let settled = false;
|
|
29711
|
+
const finish = (result2) => {
|
|
29712
|
+
if (settled) return;
|
|
29713
|
+
settled = true;
|
|
29714
|
+
clearTimeout(timer);
|
|
29715
|
+
resolve(result2);
|
|
29716
|
+
};
|
|
29717
|
+
const timer = setTimeout(() => {
|
|
29718
|
+
console.warn("[lens] annotation overlay rasterization timed out");
|
|
29719
|
+
finish(null);
|
|
29720
|
+
}, SVG_RASTERIZE_TIMEOUT_MS);
|
|
29721
|
+
const img = new Image();
|
|
29722
|
+
img.onload = () => finish(img);
|
|
29723
|
+
img.onerror = () => {
|
|
29724
|
+
console.warn("[lens] annotation overlay failed to rasterize");
|
|
29725
|
+
finish(null);
|
|
29726
|
+
};
|
|
29727
|
+
img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg2);
|
|
29728
|
+
});
|
|
29729
|
+
}
|
|
29730
|
+
function canvasToJpegBlob(canvas) {
|
|
29731
|
+
return new Promise((resolve) => {
|
|
29732
|
+
canvas.toBlob((out) => resolve(out), "image/jpeg", 0.82);
|
|
29733
|
+
});
|
|
29734
|
+
}
|
|
29735
|
+
async function cropBlob(blob2, crop, scale) {
|
|
29736
|
+
try {
|
|
29737
|
+
if (typeof createImageBitmap !== "function" || typeof document === "undefined") return null;
|
|
29738
|
+
const bitmap = await createImageBitmap(blob2);
|
|
29739
|
+
const rect = computeCropRect(crop, scale, bitmap.width, bitmap.height);
|
|
29740
|
+
if (!rect) {
|
|
29741
|
+
bitmap.close?.();
|
|
29742
|
+
return null;
|
|
29743
|
+
}
|
|
29744
|
+
const canvas = document.createElement("canvas");
|
|
29745
|
+
canvas.width = rect.sw;
|
|
29746
|
+
canvas.height = rect.sh;
|
|
29747
|
+
const ctx = canvas.getContext("2d");
|
|
29748
|
+
if (!ctx) {
|
|
29749
|
+
bitmap.close?.();
|
|
29750
|
+
return null;
|
|
29751
|
+
}
|
|
29752
|
+
ctx.drawImage(bitmap, rect.sx, rect.sy, rect.sw, rect.sh, 0, 0, rect.sw, rect.sh);
|
|
29753
|
+
bitmap.close?.();
|
|
29754
|
+
return await canvasToJpegBlob(canvas);
|
|
29755
|
+
} catch (err) {
|
|
29756
|
+
console.warn("[lens] screenshot crop failed:", err);
|
|
29757
|
+
return null;
|
|
29758
|
+
}
|
|
29759
|
+
}
|
|
29503
29760
|
function parseBrowser(ua) {
|
|
29504
29761
|
if (!ua) return void 0;
|
|
29505
29762
|
const checks = [
|
|
@@ -29764,7 +30021,11 @@ var LensClient = class {
|
|
|
29764
30021
|
const wantErrors = include.errors !== false;
|
|
29765
30022
|
const wantScreenshot = include.screenshot !== false;
|
|
29766
30023
|
const wantDom = include.dom !== false;
|
|
29767
|
-
const screenshotPromise = this.screenshotEnabled && wantScreenshot ? captureScreenshot(
|
|
30024
|
+
const screenshotPromise = this.screenshotEnabled && wantScreenshot ? captureScreenshot(
|
|
30025
|
+
input2.excludeFromScreenshot,
|
|
30026
|
+
input2.screenshotCrop,
|
|
30027
|
+
input2.annotationComposite
|
|
30028
|
+
) : Promise.resolve(null);
|
|
29768
30029
|
const replayEvents = wantReplay ? this.replay?.snapshot() ?? [] : [];
|
|
29769
30030
|
const errorEntries = this.errorBuffer.drain();
|
|
29770
30031
|
const consoleTail = this.consoleBuffer.tail(CONSOLE_TAIL_SIZE);
|
|
@@ -29975,170 +30236,933 @@ function el(tag, props = {}, children = []) {
|
|
|
29975
30236
|
function clear(node2) {
|
|
29976
30237
|
while (node2.firstChild) node2.removeChild(node2.firstChild);
|
|
29977
30238
|
}
|
|
29978
|
-
var
|
|
30239
|
+
var THIN_MIN_DIST = 3;
|
|
30240
|
+
var MAX_POINTS = 2e3;
|
|
30241
|
+
var HIGHLIGHTER_WIDTH = 14;
|
|
30242
|
+
var STEP_RADIUS = 12;
|
|
30243
|
+
function distance(a, b) {
|
|
30244
|
+
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
30245
|
+
}
|
|
30246
|
+
function shouldKeepPoint(last, candidate, minDist = THIN_MIN_DIST) {
|
|
30247
|
+
return distance(last, candidate) >= minDist;
|
|
30248
|
+
}
|
|
30249
|
+
function perpendicularDistance(p, a, b) {
|
|
30250
|
+
const dx = b.x - a.x;
|
|
30251
|
+
const dy = b.y - a.y;
|
|
30252
|
+
const len2 = dx * dx + dy * dy;
|
|
30253
|
+
if (len2 === 0) return distance(p, a);
|
|
30254
|
+
let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2;
|
|
30255
|
+
t = Math.max(0, Math.min(1, t));
|
|
30256
|
+
return distance(p, { x: a.x + t * dx, y: a.y + t * dy });
|
|
30257
|
+
}
|
|
30258
|
+
function simplifyPoints(points, tolerance = 1) {
|
|
30259
|
+
if (points.length <= 2 || tolerance <= 0) return points.map((p) => ({ ...p }));
|
|
30260
|
+
const first = points[0];
|
|
30261
|
+
const last = points[points.length - 1];
|
|
30262
|
+
let maxDist = 0;
|
|
30263
|
+
let index2 = 0;
|
|
30264
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
30265
|
+
const d = perpendicularDistance(points[i], first, last);
|
|
30266
|
+
if (d > maxDist) {
|
|
30267
|
+
maxDist = d;
|
|
30268
|
+
index2 = i;
|
|
30269
|
+
}
|
|
30270
|
+
}
|
|
30271
|
+
if (maxDist > tolerance) {
|
|
30272
|
+
const left = simplifyPoints(points.slice(0, index2 + 1), tolerance);
|
|
30273
|
+
const right = simplifyPoints(points.slice(index2), tolerance);
|
|
30274
|
+
return left.slice(0, -1).concat(right);
|
|
30275
|
+
}
|
|
30276
|
+
return [{ ...first }, { ...last }];
|
|
30277
|
+
}
|
|
30278
|
+
function arrowheadPolygon(x1, y1, x2, y2, size = 14) {
|
|
30279
|
+
const angle = Math.atan2(y2 - y1, x2 - x1);
|
|
30280
|
+
const spread = Math.PI / 7;
|
|
30281
|
+
return [
|
|
30282
|
+
{ x: x2, y: y2 },
|
|
30283
|
+
{ x: x2 - size * Math.cos(angle - spread), y: y2 - size * Math.sin(angle - spread) },
|
|
30284
|
+
{ x: x2 - size * Math.cos(angle + spread), y: y2 - size * Math.sin(angle + spread) }
|
|
30285
|
+
];
|
|
30286
|
+
}
|
|
30287
|
+
function boundsOfAnnotation(a) {
|
|
30288
|
+
switch (a.kind) {
|
|
30289
|
+
case "rect":
|
|
30290
|
+
case "ellipse":
|
|
30291
|
+
case "blur": {
|
|
30292
|
+
const x = Math.min(a.x, a.x + a.width);
|
|
30293
|
+
const y = Math.min(a.y, a.y + a.height);
|
|
30294
|
+
return { x, y, width: Math.abs(a.width), height: Math.abs(a.height) };
|
|
30295
|
+
}
|
|
30296
|
+
case "arrow":
|
|
30297
|
+
case "line": {
|
|
30298
|
+
const x = Math.min(a.x1, a.x2);
|
|
30299
|
+
const y = Math.min(a.y1, a.y2);
|
|
30300
|
+
return { x, y, width: Math.abs(a.x2 - a.x1), height: Math.abs(a.y2 - a.y1) };
|
|
30301
|
+
}
|
|
30302
|
+
case "pencil":
|
|
30303
|
+
case "highlighter": {
|
|
30304
|
+
if (a.points.length === 0) return null;
|
|
30305
|
+
let minX = Infinity;
|
|
30306
|
+
let minY = Infinity;
|
|
30307
|
+
let maxX = -Infinity;
|
|
30308
|
+
let maxY = -Infinity;
|
|
30309
|
+
for (const p of a.points) {
|
|
30310
|
+
minX = Math.min(minX, p.x);
|
|
30311
|
+
minY = Math.min(minY, p.y);
|
|
30312
|
+
maxX = Math.max(maxX, p.x);
|
|
30313
|
+
maxY = Math.max(maxY, p.y);
|
|
30314
|
+
}
|
|
30315
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
30316
|
+
}
|
|
30317
|
+
case "text": {
|
|
30318
|
+
const fontSize = a.fontSize ?? 16;
|
|
30319
|
+
const width = Math.max(1, a.text.length * fontSize * 0.6);
|
|
30320
|
+
return { x: a.x, y: a.y, width, height: fontSize * 1.2 };
|
|
30321
|
+
}
|
|
30322
|
+
case "step": {
|
|
30323
|
+
return {
|
|
30324
|
+
x: a.x - STEP_RADIUS,
|
|
30325
|
+
y: a.y - STEP_RADIUS,
|
|
30326
|
+
width: STEP_RADIUS * 2,
|
|
30327
|
+
height: STEP_RADIUS * 2
|
|
30328
|
+
};
|
|
30329
|
+
}
|
|
30330
|
+
}
|
|
30331
|
+
}
|
|
30332
|
+
function unionBounds(list) {
|
|
30333
|
+
if (list.length === 0) return null;
|
|
30334
|
+
let minX = Infinity;
|
|
30335
|
+
let minY = Infinity;
|
|
30336
|
+
let maxX = -Infinity;
|
|
30337
|
+
let maxY = -Infinity;
|
|
30338
|
+
for (const b of list) {
|
|
30339
|
+
minX = Math.min(minX, b.x);
|
|
30340
|
+
minY = Math.min(minY, b.y);
|
|
30341
|
+
maxX = Math.max(maxX, b.x + b.width);
|
|
30342
|
+
maxY = Math.max(maxY, b.y + b.height);
|
|
30343
|
+
}
|
|
30344
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
30345
|
+
}
|
|
30346
|
+
function padAndClampBounds(b, padding, maxWidth, maxHeight) {
|
|
30347
|
+
const x = Math.max(0, b.x - padding);
|
|
30348
|
+
const y = Math.max(0, b.y - padding);
|
|
30349
|
+
const right = Math.min(maxWidth, b.x + b.width + padding);
|
|
30350
|
+
const bottom = Math.min(maxHeight, b.y + b.height + padding);
|
|
30351
|
+
return { x, y, width: Math.max(0, right - x), height: Math.max(0, bottom - y) };
|
|
30352
|
+
}
|
|
30353
|
+
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>`;
|
|
30354
|
+
var iconEye = svg(
|
|
30355
|
+
'<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"/>'
|
|
30356
|
+
);
|
|
30357
|
+
var iconCrosshair = svg(
|
|
30358
|
+
'<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"/>'
|
|
30359
|
+
);
|
|
30360
|
+
var iconClose = svg('<line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/>');
|
|
30361
|
+
var iconMark = svg(
|
|
30362
|
+
'<rect x="3" y="3" width="18" height="18" rx="2" stroke-dasharray="4 3"/>'
|
|
30363
|
+
);
|
|
30364
|
+
var iconCopy = svg(
|
|
30365
|
+
'<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/>'
|
|
30366
|
+
);
|
|
30367
|
+
var iconCheck = svg('<polyline points="20 6 9 17 4 12"/>');
|
|
30368
|
+
var iconSend = svg('<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>');
|
|
30369
|
+
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>`;
|
|
30370
|
+
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>`;
|
|
30371
|
+
var iconRectTool = svg('<rect x="4" y="6" width="16" height="12" rx="1"/>');
|
|
30372
|
+
var iconEllipseTool = svg('<ellipse cx="12" cy="12" rx="8" ry="6"/>');
|
|
30373
|
+
var iconArrowTool = svg('<line x1="5" y1="19" x2="18" y2="6"/><polyline points="18 12 18 6 12 6"/>');
|
|
30374
|
+
var iconLineTool = svg('<line x1="5" y1="19" x2="19" y2="5"/>');
|
|
30375
|
+
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"/>');
|
|
30376
|
+
var iconHighlighter = svg(
|
|
30377
|
+
'<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"/>'
|
|
30378
|
+
);
|
|
30379
|
+
var iconText = svg(
|
|
30380
|
+
'<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"/>'
|
|
30381
|
+
);
|
|
30382
|
+
var iconStepTool = svg('<circle cx="12" cy="12" r="9"/><path d="M11.2 9 13 8v8"/>');
|
|
30383
|
+
var iconBlur = svg('<path d="M12 3s6 6.4 6 10a6 6 0 0 1-12 0c0-3.6 6-10 6-10Z"/>');
|
|
30384
|
+
var iconUndo = svg('<path d="M9 14 4 9l5-5"/><path d="M4 9h11a5 5 0 0 1 0 10h-1"/>');
|
|
30385
|
+
var iconTrash = svg(
|
|
30386
|
+
'<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"/>'
|
|
30387
|
+
);
|
|
30388
|
+
var iconAnnotate = iconPencil;
|
|
30389
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
30390
|
+
var FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
|
|
30391
|
+
var MIN_DRAG = 6;
|
|
30392
|
+
var TOOLS = [
|
|
30393
|
+
{ tool: "rect", icon: iconRectTool, label: "Rectangle" },
|
|
30394
|
+
{ tool: "ellipse", icon: iconEllipseTool, label: "Ellipse" },
|
|
30395
|
+
{ tool: "arrow", icon: iconArrowTool, label: "Arrow" },
|
|
30396
|
+
{ tool: "line", icon: iconLineTool, label: "Line" },
|
|
30397
|
+
{ tool: "pencil", icon: iconPencil, label: "Pencil" },
|
|
30398
|
+
{ tool: "highlighter", icon: iconHighlighter, label: "Highlighter" },
|
|
30399
|
+
{ tool: "text", icon: iconText, label: "Text" },
|
|
30400
|
+
{ tool: "step", icon: iconStepTool, label: "Step number" },
|
|
30401
|
+
{ tool: "blur", icon: iconBlur, label: "Blur / redact" }
|
|
30402
|
+
];
|
|
30403
|
+
var COLORS = ["#ef4444", "#f59e0b", "#22c55e", "#3b82f6", "#8b5cf6", "#111827"];
|
|
30404
|
+
var WIDTHS = [
|
|
30405
|
+
{ label: "S", value: 2 },
|
|
30406
|
+
{ label: "M", value: 3.5 },
|
|
30407
|
+
{ label: "L", value: 6 }
|
|
30408
|
+
];
|
|
30409
|
+
var AnnotationEditor = class {
|
|
29979
30410
|
constructor(host2) {
|
|
29980
30411
|
this.host = host2;
|
|
29981
30412
|
}
|
|
29982
30413
|
host;
|
|
29983
|
-
|
|
29984
|
-
|
|
29985
|
-
|
|
29986
|
-
|
|
29987
|
-
|
|
29988
|
-
|
|
29989
|
-
dragging = false;
|
|
30414
|
+
svg = null;
|
|
30415
|
+
bg = null;
|
|
30416
|
+
toolbar = null;
|
|
30417
|
+
textInputEl = null;
|
|
30418
|
+
textAnchor = null;
|
|
30419
|
+
textColor = COLORS[0];
|
|
29990
30420
|
annotations = [];
|
|
29991
|
-
|
|
30421
|
+
markNodes = [];
|
|
30422
|
+
drag = null;
|
|
30423
|
+
active = false;
|
|
29992
30424
|
sessionBase = 0;
|
|
30425
|
+
stepCounter = 0;
|
|
30426
|
+
idSeq = 0;
|
|
29993
30427
|
onExit = null;
|
|
30428
|
+
tool = "rect";
|
|
30429
|
+
color = COLORS[0];
|
|
30430
|
+
width = 3.5;
|
|
30431
|
+
docW = 0;
|
|
30432
|
+
docH = 0;
|
|
30433
|
+
resizeObserver = null;
|
|
30434
|
+
toolBtns = /* @__PURE__ */ new Map();
|
|
30435
|
+
colorBtns = /* @__PURE__ */ new Map();
|
|
30436
|
+
widthBtns = /* @__PURE__ */ new Map();
|
|
30437
|
+
undoBtn = null;
|
|
30438
|
+
clearBtn = null;
|
|
29994
30439
|
get isActive() {
|
|
29995
30440
|
return this.active;
|
|
29996
30441
|
}
|
|
29997
30442
|
get count() {
|
|
29998
30443
|
return this.annotations.length;
|
|
29999
30444
|
}
|
|
30445
|
+
/** Deep-copied document-coord annotations per the schema contract. */
|
|
30000
30446
|
list() {
|
|
30001
|
-
return this.annotations.map((a) =>
|
|
30447
|
+
return this.annotations.map((a) => {
|
|
30448
|
+
const copy2 = { ...a };
|
|
30449
|
+
if ("points" in copy2 && Array.isArray(copy2.points)) {
|
|
30450
|
+
copy2.points = copy2.points.map((p) => ({ ...p }));
|
|
30451
|
+
}
|
|
30452
|
+
if (copy2.kind === "rect" && copy2.viewport) copy2.viewport = { ...copy2.viewport };
|
|
30453
|
+
return copy2;
|
|
30454
|
+
});
|
|
30002
30455
|
}
|
|
30456
|
+
/** Remove every mark. Tears the layer down when not actively drawing. */
|
|
30003
30457
|
clear() {
|
|
30458
|
+
for (const node2 of this.markNodes) node2.remove();
|
|
30459
|
+
this.markNodes = [];
|
|
30004
30460
|
this.annotations = [];
|
|
30005
|
-
|
|
30006
|
-
this.
|
|
30461
|
+
this.stepCounter = 0;
|
|
30462
|
+
if (!this.active) this.teardownLayer();
|
|
30463
|
+
this.syncActionButtons();
|
|
30007
30464
|
}
|
|
30008
30465
|
start(onExit) {
|
|
30009
30466
|
if (this.active) return;
|
|
30467
|
+
this.ensureLayer();
|
|
30468
|
+
const svg2 = this.svg;
|
|
30010
30469
|
this.active = true;
|
|
30011
30470
|
this.onExit = onExit;
|
|
30012
30471
|
this.sessionBase = this.annotations.length;
|
|
30013
|
-
this.
|
|
30014
|
-
this.
|
|
30015
|
-
this.
|
|
30016
|
-
|
|
30472
|
+
this.stepCounter = 0;
|
|
30473
|
+
this.sizeLayer();
|
|
30474
|
+
this.updateTransform();
|
|
30475
|
+
svg2.style.pointerEvents = "auto";
|
|
30476
|
+
svg2.style.touchAction = "none";
|
|
30477
|
+
svg2.style.cursor = "crosshair";
|
|
30478
|
+
svg2.addEventListener("pointerdown", this.onPointerDown);
|
|
30479
|
+
this.buildToolbar();
|
|
30480
|
+
window.addEventListener("keydown", this.onKey, true);
|
|
30481
|
+
}
|
|
30482
|
+
/** Done — keep this session's marks, exit draw mode. */
|
|
30483
|
+
exit() {
|
|
30484
|
+
if (!this.active) return;
|
|
30485
|
+
this.active = false;
|
|
30486
|
+
this.commitTextInput();
|
|
30487
|
+
if (this.drag) {
|
|
30488
|
+
this.drag.node.remove();
|
|
30489
|
+
this.detachDragListeners();
|
|
30490
|
+
this.drag = null;
|
|
30491
|
+
}
|
|
30492
|
+
const svg2 = this.svg;
|
|
30493
|
+
if (svg2) {
|
|
30494
|
+
svg2.removeEventListener("pointerdown", this.onPointerDown);
|
|
30495
|
+
svg2.style.pointerEvents = "none";
|
|
30496
|
+
svg2.style.touchAction = "";
|
|
30497
|
+
svg2.style.cursor = "";
|
|
30498
|
+
}
|
|
30499
|
+
window.removeEventListener("keydown", this.onKey, true);
|
|
30500
|
+
this.removeToolbar();
|
|
30501
|
+
const cb = this.onExit;
|
|
30502
|
+
this.onExit = null;
|
|
30503
|
+
cb?.();
|
|
30504
|
+
}
|
|
30505
|
+
/** Cancel — discard marks drawn during this session, then exit. */
|
|
30506
|
+
cancel() {
|
|
30507
|
+
this.cancelTextInput();
|
|
30508
|
+
const removed = this.markNodes.splice(this.sessionBase);
|
|
30509
|
+
for (const node2 of removed) node2.remove();
|
|
30510
|
+
this.annotations.splice(this.sessionBase);
|
|
30511
|
+
this.exit();
|
|
30512
|
+
}
|
|
30513
|
+
undo() {
|
|
30514
|
+
const node2 = this.markNodes.pop();
|
|
30515
|
+
if (!node2) return;
|
|
30516
|
+
node2.remove();
|
|
30517
|
+
this.annotations.pop();
|
|
30518
|
+
this.syncActionButtons();
|
|
30519
|
+
}
|
|
30520
|
+
// --------------------------------------------------------------------------
|
|
30521
|
+
// Composite / crop
|
|
30522
|
+
// --------------------------------------------------------------------------
|
|
30523
|
+
/**
|
|
30524
|
+
* Complete standalone SVG of every NON-blur mark, plus blur regions returned
|
|
30525
|
+
* separately. `null` when there are no marks.
|
|
30526
|
+
*/
|
|
30527
|
+
getComposite() {
|
|
30528
|
+
if (this.annotations.length === 0 || !this.svg) return null;
|
|
30529
|
+
this.sizeLayer();
|
|
30530
|
+
const docWidth = this.docW;
|
|
30531
|
+
const docHeight = this.docH;
|
|
30532
|
+
const clone2 = this.svg.cloneNode(true);
|
|
30533
|
+
clone2.removeAttribute("style");
|
|
30534
|
+
clone2.removeAttribute("class");
|
|
30535
|
+
clone2.querySelectorAll("[data-lens-chrome],[data-lens-draft],[data-lens-blur]").forEach((n2) => n2.remove());
|
|
30536
|
+
clone2.querySelector("defs")?.remove();
|
|
30537
|
+
clone2.setAttribute("xmlns", SVG_NS);
|
|
30538
|
+
clone2.setAttribute("viewBox", `0 0 ${docWidth} ${docHeight}`);
|
|
30539
|
+
clone2.setAttribute("width", String(docWidth));
|
|
30540
|
+
clone2.setAttribute("height", String(docHeight));
|
|
30541
|
+
const svgMarkup = new XMLSerializer().serializeToString(clone2);
|
|
30542
|
+
const blurRegions = this.annotations.filter((a) => a.kind === "blur").map((a) => ({ x: a.x, y: a.y, width: a.width, height: a.height }));
|
|
30543
|
+
return { svgMarkup, docWidth, docHeight, blurRegions };
|
|
30544
|
+
}
|
|
30545
|
+
/** Union bbox of all marks + 24px padding, clamped to the document. */
|
|
30546
|
+
getCropBounds() {
|
|
30547
|
+
if (this.annotations.length === 0) return void 0;
|
|
30548
|
+
const boxes = [];
|
|
30549
|
+
for (let i = 0; i < this.annotations.length; i++) {
|
|
30550
|
+
const a = this.annotations[i];
|
|
30551
|
+
const b = a.kind === "text" ? this.textBounds(i) : boundsOfAnnotation(a);
|
|
30552
|
+
if (b) boxes.push(b);
|
|
30553
|
+
}
|
|
30554
|
+
const u = unionBounds(boxes);
|
|
30555
|
+
if (!u) return void 0;
|
|
30556
|
+
this.sizeLayer();
|
|
30557
|
+
return padAndClampBounds(u, 24, this.docW, this.docH);
|
|
30558
|
+
}
|
|
30559
|
+
/** Prefer a live getBBox() for text; fall back to the geometric estimate. */
|
|
30560
|
+
textBounds(index2) {
|
|
30561
|
+
const node2 = this.markNodes[index2];
|
|
30562
|
+
const annotation = this.annotations[index2];
|
|
30563
|
+
if (node2 && typeof node2.getBBox === "function") {
|
|
30564
|
+
try {
|
|
30565
|
+
const box = node2.getBBox();
|
|
30566
|
+
if (box.width > 0 && box.height > 0) {
|
|
30567
|
+
return { x: box.x, y: box.y, width: box.width, height: box.height };
|
|
30568
|
+
}
|
|
30569
|
+
} catch {
|
|
30570
|
+
}
|
|
30571
|
+
}
|
|
30572
|
+
return boundsOfAnnotation(annotation);
|
|
30573
|
+
}
|
|
30574
|
+
// --------------------------------------------------------------------------
|
|
30575
|
+
// Layer lifecycle
|
|
30576
|
+
// --------------------------------------------------------------------------
|
|
30577
|
+
ensureLayer() {
|
|
30578
|
+
if (this.svg) return;
|
|
30579
|
+
const svg2 = document.createElementNS(SVG_NS, "svg");
|
|
30580
|
+
svg2.setAttribute("class", "lens-annotate-svg");
|
|
30581
|
+
svg2.setAttribute("xmlns", SVG_NS);
|
|
30582
|
+
const defs = document.createElementNS(SVG_NS, "defs");
|
|
30583
|
+
defs.setAttribute("data-lens-chrome", "1");
|
|
30584
|
+
const pattern = document.createElementNS(SVG_NS, "pattern");
|
|
30585
|
+
pattern.setAttribute("id", "lens-blur-hatch");
|
|
30586
|
+
pattern.setAttribute("width", "8");
|
|
30587
|
+
pattern.setAttribute("height", "8");
|
|
30588
|
+
pattern.setAttribute("patternUnits", "userSpaceOnUse");
|
|
30589
|
+
pattern.setAttribute("patternTransform", "rotate(45)");
|
|
30590
|
+
const patternBase = document.createElementNS(SVG_NS, "rect");
|
|
30591
|
+
patternBase.setAttribute("width", "8");
|
|
30592
|
+
patternBase.setAttribute("height", "8");
|
|
30593
|
+
patternBase.setAttribute("fill", "rgba(17,24,39,0.12)");
|
|
30594
|
+
const patternLine = document.createElementNS(SVG_NS, "line");
|
|
30595
|
+
patternLine.setAttribute("x1", "0");
|
|
30596
|
+
patternLine.setAttribute("y1", "0");
|
|
30597
|
+
patternLine.setAttribute("x2", "0");
|
|
30598
|
+
patternLine.setAttribute("y2", "8");
|
|
30599
|
+
patternLine.setAttribute("stroke", "rgba(17,24,39,0.55)");
|
|
30600
|
+
patternLine.setAttribute("stroke-width", "4");
|
|
30601
|
+
pattern.append(patternBase, patternLine);
|
|
30602
|
+
defs.append(pattern);
|
|
30603
|
+
svg2.append(defs);
|
|
30604
|
+
const bg = document.createElementNS(SVG_NS, "rect");
|
|
30605
|
+
bg.setAttribute("data-lens-chrome", "1");
|
|
30606
|
+
bg.setAttribute("x", "0");
|
|
30607
|
+
bg.setAttribute("y", "0");
|
|
30608
|
+
bg.setAttribute("fill", "transparent");
|
|
30609
|
+
bg.style.pointerEvents = "all";
|
|
30610
|
+
svg2.append(bg);
|
|
30611
|
+
this.svg = svg2;
|
|
30612
|
+
this.bg = bg;
|
|
30613
|
+
this.host.append(svg2);
|
|
30614
|
+
window.addEventListener("scroll", this.onScroll, { passive: true, capture: true });
|
|
30615
|
+
window.addEventListener("resize", this.onResize);
|
|
30616
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
30617
|
+
this.resizeObserver = new ResizeObserver(this.onResize);
|
|
30618
|
+
this.resizeObserver.observe(document.documentElement);
|
|
30619
|
+
}
|
|
30620
|
+
this.sizeLayer();
|
|
30621
|
+
this.updateTransform();
|
|
30622
|
+
}
|
|
30623
|
+
teardownLayer() {
|
|
30624
|
+
window.removeEventListener("scroll", this.onScroll, true);
|
|
30625
|
+
window.removeEventListener("resize", this.onResize);
|
|
30626
|
+
this.resizeObserver?.disconnect();
|
|
30627
|
+
this.resizeObserver = null;
|
|
30628
|
+
this.svg?.remove();
|
|
30629
|
+
this.svg = null;
|
|
30630
|
+
this.bg = null;
|
|
30631
|
+
}
|
|
30632
|
+
docSize() {
|
|
30633
|
+
const d = document.documentElement;
|
|
30634
|
+
const b = document.body;
|
|
30635
|
+
const width = Math.max(d.scrollWidth, b?.scrollWidth ?? 0, d.clientWidth);
|
|
30636
|
+
const height = Math.max(d.scrollHeight, b?.scrollHeight ?? 0, d.clientHeight);
|
|
30637
|
+
return { width, height };
|
|
30638
|
+
}
|
|
30639
|
+
sizeLayer() {
|
|
30640
|
+
if (!this.svg) return;
|
|
30641
|
+
const { width, height } = this.docSize();
|
|
30642
|
+
this.docW = width;
|
|
30643
|
+
this.docH = height;
|
|
30644
|
+
this.svg.setAttribute("width", String(width));
|
|
30645
|
+
this.svg.setAttribute("height", String(height));
|
|
30646
|
+
this.svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
30647
|
+
this.svg.style.width = `${width}px`;
|
|
30648
|
+
this.svg.style.height = `${height}px`;
|
|
30649
|
+
if (this.bg) {
|
|
30650
|
+
this.bg.setAttribute("width", String(width));
|
|
30651
|
+
this.bg.setAttribute("height", String(height));
|
|
30652
|
+
}
|
|
30653
|
+
}
|
|
30654
|
+
updateTransform() {
|
|
30655
|
+
if (!this.svg) return;
|
|
30656
|
+
const scrollX = window.scrollX || window.pageXOffset || 0;
|
|
30657
|
+
const scrollY = window.scrollY || window.pageYOffset || 0;
|
|
30658
|
+
this.svg.style.left = `${-scrollX}px`;
|
|
30659
|
+
this.svg.style.top = `${-scrollY}px`;
|
|
30660
|
+
}
|
|
30661
|
+
onScroll = () => this.updateTransform();
|
|
30662
|
+
onResize = () => {
|
|
30663
|
+
this.sizeLayer();
|
|
30664
|
+
this.updateTransform();
|
|
30665
|
+
};
|
|
30666
|
+
// --------------------------------------------------------------------------
|
|
30667
|
+
// Toolbar
|
|
30668
|
+
// --------------------------------------------------------------------------
|
|
30669
|
+
buildToolbar() {
|
|
30670
|
+
this.toolBtns.clear();
|
|
30671
|
+
this.colorBtns.clear();
|
|
30672
|
+
this.widthBtns.clear();
|
|
30673
|
+
const toolGroup = el("div", { class: "lens-tool-group" });
|
|
30674
|
+
for (const { tool, icon, label } of TOOLS) {
|
|
30675
|
+
const btn = el("button", {
|
|
30676
|
+
class: "lens-tool-btn",
|
|
30677
|
+
ariaLabel: label,
|
|
30678
|
+
title: label,
|
|
30679
|
+
html: icon,
|
|
30680
|
+
attrs: { type: "button", "aria-pressed": String(tool === this.tool) },
|
|
30681
|
+
on: { click: () => this.selectTool(tool) }
|
|
30682
|
+
});
|
|
30683
|
+
this.toolBtns.set(tool, btn);
|
|
30684
|
+
toolGroup.append(btn);
|
|
30685
|
+
}
|
|
30686
|
+
const colorGroup = el("div", { class: "lens-tool-group lens-swatches" });
|
|
30687
|
+
for (const c of COLORS) {
|
|
30688
|
+
const btn = el("button", {
|
|
30689
|
+
class: "lens-swatch",
|
|
30690
|
+
ariaLabel: `Color ${c}`,
|
|
30691
|
+
title: c,
|
|
30692
|
+
attrs: { type: "button", "aria-pressed": String(c === this.color) },
|
|
30693
|
+
style: { background: c },
|
|
30694
|
+
on: { click: () => this.selectColor(c) }
|
|
30695
|
+
});
|
|
30696
|
+
this.colorBtns.set(c, btn);
|
|
30697
|
+
colorGroup.append(btn);
|
|
30698
|
+
}
|
|
30699
|
+
const widthGroup = el("div", { class: "lens-tool-group" });
|
|
30700
|
+
for (const { label, value } of WIDTHS) {
|
|
30701
|
+
const btn = el("button", {
|
|
30702
|
+
class: "lens-width-btn",
|
|
30703
|
+
text: label,
|
|
30704
|
+
ariaLabel: `Stroke width ${label}`,
|
|
30705
|
+
title: `Stroke ${label} (${value}px)`,
|
|
30706
|
+
attrs: { type: "button", "aria-pressed": String(value === this.width) },
|
|
30707
|
+
on: { click: () => this.selectWidth(value) }
|
|
30708
|
+
});
|
|
30709
|
+
this.widthBtns.set(value, btn);
|
|
30710
|
+
widthGroup.append(btn);
|
|
30711
|
+
}
|
|
30712
|
+
this.undoBtn = el("button", {
|
|
30713
|
+
class: "lens-tool-btn",
|
|
30714
|
+
ariaLabel: "Undo",
|
|
30715
|
+
title: "Undo",
|
|
30716
|
+
html: iconUndo,
|
|
30717
|
+
attrs: { type: "button" },
|
|
30718
|
+
on: { click: () => this.undo() }
|
|
30719
|
+
});
|
|
30720
|
+
this.clearBtn = el("button", {
|
|
30721
|
+
class: "lens-tool-btn",
|
|
30722
|
+
ariaLabel: "Clear all",
|
|
30723
|
+
title: "Clear all",
|
|
30724
|
+
html: iconTrash,
|
|
30725
|
+
attrs: { type: "button" },
|
|
30726
|
+
on: { click: () => this.clear() }
|
|
30727
|
+
});
|
|
30728
|
+
const editGroup = el("div", { class: "lens-tool-group" }, [this.undoBtn, this.clearBtn]);
|
|
30729
|
+
const doneGroup = el("div", { class: "lens-tool-group" }, [
|
|
30017
30730
|
el("button", {
|
|
30018
30731
|
class: "lens-pick-chip__btn",
|
|
30019
30732
|
text: "Cancel",
|
|
30733
|
+
attrs: { type: "button" },
|
|
30020
30734
|
on: { click: () => this.cancel() }
|
|
30021
30735
|
}),
|
|
30022
30736
|
el("button", {
|
|
30023
30737
|
class: "lens-pick-chip__btn lens-pick-chip__btn--primary",
|
|
30024
30738
|
text: "Done",
|
|
30739
|
+
attrs: { type: "button" },
|
|
30025
30740
|
on: { click: () => this.exit() }
|
|
30026
30741
|
})
|
|
30027
30742
|
]);
|
|
30028
|
-
this.
|
|
30029
|
-
|
|
30030
|
-
|
|
30031
|
-
|
|
30032
|
-
|
|
30033
|
-
|
|
30034
|
-
|
|
30035
|
-
|
|
30036
|
-
|
|
30743
|
+
this.toolbar = el(
|
|
30744
|
+
"div",
|
|
30745
|
+
{ class: "lens-annotate-toolbar", attrs: { role: "toolbar", "aria-label": "Annotation tools" } },
|
|
30746
|
+
[
|
|
30747
|
+
toolGroup,
|
|
30748
|
+
el("span", { class: "lens-tool-sep" }),
|
|
30749
|
+
colorGroup,
|
|
30750
|
+
el("span", { class: "lens-tool-sep" }),
|
|
30751
|
+
widthGroup,
|
|
30752
|
+
el("span", { class: "lens-tool-sep" }),
|
|
30753
|
+
editGroup,
|
|
30754
|
+
el("span", { class: "lens-tool-sep" }),
|
|
30755
|
+
doneGroup
|
|
30756
|
+
]
|
|
30757
|
+
);
|
|
30758
|
+
this.host.append(this.toolbar);
|
|
30759
|
+
this.syncActionButtons();
|
|
30760
|
+
}
|
|
30761
|
+
removeToolbar() {
|
|
30762
|
+
this.toolbar?.remove();
|
|
30763
|
+
this.toolbar = null;
|
|
30764
|
+
this.toolBtns.clear();
|
|
30765
|
+
this.colorBtns.clear();
|
|
30766
|
+
this.widthBtns.clear();
|
|
30767
|
+
this.undoBtn = null;
|
|
30768
|
+
this.clearBtn = null;
|
|
30769
|
+
}
|
|
30770
|
+
selectTool(tool) {
|
|
30771
|
+
this.commitTextInput();
|
|
30772
|
+
this.tool = tool;
|
|
30773
|
+
for (const [t, btn] of this.toolBtns) {
|
|
30774
|
+
const on2 = t === tool;
|
|
30775
|
+
btn.classList.toggle("is-active", on2);
|
|
30776
|
+
btn.setAttribute("aria-pressed", String(on2));
|
|
30777
|
+
}
|
|
30778
|
+
if (this.svg) this.svg.style.cursor = tool === "text" ? "text" : "crosshair";
|
|
30779
|
+
}
|
|
30780
|
+
selectColor(color) {
|
|
30781
|
+
this.color = color;
|
|
30782
|
+
for (const [c, btn] of this.colorBtns) {
|
|
30783
|
+
const on2 = c === color;
|
|
30784
|
+
btn.classList.toggle("is-active", on2);
|
|
30785
|
+
btn.setAttribute("aria-pressed", String(on2));
|
|
30786
|
+
}
|
|
30787
|
+
}
|
|
30788
|
+
selectWidth(value) {
|
|
30789
|
+
this.width = value;
|
|
30790
|
+
for (const [v, btn] of this.widthBtns) {
|
|
30791
|
+
const on2 = v === value;
|
|
30792
|
+
btn.classList.toggle("is-active", on2);
|
|
30793
|
+
btn.setAttribute("aria-pressed", String(on2));
|
|
30794
|
+
}
|
|
30795
|
+
}
|
|
30796
|
+
syncActionButtons() {
|
|
30797
|
+
const empty = this.markNodes.length === 0;
|
|
30798
|
+
if (this.undoBtn) this.undoBtn.disabled = empty;
|
|
30799
|
+
if (this.clearBtn) this.clearBtn.disabled = empty;
|
|
30800
|
+
}
|
|
30801
|
+
// --------------------------------------------------------------------------
|
|
30802
|
+
// Pointer / drawing
|
|
30803
|
+
// --------------------------------------------------------------------------
|
|
30804
|
+
toDoc(e) {
|
|
30805
|
+
return {
|
|
30806
|
+
x: e.clientX + (window.scrollX || window.pageXOffset || 0),
|
|
30807
|
+
y: e.clientY + (window.scrollY || window.pageYOffset || 0)
|
|
30808
|
+
};
|
|
30037
30809
|
}
|
|
30038
|
-
|
|
30810
|
+
onPointerDown = (e) => {
|
|
30039
30811
|
if (!this.active) return;
|
|
30040
|
-
|
|
30041
|
-
|
|
30042
|
-
|
|
30043
|
-
|
|
30044
|
-
|
|
30045
|
-
|
|
30046
|
-
|
|
30047
|
-
this.draft = null;
|
|
30812
|
+
if (e.button !== void 0 && e.button !== 0) return;
|
|
30813
|
+
e.preventDefault();
|
|
30814
|
+
const p = this.toDoc(e);
|
|
30815
|
+
const tool = this.tool;
|
|
30816
|
+
if (tool === "text") {
|
|
30817
|
+
this.openTextInput(e.clientX, e.clientY, p);
|
|
30818
|
+
return;
|
|
30048
30819
|
}
|
|
30049
|
-
if (
|
|
30050
|
-
this.
|
|
30051
|
-
|
|
30052
|
-
this.layer = null;
|
|
30820
|
+
if (tool === "step") {
|
|
30821
|
+
this.placeStep(p);
|
|
30822
|
+
return;
|
|
30053
30823
|
}
|
|
30054
|
-
|
|
30055
|
-
|
|
30056
|
-
|
|
30824
|
+
const color = this.color;
|
|
30825
|
+
const width = this.width;
|
|
30826
|
+
let drag;
|
|
30827
|
+
if (tool === "pencil" || tool === "highlighter") {
|
|
30828
|
+
const poly = this.svgEl("polyline", {
|
|
30829
|
+
"data-lens-draft": "1",
|
|
30830
|
+
fill: "none",
|
|
30831
|
+
stroke: color,
|
|
30832
|
+
"stroke-width": tool === "highlighter" ? HIGHLIGHTER_WIDTH : width,
|
|
30833
|
+
"stroke-linecap": "round",
|
|
30834
|
+
"stroke-linejoin": "round",
|
|
30835
|
+
...tool === "highlighter" ? { "stroke-opacity": "0.35" } : {},
|
|
30836
|
+
points: `${p.x},${p.y}`
|
|
30837
|
+
});
|
|
30838
|
+
this.svg.append(poly);
|
|
30839
|
+
drag = { tool, color, width, start: p, node: poly, points: [p], last: p };
|
|
30840
|
+
} else if (tool === "arrow") {
|
|
30841
|
+
const g = this.svgEl("g", { "data-lens-draft": "1" });
|
|
30842
|
+
const line = this.svgEl("line", {
|
|
30843
|
+
stroke: color,
|
|
30844
|
+
"stroke-width": width,
|
|
30845
|
+
"stroke-linecap": "round"
|
|
30846
|
+
});
|
|
30847
|
+
const head = this.svgEl("polygon", { fill: color });
|
|
30848
|
+
g.append(line, head);
|
|
30849
|
+
this.svg.append(g);
|
|
30850
|
+
drag = { tool, color, width, start: p, node: g, line, head };
|
|
30851
|
+
this.updateArrow(drag, p);
|
|
30852
|
+
} else {
|
|
30853
|
+
const node2 = this.createShapeNode(tool, color, width);
|
|
30854
|
+
this.svg.append(node2);
|
|
30855
|
+
drag = { tool, color, width, start: p, node: node2 };
|
|
30856
|
+
this.updateShape(drag, p);
|
|
30857
|
+
}
|
|
30858
|
+
this.drag = drag;
|
|
30859
|
+
window.addEventListener("pointermove", this.onPointerMove, true);
|
|
30860
|
+
window.addEventListener("pointerup", this.onPointerUp, true);
|
|
30861
|
+
};
|
|
30862
|
+
onPointerMove = (e) => {
|
|
30863
|
+
const d = this.drag;
|
|
30864
|
+
if (!d) return;
|
|
30865
|
+
const p = this.toDoc(e);
|
|
30866
|
+
if (d.tool === "pencil" || d.tool === "highlighter") {
|
|
30867
|
+
const pts = d.points;
|
|
30868
|
+
if (d.last && shouldKeepPoint(d.last, p) && pts.length < MAX_POINTS) {
|
|
30869
|
+
pts.push(p);
|
|
30870
|
+
d.last = p;
|
|
30871
|
+
d.node.setAttribute("points", this.pointsAttr(pts));
|
|
30872
|
+
}
|
|
30873
|
+
} else if (d.tool === "arrow") {
|
|
30874
|
+
this.updateArrow(d, p);
|
|
30875
|
+
} else {
|
|
30876
|
+
this.updateShape(d, p);
|
|
30877
|
+
}
|
|
30878
|
+
};
|
|
30879
|
+
onPointerUp = (e) => {
|
|
30880
|
+
const d = this.drag;
|
|
30881
|
+
this.detachDragListeners();
|
|
30882
|
+
this.drag = null;
|
|
30883
|
+
if (!d) return;
|
|
30884
|
+
const p = this.toDoc(e);
|
|
30885
|
+
let annotation = null;
|
|
30886
|
+
if (d.tool === "pencil" || d.tool === "highlighter") {
|
|
30887
|
+
let pts = d.points;
|
|
30888
|
+
if (pts.length >= 2) pts = simplifyPoints(pts, 1);
|
|
30889
|
+
if (pts.length < 2) {
|
|
30890
|
+
d.node.remove();
|
|
30891
|
+
return;
|
|
30892
|
+
}
|
|
30893
|
+
d.node.setAttribute("points", this.pointsAttr(pts));
|
|
30894
|
+
annotation = d.tool === "pencil" ? { kind: "pencil", points: pts, color: d.color, strokeWidth: d.width, id: this.nextId() } : {
|
|
30895
|
+
kind: "highlighter",
|
|
30896
|
+
points: pts,
|
|
30897
|
+
color: d.color,
|
|
30898
|
+
strokeWidth: HIGHLIGHTER_WIDTH,
|
|
30899
|
+
id: this.nextId()
|
|
30900
|
+
};
|
|
30901
|
+
} else if (d.tool === "arrow" || d.tool === "line") {
|
|
30902
|
+
if (distance(d.start, p) < MIN_DRAG) {
|
|
30903
|
+
d.node.remove();
|
|
30904
|
+
return;
|
|
30905
|
+
}
|
|
30906
|
+
if (d.tool === "arrow") {
|
|
30907
|
+
this.updateArrow(d, p);
|
|
30908
|
+
annotation = {
|
|
30909
|
+
kind: "arrow",
|
|
30910
|
+
x1: d.start.x,
|
|
30911
|
+
y1: d.start.y,
|
|
30912
|
+
x2: p.x,
|
|
30913
|
+
y2: p.y,
|
|
30914
|
+
color: d.color,
|
|
30915
|
+
strokeWidth: d.width,
|
|
30916
|
+
id: this.nextId()
|
|
30917
|
+
};
|
|
30918
|
+
} else {
|
|
30919
|
+
this.updateShape(d, p);
|
|
30920
|
+
annotation = {
|
|
30921
|
+
kind: "line",
|
|
30922
|
+
x1: d.start.x,
|
|
30923
|
+
y1: d.start.y,
|
|
30924
|
+
x2: p.x,
|
|
30925
|
+
y2: p.y,
|
|
30926
|
+
color: d.color,
|
|
30927
|
+
strokeWidth: d.width,
|
|
30928
|
+
id: this.nextId()
|
|
30929
|
+
};
|
|
30930
|
+
}
|
|
30931
|
+
} else {
|
|
30932
|
+
const x = Math.min(d.start.x, p.x);
|
|
30933
|
+
const y = Math.min(d.start.y, p.y);
|
|
30934
|
+
const width = Math.abs(p.x - d.start.x);
|
|
30935
|
+
const height = Math.abs(p.y - d.start.y);
|
|
30936
|
+
if (width < MIN_DRAG || height < MIN_DRAG) {
|
|
30937
|
+
d.node.remove();
|
|
30938
|
+
return;
|
|
30939
|
+
}
|
|
30940
|
+
this.updateShape(d, p);
|
|
30941
|
+
if (d.tool === "rect") {
|
|
30942
|
+
annotation = {
|
|
30943
|
+
kind: "rect",
|
|
30944
|
+
x,
|
|
30945
|
+
y,
|
|
30946
|
+
width,
|
|
30947
|
+
height,
|
|
30948
|
+
color: d.color,
|
|
30949
|
+
strokeWidth: d.width,
|
|
30950
|
+
id: this.nextId()
|
|
30951
|
+
};
|
|
30952
|
+
} else if (d.tool === "ellipse") {
|
|
30953
|
+
annotation = {
|
|
30954
|
+
kind: "ellipse",
|
|
30955
|
+
x,
|
|
30956
|
+
y,
|
|
30957
|
+
width,
|
|
30958
|
+
height,
|
|
30959
|
+
color: d.color,
|
|
30960
|
+
strokeWidth: d.width,
|
|
30961
|
+
id: this.nextId()
|
|
30962
|
+
};
|
|
30963
|
+
} else {
|
|
30964
|
+
annotation = { kind: "blur", x, y, width, height, id: this.nextId() };
|
|
30965
|
+
}
|
|
30057
30966
|
}
|
|
30058
|
-
|
|
30059
|
-
this.
|
|
30060
|
-
exit?.();
|
|
30061
|
-
}
|
|
30062
|
-
onDown = (e) => {
|
|
30063
|
-
e.preventDefault();
|
|
30064
|
-
this.dragging = true;
|
|
30065
|
-
this.startX = e.clientX;
|
|
30066
|
-
this.startY = e.clientY;
|
|
30067
|
-
this.draft = el("div", { class: "lens-rect" });
|
|
30068
|
-
this.host.append(this.draft);
|
|
30069
|
-
this.position(this.draft, e.clientX, e.clientY);
|
|
30070
|
-
window.addEventListener("pointermove", this.onMove, true);
|
|
30071
|
-
window.addEventListener("pointerup", this.onUp, true);
|
|
30967
|
+
d.node.removeAttribute("data-lens-draft");
|
|
30968
|
+
this.commitMark(annotation, d.node);
|
|
30072
30969
|
};
|
|
30073
|
-
|
|
30074
|
-
|
|
30075
|
-
|
|
30076
|
-
|
|
30077
|
-
|
|
30078
|
-
|
|
30079
|
-
this.
|
|
30080
|
-
|
|
30081
|
-
|
|
30082
|
-
|
|
30083
|
-
|
|
30084
|
-
|
|
30085
|
-
|
|
30086
|
-
|
|
30087
|
-
|
|
30088
|
-
|
|
30089
|
-
|
|
30090
|
-
|
|
30091
|
-
|
|
30092
|
-
|
|
30093
|
-
|
|
30094
|
-
|
|
30970
|
+
detachDragListeners() {
|
|
30971
|
+
window.removeEventListener("pointermove", this.onPointerMove, true);
|
|
30972
|
+
window.removeEventListener("pointerup", this.onPointerUp, true);
|
|
30973
|
+
}
|
|
30974
|
+
commitMark(annotation, node2) {
|
|
30975
|
+
this.annotations.push(annotation);
|
|
30976
|
+
this.markNodes.push(node2);
|
|
30977
|
+
this.syncActionButtons();
|
|
30978
|
+
}
|
|
30979
|
+
createShapeNode(tool, color, width) {
|
|
30980
|
+
if (tool === "ellipse") {
|
|
30981
|
+
return this.svgEl("ellipse", {
|
|
30982
|
+
"data-lens-draft": "1",
|
|
30983
|
+
fill: "none",
|
|
30984
|
+
stroke: color,
|
|
30985
|
+
"stroke-width": width
|
|
30986
|
+
});
|
|
30987
|
+
}
|
|
30988
|
+
if (tool === "line") {
|
|
30989
|
+
return this.svgEl("line", {
|
|
30990
|
+
"data-lens-draft": "1",
|
|
30991
|
+
stroke: color,
|
|
30992
|
+
"stroke-width": width,
|
|
30993
|
+
"stroke-linecap": "round"
|
|
30994
|
+
});
|
|
30995
|
+
}
|
|
30996
|
+
if (tool === "blur") {
|
|
30997
|
+
return this.svgEl("rect", {
|
|
30998
|
+
"data-lens-draft": "1",
|
|
30999
|
+
"data-lens-blur": "1",
|
|
31000
|
+
fill: "url(#lens-blur-hatch)",
|
|
31001
|
+
stroke: "#111827",
|
|
31002
|
+
"stroke-width": 1,
|
|
31003
|
+
"stroke-dasharray": "4 3"
|
|
31004
|
+
});
|
|
31005
|
+
}
|
|
31006
|
+
return this.svgEl("rect", {
|
|
31007
|
+
"data-lens-draft": "1",
|
|
31008
|
+
fill: "none",
|
|
31009
|
+
stroke: color,
|
|
31010
|
+
"stroke-width": width,
|
|
31011
|
+
rx: 2
|
|
30095
31012
|
});
|
|
30096
|
-
|
|
30097
|
-
|
|
30098
|
-
|
|
30099
|
-
|
|
30100
|
-
|
|
30101
|
-
|
|
30102
|
-
|
|
30103
|
-
|
|
30104
|
-
|
|
31013
|
+
}
|
|
31014
|
+
updateShape(d, cur) {
|
|
31015
|
+
const x = Math.min(d.start.x, cur.x);
|
|
31016
|
+
const y = Math.min(d.start.y, cur.y);
|
|
31017
|
+
const w = Math.abs(cur.x - d.start.x);
|
|
31018
|
+
const h = Math.abs(cur.y - d.start.y);
|
|
31019
|
+
const node2 = d.node;
|
|
31020
|
+
if (d.tool === "ellipse") {
|
|
31021
|
+
node2.setAttribute("cx", String(x + w / 2));
|
|
31022
|
+
node2.setAttribute("cy", String(y + h / 2));
|
|
31023
|
+
node2.setAttribute("rx", String(w / 2));
|
|
31024
|
+
node2.setAttribute("ry", String(h / 2));
|
|
31025
|
+
} else if (d.tool === "line") {
|
|
31026
|
+
node2.setAttribute("x1", String(d.start.x));
|
|
31027
|
+
node2.setAttribute("y1", String(d.start.y));
|
|
31028
|
+
node2.setAttribute("x2", String(cur.x));
|
|
31029
|
+
node2.setAttribute("y2", String(cur.y));
|
|
31030
|
+
} else {
|
|
31031
|
+
node2.setAttribute("x", String(x));
|
|
31032
|
+
node2.setAttribute("y", String(y));
|
|
31033
|
+
node2.setAttribute("width", String(w));
|
|
31034
|
+
node2.setAttribute("height", String(h));
|
|
31035
|
+
}
|
|
31036
|
+
}
|
|
31037
|
+
updateArrow(d, cur) {
|
|
31038
|
+
if (!d.line || !d.head) return;
|
|
31039
|
+
d.line.setAttribute("x1", String(d.start.x));
|
|
31040
|
+
d.line.setAttribute("y1", String(d.start.y));
|
|
31041
|
+
d.line.setAttribute("x2", String(cur.x));
|
|
31042
|
+
d.line.setAttribute("y2", String(cur.y));
|
|
31043
|
+
const size = Math.max(12, d.width * 3);
|
|
31044
|
+
const poly = arrowheadPolygon(d.start.x, d.start.y, cur.x, cur.y, size);
|
|
31045
|
+
d.head.setAttribute("points", this.pointsAttr(poly));
|
|
31046
|
+
}
|
|
31047
|
+
// --------------------------------------------------------------------------
|
|
31048
|
+
// Text tool
|
|
31049
|
+
// --------------------------------------------------------------------------
|
|
31050
|
+
openTextInput(clientX, clientY, docPoint) {
|
|
31051
|
+
this.commitTextInput();
|
|
31052
|
+
const input2 = el("input", {
|
|
31053
|
+
class: "lens-text-input",
|
|
31054
|
+
attrs: { type: "text", "aria-label": "Annotation text" },
|
|
31055
|
+
style: { left: `${clientX}px`, top: `${clientY}px`, color: this.color }
|
|
30105
31056
|
});
|
|
30106
|
-
|
|
30107
|
-
|
|
30108
|
-
|
|
30109
|
-
|
|
30110
|
-
|
|
30111
|
-
|
|
30112
|
-
|
|
30113
|
-
|
|
30114
|
-
|
|
31057
|
+
input2.addEventListener("blur", () => this.commitTextInput());
|
|
31058
|
+
this.host.append(input2);
|
|
31059
|
+
this.textInputEl = input2;
|
|
31060
|
+
this.textAnchor = docPoint;
|
|
31061
|
+
this.textColor = this.color;
|
|
31062
|
+
input2.focus();
|
|
31063
|
+
}
|
|
31064
|
+
commitTextInput() {
|
|
31065
|
+
const input2 = this.textInputEl;
|
|
31066
|
+
const anchor = this.textAnchor;
|
|
31067
|
+
if (!input2 || !anchor) return;
|
|
31068
|
+
const value = input2.value.trim();
|
|
31069
|
+
this.textInputEl = null;
|
|
31070
|
+
this.textAnchor = null;
|
|
31071
|
+
input2.remove();
|
|
31072
|
+
if (!value) return;
|
|
31073
|
+
const node2 = this.svgEl("text", {
|
|
31074
|
+
x: anchor.x,
|
|
31075
|
+
y: anchor.y,
|
|
31076
|
+
fill: this.textColor,
|
|
31077
|
+
"font-size": 16,
|
|
31078
|
+
"font-family": FONT,
|
|
31079
|
+
"dominant-baseline": "hanging"
|
|
30115
31080
|
});
|
|
31081
|
+
node2.textContent = value;
|
|
31082
|
+
this.svg.append(node2);
|
|
31083
|
+
this.commitMark(
|
|
31084
|
+
{
|
|
31085
|
+
kind: "text",
|
|
31086
|
+
x: anchor.x,
|
|
31087
|
+
y: anchor.y,
|
|
31088
|
+
text: value,
|
|
31089
|
+
fontSize: 16,
|
|
31090
|
+
color: this.textColor,
|
|
31091
|
+
id: this.nextId()
|
|
31092
|
+
},
|
|
31093
|
+
node2
|
|
31094
|
+
);
|
|
30116
31095
|
}
|
|
31096
|
+
cancelTextInput() {
|
|
31097
|
+
if (!this.textInputEl) return;
|
|
31098
|
+
this.textInputEl.remove();
|
|
31099
|
+
this.textInputEl = null;
|
|
31100
|
+
this.textAnchor = null;
|
|
31101
|
+
}
|
|
31102
|
+
// --------------------------------------------------------------------------
|
|
31103
|
+
// Step tool
|
|
31104
|
+
// --------------------------------------------------------------------------
|
|
31105
|
+
placeStep(p) {
|
|
31106
|
+
const n2 = ++this.stepCounter;
|
|
31107
|
+
const g = this.svgEl("g", {});
|
|
31108
|
+
const circle = this.svgEl("circle", {
|
|
31109
|
+
cx: p.x,
|
|
31110
|
+
cy: p.y,
|
|
31111
|
+
r: STEP_RADIUS,
|
|
31112
|
+
fill: this.color
|
|
31113
|
+
});
|
|
31114
|
+
const text = this.svgEl("text", {
|
|
31115
|
+
x: p.x,
|
|
31116
|
+
y: p.y,
|
|
31117
|
+
fill: "#ffffff",
|
|
31118
|
+
"font-size": 13,
|
|
31119
|
+
"font-weight": 700,
|
|
31120
|
+
"font-family": FONT,
|
|
31121
|
+
"text-anchor": "middle",
|
|
31122
|
+
"dominant-baseline": "central"
|
|
31123
|
+
});
|
|
31124
|
+
text.textContent = String(n2);
|
|
31125
|
+
g.append(circle, text);
|
|
31126
|
+
this.svg.append(g);
|
|
31127
|
+
this.commitMark({ kind: "step", x: p.x, y: p.y, n: n2, color: this.color, id: this.nextId() }, g);
|
|
31128
|
+
}
|
|
31129
|
+
// --------------------------------------------------------------------------
|
|
31130
|
+
// Keyboard
|
|
31131
|
+
// --------------------------------------------------------------------------
|
|
30117
31132
|
onKey = (e) => {
|
|
30118
|
-
if (
|
|
30119
|
-
|
|
30120
|
-
|
|
30121
|
-
|
|
31133
|
+
if (this.textInputEl) {
|
|
31134
|
+
if (e.key === "Escape") {
|
|
31135
|
+
e.preventDefault();
|
|
31136
|
+
e.stopPropagation();
|
|
31137
|
+
this.cancelTextInput();
|
|
31138
|
+
} else if (e.key === "Enter") {
|
|
31139
|
+
e.preventDefault();
|
|
31140
|
+
e.stopPropagation();
|
|
31141
|
+
this.commitTextInput();
|
|
31142
|
+
}
|
|
31143
|
+
return;
|
|
31144
|
+
}
|
|
31145
|
+
if (e.key === "Escape") {
|
|
31146
|
+
e.preventDefault();
|
|
31147
|
+
e.stopPropagation();
|
|
31148
|
+
this.exit();
|
|
31149
|
+
}
|
|
30122
31150
|
};
|
|
31151
|
+
// --------------------------------------------------------------------------
|
|
31152
|
+
// Small helpers
|
|
31153
|
+
// --------------------------------------------------------------------------
|
|
31154
|
+
svgEl(tag, attrs) {
|
|
31155
|
+
const node2 = document.createElementNS(SVG_NS, tag);
|
|
31156
|
+
for (const [k, v] of Object.entries(attrs)) node2.setAttribute(k, String(v));
|
|
31157
|
+
return node2;
|
|
31158
|
+
}
|
|
31159
|
+
pointsAttr(points) {
|
|
31160
|
+
return points.map((p) => `${p.x},${p.y}`).join(" ");
|
|
31161
|
+
}
|
|
31162
|
+
nextId() {
|
|
31163
|
+
return `lens-${++this.idSeq}`;
|
|
31164
|
+
}
|
|
30123
31165
|
};
|
|
30124
|
-
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>`;
|
|
30125
|
-
var iconEye = svg(
|
|
30126
|
-
'<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"/>'
|
|
30127
|
-
);
|
|
30128
|
-
var iconCrosshair = svg(
|
|
30129
|
-
'<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"/>'
|
|
30130
|
-
);
|
|
30131
|
-
var iconClose = svg('<line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/>');
|
|
30132
|
-
var iconMark = svg(
|
|
30133
|
-
'<rect x="3" y="3" width="18" height="18" rx="2" stroke-dasharray="4 3"/>'
|
|
30134
|
-
);
|
|
30135
|
-
var iconCopy = svg(
|
|
30136
|
-
'<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/>'
|
|
30137
|
-
);
|
|
30138
|
-
var iconCheck = svg('<polyline points="20 6 9 17 4 12"/>');
|
|
30139
|
-
var iconSend = svg('<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>');
|
|
30140
|
-
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>`;
|
|
30141
|
-
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>`;
|
|
30142
31166
|
var INCLUDE_ITEMS = [
|
|
30143
31167
|
{ key: "replay", label: "Replay" },
|
|
30144
31168
|
{ key: "console", label: "Console" },
|
|
@@ -30295,7 +31319,7 @@ var Panel = class {
|
|
|
30295
31319
|
buildActions() {
|
|
30296
31320
|
const mark = el("button", {
|
|
30297
31321
|
class: "lens-abtn lens-abtn--ghost lens-abtn--compact",
|
|
30298
|
-
html:
|
|
31322
|
+
html: iconAnnotate + "<span>Annotate</span>",
|
|
30299
31323
|
on: { click: () => this.cb.onMarkArea() }
|
|
30300
31324
|
});
|
|
30301
31325
|
const record3 = el("button", {
|
|
@@ -30679,21 +31703,33 @@ var OVERLAY_CSS = (
|
|
|
30679
31703
|
pointer-events: none;
|
|
30680
31704
|
}
|
|
30681
31705
|
|
|
30682
|
-
/* ---------------- Annotation layer ---------------- */
|
|
30683
|
-
|
|
30684
|
-
|
|
30685
|
-
|
|
31706
|
+
/* ---------------- Annotation SVG layer ---------------- */
|
|
31707
|
+
/* One full-DOCUMENT SVG anchored to page content (translated by -scroll so
|
|
31708
|
+
marks survive scrolling). pointer-events flip to auto only while drawing. */
|
|
31709
|
+
.lens-annotate-svg {
|
|
31710
|
+
position: absolute;
|
|
31711
|
+
left: 0;
|
|
31712
|
+
top: 0;
|
|
30686
31713
|
z-index: calc(var(--lens-z) - 1);
|
|
30687
|
-
|
|
30688
|
-
|
|
30689
|
-
touch-action: none;
|
|
31714
|
+
pointer-events: none;
|
|
31715
|
+
overflow: visible;
|
|
30690
31716
|
}
|
|
30691
|
-
|
|
31717
|
+
|
|
31718
|
+
/* Floating text-entry input for the Text tool (viewport-positioned). */
|
|
31719
|
+
.lens-text-input {
|
|
30692
31720
|
position: fixed;
|
|
30693
|
-
|
|
30694
|
-
|
|
30695
|
-
|
|
30696
|
-
|
|
31721
|
+
z-index: var(--lens-z);
|
|
31722
|
+
min-width: 120px;
|
|
31723
|
+
padding: 2px 6px;
|
|
31724
|
+
border: 1px solid var(--lens-accent);
|
|
31725
|
+
border-radius: 4px;
|
|
31726
|
+
background: rgba(255, 255, 255, 0.96);
|
|
31727
|
+
color: #111827;
|
|
31728
|
+
font-family: var(--lens-font);
|
|
31729
|
+
font-size: 15px;
|
|
31730
|
+
line-height: 1.3;
|
|
31731
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
|
|
31732
|
+
outline: none;
|
|
30697
31733
|
}
|
|
30698
31734
|
|
|
30699
31735
|
/* ---------------- Panel ---------------- */
|
|
@@ -30984,6 +32020,114 @@ var OVERLAY_CSS = (
|
|
|
30984
32020
|
}
|
|
30985
32021
|
.lens-annotate-chips { z-index: var(--lens-z); }
|
|
30986
32022
|
|
|
32023
|
+
/* ---------------- Annotation toolbar ---------------- */
|
|
32024
|
+
/* Same floating top-center anchor as the chips row; wraps on narrow screens. */
|
|
32025
|
+
.lens-annotate-toolbar {
|
|
32026
|
+
position: fixed;
|
|
32027
|
+
top: calc(16px + env(safe-area-inset-top));
|
|
32028
|
+
left: 50%;
|
|
32029
|
+
transform: translateX(-50%);
|
|
32030
|
+
z-index: var(--lens-z);
|
|
32031
|
+
display: inline-flex;
|
|
32032
|
+
flex-wrap: wrap;
|
|
32033
|
+
align-items: center;
|
|
32034
|
+
justify-content: center;
|
|
32035
|
+
gap: 6px;
|
|
32036
|
+
max-width: calc(100vw - 24px);
|
|
32037
|
+
padding: 6px 8px;
|
|
32038
|
+
border-radius: 14px;
|
|
32039
|
+
background: rgba(20, 20, 23, 0.92);
|
|
32040
|
+
-webkit-backdrop-filter: blur(12px) saturate(180%);
|
|
32041
|
+
backdrop-filter: blur(12px) saturate(180%);
|
|
32042
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
32043
|
+
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
|
|
32044
|
+
color: #f4f4f5;
|
|
32045
|
+
}
|
|
32046
|
+
.lens-tool-group {
|
|
32047
|
+
display: inline-flex;
|
|
32048
|
+
align-items: center;
|
|
32049
|
+
gap: 4px;
|
|
32050
|
+
flex-wrap: wrap;
|
|
32051
|
+
}
|
|
32052
|
+
.lens-tool-sep {
|
|
32053
|
+
width: 1px;
|
|
32054
|
+
align-self: stretch;
|
|
32055
|
+
min-height: 22px;
|
|
32056
|
+
background: rgba(255, 255, 255, 0.16);
|
|
32057
|
+
margin: 0 2px;
|
|
32058
|
+
}
|
|
32059
|
+
.lens-tool-btn {
|
|
32060
|
+
display: inline-flex;
|
|
32061
|
+
align-items: center;
|
|
32062
|
+
justify-content: center;
|
|
32063
|
+
width: 30px;
|
|
32064
|
+
height: 30px;
|
|
32065
|
+
padding: 0;
|
|
32066
|
+
border: 1px solid transparent;
|
|
32067
|
+
border-radius: 8px;
|
|
32068
|
+
background: transparent;
|
|
32069
|
+
color: #d4d4d8;
|
|
32070
|
+
cursor: pointer;
|
|
32071
|
+
transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
|
|
32072
|
+
}
|
|
32073
|
+
.lens-tool-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
|
|
32074
|
+
.lens-tool-btn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
|
|
32075
|
+
.lens-tool-btn.is-active {
|
|
32076
|
+
background: var(--lens-accent);
|
|
32077
|
+
border-color: var(--lens-accent);
|
|
32078
|
+
color: #fff;
|
|
32079
|
+
}
|
|
32080
|
+
.lens-tool-btn:disabled { opacity: 0.4; cursor: default; }
|
|
32081
|
+
.lens-tool-btn:disabled:hover { background: transparent; color: #d4d4d8; }
|
|
32082
|
+
|
|
32083
|
+
.lens-swatch {
|
|
32084
|
+
width: 22px;
|
|
32085
|
+
height: 22px;
|
|
32086
|
+
padding: 0;
|
|
32087
|
+
border-radius: 999px;
|
|
32088
|
+
border: 2px solid transparent;
|
|
32089
|
+
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25) inset;
|
|
32090
|
+
cursor: pointer;
|
|
32091
|
+
transition: transform 120ms ease, border-color 120ms ease;
|
|
32092
|
+
}
|
|
32093
|
+
.lens-swatch:hover { transform: scale(1.1); }
|
|
32094
|
+
.lens-swatch:focus-visible { outline: 2px solid #fff; outline-offset: 1px; }
|
|
32095
|
+
.lens-swatch.is-active {
|
|
32096
|
+
border-color: #fff;
|
|
32097
|
+
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.4);
|
|
32098
|
+
}
|
|
32099
|
+
|
|
32100
|
+
.lens-width-btn {
|
|
32101
|
+
display: inline-flex;
|
|
32102
|
+
align-items: center;
|
|
32103
|
+
justify-content: center;
|
|
32104
|
+
min-width: 26px;
|
|
32105
|
+
height: 30px;
|
|
32106
|
+
padding: 0 6px;
|
|
32107
|
+
border: 1px solid transparent;
|
|
32108
|
+
border-radius: 8px;
|
|
32109
|
+
background: transparent;
|
|
32110
|
+
color: #d4d4d8;
|
|
32111
|
+
font-family: var(--lens-font);
|
|
32112
|
+
font-size: 12px;
|
|
32113
|
+
font-weight: 700;
|
|
32114
|
+
cursor: pointer;
|
|
32115
|
+
transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
|
|
32116
|
+
}
|
|
32117
|
+
.lens-width-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
|
|
32118
|
+
.lens-width-btn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
|
|
32119
|
+
.lens-width-btn.is-active {
|
|
32120
|
+
background: var(--lens-accent);
|
|
32121
|
+
border-color: var(--lens-accent);
|
|
32122
|
+
color: #fff;
|
|
32123
|
+
}
|
|
32124
|
+
|
|
32125
|
+
/* Coarse pointers: bump the tool targets toward 40px. */
|
|
32126
|
+
@media (pointer: coarse) {
|
|
32127
|
+
.lens-tool-btn, .lens-width-btn { width: 38px; height: 38px; }
|
|
32128
|
+
.lens-swatch { width: 30px; height: 30px; }
|
|
32129
|
+
}
|
|
32130
|
+
|
|
30987
32131
|
/* Stacking inside the shadow layer: the panel, pill and floating chips always
|
|
30988
32132
|
sit above the annotation layer/rects and picker highlight, so marked areas
|
|
30989
32133
|
never paint over interactive UI. */
|
|
@@ -31109,7 +32253,7 @@ var Overlay = class {
|
|
|
31109
32253
|
this.layer = el("div", { class: "lens-layer" });
|
|
31110
32254
|
this.shadow.append(this.layer);
|
|
31111
32255
|
this.picker = new ElementPicker(this.layer, this.host);
|
|
31112
|
-
this.annotator = new
|
|
32256
|
+
this.annotator = new AnnotationEditor(this.layer);
|
|
31113
32257
|
this.layer.append(this.buildPill());
|
|
31114
32258
|
document.documentElement.append(this.host);
|
|
31115
32259
|
window.addEventListener("keydown", this.onGlobalKey, true);
|
|
@@ -31277,11 +32421,15 @@ var Overlay = class {
|
|
|
31277
32421
|
}
|
|
31278
32422
|
async submit(comment2, include) {
|
|
31279
32423
|
const annotations = this.annotator.list();
|
|
32424
|
+
const composite = this.annotator.getComposite() ?? void 0;
|
|
32425
|
+
const screenshotCrop = this.annotator.getCropBounds();
|
|
31280
32426
|
const result2 = await this.deps.client.submitReport({
|
|
31281
32427
|
comment: comment2 || void 0,
|
|
31282
32428
|
annotations,
|
|
31283
32429
|
target: this.target ?? void 0,
|
|
31284
32430
|
excludeFromScreenshot: this.host,
|
|
32431
|
+
screenshotCrop,
|
|
32432
|
+
annotationComposite: composite,
|
|
31285
32433
|
include
|
|
31286
32434
|
});
|
|
31287
32435
|
this.target = null;
|