HTML5 Canvas获得变换矩阵?

时间:2011-09-13 01:18:01

标签: javascript html5 canvas

有没有办法获得画布的当前转换矩阵?有一个context.setTransform()函数,但据我所知,似乎没有getTransform()等价。

更具体地说,我想获得矩阵的当前比例和平移元素。谷歌一直对此毫无帮助。

4 个答案:

答案 0 :(得分:30)

不,根本就没有。 :(

大多数canavs库(例如cake.js)已经实现了自己的矩阵类来跟踪当前的转换矩阵。

cake.js的创建者认为没有办法获得当前的矩阵是荒谬的,足以保证bug report about it.不幸的是,这是在2007年,并且没有努力在规范中包含getCurrentTransform

编辑:我创建了一个简单的Transformation类,可以让您轻松制作自己的getCanvas()或者并排跟踪Canvas的矩阵。 Here it is.我希望有所帮助!

编辑2012年6月:新规范确实包含获取当前转换矩阵的方法! context.currentTransform可用于获取或设置当前转换矩阵。遗憾的是,虽然Firefox确实在其上下文中具有特定于供应商的mozCurrentTransform属性,但尚未实现它。所以你还不能使用它,但很快就会出现在规范中!

答案 1 :(得分:12)

编辑(2016年6月27日):WHATWG规范现在有一个函数getTransform()而不是currentTransform,从语义上看,getTransform()创建副本。看起来主浏览器仍然缺少它。

再次编辑:

这是一个粗略的实现:

//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;  
};

编辑: 属性currentTransform has been added to the spec;据报道,Firefox和Opera支持它。我检查了Firefox,发现它以供应商为前缀为mozCurrentTransform。据推测,它可用于获取和设置变换矩阵。

OLDER STUFF,仍然是最好的:

如果您想获得当前的转换矩阵,您必须自己跟踪它。一种方法是使用Javascript的原型继承来添加getMatrix()方法并扩充修改矩阵的方法:

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();

要真正做到正确,您需要在使用上下文的save()restore()方法时跟踪多个矩阵。

答案 2 :(得分:2)

正如@ellisbben提到的那样,你能做到这一点的唯一方法就是自己跟踪它。您可以找到一个解决方案here。它将上下文包装在一个包装器中,然后在那里处理令人讨厌的位。

答案 3 :(得分:0)

this answer的启发,我更新了@ellisbben的答案,使其使用代理而不是原型继承(这对我不起作用)。覆盖CanvasRenderingContext2D.prototype的代码linked in the comments of @ellisbben's answer也不起作用。 (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();