I'm drawing a game map into canvas. The ground is made of tiles - simple 64x64 png images.
When I draw it in Chrome, it looks ok (left), but when I draw it in Firefox/Opera/IE (right), I get visible edges:
The problem disappears when I use rounded numbers:
ctx.drawImage(img, parseInt(x), parseInt(y), w, h);
But that doesn't help when I use scaling:
ctx.scale(scale); // anything from 0.1 to 2.0
I also tried these, but no change:
ctx.drawImage(img, 5, 5, 50, 50, x, y, w, h); // so not an issue of clamping
ctx.imageSmoothingEnabled = false;
image-rendering: -moz-crisp-edges; (css)
Is there any way to make it work in ff/op/ie?
Edit: Partial solution found
Adding 1 pixel to width/height and compensating it by scale (width+1/scale) seems to help:
ctx.drawImage(props.img, 0, 0, width + 1/scale, height + 1/scale);
It makes some artifacts, but I think it's acceptable. On this image, you can see green tiles without edges, and blue windows, which are not compensated, still with visible edges:
The simplest solution (and I'd argue most effective) is to use tiles that have a 1 pixel overlap (are either 1x1 or 2x2 larger) when drawing the background tiles of your game.
Nothing fancy, just draw slightly more than you would normally. This avoids complications and performance considerations of bringing extra transformations into the mix.
For example:
var img = new Image();
img.onload = function () {
for (var x = 0.3; x < 200; x += 15) {
for (var y = 0.3; y < 200; y += 15) {
ctx.drawImage(img, 0, 0, 15, 15, x, y, 15, 15);
// If we merely use 16x16 tiles instead,
// this will never happen:
//ctx.drawImage(img, 0, 0, 16, 16, x, y, 16, 16);
}
}
}
img.src = "http://upload.wikimedia.org/wikipedia/en/thumb/0/06/Neptune.jpg/100px-Neptune.jpg";
Before: http://jsfiddle.net/d9MSV
And after: http://jsfiddle.net/d9MSV/1/
Note as the asker pointed out, the extra pixel needs to account for scaling, so a more correct solution is his modification: http://jsfiddle.net/d9MSV/3/
Cause
This is caused by anti-aliasing.
Canvas is still work-in-progress and browser has different implementations for handling anti-aliasing.
Possible solutions
1
You can try turning off anti-aliasing for images in Firefox like this:
context.mozImageSmoothingEnabled = false;
In Chrome:
context.webkitImageSmoothingEnabled = false;
and add a class to the element like this (should work with Opera):
canvas {
image-rendering: optimizeSpeed; // Older versions of FF
image-rendering: -moz-crisp-edges; // FF 6.0+
image-rendering: -webkit-optimize-contrast; // Webkit
image-rendering: -o-crisp-edges; // OS X & Windows Opera (12.02+)
image-rendering: optimize-contrast; // Possible future browsers.
-ms-interpolation-mode: nearest-neighbor; // IE
}
Here's a browser test I made to see the effect of turning off anti-aliasing:
ANTI-ALIAS BROWSER TEST
2
Translate the whole canvas by 0.5 point.
ctx.translate(0.5, 0.5);
This doesn't always work and might come in conflict with other translations. However you can add a fixed offset each time:
ctx.translate(scrollX + 0.5, scrollY + 0.5);
3
Another option is to do a compromise that you either pad the tiles with one extra pixel which I don't recommend due to the extra work you'll get maintaining this.
4
This method draws the tiles a bit scaled so they overlap:
ctx.drawImage(tile, x, y, 65, 65); //source tile = 64x64
This might be enough to cover the glitch. Combined with turning anti-alias off (or using nearest neighbor) it won't affect much of the tile graphics, but it might reduce performance a tad due to the scaling.
If you turn off anti-aliasing (and that didn't work on its own) the overhead will be minimal as some goes to interpolate the image.
5
Simply draw everything offset -1 position (ie. grid = 63x63). Of course this will screw up everything else regarding checks so...
In every tile draw use Math.floor when there is division involved, like this:
ctx.drawImage(image,Math.floor(xpos/3),ypos+1)
Also, if you have a loop to draw, that calls itself, always use requestAnimationFrame. I don't know why, but since I moved from timer timeout to requestAnimationFrame I have no more artifacts.
I draw all of my tiles to a perfectly sized buffer and then draw that buffer to the display canvas with drawImage, which takes care of scaling. If you have 16x16 tiles, make your buffer some multiple of 16, like 256x128 or 64x96 or something along those lines. This eliminates spaces between tiles that arise due to drawing with scaled dimensions. The only downside is that you must draw the full background twice: once to draw the background in pixel perfect space, and once to draw the scaled image to the final display canvas. Remember to maintain aspect ratio between the buffer and display canvas to avoid skewing your final image.
Related
I've been playing with canvas element and discovered that when I attempt to draw NxN uniform solid-colored cells next to each other, in some width/height configurations, there are blurry white-ish lines between them.
For instance, this canvas is supposed to look black but contains some sort of grid which I conjecture to be a result of faulty antialiasing in the browser.
Suffice to say, this bug appears only in some configurations but I would like to get rid of it for good. Is there any way to circumvent this? Have you ever had problems with antialiasing in canvas?
I have made this fiddle which demonstrates the issue and allows you to play with the dimensions of the canvas and number of cells. It also contains the code I use to draw the cells, so that you can inspect it and tell me if I'm doing anything wrong.
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
for (var i = 0; i < numberOfCells; ++i) {
for (var j = 0; j < numberOfCells; ++j) {
ctx.fillStyle = '#000';
ctx.fillRect(j * cellWidth, i * cellHeight, cellWidth, cellHeight);
}
}
Thanks in advance,
Petr.
jsFiddle : https://jsfiddle.net/ngxjnywz/2/
snippet of javascript
var cellWidth = Math.ceil(canvasWidth / numberOfCells);
var cellHeight = Math.ceil(canvasHeight / numberOfCells);
Depending on the width, height and your numberOfCells you are sometimes getting a... lets say 4.2 which is 4, however this would be displayed wrong and will allow a 1 pixel blank line to appear. So all you need to do is use the Math.ceil function and this will cause your cellWidth and cellHeight to always be the higher number and you won't get blank lines anymore
The best solution is to add a 0.5 pixel wide stroke around all the fills, using the same style as the fill and offsetting all drawing so that you render at the center of pixels rather than the top left.
If you add scaling or translation you will have to adjust the coordinates so that you still give the centers for your drawing coordinates.
In the end you can only reduce the artifacts but for many situations you will not be able to completely remove them.
This answer shows you how to remove the artifacts for an untransformed canvas.
How to fill the gaps
After reading through and trying several approaches, I've decided to come up with my own. I've created another (virtual) canvas which had integer dimensions corresponding to the number of cells in the grid.
After drawing all the cells in there, I call context.drawImage() on the main canvas and pass the virtual canvas as an argument along with offset and scale parameters to make it fit rest of my drawing. Assuming that the browser would scale the virtual canvas's image as a whole (and not as individual cells), I was hoping to get rid of the unwanted separator lines.
In spite of my efforts, the lines are still there. Any suggestions?
Here's the fiddle demonstrating my technique: https://jsfiddle.net/ngxjnywz/5/
I'm using javascript and a canvas to draw a mathematically designed scale (for measuring torque, it includes newton-meters and foot-pounds). I had been positioning my ticks using trigonometry, and drawing arcing lines using arc, naturally. The problem came when they needed to line up, but there was some strange distortion. I then drew a very large circle and compared it with a circular selection in GIMP and there was a distortion. The circles are consistent every 45 degrees around the circles, but between those nodes the canvas drawn circle deviates outwards, much like an octagon that can been rounded too far outwards to approximate a circle.
Why do these distortions occur? How can I draw a truly circular circle on a canvas?
This is the code I used to generate my distorted circle.
<!DOCTYPE html>
<html>
<body>
<canvas width=8000 height=8000 id='myCanvas'></canvas>
<script>
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
context.beginPath();
context.arc(4000, 4000, 3200, 0, Math.PI*2, false);
context.lineWidth = 1;
context.strokeStyle = '#ff0000';
context.lineCap = 'square';
context.stroke();
</script>
</body>
</html>
This may not be minimal, I know not of the relevance of context.lineCap, but it's a vestige of the code this is based on.
The following screenshot shows the difference between the circle drawn by GIMP and the circle drawn by Chrome
This is Chromium Bug #164241 (declared fixed, although the fix may not have made it out yet). The short answer is: Chrome is approximating circles with composite Bezier curves.
I was unable to replicate this myself on Chromium (43.0.2357.130, Ubuntu) but it does occur on Firefox 39, although I couldn't find a similar bug report for Firefox. The fact that it's correct every 90 degrees (not 45, at least not for me) indicates that circles are being approximated by 4 curves, as the curve endpoints are guaranteed to be correct.
Luckily there's a simple workaround: create a path consisting of more than 4 curves. 8 or even 6 should be sufficient for the radii you're using but you can use more if you want to play it safe. Or you could increase the number of curves as a function of the radius, although calculating the optimal function (the minimum n that will produce an accurate circle for a given radius r) is non-trivial.
context.beginPath();
var n=8; // 4 is the default behaviour. higher is better, also slower
for (var i=0; i<n; i++) {
context.arc(4000, 4000, 3200, Math.PI*2*i/n, Math.PI*2*(i+1)/n, false);
}
context.stroke();
See also this question.
(And yes, lineCap is a red herring.)
I have a canvas object from which i want to get the color of a position with canvas.getImageData. When i paint a gradient on it and try to get the color from point (0,0) chrome behaves as expected and returns the color value 255,255,255 whereas IE returns 254,253,253.
I have here a plunker which shows what i mean :
http://plnkr.co/edit/BWSn2J2N2A6poaH4WR6a?p=preview
You can just execute this in IE and in Chrome and you'll see the difference.
I use the IE v11 and the Chrome v40.
Maybe the error is during my creation of the canvas.
var RepaintGradient = function(gradient)
{
_gradientContext.fillStyle = 'rgb(255,0,0)';
_gradientContext.fillRect(0,0,width,height);
var gradientWTT = _gradientContext.createLinearGradient(0, 0, width, 0);
gradientWTT.addColorStop(0, "white");
gradientWTT.addColorStop(1, gradient);
_gradientContext.fillStyle = gradientWTT;
_gradientContext.fillRect(0, 0, width, height);
var gradientBTT = _gradientContext.createLinearGradient(0, width, 0, 0);
gradientBTT.addColorStop(0, "black");
gradientBTT.addColorStop(1, "transparent");
_gradientContext.fillStyle = gradientBTT;
_gradientContext.fillRect(0, 0, width, height);
var color = _gradientContext.getImageData(0, 0, 1, 1).data;
alert(color[0]+' '+color[1]+' '+color[2]);
};
Is this a normal behavior and IE and Chrome have this difference or is it something that i missed during the creation of the canvas ?
This is due to rounding errors during blending and composition when sub-pixeling is used - different browsers gain different result based on their approach (IE and FireFox gives the same result, webkit browsers a different result).
It's not much we can do with the inner working, but we can adjust for sub-pixeling so it won't happen by simply translating the canvas half pixel.
Add this line to the top of the gradient function:
_gradientContext.setTransform(1,0,0,1, 0.5,0.5); // offset 0.5 pixel
and it should work
The reason being that pixels are initially rendered from the pixel's center, meaning 0.5 of the pixel is interpolated both ways. Translating 0.5 pixel will match the center to the pixel grid.
Update (from comments):
Another approach using coordinates to calculate color value (HSL) instead of picking a pixel (this approach will also allow you to set a point in the picker based on input values):
It's normal...
Different browsers are allowed to impliment slightly different renderings of canvas gradients.
Even one image will be rendered slightly differently on each browser.
I have got a canvas and I display image using it via the putImageData() method after updating bytes in clamped array got by getImageData.data.
That works nicely, but I need to scale *2 in Y-direction.
Of course I can do it without a question here too by repeating every lines twice, but it requires too much time to render a frame then (I display 25 frames per second). I've read about the ability to scale image with drawImage() method. The only problem that as far as I know, using drawImage() is slower than using putImageData(), and it was required in old browser versions like Firefox/2 (or such).
How can I upscale the image in Y direction twice as fast as possible?
By the way, is it possible to get similar solution to gain full screen resolution somehow (a flash - not JS - example: like what youtube does when you go to fullscreen)?
If you want the whole canvas to scale, you can do this without any extra javascript. Just set a size in CSS that has twice the width and height specified in the canvas.
<canvas width="200" height="100" style="width:200; height: 200">
See the W3 spec.
Edit:
Looking at the question linked by Alnitak below: If you want nearest-neighbor scaling rather than antialiasing you can do this:
canvas {
image-rendering: -moz-crisp-edges;
image-rendering: -o-crisp-edges;
image-rendering: -webkit-optimize-contrast;
-ms-interpolation-mode: nearest-neighbor;
}
You can achieve vertical scaling (without anti-aliasing) by just grabbing one image row at a time with getImageData and then replicating that row multiple times with putImageData, e.g.:
var oy = 0;
for (var y = 0; y < sh; ++y) {
var data = src_ctx.getImageData(0, y, sw, 1);
for (var n = 0; n < scale; ++n) {
dst_ctx.putImageData(data, 0, oy++);
}
}
See http://jsfiddle.net/alnitak/hYZ3U/ for a worked example that completes in 3ms on my machine.
I'm working on a 2d canvas-based app using EaselJS where the user can move indefinitely on the xy-plane by dragging the background. Think google maps, except the background is a repeating tile.
The code for the movement is very simple and looks something like this:
// container - the createjs.Container being panned
// background - a createjs.Shape child of container, on which the
// background is drawn
background.onPress = function(evt) {
var x = evt.stageX, y = evt.stageY;
evt.onMouseMove = function(evt) {
// the canvas is positioned in the center of the window, so the apparent
// movement works by changing the registration point of the container in
// the opposite direction.
container.regX -= evt.stageX - x;
container.regY -= evt.stageY - y;
x = evt.stageX;
y = evt.stageY;
stage.update();
};
evt.onMouseUp = function(evt) {
// Here the background would be redrawn based on the new container coords.
// However the issue occurs even before the mouse is released.
background.redraw();
stage.update();
};
};
All works as expected until reaching 32678px (2^15) on either axis. What occurs is different in different browsers, but the point where it first happens is the same.
In Firefox, it will suddenly shift a large chunk of pixels (~100) rather than 1. It will then happen again at 65538 (2^16+2), perhaps more after that, but I haven't witnessed it. After the trouble points, the drag will continue smoothly, as expected, but remaining shifted.
In Chrome, the effect is more dramatic. The drawing breaks and results in repeated ~100px wide "stripes" of the background across the page at 32768, and does not correct itself on redraw.
Oddly, the numbers in EaselJS don't reflect the issue. The only change in the container's transform matrix is the tx or ty incrementing by 1. No other matrices change. It seems as though EaselJS is getting all the numbers right.
Can anyone shed any light this issue?
Edit:
I worked around this problem by redrawing parts of the container using a calculated regX/regY, rather than attempting to translate very large regX/regY coords on the canvas.
This question may be related
What is the maximum size for an HTML canvas?
From what I gather browsers use short int to store canvas sizes with 32,767 being the maximum possible value.
Links possibly related to your issue,
Mozilla - HTML5 Canvas slow/out of memory
https://stackoverflow.com/a/12535564/149636