1
0
Fork 0

Merge branch 'master' of github.com:espruino/BangleApps

master
Gordon Williams 2022-12-19 12:23:42 +00:00
commit 1f1b8a20d5
10 changed files with 109 additions and 282 deletions

View File

@ -1,3 +1,9 @@
0.01: New App! 0.01: New App!
0.02: Support Bangle.js 2 0.02: Support Bangle.js 2
0.03: Fix bug for Bangle.js 2 where g.flip was not being called. 0.03: Fix bug for Bangle.js 2 where g.flip was not being called.
0.04: Combine code for both apps
Better colors for Bangle.js 2
Fix selection animation for Bangle.js 2
New icon
Slightly wider arc segments for better visibility
Extract arc drawing code in library

View File

@ -11,16 +11,21 @@ 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 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. 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, You can use Choozi to randomly select an element from any set with 2 to 15 members,
as long as you can define a bijection between members of the set and coloured as long as you can define a bijection between members of the set and coloured
segments on the Bangle.js display. segments on the Bangle.js display.
## Controls ## Controls Bangle 1
BTN1: increase the number of segments BTN1: increase the number of segments
BTN2: choose a segment at random BTN2: choose a segment at random
BTN3: decrease the number of segments BTN3: decrease the number of segments
## Controls Bangle 2
Swipe up/down: increase/decrease the number of segments
BTN1 or tap: choose a segment at random
## Creator ## Creator
James Stanley James Stanley

View File

@ -1 +1 @@
require("heatshrink").decompress(atob("mEwggLIrnM4uqAAIhPgvMAAPFzIABzWgCxkMCweqC4QABDBYtC5QVFDBoWCCo5KLOQIWKDARFICxhJIFwOpC5owFFyAwGUYIuOGAwuRC4guSJAgXBCyIwDIyQXF5IXSzJeVMAReUAAOQhheTMAVcC6yOUC4aOUC7GZUyoXXzWqhQXVxGqC9mYC7OqC9eoxEKC6uBC6uIwAXBPCSmBwEAC6Z2BiAXBJCR2BgEAjQXSlGBC4JgSLwYABJCJGBLwJIDGB+IIwRIDGByNBIwZIDGBhdBRoQwSLoIuFGAYYKCwIuGGAgYI1QWBRgYYJMYmaFoSMEAAyrBAAgVCCxgYGjAWQAAMBC4UILZQA==")) require("heatshrink").decompress(atob("mEwwcH/4AW/u27dt2wQL/YOBCIXbv4QI+AODAQVsh4RHwEbCI0LCI9gCIOANAXbsFbG437tkDPg1btoRFFoILBgmSpMggECHQO/CAf2CIVJkgRBAQIjC24RFsECCItIgIRFMYMAiQRFpMAlqmDVwPYgAOEAQUggu274RD4BWCCIskCIPbCIPt20ABwwCCwARFgIRJyEWCIVt2EJCJi2BCJmSUgIRCwARNt/7CIIOICI1sWAwCFoFbCOtt8EACJsAgARR8hwBCJlJk4RlgARQAgIRKDwMn/gRBdJgRPyARBn4RBpARLiQRB/4RBgIRJwAREpIRLAYP///ypMgCJMACI0ECI4JCp4RB/wZECIsAAYN/CIP/5JPDCIhjDCIraHTIWTCAX//K7DCI+fCIf/EZA1CCAn//ipCLIsBk4RF/5ZHCIIQG//wPo8vCI//6QRFpYQIAAPpCIeXCBQAC/VfBI4="))

View File

