﻿/**
* CoolClock 2.1.4
* Copyright 2010, Simon Baird
* Released under the BSD License.
*
* Display an analog clock using canvas.
* http://randomibis.com/coolclock/
*
*/

// Constructor for CoolClock objects
window.CoolClock = function(options) {
    return this.init(options);
}

// Config contains some defaults, and clock skins
CoolClock.config = {
    tickDelay: 1000,
    longTickDelay: 15000,
    defaultRadius: 85,
    renderRadius: 100,
    defaultSkin: "chunkySwiss",
    // Should be in skin probably...
    // (TODO: allow skinning of digital display)
    showSecs: true,
    showAmPm: true,

    skins: {
        // There are more skins in moreskins.js
        // Try making your own skin by copy/pasting one of these and tweaking it
        swissRail: {
            outerBorder: { lineWidth: 2, radius: 95, color: "black", alpha: 1 },
            smallIndicator: { lineWidth: 2, startAt: 88, endAt: 92, color: "black", alpha: 1 },
            largeIndicator: { lineWidth: 4, startAt: 79, endAt: 92, color: "black", alpha: 1 },
            hourHand: { lineWidth: 8, startAt: -15, endAt: 50, color: "black", alpha: 1 },
            minuteHand: { lineWidth: 7, startAt: -15, endAt: 75, color: "black", alpha: 1 },
            secondHand: { lineWidth: 1, startAt: -20, endAt: 85, color: "red", alpha: 1 },
            secondDecoration: { lineWidth: 1, startAt: 70, radius: 4, fillColor: "red", color: "red", alpha: 1 }
        },
        chunkySwiss: {
            outerBorder: { lineWidth: 4, radius: 97, color: "black", alpha: 1 },
            smallIndicator: { lineWidth: 4, startAt: 89, endAt: 93, color: "black", alpha: 1 },
            largeIndicator: { lineWidth: 8, startAt: 80, endAt: 93, color: "black", alpha: 1 },
            hourHand: { lineWidth: 12, startAt: -15, endAt: 60, color: "black", alpha: 1 },
            minuteHand: { lineWidth: 10, startAt: -15, endAt: 85, color: "black", alpha: 1 },
            secondHand: { lineWidth: 4, startAt: -20, endAt: 85, color: "red", alpha: 1 },
            secondDecoration: { lineWidth: 2, startAt: 70, radius: 8, fillColor: "red", color: "red", alpha: 1 }
        },
        chunkySwissOnBlack: {
            outerBorder: { lineWidth: 4, radius: 97, color: "white", alpha: 1 },
            smallIndicator: { lineWidth: 4, startAt: 89, endAt: 93, color: "white", alpha: 1 },
            largeIndicator: { lineWidth: 8, startAt: 80, endAt: 93, color: "white", alpha: 1 },
            hourHand: { lineWidth: 12, startAt: -15, endAt: 60, color: "white", alpha: 1 },
            minuteHand: { lineWidth: 10, startAt: -15, endAt: 85, color: "white", alpha: 1 },
            secondHand: { lineWidth: 4, startAt: -20, endAt: 85, color: "red", alpha: 1 },
            secondDecoration: { lineWidth: 2, startAt: 70, radius: 8, fillColor: "red", color: "red", alpha: 1 }
        }

    },

    // Test for IE so we can nurse excanvas in a couple of places
    isIE: !!document.all,

    // Will store (a reference to) each clock here, indexed by the id of the canvas element
    clockTracker: {},

    // For giving a unique id to coolclock canvases with no id
    noIdCount: 0
};

