* feat(skills): teach hyperframes skill the HTML-in-Canvas API Vendored skill predates upstream v0.5.1, which added the drawElementImage HTML-in-Canvas guide and the vfx-iphone-device / vfx-liquid-glass / vfx-portal catalog blocks. Without that reference, agents asked to build 'live HTML on a 3D phone screen' compositions had no idea the API existed and produced renders where the device screen was blank or static. - Add references/html-in-canvas.md adapted from the upstream guide, with an OD-specific note about render-loop re-capture (the most common reason output 'looks dead' inside a generated composition). - Cross-link the new reference from SKILL.md and add triggers for 'html in canvas', 'drawElementImage', 'html shader', and the two most-requested vfx blocks. Daemon render path is unchanged: 'npx hyperframes render' already auto-enables --enable-features=CanvasDrawElement, and we always pull the latest published hyperframes via npx, so no version pinning needs to move. * docs(skills): wait for canvas paint in hyperframes HTML-in-Canvas examples The drawElementImage API only refreshes its element snapshot when the canvas paints. Calling it during initial script evaluation can throw because no snapshot exists yet, and calling it later from outside a paint event silently reads the previous snapshot. On HyperFrames' seek-driven renders that surfaces as a failed or stale first texture. - Drive the basic capture example from canvas.onpaint and kick it off with requestPaint() instead of calling drawElementImage at script eval time. - Rewrite the per-frame re-capture pattern to put drawElementImage inside onpaint and call requestPaint() from the render loop, so each frame sees a fresh snapshot rather than the previous one. - Add a callout explaining the paint-event requirement so agents do not regress to the script-eval-time pattern. Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code) * docs(skills): add vfx-portal trigger to hyperframes skill The hyperframes skill's reference callout already names vfx-portal alongside vfx-iphone-device and vfx-liquid-glass as effects that should auto-load references/html-in-canvas.md, but the triggers list only includes the other two. A prompt like "make a vfx-portal clip" therefore misses the HTML-in-Canvas guidance the new reference adds. - Add "vfx-portal" to triggers so the trigger surface matches the documented entry points. Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code)
6.8 KiB
HTML-in-Canvas
Render live HTML as WebGL textures — GPU shaders, 3D geometry, and cinematic effects on any DOM content.
The HTML-in-Canvas API (drawElementImage) lets you capture live, rendered DOM elements directly into a canvas at GPU speed. This means you can take any HTML — dashboards, forms, landing pages, app UIs — and render them as textures in WebGL scenes with shaders, 3D transformations, and post-processing effects.
Chrome flag required for live preview only. The
drawElementImageAPI is experimental.
- Open
chrome://flags/#canvas-draw-elementin Chrome or Brave- Set CanvasDrawElement to Enabled
- Restart the browser
HyperFrames enables this flag automatically during rendering (
--enable-features=CanvasDrawElement), so rendered videos work without manual setup. The flag is only needed for live preview in the Studio.
When this skill runs inside Open Design, the daemon shells out to npx hyperframes render, which inherits the auto-enable. You do not need to add browser flags or pass extra CLI args from the agent.
How it works
- Place HTML content inside a
<canvas layoutsubtree>element - The browser renders the HTML children as normal DOM
- Wait for the canvas to paint, then call
ctx.drawElementImage(element, x, y, w, h)to capture the rendered pixels - Use the canvas as a Three.js texture, apply shaders, map to 3D geometry
Always capture from a paint event. The element snapshot the API draws from is only refreshed when the canvas paints. Calling
drawElementImageduring initial script evaluation can throw because the first snapshot does not exist yet; calling it outsidepaintafter that point silently reads the previous snapshot. Drive both first-time capture and per-frame updates fromcanvas.onpaint, and usecanvas.requestPaint()to ask for a fresh snapshot.
<!-- 1. HTML content lives inside the canvas -->
<canvas id="capture" layoutsubtree width="1920" height="1080">
<div class="my-dashboard">
<h1>Revenue: $4.2M</h1>
<div class="chart">...</div>
</div>
</canvas>
<!-- 2. WebGL canvas for 3D rendering -->
<canvas id="theater" width="1920" height="1080"></canvas>
// 3. Capture HTML to canvas — wait for paint so the element snapshot exists
var capCanvas = document.getElementById("capture");
var ctx = capCanvas.getContext("2d");
var texture, material;
capCanvas.onpaint = function () {
ctx.drawElementImage(capCanvas.querySelector(".my-dashboard"), 0, 0, 1920, 1080);
if (!texture) {
// 4. Use as Three.js texture
texture = new THREE.CanvasTexture(capCanvas);
material = new THREE.MeshBasicMaterial({ map: texture });
} else {
texture.needsUpdate = true;
}
};
// Kick off the first paint; subsequent re-captures call requestPaint() again
capCanvas.requestPaint();
What makes this different
Traditional approaches like html2canvas re-parse and re-render the DOM in JavaScript — they're slow, lossy, and miss CSS features like backdrop-filter, complex shadows, and web fonts. The drawElementImage API uses the browser's own compositor, so:
- Pixel-perfect — every CSS feature is supported because the browser renders it natively
- GPU-accelerated — captures at 60fps, fast enough for real-time animation
- Live content — the HTML can animate, scroll, and change between captures
- Multiple captures simultaneously — no nesting restrictions; multiple
<canvas layoutsubtree>elements can capture different content in the same composition
Feature detection
Always feature-detect before using the API. Compositions should fall back gracefully for browsers without the flag enabled. (Render path is always fine — the fallback only matters when a user opens the composition in a browser without CanvasDrawElement.)
function isSupported() {
var tc = document.createElement("canvas");
if (!("layoutSubtree" in tc)) return false;
tc.setAttribute("layoutsubtree", "");
var ctx = tc.getContext("2d");
return ctx && typeof ctx.drawElementImage === "function";
}
if (isSupported()) {
ctx.drawElementImage(element, 0, 0, w, h);
} else {
// Fallback: draw text directly on canvas, use static image, etc.
}
Re-capturing every frame
For animated content (scrolling, transitions, counters), drive the capture from the canvas's paint event and ask for a fresh snapshot each frame with requestPaint(). Calling drawElementImage directly from the render loop reads the previous paint's snapshot, which on seek-driven HyperFrames renders shows up as a stale or frozen first texture.
// Capture runs whenever the canvas paints, so the snapshot is always fresh
capCanvas.onpaint = function () {
ctx.clearRect(0, 0, W, H);
ctx.drawElementImage(htmlElement, 0, 0, W, H);
texture.needsUpdate = true;
};
function render() {
// Update HTML state
scrollContainer.style.transform = "translateY(-" + scrollOffset + "px)";
counterEl.textContent = Math.round(currentValue);
// Schedule a fresh snapshot; the onpaint handler above runs the capture
capCanvas.requestPaint();
// Render 3D scene with updated texture
renderer.render(scene, camera);
}
When a HyperFrames timeline drives the underlying HTML (counter ticks, scroll animation), the render loop must run on every frame the texture is visible — otherwise the WebGL surface freezes on the first capture and the user sees a static screen embedded in your 3D scene. This is the most common reason an HTML-in-Canvas composition "looks dead" after rendering.
Catalog blocks
Install all HTML-in-Canvas blocks at once:
npx hyperframes add html-in-canvas
Or install individually:
| Block | Description | Install |
|---|---|---|
| Liquid Glass | Voronoi glass fracture with parallax reveal | npx hyperframes add vfx-liquid-glass |
| iPhone & MacBook | Real 3D GLTF devices with live HTML screens | npx hyperframes add vfx-iphone-device |
| Text Cursor | Dramatic text reveal with chromatic shadows | npx hyperframes add vfx-text-cursor |
| Portal | Dimension breach with volumetric light | npx hyperframes add vfx-portal |
| Shatter | HTML shatters into glass fragments | npx hyperframes add vfx-shatter |
| Magnetic | Magnetic field particle visualization | npx hyperframes add vfx-magnetic |
| Liquid Background | Organic liquid simulation | npx hyperframes add vfx-liquid-background |
Block reference pages live at https://hyperframes.heygen.com/catalog/blocks/<name>.
Rendering
HyperFrames enables the Chrome flag automatically during rendering. No special configuration needed:
npx hyperframes render --output my-video.mp4
For Docker renders, the flag is also enabled automatically inside the container. Inside Open Design, the daemon's npx hyperframes render call (apps/daemon/src/media.ts) inherits the same default — you don't need to thread anything through.