在前端开发中不论CSS3也好,SVG也好,还是Canvas,无论多复杂的图形,都是由一个个点组成的。一个元素渲染后就可以得到一张位图,然后对这个位图上每一点进行变换,就可以得到新的一张位图,从而产生了视觉上的平移translate、旋转rotate、缩放scale、拉伸skew 等效果。这一切都是变换矩阵的功劳。
2d与3d矩阵变换公式
CSS3中的2d矩阵变换
-
CSS3中的
transform: translate(tx, ty)
等价于transform: matrix(1,0,0,1,tx,ty)
-
CSS3中的
transform:rotate(θ)
等价于transform: matrix(cosθ,sinθ,-sinθ,cosθ,0,0)
-
CSS3中的
transform: scale(Sx, Sy)
等价于transform: matrix(Sx,0,0,Sy,0,0)
-
CSS 3中的
transform: skew(θx,θy)
等价于transform:matrix(tan(θx),0,0,tan(θy),0,0)
CSS3中的3d矩阵变换
给个简单的demo,利用CSS3动画库 bounce.js 通过设置元素属性 transform:matrix3d
(1-16位参数值) 让元素偏移 200px。 从matrix三阶矩阵到matrix3d四阶矩阵,本质上很多东西都与2D大同小异,只是复杂度不一样而已。
<!--dom-->
<div class="animation-target"></div>
<!--css代码-->
.animation-target {
background-color: #333;
height: 100px;
width: 100px;
}
<!--js代码-->
let bounce = new Bounce();
bounce.translate({
from: { x: 0 },
to: { x: 200 }
})
bounce.applyTo(document.querySelector('.animation-target'))
/*
bounce.js 其余scale、rotate、skew等api最终都是通过transform:matrix3d(1-16位参数值)来实现元素的动画效果
*/
效果图如下:matrix3d对应的矩阵运算细节可参考 bounce.js
svg中的矩阵变换
svg中的变换原理和css3中的类似,但是在svg中没有相关的属性来设置变换的中心点,只能通过translate来模拟。如下设置transform-origin是不生效的
<!--dom-->
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="100" height="190">
<rect id="SvgjsRect1008" width="100" height="100" transform-origin="0% 0%" transform="translate(100, 0)"></rect>
</svg>
/*
transform-origin="0% 0%" 设置是不生效的
*/
下面结合SVG.js 这个开源库来看下svg中的矩阵变换应用
<!--dom 作为SVG.js的挂载节点-->
<div id="drawing"></div>
<!--js-->
let draw = SVG('drawing').size(300, 300)
let rect = draw.rect(100, 100)
rect.rotate(45) // 旋转45度
如图:
那么rect.rotate(45)
是怎么变成 上图中的 matrix
的呢? 如下:
<!--SVG.js 中的关键代码截取-->
rect.rotate(45)
=>
rotate: function(d, cx, cy) {
return this.transform({ rotation: d, cx: cx, cy: cy })
}
transform: function(o, relative) {
var target = this, matrix; // this 通过SVG.js new 出的一个react实例对象
matrix = new SVG.Matrix(target) // 获取当前元素的transform: matrix矩阵值,具体实现可以查阅SVG.js源码
... 此处省略部分代码
ensureCentre(o, target) // 通过svg原生api, element.node.getBBox()来计算元素的中心点o
// 应用矩阵
matrix = relative ?
// relative
matrix.rotate(o.rotation, o.cx, o.cy) :
// absolute
matrix.rotate(o.rotation - matrix.extract().rotation, o.cx, o.cy)
}
rotate: function(r, cx, cy) {
// convert degrees to radians
r = SVG.utils.radians(r)
// 计算对应的旋转矩阵, 对应a,b,c,d,e,f值如下
return this.around(cx, cy, new SVG.Matrix(Math.cos(r), Math.sin(r), -Math.sin(r), Math.cos(r), 0, 0))
}
// Transform around a center point 以中心点来进行旋转
around: function(cx, cy, matrix) {
//这里很重要:svg以某个参考点变换时 => 通过先平移translate(cx,cy), 然后transform目标matrix值, 再进行反平移translate(-cx,-cy)
return this
.multiply(new SVG.Matrix(1, 0, 0, 1, cx || 0, cy || 0))
.multiply(matrix)
.multiply(new SVG.Matrix(1, 0, 0, 1, -cx || 0, -cy || 0))
}
multiply: function(matrix) {
// 这里是把元素当前的transform: matrix属性值与目标矩阵相乘
return new SVG.Matrix(this.native().multiply(parseMatrix(matrix).native()))
}
native: function() {
// SVG.js 中的矩阵相乘, 利用的是svg元素的原生api, createSVGMatrix()来实现
// create new matrix
var matrix = SVG.parser.native.createSVGMatrix()
// update with current values
for (var i = abcdef.length - 1; i >= 0; i--)
matrix[abcdef[i]] = this[abcdef[i]]
return matrix
}
//最后把最终计算的matrix变换值设置到react元素上就完成了
this.attr('transform', matrix)
canvas中的矩阵变换
同样结合一个canvas开源库 fabric.js 来说下canvas中的矩阵变换应用,这个库实现很牛叉,里面有大量的功能点,比如: 通过 canvas 绘制的元素如何解决Retina 屏中元素模糊的问题,以及如何采用离线canvas做性能提升等等。
<!--dom 作为canvas绘制画布-->
<div id="drawing"></div>
<!--js-->
let canvas = new fabric.Canvas('canvas', { width: "375", height: "500" })
let react = new fabric.Rect({ width: 100, height: 100, left: 100, top: 100, fill: 'red', angle: 10 })
canvas.add(react)
如图:
同样说下fabric.js 是如何绘制的
<!-- fabric.js 部分代码截取,有大量的代码省略,只挑取关键流程代码 -->
canvas.add(react)
=>
add: function () {
/* 变量说明:
this => new fabric.Canvas 实例对象
*/
this.renderOnAddRemove && this.requestRenderAll();
return this;
},
// ...省略部分代码...
renderAll: function () {
/* 变量说明:
this => new fabric.Canvas 实例对象
this.contextContainer => 如上图class="lower-canvas" canvas元素的2d绘图上下文
this._chooseObjectsToRender() => 获取要渲染的object对象,这里就是react实例对象
*/
var canvasToDrawOn = this.contextContainer;
this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender());
return this;
},
renderCanvas: function(ctx, objects) {
/* 变量说明:
this => new fabric.Canvas 实例对象
ctx => 如上图class="lower-canvas" canvas元素的2d绘图上下文
objects => fabric.js 创建的react实例对象
*/
// 擦除lower-canvas
this.clearContext(ctx);
ctx.save();
// 对object元素进行绘制draw
this._renderObjects(ctx, objects);
ctx.restore();
},
// ...省略部分代码,最终调用object的render方法,如下 ...
render: function(ctx) {
/* 变量说明:
this => fabric.js 创建的react实例对象
ctx => 如上图class="lower-canvas" canvas元素的2d绘图上下文
*/
ctx.save();
// 对ctx坐标进行变换 很重要
this.transform(ctx);
// 如果元素需要走缓存策略:则会创建缓存canvas, 然后在缓存canvas上绘制object对象(这里是fabric.js 创建的react实例对象),最后再把缓存canvas => ctx.drawImage(this._cacheCanvas, -this.cacheTranslationX, -this.cacheTranslationY); 绘制到lower-canvas上
if (this.shouldCache()) {
// 创建了一个缓存canvas、并在缓存canvas上draw绘制object元素
this.renderCache();
// 然后将缓存的canvas绘制到lower-canvas中
this.drawCacheOnCanvas(ctx);
}
else {
this._removeCacheCanvas();
this.drawObject(ctx);
}
ctx.restore();
// ...省略部分代码...
},
// 接下来主要看 this.transform(ctx); 对lower-canvas的坐标变换
transform: function(ctx) {
var m;
if (this.group && !this.group._transformDone) {
m = this.calcTransformMatrix();
}
else {
// 根据object的信息, 计算矩阵
m = this.calcOwnMatrix();
}
// 将lower-canvas坐标系平移到
ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
},
calcOwnMatrix: function() {
// ...省略部分代码...
/* 变量说明:
this => fabric.js 创建的react实例对象,也即是object
*/
var tMatrix = this._calcTranslateMatrix();
this.translateX = tMatrix[4];
this.translateY = tMatrix[5];
cache.key = key;
cache.value = fabric.util.composeMatrix(this);
return cache.value;
},
.......... 下面这部分是计算object经过scale、rotate等后的中心点坐标 ....................
_calcTranslateMatrix: function() {
var center = this.getCenterPoint();
return [1, 0, 0, 1, center.x, center.y];
},
getCenterPoint: function() {
var leftTop = new fabric.Point(this.left, this.top);
// 这里的 this.originX, this.originY就是rotate的旋转参考点
return this.translateToCenterPoint(leftTop, this.originX, this.originY);
},
translateToCenterPoint: function(point, originX, originY) {
// 根据this.originX, this.originY 计算object中心点center
var p = this.translateToGivenOrigin(point, originX, originY, 'center', 'center');
// 计算得到的center点后,再计算旋转后的点坐标
if (this.angle) {
return fabric.util.rotatePoint(p, point, degreesToRadians(this.angle));
}
return p;
},
rotatePoint: function(point, origin, radians) {
// 这里计算旋转后的坐标,先减去旋转参考点this.originX, this.originY 的值,然后将得到的值经过旋转radians角度后,再将得到的坐标加上参考点this.originX, this.originY的值,既可得到最终的坐标值
point.subtractEquals(origin);
var v = fabric.util.rotateVector(point, radians);
return new fabric.Point(v.x, v.y).addEquals(origin);
},
rotateVector: function(vector, radians) {
var sin = fabric.util.sin(radians),
cos = fabric.util.cos(radians),
rx = vector.x * cos - vector.y * sin,
ry = vector.x * sin + vector.y * cos;
return {
x: rx,
y: ry
};
},
.......... 下面这段是对 fabric.util.composeMatrix(this) 说明....................
composeMatrix: function(options) {
/* 变量说明:
options => fabric.js 创建的react实例对象,也即是object
*/
// 平移中心点的矩阵
var matrix = [1, 0, 0, 1, options.translateX || 0, options.translateY || 0],
multiply = fabric.util.multiplyTransformMatrices;
// 平移中心点的矩阵 * 旋转对应的矩阵
if (options.angle) {
matrix = multiply(matrix, fabric.util.calcRotateMatrix(options));
}
// 平移中心点的矩阵 * 旋转对应的矩阵 * 缩放及拉伸的矩阵
if (options.scaleX || options.scaleY || options.skewX || options.skewY || options.flipX || options.flipY) {
matrix = multiply(matrix, fabric.util.calcDimensionsMatrix(options));
}
return matrix;
},
// 3*3 的矩阵相乘
multiplyTransformMatrices: function(a, b, is2x2) {
// Matrix multiply a * b
return [
a[0] * b[0] + a[2] * b[1],
a[1] * b[0] + a[3] * b[1],
a[0] * b[2] + a[2] * b[3],
a[1] * b[2] + a[3] * b[3],
is2x2 ? 0 : a[0] * b[4] + a[2] * b[5] + a[4],
is2x2 ? 0 : a[1] * b[4] + a[3] * b[5] + a[5]
];
},
// 计算旋转的矩阵坐标
calcRotateMatrix: function(options) {
if (!options.angle) {
return fabric.iMatrix.concat();
}
var theta = fabric.util.degreesToRadians(options.angle),
cos = fabric.util.cos(theta),
sin = fabric.util.sin(theta);
return [cos, sin, -sin, cos, 0, 0];
},
// 计算缩放和拉伸的矩阵坐标
calcDimensionsMatrix: function(options) {
var scaleX = typeof options.scaleX === 'undefined' ? 1 : options.scaleX,
scaleY = typeof options.scaleY === 'undefined' ? 1 : options.scaleY,
scaleMatrix = [
options.flipX ? -scaleX : scaleX,
0,
0,
options.flipY ? -scaleY : scaleY,
0,
0],
multiply = fabric.util.multiplyTransformMatrices,
degreesToRadians = fabric.util.degreesToRadians;
if (options.skewX) {
scaleMatrix = multiply(
scaleMatrix,
[1, 0, Math.tan(degreesToRadians(options.skewX)), 1],
true);
}
if (options.skewY) {
scaleMatrix = multiply(
scaleMatrix,
[1, Math.tan(degreesToRadians(options.skewY)), 0, 1],
true);
}
return scaleMatrix;
},
// 最后将经过平移中心点坐标矩阵 * 旋转对应的矩阵 * 缩放及拉伸的矩阵计算得到的matrix值设置到lower-canvas坐标系中, 如上transform方法所示
ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
//接下来进行元素的绘制, 上面render方法中由于篇幅的原因绘制过程就不展开了
...
到这里终于把fabric.js 绘制元素过程中坐标矩阵的变换说完了,具体的代码细节自行查阅源码吧!
最后
了解矩阵变换的概念,旨在揭开相关开源代码实现的背后细节,欢迎吐槽!