在画布中绘制1像素粗的线会创建一条2像素粗的线。

59

在这个jsfiddle中,有一条线宽为1。

http://jsfiddle.net/mailrox/9bMPD/350/

例如:

ctx.lineWidth = 1;

然而,当画在画布上时,这条线是2像素粗的,如何创建一个1像素粗的线。

我可以画一个矩形(高度为1像素),但我想让这条线也能在对角线上工作。那么,如何让这条线高度只有1像素呢?

谢谢!


3
嘿,我从未使用过画布,但是如果在1像素上输出2像素,则您可以尝试使用1/2像素来获得1像素高度! - Muhammad Talha Akbar
8个回答

118

Canvas计算从半个像素开始

ctx.moveTo(50,150.5);
ctx.lineTo(150,150.5);

所以从一半开始就能解决它。

var can = document.getElementById('canvas1');
var ctx = can.getContext('2d');

ctx.lineWidth = 1;

// linear gradient from start to end of line
var grad = ctx.createLinearGradient(50, 150, 150, 150);
grad.addColorStop(0, "red");
grad.addColorStop(1, "green");

ctx.strokeStyle = grad;

ctx.beginPath();
ctx.moveTo(50, 150.5);
ctx.lineTo(150, 150.5);

ctx.stroke();
<canvas id="canvas1" width="500" height="500"></canvas>http://jsfiddle.net/9bMPD/http://jsfiddle.net/9bMPD/

这个答案解释了为什么它能够这样工作。


1
当你意识到在编码之前应该花时间阅读文档时,非常感谢@FerryKobus。 - Bludwarf
4
2018年对我仍然有用。以下是一个简单的修复方法,无需进行任何调整:使用context.translate(.5,.5)将所有内容向右下方偏移半个像素。完成所需操作后,使用context.setTransform(1, 0, 0, 1, 0, 0)来恢复偏移。 - Manngo
5
谢谢提供链接。我会按照以下方式使用:ctx.moveTo(~~x + .5, ~~y + .5); 其中~~去掉所有小数部分,而.5则添加半个像素以使线条始终对齐。这样,我也可以将lineWidth设置为1。 - Jelle Blaauw

39

您还可以在X和Y方向上将翻译调整半个像素,然后使用整数值作为坐标(某些情况下可能需要四舍五入):

context.translate(0.5, 0.5)

context.moveTo(5,5);
context.lineTo(55,5);

请记住,如果您调整画布大小,则平移将被重置 - 因此您需要再次进行平移。

您可以在此处阅读有关平移功能及其使用方法的信息:

https://www.rgraph.net/canvas/reference/translate.html

此答案解释了它为什么会这样工作。


2
这是一个被低估的答案,感谢分享! - magritte
@Richard,哇,这太厉害了。这种方法有什么缺点吗? - Pacerier
据我所知没有这样的功能。这是我在使用我的RGraph库(www.rgraph.net)时使用的方法。如果您进行转换,则必须再次进行翻译,这有点麻烦 - 但我想这种情况并不经常发生。 - Richard
1
是的,但对于某些屏幕来说,0.5不是像素的大小。您应该使用像素比。 - Ievgen

9

使用一半的像素比而不是0.5来支持所有分辨率。 - Ievgen

6

对我来说,只有不同的“完美像素”技巧的组合才能帮助达到结果:

  1. 获取并缩放具有像素比的画布:

    pixelRatio = window.devicePixelRatio/ctx.backingStorePixelRatio

  2. 在调整大小时缩放画布(避免画布默认的拉伸缩放)。

  3. 将线宽乘以像素比例以找到适当的“实际”像素线厚度:

    context.lineWidth = thickness * pixelRatio;

  4. 检查线条厚度是奇数还是偶数,对于奇数线条值,将像素比例的一半添加到线条位置中。

    x = x + pixelRatio/2;

奇数线会放置在像素的中间。上述行用于将其向右移动一点。

function getPixelRatio(context) {
  dpr = window.devicePixelRatio || 1,
    bsr = context.webkitBackingStorePixelRatio ||
    context.mozBackingStorePixelRatio ||
    context.msBackingStorePixelRatio ||
    context.oBackingStorePixelRatio ||
    context.backingStorePixelRatio || 1;

  return dpr / bsr;
}


var canvas = document.getElementById('canvas');
var context = canvas.getContext("2d");
var pixelRatio = getPixelRatio(context);
var initialWidth = canvas.clientWidth * pixelRatio;
var initialHeight = canvas.clientHeight * pixelRatio;


window.addEventListener('resize', function(args) {
  rescale();
  redraw();
}, false);

