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 t
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>