Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using it's URL and the proper URL extention.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <canvas id="painting"></canvas>
              
            
!

CSS

              
                body, html {
  margin: 0;
  padding: 0;
}

body {
  background: #000000;
  
  canvas {
    display: block;
  }
}
              
            
!

JS

              
                // All the sources of this project are combined into this pen, hence all the ;(function(){}) blocks, as modules.

;(function() { 
  'use strict';
  // Add a notImplemented method to Object, so that superclasses can call it to throw an error when
  // a method is called that is not implemented.
  if(!("notImplemented" in Object.prototype) || typeof(Object.prototype.notImplemented) !== "function") {
    var NotImplementedException = function NotImplementedException(message) {
      this.name = "NotImplementedException";
      this.message = message || "This method is not implemented."
    };

    Object.prototype.notImplemented = function(message) {
      throw new NotImplementedException(message);
    };
  }

  // The extend function, which is also wrapped in a method in Function.prototype
  function extend(original, ctor) {
    ctor.prototype = Object.create(original.prototype);
    ctor.parent = original;
    ctor.prototype.constructor = ctor;
    return ctor;
  };

  // A method for all Functions allowing simple extending
  if(!('extend' in Function.prototype) || typeof(Function.prototype.extend) !== "function") {
    Function.prototype.extend = function(ctor) {
      return extend(this, ctor);
    };
  }
}()); 

// An object of this class keeps track of the mouse position via window.onmousemove.
;(function(root) {
  'use strict';

  var MousePositionMonitor = function() {
    this.position = {x: 0, y: 0};

    var that = this;
    window.addEventListener('mousemove', function(event) {
      var dot, eventDoc, doc, body, pageX, pageY;

      event = event || window.event; // IE-ism

      // If pageX/Y aren't available and clientX/Y are,
      // calculate pageX/Y - logic taken from jQuery.
      // (This is to support old IE)
      if (event.pageX == null && event.clientX != null) {
        eventDoc = (event.target && event.target.ownerDocument) || document;
        doc = eventDoc.documentElement;
        body = eventDoc.body;

        event.pageX = event.clientX +
          (doc && doc.scrollLeft || body && body.scrollLeft || 0) -
          (doc && doc.clientLeft || body && body.clientLeft || 0);
        event.pageY = event.clientY +
          (doc && doc.scrollTop  || body && body.scrollTop  || 0) -
          (doc && doc.clientTop  || body && body.clientTop  || 0 );
      }

      that.position.x = event.pageX;
      that.position.y = event.pageY;
    });
  };

  MousePositionMonitor.prototype.getMousePosition = function() {
    return this.position;
  };

  root.MousePositionMonitor = MousePositionMonitor;
}(window));

/**
    The base class for all renderable classes. Each renderable shape, object,
    image, text etc should extend this class for clarity.
*/
;(function(root) {
  'use strict';

  // Renderable constructor, very basic.
  var Renderable = function(x, y) {
    this.position = {
      x: x || 0, 
      y: y || 0
    };
  };

  // Each renderable should implement render, duh!
  Renderable.prototype.render = function(ctx) {
    // Object.prototype.notImplemented is defined in Inheritance.js
    this.notImplemented('render() is not implemented.');
  };

  // Each renderable should implement step for model logic, but it may be an empty function.
  Renderable.prototype.step = function(canvas) {
    this.notImplemented('step() is not implemented.');
  };

  root.Renderable = Renderable;
}(window));

/**
    The base class for all renderable shapes, therefore it extends Renderable 
    and is named Shape.
*/
;(function(root) {
  'use strict';

  /**
        Extend Renderable and set a constructor. The constructor calls its parent,
        and then sets its own properties.
    */
  var Shape = Renderable.extend(function Shape(x, y, sizeX, sizeY) {
    Shape.parent.call(this, x, y);

    this.dimension = {
      x: sizeX || 0, 
      y: sizeY || 0
    };

    // anything may be entered here, it's simply a style object with two default values.
    this.style = {
      fill: Color.fromHex('#000000'),
      stroke: Color.fromHex('#ffffff')
    };
  });
  
  root.Shape = Shape;
}(window));

