canvas画布要绘制旋转的图像,它不是旋转图像对象,它没有这个能力,它要做的是临时旋转绘画坐标系,再绘制图像。我们来看看在 400*400 的画布上绘制于画布中心的小矩形如何旋转:
尽管我们说调节矩形旋转角度——这确实也是我们直观观察到的效果——,在画布上旋转 100*40 的小矩形,实现机制却是要旋转画布坐标系。我们可以通过核心代码来理解这个问题:
ctx.clearRect(0, 0, 400, 400); //把黑板擦干净
ctx.save(); //保存画笔状态
ctx.rotate(deg * Math.PI / 180); //旋转 deg 度
ctx.fillRect(x,y,w,h); //绘制矩形 :(x,y)为矩形左上角坐标点,(w,h)为矩形宽高尺寸
ctx.restore(); //还原画笔状态
核心代码是第三行,用 rotate() 方法旋转画笔的绘图坐标系,这个坐标系也可以理解为是画布的坐标系,我们在本文的讨论过程中这两种提法都有。rotate() 需要一个参数,旋转的弧度,因此参数里是一个式子,它将获取到的 deg 角度值换算为弧度值。
演示实例的效果表明,矩形不是在自己的原始位置上旋转,它所旋转的 0 以外的任意角度,都会伴随着位置移动。这是由于画笔旋转坐标系是有一个固定的原点,在画布的左上角,坐标值是(0,0),上例中矩形的每一次旋转都围绕那个点进行,因旋转而发生的移动轨迹是一个弧形路线。也许大家会觉得奇怪,旋转为什么会产生移动?回顾一下旋转的实现机制:是画布在虚拟地旋转,即,画布旋转它的画笔坐标系,这个旋转带动矩形跟着旋转并移位。画布虚拟旋转并在其上绘制图像,可以视为是一个动态的图层,因此旋转的过程我们看不到实质性的画布旋转,而是矩形的旋转与移位。
上面的图像旋转方式应该不是我们所需要的,往往,我们对旋转运动的需求是图像在原地旋转,绕图像自己的中心点进行。画布可以做到这一点,思路与流程严格按照下面的描述进行:
① 用 translate(cx,cy) 方法临时迁移画布坐标系到图像的中心点;
② 旋转画布坐标系 deg 个角度:rotate(deg * Math.PI / 180);
③ 反向将画布坐标系移回初始坐标点 translate(-cx,-cy);
④ 绘制图像。
步骤一需要获知画布坐标系原点移动到哪里,它应当与矩形的中心重合才能达到矩形原地旋转的目的,因此,(cx,cy) 既是新坐标系的原点,也是矩形的中心点。新坐标系原点、矩形中心点共同的坐标 (cx,cy) 和矩形自身绘制时的左上角坐标(x,y)以及矩形的宽高(w,h)存在这样的数学关系:
cx = x + w / 2
cy = y + h / 2
即,矩形中心点坐标和画布新坐标系原点坐标 cx 值等于矩形左上角 x 坐标值加上矩形宽度的一半、cy 值等于矩形左上角坐标 y 坐标值加上矩形高度的一半。有了这个数据,步骤三画布坐标系原路返回就不是个问题 。
而步骤四,即绘制矩形,为什么要放在画布坐标系复原之后而不出之前呢?这是画布的思维异于常人的表现之一,它先把规划制定好,每一个规划细节都不遗漏地记录下来,规划做完了才一鼓作气把东西fill或stroke上去,而不管不顾常人的正常先后次序逻辑。切记:画布坐标系反向复原之后才能绘制矩形(或其它图像)。
这里扩展一下:如果旋转的是用 arc() 或 arcTo() 绘制成的圆,由于 arc 和 arcTo 使用圆心而不是左上角xy坐标值定位,圆的圆心即为画布坐标系移动的坐标值,省却了一些计算。
最后看效果:
附:canvas画布绘制原地旋转圆源码
<style>
.mama { font: normal 18px/26px sans-serif; }
#canv { display: block; margin: auto; border: 1px solid gray; }
.wrap { margin: 20px auto; width: 360px; }
.tMid { text-align: center; }
</style>
<div class="mama">
<h2 class="tMid">在canvas上绘制绕圆心旋转的圆</h2>
<canvas id="canv"></canvas>
<div class="wrap">
<label for="rngCx">调节圆心X坐标位置 :</label>
<input id="rngCx" type="range" min="0" max="400" value="60" />
<output id="cxData">60</output>
<br>
<label for="rngCy">调节圆心Y坐标位置 :</label>
<input id="rngCy" type="range" min="0" max="400" value="60" />
<output id="cyData">60</output>
</div>
</div>
<script>
var ctx = canv.getContext('2d');
var w = canv.width = 400, h = canv.height = 400;
var deg = 0, cx = 60, cy = 60, r = 50, raf = null;
/* 径向渐变 */
var gradient = ctx.createRadialGradient(190, 200, 15, 200, 200, 280);
gradient.addColorStop(0, 'red');
gradient.addColorStop(.15, 'orange');
gradient.addColorStop(.3, 'yellow');
gradient.addColorStop(.45, 'green');
gradient.addColorStop(.51,'cyan');
gradient.addColorStop(.85,'blue');
gradient.addColorStop(1,'purple');
ctx.fillStyle = gradient;
draw_degCircle();
/* 滑杆输入事件 :改变圆心 */
rngCx.oninput = rngCy.oninput = function(e) {
raf = cancelAnimationFrame(raf);
cx = cxData.value = rngCx.value;
cy = cyData.value = rngCy.value;
draw_degCircle();
};
/* 函数 :画绕圆心旋转的圆 */
function draw_degCircle() {
ctx.clearRect(0,0,400,400);
ctx.save();
ctx.beginPath();
ctx.translate(cx, cy);
ctx.rotate(deg * Math.PI / 180);
ctx.translate(-cx, -cy);
ctx.arc(cx, cy, r, 0, 2 * Math.PI);
ctx.fill();
ctx.restore();
deg = (deg + 1) % 360;
raf = requestAnimationFrame(draw_degCircle);
};
</script>
代码可以复制到 pencil code 运行以查看效果,也可以将代码存为本地HTML文档后运行。