Ответ
Ваша функция
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 не следует использовать, и что 99% всех потребностей преобразования могут быть выполнены быстрее и с меньшим вниманием к ctx.setTransform
, чем плохо названный ctx.translate
, ctx.scale
или ctx.rotate
Читайте, если интересно.
Понимание 2D-преобразования
При рендеринге на холст все координаты преобразуются с помощью матрицы преобразования.
Матрица состоит из 6 значений, установленных setTransform(a,b,c,d,e,f)
. Значения a,b,c,d,e,f
довольно неясны, и литература не помогает их объяснить.
Лучший способ думать о них - это то, что они делают. Я переименую их как setTransform(xAxisX, xAxisY, yAxisX, yAxisY, originX, originY)
, они представляют направление и размер оси x, оси y и начала координат.
xAxisX
, xAxisY
- ось 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, oy на 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 остается на PI / 2
(90 градусов) по часовой стрелке от оси X, а масштаб оси Y такой же, как шкала оси X.
Вы можете повернуть вектор на 90 градусов, поменяв местами x и y и отрицая новый x
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
Мы можем воспользоваться этим простым поворотом на 90, чтобы создать равномерное вращение, только определив угол оси X
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
, используя вышеизложенное, и позволить оборудованию графического процессора выполнить преобразование
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 имеет неправильное наименование, что является причиной 90% вопросов о преобразовании, которые появляются в теге html5-canvas .
Если мы переименуем вызовы API, вы лучше поймете, что они делают
ctx.translate(x, y); // should be ctx.multiplyCurrentMatirxWithTranslateMatrix
// or shorten ctx.matrixMutliplyTranslate(x, y)
Функция ctx.translate
фактически не переводит точку, а скорее переводит текущую матрицу. Для этого сначала создается матрица, а затем умножается эта матрица на текущую матрицу
Умножение одной матрицы на другую означает, что 6 значений или 3 вектора для оси X, оси Y и источника преобразуются другой матрицей.
Если написано как код
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 изменения состояния графического процессора, 81 умножение с плавающей запятой, 54 сложения с плавающей запятой, 4 высокоуровневых математических вызова и около 0,25 КБ ОЗУ, выделенных и выгруженных для очистки ГХ.
Легко и быстро
Функция setTransform
не умножает матрицы. Он преобразует 6 аргументов в матрицу 3 на 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
), одного изменения состояния графического процессора и используете только 64 байта оперативной памяти из кучи .
И поскольку ctx.setTransform
не зависит от текущего состояния 2D-преобразования, вам не нужно использовать ctx.resetTransform
или ctx.save
и restore
При анимации многих элементов выигрыш в производительности заметен. Когда вы боретесь со сложностью преобразованных матриц, простота setTransform
может сэкономить вам часы времени, потраченные на создание хорошего контента.