1

I wanted to write a little library that would draw all kind of patterns on canvas. I run into an issue that when I draw two triangles next to each other, they don't stick to each other and leave a little gap.

You can see it all there https://jsfiddle.net/dufia/8ggwn9mq/1/

function drawEquilateralTriangleB(ctx, startingPoint, size, color, opacity, row, column) {
  var h = size * Math.cos(Math.PI/6);
  ctx.beginPath();
  ctx.moveTo(startingPoint.x - (size / 2) + 0.5, startingPoint.y + 0.5);
  ctx.lineTo(startingPoint.x - (size / 2) + (size / 2) + 0.5, startingPoint.y + h + 0.5);
  ctx.lineTo(startingPoint.x + (size / 2) + 0.5, startingPoint.y + 0.5);
  ctx.lineTo(startingPoint.x + (size / 2) + 0.5, startingPoint.y + 0.5);
  ctx.fillStyle = color;
  ctx.globalAlpha = opacity;
  ctx.fill();
};

Between triangles you can see the white lines, that are in fact a background, problem doesn't affect squares.

I tried to use 0.5 coordinates and messed up with strokes. Strokes can do a job when opacity is 100% but look all messed up when pattern is transparent.

Is there a solid fix?

youbetternot
  • 2,566
  • 2
  • 17
  • 20
  • The lines you are seeing are the result of anti-aliasing of edges that are diagonal edges or are vertical/horizontal edges at partial pixel boundaries. To my knowledge, there is no way to turn anti-aliasing off. In the triangle example, you can eliminate the lines on vertical/horizontal edges by placing all the triangle vertices at whole pixel locations but you will still be stuck with the lines on diagonal edges. – Bobby Orndorff Nov 18 '15 at 17:19

1 Answers1

4

It is most annoying. Same happens in SVG. It can destroy an image after you have spent so much effort in getting everything lined up.

Not even rounding pixels to pixel boundaries, or centers helped. What I found helped in the end is adding a 0.5 pixel stroke to the fills and offsetting the coordinated by 0.5 pixels. The math does not make sense but the results are all that matters.

Bellow is an example of the problem and the solution I have used. Move the mouse over the image to see the zoomed view and see the various artifacts.

There does remain one problem, the darkening of the joining pixels. This is due to (ALL) browsers using the incorrect colour model for blending colours. They average a channel with

colorCh = (colorC1 + colorC2) / 2; // incorrect

Colour channel values represent square root of the display device output.The formula used darkens all blending. (I really wish they would fix this). You can see it in the example. The bottom two have adjoining pixels that are too dark.

The correct blend is

colorCh = sqrt( (pow(colorC1, 2 ) + pow(colorC2, 2)) / 2); // correct

And would fix that last little (big issue) problem.

// get a canvas
var canvas = document.getElementById("canV"); 
var ctx = canvas.getContext("2d");
// mouse stuff
var mouse = {x:0,y:0}  // mouse pos on canvas
function mouseMove(event){  // mouse event listener
    mouse.x = event.offsetX;  mouse.y = event.offsetY; 
    if(mouse.x === undefined){ mouse.x = event.clientX;  mouse.y = event.clientY;} 
}
// add the mouse listener
canvas.addEventListener('mousemove',mouseMove);

// clear the background with white
ctx.fillStyle = "white";
ctx.fillRect(0,0,canvas.width/2,canvas.height);

// thing to draw 
// 4 tri polys with four colours
var points = [-50,-50,50,-50,50,50,-50,50,0,0]
var polys = [[0,1,4],[1,2,4],[2,3,4],[3,0,4]];
var cols = ["red","green","blue","purple"];

// draws the polys normaly at the location x,y
function drawPolysAt(x,y){
    for(var i = 0; i < polys.length; i++){
        var p = polys[i];
        ctx.fillStyle = cols[i];
        ctx.beginPath();
        ctx.moveTo(points[p[0]*2]+x,points[p[0]*2+1]+y)
        for(var j = 1; j< p.length; j++){
            ctx.lineTo(points[p[j]*2]+x,points[p[j]*2+1]+y);
        }
        ctx.fill();
    }
}

// draws the polys with outlining stroke 0.5 pixels 
// wide.
function drawPolysAtFix(x,y){
    ctx.lineWith = 0.5;
    for(var i = 0; i < polys.length; i++){
        var p = polys[i];
        ctx.fillStyle = cols[i];
        ctx.strokeStyle = cols[i];
        ctx.beginPath();
        ctx.moveTo(points[p[0]*2]+x,points[p[0]*2+1]+y)
        for(var j = 1; j< p.length; j++){
            ctx.lineTo(points[p[j]*2]+x,points[p[j]*2+1]+y);
        }
        ctx.stroke();
        ctx.fill();
    }
}

// draws the help text
ctx.font = "12px verdana";
function text(text,x,y,col){
    ctx.textAlign = "center";
    ctx.textBaseline = "top";
    ctx.fillStyle = col;
    ctx.fillText(text,x,y);
}

// draw the first example
var posX = 60;
var posY = 60;
drawPolysAt(posX,posY);
text("Drawn on pixel",posX,posY+52,"black");
text("boundaries.",posX,posY+52+14,"black");

// draw the second example offest by 0.5 pixels
posX += 120;
drawPolysAt(posX+0.5,posY+0.5);
text("Drawn offset by",posX,posY+52,"black");
text("0.5 pixels",posX,posY +52+14,"black");
posX -= 60;
text("Makes no differance",posX,posY+55+12+16,"black");
text("Appart from one pixel bleed ",posX,posY+55+12*2+16,"black");
text("on right due to offset ",posX,posY+55+12*3+16,"black");

// draw the thrid example with the stroke but not offset
posX = 60;
posY = 240;
drawPolysAtFix(posX,posY);
text("With 0.5 pixel",posX,posY+52,"black");
text("stroke. But Bleeds!",posX,posY+52+14,"black");

// draw with stroke and 0.5 pixel offset
posX += 120
drawPolysAtFix(posX+0.5,posY+0.5);
text("With 0.5 Stroke",posX,posY+52,"black");
text("and offset 0.5 pixels",posX,posY +52+14,"black");
posX -= 60;
text("Right side is best solution ",posX,posY+55+14+16,"black");



function update(){
    // to keep the zoom crisp
    ctx.imageSmoothingEnabled = false;
    // zoom around mouse on right side 
    ctx.drawImage(canvas,mouse.x-20,mouse.y-20,40,40,canvas.width/2,0,canvas.width/2,canvas.height)

    requestAnimationFrame(update)
}
update();
.canC { width:500px;  height:400px;}
.info {
  font-size:x-small;
}
<div class="info"> Top left drawn normaly at pixel boundaries, with white pixels showing through joins.<br>
Top right drawn normaly at pixel centers (offset 0.5,0.5 pixels) bleeds into sourounding pixels.<br>
Bottom left drawn with 0.5 pixel stroke at pixel boundaries. Creates a blured (bleeding) edge<br>
Bottom right. Drawn with 0.5 pixel stroke and offset by 0.5 pixels. Note that there is no bleeding around the outside and the joins are the best posible.
<canvas class="canC" id="canV" width=500 height=400></canvas>
Blindman67
  • 51,134
  • 11
  • 73
  • 136