html-in-canvas-cracks
Real HTML, real layout, real scrolling โ rendered through a WebGPU pipeline with TSL shaders, feedback trails, and a live 3D code tunnel in the background. Click the page to crack it; click again to shatter chunks off into the tunnel. A small experiment in treating the DOM as a texture source rather than the final display surface.
Warning
Requires desktop Chrome 141+ with
chrome://flags/#canvas-draw-element
enabled.
This is the demo, please click around to see the effect. ๐
How It Works
Modern browsers can paint a live, laid-out DOM element
straight onto a 2D canvas via the new
CanvasRenderingContext2D.drawElement()
API. Once the page is a canvas, it's a texture. Once it's a
texture, every trick WebGPU knows is on the table.
This project wires that pipeline up end-to-end:
- A full-page "GitHub" layout lives in the regular DOM so selection, hover, scroll, and font metrics all behave like a real webpage.
- A hidden clone of that DOM is painted each frame into a 2D canvas that tracks the document's height (not just the viewport).
-
A
three.jsWebGPURendererpulls that canvas as aCanvasTextureand composites it through a multi-pass node-material pipeline. - Clicks fracture the page into 3D shards that sample the same live texture โ cracks propagate, chunks tilt and fall off into the tunnel behind.
- The page you see is the 3D composite โ the DOM underneath is hidden, but still receives clicks, keyboard input, and text selection.
Rendering Pipeline
Each frame goes through a few render targets before hitting the screen:
- Foreground RT. The HTML-textured base plane (pre-crack) or the shard meshes (post-crack) render alone into a transparent render target with depth. That isolation matters for compositing: the tunnel shows through wherever foreground alpha = 0 (e.g. through holes left by fallen shards).
- Tunnel + feedback RT. A separate 3D scene filled with drifting, fog-faded text quads (the "code tunnel") renders into a half-float render target. Before the tunnel draws, a full-screen quad with a slightly dim decay multiplier overwrites the previous frame in place โ this is a manual temporal feedback pass, giving every glowing element a long, exponential trail without any post-pass.
-
Composite.
PostProcessingwith a TSLoutputNodelayers the feedback RT and the foreground RT together through aBloomNode. Bloom is scoped to the background scene so the HTML stays crisp at its native resolution.
Everything uses half-float RTs in linear sRGB โ the color math works in the right space and bloom doesn't get blown out by the tone-mapper.
TSL Node Materials
Shaders are written as node graphs with three.js TSL
(three-shader-language) rather than strings of
WGSL. The pre-crack page is a
MeshBasicNodeMaterial with a UV-sampled
CanvasTexture. The tunnel's code planes are
distance-faded against the scene fog. Shards use a TSL
distance fog of their own that fades alpha (not
color) toward the far plane, so a receding shard reveals
the tunnel behind it instead of painting an opaque black
quad over it.
The Shard System
When you click the page, it cracks.
- A polygonal "root shard" covering the viewport is built lazily on the first click. Subsequent clicks propagate a randomized crack graph (a main split plus recursive branches) through whatever shard the cursor is on.
-
src/shatter.jsruns the crack lines through a DCEL face-walker to split the hit polygon into sub-polygons, then earcut-triangulates each one. - Every resulting shard becomes an extruded 3D polygon: CSS-pixel-space top and bottom caps joined by flat-shaded side walls. The sides show a flat "edge" material; the caps sample the live HTML texture through a per-vertex shader-computed UV so text and images stay aligned with the underlying document even as shards tumble.
-
Small interior shards (and some small edge ones) get a
falling-tumbling GSAP tween into the tunnel's fog. As
they pass
SHARD_FOG_FARtheir TSL fog alpha hits zero and they unblock the tunnel behind them. -
Crack outlines are drawn by a child mesh per shard โ thin
quads along each polygon edge, grown outward from the
click point via a per-vertex
growDelayattribute and a single progress uniform.
Coordinate System
Shards live in viewport-CSS-pixel space.
shardRoot applies a uniform scale of
2/vh with a negative Y (so VP-y-down maps
cleanly to world-y-down) and is anchored at the top-left
of the frustum. A crack clicked at (300, 200)
stays at (300, 200) after any resize โ the
HTML texture re-flows through the fixed shard outline as
the document relays out.
UVs are computed live in the vertex shader:
vpPx = positionLocal.xy + centroid
docY = vpPx.y + scrollY
uv = vec2(vpPx.x / sourceCssW, 1 - docY / sourceCssH)
So each shard is effectively a "fixed window onto the live DOM" โ scrolling the page reflows content behind the cracks without the cracks themselves moving.
The Code Tunnel
The background is a tunnel of floating code snippets. The
source strings are imported verbatim from the project
itself via Vite's ?raw import, so the
background is literally the code that renders it.
- Source is split on blank lines and re-joined into groups of up to three blocks, so each plane shows a coherent chunk with some breathing room.
- Planes are scattered within a cylindrical volume with a dead zone around the camera axis so nothing clips through the near plane.
-
Each plane renders to a small
CanvasTextureand aMeshBasicNodeMaterialwith custom glyph tinting.
Roughly 200 planes live at any moment. They drift forward
toward the camera, get recycled behind it, and smoothly
fog out via THREE.Fog(0x000000, near, far).
Because the tunnel renders into the feedback RT, each
glyph leaves a fading contrail that reinforces the depth
illusion.
A mouse-parallax tilt is wired to the tunnel group:
normalized cursor coordinates drive a lerped rotation with
a small TUNNEL_TILT_MAX cap, so the
background responds to the cursor without drifting into
full 3D-camera territory.
Interaction Layer
The clickable layer is the real DOM
(#source), not the 3D mesh. That keeps text
selection, link hovering, and accessibility behavior
working without any custom raycasting. The 3D composite
(#stage) sits on top with
pointer-events: none, so input passes through
to the canvas beneath.
Scroll, Resize, and Layout
Treating the DOM as a texture while keeping it scrollable turns out to have a few subtleties.
-
Fixed stage.
#stageisposition: fixed, so the browser pins the canvas to the viewport on the compositor thread. An earlier design usedposition: absolutewith a per-frametransform: translateY(scrollY)compensation โ that made VP-locked shards appear to shift vertically during scroll before snapping back once the main thread caught up. Fixed positioning makes that problem vanish. - Texture reflow, not geometry rescale. Shards store their geometry in CSS pixels and never resize. On a horizontal viewport resize, the HTML document reflows and the source canvas is resized; the shards' shader UVs then sample the new layout automatically. Cracks stay pinned to the pixel they were clicked at.
-
GPU texture reallocation on resize.
CanvasTexturein WebGPU doesn't re-allocate its GPU texture when the backing canvas'swidth/heightchange โ it silently scales the new content into the old-sized allocation.paint()detects a buffer-dimension change and callshtmlTexture.dispose()(preserving the JS object so shard materials' TSLtexture()refs keep pointing at the right node), forcing the renderer to allocate a freshGPUTextureat the new size on the next render. -
Boot hand-off. The screen mesh outputs
opaque black until the HTML texture has been painted at
least once (
screenReadyUuniform), so the foreground RT hasalpha = 1from frame zero and the tunnel can never bleed through while the pipeline warms up.bodyflips toreadyon the first successful pipeline frame, which reveals#sourceand fades in the native DOM layer underneath.
Controls
Keyboard:
-
xโ explode remaining shards outward into the tunnel. -
rโ reset shard positions to their original slots. -
tโ apply a small random tilt to each shard. -
oโ toggle OrbitControls on the camera (for debugging).
Add ?dev to the URL for the debug overlay,
Stats panel, and two draggable split dividers that slice
the viewport into three panes โ GPU composite, source
canvas, and raw HTML โ so you can see each stage of the
pipeline side-by-side.
Tech Stack
- three.js WebGPU renderer + TSL
- earcut for polygon triangulation
- GSAP for timed animations
-
Vite for the dev server and
?rawimports - Chrome's draw-element canvas API
Running Locally
npm install
npm run dev
Files of Note
-
src/main.jsโ pipeline setup, render passes, shard lifecycle, scroll/resize wiring -
src/shatter.jsโ crack-graph-to-polygons face walker built on earcut -
src/tunnel.jsโ background code tunnel (text quads, fog, recycling) src/sfx.jsโ small WebAudio utilities-
src/page.cssโ styling for the DOM layer that becomes the texture
License
MIT