danieljames
danieljames

Reputation: 875

Animate canvas from array

A function that results in a rectangle move from the top to the bottom of the canvas, and the only thing that can vary is the x position of the rectangle... so say createRect(x). Every time it is called, a new rectangle at stated x position moves from top to bottom.

Would like to have an array that stores a sequence of different x values that then calls the function in the correct order. This is more complicated by timing. I want to be able to specify say the first to be 5 seconds in, the second 10 seconds after that and so on. This is the bit I'm struggling with as I understand I could just have a loop to go through the array and create the rectangle for each value, but not sure about timing.

Example Array Data:

The result could be multiple rectangles moving at same time or appearing on canvas together. Thank you for your help in advance, Dan

Upvotes: 1

Views: 680

Answers (1)

Blindman67
Blindman67

Reputation: 54039

Do not use timer events to animate many item!

When animating you need to stay in sync with the display refresh rate so that your animations are nice and smooth.

Do not return

Returning from a render function to the idle state is actually a request to update the DOM.

You can not just setup time events and render on each item in its own time. Every time you exit a function into idle context (no code running) the browser assumes you are done with your renders and will present all changes to the display. This will slow the everything down, add animation artifacts like shearing, flicker, lost renders and janky play. Using timers to animate individual items is a very bad way to animate.

One function to rule them all

To do any animation you must do it through a single call. When you exit you should have completely re-render your scene and it should be ready to present to the display.

This can be done with requestAnimationFrame(callBack) (rAF) this function as the name may suggest, is specifically designed for animations.

You use it like so

function mainLoop(time){ // when this function is call the time is provided 
                         // by the browser.
    // from this point to the end of the function nothing will reach the 
    // display

    // render everything you need to

    // request the next frame
    requestAnimationFrame(mainLoop);

    // only after you exit will what you have rendered be presented to the 
    // display. The browser will make sure it is present in sync with the 
    // display hardware refresh rate and the rest of the DOM

}
// start the animation
requestAnimationFrame(mainLoop);

The rAF function is like a setTimeout but without you setting the time. When it calls your function it also adds the time in ms 1/1000th since the page loaded. You use this timer to control your animation. (I have found that getting a time from another source, Date or Performance is not as accurate as the time argument.)

Code

The following is how you could solve the problem using rAF and a single call per frame.

Boxes

Some code to hold the boxes. Simply when to start. Where the box start {x :?,y:?} and how long the box needs to move for.

var boxes = [];
function addBox(when,where,howLong){
    var b;
    boxes.push(b = {
        when : when,
        where : where,
        len : howLong,
    });
    return b;
}

Main loop

The main update function. Manages time and calls render function 'updateAll()'

var globalTime; //as time is important make a global for it
var startTime; // we need to have a referance point

function mainLoop(time){
    if(startTime === undefined){
         startTime = time;
    }
    globalTime = time-startTime; // set time
    updateAll();  // call the main logic
    requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);

Setup

Code to add boxes randomly because I am lazy..

const boxSize = 20; // pixel size
const playFor = 30; // how long all this will take
const numBoxes = 20; 
function setup(){
    var i
    for(i = 0; i < numBoxes; i ++){
        addBox(
            Math.random() * (playFor-1) * 1000 + 1000, // get time to start box
            {
                 x :Math.floor(Math.random() * (canvas.width - boxSize)) + boxSize / 2,
                 y : canvas.height + boxSize / 2 // start below bottom
            },
            Math.random() * 5 * 1000 + 1000  // play for over 1 sec less than 6
        );
    }
}
setup(); // make some random boxes

Update all...

Now the update function. This renders, and moves the boxes if needed. When a box has done its thing remove it from the array so we do not have to deal with it.

function updateAll(){
    ctx.clearRect(0,0,canvas.width,canvas.height); // clear the screen

    // iterate box array and check for new starts
    for(var i = 0; i < boxes.length; i ++){
        var b = boxes[i];
        if(!b.moving){ // is box not moving ???
             // check th time
             if(b.when < globalTime){  // it needs to start
                  b.moving = true; // flag it to move
             }
         }
         if(b.moving){ // move boxes that need it
              var pos = globalTime - b.when; // get pos in time
              if(pos < b.len){ // not at end yet 
                  pos /= b.len; // normalize time pos
                  pos = (b.where.y - (-boxSize /2)) * pos; // distance to past the top
                  // now draw the box;
                  ctx.fillRect(b.where.x - boxSize /2, b.where.y - boxSize - pos, boxSize, boxSize);
              }else{  // box has reached the end dont need it no more
                  // any last words Box!!
                  boxes.splice(i,1); // Bang
                  i -= 1;  // clean up the loop counter
              }
         }
    }
}

And that is all you need. The boxes will all move at the correct time, even if frames are dropped they will still keep perfect time, it does not matter how often you update.

The Demo

The demo shows it in action. The demo keeps going when boxes run out it adds more and resets the time (No point a demo doing nothing)

const boxSize = 30; // pixel size
const playFor = 60; // how long all this will take
const numBoxes = 120; 


var canvas = document.createElement("canvas");
canvas.style.position = "absolute";       // pos absolute to avoid scroll bars
canvas.style.top = canvas.style.left = "0px"; 
canvas.width = innerWidth;
canvas.height = innerHeight;

document.body.appendChild(canvas);
var ctx = canvas.getContext("2d");


var boxes = [];
function addBox(when,where,howLong){
    var b;
    boxes.push(b = {
        when : when,
        where : where,
        len : howLong,
    });
    return b;
}


var globalTime; //as time is important make a global for it
var startTime; // we need to have a referance point

function mainLoop(time){
    if(startTime === undefined){
         startTime = time;
    }
    globalTime = time-startTime; // set time
    updateAll();  // call the main logic
    requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);

function setup(){
    var i
    for(i = 0; i < numBoxes; i ++){
        addBox(
            Math.random() * (playFor-1) * 1000 + 1000, // get time to start box
            {
                 x :Math.floor(Math.random() * (canvas.width - boxSize)) + boxSize / 2,
                 y : canvas.height + boxSize / 2 // start below bottom
            },
            Math.random() * 5 * 1000 + 1000  // play for over 1 sec less than 6
        );
    }
}
setup(); // make some random boxes

function updateAll(){
    ctx.clearRect(0,0,canvas.width,canvas.height); // clear the screen

    // iterate box array and check for new starts
    for(var i = 0; i < boxes.length; i ++){
        var b = boxes[i];
        if(!b.moving){ // is box not moving ???
             // check th time
             if(b.when < globalTime){  // it needs to start
                  b.moving = true; // flag it to move
             }
         }
         if(b.moving){ // move boxes that need it
              var pos = globalTime - b.when; // get pos in time
              if(pos < b.len){ // not at end yet 
                  pos /= b.len; // normalize time pos
                  pos = (b.where.y - (-boxSize /2)) * pos; // distance to past the top
                  // now draw the box;
                  ctx.fillRect(b.where.x - boxSize /2, b.where.y - boxSize - pos, boxSize, boxSize);
              }else{  // box has reached the end dont need it no more
                  // any last words Box!!
                  boxes.splice(i,1); // Bang
                  i -= 1;  // clean up the loop counter
              }
         }
    }
    if(boxes.length === 0){ //no more boxes so add more
        setup();
        startTime = undefined; // reset start time for new boxes.
    }

}
  
          

Upvotes: 1

Related Questions