1
0
Fork 0

Merge branch 'rescalc' of https://github.com/stweedo/BangleApps into rescalc

master
stweedo 2023-06-12 10:18:32 -05:00
commit 9d77c5ecfe
123 changed files with 5097 additions and 721 deletions

View File

@ -1 +1,3 @@
0.01: New App! 0.01: New App!
0.02: Increased Legibility, GUI rework
0.03: 13 new chords

View File

@ -4,7 +4,8 @@ An app that simply describes finger placements on a Ukulele to form common chord
## Usage ## Usage
Use the button to scroll through the available chords. Select a chord to view.
Use the button to return to the chord selection menu.
## Creator ## Creator

View File

@ -16,52 +16,12 @@ const cc = [
const dd = [ const dd = [
"D", "D",
"22",
"23", "23",
"22",
"24", "24",
"x" "x"
]; ];
const gg = [
"G",
"x",
"21",
"33",
"22",
];
const am = [
"Am",
"22",
"x",
"x",
"x"
];
const em = [
"Em",
"x",
"43",
"32",
"21"
];
const aa = [
"A",
"22",
"11",
"x",
"x"
];
const ff = [
"F",
"22",
"x",
"11",
"x"
];
var ee = [ var ee = [
"E", "E",
"33", "33",
@ -70,14 +30,187 @@ var ee = [
"11" "11"
]; ];
const ff = [
"F",
"22",
"x",
"11",
"x"
];
const gg = [
"G",
"x",
"21",
"33",
"22",
];
const aa = [
"A",
"22",
"11",
"x",
"x"
];
const bb = [
"B",
"42",
"43",
"44",
"21"
];
const cm = [
"Cm",
"11",
"x",
"12",
"34"
];
const dm = [
"Dm",
"x",
"22",
"33",
"11"
];
const em = [
"Em",
"x",
"43",
"32",
"21"
];
const fm = [
"Fm",
"33",
"11",
"11",
"11"
];
const gm = [
"Gm",
"x",
"22",
"33",
"11"
];
const am = [
"Am",
"22",
"23",
"11",
"x"
];
const bm = [
"Bm",
"x",
"43",
"32",
"21"
];
const c7 = [
"C7",
"22",
"33",
"11",
"x"
];
const d7 = [
"D7",
"x",
"22",
"11",
"23"
];
const e7 = [
"E7",
"x",
"11",
"x",
"x"
];
const f7 = [
"F7",
"11",
"22",
"11",
"11"
];
const g7 = [
"G7",
"x",
"x",
"x",
"11"
];
const a7 = [
"A7",
"21",
"21",
"21",
"32"
];
const b7 = [
"B7",
"11",
"22",
"x",
"23"
];
var index = 0; var index = 0;
var chords = []; var chords = [];
var menu = {
function init() { "" : { "title" : "Uke Chords" },
g.setFontAlign(0,0); // center font "C" : function() { draw(cc); },
g.setFont("6x8",2); // bitmap font, 8x magnified "D" : function() { draw(dd); },
chords.push(cc, dd, gg, am, em, aa, ff, ee); "E" : function() { draw(ee); },
"F" : function() { draw(ff); },
"G" : function() { draw(gg); },
"A" : function() { draw(aa); },
"B" : function() { draw(bb); },
"C7" : function() { draw(c7); },
"D7" : function() { draw(d7); },
"E7" : function() { draw(e7); },
"F7" : function() { draw(f7); },
"G7" : function() { draw(g7); },
"A7" : function() { draw(a7); },
"B7" : function() { draw(b7); },
"Cm" : function() { draw(cm); },
"Dm" : function() { draw(dm); },
"Em" : function() { draw(em); },
"Fm" : function() { draw(fm); },
"Gm" : function() { draw(gm); },
"Am" : function() { draw(am); },
"Bm" : function() { draw(bm); },
"About" : function() {
E.showMessage(
"Created By:\nNovaDawn999", {
title:"About"
} }
);
}
};
function drawBase() { function drawBase() {
for (let i = 0; i < 4; i++) { for (let i = 0; i < 4; i++) {
@ -87,18 +220,18 @@ function drawBase() {
} }
function drawChord(chord) { function drawChord(chord) {
g.drawString(chord[0], g.getWidth() * 0.5 + 2, 18); g.drawString(chord[0], g.getWidth() * 0.5 - (chord[0].length * 5), 16);
for (let i = 0; i < chord.length; i++) { for (let i = 0; i < chord.length; i++) {
if (i === 0 || chord[i][0] === "x") { if (i === 0 || chord[i][0] === "x") {
continue; continue;
} }
if (chord[i][0] === "0") { if (chord[i][0] === "0") {
g.drawString(chord[i][1], x + (i - 1) * stringInterval + 1, y + fretHeight * chord[i][0], true); g.drawString(chord[i][1], x + (i - 1) * stringInterval - 5, y + fretHeight * chord[i][0] + 2, true);
g.drawCircle(x + (i - 1) * stringInterval -1, y + fretHeight * chord[i][0], 8); g.drawCircle(x + (i - 1) * stringInterval -1, y + fretHeight * chord[i][0], 10);
} }
else { else {
g.drawString(chord[i][1], x + (i - 1) * stringInterval + 1, y -fingerOffset + fretHeight * chord[i][0], true); g.drawString(chord[i][1], x + (i - 1) * stringInterval -5, y -fingerOffset + fretHeight * chord[i][0] + 2, true);
g.drawCircle(x + (i - 1) * stringInterval -1, y -fingerOffset + fretHeight * chord[i][0], 8); g.drawCircle(x + (i - 1) * stringInterval -1, y -fingerOffset + fretHeight * chord[i][0], 10);
} }
} }
} }
@ -107,22 +240,19 @@ function buttonPress() {
setWatch(() => { setWatch(() => {
buttonPress(); buttonPress();
}, BTN); }, BTN);
index++; E.showMenu(menu);
if (index >= chords.length) { index = 0; }
draw();
} }
function draw() { function draw(chord) {
g.clear(); g.clear();
drawBase(); drawBase();
drawChord(chords[index]); drawChord(chord);
} }
function main() { function main() {
init(); E.showMenu(menu);
draw();
setWatch(() => { setWatch(() => {
buttonPress(); buttonPress();
}, BTN); }, BTN);

View File

@ -1,7 +1,7 @@
{ "id": "Uke", { "id": "Uke",
"name": "Uke Chords", "name": "Uke Chords",
"shortName":"Uke", "shortName":"Uke",
"version":"0.01", "version":"0.03",
"description": "Wrist mounted ukulele chords", "description": "Wrist mounted ukulele chords",
"icon": "app.png", "icon": "app.png",
"tags": "uke, chords", "tags": "uke, chords",

View File

@ -2,3 +2,4 @@
0.02: Barometer altitude adjustment setting 0.02: Barometer altitude adjustment setting
0.03: Use default Bangle formatter for booleans 0.03: Use default Bangle formatter for booleans
0.04: Add options for units in locale and recording GPS 0.04: Add options for units in locale and recording GPS
0.05: Allow toggling of "max" values (screen tap) and recording (button press)

View File

@ -12,7 +12,7 @@ const fontFactorB2 = 2/3;
const colfg=g.theme.fg, colbg=g.theme.bg; const colfg=g.theme.fg, colbg=g.theme.bg;
const col1=colfg, colUncertain="#88f"; // if (lf.fix) g.setColor(col1); else g.setColor(colUncertain); const col1=colfg, colUncertain="#88f"; // if (lf.fix) g.setColor(col1); else g.setColor(colUncertain);
var altiGPS=0, altiBaro=0; var altiBaro=0;
var hdngGPS=0, hdngCompass=0, calibrateCompass=false; var hdngGPS=0, hdngCompass=0, calibrateCompass=false;
/*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */ /*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */
@ -183,7 +183,6 @@ var KalmanFilter = (function () {
var lf = {fix:0,satellites:0}; var lf = {fix:0,satellites:0};
var showMax = 0; // 1 = display the max values. 0 = display the cur fix var showMax = 0; // 1 = display the max values. 0 = display the cur fix
var canDraw = 1;
var time = ''; // Last time string displayed. Re displayed in background colour to remove before drawing new time. var time = ''; // Last time string displayed. Re displayed in background colour to remove before drawing new time.
var sec; // actual seconds for testing purposes var sec; // actual seconds for testing purposes
@ -194,30 +193,9 @@ max.n = 0; // counter. Only start comparing for max after a certain number of
var emulator = (process.env.BOARD=="EMSCRIPTEN" || process.env.BOARD=="EMSCRIPTEN2")?1:0; // 1 = running in emulator. Supplies test values; var emulator = (process.env.BOARD=="EMSCRIPTEN" || process.env.BOARD=="EMSCRIPTEN2")?1:0; // 1 = running in emulator. Supplies test values;
var wp = {}; // Waypoint to use for distance from cur position.
var SATinView = 0; var SATinView = 0;
function radians(a) {
return a*Math.PI/180;
}
function distance(a,b){
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
var y = radians(b.lat-a.lat);
// Distance in selected units
var d = Math.sqrt(x*x + y*y) * 6371000;
d = (d/parseFloat(cfg.dist)).toFixed(2);
if ( d >= 100 ) d = parseFloat(d).toFixed(1);
if ( d >= 1000 ) d = parseFloat(d).toFixed(0);
return d;
}
function drawFix(dat) { function drawFix(dat) {
if (!canDraw) return;
g.clearRect(0,screenYstart,screenW,screenH); g.clearRect(0,screenYstart,screenW,screenH);
var v = ''; var v = '';
@ -227,7 +205,7 @@ function drawFix(dat) {
v = (cfg.primSpd)?dat.speed.toString():dat.alt.toString(); v = (cfg.primSpd)?dat.speed.toString():dat.alt.toString();
// Primary Units // Primary Units
u = (cfg.primSpd)?cfg.spd_unit:dat.alt_units; u = (showMax ? 'max ' : '') + (cfg.primSpd?cfg.spd_unit:dat.alt_units);
drawPrimary(v,u); drawPrimary(v,u);
@ -260,14 +238,6 @@ function drawFix(dat) {
} }
function drawClock() {
if (!canDraw) return;
g.clearRect(0,screenYstart,screenW,screenH);
drawTime();
g.reset();
}
function drawPrimary(n,u) { function drawPrimary(n,u) {
//if(emulator)console.log("\n1: " + n +" "+ u); //if(emulator)console.log("\n1: " + n +" "+ u);
var s=40; // Font size var s=40; // Font size
@ -337,16 +307,6 @@ function drawSats(sats) {
g.setFont("6x8", 2); g.setFont("6x8", 2);
g.setFontAlign(1,1); //right, bottom g.setFontAlign(1,1); //right, bottom
g.drawString(sats,screenW,screenH); g.drawString(sats,screenW,screenH);
g.setFontVector(18);
g.setColor(col1);
if ( cfg.modeA == 1 ) {
if ( showMax ) {
g.setFontAlign(0,1); //centre, bottom
g.drawString('MAX',120,164);
}
}
} }
function onGPS(fix) { function onGPS(fix) {
@ -367,7 +327,6 @@ function onGPS(fix) {
var sp = '---'; var sp = '---';
var al = '---'; var al = '---';
var di = '---';
var age = '---'; var age = '---';
if (fix.fix) lf = fix; if (fix.fix) lf = fix;
@ -412,10 +371,6 @@ function onGPS(fix) {
al = Math.round(parseFloat(al)/parseFloat(cfg.alt)); al = Math.round(parseFloat(al)/parseFloat(cfg.alt));
if (parseFloat(al) > parseFloat(max.alt) && max.n > 15 ) max.alt = parseFloat(al); if (parseFloat(al) > parseFloat(max.alt) && max.n > 15 ) max.alt = parseFloat(al);
// Distance to waypoint
di = distance(lf,wp);
if (isNaN(di)) di = 0;
// Age of last fix (secs) // Age of last fix (secs)
age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000)); age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000));
} else { } else {
@ -448,15 +403,7 @@ function onGPS(fix) {
} }
} }
function setButtons(){
setWatch(_=>load(), BTN1);
onGPS(lf);
}
function updateClock() { function updateClock() {
if (!canDraw) return;
drawTime(); drawTime();
g.reset(); g.reset();
@ -545,6 +492,10 @@ function Compass_reading() {
hdngCompass = Compass_heading.toFixed(0); hdngCompass = Compass_heading.toFixed(0);
} }
function nextMode() {
showMax = 1 - showMax;
}
function start() { function start() {
Bangle.setBarometerPower(1); // needs some time... Bangle.setBarometerPower(1); // needs some time...
g.clearRect(0,screenYstart,screenW,screenH); g.clearRect(0,screenYstart,screenW,screenH);
@ -556,10 +507,30 @@ function start() {
Bangle.setCompassPower(1); Bangle.setCompassPower(1);
if (!calibrateCompass) setInterval(Compass_reading,200); if (!calibrateCompass) setInterval(Compass_reading,200);
setButtons();
if (emulator) setInterval(updateClock, 2000); if (emulator) setInterval(updateClock, 2000);
else setInterval(updateClock, 10000); else setInterval(updateClock, 10000);
let createdRecording = false;
Bangle.setUI({
mode: "custom",
touch: nextMode,
btn: () => {
const rec = WIDGETS["recorder"];
if(rec){
const active = rec.isRecording();
if(active){
createdRecording = true;
rec.setRecording(false);
}else{
rec.setRecording(true, { force: createdRecording ? "append" : "new" });
}
}else{
nextMode();
}
},
});
// can't delay loadWidgets til here - need to have already done so for recorder
Bangle.drawWidgets(); Bangle.drawWidgets();
} }
@ -571,6 +542,7 @@ if (cfg.record && WIDGETS["recorder"]) {
if (cfg.recordStopOnExit) if (cfg.recordStopOnExit)
E.on('kill', () => WIDGETS["recorder"].setRecording(false)); E.on('kill', () => WIDGETS["recorder"].setRecording(false));
} else { } else {
start(); start();
} }

View File

@ -2,7 +2,7 @@
"id": "bikespeedo", "id": "bikespeedo",
"name": "Bike Speedometer (beta)", "name": "Bike Speedometer (beta)",
"shortName": "Bike Speedometer", "shortName": "Bike Speedometer",
"version": "0.04", "version": "0.05",
"description": "Shows GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude from internal sources", "description": "Shows GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude from internal sources",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"Screenshot.png"}], "screenshots": [{"url":"Screenshot.png"}],

View File

@ -66,3 +66,4 @@
If settings.bootDebug is set, output timing for each section of .boot0 If settings.bootDebug is set, output timing for each section of .boot0
0.56: Settings.log = 0,1,2,3 for off,display, log, both 0.56: Settings.log = 0,1,2,3 for off,display, log, both
0.57: Handle the whitelist being disabled 0.57: Handle the whitelist being disabled
0.58: "Make Connectable" temporarily bypasses the whitelist

View File

@ -79,7 +79,7 @@ if (global.save) boot += `global.save = function() { throw new Error("You can't
if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`; if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`;
if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`; if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`;
if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`; if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`;
if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`; if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!NRF.ignoreWhitelist && !(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`;
if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation
// ================================================== FIXING OLDER FIRMWARES // ================================================== FIXING OLDER FIRMWARES
if (FWVERSION<215.068) // 2v15.68 and before had compass heading inverted. if (FWVERSION<215.068) // 2v15.68 and before had compass heading inverted.

View File

@ -1,7 +1,7 @@
{ {
"id": "boot", "id": "boot",
"name": "Bootloader", "name": "Bootloader",
"version": "0.57", "version": "0.58",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png", "icon": "bootloader.png",
"type": "bootloader", "type": "bootloader",

View File

@ -0,0 +1,2 @@
0.01: Initial release.
0.02: Added compatibility to OpenTracks and added HRM Location

View File

@ -0,0 +1,16 @@
# BLE GATT HRM Service
Adds the GATT HRM Service to advertise the current HRM over Bluetooth.
## Usage
This boot code runs in the background and has no user interface.
## Creator
[Another Stranger](https://github.com/anotherstranger)
## Aknowledgements
Special thanks to [Jonathan Jefferies](https://github.com/jjok) for creating the
bootgattbat app, which was the inspiration for this App!

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

70
apps/bootgatthrm/boot.js Normal file
View File

@ -0,0 +1,70 @@
(() => {
function setupHRMAdvertising() {
/*
* This function prepares BLE heart rate Advertisement.
*/
NRF.setAdvertising(
{
0x180d: undefined
},
{
// We need custom Advertisement settings for Apps like OpenTracks
connectable: true,
discoverable: true,
scannable: true,
whenConnected: true,
}
);
NRF.setServices({
0x180D: { // heart_rate
0x2A37: { // heart_rate_measurement
notify: true,
value: [0x06, 0],
},
0x2A38: { // Sensor Location: Wrist
value: 0x02,
}
}
});
}
function updateBLEHeartRate(hrm) {
/*
* Send updated heart rate measurement via BLE
*/
if (hrm === undefined || hrm.confidence < 50) return;
try {
NRF.updateServices({
0x180D: {
0x2A37: {
value: [0x06, hrm.bpm],
notify: true
},
0x2A38: {
value: 0x02,
}
}
});
} catch (error) {
if (error.message.includes("BLE restart")) {
/*
* BLE has to restart after service setup.
*/
NRF.disconnect();
}
else if (error.message.includes("UUID 0x2a37")) {
/*
* Setup service if it wasn't setup correctly for some reason
*/
setupHRMAdvertising();
} else {
console.log("[bootgatthrm]: Unexpected error occured while updating HRM over BLE! Error: " + error.message);
}
}
}
setupHRMAdvertising();
Bangle.on("HRM", function (hrm) { updateBLEHeartRate(hrm); });
})();

View File

@ -0,0 +1,15 @@
{
"id": "bootgatthrm",
"name": "BLE GATT HRM Service",
"shortName": "BLE HRM Service",
"version": "0.02",
"description": "Adds the GATT HRM Service to advertise the measured HRM over Bluetooth.\n",
"icon": "bluetooth.png",
"type": "bootloader",
"tags": "hrm,health,ble,bluetooth,gatt",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"gatthrm.boot.js","url":"boot.js"}
]
}

View File

@ -1,2 +1,3 @@
0.01: Initial Creation 0.01: Initial Creation
0.02: Fixed some sleep bugs. Added a sleep mode toggle 0.02: Fixed some sleep bugs. Added a sleep mode toggle
0.03: Reduce busy-loop and code

View File

@ -1,8 +1,8 @@
{ {
"id": "chimer", "id": "chimer",
"name": "Chimer", "name": "Chimer",
"version": "0.02", "version": "0.03",
"description": "A fork of Hour Chime that adds extra features such as: \n - Buzz or beep on every 60, 30 or 15 minutes. \n - Reapeat Chime up to 3 times \n - Set hours to disable chime", "description": "A fork of Hour Chime that adds extra features such as: \n - Buzz or beep on every 60, 30 or 15 minutes. \n - Repeat Chime up to 3 times \n - Set hours to disable chime",
"icon": "widget.png", "icon": "widget.png",
"type": "widget", "type": "widget",
"tags": "widget", "tags": "widget",

View File

@ -16,16 +16,10 @@
var settings = readSettings(); var settings = readSettings();
function sleep(milliseconds) {
const date = Date.now();
let currentDate = null;
do {
currentDate = Date.now();
} while (currentDate - date < milliseconds);
}
function chime() { function chime() {
for (var i = 0; i < settings.repeat; i++) { let count = settings.repeat;
const chime1 = () => {
if (settings.type === 1) { if (settings.type === 1) {
Bangle.buzz(100); Bangle.buzz(100);
} else if (settings.type === 2) { } else if (settings.type === 2) {
@ -33,8 +27,24 @@
} else { } else {
return; return;
} }
sleep(150); if (--count > 0)
setTimeout(chime1, 150);
};
chime1();
} }
function queueNextCheckMins(mins) {
const now = new Date(),
m = now.getMinutes(),
s = now.getSeconds(),
ms = now.getMilliseconds();
const mLeft = mins - (m + mins * 2) % mins,
sLeft = mLeft * 60 - s,
msLeft = sLeft * 1000 - ms;
setTimeout(check, msLeft);
} }
let lastHour = new Date().getHours(); let lastHour = new Date().getHours();
@ -45,88 +55,41 @@
m = now.getMinutes(), m = now.getMinutes(),
s = now.getSeconds(), s = now.getSeconds(),
ms = now.getMilliseconds(); ms = now.getMilliseconds();
if ( if (settings.sleep && (
(settings.sleep && h > settings.end) || h > settings.end ||
(settings.sleep && h >= settings.end && m !== 0) || (h >= settings.end && m !== 0) ||
(settings.sleep && h < settings.start) h < settings.start)
) { ) {
var mLeft = 60 - m, queueNextCheckMins(60);
sLeft = mLeft * 60 - s,
msLeft = sLeft * 1000 - ms;
setTimeout(check, msLeft);
return; return;
} }
if (settings.freq === 1) { switch (settings.freq) {
if ((m !== lastMinute && m === 0) || (m !== lastMinute && m === 30)) case 1:
if (m !== lastMinute && m % 30 === 0)
chime(); chime();
lastHour = h; lastHour = h;
lastMinute = m; lastMinute = m;
// check again in 30 minutes queueNextCheckMins(30);
switch (true) {
case m / 30 >= 1:
var mLeft = 30 - (m - 30),
sLeft = mLeft * 60 - s,
msLeft = sLeft * 1000 - ms;
break; break;
case m / 30 < 1: case 2:
var mLeft = 30 - m, if (m !== lastMinute && m % 15 === 0)
sLeft = mLeft * 60 - s,
msLeft = sLeft * 1000 - ms;
break;
}
setTimeout(check, msLeft);
} else if (settings.freq === 2) {
if (
(m !== lastMinute && m === 0) ||
(m !== lastMinute && m === 15) ||
(m !== lastMinute && m === 30) ||
(m !== lastMinute && m === 45)
)
chime(); chime();
lastHour = h; lastHour = h;
lastMinute = m; lastMinute = m;
// check again in 15 minutes queueNextCheckMins(15);
switch (true) {
case m / 15 >= 3:
var mLeft = 15 - (m - 45),
sLeft = mLeft * 60 - s,
msLeft = sLeft * 1000 - ms;
break; break;
case m / 15 >= 2: case 3:
var mLeft = 15 - (m - 30), // unreachable - not available in settings
sLeft = mLeft * 60 - s,
msLeft = sLeft * 1000 - ms;
break;
case m / 15 >= 1:
var mLeft = 15 - (m - 15),
sLeft = mLeft * 60 - s,
msLeft = sLeft * 1000 - ms;
break;
case m / 15 < 1:
var mLeft = 15 - m,
sLeft = mLeft * 60 - s,
msLeft = sLeft * 1000 - ms;
break;
}
setTimeout(check, msLeft);
} else if (settings.freq === 3) {
if (m !== lastMinute) chime(); if (m !== lastMinute) chime();
lastHour = h; lastHour = h;
lastMinute = m; lastMinute = m;
// check again in 1 minute queueNextCheckMins(1);
break;
var mLeft = 1, default:
sLeft = mLeft * 60 - s,
msLeft = sLeft * 1000 - ms;
setTimeout(check, msLeft);
} else {
if (h !== lastHour && m === 0) chime(); if (h !== lastHour && m === 0) chime();
lastHour = h; lastHour = h;
// check again in 60 minutes queueNextCheckMins(60);
var mLeft = 60 - m, break;
sLeft = mLeft * 60 - s,
msLeft = sLeft * 1000 - ms;
setTimeout(check, msLeft);
} }
} }

2
apps/guitar/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: More Chords, formatting, fret offset support.

12
apps/guitar/README.md Normal file
View File

@ -0,0 +1,12 @@
# Guitar Chords
An app that simply describes finger placements on a Guitar to form common chords.
## Usage
Select a chord to view.
Use the button to return to the chord selection menu.
## Creator
NovaDawn999

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkCkQA/AGMkoQXVptEFytEogwUCoIYBLqlUGIIXTopHVknUoXULylNpouUIoKmUUi0hoMUailEiMSCR/d7pdECx8tC4IYBolULqAWC7qLSFwfdiKMRC4dEoK6RFwYWBppdW7vSLiPd6gXPConVgIWCYYYtM9vdosUqgXOFwndilBqoGDLh/eqtEioXR9xHCoIXDO5SKEpvU6kVppeQ73kqgwB7wuNOwosEXqSlB9xFNR49RpwXV6pICIxhIF73ePAIXTAAgXOJApePGBQXPGA4XPGAxeOGBAWQDAouRDAgWUAH4AZ"))

340
apps/guitar/app.js Normal file
View File

@ -0,0 +1,340 @@
const stringInterval = 24;
const stringLength = 138;
const fretHeight = 35;
const fingerOffset = 17;
const xOffset = 26;
const yOffset = 34;
const cc = [
"C",
"0X",
"33",
"22",
"x",
"11",
"x",
"0"
];
const dd = [
"D",
"0X",
"0X",
"x",
"21",
"33",
"22",
"0"
];
const gg = [
"G",
"32",
"21",
"x",
"x",
"x",
"33",
"0"
];
const am = [
"Am",
"0x",
"x",
"23",
"22",
"11",
"x",
"0"
];
const em = [
"Em",
"x",
"22",
"23",
"x",
"x",
"x",
"0"
];
const aa = [
"A",
"0X",
"x",
"21",
"22",
"23",
"x",
"0"
];
var ee = [
"E",
"x",
"22",
"23",
"11",
"x",
"x",
"0"
];
var dm = [
"Dm",
"0x",
"0x",
"x",
"22",
"33",
"11",
"0"
];
var ff = [
"F",
"0x",
"0x",
"33",
"22",
"11",
"11",
"0"
];
var b7 = [
"B7",
"0x",
"22",
"11",
"23",
"x",
"24",
"0"
];
var cadd9 = [
"Cadd9",
"0x",
"32",
"21",
"x",
"33",
"34",
"0"
];
var dadd11 = [
"Dadd11",
"0x",
"33",
"22",
"x",
"11",
"x",
"3"
];
var csus2 = [
"Csus2",
"0x",
"33",
"x",
"x",
"11",
"0x",
"0"
];
var gadd9 = [
"Gadd9",
"32",
"0x",
"x",
"21",
"x",
"33",
"0"
];
var aadd9 = [
"Aadd9",
"11",
"33",
"34",
"22",
"x",
"x",
"5"
];
var fsharp7add11 = [
"F#7add11",
"21",
"43",
"44",
"32",
"x",
"x",
"0"
];
var d9 = [
"D9",
"0x",
"22",
"11",
"23",
"23",
"0x",
"4"
];
var g7 = [
"G7",
"33",
"22",
"x",
"x",
"34",
"11",
"0"
];
var bflatd = [
"Bb/D",
"0x",
"33",
"11",
"11",
"11",
"0x",
"3"
];
var e7sharp9 = [
"E7#9",
"0x",
"22",
"11",
"23",
"34",
"0x",
"6"
];
var a11 = [
"A11 3rd fret",
"33",
"0x",
"34",
"22",
"11",
"0x",
"0"
];
var a9 = [
"A9",
"32",
"0x",
"33",
"21",
"34",
"0x",
"3"
];
var index = 0;
var chords = [];
var menu = {
"" : {
"title" : "Guitar Chords"
},
"C" : function() { draw(cc); },
"D" : function() { draw(dd); },
"E" : function() { draw(ee); },
"Em" : function() { draw(em); },
"A" : function() { draw(aa); },
"Am" : function() { draw(am); },
"F" : function() { draw(ff); },
"G" : function() { draw(gg); },
"Dm" : function() { draw(dm); },
"B7" : function () { draw(b7); },
"Cadd9" : function () { draw(cadd9); },
"Dadd11" : function () { draw(dadd11); },
"Csus2" : function () { draw(csus2); },
"Gadd9" : function () { draw(gadd9); },
"Aadd9" : function () { draw(aadd9); },
"F#7add11" : function () { draw(fsharp7add11); },
"D9" : function () { draw(d9); },
"G7" : function () { draw(g7); },
"Bb/D" : function () { draw(bflatd); },
"E7#9" : function () { draw(e7sharp9); },
"A11" : function () { draw(a11); },
"A9" : function () { draw(a9); },
"About" : function() {
E.showMessage(
"Created By:\nNovaDawn999", {
title:"About"
}
);
}
};
function drawBase() {
for (let i = 0; i < 6; i++) {
g.drawLine(xOffset + i * stringInterval, yOffset, xOffset + i * stringInterval, yOffset + stringLength);
g.fillRect(xOffset- 1, yOffset + i * fretHeight - 1, xOffset + stringInterval * 5 + 1, yOffset + i * fretHeight + 1);
}
}
function drawChord(chord) {
g.drawString(chord[0], g.getWidth() * 0.5 - (chord[0].length * 5), 16);
for (let i = 0; i < chord.length - 1; i++) {
if (i === 0 || chord[i][0] === "x") {
continue;
}
if (chord[i][0] === "0") {
g.drawString(chord[i][1], xOffset + (i - 1) * stringInterval - 5, yOffset + fretHeight * chord[i][0] + 2, true);
g.drawCircle(xOffset + (i - 1) * stringInterval -1, yOffset + fretHeight * chord[i][0], 10);
}
else {
g.drawString(chord[i][1], xOffset + (i - 1) * stringInterval -5, yOffset -fingerOffset + fretHeight * chord[i][0] + 2, true);
g.drawCircle(xOffset + (i - 1) * stringInterval -1, yOffset -fingerOffset + fretHeight * chord[i][0], 10);
}
}
if (chord[7] !== "0") {
g.drawString(chord[7], 9, 50);
}
}
function buttonPress() {
setWatch(() => {
buttonPress();
}, BTN);
E.showMenu(menu);
}
function draw(chord) {
g.clear();
drawBase();
drawChord(chord);
}
function main() {
E.showMenu(menu);
setWatch(() => {
buttonPress();
}, BTN);
}
main();

BIN
apps/guitar/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

14
apps/guitar/metadata.json Normal file
View File

@ -0,0 +1,14 @@
{ "id": "guitar",
"name": "Guitar Chords",
"shortName":"Guitar",
"version":"0.02",
"description": "Wrist mounted guitar chords",
"icon": "app.png",
"tags": "guitar, chords",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"guitar.app.js","url":"app.js"},
{"name":"guitar.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -6,3 +6,4 @@
0.06: Fixing refresh issues 0.06: Fixing refresh issues
0.07: Fixed position after unlocking 0.07: Fixed position after unlocking
0.08: Handling exceptions 0.08: Handling exceptions
0.09: Add option for showing battery high mark

View File

@ -8,6 +8,8 @@ Show the current battery level and charging status in the top right of the clock
* Blue when charging * Blue when charging
* 40 pixels wide * 40 pixels wide
The high-level marker (a little bar at the 100% point) can be toggled in settings.
![](a_battery_widget-pic.jpg) ![](a_battery_widget-pic.jpg)
## Creator ## Creator

View File

@ -3,7 +3,7 @@
"name": "A Battery Widget (with percentage) - Hanks Mod", "name": "A Battery Widget (with percentage) - Hanks Mod",
"shortName":"H Battery Widget", "shortName":"H Battery Widget",
"icon": "widget.png", "icon": "widget.png",
"version":"0.08", "version":"0.09",
"type": "widget", "type": "widget",
"supports": ["BANGLEJS", "BANGLEJS2"], "supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md", "readme": "README.md",

View File

@ -1,7 +1,6 @@
(function(){ (function(){
const intervalLow = 60000; // update time when not charging const intervalLow = 60000; // update time when not charging
const intervalHigh = 2000; // update time when charging const intervalHigh = 2000; // update time when charging
var old_l;
var old_x = this.x; var old_x = this.x;
var old_y = this.y; var old_y = this.y;
@ -22,33 +21,18 @@
}; };
function draw() { function draw() {
if (typeof old_x === 'undefined') old_x = this.x; var s = width - 1;
if (typeof old_y === 'undefined') old_y = this.y;
var s = 29;
var x = this.x; var x = this.x;
var y = this.y; var y = this.y;
if ((typeof x === 'undefined') || (typeof y === 'undefined')) { if ((typeof x === 'undefined') || (typeof y === 'undefined')) {
} else { } else {
g.clearRect(old_x, old_y, old_x + width, old_y + height);
const l = E.getBattery(); // debug: Math.floor(Math.random() * 101); const l = E.getBattery(); // debug: Math.floor(Math.random() * 101);
let xl = x+4+l*(s-12)/100; let xl = x+4+l*(s-12)/100;
if ((l != old_l) && (typeof old_l != 'undefined') ){ // Delete the old value from screen
let xl_old = x+4+old_l*(s-12)/100;
g.setColor(COLORS.white);
// g.fillRect(x+2,y+5,x+s-6,y+18);
g.fillRect(x,y,xl+4,y+16+3); //Clear
g.setFontAlign(0,0);
g.setFont('Vector',16);
//g.fillRect(old_x,old_y,old_x+4+l*(s-12)/100,old_y+16+3); // clear (lazy)
g.drawString(old_l, old_x + 14, old_y + 10);
g.fillRect(x+4,y+14+3,xl_old,y+16+3); // charging bar
}
old_l = l;
//console.log(old_x);
g.setColor(levelColor(l)); g.setColor(levelColor(l));
g.fillRect(x+4,y+14+3,xl,y+16+3); // charging bar g.fillRect(x+4,y+14+3,xl,y+16+3); // charging bar
g.fillRect((x+4+100*(s-12)/100)-1,y+14+3,x+4+100*(s-12)/100,y+16+3); // charging bar "full mark"
// Show percentage // Show percentage
g.setColor(COLORS.black); g.setColor(COLORS.black);
g.setFontAlign(0,0); g.setFontAlign(0,0);
@ -65,6 +49,8 @@
Bangle.on('charging',function(charging) { draw(); }); Bangle.on('charging',function(charging) { draw(); });
var id = setInterval(()=>WIDGETS["hwid_a_battery_widget"].draw(), intervalLow); var id = setInterval(()=>WIDGETS["hwid_a_battery_widget"].draw(), intervalLow);
var width = 30;
var height = 19;
WIDGETS["hwid_a_battery_widget"]={area:"tr",width:30,draw:draw}; WIDGETS["hwid_a_battery_widget"]={area:"tr",width,draw:draw};
})(); })();

View File

@ -5,3 +5,7 @@
0.05: Tell clock widgets to hide 0.05: Tell clock widgets to hide
0.06: Fix exception when showing missing hiragana 'WO' 0.06: Fix exception when showing missing hiragana 'WO'
0.07: Fix regression in bitmap selection on some code paths 0.07: Fix regression in bitmap selection on some code paths
0.08: Speedup next/prev and fix autogenerated hiragana bitmaps
0.09: Optimize loading and rendering times, introduce transition animations
0.10: Swipe up/down for Hiragana/Katakana, right/left for next/prev letter
0.11: Sort by 'AIUEO' instead of 'AEIOU', draw Widgets every minute :?

View File

@ -1,19 +1,25 @@
# kanawatch # kanawatch
A simple watchface design with hiragana and katakana A simple watchface design perfect for learning hiragana and katakana.
cards for learning.
## Changelog * Interact with the interface using swipes
* Swipe up/down to switch between hiragana (H) and katakana (K)
* Swipe right/left to display the next or previous letter
* Tap to change accent color (always 24h, not configurable)
* Non-intrustive transition animations
* Low battery consumption
0.01: First release ## TODO
0.02: Improve battery life, sprite resolution, fix launcher issue and unaligned text bug
0.03: Reduce code size, refresh once a minute and faster refresh * Only render what needs to be repainted
0.04: Show a random kana every minute to improve learning * Dont redraw the widgets if not necessary
* Minigame to guess kata/hira phonem
## Author ## Author
Written by pancake in 2022, powered by insomnia Written by pancake in 2022, maintained during 2023 and powered by insomnia
## Screenshots ## Screenshots
![hiragana and katakana](screenshot.png) ![katakana](screenshot.png)
![hiragana ](screenshot2.png)

View File

@ -3,182 +3,214 @@ const stripe_pos = 40;
const stripe2_pos = 110; const stripe2_pos = 110;
const h = g.getHeight(); const h = g.getHeight();
const w = g.getWidth(); const w = g.getWidth();
const decompress = require("heatshrink").decompress;
/// /////////////////////////////////////////
const katakana = {}; function benchStart() {
const hiragana = {}; return {
now : +Date.now(),
diff: function() {
return (0+Date.now()) - this.now;
}
};
}
const startupTime = benchStart();
function image(x,y,b) { function image(x,y,b) {
return { return {
bpp:1, width:x,height:y, bpp:1, width:x,height:y,
buffer:require('heatshrink').decompress(atob(b)) buffer: decompress(atob(b)),
}; };
} }
katakana['A'] = image(56, 51, "v//AAfwAon//AGF/wGT/gGM/A3F/BDEn/wJQoGCj4RB//gAxUB//AAwcDAwsH/+AAwcP/4tCAwMf/wGEn/8Awl/JYYGBKQkf/I9DAwJgBGwQGDGwRlBAwJsE+42DAwPzGwYGB+J7EQIIvDQIIFEAw5DEAwRDDgCIEAxCPBKIcAR4IhER4hnCLAg9BLAgoBAwgoBcQiCBMwj0BHogGBHogGBfoooEQQREFEIgGBAokAhAGFA=");
katakana['I'] = image(54, 55, "AAkEAws+AokB/wGEg//Awk//gTE//gAwcPCYt/CYkDCYsfCYv//A0F4A0ECYg0BCYggBCYn/KwhBBGgl/EAgtBEAgMBEAZOBEAgMBEAYZB/+ABggTDBgQnDAoIaDJoIaDFgIABDQQFC74aBBgX8v4aBEwWBDQQgB/EHDQQ6BwEfGoX/+AJBDQMDWAKMBDQMPAQIaDiBFCPAgaDU4hrDDQiuDDX4acSAIaCA=");
katakana['U'] = image(52, 55, "AAMP/gGE//ABlH/AAnvAon+Bk5EDv/vIgcHBkHPBgZwBBgn/Bi8B/+PBgcf/AMFw/wBgYEDgED/6qEv4MEKYK3F8AFDj7EED4LREv/4CQn/wASEFginBDAgfEDAIfDn67BC4YABH4QXBCQcHZoQkEEoYMCHAYlBFYZEBLwk/MgpQEAAw");
katakana['E'] = image(58, 45, "h//AAfwgYGE/0AAwn/wE/AwngDgv4DjhDCv/wJQkf/gGEg//AwkB//AA4gc/Dn4cjbAv/34GF94GF/YGF/wcjwA=");
katakana['O'] = image(57, 54, "AAcf+AGEh/8AwkH/wGEgf/AwkB/+AA4n/4AGEv/gAwk/GIsf/A4P/4AE+F/Awn4n4GE/kfAwn+h4cFg4GFwYGF4IGFKwYFBMQpxFAwJxEAwJxEAwJxEAwJxEK4JxEAwKqEMoQGE/o4En/8HAl//iqEAwKqEv/+VQgNBVQgNBcYgNBcYhLBcYhSCHAQKBAwI4CAwY4CD4IGBHASxBAYI4CAwY4CYwIGBHAQGBD4I4CBIJfCHASmDHAV/PYQ4Cj5QCHAUPLwQ4CgQGCOIgABOIgABHAIGEAAY=");
katakana['KA'] = image(54, 54, "AAMP/AGEv/gAocB/+AAwcH/wTEj4arg//AAf+j4GE/F/AwnhAon/w4aZHAMP/hTEn/wKYn/4BTDgf/KYgQCDQYQCBIQQDBIQQCBIc/DQouCDQQuCEghJBEhITBH4RTBLoRTEBIJTGCAUPNwoTCDQQWBDoIuCj4TCJIX/CYQ/BZQInBH4U//0HwBTBGgPwXAXwh4PBXAXAv4PCZIIgBEYTJBn5SBDQXABAIzBCYJcCDQXwgbOCAwIDBQgI4CgEOJwIADkAGFA");
katakana['KI'] = image(58, 55, "AAU+Awv/4AGEn/wAwkP/gGEgf/Dkk/CAc//4ABwAGBj4GC8ATBAAf4h4GE/woBAAmAAwvgFAYcIwAcD/BFDFARFD/kBIoYACv5FBAAcfRL94DgkfHgf/95EBD4RgDD4MHLwf8AogAd+CPFGwiJCS4XHJgSGB8CJEkCJJUwYABg5pDD4amTNwKmXYbgcDLoY=");
katakana['KU'] = image(55, 55, "AAMHwAGEh/8Awkf/AGEv/wAwn/4AFDgf/EQkH/whF/4ACAwM/AoQQCBgY5BgIGDHIMHAwY5Bh4GD8AhEIAQFDIAIhBBIJACEIJpEj45CNIV/NgRpBDQIrBEoPgDQJlBEoQaDEoV/RwUP/wPBQ4Uf/gPBQ4QsBKAKSD8BvCSQXDDQYYBNYIaCGYIqBDQU//kPXoYYBj5QCEIPgj60DKoMcWga7FKoYABKogaDbojPBbojMDGob/ECYJBCbgYaDE4IaEPoIaDEAI1EbYQZECYgtBCZQGCLol/KwxxEAwJqEgIMFgIZEgA=");
katakana['KE'] = image(60, 54, "AAMcAwsD/4HFn/wBxl/8AGEg/+BxkP/gOF//ABxcB/+AA4kf/BCGAAZOBv4HEIQIOGAwgOBh4OFGYIOFn4OFEgoOBAwvgh52BKgYDBOwJUDv5nBBwY6BAYM/BwIKBJgJjBBQSbCWoQVBRgK1D/4oDBwJJBWos/WIS1CgIVCJoRGBWowCCj61HYgpRCdIjEGLgTLEIwTLEfAv/GYqtBEghyBGYjoCAwwkDAwQVEYwYjEHQt/CopeBQgQOEIIgOBPgxeFgZ7FA");
katakana['KO'] = image(49, 46, "v//AAYFF34FE74FE94FE+4FE/IFE/gFE/w0Dgf/AocB/+AAwf/4BHE8AFDn/wAocf/AFDh/8AocHGH4w6YZf7Aon9YYoFEejBhEAAIA=");
katakana['SA'] = image(58, 53, "AAcD/wDBg4DC//AgEB/+AgE/+AKBv/ggEP/gGBj/4DgP/DnU//4A34CQ+DAIcEDAIcDDAQDDDAYDCDAYDD/4cDIgJADAAUfIAQACh4jCAAUHD4QACJwIfBAAQtBEYgGBI4QUDFQkP/4qEVYQvEAAIxCEIK5CBwV/AwsfAwocCAwYcCJogcBNIp3F");
katakana['SI'] = image(56, 52, "gFwAwt+Awv/8AGF/gFDgP//4GGCocDAwIVDBoX/wAHCn4VFg4GB4AxEAwsfAworBEQYABv4GFj4DCjgrCBQYRFn/4JQfAIgIGD+F/JQcD/gGBMARQCOwcH/wNBCoUP/0PAwIrBj/8OwQGBn4fBGIIGCAQIlB+BcBAQKvDBIQRB8AfBIQUH4AXBP4RXBGgJmERoJsFAwv//yaFbYghBQIYaCeAi9FPQTZGdxKFCFASECFAZPBEIgNCJQaZEAwhDDAwRJDTAYGEQAiQBPIgAGA");
katakana['SU'] = image(60, 51, "gH/AAYGBh4GD/AOG4AOF/gONDo+ABxAACgY7CAAd/+AGEg4OG//gAwkP/wGEgJCCAAcfKIQzEIQIzEIQozOj4zFEgIzFn4kHGYv/M4okIGYt/IQqXBFghuBHYs/bAY6DCwrJECod/HgYVB8ZLEcoMfLQYECCwYVB+BTBCwT7CCwYrBAYIKCCoQDC8BXBEIQSBNoQVBBYP4EAIoCOQPHCoYTB/xdBIwQ8B+6SET4N/dYn/4aCFFgKRFgC+EgPghivEAoI");
katakana['SE'] = image(57, 53, "gEH/wGEgf/AwkB/+AA4n/4AGEv/gAwk/+AGEj/4AwkP/g4JjA4EBQQ4D/4DD4E/AwIuBv/vAoP/FwILCAAIuBv4GEBgn//wFEAwITEh//CgfwAwMfCIRGB/4BB/5xBAgJTBIQQGBwP/75CBAwOAD4JCBAwRmDDIKYBOIQGDOIQGDOIQbBAwSqBAwiqBAwiqBDYg4Cv4GCHAUfAwQ4Cg4GCHAUBbwbjnHAgADcYYADUYQxEEYq6CVwbDBdQi6CZQYqBAAZcCAwY1BEYi5DAAQ8CegfgA=");
katakana['SO'] = image(52, 52, "gGAAol8AYUD/Ef4AGCn/3/wFCg/+v/wAwV/8//Bgk//AMD8f/FoQMBj/8Bgfg//gBgcPFoYMBFocP/kHFof/4AtDBgMDFoYMBFoYMBgIIBgADBwAtDj4dBHQQMCFoYqCHQQqCFoc/BIIPCCwQtDKYIpBB4IwDIAQwCh45CBIVAFgSmDFIaaDOIYfCVgYfBRYYfCTASTCUoY1BQgZPCD4l/D4kfH4g4BH4YYBH4gFBGQd//4yDBYIyDn4SEJQIlEBgRXEHAg+BFYZRGZYQADBYgAG");
katakana['TA'] = image(55, 56, "AAMHwAGEh/8Awkf/AGEv/gAwn/4AFDgf/EQkH/4oF/4ACAwM/AoX+FAQGCHIMBCYY5BEIIAC+AhFIAIhDHIQFDF4IhBJQMHF4JDDNIUfHIRpCv5sCn/wDQJsCDwIaBEIIKBwEf/9gOAQaB/gbBFAIPB+YsC/AaB54RBFAIaBAIOAEoJvBOgPh/+DNAJWB+//DQPBQIZyBM4f4LQSQC8EPKAIpBFAMPPgKKCgEcYIZwBiAGDbohwEZ4bdEFILxFf4ghBXwLjEDQhLBCYoaEE4IaDdIQaDBgLBCDIRQENYYTIewRkEAwJCFHYicBOIkAEAhDBS4IAJ");
katakana['TI'] = image(57, 54, "AAkGAwsfwAGE//gAocP//wBgn//gEBgIFBAAIeBAof/wAYBAwkHAof+gEDAwf4E4YAB4AGBv4TDAAM/AwoxDKQhABLQwiCAAV/MIglBMIglBHwRwDNARbF//3Awv7Awv9Awv+Awv/MQQAD34GF74GFKAUHOIYABSAJxGaYp4Uv54FP40/P4oGHQwQGKKgt/AwrUEMIQGEVYIGLg4bMFII+Fv5TGNAsPQgsHTIoAG");
katakana['TU'] = image(54, 53, "AAMBwAGEj4FEgf8AYPwgFgn/4BIP/g+Av/ggEP/n/gP/4EAv/v/wQBFQP/z/4CAMAg/+DAMfEIICBDAN/FgN/8YYBBAIaBw4hDDQIVBAYMAn/wDAIhCCwIhDCwIBBwAIBHAIYBEIQYDBAIuBwAjBFQghCJgQhEAIIhDEYQPBh5HBM4IhDQQQhCwYeBCwMBCoSPB/0CIQQhBAQKWDvytBCYTBDv5tBZYYTCAAQTCAAYTFHAITEj4TF/4TEh4TFv4TEg//JgIMDMYIMEO4ImD/53BAAM/AwIsEEAgFBEAZNBIIgTCFocfJwo6BPgpHEgZAEgEOAogAGA==");
katakana['TE'] = image(57, 51, "h//AAfwg4GE/kDAwn+gIGE/8AAwuAv4GE4E/Awngj4GFNWJNF/gGF/5UF/+/AwvfAwvvAwv3Awv7GJn8IQV/4BJEv59Fn/wAwkf/DJFEAYABg/+AwjJBAxbQBwAGFH4gGBH4gGIIwgGNG4IGEg//LYjyBAwiyBAxc/EQoGGFIJTLdYJvEgF+fIsYAwo=");
katakana['TO'] = image(42, 54, "//AAgU/+AECh/8AgUD/4U/CgYPDn//wAUC/4VCCgIlDAgIKCCgIKCCgP//wUD//gCgQKCn/zBQQ+BDYP8CgMBEAQBBj4KBKYIKC54yBBQP7KYIKCG4QKB35YBBQIUCGQPjNAUD+BXDnB9Dgy8/CicAA=");
katakana['MA'] = image(57, 50, "/4AE/l/A4s/AwvfAwoAN/YGF/oxGHokf/wGLh4GN/4GSg4GChgGDwARBAw3gAwv4Awo7BAwn/4ACBAwIKB+AGDgJtBAwcAUgOPAwYLB94GDgaFCAwTBDAwcfAwoyBAwgyBAwgyCAwgcBAwgyBNgL0ENgIADn6oHDijhFW4wcB4AGDKwPwBwl/fwzUJDgZOFgAGGngGFhADCA");
katakana['MI'] = image(52, 53, "gPwAwkf/wFDgf///gAwU/AwIVCBgX//AME//8gEHAoQGCBgYGCv4GDFIMPBggoE4A2CCoIuCAweAAwc/BghYBMwswNw0PNwkBGAIbEG4gMCOoYMCOoQMDAwRnE4BYDKYQTEKYRuCKYY8GgCjDAAV+LAtgcTMDbYhTCHobICBwbBDBghZDZwmAZoYGCAogGBCYgiBEIidCBwQ2DS4QMCVYT2CSAb2DBoLpFn72EdJAA==");
katakana['MU'] = image(59, 54, "AAMDwAHFv/AAwkf/gVF/4VG8AGEh4VHFgoVPFdZBdRogVBgP4CokBFogVBn/wTIkHEwYrCv4ODCoMP/wVDFIP/JYQVCBwgVBGYLICCoTIDCoQCBBwQhCn5RCCoR/DNoZCDDIRRDCoQODg4+CIQYvGCoZCCCoZRDAQV//4SBRAM//4ABwEfAgQAB/ARBAAkPAwvxAwv+Dgv/8YGF/gkD/xCB543DH4P5AoaBBewsAvgGFhgGFAAQ=");
katakana['ME'] = image(55, 54, "AAcB8AGEgf/AwkP/wGEj/8Awk/+AGEv4iF//AFAuAAwcHFAsPFA34AYNwFAQvBgICCFAUHCAIoDDwQoDn4DBKIf/MYIoCDwIGB/5RBAwWDKIYGB456Dv//75RDAwP/JQQmBAwJ6Dj4GBOYYGCOYcP/5zEg//OYgGNDYw3BAwgvBAwaABAwgaBOARZC/wGDOoP8MQI1D+AGDFwPAAwJaBDAQNCJIc/AQJsBTYL3COQc/4ATBXoYdCSgU8J4SNCmCNCNQqoDAwQuBAwgFDFAITEAwK1DAAKZEAAIMFAA4=");
katakana['MO'] = image(55, 49, "j//AAfAv4GFAon/wIGFgYFE/0HAwn8h4GE/AvF8A4Bv4DCAAQzBAocB/+AAwYxBCYkH/wGEh/8MIv4Awk/+AGEGyJfFAFP9AwpOBNuikeAwxfEHoLpFNoZACAwZABIgIACJYYABIAYGCIAYwCHIoABA=");
katakana['NA'] = image(57, 55, "AAV/8AGEn/wAwkf/AGEh/8AwkH/wGEgf/AwkB/+AA4n/4A4rGoIAE/IGF/wGF/9/Awu/AwvfAwvvAwv3AwpQCOOqqEWLV/H4pGGn5GFAw0fJosfJooGGn4GGKgq6BLQoGEg4GFh4GFPoIpEDYIwFv5MFLQ4GFg6EFgaZFAAw");
katakana['NI'] = image(56, 43, "h//AAf4A25+/AH4AuWggA5A=");
katakana['NU'] = image(55, 51, "g//AAcAh4GFj4FD/0An4GD/kAv4GD/EADQnwgIGE8EDAwnAAwuAIIgvBAAcPF4IADn4vBAAd/8AGEFAIDBAQIsBFAMDCAIoDh4eBj4oCj4GBFAd/CIJRBgBZCAQIlD/+HQIIGD54oCNwZKDPQZPDOYRdDOYqmBOYi0BOYjCBBogGGYQSAEAwimDGATdDAwQTBH4JFBLIP8AwYTB+AqBAwITB4AGBE4bADBIJyBUIJ6CVgXgJAQzBg+BAoJkCgxcBCYRIEPArlEH4YGDO4ibBeQs+AokAsAGF");
katakana['NE'] = image(61, 55, "AAX/4AGEg/+Bws/+AGEgP/wAHEh/8Cwt/8AGEgf/Bwsf/AMEAAYnBj4GDHwQOEDAMHA4hVBn4WFJIIADHwMPA4hgCAwZkFCQKCGBwpHBPQwOFFAJyGBwt/BwozBBwpwDGYiYEEgP+iAkF4IPDCoP8j7WCUAXhbwYVB/4RBU4n4QISfD54vBS4f+FASPD+AEB+AFB/IjBFIPnA4LzCGAfAeYIjBGAP4eYQCBwZuBeYUH/EfIwJRCAoIDBg6ACnCmDR4oqBDIKfEHgKuFS4g5CBwo8CWwqOCAAQ8DcYg8Vn48FAAo=");
katakana['NO'] = image(47, 52, "AAcHAokP/gFDj/4Aod/+AFD//gAgUB//AAoUD/4oE/woJn4oLEQYoBwAoIh4oEj4oFJZ8HERU/EQhFEDgIiDH4JFDh4iEH4t/NAYcFHII/Dj4cEv4/DCwIcDCwIcDCwI5DCwhEBHIYQBKwf/GYYhBCwc/FoYKBFoYEBFoQKCE4RrBE4YFCHwQyBHAYnBJ4YFBcBN/AgcAPgYABA=");
katakana['HA'] = image(62, 52, "AAP/wEH/gGCgf/gE/+AHCh4MB//AA4QMBCIQeD4ARCDwv4Dwt/8AeEgI4BDwkH/weFj4eEAgIeF8AeEAgQeEAgQeEAgQeEAgQeGMggeCMggeCQYiACQYYbCDwgbCIogbCIoZZDIoYTCMggTCEwn/CYJFDBYZFDBYYmDv4LBEwYDDg4aCh5JCDQYiDaIQWBNAQ5CMAYLDcgYmCCwgqCGIYTBFwL7EJIIWEAgPgh4WDNAPACwgMBCwiHB/wWEFwV/CwZVB/YWEDgPHXgYuBDwLbDKQPwh60CGwWAngGDgAFBkAHEsAFEAAQA==");
katakana['HI'] = image(47, 51, "//AAgUB/+AAoUD/4QDg/+AocP/gFDj/4Aoc/+AFDv/gFw8BwIuDj+DFwf/FwcP/4uD///FwQKB/wuBJwIFBFwM/AoP8//PAgP/+IDCAAJdBAAXwg4FDEoQKCIIIgCLoQFBKYV//5qDB4aMuF1YFDFwIRDUIQAC+YFE8YFE44FEw4FEUgn+Aon8WwhKBXggA=");
katakana['HU'] = image(49, 50, "/4AEv4FE34FE74FE94FE+4FE/YFE/oFE/w0Dg//AocD/+AAoUB//AI4ngAod/+AFDn4FEj/4Aon8AocPAokHHgg2BHhYFDHgJCLJBZCEAopIFAoxIEAoxOEApc/AojSBbwplEAoZxBAocPAojICBQhBCGYIFDBYRZCa4P/NYQuCPoYFBSoZGFZYsPAgYABA=");
katakana['HE'] = image(61, 43, "AAMH8AHF/4HFh//wAOF/wOG/AHEv4eFg//DwoOBDwgOCDwk//YeEgf/x4eEn/8n4eDgP/4AeEj/8DAIeCBwPgLgkfDYIeECYQeDh4LBIwIeC//wDIIeCBYJdCDwV/BwIwBDwIOBCQYeBn4pCDwRIBIAQeCMIJPD/AOB4CED4BhBMwf/MISbD/kHPovwj4ODDwV/UYhYBKQJ2DRoIGDHQINEcARCCWYgGEDwIOFgb+FDwL2EDwQGFIQoeCBw0YA40AA==");
katakana['HO'] = image(61, 54, "AAV/8AGEgf/Bwsf/AHF//AAwkH/wOFn/wAwkB/+AA4kP/g8Rg//AAngv4HFCYIAE/EfA4vAAwv+Eo3wn4HFwAGFJwZ5UgfAPIJzDn/x/+PEgR/BAoJzDP4N/8JzD//D/6KDFYI8BCwYrCCAItBPQOH/wWDCgIQBCwf/4P/wIWCCQIBDWgYBCZ4KJBE4LPDEYInBh5sBBgKLBNgQ0CJoIWB4ACCBgIiBBwP8EYU/TQLXBHQQECFAI8BCwIqB8DzCDYMPAgQbCMoI3BF4IRB44OBWwQUBv4TBJIV//InBHgQCBw4OBHgUH/EfNgKOCj0A3BsCQwNgeaSdCABA=");
katakana['N'] = image(54, 50, "ggGFngFEgP+AwkPAws/AwkB/4GEh4GFn4Gaj///gNF/AGF4BEJAwITBgOAAwQTBh4GCnwJCCgVwLgRwMHAgTBHAgTGv4TEgYTFMIITEMAsHMBY0B+ClFCYiPFEAITEv//OIQMCTg3gBgggEDIIgDGYIgDMIJVDDAIABIIILCFoYYCJwZ0BHQgsBBgZnBBggnCKgYhBMIi3FgAFFgAA==");
katakana['WA'] = image(51, 50, "/4Ay4A3E/AFCh4GBAoUBAoPgAwU///8AoUHBgOAD4nwAoUf//+AoUDGRYSBGQYSCGQd/94yDh/9GQZFB34yDn/zGQcPAgYSCG4YSBC4YSNv4SKJYJwDLwISEn5QDS4QSDDAJjDDAJ2DGIJ2DUYQ+DQYKcFFYYXBDASOCGIQFDGIQRCDwTaCG4YFBEgbHHN4hiFg6HEA=");
katakana['WO'] = image(50, 52, "/4AE34FE94FE/YFE/wYYGocB/+AAwd/8AFDn/4AocP/gFDgf/KovADAnwDB43B45EE+IFE/F/KAkfBgmHAonhAonwDAn8h4MEN5X/N4l/N4k/KwkfRwgoBDwcHOohoBOoYFBEgY2BEgYFBEgYFBJIYXBFQYpBFQZ3CAoIWCKoQQCGwQLDHgR8CAoQdCAoQvCOYYFFn5gENgKREbYgAGA");
katakana['RA'] = image(51, 50, "n//AAcHAongAon8j4GEwYFE+F/Aof+h4ME4IFE/BYr+4FE/wFE//fAon7BgpYE//vAon9CQo3Ev/gAocP/gFDgP/wASX+ASJgYSFXwJ2ECQivBDAoSEWIs//wFDbYIrDAoI+DAoIYDQ4IYCFIIABDALlDGIJhBewS/EJQQYCG4YkED4QFDD4JJF4AFDA");
katakana['RI'] = image(43, 53, "AAf/7/4AgMf/f/AgMD/9/8AFBv/v/gEBh/9/+AgEB/+/+AKBn/3/wEBg/+//AFX4q3v4qDh/8FQQPBz4PDAYQvBEYQvCEYI/CGYRPBB4cfIYQpBB4cH/5TCDwJjD/4kCn4EBCgN/AgIUBDoP/FIJHBAAIyCDIYjBIYYaBQ4QaBJoZHDAAoA=");
katakana['RU'] = image(61, 53, "AAUH/wHFn/wAgUB/+B/+AA4UP/gBBCgd/8ABBAwUD/4BBBwcf/ABBA4f/4ABBHQg8FHQI8/HksYHgwYBHgkPF4I8EvwlCHwOAg4gBEYI8CCIQjBHgITBCIP+HgU/CwIRBDAIgB4AMCAgMfEAIMBDAIOCBgQYCIwQMCPYJTBAQI8BBwUHEoN/8P/IYN/+AvBj4LBBwOAj/7BwZGB/4ABBwXAAQIODM4QOFHgIOC/4OBh4OCAYJGBv4OCn4OBHgJKBAYJkBIQISBaIYhCCwIOBSoTqBJQISBeYUHd4U+bYUwcAYAKA");
katakana['RE'] = image(51, 51, "//AAocf/AFDgf/CQl/8AFDh/8AocB/+AAwc/+AFDg/+GX4ECgwyEgPgGQk+GQkP+IyDC4IyE//3GQc//gyDh//GQYYB8YyD//4GQc//wyDDAOBGQUH//gGQRvB/BlD/4DBGQU/CwIyCj4YBMoQkBBIIyBBAIYBGQIkBDAIDBGgIiD+AFBGoIyBv4eCGQIABJwQvBAAJnDEgTLCEgY8CIYLLDEgZVCAoZuBb4iaBfAj+EgE4AokAA");
katakana['RO'] = image(50, 47, "/4AEn4FE94FE/YFE/wYF34YS4A1BgIYB+A8Cv/v/gFCj4YBAoUHDH4Y/DEbglDBQ8CAAYA==");
katakana['YU'] = image(59, 46, "gP/AAX+A4M/A4fggEHAwf8BwIGD/4GBj4VFgYVGv4HDwEAh4GD+A+Eg46CAAf/4AGEj/4Coo6CCqJFBCot/KAIADh5QCQAhQBCrM/Myk/M3JQGh5QFMyIRBAH6NB");
katakana['YO'] = image(50, 49, "v//AAefAonnAon5Aon+DDA1DgP/wA8E8AFDj/4AocHDFZjfDCJjxDD5WE/+/AonvAon7PgoYX/g3DAAQ");
hiragana['A'] = image(52, 50, "gEB/wGEn/AAocD/gMcg//AAfgv4FD/wMYFIRNa54HDgYyCBgYsEBgX/+AGBHQYpBCQQaCh4JBJQPwgIdBBAP/wASB4H/j/8MIP8j5fBBIP/4P8gf+j/7/hVBj/jA4PH/C/Bn4RBv8Aj/3/Ef55FB/9/wI+D+/wj40BHwIWBL4QJB+BFBwAmB/4MBD4M/94MBD4JAB/4cBNYN/BgM//AsB/n/z4bBQgOHX4QVB/B3B/CQCAQTSC8BFCB4Q4CB4UAgIIBRQOAXojREn/gaIgAC");
hiragana['I'] = image(58, 50, "v/gAgUggEf/AGCnkAg/+AwU/gEB/+AAwQZBDgcP/gcECQIcFCQIJCCol/4AGBgYLBj/wCokHCAIABFAIQCCon/DgQECn4cDCoItCAAI+BDggVCLoZeB+BgCCocPPQZUBwZdDJAQcEGAIcEGAIcEGQPDDghIBDggyBDggyBx4cBjxIC8aaCCAIyBLAMDM4IyBSARnC//HUIk/+IyBCASdBLAJKCGQOf/kDJQV/GQRKCJ4XgEYRPC/CoCDgOHNwl/8P/84jCDgM//5HCDgMHAwIjBgP8DwIsBQgYVBSQgVBaYZnCTIgtBbQhDCUAYkCfwYOCGIgAHA");
hiragana['U'] = image(46, 50, "h//Aoc////8AFBAgIABgEDAofACwIAB/wWD//4CwgdBCIeAFQUfCwIADCwIAMj//+AEBv4tDAgQLBHAYFBAgf/8YFE54FECwRTB/wkCAoP7IAd/OgR2CKwcBQ4kH/hMEJYQcC4AWIh4WEn4tJg6EEj6EEVgIQDE4l/CAbABCAZqBBQgQDBQIQCXwIyCYYTIFeIhlCBQjxCLIQWBMgbdFvzYJ");
hiragana['E'] = image(55, 50, "gF//4GE/4AB+AFBgIGC/+AgEDAwYNBg4FC/wGBh4GC/gGF/ArFFIQAD4BRVn42FLAIGEJQYGBLAhEBLAhEBLAf/8ArDBIIyEj5fCRYZYEEgJYEN4JNFDQouFDQKcBFwYGFMIIGDLQRJFAwgaBOYQuC8Y2DFwODAwcP/0HXAc//EPcQnAj5LCPAU/MwR4Cv5ECPAQ9CLoUBd4auE/guBVwf5PARaC+5qCAwXnJwSXB//HI4QGCw5ACAwUHNIn+gj/HAAg");
hiragana['O'] = image(54, 50, "gEB/0AggGCg/4gE8AwUf8EA/gGCv+AB4QaDv/wDQn/CwIaCgP/4AaDgf/wAaCgPn/4PBAAXv/0HAwef/kfAoX+n/4v4GCAgPxCYfg/4jBAAWBGwQ1BgEDJoJQCJoJRBLYcPCAJrCgEcKAaGEHgSGDF4QPCJYYxCHoYMBn5YDBgoGBDIP8FQKiBDwabBFoIzCv/gEAJQCMwWfKAIbBh58BDQMH/l/4IaCh/xTgIaCn/P/BrD/8/4CGD/i3BDQfz/gaDv/P+AaCCAIaEHQQaDv/hGoV4h//g4VB8JnBa4ePZYRkBBwKNCbwPwCYR/C44CB4BtBfgSaD8ACBYQQWBAAYA==");
hiragana['KA'] = image(55, 49, "gEH/AGEh/wAwkf8AGEn/AAwl/wEAhgGC/4CBngCBgP+AQP8AwMDAYIyDAYUPAwQ2CAwY2Cj/4gP/AAP4j/wgYGC/gGBg4GC/0/8EPAwsfCgd/4E/Awt/FIf/LgJmBE4IGCMwMf8JjBHwIPB4IDBgZmBv+DAYMHMwP/BQRfBOwIKCL4J2BOIQvBAgJxCGQIEBHAKPCCwIYDCwQBBQoRGBviIDIQJRC4AdCXAYdCKIcHboQ/CboY4BboghBboZKCFAYhBjAoDh/8nzME+CfBF4V/RgP/EgKVBwYGBFAMH/zIBFAQeBAwIoDboRRD4DrBJQUHAQJsDAAwA=");
hiragana['KI'] = image(48, 50, "AAMB+AFDh4FL/AFDg4FIn//AAX4ArpHC/xNEAov/LQgFCDgYAlF4UfPx8/g/8CoQbBKgQhCAoMDFAkHAoeAh4FEDgQAB4E/FgIUBwE/HwQdBn/gAoM+AoPAAoMMAohFCAqIpCgI7C4BEBI4oICAoZfE4C9BAob2EAoISCaQgACA=");
hiragana['KU'] = image(33, 45, "AAsB4ADC+ADC/wDBgf/wADMg//CYIDDh4DDD4UfAY/8AY34AZRDCh4DCg4DCgYbCgI/CgH/BgU/BgREBBgIQB8AMCFIRNDLoJ2Cv42DJwQdDFQIdDFQQdDFQIdDHYRkDgYhCgADDnwDChyzE");
hiragana['KE'] = image(50, 49, "AAUB/0Ag/gAwN/wAICgEfBIIIBB4P4BAYPCh/wDAcD/gYE/4FBDAU/4AYEGIgOCDAQOBh//AAP+v+DAoX/7/AAof3+E/AoX9/gYD/9/gYFD/4YE/5QCGIJQDHYRvCJQU/N4JKCKAYYCKAQYWmAYEjwYEx6lDh/zUocDMgIYDv6cBKgUf/4yBBAMH/4eC4EBNQUfAQN/DYMPE4TjCAQQkCYgSJBDYLEBn7QCAQIbCE4UDDYP/PIV/CgLpD4EPP4UH+AkBAoIACCgIADh6LCAAMDAoYA==");
hiragana['KO'] = image(52, 50, "h//AAX+gAFD//gBgn/BgvwBiWAAon4GwUBDIQACCQQFCn//4AFCg4lBCQc/DwYfBKQJdEDwYAB8CIihAFEgJJDIgQFEg5KEMgITEj/8D4hwED4JqEOIIfEv5eEg4fEFg0PHIwsEBigmFCYkOv65CJYPnbgn+ZgIAD8IMFewvgCYjRBE4IMDegQABIoUfAoK7HA==");
hiragana['SA'] = image(51, 50, "AAMB/gFE/+AAwcf+AFDgf+DIl/4AFDg4fEgAfLgIfCj//AFQzCn/gLJYMELI5mEh6GGBgUHGAP4CAQ3COYILCBgUDIgYZBAoYmBn5REDwPgQQPgDAIVBj4fBJ4d+CQI1CgeAXhgSDKoYSEQQp1GQQpFBawXwD4IGBg42BaQngBgRlDBgmABgjzBRYZDCPIYvCv//MQoACA==");
hiragana['SI'] = image(45, 50, "v/AAgUD/wKDj/wAof/wAECg/8BQc/8AbD/4bE/AbEFgcHFgk/FgcBFgkPDYhIgFgIKDFh8eFgn+FgcH/4sDv+/FgUD/osDn/vFgQ2BFgcf+YsD/+fFgUP/gsDv/HFgSKBLId/8IsCHgIXBSod/EIIKBwIhCv/4h4WBAQOAv/+IIP8AQIAC4AYBAAIkBn4KDJQIKDCwYpBCwRWCAoJhDAoK1DAAg=");
hiragana['SU'] = image(52, 50, "AAUf8AFDgP+BjH/AYP/AAnvAon+BjJAUgf9BgZFB/4MDn4kEg4MFGIwMED4QME+E/+AyC/x0DFgPABwIMC/gMGDIn8gYMFv/4EwcP/+AKYf/BgRACBgYRB/4mCgF/AwJ6DBgoTCRohNDTZE/VAkP/gFDE4PAUQhGCI4YeEUIgYBD4gMBEpI4GgIFEAAo");
hiragana['SE'] = image(56, 50, "AAcP/ADB//AAwP8AwkHA34FBAAn+A1JalmAGFvinFv4GF//PXghEBAwfBAwoNGEQP/+AGDn4GFh//8AGDg5PCgF/AYP/wAGEgj/CAwQADAw4mCAwZCCAAQ8BFQgGBAAQGBj4GFJQIGEJQIGEgYGFGIIGCIQQVDHQgACA");
hiragana['SO'] = image(53, 50, "gP/AAXggEPAweAgF/AoX+gEDBgfwgEfCYoFD/EAg4MFAAQMCAAQwBBhQpBJQozBAAU/IAIACIYJUBAAV//gsJD4IsEn4sEOAn+NIn/+4FEAA39AwvvAwqQDAAP7UYhmCx5bDuBVB4BCDg5bEJ4JoEgJ1EEQKCESwIFEg5vEEA4TFh4TFv4TGYgiLBCYrFG/5dDd4YHCOQKkBDQjbDDQQwDWgR5DAwSGEEAgAEA==");
hiragana['TA'] = image(52, 50, "gEP+AGE/4Mjgf/AAXAgE/AoX8BjUAgP+GYkf8AFDBhHnEIQMBEQQhBn/jFAWAgYMD/AMH/gMF4f/F4UH/kQGYd/KIIACg4VBBgmAQ4gMFUJcB/8DDQZgBv6iD/wuEn/gKIJGDEIl/4KCDC4KPE/+BBgYXBBgY5BAIImCj4MBTIKFB/wMBAAKSB8EPAwXnUYIMDCwLYD95RBEAIZCFQN/AwPBKISpBwEGQAgAGA==");
hiragana['TI'] = image(51, 49, "gED/wGEv/AAocP/AFDgP/CQk/8AFDg/8Bgn/wAFDj/wBQYAqJ4M/LBZrMJYZ+Ch5aDv/f/4bCBQIABCoMDHAYTBv4+Ej4MEg4DB4IMCAoIcCwE/TwU/+ASBEQI8BVQJLCv/gS4cP/kBMgYWBjyoEgLbJEYYSCQQkHCQg2EHASCEv4SBgYOBOQ70BQoYrBEQIABFYR/DJASRED4YFCBgJDDA=");
hiragana['TU'] = image(59, 45, "AAUP/4FFAAIGCAoX//EAg4GD//ACYYAB/kBAwgOBn4OFDgoOBAYX+BYP8j4GBwEAAgPDGwQ+C/F/BgIABCwOMLQl/+AGEg/+NIv/8BwF/gGEKwIqDAAM/HAYzDEhkfEgsDEgxJGh5JFHQPACqQrBCpkfCopXBCogcBCog5BK4jSCAwxtDDYK8EZIQcCAoQcDCYTjCJgQGCEYT0DIAYGGEgQGDEgRcEv5UEA=");
hiragana['TE'] = image(57, 50, "/4AFv4GF34GF74GF94GF+4GF/YGF/oGF/w7Cn//4BCDAwOAAwpQEj4ZDAxP8AyUPAwwiFg4GMgZFFAw0BLQqlBNAkAv4GG8AGEn/wKgv4KhZGGHALeGH4oxNh4xFOJBjGEYt/VQwVFg//BwhOBAAI7Dv4GBHYYcBCwgcB/5CEDgQyFGYgrCUwkPKAwAC");
hiragana['TO'] = image(46, 49, "gEH/AFDj/wAod/4AECgP/Cwn8C0cICwcDBoIWC/4NBCwMfEgV/4f/BoIWBv//LAMH/4AB8AWBAoWAgE/BQYlBDYUAh4FBHwQPEEIJQDFYJhCgYwCLQQqCDYQKDDYIKDn5xEEAYQB/x8JDYkDCAkPYIk/JoQWTAol/AocZQwR6B8aNCAAOPAgf+TIZqBAongT4QfCBYY9BW4R1BA=");
hiragana['NA'] = image(55, 50, "AAd/wEAn4CBgH/BIXAgEB/wJEgf8AQIJCg/4AQIJBgEP+ACBBIMAj/gAQYsBEoIoCGwf/GwkB/8P/4AC4f+j4GDw/4n4GDj/wv4FC/0/8AMD/l/4IGD/H/wYGD+P/g4vELARtCMQRtDMQQKDL4YKCMQQKDMQQKDR4QKCTIYKCFYQ2bOoI2C4BgCGwWASAQ2BGQKJC8DNBBAIAB+DNBPYf4ZoKrDAgPwT4K7BAwRdBB4K3BVYIqCVYY6BAwKrB/0DVY3+v/hAwf8n4SBdIXwnxEBAwXgnBEBAwShBO4IbBSYSVCOYQAHA");
hiragana['NI'] = image(57, 50, "AAMPwAGE//gAocf//wgFwgEH////kH/AZBAwP+gf+Bof/wP/gEDAwWAAIMBAwc/FgIGDj4sBv4GBE4P8HAIdBE4IqBAwYgBKAIGCKAYKBAwN/EYIGDn4jBAwZfBDAQfBLIPAAwZZBDgItENYN/CAIfBIAIGCLIRfDLIXwAwc/RQJmCHAPv/0PEoI4B+f/AwcH/P/w50D/l/wZ0CgP+j/BK4Q4Bg/gJoQ4BwIGBIwU/4EwAQI4CIYICCAYY/EJQMHHATcCbAQKEHARGBGgQqBCIc/D4IGDaITCDT4PAAQJfCQQRYDeQQGDSIIGEYYIGEE4IGEDgYFCcAQ+CGQZsCABAA=");
hiragana['NU'] = image(58, 50, "gEP/AGEgf//wHE/4ABAwc/AwIPDh4OC8AGBg4GCEwUBAwX8Dod/EgoHC4AsF+BJFjAGDg4iEFgRfF/+AAwk/IwQjDFIgjDvAjDMYJlCgRHB4ABBFIUf/ABBFIXH/0HCoUf+BcBLwQpBCogpBCYIVDv+ACohNBn/wCoRxBCohNCMoIVBOIQVBAIJNCCAIVCEYIQBCoOAb4QtDCAQtC/gjCdIIXCN4QwBC4SVBDQIXBEYUP/gXBI4QEBHwPD/8ODgR/CwZNCCYN/8P/5/4GQOf+DtBKgXv/jtBKgX5/0PAwJxB/0/DAL8CvkDJYP/IYMMgFgg//fot/VYQACgYGFAAoA==");
hiragana['NE'] = image(67, 45, "AAXwA43/4AHFn/8A4sPCA0B//+CAt///gA4kfCA0H/4QGA4IyFn4IBGQg5BIYsD//nCAt//F/CAkf/wzBCAYFBwH//BaE8ArBwBzFCAgNBLoQQCHIPADYIQD/6dBCAk/OQIQEHIQQEHIQkCCARaBO4YUCSYQQDHIQQFHIQQERQgQCLQQQEHIKBDCAPAn5fDCAP8gbNECAaJDCAbVECAPgvj+Gg72GdoqYFCAgHFKIoQDDA0AKIjODDA0ARYQAEhwHGAAIA==");
hiragana['NO'] = image(54, 50, "h4GFn+AAocB/0IAwcH/F//4AB+Ef8IFC//A/+PAwcD/0fAoX8h/wDQk/4ITDAgMDAwcH/hGC/EAj/wIwXggF/4AGB/+AJIIFBGQJJCDQoWBDQf/wZlBDQIWBh41Dx5kE/0/Mgn4IgIGD8f8MgYaBL4IaEPQJrD/6RCGoRkCKAR/BKAgaBKAoaFNYoWCKIIaC8BKCDQWAIYQaCgJCCDQRyDDQRXDEoOBK4ahBW4K+CAgKcBDgLcBMwIwC/1/4JHBCYP5CoQwC4aND/atBRofDAgPgdQaSBHgX4hxXBHQXAhAOBAwKXCAAJlBbIIAH");
hiragana['HA'] = image(50, 50, "AAMH/gFDgP/Bgl/4AFDj/wDBsH/4AD/oFE/9/AwoARJVXhAon4JQn+j4MEw4YLn4YEJTIfCAooYCAoX4DgQwCwBdEBgMDHoYMB//3Bgd/8AUC4A7BJQP//kHBwQGB4JYBFoX8KgMP/gGBz/+h//AIPjGAXA//wAoXwh/4DgX4gP8IgQnCF4QFBgOAEIKIEv6SCAA4A==");
hiragana['HI'] = image(59, 50, "gP/AAOAA4U/AwPwAwUHAwP+CwYVC4AGCj4GB/AGCgYOCCod/AwPgGokH/g8GHQY8CHQYVCHQg8CwEfCAYEBgYQDAgV/JYYEBh5LDj/4GoJKEGoJLCAwP4JYZ9C/BLCNwSGDQgSGDOoaGDAwg6BEYQHDh//EomDAIP+ToaQBEIIvCKoJyCJgPH/yDCEIIVB4BNBMwIgB+CZCn/n4f+h5jBAQMw/+BOgKyCCoN/PIICBS4I0BCoQJBJQJqCBIP5NQfgD4KACn5tDGQSDEwADBTIJaBGQKZEDISvCToR8BeAQDBAQLbCb4RSCAAcHcQYACvwGFg45BAAj/DAAw=");
hiragana['HU'] = image(55, 50, "gED/gGEg/4AwkP+EAhwGCj/ggF+AwU/4EB/wGCv+Ag4GD/4kBAwM//4AB84GBv4GC54GBAoX/x/+gIGDh/+gYFC/0P/kHAwX8AwMPAwX4j5cCGwJOBAwJIDj5jBv4QCAwIpBNoU/+AiBNoIGCJYJtBAwPhFwPANQXjAwOAgEEv+P/A2C/H+CoI2BTIIhBwY2Bh/xwH+UgUf+CwBUgSgBBYKkCn/gh/gToI1B4Ef4AvCBIM/4ZmCIAN/44oBSgKdCFAJ3CLAY0BUgQoBGgIGBEIUPAwSID+AGBQIZHBJQRECd4Q9DI4QvBJwQ2Cj4sBGATRBJwLcDFgTcDC4QGEEILqEAwIbDIARoCBgQAGA=");
hiragana['HE'] = image(55, 50, "AAUf+AGEn/gAwl/4AECBQP/wAYC4EB/4YDwED/wYDwEH/gGCCIMP/AFBgIRBGwcDCIN/GwUH/EP/4bCDAP/AAI2C+4GCHwMfAoX/JgM/AwYjBv4GI8YGCFoN/wIGBgYCBFwIiBHYJfBNAPAn/8IwIGBwAaBh/wAwOD//4R4IfBg//+B2BDoJKB+AoBg/+JQPjOwMP/n/z/nQIMf/IOB76BBn/3/gVBMgN/94nBOQX/7/gAwKbBOwSOCHoJMCEIMH/v/CAJxBh/7/hcCF4X4KYLEC5/wj5KBEIOfGwJRCL4PzF4V/JIQvBCYJJCH4JxB4AGB/xCCFQIJDDoIMBBIRNBAQJdCIwKUCeAb5CPgQACSgIFDSgIFEAAg=");
hiragana['HO'] = image(51, 50, "AAN+AokP+AFDgf+Bgl/4ASE/ASVv//AAX8h4FD/+BAonwn4FD/0HBgnAAogoBgP/HAk/8AFDg5LEgASM/gSFwADBFQIAC8E4Iof+/5FE5/wAof5/0fAwc/8YFD8f8PAYEB54MDJ4SRDJ4KRDj/gNYaoCLAYWBLAYWCLAQWCDYJvDgYSCCwV/NYQWBGQc/+AyDg4yBj4MBgYSBAQP4OwPwbIglBQAgpBBgZiBBgYYBBgY1CU4S0DFoIRCAAo=");
hiragana['MA'] = image(55, 49, "gEP+AGEj/gAwk/4EAkAGCv+AgAPD/8AgYdCgP+EgkD/gdB/AGBg4DBv4GCj/w/wGCv////8AwQFB//4AwMBAwXwEQMDAwXgAwMHAwXAAwMPAwWAG4QvBLgQGBL4X/AwRfBKgIGCL4X8n/gLARUBn5YDMwM8NQaLBQYIoCAQSIDAQRZBRYaBDRYQhBFAIJCKIYyCDwKoBToZkBOAIJBPYKLCGwMH/h2CAwMfKoKKCI4PgSIYYB4afDJQMP/gpB+AhBMgIjB/AhC4EfAwIhCEoIGCwJdBaIIZBMgSkCjhMBgakBG4LICUgKDBAwQuBPgRKCjgGE4EQAwgEBAAIbBRAQACQgIDB");
hiragana['MI'] = image(50, 50, "h+AAocD/gFDgP/CQl/4AFDn/gv//AAOP/E/AoXj/0HAoX4/+BAoX+DAuf+EfAoXn/gYD/P/gYEBG48f+AFDg5QMMYkf8BvE/BvE/wYE/4YEKAIYYgZSCDAMBJgQYCCgYDBFoYDBj4tCDAJlDDAMBGYYYBNYYYBn4xCg/4h6ECPgIHBPgfBDwaVBQgYvBToYYCFYauBaIIwB5/wcAfz/0PAoX8cAn/IgQFC55dBAoXxFILtC/grBGgL5BYIoAGA==");
hiragana['MU'] = image(58, 50, "AAV/4AGEj/wAwkH/gGEgP/Aod+Dgv/wAcEj/gDgkH/AcEgP+Dgt/Dg3wn4mBHwYGBDAIyCAwP/8AGBAoQODh4GC/4sBgYGD/AcCAAO/IQQcC4IkCDgI7Bj5YBg//w/8EAIjCwIEBv/gMQPgLAMPFYP//h1BgZpC/4LCNwIxB4YoBFoIxB/AjBNIMH/v+n5UB/4qBn/fIoIJBv+PLYUPQwPhOIUD/gvBGYMH/3/BAX/457CBAP/84GBDgIlB/YGBCYJwB/qECDgKREwBCC34YBDgfvLYP+HIM/+YYCIwM/MoIYB/hGBMoQEBz4nBKQfDAwODGQXwKQQMB/P4j4GBAQP+ngtBUgIRBg6aBRwKiBwOAf4TNBAobjCAogAEA");
hiragana['ME'] = image(57, 50, "gEP+AGEg/4AwkD/gGEgP+Dgv/Awt/wAGEn/Agf/BIUf8EP/40CHAMf/4tBAYP4AQImBCIP8n4GB4EH//+AwXgEwP/v4CB/EBAYIPBg4jBAwX8BYJFBCQRKDFYIGBJQJxBIgUfAQIrBAYMPCAIfBBQR8CAwR8DMAZ8Cv4GCGIQGDGIU/AwR8BAwKqCWoU/FoS1Cj4tCHASEBWogGBUAQKBAwItBHARpB8BlBBQKuCAQIKBO4SqCBQX8AwX4h/9/wGC/kP/n/DYSlCv+P/ArB4K+B4/4SIV+j/jWIX8n0P+JSBDoMOMwJWBAwOCMwM//ZOCMwI4C75nB/5bC45nBv+DAwPhTgXAb4PAoCfCQQifBYoYAHA");
hiragana['MO'] = image(60, 50, "AAX//4GEv4HFj4GB/wGCg4GB//4AwMBAwX/4AcEDwcPAwYWBgYGDCwQVC54tCCoX8F4PgFYP4CYI+BgE//0P/gaB/ARB4F/4ApBwAVBg4OBj/8EgITB4AiB4InBBwQgBCAIOCPQPjD4MPJ4MH/0/+ALBwARB84kBBwQ0Bv/gBwc/+5bBj5tEHAR8Bn5lBBwInBBxY2CBwcDWIQOEGwIODJwIOFIoRKC4CNCBQP3AgKwCDIIOBKIQKB8/8IQJgBj4OB8E/MAfD/ytBEgX8J4KeBZwWDIgJCBCoP4ZgIzCAYIqBeYRQB8DnCK4gGBGoIDBwAyBF4IKCCQWBAwIVBEoPgF4RFBg/4F4Q2BAAQOBTwIADHoQADbIQAIA");
hiragana['YA'] = image(54, 50, "gEf+AGEv/AAocB/4MEg/8DUv///Aj//wEDAwIcBAwMP//8BgIGBn//+IFBAwICB54GCDQQAC/0HAgXAn45BD4IDBn45Bv4MBAYPgGYJKCFAIbB8EAgf+DQRbEv/4LYYaBOQU/4EPCwIhCCYJrCgf8CYkP+BlBCYQaBv6GDOwQaECYIaEKwIaD4JWDgP+CYIaCg/4NQYTB8Z+BFwef+4aCMgN/74aCn/z/zXCIAOH/IaCh5CB44aBJoU+a4QyBwFwDQLGBCAOBX4adBGIJMBRIQaBUYI4CDQJnDFYJ7EDQKzCDQYECAA4");
hiragana['YU'] = image(52, 49, "AAMf+AFDgP+Bgk/8AFDgYMM/gkD/4AC+EBAof/BkA5FhEAg45Cg/AgF/AQMBBIMP/4DB//gE4Xwn5dBn4GB74IBgY0Fv4FD8AfBAoYfB/gbBIAIiBg///A7B/+A/4rBCQIxBBAISB/ghBCQeBEoIMBCQI0BBgQSCDIYSB54MBgIlB+AMCj0H/0PBgIABHQQMBOgP4BgZBBBwTDCMYIMDKIIMRWQQmDAwUMYYqyBAoaxBN4IMEV4QMCcggMBWwbZCAweA");
hiragana['YO'] = image(55, 50, "AAMHAwsP+AGEn/gAwl/4AFDgP/BgkD/whF/AGEj4oFEIsA/+AEIgoFg/8EIooFJQ3/JRcHJSgoGJQxEEg//FIkfAws/Cgv/AwUGJQX/HwMP8AoB74GBj/gh/+IoU/4BzBBQJBCJQIKBNQRzBv+AWoIIDJAP4SoMBIgIkBOYMDHoKTBAIIRBXgQBBB4IfBEIQYBFALgCCwMP/iVCJAXwJ4QfDcAX/4JRBSoRvBEIZ2DcAQGCFQIhBPoIYBcAQGBDAJqBCgQ6Bg7rIAAY=");
hiragana['RA'] = image(48, 50, "gEP4AFDj//wAFE/gFE/4TCn4FBBgQFCBgQRC//gBgN/BYUP/EBAog3BGIIFCgH/BAIFCh4FEgQFEBoXwAqsfAoIuBAoROBEwIFBIwP+AoPnLIWALwZfBNQf/+AFE/AFBEIM/AoR6Bh/8OoIzBg4FBRgQFCL4UD/wlBAoikCAoM/W4QFBj5dCAoMGAohpDg4FEHYJ1EAog5DDgJWCb4Y/Cg7RDaARFCAoZFBAobiEeoruCAoQtCAoI+DAAgA=");
hiragana['RI'] = image(40, 49, "ngEDn/AAg9/4Ef/AEBwF//4EBwP//4HBw4EB4F/x4EB8F/z4EB+H/n4EDAQIjBCwUPAgUAAgX+gEH/n//gEDHIMDAg3wAgP+AgvgAhBeBAhmAAiJ3BAhf8AgRUBAhBXBAAJtBAgSgCVgRcBAAJXCEwIEDj5SCBoJDCBAKSBBASSBXwKICAgQmCAgIcCv4SCAgI0DeAY=");
hiragana['RU'] = image(51, 50, "gf/AAXAgF/AoX8gEPBgeAgIFD/EAn4MEg4FD8EACQoACn4lBAAUf/4FDDYOAAoQuBHwIACv/wDwgkEh/+DwoFDDw5ECDwRLDMwg5BLIZMBNgh/FGgIeB+AVB4AeBEYJmBBAJQBDgPBOocf/AoCVIU/Kwc/+5WDg/+Kwl/5/wh4mBh/4/A2CFgMOAoJDC8GBMgUHGAJQCCQKpCBgISBgf+SQMPCQN/4H/4YSBGIIwBCgMBDoTMCn/AEIROCLoKFEAIJvBTwZvCTAarFNIQFCXASyCYoYxBAoYAEA=");
hiragana['RE'] = image(56, 50, "gEf8AGF+AGigP/wAGDg//GYQGBh//C4M/AYICB/AGDv///gGC+P/AwQKB+YGB/wNC+//w4GDBYMDAwn4AwQ3BFQIGF8AGF4AGFgAGEAYMDHwIGBAYIGDn5XBAwhlBAwd/Axh6CAwSPBAwMHAxEDAwqdBAwidDAw5IBOoQGDU4QGDUAIGE//fAwufCgrmCh4iCAwk4nwGE/EcAwbSBjAGFegReCUgIGJOYIUEQIYGCIYOAAwPgAwIAIA=");
hiragana['RO'] = image(50, 50, "AAf4gEB/4AC8EAv4FC/kAj4MDwEHAofwDAgSBDAoACn/+AocfAokP/4FDE4OAApED//AAohJBAAI5BAocAIQIFEHghFCD4QFCBoU/KIQMBNQZ9BOAhOCQYYFE/B8CE4QFBM4JGB4YuDj/7AocD/xIE/+fP4c/84FDh/8QoZyBj5mE4aFDn5yEDAIFDGIIFDIgIXDDwKREv4eEv4eBiAFCDwMH+A8BIQLnEEgLnDSooqBQYQFCDgQ2DAoolCJAgAD");
hiragana['WA'] = image(51, 50, "AAV/4AFDh/4AocB/4DBj/ggE/AQMD/0Ag/8DgWAgH/AQMP+ASB//AgISBAoIDC4Ef///+ASBh4FB/4SBgYFC+E/4IFC/8H/F///9//g/8f/3/x/+j/nAQPwv/j/H/wf+I4N/KAJlBv+P9/4MoMP/f9/xlBAIIqBwAUBn/vFwIdBg40BNIIOBIIR7B+BbC8B7BKoX4uAyCAwM+GQX5//f8IyCn/z/hHCK4N/4/8h/8/4EB/4lBF4P/z5wB8f+RYJjBPoPAFwO/BQP4IQX/wJkCTAUfVYf4gf4BgS4BbQRiCcgbSCAAILEcALkCAAM/DoYeCC4ZLBfoIeD/ASEDAhoBAoYlBDwcAg/ABggAEA=");
hiragana['WO'] = image(50, 52, "/4AE34FE94FE/YFE/wYYGocB/+AAwd/8AFDn/4AocP/gFDgf/KovADAnwDB43B45EE+IFE/F/KAkfBgmHAonhAonwDAn8h4MEN5X/N4l/N4k/KwkfRwgoBDwcHOohoBOoYFBEgY2BEgYFBEgYFBJIYXBFQYpBFQZ3CAoIWCKoQQCGwQLDHgR8CAoQdCAoQvCOYYFFn5gENgKREbYgAGA"); // XXX there's no WO in hiragana, so we fill it with a copy of the katakana char
hiragana['N'] = image(54, 50, "AAVgAYUP8EHwAGCv/Av4RD/8D/wFCgf8g/8DQf4j/4AwU/8E/+AaDwF//4VBgIfB/4GCD4MPAwcf+YFB/4jBn4FC/4jBAof/4AYC//n/+DBYeD/wZC/f/FgIrCGIQsCKYU/444CKYP/z4xCvxOBv+/8EBQQP4B4KFCCoJeCNIYPBQgQKBj53CAYSbBCYQDBHgJbCTYUDOQZHBM4QTBTYX/GQQxBP4Y8BDQRGBTYY4Eh5MDHgZTDAojdEbAYGEHgIGEv7/DHgIhFfAh1EEIg8GEIg8GTYYhDHhYAF");
/// /////////////////////////////////////////
const katakana = {
A: image(56, 51, "v//AAfwAon//AGF/wGT/gGM/A3F/BDEn/wJQoGCj4RB//gAxUB//AAwcDAwsH/+AAwcP/4tCAwMf/wGEn/8Awl/JYYGBKQkf/I9DAwJgBGwQGDGwRlBAwJsE+42DAwPzGwYGB+J7EQIIvDQIIFEAw5DEAwRDDgCIEAxCPBKIcAR4IhER4hnCLAg9BLAgoBAwgoBcQiCBMwj0BHogGBHogGBfoooEQQREFEIgGBAokAhAGFA="),
I: image(54, 55, "AAkEAws+AokB/wGEg//Awk//gTE//gAwcPCYt/CYkDCYsfCYv//A0F4A0ECYg0BCYggBCYn/KwhBBGgl/EAgtBEAgMBEAZOBEAgMBEAYZB/+ABggTDBgQnDAoIaDJoIaDFgIABDQQFC74aBBgX8v4aBEwWBDQQgB/EHDQQ6BwEfGoX/+AJBDQMDWAKMBDQMPAQIaDiBFCPAgaDU4hrDDQiuDDX4acSAIaCA="),
U: image(52, 55, "AAMP/gGE//ABlH/AAnvAon+Bk5EDv/vIgcHBkHPBgZwBBgn/Bi8B/+PBgcf/AMFw/wBgYEDgED/6qEv4MEKYK3F8AFDj7EED4LREv/4CQn/wASEFginBDAgfEDAIfDn67BC4YABH4QXBCQcHZoQkEEoYMCHAYlBFYZEBLwk/MgpQEAAw"),
E: image(58, 45, "h//AAfwgYGE/0AAwn/wE/AwngDgv4DjhDCv/wJQkf/gGEg//AwkB//AA4gc/Dn4cjbAv/34GF94GF/YGF/wcjwA="),
O: image(57, 54, "AAcf+AGEh/8AwkH/wGEgf/AwkB/+AA4n/4AGEv/gAwk/GIsf/A4P/4AE+F/Awn4n4GE/kfAwn+h4cFg4GFwYGF4IGFKwYFBMQpxFAwJxEAwJxEAwJxEAwJxEK4JxEAwKqEMoQGE/o4En/8HAl//iqEAwKqEv/+VQgNBVQgNBcYgNBcYhLBcYhSCHAQKBAwI4CAwY4CD4IGBHASxBAYI4CAwY4CYwIGBHAQGBD4I4CBIJfCHASmDHAV/PYQ4Cj5QCHAUPLwQ4CgQGCOIgABOIgABHAIGEAAY="),
KA: image(54, 54, "AAMP/AGEv/gAocB/+AAwcH/wTEj4arg//AAf+j4GE/F/AwnhAon/w4aZHAMP/hTEn/wKYn/4BTDgf/KYgQCDQYQCBIQQDBIQQCBIc/DQouCDQQuCEghJBEhITBH4RTBLoRTEBIJTGCAUPNwoTCDQQWBDoIuCj4TCJIX/CYQ/BZQInBH4U//0HwBTBGgPwXAXwh4PBXAXAv4PCZIIgBEYTJBn5SBDQXABAIzBCYJcCDQXwgbOCAwIDBQgI4CgEOJwIADkAGFA"),
KI: image(58, 55, "AAU+Awv/4AGEn/wAwkP/gGEgf/Dkk/CAc//4ABwAGBj4GC8ATBAAf4h4GE/woBAAmAAwvgFAYcIwAcD/BFDFARFD/kBIoYACv5FBAAcfRL94DgkfHgf/95EBD4RgDD4MHLwf8AogAd+CPFGwiJCS4XHJgSGB8CJEkCJJUwYABg5pDD4amTNwKmXYbgcDLoY="),
KU: image(55, 55, "AAMHwAGEh/8Awkf/AGEv/wAwn/4AFDgf/EQkH/whF/4ACAwM/AoQQCBgY5BgIGDHIMHAwY5Bh4GD8AhEIAQFDIAIhBBIJACEIJpEj45CNIV/NgRpBDQIrBEoPgDQJlBEoQaDEoV/RwUP/wPBQ4Uf/gPBQ4QsBKAKSD8BvCSQXDDQYYBNYIaCGYIqBDQU//kPXoYYBj5QCEIPgj60DKoMcWga7FKoYABKogaDbojPBbojMDGob/ECYJBCbgYaDE4IaEPoIaDEAI1EbYQZECYgtBCZQGCLol/KwxxEAwJqEgIMFgIZEgA="),
KE: image(60, 54, "AAMcAwsD/4HFn/wBxl/8AGEg/+BxkP/gOF//ABxcB/+AA4kf/BCGAAZOBv4HEIQIOGAwgOBh4OFGYIOFn4OFEgoOBAwvgh52BKgYDBOwJUDv5nBBwY6BAYM/BwIKBJgJjBBQSbCWoQVBRgK1D/4oDBwJJBWos/WIS1CgIVCJoRGBWowCCj61HYgpRCdIjEGLgTLEIwTLEfAv/GYqtBEghyBGYjoCAwwkDAwQVEYwYjEHQt/CopeBQgQOEIIgOBPgxeFgZ7FA"),
KO: image(49, 46, "v//AAYFF34FE74FE94FE+4FE/IFE/gFE/w0Dgf/AocB/+AAwf/4BHE8AFDn/wAocf/AFDh/8AocHGH4w6YZf7Aon9YYoFEejBhEAAIA="),
SA: image(58, 53, "AAcD/wDBg4DC//AgEB/+AgE/+AKBv/ggEP/gGBj/4DgP/DnU//4A34CQ+DAIcEDAIcDDAQDDDAYDCDAYDD/4cDIgJADAAUfIAQACh4jCAAUHD4QACJwIfBAAQtBEYgGBI4QUDFQkP/4qEVYQvEAAIxCEIK5CBwV/AwsfAwocCAwYcCJogcBNIp3F"),
SI: image(56, 52, "gFwAwt+Awv/8AGF/gFDgP//4GGCocDAwIVDBoX/wAHCn4VFg4GB4AxEAwsfAworBEQYABv4GFj4DCjgrCBQYRFn/4JQfAIgIGD+F/JQcD/gGBMARQCOwcH/wNBCoUP/0PAwIrBj/8OwQGBn4fBGIIGCAQIlB+BcBAQKvDBIQRB8AfBIQUH4AXBP4RXBGgJmERoJsFAwv//yaFbYghBQIYaCeAi9FPQTZGdxKFCFASECFAZPBEIgNCJQaZEAwhDDAwRJDTAYGEQAiQBPIgAGA"),
SU: image(60, 51, "gH/AAYGBh4GD/AOG4AOF/gONDo+ABxAACgY7CAAd/+AGEg4OG//gAwkP/wGEgJCCAAcfKIQzEIQIzEIQozOj4zFEgIzFn4kHGYv/M4okIGYt/IQqXBFghuBHYs/bAY6DCwrJECod/HgYVB8ZLEcoMfLQYECCwYVB+BTBCwT7CCwYrBAYIKCCoQDC8BXBEIQSBNoQVBBYP4EAIoCOQPHCoYTB/xdBIwQ8B+6SET4N/dYn/4aCFFgKRFgC+EgPghivEAoI"),
SE: image(57, 53, "gEH/wGEgf/AwkB/+AA4n/4AGEv/gAwk/+AGEj/4AwkP/g4JjA4EBQQ4D/4DD4E/AwIuBv/vAoP/FwILCAAIuBv4GEBgn//wFEAwITEh//CgfwAwMfCIRGB/4BB/5xBAgJTBIQQGBwP/75CBAwOAD4JCBAwRmDDIKYBOIQGDOIQGDOIQbBAwSqBAwiqBAwiqBDYg4Cv4GCHAUfAwQ4Cg4GCHAUBbwbjnHAgADcYYADUYQxEEYq6CVwbDBdQi6CZQYqBAAZcCAwY1BEYi5DAAQ8CegfgA="),
SO: image(52, 52, "gGAAol8AYUD/Ef4AGCn/3/wFCg/+v/wAwV/8//Bgk//AMD8f/FoQMBj/8Bgfg//gBgcPFoYMBFocP/kHFof/4AtDBgMDFoYMBFoYMBgIIBgADBwAtDj4dBHQQMCFoYqCHQQqCFoc/BIIPCCwQtDKYIpBB4IwDIAQwCh45CBIVAFgSmDFIaaDOIYfCVgYfBRYYfCTASTCUoY1BQgZPCD4l/D4kfH4g4BH4YYBH4gFBGQd//4yDBYIyDn4SEJQIlEBgRXEHAg+BFYZRGZYQADBYgAG"),
TA: image(55, 56, "AAMHwAGEh/8Awkf/AGEv/gAwn/4AFDgf/EQkH/4oF/4ACAwM/AoX+FAQGCHIMBCYY5BEIIAC+AhFIAIhDHIQFDF4IhBJQMHF4JDDNIUfHIRpCv5sCn/wDQJsCDwIaBEIIKBwEf/9gOAQaB/gbBFAIPB+YsC/AaB54RBFAIaBAIOAEoJvBOgPh/+DNAJWB+//DQPBQIZyBM4f4LQSQC8EPKAIpBFAMPPgKKCgEcYIZwBiAGDbohwEZ4bdEFILxFf4ghBXwLjEDQhLBCYoaEE4IaDdIQaDBgLBCDIRQENYYTIewRkEAwJCFHYicBOIkAEAhDBS4IAJ"),
TI: image(57, 54, "AAkGAwsfwAGE//gAocP//wBgn//gEBgIFBAAIeBAof/wAYBAwkHAof+gEDAwf4E4YAB4AGBv4TDAAM/AwoxDKQhABLQwiCAAV/MIglBMIglBHwRwDNARbF//3Awv7Awv9Awv+Awv/MQQAD34GF74GFKAUHOIYABSAJxGaYp4Uv54FP40/P4oGHQwQGKKgt/AwrUEMIQGEVYIGLg4bMFII+Fv5TGNAsPQgsHTIoAG"),
TU: image(54, 53, "AAMBwAGEj4FEgf8AYPwgFgn/4BIP/g+Av/ggEP/n/gP/4EAv/v/wQBFQP/z/4CAMAg/+DAMfEIICBDAN/FgN/8YYBBAIaBw4hDDQIVBAYMAn/wDAIhCCwIhDCwIBBwAIBHAIYBEIQYDBAIuBwAjBFQghCJgQhEAIIhDEYQPBh5HBM4IhDQQQhCwYeBCwMBCoSPB/0CIQQhBAQKWDvytBCYTBDv5tBZYYTCAAQTCAAYTFHAITEj4TF/4TEh4TFv4TEg//JgIMDMYIMEO4ImD/53BAAM/AwIsEEAgFBEAZNBIIgTCFocfJwo6BPgpHEgZAEgEOAogAGA=="),
TE: image(57, 51, "h//AAfwg4GE/kDAwn+gIGE/8AAwuAv4GE4E/Awngj4GFNWJNF/gGF/5UF/+/AwvfAwvvAwv3Awv7GJn8IQV/4BJEv59Fn/wAwkf/DJFEAYABg/+AwjJBAxbQBwAGFH4gGBH4gGIIwgGNG4IGEg//LYjyBAwiyBAxc/EQoGGFIJTLdYJvEgF+fIsYAwo="),
TO: image(42, 54, "//AAgU/+AECh/8AgUD/4U/CgYPDn//wAUC/4VCCgIlDAgIKCCgIKCCgP//wUD//gCgQKCn/zBQQ+BDYP8CgMBEAQBBj4KBKYIKC54yBBQP7KYIKCG4QKB35YBBQIUCGQPjNAUD+BXDnB9Dgy8/CicAA="),
MA: image(57, 50, "/4AE/l/A4s/AwvfAwoAN/YGF/oxGHokf/wGLh4GN/4GSg4GChgGDwARBAw3gAwv4Awo7BAwn/4ACBAwIKB+AGDgJtBAwcAUgOPAwYLB94GDgaFCAwTBDAwcfAwoyBAwgyBAwgyCAwgcBAwgyBNgL0ENgIADn6oHDijhFW4wcB4AGDKwPwBwl/fwzUJDgZOFgAGGngGFhADCA"),
MI: image(52, 53, "gPwAwkf/wFDgf///gAwU/AwIVCBgX//AME//8gEHAoQGCBgYGCv4GDFIMPBggoE4A2CCoIuCAweAAwc/BghYBMwswNw0PNwkBGAIbEG4gMCOoYMCOoQMDAwRnE4BYDKYQTEKYRuCKYY8GgCjDAAV+LAtgcTMDbYhTCHobICBwbBDBghZDZwmAZoYGCAogGBCYgiBEIidCBwQ2DS4QMCVYT2CSAb2DBoLpFn72EdJAA=="),
MU: image(59, 54, "AAMDwAHFv/AAwkf/gVF/4VG8AGEh4VHFgoVPFdZBdRogVBgP4CokBFogVBn/wTIkHEwYrCv4ODCoMP/wVDFIP/JYQVCBwgVBGYLICCoTIDCoQCBBwQhCn5RCCoR/DNoZCDDIRRDCoQODg4+CIQYvGCoZCCCoZRDAQV//4SBRAM//4ABwEfAgQAB/ARBAAkPAwvxAwv+Dgv/8YGF/gkD/xCB543DH4P5AoaBBewsAvgGFhgGFAAQ="),
ME: image(55, 54, "AAcB8AGEgf/AwkP/wGEj/8Awk/+AGEv4iF//AFAuAAwcHFAsPFA34AYNwFAQvBgICCFAUHCAIoDDwQoDn4DBKIf/MYIoCDwIGB/5RBAwWDKIYGB456Dv//75RDAwP/JQQmBAwJ6Dj4GBOYYGCOYcP/5zEg//OYgGNDYw3BAwgvBAwaABAwgaBOARZC/wGDOoP8MQI1D+AGDFwPAAwJaBDAQNCJIc/AQJsBTYL3COQc/4ATBXoYdCSgU8J4SNCmCNCNQqoDAwQuBAwgFDFAITEAwK1DAAKZEAAIMFAA4="),
MO: image(55, 49, "j//AAfAv4GFAon/wIGFgYFE/0HAwn8h4GE/AvF8A4Bv4DCAAQzBAocB/+AAwYxBCYkH/wGEh/8MIv4Awk/+AGEGyJfFAFP9AwpOBNuikeAwxfEHoLpFNoZACAwZABIgIACJYYABIAYGCIAYwCHIoABA="),
NA: image(57, 55, "AAV/8AGEn/wAwkf/AGEh/8AwkH/wGEgf/AwkB/+AA4n/4A4rGoIAE/IGF/wGF/9/Awu/AwvfAwvvAwv3AwpQCOOqqEWLV/H4pGGn5GFAw0fJosfJooGGn4GGKgq6BLQoGEg4GFh4GFPoIpEDYIwFv5MFLQ4GFg6EFgaZFAAw"),
NI: image(56, 43, "h//AAf4A25+/AH4AuWggA5A="),
NU: image(55, 51, "g//AAcAh4GFj4FD/0An4GD/kAv4GD/EADQnwgIGE8EDAwnAAwuAIIgvBAAcPF4IADn4vBAAd/8AGEFAIDBAQIsBFAMDCAIoDh4eBj4oCj4GBFAd/CIJRBgBZCAQIlD/+HQIIGD54oCNwZKDPQZPDOYRdDOYqmBOYi0BOYjCBBogGGYQSAEAwimDGATdDAwQTBH4JFBLIP8AwYTB+AqBAwITB4AGBE4bADBIJyBUIJ6CVgXgJAQzBg+BAoJkCgxcBCYRIEPArlEH4YGDO4ibBeQs+AokAsAGF"),
NE: image(61, 55, "AAX/4AGEg/+Bws/+AGEgP/wAHEh/8Cwt/8AGEgf/Bwsf/AMEAAYnBj4GDHwQOEDAMHA4hVBn4WFJIIADHwMPA4hgCAwZkFCQKCGBwpHBPQwOFFAJyGBwt/BwozBBwpwDGYiYEEgP+iAkF4IPDCoP8j7WCUAXhbwYVB/4RBU4n4QISfD54vBS4f+FASPD+AEB+AFB/IjBFIPnA4LzCGAfAeYIjBGAP4eYQCBwZuBeYUH/EfIwJRCAoIDBg6ACnCmDR4oqBDIKfEHgKuFS4g5CBwo8CWwqOCAAQ8DcYg8Vn48FAAo="),
NO: image(47, 52, "AAcHAokP/gFDj/4Aod/+AFD//gAgUB//AAoUD/4oE/woJn4oLEQYoBwAoIh4oEj4oFJZ8HERU/EQhFEDgIiDH4JFDh4iEH4t/NAYcFHII/Dj4cEv4/DCwIcDCwIcDCwI5DCwhEBHIYQBKwf/GYYhBCwc/FoYKBFoYEBFoQKCE4RrBE4YFCHwQyBHAYnBJ4YFBcBN/AgcAPgYABA="),
HA: image(62, 52, "AAP/wEH/gGCgf/gE/+AHCh4MB//AA4QMBCIQeD4ARCDwv4Dwt/8AeEgI4BDwkH/weFj4eEAgIeF8AeEAgQeEAgQeEAgQeEAgQeGMggeCMggeCQYiACQYYbCDwgbCIogbCIoZZDIoYTCMggTCEwn/CYJFDBYZFDBYYmDv4LBEwYDDg4aCh5JCDQYiDaIQWBNAQ5CMAYLDcgYmCCwgqCGIYTBFwL7EJIIWEAgPgh4WDNAPACwgMBCwiHB/wWEFwV/CwZVB/YWEDgPHXgYuBDwLbDKQPwh60CGwWAngGDgAFBkAHEsAFEAAQA=="),
HI: image(47, 51, "//AAgUB/+AAoUD/4QDg/+AocP/gFDj/4Aoc/+AFDv/gFw8BwIuDj+DFwf/FwcP/4uD///FwQKB/wuBJwIFBFwM/AoP8//PAgP/+IDCAAJdBAAXwg4FDEoQKCIIIgCLoQFBKYV//5qDB4aMuF1YFDFwIRDUIQAC+YFE8YFE44FEw4FEUgn+Aon8WwhKBXggA="),
HU: image(49, 50, "/4AEv4FE34FE74FE94FE+4FE/YFE/oFE/w0Dg//AocD/+AAoUB//AI4ngAod/+AFDn4FEj/4Aon8AocPAokHHgg2BHhYFDHgJCLJBZCEAopIFAoxIEAoxOEApc/AojSBbwplEAoZxBAocPAojICBQhBCGYIFDBYRZCa4P/NYQuCPoYFBSoZGFZYsPAgYABA="),
HE: image(61, 43, "AAMH8AHF/4HFh//wAOF/wOG/AHEv4eFg//DwoOBDwgOCDwk//YeEgf/x4eEn/8n4eDgP/4AeEj/8DAIeCBwPgLgkfDYIeECYQeDh4LBIwIeC//wDIIeCBYJdCDwV/BwIwBDwIOBCQYeBn4pCDwRIBIAQeCMIJPD/AOB4CED4BhBMwf/MISbD/kHPovwj4ODDwV/UYhYBKQJ2DRoIGDHQINEcARCCWYgGEDwIOFgb+FDwL2EDwQGFIQoeCBw0YA40AA=="),
HO: image(61, 54, "AAV/8AGEgf/Bwsf/AHF//AAwkH/wOFn/wAwkB/+AA4kP/g8Rg//AAngv4HFCYIAE/EfA4vAAwv+Eo3wn4HFwAGFJwZ5UgfAPIJzDn/x/+PEgR/BAoJzDP4N/8JzD//D/6KDFYI8BCwYrCCAItBPQOH/wWDCgIQBCwf/4P/wIWCCQIBDWgYBCZ4KJBE4LPDEYInBh5sBBgKLBNgQ0CJoIWB4ACCBgIiBBwP8EYU/TQLXBHQQECFAI8BCwIqB8DzCDYMPAgQbCMoI3BF4IRB44OBWwQUBv4TBJIV//InBHgQCBw4OBHgUH/EfNgKOCj0A3BsCQwNgeaSdCABA="),
N: image(54, 50, "ggGFngFEgP+AwkPAws/AwkB/4GEh4GFn4Gaj///gNF/AGF4BEJAwITBgOAAwQTBh4GCnwJCCgVwLgRwMHAgTBHAgTGv4TEgYTFMIITEMAsHMBY0B+ClFCYiPFEAITEv//OIQMCTg3gBgggEDIIgDGYIgDMIJVDDAIABIIILCFoYYCJwZ0BHQgsBBgZnBBggnCKgYhBMIi3FgAFFgAA=="),
WA: image(51, 50, "/4Ay4A3E/AFCh4GBAoUBAoPgAwU///8AoUHBgOAD4nwAoUf//+AoUDGRYSBGQYSCGQd/94yDh/9GQZFB34yDn/zGQcPAgYSCG4YSBC4YSNv4SKJYJwDLwISEn5QDS4QSDDAJjDDAJ2DGIJ2DUYQ+DQYKcFFYYXBDASOCGIQFDGIQRCDwTaCG4YFBEgbHHN4hiFg6HEA="),
WO: image(50, 52, "/4AE34FE94FE/YFE/wYYGocB/+AAwd/8AFDn/4AocP/gFDgf/KovADAnwDB43B45EE+IFE/F/KAkfBgmHAonhAonwDAn8h4MEN5X/N4l/N4k/KwkfRwgoBDwcHOohoBOoYFBEgY2BEgYFBEgYFBJIYXBFQYpBFQZ3CAoIWCKoQQCGwQLDHgR8CAoQdCAoQvCOYYFFn5gENgKREbYgAGA"),
RA: image(51, 50, "n//AAcHAongAon8j4GEwYFE+F/Aof+h4ME4IFE/BYr+4FE/wFE//fAon7BgpYE//vAon9CQo3Ev/gAocP/gFDgP/wASX+ASJgYSFXwJ2ECQivBDAoSEWIs//wFDbYIrDAoI+DAoIYDQ4IYCFIIABDALlDGIJhBewS/EJQQYCG4YkED4QFDD4JJF4AFDA"),
RI: image(43, 53, "AAf/7/4AgMf/f/AgMD/9/8AFBv/v/gEBh/9/+AgEB/+/+AKBn/3/wEBg/+//AFX4q3v4qDh/8FQQPBz4PDAYQvBEYQvCEYI/CGYRPBB4cfIYQpBB4cH/5TCDwJjD/4kCn4EBCgN/AgIUBDoP/FIJHBAAIyCDIYjBIYYaBQ4QaBJoZHDAAoA="),
RU: image(61, 53, "AAUH/wHFn/wAgUB/+B/+AA4UP/gBBCgd/8ABBAwUD/4BBBwcf/ABBA4f/4ABBHQg8FHQI8/HksYHgwYBHgkPF4I8EvwlCHwOAg4gBEYI8CCIQjBHgITBCIP+HgU/CwIRBDAIgB4AMCAgMfEAIMBDAIOCBgQYCIwQMCPYJTBAQI8BBwUHEoN/8P/IYN/+AvBj4LBBwOAj/7BwZGB/4ABBwXAAQIODM4QOFHgIOC/4OBh4OCAYJGBv4OCn4OBHgJKBAYJkBIQISBaIYhCCwIOBSoTqBJQISBeYUHd4U+bYUwcAYAKA"),
RE: image(51, 51, "//AAocf/AFDgf/CQl/8AFDh/8AocB/+AAwc/+AFDg/+GX4ECgwyEgPgGQk+GQkP+IyDC4IyE//3GQc//gyDh//GQYYB8YyD//4GQc//wyDDAOBGQUH//gGQRvB/BlD/4DBGQU/CwIyCj4YBMoQkBBIIyBBAIYBGQIkBDAIDBGgIiD+AFBGoIyBv4eCGQIABJwQvBAAJnDEgTLCEgY8CIYLLDEgZVCAoZuBb4iaBfAj+EgE4AokAA"),
RO: image(50, 47, "/4AEn4FE94FE/YFE/wYF34YS4A1BgIYB+A8Cv/v/gFCj4YBAoUHDH4Y/DEbglDBQ8CAAYA=="),
YU: image(59, 46, "gP/AAX+A4M/A4fggEHAwf8BwIGD/4GBj4VFgYVGv4HDwEAh4GD+A+Eg46CAAf/4AGEj/4Coo6CCqJFBCot/KAIADh5QCQAhQBCrM/Myk/M3JQGh5QFMyIRBAH6NB"),
YO: image(50, 49, "v//AAefAonnAon5Aon+DDA1DgP/wA8E8AFDj/4AocHDFZjfDCJjxDD5WE/+/AonvAon7PgoYX/g3DAAQ"),
};
const hiragana = {
// hiragana
A: image(52, 50, "gEB/wGEn/AAocD/gMcg//AAfgv4FD/wMYFIRNa54HDgYyCBgYsEBgX/+AGBHQYpBCQQaCh4JBJQPwgIdBBAP/wASB4H/j/8MIP8j5fBBIP/4P8gf+j/7/hVBj/jA4PH/C/Bn4RBv8Aj/3/Ef55FB/9/wI+D+/wj40BHwIWBL4QJB+BFBwAmB/4MBD4M/94MBD4JAB/4cBNYN/BgM//AsB/n/z4bBQgOHX4QVB/B3B/CQCAQTSC8BFCB4Q4CB4UAgIIBRQOAXojREn/gaIgAC"),
I:image(58, 50, "v/gAgUggEf/AGCnkAg/+AwU/gEB/+AAwQZBDgcP/gcECQIcFCQIJCCol/4AGBgYLBj/wCokHCAIABFAIQCCon/DgQECn4cDCoItCAAI+BDggVCLoZeB+BgCCocPPQZUBwZdDJAQcEGAIcEGAIcEGQPDDghIBDggyBDggyBx4cBjxIC8aaCCAIyBLAMDM4IyBSARnC//HUIk/+IyBCASdBLAJKCGQOf/kDJQV/GQRKCJ4XgEYRPC/CoCDgOHNwl/8P/84jCDgM//5HCDgMHAwIjBgP8DwIsBQgYVBSQgVBaYZnCTIgtBbQhDCUAYkCfwYOCGIgAHA"),
U: image(46, 50, "h//Aoc////8AFBAgIABgEDAofACwIAB/wWD//4CwgdBCIeAFQUfCwIADCwIAMj//+AEBv4tDAgQLBHAYFBAgf/8YFE54FECwRTB/wkCAoP7IAd/OgR2CKwcBQ4kH/hMEJYQcC4AWIh4WEn4tJg6EEj6EEVgIQDE4l/CAbABCAZqBBQgQDBQIQCXwIyCYYTIFeIhlCBQjxCLIQWBMgbdFvzYJ"),
E:image(55, 50, "gF//4GE/4AB+AFBgIGC/+AgEDAwYNBg4FC/wGBh4GC/gGF/ArFFIQAD4BRVn42FLAIGEJQYGBLAhEBLAhEBLAf/8ArDBIIyEj5fCRYZYEEgJYEN4JNFDQouFDQKcBFwYGFMIIGDLQRJFAwgaBOYQuC8Y2DFwODAwcP/0HXAc//EPcQnAj5LCPAU/MwR4Cv5ECPAQ9CLoUBd4auE/guBVwf5PARaC+5qCAwXnJwSXB//HI4QGCw5ACAwUHNIn+gj/HAAg"),
O: image(54, 50, "gEB/0AggGCg/4gE8AwUf8EA/gGCv+AB4QaDv/wDQn/CwIaCgP/4AaDgf/wAaCgPn/4PBAAXv/0HAwef/kfAoX+n/4v4GCAgPxCYfg/4jBAAWBGwQ1BgEDJoJQCJoJRBLYcPCAJrCgEcKAaGEHgSGDF4QPCJYYxCHoYMBn5YDBgoGBDIP8FQKiBDwabBFoIzCv/gEAJQCMwWfKAIbBh58BDQMH/l/4IaCh/xTgIaCn/P/BrD/8/4CGD/i3BDQfz/gaDv/P+AaCCAIaEHQQaDv/hGoV4h//g4VB8JnBa4ePZYRkBBwKNCbwPwCYR/C44CB4BtBfgSaD8ACBYQQWBAAYA=="),
KA: image(55, 49, "gEH/AGEh/wAwkf8AGEn/AAwl/wEAhgGC/4CBngCBgP+AQP8AwMDAYIyDAYUPAwQ2CAwY2Cj/4gP/AAP4j/wgYGC/gGBg4GC/0/8EPAwsfCgd/4E/Awt/FIf/LgJmBE4IGCMwMf8JjBHwIPB4IDBgZmBv+DAYMHMwP/BQRfBOwIKCL4J2BOIQvBAgJxCGQIEBHAKPCCwIYDCwQBBQoRGBviIDIQJRC4AdCXAYdCKIcHboQ/CboY4BboghBboZKCFAYhBjAoDh/8nzME+CfBF4V/RgP/EgKVBwYGBFAMH/zIBFAQeBAwIoDboRRD4DrBJQUHAQJsDAAwA="),
KI: image(48, 50, "AAMB+AFDh4FL/AFDg4FIn//AAX4ArpHC/xNEAov/LQgFCDgYAlF4UfPx8/g/8CoQbBKgQhCAoMDFAkHAoeAh4FEDgQAB4E/FgIUBwE/HwQdBn/gAoM+AoPAAoMMAohFCAqIpCgI7C4BEBI4oICAoZfE4C9BAob2EAoISCaQgACA="),
KU: image(33, 45, "AAsB4ADC+ADC/wDBgf/wADMg//CYIDDh4DDD4UfAY/8AY34AZRDCh4DCg4DCgYbCgI/CgH/BgU/BgREBBgIQB8AMCFIRNDLoJ2Cv42DJwQdDFQIdDFQQdDFQIdDHYRkDgYhCgADDnwDChyzE"),
KE: image(50, 49, "AAUB/0Ag/gAwN/wAICgEfBIIIBB4P4BAYPCh/wDAcD/gYE/4FBDAU/4AYEGIgOCDAQOBh//AAP+v+DAoX/7/AAof3+E/AoX9/gYD/9/gYFD/4YE/5QCGIJQDHYRvCJQU/N4JKCKAYYCKAQYWmAYEjwYEx6lDh/zUocDMgIYDv6cBKgUf/4yBBAMH/4eC4EBNQUfAQN/DYMPE4TjCAQQkCYgSJBDYLEBn7QCAQIbCE4UDDYP/PIV/CgLpD4EPP4UH+AkBAoIACCgIADh6LCAAMDAoYA=="),
KO: image(52, 50, "h//AAX+gAFD//gBgn/BgvwBiWAAon4GwUBDIQACCQQFCn//4AFCg4lBCQc/DwYfBKQJdEDwYAB8CIihAFEgJJDIgQFEg5KEMgITEj/8D4hwED4JqEOIIfEv5eEg4fEFg0PHIwsEBigmFCYkOv65CJYPnbgn+ZgIAD8IMFewvgCYjRBE4IMDegQABIoUfAoK7HA=="),
SA:image(51, 50, "AAMB/gFE/+AAwcf+AFDgf+DIl/4AFDg4fEgAfLgIfCj//AFQzCn/gLJYMELI5mEh6GGBgUHGAP4CAQ3COYILCBgUDIgYZBAoYmBn5REDwPgQQPgDAIVBj4fBJ4d+CQI1CgeAXhgSDKoYSEQQp1GQQpFBawXwD4IGBg42BaQngBgRlDBgmABgjzBRYZDCPIYvCv//MQoACA=="),
SI: image(45, 50, "v/AAgUD/wKDj/wAof/wAECg/8BQc/8AbD/4bE/AbEFgcHFgk/FgcBFgkPDYhIgFgIKDFh8eFgn+FgcH/4sDv+/FgUD/osDn/vFgQ2BFgcf+YsD/+fFgUP/gsDv/HFgSKBLId/8IsCHgIXBSod/EIIKBwIhCv/4h4WBAQOAv/+IIP8AQIAC4AYBAAIkBn4KDJQIKDCwYpBCwRWCAoJhDAoK1DAAg="),
SU: image(52, 50, "AAUf8AFDgP+BjH/AYP/AAnvAon+BjJAUgf9BgZFB/4MDn4kEg4MFGIwMED4QME+E/+AyC/x0DFgPABwIMC/gMGDIn8gYMFv/4EwcP/+AKYf/BgRACBgYRB/4mCgF/AwJ6DBgoTCRohNDTZE/VAkP/gFDE4PAUQhGCI4YeEUIgYBD4gMBEpI4GgIFEAAo"),
SE: image(56, 50, "AAcP/ADB//AAwP8AwkHA34FBAAn+A1JalmAGFvinFv4GF//PXghEBAwfBAwoNGEQP/+AGDn4GFh//8AGDg5PCgF/AYP/wAGEgj/CAwQADAw4mCAwZCCAAQ8BFQgGBAAQGBj4GFJQIGEJQIGEgYGFGIIGCIQQVDHQgACA"),
SO: image(53, 50, "gP/AAXggEPAweAgF/AoX+gEDBgfwgEfCYoFD/EAg4MFAAQMCAAQwBBhQpBJQozBAAU/IAIACIYJUBAAV//gsJD4IsEn4sEOAn+NIn/+4FEAA39AwvvAwqQDAAP7UYhmCx5bDuBVB4BCDg5bEJ4JoEgJ1EEQKCESwIFEg5vEEA4TFh4TFv4TGYgiLBCYrFG/5dDd4YHCOQKkBDQjbDDQQwDWgR5DAwSGEEAgAEA=="),
TA: image(52, 50, "gEP+AGE/4Mjgf/AAXAgE/AoX8BjUAgP+GYkf8AFDBhHnEIQMBEQQhBn/jFAWAgYMD/AMH/gMF4f/F4UH/kQGYd/KIIACg4VBBgmAQ4gMFUJcB/8DDQZgBv6iD/wuEn/gKIJGDEIl/4KCDC4KPE/+BBgYXBBgY5BAIImCj4MBTIKFB/wMBAAKSB8EPAwXnUYIMDCwLYD95RBEAIZCFQN/AwPBKISpBwEGQAgAGA=="),
TI: image(51, 49, "gED/wGEv/AAocP/AFDgP/CQk/8AFDg/8Bgn/wAFDj/wBQYAqJ4M/LBZrMJYZ+Ch5aDv/f/4bCBQIABCoMDHAYTBv4+Ej4MEg4DB4IMCAoIcCwE/TwU/+ASBEQI8BVQJLCv/gS4cP/kBMgYWBjyoEgLbJEYYSCQQkHCQg2EHASCEv4SBgYOBOQ70BQoYrBEQIABFYR/DJASRED4YFCBgJDDA="),
TU: image(59, 45, "AAUP/4FFAAIGCAoX//EAg4GD//ACYYAB/kBAwgOBn4OFDgoOBAYX+BYP8j4GBwEAAgPDGwQ+C/F/BgIABCwOMLQl/+AGEg/+NIv/8BwF/gGEKwIqDAAM/HAYzDEhkfEgsDEgxJGh5JFHQPACqQrBCpkfCopXBCogcBCog5BK4jSCAwxtDDYK8EZIQcCAoQcDCYTjCJgQGCEYT0DIAYGGEgQGDEgRcEv5UEA="),
TE: image(57, 50, "/4AFv4GF34GF74GF94GF+4GF/YGF/oGF/w7Cn//4BCDAwOAAwpQEj4ZDAxP8AyUPAwwiFg4GMgZFFAw0BLQqlBNAkAv4GG8AGEn/wKgv4KhZGGHALeGH4oxNh4xFOJBjGEYt/VQwVFg//BwhOBAAI7Dv4GBHYYcBCwgcB/5CEDgQyFGYgrCUwkPKAwAC"),
TO: image(46, 49, "gEH/AFDj/wAod/4AECgP/Cwn8C0cICwcDBoIWC/4NBCwMfEgV/4f/BoIWBv//LAMH/4AB8AWBAoWAgE/BQYlBDYUAh4FBHwQPEEIJQDFYJhCgYwCLQQqCDYQKDDYIKDn5xEEAYQB/x8JDYkDCAkPYIk/JoQWTAol/AocZQwR6B8aNCAAOPAgf+TIZqBAongT4QfCBYY9BW4R1BA="),
NA: image(55, 49, "gEP+AGEj/gAwk/4EAkAGCv+AgAPD/8AgYdCgP+EgkD/gdB/AGBg4DBv4GCj/w/wGCv////8AwQFB//4AwMBAwXwEQMDAwXgAwMHAwXAAwMPAwWAG4QvBLgQGBL4X/AwRfBKgIGCL4X8n/gLARUBn5YDMwM8NQaLBQYIoCAQSIDAQRZBRYaBDRYQhBFAIJCKIYyCDwKoBToZkBOAIJBPYKLCGwMH/h2CAwMfKoKKCI4PgSIYYB4afDJQMP/gpB+AhBMgIjB/AhC4EfAwIhCEoIGCwJdBaIIZBMgSkCjhMBgakBG4LICUgKDBAwQuBPgRKCjgGE4EQAwgEBAAIbBRAQACQgIDB"),
NI: image(50, 50, "h+AAocD/gFDgP/CQl/4AFDn/gv//AAOP/E/AoXj/0HAoX4/+BAoX+DAuf+EfAoXn/gYD/P/gYEBG48f+AFDg5QMMYkf8BvE/BvE/wYE/4YEKAIYYgZSCDAMBJgQYCCgYDBFoYDBj4tCDAJlDDAMBGYYYBNYYYBn4xCg/4h6ECPgIHBPgfBDwaVBQgYvBToYYCFYauBaIIwB5/wcAfz/0PAoX8cAn/IgQFC55dBAoXxFILtC/grBGgL5BYIoAGA=="),
NU: image(58, 50, "AAV/4AGEj/wAwkH/gGEgP/Aod+Dgv/wAcEj/gDgkH/AcEgP+Dgt/Dg3wn4mBHwYGBDAIyCAwP/8AGBAoQODh4GC/4sBgYGD/AcCAAO/IQQcC4IkCDgI7Bj5YBg//w/8EAIjCwIEBv/gMQPgLAMPFYP//h1BgZpC/4LCNwIxB4YoBFoIxB/AjBNIMH/v+n5UB/4qBn/fIoIJBv+PLYUPQwPhOIUD/gvBGYMH/3/BAX/457CBAP/84GBDgIlB/YGBCYJwB/qECDgKREwBCC34YBDgfvLYP+HIM/+YYCIwM/MoIYB/hGBMoQEBz4nBKQfDAwODGQXwKQQMB/P4j4GBAQP+ngtBUgIRBg6aBRwKiBwOAf4TNBAobjCAogAEA"),
NE: image(57, 50, "gEP+AGEg/4AwkD/gGEgP+Dgv/Awt/wAGEn/Agf/BIUf8EP/40CHAMf/4tBAYP4AQImBCIP8n4GB4EH//+AwXgEwP/v4CB/EBAYIPBg4jBAwX8BYJFBCQRKDFYIGBJQJxBIgUfAQIrBAYMPCAIfBBQR8CAwR8DMAZ8Cv4GCGIQGDGIU/AwR8BAwKqCWoU/FoS1Cj4tCHASEBWogGBUAQKBAwItBHARpB8BlBBQKuCAQIKBO4SqCBQX8AwX4h/9/wGC/kP/n/DYSlCv+P/ArB4K+B4/4SIV+j/jWIX8n0P+JSBDoMOMwJWBAwOCMwM//ZOCMwI4C75nB/5bC45nBv+DAwPhTgXAb4PAoCfCQQifBYoYAHA"),
NO: image(60, 50, "AAX//4GEv4HFj4GB/wGCg4GB//4AwMBAwX/4AcEDwcPAwYWBgYGDCwQVC54tCCoX8F4PgFYP4CYI+BgE//0P/gaB/ARB4F/4ApBwAVBg4OBj/8EgITB4AiB4InBBwQgBCAIOCPQPjD4MPJ4MH/0/+ALBwARB84kBBwQ0Bv/gBwc/+5bBj5tEHAR8Bn5lBBwInBBxY2CBwcDWIQOEGwIODJwIOFIoRKC4CNCBQP3AgKwCDIIOBKIQKB8/8IQJgBj4OB8E/MAfD/ytBEgX8J4KeBZwWDIgJCBCoP4ZgIzCAYIqBeYRQB8DnCK4gGBGoIDBwAyBF4IKCCQWBAwIVBEoPgF4RFBg/4F4Q2BAAQOBTwIADHoQADbIQAIA"),
HA: image(55, 50, "AAd/wEAn4CBgH/BIXAgEB/wJEgf8AQIJCg/4AQIJBgEP+ACBBIMAj/gAQYsBEoIoCGwf/GwkB/8P/4AC4f+j4GDw/4n4GDj/wv4FC/0/8AMD/l/4IGD/H/wYGD+P/g4vELARtCMQRtDMQQKDL4YKCMQQKDMQQKDR4QKCTIYKCFYQ2bOoI2C4BgCGwWASAQ2BGQKJC8DNBBAIAB+DNBPYf4ZoKrDAgPwT4K7BAwRdBB4K3BVYIqCVYY6BAwKrB/0DVY3+v/hAwf8n4SBdIXwnxEBAwXgnBEBAwShBO4IbBSYSVCOYQAHA"),
HI: image(57, 50, "AAMPwAGE//gAocf//wgFwgEH////kH/AZBAwP+gf+Bof/wP/gEDAwWAAIMBAwc/FgIGDj4sBv4GBE4P8HAIdBE4IqBAwYgBKAIGCKAYKBAwN/EYIGDn4jBAwZfBDAQfBLIPAAwZZBDgItENYN/CAIfBIAIGCLIRfDLIXwAwc/RQJmCHAPv/0PEoI4B+f/AwcH/P/w50D/l/wZ0CgP+j/BK4Q4Bg/gJoQ4BwIGBIwU/4EwAQI4CIYICCAYY/EJQMHHATcCbAQKEHARGBGgQqBCIc/D4IGDaITCDT4PAAQJfCQQRYDeQQGDSIIGEYYIGEE4IGEDgYFCcAQ+CGQZsCABAA="),
HU: image(58, 50, "gEP/AGEgf//wHE/4ABAwc/AwIPDh4OC8AGBg4GCEwUBAwX8Dod/EgoHC4AsF+BJFjAGDg4iEFgRfF/+AAwk/IwQjDFIgjDvAjDMYJlCgRHB4ABBFIUf/ABBFIXH/0HCoUf+BcBLwQpBCogpBCYIVDv+ACohNBn/wCoRxBCohNCMoIVBOIQVBAIJNCCAIVCEYIQBCoOAb4QtDCAQtC/gjCdIIXCN4QwBC4SVBDQIXBEYUP/gXBI4QEBHwPD/8ODgR/CwZNCCYN/8P/5/4GQOf+DtBKgXv/jtBKgX5/0PAwJxB/0/DAL8CvkDJYP/IYMMgFgg//fot/VYQACgYGFAAoA=="),
HE: image(67, 45, "AAXwA43/4AHFn/8A4sPCA0B//+CAt///gA4kfCA0H/4QGA4IyFn4IBGQg5BIYsD//nCAt//F/CAkf/wzBCAYFBwH//BaE8ArBwBzFCAgNBLoQQCHIPADYIQD/6dBCAk/OQIQEHIQQEHIQkCCARaBO4YUCSYQQDHIQQFHIQQERQgQCLQQQEHIKBDCAPAn5fDCAP8gbNECAaJDCAbVECAPgvj+Gg72GdoqYFCAgHFKIoQDDA0AKIjODDA0ARYQAEhwHGAAIA=="),
HO: image(53, 49, "h4GFv4FEg/4kAGDn/D/4ACwP+j4FC/kf+IMD8H/w4GDEAM/AoQEB4IMD4f+g4FCEoPwGIXggH/wEAgP/IIP8KQX4B4PAKQXAgP+AoMDAYMPEAQkC/+DEIIkBEAJVD/8/8IFD/P/h4GD5/wv5IDv+DBgfz/gTEz/gCYf4KIIABGgRRBLIZVDNIJVDNIRVDNIRlBNIZlCKwIDC+EDGYJpCwClCNIQMCCYIwBBgX8GAIBBJwRIBPofwJAIeBLwKCBBwIiCx/4H4IVCv/BFYIFB/f+KYIMCx6RD94YBwLfDwYTBGYV8LgJICgI5CBgUCgaGBLYQACAwLVBgA"),
MA: image(50, 49, "AAMH/gFDgP/Bgl/4AFDj/wDBsH/4AD/oFE/9/AwoARJVXhAon4JQn+j4MEw4YLn4YEJTIfCAooYCAoX4DgQwCwBdEBgMDHoYMB//3Bgd/8AUC4A7BJQP//kHBwQGB4JYBFoX8KgMP/gGBz/+h//AIPjGAXA//wAoXwh/4DgX4gP8IgQnCF4QFBgOAEIKIEv6SCAAIA=="),
MI: image(58, 49, "gP/AAOAA4V/AwPgAwUfAwP4AwUHAwP+DjAABgYcDDwYcDDwQcDDwQcFg/8gAXDDgMAn4XDv/Ah4XDj/wGgkPDgpQBDghPB+AcDMoXjDgQGCNwZsCNwYGEDgM/AwYcBPQQAC/kP/4IEw//MgIYC+f/wZHBCAP8//AGwMDEgKGBRAQVBz/4NYI2C44sBNYMP/PxFQI9BAQMY/+BFQKvCOoIsBEYKSCFQU/SQP8WYQCCGYIqCEwI0BFQQmBMgIDBJwOAfgXAAYItBRAJVCKIIVBAYN/FQIYBAYN/FoIrBTQSzCdgRfCAAg0BAAkfbwQACgY4BAAgGDA"),
MU: image(55, 49, "gED/gGEg/4AwkP+EAhwGCj/ggF+AwU/4EB/wGCv+Ag4GD/4kBAwM//4AB84GBv4GC54GBAoX/x/+gIGDh/+gYFC/0P/kHAwX8AwMPAwX4j5cCGwJOBAwJIDj5jBv4QCAwIpBNoU/+AiBNoIGCJYJtBAwPhFwPANQXjAwOAgEEv+P/A2C/H+CoI2BTIIhBwY2Bh/xwH+UgUf+CwBUgSgBBYKkCn/gh/gToI1B4Ef4AvCBIM/4ZmCIAN/44oBSgKdCFAJ3CLAY0BUgQoBGgIGBEIUPAwSID+AGBQIZHBJQRECd4Q9DI4QvBJwQ2Cj4sBGATRBJwLcDFgTcDC4QGEEILqEAwIbDIARoCBgQ"),
ME: image(55, 49, "AAUf+AGEn/gAwl/4AECBQP/wAYC4EB/4YDwED/wYDwEH/gGCCIMP/AFBgIRBGwcDCIN/GwUH/EP/4bCDAP/AAI2C+4GCHwMfAoX/JgM/AwYjBv4GI8YGCFoN/wIGBgYCBFwIiBHYJfBNAPAn/8IwIGBwAaBh/wAwOD//4R4IfBg//+B2BDoJKB+AoBg/+JQPjOwMP/n/z/nQIMf/IOB76BBn/3/gVBMgN/94nBOQX/7/gAwKbBOwSOCHoJMCEIMH/v/CAJxBh/7/hcCF4X4KYLEC5/wj5KBEIOfGwJRCL4PzF4V/JIQvBCYJJCH4JxB4AGB/xCCFQIJDDoIMBBIRNBAQJdCIwKUCeAb5CPgQACSgIFDSgIDC"),
MO: image(50, 49, "AAN+Aokf8AFDh/4AocD/wSE/+AAod/4AeE+AFDg/8CAf/AAX8j4FD/8HAonBAonwDBY3OKwkBKxc/N5M/GwcHh42D3/DAofn/AFD/P+DAf+v/PBgeP+YFD8f+NAuAG4axBU4ZaCKAUBOAJQDOYIYE+AYEVYKFCDAaICDASICDAsPDAQxBgYYBj4rBAoOAYQPwPQPgE4JYDRQo6BAoglBPoQ0CAogMCAoYvBIwQA="),
YA: image(53, 49, "AAVgAYUf4EPAoUB/8B/gGCg/4j/wAwU/4F/4ATDgf/BgUP/EPDQYRBn///wTBAQP//4OBCYMfAwP4CYPPAoP/8AnBAAeAh4FD/gMD/n/+ALD8H/z4EB/v/wf+CIUH/kP+4+CLoN/CYJhBCYmAgfwCYP7CYMeIwOcOoYiBBAOAPYXggZuCIwIrCTgQrCCYIMBFYP8gYZBC4Mf8B3CTQIPBQgYwBg4MDGAKYBGITABBgZnCL4QTCj5EFAAbUBAwgTBAoYTGYAITFcwQTPfQYTCTAITYMAQTDVgUAA="),
YU: image(51, 49, "AAV/4AFDh/4AocB/4DBj/ggE/AQMD/0Ag/8DgWAgH/AQMP+ASB//AgISBAoIDC4Ef///+ASBh4FB/4SBgYFC+E/4IFC/8H/F///9//g/8f/3/x/+j/nAQPwv/j/H/wf+I4N/KAJlBv+P9/4MoMP/f9/xlBAIIqBwAUBn/vFwIdBg40BNIIOBIIR7B+BbC8B7BKoX4uAyCAwM+GQX5//f8IyCn/z/hHCK4N/4/8h/8/4EB/4lBF4P/z5wB8f+RYJjBPoPAFwO/BQP4IQX/wJkCTAUfVYf4gf4BgS4BbQRiCcgbSCAAILEcALkCAAM/DoYeCC4ZLBfoIeD/ASEDAhoBAoYlBDwcAg/AAoY"),
YO: image(49, 49, "AAMP/AFDg/8AocD/wFDgP/DAn/wAFDv/AAoc/8AFDj/wGCH/AAIwDAAImCAoQmCv4FBEwU/AoImCj4FBEwUPAoJXCMO4wEM4IWDI4IwCKYQwCL4oFCDAQFDCQIXCNgQFDEoMP/iSC+EHEIJ5CAoSSCwYaBEwXhFoMf8Y4BEAJnBCYN/+Ef/AuBz41CLoPPUQd/4YFDj/AAocD4AuBPIXgDQJ/En6REA="),
RA: image(47, 49, "gEP4AFDn//Aod///wAoX///+AgMDAoP/DIMHAoX4AowjC//gh/4gIXCj4mBj4wBn/gEoP8GYI/CvAzBwAFBkAaBIgYTCAAUHGARcCJ4YrBFAJcD4AZDFAI/CFAMPJYQOBK4XwLgZdBJwIFDMIQFCQod/+AIBOIXzO4nnRIQRB55dDDYJdDHgQEBIgM/OgUD/0+Nof8jBtDOYk/OYgyDYgQhCPwLOCFoQ4DMwIcCPYSBCAATkECwKBDCwIVCFoQFCIgSNCHASNBGIQA=="),
RI: image(39, 49, "ngEDv+AAgX/AYUD/wGB4EH/EH//wh/wn4EBj/h/4EBn/HAgV/z4EB+P+v4EB8YCB4F/8//E4N/54VBFgIWB4AEB346BgP/v/8AgP+//4IQP9//ggBABC4UPAgJRBj4qCgBKBC4IwBF4QrBDgQrB/5vBgYcDEwIcCEwI5BEwP3EIU/94hCv/fEIImBn4+BRYKWCg/8EwSLBTQU/CwScCUYSoDj4zCBoIzCHoIuDKARjBJYJUCQAR7DQAQbDEASABbgU/BATqE"),
RU: image(51, 49, "gf/AAXAgF/AoX8gEPBgeAgIFD/EAn4MEg4FD8EACQoACn4lBAAUf/4FDDYOAAoQuBHwIACv/wDwgkEh/+DwoFDDw5ECDwRLDMwg5BLIZMBNgh/FGgIeB+AVB4AeBEYJmBBAJQBDgPBOocf/AoCVIU/Kwc/+5WDg/+Kwl/5/wh4mBh/4/A2CFgMOAoJDC8GBMgUHGAJQCCQKpCBgISBgf+SQMPCQN/4H/4YSBGIIwBCgMBDoTMCn/AEIROCLoKFEAIJvBTwZvCTAarFNIQFCXASyCYoYxBAoQ"),
RE: image(55, 49, "gEf8AGEn4GFv/AAwn/wAFDgP/BgkD/wGEg/8DoIkCh/4gf/+A2C+EPAwV///gAQIGB///4ICB+AuB/+PAQPgg4DBn4GE/wSB//AEoIABwABBj4FB/hODA4PwJwYgB4BOCHwROCNoQDBJwJtCLoM/PwJdBPYN/AQMPEoQvDDQMBBIV/DwMDF4QhCg4QBEIIlBh4QBLIIlBWoRRBWol/F4eAIYIlBMwR7BEoQQBUIYvCNgIlBF4SBBEoLsBHgI2DSwP9GwaWB+ZmEj/HGwIvCj+PFgKWBjk+RgSWB/E4Lgn4sBcCIII+CGwTjDWoZFBSYYRBYYgDBYYa5CLgIGBAAI"),
RO: image(50, 49, "AAf4gEB/4AC8EAv4FC/kAj4MDwEHAofwDAgSBDAoACn/+AocfAokP/4FDE4OAApED//AAohJBAAI5BAocAIQIFEHghFCD4QFCBoU/KIQMBNQZ9BOAhOCQYYFE/B8CE4QFBM4JGB4YuDj/7AocD/xIE/+fP4c/84FDh/8QoZyBj5mE4aFDn5yEDAIFDGIIFDIgIXDDwKREv4eEv4eBiAFCDwMH+A8BIQLnEEgLnDSooqBQYQFCDgQ2DAoolCJAQA="),
WA: image(54, 49, "gEf+AGEv/AAocB/4MEg/8DUv///Aj//wEDAwIcBAwMP//8BgIGBn//+IFBAwICB54GCDQQAC/0HAgXAn45BD4IDBn45Bv4MBAYPgGYJKCFAIbB8EAgf+DQRbEv/4LYYaBOQU/4EPCwIhCCYJrCgf8CYkP+BlBCYQaBv6GDOwQaECYIaEKwIaD4JWDgP+CYIaCg/4NQYTB8Z+BFwef+4aCMgN/74aCn/z/zXCIAOH/IaCh5CB44aBJoU+a4QyBwFwDQLGBCAOBX4adBGIJMBRIQaBUYI4CDQJnDFYJ7EDQKzCDQYECgA="),
WO: image(52, 49, "AAMf+AFDgP+Bgk/8AFDgYMM/gkD/4AC+EBAof/BkA5FhEAg45Cg/AgF/AQMBBIMP/4DB//gE4Xwn5dBn4GB74IBgY0Fv4FD8AfBAoYfB/gbBIAIiBg///A7B/+A/4rBCQIxBBAISB/ghBCQeBEoIMBCQI0BBgQSCDIYSB54MBgIlB+AMCj0H/0PBgIABHQQMBOgP4BgZBBBwTDCMYIMDKIIMRWQQmDAwUMYYqyBAoaxBN4IMEV4QMCcggMBWwbZCAweA"),
N: image(54, 49, "AAMHAwsf8AGE/+AAocD/wTF+AGEv/ACZUP/ATKgP/CYv8Awk/IQgTBIQkHCYxCFCYxWTIQxWGFAhCBAwkPAwJCE/5KDCYQiBhhCBAwJlBn+Aj/+/49BDoP/8IDBgf8IQIDBKgUf/EPLAJUBv/gn/AFgKZCAIMHCIP4DQSXBAIIaC/+BCIIaBYwKZCLwIuBCYLRCFwIKBEYX/CYUfEYP4TIRACCYQ+BwZUBDwIYBOgITCRAQVCEIP//0BYISjB+CtDUYRNBAwQ5Bg7gDBQIA="),
};
const keys = [
"A","I","U","E","O",
"HA","HI","HU","HE","HO",
"KA","KI","KU","KE","KO",
"MA","MI","MU","ME","MO",
"NA","NI","NU","NE","NO",
"RA","RI","RU","RE","RO",
"SA","SI","SU","SE","SO",
"TA","TI","TU","TE","TO",
"WA","WO","YO","YU","N",
];
let kana = katakana.KA; let kana = katakana.KA;
let scroll = 0; let scroll = 0;
// const keys = Object.keys(katakana).sort();
// console.log(keys);
let hiramode = false; let hiramode = false;
let curkana = 'KA'; let curkana = 'KA';
console.log("StartupTime: "+startupTime.diff());
function next () { function next () {
let found = false; const off = keys.indexOf(curkana);
for (const k of Object.keys(katakana).sort()) { if (off !== -1 && off + 1 < keys.length) {
if (found) { return keys[off + 1];
kana = hiramode ? hiragana[k] : katakana[k];
curkana = k;
return;
} }
if (curkana === k) { return keys[0];
found = true;
}
}
curkana = 'KA';
updateWatch(ohhmm);
} }
function randKana() { function randKana() {
try { try {
const keys = Object.keys(katakana); let index = 0 | (Math.random() * keys.length);
const total = keys.length;
let index = 0 | (Math.random() * total);
curkana = keys[index]; curkana = keys[index];
} catch (e) { } catch (e) {
randKana(); randKana();
} }
} }
// const bench = benchStart();
// console.log("-->" + bench.diff());
function prev () { function prev () {
let oldk = ''; const off = keys.indexOf(curkana);
let count = 0; if (off > 0) {
for (const k of Object.keys(katakana).sort()) { return keys[off - 1];
if (curkana === k) { }
if (count > 0) { return keys[keys.length - 1];
curkana = oldk; }
return; let color = 0;
const colors = [
() => g.setColor(0,1,0),
() => g.setColor(1,1,0),
() => g.setColor(0,1,1),
() => g.setColor(1,1,1),
// too dark
() => g.setColor(0,0,1),
() => g.setColor(0,0,0),
() => g.setColor(1,0,0),
];
function nextColor() {
if (color + 1 >= colors.length) {
color = 0;
} else {
color++;
} }
} }
oldk = k; function prevColor() {
count++; if (color < 1) {
color = colors.length - 1;
} else {
color--;
} }
curkana = oldk;
updateWatch(ohhmm);
} }
const kanacolors = { function render(hhmm) {
A: []
};
function updateWatch (hhmm) {
g.setFontAlign(-1, -1, 0); g.setFontAlign(-1, -1, 0);
g.setBgColor(0, 0, 0); g.setBgColor(0, 0, 0);
g.setColor(0, 0, 0); g.setColor(0, 0, 0);
var whitecolor = false; const whitecolor = color > 3;
if (curkana.indexOf('A') != -1) { colors[color]();
g.setColor(1, 0, 0); g.fillRect(0, 30, w, h);
whitecolor = true;
} else if (curkana.indexOf('I') != -1) {
g.setColor(0, 1, 0);
} else if (curkana.indexOf('U') != -1) {
g.setColor(0, 0, 1);
whitecolor = true;
} else if (curkana.indexOf('E') != -1) {
g.setColor(1, 1, 0);
} else {
g.setColor(0, 1, 1);
}
g.fillRect(0, 0, w, h);
g.setFont('Vector', 50); g.setFont('Vector', 50);
if (whitecolor) { if (whitecolor) {
@ -196,9 +228,9 @@ function updateWatch (hhmm) {
} }
g.drawString(hhmm, x, y - 1); g.drawString(hhmm, x, y - 1);
drawKana(4 + (g.getWidth() / 6), 60); drawKana();
drawMonthDay(); drawMonthDay();
Bangle.drawWidgets(); // Bangle.drawWidgets(); // :? always draw?
} }
function drawMonthDay() { function drawMonthDay() {
@ -220,19 +252,45 @@ function getPhoneme(k) {
return k; return k;
} }
var ohhmm = '';
var ypos = 0;
var xpos = 0;
var zpos = 1;
function drawKana (x, y) { function drawKana (x, y) {
if (!x) {
x = 4 + (g.getWidth() / 6);
}
if (!y) {
y = 40;
}
x += xpos;
y += ypos;
g.setColor(0, 0, 0); g.setColor(0, 0, 0);
g.fillRect(0, 0, g.getWidth(), 6 * (h / 8) + 1); g.fillRect(0, 30, g.getWidth(), 6 * (h / 8) + 1);
g.setColor(1, 1, 1); g.setColor(1, 1, 1);
x -= ((zpos) - 1)*50;
y -= (zpos - 1)*50;
kana = hiramode ? hiragana[curkana] : katakana[curkana]; kana = hiramode ? hiragana[curkana] : katakana[curkana];
g.drawImage(kana, x + 20, 40, { scale: 1.6 }); if (guard) {
g.setColor(0.8,0.8,0.8);
}
g.drawImage(kana, x + 20, y, { scale: 1.6 * zpos });
g.setColor(1, 1, 1); g.setColor(1, 1, 1);
g.setFont('Vector', 24); g.setFont('Vector', 24);
g.drawString(getPhoneme(curkana), 4, 32); g.drawString(getPhoneme(curkana), 4, 32);
g.drawString(hiramode ? 'H' : 'K', w - 20, 32); if (hiramode) {
g.setColor(0.2,0.2,0.2)
g.drawString('K', w - 20, 32);
g.setColor(1, 1, 1);
g.drawString('H', w - 20, 32+24);
} else {
g.setColor(1, 1, 1);
g.drawString('K', w - 20, 32);
g.setColor(0.2,0.2,0.2)
g.drawString('H', w - 20, 32+24);
}
// g.drawString(hiramode ? 'H' : 'K', w - 20, 32);
} }
var ohhmm = '';
function tickWatch () { function tickWatch () {
const now = Date(); const now = Date();
@ -243,27 +301,127 @@ function tickWatch () {
} }
const hhmm = zpad(now.getHours()) + ':' + zpad(now.getMinutes()); const hhmm = zpad(now.getHours()) + ':' + zpad(now.getMinutes());
if (hhmm !== ohhmm) { if (hhmm !== ohhmm) {
randKana();
updateWatch(hhmm);
ohhmm = hhmm; ohhmm = hhmm;
randKana();
render(hhmm);
} }
} }
let guard = false;
function hiraPush(d,dx) {
if (guard) {
return;
}
xpos = 0;
ypos = 0;
zpos = 1;
guard = true;
var count = 2;
function paint() {
count--;
if (count < 0) {
guard = false;
xpos = 0;
ypos = 0;
zpos = 1;
render(ohhmm);
return;
}
zpos -= 0.04;
render(ohhmm);
setTimeout(paint, 100);
}
setTimeout (paint, 5);
}
function hiraSwipe(d,dx, dostuff) {
if (guard) {
return;
}
if (dx) {
ypos = 0;
} else {
ypos = (d * 4);
}
xpos = 0;
guard = true;
var count = 2;
function paint() {
count--;
if (count < 0) {
if (dx) {
curkana = d>0?prev():next();
} else {
if (dostuff) {
hiramode = !hiramode;
}
}
guard = false;
xpos = 0;
ypos = 0;
render(ohhmm);
return;
}
if (dx) {
xpos += (8*d);
} else {
ypos -= (4*d);
}
render(ohhmm);
setTimeout(paint, 5);
}
setTimeout (paint, 5);
}
Bangle.on('touch', function (tap, top) { Bangle.on('touch', function (tap, top) {
if (top.x < w / 4) { if (top.y < (h / 1.5)) {
prev(); if (top.x > w /2) {
} else if (top.x > (w - (w / 4))) { //hiramode = !hiramode;
next(); if (hiramode) {
hiraSwipe(1,0, hiramode);
} else { } else {
hiramode = !hiramode; hiraSwipe(-1,0, !hiramode);
} }
kana = hiramode ? hiragana[curkana] : katakana[curkana]; } else {
updateWatch(ohhmm); hiraSwipe(1,1,1);
}
} else if (top.x < w / 2) {
nextColor();
hiraPush();
// curkana = prev();
} else {
prevColor();
hiraPush();
// curkana = next();
}
render(ohhmm);
});
Bangle.on('swipe', function (x,y) {
if (x > 0) {
// nextColor();
hiraSwipe(1, 1);
} else if (x < 0) {
// prevColor();
hiraSwipe(-1,1);
} else if (y < 0) {
hiraSwipe(1, 0, hiramode);
} else if (y > 0) {
hiraSwipe(-1, 0, !hiramode);
}
render(ohhmm);
}); });
g.clear(true); g.clear(true);
// show launcher when button pressed // show launcher when button pressed
Bangle.setUI('clock'); Bangle.setUI('clock');
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets();
// redraw widgets every 10 minutes
setInterval(function() {
// maybe not always necessary
Bangle.drawWidgets();
}, 1000 * 60 * 10);
tickWatch(); tickWatch();
setInterval(tickWatch, 1000 * 60); setInterval(tickWatch, 1000 * 60);

View File

@ -2,7 +2,7 @@
"id": "kanawatch", "id": "kanawatch",
"name": "Kanawatch", "name": "Kanawatch",
"shortName": "Kanawatch", "shortName": "Kanawatch",
"version": "0.07", "version": "0.11",
"type": "clock", "type": "clock",
"description": "Learn Hiragana and Katakana", "description": "Learn Hiragana and Katakana",
"icon": "app.png", "icon": "app.png",
@ -26,6 +26,9 @@
"screenshots": [ "screenshots": [
{ {
"url": "screenshot.png" "url": "screenshot.png"
},
{
"url": "screenshot2.png"
} }
] ]
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

7
apps/kbmatry/ChangeLog Normal file
View File

@ -0,0 +1,7 @@
1.00: New keyboard
1.01: Change swipe interface to taps, speed up responses (efficiency tweaks).
1.02: Generalize drawing and letter scaling. Allow custom and auto-generated character sets. Improve documentation.
1.03: Attempt to improve keyboard load time.
1.04: Make code asynchronous and improve load time.
1.05: Fix layout issue and rename library
1.06: Touch up readme, prep for IPO, add screenshots

119
apps/kbmatry/README.md Normal file
View File

@ -0,0 +1,119 @@
# Matryoshka Keyboard
![icon](icon.png)
![screenshot](screenshot.png) ![screenshot](screenshot6.png)
![screenshot](screenshot5.png) ![screenshot](screenshot2.png)
![screenshot](screenshot3.png) ![screenshot](screenshot4.png)
Nested key input utility.
## How to type
Press your finger down on the letter group that contains the character you would like to type, then tap the letter you
want to enter. Once you are touching the letter you want, release your
finger.
![help](help.png)
Press "shft" or "caps" to access alternative characters, including upper case letters, punctuation, and special
characters.
Pressing "shft" also reveals a cancel button if you would like to terminate input without saving.
Press "ok" to finish typing and send your text to whatever app called this keyboard.
Press "del" to delete the leftmost character.
## Themes and Colors
This keyboard will attempt to use whatever theme or colorscheme is being used by your Bangle device.
## How to use in a program
This was developed to match the interface implemented for kbtouch, kbswipe, etc.
In your app's metadata, add:
```json
"dependencies": {"textinput": "type"}
```
From inside your app, call:
```js
const textInput = require("textinput");
textInput.input({text: ""})
.then(result => {
console.log("The user entered: ", result);
});
```
Alternatively, if you want to improve the load time of the keyboard, you can pre-generate the data the keyboard needs
to function and render like so:
```js
const textInput = require("textinput");
const defaultKeyboard = textInput.generateKeyboard(textInput.defaultCharSet);
const defaultShiftKeyboard = textInput.generateKeyboard(textInput.defaultCharSetShift);
// ...
textInput.input({text: "", keyboardMain: defaultKeyboard, keyboardShift: defaultShiftKeyboard})
.then(result => {
console.log("The user entered: ", result);
// And it was faster!
});
```
This isn't required, but if you are using a large character set, and the user is interacting with the keyboard a lot,
it can really smooth the experience.
The default keyboard has a full set of alphanumeric characters as well as special characters and buttons in a
pre-defined layout. If your application needs something different, or you want to have a custom layout, you can do so:
```js
const textInput = require("textinput");
const customKeyboard = textInput.generateKeyboard([
["1", "2", "3", "4"], ["5", "6", "7", "8"], ["9", "0", ".", "-"], "ok", "del", "cncl"
]);
// ...
textInput.input({text: "", keyboardMain: customKeyboard})
.then(result => {
console.log("The user entered: ", result);
// And they could only enter numbers, periods, and dashes!
});
```
This will give you a keyboard with six buttons. The first three buttons will open up a 2x2 keyboard. The second three
buttons are special keys for submitting, deleting, and cancelling respectively.
Finally if you are like, super lazy, or have a dynamic set of keys you want to be using at any given time, you can
generate keysets from strings like so:
```js
const textInput = require("textinput");
const customKeyboard = textInput.generateKeyboard(createCharSet("ABCDEFGHIJKLMNOP", ["ok", "shft", "cncl"]));
const customShiftKeyboard = textInput.generateKeyboard(createCharSet("abcdefghijklmnop", ["ok", "shft", "cncl"]));
// ...
textInput.input({text: "", keyboardMain: customKeyboard, keyboardShift: customShiftKeyboard})
.then(result => {
console.log("The user entered: ", result);
// And the keyboard was automatically generated to include "ABCDEFGHIJKLMNOP" plus an OK button, a shift button, and a cancel button!
});
```
The promise resolves when the user hits "ok" on the input or if they cancel. If the user cancels, undefined is
returned, although the user can hit "OK" with an empty string as well. If you define a custom character set and
do not include the "ok" button your user will be soft-locked by the keyboard. Fair warning!
At some point I may add swipe-for-space and swipe-for-delete as well as swipe-for-submit and swipe-for-cancel
however I want to have a good strategy for the touch screen
[affordance](https://careerfoundry.com/en/blog/ux-design/affordances-ux-design/).
## Secret features
If you long press a key with characters on it, that will enable "Shift" mode.

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwcBkmSpICVz//ABARGCBIRByA/Dk+AAgUH8AECgP4kmRCwX4n+PAoXH8YEC+IRC4HguE4/+P/EfCIXwgARHn4RG+P/j4RDJwgRBGQIRIEYNxCIRECGpV/CIXAgY1P4/8v41JOgeOn4RDGo4jER5Y1FCJWQg4RDYpeSNIQAMkmTCBwRBz4IG9YRIyA8COgJHBhMgI4+QyVJAYJrC9Mkw5rHwFAkEQCImSCJvAhIRBpazFGo3HEYVJkIjGCIIUCAQu/CKGSGo4jPLIhHMNayPLYo6zBYozpH9MvdI+TfaGSv4KHCI+Qg4GDI4IABg5HGyIYENYIAB45rGyPACKIIDx/4gF/CIPx/8fCIY1F4H8CJPA8BtCa4I1DCJFxCIYXBCILXBGpXHGplwn5HPuE4NaH4n6PLyC6CgEnYpeSpICDdJYRFz4RQARQ"))

BIN
apps/kbmatry/help.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
apps/kbmatry/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

501
apps/kbmatry/lib.js Normal file
View File

@ -0,0 +1,501 @@
/**
* Attempt to lay out a set of characters in a logical way to optimize the number of buttons with the number
* of characters per button. Useful if you need to dynamically (or frequently) change your character set
* and don't want to create a layout for ever possible combination.
* @param text The text you want to parse into a character set.
* @param specials Any special buttons you want to add to the keyboard (must match hardcoded special string values)
* @returns {*[]}
*/
function createCharSet(text, specials) {
specials = specials || [];
const mandatoryExtraKeys = specials.length;
const preferredNumChars = [1, 2, 4, 6, 9, 12];
const preferredNumKeys = [4, 6, 9, 12].map(num => num - mandatoryExtraKeys);
let keyIndex = 0, charIndex = 0;
let keySpace = preferredNumChars[charIndex] * preferredNumKeys[keyIndex];
while (keySpace < text.length) {
const numKeys = preferredNumKeys[keyIndex];
const numChars = preferredNumChars[charIndex];
const nextNumKeys = preferredNumKeys[keyIndex];
const nextNumChars = preferredNumChars[charIndex];
if (numChars <= numKeys) {
charIndex++;
} else if ((text.length / nextNumChars) < nextNumKeys) {
charIndex++;
} else {
keyIndex++;
}
keySpace = preferredNumChars[charIndex] * preferredNumKeys[keyIndex];
}
const charsPerKey = preferredNumChars[charIndex];
let charSet = [];
for (let i = 0; i < text.length; i += charsPerKey) {
charSet.push(text.slice(i, i + charsPerKey)
.split(""));
}
charSet = charSet.concat(specials);
return charSet;
}
/**
* Given the width, height, padding (between chars) and number of characters that need to fit horizontally /
* vertically, this function attempts to select the largest font it can that will still fit within the bounds when
* drawing a grid of characters. Does not handle multi-letter entries well, assumes we are laying out a grid of
* single characters.
* @param width The total width available for letters (px)
* @param height The total height available for letters (px)
* @param padding The amount of space required between characters (px)
* @param gridWidth The number of characters wide the rendering is going to be
* @param gridHeight The number of characters high the rendering is going to be
* @returns {{w: number, h: number, font: string}}
*/
function getBestFont(width, height, padding, gridWidth, gridHeight) {
let font = "4x6";
let w = 4;
let h = 6;
const charMaxWidth = width / gridWidth - padding * gridWidth;
const charMaxHeight = height / gridHeight - padding * gridHeight;
if (charMaxWidth >= 6 && charMaxHeight >= 8) {
w = 6;
h = 8;
font = "6x8";
}
if (charMaxWidth >= 12 && charMaxHeight >= 16) {
w = 12;
h = 16;
font = "6x8:2";
}
if (charMaxWidth >= 12 && charMaxHeight >= 20) {
w = 12;
h = 20;
font = "12x20";
}
if (charMaxWidth >= 20 && charMaxHeight >= 20) {
font = "Vector" + Math.floor(Math.min(charMaxWidth, charMaxHeight));
const dims = g.setFont(font)
.stringMetrics("W");
w = dims.width
h = dims.height;
}
return {w, h, font};
}
/**
* Generate a set of key objects given an array of arrays of characters to make available for typing.
* @param characterArrays
* @returns {Promise<void>}
*/
function getKeys(characterArrays) {
if (Array.isArray(characterArrays)) {
return Promise.all(characterArrays.map((chars, i) => generateKeyFromChars(characterArrays, i)));
} else {
return generateKeyFromChars(characterArrays, 0);
}
}
/**
* Given a set of characters, determine whether or not this needs to be a matryoshka key, a basic key, or a special key.
* Then generate that key. If the key is a matryoshka key, we queue up the generation of its sub-keys for later to
* improve load times.
* @param chars
* @param i
* @returns {Promise<unknown>}
*/
function generateKeyFromChars(chars, i) {
return new Promise((resolve, reject) => {
let special;
if (!Array.isArray(chars[i]) && chars[i].length > 1) {
// If it's not an array we assume it's a string. Fingers crossed I guess, lol. Be nice to my functions!
special = chars[i];
}
const key = getKeyByIndex(chars, i, special);
if (!special) {
key.chars = chars[i];
}
if (key.chars.length > 1) {
key.pendingSubKeys = true;
key.getSubKeys = () => getKeys(key.chars);
resolve(key)
} else {
resolve(key);
}
})
}
/**
* Given a set of characters (or sets of characters) get the position and dimensions of the i'th key in that set.
* @param charSet An array where each element represents a key on the hypothetical keyboard.
* @param i The index of the key in the set you want to get dimensions for.
* @param special The special property of the key - for example "del" for a key used for deleting characters.
* @returns {{special, bord: number, pad: number, w: number, x: number, h: number, y: number, chars: *[]}}
*/
function getKeyByIndex(charSet, i, special) {
// Key dimensions
const keyboardOffsetY = 40;
const margin = 3;
const padding = 4;
const border = 2;
const gridWidth = Math.ceil(Math.sqrt(charSet.length));
const gridHeight = Math.ceil(charSet.length / gridWidth);
const keyWidth = Math.floor((g.getWidth()) / gridWidth) - margin;
const keyHeight = Math.floor((g.getHeight() - keyboardOffsetY) / gridHeight) - margin;
const gridx = i % gridWidth;
const gridy = Math.floor(i / gridWidth) % gridWidth;
const x = gridx * (keyWidth + margin);
const y = gridy * (keyHeight + margin) + keyboardOffsetY;
const w = keyWidth;
const h = keyHeight;
// internal Character spacing
const numChars = charSet[i].length;
const subGridWidth = Math.ceil(Math.sqrt(numChars));
const subGridHeight = Math.ceil(numChars / subGridWidth);
const bestFont = getBestFont(w - padding, h - padding, 0, subGridWidth, subGridHeight);
const letterWidth = bestFont.w;
const letterHeight = bestFont.h;
const totalWidth = (subGridWidth - 1) * (w / subGridWidth) + padding + letterWidth + 1;
const totalHeight = (subGridHeight - 1) * (h / subGridHeight) + padding + letterHeight + 1;
const extraPadH = (w - totalWidth) / 2;
const extraPadV = (h - totalHeight) / 2;
return {
x,
y,
w,
h,
pad : padding,
bord : border,
chars: [],
special,
subGridWidth,
subGridHeight,
extraPadH,
extraPadV,
font : bestFont.font
};
}
/**
* This is probably the most intense part of this keyboard library. If you don't do it ahead of time, it will happen
* when you call the keyboard, and it can take up to 0.5 seconds for a full alphanumeric keyboard. Depending on what
* is an acceptable user experience for you, and how many keys you are actually generating, you may choose to do this
* ahead of time and pass the result to the "input" function of this library. NOTE: This function would need to be
* called once per key set - so if you have a keyboard with a "shift" key you'd need to run it once for your base
* keyset and once for your shift keyset.
* @param charSets
* @returns {Promise<unknown>}
*/
function generateKeyboard(charSets) {
if (!Array.isArray(charSets)) {
// User passed a string. We will divvy it up into a real set of subdivided characters.
charSets = createCharSet(charSets, ["ok", "del", "shft"]);
}
return getKeys(charSets);
}
// Default layout
const defaultCharSet = [
["a", "b", "c", "d", "e", "f", "g", "h", "i"],
["j", "k", "l", "m", "n", "o", "p", "q", "r"],
["s", "t", "u", "v", "w", "x", "y", "z", "0"],
["1", "2", "3", "4", "5", "6", "7", "8", "9"],
[" ", "`", "-", "=", "[", "]", "\\", ";", "'"],
[",", ".", "/"],
"ok",
"shft",
"del"
];
// Default layout with shift pressed
const defaultCharSetShift = [
["A", "B", "C", "D", "E", "F", "G", "H", "I"],
["J", "K", "L", "M", "N", "O", "P", "Q", "R"],
["S", "T", "U", "V", "W", "X", "Y", "Z", ")"],
["!", "@", "#", "$", "%", "^", "&", "*", "("],
["~", "_", "+", "{", "}", "|", ":", "\"", "<"],
[">", "?"],
"ok",
"shft",
"del"
];
/**
* Given initial options, allow the user to type a set of characters and return their entry in a promise. If you do not
* submit your own character set, a default alphanumeric keyboard will display.
* @param options The object containing initial options for the keyboard.
* @param {string} options.text The initial text to display / edit in the keyboard
* @param {array[]|string[]} [options.keyboardMain] The primary keyboard generated with generateKeyboard()
* @param {array[]|string[]} [options.keyboardShift] Like keyboardMain, but displayed when shift / capslock is pressed.
* @returns {Promise<unknown>}
*/
function input(options) {
options = options || {};
let typed = options.text || "";
let resolveFunction = () => {};
let shift = false;
let caps = false;
let activeKeySet;
const offsetX = 0;
const offsetY = 40;
E.showMessage("Loading...");
let keyboardPromise;
if (options.keyboardMain) {
keyboardPromise = Promise.all([options.keyboardMain, options.keyboardShift || Promise.resolve([])]);
} else {
keyboardPromise = Promise.all([generateKeyboard(defaultCharSet), generateKeyboard(defaultCharSetShift)])
}
let mainKeys;
let mainKeysShift;
/**
* Draw an individual keyboard key - handles special formatting and the rectangle pad, followed by the character
* rendering.
* @param key
*/
function drawKey(key) {
let bgColor = g.theme.bg;
if (key.special) {
if (key.special === "ok") bgColor = "#0F0";
if (key.special === "cncl") bgColor = "#F00";
if (key.special === "del") bgColor = g.theme.bg2;
if (key.special === "spc") bgColor = g.theme.bg2;
if (key.special === "shft") {
bgColor = shift ? g.theme.bgH : g.theme.bg2;
}
if (key.special === "caps") {
bgColor = caps ? g.theme.bgH : g.theme.bg2;
}
g.setColor(bgColor)
.fillRect({x: key.x, y: key.y, w: key.w, h: key.h});
}
g.setColor(g.theme.fg)
.drawRect({x: key.x, y: key.y, w: key.w, h: key.h});
drawChars(key);
}
/**
* Draw the characters for a given key - this handles the layout of all characters needed for the key, whether the
* key has 12 characters, 1, or if it represents a special key.
* @param key
*/
function drawChars(key) {
const numChars = key.chars.length;
if (key.special) {
g.setColor(g.theme.fg)
.setFont("12x20")
.setFontAlign(-1, -1)
.drawString(key.special, key.x + key.w / 2 - g.stringWidth(key.special) / 2, key.y + key.h / 2 - 10, false);
} else {
g.setColor(g.theme.fg)
.setFont(key.font)
.setFontAlign(-1, -1);
for (let i = 0; i < numChars; i++) {
const gridX = i % key.subGridWidth;
const gridY = Math.floor(i / key.subGridWidth) % key.subGridWidth;
const charOffsetX = gridX * (key.w / key.subGridWidth);
const charOffsetY = gridY * (key.h / key.subGridHeight);
const posX = key.x + key.pad + charOffsetX + key.extraPadH;
const posY = key.y + key.pad + charOffsetY + key.extraPadV;
g.drawString(key.chars[i], posX, posY, false);
}
}
}
/**
* Get the key set corresponding to the indicated shift state. Allows easy switching between capital letters and
* lower case by just switching the boolean passed here.
* @param shift
* @returns {*[]}
*/
function getMainKeySet(shift) {
return shift ? mainKeysShift : mainKeys;
}
/**
* Draw all the given keys on the screen.
* @param keys
*/
function drawKeys(keys) {
keys.forEach(key => {
drawKey(key);
});
}
/**
* Draw the text that the user has typed so far, includes a cursor and automatic truncation when the string is too
* long.
* @param text
* @param cursorChar
*/
function drawTyped(text, cursorChar) {
let visibleText = text;
let ellipsis = false;
const maxWidth = 176 - 40;
while (g.setFont("12x20")
.stringWidth(visibleText) > maxWidth) {
ellipsis = true;
visibleText = visibleText.slice(1);
}
if (ellipsis) {
visibleText = "..." + visibleText;
}
g.setColor(g.theme.bg2)
.fillRect(5, 5, 171, 30);
g.setColor(g.theme.fg2)
.setFont("12x20")
.drawString(visibleText + cursorChar, 15, 10, false);
}
/**
* Clear the space on the screen that the keyboard occupies (not the text the user has written).
*/
function clearKeySpace() {
g.setColor(g.theme.bg)
.fillRect(offsetX, offsetY, 176, 176);
}
/**
* Based on a touch event, determine which key was pressed by the user.
* @param touchEvent
* @param keys
* @returns {*}
*/
function getTouchedKey(touchEvent, keys) {
return keys.find((key) => {
let relX = touchEvent.x - key.x;
let relY = touchEvent.y - key.y;
return relX > 0 && relX < key.w && relY > 0 && relY < key.h;
})
}
/**
* On a touch event, determine whether a key is touched and take appropriate action if it is.
* @param button
* @param touchEvent
*/
function keyTouch(button, touchEvent) {
const pressedKey = getTouchedKey(touchEvent, activeKeySet);
if (pressedKey == null) {
// User tapped empty space.
swapKeySet(getMainKeySet(shift !== caps));
return;
}
if (pressedKey.pendingSubKeys) {
// We have to generate the subkeys for this key still, but we decided to wait until we needed it!
pressedKey.pendingSubKeys = false;
pressedKey.getSubKeys()
.then(subkeys => {
pressedKey.subKeys = subkeys;
keyTouch(undefined, touchEvent);
})
return;
}
// Haptic feedback
Bangle.buzz(25, 1);
if (pressedKey.subKeys) {
// Hold press for "shift!"
if (touchEvent.type > 1) {
shift = !shift;
swapKeySet(getMainKeySet(shift !== caps));
} else {
swapKeySet(pressedKey.subKeys);
}
} else {
if (pressedKey.special) {
evaluateSpecialFunctions(pressedKey);
} else {
typed = typed + pressedKey.chars;
shift = false;
drawTyped(typed, "");
swapKeySet(getMainKeySet(shift !== caps));
}
}
}
/**
* Manage setting, generating, and rendering new keys when a key set is changed.
* @param newKeys
*/
function swapKeySet(newKeys) {
activeKeySet = newKeys;
clearKeySpace();
drawKeys(activeKeySet);
}
/**
* Determine if the key contains any of the special strings that have their own special behaviour when pressed.
* @param key
*/
function evaluateSpecialFunctions(key) {
switch (key.special) {
case "ok":
setTimeout(() => resolveFunction(typed), 50);
break;
case "del":
typed = typed.slice(0, -1);
drawTyped(typed, "");
break;
case "shft":
shift = !shift;
swapKeySet(getMainKeySet(shift !== caps));
break;
case "caps":
caps = !caps;
swapKeySet(getMainKeySet(shift !== caps));
break;
case "cncl":
setTimeout(() => resolveFunction(), 50);
break;
case "spc":
typed = typed + " ";
break;
}
}
let isCursorVisible = true;
const blinkInterval = setInterval(() => {
if (!activeKeySet) return;
isCursorVisible = !isCursorVisible;
if (isCursorVisible) {
drawTyped(typed, "_");
} else {
drawTyped(typed, "");
}
}, 200);
/**
* We return a promise but the resolve function is assigned to a variable in the higher function scope. That allows
* us to return the promise and resolve it after we are done typing without having to return the entire scope of the
* application within the promise.
*/
return new Promise((resolve, reject) => {
g.clear(true);
resolveFunction = resolve;
keyboardPromise.then((result) => {
mainKeys = result[0];
mainKeysShift = result[1];
swapKeySet(getMainKeySet(shift !== caps));
Bangle.setUI({
mode: "custom", touch: keyTouch
});
Bangle.setLocked(false);
})
}).then((result) => {
g.clearRect(Bangle.appRect);
clearInterval(blinkInterval);
Bangle.setUI();
return result;
});
}
exports.input = input;
exports.generateKeyboard = generateKeyboard;
exports.defaultCharSet = defaultCharSet;
exports.defaultCharSetShift = defaultCharSetShift;
exports.createCharSet = createCharSet;

View File

@ -0,0 +1,14 @@
{ "id": "kbmatry",
"name": "Matryoshka Keyboard",
"version":"1.06",
"description": "A library for text input via onscreen keyboard. Easily enter characters with nested keyboards.",
"icon": "icon.png",
"type":"textinput",
"tags": "keyboard",
"supports" : ["BANGLEJS2"],
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot6.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"},{"url":"screenshot5.png"},{"url": "help.png"}],
"readme": "README.md",
"storage": [
{"name":"textinput","url":"lib.js"}
]
}

BIN
apps/kbmatry/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -3,3 +3,4 @@
0.03: Use default Bangle formatter for booleans 0.03: Use default Bangle formatter for booleans
0.04: Allow moving the cursor 0.04: Allow moving the cursor
0.05: Switch swipe directions for Caps Lock and moving cursor. 0.05: Switch swipe directions for Caps Lock and moving cursor.
0.06: Add ability to auto-lowercase after a capital letter insertion.

View File

@ -12,6 +12,6 @@ Uses the multitap keypad logic originally from here: http://www.espruino.com/Mor
![](screenshot_2.png) ![](screenshot_2.png)
![](screenshot_3.png) ![](screenshot_3.png)
Written by: [Sir Indy](https://github.com/sir-indy) and [Thyttan](https://github.com/thyttan) Written by: [Sir Indy](https://github.com/sir-indy), [Thyttan](https://github.com/thyttan) and [bobrippling](https://github.com/bobrippling).
For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/)

View File

@ -9,6 +9,7 @@ exports.input = function(options) {
if (settings.firstLaunch===undefined) { settings.firstLaunch = true; } if (settings.firstLaunch===undefined) { settings.firstLaunch = true; }
if (settings.charTimeout===undefined) { settings.charTimeout = 500; } if (settings.charTimeout===undefined) { settings.charTimeout = 500; }
if (settings.showHelpBtn===undefined) { settings.showHelpBtn = true; } if (settings.showHelpBtn===undefined) { settings.showHelpBtn = true; }
if (settings.autoLowercase===undefined) { settings.autoLowercase = true; }
var fontSize = "6x15"; var fontSize = "6x15";
var Layout = require("Layout"); var Layout = require("Layout");
@ -89,19 +90,21 @@ exports.input = function(options) {
} }
function newCharacter(ch) { function newCharacter(ch) {
displayText(); displayText(false);
if (ch && textIndex < text.length) textIndex ++; if (ch && textIndex < text.length) textIndex ++;
charCurrent = ch; charCurrent = ch;
charIndex = 0; charIndex = 0;
} }
function onKeyPad(key) { function onKeyPad(key) {
var retire = 0;
deactivateTimeout(charTimeout); deactivateTimeout(charTimeout);
// work out which char was pressed // work out which char was pressed
if (key==charCurrent) { if (key==charCurrent) {
charIndex = (charIndex+1) % letters[charCurrent].length; charIndex = (charIndex+1) % letters[charCurrent].length;
text = text.slice(0, -1); text = text.slice(0, -1);
} else { } else {
retire = charCurrent !== undefined;
newCharacter(key); newCharacter(key);
} }
var newLetter = letters[charCurrent][charIndex]; var newLetter = letters[charCurrent][charIndex];
@ -110,12 +113,21 @@ exports.input = function(options) {
text = pre + (caps ? newLetter.toUpperCase() : newLetter.toLowerCase()) + post; text = pre + (caps ? newLetter.toUpperCase() : newLetter.toLowerCase()) + post;
if(retire)
retireCurrent();
// set a timeout // set a timeout
charTimeout = setTimeout(function() { charTimeout = setTimeout(function() {
charTimeout = undefined; charTimeout = undefined;
newCharacter(); newCharacter();
retireCurrent();
}, settings.charTimeout); }, settings.charTimeout);
displayText(charTimeout); displayText(true);
}
function retireCurrent(why) {
if (caps && settings.autoLowercase)
setCaps();
} }
var moveMode = false; var moveMode = false;

View File

@ -1,6 +1,6 @@
{ "id": "kbmulti", { "id": "kbmulti",
"name": "Multitap keyboard", "name": "Multitap keyboard",
"version":"0.05", "version":"0.06",
"description": "A library for text input via multitap/T9 style keypad", "description": "A library for text input via multitap/T9 style keypad",
"icon": "app.png", "icon": "app.png",
"type":"textinput", "type":"textinput",

View File

@ -3,6 +3,7 @@
var settings = require('Storage').readJSON("kbmulti.settings.json", true) || {}; var settings = require('Storage').readJSON("kbmulti.settings.json", true) || {};
if (settings.showHelpBtn===undefined) { settings.showHelpBtn = true; } if (settings.showHelpBtn===undefined) { settings.showHelpBtn = true; }
if (settings.charTimeout===undefined) { settings.charTimeout = 500; } if (settings.charTimeout===undefined) { settings.charTimeout = 500; }
if (settings.autoLowercase===undefined) { settings.autoLowercase = true; }
return settings; return settings;
} }
@ -21,6 +22,10 @@
format: v => v, format: v => v,
onchange: v => updateSetting("charTimeout", v), onchange: v => updateSetting("charTimeout", v),
}, },
/*LANG*/'Lowercase after first uppercase': {
value: !!settings().autoLowercase,
onchange: v => updateSetting("autoLowercase", v)
},
/*LANG*/'Show help button?': { /*LANG*/'Show help button?': {
value: !!settings().showHelpBtn, value: !!settings().showHelpBtn,
onchange: v => updateSetting("showHelpBtn", v) onchange: v => updateSetting("showHelpBtn", v)

View File

@ -5,3 +5,4 @@
0.05: Keep drag-function in ram, hopefully improving performance and input reliability somewhat. 0.05: Keep drag-function in ram, hopefully improving performance and input reliability somewhat.
0.06: Support input of numbers and uppercase characters. 0.06: Support input of numbers and uppercase characters.
0.07: Support input of symbols. 0.07: Support input of symbols.
0.08: Redone patterns a,e,m,w,z.

View File

@ -10,11 +10,11 @@ on the left of the IDE, then do a stroke and copy out the Uint8Array line
*/ */
exports.getStrokes = function(mode, cb) { exports.getStrokes = function(mode, cb) {
if (mode === exports.INPUT_MODE_ALPHA) { if (mode === exports.INPUT_MODE_ALPHA) {
cb("a", new Uint8Array([58, 159, 58, 155, 62, 144, 69, 127, 77, 106, 86, 90, 94, 77, 101, 68, 108, 62, 114, 59, 121, 59, 133, 61, 146, 70, 158, 88, 169, 107, 176, 124, 180, 135, 183, 144, 185, 152])); cb("a", new Uint8Array([31, 157, 33, 149, 37, 131, 42, 112, 46, 97, 49, 83, 52, 72, 56, 64, 59, 59, 63, 53, 68, 48, 74, 47, 80, 47, 88, 50, 98, 63, 109, 94, 114, 115, 116, 130, 117, 141]));
cb("b", new Uint8Array([51, 47, 51, 77, 56, 123, 60, 151, 65, 163, 68, 164, 68, 144, 67, 108, 67, 76, 72, 43, 104, 51, 121, 74, 110, 87, 109, 95, 131, 117, 131, 140, 109, 152, 88, 157])); cb("b", new Uint8Array([51, 47, 51, 77, 56, 123, 60, 151, 65, 163, 68, 164, 68, 144, 67, 108, 67, 76, 72, 43, 104, 51, 121, 74, 110, 87, 109, 95, 131, 117, 131, 140, 109, 152, 88, 157]));
cb("c", new Uint8Array([153, 62, 150, 62, 145, 62, 136, 62, 123, 62, 106, 65, 85, 70, 65, 75, 50, 82, 42, 93, 37, 106, 36, 119, 36, 130, 40, 140, 49, 147, 61, 153, 72, 156, 85, 157, 106, 158, 116, 158])); cb("c", new Uint8Array([153, 62, 150, 62, 145, 62, 136, 62, 123, 62, 106, 65, 85, 70, 65, 75, 50, 82, 42, 93, 37, 106, 36, 119, 36, 130, 40, 140, 49, 147, 61, 153, 72, 156, 85, 157, 106, 158, 116, 158]));
cb("d", new Uint8Array([57, 178, 57, 176, 55, 171, 52, 163, 50, 154, 49, 146, 47, 135, 45, 121, 44, 108, 44, 97, 44, 85, 44, 75, 44, 66, 44, 58, 44, 48, 44, 38, 46, 31, 48, 26, 58, 21, 75, 20, 99, 26, 120, 35, 136, 51, 144, 70, 144, 88, 137, 110, 124, 131, 106, 145, 88, 153])); cb("d", new Uint8Array([57, 178, 57, 176, 55, 171, 52, 163, 50, 154, 49, 146, 47, 135, 45, 121, 44, 108, 44, 97, 44, 85, 44, 75, 44, 66, 44, 58, 44, 48, 44, 38, 46, 31, 48, 26, 58, 21, 75, 20, 99, 26, 120, 35, 136, 51, 144, 70, 144, 88, 137, 110, 124, 131, 106, 145, 88, 153]));
cb("e", new Uint8Array([150, 72, 141, 69, 114, 68, 79, 69, 48, 77, 32, 81, 31, 85, 46, 91, 73, 95, 107, 100, 114, 103, 83, 117, 58, 134, 66, 143, 105, 148, 133, 148, 144, 148])); cb("e", new Uint8Array([107, 50, 101, 46, 94, 42, 85, 40, 75, 40, 65, 40, 58, 40, 51, 40, 47, 40, 44, 43, 45, 54, 52, 68, 63, 79, 70, 84, 70, 85, 59, 89, 52, 96, 45, 108, 39, 119, 37, 126, 37, 132, 37, 137, 41, 143, 48, 147, 60, 148, 69, 148, 78, 148, 84, 148, 89, 148]));
cb("f", new Uint8Array([157, 52, 155, 52, 148, 52, 137, 52, 124, 52, 110, 52, 96, 52, 83, 52, 74, 52, 67, 52, 61, 52, 57, 52, 55, 52, 52, 52, 52, 54, 52, 58, 52, 64, 54, 75, 58, 97, 59, 117, 60, 130])); cb("f", new Uint8Array([157, 52, 155, 52, 148, 52, 137, 52, 124, 52, 110, 52, 96, 52, 83, 52, 74, 52, 67, 52, 61, 52, 57, 52, 55, 52, 52, 52, 52, 54, 52, 58, 52, 64, 54, 75, 58, 97, 59, 117, 60, 130]));
cb("g", new Uint8Array([160, 66, 153, 62, 129, 58, 90, 56, 58, 57, 38, 65, 31, 86, 43, 125, 69, 152, 116, 166, 145, 154, 146, 134, 112, 116, 85, 108, 97, 106, 140, 106, 164, 106])); cb("g", new Uint8Array([160, 66, 153, 62, 129, 58, 90, 56, 58, 57, 38, 65, 31, 86, 43, 125, 69, 152, 116, 166, 145, 154, 146, 134, 112, 116, 85, 108, 97, 106, 140, 106, 164, 106]));
cb("h", new Uint8Array([58, 50, 58, 55, 58, 64, 58, 80, 58, 102, 58, 122, 58, 139, 58, 153, 58, 164, 58, 171, 58, 177, 58, 179, 58, 181, 58, 180, 58, 173, 58, 163, 59, 154, 61, 138, 64, 114, 68, 95, 72, 84, 80, 79, 91, 79, 107, 82, 123, 93, 137, 111, 145, 130, 149, 147, 150, 154, 150, 159])); cb("h", new Uint8Array([58, 50, 58, 55, 58, 64, 58, 80, 58, 102, 58, 122, 58, 139, 58, 153, 58, 164, 58, 171, 58, 177, 58, 179, 58, 181, 58, 180, 58, 173, 58, 163, 59, 154, 61, 138, 64, 114, 68, 95, 72, 84, 80, 79, 91, 79, 107, 82, 123, 93, 137, 111, 145, 130, 149, 147, 150, 154, 150, 159]));
@ -22,7 +22,7 @@ exports.getStrokes = function(mode, cb) {
cb("j", new Uint8Array([130, 57, 130, 61, 130, 73, 130, 91, 130, 113, 130, 133, 130, 147, 130, 156, 130, 161, 130, 164, 130, 166, 129, 168, 127, 168, 120, 168, 110, 168, 91, 167, 81, 167, 68, 167])); cb("j", new Uint8Array([130, 57, 130, 61, 130, 73, 130, 91, 130, 113, 130, 133, 130, 147, 130, 156, 130, 161, 130, 164, 130, 166, 129, 168, 127, 168, 120, 168, 110, 168, 91, 167, 81, 167, 68, 167]));
cb("k", new Uint8Array([149, 63, 147, 68, 143, 76, 136, 89, 126, 106, 114, 123, 100, 136, 86, 147, 72, 153, 57, 155, 45, 152, 36, 145, 29, 131, 26, 117, 26, 104, 27, 93, 30, 86, 35, 80, 45, 77, 62, 80, 88, 96, 113, 116, 130, 131, 140, 142, 145, 149, 148, 153])); cb("k", new Uint8Array([149, 63, 147, 68, 143, 76, 136, 89, 126, 106, 114, 123, 100, 136, 86, 147, 72, 153, 57, 155, 45, 152, 36, 145, 29, 131, 26, 117, 26, 104, 27, 93, 30, 86, 35, 80, 45, 77, 62, 80, 88, 96, 113, 116, 130, 131, 140, 142, 145, 149, 148, 153]));
cb("l", new Uint8Array([42, 55, 42, 59, 42, 69, 44, 87, 44, 107, 44, 128, 44, 143, 44, 156, 44, 163, 44, 167, 44, 169, 45, 170, 49, 170, 59, 169, 76, 167, 100, 164, 119, 162, 139, 160, 163, 159])); cb("l", new Uint8Array([42, 55, 42, 59, 42, 69, 44, 87, 44, 107, 44, 128, 44, 143, 44, 156, 44, 163, 44, 167, 44, 169, 45, 170, 49, 170, 59, 169, 76, 167, 100, 164, 119, 162, 139, 160, 163, 159]));
cb("m", new Uint8Array([49, 165, 48, 162, 46, 156, 44, 148, 42, 138, 42, 126, 42, 113, 43, 101, 45, 91, 47, 82, 49, 75, 51, 71, 54, 70, 57, 70, 61, 74, 69, 81, 75, 91, 84, 104, 94, 121, 101, 132, 103, 137, 106, 130, 110, 114, 116, 92, 125, 75, 134, 65, 139, 62, 144, 66, 148, 83, 151, 108, 155, 132, 157, 149])); cb("m", new Uint8Array([36, 139, 36, 120, 36, 99, 36, 79, 36, 61, 41, 45, 56, 43, 71, 46, 77, 66, 77, 93, 77, 97, 84, 69, 93, 51, 107, 47, 118, 53, 123, 79, 124, 115, 124, 140]));
cb("n", new Uint8Array([50, 165, 50, 160, 50, 153, 50, 140, 50, 122, 50, 103, 50, 83, 50, 65, 50, 52, 50, 45, 50, 43, 52, 52, 57, 67, 66, 90, 78, 112, 93, 131, 104, 143, 116, 152, 127, 159, 135, 160, 141, 150, 148, 125, 154, 96, 158, 71, 161, 56, 162, 49])); cb("n", new Uint8Array([50, 165, 50, 160, 50, 153, 50, 140, 50, 122, 50, 103, 50, 83, 50, 65, 50, 52, 50, 45, 50, 43, 52, 52, 57, 67, 66, 90, 78, 112, 93, 131, 104, 143, 116, 152, 127, 159, 135, 160, 141, 150, 148, 125, 154, 96, 158, 71, 161, 56, 162, 49]));
cb("o", new Uint8Array([107, 58, 104, 58, 97, 61, 87, 68, 75, 77, 65, 88, 58, 103, 54, 116, 53, 126, 55, 135, 61, 143, 75, 149, 91, 150, 106, 148, 119, 141, 137, 125, 143, 115, 146, 104, 146, 89, 142, 78, 130, 70, 116, 65, 104, 62])); cb("o", new Uint8Array([107, 58, 104, 58, 97, 61, 87, 68, 75, 77, 65, 88, 58, 103, 54, 116, 53, 126, 55, 135, 61, 143, 75, 149, 91, 150, 106, 148, 119, 141, 137, 125, 143, 115, 146, 104, 146, 89, 142, 78, 130, 70, 116, 65, 104, 62]));
cb("p", new Uint8Array([29, 47, 29, 55, 29, 75, 29, 110, 29, 145, 29, 165, 29, 172, 29, 164, 30, 149, 37, 120, 50, 91, 61, 74, 72, 65, 85, 61, 103, 61, 118, 63, 126, 69, 129, 76, 130, 87, 126, 98, 112, 108, 97, 114, 87, 116])); cb("p", new Uint8Array([29, 47, 29, 55, 29, 75, 29, 110, 29, 145, 29, 165, 29, 172, 29, 164, 30, 149, 37, 120, 50, 91, 61, 74, 72, 65, 85, 61, 103, 61, 118, 63, 126, 69, 129, 76, 130, 87, 126, 98, 112, 108, 97, 114, 87, 116]));
@ -32,10 +32,10 @@ exports.getStrokes = function(mode, cb) {
cb("t", new Uint8Array([45, 55, 48, 55, 55, 55, 72, 55, 96, 55, 120, 55, 136, 55, 147, 55, 152, 55, 155, 55, 157, 55, 158, 56, 158, 60, 156, 70, 154, 86, 151, 102, 150, 114, 148, 125, 148, 138, 148, 146])); cb("t", new Uint8Array([45, 55, 48, 55, 55, 55, 72, 55, 96, 55, 120, 55, 136, 55, 147, 55, 152, 55, 155, 55, 157, 55, 158, 56, 158, 60, 156, 70, 154, 86, 151, 102, 150, 114, 148, 125, 148, 138, 148, 146]));
cb("u", new Uint8Array([35, 52, 35, 59, 35, 73, 35, 90, 36, 114, 38, 133, 42, 146, 49, 153, 60, 157, 73, 158, 86, 156, 100, 152, 112, 144, 121, 131, 127, 114, 132, 97, 134, 85, 135, 73, 136, 61, 136, 56])); cb("u", new Uint8Array([35, 52, 35, 59, 35, 73, 35, 90, 36, 114, 38, 133, 42, 146, 49, 153, 60, 157, 73, 158, 86, 156, 100, 152, 112, 144, 121, 131, 127, 114, 132, 97, 134, 85, 135, 73, 136, 61, 136, 56]));
cb("v", new Uint8Array([36, 55, 37, 59, 40, 68, 45, 83, 51, 100, 58, 118, 64, 132, 69, 142, 71, 149, 73, 156, 76, 158, 77, 160, 77, 159, 80, 151, 82, 137, 84, 122, 86, 111, 90, 91, 91, 78, 91, 68, 91, 63, 92, 61, 97, 61, 111, 61, 132, 61, 150, 61, 162, 61])); cb("v", new Uint8Array([36, 55, 37, 59, 40, 68, 45, 83, 51, 100, 58, 118, 64, 132, 69, 142, 71, 149, 73, 156, 76, 158, 77, 160, 77, 159, 80, 151, 82, 137, 84, 122, 86, 111, 90, 91, 91, 78, 91, 68, 91, 63, 92, 61, 97, 61, 111, 61, 132, 61, 150, 61, 162, 61]));
cb("w", new Uint8Array([25, 46, 25, 82, 25, 119, 33, 143, 43, 153, 60, 147, 73, 118, 75, 91, 76, 88, 85, 109, 96, 134, 107, 143, 118, 137, 129, 112, 134, 81, 134, 64, 134, 55])); cb("w", new Uint8Array([35, 37, 35, 44, 35, 58, 35, 81, 35, 110, 35, 129, 39, 136, 45, 140, 51, 141, 60, 137, 70, 121, 76, 99, 78, 79, 78, 70, 78, 69, 83, 89, 89, 112, 93, 127, 97, 135, 102, 136, 108, 131, 115, 116, 119, 93, 122, 72, 123, 55, 123, 43]));
cb("x", new Uint8Array([56, 63, 56, 67, 57, 74, 60, 89, 66, 109, 74, 129, 85, 145, 96, 158, 107, 164, 117, 167, 128, 164, 141, 155, 151, 140, 159, 122, 166, 105, 168, 89, 170, 81, 170, 73, 169, 66, 161, 63, 141, 68, 110, 83, 77, 110, 55, 134, 47, 145])); cb("x", new Uint8Array([56, 63, 56, 67, 57, 74, 60, 89, 66, 109, 74, 129, 85, 145, 96, 158, 107, 164, 117, 167, 128, 164, 141, 155, 151, 140, 159, 122, 166, 105, 168, 89, 170, 81, 170, 73, 169, 66, 161, 63, 141, 68, 110, 83, 77, 110, 55, 134, 47, 145]));
cb("y", new Uint8Array([30, 41, 30, 46, 30, 52, 30, 63, 30, 79, 33, 92, 38, 100, 47, 104, 54, 107, 66, 105, 79, 94, 88, 82, 92, 74, 94, 77, 96, 98, 96, 131, 94, 151, 91, 164, 85, 171, 75, 171, 71, 162, 74, 146, 84, 130, 95, 119, 106, 113])); cb("y", new Uint8Array([30, 41, 30, 46, 30, 52, 30, 63, 30, 79, 33, 92, 38, 100, 47, 104, 54, 107, 66, 105, 79, 94, 88, 82, 92, 74, 94, 77, 96, 98, 96, 131, 94, 151, 91, 164, 85, 171, 75, 171, 71, 162, 74, 146, 84, 130, 95, 119, 106, 113]));
cb("z", new Uint8Array([29, 62, 35, 62, 43, 62, 63, 62, 87, 62, 110, 62, 125, 62, 134, 62, 138, 62, 136, 63, 122, 68, 103, 77, 85, 91, 70, 107, 59, 120, 50, 132, 47, 138, 43, 143, 41, 148, 42, 151, 53, 155, 80, 157, 116, 158, 146, 158, 163, 158])); cb("z", new Uint8Array([39, 38, 45, 38, 53, 38, 62, 38, 72, 38, 82, 38, 89, 38, 96, 38, 99, 39, 95, 48, 82, 68, 70, 87, 60, 100, 50, 117, 42, 132, 42, 140, 45, 143, 53, 143, 67, 143, 81, 143]));
cb("SHIFT", new Uint8Array([100, 160, 100, 50])); cb("SHIFT", new Uint8Array([100, 160, 100, 50]));
} else if (mode === exports.INPUT_MODE_NUM) { } else if (mode === exports.INPUT_MODE_NUM) {
cb("0", new Uint8Array([82, 50, 76, 50, 67, 50, 59, 50, 50, 51, 43, 57, 38, 68, 34, 83, 33, 95, 33, 108, 34, 121, 42, 136, 57, 148, 72, 155, 85, 157, 98, 155, 110, 149, 120, 139, 128, 127, 134, 119, 137, 114, 138, 107, 138, 98, 138, 88, 138, 77, 137, 71, 134, 65, 128, 60, 123, 58])); cb("0", new Uint8Array([82, 50, 76, 50, 67, 50, 59, 50, 50, 51, 43, 57, 38, 68, 34, 83, 33, 95, 33, 108, 34, 121, 42, 136, 57, 148, 72, 155, 85, 157, 98, 155, 110, 149, 120, 139, 128, 127, 134, 119, 137, 114, 138, 107, 138, 98, 138, 88, 138, 77, 137, 71, 134, 65, 128, 60, 123, 58]));
@ -210,7 +210,7 @@ exports.input = function(options) {
if (o.stroke!==undefined && o.xy.length >= 6 && isStrokeInside(R, o.xy)) { if (o.stroke!==undefined && o.xy.length >= 6 && isStrokeInside(R, o.xy)) {
var ch = o.stroke; var ch = o.stroke;
if (ch=="\b") text = text.slice(0,-1); if (ch=="\b") text = text.slice(0,-1);
else if (ch==="SHIFT") { shift=!shift; Bangle.drawWidgets(); } else if (ch==="SHIFT") { shift=!shift; WIDGETS.kbswipe.draw(); }
else text += shift ? ch.toUpperCase() : ch; else text += shift ? ch.toUpperCase() : ch;
} }
lastDrag = undefined; lastDrag = undefined;
@ -226,7 +226,7 @@ exports.input = function(options) {
shift = false; shift = false;
setupStrokes(); setupStrokes();
show(); show();
Bangle.drawWidgets(); WIDGETS.kbswipe.draw();
} }
Bangle.on('stroke',strokeHandler); Bangle.on('stroke',strokeHandler);
@ -239,7 +239,7 @@ exports.input = function(options) {
area:"tl", area:"tl",
width: 36, // 3 chars, 6*2 px/char width: 36, // 3 chars, 6*2 px/char
draw: function() { draw: function() {
g.reset(); g.reset().clearRect(this.x, this.y, this.x + this.width-1, this.y + 23);
g.setFont("6x8:2x3"); g.setFont("6x8:2x3");
g.setColor("#f00"); g.setColor("#f00");
if (input_mode === exports.INPUT_MODE_ALPHA) { if (input_mode === exports.INPUT_MODE_ALPHA) {
@ -251,6 +251,7 @@ exports.input = function(options) {
} }
} }
}; };
Bangle.drawWidgets();
return new Promise((resolve,reject) => { return new Promise((resolve,reject) => {
Bangle.setUI({mode:"custom", drag:e=>{ Bangle.setUI({mode:"custom", drag:e=>{

View File

@ -1,6 +1,6 @@
{ "id": "kbswipe", { "id": "kbswipe",
"name": "Swipe keyboard", "name": "Swipe keyboard",
"version":"0.07", "version":"0.08",
"description": "A library for text input via PalmOS style swipe gestures (beta!)", "description": "A library for text input via PalmOS style swipe gestures (beta!)",
"icon": "app.png", "icon": "app.png",
"type":"textinput", "type":"textinput",

View File

@ -4,3 +4,4 @@
0.04: Add masking widget input to other apps (using espruino/Espruino#2151), add a oversize option to increase the touch area. 0.04: Add masking widget input to other apps (using espruino/Espruino#2151), add a oversize option to increase the touch area.
0.05: Prevent drawing into app area. 0.05: Prevent drawing into app area.
0.06: Fix issue where .draw was being called by reference (not allowing widgets to be hidden) 0.06: Fix issue where .draw was being called by reference (not allowing widgets to be hidden)
0.07: Handle the swipe event that is generated when draging to change light intensity, so it doesn't trigger some other swipe handler.

View File

@ -2,7 +2,7 @@
"id": "lightswitch", "id": "lightswitch",
"name": "Light Switch Widget", "name": "Light Switch Widget",
"shortName": "Light Switch", "shortName": "Light Switch",
"version": "0.06", "version": "0.07",
"description": "A fast way to switch LCD backlight on/off, change the brightness and show the lock status. All in one widget.", "description": "A fast way to switch LCD backlight on/off, change the brightness and show the lock status. All in one widget.",
"icon": "images/app.png", "icon": "images/app.png",
"screenshots": [ "screenshots": [

View File

@ -165,13 +165,12 @@
w.changeValue(value, event.b); w.changeValue(value, event.b);
// masks this drag event by messing up the event handler // masks this drag event by messing up the event handler
// see https://github.com/espruino/Espruino/issues/2151 E.stopEventPropagation&&E.stopEventPropagation();
Bangle.removeListener("drag", w.dragListener);
Bangle["#ondrag"] = [w.dragListener].concat(Bangle["#ondrag"]);
// on touch release remove drag listener and reset drag status to indicate stopped drag action // on touch release remove drag listener and reset drag status to indicate stopped drag action
if (!event.b) { if (!event.b) {
Bangle.removeListener("drag", w.dragListener); Bangle.removeListener("drag", w.dragListener);
Bangle.removeListener("swipe", w.swipeListener);
w.dragStatus = "off"; w.dragStatus = "off";
} }
@ -181,6 +180,11 @@
value = undefined; value = undefined;
}, },
swipeListener: function(_,__) {
// masks this swipe event by messing up the event handler
E.stopEventPropagation&&E.stopEventPropagation();
},
// listener function // // listener function //
// touch listener for light control // touch listener for light control
touchListener: function(button, cursor) { touchListener: function(button, cursor) {
@ -197,12 +201,14 @@
Bangle.buzz(25); Bangle.buzz(25);
// check if drag is disabled // check if drag is disabled
if (w.dragDelay) { if (w.dragDelay) {
// add drag listener at first position // add drag and swipe listeners at respective first position
Bangle["#ondrag"] = [w.dragListener].concat(Bangle["#ondrag"]); Bangle["#ondrag"] = [w.dragListener].concat(Bangle["#ondrag"]);
Bangle["#onswipe"] = [w.swipeListener].concat(Bangle["#onswipe"]);
// set drag timeout // set drag timeout
w.dragStatus = setTimeout((w) => { w.dragStatus = setTimeout((w) => {
// remove drag listener // remove drag and swipe listeners
Bangle.removeListener("drag", w.dragListener); Bangle.removeListener("drag", w.dragListener);
Bangle.removeListener("swipe", w.swipeListener);
// clear drag timeout // clear drag timeout
if (typeof w.dragStatus === "number") clearTimeout(w.dragStatus); if (typeof w.dragStatus === "number") clearTimeout(w.dragStatus);
// reset drag status to indicate stopped drag action // reset drag status to indicate stopped drag action

View File

@ -91,3 +91,8 @@
0.66: Updated Navigation handling to work with new Gadgetbridge release 0.66: Updated Navigation handling to work with new Gadgetbridge release
0.67: Support for 'Ignore' for messages from Gadgetbridge 0.67: Support for 'Ignore' for messages from Gadgetbridge
Message view is now taller, and we use swipe left/right to dismiss messages rather than buttons Message view is now taller, and we use swipe left/right to dismiss messages rather than buttons
0.68: More navigation icons (for roundabouts)
0.69: More navigation icons (keep/uturn left/right)
Nav messages with '/' now get split on newlines
0.70: Handle nav messages from newer Gadgetbridge builds that output distance as a String
If we receive a 'music' message and we're in the messages app (but not showing a message) show music (#2814)

View File

@ -15,8 +15,9 @@
// a message // a message
require("messages").pushMessage({"t":"add","id":1575479849,"src":"Skype","title":"My Friend","body":"Hey! How's everything going?",positive:1,negative:1}) require("messages").pushMessage({"t":"add","id":1575479849,"src":"Skype","title":"My Friend","body":"Hey! How's everything going?",positive:1,negative:1})
// maps // maps
GB({t:"nav",src:"maps",title:"Navigation",instr:"High St towards Tollgate Rd",distance:966,action:"continue",eta:"08:39"}) GB({t:"nav",src:"maps",title:"Navigation",instr:"High St towards Tollgate Rd",distance:"966yd",action:"continue",eta:"08:39"})
GB({t:"nav",src:"maps",title:"Navigation",instr:"High St",distance:12345,action:"left_slight",eta:"08:39"}) GB({t:"nav",src:"maps",title:"Navigation",instr:"High St",distance:"12km",action:"left_slight",eta:"08:39"})
GB({t:"nav",src:"maps",title:"Navigation",instr:"Main St / I-29 ALT / Centerpoint Dr",distance:12345,action:"left_slight",eta:"08:39"})
// call // call
require("messages").pushMessage({"t":"add","id":"call","src":"Phone","title":"Bob","body":"12421312",positive:true,negative:true}) require("messages").pushMessage({"t":"add","id":"call","src":"Phone","title":"Bob","body":"12421312",positive:true,negative:true})
*/ */
@ -27,7 +28,7 @@ var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2";
var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2"; var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2";
var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4"; var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4";
var fontVLarge = g.getFonts().includes("6x15")?"12x20:2":"6x8:5"; var fontVLarge = g.getFonts().includes("6x15")?"12x20:2":"6x8:5";
var active; // active screen var active; // active screen (undefined/"list"/"music"/"map"/"message"/"scroller"/"settings")
var openMusic = false; // go back to music screen after we handle something else? var openMusic = false; // go back to music screen after we handle something else?
// hack for 2v10 firmware's lack of ':size' font handling // hack for 2v10 firmware's lack of ':size' font handling
try { try {
@ -67,7 +68,7 @@ var onMessagesModified = function(type,msg) {
} }
if (msg && msg.id=="music") { if (msg && msg.id=="music") {
if (msg.state && msg.state!="play") openMusic = false; // no longer playing music to go back to if (msg.state && msg.state!="play") openMusic = false; // no longer playing music to go back to
if (active!="music") return; // don't open music over other screens if ((active!=undefined) && (active!="list") && (active!="music")) return; // don't open music over other screens (but do if we're in the main menu)
} }
showMessage(msg&&msg.id); showMessage(msg&&msg.id);
}; };
@ -81,22 +82,36 @@ E.on("kill", saveMessages);
function showMapMessage(msg) { function showMapMessage(msg) {
active = "map"; active = "map";
var m, distance, street, target, img; var m, distance, street, target, img;
if (msg.distance!==undefined) if ("string"==typeof msg.distance) // new gadgetbridge
distance = msg.distance;
else if ("number"==typeof msg.distance) // 0.74 gadgetbridge
distance = require("locale").distance(msg.distance); distance = require("locale").distance(msg.distance);
if (msg.instr) { if (msg.instr) {
if (msg.instr.includes("towards") || msg.instr.includes("toward")) { var instr = msg.instr.replace(/\s*\/\s*/g," \/\n"); // convert slashes to newlines
m = msg.instr.split(/towards|toward/); if (instr.includes("towards") || instr.includes("toward")) {
m = instr.split(/towards|toward/);
target = m[0].trim(); target = m[0].trim();
street = m[1].trim(); street = m[1].trim();
}else }else
target = msg.instr; target = instr;
} }
if (msg.action=="continue") img = "EBgBAIABwAPgD/Af+D/8f/773/PPY8cDwAPAA8ADwAPAA8AAAAPAA8ADwAAAA8ADwAPA"; switch (msg.action) {
else if (msg.action=="left") img = "GhcBAYAAAPAAAHwAAD4AAB8AAA+AAAf//8P///x///+PAAPx4AA8fAAHD4ABwfAAcDwAHAIABwAAAcAAAHAAABwAAAcAAAHAAABwAAAc"; case "continue": img = "EBgBAIABwAPgD/Af+D/8f/773/PPY8cDwAPAA8ADwAPAA8AAAAPAA8ADwAAAA8ADwAPA";break;
else if (msg.action=="right") img = "GhcBAABgAAA8AAAPgAAB8AAAPgAAB8D///j///9///+/AAPPAAHjgAD44AB8OAA+DgAPA4ABAOAAADgAAA4AAAOAAADgAAA4AAAOAAAA"; case "left": img = "GhcBAYAAAPAAAHwAAD4AAB8AAA+AAAf//8P///x///+PAAPx4AA8fAAHD4ABwfAAcDwAHAIABwAAAcAAAHAAABwAAAcAAAHAAABwAAAc";break;
else if (msg.action=="left_slight") img = "ERgB//B/+D/8H4AP4Af4A74Bz4Dj4HD4OD4cD4AD4ADwADwADgAHgAPAAOAAcAA4ABwADgAH"; case "right": img = "GhcBAABgAAA8AAAPgAAB8AAAPgAAB8D///j///9///+/AAPPAAHjgAD44AB8OAA+DgAPA4ABAOAAADgAAA4AAAOAAADgAAA4AAAOAAAA";break;
else if (msg.action=="right_slight") img = "ERgBB/+D/8H/4APwA/gD/APuA+cD44Phw+Dj4HPgAeAB4ADgAPAAeAA4ABwADgAHAAOAAcAA"; case "left_slight": img = "ERgB//B/+D/8H4AP4Af4A74Bz4Dj4HD4OD4cD4AD4ADwADwADgAHgAPAAOAAcAA4ABwADgAH";break;
else if (msg.action=="finish") img = "HhsBAcAAAD/AAAH/wAAPB4AAeA4AAcAcAAYIcAA4cMAA48MAA4cMAAYAcAAcAcAAcA4AAOA4AAOBxjwHBzjwHjj/4Dnn/4B3P/4B+Pj4A8fj8Acfj8AI//8AA//+AA/j+AB/j+AB/j/A"; case "right_slight": img = "ERgBB/+D/8H/4APwA/gD/APuA+cD44Phw+Dj4HPgAeAB4ADgAPAAeAA4ABwADgAHAAOAAcAA";break;
case "keep_left": img = "ERmBAACAAOAB+AD+AP+B/+H3+PO+8c8w4wBwADgAHgAPAAfAAfAAfAAfAAeAAeAAcAA8AA4ABwADgA==";break;
case "keep_right": img = "ERmBAACAAOAA/AD+AP+A//D/fPueeceY4YBwADgAPAAeAB8AHwAfAB8ADwAPAAcAB4ADgAHAAOAAAA==";break;
case "uturn_left": img = "GRiBAAAH4AAP/AAP/wAPj8APAfAPAHgHgB4DgA8BwAOA4AHAcADsOMB/HPA7zvgd9/gOf/gHH/gDh/gBwfgA4DgAcBgAOAAAHAAADgAABw==";break;
case "uturn_right": img = "GRiBAAPwAAf+AAf/gAfj4AfAeAPAHgPADwHgA4DgAcBwAOA4AHAcBjhuB5x/A+57gP99wD/84A/8cAP8OAD8HAA4DgAMBwAAA4AAAcAAAA==";break;
case "finish": img = "HhsBAcAAAD/AAAH/wAAPB4AAeA4AAcAcAAYIcAA4cMAA48MAA4cMAAYAcAAcAcAAcA4AAOA4AAOBxjwHBzjwHjj/4Dnn/4B3P/4B+Pj4A8fj8Acfj8AI//8AA//+AA/j+AB/j+AB/j/A";break;
case "roundabout_left": img = "HBaCAAADwAAAAAAAD/AAAVUAAD/wABVVUAD/wABVVVQD/wAAVABUD/wAAVAAFT/////wABX/////8AAF//////AABT/////wABUP/AAD/AAVA/8AA/8AVAD/wAD//VQAP/AAP/1QAA/wAA/9AAADwAAD/AAAAAAAA/wAAAAAAAP8AAAAAAAD/AAAAAAAA/wAAAAAAAP8AAAAAAAD/AA=";break;
case "roundabout_right": img = "HRaCAAAAAAAA8AAAP/8AAP8AAD///AA/8AA////AA/8AP/A/8AA/8A/wAP8AA/8P8AA/////8/wAD///////AAD//////8AAP////8P8ABUAAP/A/8AVQAD/wA//1UAA/8AA//VAAP/AAA/9AAA/wAAAPwAAA8AAAA/AAAAAAAAD8AAAAAAAAPwAAAAAAAA/AAAAAAAAD8AAAAAAAAPwAAAAAAA=";break;
case "roundabout_straight": img = "EBuCAAADwAAAD/AAAD/8AAD//wAD///AD///8D/P8/z/D/D//A/wPzAP8AwA//UAA//1QA//9VA/8AFUP8AAVD8AAFQ/AABUPwAAVD8AAFQ/wABUP/ABVA//9VAD//VAAP/1AAAP8AAAD/AAAA/wAA==";break;
case "roundabout_uturn": img = "ICCBAAAAAAAAAAAAAAAAAAAP4AAAH/AAAD/4AAB4fAAA8DwAAPAcAADgHgAA4B4AAPAcAADwPAAAeHwAADz4AAAc8AAABPAAAADwAAAY8YAAPPPAAD73gAAf/4AAD/8AABf8AAAb+AAAHfAAABzwAAAcYAAAAAAAAAAAAAAAAAAAAAAA";break;
}
//FIXME: what about countries where we drive on the right? How will we know to flip the icons?
layout = new Layout({ type:"v", c: [ layout = new Layout({ type:"v", c: [
{type:"txt", font:street?fontMedium:fontLarge, label:target, bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:3 }, {type:"txt", font:street?fontMedium:fontLarge, label:target, bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:3 },
@ -202,7 +217,7 @@ function showMessageScroller(msg) {
var bodyFont = fontBig; var bodyFont = fontBig;
g.setFont(bodyFont); g.setFont(bodyFont);
var lines = []; var lines = [];
if (msg.title) lines = g.wrapString(msg.title, g.getWidth()-10) if (msg.title) lines = g.wrapString(msg.title, g.getWidth()-10);
var titleCnt = lines.length; var titleCnt = lines.length;
if (titleCnt) lines.push(""); // add blank line after title if (titleCnt) lines.push(""); // add blank line after title
lines = lines.concat(g.wrapString(msg.body, g.getWidth()-10),["",/*LANG*/"< Back"]); lines = lines.concat(g.wrapString(msg.body, g.getWidth()-10),["",/*LANG*/"< Back"]);
@ -390,6 +405,7 @@ function checkMessages(options) {
options=options||{}; options=options||{};
// If no messages, just show 'no messages' and return // If no messages, just show 'no messages' and return
if (!MESSAGES.length) { if (!MESSAGES.length) {
active=undefined; // no messages
if (!options.clockIfNoMsg) return E.showPrompt(/*LANG*/"No Messages",{ if (!options.clockIfNoMsg) return E.showPrompt(/*LANG*/"No Messages",{
title:/*LANG*/"Messages", title:/*LANG*/"Messages",
img:require("heatshrink").decompress(atob("kkk4UBrkc/4AC/tEqtACQkBqtUDg0VqAIGgoZFDYQIIM1sD1QAD4AIBhnqA4WrmAIBhc6BAWs8AIBhXOBAWz0AIC2YIC5wID1gkB1c6BAYFBEQPqBAYXBEQOqBAnDAIQaEnkAngaEEAPDFgo+IKA5iIOhCGIAFb7RqAIGgtUBA0VqobFgNVA")), img:require("heatshrink").decompress(atob("kkk4UBrkc/4AC/tEqtACQkBqtUDg0VqAIGgoZFDYQIIM1sD1QAD4AIBhnqA4WrmAIBhc6BAWs8AIBhXOBAWz0AIC2YIC5wID1gkB1c6BAYFBEQPqBAYXBEQOqBAnDAIQaEnkAngaEEAPDFgo+IKA5iIOhCGIAFb7RqAIGgtUBA0VqobFgNVA")),
@ -419,7 +435,7 @@ function checkMessages(options) {
// no new messages - go to clock? // no new messages - go to clock?
if (options.clockIfAllRead && newMessages.length==0) if (options.clockIfAllRead && newMessages.length==0)
return load(); return load();
active = "main"; active = "list";
// Otherwise show a menu // Otherwise show a menu
E.showScroller({ E.showScroller({
h : 48, h : 48,

View File

@ -2,7 +2,7 @@
"id": "messagegui", "id": "messagegui",
"name": "Message UI", "name": "Message UI",
"shortName": "Messages", "shortName": "Messages",
"version": "0.67", "version": "0.70",
"description": "Default app to display notifications from iOS and Gadgetbridge/Android", "description": "Default app to display notifications from iOS and Gadgetbridge/Android",
"icon": "app.png", "icon": "app.png",
"type": "app", "type": "app",

View File

@ -3,3 +3,4 @@
0.57: Optimize saving empty message list 0.57: Optimize saving empty message list
0.58: show/hide "messages" widget directly, instead of through library stub 0.58: show/hide "messages" widget directly, instead of through library stub
0.59: fixes message timeout by using setinterval, as it was intended. So the buzz is triggered every x seconds until the timeout occours. 0.59: fixes message timeout by using setinterval, as it was intended. So the buzz is triggered every x seconds until the timeout occours.
0.60: Bump version to allow new buzz.js module to be loaded - fixes memory/performance hog when buzz called

View File

@ -1,7 +1,7 @@
{ {
"id": "messages", "id": "messages",
"name": "Messages", "name": "Messages",
"version": "0.59", "version": "0.60",
"description": "Library to handle, load and store message events received from Android/iOS", "description": "Library to handle, load and store message events received from Android/iOS",
"icon": "app.png", "icon": "app.png",
"type": "module", "type": "module",

View File

@ -9,3 +9,4 @@
0.10: Improvements to help notifications work with themes 0.10: Improvements to help notifications work with themes
0.11: Fix regression that caused no notifications and corrupted background 0.11: Fix regression that caused no notifications and corrupted background
0.12: Add Bangle.js 2 support with Bangle.setLCDOverlay 0.12: Add Bangle.js 2 support with Bangle.setLCDOverlay
0.13: Add a default title background for the dark theme

View File

@ -2,7 +2,7 @@
"id": "notify", "id": "notify",
"name": "Notifications (default)", "name": "Notifications (default)",
"shortName": "Notifications", "shortName": "Notifications",
"version": "0.12", "version": "0.13",
"description": "Provides the default `notify` module used by applications to display notifications on the screen. This module is installed by default by client applications such as the Gadgetbridge app. Installing `Fullscreen Notifications` replaces this module with a version that displays the notifications using the full screen", "description": "Provides the default `notify` module used by applications to display notifications on the screen. This module is installed by default by client applications such as the Gadgetbridge app. Installing `Fullscreen Notifications` replaces this module with a version that displays the notifications using the full screen",
"icon": "notify.png", "icon": "notify.png",
"type": "notify", "type": "notify",

View File

@ -103,7 +103,7 @@ exports.show = function(options) {
b -= 2;h -= 2; b -= 2;h -= 2;
// title bar // title bar
if (options.title || options.src) { if (options.title || options.src) {
g.setColor(options.titleBgColor||0x39C7).fillRect(x,y, r,y+20); g.setColor("titleBgColor" in options ? options.titleBgColor : g.theme.dark ? 0x1 : 0x39C7).fillRect(x,y, r,y+20);
const title = options.title||options.src; const title = options.title||options.src;
g.setColor(g.theme.fg).setFontAlign(-1, -1, 0).setFont("6x8", 2); g.setColor(g.theme.fg).setFontAlign(-1, -1, 0).setFont("6x8", 2);
g.drawString(title.trim().substring(0, 13), x+25,y+3); g.drawString(title.trim().substring(0, 13), x+25,y+3);

View File

@ -100,7 +100,7 @@ exports.show = function(options) {
gg.clearRect(x,y, r,b); gg.clearRect(x,y, r,b);
// title bar // title bar
if (options.title || options.src) { if (options.title || options.src) {
gg.setColor(options.titleBgColor||0x39C7).fillRect(x,y, r,y+20); gg.setColor("titleBgColor" in options ? options.titleBgColor : g.theme.dark ? 0x1 : 0x39C7).fillRect(x,y, r,y+20);
const title = options.title||options.src; const title = options.title||options.src;
gg.setColor(g.theme.fg).setFontAlign(-1, -1, 0).setFont("6x8", 2); gg.setColor(g.theme.fg).setFontAlign(-1, -1, 0).setFont("6x8", 2);
gg.drawString(title.trim().substring(0, 13), x+25,y+3); gg.drawString(title.trim().substring(0, 13), x+25,y+3);

View File

@ -1,3 +1,4 @@
0.01: New App! 0.01: New App!
0.02: New Results menu item to show the formula and values used. 0.02: New Results menu item to show the formula and values used.
0.03: Adds haptics to Input screen and long press "C" to go back. 0.03: Adds haptics to Input screen and long press "C" to go back.
0.04: Fix font size not resetting on subsequent values in results screen

View File

@ -291,13 +291,11 @@ function calculateValue(calculatedVariable, variableValues) {
function drawResultScreen(result) { function drawResultScreen(result) {
let drawPage = function() { let drawPage = function() {
clearScreen(); clearScreen();
let fontSize = 30; // Initial font size
let lineSpacing = 15; // Space between lines
// Define the vertical positions of the titles // Define the vertical positions of the titles
let titlePositions = [10, 72, 132]; let titlePositions = [10, 72, 132];
let lineSpacing = 15; // Space between lines
for (let i = 0; i < result.result.length; i++) { for (let i = 0; i < result.result.length; i++) {
let fontSize = 30; // Initial font size
let currentResult = result.result[i]; let currentResult = result.result[i];
let resultTitle = currentResult[0]; let resultTitle = currentResult[0];
let resultValue = currentResult[1]; let resultValue = currentResult[1];

View File

@ -2,7 +2,7 @@
"id": "ohmcalc", "id": "ohmcalc",
"name": "Ohm's Law Calculator", "name": "Ohm's Law Calculator",
"shortName": "Ohm's Law Calc", "shortName": "Ohm's Law Calc",
"version": "0.03", "version": "0.04",
"description": "A smart and simple calculator for Ohm's Law calculations, designed specifically for Bangle.js 2 smartwatches. Handles voltage, current, resistance, and power calculations with smart logic to prevent invalid inputs.", "description": "A smart and simple calculator for Ohm's Law calculations, designed specifically for Bangle.js 2 smartwatches. Handles voltage, current, resistance, and power calculations with smart logic to prevent invalid inputs.",
"icon": "app.png", "icon": "app.png",
"type": "app", "type": "app",

View File

@ -16,6 +16,5 @@
], ],
"data": [ "data": [
{"name":"popcon.cache.json"} {"name":"popcon.cache.json"}
], ]
"sortorder": -10
} }

View File

@ -30,3 +30,6 @@
Altitude graphing now uses barometer altitude if it exists Altitude graphing now uses barometer altitude if it exists
plotTrack in widget allows track to be drawn in the background (doesn't block execution) plotTrack in widget allows track to be drawn in the background (doesn't block execution)
0.24: Can now specify `setRecording(true, {force:...` to not show a menu 0.24: Can now specify `setRecording(true, {force:...` to not show a menu
0.25: Widget now has `isRecording()` for retrieving recording status.
0.26: Now record filename based on date
0.27: Fix first ever recorded filename being log0 (now all are dated)

View File

@ -33,10 +33,8 @@ function updateSettings() {
function getTrackNumber(filename) { function getTrackNumber(filename) {
var trackNum = 0; var trackNum = 0;
var matches = filename.match(/^recorder\.log(.*)\.csv$/); var matches = filename.match(/^recorder\.log(.*)\.csv$/);
if (matches) { if (matches) return matches[1];
trackNum = parseInt(matches[1]||0); return 0;
}
return trackNum;
} }
function showMainMenu() { function showMainMenu() {
@ -62,23 +60,13 @@ function showMainMenu() {
WIDGETS["recorder"].setRecording(v).then(function() { WIDGETS["recorder"].setRecording(v).then(function() {
//print("Record start Complete"); //print("Record start Complete");
loadSettings(); loadSettings();
print("Recording: "+settings.recording); //print("Recording: "+settings.recording);
showMainMenu(); showMainMenu();
}); });
}, 1); }, 1);
} }
}, },
/*LANG*/'File #': { /*LANG*/'File' : {value:getTrackNumber(settings.file)},
value: getTrackNumber(settings.file),
min: 0,
max: 99,
step: 1,
onchange: v => {
settings.recording = false; // stop recording if we change anything
settings.file = "recorder.log"+v+".csv";
updateSettings();
}
},
/*LANG*/'View Tracks': ()=>{viewTracks();}, /*LANG*/'View Tracks': ()=>{viewTracks();},
/*LANG*/'Time Period': { /*LANG*/'Time Period': {
value: settings.period||10, value: settings.period||10,
@ -110,7 +98,7 @@ function viewTracks() {
var found = false; var found = false;
require("Storage").list(/^recorder\.log.*\.csv$/,{sf:true}).forEach(filename=>{ require("Storage").list(/^recorder\.log.*\.csv$/,{sf:true}).forEach(filename=>{
found = true; found = true;
menu[/*LANG*/"Track "+getTrackNumber(filename)] = ()=>viewTrack(filename,false); menu[/*LANG*/getTrackNumber(filename)] = ()=>viewTrack(filename,false);
}); });
if (!found) if (!found)
menu[/*LANG*/"No Tracks found"] = function(){}; menu[/*LANG*/"No Tracks found"] = function(){};

View File

@ -2,7 +2,7 @@
"id": "recorder", "id": "recorder",
"name": "Recorder", "name": "Recorder",
"shortName": "Recorder", "shortName": "Recorder",
"version": "0.24", "version": "0.27",
"description": "Record GPS position, heart rate and more in the background, then download to your PC.", "description": "Record GPS position, heart rate and more in the background, then download to your PC.",
"icon": "app.png", "icon": "app.png",
"tags": "tool,outdoors,gps,widget", "tags": "tool,outdoors,gps,widget",
@ -15,5 +15,8 @@
{"name":"recorder.wid.js","url":"widget.js"}, {"name":"recorder.wid.js","url":"widget.js"},
{"name":"recorder.settings.js","url":"settings.js"} {"name":"recorder.settings.js","url":"settings.js"}
], ],
"data": [{"name":"recorder.json","url":"app-settings.json"},{"wildcard":"recorder.log?.csv","storageFile":true}] "data": [
{"name":"recorder.json","url":"app-settings.json"},
{"wildcard":"recorder.log?.csv","storageFile":true}
]
} }

View File

@ -231,6 +231,8 @@
},getRecorders:getRecorders,reload:function() { },getRecorders:getRecorders,reload:function() {
reload(); reload();
Bangle.drawWidgets(); // relayout all widgets Bangle.drawWidgets(); // relayout all widgets
},isRecording:function() {
return !!writeInterval;
},setRecording:function(isOn, options) { },setRecording:function(isOn, options) {
/* options = { /* options = {
force : [optional] "append"/"new"/"overwrite" - don't ask, just do what's requested force : [optional] "append"/"new"/"overwrite" - don't ask, just do what's requested
@ -238,13 +240,14 @@
var settings = loadSettings(); var settings = loadSettings();
options = options||{}; options = options||{};
if (isOn && !settings.recording) { if (isOn && !settings.recording) {
var date=(new Date()).toISOString().substr(0,10).replace(/-/g,""), trackNo=10;
if (!settings.file) { // if no filename set if (!settings.file) { // if no filename set
settings.file = "recorder.log0.csv"; settings.file = "recorder.log" + date + trackNo.toString(36) + ".csv";
} else if (require("Storage").list(settings.file).length){ // if file exists } else if (require("Storage").list(settings.file).length){ // if file exists
if (!options.force) { // if not forced, ask the question if (!options.force) { // if not forced, ask the question
g.reset(); // work around bug in 2v17 and earlier where bg color wasn't reset g.reset(); // work around bug in 2v17 and earlier where bg color wasn't reset
return E.showPrompt( return E.showPrompt(
/*LANG*/"Overwrite\nLog " + settings.file.match(/\d+/)[0] + "?", /*LANG*/"Overwrite\nLog " + settings.file.match(/^recorder\.log(.*)\.csv$/)[1] + "?",
{ title:/*LANG*/"Recorder", { title:/*LANG*/"Recorder",
buttons:{/*LANG*/"Yes":"overwrite",/*LANG*/"No":"cancel",/*LANG*/"New":"new",/*LANG*/"Append":"append"} buttons:{/*LANG*/"Yes":"overwrite",/*LANG*/"No":"cancel",/*LANG*/"New":"new",/*LANG*/"Append":"append"}
}).then(selection=>{ }).then(selection=>{
@ -260,11 +263,12 @@
// wipe the file // wipe the file
require("Storage").open(settings.file,"r").erase(); require("Storage").open(settings.file,"r").erase();
} else if (options.force=="new") { } else if (options.force=="new") {
// new file - find the max log file number and add one // new file - use the current date
var maxNumber=0; var newFileName;
require("Storage").list(/recorder.log.*/).forEach( fn => maxNumber = Math.max(maxNumber, fn.match(/\d+/)[0]) ); do { // while a file exists, add one to the letter after the date
var newFileName = "recorder.log" + (maxNumber + 1) + ".csv"; newFileName = "recorder.log" + date + trackNo.toString(36) + ".csv";
// FIXME: use date? trackNo++;
} while (require("Storage").list(newFileName).length);
settings.file = newFileName; settings.file = newFileName;
} else throw new Error("Unknown options.force, "+options.force); } else throw new Error("Unknown options.force, "+options.force);
} }

View File

@ -36,25 +36,35 @@ function colorBandsToResistance(colorBands) {
} }
function resistanceToColorBands(resistance, tolerance) { function resistanceToColorBands(resistance, tolerance) {
let resistanceStr = resistance.toString();
let firstDigit, secondDigit, multiplier; let firstDigit, secondDigit, multiplier;
if (resistance < 1) {
// The resistance is less than 1, so we need to handle this case specially
let count = 0;
while (resistance < 1) {
resistance *= 10;
count++;
}
// Now, resistance is a whole number and count is how many times we had to multiply by 10
let resistanceStr = resistance.toString();
firstDigit = 0; // Set the first band color to be black
secondDigit = Number(resistanceStr.charAt(0)); // Set the second band color to be the significant digit
// Use count to determine the multiplier
multiplier = count === 1 ? 0.1 : 0.01;
} else {
// Convert the resistance to a string so we can manipulate it easily
let resistanceStr = resistance.toString();
if (resistanceStr.length === 1) { // Check if resistance is a single digit if (resistanceStr.length === 1) { // Check if resistance is a single digit
firstDigit = 0; firstDigit = 0;
secondDigit = Number(resistanceStr.charAt(0)); secondDigit = Number(resistanceStr.charAt(0));
multiplier = 0; multiplier = 1; // Set multiplier to 1 for single digit resistance values
} else if (resistance >= 100) { } else {
// Extract the first two digits from the resistance value // Extract the first two digits from the resistance value
firstDigit = Number(resistanceStr.charAt(0)); firstDigit = Number(resistanceStr.charAt(0));
secondDigit = Number(resistanceStr.charAt(1)); secondDigit = Number(resistanceStr.charAt(1));
// Calculate the multiplier // Calculate the multiplier by matching it directly with the length of digits
multiplier = resistanceStr.length - 2; multiplier = resistanceStr.length - 2 >= 0 ? Math.pow(10, resistanceStr.length - 2) : Math.pow(10, resistanceStr.length - 1);
} else { }
// For values between 10-99, shift the color to the first band
firstDigit = Number(resistanceStr.charAt(0));
secondDigit = Number(resistanceStr.charAt(1));
multiplier = 0;
} }
let firstBandEntry = Object.entries(colorData).find(function (entry) { let firstBandEntry = Object.entries(colorData).find(function (entry) {
return entry[1].value === firstDigit; return entry[1].value === firstDigit;
}); });
@ -64,7 +74,7 @@ function resistanceToColorBands(resistance, tolerance) {
}); });
let secondBand = secondBandEntry ? secondBandEntry[1].hex : undefined; let secondBand = secondBandEntry ? secondBandEntry[1].hex : undefined;
let multiplierBandEntry = Object.entries(colorData).find(function (entry) { let multiplierBandEntry = Object.entries(colorData).find(function (entry) {
return entry[1].multiplier === Math.pow(10, multiplier); return entry[1].multiplier === multiplier;
}); });
let multiplierBand = multiplierBandEntry ? multiplierBandEntry[1].hex : undefined; let multiplierBand = multiplierBandEntry ? multiplierBandEntry[1].hex : undefined;
let toleranceBandEntry = Object.entries(colorData).find(function (entry) { let toleranceBandEntry = Object.entries(colorData).find(function (entry) {

View File

@ -13,10 +13,12 @@
0.12: Fix for recorder not stopping at end of run. Bug introduced in 0.11 0.12: Fix for recorder not stopping at end of run. Bug introduced in 0.11
0.13: Revert #1578 (stop duplicate entries) as with 2v12 menus it causes other boxes to be wiped (fix #1643) 0.13: Revert #1578 (stop duplicate entries) as with 2v12 menus it causes other boxes to be wiped (fix #1643)
0.14: Fix Bangle.js 1 issue where after the 'overwrite track' menu, the start/stop button stopped working 0.14: Fix Bangle.js 1 issue where after the 'overwrite track' menu, the start/stop button stopped working
0.15: Diverge from the standard "Run" app. Swipe to intensity interface a la Karvonen (curtesy of FTeacher at https://github.com/f-teacher) 0.15: Keep run state between runs (allowing you to exit and restart the app)
Keep run state between runs (allowing you to exit and restart the app) 0.16: Added ability to resume a run that was stopped previously (fix #1907)
0.16: Don't clear zone 2b indicator segment when updating HRM reading. 0.17: Diverge from the standard "Run" app. Swipe to intensity interface a la Karvonen (curtesy of FTeacher at https://github.com/f-teacher)
0.18: Don't clear zone 2b indicator segment when updating HRM reading.
Write to correct settings file, fixing settings not working. Write to correct settings file, fixing settings not working.
0.17: Fix typo in variable name preventing starting a run. 0.19: Fix typo in variable name preventing starting a run
0.18: Tweak HRM min/max defaults. Extend min/max intervals in settings. Fix 0.20: Tweak HRM min/max defaults. Extend min/max intervals in settings. Fix
another typo. another typo.
0.21: Rebase on "Run" app ver. 0.16.

View File

@ -61,35 +61,46 @@ function setStatus(running) {
// Called to start/stop running // Called to start/stop running
function onStartStop() { function onStartStop() {
let running = !exs.state.active; var running = !exs.state.active;
let prepPromises = []; var shouldResume = false;
var promise = Promise.resolve();
if (running && exs.state.duration > 10000) { // if more than 10 seconds of duration, ask if we should resume?
promise = promise.
then(() => {
isMenuDisplayed = true;
return E.showPrompt("Resume run?",{title:"Run"});
}).then(r => {
isMenuDisplayed=false;shouldResume=r;
});
}
// start/stop recording // start/stop recording
// Do this first in case recorder needs to prompt for // Do this first in case recorder needs to prompt for
// an overwrite before we start tracking exstats // an overwrite before we start tracking exstats
if (settings.record && WIDGETS["recorder"]) { if (settings.record && WIDGETS["recorder"]) {
if (running) { if (running) {
isMenuDisplayed = true; isMenuDisplayed = true;
prepPromises.push( promise = promise.
WIDGETS["recorder"].setRecording(true).then(() => { then(() => WIDGETS["recorder"].setRecording(true, { force : shouldResume?"append":undefined })).
then(() => {
isMenuDisplayed = false; isMenuDisplayed = false;
layout.setUI(); // grab our input handling again layout.setUI(); // grab our input handling again
layout.forgetLazyState(); layout.forgetLazyState();
layout.render(); layout.render();
}) });
);
} else { } else {
prepPromises.push( promise = promise.then(
WIDGETS["recorder"].setRecording(false) () => WIDGETS["recorder"].setRecording(false)
); );
} }
} }
if (!prepPromises.length) // fix for Promise.all bug in 2v12 promise = promise.then(() => {
prepPromises.push(Promise.resolve());
Promise.all(prepPromises)
.then(() => {
if (running) { if (running) {
if (shouldResume)
exs.resume()
else
exs.start(); exs.start();
} else { } else {
exs.stop(); exs.stop();

View File

@ -1,7 +1,7 @@
{ {
"id": "runplus", "id": "runplus",
"name": "Run+", "name": "Run+",
"version": "0.18", "version": "0.21",
"description": "Displays distance, time, steps, cadence, pace and more for runners. Based on the Run app, but extended with additional screen for heart rate interval training.", "description": "Displays distance, time, steps, cadence, pace and more for runners. Based on the Run app, but extended with additional screen for heart rate interval training.",
"icon": "app.png", "icon": "app.png",
"tags": "run,running,fitness,outdoors,gps,karvonen,karvonnen", "tags": "run,running,fitness,outdoors,gps,karvonen,karvonnen",

View File

@ -86,31 +86,74 @@ function eventToAlarm(event, offsetMs) {
} }
function upload() { function upload() {
// kick off all the (active) timers
const now = new Date();
const currentTime = now.getHours()*3600000
+ now.getMinutes()*60000
+ now.getSeconds()*1000;
for (const alarm of alarms)
if (alarm.timer != undefined && alarm.on)
alarm.t = currentTime + alarm.timer;
Util.showModal("Saving..."); Util.showModal("Saving...");
Util.writeStorage("sched.json", JSON.stringify(alarms), () => { Util.writeStorage("sched.json", JSON.stringify(alarms), () => {
Puck.write(`\x10require("sched").reload();\n`, () => {
location.reload(); // reload so we see current data location.reload(); // reload so we see current data
}); });
});
} }
function renderAlarm(alarm, exists) { function renderAlarm(alarm, exists) {
const localDate = dateFromAlarm(alarm); const localDate = alarm.date ? dateFromAlarm(alarm) : null;
const tr = document.createElement('tr'); const tr = document.createElement('tr');
tr.classList.add('event-row'); tr.classList.add('event-row');
tr.dataset.uid = alarm.id; tr.dataset.uid = alarm.id;
const tdTime = document.createElement('td'); const tdType = document.createElement('td');
tr.appendChild(tdTime); tdType.type = "text";
tdType.classList.add('event-summary');
tr.appendChild(tdType);
const inputTime = document.createElement('input'); const inputTime = document.createElement('input');
if (localDate) {
tdType.textContent = "Event";
inputTime.type = "datetime-local"; inputTime.type = "datetime-local";
inputTime.classList.add('event-date');
inputTime.classList.add('form-input');
inputTime.dataset.uid = alarm.id;
inputTime.value = localDate.toISOString().slice(0,16); inputTime.value = localDate.toISOString().slice(0,16);
inputTime.onchange = (e => { inputTime.onchange = (e => {
const date = new Date(inputTime.value); const date = new Date(inputTime.value);
alarm.t = dateToMsSinceMidnight(date); alarm.t = dateToMsSinceMidnight(date);
alarm.date = formatDate(date); alarm.date = formatDate(date);
}); });
} else {
const [hours, mins, secs] = msToHMS(alarm.timer || alarm.t);
inputTime.type = "time";
inputTime.step = 1; // display seconds
inputTime.value = `${hours}:${mins}:${secs}`;
if (alarm.timer) {
tdType.textContent = "Timer";
inputTime.onchange = e => {
alarm.timer = hmsToMs(inputTime.value);
// alarm.t is set on upload
};
} else {
tdType.textContent = "Alarm";
inputTime.onchange = e => {
alarm.t = hmsToMs(inputTime.value);
};
}
}
if (!exists) {
const asterisk = document.createElement('sup');
asterisk.textContent = '*';
tdType.appendChild(asterisk);
}
inputTime.classList.add('event-date');
inputTime.classList.add('form-input');
inputTime.dataset.uid = alarm.id;
const tdTime = document.createElement('td');
tr.appendChild(tdTime);
tdTime.appendChild(inputTime); tdTime.appendChild(inputTime);
const tdSummary = document.createElement('td'); const tdSummary = document.createElement('td');
@ -130,13 +173,31 @@ function renderAlarm(alarm, exists) {
tdSummary.appendChild(inputSummary); tdSummary.appendChild(inputSummary);
inputSummary.onchange(); inputSummary.onchange();
const tdOptions = document.createElement('td');
tr.appendChild(tdOptions);
const onOffCheck = document.createElement('input');
onOffCheck.type = 'checkbox';
onOffCheck.checked = alarm.on;
onOffCheck.onchange = e => {
alarm.on = !alarm.on;
if (alarm.on) delete alarm.last;
};
const onOffIcon = document.createElement('i');
onOffIcon.classList.add('form-icon');
const onOff = document.createElement('label');
onOff.classList.add('form-switch');
onOff.appendChild(onOffCheck);
onOff.appendChild(onOffIcon);
tdOptions.appendChild(onOff);
const tdInfo = document.createElement('td'); const tdInfo = document.createElement('td');
tr.appendChild(tdInfo); tr.appendChild(tdInfo);
const buttonDelete = document.createElement('button'); const buttonDelete = document.createElement('button');
buttonDelete.classList.add('btn'); buttonDelete.classList.add('btn');
buttonDelete.classList.add('btn-action'); buttonDelete.classList.add('btn-action');
tdInfo.prepend(buttonDelete); tdInfo.appendChild(buttonDelete);
const iconDelete = document.createElement('i'); const iconDelete = document.createElement('i');
iconDelete.classList.add('icon'); iconDelete.classList.add('icon');
iconDelete.classList.add('icon-delete'); iconDelete.classList.add('icon-delete');
@ -150,12 +211,53 @@ function renderAlarm(alarm, exists) {
document.getElementById('upload').disabled = false; document.getElementById('upload').disabled = false;
} }
function msToHMS(ms) {
let secs = Math.floor(ms / 1000) % 60;
let mins = Math.floor(ms / 1000 / 60) % 60;
let hours = Math.floor(ms / 1000 / 60 / 60);
if (secs < 10) secs = "0" + secs;
if (mins < 10) mins = "0" + mins;
if (hours < 10) hours = "0" + hours;
return [hours, mins, secs];
}
function hmsToMs(hms) {
let [hours, mins, secs] = hms.split(":");
hours = Number(hours);
mins = Number(mins);
secs = Number(secs);
return ((hours * 60 + mins) * 60 + secs) * 1000;
}
function addEvent() {
const event = getAlarmDefaults();
renderAlarm(event);
alarms.push(event);
}
function addAlarm() { function addAlarm() {
const alarm = getAlarmDefaults(); const alarm = getAlarmDefaults();
delete alarm.date;
renderAlarm(alarm); renderAlarm(alarm);
alarms.push(alarm); alarms.push(alarm);
} }
function addTimer() {
const alarmDefaults = getAlarmDefaults();
const timer = {
timer: hmsToMs("00:00:30"),
t: 0,
on: alarmDefaults.on,
dow: alarmDefaults.dow,
last: alarmDefaults.last,
rp: alarmDefaults.rp,
vibrate: alarmDefaults.vibrate,
as: alarmDefaults.as,
};;
renderAlarm(timer);
alarms.push(timer);
}
function getData() { function getData() {
Util.showModal("Loading..."); Util.showModal("Loading...");
Util.readStorage('sched.json',data=>{ Util.readStorage('sched.json',data=>{
@ -164,10 +266,19 @@ function getData() {
Util.readStorage('sched.settings.json',data=>{ Util.readStorage('sched.settings.json',data=>{
schedSettings = JSON.parse(data || "{}") || {}; schedSettings = JSON.parse(data || "{}") || {};
Util.hideModal(); Util.hideModal();
alarms.sort((a, b) => {
let x;
x = !!b.date - !!a.date;
if(x) return x;
x = !!a.timer - !!b.timer;
if(x) return x;
return a.t - b.t;
});
alarms.forEach(alarm => { alarms.forEach(alarm => {
if (alarm.date) {
renderAlarm(alarm, true); renderAlarm(alarm, true);
}
}); });
}); });
}); });
@ -183,16 +294,27 @@ function onInit() {
<h4>Manage dated events</h4> <h4>Manage dated events</h4>
<div class="float-right"> <div class="float-right">
<button class="btn" onclick="addEvent()">
<i class="icon icon-plus"></i>
Event
</button>
<button class="btn" onclick="addAlarm()"> <button class="btn" onclick="addAlarm()">
<i class="icon icon-plus"></i> <i class="icon icon-plus"></i>
Alarm
</button>
<button class="btn" onclick="addTimer()">
<i class="icon icon-plus"></i>
Timer
</button> </button>
</div> </div>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th>Date</th> <th>Type</th>
<th>Date/Time</th>
<th>Summary</th> <th>Summary</th>
<th>On?</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>

View File

@ -2,3 +2,5 @@
0.02: Less time used during boot if disabled 0.02: Less time used during boot if disabled
0.03: Fixed some test data 0.03: Fixed some test data
0.04: Correct type of time attribute in gps to Date 0.04: Correct type of time attribute in gps to Date
0.05: Fix gps emulation interpolation
Add setting for log output

View File

@ -5,40 +5,56 @@ This allows to simulate sensor behaviour for development purposes
## Per Sensor settings: ## Per Sensor settings:
enabled: Enabled:
true or false * **true**
mode: * **false**
emulate: Completely craft events for this sensor
modify: Take existing events from real sensor and modify their data Mode:
name: * **emulate**: Completely craft events for this sensor
name of the emulation or modification mode * **modify**: Take existing events from real sensor and modify their data
power:
emulate: Simulate Bangle._PWR changes, but do not call real power function Name:
nop: Do nothing, ignore all power calls for this sensor but return true * name of the emulation or modification mode
passthrough: Just pass all power calls unmodified
on: Do not allow switching the sensor off, all calls are switching the real sensor on Power:
* **emulate**: Simulate Bangle._PWR changes, but do not call real power function
* **nop**: Do nothing, ignore all power calls for this sensor but return true
* **passthrough**: Just pass all power calls unmodified
* **on**: Do not allow switching the sensor off, all calls are switching the real sensor on
### HRM ### HRM
Modes: modify, emulate Modes:
* **modify**: Modify the original events from this sensor
* **emulate**: Create events simulating sensor activity
Modification: Modification:
bpmtrippled: Multiply the bpm value of the original HRM values with 3 * **bpmtrippled**: Multiply the bpm value of the original HRM values with 3
Emulation: Emulation:
sin: Calculate bpm changes by using sin * **sin**: Calculate bpm changes by using sin
### GPS ### GPS
Modes: emulate Modes:
* **emulate**
Emulation: Emulation:
staticfix: static complete fix with all values * **staticfix**: static complete fix with all values
route: A square route starting in the SW corner and moving SW->NW->NO->SW... * **route**: A square route starting in the SW corner and moving SW->NW->NO->SW... [Download as gpx](square.gpx)
routeFuzzy: Roughly the same square as route, but with 100m seqments with some variaton in course * **routeFuzzy**: Roughly the same square as route, but with 100m seqments with some variaton in course [Download as gpx](squareFuzzy.gpx)
nofix: All values NaN but time,sattelites,fix and fix == 0 * **nofix**: All values NaN but time,sattelites,fix and fix == 0
changingfix: A fix with randomly changing values * **changingfix**: A fix with randomly changing values
### Compass ### Compass
Modes: emulate Modes:
* **emulate**
Emulation: Emulation:
static: All values but heading are 1, heading == 0 * **static**: All values but heading are 1, heading == 0
rotate: All values but heading are 1, heading rotates 360° * **rotate**: All values but heading are 1, heading rotates 360°
# Creator
[halemmerich](https://github.com/halemmerich)

View File

@ -1,5 +1,6 @@
{ {
"enabled": false, "enabled": false,
"log": false,
"mag": { "mag": {
"enabled": false, "enabled": false,
"mode": "emulate", "mode": "emulate",

View File

@ -5,6 +5,7 @@ exports.enable = () => {
); );
let log = function(text, param) { let log = function(text, param) {
if (!settings.log) return;
let logline = new Date().toISOString() + " - " + "Sensortools - " + text; let logline = new Date().toISOString() + " - " + "Sensortools - " + text;
if (param) logline += ": " + JSON.stringify(param); if (param) logline += ": " + JSON.stringify(param);
print(logline); print(logline);
@ -138,63 +139,63 @@ exports.enable = () => {
function interpolate(a,b,progress){ function interpolate(a,b,progress){
return { return {
lat: a.lat * progress + b.lat * (1-progress), lat: b.lat * progress + a.lat * (1-progress),
lon: a.lon * progress + b.lon * (1-progress), lon: b.lon * progress + a.lon * (1-progress),
ele: a.ele * progress + b.ele * (1-progress) alt: b.alt * progress + a.alt * (1-progress)
} }
} }
function getSquareRoute(){ function getSquareRoute(){
return [ return [
{lat:"47.2577411",lon:"11.9927442",ele:2273}, {lat:47.2577411,lon:11.9927442,alt:2273},
{lat:"47.266761",lon:"11.9926673",ele:2166}, {lat:47.266761,lon:11.9926673,alt:2166},
{lat:"47.2667605",lon:"12.0059511",ele:2245}, {lat:47.2667605,lon:12.0059511,alt:2245},
{lat:"47.2577516",lon:"12.0059925",ele:1994} {lat:47.2577516,lon:12.0059925,alt:1994}
]; ];
} }
function getSquareRouteFuzzy(){ function getSquareRouteFuzzy(){
return [ return [
{lat:"47.2578455",lon:"11.9929891",ele:2265}, {lat:47.2578455,lon:11.9929891,alt:2265},
{lat:"47.258592",lon:"11.9923341",ele:2256}, {lat:47.258592,lon:11.9923341,alt:2256},
{lat:"47.2594506",lon:"11.9927412",ele:2230}, {lat:47.2594506,lon:11.9927412,alt:2230},
{lat:"47.2603323",lon:"11.9924949",ele:2219}, {lat:47.2603323,lon:11.9924949,alt:2219},
{lat:"47.2612056",lon:"11.9928175",ele:2199}, {lat:47.2612056,lon:11.9928175,alt:2199},
{lat:"47.2621002",lon:"11.9929817",ele:2182}, {lat:47.2621002,lon:11.9929817,alt:2182},
{lat:"47.2629025",lon:"11.9923915",ele:2189}, {lat:47.2629025,lon:11.9923915,alt:2189},
{lat:"47.2637828",lon:"11.9926486",ele:2180}, {lat:47.2637828,lon:11.9926486,alt:2180},
{lat:"47.2646733",lon:"11.9928167",ele:2191}, {lat:47.2646733,lon:11.9928167,alt:2191},
{lat:"47.2655617",lon:"11.9930357",ele:2185}, {lat:47.2655617,lon:11.9930357,alt:2185},
{lat:"47.2662862",lon:"11.992252",ele:2186}, {lat:47.2662862,lon:11.992252,alt:2186},
{lat:"47.2669305",lon:"11.993173",ele:2166}, {lat:47.2669305,lon:11.993173,alt:2166},
{lat:"47.266666",lon:"11.9944419",ele:2171}, {lat:47.266666,lon:11.9944419,alt:2171},
{lat:"47.2667579",lon:"11.99576",ele:2194}, {lat:47.2667579,lon:11.99576,alt:2194},
{lat:"47.2669409",lon:"11.9970579",ele:2207}, {lat:47.2669409,lon:11.9970579,alt:2207},
{lat:"47.2666562",lon:"11.9983128",ele:2212}, {lat:47.2666562,lon:11.9983128,alt:2212},
{lat:"47.2666027",lon:"11.9996335",ele:2262}, {lat:47.2666027,lon:11.9996335,alt:2262},
{lat:"47.2667245",lon:"12.0009395",ele:2278}, {lat:47.2667245,lon:12.0009395,alt:2278},
{lat:"47.2668457",lon:"12.002256",ele:2297}, {lat:47.2668457,lon:12.002256,alt:2297},
{lat:"47.2666126",lon:"12.0035373",ele:2303}, {lat:47.2666126,lon:12.0035373,alt:2303},
{lat:"47.2664554",lon:"12.004841",ele:2251}, {lat:47.2664554,lon:12.004841,alt:2251},
{lat:"47.2669461",lon:"12.005948",ele:2245}, {lat:47.2669461,lon:12.005948,alt:2245},
{lat:"47.2660877",lon:"12.006323",ele:2195}, {lat:47.2660877,lon:12.006323,alt:2195},
{lat:"47.2652729",lon:"12.0057552",ele:2163}, {lat:47.2652729,lon:12.0057552,alt:2163},
{lat:"47.2643926",lon:"12.0060123",ele:2131}, {lat:47.2643926,lon:12.0060123,alt:2131},
{lat:"47.2634978",lon:"12.0058302",ele:2095}, {lat:47.2634978,lon:12.0058302,alt:2095},
{lat:"47.2626129",lon:"12.0060759",ele:2066}, {lat:47.2626129,lon:12.0060759,alt:2066},
{lat:"47.2617325",lon:"12.0058188",ele:2037}, {lat:47.2617325,lon:12.0058188,alt:2037},
{lat:"47.2608668",lon:"12.0061784",ele:1993}, {lat:47.2608668,lon:12.0061784,alt:1993},
{lat:"47.2600155",lon:"12.0057392",ele:1967}, {lat:47.2600155,lon:12.0057392,alt:1967},
{lat:"47.2591203",lon:"12.0058233",ele:1949}, {lat:47.2591203,lon:12.0058233,alt:1949},
{lat:"47.2582307",lon:"12.0059718",ele:1972}, {lat:47.2582307,lon:12.0059718,alt:1972},
{lat:"47.2578014",lon:"12.004804",ele:2011}, {lat:47.2578014,lon:12.004804,alt:2011},
{lat:"47.2577232",lon:"12.0034834",ele:2044}, {lat:47.2577232,lon:12.0034834,alt:2044},
{lat:"47.257745",lon:"12.0021656",ele:2061}, {lat:47.257745,lon:12.0021656,alt:2061},
{lat:"47.2578682",lon:"12.0008597",ele:2065}, {lat:47.2578682,lon:12.0008597,alt:2065},
{lat:"47.2577082",lon:"11.9995526",ele:2071}, {lat:47.2577082,lon:11.9995526,alt:2071},
{lat:"47.2575917",lon:"11.9982348",ele:2102}, {lat:47.2575917,lon:11.9982348,alt:2102},
{lat:"47.2577401",lon:"11.996924",ele:2147}, {lat:47.2577401,lon:11.996924,alt:2147},
{lat:"47.257715",lon:"11.9956061",ele:2197}, {lat:47.257715,lon:11.9956061,alt:2197},
{lat:"47.2578996",lon:"11.9943081",ele:2228} {lat:47.2578996,lon:11.9943081,alt:2228}
]; ];
} }
@ -215,19 +216,20 @@ exports.enable = () => {
let interpSteps; let interpSteps;
if (settings.gps.name == "routeFuzzy"){ if (settings.gps.name == "routeFuzzy"){
route = getSquareRouteFuzzy(); route = getSquareRouteFuzzy();
interpSteps = 5; interpSteps = 74;
} else { } else {
route = getSquareRoute(); route = getSquareRoute();
interpSteps = 50; interpSteps = 740;
} }
let step = 0; let step = 0;
let routeIndex = 0; let routeIndex = 0;
modGps(() => { modGps(() => {
let newIndex = (routeIndex + 1)%route.length; let newIndex = (routeIndex + 1)%route.length;
let followingIndex = (routeIndex + 2)%route.length;
let result = { let result = {
"speed": Math.random() * 3 + 2, "speed": Math.random()*1 + 4.5,
"time": new Date(), "time": new Date(),
"satellites": Math.floor(Math.random()*5)+3, "satellites": Math.floor(Math.random()*5)+3,
"fix": 1, "fix": 1,
@ -235,31 +237,22 @@ exports.enable = () => {
}; };
let oldPos = route[routeIndex]; let oldPos = route[routeIndex];
if (step != 0){
oldPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,step/interpSteps));
}
let newPos = route[newIndex]; let newPos = route[newIndex];
if (step < interpSteps - 1){ let followingPos = route[followingIndex];
newPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,(step+1)%interpSteps/interpSteps)); let interpPos = interpolate(oldPos, newPos, E.clip(0,1,step/interpSteps));
}
if (step == interpSteps - 1){
let followingIndex = (routeIndex + 2)%route.length;
newPos = interpolate(route[newIndex], route[followingIndex], E.clip(0,1,1/interpSteps));
}
result.lat = oldPos.lat;
result.lon = oldPos.lon;
result.alt = oldPos.ele;
if (step > 0.5* interpSteps) {
result.course = bearing(interpPos, interpolate(newPos, followingPos, E.clip(0,1,(step-0.5*interpSteps)/interpSteps)));
} else {
result.course = bearing(oldPos, newPos); result.course = bearing(oldPos, newPos);
}
step++; step++;
if (step == interpSteps){ if (step == interpSteps){
routeIndex = (routeIndex + 1) % route.length; routeIndex = (routeIndex + 1) % route.length;
step = 0; step = 0;
} }
return result; return Object.assign(result, interpPos);
}); });
} else if (settings.gps.name == "nofix") { } else if (settings.gps.name == "nofix") {
modGps(() => { return { modGps(() => { return {
@ -281,6 +274,7 @@ exports.enable = () => {
let currentDir=1000; let currentDir=1000;
let currentAlt=500; let currentAlt=500;
let currentSats=5; let currentSats=5;
modGps(() => { modGps(() => {
currentLat += 0.01; currentLat += 0.01;
if (currentLat > 50) currentLat = 20; if (currentLat > 50) currentLat = 20;

View File

@ -2,7 +2,7 @@
"id": "sensortools", "id": "sensortools",
"name": "Sensor tools", "name": "Sensor tools",
"shortName": "Sensor tools", "shortName": "Sensor tools",
"version": "0.04", "version": "0.05",
"description": "Tools for testing and debugging apps that use sensor input", "description": "Tools for testing and debugging apps that use sensor input",
"icon": "icon.png", "icon": "icon.png",
"type": "bootloader", "type": "bootloader",

View File

@ -88,6 +88,12 @@
writeSettings("enabled",v); writeSettings("enabled",v);
}, },
}, },
'Log': {
value: !!settings.log,
onchange: v => {
writeSettings("log",v);
},
},
'GPS': ()=>{showSubMenu("GPS","gps",["nop", "staticfix", "nofix", "changingfix", "route", "routeFuzzy"],[]);}, 'GPS': ()=>{showSubMenu("GPS","gps",["nop", "staticfix", "nofix", "changingfix", "route", "routeFuzzy"],[]);},
'Compass': ()=>{showSubMenu("Compass","mag",["nop", "static", "rotate"],[]);}, 'Compass': ()=>{showSubMenu("Compass","mag",["nop", "static", "rotate"],[]);},
'HRM': ()=>{showSubMenu("HRM","hrm",["nop", "static"],["bpmtrippled"],["sin"]);} 'HRM': ()=>{showSubMenu("HRM","hrm",["nop", "static"],["bpmtrippled"],["sin"]);}

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="OsmAndRouterV2" xmlns="http://www.topografix.com/GPX/1/1" xmlns:osmand="https://osmand.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<metadata>
<name>1kmsquare</name>
<desc>Export from GpsPrune</desc>
</metadata>
<trk>
<name>1kmsquare</name>
<number>1</number>
<trkseg>
<trkpt lat="47.2577411" lon="11.9927442">
<ele>2273</ele>
<name>Lower left</name>
</trkpt>
<trkpt lat="47.266761" lon="11.9926673">
<ele>2166</ele>
<name>Top left</name>
</trkpt>
<trkpt lat="47.2667605" lon="12.0059511">
<ele>2245</ele>
<name>Top right</name>
</trkpt>
<trkpt lat="47.2577516" lon="12.0059925">
<ele>1994</ele>
<name>Lower right</name>
</trkpt>
<trkpt lat="47.2577412" lon="11.9927442">
<ele>2273</ele>
<name>Destination</name>
</trkpt>
</trkseg>
</trk>
</gpx>

View File

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="OsmAndRouterV2" xmlns="http://www.topografix.com/GPX/1/1" xmlns:osmand="https://osmand.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<metadata>
<name>1kmsquare100</name>
<desc>Export from GpsPrune</desc>
</metadata>
<trk>
<name>1kmsquare100</name>
<number>1</number>
<trkseg>
<trkpt lat="47.2578455" lon="11.9929891">
<ele>2265</ele>
<name>Lower left</name>
</trkpt>
<trkpt lat="47.258592" lon="11.9923341">
<ele>2256</ele>
</trkpt>
<trkpt lat="47.2594506" lon="11.9927412">
<ele>2230</ele>
</trkpt>
<trkpt lat="47.2603323" lon="11.9924949">
<ele>2219</ele>
</trkpt>
<trkpt lat="47.2612056" lon="11.9928175">
<ele>2199</ele>
</trkpt>
<trkpt lat="47.2621002" lon="11.9929817">
<ele>2182</ele>
</trkpt>
<trkpt lat="47.2629025" lon="11.9923915">
<ele>2189</ele>
</trkpt>
<trkpt lat="47.2637828" lon="11.9926486">
<ele>2180</ele>
</trkpt>
<trkpt lat="47.2646733" lon="11.9928167">
<ele>2191</ele>
</trkpt>
<trkpt lat="47.2655617" lon="11.9930357">
<ele>2185</ele>
</trkpt>
<trkpt lat="47.2662862" lon="11.992252">
<ele>2186</ele>
</trkpt>
<trkpt lat="47.2669305" lon="11.993173">
<ele>2166</ele>
<name>Top left</name>
</trkpt>
<trkpt lat="47.266666" lon="11.9944419">
<ele>2171</ele>
</trkpt>
<trkpt lat="47.2667579" lon="11.99576">
<ele>2194</ele>
</trkpt>
<trkpt lat="47.2669409" lon="11.9970579">
<ele>2207</ele>
</trkpt>
<trkpt lat="47.2666562" lon="11.9983128">
<ele>2212</ele>
</trkpt>
<trkpt lat="47.2666027" lon="11.9996335">
<ele>2262</ele>
</trkpt>
<trkpt lat="47.2667245" lon="12.0009395">
<ele>2278</ele>
</trkpt>
<trkpt lat="47.2668457" lon="12.002256">
<ele>2297</ele>
</trkpt>
<trkpt lat="47.2666126" lon="12.0035373">
<ele>2303</ele>
</trkpt>
<trkpt lat="47.2664554" lon="12.004841">
<ele>2251</ele>
</trkpt>
<trkpt lat="47.2669461" lon="12.005948">
<ele>2245</ele>
<name>Top right</name>
</trkpt>
<trkpt lat="47.2660877" lon="12.006323">
<ele>2195</ele>
</trkpt>
<trkpt lat="47.2652729" lon="12.0057552">
<ele>2163</ele>
</trkpt>
<trkpt lat="47.2643926" lon="12.0060123">
<ele>2131</ele>
</trkpt>
<trkpt lat="47.2634978" lon="12.0058302">
<ele>2095</ele>
</trkpt>
<trkpt lat="47.2626129" lon="12.0060759">
<ele>2066</ele>
</trkpt>
<trkpt lat="47.2617325" lon="12.0058188">
<ele>2037</ele>
</trkpt>
<trkpt lat="47.2608668" lon="12.0061784">
<ele>1993</ele>
</trkpt>
<trkpt lat="47.2600155" lon="12.0057392">
<ele>1967</ele>
</trkpt>
<trkpt lat="47.2591203" lon="12.0058233">
<ele>1949</ele>
</trkpt>
<trkpt lat="47.2582307" lon="12.0059718">
<ele>1972</ele>
</trkpt>
<trkpt lat="47.2578014" lon="12.004804">
<ele>2011</ele>
<name>Lower right</name>
</trkpt>
<trkpt lat="47.2577232" lon="12.0034834">
<ele>2044</ele>
</trkpt>
<trkpt lat="47.257745" lon="12.0021656">
<ele>2061</ele>
</trkpt>
<trkpt lat="47.2578682" lon="12.0008597">
<ele>2065</ele>
</trkpt>
<trkpt lat="47.2577082" lon="11.9995526">
<ele>2071</ele>
</trkpt>
<trkpt lat="47.2575917" lon="11.9982348">
<ele>2102</ele>
</trkpt>
<trkpt lat="47.2577401" lon="11.996924">
<ele>2147</ele>
</trkpt>
<trkpt lat="47.257715" lon="11.9956061">
<ele>2197</ele>
</trkpt>
<trkpt lat="47.2578996" lon="11.9943081">
<ele>2228</ele>
</trkpt>
<trkpt lat="47.2578436" lon="11.992992">
<ele>2265</ele>
<name>Destination</name>
</trkpt>
</trkseg>
</trk>
</gpx>

View File

@ -67,3 +67,4 @@ of 'Select Clock'
0.59: Preserve BLE whitelist even when disabled 0.59: Preserve BLE whitelist even when disabled
0.60: Moved LCD calibration to top of menu, and use 12 taps (not 8) 0.60: Moved LCD calibration to top of menu, and use 12 taps (not 8)
LCD calibration will now error if the calibration is obviously wrong LCD calibration will now error if the calibration is obviously wrong
0.61: Permit temporary bypass of the BLE whitelist

View File

@ -1,7 +1,7 @@
{ {
"id": "setting", "id": "setting",
"name": "Settings", "name": "Settings",
"version": "0.60", "version": "0.61",
"description": "A menu for setting up Bangle.js", "description": "A menu for setting up Bangle.js",
"icon": "settings.png", "icon": "settings.png",
"tags": "tool,system", "tags": "tool,system",

View File

@ -658,6 +658,7 @@ function showUtilMenu() {
function makeConnectable() { function makeConnectable() {
try { NRF.wake(); } catch (e) { } try { NRF.wake(); } catch (e) { }
Bluetooth.setConsole(1); Bluetooth.setConsole(1);
NRF.ignoreWhitelist = 1;
var name = "Bangle.js " + NRF.getAddress().substr(-5).replace(":", ""); var name = "Bangle.js " + NRF.getAddress().substr(-5).replace(":", "");
E.showPrompt(name + /*LANG*/"\nStay Connectable?", { title: /*LANG*/"Connectable" }).then(r => { E.showPrompt(name + /*LANG*/"\nStay Connectable?", { title: /*LANG*/"Connectable" }).then(r => {
if (settings.ble != r) { if (settings.ble != r) {
@ -665,6 +666,7 @@ function makeConnectable() {
updateSettings(); updateSettings();
} }
if (!r) try { NRF.sleep(); } catch (e) { } if (!r) try { NRF.sleep(); } catch (e) { }
delete NRF.ignoreWhitelist;
showMainMenu(); showMainMenu();
}); });
} }

View File

@ -2,3 +2,4 @@
0.02: Add "from Consec."-setting 0.02: Add "from Consec."-setting
0.03: Correct how to ignore last triggered alarm 0.03: Correct how to ignore last triggered alarm
0.04: Make "disable alarm" possible on next day; correct alarm filtering; improve settings 0.04: Make "disable alarm" possible on next day; correct alarm filtering; improve settings
0.05: Correct hide function + replace all `var` with `let`.

View File

@ -1,6 +1,6 @@
# Sleep Log Alarm # Sleep Log Alarm
This widget searches for active alarms and raises an own alarm event up to the defined time earlier, if in light sleep or awake phase. Optional the earlier alarm will only be triggered if comming from or in consecutive sleep. The settings of the earlier alarm can be adjusted and it is possible to filter the targeting alarms by time and message. By default the time of the targeting alarm is displayed inside the widget which can be adjusted, too. This widget searches for active alarms and raises an own alarm event up to the defined time earlier, if in light sleep or awake phase. Optional the earlier alarm will only be triggered if comming from or in consecutive sleep. The settings of the earlier alarm can be adjusted and it is possible to filter the targeting alarms by time and message. The widget is only displayed if an active alarm is detected. The time of the targeting alarm is displayed inside the widget, too. The time or the complete widget can be hidden in the options.
_This widget does not detect sleep on its own and can not create alarms. It requires the [sleeplog](/apps/?id=sleeplog) app and any alarm app that uses [sched](/apps/?id=sched) to be installed._ _This widget does not detect sleep on its own and can not create alarms. It requires the [sleeplog](/apps/?id=sleeplog) app and any alarm app that uses [sched](/apps/?id=sched) to be installed._
@ -30,7 +30,7 @@ _This widget does not detect sleep on its own and can not create alarms. It requ
- __msg includes__ | include only alarms including this string in msg - __msg includes__ | include only alarms including this string in msg
__""__ / ... __""__ / ...
- __Widget__ submenu - __Widget__ submenu
- __hide__ | completely hide the widget - __hide always__ | completely hide the widget
_on_ / __off__ _on_ / __off__
- __show time__ | show the time of the targeting alarm - __show time__ | show the time of the targeting alarm
__on__ / _off_ __on__ / _off_

View File

@ -1,5 +1,5 @@
// load library // load library
var sched = require("sched"); let sched = require("sched");
// find next active alarm in range // find next active alarm in range
function getNextAlarm(allAlarms, fo, withId) { function getNextAlarm(allAlarms, fo, withId) {
@ -10,7 +10,7 @@ function getNextAlarm(allAlarms, fo, withId) {
// return next active alarms in range, filter for // return next active alarms in range, filter for
// active && not timer && not own alarm && // active && not timer && not own alarm &&
// after from && before to && includes msg // after from && before to && includes msg
var ret = allAlarms.filter( let ret = allAlarms.filter(
a => a.on && !a.timer && a.id !== "sleeplog" && a => a.on && !a.timer && a.id !== "sleeplog" &&
a.t >= fo.from && a.t < fo.to && (!fo.msg || a.msg.includes(fo.msg)) a.t >= fo.from && a.t < fo.to && (!fo.msg || a.msg.includes(fo.msg))
).map(a => { // add time to alarm ).map(a => { // add time to alarm
@ -21,7 +21,7 @@ function getNextAlarm(allAlarms, fo, withId) {
).sort((a, b) => a.tTo - b.tTo); ).sort((a, b) => a.tTo - b.tTo);
// prevent triggering for an already triggered alarm again if available // prevent triggering for an already triggered alarm again if available
if (fo.lastDate) { if (fo.lastDate) {
var toLast = fo.lastDate - new Date().valueOf() + 1000; let toLast = fo.lastDate - new Date().valueOf() + 1000;
if (toLast > 0) ret = ret.filter(a => a.tTo > toLast); if (toLast > 0) ret = ret.filter(a => a.tTo > toLast);
} }
// return first entry // return first entry
@ -59,7 +59,7 @@ exports = {
if (typeof (global.sleeplog || {}).trigger !== "object") return; if (typeof (global.sleeplog || {}).trigger !== "object") return;
// read settings to calculate alarm range // read settings to calculate alarm range
var settings = exports.getSettings(); let settings = exports.getSettings();
// set the alarm time // set the alarm time
this.time = getNextAlarm(sched.getAlarms(), settings.filter).t; this.time = getNextAlarm(sched.getAlarms(), settings.filter).t;
@ -68,7 +68,7 @@ exports = {
if (!this.time) return; if (!this.time) return;
// set widget width if not hidden // set widget width if not hidden
if (!this.hidden) this.width = 8; if (!settings.wid.hide) this.width = 8;
// insert sleeplogalarm conditions and function // insert sleeplogalarm conditions and function
sleeplog.trigger.sleeplogalarm = { sleeplog.trigger.sleeplogalarm = {
@ -87,22 +87,22 @@ exports = {
// trigger function // trigger function
trigger: function() { trigger: function() {
// read settings // read settings
var settings = exports.getSettings(); let settings = exports.getSettings();
// read all alarms // read all alarms
var allAlarms = sched.getAlarms(); let allAlarms = sched.getAlarms();
// find first active alarm // find first active alarm
var alarm = getNextAlarm(sched.getAlarms(), settings.filter, settings.disableOnAlarm); let alarm = getNextAlarm(sched.getAlarms(), settings.filter, settings.disableOnAlarm);
// return if no alarm is found // return if no alarm is found
if (!alarm) return; if (!alarm) return;
// get now // get now
var now = new Date(); let now = new Date();
// get date of the alarm // get date of the alarm
var aDate = new Date(now + alarm.tTo); let aDate = new Date(now + alarm.tTo);
// disable earlier triggered alarm if set // disable earlier triggered alarm if set
if (settings.disableOnAlarm) { if (settings.disableOnAlarm) {

View File

@ -2,7 +2,7 @@
"id":"sleeplogalarm", "id":"sleeplogalarm",
"name":"Sleep Log Alarm", "name":"Sleep Log Alarm",
"shortName": "SleepLogAlarm", "shortName": "SleepLogAlarm",
"version": "0.04", "version": "0.05",
"description": "Enhance your morning and let your alarms wake you up when you are in light sleep.", "description": "Enhance your morning and let your alarms wake you up when you are in light sleep.",
"icon": "app.png", "icon": "app.png",
"type": "widget", "type": "widget",

View File

@ -1,6 +1,6 @@
(function(back) { (function(back) {
// read settings // read settings
var settings = require("sleeplogalarm").getSettings(); let settings = require("sleeplogalarm").getSettings();
// write change to storage // write change to storage
function writeSetting() { function writeSetting() {
@ -23,7 +23,7 @@
// show widget menu // show widget menu
function showFilterMenu() { function showFilterMenu() {
// set menu // set menu
var filterMenu = { let filterMenu = {
"": { "": {
title: "Filter Alarm" title: "Filter Alarm"
}, },
@ -64,22 +64,22 @@
}) })
} }
}; };
var menu = E.showMenu(filterMenu); let menu = E.showMenu(filterMenu);
} }
// show widget menu // show widget menu
function showWidMenu() { function showWidMenu() {
// define color values and names // define color values and names
var colName = ["red", "yellow", "green", "cyan", "blue", "magenta", "black", "white"]; let colName = ["red", "yellow", "green", "cyan", "blue", "magenta", "black", "white"];
var colVal = [63488, 65504, 2016, 2047, 31, 63519, 0, 65535]; let colVal = [63488, 65504, 2016, 2047, 31, 63519, 0, 65535];
// set menu // set menu
var widgetMenu = { let widgetMenu = {
"": { "": {
title: "Widget Settings" title: "Widget Settings"
}, },
/*LANG*/"< Back": () => showMain(9), /*LANG*/"< Back": () => showMain(9),
/*LANG*/"hide": { /*LANG*/"hide always": {
value: settings.wid.hide, value: settings.wid.hide,
onchange: v => { onchange: v => {
settings.wid.hide = v; settings.wid.hide = v;
@ -105,13 +105,13 @@
} }
} }
}; };
var menu = E.showMenu(widgetMenu); let menu = E.showMenu(widgetMenu);
} }
// show main menu // show main menu
function showMain(selected) { function showMain(selected) {
// set menu // set menu
var mainMenu = { let mainMenu = {
"": { "": {
title: "Sleep Log Alarm", title: "Sleep Log Alarm",
selected: selected selected: selected
@ -184,7 +184,7 @@
} }
} }
}; };
var menu = E.showMenu(mainMenu); let menu = E.showMenu(mainMenu);
} }
// draw main menu // draw main menu

View File

@ -1,7 +1,7 @@
// check if enabled in settings // check if enabled in settings
if ((require("Storage").readJSON("sleeplogalarm.settings.json", true) || {enabled: true}).enabled) { if ((require("Storage").readJSON("sleeplogalarm.settings.json", true) || {enabled: true}).enabled) {
// read settings // read settings
settings = require("sleeplogalarm").getSettings(); // is undefined if used with var let settings = require("sleeplogalarm").getSettings();
// insert neccessary settings into widget // insert neccessary settings into widget
WIDGETS.sleeplogalarm = { WIDGETS.sleeplogalarm = {
@ -10,10 +10,13 @@ if ((require("Storage").readJSON("sleeplogalarm.settings.json", true) || {enable
time: 0, time: 0,
earlier: settings.earlier, earlier: settings.earlier,
draw: function () { draw: function () {
// draw if width is set
if (this.width) {
// draw zzz // draw zzz
g.reset().setColor(settings.wid.color).drawImage(atob("BwoBD8SSSP4EEEDg"), this.x + 1, this.y); g.reset().setColor(settings.wid.color).drawImage(atob("BwoBD8SSSP4EEEDg"), this.x + 1, this.y);
// call function to draw the time of alarm if a alarm is found // call function to draw the time of alarm if a alarm is found
if (this.time) this.drawTime(this.time + 1); if (this.time) this.drawTime(this.time + 1);
}
}, },
drawTime: () => {}, drawTime: () => {},
reload: require("sleeplogalarm").widReload reload: require("sleeplogalarm").widReload

4
apps/sunrise/ChangeLog Normal file
View File

@ -0,0 +1,4 @@
0.01: First release
0.02: Faster sinus line and fix button to open menu
0.03: Show day/month, add animations, fix !mylocation and text glitch
0.04: Always show the widgets, swifter animations and lighter sea line

25
apps/sunrise/README.md Normal file
View File

@ -0,0 +1,25 @@
# sunrise watchface
This app mimics the Apple Watch watchface that shows the sunrise and sunset time.
This is a work-in-progress app, so you may expect missfeatures, bugs and heavy
battery draining. There's still a lot of things to optimize and improve, so take
this into account before complaining :-)
* Requires to configure the location in Settings -> Apps -> My Location
* Shows sea level and make the sun/moon glow depending on the x position
* The sinus is fixed, so the sea level is curved to match the sunrise/sunset positions)
## TODO
* Improved gradients and add support for banglejs1
* Faster rendering, by reducing sinus stepsize, only refreshing whats needed, etc
* Show red vertical lines or dots inside the sinus if there are alarms
## Author
Written by pancake in 2023
## Screenshots
![sunrise](screenshot.png)

Some files were not shown because too many files have changed in this diff Show More