From 3338a6f3503e98249b48555303c69c515f40595b Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Sat, 11 Jul 2026 13:03:40 -0700 Subject: [PATCH] fix(cli): fold canvas pixel hashes into the frozen-sweep fingerprint --- .../cli/src/commands/layout-audit.browser.js | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/commands/layout-audit.browser.js b/packages/cli/src/commands/layout-audit.browser.js index f939a9dcee..e3d9e25f1f 100644 --- a/packages/cli/src/commands/layout-audit.browser.js +++ b/packages/cli/src/commands/layout-audit.browser.js @@ -895,6 +895,31 @@ // actually moved anything and the whole audit run is unreliable. Deliberately // a single opaque string (not a structured array) since Node only ever needs // equality, not per-element diffing. + // Pixel-only canvas motion (a 2D/WebGL canvas repainting without any element + // moving) is invisible to a geometry+opacity fingerprint and false-positived + // sweep_static (wild report: 4K WebGL comp needed a transparent moving DOM + // sentinel to pass). Downsample each visible canvas to 8x8 and fold its + // pixels into the fingerprint. Tainted/zero-sized/unreadable canvases hash + // to a constant — no worse than today, never a new false NEGATIVE for + // DOM-motion comps. + function canvasPixelHash(canvas) { + try { + if (!canvas.width || !canvas.height) return "x"; + const off = document.createElement("canvas"); + off.width = 8; + off.height = 8; + const ctx = off.getContext("2d"); + if (!ctx) return "x"; + ctx.drawImage(canvas, 0, 0, 8, 8); + const data = ctx.getImageData(0, 0, 8, 8).data; + let hash = 0; + for (let i = 0; i < data.length; i++) hash = (hash * 31 + data[i]) >>> 0; + return String(hash); + } catch { + return "x"; + } + } + window.__hyperframesLayoutGeometry = function collectLayoutGeometry() { const root = document.querySelector("[data-composition-id][data-width][data-height]") || @@ -903,12 +928,15 @@ const elements = Array.from(root.querySelectorAll("*")).filter((element) => isVisibleElement(element), ); - return elements - .map((element) => { - const rect = toRect(element.getBoundingClientRect()); - const opacity = round(opacityChain(element)); - return `${rect.left},${rect.top},${rect.width},${rect.height},${opacity}`; - }) - .join("|"); + const parts = elements.map((element) => { + const rect = toRect(element.getBoundingClientRect()); + const opacity = round(opacityChain(element)); + return `${rect.left},${rect.top},${rect.width},${rect.height},${opacity}`; + }); + for (const canvas of root.querySelectorAll("canvas")) { + if (!isVisibleElement(canvas)) continue; + parts.push(`c:${canvasPixelHash(canvas)}`); + } + return parts.join("|"); }; })();