Fabric.js矩阵变换

全篇共 5551 字。按500字/分钟阅读速度,阅读完预计需要 11.1 分钟。

Fabric.js 支持矩阵变换。无论在 fabric.Canvas 实例中,或是在继承自 fabric.Object 的各种形状实例中,还是在 fabric.util 中提供的工具函数,都有设置或者获取矩阵变换的相应属性或方法。在 fabric 中,矩阵由一个包含6个数字元素的数组表示,这6个数字能一次性把坐标系的平移、旋转和缩放信息表示出来。

官方提供的 矩阵变换 Demo。点击 Merge transfrom 按钮,对当前变换应用叠加,示例演示了当分别修改数组中的6个数字元素后,会对画布中的图形产生的影响。

变换矩阵的数组表示法

下面代码中列出了在 Fabricjs 中那些和矩阵变换操作相关的属性和方法,共分为三部分,首先是 Canvas 画布上的矩阵变换,其次是对于那些继承自 fabric.Object 的类的实例上的矩阵变换,最后 Fabricjs 在 fabric.util 命名空间内也提供了几个和矩阵变换相关的工具函数。

Canvas:

  • vieportTransform = matrix;

Objects:

  • matrix = fabric.Object.prototype.calcTransformMatrix();
  • matrix = fabric.Object.prototype.calcOwnMatrix();

Utils:

  • point = fabric.util.transformPoint(point, matrix);
  • matrix = fabric.util.multiplyTransformMatrices(matrix, matrix);
  • matrix = fabric.util.invertTransform(matrix);
  • options = fabric.util.qrDecompose(matrix);
  • matrix = fabrix.util.composeMatrix(options);

下面以这三部分做为学习 Fabricjs 中的矩阵变换的框架,把我对矩阵变换的理解尽可能详细的记录下来。

Canvas 矩阵变换

首先声明一个常量,用来存储 fabric.Canvas 类的一个实例。先在 HTML 中的 body 标签内,JavaScript 脚本之前的位置创建一个 <canvas id="c"></canvas> 标记,它的 id 属性为 c。

const canvas = new fabric.Canvas('c');
canvas.viewportTransform

控制台输出包含6个数字元素的数组 [1, 0, 0, 1, 0, 0],代表当前 canvas 的矩阵变换情况。在控制台输出常量 canvas,在它的属性和原型链上查找和矩阵变换有关的属性和方法。Canvas 类和 StaticCanvas 类的继承关系:fabric.Canvas => fabric.StaticCanvas

Objects 矩阵变换

涉及到两个方法:

  • calcTransformMatrix()
  • calcOwnMatrix()

所有继承自 fabric.Object 的类(fabric.Rect 等)的实例都可以调用这两个方法。下面是这两个方法的源代码,调用 calcTransformMatrix 方法时可以传入一个布尔值参数,表示是否跳过组变换,如果为真值,则直接调用 calcOwnMatrix 方法。

/**
 * calculate transform matrix that represents the current transformations from the
 * object's properties.
 * @param {Boolean} [skipGroup] return transform matrix for object not counting parent transformations
 * @return {Array} transform matrix for the object
 */
calcTransformMatrix: function(skipGroup) {
  if (skipGroup) {
    return this.calcOwnMatrix();
  }
  var key = this.transformMatrixKey(), cache = this.matrixCache || (this.matrixCache = {});
  if (cache.key === key) {
    return cache.value;
  }
  var matrix = this.calcOwnMatrix();
  if (this.group) {
    matrix = multiplyMatrices(this.group.calcTransformMatrix(), matrix);
  }
  cache.key = key;
  cache.value = matrix;
  return matrix;
}

/**
 * calculate transform matrix that represents the current transformations from the
 * object's properties, this matrix does not include the group transformation
 * @return {Array} transform matrix for the object
 */
calcOwnMatrix: function() {
  var key = this.transformMatrixKey(true), cache = this.ownMatrixCache || (this.ownMatrixCache = {});
  if (cache.key === key) {
    return cache.value;
  }
  var tMatrix = this._calcTranslateMatrix();
  this.translateX = tMatrix[4];
  this.translateY = tMatrix[5];
  cache.key = key;
  cache.value = fabric.util.composeMatrix(this);
  return cache.value;
}

矩阵变换工具函数

上面 canvas.viewportTransform 属性和 object.calcTransformMatrix() object.calcOwnMatrix() 两个方法执行后都返回一个包含6个数字元素的数组。借助 fabric.util.qrDecompose() 方法,将数组形式的矩阵变换形式转换成可读性强的对象属性形式:

canvas.viewportTransform;
输出:[1, 0, 0, 1, 0, 0]

fabric.util.qrDecompose(canvas.viewportTransform);
输出:{
  angle: 0
  scaleX: 1
  scaleY: 1
  skewX: 0
  skewY: 0
  translateX: 0
  translateY: 0
}

options = fabric.util.qrDecompose() 相对的方法是 matrix = fabric.util.composeMatrix(options) 方法,传入包含7个描述对象变换属性的对象,返回包含6个数字的矩阵。

