Reputation: 2246
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
Reputation: 54026
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.
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 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