2021SC@SDUSC山东大学软件学院软件工程应用与实践--quark renderer代码分析 第十三篇 绘画系统分析(4):变换(transform)

2021SC@SDUSC

大概是我负责的最后一篇了,最后的最后,我们来看一下绘画系统中的图形变换功能(transform)。原本前面应该是还有个图形部分,但这一部分的内容与线的绘制等方式大同小异,只是单纯的运算,所以直接跳过了。

这个类是为 Element 类提供变换功能,例如:平移、缩放、扭曲、旋转、翻转、形状、样式。 通过用新的事件机制和继承机制,把 Element 类里面与变形有关的逻辑移到本类中来,保持 Element 干净整洁。

在我们研究转换方法之前,让我们先看看另外两种方法,一旦要开始生成更复杂的绘图,它们就必不可少。

save()

保存画布的整个状态。

restore()

恢复最近保存的画布状态。

画布状态存储在堆栈中。每次save()调用该方法时,都会将当前绘图状态压入堆栈。绘图状态包括

已应用的转换(即translaterotatescale等)。

以下属性的当前值:strokeStylefillStyleglobalAlphalineWidthlineCaplineJoinmiterLimitlineDashOffsetshadowOffsetXshadowOffsetYshadowBlurshadowColorglobalCompositeOperationfonttextAligntextBaselinedirectionimageSmoothingEnabled.

可以save()根据需要多次调用该方法。每次restore()调用该方法时,都会从堆栈中弹出最后保存的状态并恢复所有保存的设置。

首先是构造一个可转换的函数:

var Transformable = function Transformable() {
  var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};

这个函数可以为后面的操作提供一个基础平台。

this.origin = options.origin === null || options.origin === undefined ? [0, 0] : options.origin;

这个是几何变换的原点,默认为 canvas 最左上角的(0,0)点

this.rotation = options.rotation === null || options.rotation === undefined ? 0 : options.rotation;

这里是几何变换的旋转弧度。

this.position = options.position === null || options.position === undefined ? [0, 0] : options.position;

这个是实现平移的数组。为了方便理解,用 position 这个名字来替代 W3C canvas 标准里面的 translate 。

this.scale = options.scale === null || options.scale === undefined ? [1, 1] : options.scale;
this.skew = options.skew === null || options.skew === undefined ? [0, 0] : options.skew;

这两个是缩放和斜切的数组。

this.transform = matrixUtil.create();

  this.inverseTransform = null;


  this.globalScaleRatio = 1;
};

这里是变换矩阵。为了能和动画机制很好地配合,请不要直接修改 transform 属性, SVGPainter 除外。其中

this.inverseTransform = null;

是逆变换矩阵而

this.globalScaleRatio = 1;

是全局缩放函数,默认值为1。

needLocalTransform: function needLocalTransform() {
    return dataUtil.isNotAroundZero(this.rotation) || dataUtil.isNotAroundZero(this.position[0]) || dataUtil.isNotAroundZero(this.position[1]) || dataUtil.isNotAroundZero(this.scale[0] - 1) || dataUtil.isNotAroundZero(this.scale[1] - 1) || dataUtil.isNotAroundZero(this.skew[0] - 1) || dataUtil.isNotAroundZero(this.skew[1] - 1);
  },

这里是对变化的一个判断如果变化的值小于5e-5(0.00005),则不需要变换。

applyTransform: function applyTransform(ctx) {
    var m = this.transform;
    var dpr = ctx.dpr || 1;

    if (m) {
      ctx.setTransform(dpr * m[0], dpr * m[1], dpr * m[2], dpr * m[3], dpr * m[4], dpr * m[5]);
    } else {
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    }
  },

这段可以将将 this.transform 应用到 canvas context 上。

restoreTransform: function restoreTransform(ctx) {
    var dpr = ctx.dpr || 1;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  },

最后在变换结束后要重置变换矩阵

getLocalTransform: function getLocalTransform() {
    var origin = this.origin || [0, 0];
    var rotation = this.rotation || 0;
    var position = this.position || [0, 0];
    var scale = this.scale || [1, 1];
    var skew = this.skew || [0, 0];
    var m = matrixUtil.create(); 

    m[4] -= origin[0];
    m[5] -= origin[1];
    m = matrixUtil.skew(m, skew);
    m = matrixUtil.scale(m, scale);
    m = matrixUtil.rotate(m, rotation); 

    m[4] += origin[0];
    m[5] += origin[1]; 

    m[4] += position[0];
    m[5] += position[1];
    return m;
  },

这里的功能是获取本地变换矩阵。

注意:这里的实现没有考虑仿射变换中的矩阵乘法顺序,因为 API 调用者  在提供配置项时并不会留意数学意义上的变换顺序,而总是采用的直觉意义  上的变换顺序,也就是:skew->scale->rotation->position 。 也就是


     rect.animate()
     .when(1000,{
         position:[100,100],
         skew:[2,2],
         scale:[2,2],
         rotate:Math.PI
     })
     .when(2000,{
         position:[200,100],
         scale:[1,1],
         skew:[1,1],
         rotate:-Math.PI
     })
     .start();

这种实现方式有一个重大的缺点,它不能很好地对应 SVG 中的 transform 机制, *比如:<path transform="rotation(Math.PI);scale(2,2);"> 。 这个 transform 属性表达的意思是:先 rotation ,然后 scale ,这就要求严格按照 *仿射变换的顺序来进行矩阵运算,但是这里的实现不能支持这种操作。

 composeParentTransform: function composeParentTransform() {
    var needLocalTransform = this.needLocalTransform();
    var m = matrixUtil.identity(this.transform); 

    if (needLocalTransform) {
      m = this.getLocalTransform();
    } 


    var parent = this.parent;
    var parentHasTransform = parent && parent.transform;

    if (parentHasTransform) {
      if (needLocalTransform) {
        m = matrixUtil.mul(parent.transform, m);
      } else {
        matrixUtil.copy(m, parent.transform);
      }
    } 


    if (this.globalScaleRatio != null && this.globalScaleRatio !== 1) {
      this.getGlobalScale(scaleTmp);
      var relX = scaleTmp[0] < 0 ? -1 : 1;
      var relY = scaleTmp[1] < 0 ? -1 : 1;
      var sx = ((scaleTmp[0] - relX) * this.globalScaleRatio + relX) / scaleTmp[0] || 0;
      var sy = ((scaleTmp[1] - relY) * this.globalScaleRatio + relY) / scaleTmp[1] || 0;
      m[0] *= sx;
      m[1] *= sx;
      m[2] *= sy;
      m[3] *= sy;
    }

    this.transform = m;
    this.inverseTransform = this.inverseTransform || matrixUtil.create();
    this.inverseTransform = matrixUtil.invert(this.inverseTransform, m);
    return this.transform;
  }

此方法的主要作用是复合父层的变换矩阵,当元素出现嵌套时,需要此方法来复合父层上的变换。其中先后进行了自我元素的转化和父元素的转化,最后处理全局变量。

getGlobalScale: function getGlobalScale() {
    var out = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
    var m = this.transform;
    out[0] = mathSqrt(m[0] * m[0] + m[1] * m[1]); 

    out[1] = mathSqrt(m[2] * m[2] + m[3] * m[3]); 

    if (m[0] < 0) {
      out[0] = -out[0];
    }

    if (m[3] < 0) {
      out[1] = -out[1];
    }

    return out;
  },

这一段通过分别获取x和y方向上的缩放比例来最终获取全局缩放比例。

主要的内容就是这么多,讲到这里,quark renderer的图像处理部分也差不多就结束了,我的分析部分也就完结了。再见各位。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云亦纵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值