matrix = fabric.util.invertTransform(matrix) 就是将变换反转,逆向变换。

fabric.util.invertTransform([1, 0, 0, 1, 151.5, 151.5])
输出:[1, -0, -0, 1, -151.5, -151.5]

matrix = fabric.util.multiplyTransformMatrices(matrix, matrix) 就是将两个变换叠加,得到新的变换矩阵。

fabric.util.multiplyTransformMatrices([1, 0, 0, 1, 151.5, 151.5], [1, -0, -0, 1, -151.5, -151.5]);
输出:[1, 0, 0, 1, 0, 0]

point = fabric.util.transformPoint(point, matrix) ,顾名思义就是把一个点的坐标,按照指定的变换规则变换,最终得到变换后的新点。

官方示例

官方文档中有介绍 如何在 Fabricjs 使用变换,其中给出了一个绑定变换的例子,代码在下面,我根据自己的理解为下面的代码添加了注释。

// 官方代码中没有为画布指定宽度和高度,我指定画布的宽高均为 500。
var canvas = new fabric.Canvas('c', { width: 500, height: 500 });

// 创建一个红色的矩形,作为“主人”
var boss = new fabric.Rect({ width: 150, height: 200, fill: 'red' });

// 创建两个蓝色的矩形,作为“随从”
var minion1 = new fabric.Rect({ width: 40, height: 40, fill: 'blue' });
var minion2 = new fabric.Rect({ width: 40, height: 40, fill: 'blue' });

// 将上面创建的三个矩形添加到舞台画布上
canvas.add(boss, minion1, minion2);

// 为“主人”添加当触发“被拖动”事件时的处理函数
boss.on('moving', updateMinions);

// 为“主人”添加当触发“被旋转”事件时的处理函数
boss.on('rotating', updateMinions);

// 为“主人”添加当触发“被缩放”事件时的处理函数
boss.on('scaling', updateMinions);

// 为了代码清晰、简便,所以用简短的变量引用这俩函数
var multiply = fabric.util.multiplyTransformMatrices; // 叠加变换
var invert = fabric.util.invertTransform; // 逆转变换

// “主人”被“拖动、旋转、缩放”时,执行该函数
function updateMinions() {
  // 拿到全部“随从”,返回一个包含全部“随从”的数组
  var minions = canvas.getObjects().filter(o => o !== boss);
  
  // 遍历每一个“随从”
  minions.forEach(o => {
    // 如果“随从”没有该属性,说明还没有点击过 id 为 bind 的按钮
    // 直接返回
    if (!o.relationship) {
      return;
    }
    
    // 依然为了使用简便,将对象引用赋值给新的变量
    var relationship = o.relationship;
    
    // 将两个矩阵变换叠加,得到新的变换规则
    var newTransform = multiply(
    
      // 返回当前 “主人” 经过 “拖拽或旋转或缩放”操作后的变换矩阵
      boss.calcTransformMatrix(),
      
      // 和 “主随关系” 矩阵相叠加
      relationship
    );
    
    // 将包含6个数字元素的数组转换为属性的集合即对象
    opt = fabric.util.qrDecompose(newTransform);
    
    // 设置“随从” X/Y 轴平方向都不翻转
    o.set({
      flipX: false,
      flipY: false,
    });
    
    // 设置“随从”原点的位置,这里将矩形的中心作为原点
    o.setPositionByOrigin(
      { x: opt.translateX, y: opt.translateY },
      'center',
      'center'
    );
    
    // 将上面从矩阵数组转换而得到的属性集合对象作为“随从”的新配置
    o.set(opt);
    
    // set 方法并不能让和坐标相关的矩阵变换生效,所以还需要再执行下面的方法
    o.setCoords();
  });
}

// 除了 canvas 外还有一个 id 为 bind 的按钮,下方为其添加点击事件
// 每次点击该按钮,重新计算两个“随从”和“主人”的变换关系,并将关系记录在每个“随从”中
document.getElementById('bind').onclick = function () {

  // 拿到全部“随从”,返回一个包含全部“随从”的数组
  var minions = canvas.getObjects().filter(o => o !== boss);
  
  // 计算“主人”当前的变换矩阵,并得到“主人”的逆转变换
  var bossTransform = boss.calcTransformMatrix();
  var invertedBossTransform = invert(bossTransform);
  
  // 遍历每一个“随从”
  minions.forEach(o => {
    
    // 关键:拿到能描述“主随”关系的变换矩阵
    // 该方法接收三个参数,前两个参数不分先后
    var desiredTransform = multiply(
      invertedBossTransform,
      
      // 返回“随从”的变换矩阵
      o.calcTransformMatrix()
    );
    
    // 将“主随关系”的变换矩阵保存在“随从”上
    o.relationship = desiredTransform;
  });
}
原创作者 » 陈帅华
版权声明 » 自由转载-保持署名-非商用-非衍生
发布日期 » 2019年11月3日 周日
更新日期 » 2020年1月27日 周一
上一篇 » Airglass与在线图像标注工具
下一篇 » 微信公众号开发前的准备
:)记录此刻想法
请选择登录方式,开始记录你的想法。
授权微博登录
授权Github登录