从零打造Echarts —— V4 平移、旋转和缩放
February 21, 2019 · View on GitHub
本文开始v4版本。
回顾v3
v3版本我们在v2的基础上添加了动画功能,可以实现对样式形状等的更新动画,但是最后发现少了很关键的transform动画,本版本就将为我们的xrender添加transform系统并同样实现它动画。
开始
什么是transform
就表现而言,是translate(平移)、scale(缩放)、rotate(旋转)、skew(拉伸)等效果,而其本质,则是相对坐标系的变换。平移和缩放很好理解,以矩形举例:
前方灵魂画手上线,请注意躲避!
对该矩形放大(scale)1.5倍,则将其所以点的坐标都乘以1.5,则可得到变换后的矩形(以原点为变换中心点(下面会讲到),下同)。平移同理,加减即可。
拉伸则取对应轴角度的tan值即可。
而旋转则稍微复杂一点,容易想象初始矩形顺时针旋转90度(π / 2)的样子。下图绿色部分。
点(0, 1)是如何变换到(1, 0)的?很容易看出来
x=x * cos(π / 2) - y * sin(π / 2)y=y * sin(π / 2) + x * sin(π / 2)

啊住手!!
旋转90度是为了好画,并不有利于证明,以最容易的角度考虑,将A(x1, y1)点逆时针旋转β度到A'(x2, y2),都在第一象限内,其它情况也差不多。
。
因为有些浏览器不支持公式表达,所以用了图片

上述所有变换用矩阵来表示就是

上面的六个变量,就是transform的基础。和css中有matrix一样,canvas提供了这样的api——setTransform。
但是想实现变换却没有这么简单。
- 首先我们使用时不可能直接用矩阵去设置,需要将对应的属性转换成矩阵,如
translateX: 20,转换为[1, 0, 0, 1, 20, 0]。
不过
canvas提供了translate等方法,我们可以直接使用。
- 其次存在多个变换属性时,如同时偏移和旋转,需要将二者经过第一步转换后再叠加计算,而这又牵扯到顺序问题了,是先偏移再旋转还是先旋转再偏移?是提供一个约定的方式,还是提供可选择的配置?
同样
canvas也提供了transform方法可以叠加变换。
那么上面说辣么多有什么用呢?后续会用到的。
属性设计
那么一个元素应该有这些transform属性。
rotation旋转角度,和rotate方法以示区分。position位置,即偏移,一个数组,[translateX,translateY]。scale缩放,同样是一个数组,[scaleX,scaleY]。origin变换中心,缩放和旋转会用到,也是一个数组,[originX,originY]。
编辑XElement.ts。
首先声明一个Transform接口。
interface Transform {
/**
* 位置,即偏移
*/
position: [number, number]
/**
* 缩放
*/
scale: [number, number]
/**
* 旋转
*/
rotation: number
/**
* 变换中心
*/
origin: [number, number]
}
然后让XElement实现它,并在updateOptions中应用传入的选项。
class XElement implements Transform {
// ...
position: [number, number] = [0, 0]
scale: [number, number] = [1, 1]
// 默认以左上角为变换中心,因为无法用百分比————每个图形元素的形状都不相同
origin: [number, number] = [0, 0]
rotation = 0
// ...
updateOptions () {
let opt = this.options
// ...
['zLevel', 'origin', 'scale', 'position', 'rotation'].forEach(key => {
if (opt[key]) {
this[key] = opt[key]
}
})
}
}
紧接着编写setTransform,它将应用变换到上下文中,然后在beforeRender中调用它。
class XElement implements Transform {
beforeRender (ctx: CanvasRenderingContext2D) {
// ...
this.setTransform(ctx)
// ...
}
/**
* 设置变换
*/
setTransform (ctx: CanvasRenderingContext2D) {
// 首先变换中心点
ctx.translate(...this.origin)
// 应用缩放
ctx.scale(...this.scale)
// 应用旋转
ctx.rotate(this.rotation)
// 恢复
ctx.translate(-this.origin[0], -this.origin[1])
// 平移
ctx.translate(...this.position)
}
}
尝试一下
let rect = new xrender.Rect({
shape: {
x: 120,
y: 120,
width: 40,
height: 40
},
style: {
fill: 'transparent'
},
origin: [120, 120],
rotation: 0.8
})
可以看到有效果了。
动画
那么动画呢?都不用尝试,就知道不行,因为之前的动画中没有对值为数组的情况进行处理。
function getNestedValue (
preValue: any,
nextValue: any,
currentTime: number,
duringTime: number,
easingFn: EasingFn
) {
let value
// 假定前后两次值的类型相同
if (isObject(nextValue)) {
value = {}
for (let key in nextValue) {
value[key] = getNestedValue(preValue[key], nextValue[key], currentTime, duringTime, easingFn)
}
// 数组类型
} else if (Array.isArray(nextValue)) {
value = []
for (let i = 0; i < nextValue.length; i += 1) {
value[i] = getNestedValue(preValue[i], nextValue[i], currentTime, duringTime, easingFn)
}
}
//...
}
其次是之前为元素创建动画时也没有传入这些属性。
class Animation {
animateTo () {
// ...
let animateProps = [
'shape',
'style',
'position',
'scale',
'origin',
'rotation'
]
let animteTarget = {}
animateProps.forEach(prop => {
animteTarget[prop] = this[prop]
})
this.animation = new Animation(animteTarget)
// ...
}
}
走你animateTo({position: [20, 90]}, 400)。可以看见已完全ok!
小结
本版本主要为元素应用了常见的transform变换,并完善了动画相关的细节。