From 06149002d74e2941a69151f6e9a47298314e0b53 Mon Sep 17 00:00:00 2001 From: Tom Gidden Date: Tue, 5 May 2020 14:15:13 +0100 Subject: [PATCH] Beebclock --- apps.json | 14 ++ apps/beebclock/ChangeLog | 2 + apps/beebclock/beebclock-icon.js | 1 + apps/beebclock/beebclock.js | 397 +++++++++++++++++++++++++++++++ apps/beebclock/beebclock.png | Bin 0 -> 3318 bytes 5 files changed, 414 insertions(+) create mode 100644 apps/beebclock/ChangeLog create mode 100644 apps/beebclock/beebclock-icon.js create mode 100644 apps/beebclock/beebclock.js create mode 100644 apps/beebclock/beebclock.png 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 0000000000000000000000000000000000000000..447ec9a41a99e765323f2d62403904f3ce73f1c6 GIT binary patch literal 3318 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 literal 0 HcmV?d00001