58

In this jsfiddle there's a line with a lineWidth of 1.

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

e.g:

ctx.lineWidth = 1;

However the line is 2px thick when it's drawn on the canvas, how do you create a 1px thick line.

I could draw a rectangle (with 1px height) however I want the line to also work on diagonals. So how do you get this line to be 1px high?

Thanks!

gman
  • 100,619
  • 31
  • 269
  • 393
CafeHey
  • 5,699
  • 19
  • 82
  • 145

8 Answers8

117

Canvas calculates from the half of a pixel

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

So starting at a half will fix it.

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/

This answer explains why it works that way.

Lee Taylor
  • 7,761
  • 16
  • 33
  • 49
Ferry Kobus
  • 2,009
  • 2
  • 14
  • 18
  • 1
    When you realize that you should have take your time to read the docs before coding. Thanks a lot @FerryKobus – Bludwarf May 05 '15 at 15:32
  • 4
    Still useful to me in 2018. Here’s a simple fix without having to make the adjustments: `context.translate(.5,.5);` to offset everything by half a pixel. After doing what needs to be done, `context.setTransform(1, 0, 0, 1, 0, 0);` to restore the offset. – Manngo Aug 18 '18 at 11:37
  • 4
    Thanks for the links. I use it in the following way: `ctx.moveTo(~~x + .5, ~~y + .5);` where `~~` removes all decimals and `+ .5`, well, it adds a half pixel so the line is always aligned well. With this, I can also set my `lineWidth` to 1. – Jelle Blaauw Oct 01 '18 at 09:44
39

You can also translate by half a pixel in the X and Y directions and then use whole values for your coordinates (you may need to round them in some cases):

context.translate(0.5, 0.5)

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

Keep in mind that if you resize your canvas the translate will be reset - so you'll have to translate again.

You can read about the translate function and how to use it here:

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

This answer explains why it works that way.

Richard
  • 4,809
  • 3
  • 27
  • 46
  • 2
    this is a much underrated answer, thanks for sharing! – magritte Oct 23 '15 at 08:26
  • @Richard, Wow, this is genius. Any drawbacks from this approach? – Pacerier Aug 20 '16 at 03:21
  • Not that I know of. This is what I use with my RGraph library ( www.rgraph.net ). The fact that you have to translate again if you do a transformation is a bit annoying - but they're not often done I guess. – Richard Aug 20 '16 at 07:46
  • 1
    Yes, but 0.5 is not a size of the pixel for some screens. You should use the pixel ratio. – Ievgen Jul 11 '19 at 22:00
8

Or as this answer states, to get a width of 1, you need to start at a half pixel.

ctx.moveTo(50.5,150.5);
ctx.lineTo(150.5,150.5);

http://jsfiddle.net/9bMPD/355/

Community
  • 1
  • 1
tonycoupland
  • 4,127
  • 1
  • 28
  • 27
6

For me, only a combination of different 'pixel perfect' techniques helped to archive the results:

  1. Get and scale canvas with the pixel ratio:

    pixelRatio = window.devicePixelRatio/ctx.backingStorePixelRatio

  2. Scale the canvas on the resize (avoid canvas default stretch scaling).

  3. multiple the lineWidth with pixelRatio to find proper 'real' pixel line thickness:

    context.lineWidth = thickness * pixelRatio;

  4. Check whether the thickness of the line is odd or even. add half of the pixelRatio to the line position for the odd thickness values.

    x = x + pixelRatio/2;

The odd line will be placed in the middle of the pixel. The line above is used to move it a little bit.

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>

Resize event is not fired in the snipped so you can try the file on the github

Ievgen
  • 4,261
  • 7
  • 75
  • 124
4

The Canvas can draw clean straight lines with fillRect(). A rectangle with a 1px height or a 1px width does the job. It doesn't need half-pixel value:

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/

Tom Ah
  • 555
  • 6
  • 6
  • 2
    Works nicely but makes rounded corners considerably more complicated ;) – brad Feb 16 '20 at 20:13
  • This literally does not work at all. When you add 0.5 to the x or y value you run into the same problem. – notak Jan 31 '22 at 18:07
3

Did you see the first hit on google? (search for canvas line width 1px). Though I have to admit this isn't exactly "clean" or "lean". Ferry Kobus' solution is much better. Then again: it sucks you need to use "half pixels" in the first place...

Community
  • 1
  • 1
RobIII
  • 8,488
  • 2
  • 43
  • 93
  • Thanks, I did see that but I thought there might be a better way and that might have just confused matters. I agree, half-a-pixel is a bit odd to me too! – CafeHey Dec 14 '12 at 13:44
1

The fillRect() method can be used to draw thin horizontal or vertical lines in canvas (without having to apply the +0.5 shift on coordinates):

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

And you can actually make the lines even thinner by just replacing this code by something like:

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

Lines will be thinner (tending to reach 1 pixel wide) but their color a bit attenuated.

-> working example

To be noted that if we set ctx.lineWidth=0.7 (for the classical beginPath/moveTo/lineTo/stroke sequence), it does not work on Chrome (0.7 and 1 are interpreted the same way). Thus an interest for this fillRect() method.

Ghislain
  • 21
  • 2
1

If none of these answers worked for you, check your browser zoom. Mine was somehow at 125% so every fourth 1px line was drawn 2px wide.

I spent hours trying to figure out why every fiddle on the internet worked and mine didn't (the zoom was only set for my dev tab)

Curtis
  • 2,486
  • 5
  • 40
  • 44