@comty.io/core 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/comty-sdk.js +1046 -210
- package/package.json +2 -2
- package/src/browser.js +531 -99
- package/src/capture.js +104 -1
- package/src/dev.mjs +3 -2
- package/src/measurements.js +209 -0
- package/src/style.js +203 -111
- package/src/vite.mjs +3 -2
package/src/capture.js
CHANGED
|
@@ -440,7 +440,7 @@ export function createScreenshotCapture() {
|
|
|
440
440
|
|
|
441
441
|
const clonedBody = clone.querySelector("body");
|
|
442
442
|
if (clonedBody instanceof HTMLElement) {
|
|
443
|
-
clonedBody.style.width =
|
|
443
|
+
clonedBody.style.width = getComputedStyle(document.body).width;
|
|
444
444
|
clonedBody.style.minHeight = `${documentHeight}px`;
|
|
445
445
|
clonedBody.style.margin = clonedBody.style.margin || "0";
|
|
446
446
|
}
|
|
@@ -470,3 +470,106 @@ export function createScreenshotCapture() {
|
|
|
470
470
|
|
|
471
471
|
return { waitForPaint, waitForCustomElements, captureViewportScreenshot };
|
|
472
472
|
}
|
|
473
|
+
|
|
474
|
+
// Page-local counterpart of the native eyedropper; never captures outside the page.
|
|
475
|
+
export function createColorMagnifier({ root, isComtyElement, capture }) {
|
|
476
|
+
const lens = document.createElement('div');
|
|
477
|
+
lens.className = 'comty-color-magnifier';
|
|
478
|
+
lens.setAttribute('aria-hidden', 'true');
|
|
479
|
+
lens.hidden = true;
|
|
480
|
+
lens.innerHTML = '<div class="comty-color-magnifier-glass"><canvas width="9" height="9"></canvas><span></span></div>';
|
|
481
|
+
root.appendChild(lens);
|
|
482
|
+
const canvas = lens.querySelector('canvas');
|
|
483
|
+
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
484
|
+
let enabled = false, image = null, point = null, revision = 0, timer = 0;
|
|
485
|
+
let sampledHex = null;
|
|
486
|
+
|
|
487
|
+
function paint() {
|
|
488
|
+
if (!enabled || !point || !context) return;
|
|
489
|
+
const { x, y, element } = point;
|
|
490
|
+
if (!element.isConnected) { lens.hidden = true; return; }
|
|
491
|
+
// An unavailable/unsafe page capture falls back to the element's CSS colour.
|
|
492
|
+
let background = element;
|
|
493
|
+
while (background.parentElement && getComputedStyle(background).backgroundColor === 'rgba(0, 0, 0, 0)') background = background.parentElement;
|
|
494
|
+
const color = getComputedStyle(background).backgroundColor;
|
|
495
|
+
context.clearRect(0, 0, 9, 9);
|
|
496
|
+
context.fillStyle = getComputedStyle(root).getPropertyValue('--comty-paper');
|
|
497
|
+
context.fillRect(0, 0, 9, 9);
|
|
498
|
+
context.fillStyle = color;
|
|
499
|
+
context.fillRect(0, 0, 9, 9);
|
|
500
|
+
if (image) context.drawImage(image, Math.floor(x) - 4, Math.floor(y) - 4, 9, 9, 0, 0, 9, 9);
|
|
501
|
+
lens.dataset.preview = image ? 'pixels' : 'color';
|
|
502
|
+
sampledHex = null;
|
|
503
|
+
try {
|
|
504
|
+
const [r, g, b] = context.getImageData(4, 4, 1, 1).data;
|
|
505
|
+
if (image) sampledHex = '#' + [r, g, b].map(channel => channel.toString(16).padStart(2, '0')).join('');
|
|
506
|
+
lens.dataset.tone = .2126 * r + .7152 * g + .0722 * b < 128 ? 'dark' : 'light';
|
|
507
|
+
} catch { lens.dataset.tone = 'light'; }
|
|
508
|
+
lens.style.left = `${x}px`;
|
|
509
|
+
lens.style.top = `${y}px`;
|
|
510
|
+
lens.hidden = false;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function refresh() {
|
|
514
|
+
const version = ++revision;
|
|
515
|
+
image = null;
|
|
516
|
+
sampledHex = null;
|
|
517
|
+
if (!enabled) return;
|
|
518
|
+
try {
|
|
519
|
+
const { blob } = await capture();
|
|
520
|
+
if (!enabled || version !== revision) return;
|
|
521
|
+
const dataUrl = await new Promise((resolve, reject) => {
|
|
522
|
+
const reader = new FileReader();
|
|
523
|
+
reader.onload = () => resolve(reader.result);
|
|
524
|
+
reader.onerror = reject;
|
|
525
|
+
reader.readAsDataURL(blob);
|
|
526
|
+
});
|
|
527
|
+
const next = new Image();
|
|
528
|
+
next.src = dataUrl;
|
|
529
|
+
await next.decode();
|
|
530
|
+
if (!enabled || version !== revision) return;
|
|
531
|
+
// Verify the bitmap can be sampled before using it for both preview and pick.
|
|
532
|
+
const probe = document.createElement('canvas').getContext('2d');
|
|
533
|
+
probe.drawImage(next, 0, 0);
|
|
534
|
+
probe.getImageData(0, 0, 1, 1);
|
|
535
|
+
image = next;
|
|
536
|
+
paint();
|
|
537
|
+
} catch { /* The CSS-colour fallback remains available without screen permission. */ }
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function setEnabled(value) {
|
|
541
|
+
if (enabled === value) return;
|
|
542
|
+
enabled = value;
|
|
543
|
+
lens.hidden = true;
|
|
544
|
+
point = null;
|
|
545
|
+
clearTimeout(timer);
|
|
546
|
+
if (enabled) refresh();
|
|
547
|
+
else { revision++; image = null; sampledHex = null; }
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
document.addEventListener('pointermove', event => {
|
|
551
|
+
if (!enabled) return;
|
|
552
|
+
if (!(event.target instanceof Element) || isComtyElement(event.target)) {
|
|
553
|
+
point = null;
|
|
554
|
+
sampledHex = null;
|
|
555
|
+
lens.hidden = true;
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
point = { x: event.clientX, y: event.clientY, element: event.target };
|
|
559
|
+
paint();
|
|
560
|
+
}, true);
|
|
561
|
+
document.documentElement.addEventListener('pointerleave', () => { point = null; lens.hidden = true; });
|
|
562
|
+
function invalidate(event) {
|
|
563
|
+
if (!enabled || event.target instanceof Element && isComtyElement(event.target)) return;
|
|
564
|
+
revision++;
|
|
565
|
+
image = null;
|
|
566
|
+
sampledHex = null;
|
|
567
|
+
point = null;
|
|
568
|
+
lens.hidden = true;
|
|
569
|
+
clearTimeout(timer);
|
|
570
|
+
timer = setTimeout(refresh, 120);
|
|
571
|
+
}
|
|
572
|
+
document.addEventListener('scroll', invalidate, true);
|
|
573
|
+
window.addEventListener('resize', invalidate);
|
|
574
|
+
return { setEnabled, sample: () => sampledHex };
|
|
575
|
+
}
|
package/src/dev.mjs
CHANGED
|
@@ -19,6 +19,7 @@ export async function startDev({ cwd = process.cwd(), target = 'http://127.0.0.1
|
|
|
19
19
|
const anchors = await readFile(new URL('./anchors.js', import.meta.url));
|
|
20
20
|
const capture = await readFile(new URL('./capture.js', import.meta.url));
|
|
21
21
|
const comments = await readFile(new URL('./comments.js', import.meta.url));
|
|
22
|
+
const measurements = await readFile(new URL('./measurements.js', import.meta.url));
|
|
22
23
|
let companion, origin, child, closing, finished;
|
|
23
24
|
const sockets = new Set();
|
|
24
25
|
const proxies = new Set();
|
|
@@ -34,9 +35,9 @@ export async function startDev({ cwd = process.cwd(), target = 'http://127.0.0.1
|
|
|
34
35
|
if (request.headers.origin && request.headers.origin !== origin) {
|
|
35
36
|
response.writeHead(403); response.end(); return;
|
|
36
37
|
}
|
|
37
|
-
if (request.method === 'GET' && ['/__comty/browser.js', '/__comty/style.js', '/__comty/anchors.js', '/__comty/capture.js', '/__comty/comments.js', '/__comty/start.js'].includes(url.pathname)) {
|
|
38
|
+
if (request.method === 'GET' && ['/__comty/browser.js', '/__comty/style.js', '/__comty/anchors.js', '/__comty/capture.js', '/__comty/comments.js', '/__comty/measurements.js', '/__comty/start.js'].includes(url.pathname)) {
|
|
38
39
|
response.writeHead(200, { 'Content-Type': 'text/javascript', 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' });
|
|
39
|
-
response.end(url.pathname.endsWith('/browser.js') ? runtime : url.pathname.endsWith('/style.js') ? style : url.pathname.endsWith('/anchors.js') ? anchors : url.pathname.endsWith('/capture.js') ? capture : url.pathname.endsWith('/comments.js') ? comments :
|
|
40
|
+
response.end(url.pathname.endsWith('/browser.js') ? runtime : url.pathname.endsWith('/style.js') ? style : url.pathname.endsWith('/anchors.js') ? anchors : url.pathname.endsWith('/capture.js') ? capture : url.pathname.endsWith('/comments.js') ? comments : url.pathname.endsWith('/measurements.js') ? measurements :
|
|
40
41
|
`import { initComty } from '/__comty/browser.js';\ninitComty(${JSON.stringify({ projectKey, apiUrl, agent: { url: `${origin}/__comty`, token: companion.token } })}).catch(console.error);`);
|
|
41
42
|
return;
|
|
42
43
|
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
const edgeAxis = { top: "y", bottom: "y", left: "x", right: "x" };
|
|
2
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
3
|
+
|
|
4
|
+
/** Pick a nearby border segment, including when the pointer is just outside it. */
|
|
5
|
+
export function nearestMeasurementEdge(candidates, point, { axis, exclude, tolerance = 10 } = {}) {
|
|
6
|
+
let nearest = null;
|
|
7
|
+
let distance = tolerance;
|
|
8
|
+
for (const { element, rect } of candidates) {
|
|
9
|
+
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
10
|
+
for (const edge of ["top", "bottom", "left", "right"]) {
|
|
11
|
+
if (axis && edgeAxis[edge] !== axis) continue;
|
|
12
|
+
if (exclude?.element === element && exclude.edge === edge) continue;
|
|
13
|
+
const vertical = edgeAxis[edge] === "y";
|
|
14
|
+
const x = vertical ? clamp(point.x, rect.left, rect.right) : rect[edge];
|
|
15
|
+
const y = vertical ? rect[edge] : clamp(point.y, rect.top, rect.bottom);
|
|
16
|
+
const nextDistance = Math.hypot(point.x - x, point.y - y);
|
|
17
|
+
if (nextDistance > distance || (nearest && nextDistance === distance)) continue;
|
|
18
|
+
distance = nextDistance;
|
|
19
|
+
nearest = { element, edge, axis: edgeAxis[edge], offset: vertical ? (x - rect.left) / rect.width : (y - rect.top) / rect.height };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return nearest;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function measurementPoint(anchor, rect, scroll = { x: 0, y: 0 }) {
|
|
26
|
+
if (!anchor.element) return { x: anchor.x - scroll.x, y: anchor.y - scroll.y };
|
|
27
|
+
return anchor.axis === "y"
|
|
28
|
+
? { x: rect.left + rect.width * anchor.offset, y: rect[anchor.edge] }
|
|
29
|
+
: { x: rect[anchor.edge], y: rect.top + rect.height * anchor.offset };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Border measurements live for one activation of the existing ruler tool. */
|
|
33
|
+
export function createMeasurements({ root, isComtyElement, isElementTreeHidden, onExit }) {
|
|
34
|
+
let enabled = false;
|
|
35
|
+
let hover = null;
|
|
36
|
+
let drag = null;
|
|
37
|
+
let pointer = null;
|
|
38
|
+
let signature = "";
|
|
39
|
+
const measurements = [];
|
|
40
|
+
const svgNS = "http://www.w3.org/2000/svg";
|
|
41
|
+
const layer = document.createElementNS(svgNS, "svg");
|
|
42
|
+
layer.classList.add("comty-distance-layer");
|
|
43
|
+
layer.setAttribute("aria-hidden", "true");
|
|
44
|
+
root.prepend(layer);
|
|
45
|
+
const announcement = document.createElement("span");
|
|
46
|
+
announcement.className = "comty-distance-announcement";
|
|
47
|
+
announcement.setAttribute("role", "status");
|
|
48
|
+
root.appendChild(announcement);
|
|
49
|
+
|
|
50
|
+
function rectOf(element) {
|
|
51
|
+
if (element === document.documentElement) {
|
|
52
|
+
const width = Math.max(element.clientWidth, element.scrollWidth);
|
|
53
|
+
const height = Math.max(element.clientHeight, element.scrollHeight);
|
|
54
|
+
return { left: -scrollX, top: -scrollY, right: width - scrollX, bottom: height - scrollY, width, height };
|
|
55
|
+
}
|
|
56
|
+
return element.getBoundingClientRect();
|
|
57
|
+
}
|
|
58
|
+
function pointOf(anchor) {
|
|
59
|
+
return measurementPoint(anchor, anchor.element && rectOf(anchor.element), { x: scrollX, y: scrollY });
|
|
60
|
+
}
|
|
61
|
+
function candidatesAt(point) {
|
|
62
|
+
const elements = new Set();
|
|
63
|
+
// Sample around the cursor as well as underneath it to catch an outside edge.
|
|
64
|
+
for (const [dx, dy] of [[0, 0], [-10, 0], [10, 0], [0, -10], [0, 10]]) {
|
|
65
|
+
for (const hit of document.elementsFromPoint(clamp(point.x + dx, 0, innerWidth - 1), clamp(point.y + dy, 0, innerHeight - 1))) {
|
|
66
|
+
if (isComtyElement(hit)) continue;
|
|
67
|
+
for (let element = hit; element; element = element.parentElement) elements.add(element);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return [...elements].filter(element => !isElementTreeHidden(element)).map(element => ({ element, rect: rectOf(element) }));
|
|
71
|
+
}
|
|
72
|
+
function findEdge(point, start) {
|
|
73
|
+
return nearestMeasurementEdge(candidatesAt(point), point, { axis: start?.axis, exclude: start });
|
|
74
|
+
}
|
|
75
|
+
function setCursor(axis) {
|
|
76
|
+
document.documentElement.classList.toggle("comty-measure-vertical", axis === "y");
|
|
77
|
+
document.documentElement.classList.toggle("comty-measure-horizontal", axis === "x");
|
|
78
|
+
}
|
|
79
|
+
function updatePointer(event) {
|
|
80
|
+
pointer = { x: event.clientX, y: event.clientY };
|
|
81
|
+
hover = findEdge(pointer, drag?.start);
|
|
82
|
+
if (drag) {
|
|
83
|
+
drag.end = hover || { x: pointer.x + scrollX, y: pointer.y + scrollY };
|
|
84
|
+
// Keep the two anchors perpendicular when both borders share that coordinate.
|
|
85
|
+
if (hover) {
|
|
86
|
+
const start = pointOf(drag.start), rect = rectOf(hover.element);
|
|
87
|
+
drag.end = { ...hover, offset: drag.start.axis === "y"
|
|
88
|
+
? clamp((start.x - rect.left) / rect.width, 0, 1)
|
|
89
|
+
: clamp((start.y - rect.top) / rect.height, 0, 1) };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
setCursor(drag?.start.axis || hover?.axis);
|
|
93
|
+
render();
|
|
94
|
+
}
|
|
95
|
+
function node(tag, attributes, parent = layer) {
|
|
96
|
+
const element = document.createElementNS(svgNS, tag);
|
|
97
|
+
for (const [key, value] of Object.entries(attributes)) element.setAttribute(key, value);
|
|
98
|
+
parent.appendChild(element);
|
|
99
|
+
return element;
|
|
100
|
+
}
|
|
101
|
+
function valid(anchor) {
|
|
102
|
+
return !anchor.element || (anchor.element.isConnected && !isElementTreeHidden(anchor.element));
|
|
103
|
+
}
|
|
104
|
+
function render() {
|
|
105
|
+
if (!enabled) return;
|
|
106
|
+
const visible = [...measurements, ...(drag ? [drag] : [])].filter(item => valid(item.start) && valid(item.end));
|
|
107
|
+
const geometry = visible.map(item => ({ start: pointOf(item.start), end: pointOf(item.end), axis: item.start.axis }));
|
|
108
|
+
const hoverPoint = hover && valid(hover) ? pointOf(hover) : null;
|
|
109
|
+
const next = JSON.stringify([geometry, hoverPoint]);
|
|
110
|
+
if (signature === next) return;
|
|
111
|
+
signature = next;
|
|
112
|
+
layer.replaceChildren();
|
|
113
|
+
for (const { start, end, axis } of geometry) {
|
|
114
|
+
const vertical = axis === "y";
|
|
115
|
+
const distance = Math.round(Math.abs(end[axis] - start[axis]));
|
|
116
|
+
if (distance < 1) continue;
|
|
117
|
+
const x2 = vertical ? start.x : end.x, y2 = vertical ? end.y : start.y;
|
|
118
|
+
const group = node("g", { "data-comty-distance": distance });
|
|
119
|
+
node("path", { d: `M${start.x} ${start.y}L${x2} ${y2}`, class: "comty-distance-line" }, group);
|
|
120
|
+
if (Math.hypot(end.x - x2, end.y - y2) > 1) node("path", { d: `M${x2} ${y2}L${end.x} ${end.y}`, class: "comty-distance-extension" }, group);
|
|
121
|
+
const direction = Math.sign(end[axis] - start[axis]);
|
|
122
|
+
for (const [x, y, inward] of [[start.x, start.y, direction], [x2, y2, -direction]]) {
|
|
123
|
+
const points = vertical ? `${x},${y} ${x - 3},${y + inward * 6} ${x + 3},${y + inward * 6}` : `${x},${y} ${x + inward * 6},${y - 3} ${x + inward * 6},${y + 3}`;
|
|
124
|
+
node("polygon", { points, class: "comty-distance-arrow" }, group);
|
|
125
|
+
}
|
|
126
|
+
const text = String(distance), width = text.length * 7.5 + 12;
|
|
127
|
+
const labelX = vertical ? start.x - width - 8 : (start.x + x2 - width) / 2;
|
|
128
|
+
const labelY = vertical ? (start.y + y2) / 2 - 10 : start.y - 28;
|
|
129
|
+
const label = node("g", { transform: `translate(${clamp(labelX, 2, innerWidth - width - 2)} ${clamp(labelY, 2, innerHeight - 22)})` }, group);
|
|
130
|
+
node("rect", { width, height: 20, rx: 3, class: "comty-distance-label" }, label);
|
|
131
|
+
node("text", { x: width / 2, y: 14, "text-anchor": "middle", class: "comty-distance-value" }, label).textContent = text;
|
|
132
|
+
}
|
|
133
|
+
if (hoverPoint) node("circle", { cx: hoverPoint.x, cy: hoverPoint.y, r: 4, class: "comty-distance-snap" });
|
|
134
|
+
}
|
|
135
|
+
function cancel() {
|
|
136
|
+
const pointerId = drag?.pointerId;
|
|
137
|
+
drag = null;
|
|
138
|
+
if (pointerId !== undefined && document.documentElement.hasPointerCapture(pointerId)) document.documentElement.releasePointerCapture(pointerId);
|
|
139
|
+
hover = null;
|
|
140
|
+
pointer = null;
|
|
141
|
+
setCursor(null);
|
|
142
|
+
render();
|
|
143
|
+
}
|
|
144
|
+
function setEnabled(value) {
|
|
145
|
+
if (enabled === value) return;
|
|
146
|
+
enabled = value;
|
|
147
|
+
layer.style.display = value ? "" : "none";
|
|
148
|
+
if (!value) {
|
|
149
|
+
measurements.length = 0;
|
|
150
|
+
cancel();
|
|
151
|
+
layer.replaceChildren();
|
|
152
|
+
announcement.textContent = "";
|
|
153
|
+
signature = "";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function isHostEvent(event) {
|
|
157
|
+
if (!enabled || !(event.target instanceof Element)) return false;
|
|
158
|
+
if (!isComtyElement(event.target)) return true;
|
|
159
|
+
// The outer ruler strip must still let us anchor to the page's top/left edge.
|
|
160
|
+
return event.target.matches(".comty-ruler") && (event.clientX + scrollX <= 10 || event.clientY + scrollY <= 10);
|
|
161
|
+
}
|
|
162
|
+
document.addEventListener("pointermove", event => {
|
|
163
|
+
if (!enabled) return;
|
|
164
|
+
if (drag && event.pointerId !== drag.pointerId) return;
|
|
165
|
+
if (!isHostEvent(event) && !drag) { hover = null; setCursor(null); render(); return; }
|
|
166
|
+
event.stopImmediatePropagation();
|
|
167
|
+
updatePointer(event);
|
|
168
|
+
}, true);
|
|
169
|
+
document.addEventListener("pointerdown", event => {
|
|
170
|
+
if (!isHostEvent(event) || event.button !== 0) return;
|
|
171
|
+
event.preventDefault(); event.stopImmediatePropagation();
|
|
172
|
+
updatePointer(event);
|
|
173
|
+
if (hover) {
|
|
174
|
+
drag = { start: hover, end: hover, pointerId: event.pointerId };
|
|
175
|
+
if (event.isTrusted) document.documentElement.setPointerCapture(event.pointerId);
|
|
176
|
+
}
|
|
177
|
+
}, true);
|
|
178
|
+
document.addEventListener("pointerup", event => {
|
|
179
|
+
if (!drag || event.pointerId !== drag.pointerId) return;
|
|
180
|
+
event.preventDefault(); event.stopImmediatePropagation();
|
|
181
|
+
updatePointer(event);
|
|
182
|
+
const distance = Math.round(Math.abs(pointOf(drag.end)[drag.start.axis] - pointOf(drag.start)[drag.start.axis]));
|
|
183
|
+
if (distance > 0) {
|
|
184
|
+
measurements.push(drag);
|
|
185
|
+
announcement.textContent = `${distance} pixels ${drag.start.axis === "y" ? "vertically" : "horizontally"}`;
|
|
186
|
+
}
|
|
187
|
+
drag = null;
|
|
188
|
+
if (document.documentElement.hasPointerCapture(event.pointerId)) document.documentElement.releasePointerCapture(event.pointerId);
|
|
189
|
+
render();
|
|
190
|
+
}, true);
|
|
191
|
+
document.addEventListener("click", event => {
|
|
192
|
+
if (isHostEvent(event)) { event.preventDefault(); event.stopImmediatePropagation(); }
|
|
193
|
+
}, true);
|
|
194
|
+
document.addEventListener("keydown", event => {
|
|
195
|
+
if (!enabled || event.key !== "Escape" || event.target.closest?.(".comty-auth-dialog, input, textarea, [contenteditable=true]")) return;
|
|
196
|
+
event.preventDefault(); event.stopImmediatePropagation();
|
|
197
|
+
if (drag) cancel(); else onExit();
|
|
198
|
+
}, true);
|
|
199
|
+
document.addEventListener("pointercancel", cancel, true);
|
|
200
|
+
document.documentElement.addEventListener("lostpointercapture", event => { if (drag?.pointerId === event.pointerId) cancel(); });
|
|
201
|
+
document.documentElement.addEventListener("pointerleave", () => { if (!drag) cancel(); });
|
|
202
|
+
window.addEventListener("blur", cancel);
|
|
203
|
+
document.addEventListener("scroll", () => {
|
|
204
|
+
if (!enabled) return;
|
|
205
|
+
if (pointer) updatePointer({ clientX: pointer.x, clientY: pointer.y });
|
|
206
|
+
else render();
|
|
207
|
+
}, { capture: true, passive: true });
|
|
208
|
+
return { setEnabled, render };
|
|
209
|
+
}
|