Search code examples
javascripthtml5-canvasfabricjs

Save dataImage of canvas before rendering


I am developing a project where I have two canvases. The first canvas supports scaling and drawing, the second canvas is a duplicate of the first canvas, which should display a general view of the first canvas without scaling. I already asked about this here, and this what I have now. In this solution, I copy the state of the canvas before scaling using function toDataURL. But this is a very slow way and I get poor performance. I tried using getImageData instead and got not what I expected. Also i tried to play with functions after:render and before:render, but without result, please, look this. What am I doing wrong?


Solution

  • 1) Fabric.js scales its canvas up if devicePixelRatio is > 1. That's why you're getting a 'zoomed' picture when you draw pixels obtained from getImageData(). A better way would be to use drawImage() and supply it the destination width/height so that your browser decides if it should scale those pixels down or up according to the source image dimensions.

    2) You're right about resetting viewport transform but it's not enough - you should apply this new transform and re-render the canvas somehow. That's what Fabric.js does when you call toDataURL() - it invokes toCanvasElement(), which makes a copy of canvas and renders all objects on it. Your implementation should mirror this logic.

    The solution below introduces drawOnCopyCanvas() method, which is a patched version of toCanvasElement(). It doesn't use an intermediate canvas and draws directly on a supplied canvas instead.

    fabric.StaticCanvas.prototype.drawCopyOnCanvas = function(canvasEl) {
      // save values
      var scaledWidth = this.width,
          scaledHeight = this.height,
          vp = this.viewportTransform,
          originalInteractive = this.interactive,
          newVp = [1, 0, 0, 1, 0, 0],
          originalRetina = this.enableRetinaScaling,
          originalContextTop = this.contextTop;
      // reset
      this.contextTop = null;
      this.enableRetinaScaling = false;
      this.interactive = false;
      this.viewportTransform = newVp;
      this.calcViewportBoundaries();
      // draw on copy
      this.renderCanvas(canvasEl.getContext('2d'), this._objects);
      // restore values
      this.viewportTransform = vp;
      this.calcViewportBoundaries();
      this.interactive = originalInteractive;
      this.enableRetinaScaling = originalRetina;
      this.contextTop = originalContextTop;
    }
    
    function afterRender() {
      // remove 'after:render' listener as canvas.toCanvasElement()
      // calls renderCanvas(), which results in an infinite recursion
      canvas.off('after:render', afterRender);
      // draw c1 contents on c2
      canvas.drawCopyOnCanvas(c2);
      setTimeout(() => {
        // re-attach the listener in the next event loop
        canvas.on('after:render', afterRender);
      });
    }
    
    canvas.on('after:render', afterRender);