From 7ab3fe2f37de68b802dcab074b3e00e4cfa1b45c Mon Sep 17 00:00:00 2001 From: Paul Spenke Date: Fri, 5 Jan 2024 12:15:31 +0100 Subject: [PATCH] Add new Line Clock application This commit introduces the Line Clock application. The Line Clock is a readable analog clock that is customizable via the theme configuration. It includes a JavaScript logic file, an app icon, a metadata JSON file, a README, and a ChangeLog file. This also includes the MIT license file. --- apps/line_clock/ChangeLog | 1 + apps/line_clock/LICENSE | 21 +++ apps/line_clock/README.md | 11 ++ apps/line_clock/app-icon.js | 1 + apps/line_clock/app-icon.png | Bin 0 -> 3511 bytes apps/line_clock/app-screenshot.png | Bin 0 -> 2653 bytes apps/line_clock/app.js | 273 +++++++++++++++++++++++++++++ apps/line_clock/metadata.json | 17 ++ 8 files changed, 324 insertions(+) create mode 100644 apps/line_clock/ChangeLog create mode 100644 apps/line_clock/LICENSE create mode 100644 apps/line_clock/README.md create mode 100644 apps/line_clock/app-icon.js create mode 100644 apps/line_clock/app-icon.png create mode 100644 apps/line_clock/app-screenshot.png create mode 100644 apps/line_clock/app.js create mode 100644 apps/line_clock/metadata.json diff --git a/apps/line_clock/ChangeLog b/apps/line_clock/ChangeLog new file mode 100644 index 000000000..0f7cf828d --- /dev/null +++ b/apps/line_clock/ChangeLog @@ -0,0 +1 @@ +0.1 init app diff --git a/apps/line_clock/LICENSE b/apps/line_clock/LICENSE new file mode 100644 index 000000000..404cbc7a0 --- /dev/null +++ b/apps/line_clock/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Paul Spenke + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/line_clock/README.md b/apps/line_clock/README.md new file mode 100644 index 000000000..5789acbbc --- /dev/null +++ b/apps/line_clock/README.md @@ -0,0 +1,11 @@ +# Line Clock + +This app displays a simple, different looking, analog clock. It considers the +currently configured "theme" (and may therefore look different than shown in +the screenshot on your watch depending on which theme you prefer). + +![](app-screenshot.png) + +## License + +[MIT License](LICENSE) diff --git a/apps/line_clock/app-icon.js b/apps/line_clock/app-icon.js new file mode 100644 index 000000000..eaaf719b4 --- /dev/null +++ b/apps/line_clock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgYMJh/4AgUD+AeKgIRDj/+n41O/4RQABcfIJYAEKZgAkL4U/8ARNBwIRP/+AGx6YBPSH/4ASPh/A/hfDAAZAHg/8gP/LguSoARHEwIRFiVJkDCFjgRHgEJkg4CcwQjIAAMEHAUDCoIRB46kIHAkH//xLIw4I8eAnCNKHAYAO/xxEABg4ByASPHAkBKAbUE/5xGhP//wRFv4RDOIYIB//ACQr1FHAIRJAA0TCAP/ZwIALgYRJVowRCj/4BIkBLIgABgRHC/KqFaI4RC5MkJBlPR4UECJizJJwoAKCKImVQAwAJv0HL5S6CbwIjLCKMAn4RDh0/LMKMhWaYAKA=")) diff --git a/apps/line_clock/app-icon.png b/apps/line_clock/app-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2753538122d298ede31a6526c23ac519dee6d1e7 GIT binary patch literal 3511 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 literal 0 HcmV?d00001 diff --git a/apps/line_clock/app.js b/apps/line_clock/app.js new file mode 100644 index 000000000..0596b865e --- /dev/null +++ b/apps/line_clock/app.js @@ -0,0 +1,273 @@ +const handWidth = 6; +const hourRadius = 4; +const hourWidth = 8; +const hourLength = 40; +const hourSLength = 20; +const radius = 220; +const lineOffset = 115; +const hourOffset = 32; +const numberOffset = 85; +const numberSize = 22; + +let settings = { + showLock: true +}; + +let gWidth = g.getWidth(), gCenterX = gWidth/2; +let gHeight = g.getHeight(), gCenterY = gHeight/2; + +let currentTime = new Date(); +let currentHour = currentTime.getHours(); +let currentMinute = currentTime.getMinutes(); + +let drawTimeout; + +function imgLock() { + return { + width : 16, height : 16, bpp : 1, + transparent : 0, + buffer : E.toArrayBuffer(atob("A8AH4A5wDDAYGBgYP/w//D/8Pnw+fD58Pnw//D/8P/w=")) + }; +} + +/** + * Retrieves the angle of the hour hand for the current time. + * + * @returns {number} The angle of the hour hand in degrees. + */ +function getHourHandAngle() { + let hourHandAngle = 30 * currentHour; + hourHandAngle += 0.5 * currentMinute; + return hourHandAngle; +} + +let hourAngle = getHourHandAngle(); + +/** + * Converts degrees to radians. + * + * @param {number} degrees - The degrees to be converted to radians. + * @return {number} - The equivalent value in radians. + */ +function degreesToRadians(degrees) { + return degrees * (Math.PI / 180); +} + +/** + * Rotates an array of points around a given angle and radius. + * + * @param {Array} points - The array of points to be rotated. + * @param {number} angle - The angle in degrees to rotate the points. + * @param {number} rad - The radius to offset the rotation. + * @returns {Array} - The array of rotated points. + */ +function rotatePoints(points, angle, rad) { + const ang = degreesToRadians(angle); + const hAng = degreesToRadians(hourAngle); + const rotatedPoints = []; + points.map(function(point) { + return { + x: point.x * Math.cos(ang) - point.y * Math.sin(ang), + y: point.x * Math.sin(ang) + point.y * Math.cos(ang) + }; + }).forEach(function(point) { + rotatedPoints.push(point.x + gCenterX - (rad * Math.sin(hAng))); + rotatedPoints.push(point.y + gCenterY + (rad * Math.cos(hAng))); + }); + return rotatedPoints; +} + +/** + * Draws a hand on the canvas. + * + * @function drawHand + * + * @returns {void} + */ +function drawHand() { + g.setColor(0xF800); + const halfWidth = handWidth / 2; + + const points = [{ + x: -halfWidth, + y: -gHeight + }, { + x: halfWidth, + y: -gHeight + }, { + x: halfWidth, + y: gHeight + }, { + x: -halfWidth, + y: gHeight + }]; + + g.fillPolyAA(rotatePoints(points, hourAngle, 0)); +} + +/** + * Retrieves the hour coordinates for a given small flag. + * @param {boolean} small - Determines if the flag is small. + * @returns {Array} - An array of hour coordinates. + */ +function getHourCoordinates(small) { + const dist = small ? (hourSLength - hourLength) : 0; + const halfWidth = hourWidth / 2; + const gh = gHeight + lineOffset; + return [{ + x: -halfWidth, + y: -gh - dist + }, { + x: halfWidth, + y: -gh - dist + }, { + x: halfWidth, + y: -gh + hourLength + }, { + x: -halfWidth, + y: -gh + hourLength + }]; +} + +/** + * Assign the given time to the hour dot on the clock face. + * + * @param {number} a - The time value to assign to the hour dot. + * @return {void} + */ +function hourDot(a) { + const h = gHeight + lineOffset; + const rotatedPoints = rotatePoints( + [{ + x: 0, + y: -h + hourLength - (hourRadius / 2) + }], a, radius + ); + g.fillCircle(rotatedPoints[0], rotatedPoints[1], hourRadius); +} + +/** + * Convert an hour into a number and display it on the clock face. + * + * @param {number} a - The hour to be converted (between 0 and 360 degrees). + */ +function hourNumber(a) { + const h = gHeight + lineOffset; + const rotatedPoints = rotatePoints( + [{ + x: 0, + y: -h + hourLength + hourOffset + }], a, radius + ); + g.drawString(String(a / 30), rotatedPoints[0], rotatedPoints[1]); +} + +/** + * Draws a number on the display. + * + * @param {number} n - The number to be drawn. + * @return {void} + */ +function drawNumber(n) { + const h = gHeight + lineOffset; + const halfWidth = handWidth / 2; + const rotatedPoints = rotatePoints( + [{ + x: 0, + y: -h + hourLength + numberOffset + }], hourAngle, radius + ); + g.setColor(0xF800); + g.fillCircle(rotatedPoints[0], rotatedPoints[1], numberSize+ halfWidth); + g.setColor(g.theme.bg); + g.fillCircle(rotatedPoints[0], rotatedPoints[1], numberSize - halfWidth); + g.setColor(g.theme.fg); + g.setFont("Vector:"+numberSize); + g.drawString(String(n), rotatedPoints[0], rotatedPoints[1]); +} + +const hourPoints = getHourCoordinates(false); +const hourSPoints = getHourCoordinates(true); + +/** + * Draws an hour on a clock face. + * + * @param {number} h - The hour to be drawn on the clock face. + * @return {undefined} + */ +function drawHour(h) { + if (h === 0) { h= 12; } + if (h === 13) { h= 1; } + g.setColor(g.theme.fg); + g.setFont("Vector:32"); + const a = h * 30; + g.fillPolyAA(rotatePoints(hourPoints, a, radius)); + g.fillPolyAA(rotatePoints(hourSPoints, a + 15, radius)); + hourNumber(a); + hourDot(a + 5); + hourDot(a + 10); + hourDot(a + 20); + hourDot(a + 25); +} + +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + +function lockListenerBw(isLocked) { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + draw(); +} +Bangle.on('lock', lockListenerBw); + +Bangle.setUI({ + mode : "clock", + remove : function() { + Bangle.removeListener('lock', lockListenerBw); + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +/** + * Draws a clock on the canvas using the current time. + * + * @return {undefined} + */ +function draw() { + queueDraw(); + currentTime = new Date(); + currentHour = currentTime.getHours(); + if (currentHour > 12) { + currentHour -= 12; + } + currentMinute = currentTime.getMinutes(); + + hourAngle = getHourHandAngle(); + + g.clear(); + g.setFontAlign(0, 0); + + g.setColor(g.theme.bg); + g.fillRect(0, 0, gWidth, gHeight); + + if(settings.showLock && Bangle.isLocked()){ + g.setColor(g.theme.fg); + g.drawImage(imgLock(), gWidth-16, 2); + } + + drawHour(currentHour); + drawHour(currentHour-1); + drawHour(currentHour+1); + + + drawHand(); + drawNumber(currentMinute); +} + +draw(); diff --git a/apps/line_clock/metadata.json b/apps/line_clock/metadata.json new file mode 100644 index 000000000..a2aad5f58 --- /dev/null +++ b/apps/line_clock/metadata.json @@ -0,0 +1,17 @@ +{ "id": "line_clock", + "name": "Line Clock", + "shortName":"Line Clock", + "version":"0.1", + "description": "a readable analog clock", + "icon": "app-icon.png", + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "allow_emulator": true, + "screenshots": [{"url":"app-screenshot.png"}], + "readme": "README.md", + "storage": [ + {"name":"line_clock.app.js","url":"app.js"}, + {"name":"line_clock.img","url":"app-icon.js","evaluate":true} + ] +}