我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!
写在前面
前面用canvas
复刻了小游戏坦克大战 万字长文,用canvas实现经典游戏《坦克大战》,里面坦克的转向是用4个方向的图片实现的,每个方向一张,我发现在切换方向的时候,会出现闪烁的情况,所以打算用旋转来处理坦克的转向,这样不会出现切换图片闪烁的问题,还能添加转向动画,看上去更自然。
于是我就开始看canvas
旋转的api
,看了很多文章之后,发现这些文章都讲的很简单,把api
调用一下发现有效果就完了,根本没有考虑到更复杂更实际的场景,所以才有了这篇文章。
问题
- 只考虑单个元素旋转,不考虑多元素只旋转某个元素
- 没有考虑旋转对元素绘制的影响
- 没有考虑旋转基点,只能基于元素左上角旋转
- 没有考虑组合旋转,即一个元素在另一个元素上的旋转
- 其他
绘制上下文context
旋转的api
很简单
context.rotate(deg); // 把绘制上下文按照顺时针旋转deg度
这里简单说一下canvas
的绘制上下文context
,任何绘制都是基于这个context
的,而且是共享的,初始时这个context
的基点是canvas
左上角的顶点,context
改变之后,后续所有的绘制都会受到影响,问题1
就是由这个导致的,所以如果绘制是针对单个元素的,设置完context
之后一定要重置,不然会影响其他元素.
context.rotate(10 * Math.PI / 180);
context.drawImage(img1, 0, 0, 100, 100);
context.drawImage(img2, 100, 100, 100, 100);
我只想旋转左上角片的图片,但是下面那张图片也被旋转了
在绘制完img1
之后,把context
重置,再绘制img2
,这样就解决了问题1
,可以指定旋转任意元素而不影响其他元素了
context.rotate(10 * Math.PI / 180);
context.drawImage(this, 0, 0, 100, 100);
context.setTransform(1, 0, 0, 1, 0, 0);
// 再绘制img2
context.drawImage(this, 100, 100, 100, 100);
任意位置的旋转
上面提到,旋转是要改变context
的,这个改变不止影响到其他元素的绘制,还会影响到旋转元素自身的绘制
例如一张100 x 100
的图片绘制在100 x 100
的位置,这个很简单
context.drawImage(img, 100, 100, 100, 100)
但是我们如果想要绕图片左上角旋转这张图片,先要把context
移动到100 x 100
,然后旋转
context.translate(100, 100)
context.rotate(10 * Math.PI / 180);
context.drawImage(img, 100, 100, 100, 100);
看上去很合情合理对吧,逻辑也没问题,但是
上面那张图是正常没旋转的,我画出来用来标注位置,下面那张图才是我们要绘制的,但是为什么会偏这么多呢?
这个就是问题2
了,context
的旋转只是改变角度,但是基点的移动会影响元素绘制的参数
context.drawImage(img, x, y, w, h)
这行代码可以这样理解,把img
绘制在context
基点的xy
处,绘制大小为wh
,而由于我们移动了基点,所以img
的绘制位置就要重新计算了,这里我们把基点移到了100 x 100
,那么img
的绘制就变成了这样
context.drawImage(img, 0, 0, 100, 100); // 位置从100 * 100 -> 0 * 0
设置旋转基点
这个我看很多文章里都没有提到,我就很疑惑,难道他们的需求都是绕着左上角旋转的吗,最起码绕中心旋转是很常见的啊
css里的旋转是可以设置旋转基点的
transform-origin: 50% 50%; // 2个数值就可以确定平面一个点了,用百分比就可以忽略元素尺寸了
那么也可以这样给canvas
里的元素设置旋转基点,元素的旋转逻辑是这样的
let origin = [50, 50]
// 1 先移到基点
context.translate(x + img.w * origin[0] / 100, y + img.h * origin[1] / 100);
// 2 旋转一定度数
context.rotate(angle * Math.PI / 180);
// 3 绘制元素,要基于translate之后重新计算位置,元素绘制是基于左上角xy的
context.drawImage(img, -img.w * origin[0] / 100, -img.h * origin[1] / 100, img.w, img.h);
// 4 重置context
context.setTransform(1, 0, 0, 1, 0, 0);
这样,通过给旋转角度施加一个过渡,就能得到一个旋转动画了
组合旋转
这个问题是额外的考虑,因为复杂系统里的很多元素不是孤立存在的,比如我们描述一只鸟右翅
的位置,肯定是基于鸟整体
的位置去描述的,而不是基于整个场景
let bird = {
x: 100,
y: 100,
w: 200,
h: 50,
rightwing: {
x: 20,
h: 45,
w: 20,
h: 20
}
}
// 不管鸟在哪里,右翅膀始终是在鸟的20 x 45位置
那么当右翅旋转45度
的时候,鸟又倾斜了20度
,且一直在移动,这个时候右翅的旋转角度,自然是要与鸟的旋转角度进行综合计算,右翅的位置也要加上鸟的位置,这样整体的效果才比较自然