我试图在画圆时使用平移功能,但是当我尝试这样做时,它无法正常工作。与其绘制圆,不如画圆:
如果图像未显示: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的翻译功能与我认为的有所不同。
答案 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.translate
,ctx.scale
或ctx.rotate
请继续阅读
。渲染到画布上时,所有坐标都通过转换矩阵进行转换。
矩阵由setTransform(a,b,c,d,e,f)
设置的6个值组成。值a,b,c,d,e,f
相当模糊,文献资料也无法解释它们。
想到他们的最好方法是根据他们的所作所为。我将它们重命名为setTransform(xAxisX, xAxisY, yAxisX, yAxisY, originX, originY)
,它们代表x轴,y轴和原点的方向和大小。
xAxisX
,xAxisY
是X轴X,X轴Y yAxisX
,yAxisY
是Y轴X,Y轴Y originX
,originY
是原点的画布实际像素坐标默认变换为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);
因此对于角度0
,PI * (1 / 2)
,PI
,PI * (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个值(原点x
,y
,scale
和rotate
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.translate
,ctx.scale
或ctx.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.resetTransform
或ctx.save
和restore
在对许多项目进行动画处理时,性能优势非常明显。在面对转换矩阵的复杂性时,setTransform
的简单性可以为您节省数小时的时间,使您可以更好地花费在创建优质内容上。