mirror of https://github.com/espruino/BangleApps
commit
c2f9e7bf6f
14
apps.json
14
apps.json
|
@ -1620,5 +1620,19 @@
|
|||
"evaluate": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "beebclock",
|
||||
"name": "Beeb Clock",
|
||||
"icon": "beebclock.png",
|
||||
"version":"0.02",
|
||||
"description": "Clock face that may be coincidentally familiar to BBC viewers",
|
||||
"tags": "clock",
|
||||
"type": "clock",
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{"name":"beebclock.app.js","url":"beebclock.js"},
|
||||
{"name":"beebclock.img","url":"beebclock-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
0.01: Initial commit. Not very efficient, and widgets not working for some reason.
|
||||
0.02: Fixes; widget support
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwkHA4dQAgcFgPyl8QDxgNE0EAggXGAgcFDQ0TBgQXBkcgBQURCw8GBYUj+AEBI430AgI7BBAVgCIU/if0DoMC+UfiwLBgUyEQRyGgEzmPzCIQvCBwMPj4rCAwJECAAUD+MvkEQgMhkRABgEvkaKIJAXzj49BBYMBBIOm+IIBgMVPQxiBn8xkAIDAYMBj6TBSIYyFhUPBoJRCF4RlBAoJRBBggSBIIgAI0qhCFgUB/4WFTIYDDFwJMCCAUSifzDoYsGBQJIBfoM0kIEBn81168CfAwACKwMS+UT+ovC/8gmRRCGQqQBRgUjocyB4YYBI4QrEDwRdCfAQ4EsD5DAA5dCDYbMDCoTPCBAsQaYprHRosR0ICBB4ZtDEYJZHM4X/kMKFAwSGAocBn8hkX/NBMFEAJXDAQMD+IcBkcwBIZ1EHgP/BgIzD17QBDYPwI4kCn8/mcjkUyCAQlCVocB+IqDC4IVBmYWBkVAVAkvaIboDqAGBCwMyIwM/I4IVBoYHBI4qzDI4egLYURiaiCO4UAl4bCMIJLEEAUj//zlVQgynBmNC/5LBcQsA0BXBCoNCeQkDKQX1X5Ef+clTQIkCT4URiJYBXwYlEirHGOAkAJYIvHEAUTNoadBegn/EYUCB4IjDiRtCCoWgEwj8BCQMCCAQkBAoMhkZJDC4kFh/yNAMyifzE4U/kMf+RRGM4beCp/xibLBqERj6EDboQjCT4beDmMhQwRNEQIoACiISCIYILCgKNCXgQXFGYoTBC4a/DgcmBoRLCEAMDCQQPBbwxVBmLDDGwUCj/wHY4ADn8TBwbYD+3xCY8AhQlB+M/JwS3BGIXzj5RENAS1Cj86YQUB+U/KIdvmB6FIw8Qg3yl5KCgcyMAgZFiNPOwYXDAoURL45LCiSLD+YXBAoTXDAAbTIL4oJCCIRdEDA1gI4ooFgAA=="))
|
|
@ -0,0 +1,397 @@
|
|||
/* jshint esversion: 6 */
|
||||
// Beebclock
|
||||
// © 2020, Tom Gidden
|
||||
// https://github.com/tomgidden
|
||||
|
||||
const storage = require("Storage");
|
||||
const filename = 'beebjson';
|
||||
|
||||
require('FontTeletext10x18Ascii').add(Graphics);
|
||||
|
||||
// Double height text
|
||||
Graphics.prototype.drawStringDH = function (txt, px, py, align, gw) {
|
||||
let g2 = Graphics.createArrayBuffer(gw,18,1,{msb:true});
|
||||
g2.setFontTeletext10x18Ascii();
|
||||
let w = g2.stringWidth(txt);
|
||||
let c = (w+3)>>2;
|
||||
g2.drawString(txt);
|
||||
let img = {width:w,height:1,transparent:0,buffer:new ArrayBuffer(c)};
|
||||
let a = new Uint8Array(img.buffer);
|
||||
|
||||
let x;
|
||||
switch (align) {
|
||||
case 'C': x = px + (gw - w)/2; break;
|
||||
case 'R': x = gw - w + px; break;
|
||||
default: x = px;
|
||||
}
|
||||
|
||||
for (var y=0;y<18;y++) {
|
||||
a.set(new Uint8Array(g2.buffer,gw*y/8,c));
|
||||
this.drawImage(img,x,py+y*2);
|
||||
this.drawImage(img,x,py+1+y*2);
|
||||
}
|
||||
};
|
||||
|
||||
// Fill rectangle rotated around the centre
|
||||
Graphics.prototype.fillRotRect = function (sina, cosa, cx, cy, x0, x1, y0, y1) {
|
||||
let fn = Math.ceil;
|
||||
return this.fillPoly([
|
||||
fn(cx - x0*cosa + y0*sina), fn(cy - x0*sina - y0*cosa),
|
||||
fn(cx - x1*cosa + y0*sina), fn(cy - x1*sina - y0*cosa),
|
||||
fn(cx - x1*cosa + y1*sina), fn(cy - x1*sina - y1*cosa),
|
||||
fn(cx - x0*cosa + y1*sina), fn(cy - x0*sina - y1*cosa)
|
||||
]);
|
||||
};
|
||||
|
||||
// Draw a line from r1,a to r2,a relative to cx+cy
|
||||
Graphics.prototype.drawRotLine = function (sina, cosa, cx, cy, r1, r2) {
|
||||
return this.drawLine(
|
||||
cx + r1*sina, cy - r1*cosa,
|
||||
cx + r2*sina, cy - r2*cosa
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
(function(g) {
|
||||
// Display modes
|
||||
//
|
||||
// 0: full-screen
|
||||
// 1: with widgets
|
||||
// 2: centred on Bangle (v.1), no widgets or time/date
|
||||
// 3: centred with time above
|
||||
// 4: centred with date above
|
||||
// 5: centred with time and date above
|
||||
let mode;
|
||||
|
||||
// R1, R2: Outer and inner radii of hour marks
|
||||
// RC1, RC2: Outer and inner radii of hub
|
||||
// CX, CY: Centre location, relative to buffer (not screen, necessarily)
|
||||
// HW2, MW2: Half-width of hour and minute hand
|
||||
// HR, MR: Length of hour and minute hand, relative to CX,CY
|
||||
// M: Half-width of gap in hour marks
|
||||
// HSCALE: Half-width of hour mark as function(0<h<13)
|
||||
let R1, R2, RC1, RC2, CX, CY, HW2, MW2, HR, MR, M, HSCALE;
|
||||
|
||||
// Screen size
|
||||
const GW = g.getWidth();
|
||||
const GH = g.getHeight();
|
||||
|
||||
// Top margin: the gap taken from the top of the buffer, except when
|
||||
// in mode 0 (full screen)
|
||||
let TM;
|
||||
|
||||
// Buffer image. undefined means it needs regenerating
|
||||
let faceImg;
|
||||
|
||||
// with_seconds flag determines whether the face is updated every
|
||||
// second or every minute, and to draw the hand or not.
|
||||
let with_seconds = true;
|
||||
|
||||
// Display flags, determined from `mode` by setMode()
|
||||
let with_widgets = false;
|
||||
let with_digital_time = true;
|
||||
let with_digital_date = true;
|
||||
|
||||
// Create offscreen buffer for the once-per-minute face draw
|
||||
const G1 = Graphics.createArrayBuffer(g.getWidth(), g.getHeight(), 1, {msb:true});
|
||||
|
||||
// Precalculate sin/cos for the hour marks. Might be premature
|
||||
// optimisation, but might as well.
|
||||
let ss = [], cs = [];
|
||||
for (let h=1; h<=12; h++) {
|
||||
const a = Math.PI * h / 6;
|
||||
ss[h] = Math.sin(a);
|
||||
cs[h] = Math.cos(a);
|
||||
}
|
||||
|
||||
// Draw the face with hour and minute hand. Ideally, we'd separate
|
||||
// the face from the hands and double-buffer, but memory is limited,
|
||||
// so we buffer once and minute, and draw the second hand dynamically
|
||||
// (with a bit of flicker)
|
||||
const drawFace = (G) => {
|
||||
const fw = R1 * 2;
|
||||
const fh = R1 * 2;
|
||||
const fw2 = R1;
|
||||
const fh2 = R1;
|
||||
let hs = [];
|
||||
|
||||
// Wipe the image and start with white
|
||||
G.clear();
|
||||
G.setColor(1,1,1);
|
||||
|
||||
// Draw the hour marks.
|
||||
for (let h=1; h<=12; h++) {
|
||||
hs[h] = HSCALE(h);
|
||||
G.fillRotRect(ss[h], cs[h], CX, CY, -hs[h], hs[h], R2, R1);
|
||||
|
||||
}
|
||||
|
||||
// Draw the hub
|
||||
G.fillCircle(CX, CY, RC1);
|
||||
|
||||
// Black
|
||||
G.setColor(0,0,0);
|
||||
|
||||
// Clear the centre of the hub
|
||||
G.fillCircle(CX, CY, RC2);
|
||||
|
||||
// Draw the gap in the hour marks
|
||||
for (let h=1; h<=12; h++) {
|
||||
G.fillRotRect(ss[h], cs[h], CX, CY, -M, M, R2-1, R1+1);
|
||||
}
|
||||
|
||||
// Back to white for future draw operations
|
||||
G.setColor(1,1,1);
|
||||
|
||||
// While the buffer remains full-screen, we may trim out the
|
||||
// bottom of the image so we can shift the whole thing down for
|
||||
// widgets.
|
||||
const img = {width:GW,height:GH-TM,buffer:G.buffer};
|
||||
return img;
|
||||
};
|
||||
|
||||
let hours, minutes, seconds, date;
|
||||
|
||||
// Schedule event for calling at the start of the next second
|
||||
const inOneSecond = (cb) => {
|
||||
let now = new Date();
|
||||
clearTimeout();
|
||||
setTimeout(cb, 1000 - now.getMilliseconds());
|
||||
};
|
||||
|
||||
// Schedule event for calling at the start of the next minute
|
||||
const inOneMinute = (cb) => {
|
||||
let now = new Date();
|
||||
clearTimeout();
|
||||
setTimeout(cb, 60000 - (now.getSeconds() * 1000 + now.getMilliseconds()));
|
||||
};
|
||||
|
||||
// Draw a fat hour/minute hand
|
||||
const drawHand = (G, a, w2, r1, r2) =>
|
||||
G.fillRotRect(Math.sin(a), Math.cos(a), CX, CY, -w2, w2, r1, r2);
|
||||
|
||||
// Redraw function
|
||||
const drawAll = (force) => {
|
||||
let now = new Date();
|
||||
|
||||
if (!faceImg) force = true;
|
||||
|
||||
let face_changed = force;
|
||||
let date_changed = false;
|
||||
|
||||
tmp = hours;
|
||||
hours = now.getHours();
|
||||
if (tmp !== hours)
|
||||
face_changed = true;
|
||||
|
||||
tmp = minutes;
|
||||
minutes = now.getMinutes();
|
||||
if (tmp !== minutes)
|
||||
face_changed = true;
|
||||
|
||||
// If the face has been updated and/or needs a redraw,
|
||||
// face_changed is true.
|
||||
|
||||
let time_changed = face_changed;
|
||||
|
||||
// If the screen needs an update, regardless of whether the face
|
||||
// needs a redraw, time_changed is true.
|
||||
|
||||
if (with_seconds) {
|
||||
// If we're going by second, we always need an update.
|
||||
seconds = now.getSeconds();
|
||||
time_changed = true;
|
||||
}
|
||||
|
||||
if (with_digital_date) {
|
||||
// See if the date has changed. If it has, then we need a
|
||||
// full-blown redraw of the screen and the face, plus text.
|
||||
tmp = date;
|
||||
date = now.getDate();
|
||||
if (tmp !== date) {
|
||||
date_changed = true;
|
||||
face_changed = true; // Should have changed anyway with hour/minute rollover
|
||||
}
|
||||
}
|
||||
|
||||
if (face_changed) {
|
||||
// Redraw the face and hands onto the buffer G1.
|
||||
faceImg = drawFace(G1);
|
||||
drawHand(G1, Math.PI*hours/6, HW2, RC1, HR);
|
||||
drawHand(G1, Math.PI*minutes/30, MW2, RC1, MR);
|
||||
}
|
||||
|
||||
// Has the time updated? If so, we'll need to draw something.
|
||||
if (time_changed) {
|
||||
|
||||
// Are we adding text?
|
||||
if (with_digital_date || with_digital_time) {
|
||||
|
||||
// Construct the date/time text to add above the face
|
||||
let d = now.toString();
|
||||
let da = d.toString().split(" ");
|
||||
let txt;
|
||||
|
||||
if (with_digital_time) {
|
||||
txt = da[4].substr(0, 5);
|
||||
if (with_digital_date)
|
||||
G1.drawStringDH(txt+',', 24, 0, 'L', GW);
|
||||
else
|
||||
G1.drawStringDH(txt, 0, 0, 'C', GW);
|
||||
}
|
||||
|
||||
if (with_digital_date) {
|
||||
let txt = [da[0], da[1], da[2]].join(" ");
|
||||
if (with_digital_time)
|
||||
G1.drawStringDH(txt, -24, 0, 'R', GW);
|
||||
else
|
||||
G1.drawStringDH(txt, 0, 0, 'C', GW);
|
||||
}
|
||||
}
|
||||
|
||||
// If the time has updated, we need to _at least_ draw the
|
||||
// image to the screen.
|
||||
g.setColor(1,1,1);
|
||||
g.drawImage({width:GW,
|
||||
height:GH-TM,
|
||||
buffer:G1.buffer}, 0, TM);
|
||||
|
||||
// and possibly add the second hand
|
||||
if (with_seconds) {
|
||||
let a = 2.0 * Math.PI * seconds / 60.0;
|
||||
g.drawRotLine(Math.sin(a), Math.cos(a), CX, CY+TM, RC1, R1);
|
||||
}
|
||||
|
||||
// Clock chime on the hour.
|
||||
if (hours >= 0 && minutes === 0)
|
||||
try {
|
||||
Bangle.buzz();
|
||||
} catch (e) { }
|
||||
|
||||
// And draw widgets if we're in that mode
|
||||
if (with_widgets)
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
|
||||
// Schedule to repeat this. A `setTimeout(1000)` isn't good
|
||||
// enough, as all the above might've taken some milliseconds and
|
||||
// we don't want to drift.
|
||||
if (with_seconds)
|
||||
inOneSecond(drawAll);
|
||||
else
|
||||
inOneMinute(drawAll);
|
||||
};
|
||||
|
||||
const setButtons = () => {
|
||||
const opts = { repeat: true, edge:'rising', debounce:30};
|
||||
|
||||
// BTN1: enable/disable second hand
|
||||
setWatch(changeSeconds, BTN1, opts);
|
||||
|
||||
// BTN2: return to launcher
|
||||
setWatch(Bangle.showLauncher, BTN2, { repeat:false, edge:'falling' });
|
||||
|
||||
// BTN3: change display mode
|
||||
setWatch(function () { ++mode; setMode(); drawAll(true); }, BTN3, opts);
|
||||
};
|
||||
|
||||
// Load display parameters based on `mode`
|
||||
const setMode = () => {
|
||||
// Normalize mode to 0 <= mode <= 5
|
||||
mode = (6+mode) % 6;
|
||||
|
||||
// [R1, R2, RC1, RC2, HW2, MW3, HR, MR, M, HSCALE] =
|
||||
const scales = [
|
||||
[120, 84, 17, 12.4, 4.6, 2.2, 8, 2, 1, h => (3.0 + Math.ceil(h/1.5)) ],
|
||||
[102, 70, 14.6, 10.7, 3.88, 1.8, 8, 2, 1, h => (2.4 + Math.ceil(h/1.6)) ],
|
||||
];
|
||||
|
||||
if (mode < 3) {
|
||||
// Face without time/date text. Might have widgets though.
|
||||
with_digital_time = with_digital_date = false;
|
||||
with_widgets = (mode == 1);
|
||||
}
|
||||
else {
|
||||
// Face with time/date text, but no widgets
|
||||
with_digital_time = (mode-2)&1;
|
||||
with_digital_date = (mode-2)&2;
|
||||
with_widgets = false;
|
||||
}
|
||||
|
||||
// Destructure the array to the global display parameters
|
||||
let arr = scales[mode > 0 ? 1 : 0];
|
||||
R1 = arr[0];
|
||||
R2 = arr[1];
|
||||
RC1 = arr[2];
|
||||
RC2 = arr[3];
|
||||
HW2 = arr[4];
|
||||
MW2 = arr[5];
|
||||
HR = R2 - arr[6];
|
||||
MR = R1 - arr[7];
|
||||
M = arr[8];
|
||||
HSCALE = arr[9];
|
||||
TM = with_widgets ? 36 : 0;
|
||||
|
||||
CX = GW/2;
|
||||
CY = R1;
|
||||
|
||||
// If we're in the small-face + text regime, we're going to buffer
|
||||
// the full screen but draw the clock face further down to give
|
||||
// space for the text.
|
||||
//
|
||||
// Compare with modes 0 (full-screen) and 1 (with_widgets==true)
|
||||
// where the face is drawn at the top of the buffer, but drawn
|
||||
// lower down the screen (so CY doesn't move)
|
||||
if (mode > 1) {
|
||||
CY += 36;
|
||||
}
|
||||
|
||||
// We only don't bother redrawing the face from modes 2 to 5, as
|
||||
// they're the same.
|
||||
if (!faceImg || mode<3) {
|
||||
faceImg = undefined;
|
||||
}
|
||||
|
||||
// Store the settings for next time
|
||||
try {
|
||||
storage.writeJSON(filename, [mode,with_seconds]);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
// Clear the screen: we need to make sure all parts are cleaned off.
|
||||
g.clear();
|
||||
};
|
||||
|
||||
const changeSeconds = () => {
|
||||
with_seconds = !with_seconds;
|
||||
drawAll(true);
|
||||
};
|
||||
|
||||
Bangle.loadWidgets();
|
||||
|
||||
// Restore mode
|
||||
try {
|
||||
conf = storage.readJSON(filename);
|
||||
mode = conf[0];
|
||||
with_seconds = conf[1];
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
mode = 1;
|
||||
}
|
||||
|
||||
setButtons();
|
||||
setMode();
|
||||
drawAll();
|
||||
|
||||
Bangle.on('lcdPower', (on) => {
|
||||
if (on) {
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
drawAll();
|
||||
} else {
|
||||
clearTimeout();
|
||||
}
|
||||
});
|
||||
|
||||
})(g);
|
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
Loading…
Reference in New Issue