gpucanvas 0.3.1 → 0.4.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/README.md +126 -7
- package/dist/{chunk-RBXGH44N.js → chunk-BQQVSR4B.js} +661 -9
- package/dist/chunk-BQQVSR4B.js.map +1 -0
- package/dist/{editor-BSRqjhN8.d.ts → editor-D87kaZBM.d.ts} +10 -1
- package/dist/index.d.ts +165 -3
- package/dist/index.js +17 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/package.json +5 -3
- package/dist/chunk-RBXGH44N.js.map +0 -1
|
@@ -245,6 +245,38 @@ fn effect(input: EffectInput) -> vec4f {
|
|
|
245
245
|
),
|
|
246
246
|
css: ([progress]) => `opacity(${1 - progress})`
|
|
247
247
|
});
|
|
248
|
+
var fade = defineEffect({
|
|
249
|
+
name: "fade",
|
|
250
|
+
params: [0.5],
|
|
251
|
+
wgsl: (
|
|
252
|
+
/* wgsl */
|
|
253
|
+
`
|
|
254
|
+
fn effect(input: EffectInput) -> vec4f {
|
|
255
|
+
return source(input.uv) * (1.0 - input.params.x);
|
|
256
|
+
}`
|
|
257
|
+
),
|
|
258
|
+
css: ([progress]) => `opacity(${1 - progress})`
|
|
259
|
+
});
|
|
260
|
+
var wipe = defineEffect({
|
|
261
|
+
name: "wipe",
|
|
262
|
+
params: [0.5, 0, 32],
|
|
263
|
+
wgsl: (
|
|
264
|
+
/* wgsl */
|
|
265
|
+
`
|
|
266
|
+
fn effect(input: EffectInput) -> vec4f {
|
|
267
|
+
let angle = radians(input.params.y);
|
|
268
|
+
let toward = vec2f(cos(angle), sin(angle));
|
|
269
|
+
let half = input.size * 0.5;
|
|
270
|
+
let extent = abs(toward.x) * half.x + abs(toward.y) * half.y;
|
|
271
|
+
// 0 where the wipe starts, 1 where it ends.
|
|
272
|
+
let along = (dot(input.uv * input.size - half, toward) / extent + 1.0) * 0.5;
|
|
273
|
+
let soft = max(input.params.z, 0.5) / (2.0 * extent);
|
|
274
|
+
let edge = input.params.x * (1.0 + soft);
|
|
275
|
+
return source(input.uv) * smoothstep(edge - soft, edge, along);
|
|
276
|
+
}`
|
|
277
|
+
),
|
|
278
|
+
css: ([progress]) => `opacity(${1 - progress})`
|
|
279
|
+
});
|
|
248
280
|
var glass = defineEffect({
|
|
249
281
|
name: "glass",
|
|
250
282
|
animated: true,
|
|
@@ -437,6 +469,8 @@ var EFFECTS = {
|
|
|
437
469
|
blur,
|
|
438
470
|
pixelate,
|
|
439
471
|
dissolve,
|
|
472
|
+
fade,
|
|
473
|
+
wipe,
|
|
440
474
|
grayscale
|
|
441
475
|
};
|
|
442
476
|
|
|
@@ -683,6 +717,7 @@ function createDomCanvas(host, options) {
|
|
|
683
717
|
// The browser repaints DOM nodes itself.
|
|
684
718
|
invalidate() {
|
|
685
719
|
},
|
|
720
|
+
snapshot: () => Promise.reject(new Error("snapshot needs the GPU backend (WebGPU and HTML-in-Canvas)")),
|
|
686
721
|
setBackground(color) {
|
|
687
722
|
surface.style.background = color;
|
|
688
723
|
},
|
|
@@ -1479,7 +1514,8 @@ async function createGpuCanvas(host, options) {
|
|
|
1479
1514
|
});
|
|
1480
1515
|
resizeObserver.observe(canvas, { box: "device-pixel-content-box" });
|
|
1481
1516
|
const transitioning = /* @__PURE__ */ new Map();
|
|
1482
|
-
const
|
|
1517
|
+
const captures = [];
|
|
1518
|
+
const isPinned = (page) => captures.some((capture) => capture.node.page === page) || live?.page === page || lingering?.page === page || focused?.page === page || [...transitioning.keys()].some((node) => node.page === page);
|
|
1483
1519
|
let detailScale = 1;
|
|
1484
1520
|
let frameView = { x: 0, y: 0, width: 0, height: 0 };
|
|
1485
1521
|
let contentTimer = 0;
|
|
@@ -1805,10 +1841,10 @@ async function createGpuCanvas(host, options) {
|
|
|
1805
1841
|
return pending;
|
|
1806
1842
|
};
|
|
1807
1843
|
const composite = () => {
|
|
1808
|
-
let
|
|
1809
|
-
for (const node of visible)
|
|
1810
|
-
|
|
1811
|
-
renderer.reserve(
|
|
1844
|
+
let instances2 = visible.length;
|
|
1845
|
+
for (const node of visible) instances2 += node.tiles.length + (node.preview ? 1 : 0);
|
|
1846
|
+
instances2 += selection.size + 1;
|
|
1847
|
+
renderer.reserve(instances2);
|
|
1812
1848
|
const data = renderer.instances;
|
|
1813
1849
|
let count = 0;
|
|
1814
1850
|
runs.length = 0;
|
|
@@ -1960,7 +1996,67 @@ async function createGpuCanvas(host, options) {
|
|
|
1960
1996
|
if (pending > 0 || effectsMoving) schedule();
|
|
1961
1997
|
};
|
|
1962
1998
|
let awaitingPaint = false;
|
|
1999
|
+
const drawCaptures = () => {
|
|
2000
|
+
for (const capture of [...captures]) {
|
|
2001
|
+
const { node, scale } = capture;
|
|
2002
|
+
const width = Math.max(1, Math.round(node.rect.width * scale));
|
|
2003
|
+
const height = Math.max(1, Math.round(node.rect.height * scale));
|
|
2004
|
+
const texture = device.createTexture({
|
|
2005
|
+
size: { width, height },
|
|
2006
|
+
format: "rgba8unorm",
|
|
2007
|
+
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
|
2008
|
+
});
|
|
2009
|
+
try {
|
|
2010
|
+
device.queue.drawElementImageToTexture(
|
|
2011
|
+
{
|
|
2012
|
+
source: node.page.element,
|
|
2013
|
+
sourceX: node.pageX,
|
|
2014
|
+
sourceY: node.pageY,
|
|
2015
|
+
sourceWidth: node.rect.width,
|
|
2016
|
+
sourceHeight: node.rect.height
|
|
2017
|
+
},
|
|
2018
|
+
{ texture, size: { width, height }, premultipliedAlpha: false }
|
|
2019
|
+
);
|
|
2020
|
+
} catch (error) {
|
|
2021
|
+
texture.destroy();
|
|
2022
|
+
if (++capture.tries > 20) {
|
|
2023
|
+
captures.splice(captures.indexOf(capture), 1);
|
|
2024
|
+
capture.reject(error instanceof Error ? error : new Error(String(error)));
|
|
2025
|
+
}
|
|
2026
|
+
continue;
|
|
2027
|
+
}
|
|
2028
|
+
captures.splice(captures.indexOf(capture), 1);
|
|
2029
|
+
readTexture(texture, width, height).then(capture.resolve, capture.reject);
|
|
2030
|
+
}
|
|
2031
|
+
if (captures.length) {
|
|
2032
|
+
awaitingPaint = true;
|
|
2033
|
+
canvas.requestPaint();
|
|
2034
|
+
} else schedule();
|
|
2035
|
+
};
|
|
2036
|
+
const readTexture = async (texture, width, height) => {
|
|
2037
|
+
const bytesPerRow = Math.ceil(width * 4 / 256) * 256;
|
|
2038
|
+
const buffer = device.createBuffer({
|
|
2039
|
+
size: bytesPerRow * height,
|
|
2040
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
2041
|
+
});
|
|
2042
|
+
const encoder = device.createCommandEncoder();
|
|
2043
|
+
encoder.copyTextureToBuffer({ texture }, { buffer, bytesPerRow }, { width, height });
|
|
2044
|
+
device.queue.submit([encoder.finish()]);
|
|
2045
|
+
try {
|
|
2046
|
+
await buffer.mapAsync(GPUMapMode.READ);
|
|
2047
|
+
const rows = new Uint8Array(buffer.getMappedRange());
|
|
2048
|
+
const pixels = new Uint8ClampedArray(width * height * 4);
|
|
2049
|
+
for (let y = 0; y < height; y++) {
|
|
2050
|
+
pixels.set(rows.subarray(y * bytesPerRow, y * bytesPerRow + width * 4), y * width * 4);
|
|
2051
|
+
}
|
|
2052
|
+
return new ImageData(pixels, width, height);
|
|
2053
|
+
} finally {
|
|
2054
|
+
buffer.destroy();
|
|
2055
|
+
texture.destroy();
|
|
2056
|
+
}
|
|
2057
|
+
};
|
|
1963
2058
|
canvas.onpaint = (event) => {
|
|
2059
|
+
if (captures.length) drawCaptures();
|
|
1964
2060
|
let changed = false;
|
|
1965
2061
|
for (const element of event.changedElements) {
|
|
1966
2062
|
const page = pageByElement.get(element);
|
|
@@ -2359,6 +2455,19 @@ async function createGpuCanvas(host, options) {
|
|
|
2359
2455
|
getCamera: () => camera,
|
|
2360
2456
|
setCamera,
|
|
2361
2457
|
invalidate: onFontsLoaded,
|
|
2458
|
+
snapshot(element, options2 = {}) {
|
|
2459
|
+
const node = nodes.get(element);
|
|
2460
|
+
if (!node) return Promise.reject(new Error("Not a node on this canvas"));
|
|
2461
|
+
const largest = device.limits.maxTextureDimension2D;
|
|
2462
|
+
const wanted = options2.scale ?? devicePixelRatio;
|
|
2463
|
+
const scale = Math.min(wanted, largest / node.rect.width, largest / node.rect.height);
|
|
2464
|
+
return new Promise((resolve, reject) => {
|
|
2465
|
+
captures.push({ node, scale, resolve, reject, tries: 0 });
|
|
2466
|
+
node.page.setDrawable(true, frameNumber);
|
|
2467
|
+
awaitingPaint = true;
|
|
2468
|
+
canvas.requestPaint();
|
|
2469
|
+
});
|
|
2470
|
+
},
|
|
2362
2471
|
setBackground(color) {
|
|
2363
2472
|
[renderer.background, renderer.placeholder] = colorsFor(color);
|
|
2364
2473
|
schedule();
|
|
@@ -2413,6 +2522,155 @@ async function createGpuCanvas(host, options) {
|
|
|
2413
2522
|
};
|
|
2414
2523
|
}
|
|
2415
2524
|
|
|
2525
|
+
// src/agent.ts
|
|
2526
|
+
var IMPLICIT_ROLES = {
|
|
2527
|
+
a: "link",
|
|
2528
|
+
button: "button",
|
|
2529
|
+
img: "img",
|
|
2530
|
+
input: "textbox",
|
|
2531
|
+
textarea: "textbox",
|
|
2532
|
+
select: "combobox",
|
|
2533
|
+
h1: "heading",
|
|
2534
|
+
h2: "heading",
|
|
2535
|
+
h3: "heading",
|
|
2536
|
+
h4: "heading",
|
|
2537
|
+
h5: "heading",
|
|
2538
|
+
h6: "heading",
|
|
2539
|
+
nav: "navigation",
|
|
2540
|
+
ul: "list",
|
|
2541
|
+
ol: "list",
|
|
2542
|
+
li: "listitem",
|
|
2543
|
+
table: "table",
|
|
2544
|
+
dialog: "dialog"
|
|
2545
|
+
};
|
|
2546
|
+
var INPUT_ROLES = {
|
|
2547
|
+
checkbox: "checkbox",
|
|
2548
|
+
radio: "radio",
|
|
2549
|
+
range: "slider",
|
|
2550
|
+
button: "button",
|
|
2551
|
+
submit: "button",
|
|
2552
|
+
reset: "button",
|
|
2553
|
+
search: "searchbox"
|
|
2554
|
+
};
|
|
2555
|
+
var SKIP = /* @__PURE__ */ new Set(["script", "style", "template", "noscript"]);
|
|
2556
|
+
var MAX_TEXT = 120;
|
|
2557
|
+
var ownText = (element) => {
|
|
2558
|
+
let text = "";
|
|
2559
|
+
for (const child of element.childNodes) {
|
|
2560
|
+
if (child.nodeType === Node.TEXT_NODE) text += child.textContent;
|
|
2561
|
+
}
|
|
2562
|
+
text = text.replace(/\s+/g, " ").trim();
|
|
2563
|
+
return text.length > MAX_TEXT ? `${text.slice(0, MAX_TEXT - 1)}\u2026` : text;
|
|
2564
|
+
};
|
|
2565
|
+
var labelText = (element) => {
|
|
2566
|
+
const label = element.labels?.[0];
|
|
2567
|
+
if (!label) return "";
|
|
2568
|
+
let text = "";
|
|
2569
|
+
for (const node of label.childNodes) {
|
|
2570
|
+
if (node !== element && !(node instanceof Element && node.contains(element))) {
|
|
2571
|
+
text += node.textContent;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
return text.replace(/\s+/g, " ").trim();
|
|
2575
|
+
};
|
|
2576
|
+
var sameRect = (a, b) => Math.abs(a.x - b.x) < 0.5 && Math.abs(a.y - b.y) < 0.5 && Math.abs(a.width - b.width) < 0.5 && Math.abs(a.height - b.height) < 0.5;
|
|
2577
|
+
var round = (rect) => ({
|
|
2578
|
+
x: Math.round(rect.x),
|
|
2579
|
+
y: Math.round(rect.y),
|
|
2580
|
+
width: Math.round(rect.width),
|
|
2581
|
+
height: Math.round(rect.height)
|
|
2582
|
+
});
|
|
2583
|
+
function createAgentView(canvas) {
|
|
2584
|
+
const ids = /* @__PURE__ */ new WeakMap();
|
|
2585
|
+
const elements = /* @__PURE__ */ new Map();
|
|
2586
|
+
let next = 1;
|
|
2587
|
+
const idOf = (element) => {
|
|
2588
|
+
let id = ids.get(element);
|
|
2589
|
+
if (!id) {
|
|
2590
|
+
id = `e${next++}`;
|
|
2591
|
+
ids.set(element, id);
|
|
2592
|
+
elements.set(id, new WeakRef(element));
|
|
2593
|
+
}
|
|
2594
|
+
return id;
|
|
2595
|
+
};
|
|
2596
|
+
const read = (element, depth, options) => {
|
|
2597
|
+
const tag = element.localName;
|
|
2598
|
+
if (SKIP.has(tag)) return [];
|
|
2599
|
+
const rect = canvas.getElementRect(element);
|
|
2600
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) return [];
|
|
2601
|
+
const field = element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement ? element : null;
|
|
2602
|
+
const input = element instanceof HTMLInputElement ? element : null;
|
|
2603
|
+
const toggles = input?.type === "checkbox" || input?.type === "radio";
|
|
2604
|
+
const role = element.getAttribute("role") ?? (input ? INPUT_ROLES[input.type] ?? "textbox" : IMPLICIT_ROLES[tag]);
|
|
2605
|
+
const name = element.getAttribute("aria-label") ?? element.getAttribute("alt") ?? element.getAttribute("title") ?? (field && (labelText(field) || field.getAttribute("placeholder")) || options.name?.(element) || void 0);
|
|
2606
|
+
const text = ownText(element) || void 0;
|
|
2607
|
+
const value = field && !toggles ? field.value || void 0 : void 0;
|
|
2608
|
+
const checked = toggles ? input.checked : void 0;
|
|
2609
|
+
const meaningful = !!(role || name || text || value);
|
|
2610
|
+
const below = meaningful ? depth - 1 : depth;
|
|
2611
|
+
const children = below >= 0 && tag !== "svg" ? [...element.children].flatMap((child) => read(child, below, options)) : [];
|
|
2612
|
+
if (!meaningful && tag !== "svg") {
|
|
2613
|
+
if (children.length === 0) return [];
|
|
2614
|
+
if (children.length === 1 && sameRect(children[0].rect, rect)) return children;
|
|
2615
|
+
}
|
|
2616
|
+
const described = { id: idOf(element), tag, rect: round(rect) };
|
|
2617
|
+
if (role ?? tag === "svg") described.role = role ?? "img";
|
|
2618
|
+
if (name) described.name = name;
|
|
2619
|
+
if (text) described.text = text;
|
|
2620
|
+
if (value) described.value = value;
|
|
2621
|
+
if (checked !== void 0) described.checked = checked;
|
|
2622
|
+
if (children.length) described.children = children;
|
|
2623
|
+
return [described];
|
|
2624
|
+
};
|
|
2625
|
+
return {
|
|
2626
|
+
/** What is in `rect` (default: the view), node by node, as plain JSON. */
|
|
2627
|
+
describe(options = {}) {
|
|
2628
|
+
const area = options.rect ?? visibleWorldRect(
|
|
2629
|
+
canvas.getCamera(),
|
|
2630
|
+
canvas.surface.clientWidth,
|
|
2631
|
+
canvas.surface.clientHeight
|
|
2632
|
+
);
|
|
2633
|
+
return canvas.getNodesIn(area).flatMap((node) => read(node, options.depth ?? 6, options));
|
|
2634
|
+
},
|
|
2635
|
+
/** The element behind an id from `describe`, or null once it is gone. */
|
|
2636
|
+
element(id) {
|
|
2637
|
+
const element = elements.get(id)?.deref();
|
|
2638
|
+
return element?.isConnected ? element : null;
|
|
2639
|
+
},
|
|
2640
|
+
/** Where an element (or the element behind an id) sits, in world units. */
|
|
2641
|
+
rect(target) {
|
|
2642
|
+
const element = typeof target === "string" ? this.element(target) : target;
|
|
2643
|
+
return element ? canvas.getElementRect(element) : null;
|
|
2644
|
+
},
|
|
2645
|
+
/**
|
|
2646
|
+
* An image of a world rect (an element's box when given one) as the nodes look now, at
|
|
2647
|
+
* `scale` texels per world unit. GPU backend only.
|
|
2648
|
+
*/
|
|
2649
|
+
async capture(target, { scale = 1, background } = {}) {
|
|
2650
|
+
const area = typeof target === "object" && "width" in target ? target : this.rect(target);
|
|
2651
|
+
if (!area) throw new Error("Nothing to capture");
|
|
2652
|
+
const fit = Math.min(scale, 8192 / area.width, 8192 / area.height);
|
|
2653
|
+
const width = Math.max(1, Math.round(area.width * fit));
|
|
2654
|
+
const height = Math.max(1, Math.round(area.height * fit));
|
|
2655
|
+
const out = new OffscreenCanvas(width, height);
|
|
2656
|
+
const context = out.getContext("2d");
|
|
2657
|
+
if (background) {
|
|
2658
|
+
context.fillStyle = background;
|
|
2659
|
+
context.fillRect(0, 0, width, height);
|
|
2660
|
+
}
|
|
2661
|
+
const nodes = canvas.getNodesIn(area);
|
|
2662
|
+
const images = await Promise.all(nodes.map((node) => canvas.snapshot(node, { scale: fit })));
|
|
2663
|
+
nodes.forEach((node, i) => {
|
|
2664
|
+
const rect = canvas.getNodeRect(node);
|
|
2665
|
+
const bitmap = new OffscreenCanvas(images[i].width, images[i].height);
|
|
2666
|
+
bitmap.getContext("2d").putImageData(images[i], 0, 0);
|
|
2667
|
+
context.drawImage(bitmap, (rect.x - area.x) * fit, (rect.y - area.y) * fit);
|
|
2668
|
+
});
|
|
2669
|
+
return context.getImageData(0, 0, width, height);
|
|
2670
|
+
}
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2416
2674
|
// src/layers.ts
|
|
2417
2675
|
var sameLayer = (a, b) => !!a && !!b && a[0] === b[0];
|
|
2418
2676
|
var layerIn = (layer, list) => list.some((other) => sameLayer(layer, other));
|
|
@@ -2434,7 +2692,7 @@ function unionRect(a, b) {
|
|
|
2434
2692
|
height: Math.max(a.y + a.height, b.y + b.height) - y
|
|
2435
2693
|
};
|
|
2436
2694
|
}
|
|
2437
|
-
var
|
|
2695
|
+
var sameRect2 = (a, b) => !!a && !!b && Math.abs(a.x - b.x) < 0.5 && Math.abs(a.y - b.y) < 0.5 && Math.abs(a.width - b.width) < 0.5 && Math.abs(a.height - b.height) < 0.5;
|
|
2438
2696
|
function createLayers(canvas, pick) {
|
|
2439
2697
|
const chainOf = (element) => {
|
|
2440
2698
|
const node = canvas.getNodeOf(element);
|
|
@@ -2453,7 +2711,7 @@ function createLayers(canvas, pick) {
|
|
|
2453
2711
|
let previous = canvas.getNodeRect(node);
|
|
2454
2712
|
for (const layer of inner) {
|
|
2455
2713
|
const rect = layerRect(canvas, layer);
|
|
2456
|
-
if (
|
|
2714
|
+
if (sameRect2(rect, previous)) continue;
|
|
2457
2715
|
chain.push(layer);
|
|
2458
2716
|
previous = rect;
|
|
2459
2717
|
}
|
|
@@ -2471,7 +2729,7 @@ function createLayers(canvas, pick) {
|
|
|
2471
2729
|
for (const child of element.children) {
|
|
2472
2730
|
const rect = canvas.getElementRect(child);
|
|
2473
2731
|
if (!rect || rect.width <= 0 || rect.height <= 0) continue;
|
|
2474
|
-
if (
|
|
2732
|
+
if (sameRect2(rect, outer)) visit(child, outer);
|
|
2475
2733
|
else children.push([child]);
|
|
2476
2734
|
}
|
|
2477
2735
|
};
|
|
@@ -3096,6 +3354,392 @@ function bindEditor(canvas, options = {}) {
|
|
|
3096
3354
|
};
|
|
3097
3355
|
}
|
|
3098
3356
|
|
|
3357
|
+
// src/marks.ts
|
|
3358
|
+
var SVG = "http://www.w3.org/2000/svg";
|
|
3359
|
+
var FONT = "500 11px/16px system-ui, sans-serif";
|
|
3360
|
+
var instances = 0;
|
|
3361
|
+
var pillStyle = (color) => ({
|
|
3362
|
+
position: "absolute",
|
|
3363
|
+
left: "0",
|
|
3364
|
+
top: "0",
|
|
3365
|
+
font: FONT,
|
|
3366
|
+
padding: "0 6px",
|
|
3367
|
+
borderRadius: "4px",
|
|
3368
|
+
color: "#fff",
|
|
3369
|
+
background: color,
|
|
3370
|
+
whiteSpace: "nowrap",
|
|
3371
|
+
pointerEvents: "none"
|
|
3372
|
+
});
|
|
3373
|
+
function createMarks(canvas, { color = "#4f8cff" } = {}) {
|
|
3374
|
+
const id = `gpucanvas-marks-${++instances}`;
|
|
3375
|
+
const root = document.createElement("div");
|
|
3376
|
+
Object.assign(root.style, {
|
|
3377
|
+
position: "absolute",
|
|
3378
|
+
inset: "0",
|
|
3379
|
+
overflow: "hidden",
|
|
3380
|
+
pointerEvents: "none",
|
|
3381
|
+
zIndex: "1"
|
|
3382
|
+
});
|
|
3383
|
+
const svg = document.createElementNS(SVG, "svg");
|
|
3384
|
+
Object.assign(svg.style, { position: "absolute", inset: "0", width: "100%", height: "100%" });
|
|
3385
|
+
const defs = document.createElementNS(SVG, "defs");
|
|
3386
|
+
svg.append(defs);
|
|
3387
|
+
root.append(svg);
|
|
3388
|
+
canvas.surface.append(root);
|
|
3389
|
+
const arrows = /* @__PURE__ */ new Map();
|
|
3390
|
+
const arrowFor = (stroke) => {
|
|
3391
|
+
let ref = arrows.get(stroke);
|
|
3392
|
+
if (!ref) {
|
|
3393
|
+
ref = `${id}-arrow-${arrows.size}`;
|
|
3394
|
+
const marker = document.createElementNS(SVG, "marker");
|
|
3395
|
+
for (const [key, value] of Object.entries({
|
|
3396
|
+
id: ref,
|
|
3397
|
+
viewBox: "0 0 10 10",
|
|
3398
|
+
refX: "8",
|
|
3399
|
+
refY: "5",
|
|
3400
|
+
markerWidth: "7",
|
|
3401
|
+
markerHeight: "7",
|
|
3402
|
+
orient: "auto"
|
|
3403
|
+
})) {
|
|
3404
|
+
marker.setAttribute(key, value);
|
|
3405
|
+
}
|
|
3406
|
+
const head = document.createElementNS(SVG, "path");
|
|
3407
|
+
head.setAttribute("d", "M0,1 L9,5 L0,9 z");
|
|
3408
|
+
head.setAttribute("fill", stroke);
|
|
3409
|
+
marker.append(head);
|
|
3410
|
+
defs.append(marker);
|
|
3411
|
+
arrows.set(stroke, ref);
|
|
3412
|
+
}
|
|
3413
|
+
return ref;
|
|
3414
|
+
};
|
|
3415
|
+
const toScreen = (point) => canvas.worldToScreen(point);
|
|
3416
|
+
const screenRect = (rect) => {
|
|
3417
|
+
const a = toScreen(rect);
|
|
3418
|
+
const b = toScreen({ x: rect.x + rect.width, y: rect.y + rect.height });
|
|
3419
|
+
return { x: a.x, y: a.y, width: b.x - a.x, height: b.y - a.y };
|
|
3420
|
+
};
|
|
3421
|
+
const worldRect = (target) => target instanceof Element ? canvas.getElementRect(target) : { x: target.x, y: target.y, width: 0, height: 0 };
|
|
3422
|
+
const place = (element, x, y) => {
|
|
3423
|
+
const transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)`;
|
|
3424
|
+
if (element.style.transform !== transform) element.style.transform = transform;
|
|
3425
|
+
};
|
|
3426
|
+
const show = (element, visible) => {
|
|
3427
|
+
const display = visible ? "" : "none";
|
|
3428
|
+
if (element.style.display !== display) element.style.display = display;
|
|
3429
|
+
};
|
|
3430
|
+
const div = (style, text) => {
|
|
3431
|
+
const element = document.createElement("div");
|
|
3432
|
+
Object.assign(element.style, style);
|
|
3433
|
+
if (text) element.textContent = text;
|
|
3434
|
+
root.append(element);
|
|
3435
|
+
return element;
|
|
3436
|
+
};
|
|
3437
|
+
const marks = /* @__PURE__ */ new Set();
|
|
3438
|
+
let frame = 0;
|
|
3439
|
+
const loop = () => {
|
|
3440
|
+
frame = marks.size ? requestAnimationFrame(loop) : 0;
|
|
3441
|
+
for (const draw of marks) draw();
|
|
3442
|
+
};
|
|
3443
|
+
const add = (draw, remove) => {
|
|
3444
|
+
marks.add(draw);
|
|
3445
|
+
if (!frame) frame = requestAnimationFrame(loop);
|
|
3446
|
+
return {
|
|
3447
|
+
remove() {
|
|
3448
|
+
marks.delete(draw);
|
|
3449
|
+
remove();
|
|
3450
|
+
}
|
|
3451
|
+
};
|
|
3452
|
+
};
|
|
3453
|
+
return {
|
|
3454
|
+
/** An arrow from one element (or world point) to another, with an optional label. */
|
|
3455
|
+
connect(from, to, options = {}) {
|
|
3456
|
+
const stroke = options.color ?? color;
|
|
3457
|
+
const path = document.createElementNS(SVG, "path");
|
|
3458
|
+
path.setAttribute("fill", "none");
|
|
3459
|
+
path.setAttribute("stroke", stroke);
|
|
3460
|
+
path.setAttribute("stroke-width", "2");
|
|
3461
|
+
path.setAttribute("stroke-linecap", "round");
|
|
3462
|
+
if (options.dashed) path.setAttribute("stroke-dasharray", "6 5");
|
|
3463
|
+
path.setAttribute("marker-end", `url(#${arrowFor(stroke)})`);
|
|
3464
|
+
svg.append(path);
|
|
3465
|
+
const label = options.label ? div(pillStyle(stroke), options.label) : null;
|
|
3466
|
+
return add(
|
|
3467
|
+
() => {
|
|
3468
|
+
const a = worldRect(from);
|
|
3469
|
+
const b = worldRect(to);
|
|
3470
|
+
show(path, !!a && !!b);
|
|
3471
|
+
if (label) show(label, !!a && !!b);
|
|
3472
|
+
if (!a || !b) return;
|
|
3473
|
+
const p = screenRect(a);
|
|
3474
|
+
const q = screenRect(b);
|
|
3475
|
+
const dx = q.x + q.width / 2 - (p.x + p.width / 2);
|
|
3476
|
+
const dy = q.y + q.height / 2 - (p.y + p.height / 2);
|
|
3477
|
+
const across = Math.abs(dx) >= Math.abs(dy);
|
|
3478
|
+
const start = across ? { x: dx > 0 ? p.x + p.width : p.x, y: p.y + p.height / 2 } : { x: p.x + p.width / 2, y: dy > 0 ? p.y + p.height : p.y };
|
|
3479
|
+
const end = across ? { x: dx > 0 ? q.x - 2 : q.x + q.width + 2, y: q.y + q.height / 2 } : { x: q.x + q.width / 2, y: dy > 0 ? q.y - 2 : q.y + q.height + 2 };
|
|
3480
|
+
const bend = Math.max(
|
|
3481
|
+
30,
|
|
3482
|
+
(across ? Math.abs(end.x - start.x) : Math.abs(end.y - start.y)) / 2
|
|
3483
|
+
);
|
|
3484
|
+
const c1 = across ? { x: start.x + Math.sign(dx) * bend, y: start.y } : { x: start.x, y: start.y + Math.sign(dy) * bend };
|
|
3485
|
+
const c2 = across ? { x: end.x - Math.sign(dx) * bend, y: end.y } : { x: end.x, y: end.y - Math.sign(dy) * bend };
|
|
3486
|
+
const d = `M${start.x.toFixed(1)},${start.y.toFixed(1)} C${c1.x.toFixed(1)},${c1.y.toFixed(1)} ${c2.x.toFixed(1)},${c2.y.toFixed(1)} ${end.x.toFixed(1)},${end.y.toFixed(1)}`;
|
|
3487
|
+
if (path.getAttribute("d") !== d) path.setAttribute("d", d);
|
|
3488
|
+
if (label) {
|
|
3489
|
+
const mx = (start.x + 3 * c1.x + 3 * c2.x + end.x) / 8;
|
|
3490
|
+
const my = (start.y + 3 * c1.y + 3 * c2.y + end.y) / 8;
|
|
3491
|
+
place(label, mx - label.offsetWidth / 2, my - 8);
|
|
3492
|
+
}
|
|
3493
|
+
},
|
|
3494
|
+
() => {
|
|
3495
|
+
path.remove();
|
|
3496
|
+
label?.remove();
|
|
3497
|
+
}
|
|
3498
|
+
);
|
|
3499
|
+
},
|
|
3500
|
+
/**
|
|
3501
|
+
* A comment pin at an element's top right corner, or at a world point. Its text shows on
|
|
3502
|
+
* hover; `onClick` makes it a button.
|
|
3503
|
+
*/
|
|
3504
|
+
pin(target, options = {}) {
|
|
3505
|
+
const fill = options.color ?? color;
|
|
3506
|
+
const dot = div(
|
|
3507
|
+
{
|
|
3508
|
+
position: "absolute",
|
|
3509
|
+
left: "0",
|
|
3510
|
+
top: "0",
|
|
3511
|
+
width: "24px",
|
|
3512
|
+
height: "24px",
|
|
3513
|
+
borderRadius: "12px 12px 12px 2px",
|
|
3514
|
+
background: fill,
|
|
3515
|
+
border: "2px solid #fff",
|
|
3516
|
+
boxShadow: "0 1px 4px rgb(0 0 0 / 0.25)",
|
|
3517
|
+
color: "#fff",
|
|
3518
|
+
font: "600 11px/20px system-ui, sans-serif",
|
|
3519
|
+
textAlign: "center",
|
|
3520
|
+
boxSizing: "border-box",
|
|
3521
|
+
pointerEvents: "auto",
|
|
3522
|
+
cursor: options.onClick ? "pointer" : "default"
|
|
3523
|
+
},
|
|
3524
|
+
options.label ?? ""
|
|
3525
|
+
);
|
|
3526
|
+
if (options.text) dot.title = options.text;
|
|
3527
|
+
if (options.onClick) dot.addEventListener("click", options.onClick);
|
|
3528
|
+
return add(
|
|
3529
|
+
() => {
|
|
3530
|
+
const rect = worldRect(target);
|
|
3531
|
+
show(dot, !!rect);
|
|
3532
|
+
if (!rect) return;
|
|
3533
|
+
const r = screenRect(rect);
|
|
3534
|
+
place(dot, r.x + r.width - 4, r.y - 20);
|
|
3535
|
+
},
|
|
3536
|
+
() => dot.remove()
|
|
3537
|
+
);
|
|
3538
|
+
},
|
|
3539
|
+
/** A dashed frame around an element with a note above it: what someone is pointing at. */
|
|
3540
|
+
note(target, text, options = {}) {
|
|
3541
|
+
const stroke = options.color ?? "#f59e0b";
|
|
3542
|
+
const frameBox = div({
|
|
3543
|
+
position: "absolute",
|
|
3544
|
+
left: "0",
|
|
3545
|
+
top: "0",
|
|
3546
|
+
border: `2px dashed ${stroke}`,
|
|
3547
|
+
borderRadius: "6px",
|
|
3548
|
+
boxSizing: "border-box"
|
|
3549
|
+
});
|
|
3550
|
+
const label = div({ ...pillStyle(stroke), maxWidth: "320px", whiteSpace: "normal" }, text);
|
|
3551
|
+
return add(
|
|
3552
|
+
() => {
|
|
3553
|
+
const rect = worldRect(target);
|
|
3554
|
+
show(frameBox, !!rect);
|
|
3555
|
+
show(label, !!rect);
|
|
3556
|
+
if (!rect) return;
|
|
3557
|
+
const r = screenRect(rect);
|
|
3558
|
+
place(frameBox, r.x - 4, r.y - 4);
|
|
3559
|
+
const width = `${Math.round(r.width + 8)}px`;
|
|
3560
|
+
const height = `${Math.round(r.height + 8)}px`;
|
|
3561
|
+
if (frameBox.style.width !== width) frameBox.style.width = width;
|
|
3562
|
+
if (frameBox.style.height !== height) frameBox.style.height = height;
|
|
3563
|
+
place(label, r.x - 4, r.y - 8 - label.offsetHeight);
|
|
3564
|
+
},
|
|
3565
|
+
() => {
|
|
3566
|
+
frameBox.remove();
|
|
3567
|
+
label.remove();
|
|
3568
|
+
}
|
|
3569
|
+
);
|
|
3570
|
+
},
|
|
3571
|
+
/** Someone's cursor, with their name. It glides to each point given to `move`. */
|
|
3572
|
+
cursor(name, options = {}) {
|
|
3573
|
+
const fill = options.color ?? color;
|
|
3574
|
+
const box = div({ position: "absolute", left: "0", top: "0", display: "none" });
|
|
3575
|
+
box.innerHTML = `<svg width="18" height="20" viewBox="0 0 18 20" style="display:block"><path d="M1 1 L1 16 L5 12 L8 19 L11 18 L8 11 L14 11 Z" fill="${fill}" stroke="#fff" stroke-width="1.5" stroke-linejoin="round"/></svg>`;
|
|
3576
|
+
const tag = document.createElement("div");
|
|
3577
|
+
Object.assign(tag.style, pillStyle(fill), { position: "relative", marginLeft: "12px" });
|
|
3578
|
+
tag.textContent = name;
|
|
3579
|
+
box.append(tag);
|
|
3580
|
+
let target = null;
|
|
3581
|
+
let at = null;
|
|
3582
|
+
const mark = add(
|
|
3583
|
+
() => {
|
|
3584
|
+
show(box, !!target);
|
|
3585
|
+
if (!target) return;
|
|
3586
|
+
at = at ? { x: at.x + (target.x - at.x) * 0.25, y: at.y + (target.y - at.y) * 0.25 } : target;
|
|
3587
|
+
const p = toScreen(at);
|
|
3588
|
+
place(box, p.x, p.y);
|
|
3589
|
+
},
|
|
3590
|
+
() => box.remove()
|
|
3591
|
+
);
|
|
3592
|
+
return {
|
|
3593
|
+
move(point) {
|
|
3594
|
+
target = point;
|
|
3595
|
+
if (!point) at = null;
|
|
3596
|
+
},
|
|
3597
|
+
remove: mark.remove
|
|
3598
|
+
};
|
|
3599
|
+
},
|
|
3600
|
+
/** Removes every mark. */
|
|
3601
|
+
clear() {
|
|
3602
|
+
for (const draw of [...marks]) marks.delete(draw);
|
|
3603
|
+
root.querySelectorAll(":scope > div").forEach((element) => element.remove());
|
|
3604
|
+
svg.querySelectorAll(":scope > path").forEach((element) => element.remove());
|
|
3605
|
+
},
|
|
3606
|
+
destroy() {
|
|
3607
|
+
cancelAnimationFrame(frame);
|
|
3608
|
+
marks.clear();
|
|
3609
|
+
root.remove();
|
|
3610
|
+
}
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
// src/snapshots.ts
|
|
3615
|
+
function toBlob(image, type = "image/png", quality) {
|
|
3616
|
+
const canvas = new OffscreenCanvas(image.width, image.height);
|
|
3617
|
+
canvas.getContext("2d").putImageData(image, 0, 0);
|
|
3618
|
+
return canvas.convertToBlob({ type, quality });
|
|
3619
|
+
}
|
|
3620
|
+
function diffImages(before, after, { threshold = 24, color = [255, 64, 96] } = {}) {
|
|
3621
|
+
const width = Math.max(before.width, after.width);
|
|
3622
|
+
const height = Math.max(before.height, after.height);
|
|
3623
|
+
const out = new Uint8ClampedArray(width * height * 4);
|
|
3624
|
+
let changed = 0;
|
|
3625
|
+
let minX = width;
|
|
3626
|
+
let minY = height;
|
|
3627
|
+
let maxX = -1;
|
|
3628
|
+
let maxY = -1;
|
|
3629
|
+
const read = (image, x, y) => x < image.width && y < image.height ? (y * image.width + x) * 4 : -1;
|
|
3630
|
+
for (let y = 0; y < height; y++) {
|
|
3631
|
+
for (let x = 0; x < width; x++) {
|
|
3632
|
+
const a = read(before, x, y);
|
|
3633
|
+
const b = read(after, x, y);
|
|
3634
|
+
const o = (y * width + x) * 4;
|
|
3635
|
+
let differs = a < 0 || b < 0;
|
|
3636
|
+
if (!differs) {
|
|
3637
|
+
for (let c = 0; c < 4 && !differs; c++) {
|
|
3638
|
+
differs = Math.abs(before.data[a + c] - after.data[b + c]) > threshold;
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
if (differs) {
|
|
3642
|
+
changed++;
|
|
3643
|
+
if (x < minX) minX = x;
|
|
3644
|
+
if (y < minY) minY = y;
|
|
3645
|
+
if (x > maxX) maxX = x;
|
|
3646
|
+
if (y > maxY) maxY = y;
|
|
3647
|
+
out[o] = color[0];
|
|
3648
|
+
out[o + 1] = color[1];
|
|
3649
|
+
out[o + 2] = color[2];
|
|
3650
|
+
out[o + 3] = 255;
|
|
3651
|
+
} else {
|
|
3652
|
+
for (let c = 0; c < 3; c++) out[o + c] = 255 - (255 - after.data[b + c]) * 0.25;
|
|
3653
|
+
out[o + 3] = after.data[b + 3];
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
return {
|
|
3658
|
+
changed,
|
|
3659
|
+
total: width * height,
|
|
3660
|
+
bounds: maxX < 0 ? null : { x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1 },
|
|
3661
|
+
image: new ImageData(out, width, height)
|
|
3662
|
+
};
|
|
3663
|
+
}
|
|
3664
|
+
function recordFilmstrip(canvas, node, {
|
|
3665
|
+
scale = 0.25,
|
|
3666
|
+
max = 30,
|
|
3667
|
+
settleMs = 400,
|
|
3668
|
+
onFrame
|
|
3669
|
+
} = {}) {
|
|
3670
|
+
const frames = [];
|
|
3671
|
+
let stopped = false;
|
|
3672
|
+
let timer = 0;
|
|
3673
|
+
const take = () => {
|
|
3674
|
+
canvas.snapshot(node, { scale }).then((image) => {
|
|
3675
|
+
if (stopped) return;
|
|
3676
|
+
const frame = { at: performance.now(), image };
|
|
3677
|
+
frames.push(frame);
|
|
3678
|
+
if (frames.length > max) frames.shift();
|
|
3679
|
+
onFrame?.(frame, frames);
|
|
3680
|
+
}).catch(() => {
|
|
3681
|
+
});
|
|
3682
|
+
};
|
|
3683
|
+
const observer = new MutationObserver((records) => {
|
|
3684
|
+
if (records.every((record) => record.type === "attributes" && record.target === node)) return;
|
|
3685
|
+
clearTimeout(timer);
|
|
3686
|
+
timer = window.setTimeout(take, settleMs);
|
|
3687
|
+
});
|
|
3688
|
+
observer.observe(node, { subtree: true, childList: true, characterData: true, attributes: true });
|
|
3689
|
+
take();
|
|
3690
|
+
return {
|
|
3691
|
+
frames,
|
|
3692
|
+
stop() {
|
|
3693
|
+
stopped = true;
|
|
3694
|
+
clearTimeout(timer);
|
|
3695
|
+
observer.disconnect();
|
|
3696
|
+
}
|
|
3697
|
+
};
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
// src/transitions.ts
|
|
3701
|
+
var easeInOut = (t) => t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
|
|
3702
|
+
var running = /* @__PURE__ */ new WeakMap();
|
|
3703
|
+
function animateEffect(canvas, node, effect, { from = 0, to = 1, duration = 500, easing = easeInOut, params = [] } = {}) {
|
|
3704
|
+
running.get(node)?.();
|
|
3705
|
+
return new Promise((resolve) => {
|
|
3706
|
+
const start = performance.now();
|
|
3707
|
+
let frame = 0;
|
|
3708
|
+
const set = (progress) => canvas.setEffect(node, effect, [progress, ...params]);
|
|
3709
|
+
const stop = () => {
|
|
3710
|
+
cancelAnimationFrame(frame);
|
|
3711
|
+
running.delete(node);
|
|
3712
|
+
resolve(false);
|
|
3713
|
+
};
|
|
3714
|
+
running.set(node, stop);
|
|
3715
|
+
const step = (now) => {
|
|
3716
|
+
const t = duration > 0 ? Math.min(1, (now - start) / duration) : 1;
|
|
3717
|
+
const progress = from + (to - from) * easing(t);
|
|
3718
|
+
if (t < 1) {
|
|
3719
|
+
set(progress);
|
|
3720
|
+
frame = requestAnimationFrame(step);
|
|
3721
|
+
return;
|
|
3722
|
+
}
|
|
3723
|
+
running.delete(node);
|
|
3724
|
+
if (to === 0) canvas.setEffect(node, null);
|
|
3725
|
+
else set(to);
|
|
3726
|
+
resolve(true);
|
|
3727
|
+
};
|
|
3728
|
+
set(from);
|
|
3729
|
+
frame = requestAnimationFrame(step);
|
|
3730
|
+
});
|
|
3731
|
+
}
|
|
3732
|
+
async function swapNodes(canvas, outgoing, incoming, {
|
|
3733
|
+
effect = dissolve,
|
|
3734
|
+
...options
|
|
3735
|
+
} = {}) {
|
|
3736
|
+
const done = await Promise.all([
|
|
3737
|
+
animateEffect(canvas, outgoing, effect, { ...options, from: 0, to: 1 }),
|
|
3738
|
+
animateEffect(canvas, incoming, effect, { ...options, from: 1, to: 0 })
|
|
3739
|
+
]);
|
|
3740
|
+
return done.every(Boolean);
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3099
3743
|
// src/index.ts
|
|
3100
3744
|
async function createGpuCanvas2(host, options = {}) {
|
|
3101
3745
|
const mode = options.mode ?? "auto";
|
|
@@ -3118,7 +3762,15 @@ export {
|
|
|
3118
3762
|
defineEffect,
|
|
3119
3763
|
EFFECTS,
|
|
3120
3764
|
supportsGpuCanvas,
|
|
3765
|
+
createAgentView,
|
|
3121
3766
|
bindEditor,
|
|
3767
|
+
createMarks,
|
|
3768
|
+
toBlob,
|
|
3769
|
+
diffImages,
|
|
3770
|
+
recordFilmstrip,
|
|
3771
|
+
easeInOut,
|
|
3772
|
+
animateEffect,
|
|
3773
|
+
swapNodes,
|
|
3122
3774
|
createGpuCanvas2 as createGpuCanvas
|
|
3123
3775
|
};
|
|
3124
|
-
//# sourceMappingURL=chunk-
|
|
3776
|
+
//# sourceMappingURL=chunk-BQQVSR4B.js.map
|