Oh dear, no HTML5 canvas available!

Clock is a possible JS1K entry. This is a competition for JavaScript scripts no larger than one kilobyte. The script for the morphing clock display above is only 1017 bytes. Although it's nowhere nearly as impressive as entries to recent competitions, the process I went through to make it may be instructive.

The original script, heavily hand-optimised was as follows:

// "Clock" by Ian Taylor

// ==ClosureCompiler==
// @compilation_level ADVANCED_OPTIMIZATIONS
// @output_file_name clock_closure.js
// ==/ClosureCompiler==

"use strict";

// Creates a ten element array for digits "zero" to "nine" and "colon"
var points = "akaeeakaqaueukuqqukueuaqak0eegekckakekdkhklkjknkrkqku0aeaaiakamauaueukakatctstut0cecaiakavavikixixukucuaqao0mumsmcmakcckamcmimkmmmsmum0ubsbcbabadagaiyayukucuaqao0sesamaka_aamaoaguguouwawao0abcbibkbmbsbubsdohmjinisiu0kavaviki^i^ukuxuxiki`i`aka0uguoaoaga_u_uguiwukuiucucq0kgko".split(0).map(function (x) {
    // Each array is either 26 control points (x,y) for four Bezier curves or 4 points for the dots in the "colon"
    return [].map.call(x, function (x, i) {
        // Even elements are x-ordinates, odd elements are y-ordinates
        // We must be careful not to allow the x-ordinate to be zero (simplifies later "if" condition)
        return x.charCodeAt(0) * [6, 10][i & 1] - [641, 1200][i & 1];
    });
});
// Called every 100th second at most
setInterval(function () {
    function sinusoid(value) {
        // This function is inlined by the Google Closure compiler
        return Math.sin(now * (value % 1.7)) / 31;
    }
    // Clear our canvas by setting the size
    c.width = 1200;
    c.height = 300;
    a.lineCap = a.lineJoin = "round";
    // Work out the local time in half-second units
    var now = new Date;
    now = now.getTime() / 500 - now.getTimezoneOffset() * 120;
    // Digits lerp in the second half of each second
    var lerp = Math.max(now % 2 - 1, 0);
    // Work out the "?HH:MM:SS" digits arrays for now and in half a second's time
    var xy = [now, now + 1].map(function (x) {
        // The first element will be ignored, but shaves off the date components
        return [172800, 72000, 7200, 0, 1200, 120, 0, 20, 2].map(function (y) {
            return y ? [~~(x / y), x %= y][0] : 10;
        });
    });
    a.translate(1222, 280);
    for (var digit = 9; --digit;) {
        // Work back through the digits
        var xx = xy[0][digit];
        var yy = xy[1][digit];
        a.translate(-140, 0);
        if ((xx < 10) || lerp) {
            // If this is not an invisible colon...
            var transformed = points[xx].map(function (x, i) {
                // Lerp all the control points for the current digit
                return x + (points[yy][i] - x) * lerp;
            });
            // Adjust the canvas transformation to achieve nice wobbliness
            a.save();
            a.scale(sinusoid(digit + 9) + 1, sinusoid(19 - digit) + 1);
            a.rotate(sinusoid(digit));
            // Draw the digits twice: with black outline and coloured insides
            [60, 32].map(function (x) {
                a.lineWidth = x;
                a.beginPath();
                a.moveTo(transformed[0], transformed[1]);
                if (transformed[4]) {
                    // Four joined Bezier curves
                    for (var i = 2; i < 26; i += 6) {
                        a.bezierCurveTo.apply(a, transformed.slice(i, i + 6));
                    }
                } else {
                    // Two dots (actually short lines)
                    a.lineTo(transformed[0], transformed[1] + 1);
                    a.moveTo(transformed[2], transformed[3] + 1);
                    a.lineTo(transformed[2], transformed[3]);
                }
                a.stroke();
                // The inside colour is lerped in HSL space, but handle "23:59:59" to "00:00:00" et al gracefully
                a.strokeStyle = "hsl(" + (((xx + ((yy - xx + 10) % 10) * lerp) * 36) % 360) + ",80%,80%)";
            });
            a.restore();
        }
    }
}, 10);

This was fed into the excellent online Google Closure compiler to produce the following (with whitespace reinserted):

