gpucanvas 0.3.1 → 0.4.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.
@@ -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 isPinned = (page) => live?.page === page || lingering?.page === page || focused?.page === page || [...transitioning.keys()].some((node) => node.page === page);
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 instances = visible.length;
1809
- for (const node of visible) instances += node.tiles.length + (node.preview ? 1 : 0);
1810
- instances += selection.size + 1;
1811
- renderer.reserve(instances);
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 sameRect = (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;
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 (sameRect(rect, previous)) continue;
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 (sameRect(rect, outer)) visit(child, outer);
2732
+ if (sameRect2(rect, outer)) visit(child, outer);
2475
2733
  else children.push([child]);
2476
2734
  }
2477
2735
  };
@@ -2758,6 +3016,7 @@ function bindEditor(canvas, options = {}) {
2758
3016
  let pointer = null;
2759
3017
  let shift = false;
2760
3018
  let swallowClick = false;
3019
+ let keepFocus = false;
2761
3020
  const commit = (next, nextScopes = scopes) => {
2762
3021
  selection = next.filter((layer, i) => next.findIndex((other) => sameLayer(layer, other)) === i);
2763
3022
  scopes = selection.length ? nextScopes : [];
@@ -2776,6 +3035,15 @@ function bindEditor(canvas, options = {}) {
2776
3035
  return { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
2777
3036
  };
2778
3037
  const chainAt = (point) => layers.chainAt(canvas.screenToWorld(point));
3038
+ const fieldAt = (point) => {
3039
+ const element = canvas.getElementAt(canvas.screenToWorld(point));
3040
+ const field = element?.closest("input, textarea, select, [contenteditable]") ?? null;
3041
+ return isEditable(field) ? field : null;
3042
+ };
3043
+ const typingIn = () => {
3044
+ const active = document.activeElement;
3045
+ return isEditable(active) && canvas.getNodeOf(active) ? active : null;
3046
+ };
2779
3047
  const targetIn = (chain) => {
2780
3048
  if (!chain.length) return null;
2781
3049
  for (let i = scopes.length - 1; i >= 0; i--) {
@@ -2929,8 +3197,15 @@ function bindEditor(canvas, options = {}) {
2929
3197
  swallowClick = false;
2930
3198
  if (event.button !== 0) return;
2931
3199
  const point = at(event);
3200
+ const field = typingIn();
3201
+ keepFocus = false;
3202
+ if (field && field.contains(fieldAt(point))) {
3203
+ keepFocus = !field.contains(event.target);
3204
+ return;
3205
+ }
2932
3206
  const take = () => {
2933
3207
  event.stopPropagation();
3208
+ field?.blur();
2934
3209
  swallowClick = true;
2935
3210
  surface.setPointerCapture(event.pointerId);
2936
3211
  };
@@ -3003,7 +3278,20 @@ function bindEditor(canvas, options = {}) {
3003
3278
  if (!selecting(event.shiftKey)) return;
3004
3279
  event.preventDefault();
3005
3280
  event.stopPropagation();
3006
- const chain = chainAt(at(event));
3281
+ const point = at(event);
3282
+ const field = trigger === "click" ? fieldAt(point) : null;
3283
+ if (field) {
3284
+ if (selection.length) commit([], []);
3285
+ field.focus();
3286
+ if (field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement) {
3287
+ try {
3288
+ field.setSelectionRange(field.value.length, field.value.length);
3289
+ } catch {
3290
+ }
3291
+ }
3292
+ return;
3293
+ }
3294
+ const chain = chainAt(point);
3007
3295
  let depth = -1;
3008
3296
  chain.forEach((layer, i) => {
3009
3297
  if (layerIn(layer, selection)) depth = i;
@@ -3015,6 +3303,11 @@ function bindEditor(canvas, options = {}) {
3015
3303
  setHovered(null);
3016
3304
  };
3017
3305
  const onMouseDown = (event) => {
3306
+ if (keepFocus) {
3307
+ keepFocus = false;
3308
+ event.preventDefault();
3309
+ return;
3310
+ }
3018
3311
  if (!swallowClick) return;
3019
3312
  event.preventDefault();
3020
3313
  event.stopPropagation();
@@ -3034,7 +3327,11 @@ function bindEditor(canvas, options = {}) {
3034
3327
  shift = event.type === "keydown";
3035
3328
  return updateHover();
3036
3329
  }
3037
- if (event.type !== "keydown" || isEditable(document.activeElement)) return;
3330
+ if (event.type !== "keydown") return;
3331
+ if (isEditable(document.activeElement)) {
3332
+ if (event.key === "Escape" && trigger === "click") typingIn()?.blur();
3333
+ return;
3334
+ }
3038
3335
  if (event.key === "Escape") {
3039
3336
  if (gesture?.kind === "move" || gesture?.kind === "resize") {
3040
3337
  emit("cancel");
@@ -3096,6 +3393,392 @@ function bindEditor(canvas, options = {}) {
3096
3393
  };
3097
3394
  }
3098
3395
 
3396
+ // src/marks.ts
3397
+ var SVG = "http://www.w3.org/2000/svg";
3398
+ var FONT = "500 11px/16px system-ui, sans-serif";
3399
+ var instances = 0;
3400
+ var pillStyle = (color) => ({
3401
+ position: "absolute",
3402
+ left: "0",
3403
+ top: "0",
3404
+ font: FONT,
3405
+ padding: "0 6px",
3406
+ borderRadius: "4px",
3407
+ color: "#fff",
3408
+ background: color,
3409
+ whiteSpace: "nowrap",
3410
+ pointerEvents: "none"
3411
+ });
3412
+ function createMarks(canvas, { color = "#4f8cff" } = {}) {
3413
+ const id = `gpucanvas-marks-${++instances}`;
3414
+ const root = document.createElement("div");
3415
+ Object.assign(root.style, {
3416
+ position: "absolute",
3417
+ inset: "0",
3418
+ overflow: "hidden",
3419
+ pointerEvents: "none",
3420
+ zIndex: "1"
3421
+ });
3422
+ const svg = document.createElementNS(SVG, "svg");
3423
+ Object.assign(svg.style, { position: "absolute", inset: "0", width: "100%", height: "100%" });
3424
+ const defs = document.createElementNS(SVG, "defs");
3425
+ svg.append(defs);
3426
+ root.append(svg);
3427
+ canvas.surface.append(root);
3428
+ const arrows = /* @__PURE__ */ new Map();
3429
+ const arrowFor = (stroke) => {
3430
+ let ref = arrows.get(stroke);
3431
+ if (!ref) {
3432
+ ref = `${id}-arrow-${arrows.size}`;
3433
+ const marker = document.createElementNS(SVG, "marker");
3434
+ for (const [key, value] of Object.entries({
3435
+ id: ref,
3436
+ viewBox: "0 0 10 10",
3437
+ refX: "8",
3438
+ refY: "5",
3439
+ markerWidth: "7",
3440
+ markerHeight: "7",
3441
+ orient: "auto"
3442
+ })) {
3443
+ marker.setAttribute(key, value);
3444
+ }
3445
+ const head = document.createElementNS(SVG, "path");
3446
+ head.setAttribute("d", "M0,1 L9,5 L0,9 z");
3447
+ head.setAttribute("fill", stroke);
3448
+ marker.append(head);
3449
+ defs.append(marker);
3450
+ arrows.set(stroke, ref);
3451
+ }
3452
+ return ref;
3453
+ };
3454
+ const toScreen = (point) => canvas.worldToScreen(point);
3455
+ const screenRect = (rect) => {
3456
+ const a = toScreen(rect);
3457
+ const b = toScreen({ x: rect.x + rect.width, y: rect.y + rect.height });
3458
+ return { x: a.x, y: a.y, width: b.x - a.x, height: b.y - a.y };
3459
+ };
3460
+ const worldRect = (target) => target instanceof Element ? canvas.getElementRect(target) : { x: target.x, y: target.y, width: 0, height: 0 };
3461
+ const place = (element, x, y) => {
3462
+ const transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)`;
3463
+ if (element.style.transform !== transform) element.style.transform = transform;
3464
+ };
3465
+ const show = (element, visible) => {
3466
+ const display = visible ? "" : "none";
3467
+ if (element.style.display !== display) element.style.display = display;
3468
+ };
3469
+ const div = (style, text) => {
3470
+ const element = document.createElement("div");
3471
+ Object.assign(element.style, style);
3472
+ if (text) element.textContent = text;
3473
+ root.append(element);
3474
+ return element;
3475
+ };
3476
+ const marks = /* @__PURE__ */ new Set();
3477
+ let frame = 0;
3478
+ const loop = () => {
3479
+ frame = marks.size ? requestAnimationFrame(loop) : 0;
3480
+ for (const draw of marks) draw();
3481
+ };
3482
+ const add = (draw, remove) => {
3483
+ marks.add(draw);
3484
+ if (!frame) frame = requestAnimationFrame(loop);
3485
+ return {
3486
+ remove() {
3487
+ marks.delete(draw);
3488
+ remove();
3489
+ }
3490
+ };
3491
+ };
3492
+ return {
3493
+ /** An arrow from one element (or world point) to another, with an optional label. */
3494
+ connect(from, to, options = {}) {
3495
+ const stroke = options.color ?? color;
3496
+ const path = document.createElementNS(SVG, "path");
3497
+ path.setAttribute("fill", "none");
3498
+ path.setAttribute("stroke", stroke);
3499
+ path.setAttribute("stroke-width", "2");
3500
+ path.setAttribute("stroke-linecap", "round");
3501
+ if (options.dashed) path.setAttribute("stroke-dasharray", "6 5");
3502
+ path.setAttribute("marker-end", `url(#${arrowFor(stroke)})`);
3503
+ svg.append(path);
3504
+ const label = options.label ? div(pillStyle(stroke), options.label) : null;
3505
+ return add(
3506
+ () => {
3507
+ const a = worldRect(from);
3508
+ const b = worldRect(to);
3509
+ show(path, !!a && !!b);
3510
+ if (label) show(label, !!a && !!b);
3511
+ if (!a || !b) return;
3512
+ const p = screenRect(a);
3513
+ const q = screenRect(b);
3514
+ const dx = q.x + q.width / 2 - (p.x + p.width / 2);
3515
+ const dy = q.y + q.height / 2 - (p.y + p.height / 2);
3516
+ const across = Math.abs(dx) >= Math.abs(dy);
3517
+ 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 };
3518
+ 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 };
3519
+ const bend = Math.max(
3520
+ 30,
3521
+ (across ? Math.abs(end.x - start.x) : Math.abs(end.y - start.y)) / 2
3522
+ );
3523
+ const c1 = across ? { x: start.x + Math.sign(dx) * bend, y: start.y } : { x: start.x, y: start.y + Math.sign(dy) * bend };
3524
+ const c2 = across ? { x: end.x - Math.sign(dx) * bend, y: end.y } : { x: end.x, y: end.y - Math.sign(dy) * bend };
3525
+ 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)}`;
3526
+ if (path.getAttribute("d") !== d) path.setAttribute("d", d);
3527
+ if (label) {
3528
+ const mx = (start.x + 3 * c1.x + 3 * c2.x + end.x) / 8;
3529
+ const my = (start.y + 3 * c1.y + 3 * c2.y + end.y) / 8;
3530
+ place(label, mx - label.offsetWidth / 2, my - 8);
3531
+ }
3532
+ },
3533
+ () => {
3534
+ path.remove();
3535
+ label?.remove();
3536
+ }
3537
+ );
3538
+ },
3539
+ /**
3540
+ * A comment pin at an element's top right corner, or at a world point. Its text shows on
3541
+ * hover; `onClick` makes it a button.
3542
+ */
3543
+ pin(target, options = {}) {
3544
+ const fill = options.color ?? color;
3545
+ const dot = div(
3546
+ {
3547
+ position: "absolute",
3548
+ left: "0",
3549
+ top: "0",
3550
+ width: "24px",
3551
+ height: "24px",
3552
+ borderRadius: "12px 12px 12px 2px",
3553
+ background: fill,
3554
+ border: "2px solid #fff",
3555
+ boxShadow: "0 1px 4px rgb(0 0 0 / 0.25)",
3556
+ color: "#fff",
3557
+ font: "600 11px/20px system-ui, sans-serif",
3558
+ textAlign: "center",
3559
+ boxSizing: "border-box",
3560
+ pointerEvents: "auto",
3561
+ cursor: options.onClick ? "pointer" : "default"
3562
+ },
3563
+ options.label ?? ""
3564
+ );
3565
+ if (options.text) dot.title = options.text;
3566
+ if (options.onClick) dot.addEventListener("click", options.onClick);
3567
+ return add(
3568
+ () => {
3569
+ const rect = worldRect(target);
3570
+ show(dot, !!rect);
3571
+ if (!rect) return;
3572
+ const r = screenRect(rect);
3573
+ place(dot, r.x + r.width - 4, r.y - 20);
3574
+ },
3575
+ () => dot.remove()
3576
+ );
3577
+ },
3578
+ /** A dashed frame around an element with a note above it: what someone is pointing at. */
3579
+ note(target, text, options = {}) {
3580
+ const stroke = options.color ?? "#f59e0b";
3581
+ const frameBox = div({
3582
+ position: "absolute",
3583
+ left: "0",
3584
+ top: "0",
3585
+ border: `2px dashed ${stroke}`,
3586
+ borderRadius: "6px",
3587
+ boxSizing: "border-box"
3588
+ });
3589
+ const label = div({ ...pillStyle(stroke), maxWidth: "320px", whiteSpace: "normal" }, text);
3590
+ return add(
3591
+ () => {
3592
+ const rect = worldRect(target);
3593
+ show(frameBox, !!rect);
3594
+ show(label, !!rect);
3595
+ if (!rect) return;
3596
+ const r = screenRect(rect);
3597
+ place(frameBox, r.x - 4, r.y - 4);
3598
+ const width = `${Math.round(r.width + 8)}px`;
3599
+ const height = `${Math.round(r.height + 8)}px`;
3600
+ if (frameBox.style.width !== width) frameBox.style.width = width;
3601
+ if (frameBox.style.height !== height) frameBox.style.height = height;
3602
+ place(label, r.x - 4, r.y - 8 - label.offsetHeight);
3603
+ },
3604
+ () => {
3605
+ frameBox.remove();
3606
+ label.remove();
3607
+ }
3608
+ );
3609
+ },
3610
+ /** Someone's cursor, with their name. It glides to each point given to `move`. */
3611
+ cursor(name, options = {}) {
3612
+ const fill = options.color ?? color;
3613
+ const box = div({ position: "absolute", left: "0", top: "0", display: "none" });
3614
+ 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>`;
3615
+ const tag = document.createElement("div");
3616
+ Object.assign(tag.style, pillStyle(fill), { position: "relative", marginLeft: "12px" });
3617
+ tag.textContent = name;
3618
+ box.append(tag);
3619
+ let target = null;
3620
+ let at = null;
3621
+ const mark = add(
3622
+ () => {
3623
+ show(box, !!target);
3624
+ if (!target) return;
3625
+ at = at ? { x: at.x + (target.x - at.x) * 0.25, y: at.y + (target.y - at.y) * 0.25 } : target;
3626
+ const p = toScreen(at);
3627
+ place(box, p.x, p.y);
3628
+ },
3629
+ () => box.remove()
3630
+ );
3631
+ return {
3632
+ move(point) {
3633
+ target = point;
3634
+ if (!point) at = null;
3635
+ },
3636
+ remove: mark.remove
3637
+ };
3638
+ },
3639
+ /** Removes every mark. */
3640
+ clear() {
3641
+ for (const draw of [...marks]) marks.delete(draw);
3642
+ root.querySelectorAll(":scope > div").forEach((element) => element.remove());
3643
+ svg.querySelectorAll(":scope > path").forEach((element) => element.remove());
3644
+ },
3645
+ destroy() {
3646
+ cancelAnimationFrame(frame);
3647
+ marks.clear();
3648
+ root.remove();
3649
+ }
3650
+ };
3651
+ }
3652
+
3653
+ // src/snapshots.ts
3654
+ function toBlob(image, type = "image/png", quality) {
3655
+ const canvas = new OffscreenCanvas(image.width, image.height);
3656
+ canvas.getContext("2d").putImageData(image, 0, 0);
3657
+ return canvas.convertToBlob({ type, quality });
3658
+ }
3659
+ function diffImages(before, after, { threshold = 24, color = [255, 64, 96] } = {}) {
3660
+ const width = Math.max(before.width, after.width);
3661
+ const height = Math.max(before.height, after.height);
3662
+ const out = new Uint8ClampedArray(width * height * 4);
3663
+ let changed = 0;
3664
+ let minX = width;
3665
+ let minY = height;
3666
+ let maxX = -1;
3667
+ let maxY = -1;
3668
+ const read = (image, x, y) => x < image.width && y < image.height ? (y * image.width + x) * 4 : -1;
3669
+ for (let y = 0; y < height; y++) {
3670
+ for (let x = 0; x < width; x++) {
3671
+ const a = read(before, x, y);
3672
+ const b = read(after, x, y);
3673
+ const o = (y * width + x) * 4;
3674
+ let differs = a < 0 || b < 0;
3675
+ if (!differs) {
3676
+ for (let c = 0; c < 4 && !differs; c++) {
3677
+ differs = Math.abs(before.data[a + c] - after.data[b + c]) > threshold;
3678
+ }
3679
+ }
3680
+ if (differs) {
3681
+ changed++;
3682
+ if (x < minX) minX = x;
3683
+ if (y < minY) minY = y;
3684
+ if (x > maxX) maxX = x;
3685
+ if (y > maxY) maxY = y;
3686
+ out[o] = color[0];
3687
+ out[o + 1] = color[1];
3688
+ out[o + 2] = color[2];
3689
+ out[o + 3] = 255;
3690
+ } else {
3691
+ for (let c = 0; c < 3; c++) out[o + c] = 255 - (255 - after.data[b + c]) * 0.25;
3692
+ out[o + 3] = after.data[b + 3];
3693
+ }
3694
+ }
3695
+ }
3696
+ return {
3697
+ changed,
3698
+ total: width * height,
3699
+ bounds: maxX < 0 ? null : { x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1 },
3700
+ image: new ImageData(out, width, height)
3701
+ };
3702
+ }
3703
+ function recordFilmstrip(canvas, node, {
3704
+ scale = 0.25,
3705
+ max = 30,
3706
+ settleMs = 400,
3707
+ onFrame
3708
+ } = {}) {
3709
+ const frames = [];
3710
+ let stopped = false;
3711
+ let timer = 0;
3712
+ const take = () => {
3713
+ canvas.snapshot(node, { scale }).then((image) => {
3714
+ if (stopped) return;
3715
+ const frame = { at: performance.now(), image };
3716
+ frames.push(frame);
3717
+ if (frames.length > max) frames.shift();
3718
+ onFrame?.(frame, frames);
3719
+ }).catch(() => {
3720
+ });
3721
+ };
3722
+ const observer = new MutationObserver((records) => {
3723
+ if (records.every((record) => record.type === "attributes" && record.target === node)) return;
3724
+ clearTimeout(timer);
3725
+ timer = window.setTimeout(take, settleMs);
3726
+ });
3727
+ observer.observe(node, { subtree: true, childList: true, characterData: true, attributes: true });
3728
+ take();
3729
+ return {
3730
+ frames,
3731
+ stop() {
3732
+ stopped = true;
3733
+ clearTimeout(timer);
3734
+ observer.disconnect();
3735
+ }
3736
+ };
3737
+ }
3738
+
3739
+ // src/transitions.ts
3740
+ var easeInOut = (t) => t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
3741
+ var running = /* @__PURE__ */ new WeakMap();
3742
+ function animateEffect(canvas, node, effect, { from = 0, to = 1, duration = 500, easing = easeInOut, params = [] } = {}) {
3743
+ running.get(node)?.();
3744
+ return new Promise((resolve) => {
3745
+ const start = performance.now();
3746
+ let frame = 0;
3747
+ const set = (progress) => canvas.setEffect(node, effect, [progress, ...params]);
3748
+ const stop = () => {
3749
+ cancelAnimationFrame(frame);
3750
+ running.delete(node);
3751
+ resolve(false);
3752
+ };
3753
+ running.set(node, stop);
3754
+ const step = (now) => {
3755
+ const t = duration > 0 ? Math.min(1, (now - start) / duration) : 1;
3756
+ const progress = from + (to - from) * easing(t);
3757
+ if (t < 1) {
3758
+ set(progress);
3759
+ frame = requestAnimationFrame(step);
3760
+ return;
3761
+ }
3762
+ running.delete(node);
3763
+ if (to === 0) canvas.setEffect(node, null);
3764
+ else set(to);
3765
+ resolve(true);
3766
+ };
3767
+ set(from);
3768
+ frame = requestAnimationFrame(step);
3769
+ });
3770
+ }
3771
+ async function swapNodes(canvas, outgoing, incoming, {
3772
+ effect = dissolve,
3773
+ ...options
3774
+ } = {}) {
3775
+ const done = await Promise.all([
3776
+ animateEffect(canvas, outgoing, effect, { ...options, from: 0, to: 1 }),
3777
+ animateEffect(canvas, incoming, effect, { ...options, from: 1, to: 0 })
3778
+ ]);
3779
+ return done.every(Boolean);
3780
+ }
3781
+
3099
3782
  // src/index.ts
3100
3783
  async function createGpuCanvas2(host, options = {}) {
3101
3784
  const mode = options.mode ?? "auto";
@@ -3118,7 +3801,15 @@ export {
3118
3801
  defineEffect,
3119
3802
  EFFECTS,
3120
3803
  supportsGpuCanvas,
3804
+ createAgentView,
3121
3805
  bindEditor,
3806
+ createMarks,
3807
+ toBlob,
3808
+ diffImages,
3809
+ recordFilmstrip,
3810
+ easeInOut,
3811
+ animateEffect,
3812
+ swapNodes,
3122
3813
  createGpuCanvas2 as createGpuCanvas
3123
3814
  };
3124
- //# sourceMappingURL=chunk-RBXGH44N.js.map
3815
+ //# sourceMappingURL=chunk-DPCICAYZ.js.map