Canvas: animating a simple star field

2019-04-01 17:54发布

问题:

I'm relatively new to Canvas. In finding my feet I'm creating a simple arcade game.

My question is regarding CPU performance / efficiency.

I'm creating 100 randomly positioned white dots as stars on a black background. On each requestAnimationFrame, the stars move one pixel to the left and as they get to the extreme left that column of pixels is placed on the extreme right of the screen.

Using requestAnimationFrame I'm calling the following function:

bgAnimateId = requestAnimationFrame( scrollBg );

function scrollBg() {
    var imgData = ctx.getImageData( 0, 0, 1, canvas.height );
    var areaToMoveLeft = ctx.getImageData( 1, 0, canvas.width-1, canvas.height );
    ctx.putImageData( areaToMoveLeft, 0, 0 );
    ctx.putImageData( imgData, canvas.width-1, 0 );
    bgAnimateId = requestAnimationFrame( scrollBg );
}

My concern is - would it be better to create 100 small canvas elements (or 100 divs) and animate those, or is it better to utilise the pixel methods that I've used above.

Many thanks for your help / guidance in advance :-)

回答1:

It turns out that context.getImageData and context.putImageData are very expensive to perform and having 100 canvases is too many.

So here’s a plan for creating an efficient panning starfield:

Using context.drawImage is very efficient and not very expensive to perform.

Here’s how to draw a random starfield on a canvas and then save that canvas as an image:

// draw a random starfield on the canvas
bkCtx.beginPath();
bkCtx.fillStyle="darkblue";
bkCtx.rect(0,0,background.width,background.height);
bkCtx.fill();
bkCtx.beginPath();
for(var n=0;n<100;n++){
    var x=parseInt(Math.random()*canvas.width);
    var y=parseInt(Math.random()*canvas.height);
    var radius=Math.random()*3;
    bkCtx.arc(x,y,radius,0,Math.PI*2,false);
    bkCtx.closePath();
}
bkCtx.fillStyle="white";
bkCtx.fill();

// create an new image using the starfield canvas
var img=document.createElement("img");
img.src=background.toDataURL();

You will have 2 kinds of drawing going on:

  1. A panning background of stars
  2. A foreground where your game objects will be drawn.

So create 2 canvases aligned on top of each other. The back canvas is for the stars and the front canvas for you game objects.

This is the background canvas that pans the moving image of the starfield:

This is the foreground canvas where your game objects go -- see my cheesy “rocket”

These are the 2 canvases stacked to create a background/foreground combination:

Here is the Html+CSS to stack the 2 canvases:

<div id="container">
  <canvas id="background" class="subcanvs" width=300; height=300;></canvas>
  <canvas id="canvas" class="subcanvs" width=300; height=300;></canvas>
</div>

#container{
  position:relative;
  border:1px solid blue;
  width:300px;
  height:300px;
}
.subcanvs{
  position:absolute;
}

Here’s how to use the starfield image to create a panning starfield on the background canvas:

var fps = 60;
var offsetLeft=0;
panStars();

function panStars() {

    // increase the left offset
    offsetLeft+=1;
    if(offsetLeft>backImage.width){ offsetLeft=0; }

    // draw the starfield image and
    // draw it again to fill the empty space on the right of the first image
    bkCtx.clearRect(0,0,background.width,background.height);
    bkCtx.drawImage(backImage,-offsetLeft,0);
    bkCtx.drawImage(backImage,backImage.width-offsetLeft,0);

    setTimeout(function() {
        requestAnimationFrame(panStars);
    }, 1000 / fps);
}

Now the front canvas is used for all your game objects.

Your game is efficient and performant with 2 canvases dedicated to their own purposes.

Here is code and a Fiddle: http://jsfiddle.net/m1erickson/5vfVb/

<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>

<style>
body{padding:20px;}
#container{
  position:relative;
  border:1px solid blue;
  width:300px;
  height:300px;
}
.subcanvs{
  position:absolute;
}
</style>

<script>
$(function(){

    // Paul Irish's great RAF shim
    window.requestAnimFrame = (function(callback) {
      return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
      function(callback) {
        window.setTimeout(callback, 1000 / 60);
      };
    })();

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");
    var background=document.getElementById("background");
    var bkCtx=background.getContext("2d");

    // create an image of random stars
    var backImage=RandomStarsImage();

    // draw on the front canvas
    ctx.beginPath();
    ctx.fillStyle="red";
    ctx.rect(75,100,100,50);
    ctx.arc(175,125,25,0,Math.PI*2,false);
    ctx.closePath();
    ctx.fill();

    // start panning the random stars image across the background canvas
    var fps = 60;
    var offsetLeft=0;
    panStars();

    function panStars() {

        // increase the left offset
        offsetLeft+=1;
        if(offsetLeft>backImage.width){ offsetLeft=0; }

        // draw the starfield image and draw it again 
        // to fill the empty space on the right of the first image
        bkCtx.clearRect(0,0,background.width,background.height);
        bkCtx.drawImage(backImage,-offsetLeft,0);
        bkCtx.drawImage(backImage,backImage.width-offsetLeft,0);

        setTimeout(function() {
            requestAnimFrame(panStars);
        }, 1000 / fps);
    }

    function RandomStarsImage(){

        // draw a random starfield on the canvas
        bkCtx.beginPath();
        bkCtx.fillStyle="darkblue";
        bkCtx.rect(0,0,background.width,background.height);
        bkCtx.fill();
        bkCtx.beginPath();
        for(var n=0;n<100;n++){
            var x=parseInt(Math.random()*canvas.width);
            var y=parseInt(Math.random()*canvas.height);
            var radius=Math.random()*3;
            bkCtx.arc(x,y,radius,0,Math.PI*2,false);
            bkCtx.closePath();
        }
        bkCtx.fillStyle="white";
        bkCtx.fill();

        // create an new image using the starfield canvas
        var img=document.createElement("img");
        img.src=background.toDataURL();
        return(img);
    }

}); // end $(function(){});
</script>

</head>

<body>
    <div id="container">
      <canvas id="background" class="subcanvs" width=300; height=300;></canvas>
      <canvas id="canvas" class="subcanvs" width=300; height=300;></canvas>
    </div>
</body>
</html>