/**
    The 'playing field' or 'view target', a class which holds a list of
    renderables. Objects of this class have a reference to a canvas element. 
    All renderables will be drawn onto this canvas.
*/
;(function(root) {
  'use strict';

  // A few type-testing functions
  var is  = function(v,t) {return (typeof(v) === t);};
  var iss = function(v)   {return is(v, 'string');};
  var isf = function(v)   {return is(v, 'function')&&v instanceof Function;};

  /*
        A simple compatibility test, to see if the canvas element is supported
        in this browser. The variable canvasSupported will hold true if support
        for Canvas is available and false if this browser does not support Canvas.
    */
  var canvasSupported = (function () {
    var elem = document.createElement('canvas');
    return !!(elem.getContext && elem.getContext('2d'));
  }());

  // A custom exception which can be thrown when an error occurs.
  var CanvasException = function(message) {
    this.name = "CanvasException";
    this.message = message;
  };

  /**
        The Canvas constructor, which expects an HTML element referring to a canvas.
    */
  var Canvas = function(element) {
    if(!canvasSupported) {
      // Throw the CanvasException, no support for Canvas is available
      throw new CanvasException("The HTML5 Canvas element is not supported in this browser.");
    }

    // Test if the provided element actually is a canvas.
    var elementIsCanvas = !!(element.getContext && element.getContext('2d'));
    if(!elementIsCanvas) {
      // Throw the CanvasException, the element is not a canvas
      throw new CanvasException("The provided element is not a canvas element.");
    }

    // Store the reference to the DOM-element and the canvas 2d context.
    this.element = element;
    this.context = element.getContext('2d');

    // keep a list of renderable items!
    this.renderables = [];
    this.renderFPS   = true;
    this.stepFPS     = true;
  };

  /**
        Add a renderable object to the render list. All of these items will automatically
        be stepped and rendered when the Canvas step() and render() methods are called.
    */
  Canvas.prototype.addRenderable = function(renderable) {
    if(typeof(renderable) !== "object" || !(renderable instanceof Renderable)) {
      throw new TypeError("Invalid renderable provided");
    }

    this.renderables.push(renderable);
  };

  /**
        Call function `f` with a reference to the Canvas instance as `this` 
        parameter and the 2d context as argument. The function can then 
        safely create variables without placing garbage in the calling scope.
    */
  Canvas.prototype.with2d = function(f) {
    if(!isf(f)) {
      throw new TypeError("Invalid function provided");
    }

    f.call(this, this.context);
  };

  /**
        Step all the renderables, allowing them to do their logic.
    */
  Canvas.prototype.step = function() {
    this.calculateFPS();
    for(var i in this.renderables) {
      if(this.renderables.hasOwnProperty(i)) {
        if(this.renderables[i].collides) {
          for(var j in this.renderables) {
            if(this.renderables.hasOwnProperty(j) && this.renderables[j] !== this.renderables[i]) {
              this.renderables[i].doesCollideWith(this.renderables[j]);
            }
          }
        }
        this.renderables[i].step(this);
      }
    }
  };

  /**
        Calculate the frames per second each 50 milliseconds, only if
        renderFPS is set to true!
    */  
  Canvas.prototype.calculateFPS = function() {
    if(this.stepFPS) {
      if(!this.lct) {
        this.lct = Date.now();
        this.fps = 0;
        this.lut = null;
      }

      var now = Date.now();
      if(!this.lut || (now - this.lut) >= 50) {
        var delta = (new Date().getTime() - this.lct);
        this.fps = (1000 / delta);
        this.lut = now;
      }

      this.lct = now;

      if(!isFinite(this.fps)) {
        this.fps = 0;
      }
    }
  };

  /**
        Render all the renderables, like they want to be rendered!
    */
  Canvas.prototype.render = function() {
    this.with2d(function(ctx) {
      this.clear();

      for(var i in this.renderables) {
        if(this.renderables.hasOwnProperty(i)) {
          this.renderables[i].render(ctx);   
        }
      }

      if(this.renderFPS && this.stepFPS) {
        ctx.font = '300 10px roboto';
        ctx.fillStyle = '#ffffff';
        ctx.fillText(Math.ceil(this.fps) + ' FPS', 10, 15);
      }
    });
  };

  /**
        Clear the canvas area
    */
  Canvas.prototype.clear = function() {
    this.context.clearRect(0, 0, this.element.width, this.element.height);
  };

  /**
        Resize the canvas drawing area and element size
    */
  Canvas.prototype.resize = function(width, height) {
    this.element.style.width = width + 'px';
    this.element.style.height = height + 'px';
    this.context.canvas.width = width;
    this.context.canvas.height = height;
  };

  /**
        Return the current size of the canvas drawing area
    */
  Canvas.prototype.getSize = function() {
    return {
      width: this.context.canvas.width,
      height: this.context.canvas.height
    };
  };

  // export the canvas support boolean to Canvas and the classes to `root`
  Canvas.support       = canvasSupported;
  root.CanvasException = CanvasException;
  root.Canvas          = Canvas;
}(window));

