diff --git a/apps.json b/apps.json index cc316841b..83b671ada 100644 --- a/apps.json +++ b/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} + ] } ] diff --git a/apps/beebclock/ChangeLog b/apps/beebclock/ChangeLog new file mode 100644 index 000000000..14dd12220 --- /dev/null +++ b/apps/beebclock/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial commit. Not very efficient, and widgets not working for some reason. +0.02: Fixes; widget support diff --git a/apps/beebclock/beebclock-icon.js b/apps/beebclock/beebclock-icon.js new file mode 100644 index 000000000..b4d173068 --- /dev/null +++ b/apps/beebclock/beebclock-icon.js @@ -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==")) diff --git a/apps/beebclock/beebclock.js b/apps/beebclock/beebclock.js new file mode 100644 index 000000000..7b071c22d --- /dev/null +++ b/apps/beebclock/beebclock.js @@ -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 { + 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); diff --git a/apps/beebclock/beebclock.png b/apps/beebclock/beebclock.png new file mode 100644 index 000000000..447ec9a41 Binary files /dev/null and b/apps/beebclock/beebclock.png differ