Maintainers
Pricing
Enterprise
More
More
Documentation
Try Live
Rankings
Install
Blog
Docs7
Ask
New
Add Docs
snapDOM
https://github.com/zumerlab/snapdom
Admin
snapDOM captures HTML elements to images with exceptional speed and accuracy.
Tokens:
250,533
Snippets:
2,002
Trust Score:
7.4
License:
MIT
Update:
1 month ago
Context
Chat
Benchmark
84.34
Latest
Show doc for...
Code
Info
Show Results
Tokens:
Raw
Copy
Link
Tokens:
Raw
Copy
Link
### Implement clone-only and full-pipeline plugins Source: https://github.com/zumerlab/snapdom/blob/main/docs/labs/capture-stages/index.html Example setup demonstrating a plugin that only requires the clone stage and a default plugin that triggers the full rendering pipeline. ```javascript import { snapdom } from 'https://unpkg.com/@zumer/snapdom@latest/dist/snapdom.mjs' // ── the element under capture ──────────────────────────────────────────────────────── const sample = document.getElementById('sample') sample.innerHTML = Array.from({ length: 36 }, (_, i) => ` <article> <h4>Item ${i + 1}</h4> <p>Short description for row ${i + 1}.</p> <button>Open ${i + 1}</button> </article>`).join('') // ── two plugins, same knob, different depth ────────────────────────────────────────── // 'clone': stamps the frozen tree. No pixels asked for, so no render runs. const stampPlugin = () => { let html = '' return { name: 'stamp', needs: 'clone', afterClone(ctx) { const tag = document.createElement('b') tag.textContent = ' [stamped]' ctx.clone.querySelector('h4')?.appendChild(tag) html = ctx.clone.querySelector('article')?.outerHTML || '' }, defineExports: () => ({ stamped: async () => html }), } } // no declaration: the whole pipeline, the historical behaviour const plainPlugin = () => ({ name: 'plain' }) const REPS = 7 const median = (a) => a.slice().sort((x, y) => x - y)[Math.floor(a.length / 2)] async function time(makePlugins) { const runs = [] let last for (let i = 0; i < REPS; i++) { const t = performance.now() last = await snapdom(sample, { plugins: makePlugins(), cache: 'disabled', burst: false }) runs.push(performance.now() - t) } return { ms: median(runs), result: last } } const cards = document.getElementById('cards') const el = (html) => { const d = document.createElement('div'); d.innerHTML = html.trim(); return d.firstElementChild } document.getElementById('go').addEventListener('click', async () => { const status = document.getElementById('status') status.textContent = `capturing ${sample.querySelectorAll('*').length + 1} nodes, ${REPS} runs each…` cards.innerHTML = '' const specs = [ { stage: 'clone', decl: "needs: 'clone'", make: () => [stampPlugin()] }, { stage: 'render', decl: '(no declaration)', make: () => [plainPlugin()] }, ] const done = [] for (const spec of specs) done.push({ ...spec, ...(await time(spec.make)) }) const worst = Math.max(...done.map((d) => d.ms)) for (const d of done) { const card = el(` <div class="card"> <h3>${d.stage}</h3> <code class="decl">${d.decl}</code> <div class="ms">${d.ms.toFixed(1)}<small> ms</small></div> <div class="bar"><i style="width:${(d.ms / worst * 100).toFixed(1)}%"></i></div> <div class="out"></div> </div>`) const out = card.querySelector('.out') // what this stage DID produce if (d.stage === 'clone') { const pre = document.createElement('pre') pre.className = 'outline' pre.textContent = await d.result.toStamped() out.appendChild(pre) } // and the same door on every card: ask it for the image try { const img = await d.result.toPng() img.style.maxWidth = '100%' out.appendChild(img) } catch (e) { out.appendChild(el(`<div class="err"><b>result.toPng()</b><br>${String(e.message)}</div>`)) } cards.appendChild(card) } const [clone, render] = done status.textContent = `${sample.querySelectorAll('*').length + 1} nodes, median of ${REPS} runs` document.getElementById('verdict').innerHTML = `Stopping at <b>clone</b> skipped ${Math.max(0, 100 - clone.ms / render.ms * 100).toFixed(0)}% of the capture. ` + 'These timings compare the clone stage with a full capture of this element. ' + 'Use the clone stage when your plugin only needs structured data; image exports require render.' }) ``` -------------------------------- ### CDN installation for SnapDOM Source: https://github.com/zumerlab/snapdom/blob/main/docs/plugins.html Example of using an import map and module scripts to load SnapDOM and plugins via CDN without local installation. ```html <script type="importmap"> { "imports": { "@zumer/snapdom": "https://unpkg.com/@zumer/snapdom@latest/dist/snapdom.mjs" } } </script> <script type="module"> import { snapdom } from '@zumer/snapdom'; import { filter } from 'https://esm.sh/@zumer/snapdom-plugins@latest/filter?external=@zumer/snapdom'; </script> ``` -------------------------------- ### Install with npm and yarn Source: https://github.com/zumerlab/snapdom/blob/main/docs/docs/index.html Installation commands for SnapDOM using npm and yarn. ```bash npm i @zumer/snapdom@latest ``` ```bash yarn add @zumer/snapdom@latest ``` -------------------------------- ### Development setup and test commands Source: https://github.com/zumerlab/snapdom/blob/main/README.md Run these commands in order from the repository checkout to install dependencies, compile, lint, run type and bundle tests, execute browser tests with all browsers, and run the pack test. `npm test` checks lint without modifying files; use `npm run lint:fix` to apply fixes. ```sh npm install npx playwright install npm run compile npm run lint npm run test:types npm run test:bundle BROWSER=all npx vitest run __tests__ --browser.headless npm run test:pack ``` -------------------------------- ### Install SnapDOM v3 and plugins Source: https://github.com/zumerlab/snapdom/blob/main/docs/blog/snapdom-v3/index.html Installation commands for SnapDOM v3 and the official plugins package. Run these in your project directory to upgrade. ```bash npm install @zumer/snapdom@3 # If your app uses the official plugins: npm install @zumer/snapdom-plugins@3 ``` -------------------------------- ### Usage example with plugins Source: https://github.com/zumerlab/snapdom/blob/main/docs/plugins.html Demonstrates how to initialize SnapDOM with filter and timestamp-overlay plugins and generate a PNG. ```javascript import { snapdom } from '@zumer/snapdom'; import { filter } from '@zumer/snapdom-plugins/filter'; import { timestampOverlay } from '@zumer/snapdom-plugins/timestamp-overlay'; const result = await snapdom(element, { plugins: [filter({ preset: 'grayscale' }), timestampOverlay()] }); const png = await result.toPng(); ``` -------------------------------- ### Install SnapDOM and Chart.js via npm Source: https://github.com/zumerlab/snapdom/blob/main/docs/how-to/save-chart-js-as-png/index.html Use this command to install the required packages in your project. ```bash npm install @zumer/snapdom@latest chart.js ``` -------------------------------- ### Install plugins with npm Source: https://github.com/zumerlab/snapdom/blob/main/packages/plugins/README.md Install the plugins next to a matching core. Plugins 3.x require a 3.x core. ```bash npm install @zumer/snapdom@latest @zumer/snapdom-plugins@latest ``` -------------------------------- ### Install SnapDOM and Playwright Source: https://github.com/zumerlab/snapdom/blob/main/docs/blog/dom-capture-boundaries/index.html Install the SnapDOM package and Playwright, then download the Chromium browser binary required by the script. ```bash npm install @zumer/snapdom@latest playwright npx playwright install chromium ``` -------------------------------- ### Install SnapDOM via npm Source: https://github.com/zumerlab/snapdom/blob/main/docs/index.html Install the latest version of SnapDOM using npm. This is the standard package manager installation method. ```bash npm i @zumer/snapdom@latest ``` -------------------------------- ### Initialize SnapDOM and Capture Source: https://github.com/zumerlab/snapdom/blob/main/docs/index.html Basic import and setup for capturing DOM elements using SnapDOM. ```javascript import { snapdom } from 'https://unpkg.com/@zumer/snapdom@latest/dist/snapdom.mjs' const pluginRolls = [ { mod: 'filter', fn: 'filter', opts: { preset: 'vintage' }, label: "filter({ preset: 'vintage' })" }, { mod: 'filter', fn: 'filter', opts: { preset: 'dramatic' }, label: "filter({ preset: 'dramatic' })" }, { mod: 'filter', fn: 'filter', opts: { preset: 'grayscale' }, label: "filter({ preset: 'grayscale' })" }, { mod: 'filter', fn: 'filter', opts: { preset: 'invert' }, label: "filter({ preset: 'invert' })" }, { mod: 'color-tint', fn: 'colorTint', opts: { color: '#2D5BFF', opacity: .9 }, label: "colorTint({ color: '#2D5BFF' })" }, { mod: 'color-tint', fn: 'colorTint', opts: { color: '#FF6B35', opacity: .9 }, label: "colorTint({ color: '#FF6B35' })" }, { mod: 'timestamp-overlay', fn: 'timestampOverlay', opts: { format: 'time', position: 'bottom-right', fontSize: 26 }, label: "timestampOverlay({ format: 'time' })" } ] let captureBusy = false let captureCount = 0 let lastPlugin = -1 let starShown = false function nextPlugin() { if (captureCount++ === 0) return null let index = lastPlugin while (index === lastPlugin) index = Math.floor(Math.random() * pluginRolls.length) lastPlugin = index return pluginRolls[index] } async function captureHero() { if (captureBusy || document.querySelector('.star-overlay')) return captureBusy = true const button = document.getElementById('hero-shutter') const stage = document.getElementById('hero-stage') const tray = document.getElementById('hero-tray') const flash = document.getElementById('hero-flash') button.disabled = true try { flash.classList.remove('flashing') void flash.offsetWidth flash.classList.add('flashing') const roll = nextPlugin() let plugins = [] let label = 'snapdom.toPng()' if (roll) { try { const plugin = await import(`https://esm.sh/@zumer/snapdom-plugins@latest/${roll.mod}?external=@zumer/snapdom`) plugins = [plugin[roll.fn](roll.opts)] label = roll.label } catch (error) { console.warn('Hero plugin failed; using a plain capture.', error) } } const started = performance.now() const image = await snapdom.toPng(stage, { scale: .32, embedFonts: false, cache: 'auto', exclude: ['#hero-flash'], plugins }) const print = document.createElement('div') print.className = `print r${(tray.childElementCount % 2) + 1}` image.alt = 'Mr. ``` -------------------------------- ### Setup renderer, plugin, and card mutation data Source: https://github.com/zumerlab/snapdom/blob/main/demos/d-plugin-webgl-time-tunnel.html Creates the renderer, plugin, and defines mutable card data with palettes, titles, and copy texts. This is the setup for the demo's interactive mutation. ```javascript const renderer = new TunnelRenderer(document.querySelector('#gl')) const tunnelPlugin = timeTunnel(renderer) const source = document.querySelector('#source-card') const palettes = ['#c8ff45', '#5ee8db', '#816dff', '#ff5d8f', '#ffb347'] const titles = ['Signal found', 'Memory leak', 'Ghost protocol', 'Future echo', 'Last contact'] const copies = ['Una señal desconocida atraviesa la interfaz.', 'El sistema recuerda algo que nunca ocurrió.', 'La copia comienza a observar al original.', 'El próximo estado ya está respondiendo.', 'La transmisión termina antes de comenzar.'] let stateIndex = 0 ``` -------------------------------- ### Run local benchmarks and tests Source: https://github.com/zumerlab/snapdom/blob/main/BENCHMARKS.md Commands to install dependencies, compile the project, and execute benchmark suites. ```sh npm install npx playwright install npm run compile npm run test:benchmark npm run test:benchmark:legacy npx vitest run __tests__/category.capabilities.test.js --browser.headless --reporter=verbose ``` -------------------------------- ### Canvas drawing setup Source: https://github.com/zumerlab/snapdom/blob/main/demos/d5-demo-gallery.html Gets the 2D context of a canvas element and draws a green rectangle. Used to demonstrate capturing canvas content. ```javascript const ctx = document.getElementById('myCanvas').getContext('2d'); ctx.fillStyle = "green"; ctx.fillRect(20, 20, 100, 100); ``` -------------------------------- ### Create plugin package directory and initialize npm Source: https://github.com/zumerlab/snapdom/blob/main/PLUGIN_SPEC.md Create a directory for your SnapDOM plugin and initialize a package.json with npm init -y. ```bash mkdir snapdom-plugin-yourname && cd $_ npm init -y ``` -------------------------------- ### Main setup and refresh loop with snapdom capture Source: https://github.com/zumerlab/snapdom/blob/main/docs/labs/webgl-seamless-dom/index.html Initializes the renderer and plugin, sets up a refresh loop that captures DOM snapshots and exports to WebGL. Includes event listeners for theme swatches and text input synchronization. ```javascript const renderer = new SeamlessRenderer(document.querySelector('#gl')) const plugin = seamlessWebgl(renderer) const source = document.querySelector('#dom-source') const swatchesEl = document.querySelector('#themeSwatches') let dirty = false let working = false async function refresh () { dirty = true if (working) return working = true while (dirty) { dirty = false const capture = await snapdom(source, { plugins: [plugin] }) await capture.to('webgl') } working = false } function syncText () { document.querySelector('#source-title').textContent = document.querySelector('#proxy-title').innerText document.querySelector('#source-copy').textContent = document.querySelector('#proxy-copy').innerText refresh() } function insertPlainText (text) { const selection = window.getSelection() if (!selection.rangeCount) return const range = selection.getRangeAt(0) range.deleteContents() const node = document.createTextNode(text) range.insertNode(node) range.setStartAfter(node) range.collapse(true) selection.removeAllRanges() selection.addRange(range) } COLORS.forEach((c, i) => { const btn = document.createElement('button') btn.className = 'theme' + (i === 0 ? ' active' : '') btn.style.setProperty('--color', c) btn.setAttribute('aria-label', `Theme ${c}`) btn.addEventListener('click', () => { swatchesEl.querySelector('.theme.active')?.classList.remove('active') btn.classList.add('active') source.style.setProperty('--local-accent', c) refresh() }) swatchesEl.appendChild(btn) }) document.querySelector('#proxy-title').addEventListener('input', syncText) document.querySelector('#proxy-copy').addEventListener('input', syncText) ``` -------------------------------- ### Convert HTML element to SVG with snapdom.toSvg() Source: https://github.com/zumerlab/snapdom/blob/main/docs/how-to/html-to-svg/index.html Install @zumer/snapdom, select the rendered element, and capture it. The example uses embedFonts: true to include fonts in the SVG. ```javascript import { snapdom } from '@zumer/snapdom' const element = document.querySelector('#card') const svg = await snapdom.toSvg(element, { embedFonts: true }) document.body.appendChild(svg) ``` -------------------------------- ### Capture full page with SnapDOM Source: https://github.com/zumerlab/snapdom/blob/main/docs/how-to/capture-full-page/index.html Install @zumer/snapdom, select the document root, and capture it as a PNG. The example embeds fonts and includes outer shadows, then downloads the result as 'full-page.png'. ```javascript const page = document.documentElement const capture = await snapdom(page, { embedFonts: true, outerShadows: true, }) await capture.download({ format: 'png', filename: 'full-page' }) ``` -------------------------------- ### Create a plugin from the template with degit Source: https://github.com/zumerlab/snapdom/blob/main/packages/plugins/README.md Scaffolds a new plugin from the official template and installs the latest snapdom as a dev dependency. See PLUGIN_SPEC.md for the hook specification and CONTRIBUTING_PLUGINS.md to get listed. ```bash npx degit zumerlab/snapdom/packages/plugin-template my-plugin cd my-plugin && npm install --save-dev @zumer/snapdom@latest ``` -------------------------------- ### WebGL setup and shared vertex shader Source: https://github.com/zumerlab/snapdom/blob/main/demos/d-webgl-shatter.html Gets the WebGL context and defines a common vertex shader used by all effects to position a textured quad. The shader converts screen pixel coordinates to clip space. ```javascript const card = document.getElementById('card') const applyBtn = document.getElementById('applyBtn') const resetBtn = document.getElementById('resetBtn') const hintText = document.getElementById('hintText') const fx = document.getElementById('fx') const gl = fx.getContext('webgl', { alpha: true, premultipliedAlpha: false }) // Every effect positions a textured quad the same way (center/size/ // rotation in screen px → clip space), so all four fragment shaders // share one vertex shader; only the pixel logic differs per program. const VS = ` attribute vec2 aPos; uniform vec2 uResolution; uniform vec2 uCenter; uniform vec2 uSize; uniform float uRotation; varying vec2 vUV; void main() { vec2 local = (aPos - 0.5) * uSize; float c = cos(uRotation); float s = sin(uRotation); vec2 rotated = vec2(local.x * c - local.y * s, local.x * s + local.y * c); vec2 pos = uCenter + rotated; vec2 clip = (pos / uResolution) * 2.0 - 1.0; clip.y = -clip.y; gl_Position = vec4(clip, 0.0, 1.0); vUV = aPos; } ` ``` -------------------------------- ### Program creation and buffer setup Source: https://github.com/zumerlab/snapdom/blob/main/demos/d-webgl-shatter.html Creates all effect programs, sets up a quad buffer, and enables blending. ```javascript const programs = { plain: compileProgram(FS_PLAIN), ripple: compileProgram(FS_RIPPLE), glitch: compileProgram(FS_GLITCH), dissolve: compileProgram(FS_DISSOLVE), metal: compileMetalProgram() } ``` ```javascript const quadBuf = gl.createBuffer() gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf) gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]), gl.STATIC_DRAW) gl.enable(gl.BLEND) gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) ``` -------------------------------- ### Export React component as PNG with SnapDOM Source: https://github.com/zumerlab/snapdom/blob/main/docs/how-to/export-react-component-as-image/index.html Install @zumer/snapdom, attach a ref to the rendered element, and call snapdom.toPng(ref.current) in a click handler. The example uses snapdom(cardRef.current, { embedFonts: true }) and capture.download({ format: 'png', filename: 'share-card' }). Capture must run in the browser, not during server rendering. ```jsx import { useRef } from 'react' import { snapdom } from '@zumer/snapdom' export function ShareCard() { const cardRef = useRef(null) const save = async () => { const capture = await snapdom(cardRef.current, { embedFonts: true }) await capture.download({ format: 'png', filename: 'share-card' }) } return <> <article ref={cardRef}>Your component</article> <button onClick={save}>Download PNG</button> </> } ``` -------------------------------- ### Minimal capture component (Composition API) Source: https://github.com/zumerlab/snapdom/blob/main/docs/guides/vue/index.html A complete Vue 3 single-file component that binds a template ref, installs @zumer/snapdom, and calls snapdom.toPng(cardRef.value) from a button handler. The template ref name must match the variable declared in the script setup. SnapDOM returns an HTMLImageElement ready to append. ```vue <script setup> import { ref } from 'vue'; import { snapdom } from '@zumer/snapdom'; const cardRef = ref(null); async function capture() { if (!cardRef.value) return; const img = await snapdom.toPng(cardRef.value, { scale: 2 }); document.body.appendChild(img); } </script> <template> <div> <div ref="cardRef" class="card"> <h3>Hello SnapDOM</h3> <p>This whole div will be captured.</p> </div> <button @click="capture">Capture</button> </div> </template> ``` -------------------------------- ### Initialize renderer and plugin Source: https://github.com/zumerlab/snapdom/blob/main/docs/labs/webgl-live-mirror/index.html Sets up the WebGLMirror instance and the custom plugin, then defines the updateMirror function that uses snapdom with the plugin. ```javascript const editable = document.querySelector('#lwmEditable') const renderer = new WebGLMirror(document.querySelector('#lwmGl')) const mirrorPlugin = webglMirror(renderer) let dirty = false let working = false // Rapid typing collapses into exactly one trailing capture: `dirty` is // re-checked right after each capture finishes, so bursts of input never // pile up a queue of stale renders — only the latest edit ever wins. async function updateMirror() { dirty = true if (working) return working = true while (dirty) { dirty = false const capture = await snapdom(editable, { plugins: [mirrorPlugin] }) await capture.toWebgl() } working = false } ``` -------------------------------- ### Setup and capture cleanup for stop-motion Source: https://github.com/zumerlab/snapdom/blob/main/demos/d-plugin-stop-motion-studio.html Initializes the stop-motion studio: imports snapdom and gifExport, selects DOM elements, sets up state variables, and defines captureCleanup to hide onion skin and selection before cloning, then removes them from the clone. Also defines makeObject to create draggable objects with position, rotation, and color. ```javascript import { snapdom } from '../dist/snapdom.mjs' import { gifExport } from '../packages/plugins/gif-export.js' const svgNS = 'http://www.w3.org/2000/svg' const scene = document.querySelector('#scene') const drawing = document.querySelector('#drawing') const onion = document.querySelector('#onion') const playback = document.querySelector('#playback') const framesNode = document.querySelector('#frames') const color = document.querySelector('#color') const status = document.querySelector('#status') const frames = [] let mode = 'select' let selectedObject = null let selectedFrame = -1 let nextId = 1 let dragging = null let drawingPath = null let drawPoints = [] function captureCleanup(onCapture) { let selected = null let onionWasVisible = false return { name: 'stop-motion-frame', beforeClone(ctx) { selected = ctx.element.querySelector('.selected') selected?.classList.remove('selected') onionWasVisible = onion.classList.contains('visible') onion.classList.remove('visible') }, afterClone(ctx) { selected?.classList.add('selected') if (onionWasVisible) onion.classList.add('visible') ctx.clone.querySelectorAll('.object').forEach(node => { node.classList.remove('selected') node.style.setProperty('outline', 'none', 'important') }) ctx.clone.querySelector('#onion')?.remove() ctx.clone.querySelector('#playback')?.remove() }, afterRender(ctx) { onCapture(ctx.dataURL) } } } function makeObject(kind, data = {}) { const object = document.createElement('div') object.className = 'object' object.dataset.id = data.id ?? nextId++ object.dataset.kind = kind object.style.setProperty('--x', `${data.x ?? 50}%`) object.style.setProperty('--y', `${data.y ?? 50}%`) object.style.setProperty('--rotation', `${data.rotation ?? 0}deg`) object.style.setProperty('--color', data.color || color.value) object.innerHTML = '<span class= ``` -------------------------------- ### Install SnapDOM as dev dependency Source: https://github.com/zumerlab/snapdom/blob/main/CONTRIBUTING_PLUGINS.md Install SnapDOM v3 as a dev dependency using npm. ```bash npm install --save-dev @zumer/snapdom@latest ``` -------------------------------- ### snapdom.preCapture() → void Source: https://github.com/zumerlab/snapdom/blob/main/docs/llms-full.txt Performs any pre-capture setup that may be needed before capturing. This is a static method that can be called to prepare the environment. ```APIDOC ## snapdom.preCapture() ### Description Performs any pre-capture setup that may be needed before capturing. This is a static method that can be called to prepare the environment. ### Method snapdom.preCapture ### Endpoint snapdom.preCapture() ### Parameters None ### Returns - **void** - No return value. ### Example ```js snapdom.preCapture(); ``` ``` -------------------------------- ### Example community plugin entry Source: https://github.com/zumerlab/snapdom/blob/main/CONTRIBUTING_PLUGINS.md Example row for a watermark plugin in the community plugins table. ```text | snapdom-plugin-watermark | Add text or image watermarks to captures | transform | snapdom-plugin-watermark | https://github.com/you/snapdom-plugin-watermark | @you | ``` -------------------------------- ### Initialize preCapture for performance Source: https://github.com/zumerlab/snapdom/blob/main/docs/docs/cache/index.html Call 'snapdom.preCapture()' during setup to allow SnapDOM to prepare captures based on user intent events like focus or pointer-enter. ```javascript import { snapdom } from '@zumer/snapdom'; snapdom.preCapture(); // The first click teaches the association; later intent can prepare the capture. button.onclick = () => snapdom.toPng(hero, { scale: 2 }); ``` -------------------------------- ### Use the plugin with SnapDOM Source: https://github.com/zumerlab/snapdom/blob/main/packages/plugin-template/README.md Import the plugin and SnapDOM, then call snapdom with the plugin and convert the result to PNG. ```js import { snapdom } from '@zumer/snapdom'; import { myPlugin } from './index.js'; const result = await snapdom(element, { plugins: [myPlugin({ example: 'value' })] }); const image = await result.toPng(); ``` -------------------------------- ### Initialize agent-map plugin Source: https://github.com/zumerlab/snapdom/blob/main/packages/plugins/README.md Shows how to import and register the agent-map plugin with snapdom to retrieve an annotated image and element map. ```js import { agentMap } from '@zumer/snapdom-plugins/agent-map'; const result = await snapdom(el, { plugins: [agentMap()] }); const { image, map, dimensions } = await result.toAgentMap(); // image: data URL of the screenshot with numbered red badges overlaid // map: [{ i, n, r, b, s? }, …]: index, name, role, bbox, state // Agent says "click element 2" → map[2].b gives [x, y, w, h] ``` -------------------------------- ### Implement tileCapture and usage example Source: https://github.com/zumerlab/snapdom/blob/main/docs/blog/huge-page-mosaic/index.html The tileCapture function iterates through a capture area in tiles, while the usage example demonstrates how to consume these tiles by converting them to blobs and uploading them. ```javascript async function tileCapture(capture, options, consume) { const { tileWidth = 2048, tileHeight = 2048, scale = 1, dpr = 1 } = options const { contentX, contentY, w0, h0 } = capture.meta for (let y = 0, row = 0; y < h0; y += tileHeight, row++) { for (let x = 0, col = 0; x < w0; x += tileWidth, col++) { const width = Math.min(tileWidth, w0 - x) const height = Math.min(tileHeight, h0 - y) const crop = { x: contentX + x, y: contentY + y, width, height } const canvas = await capture.toCanvas({ crop, scale, dpr }) await consume({ canvas, crop, row, col }) } } } const capture = await snapdom(document.documentElement, { dpr: 1 }) await tileCapture(capture, {}, async ({ canvas, row, col }) => { const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png')) if (!blob) throw new Error('PNG encoding failed') try { await uploadTile(blob, { row, col }) } finally { canvas.width = canvas.height = 0 } }) ``` -------------------------------- ### Paint Canvas Setup and Rendering Logic Source: https://github.com/zumerlab/snapdom/blob/main/demos/d-paint-canvas.html Sets up the canvas, plugin, and rendering pipeline. Handles stroke recording, scheduling renders, and converting client coordinates to canvas space. ```javascript clear () { skipNext = true } } } const board = document.getElementById('board') const liveSvg = document.getElementById('liveStrokes') const brushCursor = document.getElementById('brushCursor') const swatchesEl = document.getElementById('swatches') const customColor = document.getElementById('customColor') const brushSize = document.getElementById('brushSize') const clearBtn = document.getElementById('clearBtn') const downloadLink = document.getElementById('downloadLink') const canvasSource = document.createElement('div') canvasSource.style.cssText = 'position:absolute; left:-9999px; top:0; width:420px; height:420px; background:#fff;' canvasSource.innerHTML = '<svg class="strokes" viewBox="0 0 420 420" preserveAspectRatio="none" style="position:absolute; inset:0; width:100%; height:100%;"></svg>' document.body.appendChild(canvasSource) const plugin = createPaintPlugin(canvasSource) let color = COLORS[0] let size = Number(brushSize.value) let drawing = false let points = [] let livePath = null let strokeId = 0 let activeStrokeId = 0 let lastCaptureTime = 0 let pendingStroke = null let rendering = false let pending = false COLORS.forEach(c => { const btn = document.createElement('button') btn.className = 'swatch' + (c === color ? ' active' : '') btn.style.background = c btn.addEventListener('click', () => { color = c swatchesEl.querySelectorAll('.swatch').forEach(s => s.classList.remove('active')) btn.classList.add('active') }) swatchesEl.appendChild(btn) }) customColor.addEventListener('input', () => { color = customColor.value swatchesEl.querySelectorAll('.swatch').forEach(s => s.classList.remove('active')) }) brushSize.addEventListener('input', () => { size = Number(brushSize.value) }) async function render (stroke) { if (stroke) plugin.recordStroke(stroke) const img = await snapdom.toPng(canvasSource, { plugins: [plugin] }) img.style.width = '100%' img.style.height = '100%' board.querySelector('img')?.remove() board.insertBefore(img, liveSvg) downloadLink.href = img.src } async function scheduleRender (stroke) { if (stroke) pendingStroke = stroke if (rendering) { pending = true; return } rendering = true do { pending = false const s = pendingStroke pendingStroke = null await render(s) } while (pending) rendering = false } function pointFromClient (clientX, clientY) { const rect = board.getBoundingClientRect() return { x: ((clientX - rect.left) / rect.width) * 420, y: ((clientY - rect.top) / rect.height) * 420 } } ``` -------------------------------- ### Install official snapdom plugins via npm Source: https://github.com/zumerlab/snapdom/blob/main/PLUGIN_SPEC.md Command to install the core snapdom package and the official plugins package. Requires matching core major versions. ```bash npm i @zumer/snapdom@latest @zumer/snapdom-plugins@latest ``` -------------------------------- ### Migration snippet: before and after Source: https://github.com/zumerlab/snapdom/blob/main/docs/compare/dom-to-image/index.html Shows how to migrate from dom-to-image to SnapDOM, including the change from bgcolor to backgroundColor. The before code uses domtoimage.toPng, the after uses snapdom.toPng. ```javascript // Before import domtoimage from 'dom-to-image'; const dataUrl = await domtoimage.toPng(el, { bgcolor: '#fff' }); ``` ```javascript // After import { snapdom } from '@zumer/snapdom'; const img = await snapdom.toPng(el, { backgroundColor: '#fff' }); ``` -------------------------------- ### Plugin factory example (index.js) Source: https://github.com/zumerlab/snapdom/blob/main/CONTRIBUTING_PLUGINS.md Example plugin factory that returns a plugin object with an afterClone hook. The hook receives a context object with a clone property that can be modified. ```js // index.js export function yourPlugin(options = {}) { return { name: 'your-plugin', afterClone(ctx) { // modify ctx.clone } }; } ``` -------------------------------- ### Install @zumer/snapdom Source: https://github.com/zumerlab/snapdom/blob/main/docs/guides/vue/index.html Install the latest version of the @zumer/snapdom package via npm. It has zero runtime dependencies and an ES module entry. In Nuxt, keep imports and capture calls inside the browser boundary. ```bash npm install @zumer/snapdom@latest ``` -------------------------------- ### WebGL Mirror with SnapDOM and Plugin Source: https://github.com/zumerlab/snapdom/blob/main/demos/d-plugin-webgl-live-dom.html This snippet sets up a WebGL mirror by creating a WebGLMirror instance, attaching the webglMirror plugin to SnapDOM, and updating the mirror on input and theme changes. It also handles the 'data-theme' buttons and the '#effect' selector to adjust the renderer's effect. ```javascript renderer.update(ctx.export.url) document.querySelector('#stats').textContent = `${elapsed.toFixed(0)} ms · GPU updated` return canvas } } } } } const editable = document.querySelector('#editable') const renderer = new WebGLMirror(document.querySelector('#gl')) const mirrorPlugin = webglMirror(renderer) let dirty = false let working = false async function updateMirror() { dirty = true if (working) return working = true while (dirty) { dirty = false const capture = await snapdom(editable, { plugins: [mirrorPlugin], cache: 'full', fast: true }) await capture.toWebgl() } working = false } editable.addEventListener('input', updateMirror) document.querySelectorAll('[data-theme]').forEach(button => button.addEventListener('click', () => { document.querySelector('.tool.active').classList.remove('active') button.classList.add('active') editable.style.setProperty('--local-accent', button.dataset.theme) updateMirror() })) document.querySelector('#effect').addEventListener('change', event => { renderer.effect = Number(event.target.value) }) await updateMirror() editable.focus() ```