Matthew
Matthew

Reputation: 2246

HTML Canvas: Animation Delay

Here is a demo of my Canvas.

The canvas generates a random rectangle and animates it by scaling it from 1.0 to 1.2 and back to 1.0 again. (Kinda like a human heart). This animation takes approximately 2 seconds to complete. There are 60 totalIterations. It starts with 0 and increments by one for every frame until it reaches 60. Once it reaches 60, the iteration is set back to 0 and animates from 1.2 scale back to 1.0.

What I want to do is before the execution of the next cycle (cycle meaning from 1.0 scale, to 1.2, and back to 1.0), I want to defer the scale.

Here is what I tried to do:

Context:

this.intermission = 3; // time to wait, or "defer" for
elapsed = (Date.now() - this.initTime) / 1000; // time elapsed since initialization (in seconds)

Condition:

if((elapsed % this.intermission >= (this.intermission - (this.intermission-1))) && (elapsed % this.intermission <= (this.intermission + (this.intermission-1)))) {
  ctx.scale(this.easing, this.easing);
} 

Condition Explained (Probably makes no sense):

If the remainder from dividing the elapsed time by 3 is greater than or equal to 2 AND the remainder from dividing the elapsed time by 3 is less than or equal to 5, scale the rectangle using the ease function.

... I wanted to give it some "buffer" room to complete the animation

If I were to increase the intermission to 10, the above condition would not work anymore, so I need a much better solution.

I thought about using setTimeout(function(){...}, x), but this is inside the JavaScript class.

Upvotes: 0

Views: 2192

Answers (1)

Blindman67
Blindman67

Reputation: 54026

Animation Lists / Stacks.

Keyframing

The best way would be to set up code to manage keyframes so you could just create a list of keyframes for each property of an object you wish to change over time. This allows you to create very complex animations that can be serialized to a JSON file. This decouples the animation from the code, but requires a lot more code.

Animation List

If it is just a simple animation then you can create an animation stack (or list if animation order is static), which is just a set of functions that get called in turn for each part of the animation.

You set a startTime for each part of the animation, being careful to set it consistently so you do not get any time drift. If the animation cycle is 4 seconds long then it should repeat every 4 seconds and in 4,000,000 seconds it should be just as precise.

Always use requestAnimationFrame (rAF) when animating anything on the page. rAF calls the callback passing as the first argument the time in ms (1/1000th) with a precision of 1/1,000,000 (0.001ms).

Endless animation using animation list

const canvas = document.createElement("canvas");
canvas.height = canvas.width = 300;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas)

// ease function
function easeInOut(x, pow = 2) { 
    x = x < 0 ? 0: x > 1 ? 1 : x;
    var xx = Math.pow(x,pow); 
    return xx/(xx+Math.pow(1-x,pow)); 
};
function MyObj(){
    this.x = 100;
    this.y = 100;
    this.size = 40;
    this.scale = 1;
}
MyObj.prototype = {
    getUnitTime(duration){  // get the unit time
        var unitTime = (globalTime - startTime) / duration;
        if(unitTime >= 1){ // if over time 
             unitTime = 1; // make sure that the current frame is not over
             startTime = startTime + duration; // next frame start (could be in the past)
             currentAnim += 1;   // next animation in the list
        }
        return unitTime;
    },
    grow(){
        drawText("Grow 1s");
        // grow for 1 second
        this.scale = easeInOut(this.getUnitTime(1000)) * 0.6 + 1;        
    },
    shrink(){
        drawText("Shrink 1s");
        // shrink for 1 second
        this.scale = 1.6 - easeInOut(this.getUnitTime(1000)) * 0.6 ;
    },
    wait(){
       drawText("Wait 2s");
       this.getUnitTime(2000);  // wait two seconds
    },
    draw(ctx){
       ctx.fillStyle = "red"; 
       ctx.beginPath();
       ctx.arc(this.x, this.y, this.size * this.scale, 0, Math.PI * 2);
       ctx.fill();
    }
}

function drawText(text){
    ctx.fillStyle = "black";
    ctx.fillText(text,100,36);
}    
var obj = new MyObj(); // create the object

