HTML5 canvas is flickering in Google Chrome - javascript

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.

Related

Curious bug (?) of HTML5 canvas rendering rectangles

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.

Blurry to pixelated custom Fade In

I'm seeking to create a custom fade in with jQuery or JS. I'd like the fade to be exaggerated and go from very blurry, to pixelated, to then the clear crisp image. On page load, one time.
This is for one image. Yes, tradditionaly done via making an animated gif I suppose. But can I write the effect with jQuery?
On page load. Grabbing the image #div >
Image loads blurry > then pixelates in > and then the clear original HQ image resolves.
To pixelate an image you can use canvas directly in a simple way like this (here assuming image has already been loaded):
/// get a block size (see demo for this approach)
size = blocks.value / 100,
w = canvas.width * size,
h = canvas.height * size;
/// draw the original image at a fraction of the final size
ctx.drawImage(img, 0, 0, w, h);
/// turn off image aliasing (see comment below)
ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false; /// future
/// enlarge the minimized image to full size
ctx.drawImage(canvas, 0, 0, w, h, 0, 0, canvas.width, canvas.height);
Here is an online demo.
You can use the slider at the bottom to see various sizes and the speed which would be needed for your transition (use Chrome to see live update when moving the slider) or simply hit the animate button.
As we turn off interpolation (image smoothing) the new enlarged image will appear blocky. This method is also faster than most other approached mainly due to image smoothing can now be turned off in the major browsers.
See this answer for a more complete instruction on how to turn smoothing off in most browsers:
Images in web browser are slightly blurred. How to disable it?
The last drawImage will use a clipped part of the source (here: canvas itself). See here for more details on how to use this method.
As for blur effect I would recommend you to pre-blur the image as this process has a large overhead and the pause will be noticeable on most common computers.
In case you still want to blur the image "live" you can use this stack-blur:
http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html
You could use Close Pixelate by David DeSandro. The Pixelator is a good example of it in action.

Canvas drawImage - visible edges of tiles in firefox/opera/ie (not chrome)

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.

Taking advantage of foreground and background canvases in HTML5

Right now I have one large canvas where everything is drawn. The canvas is created directly in the JS file:
var canvas = document.createElement ("canvas");
var ctx = canvas.getContext("2d");
canvas.width = document.width;
canvas.height = document.height;
document.body.appendChild(canvas);
It resizes itself to the browser window.
I have a gradient background being drawn onto the one canvas along with all the other elements. The colors for the background are randomly generated at each game mode change (eg when the main menu is toggled to the game, then the level end screen, etc). Currently I'm drawing these onto the canvas like this:
var grad1 = ctx.createRadialGradient(canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, 500);
grad1.addColorStop(0, bgGradStop1);
grad1.addColorStop(1, bgGradStop2);
ctx.fillStyle = grad1;
ctx.fillRect(0, 0, canvas.width, canvas.height);
I read numerous times that having a separate canvas for the background is better for performance. Is there anything specific that I need to do to make this effective or can I simply create another canvas and draw the background using the same exact code, only modified to use the other canvas, like this:
var grad1 = ctxBg.createRadialGradient(canvasBg.width / 2, canvasBg.height / 2, 0, canvasBg.width / 2, canvasBg.height / 2, 500);
grad1.addColorStop(0, bgGradStop1);
grad1.addColorStop(1, bgGradStop2);
ctxBg.fillStyle = grad1;
ctxBg.fillRect(0, 0, canvasBg.width, canvasBg.height);
Or does getting any performance benefit involve some totally different method of using the second canvas?
Also, if it really is just a matter of doing the exact same thing but on a different canvas, would there be any benefit to also for example moving the HUD text to its own canvas while other entities are moving? Even separating various types of entities onto their own canvases? How far does the benefit of multiple canvases actually stretch?
Or does getting any performance benefit involve some totally different method of using the second canvas?
You've got the right idea of how to do it, though I don't think a second canvas is necessary in your case.
The important thing from a performance perspective is that you don't want to have to keep constructing and filling the gradient. If all you're doing in your draw loop is:
ctx.fillStyle = grad1;
ctx.fillRect(0, 0, canvas.width, canvas.height);
Then you should be pretty swell here. I don't think having a second canvas in the background will help you much after that. There might be a slight performance boost, but who wants to have to keep track of additional DOM elements if you don't have to?
It's a little hard to test the performance of having a second canvas behind your main one, but instead of having two large canvases in the DOM one similar alternative is to draw the gradient an in-memory canvas and always call ctx.drawImage(inMemCan, 0, 0); instead of setting the gradient and drawing it. Drawing images is known to be fast, and using two canvases this way is probably close to the speed you could expect from two on-page canvases (and hopefully it would be faster).
So we could test drawing the gradient from an in-memory canvas to your main canvas versus drawing the plain old gradient. Here's a simple test with a large canvas:
http://jsperf.com/gradient-vs-immemcan
They're pretty equal it seems. If this one thing is the only thing in your background I wouldn't worry about it. There are probably bigger performance fish for you to fry first.
I'd save the benefit of multiple canvases for when a relatively complicated background updates rarely but independently of the foreground. If your background was instead made with 30 gradients and some paths, then using a second canvas (or an in-memory canvas to cache an image) would give you a sizable boost.
It resizes itself to the browser window.
Just a reminder that the full screen API works pretty well in webkit and firefox if you want to look into it in the future.

html canvas motion blur with transparent background

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!

Categories

Resources