深度解析canvas旋转

407 阅读5分钟

我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!

写在前面

前面用canvas复刻了小游戏坦克大战 万字长文,用canvas实现经典游戏《坦克大战》,里面坦克的转向是用4个方向的图片实现的,每个方向一张,我发现在切换方向的时候,会出现闪烁的情况,所以打算用旋转来处理坦克的转向,这样不会出现切换图片闪烁的问题,还能添加转向动画,看上去更自然。

r.gif

于是我就开始看canvas旋转的api,看了很多文章之后,发现这些文章都讲的很简单,把api调用一下发现有效果就完了,根本没有考虑到更复杂更实际的场景,所以才有了这篇文章。

问题

  1. 只考虑单个元素旋转,不考虑多元素只旋转某个元素
  2. 没有考虑旋转对元素绘制的影响
  3. 没有考虑旋转基点,只能基于元素左上角旋转
  4. 没有考虑组合旋转,即一个元素在另一个元素上的旋转
  5. 其他

绘制上下文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);

image.png

我只想旋转左上角片的图片,但是下面那张图片也被旋转了

在绘制完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);

image.png

任意位置的旋转

上面提到,旋转是要改变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);

看上去很合情合理对吧,逻辑也没问题,但是

image.png

上面那张图是正常没旋转的,我画出来用来标注位置,下面那张图才是我们要绘制的,但是为什么会偏这么多呢? 这个就是问题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);

这样,通过给旋转角度施加一个过渡,就能得到一个旋转动画了

r.gif

组合旋转

这个问题是额外的考虑,因为复杂系统里的很多元素不是孤立存在的,比如我们描述一只鸟右翅的位置,肯定是基于鸟整体的位置去描述的,而不是基于整个场景

let bird = {
    x: 100,
    y: 100,
    w: 200,
    h: 50,
    rightwing: {
        x: 20,
        h: 45,
        w: 20,
        h: 20
    }
}
// 不管鸟在哪里,右翅膀始终是在鸟的20 x 45位置

那么当右翅旋转45度的时候,鸟又倾斜了20度,且一直在移动,这个时候右翅的旋转角度,自然是要与鸟的旋转角度进行综合计算,右翅的位置也要加上鸟的位置,这样整体的效果才比较自然

r.gif

r.gif