diff --git a/apps/cc_clock24/ChangeLog b/apps/cc_clock24/ChangeLog new file mode 100644 index 000000000..c807c40a0 --- /dev/null +++ b/apps/cc_clock24/ChangeLog @@ -0,0 +1,3 @@ +0.01: copied from andark (V0.08) + refactored + add 24 hour mode diff --git a/apps/cc_clock24/README.md b/apps/cc_clock24/README.md new file mode 100644 index 000000000..84b1fa874 --- /dev/null +++ b/apps/cc_clock24/README.md @@ -0,0 +1,16 @@ +# Analog Clock With 24 hour hands + +## Features + +* second hand (only on unlocked screen) +* date +* battery percentage (showing charge status with color) +* turned off or swipeable widgets (choose in settings) + +![logo](cc_clock24_screen.png) + +## Settings + +* whether to load widgets, or not; if widgets are loaded, they are swipeable from the top; if not, NO ACTIONS of widgets are available +* date and battery can be printed both below hands (as if hands were physical) and above (more readable) +* hour hand can be made slighly shorter to improve readability when minute hand is behind a number diff --git a/apps/cc_clock24/app.js b/apps/cc_clock24/app.js new file mode 100644 index 000000000..82487a5dd --- /dev/null +++ b/apps/cc_clock24/app.js @@ -0,0 +1,277 @@ +// ----- const ----- + +const defaultSettings = { + loadWidgets : false, + textAboveHands : false, + shortHrHand : false, + show24HourMode : false +}; + +const settings = Object.assign(defaultSettings, require('Storage').readJSON('cc_clock24.json', 1) || {}); + +const center = { + "x": g.getWidth()/2, + "y": g.getHeight()/2 +}; + +const hourNumberPositions = (function() { + let positions = []; + + for (let hour = 1; hour <= 12; hour += 1) { + let phi = 30 * (hour - 3) * (Math.PI / 180); + let x = center.x + 2 + Math.cos(phi) * (center.x - 10); + let y = center.y + 2 + Math.sin(phi) * (center.x - 10); + + // fix positions, which are not on a circle + if (hour == 3){ x -= 10; } + else if (hour == 6){ y -= 10; } + else if (hour == 9){ x += 10; } + else if (hour == 12){ y += 10; } + else if (hour == 10){ x += 3; } + + positions.push([hour, x, y]); + } + return positions; +})(); + + +// ----- global vars ----- + +let drawTimeout; +let queueMillis = 1000; +let unlock = true; +let lastBatteryStates = [E.getBattery()]; + + +// ----- functions ----- + +function updateState() { + updateBatteryStates(); + + if (Bangle.isLCDOn()) { + if (!Bangle.isLocked()) { + queueMillis = 1000; + unlock = true; + } + else { + queueMillis = 60000; + unlock = false; + } + draw(); + } + else { + if (drawTimeout) + clearTimeout(drawTimeout); + drawTimeout = undefined; + } +} + + +function updateBatteryStates() { + lastBatteryStates.push(E.getBattery()); + if (lastBatteryStates.length > 5) + lastBatteryStates.shift(); // remove 1st item +} + +function drawTicks() { // draws the scale once the app is startet + // clear screen + g.setBgColor(0, 0, 0); + g.clear(); + + // draw ticks + for (let i = 1; i <= 60; i++){ + const phi = 6 * i * (Math.PI / 180); + let thickness = 2; + if (i % 5 == 0) + thickness = 5; + + g.fillPoly(calcHandPolygon(300, thickness, phi), true); + g.setColor(0, 0, 0); + g.fillRect(10, 10, 2 * center.x - 10, 2 * center.x - 10); + g.setColor(1, 1, 1); + } +} + +function calcHandPolygon(len, thickness, phi) { + const x = center.x + Math.cos(phi) * len/2, + y = center.y + Math.sin(phi) * len/2, + d = { + "d": 3, + "x": thickness/2 * Math.cos(phi + Math.PI/2), + "y": thickness/2 * Math.sin(phi + Math.PI/2) + }, + polygon = [ + center.x - d.x, + center.y - d.y, + center.x + d.x, + center.y + d.y, + x + d.x, + y + d.y, + x - d.x, + y - d.y + ]; + return polygon; +} + + +// ----- draw ---- + +function draw() { + // draw black rectangle in the middle to clear screen from scale and hands + g.setColor(0, 0, 0); + g.fillRect(10, 10, 2 * center.x - 10, 2 * center.x - 10); + + g.setFontAlign(0, 0); + drawNumbers(); + + const date = new Date(); + if (settings.textAboveHands) { + drawHands(date); + drawText(date); + } + else { + drawText(date); + drawHands(date); + } + queueDraw(); +} + + +function drawNumbers() { + g.setFont("Vector", 20); + + const batteryState = calcAvgBatteryState(); + if (batteryState < 20) + g.setColor(1, 0, 0); // draw in red, if battery is low + else if (batteryState < 40) + g.setColor(1, 1, 0); + else + g.setColor(1, 1, 1); + + g.setBgColor(0, 0, 0); + for(let i = 0; i < 12; i++) { + let hour = hourNumberPositions[i][0]; + if (settings.show24HourMode) + hour *= 2; + + g.drawString(hour, hourNumberPositions[i][1], hourNumberPositions[i][2], true); + } +} + +function calcAvgBatteryState() { + const n = lastBatteryStates.length; + if (n == 0) + return 100; + + let sum = lastBatteryStates.reduce((acc, value) => acc + value, 0); + return Math.round(sum / n); +} + +function drawHands(date) { + let m = date.getMinutes(), + h = date.getHours(), + s = date.getSeconds(); + + g.setColor(1, 1, 1); + + let numHoursForHourHand = settings.show24HourMode? 24 : 12; + + if (h > numHoursForHourHand) + h = h - numHoursForHourHand; + + + const hour_angle = 2 * Math.PI / numHoursForHourHand * (h + m/60) - Math.PI/2, + minute_angle = 2 * Math.PI / 60 * m - Math.PI/2, + second_angle = 2 * Math.PI / 60 * s - Math.PI/2; + + const hourPolygon = calcHandPolygon(settings.shortHrHand ? 88 : 100, 5, hour_angle); + g.fillPoly(hourPolygon, true); + + const minutePolygon = calcHandPolygon(150, 5, minute_angle); + g.fillPoly(minutePolygon, true); + + if (unlock) { + const secondPolygon = calcHandPolygon(150, 2, second_angle); + g.fillPoly(secondPolygon, true); + } + g.fillCircle(center.x, center.y, 4); +} + +function drawText(date) { + if (!unlock) + return; + + g.setBgColor(0, 0, 0); + g.setColor(1, 1, 1); + + const today = new Date(); + const dateStr = formatDate(today); + g.setFont("Vector", 16); + g.drawString(dateStr, center.x + 5, center.y - 30, true); + + const batteryStr = calcAvgBatteryState() + "%"; + + if (Bangle.isCharging()) + g.setBgColor(1, 0, 0); + + g.setFont("Vector", 24); + g.drawString(batteryStr, center.x, center.y + 30, true); +} + +function formatDate(date) { + const weekdays = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"]; + const month_names = ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"]; + const day = date.getDate(); + const month_name = month_names[date.getMonth()]; + const weekday = weekdays[date.getDay()]; + + return weekday + " " + day + ". " + month_name; +} + +function queueDraw() { + if (drawTimeout) + clearTimeout(drawTimeout); + + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, queueMillis - (Date.now() % queueMillis)); +} + + +//// main running sequence //// + +// Show launcher when middle button pressed, and widgets that we're clock +Bangle.setUI({ + mode: "clock", + remove: function() { + Bangle.removeListener('lcdPower', updateState); + Bangle.removeListener('lock', updateState); + Bangle.removeListener('charging', draw); + + // We clear drawTimout after removing all listeners, because they can add one again + if (drawTimeout) + clearTimeout(drawTimeout); + + drawTimeout = undefined; + require("widget_utils").show(); + } +}); + +// Load widgets if needed, and make them show swipeable +if (settings.loadWidgets) { + Bangle.loadWidgets(); + require("widget_utils").swipeOn(); +} +else if (global.WIDGETS) { + require("widget_utils").hide(); +} + +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower', updateState); +Bangle.on('lock', updateState); +Bangle.on('charging', draw); // Immediately redraw when charger (dis)connected + +updateState(); +drawTicks(); +draw(); diff --git a/apps/cc_clock24/app_icon.js b/apps/cc_clock24/app_icon.js new file mode 100644 index 000000000..b213fe5c8 --- /dev/null +++ b/apps/cc_clock24/app_icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgIEBoUAiAKCgUCBQUEColEAYUQhAmKCwgeCAAcCgEDjwEBkEAg8TBocNgYFDh8GAYMDxkPjEA8EAwkHJgIcBAoPfAoYWCBYYFIgfvAoX4FYRJEAp9gAomYNAOAArPwAogAC4AFiRoIFJLgIFJuADCg//Q4U//4FDj4FEAAV4Aoi0CSxBsCA==")) \ No newline at end of file diff --git a/apps/cc_clock24/cc_clock24_icon.png b/apps/cc_clock24/cc_clock24_icon.png new file mode 100644 index 000000000..cded02071 Binary files /dev/null and b/apps/cc_clock24/cc_clock24_icon.png differ diff --git a/apps/cc_clock24/cc_clock24_screen.png b/apps/cc_clock24/cc_clock24_screen.png new file mode 100644 index 000000000..1f0e5b089 Binary files /dev/null and b/apps/cc_clock24/cc_clock24_screen.png differ diff --git a/apps/cc_clock24/metadata.json b/apps/cc_clock24/metadata.json new file mode 100644 index 000000000..e450893b7 --- /dev/null +++ b/apps/cc_clock24/metadata.json @@ -0,0 +1,18 @@ +{ "id": "cc_clock24", + "name": "CC Clock 24", + "shortName":"CC-Clock24", + "version":"0.01", + "description": "analog clock face with 24 hour pointer", + "icon": "cc_clock24_icon.png", + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "screenshots": [{"url":"cc_clock24_screen.png"}], + "readme": "README.md", + "storage": [ + {"name":"cc_clock24.app.js","url":"app.js"}, + {"name":"cc_clock24.settings.js","url":"settings.js"}, + {"name":"cc_clock24.img","url":"app_icon.js","evaluate":true} + ], + "data": [{"name":"cc_clock24.json"}] +} diff --git a/apps/cc_clock24/settings.js b/apps/cc_clock24/settings.js new file mode 100644 index 000000000..4aa19215d --- /dev/null +++ b/apps/cc_clock24/settings.js @@ -0,0 +1,33 @@ +(function(back) { + const defaultSettings = { + loadWidgets : false, + textAboveHands : false, + shortHrHand : false, + show24HourMode : false + } + let settings = Object.assign(defaultSettings, require('Storage').readJSON('cc_clock24.json',1) || {}); + + const save = () => require('Storage').write('cc_clock24.json', settings); + + const appMenu = { + '': {title: 'cc_clock24'}, '< Back': back, + /*LANG*/'Load widgets': { + value : !!settings.loadWidgets, + onchange : v => { settings.loadWidgets=v; save();} + }, + /*LANG*/'Text above hands': { + value : !!settings.textAboveHands, + onchange : v => { settings.textAboveHands=v; save();} + }, + /*LANG*/'Short hour hand': { + value : !!settings.shortHrHand, + onchange : v => { settings.shortHrHand=v; save();} + }, + /*LANG*/'Show 24 hour mode': { + value : !!settings.show24HourMode, + onchange : v => { settings.show24HourMode=v; save();} + }, + }; + + E.showMenu(appMenu); +})