I'm trying to draw a huge canvas rectangle on top of the page (some kind of lightbox background), the code is quite straightforward:
var el = document.createElement('canvas');
el.style.position = 'absolute';
el.style.top = 0;
el.style.left = 0;
el.style.zIndex = 1000;
el.width = window.innerWidth + window.scrollMaxX;
el.height = window.innerHeight + window.scrollMaxY;
...
document.body.appendChild(el);
// and later
var ctx = el.getContext("2d");
ctx.fillStyle = "rgba(0, 0, 0, 0.4)";
ctx.fillRect(0, 0, el.width, el.height);
And sometimes (not always) the last line throws:
Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsIDOMCanvasRenderingContext2D.fillRect]
I've been guessing if that happens because of image size or because of the content type beneath the canvas (e.g. embeded video playing), but apparently not.
So I'm looking for any ideas on how to isolate and/or solve this issue.
Looking at the nsIDOMCanvasRenderingContext2D.fillRect() implementation (and going through the functions it calls) - there aren't all too many conditions that will return NS_ERROR_FAILURE. It can only happen if either EnsureSurface() or mThebes->CopyPath() fail. And the following two lines in EnsureSurface() are most likely the source of your issue:
// Check that the dimensions are sane
if (gfxASurface::CheckSurfaceSize(gfxIntSize(mWidth, mHeight), 0xffff)) {
What's being checked here:
Neither the width nor the height of the canvas can exceed 65535 pixels.
The height cannot exceed 32767 pixels on Mac OS X (platform limitation).
The size of canvas data (width * height * 4) cannot exceed 2 GB.
If any of these conditions is violated EnsureSurface() will return false and consequently produce the exception you've seen. Note that the above are implementation details that can change at any time, you shouldn't rely on them. But they might give you an idea which particular limit your code violates.
You could apply a try-catch logic. Firefox seems to be the only browser which behaves this a bit odd way.
el.width = window.innerWidth + window.scrollMaxX;
el.height = window.innerHeight + window.scrollMaxY;
// try first to draw something to check that the size is ok
try
{
var ctx = el.getContext("2d");
ctx.fillRect(0, 0, 1, 1);
}
// if it fails, decrease the canvas size
catch(err)
{
el.width = el.width - 1000;
el.height = el.height - 1000;
}
I haven't found any variable that tells what is the maximum canvas size. It varies from browser to browser and device to device.
The only cross browser method to detect the maximum canvas size seems to be a loop that decreases the canvas size eg. by 100px until it doesn't produce the error. I tested a loop, and it is rather fast way to detect the maximum canvas size. Because other browsers doesn't throw an error when trying to draw on an over sized canvas, it is better to try to draw eg. red rect and read a pixel and check if it is red.
To maximize detection performance:
- While looping, the canvas should be out of the DOM to maximize speed
- Set the starting size to a well known maximum which seems to be 32,767px (SO: Maximum size of a <canvas> element)
- You can make a more intelligent loop which forks the maximum size: something like using first bigger decrease step (eg.1000px) and when an accepted size is reached, tries to increase the size by 500px. If this is accepted size, then increase by 250px and so on. This way the maximum should be found in least amount of trials.
Related
I need to scale lots of text nodes in the browser (support of all modern desktop and mobile browsers).
If I am right there are two options that offer good performance: scaling text objects in Canvas or scaling text nodes in the DOM using transform:matrix.
I have created a scenario to test both versions but the results are inconclusive. Uncomment testDOM() or testCanvas() function to start the test. (I am using JQuery and CreateJS framework because it was convenient. It is possible to use vanilla JS but I don't think that is the bottleneck here). (It matters what portion of the screen you actually see so please switch to full screen view in codepen).
http://codepen.io/dandare/pen/pEJyYG
var WIDTH = 500;
var HEIGHT = 500;
var COUNT = 200;
var STEP = 1.02;
var MIN = 0.1;
var MAX = 10;
var stage;
var canvas;
var bg;
var canvasTexts = [];
var domTexts = [];
var domMatrix = [];
var dom;
function testDOM() {
for (var i = 0; i < COUNT; i++) {
var text = $("<div>Hello World</div>");
var scale = MIN + Math.random() * 10;
var matrix = [scale, 0, 0, scale, Math.random() * WIDTH, Math.random() * HEIGHT];
text.css("transform", "matrix(" + matrix.join(',') + ")");
domTexts.push(text);
domMatrix.push(matrix);
}
dom = $('#dom');
dom.append(domTexts);
setTimeout(tickDOM, 1000);
}
function tickDOM() {
for (var i = 0; i < domTexts.length; i++) {
var text = domTexts[i];
var matrix = domMatrix[i];
var scale = matrix[0];
scale *= STEP;
if (scale > MAX)
scale = MIN;
matrix[0] = matrix[3] = scale;
text.css("transform", "matrix(" + matrix.join(',') + ")");
}
requestAnimationFrame(tickDOM);
}
function testCanvas() {
$('#dom').hide();
stage = new createjs.Stage('canvas');
createjs.Touch.enable(stage);
createjs.Ticker.timingMode = createjs.Ticker.RAF;
canvas = stage.canvas;
devicePixelRatio = window.devicePixelRatio || 1;
stage.scaleX = devicePixelRatio;
stage.scaleY = devicePixelRatio;
console.log('devicePixelRatio = ' + devicePixelRatio);
stage.mouseMoveOutside = true;
stage.preventSelection = false;
stage.tickEnabled = false;
stage.addChild(bg = new createjs.Shape());
bg.graphics.clear();
bg.graphics.f('#F2F2F2').drawRect(0, 0, 2 * WIDTH, HEIGHT);
canvas.width = 2 * WIDTH * devicePixelRatio;
canvas.height = HEIGHT * devicePixelRatio;
canvas.style.width = 2 * WIDTH + 'px';
canvas.style.height = HEIGHT + 'px';
stage.update();
for (var i = 0; i < COUNT; i++) {
var text = new createjs.Text("Hello World", "10px", "#333333");
text.scaleX = text.scaleY = MIN + Math.random() * 10;
text.x = Math.random() * WIDTH;
text.y = Math.random() * HEIGHT;
stage.addChild(text);
canvasTexts.push(text);
}
stage.update();
setTimeout(tickCanvas, 1000);
}
function tickCanvas() {
for (var i = 0; i < canvasTexts.length; i++) {
var text = canvasTexts[i];
text.scaleX = text.scaleY *= STEP;
if (text.scaleX > MAX)
text.scaleX = text.scaleY = MIN;
}
stage.update();
requestAnimationFrame(tickCanvas);
}
testDOM();
//testCanvas();
My questions:
Is it possible to improve the performance of my tests? Am I doing something wrong?
The first 5-10 seconds are significantly slower but I don't understand why. Does the browser somehow cashes the text objects after some time? If yes, is the test unusable for real world scenario testing where the objects don't zoom in a loop for longer period of time?
According to the Chrome Profiling tool the DOM version leaves 40% more idle time (is 40% more faster) than the Canvas version but the Canvas animation looks much smoother (after the initial 5-10 seconds of lagging), how should I interpret the Profiling tool results?
In the DOM version I am trying to hide the parent of the text nodes before I apply the transformations and then unhide it but it probably does not matter because transform:matrix on absolutely positioned element does not cause reflow, am I right?
The DOM text nodes have some advantages over the Canvas nodes like native mouse over detection with cursor: pointer or support for decorations (you can not have underlined text in Canvas). Anything else I should know?
When setting the transform:matrix I have to create a string that the compiler must to parse back to numbers, is there a more efficient way of using transform:matrix?
Q.1
Is it possible to improve the performance of my tests? Am I doing
something wrong?
Yes and no. (yes improve and no nothing inherently wrong (ignoring jQuery))
Performance is browser, and device dependent, for example Firefox handles objects better than arrays, while Chrome prefers arrays. There is a long list of differences just for the javascript.
Then the rendering is a dependent on the hardware, How much memory, what capabilities, and the particular drivers. Some hardware hates state changes, while others handle them at full speed. Limiting state changes can improve the speed on one machine while the extra code complexity will impact devices that don't need the optimisation.
The OS also plays a part.
Q.2
The first 5-10 seconds are significantly slower but I don't understand
why. Does the browser somehow cashes the text objects after some time?
If yes, is the test unusable for real world scenario testing where the
objects don't zoom in a loop for longer period of time?
Performance testing in Javascript is very complicated and as a whole application (like your test) is not at all practical.
Why slow?
Many reasons, moving memory to the display device, javascript optimising compilers that run while the codes runs and will recompile if it sees fit, this impacts the performance Un-optimised JS is SLOOOOOWWWWWWWW... and you are seeing it run unoptimised.
As well. In an environment like code pen you are also having to deal with all its code that runs in the same context as yours, it has memory, dom, cpu, GC demands in the same environment as yours and thus your code can not be said to be isolated and profiling results accurate.
Q.3
According to the Chrome Profiling tool the DOM version leaves 40% more
idle time (is 40% more faster) than the Canvas version but the Canvas
animation looks much smoother (after the initial 5-10 seconds of
lagging), how should I interpret the Profiling tool results?
That is the nature of requestAnimationFrame (rAF), it will wait till the next frame is ready before it calls your function. Thus if you run 1ms past 1/60th of a second you have missed the presentation of the current display refresh and rAF will wait till the next is due 1/60th minus 1ms before presentation and the next request is called. This will result in ~50% idle time.
There is not much that can be done than make you render function smaller and call it more often, but then you will get extra overhead with the calls.
rAF can be called many times during a frame and will present all renders during that frame at the same time. That way you will not get the overrun idle time if you keep an eye on the current time and ensure you do not overrun the 1/60th second window of opportunity.
Q.4
In the DOM version I am trying to hide the parent of the text nodes
before I apply the transformations and then unhide it but it probably
does not matter because transform:matrix on absolutely positioned
element does not cause reflow, am I right?
Reflow will not be triggered until you exit the function, hiding the parent at the start of a function and then unhiding it at the end will not make much difference. Javascript is blocking, that means nothing will happen while you are in a function.
Q.5
The DOM text nodes have some advantages over the Canvas nodes like
native mouse over detection with cursor: pointer or support for
decorations (you can not have underlined text in Canvas). Anything
else I should know?
That will depend on what the intended use is. DOM offers a full API for UI and presentation. Canvas offers rendering and pixel manipulation. The logic I use is if it takes more code to do it via DOM then canvas, then it is a canvas job and visa versa
Q.6
When setting the transform:matrix I have to create a string that the
compiler must to parse back to numbers, is there a more efficient way
of using transform:matrix?
No. That is the CSS way.
I hava JS script that takes an image and draws it to a canvas. The code works perfectly fine in all browsers except Chrome. I have included the relevant
this.tile = document.createElement('canvas');
var ctx = this.tile.getContext("2d");
this.tile.width = ((size.cm_size) * (((this.image.width) / this.app.dpi.active) * 2.54)).toFixed(2);
this.tile.height = ((size.cm_size) * (((this.image.height) / this.app.dpi.active) * 2.54)).toFixed(2);
if (this.scale > 0) {
// These 2 lines break it in Chrome.
this.tile.width = this.tile.width * this.scale;
this.tile.height = this.tile.height * this.scale;
}
ctx.drawImage(this.image, 0, 0, this.image.width, this.image.height, 0, 0, this.image.width, this.image.height);
I have commented the 2 lines that break things in Chrome. Removing these lines makes things work.
With these lines in place, nothing is displayed until I increase the 'this.app.dpi.active' variable to 350 (it starts at 150). As soon as this happens it displays how it does in Firefox.
There are no error messages, the tile is just empty.
Here's a list of values for 'this.app.dpi.active' and the resulting tile width and height. The tile doesn't display until the value DPI hits 350.
150 DPI = 9444 x 9444
200 DPI = 7084 x 7084
250 DPI = 5668 x 5668
300 DPI = 4720 x 4720
350 DPI = 4048 x 4048
I have checked these vales in Chrome and Firefox and they are identical but things work in Firefox. The this.scale value = 4 and when the width and height aren't multipled by this.scale, it works fine in FF and Chrome.
Please let me know if you need any more code or have any questions.
I don't think you can use the Element constructor. I've always used document.createElement('canvas'). In my FF console, using the Element constructor throws an error.
Okay, I figured this one out myself but thank you everyone for your input!
I noticed that once an image got to a certain size, Chrome display it correctly. This led me to believe that Chrome doesn't allow canvas elements over a certain size (probably for memory/efficiency reasons). To fix this I re-designed my system slightly so that canvas elements never get too big and therefore always displayed correctly. This fixed the issue in Chrome and made my system a fair bit quicker.
I remove some pixel with clearRect on mouse move on my 200x200px canvas element.
Now, I would like to check if there are no pixels left to remove (all 40.000px are removed), then reset or load a new image.
canvas.onmousemove = function(e) {
x = e.clientX - e.target.offsetLeft;
y = e.clientY - e.target.offsetTop;
context.clearRect(x, y, 20, 20);
}
There are two ways to detect if the canvas is blank:
Compare data-uris
Initially grab a string from the blank canvas before starting to draw to it. If you change the size of the canvas you also need to update this string.
var blankCanvas = canvas.toDataURL('image/bmp');
This will get a BMP (uncompressed) image in browsers which support this format, or default to PNG if not. Obtaining an uncompressed image will increase memory use but also performance when obtaining the string (but slow down when comparing it, if large).
Then do the same when you want to compare:
var currentCanvas = canvas.toDataURL('image/bmp');
if (currentCanvas === blankCanvas) {
/* it's empty */
}
Compare pixels
The only way is to iterate through the pixels to see if all the values are black transparent.
Here is one way: call this method every time you need to check (typically on mouseup event) -
function hasPixels() {
var idata = context.getImageData(0, 0, canvas.width, canvas.height),
buffer = new Uint32Array(idata.data.buffer), // use 32-bit buffer
len = buffer.length,
i;
for(; i < len; i++) {
if (buffer[i] !== 0) return true;
}
return false;
}
This will return true if there are any pixels remaining on canvas.
The 32-bit buffer allows us to check one complete pixel at the time and will perform better that comparing each component.
Notes
Both approaches require CORS requirements in relation to the images be fulfilled for this to work. This is a security mechanism in browsers preventing extraction of pixels from canvas cross-origin.
Which approach is faster depends on various factors such as the size of the canvas, browser implementation and so forth. You would need to check for your scenario. I would perhaps add a coin to the second solution for general usage together with the benefit of low resource consumption. But for a 200x200 canvas both should work well.
I do know about the case of float/integer values for drawImage's x and y. But what I need is a smooth animation with an ability to cache my shapes.
Article on caching complex paths with backup canvas
Article on drawImage with float parameters
For example, I want to draw some complex shape (i.e. SVG-tiger, converted to canvas-commands) to canvas just once and then move it smoothly with ctx.translate and ctx.drawImage. I need the float values then, because instead I get a step-by-step moving:
Here's the examples at JSFiddle:
One: Fast speed, with Math.floor applied to translate parameters (x and y are equal to time in seconds multiplied by 10): Animation is weird (sequential, not smooth).
Two: Slow speed, with Math.floor applied to translate parameters (x and y are equal to time in seconds): Animation is weird (sequential, not smooth).
Three: Fast speed, no rounding, float values (x and y are equal to time in seconds multiplied by 10). Speed is fast, so animation looks good.
Four: Slow speed, no rounding, float values (x and y are equal to time in seconds). Speed is slow, so animation looks pulsating. Why?
The last case is the one that confuses me. Am I wrong in my tryings and there is a possibility to make this caching trick work nice?
In Firefox, there is a property of canvas named mozImageSmoothingEnabled (see), but there is no help from that in other browsers. And it also removes paths smoothing.
Code extract:
var shapeCanvas = null;
var w = 320, h = 240;
var startTime = 0;
function start() {
startTime = Date.now();
var docCanvas = document.getElementById('canvas');
. . .
shapeCanvas = document.createElement('canvas');
. . .
drawShape(shapeCanvas.getContext('2d'));
drawNext(docCanvas.getContext('2d'));
}
function drawNext(ctx) {
var msec = (Date.now() - startTime);
var time = msec / 1000; // seconds passed from start
ctx.clearRect(0, 0, w, h);
ctx.save();
// the lines to change: time | (time * 10) | Math.floor(time * 10)
ctx.translate((time < 500) ? Math.floor(time * 10) : 500,
(time < 500) ? Math.floor(time * 10) : 500);
ctx.drawImage(shapeCanvas, 0, 0);
ctx.restore();
__nextFrame(function() {
drawNext(ctx);
});
}
function drawShape(ctx) {
. . .
}
I wrote the tutorial in your first link.
Just to clear the air:
shapeCanvas.style.width = w + 'px';
shapeCanvas.style.height = h + 'px';
is not really worth doing. No point setting the style if its just a in-memory canvas, and you shouldn't really ever want to set the width and height style of a canvas anyway, it just confounds things.
What ellisbben said in the comment is pretty much what's happening.
It's possible to get around it in a few hackish ways I bet. One way might be to make sure its never drawn on an integer pixel. Another might be to use ctx.scale(.99,.99) before drawing anything so it is always anti-aliased. It's tough to get a consistent solution here because different browswer's implementations of anti-aliasing are different.
Here are a few experiments from myself:
http://jsfiddle.net/KYZYT/29/
The first two are the shape drawn from a canvas and also drawn from a PNG
The second two are the same pair but scaled by .99,.99
The last one is the real thing. It still blurs a bit but looks a lot sharper than using the images.
None of my experiments lead to an end of your pulsating, at least not on a microscopic level. I think this is just something you're going to have to live with if you want to animate pixel-perfect images onto half-pixel spaces.
If you really feel you can't just draw on perfect pixels then your (second) best bet for consistency is probably to find a way to force anti-aliasing at all times. Making sure you are always translated to a non-integer or scaling it ever so slightly are decent candidates, but there may be others.
To be honest, you best bet is to not cache these animated paths until you absolutely need the performance from them. Cache the stylized buttons and other static things you make, but if you've got animated paths that need to move slowly and precisely and look very good, you might as well stick with the true thing over my caching optimization unless you really need it for those too.
Bit shameless plug but: I've implement smooth animation in HTML5 slot game with bit hacky way. The generated cached Image is drawn on small canvas once and then I used translate3d() with -moz-transform / -webkit-transform styles for the canvas to move, mirror and scale the image around.
Pregeneration
Create Image
Draw image content
Create canvas object in DOM
Animation phase
Clear canvas
Draw the cached image to the canvas
Use CSS3 transforms (scale3d and translate3d) to move canvas around.
I'm trying to put together a tool that checks whether a given character is displayed in the specified style font or a system default font. My ultimate goal is to be able to check, at least in modern (read: IE8+) browsers, whether a ligature is supported in a given font.
I've got two canvases displaying the same ligature (in this case, st). I turn those canvases into data and compare them to see if the characters match.
Arial (like most fonts) does not have an st ligature, so it falls back to the default serif font. Here's where it gets weird: although they're displaying the same font, the two canvases don't have the same data.
Why? Because their positions on the canvas aren't exactly the same. I'm guessing it has something to do with the different relative heights of the fonts (one is slightly taller than the other, though which varies font to font). The difference appears to be one of a pixel or two, and it varies font by font.
How might one go about solving this? My only current idea is finding a way to measure the height of the font and adjusting its position accordingly, but I have no idea how to do that, unfortunately. Are there other approaches I might take to make the two images identical?
You can see the code below. Both canvases are successfully initialized and appended to the body of the element so I can see what's going on visually (though that's not necessary in the actual script I'm working on). I've dropped the initialization and context, as those are all working just fine.
function checkLig() {
lig = 'fb06' // this is the unicode for the st ligature
canvas0.width = 250;
canvas0.height = 50;
context0.fillStyle = 'rgb(0, 0, 0)';
context0.textBaseline = 'top';
context0.font = 'normal normal normal 40px Arial';
context0.fillText(String.fromCharCode(parseInt(lig, 16)), 0, 0);
var print0 = context0.getImageData(0, 0, 720, 50).data;
canvas1.width = 250;
canvas1.height = 50;
context1.fillStyle = 'rgb(0, 0, 0)';
context1.textBaseline = 'top';
context1.font = 'normal normal normal 40px serif';
context1.fillText(String.fromCharCode(parseInt(lig, 16)), 0, 0);
var print1 = context1.getImageData(0, 0, 720, 50).data;
var i = 0, same = true, len = 720 * 50 * 4;
while (i < len && same === true) {
if (print0[i] !== print1[i]) {
same = false;
}
else {
i++;
}
}
return same;
}
So i understand the question correctly, the problem is one canvas is specifying Arial (but falls back to Serif) and the other is Serif and when you do the pixel matching, and it's not a match because one of them has a slight offset?
One suggestion is to grab a reference pixel from each canvas and compare the positions of the two reference pixels to get an offset. And then factor that offset into your comparison loop.
For example, in this case you could get your reference pixel by starting to check pixels from the upper left corner of one canvas and scan downwards in that column and when you reach the bottom, go back to the top of the next column and scan down.
As soon as you hit a pixel that is not the color of your background, record the position and use that as your reference pixel. That should be the edge of your font.
Do the same with your next canvas and then compare the positions of the two reference pixels to get an offset. Take that offset into consideration in your comparison loop.
Hope I have the right idea of the problem and i hope that helps!