HTML5 canvas-奇怪地翻译功能

时间:2019-01-05 00:50:11

标签: javascript html5-canvas

我试图在画圆时使用平移功能,但是当我尝试这样做时,它无法正常工作。与其绘制圆,不如画圆:

screenshot

如果图像未显示:click here

这是我绘制圆的代码(在圆类内):

ctx.strokeStyle = "white"
ctx.translate(this.x, this.y)
ctx.beginPath()
// Draws the circle
ctx.arc(0, 0, this.r, 0, 2 * Math.PI)
ctx.stroke()
ctx.closePath()
// tried with and without translating back, inside and outside of this function
ctx.translate(0, 0)

这是我其余的代码:

let canvas
let ctx
let circle

function init() {
    canvas = document.querySelector("#canvas")
    ctx = canvas.getContext("2d")

                               // x, y, radius
    circle = new Circle(canvas.width/5, canvas.height/2, 175)

    requestAnimationFrame(loop)
}

function loop() {
    // Background
    ctx.fillStyle = "black"
    ctx.fillRect(0, 0, canvas.width, canvas.height)
    // The function with the drawing of the circle
    circle.draw()
    requestAnimationFrame(loop)
}

顺便说一句:当我不使用平移功能时,它将正常绘制圆。

编辑:

我在下面回答了我自己的问题,因为我发现javascript的翻译功能与我认为的有所不同。

4 个答案:

答案 0 :(得分:1)

问题在于,Circle.draw()中的每个翻译之后,上下文都没有恢复到其原始状态。未来的translate(this.x, this.y);调用将使上下文相对于上一个转换不断地向右和向下移动。

ctx.save()函数的开头和结尾处使用ctx.restore()draw(),以便在绘制后将上下文移回其原始位置。

class Circle {
  constructor(x, y, r) {
    this.x = x;
    this.y = y;
    this.r = r;
  }

  draw() {
    ctx.save();
    
    ctx.strokeStyle = "white";
    ctx.translate(this.x, this.y);
    ctx.beginPath();
    ctx.arc(0, 0, this.r, 0, 2 * Math.PI);
    ctx.closePath();
    ctx.stroke();
    
    ctx.restore();
  }
}

let canvas;
let ctx;
let circle;

(function init() {
  canvas = document.querySelector("canvas");
  canvas.width = innerWidth;
  canvas.height = innerHeight;
  ctx = canvas.getContext("2d");
  circle = new Circle(canvas.width / 2, canvas.height / 2, 30);
  loop();
})();

function loop() {
  ctx.fillStyle = "black";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  circle.draw();
  requestAnimationFrame(loop);
}
body {
  margin: 0;
  height: 100vh;
}
<canvas></canvas>

或者,您可以只写:

ctx.strokeStyle = "white";
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();

并完全跳过翻译步骤。

答案 1 :(得分:0)

我刚找到答案。正如@mpen所说,ctx.translate(0, 0)不会重置翻译,但是会重置:ctx.setTransform(1, 0, 0, 1, 0, 0);。 ctx.translate函数的翻译与先前的翻译有关。

答案 2 :(得分:0)

在您的代码中,ctx.translate(0, 0)绝对不执行任何操作,因为该函数设置相对于当前转换的转换。您告诉上下文“向右移动0像素,向下移动0像素”。您可以通过将行更改为ctx.translate(-this.x, -this.y)来解决此问题,以便执行相反的转换。

但是,通常,这是通过在进行转换之前用CanvasRenderingContext2D.save保存上下文状态,然后使用CanvasRenderingContext2D.restore恢复上下文状态来完成的。在您的示例中,它看起来像这样:

ctx.save();  // here, we are saving state of the context
ctx.strokeStyle = "white";
ctx.translate(this.x, this.y);
ctx.beginPath();
// Draws the circle
ctx.arc(0, 0, this.r, 0, 2 * Math.PI);
ctx.stroke();
ctx.closePath();
ctx.restore();  // after this, context will have the state it had when we called save()

当您希望在操作后将上下文返回到其原始状态而不是默认状态(在执行更复杂的操作时通常会执行此操作)时,并且当您执行多个转换时,这种方法非常有用恢复很复杂。

