/**
* 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($) {
/**
* 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 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();
}
var TC_Instance_List = {};
var TC_Instance = function(element, options) {
this.element = element;
this.container;
this.timer = null;
this.data = {
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.listeners = [];
this.config = null;
this.setOptions(options);
this.container = $("<div>");
this.container.addClass('time_circles');
this.container.appendTo(this.element);
this.data.attributes.canvas = $("<canvas>");
this.data.attributes.context = this.data.attributes.canvas[0].getContext('2d');
var height = this.element.offsetHeight;
var width = this.element.offsetWidth;
if(height === 0 && width > 0) height = width / 4;
else if(width === 0 && height > 0) width = height * 4;
this.data.attributes.canvas[0].height = height;
this.data.attributes.canvas[0].width = width;
this.data.attributes.canvas.appendTo(this.container);
this.data.attributes.item_size = Math.min(this.data.attributes.canvas[0].width / 4, this.data.attributes.canvas[0].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) {
var textElement = $("<div>");
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 numberElement = $("<span>");
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);
var headerElement = $("<h4>");
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);
this.data.text_elements[key] = numberElement;
}
if (this.config.start)
this.start();
};
TC_Instance.prototype.updateArc = function() {
var diff, old_diff;
var interval = (1000 * this.config.refresh_interval);
var curDate = new Date();
// Compare current time with reference
if (this.config.count_past_zero) {
var prevDate = curDate - interval;
diff = Math.abs(curDate - this.data.attributes.ref_date) / 1000;
old_diff = Math.abs(this.data.attributes.ref_date - prevDate) / 1000;
}
else {
diff = Math.max(this.data.attributes.ref_date - curDate, 0) / 1000;
old_diff = diff + (curDate > this.data.attributes.ref_date) ? 0 : interval;
}
var time = {
Days: (diff / 60 / 60 / 24),
Hours: (diff / 60 / 60) % 24,
Minutes: (diff / 60) % 60,
Seconds: diff % 60
};
var pct = {
Days: time.Days / 365,
Hours: time.Hours / 24,
Minutes: time.Minutes / 60,
Seconds: time.Seconds / 60
};
var old_time = {
Days: (old_diff / 60 / 60 / 24),
Hours: (old_diff / 60 / 60) % 24,
Minutes: (old_diff / 60) % 60,
Seconds: old_diff % 60
};
var i = 0;
var lastKey = null;
for (var key in time) {
// Set the text value
this.data.text_elements[key].text(Math.floor(time[key]));
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;
if(Math.floor(time[key]) !== Math.floor(old_time[key])) {
this.notifyListeners(key, Math.floor(time[key]), Math.floor(diff));
}
// TODO: Check options for fading == true
if (lastKey !== null) {
if (Math.floor(time[lastKey]) > Math.floor(old_time[lastKey])) {
this.radialFade(x, y, color, 1, key);
this.data.state.fading[key] = true;
}
else if (Math.floor(time[lastKey]) < Math.floor(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, pct[key]);
}
lastKey = key;
i++;
}
};
TC_Instance.prototype.drawArc = function(x, y, color, pct) {
var clear_radius = Math.max(this.data.attributes.outer_radius, this.data.attributes.item_size / 2);
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();
}
var startAngle = (-0.5 * Math.PI);
var endAngle = (-0.5 * Math.PI) + (2 * pct * Math.PI);
var counterClockwise = false;
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 rgba = "rgba(" + rgb.r + ", " + rgb.g + ", " + rgb.b + ", " + (Math.round(from * 10) / 10) + ")";
setTimeout(function() {
_this.drawArc(x, y, rgba, 1);
}, 50 * i);
}());
from += step;
}
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() {
// Check if a date was passed in html attribute, if not, fall back to config
var attr_data_date = $(this.element).data('date');
if (typeof attr_data_date === "string") {
if (attr_data_date.match(/^[0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-9]{1,2}:[0-9]{2}:[0-9]{2}$/).length > 0) {
attr_data_date = attr_data_date.replace(' ', 'T');
}
this.data.attributes.ref_date = Date.parse(attr_data_date);
}
else {
var attr_data_timer = $(this.element).attr('data-timer');
if (typeof attr_data_timer === "string") {
this.data.attributes.timer = parseFloat(attr_data_timer);
$(this.element).removeAttr('data-timer');
}
else if (typeof this.config.timer === "string") {
this.data.attributes.timer = parseFloat(this.config.timer);
this.config.timer = null;
}
else if (typeof this.config.timer === "number") {
this.data.attributes.timer = _this.config.timer;
this.config.timer = null;
}
if (typeof this.data.attributes.timer === "number") {
this.data.attributes.ref_date = (new Date()).getTime() + (this.data.attributes.timer * 1000);
}
else {
this.data.attributes.ref_date = this.config.ref_date;
}
}
// Start running
var _this = this;
this.timer = setInterval(function() { _this.updateArc(); }, this.config.refresh_interval * 1000);
};
TC_Instance.prototype.stop = function() {
if (typeof this.data.attributes.timer === "number") {
this.data.attributes.timer = this.timeLeft(this);
}
// Stop running
clearInterval(this.timer);
};
TC_Instance.prototype.destroy = function() {
this.stop();
this.container.remove();
$(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);
};
TC_Instance.prototype.addListener = function(f) {
if(typeof f !== "function") return;
this.listeners.push(f);
};
TC_Instance.prototype.notifyListeners = function(unit, value, total) {
for(var i = 0; i < this.listeners.length; i++) {
this.listeners[i](unit, value, total);
}
}
TC_Instance.prototype.default_options = {
ref_date: new Date(),
start: true,
refresh_interval: 0.1,
count_past_zero: true,
circle_bg_color: "#60686F",
use_background: true,
fg_width: 0.1,
bg_width: 1.2,
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.foreach = function(callback) {
var _this = this;
this.elements.each(function() {
var instance;
var cur_id = $(this).data("tc-id");
if (typeof cur_id === "undefined") {
cur_id = guid();
$(this).data("tc-id", cur_id);
}
if (typeof TC_Instance_List[cur_id] === "undefined") {
var element_options = $(this).data('options');
var options = _this.options;
if(typeof element_options === "object") {
options = $.extend(true, {}, _this.options, element_options);
}
instance = new TC_Instance(this, options);
TC_Instance_List[cur_id] = instance;
}
else {
instance = TC_Instance_List[cur_id];
if (typeof _this.options !== "undefined") {
instance.setOptions(_this.options);
}
}
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.addListener = function(f) {
this.foreach(function(instance) {
instance.addListener(f);
});
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)); |