Edit in JSFiddle

console.clear();
/*
TODO: figure out rotations
*/

class CanvasApp {
  constructor(container) {
    this.container = container;

    this.shapeDefs = {
      circle: 50,
      square: 50,
      angle: 0
    }

    this.images = {};

  }
  get dimensions() {
    return {
      width: this.container.offsetWidth,
      height: this.container.offsetHeight
    };
  }

  get context() {
    return this.canvas.getContext('2d');
  }

  get backContext() {
    return this.backCanvas.getContext('2d');
  }



  // Only run once
  createCanvas() {
    this.canvas = document.createElement('canvas');
    this.backCanvas = document.createElement('canvas');

    this.container.appendChild(this.canvas);
  }

  sizeCanvas() {
    this.canvas.height = this.dimensions.height;
    this.canvas.width = this.dimensions.width;

    this.backCanvas.height = this.dimensions.height;
    this.backCanvas.width = this.dimensions.width;
  }

  clearCanvas() {
    this.context.clearRect(0, 0, this.dimensions.width, this.dimensions.height);
  }

  rotateCanvas(degrees) {
    this.context.rotate((Math.PI / 180) * degrees);
  }

  //useful for clipping images, or other controls where you need a one-off canvas and not a back canvas
  BlankCanvas(w, h = w) {
    let canvas = document.createElement('canvas');

    canvas.width = w;
    canvas.height = h;

    return canvas;
  }

  Circle(x, y, radius) {
    let circle = new Path2D();
    let startA = 0;
    let endA = Math.PI / 180 * 360;

    circle.arc(x, y, radius, startA, endA, false);

    return circle;
  }

  drawCircle(x, y, radius) {
    let circle = this.Circle(x, y, radius);

    this.context.stroke(circle);
  }

  clipCircle(x, y, radius) {
    let circle = this.Circle(x, y, radius);

    this.context.clip(circle);
  }

  drawCircleSet(baseX, baseY, nCircles = 4) {
    let angle = this.shapeDefs.angle;
    let angles = [];
    let radius = this.dimensions.width - ((this.shapeDefs.circle * 2.5) * 2);
    let radians = Math.PI / 180;
    let i = 0;

		for (; i < nCircles; i++) {
    angles.push(angle);
    angle+=(360/nCircles);
    }

    for (let i = 0; i <= angles.length; i++) {
      angle = angles[i];
      let x = baseX + (this.shapeDefs.circle * 2.5) * Math.cos(angle * radians );
      let y = baseY + (this.shapeDefs.circle * 2.5) * Math.sin(angle * radians ) ;
      this.drawCircle(x, y, this.shapeDefs.circle);
    }
  }

  /*Square methods*/

  Square(x, y, w, h = w) {
    let rect = new Path2D();
    rect.moveTo(x, y);
    rect.rect(x, y, w, h);

    return rect;
  }

  drawSquare(x, y, w, h = w) {
    let rect = this.Square(x, y, w, h);

    this.context.stroke(rect);
  }

  clipSquare(x, y, w, h = w) {
    let rect = this.Square(x, y, w, h);

    this.context.clip(rect);
  }

  drawSquareSet(baseX, baseY) {
    let minX = baseX - this.shapeDefs.square * 2.5;
    let maxX = baseX + this.shapeDefs.square * 2.5;
    let minY = baseY - this.shapeDefs.square * 2.5;
    let maxY = baseY + this.shapeDefs.square * 2.5;

    this.drawSquare(baseX, baseY, this.shapeDefs.square);

    this.drawSquare(minX, baseY, this.shapeDefs.square);
    this.drawSquare(maxX, baseY, this.shapeDefs.square);

    this.drawSquare(baseX, minY, this.shapeDefs.square);
    this.drawSquare(baseX, maxY, this.shapeDefs.square);
  }

  /*Image Methods*/

  storeImage(src, id, loadCB) {
    /*storeImage is weird, but it gets around the need to use promises and be async.
    Pass in the src and the id, and when the image loads, it gets addeed to an internal list*/
    var _this = this;
    let img = new Image();

    img.src = src;

    img.onload = function() {
      _this.images[id] = img;
      if (loadCB) loadCB.call(this);
    };
  }

  drawImage(imageId, x, y, w, h = w) {
    if (imageId in this.images) this.context.drawImage(this.images[imageId], x, y, w, h);
  }

  ClippedImage(image, clipShape, w, h) {
    let imgCanvas = this.BlankCanvas(w, h);
    let imgContext = imgCanvas.getContext('2d');
    imgContext.clip(clipShape);
    imgContext.drawImage(image, 0, 0, w, h);
    return imgCanvas;
  }

