<div class="canvas-container">
    <canvas class="canvas"></canvas>
</div>
canvas {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    
    @media
    (-webkit-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

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.