I'm making a JavaScript game engine - demo - and I'm wondering which of these options would be the least expensive for drawing on canvas:
Have a condition to check if an object's x and y coords are within the height and width ranges.
Draw the objects regardless, although many won't appear.
The first option would require comparing four or so many variables, for each object and each animation loop. I'm wondering if that would be less expensive than drawing all objects regardless.
The actual game will have a number of objects that have x and y coords, with canvas that centres around the human player on a larger game map.
I use requestAnimationFrame to do the draw loop. I'm also not using any frameworks.
Offscreen drawings are ignored by the GPU and therefore take less time to render (ummm--not render!). However the CPU will still process the drawing commands so offscreen drawings are not "free".
But you have a second performance hit. You are drawing every circle in its own path:
ctx.beginPath();
ctx.arc(p.x,p.y,p.r,0,PI2);
ctx.fillStyle=p.color;
ctx.fill();
It's more efficient to draw all commonly colored arcs in a single (multi-circle) path:
ctx.fillStyle='red';
ctx.beginPath();
for(var i=0;i<len;i++){
var p=players[i];
if(p.color=='red'){
ctx.moveTo(p.x,p.y);
ctx.arc(p.x,p.y,p.r,0,PI2);
}
}
ctx.fill();
Here's a highly non-optimized set of perf tests showing:
Drawing on-screen circles with individual beginPath paths.
Drawing off-screen circles with individual beginPath paths.
Drawing on-screen circles with 1 multiple-circle path per color.
Drawing off-screen circles with 1 multiple-circle path per color.
On my modest development computer these are the perf results:
Onscreen / individual paths: 1050ms
Offscreen / individual paths: 427ms
Onscreen / one path per color: 528ms
Offscreen / one path per color: 220ms
highly non-optimized perf tests:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var colors=['red','green','blue','gold'];
var playersOnscreen=[];
var playersOffscreen=[];
for(var c=0;c<colors.length;c++){
var color=colors[c];
for(var p=0;p<100;p++){
playersOnscreen.push({x:50,y:50,r:10,color:color});
}
}
for(var c=0;c<colors.length;c++){
var color=colors[c];
for(var p=0;p<100;p++){
playersOffscreen.push({x:-50,y:50,r:10,color:color});
}
}
var count=100;
var t1=performance.now();
for(var i=0;i<count;i++){ drawIndividualPaths(playersOnscreen); }
var t2=performance.now();
for(var i=0;i<count;i++){ drawIndividualPaths(playersOffscreen); }
var t3=performance.now();
for(var i=0;i<count;i++){ drawColorPaths(playersOnscreen); }
var t4=performance.now();
for(var i=0;i<count;i++){ drawColorPaths(playersOffscreen); }
var t5=performance.now();
alert('onscreen/individual='+parseInt(t2-t1)+', offscreen/individual='+parseInt(t3-t2)+', onscreen/by color='+parseInt(t4-t3)+', offscreen/by color='+parseInt(t5-t4));
function drawIndividualPaths(players){
var len=players.length;
var PI2=Math.PI*2;
for(var i=0;i<len;i++){
var p=players[i];
ctx.beginPath();
ctx.arc(p.x,p.y,p.r,0,PI2);
ctx.fillStyle=p.color;
ctx.fill();
}
}
function drawColorPaths(players){
var len=players.length;
var PI2=Math.PI*2;
var clen=colors.length;
for(var c=0;c<clen;c++){
var color=colors[c];
ctx.fillStyle=color;
ctx.beginPath();
for(var i=0;i<len;i++){
var p=players[i];
if(p.color==color){
ctx.moveTo(p.x,p.y);
ctx.arc(p.x,p.y,p.r,0,PI2);
}
}
ctx.fill();
}
}
body{ background-color: ivory; }
#canvas{border:1px solid red; margin:0 auto; }
<h2>Please wait for perf tests to complete</h2>
<canvas id="canvas" width=300 height=300></canvas>
Related
im using a canvas to visualize a small game of mine.
Basicly i have two objects that represent space ships, each of them has a "Location" array which holds the ships current x/y.
According to these arrays, i drawImage on the canvas (totalw/h is 300/300 fyi).
Now, for the difficult part.
i want to draw animations (gunfire) on that canvas. basicly from ship1 x/y to ship2 x/y.
For the animation function itself, im passing an effects object that holds 3 Arrays, shooter.location[x, y], target.location[x, y] and a third array that holds where the EFFECT is currently at [x, y].
this.animateEffects = function(effects){
var shooter = effects.shooter;
var target = effects.target;
var current = effects.current;
var canvas = document.getElementById("effects");
var context = canvas.getContext("2d");
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
context.fillStyle = "red";
context.arc(current[0], current[1], 5, 0, 2*Math.PI);
effects.current[0]++
effects.current[1]++
context.fill();
if (current == target){
console.log("ding");
this.end()
}
}
My "problem" is that im, if possible at all, looking for a smart way to determine (for each frame) if effects[x, y] should go ++ or -- or a combination of the two, depending on where the "moving" ships are located at (at the time, the shooting started).
Any advise or hints are appreciated.
You can fire a bullet from shooter to target using linear interpolation.
Calculate the difference in the original X & Y positions of the shooter and target.
// save the starting position of the bullet (== shooter's original position)
// (these original X & Y are needed in the linear interpolation formula)
bulletOriginalX=shooter.x;
bulletOriginalY=shooter.y;
// calc the delta-X & delta-Y of the shooter & target positions
// (these deltas are needed in the linear interpolation formula)
dx=target.x-shooter.x;
dy=target.y-shooter.y;
Move the bullet towards the target using the interpolation formula
// where percent == the percent you want the bullet to be between
// it's starting & ending positions
// (between starting shooter & starting target positions)
currentBulletX=bulletOriginalX+dx*percent;
currentBulletY=bulletOriginalY+dy*percent;
Here's an example:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
shooter={x:50,y:50};
target={x:100,y:100};
effect={x:50,y:50,dx:0,dy:0,pct:0,speedPct:0.25};
draw();
fire();
$('#test').click(function(){
moveEffect();
draw();
});
function fire(){
effect.x=shooter.x;
effect.y=shooter.y;
effect.dx=target.x-shooter.x;
effect.dy=target.y-shooter.y;
effect.pct=0;
}
function moveEffect(){
effect.pct+=effect.speedPct;
}
function draw(){
ctx.clearRect(0,0,cw,ch);
ctx.beginPath();
ctx.arc(shooter.x,shooter.y,15,0,Math.PI*2);
ctx.closePath();
ctx.strokeStyle='green';
ctx.stroke();
ctx.beginPath();
ctx.arc(target.x,target.y,15,0,Math.PI*2);
ctx.closePath();
ctx.strokeStyle='red';
ctx.stroke();
if(effect.pct>1){return;}
var x=effect.x+effect.dx*effect.pct;
var y=effect.y+effect.dy*effect.pct;
ctx.beginPath();
ctx.arc(x,y,3,0,Math.PI*2);
ctx.closePath();
ctx.fillStyle='black';
ctx.fill();
}
body{ background-color: ivory; padding:10px; }
#canvas{border:1px solid red;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<button id=test>Animate 1 frame</button>
<br><canvas id="canvas" width=300 height=300></canvas>
I have two JS Fiddles, both with 10,000 snow flakes moving around but with two different approaches.
The first fiddle: http://jsfiddle.net/6eypdhjp/
Uses fillRect with a 4 by 4 white square, providing roughly 60 frames per second # 10,000 snow flakes.
So I wondered if I could improve this and found a bit of information on HTML5Rocks' website regarding canvas performance. One such suggestion was to pre-render the snow flakes to canvases and then draw the canvases using drawImage.
The suggestion is here http://www.html5rocks.com/en/tutorials/canvas/performance/, namely under the title Pre-render to an off-screen canvas. Use Ctrl + f to find that section.
So I tried their suggestion with this fiddle: http://jsfiddle.net/r973sr7c/
How ever, I get about 3 frames per second # 10,000 snow flakes. Which is very odd given jsPerf even shows a performance boost here using the same method http://jsperf.com/render-vs-prerender
The code I used for pre-rendering is here:
//snowflake particles
var mp = 10000; //max particles
var particles = [];
for(var i = 0; i < mp; i++) {
var m_canvas = document.createElement('canvas');
m_canvas.width = 4;
m_canvas.height = 4;
var tmp = m_canvas.getContext("2d");
tmp.fillStyle = "rgba(255,255,255,0.8)";
tmp.fillRect(0,0,4,4);
particles.push({
x : Math.random()*canvas.width, //x-coordinate
y : Math.random()*canvas.height, //y-coordinate
r : Math.random()*4+1, //radius
d : Math.random()*mp, //density
img: m_canvas //tiny canvas
})
}
//Lets draw the flakes
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for(var i = 0; i < particles.length; i++) {
var flake = particles[i];
ctx.drawImage(flake.img, flake.x,flake.y);
}
}
So I wondered why am I getting such horrendous frame rate? And is there any better way to get higher particle counts moving on screen whilst maintaining 60 frames per second?
Best frame rates are achieved by drawing pre-rendered images (or pre-rendered canvases).
You could refactor your code to:
Create about 2-3 offscreen (in-memory) canvases each with 1/3 of your particles drawn on them
Assign each canvas a fallrate and a driftrate.
In each animation frame, draw each offscreen canvas (with an offset according to its own fallrate & driftrate) onto the on-screen canvas.
The result should be about 60 frames-per-second.
This technique trades increased memory usage to achieve maximum frame rates.
Here's example code and a Demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var mp=10000;
var particles=[];
var panels=[];
var panelCount=2;
var pp=panelCount-.01;
var maxFallrate=2;
var minOffsetX=-parseInt(cw*.25);
var maxOffsetX=0;
// create all particles
for(var i=0;i<mp;i++){
particles.push({
x: Math.random()*cw*1.5, //x-coordinate
y: Math.random()*ch, //y-coordinate
r: 1, //radius
panel: parseInt(Math.random()*pp) // panel==0 thru panelCount
})
}
// create a canvas for each panel
var drift=.25;
for(var p=0;p<panelCount;p++){
var c=document.createElement('canvas');
c.width=cw*1.5;
c.height=ch*2;
var offX=(drift<0)?minOffsetX:maxOffsetX;
panels.push({
canvas:c,
ctx:c.getContext('2d'),
offsetX:offX,
offsetY:-ch,
fallrate:2+Math.random()*(maxFallrate-1),
driftrate:drift
});
// change to opposite drift direction for next panel
drift=-drift;
}
// pre-render all particles
// on the specified panel canvases
for(var i=0;i<particles.length;i++){
var p=particles[i];
var cctx=panels[p.panel].ctx;
cctx.fillStyle='white';
cctx.fillRect(p.x,p.y,1,1);
}
// duplicate the top half of each canvas
// onto the bottom half of the same canvas
for(var p=0;p<panelCount;p++){
panels[p].ctx.drawImage(panels[p].canvas,0,ch);
}
// begin animating
drawStartTime=performance.now();
requestAnimationFrame(animate);
function draw(time){
ctx.clearRect(0,0,cw,ch);
for(var i=0;i<panels.length;i++){
var panel=panels[i];
ctx.drawImage(panel.canvas,panel.offsetX,panel.offsetY);
}
}
function animate(time){
for(var i=0;i<panels.length;i++){
var p=panels[i];
p.offsetX+=p.driftrate;
if(p.offsetX<minOffsetX || p.offsetX>maxOffsetX){
p.driftrate*=-1;
p.offsetX+=p.driftrate;
}
p.offsetY+=p.fallrate;
if(p.offsetY>=0){p.offsetY=-ch;}
draw(time);
}
requestAnimationFrame(animate);
}
body{ background-color:#6b92b9; padding:10px; }
#canvas{border:1px solid red;}
<canvas id="canvas" width=300 height=300></canvas>
I don't think you want to create a new canvas element every time. Doing so causes a huge performance drain.
When I moved this code out of the for loop, the performance instantly improved. I think doing so will allow you to optimize your code to achieve the intended behavior:
var m_canvas = document.createElement('canvas');
m_canvas.width = 4;
m_canvas.height = 4;
var tmp = m_canvas.getContext("2d");
tmp.fillStyle = "rgba(255,255,255,0.8)";
tmp.fillRect(0, 0, 4, 4);
Check out this revised JSFiddle.
Hope this helped!
You would pre-render elements you paint many times.
Say for example you have a landscape where you paint bushes (of same form) on various locations during a game scroll. The the use of memory canvas would be ok.
For your code you should try to divide your flakes into for example 10 sizes. Thus create 10 memory canvases. Then paint these into random possitions.
In other words you copy 10 canvases 1.000 times. Not 10.000 canvases 10.000 times.
im using a canvas to visualize a small game of mine.
Basicly i have two objects that represent space ships, each of them has a "Location" array which holds the ships current x/y.
According to these arrays, i drawImage on the canvas (totalw/h is 300/300 fyi).
Now, for the difficult part.
i want to draw animations (gunfire) on that canvas. basicly from ship1 x/y to ship2 x/y.
For the animation function itself, im passing an effects object that holds 3 Arrays, shooter.location[x, y], target.location[x, y] and a third array that holds where the EFFECT is currently at [x, y].
this.animateEffects = function(effects){
var shooter = effects.shooter;
var target = effects.target;
var current = effects.current;
var canvas = document.getElementById("effects");
var context = canvas.getContext("2d");
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
context.fillStyle = "red";
context.arc(current[0], current[1], 5, 0, 2*Math.PI);
effects.current[0]++
effects.current[1]++
context.fill();
if (current == target){
console.log("ding");
this.end()
}
}
My "problem" is that im, if possible at all, looking for a smart way to determine (for each frame) if effects[x, y] should go ++ or -- or a combination of the two, depending on where the "moving" ships are located at (at the time, the shooting started).
Any advise or hints are appreciated.
You can fire a bullet from shooter to target using linear interpolation.
Calculate the difference in the original X & Y positions of the shooter and target.
// save the starting position of the bullet (== shooter's original position)
// (these original X & Y are needed in the linear interpolation formula)
bulletOriginalX=shooter.x;
bulletOriginalY=shooter.y;
// calc the delta-X & delta-Y of the shooter & target positions
// (these deltas are needed in the linear interpolation formula)
dx=target.x-shooter.x;
dy=target.y-shooter.y;
Move the bullet towards the target using the interpolation formula
// where percent == the percent you want the bullet to be between
// it's starting & ending positions
// (between starting shooter & starting target positions)
currentBulletX=bulletOriginalX+dx*percent;
currentBulletY=bulletOriginalY+dy*percent;
Here's an example:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
shooter={x:50,y:50};
target={x:100,y:100};
effect={x:50,y:50,dx:0,dy:0,pct:0,speedPct:0.25};
draw();
fire();
$('#test').click(function(){
moveEffect();
draw();
});
function fire(){
effect.x=shooter.x;
effect.y=shooter.y;
effect.dx=target.x-shooter.x;
effect.dy=target.y-shooter.y;
effect.pct=0;
}
function moveEffect(){
effect.pct+=effect.speedPct;
}
function draw(){
ctx.clearRect(0,0,cw,ch);
ctx.beginPath();
ctx.arc(shooter.x,shooter.y,15,0,Math.PI*2);
ctx.closePath();
ctx.strokeStyle='green';
ctx.stroke();
ctx.beginPath();
ctx.arc(target.x,target.y,15,0,Math.PI*2);
ctx.closePath();
ctx.strokeStyle='red';
ctx.stroke();
if(effect.pct>1){return;}
var x=effect.x+effect.dx*effect.pct;
var y=effect.y+effect.dy*effect.pct;
ctx.beginPath();
ctx.arc(x,y,3,0,Math.PI*2);
ctx.closePath();
ctx.fillStyle='black';
ctx.fill();
}
body{ background-color: ivory; padding:10px; }
#canvas{border:1px solid red;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<button id=test>Animate 1 frame</button>
<br><canvas id="canvas" width=300 height=300></canvas>
I want to create puzzle images out of original images, meaning that an image is cut into 9 pieces (3x3) and then shuffled and stored as a new image. Does anyone know which method is the best to do so and how to achieve it? Perhaps with CamanJS? Does anyone have an example code?
Canvas can do this using the clipping version of context.drawImage.
context.drawImage allows you to clip your 9 sub-pieces from the original image and then draw them anywhere on the canvas.
The clipping version of drawImage takes these arguments:
the image to be clipped: img
the [clipLeft, clipTop] in the original image where clipping starts
the [clipWidth, clipHeight] size of the sub-image to be clipped from the original image
the [drawLeft, drawTop] on the Canvas where the clipped sub-image will start drawing
the [drawWidth, drawHeight] is scaled size of the sub-image to be drawn on the canvas
If drawWidth==clipWidth and drawHeight==clipHeight, the sub-image will be drawn at the same size clipped from the original.
If drawWidth!==clipWidth and drawHeight!==clipHeight, the sub-image will be scaled and then drawn.
Here's example code and a Demo that randomly draws the clipped pieces onto the canvas. It shuffles an array to define random positions for the pieces and then draws those pieces using drawImage.
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var rows=3;
var cols=3;
var img=new Image();
img.onload=start;
img.src="https://dl.dropboxusercontent.com/u/139992952/multple/sailboat.png";
function start(){
var iw=canvas.width=img.width;
var ih=canvas.height=img.height;
var pieceWidth=iw/cols;
var pieceHeight=ih/rows;
var pieces = [
{col:0,row:0},
{col:1,row:0},
{col:2,row:0},
{col:0,row:1},
{col:1,row:1},
{col:2,row:1},
{col:0,row:2},
{col:1,row:2},
{col:2,row:2},
]
shuffle(pieces);
var i=0;
for(var y=0;y<rows;y++){
for(var x=0;x<cols;x++){
var p=pieces[i++];
ctx.drawImage(
// from the original image
img,
// take the next x,y piece
x*pieceWidth, y*pieceHeight, pieceWidth, pieceHeight,
// draw it on canvas based on the shuffled pieces[] array
p.col*pieceWidth, p.row*pieceHeight, pieceWidth, pieceHeight
);
}}
}
function shuffle(a){
for(var j, x, i = a.length; i; j = Math.floor(Math.random() * i), x = a[--i], a[i] = a[j], a[j] = x);
return a;
};
body{ background-color: ivory; padding:10px; }
#canvas{border:1px solid red;}
<canvas id="canvas" width=300 height=300></canvas>
This program runs. But, as you can, see there is an artifact when the square turns over. The matrix values must be represented, and this representation should also bee seen depending on an angle. Is there any way to archieve good visualization. Why is this happening to my code?
var canvas=document.getElementById('canvas');
var ctx=canvas.getContext('2d');
var x=100;
var y=100;
var width=200;
var height=200;
var radianAngle=0;
Rotar();
var array = new Array2D(200,200);
function Array2D(NumOfRows,NumOfCols)
{
var k=new Array(NumOfRows);
for (i = 0; i < k.length; ++i)
k[i] = new Array(NumOfCols);
return k;
}
function Rotar(){
//Borramos
ctx.clearRect(0,0,canvas.width,canvas.height);
//Salvamos el estado
ctx.save();
// Transladamos su centro de gravedad
ctx.translate(x+width/2,y+height/2);
//Otra mⳍ
ctx.rotate(radianAngle);
var array = new Array2D(200,200);
for(i=0; i<200; i++)
{
for(j=0;j<200; j++)
{
array[i][j]=i+j;
var r,g,b;
r = array[i][j];
g=50;
b=50;
//La parte de dibujo
ctx.fillStyle = "rgba("+r+","+g+","+b+",100)";
ctx.fillRect( i, j, 1, 1 );
}
}
ctx.restore();
}
$("#test").click(function(){
radianAngle+=Math.PI/60;
// call rotateSquare
Rotar();
});
body {
background: #dddddd;
}
#canvas {
background: #eeeeee;
border: thin solid #aaaaaa;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="canvas" height="500" width="500"></canvas>
<button id="test">Rotate</button><br>
Rotar();
});
This is a typical rounding issue : You rotate the context, then iterate on (x,y) in [0,199]. But while drawing the small one pixel wide rectangles, they won't fit perfectly one pixel, so the renderer has to 'diffuse' the color on several real device pixels, and that goes with a rounding error since r,g,b are only stored on 8 bits. Add to this the error on the coordinates of the tiny rectangles -that will be rasterized on up to 4 pixels-, and you have the grid you're seing.
When doing such transform, the rule is to rasterize : iterate the pixels of the destination and find where they originate in the source, not the other way around.
So a simple way do that : find the bounding box of the rotated rect, iterate in this BBox, and if a point is in the rect compute its color.
Or build an algorithm that rasterize the rect (most easy would be to use triangles, see here for an example of a triangle rasterization i played with : http://jsfiddle.net/gamealchemist/5cnkr2s5/ )
But... for what you are drawing here most simple and way faster is to build a linear gradient, use it as the fillStyle, and draw the whole rectangle in a single fillRect call. In other words : let the canvas (and, behind the scene, the GPU) do the math for you.
var grad = ctx.createLinearGradient(0,0,200,200);
grad.addColorStop(0,'#000');
grad.addColorStop(1,'#F00');
ctx.fillStyle = grad;
//
ctx.save();
ctx.clearRect( ... );
ctx.translate ( the upper left point) ;
ctx.rotate ( some angle );
ctx.fillRect(0, 0, 200, 200);
ctx.restore();
most simple example here (click 'run' several times, angle is randomized ):
http://jsfiddle.net/gamealchemist/j57msxr5/11/
(Rq : You can have an idea of the speed up of using the context2D by comparing the triangle rasterization i mentioned above with the same work done by the context2D : http://jsfiddle.net/gamealchemist/zch3gdrx/ )