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

3
.gitignore vendored
View File

@ -6,4 +6,5 @@ package-lock.json
appdates.csv
.vscode
.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
* 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
try and keep filenames short to avoid overflowing the buffer.
* Create a folder called `apps/<id>`, lets assume `apps/7chname`

142
apps.json
View File

@ -118,7 +118,7 @@
{ "id": "welcome",
"name": "Welcome",
"icon": "app.png",
"version":"0.11",
"version":"0.12",
"description": "Appears at first boot and explains how to use Bangle.js",
"tags": "start,welcome",
"allow_emulator":true,
@ -136,7 +136,7 @@
"name": "Customised Welcome",
"shortName": "My Welcome",
"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",
"tags": "start,welcome",
"custom":"custom.html",
@ -153,7 +153,7 @@
{ "id": "gbridge",
"name": "Gadgetbridge",
"icon": "app.png",
"version":"0.23",
"version":"0.24",
"description": "The default notification handler for Gadgetbridge notifications from Android",
"tags": "tool,system,android,widget",
"readme": "README.md",
@ -532,7 +532,7 @@
{ "id": "heart",
"name": "Heart Rate Recorder",
"icon": "app.png",
"version":"0.05",
"version":"0.06",
"interface": "interface.html",
"description": "Application that allows you to record your heart rate. Can run in background",
"tags": "tool,health,widget",
@ -571,7 +571,7 @@
{ "id": "weather",
"name": "Weather",
"icon": "icon.png",
"version":"0.05",
"version":"0.07",
"description": "Show Gadgetbridge weather report",
"readme": "readme.md",
"tags": "widget,outdoors",
@ -639,10 +639,11 @@
"name": "Battery Level Widget (with percentage)",
"shortName": "Battery Widget",
"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",
"tags": "widget,battery",
"tags": "widget,battery,b2",
"type":"widget",
"readme": "README.md",
"storage": [
{"name":"widbatpc.wid.js","url":"widget.js"},
{"name":"widbatpc.settings.js","url":"settings.js"}
@ -721,7 +722,7 @@
{ "id": "widhrm",
"name": "Simple Heart Rate widget",
"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.",
"tags": "health,widget",
"type": "widget",
@ -1287,7 +1288,7 @@
{ "id": "wohrm",
"name": "Workout HRM",
"icon": "app.png",
"version":"0.07",
"version":"0.08",
"readme": "README.md",
"description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.",
"tags": "hrm,workout",
@ -1466,7 +1467,7 @@
"id": "balltastic",
"name": "Balltastic",
"icon": "app.png",
"version": "0.01",
"version": "0.02",
"description": "Simple but fun ball eats dots game.",
"tags": "game,fun",
"type": "app",
@ -1517,7 +1518,7 @@
"name": "OpenStreetMap",
"shortName":"OpenStMap",
"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",
"tags": "outdoors,gps,b2",
"custom": "custom.html", "customConnect":true,
@ -1895,7 +1896,7 @@
{ "id": "ballmaze",
"name": "Ball Maze",
"icon": "icon.png",
"version": "0.01",
"version": "0.02",
"description": "Navigate a ball through a maze by tilting your watch.",
"readme": "README.md",
"tags": "game",
@ -1944,26 +1945,16 @@
"id": "largeclock",
"name": "Large Clock",
"icon": "largeclock.png",
"version": "0.09",
"version": "0.10",
"description": "A readable and informational digital watch, with date, seconds and moon phase",
"readme": "README.md",
"tags": "clock",
"type": "clock",
"allow_emulator": true,
"storage": [
{
"name": "largeclock.app.js",
"url": "largeclock.js"
},
{
"name": "largeclock.img",
"url": "largeclock-icon.js",
"evaluate": true
},
{
"name": "largeclock.settings.js",
"url": "settings.js"
}
{"name": "largeclock.app.js", "url": "largeclock.js"},
{"name": "largeclock.img", "url": "largeclock-icon.js", "evaluate": true},
{"name": "largeclock.settings.js", "url": "settings.js"}
],
"data": [
{"name":"largeclock.json"}
@ -3265,14 +3256,15 @@
"name": "Hour Strike",
"shortName": "Hour Strike",
"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!",
"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}
{"name":"hourstrike.img","url":"app-icon.js","evaluate":true},
{"name":"hourstrike.json","url":"hourstrike.json"}
]
},
{ "id": "whereworld",
@ -3430,13 +3422,103 @@
"name": "Car Crazy",
"shortName":"Car Crazy",
"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.",
"tags": "game",
"readme": "README.md",
"storage": [
{"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.02: Set LCD timeout for Espruino 2v10 compatibility

View File

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

View File

@ -1 +1,2 @@
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.setLCDMode("doublebuffered");
Bangle.setLCDTimeout(0);
let points = 0;
let level = 1;

View File

@ -1 +1,3 @@
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 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:
(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.
### 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"));
function consoleDebug(message) {
//console.log(message);
}
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
@ -30,21 +34,55 @@ function getRandomInt(min, max) {
function moveEnemyPosition(){
score += 1;
randomRoadPositionIndicator = getRandomInt(1, 4);
if ((randomRoadPositionIndicator == 1)) {
randomRoadPosition = 85;
}else if((randomRoadPositionIndicator == 2)){
randomRoadPosition = 120;
}else {
randomRoadPosition = 155;
checkForNextLevel();
if(level == 1){
randomRoadPositionIndicator = getRandomInt(1, 4);
if ((randomRoadPositionIndicator == 1)) {
enemyPositonCenterX = 85;
}else if((randomRoadPositionIndicator == 2)){
enemyPositonCenterX = 120;
}else {
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(){
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
(
(enemyCarFrontY > playerCarFrontY)
(enemyCarFrontY < 300 && enemyCarFrontY > playerCarFrontY)
&&
(
(enemyCarLeftX > playerCarLeftX && enemyCarLeftX < playerCarRightX)
@ -52,51 +90,105 @@ function collision(){
(enemyCarRightX > playerCarLeftX && enemyCarRightX < playerCarRightX)
)
){
// hit
setTimeout(collision, 2500); // wait 2.5 second for the function to actiavte agian.
numberofHearts -= 1;
score -= 1;
Bangle.buzz();
}else{
// miss
setTimeout(collision, 1); // try again in 1 milliseconds.
// hit car 1
consoleDebug("1 HIT");
enemyPositonCenterY = 300;
numberofHearts -= 1;
Bangle.buzz(50,50);
}else if
(
(enemyCarFrontY2 < 300 && enemyCarFrontY2 > playerCarFrontY)
&&
(
(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) {
// ensure there are less than 500 elements in the array
while (log.length >= 500) log.shift();
// append a new item to the array
log.push(data);
function checkForNextLevel(){
if(score < 10){
level = 1;
}else if(score >= 10 && score < 20){
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 currentHighScore = file.readLine();
if (currentHighScore == undefined) currentHighScore = 0;
var BackgroundStartingPosition = 75;
var carScale = 0.5;
var accel = Bangle.getAccel();
var playerCarPosition = 120-accel.x*40;
var BackgroundYPosition = BackgroundStartingPosition;
var randomRoadPositionIndicator = getRandomInt(1, 3);
var randomRoadPosition = 120;
var enemyPositonY = 30;
var randomRoadPositionIndicator;
var randomRoadPositionIndicator2;
var enemyPositonCenterX;
var enemyPositonCenterX2;
var carScale = 0.5;
var carWidth = 30;
var carHeight = 60;
var playerCarY = 130;
var enemyCarLeftX;
var enemyCarRightX;
var playerCarCenterY = 130;
var playerCarCenterX;
var enemyPositonCenterY = 0 - carHeight/2;
var enemyPositonCenterY2 = 0 - carHeight/2;
var playerCarLeftX;
var playerCarRightX;
var enemyCarFrontY;
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 GAMEOVER = 2;
var GAMESTART = 3;
var gameStatus = GAMESTART;
var score = 0;
var level = 1;
moveEnemyPosition();
collision();
@ -114,11 +206,16 @@ function draw(){
if(gameStatus == GAMEPLAYING){
BackgroundYPosition += 10;
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.drawImage(backgroundImage,125,BackgroundYPosition, {scale:13,rotate:0});
g.drawImage(RedCar,playerCarPosition,playerCarY, {scale:carScale,rotate:3.142});
g.drawImage(OrangeCar,randomRoadPosition,enemyPositonY, {scale:carScale,rotate:0});
g.drawImage(RedCar,playerCarCenterX,playerCarCenterY, {scale:carScale,rotate:3.142});
g.drawImage(OrangeCar,enemyPositonCenterX,enemyPositonCenterY, {scale:carScale,rotate:0});
if(level>=4){
g.drawImage(OrangeCar,enemyPositonCenterX2,enemyPositonCenterY2, {scale:carScale,rotate:0});
}
if(numberofHearts==3){
g.drawImage(heartImage,10,10, {scale:2,rotate:0});
@ -141,23 +238,38 @@ function draw(){
}
}
playerCarFrontY = playerCarY-carHeight/2;
playerCarBackY = playerCarY+carHeight/2;
playerCarLeftX = playerCarPosition-carWidth/2;
playerCarRightX = playerCarPosition+carWidth/2;
playerCarFrontY = playerCarCenterY-carHeight/2;
playerCarBackY = playerCarCenterY+carHeight/2;
playerCarLeftX = playerCarCenterX-carWidth/2;
playerCarRightX = playerCarCenterX+carWidth/2;
enemyCarFrontY = enemyPositonY+carHeight/2;
enemyCarBackY = enemyPositonY-carHeight/2;
enemyCarLeftX = randomRoadPosition-carWidth/2;
enemyCarRightX = randomRoadPosition+carWidth/2;
enemyCarFrontY = enemyPositonCenterY+carHeight/2;
enemyCarBackY = enemyPositonCenterY-carHeight/2;
enemyCarLeftX = enemyPositonCenterX-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(enemyCarLeftX, enemyCarFrontY, enemyCarRightX, enemyCarBackY);
//g.drawRect(enemyCarLeftX2, enemyCarFrontY2, enemyCarRightX2, enemyCarBackY2);
g.setColor(0,0,0);
g.drawString("Score: "+score,180,5);
g.drawString("HighScore:",178,15);
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){
BackgroundYPosition = BackgroundStartingPosition;
@ -200,41 +312,75 @@ function draw(){
g.drawString("2 To Start",10,130);
g.drawImage(LightGreenCar,180,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(){
if(gameStatus == GAMEPLAYING){
enemyPositonY = enemyPositonY + 10;
if((enemyPositonY > 200)){
enemyPositonY = 30;
if(level==1||level==2){
enemyPositonCenterY = enemyPositonCenterY + 10;
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();
}
}
}
setInterval(moveEnemyCar,10);
setInterval(moveEnemyCar,50);
setWatch(() => {
if(gameStatus == GAMESTART){
gameStatus = GAMEPLAYING;
collision();
enemyPositonY = 0;
numberofHearts = 3;
enemyPositonCenterX = 120;
enemyPositonCenterY = 0 - carHeight/2;
enemyPositonCenterX2 = 120;
enemyPositonCenterY2 = 0 - carHeight/2;
score = 0;
level = 1;
checkForNextLevel();
}else if(gameStatus == GAMEOVER){
gameStatus = GAMEPLAYING;
collision();
enemyPositonY = 0;
enemyPositonCenterX = 120;
enemyPositonCenterY = 0 - carHeight/2;
enemyPositonCenterX2 = 120;
enemyPositonCenterY2 = 0 - carHeight/2;
numberofHearts = 3;
score = 0;
level = 1;
checkForNextLevel();
}
}, 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.22: Respect Quiet Mode
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)
clearInterval(activityInterval);
activityInterval = undefined;
if (s.hrm) Bangle.setHRMPower(1);
if (s.hrm) Bangle.setHRMPower(1,"gbr");
if (s.hrm) {
if (realtime) {
// if realtime reporting, leave HRM on and use that to trigger events
@ -138,7 +138,7 @@
hrmTimeout = 5;
activityInterval = setInterval(function() {
hrmTimeout = 5;
Bangle.setHRMPower(1);
Bangle.setHRMPower(1,"gbr");
}, interval*1000);
}
} else {
@ -281,7 +281,7 @@
if (hrmTimeout!==undefined) hrmTimeout--;
if (ok || hrmTimeout<=0) {
if (hrmTimeout!==undefined)
Bangle.setHRMPower(0);
Bangle.setHRMPower(0,"gbr");
sendActivity(hrm.confidence>20 ? hrm.bpm : -1);
}
});

View File

@ -11,3 +11,4 @@
Reduce memory usage by ~30%
Generate scale based on defined minimum and maximum measurement
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) {
WIDGETS["heart"].width = 24;
Bangle.on('HRM',onHRM);
Bangle.setHRMPower(1);
Bangle.setHRMPower(1,"heart");
var n = settings.fileNbr.toString(36);
recFile = require("Storage").open(".heart"+n,"a");
} else {
WIDGETS["heart"].width = 0;
Bangle.setHRMPower(0);
Bangle.setHRMPower(0,"heart");
recFile = undefined;
}
}

View File

@ -5,3 +5,4 @@
0.05: Add display for the next strike time
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.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');
let settings;
var settings = storage.readJSON('hourstrike.json', 1);
function updateSettings() {
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() {
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];

View File

@ -1,6 +1,6 @@
(function() {
function setup () {
var settings = require('Storage').readJSON('hourstrike.json',1)||[];
var settings = require('Storage').readJSON('hourstrike.json',1);
var t = new Date();
var t_min_sec = t.getMinutes()*60+t.getSeconds();
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.08: Use Bangle.setUI for button/launcher handling
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
- 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.
- Configure the text direction on the side depending on the wrist on which you wear your watch.
## How to use it

View File

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

View File

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

View File

@ -28,7 +28,8 @@
const settings = s.readJSON("largeclock.json", 1) || {
BTN1: "",
BTN3: ""
BTN3: "",
right_hand: false
};
function showApps(btn) {
@ -67,10 +68,19 @@
}
const mainMenu = {
"": { title: "Large Clock Settings" },
"": { title: "Large Clock" },
"< Back": back,
"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);
});

View File

@ -13,3 +13,4 @@
BTN2 now goes to menu on release
0.10: Add birthday style
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(()=>{
// 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.07: Move to 96px tiles - less files (64 -> 25) and speed up rendering
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;
Bangle.on('GPS',function(f) {
fix=f;
g.clearRect(0,y1,240,y1+8);
g.setColor(1,1,1);
g.setFont("6x8");
g.setFontAlign(0,0);
g.reset().clearRect(0,y1,240,y1+8).setFont("6x8").setFontAlign(0,0);
var txt = fix.satellites+" satellites";
if (!fix.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

@ -1,4 +1,6 @@
0.02: Make minor adjustments to widget, and discard stale weather data after a configurable period.
0.03: Fix flickering last updated time.
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() {
let w = weather.current;
g.reset();
g.setColor(0).fillRect(0, 24, 239, 239);
g.clearRect(0, 24, 239, 239);
weather.drawIcon(w.txt, 65, 90, 55);
const locale = require("locale");
g.setColor(-1);
g.reset();
const temp = locale.temp(w.temp-273.15).match(/^(\D*\d*)(.*)$/);
let width = g.setFont("Vector", 40).stringWidth(temp[1]);
@ -54,8 +54,8 @@
if (!weather.current || !weather.current.time) return;
let text = `Last update received ${formatDuration(Date.now() - weather.current.time)} ago`;
g.reset();
g.setColor(0).fillRect(0, 202, 239, 210);
g.setColor(-1).setFont("6x8", 1).setFontAlign(0, 0, 0);
g.clearRect(0, 202, 239, 210);
g.setFont("6x8", 1).setFontAlign(0, 0, 0);
g.drawString(text, 120, 206);
}
@ -88,7 +88,7 @@
update();
// Show launcher when middle button pressed
setWatch(Bangle.showLauncher, BTN2, {repeat: false, edge: 'falling'});
Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();

View File

@ -68,13 +68,13 @@ setCurrentWeather(storage.readJSON('weather.json')||{});
exports.drawIcon = function(cond, x, y, r) {
function drawSun(x, y, r) {
g.setColor("#FF7700");
g.setColor(g.theme.dark ? "#FE0" : "#FC0");
g.fillCircle(x, y, r);
}
function drawCloud(x, y, r, c) {
const u = r/12;
if (c==null) c = "#EEEEEE";
if (c==null) c = g.theme.dark ? "#BBB" : "#AAA";
g.setColor(c);
g.fillCircle(x-8*u, y+3*u, 4*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) {
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);
}
@ -101,24 +101,25 @@ exports.drawIcon = function(cond, 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 y2 = y+1*r;
g.fillPoly([
x-6/12*r+1, y1,
x-8/12*r+1, y2,
g.fillPolyAA([
x-6/12*r, y1,
x-8/12*r, y2,
x-7/12*r, y2,
x-5/12*r, y1,
]);
g.fillPoly([
x-2/12*r+1, y1,
x-4/12*r+1, y2,
g.fillPolyAA([
x-2/12*r, y1,
x-4/12*r, y2,
x-3/12*r, y2,
x-1/12*r, y1,
]);
g.fillPoly([
x+2/12*r+1, y1,
x+0/12*r+1, y2,
g.fillPolyAA([
x+2/12*r, y1,
x+0/12*r, y2,
x+1/12*r, y2,
x+3/12*r, y1,
]);
@ -136,7 +137,7 @@ exports.drawIcon = function(cond, x, y, r) {
function drawThunderstorm(x, y, r) {
function drawLightning(x, y, r) {
g.setColor("#FF7700");
g.setColor(g.theme.dark ? "#FE0" : "#FC0");
g.fillPoly([
x-2/6*r, y-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;
for(let i = 0; i<=6; ++i) {
const points = [
@ -199,7 +200,7 @@ exports.drawIcon = function(cond, x, y, r) {
[-0.2, 0.3],
];
g.setColor("#FFFFFF");
g.setColor(g.theme.dark ? "#FFF" : "#CCC");
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,
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) {
if (!condition) return () => {};
condition = condition.toLowerCase();
@ -225,7 +231,18 @@ exports.drawIcon = function(cond, x, y, r) {
if (condition.includes("few clouds")) return drawFewClouds;
if (condition.includes("scattered clouds")) return drawCloud;
if (condition.includes("clouds")) return drawBrokenClouds;
return drawMist;
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 drawUnknown;
}
chooseIcon(cond)(x, y, r);

View File

@ -5,16 +5,16 @@
const w = weather.current;
if (!w) return;
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) {
weather.drawIcon(w.txt, this.x+10, this.y+8, 7.5);
}
if (w.temp) {
let t = require('locale').temp(w.temp-273.15); // applies conversion
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.setFont('6x8', 1);
g.setColor(-1);
g.drawString(t, this.x+10, this.y+24);
}
}

View File

@ -13,3 +13,4 @@
BTN2 now goes to menu on release
0.10: Tweaks to reduce memory usage
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(()=>{
// If we're on the last page

View File

@ -8,3 +8,4 @@
0.09: Fix regression stopping correct widget updates
0.10: Add 'hide if charge greater than'
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(){
const COLORS = {
'white': -1,
'charging': 0x07E0, // "Green"
'high': 0x05E0, // slightly darker green
'ok': 0xFD20, // "Orange"
'low':0xF800, // "Red"
let COLORS = {};
if (process.env.HWVERSION == 1) {
COLORS = {
'white': -1, // White
'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'
@ -66,29 +79,28 @@
// else...
var s = 39;
var x = this.x, y = this.y;
const l = E.getBattery(),
c = levelColor(l);
const l = E.getBattery();
const xl = x+4+l*(s-12)/100
c = levelColor(l);
if (Bangle.isCharging() && setting('charger')) {
g.setColor(chargerColor()).drawImage(atob(
"DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y);
x+=16;
}
g.setColor(-1);
g.setColor(g.theme.fg);
g.fillRect(x,y+2,x+s-4,y+21);
g.clearRect(x+2,y+4,x+s-6,y+19);
g.fillRect(x+s-3,y+10,x+s,y+14);
g.setColor(c).fillRect(x+4,y+6,xl,y+17);
g.setColor(-1);
g.setColor(g.theme.fg);
if (!setting('percentage')) {
return;
}
let gfx = g
if (setting('color') === 'Monochrome') {
// 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)})
}
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.02: Tweaks for 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
Bangle.on('lcdPower', function(on) {
if (on) {
Bangle.setHRMPower(1);
Bangle.setHRMPower(1,"widhrm");
firstBPM = true;
currentBPM = undefined;
WIDGETS["hrm"].draw();
} else {
Bangle.setHRMPower(0);
Bangle.setHRMPower(0,"widhrm");
}
});
@ -44,7 +44,7 @@
}
WIDGETS["hrm"].draw();
});
Bangle.setHRMPower(Bangle.isLCDOn());
Bangle.setHRMPower(Bangle.isLCDOn(),"widhrm");
// add your widget
WIDGETS["hrm"]={area:"tl",width:24,draw:draw};

View File

@ -4,4 +4,5 @@
0.04: Only buzz on high confidence (>85%)
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.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

@ -4,7 +4,7 @@ const Setter = {
UPPER: 'upper',
LOWER: 'lower'
};
const shortBuzzTimeInMs = 80;
const longBuzzTimeInMs = 400;
@ -164,12 +164,12 @@ function renderPlusMinusIcons() {
} else {
g.setColor(1, 1, 1);
}
g.setFontVector(14);
//+ for Btn1
g.drawString("+", 222, 50);
//- for Btn3
g.drawString("-", 222,165);
@ -288,7 +288,7 @@ function resetHighlightTimeout() {
}
function switchOffApp(){
Bangle.setHRMPower(0);
Bangle.setHRMPower(0,"wohrm");
Bangle.showLauncher();
}
@ -306,7 +306,7 @@ Bangle.on('lcdPower', (on) => {
}
});
Bangle.setHRMPower(1);
Bangle.setHRMPower(1,"wohrm");
Bangle.on('HRM', onHrm);
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.';
}
var RECOMMENDED_VERSION = "2v09";
var RECOMMENDED_VERSION = "2v10";
// could check http://www.espruino.com/json/BANGLEJS.json for this
(function() {

View File

@ -1,11 +1,10 @@
/*
Usage:
```
var Layout = require("Layout");
var layout = new Layout( layoutObject, btns )
var layout = new Layout( layoutObject, btns, options )
layout.render(optionalObject);
```
@ -42,8 +41,8 @@ layoutObject has:
* 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
* A `fillx` int to choose if the object should fill available space in x. 0=no, 1=yes, 2=2x more space
* 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
btns is an array of objects containing:
@ -52,6 +51,13 @@ btns is an array of objects containing:
* `cb` - a callback function
* `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
to each object:
@ -69,17 +75,20 @@ Other functions:
*/
function Layout(layout, buttons) {
function Layout(layout, buttons, options) {
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) {
options = options || {};
this.lazy = options.lazy || false;
if (buttons) {
if (this.physBtns >= buttons.length) {
// 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);
Bangle.btnWatch = [];
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))}
]};
} 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 = {type:"h", c: [
this._l,
@ -104,7 +113,7 @@ function Layout(layout, buttons) {
}
}
if (process.env.HWVERSION==2) {
Bangle.touchHandler = (_,e) => touchHandler(layout,e);
Bangle.touchHandler = function(_,e){touchHandler(layout,e)};
Bangle.on('touch',Bangle.touchHandler);
}
@ -112,6 +121,7 @@ function Layout(layout, buttons) {
var ll = this;
function idRecurser(l) {
if (l.id) ll[l.id] = l;
if (!l.type) l.type="";
if (l.c) l.c.forEach(idRecurser);
}
idRecurser(layout);
@ -144,79 +154,41 @@ function touchHandler(l,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) {
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];
if (!delete rectsToClear[hash]) {
rects[hash] = {bg: bgCol, r: [l.x,l.y,l.x+l.w-1,l.y+l.h-1]};
if (drawList) {
drawList.push(l);
drawList = null; // Prevent children from being redundantly added to the drawList
}
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 (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);
if (l.c) for (let ch of l.c) prepareLazyRender(ch, rectsToClear, drawList, rects, l.bgCol == null ? bgCol : l.bgCol);
}
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":
Layout.prototype.render = function (l) {
if (!l) l = this._l;
function render(l) {"ram"
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-1,l.y+l.h-1);
cb[l.type](l);
}
var cb = {
"":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 = [
l.x,l.y+4,
l.x+4,l.y,
@ -229,33 +201,41 @@ function render(l) {
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":
}, "img":function(l){
g.drawImage(l.src(), l.x, l.y);
break;
case "custom":
}, "custom":function(l){
l.render(l);
break;
}
if (l.c) l.c.forEach(render);
}
},"h":function(l) { l.c.forEach(render); },
"v":function(l) { l.c.forEach(render); }
};
Layout.prototype.render = function (l) {
if (!l) l = this._l;
render(l);
if (this.lazy) {
// 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);
}
};
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;
var fillx = l.c && l.c.reduce((a,l)=>a+(0|l.fillx),0);
if (fillx) { x = l.x; }
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.x = x;
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.h += c.pad*2;
}
if (c.c) {
this.layout(c);
}
if (c.c) this.layout(c);
});
break;
}
case "v": {
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; }
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.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.y = y;
y += c.h;
@ -304,6 +283,62 @@ Layout.prototype.update = function() {
var y = this.yOffset;
var h = g.getHeight()-y;
// 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);
// center
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;