¿Cómo calcular el desplazamiento para dibujar al píxel más cercano en un lienzo con la transformación de escala aplicada?

CorePress2024-01-16  11

Estoy intentando dibujar un rectángulo en un lienzo HTML con un posicionamiento perfecto en píxeles, independientemente de la traducción y la escala aplicadas a la transformación del lienzo (supongamos que no se utiliza la rotación en este escenario).

En este caso, siempre quiero que el borde del rectángulo tenga 1 px de ancho en la pantalla, independientemente de qué tan ampliado esté el rectángulo con la escala aplicada (que tengo trabajando en la demostración a continuación), sin embargo, también quiero se dibuja en coordenadas exactas de píxeles para que no se aplique antialiasing.

En el código siguiente, estoy bastante seguro de que tengo que manipular los argumentos de ctx.rect para agregar compensaciones a cada posición con el fin de redondearla para que se muestre exactamente en el píxel más cercano en la pantalla. ¿Pero no estoy seguro de qué matemáticas usar para llegar ahí?

Como puedes ver en esta demostración, con 1.5 escala aplicada, el rectángulo ya no se dibuja en coordenadas perfectas de píxeles.

const canvases = [
  {
    ctx: document.getElementById('canvasOriginal').getContext('2d'),
    scale: 1,
    translateX: 0,
    translateY: 0
  },
  {
    ctx: document.getElementById('canvasZoomed').getContext('2d'),
    scale: 1.5,
    translateX: -0.5,
    translateY: -0.5
  }
];

for (const { ctx, scale, translateX, translateY } of canvases) {
  ctx.translate(translateX, translateY);
  ctx.scale(scale, scale);
  ctx.beginPath();
  ctx.rect(1.5, 1.5, 4, 4);
  ctx.lineWidth = 1 / scale;
  ctx.strokeStyle = 'red';
  ctx.stroke();
}
canvas {
  border: 1px solid #ccc;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  width: 100px;
  height: 100px;
}
<canvas id="canvasOriginal" width="10" height="10"></canvas>
<canvas id="canvasZoomed" width="10" height="10"></canvas>

Este es el resultado que deseo de la imagen escalada en el fragmento de arriba:

EDITAR: No ignore la traducción.

¿Esto responde a tu pregunta? El lienzo HTML5 evita el escalado del ancho de línea

-Rojo

19/03/2021 a las 16:07

¡Quizás! Jugaré con él.

- usuario2867288

19/03/2021 a las 16:12

No, es un truco interesante, pero produce exactamente el mismo resultado que la demostración que proporcioné.

- usuario2867288

19/03/2021 a las 16:17



------------------------------------

La respuesta dada tiene muchos aspectos generales, como obtener y crear una matriz DOM, crear un punto DOM, transformar el punto y luego invertirlo.la matriz y la transformación de nuevo son literalmente más de 100 multiplicaciones y sumas (ignorando la sobrecarga de administración de memoria).

Como solo estás escalando y sin traslación ni rotación, se puede hacer en una división, ocho multiplicaciones y ocho sumas/restas, y ser al menos un orden de magnitud más rápido.

Ejemplo

const canvases = [
  { ctx: document.getElementById('canvasOriginal').getContext('2d'), scale: 1 },
  { ctx: document.getElementById('canvasZoomed').getContext('2d'), scale: 1.5 },
];
function pathRectPixelAligned(ctx, scale, x, y, w, h) {
    const invScale = 1 / scale;
    x = (Math.round(x * scale) + 0.5) * invScale ;   
    y = (Math.round(y * scale) + 0.5) * invScale ;  
    w = (Math.round((x + w) * scale) - 0.5) * invScale - x;   
    h = (Math.round((y + h) * scale) - 0.5) * invScale - y; 
   ctx.rect(x, y, w, h);
}
for (const { ctx, scale } of canvases) {
  ctx.scale(scale, scale);
  ctx.beginPath();
  pathRectPixelAligned(ctx, scale, 1, 1, 4, 4)
  ctx.lineWidth = 1;
  ctx.strokeStyle = 'red';
  ctx.setTransform(1,0,0,1,0,0);
  ctx.stroke();
}
canvas {
  border: 1px solid #ccc;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  width: 100px;
  height: 100px;
}
<canvas id="canvasOriginal" width="10" height="10"></canvas>
<canvas id="canvasZoomed" width="10" height="10"></canvas>

2

Gracias por el consejo sobre rendimiento, pero este ejemplo falla cuando aplico una traducción no entera a la matriz de transformación. Me doy cuenta de que estaba implícito abresalte esto en la pregunta ya que solo dije ignorar la rotación. jsfiddle.net/oe07xykq

- usuario2867288

21/03/2021 a las 15:16

En realidad estoy aplicando traslación, rotación y escala. Solo quiero que se vea perfecto en píxeles cuando la rotación sea múltiplo de 90 grados. Por eso dije ignorar la rotación, ya que el píxel perfecto no importa en ese escenario. Pero sí, nunca dije que ignoremos la traducción.

- usuario2867288

21 de marzo de 2021 a las 17:59



------------------------------------

Vale, yo... lo descubrí yo mismo. Puede utilizar la clase DOMPoint para transformar una coordenada a través de la matriz de transformación del lienzo. Así que escribí una función que transforma el punto a través de la matriz, lo redondea al medio píxel más cercano (ya que un trazo de 1 píxel de ancho se representa en el centro, por ejemplo, el medio punto de un píxel), luego lo transforma nuevamente a través de la inversa de la matriz.

Esto da como resultado que el trazo escalado de 1 px se represente al píxel más cercano en la pantalla.

Espero que esta pregunta sea útil para otras personas que navegan por Internet, ya que me tomó una eternidad descubrir este problema antes de publicar esta pregunta...

const canvases = [
  {
    ctx: document.getElementById('canvasOriginal').getContext('2d'),
    scale: 1,
    translateX: 0,
    translateY: 0
  },
  {
    ctx: document.getElementById('canvasZoomed').getContext('2d'),
    scale: 1.5,
    translateX: -0.5,
    translateY: -0.5
  }
];

const roundPointToHalfIdentityCoordinates = (ctx, x, y) => {
  let point = new DOMPoint(x, y);
  point = point.matrixTransform(ctx.getTransform());
  point.x = Math.round(point.x - 0.5) + 0.5;
  point.y = Math.round(point.y - 0.5) + 0.5;
  point = point.matrixTransform(ctx.getTransform().inverse());
  return point;
};

for (const { ctx, scale, translateX, translateY } of canvases) {
  ctx.translate(translateX, translateY);
  ctx.scale(scale, scale);
  ctx.beginPath();
  const topLeft = roundPointToHalfIdentityCoordinates(ctx, 1.5, 1.5);
  const bottomRight = roundPointToHalfIdentityCoordinates(ctx, 5.5, 5.5);
  ctx.rect(
    topLeft.x,
    topLeft.y,
    bottomRight.x - topLeft.x,
    bottomRight.y - topLeft.y
  );
  ctx.lineWidth = 1 / scale;
  ctx.strokeStyle = 'red';
  ctx.stroke();
}
canvas {
  border: 1px solid #ccc;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  width: 100px;
  height: 100px;
}
<canvas id="canvasOriginal" width="10" height="10"></canvas>
<canvas id="canvasZoomed" width="10" height="10"></canvas>

Su guía para un futuro mejor - libreflare
Su guía para un futuro mejor - libreflare