Large numbers in HTML canvas translate result in strange behavior - javascript

I'm trying to figure out why, when I give large a large number to the x component of a translate() call on an HTML canvas, rectangles no longer seem to draw at the appropriate coordinates.
In the example below, the two rectangles that are drawn differ only by 1 in the x component. Therefore, I would expect their left edges to be only one pixel apart. As you can see in Firefox and Chrome, that is not what happens.
<html>
<head>
<script type="text/javascript">
function go() {
var canvas = document.getElementById("canvas");
if(canvas.getContext) {
var ctx = canvas.getContext("2d");
ctx.translate(-1000000000, -500);
ctx.fillStyle = '#F00'
ctx.fillRect(1000000033, 520, 100, 200);
ctx.fillStyle = '#00F';
ctx.fillRect(1000000032, 720, 100, 200);
}
}
</script>
</head>
<body onload="go();">
<canvas id="canvas" width="800" height="600">
</canvas>
</body>
</html>
Here's the same example ready to look at:
http://jsfiddle.net/pQst6/
I also made an animated version. http://jsfiddle.net/ry35e/ As you can see, the drawing starts normal, but as the x parameter to the translate method increases, it proceeds to have weirder and weirder behavior.
Both Firefox and Chrome appear to behave the same, so it seems like expected behavior, not a browser bug. I am guessing it is some kind of precision problem. Do you agree/disagree?
If it is some kind of precision problem with canvas, I am guessing I will have to find or implement my own transformation matrix and wrap it around the canvas context. I'm sure it wouldn't be very hard to make, but does anybody know of a javascript library that does such transformations (with pop, push, etc)?
Thanks!

The deviations occur when you invoke fillRect with the integer 2^24 (16 777 216) or greater. This also happens to be the largest integer you can express with single precision. The working draft states support for double precision (2^53) but apparently that's not the case (yet). The Mozilla Developer Network (MDN) documentation of the CanvasRenderingContext2D states support for only single precision (float).
Single precision: http://en.wikipedia.org/wiki/Single-precision
Double precision: http://en.wikipedia.org/wiki/Double-precision
Working draft: http://www.w3.org/TR/2dcontext/
MDN Docs: https://developer.mozilla.org/en/DOM/CanvasRenderingContext2D#fillRect()

Related

HTML5 canvas is flickering in Google Chrome

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.

How can I draw a circle on a canvas?

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.)

Html5 detecting draw string length with javascript

Casually i would detect the width and height of a string by doing this..
canvas.getContext("2d").font="12px Arial";
canvas.getContext("2d").fillStyle = "red";
canvas.getContext("2d").fillText("StackOverflow",x,y);
var width = canvas.getContext('2d').measureText(ct_string).width;
however, i need to calculate the width before i draw it into the canvas so i can do this..
canvas.getContext("2d").fillText("StackOverflow",x - width,y);
Would love some help on this, maybe i am not understanding the measureText fully, i believe it can only be used after the string has been drawn to the canvas though i am not certain.
( i am programming without a server and can not test the code i am running ).
Creating a second html5 canvas is out of the question.
I need a better understanding

Using HTML5's globalCompositeOperation "xor" with shape and text elements in Chrome

I have run into an issue when attempting to globalCompositeOperation to mask/blend shapes and text (shapes mask/blended with other shapes works just fine) in Chrome (more specifically I am using Chrome 12.0.7). Can anyone suggest where I might have gone astray here or suggest a workaround within the canvas element?
Here is an image showing what I'm seeing: http://i.stack.imgur.com/wRunv.jpg
Here is the code that will reproduce these results:
<!DOCTYPE HTML>
<html>
<body>
<canvas id="testCanvas" width="500" height="500"></canvas>
<script type="text/javascript">
// setup canvas
var tcanvas = document.getElementById("testCanvas");
var tcontext = tcanvas.getContext("2d");
// draw square
tcontext.fillStyle = "#FF3366";
tcontext.fillRect(15,15,70,70);
// set composite property
tcontext.globalCompositeOperation = "xor";
// draw text
tcontext.fillStyle="#0099FF";
tcontext.font = "35px sans-serif";
tcontext.fillText("test", 22, 25);
</script>
</body>
</html>
seems like the XOR globalCompositeOperation problem is a chrome bug that happens only with fillText.
Other drawing methods seem to work, see here: http://jsfiddle.net/Y5wvb/
You should report this bug to the Chromium project: http://code.google.com/p/chromium/issues/list
When you do, post the url of the posted issue here to we can vote it up :)
I found out that if you change the order of drawing, e.g. draw the text before filling the rectangle, the XOR works just fine. see here: http://jsfiddle.net/Y5wvb/1/

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