Skip to content

WebGL canvas becomes transparent in PNG and SVG exports starting with v2.22.0 #480

Description

@lucasortet

Describe it simply:

A WebGL canvas rendered with preserveDrawingBuffer: false becomes transparent when captured with SnapDOM v2.22.0.

The same canvas is captured correctly with v2.18.0. The issue affects both PNG and SVG exports, while surrounding HTML elements remain visible.

Quick demo to reproduce

Save this as an HTML file and open it in a browser:

<!doctype html>
<html lang="en">
  <body>
    <div id="target">
      <canvas id="canvas" width="640" height="320"></canvas>
      <p>This text remains visible in the exported image.</p>
    </div>

    <button id="capture-png">Capture PNG</button>
    <button id="capture-svg">Capture SVG</button>
    <div id="result"></div>

    <script type="module">
      import { snapdom } from
        'https://unpkg.com/@zumer/snapdom@2.22.0/dist/snapdom.mjs';

      const canvas = document.querySelector('#canvas');
      const gl = canvas.getContext('webgl2', {
        alpha: true,
        antialias: true,
        preserveDrawingBuffer: false,
      });

      function render(time) {
        const width = gl.drawingBufferWidth;
        const height = gl.drawingBufferHeight;
        const rectangleWidth = Math.round(width * 0.3);
        const x = Math.round(
          ((Math.sin(time / 700) + 1) / 2) * (width - rectangleWidth)
        );

        gl.enable(gl.SCISSOR_TEST);

        gl.scissor(0, 0, width, height);
        gl.clearColor(0.03, 0.07, 0.14, 1);
        gl.clear(gl.COLOR_BUFFER_BIT);

        gl.scissor(x, 70, rectangleWidth, 180);
        gl.clearColor(0.05, 0.8, 0.92, 1);
        gl.clear(gl.COLOR_BUFFER_BIT);

        requestAnimationFrame(render);
      }

      requestAnimationFrame(render);

      const target = document.querySelector('#target');
      const result = document.querySelector('#result');

      document.querySelector('#capture-png').addEventListener('click', async () => {
        result.replaceChildren(
          await snapdom.toPng(target, { backgroundColor: '#ffffff' })
        );
      });

      document.querySelector('#capture-svg').addEventListener('click', async () => {
        result.replaceChildren(
          await snapdom.toSvg(target, { backgroundColor: '#ffffff' })
        );
      });
    </script>
  </body>
</html>

With v2.22.0, the exported canvas is transparent, although it is visible on the live page.

Changing the imported version to v2.18.0 restores the WebGL content in both PNG and SVG captures.

Anything else we should know?

The regression appears related to commit a5536ae, which made the canvas pre-requestAnimationFrame wait Safari-only.

For a WebGL context using preserveDrawingBuffer: false, toDataURL() can return a valid but fully transparent PNG after the frame has been presented. Because the data URL is neither empty nor equal to data:,, the existing retry is not triggered.

Waiting for an animation frame in a beforeClone plugin restores both exports:

const waitForAnimationFramePlugin = {
  name: 'wait-for-animation-frame-before-clone',
  beforeClone: () =>
    new Promise((resolve) => {
      requestAnimationFrame(() => resolve());
    }),
};

const image = await snapdom.toPng(target, {
  plugins: [waitForAnimationFramePlugin],
});

Bug reproduced with Google Chrome.

Same behavior with v2.23.2.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions