canvas 变形
状态保存 save() 和 restore()
save
CanvasRenderingContext2D.save() 是 Canvas 2D API 通过将当前状态放入栈中,保存 canvas 全部状态的方法。
void ctx.save();保存的状态又下面这些部分组成:
- 当前的变换矩阵。
- 当前的剪切区域。
- 当前的虚线列表。
- 以下属性当前的值:strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled.
restore
CanvasRenderingContext2D.restore() 将 canvas 当前的状态恢复到上一次调用 save() 时保存的状态。注意已经在 canvas 上绘制的图案不会恢复。这就像你一开始用装着黑墨水的钢笔画了一个圆,然后又换了红墨水再画一个圆,然后钢笔又恢复到最开始装着黑墨水的状态。但已经画在纸上的圆不会消失。
示例
代码:
<template>
<canvas id='save-restore' width="200" height="200"></canvas>
</template>
<script setup lang='ts'>
import { onMounted } from 'vue';
onMounted(() => {
const ctx = (document.getElementById('save-restore') as HTMLCanvasElement).getContext('2d')!
// 保存当前状态
ctx.save()
// 画一个绿色矩形
ctx.fillStyle = 'green'
ctx.fillRect(0, 0, 100, 200)
// 恢复状态
ctx.restore()
// 画一个黑色矩形
// 因为已经恢复状态,而填充颜色默认就是黑色,所以不用设置填充色,直接填充矩形
ctx.fillRect(100, 0, 100, 200)
})
</script>移动 translate
Canvas 2D API 的 CanvasRenderingContext2D.translate() 用于移动网格(坐标原点)。
void ctx.translate(x, y);translate() 方法,将 canvas 按原始 x 点的水平方向、原始的 y 点垂直方向进行平移变换。