;(function(root) {
  'use strict';

  /**
     * an FPS graph
     * @param {Number} x               x-position
     * @param {Number} y               y-position
     * @param {Number} width           width in pixels
     * @param {Number} height          height in pixels
     * @param {Number} [historySize=0] the size of the list to keep fps values in
     */
  var FPSGraph = Renderable.extend(function FPSGraph(x, y, width, height, historySize) {
    historySize = historySize || 0;
    historySize = (historySize < width ? width : historySize);
    FPSGraph.parent.call(this, x, y);

    this.history     = new Array(historySize);
    this.historySize = historySize;
    this.currentFPS  = 0;
    this.minFPS = this.maxFPS = 60;

    for(var i = 0; i < historySize; i++) {
      this.push(0);
    }

    this.dimension = {
      x: width,
      y: height
    };

    this.style = {
      font:       '500 12px roboto',
      fontColor:  Color.fromHex('#ffffff'),
      background: Color.fromHex('#000000'),
      border:     Color.fromHex('#ffffff'),
      line:       Color.fromHex('#00ff00'),
      fill:       Color.fromHex('#00be00'),
      minAlpha:   26,
      maxAlpha:   230,
      alpha:      26,
      alphaStep:  20
    };
  });

  /**
     * Set a MousePositionMonitor so that FPSGraph can track the mouse position
     * @param {MousePositionMonitor} mon an instance of MousePositionMonitor.
     */
  FPSGraph.prototype.setMousePositionMonitor = function(mon) {
    if(!(mon instanceof MousePositionMonitor)) {
      throw new TypeError("the monitor has to be an instance of MousePositionMonitor");
    }

    this.monitor = mon;
  };

  /**
     * Test if a point's x and y are in rect.
     * @param   {Object}  point a point object with an x and y value
     * @param   {Object}  rect  a rect object with x, y, w and h
     * @returns {Boolean} returns true if point.x and point.y are inside rect
     */
  FPSGraph.prototype.pointInRect = function(point, rect) {
    return (point.x >= rect.x && point.x <= rect.x + rect.w &&
            point.y >= rect.y && point.y <= rect.y + rect.h);
  };

  /**
     * Determine if this renderable is hovered by the mouse.
     * @param   {Object}  canvas the canvas object, to get its size
     * @returns {Boolean} returns true if the mouse is currently hovering this renderable.
     */
  FPSGraph.prototype.mouseHit = function(canvas) {
    if(this.monitor) {            
      return this.pointInRect(this.monitor.getMousePosition(), {
        x: (this.position.x < 0 ? canvas.getSize().width  + this.position.x : this.position.x),
        y: (this.position.y < 0 ? canvas.getSize().height + this.position.y : this.position.y),
        w: this.dimension.x,
        h: this.dimension.y
      });
    }
  };

  /**
     * Push a new history item onto the list and remove old ones
     * @param {Number} item a numeric value representing the FPS of that moment
     */
  FPSGraph.prototype.push = function(item) {
    this.history.push(item);
    if(this.history.length > this.historySize) {
      this.history.shift();
    }
  };

  /**
     * Step the context
     * @param {Object} canvas the canvas argument is provided by Canvas.js
     */
  FPSGraph.prototype.step = function(canvas) {
    if(canvas.fps) {
      this.currentFPS = canvas.fps;
      this.minFPS     = Math.min(this.minFPS, this.currentFPS);
      this.maxFPS     = Math.max(this.maxFPS, this.currentFPS);
      this.push(this.currentFPS);
    }

    // a simple hover animation
    if(this.mouseHit(canvas)) {
      if(this.style.alpha < this.style.maxAlpha) {
        this.style.alpha += this.style.alphaStep;
        if(this.style.alpha >= this.style.maxAlpha) {
          this.style.alpha = this.style.maxAlpha;
        }
      }
    } else {
      if(this.style.alpha > this.style.minAlpha) {
        this.style.alpha -= this.style.alphaStep;
        if(this.style.alpha <= this.style.minAlpha) {
          this.style.alpha = this.style.minAlpha;
        }
      }
    }

    for(var i in this.style) {
      if(this.style.hasOwnProperty(i) && this.style[i] instanceof Color) {
        this.style[i].a = this.style.alpha;
      }
    }
  };

  /**
     * Render the current state
     * @param {Object} ctx the 2d-context is provided by Canvas.js
     */
  FPSGraph.prototype.render = function(ctx) {
    ctx.lineWidth = 1;
    var x = (this.position.x < 0 ? ctx.canvas.width  + this.position.x : this.position.x);
    var y = (this.position.y < 0 ? ctx.canvas.height + this.position.y : this.position.y);

    // main rectangle fill
    ctx.fillStyle = this.style.background;
    ctx.fillRect(x, y, this.dimension.x, this.dimension.y);

    // graph!
    ctx.beginPath();
      ctx.moveTo(x, y + this.dimension.y);
      for(var i = 0; i < this.history.length; i++) {
        var fpsHeight = Math.ceil((this.history[i] / 90) * this.dimension.y);
        if(fpsHeight > this.dimension.y) {
          fpsHeight = this.dimension.y;
        }
        ctx.lineTo(x + i, y + this.dimension.y - fpsHeight);
      }
      ctx.lineTo(x + this.dimension.x, y + this.dimension.y);
    ctx.closePath();

    // stroke 'nd fill the graph
    ctx.fillStyle = this.style.fill;
    ctx.strokeStyle = this.style.line;

    ctx.save(); 
      // one shadow is enough!
      var prevShadow = ctx.shadowBlur;
      ctx.shadowBlur = null;
      ctx.fill();
      ctx.stroke();

      // main rectangle border
      ctx.strokeStyle = this.style.border;
      ctx.strokeRect(x, y, this.dimension.x, this.dimension.y);

      // FPS text
      var min = Math.round(this.minFPS + 1);
      var max = Math.round(this.maxFPS + 1);
      var txt = Math.round(this.currentFPS + 1) + ' FPS (' + min + ' - ' + max + ')';
      var dim = ctx.measureText(txt);
      ctx.shadowBlur = prevShadow;
      ctx.textBaseline = "top"; 
      ctx.font = this.style.font;
      ctx.fillStyle = this.style.fontColor;
      ctx.fillText(txt, x + 5, y + 5);
    ctx.restore();
  };

  root.FPSGraph = FPSGraph;
}(window));

// Just an extension of Shape
;(function(root) {
  'use strict';

  var Circle = Shape.extend(function Circle(x, y, radius) {
    Circle.parent.call(this, x, y, radius*2, radius*2);
    this.dimension.radius = radius;
  });

  Circle.prototype.render = function(ctx) {  
    ctx.beginPath();
      ctx.arc(this.position.x, this.position.y, this.dimension.radius, 0, 2 * Math.PI, false);

      if(this.style.fill) {
        ctx.fillStyle = this.style.fill;
        ctx.fill();
      }

      if(this.style.stroke) {
        ctx.lineWidth = 1;
        ctx.strokeStyle = this.style.stroke;
        ctx.stroke();
      }
    ctx.closePath();
  };

  Circle.prototype.step = function(canvas) {
    return;
  };

  root.Circle = Circle;
}(window));