答案 3 :(得分:0)

答案

您的功能

ctx.strokeStyle = "white"
ctx.translate(this.x, this.y)
ctx.beginPath()
// Draws the circle
ctx.arc(0, 0, this.r, 0, 2 * Math.PI)
ctx.stroke()
ctx.closePath()
// tried with and without translating back, inside and outside of this function
ctx.translate(0, 0)

可以进行如下改进

ctx.strokeStyle = "white"
ctx.setTransform(1, 0, 0, 1, this.x, this.y); //BM67 This call is faster than ctx.translate
ctx.beginPath()
ctx.arc(0, 0, this.r, 0, 2 * Math.PI)
ctx.stroke()
// ctx.closePath() //BM67 This line does nothing and is not related to beginPath.

// tried with and without translating back, inside and outside of this function

//ctx.translate(0, 0) //BM67 You don't need to reset the transform
                      //     The call to ctx.setTransfrom replaces
                      //     the current transform before you draw the circle

看起来像

ctx.strokeStyle = "white"
ctx.setTransform(1, 0, 0, 1, this.x, this.y);
ctx.beginPath()
ctx.arc(0, 0, this.r, 0, 2 * Math.PI)
ctx.stroke()

为什么这样更好,将需要您了解2D转换的工作原理以及为什么不应该使用某些2D API调用,并且使用ctx.setTransform可以更快,更省力地完成所有转换需求的99%比名称不正确的ctx.translatectx.scalectx.rotate

请继续阅读

了解2D变换

渲染到画布上时,所有坐标都通过转换矩阵进行转换。

矩阵由setTransform(a,b,c,d,e,f)设置的6个值组成。值a,b,c,d,e,f相当模糊,文献资料也无法解释它们。

想到他们的最好方法是根据他们的所作所为。我将它们重命名为setTransform(xAxisX, xAxisY, yAxisX, yAxisY, originX, originY),它们代表x轴,y轴和原点的方向和大小。

  • xAxisXxAxisY是X轴X,X轴Y
  • yAxisXyAxisY是Y轴X,Y轴Y
  • originXoriginY是原点的画布实际像素坐标

默认变换为setTransform(1, 0, 0, 1, 0, 0),这意味着X轴从1向下移动0,Y轴从0向下移动1,并且原点位于0,0

您可以按如下所示将变换手动应用于2D点

function transformPoint(x, y) {
    return {
       // Move x dist along X part of X Axis
       // Move y dist along X part of Y Axis
       // Move to the X origin
        x : x * xAxisX + y * yAxisX + originX,   

       // Move x dist along Y part of X Axis
       // Move y dist along Y part of Y Axis
       // Move to the Y origin
        y : x * xAxisY + y * yAxisY + originY,   
     };
 }

如果我们替换默认矩阵setTransform(1, 0, 0, 1, 0, 0),则会得到

 {
     x : x * 1 + y * 0 + 0,   
     y : x * 0 + y * 1 + 0,   
 }

 // 0 * n is 0 so removing the * 0
 {
     x : x * 1,   
     y : y * 1,   
 }
 // 1 time n is n so remove the * 1                                     
 {
     x : x,
     y : y,
 }

如您所见,默认转换对这一点无济于事

翻译

如果我们将转换ox设置为setTransform(1, 0, 0, 1, 100, 200),则转换为

 {
     x : x * 1 + y * 0 + 100,   
     y : x * 0 + y * 1 + 200,   
 }
 // or simplified as
 {
     x : x + 100,   
     y : y + 200,   
 }

规模

如果我们将X轴和Y轴的比例设置为setTransform(2, 0, 0, 2, 100, 200),则变换为

 {
     x : x * 2 + y * 0 + 100,   
     y : x * 0 + y * 2 + 200,   
 }
 // or simplified as
 {
     x : x * 2 + 100,   
     y : y * 2 + 200,   
 }

旋转

旋转有点复杂,需要一些触发。您可以使用cos和sin来获取方向角的单位矢量(注意,所有角度均以弧度表示,PI * 2为360度,PI为180度,PI / 2为90度)

