Merge branch 'master' into layout-img-object

master
Gordon Williams 2021-09-20 10:14:10 +01:00 committed by GitHub
commit bc2ed2f495
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 1408 additions and 598 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ appdates.csv
.vscode .vscode
.idea/ .idea/
_config.yml _config.yml

View File

@ -49,7 +49,7 @@ easily distinguish between file types, we use the following:
## Adding your app to the menu ## Adding your app to the menu
* Come up with a unique (all lowercase, nu spaces) name, we'll assume `7chname`. Bangle.js * Come up with a unique (all lowercase, no spaces) name, we'll assume `7chname`. Bangle.js
is limited to 28 char filenames and appends a file extension (eg `.js`) so please is limited to 28 char filenames and appends a file extension (eg `.js`) so please
try and keep filenames short to avoid overflowing the buffer. try and keep filenames short to avoid overflowing the buffer.
* Create a folder called `apps/<id>`, lets assume `apps/7chname` * Create a folder called `apps/<id>`, lets assume `apps/7chname`

142
apps.json
View File

@ -118,7 +118,7 @@
{ "id": "welcome", { "id": "welcome",
"name": "Welcome", "name": "Welcome",
"icon": "app.png", "icon": "app.png",
"version":"0.11", "version":"0.12",
"description": "Appears at first boot and explains how to use Bangle.js", "description": "Appears at first boot and explains how to use Bangle.js",
"tags": "start,welcome", "tags": "start,welcome",
"allow_emulator":true, "allow_emulator":true,
@ -136,7 +136,7 @@
"name": "Customised Welcome", "name": "Customised Welcome",
"shortName": "My Welcome", "shortName": "My Welcome",
"icon": "app.png", "icon": "app.png",
"version":"0.11", "version":"0.12",
"description": "Appears at first boot and explains how to use Bangle.js. Like 'Welcome', but can be customised with a greeting", "description": "Appears at first boot and explains how to use Bangle.js. Like 'Welcome', but can be customised with a greeting",
"tags": "start,welcome", "tags": "start,welcome",
"custom":"custom.html", "custom":"custom.html",
@ -153,7 +153,7 @@
{ "id": "gbridge", { "id": "gbridge",
"name": "Gadgetbridge", "name": "Gadgetbridge",
"icon": "app.png", "icon": "app.png",
"version":"0.23", "version":"0.24",
"description": "The default notification handler for Gadgetbridge notifications from Android", "description": "The default notification handler for Gadgetbridge notifications from Android",
"tags": "tool,system,android,widget", "tags": "tool,system,android,widget",
"readme": "README.md", "readme": "README.md",
@ -532,7 +532,7 @@
{ "id": "heart", { "id": "heart",
"name": "Heart Rate Recorder", "name": "Heart Rate Recorder",
"icon": "app.png", "icon": "app.png",
"version":"0.05", "version":"0.06",
"interface": "interface.html", "interface": "interface.html",
"description": "Application that allows you to record your heart rate. Can run in background", "description": "Application that allows you to record your heart rate. Can run in background",
"tags": "tool,health,widget", "tags": "tool,health,widget",
@ -571,7 +571,7 @@
{ "id": "weather", { "id": "weather",
"name": "Weather", "name": "Weather",
"icon": "icon.png", "icon": "icon.png",
"version":"0.05", "version":"0.07",
"description": "Show Gadgetbridge weather report", "description": "Show Gadgetbridge weather report",
"readme": "readme.md", "readme": "readme.md",
"tags": "widget,outdoors", "tags": "widget,outdoors",
@ -639,10 +639,11 @@
"name": "Battery Level Widget (with percentage)", "name": "Battery Level Widget (with percentage)",
"shortName": "Battery Widget", "shortName": "Battery Widget",
"icon": "widget.png", "icon": "widget.png",
"version":"0.11", "version":"0.12",
"description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage",
"tags": "widget,battery", "tags": "widget,battery,b2",
"type":"widget", "type":"widget",
"readme": "README.md",
"storage": [ "storage": [
{"name":"widbatpc.wid.js","url":"widget.js"}, {"name":"widbatpc.wid.js","url":"widget.js"},
{"name":"widbatpc.settings.js","url":"settings.js"} {"name":"widbatpc.settings.js","url":"settings.js"}
@ -721,7 +722,7 @@
{ "id": "widhrm", { "id": "widhrm",
"name": "Simple Heart Rate widget", "name": "Simple Heart Rate widget",
"icon": "widget.png", "icon": "widget.png",
"version":"0.03", "version":"0.04",
"description": "When the screen is on, the widget turns on the heart rate monitor and displays the current heart rate (or last known in grey). For this to work well you'll need at least a 15 second LCD Timeout.", "description": "When the screen is on, the widget turns on the heart rate monitor and displays the current heart rate (or last known in grey). For this to work well you'll need at least a 15 second LCD Timeout.",
"tags": "health,widget", "tags": "health,widget",
"type": "widget", "type": "widget",
@ -1287,7 +1288,7 @@
{ "id": "wohrm", { "id": "wohrm",
"name": "Workout HRM", "name": "Workout HRM",
"icon": "app.png", "icon": "app.png",
"version":"0.07", "version":"0.08",
"readme": "README.md", "readme": "README.md",
"description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.", "description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.",
"tags": "hrm,workout", "tags": "hrm,workout",
@ -1466,7 +1467,7 @@
"id": "balltastic", "id": "balltastic",
"name": "Balltastic", "name": "Balltastic",
"icon": "app.png", "icon": "app.png",
"version": "0.01", "version": "0.02",
"description": "Simple but fun ball eats dots game.", "description": "Simple but fun ball eats dots game.",
"tags": "game,fun", "tags": "game,fun",
"type": "app", "type": "app",
@ -1517,7 +1518,7 @@
"name": "OpenStreetMap", "name": "OpenStreetMap",
"shortName":"OpenStMap", "shortName":"OpenStMap",
"icon": "app.png", "icon": "app.png",
"version":"0.08", "version":"0.09",
"description": "[BETA] Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are", "description": "[BETA] Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are",
"tags": "outdoors,gps,b2", "tags": "outdoors,gps,b2",
"custom": "custom.html", "customConnect":true, "custom": "custom.html", "customConnect":true,
@ -1895,7 +1896,7 @@
{ "id": "ballmaze", { "id": "ballmaze",
"name": "Ball Maze", "name": "Ball Maze",
"icon": "icon.png", "icon": "icon.png",
"version": "0.01", "version": "0.02",
"description": "Navigate a ball through a maze by tilting your watch.", "description": "Navigate a ball through a maze by tilting your watch.",
"readme": "README.md", "readme": "README.md",
"tags": "game", "tags": "game",
@ -1944,26 +1945,16 @@
"id": "largeclock", "id": "largeclock",
"name": "Large Clock", "name": "Large Clock",
"icon": "largeclock.png", "icon": "largeclock.png",
"version": "0.09", "version": "0.10",
"description": "A readable and informational digital watch, with date, seconds and moon phase", "description": "A readable and informational digital watch, with date, seconds and moon phase",
"readme": "README.md", "readme": "README.md",
"tags": "clock", "tags": "clock",
"type": "clock", "type": "clock",
"allow_emulator": true, "allow_emulator": true,
"storage": [ "storage": [
{ {"name": "largeclock.app.js", "url": "largeclock.js"},
"name": "largeclock.app.js", {"name": "largeclock.img", "url": "largeclock-icon.js", "evaluate": true},
"url": "largeclock.js" {"name": "largeclock.settings.js", "url": "settings.js"}
},
{
"name": "largeclock.img",
"url": "largeclock-icon.js",
"evaluate": true
},
{
"name": "largeclock.settings.js",
"url": "settings.js"
}
], ],
"data": [ "data": [
{"name":"largeclock.json"} {"name":"largeclock.json"}
@ -3265,14 +3256,15 @@
"name": "Hour Strike", "name": "Hour Strike",
"shortName": "Hour Strike", "shortName": "Hour Strike",
"icon": "app-icon.png", "icon": "app-icon.png",
"version": "0.07", "version": "0.08",
"description": "Strike the clock on the hour. A great tool to remind you an hour has passed!", "description": "Strike the clock on the hour. A great tool to remind you an hour has passed!",
"tags": "tool,alarm", "tags": "tool,alarm",
"readme": "README.md", "readme": "README.md",
"storage": [ "storage": [
{"name":"hourstrike.app.js","url":"app.js"}, {"name":"hourstrike.app.js","url":"app.js"},
{"name":"hourstrike.boot.js","url":"boot.js"}, {"name":"hourstrike.boot.js","url":"boot.js"},
{"name":"hourstrike.img","url":"app-icon.js","evaluate":true} {"name":"hourstrike.img","url":"app-icon.js","evaluate":true},
{"name":"hourstrike.json","url":"hourstrike.json"}
] ]
}, },
{ "id": "whereworld", { "id": "whereworld",
@ -3430,13 +3422,103 @@
"name": "Car Crazy", "name": "Car Crazy",
"shortName":"Car Crazy", "shortName":"Car Crazy",
"icon": "carcrash.png", "icon": "carcrash.png",
"version":"0.01", "version":"0.03",
"description": "A simple car game where you try to avoid the other cars by tilting your wrist left and right. Hold down button 2 to start.", "description": "A simple car game where you try to avoid the other cars by tilting your wrist left and right. Hold down button 2 to start.",
"tags": "game", "tags": "game",
"readme": "README.md", "readme": "README.md",
"storage": [ "storage": [
{"name":"carcrazy.app.js","url":"app.js"}, {"name":"carcrazy.app.js","url":"app.js"},
{"name":"carcrazy.img","url":"app-icon.js","evaluate":true} {"name":"carcrazy.img","url":"app-icon.js","evaluate":true},
{"name":"carcrazy.settings.js","url":"settings.js"}
],
"data": [
{"name":"app.json"}
]
},
{ "id": "shortcuts",
"name": "Shortcuts",
"shortName":"Shortcuts",
"icon": "app.png",
"version":"0.01",
"description": "Quickly load your favourite apps from (almost) any watch face.",
"tags": "tool",
"type": "bootloader",
"readme": "README.md",
"storage": [
{"name":"shortcuts.boot.js","url":"boot.js"},
{"name":"shortcuts.settings.js","url":"settings.js"}
],
"data": [
{"name":"shortcuts.json"}
]
},
{ "id": "vectorclock",
"name": "Vector Clock",
"icon": "app.png",
"version": "0.02",
"description": "A digital clock that uses the built-in vector font.",
"tags": "clock",
"type": "clock",
"allow_emulator": true,
"storage": [
{"name":"vectorclock.app.js","url":"app.js"},
{"name":"vectorclock.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "fd6fdetect",
"name": "fd6fdetect",
"shortName":"fd6fdetect",
"icon": "app.png",
"version":"0.1",
"description": "Allows you to see 0xFD6F beacons near you.",
"tags": "tool",
"storage": [
{"name":"fd6fdetect.app.js","url":"app.js"},
{"name":"fd6fdetect.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "choozi",
"name": "Choozi",
"icon": "app.png",
"version":"0.01",
"description": "Choose people or things at random using Bangle.js.",
"tags": "tool",
"readme": "README.md",
"allow_emulator":true,
"storage": [
{"name":"choozi.app.js","url":"app.js"},
{"name":"choozi.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "widclkbttm",
"name": "Digital clock (Bottom) widget",
"shortName":"Digital clock Bottom Widget",
"icon": "widclkbttm.png",
"version":"0.03",
"description": "Displays time in the bottom area.",
"readme": "README.md",
"tags": "widget",
"type": "widget",
"storage": [
{"name":"widclkbttm.wid.js","url":"widclkbttm.wid.js"}
]
},
{ "id": "pastel",
"name": "Pastel Clock",
"shortName": "Pastel",
"icon": "pastel.png",
"version":"0.01",
"description": "A Configurable clock with custom fonts and background",
"tags": "clock,b2",
"type":"clock",
"readme": "README.md",
"storage": [
{"name":"pastel.app.js","url":"pastel.app.js"},
{"name":"pastel.img","url":"pastel.icon.js","evaluate":true},
{"name":"pastel.settings.js","url":"pastel.settings.js"}
],
"data": [
{"name":"pastel.json"}
] ]
} }
] ]

View File

@ -1 +1,2 @@
0.01: New App! 0.01: New App!
0.02: Set LCD timeout for Espruino 2v10 compatibility

View File

@ -1,4 +1,5 @@
(() => { (() => {
Bangle.setLCDTimeout(0);
let intervalID; let intervalID;
let settings = require("Storage").readJSON("ballmaze.json",true) || {}; let settings = require("Storage").readJSON("ballmaze.json",true) || {};

View File

@ -1 +1,2 @@
0.01: Initial version of Balltastic released! Happy! 0.01: Initial version of Balltastic released! Happy!
0.02: Set LCD timeout for Espruino 2v10 compatibility

View File

@ -1,5 +1,6 @@
Bangle.setLCDBrightness(1); Bangle.setLCDBrightness(1);
Bangle.setLCDMode("doublebuffered"); Bangle.setLCDMode("doublebuffered");
Bangle.setLCDTimeout(0);
let points = 0; let points = 0;
let level = 1; let level = 1;

View File

@ -1 +1,3 @@
0.01: Car Crazy is now avialable for testing in beta! 0.01: Car Crazy is now avialable for testing in beta!
0.02: 10 Levels are now added making the game harder as it goes along. Some of the levels include multiple cars and faster cars. More levels coming soon.
0.03: Settings are now added so that you can reset your high score.

View File

@ -1,5 +1,5 @@
# Car Crazy # Car Crazy
Car crazy is a fun game where you tilt your wrist left and right to avoid incoming cars. If you get hit by a car you lose a heart. In the game you have three hearts, if you get hit 3 times you are sent to the game over screen. Your goal is to try to last as long as you can. Because this game is still in beta please report any bugs here: https://forms.office.com/r/HnwYzG9Sk7. Car crazy is a fun game where you tilt your wrist left and right to avoid incoming cars. If you get hit by a car you lose a heart. In the game you have three hearts, if you get hit 3 times you are sent to the game over screen. Recently levels have been added making the game get harder as you play. Your goal is to try to last as long as you can. Because this game is still in production please report any bugs here: https://forms.office.com/r/HnwYzG9Sk7.
### Images: ### Images:
(Coming Soon) (Coming Soon)
@ -11,8 +11,50 @@ BNT2: Hold down this button to start the game if you are on the starting page an
Tilting Left-Right: Tilt your wrist left and right to steer your car and try not to get hit by the enemy car. Tilting Left-Right: Tilt your wrist left and right to steer your car and try not to get hit by the enemy car.
### Feautures Coming Soon: ### Feautures Coming Soon:
0.02: Levels are creating making the game get harder as it goes along. 0.02: Levels are creating making the game get harder as it goes along. (Completed)
0.03: Optional soundtrack in settings. More levels. 0.03: Setting for reseting high score. (Completed)
0.04: With higher scores you can now unlock different colors of cars. 0.04: Optional soundtrack in settings. More levels.
0.05: With higher scores you can now unlock different colors of cars. More settings.
0.06: Car selector at game over screen and start screen.
0.07 More levels.
0.08 More types of Cars.
### Levels
Here is a list of all the levels:
##### Level 1:
Basic single car moving at 10 speed.
##### Level 2:
A single car can come from any position at the top of the screen.
##### Level 3:
A single car are coming at 12 speed and can come from any position at the top of the screen.
##### Level 4:
2 cars come at 8 speed.
##### Level 5:
2 cars come at 9 speed.
##### Level 6:
2 cars come at 9.5 and 8 speed.
##### Level 7:
2 cars come at 10 and 8 speed.
##### Level 8:
2 cars come at 11.5 speed.
##### Level 9 and Above:
2 cars come at 13 and 14 speed.
### Other
-Settings are now avialable for resetting your high score.

View File

@ -22,6 +22,10 @@ var PurpleCar = require("heatshrink").decompress(atob("ol74UBitg///BIP/7lVqtUDJU
var LightGreenCar = require("heatshrink").decompress(atob("ol74UBocF///BIP1z9VqtUDJUVBwIABq2qABOVCCkolQJC0AwDgWolAQD1EqwBCH0EqCCdKxQuEAAkKwGhCH4Q/CGD7ECDINEAAoQIwBACgQQYwQ+EAAcC1EACAeAlQfDAAheBCG2oCBUCCB8qCCiQBdp3+fxW/CH4Q/CH4QxgQQKwAQD1QQK1QQUmQQKxiH/CBGqCB3/5WACA+j/4Qf14QxKjGqRI0CCAv+x4QBEYcC1Wo/SpFCBtUlQQBBgISBAAQGBCAOo6te1QQB///lQPC1AGBCAOqyoQQlAuBCAXPAQIAD/Q2BlGoCAgAGCAUqCBnyCAVS1WrB5AAB9RUDCB1WCB0VVIIQMXQYQ/CAbFBBw/8BQIQFn4QewDNBCQgOCgEACDADBAAMC1T5BCAwABCQIACA4QQZABAQ/CH4Q/CH4QC1APKhQQPlQQUxWgGJOCCAcCCBWACAmqGhAQFwEqCA8qFYIQTuWohWqAAIQE1WghUFCANVD4JFBAAqzBlQPCCCNyHIIQGMoONCAdWQ5WVCCjNJZIYQM1WtCAt6KYwAB0oQFIoIPGIIoQTqhDG6oMDA")); var LightGreenCar = require("heatshrink").decompress(atob("ol74UBocF///BIP1z9VqtUDJUVBwIABq2qABOVCCkolQJC0AwDgWolAQD1EqwBCH0EqCCdKxQuEAAkKwGhCH4Q/CGD7ECDINEAAoQIwBACgQQYwQ+EAAcC1EACAeAlQfDAAheBCG2oCBUCCB8qCCiQBdp3+fxW/CH4Q/CH4QxgQQKwAQD1QQK1QQUmQQKxiH/CBGqCB3/5WACA+j/4Qf14QxKjGqRI0CCAv+x4QBEYcC1Wo/SpFCBtUlQQBBgISBAAQGBCAOo6te1QQB///lQPC1AGBCAOqyoQQlAuBCAXPAQIAD/Q2BlGoCAgAGCAUqCBnyCAVS1WrB5AAB9RUDCB1WCB0VVIIQMXQYQ/CAbFBBw/8BQIQFn4QewDNBCQgOCgEACDADBAAMC1T5BCAwABCQIACA4QQZABAQ/CH4Q/CH4QC1APKhQQPlQQUxWgGJOCCAcCCBWACAmqGhAQFwEqCA8qFYIQTuWohWqAAIQE1WghUFCANVD4JFBAAqzBlQPCCCNyHIIQGMoONCAdWQ5WVCCjNJZIYQM1WtCAt6KYwAB0oQFIoIPGIIoQTqhDG6oMDA"));
function consoleDebug(message) {
//console.log(message);
}
function getRandomInt(min, max) { function getRandomInt(min, max) {
min = Math.ceil(min); min = Math.ceil(min);
max = Math.floor(max); max = Math.floor(max);
@ -30,21 +34,55 @@ function getRandomInt(min, max) {
function moveEnemyPosition(){ function moveEnemyPosition(){
score += 1; score += 1;
checkForNextLevel();
if(level == 1){
randomRoadPositionIndicator = getRandomInt(1, 4); randomRoadPositionIndicator = getRandomInt(1, 4);
if ((randomRoadPositionIndicator == 1)) { if ((randomRoadPositionIndicator == 1)) {
randomRoadPosition = 85; enemyPositonCenterX = 85;
}else if((randomRoadPositionIndicator == 2)){ }else if((randomRoadPositionIndicator == 2)){
randomRoadPosition = 120; enemyPositonCenterX = 120;
}else { }else {
randomRoadPosition = 155; enemyPositonCenterX = 155;
}
}else if(level == 2||level==3){
enemyPositonCenterX = getRandomInt(85, 155);
}else if(level == 4 || level == 5 || level == 6 || level == 8 || level == 9 || level == 10 || level > 10){
do{
randomRoadPositionIndicator = getRandomInt(1, 4);
randomRoadPositionIndicator2 = getRandomInt(1, 4);
}while(randomRoadPositionIndicator==randomRoadPositionIndicator2);
if ((randomRoadPositionIndicator == 1)) {
enemyPositonCenterX = 85;
}else if((randomRoadPositionIndicator == 2)){
enemyPositonCenterX = 120;
}else if((randomRoadPositionIndicator == 3)){
enemyPositonCenterX = 155;
}
if ((randomRoadPositionIndicator2 == 1)) {
enemyPositonCenterX2 = 85;
}else if((randomRoadPositionIndicator2 == 2)){
enemyPositonCenterX2 = 120;
}else if((randomRoadPositionIndicator2 == 3)){
enemyPositonCenterX2 = 155;
}else if(level == 7||level == 8){
}
} }
} }
function collision(){ function collision(){
if(gameStatus == GAMEPLAYING){ if(gameStatus == GAMEPLAYING){
consoleDebug("Px:"+playerCarLeftX+", "+playerCarRightX);
consoleDebug("1x:"+enemyCarLeftX+", "+enemyCarRightX);
consoleDebug("2x:"+enemyCarLeftX2+", "+enemyCarRightX2);
consoleDebug("Py:"+playerCarFrontY);
consoleDebug("1y:"+enemyCarFrontY);
consoleDebug("2y:"+enemyCarFrontY2);
if if
( (
(enemyCarFrontY > playerCarFrontY) (enemyCarFrontY < 300 && enemyCarFrontY > playerCarFrontY)
&& &&
( (
(enemyCarLeftX > playerCarLeftX && enemyCarLeftX < playerCarRightX) (enemyCarLeftX > playerCarLeftX && enemyCarLeftX < playerCarRightX)
@ -52,51 +90,105 @@ function collision(){
(enemyCarRightX > playerCarLeftX && enemyCarRightX < playerCarRightX) (enemyCarRightX > playerCarLeftX && enemyCarRightX < playerCarRightX)
) )
){ ){
// hit // hit car 1
setTimeout(collision, 2500); // wait 2.5 second for the function to actiavte agian. consoleDebug("1 HIT");
enemyPositonCenterY = 300;
numberofHearts -= 1; numberofHearts -= 1;
score -= 1; Bangle.buzz(50,50);
Bangle.buzz(); }else if
}else{ (
// miss (enemyCarFrontY2 < 300 && enemyCarFrontY2 > playerCarFrontY)
setTimeout(collision, 1); // try again in 1 milliseconds. &&
(
(enemyCarLeftX2 > playerCarLeftX && enemyCarLeftX2 < playerCarRightX)
||
(enemyCarRightX2 > playerCarLeftX && enemyCarRightX2 < playerCarRightX)
)
){
// hit car 2
consoleDebug("2 HIT");
enemyPositonCenterY2 = 300;
numberofHearts -= 1;
Bangle.buzz(50,50);
} }
setTimeout(collision, 50); // try again in 50 milliseconds.
} }
} }
function storeMyData(data) { function checkForNextLevel(){
// ensure there are less than 500 elements in the array if(score < 10){
while (log.length >= 500) log.shift(); level = 1;
// append a new item to the array }else if(score >= 10 && score < 20){
log.push(data); level = 2;
}else if(score >= 20 && score < 30){
level = 3;
}else if(score >= 30 && score < 40){
level = 4;
}else if(score >= 40 && score < 50){
level = 5;
}else if(score >= 50 && score < 60){
level = 6;
}else if(score >= 60 && score < 70){
level = 7;
}else if(score >= 70 && score < 80){
level = 8;
}else if(score >= 80 && score < 90){
level = 9;
}else if(score >= 90){
level = 10;
}
} }
var accel = Bangle.getAccel();
var file = require("Storage").open("CarCrazy.csv","r"); var file = require("Storage").open("CarCrazy.csv","r");
var currentHighScore = file.readLine(); var currentHighScore = file.readLine();
if (currentHighScore == undefined) currentHighScore = 0; if (currentHighScore == undefined) currentHighScore = 0;
var BackgroundStartingPosition = 75; var BackgroundStartingPosition = 75;
var carScale = 0.5;
var accel = Bangle.getAccel();
var playerCarPosition = 120-accel.x*40;
var BackgroundYPosition = BackgroundStartingPosition; var BackgroundYPosition = BackgroundStartingPosition;
var randomRoadPositionIndicator = getRandomInt(1, 3);
var randomRoadPosition = 120; var randomRoadPositionIndicator;
var enemyPositonY = 30; var randomRoadPositionIndicator2;
var enemyPositonCenterX;
var enemyPositonCenterX2;
var carScale = 0.5;
var carWidth = 30; var carWidth = 30;
var carHeight = 60; var carHeight = 60;
var playerCarY = 130;
var enemyCarLeftX; var playerCarCenterY = 130;
var enemyCarRightX; var playerCarCenterX;
var enemyPositonCenterY = 0 - carHeight/2;
var enemyPositonCenterY2 = 0 - carHeight/2;
var playerCarLeftX; var playerCarLeftX;
var playerCarRightX; var playerCarRightX;
var enemyCarFrontY;
var playerCarFrontY; var playerCarFrontY;
var playerCarFrontY;
var playerCarBackY;
var playerCarLeftX;
var playerCarRightX;
var enemyCarFrontY;
var enemyCarBackY;
var enemyCarLeftX;
var enemyCarRightX;
var enemyCarFrontY2;
var enemyCarBackY2;
var enemyCarLeftX2;
var enemyCarRightX2;
var GAMEPLAYING = 1; var GAMEPLAYING = 1;
var GAMEOVER = 2; var GAMEOVER = 2;
var GAMESTART = 3; var GAMESTART = 3;
var gameStatus = GAMESTART; var gameStatus = GAMESTART;
var score = 0; var score = 0;
var level = 1;
moveEnemyPosition(); moveEnemyPosition();
collision(); collision();
@ -114,11 +206,16 @@ function draw(){
if(gameStatus == GAMEPLAYING){ if(gameStatus == GAMEPLAYING){
BackgroundYPosition += 10; BackgroundYPosition += 10;
accel = Bangle.getAccel(); accel = Bangle.getAccel();
playerCarPosition = 120-accel.x*40; playerCarCenterX = Math.round(120-accel.x*120);
if (playerCarCenterX > 170) { playerCarCenterX = 170; }
if (playerCarCenterX < 70) { playerCarCenterX = 70; }
g.flip(); g.flip();
g.drawImage(backgroundImage,125,BackgroundYPosition, {scale:13,rotate:0}); g.drawImage(backgroundImage,125,BackgroundYPosition, {scale:13,rotate:0});
g.drawImage(RedCar,playerCarPosition,playerCarY, {scale:carScale,rotate:3.142}); g.drawImage(RedCar,playerCarCenterX,playerCarCenterY, {scale:carScale,rotate:3.142});
g.drawImage(OrangeCar,randomRoadPosition,enemyPositonY, {scale:carScale,rotate:0}); g.drawImage(OrangeCar,enemyPositonCenterX,enemyPositonCenterY, {scale:carScale,rotate:0});
if(level>=4){
g.drawImage(OrangeCar,enemyPositonCenterX2,enemyPositonCenterY2, {scale:carScale,rotate:0});
}
if(numberofHearts==3){ if(numberofHearts==3){
g.drawImage(heartImage,10,10, {scale:2,rotate:0}); g.drawImage(heartImage,10,10, {scale:2,rotate:0});
@ -141,23 +238,38 @@ function draw(){
} }
} }
playerCarFrontY = playerCarY-carHeight/2; playerCarFrontY = playerCarCenterY-carHeight/2;
playerCarBackY = playerCarY+carHeight/2; playerCarBackY = playerCarCenterY+carHeight/2;
playerCarLeftX = playerCarPosition-carWidth/2; playerCarLeftX = playerCarCenterX-carWidth/2;
playerCarRightX = playerCarPosition+carWidth/2; playerCarRightX = playerCarCenterX+carWidth/2;
enemyCarFrontY = enemyPositonY+carHeight/2; enemyCarFrontY = enemyPositonCenterY+carHeight/2;
enemyCarBackY = enemyPositonY-carHeight/2; enemyCarBackY = enemyPositonCenterY-carHeight/2;
enemyCarLeftX = randomRoadPosition-carWidth/2; enemyCarLeftX = enemyPositonCenterX-carWidth/2;
enemyCarRightX = randomRoadPosition+carWidth/2; enemyCarRightX = enemyPositonCenterX+carWidth/2;
enemyCarFrontY2 = enemyPositonCenterY2+carHeight/2;
enemyCarBackY2 = enemyPositonCenterY2-carHeight/2;
enemyCarLeftX2 = enemyPositonCenterX2-carWidth/2;
enemyCarRightX2 = enemyPositonCenterX2+carWidth/2;
g.setColor(255,0,0);
//g.drawRect(playerCarLeftX, playerCarFrontY, playerCarRightX, playerCarBackY); //g.drawRect(playerCarLeftX, playerCarFrontY, playerCarRightX, playerCarBackY);
//g.drawRect(enemyCarLeftX, enemyCarFrontY, enemyCarRightX, enemyCarBackY); //g.drawRect(enemyCarLeftX, enemyCarFrontY, enemyCarRightX, enemyCarBackY);
//g.drawRect(enemyCarLeftX2, enemyCarFrontY2, enemyCarRightX2, enemyCarBackY2);
g.setColor(0,0,0); g.setColor(0,0,0);
g.drawString("Score: "+score,180,5); g.drawString("Score: "+score,180,5);
g.drawString("HighScore:",178,15); g.drawString("HighScore:",178,15);
g.drawString(currentHighScore,205,25); g.drawString(currentHighScore,205,25);
g.drawString("Level: "+level,180,150);
//g.drawString("P:"+playerCarLeftX+", "+playerCarRightX,180,90);
//g.drawString("1:"+enemyCarLeftX+", "+enemyCarRightX,180,100);
//g.drawString("2:"+enemyCarLeftX2+", "+enemyCarRightX2,180,110);
//g.drawString("P:"+playerCarFrontY,180,120);
//g.drawString("1:"+enemyCarFrontY,180,130);
//g.drawString("2:"+enemyCarFrontY2,180,140);
if(BackgroundYPosition > 170){ if(BackgroundYPosition > 170){
BackgroundYPosition = BackgroundStartingPosition; BackgroundYPosition = BackgroundStartingPosition;
@ -200,41 +312,75 @@ function draw(){
g.drawString("2 To Start",10,130); g.drawString("2 To Start",10,130);
g.drawImage(LightGreenCar,180,115, {scale:0.5,rotate:3}); g.drawImage(LightGreenCar,180,115, {scale:0.5,rotate:3});
g.drawImage(PurpleCar,215,115, {scale:0.5,rotate:3}); g.drawImage(PurpleCar,215,115, {scale:0.5,rotate:3});
//setTimeout(displayPopup, 3000);
} }
} }
setInterval(draw ,10); setInterval(draw, 50);
function moveEnemyCar(){ function moveEnemyCar(){
if(gameStatus == GAMEPLAYING){ if(gameStatus == GAMEPLAYING){
enemyPositonY = enemyPositonY + 10; if(level==1||level==2){
if((enemyPositonY > 200)){ enemyPositonCenterY = enemyPositonCenterY + 10;
enemyPositonY = 30; enemyPositonCenterY2 = 0;
}else if(level==3){
enemyPositonCenterY = enemyPositonCenterY + 12;
enemyPositonCenterY2 = 0;
}else if(level==4){
enemyPositonCenterY = enemyPositonCenterY + 8;
enemyPositonCenterY2 = enemyPositonCenterY2 + 8;
}else if(level==5){
enemyPositonCenterY = enemyPositonCenterY + 9;
enemyPositonCenterY2 = enemyPositonCenterY2 + 9;
}else if(level==6){
enemyPositonCenterY = enemyPositonCenterY + 9.5;
enemyPositonCenterY2 = enemyPositonCenterY2 + 8;
}else if(level==7){
enemyPositonCenterY = enemyPositonCenterY + 10;
enemyPositonCenterY2 = enemyPositonCenterY2 + 8;
}else if(level==8){
enemyPositonCenterY = enemyPositonCenterY + 11.5;
enemyPositonCenterY2 = enemyPositonCenterY2 + 11.5;
}else if(level>=9){
enemyPositonCenterY = enemyPositonCenterY + 13;
enemyPositonCenterY2 = enemyPositonCenterY2 + 14;
}
if(enemyPositonCenterY > 200){
enemyPositonCenterY = 300;
}
if(enemyPositonCenterY2 > 200){
enemyPositonCenterY2 = 300;
}
if(enemyPositonCenterY > 200 && (enemyPositonCenterY2 > 200 || level < 4)){
enemyPositonCenterY = 0 - carHeight/2;
if (level >= 4) { enemyPositonCenterY2 = 0 - carHeight/2; }
moveEnemyPosition(); moveEnemyPosition();
} }
} }
} }
setInterval(moveEnemyCar,10); setInterval(moveEnemyCar,50);
setWatch(() => { setWatch(() => {
if(gameStatus == GAMESTART){ if(gameStatus == GAMESTART){
gameStatus = GAMEPLAYING; gameStatus = GAMEPLAYING;
collision(); collision();
enemyPositonY = 0; numberofHearts = 3;
enemyPositonCenterX = 120;
enemyPositonCenterY = 0 - carHeight/2;
enemyPositonCenterX2 = 120;
enemyPositonCenterY2 = 0 - carHeight/2;
score = 0; score = 0;
level = 1;
checkForNextLevel();
}else if(gameStatus == GAMEOVER){ }else if(gameStatus == GAMEOVER){
gameStatus = GAMEPLAYING; gameStatus = GAMEPLAYING;
collision(); collision();
enemyPositonY = 0; enemyPositonCenterX = 120;
enemyPositonCenterY = 0 - carHeight/2;
enemyPositonCenterX2 = 120;
enemyPositonCenterY2 = 0 - carHeight/2;
numberofHearts = 3; numberofHearts = 3;
score = 0; score = 0;
level = 1;
checkForNextLevel();
} }
}, BTN2, {repeat:true}); }, BTN2, {repeat:true});

20
apps/carcrazy/settings.js Normal file
View File

@ -0,0 +1,20 @@
(function (back) {
const menu = {
'': { 'title': 'Car Crazy' },
'< Back': back,
'Reset Highscore': () => {
E.showPrompt('Reset Highscore?').then((v) => {
let delay = 50;
if (v) {
delay = 500;
E.showMessage('Resetting');
var f = require('Storage').open('CarCrazy.csv', 'w');
f.write('0\n');
}
setTimeout(() => E.showMenu(menu), delay);
});
}
};
E.showMenu(menu);
});

1
apps/choozi/ChangeLog Normal file
View File

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

27
apps/choozi/README.md Normal file
View File

@ -0,0 +1,27 @@
# Choozi
Choose people or things at random using Bangle.js.
<iframe width="560" height="315" src="https://www.youtube.com/embed/4cqOLNM5ei8" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
## Usage
You can use Choozi to pick a person to play first in a board game. With all
the players seated in a circle, set the number of segments equal to the number
of players, ensure that each person knows which colour represents them, and then
choose a segment. After a short animation, the chosen segment will fill the screen.
You can use Choozi to randomly select an element from any set with 2 to 13 members,
as long as you can define a bijection between members of the set and coloured
segments on the Bangle.js display.
## Controls
BTN1: increase the number of segments
BTN2: choose a segment at random
BTN3: decrease the number of segments
## Creator
James Stanley
September 2021

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwggLIrnM4uqAAIhPgvMAAPFzIABzWgCxkMCweqC4QABDBYtC5QVFDBoWCCo5KLOQIWKDARFICxhJIFwOpC5owFFyAwGUYIuOGAwuRC4guSJAgXBCyIwDIyQXF5IXSzJeVMAReUAAOQhheTMAVcC6yOUC4aOUC7GZUyoXXzWqhQXVxGqC9mYC7OqC9eoxEKC6uBC6uIwAXBPCSmBwEAC6Z2BiAXBJCR2BgEAjQXSlGBC4JgSLwYABJCJGBLwJIDGB+IIwRIDGByNBIwZIDGBhdBRoQwSLoIuFGAYYKCwIuGGAgYI1QWBRgYYJMYmaFoSMEAAyrBAAgVCCxgYGjAWQAAMBC4UILZQA=="))

208
apps/choozi/app.js Normal file
View File

@ -0,0 +1,208 @@
/* Choozi - Choose people or things at random using Bangle.js.
* Inspired by the "Chwazi" Android app
*
* James Stanley 2021
*/
var colours = ['#ff0000', '#ff8080', '#00ff00', '#80ff80', '#0000ff', '#8080ff', '#ffff00', '#00ffff', '#ff00ff', '#ff8000', '#ff0080', '#8000ff', '#0080ff'];
var stepAngle = 0.18; // radians - resolution of polygon
var gapAngle = 0.035; // radians - gap between segments
var perimMin = 110; // px - min. radius of perimeter
var perimMax = 120; // px - max. radius of perimeter
var segmentMax = 106; // px - max radius of filled-in segment
var segmentStep = 5; // px - step size of segment fill animation
var circleStep = 4; // px - step size of circle fill animation
// rolling ball animation:
var maxSpeed = 0.08; // rad/sec
var minSpeed = 0.001; // rad/sec
var animStartSteps = 300; // how many steps before it can start slowing?
var accel = 0.0002; // rad/sec/sec - acc-/deceleration rate
var ballSize = 3; // px - ball radius
var ballTrack = 100; // px - radius of ball path
var centreX = 120; // px - centre of screen
var centreY = 120; // px - centre of screen
var fontSize = 50; // px
var radians = 2*Math.PI; // radians per circle
var defaultN = 3; // default value for N
var minN = 2;
var maxN = colours.length;
var N;
var arclen;
// https://www.frankmitchell.org/2015/01/fisher-yates/
function shuffle (array) {
var i = 0
, j = 0
, temp = null;
for (i = array.length - 1; i > 0; i -= 1) {
j = Math.floor(Math.random() * (i + 1));
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
// draw an arc between radii minR and maxR, and between
// angles minAngle and maxAngle
function arc(minR, maxR, minAngle, maxAngle) {
var step = stepAngle;
var angle = minAngle;
var inside = [];
var outside = [];
var c, s;
while (angle < maxAngle) {
c = Math.cos(angle);
s = Math.sin(angle);
inside.push(centreX+c*minR); // x
inside.push(centreY+s*minR); // y
// outside coordinates are built up in reverse order
outside.unshift(centreY+s*maxR); // y
outside.unshift(centreX+c*maxR); // x
angle += step;
}
c = Math.cos(maxAngle);
s = Math.sin(maxAngle);
inside.push(centreX+c*minR);
inside.push(centreY+s*minR);
outside.unshift(centreY+s*maxR);
outside.unshift(centreX+c*maxR);
var vertices = inside.concat(outside);
g.fillPoly(vertices, true);
}
// draw the arc segments around the perimeter
function drawPerimeter() {
g.clear();
for (var i = 0; i < N; i++) {
g.setColor(colours[i%colours.length]);
var minAngle = (i/N)*radians;
arc(perimMin,perimMax,minAngle,minAngle+arclen);
}
}
// animate a ball rolling around and settling at "target" radians
function animateChoice(target) {
var angle = 0;
var speed = 0;
var oldx = -10;
var oldy = -10;
var decelFromAngle = -1;
var allowDecel = false;
for (var i = 0; true; i++) {
angle = angle + speed;
if (angle > radians) angle -= radians;
if (i < animStartSteps || (speed < maxSpeed && !allowDecel)) {
speed = speed + accel;
if (speed > maxSpeed) {
speed = maxSpeed;
/* when we reach max speed, we know how long it takes
* to accelerate, and therefore how long to decelerate, so
* we can work out what angle to start decelerating from */
if (decelFromAngle < 0) {
decelFromAngle = target-angle;
while (decelFromAngle < 0) decelFromAngle += radians;
while (decelFromAngle > radians) decelFromAngle -= radians;
}
}
} else {
if (!allowDecel && (angle < decelFromAngle) && (angle+speed >= decelFromAngle)) allowDecel = true;
if (allowDecel) speed = speed - accel;
if (speed < minSpeed) speed = minSpeed;
if (speed == minSpeed && angle < target && angle+speed >= target) return;
}
var r = i/2;
if (r > ballTrack) r = ballTrack;
var x = centreX+Math.cos(angle)*r;
var y = centreY+Math.sin(angle)*r;
g.setColor('#000000');
g.fillCircle(oldx,oldy,ballSize+1);
g.setColor('#ffffff');
g.fillCircle(x, y, ballSize);
oldx=x;
oldy=y;
}
}
// choose a winning segment and animate its selection
function choose() {
var chosen = Math.floor(Math.random()*N);
var minAngle = (chosen/N)*radians;
var maxAngle = minAngle + arclen;
animateChoice((minAngle+maxAngle)/2);
g.setColor(colours[chosen%colours.length]);
for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep)
arc(i, perimMax, minAngle, maxAngle);
arc(0, perimMax, minAngle, maxAngle);
for (var r = 1; r < segmentMax; r += circleStep)
g.fillCircle(centreX,centreY,r);
g.fillCircle(centreX,centreY,segmentMax);
}
// draw the current value of N in the middle of the screen, with
// up/down arrows
function drawN() {
g.setColor('#ffffff');
g.setFont("Vector",fontSize);
g.drawString(N,centreX-g.stringWidth(N)/2+4,centreY-fontSize/2);
if (N < maxN)
g.fillPoly([centreX-6,centreY-fontSize/2-7, centreX+6,centreY-fontSize/2-7, centreX, centreY-fontSize/2-14]);
if (N > minN)
g.fillPoly([centreX-6,centreY+fontSize/2+5, centreX+6,centreY+fontSize/2+5, centreX, centreY+fontSize/2+12]);
}
// update number of segments, with min/max limit, "arclen" update,
// and screen reset
function setN(n) {
N = n;
if (N < minN) N = minN;
if (N > maxN) N = maxN;
arclen = radians/N - gapAngle;
drawPerimeter();
}
// save N to choozi.txt
function writeN() {
var file = require("Storage").open("choozi.txt","w");
file.write(N);
}
// load N from choozi.txt
function readN() {
var file = require("Storage").open("choozi.txt","r");
var n = file.readLine();
if (n !== undefined) setN(parseInt(n));
else setN(defaultN);
}
shuffle(colours); // is this really best?
Bangle.setLCDMode("direct");
Bangle.setLCDTimeout(0); // keep screen on
readN();
drawN();
setWatch(() => {
setN(N+1);
drawN();
}, BTN1, {repeat:true});
setWatch(() => {
writeN();
drawPerimeter();
choose();
}, BTN2, {repeat:true});
setWatch(() => {
setN(N-1);
drawN();
}, BTN3, {repeat:true});

BIN
apps/choozi/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1 @@
0.1: Added source code

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwIjgg/gAp0IgfAiAFBjkP+E4AoM8n/8ngFBvn//8+AoP//Ef/4FBv/Agf+AoMPwEB+AFCjEYAoUenk8vAvCAoIvCnAFBjgFCC4IFCCgUeEQNwAoMO+EPuPD4eOAoPz8fH54FH+IRBx4FBDogpFGoxBFJopZFMopxFPoqJFSoqhFVoq5FgAFBa6gAW"))

23
apps/fd6fdetect/app.js Normal file
View File

@ -0,0 +1,23 @@
g.clear();
let amount = 'global value';
function FindFD6FBeacons() {
NRF.findDevices(function(devices) {
g.setFont('Vector', 75);
g.setFontAlign(0,0);
var amount = devices.length;
g.clear();
g.drawString(amount, 125, 100);
if (amount == 1) {
g.setFont('Vector', 25);
g.drawString('FD6F', 125, 150);
g.drawString('beacon', 125, 175);
g.drawString('nearby', 125, 200);
} else{
g.setFont('Vector', 25);
g.drawString('FD6F', 125, 150);
g.drawString('beacons', 125, 175);
g.drawString('nearby', 125, 200);
}
}, {timeout : 1000, filters : [{services: ['fd6f'] }] });
}
setInterval(FindFD6FBeacons, 2000);

BIN
apps/fd6fdetect/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -23,3 +23,4 @@
0.21: Fix HRM setting 0.21: Fix HRM setting
0.22: Respect Quiet Mode 0.22: Respect Quiet Mode
0.23: Allow notification dismiss to remove from phone too 0.23: Allow notification dismiss to remove from phone too
0.24: tag HRM power requests to allow this to work alongside other widgets/apps (fix #799)

View File

@ -128,7 +128,7 @@
if (activityInterval) if (activityInterval)
clearInterval(activityInterval); clearInterval(activityInterval);
activityInterval = undefined; activityInterval = undefined;
if (s.hrm) Bangle.setHRMPower(1); if (s.hrm) Bangle.setHRMPower(1,"gbr");
if (s.hrm) { if (s.hrm) {
if (realtime) { if (realtime) {
// if realtime reporting, leave HRM on and use that to trigger events // if realtime reporting, leave HRM on and use that to trigger events
@ -138,7 +138,7 @@
hrmTimeout = 5; hrmTimeout = 5;
activityInterval = setInterval(function() { activityInterval = setInterval(function() {
hrmTimeout = 5; hrmTimeout = 5;
Bangle.setHRMPower(1); Bangle.setHRMPower(1,"gbr");
}, interval*1000); }, interval*1000);
} }
} else { } else {
@ -281,7 +281,7 @@
if (hrmTimeout!==undefined) hrmTimeout--; if (hrmTimeout!==undefined) hrmTimeout--;
if (ok || hrmTimeout<=0) { if (ok || hrmTimeout<=0) {
if (hrmTimeout!==undefined) if (hrmTimeout!==undefined)
Bangle.setHRMPower(0); Bangle.setHRMPower(0,"gbr");
sendActivity(hrm.confidence>20 ? hrm.bpm : -1); sendActivity(hrm.confidence>20 ? hrm.bpm : -1);
} }
}); });

View File

@ -11,3 +11,4 @@
Reduce memory usage by ~30% Reduce memory usage by ~30%
Generate scale based on defined minimum and maximum measurement Generate scale based on defined minimum and maximum measurement
Added background line on 50% to ease estimation of drawn values Added background line on 50% to ease estimation of drawn values
0.06: tag HRM power requests to allow this ot work alongside other widgets/apps (fix #799)

View File

@ -31,12 +31,12 @@
if (settings.isRecording) { if (settings.isRecording) {
WIDGETS["heart"].width = 24; WIDGETS["heart"].width = 24;
Bangle.on('HRM',onHRM); Bangle.on('HRM',onHRM);
Bangle.setHRMPower(1); Bangle.setHRMPower(1,"heart");
var n = settings.fileNbr.toString(36); var n = settings.fileNbr.toString(36);
recFile = require("Storage").open(".heart"+n,"a"); recFile = require("Storage").open(".heart"+n,"a");
} else { } else {
WIDGETS["heart"].width = 0; WIDGETS["heart"].width = 0;
Bangle.setHRMPower(0); Bangle.setHRMPower(0,"heart");
recFile = undefined; recFile = undefined;
} }
} }

View File

@ -5,3 +5,4 @@
0.05: Add display for the next strike time 0.05: Add display for the next strike time
0.06: Move the next strike time to the first row of display 0.06: Move the next strike time to the first row of display
0.07: Change the boot function to avoid reloading the entire watch 0.07: Change the boot function to avoid reloading the entire watch
0.08: Default to no strikes. Fix file-not-found issue during the first boot. Add data file.

View File

@ -1,25 +1,10 @@
const storage = require('Storage'); const storage = require('Storage');
let settings; var settings = storage.readJSON('hourstrike.json', 1);
function updateSettings() { function updateSettings() {
storage.write('hourstrike.json', settings); storage.write('hourstrike.json', settings);
} }
function resetSettings() {
settings = {
interval: 3600,
start: 9,
end: 21,
vlevel: 0.5,
next_hour: -1,
next_minute: -1,
};
updateSettings();
}
settings = storage.readJSON('hourstrike.json', 1);
if (!settings) resetSettings();
function showMainMenu() { function showMainMenu() {
var mode_txt = ['Off','1 min','5 min','10 min','1/4 h','1/2 h','1 h']; var mode_txt = ['Off','1 min','5 min','10 min','1/4 h','1/2 h','1 h'];
var mode_interval = [-1,60,300,600,900,1800,3600]; var mode_interval = [-1,60,300,600,900,1800,3600];

View File

@ -1,6 +1,6 @@
(function() { (function() {
function setup () { function setup () {
var settings = require('Storage').readJSON('hourstrike.json',1)||[]; var settings = require('Storage').readJSON('hourstrike.json',1);
var t = new Date(); var t = new Date();
var t_min_sec = t.getMinutes()*60+t.getSeconds(); var t_min_sec = t.getMinutes()*60+t.getSeconds();
var wait_msec = settings.interval>0?(settings.interval-t_min_sec%settings.interval)*1000:-1; var wait_msec = settings.interval>0?(settings.interval-t_min_sec%settings.interval)*1000:-1;

View File

@ -0,0 +1 @@
{"interval":-1,"start":9,"end":21,"vlevel":0.5,"next_hour":-1,"next_minute":-1}

View File

@ -7,3 +7,4 @@
0.07: Don't clear all intervals during initialisation 0.07: Don't clear all intervals during initialisation
0.08: Use Bangle.setUI for button/launcher handling 0.08: Use Bangle.setUI for button/launcher handling
0.09: fix font size for latest firmwares 0.09: fix font size for latest firmwares
0.10: Configure the side text direction based on the wrist on which you wear your watch

View File

@ -7,6 +7,7 @@ A readable and informational digital watch, with date, seconds and moon phase an
- Readable - Readable
- Informative: hours, minutes, secondsa, date, year and moon phase - Informative: hours, minutes, secondsa, date, year and moon phase
- Pairs nicely with any other apps: in setting > large clock any installed app can be assigned to BTN1 and BTN3 in order to open it easily directly from the watch, without the hassle of passing trough the launcher. For example BTN1 can be assigned to alarm and BTN3 to chronometer. - Pairs nicely with any other apps: in setting > large clock any installed app can be assigned to BTN1 and BTN3 in order to open it easily directly from the watch, without the hassle of passing trough the launcher. For example BTN1 can be assigned to alarm and BTN3 to chronometer.
- Configure the text direction on the side depending on the wrist on which you wear your watch.
## How to use it ## How to use it

View File

@ -14,6 +14,9 @@ const settings = require("Storage").readJSON("largeclock.json", 1)||{};
const BTN1app = settings.BTN1 || ""; const BTN1app = settings.BTN1 || "";
const BTN3app = settings.BTN3 || ""; const BTN3app = settings.BTN3 || "";
const right_hand = !!settings.right_hand;
const rotation = right_hand ? 3 : 1;
function drawMoon(d) { function drawMoon(d) {
const BLACK = 0, const BLACK = 0,
MOON = 0x41f, MOON = 0x41f,
@ -145,9 +148,9 @@ function drawTime(d) {
g.setColor(1, 50, 1); g.setColor(1, 50, 1);
g.drawString(minutes, 40, 130, true); g.drawString(minutes, 40, 130, true);
g.setFont("Vector", 20); g.setFont("Vector", 20);
g.setRotation(3); g.setRotation(rotation);
g.drawString(`${dow} ${day} ${month}`, 60, 10, true); g.drawString(`${dow} ${day} ${month}`, 60, right_hand?10:205, true);
g.drawString(year, is12Hour ? 46 : 75, 205, true); g.drawString(year, is12Hour?(right_hand?56:120):(right_hand?85:115), right_hand?205:10, true);
lastMinutes = minutes; lastMinutes = minutes;
} }
g.setRotation(0); g.setRotation(0);

View File

@ -1,4 +1,5 @@
{ {
"BTN1": "", "BTN1": "",
"BTN3": "" "BTN3": "",
"right_hand": false
} }

View File

@ -28,7 +28,8 @@
const settings = s.readJSON("largeclock.json", 1) || { const settings = s.readJSON("largeclock.json", 1) || {
BTN1: "", BTN1: "",
BTN3: "" BTN3: "",
right_hand: false
}; };
function showApps(btn) { function showApps(btn) {
@ -67,10 +68,19 @@
} }
const mainMenu = { const mainMenu = {
"": { title: "Large Clock Settings" }, "": { title: "Large Clock" },
"< Back": back, "< Back": back,
"BTN1 app": () => showApps("BTN1"), "BTN1 app": () => showApps("BTN1"),
"BTN3 app": () => showApps("BTN3") "BTN3 app": () => showApps("BTN3"),
"On right hand": {
value: !!settings.right_hand,
format: v=>v?"Yes":"No",
onchange: v=>{
settings.right_hand = v;
s.writeJSON("largeclock.json", settings);
}
}
}; };
E.showMenu(mainMenu); E.showMenu(mainMenu);
}); });

View File

@ -13,3 +13,4 @@
BTN2 now goes to menu on release BTN2 now goes to menu on release
0.10: Add birthday style 0.10: Add birthday style
0.11: Skip double buffering, use 240x240 size 0.11: Skip double buffering, use 240x240 size
0.12: Fix swipe direction (#800)

View File

@ -285,7 +285,7 @@ function move(dir) {
Bangle.on('swipe',move); Bangle.on('swipe', dir => move(-dir));
setWatch(()=>move(1), BTN3, {repeat:true}); setWatch(()=>move(1), BTN3, {repeat:true});
setWatch(()=>{ setWatch(()=>{
// If we're on the last page // If we're on the last page

View File

@ -6,3 +6,4 @@
0.06: Add support for scrolling, option for 3 bit maps 0.06: Add support for scrolling, option for 3 bit maps
0.07: Move to 96px tiles - less files (64 -> 25) and speed up rendering 0.07: Move to 96px tiles - less files (64 -> 25) and speed up rendering
0.08: Update for drag event refactor 0.08: Update for drag event refactor
0.09: Use current theme cols when drawing GPS info

View File

@ -24,10 +24,7 @@ function drawMarker() {
var fix; var fix;
Bangle.on('GPS',function(f) { Bangle.on('GPS',function(f) {
fix=f; fix=f;
g.clearRect(0,y1,240,y1+8); g.reset().clearRect(0,y1,240,y1+8).setFont("6x8").setFontAlign(0,0);
g.setColor(1,1,1);
g.setFont("6x8");
g.setFontAlign(0,0);
var txt = fix.satellites+" satellites"; var txt = fix.satellites+" satellites";
if (!fix.fix) if (!fix.fix)
txt += " - NO FIX"; txt += " - NO FIX";

1
apps/pastel/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: First release

17
apps/pastel/README.md Normal file
View File

@ -0,0 +1,17 @@
# Pastel Clock - a configurable clock with custom fonts and background
* Designed specifically for Bangle 1 and Bangle 2
* A choice of 5 different custom fonts
* Supports the Light and Dark themes
* Has a settings menu, change font, enable/disable the grid and the date display
I came up with the name Pastel due to the shade of the grid background.
![](screenshot_lato.jpg)
![](screenshot_architech.jpg)
![](screenshot_gochi.jpg)
![](screenshot_b1_light.jpg)
![](screenshot_b2_dark.jpg)

158
apps/pastel/pastel.app.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A1PYIqqAAUqp0kGMopCuVOvHHAAV4GEcAFQgAFuQwhLoIuJAAMqGEEAzovL44vgkguM49OGD0ApwvNzovfdpSQjF+FyEwskewyPFgAADX7kkA414EwIqCp14zmcuUkGJ5FEkgnFpwHHFgXBTQ0kFhpSBvAABD4KHHEY0ATAw7EFpTnHRA8Azg2FFxIvMCxGcHI0qG4kAlQuJF5gXJcIIwEvARDlSVGfZAwJuQWKSQwuCRpQAB4IvK/0kC5K0BGAouHzucvAIFL5bvHvAbCGAMkEAQuEzi0BAAZ9FX5QuH48kDgILB4IFCAAPB4IsCklySZIvKUxWdLYYABpyhBuUkIxAADHoJfSJI140SOBzgTNFxQwPQ4QuBAgK1FOoyiCF5QwBpwaIzgZBzlydgT2BCZGcHwMrmMHGJjnDAAd4JAImCvDRDGAIKBvF4uTJBA4MrrtjrljrowOAAIZCEQPBFQTuBHgpXBCYkGnBcCmVjsaTOJYOcFgZ+BeAXHkiRBAAprBrkxFAM4MAMyMgIvMkjaGKQICBMQUqfxInBlZ+DMIIGBF5QgGgAHBWQLCGAAnBgC5BFIUrR4KQNF4wGCki+DgCQHBwMySQNcFQMHSQKPTFQ4vJlS/BrhaBMgIwBd5ofHXwYALXALuCGQcxHAIvLznBD4q6JX41jmIvCdoKOBMB0ASQtyTJIADp0GdIVjgwvBMYQvMGISzEL5ucRIQAIg4vOzmdzpdBM4NOZA1OlUqBoUrFIZeCAAUrF5qSCAAIGDlSIEBgoNBGAVdBQMGmJoBgwvOG5JZBFgoNFLoIMCJgwxXO5wpYAH4A/AH4A/ABwA="))

View File

@ -0,0 +1,7 @@
require("Storage").write("pastel.info",{
"id":"pastel",
"name":"Configurable clock with custom fonts and background",
"src":"pastel.app.js",
"icon":"pastel.img",
"type":"clock"
});

BIN
apps/pastel/pastel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,56 @@
(function(back) {
const SETTINGS_FILE = "pastel.json";
// initialize with default settings...
let s = {
'grid': false,
'date': false,
'font': "Lato"
}
// ...and overwrite them with any saved values
// This way saved values are preserved if a new version adds more settings
const storage = require('Storage')
let settings = storage.readJSON(SETTINGS_FILE, 1) || {}
const saved = settings || {}
for (const key in saved) {
s[key] = saved[key]
}
function save() {
settings = s
storage.write(SETTINGS_FILE, settings)
}
var font_options = ["Lato","Architect","GochiHand","CabinSketch","Orbitron"];
E.showMenu({
'': { 'title': 'Pastel Clock' },
'Font': {
value: 0 | font_options.indexOf(s.font),
min: 0, max: 4,
format: v => font_options[v],
onchange: v => {
s.font = font_options[v];
save();
},
},
'Show Grid': {
value: s.grid,
format: () => (s.grid ? 'Yes' : 'No'),
onchange: () => {
s.grid = !s.grid
save()
},
},
'Show Date': {
value: s.date,
format: () => (s.date ? 'Yes' : 'No'),
onchange: () => {
s.date = !s.date
save()
},
},
'< Back': back,
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -0,0 +1,6 @@
const SETTINGS_FILE = "pastel.json";
let settings = {"grid":false, "date":false, "font":"Lato"}
require("Storage").write(SETTINGS_FILE, settings);
settings = require("Storage").readJSON(SETTINGS_FILE,1)||{};
console.log(settings);

1
apps/shortcuts/ChangeLog Normal file
View File

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

7
apps/shortcuts/README.md Normal file
View File

@ -0,0 +1,7 @@
# Shortcuts
Any installed app can be assigned to BTN1 and BTN3 and launched directly from compatible watch faces. This works with any watch face that uses `Bangle.setUI("clock")`.
## Credits
<a target="_blank" href="https://icons8.com/icon/i1z7pQ2orcJk/shortcut">Shortcut</a> icon by <a target="_blank" href="https://icons8.com">Icons8</a>

BIN
apps/shortcuts/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

15
apps/shortcuts/boot.js Normal file
View File

@ -0,0 +1,15 @@
(function() {
var sui = Bangle.setUI;
Bangle.setUI = function(mode, cb) {
if (mode!="clock") return sui(mode,cb);
return sui("clockupdown", (dir) => {
let settings = require("Storage").readJSON("shortcuts.json", 1)||{};
if (dir == -1) {
if (settings.BTN1) load(settings.BTN1);
} else if (dir == 1) {
if (settings.BTN3) load(settings.BTN3);
}
});
};
})();

View File

@ -0,0 +1,66 @@
(function(back) {
const s = require("Storage");
const apps = s
.list(/\.info$/)
.map(app => {
var a = s.readJSON(app, 1);
return a && (a.type=="app" || a.type=="clock" || !a.type) && {n: a.name, src: a.src};
})
.filter(Boolean);
apps.sort((a, b) => {
if (a.n < b.n) return -1;
if (a.n > b.n) return 1;
return 0;
});
apps.push({n: "NONE", src: null});
const settings = s.readJSON("shortcuts.json", 1) || {
BTN1: null,
BTN3: null
};
function showApps(btn) {
function format(v) {
return v === settings[btn] ? "*" : "";
}
function onchange(v) {
settings[btn] = v;
s.writeJSON("shortcuts.json", settings);
}
const btnMenu = {
"": {
title: `Apps for ${btn}`
},
"< Back": () => E.showMenu(mainMenu)
};
if (apps.length > 0) {
for (let a of apps) {
btnMenu[a.n] = {
value: a.src,
format: format,
onchange: onchange
};
}
} else {
btnMenu["...No Apps..."] = {
value: undefined,
format: () => "",
onchange: () => {}
};
}
E.showMenu(btnMenu);
}
const mainMenu = {
"": { title: "Shortcuts Settings" },
"< Back": back,
"BTN1 app": () => showApps("BTN1"),
"BTN3 app": () => showApps("BTN3")
};
E.showMenu(mainMenu);
});

View File

@ -0,0 +1,2 @@
0.1: New watch face
0.2: Use Bangle.setUI for button/launcher handling

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkEIf4AxgMhgUAiIHCmYKCmcgBAUCmQDEgUzkcg+QJBl//AYfzDgX///wAYcCmMzmQXC+c/AYM/kU/iEAgfzkfyAYcCAYM/HIUwC4QxCkEhgcwEYIDDgUwgcSC4QsBLQf/iATEAYcBiB3FC4cvKAIvINAMxgSGDC4UT/4ICC5EDkcjI40/mIHCgaNBC4IDCBAMDmUiXwU/mcQ/8zAYMyQ4M/RYQDBC4yxBIgIDDE4M//5FBAYasBifxf5QA/AC0iAALFDA4ICBgMhC5SBB//wA4gCBUoIXLXYKaBAAUjC54KJC6Ejmcxe4MAiczC4IJBmEjNwILBL4b5Bl7vCD4IEB+cCNgUP+A3EL4MyC4IkBiE/BoMD+cP+MvCoPygfxI4wXBA4UD+QZBh8wgacB/4ODC5YvC+EfC4JVBiAXLgP/n5JBZgcPSwgXIRYPz+cBQoIXBLwgARgU/c4gAQJYJeDF6TXBAH5OMmUBmcQkcgicxBQMTkBbDBIMCSAcTl8jmcxmXymczBQIECCIQEBkbDBAAUfFgMwgPymUDiUg+EjiUwgUhBIMQC4cCfgIXBeAINBicwC4JBBgY7BgcAC4cfkEDkUx+UAmUjBQPxmZZDBIQXDl//kQBC+UvDQIKBmM//4FCBYP/bX4A/ACIA="))

93
apps/vectorclock/app.js Normal file
View File

@ -0,0 +1,93 @@
const is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"];
const locale = require("locale");
function padNum(n, l) {
return ("0".repeat(l)+n).substr(-l);
}
let rects = {};
let rectsToClear = {};
let commands = [];
function pushCommand(command) {
let hash = E.CRC32(E.toJS(arguments));
if (!delete rectsToClear[hash]) {
commands.push({hash: hash, command: Function.apply.bind(command, null, arguments.slice(1))});
}
}
function executeCommands() {
"ram";
for (let hash in rectsToClear) delete rects[hash];
for (let r of rectsToClear) if (r) g.clearRect(r.x1, r.y1, r.x2, r.y2);
g.getModified(true);
for (let c of commands) {
c.command();
rects[c.hash] = g.getModified(true);
}
rectsToClear = Object.assign({}, rects);
commands = [];
}
function drawVectorText(text, size, x, y, alignX, alignY) {
g.setFont("Vector", size).setFontAlign(alignX, alignY).drawString(text, x, y);
}
function draw() {
g.reset();
let d = new Date();
let hours = is12Hour ? ((d.getHours() + 11) % 12) + 1 : d.getHours();
let timeText = `${hours}:${padNum(d.getMinutes(), 2)}`;
let meridian = is12Hour ? ((d.getHours() < 12) ? "AM" : "PM") : "";
let secondsText = padNum(d.getSeconds(), 2);
let dowText = locale.dow(d);
let dateText = locale.date(d, true);
g.setFont("Vector", 256);
let timeFontSize = g.getWidth() / ((g.stringWidth(timeText) / 256) + (Math.max(g.stringWidth(meridian), g.stringWidth(secondsText)) / 512 * 9 / 10));
let dowFontSize = g.getWidth() / (g.stringWidth(dowText) / 256);
let dateFontSize = g.getWidth() / (g.stringWidth(dateText) / 256);
let timeHeight = g.setFont("Vector", timeFontSize).getFontHeight() * 9 / 10;
let dowHeight = g.setFont("Vector", dowFontSize).getFontHeight();
let dateHeight = g.setFont("Vector", dateFontSize).getFontHeight();
let remainingHeight = g.getHeight() - 24 - timeHeight - dowHeight - dateHeight;
let spacer = remainingHeight / 4;
let y = 24 + spacer;
pushCommand(drawVectorText, timeText, timeFontSize, 0, y, -1, -1);
pushCommand(drawVectorText, meridian, timeFontSize*9/20, g.getWidth(), y, 1, -1);
pushCommand(drawVectorText, secondsText, timeFontSize*9/20, g.getWidth(), y + timeHeight, 1, 1);
y += timeHeight + spacer;
pushCommand(drawVectorText, dowText, dowFontSize, g.getWidth()/2, y, 0, -1);
y += dowHeight + spacer;
pushCommand(drawVectorText, dateText, dateFontSize, g.getWidth()/2, y, 0, -1);
executeCommands();
}
let timeout;
function tick() {
draw();
timeout = setTimeout(tick, 1000 - getTime() % 1 * 1000);
}
Bangle.on('lcdPower', function(on) {
if (timeout) clearTimeout(timeout);
timeout = null;
if (on) tick();
});
g.clear();
tick();
Bangle.loadWidgets();
Bangle.drawWidgets();
// Show launcher when middle button pressed
Bangle.setUI("clock");

BIN
apps/vectorclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -2,3 +2,5 @@
0.03: Fix flickering last updated time. 0.03: Fix flickering last updated time.
0.04: Adjust "weather unknown" message according to Bluetooth connection. 0.04: Adjust "weather unknown" message according to Bluetooth connection.
0.05: Add wind direction. 0.05: Add wind direction.
0.06: Use setUI for launcher.
0.07: Add theme support and unknown icon.

View File

@ -12,12 +12,12 @@
function draw() { function draw() {
let w = weather.current; let w = weather.current;
g.reset(); g.reset();
g.setColor(0).fillRect(0, 24, 239, 239); g.clearRect(0, 24, 239, 239);
weather.drawIcon(w.txt, 65, 90, 55); weather.drawIcon(w.txt, 65, 90, 55);
const locale = require("locale"); const locale = require("locale");
g.setColor(-1); g.reset();
const temp = locale.temp(w.temp-273.15).match(/^(\D*\d*)(.*)$/); const temp = locale.temp(w.temp-273.15).match(/^(\D*\d*)(.*)$/);
let width = g.setFont("Vector", 40).stringWidth(temp[1]); let width = g.setFont("Vector", 40).stringWidth(temp[1]);
@ -54,8 +54,8 @@
if (!weather.current || !weather.current.time) return; if (!weather.current || !weather.current.time) return;
let text = `Last update received ${formatDuration(Date.now() - weather.current.time)} ago`; let text = `Last update received ${formatDuration(Date.now() - weather.current.time)} ago`;
g.reset(); g.reset();
g.setColor(0).fillRect(0, 202, 239, 210); g.clearRect(0, 202, 239, 210);
g.setColor(-1).setFont("6x8", 1).setFontAlign(0, 0, 0); g.setFont("6x8", 1).setFontAlign(0, 0, 0);
g.drawString(text, 120, 206); g.drawString(text, 120, 206);
} }
@ -88,7 +88,7 @@
update(); update();
// Show launcher when middle button pressed // Show launcher when middle button pressed
setWatch(Bangle.showLauncher, BTN2, {repeat: false, edge: 'falling'}); Bangle.setUI("clock");
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();

View File

@ -68,13 +68,13 @@ setCurrentWeather(storage.readJSON('weather.json')||{});
exports.drawIcon = function(cond, x, y, r) { exports.drawIcon = function(cond, x, y, r) {
function drawSun(x, y, r) { function drawSun(x, y, r) {
g.setColor("#FF7700"); g.setColor(g.theme.dark ? "#FE0" : "#FC0");
g.fillCircle(x, y, r); g.fillCircle(x, y, r);
} }
function drawCloud(x, y, r, c) { function drawCloud(x, y, r, c) {
const u = r/12; const u = r/12;
if (c==null) c = "#EEEEEE"; if (c==null) c = g.theme.dark ? "#BBB" : "#AAA";
g.setColor(c); g.setColor(c);
g.fillCircle(x-8*u, y+3*u, 4*u); g.fillCircle(x-8*u, y+3*u, 4*u);
g.fillCircle(x-4*u, y-2*u, 5*u); g.fillCircle(x-4*u, y-2*u, 5*u);
@ -91,7 +91,7 @@ exports.drawIcon = function(cond, x, y, r) {
} }
function drawBrokenClouds(x, y, r) { function drawBrokenClouds(x, y, r) {
drawCloud(x+1/8*r, y-1/8*r, 7/8*r, "#777777"); drawCloud(x+1/8*r, y-1/8*r, 7/8*r, "#777");
drawCloud(x-1/8*r, y+1/8*r, 7/8*r); drawCloud(x-1/8*r, y+1/8*r, 7/8*r);
} }
@ -101,24 +101,25 @@ exports.drawIcon = function(cond, x, y, r) {
} }
function drawRainLines(x, y, r) { function drawRainLines(x, y, r) {
g.setColor("#FFFFFF"); g.setColor(g.theme.dark ? "#0CF" : "#07F");
const y1 = y+1/2*r; const y1 = y+1/2*r;
const y2 = y+1*r; const y2 = y+1*r;
g.fillPoly([
x-6/12*r+1, y1, g.fillPolyAA([
x-8/12*r+1, y2, x-6/12*r, y1,
x-8/12*r, y2,
x-7/12*r, y2, x-7/12*r, y2,
x-5/12*r, y1, x-5/12*r, y1,
]); ]);
g.fillPoly([ g.fillPolyAA([
x-2/12*r+1, y1, x-2/12*r, y1,
x-4/12*r+1, y2, x-4/12*r, y2,
x-3/12*r, y2, x-3/12*r, y2,
x-1/12*r, y1, x-1/12*r, y1,
]); ]);
g.fillPoly([ g.fillPolyAA([
x+2/12*r+1, y1, x+2/12*r, y1,
x+0/12*r+1, y2, x+0/12*r, y2,
x+1/12*r, y2, x+1/12*r, y2,
x+3/12*r, y1, x+3/12*r, y1,
]); ]);
@ -136,7 +137,7 @@ exports.drawIcon = function(cond, x, y, r) {
function drawThunderstorm(x, y, r) { function drawThunderstorm(x, y, r) {
function drawLightning(x, y, r) { function drawLightning(x, y, r) {
g.setColor("#FF7700"); g.setColor(g.theme.dark ? "#FE0" : "#FC0");
g.fillPoly([ g.fillPoly([
x-2/6*r, y-r, x-2/6*r, y-r,
x-4/6*r, y+1/6*r, x-4/6*r, y+1/6*r,
@ -164,7 +165,7 @@ exports.drawIcon = function(cond, x, y, r) {
} }
} }
g.setColor("#FFFFFF"); g.setColor(g.theme.dark ? "#FFF" : "#CCC");
const w = 1/12*r; const w = 1/12*r;
for(let i = 0; i<=6; ++i) { for(let i = 0; i<=6; ++i) {
const points = [ const points = [
@ -199,7 +200,7 @@ exports.drawIcon = function(cond, x, y, r) {
[-0.2, 0.3], [-0.2, 0.3],
]; ];
g.setColor("#FFFFFF"); g.setColor(g.theme.dark ? "#FFF" : "#CCC");
for(let i = 0; i<5; ++i) { for(let i = 0; i<5; ++i) {
g.fillRect(x+layers[i][0]*r, y+(0.4*i-0.9)*r, x+layers[i][1]*r, g.fillRect(x+layers[i][0]*r, y+(0.4*i-0.9)*r, x+layers[i][1]*r,
y+(0.4*i-0.7)*r-1); y+(0.4*i-0.7)*r-1);
@ -208,6 +209,11 @@ exports.drawIcon = function(cond, x, y, r) {
} }
} }
function drawUnknown(x, y, r) {
drawCloud(x, y, r, "#777");
g.setColor(g.theme.fg).setFontAlign(0, 0).setFont('Vector', r*2).drawString("?", x+r/10, y+r/6);
}
function chooseIcon(condition) { function chooseIcon(condition) {
if (!condition) return () => {}; if (!condition) return () => {};
condition = condition.toLowerCase(); condition = condition.toLowerCase();
@ -225,8 +231,19 @@ exports.drawIcon = function(cond, x, y, r) {
if (condition.includes("few clouds")) return drawFewClouds; if (condition.includes("few clouds")) return drawFewClouds;
if (condition.includes("scattered clouds")) return drawCloud; if (condition.includes("scattered clouds")) return drawCloud;
if (condition.includes("clouds")) return drawBrokenClouds; if (condition.includes("clouds")) return drawBrokenClouds;
if (condition.includes("mist") ||
condition.includes("smoke") ||
condition.includes("haze") ||
condition.includes("sand") ||
condition.includes("dust") ||
condition.includes("fog") ||
condition.includes("ash") ||
condition.includes("squalls") ||
condition.includes("tornado")) {
return drawMist; return drawMist;
} }
return drawUnknown;
}
chooseIcon(cond)(x, y, r); chooseIcon(cond)(x, y, r);
}; };

View File

@ -5,16 +5,16 @@
const w = weather.current; const w = weather.current;
if (!w) return; if (!w) return;
g.reset(); g.reset();
g.setColor(0).fillRect(this.x, this.y, this.x+this.width-1, this.y+23); g.clearRect(this.x, this.y, this.x+this.width-1, this.y+23);
if (w.txt) { if (w.txt) {
weather.drawIcon(w.txt, this.x+10, this.y+8, 7.5); weather.drawIcon(w.txt, this.x+10, this.y+8, 7.5);
} }
if (w.temp) { if (w.temp) {
let t = require('locale').temp(w.temp-273.15); // applies conversion let t = require('locale').temp(w.temp-273.15); // applies conversion
t = t.match(/[\d\-]*/)[0]; // but we have no room for units t = t.match(/[\d\-]*/)[0]; // but we have no room for units
g.reset();
g.setFontAlign(0, 1); // center horizontally at bottom of widget g.setFontAlign(0, 1); // center horizontally at bottom of widget
g.setFont('6x8', 1); g.setFont('6x8', 1);
g.setColor(-1);
g.drawString(t, this.x+10, this.y+24); g.drawString(t, this.x+10, this.y+24);
} }
} }

View File

@ -13,3 +13,4 @@
BTN2 now goes to menu on release BTN2 now goes to menu on release
0.10: Tweaks to reduce memory usage 0.10: Tweaks to reduce memory usage
0.11: Fix initial screen fill colour 0.11: Fix initial screen fill colour
0.12: Fix swipe direction (#800)

View File

@ -280,7 +280,7 @@ function move(dir) {
Bangle.on('swipe',move); Bangle.on('swipe', dir => move(-dir));
setWatch(()=>move(1), BTN3, {repeat:true}); setWatch(()=>move(1), BTN3, {repeat:true});
setWatch(()=>{ setWatch(()=>{
// If we're on the last page // If we're on the last page

View File

@ -8,3 +8,4 @@
0.09: Fix regression stopping correct widget updates 0.09: Fix regression stopping correct widget updates
0.10: Add 'hide if charge greater than' 0.10: Add 'hide if charge greater than'
0.11: Don't overwrite existing settings on app update 0.11: Don't overwrite existing settings on app update
0.12: Fixed for Bangle 2

8
apps/widbatpc/README.md Normal file
View File

@ -0,0 +1,8 @@
# Battery Widget
Show the current battery level and charging status in the top right of the clock, with charge percentage
Works with Bangle 1 and Bangle 2
![](screenshot.jpg)

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,10 +1,23 @@
(function(){ (function(){
const COLORS = { let COLORS = {};
'white': -1,
'charging': 0x07E0, // "Green" if (process.env.HWVERSION == 1) {
'high': 0x05E0, // slightly darker green COLORS = {
'ok': 0xFD20, // "Orange" 'white': -1, // White
'low':0xF800, // "Red" 'charging': 0x07E0, // Green
'high': 0x05E0, // lightly darker green
'ok': 0xFD20, // Orange
'low': 0xF800, // Red
};
} else {
// bangle 2 is only 7 bit colors
COLORS = {
'white': "#fff", // White
'charging': "#0f0", // Green
'high': "#0f0", // Green
'ok': "#ff0", // Orange
'low': "#f00", // Red
};
} }
const SETTINGS_FILE = 'widbatpc.json' const SETTINGS_FILE = 'widbatpc.json'
@ -66,29 +79,28 @@
// else... // else...
var s = 39; var s = 39;
var x = this.x, y = this.y; var x = this.x, y = this.y;
const l = E.getBattery(), const l = E.getBattery();
c = levelColor(l);
const xl = x+4+l*(s-12)/100 const xl = x+4+l*(s-12)/100
c = levelColor(l);
if (Bangle.isCharging() && setting('charger')) { if (Bangle.isCharging() && setting('charger')) {
g.setColor(chargerColor()).drawImage(atob( g.setColor(chargerColor()).drawImage(atob(
"DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y); "DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y);
x+=16; x+=16;
} }
g.setColor(-1); g.setColor(g.theme.fg);
g.fillRect(x,y+2,x+s-4,y+21); g.fillRect(x,y+2,x+s-4,y+21);
g.clearRect(x+2,y+4,x+s-6,y+19); g.clearRect(x+2,y+4,x+s-6,y+19);
g.fillRect(x+s-3,y+10,x+s,y+14); g.fillRect(x+s-3,y+10,x+s,y+14);
g.setColor(c).fillRect(x+4,y+6,xl,y+17); g.setColor(c).fillRect(x+4,y+6,xl,y+17);
g.setColor(-1); g.setColor(g.theme.fg);
if (!setting('percentage')) { if (!setting('percentage')) {
return; return;
} }
let gfx = g let gfx = g
if (setting('color') === 'Monochrome') { if (setting('color') === 'Monochrome') {
// draw text inverted on battery level // draw text inverted on battery level
gfx = Graphics.createCallback(240, 240, 1, gfx = Graphics.createCallback(g.getWidth(),g.getHeight(), 1,
(x,y) => {g.setPixel(x,y,x<=xl?0:-1)}) (x,y) => {g.setPixel(x,y,x<=xl?0:-1)})
} }
gfx.setFontAlign(-1,-1); gfx.setFontAlign(-1,-1);

View File

@ -0,0 +1,4 @@
0.01: Fork of widclk v0.04 github.com/espruino/BangleApps/tree/master/apps/widclk
0.02: Modification for bottom widget area and text color
0.03: based in widclk v0.05 compatible at same time, bottom area and color

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

@ -0,0 +1,28 @@
# Digital clock widget (bottom widget area)
This very basic widget clock allows to test the unfrequently used widget bottom area.
forked from
https://github.com/espruino/BangleApps/tree/master/apps/widclk
## Photo
Example of usage
![](widTextBottom_ss1.jpg)
## Usage
Upload the widget file
Open an app that supports displaying widgets
## 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: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

View File

@ -0,0 +1,31 @@
(function() {
// don't show widget if we know we have a clock app running
if (Bangle.CLOCK) return;
let intervalRef = null;
var width = 5 * 6*2;
var text_color=0x07FF;//cyan
function draw() {
g.reset().setFont("6x8", 2).setFontAlign(-1, 0).setColor(text_color);
var time = require("locale").time(new Date(),1);
g.drawString(time, this.x, this.y+11, true); // 5 * 6*2 = 60
}
function clearTimers(){
if(intervalRef) {
clearInterval(intervalRef);
intervalRef = null;
}
}
function startTimers(){
intervalRef = setInterval(()=>WIDGETS["wdclkbttm"].draw(), 60*1000);
WIDGETS["wdclkbttm"].draw();
}
Bangle.on('lcdPower', (on) => {
clearTimers();
if (on) startTimers();
});
WIDGETS["wdclkbttm"]={area:"br",width:width,draw:draw};
if (Bangle.isLCDOn) intervalRef = setInterval(()=>WIDGETS["wdclkbttm"].draw(), 60*1000);
})()

View File

@ -1,3 +1,4 @@
0.01: New Widget! 0.01: New Widget!
0.02: Tweaks for variable size widget system 0.02: Tweaks for variable size widget system
0.03: Ensure redrawing works with variable size widget system 0.03: Ensure redrawing works with variable size widget system
0.04: tag HRM power requests to allow this ot work alongside other widgets/apps (fix #799)

View File

@ -26,12 +26,12 @@
// redraw when the LCD turns on // redraw when the LCD turns on
Bangle.on('lcdPower', function(on) { Bangle.on('lcdPower', function(on) {
if (on) { if (on) {
Bangle.setHRMPower(1); Bangle.setHRMPower(1,"widhrm");
firstBPM = true; firstBPM = true;
currentBPM = undefined; currentBPM = undefined;
WIDGETS["hrm"].draw(); WIDGETS["hrm"].draw();
} else { } else {
Bangle.setHRMPower(0); Bangle.setHRMPower(0,"widhrm");
} }
}); });
@ -44,7 +44,7 @@
} }
WIDGETS["hrm"].draw(); WIDGETS["hrm"].draw();
}); });
Bangle.setHRMPower(Bangle.isLCDOn()); Bangle.setHRMPower(Bangle.isLCDOn(),"widhrm");
// add your widget // add your widget
WIDGETS["hrm"]={area:"tl",width:24,draw:draw}; WIDGETS["hrm"]={area:"tl",width:24,draw:draw};

View File

@ -5,3 +5,4 @@
0.05: Improved buzz timing and rendering 0.05: Improved buzz timing and rendering
0.06: Removed debug outputs, fixed rendering for upper limit, improved rendering for +/- icons, changelog version order fixed 0.06: Removed debug outputs, fixed rendering for upper limit, improved rendering for +/- icons, changelog version order fixed
0.07: Home button fixed and README added 0.07: Home button fixed and README added
0.08: tag HRM power requests to allow this ot work alongside other widgets/apps (fix #799)

View File

@ -288,7 +288,7 @@ function resetHighlightTimeout() {
} }
function switchOffApp(){ function switchOffApp(){
Bangle.setHRMPower(0); Bangle.setHRMPower(0,"wohrm");
Bangle.showLauncher(); Bangle.showLauncher();
} }
@ -306,7 +306,7 @@ Bangle.on('lcdPower', (on) => {
} }
}); });
Bangle.setHRMPower(1); Bangle.setHRMPower(1,"wohrm");
Bangle.on('HRM', onHrm); Bangle.on('HRM', onHrm);
setWatch(incrementLimit, BTN1, {edge:"rising", debounce:50, repeat:true}); setWatch(incrementLimit, BTN1, {edge:"rising", debounce:50, repeat:true});

2
core

@ -1 +1 @@
Subproject commit 39f89acf1468dc7d58ac54d15a092b2bd6d73cce Subproject commit 2aac601e38d659876eb7db5aebc7a12dd3c39da7

View File

@ -11,7 +11,7 @@ if (window.location.host=="banglejs.com") {
'This is not the official Bangle.js App Loader - you can try the <a href="https://banglejs.com/apps/">Official Version</a> here.'; 'This is not the official Bangle.js App Loader - you can try the <a href="https://banglejs.com/apps/">Official Version</a> here.';
} }
var RECOMMENDED_VERSION = "2v09"; var RECOMMENDED_VERSION = "2v10";
// could check http://www.espruino.com/json/BANGLEJS.json for this // could check http://www.espruino.com/json/BANGLEJS.json for this
(function() { (function() {

View File

@ -1,11 +1,10 @@
/* /*
Usage: Usage:
``` ```
var Layout = require("Layout"); var Layout = require("Layout");
var layout = new Layout( layoutObject, btns ) var layout = new Layout( layoutObject, btns, options )
layout.render(optionalObject); layout.render(optionalObject);
``` ```
@ -42,8 +41,8 @@ layoutObject has:
* A `halign` field to set horizontal alignment. `-1`=left, `1`=right, `0`=center * A `halign` field to set horizontal alignment. `-1`=left, `1`=right, `0`=center
* A `valign` field to set vertical alignment. `-1`=top, `1`=bottom, `0`=center * A `valign` field to set vertical alignment. `-1`=top, `1`=bottom, `0`=center
* A `pad` integer field to set pixels padding * A `pad` integer field to set pixels padding
* A `fillx` boolean to choose if the object should fill available space in x * A `fillx` int to choose if the object should fill available space in x. 0=no, 1=yes, 2=2x more space
* A `filly` boolean to choose if the object should fill available space in y * A `filly` int to choose if the object should fill available space in y. 0=no, 1=yes, 2=2x more space
* `width` and `height` fields to optionally specify minimum size * `width` and `height` fields to optionally specify minimum size
btns is an array of objects containing: btns is an array of objects containing:
@ -52,6 +51,13 @@ btns is an array of objects containing:
* `cb` - a callback function * `cb` - a callback function
* `cbl` - a callback function for long presses * `cbl` - a callback function for long presses
options is an object containing:
* `lazy` - a boolean specifying whether to enable automatic lazy rendering
If automatic lazy rendering is enabled, calls to `layout.render()` will attempt to automatically
determine what objects have changed or moved, clear their previous locations, and re-render just those objects.
Once `layout.update()` is called, the following fields are added Once `layout.update()` is called, the following fields are added
to each object: to each object:
@ -69,17 +75,20 @@ Other functions:
*/ */
function Layout(layout, buttons) { function Layout(layout, buttons, options) {
this._l = this.l = layout; this._l = this.l = layout;
this.b = buttons; this.b = buttons;
// Do we have >1 physical buttons? // Do we have >1 physical buttons?
this.physBtns = (process.env.HWVERSION==2) ? 1 : 3; this.physBtns = (process.env.HWVERSION==2) ? 1 : 3;
this.yOffset = Object.keys(global.WIDGETS).length ? 24 : 0; this.yOffset = Object.keys(global.WIDGETS).length ? 24 : 0;
options = options || {};
this.lazy = options.lazy || false;
if (buttons) { if (buttons) {
if (this.physBtns >= buttons.length) { if (this.physBtns >= buttons.length) {
// enough physical buttons // enough physical buttons
var btnHeight = Math.floor((g.getHeight()-this.yOffset) / this.physBtns); let btnHeight = Math.floor((g.getHeight()-this.yOffset) / this.physBtns);
if (Bangle.btnWatch) Bangle.btnWatch.forEach(clearWatch); if (Bangle.btnWatch) Bangle.btnWatch.forEach(clearWatch);
Bangle.btnWatch = []; Bangle.btnWatch = [];
if (this.physBtns > 2 && buttons.length==1) if (this.physBtns > 2 && buttons.length==1)
@ -95,7 +104,7 @@ function Layout(layout, buttons) {
{type:"v", pad:1, filly:1, c: buttons.map(b=>(b.type="txt",b.font="6x8",b.height=btnHeight,b.r=1,b))} {type:"v", pad:1, filly:1, c: buttons.map(b=>(b.type="txt",b.font="6x8",b.height=btnHeight,b.r=1,b))}
]}; ]};
} else { } else {
var btnHeight = Math.floor((g.getHeight()-this.yOffset) / buttons.length); let btnHeight = Math.floor((g.getHeight()-this.yOffset) / buttons.length);
this._l.width = g.getWidth()-20; // button width this._l.width = g.getWidth()-20; // button width
this._l = {type:"h", c: [ this._l = {type:"h", c: [
this._l, this._l,
@ -104,7 +113,7 @@ function Layout(layout, buttons) {
} }
} }
if (process.env.HWVERSION==2) { if (process.env.HWVERSION==2) {
Bangle.touchHandler = (_,e) => touchHandler(layout,e); Bangle.touchHandler = function(_,e){touchHandler(layout,e)};
Bangle.on('touch',Bangle.touchHandler); Bangle.on('touch',Bangle.touchHandler);
} }
@ -112,6 +121,7 @@ function Layout(layout, buttons) {
var ll = this; var ll = this;
function idRecurser(l) { function idRecurser(l) {
if (l.id) ll[l.id] = l; if (l.id) ll[l.id] = l;
if (!l.type) l.type="";
if (l.c) l.c.forEach(idRecurser); if (l.c) l.c.forEach(idRecurser);
} }
idRecurser(layout); idRecurser(layout);
@ -144,79 +154,41 @@ function touchHandler(l,e) {
if (l.c) l.c.forEach(n => touchHandler(n,e)); if (l.c) l.c.forEach(n => touchHandler(n,e));
} }
function prepareLazyRender(l, rectsToClear, drawList, rects, bgCol) {
if ((l.bgCol != null && l.bgCol != bgCol) || l.type == "txt" || l.type == "btn" || l.type == "img" || l.type == "custom") {
// Hash the layoutObject without including its children
let c = l.c;
delete l.c;
let hash = "H"+E.CRC32(E.toJS(l)); // String keys maintain insertion order
if (c) l.c = c;
function updateMin(l) { if (!delete rectsToClear[hash]) {
switch (l.type) { rects[hash] = {bg: bgCol, r: [l.x,l.y,l.x+l.w-1,l.y+l.h-1]};
case "txt": { if (drawList) {
if (l.font.endsWith("%")) drawList.push(l);
l.font = "Vector"+Math.round(g.getHeight()*l.font.slice(0,-1)/100); drawList = null; // Prevent children from being redundantly added to the drawList
// FIXME ':'/fsz not needed in new firmwares - it's handled internally
if (l.font.includes(":")) {
var f = l.font.split(":");
l.font = f[0];
l.fsz = f[1];
} }
g.setFont(l.font,l.fsz);
l._h = g.getFontHeight();
l._w = g.stringWidth(l.label);
break;
} }
case "btn": {
l._h = 24;
l._w = 14 + l.label.length*8;
break;
} }
case "img": {
var src = l.src(); if (l.c) for (let ch of l.c) prepareLazyRender(ch, rectsToClear, drawList, rects, l.bgCol == null ? bgCol : l.bgCol);
if (typeof(src) === 'object') {
l._h = ("width" in src) ? src.width : src.getWidth();
l._w = ("height" in src) ? src.height : src.getHeight();
} else {
var im = E.toString(src);
l._h = im.charCodeAt(0);
l._w = im.charCodeAt(1);
}
break;
}
case undefined:
case "custom": {
// size should already be set up in width/height
l._w = 0;
l._h = 0;
break;
}
case "h": {
l.c.forEach(updateMin);
l._h = l.c.reduce((a,b)=>Math.max(a,b._h+(b.pad<<1)),0);
l._w = l.c.reduce((a,b)=>a+b._w+(b.pad<<1),0);
l.fillx |= l.c.some(c=>c.fillx);
break;
}
case "v": {
l.c.forEach(updateMin);
l._h = l.c.reduce((a,b)=>a+b._h+(b.pad<<1),0);
l._w = l.c.reduce((a,b)=>Math.max(a,b._w+(b.pad<<1)),0);
l.filly |= l.c.some(c=>c.filly);
break;
}
default: throw "Unknown item type "+l.type;
}
if (l.r&1) { // rotation
var t = l._w;l._w=l._h;l._h=t;
}
l._w = Math.max(l._w, 0|l.width);
l._h = Math.max(l._h, 0|l.height);
} }
function render(l) {
if (!l) l = this.l; Layout.prototype.render = function (l) {
if (!l) l = this._l;
function render(l) {"ram"
g.reset(); g.reset();
if (l.col) g.setColor(l.col); if (l.col) g.setColor(l.col);
if (l.bgCol!==undefined) g.setBgColor(l.bgCol).clearRect(l.x,l.y,l.x+l.w,l.y+l.h); if (l.bgCol!==undefined) g.setBgColor(l.bgCol).clearRect(l.x,l.y,l.x+l.w-1,l.y+l.h-1);
switch (l.type) { cb[l.type](l);
case "txt": }
g.setFont(l.font,l.fsz).setFontAlign(0,0,l.r).drawString(l.label, l.x+(l.w>>1), l.y+(l.h>>1), true/*solid bg*/);
break; var cb = {
case "btn": "":function(){},
"txt":function(l){
g.setFont(l.font,l.fsz).setFontAlign(0,0,l.r).drawString(l.label, l.x+(l.w>>1), l.y+(l.h>>1));
}, "btn":function(l){
var poly = [ var poly = [
l.x,l.y+4, l.x,l.y+4,
l.x+4,l.y, l.x+4,l.y,
@ -229,33 +201,41 @@ function render(l) {
l.x,l.y+4 l.x,l.y+4
]; ];
g.setColor(g.theme.bgH).fillPoly(poly).setColor(l.selected ? g.theme.fgH : g.theme.fg).drawPoly(poly).setFont("4x6",2).setFontAlign(0,0,l.r).drawString(l.label,l.x+l.w/2,l.y+l.h/2); g.setColor(g.theme.bgH).fillPoly(poly).setColor(l.selected ? g.theme.fgH : g.theme.fg).drawPoly(poly).setFont("4x6",2).setFontAlign(0,0,l.r).drawString(l.label,l.x+l.w/2,l.y+l.h/2);
break; }, "img":function(l){
case "img":
g.drawImage(l.src(), l.x, l.y); g.drawImage(l.src(), l.x, l.y);
break; }, "custom":function(l){
case "custom":
l.render(l); l.render(l);
break; },"h":function(l) { l.c.forEach(render); },
} "v":function(l) { l.c.forEach(render); }
if (l.c) l.c.forEach(render); };
}
Layout.prototype.render = function (l) { if (this.lazy) {
if (!l) l = this._l; // we have to use 'var' here not 'let', otherwise the minifier
// renames vars to the same name, which causes problems as Espruino
// doesn't yet honour the scoping of 'let'
if (!this.rects) this.rects = {};
var rectsToClear = this.rects.clone();
var drawList = [];
prepareLazyRender(l, rectsToClear, drawList, this.rects, g.getBgColor());
for (var h in rectsToClear) delete this.rects[h];
var clearList = Object.keys(rectsToClear).map(k=>rectsToClear[k]).reverse(); // Rects are cleared in reverse order so that the original bg color is restored
for (var r of clearList) g.setBgColor(r.bg).clearRect.apply(g, r.r);
drawList.forEach(render);
} else { // non-lazy
render(l); render(l);
}
}; };
Layout.prototype.layout = function (l) { Layout.prototype.layout = function (l) {
// l = current layout element // l = current layout element
// exw,exh = extra width/height available // exw,exh = extra width/height available
var fillx = l.c && l.c.reduce((a,l)=>a+(0|l.fillx),0);
var filly = l.c && l.c.reduce((a,l)=>a+(0|l.filly),0);
switch (l.type) { switch (l.type) {
case "h": { case "h": {
let x = l.x + (l.w-l._w)/2; let x = l.x + (l.w-l._w)/2;
var fillx = l.c && l.c.reduce((a,l)=>a+(0|l.fillx),0);
if (fillx) { x = l.x; } if (fillx) { x = l.x; }
l.c.forEach(c => { l.c.forEach(c => {
c.w = c._w + (c.fillx?(l.w-l._w)/fillx:0); c.w = c._w + ((0|c.fillx)*(l.w-l._w)/(fillx||1));
c.h = c.filly ? l.h : c._h; c.h = c.filly ? l.h : c._h;
c.x = x; c.x = x;
c.y = l.y + (1+(0|c.valign))*(l.h-c.h)/2; c.y = l.y + (1+(0|c.valign))*(l.h-c.h)/2;
@ -265,18 +245,17 @@ Layout.prototype.layout = function (l) {
c.w += c.pad*2; c.w += c.pad*2;
c.h += c.pad*2; c.h += c.pad*2;
} }
if (c.c) { if (c.c) this.layout(c);
this.layout(c);
}
}); });
break; break;
} }
case "v": { case "v": {
let y = l.y + (l.h-l._h)/2; let y = l.y + (l.h-l._h)/2;
var filly = l.c && l.c.reduce((a,l)=>a+(0|l.filly),0);
if (filly) { y = l.y; } if (filly) { y = l.y; }
l.c.forEach(c => { l.c.forEach(c => {
c.w = c.fillx ? l.w : c._w; c.w = c.fillx ? l.w : c._w;
c.h = c._h + (c.filly?(l.h-l._h)/filly:0); c.h = c._h + ((0|c.filly)*(l.h-l._h)/(filly||1));
c.x = l.x + (1+(0|c.halign))*(l.w-c.w)/2; c.x = l.x + (1+(0|c.halign))*(l.w-c.w)/2;
c.y = y; c.y = y;
y += c.h; y += c.h;
@ -304,6 +283,62 @@ Layout.prototype.update = function() {
var y = this.yOffset; var y = this.yOffset;
var h = g.getHeight()-y; var h = g.getHeight()-y;
// update sizes // update sizes
function updateMin(l) {"ram"
cb[l.type](l);
if (l.r&1) { // rotation
var t = l._w;l._w=l._h;l._h=t;
}
l._w = Math.max(l._w, 0|l.width);
l._h = Math.max(l._h, 0|l.height);
}
var cb = {
"txt" : function(l) {
if (l.font.endsWith("%"))
l.font = "Vector"+Math.round(g.getHeight()*l.font.slice(0,-1)/100);
// FIXME ':'/fsz not needed in new firmwares - it's handled internally
if (l.font.includes(":")) {
var f = l.font.split(":");
l.font = f[0];
l.fsz = f[1];
}
g.setFont(l.font,l.fsz);
l._h = g.getFontHeight();
l._w = g.stringWidth(l.label);
}, "btn": function(l) {
l._h = 24;
l._w = 14 + l.label.length*8;
}, "img": function(l) {
var src = l.src();
if (typeof(src) === 'object') {
l._h = ("width" in src) ? src.width : src.getWidth();
l._w = ("height" in src) ? src.height : src.getHeight();
} else {
var im = E.toString(src);
l._h = im.charCodeAt(0);
l._w = im.charCodeAt(1);
}
}, "": function(l) {
// size should already be set up in width/height
l._w = 0;
l._h = 0;
}, "custom": function(l) {
// size should already be set up in width/height
l._w = 0;
l._h = 0;
}, "h": function(l) {
l.c.forEach(updateMin);
l._h = l.c.reduce((a,b)=>Math.max(a,b._h+(b.pad<<1)),0);
l._w = l.c.reduce((a,b)=>a+b._w+(b.pad<<1),0);
if (l.c.some(c=>c.fillx)) l.fillx = 1;
if (l.c.some(c=>c.filly)) l.filly = 1;
}, "v": function(l) {
l.c.forEach(updateMin);
l._h = l.c.reduce((a,b)=>a+b._h+(b.pad<<1),0);
l._w = l.c.reduce((a,b)=>Math.max(a,b._w+(b.pad<<1)),0);
if (l.c.some(c=>c.fillx)) l.fillx = 1;
if (l.c.some(c=>c.filly)) l.filly = 1;
}
};
updateMin(l); updateMin(l);
// center // center
if (l.fillx || l.filly) { if (l.fillx || l.filly) {

325
modules/Layout.min.js vendored
View File

@ -1,325 +0,0 @@
/*
Usage:
```
var Layout = require("Layout");
var layout = new Layout( layoutObject, btns )
layout.render(optionalObject);
```
For example:
```
var Layout = require("Layout");
var layout = new Layout( {
type:"v", c: [
{type:"txt", font:"20%", label:"12:00" },
{type:"txt", font:"6x8", label:"The Date" }
]
});
g.clear();
layout.render();
```
layoutObject has:
* A `type` field of:
* `undefined` - blank, can be used for padding
* `"txt"` - a text label, with value `label` and `r` for text rotation. 'font' is required
* `"btn"` - a button, with value `label` and callback `cb`
* `"img"` - an image where the function `src` is called to return an image to draw
* `"custom"` - a custom block where `render(layoutObj)` is called to render
* `"h"` - Horizontal layout, `c` is an array of more `layoutObject`
* `"v"` - Veritical layout, `c` is an array of more `layoutObject`
* A `id` field. If specified the object is added with this name to the
returned `layout` object, so can be referenced as `layout.foo`
* A `font` field, eg `6x8` or `30%` to use a percentage of screen height
* A `col` field, eg `#f00` for red
* A `bgCol` field for background color (will automatically fill on render)
* A `halign` field to set horizontal alignment. `-1`=left, `1`=right, `0`=center
* A `valign` field to set vertical alignment. `-1`=top, `1`=bottom, `0`=center
* A `pad` integer field to set pixels padding
* A `fillx` boolean to choose if the object should fill available space in x
* A `filly` boolean to choose if the object should fill available space in y
* `width` and `height` fields to optionally specify minimum size
btns is an array of objects containing:
* `label` - the text on the button
* `cb` - a callback function
* `cbl` - a callback function for long presses
Once `layout.update()` is called, the following fields are added
to each object:
* `x` and `y` for the top left position
* `w` and `h` for the width and height
* `_w` and `_h` for the **minimum** width and height
Other functions:
* `layout.update()` - update positions of everything if contents have changed
* `layout.debug(obj)` - draw outlines for objects on screen
* `layout.clear(obj)` - clear the given object (you can also just specify `bgCol` to clear before each render)
*/
function Layout(layout, buttons) {
this._l = this.l = layout;
this.b = buttons;
// Do we have >1 physical buttons?
this.physBtns = (process.env.HWVERSION==2) ? 1 : 3;
this.yOffset = Object.keys(global.WIDGETS).length ? 24 : 0;
if (buttons) {
if (this.physBtns >= buttons.length) {
// enough physical buttons
var btnHeight = Math.floor((g.getHeight()-this.yOffset) / this.physBtns);
if (Bangle.btnWatch) Bangle.btnWatch.forEach(clearWatch);
Bangle.btnWatch = [];
if (this.physBtns > 2 && buttons.length==1)
buttons.unshift({label:""}); // pad so if we have a button in the middle
while (this.physBtns > buttons.length)
buttons.push({label:""});
if (buttons[0]) Bangle.btnWatch.push(setWatch(pressHandler.bind(this,0), BTN1, {repeat:true,edge:-1}));
if (buttons[1]) Bangle.btnWatch.push(setWatch(pressHandler.bind(this,1), BTN2, {repeat:true,edge:-1}));
if (buttons[2]) Bangle.btnWatch.push(setWatch(pressHandler.bind(this,2), BTN3, {repeat:true,edge:-1}));
this._l.width = g.getWidth()-8; // text width
this._l = {type:"h", filly:1, c: [
this._l,
{type:"v", pad:1, filly:1, c: buttons.map(b=>(b.type="txt",b.font="6x8",b.height=btnHeight,b.r=1,b))}
]};
} else {
var btnHeight = Math.floor((g.getHeight()-this.yOffset) / buttons.length);
this._l.width = g.getWidth()-20; // button width
this._l = {type:"h", c: [
this._l,
{type:"v", c: buttons.map(b=>(b.type="btn",b.h=btnHeight,b.w=32,b.r=1,b))}
]};
}
}
if (process.env.HWVERSION==2) {
Bangle.touchHandler = (_,e) => touchHandler(layout,e);
Bangle.on('touch',Bangle.touchHandler);
}
// add IDs
var ll = this;
function idRecurser(l) {
if (l.id) ll[l.id] = l;
if (l.c) l.c.forEach(idRecurser);
}
idRecurser(layout);
this.update();
}
Layout.prototype.remove = function (l) {
if (Bangle.btnWatch) {
Bangle.btnWatch.forEach(clearWatch);
delete Bangle.btnWatch;
}
if (Bangle.touchHandler) {
Bangle.removeListener("touch",Bangle.touchHandler);
delete Bangle.touchHandler;
}
};
// Handler for button watch events
function pressHandler(btn,e) {
if (e.time-e.lastTime > 0.75 && this.b[btn].cbl)
this.b[btn].cbl(e);
else
if (this.b[btn].cb) this.b[btn].cb(e);
}
// Handler for touch events
function touchHandler(l,e) {
if (l.type=="btn" && l.cb && e.x>=l.x && e.y>=l.y && e.x<=l.x+l.w && e.y<=l.y+l.h)
l.cb(e);
if (l.c) l.c.forEach(n => touchHandler(n,e));
}
function updateMin(l) {
switch (l.type) {
case "txt": {
if (l.font.endsWith("%"))
l.font = "Vector"+Math.round(g.getHeight()*l.font.slice(0,-1)/100);
// FIXME ':'/fsz not needed in new firmwares - it's handled internally
if (l.font.includes(":")) {
var f = l.font.split(":");
l.font = f[0];
l.fsz = f[1];
}
g.setFont(l.font,l.fsz);
l._h = g.getFontHeight();
l._w = g.stringWidth(l.label);
break;
}
case "btn": {
l._h = 24;
l._w = 14 + l.label.length*8;
break;
}
case "img": {
var im = E.toString(l.src());
l._h = im.charCodeAt(0);
l._w = im.charCodeAt(1);
break;
}
case undefined:
case "custom": {
// size should already be set up in width/height
l._w = 0;
l._h = 0;
break;
}
case "h": {
l.c.forEach(updateMin);
l._h = l.c.reduce((a,b)=>Math.max(a,b._h+(b.pad<<1)),0);
l._w = l.c.reduce((a,b)=>a+b._w+(b.pad<<1),0);
l.fillx |= l.c.some(c=>c.fillx);
break;
}
case "v": {
l.c.forEach(updateMin);
l._h = l.c.reduce((a,b)=>a+b._h+(b.pad<<1),0);
l._w = l.c.reduce((a,b)=>Math.max(a,b._w+(b.pad<<1)),0);
l.filly |= l.c.some(c=>c.filly);
break;
}
default: throw "Unknown item type "+l.type;
}
if (l.r&1) { // rotation
var t = l._w;l._w=l._h;l._h=t;
}
l._w = Math.max(l._w, 0|l.width);
l._h = Math.max(l._h, 0|l.height);
}
function render(l) {
if (!l) l = this.l;
g.reset();
if (l.col) g.setColor(l.col);
if (l.bgCol!==undefined) g.setBgColor(l.bgCol).clearRect(l.x,l.y,l.x+l.w,l.y+l.h);
switch (l.type) {
case "txt":
g.setFont(l.font,l.fsz).setFontAlign(0,0,l.r).drawString(l.label, l.x+(l.w>>1), l.y+(l.h>>1), true/*solid bg*/);
break;
case "btn":
var poly = [
l.x,l.y+4,
l.x+4,l.y,
l.x+l.w-5,l.y,
l.x+l.w-1,l.y+4,
l.x+l.w-1,l.y+l.h-5,
l.x+l.w-5,l.y+l.h-1,
l.x+4,l.y+l.h-1,
l.x,l.y+l.h-5,
l.x,l.y+4
];
g.setColor(g.theme.bgH).fillPoly(poly).setColor(l.selected ? g.theme.fgH : g.theme.fg).drawPoly(poly).setFont("4x6",2).setFontAlign(0,0,l.r).drawString(l.label,l.x+l.w/2,l.y+l.h/2);
break;
case "img":
g.drawImage(l.src(), l.x, l.y);
break;
case "custom":
l.render(l);
break;
}
if (l.c) l.c.forEach(render);
}
Layout.prototype.render = function (l) {
if (!l) l = this._l;
render(l);
};
Layout.prototype.layout = function (l) {
// l = current layout element
// exw,exh = extra width/height available
var fillx = l.c && l.c.reduce((a,l)=>a+(0|l.fillx),0);
var filly = l.c && l.c.reduce((a,l)=>a+(0|l.filly),0);
switch (l.type) {
case "h": {
let x = l.x + (l.w-l._w)/2;
if (fillx) { x = l.x; }
l.c.forEach(c => {
c.w = c._w + (c.fillx?(l.w-l._w)/fillx:0);
c.h = c.filly ? l.h : c._h;
c.x = x;
c.y = l.y + (1+(0|c.valign))*(l.h-c.h)/2;
x += c.w;
if (c.pad) {
x += c.pad*2;
c.w += c.pad*2;
c.h += c.pad*2;
}
if (c.c) {
this.layout(c);
}
});
break;
}
case "v": {
let y = l.y + (l.h-l._h)/2;
if (filly) { y = l.y; }
l.c.forEach(c => {
c.w = c.fillx ? l.w : c._w;
c.h = c._h + (c.filly?(l.h-l._h)/filly:0);
c.x = l.x + (1+(0|c.halign))*(l.w-c.w)/2;
c.y = y;
y += c.h;
if (c.pad) {
y += c.pad*2;
c.w += c.pad*2;
c.h += c.pad*2;
}
if (c.c) this.layout(c);
});
break;
}
}
};
Layout.prototype.debug = function(l,c) {
if (!l) l = this._l;
c=c||1;
g.setColor(c&1,c&2,c&4).drawRect(l.x+c-1, l.y+c-1, l.x+l.w-c, l.y+l.h-c);
c++;
if (l.c) l.c.forEach(n => this.debug(n,c));
};
Layout.prototype.update = function() {
var l = this._l;
var w = g.getWidth();
var y = this.yOffset;
var h = g.getHeight()-y;
// update sizes
updateMin(l);
// center
if (l.fillx || l.filly) {
l.w = w;
l.h = h;
l.x = 0;
l.y = y;
} else {
l.w = l._w;
l.h = l._h;
l.x = (w-l.w)/2;
l.y = y+(h-l.h)/2;
}
// layout children
this.layout(l);
};
Layout.prototype.clear = function(l) {
if (!l) l = this._l;
g.reset();
if (l.bgCol!==undefined) g.setBgColor(l.bgCol);
g.clearRect(l.x,l.y,l.x+l.w-1,l.y+l.h-1);
};
exports = Layout;