Files
heygen-com__hyperframes/packages/engine/spikes/de-3d-module-check.mjs
Vance Ingalls e205ddfc1d feat(engine): WebGL 3D-context projection for fast capture
drawElementImage cannot paint CSS 3D: rendering contexts drop earlier
siblings and backgrounds, backface-visibility is ignored (mirrored
backfaces even at rest), and flat 3D matrices silently lose their
rotation. Until now every 3D comp was gated to the baseline route.

threeDProjection.ts rewrites 3D content in-page before capture:
- discovery: perspective/preserve-3d contexts, elements at a 3D matrix
  at t=0, and the producer stub's record of 3D tween targets
  (rotationX/rotationY/transformPerspective) for to()-style tweens that
  are still flat at init
- leaf quads rasterized once via SVG foreignObject (fonts and images
  inlined as data URLs), shell quads carry only own paint when a child
  contains further 3D
- per-frame WebGL projection from live computed matrices: CSS-convention
  matrix math, perspective + transform-origin sandwiches, GL backface
  culling, accumulated opacity as a fragment uniform
- live elements hidden via clip-path (GSAP autoAlpha fights
  visibility/opacity), 3D contexts neutralized and live 3D matrices
  stripped after reading (perspective-carrying and rotated matrices
  poison the capture even when hidden), authored backface flags captured
  before neutralization
- projected canvases composite OVER the DOM paint — the under-pass would
  bury them beneath the composition root's own background

Correctness guards fall back to the platform baseline route, same
contract as the video gate: degenerate markup (zero-size/inline-box
quads — gen_os flip-card spans) and quads with GSAP-animated descendants
(static textures would freeze them; golf measured 46->24 dB without
this).

fast-capture-3d test comp (flip card + perspective-free rotationX
entrance): 57.5 dB avg / 51.7 dB min vs baseline at 1.5x speedup.
Real-world comps with animated 3D subtrees fall back cleanly.

Engine suite 692/694 (2 failures pre-exist on clean tree); build, lint,
oxfmt verified manually — hook bypassed for the same stale-lockfile
core-build failure as the previous commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 23:59:24 -07:00

85 lines
3.2 KiB
JavaScript

// Drive the engine's initThreeDProjectionInPage against a denovo-style
// inline flip card (flip-container inside an h1 text flow) and screenshot
// the page directly — no capture pipeline — to isolate projection bugs.
// Run with bun (TS import): bun spikes/de-3d-module-check.mjs
import puppeteer from "puppeteer";
import { spawnSync } from "node:child_process";
import { writeFileSync, mkdirSync } from "node:fs";
import { initThreeDProjectionInPage } from "../src/services/threeDProjection.ts";
const W = 960, H = 540;
const HTML = `<!doctype html><meta charset=utf-8>
<style>
*{margin:0;padding:0}
html,body{width:${W}px;height:${H}px;background:#ece8dd}
#root{position:relative;width:${W}px;height:${H}px;background:#ece8dd}
h1.headline{position:absolute;top:160px;left:0;width:100%;text-align:center;
font:600 64px serif;color:#1a2640}
.flip-container{display:inline-block;perspective:1000px;vertical-align:baseline;
height:1.2em;position:relative;min-width:300px}
.flip-card{position:relative;width:100%;height:100%;transform-style:preserve-3d}
.flip-front,.flip-back{position:absolute;width:100%;height:100%;
backface-visibility:hidden;display:flex;align-items:center;justify-content:center;
left:0;top:0}
.flip-back{transform:rotateX(-180deg);color:#C8A870;font-style:italic;white-space:nowrap}
</style>
<div id=root data-composition-id="check">
<h1 class=headline>tools are <span class=flip-container><span class=flip-card id=card>
<span class=flip-front>agentic workflows</span>
<span class=flip-back>blind</span>
</span></span></h1>
</div>
<script>
window.__setAngle = a => {
document.getElementById("card").style.transform = "rotateX(" + a + "deg)";
};
</script>`;
const browser = await puppeteer.launch({
headless: true,
executablePath: process.env.CHROME_PATH || undefined,
args: ["--no-sandbox", "--use-gl=angle", `--window-size=${W},${H}`],
});
const page = await browser.newPage();
await page.setViewport({ width: W, height: H });
await page.setContent(HTML, { waitUntil: "load" });
page.on("console", (m) => console.log("[page]", m.text()));
mkdirSync("/tmp/de-3d-module", { recursive: true });
function psnr(a, b) {
const r = spawnSync("ffmpeg", ["-i", a, "-i", b, "-lavfi", "psnr", "-f", "null", "-"],
{ encoding: "utf8" });
const m = String(r.stderr).match(/average:([\d.inf]+)/);
return m ? m[1] : "n/a";
}
const ANGLES = [0, 45, 90, 135, 180];
// refs: real CSS 3D
for (const a of ANGLES) {
await page.evaluate((x) => window.__setAngle(x), a);
await page.evaluate(() => new Promise((r) => requestAnimationFrame(() => setTimeout(r, 30))));
writeFileSync(`/tmp/de-3d-module/ref-${a}.png`, await page.screenshot({ type: "png" }));
}
// engine projection
const res = await page.evaluate(initThreeDProjectionInPage);
console.log("init:", JSON.stringify(res));
for (const a of ANGLES) {
await page.evaluate((x) => {
window.__setAngle(x);
window.__hf3d?.update();
}, a);
await page.evaluate(() => new Promise((r) => requestAnimationFrame(() => setTimeout(r, 30))));
const out = `/tmp/de-3d-module/proj-${a}.png`;
writeFileSync(out, await page.screenshot({ type: "png" }));
console.log(`angle=${a}: psnr=${psnr(`/tmp/de-3d-module/ref-${a}.png`, out)}`);
}
console.log(await browser.version());
await browser.close();