// Define the CoolClock object's methods
CoolClock.prototype = {

    // Initialise using the parameters parsed from the colon delimited class
    init: function(options) {
        // Parse and store the options
        this.canvasId = options.canvasId;
        this.skinId = options.skinId || CoolClock.config.defaultSkin;
        this.displayRadius = options.displayRadius || CoolClock.config.defaultRadius;
        this.showSecondHand = typeof options.showSecondHand == "boolean" ? options.showSecondHand : true;
        this.gmtOffset = (options.gmtOffset != null && options.gmtOffset != '') ? parseFloat(options.gmtOffset) : null;
        this.showDigital = typeof options.showDigital == "boolean" ? options.showDigital : false;
        this.logClock = typeof options.logClock == "boolean" ? options.logClock : false;
        this.logClockRev = typeof options.logClock == "boolean" ? options.logClockRev : false;

        this.tickDelay = CoolClock.config[this.showSecondHand ? "tickDelay" : "longTickDelay"];

        // Get the canvas element
        this.canvas = document.getElementById(this.canvasId);

        // Make the canvas the requested size. It's always square.
        this.canvas.setAttribute("width", this.displayRadius * 2);
        this.canvas.setAttribute("height", this.displayRadius * 2);
        this.canvas.style.width = this.displayRadius * 2 + "px";
        this.canvas.style.height = this.displayRadius * 2 + "px";

        // Explain me please...?
        this.renderRadius = CoolClock.config.renderRadius;
        this.scale = this.displayRadius / this.renderRadius;

        // Initialise canvas context
        this.ctx = this.canvas.getContext("2d");
        this.ctx.scale(this.scale, this.scale);

        // Keep track of this object
        CoolClock.config.clockTracker[this.canvasId] = this;

        // Start the clock going
        this.tick();

        return this;
    },

    // Draw a circle at point x,y with params as defined in skin
    fullCircleAt: function(x, y, skin) {
        this.ctx.save();
        this.ctx.globalAlpha = skin.alpha;
        this.ctx.lineWidth = skin.lineWidth;

        if (!CoolClock.config.isIE) {
            this.ctx.beginPath();
        }

        if (CoolClock.config.isIE) {
            // excanvas doesn't scale line width so we will do it here
            this.ctx.lineWidth = this.ctx.lineWidth * this.scale;
        }

        this.ctx.arc(x, y, skin.radius, 0, 2 * Math.PI, false);

        if (CoolClock.config.isIE) {
            // excanvas doesn't close the circle so let's fill in the tiny gap
            this.ctx.arc(x, y, skin.radius, -0.1, 0.1, false);
        }

        if (skin.fillColor) {
            this.ctx.fillStyle = skin.fillColor
            this.ctx.fill();
        }
        else {
            // XXX why not stroke and fill
            this.ctx.strokeStyle = skin.color;
            this.ctx.stroke();
        }
        this.ctx.restore();
    },

    // Draw some text centered vertically and horizontally
    drawTextAt: function(theText, x, y) {
        this.ctx.save();
        this.ctx.font = '15px sans-serif';
        var tSize = this.ctx.measureText(theText);
        if (!tSize.height) tSize.height = 15; // no height in firefox.. :(
        this.ctx.fillText(theText, x - tSize.width / 2, y - tSize.height / 2);
        this.ctx.restore();
    },

    lpad2: function(num) {
        return (num < 10 ? '0' : '') + num;
    },

    tickAngle: function(second) {
        // Log algorithm by David Bradshaw
        var tweak = 3; // If it's lower the one second mark looks wrong (?)
        if (this.logClock) {
            return second == 0 ? 0 : (Math.log(second * tweak) / Math.log(60 * tweak));
        }
        else if (this.logClockRev) {
            // Flip the seconds then flip the angle (trickiness)
            second = (60 - second) % 60;
            return 1.0 - (second == 0 ? 0 : (Math.log(second * tweak) / Math.log(60 * tweak)));
        }
        else {
            return second / 60.0;
        }
    },

    timeText: function(hour, min, sec) {
        var c = CoolClock.config;
        return '' +
			(c.showAmPm ? ((hour % 12) == 0 ? 12 : (hour % 12)) : hour) + ':' +
			this.lpad2(min) +
			(c.showSecs ? ':' + this.lpad2(sec) : '') +
			(c.showAmPm ? (hour < 12 ? ' am' : ' pm') : '')
		;
    },

    // Draw a radial line by rotating then drawing a straight line
    // Ha ha, I think I've accidentally used Taus, (see http://tauday.com/)
    radialLineAtAngle: function(angleFraction, skin) {
        this.ctx.save();
        this.ctx.translate(this.renderRadius, this.renderRadius);
        this.ctx.rotate(Math.PI * (2.0 * angleFraction - 0.5));
        this.ctx.globalAlpha = skin.alpha;
        this.ctx.strokeStyle = skin.color;
        this.ctx.lineWidth = skin.lineWidth;

        if (CoolClock.config.isIE)
        // excanvas doesn't scale line width so we will do it here
            this.ctx.lineWidth = this.ctx.lineWidth * this.scale;

        if (skin.radius) {
            this.fullCircleAt(skin.startAt, 0, skin)
        }
        else {
            this.ctx.beginPath();
            this.ctx.moveTo(skin.startAt, 0)
            this.ctx.lineTo(skin.endAt, 0);
            this.ctx.stroke();
        }
        this.ctx.restore();
    },

    render: function(hour, min, sec) {
        // Get the skin
        var skin = CoolClock.config.skins[this.skinId];
        if (!skin) skin = CoolClock.config.skins[CoolClock.config.defaultSkin];

        // Clear
        this.ctx.clearRect(0, 0, this.renderRadius * 2, this.renderRadius * 2);

        // Draw the outer edge of the clock
        if (skin.outerBorder)
            this.fullCircleAt(this.renderRadius, this.renderRadius, skin.outerBorder);

        // Draw the tick marks. Every 5th one is a big one
        for (var i = 0; i < 60; i++) {
            (i % 5) && skin.smallIndicator && this.radialLineAtAngle(this.tickAngle(i), skin.smallIndicator);
            !(i % 5) && skin.largeIndicator && this.radialLineAtAngle(this.tickAngle(i), skin.largeIndicator);
        }

        // Write the time
        if (this.showDigital) {
            this.drawTextAt(
				this.timeText(hour, min, sec),
				this.renderRadius,
				this.renderRadius + this.renderRadius / 2
			);
        }

        // Draw the hands
        if (skin.hourHand)
            this.radialLineAtAngle(this.tickAngle(((hour % 12) * 5 + min / 12.0)), skin.hourHand);

        if (skin.minuteHand)
            this.radialLineAtAngle(this.tickAngle((min + sec / 60.0)), skin.minuteHand);

        if (this.showSecondHand && skin.secondHand)
            this.radialLineAtAngle(this.tickAngle(sec), skin.secondHand);

        // Second hand decoration doesn't render right in IE so lets turn it off
        if (!CoolClock.config.isIE && this.showSecondHand && skin.secondDecoration)
            this.radialLineAtAngle(this.tickAngle(sec), skin.secondDecoration);
    },

    // Check the time and display the clock
    refreshDisplay: function() {
        var now = new Date();
        if (this.gmtOffset != null) {
            // Use GMT + gmtOffset
            var offsetNow = new Date(now.valueOf() + (this.gmtOffset * 1000 * 60 * 60));
            this.render(offsetNow.getUTCHours(), offsetNow.getUTCMinutes(), offsetNow.getUTCSeconds());
        }
        else {
            // Use local time
            this.render(now.getHours(), now.getMinutes(), now.getSeconds());
        }
    },

    // Set timeout to trigger a tick in the future
    nextTick: function() {
        setTimeout("CoolClock.config.clockTracker['" + this.canvasId + "'].tick()", this.tickDelay);
    },

    // Check the canvas element hasn't been removed
    stillHere: function() {
        return document.getElementById(this.canvasId) != null;
    },

    // Main tick handler. Refresh the clock then setup the next tick
    tick: function() {
        if (this.stillHere()) {
            this.refreshDisplay()
            this.nextTick();
        }
    }
};

