2021SC@SDUSC
大概是我负责的最后一篇了,最后的最后,我们来看一下绘画系统中的图形变换功能(transform)。原本前面应该是还有个图形部分,但这一部分的内容与线的绘制等方式大同小异,只是单纯的运算,所以直接跳过了。
这个类是为 Element 类提供变换功能,例如:平移、缩放、扭曲、旋转、翻转、形状、样式。 通过用新的事件机制和继承机制,把 Element 类里面与变形有关的逻辑移到本类中来,保持 Element 干净整洁。
在我们研究转换方法之前,让我们先看看另外两种方法,一旦要开始生成更复杂的绘图,它们就必不可少。
保存画布的整个状态。
恢复最近保存的画布状态。
画布状态存储在堆栈中。每次save()
调用该方法时,都会将当前绘图状态压入堆栈。绘图状态包括
已应用的转换(即translate
,rotate
和scale等
)。
以下属性的当前值:strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled.
可以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的图像处理部分也差不多就结束了,我的分析部分也就完结了。再见各位。