-
Notifications
You must be signed in to change notification settings - Fork 2
HTML5 Canvas Animations
- Preparation
- Example 01 - Start template
- Example 02 - Animation
- Example 03 - Movement
- Example 04 - Wall Collision
- Example 05 - Two Actor Collision
- Example 06 - Multiple Actor Collision
- Example 07 - Easing, spring
- Example 08 - Chaining Balls
- Example 09 - Character Animation
- Example 10 - Particle System
requestAnimationFrame polyfill https://gist.github.com/1579671
setimmediate polyfill https://github.com/NobleJS/setImmediate
dat.GUI http://workshop.chromeexperiments.com/examples/gui/#1--Basic-Usage
-
Google Chrome Canary
chrome://flags/
Chrome Developer Tools
Stage Object
function Stage() {
var _stage = this;
this.render = function() {
_stage.clear();
// draw each actor
for (var actor in actors) { /* anti pattern - for in loop over array */
actors[actor].calc();
actors[actor].draw();
}
};
this.clear = function() {
context.fillStyle = '#000000';
context.fillRect(0, 0, WIDTH, HEIGHT);
};
}
Actor Object
function Actor() {
this.x = WIDTH / 2;
this.y = HEIGHT / 2;
this.r = 20;
this.calc = function() {
// sample code goes here
};
this.draw = function() {
context.fillStyle = '#FFFFFF';
context.beginPath();
context.arc(this.x, this.y, this.r, 0, Math.PI * 2, true);
context.fill();
};
}
Init function.
function init() {
var canvas = document.getElementById('canvas');
var stage = new Stage();
canvas.width = WIDTH;
canvas.height = HEIGHT;
context = canvas.getContext("2d");
// create set of actors
for (var i = 0; i < MAX_ACTORS; i++) {
actors.push(new Actor());
}
setInterval(stage.render, 1000 / FPS);
}
Improvements:
- Use an IIFE to make a simple module and avoid polluting the global object (window).
- Move the functions of each Object (methods) to the prototype of the object.
- Use a timer tick function.
- bounce animation
- request animation frame
var onFrame = window.requestAnimationFrame;
function tick(timestamp) {
stage.render();
onFrame(tick);
}
onFrame(tick);
- fading canvas
clear: function() {
context.fillStyle = "rgba(0, 0, 0, .3)";
context.fillRect(0, 0, WIDTH, HEIGHT);
}
circle movement
calc: function() {
this.y = HEIGHT / 2 + Math.sin(this.angle) * 100;
this.x = WIDTH / 2 + Math.cos(this.angle) * 100;
this.angle = this.angle + 0.1;
},
movement with x velocity
function Actor() {
this.x = 0;
this.y = HEIGHT / 2;
this.r = 20; // radius
this.a = 0; // angle
this.vx = 25; // velocity x
}
calc: function() {
if( this.x < WIDTH ){
this.x = this.x + this.vx;
}
}
movement with x,y velocity
function Actor() {
//...
this.vx = 15; // velocity x
this.vy = 15; // velocity y
}
calc: function() {
if( this.x < WIDTH ){
this.x = this.x + this.vx;
this.y = this.y + this.vy;
}
}
movement with x,y velocity and angle
function Actor() {
this.angle = 0.4; // angle
this.speed = 5; // speed
this.vx = Math.cos(this.angle) * this.speed; // velocity x
this.vy = Math.sin(this.angle) * this.speed; // velocity y
}
function Stage() {
this.l = 0; // left
this.r = WIDTH; // right
this.t = 0; // top
this.b = HEIGHT; // bottom
}
Make the stage object global. Antipattern!
var stage = {};
Add more actors and randomize it.
var MAX_ACTORS = 15;
function Actor() {
this.x = WIDTH / 2 * Math.random();
this.y = HEIGHT / 2 * Math.random();
this.r = 20 * Math.random(); // radius
this.vx = 5 * Math.random(); // velocity x
this.vy = 5 * Math.random(); // velocity y
}
wall collision and border bouncing
calc: function() {
this.x = this.x + this.vx;
this.y = this.y + this.vy;
if (this.x + this.r > stage.r) {
this.x = stage.r - this.r;
this.vx = this.vx * -1;
} else if (this.x - this.r < stage.l) {
this.x = stage.l + this.r;
this.vx = this.vx * -1;
}
if (this.y + this.r > stage.b) {
this.y = stage.b - this.r;
this.vy = this.vy * -1;
} else if (this.y - this.r < stage.t) {
this.y = stage.t + this.r;
this.vy = this.vy * -1;
}
}
screen wrapping
calc: function() {
this.x = this.x + this.vx;
this.y = this.y + this.vy;
if (this.x - this.r > stage.r) {
this.x = stage.l - this.r;
} else if (this.x + this.r < stage.l) {
this.x = stage.r + this.r;
}
if (this.y - this.r > stage.b) {
this.y = stage.t - this.r;
} else if (this.y + this.r < stage.t) {
this.y = stage.b + this.r;
}
}
two circle collison detection
- Get only 2 Actors.
- Add color to stage and actor.
function Stage() {
this.c = "#000000"; // color
}
clear: function() {
context.fillStyle = this.c;
}
function Actor() {
this.c = "#000000"; // color
}
draw: function() {
context.fillStyle = this.c;
}
- Test for circle collision
circleHitTest: function(a,b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var diameter = a.r + b.r;
if (dist < diameter){
a.c = '#000000';
b.c = '#000000';
this.c = '#FFFFFF';
} else {
a.c = '#FFFFFF';
b.c = '#FFFFFF';
this.c = '#000000';
}
}
multiple circle collison detection
render: function() {
var i, j;
this.clear();
// draw each actor
for (i = 0; i < actors.length; i++) {
actors[i].c = '#666666';
for (j = 0; j < actors.length; j++) {
if (i !== j) {
this.circleHitTest(actors[i],actors[j]);
}
}
actors[i].calc();
actors[i].draw();
}
}
circleHitTest: function(a,b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var diameter = a.r + b.r;
if (dist < diameter){
a.c = '#FFFFFF';
b.c = '#FFFFFF';
}
}
multiple circle collison detection and gravity
var bounce = -0.5;
var spring = 0.25;
var gravity = 0.1;
TODO
Collision dection optimization:
- Dirty Rectangles
- Nearfield calculation (use circle for all and polygon for near collisions)
- Web Worker (rendering in main thread, physics in worker thread)
mouse following movement with easing new globals
var easing = 0.2;
var targetX = 0;
var targetY = 0;
calc: function() {
var vx = (targetX - this.x) * easing;
var vy = (targetY - this.y) * easing;
this.x += vx;
this.y += vy;
}
add target point and event handler
// add event handler
canvas.addEventListener("click", function(e){
targetX = e.clientX - canvas.offsetLeft;
targetY = e.clientY - canvas.offsetTop;
}, false);
//set target point
targetX = WIDTH / 2;
targetY = HEIGHT / 2;
mouse following movement with spring and friction new globals:
var spring = 0.1;
var friction = 0.95;
calc: function() {
// movement with spring and friction
var dx = targetX - this.x;
var dy = targetY - this.y;
var ax = dx * spring;
var ay = dy * spring;
this.vx += ax;
this.vy += ay;
this.vx *= friction;
this.vy *= friction;
this.x += this.vx;
this.y += this.vy;
}
mouse following movement with rubber band
draw: function() {
// ball
context.fillStyle = this.c;
context.beginPath();
context.arc(this.x, this.y, this.r, 0, Math.PI * 2, true);
context.fill();
// rubber band
context.strokeStyle = this.c;
context.moveTo(this.x,this.y);
context.lineTo(targetX,targetY);
context.stroke();
}
TODO
new globals:
var spring = 0.1;
var friction = 0.8;
var gravity = 5;
render: function() {
this.clear();
actors[0].move(targetX, targetY);
actors[0].draw();
// draw each actor
for (var i=1; i<actors.length; i++) {
var actorA = actors[i-1];
var actorB = actors[i];
actorB.move(actorA.x, actorA.y);
actors[i].draw();
}
}
Update Actor properties
function Actor() {
this.r = 10; // radius
this.vx = 0; // velocity x
this.vy = 0; // velocity y
}
New move method of Actor.
move: function(tx, ty) {
// movement with spring, friction and gravity
var dx = tx - this.x;
var dy = ty - this.y;
var ax = dx * spring;
var ay = dy * spring;
this.vx += ax;
this.vy += ay;
this.vy += gravity;
this.vx *= friction;
this.vy *= friction;
this.x += this.vx;
this.y += this.vy;
}
draw: function(sx, sy) {
// ball
context.fillStyle = this.c;
context.beginPath();
context.arc(this.x, this.y, this.r, 0, Math.PI * 2, true);
context.closePath();
context.fill();
// rubber band
context.strokeStyle = this.c;
context.moveTo(sx, sy);
context.lineTo(this.x, this.y);
context.stroke();
}
// draw each actor
for (var i=1; i<actors.length; i++) {
var actorA = actors[i-1];
var actorB = actors[i];
actorB.move(actorA.x, actorA.y);
actorB.draw(actorA.x, actorA.y);
}
Handle Images and Sprites.
var images = [];
var sprites = [];
// Sprite Object
function Sprite(n,x,y,h,w) {
this.n = n; // sprite name
this.x = x;
this.y = y;
this.h = h;
this.w = w;
}
function loadImage(file, callback) {
images[file] = new Image();
images[file].onload = function() {
// create and fire own custom event
var myEvent = document.createEvent("HTMLEvents");
myEvent.initEvent("imageLoadReady", true, true);
canvas.dispatchEvent(myEvent);
};
images[file].src = file + ".png";
}
Update Stage and render method.
function Stage() {
this.i = 0;
}
render: function() {
this.i++;
if(this.i === sprites.length) this.i = 0;
var s = sprites[this.i];
this.clear();
actors[0].calc();
actors[0].draw(s);
}
Update Actor
// ACTOR Object
function Actor() {
this.y = 10;
this.r = 87;
this.vy = 10; // velocity y
}
Add screen wrapping for infinite character animation.
calc: function() {
// screen wrapping
this.y = this.y + this.vy;
if (this.y - this.r > stage.b) {
this.y = stage.t - this.r;
} else if (this.y + this.r < stage.t) {
this.y = stage.b + this.r;
}
}
New drawing function for image sprites.
draw: function(s) {
context.drawImage(images[s.n], s.x, s.y, s.h, s.w, this.y, 100, 85, 150);
}
We need setIntrval again for better FrameRate handling. Start animation after image is loaded.
canvas.addEventListener("imageLoadReady", function(e){
function tick(){
stage.render();
}
setInterval(tick, 1000 / FPS);
}, false);
var FPS = 10;