API Docs for: 0.0.8
Show:

File: src\overtime.js

"use strict";

module.exports = Overtime;

var EventDispatcher = require("@zayesh/eventdispatcher");

/**
 * A time limit visualization library.
 *
 * @class Overtime
 * @constructor
 * @param {Object} [options] - The settings.
 * @param {Number} [options.time] - The time limit.
 * @param {Number} [options.canvas] - The canvas to use. A new one will be created if none is supplied.
 * @param {Boolean} [options.clearCanvas=true] - Whether the canvas should be cleared before rendering.
 * @param {Array} [options.size] - The size of the canvas.
 * @param {Overtime.TimeMeasure} [options.timeMeasure=Overtime.TimeMeasure.SECONDS] - The time measure of the supplied time limit.
 */

function Overtime(options)
{
 var self = this, o;

 EventDispatcher.call(this);

 /**
  * PI * 2.
  *
  * @property TWO_PI
  * @type Number
  * @private
  * @final
  */

 this.TWO_PI = Math.PI * 2.0;

 /**
  * PI / 2.
  *
  * @property HALF_PI
  * @type Number
  * @private
  * @final
  */

 this.HALF_PI = Math.PI * 0.5;

 /**
  * Clear canvas flag.
  *
  * @property clearCanvas
  * @type Boolean
  */

 this.clearCanvas = true;

 /**
  * Animation id of the currently requested frame.
  *
  * @property animId
  * @type Number
  * @private
  */

 this.animId = 0;

 /**
  * Used for time-based rendering.
  *
  * @property now
  * @type Number
  * @private
  */

 this.now = Date.now();

 /**
  * Used for time-based rendering.
  *
  * @property then
  * @type Number
  * @private
  */

 this.then = this.now;

 /**
  * The rendering context.
  *
  * @property ctx
  * @type CanvasRenderingContext2D
  * @private
  */

 this.ctx = null;

 // Set the initial canvas.
 this.canvas = document.createElement("canvas");

 /**
  * the start angle.
  *
  * @property startAngle
  * @type Number
  * @private
  */

 this.startAngle = -this.HALF_PI;

 /**
  * A float threshold for the chrome rendering hack.
  *
  * @property threshold
  * @type Number
  * @private
  */

 this.threshold = 0.023;

 /**
  * Radians of the full circle plus the start angle.
  *
  * @property fullCircle
  * @type Number
  * @private
  */

 this.fullCircle = this.startAngle + this.TWO_PI;

 /**
  * Colour of the progressing circle.
  *
  * @property primaryStrokeStyle
  * @type String
  * @default rgba(255, 100, 0, 0.9)
  */

 this.primaryStrokeStyle = "rgba(255, 100, 0, 0.9)";

 /**
  * Colour of the empty circle.
  *
  * @property secondaryStrokeStyle
  * @type String
  * @default rgba(0, 0, 0, 0.1)
  */

 this.secondaryStrokeStyle = "rgba(0, 0, 0, 0.1)";

 /**
  * Returns the remaining time.
  *
  * @event update
  * @param {Number} time - The remaining time.
  */

 this.updateEvent = {type: "update", time: 0};

 /**
  * The currently set time measure.
  *
  * @property tm
  * @type Overtime.TimeMeasure
  * @private
  */

 this.tm = Overtime.TimeMeasure.MILLISECONDS;

 /**
  * The remaining time in milliseconds.
  *
  * @property t
  * @type Number
  * @private
  */

 this.t = 1;

 // Overwrite the defaults.
 if(options !== undefined)
 {
  if(options.timeMeasure > 0) { this.tm = options.timeMeasure; }
  if(options.time > 0) { this.t = options.time; }
  if(options.canvas !== undefined) { this.canvas = options.canvas; }
  this.size = options.size;
 }

 // Update the time.
 this.t *= this.tm;

 /**
  * The total time.
  *
  * @property T
  * @type Number
  * @private
  */

 this.T = this.t;

 // Try to recover time values from a previous session.
 if(localStorage.getItem("overtime"))
 {
  try
  {
   o = JSON.parse(localStorage.getItem("overtime"));
   if(o.tm !== undefined) { this.tm = o.tm; }
   if(o.t !== undefined) { this.t = o.t; }
   if(o.T !== undefined) { this.T = o.T; }
  }
  catch(e) { /* Swallow. */ }
 }

 /**
  * Stores the time values for the next session.
  *
  * @method persist
  * @private
  */

 window.addEventListener("unload", function persist()
 {
  localStorage.setItem("overtime", JSON.stringify({
   tm: self.tm,
   t: self.t,
   T: self.T
  }));
 });

 /**
  * The update function.
  *
  * @method update
  */

 this.update = function() { self._update(); };
}

