Deck.gl: Way of saving canvas to image in deck.gl

Created on 30 Mar 2020  路  9Comments  路  Source: visgl/deck.gl

Hi, I want to save the webgl canvas to image by using toDataURL or gl.readPixels and have some questions:

  • In order to read from canvas, seems I need to use deck.redraw() to trigger the render and read from framebuffer. Since I am using <DeckGL> in React and, according to the code the this.deck is not exposed from <DeckGL>, how could I achieve this without passing customized deck instance? Use ref.current.deck.redraw(true)
  • What is your recommendation of the way saving canvas to image, in deck.gl?
    Thanks!
question

Most helpful comment

Great! It doesn't look like you need that html2canvas tool though. Deck.gl maintains a reference to its canvas. I believe the rest of your code will work the same way, i.e.

let canvas = $scope.scatterLayer.canvas;
document.body.appendChild(canvas);
let a = document.createElement('a');
// toDataURL defaults to png, so we need to request a jpeg, then convert for file download.
a.href = canvas.toDataURL("image/png").replace("image/png", "image/octet-stream");
a.download = 'screenshot.png';
a.click();

All 9 comments

You probably want to use a ref, e.g.:

        <DeckGL
          ref={ref => {
            this.deck = ref;
          }}

Then you can access deck.gl in the class through this.deck

@kylebarron Thank you, that makes sense. ref works here.

Hi, I'm glad I saw this issue. I'm facing the same issue here, wanna save deck.gl scatterLayer result to an image. But I am using plain JS, can you elaborate more on how to do this? Thank you!

My code looks like this:

<div id="deck-gl-canvas"></div>
$scope.scatterLayer = new deck.DeckGL({
              container: 'deck-gl-canvas',
              initialViewState: {
                  latitude: $scope.map.getCenter().lat,
                  longitude: $scope.map.getCenter().lng,
                  zoom: $scope.map.getZoom() - 1,
                  bearing: 0,
                  pitch: 0
              },
              controller: true,
              layers: []
       });

const deckGLScatterplot = new deck.ScatterplotLayer({
              id: 'deck-gl-scatter',
              data: $scope.points,
              radiusMinPixels: Math.round($scope.pointRadius),
              getPosition: d => [d[1], d[0]],     // returns longitude, latitude, [altitude]
              getRadius: d => $scope.pointRadius,  // returns radius in meters
              getFillColor: d => [0, 0, 255]           // returns R, G, B, [A] in 0-255 range
    });

$scope.scatterLayer.setProps(
          {
                layers: [deckGLScatterplot],
                viewState:
                  {
                    latitude: $scope.map.getCenter().lat,
                    longitude: $scope.map.getCenter().lng,
                    zoom: $scope.map.getZoom() - 1,
                    bearing: 0,
                    pitch: 0
                  }
          });

The general idea:

let screenshotCb = null;
const myDeckInstance = new deck.Deck({
  ...
  glOptions: {
    preserveDrawingBuffer: true  // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext
  },
  onAfterRender: () => {
    if (screenshotCb) {
      screenshotCb(myDeckInstance.canvas.toDataURL());
      screenshotCb = null;
    }
  }
});

If you use the onAfterRender callback, you shouldn't need preserveDrawingBuffer (which has some performance implications), since it will be in the same requestAnimationFrame loop.

Hi @Pessimistress and @tsherif , thank you for your suggestions! Actually I figured out myself with using the html2canvas tool, and it's quite simple, just call the DeckGL.redraw(true) before capturing the canvas content. The code snippet is pasted here in case anyone in the future may also need this:

Please NOTE: my method only works for static plots of deck.gl, not for animations

function screenshot() {
        // force redraw of deckgl
        $scope.scatterLayer.redraw(true);
        // get the container div of deck.gl
        let div = document.getElementById("deck-gl-canvas");
        // use html2canvas tool to capture the content of the canvas inside the div and download it as an png file
        html2canvas(div).then(canvas => {
              document.body.appendChild(canvas);
              let a = document.createElement('a');
              // toDataURL defaults to png, so we need to request a jpeg, then convert for file download.
              a.href = canvas.toDataURL("image/png").replace("image/png", "image/octet-stream");
              a.download = 'screenshot.png';
              a.click();
        });
}

Best,
Qiushi

Great! It doesn't look like you need that html2canvas tool though. Deck.gl maintains a reference to its canvas. I believe the rest of your code will work the same way, i.e.

let canvas = $scope.scatterLayer.canvas;
document.body.appendChild(canvas);
let a = document.createElement('a');
// toDataURL defaults to png, so we need to request a jpeg, then convert for file download.
a.href = canvas.toDataURL("image/png").replace("image/png", "image/octet-stream");
a.download = 'screenshot.png';
a.click();

Anyone else getting a blank image when using the above ^ ? Thoughts on how to fix?

NM, forgot to redraw() before saving.

Was this page helpful?
0 / 5 - 0 ratings