I am using HTML5 canvas to pre-render sprites and have come across some weird behavior which looks like a rendering bug. The following minimal example produces it:
var CT = document.getElementById("myCanvas").getContext("2d");
CT.scale(24, 24);
CT.translate(1.0717, 0.1);
CT.rect(0.2, 0.35, 0.4, 0.1);
CT.rect(-0.05, -0.05, 0.1, 1);
CT.translate(0.4, 0);
CT.rect(-0.05, -0.05, 0.5, 1);
CT.fill();
<canvas id="myCanvas" width="50" height="30" style="border:1px solid #d3d3d3;"></canvas>
Looking at the resulting image I notice that the horizontal bar interferes with the left side vertical one, although it does not touch it. Changing the geometry (e.g. removing the right-side vertical bar), changes the artifacts in an (as far as I can see) unpredictable way.
Has anyone of you come across this issue? What could cause it and how to avoid it? This is annoying me more than it should. The behavior occurs in different browsers (I tested on IE11 and Firefox Quantum).
If it's not clear look at the left most column of pixels on the left most rectangle. The pixels pointed at the red arrow are darker than the pixels pointed at by the blue arrow even though the other 2 rectangles on the right seem like they should have absolutely no influence on the rectangle on the left.
I managed to repo the issue in Edge but not Firefox 57.0.1 or Chrome 62. This might not be the fastest solution but it did fix the problem in Edge which is to rasterize each rectangle on it's own by calling CT.fill followed by CT.beginPath after each rectangle.
var CT = document.getElementById("myCanvas").getContext("2d");
CT.scale(24, 24);
CT.translate(1.0717, 0.1);
CT.rect(0.2, 0.35, 0.4, 0.1);
CT.fill();
CT.beginPath();
CT.rect(-0.05, -0.05, 0.1, 1);
CT.fill();
CT.beginPath();
CT.translate(0.4, 0);
CT.rect(-0.05, -0.05, 0.5, 1);
CT.fill();
<canvas id="myCanvas" width="50" height="30" style="border:1px solid #d3d3d3;"></canvas>
Honestly I'd file a bug. While I know the canvas spec is somewhat lenient it's hard to imagine this particular issue is spec compliant. (though it may be)
The problem is a precision error in the floating point math used by the GPU.
Because you have zoomed into the rendering area 24 times normal the error is amplified.
There is not much you can do apart from rendering on pixel boundary and/or avoid very large scaling.
The image below rendered on FF. The canvas is top red box with zoomed in sections marked in red to show the anomalies. Was originally rendered on transparent cleared canvas, the white background was added in post.
Note that the effect can affect the width of the canvas.
Also note that some coordinates do not show the effect (columns marked with green arrows)
It does not happen on Chrome and I have not tried Edge.
Related
Seems like an update on google chrome messed up my canvas rendering.
I have a pretty simple code that renders an image and text on canvas:
var onDraw = function() {
context.clearRect(0, 0, 256, 256);
context.drawImage(image, 0, 0, 256, 256);
context.fillText('TEST', 0, 20);
requestAnimationFrame(onDraw);
};
This code is terribly flickers on Chrome: https://jsfiddle.net/gp9jxn6q/ (just move your mouse over the page).
There are only 2 ways I found to prevent this behavior:
Call context.clearRect() for the whole canvas. But in this case I can not redraw dirty rectangles only.
Set image-rendering: pixelated for the canvas. In this case all fonts look terrible.
What else can be done with this?
This is a bug that started to appear more frequently since Chromium version 83 and even more so since 85 (so in addition to Chrome this also effects Opera and the new Edge).
I filed an issue at Chromium a few months ago and they are currently working on a fix:
https://bugs.chromium.org/p/chromium/issues/detail?id=1092080
What happens is that the antialiasing is set to "nearest neighbour" in the next monitor frame after the drawImage() call. This can affect the source and destination element, and this affects any CanvasImageSource (image, video, canvas: https://developer.mozilla.org/en-US/docs/Web/API/CanvasImageSource).
It happens randomly because it is probably bound to the performance of the device and timing, because some graphics settings can fix the bug while at the same time they can create the bug on another device.
But I also have some good news. I think I've finally found a workaround you can execute after the drawImage() call that resets the antialiasing: http://jsfiddle.net/u7k5qz2p/
<div class="chromium-issue-1092080-workaround__wrapper">
<canvas id="canvas" width="300" height="300"></canvas>
<div class="chromium-issue-1092080-workaround__overlay"></div>
</div>
context.drawImage(image, 0, 0, 256, 256);
chromiumIssue1092080WorkaroundOverlay.style.transform = `scaleX(${Math.random()})`
What it does is overlay a div on top of the canvas. And after each drawImage() call changes the scaleX in the transform style to trigger a reset of the antialiasing setting in the canvas.
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.
I don't know why that white point appear at the bottom right corner of the created rectangle.
(Only visible in 21.0.1180.83 and .89 under WinXP)
It depends on the canvas height...
http://jsbin.com/ejeyef/1/
It probably has something to do with Subpixel rendering . Looking at your code, you do the following:
c.rect(10.5, 10.5, 100, 100);
Which means: "Draw a rectangle, with a size of 100x100px at the coordinates 10.5px from X, and 10.5px from Y". The screen/browser can't render a "half pixel", so it's always rounded somewhere. That might be the cause of your problem.
To fix this, simply don't use half values for this kind of things. This works fine:
c.rect(10, 10, 100, 100);
I am working on a canvas animation, and one of the images is supposed to be a diamond.
Right now, I got as far as this:
ctx[0].beginPath();
ctx[0].moveTo(0,-80);
ctx[0].lineTo(-60,-130);
ctx[0].lineTo(-36,-160);
ctx[0].lineTo(36,-160);
ctx[0].lineTo(60,-130);
ctx[0].closePath();
ctx[0].fillStyle = "rgba(175,175,255,0.7)";
ctx[0].fill();
which draws a plain, light blue translucid diamond shape.
This is far too simple, but I'm having serious problems with the "color". I'm guessing something glass-like should do the trick, but I haven't found anything useful so far. I can add as many lines as needed, if it helps, but the color is my main problem.
This'll be pre-rendered, so long, complex code is not much of a problem. I'd rather not use images, though.
To sum up: I need a glass-ish effect for canvas. Any ideas?
I think what you are looking for in glass (or, presumably, diamond) is that it is not entirely transparent or flat. Instead, it reflects its surroundings and very slightly distorts its background. You can give the appearance of a reflection by means of a radial gradient. The distortion, however, is trickier. You could move and scale every pixel behind the object, but that would be incredibly difficult to implement, not to mention grindingly slow. Alternatively, you could implement a very fine, rapidly shifting gradient, which would give the appearance of a distortion of the pixels underneath, even though none is actually taking place.
Here is an implementation of a pane of glass with reflection and distortion.
<html>
<canvas id="canvas" style="position:fixed;">
</canvas>
<script type="text/javascript">
document.getElementById("canvas").height=window.innerHeight;
document.getElementById("canvas").width=window.innerWidth;
ctx=document.getElementById("canvas").getContext("2d");
textWidth=ctx.measureText("Hello World! ");
textWidth=Math.ceil(textWidth.width);
ctx.lineWidth=3;
targetWidth=Math.floor(window.innerWidth/textWidth)*textWidth;
for(i=0;i<500;i++)
{
ctx.fillText("Hello World! ",((i*textWidth)%(targetWidth)),(16*Math.floor((i+1)*textWidth/window.innerWidth)+16));
}
var glass = ctx.createRadialGradient(80,110,0,100,140,100);
for(i=0;i<=100;i++)
{
redComponent=Math.round(210-(i%11));
greenComponent=Math.round(245-(i%7));
blueComponent=Math.round(255-(i%5));
opacity=Math.round(((i%3)+1)*Math.sin(i/200*Math.PI)*1000)/3000;
glass.addColorStop(i/100,"rgba("+redComponent+","+greenComponent+","+blueComponent+","+opacity+")");
}
glass.addColorStop(1,"rgba(0,0,0,0)")
ctx.fillStyle=glass;
ctx.beginPath();
ctx.translate(100,0);
ctx.moveTo(100,100);
ctx.lineTo(187,150);
ctx.lineTo(137,237);
ctx.lineTo(50,187);
ctx.lineTo(100,100);
ctx.closePath;
ctx.fill();
ctx.stroke();
</script>
</html>
And the result is:
I just created a fancy canvas effect using cheap motion blur
ctx.fillStyle = "rgba(255,255,255,0.2)";
ctx.fillRect(0,0,canvas.width,canvas.height);
Now i want to do the same, but with transparent background. Is there any way to do something like that? I'm playing with globalAlpha, but this is probably a wrong way.
PS: Google really don't like me today
Here's a more performance friendly way of doing it, it requires an invisible buffer and a visible canvas.
buffer.save();
buffer.globalCompositeOperation = 'copy';
buffer.globalAlpha = 0.2;
buffer.drawImage(screen.canvas, 0, 0, screen.canvas.width, screen.canvas.height);
buffer.restore();
Basically you draw your objs to the buffer, which being invisible is very fast, then draw it to the screen. Then you replace clearing the buffer with copying the last frame onto the buffer using the global alpha, and globalCompositeOperation 'copy' to make the buffer into a semi-transparent version of the previous frame.
You can create an effect like this by using globalAlpha and two different canvas objects: one for the foreground, and one for the background. For example, with the following canvas elements:
<canvas id="bg" width="256" height="256"></canvas>
<canvas id="fg" width="256" height="256"></canvas>
You could copy draw both a background texture and a motion blurred copied of foreground like so:
bg.globalAlpha = 0.1;
bg.fillStyle = bgPattern;
bg.fillRect(0, 0, bgCanvas.width, bgCanvas.height);
bg.globalAlpha = 0.3;
bg.drawImage(fgCanvas, 0, 0);
Here is a jsFiddle example of this.
OP asked how to do this with an HTML background. Since you can't keep a copy of the background, you have to hold onto copies of previous frames, and draw all of them at various alphas each frame. Nostalgia: the old 3dfx Voodoo 5 video card had a hardware feature called a "t-buffer", which basically let you do this technique with hardware acceleration.
Here is a jsFiddle example of that style. This is nowhere near as performant as the previous method, though.
What you are doing in the example is partially clear the screen with a semi transparent color, but as it is, you will always gonna to "add" to the alpha channel up to 1 (no transparency).
To have this working with transparent canvas (so you can see what lies below) you should subtract the alpha value instead of adding, but I don't know a way to do this with the available tools, except running all the pixels one by one and decrease the alpha value, but this will be really, really slow.
If you are keeping track of the entities on screen you can do this by spawning new entities as the mouse moves and then setting their alpha level in a tween down to zero. Once they reach zero alpha, remove the entity from memory.
This requires multiple drawing and will slow down rendering if you crank it up too much. Obviously the two-canvas approach is the simplest and cheapest from a render performance perspective but it doesn't allow you to control other features like making the "particles" move erratically or apply physics to them!