Search code examples
html5-canvas

How to transform the canvas using DOMMatrix?


I'm reading through the MDN documentation for canvas and above the transformations section, it says "The methods listed below remain for historical and compatibility reasons as DOMMatrix objects are used in most parts of the API nowadays and will be used in the future instead." This seems to suggest that transform methods (such as .rotate() and .scale()) used directly aginst the CanvasRenderingContext2D are obsolete. However, I don't see any clear explanation as to what the new mechanism is for doing things like rotating and scaling the entire canvas using the DOMMatrix mechanism. How can this be done and is there any decent documentation for it? Even MDN's own canvas tutorial still calls transform methods against the canvas rendering context!


Solution

  • These methods aren't obsolete, you are still safe to use them and this paragraph is I believe misleading. I'll think on it but I may end up removing it from MDN since we've got no intention of removing these methods.
    And while this will be implementation dependent, I know that at least in Chromium both don't end up in the same path internally, and I wouldn't be surprised that using a DOMMatrix object would be somehow slower than using the relative transforms. There are also cases where using a DOMMatrix object just makes your code more complex to read and maintain.
    So you'd better not drop the tranform methods just because someone wrote that line in this article.

    Anyway, DOMMatrix objects are convenient and there are definitely cases where you'll want them. To do so, apply the transforms on that object, and then apply it through context.setTransform(matrix). This will set the context's current transform matrix (CTM) to the one represented by the DOMMatrix object, and disregard whatever was set as CTM before.

    So for instance to translate your context:

    const canvas = document.querySelector("canvas");
    const ctx = canvas.getContext("2d");
    ctx.fillStyle = "red";
    ctx.fillRect(0, 0, 50, 50); // untransformed
    const mat = new DOMMatrix();
    // mat.translate() would return a new DOMMatrix without modifying this one
    mat.translateSelf(120, 50);
    // set the context CTM to our DOMMatrix
    ctx.setTransform(mat);
    ctx.fillStyle = "green";
    ctx.fillRect(0, 0, 50, 50); // transformed
    <canvas></canvas>

    However beware there is a huge bug in the DOMMatrix API: the rotation angle has been wrongfully defined as degrees. This is basically the only place in almost all the Web-APIs that a JS angle is defined as degrees instead of being defined as radians. So we have to do stoopid conversions there and scratch our head every time we see our rotation didn't work...

    const canvas = document.querySelector("canvas");
    const ctx = canvas.getContext("2d");
    const angle = Math.PI * 1.8; // radians
    ctx.translate(150, 75);
    ctx.rotate(angle);
    ctx.translate(-50, -50);
    ctx.fillStyle = "blue";
    ctx.fillRect(0, 0, 50, 50); // default ctx.rotate();
    
    const mat = new DOMMatrix();
    mat.translateSelf(150, 75);
    mat.rotateSelf(angle); // this should have been in degrees!
    mat.translateSelf(-50, -50);
    ctx.setTransform(mat);
    ctx.fillStyle = "green";
    ctx.fillRect(0, 0, 50, 50); // that's not what we expected
    <canvas></canvas>

    Also, to make only relative updates to the current transform matrix (CTM), you'd have to either keep your DOMMatrix object around in your code, or to retrieve it from the context's .getTransform() method.
    Once you got the context's CTM, you can either apply relative transforms using the DOMMatrix.\[...\]Self methods, or even multiply this DOMMatrix object with another one.

    const canvas = document.querySelector("canvas");
    const ctx = canvas.getContext("2d");
    ctx.fillStyle = "green";
    ctx.translate(150, 75); // not gonna disappear
    const identity = new DOMMatrix();
    
    const anim = () => {
      const mat = ctx.getTransform();
      ctx.setTransform(identity); // to clear the context, reset to identity
                                  // after you got the previous CTM
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      mat.rotateSelf(1); // one degree
      ctx.setTransform(mat);
      ctx.fillRect(-25, -25, 50, 50);
      requestAnimationFrame(anim);
    };
    
    requestAnimationFrame(anim);
    <canvas></canvas>

    Finally, note that while the DOMMatrix interface does support 3D transforms, the canvas 2D API still doesn't support non-affine transformations. You still won't have perspective even when passing a 3D transform.