var l="akaeeakaqaueukuqqukueuaqak0eegekckakekdkhklkjknkrkqku0aeaaiakamauaueukakatctstut0cecaiakavavikixixukucuaqao0mumsmcmakcckamcmimkmmmsmum0ubsbcbabadagaiyayukucuaqao0sesamaka_aamaoaguguouwawao0abcbibkbmbsbubsdohmjinisiu0kavaviki^i^ukuxuxiki`i`aka0uguoaoaga_u_uguiwukuiucucq0kgko".split(0).map(function(b){return[].map.call(b,function(b,f){return b.charCodeAt(0)*[6,10][f&1]-[641,1200][f&1]})});
setInterval(function(){
    c.width=1200;
    c.height=300;
    a.lineCap=a.lineJoin="round";
    var b=new Date,b=b.getTime()/500-120*b.getTimezoneOffset(),h=Math.max(b%2-1,0),f=[b,b+1].map(function(k){
        return[172800,72E3,7200,0,1200,120,0,20,2].map(function(b){
            return b?[~~(k/b),k%=b][0]:10
        })
    });
    a.translate(1222,280);
    for(var e=9;--e;){
        var g=f[0][e],m=f[1][e];
        a.translate(-140,0);
        if(10>g||h){
            var d=l[g].map(function(b,d){
                return b+(l[m][d]-b)*h
            });
            a.save();
            a.scale(Math.sin(b*((e+9)%1.7))/31+1,Math.sin(b*((19-e)%1.7))/31+1);
            a.rotate(Math.sin(b*(e%1.7))/31);
            [60,32].map(function(b){
                a.lineWidth=b;
                a.beginPath();
                a.moveTo(d[0],d[1]);
                if(d[4])
                    for(b=2;26>b;b+=6)
                        a.bezierCurveTo.apply(a,d.slice(b,b+6));
                else
                    a.lineTo(d[0],d[1]+1),a.moveTo(d[2],d[3]+1),a.lineTo(d[2],d[3]);
                a.stroke();
                a.strokeStyle="hsl("+36*(g+(m-g+10)%10*h)%360+",80%,80%)"
            });
            a.restore()
        }
    }
},10);

This in turn was fed into Siorki's JavaScript Packer to produce the final script. The actual script contains valid control codes, but the gist is clever string processing to take advantage of frequently occurring sequences:

for(_=');Z][Y){XinHatGmaFfunction(E.Fp(E]bXreturn bugu+1e(a.To(d[MGh.idth=0)vaviki0,Zstroke0Yf\&1]uku120\fb.getTimeaka\ttranslG\bcuaqao0var 2],d[3]0],d[1]%1.7))\x2F31lHesH(b*(l=\"\tee\tqaueqqeuaqak0eegekckakekdkhklkjknkrkqku0aeaai\tFuaueuk\ttctstut0cecai\txixmumsmcFkcckamcmimkmmmsmum0ubsbcbabadagaiyaysesam\t_aaFoagouwawao0abcbibkbmbsbubsdohmjHisiu0ka^i^xuxiki`i`\t0oaoaga_u_iwiucucq0kgko\".split(b[].Fp.call(b,Eb,f.charCodeAt(*[6,1-[641,\f})}ZsetInterval(EXc.w\f0;c.height=300;Cap=JoH=\"round\";b=new DGe,b=()\x2F500-\f*zoneOffset(),h=Fx(b%2-1,,f=[b,b]k[1728072e3,720\f\f,22?[~~(k\x2Fb),k%=bY0]:10})}\b1222,28;for(e=9;--e;Xg=f[0Ye],m=f[1Ye];\b-14;if(10\x3Eg||hXd=l[g,d+(l[mYd]-b)*h}savscal(e+9),(19-e)rotGeZ[632XWb;begHPGh(moveZif(d[4])for(b=2;26\x3Eb;b+=6)bezierCurveTo.apply(a,d.slicb,b+6)Zelse ),move),(Style=\"hsl(\"+36*(g+(m-g%10*h)%360+\",80%,80%)\"}restor)}}},1';
        g=/[-E-HX-Z]/.exec(_);)
    with(_.split(g))
        _=join(shift());
eval(_)

It does, of course, require the evil eval() function. But it's only a demo!

More clock demos.