/**
* Basic structure: TC_Class is the public class that is returned upon being called
*
* So, if you do
* var tc = $(".timer").TimeCircles();
*
* tc will contain an instance of the public TimeCircles class. It is important to
* note that TimeCircles is not chained in the conventional way, check the
* documentation for more info on how TimeCircles can be chained.
*
* After being called/created, the public TimerCircles class will then- for each element
* within it's collection, either fetch or create an instance of the private class.
* Each function called upon the public class will be forwarded to each instance
* of the private classes within the relevant element collection
**/
(function($) {
// Used to disable some features on IE8
var limited_mode = false;
var tick_duration = 200; // in ms
var debug = (location.hash === "#debug");
function debug_log(msg) {
if (debug) {
console.log(msg);
}
}
var allUnits = ["Days", "Hours", "Minutes", "Seconds"];
var nextUnits = {
Seconds: "Minutes",
Minutes: "Hours",
Hours: "Days",
Days: "Years"
};
var secondsIn = {
Seconds: 1,
Minutes: 60,
Hours: 3600,
Days: 86400,
Months: 2678400,
Years: 31536000
};
/**
* Converts hex color code into object containing integer values for the r,g,b use
* This function (hexToRgb) originates from:
* http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
* @param {string} hex color code
*/
function hexToRgb(hex) {
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, function(m, r, g, b) {
return r + r + g + g + b + b;
});
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function isCanvasSupported() {
var elem = document.createElement('canvas');
return !!(elem.getContext && elem.getContext('2d'));
}
/**
* Function s4() and guid() originate from:
* http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
*/
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
/**
* Creates a unique id
* @returns {String}
*/
function guid() {
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
/**
* Array.prototype.indexOf fallback for IE8
* @param {Mixed} mixed
* @returns {Number}
*/
if (!Array.prototype.indexOf)
{
Array.prototype.indexOf = function(elt /*, from*/)
{
var len = this.length >>> 0;
var from = Number(arguments[1]) || 0;
from = (from < 0)
? Math.ceil(from)
: Math.floor(from);
if (from < 0)
from += len;
for (; from < len; from++)
{
if (from in this &&
this[from] === elt)
return from;
}
return -1;
};
}
function parse_date(str) {
var match = str.match(/^[0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-9]{1,2}:[0-9]{2}:[0-9]{2}$/);
if (match !== null && match.length > 0) {
var parts = str.split(" ");
var date = parts[0].split("-");
var time = parts[1].split(":");
return new Date(date[0], date[1] - 1, date[2], time[0], time[1], time[2]);
}
// Fallback for different date formats
var d = Date.parse(str);
if (!isNaN(d))
return d;
d = Date.parse(str.replace(/-/g, '/').replace('T', ' '));
if (!isNaN(d))
return d;
// Cant find anything
return new Date();
}
function parse_times(diff, old_diff, total_duration, units, floor) {
var raw_time = {};
var raw_old_time = {};
var time = {};
var pct = {};
var old_pct = {};
var old_time = {};
var greater_unit = null;
for (var i in units) {
var unit = units[i];
var maxUnits;
if (greater_unit === null) {
maxUnits = total_duration / secondsIn[unit];
}
else {
maxUnits = secondsIn[greater_unit] / secondsIn[unit];
}
var curUnits = (diff / secondsIn[unit]);
var oldUnits = (old_diff / secondsIn[unit]);
if (floor)
curUnits = Math.floor(curUnits);
if (floor)
oldUnits = Math.floor(oldUnits);
if (unit !== "Days") {
curUnits = curUnits % maxUnits;
oldUnits = oldUnits % maxUnits;
}
raw_time[unit] = curUnits;
time[unit] = Math.abs(curUnits);
raw_old_time[unit] = oldUnits;
old_time[unit] = Math.abs(oldUnits);
pct[unit] = Math.abs(curUnits) / maxUnits;
old_pct[unit] = Math.abs(oldUnits) / maxUnits;
greater_unit = unit;
}
return {
raw_time: raw_time,
raw_old_time: raw_old_time,
time: time,
old_time: old_time,
pct: pct,
old_pct: old_pct
};
}
var TC_Instance_List = {};
// Try fetch/share instance
if (window !== window.top && typeof window.top.TC_Instance_List !== "undefined") {
TC_Instance_List = window.top.TC_Instance_List;
}
else {
window.top.TC_Instance_List = TC_Instance_List;
}
(function() {
var vendors = ['webkit', 'moz'];
for (var x = 0; x < vendors.length && !window.top.requestAnimationFrame; ++x) {
window.top.requestAnimationFrame = window.top[vendors[x] + 'RequestAnimationFrame'];
window.top.cancelAnimationFrame = window.top[vendors[x] + 'CancelAnimationFrame'];
}
if (!window.top.requestAnimationFrame || !window.top.cancelAnimationFrame) {
window.top.requestAnimationFrame = function(callback, element, instance) {
if (typeof instance === "undefined")
instance = {data: {last_frame: 0}};
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - instance.data.last_frame));
var id = window.top.setTimeout(function() {
callback(currTime + timeToCall);
}, timeToCall);
instance.data.last_frame = currTime + timeToCall;
return id;
};
window.top.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}
})();
var TC_Instance = function(element, options) {
this.element = element;
this.container;
this.listeners = null;
this.data = {
paused: false,
last_frame: 0,
animation_frame: null,
timer: false,
total_duration: null,
prev_time: null,
drawn_units: [],
text_elements: {
Days: null,
Hours: null,
Minutes: null,
Seconds: null
},
attributes: {
canvas: null,
context: null,
item_size: null,
line_width: null,
radius: null,
outer_radius: null
},
state: {
fading: {
Days: false,
Hours: false,
Minutes: false,
Seconds: false
}
}
};
this.config = null;
this.setOptions(options);
this.initialize();
};
TC_Instance.prototype.initialize = function(clear_listeners) {
// Initialize drawn units
this.data.drawn_units = [];
for (var unit in this.config.time) {
if (this.config.time[unit].show) {
this.data.drawn_units.push(unit);
}
}
// Avoid stacking
$(this.element).children('div.time_circles').remove();
if (typeof clear_listeners === "undefined")
clear_listeners = true;
if (clear_listeners || this.listeners === null) {
this.listeners = {all: [], visible: []};
}
this.container = $("
");
this.container.addClass('time_circles');
this.container.appendTo(this.element);
// Determine the needed width and height of TimeCircles
var height = this.element.offsetHeight;
var width = this.element.offsetWidth;
if (height === 0)
height = $(this.element).height();
if (width === 0)
width = $(this.element).width();
if (height === 0 && width > 0)
height = width / this.data.drawn_units.length;
else if (width === 0 && height > 0)
width = height * this.data.drawn_units.length;
// Create our canvas and set it to the appropriate size
var canvasElement = document.createElement('canvas');
canvasElement.width = width;
canvasElement.height = height;
// Add canvas elements
this.data.attributes.canvas = $(canvasElement);
this.data.attributes.canvas.appendTo(this.container);
// Check if the browser has browser support
var canvasSupported = isCanvasSupported();
// If the browser doesn't have browser support, check if explorer canvas is loaded
// (A javascript library that adds canvas support to browsers that don't have it)
if(!canvasSupported && typeof G_vmlCanvasManager !== "undefined") {
G_vmlCanvasManager.initElement(canvasElement);
limited_mode = true;
canvasSupported = true;
}
if(canvasSupported) {
this.data.attributes.context = canvasElement.getContext('2d');
}
this.data.attributes.item_size = Math.min(width / this.data.drawn_units.length, height);
this.data.attributes.line_width = this.data.attributes.item_size * this.config.fg_width;
this.data.attributes.radius = ((this.data.attributes.item_size * 0.8) - this.data.attributes.line_width) / 2;
this.data.attributes.outer_radius = this.data.attributes.radius + 0.5 * Math.max(this.data.attributes.line_width, this.data.attributes.line_width * this.config.bg_width);
// Prepare Time Elements
var i = 0;
for (var key in this.data.text_elements) {
if (!this.config.time[key].show)
continue;
var textElement = $("
");
textElement.addClass('textDiv_' + key);
textElement.css("top", Math.round(0.35 * this.data.attributes.item_size));
textElement.css("left", Math.round(i++ * this.data.attributes.item_size));
textElement.css("width", this.data.attributes.item_size);
textElement.appendTo(this.container);
var headerElement = $("
");
headerElement.text(this.config.time[key].text); // Options
headerElement.css("font-size", Math.round(0.07 * this.data.attributes.item_size));
headerElement.css("line-height", Math.round(0.07 * this.data.attributes.item_size) + "px");
headerElement.appendTo(textElement);
var numberElement = $("");
numberElement.css("font-size", Math.round(0.21 * this.data.attributes.item_size));
numberElement.css("line-height", Math.round(0.07 * this.data.attributes.item_size) + "px");
numberElement.appendTo(textElement);
this.data.text_elements[key] = numberElement;
}
if (this.config.start && this.data.paused === false)
this.start();
};
TC_Instance.prototype.update = function() {
if(limited_mode) {
//Per unit clearing doesn't work in IE8 using explorer canvas, so do it in one time. The downside is that radial fade cant be used
this.data.attributes.context.clearRect(0, 0, this.data.attributes.canvas[0].width, this.data.attributes.canvas[0].hright);
}
var diff, old_diff;
var prevDate = this.data.prev_time;
var curDate = new Date();
this.data.prev_time = curDate;
if (prevDate === null)
prevDate = curDate;
// If not counting past zero, and time < 0, then simply draw the zero point once, and call stop
if (!this.config.count_past_zero) {
if (curDate > this.data.attributes.ref_date) {
for (var i in this.data.drawn_units) {
// TODO: listeners!
var key = this.data.drawn_units[i];
// Set the text value
this.data.text_elements[key].text("0");
var x = (i * this.data.attributes.item_size) + (this.data.attributes.item_size / 2);
var y = this.data.attributes.item_size / 2;
var color = this.config.time[key].color;
this.drawArc(x, y, color, 0);
}
this.stop();
return;
}
}
// Compare current time with reference
diff = (this.data.attributes.ref_date - curDate) / 1000;
old_diff = (this.data.attributes.ref_date - prevDate) / 1000;
var floor = this.config.animation !== "smooth";
var visible_times = parse_times(diff, old_diff, this.data.total_duration, this.data.drawn_units, floor);
var all_times = parse_times(diff, old_diff, secondsIn["Years"], allUnits, floor);
var i = 0;
var j = 0;
var lastKey = null;
var cur_shown = this.data.drawn_units.slice();
for (var i in allUnits) {
var key = allUnits[i];
// Notify (all) listeners
if (Math.floor(all_times.raw_time[key]) !== Math.floor(all_times.raw_old_time[key])) {
this.notifyListeners(key, Math.floor(all_times.time[key]), Math.floor(diff), "all");
}
if (cur_shown.indexOf(key) < 0)
continue;
// Notify (visible) listeners
if (Math.floor(visible_times.raw_time[key]) !== Math.floor(visible_times.raw_old_time[key])) {
this.notifyListeners(key, Math.floor(visible_times.time[key]), Math.floor(diff), "visible");
}
// Set the text value
this.data.text_elements[key].text(Math.floor(Math.abs(visible_times.time[key])));
var x = (j * this.data.attributes.item_size) + (this.data.attributes.item_size / 2);
var y = this.data.attributes.item_size / 2;
var color = this.config.time[key].color;
if (this.config.animation === "smooth") {
if (lastKey !== null && !limited_mode) {
if (Math.floor(visible_times.time[lastKey]) > Math.floor(visible_times.old_time[lastKey])) {
this.radialFade(x, y, color, 1, key);
this.data.state.fading[key] = true;
}
else if (Math.floor(visible_times.time[lastKey]) < Math.floor(visible_times.old_time[lastKey])) {
this.radialFade(x, y, color, 0, key);
this.data.state.fading[key] = true;
}
}
if (!this.data.state.fading[key]) {
this.drawArc(x, y, color, visible_times.pct[key]);
}
}
else {
this.animateArc(x, y, color, visible_times.pct[key], visible_times.old_pct[key], (new Date()).getTime() + tick_duration);
}
lastKey = key;
j++;
}
// We need this for our next frame either way
var _this = this;
var update = function() {
_this.update.call(_this);
};
// Either call next update immediately, or in a second
if (this.config.animation === "smooth") {
// Smooth animation, Queue up the next frame
this.data.animation_frame = window.top.requestAnimationFrame(update, _this.element, _this);
}
else {
// Tick animation, Don't queue until very slightly after the next second happens
var delay = (diff % 1) * 1000;
if (delay < 0)
delay = 1000 + delay;
delay += 50;
_this.data.animation_frame = window.top.setTimeout(function() {
_this.data.animation_frame = window.top.requestAnimationFrame(update, _this.element, _this);
}, delay);
}
};
TC_Instance.prototype.animateArc = function(x, y, color, target_pct, cur_pct, animation_end) {
if (this.data.attributes.context === null)
return;
var diff = cur_pct - target_pct;
if (Math.abs(diff) > 0.5) {
if (target_pct === 0) {
this.radialFade(x, y, color, 1);
}
else {
this.radialFade(x, y, color, 0);
}
}
else {
var progress = (tick_duration - (animation_end - (new Date()).getTime())) / tick_duration;
if (progress > 1)
progress = 1;
var pct = (cur_pct * (1 - progress)) + (target_pct * progress);
this.drawArc(x, y, color, pct);
//var show_pct =
if (progress >= 1)
return;
var _this = this;
window.top.requestAnimationFrame(function() {
_this.animateArc(x, y, color, target_pct, cur_pct, animation_end);
}, this.element);
}
};
TC_Instance.prototype.drawArc = function(x, y, color, pct) {
if (this.data.attributes.context === null)
return;
var clear_radius = Math.max(this.data.attributes.outer_radius, this.data.attributes.item_size / 2);
if(!limited_mode) {
this.data.attributes.context.clearRect(
x - clear_radius,
y - clear_radius,
clear_radius * 2,
clear_radius * 2
);
}
if (this.config.use_background) {
this.data.attributes.context.beginPath();
this.data.attributes.context.arc(x, y, this.data.attributes.radius, 0, 2 * Math.PI, false);
this.data.attributes.context.lineWidth = this.data.attributes.line_width * this.config.bg_width;
// line color
this.data.attributes.context.strokeStyle = this.config.circle_bg_color;
this.data.attributes.context.stroke();
}
// Direction
var startAngle, endAngle, counterClockwise;
var defaultOffset = (-0.5 * Math.PI);
var fullCircle = 2 * Math.PI;
startAngle = defaultOffset + (this.config.start_angle / 360 * fullCircle);
var offset = (2 * pct * Math.PI);
if (this.config.direction === "Both") {
counterClockwise = false;
startAngle -= (offset / 2);
endAngle = startAngle + offset;
}
else {
if (this.config.direction === "Clockwise") {
counterClockwise = false;
endAngle = startAngle + offset;
}
else {
counterClockwise = true;
endAngle = startAngle - offset;
}
}
this.data.attributes.context.beginPath();
this.data.attributes.context.arc(x, y, this.data.attributes.radius, startAngle, endAngle, counterClockwise);
this.data.attributes.context.lineWidth = this.data.attributes.line_width;
// line color
this.data.attributes.context.strokeStyle = color;
this.data.attributes.context.stroke();
};
TC_Instance.prototype.radialFade = function(x, y, color, from, key) {
// TODO: Make fade_time option
var rgb = hexToRgb(color);
var _this = this; // We have a few inner scopes here that will need access to our instance
var step = 0.2 * ((from === 1) ? -1 : 1);
var i;
for (i = 0; from <= 1 && from >= 0; i++) {
// Create inner scope so our variables are not changed by the time the Timeout triggers
(function() {
var delay = 50 * i;
var rgba = "rgba(" + rgb.r + ", " + rgb.g + ", " + rgb.b + ", " + (Math.round(from * 10) / 10) + ")";
window.top.setTimeout(function() {
_this.drawArc(x, y, rgba, 1);
}, delay);
}());
from += step;
}
if (typeof key !== undefined) {
window.top.setTimeout(function() {
_this.data.state.fading[key] = false;
}, 50 * i);
}
};
TC_Instance.prototype.timeLeft = function() {
var now = new Date();
return ((this.data.attributes.ref_date - now) / 1000);
};
TC_Instance.prototype.start = function() {
window.top.cancelAnimationFrame(this.data.animation_frame);
window.top.clearTimeout(this.data.animation_frame)
// Check if a date was passed in html attribute or jquery data
var attr_data_date = $(this.element).data('date');
if (typeof attr_data_date === "undefined") {
attr_data_date = $(this.element).attr('data-date');
}
if (typeof attr_data_date === "string") {
this.data.attributes.ref_date = parse_date(attr_data_date);
}
// Check if this is an unpause of a timer
else if (typeof this.data.timer === "number") {
if (this.data.paused) {
this.data.attributes.ref_date = (new Date()).getTime() + (this.data.timer * 1000);
}
}
else {
// Try to get data-timer
var attr_data_timer = $(this.element).data('timer');
if (typeof attr_data_timer === "undefined") {
attr_data_timer = $(this.element).attr('data-timer');
}
if (typeof attr_data_timer === "string") {
attr_data_timer = parseFloat(attr_data_timer);
}
if (typeof attr_data_timer === "number") {
this.data.timer = attr_data_timer;
this.data.attributes.ref_date = (new Date()).getTime() + (attr_data_timer * 1000);
}
else {
// data-timer and data-date were both not set
// use config date
this.data.attributes.ref_date = this.config.ref_date;
}
}
// Start running
this.data.paused = false;
this.update.call(this);
};
TC_Instance.prototype.restart = function() {
this.data.timer = false;
this.start();
};
TC_Instance.prototype.stop = function() {
if (typeof this.data.timer === "number") {
this.data.timer = this.timeLeft(this);
}
// Stop running
this.data.paused = true;
window.top.cancelAnimationFrame(this.data.animation_frame);
};
TC_Instance.prototype.destroy = function() {
this.stop();
this.container.remove();
$(this.element).removeAttr('data-tc-id');
$(this.element).removeData('tc-id');
};
TC_Instance.prototype.setOptions = function(options) {
if (this.config === null) {
this.default_options.ref_date = new Date();
this.config = $.extend(true, {}, this.default_options);
}
$.extend(true, this.config, options);
this.data.total_duration = this.config.total_duration;
if (typeof this.data.total_duration === "string") {
if (typeof secondsIn[this.data.total_duration] !== "undefined") {
// If set to Years, Months, Days, Hours or Minutes, fetch the secondsIn value for that
this.data.total_duration = secondsIn[this.data.total_duration];
}
else if (this.data.total_duration === "Auto") {
// If set to auto, total_duration is the size of 1 unit, of the unit type bigger than the largest shown
for (var unit in this.config.time) {
if (this.config.time[unit].show) {
this.data.total_duration = secondsIn[nextUnits[unit]];
break;
}
}
}
else {
// If it's a string, but neither of the above, user screwed up.
this.data.total_duration = secondsIn["Years"];
console.error("Valid values for TimeCircles config.total_duration are either numeric, or (string) Years, Months, Days, Hours, Minutes, Auto");
}
}
};
TC_Instance.prototype.addListener = function(f, context, type) {
if (typeof f !== "function")
return;
if (typeof type === "undefined")
type = "visible";
this.listeners[type].push({func: f, scope: context});
};
TC_Instance.prototype.notifyListeners = function(unit, value, total, type) {
for (var i = 0; i < this.listeners[type].length; i++) {
var listener = this.listeners[type][i];
listener.func.apply(listener.scope, [unit, value, total]);
}
};
TC_Instance.prototype.default_options = {
ref_date: new Date(),
start: true,
animation: "smooth",
count_past_zero: true,
circle_bg_color: "#60686F",
use_background: true,
fg_width: 0.1,
bg_width: 1.2,
total_duration: "Auto",
direction: "Clockwise",
start_angle: 0,
time: {
Days: {
show: true,
text: "Days",
color: "#FC6"
},
Hours: {
show: true,
text: "Hours",
color: "#9CF"
},
Minutes: {
show: true,
text: "Minutes",
color: "#BFB"
},
Seconds: {
show: true,
text: "Seconds",
color: "#F99"
}
}
};
// Time circle class
var TC_Class = function(elements, options) {
this.elements = elements;
this.options = options;
this.foreach();
};
TC_Class.prototype.getInstance = function(element) {
var instance;
var cur_id = $(element).data("tc-id");
if (typeof cur_id === "undefined") {
cur_id = guid();
$(element).attr("data-tc-id", cur_id);
}
if (typeof TC_Instance_List[cur_id] === "undefined") {
var options = this.options;
var element_options = $(element).data('options');
if (typeof element_options === "string") {
element_options = JSON.parse(element_options);
}
if (typeof element_options === "object") {
options = $.extend(true, {}, this.options, element_options);
}
instance = new TC_Instance(element, options);
TC_Instance_List[cur_id] = instance;
}
else {
instance = TC_Instance_List[cur_id];
if (typeof this.options !== "undefined") {
instance.setOptions(this.options);
}
}
return instance;
};
TC_Class.prototype.foreach = function(callback) {
var _this = this;
this.elements.each(function() {
var instance = _this.getInstance(this);
if (typeof callback === "function") {
callback(instance);
}
});
return this;
};
TC_Class.prototype.start = function() {
this.foreach(function(instance) {
instance.start();
});
return this;
};
TC_Class.prototype.stop = function() {
this.foreach(function(instance) {
instance.stop();
});
return this;
};
TC_Class.prototype.restart = function() {
this.foreach(function(instance) {
instance.restart();
});
return this;
};
TC_Class.prototype.rebuild = function() {
this.foreach(function(instance) {
instance.initialize(false);
});
return this;
};
TC_Class.prototype.getTime = function() {
return this.getInstance(this.elements[0]).timeLeft();
};
TC_Class.prototype.addListener = function(f, type) {
if (typeof type === "undefined")
type = "visible";
var _this = this;
this.foreach(function(instance) {
instance.addListener(f, _this.elements, type);
});
return this;
};
TC_Class.prototype.destroy = function() {
this.foreach(function(instance) {
instance.destroy();
});
return this;
};
TC_Class.prototype.end = function() {
return this.elements;
};
$.fn.TimeCircles = function(options) {
return new TC_Class(this, options);
};
}(jQuery));