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.02: Support Bangle.js 2
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
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
segments on the Bangle.js display.
## Controls
## Controls Bangle 1
BTN1: increase the number of segments
BTN2: choose a segment at random
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
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
*/
var colours = ['#ff0000', '#ff8080', '#00ff00', '#80ff80', '#0000ff', '#8080ff', '#ffff00', '#00ffff', '#ff00ff', '#ff8000', '#ff0080', '#8000ff', '#0080ff'];
const GU = require("graphics_utils");
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 gapAngle = 0.035; // radians - gap between segments
var perimMin = 110; // px - min. radius of perimeter
var perimMax = 120; // px - max. radius of perimeter
var perimMin = g.getWidth()*0.40; // px - min. 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 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 accel = 0.0002; // rad/sec/sec - acc-/deceleration rate
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 centreY = 120; // px - centre of screen
var centreX = g.getWidth()*0.5; // px - centre of screen
var centreY = g.getWidth()*0.5; // px - centre of screen
var fontSize = 50; // px
@ -33,7 +34,6 @@ 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;
@ -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
function drawPerimeter() {
g.setBgColor('#000000');
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);
GU.fillArc(g, centreX, centreY, perimMin,perimMax,minAngle,minAngle+arclen, stepAngle);
}
}
@ -131,6 +103,7 @@ function animateChoice(target) {
g.fillCircle(x, y, ballSize);
oldx=x;
oldy=y;
if (process.env.HWVERSION == 2) g.flip();
}
}
@ -141,11 +114,15 @@ function choose() {
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)
for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep){
GU.fillArc(g, centreX, centreY, i, perimMax, minAngle, maxAngle, stepAngle);
if (process.env.HWVERSION == 2) g.flip();
}
GU.fillArc(g, centreX, centreY, 0, perimMax, minAngle, maxAngle, stepAngle);
for (var r = 1; r < segmentMax; r += circleStep){
g.fillCircle(centreX,centreY,r);
if (process.env.HWVERSION == 2) g.flip();
}
g.fillCircle(centreX,centreY,segmentMax);
}
@ -171,38 +148,47 @@ function setN(n) {
drawPerimeter();
}
// save N to choozi.txt
// save N to choozi.save
function writeN() {
var file = require("Storage").open("choozi.txt","w");
file.write(N);
var savedN = read();
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() {
var file = require("Storage").open("choozi.txt","r");
var n = file.readLine();
if (n !== undefined) setN(parseInt(n));
else setN(defaultN);
setN(read());
}
shuffle(colours); // is this really best?
Bangle.setLCDMode("direct");
Bangle.setLCDTimeout(0); // keep screen on
if (process.env.HWVERSION == 1){
colours=colours.concat(colours2);
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();
drawN();
setWatch(() => {
setN(N+1);
drawN();
}, BTN1, {repeat:true});
setWatch(() => {
writeN();
drawPerimeter();
choose();
}, BTN2, {repeat:true});
setWatch(() => {
setN(N-1);
drawN();
}, BTN3, {repeat:true});
Bangle.setUI("updown", (v)=>{
if (!v){
writeN();
drawPerimeter();
choose();
} else {
setN(N-v);
drawN();
}
});

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",
"name": "Choozi",
"version": "0.03",
"version": "0.04",
"description": "Choose people or things at random using Bangle.js.",
"icon": "app.png",
"tags": "tool",
@ -10,8 +10,10 @@
"allow_emulator": true,
"screenshots": [{"url":"bangle1-choozi-screenshot1.png"},{"url":"bangle1-choozi-screenshot2.png"}],
"storage": [
{"name":"choozi.app.js","url":"app.js","supports": ["BANGLEJS"]},
{"name":"choozi.app.js","url":"appb2.js","supports": ["BANGLEJS2"]},
{"name":"choozi.app.js","url":"app.js"},
{"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;
}