;(function(root) {
  var SatellitedCircle = Circle.extend(function SatellitedCircle(x, y, radius) {
    SatellitedCircle.parent.call(this, x, y, radius);
    this.speedFactor   = 1;
    this.satellites    = [];
    this.previousHover = null;
  });

  SatellitedCircle.prototype.addSatellite = function(sat, trajectory, speed, startAngle, direction, drawTrajectory) {
    if(!(sat instanceof Shape)) {
      throw new TypeError('sattelite object must be an instance of shape.');
    }

    this.satellites.push({
      shape: sat,
      trajectory: trajectory,
      speed: speed || 0.01,
      angle: startAngle || 0,
      direction: direction || 1,
      drawTrajectory: typeof(drawTrajectory) === 'boolean' ? drawTrajectory : true
    });
  };

  SatellitedCircle.prototype.setMousePositionMonitor = function(m) {
    if(typeof(m) !== 'object' || !(m instanceof MousePositionMonitor)) {
      throw new TypeError("Monitor should be a MousePositionMonitor instance.");
    }

    this.mousePositionMonitor = m;
  };

  SatellitedCircle.prototype.step = function(canvas) {   
    var mp = null;
    if(this.mousePositionMonitor) {
      mp = this.mousePositionMonitor.getMousePosition();
    }
    
    for(var i in this.satellites) {
      if(this.satellites.hasOwnProperty(i)) {
        var satellite = this.satellites[i];

        // new X and Y for the sattelite body
        satellite.shape.position.x = Math.cos(satellite.angle) * satellite.trajectory + this.position.x;
        satellite.shape.position.y = Math.sin(satellite.angle) * satellite.trajectory + this.position.y;

        if(satellite.direction === 1) {
          satellite.angle += satellite.speed * this.speedFactor;
          if(satellite.angle > (Math.PI * 2)) {
            satellite.angle = 0;
          }
        } else {
          satellite.angle -= satellite.speed * this.speedFactor;
          if(satellite.angle < 0) {
            satellite.angle = (Math.PI * 2);
          }
        }

        if(this.speedFactor > 1) {
          this.speedFactor -= 0.05;
        }

        // check mouse position
        if(mp) {
          var centerX = (canvas.getSize().width / 2);
          var centerY = (canvas.getSize().height / 2);
          var delta   = 15;
          var radius1 = satellite.trajectory + delta;
          var radius2 = satellite.trajectory - delta;
          var deltaX  = Math.abs(mp.x - centerX);
          var deltaY  = Math.abs(mp.y - centerY);

          // distance between center sun to trajectory needed.
          if(this.pointInCircle(centerX, centerY, mp.x, mp.y, radius1) && 
             !this.pointInCircle(centerX, centerY, mp.x, mp.y, radius2)) {
            if(satellite.shape.hover && !satellite.hovering) {
              satellite.shape.hover.call(satellite.shape);
            }
            satellite.hovering = true;
          } else {
            if(satellite.shape.hout && satellite.hovering) {
              satellite.shape.hout.call(satellite.shape);
            }
            satellite.hovering = false;
          }   
        }

        // bubble stepper!
        satellite.shape.step(canvas);
      }
    }
  };

  SatellitedCircle.prototype.pointInCircle = function(cx, cy, mx, my, radius) {
    return Math.sqrt((mx - cx) * (mx - cx) + (my - cy) * (my - cy)) < radius;
  };

  SatellitedCircle.prototype.renderSatelliteHighlight = function(ctx, satellite) {
    // what to do when the user points at the trajectory of a satellite?
    if(satellite.hovering) {
      ctx.save();
        // clear a radius behind the satellite
        ctx.beginPath();
          ctx.fillStyle = '#000000';
          ctx.arc(satellite.shape.position.x, satellite.shape.position.y, satellite.shape.dimension.radius + 5, 0, 2 * Math.PI, false);
          ctx.fill();
        ctx.closePath();

        // the dotted line color
        if(this.style.path && !satellite.hovering) {
          ctx.strokeStyle = this.style.path;
        } else if(this.style.pathHover && satellite.hovering) {
          ctx.strokeStyle = this.style.pathHover;
        } else {
          ctx.strokeStyle = this.style.path;
        }

        // draw a small stroked circle around the satellite
        ctx.beginPath();
          var strokes = this.dottedPoints(satellite.shape.position.x, satellite.shape.position.y, satellite.shape.dimension.radius + 5, 1, 2);
          for(var p = 0; p < strokes.length; p++) {
            ctx.moveTo(strokes[p].x, strokes[p].y);
            ctx.lineTo(strokes[p].ex, strokes[p].ey);
          }

          ctx.lineWidth = 1;
          ctx.strokeStyle = this.style.stroke;
          ctx.stroke();
        ctx.closePath();
      ctx.restore();
    }
  }

  SatellitedCircle.prototype.renderSatellitePaths = function(ctx, satellite) {
    ctx.save();     
      ctx.shadowBlur = null;

      if(satellite.drawTrajectory) {
        ctx.save();
          if(this.style.path && !satellite.hovering) {
            ctx.strokeStyle = this.style.path;
          } else if(this.style.pathHover && satellite.hovering) {
            ctx.strokeStyle = this.style.pathHover;
          } else {
            ctx.strokeStyle = this.style.path;
          }

          ctx.beginPath();
            // dotted outline of the path
            var strokes = this.dottedPoints(this.position.x, this.position.y, satellite.trajectory, 1);
            for(var p = 0; p < strokes.length; p++){
              ctx.moveTo(strokes[p].x, strokes[p].y);
              ctx.lineTo(strokes[p].ex, strokes[p].ey);
            }

            ctx.lineWidth = 1;
            ctx.strokeStyle = this.style.stroke;
            ctx.stroke();
          ctx.closePath();
        ctx.restore();

        this.renderSatelliteHighlight(ctx, satellite);
      }
    
    ctx.restore();
  };

  SatellitedCircle.prototype.render = function(ctx) {
    SatellitedCircle.parent.prototype.render.call(this, ctx);

    for(var i in this.satellites) {
      if(this.satellites.hasOwnProperty(i)) {
        var satellite = this.satellites[i];
        this.renderSatellitePaths(ctx, satellite);
      }
    }

    var hoverTarget = null;
    for(var i in this.satellites) {
      if(this.satellites.hasOwnProperty(i)) {
        if(this.satellites[i].hovering) {
          hoverTarget = this.satellites[i];
        } else {
          ctx.save();
            ctx.shadowBlur = null;
            this.satellites[i].shape.render(ctx);
          ctx.restore(); 
        }
      }
    }

    if(hoverTarget) {
      ctx.save();
        ctx.shadowBlur = null;
        hoverTarget.shape.render(ctx);
      ctx.restore();
    }
  };

  /**
        Calculate the points required to have a dashed circle.

        Each item in the resulting list contains x and y for 
        starting coordinates. ex and ey are the ending coordinates
        for a dash.
    */
  SatellitedCircle.prototype.dottedPoints = function (cx,cy, rad, dashLength, dashDelta) {
    dashDelta = dashDelta || 1
    var n = rad/dashLength,
        alpha = Math.PI * 2 / (n * dashDelta),
        pointObj = {},
        points = [],
        i = -1;

    while( i < n * dashDelta) {
      var theta = alpha * i,
          theta2 = alpha * (i+1);

      points.push({x : (Math.cos(theta) * rad) + cx, y : (Math.sin(theta) * rad) + cy, ex : (Math.cos(theta2) * rad) + cx, ey : (Math.sin(theta2) * rad) + cy});
      i+=2;
    }              
    return points;            
  };

  root.SatellitedCircle = SatellitedCircle;
}(window));

