我正玩着在html画布上作画的游戏,我对不同坐标系的实际工作原理感到有点困惑.到目前为止,我了解到的是有更多的坐标系:

  • 画布坐标系
  • Css坐标系
  • 物理(显示)坐标系

所以当我用CanvasRenderingContext2D画一条线时

ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(3, 1);
ctx.lineTo(3, 5);
ctx.stroke();

在将像素绘制到显示器之前,路径需要

  1. 根据CTX转换矩阵(如果有)进行调整
  2. 根据css画布元素尺寸(canvas.style.widthcanvas.style.height)与画布绘制尺寸(canvas.widthcanvas.height)之间的比例进行zoom
  3. 根据window.devicePixelRatio调整比例(高分辨率显示屏)

现在,当我想要划清界限时,我发现有两个东西可以用来战斗.第一个问题是Canvas使用抗锯齿.所以当我在整数坐标上画一条厚度为1的线时,它会变得模糊.

enter image description here

要解决此问题,需要将其移位0.5像素

ctx.moveTo(3.5, 1);
ctx.lineTo(3.5, 5);

第二件要考虑的事情是window.devicePixelRatio.它用于将逻辑css像素映射到物理像素.如何使画布适应高分辨率设备的方法是根据比例进行zoom

const ratio = window.devicePixelRatio || 1;
const clientBoundingRectangle = canvas.getBoundingClientRect();
  
canvas.width = clientBoundingRectangle.width * ratio;
canvas.height = clientBoundingRectangle.height * ratio;

const ctx = canvas.getContext('2d');
ctx.scale(ratio, ratio);

我的问题是,抗锯齿问题的解决方案与高分辨率显示器的zoom 有什么关系?

假设我的显示器是高分辨率的,window.devicePixelRatio2.0.当我应用上下文zoom 以使画布适应高分辨率显示,并希望绘制粗细为1的线时,我可以忽略上下文zoom 并绘制吗

ctx.moveTo(3.5, 1);
ctx.lineTo(3.5, 5);

在这种情况下,这是有效的

ctx.moveTo(7, 2);
ctx.lineTo(7, 10);

或者,我是否必须考虑zoom 比例并使用类似

ctx.moveTo(3.75, 1);
ctx.lineTo(3.75, 5);

为了得到清脆的线条吗?

推荐答案

抗锯齿可以在绘制到画布位图缓冲区时在画布位图缓冲区上进行渲染时发生,也可以在通过CSS在监视器上显示时进行.

直线的0.5px偏移量仅适用于为奇数的线宽.正如您所暗示的,这是为了使只能与路径中心对齐的笔划,从而以线条宽度的一半在实际路径内外扩散,落在全像素坐标上.有关详细说明,请参阅this previous answer of mine.

Scaling the canvas buffer to the monitor's pixel ratio works because on high-res devices, multiple physical dots will be used to cover a single px area. This allows to have more details e.g in texts, or other vector graphics. However, for bitmaps this means the browser has to "pretend" it was bigger in the first place. For instance a 100x100 image, rendered on a 2x monitor will have to be rendered as if it was a 200x200 image to have the same size as on a 1x monitor. During that scaling, the browser may yet again use antialiasing, or another scaling algorithm to "create" the missing pixels.
By directly scaling up the canvas by the pixel ratio, and scaling it down through CSS, we end up with an original bitmap that's the size it will be rendered, and there is no need for CSS to scale anything anymore.

但现在,您的画布上下文也按此像素比例zoom ,如果我们回到直线,仍然假设有2x的显示器,0.5px的偏移量现在实际上变成了1px的偏移量,这是无用的.lineWidth1实际上将生成2px笔划,不需要任何偏移.

所以不,不要忽略zoom ,当偏移直线的上下文时.

但最好的办法可能是根本不使用这种偏移技巧,而是使用rect()个调用和fill()个调用,如果您希望行完全适合像素的话.

const canvas = document.querySelector("canvas");
// devicePixelRatio may not be accurate, see below
setCanvasSize(canvas);

function draw() {
  const dPR = devicePixelRatio;
  const ctx = canvas.getContext("2d");
  // scale() with weird zoom levels may produce antialiasing
  // So one might prefer to do the scaling of all coords manually:
  const lineWidth = Math.round(1 * dPR);
  const cellSize  = Math.round(10 * dPR);
  for (let x = cellSize; x < canvas.width; x += cellSize) {
    ctx.rect(x, 0, lineWidth, canvas.height);
  }
  for (let y = cellSize; y < canvas.height; y += cellSize) {
    ctx.rect(0, y, canvas.width, lineWidth);
  }
  ctx.fill();
}

function setCanvasSize(canvas) {
// We resize the canvas bitmap based on the size of the viewport
// while respecting the actual dPR
// Thanks to gman for the reminder of how to suppport all early impl.
// https://stackoverflow.com/a/65435847/3702797
  const observer = new ResizeObserver(([entry]) => {
    let width;
    let height;
    const dPR = devicePixelRatio;
    if (entry.devicePixelContentBoxSize) {
      width  = entry.devicePixelContentBoxSize[0].inlineSize;
      height = entry.devicePixelContentBoxSize[0].blockSize;
    } else if (entry.contentBoxSize) {
      if ( entry.contentBoxSize[0]) {
        width  = entry.contentBoxSize[0].inlineSize * dPR;
        height = entry.contentBoxSize[0].blockSize  * dPR;
      } else {
        width  = entry.contentBoxSize.inlineSize * dPR;
        height = entry.contentBoxSize.blockSize  * dPR;
      }
    } else {
      width  = entry.contentRect.width  * dPR;
      height = entry.contentRect.height * dPR;
    }
    canvas.width  = width;
    canvas.height = height;
    canvas.style.width  = (width  / dPR) + 'px';
    canvas.style.height = (height / dPR) + 'px';
    // we need to redraw
    draw();
  });
  // observe the scrollbox size changes
  try {
    observer.observe(canvas, { box: 'device-pixel-content-box' });
  }
  catch(err) {
    observer.observe(canvas, { box: 'content-box' });
  }
}
canvas { width: 300px; height: 150px; }
<canvas></canvas>

Javascript相关问答推荐

使用JQuery单击元素从新弹出窗口获取值

Next.js服务器端组件请求,如何发送我的cookie token?

JS:XML insertBefore插入元素

如何将数据块添加到d3力有向图中?

覆盖加载器页面避免对页面上的元素进行操作

面对代码中的错误作为前端与后端的集成

如何限制显示在分页中的可见页面的数量

<;img>;标记无法呈现图像

WebSocketException:远程方在未完成关闭握手的情况下关闭了WebSocket连接.&#三十九岁;

对具有相似属性的对象数组进行分组,并使用串连的值获得结果

react :图表负片区域不同 colored颜色

Select 所有输入.值

为什么Reaction useEffect函数会将控制台日志(log)呈现两次

2.0.0-dev quill拖动文本不起作用如何使其拖动文本

This.forceUPDATE并通过Reaction-Router-DOM链接组件传递状态

为什么我的AJAX联系人表单不能留在同一页上?

Mongoose DeleteOne()和DeleteMany()不工作

如何有条件地返回Java脚本中的函数?

为什么我的reducer函数被多次调用?

当每行和每列中的正方形数量超过或超过4;时,我的网格中的正方形就会从容器溢出