Wednesday, August 1, 2012

Adventures in Windows 8: Timer Metro App

As part of learning Windows 8 I am create Metro apps large and small almost daily, and I'll be sharing some of those here on this blog. Today I'll share Timer, a simple countdown-timer app you might use if you were giving a classroom presentation that had a hands-on exercise or a test where you need a "time remaining" clock.
Like many of the Metro apps I create, this one is in HTML5. The Windows runtime (WinRT) allows development in C++, .NET, or HTML5/JavaScript with equal treatment for each development path. The runtime API for JavaScript is called WinJS.

Tour of the App
Here's what Timer looks like when you run it. You enter the number of minutes and click the Start button.


Once you click Start, you get a display that shows minutes remaining and counts down to 0.


Graphically, seconds remaining is shown by an inner circle and minutes remaining by an outer circle.


Once the timer reaches zero, the rings turn red and audible alarm plays - time's up!


That's about all there is to the app. You can also stop the timer early on with the Stop button and restart it.

The HTML
Here's the HTML markup for the app, which you can see is very short. The primary elements are start and stop buttons that reference JavaScript start() and stop() functions, and an HTML5 canvas where the graphical timer will be rendered. There's also an <audio> tag for the alarm sound.
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Timer</title>

    <!-- WinJS references -->
    <link href="//Microsoft.WinJS.1.0.RC/css/ui-dark.css" rel="stylesheet" />
    <script src="//Microsoft.WinJS.1.0.RC/js/base.js"></script>
    <script src="//Microsoft.WinJS.1.0.RC/js/ui.js"></script>

    <!-- Timer references -->
    <link href="/css/default.css" rel="stylesheet" />
    <script src="/js/default.js"></script>
</head>
<body>
    <div data-win-control="WinJS.UI.ViewBox">
        <div class="fixedlayout">
            <div class="center">
                <div class="center">
                    <p>Minutes:&nbsp;<input type="text" id="minutes" size="4" value="60" />&nbsp;
                        <button id="startButton" onclick="start()">Start</button>
                        <button id="stopButton" onclick="stop()" style="display:none">Stop</button></p>
                </div>
            <canvas id="canvas" class="center" height="600" width="600"></canvas>
            </div>
        </div>
    </div>

<audio id="alarm">
    <source src="audio/timer.mp3" />
</audio>

</body>
</html>


The JavaScript
And here's the JavaScript, also fairly small. The largest function is showTime(), which displays the timer on the canvas.

var minutes = 60;
var seconds = 60;
var running = false;

function start() {
    minutes = parseInt(document.getElementById("minutes").value, 10);
    if (minutes > 0) {
        seconds = 60;
        showTime();
        running = true;
        document.getElementById("startButton").style.display = "none";
        document.getElementById("stopButton").style.display = "inline";
        setTimeout(update, 1000);
    }
}

function stop() {
    clearTimeout(update);
    running = false;
    document.getElementById("startButton").style.display = "inline";
    document.getElementById("stopButton").style.display = "none";
}

function update()
{
    if (!running) return;

    seconds--;
    if (seconds === 0) {
        if (minutes > 0) {
            minutes--;
            if (minutes > 0) {
                seconds = 60;
            }
        }
     }
    showTime(minutes);

    if (running) {

        if (minutes > 0 || seconds > 0) {
            setTimeout(update, 1000);
        }
        else {
            alarm = document.getElementById('alarm');
            alarm.play();
            running = false;
            document.getElementById("startButton").style.display = "inline";
            document.getElementById("stopButton").style.display = "none";
        }
    }
}