;(function(root) {
  'use strict';

  var Sun = SatellitedCircle.extend(function Sun(x, y, radius, info) {
    Sun.parent.call(this, x, y, radius);
    this.style.infoFill = new Color(0, 0, 0, 0.7 * 255);
    this.style.infoLine = new Color(255, 255, 255);
    this.style.infoText = new Color(255, 255, 255);
    this.style.stroke    = null;
    this.style.glow      = Color.fromHex('#898900');
    this.style.path      = Color.fromHex('#232323');
    this.style.pathHover = Color.fromHex('#565656');
    this.style.gradient  = [Color.fromHex('#cece00'), Color.fromHex('#898900')];
    this.info = info;
    this.hovering = false;
    if(!this.info || typeof(this.info.data) !== 'object') {
      if('console' in window && 'warn' in console) {
        console.warn('No planetary information provided...');
      }
    }
  });

  Sun.prototype.spacedText = function(ctx, t1, t2, x, y, space) {
    ctx.fillText(t1, x, y);
    ctx.fillText(t2, x + space, y);
  };
  
  Sun.prototype.step = function(canvas) {
    Sun.parent.prototype.step.call(this, canvas);
    var centerX = canvas.getSize().width / 2;
    var centerY = canvas.getSize().height / 2;
    this.position.x = centerX;
    this.position.y = centerY;

    var mp = this.mousePositionMonitor.getMousePosition();
    this.hovering = this.pointInCircle(centerX, centerY, mp.x, mp.y, this.dimension.radius);

    // create radial gradient
    var grd = canvas.context.createRadialGradient(centerX, centerY, 10, centerX, centerY, this.dimension.radius);
    grd.addColorStop(0, this.style.gradient[0].toString());
    grd.addColorStop(1, this.style.gradient[1].toString());

    this.style.fill = grd;
  };

  Sun.prototype.render = function(ctx) {
    ctx.save();
      ctx.shadowBlur = 50;
      ctx.shadowColor = this.style.glow;
      Sun.parent.prototype.render.call(this, ctx);
    ctx.restore();
    
    // draw the text if hovering
    if(this.hovering) {
      ctx.save();
        var height = 102;
        var width  = 335;
        if(!this.info.data) {
          height = 33;
          width  = 75;
        }

        ctx.fillStyle = this.style.infoFill;
        ctx.strokeStyle = this.style.infoLine;
        ctx.fillRect(this.position.x + this.dimension.radius + 5, this.position.y - 16, width, height);
        ctx.strokeRect(this.position.x + this.dimension.radius + 5, this.position.y - 16, width, height);

        var fontstyle = '300 small-caps'
        var fontname = 'roboto';
        ctx.font = fontstyle + ' 18px ' + fontname;
        ctx.fillStyle = this.style.infoText;
        ctx.textBaseline = "middle"; 
        ctx.fillText(this.info.name, this.position.x + this.dimension.radius + 10, this.position.y);

        if(this.info.data) {
          var tx = this.position.x + this.dimension.radius + 10;
          var ty = this.position.y + 16;
          ctx.font = fontstyle + ' 14px ' + fontname;

          var space = 130;

          this.spacedText(ctx, "Mass:", this.info.data.mass + " Kg", tx, ty, space); ty += 14;
          this.spacedText(ctx, "Speed:", this.info.data.speed + " Kmph", tx, ty, space); ty += 14;
          this.spacedText(ctx, "Circumference:", this.info.data.circumference + " Km", tx, ty, space); ty += 14;
          this.spacedText(ctx, "Radius:", this.info.data.radius + " Km", tx, ty, space); ty += 14;
          this.spacedText(ctx, "Escape velocity:", this.info.data.escapeVelocity + " Km/s", tx, ty, space); ty += 14;

        }
      ctx.restore();
    }
  };

  root.Sun = Sun;
}(window));

