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

master
Gordon Williams 2023-06-05 16:51:24 +01:00
commit 8cca43442b
61 changed files with 1597 additions and 511 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

@ -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",

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

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

@ -156,7 +156,7 @@ exports = { name : "en_GB", currencySym:"£",
"%-m": "d.getMonth()+1",
"%d": "('0'+d.getDate()).slice(-2)",
"%-d": "d.getDate()",
"%HH": "('0'+getHours(d)).slice(-2)",
"%HH": "getHours(d)",
"%MM": "('0'+d.getMinutes()).slice(-2)",
"%SS": "('0'+d.getSeconds()).slice(-2)",
"%A": `${js(locale.day)}.split(',')[d.getDay()]`,
@ -191,9 +191,9 @@ function round(n, dp) {
var is12;
function getHours(d) {
var h = d.getHours();
if (is12===undefined) is12 = (require('Storage').readJSON('setting.json',1)||{})["12hour"];
if (!is12) return h;
return (h%12==0) ? 12 : h%12;
if (is12 === undefined) is12 = (require('Storage').readJSON('setting.json', 1) || {})["12hour"];
if (!is12) return ('0' + h).slice(-2);
return ((h % 12 == 0) ? 12 : h % 12).toString();
}
exports = {
name: ${js(locale.lang)},

View File

@ -1,6 +1,7 @@
/* jshint esversion: 6 */
const distanceUnits = { // how many meters per X?
"m": 1,
"ft": 0.3048,
"yd": 0.9144,
"mi": 1609.34,
"km": 1000,
@ -160,7 +161,7 @@ var locales = {
currency_symbol: "$", currency_first: true,
int_curr_symbol: "USD",
speed: "mph",
distance: { 0: "m", 1: "mi" },
distance: { 0: "ft", 1: "mi" },
temperature: "°F",
ampm: { 0: "am", 1: "pm" },
timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },

View File

@ -84,8 +84,8 @@ function showMapMessage(msg) {
if (msg.distance!==undefined)
distance = require("locale").distance(msg.distance);
if (msg.instr) {
if (msg.instr.includes("towards")) {
m = msg.instr.split("towards");
if (msg.instr.includes("towards") || msg.instr.includes("toward")) {
m = msg.instr.split(/towards|toward/);
target = m[0].trim();
street = m[1].trim();
}else

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);

3
apps/ohmcalc/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
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.

22
apps/ohmcalc/README.md Normal file
View File

@ -0,0 +1,22 @@
# Ohm's Law Calculator
This is a simple and intuitive Ohm's Law Calculator application designed for the Bangle.js 2 Smartwatch. The calculator focuses on providing an easy-to-navigate user interface with automatic calculation logic, allowing you to quickly and efficiently calculate electrical measurements, including Voltage, Current, Resistance, and Power.
## Usage Guide
* __Select the Electrical Measurement:__ On the main menu, select the electrical measurement that you wish to calculate (Voltage, Current, Resistance, or Power).
* __Enter Known Values:__ You will then be prompted to enter the known values. The application will present a menu listing the remaining electrical measurements. Select one and you will be taken to a numeric input screen. Repeat this step for the second known value.
* __View Results:__ Once the two known values have been entered, the application will automatically calculate and display the value of the selected electrical measurement. The Results menu will show the calculated value, the unit of measurement, and the formula used for calculation.
* __Navigation:__ Whether you're deep in calculations or perusing the results, the power to navigate the app remains in your hands. While the 'Back' button is present in most scenarios, it isn't available in the Results menu and the Input screen. However, you can always return to the beginning by selecting 'Main Menu' from the Results menu. And if you're on the numeric Input screen, simply hold down the 'Clear' button to navigate back.
## Compatibility
This app was built and tested with Bangle.js 2.
## Feedback
If you have any issues or suggestions, please open an issue on this GitHub repository. Contributions to improve the application are also welcomed.
## Creator
[stweedo](https://github.com/stweedo)

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4AyQBIvmnM/AAk5F8pWBFwoABMDweGgEHF48HCAw2VE4MGDAgvCRIIACSoIvFgEGG4wuPKAYZCgENn8UgFVqsAik/hoNDC4ouUAAMOLAYoBF4Ok0gGEFwUOTJQuPAAM4DIIJCFAI2GRYM4ZRguNg7pIh0NhpXDAwIVIGBguFdwJlHABbTCgwwPCIgQB1V6dYSXGQ4zzCvWqIwhNBMBwvBvVPbokUKQQADg7BBSQUAp5FBDojANFAQmCAoTxCgEsmc0mkzlgxCMoQwBFwYFBFxgvBgAaCcYcUgEBmdfAA0zCoJiCegc4BIIvNeQwuCg4qEg8rAwowDdhwvLI4IuFLIIHFGAT4EF5rdEJAkHgImHF41fI4p2BAAYtIg8HhpGECwK7FXAQvHBQJIEikNEYIxGgE5UQh2FLw0rlYvHMAwAEnIvDGgIJDigABAwUAlhTGwAvBmgABnQMDlgfDDwRVDMAYvETocOAwSOFLwPVp6wEGAY8BC4MOBgYvMqukgENXwU0Lw3V5+kAALDFBoLaBhsA0lVF6U4F4rFBFgXP6uANoovVR5U6RQlP6ryGR6jvMnTqCYQJeGd6AwBBIYAFRIIiEeQheGr7FBDxE5FwZgCCQMHhqkBIwZTGYYYKGRwJ4DDIMNEYIoCF4YxEAAIWEMBAIBLxhIBAAYtFGYwXEnAmHF4JeFA4J4EAwIrLF5JICGAq9GE4J2EF6JsBI4UNhx5EYY67CFwcOhp3DGB0AFQTQCAoU4AwUsmYAClgHBg5EChwGCAoaNQE4N6p4wDMQIxCAAcHRYYoBp96DoouLgyjE1QZBPYQAEnDmEAAUNIoOqbYkGGBTsFCIL0GABhsCJoZgOFAkHFxEOAASZDNwYVFFxgwHdogJCii+DXoIGCgyXGFxwwHboIoG0mkAwgWBgBnDFyIwFVYQICQgIoBqtVF4TrBBoQXFFyAwDeAI4GnJmDnImGSYIuUJQbKMKxAXGAC4eBF44oeGBCJBAAiVBF0hgCAA4vlGI4tnADg="))

373
apps/ohmcalc/app.js Normal file
View File

@ -0,0 +1,373 @@
let Layout = require("Layout");
// Definitions for units and formulas for electrical measurements.
const UNITS = {
"Voltage (V)": "Volts",
"Current (I)": "Amps",
"Resistance (R)": "Ohms",
"Power (P)": "Watts",
};
const FORMULAS = {
'Voltage (V)': {
'Current (I), Resistance (R)': { equation: "{0} * {1}", display: "V = I * R" },
'Power (P), Current (I)': { equation: "{0} / {1}", display: "V = P / I" },
'Power (P), Resistance (R)': { equation: "Math.sqrt({0} * {1})", display: "V = sqrt(P * R)" }
},
'Current (I)': {
'Voltage (V), Resistance (R)': { equation: "{0} / {1}", display: "I = V / R" },
'Power (P), Voltage (V)': { equation: "{0} / {1}", display: "I = P / V" },
'Power (P), Resistance (R)': { equation: "Math.sqrt({0} / {1})", display: "I = sqrt(P / R)" }
},
'Resistance (R)': {
'Voltage (V), Current (I)': { equation: "{0} / {1}", display: "R = V / I" },
'Power (P), Current (I)': { equation: "{0} / (Math.pow({1}, 2))", display: "R = P / I^2" },
'Power (P), Voltage (V)': { equation: "(Math.pow({1}, 2)) / {0}", display: "R = V^2 / P" }
},
'Power (P)': {
'Voltage (V), Current (I)': { equation: "{0} * {1}", display: "P = V * I" },
'Current (I), Resistance (R)': { equation: "(Math.pow({0}, 2)) * {1}", display: "P = I^2 * R" },
'Voltage (V), Resistance (R)': { equation: "(Math.pow({0}, 2)) / {1}", display: "P = V^2 / R" }
},
};
// Screen positioning settings
let btnSize = 23;
let xCenter = g.getWidth() / 2;
let yCenter = g.getHeight() / 2;
// Variables to hold state
let lastStringWidth = 0;
let halfStringWidth = lastStringWidth / 2;
let calculatedVariable;
let selectedVariable;
let variableValues = {};
let inputStr = "";
let invalidInput = false;
// Function references
let handleEnter;
let showVariableSelectionMenu;
let showInputMenu;
let resultsMenu;
let mainMenu;
// Setup the layout for input buttons
let layout = new Layout({
type: "v",
c: [
{ type: "txt", font: "6x8:3", label: "", id: "label" },
{ type: "h", c: "123".split("").map(i => ({ type: "btn", font: "6x8:3", label: i, cb: () => { handleButtonPress(i); }, fillx: 1, filly: 1 })) },
{ type: "h", c: "456".split("").map(i => ({ type: "btn", font: "6x8:3", label: i, cb: () => { handleButtonPress(i); }, fillx: 1, filly: 1 })) },
{ type: "h", c: "789".split("").map(i => ({ type: "btn", font: "6x8:3", label: i, cb: () => { handleButtonPress(i); }, fillx: 1, filly: 1 })) },
{ type: "h", c: ".0C".split("").map(i => ({ type: "btn", font: "6x8:3", label: i, cb: () => { handleButtonPress(i); }, cbl: i === "C" ? () => { showVariableSelectionMenu(); Bangle.buzz(20); } : undefined, fillx: 1, filly: 1 })) },
{ type: "h", c: [{ type: "btn", font: "6x8:2", label: "Enter", cb: () => { handleEnter(); }, fillx: 1, filly: 1 }] }
]
}, { lazy: false });
// Clears area at the top of the screen where the text is displayed
function clearTextArea() { // Except Back Button
let x2 = g.getWidth();
g.clearRect(0, 0, x2, btnSize);
}
// Function to clear the entire screen, except for the Back button
function clearScreen() {
let x2 = g.getWidth();
let y2 = g.getHeight();
g.clearRect(btnSize, 0, x2, btnSize);
g.clearRect(0, btnSize, x2, y2);
}
// Function to set up and display the calculator input screen
function showCalculatorInputScreen(variable) {
selectedVariable = variable;
layout.setUI();
layout.render();
}
// Function to set and display the current value of the input
// Adjusts the font size to fit the screen width
function setValue(newStr) {
clearTextArea();
inputStr = newStr;
// Here we check the width of the string and adjust the font size
// Start with a standard font size and increase if the string is too wide
let fontSize = "6x8:3"; // start with a standard size
g.setFont(fontSize);
// If the string is too long to fit on the screen, adjust the font size
while (g.stringWidth(inputStr) > g.getWidth() - 10 && fontSize !== "6x8:1") {
fontSize = "6x8:" + (Number(fontSize.split(":")[1]) - 1).toString();
g.setFont(fontSize);
}
layout.label.label = inputStr;
g.drawString(inputStr, layout.label.x, layout.label.y + 11);
lastStringWidth = g.stringWidth(inputStr);
}
// Function to handle the press of a button and append its value to the current input
function handleButtonPress(value) {
Bangle.buzz(20);
if (invalidInput) {
return; // Don't allow input if an invalid input error message is displayed
}
inputStr = value === 'C' ? '' : inputStr + value;
setValue(inputStr);
}
// Function to format the unit of measurement
function formatUnit(unit, value) {
return parseFloat(value) === 1 ? unit.slice(0, -1) : unit;
}
// Calculates the value of the selected variable based on the entered values
// Also handles rounding and trimming of long decimal numbers
function calculateValue(calculatedVariable, variableValues) {
let formulas = FORMULAS[calculatedVariable];
let formulaKeys = Object.keys(formulas);
for (let i = 0; i < formulaKeys.length; i++) {
let formulaKey = formulaKeys[i];
let variables = formulaKey.split(', ');
if (variables.every(variable => variableValues.hasOwnProperty(variable))) {
let formulaData = formulas[formulaKey];
let formula = formulaData.equation;
let formulaDisplay = formulaData.display;
let formulaValues = variables.map(variable => variableValues[variable]);
let calculatedValue = eval(formula.replace(/\{(\d+)\}/g, (_, index) => formulaValues[index]));
// Check if the number is an integer
let isInteger = Math.floor(calculatedValue) === calculatedValue;
// Round and trim long decimal numbers
if (!isInteger) {
calculatedValue = calculatedValue.toFixed(3);
calculatedValue = calculatedValue.replace(/\.0+$/, '').replace(/(\.\d*[1-9])0+$/, '$1');
}
let result = Object.entries(variableValues).map(function (entry) {
let variable = entry[0];
let value = entry[1];
return [variable, `${value} ${formatUnit(UNITS[variable], value.toString())}`];
});
result.push([calculatedVariable, `${calculatedValue} ${formatUnit(UNITS[calculatedVariable], calculatedValue.toString())}`]);
return {
formula: formulaDisplay.replace(/\{(\d+)\}/g, (_, index) => formulaValues[index]),
value: calculatedValue,
unit: formatUnit(UNITS[calculatedVariable], calculatedValue),
result: result,
};
}
}
}
// Main function to initialize the application and setup the main menu
(function () {
mainMenu = {
'': { 'title': 'Ohm\'s Law Calc' },
'< Back': () => Bangle.showClock()
};
Object.keys(UNITS).forEach(unit => {
mainMenu[unit] = () => handleUnitSelection(unit);
});
// Function to present the menu for selecting the variable
// Filters out the calculated variable and already set variables from the menu
showVariableSelectionMenu = function () {
clearScreen();
let variableSelectionMenu = {
'': { 'title': 'Select Variable' },
'< Back': () => {
E.showMenu(mainMenu);
variableValues = {};
}
};
let variables = Object.keys(UNITS);
let remainingVariables = variables.filter(v => v !== calculatedVariable && !variableValues[v]);
remainingVariables.forEach(variable => {
variableSelectionMenu[variable] = function () {
showInputMenu(variable);
};
});
E.showMenu(variableSelectionMenu);
};
// Function to handle the input of variable values
// It sets the current selected variable and displays the calculator input screen
showInputMenu = function (variable) {
setValue("");
selectedVariable = variable;
let inputMenu = {
'': { 'title': variable },
};
E.showMenu(inputMenu);
showCalculatorInputScreen(variable);
};
// Function to handle the event of pressing 'Enter'
// It checks if the input is valid, if so, it saves the value and
// either calculates the result (if enough variables are present) or opens variable selection menu
handleEnter = function () {
// Check if the input is valid
if (inputStr === "" || isNaN(inputStr) || (inputStr.match(/\./g) || []).length > 1) {
// Show error message
setValue("Invalid Input");
invalidInput = true; // Prevent further input
// Clear error message after 2 seconds
setTimeout(() => {
setValue('');
invalidInput = false; // Allow input again
}, 2000);
return;
}
if (calculatedVariable === null) {
return;
}
variableValues[selectedVariable] = parseFloat(inputStr);
if (Object.keys(variableValues).length === 2) {
let result = calculateValue(calculatedVariable, variableValues);
showResultsScreen(result);
calculatedVariable = null;
variableValues = {};
} else {
setValue("");
showVariableSelectionMenu();
return;
}
return;
};
// Function to handle the selection of a unit of electical measurement
function handleUnitSelection(unit) {
calculatedVariable = unit;
showVariableSelectionMenu();
}
// Function to display the results screen with the calculated value
function drawValueScreen
(result) {
let drawPage = function () {
clearScreen();
let fontSize = g.getHeight() / 3;
g.setFontVector(fontSize);
// Reduce the font size until both the value and unit fit on the screen
while (g.stringWidth(result.value) > g.getWidth() - 10 || g.getFontHeight() > g.getHeight() / 2) {
fontSize--;
g.setFontVector(fontSize);
}
let valueY = yCenter - g.getFontHeight() / 2;
let unitY = yCenter + g.getFontHeight() / 2;
let valueWidth = g.stringWidth(result.value);
let unitWidth = g.stringWidth(result.unit);
let valueX = (g.getWidth() - valueWidth) / 2;
let unitX = (g.getWidth() - unitWidth) / 2;
g.drawString(result.value, valueX, valueY);
g.drawString(result.unit, unitX, unitY);
g.flip();
};
// Shows the back button on the value screen
return function () {
clearScreen();
let valueMenu = {
'': { 'title': 'Results' },
'< Back': function () {
E.showMenu(resultsMenu);
}
};
E.showMenu(valueMenu);
drawPage();
};
}
// Function to display the results screen with the values from the result.result array
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];
for (let i = 0; i < result.result.length; i++) {
let currentResult = result.result[i];
let resultTitle = currentResult[0];
let resultValue = currentResult[1];
// Draw title
g.setFontVector(fontSize / 2); // Small font for title
let titleX = (g.getWidth() - g.stringWidth(resultTitle)) / 2;
let titleY = titlePositions[i]; // Get the vertical position for the title
g.drawString(resultTitle, titleX, titleY); // Draw at the desired position
let underlineYPosition = titleY + g.getFontHeight() - 3;
g.drawLine(titleX, underlineYPosition, titleX + g.stringWidth(resultTitle), underlineYPosition); // Draw underline
let valueX;
let valueY = titleY + g.getFontHeight(); // Draw below the title
// Calculate the font size for value dynamically
g.setFontVector(fontSize);
while (g.stringWidth(resultValue) > g.getWidth() - 10) {
fontSize--; // Reduce the font size by 1
g.setFontVector(fontSize);
}
valueY += g.getFontHeight() / 2 + 2;
valueX = (g.getWidth() - g.stringWidth(resultValue)) / 2;
g.drawString(resultValue, valueX, valueY); // Draw at the desired position
// Move down for the next entry
let nextTitleY = (i + 1 < titlePositions.length) ? titlePositions[i + 1] : titleY + 1.5 * fontSize + lineSpacing;
yPosition = nextTitleY;
}
g.flip();
};
// Shows the back button on the result screen
return function() {
clearScreen();
let resultMenu = {
'': { 'title': 'Results' },
'< Back': function() {
E.showMenu(resultsMenu);
}
};
E.showMenu(resultMenu);
drawPage();
};
}
// Shows the results menu with the calculated results and options
function showResultsScreen(result) {
let backButton = function () {
clearScreen();
E.showMenu(resultsMenu);
};
g.clear();
resultsMenu = {
'': { 'title': 'Results' },
'Main Menu': function () {
E.showMenu(mainMenu);
},
};
resultsMenu[result.value + ' ' + result.unit] = drawValueScreen(result);
resultsMenu[result.formula] = drawResultScreen(result);
resultsMenu['Exit'] = function () {
Bangle.showClock();
};
E.showMenu(resultsMenu);
}
clearTextArea();
E.showMenu(mainMenu);
})();

BIN
apps/ohmcalc/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,23 @@
{
"id": "ohmcalc",
"name": "Ohm's Law Calculator",
"shortName": "Ohm's Law Calc",
"version": "0.03",
"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",
"tags": "calculator, utilities, electric",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"storage": [{
"name": "ohmcalc.app.js",
"url": "app.js"
},
{
"name": "ohmcalc.img",
"url": "app-icon.js",
"evaluate": true
}
]
}

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Fixes colors not matching user input from color menu in some cases, 3 bands are now shown larger, various code improvements.

View File

@ -3,75 +3,19 @@
// https://icons8.com/icon/ISAVBnskZod0/resistor
let colorData = {
black: {
value: 0,
multiplier: Math.pow(10, 0),
hex: '#000'
},
brown: {
value: 1,
multiplier: Math.pow(10, 1),
tolerance: 1,
hex: '#8B4513'
},
red: {
value: 2,
multiplier: Math.pow(10, 2),
tolerance: 2,
hex: '#f00'
},
orange: {
value: 3,
multiplier: Math.pow(10, 3),
hex: '#FF9900'
},
yellow: {
value: 4,
multiplier: Math.pow(10, 4),
hex: '#ff0'
},
green: {
value: 5,
multiplier: Math.pow(10, 5),
tolerance: 0.5,
hex: '#0f0'
},
blue: {
value: 6,
multiplier: Math.pow(10, 6),
tolerance: 0.25,
hex: '#00f'
},
violet: {
value: 7,
multiplier: Math.pow(10, 7),
tolerance: 0.1,
hex: '#f0f'
},
grey: {
value: 8,
multiplier: Math.pow(10, 8),
tolerance: 0.05,
hex: '#808080'
},
white: {
value: 9,
multiplier: Math.pow(10, 9),
hex: '#fff'
},
gold: {
multiplier: Math.pow(10, -1),
tolerance: 5,
hex: '#FFD700'
},
silver: {
multiplier: Math.pow(10, -2),
tolerance: 10,
hex: '#C0C0C0'
},
none: {
tolerance: 20
},
black: { value: 0, multiplier: 1, hex: '#000' },
brown: { value: 1, multiplier: 10, tolerance: 1, hex: '#8B4513' },
red: { value: 2, multiplier: 100, tolerance: 2, hex: '#f00' },
orange: { value: 3, multiplier: 1000, hex: '#FF9900' },
yellow: { value: 4, multiplier: 10000, hex: '#ff0' },
green: { value: 5, multiplier: 100000, tolerance: 0.5, hex: '#0f0' },
blue: { value: 6, multiplier: 1000000, tolerance: 0.25, hex: '#00f' },
violet: { value: 7, multiplier: 10000000, tolerance: 0.1, hex: '#f0f' },
grey: { value: 8, multiplier: 100000000, tolerance: 0.05, hex: '#808080' },
white: { value: 9, multiplier: 1000000000, hex: '#fff' },
gold: { multiplier: 0.1, tolerance: 5, hex: '#FFD700' },
silver: { multiplier: 0.01, tolerance: 10, hex: '#C0C0C0' },
none: { tolerance: 20 },
};
function clearScreen() { // Except Back Button
@ -91,38 +35,55 @@ function colorBandsToResistance(colorBands) {
return [resistance, tolerance];
}
function log10(val) {
return Math.log(val) / Math.log(10);
}
function resistanceToColorBands(resistance, tolerance) {
let multiplier = Math.floor(log10(resistance));
let firstDigit = Math.floor(resistance / Math.pow(10, multiplier));
resistance -= firstDigit * Math.pow(10, multiplier);
multiplier--; // for the next digit
let secondDigit = Math.floor(resistance / Math.pow(10, multiplier));
resistance -= secondDigit * Math.pow(10, multiplier);
console.log("First Digit: " + firstDigit);
console.log("Second Digit: " + secondDigit);
console.log("Multiplier: " + multiplier);
let firstBandEntry = Object.entries(colorData).find(function(entry) {
let firstDigit, secondDigit, multiplier;
if (resistance < 1) {
// The resistance is less than 1, so we need to handle this case specially
let count = 0;
while (resistance < 1) {
resistance *= 10;
count++;
}
// Now, resistance is a whole number and count is how many times we had to multiply by 10
let resistanceStr = resistance.toString();
firstDigit = 0; // Set the first band color to be black
secondDigit = Number(resistanceStr.charAt(0)); // Set the second band color to be the significant digit
// Use count to determine the multiplier
multiplier = count === 1 ? 0.1 : 0.01;
} else {
// Convert the resistance to a string so we can manipulate it easily
let resistanceStr = resistance.toString();
if (resistanceStr.length === 1) { // Check if resistance is a single digit
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) {
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;
console.log("Color bands: " + [firstBand, secondBand, multiplierBand, toleranceBand]);
return [firstBand, secondBand, multiplierBand, toleranceBand];
let bands = [firstBand, secondBand, multiplierBand];
if (toleranceBand) bands.push(toleranceBand);
return bands;
}
function drawResistor(colorBands, tolerance) {
@ -130,32 +91,27 @@ function drawResistor(colorBands, tolerance) {
let resistorBodyWidth = 51;
let resistorBodyHeight = 43;
let resistorStartX = 52;
var bandColors = colorBands;
var numcolorBands = bandColors.length;
var resistorStartY = ((g.getHeight() - resistorBodyHeight) / 2) + 48;
let bandColors = colorBands;
let numColorBands = bandColors.length;
let resistorStartY = ((g.getHeight() - resistorBodyHeight) / 2) + 48;
clearScreen();
g.drawImage(img, 0, 112);
var bandWidth = (resistorBodyWidth - (numcolorBands * 2 - 1)) / numcolorBands; // width of each band, accounting for the spacing
var bandHeight = resistorBodyHeight; // height of each band
var currentX = resistorStartX; // starting point for the first band
let bandWidth = (resistorBodyWidth - (numColorBands * 2 - 1)) / numColorBands; // width of each band, accounting for the spacing
let bandHeight = resistorBodyHeight; // height of each band
let currentX = resistorStartX; // starting point for the first band
// Define the tolerance values that will trigger the fourth band
var validTolerances = [1, 2, 0.5, 0.25, 0.1, 0.05, 5, 10];
for (var i = 0; i < numcolorBands; i++) {
let validTolerances = [1, 2, 0.5, 0.25, 0.1, 0.05, 5, 10];
for (let i = 0; i < numColorBands; i++) {
// Skip the fourth band and its outlines if the tolerance is not in the valid list
if (i === 3 && !validTolerances.includes(tolerance)) continue;
var bandX = currentX; // calculate the x-coordinate of the band
var bandY = resistorStartY; // y-coordinate of the band
let bandX = currentX; // calculate the x-coordinate of the band
let bandY = resistorStartY; // y-coordinate of the band
g.setColor(bandColors[i]); // set the color for the band
g.fillRect(bandX, bandY, bandX + bandWidth, bandY + bandHeight);
// Draw band outlines
g.setColor('#000'); // set the color for the outline
g.drawLine(bandX, bandY, bandX, bandY + bandHeight); // left outline
g.drawLine(bandX + bandWidth, bandY, bandX + bandWidth, bandY + bandHeight); // right outline
// if it's the fourth band, shift it over by an additional 12 pixels
if (i === 2) {
currentX = bandX + bandWidth + 5 + 12; // update the current X position for the next band, accounting for the spacing
@ -170,14 +126,14 @@ function omega() {
}
function formatResistance(resistance) {
var units = ["", "k", "M", "G"];
var unitIndex = 0;
let units = ["", "k", "M", "G"];
let unitIndex = 0;
while (resistance >= 1000 && unitIndex < units.length - 1) {
resistance /= 1000;
unitIndex++;
}
// Convert to string and truncate unnecessary zeroes
var resistanceStr = String(resistance);
let resistanceStr = String(resistance);
if (resistanceStr.length > 5) { // if length is more than 5 including decimal point
resistanceStr = resistance.toFixed(2);
}
@ -188,49 +144,40 @@ function formatResistance(resistance) {
}
function drawResistance(resistance, tolerance) {
var x = g.getWidth() / 2;
var y = 40;
var formattedResistance = formatResistance(resistance);
var resistanceStr = formattedResistance.value;
var unit = formattedResistance.unit;
let x = g.getWidth() / 2;
let y = 40;
let formattedResistance = formatResistance(resistance);
let resistanceStr = formattedResistance.value;
let unit = formattedResistance.unit;
g.reset();
// draw resistance value
g.setFontAlign(0, 0).setFont("Vector", 54);
g.clearRect(0, y - 15, g.getWidth(), y + 25); // clear the background
g.drawString(resistanceStr, x + 4, y);
// draw unit, symbol and tolerance
// draw unit, symbol, and tolerance
y += 46;
g.setFontAlign(-1, 0).setFont("Vector", 27);
var toleranceShift = tolerance.toString().replace('.', '').length > 2 ? 8 : 0;
var unitX = ((unit === "M" || unit === "G") ? 0 : 8) - toleranceShift;
var omegaX = (unit ? 46 : 36) - toleranceShift; // Shift the Omega symbol to the left if there is no unit
let toleranceShift = tolerance.toString().replace('.', '').length > 2 ? 8 : 0;
let unitX = ((unit === "M" || unit === "G") ? 0 : 8) - toleranceShift;
let omegaX = (unit ? 46 : 36) - toleranceShift; // Shift the Omega symbol to the left if there is no unit
g.drawString(unit.padStart(3), unitX, y);
// Draw the Ohm symbol to the right of the unit
g.drawImage(omega(), omegaX, y - 13, {
scale: 0.45
});
g.drawImage(omega(), omegaX, y - 13, { scale: 0.45 });
g.setFontAlign(1, 0).setFont("Vector", 27);
// Define the tolerance values that will trigger the fourth band
var validTolerances = [1, 2, 0.5, 0.25, 0.1, 0.05, 5, 10];
let validTolerances = [1, 2, 0.5, 0.25, 0.1, 0.05, 5, 10];
// Check if the tolerance is not in the valid list, and if it's not, set it to 20
if (!validTolerances.includes(tolerance)) {
tolerance = 20;
}
var toleranceStr = "±" + tolerance + "%";
var toleranceX = tolerance.toString().replace('.', '').length > 2 ? 10 : 14;
let toleranceStr = "±" + tolerance + "%";
let toleranceX = tolerance.toString().replace('.', '').length > 2 ? 10 : 14;
g.drawString(toleranceStr.padEnd(4), 176 - toleranceX, y);
}
(function() {
(function () {
let colorBands;
let inputColorBands;
let settings = {
resistance: 0,
tolerance: 0,
@ -244,6 +191,8 @@ function drawResistance(resistance, tolerance) {
colorBands: ["", "", "", ""]
};
settings = emptySettings;
colorBands = null;
inputColorBands = null;
}
function showColorBandMenu(bandNumber) {
@ -251,7 +200,7 @@ function drawResistance(resistance, tolerance) {
'': {
'title': `Band ${bandNumber}`
},
'< Back': function() {
'< Back': function () {
E.showMenu(colorEntryMenu);
},
};
@ -260,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);
@ -289,11 +238,8 @@ function drawResistance(resistance, tolerance) {
function setBandColor(bandNumber, color) {
settings.colorBands[bandNumber - 1] = color; // arrays are 0-indexed
console.log(`Band ${bandNumber} color set to ${color}`);
// Update the color band in the colorEntryMenu
colorEntryMenu[`${bandNumber}:`].value = color;
showColorEntryMenu();
}
@ -302,7 +248,7 @@ function drawResistance(resistance, tolerance) {
'': {
'title': 'Band Color'
},
'< Back': function() {
'< Back': function () {
clearScreen();
E.showMenu(mainMenu);
},
@ -338,9 +284,9 @@ function drawResistance(resistance, tolerance) {
setTimeout(() => showColorBandMenu(4), 5);
}
},
'Draw Resistor': function() {
let colorBands = settings.colorBands;
let values = colorBandsToResistance(colorBands);
'Draw Resistor': function () {
inputColorBands = settings.colorBands;
let values = colorBandsToResistance(inputColorBands);
settings.resistance = values[0];
settings.tolerance = values[1];
showDrawingMenu();
@ -355,7 +301,7 @@ function drawResistance(resistance, tolerance) {
'': {
'title': 'Multiplier'
},
'< Back': function() {
'< Back': function () {
showResistanceEntryMenu();
}
};
@ -367,9 +313,8 @@ function drawResistance(resistance, tolerance) {
let formattedMultiplier = formatMultiplier(multiplierValue);
multiplierMenu[`${formattedMultiplier}`] = () => {
settings.multiplier = multiplierValue;
console.log(`Multiplier changed to: ${settings.multiplier}`);
// Update the value of 'Multiplier' in resistanceEntryMenu
resistanceEntryMenu["Multiplier"] = function() {
resistanceEntryMenu["Multiplier"] = function () {
showMultiplierMenu();
};
showResistanceEntryMenu();
@ -395,7 +340,7 @@ function drawResistance(resistance, tolerance) {
'': {
'title': 'Tolerance'
},
'< Back': function() {
'< Back': function () {
showResistanceEntryMenu();
}
};
@ -406,22 +351,29 @@ function drawResistance(resistance, tolerance) {
let tolerance = parseFloat(colorData[color].tolerance); // Parse the tolerance as a float
toleranceMenu[`${tolerance}%`] = () => {
settings.tolerance = tolerance;
console.log(settings.tolerance);
// Update the value of 'Tolerance (%)' in resistanceEntryMenu
resistanceEntryMenu["Tolerance (%)"] = function() {
resistanceEntryMenu["Tolerance (%)"] = function () {
showToleranceMenu();
};
showResistanceEntryMenu();
};
}
}
E.showMenu(toleranceMenu);
}
function drawResistorAndResistance(resistance, tolerance, multipliedResistance) {
console.log('Draw Resistor clicked');
let colorBands = resistanceToColorBands(multipliedResistance || resistance, tolerance);
function drawResistorAndResistance(resistance, tolerance) {
if (inputColorBands) {
colorBands = inputColorBands.map(color => {
if (colorData.hasOwnProperty(color)) {
return colorData[color].hex;
} else {
return;
}
});
} else {
colorBands = resistanceToColorBands(resistance, tolerance);
}
drawResistor(colorBands, tolerance);
drawResistance(resistance, tolerance);
resetSettings();
@ -431,7 +383,7 @@ function drawResistance(resistance, tolerance) {
'': {
'title': 'Resistance'
},
'< Back': function() {
'< Back': function () {
clearScreen();
E.showMenu(mainMenu);
},
@ -441,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();
}
};
@ -469,25 +421,21 @@ function drawResistance(resistance, tolerance) {
};
resistanceEntryMenu['Ohms'].onchange = v => {
settings.resistance = v || 0;
console.log('Resistance changed to: ', settings.resistance);
};
E.showMenu(resistanceEntryMenu);
}
function showDrawingMenu() {
let drawingMenu = {
'': {
'title': 'Resistor Drawing'
'title': ''
},
'< Back': function() {
'< Back': function () {
clearScreen();
E.showMenu(mainMenu);
},
};
E.showMenu(drawingMenu);
let resistance = settings.resistance * (settings.multiplier || 1);
let tolerance = settings.tolerance;
drawResistorAndResistance(resistance, tolerance);
@ -498,14 +446,14 @@ 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();
},
};
E.showMenu(mainMenu);
})();
})();

View File

@ -3,7 +3,7 @@
"name": "Resistor Calculator",
"shortName": "Resistor Calc",
"icon": "rescalc.png",
"version":"0.01",
"version":"0.02",
"screenshots": [
{"url": "screenshot.png"},
{"url": "screenshot-1.png"},

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

@ -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,3 +1,4 @@
0.01: New App!
0.02: New 'Settings Menu' to choose your favorite color and switch between light or dark themes
0.03: New 'Leading Zero' and 'Date Suffix' options in 'Settings Menu'
0.04: Updated settings menu to better maintain app settings and system settings

View File

@ -37,7 +37,6 @@ let color = appSettings.color !== undefined ? appSettings.color : "#0ff";
let enableLeadingZero = appSettings.enableLeadingZero !== undefined ? appSettings.enableLeadingZero : false;
let enableSuffix = appSettings.enableSuffix !== undefined ? appSettings.enableSuffix : true;
// Draw the time and date
(function () {
let drawTimeout;
@ -46,30 +45,17 @@ let enableSuffix = appSettings.enableSuffix !== undefined ? appSettings.enableSu
var y = g.getHeight() / 2;
g.reset().clearRect(Bangle.appRect);
var date = new Date();
var hour = date.getHours();
var minutes = String(date.getMinutes()).padStart(2, '0');
var locale = require("locale");
var timeStr = locale.time(date, 1);
// Handle 12-hour format
if (is12Hour) {
hour = hour % 12 || 12; // Convert 0 to 12 for 12-hour format
} else {
// If the leading zero option is enabled and hour is less than 10, add leading zero
if (enableLeadingZero && hour < 10) {
hour = '0' + hour;
}
}
var timeStr = hour + ':' + minutes;
// Handle midnight in 12-hour format specifically
if (is12Hour && hour === 0) {
timeStr = '12' + timeStr.substring(2);
// If 24-hour format and leading zero should be removed
if (!is12Hour && !enableLeadingZero && timeStr.charAt(0) === '0') {
timeStr = timeStr.substr(1);
}
g.setFontAlign(0, 0).setFont("LondrinaSolid").setColor(color).drawString(timeStr, x - 1, y);
g.reset().setFontAlign(0, 0).setFont("LondrinaShadow").drawString(timeStr, x - 1, y);
var locale = require("locale");
var dayOfMonth = date.getDate();
var month = locale.month(date, 1).slice(0, 1).toUpperCase() + locale.month(date, 1).slice(1).toLowerCase();
var year = date.getFullYear();
@ -110,4 +96,4 @@ let enableSuffix = appSettings.enableSuffix !== undefined ? appSettings.enableSu
Bangle.loadWidgets();
draw();
setTimeout(Bangle.drawWidgets, 0);
})();
})();

View File

@ -1,7 +1,7 @@
{
"id": "shadowclk",
"name": "Shadow Clock",
"version": "0.03",
"version": "0.04",
"description": "A simple clock using the Londrina font in color with a shadowed outline. Based on the Anton Clock.",
"icon": "app.png",
"screenshots": [{
@ -32,4 +32,4 @@
"data": [{
"name": "shadowclk.json"
}]
}
}

View File

@ -9,9 +9,18 @@
theme: 'light',
enableSuffix: true,
enableLeadingZero: false,
enable12Hour: false // default time mode
enable12Hour: false
}, require('Storage').readJSON("shadowclk.json", true) || {});
// Check if shadowclk is the selected clock
if (sysSettings.clock === "shadowclk.app.js") {
// Sync app settings with system settings
appSettings.theme = sysSettings.theme.dark ? 'dark' : 'light';
if (sysSettings['12hour'] !== undefined) {
appSettings.enable12Hour = sysSettings['12hour'];
}
}
// Colors from 'Light BW' and 'Dark BW' themes
function createThemeColors(mode) {
let cl = x => g.setColor(x).getColor();
@ -37,9 +46,10 @@
// Switch theme and save to storage
function switchTheme(mode) {
if (mode === g.theme.dark) return;
let s = require('Storage').readJSON("setting.json", 1) || {};
s.theme = createThemeColors(mode);
require('Storage').writeJSON("setting.json", s);
sysSettings.theme = createThemeColors(mode);
if (sysSettings.clock === "shadowclk.app.js") {
require('Storage').writeJSON("setting.json", sysSettings);
}
updateTheme(mode);
}
@ -85,8 +95,10 @@
}
function writeTimeModeSetting() {
sysSettings['12hour'] = appSettings.enable12Hour;
require('Storage').writeJSON("setting.json", sysSettings);
if (sysSettings.clock === "shadowclk.app.js") {
sysSettings['12hour'] = appSettings.enable12Hour;
require('Storage').writeJSON("setting.json", sysSettings);
}
}
function showMenu() {

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