<div class="canvas-container">
<canvas class="canvas"></canvas>
</div>
canvas {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
@media
(min-device-pixel-ratio: 2),
(min-resolution: 192dpi) {
transform: translate(-50%, -50%) scale(.5);
}
}
View Compiled
const ease = {
inQuad: function(t) {
return t * t;
},
outQuad: function(t) {
return t * (2 - t);
},
inOutQuad: function(t) {
return t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
},
inCubic: function(t) {
return t * t * t;
},
outCubic: function(t) {
return (--t) * t * t + 1;
},
inOutCubic: function(t) {
return t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
},
inQuart: function(t) {
return t * t * t * t;
},
outQuart: function(t) {
return 1 - (--t) * t * t * t;
},
inOutQuart: function(t) {
return t < .5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t;
},
inQuint: function(t) {
return t * t * t * t * t;
},
outQuint: function(t) {
return 1 + (--t) * t * t * t * t;
},
inOutQuint: function(t) {
return t < .5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t;
}
};
const canvasAnimation = function() {
let anim;
let progress = 0;
let startTime = null;
let play = false;
let shapeCount = 0;
let shapes = [];
let imageCount = 0;
let images = [];
let ready = false;
let clickEvent;
let shapeClick = function() {
return
}
const _draw = () => {
const now = performance.now();
if (!startTime) {
startTime = now;
}
progress = (now - startTime) / anim.duration;
if (progress > 1) {
progress = 1;
}
anim.ctx.clearRect(0, 0, anim.ctx.canvas.width, anim.ctx.canvas.height);
for (let i = 0; i < imageCount; i++) {
anim.ctx.globalCompositeOperation = images[i].GCO;
let xy = images[i].locate();
anim.ctx.drawImage(images[i].image, xy.x, xy.y, images[i].image.width, images[i].image.height);
}
for (let i = 0; i < shapeCount; i++) {
anim.ctx.globalCompositeOperation = shapes[i].GCO;
anim.ctx.beginPath();
let pos = shapes[i].currentPosition(progress);
shapes[i].draw(pos)
anim.ctx.fillStyle = shapes[i].fill;
anim.ctx.fill();
}
if (progress >= 1) {
startTime = null;
progress = 0;
play = anim.loop;
anim.ctx.globalCompositeOperation = 'source-over';
}
if (play) requestAnimationFrame(_draw);
};
const _clickCanvas = (el, e) => {
let x = e.pageX;
let y = e.pageY;
let clicked = [];
shapes.forEach(element => {
let pos = element.currentPosition(progress);
if (element instanceof Circle) {
if (y > pos[1] - element.r && y < pos[1] + element.r && x > pos[0] - element.r && x < pos[0] + element.r) {
// Circle collision detection isn't perfect yet as you can still hit the corners.
clicked.push(element);
}
} else if (element instanceof Rectangle) {
if (y > pos[1] && y < pos[1] + element.h &&
x > pos[0] && x < pos[0] + element.w) {
clicked.push(element);
}
}
});
if (clicked.length > 0) {
return clicked;
}
return false;
};
class Animation {
constructor(params = {
size: [window.innerWidth, window.innerHeight],
duration: 1000,
easing: function(t) {
return t;
},
loop: false,
el: 'canvas',
resize: false
}) {
this.duration = (typeof params.duration === 'number') ? params.duration : 1000;
this.easing = (typeof params.easing === 'function') ? params.easing : function(t) {
return t;
};
this.loop = (typeof params.loop === 'boolean') ? params.loop : false;
this.c = (typeof params.el === 'string') ? document.querySelector(params.el) : document.querySelector('canvas');
this.ctx = this.c.getContext('2d');
if (params.resize) {
window.addEventListener('resize', () => {
this.setSize(params.size);
});
}
this.setSize(params.size);
}
setSize(obj) {
this.ctx.canvas.width = (typeof obj === 'object') ? obj[0] * window.devicePixelRatio : window.innerWidth * window.devicePixelRatio;
this.ctx.canvas.height = (typeof obj === 'object') ? obj[1] * window.devicePixelRatio : window.innerHeight * window.devicePixelRatio;
this.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
}
}
class Shape {
constructor(params) {
this.type;
this.GCO = (typeof params.GCO === 'string') ? params.GCO : 'source-over';
if (typeof params.x === 'object' && typeof params.y === 'object') {
this.x = params.x;
this.y = params.y;
this.fill = (typeof params.fill === 'string') ? params.fill : '#000000';
} else {
throw new Error('New shapes require two arrays: [startX, endX], [startY, endY]');
}
}
currentPosition(p) {
const x = this.x[0] + (this.y[0] - this.x[0]) * anim.easing(p);
const y = this.x[1] + (this.y[1] - this.x[1]) * anim.easing(p);
return [x, y];
}
}
class Circle extends Shape {
constructor(params) {
super(params);
this.r = (typeof params.r === 'number') ? params.r : 75;
}
draw(pos) {
anim.ctx.arc(pos[0], pos[1], this.r, 0, 2 * Math.PI);
}
grow(fac) {
this.r = (typeof fac === 'number') ? this.r * fac : this.r;
}
}
class Rectangle extends Shape {
constructor(params) {
super(params);
this.w = (typeof params.w === 'number') ? params.w : 75;
this.h = (typeof params.h === 'number') ? params.h : 75;
}
draw(pos) {
anim.ctx.rect(pos[0], pos[1], this.w, this.h);
}
grow(fac) {
if (typeof fac === 'number') {
this.w = this.w * fac;
this.h = this.h * fac;
}
}
}
class CanvasImage {
constructor(params) {
this.GCO = (typeof params.GCO === 'string') ? params.GCO : 'source-over';
this.image = new Image();
this.loaded = false;
this.image.onLoad = () => {
this.laoded = true;
};
this.image.src = (typeof params.src === 'string') ? params.src : 'http://placehold.it/350x350?text=Image+Must+Be+A+String';
if (typeof params.width !== 'undefined') this.image.width = params.width;
if (typeof params.height !== 'undefined') this.image.height = params.height;
let xy = this.locate(params);
this.x = xy.x;
this.y = xy.y;
}
locate(params) {
let x, y;
if (typeof this.x !== 'undefined' && typeof this.y !== 'undefined') {
x = this.x;
y = this.y;
} else {
x = (typeof params.x !== 'undefined') ? params.x : (window.innerWidth / 2) - (this.image.width / 2);
y = (typeof params.y !== 'undefined') ? params.y : (window.innerHeight / 2) - (this.image.height / 2);
}
return {
x,
y
}
}
}
return {
setup: obj => {
anim = new Animation(obj);
ready = true;
},
addShape: obj => {
if (typeof obj['shape'] === 'string') {
switch (obj['shape']) {
case 'circ':
shapes.push(new Circle(obj));
break;
case 'rect':
shapes.push(new Rectangle(obj));
break;
deafult:
throw new Error('Provided shape not valid.');
shapeCount--;
break;
}
shapeCount++;
} else {
throw new Error('Function incorrectly called. Object required with shape property (string).');
}
},
clearShapes: () => {
shapeCount = 0;
shapes = [];
},
addImage: obj => {
images.push(new CanvasImage(obj));
imageCount++;
},
clearImages: () => {
imageCount = 0;
images = [];
},
run: () => {
if (shapeCount > 0 && ready) {
requestAnimationFrame(_draw);
play = true;
clickEvent = anim.c.addEventListener('click', e => {
let result = _clickCanvas(anim.c, e);
if (result) {
shapeClick(result);
}
});
}
},
stop: () => {
play = false;
},
getWindowSize: () => {
if (anim.ctx !== 'undefined') {
let size = {
width: anim.ctx.canvas.width,
height: anim.ctx.canvas.height
}
return size;
}
throw new Error('Canvas must be set up before this function is called.');
},
getCurrentProgress: () => {
return progress;
},
onShapeClick: f => {
if (typeof f === 'function') {
shapeClick = f;
}
},
shapes,
images
};
}();
canvasAnimation.setup({
duration: 2500,
easing: ease.inOutQuint,
loop: true,
resize: true,
el: '.canvas'
});
canvasAnimation.addImage({
src: 'https://parallaxfiles.s3.amazonaws.com/parallax/p-logo.svg',
width: 53.73,
height: 75
});
canvasAnimation.addShape({
shape: 'circ',
x: [0, window.innerWidth / 2],
y: [window.innerHeight / 3, 160],
r: 75,
fill: 'rgba(170, 12, 219, .5)'
});
canvasAnimation.addShape({
shape: 'circ',
x: [window.innerWidth / 1.2, 0],
y: [0, window.innerHeight - 20],
r: 50,
fill: 'rgba(12, 140, 255, .4)'
});
canvasAnimation.addShape({
shape: 'rect',
x: [window.innerWidth / 2.6, window.innerWidth / 2.3],
y: [20, (window.innerHeight - 50) / 1.3],
w: 200,
h: 200,
fill: 'rgba(45, 230, 38, .3)',
GCO: 'source-atop'
});
canvasAnimation.onShapeClick(function(e) {
for (let i = 0; i < e.length; i++) {
e[i].grow(1.2);
}
});
canvasAnimation.run();
console.log(canvasAnimation);
/*
TO DO:
- Animation type (movement, interactive, orbit)
- SVG support
- User interaction [IN PROGRESS]
*/
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.