Specimen 004 · Experiment
Floating real HTML on a canvas swamp
This site's text is drawn onto the water with the new HTML-in-Canvas API, so live, clickable HTML can bob, reflect and ripple. What the spec asks for, and what broke on the way.
The idea
The HTML-in-Canvas proposal lets a canvas draw its own child elements with drawElementImage(). What comes out is the browser’s real rendering of live HTML, so the text stays selectable, links stay clickable and screen readers still see it. Most demos stop at “look, a form inside a canvas”. I wanted to find out what it is actually good for, so every card on this site floats on the swamp.
What it does here
- The headline sits at the waterline, with its reflection rippling underneath.
- Cards bob on the surface, cast reflections and have water lapping across their lower edge.
- When a ripple ring passes through a card, the card wobbles with it.
- The duckweed treats the cards as solid and heaps up against their edges.
The point is not any one effect. It is that the page’s HTML now lives in the same drawing pipeline as the water, the weed and the crocodile, so it can react to them.
What the spec asks for
- The canvas needs
content="drawable", and every element you draw needs adrawableattribute, or the call throws. Earlier builds usedlayoutsubtreeon the canvas instead. I set both. - Nothing can be drawn until the browser has recorded a snapshot of the element, which it announces with a
paintevent. Before that, the call throwsNo cached paint record for element. - Drawing HTML taints the canvas, so its pixels can never be read back.
- In the current revision, the call moves the element’s hit testing to wherever it was drawn. In the earlier one it returned a matrix that you wrote to
style.transformyourself. Both are handled.
What broke
- The first sign never drew. My first attempt did not set
drawable, so in Chrome Canary it probably failed every frame and quietly fell back to plain HTML. - Everything narrow got stretched. I sized the canvas bitmap from the element’s width, but the canvas itself filled its container, so anything with a
max-widthcame out stretched sideways. - A hundred API calls a frame. Each reflection strip was its own
drawElementImagecall. Now each element is drawn once, those pixels are copied to a small scratch canvas, and the reflection and the ripple are built from the copy. - Five seconds of nothing. The limits for giving up were counted in frames. At 20 frames a second that meant five seconds of blank content before recovering. They are in milliseconds now.
Keeping it safe
The HTML that is served never changes. Elements move into canvases at runtime, and only where the API exists, so crawlers and every other browser get the plain page.
Two safety nets sit on top. If a browser can paint HTML into a canvas but cannot route clicks to it, the page notices, using elementFromPoint on the drawn element, and puts everything back as ordinary HTML. If the API keeps throwing, everything comes back within about a second. The sign in the footer says which of these happened.
Try it
You need Chrome Canary 149 or later with chrome://flags/#canvas-draw-element enabled, or Chrome 148 to 150 with the origin trial. Everywhere else you are reading the static version of this page, which has all of the words and none of the water effects.