  drawClippedImage(imageId, canvasX, canvasY, w, h, clipShape) {
    let image, clippedImage;

    if (imageId in this.images) {
      image = this.images[imageId];
      clippedImage = this.ClippedImage(image, clipShape, image.naturalWidth, image.naturalHeight);
      this.context.drawImage(clippedImage, canvasX, canvasY, w, h);
    }
  }

  animateCanvas(baseX = this.dimensions.width / 2, baseY = this.dimensions.height / 2) {
    var _this = this;
    let height = this.dimensions.height;
    let width = this.dimensions.width;
    let ctx = this.context;

    /*INSERT PRE-DRAWING GOODIES HERE*/
    let clipCircleSize = this.shapeDefs.circle * 2;
    let circleClipShape = this.Circle(100, 100, clipCircleSize);
    this.storeImage('https://placekitten.com/g/200/200', 'kitten1', function() {
      _this.images['kitten1clip'] = _this.ClippedImage(this, circleClipShape, this.naturalWidth, this.naturalHeight);
    });
    this.storeImage('https://placebear.com/200/200', 'bear1', function() {
      _this.images['bear1clip'] = _this.ClippedImage(this, circleClipShape, this.naturalWidth, this.naturalHeight);
    });
    this.storeImage('https://placebear.com/g/200/200', 'bear2', function() {
      _this.images['bear2clip'] = _this.ClippedImage(this, circleClipShape, this.naturalWidth, this.naturalHeight);
    });

    (function loop() {
      let clipCircleSize = _this.shapeDefs.circle * 2;
      let midX = baseX - (_this.shapeDefs.circle * 2.5);
      let maxX = baseX + (_this.shapeDefs.circle * 2.5);
      let midY = baseY - (_this.shapeDefs.circle * 2.5);
      let maxY = baseY + (_this.shapeDefs.circle * 2.5);
      let clipBaseX = baseX - (clipCircleSize / 2);
      let clipBaseY = baseY - (clipCircleSize / 2);
      let clipMidX = midX - (clipCircleSize / 2);
      let clipMaxX = maxX - (clipCircleSize / 2);
      let clipMaxY = maxY - (clipCircleSize / 2);

      _this.clearCanvas();
      ctx.restore();
      /*INSERT DRAWING GOODIES HERE*/

      _this.drawImage('bear1clip', clipMidX, clipBaseY, clipCircleSize, clipCircleSize);
      _this.drawImage('kitten1clip', clipBaseX, clipBaseY, clipCircleSize, clipCircleSize);
      _this.drawImage('bear2clip', clipMaxX, clipBaseY, clipCircleSize, clipCircleSize);

      _this.drawCircleSet(baseX, baseY);

      ctx.save();
      window.requestAnimationFrame(loop)
    })();
  }

  // This is what should be accessible to outside APIs for drawing
  drawCanvas() {
    this.sizeCanvas();
    this.animateCanvas();
  }
}

appControls = {
  data: {
    width: 0,
    height: 0,
    baseX: 0,
    baseY: 0,
    angle: 0,
  },

  bindEvts: function() {

    window.addEventListener('keydown', (evt) => {
      switch (evt.keyCode) {
        case 38:
          this.app.animateCanvas(this.data.baseX, this.data.baseY--);
          break;
        case 40:
          this.app.animateCanvas(this.data.baseX, this.data.baseY++);
          break;
        case 37:
          this.app.animateCanvas(this.data.baseX--, this.data.baseY);
          break;
        case 39:
          this.app.animateCanvas(this.data.baseX++, this.data.baseY);
          break;
        case 187:
          this.app.shapeDefs.circle++;
          break;
        case 189:
          this.app.shapeDefs.circle--;
          break;
        case 65: 
        	this.app.shapeDefs.angle--;
          break;
        case 68:
        	this.app.shapeDefs.angle++;
          break;
        default:
          break;
      }
    });

    window.addEventListener("resize", evt => {
      this.app.drawCanvas();
    });
  },

  init: function(container) {
    this.app = new CanvasApp(container);
    this.app.createCanvas();
    this.app.sizeCanvas();
    this.app.drawCanvas();

    this.data.width = this.app.dimensions.width;
    this.data.height = this.app.dimensions.height;
    this.data.baseX = this.data.width / 2;
    this.data.baseY = this.data.height / 2;

    this.bindEvts();
    console.log(this.app);
  }
}

appControls.init(document.querySelector('.canvasContainer'));