mirror of https://github.com/espruino/BangleApps
commit
eb92dbc769
|
@ -10,3 +10,5 @@ Changed for individual apps are listed in `apps/appname/ChangeLog`
|
|||
* Add `Favourite` functionality
|
||||
* Version number now clickable even when you're at the latest version (fix #291)
|
||||
* Rewrite 'getInstalledApps' to minimize RAM usage
|
||||
* Added code to handle Settings
|
||||
* Added espruinotools.js for pretokenisation
|
||||
|
|
89
apps.json
89
apps.json
|
@ -2,7 +2,7 @@
|
|||
{ "id": "boot",
|
||||
"name": "Bootloader",
|
||||
"icon": "bootloader.png",
|
||||
"version":"0.14",
|
||||
"version":"0.15",
|
||||
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
|
||||
"tags": "tool,system",
|
||||
"type":"bootloader",
|
||||
|
@ -53,7 +53,7 @@
|
|||
{ "id": "about",
|
||||
"name": "About",
|
||||
"icon": "app.png",
|
||||
"version":"0.04",
|
||||
"version":"0.05",
|
||||
"description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers",
|
||||
"tags": "tool,system",
|
||||
"allow_emulator":true,
|
||||
|
@ -122,9 +122,10 @@
|
|||
{ "id": "setting",
|
||||
"name": "Settings",
|
||||
"icon": "settings.png",
|
||||
"version":"0.18",
|
||||
"version":"0.19",
|
||||
"description": "A menu for setting up Bangle.js",
|
||||
"tags": "tool,system",
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"setting.app.js","url":"settings.js"},
|
||||
{"name":"setting.boot.js","url":"boot.js"},
|
||||
|
@ -163,10 +164,23 @@
|
|||
{"name":"wclock.img","url":"clock-word-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "impwclock",
|
||||
"name": "Imprecise Word Clock",
|
||||
"icon": "clock-impword.png",
|
||||
"version":"0.01",
|
||||
"description": "Imprecise word clock for vacations, weekends, and those who never need accurate time.",
|
||||
"tags": "clock",
|
||||
"type":"clock",
|
||||
"allow_emulator":true,
|
||||
"storage": [
|
||||
{"name":"impwclock.app.js","url":"clock-impword.js"},
|
||||
{"name":"impwclock.img","url":"clock-impword-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "aclock",
|
||||
"name": "Analog Clock",
|
||||
"icon": "clock-analog.png",
|
||||
"version": "0.11",
|
||||
"version": "0.12",
|
||||
"description": "An Analog Clock",
|
||||
"tags": "clock",
|
||||
"type":"clock",
|
||||
|
@ -467,7 +481,7 @@
|
|||
"name": "Bluetooth Music Controls",
|
||||
"shortName": "Music Control",
|
||||
"icon": "hid-music.png",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "Enable HID in settings, pair with your phone, then use this app to control music from your watch!",
|
||||
"tags": "bluetooth",
|
||||
"storage": [
|
||||
|
@ -479,7 +493,7 @@
|
|||
"name": "Bluetooth Keyboard",
|
||||
"shortName": "Bluetooth Kbd",
|
||||
"icon": "hid-keyboard.png",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "Enable HID in settings, pair with your phone/PC, then use this app to control other apps",
|
||||
"tags": "bluetooth",
|
||||
"storage": [
|
||||
|
@ -491,7 +505,7 @@
|
|||
"name": "Binary Bluetooth Keyboard",
|
||||
"shortName": "Binary BT Kbd",
|
||||
"icon": "hid-binary-keyboard.png",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "Enable HID in settings, pair with your phone/PC, then type messages using the onscreen keyboard by tapping repeatedly on the key you want",
|
||||
"tags": "bluetooth",
|
||||
"storage": [
|
||||
|
@ -1012,7 +1026,7 @@
|
|||
{ "id": "barclock",
|
||||
"name": "Bar Clock",
|
||||
"icon": "clock-bar.png",
|
||||
"version":"0.04",
|
||||
"version":"0.05",
|
||||
"description": "A simple digital clock showing seconds as a bar",
|
||||
"tags": "clock",
|
||||
"type":"clock",
|
||||
|
@ -1175,7 +1189,7 @@
|
|||
"name": "Active Pedometer",
|
||||
"shortName":"Active Pedometer",
|
||||
"icon": "app.png",
|
||||
"version":"0.03",
|
||||
"version":"0.04",
|
||||
"description": "Pedometer that filters out arm movement and displays a step goal progress. Steps are saved to a daily file and can be viewed as graph.",
|
||||
"tags": "outdoors,widget",
|
||||
"readme": "README.md",
|
||||
|
@ -1241,7 +1255,7 @@
|
|||
"name": "Battery Chart",
|
||||
"shortName":"Battery Chart",
|
||||
"icon": "app.png",
|
||||
"version":"0.08",
|
||||
"version":"0.09",
|
||||
"readme": "README.md",
|
||||
"description": "A widget and an app for recording and visualizing battery percentage over time.",
|
||||
"tags": "app,widget,battery,time,record,chart,tool",
|
||||
|
@ -1391,6 +1405,7 @@
|
|||
"name": "Metronome",
|
||||
"icon": "metronome_icon.png",
|
||||
"version": "0.03",
|
||||
"readme": "README.md",
|
||||
"description": "Makes the watch blinking and vibrating with a given rate",
|
||||
"tags": "tool",
|
||||
"allow_emulator": true,
|
||||
|
@ -1423,7 +1438,7 @@
|
|||
"name": "Camera shutter",
|
||||
"shortName":"Cam shutter",
|
||||
"icon": "app.png",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "Enable HID, connect to your phone, start your camera and trigger the shot on your Bangle",
|
||||
"tags": "tools",
|
||||
"storage": [
|
||||
|
@ -1473,14 +1488,64 @@
|
|||
"name": "Pong",
|
||||
"shortName": "Pong",
|
||||
"icon": "pong.png",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "A clone of the Atari game Pong",
|
||||
"tags": "game",
|
||||
"type": "app",
|
||||
"allow_emulator": true,
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"pong.app.js","url":"app.js"},
|
||||
{"name":"pong.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "ballmaze",
|
||||
"name": "Ball Maze",
|
||||
"icon": "icon.png",
|
||||
"version": "0.01",
|
||||
"description": "Navigate a ball through a maze by tilting your watch.",
|
||||
"readme": "README.md",
|
||||
"tags": "game",
|
||||
"type": "app",
|
||||
"storage": [
|
||||
{"name": "ballmaze.app.js","url":"app.js"},
|
||||
{"name": "ballmaze.img","url":"icon.js","evaluate": true}
|
||||
],
|
||||
"data": [
|
||||
{"name": "ballmaze.json"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "calendar",
|
||||
"name": "Calendar",
|
||||
"icon": "calendar.png",
|
||||
"version": "0.01",
|
||||
"description": "Simple calendar",
|
||||
"tags": "calendar",
|
||||
"readme": "README.md",
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{
|
||||
"name": "calendar.app.js",
|
||||
"url": "calendar.js"
|
||||
},
|
||||
{
|
||||
"name": "calendar.img",
|
||||
"url": "calendar-icon.js",
|
||||
"evaluate": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{ "id": "hidjoystick",
|
||||
"name": "Bluetooth Joystick",
|
||||
"shortName": "Joystick",
|
||||
"icon": "app.png",
|
||||
"version":"0.01",
|
||||
"description": "Emulates a 2 axis/5 button Joystick using the accelerometer as stick input and buttons 1-3, touch left as button 4 and touch right as button 5.",
|
||||
"tags": "bluetooth",
|
||||
"storage": [
|
||||
{"name":"hidjoystick.app.js","url":"app.js"},
|
||||
{"name":"hidjoystick.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
0.02: Update version checker for new filename type
|
||||
0.03: Actual pixels as of 5 Mar 2020
|
||||
0.04: Actual pixels as of 9 Mar 2020
|
||||
0.05: Actual pixels as of 27 Apr 2020
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -6,3 +6,4 @@
|
|||
0.09: center date, remove box around it, internal refactor to remove redundant code.
|
||||
0.10: remove debug, refactor seconds to show elapsed secs each time app is displayed
|
||||
0.11: shift face down for widget area, maximize face size, 0 pad single digit date, use locale for date
|
||||
0.12: Fix regression after 0.11
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
// eliminate ide undefined errors
|
||||
let g;
|
||||
let Bangle;
|
||||
|
||||
// http://forum.espruino.com/conversations/345155/#comment15172813
|
||||
const locale = require('locale');
|
||||
const p = Math.PI / 2;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
0.01: New Widget!
|
||||
0.02: Distance calculation and display
|
||||
0.03: Data logging and display
|
||||
0.04: Steps are set to 0 in log on new day
|
|
@ -18,7 +18,7 @@ Steps are saved to a datafile every 5 minutes. You can watch a graph using the a
|
|||
* 10600 steps
|
||||

|
||||
|
||||
## Features
|
||||
## Features Widget
|
||||
|
||||
* Two line display
|
||||
* Can display distance (in km) or steps in each line
|
||||
|
@ -32,22 +32,23 @@ Steps are saved to a datafile every 5 minutes. You can watch a graph using the a
|
|||
* Steps are saved to a file and read-in at start (to not lose step progress)
|
||||
* Settings can be changed in Settings - App/widget settings - Active Pedometer
|
||||
|
||||
## Features App
|
||||
|
||||
* The app accesses the data stored for the current day
|
||||
* Timespan is choseable (1h, 4h, 8h, 12h, 16h, 20, 24h), standard is 24h, the whole current day
|
||||
|
||||
## Data storage
|
||||
|
||||
* Data is stored to a file
|
||||
* Data is stored to a file named activepedomYYYYMMDD.data (activepedom20200427.data)
|
||||
* One file is created for each day
|
||||
* Format: now,stepsCounted,active,stepsTooShort,stepsTooLong,stepsOutsideTime
|
||||
* now is UNIX timestamp in ms
|
||||
* You can chose the app to watch a steps graph
|
||||
* 'now' is UNIX timestamp in ms
|
||||
* You can use the app to watch a steps graph
|
||||
* You can import the file into Excel
|
||||
* The file does not include a header
|
||||
* You can convert UNIX timestamp to a date in Excel using this formula: =DATUM(1970;1;1)+(LINKS(A2;10)/86400)
|
||||
* You have to format the cell with the formula to a date cell. Example: JJJJ-MM-TT-hh-mm-ss
|
||||
|
||||
## App
|
||||
|
||||
* The app accesses the data stored for the current day
|
||||
* Timespan is choseable (1h, 4h, 8h, 12h, 16h, 20, 24h), standard is 24h, the whole current day
|
||||
|
||||
## Settings
|
||||
|
||||
* Max time (ms): Maximum time between two steps in milliseconds, steps will not be counted if exceeded. Standard: 1100
|
||||
|
|
|
@ -33,27 +33,28 @@
|
|||
|
||||
function storeData() {
|
||||
now = new Date();
|
||||
month = now.getMonth() + 1;
|
||||
if (month < 10) month = "0" + month;
|
||||
filename = filename = "activepedom" + now.getFullYear() + month + now.getDate() + ".data";
|
||||
month = now.getMonth() + 1; //month is 0-based
|
||||
if (month < 10) month = "0" + month; //leading 0
|
||||
filename = filename = "activepedom" + now.getFullYear() + month + now.getDate() + ".data"; //new file for each day
|
||||
dataFile = s.open(filename,"a");
|
||||
if (dataFile) {
|
||||
if (dataFile) { //check if filen already exists
|
||||
if (dataFile.getLength() == 0) {
|
||||
stepsToWrite = 0;
|
||||
}
|
||||
else {
|
||||
stepsToWrite = stepsCounted;
|
||||
//new day, set steps to 0
|
||||
stepsCounted = 0;
|
||||
stepsTooShort = 0;
|
||||
stepsTooLong = 0;
|
||||
stepsOutsideTime = 0;
|
||||
}
|
||||
dataFile.write([
|
||||
now.getTime(),
|
||||
stepsToWrite,
|
||||
stepsCounted,
|
||||
active,
|
||||
stepsTooShort,
|
||||
stepsTooLong,
|
||||
stepsOutsideTime,
|
||||
].join(",")+"\n");
|
||||
}
|
||||
dataFile = undefined;
|
||||
dataFile = undefined; //save memory
|
||||
}
|
||||
|
||||
//return setting
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
# Ball Maze
|
||||
|
||||
Navigate a ball through a maze by tilting your watch.
|
||||
|
||||

|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
Select a maze size to begin the game.
|
||||
Tilt your watch to steer the ball towards the target and advance to the next level.
|
||||
|
||||
## Creator
|
||||
|
||||
Richard de Boer <rigrig+banglejs@tubul.net>
|
|
@ -0,0 +1,552 @@
|
|||
(() => {
|
||||
let intervalID;
|
||||
let settings = require("Storage").readJSON("ballmaze.json",true) || {};
|
||||
|
||||
// density, elasticity of bounces, "drag coefficient"
|
||||
const rho = 100, e = 0.3, C = 0.01;
|
||||
// screen width & height in pixels
|
||||
const sW = 240, sH = 160;
|
||||
// gravity constant (lowercase was already taken)
|
||||
const G = 9.80665;
|
||||
|
||||
// wall bit flags
|
||||
const TOP = 1<<0, LEFT = 1<<1, BOTTOM = 1<<2, RIGHT = 1<<3,
|
||||
LINKED = 1<<4; // used in maze generation
|
||||
|
||||
// The play area is 240x160, sizes are the ball radius, so we can use common
|
||||
// denominators of 120x80 to get square rooms
|
||||
// Reverse the order to show the easiest on top of the menu
|
||||
const sizes = [1, 2, 4, 5, 8, 10, 16, 20, 40].reverse(),
|
||||
// even size 1 actually works, but larger mazes take forever to generate
|
||||
minSize = 4, defaultSize = 10;
|
||||
const sizeNames = {
|
||||
1: "Insane", 2: "Gigantic", 4: "Enormous", 5: "Huge", 8: "Large",
|
||||
10: "Medium", 16: "Small", 20: "Tiny", 40: "Trivial",
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw something to all screen buffers
|
||||
* @param draw {function} Callback which performs the drawing
|
||||
*/
|
||||
function drawAll(draw) {
|
||||
draw();
|
||||
g.flip();
|
||||
draw();
|
||||
g.flip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all buffers
|
||||
*/
|
||||
function clearAll() {
|
||||
drawAll(() => g.clear());
|
||||
}
|
||||
|
||||
// use unbuffered graphics for UI stuff
|
||||
function showMessage(message, title) {
|
||||
Bangle.setLCDMode();
|
||||
return E.showMessage(message, title);
|
||||
}
|
||||
|
||||
function showPrompt(prompt, options) {
|
||||
Bangle.setLCDMode();
|
||||
return E.showPrompt(prompt, options);
|
||||
}
|
||||
|
||||
function showMenu(menu) {
|
||||
Bangle.setLCDMode();
|
||||
return E.showMenu(menu);
|
||||
}
|
||||
|
||||
const sign = (n) => n<0?-1:1; // we don't really care about zero
|
||||
|
||||
/**
|
||||
* Play the game, using a ball with radius size
|
||||
* @param size {number}
|
||||
*/
|
||||
function playMaze(size) {
|
||||
const r = size;
|
||||
// ball mass, weight, "drag"
|
||||
// Yes, larger maze = larger ball = heavier ball
|
||||
// (atm our physics is so oversimplified that mass cancels out though)
|
||||
const m = rho*(r*r*r), w = G*m, d = C*w;
|
||||
|
||||
// number of columns/rows
|
||||
const cols = Math.round(sW/(r*2.5)),
|
||||
rows = Math.round(sH/(r*2.5));
|
||||
// width & height of one column/row in pixels
|
||||
const cW = sW/cols, rH = sH/rows;
|
||||
|
||||
// list of rooms, every room can have one or more wall bits set
|
||||
// actual layout: 0 1 2
|
||||
// 3 4 5
|
||||
// this means that for room with index "i": (except edge cases!)
|
||||
// i-1 = room to the left
|
||||
// i+1 = room to the right
|
||||
// i-cols = room above
|
||||
// i+cols = room below
|
||||
let rooms = new Uint8Array(rows*cols);
|
||||
// shortest route from start to finish
|
||||
let route;
|
||||
|
||||
let x, y, // current position
|
||||
px, py, ppx, ppy, // previous positions (for erasing old image)
|
||||
vx, vy; // velocity
|
||||
|
||||
function start() {
|
||||
// start in top left corner
|
||||
x = cW/2;
|
||||
y = rH/2;
|
||||
vx = vy = 0;
|
||||
ppx = px = x;
|
||||
ppy = py = y;
|
||||
|
||||
generateMaze(); // this shows unbuffered progress messages
|
||||
if (settings.cheat && r>1) findRoute(); // not enough memory for r==1 :-(
|
||||
|
||||
Bangle.setLCDMode("doublebuffered");
|
||||
clearAll();
|
||||
drawAll(drawMaze);
|
||||
intervalID = setInterval(tick, 100);
|
||||
}
|
||||
|
||||
// Position conversions
|
||||
// index: index of room in rooms[]
|
||||
// rowcol: position measured in roomsizes
|
||||
// xy: position measured in pixels
|
||||
/**
|
||||
* Index from RowCol
|
||||
* @param row {number}
|
||||
* @param col {number}
|
||||
* @returns {number} rooms[] index
|
||||
*/
|
||||
function iFromRC(row, col) {
|
||||
return row*cols+col;
|
||||
}
|
||||
|
||||
/**
|
||||
* RowCol from index
|
||||
* @param index {number}
|
||||
* @returns {(number)[]} [row,column]
|
||||
*/
|
||||
function rcFromI(index) {
|
||||
return [
|
||||
Math.floor(index/cols),
|
||||
index%cols,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* RowCol from Xy
|
||||
* @param x {number}
|
||||
* @param y {number}
|
||||
* @returns {(number)[]} [row,column]
|
||||
*/
|
||||
function rcFromXy(x, y) {
|
||||
return [
|
||||
Math.floor(y/sH*rows),
|
||||
Math.floor(x/sW*cols),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Link another room up
|
||||
* @param index {number} Dig from already linked room with this index
|
||||
* @param dir {number} in this direction
|
||||
* @return {number} index of room we just linked up
|
||||
*/
|
||||
function dig(index, dir) {
|
||||
rooms[index] &= ~dir;
|
||||
let neighbour;
|
||||
switch(dir) {
|
||||
case LEFT:
|
||||
neighbour = index-1;
|
||||
rooms[neighbour] &= ~RIGHT;
|
||||
break;
|
||||
case RIGHT:
|
||||
neighbour = index+1;
|
||||
rooms[neighbour] &= ~LEFT;
|
||||
break;
|
||||
case TOP:
|
||||
neighbour = index-cols;
|
||||
rooms[neighbour] &= ~BOTTOM;
|
||||
break;
|
||||
case BOTTOM:
|
||||
neighbour = index+cols;
|
||||
rooms[neighbour] &= ~TOP;
|
||||
break;
|
||||
}
|
||||
rooms[neighbour] |= LINKED;
|
||||
return neighbour;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the maze
|
||||
*/
|
||||
function generateMaze() {
|
||||
// Maze generation basically works like this:
|
||||
// 1. Start with all rooms set to completely walled off and "unlinked"
|
||||
// 2. Then mark a room as "linked", and add it to the "to do" list
|
||||
// 3. When the "to do" list is empty, we're done
|
||||
// 4. pick a random room from the list
|
||||
// 5. if all adjacent rooms are linked -> remove room from list, goto 3
|
||||
// 6. pick a random unlinked adjacent room
|
||||
// 7. remove the walls between the rooms
|
||||
// 8. mark the adjacent room as linked and add it to the "to do" list
|
||||
// 9. go to 4
|
||||
let pdotnum = 0;
|
||||
const title = "Please wait",
|
||||
message = "Generating maze\n",
|
||||
showProgress = (done, total) => {
|
||||
const dotnum = Math.floor(done/total*10);
|
||||
if (dotnum>pdotnum) {
|
||||
const dots = ".".repeat(dotnum)+" ".repeat(10-dotnum);
|
||||
showMessage(message+dots, title);
|
||||
pdotnum = dotnum;
|
||||
}
|
||||
};
|
||||
showProgress(0, 100);
|
||||
// start with all rooms completely walled off
|
||||
rooms.fill(TOP|LEFT|BOTTOM|RIGHT);
|
||||
const
|
||||
// is room at row,col already linked?
|
||||
linked = (row, col) => !!(rooms[iFromRC(row, col)]&LINKED),
|
||||
// pick random array element
|
||||
pickRandom = (arr) => arr[Math.floor(Math.random()*arr.length)];
|
||||
// starting with top-right room seems to generate more interesting mazes
|
||||
rooms[cols] |= LINKED;
|
||||
let todo = [cols], done = 1;
|
||||
while(todo.length) {
|
||||
const index = pickRandom(todo);
|
||||
const rc = rcFromI(index),
|
||||
row = rc[0], col = rc[1];
|
||||
let sides = [];
|
||||
if ((col>0) && !linked(row, col-1)) sides.push(LEFT);
|
||||
if ((col<cols-1) && !linked(row, col+1)) sides.push(RIGHT);
|
||||
if ((row>0) && !linked(row-1, col)) sides.push(TOP);
|
||||
if ((row<rows-1) && !linked(row+1, col)) sides.push(BOTTOM);
|
||||
if (sides.length<=1) {
|
||||
// no need to visit this room again
|
||||
todo.splice(todo.indexOf(index), 1);
|
||||
}
|
||||
if (!sides.length) {
|
||||
// no neighbours need linking
|
||||
continue;
|
||||
}
|
||||
todo.push(dig(index, pickRandom(sides)));
|
||||
showProgress(done++, rooms.length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We wouldn't want to generate a maze we can't solve ourselves...
|
||||
*/
|
||||
function findRoute() {
|
||||
let dist = new Uint16Array(rooms.length), todo = [0];
|
||||
dist.fill(-1);
|
||||
dist[0] = 0;
|
||||
while(true) {
|
||||
const i = todo.shift(), d = dist[i], walls = rooms[i],
|
||||
rc = rcFromI(i),
|
||||
row = rc[0], col = rc[1];
|
||||
if (i===rooms.length-1) { break; }
|
||||
if (col>0 && !(walls&LEFT) && dist[i-1]>d+1) {
|
||||
dist[i-1] = d+1;
|
||||
todo.push(i-1);
|
||||
}
|
||||
if (row>0 && !(walls&TOP) && dist[i-cols]>d+1) {
|
||||
dist[i-cols] = d+1;
|
||||
todo.push(i-cols);
|
||||
}
|
||||
if (col<cols-1 && !(walls&RIGHT) && dist[i+1]>d+1) {
|
||||
dist[i+1] = d+1;
|
||||
todo.push(i+1);
|
||||
}
|
||||
if (row<rows-1 && !(walls&BOTTOM) && dist[i+cols]>d+1) {
|
||||
dist[i+cols] = d+1;
|
||||
todo.push(i+cols);
|
||||
}
|
||||
}
|
||||
|
||||
route = [rooms.length-1];
|
||||
while(true) {
|
||||
const i = route[0], d = dist[i], walls = rooms[i],
|
||||
rc = rcFromI(i),
|
||||
row = rc[0], col = rc[1];
|
||||
if (i===0) { break; }
|
||||
if (col<cols-1 && !(walls&RIGHT) && dist[i+1]<d) {
|
||||
route.unshift(i+1);
|
||||
continue;
|
||||
}
|
||||
if (row<rows-1 && !(walls&BOTTOM) && dist[i+cols]<d) {
|
||||
route.unshift(i+cols);
|
||||
continue;
|
||||
}
|
||||
if (row>0 && !(walls&TOP) && dist[i-cols]<d) {
|
||||
route.unshift(i-cols);
|
||||
continue;
|
||||
}
|
||||
if (col>0 && !(walls&LEFT) && dist[i-1]<d) {
|
||||
route.unshift(i-1);
|
||||
continue;
|
||||
}
|
||||
// this should never happen!
|
||||
console.log("No route found!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the maze:
|
||||
* - room borders
|
||||
* - maze border
|
||||
* - exit
|
||||
*/
|
||||
function drawMaze() {
|
||||
const range = {top: 0, left: 0, bottom: rows, right: cols};
|
||||
const w = sW/cols, h = sH/rows;
|
||||
g.clear();
|
||||
g.setColor(0.76, 0.60, 0.42);
|
||||
for(let row = range.top; row<=range.bottom; row++) {
|
||||
for(let col = range.left; col<=range.right; col++) {
|
||||
const walls = rooms[row*cols+col], x = col*w, y = row*h;
|
||||
if (walls&BOTTOM) g.drawLine(x, y+h, x+w, y+h);
|
||||
if (walls&RIGHT) g.drawLine(x+w, y, x+w, y+h);
|
||||
}
|
||||
}
|
||||
// outline
|
||||
g.setColor(0.29, 0.23, 0.17).drawRect(0, 0, sW-1, sH-1);
|
||||
// target
|
||||
g.setColor(0, 0.5, 0).fillCircle(sW-cW/2, sH-rH/2, r-1);
|
||||
if (route) drawRoute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redraw a part of the maze (after we erased the ball image)
|
||||
* @param range Draw rooms in this range {top,left,bottom,right}
|
||||
*/
|
||||
function redrawMaze(range) {
|
||||
const w = sW/cols, h = sH/rows;
|
||||
g.setColor(0.76, 0.60, 0.42);
|
||||
for(let row = range.top; row<=range.bottom; row++) {
|
||||
for(let col = range.left; col<=range.right; col++) {
|
||||
const walls = rooms[row*cols+col], x = col*w, y = row*h;
|
||||
if (row===range.top && walls&TOP) g.drawLine(x, y, x+w, y);
|
||||
if (col===range.left && walls&LEFT) g.drawLine(x, y, x, y+h);
|
||||
if (walls&BOTTOM) g.drawLine(x, y+h, x+w, y+h);
|
||||
if (walls&RIGHT) g.drawLine(x+w, y, x+w, y+h);
|
||||
}
|
||||
}
|
||||
g.setColor(0.29, 0.23, 0.17).drawRect(0, 0, sW-1, sH-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the ball, with glare offset depending on ball position
|
||||
*/
|
||||
function drawBall() {
|
||||
g.setColor(0.7, 0.7, 0.8).fillCircle(x, y, r-1);
|
||||
const gx = -x/sW, gy = -y/sH;
|
||||
g.setColor(0.8, 0.8, 0.9).fillCircle(x+gx*r/5, y+gy*r/5, r/2)
|
||||
.setColor(0.85, 0.85, 0.95).fillCircle(x+gx*r/4, y+gy*r/4.5, r/2.5)
|
||||
.setColor(0.9, 0.9, 1).fillCircle(x+gx*r/3, y+gy*r/3, r/3.5)
|
||||
.setColor(1, 1, 1).fillCircle(x+gx*r/3, y+gy*r/3, r/6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the screen:
|
||||
* - erase previous ball image
|
||||
* - redraw maze around the erased area
|
||||
* - draw the ball
|
||||
*/
|
||||
function drawUpdate() {
|
||||
g.clearRect(ppx-r, ppy-r, ppx+r, ppy+r);
|
||||
const rc = rcFromXy(ppx, ppy),
|
||||
row = rc[0], col = rc[1];
|
||||
redrawMaze({top: row-1, left: col-1, bottom: row+1, right: col+1});
|
||||
drawBall();
|
||||
g.flip();
|
||||
}
|
||||
|
||||
function drawRoute() {
|
||||
let i = route[0], rc = rcFromI(i),
|
||||
row = rc[0], col = rc[1],
|
||||
x = (col+0.5)*cW, y = (row+0.5)*rH;
|
||||
g.setColor(1, 0, 0).moveTo(x, y);
|
||||
route.forEach(i => {
|
||||
const rc = rcFromI(i),
|
||||
row = rc[0], col = rc[1],
|
||||
x = (col+0.5)*cW, y = (row+0.5)*rH;
|
||||
g.lineTo(x, y);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the ball
|
||||
*/
|
||||
function move() {
|
||||
const a = Bangle.getAccel();
|
||||
const fx = (-a.x*w)-(sign(vx)*d*a.z), fy = (-a.y*w)-(sign(vy)*d*a.z);
|
||||
vx += fx/m;
|
||||
vy += fy/m;
|
||||
const s = Math.ceil(Math.max(Math.abs(vx), Math.abs(vy)));
|
||||
for(let n = s; n>0; n--) {
|
||||
x += vx/s;
|
||||
y += vy/s;
|
||||
bounce();
|
||||
}
|
||||
if (x>sW-cW && y>sH-rH) win();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether we hit any walls, and if so: Bounce.
|
||||
*
|
||||
* Bounce = reverse velocity in bounce direction, multiply with elasticity
|
||||
* Also apply drag in perpendicular direction ("friction with the wall")
|
||||
*/
|
||||
function bounce() {
|
||||
const row = Math.floor(y/sH*rows), col = Math.floor(x/sW*cols),
|
||||
i = row*cols+col, walls = rooms[i];
|
||||
const left = col*cW,
|
||||
right = (col+1)*cW,
|
||||
top = row*rH,
|
||||
bottom = (row+1)*rH;
|
||||
let bounced = false;
|
||||
if (vx<0) {
|
||||
if ((walls&LEFT) && x<=left+r) {
|
||||
x += (1+e)*(left+r-x);
|
||||
const fy = sign(vy)*d*Math.abs(vx);
|
||||
vy -= fy/m;
|
||||
vx = -vx*e;
|
||||
bounced = true;
|
||||
}
|
||||
} else {
|
||||
if ((walls&RIGHT) && x>=right-r) {
|
||||
x -= (1+e)*(x+r-right);
|
||||
const fy = sign(vy)*d*Math.abs(vx);
|
||||
vy -= fy/m;
|
||||
vx = -vx*e;
|
||||
bounced = true;
|
||||
}
|
||||
}
|
||||
if (vy<0) {
|
||||
if ((walls&TOP) && y<=top+r) {
|
||||
y += (1+e)*(top+r-y);
|
||||
const fx = sign(vx)*d*Math.abs(vy);
|
||||
vx -= fx/m;
|
||||
vy = -vy*e;
|
||||
bounced = true;
|
||||
}
|
||||
} else {
|
||||
if ((walls&BOTTOM) && y>=bottom-r) {
|
||||
y -= (1+e)*(y+r-bottom);
|
||||
const fx = sign(vx)*d*Math.abs(vy);
|
||||
vx -= fx/m;
|
||||
vy = -vy*e;
|
||||
bounced = true;
|
||||
}
|
||||
}
|
||||
if (bounced) return;
|
||||
let cx, cy;
|
||||
if ((rooms[i-1]&TOP) || rooms[i-cols]&LEFT) {
|
||||
if ((x-left)*(x-left)+(y-top)*(y-top)<=r*r) {
|
||||
cx = left;
|
||||
cy = top;
|
||||
}
|
||||
}
|
||||
else if ((rooms[i-1]&BOTTOM) || rooms[i+cols]&LEFT) {
|
||||
if ((x-left)*(x-left)+(bottom-y)*(bottom-y)<=r*r) {
|
||||
cx = left;
|
||||
cy = bottom;
|
||||
}
|
||||
}
|
||||
else if ((rooms[i+1]&TOP) || rooms[i-cols]&RIGHT) {
|
||||
if ((right-x)*(right-x)+(y-top)*(y-top)<=r*r) {
|
||||
cx = right;
|
||||
cy = top;
|
||||
}
|
||||
}
|
||||
else if ((rooms[i+1]&BOTTOM) || rooms[i+cols]&RIGHT) {
|
||||
if ((right-x)*(right-x)+(bottom-y)*(bottom-y)<=r*r) {
|
||||
cx = right;
|
||||
cy = bottom;
|
||||
}
|
||||
}
|
||||
if (!cx) return;
|
||||
let nx = x-cx, ny = y-cy;
|
||||
const l = Math.sqrt(nx*nx+ny*ny);
|
||||
nx /= l;
|
||||
ny /= l;
|
||||
const p = vx*nx+vy*ny;
|
||||
vx -= 2*p*nx*e;
|
||||
vy -= 2*p*ny*e;
|
||||
}
|
||||
|
||||
/**
|
||||
* You reached the bottom-right corner, you win!
|
||||
*/
|
||||
function win() {
|
||||
clearInterval(intervalID);
|
||||
Bangle.buzz().then(askAgain);
|
||||
}
|
||||
|
||||
/**
|
||||
* You solved the maze, try the next one?
|
||||
*/
|
||||
function askAgain() {
|
||||
const nextLevel = (size>minSize)?"next level":"again";
|
||||
const nextSize = (size>minSize)?sizes[sizes.indexOf(size)+1]:size;
|
||||
showPrompt(`Well done!\n\nPlay ${nextLevel}?`,
|
||||
{"title": "Congratulations!"})
|
||||
.then(function(again) {
|
||||
if (again) {
|
||||
playMaze(nextSize);
|
||||
} else {
|
||||
startGame();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function tick() {
|
||||
ppx = px;
|
||||
ppy = py;
|
||||
px = x;
|
||||
py = y;
|
||||
move();
|
||||
drawUpdate();
|
||||
}
|
||||
|
||||
start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask player what size maze they would like to play
|
||||
*/
|
||||
function startGame() {
|
||||
let menu = {
|
||||
"": {
|
||||
title: "Select Maze Size",
|
||||
selected: sizes.indexOf(settings.size || defaultSize),
|
||||
},
|
||||
};
|
||||
sizes.filter(s => s>=minSize).forEach(size => {
|
||||
let name = sizeNames[size];
|
||||
if (size<minSize) name = "! "+size;
|
||||
let cols = Math.round(sW/(size*2.5)),
|
||||
rows = Math.round(sH/(size*2.5));
|
||||
if (rows<10) rows = " "+rows;
|
||||
if (cols<10) cols = " "+cols;
|
||||
name += " ".repeat(14-name.length);
|
||||
name += `${cols}x${rows}`;
|
||||
menu[name] = () => {
|
||||
// remember chosen size
|
||||
settings.size = size;
|
||||
require("Storage").write("ballmaze.json", settings);
|
||||
playMaze(size);
|
||||
};
|
||||
});
|
||||
menu["< Exit"] = () => load();
|
||||
showMenu(menu);
|
||||
}
|
||||
|
||||
startGame();
|
||||
})();
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AH4AU9wAOCw0OC5/gFyowHC+Hs5gACC7HhiMRjwXSCoIADC5wCB4MSkIXDGIoXKiUikQwJC5PhCwIXFGAgXJFwRHEGAnOC5HhC5IwC5gXJIw4XF4AXKFwwXEGAoXCiKlFMAzNCgDpDC4QAKcgZJBC6wADF6kAhgXP5xfEC58SC4iNCC4nhC5McC4S/DC6a9DC4IACC5MhC4XOC5HuLxPMC4PuC5IwHkUeC44ABA4IACFw5cBC5owEkUhjwXPGAyMCC5wxDLgIACC54ADC94AGC7sOCx/gC4owQCwwA/AH4AMA"))
|
Binary file not shown.
After Width: | Height: | Size: 444 B |
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
|
@ -2,3 +2,4 @@
|
|||
0.02: Apply locale, 12-hour setting
|
||||
0.03: Fix dates drawing over each other at midnight
|
||||
0.04: Small bugfix
|
||||
0.05: Clock does not start if app Languages is not installed
|
|
@ -12,7 +12,12 @@
|
|||
date.setMonth(1, 3) // februari: months are zero-indexed
|
||||
const localized = locale.date(date, true)
|
||||
locale.dayFirst = /3.*2/.test(localized)
|
||||
locale.hasMeridian = (locale.meridian(date) !== '')
|
||||
|
||||
locale.hasMeridian = false
|
||||
if(typeof locale.meridian === 'function') { // function does not exists if languages app is not installed
|
||||
locale.hasMeridian = (locale.meridian(date) !== '')
|
||||
}
|
||||
|
||||
}
|
||||
const screen = {
|
||||
width: g.getWidth(),
|
||||
|
|
|
@ -6,3 +6,4 @@
|
|||
0.06: Fixes widget events and charting of component states
|
||||
0.07: Improve logging and charting of component states and add widget icon
|
||||
0.08: Fix for Home button in the app and README added.
|
||||
0.09: Fix failing dismissal of Gadgetbridge notifications, record (coarse) bluetooth state
|
|
@ -8,7 +8,7 @@ const GraphXMax = GraphXZero + MaxValueCount;
|
|||
|
||||
const GraphLcdY = GraphYZero + 10;
|
||||
const GraphCompassY = GraphYZero + 16;
|
||||
// const GraphBluetoothY = GraphYZero + 22;
|
||||
const GraphBluetoothY = GraphYZero + 22;
|
||||
const GraphGpsY = GraphYZero + 28;
|
||||
const GraphHrmY = GraphYZero + 34;
|
||||
|
||||
|
@ -175,13 +175,13 @@ function renderData(dataArray) {
|
|||
g.drawLine(GraphXZero + i, GraphCompassY, GraphXZero + i, GraphCompassY + 1);
|
||||
}
|
||||
|
||||
// // Bluetooth state
|
||||
// if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) {
|
||||
// g.setColor(0, 0, 1);
|
||||
// g.setFontAlign(1, -1, 0);
|
||||
// g.drawString("BLE", GraphXZero - GraphMarkerOffset, GraphBluetoothY - 2, true);
|
||||
// g.drawLine(GraphXZero + i, GraphBluetoothY, GraphXZero + i, GraphBluetoothY + 1);
|
||||
// }
|
||||
// Bluetooth state
|
||||
if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.bluetooth) {
|
||||
g.setColor(0, 0, 1);
|
||||
g.setFontAlign(1, -1, 0);
|
||||
g.drawString("BLE", GraphXZero - GraphMarkerOffset, GraphBluetoothY - 2, true);
|
||||
g.drawLine(GraphXZero + i, GraphBluetoothY, GraphXZero + i, GraphBluetoothY + 1);
|
||||
}
|
||||
|
||||
// Gps state
|
||||
if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.gps) {
|
||||
|
|
|
@ -71,8 +71,10 @@
|
|||
enabledConsumers = enabledConsumers | switchableConsumers.gps;
|
||||
if (hrmEventReceived)
|
||||
enabledConsumers = enabledConsumers | switchableConsumers.hrm;
|
||||
//if (Bangle.isBluetoothOn())
|
||||
// enabledConsumers = enabledConsumers | switchableConsumers.bluetooth;
|
||||
|
||||
// Very coarse first approach to check if the BLE device is on.
|
||||
if (NRF.getSecurityStatus().connected)
|
||||
enabledConsumers = enabledConsumers | switchableConsumers.bluetooth;
|
||||
|
||||
// Reset the event registration vars
|
||||
compassEventReceived = false;
|
||||
|
@ -110,19 +112,14 @@
|
|||
}
|
||||
|
||||
function reload() {
|
||||
WIDGETS.batchart.width = 24;
|
||||
WIDGETS["batchart"].width = 24;
|
||||
|
||||
recordingInterval = setInterval(logBatteryData, recordingInterval10Min);
|
||||
|
||||
logBatteryData();
|
||||
}
|
||||
|
||||
// add the widget
|
||||
WIDGETS.batchart = {
|
||||
area: "tl", width: 24, draw: draw, reload: function () {
|
||||
reload();
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
WIDGETS["batchart"] = {
|
||||
area: "tl", width: 24, draw: draw, reload: reload
|
||||
};
|
||||
|
||||
reload();
|
||||
|
|
|
@ -13,3 +13,4 @@
|
|||
0.13: Now automatically load *.boot.js at startup
|
||||
Move alarm code into alarm.boot.js
|
||||
0.14: Move welcome loaders to *.boot.js
|
||||
0.15: Added BLE HID option for Joystick and bare Keyboard
|
||||
|
|
|
@ -4,7 +4,9 @@ E.setFlags({pretokenise:1});
|
|||
var s = require('Storage').readJSON('setting.json',1)||{};
|
||||
if (s.ble!==false) {
|
||||
if (s.HID) { // Human interface device
|
||||
Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA=="));
|
||||
if (s.HID=="joy") Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));
|
||||
else if (s.HID=="kb") Bangle.HID = E.toUint8Array(atob("BQEJBqEBBQcZ4CnnFQAlAXUBlQiBApUBdQiBAZUFdQEFCBkBKQWRApUBdQORAZUGdQgVACVzBQcZAClzgQAJBRUAJv8AdQiVArECwA=="));
|
||||
else /*kbmedia*/Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA=="));
|
||||
NRF.setServices({}, {uart:true, hid:Bangle.HID});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
|
||||
## Joystick:
|
||||
|
||||
https://github.com/espruino/BangleApps/issues/349#issuecomment-620231524
|
||||
|
||||
```
|
||||
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||
0x09, 0x04, // Usage (Joystick)
|
||||
0xA1, 0x01, // Collection (Application)
|
||||
0x09, 0x01, // Usage (Pointer)
|
||||
0xA1, 0x00, // Collection (Physical)
|
||||
// Buttons
|
||||
0x05, 0x09, // Usage Page (Buttons)
|
||||
0x19, 0x01, // Usage Minimum (1)
|
||||
0x29, 0x05, // Usage Maximum (5)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x01, // Logical Maximum (1)
|
||||
0x95, 0x05, // Report Count (5)
|
||||
0x75, 0x01, // Report Size (1)
|
||||
0x81, 0x02, // Input (Data, Variable, Absolute)
|
||||
|
||||
// padding bits
|
||||
0x95, 0x03, // Report Count (3)
|
||||
0x75, 0x01, // Report Size (1)
|
||||
0x81, 0x03, // Input (Constant)
|
||||
|
||||
// Stick
|
||||
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||
0x09, 0x30, // Usage (X)
|
||||
0x09, 0x31, // Usage (Y)
|
||||
0x15, 0x81, // Logical Minimum (-127)
|
||||
0x25, 0x7f, // Logical Maximum (127)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x95, 0x02, // Report Count (2)
|
||||
0x81, 0x02, // Input (Data, Variable, Absolute)
|
||||
0xC0, // End Collection (Physical)
|
||||
0xC0 // End Collection (Application)
|
||||
```
|
||||
|
||||
## Keyboard
|
||||
|
||||
http://www.espruino.com/BLE+Keyboard
|
||||
|
||||
```
|
||||
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||
0x09, 0x06, // Usage (Keyboard)
|
||||
0xA1, 0x01, // Collection (Application)
|
||||
0x05, 0x07, // Usage Page (Key Codes)
|
||||
0x19, 0xe0, // Usage Minimum (224)
|
||||
0x29, 0xe7, // Usage Maximum (231)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x01, // Logical Maximum (1)
|
||||
0x75, 0x01, // Report Size (1)
|
||||
0x95, 0x08, // Report Count (8)
|
||||
0x81, 0x02, // Input (Data, Variable, Absolute)
|
||||
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x81, 0x01, // Input (Constant) reserved byte(1)
|
||||
|
||||
0x95, 0x05, // Report Count (5)
|
||||
0x75, 0x01, // Report Size (1)
|
||||
0x05, 0x08, // Usage Page (Page# for LEDs)
|
||||
0x19, 0x01, // Usage Minimum (1)
|
||||
0x29, 0x05, // Usage Maximum (5)
|
||||
0x91, 0x02, // Output (Data, Variable, Absolute), Led report
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x75, 0x03, // Report Size (3)
|
||||
0x91, 0x01, // Output (Data, Variable, Absolute), Led report padding
|
||||
|
||||
0x95, 0x06, // Report Count (6)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x73, // Logical Maximum (115 - include F13, etc)
|
||||
0x05, 0x07, // Usage Page (Key codes)
|
||||
0x19, 0x00, // Usage Minimum (0)
|
||||
0x29, 0x73, // Usage Maximum (115 - include F13, etc)
|
||||
0x81, 0x00, // Input (Data, Array) Key array(6 bytes)
|
||||
|
||||
0x09, 0x05, // Usage (Vendor Defined)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x26, 0xFF, 0x00, // Logical Maximum (255)
|
||||
0x75, 0x08, // Report Count (2)
|
||||
0x95, 0x02, // Report Size (8 bit)
|
||||
0xB1, 0x02, // Feature (Data, Variable, Absolute)
|
||||
|
||||
0xC0 // End Collection (Application)
|
||||
```
|
|
@ -0,0 +1 @@
|
|||
0.01: Basic calendar
|
|
@ -0,0 +1,8 @@
|
|||
# Calendar
|
||||
|
||||
Basic calendar
|
||||
|
||||
## Usage
|
||||
|
||||
- Use `BTN4` (left screen tap) to go to the previous month
|
||||
- Use `BTN5` (right screen tap) to go to the next month
|
|
@ -0,0 +1,5 @@
|
|||
require("heatshrink").decompress(
|
||||
atob(
|
||||
"mEwxH+AH4A/ADuIUCARRDhgePCKIv13YAEDoYJFAA4RJFyQvcGBYRGy4dDy4uLCJgv/DoOBDgOBF5oRLF6IeBDgIvNCJYvQDwQuNCJovRADov/F9OsAEgv/F/4vhwIACAqYv/F/4vnd94vvX/4v/F/7vvF96//F/4v/d94v/F/4wsFxQwjFxgA/AH4A/AH4AZA=="
|
||||
)
|
||||
)
|
|
@ -0,0 +1,160 @@
|
|||
const maxX = 240;
|
||||
const maxY = 240;
|
||||
const rowN = 7;
|
||||
const colN = 7;
|
||||
const headerH = maxY / 7;
|
||||
const rowH = (maxY - headerH) / rowN;
|
||||
const colW = maxX / colN;
|
||||
const color1 = "#035AA6";
|
||||
const color2 = "#4192D9";
|
||||
const color3 = "#026873";
|
||||
const color4 = "#038C8C";
|
||||
const color5 = "#03A696";
|
||||
const black = "#000000";
|
||||
const white = "#ffffff";
|
||||
const gray1 = "#444444";
|
||||
const gray2 = "#888888";
|
||||
const gray3 = "#bbbbbb";
|
||||
const red = "#d41706";
|
||||
|
||||
function drawCalendar(date) {
|
||||
g.setBgColor(color4);
|
||||
g.clearRect(0, 0, maxX, maxY);
|
||||
g.setBgColor(color1);
|
||||
g.clearRect(0, 0, maxX, headerH);
|
||||
g.setBgColor(color2);
|
||||
g.clearRect(0, headerH, maxX, headerH + rowH);
|
||||
g.setBgColor(color3);
|
||||
g.clearRect(colW * 5, headerH + rowH, maxX, maxY);
|
||||
for (let y = headerH; y < maxY; y += rowH) {
|
||||
g.drawLine(0, y, maxX, y);
|
||||
}
|
||||
for (let x = 0; x < maxX; x += colW) {
|
||||
g.drawLine(x, headerH, x, maxY);
|
||||
}
|
||||
|
||||
const month = date.getMonth();
|
||||
const year = date.getFullYear();
|
||||
const monthMap = {
|
||||
0: "January",
|
||||
1: "February",
|
||||
2: "March",
|
||||
3: "April",
|
||||
4: "May",
|
||||
5: "June",
|
||||
6: "July",
|
||||
7: "August",
|
||||
8: "September",
|
||||
9: "October",
|
||||
10: "November",
|
||||
11: "December"
|
||||
};
|
||||
g.setFontAlign(0, 0);
|
||||
g.setFont("6x8", 2);
|
||||
g.setColor(white);
|
||||
g.drawString(`${monthMap[month]} ${year}`, maxX / 2, headerH / 2);
|
||||
g.drawPoly([10, headerH / 2, 20, 10, 20, headerH - 10], true);
|
||||
g.drawPoly(
|
||||
[maxX - 10, headerH / 2, maxX - 20, 10, maxX - 20, headerH - 10],
|
||||
true
|
||||
);
|
||||
|
||||
g.setFont("6x8", 2);
|
||||
const dowLbls = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
|
||||
dowLbls.forEach((lbl, i) => {
|
||||
g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2);
|
||||
});
|
||||
|
||||
date.setDate(1);
|
||||
const dow = date.getDay();
|
||||
const dowNorm = dow === 0 ? 7 : dow;
|
||||
|
||||
const monthMaxDayMap = {
|
||||
0: 31,
|
||||
1: (2020 - year) % 4 === 0 ? 29 : 28,
|
||||
2: 31,
|
||||
3: 30,
|
||||
4: 31,
|
||||
5: 30,
|
||||
6: 31,
|
||||
7: 31,
|
||||
8: 30,
|
||||
9: 31,
|
||||
10: 30,
|
||||
11: 31
|
||||
};
|
||||
|
||||
let days = [];
|
||||
let nextMonthDay = 1;
|
||||
let thisMonthDay = 51;
|
||||
let prevMonthDay = monthMaxDayMap[month > 0 ? month - 1 : 11] - dowNorm;
|
||||
for (let i = 0; i < colN * (rowN - 1) + 1; i++) {
|
||||
if (i < dowNorm) {
|
||||
days.push(prevMonthDay);
|
||||
prevMonthDay++;
|
||||
} else if (thisMonthDay <= monthMaxDayMap[month] + 50) {
|
||||
days.push(thisMonthDay);
|
||||
thisMonthDay++;
|
||||
} else {
|
||||
days.push(nextMonthDay);
|
||||
nextMonthDay++;
|
||||
}
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
for (y = 0; y < rowN - 1; y++) {
|
||||
for (x = 0; x < colN; x++) {
|
||||
i++;
|
||||
const day = days[i];
|
||||
const isToday =
|
||||
today.year === year && today.month === month && today.day === day - 50;
|
||||
if (isToday) {
|
||||
g.setColor(red);
|
||||
g.drawRect(
|
||||
x * colW,
|
||||
y * rowH + headerH + rowH,
|
||||
x * colW + colW - 1,
|
||||
y * rowH + headerH + rowH + rowH
|
||||
);
|
||||
}
|
||||
g.setColor(day < 50 ? gray3 : white);
|
||||
g.drawString(
|
||||
(day > 50 ? day - 50 : day).toString(),
|
||||
x * colW + colW / 2,
|
||||
headerH + rowH + y * rowH + rowH / 2
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const date = new Date();
|
||||
const today = {
|
||||
day: date.getDate(),
|
||||
month: date.getMonth(),
|
||||
year: date.getFullYear()
|
||||
};
|
||||
drawCalendar(date);
|
||||
clearWatch();
|
||||
setWatch(
|
||||
() => {
|
||||
const month = date.getMonth();
|
||||
const prevMonth = month > 0 ? month - 1 : 11;
|
||||
if (prevMonth === 11) date.setFullYear(date.getFullYear() - 1);
|
||||
date.setMonth(prevMonth);
|
||||
drawCalendar(date);
|
||||
},
|
||||
BTN4,
|
||||
{ repeat: true }
|
||||
);
|
||||
setWatch(
|
||||
() => {
|
||||
const month = date.getMonth();
|
||||
const prevMonth = month < 11 ? month + 1 : 0;
|
||||
if (prevMonth === 0) date.setFullYear(date.getFullYear() + 1);
|
||||
date.setMonth(month + 1);
|
||||
drawCalendar(date);
|
||||
},
|
||||
BTN5,
|
||||
{ repeat: true }
|
||||
);
|
||||
setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" });
|
Binary file not shown.
After Width: | Height: | Size: 540 B |
|
@ -0,0 +1,2 @@
|
|||
0.01: Core functionnality
|
||||
0.02: Offer to enable HID if disabled. Handle with/without media keys
|
|
@ -45,13 +45,7 @@ const KEY = {
|
|||
0 : 39
|
||||
};
|
||||
|
||||
function sendHID(code) {
|
||||
return new Promise(resolve=>{
|
||||
NRF.sendHIDReport([2,0,0,code,0,0,0,0,0], () => {
|
||||
NRF.sendHIDReport([2,0,0,0,0,0,0,0,0], resolve);
|
||||
});
|
||||
});
|
||||
};
|
||||
var sendHID;
|
||||
|
||||
function showChars(x,chars) {
|
||||
var lines = Math.round(Math.sqrt(chars.length)*2);
|
||||
|
@ -103,10 +97,24 @@ function startKeyboardHID() {
|
|||
}).then(startKeyboardHID);
|
||||
};
|
||||
|
||||
if (!settings.HID) {
|
||||
E.showMessage('HID disabled');
|
||||
setTimeout(load, 1000);
|
||||
} else {
|
||||
if (settings.HID=="kb" || settings.HID=="kbmedia") {
|
||||
if (settings.HID=="kbmedia") {
|
||||
sendHID = function(code) {
|
||||
return new Promise(resolve=>{
|
||||
NRF.sendHIDReport([2,0,0,code,0,0,0,0,0], () => {
|
||||
NRF.sendHIDReport([2,0,0,0,0,0,0,0,0], resolve);
|
||||
});
|
||||
});
|
||||
};
|
||||
} else {
|
||||
sendHID = function(code) {
|
||||
return new Promise(resolve=>{
|
||||
NRF.sendHIDReport([0,0,code,0,0,0,0,0], () => {
|
||||
NRF.sendHIDReport([0,0,0,0,0,0,0,0], resolve);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
startKeyboardHID();
|
||||
setWatch(() => {
|
||||
sendHID(44); // space
|
||||
|
@ -114,4 +122,12 @@ if (!settings.HID) {
|
|||
setWatch(() => {
|
||||
sendHID(40); // enter
|
||||
}, BTN3, {repeat:true});
|
||||
} else {
|
||||
E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) {
|
||||
if (enable) {
|
||||
settings.HID = "kb";
|
||||
require("Storage").write('setting.json', settings);
|
||||
setTimeout(load, 1000, "hidbkbd.app.js");
|
||||
} else setTimeout(load, 1000);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: Core functionnality
|
||||
0.02: Offer to enable HID if disabled
|
||||
|
|
|
@ -4,7 +4,7 @@ const settings = storage.readJSON('setting.json',1) || { HID: false };
|
|||
|
||||
var sendHid, camShot, profile;
|
||||
|
||||
if (settings.HID) {
|
||||
if (settings.HID=="kbmedia") {
|
||||
profile = 'camShutter';
|
||||
sendHid = function (code, cb) {
|
||||
try {
|
||||
|
@ -19,8 +19,13 @@ if (settings.HID) {
|
|||
};
|
||||
camShot = function (cb) { sendHid(0x80, cb); };
|
||||
} else {
|
||||
E.showMessage('HID disabled');
|
||||
setTimeout(load, 1000);
|
||||
E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) {
|
||||
if (enable) {
|
||||
settings.HID = "kbmedia";
|
||||
require("Storage").write('setting.json', settings);
|
||||
setTimeout(load, 1000, "hidcam.app.js");
|
||||
} else setTimeout(load, 1000);
|
||||
});
|
||||
}
|
||||
function drawApp() {
|
||||
g.clear();
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AH4ADhvd6AWVAAIYTCwQABC9JGDJCYX/R+7XYgEE7tACycAgczmAX/C/4X/C6kBiMQCyoABDB0N7vdAgIWCAAIXjxAAQCwkIC6OAC/4X/C/4XbgAXRCwgA/AH4ANA"))
|
|
@ -0,0 +1,74 @@
|
|||
var storage = require('Storage');
|
||||
const settings = storage.readJSON('setting.json',1) || { HID: false };
|
||||
|
||||
var sendInProgress = false; // Only send one message at a time, do not flood
|
||||
|
||||
const sendHid = function (x, y, btn1, btn2, btn3, btn4, btn5, cb) {
|
||||
try {
|
||||
const buttons = (btn5<<4) | (btn4<<3) | (btn3<<2) | (btn2<<1) | (btn1<<0);
|
||||
if (!sendInProgress) {
|
||||
sendInProgress = true;
|
||||
NRF.sendHIDReport([buttons, x, y], () => {
|
||||
sendInProgress = false;
|
||||
if (cb) cb();
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
print(e);
|
||||
}
|
||||
};
|
||||
|
||||
function drawApp() {
|
||||
g.clear();
|
||||
g.setFont("6x8",2);
|
||||
g.setFontAlign(0,0);
|
||||
g.drawString("Joystick", 120, 120);
|
||||
const d = g.getWidth() - 18;
|
||||
|
||||
function c(a) {
|
||||
return {
|
||||
width: 8,
|
||||
height: a.length,
|
||||
bpp: 1,
|
||||
buffer: (new Uint8Array(a)).buffer
|
||||
};
|
||||
}
|
||||
|
||||
g.drawImage(c([16,56,124,254,16,16,16,16]),d,40);
|
||||
g.drawImage(c([16,16,16,16,254,124,56,16]),d,194);
|
||||
g.drawImage(c([0,8,12,14,255,14,12,8]),d,116);
|
||||
}
|
||||
|
||||
function update() {
|
||||
const btn1 = BTN1.read();
|
||||
const btn2 = BTN2.read();
|
||||
const btn3 = BTN3.read();
|
||||
const btn4 = BTN4.read();
|
||||
const btn5 = BTN5.read();
|
||||
const acc = Bangle.getAccel();
|
||||
var x = acc.x*-127;
|
||||
var y = acc.y*-127;
|
||||
|
||||
// check limits
|
||||
if (x > 127) x = 127;
|
||||
else if (x < -127) x = -127;
|
||||
if (y > 127) y = 127;
|
||||
else if (y < -127) y = -127;
|
||||
|
||||
sendHid(x & 0xff, y & 0xff, btn1, btn2, btn3, btn4, btn5);
|
||||
}
|
||||
|
||||
if (settings.HID === "joy") {
|
||||
drawApp();
|
||||
setInterval(update, 100); // 10 Hz
|
||||
} else {
|
||||
E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) {
|
||||
if (enable) {
|
||||
settings.HID = "joy";
|
||||
storage.write('setting.json', settings);
|
||||
setTimeout(load, 1000, "hidjoystick.app.js");
|
||||
} else {
|
||||
setTimeout(load, 1000);
|
||||
}
|
||||
});
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 655 B |
|
@ -0,0 +1,2 @@
|
|||
0.01: Core functionnality
|
||||
0.02: Offer to enable HID if disabled. Handle with/without media keys
|
|
@ -4,27 +4,46 @@ const settings = storage.readJSON('setting.json',1) || { HID: false };
|
|||
|
||||
var sendHid, next, prev, toggle, up, down, profile;
|
||||
|
||||
if (settings.HID) {
|
||||
if (settings.HID=="kb" || settings.HID=="kbmedia") {
|
||||
profile = 'Keyboard';
|
||||
sendHid = function (code, cb) {
|
||||
try {
|
||||
NRF.sendHIDReport([2,0,0,code,0,0,0,0,0], () => {
|
||||
NRF.sendHIDReport([2,0,0,0,0,0,0,0,0], () => {
|
||||
if (cb) cb();
|
||||
if (settings.HID=="kbmedia") {
|
||||
sendHid = function (code, cb) {
|
||||
try {
|
||||
NRF.sendHIDReport([2,0,0,code,0,0,0,0,0], () => {
|
||||
NRF.sendHIDReport([2,0,0,0,0,0,0,0,0], () => {
|
||||
if (cb) cb();
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch(e) {
|
||||
print(e);
|
||||
}
|
||||
};
|
||||
} catch(e) {
|
||||
print(e);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
sendHid = function (code, cb) {
|
||||
try {
|
||||
NRF.sendHIDReport([0,0,code,0,0,0,0,0], () => {
|
||||
NRF.sendHIDReport([0,0,0,0,0,0,0,0], () => {
|
||||
if (cb) cb();
|
||||
});
|
||||
});
|
||||
} catch(e) {
|
||||
print(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
next = function (cb) { sendHid(0x4f, cb); };
|
||||
prev = function (cb) { sendHid(0x50, cb); };
|
||||
toggle = function (cb) { sendHid(0x2c, cb); };
|
||||
up = function (cb) {sendHid(0x52, cb); };
|
||||
down = function (cb) { sendHid(0x51, cb); };
|
||||
} else {
|
||||
E.showMessage('HID disabled');
|
||||
setTimeout(load, 1000);
|
||||
E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) {
|
||||
if (enable) {
|
||||
settings.HID = "kb";
|
||||
require("Storage").write('setting.json', settings);
|
||||
setTimeout(load, 1000, "hidkbd.app.js");
|
||||
} else setTimeout(load, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
function drawApp() {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: Core functionnality
|
|
@ -4,7 +4,7 @@ const settings = storage.readJSON('setting.json',1) || { HID: false };
|
|||
|
||||
var sendHid, next, prev, toggle, up, down, profile;
|
||||
|
||||
if (settings.HID) {
|
||||
if (settings.HID=="kbmedia") {
|
||||
profile = 'Music';
|
||||
sendHid = function (code, cb) {
|
||||
try {
|
||||
|
@ -23,8 +23,13 @@ if (settings.HID) {
|
|||
up = function (cb) {sendHid(0x40, cb); };
|
||||
down = function (cb) { sendHid(0x80, cb); };
|
||||
} else {
|
||||
E.showMessage('HID disabled');
|
||||
setTimeout(load, 1000);
|
||||
E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) {
|
||||
if (enable) {
|
||||
settings.HID = "kbmedia";
|
||||
require("Storage").write('setting.json', settings);
|
||||
setTimeout(load, 1000, "hidmsc.app.js");
|
||||
} else setTimeout(load, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
function drawApp() {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
# Imprecise Word Clock
|
||||
|
||||
This clock tells time in very rough approximation, as in "Late morning" or "Early afternoon." Good for vacations and weekends. Press button 1 to see the time in accurate, digital form. But do you really need to know the exact time?
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwkEIf4A3iIBEn8ggP//8wgX/+cQl8Agc/BQPyCokQgHzmEB+ET+EfmMj+AXCmABBF4MBiIABiEC+PxC4Uwn4NB+QXMBAMzI4UxmYOBC5sfCgIvBgPzF4cfC5BgCFAMPkPwiXzL4cPmMvkAXDPAnzEgMxR4wDCGITl/AH4ApgUQbIICBAgXwBYMD+UAYoP/l4CBiUhd4QXFgIXCh73BfQUfAgIPBC4cQiIACC4cvj4PBC5AuCC48zgcwC4ZHBC5sBCAIEBF5EAC4RgDCQItCPAIXLCoQBBFgM/IoZHER4QA/AH4Anj8wgXzgX/+cQWoPyYQK9Bn/zj/wb4MTCAMf+MDAYMxkfwj8BmYXBmEzCYMf+cDmPzkMvj8zAIM/eoPyC4fy+IXDl8TmfwI4UvmYABAwIXB//xgPwBIIXCgYFBmEP/8fh/yF4sDC4QjBC4RvBF4UPB4JUBL4kAn8ROIJbBC4IIBL4hDBmaPEgBuB+EB+aPCUQUjCALn/AH4A/A"))
|
|
@ -0,0 +1,160 @@
|
|||
/* Imprecise Word Clock - A. Blanton
|
||||
A remix of word clock
|
||||
by Gordon Williams https://github.com/gfwilliams
|
||||
- Changes the representation of time to be more general
|
||||
- Shows accurate digital time when button 1 is pressed
|
||||
*/
|
||||
/* jshint esversion: 6 */
|
||||
|
||||
const allWords = [
|
||||
"AEARLYDN",
|
||||
"LATEYRZO",
|
||||
"MORNINGO",
|
||||
"KMIDDLEN",
|
||||
"AFTERDAY",
|
||||
"OFDZTHEC",
|
||||
"EVENINGR",
|
||||
"ORMNIGHT"
|
||||
];
|
||||
|
||||
|
||||
const timeOfDay = {
|
||||
0: ["", 0, 0],
|
||||
1: ["EARLYMORNING", 10, 20, 30, 40, 50, 02, 12, 22, 32, 42, 52, 62],
|
||||
2: ["MORNING", 02, 12, 22, 32, 42, 52, 62],
|
||||
3: ["LATEMORNING", 01, 11, 21, 31, 02, 12, 22, 32, 42, 52, 62],
|
||||
4: ["MIDDAY", 13, 23, 33, 54, 64, 74],
|
||||
5: ["EARLYAFTERNOON", 10, 20, 30, 40, 50, 04, 14, 24, 34, 44, 70, 71, 72, 73],
|
||||
6: ["AFTERNOON", 04, 14, 24, 34, 44, 70, 71, 72, 73],
|
||||
7: ["LATEAFTERNOON", 01, 11, 21, 31, 04, 14, 24, 34, 44, 70, 71, 72, 73],
|
||||
8: ["EARLYEVENING", 10, 20, 30, 40, 50, 06, 16, 26, 36, 46, 56, 66],
|
||||
9: ["EVENING", 06, 16, 26, 36, 46, 56, 66],
|
||||
10: ["NIGHT", 37, 47, 57, 67, 77],
|
||||
11: ["MIDDLEOFTHENIGHT", 13, 23, 33, 43, 53, 63, 05, 15, 45, 55, 65, 37,47,57,67,77 ],
|
||||
};
|
||||
|
||||
|
||||
// offsets and increments
|
||||
const xs = 35;
|
||||
const ys = 31;
|
||||
const dy = 22;
|
||||
const dx = 25;
|
||||
|
||||
// font size and color
|
||||
const fontSize = 3; // "6x8"
|
||||
const passivColor = 0x3186 /*grey*/ ;
|
||||
const activeColorNight = 0xF800 /*red*/ ;
|
||||
const activeColorDay = 0xFFFF /* white */;
|
||||
|
||||
function drawWordClock() {
|
||||
|
||||
|
||||
// get time
|
||||
var t = new Date();
|
||||
var h = t.getHours();
|
||||
var m = t.getMinutes();
|
||||
var time = ("0" + h).substr(-2) + ":" + ("0" + m).substr(-2);
|
||||
var day = t.getDay();
|
||||
|
||||
var hidx;
|
||||
|
||||
var activeColor = activeColorDay;
|
||||
if(h < 7 || h > 19) {activeColor = activeColorNight;}
|
||||
|
||||
g.setFont("6x8",fontSize);
|
||||
g.setColor(passivColor);
|
||||
g.setFontAlign(0, -1, 0);
|
||||
|
||||
// draw allWords
|
||||
var c;
|
||||
var y = ys;
|
||||
var x = xs;
|
||||
allWords.forEach((line) => {
|
||||
x = xs;
|
||||
for (c in line) {
|
||||
g.drawString(line[c], x, y);
|
||||
x += dx;
|
||||
}
|
||||
y += dy;
|
||||
});
|
||||
|
||||
|
||||
// Switch case isn't good for this in Js apparently so...
|
||||
if(h < 3){
|
||||
// Middle of the Night
|
||||
hidx = 11;
|
||||
}
|
||||
else if (h < 7){
|
||||
// Early Morning
|
||||
hidx = 1;
|
||||
}
|
||||
else if (h < 10){
|
||||
// Morning
|
||||
hidx = 2;
|
||||
}
|
||||
else if (h < 12){
|
||||
// Late Morning
|
||||
hidx = 3;
|
||||
}
|
||||
else if (h < 13){
|
||||
// Midday
|
||||
hidx = 4;
|
||||
}
|
||||
else if (h < 14){
|
||||
// Early afternoon
|
||||
hidx = 5;
|
||||
}
|
||||
else if (h < 16){
|
||||
// Afternoon
|
||||
hidx = 6;
|
||||
}
|
||||
else if (h < 17){
|
||||
// Late Afternoon
|
||||
hidx = 7;
|
||||
}
|
||||
else if (h < 19){
|
||||
// Early evening
|
||||
hidx = 8;
|
||||
}
|
||||
else if (h < 21){
|
||||
// evening
|
||||
hidx = 9;
|
||||
}
|
||||
else if (h < 24){
|
||||
// Night
|
||||
hidx = 10;
|
||||
}
|
||||
|
||||
// write hour in active color
|
||||
g.setColor(activeColor);
|
||||
timeOfDay[hidx][0].split('').forEach((c, pos) => {
|
||||
x = xs + (timeOfDay[hidx][pos + 1] / 10 | 0) * dx;
|
||||
y = ys + (timeOfDay[hidx][pos + 1] % 10) * dy;
|
||||
g.drawString(c, x, y);
|
||||
});
|
||||
|
||||
|
||||
// Display digital time while button 1 is pressed
|
||||
if (BTN1.read()){
|
||||
g.setColor(activeColor);
|
||||
g.clearRect(0, 215, 240, 240);
|
||||
g.drawString(time, 120, 215);
|
||||
} else { g.clearRect(0, 215, 240, 240); }
|
||||
|
||||
}
|
||||
|
||||
Bangle.on('lcdPower', function(on) {
|
||||
if (on) drawWordClock();
|
||||
});
|
||||
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
setInterval(drawWordClock, 1E4);
|
||||
drawWordClock();
|
||||
|
||||
// Show digital time while top button is pressed
|
||||
setWatch(drawWordClock, BTN1, {repeat:true,edge:"both"});
|
||||
|
||||
// Show launcher when middle button pressed
|
||||
setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
|
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1,206 @@
|
|||
var locale = require("locale");
|
||||
var CHARW = 34;
|
||||
var CHARP = 2;
|
||||
var Y = 50;
|
||||
// Offscreen buffer
|
||||
var buf = Graphics.createArrayBuffer(CHARW+CHARP*2,CHARW*2 + CHARP*2,1,{msb:true});
|
||||
var bufimg = {width:buf.getWidth(),height:buf.getHeight(),buffer:buf.buffer};
|
||||
// The last time that we displayed
|
||||
var lastTime = " ";
|
||||
// If animating, this is the interval's id
|
||||
var animInterval;
|
||||
var timeInterval;
|
||||
|
||||
/* Get array of lines from digit d to d+1.
|
||||
n is the amount (0..1)
|
||||
maxFive is true is this digit only counts 0..5 */
|
||||
const DIGITS = {
|
||||
" ":(g,s,p,n)=>{},
|
||||
"0":(g,s,p,n)=>{
|
||||
g.fillRect(1+s*n,1-p, 1+s,1+p);
|
||||
g.fillRect(1+s-p,1, 1+s+p,1+s);
|
||||
g.fillRect(1+s-p,1+s, 1+s+p,1+2*s);
|
||||
g.fillRect(1+s*n,1+2*s-p, 1+s,1+2*s+p);
|
||||
g.fillRect(1+s*n,1+s-p, 1+s*n,1+2*s+p);
|
||||
g.fillRect(1+s*n-p,1, 1+s*n+p,1+s)},
|
||||
"1":(g,s,p,n)=>{
|
||||
g.fillRect(1+(1-n)*s,1-p, 1+s,1+p);
|
||||
g.fillRect(1+s-p,1, 1+s+p,1+s);
|
||||
g.fillRect(1+(1-n)*s,1+s-p, 1+s,1+s+p);
|
||||
g.fillRect(1-p+(1-n)*s,1+s, 1+p+(1-n)*s,1+2*s);
|
||||
g.fillRect(1+(1-n)*s,1-p+2*s, 1+s,1+p+2*s)},
|
||||
"2":(g,s,p,n)=>{
|
||||
g.fillRect(1,1-p, 1+s,1+p);
|
||||
g.fillRect(1+s-p,1, 1+s+p,1+s);
|
||||
g.fillRect(1,1+s-p, 1+s,1+s+p);
|
||||
g.fillRect(1-p,1+(1+n)*s, 1+p,1+2*s);
|
||||
g.fillRect(1+s-p,1+(2-n)*s, 1+s+p,1+2*s);
|
||||
g.fillRect(1,1+2*s-p, 1+s,1+2*s+p)},
|
||||
"3":(g,s,p,n)=>{
|
||||
g.fillRect(1,1-p, 1+(1-n)*s,1+p);
|
||||
g.fillRect(1-p,1, 1+p,n);
|
||||
g.fillRect(1+s-p,1, 1+s+p,1+s);
|
||||
g.fillRect(1,1+s-p, 1+s,1+s+p);
|
||||
g.fillRect(1+s-p,1+s, 1+s+p,1+2*s);
|
||||
g.fillRect(1+s*n,1+2*s-p, 1+s,1+2*s+p)},
|
||||
"4":(g,s,p,n)=>{
|
||||
g.fillRect(1-p,1, 1+p,1+s);
|
||||
g.fillRect(1+s,1-p, 1+(1-n)*s,1+p);
|
||||
g.fillRect(1+s-p,1, 1+s+p,1+(1-n)*s);
|
||||
g.fillRect(1,1+s-p, 1+s,1+s+p);
|
||||
g.fillRect(1+s-p,1+s, 1+s+p,1+2*s);
|
||||
g.fillRect(1+(1-n)*s,1+2*s-p, 1+s,1+2*s+p)},
|
||||
"5to0": (g,s,p,n)=>{ // 5 -> 0
|
||||
g.fillRect(1-p,1, 1+p,1+s);
|
||||
g.fillRect(1,1-p, 1+s,1+p);
|
||||
g.fillRect(1+s*n,1+s-p, 1+s,1+s+p);
|
||||
g.fillRect(1+s-p,1+s, 1+s+p,1+2*s);
|
||||
g.fillRect(1,1+2*s*p, 1+s,1+2*s+p);
|
||||
g.fillRect(1,1+2*s-p, 1,1+2*s+p);
|
||||
g.fillRect(1+s-p,1+(1-n)*s, 1+s+p,1+s);
|
||||
g.fillRect(1-p,1+s, 1+p,1+(1+n)*s)},
|
||||
"5to6": (g,s,p,n)=>{ // 5 -> 6
|
||||
g.fillRect(1-p,1, 1+p,1+s);
|
||||
g.fillRect(1,1-p, 1+s,1+p);
|
||||
g.fillRect(1,1+s-p, 1+s,1+s+p);
|
||||
g.fillRect(1+s-p,1+s, 1+s+p,1+2*s);
|
||||
g.fillRect(1,1+2*s-p, 1+s,1+2*s+p);
|
||||
g.fillRect(1-p,2-n, 1+p,1+2*s)},
|
||||
"6":(g,s,p,n)=>{
|
||||
g.fillRect(1-p,1, 1+p,1+(1-n)*s);
|
||||
g.fillRect(1,1-p, 1+s,1+p);
|
||||
g.fillRect(1+s*n,1+s-p, 1+s,1+s+p);
|
||||
g.fillRect(1+s-p,1+(1-n)*s, 1+s+p,1+s);
|
||||
g.fillRect(1+s-p,1+s, 1+s+p,1+2*s);
|
||||
g.fillRect(1+s*n,1+2*s-p, 1+s,1+2*s+p);
|
||||
g.fillRect(1-p,1+(1-n)*s, 1+p,1+s*(2-2*n))},
|
||||
"7":(g,s,p,n)=>{
|
||||
g.fillRect(1-p,1, 1+p,n);
|
||||
g.fillRect(1,1-p, 1+s,1+p);
|
||||
g.fillRect(1+s-p,1, 1+s+p,1+s);
|
||||
g.fillRect(1+(1-n)*s,1+s-p, 1+s,1+s+p);
|
||||
g.fillRect(1+s-p,1+s, 1+s+p,1+2*s);
|
||||
g.fillRect(1+(1-n)*s,1+2*s-p, 1+s,1+2*s+p);
|
||||
g.fillRect(1+(1-n)*s-p,1+s, 1+(1-n)*s+p,1+2*s)},
|
||||
"8":(g,s,p,n)=>{
|
||||
g.fillRect(1-p,1, 1+p,1+s);
|
||||
g.fillRect(1,1-p, 1+s,1+p);
|
||||
g.fillRect(1+s-p,1, 1+s+p,1+s);
|
||||
g.fillRect(1,1+s-p, 1+s,1+s+p);
|
||||
g.fillRect(1+s-p,1+s, 1+s+p,1+2*s);
|
||||
g.fillRect(1,1+2*s-p, 1+s,1+2*s+p);
|
||||
g.fillRect(1-p,1+s, 1+p,1+s*(2-n))},
|
||||
"9":(g,s,p,n)=>{
|
||||
g.fillRect(1-p,1, 1+p,1+s);
|
||||
g.fillRect(1,1-p, 1+s,1+p);
|
||||
g.fillRect(1+s-p,1, 1+s+p,1+s);
|
||||
g.fillRect(1,1+s-p, 1+(1-n)*s,1+s+p);
|
||||
g.fillRect(1-p,1+s, 1+p,1+(1+n)*s);
|
||||
g.fillRect(1+s-p,1+s, 1+s+p,1+2*s);
|
||||
g.fillRect(1,1+2*s-p, 1+s,1+2*s+p)},
|
||||
":":(g,s,p,n)=>{
|
||||
g.fillRect(1+s*0.4,1+s*0.4-p, 1+s*0.6,1+s*0.4+p);
|
||||
g.fillRect(1+s*0.6-p,1+s*0.4, 1+s*0.6+p,1+s*0.6);
|
||||
g.fillRect(1+s*0.6,1+s*0.6-p, 1+s*0.4,1+s*0.6+p);
|
||||
g.fillRect(1+s*0.4-p,1+s*0.4, 1+s*0.4+p,1+s*0.6);
|
||||
g.fillRect(1+s*0.4,1+s*1.4-p, 1+s*0.6,1+s*1.4+p);
|
||||
g.fillRect(1+s*0.6-p,1+s*1.4, 1+s*0.6+p,1+s*1.6);
|
||||
g.fillRect(1+s*0.6,1+s*1.6-p, 1+s*0.4,1+s*1.6+p);
|
||||
g.fillRect(1+s*0.4-p,1+s*1.4, 1+s*0.4+p,1+s*1.6)
|
||||
}};
|
||||
|
||||
/* Draw a transition between lastText and thisText.
|
||||
'n' is the amount - 0..1 */
|
||||
function drawDigits(lastText,thisText,n) {
|
||||
const p = CHARP; // padding around digits
|
||||
const s = CHARW; // character size
|
||||
var x = p; // x offset
|
||||
var y = Y+p; // y offset
|
||||
g.reset();
|
||||
for (var i=0;i<lastText.length;i++) {
|
||||
var lastCh = lastText[i];
|
||||
var thisCh = thisText[i];
|
||||
if (thisCh==":") x-=4;
|
||||
if (lastCh!=thisCh) {
|
||||
var ch, chn = n;
|
||||
if ((thisCh-1==lastCh ||
|
||||
(thisCh==0 && lastCh==5) ||
|
||||
(thisCh==0 && lastCh==9)))
|
||||
ch = lastCh;
|
||||
else {
|
||||
ch = thisCh;
|
||||
chn = 0;
|
||||
}
|
||||
if (ch=="5") ch = (lastCh==5 && thisCh==0)?"5to0":"5to6";
|
||||
buf.clear();
|
||||
DIGITS[ch](buf,s,p,chn);
|
||||
g.drawImage(bufimg,x-1,y-1);
|
||||
}
|
||||
if (thisCh==":") x-=4;
|
||||
x+=s+p+7;
|
||||
}
|
||||
}
|
||||
function drawSeconds() {
|
||||
var x = CHARW*6 + CHARP*2 - 4;
|
||||
var y = Y + 2*CHARW + CHARP;
|
||||
var d = new Date();
|
||||
g.reset();
|
||||
g.setFont("6x8");
|
||||
g.setFontAlign(-1,-1);
|
||||
g.drawString(("0"+d.getSeconds()).substr(-2), x, y-8, true);
|
||||
// date
|
||||
g.setFontAlign(0,-1);
|
||||
var date = locale.date(d,false);
|
||||
g.drawString(date, g.getWidth()/2, y+8, true);
|
||||
}
|
||||
|
||||
/* Show the current time, and animate if needed */
|
||||
function showTime() {
|
||||
if (animInterval) return; // in animation - quit
|
||||
var d = new Date();
|
||||
var t = (" "+d.getHours()).substr(-2)+":"+
|
||||
("0"+d.getMinutes()).substr(-2);
|
||||
var l = lastTime;
|
||||
// same - don't animate
|
||||
if (t==l || l==" ") {
|
||||
drawDigits(t,l,0);
|
||||
drawSeconds();
|
||||
return;
|
||||
}
|
||||
var n = 0;
|
||||
animInterval = setInterval(function() {
|
||||
n += 1/10;
|
||||
if (n>=1) {
|
||||
n=1;
|
||||
clearInterval(animInterval);
|
||||
animInterval = undefined;
|
||||
}
|
||||
drawDigits(l,t,n);
|
||||
}, 20);
|
||||
lastTime = t;
|
||||
}
|
||||
|
||||
Bangle.on('lcdPower',function(on) {
|
||||
if (animInterval) {
|
||||
clearInterval(animInterval);
|
||||
animInterval = undefined;
|
||||
}
|
||||
if (timeInterval) {
|
||||
clearInterval(timeInterval);
|
||||
timeInterval = undefined;
|
||||
}
|
||||
if (on) {
|
||||
showTime();
|
||||
timeInterval = setInterval(showTime, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
// Update time once a second
|
||||
timeInterval = setInterval(showTime, 1000);
|
||||
showTime();
|
||||
|
||||
// Show launcher when middle button pressed
|
||||
setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: 2 players local + improve ai
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
# Pong
|
||||
|
||||
A clone of the Atari game Pong
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/702227/79855656-2a507a00-83c3-11ea-9162-65732729b992.png" height="384" width="384" />
|
||||
|
||||
## Features
|
||||
|
||||
- Play against a dumb AI
|
||||
- Play local Multiplayer against your friends
|
||||
|
||||
## Controls
|
||||
|
||||
Player's controls:
|
||||
- UP: BTN1
|
||||
- DOWN: BTN2
|
||||
long press to move faster
|
||||
|
||||
Restart a game:
|
||||
- RESET: BTN3
|
||||
|
||||
Buttons for player 2:
|
||||
- UP: BTN4
|
||||
- DOWN: BTN5
|
||||
|
||||
## Creator
|
||||
|
||||
<https://twitter.com/fredericrous>
|
288
apps/pong/app.js
288
apps/pong/app.js
|
@ -8,6 +8,7 @@
|
|||
* - Let's make pong, One Man Army Studios, Youtube
|
||||
* - Pong.js, KanoComputing, Github
|
||||
* - Coding Challenge #67: Pong!, The Coding Train, Youtube
|
||||
* - Pixl.js Multiplayer Pong, espruino website
|
||||
*/
|
||||
|
||||
const SCREEN_WIDTH = 240;
|
||||
|
@ -15,6 +16,13 @@ const FPS = 16;
|
|||
const MAX_SCORE = 11;
|
||||
let scores = [0, 0];
|
||||
let aiSpeedRandom = 0;
|
||||
let winnerMessage = '';
|
||||
|
||||
const sound = {
|
||||
ping: () => Bangle.beep(8, 466),
|
||||
pong: () => Bangle.beep(8, 220),
|
||||
fall: () => Bangle.beep(16*3, 494).then(_ => Bangle.beep(32*3, 3322))
|
||||
};
|
||||
|
||||
function Vector(x, y) {
|
||||
this.x = x;
|
||||
|
@ -28,12 +36,18 @@ Vector.prototype.add = function (x) {
|
|||
|
||||
const constrain = (n, low, high) => Math.max(Math.min(n, high), low);
|
||||
const random = (min, max) => Math.random() * (max - min) + min;
|
||||
const intersects = (circ, rect) => {
|
||||
var c1 = circ.pos, c2 = {x: circ.pos.x+circ.r, y: circ.pos.y+circ.r};
|
||||
var r1 = rect.pos, r2 = {x: rect.pos.x+rect.width*2, y: rect.pos.y+rect.height};
|
||||
return !(c1.x > r2.x || c2.x < r1.x ||
|
||||
c1.y > r2.y || c2.y < r1.y);
|
||||
};
|
||||
const intersects = (circ, rect, right) => {
|
||||
var c = circ.pos;
|
||||
var r = circ.r;
|
||||
if (c.y - r < rect.pos.y + rect.height && c.y + r > rect.pos.y) {
|
||||
if (right) {
|
||||
return c.x + r > rect.pos.x - rect.width*2 && c.x < rect.pos.x + rect.width
|
||||
} else {
|
||||
return c.x - r < rect.pos.x + rect.width*2 && c.x > rect.pos.x - rect.width
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
///////////////////////////// Ball //////////////////////////////////////////
|
||||
|
||||
|
@ -45,12 +59,26 @@ function Ball() {
|
|||
|
||||
this.reset();
|
||||
}
|
||||
Ball.prototype.show = function () {
|
||||
Ball.prototype.reset = function() {
|
||||
this.speed = this.originalSpeed;
|
||||
var x = scores[0] < scores[1] || (scores[0] === 0 && scores[1] === 0) ? -this.speed : this.speed;
|
||||
var bounceAngle = Math.PI/6;
|
||||
this.velocity = new Vector(x * Math.cos(bounceAngle), this.speed * -Math.sin(bounceAngle));
|
||||
this.pos = new Vector(SCREEN_WIDTH/2, random(0, SCREEN_WIDTH));
|
||||
this.ballReturn = 0;
|
||||
};
|
||||
Ball.prototype.restart = function() {
|
||||
this.reset();
|
||||
ai.pos = new Vector(SCREEN_WIDTH - ai.width*2, SCREEN_WIDTH/2 - ai.height/2);
|
||||
player.pos = new Vector(player.width*2, SCREEN_WIDTH/2 - player.height/2);
|
||||
this.pos = new Vector(SCREEN_WIDTH/2, SCREEN_WIDTH/2);
|
||||
};
|
||||
Ball.prototype.show = function (invert) {
|
||||
if (this.prevPos != null) {
|
||||
g.setColor(0);
|
||||
g.setColor(invert ? -1 : 0);
|
||||
g.fillCircle(this.prevPos.x, this.prevPos.y, this.prevPos.r);
|
||||
}
|
||||
g.setColor(-1);
|
||||
g.setColor(invert ? 0 : -1);
|
||||
g.fillCircle(this.pos.x, this.pos.y, this.r);
|
||||
this.prevPos = {
|
||||
x: this.pos.x,
|
||||
|
@ -58,55 +86,62 @@ Ball.prototype.show = function () {
|
|||
r: this.r
|
||||
};
|
||||
};
|
||||
Ball.prototype.bouncePlayer = function (multiplyX, multiplyY, player) {
|
||||
function bounceAngle(playerY, ballY, playerHeight, maxHangle) {
|
||||
let relativeIntersectY = (playerY + (playerHeight/2)) - ballY;
|
||||
let normalizedRelativeIntersectionY = relativeIntersectY / (playerHeight/2);
|
||||
let bounceAngle = normalizedRelativeIntersectionY * maxHangle;
|
||||
return { x: Math.cos(bounceAngle), y: -Math.sin(bounceAngle) };
|
||||
}
|
||||
Ball.prototype.bouncePlayer = function (directionX, directionY, player) {
|
||||
this.ballReturn++;
|
||||
this.speed = constrain(this.speed + 2, this.originalSpeed, this.maxSpeed);
|
||||
var relativeIntersectY = (player.pos.y+(player.height/2)) - this.pos.y;
|
||||
var normalizedRelativeIntersectionY = (relativeIntersectY/(player.height/2));
|
||||
var MAX_BOUNCE_ANGLE = 4 * Math.PI/12;
|
||||
var bounceAngle = normalizedRelativeIntersectionY * MAX_BOUNCE_ANGLE;
|
||||
this.velocity.x = this.speed * Math.cos(bounceAngle) * multiplyX;
|
||||
this.velocity.y = this.speed * -Math.sin(bounceAngle) * multiplyY;
|
||||
var angle = bounceAngle(player.pos.y, this.pos.y, player.height, MAX_BOUNCE_ANGLE)
|
||||
this.velocity.x = this.speed * angle.x * directionX;
|
||||
this.velocity.y = this.speed * angle.y * directionY;
|
||||
this.ballReturn % 2 === 0 ? sound.ping() : sound.pong();
|
||||
};
|
||||
Ball.prototype.bounce = function (multiplyX, multiplyY, player) {
|
||||
Ball.prototype.bounce = function (directionX, directionY, player) {
|
||||
if (player)
|
||||
return this.bouncePlayer(multiplyX, multiplyY, player);
|
||||
return this.bouncePlayer(directionX, directionY, player);
|
||||
|
||||
if (multiplyX) {
|
||||
this.velocity.x = Math.abs(this.velocity.x) * multiplyX;
|
||||
if (directionX) {
|
||||
this.velocity.x = Math.abs(this.velocity.x) * directionX;
|
||||
}
|
||||
if (multiplyY) {
|
||||
this.velocity.y = Math.abs(this.velocity.y) * multiplyY;
|
||||
if (directionY) {
|
||||
this.velocity.y = Math.abs(this.velocity.y) * directionY;
|
||||
}
|
||||
};
|
||||
Ball.prototype.checkWallsCollision = function () {
|
||||
Ball.prototype.fall = function (playerId) {
|
||||
scores[playerId]++;
|
||||
if (scores[playerId] >= MAX_SCORE) {
|
||||
this.restart();
|
||||
state = 3;
|
||||
if (playerId === 1) {
|
||||
winnerMessage = startOption === 0 ? "AI Wins!" : "Player 2 Wins!";
|
||||
} else {
|
||||
winnerMessage = startOption === 0 ? "You Win!" : "Player 1 Wins!";
|
||||
}
|
||||
} else {
|
||||
sound.fall();
|
||||
this.reset();
|
||||
}
|
||||
};
|
||||
Ball.prototype.wallCollision = function () {
|
||||
if (this.pos.y < 0) {
|
||||
this.bounce(0, 1);
|
||||
} else if (this.pos.y > SCREEN_WIDTH) {
|
||||
this.bounce(0, -1);
|
||||
} else if (this.pos.x < 0) {
|
||||
scores[1]++;
|
||||
if (scores[1] >= MAX_SCORE) {
|
||||
this.restart();
|
||||
state = 3;
|
||||
winnerMessage = "AI Wins!";
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
this.fall(1);
|
||||
} else if (this.pos.x > SCREEN_WIDTH) {
|
||||
scores[0]++;
|
||||
if (scores[0] >= MAX_SCORE) {
|
||||
this.restart();
|
||||
state = 3;
|
||||
winnerMessage = "You Win!";
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
this.fall(0);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
Ball.prototype.checkPlayerCollision = function (player) {
|
||||
Ball.prototype.playerCollision = function (player) {
|
||||
if (intersects(this, player)) {
|
||||
if (this.pos.x < SCREEN_WIDTH/2) {
|
||||
this.bounce(1, 1, player);
|
||||
|
@ -120,8 +155,8 @@ Ball.prototype.checkPlayerCollision = function (player) {
|
|||
}
|
||||
return false;
|
||||
};
|
||||
Ball.prototype.checkCollisions = function () {
|
||||
return this.checkWallsCollision() || this.checkPlayerCollision(player) || this.checkPlayerCollision(ai);
|
||||
Ball.prototype.collisions = function () {
|
||||
return this.wallCollision() || this.playerCollision(player) || this.playerCollision(ai);
|
||||
};
|
||||
Ball.prototype.updatePosition = function () {
|
||||
var elapsed = new Date().getTime() - this.lastUpdate;
|
||||
|
@ -132,31 +167,20 @@ Ball.prototype.updatePosition = function () {
|
|||
Ball.prototype.update = function () {
|
||||
this.updatePosition();
|
||||
this.lastUpdate = new Date().getTime();
|
||||
this.checkCollisions();
|
||||
};
|
||||
Ball.prototype.reset = function() {
|
||||
this.speed = this.originalSpeed;
|
||||
var x = scores[0] < scores[1] || (scores[0] === 0 && scores[1] === 0) ? -this.speed : this.speed;
|
||||
var bounceAngle = Math.PI/6;
|
||||
this.velocity = new Vector(x * Math.cos(bounceAngle), this.speed * -Math.sin(bounceAngle));
|
||||
this.pos = new Vector(SCREEN_WIDTH/2, random(0, SCREEN_WIDTH));
|
||||
};
|
||||
Ball.prototype.restart = function() {
|
||||
ai.pos = new Vector(SCREEN_WIDTH - ai.width*2, SCREEN_WIDTH/2 - ai.height/2);
|
||||
player.pos = new Vector(player.width*2, SCREEN_WIDTH/2 - player.height/2);
|
||||
this.pos = new Vector(SCREEN_WIDTH/2, SCREEN_WIDTH/2);
|
||||
this.collisions();
|
||||
};
|
||||
|
||||
//////////////////////////// Player /////////////////////////////////////////
|
||||
|
||||
function Player() {
|
||||
function Player(right) {
|
||||
this.width = 4;
|
||||
this.height = 30;
|
||||
this.pos = new Vector(this.width*2, SCREEN_WIDTH/2 - this.height/2);
|
||||
this.pos = new Vector(right ? SCREEN_WIDTH-this.width : this.width, SCREEN_WIDTH/2 - this.height/2);
|
||||
this.acc = new Vector(0, 0);
|
||||
this.speed = 15;
|
||||
this.maxSpeed = 25;
|
||||
this.prevPos = null;
|
||||
this.right = right;
|
||||
}
|
||||
Player.prototype.show = function () {
|
||||
if (this.prevPos != null) {
|
||||
|
@ -196,11 +220,14 @@ function AI() {
|
|||
AI.prototype = Object.create(Player.prototype);
|
||||
AI.prototype.constructor = Player;
|
||||
AI.prototype.update = function () {
|
||||
var y = ball.pos.y - (this.height/2 * aiSpeedRandom);
|
||||
var yConstrained = constrain(y, 0, SCREEN_WIDTH-this.height);
|
||||
var y = ball.pos.y - this.height/2;
|
||||
var randomizedY = ball.ballReturn < 3 ? y : y + (aiSpeedRandom * this.height/2);
|
||||
var yConstrained = constrain(randomizedY, 0, SCREEN_WIDTH-this.height);
|
||||
this.pos = new Vector(this.pos.x, yConstrained);
|
||||
};
|
||||
|
||||
/////////////////////////////// Scenes ////////////////////////////////////////
|
||||
|
||||
function net() {
|
||||
var dashSize = 5;
|
||||
for (let y = dashSize/2; y < SCREEN_WIDTH; y += dashSize*2) {
|
||||
|
@ -210,12 +237,6 @@ function net() {
|
|||
}
|
||||
}
|
||||
|
||||
var player = new Player();
|
||||
var ai = new AI();
|
||||
var ball = new Ball();
|
||||
var state = 0;
|
||||
var prevScores = [0, 0];
|
||||
|
||||
function drawScores() {
|
||||
let x1 = SCREEN_WIDTH/4-5;
|
||||
let x2 = SCREEN_WIDTH*3/4-5;
|
||||
|
@ -233,10 +254,80 @@ function drawScores() {
|
|||
|
||||
function drawGameOver() {
|
||||
g.setFont("Vector", 20);
|
||||
g.drawString(winnerMessage, 75, SCREEN_WIDTH/2 - 10);
|
||||
g.drawString(winnerMessage, startOption === 0 ? 55 : 75, SCREEN_WIDTH/2 - 10);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
function showControls(hide) {
|
||||
g.setColor(hide ? 0 : -1);
|
||||
g.setFont("Vector", 8);
|
||||
var topArrowString = `
|
||||
########
|
||||
##
|
||||
## ##
|
||||
### ##
|
||||
### ##
|
||||
###
|
||||
##
|
||||
`;
|
||||
|
||||
var arrows = [Graphics.createImage(topArrowString), Graphics.createImage(`
|
||||
##
|
||||
##
|
||||
####################
|
||||
##
|
||||
##
|
||||
`), Graphics.createImage(topArrowString.split('\n').reverse().join('\n'))
|
||||
];
|
||||
|
||||
g.drawString('UP', 170, 50);
|
||||
g.drawImage(arrows[0], 200, 40);
|
||||
g.drawString('DOWN', 156, 120);
|
||||
g.drawImage(arrows[1], 200, 120);
|
||||
g.drawString('START', 152, 190);
|
||||
g.drawImage(arrows[2], 200, 200);
|
||||
}
|
||||
|
||||
function drawStartScreen(hide) {
|
||||
g.setColor(hide ? 0 : -1);
|
||||
g.setFont("Vector", 10);
|
||||
g.drawString("1 PLAYER", 95, 80);
|
||||
g.drawString("2 PLAYERS", 95, 110);
|
||||
|
||||
const ball1 = new Ball();
|
||||
ball1.prevPos = null;
|
||||
ball1.pos = new Vector(87, 86);
|
||||
ball1.show(hide || !(startOption === 0));
|
||||
|
||||
const ball2 = new Ball();
|
||||
ball2.prevPos = null;
|
||||
ball2.pos = new Vector(87, 116);
|
||||
ball2.show(hide || !(startOption === 1));
|
||||
}
|
||||
|
||||
function drawStartTimer(count, callback) {
|
||||
setTimeout(_ => {
|
||||
player.show();
|
||||
ai.show();
|
||||
net();
|
||||
g.setColor(0);
|
||||
g.fillRect(117-7, 115-7, 117+14, 115+14);
|
||||
if (count >= 0) {
|
||||
g.setFont("Vector", 10);
|
||||
g.drawString(count+1, 115, 115);
|
||||
g.setColor(-1);
|
||||
g.drawString(count === 0 ? 'Go!' : count, 115 - (count === 0 ? 4: 0), 115);
|
||||
drawStartTimer(count - 1, callback);
|
||||
} else {
|
||||
g.setColor(0);
|
||||
g.fillRect(117-7, 115-7, 117+14, 115+14);
|
||||
callback();
|
||||
}
|
||||
}, 800);
|
||||
}
|
||||
|
||||
//////////////////////////////// Main /////////////////////////////////////////
|
||||
|
||||
function onFrame() {
|
||||
if (state === 1) {
|
||||
ball.update();
|
||||
player.update();
|
||||
|
@ -261,22 +352,73 @@ function draw() {
|
|||
drawScores();
|
||||
}
|
||||
|
||||
function startThatGame() {
|
||||
player.show();
|
||||
ai.show();
|
||||
net();
|
||||
drawScores();
|
||||
drawStartTimer(3, () => setInterval(onFrame, 1000 / FPS));
|
||||
}
|
||||
|
||||
var player = new Player();
|
||||
var ai;
|
||||
var ball = new Ball();
|
||||
var state = 0;
|
||||
var prevScores = [0, 0];
|
||||
var playerBle = null;
|
||||
var startOption = 0;
|
||||
|
||||
g.clear();
|
||||
g.setColor(0);
|
||||
g.fillRect(0,0,240,240);
|
||||
showControls();
|
||||
setTimeout(() => {
|
||||
showControls(true);
|
||||
drawStartScreen();
|
||||
}, 2000);
|
||||
|
||||
setInterval(draw, 1000 / FPS);
|
||||
////////////////////////////// Controls ///////////////////////////////////////
|
||||
|
||||
setWatch(o => o.state ? player.up() : player.stop(), BTN1, {repeat: true, edge: 'both'});
|
||||
setWatch(o => o.state ? player.down() : player.stop(), BTN3, {repeat: true, edge: 'both'});
|
||||
//setWatch(o => o.state ? player.down() : player.stop(), BTN5, {repeat: true, edge: 'both'});
|
||||
setWatch(o => {
|
||||
if (state === 0) {
|
||||
if (o.state) {
|
||||
startOption = startOption === 0 ? startOption : startOption - 1;
|
||||
drawStartScreen();
|
||||
}
|
||||
} else o.state ? player.up() : player.stop();
|
||||
}, BTN1, {repeat: true, edge: 'both'});
|
||||
setWatch(o => {
|
||||
if (state === 0) {
|
||||
if (o.state) {
|
||||
startOption = startOption === 1 ? startOption : startOption + 1;
|
||||
drawStartScreen();
|
||||
}
|
||||
} else o.state ? player.down() : player.stop();
|
||||
}, BTN2, {repeat: true, edge: 'both'});
|
||||
setWatch(o => {
|
||||
state++;
|
||||
clearInterval();
|
||||
if (state >= 2) {
|
||||
ball.restart();
|
||||
g.setColor(0);
|
||||
g.fillRect(0,0,240,240);
|
||||
g.fillRect(0, 0, 240, 240);
|
||||
ball.show(true);
|
||||
scores = [0, 0];
|
||||
playerBle = null;
|
||||
ball = new Ball();
|
||||
state = 1;
|
||||
startThatGame();
|
||||
} else {
|
||||
drawStartScreen(true);
|
||||
showControls(true);
|
||||
if (startOption === 1) {
|
||||
ai = new Player(true);
|
||||
startThatGame();
|
||||
} else {
|
||||
ai = new AI();
|
||||
startThatGame();
|
||||
}
|
||||
}
|
||||
}, BTN2, {repeat: true});
|
||||
}, BTN3, {repeat: true});
|
||||
|
||||
setWatch(o => startOption === 1 && (o.state ? ai.up() : ai.stop()), BTN4, {repeat: true, edge: 'both'});
|
||||
setWatch(o => startOption === 1 && (o.state ? ai.down() : ai.stop()), BTN5, {repeat: true, edge: 'both'});
|
||||
|
|
|
@ -20,3 +20,4 @@
|
|||
0.16: Reduce memory usage further when running app settings page
|
||||
0.17: Remove need for "settings" in appid.info
|
||||
0.18: Don't overwrite existing settings on app update
|
||||
0.19: Allow BLE HID settings, add README.md
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# Settings
|
||||
|
||||
This is Bangle.js's settings menu
|
||||
|
||||
* **Make Connectable** regardless of the current Bluetooth settings, makes Bangle.js so you can connect to it (while the window is up)
|
||||
* **App/Widget Settings** settings specific to installed applications
|
||||
* **BLE** is Bluetooth LE enabled and the watch connectable?
|
||||
* **Programmable** if BLE is on, can the watch be connected to in order to program/upload apps?
|
||||
* **Debug Info** should debug info be shown on the watch's screen or not?
|
||||
* **Beep** most Bangle.js do not have a speaker inside, but they can use the vibration motor to beep in different pitches. You can change the behaviour here to use a Piezo speaker if one is connected
|
||||
* **Vibration** enable/disable the vibration motor
|
||||
* **Locale** set time zone/whether the clock is 12/24 hour (for supported clocks)
|
||||
* **Select Clock** if you have more than one clock face, select the default one
|
||||
* **HID** When Bluetooth is enabled, Bangle.js can appear as a Bluetooth Keyboard/Joystick/etc to send keypresses to a connected device. **Note:** on some platforms enabling HID can cause you problems when trying to connect to Bangle.js to upload apps.
|
||||
* **Set Time** Configure the current time - Note that this can be done much more easily by choosing 'Set Time' from the App Loader
|
||||
* **LCD** Configure settings about the screen. How long it stays on, how bright it is, and when it turns on.
|
||||
* **Reset Settings** Reset the settings to defaults
|
||||
* **Turn Off** Turn Bangle.js off
|
|
@ -61,6 +61,8 @@ const boolFormat = v => v ? "On" : "Off";
|
|||
function showMainMenu() {
|
||||
var beepV = [false, true, "vib"];
|
||||
var beepN = ["Off", "Piezo", "Vibrate"];
|
||||
var hidV = [false, "kbmedia", "kb", "joy"];
|
||||
var hidN = ["Off", "Kbrd & Media", "Kbrd","Joystick"];
|
||||
const mainmenu = {
|
||||
'': { 'title': 'Settings' },
|
||||
'Make Connectable': ()=>makeConnectable(),
|
||||
|
@ -115,10 +117,11 @@ function showMainMenu() {
|
|||
'Locale': ()=>showLocaleMenu(),
|
||||
'Select Clock': ()=>showClockMenu(),
|
||||
'HID': {
|
||||
value: settings.HID,
|
||||
format: boolFormat,
|
||||
onchange: () => {
|
||||
settings.HID = !settings.HID;
|
||||
value: 0 | hidV.indexOf(settings.HID),
|
||||
min: 0, max: 3,
|
||||
format: v => hidN[v],
|
||||
onchange: v => {
|
||||
settings.HID = hidV[v];
|
||||
updateSettings();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -138,6 +138,14 @@
|
|||
<button class="btn" id="removeall">Remove all Apps</button>
|
||||
<button class="btn" id="installdefault">Install default apps</button>
|
||||
<button class="btn" id="installfavourite">Install favourite apps</button></p>
|
||||
<h3>Settings</h3>
|
||||
<div class="form-group">
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" id="settings-pretokenise">
|
||||
<i class="form-icon"></i> Pretokenise apps before upload (smaller, faster apps)
|
||||
</label>
|
||||
<button class="btn" id="defaultsettings">Default settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -156,6 +164,7 @@
|
|||
<script src="js/comms.js"></script>
|
||||
<script src="js/appinfo.js"></script>
|
||||
<script src="js/index.js"></script>
|
||||
<script src="js/espruinotools.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="js/pwa.js" defer></script>
|
||||
</body>
|
||||
|
|
|
@ -11,6 +11,16 @@ var AppInfo = {
|
|||
return Promise.resolve(storageFile);
|
||||
else if (storageFile.url)
|
||||
return fileGetter(`apps/${app.id}/${storageFile.url}`).then(content => {
|
||||
if (storageFile.url.endsWith(".js") && !storageFile.url.endsWith(".min.js")) { // if original file ends in '.js'...
|
||||
return Espruino.transform(content, {
|
||||
SET_TIME_ON_WRITE : false,
|
||||
PRETOKENISE : SETTINGS.pretokenise,
|
||||
//MINIFICATION_LEVEL : "ESPRIMA", // disable due to https://github.com/espruino/BangleApps/pull/355#issuecomment-620124162
|
||||
builtinModules : "Flash,Storage,heatshrink,tensorflow,locale"
|
||||
});
|
||||
} else
|
||||
return content;
|
||||
}).then(content => {
|
||||
return {
|
||||
name : storageFile.name,
|
||||
content : content,
|
||||
|
|
File diff suppressed because it is too large
Load Diff
68
js/index.js
68
js/index.js
|
@ -1,7 +1,11 @@
|
|||
var appJSON = []; // List of apps and info from apps.json
|
||||
var appsInstalled = []; // list of app JSON
|
||||
var files = []; // list of files on Bangle
|
||||
const FAVOURITE = "favouriteapps.json";
|
||||
var DEFAULTSETTINGS = {
|
||||
pretokenise : true,
|
||||
favourites : ["boot","launch","setting"]
|
||||
};
|
||||
var SETTINGS = JSON.parse(JSON.stringify(DEFAULTSETTINGS)); // clone
|
||||
|
||||
httpGet("apps.json").then(apps=>{
|
||||
try {
|
||||
|
@ -143,23 +147,18 @@ function handleAppInterface(app) {
|
|||
});
|
||||
}
|
||||
|
||||
function getAppFavourites() {
|
||||
var f = localStorage.getItem(FAVOURITE);
|
||||
return (f === null) ? ["boot","launch","setting"] : JSON.parse(f);
|
||||
}
|
||||
|
||||
function changeAppFavourite(favourite, app) {
|
||||
var favourites = getAppFavourites();
|
||||
var favourites = SETTINGS.favourites;
|
||||
if (favourite) {
|
||||
favourites = favourites.concat([app.id]);
|
||||
SETTINGS.favourites = SETTINGS.favourites.concat([app.id]);
|
||||
} else {
|
||||
if ([ "boot","setting"].includes(app.id)) {
|
||||
showToast(app.name + ' is required, can\'t remove it' , 'warning');
|
||||
}else {
|
||||
favourites = favourites.filter(e => e != app.id);
|
||||
SETTINGS.favourites = SETTINGS.favourites.filter(e => e != app.id);
|
||||
}
|
||||
}
|
||||
localStorage.setItem(FAVOURITE, JSON.stringify(favourites));
|
||||
saveSettings();
|
||||
refreshLibrary();
|
||||
}
|
||||
|
||||
|
@ -192,7 +191,7 @@ function refreshFilter(){
|
|||
function refreshLibrary() {
|
||||
var panelbody = document.querySelector("#librarycontainer .panel-body");
|
||||
var visibleApps = appJSON;
|
||||
var favourites = getAppFavourites();
|
||||
var favourites = SETTINGS.favourites;
|
||||
|
||||
if (activeFilter) {
|
||||
if ( activeFilter == "favourites" ) {
|
||||
|
@ -590,6 +589,49 @@ 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.';
|
||||
}
|
||||
|
||||
// Settings
|
||||
var SETTINGS_HOOKS = {}; // stuff to get called when a setting is loaded
|
||||
/// Load settings and update controls
|
||||
function loadSettings() {
|
||||
var j = localStorage.getItem("settings");
|
||||
if (typeof j != "string") return;
|
||||
try {
|
||||
var s = JSON.parse(j);
|
||||
Object.keys(s).forEach( k => {
|
||||
SETTINGS[k]=s[k];
|
||||
if (SETTINGS_HOOKS[k]) SETTINGS_HOOKS[k]();
|
||||
} );
|
||||
} catch (e) {
|
||||
console.error("Invalid settings");
|
||||
}
|
||||
}
|
||||
/// Save settings
|
||||
function saveSettings() {
|
||||
localStorage.setItem("settings", JSON.stringify(SETTINGS));
|
||||
console.log("Changed settings", SETTINGS);
|
||||
}
|
||||
// Link in settings DOM elements
|
||||
function settingsCheckbox(id, name) {
|
||||
var setting = document.getElementById(id);
|
||||
function update() {
|
||||
setting.checked = SETTINGS[name];
|
||||
}
|
||||
SETTINGS_HOOKS[name] = update;
|
||||
setting.addEventListener('click', function() {
|
||||
SETTINGS[name] = setting.checked;
|
||||
saveSettings();
|
||||
});
|
||||
}
|
||||
settingsCheckbox("settings-pretokenise", "pretokenise");
|
||||
loadSettings();
|
||||
|
||||
document.getElementById("defaultsettings").addEventListener("click",event=>{
|
||||
SETTINGS = JSON.parse(JSON.stringify(DEFAULTSETTINGS)); // clone
|
||||
saveSettings();
|
||||
loadSettings(); // update all settings
|
||||
refreshLibrary(); // favourites were in settings
|
||||
});
|
||||
|
||||
document.getElementById("settime").addEventListener("click",event=>{
|
||||
Comms.setTime().then(()=>{
|
||||
showToast("Time set successfully","success");
|
||||
|
@ -620,9 +662,9 @@ document.getElementById("installdefault").addEventListener("click",event=>{
|
|||
});
|
||||
});
|
||||
|
||||
// Install all favoutrie apps in one go
|
||||
// Install all favourite apps in one go
|
||||
document.getElementById("installfavourite").addEventListener("click",event=>{
|
||||
var favApps = getAppFavourites();
|
||||
var favApps = SETTINGS.favourites;
|
||||
installMultipleApps(favApps, "favourite").catch(err=>{
|
||||
Progress.hide({sticky:true});
|
||||
showToast("App Install failed, "+err,"error");
|
||||
|
|
Loading…
Reference in New Issue