Skip to content

Commit 935069a

Browse files
committed
update html-in-canva to new API
1 parent 5d566fa commit 935069a

2 files changed

Lines changed: 64 additions & 34 deletions

File tree

docs/labs.html

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ <h1><span class="accent">Snap</span><span class="accent-ink">DOM</span> Labs</h1
172172
</div>
173173

174174
<div class="btn-row">
175-
<button class="btn btn-ghost" id="btn-htmlcanvas-default" data-umami-event="Labs capture png">Capture (PNG)</button>
175+
<button class="btn btn-ghost" id="btn-htmlcanvas-default" data-umami-event="Labs capture png">Capture</button>
176176
<button class="btn btn-primary" id="btn-htmlcanvas-api" data-umami-event="Labs capture html-in-canvas">Capture via html-in-canvas</button>
177177
</div>
178178

@@ -222,19 +222,25 @@ <h1><span class="accent">Snap</span><span class="accent-ink">DOM</span> Labs</h1
222222
const outApi = document.getElementById('out-htmlcanvas-api');
223223
const statusApi = document.getElementById('status-htmlcanvas');
224224

225-
function isDrawElementImageAvailable() {
225+
// The WICG canvas-place-element spec evolved: Chrome ~130+ ships drawElement(),
226+
// earlier flagged builds shipped drawElementImage(). Detect either.
227+
function detectDrawApi() {
226228
try {
227229
const c = document.createElement('canvas');
228230
const ctx = c.getContext('2d');
229-
return ctx && typeof ctx.drawElementImage === 'function';
230-
} catch { return false; }
231+
if (!ctx) return null;
232+
if (typeof ctx.drawElement === 'function') return 'drawElement';
233+
if (typeof ctx.drawElementImage === 'function') return 'drawElementImage';
234+
return null;
235+
} catch { return null; }
231236
}
232237

233-
const available = isDrawElementImageAvailable();
238+
const drawApi = detectDrawApi();
239+
const available = !!drawApi;
234240
if (!available) {
235-
statusApi.innerHTML = '<span style="color:var(--rose)">drawElementImage not available. Enable chrome://flags/#canvas-draw-element</span>';
241+
statusApi.innerHTML = '<span style="color:var(--rose)">drawElement not available. Enable <code>chrome://flags/#canvas-draw-element</code> in Chrome.</span>';
236242
} else {
237-
statusApi.textContent = 'drawElementImage available';
243+
statusApi.textContent = `${drawApi}() available`;
238244
statusApi.style.color = 'var(--green)';
239245
}
240246

@@ -290,24 +296,32 @@ <h1><span class="accent">Snap</span><span class="accent-ink">DOM</span> Labs</h1
290296

291297
canvas.appendChild(wrapper);
292298

293-
const container = document.createElement('div');
294-
container.id = 'snapdom-html-in-canvas-temp';
295-
container.style.cssText = 'position:fixed;left:-9999px;top:0;visibility:hidden;';
296-
container.appendChild(canvas);
297-
document.body.appendChild(container);
299+
// Append directly to body, taken out of flow with position:fixed +
300+
// z-index:-1 so the body's background covers it visually while the
301+
// browser still paints it (paint record needed for drawElement).
302+
canvas.style.cssText = 'position:fixed;top:0;left:0;z-index:-1;';
303+
document.body.appendChild(canvas);
298304

299305
try {
300-
await new Promise(r => requestAnimationFrame(r));
306+
canvas.getBoundingClientRect();
307+
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
308+
301309
const ctx2d = canvas.getContext('2d');
302-
if (!ctx2d || typeof ctx2d.drawElementImage !== 'function')
303-
throw new Error('drawElementImage not available');
310+
const fn = ctx2d && (ctx2d[drawApi] || ctx2d.drawElement || ctx2d.drawElementImage);
311+
if (typeof fn !== 'function') throw new Error('drawElement / drawElementImage not available');
312+
304313
ctx2d.save();
305314
ctx2d.scale(dpr * scale, dpr * scale);
306-
ctx2d.drawElementImage(wrapper, 0, 0, width, height);
315+
fn.call(ctx2d, wrapper, 0, 0, width, height);
307316
ctx2d.restore();
308317
return canvas;
318+
} catch (e) {
319+
if (e && /paint record/i.test(e.message || '')) {
320+
throw new Error('Browser had no paint record for the element. Make sure the page is fully loaded and visible before capturing.');
321+
}
322+
throw e;
309323
} finally {
310-
try { document.body.removeChild(container); } catch {}
324+
try { canvas.remove(); } catch {}
311325
}
312326
}
313327
};

