Skip to content

Commit

Permalink
Merge pull request espruino#1821 from rigrig/clock-module
Browse files Browse the repository at this point in the history
ClockFace module
  • Loading branch information
gfwilliams committed May 16, 2022
2 parents 22a7de9 + 8f342e2 commit c59eea3
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 60 deletions.
1 change: 1 addition & 0 deletions apps/barclock/ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
0.07: Update to use Bangle.setUI instead of setWatch
0.08: Use theme colors, Layout library
0.09: Fix time/date disappearing after fullscreen notification
0.10: Use ClockFace library
105 changes: 46 additions & 59 deletions apps/barclock/clock-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,9 @@ let locale = require("locale");
date.setMonth(1, 3); // februari: months are zero-indexed
const localized = locale.date(date, true);
locale.dayFirst = /3.*2/.test(localized);

locale.hasMeridian = false;
if (typeof locale.meridian==="function") { // function does not exist if languages app is not installed
locale.hasMeridian = (locale.meridian(date)!=="");
}
locale.hasMeridian = (locale.meridian(date)!=="");
}
Bangle.loadWidgets();

function renderBar(l) {
if (!this.fraction) {
// zero-size fillRect stills draws one line of pixels, we don't want that
Expand All @@ -27,32 +23,6 @@ function renderBar(l) {
g.fillRect(l.x, l.y, l.x+width-1, l.y+l.height-1);
}

const Layout = require("Layout");
const layout = new Layout({
type: "v", c: [
{
type: "h", c: [
{id: "time", label: "88:88", type: "txt", font: "6x8:5", bgCol: g.theme.bg}, // size updated below
{id: "ampm", label: " ", type: "txt", font: "6x8:2", bgCol: g.theme.bg},
],
},
{id: "bar", type: "custom", fraction: 0, fillx: 1, height: 6, col: g.theme.fg2, render: renderBar},
{height: 40},
{id: "date", type: "txt", font: "10%", valign: 1},
],
}, {lazy: true});
// adjustments based on screen size and whether we display am/pm
let thickness; // bar thickness, same as time font "pixel block" size
if (is12Hour) {
// Maximum font size = (<screen width> - <ampm: 2chars * (2*6)px>) / (5chars * 6px)
thickness = Math.floor((g.getWidth()-24)/(5*6));
} else {
layout.ampm.label = "";
thickness = Math.floor(g.getWidth()/(5*6));
}
layout.bar.height = thickness+1;
layout.time.font = "6x8:"+thickness;
layout.update();

function timeText(date) {
if (!is12Hour) {
Expand All @@ -78,31 +48,48 @@ function dateText(date) {
return `${dayName} ${dayMonth}`;
}

draw = function draw(force) {
if (!Bangle.isLCDOn()) {return;} // no drawing, also no new update scheduled
const date = new Date();
layout.time.label = timeText(date);
layout.ampm.label = ampmText(date);
layout.date.label = dateText(date);
const SECONDS_PER_MINUTE = 60;
layout.bar.fraction = date.getSeconds()/SECONDS_PER_MINUTE;
if (force) {
Bangle.drawWidgets();
layout.forgetLazyState();
}
layout.render();
// schedule update at start of next second
const millis = date.getMilliseconds();
setTimeout(draw, 1000-millis);
};

// Show launcher when button pressed
Bangle.setUI("clock");
Bangle.on("lcdPower", function(on) {
if (on) {
draw(true);
}
});
g.reset().clear();
Bangle.drawWidgets();
draw();
const ClockFace = require("ClockFace"),
clock = new ClockFace({
precision:1,
init: function() {
const Layout = require("Layout");
this.layout = new Layout({
type: "v", c: [
{
type: "h", c: [
{id: "time", label: "88:88", type: "txt", font: "6x8:5", col:g.theme.fg, bgCol: g.theme.bg}, // size updated below
{id: "ampm", label: " ", type: "txt", font: "6x8:2", col:g.theme.fg, bgCol: g.theme.bg},
],
},
{id: "bar", type: "custom", fraction: 0, fillx: 1, height: 6, col: g.theme.fg2, render: renderBar},
{height: 40},
{id: "date", type: "txt", font: "10%", valign: 1},
],
}, {lazy: true});
// adjustments based on screen size and whether we display am/pm
let thickness; // bar thickness, same as time font "pixel block" size
if (is12Hour) {
// Maximum font size = (<screen width> - <ampm: 2chars * (2*6)px>) / (5chars * 6px)
thickness = Math.floor((Bangle.appRect.w-24)/(5*6));
} else {
this.layout.ampm.label = "";
thickness = Math.floor(Bangle.appRect.w/(5*6));
}
this.layout.bar.height = thickness+1;
this.layout.time.font = "6x8:"+thickness;
this.layout.update();
},
update: function(date, c) {
if (c.m) this.layout.time.label = timeText(date);
if (c.h) this.layout.ampm.label = ampmText(date);
if (c.d) this.layout.date.label = dateText(date);
const SECONDS_PER_MINUTE = 60;
if (c.s) this.layout.bar.fraction = date.getSeconds()/SECONDS_PER_MINUTE;
this.layout.render();
},
resume: function() {
this.layout.forgetLazyState();
},
});
clock.start();
2 changes: 1 addition & 1 deletion apps/barclock/metadata.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "barclock",
"name": "Bar Clock",
"version": "0.09",
"version": "0.10",
"description": "A simple digital clock showing seconds as a bar",
"icon": "clock-bar.png",
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}],
Expand Down
103 changes: 103 additions & 0 deletions modules/ClockFace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
Most of the boilerplate needed to run a clock.
See ClockFace.md for documentation
*/
function ClockFace(options) {
if ("function"=== typeof options) options = {draw: options}; // simple usage
// some validation, in the hopes of at least catching typos/basic mistakes
Object.keys(options).forEach(k => {
if (![
"precision",
"init", "draw", "update",
"pause", "resume",
"up", "down", "upDown"
].includes(k)) throw `Invalid ClockFace option: ${k}`;
});
if (!options.draw && !options.update) throw "ClockFace needs at least one of draw() or update() functions";
this.draw = options.draw || (t=> {
options.update.apply(this, [t, {d: true, h: true, m: true, s: true}]);
});
this.update = options.update || (t => {
g.clear();
options.draw.apply(this, [t, {d: true, h: true, m: true, s: true}]);
});
if (options.precision===1000||options.precision===60000) throw "ClockFace precision is in seconds, not ms";
this.precision = (options.precision || 60);
if (options.init) this.init = options.init;
if (options.pause) this._pause = options.pause;
if (options.resume) this._resume = options.resume;
if ((options.up || options.down) && options.upDown) throw "ClockFace up/down and upDown cannot be used together";
if (options.up || options.down) this._upDown = (dir) => {
if (dir<0 && options.up) options.up.apply(this);
if (dir>0 && options.down) options.down.apply(this);
};
if (options.upDown) this._upDown = options.upDown;
}

ClockFace.prototype.tick = function() {
const time = new Date();
const now = {
d: `${time.getFullYear()}-${time.getMonth()}-${time.getDate()}`,
h: time.getHours(),
m: time.getMinutes(),
s: time.getSeconds(),
};
if (!this._last) {
g.clear(true);
Bangle.drawWidgets();
g.reset();
this.draw.apply(this, [time, {d: true, h: true, m: true, s: true}]);
} else {
let c = {d: false, h: false, m: false, s: false}; // changed
if (now.d!==this._last.d) c.d = c.h = c.m = c.s = true;
else if (now.h!==this._last.h) c.h = c.m = c.s = true;
else if (now.m!==this._last.m) c.m = c.s = true;
else if (now.s!==this._last.s) c.s = true;
g.reset();
this.update.apply(this, [time, c]);
}
this._last = now;
if (this.paused) return; // called redraw() while still paused
// figure out timeout: if e.g. precision=60s, update at the start of a new minute
const interval = this.precision*1000;
this._timeout = setTimeout(() => this.tick(), interval-(Date.now()%interval));
};

ClockFace.prototype.start = function() {
Bangle.loadWidgets();
if (this.init) this.init.apply(this);
if (this._upDown) Bangle.setUI("clockupdown", d=>this._upDown.apply(this,[d]));
else Bangle.setUI("clock");
delete this._last;
this.tick();

Bangle.on("lcdPower", on => {
if (on) this.resume();
else this.pause();
});
};

ClockFace.prototype.pause = function() {
if (!this._timeout) return; // already paused
clearTimeout(this._timeout);
delete this._timeout;
this.paused = true; // apps might want to check this
if (this._pause) this._pause.apply(this);
};
ClockFace.prototype.resume = function() {
if (this._timeout) return; // not paused
delete this._last;
delete this.paused;
if (this._resume) this._resume.apply(this);
this.tick(true);
};

/**
* Force a complete redraw
*/
ClockFace.prototype.redraw = function() {
delete this._last;
this.tick();
};

exports = ClockFace;
110 changes: 110 additions & 0 deletions modules/ClockFace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
ClockFace
=========

This module handles most of the tasks needed to set up a clock, so you can
concentrate on drawing the time.

Example
-------
Tthe [tutorial clock](https://www.espruino.com/Bangle.js+Clock) converted to use
this module:

```js

// Load fonts
require("Font7x11Numeric7Seg").add(Graphics);
// position on screen
const X = 160, Y = 140;

var ClockFace = require("ClockFace");
var clock = new ClockFace({
precision: 1, // update every second
draw: function(d) {
// work out how to display the current time
var h = d.getHours(), m = d.getMinutes();
var time = (" "+h).substr(-2)+":"+("0"+m).substr(-2);
// draw the current time (4x size 7 segment)
g.setFont("7x11Numeric7Seg", 4);
g.setFontAlign(1, 1); // align right bottom
g.drawString(time, X, Y, true /*clear background*/);
// draw the seconds (2x size 7 segment)
g.setFont("7x11Numeric7Seg", 2);
g.drawString(("0"+d.getSeconds()).substr(-2), X+30, Y, true /*clear background*/);
// draw the date, in a normal font
g.setFont("6x8");
g.setFontAlign(0, 1); // align center bottom
// pad the date - this clears the background if the date were to change length
var dateStr = " "+require("locale").date(d)+" ";
g.drawString(dateStr, g.getWidth()/2, Y+15, true /*clear background*/);
}
});
clock.start();

```



Complete Usage
--------------

```js

var ClockFace = require("ClockFace");
var clock = new ClockFace({
precision: 1, // optional, defaults to 60: how often to call update(), in seconds
init: function() { // optional
// called only once before starting the clock, but after setting up the
// screen/widgets, so you can use Bangle.appRect
},
draw: function(time, changed) { // at least draw or update is required
// (re)draw entire clockface, time is a Date object
// `changed` is the same format as for update() below, but always all true
},
// The difference between draw() and update() is that the screen is cleared
// before draw() is called, so it needs to always redraw the entire clock
update: function(time, changed) { // at least draw or update is required
// redraw date/time, time is a Date object
// if you want, you can only redraw the changed parts:
if (changed.d) // redraw date (changed.h/m/s will also all be true)
if (changed.h) // redraw hours
if (changed.m) // redraw minutes
if (changed.s) // redraw seconds
},
pause: function() { // optional, called when the screen turns off
// for example: turn off GPS/compass if the watch used it
},
resume: function() { // optional, called when the screen turns on
// for example: turn GPS/compass back on
},
up: function() { // optional, up handler
},
down: function() { // optional, down handler
},
upDown: function(dir) { // optional, combined up/down handler
if (dir === -1) // Up
else // (dir === 1): Down
},
});
clock.start();

```


Simple Usage
------------
Basic clocks can pass just a function to redraw the entire screen every minute:

```js

var ClockFace = require("ClockFace");
var clock = new ClockFace(function(time) {
// draw the current time at the center of the screen
g.setFont("Vector:50").setFontAlign(0, 0)
.drawString(
require("locale").time(time, true),
Bangle.appRect.w/2, Bangle.appRect.h/2
);
});
clock.start();

```

0 comments on commit c59eea3

Please sign in to comment.