mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-15 13:58:20 +00:00
feat(cli): check names a WebGL scene that draws while it is not on screen
This commit is contained in:
@@ -2206,6 +2206,55 @@
|
||||
return issues;
|
||||
}
|
||||
|
||||
// Canvases whose WebGL context drew since the last audit; the author's own hf-seek loop decides what draws.
|
||||
const webglDraws = (window.__hyperframesWebglDraws ??= new Set());
|
||||
const drawGate = Symbol.for("hyperframes.webglDrawGate");
|
||||
for (const name of ["WebGLRenderingContext", "WebGL2RenderingContext"]) {
|
||||
const proto = window[name]?.prototype;
|
||||
if (!proto || proto.__hyperframesDrawCount) continue;
|
||||
proto.__hyperframesDrawCount = true;
|
||||
for (const method of [
|
||||
"clear",
|
||||
"drawArrays",
|
||||
"drawElements",
|
||||
"drawArraysInstanced",
|
||||
"drawElementsInstanced",
|
||||
]) {
|
||||
const draw = proto[method];
|
||||
if (typeof draw !== "function") continue;
|
||||
proto[method] = function (...args) {
|
||||
if (!window[drawGate]?.skip) webglDraws.add(this.canvas);
|
||||
return draw.apply(this, args);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function inactiveWebglIssues(root, time) {
|
||||
const drawn = [...webglDraws].filter((canvas) => canvas?.isConnected);
|
||||
webglDraws.clear();
|
||||
return drawn
|
||||
.filter(
|
||||
(canvas) => canvas.closest(RUNTIME_HIDDEN_CLIP) || canvas.checkVisibility?.() === false,
|
||||
)
|
||||
.map((canvas) => {
|
||||
const scene =
|
||||
canvas.closest("[data-composition-id]:not([data-width])") ??
|
||||
canvas.closest("[data-composition-id]");
|
||||
const name = scene?.getAttribute("data-composition-id") || uniqueSelectorFor(canvas);
|
||||
return {
|
||||
code: "webgl_draws_while_inactive",
|
||||
severity: "warning",
|
||||
time,
|
||||
selector: uniqueSelectorFor(canvas),
|
||||
containerSelector: uniqueSelectorFor(root),
|
||||
message: `Scene "${name}" draws WebGL while it is not on screen, so every seek pays for it.`,
|
||||
rect: toRect(canvas.getBoundingClientRect()),
|
||||
fixHint:
|
||||
"Draw only while the scene is active (its data-start to data-start + data-duration), or skip the draw in your hf-seek listener when the scene is hidden.",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Soft prior only — the counterfactual attach test (below) is what makes detachment a finding.
|
||||
const CONNECTOR_NAME = /\b(conn(ector)?|arrow|edge|link|flow|wire)\b/i;
|
||||
const CONNECTOR_SKIP_CONTAINERS = "defs, marker, clipPath, mask, symbol, pattern";
|
||||
@@ -2631,6 +2680,7 @@
|
||||
issues.push(...escaped.issues);
|
||||
issues.push(...panelOutOfCanvasIssues(root, rootRect, time, tolerance, escaped.flagged));
|
||||
issues.push(...canvasEdgeIssues(root, rootRect, time, tolerance));
|
||||
issues.push(...inactiveWebglIssues(root, time));
|
||||
issues.push(...connectorDetachmentIssues(root, rootRect, time));
|
||||
issues.push(...connectorOrphanIssues(root, rootRect, time));
|
||||
return issues;
|
||||
|
||||
@@ -1310,6 +1310,48 @@ describe("layout-audit.browser coordinate-frame findings", () => {
|
||||
expect(issues.find((issue) => issue.selector === "#bleed")).toMatchObject({ severity: "info" });
|
||||
});
|
||||
|
||||
describe("webgl_draws_while_inactive", () => {
|
||||
function mountScenes(hidden: boolean): HTMLCanvasElement {
|
||||
document.body.innerHTML = `
|
||||
<div id="root" data-composition-id="main" data-width="640" data-height="360">
|
||||
<div id="scene2" data-composition-id="scene2" data-start="4" data-duration="2" ${hidden ? "data-hf-clip-hidden" : ""}>
|
||||
<canvas id="shader" width="640" height="360"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
installGeometry({
|
||||
root: rect({ left: 0, top: 0, width: 640, height: 360 }),
|
||||
scene2: rect({ left: 0, top: 0, width: 640, height: 360 }),
|
||||
shader: rect({ left: 0, top: 0, width: 640, height: 360 }),
|
||||
});
|
||||
installAuditScript();
|
||||
return document.getElementById("shader") as HTMLCanvasElement;
|
||||
}
|
||||
|
||||
const drawn = () =>
|
||||
(window as unknown as { __hyperframesWebglDraws: Set<Element> }).__hyperframesWebglDraws;
|
||||
|
||||
it("names the scene whose canvas draws while its clip is hidden", () => {
|
||||
drawn().add(mountScenes(true));
|
||||
|
||||
const found = runAudit().filter((issue) => issue.code === "webgl_draws_while_inactive");
|
||||
|
||||
expect(found).toHaveLength(1);
|
||||
expect(found[0]).toMatchObject({ severity: "warning", selector: "#shader" });
|
||||
expect(found[0]?.message).toContain("scene2");
|
||||
});
|
||||
|
||||
it("stays quiet for a canvas drawing on screen, and reports a draw once", () => {
|
||||
drawn().add(mountScenes(false));
|
||||
expect(runAudit().some((issue) => issue.code === "webgl_draws_while_inactive")).toBe(false);
|
||||
|
||||
drawn().add(document.getElementById("shader") as HTMLCanvasElement);
|
||||
document.getElementById("scene2")?.setAttribute("data-hf-clip-hidden", "");
|
||||
expect(runAudit().filter((i) => i.code === "webgl_draws_while_inactive")).toHaveLength(1);
|
||||
expect(runAudit().some((issue) => issue.code === "webgl_draws_while_inactive")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("canvas_content_at_edge", () => {
|
||||
// One grey RGBA pixel per entry: a 1px-thick band, read the same along every edge.
|
||||
function edgeLine(levels: number[]): Uint8ClampedArray {
|
||||
|
||||
@@ -2189,6 +2189,7 @@ const LAYOUT_ISSUE_CODES: readonly LayoutIssueCode[] = [
|
||||
"escaped_container",
|
||||
"panel_out_of_canvas",
|
||||
"canvas_content_at_edge",
|
||||
"webgl_draws_while_inactive",
|
||||
"connector_detached",
|
||||
"connector_orphan",
|
||||
"rotation_pivot_drift",
|
||||
|
||||
@@ -27,6 +27,7 @@ export type LayoutIssueCode =
|
||||
| "escaped_container"
|
||||
| "panel_out_of_canvas"
|
||||
| "canvas_content_at_edge"
|
||||
| "webgl_draws_while_inactive"
|
||||
| "connector_detached"
|
||||
| "connector_orphan"
|
||||
// Cross-sample rotation finding — a spinning element whose bbox center drifts
|
||||
|
||||
Reference in New Issue
Block a user