Html5 canvas - функция перевода ведет себя странно - PullRequest
0 голосов
/ 05 января 2019

Я пытаюсь использовать функцию перевода при рисовании круга, но когда я пытаюсь сделать это, он не ведет себя должным образом. Вместо того, чтобы нарисовать круг, он рисует это:

screenshot

если изображение не отображается: нажмите здесь

Это мой код для рисования круга (внутри класса круга):

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)
}

Кстати: когда я не использую функцию перевода, он обычно рисует круг.

Edit:

Я ответил на свой вопрос ниже, так как обнаружил, что в javascript перевод работает немного иначе, чем я думал.

Ответы [ 4 ]

0 голосов
/ 06 января 2019

Ответ

Ваша функция

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 может сэкономить вам часы времени, потраченные на создание хорошего контента.

0 голосов
/ 05 января 2019

Проблема в том, что после каждого перевода в 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();

и полностью пропустите шаг перевода.

0 голосов
/ 05 января 2019

В вашем коде 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()

Этот способ хорош в случаях, когда вы хотите вернуть контекст в исходное состояние после операции, а не в состояние по умолчанию (которое вы обычно делаете при выполнении более сложных операций), и когда вы делаете несколько преобразований, которые бы сложный для возврата.

0 голосов
/ 05 января 2019

Я только что нашел ответ. Как заметил @mpen ctx.translate(0, 0) не сбрасывает перевод, но это так: ctx.setTransform(1, 0, 0, 1, 0, 0);. Функция ctx.translate выполняет перевод, связанный с предыдущим переводом.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...