因此0弧度的单位向量为

 xAxisX = Math.cos(0);
 yAxisY = Math.sin(0);

因此对于角度0PI * (1 / 2)PIPI * (3 / 2)PI * 2

 angle = 0; 
 xAxisX = Math.cos(angle); // 1
 yAxisY = Math.sin(angle); // 0

 angle = Math.PI * (1 / 2);  // 90deg (points down screen) 
 xAxisX = Math.cos(angle); // 0
 yAxisY = Math.sin(angle); // 1

 angle = Math.PI;  // 180deg (points to left screen) 
 xAxisX = Math.cos(angle); // -1
 yAxisY = Math.sin(angle); // 0

 angle = Math.PI * (3 / 2);  // 270deg (points to up screen) 
 xAxisX = Math.cos(angle); // 0
 yAxisY = Math.sin(angle); // -1

统一变换

在90%的情况下,当变换点时,您希望这些点保持正方形,即Y轴沿X轴顺时针保持在PI / 2(90度),并且Y轴的比例相同作为X轴的比例。

您可以通过交换x和y并取反新的x来将向量旋转90度

 x = 1;  // X axis points from left to right
 y = 0;  // No downward part
 // Rotate 90deg clockwise
 x90 = -y;  // 0 no horizontal part
 y90 = x;   // Points down the screen

仅通过定义X轴的角度,我们就可以利用这种简单的90度旋转来创建均匀的旋转

 xAxisX = Math.cos(angle);
 xAxisY = Math.sin(angle);
 // create a matrix as setTransform(xAxisX, xAxisY, -xAxisY, xAxisX, 0, 0)

 // to transform the point
 {
     x : x * xAxisX + y * (-xAxisY) + 0,   
     y : x * xAxisY + y *   xAxisX  + 0,   
 }
 // to simplify
 {
     x : x * xAxisX - y * xAxisY,   
     y : x * xAxisY + y * xAxisX,   
 }

旋转,缩放和翻译