;(function(root) {
  'use strict';

  var PlanetaryObject = SatellitedCircle.extend(function PlanetaryObject(radius, info) {
    PlanetaryObject.parent.call(this, 0, 0, radius);

    this.style.stroke = null;
    this.style.fill = Color.fromHex('#cecece');
    this.style.fillShade = Color.fromHex('#cecece');
    this.style.infoFill = new Color(0, 0, 0, 0.7 * 255);
    this.style.infoLine = new Color(255, 255, 255);
    this.style.infoText = new Color(255, 255, 255);
    this.angleToSun = 0;
    this.info = info;
    if(typeof(this.info.data) !== 'object') {
      if('console' in window && 'warn' in console) {
        console.warn('No planetary information provided...');
      }
    }
  });

  PlanetaryObject.prototype.hover = function() {
    this.hovering = true;
  };

  PlanetaryObject.prototype.hout = function() {
    this.hovering = false;
  };

  PlanetaryObject.prototype.step = function(canvas) {
    PlanetaryObject.parent.prototype.step.call(this, canvas);

    if(this.style.fill) {
      // the sun is always centered.
      var sunX    = canvas.getSize().width / 2;
      var sunY    = canvas.getSize().height / 2;

      // the position of this planet (center point)
      var centerX = this.position.x;
      var centerY = this.position.y;

      // calculate light source angle
      var angle   = Math.atan2(sunX - centerX, sunY - centerY);
      this.angleToSun = angle;
      // calculate light hit position, basically move gradient x and y slightly towards the sun
      var lightX = centerX + Math.sin(angle) * (this.dimension.radius / 2);
      var lightY = centerY + Math.cos(angle) * (this.dimension.radius / 2);

      // create radial gradient which displays the light
      var grd = canvas.context.createRadialGradient(lightX, lightY, this.dimension.radius / 2, lightX, lightY, this.dimension.radius);

      // add color stops based on stop1, darken and lighten them slightly
      grd.addColorStop(0, this.style.fillShade.shade(0.1)); // in light
      grd.addColorStop(1, this.style.fillShade.shade(-0.4)); // out of light

      this.style.fill = grd;
    }
  };

  PlanetaryObject.prototype.spacedText = function(ctx, t1, t2, x, y, space) {
    ctx.fillText(t1, x, y);
    ctx.fillText(t2, x + space, y);
  };

  PlanetaryObject.prototype.render = function(ctx) {
    // parent draws the object (SatellitedCircle)
    PlanetaryObject.parent.prototype.render.call(this, ctx);

    // draw the text if hovering
    if(this.hovering) {
      ctx.save();
        var height = 102;
        var width  = 252;
        if(!this.info.data) {
          height = 33;
          width  = 75;
        }

        ctx.fillStyle = this.style.infoFill;
        ctx.strokeStyle = this.style.infoLine;
        ctx.fillRect(this.position.x + this.dimension.radius + 5, this.position.y - 16, width, height);
        ctx.strokeRect(this.position.x + this.dimension.radius + 5, this.position.y - 16, width, height);

        var fontstyle = '300 small-caps'
        var fontname = 'roboto';
        ctx.font = fontstyle + ' 18px ' + fontname;
        ctx.fillStyle = this.style.infoText;
        ctx.textBaseline = "middle"; 
        ctx.fillText(this.info.name, this.position.x + this.dimension.radius + 10, this.position.y);

        if(this.info.data) {
          var tx = this.position.x + this.dimension.radius + 10;
          var ty = this.position.y + 16;
          ctx.font = fontstyle + ' 14px ' + fontname;

          var space = 130;

          this.spacedText(ctx, "Mass:", this.info.data.mass + " Kg", tx, ty, space); ty += 14;
          this.spacedText(ctx, "Speed:", this.info.data.speed + " Kmph", tx, ty, space); ty += 14;
          this.spacedText(ctx, "Circumference:", this.info.data.circumference + " Km", tx, ty, space); ty += 14;
          this.spacedText(ctx, "Radius:", this.info.data.radius + " Km", tx, ty, space); ty += 14;
          this.spacedText(ctx, "Escape velocity:", this.info.data.escapeVelocity + " Km/s", tx, ty, space); ty += 14;

        }
      ctx.restore();
    }
  };

  root.PlanetaryObject = PlanetaryObject;
}(window));

