I have got a canvas and I display image using it via the putImageData() method after updating bytes in clamped array got by getImageData.data.
That works nicely, but I need to scale *2 in Y-direction.
Of course I can do it without a question here too by repeating every lines twice, but it requires too much time to render a frame then (I display 25 frames per second). I've read about the ability to scale image with drawImage() method. The only problem that as far as I know, using drawImage() is slower than using putImageData(), and it was required in old browser versions like Firefox/2 (or such).
How can I upscale the image in Y direction twice as fast as possible?
By the way, is it possible to get similar solution to gain full screen resolution somehow (a flash - not JS - example: like what youtube does when you go to fullscreen)?
If you want the whole canvas to scale, you can do this without any extra javascript. Just set a size in CSS that has twice the width and height specified in the canvas.
<canvas width="200" height="100" style="width:200; height: 200">
See the W3 spec.
Edit:
Looking at the question linked by Alnitak below: If you want nearest-neighbor scaling rather than antialiasing you can do this:
canvas {
image-rendering: -moz-crisp-edges;
image-rendering: -o-crisp-edges;
image-rendering: -webkit-optimize-contrast;
-ms-interpolation-mode: nearest-neighbor;
}
You can achieve vertical scaling (without anti-aliasing) by just grabbing one image row at a time with getImageData and then replicating that row multiple times with putImageData, e.g.:
var oy = 0;
for (var y = 0; y < sh; ++y) {
var data = src_ctx.getImageData(0, y, sw, 1);
for (var n = 0; n < scale; ++n) {
dst_ctx.putImageData(data, 0, oy++);
}
}
See http://jsfiddle.net/alnitak/hYZ3U/ for a worked example that completes in 3ms on my machine.
Related
I need to build a kind of map in canvas, which must be able to hold more than 10.000 elements and thus has quiet big dimensions in some cases (> 8000px width, >4000 px height). Also I need to pan and zoom the map.
After some fiddeling around with existing libraries (Paper.js) and possible other solutions (Leaflet Map) I eventually wrote an own library from scratch, because the main requirement is, that is should be really really fast (loading, mouseovers, ...) and none of the libraries I tried could offer all of the aspects.
The structure is as follows:
I have one map object with an associated Control object, which registers events and has resize methods etc.
A map is divided in mutliple even sized tiles (1024px x 1024px - customizable) because using the map with only one canvas at a size over 8000px width made it incredibly slow
Each tile is associated with a canvas
The elements (just circles) are added to one or multiple tiles (If it's on the edge) - more specifically to the tiles' canvas.
The tiles are placed within an container div which has the dimensions of the map area (when not zoomed out)
The container div is placed within a viewport div to enable the map being displayed as a "widget"
Zooming scales every tile/canvas and the container. For sake of performance I sacrificed smooth zoom and implemented a customizable amount of zoom steps, which still feels okay.
Panning set's the topand left style of the container.
Events used are window.resize, mousewheel, DOMMouseScrol, mousedown, mouseup, mousemove, touchstart,touchend,touchmove and Hammertime pinch
This alltogether runs satisfying on Desktop Browsers, and iPhones (tested with SE, 6S) but on every Android device I tested it (Samsung S4, One Plus One and another 1 year old device, and android studio emulator) it runs extremly slow. Drawing of the Map is fine in speed, but zooming and panning is near to impossible.
The code is too comprehensive to post it here, so I'm asking you if there are any known problems with canvas on android, that could explain this problem, or maybe some issues with the way I built the structure that could produce issues with android. I'm really clueless here, since it works on desktop and iPhone.
The real problem you're hitting is you're overloading the GPU. Loading that much data all and once then moving it around is going to put a toll on the GPU and likely force the browser into software rendering mode, which is a big performance hit.
Instead, I'd suggest changing your approach. Rather than having various large canvases, you should have one canvas that is, at most, the size of the users screen. Then, utilize methods of the canvas API such as scale and translate to render what you need. For an added bonus, avoid trying to render things which are off screen.
It may seem like having to redraw the scene every time you move around would be slow but it's not. The reality is that either you specify exactly what needs to be drawn or the browser has to attempt to draw all of it again when you shift it around. Here's a brief example of how you can render and move large images.
var ctx = document.querySelector('canvas').getContext('2d');
var img = new Image();
img.src = 'https://placeimg.com/1000/1000/nature';
img.onload = start;
function start() {
var xDirection = -1;
var yDirection = -1;
var xPosition = 0;
var yPosition = 0;
var prev = Date.now();
(function render() {
var now = Date.now();
var delta = (now - prev) / 1000;
xPosition += xDirection * delta * 20;
yPosition += yDirection * delta * 40;
if (xPosition > 0) {
xPosition = 0;
xDirection *= -1;
} else if (xPosition < -320) {
xPosition = -320;
xDirection *= -1;
}
if (yPosition > 0) {
yPosition = 0;
yDirection *= -1;
} else if (yPosition < -240) {
yPosition = -240;
yDirection *= -1;
}
prev = now;
ctx.save();
ctx.translate(xPosition, yPosition);
ctx.drawImage(img, 0, 0);
ctx.restore();
requestAnimationFrame(render);
})();
}
body {
background: #111;
}
canvas {
background: #FFF;
}
<canvas width="320" height="240"></canvas>
I've been playing with canvas element and discovered that when I attempt to draw NxN uniform solid-colored cells next to each other, in some width/height configurations, there are blurry white-ish lines between them.
For instance, this canvas is supposed to look black but contains some sort of grid which I conjecture to be a result of faulty antialiasing in the browser.
Suffice to say, this bug appears only in some configurations but I would like to get rid of it for good. Is there any way to circumvent this? Have you ever had problems with antialiasing in canvas?
I have made this fiddle which demonstrates the issue and allows you to play with the dimensions of the canvas and number of cells. It also contains the code I use to draw the cells, so that you can inspect it and tell me if I'm doing anything wrong.
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
for (var i = 0; i < numberOfCells; ++i) {
for (var j = 0; j < numberOfCells; ++j) {
ctx.fillStyle = '#000';
ctx.fillRect(j * cellWidth, i * cellHeight, cellWidth, cellHeight);
}
}
Thanks in advance,
Petr.
jsFiddle : https://jsfiddle.net/ngxjnywz/2/
snippet of javascript
var cellWidth = Math.ceil(canvasWidth / numberOfCells);
var cellHeight = Math.ceil(canvasHeight / numberOfCells);
Depending on the width, height and your numberOfCells you are sometimes getting a... lets say 4.2 which is 4, however this would be displayed wrong and will allow a 1 pixel blank line to appear. So all you need to do is use the Math.ceil function and this will cause your cellWidth and cellHeight to always be the higher number and you won't get blank lines anymore
The best solution is to add a 0.5 pixel wide stroke around all the fills, using the same style as the fill and offsetting all drawing so that you render at the center of pixels rather than the top left.
If you add scaling or translation you will have to adjust the coordinates so that you still give the centers for your drawing coordinates.
In the end you can only reduce the artifacts but for many situations you will not be able to completely remove them.
This answer shows you how to remove the artifacts for an untransformed canvas.
How to fill the gaps
After reading through and trying several approaches, I've decided to come up with my own. I've created another (virtual) canvas which had integer dimensions corresponding to the number of cells in the grid.
After drawing all the cells in there, I call context.drawImage() on the main canvas and pass the virtual canvas as an argument along with offset and scale parameters to make it fit rest of my drawing. Assuming that the browser would scale the virtual canvas's image as a whole (and not as individual cells), I was hoping to get rid of the unwanted separator lines.
In spite of my efforts, the lines are still there. Any suggestions?
Here's the fiddle demonstrating my technique: https://jsfiddle.net/ngxjnywz/5/
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'm working on a 2d canvas-based app using EaselJS where the user can move indefinitely on the xy-plane by dragging the background. Think google maps, except the background is a repeating tile.
The code for the movement is very simple and looks something like this:
// container - the createjs.Container being panned
// background - a createjs.Shape child of container, on which the
// background is drawn
background.onPress = function(evt) {
var x = evt.stageX, y = evt.stageY;
evt.onMouseMove = function(evt) {
// the canvas is positioned in the center of the window, so the apparent
// movement works by changing the registration point of the container in
// the opposite direction.
container.regX -= evt.stageX - x;
container.regY -= evt.stageY - y;
x = evt.stageX;
y = evt.stageY;
stage.update();
};
evt.onMouseUp = function(evt) {
// Here the background would be redrawn based on the new container coords.
// However the issue occurs even before the mouse is released.
background.redraw();
stage.update();
};
};
All works as expected until reaching 32678px (2^15) on either axis. What occurs is different in different browsers, but the point where it first happens is the same.
In Firefox, it will suddenly shift a large chunk of pixels (~100) rather than 1. It will then happen again at 65538 (2^16+2), perhaps more after that, but I haven't witnessed it. After the trouble points, the drag will continue smoothly, as expected, but remaining shifted.
In Chrome, the effect is more dramatic. The drawing breaks and results in repeated ~100px wide "stripes" of the background across the page at 32768, and does not correct itself on redraw.
Oddly, the numbers in EaselJS don't reflect the issue. The only change in the container's transform matrix is the tx or ty incrementing by 1. No other matrices change. It seems as though EaselJS is getting all the numbers right.
Can anyone shed any light this issue?
Edit:
I worked around this problem by redrawing parts of the container using a calculated regX/regY, rather than attempting to translate very large regX/regY coords on the canvas.
This question may be related
What is the maximum size for an HTML canvas?
From what I gather browsers use short int to store canvas sizes with 32,767 being the maximum possible value.
Links possibly related to your issue,
Mozilla - HTML5 Canvas slow/out of memory
https://stackoverflow.com/a/12535564/149636
A while back webkit (and thus Safari) began to support CSS canvas-backgrounds for elements (Source: http://www.webkit.org/blog/176/css-canvas-drawing/).
This could greatly simplify the creation of games and multimedia, in that you dont need to inject a canvas-tag into a DIV (for instance), but simply hook into the background of the DIV directly. Something like this perhaps:
<div id="gameview"
style="background: -webkit-canvas(myscreen); width: 320px; height: 480px;">
</div>
<script>
var target = document.getElementById("gameview");
var wd = target.clientWidth;
var hd = target.clientHeight;
var context = document.getCSSCanvasContext("2d", "myscreen", wd, hd);
/* draw stuff here */
</script>
I was wondering, are there any speed penalties involved in this? In theory i think drawing to a background canvas should be faster than drawing to a canvas tag, especially if the target element is empty.
Have anyone tested this for high-speed demos or games?
According to my tests (also run in reversed order), original canvas element is slightly but consistently slower than the background canvas.
Chromium 17 draws a chess-board 10000 times in:
~470 ms on the background canvas
~520 ms on a canvas element
Safari 5 shows similar dynamics.
Try setting the number of iterations to 100000, results should be consistent with the above.
Update half a year later
We tried the background canvas approach in one project (as an attempt for a minor optimization), and the results were dramatically opposite to our expectations. The whole thing (two layers: one – a div with background canvas, the other – a regular canvas) became marginally slower. In fact, when we introduced the background canvas, the app became slow as hell. Tested in Chrome 21.
I definitely would not vouch for the background canvas to be faster in all situations.
test.php:11Regular Canvas 606
test.php:20Background Canvas 449
test.php:11Regular Canvas 516
test.php:20Background Canvas 483
Regular seems to underperform compared to background canvas in my tests on chrome in linux debian, heres the code used ( also added to http://jsfiddle.net/hDPVr/ )
<div style="width:300; height:200; background: -webkit-canvas(test_canvas); "></div>
<canvas id="canvas" style="width:300; height:200;"></div>
<script type="text/javascript">
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var regular_timer = new Date().getTime() ;
for( var i = 0; i<100000; i++ ){
context.fillRect( 0,0,10,10);
}
console.log( 'Regular Canvas', regular_timer - (new Date().getTime()) )
var context = document.getCSSCanvasContext('2d', 'test_canvas', 300, 200);
var background_timer = new Date().getTime() ;
for( var i = 0; i<100000; i++ ){
context.fillRect( 0,0,10,10);
}
console.log( 'Background Canvas', background_timer - (new Date().getTime()) )
</script>
So the only thing that I did for testing is fillRect, but it's still at least 10% better in some cases