canvas 基点变换

156 阅读8分钟

源代码地址:gitee.com/huohuofei/c…

在canvas 中,坐标原点在画布的左上角,这就意味着当我们对画布进行变换操作(平移、旋转、缩放)时,变换的基点就是左上角。

1.png

注1:由上图可知,平移、缩放、旋转,都是针对坐标原点(旋转和缩放的基点,是第一次执行平移后的原点)

function draw1() {
  if (!canvasRef1.value) return;
  const ctx = canvasRef1.value.getContext('2d') as CanvasRenderingContext2D;
  drawGrid(ctx);
  ctx.fillStyle = 'skyblue';
  ctx.fillRect(0, 0, 50, 50);
  // 位移 (150,100)
  ctx.translate(150, 100);
  ctx.fillRect(0, 0, 50, 50);

  // 旋转 45
  ctx.rotate(Math.PI / 4);
  ctx.fillStyle = 'yellow';
  ctx.fillRect(0, 0, 50, 50);

  // 缩放 (2,3)
  ctx.scale(2, 3);
  ctx.fillStyle = 'rgba(255,0,0,0.5)';
  ctx.fillRect(0, 0, 50, 50);
}

这种基于原点的变换,并不能满足开发中的所有场景。我们需要想要基于物体的中心点、左上角点、任意点进行变换。 将变换的原点移动到我们指定的位置,这就是基点变换

2.png

注2:上图缩放变换的基点,就是图形的中心点。

let scale2 = 1;
let dir2 = 1;
function draw2() {
  if (scale2 > 2 || scale2 < 1) {
    dir2 *= -1;
  }
  scale2 += 0.004 * dir2;
  if (!canvasRef2.value) return;
  const ctx = canvasRef2.value.getContext('2d') as CanvasRenderingContext2D;
  ctx.clearRect(-300, -300, 600, 600);
  ctx.resetTransform();
  drawGrid(ctx);
  const translateMatrix = new Matrix3().makeTranslation(150, 100);
  const translateMatrixOrigin = translateMatrix.clone();
  const scaleMatrix = new Matrix3().makeScale(scale2, 1.2 * scale2);
  const posToCenterMatrix = new Matrix3().makeTranslation(-25, -25);
  const posToCenterMatrixInvert = new Matrix3().makeTranslation(25, 25);

  ctx.save();
  ctx.transform(
    translateMatrixOrigin.elements[0],
    translateMatrixOrigin.elements[1],
    translateMatrixOrigin.elements[3],
    translateMatrixOrigin.elements[4],
    translateMatrixOrigin.elements[6],
    translateMatrixOrigin.elements[7]
  )

  ctx.fillStyle = 'rgba(255,255,0,1)';
  ctx.fillRect(0, 0, 50, 50);
  ctx.restore();

  const matrix = new Matrix3()
    .multiply(translateMatrix)
    .multiply(posToCenterMatrixInvert)
    .multiply(scaleMatrix)
    .multiply(posToCenterMatrix);
  ctx.transform(
    matrix.elements[0],
    matrix.elements[1],
    matrix.elements[3],
    matrix.elements[4],
    matrix.elements[6],
    matrix.elements[7]
  );
  ctx.fillStyle = 'rgba(255,0,0,0.5)';
  ctx.fillRect(0, 0, 50, 50);
  requestAnimationFrame(draw2);
}

canvas 的变换机制我们无法改变(基于画布原点),但我们可以通过多个变换的组合,达到预期的效果。 举例来说:已知画布上有一个 100x100 的矩形(矩形的左上角点和画布的原点重合),现在想要缩放到 200x200,条件是不能改变矩形的中心位置。

现在停下来想一想,如果将100x100的矩形以中心缩放的形式缩放到200x200,那么现在矩形的位置在哪(即左上角点)? 显然,现在矩形的左上角点在画布上处于(-50,-50)的位置上

等等!我们只想要对矩形进行缩放,可是最终的效果是,矩形不仅进行了缩放操作,还进行了位移! 也就是说,对物体进行中心缩放操作,是由 缩放变换 + 位移变换共同作用的结果。

