Skip to content

canvas 变形

状态保存 save() 和 restore()

save

CanvasRenderingContext2D.save() 是 Canvas 2D API 通过将当前状态放入栈中,保存 canvas 全部状态的方法。

js
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 上绘制的图案不会恢复。这就像你一开始用装着黑墨水的钢笔画了一个圆,然后又换了红墨水再画一个圆,然后钢笔又恢复到最开始装着黑墨水的状态。但已经画在纸上的圆不会消失。

示例

代码:

vue
<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() 用于移动网格(坐标原点)。

js
void ctx.translate(x, y);

translate() 方法,将 canvas 按原始 x 点的水平方向、原始的 y 点垂直方向进行平移变换。

例子:利用 translate 移动坐标原点,在画布中间画一个圆。

代码:

vue
<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 的坐标原点为中心,进行逆时针或顺时针旋转。

js
void ctx.rotate(angle);

参数

  • angle

顺时针旋转的弧度。如果你想通过角度值计算,可以使用公式:degree * Math.PI / 180 。

旋转中心点一直是 canvas 的起始点。如果想改变中心点,我们可以通过 translate() 方法移动 canvas。

代码:

vue
<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 中的像素数目,对形状,位图进行缩小或者放大。

js
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 以镜像的样子显示。

代码:

vue
<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 上下文,包括坐标轴、网格、已绘制的图像。

执行这个方法时,参数的参照物为本身当前的矩阵。

语法

js
void ctx.transform(a, b, c, d, e, f);

变换矩阵的描述:

(1)[acebdf001]

参数

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

代码:

vue
<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(加载中图案)

代码:

vue
<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>

Released under the MIT License.