Overtime.prototype = Object.create(EventDispatcher.prototype);
Overtime.prototype.constructor = Overtime;

/**
 * The internal canvas.
 *
 * @property canvas
 * @type HTMLCanvasElement
 */

Object.defineProperty(Overtime.prototype, "canvas", {
 get: function() { return this.ctx.canvas; },
 set: function(c)
 {
  if(c !== undefined && c.getContext !== undefined)
  {
   this.stop();
   this.ctx = c.getContext("2d");
   this.ctx.strokeStyle = this.primaryStrokeStyle;
   this.size = [c.width, c.height];
  }
 }
});

/**
 * The time. When set, the given value will be translated to the current time measure.
 *
 * @property time
 * @type Number
 */

Object.defineProperty(Overtime.prototype, "time", {
 get: function() { return this.t; },
 set: function(t)
 {
  if(t >= 0)
  {
   this.stop();
   this.t = t * this.tm;
   this.T = this.t;
   this._render();
  }
 }
});

/**
 * The current time measure.
 * The current time will not be affected by this in any way.
 *
 * @property timeMeasure
 * @type Overtime.TimeMeasure
 */

Object.defineProperty(Overtime.prototype, "timeMeasure", {
 get: function() { return this.tm; },
 set: function(tm)
 {
  if(tm > 0)
  {
   this.tm = tm;
  }
 }
});

/**
 * The size of the canvas.
 *
 * @property size
 * @type Number
 * @example
 *  [width, height]
 */

Object.defineProperty(Overtime.prototype, "size", {
 get: function()
 {
  return [
   this.ctx.canvas.width,
   this.ctx.canvas.height
  ];
 },
 set: function(s)
 {
  if(s !== undefined)
  {
   this.ctx.canvas.width = s[0];
   this.ctx.canvas.height = s[1];
   this.ctx.lineWidth = (s[0] < s[1]) ? s[0] * 0.05 : s[1] * 0.05;
   this._render();
  }
 }
});

/**
 * Renders the time progress on the canvas.
 *
 * @method _render
 * @private
 */

Overtime.prototype._render = function()
{
 var ctx = this.ctx,
  w = ctx.canvas.width,
  h = ctx.canvas.height,
  hw = w >> 1, hh = h >> 1,
  radius = w < h ? hw : hh,
  endAngle,
  tooThin; // Chrome hack.

 if(this.clearCanvas) { ctx.clearRect(0, 0, w, h); }

 // Don't bleed over the edge.
 radius -= ctx.lineWidth;

 // Draw the progress.
 endAngle = this.startAngle + this.TWO_PI * ((this.T - this.t) / this.T);
 tooThin = (endAngle - this.startAngle < this.threshold); // Chrome hack.
 ctx.strokeStyle = this.primaryStrokeStyle;
 ctx.beginPath();
 ctx.arc(hw, hh, radius, tooThin ? this.startAngle - this.threshold : this.startAngle, endAngle, false); // Chrome hack.
 //ctx.arc(hw, hh, radius, this.startAngle, endAngle, false);
 ctx.stroke();
 if(tooThin) { ctx.clearRect(0, 0, hw - this.threshold, hh); } // Chrome hack.

 // Draw the rest of the circle in another color.
 if(endAngle < this.fullCircle)
 {
  // No hacking here cause can't clear.
  ctx.strokeStyle = this.secondaryStrokeStyle;
  ctx.beginPath();
  ctx.arc(hw, hh, radius, endAngle, this.fullCircle, false);
  ctx.stroke();
 }
};