3.png

注3:上图中,我们在缩放开始之前,先进行平移变换。让物体的中心点和坐标原点重合后(此时平移50),再次乘上缩放矩阵,此时就是中心平移的效果

let deltaT = 0
let deltaDir = 1
function draw3() {
  if (!canvasRef3.value) return;
  if (deltaT > 50 || deltaT < 0) {
    deltaDir *= -1
  }
  deltaT += 0.3 * deltaDir


  const ctx = canvasRef3.value.getContext('2d') as CanvasRenderingContext2D;
  ctx.clearRect(-300, -300, 1000, 1000);
  ctx.resetTransform();
  drawGrid(ctx);
  // 这个矩阵是为了将画布中心移到(200,200),方便观察效果
  const translateMatrix = new Matrix3().makeTranslation(100, 100);
  const translateMatrixOrigin = translateMatrix.clone();
  ctx.save();
  ctx.transform(
    translateMatrixOrigin.elements[0],
    translateMatrixOrigin.elements[1],
    translateMatrixOrigin.elements[3],
    translateMatrixOrigin.elements[4],
    translateMatrixOrigin.elements[6],
    translateMatrixOrigin.elements[7]
  )

  ctx.fillStyle = 'rgba(135, 206, 235, .5)';
  ctx.fillRect(0, 0, 100, 100);
  ctx.restore();

  ctx.save()
  ctx.fillStyle = 'rgba(255, 255,0, .3)';
  const scaleMatrix = new Matrix3().makeScale(2, 2)
  const tranRectMatrix = new Matrix3().makeTranslation(-deltaT, -deltaT)
  const matrix = translateMatrixOrigin.clone().multiply(tranRectMatrix).multiply(scaleMatrix)
  ctx.transform(
    matrix.elements[0],
    matrix.elements[1],
    matrix.elements[3],
    matrix.elements[4],
    matrix.elements[6],
    matrix.elements[7])
  ctx.fillRect(0, 0, 100, 100);
  ctx.restore()

  requestAnimationFrame(draw3);
}

仔细观察我们的变换矩阵 matrix = mto * mt * ms ,除了最开始的translateMatrixOrigin 矩阵之外(将坐标原点200x200 方便观察),还有一个位移矩阵和缩放矩阵。

通过上一章,我们知道这个矩阵改如何解释:从左往右理解,先将 画布 位移-50*-50,之后再将 画布 扩大一倍。 从右往左理解,先将 画布中的物体 尺寸扩大一倍(此时canvas 画布的坐标系没有发生变化), 之后再将 画布中的物体 移动-50*-50

注意加粗的部分,从左往右,是变换的画布,物体没有变换。从右往左,变换的是物体,画布没变。

为了验证区分二者,我们再一次做个尝试,将上方的后两个矩阵的顺序对调下,想想会发生什么?

4.png

注4:现在的矩阵顺序matrix = mto * ms * mt

let deltaT2 = 0
let deltaDir2 = 1
function draw4() {
  if (!canvasRef4.value) return;
  if (deltaT2 > 50 || deltaT2 < 0) {
    deltaDir2 *= -1
  }
  deltaT2 += 0.3 * deltaDir2


  const ctx = canvasRef4.value.getContext('2d') as CanvasRenderingContext2D;
  ctx.clearRect(-300, -300, 1000, 1000);
  ctx.resetTransform();
  drawGrid(ctx);
  // 这个矩阵是为了将画布中心移到(200,200),方便观察效果
  const translateMatrix = new Matrix3().makeTranslation(100, 100);
  const translateMatrixOrigin = translateMatrix.clone();
  ctx.save();
  ctx.transform(
    translateMatrixOrigin.elements[0],
    translateMatrixOrigin.elements[1],
    translateMatrixOrigin.elements[3],
    translateMatrixOrigin.elements[4],
    translateMatrixOrigin.elements[6],
    translateMatrixOrigin.elements[7]
  )

  ctx.fillStyle = 'rgba(135, 206, 235, .5)';
  ctx.fillRect(0, 0, 100, 100);
  ctx.restore();

  ctx.save()
  ctx.fillStyle = 'rgba(255, 255,0, .3)';
  const scaleMatrix = new Matrix3().makeScale(2, 2)
  const tranRectMatrix = new Matrix3().makeTranslation(-deltaT2, -deltaT2)
  const matrix = translateMatrixOrigin.clone().multiply(scaleMatrix).multiply(tranRectMatrix)
  ctx.transform(
    matrix.elements[0],
    matrix.elements[1],
    matrix.elements[3],
    matrix.elements[4],
    matrix.elements[6],
    matrix.elements[7])
  ctx.fillRect(0, 0, 100, 100);
  ctx.restore()

  requestAnimationFrame(draw4);
}

