I am playing around with multiple clipped shapes on a canvas like this:
But (only in Chrome), if you increase the width or height of that canvas element by even 1px, it doesn't render all the shapes.
Any ideas? Have a look at the jsfiddle:
https://jsfiddle.net/entozoon/6fqq0567/
The code is pretty straight forward:
for (i=1;i<=5;i++) {
ctx.save();
// clipping mask
ctx.beginPath();
ctx.arc(50 * i, 50, 20, 0, Math.PI * 2, true);
ctx.closePath();
ctx.clip();
// shape to be clipped
ctx.beginPath();
ctx.fillStyle = "red";
ctx.arc(50 * i, 70, 20, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
ctx.restore();
}
-- UPDATE --
It turns out that the problem is to do with 2D canvas acceleration. If I disable
'Accelerated 2D canvas' in chrome://flags that fixes it.
This is definitely NOT a solution though!
Must be a graphics issue..? (yes I have up-to-date drivers, chrome, etc.)
I have the same issue on Win10, chrome 50.0.2661.75 m. The issue started when I updated from 49, and is not reproducible in all computers with the same OS and Chrome version configurations, so probably it has to do with graphics hardware, I got an AMD Radeon HD6570. But on latest canary chrome 52.0.2713.0, the issue is not reproducible, so I can deduce that is a hardware handling issue brought on chrome 50 version. (I can't comment the question, so I wrote here some extra info that can be useful)
Related
Ok, so i am making a 3D rendering engine in pure javascript, as a challenge of course - to test my linear algebra skills. I am not using webgl, so please do not say "use webgl".
Anyways, the software will take in triangles, a camera and local transformations, and render the data onto the screen (i even made it interactive)
There are only 6 lines of rendering code, however, which are:
// some shading and math calculations then this:
context.fillStyle = color;
context.strokeStyle = color;
context.beginPath();
context.moveTo(x0, y0);
context.lineTo(x1, y1);
context.lineTo(x2, y2);
context.lineTo(x0, y0);
context.closePath();
context.fill();
context.stroke();
And while that works, it drops to 10fps with 4k+ faces on my Chromebook. (60fps on a regular computer)
Anyways, that outputs this:
But to make it faster, and because canvas state changes are slow, i removed the stroke, making the rendering code:
// some shading and math calculations then this:
context.fillStyle = color;
//context.strokeStyle = color;
context.beginPath();
context.moveTo(x0, y0);
context.lineTo(x1, y1);
context.lineTo(x2, y2);
context.lineTo(x0, y0);
context.closePath();
context.fill();
//context.stroke();
which runs twice as fast, but the resulting thing that gets rendered to the screen is this: (different model)
which has ugly lines everywhere at the edges of the triangles (which get removed when I re-add the stroke)
However, the fps doubles and performance gains are great...
So i believe the lines are caused because the canvas fill doesnt include the area where it would have stroked (the outline, as you may say).
I have tried to fix it with math, and although it works there are some edge cases where it doesn't
So my question is as follows:
Is there a way to make the context fill include the stroke area without stroking, because it is very expensive?
Using both stroke and fill will force the rasterization twice which explains the approximate double time.
The reason why you get glitches between the triangles is because of rounding errors and anti-aliasing. There is not a straight-forward solution to this; the stroke will cover the glitches of course, but to do it without the stroke will require you to offset and expand at least every other triangle.
However, you could use a small trick to cover up the gap and that is to redraw the entire image (as bitmap) on top offset just a single pixel (you might get away with 0.5 pixel but then anti-aliasing is needed). This adds to the time, but far less than rasterization or recalculation of the paths.
Say that the result on the left is what you have (simulated here) with a clear gap. Redrawing it on top as shown in the right will cover the gap without too much distortion.
Simply use:
ctx.drawImage(sourceCanvas, 1, 1);
Tip: when only calling fill() you don't need closePath() as it is called implicit, saving one op. Microscopic gain perhaps but still (with more complex geometry it even might have an influence :) ).
Note: drawing to itself will cause an internal allocation of a temporary bitmap copy. However, you will only need to do one extra drawImage() operation. The option is to use off-canvas render but draw twice to a main displayed canvas. Either way...
var ctx = c.getContext("2d");
ctx.fillStyle = "#777";
tri(10,10, 72,17, 40.2, 100);
// simulates gap
ctx.fillStyle = "#222";
tri(72.5,17.5, 40.7,100.5, 90,25);
// fill entire image back again, drawn twice here for demo
ctx.drawImage(c, 100, 0);
ctx.drawImage(c, 0, 0, 100, 150, 101, 1, 100, 150);
ctx.fillText("Raster", 5, 8);
ctx.fillText("Offset self", 105, 8);
function tri(x0,y0,x1,y1,x2,y2) {
ctx.beginPath();
ctx.moveTo(x0, y0);
ctx.lineTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.fill();
}
<canvas id=c></canvas>
EDIT: This bug was fixed in version 38.
A recent version of Chrome introduced an issue in an application I maintain. I'm not sure if this is one of those weird "seems wrong but is actually correct" issues or if it's an honest-to-god bug, but it only presents in recent versions of Chrome (it started happening about a month ago, I'm not sure exactly which version introduced it)
The bug presents when using the context fill() method on certain paths that are drawn using the context arc() method. Rather than drawing a filled arc, what is filled is an oddly-shaped polygon.
Here's a demonstration of what I mean -- the shape in the upper right should be a filled arc:
var ctx = document.getElementById('cvs').getContext('2d');
// draw stroked arc
ctx.beginPath();
ctx.arc(75, 75, 50, 0, Math.PI/2);
ctx.lineTo(125, 125);
ctx.closePath();
ctx.stroke();
// draw filled arc
ctx.beginPath();
ctx.arc(225, 75, 50, 0, Math.PI/2);
ctx.lineTo(275, 125);
ctx.closePath();
ctx.fill();
// draw stroked triangle
ctx.beginPath();
ctx.moveTo(125, 225);
ctx.lineTo(75, 275);
ctx.lineTo(125, 275);
ctx.closePath();
ctx.stroke();
// draw filled triangle
ctx.beginPath();
ctx.moveTo(275, 225);
ctx.lineTo(225, 275);
ctx.lineTo(275, 275);
ctx.closePath();
ctx.fill();
<div><canvas id="cvs" width="300" height="300" style="border: solid black 1px"></canvas></div>
My question is this: is there a workaround for this issue? Preferably one that doesn't require me to write my own filled-arc renderer.
I do see this bug on Chrome 37.0.2062.124 on OS X. This may or may not be related to the bug described here, which is supposedly to be fixed in Chrome 38.
As a workaround, rotating a few degrees and immediately rotating it back before filling the arc seems to work.
// draw filled arc
ctx.beginPath();
ctx.arc(225, 75, 50, 0, Math.PI/2);
ctx.lineTo(275, 125);
ctx.closePath();
ctx.rotate(1*Math.PI/180); // Rotate 1 degree
ctx.rotate(-1*Math.PI/180); // Reverse rotation
ctx.fill();
Here's a fiddle demonstrating the workaround: http://jsfiddle.net/ejacpd1w/1/
In putting together a small canvas app I've stumbled across a weird behavior that only seems to occur in the default browser in Android.
When drawing to a canvas that has the globalCompositeOperation set to 'destination-out' to act as the 'eraser' tool, Android browser sometimes acts as expected, sometimes does not update the pixels in the canvas at all.
the setup:
context.clearRect(0,0, canvas.width, canvas.height);
context.drawImage(img, 0, 0, canvas.width, canvas.height);
context.globalCompositeOperation = 'destination-out';
draw a circle to erase pixels from the canvas:
context.fillStyle = '#FFFFFF';
context.beginPath();
context.arc(x,y,25,0,TWO_PI,true);
context.fill();
context.closePath();
a small demo to illustrate the issue can be seen here:
http://gumbojuice.com/files/source-out/
and the javascript is here:
http://gumbojuice.com/files/source-out/js/main.js
this has been tested in multiple desktop and mobile browsers and behaves as expected. On Android native browser after refreshing the page sometimes it works, sometimes nothing happens.
I've seen other hacks that move the canvas by a pixel in order to force a redraw but this is not an ideal solution..
Thanks all.
I did something like this, which forces the detachment of the canvas:
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (isStockAndroid) {
canvas.style.display = "none";
canvas.offsetHeight;
canvas.style.display = "block";
}
That seems to be the most efficient as far as FPS is concerned. Otherwise it's the not-so-nice:
canvas.width = canvas.width;
...which seemed to also get it all working normally for me. Haven't tested to see if the first is essentially the same as the second and resets canvas settings, though, but it seems to be getting a higher frame rate? Anyway that definitely clears things. For the native detection stuff try here: How to detect only the native Android browser
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.
So I have a rotating canvas element which has an arc drawn inside it (the smaller planet):
http://jsfiddle.net/neuroflux/9L689/4/ (updated)
But I can't seem to get the anti-aliasing on the edges of the smaller planet smoother - any ideas?
Cheers!
edit: is there a way to increase the number of iterations used within an arc?
Your problem is not that the arc doesn't have enough points, but that in Chrome the .clip() operation doesn't use anti-aliasing to produce the clipping path.
See Chromium Issues 7508 and 132442
To see this in action, look at http://jsfiddle.net/alnitak/YMtdZ/ in Chrome.
markup:
<canvas id="c" width="600" height="300" />
code:
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
ctx.fillStyle = 'black';
ctx.save();
ctx.beginPath();
ctx.arc(150, 150, 140, 0, 2 * Math.PI);
ctx.clip();
ctx.fillRect(0, 0, 600, 300);
ctx.restore();
ctx.beginPath();
ctx.arc(450, 150, 140, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
The left-hand circle is drawn with clipping, and is aliased. The right-hand circle is drawn "normally", and is anti-aliased.
FWIW, in Firefox and Safari both images look the same. I can't test it on IE.
The only work around I can imagine (until Chrome gets fixed) would be to render the image off-screen into a canvas 3 or 4 times larger, and then copy that with down-sampling into the displayed canvas.