// Find all canvas elements that have the CoolClock class and turns them into clocks
CoolClock.findAndCreateClocks = function() {
    // (Let's not use a jQuery selector here so it's easier to use frameworks other than jQuery)
    var canvases = document.getElementsByTagName("canvas");
    for (var i = 0; i < canvases.length; i++) {
        // Pull out the fields from the class. Example "CoolClock:chunkySwissOnBlack:1000"
        var fields = canvases[i].className.split(" ")[0].split(":");
        if (fields[0] == "CoolClock") {
            if (!canvases[i].id) {
                // If there's no id on this canvas element then give it one
                canvases[i].id = '_coolclock_auto_id_' + CoolClock.config.noIdCount++;
            }
            // Create a clock object for this element
            new CoolClock({
                canvasId: canvases[i].id,
                skinId: fields[1],
                displayRadius: fields[2],
                showSecondHand: fields[3] != 'noSeconds',
                gmtOffset: fields[4],
                showDigital: fields[5] == 'showDigital',
                logClock: fields[6] == 'logClock',
                logClockRev: fields[6] == 'logClockRev'
            });
        }
    }
};

// If you don't have jQuery then you need a body onload like this: <body onload="CoolClock.findAndCreateClocks()">
// If you do have jQuery and it's loaded already then we can do it right now
if (window.jQuery) jQuery(document).ready(CoolClock.findAndCreateClocks);