此时物体的大小是我们所期望的,但并不是中心缩放!而是右下角缩放! 我们可以想想发生了什么,从左往右:画布先扩大了一倍,然后画布移动-50 * -50,从右往左,物体移动-50 * -50,物体扩大一倍。 咦,看上去好像没什么变换。no no no 让我们继续分析 (如果你读过上一篇文章,相信你已近清楚了!)。 第一种解释中,我们认为变换的是画布,先扩大一倍注意:此时的扩大,是在画布没有缩放的情况下,之后画布再位移 -50 *- 50,!此时的位移已近是扩大后的画布了! 我们上一步操作,将画布的基向量扩大了一倍,所以在移动同样的 50单位时,扩大后的移动距离是扩大前的两倍! 再来看第二种解释,物体先位移50单位,此时的坐标原点没有改变,只是物体的坐标发生变换。之后再将物体扩大一倍,注意此时的缩放中心 依然在画布的原点上,并且现在的矩形中心就在原点上,就相当于以物体的右下角点为基点,将物体扩大了一倍。

让我们再来想一想,如果我就想要当前的顺序,还要达到物体中心缩放的效果该怎么办呢? 按照第一种解释,矩形不在中心的原因,是画布的基向量已近扩大了一倍,所以我们在位移的时候只能移动预想的一半,也就是 25 * 25。 第二种解释,矩形不在中心的原因,是物体先位移了50 单位,导致物体的中心点处于画布原点,所以我们只能移动25单位! 芜湖,逻辑闭环!

5.png

注5:现在的效果和注3一样了,但矩阵的顺序却发生了变化

let deltaT3 = 0
let deltaDir3 = 1
function draw5() {
  if (!canvasRef5.value) return;
  if (deltaT3 > 50 || deltaT3 < 0) {
    deltaDir3 *= -1
  }
  deltaT3 += 0.4 * deltaDir3


  const ctx = canvasRef5.value.getContext('2d') as CanvasRenderingContext2D;
  ctx.clearRect(-300, -300, 1000, 1000);
  ctx.resetTransform();
  drawGrid(ctx);
  // 这个矩阵是为了将画布中心移到(200,200),方便观察效果
  const translateMatrix = new Matrix3().makeTranslation(100, 100);
  const translateMatrixOrigin = translateMatrix.clone();
  ctx.save();
  ctx.transform(
    translateMatrixOrigin.elements[0],
    translateMatrixOrigin.elements[1],
    translateMatrixOrigin.elements[3],
    translateMatrixOrigin.elements[4],
    translateMatrixOrigin.elements[6],
    translateMatrixOrigin.elements[7]
  )

  ctx.fillStyle = 'rgba(135, 206, 235, .5)';
  ctx.fillRect(0, 0, 100, 100);
  ctx.restore();

  ctx.save()
  ctx.fillStyle = 'rgba(255, 255,0, .3)';
  const scaleMatrix = new Matrix3().makeScale(2, 2)
  const tranRectMatrix = new Matrix3().makeTranslation(-deltaT3 / 2, -deltaT3 / 2)
  const matrix = translateMatrixOrigin.clone().multiply(scaleMatrix).multiply(tranRectMatrix)
  ctx.transform(
    matrix.elements[0],
    matrix.elements[1],
    matrix.elements[3],
    matrix.elements[4],
    matrix.elements[6],
    matrix.elements[7])
  ctx.fillRect(0, 0, 100, 100);
  ctx.restore()

  requestAnimationFrame(draw5);
}

