Merge remote-tracking branch 'upstream/master'

pull/757/head
Francesco Bedussi 2021-06-06 17:26:20 +02:00
commit a3edf90bc5
379 changed files with 33759 additions and 1929 deletions

View File

@ -29,3 +29,4 @@ Changed for individual apps are listed in `apps/appname/ChangeLog`
* Added progress bar on Bangle.js for uploads
* Provide a proper error message in case JSON decode fails
* Check you're connecting with a Bangle.js of the correct version
* Allow 'data' style app files to be uploaded with the app (and switch over settings files for various apps)

View File

@ -249,14 +249,20 @@ and which gives information about the app for the Launcher.
{"name":"appid.js", // filename to use in storage.
// If name=='RAM', the code is sent directly to Bangle.js and is not saved to a file
"url":"", // URL of file to load (currently relative to apps/)
"content":"..." // if supplied, this content is loaded directly
"evaluate":true // if supplied, data isn't quoted into a String before upload
"content":"...", // if supplied, this content is loaded directly
"evaluate":true, // if supplied, data isn't quoted into a String before upload
// (eg it's evaluated as JS)
"noOverwrite":true // if supplied, this file will not be overwritten if it
// already exists
},
]
"data": [ // list of files the app writes to
{"name":"appid.data.json", // filename used in storage
"storageFile":true // if supplied, file is treated as storageFile
"url":"", // if supplied URL of file to load (currently relative to apps/)
"content":"...", // if supplied, this content is loaded directly
"evaluate":true, // if supplied, data isn't quoted into a String before upload
// (eg it's evaluated as JS)
},
{"wildcard":"appid.data.*" // wildcard of filenames used in storage
}, // this is mutually exclusive with using "name"
@ -439,13 +445,16 @@ The screen is parted in a widget and app area for lcd mode `direct`(default).
| areas | as rectangle or point |
| :-:| :-: |
| Widget | (0,0,239,23) |
| Apps | (0,24,239,239) |
| Widget bottom bar (optional) | (0,216,239,239) |
| Apps | (0,24,239,239) (see below) |
| BTN1 | (230, 55) |
| BTN2 | (230, 140) |
| BTN3 | (230, 210) |
| BTN4 | (0,0,119, 239)|
| BTN5 | (120,0,239,239) |
- If there are widgets at the bottom of the screen, apps should actually keep the bottom 24px free, so should keep to the area (0,24,239,215)
- Use `g.setFontAlign(0, 0, 3)` to draw rotated string to BTN1-BTN3 with `g.drawString()`.
- For BTN4-5 the touch area is named

611
apps.json
View File

@ -4,11 +4,12 @@
"tags": "tool,system",
"type":"bootloader",
"icon": "bootloader.png",
"version":"0.22",
"version":"0.25",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"storage": [
{"name":".boot0","url":"boot0.js"},
{"name":".bootcde","url":"bootloader.js"}
{"name":".bootcde","url":"bootloader.js"},
{"name":"bootupdate.js","url":"bootupdate.js"}
],
"sortorder" : -10
},
@ -41,7 +42,7 @@
"name": "Launcher (Default)",
"shortName":"Launcher",
"icon": "app.png",
"version":"0.04",
"version":"0.06",
"description": "This is needed by Bangle.js to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.",
"tags": "tool,system,launcher",
"type":"launch",
@ -53,7 +54,7 @@
{ "id": "about",
"name": "About",
"icon": "app.png",
"version":"0.07",
"version":"0.08",
"description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers",
"tags": "tool,system",
"allow_emulator":true,
@ -65,7 +66,7 @@
{ "id": "locale",
"name": "Languages",
"icon": "locale.png",
"version":"0.08",
"version":"0.09",
"description": "Translations for different countries",
"tags": "tool,system,locale,translate",
"type": "locale",
@ -80,7 +81,7 @@
"name": "Notifications (default)",
"shortName":"Notifications",
"icon": "notify.png",
"version":"0.05",
"version":"0.08",
"description": "A handler for displaying notifications that displays them in a bar at the top of the screen",
"tags": "widget",
"type": "notify",
@ -93,7 +94,7 @@
"name": "Fullscreen Notifications",
"shortName":"Notifications",
"icon": "notify.png",
"version":"0.06",
"version":"0.08",
"description": "A handler for displaying notifications that displays them fullscreen. This may not fully restore the screen after on some apps. See `Notifications (default)` for more information about the notifications library.",
"tags": "widget",
"type": "notify",
@ -104,7 +105,7 @@
{ "id": "welcome",
"name": "Welcome",
"icon": "app.png",
"version":"0.09",
"version":"0.10",
"description": "Appears at first boot and explains how to use Bangle.js",
"tags": "start,welcome",
"allow_emulator":true,
@ -139,7 +140,7 @@
{ "id": "gbridge",
"name": "Gadgetbridge",
"icon": "app.png",
"version":"0.18",
"version":"0.22",
"description": "The default notification handler for Gadgetbridge notifications from Android",
"tags": "tool,system,android,widget",
"readme": "README.md",
@ -171,22 +172,24 @@
{ "id": "setting",
"name": "Settings",
"icon": "settings.png",
"version":"0.23",
"version":"0.26",
"description": "A menu for setting up Bangle.js",
"tags": "tool,system",
"readme": "README.md",
"storage": [
{"name":"setting.app.js","url":"settings.js"},
{"name":"setting.boot.js","url":"boot.js"},
{"name":"setting.img","url":"settings-icon.js","evaluate":true}
],
"data": [
{"name":"setting.json", "url":"settings.min.json","evaluate":true}
],
"sortorder" : -2
},
{ "id": "alarm",
"name": "Default Alarm",
"shortName":"Alarms",
"icon": "app.png",
"version":"0.10",
"version":"0.11",
"description": "Set and respond to alarms",
"tags": "tool,alarm,widget",
"storage": [
@ -213,6 +216,69 @@
{"name":"wclock.img","url":"clock-word-icon.js","evaluate":true}
]
},
{ "id": "fontclock",
"name": "Font Clock",
"icon": "fontclock.png",
"version":"0.01",
"description": "Choose the font and design of clock face from a library of available designs",
"tags": "clock",
"type":"clock",
"allow_emulator":false,
"readme": "README.md",
"custom":"custom.html",
"storage": [
{"name":"fontclock.app.js","url":"fontclock.js"},
{"name":"fontclock.img","url":"fontclock-icon.js","evaluate":true},
{"name":"fontclock.hand.js","url":"fontclock.hand.js"},
{"name":"fontclock.thinhand.js","url":"fontclock.thinhand.js"},
{"name":"fontclock.thickhand.js","url":"fontclock.thickhand.js"},
{"name":"fontclock.hourscriber.js","url":"fontclock.hourscriber.js"},
{"name":"fontclock.font.js","url":"fontclock.font.js"},
{"name":"fontclock.font.abril_ff50.js","url":"fontclock.font.abril_ff50.js"},
{"name":"fontclock.font.cpstc58.js","url":"fontclock.font.cpstc58.js"},
{"name":"fontclock.font.mntn25.js","url":"fontclock.font.mntn25.js"},
{"name":"fontclock.font.mntn50.js","url":"fontclock.font.mntn50.js"},
{"name":"fontclock.font.vector25.js","url":"fontclock.font.vector25.js"},
{"name":"fontclock.font.vector50.js","url":"fontclock.font.vector50.js"}
]
},
{ "id": "slidingtext",
"name": "Sliding Clock",
"icon": "slidingtext.png",
"version":"0.05",
"description": "Inspired by the Pebble sliding clock, old times are scrolled off the screen and new times on. You are also able to change language on the fly so you can see the time written in other languages using button 1. Currently English, French, Japanese, Spanish and German are supported",
"tags": "clock",
"type":"clock",
"allow_emulator":false,
"readme": "README.md",
"custom":"custom.html",
"storage": [
{"name":"slidingtext.app.js","url":"slidingtext.js"},
{"name":"slidingtext.img","url":"slidingtext-icon.js","evaluate":true},
{"name":"slidingtext.locale.en.js","url":"slidingtext.locale.en.js"},
{"name":"slidingtext.locale.en2.js","url":"slidingtext.locale.en2.js"},
{"name":"slidingtext.utils.en.js","url":"slidingtext.utils.en.js"},
{"name":"slidingtext.locale.es.js","url":"slidingtext.locale.es.js"},
{"name":"slidingtext.locale.fr.js","url":"slidingtext.locale.fr.js"},
{"name":"slidingtext.locale.jp.js","url":"slidingtext.locale.jp.js"},
{"name":"slidingtext.locale.de.js","url":"slidingtext.locale.de.js"},
{"name":"slidingtext.dtfmt.js","url":"slidingtext.dtfmt.js"}
]
},
{ "id": "sweepclock",
"name": "Sweep Clock",
"icon": "sweepclock.png",
"version":"0.04",
"description": "Smooth sweep secondhand with single hour numeral. Use button 1 to toggle the numeral font, button 3 to change the colour theme and button 4 to change the date placement",
"tags": "clock",
"type":"clock",
"allow_emulator":true,
"readme": "README.md",
"storage": [
{"name":"sweepclock.app.js","url":"sweepclock.js"},
{"name":"sweepclock.img","url":"sweepclock-icon.js","evaluate":true}
]
},
{ "id": "imgclock",
"name": "Image background clock",
"shortName":"Image Clock",
@ -246,7 +312,7 @@
{ "id": "aclock",
"name": "Analog Clock",
"icon": "clock-analog.png",
"version": "0.13",
"version": "0.14",
"description": "An Analog Clock",
"tags": "clock",
"type":"clock",
@ -269,6 +335,30 @@
{"name":"clock2x3.img","url":"clock2x3-icon.js","evaluate":true}
]
},
{ "id": "geissclk",
"name": "Geiss Clock",
"icon": "clock.png",
"version":"0.02",
"description": "7 segment clock with animated background in the style of Ryan Geiss' music visualisation. NOTE: The first run will take ~1 minute to do some precalculation",
"tags": "clock",
"type":"clock",
"storage": [
{"name":"geissclk.app.js","url":"clock.js"},
{"name":"geissclk.precompute.js","url":"precompute.js"},
{"name":"geissclk.img","url":"clock-icon.js","evaluate":true}
],
"data": [
{"name":"geissclk.0.map"},
{"name":"geissclk.1.map"},
{"name":"geissclk.2.map"},
{"name":"geissclk.3.map"},
{"name":"geissclk.4.map"},
{"name":"geissclk.5.map"},
{"name":"geissclk.0.pal"},
{"name":"geissclk.1.pal"},
{"name":"geissclk.2.pal"}
]
},
{ "id": "trex",
"name": "T-Rex",
"icon": "trex.png",
@ -363,7 +453,7 @@
{ "id": "gpsrec",
"name": "GPS Recorder",
"icon": "app.png",
"version":"0.17",
"version":"0.19",
"interface": "interface.html",
"description": "Application that allows you to record a GPS track. Can run in background",
"tags": "tool,outdoors,gps,widget",
@ -388,14 +478,16 @@
"interface":"waypoints.html",
"storage": [
{"name":"gpsnav.app.js","url":"app.min.js"},
{"name":"waypoints.json","url":"waypoints.json","evaluate":false},
{"name":"gpsnav.img","url":"app-icon.js","evaluate":true}
],
"data": [
{"name":"waypoints.json","url":"waypoints.json"}
]
},
{ "id": "heart",
"name": "Heart Rate Recorder",
"icon": "app.png",
"version":"0.02",
"version":"0.05",
"interface": "interface.html",
"description": "Application that allows you to record your heart rate. Can run in background",
"tags": "tool,health,widget",
@ -479,7 +571,7 @@
{ "id": "widbat",
"name": "Battery Level Widget",
"icon": "widget.png",
"version":"0.05",
"version":"0.06",
"description": "Show the current battery level and charging status in the top right of the clock",
"tags": "widget,battery",
"type":"widget",
@ -487,6 +579,17 @@
{"name":"widbat.wid.js","url":"widget.js"}
]
},
{ "id": "widlock",
"name": "Lock Widget",
"icon": "widget.png",
"version":"0.01",
"description": "On devices with always-on display (Bangle.js 2) this displays lock icon whenever the display is locked",
"tags": "widget,lock",
"type":"widget",
"storage": [
{"name":"widlock.wid.js","url":"widget.js"}
]
},
{ "id": "widbatpc",
"name": "Battery Level Widget (with percentage)",
"shortName": "Battery Widget",
@ -508,7 +611,7 @@
"shortName": "Battery Warning",
"icon": "widget.png",
"readme": "README.md",
"version":"0.01",
"version":"0.02",
"description": "Show a warning when the battery runs low.",
"tags": "tool,battery",
"type":"widget",
@ -532,6 +635,21 @@
{"name":"widbt.wid.js","url":"widget.js"}
]
},
{ "id": "widchime",
"name": "Hour Chime",
"icon": "widget.png",
"version":"0.02",
"description": "Buzz or beep on every whole hour.",
"tags": "widget",
"type": "widget",
"storage": [
{"name":"widchime.wid.js","url":"widget.js"},
{"name":"widchime.settings.js","url":"settings.js"}
],
"data": [
{"name":"widchime.json"}
]
},
{ "id": "widram",
"name": "RAM Widget",
"shortName":"RAM Widget",
@ -547,7 +665,7 @@
{ "id": "hrm",
"name": "Heart Rate Monitor",
"icon": "heartrate.png",
"version":"0.02",
"version":"0.04",
"description": "Measure your heart rate and see live sensor data",
"tags": "health",
"storage": [
@ -673,7 +791,7 @@
{ "id": "route",
"name": "Route Viewer",
"icon": "app.png",
"version":"0.01",
"version":"0.02",
"description": "Upload a KML file of a route, and have your watch display a map with how far around it you are",
"tags": "",
"custom": "custom.html",
@ -739,10 +857,23 @@
{"name":"sclock.img","url":"clock-simple-icon.js","evaluate":true}
]
},
{ "id": "s7clk",
"name": "Simple 7 segment Clock",
"icon": "icon.png",
"version":"0.02",
"description": "A simple 7 segment Clock with date",
"tags": "clock",
"type":"clock",
"allow_emulator":true,
"storage": [
{"name":"s7clk.app.js","url":"app.js"},
{"name":"s7clk.img","url":"icon.js","evaluate":true}
]
},
{ "id": "vibrclock",
"name": "Vibrate Clock",
"icon": "app.png",
"version":"0.01",
"version":"0.02",
"description": "When BTN1 is pressed, vibrate out the time as a series of buzzes, one digit at a time. Hours, then Minutes. Zero is signified by one long buzz. Otherwise a simple digital clock.",
"tags": "clock",
"type":"clock",
@ -970,7 +1101,7 @@
"name": "Large Digit Blob Clock",
"shortName" : "Blob Clock",
"icon": "clock-blob.png",
"version":"0.04",
"version":"0.05",
"description": "A clock with big digits",
"tags": "clock",
"type":"clock",
@ -1007,7 +1138,7 @@
{ "id": "widpedom",
"name": "Pedometer widget",
"icon": "widget.png",
"version":"0.11",
"version":"0.13",
"description": "Daily pedometer widget",
"tags": "widget",
"type":"widget",
@ -1138,7 +1269,7 @@
{ "id": "marioclock",
"name": "Mario Clock",
"icon": "marioclock.png",
"version":"0.14",
"version":"0.15",
"description": "Animated retro Mario clock, with Gameboy style 8-bit grey-scale graphics.",
"tags": "clock,mario,retro",
"type": "clock",
@ -1153,7 +1284,7 @@
"name": "Commandline-Clock",
"shortName":"CLI-Clock",
"icon": "app.png",
"version":"0.11",
"version":"0.12",
"description": "Simple CLI-Styled Clock",
"tags": "clock,cli,command,bash,shell",
"type":"clock",
@ -1190,7 +1321,7 @@
{ "id": "dotclock",
"name": "Dot Clock",
"icon": "clock-dot.png",
"version":"0.01",
"version":"0.02",
"description": "A Minimal Dot Analog Clock",
"tags": "clock",
"type":"clock",
@ -1492,7 +1623,7 @@
"name": "Digital Assistant, not EDITH",
"shortName": "DANE",
"icon": "app.png",
"version": "0.15",
"version": "0.16",
"description": "A Watchface inspired by Tony Stark's EDITH and based on https://arwes.dev/",
"tags": "clock",
"type": "clock",
@ -1553,7 +1684,7 @@
"name": "BangleRun",
"shortName": "BangleRun",
"icon": "banglerun.png",
"version": "0.09",
"version": "0.10",
"interface": "interface.html",
"description": "An app for running sessions. Displays info and logs your run for later viewing.",
"tags": "run,running,fitness,outdoors",
@ -1636,9 +1767,9 @@
{
"id": "rclock",
"name": "Round clock with seconds, minutes and date",
"shortName":"Round Clock",
"shortName": "Round Clock",
"icon": "app.png",
"version":"0.03",
"version": "0.05",
"description": "Designed round clock with ticks for minutes and seconds and heart rate indication",
"tags": "clock",
"type": "clock",
@ -1647,6 +1778,20 @@
{"name":"rclock.img","url":"app-icon.js","evaluate":true}
]
},
{
"id": "fclock",
"name": "fclock",
"shortName": "F Clock",
"icon": "app.png",
"version": "0.01",
"description": "Simple design of a digital clock",
"tags": "clock",
"type": "clock",
"storage": [
{"name":"fclock.app.js","url":"fclock.app.js"},
{"name":"fclock.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "hamloc",
"name": "QTH Locator / Maidenhead Locator System",
"shortName": "QTH Locator",
@ -1835,7 +1980,7 @@
"id": "beebclock",
"name": "Beeb Clock",
"icon": "beebclock.png",
"version":"0.02",
"version":"0.03",
"description": "Clock face that may be coincidentally familiar to BBC viewers",
"tags": "clock",
"type": "clock",
@ -2035,7 +2180,7 @@
"name": "SleepPhaseAlarm",
"shortName":"SleepPhaseAlarm",
"icon": "app.png",
"version":"0.01",
"version":"0.02",
"description": "Uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments (ESS, see https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en). This app will read the next alarm from the alarm application and will wake you up to 30 minutes early at the best guessed time when you are almost already awake.",
"tags": "alarm",
"storage": [
@ -2139,7 +2284,7 @@
{ "id": "multiclock",
"name": "Multi Clock",
"icon": "multiclock.png",
"version":"0.11",
"version":"0.13",
"description": "Clock with multiple faces - Big, Analogue, Digital, Text, Time-Date.\n Switch between faces with BTN1 & BTN3",
"readme": "README.md",
"tags": "clock",
@ -2153,8 +2298,6 @@
{"name":"txt.face.js","url":"txt.js"},
{"name":"timdat.face.js","url":"timdat.js"},
{"name":"ped.face.js","url":"ped.js"},
{"name":"gps.face.js","url":"gps.js"},
{"name":"osref.face.js","url":"osref.js"},
{"name":"multiclock.img","url":"multiclock-icon.js","evaluate":true}
]
},
@ -2162,7 +2305,7 @@
"name": "Apple Notification Widget",
"shortName":"ANCS Widget",
"icon": "widget.png",
"version":"0.06",
"version":"0.07",
"description": "Displays call, message etc notifications from a paired iPhone. Read README before installation as it only works with compatible apps",
"readme": "README.md",
"tags": "widget",
@ -2259,7 +2402,7 @@
{"id": "counter",
"name": "Counter",
"icon": "counter_icon.png",
"version": "0.02",
"version": "0.03",
"description": "Simple counter",
"tags": "tool",
"allow_emulator": true,
@ -2336,9 +2479,11 @@
"readme": "README.md",
"storage": [
{"name":"worldclock.app.js","url":"app.js"},
{"name":"worldclock.settings.json"},
{"name":"worldclock.img","url":"worldclock-icon.js","evaluate":true}
]
],
"data": [
{"name":"worldclock.settings.json"}
]
},
{ "id": "digiclock",
"name": "Digital Clock Face",
@ -2530,7 +2675,7 @@
"name": "Hard Alarm",
"shortName":"HardAlarm",
"icon": "app.png",
"version":"0.01",
"version":"0.02",
"description": "Make sure you wake up! Count to the right number to turn off the alarm",
"tags": "tool,alarm,widget",
"storage": [
@ -2581,14 +2726,16 @@
"readme": "README.md",
"storage": [
{"name":"breath.app.js","url":"app.js"},
{"name":"breath.settings.json","url":"settings.json"},
{"name":"breath.img","url":"app-icon.js","evaluate":true}
],
"data": [
{"name":"breath.settings.json","url":"settings.json"}
]
},
{ "id": "lazyclock",
"name": "Lazy Clock",
"icon": "lazyclock.png",
"version":"0.01",
"version":"0.02",
"readme": "README.md",
"description": "Tells the time, roughly",
"tags": "clock",
@ -2639,11 +2786,11 @@
]
},
{ "id": "speedalt",
"name": "GPS Speedo and Altimeter",
"shortName":"GPS Speed Alt",
"name": "GPS Adventure Sports",
"shortName":"GPS Adv Sport",
"icon": "app.png",
"version":"0.07",
"description": "GPS speed and altitude display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.",
"version":"1.02",
"description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.",
"tags": "tool,outdoors",
"type":"app",
"allow_emulator":true,
@ -2679,9 +2826,11 @@
"storage": [
{"name":"gpsservice.app.js","url":"app.js"},
{"name":"gpsservice.settings.js","url":"settings.js"},
{"name":"gpsservice.settings.json","url":"settings.json"},
{"name":"gpsservice.wid.js","url":"widget.js"},
{"name":"gpsservice.img","url":"gpsservice-icon.js","evaluate":true}
],
"data": [
{"name":"gpsservice.settings.json","url":"settings.json"}
]
},
{ "id": "mclockplus",
@ -2714,12 +2863,15 @@
"name": "Planetarium",
"shortName":"Planetarium",
"icon": "planetarium.png",
"version":"0.02",
"readme": "README.md",
"version":"0.03",
"description": "Planetarium showing up to 500 stars using the watch location and time",
"tags": "",
"storage": [
{"name":"planetarium.app.js","url":"planetarium.app.js"},
{"name":"planetarium.data.csv","url":"planetarium.data.csv"},
{"name":"planetarium.const.csv","url":"planetarium.const.csv"},
{"name":"planetarium.extra.csv","url":"planetarium.extra.csv"},
{"name":"planetarium.settings.js","url":"settings.js"},
{"name":"planetarium.img","url":"planetarium-icon.js","evaluate":true}
],
@ -2730,14 +2882,14 @@
{ "id": "tapelauncher",
"name": "Tape Launcher",
"icon": "icon.png",
"version":"0.01",
"version":"0.02",
"description": "An App launcher, icons displayed in a horizontal tape, swipe or use buttons",
"readme": "README.md",
"tags": "tool,system,launcher",
"type":"launch",
"storage": [
{"name":"tapelaunch.app.js","url":"app.js"},
{"name":"tapelaunch.img","url":"icon.js","evaluate":true}
{"name":"tapelauncher.app.js","url":"app.js"},
{"name":"tapelauncher.img","url":"icon.js","evaluate":true}
]
},
{ "id": "oblique",
@ -2750,6 +2902,361 @@
{"name":"oblique.app.js","url":"app.js"},
{"name":"oblique.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "testuserinput",
"name": "Test User Input",
"shortName":"Test User Input",
"icon": "app.png",
"version":"0.06",
"description": "App to test the bangle.js input interface. It displays the user action in text, circle buttons or on/off switch UI elements.",
"readme": "README.md",
"tags": "input,interface,buttons,touch,UI",
"storage": [
{"name":"testuserinput.app.js","url":"app.js"},
{"name":"testuserinput.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "gpssetup",
"name": "GPS Setup",
"shortName":"GPS Setup",
"icon": "gpssetup.png",
"version":"0.02",
"description": "Configure the GPS power options and store them in the GPS nvram",
"tags": "gps, tools, outdoors",
"readme": "README.md",
"storage": [
{"name":"gpssetup","url":"gpssetup.js"},
{"name":"gpssetup.settings.js","url":"settings.js"},
{"name":"gpssetup.app.js","url":"app.js"},
{"name":"gpssetup.img","url":"icon.js","evaluate":true}
],
"data": [
{"name":"gpssetup.settings.json","url":"settings.json"}
]
},
{ "id": "walkersclock",
"name": "Walkers Clock",
"shortName":"Walkers Clock",
"icon": "walkersclock48.png",
"version":"0.04",
"description": "A large font watch, displays steps, can switch GPS on/off, displays grid reference",
"type":"clock",
"tags": "clock, gps, tools, outdoors",
"readme": "README.md",
"storage": [
{"name":"walkersclock.app.js","url":"app.js"},
{"name":"walkersclock.img","url":"icon.js","evaluate":true}
]
},
{ "id": "widgps",
"name": "GPS Widget",
"icon": "widget.png",
"version":"0.02",
"description": "Tiny widget to show the power on/off status of the GPS. Require firmware v2.08.167 or later",
"tags": "widget,gps",
"type":"widget",
"readme": "README.md",
"storage": [
{"name":"widgps.wid.js","url":"widget.js"}
]
},
{ "id": "widhrt",
"name": "HRM Widget",
"icon": "widget.png",
"version":"0.02",
"description": "Tiny widget to show the power on/off status of the Heart Rate Monitor. Requires firmware v2.08.167 or later",
"tags": "widget, hrm",
"type":"widget",
"readme": "README.md",
"storage": [
{"name":"widhrt.wid.js","url":"widget.js"}
]
},
{ "id": "countdowntimer",
"name" : "Countdown Timer",
"icon": "countdowntimer.png",
"version": "0.01",
"description": "A simple countdown timer with a focus on usability",
"tags": "timer, tool",
"readme": "README.md",
"storage": [
{"name": "countdowntimer.app.js", "url": "countdowntimer.js"},
{"name": "countdowntimer.img", "url": "countdowntimer-icon.js", "evaluate": true}
]
},
{ "id": "helloworld",
"name": "hello, world!",
"shortName":"hello world",
"icon": "app.png",
"version":"0.02",
"description": "A cross cultural hello world!/hola mundo! app with colors and languages",
"readme": "README.md",
"tags": "input,interface,buttons,touch",
"storage": [
{"name":"helloworld.app.js","url":"app.js"},
{"name":"helloworld.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "widcom",
"name": "Compass Widget",
"icon": "widget.png",
"version":"0.01",
"description": "Tiny widget to show the power on/off status of the Compass. Requires firmware v2.08.167 or later",
"tags": "widget, compass",
"type":"widget",
"readme": "README.md",
"storage": [
{"name":"widcom.wid.js","url":"widget.js"}
]
},
{ "id": "arrow",
"name": "Arrow Compass",
"icon": "arrow.png",
"type":"app",
"version":"0.04",
"description": "Moving arrow compass that points North, shows heading, with tilt correction. Based on jeffmer's Navigation Compass",
"tags": "tool,outdoors",
"readme": "README.md",
"storage": [
{"name":"arrow.app.js","url":"app.js"},
{"name":"arrow.img","url":"icon.js","evaluate":true}
]
},
{ "id": "waypointer",
"name": "Way Pointer",
"icon": "waypointer.png",
"version":"0.01",
"description": "Navigate to a waypoint using the GPS for bearing and compass to point way, uses the same waypoint interface as GPS Navigation",
"tags": "tool,outdoors,gps",
"readme": "README.md",
"interface":"waypoints.html",
"storage": [
{"name":"waypointer.app.js","url":"app.js"},
{"name":"waypointer.img","url":"icon.js","evaluate":true}
],
"data": [
{"name":"waypoints.json","url":"waypoints.json"}
]
},
{ "id": "color_catalog",
"name": "Colors Catalog",
"shortName":"Colors Catalog",
"icon": "app.png",
"version":"0.01",
"description": "Displays RGB565 and RGB888 colors, its name and code in screen.",
"readme": "README.md",
"tags": "Color, input,buttons,touch,UI",
"storage": [
{"name":"color_catalog.app.js","url":"app.js"},
{"name":"color_catalog.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "UI4swatch",
"name": "UI 4 swatch",
"shortName":"UI 4 swatch",
"icon": "app.png",
"version":"0.01",
"description": "A UI/UX for espruino smartwatches, displays dinamically calc. x,y coordinates.",
"readme": "README.md",
"tags": "Color, input,buttons,touch,UI",
"storage": [
{"name":"UI4swatch.app.js","url":"app.js"},
{"name":"UI4swatch.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "simplest",
"name": "Simplest Clock",
"icon": "simplest.png",
"version":"0.01",
"description": "The simplest working clock, acts as a tutorial piece",
"tags": "clock",
"type":"clock",
"readme": "README.md",
"storage": [
{"name":"simplest.app.js","url":"app.js"},
{"name":"simplest.img","url":"icon.js","evaluate":true}
]
},
{ "id": "stepo",
"name": "Stepometer Clock",
"icon": "stepo.png",
"version":"0.03",
"description": "A large font watch, displays step count in a doughnut guage and warns of low battery, requires one of the steps widgets to be installed",
"tags": "clock",
"type":"clock",
"readme": "README.md",
"storage": [
{"name":"stepo.app.js","url":"app.js"},
{"name":"stepo.img","url":"icon.js","evaluate":true}
]
},
{ "id": "gbmusic",
"name": "Gadgetbridge Music Controls",
"shortName":"Music Controls",
"icon": "icon.png",
"version":"0.05",
"description": "Control the music on your Gadgetbridge-connected phone",
"tags": "tools,bluetooth,gadgetbridge,music",
"type":"app",
"allow_emulator": false,
"readme": "README.md",
"storage": [
{"name":"gbmusic.app.js","url":"app.js"},
{"name":"gbmusic.settings.js","url":"settings.js"},
{"name":"gbmusic.wid.js","url":"widget.js"},
{"name":"gbmusic.img","url":"icon.js","evaluate":true}
],
"data": [
{"name":"gbmusic.json"},
{"name":"gbmusic.load.json"}
]
},
{
"id": "battleship",
"name":"Battleship",
"icon":"battleship-icon.png",
"version": "0.01",
"readme": "README.md",
"description": "The classic game of battleship",
"tags": "game",
"allow_emulator": true,
"storage": [
{
"name": "battleship.app.js",
"url": "battleship.js"
},
{
"name": "battleship.img",
"url": "battleship-icon.js",
"evaluate": true
}
]
},
{ "id": "kitchen",
"name": "Kitchen Combo",
"icon": "kitchen.png",
"version":"0.10",
"description": "Combination of the Stepo, Walkersclock, Arrow and Waypointer apps into a multiclock format. 'Everything but the kitchen sink'. Requires firmware v2.08.167 or later",
"tags": "tool,outdoors,gps",
"type":"clock",
"readme": "README.md",
"interface":"waypoints.html",
"storage": [
{"name":"kitchen.app.js","url":"kitchen.app.js"},
{"name":"stepo.kit.js","url":"stepo.kit.js"},
{"name":"gps.kit.js","url":"gps.kit.js"},
{"name":"digi.kit.js","url":"digi.kit.js"},
{"name":"heart.kit.js","url":"heart.kit.js"},
{"name":"swatch.kit.js","url":"swatch.kit.js"},
{"name":"compass.kit.js","url":"compass.kit.js"},
{"name":"kitchen.img","url":"kitchen.icon.js","evaluate":true}
],
"data": [
{"name":"waypoints.json","url":"waypoints.json"}
]
},
{ "id": "qmsched",
"name": "Quiet Mode Schedule",
"shortName":"Quiet Mode",
"icon": "app.png",
"version":"0.01",
"description": "Automatically turn Quiet Mode on or off at set times",
"readme": "README.md",
"tags": "tool",
"storage": [
{"name":"qmsched","url":"lib.js"},
{"name":"qmsched.app.js","url":"app.js"},
{"name":"qmsched.boot.js","url":"boot.js"},
{"name":"qmsched.img","url":"icon.js","evaluate":true}
],
"data": [
{"name":"qmsched.json"}
]
},
{
"id": "hourstrike",
"name": "Hour Strike",
"shortName": "Hour Strike",
"icon": "app-icon.png",
"version": "0.07",
"description": "Strike the clock on the hour. A great tool to remind you an hour has passed!",
"tags": "tool,alarm",
"readme": "README.md",
"storage": [
{"name":"hourstrike.app.js","url":"app.js"},
{"name":"hourstrike.boot.js","url":"boot.js"},
{"name":"hourstrike.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "whereworld",
"name": "Where in the World?",
"shortName" : "Where World",
"icon": "app.png",
"version": "0.01",
"description": "Shows your current location on the world map",
"tags": "gps",
"storage": [
{"name":"whereworld.app.js","url":"app.js"},
{"name":"whereworld.img","url":"app-icon.js","evaluate":true},
{"name":"whereworld.worldmap","url":"worldmap"}
]
},
{
"id": "omnitrix",
"name":"Omnitrix",
"icon":"omnitrix.png",
"version": "0.01",
"readme": "README.md",
"description": "An Omnitrix Showpiece",
"tags": "game",
"readme": "README.md",
"storage": [
{"name":"omnitrix.app.js","url":"omnitrix.app.js"},
{"name":"omnitrix.img","url":"omnitrix.icon.js","evaluate":true}
]
},
{ "id": "batclock",
"name": "Bat Clock",
"shortName":"Bat Clock",
"icon": "bat-clock.png",
"version":"0.01",
"description": "Morphing Clock, with an awesome \"The Dark Knight\" themed logo.",
"tags": "clock",
"type": "clock",
"readme": "README.md",
"storage": [
{"name":"batclock.app.js","url":"bat-clock.app.js"},
{"name":"batclock.img","url":"bat-clock.icon.js","evaluate":true}
]
},
{ "id":"doztime",
"name":"Dozenal Time",
"shortName":"Dozenal Time",
"icon":"app.png",
"version":"0.01",
"description":"A dozenal Holocene calendar and dozenal diurnal clock",
"tags":"clock",
"type":"clock",
"allow_emulator":true,
"readme": "README.md",
"storage": [
{"name":"doztime.app.js","url":"app.js"},
{"name":"doztime.img","url":"app-icon.js","evaluate":true}
]
},
{ "id":"gbtwist",
"name":"Gadgetbridge Twist Control",
"shortName":"Twist Control",
"icon":"app.png",
"version":"0.01",
"description":"Shake your wrist to control your music app via Gadgetbridge",
"tags":"tools,bluetooth,gadgetbridge,music",
"type":"app",
"allow_emulator":false,
"readme": "README.md",
"storage": [
{"name":"gbtwist.app.js","url":"app.js"},
{"name":"gbtwist.img","url":"app-icon.js","evaluate":true}
]
}
]

1
apps/UI4swatch/Changelog Normal file
View File

@ -0,0 +1 @@
0.01: 1st ver, defining a common UI/UX

38
apps/UI4swatch/README.md Normal file
View File

@ -0,0 +1,38 @@
# UI/UX for Espruino Smartwatches
This is a very basic app that defines a common UI/UX for espruino smartwatchesm and specifically for the *bangle.js*, also it displays dinamically calculated x,y position coordinates and screen areas based in detected smartwatch models.
Launcher icon
![](UI4swatch_icon.png)
1st screen - Main page
![](UI4swatch_s1.png)
## Usage
Open and see x,y coordinates for areas
Interact with buttons or touch screen to print the event or leave the app
## Features
Colours, font, user input,, load widgets
## Controls
Press left area - Prints Touch1
Press righ area - Prints Touch2
Press center area - Prints Touch3
Swipe Left - Prints <--
Swipe Right - Prints -->
BTN1 - Prints Button1
BTN2 - Prints Button2
BTN3 - Quit to Launcher
## Support
This app is so basic that probably the easiest is to just edit the code
Otherwise you can contact me [here](https://github.com/dapgo)

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AEMimYASkQXBkYXTmQXzn//mYBBn/zC6nzC6wvTR/4XG/4ASC/4XlQgwOCkQAEdAIXEdI4OBBIwvGC5JHNC/J3XL/53/C/4JGO/533L4wAJC65HNC9rKFAB4XBAH4AeA="))

196
apps/UI4swatch/app.js Normal file
View File

@ -0,0 +1,196 @@
/* UI/UX for Espruino and Bangle.js
Testing USer interface and User usability
v20200307
identify device and dimensions
max printable position max_x-1 i.e 239
*/
var colbackg='#111111';//black
var colorange='#e56e06'; //RGB format rrggbb
var v_color_lines=0xFFFF; //White hex format
var v_color_b_area='#111111';
var v_color_text=0x07E0;//'#FB0E01';//red
var v_font1size=10; //out of quotes
var v_font2size=12;
var v_font_banner_size=30;
var v_clicks=0;
//pend identify widget area dinamically
var v_model=process.env.BOARD;
console.log("device="+v_model);
var x_max_screen=g.getWidth();//240;
var y_max_screen=g.getHeight(); //240;
var y_wg_bottom=g.getHeight()-25;
var y_wg_top=25;
if (v_model=='BANGLEJS') {
var x_btn_area=215;
var x_max_usable_area=x_btn_area;//Pend! only for bangle.js
var y_btn2=124; //harcoded for bangle.js cuz it is not the half of display height
} else x_max_usable_area=240;
var x_mid_screen=x_max_screen/2;
//PEND comment
console.log("*** UI dimensions***");
console.log("x="+x_max_screen);
console.log("y_wg_bottom="+y_wg_bottom);
//the biggest usable area, button area not included
function ClearActiveArea(){
g.setColor(v_color_b_area);
g.fillRect(0,y_wg_top,x_max_usable_area,y_wg_bottom); //fill all screen except widget areas
g.flip();
}
//Clean fill top area with a color
function ClearBannerArea(){
g.setColor(v_color_b_area);
g.fillRect(55,28,185,60);
g.flip();
}
function PrintUserInput(boton){
//var v_font_banner_size=30;//now global size= almost px height
console.log("Pressed touch/BTN",boton);
ClearBannerArea();
g.setColor(colorange);
//print in banner area
g.setFontVector(v_font_banner_size).drawString(boton, 63, 29);
g.flip();
}
function PrintBtn1(boton){
console.log("Pressed BTN1");
PrintUserInput("Button1");
}
function PrintBtn2(boton){
console.log("Pressed BTN2");
PrintUserInput("Button2");
}
function DrawBangleButtons(){
g.setFontVector(v_font1size);
g.setColor(v_color_lines);//White
//line-separation with buttons area
g.drawLine(x_btn_area, 35, x_btn_area, 180);//vline right sep buttons
//x for Border position in 2 lines
g.drawString("x=",x_max_screen-g.stringWidth("x= "),68);
g.drawString(x_max_screen,x_max_screen-g.stringWidth("3ch"),78);
g.drawString("Dwn", x_max_screen-g.stringWidth("Dwn"),y_wg_top+v_font1size+1);
//above Btn2
//g.setFontVector(v_font1size).drawString("Off", x_max_screen-g.stringWidth("Off"),y_btn2-(2*v_font1size));
g.drawString("Set", x_max_screen-g.stringWidth("Set"),y_btn2-v_font1size);
//above Btn3
g.drawString("Quit", x_max_screen-g.stringWidth("Quit"),y_wg_bottom-(2*v_font1size));
g.flip();
g.setColor(v_color_text); //green
g.setFontVector(v_font1size);
g.drawString("B1", x_max_screen-g.stringWidth("B1"),y_wg_top);
g.drawString("B2", x_max_screen-g.stringWidth("B2"),y_btn2);
g.drawString("B3",x_max_screen-g.stringWidth("B3"),y_wg_bottom-v_font1size);
g.flip();
}
function DrawBottomInfoBanner(){
/* External Vars:v_color_text,v_font2size,x_max_usable_area,y_wg_bottom
*/
g.setColor(v_color_text);
var info_text1="Swipe: Next/Back screen";
var info_text2="Touch: Left=Up Right=Down";
//aligned left of max usable area
g.setFontVector(v_font2size);
g.drawString(info_text2, x_max_usable_area-g.stringWidth(info_text2)-2 ,y_wg_bottom-(2*v_font2size));
g.drawString(info_text1, x_max_usable_area-g.stringWidth(info_text1)-2 ,y_wg_bottom-v_font2size);
g.flip();
}
function PrintAreas(){
console.log("********************************");
console.log("Log: *** Print Areas in screen");
ClearActiveArea();
g.setColor(v_color_lines);
// **** Borders and Separation Lines for areas
g.drawLine(1, 35, 1, 180);//line for left border
//
g.drawLine(x_max_screen-1, 50, x_max_screen-1, 75);//vlide right border
g.drawLine(x_mid_screen, 80, x_mid_screen, 105);//vline middle separation part1 up
g.setFontVector(v_font2size).drawString("Output area for "+v_model,(x_max_usable_area-g.stringWidth("Output area for "+v_model))/2,67);
g.setFontVector(v_font2size).drawString("x="+x_mid_screen,x_mid_screen-g.stringWidth("x=xxx"),85);
g.drawLine(x_mid_screen, 140, x_mid_screen, 180);//vline middle separation part2 down
//y=26 after widget line y=215 below widget line
if (v_model=='BANGLEJS') DrawBangleButtons();
g.setFontVector(v_font2size);
g.setColor(v_color_text);
g.drawString("Touch", 80,110);
g.drawString("Middle area", 80,125);
g.drawString("Left area", 15, 145);
g.drawString("Right area", 140,145);
g.flip();
DrawBottomInfoBanner();
}
function UserInput(){
Bangle.on('touch', function(button){
switch(button){
case 1:
PrintUserInput("Touch 1");//left
break;
case 2:
PrintUserInput("Touch 2");//right
break;
case 3:
PrintUserInput("Touch 3");//center 1+2
break;
}
});
if (v_model=='BANGLEJS') {
//only the name of the function
setWatch(PrintBtn1, BTN1, { repeat: true });
setWatch(PrintBtn2, BTN2, { repeat: true });
setWatch(Bangle.showLauncher, BTN3, { repeat: true });
}
Bangle.on('swipe', dir => {
if(dir == 1) PrintUserInput(" --->");
else PrintUserInput(" <---");
});
console.log("Log: Input conditions loaded");
} //end of UserInput
//Main code
ClearActiveArea();
PrintAreas();
Bangle.loadWidgets();
Bangle.drawWidgets();
//optional lines below and above both widget areas
g.setColor(v_color_lines);
//line-separation with top widget area
g.drawLine(60, y_wg_top, 180, y_wg_top);
g.setFontVector(v_font2size).drawString("y="+y_wg_top,10,y_wg_top+1);
//line-separation with bottom widget area
g.drawLine(60, y_wg_bottom, 180, y_wg_bottom);
g.setFontVector(v_font2size).drawString("y="+y_wg_bottom,10,y_wg_bottom-v_font2size);
g.flip();
//end optional
UserInput();

BIN
apps/UI4swatch/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 B

View File

@ -5,3 +5,4 @@
0.05: Actual pixels as of 27 Apr 2020
0.06: Actual pixels as of 12 Jun 2020
0.07: Pressing a button now exits immediately (fix #618)
0.08: Make about (mostly) work on non-240px screens

File diff suppressed because one or more lines are too long

View File

@ -8,3 +8,4 @@
0.11: shift face down for widget area, maximize face size, 0 pad single digit date, use locale for date
0.12: Fix regression after 0.11
0.13: Fix broken date padding (fix #376)
0.14: Remove hardcoded hour buzz (you can install widchime if you miss it)

View File

@ -113,9 +113,6 @@ const onMinute = () => {
g.setColor(1, 1, 0.9);
// Minute
hand((360 * currentDate.getMinutes()) / 60, -8, faceWidth - 10);
if (currentDate.getHours() >= 0 && currentDate.getMinutes() === 0) {
Bangle.buzz();
}
drawDate();
};

View File

@ -8,3 +8,4 @@
0.08: Make alarm scheduling more reliable
0.09: Add per alarm auto-snooze option
0.10: Fix auto-snooze option (this stopped new alarms being added) (fix #506)
0.11: Respect Quiet Mode

View File

@ -38,6 +38,7 @@ function showAlarm(alarm) {
load();
});
function buzz() {
if ((require('Storage').readJSON('setting.json',1)||{}).quiet>1) return; // total silence
Bangle.buzz(100).then(()=>{
setTimeout(()=>{
Bangle.buzz(100).then(function() {

4
apps/arrow/ChangeLog Normal file
View File

@ -0,0 +1,4 @@
0.01: First version
0.02: Moved arrow image load to global scope
0.03: faster drawCompass() function, does not cause buttons to become unresponsive
0.04: removed LCD1.write() as it was keeping LCD on

41
apps/arrow/README.md Normal file
View File

@ -0,0 +1,41 @@
# Arrow Compass
A variation of jeffmer's Navigation Compass. The compass points
North and shows the current heading.
This is a tilt and roll compensated compass with a linear
display. The compass will display the same direction that it shows
when flat as when it is tilted (rotation around the W-S axis) or
rolled (rotation around the N-S) axis. *Even with compensation, it
would be beyond foolish to rely solely on this app for any serious
navigational purpose.*
![](arrow_screenshot.jpg)
## Calibration
Correct operation of this app depends critically on calibration. When
first run on a Bangle, the app will request calibration. This lasts
for 30 seconds during which you should move the watch slowly through
figures of 8. It is important that during calibration the watch is
fully rotated around each of it axes. If the app does give the
correct direction heading or is not stable with respect to tilt and
roll - redo the calibration by pressing *BTN3*. Calibration data is
recorded in a storage file named `magnav.json`.
It is also worth noting that the presence of the magnetic charging
clamps will require the compass to be recalibrated after every
charge.
## Controls
*BTN1* - switches to your selected clock app.
*BTN2* - switches to the app launcher.
*BTN3* - invokes calibration ( can be cancelled if pressed accidentally)
## Acknowledgement
This app is based in the work done by [jeffmer](https://github.com/jeffmer/JeffsBangleAppsDev)

187
apps/arrow/app.js Normal file
View File

@ -0,0 +1,187 @@
var pal1color = new Uint16Array([0x0000,0xFFC0],0,1);
var pal2color = new Uint16Array([0x0000,0xffff],0,1);
var buf1 = Graphics.createArrayBuffer(128,128,1,{msb:true});
var buf2 = Graphics.createArrayBuffer(80,40,1,{msb:true});
var intervalRef;
var bearing=0; // always point north
var heading = 0;
var oldHeading = 0;
var candraw = false;
var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null;
function flip1(x,y) {
g.drawImage({width:128,height:128,bpp:1,buffer:buf1.buffer, palette:pal1color},x,y);
buf1.clear();
}
function flip2(x,y) {
g.drawImage({width:80,height:40,bpp:1,buffer:buf2.buffer, palette:pal2color},x,y);
buf2.clear();
}
function radians(d) {
return (d*Math.PI) / 180;
}
// takes 32ms
function drawCompass(hd) {
if(!candraw) return;
if (Math.abs(hd - oldHeading) < 2) return 0;
hd=hd*Math.PI/180;
var p = [0, 1.1071, Math.PI/4, 2.8198, 3.4633, 7*Math.PI/4 , 5.1760];
// using polar cordinates, 64,64 is the offset from the 0,0 origin
var poly = [
64+60*Math.sin(hd+p[0]), 64-60*Math.cos(hd+p[0]),
64+44.7214*Math.sin(hd+p[1]), 64-44.7214*Math.cos(hd+p[1]),
64+28.2843*Math.sin(hd+p[2]), 64-28.2843*Math.cos(hd+p[2]),
64+63.2455*Math.sin(hd+p[3]), 64-63.2455*Math.cos(hd+p[3]),
64+63.2455*Math.sin(hd+p[4]), 64-63.2455*Math.cos(hd+p[4]),
64+28.2843*Math.sin(hd+p[5]), 64-28.2843*Math.cos(hd+p[5]),
64+44.7214*Math.sin(hd+p[6]), 64-44.7214*Math.cos(hd+p[6])
];
buf1.fillPoly(poly);
flip1(56, 56);
}
// stops violent compass swings and wobbles, takes 3ms
function newHeading(m,h){
var s = Math.abs(m - h);
var delta = (m>h)?1:-1;
if (s>=180){s=360-s; delta = -delta;}
if (s<2) return h;
var hd = h + delta*(1 + Math.round(s/5));
if (hd<0) hd+=360;
if (hd>360)hd-= 360;
return hd;
}
// takes approx 7ms
function tiltfixread(O,S){
var start = Date.now();
var m = Bangle.getCompass();
var g = Bangle.getAccel();
m.dx =(m.x-O.x)*S.x; m.dy=(m.y-O.y)*S.y; m.dz=(m.z-O.z)*S.z;
var d = Math.atan2(-m.dx,m.dy)*180/Math.PI;
if (d<0) d+=360;
var phi = Math.atan(-g.x/-g.z);
var cosphi = Math.cos(phi), sinphi = Math.sin(phi);
var theta = Math.atan(-g.y/(-g.x*sinphi-g.z*cosphi));
var costheta = Math.cos(theta), sintheta = Math.sin(theta);
var xh = m.dy*costheta + m.dx*sinphi*sintheta + m.dz*cosphi*sintheta;
var yh = m.dz*sinphi - m.dx*cosphi;
var psi = Math.atan2(yh,xh)*180/Math.PI;
if (psi<0) psi+=360;
return psi;
}
function reading() {
var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale);
heading = newHeading(d,heading);
var dir = bearing - heading;
if (dir < 0) dir += 360;
if (dir > 360) dir -= 360;
drawCompass(dir); // we want compass to show us where to go
oldHeading = dir;
buf2.setColor(1);
buf2.setFontAlign(-1,-1);
buf2.setFont("Vector",38);
var course = Math.round(heading);
var cs = course.toString();
cs = course<10?"00"+cs : course<100 ?"0"+cs : cs;
buf2.drawString(cs,0,0);
flip2(90, 200);
}
function calibrate(){
var max={x:-32000, y:-32000, z:-32000},
min={x:32000, y:32000, z:32000};
var ref = setInterval(()=>{
var m = Bangle.getCompass();
max.x = m.x>max.x?m.x:max.x;
max.y = m.y>max.y?m.y:max.y;
max.z = m.z>max.z?m.z:max.z;
min.x = m.x<min.x?m.x:min.x;
min.y = m.y<min.y?m.y:min.y;
min.z = m.z<min.z?m.z:min.z;
}, 100);
return new Promise((resolve) => {
setTimeout(()=>{
if(ref) clearInterval(ref);
var offset = {x:(max.x+min.x)/2,y:(max.y+min.y)/2,z:(max.z+min.z)/2};
var delta = {x:(max.x-min.x)/2,y:(max.y-min.y)/2,z:(max.z-min.z)/2};
var avg = (delta.x+delta.y+delta.z)/3;
var scale = {x:avg/delta.x, y:avg/delta.y, z:avg/delta.z};
resolve({offset:offset,scale:scale});
},30000);
});
}
function docalibrate(e,first){
const title = "Calibrate";
const msg = "takes 30 seconds";
function action(b){
if (b) {
buf1.setColor(1);
buf1.setFont("Vector", 20);
buf1.setFontAlign(0,-1);
buf1.drawString("Figure 8s",64, 0);
buf1.drawString("to",64, 40);
buf1.drawString("Calibrate",64, 80);
flip1(56,56);
calibrate().then((r)=>{
require("Storage").write("magnav.json",r);
Bangle.buzz();
CALIBDATA = r;
startdraw();
setButtons();
});
} else {
startdraw();
setTimeout(setButtons,1000);
}
}
if (first===undefined) first=false;
stopdraw();
clearWatch();
if (first)
E.showAlert(msg,title).then(action.bind(null,true));
else
E.showPrompt(msg,{title:title,buttons:{"Start":true,"Cancel":false}}).then(action);
}
function startdraw(){
g.clear();
g.setColor(1,1,1);
Bangle.drawWidgets();
candraw = true;
intervalRef = setInterval(reading,500);
}
function stopdraw() {
candraw=false;
if(intervalRef) {clearInterval(intervalRef);}
}
function setButtons(){
setWatch(()=>{load();}, BTN1, {repeat:false,edge:"falling"});
setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
setWatch(docalibrate, BTN3, {repeat:false,edge:"falling"});
}
Bangle.on('lcdPower',function(on) {
if (on) {
startdraw();
} else {
stopdraw();
}
});
Bangle.on('kill',()=>{Bangle.setCompassPower(0);});
Bangle.loadWidgets();
Bangle.setCompassPower(1);
startdraw();
setButtons();

BIN
apps/arrow/arrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

1
apps/arrow/icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mUywIebg/4AocP//AAoUf//+BYgMDh/+j/8Dol/wEAgYFBg/wgEBFIV+AQIVCh4fBnwFBgISBj8AhgJCh+Ag4BB4ED8ED+ASCAYJDBnkAvkAIYIWBjw8B/EB8AcBn//gF4DwJdBAQMA/EP738FYM8g/nz+A+EPgHx8YKBgfAjF4sAKBHIItBBQJMBFoJEBHII1BIQIDCvAUCAYYUBHIIDBMIXACgQpBRAIUBMIIrBDAIWCVYaiBTYQJCn4FBQgIIBEYKrDQ4MBVYUf8CQCCoP/w6DBAAKIBAocHAoIwBBgb5DDoYAZA="))

View File

@ -7,3 +7,7 @@
0.07: Fixed GPS update, added guards against NaN values
0.08: Fix issue with GPS coordinates being wrong after the first one
0.09: Another GPS fix (log raw coordinates - not filtered ones)
0.10: Removed kalman filtering to allow distance log to work
Only log data every 5 seconds (not 1 sec)
Don't create a file until the first log entry is ready
Add labels for buttons

View File

@ -1 +1 @@
!function(){"use strict";const t={STOP:63488,PAUSE:65504,RUN:2016};function n(t,n,r){g.setColor(0),g.fillRect(n-60,r,n+60,r+30),g.setColor(65535),g.drawString(t,n,r)}function r(r){var e;g.setFontVector(30),g.setFontAlign(0,-1,0),n((r.distance/1e3).toFixed(2),60,55),n(function(t){const n=Math.round(t),r=Math.floor(n/3600),e=Math.floor(n/60)%60,o=n%60;return(r?r+":":"")+("0"+e).substr(-2)+":"+("0"+o).substr(-2)}(r.duration),180,55),n(function(t){if(t<.1667)return"__'__\"";const n=Math.round(1e3/t),r=Math.floor(n/60),e=n%60;return("0"+r).substr(-2)+"'"+("0"+e).substr(-2)+'"'}(r.speed),60,115),n(r.hr.toFixed(0),180,115),n(r.steps.toFixed(0),60,175),n(r.cadence.toFixed(0),180,175),g.setFont("6x8",2),g.setColor(r.gpsValid?2016:63488),g.fillRect(0,216,80,240),g.setColor(0),g.drawString("GPS",40,220),g.setColor(65535),g.fillRect(80,216,160,240),g.setColor(0),g.drawString(("0"+(e=new Date).getHours()).substr(-2)+":"+("0"+e.getMinutes()).substr(-2),120,220),g.setColor(t[r.status]),g.fillRect(160,216,240,240),g.setColor(0),g.drawString(r.status,200,220)}function e(t){g.clear(),g.setColor(50712),g.setFont("6x8",2),g.setFontAlign(0,-1,0),g.drawString("DIST (KM)",60,32),g.drawString("TIME",180,32),g.drawString("PACE",60,92),g.drawString("HEART",180,92),g.drawString("STEPS",60,152),g.drawString("CADENCE",180,152),r(t),Bangle.drawWidgets()}var o;function a(t){t.status===o.Stopped&&function(t){const n=(new Date).toISOString().replace(/[-:]/g,""),r=`banglerun_${n.substr(2,6)}_${n.substr(9,6)}`;t.file=require("Storage").open(r,"w"),t.file.write(["timestamp","latitude","longitude","altitude","duration","distance","heartrate","steps"].join(",")+"\n")}(t),t.status===o.Running?t.status=o.Paused:t.status=o.Running,r(t)}!function(t){t.Stopped="STOP",t.Paused="PAUSE",t.Running="RUN"}(o||(o={}));const s={fix:NaN,lat:NaN,lon:NaN,alt:NaN,vel:NaN,dop:NaN,gpsValid:!1,x:NaN,y:NaN,z:NaN,v:NaN,t:NaN,dt:NaN,pError:NaN,vError:NaN,hr:60,hrError:100,file:null,drawing:!1,status:o.Stopped,duration:0,distance:0,speed:0,steps:0,cadence:0};var i;i=s,Bangle.on("GPS",t=>function(t,n){t.lat=n.lat,t.lon=n.lon,t.alt=n.alt,t.vel=n.speed/3.6,t.fix=n.fix,t.dop=n.hdop,t.gpsValid=t.fix>0&&t.dop<=5,function(t){const n=Date.now();let r=(n-t.t)/1e3;if(isFinite(r)||(r=0),t.t=n,t.dt+=r,t.status===o.Running&&(t.duration+=r),!t.gpsValid)return;const e=6371008.8+t.alt,a=t.lat*Math.PI/180,s=t.lon*Math.PI/180,i=e*Math.cos(a)*Math.cos(s),d=e*Math.cos(a)*Math.sin(s),u=e*Math.sin(a),g=t.vel;if(!t.x)return t.x=i,t.y=d,t.z=u,t.v=g,t.pError=2.5*t.dop,void(t.vError=.05*t.dop);const l=i-t.x,c=d-t.y,p=u-t.z,f=g-t.v,N=Math.sqrt(l*l+c*c+p*p),h=Math.abs(f);t.pError+=t.v*t.dt,t.dt=0;const S=N+2.5*t.dop,E=h+.05*t.dop,w=t.pError/(t.pError+S)||0,x=t.vError/(t.vError+E)||0;t.x+=l*w,t.y+=c*w,t.z+=p*w,t.v+=f*x,t.pError+=(S-t.pError)*w,t.vError+=(E-t.vError)*x,t.status===o.Running&&(t.distance+=N*w,t.speed=t.distance/t.duration||0,t.cadence=60*t.steps/t.duration||0)}(t),r(t),t.gpsValid&&t.status===o.Running&&function(t){t.file.write([Date.now().toFixed(0),t.lat.toFixed(6),t.lon.toFixed(6),t.alt.toFixed(2),t.duration.toFixed(0),t.distance.toFixed(2),t.hr.toFixed(0),t.steps.toFixed(0)].join(",")+"\n")}(t)}(i,t)),Bangle.setGPSPower(1),function(t){Bangle.on("HRM",n=>function(t,n){if(0===n.confidence)return;const r=n.bpm-t.hr,e=Math.abs(r)+101-n.confidence,o=t.hrError/(t.hrError+e)||0;t.hr+=r*o,t.hrError+=(e-t.hrError)*o}(t,n)),Bangle.setHRMPower(1)}(s),function(t){Bangle.on("step",()=>function(t){t.status===o.Running&&(t.steps+=1)}(t))}(s),function(t){Bangle.loadWidgets(),Bangle.on("lcdPower",n=>{t.drawing=n,n&&e(t)}),e(t)}(s),setWatch(()=>a(s),BTN1,{repeat:!0,edge:"falling"}),setWatch(()=>function(t){t.status===o.Paused&&function(t){t.duration=0,t.distance=0,t.speed=0,t.steps=0,t.cadence=0}(t),t.status===o.Running?t.status=o.Paused:t.status=o.Stopped,r(t)}(s),BTN3,{repeat:!0,edge:"falling"})}();
!function(){"use strict";var t;!function(t){t.Stopped="STOP",t.Paused="PAUSE",t.Running="RUN"}(t||(t={}));const n={STOP:63488,PAUSE:65504,RUN:2016};function e(t,n,e){g.setColor(0),g.fillRect(n-60,e,n+60,e+30),g.setColor(65535),g.drawString(t,n,e)}function i(i){var s;g.setFontVector(30),g.setFontAlign(0,-1,0),e((i.distance/1e3).toFixed(2),60,55),e(function(t){const n=Math.round(t),e=Math.floor(n/3600),i=Math.floor(n/60)%60,s=n%60;return(e?e+":":"")+("0"+i).substr(-2)+":"+("0"+s).substr(-2)}(i.duration),172,55),e(function(t){if(t<.1667)return"__'__\"";const n=Math.round(1e3/t),e=Math.floor(n/60),i=n%60;return("0"+e).substr(-2)+"'"+("0"+i).substr(-2)+'"'}(i.speed),60,115),e(i.hr.toFixed(0),172,115),e(i.steps.toFixed(0),60,175),e(i.cadence.toFixed(0),172,175),g.setFont("6x8",2),g.setColor(i.gpsValid?2016:63488),g.fillRect(0,216,80,240),g.setColor(0),g.drawString("GPS",40,220),g.setColor(65535),g.fillRect(80,216,160,240),g.setColor(0),g.drawString(("0"+(s=new Date).getHours()).substr(-2)+":"+("0"+s.getMinutes()).substr(-2),120,220),g.setColor(n[i.status]),g.fillRect(160,216,230,240),g.setColor(0),g.drawString(i.status,200,220),g.setFont("6x8").setFontAlign(0,0,1).setColor(-1),i.status===t.Paused?g.drawString("START",236,60,1).drawString(" CLEAR ",236,180,1):i.status===t.Running?g.drawString(" PAUSE ",236,60,1).drawString(" PAUSE ",236,180,1):g.drawString("START",236,60,1).drawString(" ",236,180,1)}function s(t){g.clear(),g.setColor(50712),g.setFont("6x8",2),g.setFontAlign(0,-1,0),g.drawString("DIST (KM)",60,32),g.drawString("TIME",180,32),g.drawString("PACE",60,92),g.drawString("HEART",180,92),g.drawString("STEPS",60,152),g.drawString("CADENCE",180,152),i(t),Bangle.drawWidgets()}function a(n){n.status===t.Stopped&&function(t){const n=(new Date).toISOString().replace(/[-:]/g,""),e=`banglerun_${n.substr(2,6)}_${n.substr(9,6)}`;t.file=require("Storage").open(e,"w"),t.fileWritten=!1}(n),n.status===t.Running?n.status=t.Paused:n.status=t.Running,i(n)}const r={fix:NaN,lat:NaN,lon:NaN,alt:NaN,vel:NaN,dop:NaN,gpsValid:!1,x:NaN,y:NaN,z:NaN,t:NaN,timeSinceLog:0,hr:60,hrError:100,file:null,fileWritten:!1,drawing:!1,status:t.Stopped,duration:0,distance:0,speed:0,steps:0,cadence:0};var o;o=r,Bangle.on("GPS",n=>function(n,e){n.lat=e.lat,n.lon=e.lon,n.alt=e.alt,n.vel=e.speed/3.6,n.fix=e.fix,n.dop=e.hdop,n.gpsValid=n.fix>0,function(n){const e=Date.now();let i=(e-n.t)/1e3;if(isFinite(i)||(i=0),n.t=e,n.timeSinceLog+=i,n.status===t.Running&&(n.duration+=i),!n.gpsValid)return;const s=6371008.8+n.alt,a=n.lat*Math.PI/180,r=n.lon*Math.PI/180,o=s*Math.cos(a)*Math.cos(r),g=s*Math.cos(a)*Math.sin(r),d=s*Math.sin(a);if(!n.x)return n.x=o,n.y=g,void(n.z=d);const u=o-n.x,l=g-n.y,c=d-n.z,f=Math.sqrt(u*u+l*l+c*c);n.x=o,n.y=g,n.z=d,n.status===t.Running&&(n.distance+=f,n.speed=n.distance/n.duration||0,n.cadence=60*n.steps/n.duration||0)}(n),i(n),n.gpsValid&&n.status===t.Running&&n.timeSinceLog>5&&(n.timeSinceLog=0,function(t){t.fileWritten||(t.file.write(["timestamp","latitude","longitude","altitude","duration","distance","heartrate","steps"].join(",")+"\n"),t.fileWritten=!0),t.file.write([Date.now().toFixed(0),t.lat.toFixed(6),t.lon.toFixed(6),t.alt.toFixed(2),t.duration.toFixed(0),t.distance.toFixed(2),t.hr.toFixed(0),t.steps.toFixed(0)].join(",")+"\n")}(n))}(o,n)),Bangle.setGPSPower(1),function(t){Bangle.on("HRM",n=>function(t,n){if(0===n.confidence)return;const e=n.bpm-t.hr,i=Math.abs(e)+101-n.confidence,s=t.hrError/(t.hrError+i)||0;t.hr+=e*s,t.hrError+=(i-t.hrError)*s}(t,n)),Bangle.setHRMPower(1)}(r),function(n){Bangle.on("step",()=>function(n){n.status===t.Running&&(n.steps+=1)}(n))}(r),function(t){Bangle.loadWidgets(),Bangle.on("lcdPower",n=>{t.drawing=n,n&&s(t)}),s(t)}(r),setWatch(()=>a(r),BTN1,{repeat:!0,edge:"falling"}),setWatch(()=>function(n){n.status===t.Paused&&function(t){t.duration=0,t.distance=0,t.speed=0,t.steps=0,t.cadence=0}(n),n.status===t.Running?n.status=t.Paused:n.status=t.Stopped,i(n)}(r),BTN3,{repeat:!0,edge:"falling"})}();

View File

@ -1,4 +1,4 @@
import { AppState } from './state';
import { ActivityStatus, AppState } from './state';
declare var Bangle: any;
declare var g: any;
@ -26,11 +26,11 @@ function drawBackground(): void {
g.setFont('6x8', 2);
g.setFontAlign(0, -1, 0);
g.drawString('DIST (KM)', 60, 32);
g.drawString('TIME', 180, 32);
g.drawString('TIME', 172, 32);
g.drawString('PACE', 60, 92);
g.drawString('HEART', 180, 92);
g.drawString('HEART', 172, 92);
g.drawString('STEPS', 60, 152);
g.drawString('CADENCE', 180, 152);
g.drawString('CADENCE', 172, 152);
}
function drawValue(value: string, x: number, y: number) {
@ -45,11 +45,11 @@ function draw(state: AppState): void {
g.setFontAlign(0, -1, 0);
drawValue(formatDistance(state.distance), 60, 55);
drawValue(formatTime(state.duration), 180, 55);
drawValue(formatTime(state.duration), 172, 55);
drawValue(formatPace(state.speed), 60, 115);
drawValue(state.hr.toFixed(0), 180, 115);
drawValue(state.hr.toFixed(0), 172, 115);
drawValue(state.steps.toFixed(0), 60, 175);
drawValue(state.cadence.toFixed(0), 180, 175);
drawValue(state.cadence.toFixed(0), 172, 175);
g.setFont('6x8', 2);
@ -64,9 +64,18 @@ function draw(state: AppState): void {
g.drawString(formatClock(new Date()), 120, 220);
g.setColor(STATUS_COLORS[state.status]);
g.fillRect(160, 216, 240, 240);
g.fillRect(160, 216, 230, 240);
g.setColor(0x0000);
g.drawString(state.status, 200, 220);
g.setFont("6x8").setFontAlign(0,0,1).setColor(-1);
if (state.status === ActivityStatus.Paused) {
g.drawString("START",236,60,1).drawString(" CLEAR ",236,180,1);
} else if (state.status === ActivityStatus.Running) {
g.drawString(" PAUSE ",236,60,1).drawString(" PAUSE ",236,180,1);
} else {
g.drawString("START",236,60,1).drawString(" ",236,180,1);
}
}
function drawAll(state: AppState) {

View File

@ -14,8 +14,6 @@ interface GpsEvent {
}
const EARTH_RADIUS = 6371008.8;
const POS_ACCURACY = 2.5;
const VEL_ACCURACY = 0.05;
function initGps(state: AppState): void {
Bangle.on('GPS', (gps: GpsEvent) => readGps(state, gps));
@ -29,13 +27,17 @@ function readGps(state: AppState, gps: GpsEvent): void {
state.vel = gps.speed / 3.6;
state.fix = gps.fix;
state.dop = gps.hdop;
state.gpsValid = state.fix > 0 && state.dop <= 5;
state.gpsValid = state.fix > 0;
updateGps(state);
draw(state);
if (state.gpsValid && state.status === ActivityStatus.Running) {
/* Only log GPS data every 5 secs if we
have a fix and we're running. */
if (state.gpsValid &&
state.status === ActivityStatus.Running &&
state.timeSinceLog > 5) {
state.timeSinceLog = 0;
updateLog(state);
}
}
@ -44,9 +46,8 @@ function updateGps(state: AppState): void {
const t = Date.now();
let dt = (t - state.t) / 1000;
if (!isFinite(dt)) dt=0;
state.t = t;
state.dt += dt;
state.timeSinceLog += dt;
if (state.status === ActivityStatus.Running) {
state.duration += dt;
@ -62,52 +63,25 @@ function updateGps(state: AppState): void {
const x = r * Math.cos(lat) * Math.cos(lon);
const y = r * Math.cos(lat) * Math.sin(lon);
const z = r * Math.sin(lat);
const v = state.vel;
if (!state.x) {
state.x = x;
state.y = y;
state.z = z;
state.v = v;
state.pError = state.dop * POS_ACCURACY;
state.vError = state.dop * VEL_ACCURACY;
return;
}
const dx = x - state.x;
const dy = y - state.y;
const dz = z - state.z;
const dv = v - state.v;
const dpMag = Math.sqrt(dx * dx + dy * dy + dz * dz);
const dvMag = Math.abs(dv);
state.pError += state.v * state.dt;
state.dt = 0;
const pError = dpMag + state.dop * POS_ACCURACY;
const vError = dvMag + state.dop * VEL_ACCURACY;
const pGain = (state.pError / (state.pError + pError)) || 0;
const vGain = (state.vError / (state.vError + vError)) || 0;
state.x += dx * pGain;
state.y += dy * pGain;
state.z += dz * pGain;
state.v += dv * vGain;
state.pError += (pError - state.pError) * pGain;
state.vError += (vError - state.vError) * vGain;
/*// we're not currently updating lat/lon with the kalman filter
// as it seems not to update them correctly at the moment
// and we only use them for logging (where it makes sense to use
// raw GPS coordinates)
const pMag = Math.sqrt(state.x * state.x + state.y * state.y + state.z * state.z);
state.lat = (Math.asin(state.z / pMag) * 180 / Math.PI) || 0;
state.lon = (Math.atan2(state.y, state.x) * 180 / Math.PI) || 0;
state.alt = pMag - EARTH_RADIUS;*/
state.x = x;
state.y = y;
state.z = z;
if (state.status === ActivityStatus.Running) {
state.distance += dpMag * pGain;
state.distance += dpMag;
state.speed = (state.distance / state.duration) || 0;
state.cadence = (60 * state.steps / state.duration) || 0;
}

View File

@ -8,19 +8,23 @@ function initLog(state: AppState): void {
const time = datetime.substr(9, 6);
const filename = `banglerun_${date}_${time}`;
state.file = require('Storage').open(filename, 'w');
state.file.write([
'timestamp',
'latitude',
'longitude',
'altitude',
'duration',
'distance',
'heartrate',
'steps',
].join(',') + '\n');
state.fileWritten = false;
}
function updateLog(state: AppState): void {
if (!state.fileWritten) {
state.file.write([
'timestamp',
'latitude',
'longitude',
'altitude',
'duration',
'distance',
'heartrate',
'steps',
].join(',') + '\n');
state.fileWritten = true;
}
state.file.write([
Date.now().toFixed(0),
state.lat.toFixed(6),

View File

@ -14,15 +14,14 @@ interface AppState {
dop: number;
gpsValid: boolean;
// GPS Kalman data
// Absolute position data
x: number;
y: number;
z: number;
v: number;
// Last fix time
t: number;
dt: number;
pError: number;
vError: number;
// Last time we saved log info
timeSinceLog : number;
// HRM data
hr: number,
@ -30,6 +29,7 @@ interface AppState {
// Logger data
file: File;
fileWritten: boolean;
// Drawing data
drawing: boolean;
@ -62,16 +62,14 @@ function initState(): AppState {
x: NaN,
y: NaN,
z: NaN,
v: NaN,
t: NaN,
dt: NaN,
pError: NaN,
vError: NaN,
timeSinceLog : 0,
hr: 60,
hrError: 100,
file: null,
fileWritten: false,
drawing: false,

1
apps/batclock/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: App Created!

25
apps/batclock/README.md Normal file
View File

@ -0,0 +1,25 @@
<div align="center">
<h1> BatClock </h1>
<p align="center">
<img src="screenshot.png" height="240px">
</p>
<h3>Based on Morphing Clock, with an awesome "The Dark Knight" themed logo.
</h3>
<br>
<img src="https://img.shields.io/badge/Made%20with-JavaScript-f0db4f?style=for-the-badge&amp;logo=javascipt" alt="Made with JavaScipt"> <img src="https://img.shields.io/badge/Open%20Source%20Hardware-%E2%9A%99-blue?style=for-the-badge&amp;logo=open-source-initiative" alt="Open Source Hardware"> <img src="https://img.shields.io/badge/espruino-%E2%98%95-white?style=for-the-badge" alt="Espruino"> <img src="https://img.shields.io/badge/Built%20With-%E2%99%A1-critical?style=for-the-badge&amp;logo=github" alt="Built with Love">
</div>
<br>
## Requests
Please leave bug reports and requests by raising an issue [here](https://github.com/ra101/BangleApps).
<br>
## Creator
[< RA >](https://github.com/ra101)

View File

@ -0,0 +1,263 @@
// Initailize Variables
var is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"];
var locale = require("locale");
var CHARW = 9; // how tall are digits?
var CHARP = 1; // how chunky are digits?
var Y = 105; // start height
// Offscreen buffer
var buf = Graphics.createArrayBuffer(CHARW + CHARP * 2, CHARW * 2 + CHARP * 2, 1, {
msb: true
});
var bufimg = {
width: buf.getWidth(),
height: buf.getHeight(),
buffer: buf.buffer
};
// The last time that we displayed
var lastTime = "-----";
// If animating, this is the interval's id
var animInterval;
var timeInterval;
// Background Image
const bg_crack = require("heatshrink").decompress(atob("+HwgJC/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/gEIDrkYDrkwDrlgDrnADvMBwAdcLDkCBhcQDp8EfdMMDrkGDrkHDrkDCB8wDpj8PuD8M4AdO8ANaFp/wgEIDrV4gEEQ5gtBDprhN/wOMngCBLRcEj7EMj5qBBxcMgamMFYWEBAgzFjEA/AdMUgMGDpUQgEeDA0gAgd/DoNCDpUwS4J4GhgED/ZnBkIdKsACBaYxRDgPxBgMRDpVAYogADgJUBfoPgQoQYEwECAoYSCKQgACuD9CwE4d4ISCEQ0BdpRDCh4ECghOCDoUEAgUDZpQaBgEcYIUCRQQdGLogAGgZHBK4MfCQLaEKYj6FAAzNBPQMPwEBiAdEhDkHAA84WwUfGgOYDopCCEIYAIjxbCj4gBCYhZEjAdLg/GAYMfLwNMDogZDmAdLgP8AYM/+EAkgdEPoafEABF8AQP/DoMWDpDbEPBJcB//4bIJ3IQYJPBDpMPOwPPLoMLDo8BAgQqEAAqzBv8H/wUEDpEGSxXB/kD/4ZFAYY3Dgh4KOoIdBC4JuCDog3Dhg7K//gAQIbBVQYdDhwDChA7Kv/AEAUMDo8ejgDBvBZLwBZB+0cmAdGn+eFYL+BAAj9DAAscBQYdDnw3KDpMYDo1uDqiJDDofGDqceYo0BwwdTh0EHYsBd4YdRgQHFgQlDDqIHGggdUh4HGhB/GDqK2DgAdUg4dESwMQDqqvCHYcgIAgdXoEQDqcDDoQAD4EYDq47DwEwDqcCDoTqCgMA+AdTKQICBg04IQMD8AdXAAUGg4dXLAUAh0PDijKBgMeAoUeAgYASsEBx4EBkEengdViEAw4dCn14DqsMgHHDoUzuAdVgOAsYEBmF3WSoABvExDoeADq0P/ADBjH8DiwABDIUPeoIA/AH4A/AAUgDvVgC60DAonwDq0MAgcBDq84OYn4DisB8EAg4CBgF+DqsHwADB/+DwE8DqseEIf/8AdWvAEDn/wDqsfOYR8C//+Oqn/OwQACh//DqcPGY8DDqZwBRqodG4AdcDjcAcywAGsAdcAH4A/AH4A/AH4AnmBA/AEUBw//+Adaj///AOLFRsD//vCBk8DpkH85ZBg/ABxGAjwdMBoP/8JbJgeAg4dMsEAv49COxHAgfQQpeAAQOD//gBo4dBgCjLggED/53HgcAE4N8ap5tBBA0PDoSWNW4eAGgKiGDoIMBDp0GKQQAEjkAuAKBbxIdHAIIAE/gdDUQ4AHjEAhiyFV4M4DoLeIAAwSBDosAvAKCgU8Dp1gHoQADiEwDoS0JoBQFQ4IUCIYkAj/+WgL8IaA0AGgJYFDoJ6BWg8IAwsPDoJbBDo0PDQMBWgwTFgE/BI4dDAwpvDGYIAEvYCBGIMQBAV8NQMOAwS0Fgh+GEoInCMod8KgIdDH4bACDpbxDvkA/gGDg6WELAwICXgQWCiFmQQIdDgPwDA4ACjACBgQCBKQQdCj/8CIbVBABLrCg0AoEHBAViKgIdEnwdKZoQdCUQUQscAgYdEh5zIZoZ0CoClDsIDBvymE8AdJFAUcDovhKg94LRQdDcIngFAMeBIk8Dpi2CDofADoK6CAAQFFDp6fDAAcHSxQdICYUDBIkBE4QAJiAjFDoUBCAq0KDpAQJnAdQggdKYgodIhAdNg4dNhgdNga0LC4IsDDpUAWhaEBcIYdLuALKmDnEDpYpRVBaHCABD8EiAdLgSDMaIUYchgAJgKSBJIUYNZYAKSQQdDH4QATgwdFDisAggCBG4R2WHA0fTYIAUDQQ7CDq8wdgQdZkAdFa4IAUoAFEDoqhCABxSFjIdVgJSFd4odRXJAACgz1WDosCDv4dYgYdWhiiLDv4dSgGADv4dYoAdzgitWDpkgLLgddACwd/Dv4d/Dv4drADsIDv6z/LOQA/AH4A/AH4A/AGw"));
const batman = require("heatshrink").decompress(atob("+HwwJC/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4Ash//AA/gCQ8BCRH8DrsHDrf4BRIdSCQN/BQ/wDqOAgEfDqEDCI/+BYJ4IDqP4BZQdRCIYdMgYdPSw5HCgFwDpnABgSWHDoqmBRJJMDDpcwDocPWRIqJ/gLCvA7LCAZmIBgZZEHY6mEgI7Lg4dK8AdLMwd5DoaIHDojSHDoYFBDpQKCAAU/YBLjEB5TSKFYQdLaAiFJDo5pGaAjfJQoQdDUo/4DorwHb4QdDBxQdLNAQdDQw4dGJQ54CDoZ2GDo4TCB458CJI6GDDpn/DIXwdo69DAAhLHABrgCAAguIABgcGDrzDHABjfCDscPDqf8DssHDqf4DssDDuPwDssBHePgHbgdIgAdx4Ad/Dv4dywBZ/Dv6z/aP4d/Dv4drgIdT8Adm/gDBh4ZLB4YdIAAkfDhP+DBhePGxoAFn4dIDiUAg4cH/AdTepDpIABlfSbAADagzOCACcDDovwDqp4GOyodHoAdWeIocWSwqUWAAMHDof4Dq8BDofgDq6WEWS4ABv4dCwAd2j4cB/wcYaQbQYaQjQYAAMDDoPwDrjuZgEBDrkADoPADvF/Dr2ADrU//4caDoP+DrcfDrkPDrv8DvMH/Ad5gfwDv4AXgIdd8Ad/ADHADv4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHY"));
/* Get array of lines from digit d to d+1.
n is the amount (0..1)
maxFive is true is this digit only counts 0..5 */
const DIGITS = {
" ": n => [],
"0": n => [
[n, 0, 1, 0],
[1, 0, 1, 1],
[1, 1, 1, 2],
[n, 2, 1, 2],
[n, 1, n, 2],
[n, 0, n, 1]
],
"1": n => [
[1 - n, 0, 1, 0],
[1, 0, 1, 1],
[1 - n, 1, 1, 1],
[1 - n, 1, 1 - n, 2],
[1 - n, 2, 1, 2]
],
"2": n => [
[0, 0, 1, 0],
[1, 0, 1, 1],
[0, 1, 1, 1],
[0, 1 + n, 0, 2],
[1, 2 - n, 1, 2],
[0, 2, 1, 2]
],
"3": n => [
[0, 0, 1 - n, 0],
[0, 0, 0, n],
[1, 0, 1, 1],
[0, 1, 1, 1],
[1, 1, 1, 2],
[n, 2, 1, 2]
],
"4": n => [
[0, 0, 0, 1],
[1, 0, 1 - n, 0],
[1, 0, 1, 1 - n],
[0, 1, 1, 1],
[1, 1, 1, 2],
[1 - n, 2, 1, 2]
],
"5to0": n => [ // 5 -> 0
[0, 0, 0, 1],
[0, 0, 1, 0],
[n, 1, 1, 1],
[1, 1, 1, 2],
[0, 2, 1, 2],
[0, 2, 0, 2],
[1, 1 - n, 1, 1],
[0, 1, 0, 1 + n]
],
"5to6": n => [ // 5 -> 6
[0, 0, 0, 1],
[0, 0, 1, 0],
[0, 1, 1, 1],
[1, 1, 1, 2],
[0, 2, 1, 2],
[0, 2 - n, 0, 2]
],
"6": n => [
[0, 0, 0, 1 - n],
[0, 0, 1, 0],
[n, 1, 1, 1],
[1, 1 - n, 1, 1],
[1, 1, 1, 2],
[n, 2, 1, 2],
[0, 1 - n, 0, 2 - 2 * n]
],
"7": n => [
[0, 0, 0, n],
[0, 0, 1, 0],
[1, 0, 1, 1],
[1 - n, 1, 1, 1],
[1, 1, 1, 2],
[1 - n, 2, 1, 2],
[1 - n, 1, 1 - n, 2]
],
"8": n => [
[0, 0, 0, 1],
[0, 0, 1, 0],
[1, 0, 1, 1],
[0, 1, 1, 1],
[1, 1, 1, 2],
[0, 2, 1, 2],
[0, 1, 0, 2 - n]
],
"9": n => [
[0, 0, 0, 1],
[0, 0, 1, 0],
[1, 0, 1, 1],
[0, 1, 1 - n, 1],
[0, 1, 0, 1 + n],
[1, 1, 1, 2],
[0, 2, 1, 2]
],
":": n => [
[0.4, 0.4, 0.6, 0.4],
[0.6, 0.4, 0.6, 0.6],
[0.6, 0.6, 0.4, 0.6],
[0.4, 0.4, 0.4, 0.6],
[0.4, 1.4, 0.6, 1.4],
[0.6, 1.4, 0.6, 1.6],
[0.6, 1.6, 0.4, 1.6],
[0.4, 1.4, 0.4, 1.6]
]
};
/* Draw a transition between lastText and thisText.
'n' is the amount - 0..1 */
function drawDigits(lastText, thisText, n) {
"ram"
const p = CHARP; // padding around digits
const s = CHARW; // character size
var x = 80; // x offset
g.reset();
g.setColor(0, 0, 0);
g.setBgColor(1, 1, 1);
for (var i = 0; i < lastText.length; i++) {
var lastCh = lastText[i];
var thisCh = thisText[i];
if (thisCh == ":") x -= 4;
if (lastCh != thisCh) {
var ch, chn = n;
if ((thisCh - 1 == lastCh ||
(thisCh == 0 && lastCh == 5) ||
(thisCh == 0 && lastCh == 9)))
ch = lastCh;
else {
ch = thisCh;
chn = 0;
}
buf.clear();
if (ch == "5") ch = (lastCh == 5 && thisCh == 0) ? "5to0" : "5to6";
var l = DIGITS[ch](chn);
l.forEach(c => {
if (c[0] != c[2]) // horiz
buf.fillRect(p + c[0] * s, c[1] * s, p + c[2] * s, 2 * p + c[3] * s);
else if (c[1] != c[3]) // vert
buf.fillRect(c[0] * s, p + c[1] * s, 2 * p + c[2] * s, p + c[3] * s);
});
g.drawImage(bufimg, x, Y);
}
if (thisCh == ":") x -= 4;
x += s + p + 7;
}
}
function drawEverythingElse() {
var x = (CHARW + CHARP + 6) * 5 + 80;
var y = Y + 2 * CHARW + CHARP;
var d = new Date();
g.reset();
g.setBgColor(1, 1, 1);
g.setColor(1, 0, 0);
g.setFont("6x8");
g.setFontAlign(-1, -1);
g.drawString(("0" + d.getSeconds()).substr(-2), x, y - 8, true);
// meridian
if (is12Hour) g.drawString((d.getHours() < 12) ? "AM" : "PM", x,
+4, true);
// date
g.setFontAlign(0, -1);
var date = locale.date(d, false);
g.drawString(date, g.getWidth() / 2, y + 8, true);
}
/* Show the current time, and animate if needed */
function showTime() {
if (animInterval) return; // in animation - quit
var d = new Date();
var hours = d.getHours();
if (is12Hour) hours = ((hours + 11) % 12) + 1;
var t = (" " + hours).substr(-2) + ":" +
("0" + d.getMinutes()).substr(-2);
var l = lastTime;
// same - don't animate
if (t == l || l == "-----") {
drawDigits(l, t, 0);
drawEverythingElse();
lastTime = t;
return;
}
var n = 0;
animInterval = setInterval(function () {
n += 1 / 10;
if (n >= 1) {
n = 1;
clearInterval(animInterval);
animInterval = undefined;
}
drawDigits(l, t, n);
}, 20);
lastTime = t;
}
Bangle.on('lcdPower', function (on) {
if (animInterval) {
clearInterval(animInterval);
animInterval = undefined;
}
if (timeInterval) {
clearInterval(timeInterval);
timeInterval = undefined;
}
if (on) {
showTime();
timeInterval = setInterval(showTime, 1000);
} else {
lastTime = "-----";
}
});
g.clear();
// Draw Backgound before displaying time
g.setColor(0, 0.5, 0).drawImage(bg_crack);
g.setColor(1, 1, 1).drawImage(batman);
Bangle.loadWidgets();
Bangle.drawWidgets();
// Update time once a second
timeInterval = setInterval(showTime, 1000);
showTime();
// Show launcher when middle button pressed
setWatch(Bangle.showLauncher, BTN2, {
repeat: false,
edge: "falling"
});

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgJC/ACc//8AgP//kf/+AAoP4h4FD8ED/+Dwf/4AFB8fHBwYACwEAAoYqB/2vxAFD1uuAoePAont13+AoXvxwLCEYsAv4QCAAMf/AFDh/gAocH4AFDgYFEgJLBAH4AaA=="))

BIN
apps/batclock/bat-clock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@ -0,0 +1 @@
0.01: New App!

18
apps/battleship/README.md Normal file
View File

@ -0,0 +1,18 @@
# Battleship
The classic game of battleship.
## Usage
In the beginning, each player is required to place
all ships in his fleet on the field.
Navigation of the cursor is performed using BTN1 and
BTN3 as well as left and right on the touch screen.
To place a ship use BTN2 to initialize a placement
and BTN2 again to complete it.
In the next phase the players take alternating turns
in trying to hit an opposing ship.
After a player succeeds in sinking the entire opposing
fleet the game ends.

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwyBC/AH4A/AH4A/AH4A/AH4AEgeGAIVmAogBHBoRV/LZQBaLf4BK9EMlMMpQBClIJBMf5dM08Utcdh0luABNCIMUpYZBMO5bK1hZPAJYdBMecDswxFhkqktQLrYBEqAlBMI1mXdq5dYpxhqFYsc5pdnAIM16VeuQ1FLs67pAIM9+WP7GHrFN2JhjEYsMlRdtv9Yu34v8YlnNHolmL8GnktQLtkXt3XuwBB/ADBhfIYLq9FimsLtd2+9m21u25hFouQIIq9eLtVu+1eu1myxhIYLi9GtZdps3Wq11u83w94t3WMIZfDmsOL78dhxdo2tOmhZBy/5AIILCYYhfBr0TIopfUswZDLsc+iRRBr2Vp0UL4X2L4teL4JhEAIMD05fYPIfoLstWuk9+dGiZhDu83w+Ys3Wr11MI8UlJfbhkqLsdOqk16U96ZhHAINWqoBBMI8kxZfcpRddmvRLoNGmct2M12RhLqxhKlmsL/ZhDkuRloBBL4JhRL4JhCkhfdlJffAIhhajmLL7cD9BfkuBfCMJlGMJEUhJfcwxflMLFUgenL7sdhxhpnvxp0RnvSMIXzMI89yJFFL7MUpZfnmuxq0xAIbDEMI0k1hdXMJGnL9HRL5BhD+RhBovzHoJfcszBE1hhnovxp0RYoMtyJhG+ck9qhEL7DBIqBhnAIlxMInSNIK9dL5GGhkqLMstyEt2BhJilKXr5hJimsLsdGiABBnvwMIsc5hdjMJXNL7812BfDopfEFoJdnL4VmYc2QXYJdBMoJdC1gxFL8phJhkqktQYr4hBEoJdtMJcD07FdXIWnLuJjGG4xjCpcc9xZPCIMMpZb5YpwBF9EMlMMpQBClIJBC5hd2YpwBWLfZldKf4A/AH4A/AH4A/AH4A/AAo"))

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="battleship-icon.svg"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
id="svg8"
version="1.1"
viewBox="0 0 12.7 12.7"
height="48"
width="48"
inkscape:export-filename="battleship-icon.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2">
<linearGradient
id="linearGradient848"
inkscape:collect="always">
<stop
id="stop844"
offset="0"
style="stop-color:#88e7cd;stop-opacity:1;" />
<stop
id="stop846"
offset="1"
style="stop-color:#88e7cd;stop-opacity:0;" />
</linearGradient>
<linearGradient
gradientUnits="userSpaceOnUse"
y2="6.3499999"
x2="6.3499999"
y1="3.0260465"
x1="9.7076998"
id="linearGradient850"
xlink:href="#linearGradient848"
inkscape:collect="always" />
</defs>
<sodipodi:namedview
inkscape:window-maximized="1"
inkscape:window-y="-9"
inkscape:window-x="-9"
inkscape:window-height="1013"
inkscape:window-width="1920"
units="px"
showgrid="false"
inkscape:document-rotation="0"
inkscape:current-layer="layer1"
inkscape:document-units="px"
inkscape:cy="50.37804"
inkscape:cx="120.0705"
inkscape:zoom="3.959798"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:groupmode="layer"
inkscape:label="Layer 1">
<path
id="path833"
d="M 11.074702,6.3499999 A 4.7247024,4.7247024 0 0 1 6.3499999,11.074702 4.7247024,4.7247024 0 0 1 1.6252975,6.3499999 4.7247024,4.7247024 0 0 1 6.3499999,1.6252975 4.7247024,4.7247024 0 0 1 11.074702,6.3499999 Z"
style="fill:#23ae87;fill-opacity:1;stroke:none;stroke-width:0.765;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
d="M 9.7076995,3.0260466 A 4.7247024,4.7247024 0 0 1 9.8531236,9.5203211 L 6.3499999,6.3499999 Z"
sodipodi:arc-type="slice"
sodipodi:end="0.73556977"
sodipodi:start="5.5028377"
sodipodi:ry="4.7247024"
sodipodi:rx="4.7247024"
sodipodi:cy="6.3499999"
sodipodi:cx="6.3499999"
sodipodi:type="arc"
style="fill:url(#linearGradient850);fill-opacity:1;stroke:none;stroke-width:0.765;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle835" />
<circle
style="fill:none;stroke-width:0.765;stroke-linecap:round;stroke-linejoin:round;stroke:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
id="circle837"
cx="6.3499999"
cy="6.3499999"
r="4.7247024" />
<path
id="path852"
d="M 6.3499999,6.3499999 9.7076994,3.0260467"
style="fill:#00ff00;stroke:#93ffc9;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
style="fill:none;fill-opacity:1;stroke:#007033;stroke-width:0.765;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 11.074702,6.3499999 A 4.7247024,4.7247024 0 0 1 6.3499999,11.074702 4.7247024,4.7247024 0 0 1 1.6252975,6.3499999 4.7247024,4.7247024 0 0 1 6.3499999,1.6252975 4.7247024,4.7247024 0 0 1 11.074702,6.3499999 Z"
id="path842" />
<circle
r="0.46234927"
cy="5.3087621"
cx="8.8007536"
id="path839"
style="fill:#93ffc9;fill-opacity:1;stroke:none;stroke-width:0.38708;stroke-linecap:round;stroke-linejoin:round" />
<circle
style="fill:#57c78f;fill-opacity:1;stroke:none;stroke-width:0.38708;stroke-linecap:round;stroke-linejoin:round"
id="circle841"
cx="8.2715864"
cy="7.9545956"
r="0.46234927" />
<circle
r="0.46234927"
cy="9.3416424"
cx="8.2715864"
id="circle843"
style="fill:#43c082;fill-opacity:1;stroke:none;stroke-width:0.38708;stroke-linecap:round;stroke-linejoin:round" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1,321 @@
const FIELD_WIDTH = [11, 11, 15]; // for each phase
const FIELD_HEIGHT = FIELD_WIDTH;
const FIELD_LINE_WIDTH = 2;
const FIELD_MARGIN = 2;
const FIELD_COUNT_X = 10;
const FIELD_COUNT_Y = FIELD_COUNT_X;
const MARGIN_LEFT = 16;
const MARGIN_TOP = 42;
const HEADING_COLOR = ['#FF7070', '#7070FF']; // for each player
const FIELD_LINE_COLOR = '#FFFFFF';
const FIELD_BG_COLOR_REGULAR = '#808080';
const FIELD_BG_COLOR_SELECTED = '#FFFFFF';
const SHIP_COLOR_PLACED = '#507090';
const SHIP_COLOR_AVAIL = '#204070';
const STATE_HIT_COLOR = ['#B00000', '#0000B0']; // for each player
const STATE_MISS_COLOR = '#404040';
const SHIP_CAPS = [
1, // Carrier (type 0, size 5)
2, // Battleship (type 1, size 4)
3, // Destroyer (type 2, size 3)
4 // Patrol Boat (type 3, size 2)
];
const FULL_HITS = SHIP_CAPS.reduce((a, c, i) => a + c*(5 -i), 0);
const INDICATOR_LAYOUT = [
[0, 1, 1, 3],
[2, 2, 2, 3, 3, 3]
];
const INDICATORS = INDICATOR_LAYOUT.reduce((a, c, i) => {
let y = FIELD_COUNT_Y + 1 + i;
let x1 = 0;
c.forEach(type => {
let size = 5 - type;
let x2 = x1 + size - 1;
a.push({ "type": type, "position": [x1, y, x2, y] });
x1 += size;
});
return a;
}, []).sort((l, r) => (l.type - r.type)*FIELD_COUNT_X*FIELD_COUNT_Y
+ (l.position[0] + l.position[1]*FIELD_COUNT_X
- (r.position[0] + r.position[1]*FIELD_COUNT_X)));
let phase = 0;
let player = 0;
let selected = [-10, -10];
let to_add = null;
let to_rem = null;
let placements = [[],[]];
let field_states = [new Array(100).fill(0), new Array(FIELD_COUNT_X*FIELD_COUNT_Y).fill(0)];
let current = [[0, 0],[0, 0]];
let behaviours = []; // depending on phase
function getLeftOffset(x) {
return MARGIN_LEFT + x*(FIELD_WIDTH[phase] + FIELD_MARGIN + 1);
}
function getTopOffset(y) {
return MARGIN_TOP + y*(FIELD_HEIGHT[phase] + FIELD_MARGIN + 1);
}
function getFieldState(x, y) {
return field_states[player][x + FIELD_COUNT_X*y];
}
function setFieldState(x, y, value) {
field_states[player][x + FIELD_COUNT_X*y] = value;
}
function updateFieldStates() {
placements.forEach((ps, i) => {
ps.forEach(p => {
let pos = p.position;
for (let x = pos[0]; x <= pos[2]; x++)
for (let y = pos[1]; y <= pos[3]; y++) {
field_states[i][x + FIELD_COUNT_X*y] = 1;
}
});
});
}
function getHitCount() {
return field_states[player].reduce(
(v, state) => state == 3 ? v + 1 : v,
0);
}
function drawField(x, y, selected) {
let x1 = getLeftOffset(x);
let y1 = getTopOffset(y);
let x2 = x1 + FIELD_WIDTH[phase];
let y2 = y1 + FIELD_HEIGHT[phase];
let field_state = getFieldState(x, y);
g.setColor(selected ? FIELD_BG_COLOR_SELECTED : FIELD_BG_COLOR_REGULAR);
g.fillRect(x1, y1, x2, y2);
g.setColor(FIELD_LINE_COLOR);
g.drawRect(x1, y1, x2, y2);
switch (field_state) {
case 2:
g.setColor(STATE_MISS_COLOR);
g.fillCircle(x1 + FIELD_WIDTH[phase]/2 + 1, y1 + FIELD_HEIGHT[phase]/2 + 1, FIELD_WIDTH[phase]/2 - 3);
break;
case 3:
g.setColor(STATE_HIT_COLOR[player]);
g.fillCircle(x1 + FIELD_WIDTH[phase]/2 + 1, y1 + FIELD_HEIGHT[phase]/2 + 1, FIELD_WIDTH[phase]/2 - 1);
break;
default:
break;
}
}
function drawFields(x1, y1, x2, y2) {
let l = getLeftOffset(x1);
let t = getTopOffset(y1);
let r = getLeftOffset(x2) + FIELD_WIDTH[phase] + FIELD_MARGIN;
let b = getTopOffset(y2) + FIELD_HEIGHT[phase] + FIELD_MARGIN;
g.clearRect(l, t, r, b);
for (let x = x1; x <= x2; x++)
for (let y = y1; y <= y2; y++) {
drawField(x, y, x == current[player][0] && y == current[player][1]);
}
}
function drawShip(x1, y1, x2, y2, color) {
g.setColor(color);
let diam = Math.min(FIELD_HEIGHT[phase], FIELD_WIDTH[phase]) - 3;
let rad = diam/2;
let cx1 = getLeftOffset(x1) + FIELD_WIDTH[phase]/2 + 1;
let cy1 = getTopOffset(y1) + FIELD_HEIGHT[phase]/2 + 1;
let cx2 = getLeftOffset(x2) + FIELD_WIDTH[phase]/2 + 1;
let cy2 = getTopOffset(y2) + FIELD_HEIGHT[phase]/2 + 1;
if (x1 == x2) {
g.fillRect(cx1 - rad, cy1, cx1 + rad, cy2);
} else {
g.fillRect(cx1, cy1 - rad, cx2, cy1 + rad);
}
g.fillCircle(cx1, cy1, rad);
g.fillCircle(cx2, cy2, rad);
}
function hasCollision(pos) {
return placements[player].some(
p => pos[0] <= p.position[2]
&& pos[2] >= p.position[0]
&& pos[1] <= p.position[3]
&& pos[3] >= p.position[1]);
}
function isAvailable(type) {
let count = placements[player].reduce(
(v, p) => p.type == type ? v + 1 : v,
0);
return count < SHIP_CAPS[type];
}
function determineChanges() {
to_rem = to_add;
to_add = null;
if (selected[0] == current[player][0] && selected[1] == current[player][1]) return;
if (selected[0] == current[player][0]) {
let size = Math.abs(selected[1] - current[player][1]) + 1;
if (size < 2 || size > 5 ) return;
let y1 = Math.min(selected[1], current[player][1]);
let y2 = Math.max(selected[1], current[player][1]);
let pos = [current[player][0], y1, current[player][0], y2];
let type = 5 - size;
if (!hasCollision(pos) && isAvailable(type)) {
to_add = { "type": type, "position": pos };
}
}
if (selected[1] == current[player][1]) {
let size = Math.abs(selected[0] - current[player][0]) + 1;
if (size < 2 || size > 5 ) return;
let x1 = Math.min(selected[0], current[player][0]);
let x2 = Math.max(selected[0], current[player][0]);
let pos = [x1, current[player][1], x2, current[player][1]];
let type = 5 - size;
if (!hasCollision(pos) && isAvailable(type)) {
to_add = { "type": type, "position": pos };
}
}
}
function addPlacement(descriptor) {
placements[player].push(descriptor);
placements[player].sort((l, r) => l.type - r.type);
}
function drawShipPlacements() {
if (to_rem) {
drawFields.apply(null, to_rem.position);
}
placements[player].forEach(
p => drawShip.apply(null, p.position.concat([SHIP_COLOR_PLACED])));
if (to_add) {
drawShip.apply(null, to_add.position.concat([SHIP_COLOR_PLACED]));
}
}
function drawShipIndicator() {
let p = to_add
? placements[player].concat(to_add).sort((l, r) => l.type - r.type)
: placements[player];
let pi = 0;
INDICATORS.forEach(indicator => {
let color = SHIP_COLOR_AVAIL;
if (pi < p.length && p[pi].type == indicator.type) {
pi += 1;
color = SHIP_COLOR_PLACED;
}
drawShip.apply(null, indicator.position.concat(color));
});
}
function drawHeading(text) {
g.clearRect(0, 20, 100, 32);
g.setColor(HEADING_COLOR[player]);
g.setFont('4x6', 2.8);
g.drawString(text, MARGIN_LEFT, 20);
}
function reset() {
g.clear();
drawHeading('Player ' + (player + 1));
drawFields(0, 0, 9, 9);
}
function showResults() {
let text1 = 'Player ' + (player + 1) + ' won!';
let text2 = 'Congratulations!';
g.clear();
g.clearRect(0, 20, 100, 32);
g.setColor(HEADING_COLOR[player]);
g.setFont('Vector', 20);
g.drawString(text1, MARGIN_LEFT, 80);
g.drawString(text2, MARGIN_LEFT, 120);
}
function moveSelection(dx, dy) {
let x = current[player][0];
let y = current[player][1];
drawField(x, y, false);
current[player][0] = x = (x + dx + FIELD_COUNT_X)%FIELD_COUNT_X;
current[player][1] = y = (y + dy + FIELD_COUNT_Y)%FIELD_COUNT_Y;
drawField(x, y, true);
}
behaviours.push({
"move": (dx, dy) => {
moveSelection(dx, dy);
determineChanges();
drawShipPlacements();
drawShipIndicator();
},
"action": _ => {
if (to_add) {
addPlacement(to_add);
to_add = null;
selected = [-10, -10];
if (placements[player].length == 10) {
behaviours[phase].transition();
}
} else {
selected = [current[player][0], current[player][1]];
}
},
"transition": _ => {
current[0] = [0, 0];
player = 1;
phase = 1;
reset();
drawShipIndicator();
}
});
behaviours.push({
"move": behaviours[0].move,
"action": behaviours[0].action,
"transition": _ => {
current[1] = [0, 0];
player = 0;
phase = 2;
updateFieldStates();
reset();
}
});
behaviours.push({
"move": (dx, dy) => moveSelection(dx, dy),
"action": _ => {
let x = current[player][0];
let y = current[player][1];
let field_state = getFieldState(x, y);
if (field_state > 1) return;
setFieldState(x, y, field_state + 2);
drawField(x, y, true);
Bangle.buzz(200 + field_state*800, 0.5 + field_state*0.5);
if (getHitCount() < FULL_HITS) {
player = (player + 1)%2;
setTimeout(reset, 1000);
} else {
setTimeout(behaviours[phase].transition, 1000);
}
},
"transition": _ => {
phase = 3;
showResults();
}
});
behaviours.push({
"move": _ => {},
"action": _ => {}
});
reset();
drawShipIndicator();
setWatch(_ => behaviours[phase].move(0, -1), BTN1, {repeat: true, debounce: 100});
setWatch(_ => behaviours[phase].move(0, 1), BTN3, {repeat: true, debounce: 100});
setWatch(_ => behaviours[phase].move(-1, 0), BTN4, {repeat: true, debounce: 100});
setWatch(_ => behaviours[phase].move(1, 0), BTN5, {repeat: true, debounce: 100});
setWatch(_ => behaviours[phase].action(), BTN2, {repeat: true, debounce: 100});

View File

@ -1,2 +1,3 @@
0.01: Initial commit. Not very efficient, and widgets not working for some reason.
0.02: Fixes; widget support
0.03: Remove hardcoded hour buzz (you can install widchime if you miss it)

View File

@ -262,12 +262,6 @@ Graphics.prototype.drawRotLine = function (sina, cosa, cx, cy, r1, r2) {
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();

View File

@ -3,3 +3,4 @@
Remove 'faceUp' check as it's automatic
0.03: Modified for use with new bootloader and firmware
0.04: Modified to account for changes in the behavior of Graphics.fillPoly
0.05: Slight increase to draw speed after LCD on

View File

@ -86,9 +86,9 @@ function clearTimers() {
}
function startTimers() {
g.clear();
redraw();
Bangle.drawWidgets();
intervalRef = setInterval(redraw,1000);
redraw();
}
Bangle.loadWidgets();
startTimers();

View File

@ -21,3 +21,6 @@
0.20: Allow Gadgetbridge to work even with programmable:off
0.21: Handle echo off char from Gadgetbridge app when programmable:off (fix #558)
0.22: Stop LCD timeout being disabled on first run (when there is no settings.json)
0.23: Move to a precalculated .boot0 file which should speed up load time
0.24: Add Bangle.setUI polyfill
0.25: Fix error in 'no clock app' message

View File

@ -1,68 +1,2 @@
// This ALWAYS runs at boot
E.setFlags({pretokenise:1});
// Load settings...
var s = require('Storage').readJSON('setting.json',1)||{};
if (s.ble!==false) {
if (s.HID) { // Human interface device
if (s.HID=="joy") Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));
else if (s.HID=="kb") Bangle.HID = E.toUint8Array(atob("BQEJBqEBBQcZ4CnnFQAlAXUBlQiBApUBdQiBAZUFdQEFCBkBKQWRApUBdQORAZUGdQgVACVzBQcZAClzgQAJBRUAJv8AdQiVArECwA=="));
else /*kbmedia*/Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA=="));
NRF.setServices({}, {uart:true, hid:Bangle.HID});
}
}
if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth
if (s.log) Terminal.setConsole(true); // if showing debug, force REPL onto terminal
else E.setConsole(null,{force:true}); // on new (2v05+) firmware we have E.setConsole which allows a 'null' console
/* If not programmable add our own handler for Bluetooth data
to allow Gadgetbridge commands to be received*/
Bluetooth.line="";
Bluetooth.on('data',function(d) {
var l = (Bluetooth.line + d).split("\n");
Bluetooth.line = l.pop();
l.forEach(n=>Bluetooth.emit("line",n));
});
Bluetooth.on('line',function(l) {
if (l.startsWith('\x10')) l=l.slice(1);
if (l.startsWith('GB({') && l.endsWith('})') && global.GB)
try { global.GB(JSON.parse(l.slice(3,-1))); } catch(e) {}
});
} else {
if (s.log && !NRF.getSecurityStatus().connected) Terminal.setConsole(); // if showing debug, put REPL on terminal (until connection)
else Bluetooth.setConsole(true); // else if no debug, force REPL to Bluetooth
}
// we just reset, so BLE should be on.
// Don't disconnect if something is already connected to us
if (s.ble===false && !NRF.getSecurityStatus().connected) NRF.sleep();
// Set time, vibrate, beep, etc
if (!Bangle.F_BEEPSET) {
if (!s.vibrate) Bangle.buzz=Promise.resolve;
if (s.beep===false) Bangle.beep=Promise.resolve;
else if (s.beep=="vib") Bangle.beep = function (time, freq) {
return new Promise(function(resolve) {
if ((0|freq)<=0) freq=4000;
if ((0|time)<=0) time=200;
if (time>5000) time=5000;
analogWrite(D13,0.1,{freq:freq});
setTimeout(function() {
digitalWrite(D13,0);
resolve();
}, time);
});
};
}
if (s.timeout!==undefined) Bangle.setLCDTimeout(s.timeout);
if (!s.timeout) Bangle.setLCDPower(1);
E.setTimeZone(s.timezone);
delete s;
// Draw out of memory errors onto the screen
E.on('errorFlag', function(errorFlags) {
g.reset(1).setColor("#ff0000").setFont("6x8").setFontAlign(0,1).drawString(errorFlags,g.getWidth()/2,g.getHeight()-1).flip();
print("Interpreter error:", errorFlags);
E.getErrorFlags(); // clear flags so we get called next time
});
// stop users doing bad things!
global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); }
// Load *.boot.js files
require('Storage').list(/\.boot\.js/).forEach(bootFile=>{
eval(require('Storage').read(bootFile));
});
// Initially this runs and rewrites itself
eval(require('Storage').read('bootupdate.js'));

View File

@ -14,11 +14,7 @@ if (!clockApp) {
if (clockApp)
clockApp = require("Storage").read(clockApp.src);
}
if (!clockApp) clockApp=`E.showMessage("No Clock Found");
setWatch(() => {
Bangle.showLauncher();
}, BTN2, {repeat:false,edge:"falling"});)
`;
if (!clockApp) clockApp=`E.showMessage("No Clock Found");setWatch(()=>{Bangle.showLauncher();}, BTN2, {repeat:false,edge:"falling"});`;
// check to see if our clock is wrong - if it is use GPS time
if ((new Date()).getFullYear()<2000) {
E.showMessage("Searching for\nGPS time");

133
apps/boot/bootupdate.js Normal file
View File

@ -0,0 +1,133 @@
/* This rewrites boot0.js based on current settings. If settings changed then it
recalculates, but this avoids us doing a whole bunch of reconfiguration most
of the time. */
E.showMessage("Updating boot0...");
var s = require('Storage').readJSON('setting.json',1)||{};
var boot = "";
var CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/));
boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/))!=${CRC}) { eval(require('Storage').read('bootupdate.js'));} else {\n`;
boot += `E.setFlags({pretokenise:1});\n`;
if (s.ble!==false) {
if (s.HID) { // Human interface device
if (s.HID=="joy") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));`;
else if (s.HID=="kb") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBqEBBQcZ4CnnFQAlAXUBlQiBApUBdQiBAZUFdQEFCBkBKQWRApUBdQORAZUGdQgVACVzBQcZAClzgQAJBRUAJv8AdQiVArECwA=="));`
else /*kbmedia*/boot += `Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA=="));`;
boot += `NRF.setServices({}, {uart:true, hid:Bangle.HID});\n`;
}
}
if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth
if (s.log) boot += `Terminal.setConsole(true);\n`; // if showing debug, force REPL onto terminal
else boot += `E.setConsole(null,{force:true});\n`; // on new (2v05+) firmware we have E.setConsole which allows a 'null' console
/* If not programmable add our own handler for Bluetooth data
to allow Gadgetbridge commands to be received*/
boot += `
Bluetooth.line="";
Bluetooth.on('data',function(d) {
var l = (Bluetooth.line + d).split("\n");
Bluetooth.line = l.pop();
l.forEach(n=>Bluetooth.emit("line",n));
});
Bluetooth.on('line',function(l) {
if (l.startsWith('\x10')) l=l.slice(1);
if (l.startsWith('GB({') && l.endsWith('})') && global.GB)
try { global.GB(JSON.parse(l.slice(3,-1))); } catch(e) {}
});\n`;
} else {
if (s.log) boot += `if (!NRF.getSecurityStatus().connected) Terminal.setConsole();\n`; // if showing debug, put REPL on terminal (until connection)
else boot += `Bluetooth.setConsole(true);\n`; // else if no debug, force REPL to Bluetooth
}
// we just reset, so BLE should be on.
// Don't disconnect if something is already connected to us
if (s.ble===false) boot += `if (!NRF.getSecurityStatus().connected) NRF.sleep();\n`;
// Set time
if (s.timeout!==undefined) boot += `Bangle.setLCDTimeout(${s.timeout});\n`;
if (!s.timeout) boot += `Bangle.setLCDPower(1);\n`;
boot += `E.setTimeZone(${s.timezone});`;
// Set vibrate, beep, etc IF on older firmwares
if (!Bangle.F_BEEPSET) {
if (!s.vibrate) boot += `Bangle.buzz=Promise.resolve;\n`
if (s.beep===false) boot += `Bangle.beep=Promise.resolve;\n`
else if (s.beep=="vib") boot += `Bangle.beep = function (time, freq) {
return new Promise(function(resolve) {
if ((0|freq)<=0) freq=4000;
if ((0|time)<=0) time=200;
if (time>5000) time=5000;
analogWrite(D13,0.1,{freq:freq});
setTimeout(function() {
digitalWrite(D13,0);
resolve();
}, time);
});
};\n`;
}
// Draw out of memory errors onto the screen
boot += `E.on('errorFlag', function(errorFlags) {
g.reset(1).setColor("#ff0000").setFont("6x8").setFontAlign(0,1).drawString(errorFlags,g.getWidth()/2,g.getHeight()-1).flip();
print("Interpreter error:", errorFlags);
E.getErrorFlags(); // clear flags so we get called next time
});\n`;
// stop users doing bad things!
if (global.save) boot += `global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); }\n`;
// Apply any settings-specific stuff
if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`;
if (s.quiet && s.qmOptions) boot+=`Bangle.setOptions(${E.toJS(s.qmOptions)});\n`;
if (s.quiet && s.qmBrightness) {
if (s.qmBrightness!=1) boot+=`Bangle.setLCDBrightness(${s.qmBrightness});\n`;
} else {
if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`;
}
if (s.quiet && s.qmTimeout) boot+=`Bangle.setLCDTimeout(${s.qmTimeout});\n`;
if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${s.passkey}, mitm:1, display:1});\n`;
if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`;
// Pre-2v10 firmwares without a theme/setUI
if (!g.theme) {
boot += `g.theme={fg:-1,bg:0,fg2:-1,bg2:7,fgH:-1,bgH:0x02F7};\n`;
}
if (!Bangle.setUI) {
boot += `Bangle.setUI=function(mode, cb) {
if (Bangle.btnWatches) {
Bangle.btnWatches.forEach(clearWatch);
delete Bangle.btnWatches;
}
if (Bangle.swipeHandler) {
Bangle.removeListener("swipe", Bangle.swipeHandler);
delete Bangle.swipeHandler;
}
if (Bangle.touchandler) {
Bangle.removeListener("touch", Bangle.touchHandler);
delete Bangle.touchHandler;
}
function b() {
try{Bangle.buzz(20);}catch(e){}
}
if (!mode) return;
else if (mode=="updown") {
Bangle.btnWatches = [
setWatch(function() { b();cb(-1); }, BTN1, {repeat:1}),
setWatch(function() { b();cb(1); }, BTN3, {repeat:1}),
setWatch(function() { b();cb(); }, BTN2, {repeat:1})
];
} else if (mode=="leftright") {
Bangle.btnWatches = [
setWatch(function() { b();cb(-1); }, BTN1, {repeat:1}),
setWatch(function() { b();cb(1); }, BTN3, {repeat:1}),
setWatch(function() { b();cb(); }, BTN2, {repeat:1})
];
Bangle.swipeHandler = d => {b();cb(d);};
Bangle.on("swipe", Bangle.swipeHandler);
Bangle.touchHandler = d => {b();cb();};
Bangle.on("touch", Bangle.touchHandler);
} else
throw new Error("Unknown UI mode");
};\n`;
}
// Append *.boot.js files
require('Storage').list(/\.boot\.js/).forEach(bootFile=>{
boot += "//"+bootFile+"\n"+require('Storage').read(bootFile)+"\n";
});
boot += "}\n";// initial 'if'
var s = require('Storage').write('.boot0',boot);
delete boot;
E.showMessage("Reloading...");
eval(require('Storage').read('.boot0'));
eval(require('Storage').read('.bootcde'));

View File

@ -3,3 +3,4 @@
0.09: Add BTN1 status line with ID,Fw ver, mem %, battery %
0.10: Icon fixed for transparency
0.11: added Heart Rate Monitor status and ability to turn on/off
0.12: added support for different locales

View File

@ -2,7 +2,6 @@ var fontsize = 3;
var locale = require("locale");
var marginTop = 40;
var flag = false;
var WeekDays = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
var hrtOn = false;
var hrtStr = "Hrt: ??? bpm";
@ -26,19 +25,14 @@ function drawAll(){
}
function updateRest(now){
let date = locale.date(now,false);
writeLine(WeekDays[now.getDay()],1);
writeLine(date,2);
writeLine(locale.dow(now),1);
writeLine(locale.date(now,1),2);
drawInfo(5);
}
function updateTime(){
if (!Bangle.isLCDOn()) return;
let now = new Date();
let h = now.getHours();
let m = now.getMinutes();
h = h>=10?h:"0"+h;
m = m>=10?m:"0"+m;
writeLine(h+":"+m,0);
writeLine(locale.time(now,1),0);
writeLine(flag?" ":"_",3);
flag = !flag;
if(now.getMinutes() == 0)

View File

@ -0,0 +1 @@
0.01: 1st ver,RGB565 and RGB888 colors in a common UI/UX

View File

@ -0,0 +1,43 @@
# Color Catalog
This is a very basic app that displays RGB565 and RGB888 colors, its name and code in screen.
This apps is based in the "common UI/UX".
Launcher icon
![](color_catalog.png)
1st screen - page 1
![](color_catalog_s1.png)
2nd screen - page
![](color_catalog_s2.png)
## Usage
Open and 2 rows of colors per page
Interact with a horizontal swipe/slide to move to next or previos page in order to display other colors
## Features
Colours, font, user input, load widgets
## Controls
Press left area -
Press righ area -
Press center area -
Swipe Left - Load the previous page and its colors
Swipe Right - Load the next page and its colors
BTN1 - Prints Button1
BTN2 - Prints Button2
BTN3 - Quit to Launcher
## Support
This app is so basic that probably the easiest is to just edit the code
Otherwise you can contact me [here](https://github.com/dapgo)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AHczAAYXyh3jC6s+93gC6nuAAMwC6UDC4QYMC40OC4fuC6JeBC53/AAZeEAAKSLC4sPmYXDSJfdAAZNB/4YDR5YXFIQP/MILZMC4iNCmfznxGCgbKDC5PtIgQYBXwUykTDHC4ndOoc/CwcikAXQIwMCCwMikYXRh4uCAAIXQgUT+YXDMAzXEnoWC8kRiQXEJAwXEma8B93hiMRn5IK5gADka8B91BC4IGBF5IXE7s/+fuCwJIFL4wXFDAMxC4ZICa44XG6cyC4URl8yCw4XH7szkIXCiYWIC4/UkZIDag4XJ61jJAUSCxIXHi1i+YWMa4pGBszUBkUq1UicIIACC5XWsMRj8yCwIXPmVGC4MT1WiC6EzIwMRmYWCC5gMBF4IXBjUzCwQXQsMa1R3BC5xHDRQOqD4IXRnQWBC6gWCC84A/AGg="))

182
apps/color_catalog/app.js Normal file
View File

@ -0,0 +1,182 @@
/* Color show for Bangle.js
grey
RGB888:#404040 / 0x404040
RGB565:#4208 / 0x4208
grey RGB888:#5c5c5c / 0x5c5c5c
RGB565:#5AEB / 0x5AEB
*/
var v_model=process.env.BOARD;
console.log("device="+v_model);
var x_max_screen=g.getWidth();//240;
var y_max_screen=g.getHeight(); //240;
var y_wg_bottom=g.getHeight()-25;
var y_wg_top=25;
if (v_model=='BANGLEJS') {
var x_btn_area=215;
var x_max_usable_area=x_btn_area;//Pend! only for bangle.js
var y_btn2=124; //harcoded for bangle.js cuz it is not the half of
} else x_max_usable_area=240;
var contador=1;
var cont_items=0;
var cont_row=0;
var v_boxes_row=4;
var cont_page=1;
var v_boxwidth=40;
var v_boxheight=10;
var v_acolorpos=0;
var v_font1size=11;
var v_fontsize=13;
var v_color_b_area='#111111';//black
var v_color_b_area2=0x5AEB;//Dark
var v_color_text='#FB0E01';
var v_color_statictxt='#e56e06'; //orange RGB format rrggbb
//RGB565 requires only 16 (5+6+5) bits/2 bytes
var a_colors_str= Array('White RGB565 0x','Orange','DarkGreen','Yellow',
'Maroon','Blue','green','Purple',
'cyan','olive','DarkCyan','DarkGrey',
'Navy','Red','Magenta','GreenYellow',
'Blush RGB888','pure red','Orange','Grey green',
'D. grey','Almond','Amber','Bone',
'Canary','Aero blue','Camel','Baby pink',
'Y.Corn','Cultured','Eigengrau','Citrine');
var a_colors= Array(0xFFFF,0xFD20,0x03E0,0xFFE0,
0x7800,0x001F,0x07E0,0x780F,
0x07FF,0x7BE0,0x03EF,0x7BEF,
0x000F,0xF800,0xF81F,0xAFE5,
'#DE5D83','#FB0E01','#E56E06','#7E795C',
'#404040','#EFDECD','#FFBF00','#E3DAC9',
'#FFFF99','#C0E8D5','#C19A6B','#F4C2C2',
'#FBEC5D','#F5F5F5','#16161D','#E4D00A');
var v_color_lines=0xFFFF; //White hex format
//the biggest usable area, button area not included
function ClearActiveArea(){
g.setColor(v_color_b_area);
g.fillRect(0,y_wg_top,x_max_usable_area,y_wg_bottom); //fill all screen except widget areas
g.flip();
}
function UserInput(){
Bangle.on('touch', function(button){
switch(button){
case 1:
console.log("Touch 1");//left
break;
case 2:
console.log("Touch 2");//right
break;
case 3:
console.log("Touch 3");//center 1+2
break;
}
});
if (v_model=='BANGLEJS') {
//only the name of the function
setWatch(Bangle.showLauncher, BTN3, { repeat: true });
}
Bangle.on('swipe', dir => {
if(dir == 1) {
console.log("v_acolorpos"+v_acolorpos+"a_colors.length"+a_colors.length);
if (v_acolorpos<a_colors.length-1) {
cont_page++;
console.log("swipe page"+cont_page);
PrintScreen(cont_page);
}
}
else {
if (cont_page>0) cont_page--;
console.log("swipe page"+cont_page);
PrintScreen(cont_page);
}
});
console.log("Log: Input conditions loaded");
} //end of UserInput
function DrawBangleButtons(){
g.setFontVector(v_font1size);
g.setColor(v_color_lines);//White
//g.drawString("Dwn", x_max_screen-g.stringWidth("Dwn"),y_wg_top+v_font1size+1);
//above Btn2
//g.setFontVector(v_font1size).drawString("Off", x_max_screen-g.stringWidth("Off"),y_btn2-(2*v_font1size));
//g.drawString("Set", x_max_screen-g.stringWidth("Set"),y_btn2-v_font1size);
//above Btn3
g.drawString("Quit", x_max_screen-g.stringWidth("Quit"),y_wg_bottom-(2*v_font1size));
g.flip();
g.setColor(v_color_text); //green
g.setFontVector(v_font1size);
g.drawString("B1", x_max_screen-g.stringWidth("B1"),y_wg_top);
g.drawString("B2", x_max_screen-g.stringWidth("B2"),y_btn2);
g.drawString("B3",x_max_screen-g.stringWidth("B3"),y_wg_bottom-v_font1size);
g.flip();
}
function PrintScreen(page){
ClearActiveArea();
g.setColor(v_color_statictxt);
g.setFont("Vector",v_fontsize);
g.drawString("Page "+page,10,y_wg_top+5);
g.flip();
v_acolorpos=page*(v_boxes_row*2);
console.log("page"+cont_page+"arraypos"+v_acolorpos);
for (cont_row=0;cont_row<2;cont_row++){
console.log("row"+cont_row);
for (cont_items=0;cont_items<v_boxes_row;cont_items++){
g.setColor(a_colors[v_acolorpos]); //dynamic color
//console.log("col"+cont_items);
var v_x1_box=(v_boxwidth*cont_items)+5;
var v_x2_box=(v_boxwidth*(cont_items+1))+5;
//y for second row is v_boxheight+strings
var v_pos_row=y_wg_top+v_fontsize+5+(cont_row*v_boxheight)+(cont_row*v_boxes_row*(v_fontsize+4));
// console.log("v_pos_row"+v_pos_row);
var v_y1_box=(v_pos_row);
var v_y2_box=(v_pos_row+v_boxheight);
g.fillRect(v_x1_box,v_y1_box,v_x2_box,v_y2_box); //fill all screen except widget areas
//identify color format
var v_string=v_acolorpos+" "+a_colors_str[v_acolorpos]+" "+a_colors[v_acolorpos].toString(16);
//Align always inside the display
if ((v_x1_box+g.stringWidth(v_string))<x_max_usable_area){
var v_x_pos_str=v_x1_box;
}
else var v_x_pos_str=x_max_usable_area-g.stringWidth(v_string);
//y positions next text line
var v_y_pos_str=v_y2_box+8+((v_fontsize+2)*cont_items);
g.setFont("Vector",v_fontsize);
g.drawString(v_string,v_x_pos_str,v_y_pos_str);
//line below 2nd string
g.flip();
if (v_acolorpos<a_colors.length-1) v_acolorpos++;
else break; //no more print until swipe
}
}
}
console.log("**************************");
console.log("Log: *** Color Catalog app");
console.log("array length"+a_colors.length);
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
if (v_model=='BANGLEJS') DrawBangleButtons();
UserInput();
PrintScreen(0);

BIN
apps/color_catalog/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

View File

@ -0,0 +1 @@
0.01: Initial version

View File

@ -0,0 +1,12 @@
# countdown-timer
A basic bangle.js timer with a focus on usability.
* Start or Pause the timer with BTN1
* Reset the timer with BTN2
* Exit the application with BTN3
* Touch the right side of the screen to increase the time amount by 1 second
* Touch the left side of the scren to decrease the time amount by 1 second
* Touching and holding the screen will increase or decrease the time amount by 60 seconds at a time.
Icons made by [Freepik](https://www.freepik.com) from [Flaticon](https://www.flaticon.com/).

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwgMJhvd7tEogkSC4lAC6xWUgUgNysiC6hGBL58BiMQLoYXDMJkRAAIWEC4gYJFwIABCwgXFDBAWCpoXLMYwuCigUD6czmUSmQYKC4QuD6f//8xiUzmckJJIuECwQXEDAgXGFwc/C4XykYXCmZIJCwVPCwQABCwczmgwHXYXUCwgXFGBAXC74XLGAQXELowXIVgYXF6QWF+Z3EJAhGFUganHJAoXFRooXLMAQXCLwwXHMAQXUMAQXCRwQWFC9HSiMvC40iCocxiKoEC4cTC4sRiLBDC5MiF4wXFmQXHL4/zkRHEO6H/OwwXFX5IXfd5HfC5s0C4/TC5skRwZ4LLxAXHMAxeIC4hIJLxYXEJAxGMJAgwFFw4XGGAZhD+UjLoxGEC4owDmMSIoouGJAgYBGIIXDCwYuGGAoABIQMiaIQuKDA/dCoguJJIwXHCxQYGCyIYFCyRjELZYA="))

View File

@ -0,0 +1,232 @@
const heatshrinkDecompress = require("heatshrink").decompress;
const playIcon = heatshrinkDecompress(atob("jEYwhC/gFwBZV3BhV3u4LLBhILCEpALCBhALDu9gBaojKHZZrVQZSbLAG4A="));
const pauseIcon = heatshrinkDecompress(atob("jEYwhC/xGIAYoL/Bf4LfAHA="));
const resetIcon = heatshrinkDecompress(atob("jEYwg30h3u93gAgIKHBgXuBYgIBoEEBoQWFAgQMCBYgrBE4giEBYYjGAgY+DBY4AHBZlABZQ7DLIpTFAo5ZJLYYDFTZKzLAGQA=="));
const closeIcon = heatshrinkDecompress(atob("jEYwhC/4AEDhgKEhnMAofMCIgGECAoHFCwwIDCw4YDCxAYCCxALMEZY7KKZZrKQZibKAHIA="));
const timerState = {
IDLE: 0,
RUNNING: 1
};
let currentState = timerState.IDLE;
let remainingSeconds = 0;
let countdownInterval = null;
let increasingInterval = null;
let decreasingInterval = null;
let isDecreasingRemainingSeconds = false;
let isIncreasingRemainingSeconds = false;
function main() {
g.clear();
g.setFont("Vector", 40);
g.setFontAlign(0, 0);
registerInputHandlers();
draw();
}
function registerInputHandlers() {
setWatch(onPrimaryButtonPressed, BTN1, { repeat: true });
setWatch(onResetButtonPressed, BTN2, { repeat: true });
setWatch(onExitButtonPressed, BTN3, { repeat: true });
setWatch(onDecreaseRemainingSecondsPressed, BTN4, { repeat: true, edge: "rising" });
setWatch(onIncreaseRemainingSecondsPressed, BTN5, { repeat: true, edge: "rising" });
setWatch(onDecreaseRemainingSecondsReleased, BTN4, { repeat: true, edge: "falling" });
setWatch(onIncreaseRemainingSecondsReleased, BTN5, { repeat: true, edge: "falling" });
}
function draw() {
g.clearRect(200, 0, 240, 240);
g.clearRect(0, 0, 240, 80);
drawRemainingSecondsPanel();
g.drawImage(resetIcon, 216, 108);
g.drawImage(closeIcon, 216, 188);
if (currentState == timerState.IDLE) {
g.drawImage(playIcon, 216, 28);
} else {
g.drawImage(pauseIcon, 216, 28);
}
g.flip();
}
function drawRemainingSecondsPanel() {
g.clearRect(0, 100, 200, 140);
g.drawString(formatRemainingSeconds(), 105, 120);
if (currentState == timerState.IDLE) {
drawSubtractRemainingSeconds();
drawIncreaseRemainingSeconds();
} else {
g.setColor(0.4, 0.4, 0.4);
drawSubtractRemainingSeconds();
drawIncreaseRemainingSeconds();
g.setColor(-1);
}
}
function drawSubtractRemainingSeconds() {
if (isDecreasingRemainingSeconds) {
drawFilledCircle(22, 117, 15);
}
g.drawString("-", 25, 120);
}
function drawIncreaseRemainingSeconds() {
if (isIncreasingRemainingSeconds) {
drawFilledCircle(182, 117, 15);
}
g.drawString("+", 185, 120);
}
function drawFilledCircle(x, y, radians) {
g.setColor(0.1, 0.37, 0.87);
g.fillCircle(x, y, radians);
g.setColor(-1);
}
function formatRemainingSeconds() {
const minutes = Math.floor(remainingSeconds / 60);
const minutesTens = Math.floor(minutes / 10);
const minutesUnits = minutes % 10;
const seconds = remainingSeconds % 60;
const secondsTens = Math.floor(seconds / 10);
const secondsUnits = seconds % 10;
return `${minutesTens}${minutesUnits}:${secondsTens}${secondsUnits}`;
}
function onPrimaryButtonPressed() {
if (isIncreasingRemainingSeconds || isDecreasingRemainingSeconds) return;
if (currentState == timerState.IDLE) {
if (remainingSeconds == 0) return;
currentState = timerState.RUNNING;
beginCountdown();
draw();
} else {
currentState = timerState.IDLE;
stopCountdown();
draw();
}
}
function beginCountdown() {
countdownInterval = setInterval(countdown, 1000);
}
function countdown() {
--remainingSeconds;
if (remainingSeconds <= 0) {
remainingSeconds = 0;
stopCountdown();
}
drawRemainingSecondsPanel();
if (remainingSeconds <= 0) {
drawStopMessage();
}
}
function drawStopMessage() {
draw();
Bangle.buzz(800);
g.setFont("Vector", 30);
g.setColor(1.0, 0.91, 0);
g.drawString("Time's Up!", 105, 40);
g.setColor(-1);
g.setFont("Vector", 40);
}
function stopCountdown() {
clearInterval(countdownInterval);
countdownInterval = null;
currentState = timerState.IDLE;
}
function onResetButtonPressed() {
currentState = timerState.IDLE;
remainingSeconds = 0;
draw();
}
function onExitButtonPressed() {
Bangle.showLauncher();
}
function onIncreaseRemainingSecondsPressed() {
if (currentState == timerState.RUNNING) return;
incremementRemainingSeconds();
increasingInterval = setInterval(() => {
remainingSeconds += 60;
if (remainingSeconds >= 5999) {
remainingSeconds = 5999;
}
drawRemainingSecondsPanel();
}, 250);
isIncreasingRemainingSeconds = true;
drawRemainingSecondsPanel();
}
function incremementRemainingSeconds() {
if (remainingSeconds >= 5999) return;
++remainingSeconds;
}
function onIncreaseRemainingSecondsReleased() {
if (currentState == timerState.RUNNING) return;
clearInterval(increasingInterval);
isIncreasingRemainingSeconds = false;
drawRemainingSecondsPanel();
}
function onDecreaseRemainingSecondsPressed() {
if (currentState == timerState.RUNNING) return;
decreaseRemainingSeconds();
decreasingInterval = setInterval(() => {
remainingSeconds -= 60;
if (remainingSeconds < 0) {
remainingSeconds = 0;
}
drawRemainingSecondsPanel();
}, 250);
isDecreasingRemainingSeconds = true;
drawRemainingSecondsPanel();
}
function decreaseRemainingSeconds() {
if (remainingSeconds <= 0) return;
--remainingSeconds;
}
function onDecreaseRemainingSecondsReleased() {
if (currentState == timerState.RUNNING) return;
clearInterval(decreasingInterval);
isDecreasingRemainingSeconds = false;
draw();
}
main();

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,2 +1,3 @@
0.01: New App!
0.02: Added decrement and touch functions
0.03: Set color - ensures widgets don't end up coloring the counter's text

View File

@ -1,9 +1,8 @@
var counter = 0;
g.setColor(0xFFFF);
function updateScreen() {
g.clearRect(0, 50, 250, 150);
g.setColor(0xFFFF);
g.setFont("Vector",40).setFontAlign(0,0);
g.drawString(Math.floor(counter), g.getWidth()/2, 100);
g.drawString('-', 45, 100);
@ -44,7 +43,7 @@ g.drawString('Tap right or BTN1 to increase\nTap left or BTN3 to decrease\nPress
Bangle.loadWidgets();
Bangle.drawWidgets();
updateScreen();
// TODO: Enable saving counts to file
// Does not work if widgets are not visible
// Add small watch

View File

@ -10,4 +10,5 @@
0.12: Move code to Arwes Module
0.13: Improve icon
0.14: Switch Icon back to 8bit web palette to fix it
0.15: Hotfix: Remove var declaration from app image
0.15: Hotfix: Remove var declaration from app image
0.16: Revert: Change Counter back to button control

View File

@ -67,7 +67,7 @@ function levelColor(l) {
function drawBattery() {
const l = E.getBattery(), c = levelColor(l);
count = l;
// count = l;
const xl = 45 + l * (194 - 46) / 100;
g.clearRect(46, 58 + 80 + yOffset + 37, 193, height - 5);
g.setColor(c).fillRect(46, 58 + 80 + yOffset + 37, xl, height - 5);
@ -124,14 +124,14 @@ drawClock();
setWatch(Bangle.showLauncher, BTN2, {repeat: false, edge: "falling"});
// setWatch(function () {
// count++;
// drawCounterText();
// }, BTN1, {repeat: true, edge: "falling"});
// setWatch(function () {
// count--;
// drawCounterText();
// }, BTN3, {repeat: true, edge: "falling"});
setWatch(function () {
count++;
drawCounterText();
}, BTN1, {repeat: true, edge: "falling"});
setWatch(function () {
count--;
drawCounterText();
}, BTN3, {repeat: true, edge: "falling"});
// refesh every 100 milliseconds
setInterval(updateClock, 500);

View File

@ -1 +1,2 @@
0.01: Based on the Analog Clock app, minimal dot interface
0.01: Based on the Analog Clock app, minimal dot
0.02: Remove hardcoded hour buzz (you can install widchime if you miss it)

View File

@ -128,9 +128,6 @@ const onMinute = () => {
g.setColor(1, 0.9, 0.9);
// Minute
minDot((360 * currentDate.getMinutes()) / 60,3);
if (currentDate.getHours() >= 0 && currentDate.getMinutes() === 0) {
Bangle.buzz();
}
drawDate();
};

1
apps/doztime/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New App!

14
apps/doztime/README.md Normal file
View File

@ -0,0 +1,14 @@
Dozenal Time
============
A dozenal Holocene calendar and a dozenal diurnal clock. For information about them, go to https://dozenal.ae-web.ca/pdf/dozenal-calendar.pdf and https://dozenal.ae-web.ca/pdf/about-short.pdf. They've been in use for some years.
In the dozenal number base, ten and eleven are single digits, and 10 is a dozen. The clock simply divides the day by successive powers of a dozen. The day or parts of it may be divided easily into halves, thirds, quarters, sixths, or twelfths (dozenths). There is no conglomeration of bases two, ten, twelve, and sixty, as in the current system of time measurement.
The annual calendar has a dozen months of 5 weeks each, each week having 6 days. The 5 or 6 days beyond 360 (dozenal 260) are added where they keep the season beginnings the most accurate.
The year itself begins on the December solstice. Because that always happens, there is no need of a leap-year rule to keep the seasons from drifting.
The epoch (year numbering) begins in the last year when the perihelion coincided with the June solstice, near the beginning of the Holocene era. That astronomical basis makes the calendar free from politics, religion, or geography.
While the year number remains cardinal, BTN5 toggles between cardinal and ordinal for the rest of the calendar segments. BTN4 adds or removes a quickly changing digit to or from the clock.

1
apps/doztime/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("lEowggdkUiCKIADCJcCkUjmYACmUikAlKB4ImDAoQSJkYhBFAQECAQI5HBQU//4AC+YUCHowzBCQfzAYYKCEw8vEgYqD+QoGgQbBHAYADCwIoBCYkiEwhPEBAIoBHgY6BExHyHwQhBFAQ6BkYTHDgcyHgcCHRZlDCYQsBTYg6GDAJQDPoI6LAAIPBCYRiHHQhkDCYRiHHQhkCCYKKBCYzzBA4yMBCYTVEGYITEBYITZHY5PHUAJjITIJjHRZINBIYoTDWZAoFWYbbJFALbHgUyX4oPDXIcjMQITBmZkHFYszCYZkJMQoTCKAQ8IHQZOCHgYoKkQ6DHgYoEcIgmBHQg8CFAIPCCYfzBQQSEFAbrFCQImHFAQUCkczmYECAQISGHoYzBAAQFCCRA9BEwYoDHI4pFAAgRLCooRPABg="))

225
apps/doztime/app.js Normal file
View File

@ -0,0 +1,225 @@
// Positioning values for graphics buffers
const g_height = 80; // total graphics height
const g_x_off = 16; // position from left
const g_y_off = (240 - g_height)/2; // vertical center for graphics region
const g_width = 240 - 2 * g_x_off; // total graphics width
const g_height_d = 32; // height of date region
const g_y_off_d = 0; // y position of date region within graphics region
const spacing = 0; // space between date and time in graphics region
const g_y_off_t = g_y_off_d + g_height_d + spacing; // y position of time within graphics region
const g_height_t = 48; // height of time region
// Other vars
const A1 = [30,30,30,30,31,31,31,31,31,31,30,30];
const B1 = [30,30,30,30,30,31,31,31,31,31,30,30];
const B2 = [30,30,30,30,31,31,31,31,31,30,30,30];
const timeColour = "#f2f2f2";
const dateColours = ["#ff0000","#ffa500","#ffff00","#00b800","#0000ff","#ff00ff","#ff0080"];
const calen10 = {"size":32,"pt0":[32-g_x_off,16],"step":[20,0],"dx":-4.5,"dy":-4.5}; // positioning for usual calendar line
const calen7 = {"size":32,"pt0":[62-g_x_off,16],"step":[20,0],"dx":-4.5,"dy":-4.5}; // positioning for S-day calendar line
const time5 = {"size":48,"pt0":[64-g_x_off,24],"step":[30,0],"dx":-6.5,"dy":-6.5}; // positioning for lull time line; was 64
const time6 = {"size":48,"pt0":[48-g_x_off,24],"step":[30,0],"dx":-6.5,"dy":-6.5}; // positioning for twinkling time line
const baseYear = 11584;
const baseDate = Date(2020,11,21); // month values run from 0 to 11
let accum = new Date(baseDate.getTime());
let sequence = [];
let timeActiveUntil;
let addTimeDigit = false;
let dateFormat = false;
let lastX = 999999999;
let res = {};
//var last_time_log = 0;
// Date and time graphics buffers
var dateColour = "#ffffff"; // override later
var g_d = Graphics.createArrayBuffer(g_width,g_height_d,1,{'msb':true});
var g_t = Graphics.createArrayBuffer(g_width,g_height_t,1,{'msb':true});
// Set screen mode and function to write graphics buffers
Bangle.setLCDMode();
g.clear(); // start with blank screen
g.flip = function()
{
g.setColor(dateColour);
g.drawImage(
{
width:g_width,
height:g_height_d,
buffer:g_d.buffer
}, g_x_off, g_y_off + g_y_off_d);
g.setColor(timeColour);
g.drawImage(
{
width:g_width,
height:g_height_t,
buffer:g_t.buffer
}, g_x_off, g_y_off + g_y_off_t);
};
setWatch(function(){ modeTime(); }, BTN1, {repeat:true} );
setWatch(function(){ Bangle.showLauncher(); }, BTN2, { repeat: false, edge: "falling" });
setWatch(function(){ modeWeather(); }, BTN3, {repeat:true});
setWatch(function(){ toggleTimeDigits(); }, BTN4, {repeat:true});
setWatch(function(){ toggleDateFormat(); }, BTN5, {repeat:true});
function buildSequence(targ){
for(let i=0;i<targ.length;++i){
sequence.push(new Date(accum.getTime()));
accum.setDate(accum.getDate()+targ[i]);
}
}
buildSequence(B2);
buildSequence(B2);
buildSequence(A1);
buildSequence(B1);
buildSequence(B2);
buildSequence(B2);
buildSequence(A1);
buildSequence(B1);
buildSequence(B2);
buildSequence(B2);
buildSequence(A1);
buildSequence(B1);
buildSequence(B2);
function getDate(dt){
let index = sequence.findIndex(n => n > dt)-1;
let year = baseYear+parseInt(index/12);
let month = index % 12;
let day = parseInt((dt-sequence[index])/86400000);
let colour = dateColours[day % 6];
if(day==30){ colour=dateColours[6]; }
return({"year":year,"month":month,"day":day,"colour":colour});
}
function toggleTimeDigits(){
addTimeDigit = !addTimeDigit;
modeTime();
}
function toggleDateFormat(){
dateFormat = !dateFormat;
modeTime();
}
function formatDate(res,dateFormat){
let yyyy = res.year.toString(12);
calenDef = calen10;
if(!dateFormat){ //ordinal format
let mm = ("0"+(res.month+1).toString(12)).substr(-2);
let dd = ("0"+(res.day+1).toString(12)).substr(-2);
if(res.day==30){
calenDef = calen7;
let m = ((res.month+1).toString(12)).substr(-2);
return(yyyy+"-"+"S"+m); // ordinal format
}
return(yyyy+"-"+mm+"-"+dd);
}
let m = res.month.toString(12); // cardinal format
let w = parseInt(res.day/6);
let d = res.day%6;
//return(yyyy+"-"+res.month+"-"+w+"-"+d);
return(yyyy+"-"+m+"-"+w+"-"+d);
}
function writeDozTime(text,def,colour){
let pts = def.pts;
let x=def.pt0[0];
let y=def.pt0[1];
g_t.clear();
g_t.setFont("Vector",def.size);
for(let i in text){
if(text[i]=="a"){ g_t.setFontAlign(0,0,2); g_t.drawString("2",x+def.dx,y+def.dy); }
else if(text[i]=="b"){ g_t.setFontAlign(0,0,2); g_t.drawString("3",x+def.dx,y+def.dy); }
else{ g_t.setFontAlign(0,0,0); g_t.drawString(text[i],x,y); }
x = x+def.step[0];
y = y+def.step[1];
}
}
function writeDozDate(text,def,colour){
dateColour = colour;
let pts = def.pts;
let x=def.pt0[0];
let y=def.pt0[1];
g_d.clear();
g_d.setFont("Vector",def.size);
for(let i in text){
if(text[i]=="a"){ g_d.setFontAlign(0,0,2); g_d.drawString("2",x+def.dx,y+def.dy); }
else if(text[i]=="b"){ g_d.setFontAlign(0,0,2); g_d.drawString("3",x+def.dx,y+def.dy); }
else{ g_d.setFontAlign(0,0,0); g_d.drawString(text[i],x,y); }
x = x+def.step[0];
y = y+def.step[1];
}
}
// Functions for time mode
function drawTime()
{
let dt = new Date();
let date = "";
let timeDef;
let x = 0;
dt.setDate(dt.getDate());
if(addTimeDigit){
x =
10368*dt.getHours()+172.8*dt.getMinutes()+2.88*dt.getSeconds()+0.00288*dt.getMilliseconds();
let msg = "00000"+Math.floor(x).toString(12);
let time = msg.substr(-5,3)+"."+msg.substr(-2);
let wait = 347*(1-(x%1));
timeDef = time6;
} else {
x =
864*dt.getHours()+14.4*dt.getMinutes()+0.24*dt.getSeconds()+0.00024*dt.getMilliseconds();
let msg = "0000"+Math.floor(x).toString(12);
let time = msg.substr(-4,3)+"."+msg.substr(-1);
let wait = 4167*(1-(x%1));
timeDef = time5;
}
if(lastX > x){ res = getDate(dt); } // calculate date once at start-up and once when turning over to a new day
date = formatDate(res,dateFormat);
if(dt<timeActiveUntil)
{
// Write to background buffers, then display on screen
writeDozDate(date,calenDef,res.colour);
writeDozTime(time,timeDef,timeColour);
g.flip();
// Ready next interval
setTimeout(drawTime,wait);
}
else
{
// Clear screen
g_d.clear();
g_t.clear();
g.flip();
}
lastX = x;
}
function modeTime()
{
timeActiveUntil = new Date();
timeActiveUntil.setDate(timeActiveUntil.getDate());
timeActiveUntil.setSeconds(timeActiveUntil.getSeconds()+15);
//Bangle.setLCDPower(true);
clearTimeout();
drawTime();
}
Bangle.loadWidgets();
// Time-logging function
/*function logTime(label)
{
var d = new Date();
var t = d.getTime();
var diff_test = t - last_time_log;
last_time_log = t;
console.log(label + " at time: " + t + ", since last: " + diff_test);
}*/
// Functions for weather mode - TODO
function drawWeather() {}
function modeWeather() {}
// Start time on twist
Bangle.on('twist', function() {
modeTime();
});
Bangle.drawWidgets();

BIN
apps/doztime/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

1
apps/fclock/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: First published version of app

1
apps/fclock/app-icon.js Normal file

File diff suppressed because one or more lines are too long

BIN
apps/fclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

206
apps/fclock/fclock.app.js Normal file
View File

@ -0,0 +1,206 @@
{
var minutes;
var seconds;
var hours;
var date;
var first = true;
var locale = require('locale');
var _12hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"] || false;
//HR variables
var id = 0;
var grow = true;
var size=10;
//Screen dimensions
const screen = {
width: g.getWidth(),
height: g.getWidth(),
middle: g.getWidth() / 2,
center: g.getHeight() / 2,
};
// Ssettings
const settings = {
time: {
color: '#dddddd',
font: 'Vector',
size: 100,
middle: screen.middle,
center: screen.center,
},
date: {
color: '#dddddd',
font: 'Vector',
size: 15,
middle: screen.height-17, // at bottom of screen
center: screen.center,
},
circle: {
colormin: '#ffffff',
colorsec: '#ffffff',
width: 10,
middle: screen.middle,
center: screen.center,
height: screen.height
},
hr: {
color: '#333333',
size: 20,
x: screen.center,
y: screen.middle + 65
}
};
const dateStr = function (date) {
return locale.date(new Date(), 1);
};
const getFormated = function(val) {
if (val<10) {
val='0'+val;
}
return val;
};
const drawMin = function (sections, color) {
g.setFontAlign(0, 0, 0);
g.setColor('#000000');
g.setFont(settings.time.font, settings.time.size/2);
g.drawString(getFormated(sections-1), settings.time.center+50, settings.time.middle);
g.setColor(settings.time.color);
g.setFont(settings.time.font, settings.time.size/2);
g.drawString(getFormated(sections), settings.time.center+50, settings.time.middle);
};
const drawSec = function (sections, color) {
g.setFontAlign(0, 0, 0);
g.setColor('#000000');
g.setFont(settings.time.font, settings.time.size/4);
g.drawString(getFormated(sections-1), settings.time.center+100, settings.time.middle);
g.setColor(settings.time.color);
g.setFont(settings.time.font, settings.time.size/4);
g.drawString(getFormated(sections), settings.time.center+100, settings.time.middle);
};
const drawClock = function () {
currentTime = new Date();
//Get date as a string
date = dateStr(currentTime);
if(seconds==59) {
g.clear();
}
// Update minutes when needed
if (minutes != currentTime.getMinutes()) {
minutes = currentTime.getMinutes();
drawMin(minutes, settings.circle.colormin);
}
//Update seconds when needed
if (seconds != currentTime.getSeconds()) {
seconds = currentTime.getSeconds();
drawSec(seconds, settings.circle.colorsec);
}
//Write the time as configured in the settings
hours = currentTime.getHours();
if (_12hour && hours > 13) {
hours = hours - 12;
}
var meridian;
if (typeof locale.meridian === "function") {
meridian = locale.meridian(new Date());
} else {
meridian = "";
}
var timestr;
if (meridian.length > 0 && _12hour) {
timestr = hours + " " + meridian;
} else {
timestr = hours;
}
g.setFontAlign(0, 0, 0);
g.setColor(settings.time.color);
g.setFont(settings.time.font, settings.time.size);
g.drawString(timestr, settings.time.center-40, settings.time.middle);
//Write the date as configured in the settings
g.setColor(settings.date.color);
g.setFont(settings.date.font, settings.date.size);
g.drawString(date, settings.date.center, settings.date.middle);
};
//setInterval for HR visualisation
const newBeats = function (hr) {
if (id != 0) {
changeInterval(id, 6e3 / hr.bpm);
} else {
id = setInterval(drawHR, 6e3 / hr.bpm);
}
};
//visualize HR with circles pulsating
const drawHR = function () {
if (grow && size < settings.hr.size) {
size++;
}
if (!grow && size > 3) {
size--;
}
if (size == settings.hr.size || size == 3) {
grow = !grow;
}
if (grow) {
color = settings.hr.color;
g.setColor(color);
g.fillCircle(settings.hr.x, settings.hr.y, size);
} else {
color = "#000000";
g.setColor(color);
g.drawCircle(settings.hr.x, settings.hr.y, size);
}
};
// clean app screen
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
//manage when things should be enabled and not
Bangle.on('lcdPower', function (on) {
if (on) {
Bangle.setHRMPower(1);
} else {
Bangle.setHRMPower(0);
}
});
// refesh every second
setInterval(drawClock, 1E3);
//start HR monitor and update frequency of update
Bangle.setHRMPower(1);
Bangle.on('HRM', function (d) {
newBeats(d);
});
// draw now
drawClock();
// Show launcher when middle button pressed
setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" });
}

1
apps/fontclock/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Initial Release

28
apps/fontclock/README.md Normal file
View File

@ -0,0 +1,28 @@
# Font Clock
The Font Clock allows you to choose the font and clock style.
![](app.png)
## Usage
### Choose the Clock Face from the selection
Before uploading the upload page will ask which clock face you like to choose. Please choose using the provided pull down. As you look through the different selections a sample image will be shown to the right hand side.
Once you have chosen your watch face press the upload button and the selection will be uploaded to the watch
### Button 3
Button 3 (bottom right button) is used to change the background colour.
## Further Details
For further details of design and working please visit [The Project Page](https://www.notion.so/adrianwkirk/Sweep-hand-clock-6aa5b6b3d1074d4e87fc947975b1e4b7)
## Requests
Reach out to adrian@adriankirk.com if you have feature requests or notice bugs.
## Creator
Made by [Adrian Kirk](mailto:adrian@adriankirk.com)

BIN
apps/fontclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

210
apps/fontclock/custom.html Normal file
View File

@ -0,0 +1,210 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<p>Please select watch display</p>
<table>
<tr>
<td>
<select id="display_selection" name="display_selection" onchange="change_image()" >
</select>
</td>
<td>
<img id="selected_image" src="display-01.png">
</td>
</tr>
</table>
<p>Click <button id="upload" class="btn btn-primary">Upload</button></p>
<script src="../../core/lib/customize.js"></script>
<script>
function change_image() {
var idx = document.getElementById("display_selection").selectedIndex;
set_image(idx);
}
function set_image(idx){
var image = document.getElementById('selected_image');
image.src = "display-0" + (idx + 1) + ".png";
}
var displays_choices=[
{
name: "Abril FatFace 4",
numerals: [12,3,6,9],
fonts: ["abril_ff50"],
radius: 80,
color_schemes : [
{
name: "black",
background : [0.0,0.0,0.0],
second_hand: [1.0,1.0,0.0]
},
{
name: "red",
background : [1.0,0.0,0.0],
second_hand: [1.0,1.0,0.0]
},
{
name: "grey",
background : [0.5,0.5,0.5],
},
{
name: "purple",
background : [1.0,0.0,1.0]
},
{
name: "blue",
background : [0.4,0.7,1.0]
}
]
},
{
name: "Montoon 4",
numerals: [12,3,6,9],
fonts: ["mntn50"],
radius: 80,
color_schemes : [
{
name: "black",
background : [0.0,0.0,0.0],
second_hand: [1.0,1.0,0.0]
},
{
name: "grey",
background : [0.5,0.5,0.5]
}
]
},
{
name: "Vector 12",
numerals: [12,1,2,3,4,5,6,7,8,9,10,11],
fonts: ["vector25"],
radius: 90,
color_schemes : [
{
name: "black",
background : [0.0,0.0,0.0],
second_hand: [1.0,0.0,0.0]
},
{
name: "grey",
background : [0.5,0.5,0.5],
second_hand: [0.0,0.0,0.0]
}
]
},
{
name: "Copaset 4",
numerals: [12,3,6,9],
fonts: ["cpstc58"],
radius: 75,
color_schemes : [
{
name: "black",
background : [0.0,0.0,0.0],
second_hand: [1.0,0.0,0.0]
},
{
name: "red",
background : [1.0,0.0,0.0],
second_hand: [1.0,1.0,0.0]
},
{
name: "grey",
background : [0.5,0.5,0.5],
second_hand: [0.0,0.0,0.0]
},
{
name: "purple",
background : [1.0,0.0,1.0]
},
{
name: "blue",
background : [0.4,0.7,1.0]
}
]
},
{
name: "Vector 4",
numerals: [12,3,6,9],
fonts: ["vector50"],
radius: 75,
color_schemes : [
{
name: "black",
background : [0.0,0.0,0.0],
second_hand: [1.0,0.0,0.0],
},
{
name: "red",
background : [1.0,0.0,0.0],
second_hand: [1.0,1.0,0.0]
},
{
name: "grey",
background : [0.5,0.5,0.5],
second_hand: [0.0,0.0,0.0]
},
{
name: "purple",
background : [1.0,0.0,1.0]
},
{
name: "blue",
background : [0.4,0.7,1.0]
}
]
}
];
var selected_choice = "Abril FatFace 4"
try{
var stored = localStorage.getItem('fontclock.font.json')
if(stored) {
var selected_config = JSON.parse(stored);
selected_choice = selected_config.name;
}
} catch(e){
console.log("failed to load languages:" + e);
}
console.log("selected choice:" + selected_choice);
var selection=document.getElementById("display_selection");
for (var i=0; i<displays_choices.length; i++) {
var option = document.createElement('option');
var curr_choice = displays_choices[i];
option.name = curr_choice.name;
option.text = curr_choice.name;
selection.add(option);
}
selection.value = selected_choice;
set_image(selection.selectedIndex)
// When the 'upload' button is clicked...
document.getElementById("upload").addEventListener("click", function() {
var new_config;
console.log("selection:" + selection.value);
for(var i=0; i<displays_choices.length; i++) {
if (displays_choices[i].name == selection.value) {
new_config = displays_choices[i];
console.log("new_config:" + JSON.stringify(new_config));
}
}
localStorage.setItem('fontclock.font.json',JSON.stringify(new_config));
// send finished app (in addition to contents of app.json)
sendCustomizedApp({
storage:[
{name:"fontclock.font.json", content:JSON.stringify(new_config)},
]
});
});
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("lEowkA/4AvmUiAA0/CRHzkczAA0vExM/n/zn8zAIPzCZUi/8j+cvmUzAgI7JBQITHkY6JCwRNEIYITIDoQSEExXyDoQSDn4mKHQ4mKLoImRHQQmPMIYTDExY6HExY6HExQ6HYgISJHQ4TBAgbXOAAb3Ba5giBn8/H4zXHMYfzEww6I+cyPJAtEToizBNoQTFLo0yBAKMI+UikUjIwQSBJg61ICALGMPQgQBJhB6IbJjcGJhw6DCQJMMUIhMOHQavBCRo6CJh46DTJo6EJh5eCTJwADdwISQJiIAo"))

View File

@ -0,0 +1,51 @@
var NumeralFont = require("fontclock.font.js");
const DIM_30x38 = [30,38];
const DIM_49x38 = [49,38];
class DigitNumeralFont extends NumeralFont{
constructor(){
super();
// dimension map provides the dimensions of the character for
// each number for plotting and collision detection
this.widths = atob("DRIhFRwdHhsfGh8fDQ==");
this.font = atob("AAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAD/AAAAAAB/4AAAAAAf+AAAAAAH/gAAAAAB/4AAAAAAf+AAAAAAD/AAAAAAA/gAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAAAPwAAAAAAf8AAAAAA/+AAAAAB/8AAAAAD/wAAAAAH/gAAAAAP/AAAAAAf+AAAAAA/8AAAAAB/4AAAAAD/wAAAAAH/gAAAAAP/AAAAAAH+AAAAAAB8AAAAAAAIAAAAAAAAAAAAAAAAAAH/8AAAAAP//8AAAAP///wAAAH///+AAAD////4AAB/////AAA/////wAAf////+AAH/////wAD/////8AA//////AAP/gAA/wADwAAAAeAA4AAAADgAMAAAAA4ADAAAAAOAAwAAAADgAMAAAAA4ADgAAAAeAA/gAAA/AAP/////wAD/////8AAf/////AAH/////gAA/////4AAH////8AAB////+AAAP////AAAA////gAAAD///gAAAAH//AAAAAAAAAAAAGAAAAAwABgAAAAMAAYAAAADAAGAAAAAwADgAAAAMAA//////AAP/////wAD/////8AA//////AAP/////wAD/////8AA//////AAP/////wAD/////8AA//////AAP/////wAAAAAAAMAAAAAAADAAAAAAAAwAAAAAAAMAAAAAAAAAAAAAAAAAAAHwAAD8AAH/AAB/AAD/wAA/wAA/+AAf8AAf/gAP/AAH/4AH/wAD/+AD/8AA//gB//AAOPwA//wADD4Aff8AAwAAPn/AAMAAPx/wADAAH8f8AA4AH+H/AAPgP/B/wAD///gf8AA///4H/AAP//8B/wAD//+Af8AAf//AH/AAH//wB/wAA//4A/8AAH/4Af/AAA/8A//wAAD8Af/8AAAAAD+AAAAAAAAAAAAAAAAAAAAAAAAPwAAA/gAP+AAAf8AD/wAAP/gB/+AAH/4Af/wAB/+AH/8AAf/gB//AAP/4wP/wAD/8OD+OAAw/DgPDgAMDAwAA4ADAAcAAOAAwAHAADgAOAH4AA4AD///AAeAA///+A/AAP/////wAD/////8AA//9///AAP//f//gAB//n//4AAf/w//+AAD/8P//AAAf+B//gAAB+AP/wAAAAAB/4AAAAAADwAAAAAAAAAAAAAAAeAAAAAAAfgAAAAAAf4AAAAAAPmAAAAAAPhgAAAAAPwYAAAAAPwGAAAAAHwBgAAAAHwAYDAAAH4AGAwAAH4ABgMAAH4AAYDAAD4AAGAwAD/////8AA//////AAP/////wAD/////8AA//////AAP/////wAD/////8AA//////AAP/////wAD/////8AAAAAAYDAAAAAAGAwAAAAABgMAAAAAAYBAAAAAB/4AAAAAAf+AAAAAAAAAAAAAAAD4AAAAAAB/gAAAAAA/8AAP//wf/gAD//8H/4AA//3B//AAP8Bgf/wAD/A4D/8AAfwOA/jgAH8DAH44AB/gwAAOAAf4MAADgAH+DAAA4AB/g4AAeAAf8PwA/AAH/D///wAB/w///8AAf8P///AAD/j///wAA/4f//4AAP+H//+AAH/A///AAD/wP//gAA/gB//wAAAAAH/4AAAAAAfwAAAAAAAAAAAAAAAAAAAAAD//wAAAAH///AAAAH///8AAAD////gAAD////8AAB/////gAAf////4AAP/////AAH/////wAB/////8AA//////gAP8B4AD4AD4A4AAOAA4AMAADgAOAHAAA4ADABwAAOAAwAcAAHgAMAH4AP4ADD5///8AA5/f///AAP/////wAD/////8AAf/v//+AAH/7///AAA/+f//wAAH/H//4AAA/gf/4AAABgD/8AAAAAAH4AAAAAAAAAAAAAAAAAAAAf/wAAAAAP/8AAAAAD/8AAAAAA/8AAAAAAP+AAA/AAD/gAA/4AA/4AA//AAP+AAf/wAD/gAf/+AA/4AP//gAP+AH//4AD/gD//+AA/4B///gAP+B/+BwAD/g/8AAAA/4f8AAAAP+P8AAAAD/n8AAAAA/78AAAAAP/+AAAAAD/+AAAAAA/+AAAAAAP+AAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAB/gH/4AAA/8D//AAAf/h//wAAP/8f/+AAH//v//gAB//7//8AAf/////AAP/////wAD/////+AA//////gAP//+AB4ADgAeAAOAAwADgADgAMAA4AA4ADAAOAAOAA4AHwADgAP//+AD4AD/////+AA//////AAP/////wAD//7//8AAf/+///AAH//P//gAA//x//4AAH/4f/8AAA/8D/+AAAD8Af/AAAAAAB/AAAAAAAAAAAAA/gAAAAAA//APwAAA//8H+AAAf//j/wAAP//4/+AAD///f/gAB/////8AAf//+//AAP///v/wAD///7+eAA////PjgAPgAPwA4ADgAA4AOAAwAAOADgAMAADgA4ADAAA4AeAAwAAcAfgAPAAeA/wAD/////8AA//////AAP/////gAB/////wAAf////8AAD////+AAAf////AAAH////gAAAf///gAAAD///gAAAAH//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AH4AAAB/gD/AAAAf8B/4AAAP/Af+AAAD/wH/gAAA/8B/4AAAH/Af+AAAB/wD/AAAAP4A/gAAAAwABgAAAAAAAA==");
var scale = 1; // size multiplier for this font
this.size = 50+(scale<<8)+(1<<16);
this.y_offset = -12;
}
getDimensions(hour){
//return this.dimension_map[hour];
switch (hour){
case 10:
case 11:
case 12:
return DIM_49x38;
default:
return DIM_30x38;
}
}
hour_txt(hour){ return hour.toString(); }
draw(hour_txt,x,y){
/* going to leave this in here for future testing.
uncomment this so that it draws a box behind the string
so we can guess the digit dimensions*/
/*var dim = [30,38];
g.setColor(0.5,0,0);
g.fillPoly([x,y,
x+dim[0],y,
x+dim[0],y+dim[1],
x,y+dim[1]
]);
g.setColor(1.0,1.0,1.0);*/
g.setFontAlign(-1.0,-1.0,0);
g.setFontCustom(this.font, 46, this.widths, this.size);
g.drawString(hour_txt,x,y+this.y_offset );
}
getName(){return "Digit";}
}
module.exports = [DigitNumeralFont];

View File

@ -0,0 +1,59 @@
var NumeralFont = require("fontclock.font.js");
const DIM_20x58 = [20,58];
const DIM_30x58 = [30,58];
const DIM_40x58 = [40,58];
const DIM_50x58 = [50,58];
class DigitNumeralFont extends NumeralFont{
constructor(){
super();
// dimension map provides the dimesions of the character for
// each number for plotting and collision detection
this.font = atob("AAAA/+AAAAAAB///wAAAAB////8AAAA/////+AAAP/////8AAD//////8AAf/8AAf/8AD/8AAAH/4Af+AAAAD/wD/gAAAAD/gf4AAAAAH+D/AAAAAAP8P4AAAAAAf5/AAAAAAA/n4AAAAAAB+/gAAAAAAH/+AAAAAAAf/wAAAAAAA//AAAAAAAD/8AAAAAAAP/4AAAAAAB//gAAAAAAH9+AAAAAAAfn8AAAAAAD+fwAAAAAAP4/gAAAAAB/D/AAAAAAP8H/AAAAAB/gP+AAAAAf8Af/AAAAH/gA//gAAD/8AB//8AH//gAB//////8AAB//////AAAB/////wAAAA////4AAAAAP//4AAAAAAAAAAAAAAGAAAAAAAAA8AAAAAAAAH8AAAAAAAA/wAAAAAAAH+AAAAAAAA/wAAAAAAAH+AAAAAAAA/wAAAAAAAH////////w/////////H////////8/////////3//////////////////8AAAAAAAAAAAAAAAAAADAAAAAAAAAcQAAAAAAAHzwAAAAAAA/PwAAAAAAH9/AAAAAAB/34AAAAAAP/fgAAAAAD//+AAAAAAf//wAAAAAH///AAAAAA///8AAAAAH///4AAAAB/4//gAAAAP/D/+AAAAD/wP34AAAAf+A/fwAAAD/wD8/gAAA/8APz/AAAH/gA/H+AAB/4AD8f8AAP/AAPw/8AD/wAA/B/+A/+AAD8D////wAAPwH///8AAA/AH///gAAD8AH//4AAAPwAH/+AAAAAAAAAAAAAAD8AAAAAAAAPwAAAAAAAA/AAgAAAAD/8AHAAAAAP/wB8AAAAA//APwAAAAD/8D/AAAAAP/wf8AAAAB//H/wAAAAH/8//gAAAAfv//+AAAAD+///8AAAAP7//fwAAAB/P/w/gAAAP8/+D/AAAB/j/wH+AAAP8P8AP8AAB/w/gAf8AAf+D4AA/8AH/wPAAB////+AwAAD////gCAAAH///8AAAAAH///AAAAAAD//wAAAAAAA/wAAAAAAAAAAAAAAAAAAAQAAAAAAAAHAAAAAAAAD8AAAAAAAA/wAAAAAAAP/AAAAAAAD/8AAAAAAB//wAAAAAAf//AAAAAAH//8AAAAAB//PwAAAAAf/w/AAAAAP/8D8AAAAD//APwAAAA//gA/AAAAP/4AD8AAAH/+AAPwAAB//gAA/AAAf/4AAD8AAH/8AAAPwAD//AAAA/AAP/wAAAD8AA/8AAAAPwAD/AAAAA/gAPgAAA/////4AAAD////+AAAAP////wAAAA/////AAAAD////8AAAAAA/AAAAAAAAD8AAAAAAAAPwAAAAAAAAAAAAAA8AAAAAAAB/wAAAAAAD//AAAAAP///8AAAAA////wAAAAD////AAAAAP///8AAAAA//4PwAAAAH/8A/gAAAAf/wB+AAAAB+/AH4AAAAP78AfwAAAA/vwA/AAAAH8/AD+AAAA/z8AP8AAAD+PwAf4AAAf4/AA/wAAH/D8AD/gAA/4PwAH/gAf/A/AAP/8f/4AAAAf////AAAAAf///wAAAAA///8AAAAAAf//AAAAAAAH/gAAAAAAAAAAAAAAAAH/4AAAAAAP//8AAAAAD///+AAAAB////8AAAAf////8AAAH//8f/4AAA//8AD/wAAP//AAD/gAB//wAAH/AAP/+AAAH+AB//wAAAP4AP/+AAAAfwB//wAAAB/AP9/AAAAD+B/n4AAAAH4H8/gAAAAfg/j8AAAAB/H8PwAAAAH8fw/AAAAAPz+D8AAAAA/P4PwAAAAD9/A/AAAAAf38D8AAAAB/fgP4AAAAH5+A/gAAAAfv4B/AAAAD+/gH8AAAAPz+AP4AAAB/PwA/wAAAP8/AB/gAAB/gAAD/AAAP8AAAP+AAD/gAAAf+AA/8AAAA//gf/gAAAA////8AAAAB////gAAAAB///4AAAAAB//+AAAAAAA//AAAAAAAAAAAAPwAAAAAAAA/AAAAAAAAD8AAAAAAAAfwAAAAAAAP/AAAAAAAH/8AAAAAAD//wAAAAAD///AAAAAB///8AAAAA///vwAAAA///w/AAAAP//wD8AAAP//4APwAAH//8AA/AAD//+AAD8AD//+AAAPwB///AAAA/A///gAAAD8f//wAAAAP///4AAAAA///4AAAAAD//8AAAAAAP/+AAAAAAA/+AAAAAAAD/AAAAAAAAPgAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAH//wAAAAAB///wAAAAAf///wAAAAD////wAAAAf////gAAAH/wAf/AAAA/8AAP+AAAD/AAAf8AAAf4AAAf4AAD/AAAA/gB/P4AAAB/A///AAAAH8H//8AAAAP4///gAAAAfn//+AAAAB+f//wAAAAH/+B/AAAAAf/4H8AAAAA//APwAAAAD/8A/AAAAAP/4H8AAAAB/fw/wAAAAH9///gAAAAfj//+AAAAB+P//4AAAAP4P//wAAAA/gf//gAAAH8APD/AAAA/wAAH8AAAH+AAAf8AAA/wAAA/4AAH/AAAB/4AB/4AAAD/8A//AAAAH////wAAAAP///+AAAAAP///gAAAAAP//4AAAAAAH/+AAAAAAAAAAAAAAA/wAAAAAAA//8AAAAAAP//+AAAAAD///8AAAAA////8AAAAH////4AAAA/+AD/wAAAH/AAD/gAAA/4AAD/AAAH+AAAH+AAAfwAAAP8APz+AAAAfwA/P4AAAA/gH9/AAAAD+Af34AAAAH4B+fgAAAAfwH7+AAAAA/A/v4AAAAD8D+/AAAAAPwPz8AAAAA/B/PwAAAAD8P8/gAAAAPw/j+AAAAA/H8H4AAAAH8/wfgAAAAf3+B/AAAAB+fwH8AAAAP//AP4AAAB//4A/wAAAH//AB/gAAA//4AD/AAAP//AAH+AAB//wAAf+AAf/+AAAf/AP//gAAA/////8AAAB/////AAAAB////wAAAAB///4AAAAAB//4AAAAAAAAAAAAAAA=");
this.widths = atob("Jg8dGiAaKBsoKA==");
}
getDimensions(hour){
switch(hour){
case 1:
return DIM_20x58;
case 2:
case 3:
case 4:
case 5:
case 7:
return DIM_30x58;
case 6:
case 8:
case 9:
case 11:
case 12:
return DIM_40x58;
case 10:
return DIM_50x58;
default:
return DIM_30x58;
}
}
hour_txt(hour){ return hour.toString(); }
draw(hour_txt,x,y){
/* going to leave this in here for future testing.
uncomment this so that it draws a box behind the string
so we can guess the digit dimensions
dim = [50,58];
g.setColor(0.5,0,0);
g.fillPoly([x,y,
x+dim[0],y,
x+dim[0],y+dim[1],
x,y+dim[1]
]);
g.setColor(1.0,1.0,1.0);*/
//g.setFontCopasetic40x58Numeric();
//g.setFontAlign(-1,-1,0);
g.setFontAlign(-1,-1,0);
g.setFontCustom(this.font, 48, this.widths, 58);
g.drawString(hour_txt,x,y);
}
getName(){return "Digit";}
}
module.exports = [DigitNumeralFont];

View File

@ -0,0 +1,26 @@
/**
* We want to be able to change the font so we set up
* pure virtual for all fonts implementtions to use
*/
class NumeralFont {
/**
* The screen dimensions of what we are going to
* display for the given hour.
*/
getDimensions(hour){return [0,0];}
/**
* The characters that are going to be returned for
* the hour.
*/
hour_txt(hour){ return ""; }
/**
* method to draw text at the required coordinates
*/
draw(hour_txt,x,y){ return "";}
/**
* Called from the settings loader to identify the font
*/
getName(){return "";}
}
module.exports = NumeralFont;

View File

@ -0,0 +1,23 @@
{
"name": "Vector 4",
"numerals": [12,3,6,9],
"fonts": ["vector50"],
"radius": 75,
"color_schemes" : [
{
"name": "black",
"background" : [0.0,0.0,0.0],
"second_hand": [1.0,0.0,0.0],
},
{
"name": "red",
"background" : [1.0,0.0,0.0],
"second_hand": [1.0,1.0,0.0]
},
{
"name": "grey",
"background" : [0.5,0.5,0.5],
"second_hand": [0.0,0.0,0.0]
}
]
}

View File

@ -0,0 +1,60 @@
var NumeralFont = require("fontclock.font.js");
const DIM_25x25 = [25,25];
const DIM_10x25 = [10,25];
const DIM_20x25 = [20,25];
const DIM_31x25 = [31,25];
const DIM_15x25 = [15,25];
class DigitNumeralFont extends NumeralFont{
constructor(){
super();
// dimension map provides the dimensions of the character for
// each number for plotting and collision detection
this.widths = atob("BgsVCw8PEBEUEBQUBw==");
this.font = atob("AAAAAAAAAAAAp9bgAAAAAAAAAAAADr+vAAAAAAAAAAAAAOv68AAAAAAAAAAAAA6/rwAAAAAAAAAAAADr+fAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAXwAAAAAAAAAAAAXz//wAAAAAAAAAXz//7q+AAAAAAAXz//8q+//wAAAAXz//8q+//yr3wAXz//8q+//yr3//ZAL/8q+//yr3//ZMAAAW+//2q3//ZQAAAAAC/2q3//pQAAAAAAAAF3//pQAAAAAAAAAAAnpQAAAAAAAAAAAAAAAAABL3//tgQAAAAAAAAj//su9//0gAAAAAC7/vv///73/gAAAAB/8/9u7u7/+34AAAA78/r/////c/+9QAAf9/P/LvLu/+///AADu/++//+7/7Pv79QAv37+/sQAAb/7978AF/f3vwAAAAD+/v+4Aj9779QAAAADs+/vwCP3vv1AAAAAOz7+/AF/P3fsAAAAC+/v94AL9+/v5AAAD/9/f/QAP3+/8/9ze/7+/v2AAj9+//Lztu+/P//AAAP/f3P////6//fcAAAL/z/y7u7vv7PoAAAAD/+z////9z/oAAAAAAK//3LvO/+QAAAAAAAAH3///6zAAAAAAAAAAAAAAAAAAAAAAC96fQAAAAAAAAAAAAL769AAAAAAAAAAAAAvvr5ZmZmZmZmZmAAC++v//////////8AAL76/bu7u7u7u7uwAAvvr/7u7u7u7u7uAAC++v/u7u7u7u7u4AAL76/KqqqqqqqqqgAAvvr///////////AAAjQlVVVVVVVVVVUAAAAAAAAAAAAAAAAAAAhTAAAAAAAAAyUlAAD7+udQAAAAHfv68AAPv777AAAAX/+/rwAC+/y/kAAAn+77+vAAb8/q9gAB79//v68ACf7frzAF/9/t+/rwAH/d+/QK/u/P/7+vAAX8/t/u/f/P7Pv68AAvv7+uzv3vz/+/rwAA/P3///z/v/Pr+vAACPv9u67939EOv68AAA/6///7/3AA6/rwAAAv/Ku+/iAADr+fAAAACu//5gAAAAAAAAAAAAAAAAAAAAAAAAAAhTAAAAAAAAAAIrIAD7+udgAAAAGo379gAPr7/rAAAAAfv935AB+vvvcjMkFQ/Pv+sAT6/d9K/r+vDs+/3QB/zvvzr+v68Nz7+/AJ/d+/Ov6/rx3Pv78Ab779+I/N/PX8+/zwAvv8/P/5/v7/z7/+AA+/z+m/r7/Kv9/PkACvv7///8+//7398gAB/9/bvvzvy8/89wAABv++/93+nf/b+gAAAALv/e/9//3v+wAAAAAASd21AVrcogAAAAAAAAAAAb753JAAAAAAAAAALP/frusAAAAAAAAE3/zO+u6wAAAAAABe/7z/367rAAAAAAf/+9/7vvrusAAAAG/+rv+s/9+u6wAAAAra//rf+5367rAAAABv/q7/q//vrusAAAAK2v/5z/w1+u6wAAAAb/6d/7IAX67rAAAACsr/+AL//vru//4AAG/+YAAaqr+u7aqgAAnUAAAD///67v//AAAAAAAABVWPruxVUAAAAAAAAAACtphgAAAAAAAAAAAAAAAAAAAAA0U3d3d3d1AADMAAAL76//////0ACr9AAAvvr9zMzMyABd/vAAC++v7u7u7qAPz79QAL76//////wPz975AAvvr8rN7Oyw+vv+wAC++vQN37/ODs+/vgAL769A6/v9wN37+/AAvvr0Dq+/3Q/Pv78AC++vQN38/vv8+/3QAL769Ar8397+7/36AAvvr0Bfv8/s/7/PMAC++vQA77+9/a+fwAAL769ABP3f///f8gAAVnSRAAb/mrzP8wAAAAAAAAAC3///wQAAAAAAAAAAAAJiAAAAAAAAAAA2ZmZiAAAAAAAAAK7//////+YAAAAAA//Lu7u7up77AAAABP+//+7u7v/5/QAAAP7fyN///+y/+/cAAH+/n/2qqqvv7vzwAA/f3+r/////v8+/cAD7+/v82rye38/e6wBPv9zrn9388fv7/NAI/O+va+/Pzw3Pv68Aj93689z7/dDc+vrwBPv+v06/z90Pz7++AA+/3vnO/Pz+/PvuwAD7+/o4/O/56/38+QAN/v5QP8/f//7PzxAAP89QAL+f3czfj6AAAK9wAAH/v///z/EAAADQAAAC79q879EAAAAAAAAAAJ3//YAAAAAAAAAAAAAAAAAAAAAL3p9AAAAAAAAAAAAAvvr0AAAAAAAAAAAAC++vQAAAAAAAAAAAAL769AAAAAAAAFrgAAvvr0AAAAAFrv/9AAC++vQAAFvv/9u98AAL769Wvv/8u9//6gAArN7//8u+//67z/AACv/8u+//27z//roAAFu+//273//rvP/wAAv/273//rvP//xxAABb3//rvP//xxAAAAAL/rvP//thAAAAAAAAXP/+thAAAAAAAAAACutgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGt/qYCvv2kAAAAAAb/69/+/+vP/AAAAAT+z//q/8//6/8QAAP8/7u9/P+7z/v7AADv36//+/v///778gAfv7/Lr/7++3/vz6AG+/7+/9+/v//Pz+0Aj8789d77+/Ds+/zwCf3Pryvuv68N36+vAJ/c+vK+6/rwzfr68An9z68r7r+vDN+vrwCPzvz1v+z78ez6+/AE+/7//fzv3+/Pv98AD7+/ne+/v57e/e/QAO7v3//9/u/v+vr2AAT9/7u9+v68uvz/AAAM/O////v///z/EAAACv+6vP/+u6z/IAAAAATP//1H3//7IAAAAAAAABAAAAEAAAAAAAAABJkwAAAAAAAAAAAAr///+gAAAAEQAAAB783dvP0QAADNAAAA37/93/n8AAC79QAAT5/c/9358wBt/vAADu/v+8/+79Afz79QAPn7+//Pv58Pv975AD+f7/zP3fjw+vv+wAf7789T+/6vTs+/vgCf3fvyP8/789z7+/AG+++/SP7Pvw+fr74AL5/e+837+/X3+v3AAPv7+//d3d797/35AA3+7/vMzMzK75+/MAA/r97//////r/fwAAAz5/7uqqqqt/d8gAAAe/N//////6v9gAAAACv/bqqqr3/4gAAAAAAOM/////aUAAAAAAAAAAAAAAAAAAAAAAAAAAQEQABARAAAAAAAABvvuoF+u6wAAAAAAAG++6gX67rAAAAAAAAb77qBfrusAAAAAAABvvuoF+u6wAAAAAAAE16pwPXunAAAAAAAAAAAAAAAAAAAA==");
var scale = 1; // size multiplier for this font
this.size = 25+(scale<<8)+(4<<16);
this.y_offset = 0;
}
getDimensions(hour){
//return this.dimension_map[hour];
switch(hour){
case 0:
case 12:
return DIM_25x25;
case 1:
return DIM_10x25;
case 6:
case 8:
case 9:
case 11:
return DIM_20x25;
case 10:
return DIM_31x25;
default:
return DIM_15x25;
}
}
hour_txt(hour){ return hour.toString(); }
draw(hour_txt,x,y){
/* going to leave this in here for future testing.
uncomment this so that it draws a box behind the string
so we can guess the digit dimensions*/
/*var dim = [30,25];
g.setColor(0.5,0,0);
g.fillPoly([x,y,
x+dim[0],y,
x+dim[0],y+dim[1],
x,y+dim[1]
]);
g.setColor(1.0,1.0,1.0);*/
g.setFontAlign(-1.0,-1.0,0);
g.setFontCustom(this.font, 46, this.widths, this.size);
g.drawString(hour_txt,x,y+this.y_offset );
}
getName(){return "Digit";}
}
module.exports = [DigitNumeralFont];

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,39 @@
var NumeralFont = require("fontclock.font.js");
const DIM_14x22 = [14,22];
const DIM_27x22 = [27,22];
class DigitNumeralFont extends NumeralFont{
constructor(){
super();
}
getDimensions(hour){
if (hour < 10){
return DIM_14x22;
} else {
return DIM_27x22;
}
}
hour_txt(hour){ return hour.toString(); }
draw(hour_txt,x,y){
if(hour_txt == null)
return;
/* going to leave this in here for future testing.
uncomment this so that it draws a box behind the string
so we can guess the digit dimensions
var dim = [14,22];
g.setColor(0.5,0,0);
g.fillPoly([x,y,
x+dim[0],y,
x+dim[0],y+dim[1],
x,y+dim[1]
]);
g.setColor(1.0,1.0,1.0);*/
g.setFontAlign(-1,-1,0);
g.setFont("Vector",25);
g.drawString(hour_txt,x,y);
}
getName(){return "Digit";}
}
module.exports = [DigitNumeralFont];

View File

@ -0,0 +1,91 @@
var NumeralFont = require("fontclock.font.js");
const DIM_28x44 = [28,44];
const DIM_54x44 = [54,44];
class DigitNumeralFont extends NumeralFont{
constructor(){
super();
}
getDimensions(hour){
if (hour < 10){
return DIM_28x44;
} else {
return DIM_54x44;
}
}
hour_txt(hour){ return hour.toString(); }
draw(hour_txt,x,y){
if(hour_txt == null)
return;
/* going to leave this in here for future testing.
uncomment this so that it draws a box behind the string
so we can guess the digit dimensions
var dim = [14,22];
g.setColor(0.5,0,0);
g.fillPoly([x,y,
x+dim[0],y,
x+dim[0],y+dim[1],
x,y+dim[1]
]);
g.setColor(1.0,1.0,1.0);*/
g.setFontAlign(-1,-1,0);
g.setFont("Vector",50);
g.drawString(hour_txt,x,y);
}
getName(){return "Digit";}
}
const DIM_50x40 = [50,40];
const DIM_70x40 = [70,40];
class RomanNumeralFont extends NumeralFont{
constructor(){
super();
}
getText(hour){
switch (hour){
case 1 : return 'I';
case 2 : return 'II';
case 3 : return 'III';
case 4 : return 'IV';
case 5 : return 'V';
case 6 : return 'VI';
case 7 : return 'VII';
case 8 : return 'VIII';
case 9 : return 'IX';
case 10: return 'X';
case 11: return 'XI';
case 12: return 'XII';
default: return '';
}
}
getDimensions(hour){
switch (hour){
case 3:
case 6:
case 9:
return DIM_50x40;
case 12:
return DIM_70x40;
default:
return DIM_70x40;
}
}
hour_txt(hour){ return this.getText(hour); }
draw(hour_txt,x,y){
/*var dim = DIM_70x40;
g.setColor(0.5,0,0);
g.fillPoly([x,y,
x+dim[0],y,
x+dim[0],y+dim[1],
x,y+dim[1]
]);*/
g.setFontAlign(-1,-1,0);
g.setFont("Vector",50);
g.drawString(hour_txt,x,y);
}
getName(){return "Roman";}
}
module.exports = [DigitNumeralFont,RomanNumeralFont];

View File

@ -0,0 +1,10 @@
class Hand {
/**
* Pure virtual class for all Hand classes to extend.
* a hand class will have 1 main function
* moveTo which will move the hand to the given angle.
*/
moveTo(angle){}
}
module.exports = Hand;

View File

@ -0,0 +1,137 @@
const TWO_PI = 2* Math.PI;
// The problem with the trig inverse functions on
// a full circle is that the sector information will be lost
// Choosing to use arcsin because you can get back the
// sector with the help of the original coordinates
function reifyasin(x,y,asin_angle){
if(x >= 0 && y >= 0){
return asin_angle;
} else if(x >= 0 && y < 0){
return Math.PI - asin_angle;
} else if(x < 0 && y < 0){
return Math.PI - asin_angle;
} else {
return TWO_PI + asin_angle;
}
}
// rebase and angle so be between -pi and pi
// rather than 0 to 2PI
function rebaseNegative(angle){
if(angle > Math.PI){
return angle - TWO_PI;
} else {
return angle;
}
}
// rebase an angle so that it is between 0 to 2pi
// rather than -pi to pi
function rebasePositive(angle){
if(angle < 0){
return angle + TWO_PI;
} else {
return angle;
}
}
/**
* The Hour Scriber is responsible for drawing the numeral
* on the screen at the requested angle.
* It allows for the font to be changed on the fly.
*/
class HourScriber {
constructor(radius, numeral_font, draw_test, bg_colour_supplier, numeral_colour_supplier, hour){
this.radius = radius;
this.numeral_font = numeral_font;
this.draw_test = draw_test;
this.curr_numeral_font = numeral_font;
this.bg_colour_supplier = bg_colour_supplier;
this.numeral_colour_supplier = numeral_colour_supplier;
this.hours = hour;
this.curr_hour_x = -1;
this.curr_hour_y = -1;
this.curr_hours = -1;
this.curr_hour_str = null;
this.last_draw_time = null;
}
setNumeralFont(numeral_font){
this.numeral_font = numeral_font;
}
toString(){
return "HourScriber{numeralfont=" + this.numeral_font.getName() + ",hours=" + this.hours + "}";
}
draw(){
var changed = false;
if(this.curr_hours != this.hours || this.curr_numeral_font !=this.numeral_font){
var background = this.bg_colour_supplier();
g.setColor(background[0],background[1],background[2]);
this.curr_numeral_font.draw(this.curr_hour_str,
this.curr_hour_x,
this.curr_hour_y);
//console.log("erasing old hour display:" + this.curr_hour_str + " color:" + background);
var hours_frac = this.hours / 12;
var angle = TWO_PI*hours_frac;
var dimensions = this.numeral_font.getDimensions(this.hours);
// we set the radial coord to be in the middle
// of the drawn text.
var width = dimensions[0];
var height = dimensions[1];
var delta_center_x = this.radius*Math.sin(angle) - width/2;
var delta_center_y = this.radius*Math.cos(angle) + height/2;
this.curr_hour_x = screen_center_x + delta_center_x;
this.curr_hour_y = screen_center_y - delta_center_y;
this.curr_hour_str = this.numeral_font.hour_txt(this.hours);
// now work out the angle of the beginning and the end of the
// text box so we know when to redraw
// bottom left angle
var x1 = delta_center_x;
var y1 = delta_center_y;
var r1 = Math.sqrt(x1*x1 + y1*y1);
var angle1 = reifyasin(x1,y1,Math.asin(x1/r1));
// bottom right angle
var x2 = delta_center_x;
var y2 = delta_center_y - height;
var r2 = Math.sqrt(x2*x2 + y2*y2);
var angle2 = reifyasin(x2,y2,Math.asin(x2/r2));
// top left angle
var x3 = delta_center_x + width;
var y3 = delta_center_y;
var r3 = Math.sqrt(x3*x3 + y3*y3);
var angle3 = reifyasin(x3,y3, Math.asin(x3/r3));
// top right angle
var x4 = delta_center_x + width;
var y4 = delta_center_y - height;
var r4 = Math.sqrt(x4*x4 + y4*y4);
var angle4 = reifyasin(x4,y4,Math.asin(x4/r4));
if(Math.min(angle1,angle2,angle3,angle4) < Math.PI && Math.max(angle1,angle2,angle3,angle4) > 1.5*Math.PI){
angle1 = rebaseNegative(angle1);
angle2 = rebaseNegative(angle2);
angle3 = rebaseNegative(angle3);
angle3 = rebaseNegative(angle4);
this.angle_from = rebasePositive( Math.min(angle1,angle2,angle3,angle4) );
this.angle_to = rebasePositive( Math.max(angle1,angle2,angle3,angle4) );
} else {
this.angle_from = Math.min(angle1,angle2,angle3,angle4);
this.angle_to = Math.max(angle1,angle2,angle3,angle4);
}
//console.log(angle1 + "/" + angle2 + " / " + angle3 + " / " + angle4);
//console.log( this.angle_from + " to " + this.angle_to);
this.curr_hours = this.hours;
this.curr_numeral_font = this.numeral_font;
changed = true;
}
if(changed ||
this.draw_test(this.angle_from, this.angle_to, this.last_draw_time) ){
var numeral_color = this.numeral_colour_supplier();
g.setColor(numeral_color[0],numeral_color[1],numeral_color[2]);
this.numeral_font.draw(this.curr_hour_str,this.curr_hour_x,this.curr_hour_y);
this.last_draw_time = new Date();
//console.log("redraw digit:" + this.hours);
}
}
}
module.exports = HourScriber;

436
apps/fontclock/fontclock.js Normal file
View File

@ -0,0 +1,436 @@
/**
* Adrian Kirk 2021-03
* Simple Clock showing 1 numeral for the hour
* with a smooth sweep second.
*/
var ThinHand = require("fontclock.thinhand.js");
var ThickHand = require("fontclock.thickhand.js");
var HourScriber = require("fontclock.hourscriber.js");
const screen_center_x = g.getWidth()/2;
const screen_center_y = 10 + (g.getHeight()+10)/2;
const TWO_PI = 2* Math.PI;
SETTING_PREFIX = "fontclock";
// load the date formats and languages required
const FONTS_FILE = SETTING_PREFIX +".font.json";
const DEFAULT_FONTS = [ "cpstc58" ];
const DEFAULT_NUMERALS = [12,3,6,9];
const DEFAULT_RADIUS = 70;
var color_schemes = [
{
name: "black",
background : [0.0,0.0,0.0],
}
];
var fonts = DEFAULT_NUMERALS;
var numerals = DEFAULT_NUMERALS;
var radius = DEFAULT_RADIUS;
var fonts_info = null;
try {
fonts_info = require("Storage").readJSON(FONTS_FILE);
} catch(e){
console.log("failed to load fonts file:" + FONTS_FILE + e);
}
if(fonts_info != null){
console.log("loaded font:" + JSON.stringify(fonts_info));
fonts = fonts_info.fonts;
numerals = fonts_info.numerals;
radius = fonts_info.radius;
color_schemes = fonts_info.color_schemes;
} else {
fonts = DEFAULT_FONTS;
numerals = DEFAULT_NUMERALS;
radius = DEFAULT_RADIUS;
console.log("no fonts loaded defaulting to:" + fonts);
}
if(fonts == null || fonts.length == 0){
fonts = DEFAULT_FONTS;
console.log("defaulting fonts to locale:" + fonts);
}
let color_scheme_index = 0;
// The force draw is set to true to force all objects to redraw themselves
let force_redraw = true;
let bg_colour_supplier = ()=>color_schemes[color_scheme_index].background;
var WHITE = [1.0,1.0,1.0];
function default_white(color){
if(color == null){
return WHITE
} else {
return color;
}
}
// The seconds hand is the main focus and is set to redraw on every cycle
let seconds_hand = new ThinHand(screen_center_x,
screen_center_y,
95,
0,
(angle, last_draw_time) => false,
bg_colour_supplier,
()=>default_white(color_schemes[color_scheme_index].second_hand));
// The minute hand is set to redraw at a 250th of a circle,
// when the second hand is ontop or slighly overtaking
// or when a force_redraw is called
const minute_hand_angle_tolerance = TWO_PI/25
let minutes_hand_redraw = function(angle, last_draw_time){
return force_redraw || (seconds_hand.angle > angle &&
Math.abs(seconds_hand.angle - angle) < minute_hand_angle_tolerance &&
new Date().getTime() - last_draw_time.getTime() > 500);
};
let minutes_hand = new ThinHand(screen_center_x,
screen_center_y,
80, minute_hand_angle_tolerance,
minutes_hand_redraw,
bg_colour_supplier,
()=>default_white(color_schemes[color_scheme_index].minute_hand));
// The hour hand is a thick hand so we have to redraw when the minute hand
// overlaps from its behind angle coverage to its ahead angle coverage.
let hour_hand_redraw = function(angle_from, angle_to, last_draw_time){
return force_redraw || (seconds_hand.angle >= angle_from &&
seconds_hand.angle <= angle_to &&
new Date().getTime() - last_draw_time.getTime() > 500);
};
let hours_hand = new ThickHand(screen_center_x,
screen_center_y,
40,
TWO_PI/600,
hour_hand_redraw,
bg_colour_supplier,
() => default_white(color_schemes[color_scheme_index].hour_hand),
5,
4);
function draw_clock(){
var date = new Date();
draw_background();
draw_hour_digits();
draw_seconds(date);
draw_mins(date);
draw_hours(date);
force_redraw = false;
}
// drawing the second the millisecond as we need the fine gradation
// for the sweep second hand.
function draw_seconds(date){
var seconds = date.getSeconds() + date.getMilliseconds()/1000;
var seconds_frac = seconds / 60;
var seconds_angle = TWO_PI*seconds_frac;
seconds_hand.moveTo(seconds_angle);
}
// drawing the minute includes the second and millisec to make the
// movement as continuous as possible.
function draw_mins(date,seconds_angle){
var mins = date.getMinutes() + date.getSeconds()/60 + date.getMilliseconds()/(60*1000);
var mins_frac = mins / 60;
var mins_angle = TWO_PI*mins_frac;
var redraw = minutes_hand.moveTo(mins_angle);
if(redraw){
//console.log("redraw mins");
}
}
function draw_hours(date){
var hours = (date.getHours() % 12) + date.getMinutes()/60 + date.getSeconds()/3600;
var hours_frac = hours / 12;
var hours_angle = TWO_PI*hours_frac;
var redraw = hours_hand.moveTo(hours_angle);
if(redraw){
//console.log("redraw hours");
}
}
let numeral_fonts = [];
for(var i=0; i< fonts.length; i++) {
var file = SETTING_PREFIX +".font." + fonts[i] + ".js"
console.log("loading font set:" + fonts[i] + "->" + file);
var loaded_fonts = require(file);
for (var j = 0; j < loaded_fonts[j]; j++) {
var loaded_font = new loaded_fonts[j];
numeral_fonts.push(loaded_font);
console.log("loaded font name:" + loaded_font.getName())
}
}
let numeral_fonts_index = 0;
const ONE_POINT_FIVE_PI = 1.5*Math.PI;
/**
* predicate for deciding when the digit has to be redrawn
*/
let hour_numeral_redraw = function(angle_from, angle_to, last_draw_time){
var seconds_hand_angle = seconds_hand.angle;
// we have to cope with the 12 problem where the
// left side of the box has a value almost 2PI and the right
// side has a small positive value. The values are rebased so
// that they can be compared
if(angle_from > angle_to && angle_from > ONE_POINT_FIVE_PI){
angle_from = angle_from - TWO_PI;
if(seconds_hand_angle > Math.PI)
seconds_hand_angle = seconds_hand_angle - TWO_PI;
}
//console.log("initial:" + angle_from + "/" + angle_to + " seconds " + seconds_hand_angle);
var redraw = force_redraw ||
(seconds_hand_angle >= angle_from && seconds_hand_angle <= angle_to && seconds_hand.last_draw_time.getTime() > last_draw_time.getTime()) ||
(minutes_hand.last_draw_time.getTime() > last_draw_time.getTime());
if(redraw){
//console.log(angle_from + "/" + angle_to + " seconds " + seconds_hand_angle);
}
return redraw;
};
// now add the numbers to the clock face
var numeral_colour_supplier = () => default_white(color_schemes[color_scheme_index].numeral);
var hour_scribers = [];
console.log("numerals:" + numerals + " length:" + numerals.length)
console.log("radius:" + radius)
for(var digit_idx=0; digit_idx<numerals.length; digit_idx++){
var digit = numerals[digit_idx];
var scriber = new HourScriber(radius,
numeral_fonts[numeral_fonts_index],
hour_numeral_redraw,
bg_colour_supplier,
numeral_colour_supplier,
digit
);
hour_scribers.push(scriber);
//console.log("digit:" + digit + "->" + scriber);
}
//console.log("hour_scribers:" + hour_scribers );
/**
* Called from button 1 to change the numerals that are
* displayed on the clock face
*/
function next_font() {
var curr_font = numeral_fonts_index;
numeral_fonts_index = numeral_fonts_index + 1;
if (numeral_fonts_index >= numeral_fonts.length) {
numeral_fonts_index = 0;
}
if (curr_font != numeral_fonts_index) {
console.log("numeral font changed")
for (var i = 0; i < hour_scribers.length; i++) {
hour_scribers[i].setNumeralFont(
numeral_fonts[numeral_fonts_index]);
}
force_redraw = true;
return true;
} else {
return false;
}
}
const hour_zone_angle = hour_scribers.length/TWO_PI;
function draw_hour_digits() {
if(force_redraw){
for(var i=0; i<hour_scribers.length; i++){
var scriber = hour_scribers[i];
//console.log("idx:" + i + "->" + scriber);
scriber.draw();
}
} else {
var hour_scriber_idx = (0.5 + (seconds_hand.angle * hour_zone_angle)) | 0;
if (hour_scriber_idx >= hour_scribers.length)
hour_scriber_idx = 0;
//console.log("angle:" + seconds_hand.angle + " idx:" + hour_scriber_idx);
if (hour_scriber_idx >= 0) {
hour_scribers[hour_scriber_idx].draw();
}
}
}
function draw_background(){
if(force_redraw){
background = color_schemes[color_scheme_index].background;
g.setColor(background[0],background[1],background[2]);
g.fillPoly([0,25,
0,240,
240,240,
240,25
]);
}
}
function next_colorscheme(){
var prev_color_scheme_index = color_scheme_index;
color_scheme_index += 1;
color_scheme_index = color_scheme_index % color_schemes.length;
//console.log("color_scheme_index=" + color_scheme_index);
force_redraw = true;
if(prev_color_scheme_index == color_scheme_index){
return false;
} else {
return true;
}
}
/**
* called from load_settings on startup to
* set the color scheme to named value
*/
function set_colorscheme(colorscheme_name){
console.log("setting color scheme:" + colorscheme_name);
for (var i=0; i < color_schemes.length; i++) {
if(color_schemes[i].name == colorscheme_name){
color_scheme_index = i;
force_redraw = true;
console.log("match");
break;
}
}
}
/**
* called from load_settings on startup
* to set the font to named value
*/
function set_font(font_name){
console.log("setting font:" + font_name);
for (var i=0; i < numeral_fonts.length; i++) {
if(numeral_fonts[i].getName() == font_name) {
numeral_fonts_index = i;
force_redraw = true;
console.log("match");
for (var j = 0; j < hour_scribers.length; j++) {
hour_scribers[j].setNumeralFont(numeral_fonts[numeral_fonts_index]);
}
break;
}
}
}
/**
* Called on startup to set the watch to the last preference settings
*/
function load_settings(){
try{
var file = SETTING_PREFIX + ".settings.json";
settings = require("Storage").readJSON(file);
if(settings != null){
console.log(file + " loaded:" + JSON.stringify(settings));
if(settings.color_scheme != null){
set_colorscheme(settings.color_scheme);
}
if(settings.font != null){
set_font(settings.font);
}
} else {
console.log(file + " not found - no settings to load");
}
} catch(e){
console.log("failed to load settings:" + e);
}
}
/**
* Called on button press to save down the last preference settings
*/
function save_settings(){
var settings = {
font : numeral_fonts[numeral_fonts_index].getName(),
color_scheme : color_schemes[color_scheme_index].name,
};
var file = SETTING_PREFIX + ".settings.json";
console.log(file + ": saving:" + JSON.stringify(settings));
require("Storage").writeJSON(file,settings);
}
// Boiler plate code for setting up the clock,
// below
let intervalRef = null;
function clearTimers(){
if(intervalRef) {
clearInterval(intervalRef);
intervalRef = null;
}
}
function startTimers(){
setTimeout(scheduleDrawClock,100);
draw_clock();
}
// The clock redraw is set to 100ms. This is the smallest number
// that give the (my) human eye the illusion of a continious sweep
// second hand.
function scheduleDrawClock(){
if(intervalRef) clearTimers();
intervalRef = setInterval(draw_clock, 100);
draw_clock();
}
function reset_clock(){
force_redraw = true;
}
Bangle.on('lcdPower', (on) => {
if (on) {
console.log("lcdPower: on");
reset_clock();
startTimers();
} else {
console.log("lcdPower: off");
reset_clock();
clearTimers();
}
});
Bangle.on('faceUp',function(up){
console.log("faceUp: " + up + " LCD: " + Bangle.isLCDOn());
if (up && !Bangle.isLCDOn()) {
//console.log("faceUp and LCD off");
clearTimers();
Bangle.setLCDPower(true);
}
});
g.clear();
load_settings();
Bangle.loadWidgets();
Bangle.drawWidgets();
startTimers();
function button1pressed() {
if (next_font()) {
save_settings();
}
}
function button2pressed() {
clearTimers();
// the clock is being unloaded so we clear out the big
// data structures for the launcher
hour_scribers = [];
Bangle.showLauncher();
}
function button3pressed(){
if(next_colorscheme()) {
save_settings();
}
}
// Handle button 1 being pressed
setWatch(button1pressed, BTN1,{repeat:true,edge:"falling"});
// Handle button 1 being pressed
setWatch(button2pressed, BTN2,{repeat:true,edge:"falling"});
// Handle button 3 being pressed
setWatch(button3pressed, BTN3,{repeat:true,edge:"falling"});

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Some files were not shown because too many files have changed in this diff Show More