;(function(root) {
  'use strict';

  var Saturn = PlanetaryObject.extend(function Saturn() {
    Saturn.parent.apply(this, arguments);

    this.style.ringShade = Color.fromHex('#664d24');
    this.style.ringShade.a = 0.7 * 255;
    this.style.ringShade.updateHSL();
  });

  Saturn.prototype.step = function(canvas) {
    Saturn.parent.prototype.step.call(this, canvas);
  };

  Saturn.prototype.render = function(ctx) {
    // draw the rings
    ctx.save(); 
      var x = this.position.x;
      var y = this.position.y;
      var r = this.dimension.radius;

      // first ring / inner ring - darker
      ctx.beginPath();
        ctx.lineWidth = 3;
        ctx.strokeStyle = this.style.ringShade.shade(-0.3).toString();
        ctx.arc(x, y, r + 4, 0, 2 * Math.PI, false);
        ctx.stroke();
      ctx.closePath();

      // second ring / middle ring with lighter color
      ctx.beginPath();
        ctx.lineWidth = 4;
        ctx.strokeStyle = this.style.ringShade.shade(-0.1).toString();
        ctx.arc(x, y, r + 7, 0, 2 * Math.PI, false);
        ctx.stroke();
      ctx.closePath();

      // third ring / outer ring with a space fro the previous one caused by a moon
      ctx.beginPath();
        ctx.lineWidth = 3;
        ctx.strokeStyle = this.style.ringShade.shade(-0.3).toString();
        ctx.arc(x, y, r + 11, 0, 2 * Math.PI, false);
        ctx.stroke();
      ctx.closePath();
    ctx.restore();

    Saturn.parent.prototype.render.call(this, ctx);
  };

  root.Saturn = Saturn;
}(window));