@ -4,15 +4,16 @@
* *
* James Stanley 2021 * James Stanley 2021
*/ */
const GU = require("graphics_utils");
var colours = ['#ff0000', '#ff8080', '#00ff00', '#80ff80', '#0000ff', '#8080ff', '#ffff00', '#00ffff', '#ff00ff', '#ff8000', '#ff0080', '#8000ff', '#0080ff']; var colours = ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#00ffff', '#ff00ff', '#ffffff'];
var colours2 = ['#808080', '#404040', '#000040', '#004000', '#400000', '#ff8000', '#804000', '#4000c0'];
var stepAngle = 0.18; // radians - resolution of polygon var stepAngle = 0.18; // radians - resolution of polygon
var gapAngle = 0.035; // radians - gap between segments var gapAngle = 0.035; // radians - gap between segments
var perimMin = 110; // px - min. radius of perimeter var perimMin = g.getWidth()*0.40; // px - min. radius of perimeter
var perimMax = 120; // px - max. radius of perimeter var perimMax = g.getWidth()*0.49; // px - max. radius of perimeter
var segmentMax = 106; // px - max radius of filled-in segment var segmentMax = g.getWidth()*0.38; // px - max radius of filled-in segment
var segmentStep = 5; // px - step size of segment fill animation var segmentStep = 5; // px - step size of segment fill animation
var circleStep = 4; // px - step size of circle fill animation var circleStep = 4; // px - step size of circle fill animation
@ -22,10 +23,10 @@ var minSpeed = 0.001; // rad/sec
var animStartSteps = 300; // how many steps before it can start slowing? var animStartSteps = 300; // how many steps before it can start slowing?
var accel = 0.0002; // rad/sec/sec - acc-/deceleration rate var accel = 0.0002; // rad/sec/sec - acc-/deceleration rate
var ballSize = 3; // px - ball radius var ballSize = 3; // px - ball radius
var ballTrack = 100; // px - radius of ball path var ballTrack = perimMin - ballSize*2; // px - radius of ball path
var centreX = 120; // px - centre of screen var centreX = g.getWidth()*0.5; // px - centre of screen
var centreY = 120; // px - centre of screen var centreY = g.getWidth()*0.5; // px - centre of screen
var fontSize = 50; // px var fontSize = 50; // px
@ -33,7 +34,6 @@ var radians = 2*Math.PI; // radians per circle
var defaultN = 3; // default value for N var defaultN = 3; // default value for N
var minN = 2; var minN = 2;
var maxN = colours.length;
var N; var N;
var arclen; var arclen;
@ -51,42 +51,14 @@ function shuffle (array) {
} }
} }
// 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 // draw the arc segments around the perimeter
function drawPerimeter() { function drawPerimeter() {
g.setBgColor('#000000');
g.clear(); g.clear();
for (var i = 0; i < N; i++) { for (var i = 0; i < N; i++) {
g.setColor(colours[i%colours.length]); g.setColor(colours[i%colours.length]);
var minAngle = (i/N)*radians; var minAngle = (i/N)*radians;
arc(perimMin,perimMax,minAngle,minAngle+arclen); GU.fillArc(g, centreX, centreY, perimMin,perimMax,minAngle,minAngle+arclen, stepAngle);
} }
} }
@ -131,6 +103,7 @@ function animateChoice(target) {
g.fillCircle(x, y, ballSize); g.fillCircle(x, y, ballSize);
oldx=x; oldx=x;
oldy=y; oldy=y;
if (process.env.HWVERSION == 2) g.flip();
} }
} }
@ -141,11 +114,15 @@ function choose() {
var maxAngle = minAngle + arclen; var maxAngle = minAngle + arclen;
animateChoice((minAngle+maxAngle)/2); animateChoice((minAngle+maxAngle)/2);
g.setColor(colours[chosen%colours.length]); g.setColor(colours[chosen%colours.length]);
for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep) for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep){
arc(i, perimMax, minAngle, maxAngle); GU.fillArc(g, centreX, centreY, i, perimMax, minAngle, maxAngle, stepAngle);
arc(0, perimMax, minAngle, maxAngle); if (process.env.HWVERSION == 2) g.flip();
for (var r = 1; r < segmentMax; r += circleStep) }
GU.fillArc(g, centreX, centreY, 0, perimMax, minAngle, maxAngle, stepAngle);
for (var r = 1; r < segmentMax; r += circleStep){
g.fillCircle(centreX,centreY,r); g.fillCircle(centreX,centreY,r);
if (process.env.HWVERSION == 2) g.flip();
}
g.fillCircle(centreX,centreY,segmentMax); g.fillCircle(centreX,centreY,segmentMax);
} }
@ -171,38 +148,47 @@ function setN(n) {
drawPerimeter(); drawPerimeter();
} }
// save N to choozi.txt // save N to choozi.save
function writeN() { function writeN() {
var file = require("Storage").open("choozi.txt","w"); var savedN = read();
file.write(N); if (savedN != N) require("Storage").write("choozi.save","" + N);
} }
// load N from choozi.txt function read(){
var n = require("Storage").read("choozi.save");
if (n !== undefined) return parseInt(n);
return defaultN;
}
// load N from choozi.save
function readN() { function readN() {
var file = require("Storage").open("choozi.txt","r"); setN(read());
var n = file.readLine();
if (n !== undefined) setN(parseInt(n));
else setN(defaultN);
} }
shuffle(colours); // is this really best? if (process.env.HWVERSION == 1){
Bangle.setLCDMode("direct"); colours=colours.concat(colours2);
Bangle.setLCDTimeout(0); // keep screen on shuffle(colours);
} else {
shuffle(colours);
shuffle(colours2);
colours=colours.concat(colours2);
}
var maxN = colours.length;
if (process.env.HWVERSION == 1){
Bangle.setLCDMode("direct");
Bangle.setLCDTimeout(0); // keep screen on
}
readN(); readN();
drawN(); drawN();
setWatch(() => { Bangle.setUI("updown", (v)=>{
setN(N+1); if (!v){
drawN();
}, BTN1, {repeat:true});
setWatch(() => {
writeN(); writeN();
drawPerimeter(); drawPerimeter();
choose(); choose();
}, BTN2, {repeat:true}); } else {
setN(N-v);
setWatch(() => {
setN(N-1);
drawN(); drawN();
}, BTN3, {repeat:true}); }
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 551 B

View File

@ -1,207 +0,0 @@
/* 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 = 80; // px - min. radius of perimeter
var perimMax = 87; // px - max. radius of perimeter
var segmentMax = 70; // 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 = 75; // px - radius of ball path
var centreX = 88; // px - centre of screen
var centreY = 88; // 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;
g.flip();
}
}
// 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(g.theme.fg);
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.setLCDTimeout(0); // keep screen on
readN();
drawN();
setWatch(() => {
writeN();
drawPerimeter();
choose();
}, BTN1, {repeat:true});
Bangle.on('touch', function(zone,e) {
if(e.x>+88){
setN(N-1);
drawN();
}else{
setN(N+1);
drawN();
}
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -1,7 +1,7 @@
{ {
"id": "choozi", "id": "choozi",
"name": "Choozi", "name": "Choozi",
"version": "0.03", "version": "0.04",
"description": "Choose people or things at random using Bangle.js.", "description": "Choose people or things at random using Bangle.js.",
"icon": "app.png", "icon": "app.png",
"tags": "tool", "tags": "tool",
@ -10,8 +10,10 @@
"allow_emulator": true, "allow_emulator": true,
"screenshots": [{"url":"bangle1-choozi-screenshot1.png"},{"url":"bangle1-choozi-screenshot2.png"}], "screenshots": [{"url":"bangle1-choozi-screenshot1.png"},{"url":"bangle1-choozi-screenshot2.png"}],
"storage": [ "storage": [
{"name":"choozi.app.js","url":"app.js","supports": ["BANGLEJS"]}, {"name":"choozi.app.js","url":"app.js"},
{"name":"choozi.app.js","url":"appb2.js","supports": ["BANGLEJS2"]},
{"name":"choozi.img","url":"app-icon.js","evaluate":true} {"name":"choozi.img","url":"app-icon.js","evaluate":true}
],
"data": [
{"name":"choozi.save"}
] ]
} }

35
modules/graphics_utils.js Normal file
View File

@ -0,0 +1,35 @@
// draw an arc between radii minR and maxR, and between angles minAngle and maxAngle centered at X,Y. All angles are radians.
exports.fillArc = function(graphics, X, Y, minR, maxR, minAngle, maxAngle, stepAngle) {
var step = stepAngle || 0.2;
var angle = minAngle;
var inside = [];
var outside = [];
var c, s;
while (angle < maxAngle) {
c = Math.cos(angle);
s = Math.sin(angle);
inside.push(X+c*minR); // x
inside.push(Y+s*minR); // y
// outside coordinates are built up in reverse order
outside.unshift(Y+s*maxR); // y
outside.unshift(X+c*maxR); // x
angle += step;
}
c = Math.cos(maxAngle);
s = Math.sin(maxAngle);
inside.push(X+c*minR);
inside.push(Y+s*minR);
outside.unshift(Y+s*maxR);
outside.unshift(X+c*maxR);
var vertices = inside.concat(outside);
graphics.fillPoly(vertices, true);
}
exports.degreesToRadians = function(degrees){
return Math.PI/180 * degrees;
}
exports.radiansToDegrees = function(radians){
return 180/Math.PI * degrees;
}