1
/* Dali Clock - a melting digital clock for Palm WebOS.
2
* Copyright (c) 1991-2009 Jamie Zawinski <jwz@jwz.org>
4
* Permission to use, copy, modify, distribute, and sell this software and its
5
* documentation for any purpose is hereby granted without fee, provided that
6
* the above copyright notice appear in all copies and that both that
7
* copyright notice and this permission notice appear in supporting
8
* documentation. No representations are made about the suitability of this
9
* software for any purpose. It is provided "as is" without express or
14
// setup (canvas_element, initialization
15
// background_element,
17
// show () start animation timers
18
// hide () stop animation timers
19
// destroy () about to exit
20
// changeSettings (settings) change how clock is displayed
22
// The settings object contains:
24
// width size of clock display area
25
// height size of clock display area
26
// orientation 'up' | 'left' | 'right' | 'down'
27
// time_mode 'HHMMSS' | 'HHMM' | 'SS'
28
// date_mode 'MMDDYY' | 'DDMMYY' | 'YYMMDD'
29
// twelve_hour_p boolean, whether to display 12 or 24-hour time
30
// show_date_p boolean, whether to display date instead of time
31
// fps integer (frames per second)
32
// cps integer (color changes per second)
33
// vp_scaling_p whether canvas scaling works for antialiasing
36
function DaliClock() {
39
DaliClock.prototype.setup = function (canvas_element, background_element,
42
this.canvas = canvas_element;
43
this.clockbg = background_element;
47
this.fg_hsv = [200, 0.4, 1.0];
48
this.bg_hsv = [128, 1.0, 0.4];
50
this.fg_hsv[0] += Math.floor(Math.random()*360);
51
this.bg_hsv[0] += Math.floor(Math.random()*360);
53
this.changeSettings(undefined);
57
// Change display settings at next second-tick.
59
DaliClock.prototype.changeSettings = function(settings) {
61
var copy = new Object;
62
for (var e in settings) { copy[e] = settings[e]; }
63
this.new_settings = copy;
65
// We can process these immediately
67
this.clock_freq = settings.fps ? Math.round (1000 / settings.fps) : 0;
68
this.color_freq = settings.cps ? Math.round (1000 / settings.cps) : 0;
71
if (this.clock_freq <= 0) this.clock_freq = 1;
73
// If the clock is hidden, we can process everything immediately.
74
if (!this.shown_p) this.settings_changed();
78
// Called at the start of each sequence if the new_settings object exists.
79
// All settings changes are delayed until the second-tick.
81
DaliClock.prototype.settings_changed = function() {
83
// Changes to some settings require tearing down and rebuilding
84
// the clock. Changes to others can be animated normally.
87
(this.settings == undefined ||
88
this.settings.width != this.new_settings.width ||
89
this.settings.height != this.new_settings.height ||
90
this.settings.time_mode != this.new_settings.time_mode ||
91
this.settings.orientation != this.new_settings.orientation ||
92
this.settings.vp_scaling_p != this.new_settings.vp_scaling_p);
94
this.settings = this.new_settings;
95
this.new_settings = undefined;
97
if (reset_p) this.clock_reset();
101
// For setup tasks that have to happen each time the window becomes visible.
103
DaliClock.prototype.show = function() {
105
if (this.shown_p) return;
108
// Start the color timer.
109
this.color_timer_fn = this.color_timer.bind(this);
110
this.color_timer_fn();
112
// Start the clock timer.
113
this.clock_timer_fn = this.clock_timer.bind(this);
114
this.clock_timer_fn();
120
// Tasks that have to happen each time the window is hidden.
122
DaliClock.prototype.hide = function() {
124
if (!this.shown_p) return;
125
this.shown_p = false;
127
if (this.clock_timer_id) {
128
window.clearTimeout (this.clock_timer_id);
129
this.clock_timer_id = undefined;
131
if (this.color_timer_id) {
132
window.clearTimeout (this.color_timer_id);
133
this.color_timer_id = undefined;
140
DaliClock.prototype.cleanup = function() {
145
// Reset the animation when the settings (number of digits, orientation)
146
// has changed. We have to start over since the resolution is different.
148
DaliClock.prototype.clock_reset = function() {
150
this.pick_font_size();
152
if (! this.font.empty_frame) {
153
this.font.empty_frame = this.make_empty_frame (this.font, false);
154
this.font.empty_colon = this.make_empty_frame (this.font, true);
157
this.orig_frames = new Array(8); // what was there
158
this.current_frames = new Array(8); // current intermediate animation
159
this.target_frames = new Array(8); // where we are going
160
this.target_digits = new Array(8); // where we are going
162
for (var i = 0; i < this.current_frames.length; i++) {
163
var colonic_p = (i == 2 || i == 5);
164
var empty = (colonic_p ? this.font.empty_colon : this.font.empty_frame);
165
this.orig_frames[i] = empty;
166
this.target_frames[i] = empty;
167
this.current_frames[i] = this.copy_frame (empty);
171
// Set the CSS orientation of the canvas based on the current orientation.
172
// Webkit uses "webkitTransform". Firefox 3.5 uses "MozTransform".
173
// Maybe someday it will be just "transform". We set them all...
176
switch (this.settings.orientation) {
177
case 'left': tr = 'rotate(90deg)' ; break;
178
case 'right': tr = 'rotate(-90deg)'; break;
179
case 'down': tr = 'rotate(180deg)'; break;
180
default: tr = ''; break;
182
this.canvas.style.transform = tr;
183
this.canvas.style.webkitTransform = tr;
184
this.canvas.style.MozTransform = tr;
187
// And now set the CSS position and size of the canvas
188
// (not the same thing as size of the canvas's frame buffer).
190
var width = this.canvas.width; // size of the framebuffer
191
var height = this.canvas.height;
194
switch (this.settings.time_mode) {
195
case 'SS': nn = 2; cc = 0; break;
196
case 'HHMM': nn = 4; cc = 1; break;
197
default: nn = 6; cc = 2; break;
200
this.displayed_digits = nn + cc;
202
if (this.settings.vp_scaling_p) { // was doubled, for anti-aliasing
205
var r = height / width;
206
width = Math.floor(this.settings.width);
207
height = Math.floor(width * r);
210
x = (this.settings.width - width) / 2;
211
y = (this.settings.height - height) / 2;
213
this.ctx = this.canvas.getContext("2d");
215
this.canvas.style.left = x + 'px';
216
this.canvas.style.top = y + 'px';
217
this.canvas.style.width = width + 'px';
218
this.canvas.style.height = height + 'px';
223
// Gets the current wall clock and formats the display accordingly.
225
DaliClock.prototype.fill_target_digits = function(date) {
227
var h = date.getHours();
228
var m = date.getMinutes();
229
var s = date.getSeconds();
230
var D = date.getDate();
231
var M = date.getMonth() + 1;
232
var Y = date.getFullYear() % 100;
234
if (this.settings.twelve_hour_p) {
235
if (h > 12) { h -= 12; }
236
else if (h == 0) { h = 12; }
239
for (var i = 0; i < this.target_digits.length; i++) {
240
this.target_digits[i] = undefined;
243
if (this.settings.debug_digit != undefined) {
244
if (this.settings.debug_digit < 0 ||
245
this.settings.debug_digit > 11)
246
this.settings.debug_digit = undefined;
247
this.target_digits[0] = this.target_digits[1] =
248
this.target_digits[3] = this.target_digits[4] =
249
this.target_digits[6] = this.target_digits[7] = this.settings.debug_digit;
250
this.settings.debug_digit = undefined;
252
} else if (!this.settings.show_date_p) {
254
switch (this.settings.time_mode) {
256
this.target_digits[0] = Math.floor(s / 10);
257
this.target_digits[1] = (s % 10);
260
this.target_digits[0] = Math.floor(h / 10);
261
this.target_digits[1] = (h % 10);
262
this.target_digits[2] = 10; // colon
263
this.target_digits[3] = Math.floor(m / 10);
264
this.target_digits[4] = (m % 10);
265
if (this.settings.twelve_hour_p && this.target_digits[0] == 0) {
266
this.target_digits[0] = undefined;
270
this.target_digits[0] = Math.floor(h / 10);
271
this.target_digits[1] = (h % 10);
272
this.target_digits[2] = 10; // colon
273
this.target_digits[3] = Math.floor(m / 10);
274
this.target_digits[4] = (m % 10);
275
this.target_digits[5] = 10; // colon
276
this.target_digits[6] = Math.floor(s / 10);
277
this.target_digits[7] = (s % 10);
278
if (this.settings.twelve_hour_p && this.target_digits[0] == 0) {
279
this.target_digits[0] = undefined;
283
} else { // date mode
285
switch (this.settings.date_mode) {
287
switch (this.settings.time_mode) {
289
this.target_digits[0] = Math.floor(D / 10);
290
this.target_digits[1] = (D % 10);
293
this.target_digits[0] = Math.floor(M / 10);
294
this.target_digits[1] = (M % 10);
295
this.target_digits[2] = 11; // dash
296
this.target_digits[3] = Math.floor(D / 10);
297
this.target_digits[4] = (D % 10);
300
this.target_digits[0] = Math.floor(M / 10);
301
this.target_digits[1] = (M % 10);
302
this.target_digits[2] = 11; // dash
303
this.target_digits[3] = Math.floor(D / 10);
304
this.target_digits[4] = (D % 10);
305
this.target_digits[5] = 11; // dash
306
this.target_digits[6] = Math.floor(Y / 10);
307
this.target_digits[7] = (Y % 10);
312
switch (this.settings.time_mode) {
314
this.target_digits[0] = Math.floor(D / 10);
315
this.target_digits[1] = (D % 10);
318
this.target_digits[0] = Math.floor(D / 10);
319
this.target_digits[1] = (D % 10);
320
this.target_digits[2] = 11; // dash
321
this.target_digits[3] = Math.floor(M / 10);
322
this.target_digits[4] = (M % 10);
325
this.target_digits[0] = Math.floor(D / 10);
326
this.target_digits[1] = (D % 10);
327
this.target_digits[2] = 11; // dash
328
this.target_digits[3] = Math.floor(M / 10);
329
this.target_digits[4] = (M % 10);
330
this.target_digits[5] = 11; // dash
331
this.target_digits[6] = Math.floor(Y / 10);
332
this.target_digits[7] = (Y % 10);
337
switch (this.settings.time_mode) {
339
this.target_digits[0] = Math.floor(D / 10);
340
this.target_digits[1] = (D % 10);
343
this.target_digits[0] = Math.floor(M / 10);
344
this.target_digits[1] = (M % 10);
345
this.target_digits[2] = 11; // dash
346
this.target_digits[3] = Math.floor(D / 10);
347
this.target_digits[4] = (D % 10);
350
this.target_digits[0] = Math.floor(Y / 10);
351
this.target_digits[1] = (Y % 10);
352
this.target_digits[2] = 11; // dash
353
this.target_digits[3] = Math.floor(M / 10);
354
this.target_digits[4] = (M % 10);
355
this.target_digits[5] = 11; // dash
356
this.target_digits[6] = Math.floor(D / 10);
357
this.target_digits[7] = (D % 10);
366
// Find the largest font that fits in the canvas given the current settings
367
// (number of digits and orientation).
369
DaliClock.prototype.pick_font_size = function() {
373
switch (this.settings.time_mode) {
374
case 'SS': nn = 2; cc = 0; break;
375
case 'HHMM': nn = 4; cc = 1; break;
376
default: nn = 6; cc = 2; break;
379
var width = this.settings.width;
380
var height = this.settings.height;
382
if (this.settings.vp_scaling_p) { // double it, for anti-aliasing
387
if (this.settings.orientation == 'left' ||
388
this.settings.orientation == 'right') {
389
var swap = width; width = height; height = swap;
392
for (var i = this.fonts.length-1; i >= 0; i--) {
393
var font = this.fonts[i];
394
var w = (font.char_width * nn) + (font.colon_width * cc);
395
var h = font.char_height;
397
if ((w <= width && h <= height) ||
400
this.canvas.width = w;
401
this.canvas.height = h;
408
DaliClock.prototype.make_empty_frame = function(font, colonic_p) {
409
var cw = (colonic_p ? font.colon_width : font.char_width);
410
var ch = font.char_height;
411
var mid = Math.round(cw / 2);
412
var frame = new Array(ch);
413
for (var y = 0; y < ch; y++) {
415
line = frame[y] = new Array(1);
416
seg = line[0] = new Array(2);
417
seg[0] = seg[1] = mid;
423
DaliClock.prototype.copy_frame = function(oframe) {
425
if (oframe == undefined) { return oframe; }
426
var nframe = oframe.slice(); // copy array of lines
427
var ch = nframe.length;
428
for (var y = 0; y < ch; y++) {
429
if (nframe[y]) { nframe[y] = nframe[y].slice(); } // copy array of segs
430
var segs = nframe[y].length;
431
for (var x = 0; x < segs; x++) {
432
if (nframe[y][x]) { nframe[y][x] = nframe[y][x].slice(); } // copy segs
434
segs = nframe[y].length;
440
DaliClock.prototype.draw_frame = function(frame, x, y) {
442
var ch = this.font.char_height;
443
for (var py = 0; py < ch; py++)
445
var line = frame[py];
446
var nsegs = line.length;
447
for (var px = 0; px < nsegs; px++)
450
this.ctx.fillRect (x + seg[0], y + py,
457
// The second has ticked: we need a new set of digits to march toward.
459
DaliClock.prototype.start_sequence = function(date) {
461
if (this.new_settings)
462
this.settings_changed();
464
// Copy the (old) current_frames into the (new) orig_frames,
465
// since that's what's on the screen now.
467
for (var i = 0; i < this.current_frames.length; i++) {
468
this.orig_frames[i] = this.current_frames[i];
471
// generate new target_digits
472
this.fill_target_digits (date);
474
// Fill the (new) target_frames from the (new) target_digits.
476
for (var i = 0; i < this.target_frames.length; i++) {
477
var colonic_p = (i == 2 || i == 5);
478
var empty = (colonic_p ? this.font.empty_colon : this.font.empty_frame);
479
var frame = (this.target_digits[i] == undefined
481
: this.font.segments[this.target_digits[i]]);
482
this.target_frames[i] = frame;
489
DaliClock.prototype.one_step = function(orig, curr, target, msecs) {
491
var ch = this.font.char_height;
492
var frac = msecs / 1000.0;
494
for (var i = 0; i < ch; i++) {
497
var tline = target[i];
498
var osegs = oline.length;
499
var tsegs = tline.length;
500
var segs = (osegs > tsegs ? osegs : tsegs);
502
// orig and target might have different numbers of segments.
503
// current ends up with the maximal number.
505
for (var j = 0; j < segs; j++) {
506
var oseg = oline[j] || oline[0];
508
var tseg = tline[j] || tline[0];
510
if (! cseg) { cseg = cline[j] = new Array(2); }
512
cseg[0] = oseg[0] + Math.round (frac * (tseg[0] - oseg[0]));
513
cseg[1] = oseg[1] + Math.round (frac * (tseg[1] - oseg[1]));
519
// Compute the current animation state of each digit into target_frames
520
// according to our current position within the current wall-clock second.
522
DaliClock.prototype.tick_sequence = function() {
524
var now = new Date();
525
var ctime = now.getTime();
526
var secs = Math.floor(ctime/1000);
527
var msecs = ctime - (secs*1000); // msec position within this second
529
if (! this.last_secs) {
530
this.last_secs = secs; // fading in!
531
} else if (secs != this.last_secs) {
532
// End of the animation sequence; fill target_frames with the
533
// digits of the current time.
534
this.start_sequence (now);
535
this.last_secs = secs;
538
// Linger for about 1/10th second at the end of each cycle.
540
if (msecs > 1000) msecs = 1000;
542
// Construct current_frames by interpolating between
543
// orig_frames and target_frames.
545
for (var i = 0; i < this.orig_frames.length; i++) {
546
this.one_step (this.orig_frames[i],
547
this.current_frames[i],
548
this.target_frames[i],
554
// Render the current animation state of each digit.
556
DaliClock.prototype.draw_clock = function() {
560
this.ctx.clearRect (0, 0, this.canvas.width, this.canvas.height);
562
for (var i = 0; i < this.displayed_digits; i++) {
563
this.draw_frame (this.current_frames[i], x, y);
564
var colonic_p = (i == 2 || i == 5);
565
x += (colonic_p ? this.font.colon_width : this.font.char_width);
570
DaliClock.prototype.clock_timer = function() {
572
this.tick_sequence ();
575
// Re-trigger our timer.
576
this.clock_timer_id = window.setTimeout (this.clock_timer_fn,
581
DaliClock.prototype.tick_colors = function() {
583
this.ctx.fillStyle = this.hsv_to_rgb (this.fg_hsv[0],
586
this.clockbg.style.backgroundColor = this.hsv_to_rgb (this.bg_hsv[0],
591
if (this.fg_hsv[0] >= 360) { this.fg_hsv[0] -= 360; }
593
this.bg_hsv[0] += 0.91;
594
if (this.bg_hsv[0] >= 360) { this.bg_hsv[0] -= 360; }
598
DaliClock.prototype.color_timer = function() {
600
// cps == 0 means don't cycle colors. but the timer still goes off
601
// at least once a second in case cps has changed.
603
var when = this.color_freq;
609
this.color_timer_id = window.setTimeout(this.color_timer_fn, when);
613
// H is in the range 0 - 360;
614
// S and V are in the range 0.0 - 1.0.
615
// Returns string "rgb(255,255,255)"
617
DaliClock.prototype.hsv_to_rgb = function(h,s,v) {
626
var H = (h % 360) / 60.0;
627
var i = Math.floor(H);
629
var p1 = V * (1 - S);
630
var p2 = V * (1 - (S * f));
631
var p3 = V * (1 - (S * (1 - f)));
632
if (i == 0) { R = V; G = p3; B = p1; }
633
else if (i == 1) { R = p2; G = V; B = p1; }
634
else if (i == 2) { R = p1; G = V; B = p3; }
635
else if (i == 3) { R = p1; G = p2; B = V; }
636
else if (i == 4) { R = p3; G = p1; B = V; }
637
else { R = V; G = p1; B = p2; }
640
Math.floor(R * 255) + ',' +
641
Math.floor(G * 255) + ',' +
642
Math.floor(B * 255) + ')');
646
DaliClock.prototype.LOG = function() {
647
var logger = document.getElementById("log");
649
var args = DaliClock.prototype.LOG.arguments;
650
logger.firstChild.nodeValue = '';
651
for (var i = 0; i < args.length; i++) {
652
logger.firstChild.nodeValue += args[i] + ' ';
658
DaliClock.prototype.LOG2 = function() {
659
var logger = document.getElementById("log");
661
var args = DaliClock.prototype.LOG2.arguments;
662
//logger.firstChild.nodeValue = '';
663
for (var i = 0; i < args.length; i++) {
664
logger.firstChild.nodeValue += args[i] + ' ';