From c5b21bf64d593083f132203f39e1c1b16a65fca7 Mon Sep 17 00:00:00 2001 From: NovaDawn999 <106365882+NovaDawn999@users.noreply.github.com> Date: Thu, 25 May 2023 05:53:01 -0500 Subject: [PATCH 01/64] Update app.js - increased legibility - reworked GUI --- apps/Uke/app.js | 47 ++++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/apps/Uke/app.js b/apps/Uke/app.js index c60c49a6b..3c28381c0 100644 --- a/apps/Uke/app.js +++ b/apps/Uke/app.js @@ -72,12 +72,28 @@ var ee = [ var index = 0; var chords = []; +var menu = { + "" : { + "title" : "Uke 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); }, + "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 +103,18 @@ function drawBase() { } function drawChord(chord) { - g.drawString(chord[0], g.getWidth() * 0.5 + 2, 18); + g.drawString(chord[0], g.getWidth() * 0.5 - 3, 18); 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 +123,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); From dc27727157b28ebfc861c0e2adf413ffce7e2fd4 Mon Sep 17 00:00:00 2001 From: NovaDawn999 <106365882+NovaDawn999@users.noreply.github.com> Date: Thu, 25 May 2023 05:53:41 -0500 Subject: [PATCH 02/64] Update metadata.json 0.02 --- apps/Uke/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/Uke/metadata.json b/apps/Uke/metadata.json index 10c3b3e79..8d92718c3 100644 --- a/apps/Uke/metadata.json +++ b/apps/Uke/metadata.json @@ -1,7 +1,7 @@ { "id": "Uke", "name": "Uke Chords", "shortName":"Uke", - "version":"0.01", + "version":"0.02", "description": "Wrist mounted ukulele chords", "icon": "app.png", "tags": "uke, chords", From e4e45b5d2d2d666b789027907c1e67a8e2bbb493 Mon Sep 17 00:00:00 2001 From: NovaDawn999 <106365882+NovaDawn999@users.noreply.github.com> Date: Thu, 25 May 2023 05:54:18 -0500 Subject: [PATCH 03/64] Update ChangeLog --- apps/Uke/ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/Uke/ChangeLog b/apps/Uke/ChangeLog index 5560f00bc..366158b0e 100644 --- a/apps/Uke/ChangeLog +++ b/apps/Uke/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Increased Legibility, GUI rework From 6fb3417f82549145d36e6b67a8491f9475fed2b3 Mon Sep 17 00:00:00 2001 From: NovaDawn999 <106365882+NovaDawn999@users.noreply.github.com> Date: Thu, 25 May 2023 05:57:10 -0500 Subject: [PATCH 04/64] Update README.md --- apps/Uke/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/Uke/README.md b/apps/Uke/README.md index 49ceea1ed..b6236e307 100644 --- a/apps/Uke/README.md +++ b/apps/Uke/README.md @@ -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 From 74d91c60a1a5e3e13e428d69d9385ea782703650 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Thu, 25 May 2023 21:58:33 +0100 Subject: [PATCH 05/64] settings: permit temporarily allowing a BLE connection --- apps/setting/settings.js | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/apps/setting/settings.js b/apps/setting/settings.js index ffea3ddbb..a53cf4777 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -363,17 +363,6 @@ function showWhitelistMenu() { }; } - if (settings.whitelist) settings.whitelist.forEach(function(d){ - menu[d.substr(0,17)] = function() { - E.showPrompt(/*LANG*/'Remove\n'+d).then((v) => { - if (v) { - settings.whitelist.splice(settings.whitelist.indexOf(d),1); - updateSettings(); - } - setTimeout(showWhitelistMenu, 50); - }); - } - }); menu[/*LANG*/'Add Device']=function() { E.showAlert(/*LANG*/"Connect device\nto add to\nwhitelist",/*LANG*/"Whitelist").then(function() { NRF.removeAllListeners('connect'); @@ -389,6 +378,30 @@ function showWhitelistMenu() { showWhitelistMenu(); }); }; + + menu[/*LANG*/'Temporarily Add Device']=function() { + E.showAlert(/*LANG*/"Whitelist disabled\nConnect device",/*LANG*/"Whitelist").then(function() { + NRF.removeAllListeners('connect'); + showWhitelistMenu(); + }); + NRF.removeAllListeners('connect'); // this is sufficient to allow any device to connect + NRF.on('connect', function(addr) { + showWhitelistMenu(); + }); + }; + + if (settings.whitelist) settings.whitelist.forEach(function(d){ + menu[d.substr(0,17)] = function() { + E.showPrompt(/*LANG*/'Remove\n'+d).then((v) => { + if (v) { + settings.whitelist.splice(settings.whitelist.indexOf(d),1); + updateSettings(); + } + setTimeout(showWhitelistMenu, 50); + }); + } + }); + E.showMenu(menu); } From 974ab7f5252666758bce7954e8101cafff536bf8 Mon Sep 17 00:00:00 2001 From: NovaDawn999 <106365882+NovaDawn999@users.noreply.github.com> Date: Fri, 26 May 2023 00:15:42 -0500 Subject: [PATCH 06/64] Create app.js --- apps/guitar/app.js | 159 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 apps/guitar/app.js diff --git a/apps/guitar/app.js b/apps/guitar/app.js new file mode 100644 index 000000000..d48d9009a --- /dev/null +++ b/apps/guitar/app.js @@ -0,0 +1,159 @@ +const stringInterval = 24; +const stringLength = 138; +const fretHeight = 35; +const fingerOffset = 17; +const x = 30; +const y = 32; + +//chords +const cc = [ + "C", + "0X", + "33", + "22", + "x", + "11", + "x" +]; + +const dd = [ + "D", + "0X", + "0X", + "x", + "21", + "33", + "22" +]; + +const gg = [ + "G", + "32", + "21", + "x", + "x", + "x", + "33" +]; + +const am = [ + "Am", + "0x", + "x", + "22", + "23", + "11" +]; + +const em = [ + "Em", + "x", + "22", + "23", + "x", + "x", + "x" +]; + +const aa = [ + "A", + "0X", + "x", + "21", + "22", + "23", + "x" +]; + +const ff = [ + "F", + "0X", + "33", + "34", + "22", + "11", + "11" +]; + +var ee = [ + "E", + "x", + "22", + "23", + "11", + "x", + "x" +]; + +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); }, + "About" : function() { + E.showMessage( + "Created By:\nNovaDawn999", { + title:"About" + } + ); + } +}; + + + +function drawBase() { + for (let i = 0; i < 6; i++) { + g.drawLine(x + i * stringInterval, y, x + i * stringInterval, y + stringLength); + g.fillRect(x- 1, y + i * fretHeight - 1, x + stringInterval * 5 + 1, y + i * fretHeight + 1); + } +} + +function drawChord(chord) { + g.drawString(chord[0], g.getWidth() * 0.5 - 3, 18); + 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 - 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 -5, y -fingerOffset + fretHeight * chord[i][0] + 2, true); + g.drawCircle(x + (i - 1) * stringInterval -1, y -fingerOffset + fretHeight * chord[i][0], 10); + } + } +} + +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(); From def056b496b89324b6fbcad58514035f9d985603 Mon Sep 17 00:00:00 2001 From: NovaDawn999 <106365882+NovaDawn999@users.noreply.github.com> Date: Fri, 26 May 2023 00:16:32 -0500 Subject: [PATCH 07/64] Create ChangeLog --- apps/guitar/ChangeLog | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/guitar/ChangeLog diff --git a/apps/guitar/ChangeLog b/apps/guitar/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/guitar/ChangeLog @@ -0,0 +1 @@ +0.01: New App! From 6dc94b96cae6b41289b451b27e518ed7496108a3 Mon Sep 17 00:00:00 2001 From: NovaDawn999 <106365882+NovaDawn999@users.noreply.github.com> Date: Fri, 26 May 2023 00:17:16 -0500 Subject: [PATCH 08/64] Create README.md --- apps/guitar/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 apps/guitar/README.md diff --git a/apps/guitar/README.md b/apps/guitar/README.md new file mode 100644 index 000000000..ad4ecca4a --- /dev/null +++ b/apps/guitar/README.md @@ -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 From 724937205ec00a4fc76532c078674c357fbf2c05 Mon Sep 17 00:00:00 2001 From: NovaDawn999 <106365882+NovaDawn999@users.noreply.github.com> Date: Fri, 26 May 2023 00:19:19 -0500 Subject: [PATCH 09/64] Create app-icon.js --- apps/guitar/app-icon.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/guitar/app-icon.js diff --git a/apps/guitar/app-icon.js b/apps/guitar/app-icon.js new file mode 100644 index 000000000..7a5487de6 --- /dev/null +++ b/apps/guitar/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("AJhMMJApEAPwBjJKEF1aJRBcrRKaMFKJQhgoXQYoMQSMpRRiCF1COCjVRcsplaJFFIoLgnWsLkO73RcpJO7TQwDFKoLkgAD1KpEQsfLQuGRQYkF0+pFBcgRg+o0ki6I0we70i4iXQqIFzQqJ1aiFghmCLUKquwWAC2wxcZ3u7BIjUQQGLjvb3apRaAF0RcD3aDRIICAjuUJQ/IWgepEK6Xwl0euQe77hcaFxYiiw16j3uiMAiKYvh6ivDguP1AuERjCQKvwS8eFxQAIFziQKXjxgUFzxgOFzxgMXjhgQFkAwKLkQAFJAsqAPwAyA")) From 52d45afc1cfc61eeea4cfc9a0042eca08737523e Mon Sep 17 00:00:00 2001 From: NovaDawn999 <106365882+NovaDawn999@users.noreply.github.com> Date: Fri, 26 May 2023 00:20:08 -0500 Subject: [PATCH 10/64] Add files via upload --- apps/guitar/app.png | Bin 0 -> 1798 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/guitar/app.png diff --git a/apps/guitar/app.png b/apps/guitar/app.png new file mode 100644 index 0000000000000000000000000000000000000000..6ff5d79ca34164a3a66ddd3b8a0f8eae81286cd2 GIT binary patch literal 1798 zcmV+h2l@DkP)S(*QIL}#iWQ94#XV3&4bxd^y%O~VVPdERfK z%$jBzz-5Lr^Ue;i!*|a6ob$ZT`vLvvM?bt0%VtiRSgaZMpVy3AXu*BxOZX;v`29yC z$F`i;jFeTZ84sVcJTm}&0f{e6p7mMun5trv@%HA|Ce)mY8S}FeVlozbeI=I82$^$s zQQ(DQld&WBji9fOMvaa*6K8DwJZ7v-uSDQxcq`6Dj~sk5dbF$9WaR0X(dLX<6TUcW zGB%3{sf5Q$`{1p(5EnR8NFI+GSIO}r$P5IadGO0-Es;H`+79y)&YTVYM>BB$|qY|ypNN{F2#6A3e>%!Id-PhSwm zw0U7%J3ovk;)2{F;*01p)rl`nnuESV-ZDRoY41#H5<3;g8()!Nqpy*VnjShH-VU!N zZ;!_lHVp^uacHo`p}8~`r%I#nbe|yaNJOwB30Iv-pe#os_)7w4UxE&M9P)pfi$PvX zzG4;pTr1$HUIogklC=VKrGOR5x|rga2fkh7jpPNj={SDH0$y3`Sr)K19jsZS3;NDH zgc6PT`GLg`*0lc~qyqfJ<8^MP4VBI?KV#*L^ymKe{bZ;T=&cWO6Ex6gZ6>Q1_ zo3`nn^7F4-z#miZCZDw+MA@lC!@zq&x@D&-N z=lTF%w)iu1e#l+q#ZJ!mK)VS$dyTZig26SZc%wcO$J{w+ZOqe&w-tcD?E%|%8)(ZI z$=X~$wptBr-EH8R)uU*+gM6s6Q}qX6#rt}9DviNYJxaVX8UB?i*jk;6rut0Kouk`C zc3g;G`w;wnA6T9_l8t)@(DJ4aLr3}X-RBq^Ga1#@tHHVqJ)E-hio6xP{xR783D{)yWkbFn*S+P#36DR@MK6zK z!kqDZclsz6Xpivhnm}H%Jft!q!Ith+wGzrkHWS=ELcS~(!45N8q?gMB<-xz^>b8-c z7xFhg0dE}!YqkaO#$f}eKIx~(Cr1P`>GfcmXOCm_^COgeu4une@vzP__^&WCReO)~12=S$a!UyKmJ!fAgSDzT+6!y5E%zs`4PsxID*rj)HBg&9YEW<5o|3$R|UjE zRjW|`iy1-PPTnKnHyOsnr}8j)sOmgopO19=q(+QoA95vC?X#<*MD!#YyxQN;C{~CZbY8+;s*>7uq2sgXi?!?-6kb zyqNQ#%7g@4x>I{6d8;m3_5Tfc5xohA8R3;FxGl$i*9al=&)-G}v6B#1t5E-){7t?i zfOBDR zEyrQGRY{oT(xODN8&_E+WNp5`L*(bt+!Ny+VnGfv%M}C00@**6X_} z1pn<4BY#)7u?%&kY%EKJ5AG!#77VUhiLay*VkfyK3cf1ZvX%Q literal 0 HcmV?d00001 From cfa6321f71030e0a11216fe59be5198899109924 Mon Sep 17 00:00:00 2001 From: NovaDawn999 <106365882+NovaDawn999@users.noreply.github.com> Date: Fri, 26 May 2023 00:21:13 -0500 Subject: [PATCH 11/64] Create metadata.json --- apps/guitar/metadata.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 apps/guitar/metadata.json diff --git a/apps/guitar/metadata.json b/apps/guitar/metadata.json new file mode 100644 index 000000000..50c55bece --- /dev/null +++ b/apps/guitar/metadata.json @@ -0,0 +1,14 @@ +{ "id": "Guitar", + "name": "Guitar Chords", + "shortName":"Guitar", + "version":"0.01", + "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} + ] +} From a25c98d3ad4f351474b9775c45bec57a10300d9c Mon Sep 17 00:00:00 2001 From: NovaDawn999 <106365882+NovaDawn999@users.noreply.github.com> Date: Fri, 26 May 2023 00:25:09 -0500 Subject: [PATCH 12/64] Update metadata.json --- apps/guitar/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/guitar/metadata.json b/apps/guitar/metadata.json index 50c55bece..8f98ce44e 100644 --- a/apps/guitar/metadata.json +++ b/apps/guitar/metadata.json @@ -1,4 +1,4 @@ -{ "id": "Guitar", +{ "id": "guitar", "name": "Guitar Chords", "shortName":"Guitar", "version":"0.01", From 6f63528a612c7814b16f59c896ea8f560727b166 Mon Sep 17 00:00:00 2001 From: NovaDawn999 <106365882+NovaDawn999@users.noreply.github.com> Date: Fri, 26 May 2023 00:26:21 -0500 Subject: [PATCH 13/64] Update app-icon.js --- apps/guitar/app-icon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/guitar/app-icon.js b/apps/guitar/app-icon.js index 7a5487de6..490541b44 100644 --- a/apps/guitar/app-icon.js +++ b/apps/guitar/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("AJhMMJApEAPwBjJKEF1aJRBcrRKaMFKJQhgoXQYoMQSMpRRiCF1COCjVRcsplaJFFIoLgnWsLkO73RcpJO7TQwDFKoLkgAD1KpEQsfLQuGRQYkF0+pFBcgRg+o0ki6I0we70i4iXQqIFzQqJ1aiFghmCLUKquwWAC2wxcZ3u7BIjUQQGLjvb3apRaAF0RcD3aDRIICAjuUJQ/IWgepEK6Xwl0euQe77hcaFxYiiw16j3uiMAiKYvh6ivDguP1AuERjCQKvwS8eFxQAIFziQKXjxgUFzxgOFzxgMXjhgQFkAwKLkQAFJAsqAPwAyA")) +require("heatshrink").decompress(atob("mEwwkCkQA/AGMkoQXVptEFytEogwUCoIYBLqlUGIIXTopHVknUoXULylNpouUIoKmUUi0hoMUailEiMSCR/d7pdECx8tC4IYBolULqAWC7qLSFwfdiKMRC4dEoK6RFwYWBppdW7vSLiPd6gXPConVgIWCYYYtM9vdosUqgXOFwndilBqoGDLh/eqtEioXR9xHCoIXDO5SKEpvU6kVppeQ73kqgwB7wuNOwosEXqSlB9xFNR49RpwXV6pICIxhIF73ePAIXTAAgXOJApePGBQXPGA4XPGAxeOGBAWQDAouRDAgWUAH4AZ")) From 90276239cbbe8c8fa5bbc5dd13c761c8ca31210d Mon Sep 17 00:00:00 2001 From: pancake Date: Sun, 28 May 2023 20:37:15 +0200 Subject: [PATCH 14/64] Fix autogenerated hiragana bitmaps and speedup next/prev kanas --- apps/kanawatch/ChangeLog | 1 + apps/kanawatch/README.md | 2 +- apps/kanawatch/app.js | 78 ++++++++++++++++++++---------------- apps/kanawatch/metadata.json | 2 +- 4 files changed, 46 insertions(+), 37 deletions(-) diff --git a/apps/kanawatch/ChangeLog b/apps/kanawatch/ChangeLog index b2d2bab86..9144364d9 100644 --- a/apps/kanawatch/ChangeLog +++ b/apps/kanawatch/ChangeLog @@ -5,3 +5,4 @@ 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 diff --git a/apps/kanawatch/README.md b/apps/kanawatch/README.md index e213949dc..87a9750b1 100644 --- a/apps/kanawatch/README.md +++ b/apps/kanawatch/README.md @@ -12,7 +12,7 @@ cards for learning. ## Author -Written by pancake in 2022, powered by insomnia +Written by pancake in 2022, maintained during 2023 and powered by insomnia ## Screenshots diff --git a/apps/kanawatch/app.js b/apps/kanawatch/app.js index 264058230..793104def 100644 --- a/apps/kanawatch/app.js +++ b/apps/kanawatch/app.js @@ -58,62 +58,76 @@ katakana['RE'] = image(51, 51, "//AAocf/AFDgf/CQl/8AFDh/8AocB/+AAwc/+AFDg/+GX4EC 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 + 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"); + + +hiragana['NA'] = image(55, 49, "gEP+AGEj/gAwk/4EAkAGCv+AgAPD/8AgYdCgP+EgkD/gdB/AGBg4DBv4GCj/w/wGCv////8AwQFB//4AwMBAwXwEQMDAwXgAwMHAwXAAwMPAwWAG4QvBLgQGBL4X/AwRfBKgIGCL4X8n/gLARUBn5YDMwM8NQaLBQYIoCAQSIDAQRZBRYaBDRYQhBFAIJCKIYyCDwKoBToZkBOAIJBPYKLCGwMH/h2CAwMfKoKKCI4PgSIYYB4afDJQMP/gpB+AhBMgIjB/AhC4EfAwIhCEoIGCwJdBaIIZBMgSkCjhMBgakBG4LICUgKDBAwQuBPgRKCjgGE4EQAwgEBAAIbBRAQACQgIDB"); +hiragana['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=="); +hiragana['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"); +hiragana['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"); +hiragana['NO'] = image(60, 50, "AAX//4GEv4HFj4GB/wGCg4GB//4AwMBAwX/4AcEDwcPAwYWBgYGDCwQVC54tCCoX8F4PgFYP4CYI+BgE//0P/gaB/ARB4F/4ApBwAVBg4OBj/8EgITB4AiB4InBBwQgBCAIOCPQPjD4MPJ4MH/0/+ALBwARB84kBBwQ0Bv/gBwc/+5bBj5tEHAR8Bn5lBBwInBBxY2CBwcDWIQOEGwIODJwIOFIoRKC4CNCBQP3AgKwCDIIOBKIQKB8/8IQJgBj4OB8E/MAfD/ytBEgX8J4KeBZwWDIgJCBCoP4ZgIzCAYIqBeYRQB8DnCK4gGBGoIDBwAyBF4IKCCQWBAwIVBEoPgF4RFBg/4F4Q2BAAQOBTwIADHoQADbIQAIA"); + +hiragana['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"); +hiragana['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="); +hiragana['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=="); +hiragana['HE'] = image(67, 45, "AAXwA43/4AHFn/8A4sPCA0B//+CAt///gA4kfCA0H/4QGA4IyFn4IBGQg5BIYsD//nCAt//F/CAkf/wzBCAYFBwH//BaE8ArBwBzFCAgNBLoQQCHIPADYIQD/6dBCAk/OQIQEHIQQEHIQkCCARaBO4YUCSYQQDHIQQFHIQQERQgQCLQQQEHIKBDCAPAn5fDCAP8gbNECAaJDCAbVECAPgvj+Gg72GdoqYFCAgHFKIoQDDA0AKIjODDA0ARYQAEhwHGAAIA=="); +hiragana['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"); + +hiragana['MA'] = image(50, 49, "AAMH/gFDgP/Bgl/4AFDj/wDBsH/4AD/oFE/9/AwoARJVXhAon4JQn+j4MEw4YLn4YEJTIfCAooYCAoX4DgQwCwBdEBgMDHoYMB//3Bgd/8AUC4A7BJQP//kHBwQGB4JYBFoX8KgMP/gGBz/+h//AIPjGAXA//wAoXwh/4DgX4gP8IgQnCF4QFBgOAEIKIEv6SCAAIA=="); +hiragana['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"); +hiragana['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"); +hiragana['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"); +hiragana['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="); + +hiragana['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="); +hiragana['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"); +hiragana['YO'] = image(49, 49, "AAMP/AFDg/8AocD/wFDgP/DAn/wAFDv/AAoc/8AFDj/wGCH/AAIwDAAImCAoQmCv4FBEwU/AoImCj4FBEwUPAoJXCMO4wEM4IWDI4IwCKYQwCL4oFCDAQFDCQIXCNgQFDEoMP/iSC+EHEIJ5CAoSSCwYaBEwXhFoMf8Y4BEAJnBCYN/+Ef/AuBz41CLoPPUQd/4YFDj/AAocD4AuBPIXgDQJ/En6REA="); + +hiragana['RA'] = image(47, 49, "gEP4AFDn//Aod///wAoX///+AgMDAoP/DIMHAoX4AowjC//gh/4gIXCj4mBj4wBn/gEoP8GYI/CvAzBwAFBkAaBIgYTCAAUHGARcCJ4YrBFAJcD4AZDFAI/CFAMPJYQOBK4XwLgZdBJwIFDMIQFCQod/+AIBOIXzO4nnRIQRB55dDDYJdDHgQEBIgM/OgUD/0+Nof8jBtDOYk/OYgyDYgQhCPwLOCFoQ4DMwIcCPYSBCAATkECwKBDCwIVCFoQFCIgSNCHASNBGIQA=="); +hiragana['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"); +hiragana['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"); +hiragana['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"); +hiragana['RO'] = image(50, 49, "AAf4gEB/4AC8EAv4FC/kAj4MDwEHAofwDAgSBDAoACn/+AocfAokP/4FDE4OAApED//AAohJBAAI5BAocAIQIFEHghFCD4QFCBoU/KIQMBNQZ9BOAhOCQYYFE/B8CE4QFBM4JGB4YuDj/7AocD/xIE/+fP4c/84FDh/8QoZyBj5mE4aFDn5yEDAIFDGIIFDIgIXDDwKREv4eEv4eBiAFCDwMH+A8BIQLnEEgLnDSooqBQYQFCDgQ2DAoolCJAQA="); + +hiragana['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="); +hiragana['WO'] = image(52, 49, "AAMf+AFDgP+Bgk/8AFDgYMM/gkD/4AC+EBAof/BkA5FhEAg45Cg/AgF/AQMBBIMP/4DB//gE4Xwn5dBn4GB74IBgY0Fv4FD8AfBAoYfB/gbBIAIiBg///A7B/+A/4rBCQIxBBAISB/ghBCQeBEoIMBCQI0BBgQSCDIYSB54MBgIlB+AMCj0H/0PBgIABHQQMBOgP4BgZBBBwTDCMYIMDKIIMRWQQmDAwUMYYqyBAoaxBN4IMEV4QMCcggMBWwbZCAweA"); // XXX there's no WO in hiragana, so we fill it with a copy of the katakana char +hiragana['N'] = image(54, 49, "AAMHAwsf8AGE/+AAocD/wTF+AGEv/ACZUP/ATKgP/CYv8Awk/IQgTBIQkHCYxCFCYxWTIQxWGFAhCBAwkPAwJCE/5KDCYQiBhhCBAwJlBn+Aj/+/49BDoP/8IDBgf8IQIDBKgUf/EPLAJUBv/gn/AFgKZCAIMHCIP4DQSXBAIIaC/+BCIIaBYwKZCLwIuBCYLRCFwIKBEYX/CYUfEYP4TIRACCYQ+BwZUBDwIYBOgITCRAQVCEIP//0BYISjB+CtDUYRNBAwQ5Bg7gDBQIA="); /// ///////////////////////////////////////// let kana = katakana.KA; let scroll = 0; - +const keys = Object.keys(katakana).sort(); let hiramode = false; let curkana = 'KA'; + function next () { let found = false; - for (const k of Object.keys(katakana).sort()) { + for (const k of keys) { if (found) { kana = hiramode ? hiragana[k] : katakana[k]; curkana = k; @@ -129,7 +143,6 @@ function next () { function randKana() { try { - const keys = Object.keys(katakana); const total = keys.length; let index = 0 | (Math.random() * total); curkana = keys[index]; @@ -141,7 +154,7 @@ function randKana() { function prev () { let oldk = ''; let count = 0; - for (const k of Object.keys(katakana).sort()) { + for (const k of keys) { if (curkana === k) { if (count > 0) { curkana = oldk; @@ -155,11 +168,6 @@ function prev () { updateWatch(ohhmm); } -const kanacolors = { - A: [] -}; - - function updateWatch (hhmm) { g.setFontAlign(-1, -1, 0); g.setBgColor(0, 0, 0); diff --git a/apps/kanawatch/metadata.json b/apps/kanawatch/metadata.json index d7a6f8c23..d1fb70b53 100644 --- a/apps/kanawatch/metadata.json +++ b/apps/kanawatch/metadata.json @@ -2,7 +2,7 @@ "id": "kanawatch", "name": "Kanawatch", "shortName": "Kanawatch", - "version": "0.07", + "version": "0.08", "type": "clock", "description": "Learn Hiragana and Katakana", "icon": "app.png", From 77fd3a0c7d8c16b71f53999d4f94054ed153df80 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 29 May 2023 10:12:46 +0100 Subject: [PATCH 15/64] Revert "settings: permit temporarily allowing a BLE connection" This reverts commit 74d91c60a1a5e3e13e428d69d9385ea782703650. --- apps/setting/settings.js | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/apps/setting/settings.js b/apps/setting/settings.js index a53cf4777..ffea3ddbb 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -363,6 +363,17 @@ function showWhitelistMenu() { }; } + if (settings.whitelist) settings.whitelist.forEach(function(d){ + menu[d.substr(0,17)] = function() { + E.showPrompt(/*LANG*/'Remove\n'+d).then((v) => { + if (v) { + settings.whitelist.splice(settings.whitelist.indexOf(d),1); + updateSettings(); + } + setTimeout(showWhitelistMenu, 50); + }); + } + }); menu[/*LANG*/'Add Device']=function() { E.showAlert(/*LANG*/"Connect device\nto add to\nwhitelist",/*LANG*/"Whitelist").then(function() { NRF.removeAllListeners('connect'); @@ -378,30 +389,6 @@ function showWhitelistMenu() { showWhitelistMenu(); }); }; - - menu[/*LANG*/'Temporarily Add Device']=function() { - E.showAlert(/*LANG*/"Whitelist disabled\nConnect device",/*LANG*/"Whitelist").then(function() { - NRF.removeAllListeners('connect'); - showWhitelistMenu(); - }); - NRF.removeAllListeners('connect'); // this is sufficient to allow any device to connect - NRF.on('connect', function(addr) { - showWhitelistMenu(); - }); - }; - - if (settings.whitelist) settings.whitelist.forEach(function(d){ - menu[d.substr(0,17)] = function() { - E.showPrompt(/*LANG*/'Remove\n'+d).then((v) => { - if (v) { - settings.whitelist.splice(settings.whitelist.indexOf(d),1); - updateSettings(); - } - setTimeout(showWhitelistMenu, 50); - }); - } - }); - E.showMenu(menu); } From 37c0571b972c3621af796b1c576c8dba839cc7bd Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 29 May 2023 10:13:41 +0100 Subject: [PATCH 16/64] settings: Make Connectable temporarily bypasses the whitelist --- apps/boot/ChangeLog | 1 + apps/boot/bootupdate.js | 2 +- apps/boot/metadata.json | 2 +- apps/setting/ChangeLog | 3 ++- apps/setting/metadata.json | 2 +- apps/setting/settings.js | 2 ++ 6 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index 82e55fa91..d7405e763 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -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 diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index 84745b792..a12d41e1b 100644 --- a/apps/boot/bootupdate.js +++ b/apps/boot/bootupdate.js @@ -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. diff --git a/apps/boot/metadata.json b/apps/boot/metadata.json index c652f6136..0a4e7e9d1 100644 --- a/apps/boot/metadata.json +++ b/apps/boot/metadata.json @@ -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", diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index 42bac0ea7..d090add58 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -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 \ No newline at end of file + LCD calibration will now error if the calibration is obviously wrong +0.61: Permit temporary bypass of the BLE whitelist diff --git a/apps/setting/metadata.json b/apps/setting/metadata.json index 20213e81f..b2b19dd6b 100644 --- a/apps/setting/metadata.json +++ b/apps/setting/metadata.json @@ -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", diff --git a/apps/setting/settings.js b/apps/setting/settings.js index ffea3ddbb..d22f28412 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -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(); }); } From 78995305cbca71e1dab05260261b5b92189e8faa Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 29 May 2023 11:16:59 +0100 Subject: [PATCH 17/64] kbmulti: add ability to lowercase after first capital --- apps/kbmulti/ChangeLog | 1 + apps/kbmulti/lib.js | 11 +++++++++++ apps/kbmulti/metadata.json | 2 +- apps/kbmulti/settings.js | 4 ++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/kbmulti/ChangeLog b/apps/kbmulti/ChangeLog index 4ef8f7bda..defae902b 100644 --- a/apps/kbmulti/ChangeLog +++ b/apps/kbmulti/ChangeLog @@ -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. diff --git a/apps/kbmulti/lib.js b/apps/kbmulti/lib.js index 9b642a132..01be03feb 100644 --- a/apps/kbmulti/lib.js +++ b/apps/kbmulti/lib.js @@ -96,12 +96,14 @@ exports.input = function(options) { } 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,15 +111,24 @@ 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); } + function retireCurrent(why) { + if (caps && settings.autoLowercase) + setCaps(); + } + var moveMode = false; function onSwipe(dirLeftRight, dirUpDown) { diff --git a/apps/kbmulti/metadata.json b/apps/kbmulti/metadata.json index 510454f79..0b44b0306 100644 --- a/apps/kbmulti/metadata.json +++ b/apps/kbmulti/metadata.json @@ -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", diff --git a/apps/kbmulti/settings.js b/apps/kbmulti/settings.js index 96e72b290..0a70bab23 100644 --- a/apps/kbmulti/settings.js +++ b/apps/kbmulti/settings.js @@ -21,6 +21,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) From 192adef634e4c29f92e5dc2fcfff28b96efa9785 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 29 May 2023 11:39:55 +0100 Subject: [PATCH 18/64] kbmulti: remove displayText() false - use undefined --- apps/kbmulti/lib.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/kbmulti/lib.js b/apps/kbmulti/lib.js index 01be03feb..519860b51 100644 --- a/apps/kbmulti/lib.js +++ b/apps/kbmulti/lib.js @@ -89,7 +89,7 @@ exports.input = function(options) { } function newCharacter(ch) { - displayText(); + displayText(false); if (ch && textIndex < text.length) textIndex ++; charCurrent = ch; charIndex = 0; @@ -121,7 +121,7 @@ exports.input = function(options) { newCharacter(); retireCurrent(); }, settings.charTimeout); - displayText(charTimeout); + displayText(true); } function retireCurrent(why) { From f3f223eff52c68cda2198561c3081c7ac2057d27 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 29 May 2023 16:20:13 +0100 Subject: [PATCH 19/64] notify: permit 0x0 colour and fallback depending on theme --- apps/notify/ChangeLog | 1 + apps/notify/metadata.json | 2 +- apps/notify/notify_bjs1.js | 2 +- apps/notify/notify_bjs2.js | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/notify/ChangeLog b/apps/notify/ChangeLog index d7b754ff9..f40490488 100644 --- a/apps/notify/ChangeLog +++ b/apps/notify/ChangeLog @@ -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 diff --git a/apps/notify/metadata.json b/apps/notify/metadata.json index 1cc8f52c1..bab68127c 100644 --- a/apps/notify/metadata.json +++ b/apps/notify/metadata.json @@ -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", diff --git a/apps/notify/notify_bjs1.js b/apps/notify/notify_bjs1.js index fb56e4bbc..4ff8f88e9 100644 --- a/apps/notify/notify_bjs1.js +++ b/apps/notify/notify_bjs1.js @@ -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); diff --git a/apps/notify/notify_bjs2.js b/apps/notify/notify_bjs2.js index c202e8c55..456c4e929 100644 --- a/apps/notify/notify_bjs2.js +++ b/apps/notify/notify_bjs2.js @@ -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); From 6e48f7074aad8f88a30ac30d08da0ffdbf5dd69f Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 29 May 2023 21:12:04 +0100 Subject: [PATCH 20/64] kbmulti: default autoLowercase to true --- apps/kbmulti/lib.js | 1 + apps/kbmulti/settings.js | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/kbmulti/lib.js b/apps/kbmulti/lib.js index 519860b51..505132040 100644 --- a/apps/kbmulti/lib.js +++ b/apps/kbmulti/lib.js @@ -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"); diff --git a/apps/kbmulti/settings.js b/apps/kbmulti/settings.js index 0a70bab23..08dbb1925 100644 --- a/apps/kbmulti/settings.js +++ b/apps/kbmulti/settings.js @@ -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; } From eab0e8c620f7175f83e7be05a9d52a2a40e50938 Mon Sep 17 00:00:00 2001 From: storm64 Date: Mon, 29 May 2023 23:45:27 +0200 Subject: [PATCH 21/64] [sleeplogalarm] Correct hide + replace all `var` --- apps/sleeplogalarm/ChangeLog | 3 ++- apps/sleeplogalarm/README.md | 4 ++-- apps/sleeplogalarm/lib.js | 20 ++++++++++---------- apps/sleeplogalarm/metadata.json | 2 +- apps/sleeplogalarm/settings.js | 20 ++++++++++---------- apps/sleeplogalarm/widget.js | 11 +++++++---- 6 files changed, 32 insertions(+), 28 deletions(-) diff --git a/apps/sleeplogalarm/ChangeLog b/apps/sleeplogalarm/ChangeLog index 80f8bd7e4..e45667697 100644 --- a/apps/sleeplogalarm/ChangeLog +++ b/apps/sleeplogalarm/ChangeLog @@ -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 \ No newline at end of file +0.04: Make "disable alarm" possible on next day; correct alarm filtering; improve settings +0.041: Correct hide function + replace all `var` with `let`. diff --git a/apps/sleeplogalarm/README.md b/apps/sleeplogalarm/README.md index 005377fb1..8da369eb3 100644 --- a/apps/sleeplogalarm/README.md +++ b/apps/sleeplogalarm/README.md @@ -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_ diff --git a/apps/sleeplogalarm/lib.js b/apps/sleeplogalarm/lib.js index 343e811af..48fecdb5f 100644 --- a/apps/sleeplogalarm/lib.js +++ b/apps/sleeplogalarm/lib.js @@ -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.hidden) 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) { diff --git a/apps/sleeplogalarm/metadata.json b/apps/sleeplogalarm/metadata.json index fd85507e6..989579d58 100644 --- a/apps/sleeplogalarm/metadata.json +++ b/apps/sleeplogalarm/metadata.json @@ -2,7 +2,7 @@ "id":"sleeplogalarm", "name":"Sleep Log Alarm", "shortName": "SleepLogAlarm", - "version": "0.04", + "version": "0.041", "description": "Enhance your morning and let your alarms wake you up when you are in light sleep.", "icon": "app.png", "type": "widget", diff --git a/apps/sleeplogalarm/settings.js b/apps/sleeplogalarm/settings.js index 1f3a13272..d797ae6bc 100644 --- a/apps/sleeplogalarm/settings.js +++ b/apps/sleeplogalarm/settings.js @@ -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 diff --git a/apps/sleeplogalarm/widget.js b/apps/sleeplogalarm/widget.js index e3171751f..9bed913c1 100644 --- a/apps/sleeplogalarm/widget.js +++ b/apps/sleeplogalarm/widget.js @@ -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 From 68a7047a8ce7f5fe2cb4af84441bd6f6e3ef8853 Mon Sep 17 00:00:00 2001 From: storm64 Date: Mon, 29 May 2023 23:52:46 +0200 Subject: [PATCH 22/64] [sleeplogalarm] Second try --- apps/sleeplogalarm/ChangeLog | 2 +- apps/sleeplogalarm/lib.js | 2 +- apps/sleeplogalarm/metadata.json | 2 +- apps/sleeplogalarm/widget.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/sleeplogalarm/ChangeLog b/apps/sleeplogalarm/ChangeLog index e45667697..cf79179c4 100644 --- a/apps/sleeplogalarm/ChangeLog +++ b/apps/sleeplogalarm/ChangeLog @@ -2,4 +2,4 @@ 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.041: Correct hide function + replace all `var` with `let`. +0.042: Correct hide function + replace all `var` with `let`. diff --git a/apps/sleeplogalarm/lib.js b/apps/sleeplogalarm/lib.js index 48fecdb5f..609a45fde 100644 --- a/apps/sleeplogalarm/lib.js +++ b/apps/sleeplogalarm/lib.js @@ -68,7 +68,7 @@ exports = { if (!this.time) return; // set widget width if not hidden - if (!settings.hidden) this.width = 8; + if (!settings.wid.hide) this.width = 8; // insert sleeplogalarm conditions and function sleeplog.trigger.sleeplogalarm = { diff --git a/apps/sleeplogalarm/metadata.json b/apps/sleeplogalarm/metadata.json index 989579d58..3079435b0 100644 --- a/apps/sleeplogalarm/metadata.json +++ b/apps/sleeplogalarm/metadata.json @@ -2,7 +2,7 @@ "id":"sleeplogalarm", "name":"Sleep Log Alarm", "shortName": "SleepLogAlarm", - "version": "0.041", + "version": "0.042", "description": "Enhance your morning and let your alarms wake you up when you are in light sleep.", "icon": "app.png", "type": "widget", diff --git a/apps/sleeplogalarm/widget.js b/apps/sleeplogalarm/widget.js index 9bed913c1..a62782604 100644 --- a/apps/sleeplogalarm/widget.js +++ b/apps/sleeplogalarm/widget.js @@ -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 = { From 6529533aee8cf9130b31f8d3bc761d2a33253f59 Mon Sep 17 00:00:00 2001 From: storm64 Date: Tue, 30 May 2023 00:01:08 +0200 Subject: [PATCH 23/64] [sleeplogalarm] Correct version after testing --- apps/sleeplogalarm/ChangeLog | 2 +- apps/sleeplogalarm/metadata.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sleeplogalarm/ChangeLog b/apps/sleeplogalarm/ChangeLog index cf79179c4..286221777 100644 --- a/apps/sleeplogalarm/ChangeLog +++ b/apps/sleeplogalarm/ChangeLog @@ -2,4 +2,4 @@ 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.042: Correct hide function + replace all `var` with `let`. +0.05: Correct hide function + replace all `var` with `let`. diff --git a/apps/sleeplogalarm/metadata.json b/apps/sleeplogalarm/metadata.json index 3079435b0..30d3dcda7 100644 --- a/apps/sleeplogalarm/metadata.json +++ b/apps/sleeplogalarm/metadata.json @@ -2,7 +2,7 @@ "id":"sleeplogalarm", "name":"Sleep Log Alarm", "shortName": "SleepLogAlarm", - "version": "0.042", + "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", From 7be4bfb05edf9916b372fce9ca1fc21f8aab6d72 Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Mon, 29 May 2023 18:17:06 -0500 Subject: [PATCH 24/64] Update messagegui.app.js To include "toward" as well for better compatibility when splitting street name directions up --- apps/messagegui/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/messagegui/app.js b/apps/messagegui/app.js index 7ce568f82..ccc6acec6 100644 --- a/apps/messagegui/app.js +++ b/apps/messagegui/app.js @@ -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 From 2b66f370451b4bd0ddf6e462d71891fc4e910ee5 Mon Sep 17 00:00:00 2001 From: pancake Date: Tue, 30 May 2023 03:08:02 +0200 Subject: [PATCH 25/64] kanawatch: Optimize loading+rendering and add transition animations --- apps/kanawatch/ChangeLog | 2 + apps/kanawatch/app.js | 450 +++++++++++++++++++----------- apps/kanawatch/metadata.json | 5 +- apps/kanawatch/screenshot.old.png | Bin 0 -> 2992 bytes apps/kanawatch/screenshot.png | Bin 2992 -> 2653 bytes apps/kanawatch/screenshot2.png | Bin 0 -> 2689 bytes 6 files changed, 297 insertions(+), 160 deletions(-) create mode 100644 apps/kanawatch/screenshot.old.png create mode 100644 apps/kanawatch/screenshot2.png diff --git a/apps/kanawatch/ChangeLog b/apps/kanawatch/ChangeLog index 9144364d9..1521bed73 100644 --- a/apps/kanawatch/ChangeLog +++ b/apps/kanawatch/ChangeLog @@ -6,3 +6,5 @@ 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 diff --git a/apps/kanawatch/app.js b/apps/kanawatch/app.js index 793104def..2aa5c2a3c 100644 --- a/apps/kanawatch/app.js +++ b/apps/kanawatch/app.js @@ -3,121 +3,134 @@ 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"); + +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 -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=="); +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=="), -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=="); +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=="), -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=="); +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=="), -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="); +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="), -hiragana['NA'] = image(55, 49, "gEP+AGEj/gAwk/4EAkAGCv+AgAPD/8AgYdCgP+EgkD/gdB/AGBg4DBv4GCj/w/wGCv////8AwQFB//4AwMBAwXwEQMDAwXgAwMHAwXAAwMPAwWAG4QvBLgQGBL4X/AwRfBKgIGCL4X8n/gLARUBn5YDMwM8NQaLBQYIoCAQSIDAQRZBRYaBDRYQhBFAIJCKIYyCDwKoBToZkBOAIJBPYKLCGwMH/h2CAwMfKoKKCI4PgSIYYB4afDJQMP/gpB+AhBMgIjB/AhC4EfAwIhCEoIGCwJdBaIIZBMgSkCjhMBgakBG4LICUgKDBAwQuBPgRKCjgGE4EQAwgEBAAIbBRAQACQgIDB"); -hiragana['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=="); -hiragana['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"); -hiragana['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"); -hiragana['NO'] = image(60, 50, "AAX//4GEv4HFj4GB/wGCg4GB//4AwMBAwX/4AcEDwcPAwYWBgYGDCwQVC54tCCoX8F4PgFYP4CYI+BgE//0P/gaB/ARB4F/4ApBwAVBg4OBj/8EgITB4AiB4InBBwQgBCAIOCPQPjD4MPJ4MH/0/+ALBwARB84kBBwQ0Bv/gBwc/+5bBj5tEHAR8Bn5lBBwInBBxY2CBwcDWIQOEGwIODJwIOFIoRKC4CNCBQP3AgKwCDIIOBKIQKB8/8IQJgBj4OB8E/MAfD/ytBEgX8J4KeBZwWDIgJCBCoP4ZgIzCAYIqBeYRQB8DnCK4gGBGoIDBwAyBF4IKCCQWBAwIVBEoPgF4RFBg/4F4Q2BAAQOBTwIADHoQADbIQAIA"); +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="), -hiragana['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"); -hiragana['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="); -hiragana['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=="); -hiragana['HE'] = image(67, 45, "AAXwA43/4AHFn/8A4sPCA0B//+CAt///gA4kfCA0H/4QGA4IyFn4IBGQg5BIYsD//nCAt//F/CAkf/wzBCAYFBwH//BaE8ArBwBzFCAgNBLoQQCHIPADYIQD/6dBCAk/OQIQEHIQQEHIQkCCARaBO4YUCSYQQDHIQQFHIQQERQgQCLQQQEHIKBDCAPAn5fDCAP8gbNECAaJDCAbVECAPgvj+Gg72GdoqYFCAgHFKIoQDDA0AKIjODDA0ARYQAEhwHGAAIA=="); -hiragana['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"); +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="), -hiragana['MA'] = image(50, 49, "AAMH/gFDgP/Bgl/4AFDj/wDBsH/4AD/oFE/9/AwoARJVXhAon4JQn+j4MEw4YLn4YEJTIfCAooYCAoX4DgQwCwBdEBgMDHoYMB//3Bgd/8AUC4A7BJQP//kHBwQGB4JYBFoX8KgMP/gGBz/+h//AIPjGAXA//wAoXwh/4DgX4gP8IgQnCF4QFBgOAEIKIEv6SCAAIA=="); -hiragana['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"); -hiragana['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"); -hiragana['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"); -hiragana['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="); - -hiragana['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="); -hiragana['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"); -hiragana['YO'] = image(49, 49, "AAMP/AFDg/8AocD/wFDgP/DAn/wAFDv/AAoc/8AFDj/wGCH/AAIwDAAImCAoQmCv4FBEwU/AoImCj4FBEwUPAoJXCMO4wEM4IWDI4IwCKYQwCL4oFCDAQFDCQIXCNgQFDEoMP/iSC+EHEIJ5CAoSSCwYaBEwXhFoMf8Y4BEAJnBCYN/+Ef/AuBz41CLoPPUQd/4YFDj/AAocD4AuBPIXgDQJ/En6REA="); - -hiragana['RA'] = image(47, 49, "gEP4AFDn//Aod///wAoX///+AgMDAoP/DIMHAoX4AowjC//gh/4gIXCj4mBj4wBn/gEoP8GYI/CvAzBwAFBkAaBIgYTCAAUHGARcCJ4YrBFAJcD4AZDFAI/CFAMPJYQOBK4XwLgZdBJwIFDMIQFCQod/+AIBOIXzO4nnRIQRB55dDDYJdDHgQEBIgM/OgUD/0+Nof8jBtDOYk/OYgyDYgQhCPwLOCFoQ4DMwIcCPYSBCAATkECwKBDCwIVCFoQFCIgSNCHASNBGIQA=="); -hiragana['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"); -hiragana['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"); -hiragana['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"); -hiragana['RO'] = image(50, 49, "AAf4gEB/4AC8EAv4FC/kAj4MDwEHAofwDAgSBDAoACn/+AocfAokP/4FDE4OAApED//AAohJBAAI5BAocAIQIFEHghFCD4QFCBoU/KIQMBNQZ9BOAhOCQYYFE/B8CE4QFBM4JGB4YuDj/7AocD/xIE/+fP4c/84FDh/8QoZyBj5mE4aFDn5yEDAIFDGIIFDIgIXDDwKREv4eEv4eBiAFCDwMH+A8BIQLnEEgLnDSooqBQYQFCDgQ2DAoolCJAQA="); - -hiragana['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="); -hiragana['WO'] = image(52, 49, "AAMf+AFDgP+Bgk/8AFDgYMM/gkD/4AC+EBAof/BkA5FhEAg45Cg/AgF/AQMBBIMP/4DB//gE4Xwn5dBn4GB74IBgY0Fv4FD8AfBAoYfB/gbBIAIiBg///A7B/+A/4rBCQIxBBAISB/ghBCQeBEoIMBCQI0BBgQSCDIYSB54MBgIlB+AMCj0H/0PBgIABHQQMBOgP4BgZBBBwTDCMYIMDKIIMRWQQmDAwUMYYqyBAoaxBN4IMEV4QMCcggMBWwbZCAweA"); // XXX there's no WO in hiragana, so we fill it with a copy of the katakana char -hiragana['N'] = image(54, 49, "AAMHAwsf8AGE/+AAocD/wTF+AGEv/ACZUP/ATKgP/CYv8Awk/IQgTBIQkHCYxCFCYxWTIQxWGFAhCBAwkPAwJCE/5KDCYQiBhhCBAwJlBn+Aj/+/49BDoP/8IDBgf8IQIDBKgUf/EPLAJUBv/gn/AFgKZCAIMHCIP4DQSXBAIIaC/+BCIIaBYwKZCLwIuBCYLRCFwIKBEYX/CYUfEYP4TIRACCYQ+BwZUBDwIYBOgITCRAQVCEIP//0BYISjB+CtDUYRNBAwQ5Bg7gDBQIA="); -/// ///////////////////////////////////////// +}; let kana = katakana.KA; let scroll = 0; @@ -125,68 +138,69 @@ const keys = Object.keys(katakana).sort(); let hiramode = false; let curkana = 'KA'; +console.log("StartupTime: "+startupTime.diff()); + + function next () { - let found = false; - for (const k of keys) { - 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 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 keys) { - 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); } -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) { @@ -204,9 +218,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() { @@ -227,21 +241,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; @@ -251,27 +291,119 @@ 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, 5); + } + 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 += (4*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(); tickWatch(); setInterval(tickWatch, 1000 * 60); diff --git a/apps/kanawatch/metadata.json b/apps/kanawatch/metadata.json index d1fb70b53..0a7fd8e4a 100644 --- a/apps/kanawatch/metadata.json +++ b/apps/kanawatch/metadata.json @@ -2,7 +2,7 @@ "id": "kanawatch", "name": "Kanawatch", "shortName": "Kanawatch", - "version": "0.08", + "version": "0.10", "type": "clock", "description": "Learn Hiragana and Katakana", "icon": "app.png", @@ -26,6 +26,9 @@ "screenshots": [ { "url": "screenshot.png" + }, + { + "url": "screenshot2.png" } ] } diff --git a/apps/kanawatch/screenshot.old.png b/apps/kanawatch/screenshot.old.png new file mode 100644 index 0000000000000000000000000000000000000000..b1ed879aa1a3b8bef32bb80f4c08b1443530a177 GIT binary patch literal 2992 zcmc&$=|7Z>8s=f7LAIGt7;9!IOR|?r7?WMu!@PE3MrpCNJds@nS+fi?vW>*CX7EHv zmQW^ZrV@pWUdt%rc+c;5{(|%2e7NuHcVG8)eY&p?cY=)-Rsenk&cVSUfHO0;+sEhs z6fe(yuW6cd-v`c6JFF4Mvmwdv92|UCamME_hq?bK_#RQE&u{XCeec_*{0WR~V}n+h z(DL?LPc*$=sE4YTT2oWQQrpD>xPzzBA)^Bt%SxPZ3t>A^GF0)dmx)oAGPAr7lv$yo z%b*)FTAc8F{X6`Hz&r3gH#%f-T_X%`ap3h;=g-a$9pghOlAOrfwEM08f>u|Vd@$d;*nlML|D06 zzvZ!C@^|jKlPZD01AZ{~p`!1c4U% zJ!MIk7Cp_n-y4&+iq%eTCfL5t^DgmvhW?>(Y%mYAx-I3llDi-FEx{5ZRq*Qb?@NM5$sz42xp17GRwGy&@8FJevc){AFRq61>BUd7fI-ssr1U zr9y^xS3V-f<4C~vR!ih1Jj&>Q5cRV+-DTQf&Swm*9}s`~NDInPS&I*7(x}d6Ml2Bd z?azjr8)@vz8ukAEV9Vr)XqQDlPo4c2dl5<4*gB0NL(&m6h;u#af(;(k@wJK=YY?LZ zLyTWgd$|b$2BJ|LD-xU85}x%Do$w&VZ125(__D?7SgT(-;lE@rd|5&4T>)UA=ldV> zDF__x3ojin`|-y_dId~5*dC!%1IfRd2_<11s�Pi)X@SJ7PR%+Tce2<*}39l{e#% zPUgwTaWk5=4m^mtSnWUcc$(`dB=*7v&xPIhe*KDxk$vgNG6FyJsjEDf6LY_YE&L;$ z{WT$Sea0y5k>4daO=e#Bul08JA*?^bNtVcH^X^Hq$~f$Rzn|s!z}Yp9CMHEncNB&5 z50^k3RGcMRo#Ug*>cD~Gfm*JLPZ#pJ`&1xWoQ~luTy9(60wSyxSF??363U!5jyGui z{kJS(hOZ{j+xG7v#CU6L?)ELq>&_rwJ-zSjAw6?$))C+C>NK{qOylhQ@U0hDBZ;Gq zLTpt*;Ly^x6>WO<$J2#_f6Jx6kBg8X=+#TGJcT!+T-_6a&E-60{xsbqSX8nQ`4XL`Ew^B^}RW%e1o0%_KnU} zx6vJ!BGRS_s0dLGALp#NquN~~r4!LM@-yH=Xb`Hc(eC(Y#9Iyj$xC>d?Gap-2zLyE zp%4-L4rbUH>?R?9c%?7hF}-kUt!Ms|$c9W}4|xMOwMzk5T zxoxVatOD97sL@kDmxD*DjRKav3el^e5UjDhoFzXq;VSmG9@Y{}6L}_h&plNnXJT}S zqT%O@bN5by*)#1tU48ByIy6Ib3_mwk9g)I_Z}tlhggpkL@9EW=F0Nt0H<1YO6XZ&- z`emeU2VBpQ{PWnd_>ZXM;)8NZzNRzL_Zd%IwkP*G=BnnT6PP*Lz_$_RwSJk6fECbu z;fBNzGB|9aN{18IbLZ|l-Q>>-HdKxBTKv=1t+~KZ*3e|+hMMO;J(&C_ah$j{A}1e;eTz{n+0+wRTAF*Uk1L%W zXc;Nx7i$jJy7!@B;*qr+wQ_Lw^h)mPV)(JL+)&Er_D8d(!Ll@)A-=lAAQAQ+$*zBF z`tCnFS5Tk=QVp+UJM8c*6yCFe-RgDrg!LGP5~}E~9T_}C*Uy^3 zPM`@ckcW5nXzi6(%HaaX6?2_IgUd{vZw+FiZ!U6s1Pv;E`fX}V>wJjHcOZJ8SJmpI zw=bG+q*Zo#JpbxTf$_L==}hcamu!UA4;P$oAtY$1gs|YpeK>Ze-9XN3BxU|sS0c}= z;@oIJNmzU`%PjZ6+=N1`a>c3a>nMWsBf)E(jnuC8!91E{2Se;`awIdAz|f<1bBd*% zdi68ZftieKPXDVs;wtx|Y_0EI$QvFd3l{0{1@Pg8(RSYbI|6B#RT%8BwJ1%5Q;t=J zuvId#6MhPbA)7I^_V|~_zEKszRuBL?;_V?$)T)r!#t$vN{b-txA%D@UpzXqeO9Gc` zlrtUK$^c5QAR~4+KPxW7(ev$eJasTSv#{6S9kj>uXI?h}PefwXlzzfYDe7Sh?(TSF z&vlRf_w<$Ojtw>Wz!bNn8V9Lrq@_s3fwF?0ftE8Ow`YxHfW5!H;zUc8D0 zr(){(qUiPsQ=GW0FAJU7^-WKU*X&t7ds?W<-E&&}DXy}lEp6)zR99Dn@77<)trKWw z${~7u5`5>%9*33vkmf>c#@TOdu=O0EJZ?zzS3G(d>{1Og`FcWiP`>Uq;oO`aa!WFY zCmojP8^z&Li#zd*d=_Kp=*?HgnAGB@ED&Bb#6o+eb<&jw5_xvt0POh~+RU#n(XSDC r+JOgY3HiY|{QHiy^P>vGNswTL^R&j#U}O6J3(kQvu`+&Ugn#gF@6nHi literal 0 HcmV?d00001 diff --git a/apps/kanawatch/screenshot.png b/apps/kanawatch/screenshot.png index b1ed879aa1a3b8bef32bb80f4c08b1443530a177..4ef0ecbf286c92361988b55513459dba2dec9dcf 100644 GIT binary patch literal 2653 zcmc&$`8(A67k|%Z7L4VRY}t||yG9|)wM3I`kfp2*vhNIyeNbq@wS=;TOzDoT7<4V4 zy7nx|PR2ynE~CX#->a|wf$y*1AI^C_&+9qQd7g95>paivwiN--jXj110C1aJGP-<- zb-#igbC_$JKDr(PBK$Jm5Y!Bbtpb1*GBGl+CA)4GuSPCvaV3xLCaesZP=mzroce)} zZ|#Q<2R2y2vJdl$chvqL?{2icA%DwJ*4-g(yWIBR=lbpBaNjmoEcr z3M7o?4@^ZpRo}h*C@;~!aodhtn20f@ZnkZV_<1P+!~oPsFKzP~Y$7B&W4zS1>LG+q z-<$V?o3(tmq~DlGr4&)IbuxvH-*4Kg@RPVFY@1w$`f5d*?fEC0PeZYv2G|uT`+D*H zD^6+=PlnwnrR9%Ka1s$kvqqz8tZmKAs&P~uD2WhwIsxO4v*lhA6ohr)0f8VvG3h0T zFzmK7J5pve^5!Lp?y>auVnR%=0ewZvyoQdSOjyK)p3gHik!x@Xn+=fWUB(UbEESbm zV@h7!@}c&tPL^V!&MHQ+5(ceQM-A||&Ih<{CAB(qZs=6EaMZ-bda}(O3(&*# zC+9-s=FUA-o=Zk%{BRyLl~oHBXn3IEJUzEimj|ZojCH3SUN|Gb4zo1K3%7dx9nDU3 zbwmvKtJ6}!Ql-HEpk^Gvm#q6SE5g-HUL(|mf zfWA7>mJjGA@|3`r+^e36KcVfuBjl>(B>bs9=`m z>J-euD*m9WKU-Neg=LDv0?@h$1UZi2X`|bf*sFg;1p6L(T=7BgBp}eQ#=({!+to+S;iMb)U)TY{7m5Hcz4_ zkxh8!!r>@Wgv{iUL)mD)1LJ} z&um&AS-LrkUfRT|`fa$}G?)}UcO&Za?YLAC-=A5D))#a+Mj@e>jJ)&qJKvZ#7K{G( z%ztbm2(Rq;LuCHgcs~nmxP72Gw9RlbT}|61UGi|7+YYOt}Q4 zK{`8*WvH0fsz&9XPaT6HvQX?qZ8$p|z}M!AF=jd-@?`08+a}IypICBv1SuWqMFqO^ z^z{02U(<*m+YU4(bdpiw7Ue$UeeKBr_X%>_;)2@md9n@v(zlR58XTt~{DG$450_3MBc}c$yfIBHQni?-W*cX8 z)f9;)?oM|jVLnnAGK9amRIVkqu{BmJ{8KN88S~|UCyVFo3a)(bzqV&Tl>T~M3W*v`dh3+Y$Bo~i;08txZM}Q->}mo z((3l}0EyRD>hL$HodHfnwo(G9nzYAqpV1kIFSp>~FbvZjk%2hz8J} z?30CwUJkyWS6c9yzipH$TiDY)R+8_PaIT#fT%Y*G4%bZ1#CK_-RIN(|gTn|{vNtXy z4mhW6H*TGv|L)afSoo69IKr>^iyb39R+Re;m6Q?FmZf!&V5<}@a29<6w~M7DG_D$; z&!^bQhCRF^?Y?84fwm1?^HYYYm<6c$z z^!iD-k0hqdCI0&R035u5=aC1abImpn=3XZagLLDTI&|rdno>g#%tGIc_I1M zhuPJ`CeJ>6bFEPf>ByAMq_atX{QU0R&t97}HKWs{-Y6w4!N7sn*<}Lm&rm*Sav4D~ zmW(OD{S&5hl!Gym-kw7kMO+-Hikk&m{k{Y8K3v|2@cE=Vuh9z0??TaG`NSmy%wHoh zYe&m(>Vtz)oj2cX8{?IL^SV=^HmB)QvNf2uc%lj_cngJcDUvTmH7pBOdfQ#>_s~+q zBmRX$M_;NHoBKDDjzwcbOa6?E3UIuq=J9Ayno_8!r)7!UyYZSaT?xIe3)l(O*iH@P zgtb@4l9{WZBdMk>w2QVu1zV5!J;G}?k5Mvx9Q85r`7`rp`Q!Z!pB&Tl9kYnI^IIMI z`WvZePVlt9rI`|Z{gI8;l^P$dS0`FEoC_0c4K&(03&)wtKw6XmDdTGR>N10rwq@K| p=n$2xvqS2K1}>1+zHImt@!)~m8TlMx(xG1jCdLG#8be~ze*wopz;*xt literal 2992 zcmc&$=|7Z>8s=f7LAIGt7;9!IOR|?r7?WMu!@PE3MrpCNJds@nS+fi?vW>*CX7EHv zmQW^ZrV@pWUdt%rc+c;5{(|%2e7NuHcVG8)eY&p?cY=)-Rsenk&cVSUfHO0;+sEhs z6fe(yuW6cd-v`c6JFF4Mvmwdv92|UCamME_hq?bK_#RQE&u{XCeec_*{0WR~V}n+h z(DL?LPc*$=sE4YTT2oWQQrpD>xPzzBA)^Bt%SxPZ3t>A^GF0)dmx)oAGPAr7lv$yo z%b*)FTAc8F{X6`Hz&r3gH#%f-T_X%`ap3h;=g-a$9pghOlAOrfwEM08f>u|Vd@$d;*nlML|D06 zzvZ!C@^|jKlPZD01AZ{~p`!1c4U% zJ!MIk7Cp_n-y4&+iq%eTCfL5t^DgmvhW?>(Y%mYAx-I3llDi-FEx{5ZRq*Qb?@NM5$sz42xp17GRwGy&@8FJevc){AFRq61>BUd7fI-ssr1U zr9y^xS3V-f<4C~vR!ih1Jj&>Q5cRV+-DTQf&Swm*9}s`~NDInPS&I*7(x}d6Ml2Bd z?azjr8)@vz8ukAEV9Vr)XqQDlPo4c2dl5<4*gB0NL(&m6h;u#af(;(k@wJK=YY?LZ zLyTWgd$|b$2BJ|LD-xU85}x%Do$w&VZ125(__D?7SgT(-;lE@rd|5&4T>)UA=ldV> zDF__x3ojin`|-y_dId~5*dC!%1IfRd2_<11s�Pi)X@SJ7PR%+Tce2<*}39l{e#% zPUgwTaWk5=4m^mtSnWUcc$(`dB=*7v&xPIhe*KDxk$vgNG6FyJsjEDf6LY_YE&L;$ z{WT$Sea0y5k>4daO=e#Bul08JA*?^bNtVcH^X^Hq$~f$Rzn|s!z}Yp9CMHEncNB&5 z50^k3RGcMRo#Ug*>cD~Gfm*JLPZ#pJ`&1xWoQ~luTy9(60wSyxSF??363U!5jyGui z{kJS(hOZ{j+xG7v#CU6L?)ELq>&_rwJ-zSjAw6?$))C+C>NK{qOylhQ@U0hDBZ;Gq zLTpt*;Ly^x6>WO<$J2#_f6Jx6kBg8X=+#TGJcT!+T-_6a&E-60{xsbqSX8nQ`4XL`Ew^B^}RW%e1o0%_KnU} zx6vJ!BGRS_s0dLGALp#NquN~~r4!LM@-yH=Xb`Hc(eC(Y#9Iyj$xC>d?Gap-2zLyE zp%4-L4rbUH>?R?9c%?7hF}-kUt!Ms|$c9W}4|xMOwMzk5T zxoxVatOD97sL@kDmxD*DjRKav3el^e5UjDhoFzXq;VSmG9@Y{}6L}_h&plNnXJT}S zqT%O@bN5by*)#1tU48ByIy6Ib3_mwk9g)I_Z}tlhggpkL@9EW=F0Nt0H<1YO6XZ&- z`emeU2VBpQ{PWnd_>ZXM;)8NZzNRzL_Zd%IwkP*G=BnnT6PP*Lz_$_RwSJk6fECbu z;fBNzGB|9aN{18IbLZ|l-Q>>-HdKxBTKv=1t+~KZ*3e|+hMMO;J(&C_ah$j{A}1e;eTz{n+0+wRTAF*Uk1L%W zXc;Nx7i$jJy7!@B;*qr+wQ_Lw^h)mPV)(JL+)&Er_D8d(!Ll@)A-=lAAQAQ+$*zBF z`tCnFS5Tk=QVp+UJM8c*6yCFe-RgDrg!LGP5~}E~9T_}C*Uy^3 zPM`@ckcW5nXzi6(%HaaX6?2_IgUd{vZw+FiZ!U6s1Pv;E`fX}V>wJjHcOZJ8SJmpI zw=bG+q*Zo#JpbxTf$_L==}hcamu!UA4;P$oAtY$1gs|YpeK>Ze-9XN3BxU|sS0c}= z;@oIJNmzU`%PjZ6+=N1`a>c3a>nMWsBf)E(jnuC8!91E{2Se;`awIdAz|f<1bBd*% zdi68ZftieKPXDVs;wtx|Y_0EI$QvFd3l{0{1@Pg8(RSYbI|6B#RT%8BwJ1%5Q;t=J zuvId#6MhPbA)7I^_V|~_zEKszRuBL?;_V?$)T)r!#t$vN{b-txA%D@UpzXqeO9Gc` zlrtUK$^c5QAR~4+KPxW7(ev$eJasTSv#{6S9kj>uXI?h}PefwXlzzfYDe7Sh?(TSF z&vlRf_w<$Ojtw>Wz!bNn8V9Lrq@_s3fwF?0ftE8Ow`YxHfW5!H;zUc8D0 zr(){(qUiPsQ=GW0FAJU7^-WKU*X&t7ds?W<-E&&}DXy}lEp6)zR99Dn@77<)trKWw z${~7u5`5>%9*33vkmf>c#@TOdu=O0EJZ?zzS3G(d>{1Og`FcWiP`>Uq;oO`aa!WFY zCmojP8^z&Li#zd*d=_Kp=*?HgnAGB@ED&Bb#6o+eb<&jw5_xvt0POh~+RU#n(XSDC r+JOgY3HiY|{QHiy^P>vGNswTL^R&j#U}O6J3(kQvu`+&Ugn#gF@6nHi diff --git a/apps/kanawatch/screenshot2.png b/apps/kanawatch/screenshot2.png new file mode 100644 index 0000000000000000000000000000000000000000..3e85485de6cda33947b54c915308dc3e897dfded GIT binary patch literal 2689 zcmc(hYc!N=8^@n#W(+eqOf%#(@l{5OOG93OR&g zB8M2sj-E=YEmMq!95R)hnX$#RtIeMMuC>3v-`)@ReXaXm*SgpLe_y|Att;~|?Vt=+ z1q%Rx4AsHbO&pv5SyE{6-gJK|Kpemrw}aL|BZoK#09Z2B_JBuRz*5=5*)a?0)G|@Z z?4oPCqkpjuS}}k^+gyBy)nXRA;Hc!e;f1wt$t!XS4tHQhkTy@`Zn#1KA$16g;}5Ny z#3P*0ZUz*{eeC%ZhNSFp7XWEd+Y9vI5Dhj0Wx-BzDhCRf|KDUScycl&WpizKSRr6@ zRsC|ER?emD$JhO^l~y-TdqTb>9;^*NpqE_Tjn!2-f>M%L!I&mY?k19j(y7y&OenMG zhpERmJ#+-p3XB4sEM_j;n_tA=7+gE) z)Z#k581sKB3v#A5&<~1$_SpCHD&9Ub%SWA0hBGkF_BTK<)Nu6{23on*Kcw7%mV#n z9R#tK6`OrK$#*LqRz*l>pwbYk!`P;S+wz)P^<6DBfT2Ly1_|1h+Q($Z|7E z=_$?8&hpFBv*$KG)vZePcP|6OtOJ1Ev1R42N%YDk9G#*= zLZuOMev0}C``yQA!wWuC2#l5@X$;oj5Ykd9TJWp^@k&#q6o{Uhkl_2z$KB5c2y_Z_ z)5gonTnmPh>dzTPCqt;s)CQC(h#p@>95Wdw>mBxT$Y^h`Z(Jz<{)nyf#sVQdGMh$? z{G%}c9Idk93hq#pCSg-K6+lKL1a&&E=OSw?t!%fDMvo4NQ@xiZ@&Kfks*PC34MjRV zt4(tezU*`G#8~f%$4j9AGI(X2V6Er?L;;ampNJr7VwWU=90nnfJN zKS{pRDG`^>dYR<{#+2hICAO&+uv}bmBpI>jN#bvmLk;MERCeqJyRT=FjG+Ds%>=|~ z>ni7W|7tSGs|(FmlVMCfRV^d`*UQONw-{eD)Zn-Q((rR*7Y&klwp^k92K!z87c9R3 z?poUcqdMx~x=S3cQZjG$)@07KT5Q^P-wB7v-21KJGu-)^e!<>AK_YvN{qeqpg`1AC zMi)~!eGt+PKguvP)y=971L+XLW{%Vhiyh~fIm>G&o;Fj<>^FmLlB#3_X-lS(st|DFVgM6XSy!j(_ z6)^>lkVPC}0Q)osgcj2>{;kWn5pZ5zhjM$TCQ9i_k}fQmJGwfu)vu8WG6XmSi$_`p z8%vX3K0GU_<|m74Hh1~9$dmDvXVb5hS_s3n;mX5=Cmm?CZJ-R_AarN#1lf2P20Dt4 ztk1>_<6qRmeLGN6EkjE+K?w*uD6lE|CB$uNk`~UrXUIt7C=hs6H%$z0oVYK{c8V=L z5hGbc6-&XDiv>Dk#a|#4AHvi{WmjTAcv$37wHLp6n2N}ox0 z`s)(IIKeBMtO_$U-UU*S$MGK>qYAtFcFnC54-v#Sz~s$*I9-!FanA}aL2+3Er|ZC{ zT77{J+j1$%Sv7D;ObBf+*bAOo%fdH^CJVnBJlg}>ZyVr*6|JvxHFHle{J~RK6|N@w z{JN<_gPPvgZn_UHmDM)OIzc8eG>LbkJ?V&S!%maZ*BO?W=}?eo)cKl1sLuz2EemtC zD{67m&P0$`tYIgRVza;RXU}(L+Ef59VQy1}Z1Y+7+d0w6_#>lg0Tlav%-W|()_<|( zbgMSHzebs{B5{1eo6GgCeFeJR-_HC#s*$>fGg4qb_(y8*>UUVrwJqo_D$d5;WmhIp zF}0q~#`9&I_px*kiTPuyJV$7c*;-A$l#qHV+S>qp|7NJb(Fe=t>5;mAp%m{Gkr{)v zhqp2k+WhQleJ||*c-N=5J_^}*^`Prd@1XV*8M7hE!#+>(fOgdo_Dhy|%yLn9`NP9X zZKF5$hCjDkje6!h2iIvhFntEXI5|Uv;tJ7+;pwV1#DGnfvTU=Ld<$g!u3%+4#C&@c)_(-uL0rb0e!W@UFjgv)8d=m%sEi3L8ZkM zW^dp9eKtMouMDqi^XXqAj9f3Wdt0C*s>Z(>MlE&CEbr^)Rr3UBO*l(NY0&j*XvVgY zF=_Bu8s)NIYmR_`z8cK zp1ufUEE)0-4bIRJ+3T^P2U{PWZS!|7NYHs$5-;!$N$x9AO$v6{+ln+EWo~6uX`DEq z^_giU+^4y)xBZLoKaDXmhKbyU=k_nKB!uzcOY=`wF+1SPXXva?IkTNboQvb!V3r-E z2r5bJiCDb|D5}{&NQE4EE_oZaX~t7kzbszAiFPaV9aHZd-<{7Ue$aHmg5_eYgn0VY zX^$S;Zcalm6*wMfTB(OFw|vJeQybYu-xhaQP7m93&OPF7;J|{%&8Hluc0joL%00l! zS1(gNPjYaiXyFi$9V;ORm7m{Tl^|H54F2Zjaw@>zQ6wkP@z&q!88y=KrN`@WQP-$I z#CZ|ld&4E_7O^b=N56ZlFbZ4m`k~|>sAh#s{RqnR_P2Wutb&q(a1gLg2+eT~p Gg}(uZY{0(& literal 0 HcmV?d00001 From 9a91595e416736273884ad6d02df3ae2189da66c Mon Sep 17 00:00:00 2001 From: pancake Date: Tue, 30 May 2023 03:26:19 +0200 Subject: [PATCH 26/64] Sort kanas by AIUEO instead of AEIOU --- apps/kanawatch/ChangeLog | 1 + apps/kanawatch/README.md | 22 ++++++++++++++-------- apps/kanawatch/app.js | 28 +++++++++++++++++++++++----- apps/kanawatch/metadata.json | 2 +- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/apps/kanawatch/ChangeLog b/apps/kanawatch/ChangeLog index 1521bed73..a50406917 100644 --- a/apps/kanawatch/ChangeLog +++ b/apps/kanawatch/ChangeLog @@ -8,3 +8,4 @@ 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 :? diff --git a/apps/kanawatch/README.md b/apps/kanawatch/README.md index 87a9750b1..d13550f4d 100644 --- a/apps/kanawatch/README.md +++ b/apps/kanawatch/README.md @@ -1,14 +1,19 @@ # 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 @@ -16,4 +21,5 @@ Written by pancake in 2022, maintained during 2023 and powered by insomnia ## Screenshots -![hiragana and katakana](screenshot.png) +![katakana](screenshot.png) +![hiragana ](screenshot2.png) diff --git a/apps/kanawatch/app.js b/apps/kanawatch/app.js index 2aa5c2a3c..a81852b1b 100644 --- a/apps/kanawatch/app.js +++ b/apps/kanawatch/app.js @@ -131,16 +131,26 @@ WO: image(52, 49, "AAMf+AFDgP+Bgk/8AFDgYMM/gkD/4AC+EBAof/BkA5FhEAg45Cg/AgF/AQMBB 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(); +// const keys = Object.keys(katakana).sort(); +// console.log(keys); let hiramode = false; let curkana = 'KA'; console.log("StartupTime: "+startupTime.diff()); - function next () { const off = keys.indexOf(curkana); if (off !== -1 && off + 1 < keys.length) { @@ -318,7 +328,7 @@ function hiraPush(d,dx) { } zpos -= 0.04; render(ohhmm); - setTimeout(paint, 5); + setTimeout(paint, 100); } setTimeout (paint, 5); } @@ -352,7 +362,7 @@ function hiraSwipe(d,dx, dostuff) { return; } if (dx) { - xpos += (4*d); + xpos += (8*d); } else { ypos -= (4*d); } @@ -405,5 +415,13 @@ g.clear(true); 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); + + diff --git a/apps/kanawatch/metadata.json b/apps/kanawatch/metadata.json index 0a7fd8e4a..f3aaeae92 100644 --- a/apps/kanawatch/metadata.json +++ b/apps/kanawatch/metadata.json @@ -2,7 +2,7 @@ "id": "kanawatch", "name": "Kanawatch", "shortName": "Kanawatch", - "version": "0.10", + "version": "0.11", "type": "clock", "description": "Learn Hiragana and Katakana", "icon": "app.png", From 790db2f44251347fffef33300416085102196157 Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Tue, 30 May 2023 18:05:20 -0500 Subject: [PATCH 27/64] Update locales.js to add "ft" to en_US locale --- apps/locale/locales.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/locale/locales.js b/apps/locale/locales.js index 7b3146e15..cd720ba62 100644 --- a/apps/locale/locales.js +++ b/apps/locale/locales.js @@ -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" }, From 39f9d66041042a7cb16e8a007b20d3328e7672bc Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Tue, 30 May 2023 19:00:26 -0500 Subject: [PATCH 28/64] Update locale.html to remove leading zero in 12 hour mode --- apps/locale/locale.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/locale/locale.html b/apps/locale/locale.html index 6eb0d94ea..f9d89f846 100644 --- a/apps/locale/locale.html +++ b/apps/locale/locale.html @@ -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 with leading zero in 24-hour format + return (h % 12 == 0) ? 12 : h % 12; } exports = { name: ${js(locale.lang)}, From 7df499d2772d03c6b2fe66d5c69eb0a9702844e0 Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Wed, 31 May 2023 01:44:26 -0500 Subject: [PATCH 29/64] Update locale.html - fixes missing lead zero in 24 hour mode --- apps/locale/locale.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/locale/locale.html b/apps/locale/locale.html index f9d89f846..72c862da0 100644 --- a/apps/locale/locale.html +++ b/apps/locale/locale.html @@ -192,8 +192,8 @@ var is12; function getHours(d) { var h = d.getHours(); if (is12 === undefined) is12 = (require('Storage').readJSON('setting.json', 1) || {})["12hour"]; - if (!is12) return ('0'+h).slice(-2); // return with leading zero in 24-hour format - return (h % 12 == 0) ? 12 : h % 12; + if (!is12) return ('0' + h).slice(-2); + return ((h % 12 == 0) ? 12 : h % 12).toString(); } exports = { name: ${js(locale.lang)}, From d7f95a4ff59fd9b090eba9fc0067f8d9f538bac1 Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Wed, 31 May 2023 02:10:07 -0500 Subject: [PATCH 30/64] [shadowclk] - Better settings and hour formatting --- apps/shadowclk/ChangeLog | 1 + apps/shadowclk/app.js | 26 ++++++-------------------- apps/shadowclk/metadata.json | 4 ++-- apps/shadowclk/settings.js | 24 ++++++++++++++++++------ 4 files changed, 27 insertions(+), 28 deletions(-) diff --git a/apps/shadowclk/ChangeLog b/apps/shadowclk/ChangeLog index 7ba343b2f..ce2933f0c 100644 --- a/apps/shadowclk/ChangeLog +++ b/apps/shadowclk/ChangeLog @@ -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 diff --git a/apps/shadowclk/app.js b/apps/shadowclk/app.js index 7ffdc4683..dd47caf7b 100644 --- a/apps/shadowclk/app.js +++ b/apps/shadowclk/app.js @@ -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); -})(); \ No newline at end of file +})(); diff --git a/apps/shadowclk/metadata.json b/apps/shadowclk/metadata.json index 4e47b9845..432558b95 100644 --- a/apps/shadowclk/metadata.json +++ b/apps/shadowclk/metadata.json @@ -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" }] -} \ No newline at end of file +} diff --git a/apps/shadowclk/settings.js b/apps/shadowclk/settings.js index 21b4826a5..af94c8494 100644 --- a/apps/shadowclk/settings.js +++ b/apps/shadowclk/settings.js @@ -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 && 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 && sysSettings.clock === "shadowclk.app.js") { + sysSettings['12hour'] = appSettings.enable12Hour; + require('Storage').writeJSON("setting.json", sysSettings); + } } function showMenu() { From 4ec93084b2ecac63c23c1c648cb70ab6caf9f34a Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Wed, 31 May 2023 05:35:28 -0500 Subject: [PATCH 31/64] Update settings.js - remove unnecessary code --- apps/shadowclk/settings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/shadowclk/settings.js b/apps/shadowclk/settings.js index af94c8494..3fb774892 100644 --- a/apps/shadowclk/settings.js +++ b/apps/shadowclk/settings.js @@ -47,7 +47,7 @@ function switchTheme(mode) { if (mode === g.theme.dark) return; sysSettings.theme = createThemeColors(mode); - if (sysSettings.clock && sysSettings.clock === "shadowclk.app.js") { + if (sysSettings.clock === "shadowclk.app.js") { require('Storage').writeJSON("setting.json", sysSettings); } updateTheme(mode); @@ -95,7 +95,7 @@ } function writeTimeModeSetting() { - if (sysSettings.clock && sysSettings.clock === "shadowclk.app.js") { + if (sysSettings.clock === "shadowclk.app.js") { sysSettings['12hour'] = appSettings.enable12Hour; require('Storage').writeJSON("setting.json", sysSettings); } From 721df7cf286e798f99919524fd58b55f03a275b7 Mon Sep 17 00:00:00 2001 From: thyttan <6uuxstm66@mozmail.com⁩> Date: Wed, 31 May 2023 21:47:16 +0200 Subject: [PATCH 32/64] Delete runplus (will readd soon) --- apps/runplus/ChangeLog | 22 ---- apps/runplus/README.md | 76 ------------- apps/runplus/app-icon.js | 1 - apps/runplus/app.js | 198 --------------------------------- apps/runplus/app.png | Bin 1479 -> 0 bytes apps/runplus/karvonen.js | 215 ------------------------------------ apps/runplus/metadata.json | 20 ---- apps/runplus/screenshot.png | Bin 3716 -> 0 bytes apps/runplus/settings.js | 157 -------------------------- 9 files changed, 689 deletions(-) delete mode 100644 apps/runplus/ChangeLog delete mode 100644 apps/runplus/README.md delete mode 100644 apps/runplus/app-icon.js delete mode 100644 apps/runplus/app.js delete mode 100644 apps/runplus/app.png delete mode 100644 apps/runplus/karvonen.js delete mode 100644 apps/runplus/metadata.json delete mode 100644 apps/runplus/screenshot.png delete mode 100644 apps/runplus/settings.js diff --git a/apps/runplus/ChangeLog b/apps/runplus/ChangeLog deleted file mode 100644 index d920a3eca..000000000 --- a/apps/runplus/ChangeLog +++ /dev/null @@ -1,22 +0,0 @@ -0.01: New App! -0.02: Set pace format to mm:ss, time format to h:mm:ss, - added settings to opt out of GPS and HRM -0.03: Fixed distance calculation, tested against Garmin Etrex, Amazfit GTS 2 -0.04: Use the exstats module, and make what is displayed configurable -0.05: exstats updated so update 'distance' label is updated, option for 'speed' -0.06: Add option to record a run using the recorder app automatically -0.07: Fix crash if an odd number of active boxes are configured (fix #1473) -0.08: Added support for notifications from exstats. Support all stats from exstats -0.09: Fix broken start/stop if recording not enabled (fix #1561) -0.10: Don't allow the same setting to be chosen for 2 boxes (fix #1578) -0.11: Notifications fixes -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. diff --git a/apps/runplus/README.md b/apps/runplus/README.md deleted file mode 100644 index 659cd964d..000000000 --- a/apps/runplus/README.md +++ /dev/null @@ -1,76 +0,0 @@ -# Run App - -This app allows you to display the status of your run, it -shows distance, time, steps, cadence, pace and more. - -To use it, start the app and press the middle button so that -the red `STOP` in the bottom right turns to a green `RUN`. - -## Display - -* `DIST` - the distance travelled based on the GPS (if you have a GPS lock). - * NOTE: this is based on the GPS coordinates which are not 100% accurate, especially initially. As - the GPS updates your position as it gets more satellites your position changes and the distance - shown will increase, even if you are standing still. -* `TIME` - the elapsed time for your run -* `PACE` - the number of minutes it takes you to run a given distance, configured in settings (default 1km) **based on your run so far** -* `HEART (BPM)` - Your current heart rate -* `Max BPM` - Your maximum heart rate reached during the run -* `STEPS` - Steps since you started exercising -* `CADENCE` - Steps per second based on your step rate *over the last minute* -* `GPS` - this is green if you have a GPS lock. GPS is turned on automatically -so if you have no GPS lock you just need to wait. -* The current time is displayed right at the bottom of the screen -* `RUN/STOP` - whether the distance for your run is being displayed or not - -## Recording Tracks - -When the `Recorder` app is installed, `Run` will automatically start and stop tracks -as needed, prompting you to overwrite or begin a new track if necessary. - -## Settings - -Under `Settings` -> `App` -> `Run` you can change settings for this app. - -* `Record Run` (only displayed if `Recorder` app installed) should the Run app automatically -record GPS/HRM/etc data every time you start a run? -* `Pace` is the distance that pace should be shown over - 1km, 1 mile, 1/2 Marathon or 1 Marathon -* `Boxes` leads to a submenu where you can configure what is shown in each of the 6 boxes on the display. - Available stats are "Time", "Distance", "Steps", "Heart (BPM)", "Max BPM", "Pace (avg)", "Pace (curr)", "Speed", and "Cadence". - Any box set to "-" will display no information. - * Box 1 is the top left (defaults to "Distance") - * Box 2 is the top right (defaults to "Time") - * Box 3 is the middle left (defaults to "Pace (avg)") - * Box 4 is the middle right (defaults to "Heart (BPM)") - * Box 5 is the bottom left (defaults to "Steps") - * Box 6 is the bottom right (defaults to "Cadence") -* `Notifications` leads to a submenu where you can configure if the app will notify you after -your distance, steps, or time repeatedly pass your configured thresholds - * `Ntfy Dist`: The distance that you must pass before you are notified. Follows the `Pace` options - * "Off" (default), "1km", "1 mile", "1/2 Marathon", "1 Marathon" - * `Ntfy Steps`: The number of steps that must pass before you are notified. - * "Off" (default), 100, 500, 1000, 5000, 10000 - * `Ntfy Time`: The amount of time that must pass before you are notified. - * "Off" (default), "30 sec", "1 min", "2 min", "5 min", "10 min", "30 min", "1 hour" - * `Dist Pattern`: The vibration pattern to use to notify you about meeting your distance threshold - * `Step Pattern`: The vibration pattern to use to notify you about meeting your step threshold - * `Time Pattern`: The vibration pattern to use to notify you about meeting your time threshold - -## TODO - -* Keep a log of each run's stats (distance/steps/etc) - -## Development - -This app uses the [`exstats` module](https://github.com/espruino/BangleApps/blob/master/modules/exstats.js). When uploaded via the -app loader, the module is automatically included in the app's source. However -when developing via the IDE the module won't get pulled in by default. - -There are some options to fix this easily - please check out the [modules README.md file](https://github.com/espruino/BangleApps/blob/master/modules/README.md) -## Contributors (Run and Run+) -gfwilliams -hughbarney -GrandVizierOlaf -BartS23 -f-teacher -thyttan diff --git a/apps/runplus/app-icon.js b/apps/runplus/app-icon.js deleted file mode 100644 index a97d1b8ce..000000000 --- a/apps/runplus/app-icon.js +++ /dev/null @@ -1 +0,0 @@ -require("heatshrink").decompress(atob("mEw4UA///pH9vEFt9TIW0FqALJitUBZNVqoLqgo4BHZAUBtBTHgILB1XAEREV1WsEQ9AgWq1ALHgEO1WtBYxCBhWq0pdInWq2tABY8q1WVBZGq1XFBZS/IKQRvCDIsP9WsBZP60CTCBYs//+wLxALBTQ4AB///+AKHgYLB/gLK/4LHh//AIIwFitVr/8DIIwFLANXBAILIqogBn7DBEYrXBeQRgIBYKmHDgYLLZRBACBZYKJZIILKKRZeWgJGKAFQA==")) diff --git a/apps/runplus/app.js b/apps/runplus/app.js deleted file mode 100644 index 7cb5d4381..000000000 --- a/apps/runplus/app.js +++ /dev/null @@ -1,198 +0,0 @@ -// Use widget utils to show/hide widgets -let wu = require("widget_utils"); - -let runInterval; -let karvonenActive = false; -// Run interface wrapped in a function -let ExStats = require("exstats"); -let B2 = process.env.HWVERSION===2; -let Layout = require("Layout"); -let locale = require("locale"); -let fontHeading = "6x8:2"; -let fontValue = B2 ? "6x15:2" : "6x8:3"; -let headingCol = "#888"; -let fixCount = 0; -let isMenuDisplayed = false; - -g.reset().clear(); -Bangle.loadWidgets(); -Bangle.drawWidgets(); -wu.show(); - -// --------------------------- -let settings = Object.assign({ - record: true, - B1: "dist", - B2: "time", - B3: "pacea", - B4: "bpm", - B5: "step", - B6: "caden", - paceLength: 1000, - notify: { - dist: { - value: 0, - notifications: [], - }, - step: { - value: 0, - notifications: [], - }, - time: { - value: 0, - notifications: [], - }, - }, - HRM: { - min: 55, - max: 185, - }, -}, require("Storage").readJSON("runplus.json", 1) || {}); -let statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!==""); -let exs = ExStats.getStats(statIDs, settings); -// --------------------------- - -function setStatus(running) { - layout.button.label = running ? "STOP" : "START"; - layout.status.label = running ? "RUN" : "STOP"; - layout.status.bgCol = running ? "#0f0" : "#f00"; - layout.render(); -} - -// Called to start/stop running -function onStartStop() { - let running = !exs.state.active; - let prepPromises = []; - // 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(() => { - isMenuDisplayed = false; - layout.setUI(); // grab our input handling again - layout.forgetLazyState(); - layout.render(); - }) - ); - } else { - prepPromises.push( - WIDGETS["recorder"].setRecording(false) - ); - } - } - - if (!prepPromises.length) // fix for Promise.all bug in 2v12 - prepPromises.push(Promise.resolve()); - - Promise.all(prepPromises) - .then(() => { - if (running) { - exs.start(); - } else { - exs.stop(); - } - // if stopping running, don't clear state - // so we can at least refer to what we've done - setStatus(running); - }); -} - -let lc = []; -// Load stats in pair by pair -for (let i=0;ilayout[e.id].label = e.getString()); - if (sb) sb.on('changed', e=>layout[e.id].label = e.getString()); -} -// At the bottom put time/GPS state/etc -lc.push({ type:"h", filly:1, c:[ - {type:"txt", font:fontHeading, label:"GPS", id:"gps", fillx:1, bgCol:"#f00" }, - {type:"txt", font:fontHeading, label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg }, - {type:"txt", font:fontHeading, label:"---", id:"status", fillx:1 } -]}); -// Now calculate the layout -let layout = new Layout( { - type:"v", c: lc -},{lazy:true, btns:[{ label:"---", cb: (()=>{if (karvonenActive) {stopKarvonenUI();run();} onStartStop();}), id:"button"}]}); -delete lc; -setStatus(exs.state.active); -layout.render(); - -function configureNotification(stat) { - stat.on('notify', (e)=>{ - settings.notify[e.id].notifications.reduce(function (promise, buzzPattern) { - return promise.then(function () { - return Bangle.buzz(buzzPattern[0], buzzPattern[1]); - }); - }, Promise.resolve()); - }); -} - -Object.keys(settings.notify).forEach((statType) => { - if (settings.notify[statType].increment > 0 && exs.stats[statType]) { - configureNotification(exs.stats[statType]); - } -}); - -// Handle GPS state change for icon -Bangle.on("GPS", function(fix) { - layout.gps.bgCol = fix.fix ? "#0f0" : "#f00"; - if (!fix.fix) return; // only process actual fixes - if (fixCount++ === 0) { - Bangle.buzz(); // first fix, does not need to respect quiet mode - } -}); - -// run() function used to switch between traditional run UI and karvonen UI -function run() { - wu.show(); - layout.lazy = false; - layout.render(); - layout.lazy = true; - // We always call ourselves once a second to update - if (!runInterval){ - runInterval = setInterval(function() { - layout.clock.label = locale.time(new Date(),1); - if (!isMenuDisplayed && !karvonenActive) layout.render(); - }, 1000); - } -} -run(); - -/////////////////////////////////////////////// -// Karvonen -/////////////////////////////////////////////// - -function stopRunUI() { - // stop updating and drawing the traditional run app UI - clearInterval(runInterval); - runInterval = undefined; - karvonenActive = true; -} - -function stopKarvonenUI() { - g.reset().clear(); - clearInterval(karvonenInterval); - karvonenInterval = undefined; - karvonenActive = false; -} - -let karvonenInterval; -// Define the function to go back and forth between the different UI's -function swipeHandler(LR,_) { - if (LR==-1 && karvonenActive && !isMenuDisplayed) {stopKarvonenUI(); run();} - if (LR==1 && !karvonenActive && !isMenuDisplayed) {stopRunUI(); karvonenInterval = eval(require("Storage").read("runplus_karvonen"))(settings.HRM, exs.stats.bpm);} -} -// Listen for swipes with the swipeHandler -Bangle.on("swipe", swipeHandler); diff --git a/apps/runplus/app.png b/apps/runplus/app.png deleted file mode 100644 index 7059b8b015e20039a96de8b65c8a6b68a5e51e18..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1479 zcmV;&1vvVNP)b0~r9hI1hzN2C_Hz2bec46d zz32!1f7|Dr^Zft+eV+5aXV1bTJi@~b@gPX$zNip1Yz1>vN2mz2DMEkvlG@MN zqD@lE?a)(59(>u0=Kppec*EA5(*zQ(EG3+(+KI->CEFJ!ACP%5!I+F&=p{{UFEnH!o7WN}(7DfEF{6T=^qRTOGUUmDCINa`%K?dqCzyBw&^& zdx;n90#h)O*AZj`^*@L)UE%?ZC>&z+*w0Pj=Mr*u5X*l)p2LibbmVEnr6riETg&$lZ8)JSSrFFIvt>|1LfBuoykZR)&GJv4Nif!en;z zv+6Z;L-k`+==0YD>-(E?LK%{G3lE#^`JOB$P&RcWluLnM)#~Vk(_duC*`K_4L>V>S z)_4dF#+CBq(XN1$@$$f&f%ft_-N11%bdLd>txt@&{Fx`lWV(+H188fpdoMWEz{=c= zcS=M^psn$7;>Mk!^KP#d`utPCk6iBMM%oOs0tuJ37nQ16rohH)E z9ia`tY}y~?;W9hY+Ci=jL57GcSZ9mZX%$dBo+%nx{F~SG>u0QvN67NBtqYU0IzJ12 z!D%4xE@^d0`F6j&s+C&7&h>F)stg0CXcP_pe^xQKQS72jOY-(wh zZbX~G&?_#4KvUbwYbW!Gs64aVQ65`xr~;X3@&0)ixmMMvar5@_BseI*>bwB=Hsh4X zYQh$!?Cq~z?1DoTIA3l2sj5-q2GG>pLe-60r0n}JscF?|Pu=!AaWEs^-w{4*u-m|v z)cN}2e|5diF%6APc(zvE@zj|5xTt9M^e(*}a5ghB(al~?Yp}~eT3VX+_&;u68O^;r zFm#Zy>3N^}(KCW>&hyn7OqS~|H4F%<8>aciPOVnaXAj8ih*=;qfnl~5NeLTDd^m8~ zvnv)fz-)*f*%Y>B(}4>B-+uJwvah5;)T5WD&l4qTwmGbG5^Ni*$W&M4^}G>c@+r0 zm&L6n!K$N!Oe1_uD^fG^zOXju@RlWhrFCBC&eabb;-_mPZum831lFJkDuuov#6kU5 z4CF+`AmwiE&BPU?TrL(A+P>=nva%L?1T`4WzcBdbD0fZgQOG$4`W)s&T>c@z;lw?$ z*r#$R-lF#xBG~4P-1p~Z&LlR|e(BYTGQ71roRQk)24wckiNM_6L75_6I@Icn{4O=n ze($4X(0i|)Kwp^SEDqOJAUkp)FsXM?t`S20EwnAT>>nDL9jAl(3m`*#gmZ99% of HRR+minHR = serious risk of heart attack - let minzone2 = hrr * 0.6 + minhr; - let maxzone2 = hrr * 0.7 + minhr; - let maxzone3 = hrr * 0.8 + minhr; - let maxzone4 = hrr * 0.9 + minhr; - let maxzone5 = hrr * 0.99 + minhr; - - // HR data: large, readable, in the middle of the screen - function drawHR() { - g.setFontAlign(-1,0,0); - g.clearRect(Rdiv(x,11/4),Rdiv(y,2)-25,Rdiv(x,11/4)+50*2-14,Rdiv(y,2)+25); - g.setColor(g.theme.fg); - g.setFont("Vector",50); - g.drawString(hr, Rdiv(x,11/4), Rdiv(y,2)+4); - } - - function drawWaitHR() { - g.setColor(g.theme.fg); - // Waiting for HRM - g.setFontAlign(0,0,0); - g.setFont("Vector",50); - g.drawString("--", Rdiv(x,2)+4, Rdiv(y,2)+4); - - // Waiting for current Zone - g.setFont("Vector",24); - g.drawString("Z-", Rdiv(x,4.3)-3, Rdiv(y,2)+2); - - // waiting for upper and lower limit of current zone - g.setFont("Vector",20); - g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/2)); - g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/7)); - } - - //These functions call arcs to show different HR zones. - - //To shorten the code, I'll reference some letiables and reuse them. - let centreX = R.x + 0.5 * R.w; - let centreY = R.y + 0.5 * R.h; - let minRadius = 0.38 * R.h; - let maxRadius = 0.50 * R.h; - - //draw background image (dithered green zones)(I should draw different zones in different dithered colors) - const HRzones= require("graphics_utils"); - let minRadiusz = 0.44 * R.h; - let startAngle = HRzones.degreesToRadians(-88.5); - let endAngle = HRzones.degreesToRadians(268.5); - - function drawBgArc() { - g.setColor(g.theme.dark==false?0xC618:"#002200"); - HRzones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle); - } - - const zones = require("graphics_utils"); - //####### A function to simplify a bit the code ###### - function simplify (sA, eA, Z, currentZone, lastZone) { - let startAngle = zones.degreesToRadians(sA); - let endAngle = zones.degreesToRadians(eA); - if (currentZone == lastZone) zones.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle, endAngle); - else zones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle); - g.setFont("Vector",24); - g.clearRect(Rdiv(x,4.3)-12, Rdiv(y,2)+2-12,Rdiv(x,4.3)+12, Rdiv(y,2)+2+12); - g.setFontAlign(0,0,0); - g.drawString(Z, Rdiv(x,4.3), Rdiv(y,2)+2); - } - - function zoning (max, min) { // draw values of upper and lower limit of current zone - g.setFont("Vector",20); - g.setColor(g.theme.fg); - g.clearRect(Rdiv(x,2)-20*2, Rdiv(y,9/2)-10,Rdiv(x,2)+20*2, Rdiv(y,9/2)+10); - g.clearRect(Rdiv(x,2)-20*2, Rdiv(y,9/7)-10,Rdiv(x,2)+20*2, Rdiv(y,9/7)+10); - g.setFontAlign(0,0,0); - g.drawString(max, Rdiv(x,2), Rdiv(y,9/2)); - g.drawString(min, Rdiv(x,2), Rdiv(y,9/7)); - } - - function clearCurrentZone() { // Clears the extension of the current zone by painting the extension area in background color - g.setColor(g.theme.bg); - HRzones.fillArc(g, centreX, centreY, minRadius-1, minRadiusz, startAngle, endAngle); - } - - function getZone(zone) { - drawBgArc(); - clearCurrentZone(); - if (zone >= 0) {zoning(minzone2, minhr);g.setColor("#00ffff");simplify(-88.5, -45, "Z1", 0, zone);} - if (zone >= 1) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(-43.5, -21.5, "Z2", 1, zone);} - if (zone >= 2) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(-20, 1.5, "Z2", 2, zone);} - if (zone >= 3) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(3, 24, "Z2", 3, zone);} - if (zone >= 4) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(25.5, 46.5, "Z3", 4, zone);} - if (zone >= 5) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(48, 69, "Z3", 5, zone);} - if (zone >= 6) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(70.5, 91.5, "Z3", 6, zone);} - if (zone >= 7) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(93, 114.5, "Z4", 7, zone);} - if (zone >= 8) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(116, 137.5, "Z4", 8, zone);} - if (zone >= 9) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(139, 160, "Z4", 9, zone);} - if (zone >= 10) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(161.5, 182.5, "Z5", 10, zone);} - if (zone >= 11) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(184, 205, "Z5", 11, zone);} - if (zone == 12) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(206.5, 227.5, "Z5", 12, zone);} - } - - function getZoneAlert() { - const HRzonemax = require("graphics_utils"); - let centreX1,centreY1,maxRadius1 = 1; - let minRadius = 0.40 * R.h; - let startAngle1 = HRzonemax.degreesToRadians(-90); - let endAngle1 = HRzonemax.degreesToRadians(270); - g.setFont("Vector",38);g.setColor("#ff0000"); - HRzonemax.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle1, endAngle1); - g.drawString("ALERT", 26,66); - } - - //Subdivided zones for better readability of zones when calling the images. //Changing HR zones will trigger the function with the image and previous&next HR zones. - let subZoneLast; - function drawZones() { - if ((hr < maxhr - 2) && subZoneLast==13) {g.clear(); drawArrows(); drawHR();} // Reset UI when coming down from zone alert. - if (hr <= hrr * 0.6 + minhr) {if (subZoneLast!=0) {subZoneLast=0; getZone(subZoneLast);}} // Z1 - else if (hr <= hrr * 0.64 + minhr) {if (subZoneLast!=1) {subZoneLast=1; getZone(subZoneLast);}} // Z2a - else if (hr <= hrr * 0.67 + minhr) {if (subZoneLast!=2) {subZoneLast=2; getZone(subZoneLast);}} // Z2b - else if (hr <= hrr * 0.70 + minhr) {if (subZoneLast!=3) {subZoneLast=3; getZone(subZoneLast);}} // Z2c - else if (hr <= hrr * 0.74 + minhr) {if (subZoneLast!=4) {subZoneLast=4; getZone(subZoneLast);}} // Z3a - else if (hr <= hrr * 0.77 + minhr) {if (subZoneLast!=5) {subZoneLast=5; getZone(subZoneLast);}} // Z3b - else if (hr <= hrr * 0.80 + minhr) {if (subZoneLast!=6) {subZoneLast=6; getZone(subZoneLast);}} // Z3c - else if (hr <= hrr * 0.84 + minhr) {if (subZoneLast!=7) {subZoneLast=7; getZone(subZoneLast);}} // Z4a - else if (hr <= hrr * 0.87 + minhr) {if (subZoneLast!=8) {subZoneLast=8; getZone(subZoneLast);}} // Z4b - else if (hr <= hrr * 0.90 + minhr) {if (subZoneLast!=9) {subZoneLast=9; getZone(subZoneLast);}} // Z4c - else if (hr <= hrr * 0.94 + minhr) {if (subZoneLast!=10) {subZoneLast=10; getZone(subZoneLast);}} // Z5a - else if (hr <= hrr * 0.96 + minhr) {if (subZoneLast!=11) {subZoneLast=11; getZone(subZoneLast);}} // Z5b - else if (hr <= hrr * 0.98 + minhr) {if (subZoneLast!=12) {subZoneLast=12; getZone(subZoneLast);}} // Z5c - else if (hr >= maxhr - 2) {subZoneLast=13; g.clear();getZoneAlert();} // Alert - } - - function initDraw() { - drawArrows(); - if (hr!=0) updateUI(true); else {drawWaitHR(); drawBgArc();} - //drawZones(); - } - - let hrLast; - //h = 0; // Used to force hr update via web ui console field to trigger draws, together with `if (h!=0) hr = h;` below. - function updateUI(resetHrLast) { // Update UI, only draw if warranted by change in HR. - hrLast = resetHrLast?0:hr; // Handles correct updating on init depending on if we've got HRM readings yet or not. - hr = exsHrmStats.getValue(); - //if (h!=0) hr = h; - if (hr!=hrLast) { - drawHR(); - drawZones(); - } //g.setColor(g.theme.fg).drawLine(175/2,0,175/2,175).drawLine(0,175/2,175,175/2); // Used to align UI elements. - } - - initDraw(); - - // check for updates every second. - karvonenInterval = setInterval(function() { - if (!isMenuDisplayed && karvonenActive) updateUI(); - }, 1000); - - return karvonenInterval; -}) diff --git a/apps/runplus/metadata.json b/apps/runplus/metadata.json deleted file mode 100644 index 60860dc07..000000000 --- a/apps/runplus/metadata.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "id": "runplus", - "name": "Run+", - "version": "0.18", - "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", - "supports": ["BANGLEJS2"], - "screenshots": [{"url": "screenshot.png"}], - "readme": "README.md", - "storage": [ - {"name": "runplus.app.js", "url": "app.js"}, - {"name": "runplus.img", "url": "app-icon.js", "evaluate": true}, - {"name": "runplus.settings.js", "url": "settings.js"}, - {"name": "runplus_karvonen", "url": "karvonen.js"} - ], - "data": [ - {"name": "runplus.json"} - ] -} diff --git a/apps/runplus/screenshot.png b/apps/runplus/screenshot.png deleted file mode 100644 index 1a813f19dfe304e3d8bf711692fd2abf1013c391..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3716 zcma)9i8mD5AJ;TuD9kVjVXS4BC1Om*G9ocOV`TT9;vt4!cB3g0*-4fx8I{IZ$M(V) zq7Z|6WGORgB-^u&ERFfi?=SfM?m72dWt=rKSDd*uTr+%eqw4OIaxzdEo;5|cv`pDBSN47K|UJq=6VDzBkHWn*!;PlpF ze(^n}LDM%GMu;}M5I}j(GWAen6Un4xU5>-I1Dx#l>;Z{Cb7j|( zK<4L6>y{&Db9JC52kOi-}{Chnt6Ig+jZL(>VA`ls2Ey8Pr z5g^sQAZ*o1m(Vto)Z+Le8OKl6@3BpBwPY6bEyL(Q`O<}w+4^NdTV2%8y72;!hZNA~ z7e5!Cguz0u9-KA*OW;@CjDsB=R?KtX(W$m2Pvx*tiR%P@tJPcgjyRu_%KN-Z`7dnf9xz8B?mMSbYsWY$ z|DH%HG)k&{8R($3%9yHlny2>;l_#4B@At;kl=anmT@wvY{%ZH)iZ7^cMFZ6px|1Yg zb1ya^vEmFYaQ~Gc6CeBeow@CT2^;E8ZC`kZn$GO9|Vf ze?f%B`R>(U5;Fsxt@F(94D1X_aTMBG42no{a*+^|um0fr6#*HA0O~axgw%v{i~Gn5 z%02Wc*Kf85fv@=eY|eO2I1ODpIx{^8KPD41?X`$RNxNOt5&|Ct2}(i_R_5*k)J zrhRWRGxo0U8uqUHpH^(yB}QuJn>c3B)=f>Nyi6)) zOECeHxpTcTXV<(xwEb zL5K`^0C}h(kXu7@S)!O!?8U`7Pl_6cPWVjLk)lom+B7>(RjvIA_R~F%YcSB-v@x@@ zo{E$!+3*JZls|AoVBq3zHdp=$5cLS`VM7LhB9IgfSO>cieEb*>{Cw_r63#PFK>d_Geiezu=XmOLG9&hHfP!exIkoxY zu=Lu7Kvncq2B?F5C0 z{D1i+DS}UyX&d!9ejkpsod{l>J$TwvhFFasSSQPkFowWX5*(_FQP|JxzZK?LOcV5E zX~~8dj-F(O`rVy3P6gjS8u|#t7oqdOIXV&7Te5bH0+(BI62UQ)vg&QjqWVhPc^yY4 zrRUYvR)Bj}U7G9%6t!ZLxG>P|9aBt$Lkm?E08efy$6AtPvSxHz6D}Q9?={^jc7K*r z4&5>&fd5wdTS!ON-xx&ZXu$K@g$CP74GEv1ZmOa+S(1ZL&0gd z2wA44M^ee5V3>5YQCdwiorujC=%IR07&&Luc{Uk5p#o0>_qSu5UGM`mYg9plqT9k5Fm}m$@z|5W3Y&%BZz_t}4gunQ2dPQS0Ys-Jws1Af4#!lFjbGC3h$ZT>q z$>%JcvLGzd6(e&d1@u`l1kimf#+;zcH`&Ep;Z93=uAfnvj8rc(u{@y(Eh=D4!Y+C=>@{sS#;$nO zBe;o^+DZ0`Jmm7#Y?sguyYwCU(&r>&66>J>stxwllI=Wb(aSHh2K2S$LkQJ76TrDk zd5(p#Kg^YTp#FZFCdyO?ur#Lb?CBpzu3g%`;ty6`vlp+(o>f?e>y@Y*eIFYodI$mw zLF5;$P6`X(G=L(bmr&sLs1?6=sK+gE46(1}ZU#v!VP{1l1Lyqh=Jr1;`(IPq=V()# zY7Mrh#HE@25YZm`j9;9G>v|nruUC9Z#UGG}mNS=m z>8EXdN0Hn`mX}FkxkRd88+u!ECZ*!fsB}iJ!Lm&4MX^b5SJ4csi2S?Ltm$aVx805F zGs!LV4N2rF;F7ZGtN|)AJ3*y9qhf%e`PCm#-*o4BpCY96TsP$EONg{8A;IO91vsd_ zyC6~7Y*mX1Q6j5l>`cK`%%R_>z$oAc z{nAIY*HZF$;qcJvGESyoAuVcn`Tp&Yb-o;D7=08?eGkOjRj$jt0L6bA zwouU#--D%6gA3I*ybKPr+Z73ky6GjlyP=z@J2Q-W1hFDs|3Z_3MbJrVcISm|EgkSx z%#o7kjA(C2g5J$F2bF+t7|s&?%^Z=1zFK3c#L+lk?c20h*jLV|AJ0fDN;@p)mjX-I zx)`35$zn}cwL#_gUk&9jh^*@KP2q|Vwrhw4Vnw~CZ5p|Ak?>Y|A2aw~M?}KMH59^i zSZR_sak%ILj=h}sBL$=UkB(KRiw69X4 z+g3@Rmg66AH^Q{Pzowona#!&QJMkYAmviIum!RTbQzb{v9uCx#&24<4{*OP0sM_FN z^0IFQjCHDo7t|p|(+Bk3y=`Kx)u8r9|7s+-m`fGST>PpeB~wy5h&mC#cEsmGq)ngz zFO_6y=>(POfDggC#%KbX{i%y7Ssj(AoewgPKBvt`u_jGo*G@ek4#&$%9sHd1J7k;f zsxoe5O37SoPSzs`8~ofZ=D-1!$u$t_=L<9QWM2GXs=byV1e2twmJFzYG?>+jA*glEA|wtfm;ZnwmpG>`?G z6yps4eFN}1ZPCm#ivusQObV|16alTM(UWcQdJ1(ktbz3qO#0)|eNrMYGsVSM;-S#V z4;Kgw25E4<;+`jJ>M!AN;E?R?bhWA= zu8Q>QmqvsR*zjp*kg+>Ls(+t&g_#$jSDa1aQo@dQ(`geJV9WA z4_qe|J#kc_=XBxFT2AzD+uwLMxqrPl6+r{a16O}V46{AL6t%%z*i zU*q|ZKiWe83il}i`hXjPa_)rpovkqCjsL2m#{W{nUyJWPy*pfYy1Dn&JHpgTg=0a_ zYjGiKX*)6{I;Es}M569jLPI|f<{43aKv4^0%Rj(kC)AHEDuqeAxmHtte4qVDwV{}3 zqp%Q~Q*)rwkie6*LLvIRc`yT*`SBJY%*D64SP6-#b#1jWmy(1?ew+Jves z*3KaRLi6JQ_7gs$SFslk5g7rMHVp%Cslk@2RirS$?R4(!@hml*pD?%!4Yy&s=qBznd7RQ-OlBXj*JM}W|=r!{x0f1mfQs.id); - - // ...and overwrite them with any saved values - // This way saved values are preserved if a new version adds more settings - const storage = require('Storage') - let settings = Object.assign({ - record: true, - B1: "dist", - B2: "time", - B3: "pacea", - B4: "bpm", - B5: "step", - B6: "caden", - paceLength: 1000, // TODO: Default to either 1km or 1mi based on locale - notify: { - dist: { - increment: 0, - notifications: [], - }, - step: { - increment: 0, - notifications: [], - }, - time: { - increment: 0, - notifications: [], - }, - }, - HRM: { - min: 55, - max: 185, - }, - }, storage.readJSON(SETTINGS_FILE, 1) || {}); - function saveSettings() { - storage.write(SETTINGS_FILE, settings) - } - - function getBoxChooser(boxID) { - return { - min: 0, max: statsIDs.length-1, - value: Math.max(statsIDs.indexOf(settings[boxID]),0), - format: v => statsList[v].name, - onchange: v => { - settings[boxID] = statsIDs[v]; - saveSettings(); - }, - } - } - - function sampleBuzz(buzzPatterns) { - return buzzPatterns.reduce(function (promise, buzzPattern) { - return promise.then(function () { - return Bangle.buzz(buzzPattern[0], buzzPattern[1]); - }); - }, Promise.resolve()); - } - - var menu = { - '': { 'title': 'Run' }, - '< Back': back, - }; - if (global.WIDGETS&&WIDGETS["recorder"]) - menu[/*LANG*/"Record Run"] = { - value : !!settings.record, - onchange : v => { - settings.record = v; - saveSettings(); - } - }; - var notificationsMenu = { - '< Back': function() { E.showMenu(menu) }, - } - menu[/*LANG*/"Notifications"] = function() { E.showMenu(notificationsMenu)}; - ExStats.appendMenuItems(menu, settings, saveSettings); - ExStats.appendNotifyMenuItems(notificationsMenu, settings, saveSettings); - var vibPatterns = [/*LANG*/"Off", ".", "-", "--", "-.-", "---"]; - var vibTimes = [ - [], - [[100, 1]], - [[300, 1]], - [[300, 1], [300, 0], [300, 1]], - [[300, 1],[300, 0], [100, 1], [300, 0], [300, 1]], - [[300, 1],[300, 0],[300, 1],[300, 0],[300, 1]], - ]; - notificationsMenu[/*LANG*/"Dist Pattern"] = { - value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.dist.notifications))), - min: 0, max: vibTimes.length - 1, - format: v => vibPatterns[v]||/*LANG*/"Off", - onchange: v => { - settings.notify.dist.notifications = vibTimes[v]; - sampleBuzz(vibTimes[v]); - saveSettings(); - } - } - notificationsMenu[/*LANG*/"Step Pattern"] = { - value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.step.notifications))), - min: 0, max: vibTimes.length - 1, - format: v => vibPatterns[v]||/*LANG*/"Off", - onchange: v => { - settings.notify.step.notifications = vibTimes[v]; - sampleBuzz(vibTimes[v]); - saveSettings(); - } - } - notificationsMenu[/*LANG*/"Time Pattern"] = { - value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.time.notifications))), - min: 0, max: vibTimes.length - 1, - format: v => vibPatterns[v]||/*LANG*/"Off", - onchange: v => { - settings.notify.time.notifications = vibTimes[v]; - sampleBuzz(vibTimes[v]); - saveSettings(); - } - } - var boxMenu = { - '< Back': function() { E.showMenu(menu) }, - } - Object.assign(boxMenu,{ - 'Box 1': getBoxChooser("B1"), - 'Box 2': getBoxChooser("B2"), - 'Box 3': getBoxChooser("B3"), - 'Box 4': getBoxChooser("B4"), - 'Box 5': getBoxChooser("B5"), - 'Box 6': getBoxChooser("B6"), - }); - menu[/*LANG*/"Boxes"] = function() { E.showMenu(boxMenu)}; - - var hrmMenu = { - '< Back': function() { E.showMenu(menu) }, - } - - menu[/*LANG*/"HRM min/max"] = function() { E.showMenu(hrmMenu)}; - hrmMenu[/*LANG*/"min"] = { - min: 1, max: 100, - value: settings.HRM.min, - format: w => w, - onchange: w => { - settings.HRM.min = w; - saveSettings(); - }, - } - hrmMenu[/*LANG*/"max"] = { - min: 101, max: 220, - value: settings.HRM.max, - format: v => v, - onchange: v => { - settings.HRM.max = v; - saveSettings(); - }, - } - E.showMenu(menu); -}) From 6822d8ed709b484c37118d28f31bd3da004f3df7 Mon Sep 17 00:00:00 2001 From: thyttan <6uuxstm66@mozmail.com⁩> Date: Wed, 31 May 2023 21:58:19 +0200 Subject: [PATCH 33/64] Forking run to runplus (2nd time) --- apps/runplus/ChangeLog | 17 ++++ apps/runplus/README.md | 69 ++++++++++++++++ apps/runplus/app-icon.js | 1 + apps/runplus/app.js | 160 ++++++++++++++++++++++++++++++++++++ apps/runplus/app.png | Bin 0 -> 1479 bytes apps/runplus/metadata.json | 16 ++++ apps/runplus/screenshot.png | Bin 0 -> 3716 bytes apps/runplus/settings.js | 129 +++++++++++++++++++++++++++++ 8 files changed, 392 insertions(+) create mode 100644 apps/runplus/ChangeLog create mode 100644 apps/runplus/README.md create mode 100644 apps/runplus/app-icon.js create mode 100644 apps/runplus/app.js create mode 100644 apps/runplus/app.png create mode 100644 apps/runplus/metadata.json create mode 100644 apps/runplus/screenshot.png create mode 100644 apps/runplus/settings.js diff --git a/apps/runplus/ChangeLog b/apps/runplus/ChangeLog new file mode 100644 index 000000000..e79696c78 --- /dev/null +++ b/apps/runplus/ChangeLog @@ -0,0 +1,17 @@ +0.01: New App! +0.02: Set pace format to mm:ss, time format to h:mm:ss, + added settings to opt out of GPS and HRM +0.03: Fixed distance calculation, tested against Garmin Etrex, Amazfit GTS 2 +0.04: Use the exstats module, and make what is displayed configurable +0.05: exstats updated so update 'distance' label is updated, option for 'speed' +0.06: Add option to record a run using the recorder app automatically +0.07: Fix crash if an odd number of active boxes are configured (fix #1473) +0.08: Added support for notifications from exstats. Support all stats from exstats +0.09: Fix broken start/stop if recording not enabled (fix #1561) +0.10: Don't allow the same setting to be chosen for 2 boxes (fix #1578) +0.11: Notifications fixes +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: 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) \ No newline at end of file diff --git a/apps/runplus/README.md b/apps/runplus/README.md new file mode 100644 index 000000000..7f645b518 --- /dev/null +++ b/apps/runplus/README.md @@ -0,0 +1,69 @@ +# Run App + +This app allows you to display the status of your run, it +shows distance, time, steps, cadence, pace and more. + +To use it, start the app and press the middle button so that +the red `STOP` in the bottom right turns to a green `RUN`. + +## Display + +* `DIST` - the distance travelled based on the GPS (if you have a GPS lock). + * NOTE: this is based on the GPS coordinates which are not 100% accurate, especially initially. As + the GPS updates your position as it gets more satellites your position changes and the distance + shown will increase, even if you are standing still. +* `TIME` - the elapsed time for your run +* `PACE` - the number of minutes it takes you to run a given distance, configured in settings (default 1km) **based on your run so far** +* `HEART (BPM)` - Your current heart rate +* `Max BPM` - Your maximum heart rate reached during the run +* `STEPS` - Steps since you started exercising +* `CADENCE` - Steps per second based on your step rate *over the last minute* +* `GPS` - this is green if you have a GPS lock. GPS is turned on automatically +so if you have no GPS lock you just need to wait. +* The current time is displayed right at the bottom of the screen +* `RUN/STOP` - whether the distance for your run is being displayed or not + +## Recording Tracks + +When the `Recorder` app is installed, `Run` will automatically start and stop tracks +as needed, prompting you to overwrite or begin a new track if necessary. + +## Settings + +Under `Settings` -> `App` -> `Run` you can change settings for this app. + +* `Record Run` (only displayed if `Recorder` app installed) should the Run app automatically +record GPS/HRM/etc data every time you start a run? +* `Pace` is the distance that pace should be shown over - 1km, 1 mile, 1/2 Marathon or 1 Marathon +* `Boxes` leads to a submenu where you can configure what is shown in each of the 6 boxes on the display. + Available stats are "Time", "Distance", "Steps", "Heart (BPM)", "Max BPM", "Pace (avg)", "Pace (curr)", "Speed", and "Cadence". + Any box set to "-" will display no information. + * Box 1 is the top left (defaults to "Distance") + * Box 2 is the top right (defaults to "Time") + * Box 3 is the middle left (defaults to "Pace (avg)") + * Box 4 is the middle right (defaults to "Heart (BPM)") + * Box 5 is the bottom left (defaults to "Steps") + * Box 6 is the bottom right (defaults to "Cadence") +* `Notifications` leads to a submenu where you can configure if the app will notify you after +your distance, steps, or time repeatedly pass your configured thresholds + * `Ntfy Dist`: The distance that you must pass before you are notified. Follows the `Pace` options + * "Off" (default), "1km", "1 mile", "1/2 Marathon", "1 Marathon" + * `Ntfy Steps`: The number of steps that must pass before you are notified. + * "Off" (default), 100, 500, 1000, 5000, 10000 + * `Ntfy Time`: The amount of time that must pass before you are notified. + * "Off" (default), "30 sec", "1 min", "2 min", "5 min", "10 min", "30 min", "1 hour" + * `Dist Pattern`: The vibration pattern to use to notify you about meeting your distance threshold + * `Step Pattern`: The vibration pattern to use to notify you about meeting your step threshold + * `Time Pattern`: The vibration pattern to use to notify you about meeting your time threshold + +## TODO + +* Keep a log of each run's stats (distance/steps/etc) + +## Development + +This app uses the [`exstats` module](https://github.com/espruino/BangleApps/blob/master/modules/exstats.js). When uploaded via the +app loader, the module is automatically included in the app's source. However +when developing via the IDE the module won't get pulled in by default. + +There are some options to fix this easily - please check out the [modules README.md file](https://github.com/espruino/BangleApps/blob/master/modules/README.md) diff --git a/apps/runplus/app-icon.js b/apps/runplus/app-icon.js new file mode 100644 index 000000000..a97d1b8ce --- /dev/null +++ b/apps/runplus/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///pH9vEFt9TIW0FqALJitUBZNVqoLqgo4BHZAUBtBTHgILB1XAEREV1WsEQ9AgWq1ALHgEO1WtBYxCBhWq0pdInWq2tABY8q1WVBZGq1XFBZS/IKQRvCDIsP9WsBZP60CTCBYs//+wLxALBTQ4AB///+AKHgYLB/gLK/4LHh//AIIwFitVr/8DIIwFLANXBAILIqogBn7DBEYrXBeQRgIBYKmHDgYLLZRBACBZYKJZIILKKRZeWgJGKAFQA==")) diff --git a/apps/runplus/app.js b/apps/runplus/app.js new file mode 100644 index 000000000..507e8581a --- /dev/null +++ b/apps/runplus/app.js @@ -0,0 +1,160 @@ +var ExStats = require("exstats"); +var B2 = process.env.HWVERSION===2; +var Layout = require("Layout"); +var locale = require("locale"); +var fontHeading = "6x8:2"; +var fontValue = B2 ? "6x15:2" : "6x8:3"; +var headingCol = "#888"; +var fixCount = 0; +var isMenuDisplayed = false; + +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// --------------------------- +let settings = Object.assign({ + record: true, + B1: "dist", + B2: "time", + B3: "pacea", + B4: "bpm", + B5: "step", + B6: "caden", + paceLength: 1000, + notify: { + dist: { + value: 0, + notifications: [], + }, + step: { + value: 0, + notifications: [], + }, + time: { + value: 0, + notifications: [], + }, + }, +}, require("Storage").readJSON("run.json", 1) || {}); +var statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!==""); +var exs = ExStats.getStats(statIDs, settings); +// --------------------------- + +function setStatus(running) { + layout.button.label = running ? "STOP" : "START"; + layout.status.label = running ? "RUN" : "STOP"; + layout.status.bgCol = running ? "#0f0" : "#f00"; + layout.render(); +} + +// Called to start/stop running +function onStartStop() { + 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; + 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 { + promise = promise.then( + () => WIDGETS["recorder"].setRecording(false) + ); + } + } + + promise = promise.then(() => { + if (running) { + if (shouldResume) + exs.resume() + else + exs.start(); + } else { + exs.stop(); + } + // if stopping running, don't clear state + // so we can at least refer to what we've done + setStatus(running); + }); +} + +var lc = []; +// Load stats in pair by pair +for (var i=0;ilayout[e.id].label = e.getString()); + if (sb) sb.on('changed', e=>layout[e.id].label = e.getString()); +} +// At the bottom put time/GPS state/etc +lc.push({ type:"h", filly:1, c:[ + {type:"txt", font:fontHeading, label:"GPS", id:"gps", fillx:1, bgCol:"#f00" }, + {type:"txt", font:fontHeading, label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg }, + {type:"txt", font:fontHeading, label:"---", id:"status", fillx:1 } +]}); +// Now calculate the layout +var layout = new Layout( { + type:"v", c: lc +},{lazy:true, btns:[{ label:"---", cb: onStartStop, id:"button"}]}); +delete lc; +setStatus(exs.state.active); +layout.render(); + +function configureNotification(stat) { + stat.on('notify', (e)=>{ + settings.notify[e.id].notifications.reduce(function (promise, buzzPattern) { + return promise.then(function () { + return Bangle.buzz(buzzPattern[0], buzzPattern[1]); + }); + }, Promise.resolve()); + }); +} + +Object.keys(settings.notify).forEach((statType) => { + if (settings.notify[statType].increment > 0 && exs.stats[statType]) { + configureNotification(exs.stats[statType]); + } +}); + +// Handle GPS state change for icon +Bangle.on("GPS", function(fix) { + layout.gps.bgCol = fix.fix ? "#0f0" : "#f00"; + if (!fix.fix) return; // only process actual fixes + if (fixCount++ === 0) { + Bangle.buzz(); // first fix, does not need to respect quiet mode + } +}); +// We always call ourselves once a second to update +setInterval(function() { + layout.clock.label = locale.time(new Date(),1); + if (!isMenuDisplayed) layout.render(); +}, 1000); diff --git a/apps/runplus/app.png b/apps/runplus/app.png new file mode 100644 index 0000000000000000000000000000000000000000..7059b8b015e20039a96de8b65c8a6b68a5e51e18 GIT binary patch literal 1479 zcmV;&1vvVNP)b0~r9hI1hzN2C_Hz2bec46d zz32!1f7|Dr^Zft+eV+5aXV1bTJi@~b@gPX$zNip1Yz1>vN2mz2DMEkvlG@MN zqD@lE?a)(59(>u0=Kppec*EA5(*zQ(EG3+(+KI->CEFJ!ACP%5!I+F&=p{{UFEnH!o7WN}(7DfEF{6T=^qRTOGUUmDCINa`%K?dqCzyBw&^& zdx;n90#h)O*AZj`^*@L)UE%?ZC>&z+*w0Pj=Mr*u5X*l)p2LibbmVEnr6riETg&$lZ8)JSSrFFIvt>|1LfBuoykZR)&GJv4Nif!en;z zv+6Z;L-k`+==0YD>-(E?LK%{G3lE#^`JOB$P&RcWluLnM)#~Vk(_duC*`K_4L>V>S z)_4dF#+CBq(XN1$@$$f&f%ft_-N11%bdLd>txt@&{Fx`lWV(+H188fpdoMWEz{=c= zcS=M^psn$7;>Mk!^KP#d`utPCk6iBMM%oOs0tuJ37nQ16rohH)E z9ia`tY}y~?;W9hY+Ci=jL57GcSZ9mZX%$dBo+%nx{F~SG>u0QvN67NBtqYU0IzJ12 z!D%4xE@^d0`F6j&s+C&7&h>F)stg0CXcP_pe^xQKQS72jOY-(wh zZbX~G&?_#4KvUbwYbW!Gs64aVQ65`xr~;X3@&0)ixmMMvar5@_BseI*>bwB=Hsh4X zYQh$!?Cq~z?1DoTIA3l2sj5-q2GG>pLe-60r0n}JscF?|Pu=!AaWEs^-w{4*u-m|v z)cN}2e|5diF%6APc(zvE@zj|5xTt9M^e(*}a5ghB(al~?Yp}~eT3VX+_&;u68O^;r zFm#Zy>3N^}(KCW>&hyn7OqS~|H4F%<8>aciPOVnaXAj8ih*=;qfnl~5NeLTDd^m8~ zvnv)fz-)*f*%Y>B(}4>B-+uJwvah5;)T5WD&l4qTwmGbG5^Ni*$W&M4^}G>c@+r0 zm&L6n!K$N!Oe1_uD^fG^zOXju@RlWhrFCBC&eabb;-_mPZum831lFJkDuuov#6kU5 z4CF+`AmwiE&BPU?TrL(A+P>=nva%L?1T`4WzcBdbD0fZgQOG$4`W)s&T>c@z;lw?$ z*r#$R-lF#xBG~4P-1p~Z&LlR|e(BYTGQ71roRQk)24wckiNM_6L75_6I@Icn{4O=n ze($4X(0i|)Kwp^SEDqOJAUkp)FsXM?t`S20EwnAT>>nDL9jAl(3m`*#gmZdWt=rKSDd*uTr+%eqw4OIaxzdEo;5|cv`pDBSN47K|UJq=6VDzBkHWn*!;PlpF ze(^n}LDM%GMu;}M5I}j(GWAen6Un4xU5>-I1Dx#l>;Z{Cb7j|( zK<4L6>y{&Db9JC52kOi-}{Chnt6Ig+jZL(>VA`ls2Ey8Pr z5g^sQAZ*o1m(Vto)Z+Le8OKl6@3BpBwPY6bEyL(Q`O<}w+4^NdTV2%8y72;!hZNA~ z7e5!Cguz0u9-KA*OW;@CjDsB=R?KtX(W$m2Pvx*tiR%P@tJPcgjyRu_%KN-Z`7dnf9xz8B?mMSbYsWY$ z|DH%HG)k&{8R($3%9yHlny2>;l_#4B@At;kl=anmT@wvY{%ZH)iZ7^cMFZ6px|1Yg zb1ya^vEmFYaQ~Gc6CeBeow@CT2^;E8ZC`kZn$GO9|Vf ze?f%B`R>(U5;Fsxt@F(94D1X_aTMBG42no{a*+^|um0fr6#*HA0O~axgw%v{i~Gn5 z%02Wc*Kf85fv@=eY|eO2I1ODpIx{^8KPD41?X`$RNxNOt5&|Ct2}(i_R_5*k)J zrhRWRGxo0U8uqUHpH^(yB}QuJn>c3B)=f>Nyi6)) zOECeHxpTcTXV<(xwEb zL5K`^0C}h(kXu7@S)!O!?8U`7Pl_6cPWVjLk)lom+B7>(RjvIA_R~F%YcSB-v@x@@ zo{E$!+3*JZls|AoVBq3zHdp=$5cLS`VM7LhB9IgfSO>cieEb*>{Cw_r63#PFK>d_Geiezu=XmOLG9&hHfP!exIkoxY zu=Lu7Kvncq2B?F5C0 z{D1i+DS}UyX&d!9ejkpsod{l>J$TwvhFFasSSQPkFowWX5*(_FQP|JxzZK?LOcV5E zX~~8dj-F(O`rVy3P6gjS8u|#t7oqdOIXV&7Te5bH0+(BI62UQ)vg&QjqWVhPc^yY4 zrRUYvR)Bj}U7G9%6t!ZLxG>P|9aBt$Lkm?E08efy$6AtPvSxHz6D}Q9?={^jc7K*r z4&5>&fd5wdTS!ON-xx&ZXu$K@g$CP74GEv1ZmOa+S(1ZL&0gd z2wA44M^ee5V3>5YQCdwiorujC=%IR07&&Luc{Uk5p#o0>_qSu5UGM`mYg9plqT9k5Fm}m$@z|5W3Y&%BZz_t}4gunQ2dPQS0Ys-Jws1Af4#!lFjbGC3h$ZT>q z$>%JcvLGzd6(e&d1@u`l1kimf#+;zcH`&Ep;Z93=uAfnvj8rc(u{@y(Eh=D4!Y+C=>@{sS#;$nO zBe;o^+DZ0`Jmm7#Y?sguyYwCU(&r>&66>J>stxwllI=Wb(aSHh2K2S$LkQJ76TrDk zd5(p#Kg^YTp#FZFCdyO?ur#Lb?CBpzu3g%`;ty6`vlp+(o>f?e>y@Y*eIFYodI$mw zLF5;$P6`X(G=L(bmr&sLs1?6=sK+gE46(1}ZU#v!VP{1l1Lyqh=Jr1;`(IPq=V()# zY7Mrh#HE@25YZm`j9;9G>v|nruUC9Z#UGG}mNS=m z>8EXdN0Hn`mX}FkxkRd88+u!ECZ*!fsB}iJ!Lm&4MX^b5SJ4csi2S?Ltm$aVx805F zGs!LV4N2rF;F7ZGtN|)AJ3*y9qhf%e`PCm#-*o4BpCY96TsP$EONg{8A;IO91vsd_ zyC6~7Y*mX1Q6j5l>`cK`%%R_>z$oAc z{nAIY*HZF$;qcJvGESyoAuVcn`Tp&Yb-o;D7=08?eGkOjRj$jt0L6bA zwouU#--D%6gA3I*ybKPr+Z73ky6GjlyP=z@J2Q-W1hFDs|3Z_3MbJrVcISm|EgkSx z%#o7kjA(C2g5J$F2bF+t7|s&?%^Z=1zFK3c#L+lk?c20h*jLV|AJ0fDN;@p)mjX-I zx)`35$zn}cwL#_gUk&9jh^*@KP2q|Vwrhw4Vnw~CZ5p|Ak?>Y|A2aw~M?}KMH59^i zSZR_sak%ILj=h}sBL$=UkB(KRiw69X4 z+g3@Rmg66AH^Q{Pzowona#!&QJMkYAmviIum!RTbQzb{v9uCx#&24<4{*OP0sM_FN z^0IFQjCHDo7t|p|(+Bk3y=`Kx)u8r9|7s+-m`fGST>PpeB~wy5h&mC#cEsmGq)ngz zFO_6y=>(POfDggC#%KbX{i%y7Ssj(AoewgPKBvt`u_jGo*G@ek4#&$%9sHd1J7k;f zsxoe5O37SoPSzs`8~ofZ=D-1!$u$t_=L<9QWM2GXs=byV1e2twmJFzYG?>+jA*glEA|wtfm;ZnwmpG>`?G z6yps4eFN}1ZPCm#ivusQObV|16alTM(UWcQdJ1(ktbz3qO#0)|eNrMYGsVSM;-S#V z4;Kgw25E4<;+`jJ>M!AN;E?R?bhWA= zu8Q>QmqvsR*zjp*kg+>Ls(+t&g_#$jSDa1aQo@dQ(`geJV9WA z4_qe|J#kc_=XBxFT2AzD+uwLMxqrPl6+r{a16O}V46{AL6t%%z*i zU*q|ZKiWe83il}i`hXjPa_)rpovkqCjsL2m#{W{nUyJWPy*pfYy1Dn&JHpgTg=0a_ zYjGiKX*)6{I;Es}M569jLPI|f<{43aKv4^0%Rj(kC)AHEDuqeAxmHtte4qVDwV{}3 zqp%Q~Q*)rwkie6*LLvIRc`yT*`SBJY%*D64SP6-#b#1jWmy(1?ew+Jves z*3KaRLi6JQ_7gs$SFslk5g7rMHVp%Cslk@2RirS$?R4(!@hml*pD?%!4Yy&s=qBznd7RQ-OlBXj*JM}W|=r!{x0f1mfQs.id); + + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage') + let settings = Object.assign({ + record: true, + B1: "dist", + B2: "time", + B3: "pacea", + B4: "bpm", + B5: "step", + B6: "caden", + paceLength: 1000, // TODO: Default to either 1km or 1mi based on locale + notify: { + dist: { + increment: 0, + notifications: [], + }, + step: { + increment: 0, + notifications: [], + }, + time: { + increment: 0, + notifications: [], + }, + }, + }, storage.readJSON(SETTINGS_FILE, 1) || {}); + function saveSettings() { + storage.write(SETTINGS_FILE, settings) + } + + function getBoxChooser(boxID) { + return { + min: 0, max: statsIDs.length-1, + value: Math.max(statsIDs.indexOf(settings[boxID]),0), + format: v => statsList[v].name, + onchange: v => { + settings[boxID] = statsIDs[v]; + saveSettings(); + }, + } + } + + function sampleBuzz(buzzPatterns) { + return buzzPatterns.reduce(function (promise, buzzPattern) { + return promise.then(function () { + return Bangle.buzz(buzzPattern[0], buzzPattern[1]); + }); + }, Promise.resolve()); + } + + var menu = { + '': { 'title': 'Run' }, + '< Back': back, + }; + if (global.WIDGETS&&WIDGETS["recorder"]) + menu[/*LANG*/"Record Run"] = { + value : !!settings.record, + onchange : v => { + settings.record = v; + saveSettings(); + } + }; + var notificationsMenu = { + '< Back': function() { E.showMenu(menu) }, + } + menu[/*LANG*/"Notifications"] = function() { E.showMenu(notificationsMenu)}; + ExStats.appendMenuItems(menu, settings, saveSettings); + ExStats.appendNotifyMenuItems(notificationsMenu, settings, saveSettings); + var vibPatterns = [/*LANG*/"Off", ".", "-", "--", "-.-", "---"]; + var vibTimes = [ + [], + [[100, 1]], + [[300, 1]], + [[300, 1], [300, 0], [300, 1]], + [[300, 1],[300, 0], [100, 1], [300, 0], [300, 1]], + [[300, 1],[300, 0],[300, 1],[300, 0],[300, 1]], + ]; + notificationsMenu[/*LANG*/"Dist Pattern"] = { + value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.dist.notifications))), + min: 0, max: vibTimes.length - 1, + format: v => vibPatterns[v]||/*LANG*/"Off", + onchange: v => { + settings.notify.dist.notifications = vibTimes[v]; + sampleBuzz(vibTimes[v]); + saveSettings(); + } + } + notificationsMenu[/*LANG*/"Step Pattern"] = { + value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.step.notifications))), + min: 0, max: vibTimes.length - 1, + format: v => vibPatterns[v]||/*LANG*/"Off", + onchange: v => { + settings.notify.step.notifications = vibTimes[v]; + sampleBuzz(vibTimes[v]); + saveSettings(); + } + } + notificationsMenu[/*LANG*/"Time Pattern"] = { + value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.time.notifications))), + min: 0, max: vibTimes.length - 1, + format: v => vibPatterns[v]||/*LANG*/"Off", + onchange: v => { + settings.notify.time.notifications = vibTimes[v]; + sampleBuzz(vibTimes[v]); + saveSettings(); + } + } + var boxMenu = { + '< Back': function() { E.showMenu(menu) }, + } + Object.assign(boxMenu,{ + 'Box 1': getBoxChooser("B1"), + 'Box 2': getBoxChooser("B2"), + 'Box 3': getBoxChooser("B3"), + 'Box 4': getBoxChooser("B4"), + 'Box 5': getBoxChooser("B5"), + 'Box 6': getBoxChooser("B6"), + }); + menu[/*LANG*/"Boxes"] = function() { E.showMenu(boxMenu)}; + E.showMenu(menu); +}) From e1a51d7c68aeb4a260a11541e1d1f6996e021d04 Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Thu, 1 Jun 2023 20:02:43 +0200 Subject: [PATCH 34/64] kbswipe: Redone patterns a,e,m,w,z. --- apps/kbswipe/ChangeLog | 1 + apps/kbswipe/lib.js | 17 +++++++++-------- apps/kbswipe/metadata.json | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/kbswipe/ChangeLog b/apps/kbswipe/ChangeLog index a7b2d44c2..38d71986e 100644 --- a/apps/kbswipe/ChangeLog +++ b/apps/kbswipe/ChangeLog @@ -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. diff --git a/apps/kbswipe/lib.js b/apps/kbswipe/lib.js index ea6d78255..4c652e9a9 100644 --- a/apps/kbswipe/lib.js +++ b/apps/kbswipe/lib.js @@ -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, this.y + 24); 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=>{ diff --git a/apps/kbswipe/metadata.json b/apps/kbswipe/metadata.json index 6b597a371..3f3fbffa3 100644 --- a/apps/kbswipe/metadata.json +++ b/apps/kbswipe/metadata.json @@ -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", From dc069b0ec9c528b24e29e411521ea34358330f96 Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Fri, 2 Jun 2023 07:33:44 +0200 Subject: [PATCH 35/64] kbswipe: Fix off-by-one error --- apps/kbswipe/lib.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/kbswipe/lib.js b/apps/kbswipe/lib.js index 4c652e9a9..7d05d7a8e 100644 --- a/apps/kbswipe/lib.js +++ b/apps/kbswipe/lib.js @@ -239,7 +239,7 @@ exports.input = function(options) { area:"tl", width: 36, // 3 chars, 6*2 px/char draw: function() { - g.reset().clearRect(this.x, this.y, this.x + this.width, this.y + 24); + 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) { From d5f114762f331237befbdedc39f4b079e8bd2dc3 Mon Sep 17 00:00:00 2001 From: thyttan <6uuxstm66@mozmail.com⁩> Date: Wed, 22 Feb 2023 22:25:56 +0100 Subject: [PATCH 36/64] add changes to runplus changes to differentiate runplus from run app fix default settings fallback for HRM fixes to draw correctly and in a timely manner when swiping to the karvonnen ui depending on if we have hrm data or not Update ChangeLog small ui tweak run: Keep run state between runs (allowing you to exit and restart the app) tweak clearRect width to not clear the indicator segment bump version write to correct settings file add tag karvonen change spelling karvonnen -> karvonen in app.js, karvonen.js spelling karvonnen -> karvonen in metadata runplus - Fix typo in variable name preventing starting a run tweak and align HRM min/max defaults, change allowed min/max interval in settings bump version, fix typo follow the preferred metadata.json style Readd contributors to README.md Tweak ChangeLog --- apps/run/ChangeLog | 2 +- apps/runplus/ChangeLog | 9 +- apps/runplus/README.md | 7 ++ apps/runplus/app.js | 105 +++++++++++++----- apps/runplus/karvonen.js | 215 +++++++++++++++++++++++++++++++++++++ apps/runplus/metadata.json | 26 +++-- apps/runplus/settings.js | 30 +++++- 7 files changed, 352 insertions(+), 42 deletions(-) create mode 100644 apps/runplus/karvonen.js diff --git a/apps/run/ChangeLog b/apps/run/ChangeLog index e79696c78..ab2803ec6 100644 --- a/apps/run/ChangeLog +++ b/apps/run/ChangeLog @@ -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) \ No newline at end of file +0.16: Added ability to resume a run that was stopped previously (fix #1907) diff --git a/apps/runplus/ChangeLog b/apps/runplus/ChangeLog index e79696c78..05d24b96d 100644 --- a/apps/runplus/ChangeLog +++ b/apps/runplus/ChangeLog @@ -14,4 +14,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: 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) \ No newline at end of file +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. diff --git a/apps/runplus/README.md b/apps/runplus/README.md index 7f645b518..659cd964d 100644 --- a/apps/runplus/README.md +++ b/apps/runplus/README.md @@ -67,3 +67,10 @@ app loader, the module is automatically included in the app's source. However when developing via the IDE the module won't get pulled in by default. There are some options to fix this easily - please check out the [modules README.md file](https://github.com/espruino/BangleApps/blob/master/modules/README.md) +## Contributors (Run and Run+) +gfwilliams +hughbarney +GrandVizierOlaf +BartS23 +f-teacher +thyttan diff --git a/apps/runplus/app.js b/apps/runplus/app.js index 507e8581a..41fab7ae2 100644 --- a/apps/runplus/app.js +++ b/apps/runplus/app.js @@ -1,16 +1,23 @@ -var ExStats = require("exstats"); -var B2 = process.env.HWVERSION===2; -var Layout = require("Layout"); -var locale = require("locale"); -var fontHeading = "6x8:2"; -var fontValue = B2 ? "6x15:2" : "6x8:3"; -var headingCol = "#888"; -var fixCount = 0; -var isMenuDisplayed = false; +// Use widget utils to show/hide widgets +let wu = require("widget_utils"); -g.clear(); +let runInterval; +let karvonenActive = false; +// Run interface wrapped in a function +let ExStats = require("exstats"); +let B2 = process.env.HWVERSION===2; +let Layout = require("Layout"); +let locale = require("locale"); +let fontHeading = "6x8:2"; +let fontValue = B2 ? "6x15:2" : "6x8:3"; +let headingCol = "#888"; +let fixCount = 0; +let isMenuDisplayed = false; + +g.reset().clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); +wu.show(); // --------------------------- let settings = Object.assign({ @@ -36,9 +43,13 @@ let settings = Object.assign({ notifications: [], }, }, -}, require("Storage").readJSON("run.json", 1) || {}); -var statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!==""); -var exs = ExStats.getStats(statIDs, settings); + HRM: { + min: 55, + max: 185, + }, +}, require("Storage").readJSON("runplus.json", 1) || {}); +let statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!==""); +let exs = ExStats.getStats(statIDs, settings); // --------------------------- function setStatus(running) { @@ -100,11 +111,11 @@ function onStartStop() { }); } -var lc = []; +let lc = []; // Load stats in pair by pair -for (var i=0;i{if (karvonenActive) {stopKarvonenUI();run();} onStartStop();}), id:"button"}]}); delete lc; setStatus(exs.state.active); layout.render(); @@ -132,16 +143,16 @@ layout.render(); function configureNotification(stat) { stat.on('notify', (e)=>{ settings.notify[e.id].notifications.reduce(function (promise, buzzPattern) { - return promise.then(function () { - return Bangle.buzz(buzzPattern[0], buzzPattern[1]); - }); + return promise.then(function () { + return Bangle.buzz(buzzPattern[0], buzzPattern[1]); + }); }, Promise.resolve()); }); } Object.keys(settings.notify).forEach((statType) => { if (settings.notify[statType].increment > 0 && exs.stats[statType]) { - configureNotification(exs.stats[statType]); + configureNotification(exs.stats[statType]); } }); @@ -153,8 +164,46 @@ Bangle.on("GPS", function(fix) { Bangle.buzz(); // first fix, does not need to respect quiet mode } }); -// We always call ourselves once a second to update -setInterval(function() { - layout.clock.label = locale.time(new Date(),1); - if (!isMenuDisplayed) layout.render(); -}, 1000); + +// run() function used to switch between traditional run UI and karvonen UI +function run() { + wu.show(); + layout.lazy = false; + layout.render(); + layout.lazy = true; + // We always call ourselves once a second to update + if (!runInterval){ + runInterval = setInterval(function() { + layout.clock.label = locale.time(new Date(),1); + if (!isMenuDisplayed && !karvonenActive) layout.render(); + }, 1000); + } +} +run(); + +/////////////////////////////////////////////// +// Karvonen +/////////////////////////////////////////////// + +function stopRunUI() { + // stop updating and drawing the traditional run app UI + clearInterval(runInterval); + runInterval = undefined; + karvonenActive = true; +} + +function stopKarvonenUI() { + g.reset().clear(); + clearInterval(karvonenInterval); + karvonenInterval = undefined; + karvonenActive = false; +} + +let karvonenInterval; +// Define the function to go back and forth between the different UI's +function swipeHandler(LR,_) { + if (LR==-1 && karvonenActive && !isMenuDisplayed) {stopKarvonenUI(); run();} + if (LR==1 && !karvonenActive && !isMenuDisplayed) {stopRunUI(); karvonenInterval = eval(require("Storage").read("runplus_karvonen"))(settings.HRM, exs.stats.bpm);} +} +// Listen for swipes with the swipeHandler +Bangle.on("swipe", swipeHandler); diff --git a/apps/runplus/karvonen.js b/apps/runplus/karvonen.js new file mode 100644 index 000000000..de81494bb --- /dev/null +++ b/apps/runplus/karvonen.js @@ -0,0 +1,215 @@ +(function karvonen(hrmSettings, exsHrmStats) { + //This app is an extra feature implementation for the Run.app of the bangle.js. It's called run+ + //The calculation of the Heart Rate Zones is based on the Karvonen method. It requires to know maximum and minimum heart rates. More precise calculation methods require a lab. + //Other methods are even more approximative. + let wu = require("widget_utils"); + wu.hide(); + let R = Bangle.appRect; + + + g.reset().clearRect(R).setFontAlign(0,0,0); + + const x = "x"; const y = "y"; + function Rdiv(axis, divisor) { // Used when placing things on the screen + return axis=="x" ? (R.x + (R.w-1)/divisor):(R.y + (R.h-1)/divisor); + } + let linePoints = { //Not lists of points, but used to update points in the drawArrows function. + x: [ + 175/40, + 2, + 175/135, + ], + y: [ + 175/64, + 175/52, + 175/110, + 175/122, + ], + + }; + + function drawArrows() { + g.setColor(g.theme.fg); + // Upper + g.drawLine(Rdiv(x,linePoints.x[0]), Rdiv(y,linePoints.y[0]), Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[1])); + g.drawLine(Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[1]), Rdiv(x,linePoints.x[2]), Rdiv(y,linePoints.y[0])); + // Lower + g.drawLine(Rdiv(x,linePoints.x[0]), Rdiv(y,linePoints.y[2]), Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[3])); + g.drawLine(Rdiv(x,linePoints.x[1]), Rdiv(y,linePoints.y[3]), Rdiv(x,linePoints.x[2]), Rdiv(y,linePoints.y[2])); + } + + //To calculate Heart rate zones, we need to know the heart rate reserve (HRR) + // HRR = maximum HR - Minimum HR. minhr is minimum hr, maxhr is maximum hr. + //get the hrr (heart rate reserve). + // I put random data here, but this has to come as a menu in the settings section so that users can change it. + let minhr = hrmSettings.min; + let maxhr = hrmSettings.max; + + function calculatehrr(minhr, maxhr) { + return maxhr - minhr;} + + //test input for hrr (it works). + let hrr = calculatehrr(minhr, maxhr); + console.log(hrr); + + //Test input to verify the zones work. The following value for "hr" has to be deleted and replaced with the Heart Rate Monitor input. + let hr = exsHrmStats.getValue(); + let hr1 = hr; + // These letiables display next and previous HR zone. + //get the hrzones right. The calculation of the Heart rate zones here is based on the Karvonen method + //60-70% of HRR+minHR = zone2. //70-80% of HRR+minHR = zone3. //80-90% of HRR+minHR = zone4. //90-99% of HRR+minHR = zone5. //=>99% of HRR+minHR = serious risk of heart attack + let minzone2 = hrr * 0.6 + minhr; + let maxzone2 = hrr * 0.7 + minhr; + let maxzone3 = hrr * 0.8 + minhr; + let maxzone4 = hrr * 0.9 + minhr; + let maxzone5 = hrr * 0.99 + minhr; + + // HR data: large, readable, in the middle of the screen + function drawHR() { + g.setFontAlign(-1,0,0); + g.clearRect(Rdiv(x,11/4),Rdiv(y,2)-25,Rdiv(x,11/4)+50*2-14,Rdiv(y,2)+25); + g.setColor(g.theme.fg); + g.setFont("Vector",50); + g.drawString(hr, Rdiv(x,11/4), Rdiv(y,2)+4); + } + + function drawWaitHR() { + g.setColor(g.theme.fg); + // Waiting for HRM + g.setFontAlign(0,0,0); + g.setFont("Vector",50); + g.drawString("--", Rdiv(x,2)+4, Rdiv(y,2)+4); + + // Waiting for current Zone + g.setFont("Vector",24); + g.drawString("Z-", Rdiv(x,4.3)-3, Rdiv(y,2)+2); + + // waiting for upper and lower limit of current zone + g.setFont("Vector",20); + g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/2)); + g.drawString("--", Rdiv(x,2)+2, Rdiv(y,9/7)); + } + + //These functions call arcs to show different HR zones. + + //To shorten the code, I'll reference some letiables and reuse them. + let centreX = R.x + 0.5 * R.w; + let centreY = R.y + 0.5 * R.h; + let minRadius = 0.38 * R.h; + let maxRadius = 0.50 * R.h; + + //draw background image (dithered green zones)(I should draw different zones in different dithered colors) + const HRzones= require("graphics_utils"); + let minRadiusz = 0.44 * R.h; + let startAngle = HRzones.degreesToRadians(-88.5); + let endAngle = HRzones.degreesToRadians(268.5); + + function drawBgArc() { + g.setColor(g.theme.dark==false?0xC618:"#002200"); + HRzones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle); + } + + const zones = require("graphics_utils"); + //####### A function to simplify a bit the code ###### + function simplify (sA, eA, Z, currentZone, lastZone) { + let startAngle = zones.degreesToRadians(sA); + let endAngle = zones.degreesToRadians(eA); + if (currentZone == lastZone) zones.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle, endAngle); + else zones.fillArc(g, centreX, centreY, minRadiusz, maxRadius, startAngle, endAngle); + g.setFont("Vector",24); + g.clearRect(Rdiv(x,4.3)-12, Rdiv(y,2)+2-12,Rdiv(x,4.3)+12, Rdiv(y,2)+2+12); + g.setFontAlign(0,0,0); + g.drawString(Z, Rdiv(x,4.3), Rdiv(y,2)+2); + } + + function zoning (max, min) { // draw values of upper and lower limit of current zone + g.setFont("Vector",20); + g.setColor(g.theme.fg); + g.clearRect(Rdiv(x,2)-20*2, Rdiv(y,9/2)-10,Rdiv(x,2)+20*2, Rdiv(y,9/2)+10); + g.clearRect(Rdiv(x,2)-20*2, Rdiv(y,9/7)-10,Rdiv(x,2)+20*2, Rdiv(y,9/7)+10); + g.setFontAlign(0,0,0); + g.drawString(max, Rdiv(x,2), Rdiv(y,9/2)); + g.drawString(min, Rdiv(x,2), Rdiv(y,9/7)); + } + + function clearCurrentZone() { // Clears the extension of the current zone by painting the extension area in background color + g.setColor(g.theme.bg); + HRzones.fillArc(g, centreX, centreY, minRadius-1, minRadiusz, startAngle, endAngle); + } + + function getZone(zone) { + drawBgArc(); + clearCurrentZone(); + if (zone >= 0) {zoning(minzone2, minhr);g.setColor("#00ffff");simplify(-88.5, -45, "Z1", 0, zone);} + if (zone >= 1) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(-43.5, -21.5, "Z2", 1, zone);} + if (zone >= 2) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(-20, 1.5, "Z2", 2, zone);} + if (zone >= 3) {zoning(maxzone2, minzone2);g.setColor("#00ff00");simplify(3, 24, "Z2", 3, zone);} + if (zone >= 4) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(25.5, 46.5, "Z3", 4, zone);} + if (zone >= 5) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(48, 69, "Z3", 5, zone);} + if (zone >= 6) {zoning(maxzone3, maxzone2);g.setColor("#ffff00");simplify(70.5, 91.5, "Z3", 6, zone);} + if (zone >= 7) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(93, 114.5, "Z4", 7, zone);} + if (zone >= 8) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(116, 137.5, "Z4", 8, zone);} + if (zone >= 9) {zoning(maxzone4, maxzone3);g.setColor("#ff8000");simplify(139, 160, "Z4", 9, zone);} + if (zone >= 10) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(161.5, 182.5, "Z5", 10, zone);} + if (zone >= 11) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(184, 205, "Z5", 11, zone);} + if (zone == 12) {zoning(maxzone5, maxzone4);g.setColor("#ff0000");simplify(206.5, 227.5, "Z5", 12, zone);} + } + + function getZoneAlert() { + const HRzonemax = require("graphics_utils"); + let centreX1,centreY1,maxRadius1 = 1; + let minRadius = 0.40 * R.h; + let startAngle1 = HRzonemax.degreesToRadians(-90); + let endAngle1 = HRzonemax.degreesToRadians(270); + g.setFont("Vector",38);g.setColor("#ff0000"); + HRzonemax.fillArc(g, centreX, centreY, minRadius, maxRadius, startAngle1, endAngle1); + g.drawString("ALERT", 26,66); + } + + //Subdivided zones for better readability of zones when calling the images. //Changing HR zones will trigger the function with the image and previous&next HR zones. + let subZoneLast; + function drawZones() { + if ((hr < maxhr - 2) && subZoneLast==13) {g.clear(); drawArrows(); drawHR();} // Reset UI when coming down from zone alert. + if (hr <= hrr * 0.6 + minhr) {if (subZoneLast!=0) {subZoneLast=0; getZone(subZoneLast);}} // Z1 + else if (hr <= hrr * 0.64 + minhr) {if (subZoneLast!=1) {subZoneLast=1; getZone(subZoneLast);}} // Z2a + else if (hr <= hrr * 0.67 + minhr) {if (subZoneLast!=2) {subZoneLast=2; getZone(subZoneLast);}} // Z2b + else if (hr <= hrr * 0.70 + minhr) {if (subZoneLast!=3) {subZoneLast=3; getZone(subZoneLast);}} // Z2c + else if (hr <= hrr * 0.74 + minhr) {if (subZoneLast!=4) {subZoneLast=4; getZone(subZoneLast);}} // Z3a + else if (hr <= hrr * 0.77 + minhr) {if (subZoneLast!=5) {subZoneLast=5; getZone(subZoneLast);}} // Z3b + else if (hr <= hrr * 0.80 + minhr) {if (subZoneLast!=6) {subZoneLast=6; getZone(subZoneLast);}} // Z3c + else if (hr <= hrr * 0.84 + minhr) {if (subZoneLast!=7) {subZoneLast=7; getZone(subZoneLast);}} // Z4a + else if (hr <= hrr * 0.87 + minhr) {if (subZoneLast!=8) {subZoneLast=8; getZone(subZoneLast);}} // Z4b + else if (hr <= hrr * 0.90 + minhr) {if (subZoneLast!=9) {subZoneLast=9; getZone(subZoneLast);}} // Z4c + else if (hr <= hrr * 0.94 + minhr) {if (subZoneLast!=10) {subZoneLast=10; getZone(subZoneLast);}} // Z5a + else if (hr <= hrr * 0.96 + minhr) {if (subZoneLast!=11) {subZoneLast=11; getZone(subZoneLast);}} // Z5b + else if (hr <= hrr * 0.98 + minhr) {if (subZoneLast!=12) {subZoneLast=12; getZone(subZoneLast);}} // Z5c + else if (hr >= maxhr - 2) {subZoneLast=13; g.clear();getZoneAlert();} // Alert + } + + function initDraw() { + drawArrows(); + if (hr!=0) updateUI(true); else {drawWaitHR(); drawBgArc();} + //drawZones(); + } + + let hrLast; + //h = 0; // Used to force hr update via web ui console field to trigger draws, together with `if (h!=0) hr = h;` below. + function updateUI(resetHrLast) { // Update UI, only draw if warranted by change in HR. + hrLast = resetHrLast?0:hr; // Handles correct updating on init depending on if we've got HRM readings yet or not. + hr = exsHrmStats.getValue(); + //if (h!=0) hr = h; + if (hr!=hrLast) { + drawHR(); + drawZones(); + } //g.setColor(g.theme.fg).drawLine(175/2,0,175/2,175).drawLine(0,175/2,175,175/2); // Used to align UI elements. + } + + initDraw(); + + // check for updates every second. + karvonenInterval = setInterval(function() { + if (!isMenuDisplayed && karvonenActive) updateUI(); + }, 1000); + + return karvonenInterval; +}) diff --git a/apps/runplus/metadata.json b/apps/runplus/metadata.json index ed253a319..40256e595 100644 --- a/apps/runplus/metadata.json +++ b/apps/runplus/metadata.json @@ -1,16 +1,20 @@ -{ "id": "run", - "name": "Run", - "version":"0.16", - "description": "Displays distance, time, steps, cadence, pace and more for runners.", +{ + "id": "runplus", + "name": "Run+", + "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", - "supports" : ["BANGLEJS","BANGLEJS2"], - "screenshots": [{"url":"screenshot.png"}], + "tags": "run,running,fitness,outdoors,gps,karvonen,karvonnen", + "supports": ["BANGLEJS2"], + "screenshots": [{"url": "screenshot.png"}], "readme": "README.md", "storage": [ - {"name":"run.app.js","url":"app.js"}, - {"name":"run.img","url":"app-icon.js","evaluate":true}, - {"name":"run.settings.js","url":"settings.js"} + {"name": "runplus.app.js", "url": "app.js"}, + {"name": "runplus.img", "url": "app-icon.js", "evaluate": true}, + {"name": "runplus.settings.js", "url": "settings.js"}, + {"name": "runplus_karvonen", "url": "karvonen.js"} ], - "data": [{"name":"run.json"}] + "data": [ + {"name": "runplus.json"} + ] } diff --git a/apps/runplus/settings.js b/apps/runplus/settings.js index 0312200a3..539391a27 100644 --- a/apps/runplus/settings.js +++ b/apps/runplus/settings.js @@ -1,5 +1,5 @@ (function(back) { - const SETTINGS_FILE = "run.json"; + const SETTINGS_FILE = "runplus.json"; var ExStats = require("exstats"); var statsList = ExStats.getList(); statsList.unshift({name:"-",id:""}); // add blank menu item @@ -31,6 +31,10 @@ notifications: [], }, }, + HRM: { + min: 55, + max: 185, + }, }, storage.readJSON(SETTINGS_FILE, 1) || {}); function saveSettings() { storage.write(SETTINGS_FILE, settings) @@ -125,5 +129,29 @@ 'Box 6': getBoxChooser("B6"), }); menu[/*LANG*/"Boxes"] = function() { E.showMenu(boxMenu)}; + + var hrmMenu = { + '< Back': function() { E.showMenu(menu) }, + } + + menu[/*LANG*/"HRM min/max"] = function() { E.showMenu(hrmMenu)}; + hrmMenu[/*LANG*/"min"] = { + min: 1, max: 100, + value: settings.HRM.min, + format: w => w, + onchange: w => { + settings.HRM.min = w; + saveSettings(); + }, + } + hrmMenu[/*LANG*/"max"] = { + min: 101, max: 220, + value: settings.HRM.max, + format: v => v, + onchange: v => { + settings.HRM.max = v; + saveSettings(); + }, + } E.showMenu(menu); }) From 7c647add8b666fdca52f9f5128482268e93e37e9 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Fri, 2 Jun 2023 21:45:51 +0100 Subject: [PATCH 37/64] kbmulti: update readme --- apps/kbmulti/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/kbmulti/README.md b/apps/kbmulti/README.md index b6754711d..88b91fa80 100644 --- a/apps/kbmulti/README.md +++ b/apps/kbmulti/README.md @@ -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/) From bd540380a5e4e87018b88adda541c6eace305a8c Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Fri, 2 Jun 2023 18:09:55 -0500 Subject: [PATCH 38/64] Add files via upload --- apps/ohmcalc/ChangeLog | 1 + apps/ohmcalc/README.md | 22 +++ apps/ohmcalc/app-icon.js | 1 + apps/ohmcalc/app.js | 300 +++++++++++++++++++++++++++++++++++++ apps/ohmcalc/app.png | Bin 0 -> 2098 bytes apps/ohmcalc/metadata.json | 22 +++ apps/ohmcalc/ohmcalc.js | 300 +++++++++++++++++++++++++++++++++++++ 7 files changed, 646 insertions(+) create mode 100644 apps/ohmcalc/ChangeLog create mode 100644 apps/ohmcalc/README.md create mode 100644 apps/ohmcalc/app-icon.js create mode 100644 apps/ohmcalc/app.js create mode 100644 apps/ohmcalc/app.png create mode 100644 apps/ohmcalc/metadata.json create mode 100644 apps/ohmcalc/ohmcalc.js diff --git a/apps/ohmcalc/ChangeLog b/apps/ohmcalc/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/ohmcalc/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/ohmcalc/README.md b/apps/ohmcalc/README.md new file mode 100644 index 000000000..f45b8e977 --- /dev/null +++ b/apps/ohmcalc/README.md @@ -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 'Back' button is always available to help you step back through the app. In case you'd like to start afresh from the Results menu, just tap on 'Main Menu'. + +## 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) diff --git a/apps/ohmcalc/app-icon.js b/apps/ohmcalc/app-icon.js new file mode 100644 index 000000000..091392f44 --- /dev/null +++ b/apps/ohmcalc/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4AyQBIvmnM/AAk5F8pWBFwoABMDweGgEHF48HCAw2VE4MGDAgvCRIIACSoIvFgEGG4wuPKAYZCgENn8UgFVqsAik/hoNDC4ouUAAMOLAYoBF4Ok0gGEFwUOTJQuPAAM4DIIJCFAI2GRYM4ZRguNg7pIh0NhpXDAwIVIGBguFdwJlHABbTCgwwPCIgQB1V6dYSXGQ4zzCvWqIwhNBMBwvBvVPbokUKQQADg7BBSQUAp5FBDojANFAQmCAoTxCgEsmc0mkzlgxCMoQwBFwYFBFxgvBgAaCcYcUgEBmdfAA0zCoJiCegc4BIIvNeQwuCg4qEg8rAwowDdhwvLI4IuFLIIHFGAT4EF5rdEJAkHgImHF41fI4p2BAAYtIg8HhpGECwK7FXAQvHBQJIEikNEYIxGgE5UQh2FLw0rlYvHMAwAEnIvDGgIJDigABAwUAlhTGwAvBmgABnQMDlgfDDwRVDMAYvETocOAwSOFLwPVp6wEGAY8BC4MOBgYvMqukgENXwU0Lw3V5+kAALDFBoLaBhsA0lVF6U4F4rFBFgXP6uANoovVR5U6RQlP6ryGR6jvMnTqCYQJeGd6AwBBIYAFRIIiEeQheGr7FBDxE5FwZgCCQMHhqkBIwZTGYYYKGRwJ4DDIMNEYIoCF4YxEAAIWEMBAIBLxhIBAAYtFGYwXEnAmHF4JeFA4J4EAwIrLF5JICGAq9GE4J2EF6JsBI4UNhx5EYY67CFwcOhp3DGB0AFQTQCAoU4AwUsmYAClgHBg5EChwGCAoaNQE4N6p4wDMQIxCAAcHRYYoBp96DoouLgyjE1QZBPYQAEnDmEAAUNIoOqbYkGGBTsFCIL0GABhsCJoZgOFAkHFxEOAASZDNwYVFFxgwHdogJCii+DXoIGCgyXGFxwwHboIoG0mkAwgWBgBnDFyIwFVYQICQgIoBqtVF4TrBBoQXFFyAwDeAI4GnJmDnImGSYIuUJQbKMKxAXGAC4eBF44oeGBCJBAAiVBF0hgCAA4vlGI4tnADg=")) diff --git a/apps/ohmcalc/app.js b/apps/ohmcalc/app.js new file mode 100644 index 000000000..28f3eb9d5 --- /dev/null +++ b/apps/ohmcalc/app.js @@ -0,0 +1,300 @@ +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)': "{0} * {1}", + 'Power (P), Current (I)': "{0} / {1}", + 'Power (P), Resistance (R)': "Math.sqrt({0} * {1})" + }, + 'Current (I)': { + 'Voltage (V), Resistance (R)': "{0} / {1}", + 'Power (P), Voltage (V)': "{0} / {1}", + 'Power (P), Resistance (R)': "Math.sqrt({0} / {1})" + }, + 'Resistance (R)': { + 'Voltage (V), Current (I)': "{0} / {1}", + 'Power (P), Current (I)': "{0} / (Math.pow({1}, 2))", + 'Power (P), Voltage (V)': "(Math.pow({0}, 2)) / {1}" + }, + 'Power (P)': { + 'Voltage (V), Current (I)': "{0} * {1}", + 'Current (I), Resistance (R)': "(Math.pow({0}, 2)) * {1}", + 'Voltage (V), Resistance (R)': "(Math.pow({0}, 2)) / {1}" + }, +}; + +// 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 = ""; + +// Function references +let handleEnter; +let showVariableSelectionMenu; +let showInputMenu; +let resultsMenu; + +// 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); }, 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) { + 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 formula = formulas[formulaKey]; + 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 = Math.round(calculatedValue * 1000) / 1000; + calculatedValue = calculatedValue.toString().replace(/(\.\d*?)0+$/, '$1'); + } else { + calculatedValue = calculatedValue.toFixed(0); + } + + 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: formula.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 () { + let 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) + }; + 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"); + // Clear error message after 2 seconds + setTimeout(() => setValue(''), 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() - 20 || 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(); + }; + } + + // 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 + " = " + calculatedVariable] = {}; + resultsMenu['Exit'] = function () { + Bangle.showClock(); + }; + E.showMenu(resultsMenu); + } + + clearTextArea(); + E.showMenu(mainMenu); + +})(); diff --git a/apps/ohmcalc/app.png b/apps/ohmcalc/app.png new file mode 100644 index 0000000000000000000000000000000000000000..65d6c2bf2076b7e692e26259f617c01b76fad0a7 GIT binary patch literal 2098 zcmV-22+jA2P)ATK5s4{Aq^rCB(0@ja{@PX1KkkoJ zndvdlx%W=*y>n+erA6!RzR5|Z@5gzcbMAT1d(L@F^#A5)bBL&eh#pBn9p;>I*`lPh zABl+5k};=(#$kH^083waoTq)T&yz4_3WB-guI*LXfmkZ=7O5OPMnrpl3xGG0Pklj3 z{ey_=J;vYiSCz$w6Tm++631MR_4UHrr#_dx#<(?+rp5e~W$~=3!5DrMQ6~|p#0;PM zDyOl|FqM}dWnZ?QuzUPQAI2~Tb8Wm85p4!f@b*Vb<0k*6u>yY!7zXA{k4O4oBAP-(6ELTs^?Ct0W#2Mkgzy`E7%OPKj!7eY1wRo5Nz;yFJiB4; zVC0>rx%}N{Sg(yj2+V026V2;274h;@yB(%b)Y-z?gseCz^)Dhi01%<4?sW*z|JRK| zb^(IjYppM{4r{>!yzIQ8q(IG#QGbb);4KRGZDD_Pc}&al4d$-6d~n)8k0HQ81GJsc zhq039HYNJ9N{eIQ;l+EjLfmM8pWy&s_Qnp@qf(!a}?f2N0XS6t3|v{d$R2{C92F@l5bQ=j9&y1@s^Lagf(M0+{c z)TR|8Qz}2xBfP*9y!A*ZNeDUEKJ{7V0x0znJc6Ym5Hzn}v#~Z>xx5A* zNKC;FX7aB4IA{3+_T}XyodNWL$K>7Zjy1upcL*==v=oDR*0^{)!-TxK zKo92*xlbn%xQz2u4{18uXSjTm(2f2RZYeMtyI>6H#S~-wkY?#WtD2I=A zIX#-v4AZ>2mdoGYo9H(cu$j)IDblnPaC6}ox)SyZv$1Qw__0=20Y6_<80NZlhq&oz zjGOyd=%(IpdDZ^!5`Q-xIVXbYlS)|WIu~zmb9@Q#w$2p<)cjfSLMI#-<_KFcDfPYG zlOv4@XtzZ~M%Ojuq zXU@`O0bn-JJixCx{8JLhh3zlO=MPmkB#nWvCRnvdzJ&FK?S!*LO8uLNCT0pYYKFOc zCD=l|g2SWKV9CLbqzz#6FdMh#?b=T#fMsHO!4%eOtCIMa9D3L~Z!t4=ddtN2fLA^- z$4W+2wjT3WMdAp$5Zi>GPrfPPZ7>%$?*+`#e!!oVR)FR{mIIEtlb^XiX`j%}dn9&( z1t8KzxNb{|F0~8LIb1U0{=D!u_5;iSFJ2fOR)D4>=fpeZUUx-Cd)tyCU{17d zblN9Qefd$Iyl1@-0try9biiT)gBi z_T}gCw1Hm3+r76(_O$i~7CSamfbO&c*yDLn;ri8ykV4F-^`N)*OJ{L))l`muv!7Qk zo8y@mFW>%&%Z&C*f7$UTWDo4+xCFEHkqdyoZ)>`OPyXdW`CYKii1kYj8_{Ha$LlGz z|KhW4%x4eqcb`5j>p>^}yq=2+b45T1YF}5s;Z<1|KH4dJU@!cQb->_-?Ysp*Y$F0} zd@V`)1*NmNs4!2!TEHO$gn>|42O-^pFWT^mDZu8j6JR(Kg7>h4ShnqBPji78!JJ@L zUfOSl3EgRcDtmJbO(^LQpLQSO)+8V}p!NeR2l&cLInb%J-;6`Nc&+mgpC0)k9+4YG zvUQ%7k1*O%V+Fx_zXZGh6NkTIkH#&RhA`2K7+0yyOMP}Ni^CD|c z>51F|;4(|6{ma(&roPCYG%vEoIw{BCk~J@~|F|zQGOuUPP{PV*9ihbBs6>?r%+d)U z4j_g0|Jd`jT!@~b#Iuf2;!7erZWhGuC!*;z>R_&K3oAV!QKeTfXBWUr`)i*2k&7x9 z3Xe3w+z*0b6fJ!?y{*m2%&HbiVCDviP#R%{Tl>&_u<{SG)}9l1gwtn0gN;blZekdN&BJWu>upby(DZFF>Q0*W_W33>nWvP;x>%oAX+)qC zgA&^yJirYL0%*8OcpFJ6YpWb)Q-st}GgHc|<_)Ekn{Kr{4zSRLPwIEKi#zm9%uP<} z^YdiP?QMd?CH0N-t)zbOuUJ;;HOeYSJB5t0ig8J~DZgdu1t_cd)MuWBtw@Li7r3w$ zD03FJfB{&@r#`)4rA4-rdgw?A;4 z3(U4Za4g8^f#dGm>xJwBV2Kj{pDw07*qoM6N<$f@2pO;Q#;t literal 0 HcmV?d00001 diff --git a/apps/ohmcalc/metadata.json b/apps/ohmcalc/metadata.json new file mode 100644 index 000000000..1e1985a8b --- /dev/null +++ b/apps/ohmcalc/metadata.json @@ -0,0 +1,22 @@ +{ + "id": "ohmcalc", + "name": "Ohm's Law Calculator", + "version": "0.01", + "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 + } + ] +} diff --git a/apps/ohmcalc/ohmcalc.js b/apps/ohmcalc/ohmcalc.js new file mode 100644 index 000000000..d3dc25cfd --- /dev/null +++ b/apps/ohmcalc/ohmcalc.js @@ -0,0 +1,300 @@ +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)': "{0} * {1}", + 'Power (P), Current (I)': "{0} / {1}", + 'Power (P), Resistance (R)': "Math.sqrt({0} * {1})" + }, + 'Current (I)': { + 'Voltage (V), Resistance (R)': "{0} / {1}", + 'Power (P), Voltage (V)': "{0} / {1}", + 'Power (P), Resistance (R)': "Math.sqrt({0} / {1})" + }, + 'Resistance (R)': { + 'Voltage (V), Current (I)': "{0} / {1}", + 'Power (P), Current (I)': "{0} / (Math.pow({1}, 2))", + 'Power (P), Voltage (V)': "(Math.pow({0}, 2)) / {1}" + }, + 'Power (P)': { + 'Voltage (V), Current (I)': "{0} * {1}", + 'Current (I), Resistance (R)': "(Math.pow({0}, 2)) * {1}", + 'Voltage (V), Resistance (R)': "(Math.pow({0}, 2)) / {1}" + }, +}; + +// 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 = ""; + +// Function references +let handleEnter; +let showVariableSelectionMenu; +let showInputMenu; +let resultsMenu; + +// 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); }, 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) { + 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 formula = formulas[formulaKey]; + 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 = Math.round(calculatedValue * 1000) / 1000; + calculatedValue = calculatedValue.toString().replace(/(\.\d*?)0+$/, '$1'); + } else { + calculatedValue = calculatedValue.toFixed(0); + } + + 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: formula.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 () { + let 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) + }; + 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"); + // Clear error message after 2 seconds + setTimeout(() => setValue(''), 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 drawValueScreenpage + (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() - 20 || 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(); + }; + } + + // 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 + " = " + calculatedVariable] = {}; + resultsMenu['Exit'] = function () { + Bangle.showClock(); + }; + E.showMenu(resultsMenu); + } + + clearTextArea(); + E.showMenu(mainMenu); + +})(); \ No newline at end of file From c4fd9ca09612c2a0d73949cb8a5156c444ebf1db Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Fri, 2 Jun 2023 18:15:55 -0500 Subject: [PATCH 39/64] Delete ohmcalc.js --- apps/ohmcalc/ohmcalc.js | 300 ---------------------------------------- 1 file changed, 300 deletions(-) delete mode 100644 apps/ohmcalc/ohmcalc.js diff --git a/apps/ohmcalc/ohmcalc.js b/apps/ohmcalc/ohmcalc.js deleted file mode 100644 index d3dc25cfd..000000000 --- a/apps/ohmcalc/ohmcalc.js +++ /dev/null @@ -1,300 +0,0 @@ -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)': "{0} * {1}", - 'Power (P), Current (I)': "{0} / {1}", - 'Power (P), Resistance (R)': "Math.sqrt({0} * {1})" - }, - 'Current (I)': { - 'Voltage (V), Resistance (R)': "{0} / {1}", - 'Power (P), Voltage (V)': "{0} / {1}", - 'Power (P), Resistance (R)': "Math.sqrt({0} / {1})" - }, - 'Resistance (R)': { - 'Voltage (V), Current (I)': "{0} / {1}", - 'Power (P), Current (I)': "{0} / (Math.pow({1}, 2))", - 'Power (P), Voltage (V)': "(Math.pow({0}, 2)) / {1}" - }, - 'Power (P)': { - 'Voltage (V), Current (I)': "{0} * {1}", - 'Current (I), Resistance (R)': "(Math.pow({0}, 2)) * {1}", - 'Voltage (V), Resistance (R)': "(Math.pow({0}, 2)) / {1}" - }, -}; - -// 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 = ""; - -// Function references -let handleEnter; -let showVariableSelectionMenu; -let showInputMenu; -let resultsMenu; - -// 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); }, 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) { - 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 formula = formulas[formulaKey]; - 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 = Math.round(calculatedValue * 1000) / 1000; - calculatedValue = calculatedValue.toString().replace(/(\.\d*?)0+$/, '$1'); - } else { - calculatedValue = calculatedValue.toFixed(0); - } - - 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: formula.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 () { - let 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) - }; - 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"); - // Clear error message after 2 seconds - setTimeout(() => setValue(''), 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 drawValueScreenpage - (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() - 20 || 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(); - }; - } - - // 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 + " = " + calculatedVariable] = {}; - resultsMenu['Exit'] = function () { - Bangle.showClock(); - }; - E.showMenu(resultsMenu); - } - - clearTextArea(); - E.showMenu(mainMenu); - -})(); \ No newline at end of file From 1ff9ae7271a21cb1086d1fd0cb29504c78a4a653 Mon Sep 17 00:00:00 2001 From: stweedo Date: Fri, 2 Jun 2023 20:55:37 -0500 Subject: [PATCH 40/64] Fixed incorrect formula --- apps/ohmcalc/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ohmcalc/app.js b/apps/ohmcalc/app.js index 28f3eb9d5..1fb157b5c 100644 --- a/apps/ohmcalc/app.js +++ b/apps/ohmcalc/app.js @@ -21,7 +21,7 @@ const FORMULAS = { 'Resistance (R)': { 'Voltage (V), Current (I)': "{0} / {1}", 'Power (P), Current (I)': "{0} / (Math.pow({1}, 2))", - 'Power (P), Voltage (V)': "(Math.pow({0}, 2)) / {1}" + 'Power (P), Voltage (V)': "(Math.pow({1}, 2)) / {0}" }, 'Power (P)': { 'Voltage (V), Current (I)': "{0} * {1}", From c562097aba5e334d47d3866132bc20664243a489 Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Sat, 3 Jun 2023 11:21:56 -0500 Subject: [PATCH 41/64] Update app.js - New results menu item --- apps/ohmcalc/app.js | 98 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 83 insertions(+), 15 deletions(-) diff --git a/apps/ohmcalc/app.js b/apps/ohmcalc/app.js index 1fb157b5c..9cc7b86c8 100644 --- a/apps/ohmcalc/app.js +++ b/apps/ohmcalc/app.js @@ -7,26 +7,27 @@ const UNITS = { "Resistance (R)": "Ohms", "Power (P)": "Watts", }; + const FORMULAS = { 'Voltage (V)': { - 'Current (I), Resistance (R)': "{0} * {1}", - 'Power (P), Current (I)': "{0} / {1}", - 'Power (P), Resistance (R)': "Math.sqrt({0} * {1})" + '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)': "{0} / {1}", - 'Power (P), Voltage (V)': "{0} / {1}", - 'Power (P), Resistance (R)': "Math.sqrt({0} / {1})" + '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)': "{0} / {1}", - 'Power (P), Current (I)': "{0} / (Math.pow({1}, 2))", - 'Power (P), Voltage (V)': "(Math.pow({1}, 2)) / {0}" + '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)': "{0} * {1}", - 'Current (I), Resistance (R)': "(Math.pow({0}, 2)) * {1}", - 'Voltage (V), Resistance (R)': "(Math.pow({0}, 2)) / {1}" + '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" } }, }; @@ -125,7 +126,9 @@ function calculateValue(calculatedVariable, variableValues) { let formulaKey = formulaKeys[i]; let variables = formulaKey.split(', '); if (variables.every(variable => variableValues.hasOwnProperty(variable))) { - let formula = formulas[formulaKey]; + 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])); @@ -147,7 +150,7 @@ function calculateValue(calculatedVariable, variableValues) { }); result.push([calculatedVariable, `${calculatedValue} ${formatUnit(UNITS[calculatedVariable], calculatedValue.toString())}`]); return { - formula: formula.replace(/\{(\d+)\}/g, (_, index) => formulaValues[index]), + formula: formulaDisplay.replace(/\{(\d+)\}/g, (_, index) => formulaValues[index]), value: calculatedValue, unit: formatUnit(UNITS[calculatedVariable], calculatedValue), result: result, @@ -273,6 +276,70 @@ function calculateValue(calculatedVariable, variableValues) { }; } + // Function to display the results screen with the values from the result.result array + function drawResultScreen(result) { + let drawPage = function() { + clearScreen(); + let fontSize = 30; // Adjust to fit the display + 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 + let valueFontSize = fontSize; + + // Draw value with smaller font size if necessary + g.setFontVector(valueFontSize); // Large font for value + if (g.stringWidth(resultValue) > g.getWidth()) { + valueFontSize /= 1.5; // Small font for value + g.setFontVector(valueFontSize); + if (g.stringWidth(resultValue) > g.getWidth()) { + valueFontSize /= 1.5; // Smallest font for value + g.setFontVector(valueFontSize); + } + } + + 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 () { @@ -286,8 +353,9 @@ function calculateValue(calculatedVariable, variableValues) { E.showMenu(mainMenu); }, }; + console.log(result); resultsMenu[result.value + ' ' + result.unit] = drawValueScreen(result); - resultsMenu[result.formula + " = " + calculatedVariable] = {}; + resultsMenu[result.formula] = drawResultScreen(result); resultsMenu['Exit'] = function () { Bangle.showClock(); }; From b0ad5522ce3830deb93f800f11db4ef8d0318c93 Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Sat, 3 Jun 2023 11:22:55 -0500 Subject: [PATCH 42/64] Update ChangeLog --- apps/ohmcalc/ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/ohmcalc/ChangeLog b/apps/ohmcalc/ChangeLog index 5560f00bc..68790afee 100644 --- a/apps/ohmcalc/ChangeLog +++ b/apps/ohmcalc/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: New Results menu item to show the formula and values used. From 8baafd0809590fa67f254d1bf656937e9fac7145 Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Sat, 3 Jun 2023 11:23:09 -0500 Subject: [PATCH 43/64] Update metadata.json --- apps/ohmcalc/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ohmcalc/metadata.json b/apps/ohmcalc/metadata.json index 1e1985a8b..822203024 100644 --- a/apps/ohmcalc/metadata.json +++ b/apps/ohmcalc/metadata.json @@ -1,7 +1,7 @@ { "id": "ohmcalc", "name": "Ohm's Law Calculator", - "version": "0.01", + "version": "0.02", "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", From acd6c4aa39137e0fa6311a6f9a826b45cb753dd8 Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Sat, 3 Jun 2023 11:30:49 -0500 Subject: [PATCH 44/64] Update metadata.json - shorten launcher name --- apps/ohmcalc/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ohmcalc/metadata.json b/apps/ohmcalc/metadata.json index 822203024..51a21b9d5 100644 --- a/apps/ohmcalc/metadata.json +++ b/apps/ohmcalc/metadata.json @@ -1,6 +1,6 @@ { "id": "ohmcalc", - "name": "Ohm's Law Calculator", + "name": "Ohm's Law Calc", "version": "0.02", "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", From feafb53854d736ff189d93c39dd1fd4bc0f6c1cc Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Sat, 3 Jun 2023 11:34:40 -0500 Subject: [PATCH 45/64] Update metadata.json - Added shortened name --- apps/ohmcalc/metadata.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/ohmcalc/metadata.json b/apps/ohmcalc/metadata.json index 51a21b9d5..1272b9f91 100644 --- a/apps/ohmcalc/metadata.json +++ b/apps/ohmcalc/metadata.json @@ -1,6 +1,7 @@ { "id": "ohmcalc", - "name": "Ohm's Law Calc", + "name": "Ohm's Law Calculator", + "shortName": "Ohm's Law Calc", "version": "0.02", "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", From 79c98c2eac081a64302fee7c5f00f812347b1fab Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Sat, 3 Jun 2023 12:42:40 -0500 Subject: [PATCH 46/64] Update app.js - Fixed back button bug and disallow input during message --- apps/ohmcalc/app.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/ohmcalc/app.js b/apps/ohmcalc/app.js index 9cc7b86c8..3bd225baf 100644 --- a/apps/ohmcalc/app.js +++ b/apps/ohmcalc/app.js @@ -43,6 +43,7 @@ let calculatedVariable; let selectedVariable; let variableValues = {}; let inputStr = ""; +let invalidInput = false; // Function references let handleEnter; @@ -108,6 +109,9 @@ function setValue(newStr) { // Function to handle the press of a button and append its value to the current input function handleButtonPress(value) { + if (invalidInput) { + return; // Don't allow input if an invalid input error message is displayed + } inputStr = value === 'C' ? '' : inputStr + value; setValue(inputStr); } @@ -176,7 +180,10 @@ function calculateValue(calculatedVariable, variableValues) { clearScreen(); let variableSelectionMenu = { '': { 'title': 'Select Variable' }, - '< Back': () => E.showMenu(mainMenu) + '< Back': () => { + E.showMenu(mainMenu); + variableValues = {}; + } }; let variables = Object.keys(UNITS); let remainingVariables = variables.filter(v => v !== calculatedVariable && !variableValues[v]); @@ -208,8 +215,12 @@ function calculateValue(calculatedVariable, variableValues) { 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(''), 2000); + setTimeout(() => { + setValue(''); + invalidInput = false; // Allow input again + }, 2000); return; } From 0c5d339e26ed27f53253ca37c2dae7fd2405af9b Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Sat, 3 Jun 2023 15:17:49 -0500 Subject: [PATCH 47/64] Update app.js- remove console log --- apps/ohmcalc/app.js | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/ohmcalc/app.js b/apps/ohmcalc/app.js index 3bd225baf..4a3d97e8c 100644 --- a/apps/ohmcalc/app.js +++ b/apps/ohmcalc/app.js @@ -364,7 +364,6 @@ function calculateValue(calculatedVariable, variableValues) { E.showMenu(mainMenu); }, }; - console.log(result); resultsMenu[result.value + ' ' + result.unit] = drawValueScreen(result); resultsMenu[result.formula] = drawResultScreen(result); resultsMenu['Exit'] = function () { From 225b4be09de364f32ca79f9876adfc74b52af9fa Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Sat, 3 Jun 2023 21:11:55 -0500 Subject: [PATCH 48/64] Update README.md --- apps/ohmcalc/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ohmcalc/README.md b/apps/ohmcalc/README.md index f45b8e977..a890e590a 100644 --- a/apps/ohmcalc/README.md +++ b/apps/ohmcalc/README.md @@ -7,7 +7,7 @@ This is a simple and intuitive Ohm's Law Calculator application designed for the * __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 'Back' button is always available to help you step back through the app. In case you'd like to start afresh from the Results menu, just tap on 'Main Menu'. +* __Navigation:__ Even when you're deeply involved in entering calculations or examining 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 From ad0b24a597ce51a60ed358cc471db7bc5445f37e Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Sat, 3 Jun 2023 22:22:00 -0500 Subject: [PATCH 49/64] Update app.js - Adds haptics to Input screen and long press "C" to go back --- apps/ohmcalc/app.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/ohmcalc/app.js b/apps/ohmcalc/app.js index 4a3d97e8c..61ab4a0be 100644 --- a/apps/ohmcalc/app.js +++ b/apps/ohmcalc/app.js @@ -50,6 +50,7 @@ let handleEnter; let showVariableSelectionMenu; let showInputMenu; let resultsMenu; +let mainMenu; // Setup the layout for input buttons let layout = new Layout({ @@ -59,7 +60,7 @@ let layout = new Layout({ { 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); }, fillx: 1, filly: 1 })) }, + { type: "h", c: ".0C".split("").map(i => ({ type: "btn", font: "6x8:3", label: i, cb: () => { handleButtonPress(i); }, cbl: i === "C" ? () => { E.showMenu(mainMenu); 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 }); @@ -109,6 +110,7 @@ function setValue(newStr) { // 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 } @@ -165,7 +167,7 @@ function calculateValue(calculatedVariable, variableValues) { // Main function to initialize the application and setup the main menu (function () { - let mainMenu = { + mainMenu = { '': { 'title': 'Ohm\'s Law Calc' }, '< Back': () => Bangle.showClock() }; From dcf3a33a29181b59e8c0b24956dbff8538902f95 Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Sat, 3 Jun 2023 22:22:42 -0500 Subject: [PATCH 50/64] Update ChangeLog --- apps/ohmcalc/ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/ohmcalc/ChangeLog b/apps/ohmcalc/ChangeLog index 68790afee..af752b896 100644 --- a/apps/ohmcalc/ChangeLog +++ b/apps/ohmcalc/ChangeLog @@ -1,2 +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. From a0fcef0b92a1511ecd58db5055756fbb6efa49e7 Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Sat, 3 Jun 2023 22:22:57 -0500 Subject: [PATCH 51/64] Update metadata.json --- apps/ohmcalc/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ohmcalc/metadata.json b/apps/ohmcalc/metadata.json index 1272b9f91..de81ec6d7 100644 --- a/apps/ohmcalc/metadata.json +++ b/apps/ohmcalc/metadata.json @@ -2,7 +2,7 @@ "id": "ohmcalc", "name": "Ohm's Law Calculator", "shortName": "Ohm's Law Calc", - "version": "0.02", + "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", From 02a5f81a0efbbade9cfcdf39875224052893b9ee Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Sat, 3 Jun 2023 22:24:45 -0500 Subject: [PATCH 52/64] Update README.md --- apps/ohmcalc/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ohmcalc/README.md b/apps/ohmcalc/README.md index a890e590a..3add03736 100644 --- a/apps/ohmcalc/README.md +++ b/apps/ohmcalc/README.md @@ -7,7 +7,7 @@ This is a simple and intuitive Ohm's Law Calculator application designed for the * __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:__ Even when you're deeply involved in entering calculations or examining 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. +* __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 From ae174c6860d7b0376b8d6e4d1675b566793c6778 Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Sat, 3 Jun 2023 22:53:49 -0500 Subject: [PATCH 53/64] Update app.js - Fixes small rounding bug --- apps/ohmcalc/app.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/ohmcalc/app.js b/apps/ohmcalc/app.js index 61ab4a0be..b0a28854b 100644 --- a/apps/ohmcalc/app.js +++ b/apps/ohmcalc/app.js @@ -143,10 +143,7 @@ function calculateValue(calculatedVariable, variableValues) { // Round and trim long decimal numbers if (!isInteger) { - calculatedValue = Math.round(calculatedValue * 1000) / 1000; - calculatedValue = calculatedValue.toString().replace(/(\.\d*?)0+$/, '$1'); - } else { - calculatedValue = calculatedValue.toFixed(0); + calculatedValue = calculatedValue.toFixed(3); } let result = Object.entries(variableValues).map(function (entry) { From 592b6777889c5e5c47848513e885deb8dbed6d8f Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Sun, 4 Jun 2023 00:53:14 -0500 Subject: [PATCH 54/64] Update app.js - Fixed a small rounding error --- apps/ohmcalc/app.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/ohmcalc/app.js b/apps/ohmcalc/app.js index b0a28854b..63829d42a 100644 --- a/apps/ohmcalc/app.js +++ b/apps/ohmcalc/app.js @@ -144,6 +144,7 @@ function calculateValue(calculatedVariable, variableValues) { // 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) { From caec426589b53898da8704b3a2a9cb53a30ed65f Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Sun, 4 Jun 2023 01:10:15 -0500 Subject: [PATCH 55/64] Update app.js - Fixes "C" back button --- apps/ohmcalc/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ohmcalc/app.js b/apps/ohmcalc/app.js index 63829d42a..4101fba11 100644 --- a/apps/ohmcalc/app.js +++ b/apps/ohmcalc/app.js @@ -60,7 +60,7 @@ let layout = new Layout({ { 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" ? () => { E.showMenu(mainMenu); Bangle.buzz(20); } : undefined, 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 }); From bbbd200e2fcb2564015792747334bbc8590b580d Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Sun, 4 Jun 2023 01:25:21 -0500 Subject: [PATCH 56/64] Update app.js - Dynamically adjust Results screen font size --- apps/ohmcalc/app.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/apps/ohmcalc/app.js b/apps/ohmcalc/app.js index 4101fba11..fc6bda81c 100644 --- a/apps/ohmcalc/app.js +++ b/apps/ohmcalc/app.js @@ -291,7 +291,7 @@ function calculateValue(calculatedVariable, variableValues) { function drawResultScreen(result) { let drawPage = function() { clearScreen(); - let fontSize = 30; // Adjust to fit the display + let fontSize = 30; // Initial font size let lineSpacing = 15; // Space between lines // Define the vertical positions of the titles @@ -313,17 +313,13 @@ function calculateValue(calculatedVariable, variableValues) { let valueX; let valueY = titleY + g.getFontHeight(); // Draw below the title - let valueFontSize = fontSize; - // Draw value with smaller font size if necessary - g.setFontVector(valueFontSize); // Large font for value - if (g.stringWidth(resultValue) > g.getWidth()) { - valueFontSize /= 1.5; // Small font for value + // Calculate the font size for value dynamically + let valueFontSize = fontSize; // Initialize with maximum possible font size + g.setFontVector(valueFontSize); + while (g.stringWidth(resultValue) > g.getWidth() && valueFontSize > 1) { + valueFontSize *= 0.9; // Reduce the font size by 10% g.setFontVector(valueFontSize); - if (g.stringWidth(resultValue) > g.getWidth()) { - valueFontSize /= 1.5; // Smallest font for value - g.setFontVector(valueFontSize); - } } valueY += g.getFontHeight() / 2 + 2; From 38711975540d7957275cde1aec19657dbde97fdf Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Sun, 4 Jun 2023 02:02:42 -0500 Subject: [PATCH 57/64] Update app.js - Minor font sizing changes --- apps/ohmcalc/app.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/ohmcalc/app.js b/apps/ohmcalc/app.js index fc6bda81c..600c12431 100644 --- a/apps/ohmcalc/app.js +++ b/apps/ohmcalc/app.js @@ -256,7 +256,7 @@ function calculateValue(calculatedVariable, variableValues) { g.setFontVector(fontSize); // Reduce the font size until both the value and unit fit on the screen - while (g.stringWidth(result.value) > g.getWidth() - 20 || g.getFontHeight() > g.getHeight() / 2) { + while (g.stringWidth(result.value) > g.getWidth() - 10 || g.getFontHeight() > g.getHeight() / 2) { fontSize--; g.setFontVector(fontSize); } @@ -317,8 +317,8 @@ function calculateValue(calculatedVariable, variableValues) { // Calculate the font size for value dynamically let valueFontSize = fontSize; // Initialize with maximum possible font size g.setFontVector(valueFontSize); - while (g.stringWidth(resultValue) > g.getWidth() && valueFontSize > 1) { - valueFontSize *= 0.9; // Reduce the font size by 10% + while (g.stringWidth(resultValue) > g.getWidth() - 10) { + valueFontSize--; // Reduce the font size by 1 g.setFontVector(valueFontSize); } From e480f21b288c7b0a9fec32243bdf16e729b4e94f Mon Sep 17 00:00:00 2001 From: stweedo <108593831+stweedo@users.noreply.github.com> Date: Sun, 4 Jun 2023 02:17:10 -0500 Subject: [PATCH 58/64] Update app.js - Remove unnecessary variable --- apps/ohmcalc/app.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/ohmcalc/app.js b/apps/ohmcalc/app.js index 600c12431..aa470401c 100644 --- a/apps/ohmcalc/app.js +++ b/apps/ohmcalc/app.js @@ -315,11 +315,10 @@ function calculateValue(calculatedVariable, variableValues) { let valueY = titleY + g.getFontHeight(); // Draw below the title // Calculate the font size for value dynamically - let valueFontSize = fontSize; // Initialize with maximum possible font size - g.setFontVector(valueFontSize); + g.setFontVector(fontSize); while (g.stringWidth(resultValue) > g.getWidth() - 10) { - valueFontSize--; // Reduce the font size by 1 - g.setFontVector(valueFontSize); + fontSize--; // Reduce the font size by 1 + g.setFontVector(fontSize); } valueY += g.getFontHeight() / 2 + 2; From 1a82f314cefd3fef8c8152f54aedce98a878d226 Mon Sep 17 00:00:00 2001 From: novadawn999 Date: Sun, 4 Jun 2023 15:38:28 -0500 Subject: [PATCH 59/64] added 13 new chords support for fret offsets more formatting refinements --- apps/guitar/ChangeLog | 1 + apps/guitar/app.js | 241 +++++++++++++++++++++++++++++++++----- apps/guitar/metadata.json | 2 +- 3 files changed, 213 insertions(+), 31 deletions(-) diff --git a/apps/guitar/ChangeLog b/apps/guitar/ChangeLog index 5560f00bc..22c67383d 100644 --- a/apps/guitar/ChangeLog +++ b/apps/guitar/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: More Chords, formatting, fret offset support. \ No newline at end of file diff --git a/apps/guitar/app.js b/apps/guitar/app.js index d48d9009a..6c172f920 100644 --- a/apps/guitar/app.js +++ b/apps/guitar/app.js @@ -2,10 +2,9 @@ const stringInterval = 24; const stringLength = 138; const fretHeight = 35; const fingerOffset = 17; -const x = 30; -const y = 32; +const xOffset = 26; +const yOffset = 34; -//chords const cc = [ "C", "0X", @@ -13,7 +12,8 @@ const cc = [ "22", "x", "11", - "x" + "x", + "0" ]; const dd = [ @@ -23,7 +23,8 @@ const dd = [ "x", "21", "33", - "22" + "22", + "0" ]; const gg = [ @@ -33,16 +34,19 @@ const gg = [ "x", "x", "x", - "33" + "33", + "0" ]; const am = [ "Am", "0x", "x", - "22", "23", - "11" + "22", + "11", + "x", + "0" ]; const em = [ @@ -52,7 +56,8 @@ const em = [ "23", "x", "x", - "x" + "x", + "0" ]; const aa = [ @@ -62,17 +67,8 @@ const aa = [ "21", "22", "23", - "x" -]; - -const ff = [ - "F", - "0X", - "33", - "34", - "22", - "11", - "11" + "x", + "0" ]; var ee = [ @@ -82,9 +78,177 @@ var ee = [ "23", "11", "x", - "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 = { @@ -99,6 +263,20 @@ var menu = { "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", { @@ -112,26 +290,29 @@ var menu = { function drawBase() { for (let i = 0; i < 6; i++) { - g.drawLine(x + i * stringInterval, y, x + i * stringInterval, y + stringLength); - g.fillRect(x- 1, y + i * fretHeight - 1, x + stringInterval * 5 + 1, y + i * fretHeight + 1); + 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 - 3, 18); - for (let i = 0; i < chord.length; i++) { + 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], 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); + 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], 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); + 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() { @@ -156,4 +337,4 @@ function main() { }, BTN); } -main(); +main(); \ No newline at end of file diff --git a/apps/guitar/metadata.json b/apps/guitar/metadata.json index 8f98ce44e..6ab3ffc51 100644 --- a/apps/guitar/metadata.json +++ b/apps/guitar/metadata.json @@ -1,7 +1,7 @@ { "id": "guitar", "name": "Guitar Chords", "shortName":"Guitar", - "version":"0.01", + "version":"0.02", "description": "Wrist mounted guitar chords", "icon": "app.png", "tags": "guitar, chords", From e21fc018b43367e9d5dbfaa872cb6ce656e1b5cd Mon Sep 17 00:00:00 2001 From: novadawn999 Date: Sun, 4 Jun 2023 16:26:35 -0500 Subject: [PATCH 60/64] added 13 more chords --- apps/Uke/ChangeLog | 1 + apps/Uke/app.js | 235 ++++++++++++++++++++++++++++++----------- apps/Uke/metadata.json | 2 +- 3 files changed, 178 insertions(+), 60 deletions(-) diff --git a/apps/Uke/ChangeLog b/apps/Uke/ChangeLog index 366158b0e..ef5ffa3fe 100644 --- a/apps/Uke/ChangeLog +++ b/apps/Uke/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Increased Legibility, GUI rework +0.03: 13 new chords diff --git a/apps/Uke/app.js b/apps/Uke/app.js index 3c28381c0..411a39b70 100644 --- a/apps/Uke/app.js +++ b/apps/Uke/app.js @@ -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,27 +30,184 @@ 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); }, - "Em" : function() { draw(em); }, - "A" : function() { draw(aa); }, - "Am" : function() { draw(am); }, - "F" : function() { draw(ff); }, - "G" : function() { draw(gg); }, - "About" : function() { - E.showMessage( - "Created By:\nNovaDawn999", { - title:"About" - } - ); - } + "" : { "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" + } + ); + } }; diff --git a/apps/Uke/metadata.json b/apps/Uke/metadata.json index 8d92718c3..ef31e3663 100644 --- a/apps/Uke/metadata.json +++ b/apps/Uke/metadata.json @@ -1,7 +1,7 @@ { "id": "Uke", "name": "Uke Chords", "shortName":"Uke", - "version":"0.02", + "version":"0.03", "description": "Wrist mounted ukulele chords", "icon": "app.png", "tags": "uke, chords", From 14a4dfcd968aab7bd30b5934a380257cf93329fb Mon Sep 17 00:00:00 2001 From: novadawn999 Date: Sun, 4 Jun 2023 16:45:23 -0500 Subject: [PATCH 61/64] bugfix --- apps/Uke/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/Uke/app.js b/apps/Uke/app.js index 411a39b70..095477f3f 100644 --- a/apps/Uke/app.js +++ b/apps/Uke/app.js @@ -220,7 +220,7 @@ function drawBase() { } function drawChord(chord) { - g.drawString(chord[0], g.getWidth() * 0.5 - 3, 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; From b4abf8571cba7cdb87bf417014d650a2ae94329a Mon Sep 17 00:00:00 2001 From: stweedo Date: Sun, 4 Jun 2023 20:43:19 -0500 Subject: [PATCH 62/64] [rescalc] - update to v0.02 --- apps/rescalc/ChangeLog | 1 + apps/rescalc/app.js | 224 ++++++++++++++----------------------- apps/rescalc/metadata.json | 2 +- 3 files changed, 83 insertions(+), 144 deletions(-) diff --git a/apps/rescalc/ChangeLog b/apps/rescalc/ChangeLog index 5560f00bc..43f41aa76 100644 --- a/apps/rescalc/ChangeLog +++ b/apps/rescalc/ChangeLog @@ -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. \ No newline at end of file diff --git a/apps/rescalc/app.js b/apps/rescalc/app.js index cfb88e015..1986ac2db 100644 --- a/apps/rescalc/app.js +++ b/apps/rescalc/app.js @@ -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,20 +35,26 @@ 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 resistanceStr = resistance.toString(); + let firstDigit, secondDigit, multiplier; + if (resistanceStr.length === 1) { // Check if resistance is a single digit + firstDigit = 0; + secondDigit = Number(resistanceStr.charAt(0)); + multiplier = 0; + } else if (resistance >= 100) { + // Extract the first two digits from the resistance value + firstDigit = Number(resistanceStr.charAt(0)); + secondDigit = Number(resistanceStr.charAt(1)); + // Calculate the multiplier + multiplier = resistanceStr.length - 2; + } else { + // For values between 10-99, shift the color to the first band + firstDigit = Number(resistanceStr.charAt(0)); + secondDigit = Number(resistanceStr.charAt(1)); + multiplier = 0; + } + let firstBandEntry = Object.entries(colorData).find(function(entry) { return entry[1].value === firstDigit; }); @@ -121,8 +71,9 @@ function resistanceToColorBands(resistance, tolerance) { 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 +81,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 +116,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 +134,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() { + let colorBands; + let inputColorBands; let settings = { resistance: 0, tolerance: 0, @@ -244,6 +181,8 @@ function drawResistance(resistance, tolerance) { colorBands: ["", "", "", ""] }; settings = emptySettings; + colorBands = null; + inputColorBands = null; } function showColorBandMenu(bandNumber) { @@ -289,11 +228,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(); } @@ -339,8 +275,8 @@ function drawResistance(resistance, tolerance) { } }, 'Draw Resistor': function() { - let colorBands = settings.colorBands; - let values = colorBandsToResistance(colorBands); + inputColorBands = settings.colorBands; + let values = colorBandsToResistance(inputColorBands); settings.resistance = values[0]; settings.tolerance = values[1]; showDrawingMenu(); @@ -367,7 +303,6 @@ 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() { showMultiplierMenu(); @@ -406,7 +341,6 @@ 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() { showToleranceMenu(); @@ -415,13 +349,21 @@ function drawResistance(resistance, tolerance) { }; } } - 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(); @@ -469,25 +411,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() { clearScreen(); E.showMenu(mainMenu); }, }; - E.showMenu(drawingMenu); - let resistance = settings.resistance * (settings.multiplier || 1); let tolerance = settings.tolerance; drawResistorAndResistance(resistance, tolerance); @@ -508,4 +446,4 @@ function drawResistance(resistance, tolerance) { }, }; E.showMenu(mainMenu); -})(); +})(); \ No newline at end of file diff --git a/apps/rescalc/metadata.json b/apps/rescalc/metadata.json index c14bbd6e5..aaa5f2acf 100644 --- a/apps/rescalc/metadata.json +++ b/apps/rescalc/metadata.json @@ -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"}, From 2f0f5e8dd7dea229c211487075aa54fbd796ed4a Mon Sep 17 00:00:00 2001 From: stweedo Date: Mon, 5 Jun 2023 00:31:08 -0500 Subject: [PATCH 63/64] [rescalc] - bugfix with decimal values to colors --- apps/rescalc/app.js | 49 ++++++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/apps/rescalc/app.js b/apps/rescalc/app.js index 1986ac2db..6805589b2 100644 --- a/apps/rescalc/app.js +++ b/apps/rescalc/app.js @@ -36,25 +36,35 @@ function colorBandsToResistance(colorBands) { } function resistanceToColorBands(resistance, tolerance) { - let resistanceStr = resistance.toString(); let firstDigit, secondDigit, multiplier; - if (resistanceStr.length === 1) { // Check if resistance is a single digit - firstDigit = 0; - secondDigit = Number(resistanceStr.charAt(0)); - multiplier = 0; - } else if (resistance >= 100) { - // Extract the first two digits from the resistance value - firstDigit = Number(resistanceStr.charAt(0)); - secondDigit = Number(resistanceStr.charAt(1)); - // Calculate the multiplier - multiplier = resistanceStr.length - 2; + if (resistance < 1) { + // The resistance is less than 1, so we need to handle this case specially + let count = 0; + while (resistance < 1) { + resistance *= 10; + count++; + } + // Now, resistance is a whole number and count is how many times we had to multiply by 10 + let resistanceStr = resistance.toString(); + firstDigit = 0; // Set the first band color to be black + secondDigit = Number(resistanceStr.charAt(0)); // Set the second band color to be the significant digit + // Use count to determine the multiplier + multiplier = count === 1 ? 0.1 : 0.01; } else { - // For values between 10-99, shift the color to the first band - firstDigit = Number(resistanceStr.charAt(0)); - secondDigit = Number(resistanceStr.charAt(1)); - multiplier = 0; + // Convert the resistance to a string so we can manipulate it easily + let resistanceStr = resistance.toString(); + if (resistanceStr.length === 1) { // Check if resistance is a single digit + firstDigit = 0; + secondDigit = Number(resistanceStr.charAt(0)); + multiplier = 1; // Set multiplier to 1 for single digit resistance values + } else { + // Extract the first two digits from the resistance value + firstDigit = Number(resistanceStr.charAt(0)); + secondDigit = Number(resistanceStr.charAt(1)); + // Calculate the multiplier by matching it directly with the length of digits + multiplier = resistanceStr.length - 2 >= 0 ? Math.pow(10, resistanceStr.length - 2) : Math.pow(10, resistanceStr.length - 1); + } } - let firstBandEntry = Object.entries(colorData).find(function(entry) { return entry[1].value === firstDigit; }); @@ -64,7 +74,7 @@ function resistanceToColorBands(resistance, tolerance) { }); let secondBand = secondBandEntry ? secondBandEntry[1].hex : undefined; let multiplierBandEntry = Object.entries(colorData).find(function(entry) { - return entry[1].multiplier === Math.pow(10, multiplier); + return entry[1].multiplier === multiplier; }); let multiplierBand = multiplierBandEntry ? multiplierBandEntry[1].hex : undefined; let toleranceBandEntry = Object.entries(colorData).find(function(entry) { @@ -352,7 +362,7 @@ function drawResistance(resistance, tolerance) { E.showMenu(toleranceMenu); } - function drawResistorAndResistance(resistance, tolerance) { + function drawResistorAndResistance(resistance, tolerance) { if (inputColorBands) { colorBands = inputColorBands.map(color => { if (colorData.hasOwnProperty(color)) { @@ -362,8 +372,11 @@ function drawResistance(resistance, tolerance) { } }); } else { + console.log("Using colorBands = resistanceToColorBands(resistance, tolerance)" + "\nResistance: " + resistance + "\nTolerance: " + tolerance); colorBands = resistanceToColorBands(resistance, tolerance); } + console.log(inputColorBands); + console.log(colorBands); drawResistor(colorBands, tolerance); drawResistance(resistance, tolerance); resetSettings(); From b62d8baf75e13f77f9892db11cd533cad7ad2ac6 Mon Sep 17 00:00:00 2001 From: stweedo Date: Mon, 5 Jun 2023 00:34:44 -0500 Subject: [PATCH 64/64] [rescalc] - Remove console logs and formatting --- apps/rescalc/app.js | 57 +++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/apps/rescalc/app.js b/apps/rescalc/app.js index 6805589b2..566809837 100644 --- a/apps/rescalc/app.js +++ b/apps/rescalc/app.js @@ -65,19 +65,19 @@ function resistanceToColorBands(resistance, tolerance) { multiplier = resistanceStr.length - 2 >= 0 ? Math.pow(10, resistanceStr.length - 2) : Math.pow(10, resistanceStr.length - 1); } } - let firstBandEntry = Object.entries(colorData).find(function(entry) { + let firstBandEntry = Object.entries(colorData).find(function (entry) { return entry[1].value === firstDigit; }); let firstBand = firstBandEntry ? firstBandEntry[1].hex : undefined; - let secondBandEntry = Object.entries(colorData).find(function(entry) { + let secondBandEntry = Object.entries(colorData).find(function (entry) { return entry[1].value === secondDigit; }); let secondBand = secondBandEntry ? secondBandEntry[1].hex : undefined; - let multiplierBandEntry = Object.entries(colorData).find(function(entry) { + 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; @@ -175,7 +175,7 @@ function drawResistance(resistance, tolerance) { g.drawString(toleranceStr.padEnd(4), 176 - toleranceX, y); } -(function() { +(function () { let colorBands; let inputColorBands; let settings = { @@ -200,7 +200,7 @@ function drawResistance(resistance, tolerance) { '': { 'title': `Band ${bandNumber}` }, - '< Back': function() { + '< Back': function () { E.showMenu(colorEntryMenu); }, }; @@ -209,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); @@ -248,7 +248,7 @@ function drawResistance(resistance, tolerance) { '': { 'title': 'Band Color' }, - '< Back': function() { + '< Back': function () { clearScreen(); E.showMenu(mainMenu); }, @@ -284,7 +284,7 @@ function drawResistance(resistance, tolerance) { setTimeout(() => showColorBandMenu(4), 5); } }, - 'Draw Resistor': function() { + 'Draw Resistor': function () { inputColorBands = settings.colorBands; let values = colorBandsToResistance(inputColorBands); settings.resistance = values[0]; @@ -301,7 +301,7 @@ function drawResistance(resistance, tolerance) { '': { 'title': 'Multiplier' }, - '< Back': function() { + '< Back': function () { showResistanceEntryMenu(); } }; @@ -314,7 +314,7 @@ function drawResistance(resistance, tolerance) { multiplierMenu[`${formattedMultiplier}`] = () => { settings.multiplier = multiplierValue; // Update the value of 'Multiplier' in resistanceEntryMenu - resistanceEntryMenu["Multiplier"] = function() { + resistanceEntryMenu["Multiplier"] = function () { showMultiplierMenu(); }; showResistanceEntryMenu(); @@ -340,7 +340,7 @@ function drawResistance(resistance, tolerance) { '': { 'title': 'Tolerance' }, - '< Back': function() { + '< Back': function () { showResistanceEntryMenu(); } }; @@ -352,7 +352,7 @@ function drawResistance(resistance, tolerance) { toleranceMenu[`${tolerance}%`] = () => { settings.tolerance = tolerance; // Update the value of 'Tolerance (%)' in resistanceEntryMenu - resistanceEntryMenu["Tolerance (%)"] = function() { + resistanceEntryMenu["Tolerance (%)"] = function () { showToleranceMenu(); }; showResistanceEntryMenu(); @@ -362,7 +362,7 @@ function drawResistance(resistance, tolerance) { E.showMenu(toleranceMenu); } - function drawResistorAndResistance(resistance, tolerance) { + function drawResistorAndResistance(resistance, tolerance) { if (inputColorBands) { colorBands = inputColorBands.map(color => { if (colorData.hasOwnProperty(color)) { @@ -372,11 +372,8 @@ function drawResistance(resistance, tolerance) { } }); } else { - console.log("Using colorBands = resistanceToColorBands(resistance, tolerance)" + "\nResistance: " + resistance + "\nTolerance: " + tolerance); colorBands = resistanceToColorBands(resistance, tolerance); } - console.log(inputColorBands); - console.log(colorBands); drawResistor(colorBands, tolerance); drawResistance(resistance, tolerance); resetSettings(); @@ -386,7 +383,7 @@ function drawResistance(resistance, tolerance) { '': { 'title': 'Resistance' }, - '< Back': function() { + '< Back': function () { clearScreen(); E.showMenu(mainMenu); }, @@ -396,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(); } }; @@ -433,7 +430,7 @@ function drawResistance(resistance, tolerance) { '': { 'title': '' }, - '< Back': function() { + '< Back': function () { clearScreen(); E.showMenu(mainMenu); }, @@ -449,11 +446,11 @@ function drawResistance(resistance, tolerance) { 'title': 'Resistor Calc' }, '< Back': () => Bangle.showClock(), // return to the clock app - 'Resistance': function() { + 'Resistance': function () { resetSettings(); showResistanceEntryMenu(); }, - 'Colors': function() { + 'Colors': function () { resetSettings(); showColorEntryMenu(); },