// holds the animation list
const animationList = [
    obj.grow.bind(obj),  // bind the function calls to the object
    obj.shrink.bind(obj),
    obj.wait.bind(obj)
];

var currentAnim;  // index of current animation
var startTime;    // start time of current animation
var globalTime;   // time from the requestAnimationFrame callback argument
ctx.font = "32px arial";
ctx.textAlign = "center";

// main animation loop
function update(time){
    globalTime = time;  // set the global
    if(currentAnim === undefined){ // if not set then 
        startTime = time;  // set start time
        currentAnim = 0;   // set the index of the first animation
    }
    
    // clear the screen
    ctx.clearRect(0,0,canvas.width,canvas.height);
    
    // call the animation function
    animationList[currentAnim % animationList.length]();

    // draw the object
    obj.draw(ctx);
    
    // request next frame
    requestAnimationFrame(update);
}

// start it all happening
requestAnimationFrame(update);

Stacks

Stacks are much the same but used when the animation is conditional. You use some event to push the animation functions onto the stack. Then you shift the animation functions from the stack as needed. Or you may want the animation to repeat 10 times, then do something else, then start again. The animation stack lets you do this rather than have a huge list of animations.

Stack example using click event.

const canvas = document.createElement("canvas");
canvas.height = canvas.width = 300;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas)

// ease function
function easeInOut(x, pow = 2) { 
    x = x < 0 ? 0: x > 1 ? 1 : x;
    var xx = Math.pow(x,pow); 
    return xx/(xx+Math.pow(1-x,pow)); 
};
function MyObj(){
    this.x = 100;
    this.y = 100;
    this.size = 40;
    this.scale = 1;
}
MyObj.prototype = {
    getUnitTime(duration){  // get the unit time
        var unitTime = (globalTime - startTime) / duration;
        if(unitTime >= 1){ // if over time 
             unitTime = 1; // make sure that the current frame is not over
             startTime = startTime + duration; // next frame start (could be in the past)
             currentAnim = undefined
        }
        return unitTime;
    },
    grow(){
        drawText("Grow 1s");
        // grow for 1 second
        this.scale = easeInOut(this.getUnitTime(1000)) * 0.6 + 1;        
    },
    shrink(){
        drawText("Shrink 1s");
        // shrink for 1 second
        this.scale = 1.6 - easeInOut(this.getUnitTime(1000)) * 0.6 ;
    },
    timeup(){
       drawText("Click to Animate");
       currentAnim = undefined; 

    },
    draw(ctx){
       ctx.fillStyle = "red"; 
       ctx.beginPath();
       ctx.arc(this.x, this.y, this.size * this.scale, 0, Math.PI * 2);
       ctx.fill();
    }
}

function drawText(text){
    ctx.fillStyle = "black";
    ctx.fillText(text,100,36);
}    
var obj = new MyObj(); // create the object

// holds the animation list
const animationStack = [obj.timeup.bind(obj)];

var currentAnim;  // index of current animation
var startTime;    // start time of current animation
var globalTime;   // time from the requestAnimationFrame callback argument
ctx.font = "26px arial";
ctx.textAlign = "center";

function startAnim(){
    animationStack.length = 0;
    animationStack.push(obj.grow.bind(obj));
    animationStack.push(obj.shrink.bind(obj));
    animationStack.push(obj.timeup.bind(obj));
    if(currentAnim === undefined){// only restart if animation is not running
        requestAnimationFrame(update); 
    }
    startTime = undefined;
    currentAnim = undefined;
}
canvas.addEventListener("click",startAnim)

// main animation loop
function update(time){

    globalTime = time;  // set the global
    if(startTime === undefined){ // if not set then 
        startTime = time;  // set start time
    }
    if(currentAnim === undefined){
        if(animationStack.length > 0){
            currentAnim = animationStack.shift();
        }
    }

    if(currentAnim === undefined){
        return;
    }

    // clear the screen
    ctx.clearRect(0,0,canvas.width,canvas.height);
    
    // call the animation function
    currentAnim();

    // draw the object
    obj.draw(ctx);
    
    // request next frame

    requestAnimationFrame(update);
}

// start it all happening
requestAnimationFrame(update);

Upvotes: 3

Related Questions