HTML5 Canvas get transform matrix?

前端 未结 4 1892
一向
一向 2020-12-14 06:57

Is there a way to get the current transformation matrix for a canvas? There is a context.setTransform() function, but there does not seem to be a getTransform() equivalent a

相关标签:
4条回答
  • 2020-12-14 07:36

    As @ellisbben mentioned pretty much the only way you can do this is to keep track of it yourself. You can find one solution here. It wraps the context within a wrapper and then handles the nasty bits there.

    0 讨论(0)
  • 2020-12-14 07:44

    Motivated by this answer, I updated @ellisbben's answer to use a Proxy instead of prototype inheritance (which didn't work for me). The code linked in the comments of @ellisbben's answer overriding CanvasRenderingContext2D.prototype also didn't work. (See related question.)

    // in theory, SVGMatrix will be used by the Canvas API in the future;
    // in practice, we can borrow an SVG matrix today!
    function createMatrix() {
      const svgNamespace = 'http://www.w3.org/2000/svg';
      return document.createElementNS(svgNamespace, 'g').getCTM();
    }
    
    // `enhanceContext` takes a 2d canvas context and wraps its matrix-changing
    // functions so that `context.currentTransform` should always correspond to its
    // current transformation matrix.
    // Call `enhanceContext` on a freshly-fetched 2d canvas context for best
    // results.
    function enhanceContext(context) {
      // The main property we are enhancing the context to track
      let currentTransform = createMatrix();
    
      // the stack of saved matrices
      const savedTransforms = [currentTransform];
    
      const enhanced = {
        currentTransform,
        savedTransforms,
        // helper for manually forcing the canvas transformation matrix to
        // match the stored matrix.
        _setMatrix() {
          const m = enhanced.currentTransform;
          context.setTransform(m.a, m.b, m.c, m.d, m.e, m.f);
        },
    
        save() {
          enhanced.savedTransforms.push(enhanced.currentTransform);
          context.save();
        },
    
        // if the stack of matrices we're managing doesn't have a saved matrix,
        // we won't even call the context's original `restore` method.
        restore() {
          if (enhanced.savedTransforms.length == 0) return;
          context.restore();
          enhanced.currentTransform = enhanced.savedTransforms.pop();
          enhanced._setMatrix();
        },
    
        scale(x, y) {
          enhanced.currentTransform = enhanced.currentTransform.scaleNonUniform(
            x,
            y
          );
          context.scale(x, y);
        },
    
        rotate(theta) {
          // canvas `rotate` uses radians, SVGMatrix uses degrees.
          enhanced.currentTransform = enhanced.currentTransform.rotate(
            (theta * 180) / Math.PI
          );
          context.rotate(theta);
        },
    
        translate(x, y) {
          enhanced.currentTransform = enhanced.currentTransform.translate(x, y);
          context.translate(x, y);
        },
    
        transform(a, b, c, d, e, f) {
          const rhs = createMatrix();
          // 2x2 scale-skew matrix
          rhs.a = a;
          rhs.b = b;
          rhs.c = c;
          rhs.d = d;
    
          // translation vector
          rhs.e = e;
          rhs.f = f;
          enhanced.currentTransform = enhanced.currentTransform.multiply(rhs);
          context.transform(a, b, c, d, e, f);
        },
    
        // Warning: `resetTransform` is not implemented in at least some browsers
        // and this is _not_ a shim.
        resetTransform() {
          enhanced.currentTransform = createMatrix();
          context.resetTransform();
        },
      };
    
      const handler = {
        get: (target, key) => {
          const value =
            key in enhanced
              ? enhanced[key]
              : key in target
              ? target[key]
              : undefined;
          if (value === undefined) {
            return value;
          }
          return typeof value === 'function'
            ? (...args) => value.apply(target, args)
            : value;
        },
        set: (target, key, value) => {
          if (key in target) {
            target[key] = value;
          }
          return value;
        },
      };
    
      return new Proxy(context, handler);
    }
    
    function testIt() {
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');
      const enhanced = enhanceContext(ctx);
      const log = (msg) => {
        const { a, b, c, d, e, f } = enhanced.currentTransform;
        console.log(msg, { a, b, c, d, e, f });
      };
      window.enhanced = enhanced;
      log('initial');
    
      enhanced.save();
      enhanced.scale(1, 2);
      log('scale(1,2)');
      enhanced.restore();
    
      enhanced.save();
      enhanced.translate(10, 20);
      log('translate(10,20)');
      enhanced.restore();
    
      enhanced.save();
      enhanced.rotate(30);
      log('rotate(30)');
      enhanced.restore();
    
      enhanced.save();
      enhanced.scale(1, 2);
      enhanced.translate(10, 20);
      log('scale(1,2) translate(10,20)');
      enhanced.restore();
    }
    
    testIt();

    0 讨论(0)
  • 2020-12-14 07:49

    No, there simply isn't. :(

    Most canavs libraries (such as cake.js) have instead implemented their own matrix class to keep track of the current transformation matrix.

    The creator of cake.js thought that there being no way to get the current matrix was ridiculous enough to warrant a bug report about it. Unfortunately that was back in 2007 and there has been no effort to include a getCurrentTransform in the spec.

    Edit: I have created a simple Transformation class that will allow you to easily make your own getCanvas() or keep track of the Canvas's matrix side-by-side. Here it is. I hope that helps!

    Edit June 2012: The new specification does include a way to get the current transformation matrix! context.currentTransform can be used to get or set the current transformation matrix. Unfortunately no browsers have implemented it yet, though Firefox does have the vendor-specific mozCurrentTransform property on its context. So you can't use it just yet, but it is in the spec, so soon!

    0 讨论(0)
  • 2020-12-14 07:49

    EDIT(1/10/2020): MDN now indicates that getTransform() is supported in most major browsers; the code below may still have value as a part of implementing a polyfill for Internet Explorer, Edge, and Android Firefox.

    EDIT(6/27/2016): The WHATWG spec now has a function getTransform() instead of currentTransform and it appears semantically clear that getTransform() creates a copy of the transformation matrix. Looks like it is still missing from major browsers.

    EDIT, again:

    Here's a rough implementation:

    //in theory, SVGMatrix will be used by the Canvas API in the future;
    //in practice, we can borrow an SVG matrix today!
    var createMatrix = function() {
      var svgNamespace = "http://www.w3.org/2000/svg";
      return document.createElementNS(svgNamespace, "g").getCTM();
    }
    
    //`enhanceContext` takes a 2d canvas context and wraps its matrix-changing
    //functions so that `context._matrix` should always correspond to its
    //current transformation matrix.
    //Call `enhanceContext` on a freshly-fetched 2d canvas context for best
    //results.
    var enhanceContext = function(context) {
      var m = createMatrix();
      context._matrix = m;
    
      //the stack of saved matrices
      context._savedMatrices = [m];
    
      var super_ = context.__proto__;
      context.__proto__ = ({
    
        //helper for manually forcing the canvas transformation matrix to
        //match the stored matrix.
        _setMatrix: function() {
          var m = this._matrix;
          super_.setTransform.call(this, m.a, m.b, m.c, m.d, m.e, m.f);
        },
    
        save: function() {
          this._savedMatrices.push(this._matrix);
          super_.save.call(this);
        },
    
        //if the stack of matrices we're managing doesn't have a saved matrix,
        //we won't even call the context's original `restore` method.
        restore: function() {
          if(this._savedMatrices.length == 0)
            return;
          super_.restore.call(this);
          this._matrix = this._savedMatrices.pop();
          this._setMatrix();
        },
    
        scale: function(x, y) {
          this._matrix = this._matrix.scaleNonUniform(x, y);
          super_.scale.call(this, x, y);
        },
    
        rotate: function(theta) {
          //canvas `rotate` uses radians, SVGMatrix uses degrees.
          this._matrix = this._matrix.rotate(theta * 180 / Math.PI);
          super_.rotate.call(this, theta);
        },
    
        translate: function(x, y) {
          this._matrix = this._matrix.translate(x, y);
          super_.translate.call(this, x, y);
        },
    
        transform: function(a, b, c, d, e, f) {
          var rhs = createMatrix();
          //2x2 scale-skew matrix
          rhs.a = a; rhs.b = b;
          rhs.c = c; rhs.d = d;
    
          //translation vector
          rhs.e = e; rhs.f = f;
          this._matrix = this._matrix.multiply(rhs);
          super_.transform.call(this, a, b, c, d, e, f);
        },
    
        //warning: `resetTransform` is not implemented in at least some browsers
        //and this is _not_ a shim.
        resetTransform: function() {
          this._matrix = createMatrix();
          super_.resetTransform.call(this);
        },
    
        __proto__: super_
      });
    
      return context;  
    };
    

    EDIT: The attribute currentTransform has been added to the spec; it is reported to be supported in Firefox and Opera. I checked on Firefox and found it vendor-prefixed as mozCurrentTransform. Presumably it can be used to both get and set the transform matrix.

    OLDER STUFF, STILL MOSTLY TRUE:

    If you want to get the current transformation matrix, you'll have to keep track of it yourself. One way of doing this would be to use Javascript's prototypical inheritance to add a getMatrix() method and augment the methods which modify the matrix:

    var context = canvas.getContext("2d");
    var super = context.__proto__;
    context.__proto__ = ({
    
      __proto__: super, //"inherit" default behavior
    
      getMatrix: function() { return this.matrix; },
    
      scale: function(x, y) {
    
        //assuming the matrix manipulations are already defined...
        var newMatrix = scaleMatrix(x, y, this.getMatrix());
        this.matrix = newMatrix;
        return super.scale.call(this, x, y);
      },
      /* similar things for rotate, translate, transform, setTransform */
      /* ... */
    });
    context.matrix = makeDefaultMatrix();
    

    To really get it right, you'd need to track multiple matrices when the save() and restore() methods of the context are used.

    0 讨论(0)
提交回复
热议问题