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>
Related
I have a pixi.js html canvas with thousands of objects on it and I want the user to be able to zoom into it with the usual rectangular selection area. The brute force way to implement this would be to draw the rectangle on each mouse move and rerender the whole stage. But this seems like a waste of CPU. Plus this is so common in user interfaces, that I suspect that there is already some function in pixi.js or a plugin that solves this.
If there is no plugin: If I could save the whole buffer to some 2nd buffer when the user presses the mouse button, I could draw the rectangle on top, and on every mouse move, copy back the 2nd buffer to the primary buffer before drawing the rectangle. This would mean that I didn't have to redraw everything on every mouse move. But I don't think that one can clone the current buffer to some named secondary buffer.
Another alternative would be to move a rectangular DOM object on top of the canvas, but then I am afraid that the current pixel position will be hard to relate to the pixi.js / html5 canvas pixels.
Is there a better way? Or some plugin / search engine keyword that I'm missing? How would you implement a rubber band in html canvas or pixi.js ?
I ended up solving this with a separate DOM object that is moved over the canvas. The solution also requires the new interaction manager in PIXI 4, that offers a single callback for any mouse movement over the canvas.
In the following, I assume that the canvas is placed at canvasLeft and canvasTop pixels with CSS.
$(document.body).append("<div style='position:absolute; display:none; border: 1px solid black' id='tpSelectBox'></div>");
renderer = new PIXI.CanvasRenderer(0, 0, opt);
// setup the mouse zooming callbacks
renderer.plugins.interaction.on('mousedown', function(ev) {
mouseDownX = ev.data.global.x;
mouseDownY = ev.data.global.y; $("#tpSelectBox").css({left:mouseDownX+canvasLeft, top:mouseDownY+canvasTop}).show();
});
renderer.plugins.interaction.on('mousemove', function(ev) {
if (mouseDownX == null)
return;
var x = ev.data.global.x;
var y = ev.data.global.y;
var selectWidth = Math.abs(x - mouseDownX);
var selectHeight = Math.abs(y - mouseDownY);
var minX = Math.min(ev.data.global.x, mouseDownX);
var minY = Math.min(ev.data.global.y, mouseDownY);
var posCss = {
"left":minX+canvasLeft,
"top":minY+canvasTop,
"width":selectWidth,
"height":selectHeight
};
$("#tpSelectBox").css(posCss);
});
renderer.plugins.interaction.on('mouseup', function(ev) {
$("#tpSelectBox").hide();
mouseDownX = null;
mouseDownY = null;
$("#tpSelectBox").css({"width":0, "height":0});
});
For older version of PIXI, here is an example of pan/zoom without a rectangle
https://github.com/Arduinology/Pixi-Pan-and-Zoom/blob/master/js/functions.js
In May 2015, the Interaction Manager got extended to allow easier pan/zoom handling https://github.com/pixijs/pixi.js/issues/1825 which is what I'm using here.
Here my full code on jsfiddle:
https://jsfiddle.net/6u7bLkwc/14/
Before exposing my problem, you can create a triangles on the image like this:
Click on the image, then drag your mouse in order to create it.
What is the problem with my code ?
When i use this script, i have many lags on my PC ... especially for larger images !
What i want ?
Try to use my script without loop to prevent lags.
Issue come from this part of code:
function draw() {
ctx.drawImage(imageObj, 0, 0);
shapes.forEach(function(shape) {
var hue = Math.floor(Math.random() * 360);
ctx.fillStyle = shape.getColor();
ctx.fillRect(shape.x, shape.y, shape.width, shape.height);
});
if (isMouseDown) {
ctx.fillStyle = color;
ctx.fillRect(mouseDownX, mouseDownY, mouseX - mouseDownX, mouseY - mouseDownY);
}
}
Exactly this line:
ctx.drawImage(imageObj, 0, 0);
This line is used to apply a background image to canvas ... since i want use this script for bigger images (Like 500ko and up) and loop it many time !!! this will create a self DDOSING attack to my computer.
Why not use this line outside of draw function ?
Because when i use it outside, and mouse hover the created rectangle ... i see a black background ... but like this i see my photo. (Mouse hover a rectangle make it transparent).
Any fix or any idea on how to fix it ? any different approch ?
The reason you exspierncing lag i mostly due to this function;
can.addEventListener('mousemove', function(e) {
mouseX = e.pageX;
mouseY = e.pageY;
if (isMouseDown) return;
var hoveredAlready = false;
for (var i = shapes.length - 1; i > -1; i--) {
var shape = shapes[i];
if (mouseX < shape.getLeft() || mouseY < shape.getTop()
|| mouseX > shape.getRight()
|| mouseY > shape.getBottom()
|| hoveredAlready) {
console.log(shape);
shape.unHover();
} else {
shape.hover();
hoveredAlready = true;
}
}
});
By allowing the image to be hovered in your example with a single exsitsing rectangle it's fine but as soon as you have say like 4 and they overlap this function is being called about 100 times in less than a second so for some users they shall experience lag.
I managed to replicated your problem while trying to debug this:
Also removing the function while it does break the applications functionality does remove the lag.
Some things I'd consider doing to work back and eliminate lag, Sort out boundaries more efficiently so you know when the users cursor is within the boundaries and adjust for coming on and off the image or your going to get some really sideways looking rectangles, Consider how your transparency may affect multiple objects on different 'levels' and also consider minimizing the amount you write to console as that adds to the overhead.
I'm trying to make a simple side scrolling avoider game in html/javascript using canvas. Having some troubles removing (clearRect) the moving variable height clipped image so it doesn't also remove the sprite/image the user is controlling above, I can get it to remove everything above/below the image but cannot find how to exactly remove the moving shadow image so it doesn't remove the hero of the game as well whilst animating!
https://jsfiddle.net/6k354f5x/3/
Currently the banana is also cleared, any help would be greatly appreciated!
//<canvas id="board" width="480" height="640"></canvas>
//Getting the canvas
var board = document.getElementById("board");
var cx = board.getContext("2d");
//Example Images
var pipe = new Image();
pipe.src = "http://www.appcycle.me/flappy/img/pipe.png";
var hero = new Image();
hero.src = "http://vignette2.wikia.nocookie.net/drunken-peasants-podcast/images/9/9c/Banana-in-8-bit.png/revision/latest?cb=20150821213530";
//Pipe randomness calculated from board height
var pipeVariation = Math.floor((Math.random() * 250) + 1);
var pipeY = 456;
var pipeX = 350;
//interval
var timer = setInterval(function() {
//draw the hero
cx.drawImage(hero, 0, 150);
//clear the afterimage
cx.clearRect(pipeX, 80, pipe.width / 1.6, pipe.height / 1.6);
//move it on the X-axis some px
pipeX -= 2;
//draw the clipped pipe with some Y-axis placement variation from pipeVariation variable
cx.drawImage(pipe,
0, -pipeY+pipeVariation, pipe.width, pipe.height,
pipeX, 80, pipe.width / 1.6, pipe.height / 1.6)
//Temporary to keep pipe from running away while testing
if (pipeX <= 0) {
clearInterval(timer);
}
});
Full canvas animation redraw everything.
When rendering canvas animation that has many elements, such as a game, the most effective method to render is to redraw everything every frame.
At the start of every frame you either clear the whole screen or draw the background image over the top of the last frame (same as clearing) then draw the game elements one by one in order of lowest to highest z-index (where the highest element is on top of all the other elements)
This makes the code very simple as opposed to complex dirty rects schemes that quickly become very complex and can reduce performance.
On all but a small number of devices most rendering is done via the GPU and is very fast (NOTE this does not apply to moveTo, lineTo, shadows, arcs, text, ellipses) Clearing and re-rendering everything can easily be done in 1/60 of a second creating smooth high quality animations
Note on shadows. Do not use the context2D shadows, most browsers and the hardware do not do it well at all and can be a major performance hit. This can be so even if you render one shadow for one small image. It is best to either pre render the shadow, or load a separate shadow as an image.
I implemented a simple draggable world map for a game but the performance differs when using different browsers - which is kinda obvious. I used 256x256 pixle tiles and the script dynamically renders the number to fill the whole window plus borders.
Prototype: http://mt111102.students.fhstp.ac.at/draggable/game.html
Currently I'm doing it simply by setting the top and left style attributes on mousemove. Heres a snippet:
mouseDown : function(e) {
Map.myPreventDefault(e);
dx = map.offsetLeft - e.clientX;
dy = map.offsetTop - e.clientY;
map.addEventListener('mousemove', Map.divMove, false);
},
divMove : function(e) {
Map.myPreventDefault(e);
map.style.position = "absolute";
map.style.cursor = "move";
map.style.left = e.clientX + dx + "px";
map.style.top = e.clientY + dy + "px";
}
Later when dragging to the borders I'm gonna load new tiles with XHR and delete old ones on the other end to retain performance so that the wrapper doesn't get to big.
My question is: Would it be more performant by using CSS translate instead of just setting the top and left attributes? And do you guys have any tips how to make it smoother?
In Firefox the prototype works almost perfectly smooth but in Webkit browsers like Chrome it doesn't look very good - it lags a bit. I just wonder how Google Maps managed it to work in every modern browser with the same smoothness.
A CSS transform, especially with a Z value (which will load the transform into the GPU), will almost always be faster and smoother - especially on devices like iPads, but elsewhere as well.
For more details you can check out this tutorial on GPU acceleration:
http://creativejs.com/2011/12/day-2-gpu-accelerate-your-dom-elements/
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