packages/plugins/html-in-canvas.js

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,32 @@
99

1010
const PLUGIN_NAME = 'html-in-canvas'
1111

12-
function isDrawElementImageAvailable() {
12+
/**
13+
* The WICG canvas-place-element spec evolved: Chrome ~130+ exposes drawElement(),
14+
* earlier flagged builds shipped drawElementImage(). Detect either.
15+
* @returns {'drawElement'|'drawElementImage'|null}
16+
*/
17+
function detectDrawApi() {
1318
try {
1419
const c = document.createElement('canvas')
1520
const ctx = c.getContext('2d')
16-
return ctx && typeof ctx.drawElementImage === 'function'
21+
if (!ctx) return null
22+
if (typeof ctx.drawElement === 'function') return 'drawElement'
23+
if (typeof ctx.drawElementImage === 'function') return 'drawElementImage'
24+
return null
1725
} catch {
18-
return false
26+
return null
1927
}
2028
}
2129

2230
/**
2331
* @returns {import('../../src/core/plugins.js').Plugin}
2432
*/
2533
export function htmlInCanvasPlugin() {
26-
const available = isDrawElementImageAvailable()
34+
const drawApi = detectDrawApi()
35+
const available = !!drawApi
2736
if (!available) {
28-
console.warn('[snapdom] html-in-canvas plugin: drawElementImage not available. Enable chrome://flags/#canvas-draw-element')
37+
console.warn('[snapdom] html-in-canvas plugin: drawElement / drawElementImage not available. Enable chrome://flags/#canvas-draw-element')
2938
}
3039

3140
return {
@@ -92,27 +101,34 @@ export function htmlInCanvasPlugin() {
92101

93102
canvas.appendChild(wrapper)
94103

95-
const container = document.createElement('div')
96-
container.id = 'snapdom-html-in-canvas-temp'
97-
container.style.cssText = 'position:fixed;left:-9999px;top:0;visibility:hidden;'
98-
container.appendChild(canvas)
99-
document.body.appendChild(container)
104+
// Append directly to body, taken out of flow with position:fixed + z-index:-1
105+
// so it sits behind the page's content (covered by body/main backgrounds)
106+
// while still being painted. visibility:hidden / opacity:0 / left:-9999px
107+
// skip the paint pass and trigger "No cached paint record".
108+
canvas.style.cssText = 'position:fixed;top:0;left:0;z-index:-1;'
109+
document.body.appendChild(canvas)
100110

101111
try {
102-
await new Promise(r => requestAnimationFrame(r))
112+
canvas.getBoundingClientRect()
113+
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)))
114+
103115
const ctx2d = canvas.getContext('2d')
104-
if (!ctx2d || typeof ctx2d.drawElementImage !== 'function') {
105-
throw new Error('drawElementImage not available')
116+
const fn = ctx2d && (ctx2d[drawApi] || ctx2d.drawElement || ctx2d.drawElementImage)
117+
if (typeof fn !== 'function') {
118+
throw new Error('drawElement / drawElementImage not available on this canvas context')
106119
}
107120
ctx2d.save()
108121
ctx2d.scale(dpr * scale, dpr * scale)
109-
ctx2d.drawElementImage(wrapper, 0, 0, width, height)
122+
fn.call(ctx2d, wrapper, 0, 0, width, height)
110123
ctx2d.restore()
111124
return canvas
125+
} catch (e) {
126+
if (e && /paint record/i.test(e.message || '')) {
127+
throw new Error('Browser had no paint record for the element. Make sure the document is fully loaded and visible before calling html-in-canvas (drawElement requires a real paint pass).')
128+
}
129+
throw e
112130
} finally {
113-
try {
114-
document.body.removeChild(container)
115-
} catch {}
131+
try { canvas.remove() } catch {}
116132
}
117133
}
118134
}

0 commit comments

Comments
 (0)