function rescale() {
  var width = initialWidth * pixelRatio;
  var height = initialHeight * pixelRatio;
  if (width != context.canvas.width)
    context.canvas.width = width;
  if (height != context.canvas.height)
    context.canvas.height = height;

  context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
}

function pixelPerfectLine(x) {

  context.save();
  context.beginPath();
  thickness = 1;
  // Multiple your stroke thickness  by a pixel ratio!
  context.lineWidth = thickness * pixelRatio;

  context.strokeStyle = "Black";
  context.moveTo(getSharpPixel(thickness, x), getSharpPixel(thickness, 0));
  context.lineTo(getSharpPixel(thickness, x), getSharpPixel(thickness, 200));
  context.stroke();
  context.restore();
}

function pixelPerfectRectangle(x, y, w, h, thickness, useDash) {
  context.save();
  // Pixel perfect rectange:
  context.beginPath();

  // Multiple your stroke thickness by a pixel ratio!
  context.lineWidth = thickness * pixelRatio;
  context.strokeStyle = "Red";
  if (useDash) {
    context.setLineDash([4]);
  }
  // use sharp x,y and integer w,h!
  context.strokeRect(
    getSharpPixel(thickness, x),
    getSharpPixel(thickness, y),
    Math.floor(w),
    Math.floor(h));
  context.restore();
}

function redraw() {
  context.clearRect(0, 0, canvas.width, canvas.height);
  pixelPerfectLine(50);
  pixelPerfectLine(120);
  pixelPerfectLine(122);
  pixelPerfectLine(130);
  pixelPerfectLine(132);
  pixelPerfectRectangle();
  pixelPerfectRectangle(10, 11, 200.3, 443.2, 1, false);
  pixelPerfectRectangle(41, 42, 150.3, 443.2, 1, true);
  pixelPerfectRectangle(102, 100, 150.3, 243.2, 2, true);
}

function getSharpPixel(thickness, pos) {

  if (thickness % 2 == 0) {
    return pos;
  }
  return pos + pixelRatio / 2;

}

rescale();
redraw();
canvas {
  image-rendering: -moz-crisp-edges;
  image-rendering: -webkit-crisp-edges;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  width: 100vh;
  height: 100vh;
}
<canvas id="canvas"></canvas>

在这个示例中,调整大小事件未被触发,所以您可以在GitHub上尝试该文件。


4
画布(Canvas)可以使用 fillRect() 函数绘制干净的直线。 一个高度或宽度为1像素的矩形就能实现这个功能。 不需要半像素的值:
var ctx = document.getElementById("myCanvas").getContext("2d");

ctx.drawVerticalLine = function(left, top, width, color){
    this.fillStyle=color;
    this.fillRect(left, top, 1, width);
};

ctx.drawHorizontalLine = function(left, top, width, color){
    this.fillStyle=color;
    this.fillRect(left, top, width, 1);
}

ctx.drawVerticalLine(150, 0, 300, "green");
ctx.drawHorizontalLine(0, 150, 300, "red");

https://jsfiddle.net/ynur1rab/


2
工作得很好,但使圆角变得更加复杂 ;) - brad
1
这根本就不起作用。当你将0.5加到x或y值时,会遇到同样的问题。 - notak

3

谢谢,我看到了,但我认为可能有更好的方法,那样可能只会让事情更加混乱。我同意,半个像素对我来说也有点奇怪! - CafeHey

1

fillRect() 方法可以用于在画布上绘制细的水平或垂直线条(无需在坐标上应用 +0.5 的偏移):

this.fillRect(left, top, 1, height);
this.fillRect(left, top, width, 1);

你可以通过替换这段代码,让线条变得更加细小,例如:

this.fillRect(left, top, 0.7, height);
this.fillRect(left, top, width, 0.7);

这将使线条变细(倾向于达到1像素宽),但它们的颜色会有些衰减。

-> 工作示例

需要注意的是,如果我们设置ctx.lineWidth=0.7(对于经典的beginPath/moveTo/lineTo/stroke序列),在Chrome上不起作用(0.7和1被解释为相同)。因此,这个fillRect()方法很有用。


它也不够清晰。使用stroke rect,你肯定会看到它。 - Ievgen

1
如果以上答案都不适用于您,请检查浏览器缩放比例。我的浏览器缩放比例不知何故设置为125%,因此每四个1像素的线条会被绘制为2像素宽度。
我花了几个小时来弄清楚为什么互联网上的每个示例都可以正常工作,而我的却不行(缩放比例仅适用于我的开发选项卡)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接