例子:利用 translate 移动坐标原点,在画布中间画一个圆。
代码:
<template>
<canvas id='translate-demo' width="200" height="200"></canvas>
</template>
<script setup lang='ts'>
import { onMounted } from 'vue';
onMounted(() => {
const ctx = (document.getElementById('translate-demo') as HTMLCanvasElement).getContext('2d')!
ctx.strokeRect(0, 0, 200, 200)
ctx.translate(100, 100)
ctx.arc(0, 0, 50, 0, 2 * Math.PI)
ctx.stroke()
})
</script>旋转 rotate
CanvasRenderingContext2D.rotate() 以 canvas 的坐标原点为中心,进行逆时针或顺时针旋转。
void ctx.rotate(angle);
参数
- angle
顺时针旋转的弧度。如果你想通过角度值计算,可以使用公式:degree * Math.PI / 180 。
旋转中心点一直是 canvas 的起始点。如果想改变中心点,我们可以通过 translate() 方法移动 canvas。
代码:
<template>
<canvas id='rotate-demo' width="200" height="200"></canvas>
</template>
<script setup lang='ts'>
import { onMounted } from 'vue';
onMounted(() => {
const ctx = (document.getElementById('rotate-demo') as HTMLCanvasElement).getContext('2d')!
ctx.strokeRect(0, 0, 200, 200)
// 坐标轴移动到中心点
ctx.translate(100, 100)
for (let i = 1; i <= 5; i ++) {
ctx.fillStyle = `rgb(${51 * i}, ${255-51 * i}, 255)`
for (let j = 0, cirNum = i * 6; j < cirNum; j ++) {
ctx.save()
ctx.beginPath()
ctx.rotate(j * Math.PI * 2 / cirNum)
ctx.arc(i * 15, 0, 5, 0, 2 * Math.PI)
ctx.fill()
ctx.closePath()
ctx.restore()
}
}
})
</script>缩放 scale
我们用它来增减图形在 canvas 中的像素数目,对形状,位图进行缩小或者放大。
ctx.scale(x, y)scale 方法可以缩放画布的水平和垂直的单位。两个参数都是实数,可以为负数。x 为水平缩放因子,y 为垂直缩放因子,如果比 1 小,会缩小图形,如果比 1 大会放大图形。默认值为 1,为实际大小。
画布初始情况下,是以左上角坐标为原点的第一象限。如果参数为负实数,相当于以 x 或 y 轴作为对称轴镜像反转(例如,使用 translate(0, canvas.height); scale(1,-1) 以 y 轴作为对称轴镜像反转,就可得到著名的左下角为原点的笛卡尔坐标系)。
默认情况下,canvas 的 1 个单位为 1 个像素。举例说,如果我们设置缩放因子是 0.5,1 个单位就变成对应 0.5 个像素,这样绘制出来的形状就会是原先的一半。同理,设置为 2.0 时,1 个单位就对应变成了 2 像素,绘制的结果就是图形放大了 2 倍。
例子:
黑色正方形被拉伸为了长方形,MDN 以镜像的样子显示。
代码:
<template>
<canvas id='scale-demo' width="200" height="200"></canvas>
</template>
<script setup lang='ts'>
import { onMounted } from 'vue';
onMounted(() => {
const ctx = (document.getElementById('scale-demo') as HTMLCanvasElement).getContext('2d')!
// draw a simple rectangle, but scale it.
ctx.save();
ctx.scale(10, 3);
ctx.fillRect(1, 10, 10, 10);
ctx.restore();
// mirror horizontally
ctx.scale(-1, 1);
ctx.font = '48px serif';
ctx.fillText('MDN', -135, 120);
})
</script>变形(矩阵变换 Transforms)
transform
ctx.transform() 是 Canvas 2D API 使用矩阵多次叠加当前变换的方法,矩阵由方法的参数进行描述。你可以缩放、旋转、移动和倾斜上下文。
换句话说,你可以使用 ctx.transform() 方法来缩放、平移、移动、旋转,倾斜整个 canvas 上下文,包括坐标轴、网格、已绘制的图像。
执行这个方法时,参数的参照物为本身当前的矩阵。
语法
void ctx.transform(a, b, c, d, e, f);变换矩阵的描述:
参数
a (m11)
水平方向进行缩放。
b (m12)
水平轴变长,然后向垂直轴倾斜,直到等于传入参数的角度。
c (m21)
垂直轴变长,然后向水平轴倾斜,直到等于传入参数的角度。
d (m22)
垂直方向进行缩放。
e (dx)
水平移动。
f (dy)
垂直移动。setTransform
setTransform(a, b, c, d, e, f) 这个方法会将当前的变形矩阵重置为单位矩阵,然后用相同的参数调用 transform 方法。
从根本上来说,该方法是取消了当前变形,然后设置为指定的变形,一步完成
resetTransform
重置当前变形为单位矩阵,它和调用以下语句是一样的:ctx.setTransform(1, 0, 0, 1, 0, 0)。
例子 1
代码:
<template>
<button class="bg-blue-400 text-white rounded-1 pa-4 mb-10px" @click="transformMatrix">变换矩阵</button>
<div class="w-full pt-full relative">
<canvas id='transform-demo1' class='border border-solid border-red-500 absolute left-0 top-0 w-full h-full' width="400" height="400"></canvas>
</div>
<ul>
<li v-for="(item, index) in steps" :class="stepNum === index ? 'text-red' : ''">
<div>{{ item.funText }}</div>
<div>{{ item.tips }}</div>
</li>
</ul>
</template>
<script setup lang='ts'>
import { onMounted, ref } from 'vue';
let ctx1: CanvasRenderingContext2D
const steps = [
{
funText: 'ctx1.setTransform(1, 0, 0, 1, 100, 100)',
tips: '最初的模样'
},
{
funText: 'ctx1.setTransform(cos, 0, 0, 1, 100, 100)',
tips: '水平方向进行缩放。'
},
{
funText: 'ctx1.setTransform(cos, sin, 0, 1, 100, 100)',
tips: '水平轴变长,然后向垂直轴倾斜,直到等于传入参数的角度。'
},
{
funText: 'ctx1.setTransform(cos, sin, -sin, 1, 100, 100)',
tips: '垂直轴变长,然后向水平轴倾斜,直到等于传入参数的角度。'
},
{
funText: 'ctx1.setTransform(cos, sin, -sin, cos, 100, 100)',
tips: '垂直方向缩放'
}
] as {
funText: string,
tips: string
}[]
let stepNum = ref(0)
function transformMatrix() {
const sin = Math.sin(Math.PI / 6)
const cos = Math.cos(Math.PI / 6)
ctx1.resetTransform()
ctx1.clearRect(0, 0, 400, 400)
if (stepNum.value === 0) {
ctx1.setTransform(cos, 0, 0, 1, 200, 200)
} else if (stepNum.value === 1) {
ctx1.setTransform(cos, sin, 0, 1, 200, 200)
} else if (stepNum.value === 2) {
ctx1.setTransform(cos, sin, -sin, 1, 200, 200)
} else if (stepNum.value === 3) {
ctx1.setTransform(cos, sin, -sin, cos, 200, 200)
} else if (stepNum.value === 4) {
ctx1.setTransform(1, 0, 0, 1, 200, 200)
}
// 因为 setTransform 重置了矩阵,所有需要重新设置
stepNum.value = (stepNum.value + 1) % 5
draw()
}
function draw() {
ctx1.lineWidth = 1
ctx1.strokeStyle = '#000'
for (let i = 0; i < 8 + 1; i ++) {
if (i === 4) continue
// 把横线全部画完
ctx1.beginPath()
ctx1.moveTo(-80, i * 20 + 0.5 - 80)
ctx1.lineTo(80, i * 20 + 0.5 -80)
ctx1.stroke()
ctx1.closePath()
// 把竖线全部画完
ctx1.beginPath()
ctx1.moveTo(i * 20 + 0.5 - 80, -80)
ctx1.lineTo(i * 20 + 0.5 - 80, 80)
ctx1.stroke()
ctx1.closePath()
}
ctx1.strokeStyle = 'red'
// 画 x 坐标轴
ctx1.beginPath()
ctx1.moveTo(-80, 4 * 20 + 0.5 - 80)
ctx1.lineTo(80, 4 * 20 + 0.5 - 80)
// 画 y 坐标轴
ctx1.moveTo(4 * 20 + 0.5 - 80, -80)
ctx1.lineTo(4 * 20 + 0.5 - 80, 80)
ctx1.stroke()
ctx1.closePath()
}
onMounted(() => {
const demo1 = document.getElementById('transform-demo1') as HTMLCanvasElement
ctx1 = demo1.getContext('2d')!
ctx1.translate(200, 200)
draw()
})
</script>例子 2(加载中图案)
代码:
<template>
<canvas id='loading-demo' width="200" height="200"></canvas>
</template>
<script setup lang='ts'>
import { onMounted } from 'vue'
onMounted(() => {
const ctx = (document.getElementById('loading-demo') as HTMLCanvasElement).getContext('2d')!
const sin = Math.sin(Math.PI/6)
const cos = Math.cos(Math.PI/6)
ctx.translate(100, 100)
function draw() {
for (let i = 0; i <= 11; i ++) {
const c = Math.floor(255 / 11 * (11 - i))
ctx.fillStyle = "rgb(" + c + "," + c + "," + c + ")"
ctx.fillRect(0, 0, 100, 10)
ctx.transform(cos, sin, -sin, cos, 0, 0)
}
}
let startTime = new Date().getTime()
turn()
function turn() {
const nowTime = new Date().getTime()
if (nowTime - startTime > 100) {
startTime = nowTime
ctx.clearRect(-100, -100, 200, 200)
// ctx.rotate 方法旋转的角度是基于当前的坐标矩阵。//
// 所以可以像下面这样直接调用 ctx.rotate()
ctx.rotate(2 * Math.PI / 12)
draw()
}
requestAnimationFrame(turn)
}
})
</script>