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.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
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

View File

@ -16,52 +16,12 @@ const cc = [
const dd = [
"D",
"22",
"23",
"22",
"24",
"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 = [
"E",
"33",
@ -70,14 +30,187 @@ var ee = [
"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 chords = [];
var menu = {
"" : { "title" : "Uke Chords" },
"C" : function() { draw(cc); },
"D" : function() { draw(dd); },
"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 init() {
g.setFontAlign(0,0); // center font
g.setFont("6x8",2); // bitmap font, 8x magnified
chords.push(cc, dd, gg, am, em, aa, ff, ee);
}
function drawBase() {
for (let i = 0; i < 4; i++) {
@ -87,18 +220,18 @@ function drawBase() {
}
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++) {
if (i === 0 || chord[i][0] === "x") {
continue;
}
if (chord[i][0] === "0") {
g.drawString(chord[i][1], x + (i - 1) * stringInterval + 1, y + fretHeight * chord[i][0], true);
g.drawCircle(x + (i - 1) * stringInterval -1, y + fretHeight * chord[i][0], 8);
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], 10);
}
else {
g.drawString(chord[i][1], x + (i - 1) * stringInterval + 1, y -fingerOffset + fretHeight * chord[i][0], true);
g.drawCircle(x + (i - 1) * stringInterval -1, y -fingerOffset + fretHeight * chord[i][0], 8);
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], 10);
}
}
}
@ -107,22 +240,19 @@ function buttonPress() {
setWatch(() => {
buttonPress();
}, BTN);
index++;
if (index >= chords.length) { index = 0; }
draw();
E.showMenu(menu);
}
function draw() {
function draw(chord) {
g.clear();
drawBase();
drawChord(chords[index]);
drawChord(chord);
}
function main() {
init();
draw();
E.showMenu(menu);
setWatch(() => {
buttonPress();
}, BTN);

View File

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

View File

@ -2,3 +2,4 @@
0.02: Barometer altitude adjustment setting
0.03: Use default Bangle formatter for booleans
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 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;
/*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */
@ -183,7 +183,6 @@ var KalmanFilter = (function () {
var lf = {fix:0,satellites:0};
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 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 wp = {}; // Waypoint to use for distance from cur position.
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) {
if (!canDraw) return;
g.clearRect(0,screenYstart,screenW,screenH);
var v = '';
@ -227,7 +205,7 @@ function drawFix(dat) {
v = (cfg.primSpd)?dat.speed.toString():dat.alt.toString();
// 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);
@ -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) {
//if(emulator)console.log("\n1: " + n +" "+ u);
var s=40; // Font size
@ -337,16 +307,6 @@ function drawSats(sats) {
g.setFont("6x8", 2);
g.setFontAlign(1,1); //right, bottom
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) {
@ -367,7 +327,6 @@ function onGPS(fix) {
var sp = '---';
var al = '---';
var di = '---';
var age = '---';
if (fix.fix) lf = fix;
@ -412,10 +371,6 @@ function onGPS(fix) {
al = Math.round(parseFloat(al)/parseFloat(cfg.alt));
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 = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000));
} else {
@ -448,15 +403,7 @@ function onGPS(fix) {
}
}
function setButtons(){
setWatch(_=>load(), BTN1);
onGPS(lf);
}
function updateClock() {
if (!canDraw) return;
drawTime();
g.reset();
@ -545,6 +492,10 @@ function Compass_reading() {
hdngCompass = Compass_heading.toFixed(0);
}
function nextMode() {
showMax = 1 - showMax;
}
function start() {
Bangle.setBarometerPower(1); // needs some time...
g.clearRect(0,screenYstart,screenW,screenH);
@ -556,10 +507,30 @@ function start() {
Bangle.setCompassPower(1);
if (!calibrateCompass) setInterval(Compass_reading,200);
setButtons();
if (emulator) setInterval(updateClock, 2000);
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();
}
@ -571,6 +542,7 @@ if (cfg.record && WIDGETS["recorder"]) {
if (cfg.recordStopOnExit)
E.on('kill', () => WIDGETS["recorder"].setRecording(false));
} else {
start();
}

View File

@ -2,7 +2,7 @@
"id": "bikespeedo",
"name": "Bike Speedometer (beta)",
"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",
"icon": "app.png",
"screenshots": [{"url":"Screenshot.png"}],

View File

@ -66,3 +66,4 @@
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.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.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.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
// ================================================== FIXING OLDER FIRMWARES
if (FWVERSION<215.068) // 2v15.68 and before had compass heading inverted.

View File

@ -1,7 +1,7 @@
{
"id": "boot",
"name": "Bootloader",
"version": "0.57",
"version": "0.58",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png",
"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.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",
"name": "Chimer",
"version": "0.02",
"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",
"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 - Repeat Chime up to 3 times \n - Set hours to disable chime",
"icon": "widget.png",
"type": "widget",
"tags": "widget",

View File

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

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

@ -5,4 +5,5 @@
0.05: Deleting Background - making Font larger
0.06: Fixing refresh issues
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
* 40 pixels wide
The high-level marker (a little bar at the 100% point) can be toggled in settings.
![](a_battery_widget-pic.jpg)
## Creator

View File

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

View File

@ -1,7 +1,6 @@
(function(){
const intervalLow = 60000; // update time when not charging
const intervalHigh = 2000; // update time when charging
var old_l;
var old_x = this.x;
var old_y = this.y;
@ -22,49 +21,36 @@
};
function draw() {
if (typeof old_x === 'undefined') old_x = this.x;
if (typeof old_y === 'undefined') old_y = this.y;
var s = 29;
var s = width - 1;
var x = this.x;
var y = this.y;
if ((typeof x === 'undefined') || (typeof y === 'undefined')) {
} else {
g.clearRect(old_x, old_y, old_x + width, old_y + height);
const l = E.getBattery(); // debug: Math.floor(Math.random() * 101);
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.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
g.setColor(COLORS.black);
g.setFontAlign(0,0);
g.setFont('Vector',16);
g.drawString(l, x + 14, y + 10);
}
old_x = this.x;
old_y = this.y;
old_y = this.y;
if (Bangle.isCharging()) changeInterval(id, intervalHigh);
else changeInterval(id, intervalLow);
}
Bangle.on('charging',function(charging) { draw(); });
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.06: Fix exception when showing missing hiragana 'WO'
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
A simple watchface design with hiragana and katakana
cards for learning.
A simple watchface design perfect for learning hiragana and katakana.
## 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
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
0.04: Show a random kana every minute to improve learning
## TODO
* Only render what needs to be repainted
* Dont redraw the widgets if not necessary
* Minigame to guess kata/hira phonem
## Author
Written by pancake in 2022, powered by insomnia
Written by pancake in 2022, maintained during 2023 and powered by insomnia
## 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 h = g.getHeight();
const w = g.getWidth();
const decompress = require("heatshrink").decompress;
/// /////////////////////////////////////////
const katakana = {};
const hiragana = {};
function benchStart() {
return {
now : +Date.now(),
diff: function() {
return (0+Date.now()) - this.now;
}
};
}
const startupTime = benchStart();
function image(x,y,b) {
return {
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 scroll = 0;
// const keys = Object.keys(katakana).sort();
// console.log(keys);
let hiramode = false;
let curkana = 'KA';
console.log("StartupTime: "+startupTime.diff());
function next () {
let found = false;
for (const k of Object.keys(katakana).sort()) {
if (found) {
kana = hiramode ? hiragana[k] : katakana[k];
curkana = k;
return;
}
if (curkana === k) {
found = true;
}
const off = keys.indexOf(curkana);
if (off !== -1 && off + 1 < keys.length) {
return keys[off + 1];
}
curkana = 'KA';
updateWatch(ohhmm);
return keys[0];
}
function randKana() {
try {
const keys = Object.keys(katakana);
const total = keys.length;
let index = 0 | (Math.random() * total);
let index = 0 | (Math.random() * keys.length);
curkana = keys[index];
} catch (e) {
randKana();
}
}
// const bench = benchStart();
// console.log("-->" + bench.diff());
function prev () {
let oldk = '';
let count = 0;
for (const k of Object.keys(katakana).sort()) {
if (curkana === k) {
if (count > 0) {
curkana = oldk;
return;
}
}
oldk = k;
count++;
const off = keys.indexOf(curkana);
if (off > 0) {
return keys[off - 1];
}
return keys[keys.length - 1];
}
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++;
}
}
function prevColor() {
if (color < 1) {
color = colors.length - 1;
} else {
color--;
}
curkana = oldk;
updateWatch(ohhmm);
}
const kanacolors = {
A: []
};
function updateWatch (hhmm) {
function render(hhmm) {
g.setFontAlign(-1, -1, 0);
g.setBgColor(0, 0, 0);
g.setColor(0, 0, 0);
var whitecolor = false;
if (curkana.indexOf('A') != -1) {
g.setColor(1, 0, 0);
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);
const whitecolor = color > 3;
colors[color]();
g.fillRect(0, 30, w, h);
g.setFont('Vector', 50);
if (whitecolor) {
@ -196,9 +228,9 @@ function updateWatch (hhmm) {
}
g.drawString(hhmm, x, y - 1);
drawKana(4 + (g.getWidth() / 6), 60);
drawKana();
drawMonthDay();
Bangle.drawWidgets();
// Bangle.drawWidgets(); // :? always draw?
}
function drawMonthDay() {
@ -219,21 +251,47 @@ function getPhoneme(k) {
}
return k;
}
var ohhmm = '';
var ypos = 0;
var xpos = 0;
var zpos = 1;
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.fillRect(0, 0, g.getWidth(), 6 * (h / 8) + 1);
g.fillRect(0, 30, g.getWidth(), 6 * (h / 8) + 1);
g.setColor(1, 1, 1);
x -= ((zpos) - 1)*50;
y -= (zpos - 1)*50;
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.setFont('Vector', 24);
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 () {
const now = Date();
month = now.getMonth() + 1;
@ -243,27 +301,127 @@ function tickWatch () {
}
const hhmm = zpad(now.getHours()) + ':' + zpad(now.getMinutes());
if (hhmm !== ohhmm) {
randKana();
updateWatch(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) {
if (top.x < w / 4) {
prev();
} else if (top.x > (w - (w / 4))) {
next();
if (top.y < (h / 1.5)) {
if (top.x > w /2) {
//hiramode = !hiramode;
if (hiramode) {
hiraSwipe(1,0, hiramode);
} else {
hiraSwipe(-1,0, !hiramode);
}
} else {
hiraSwipe(1,1,1);
}
} else if (top.x < w / 2) {
nextColor();
hiraPush();
// curkana = prev();
} else {
hiramode = !hiramode;
prevColor();
hiraPush();
// curkana = next();
}
kana = hiramode ? hiragana[curkana] : katakana[curkana];
updateWatch(ohhmm);
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);
// show launcher when button pressed
Bangle.setUI('clock');
Bangle.loadWidgets();
Bangle.drawWidgets();
// redraw widgets every 10 minutes
setInterval(function() {
// maybe not always necessary
Bangle.drawWidgets();
}, 1000 * 60 * 10);
tickWatch();
setInterval(tickWatch, 1000 * 60);

View File

@ -2,7 +2,7 @@
"id": "kanawatch",
"name": "Kanawatch",
"shortName": "Kanawatch",
"version": "0.07",
"version": "0.11",
"type": "clock",
"description": "Learn Hiragana and Katakana",
"icon": "app.png",
@ -26,6 +26,9 @@
"screenshots": [
{
"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.04: Allow moving the 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_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/)

View File

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

View File

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

View File

@ -3,6 +3,7 @@
var settings = require('Storage').readJSON("kbmulti.settings.json", true) || {};
if (settings.showHelpBtn===undefined) { settings.showHelpBtn = true; }
if (settings.charTimeout===undefined) { settings.charTimeout = 500; }
if (settings.autoLowercase===undefined) { settings.autoLowercase = true; }
return settings;
}
@ -21,6 +22,10 @@
format: v => v,
onchange: v => updateSetting("charTimeout", v),
},
/*LANG*/'Lowercase after first uppercase': {
value: !!settings().autoLowercase,
onchange: v => updateSetting("autoLowercase", v)
},
/*LANG*/'Show help button?': {
value: !!settings().showHelpBtn,
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.06: Support input of numbers and uppercase characters.
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) {
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("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("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("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]));
@ -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("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("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("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]));
@ -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("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("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("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]));
} 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]));
@ -210,7 +210,7 @@ exports.input = function(options) {
if (o.stroke!==undefined && o.xy.length >= 6 && isStrokeInside(R, o.xy)) {
var ch = o.stroke;
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;
}
lastDrag = undefined;
@ -226,7 +226,7 @@ exports.input = function(options) {
shift = false;
setupStrokes();
show();
Bangle.drawWidgets();
WIDGETS.kbswipe.draw();
}
Bangle.on('stroke',strokeHandler);
@ -239,7 +239,7 @@ exports.input = function(options) {
area:"tl",
width: 36, // 3 chars, 6*2 px/char
draw: function() {
g.reset();
g.reset().clearRect(this.x, this.y, this.x + this.width-1, this.y + 23);
g.setFont("6x8:2x3");
g.setColor("#f00");
if (input_mode === exports.INPUT_MODE_ALPHA) {
@ -251,6 +251,7 @@ exports.input = function(options) {
}
}
};
Bangle.drawWidgets();
return new Promise((resolve,reject) => {
Bangle.setUI({mode:"custom", drag:e=>{

View File

@ -1,6 +1,6 @@
{ "id": "kbswipe",
"name": "Swipe keyboard",
"version":"0.07",
"version":"0.08",
"description": "A library for text input via PalmOS style swipe gestures (beta!)",
"icon": "app.png",
"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.05: Prevent drawing into app area.
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",
"name": "Light Switch Widget",
"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.",
"icon": "images/app.png",
"screenshots": [

View File

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

View File

@ -90,4 +90,9 @@
0.65: Make sure messages are saved if not in the clock app (fix #2460)
0.66: Updated Navigation handling to work with new Gadgetbridge release
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
require("messages").pushMessage({"t":"add","id":1575479849,"src":"Skype","title":"My Friend","body":"Hey! How's everything going?",positive:1,negative:1})
// 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",distance:12345,action:"left_slight",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:"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
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 fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4";
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?
// hack for 2v10 firmware's lack of ':size' font handling
try {
@ -67,7 +68,7 @@ var onMessagesModified = function(type,msg) {
}
if (msg && msg.id=="music") {
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);
};
@ -81,22 +82,36 @@ E.on("kill", saveMessages);
function showMapMessage(msg) {
active = "map";
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);
if (msg.instr) {
if (msg.instr.includes("towards") || msg.instr.includes("toward")) {
m = msg.instr.split(/towards|toward/);
var instr = msg.instr.replace(/\s*\/\s*/g," \/\n"); // convert slashes to newlines
if (instr.includes("towards") || instr.includes("toward")) {
m = instr.split(/towards|toward/);
target = m[0].trim();
street = m[1].trim();
}else
target = msg.instr;
target = instr;
}
if (msg.action=="continue") img = "EBgBAIABwAPgD/Af+D/8f/773/PPY8cDwAPAA8ADwAPAA8AAAAPAA8ADwAAAA8ADwAPA";
else if (msg.action=="left") img = "GhcBAYAAAPAAAHwAAD4AAB8AAA+AAAf//8P///x///+PAAPx4AA8fAAHD4ABwfAAcDwAHAIABwAAAcAAAHAAABwAAAcAAAHAAABwAAAc";
else if (msg.action=="right") img = "GhcBAABgAAA8AAAPgAAB8AAAPgAAB8D///j///9///+/AAPPAAHjgAD44AB8OAA+DgAPA4ABAOAAADgAAA4AAAOAAADgAAA4AAAOAAAA";
else if (msg.action=="left_slight") img = "ERgB//B/+D/8H4AP4Af4A74Bz4Dj4HD4OD4cD4AD4ADwADwADgAHgAPAAOAAcAA4ABwADgAH";
else if (msg.action=="right_slight") img = "ERgBB/+D/8H/4APwA/gD/APuA+cD44Phw+Dj4HPgAeAB4ADgAPAAeAA4ABwADgAHAAOAAcAA";
else if (msg.action=="finish") img = "HhsBAcAAAD/AAAH/wAAPB4AAeA4AAcAcAAYIcAA4cMAA48MAA4cMAAYAcAAcAcAAcA4AAOA4AAOBxjwHBzjwHjj/4Dnn/4B3P/4B+Pj4A8fj8Acfj8AI//8AA//+AA/j+AB/j+AB/j/A";
switch (msg.action) {
case "continue": img = "EBgBAIABwAPgD/Af+D/8f/773/PPY8cDwAPAA8ADwAPAA8AAAAPAA8ADwAAAA8ADwAPA";break;
case "left": img = "GhcBAYAAAPAAAHwAAD4AAB8AAA+AAAf//8P///x///+PAAPx4AA8fAAHD4ABwfAAcDwAHAIABwAAAcAAAHAAABwAAAcAAAHAAABwAAAc";break;
case "right": img = "GhcBAABgAAA8AAAPgAAB8AAAPgAAB8D///j///9///+/AAPPAAHjgAD44AB8OAA+DgAPA4ABAOAAADgAAA4AAAOAAADgAAA4AAAOAAAA";break;
case "left_slight": img = "ERgB//B/+D/8H4AP4Af4A74Bz4Dj4HD4OD4cD4AD4ADwADwADgAHgAPAAOAAcAA4ABwADgAH";break;
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: [
{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;
g.setFont(bodyFont);
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;
if (titleCnt) lines.push(""); // add blank line after title
lines = lines.concat(g.wrapString(msg.body, g.getWidth()-10),["",/*LANG*/"< Back"]);
@ -390,6 +405,7 @@ function checkMessages(options) {
options=options||{};
// If no messages, just show 'no messages' and return
if (!MESSAGES.length) {
active=undefined; // no messages
if (!options.clockIfNoMsg) return E.showPrompt(/*LANG*/"No Messages",{
title:/*LANG*/"Messages",
img:require("heatshrink").decompress(atob("kkk4UBrkc/4AC/tEqtACQkBqtUDg0VqAIGgoZFDYQIIM1sD1QAD4AIBhnqA4WrmAIBhc6BAWs8AIBhXOBAWz0AIC2YIC5wID1gkB1c6BAYFBEQPqBAYXBEQOqBAnDAIQaEnkAngaEEAPDFgo+IKA5iIOhCGIAFb7RqAIGgtUBA0VqobFgNVA")),
@ -419,7 +435,7 @@ function checkMessages(options) {
// no new messages - go to clock?
if (options.clockIfAllRead && newMessages.length==0)
return load();
active = "main";
active = "list";
// Otherwise show a menu
E.showScroller({
h : 48,

View File

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

View File

@ -3,3 +3,4 @@
0.57: Optimize saving empty message list
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.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",
"name": "Messages",
"version": "0.59",
"version": "0.60",
"description": "Library to handle, load and store message events received from Android/iOS",
"icon": "app.png",
"type": "module",

View File

@ -9,3 +9,4 @@
0.10: Improvements to help notifications work with themes
0.11: Fix regression that caused no notifications and corrupted background
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",
"name": "Notifications (default)",
"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",
"icon": "notify.png",
"type": "notify",

View File

@ -103,7 +103,7 @@ exports.show = function(options) {
b -= 2;h -= 2;
// title bar
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;
g.setColor(g.theme.fg).setFontAlign(-1, -1, 0).setFont("6x8", 2);
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);
// title bar
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;
gg.setColor(g.theme.fg).setFontAlign(-1, -1, 0).setFont("6x8", 2);
gg.drawString(title.trim().substring(0, 13), x+25,y+3);

View File

@ -1,3 +1,4 @@
0.01: New App!
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.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) {
let drawPage = function() {
clearScreen();
let fontSize = 30; // Initial font size
let lineSpacing = 15; // Space between lines
// Define the vertical positions of the titles
let titlePositions = [10, 72, 132];
let lineSpacing = 15; // Space between lines
for (let i = 0; i < result.result.length; i++) {
let fontSize = 30; // Initial font size
let currentResult = result.result[i];
let resultTitle = currentResult[0];
let resultValue = currentResult[1];

View File

@ -2,7 +2,7 @@
"id": "ohmcalc",
"name": "Ohm's Law Calculator",
"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.",
"icon": "app.png",
"type": "app",

View File

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

View File

@ -29,4 +29,7 @@
0.23: Add graphing for HRM, fix some other graphs
Altitude graphing now uses barometer altitude if it exists
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) {
var trackNum = 0;
var matches = filename.match(/^recorder\.log(.*)\.csv$/);
if (matches) {
trackNum = parseInt(matches[1]||0);
}
return trackNum;
if (matches) return matches[1];
return 0;
}
function showMainMenu() {
@ -62,23 +60,13 @@ function showMainMenu() {
WIDGETS["recorder"].setRecording(v).then(function() {
//print("Record start Complete");
loadSettings();
print("Recording: "+settings.recording);
//print("Recording: "+settings.recording);
showMainMenu();
});
}, 1);
}
},
/*LANG*/'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*/'File' : {value:getTrackNumber(settings.file)},
/*LANG*/'View Tracks': ()=>{viewTracks();},
/*LANG*/'Time Period': {
value: settings.period||10,
@ -110,7 +98,7 @@ function viewTracks() {
var found = false;
require("Storage").list(/^recorder\.log.*\.csv$/,{sf:true}).forEach(filename=>{
found = true;
menu[/*LANG*/"Track "+getTrackNumber(filename)] = ()=>viewTrack(filename,false);
menu[/*LANG*/getTrackNumber(filename)] = ()=>viewTrack(filename,false);
});
if (!found)
menu[/*LANG*/"No Tracks found"] = function(){};
@ -353,7 +341,7 @@ function viewTrack(filename, info) {
infc[i]++;
}
} else if (style=="Altitude") {
title = /*LANG*/"Altitude (m)";
title = /*LANG*/"Altitude (m)";
var altIdx = info.fields.indexOf("Barometer Altitude");
if (altIdx<0) altIdx = info.fields.indexOf("Altitude");
while(l!==undefined) {

View File

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

View File

@ -36,38 +36,48 @@ function colorBandsToResistance(colorBands) {
}
function resistanceToColorBands(resistance, tolerance) {
let resistanceStr = resistance.toString();
let firstDigit, secondDigit, multiplier;
if (resistanceStr.length === 1) { // Check if resistance is a single digit
firstDigit = 0;
secondDigit = Number(resistanceStr.charAt(0));
multiplier = 0;
} else if (resistance >= 100) {
// Extract the first two digits from the resistance value
firstDigit = Number(resistanceStr.charAt(0));
secondDigit = Number(resistanceStr.charAt(1));
// Calculate the multiplier
multiplier = resistanceStr.length - 2;
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 {
// For values between 10-99, shift the color to the first band
firstDigit = Number(resistanceStr.charAt(0));
secondDigit = Number(resistanceStr.charAt(1));
multiplier = 0;
// 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
firstDigit = 0;
secondDigit = Number(resistanceStr.charAt(0));
multiplier = 1; // Set multiplier to 1 for single digit resistance values
} else {
// Extract the first two digits from the resistance value
firstDigit = Number(resistanceStr.charAt(0));
secondDigit = Number(resistanceStr.charAt(1));
// Calculate the multiplier by matching it directly with the length of digits
multiplier = resistanceStr.length - 2 >= 0 ? Math.pow(10, resistanceStr.length - 2) : Math.pow(10, resistanceStr.length - 1);
}
}
let firstBandEntry = Object.entries(colorData).find(function(entry) {
let firstBandEntry = Object.entries(colorData).find(function (entry) {
return entry[1].value === firstDigit;
});
let firstBand = firstBandEntry ? firstBandEntry[1].hex : undefined;
let secondBandEntry = Object.entries(colorData).find(function(entry) {
let secondBandEntry = Object.entries(colorData).find(function (entry) {
return entry[1].value === secondDigit;
});
let secondBand = secondBandEntry ? secondBandEntry[1].hex : undefined;
let multiplierBandEntry = Object.entries(colorData).find(function(entry) {
return entry[1].multiplier === Math.pow(10, multiplier);
let multiplierBandEntry = Object.entries(colorData).find(function (entry) {
return entry[1].multiplier === multiplier;
});
let multiplierBand = multiplierBandEntry ? multiplierBandEntry[1].hex : undefined;
let toleranceBandEntry = Object.entries(colorData).find(function(entry) {
let toleranceBandEntry = Object.entries(colorData).find(function (entry) {
return entry[1].tolerance === tolerance;
});
let toleranceBand = toleranceBandEntry ? toleranceBandEntry[1].hex : undefined;
@ -165,7 +175,7 @@ function drawResistance(resistance, tolerance) {
g.drawString(toleranceStr.padEnd(4), 176 - toleranceX, y);
}
(function() {
(function () {
let colorBands;
let inputColorBands;
let settings = {
@ -190,7 +200,7 @@ function drawResistance(resistance, tolerance) {
'': {
'title': `Band ${bandNumber}`
},
'< Back': function() {
'< Back': function () {
E.showMenu(colorEntryMenu);
},
};
@ -199,24 +209,24 @@ function drawResistance(resistance, tolerance) {
for (let color in colorData) {
if (bandNumber === 1 || bandNumber === 2) {
if (color !== 'none' && color !== 'gold' && color !== 'silver') {
(function(color) {
colorBandMenu[color.charAt(0).toUpperCase() + color.slice(1)] = function() {
(function (color) {
colorBandMenu[color.charAt(0).toUpperCase() + color.slice(1)] = function () {
setBandColor(bandNumber, color);
};
})(color);
}
} else if (bandNumber === 3) {
if (color !== 'none') {
(function(color) {
colorBandMenu[color.charAt(0).toUpperCase() + color.slice(1)] = function() {
(function (color) {
colorBandMenu[color.charAt(0).toUpperCase() + color.slice(1)] = function () {
setBandColor(bandNumber, color);
};
})(color);
}
} else if (bandNumber === 4) {
if (colorData[color].hasOwnProperty('tolerance')) {
(function(color) {
colorBandMenu[color.charAt(0).toUpperCase() + color.slice(1)] = function() {
(function (color) {
colorBandMenu[color.charAt(0).toUpperCase() + color.slice(1)] = function () {
setBandColor(bandNumber, color);
};
})(color);
@ -238,7 +248,7 @@ function drawResistance(resistance, tolerance) {
'': {
'title': 'Band Color'
},
'< Back': function() {
'< Back': function () {
clearScreen();
E.showMenu(mainMenu);
},
@ -274,7 +284,7 @@ function drawResistance(resistance, tolerance) {
setTimeout(() => showColorBandMenu(4), 5);
}
},
'Draw Resistor': function() {
'Draw Resistor': function () {
inputColorBands = settings.colorBands;
let values = colorBandsToResistance(inputColorBands);
settings.resistance = values[0];
@ -291,7 +301,7 @@ function drawResistance(resistance, tolerance) {
'': {
'title': 'Multiplier'
},
'< Back': function() {
'< Back': function () {
showResistanceEntryMenu();
}
};
@ -304,7 +314,7 @@ function drawResistance(resistance, tolerance) {
multiplierMenu[`${formattedMultiplier}`] = () => {
settings.multiplier = multiplierValue;
// Update the value of 'Multiplier' in resistanceEntryMenu
resistanceEntryMenu["Multiplier"] = function() {
resistanceEntryMenu["Multiplier"] = function () {
showMultiplierMenu();
};
showResistanceEntryMenu();
@ -330,7 +340,7 @@ function drawResistance(resistance, tolerance) {
'': {
'title': 'Tolerance'
},
'< Back': function() {
'< Back': function () {
showResistanceEntryMenu();
}
};
@ -342,7 +352,7 @@ function drawResistance(resistance, tolerance) {
toleranceMenu[`${tolerance}%`] = () => {
settings.tolerance = tolerance;
// Update the value of 'Tolerance (%)' in resistanceEntryMenu
resistanceEntryMenu["Tolerance (%)"] = function() {
resistanceEntryMenu["Tolerance (%)"] = function () {
showToleranceMenu();
};
showResistanceEntryMenu();
@ -373,7 +383,7 @@ function drawResistance(resistance, tolerance) {
'': {
'title': 'Resistance'
},
'< Back': function() {
'< Back': function () {
clearScreen();
E.showMenu(mainMenu);
},
@ -383,15 +393,15 @@ function drawResistance(resistance, tolerance) {
max: 99,
wrap: true,
format: v => '',
onchange: v => {}
onchange: v => { }
},
'Multiplier': function() {
'Multiplier': function () {
showMultiplierMenu();
},
'Tolerance (%)': function() {
'Tolerance (%)': function () {
showToleranceMenu();
},
'Draw Resistor': function() {
'Draw Resistor': function () {
showDrawingMenu();
}
};
@ -420,7 +430,7 @@ function drawResistance(resistance, tolerance) {
'': {
'title': ''
},
'< Back': function() {
'< Back': function () {
clearScreen();
E.showMenu(mainMenu);
},
@ -436,11 +446,11 @@ function drawResistance(resistance, tolerance) {
'title': 'Resistor Calc'
},
'< Back': () => Bangle.showClock(), // return to the clock app
'Resistance': function() {
'Resistance': function () {
resetSettings();
showResistanceEntryMenu();
},
'Colors': function() {
'Colors': function () {
resetSettings();
showColorEntryMenu();
},

View File

@ -14,4 +14,4 @@
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.15: 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: Added ability to resume a run that was stopped previously (fix #1907)

View File

@ -13,10 +13,12 @@
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.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)
Keep run state between runs (allowing you to exit and restart the app)
0.16: Don't clear zone 2b indicator segment when updating HRM reading.
Write to correct settings file, fixing settings not working.
0.17: Fix typo in variable name preventing starting a run.
0.18: Tweak HRM min/max defaults. Extend min/max intervals in settings. Fix
another typo.
0.15: 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.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.
0.19: Fix typo in variable name preventing starting a run
0.20: Tweak HRM min/max defaults. Extend min/max intervals in settings. Fix
another typo.
0.21: Rebase on "Run" app ver. 0.16.

View File

@ -61,36 +61,47 @@ function setStatus(running) {
// Called to start/stop running
function onStartStop() {
let running = !exs.state.active;
let prepPromises = [];
var running = !exs.state.active;
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
// Do this first in case recorder needs to prompt for
// an overwrite before we start tracking exstats
if (settings.record && WIDGETS["recorder"]) {
if (running) {
isMenuDisplayed = true;
prepPromises.push(
WIDGETS["recorder"].setRecording(true).then(() => {
promise = promise.
then(() => WIDGETS["recorder"].setRecording(true, { force : shouldResume?"append":undefined })).
then(() => {
isMenuDisplayed = false;
layout.setUI(); // grab our input handling again
layout.forgetLazyState();
layout.render();
})
);
});
} else {
prepPromises.push(
WIDGETS["recorder"].setRecording(false)
promise = promise.then(
() => WIDGETS["recorder"].setRecording(false)
);
}
}
if (!prepPromises.length) // fix for Promise.all bug in 2v12
prepPromises.push(Promise.resolve());
Promise.all(prepPromises)
.then(() => {
promise = promise.then(() => {
if (running) {
exs.start();
if (shouldResume)
exs.resume()
else
exs.start();
} else {
exs.stop();
}

View File

@ -1,7 +1,7 @@
{
"id": "runplus",
"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.",
"icon": "app.png",
"tags": "run,running,fitness,outdoors,gps,karvonen,karvonnen",

View File

@ -86,31 +86,74 @@ function eventToAlarm(event, offsetMs) {
}
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.writeStorage("sched.json", JSON.stringify(alarms), () => {
location.reload(); // reload so we see current data
Puck.write(`\x10require("sched").reload();\n`, () => {
location.reload(); // reload so we see current data
});
});
}
function renderAlarm(alarm, exists) {
const localDate = dateFromAlarm(alarm);
const localDate = alarm.date ? dateFromAlarm(alarm) : null;
const tr = document.createElement('tr');
tr.classList.add('event-row');
tr.dataset.uid = alarm.id;
const tdTime = document.createElement('td');
tr.appendChild(tdTime);
const tdType = document.createElement('td');
tdType.type = "text";
tdType.classList.add('event-summary');
tr.appendChild(tdType);
const inputTime = document.createElement('input');
inputTime.type = "datetime-local";
if (localDate) {
tdType.textContent = "Event";
inputTime.type = "datetime-local";
inputTime.value = localDate.toISOString().slice(0,16);
inputTime.onchange = (e => {
const date = new Date(inputTime.value);
alarm.t = dateToMsSinceMidnight(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;
inputTime.value = localDate.toISOString().slice(0,16);
inputTime.onchange = (e => {
const date = new Date(inputTime.value);
alarm.t = dateToMsSinceMidnight(date);
alarm.date = formatDate(date);
});
const tdTime = document.createElement('td');
tr.appendChild(tdTime);
tdTime.appendChild(inputTime);
const tdSummary = document.createElement('td');
@ -130,13 +173,31 @@ function renderAlarm(alarm, exists) {
tdSummary.appendChild(inputSummary);
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');
tr.appendChild(tdInfo);
const buttonDelete = document.createElement('button');
buttonDelete.classList.add('btn');
buttonDelete.classList.add('btn-action');
tdInfo.prepend(buttonDelete);
tdInfo.appendChild(buttonDelete);
const iconDelete = document.createElement('i');
iconDelete.classList.add('icon');
iconDelete.classList.add('icon-delete');
@ -150,12 +211,53 @@ function renderAlarm(alarm, exists) {
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() {
const alarm = getAlarmDefaults();
delete alarm.date;
renderAlarm(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() {
Util.showModal("Loading...");
Util.readStorage('sched.json',data=>{
@ -164,10 +266,19 @@ function getData() {
Util.readStorage('sched.settings.json',data=>{
schedSettings = JSON.parse(data || "{}") || {};
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 => {
if (alarm.date) {
renderAlarm(alarm, true);
}
renderAlarm(alarm, true);
});
});
});
@ -183,16 +294,27 @@ function onInit() {
<h4>Manage dated events</h4>
<div class="float-right">
<button class="btn" onclick="addEvent()">
<i class="icon icon-plus"></i>
Event
</button>
<button class="btn" onclick="addAlarm()">
<i class="icon icon-plus"></i>
Alarm
</button>
<button class="btn" onclick="addTimer()">
<i class="icon icon-plus"></i>
Timer
</button>
</div>
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Date/Time</th>
<th>Summary</th>
<th>On?</th>
<th></th>
</tr>
</thead>

View File

@ -2,3 +2,5 @@
0.02: Less time used during boot if disabled
0.03: Fixed some test data
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:
enabled:
true or false
mode:
emulate: Completely craft events for this sensor
modify: Take existing events from real sensor and modify their data
name:
name of the emulation or modification mode
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
Enabled:
* **true**
* **false**
Mode:
* **emulate**: Completely craft events for this sensor
* **modify**: Take existing events from real sensor and modify their data
Name:
* name of the emulation or modification mode
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
Modes: modify, emulate
Modes:
* **modify**: Modify the original events from this sensor
* **emulate**: Create events simulating sensor activity
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:
sin: Calculate bpm changes by using sin
* **sin**: Calculate bpm changes by using sin
### GPS
Modes: emulate
Modes:
* **emulate**
Emulation:
staticfix: static complete fix with all values
route: A square route starting in the SW corner and moving SW->NW->NO->SW...
routeFuzzy: Roughly the same square as route, but with 100m seqments with some variaton in course
nofix: All values NaN but time,sattelites,fix and fix == 0
changingfix: A fix with randomly changing values
* **staticfix**: static complete fix with all values
* **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 [Download as gpx](squareFuzzy.gpx)
* **nofix**: All values NaN but time,sattelites,fix and fix == 0
* **changingfix**: A fix with randomly changing values
### Compass
Modes: emulate
Modes:
* **emulate**
Emulation:
static: All values but heading are 1, heading == 0
rotate: All values but heading are 1, heading rotates 360°
* **static**: All values but heading are 1, heading == 0
* **rotate**: All values but heading are 1, heading rotates 360°
# Creator
[halemmerich](https://github.com/halemmerich)

View File

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

View File

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

View File

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

View File

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

@ -66,4 +66,5 @@ of 'Select Clock'
0.58: On/Off settings items now use checkboxes
0.59: Preserve BLE whitelist even when disabled
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",
"name": "Settings",
"version": "0.60",
"version": "0.61",
"description": "A menu for setting up Bangle.js",
"icon": "settings.png",
"tags": "tool,system",

View File

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

View File

@ -1,4 +1,5 @@
0.01: New App!
0.02: Add "from Consec."-setting
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
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._
@ -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
__""__ / ...
- __Widget__ submenu
- __hide__ | completely hide the widget
- __hide always__ | completely hide the widget
_on_ / __off__
- __show time__ | show the time of the targeting alarm
__on__ / _off_

View File

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

View File

@ -2,7 +2,7 @@
"id":"sleeplogalarm",
"name":"Sleep Log Alarm",
"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.",
"icon": "app.png",
"type": "widget",

View File

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

View File

@ -1,7 +1,7 @@
// check if enabled in settings
if ((require("Storage").readJSON("sleeplogalarm.settings.json", true) || {enabled: true}).enabled) {
// read settings
settings = require("sleeplogalarm").getSettings(); // is undefined if used with var
let settings = require("sleeplogalarm").getSettings();
// insert neccessary settings into widget
WIDGETS.sleeplogalarm = {
@ -10,10 +10,13 @@ if ((require("Storage").readJSON("sleeplogalarm.settings.json", true) || {enable
time: 0,
earlier: settings.earlier,
draw: function () {
// draw zzz
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
if (this.time) this.drawTime(this.time + 1);
// draw if width is set
if (this.width) {
// draw zzz
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
if (this.time) this.drawTime(this.time + 1);
}
},
drawTime: () => {},
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

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