使用上述信息,您现在可以仅使用4个值(原点xyscalerotate

 function transformPoint(x, y, originX, originY, scale, rotate) {
      // get the direction of the X Axis
      var xAxisX = Math.cos(rotate);
      var xAxisY = Math.sin(rotate);

      // Scale the x Axis
      xAxisX *= Math.cos(rotate);
      xAxisY *= Math.sin(rotate);

      // Get the Y Axis as X Axis rotated 90 deg
      const yAxisX = -xAxisY;
      const yAxisY = xAxisX;

      // we have the 6 values for the transform 
      // [xAxisX, xAxisY, yAxisX, yAxisY, originX, originY]

      // Transform the point
      return {
          x : x * xAxisX + y * yAxisX + originX,
          y : x * xAxisY + y * yAxisY + originY,
      }
  }
  // we can simplify the above down to 
 function transformPoint(x, y, originX, originY, scale, rotate) {
      // get the direction and scale of the X Axis
      const xAxisX = Math.cos(rotate) * scale;
      const xAxisY = Math.sin(rotate) * scale;

      // Transform the point
      return {
          x : x * xAxisX - y * xAxisY + originX,
          // note the    ^ negative
          y : x * xAxisY + y * xAxisX + originY,
      }
  }

或者我们可以使用以上的ctx.setTransform创建矩阵,然后让GPU硬件进行转换

 function createTransform(originX, originY, scale, rotate) {
      const xAxisX = Math.cos(rotate) * scale;
      const xAxisY = Math.sin(rotate) * scale;
      ctx.setTransform(xAxisX, xAxisY, -xAxisY, xAxisX, originX, originY);
 }

设置或乘以变换。

我将本部分重命名为

为什么要避免使用ctx.translatectx.scalectx.rotate

2D API的命名有误,这是html5-canvas标签中出现90%转换问题的原因。

如果我们重命名API调用,您将更好地了解它们的作用

ctx.translate(x, y); // should be ctx.multiplyCurrentMatirxWithTranslateMatrix
                     // or shorten ctx.matrixMutliplyTranslate(x, y)

函数ctx.translate实际上并不转换点,而是转换当前矩阵。首先创建一个矩阵,然后将该矩阵与当前矩阵相乘

将一个矩阵与另一个矩阵相乘,意味着X轴,Y轴和原点的6个值或3个向量由另一个矩阵变换。

如果写为代码

const current = [1,0,0,1,0,0]; // Default matrix
function translate(x, y) {  // Translate current matrix
    const translationMatrix = [1,0,0,1,x,y];
    const c = current
    const m = translationMatrix 
    const r = []; // the resulting matrix

    r[0] = c[0] * m[0] + c[1] * m[2]; // rotate current X Axis with new transform
    r[1] = c[0] * m[1] + c[1] * m[3];
    r[2] = c[2] * m[0] + c[3] * m[2]; // rotate current Y Axis with new transform
    r[3] = c[2] * m[1] + c[3] * m[3];
    r[4] = c[4] + m[4]; // Translate current origine with transform
    r[5] = c[5] + m[5];

    c.length = 0;
    c.push(...r);
}

那是简单的版本。在引擎盖下,您不能将两个矩阵相乘,因为它们具有不同的尺寸。实际矩阵存储为9个值,需要27个乘法和18个加法

  // The real 2D default matrix
  const current = [1,0,0,0,1,0,0,0,1];
  // The real Translation matrix
  const translation = [1,0,0,0,1,0,x,y,1];

  //The actual transformation calculation

  const c = current
  const m = translationMatrix 
  const r = []; // the resulting matrix

  r[0] = c[0] * m[0] + c[1] * m[3] + c[2] * m[6]; 
  r[1] = c[0] * m[1] + c[1] * m[4] + c[2] * m[7];
  r[2] = c[0] * m[2] + c[1] * m[5] + c[2] * m[8];
  r[3] = c[3] * m[0] + c[4] * m[3] + c[5] * m[6]; 
  r[4] = c[3] * m[1] + c[4] * m[4] + c[5] * m[7];
  r[5] = c[3] * m[2] + c[4] * m[5] + c[5] * m[8];
  r[6] = c[6] * m[0] + c[7] * m[3] + c[8] * m[6]; 
  r[7] = c[6] * m[1] + c[7] * m[4] + c[8] * m[7];
  r[8] = c[6] * m[2] + c[7] * m[5] + c[8] * m[8];

这是一堆数学运算,当您使用ctx.translate时,总是在幕后完成,并且请注意,此数学运算未在GPU上完成,而是在CPU上完成,并将所得矩阵移至GPU。

如果我们继续重命名

ctx.translate(x, y);       // should be ctx.matrixMutliplyTranslate(
ctx.scale(scaleY, scaleX); // should be ctx.matrixMutliplyScale(
ctx.rotate(angle);         // should be ctx.matrixMutliplyRotate(
ctx.transform(a,b,c,d,e,f) // should be ctx.matrixMutliplyTransform(

对于JS脚本来说,通常使用上述功能来缩放平移和旋转,通常是反向旋转和平移,因为它们的对象不在本地原点周围定义。

因此,当您执行以下操作

ctx.rotate(angle);
ctx.scale(sx, sy);
ctx.translate(x, y);

幕后数学必须完成以下所有操作

  // create rotation matrix
  rr = [Math.cos(rot), Math.sin(rot), 0, -Math.sin(rot), Math.cos(rot), 0, 0, 0, 1];
  // Transform the current matix with the rotation matrix
  r[0] = c[0] * rr[0] + c[1] * rr[3] + c[2] * rr[6]; 
  r[1] = c[0] * rr[1] + c[1] * rr[4] + c[2] * rr[7];
  r[2] = c[0] * rr[2] + c[1] * rr[5] + c[2] * rr[8];
  r[3] = c[3] * rr[0] + c[4] * rr[3] + c[5] * rr[6]; 
  r[4] = c[3] * rr[1] + c[4] * rr[4] + c[5] * rr[7];
  r[5] = c[3] * rr[2] + c[4] * rr[5] + c[5] * rr[8];
  r[6] = c[6] * rr[0] + c[7] * rr[3] + c[8] * rr[6]; 
  r[7] = c[6] * rr[1] + c[7] * rr[4] + c[8] * rr[7];
  r[8] = c[6] * rr[2] + c[7] * rr[5] + c[8] * rr[8];

  // STOP the GPU and send the resulting matrix over the bus to set new state
  c = [...r]; // set the current matrix

  // create the scale matrix
  ss = [scaleX, 0, 0, 0, scaleY, 0, 0, 0, 1];
  // scale the current matrix      
  r[0] = c[0] * ss[0] + c[1] * ss[3] + c[2] * ss[6]; 
  r[1] = c[0] * ss[1] + c[1] * ss[4] + c[2] * ss[7];
  r[2] = c[0] * ss[2] + c[1] * ss[5] + c[2] * ss[8];
  r[3] = c[3] * ss[0] + c[4] * ss[3] + c[5] * ss[6]; 
  r[4] = c[3] * ss[1] + c[4] * ss[4] + c[5] * ss[7];
  r[5] = c[3] * ss[2] + c[4] * ss[5] + c[5] * ss[8];
  r[6] = c[6] * ss[0] + c[7] * ss[3] + c[8] * ss[6]; 
  r[7] = c[6] * ss[1] + c[7] * ss[4] + c[8] * ss[7];
  r[8] = c[6] * ss[2] + c[7] * ss[5] + c[8] * ss[8];

  // STOP the GPU and send the resulting matrix over the bus to set new state
  c = [...r]; // set the current matrix

 // create the translate matrix
  tt = [1, 0, 0, 0, 1, 0, x, y, 1];

  // translate the current matrix      
  r[0] = c[0] * tt[0] + c[1] * tt[3] + c[2] * tt[6]; 
  r[1] = c[0] * tt[1] + c[1] * tt[4] + c[2] * tt[7];
  r[2] = c[0] * tt[2] + c[1] * tt[5] + c[2] * tt[8];
  r[3] = c[3] * tt[0] + c[4] * tt[3] + c[5] * tt[6]; 
  r[4] = c[3] * tt[1] + c[4] * tt[4] + c[5] * tt[7];
  r[5] = c[3] * tt[2] + c[4] * tt[5] + c[5] * tt[8];
  r[6] = c[6] * tt[0] + c[7] * tt[3] + c[8] * tt[6]; 
  r[7] = c[6] * tt[1] + c[7] * tt[4] + c[8] * tt[7];
  r[8] = c[6] * tt[2] + c[7] * tt[5] + c[8] * tt[8];

  // STOP the GPU and send the resulting matrix over the bus to set new state
  c = [...r]; // set the current matrix

总共有3个GPU状态更改,81个浮点乘法,54个浮点加法,4个高级数学调用以及大约0.25K RAM分配并转储以供GC清理。

方便快捷

函数setTransform不将矩阵相乘。通过将值直接放入当前变换并将其移至GPU,它将6个自变量转换为3 x 3矩阵。

  // ct is the current transform 9 value under hood version
  // The 6 arguments of the ctx.setTransform call

  ct[0] = a;
  ct[1] = b;
  ct[2] = 0;
  ct[3] = c;
  ct[4] = d;
  ct[5] = 0;
  ct[6] = e;
  ct[7] = f;
  ct[8] = 1;
  // STOP the GPU and send the resulting matrix over the bus to set new state

因此,如果您使用JS函数

 function createTransform(originX, originY, scale, rotate) {
      const xAxisX = Math.cos(rotate) * scale;
      const xAxisY = Math.sin(rotate) * scale;
      ctx.setTransform(xAxisX, xAxisY, -xAxisY, xAxisX, originX, originY);
 }

您可以将复杂度降低为2个浮点乘法,2个高级数学函数调用,1个浮点加法(取反-xAxisY),1个GPU状态更改以及仅使用64个字节的RAM堆。

并且由于ctx.setTransform不依赖于2D变换的当前状态,因此您不需要使用ctx.resetTransformctx.saverestore

在对许多项目进行动画处理时,性能优势非常明显。在面对转换矩阵的复杂性时,setTransform的简单性可以为您节省数小时的时间,使您可以更好地花费在创建优质内容上。