;(function() {
  'use strict';
  
  window.addEventListener('load', function() {
    // No support? You're all out of luck!
    if(!Canvas.support) {
      alert('Your browser does not supports HTML5 Canvas!');
      return;
    }

    // Create a canvas instance
    var painting = document.getElementById("painting");
    var canvas   = new Canvas(painting);

    // FPSGraph will handle this!
    canvas.renderFPS = false;

    // mouse position monitor
    var mousePos = new MousePositionMonitor();

    // the objects
    var fpsGraph = new FPSGraph(-310, -50, 300, 40);
    fpsGraph.setMousePositionMonitor(mousePos);

    // data used for steps and size factors
    var data = {
      radStep: 34,
      sizes: {
        sun: 30,
        earth: 7
      },
      speeds: {
        earth: 0.01
      }
    };
    data.radCurr = data.sizes.sun + 20;

    data.planets = [
      {
        name: "Mercury",
        size: (data.sizes.earth * 0.3825650674192537),
        speed: (data.speeds.earth * 1.590),
        fill: Color.fromString('#aaa8ab'),
        data: {
          speed: 170505, // in kmph
          radius: 2440, // in km
          mass: 328.5E21, // in kg
          circumference: 15329, // in km
          escapeVelocity: 4.3 // in km/s
        }
      },
      {
        name: "Venus",
        size: (data.sizes.earth * 0.9488867983693948),
        speed: (data.speeds.earth * 1.176),
        fill: Color.fromString('#b06f25'),
        data: {
          speed: 126077, // in kmph again
          radius: 6052, // in km again
          mass: 4.867E24, // in kg
          circumference: 38025, // in km
          escapeVelocity: 10.36 // km/s
        }
      },
      {
        name: "Earth",
        size: data.sizes.earth,
        speed: data.speeds.earth,
        fill: Color.fromString('#565da4'),
        data: {
          speed: 108000, // in kmph again
          radius: 6371, // in km again
          mass: 5.972E24, // in kg
          circumference: 40075, // in km
          escapeVelocity: 11.19 // km/s
        },
        moons: [
          {
            name: "Moon",
            size: (data.sizes.earth * 0.2726416575105949),
            speed: (data.speeds.earth * 2.288),
            distance: 5,
            fill: Color.fromString('#bbbbbb')
          }
        ]
      },
      {
        name: "Mars",
        size: (data.sizes.earth * 0.5321417372216996),
        speed: (data.speeds.earth * 0.808),
        fill: Color.fromString('#c5a36b'),
        data: {
          speed: 86652, // in kmph again
          radius: 3390, // in km again
          mass: 639E21, // in kg
          circumference: 21344, // in km
          escapeVelocity: 5.03 // km/s
        },
        moons: [
          {
            name: "Phobos",
            size: (data.sizes.earth * 0.0017265735363365),
            speed: (data.speeds.earth * 2.238),
            distance: 3,
            fill: Color.fromString('#a6927d')
          },
          {
            name: "Deimos",
            size: (data.sizes.earth * 0.0011772092293204),
            speed: (data.speeds.earth * 1.238),
            distance: 8,
            fill: Color.fromString('#a39d91')
          }
        ]

      },
      {
        name: "Jupiter",
        size: (data.sizes.earth * 11.19002822201317),
        speed: (data.speeds.earth * 0.439),
        fill: Color.fromString('#a1846b'),
        data: {
          speed: 47016, // in kmph again
          radius: 69911, // in km again
          mass: 1.898E27, // in kg
          circumference: 449200, // in km
          escapeVelocity: 59.5 // km/s
        },
        moons: [
          {
            name: "Io",
            size: (data.sizes.earth * 0.2853555171872547),
            distance: 3,
            speed: (data.speeds.earth * 1.58),
            fill: Color.fromString('#d6cc6a')
          },
          {
            name: "Europa",
            size: (data.sizes.earth * 0.25),
            distance: 7,
            speed: (data.speeds.earth * 1.46),
            fill: Color.fromString('#e0d9e0')
          },
          {
            name: "Ganymede",
            size: (data.sizes.earth * 0.41),
            distance: 12,
            speed: (data.speeds.earth * 1.4648648648648649),
            fill: Color.fromString('#faf3d6')
          },
          {
            name: "Callisto",
            size: (data.sizes.earth * 0.37853),
            distance: 17,
            speed: (data.speeds.earth * 1.275),
            fill: Color.fromString('#faf3d6')
          }
        ]
      },
      {
        name: "Saturn",
        size: (data.sizes.earth * 9.410003135779241),
        speed: (data.speeds.earth * 0.325),
        fill: Color.fromString('#e2bc95'),
        data: {
          speed: 34848, // in kmph again
          radius: 58232, // in km again
          mass: 568.3E24, // in kg
          circumference: 378680, // in km
          escapeVelocity: 35.5 // km/s
        }
      },
      {
        name: "Uranus",
        size: (data.sizes.earth * 4.010034493571653),
        speed: (data.speeds.earth * 0.228),
        fill: Color.fromString('#c5ecf1'),
        data: {
          speed: 24480, // in kmph again
          radius: 25362, // in km again
          mass: 86.81E24, // in kg
          circumference: 160590, // in km
          escapeVelocity: 21.3 // km/s
        }
      },
      {
        name: "Neptune",
        size: (data.sizes.earth * 3.889934148635936),
        speed: (data.speeds.earth * 0.182),
        fill: Color.fromString('#3f6efa'),
        data: {
          speed: 19548, // in kmph again
          radius: 24622, // in km again
          mass: 102.4E24, // in kg
          circumference: 155600, // in km
          escapeVelocity: 23.5 // km/s
        }
      },
      {
        name: "Pluto",
        size: (data.sizes.earth * 0.1799937284415177),
        speed: (data.speeds.earth * 0.157),
        fill: Color.fromString('#8a715d'),
        data: {
          speed: 16812, // in kmph again
          radius: 1184, // in km again
          mass: 1.30900 * Math.pow(10, 22), // in kg
          circumference: 7232, // in km
          escapeVelocity: 1.2 // km/s
        }
      }
    ];

    for(var i in data.planets) {
      if(data.planets.hasOwnProperty(i)) {
        var planet = data.planets[i];
        if(planet.size > data.radStep - 25) {
          planet.size = data.radStep - 25;
        } else if (planet.size < 3) {
          planet.size = 3;
        }

        if(planet.moons) {
          for(var j in planet.moons) {
            if(planet.moons.hasOwnProperty(j)) {
              var moon = planet.moons[j];
              if(moon.size < 1) {
                moon.size = 1;
              }
            }
          }
        }
      }
    }

    // The sun, all planet renderables (Saturn or PlanetaryObjcet) will be added
    // the sun as satellite. The sun will then recursively render all of its 
    // children. The same goes for the moons that are added to planets. 
    var sun = new Sun(0, 0, data.sizes.sun, {
      name: "The Sun",
      data: {
        speed: 72000, // in kmph again
        radius: 695500, // in km again
        mass: 1.9891 * Math.pow(10, 30), // in kg
        circumference: 4369955, // in km
        escapeVelocity: 617.5 // km/s
      }
    });
    sun.setMousePositionMonitor(mousePos);

    for(var i in data.planets) {
      if(data.planets.hasOwnProperty(i)) {
        var planet = data.planets[i];
        var rndPosition = (Math.random() * (Math.PI * 2));
        if(typeof(window[planet.name]) === 'function') {
          planet.body = new window[planet.name](planet.size, planet);
        } else {
          planet.body = new PlanetaryObject(planet.size, planet);
        }

        if(planet.moons) {
          for(var j in planet.moons) {
            if(planet.moons.hasOwnProperty(j)) {
              var moon = planet.moons[j];

              if(typeof(window[moon.name]) === 'function') {
                moon.body = new window[moon.name](moon.size, moon);
              } else {
                moon.body = new PlanetaryObject(moon.size, moon);
              }

              if(moon.fill) {
                moon.body.style.fillShade = moon.fill;
              }

              var rndMoonPosition = (Math.random() * (Math.PI * 2));
              planet.body.addSatellite(moon.body, planet.size + moon.distance, moon.speed, rndMoonPosition, -1, false);
            }

          }
        }

        if(data.planets[i].fill) {
          data.planets[i].body.style.fillShade = data.planets[i].fill;
        }

        sun.addSatellite(data.planets[i].body, data.radCurr, data.planets[i].speed, rndPosition, -1, true);
        data.radCurr += data.radStep;
      }
    }

    // the sun and all of its satellites (all planets). A SatellitedCircle is recursively rendering
    canvas.addRenderable(sun);

    // the frames-per-second graph on the right 
    canvas.addRenderable(fpsGraph);

    // A resize handler to make sure the canvas fits!
    var resize = function(event) {
      canvas.resize(window.innerWidth, window.innerHeight);
    };

    // Attach the resize handler to window and call it once.
    window.addEventListener('resize', resize);
    resize();

    var fullScreen = (document.body.requestFullscreen || 
                      document.body.webkitRequestFullscreen || 
                      document.body.mozRequestFullScreen || 
                      document.body.msRequestFullscreen);

    if(fullScreen) {
      fullScreen = fullScreen.bind(document.body);

      // Go fullscreen when F is pressed
      window.addEventListener('keypress', function(e) {
        if(e.keyCode === 102 && !e.ctrlKey && !e.altKey && !e.shiftKey) {
          fullScreen();
        } 
      });
    }

    // The main loop, usually it should run at about 60FPS in Chrome and IE10+
    +(function animationLoop() {
      requestAnimFrame(animationLoop);

      canvas.step();   // step ALL the controllers containing models
      canvas.render(); // render ALL the views
    }());
    
    painting.addEventListener('contextmenu', function(e) {
      return e.preventDefault();
    });
  });
}());
              
            
!
999px

Console