现在只剩下最后一个问题,我们上面讨论的,全都是在已经知道变换的结果,去反推矩阵变换信息。可我们本篇讨论的是基点变换,基点呢? 针对基点变换,笔者采用的是从右往左的理解方式,即变换的是物体,画布没有变化。 当我们想要以物体上的任意点作为变换的原点时,只需要先将物体移动到画布的原点,再进行变换,变换完成后,再将物体移动回原来的位置即可。 注意:此时我们变换的是物体,所以变换前后的位移距离不会由于变换本身而改变!

6.png

注6:三个矩阵拆开执行的动画效果

let resT = 0
let resScale = 1
let resTInvert = 0
function draw7() {
  if (!canvasRef6.value) return;
  if (resT < 50) {
    resT += 0.2
  }


  const ctx = canvasRef6.value.getContext('2d') as CanvasRenderingContext2D;
  ctx.clearRect(-300, -300, 1000, 1000);
  ctx.resetTransform();
  drawGrid(ctx);
  // 这个矩阵是为了将画布中心移到(200,200),方便观察效果
  const translateMatrix = new Matrix3().makeTranslation(100, 100);
  const translateMatrixOrigin = translateMatrix.clone();
  ctx.save();
  ctx.transform(
    translateMatrixOrigin.elements[0],
    translateMatrixOrigin.elements[1],
    translateMatrixOrigin.elements[3],
    translateMatrixOrigin.elements[4],
    translateMatrixOrigin.elements[6],
    translateMatrixOrigin.elements[7]
  )

  ctx.fillStyle = 'rgba(135, 206, 235, .5)';
  ctx.fillRect(0, 0, 100, 100);
  ctx.restore();
  ctx.fillStyle = 'rgba(255, 0, 0, .2)';

  // 对齐基点
  function step1() {
    const tranRectMatrix = new Matrix3().makeTranslation(-resT, -resT)
    const matrix = translateMatrixOrigin.clone().multiply(tranRectMatrix)
    ctx.save()
    ctx.transform(
      matrix.elements[0],
      matrix.elements[1],
      matrix.elements[3],
      matrix.elements[4],
      matrix.elements[6],
      matrix.elements[7])
    ctx.fillRect(0, 0, 100, 100);
    ctx.restore()
    if (resT >= 50) {
      step2(tranRectMatrix)
    }
  }

  // 基于当前基点缩放物体
  function step2(mt: Matrix3) {
    if (resScale < 2) {
      resScale += 0.01
    }
    const scaleMatrix = new Matrix3().makeScale(resScale, resScale)
    const matrix = translateMatrixOrigin.clone().multiply(scaleMatrix).multiply(mt)
    ctx.save()
    ctx.fillStyle = 'rgba(255, 255,0, .2)';
    ctx.transform(
      matrix.elements[0],
      matrix.elements[1],
      matrix.elements[3],
      matrix.elements[4],
      matrix.elements[6],
      matrix.elements[7])
    ctx.fillRect(0, 0, 100, 100);
    ctx.restore()
    if (resScale >= 2) {
      step3(mt, scaleMatrix)
    }
  }

  // 将物体移回去
  function step3(mt: Matrix3, ms: Matrix3) {
    if (resTInvert < 50) {
      resTInvert += 0.2
    } else {
      resT = 0
      resScale = 1
      resTInvert = 0
    }
    const mtInvert = new Matrix3().makeTranslation(resTInvert, resTInvert)
    const matrix = translateMatrixOrigin.clone().multiply(mtInvert).multiply(ms).multiply(mt)
    ctx.save()
    ctx.fillStyle = 'rgba(255, 100,100, .3)';
    ctx.transform(
      matrix.elements[0],
      matrix.elements[1],
      matrix.elements[3],
      matrix.elements[4],
      matrix.elements[6],
      matrix.elements[7])
    ctx.fillRect(0, 0, 100, 100);
    ctx.restore()
  }
  step1()

  requestAnimationFrame(draw7)
}