/**
 * Steps the system forward.
 * This is the main loop.
 *
 * @method _update
 * @private
 */

Overtime.prototype._update = function()
{
 var elapsed;

 // Calculate the time span between this run and the last.
 this.now = Date.now();
 elapsed = this.now - this.then;
 this.then = this.now;

 // Update the time.
 this.t -= elapsed;
 if(this.t < 0) { this.t = 0; }
 this.updateEvent.time = this.t;
 this.dispatchEvent(this.updateEvent);

 // Render the time.
 this._render();

 // Continue or exit.
 if(this.t > 0)
 {
  this.animId = requestAnimationFrame(this.update);
 }
 else
 {
  this.dispatchEvent({type: "elapsed"});
 }
};

/**
 * Stops the rendering cycle. Does nothing else besides that.
 *
 * @method stop
 */

Overtime.prototype.stop = function()
{
 if(this.animId !== 0)
 {
  cancelAnimationFrame(this.animId);
  this.animId = 0;
 }
};

/**
 * Tries to start the rendering cycle if it isn't
 * running. Otherwise it restarts it.
 *
 * @method start
 */

Overtime.prototype.start = function()
{
 this.stop();
 this.now = Date.now();
 this.then = this.now;
 this.update();
};

/**
 * Sets the time back to its original length.
 *
 * @method rewind
 */

Overtime.prototype.rewind = function()
{
 this.stop();
 this.t = this.T;
 this._render();
};

/**
 * Sets the time back by the given value.
 * The time will not go back beyond the initial length.
 *
 * @method rewindBy
 * @param {Number} t - The time by which to rewind. Interpreted according to the current time measure. A negative value corresponds to fast-forwarding.
 */

Overtime.prototype.rewindBy = function(t)
{
 if(typeof t === "number" && !isNaN(t) && t !== 0)
 {
  this.stop();
  this.t += t * this.tm;
  if(this.t > this.T) { this.t = this.T; }
  this._render();
 }
};

/**
 * Goes ahead in time by a given value.
 *
 * @method advanceBy
 * @param {Number} t - The time value by which to rewind. Will be interpreted according to the current time measure. A negative value corresponds to rewinding.
 */

Overtime.prototype.advanceBy = function(t)
{
 if(typeof t === "number" && !isNaN(t))
 {
  this.rewindBy(-t);
 }
};

/**
 * Adds time.
 *
 * @method prolongBy
 * @param {Number} t - The time value to add. Will be interpreted according to the current time measure. A negative value corresponds to shortening.
 */

Overtime.prototype.prolongBy = function(t)
{
 if(typeof t === "number" && !isNaN(t) && t !== 0)
 {
  this.stop();
  t *= this.tm;
  this.t += t;
  this.T += t;
  if(this.T < 0) { this.T = this.t = 0; }
  this._render();
 }
};

/**
 * Reduces the total duration of the countdown.
 *
 * @method shortenBy
 * @param {Number} t - The time value to subtract. Will be interpreted according to the current time measure. A negative value corresponds to prolonging.
 */

Overtime.prototype.shortenBy = function(t)
{
 if(typeof t === "number" && !isNaN(t))
 {
  this.prolongBy(-t);
 }
};

/**
 * Enumeration of time measure constants.
 *
 * @property TimeMeasure
 * @type Object
 * @static
 * @final
 */

Overtime.TimeMeasure = Object.freeze({
 MILLISECONDS: 1,
 SECONDS: 1000,
 MINUTES: 60000,
 HOURS: 3600000
});