slides/asset/common/tock.js (267 lines of code) (raw):
/**
* Tock by Mr Chimp - github.com/mrchimp/tock
* Based on code by James Edwards:
* sitepoint.com/creating-accurate-timers-in-javascript/
*/
// Implements Date.now() for ie lt 9
Date.now = Date.now || function() { return +new Date(); };
// Polyfills Function.prototype.bind for IE lt 9 and Safari lt 5.1
if ( typeof Function.prototype.bind != 'function' ) {
Function.prototype.bind = function (ctx) {
var args = Array.prototype.slice.call(arguments, 1),
fn = this;
return function () {
args.push.apply(args, arguments);
return fn.apply(ctx, args);
};
};
}
(function (root, factory) {
if ( typeof define === 'function' && define.amd ) {
define(factory);
} else if ( typeof exports === 'object' ) {
module.exports = factory();
} else {
root.Tock = factory();
}
}(this, function () {
/**
* Called every tick for countdown clocks.
* i.e. once every this.interval ms
*/
function _tick () {
this.time += this.interval;
if ( this.countdown && (this.duration_ms - this.time < 0) ) {
this.final_time = 0;
this.go = false;
this.callback(this);
window.clearTimeout(this.timeout);
this.complete(this);
return;
}
else {
this.callback(this);
}
var diff = (Date.now() - this.start_time) - this.time,
next_interval_in = diff > 0 ? this.interval - diff : this.interval;
if ( next_interval_in <= 0 ) {
this.missed_ticks = Math.floor(Math.abs(next_interval_in) / this.interval);
this.time += this.missed_ticks * this.interval;
if ( this.go ) {
_tick.call(this);
}
}
else if ( this.go ) {
this.timeout = window.setTimeout(_tick.bind(this), next_interval_in);
}
}
/**
* Called by Tock internally - use start() instead
*/
function _startCountdown (duration) {
this.duration_ms = duration;
this.start_time = Date.now();
this.time = 0;
this.go = true;
_tick.call(this);
}
/**
* Called by Tock internally - use start() instead
*/
function _startTimer (start_offset) {
this.start_time = start_offset || Date.now();
this.time = 0;
this.go = true;
_tick.call(this);
}
var MILLISECONDS_RE = /^\s*(\+|-)?\d+\s*$/,
MM_SS_RE = /^(\d{1,2}):(\d{2})$/,
MM_SS_ms_OR_HH_MM_SS_RE = /^(\d{1,2}):(\d{2})(?::|\.)(\d{2,3})$/,
MS_PER_HOUR = 3600000,
MS_PER_MIN = 60000,
MS_PER_SEC = 1000,
/* The RegExp below will match a date in format `yyyy-mm-dd HH:MM:SS` and optionally with `.ms` at the end.
* It will also match ISO date string, i.e. if the whitespace separator in the middle is replaced with a `T`
* and the date string is also suffixed with a `Z` denoting UTC timezone.
*/
yyyy_mm_dd_HH_MM_SS_ms_RE = /^(\d{4})-([0-1]\d)-([0-3]\d)(?:\s|T)(\d{2}):(\d{2}):(\d{2})(?:\.(\d{3})Z?)?$/;
var Tock = function (options) {
options = options || {};
if ( ! (this instanceof Tock) ) return new Tock(options);
Tock.instances = (Tock.instances || 0) + 1;
this.go = false;
this.timeout = null;
this.missed_ticks = null;
this.interval = options.interval || 10;
this.countdown = options.countdown || false;
this.start_time = 0;
this.pause_time = 0;
this.final_time = 0;
this.duration_ms = 0;
this.time = 0;
this.callback = options.callback || function () {};
this.complete = options.complete || function () {};
};
/**
* Reset the clock
*/
Tock.prototype.reset = function () {
if ( this.countdown ) {
return false;
}
this.stop();
this.start_time = 0;
this.time = 0;
};
/**
* Start the clock.
* accepts a single "time" argument which can be in various forms:
** MM:SS
** MM:SS:ms or MM:SS.ms
** HH:MM:SS
** yyyy-mm-dd HH:MM:SS.ms
** milliseconds
*/
Tock.prototype.start = function (time) {
if (this.go) return false;
time = time ? this.timeToMS(time) : 0;
this.start_time = time;
this.pause_time = 0;
if ( this.countdown ) {
_startCountdown.call(this, time);
} else {
_startTimer.call(this, Date.now() - time);
}
};
/**
* Stop the clock.
*/
Tock.prototype.stop = function () {
this.pause_time = this.lap();
this.go = false;
window.clearTimeout(this.timeout);
if ( this.countdown ) {
this.final_time = this.duration_ms - this.time;
} else {
this.final_time = (Date.now() - this.start_time);
}
};
/**
* Stop/start the clock.
*/
Tock.prototype.pause = function () {
if ( this.go ) {
this.pause_time = this.lap();
this.stop();
}
else {
if ( this.pause_time ) {
if ( this.countdown ) {
_startCountdown.call(this, this.pause_time);
} else {
_startTimer.call(this, Date.now() - this.pause_time);
}
this.pause_time = 0;
}
}
};
/**
* Get the current clock time in ms.
* Use with Tock.msToTime() to make it look nice.
*/
Tock.prototype.lap = function () {
if ( this.go ) {
var now;
if ( this.countdown ) {
now = this.duration_ms - (Date.now() - this.start_time);
} else {
now = (Date.now() - this.start_time);
}
return now;
}
return this.pause_time || this.final_time;
};
/**
* Format milliseconds as a MM:SS.ms string.
*/
Tock.prototype.msToTime = function (ms) {
if ( ms <= 0 ) {
return '00:00.000';
}
var milliseconds = (ms % MS_PER_SEC).toString(),
seconds = Math.floor((ms / MS_PER_SEC) % 60).toString(),
minutes = Math.floor((ms / (MS_PER_MIN)) % 60).toString();
if ( milliseconds.length === 1 ) {
milliseconds = '00' + milliseconds;
}
else if ( milliseconds.length === 2 ) {
milliseconds = '0' + milliseconds;
}
if ( seconds.length === 1 ) {
seconds = '0' + seconds;
}
if ( minutes.length === 1 ) {
minutes = '0' + minutes;
}
return minutes + ':' + seconds + '.' + milliseconds;
};
/**
* Format milliseconds as HH:MM:SS
*/
Tock.prototype.msToTimecode = function (ms) {
if (ms <= 0) {
return '00:00:00';
}
var seconds = Math.floor((ms / MS_PER_SEC) % 60).toString(),
minutes = Math.floor((ms / (MS_PER_MIN)) % 60).toString(),
MS_PER_HOURs = Math.floor((ms / (MS_PER_HOUR)) % 60).toString();
if ( seconds.length === 1 ) {
seconds = '0' + seconds;
}
if ( minutes.length === 1 ) {
minutes = '0' + minutes;
}
if ( MS_PER_HOURs.length === 1 ) {
MS_PER_HOURs = '0' + MS_PER_HOURs;
}
return MS_PER_HOURs + ':' + minutes + ':' + seconds;
};
/**
* Convert a time string to milliseconds
*
* Possible inputs:
* MM:SS
* MM:SS:ms or MM:SS.ms
* HH:MM:SS
* yyyy-mm-dd HH:MM:SS.ms
*
* A milliseconds input will return it back for safety
* If the input cannot be recognized then 0 is returned
*
*/
Tock.prototype.timeToMS = function (time) {
//if milliseconds integer is input then return it back
if ( MILLISECONDS_RE.test(String(time)) ) {
return time;
}
var ms,
time_split,
match,
date,
now = new Date();
if ( MM_SS_RE.test(time) ) { // If MM:SS
time_split = time.split(':');
ms = parseInt(time_split[0], 10) * MS_PER_MIN;
ms += parseInt(time_split[1], 10) * MS_PER_SEC;
} else {
match = time.match(MM_SS_ms_OR_HH_MM_SS_RE);
if ( match ) {
if ( match[3].length == 3 || parseInt(match[3], 10) > 59 ) { // If MM:SS:ms or MM:SS.ms (e.g. 10:10:458 or 10:10.458)
ms = parseInt(match[1], 10) * MS_PER_MIN;
ms += parseInt(match[2], 10) * MS_PER_SEC;
ms += parseInt(match[3], 10);
} else { // Then it's HH:MM:SS
ms = parseInt(match[1], 10) * MS_PER_HOUR;
ms += parseInt(match[2], 10) * MS_PER_MIN;
ms += parseInt(match[3], 10) * MS_PER_SEC;
}
} else if ( yyyy_mm_dd_HH_MM_SS_ms_RE.test(time) ) { // If yyyy-mm-dd HH:MM:SS or yyyy-mm-dd HH:MM:SS.ms or yyyy-mm-ddTHH:MM:SS.msZ
date = new Date();
now = new Date();
match = time.match(yyyy_mm_dd_HH_MM_SS_ms_RE);
date.setYear(match[1]);
date.setMonth(match[2]);
date.setDate(match[3]);
date.setHours(match[4]);
date.setMinutes(match[5]);
date.setSeconds(match[6]);
if (typeof match[7] !== 'undefined') {
date.setMilliseconds(match[7]);
}
ms = Math.max(0, date.getTime() - now.getTime());
} else {
// Let's try it as a date string
now = new Date();
ms = Date.parse(time);
if (!isNaN(ms)) { // Looks ok
ms = Math.max(0, ms - now.getTime());
} else { // Could not recognize input, so start from 0
ms = 0;
}
}
}
return ms;
};
return Tock;
}));