function showTime() {
    var canvas = document.getElementById("canvas");
    var context = canvas.getContext("2d");
    var x = canvas.width / 2;
    var y = canvas.height / 2;
    var radius = 200;

    var startAngle = 0 * Math.PI;
    var endAngle = ((minutes) / 30) * Math.PI;
    var counterClockwise = false;

    context.save();
    context.clearRect(0, 0, 600, 600);

    context.fillStyle = "#ffffff";
    context.font = "200px Segoe UI";
    context.textAlign = "center";
    context.textBaseline = "middle";
    context.fillText(minutes.toString(), x, y - 15);

    context.translate(x, y);
    context.rotate(-Math.PI / 2);
    context.translate(-x, -y);

    if (minutes > 0 || seconds > 0) {
        context.beginPath();
        context.arc(x, y, radius, startAngle, endAngle, counterClockwise);
        context.lineWidth = 50;
        context.strokeStyle = "cyan";
        context.stroke();
    }
    else {
        context.beginPath();
        context.arc(x, y, radius, 0 * Math.PI, 2 * Math.PI, counterClockwise);
        context.lineWidth = 50;
        context.strokeStyle = "red";
        context.stroke();
    }

    radius = 150;

    startAngle = 0 * Math.PI;
    endAngle = ((seconds) / 30) * Math.PI;

    if (minutes === 0 && seconds === 0) {
        context.beginPath();
        context.arc(x, y, radius, 0 * Math.PI, 2 * Math.PI, counterClockwise);
        context.lineWidth = 25;
        context.strokeStyle = "red";
        context.stroke();
    }
    else {
        context.beginPath();
        context.arc(x, y, radius, startAngle, endAngle, counterClockwise);
        context.lineWidth = 25;
        context.strokeStyle = "plum";
        context.stroke();
    }

    context.restore();
}



// For an introduction to the Fixed Layout template, see the following documentation:
// http://go.microsoft.com/fwlink/?LinkId=232508
(function () {
    "use strict";
    
    var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;
    WinJS.strictProcessing();

    app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            if (args.detail.previousExecutionState !== activation.ApplicationExecutionState.terminated) {
                // TODO: This application has been newly launched. Initialize
                // your application here.
            } else {
                // TODO: This application has been reactivated from suspension.
                // Restore application state here.
            }
            args.setPromise(WinJS.UI.processAll());
        }
    };

    app.oncheckpoint = function (args) {
        // TODO: This application is about to be suspended. Save any state
        // that needs to persist across suspensions here. You might use the
        // WinJS.Application.sessionState object, which is automatically
        // saved and restored across suspension. If you need to complete an
        // asynchronous operation before your application is suspended, call
        // args.setPromise().
    };

    app.start();
})();

Let's walk through how the showTime() function displays the timer. We get the canvas element by id and then get a 2D drawing context from that.

    var canvas = document.getElementById("canvas");
    var context = canvas.getContext("2d");


We use the arc function to draw the circles  or partial circles for minutes and seconds, but we have to convert to the way the arc function thinks, shown below.


In JavaScript we can multiply the starting and ending minute points around the clock (0-59) times Math.PI to get the angles we need.

    var x = canvas.width / 2;
    var y = canvas.height / 2;
    var radius = 200;

    var startAngle = 0 * Math.PI;
    var endAngle = ((minutes) / 30) * Math.PI;
    var counterClockwise = false;

    context.beginPath();
    context.arc(x, y, radius, startAngle, endAngle, counterClockwise);
    context.lineWidth = 50;
    context.strokeStyle = "cyan";
    context.stroke();


It was a little tricky thinking through the angle orientation, since a clock starts at the top of the circle but the arc function's orientation is 90 degrees to the right. This is solved with translation and rotation.

    context.translate(x, y);
    context.rotate(-Math.PI / 2);
    context.translate(-x, -y);


There you have it - a simple, but functional Metro app that may come in handy the next time you're teaching a class.

2 comments:

Shashank Banerjea said...

Really neat. Love to use it when I try to get my kids to do their Homework. I could spice it in with more dramatic audio... :)

Ralph Kiernan said...

You can also try a timer app for their homework. I usually use Clear Timer App when helping my children in their homework. awesome feature and my kids liked it. It is easily downloadable in Itunes Page. Great!