diff --git a/apps/bwclk/ChangeLog b/apps/bwclk/ChangeLog index 74b80b73b..84ca21d20 100644 --- a/apps/bwclk/ChangeLog +++ b/apps/bwclk/ChangeLog @@ -34,3 +34,4 @@ clkinfo.addInteractive that would cause ReferenceError. 0.32: Make the border of the clock_info box extend all the way to the right of the screen. 0.33: Fix issue rendering ClockInfos with for fg+bg color set to the same (#2749) 0.34: Support 12-hour time format +0.35: Adjust clock info positions to better fit long text diff --git a/apps/bwclk/app.js b/apps/bwclk/app.js index 5053dafbb..252cffa9b 100644 --- a/apps/bwclk/app.js +++ b/apps/bwclk/app.js @@ -141,7 +141,7 @@ let clockInfoMenu = clock_info.addInteractive(clockInfoItems, { var hideClkInfo = info.text == null; g.reset().setBgColor(g.theme.fg).clearRect(options.x, options.y, options.x+options.w, options.y+options.h); - g.setFontAlign(0,0).setColor(g.theme.bg); + g.setFontAlign(-1,-1).setColor(g.theme.bg); if (options.focus){ var y = hideClkInfo ? options.y+20 : options.y+2; @@ -157,26 +157,36 @@ let clockInfoMenu = clock_info.addInteractive(clockInfoItems, { return; } - // Set text and font + // Set text and font, compute sizes. var image = info.img; + var imgWidth = image == null ? 0 : 24; + let imgWidthClear = parseInt(imgWidth*1.3); var text = String(info.text); + let strWidth; if(text.split('\n').length > 1){ g.setMiniFont(); + strWidth = g.stringWidth(text); } else { g.setSmallFont(); + strWidth = g.stringWidth(text); + if (strWidth+imgWidthClear > options.w) { + g.setMiniFont(); + text = g.wrapString(text, options.w-imgWidthClear).join("\n"); + strWidth = g.stringWidth(text); + } } - // Compute sizes - var strWidth = g.stringWidth(text); - var imgWidth = image == null ? 0 : 24; + // Compute positions var midx = options.x+options.w/2; + let imgPosX = Math.max(midx-Math.floor(imgWidthClear/2)-parseInt(strWidth/2), 0); + let strPosX = imgPosX+imgWidthClear; // Draw if (image) { var scale = imgWidth / image.width; - g.drawImage(image, midx-parseInt(imgWidth*1.3/2)-parseInt(strWidth/2), options.y+6, {scale: scale}); + g.drawImage(image, imgPosX, options.y+6, {scale: scale}); } - g.drawString(text, midx+parseInt(imgWidth*1.3/2), options.y+20); + g.drawString(text, strPosX, options.y+6); // In case we are in focus and the focus box changes (fullscreen yes/no) // we draw the time again. Otherwise it could happen that a while line is diff --git a/apps/bwclk/metadata.json b/apps/bwclk/metadata.json index de84ba947..ab2c8c81b 100644 --- a/apps/bwclk/metadata.json +++ b/apps/bwclk/metadata.json @@ -1,7 +1,7 @@ { "id": "bwclk", "name": "BW Clock", - "version": "0.34", + "version": "0.35", "description": "A very minimalistic clock.", "readme": "README.md", "icon": "app.png", diff --git a/apps/daisy/ChangeLog b/apps/daisy/ChangeLog index 11138f412..c797845c5 100644 --- a/apps/daisy/ChangeLog +++ b/apps/daisy/ChangeLog @@ -13,3 +13,4 @@ 0.13: Fixed Battery estimate Default to percentage and improved setting string 0.14: Use `power_usage` module 0.15: Ring can now show hours, minute, or seconds hand, day/night left, or battery; Allowed for 12hr time; Ring now goes up in 5% increments; Step goal can be changed; The info that is set on the watchface will retain when leaving the face +0.16: Ring is now dynamically-created, rather than displaying pre-rendered rings; Seconds update every second; Ability to see Day ring; Settings options moved around to avoid popping of Steps option disappearing when not used; In Sun setting, ring is fully illuminated between during all of sunrise and sunset. \ No newline at end of file diff --git a/apps/daisy/README.md b/apps/daisy/README.md index 5cc3060dc..f42d34e2c 100644 --- a/apps/daisy/README.md +++ b/apps/daisy/README.md @@ -4,11 +4,10 @@ cyclic information line that includes, day, date, steps, battery, sunrise and sunset times* -Written by: [Hugh Barney](https://github.com/hughbarney) For support -and discussion please post in the [Bangle JS -Forum](http://forum.espruino.com/microcosms/1424/) +Written by: [Hugh Barney](https://github.com/hughbarney) and [David Volovskiy](https://github.com/voloved) +For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/) -* Derived from [The Ring](https://banglejs.com/apps/?id=thering) proof of concept and the [Pastel clock](https://banglejs.com/apps/?q=pastel) +* Derived from [The Ring](https://banglejs.com/apps/?id=thering) proof of concept and the [Pastel clock](https://banglejs.com/apps/?q=pastel), though all rings are dynamically created. * Includes the [Lazybones](https://banglejs.com/apps/?q=lazybones) Idle warning timer * Touch the top right/top left to cycle through the info display (Day, Date, Steps, Sunrise, Sunset, Heart Rate, Battery Estimate) * The heart rate monitor is turned on only when Heart rate is selected and will take a few seconds to settle @@ -24,7 +23,8 @@ See [#1248](https://github.com/espruino/BangleApps/issues/1248) * In the settings, the ring can be set to: * Hours - Displays the ring as though it's the hour hand on an analog clock. * Minutes - Displays the ring as though it's the minute hand on an analog clock. - * Seconds - Displays the ring as though it's the seconds hand on an analog clock. + * Seconds - Displays the ring as though it's the seconds hand on an analog clock. This option uses far more battery than any other option as it updates the screen 60 times more often. + * Day - Displays the ring as how much of the day is left. Functionally, it fills the ring half as quickly as the Hours option. * Steps - Displays the ring as the amount of steps taken that day out of Step Target setting. * Battery - Displays the ring as the amount of battery percentage left. * Sun - Displays the ring as the amount of time that has passed from sunrise to sunset in the day and the amount of time between sunset and sunrise at night. diff --git a/apps/daisy/app.js b/apps/daisy/app.js index 84d08e094..d8e6ceb9c 100644 --- a/apps/daisy/app.js +++ b/apps/daisy/app.js @@ -1,12 +1,15 @@ var SunCalc = require("suncalc"); // from modules folder const storage = require('Storage'); const widget_utils = require('widget_utils'); +let settings = undefined; +let location = undefined; const SETTINGS_FILE = "daisy.json"; +const global_settings = storage.readJSON("setting.json", true) || {}; const LOCATION_FILE = "mylocation.json"; const h = g.getHeight(); const w = g.getWidth(); -let settings; -let location; +const rad = h/2; +const hyp = Math.sqrt(Math.pow(rad, 2) + Math.pow(rad, 2)); // variable for controlling idle alert let lastStep = getTime(); @@ -19,8 +22,11 @@ let pal2; // palette for 50-100% const infoLine = (3*h/4) - 6; const infoWidth = 56; const infoHeight = 11; -const sec_update = 3000; // This ms between updates when the ring is in Seconds mode +const ringEdge = 4; +const ringThick = 6; +let nextUpdateMs; var drawingSteps = false; +var prevRing = {start: null, end: null, max: null}; function log_debug(o) { //print(o); @@ -86,7 +92,7 @@ function loadSettings() { settings.fg = settings.fg||'#0f0'; settings.idle_check = (settings.idle_check === undefined ? true : settings.idle_check); settings.batt_hours = (settings.batt_hours === undefined ? false : settings.batt_hours); - settings.hr_12 = (settings.hr_12 === undefined ? false : settings.hr_12); + settings.hr_12 = (global_settings["12hour"] === undefined ? false : global_settings["12hour"]); settings.ring = settings.ring||'Steps'; settings.idxInfo = settings.idxInfo||0; settings.step_target = settings.step_target||10000; @@ -113,6 +119,7 @@ function extractTime(d){ var sunRise = "00:00"; var sunSet = "00:00"; var drawCount = 0; +var night; // In terms of minutes var sunStart; // In terms of ms var sunEnd; // In terms of minutes var sunFull; // In terms of ms @@ -140,6 +147,7 @@ function updateSunRiseSunSet(now, lat, lon, sunLeftCalcs){ sunStart = times.sunset; sunFull = timesTmrw.sunrise - sunStart; sunEnd = getMinutesFromDate(timesTmrw.sunrise); + night = getMinutesFromDate(timesTmrw.sunriseEnd); } else { sunLeft = dateCopy - times.sunrise; @@ -150,12 +158,14 @@ function updateSunRiseSunSet(now, lat, lon, sunLeftCalcs){ sunStart = timesYest.sunset; sunFull = times.sunrise - sunStart; sunEnd = getMinutesFromDate(times.sunrise); + night = getMinutesFromDate(times.sunriseEnd); } else { // We're in the middle of the day isDaytime = true; - sunStart = times.sunrise; - sunFull = times.sunset - sunStart; - sunEnd = getMinutesFromDate(times.sunset); + sunStart = times.sunriseEnd; + sunFull = times.sunsetStart - sunStart; + sunEnd = getMinutesFromDate(times.sunsetStart); + night = getMinutesFromDate(times.sunset); } } } @@ -252,48 +262,83 @@ function drawHrm() { g.drawString(hrmCurrent, (w/2) + 10, infoLine); } -function draw() { - if (!idle) - drawClock(); +function draw(drawRingOnly) { + if (!idle) { + if (drawRingOnly) { + drawGaugeImage(new Date()); + } + else { + drawClock(); + } + } else drawIdle(); queueDraw(); } +function drawGaugeImage(date) { + var hh = date.getHours(); + var mm = date.getMinutes(); + var ring_fill; + var invertRing = false; + var ring_max = 100; + switch (settings.ring) { + case 'Hours': + ring_fill = ((hh % 12) * 60) + mm; + ring_max = 720; + break; + case 'Minutes': + ring_fill = mm; + ring_max = 60; + break; + case 'Seconds': + ring_fill = date.getSeconds(); + ring_max = 60; + break; + case 'Day': + ring_fill = (hh * 60) + mm; + ring_max = 1440; + break; + case 'Steps': + ring_fill = getSteps(); + ring_max = settings.step_target; + break; + case 'Battery': + ring_fill = E.getBattery(); + break; + case 'Sun': + var dayMin = getMinutesFromDate(date); + if (dayMin >= sunEnd && dayMin <= night) ring_fill = ring_max; + else { + ring_fill = ring_max * (date - sunStart) / sunFull; + if (ring_fill > ring_max) { // If we're now past a sunrise of sunset + updateSunRiseSunSet(date, location.lat, location.lon, true); + ring_fill = ring_max * (date - sunStart) / sunFull; + } + } + invertRing = !isDaytime; + break; + } + var start = 0; + var end = Math.round(ring_fill); + if (invertRing) { + start = ring_max - end; + end = ring_max; + } + if (end !== prevRing.end || start !== prevRing.start || ring_max !== prevRing.max) { + drawRing(start, end, ring_max); + prevRing.start = start; + prevRing.end = end; + prevRing.max = ring_max; + log_debug("Redrew ring at " + hh + ":" + mm); + } + log_debug("Start: "+ start + " end: " +end); +} + function drawClock() { var date = new Date(); var hh = date.getHours(); var mm = date.getMinutes(); - var ring_percent; - var invertRing = false; - switch (settings.ring) { - case 'Hours': - ring_percent = Math.round((10*(((hh % 12) * 60) + mm))/72); - break; - case 'Minutes': - ring_percent = Math.round((10*mm)/6); - break; - case 'Seconds': - ring_percent = Math.round((10*date.getSeconds())/6); - break; - case 'Steps': - ring_percent = Math.round(100*(getSteps()/settings.step_target)); - break; - case 'Battery': - ring_percent = E.getBattery(); - break; - case 'Sun': - ring_percent = 100 * (date - sunStart) / sunFull; - if (ring_percent > 100) { // If we're now past a sunrise of sunset - updateSunRiseSunSet(date, location.lat, location.lon, true); - ring_percent = 100 * (date - sunStart) / sunFull; - } - // If we're exactly on the minute that the sun is setting/rising - if (getMinutesFromDate(date) == sunEnd) ring_percent = 100; - invertRing = !isDaytime; - break; - } - if (settings.hr_12) { hh = hh % 12; if (hh == 0) hh = 12; @@ -303,8 +348,8 @@ function drawClock() { g.reset(); g.setColor(g.theme.bg); - g.fillRect(0, 0, w, h); - g.drawImage(getGaugeImage(ring_percent, settings.ring, invertRing), 0, 0); + g.fillEllipse(ringEdge+ringThick,ringEdge+ringThick,w-ringEdge-ringThick,h-ringEdge-ringThick); // Clears the text within the circle + drawGaugeImage(date); setLargeFont(); g.setColor(settings.fg); @@ -318,8 +363,10 @@ function drawClock() { drawInfo(); // recalc sunrise / sunset every hour - if (drawCount % 60 == 0) - updateSunRiseSunSet(date, location.lat, location.lon, settings.ring == 'Sun'); + if (drawCount % 60 == 0) { + let recalcSunLeft = (settings.ring == 'Sun'); + updateSunRiseSunSet(date, location.lat, location.lon, recalcSunLeft); + } drawCount++; } @@ -359,198 +406,63 @@ Bangle.on('HRM', function(hrm) { ///////////////// GAUGE images ///////////////////////////////////// - -// putting into 1 function like this, rather than individual variables -// reduces ram usage from 70%-13% -function getGaugeImage(p, type, reverse) { - const endsDontShowList = ['Minutes', 'Seconds']; // Don't show non-5% increments with these ring types - if (reverse) p = 100 - p; - var endsDontShow = endsDontShowList.includes(type); - // p0 - if (p < 2 || (p < 5 && endsDontShow)) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal2 : pal1), - buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVAAVUFUgpDAAdAFMEBFQ4ABqBVnLMQqLLLzWEABLgbVgohEGopYaiofDBihWVHJpYYDgYPbKx1ACJhYZIwT4OcAZWYHyRYUIgQXQH4RqOThCXUYRpCHNyQVVQQTwVQiSZWIQSEQNgSYSIYiEQQSyEUCQLDSOAyCnQiSCYQiSCYQiSCZDaDARObKuBSZwcaVzR0QFYKuZWAYNZWCJJKMoKuaWAahKBhiwTJRSudURorBFTgfMVzqjDO5DaeZ5jaeJhhiKbi4rIbT4hLqoriPI7afUpS5BbTwiKFdZgIADSmHFYIqgbgIrGcgIriEYwzHADZ7HRY4rdaYrjHADcBFYoGBFcgkEGQwAeFYqKHFbzUEcQ4AdiorwiorlEogxFAD59FWoorhoArDqArjgIr/FbYwFAEJSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A")) - }; - - // p2 - if (p < 5) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal2 : pal1), - buffer : require("heatshrink").decompress(atob("AH4A/ADNUFE8FqtVq2q1AqkFIIrDAAOAFMEBFQYrE1WgKsYrGLL4qFFY2pqDWeFZdUVkAhCAQMKFYdVLDUVFQYMHlWq0oMJKyoOJlQrCLDBWDB5clB5xWOoARMCARYWKwT4OgpYXKwY+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI")) - }; - - // p5 - if (p < 10) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal2 : pal1), - buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZeUVkFUBQcKFYdVqArZioqDBg8qFYQMIKyoOJlWpBoJYYKwYPLlIPOKx1ACJgQCLCxWCawgAJgpYXKwY+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA=")) - }; - - // p10 - if (p < 15) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal2 : pal1), - buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAOkQSdUFacK1WloCCSCaAAEFYKaQQSyEC0pvQirZTbomlIh6CYZAZFOQTBxDQhyCYOQhoPQS4bQHaBzaVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI")) - }; - - // p15 - if (p < 20) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal2 : pal1), - buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWAOpbRSucWAWVO5DaeZ5jaeJhgrBbTqkLbT4hLqoriPI7afUpS5BbTwiKFdZgIADSmHFYIqgbgIrGcgIriEYwzHADZ7HRY4rdaYrjHADcBFYoGBFcgkEGQwAeFYqKHFbzUEcQ4AdiorwiorlEogxFAD59FWoorhoArDqArjgIr/FbYwFAEJSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A==")) - }; - - // p20 - if (p < 25) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal2 : pal1), - buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4AWgNVoAEGAERSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A=")) - }; - - // p25 - if (p < 30) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal2 : pal1), - buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4rbgNVoArjgGq0Ar/FbMFFc+oFYYqkgEqFf4r/FY0VqgrlhWqFf4r/Ff4rdqorowArBqArlgQr/Ff4r/Ff4r/Ff4AKgNVoAr/Ff4r/Ff4r/Ff4rNqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA=")) - }; - - // p30 - if (p < 35) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal2 : pal1), - buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4rbgNVoArjgGq0Ar/FbMFFc+oFYYqkgEqFf4r/FY0VqgrlhWqFf4r/Ff4rdqorowArBqArlgQr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlhQrCioroAYIr/Ff4r/FbcFqorllWoFf4r/FY9AFcmqFYUBFc+gFf4rZgFVqAqjgWqwAr/FbdUFccFawkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI")) - }; - - // p35 - if (p < 40) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal2 : pal1), - buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4rbgNVoArjgGq0Ar/FbMFFc+oFYYqkgEqFf4r/FY0VqgrlhWqFf4r/Ff4rdqorowArBqArlgQr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlhQrCioroAYIr/Ff4r/FbcFqorllWoFf4r/FY9AFcmqFYUBFc+gFf4rZgFVqAqjgWqwAr/FbdUFccKFYkVFcwFDitVFccqFYkFFcuoFeNAFcWqFYkBFcugFYtQFUMCFYsAFcuAFYtUFcMKFY0VFcgHFitVFcMqFY0FFceoFY9AFcGqFY0BqtQFT8C1WgFeMAqtUFb8K1WAFY7cglQrIiorgjWqBI8FqtAFb1W1ArJbjz9BFZAKBbjxMBsALIFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5")) - }; - - // p40 - if (p < 45) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal2 : pal1), - buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4rbgNVoArjgGq0Ar/FbMFFc+oFYYqkgEqFf4r/FY0VqgrlhWqFf4r/Ff4rdqorowArBqArlgQr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlhQrCioroAYIr/Ff4r/FbcFqorllWoFf4r/FY9AFcmqFYUBFc+gFf4rZgFVqAqjgWqwAr/FbdUFccKFYkVFcwFDitVFccqFYkFFcuoFeNAFcWqFYkBFcugFYtQFUMCFYsAFcuAFYtUFcMKFY0VFcgHFitVFcMqFY0FFceoFY9AFcGqFY0BqtQFT8C1WgFeMAqtUFb8K1WAFY7cglQrIioriBI8FqtAFb2q1ArJbjzaBFZEBbj7aB0ALIFcLaHbkLaJFYbcd1QrKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAiEXKwKuNQjUBQR6EaiqCPQjVVQSATCqtUFSZvB1WACiSEUY4KCQQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A")) - }; - - // p45 - if (p < 50) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal2 : pal1), - buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4rbgNVoArjgGq0Ar/FbMFFc+oFYYqkgEqFf4r/FY0VqgrlhWqFf4r/Ff4rdqorowArBqArlgQr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlhQrCioroAYIr/Ff4r/FbcFqorllWoFf4r/FY9AFcmqFYUBFc+gFf4rZgFVqAqjgWqwAr/FbdUFccKFYkVFcwFDitVFccqFYkFFcuoFeNAFcWqFYkBFcugFYtQFUMCFYsAFcuAFYtUFcMKFY0VFcgHFitVFcMqFY0FFceoFY9AFcGqFY0BqtQFT8C1WgFeMAqtUFb8K1WAFY7cglQrIioriBI8FqtAFb2q1ArJbjzaBFZEBbj7aB0ALIFcLaHbkLaJFYbcd1QrKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAiEXKwKuNQjSCQQjSCQQjSCRAAIrBqgqThQrBwAUQQiyCSQgjdSbISCRQgZYSKwKCSQghYQKwSCSQghYQKwSCTAAMVFYNUCJsKFQOqFShYEoARMrRWXLAiFMiorCFSxYEFhQ6BFYJWXLAosIBgVWKzBYGcAsFBIdWKzIhGABI1EADArNoArcFhgqeWQwAEqAqeLJRVfcBLWdAH4A5A=")) - }; - - // p50 - if (p < 55) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal1 : pal2), - buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZgGq0AqjgNVoAr/FbeoFccFFYkqFcwFDlWqFccVFYkKFctUFeOAFcVVFYkCFctQFYugFUMBFYsAFctAFYuoFcMFFY0qFcgHFlWqFcMVFY0KFcdUFY+AFcFVFY0C1WgFT8BqtQFeMA1WoFb8FqtAFY7cgiorIlQriBI8K1WAFb1VqgrJbjzaBFZECbj7aBqALIFcLaHbkLaJFYbcdqorKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAiEXKwKuNQjSCQQjSCQQjSCRAAIrB1AqTgorBoAUQQiyCSQgjdSbISCRQgZYSKwKCSQghYQKwSCSQghYQKwSCTAAMqFYOoCJsFFQNVFShYEwARMFQRWVLAiFMQIRWWLAosKFQZWXLAosIFQZWYLAzgFawZWbAAMKFgmq1IoEAANUFTQABFZtAFbgsFFYwqeWQorFVjZZJFYhVfcAwrCazoA/AHI")) - }; - - // p55 - if (p < 60) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal1 : pal2), - buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZgGq0AqjgNVoAr/FbeoFccFFYkqFcwFDlWqFccVFYkKFctUFeOAFcVVFYkCFctQFYugFUMBFYsAFctAFYuoFcMFFY0qFcgHFlWqFcMVFY0KFcdUFY+AFcFVFY0C1WgFT8BqtQFeMA1WoFb8FqtAFY7cgiorIlQriBI8K1WAFb1VqgrJbjzaBFZECbj7aBqALIFcLaHbkLaJFYbcdqorKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAiEXKwKuNQjSCQQjSCQQjSCRAAIrB1AqTgorBoAUQQiyCSQgjdSbISCRQgZYSKwKCSQghYQKwSCSQghYQKwSCTAAMqFYOoCJsFFQNVFShYEwARMFQRWVLAmVQJxWWLAgcLFQZWXLAWpJJQqDKzBYC0ofDqjWHKzYhHABA1EADArNoArcFhgqegEBFRKsbLJxVfcBLWdAH4A5A==")) - }; - - // p60 - if (p < 65) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal1 : pal2), - buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZgGq0AqjgNVoAr/FbeoFccFFYkqFcwFDlWqFccVFYkKFctUFeOAFcVVFYkCFctQFYugFUMBFYsAFctAFYuoFcMFFY0qFcgHFlWqFcMVFY0KFcdUFY+AFcFVFY0C1WgFT8BqtQFeMA1WoFb8FqtAFY7cgiorIlQriBI8K1WAFb1VqgrJbjzaBFZECbj7aBqALIFcLaHbkLaJFYbcdqorKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAelNBqCLVxqEC0oRPQS6EC0oSQQSyECFYKEVQSIABFYI/QAAcFFYJDRCgSCmYYjdSCqqYCLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A")) - }; - - // p65 - if (p < 70) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal1 : pal2), - buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZgGq0AqjgNVoAr/FbeoFccFFYkqFcwFDlWqFccVFYkKFctUFeOAFcVVFYkCFctQFYugFUMBFYsAFctAFYuoFcMFFY0qFcgHFlWqFcMVFY0KFcdUFY+AFcFVFY0C1WgFT8BqtQFeMA1WkFb8FqtAFY+VbUArIlVVFcIJHhI1IAC9VqiNJXI7aYFZAKKbS5MJFcKkJXRLafBYbcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A=")) - }; - - // p70 - if (p < 75) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal1 : pal2), - buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZAgoAggNVoAr/FbdUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA=")) - }; - - // p75 - if (p < 80) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal1 : pal2), - buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4rOqtQFf4r/Ff4r/Ff4r/FZVUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5")) - }; - - // p80 - if (p < 85) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal1 : pal2), - buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AcIdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA=")) - }; - - // p85 - if (p < 90) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal1 : pal2), - buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhEBtQrgbhEFrTacbhkFqzadbgQrIXRbcfqoribg5hJbjIrGXILlIbjIiGFdZgIADSmHFYIqgbgIrGcgIriEYwzHADZ7HRY4rdaYrjHADcBFYoGBFcgkEGQwAeFYqKHFbzUEcQ4AdiorwiorlEogxFAD59FWoorhoArDqArjgIr/FbYwFAEJSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5")) - }; - - // p90 - if (p < 95) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal1 : pal2), - buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESquq1ArTgqESNgOqwArTIYKERH4KCUQigSBbKTdGCKKCVQiTCCFSyERCALBQQjAPBoArXDZ7ARObKuBSZwcaVzR0QFYKuZWAYNZWCJJKMoKuaWAahKBhiwTJRSudURorBFTgfMVzqjDO5DaeZ5jaeJhhiKbi4rIbT4hLqoriPI7afUpS5BbTwiKFdZgIADSmHFYIqgbgIrGcgIriEYwzHADZ7HRY4rdaYrjHADcBFYoGBFcgkEGQwAeFYqKHFbzUEcQ4AdiorwiorlEogxFAD59FWoorhoArDqArjgIr/FbYwFAEJSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A")) - }; - - // p95 - if (p < 98 || (p < 100 && endsDontShow)) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal1 : pal2), - buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLsFFZrgbgNVFAeoGohYfiorDBhIACKzVVtQqIFgpYYDgVqB5xYXKwVVoARMLDJGCfBzgDKzA+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA==")) - }; - - // p98 - if (p < 100) return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal1 : pal2), - buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtV1WpqtUFUlVAAIrCAANAFMEBEoQrFqtQKsQrHLL4jEFY5ZdawIrMcDasEEIo1FLDUVD4YMUKyo5NLDAcDB7ZWOoARMLDJGCfBzgDKzA+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI")) - }; - - // p100 - return { - width : 176, height : 176, bpp : 2, - transparent : -1, - palette : (reverse ? pal1 : pal2), - buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVAAVUFUgpDAAdAFMEBFQ4ABqBVnLMQqLFjzWEABLgbVgohEGoqyaiofDBihWVHJpYYDgYPbKxz5NLDJGCfBzgDKzA+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA=")) - }; +function addPoint(loc, max) { + var angle = ((2*Math.PI)/max) * loc; + var x = hyp * Math.sin(angle); + var y = hyp * Math.cos(angle + Math.PI); + x += rad; + y += rad; + return [Math.round(x),Math.round(y)]; } +function polyArray(start, end, max) { + const eighth = max / 8; + if (start == end) return []; // No array to draw if the points are the same. + let startOrigin = start; + let endOrigin = end; + start %= max; + end %= max; + if(start == 0 && startOrigin != 0) start = max; + if(end == 0 && endOrigin != 0) end = max; + if (start > end) end += max; + var array = [g.getHeight()/2, g.getHeight()/2]; + var pt = addPoint(start, max); + array.push(pt[0], pt[1]); + + for (let i = start + eighth; i < end; i += eighth) { + pt = addPoint(i, max); + array.push(pt[0], pt[1]); + } + pt = addPoint(end, max); + array.push(pt[0], pt[1]); + log_debug("Poly Arr: " + array); + return array; +} + +function drawRing(start, end, max) { + // Create persistent `buf` inside the function scope + if (!drawRing._buf) { + drawRing._buf = Graphics.createArrayBuffer(w, h, 2, { msb: true }); + } + const buf = drawRing._buf; + let img = { width: w, height: h, transparent: 0, + bpp: 2, palette: pal1, buffer: buf.buffer }; + buf.clear(); + buf.setColor(1).fillEllipse(ringEdge,ringEdge,w-ringEdge,h-ringEdge); + buf.setColor(0).fillEllipse(ringEdge+ringThick,ringEdge+ringThick,w-ringEdge-ringThick,h-ringEdge-ringThick); + img.palette = pal2; + g.drawImage(img, 0, 0); // Draws a filled-in circle + if((end - start) >= max) return; // No need to add the unfilled circle + buf.clear(); + buf.setColor(1).fillEllipse(ringEdge,ringEdge,w-ringEdge,h-ringEdge); + buf.setColor(0).fillEllipse(ringEdge+ringThick,ringEdge+ringThick,w-ringEdge-ringThick,h-ringEdge-ringThick); + buf.setColor(0).fillPoly(polyArray(start, end, max)); // Masks the filled-in part of the segment over the unfilled part + img.palette = pal1; + g.drawImage(img, 0, 0); // Draws the unfilled-in segment + return; +} + + ///////////////// IDLE TIMER ///////////////////////////////////// function drawIdle() { @@ -607,7 +519,7 @@ function dismissPrompt() { warned = false; lastStep = getTime(); Bangle.buzz(100); - draw(); + draw(false); } var dismissBtn = new BUTTON("big",0, 3*h/4 ,w, h/4, "#0ff", dismissPrompt, "Dismiss"); @@ -682,6 +594,35 @@ function buzzer(n) { }, 500); } +function getDelayMs(prevDelayMs, ring_setting, now) { + // Much of the logic here is for slowing or speeding the delay on the seconds setting. + // returns [ms before next update, if only the ring should be updated] + const sec_batt = [20, 50]; + const sec_delay = [10000, 2000, 1000]; + const deadband = 5; + if (ring_setting == 'Seconds') { + const nearNextMinute = (now % 60000) >= (60000 - prevDelayMs); + if (nearNextMinute) { + let batt = E.getBattery(); + for (let i = 0; i < sec_batt.length; i++) { + if (batt <= sec_batt[i]) + return [sec_delay[i], false]; + } + // Check for coming out of the above states w/ deadband + for (let i = 0; i < sec_batt.length; i++) { + if (prevDelayMs == sec_delay[i] && batt >= (sec_batt[i] + deadband)) + return [sec_delay[i + 1], false]; + } + return [sec_delay[sec_delay.length - 1], false]; + } + else { + return [prevDelayMs, true]; + } + } + else + return [60000, false]; +} + /////////////////////////////////////////////////////////////////////////////// // timeout used to update every minute @@ -689,19 +630,22 @@ var drawTimeout; // schedule a draw for the next minute or every sec_update ms function queueDraw() { - let delay = settings.ring == 'Seconds' ? sec_update - (Date.now() % sec_update) : 60000 - (Date.now() % 60000); + let now = Date.now(); + var nextUpdateRet = getDelayMs(nextUpdateMs, settings.ring, now); + nextUpdateMs = nextUpdateRet[0]; + let delay = nextUpdateMs - (now % nextUpdateMs); if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = setTimeout(function() { drawTimeout = undefined; checkIdle(); - draw(); + draw(nextUpdateRet[1]); }, delay); } // Stop updates when LCD is off, restart when on Bangle.on('lcdPower',on=>{ if (on) { - draw(); // draw immediately, queue redraw + draw(false); // draw immediately, queue redraw } else { // stop draw timer if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; @@ -716,7 +660,7 @@ Bangle.setUI("clockupdown", btn=> { Bangle.setHRMPower(infoMode == "ID_HRM" ? 1 : 0); resetHrm(); log_debug("idxInfo=" + settings.idxInfo); - draw(); + draw(false); storage.write(SETTINGS_FILE, settings); // Retains idxInfo when leaving the face }); @@ -724,6 +668,7 @@ loadSettings(); loadLocation(); var infoMode = infoList[settings.idxInfo]; updateSunRiseSunSet(new Date(), location.lat, location.lon, true); +nextUpdateMs = getDelayMs(1000, settings.ring, Date.now())[0]; g.clear(); Bangle.loadWidgets(); @@ -731,4 +676,4 @@ Bangle.loadWidgets(); * we are not drawing the widgets as we are taking over the whole screen */ widget_utils.hide(); -draw(); +draw(false); diff --git a/apps/daisy/metadata.json b/apps/daisy/metadata.json index d75402153..59b533020 100644 --- a/apps/daisy/metadata.json +++ b/apps/daisy/metadata.json @@ -1,8 +1,8 @@ { "id": "daisy", "name": "Daisy", - "version": "0.15", + "version": "0.16", "dependencies": {"mylocation":"app"}, - "description": "A beautiful digital clock with large ring guage, idle timer and a cyclic information line that includes, day, date, steps, battery, sunrise and sunset times", + "description": "A beautiful digital clock with large ring gauge, idle timer and a cyclic information line that includes, day, date, steps, battery, sunrise and sunset times", "icon": "app.png", "type": "clock", "tags": "clock", diff --git a/apps/daisy/settings.js b/apps/daisy/settings.js index 74b667e2a..741d4f3ea 100644 --- a/apps/daisy/settings.js +++ b/apps/daisy/settings.js @@ -7,36 +7,35 @@ 'color': 'Green', 'check_idle' : true, 'batt_hours' : false, - 'hr_12' : false, 'ring' : 'Steps', 'idxInfo' : 0, 'step_target' : 10000}; - // ...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 = storage.readJSON(SETTINGS_FILE, 1) || s; - const saved = settings || {}; - for (const key in saved) { - s[key] = saved[key]; - } +// ...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 = storage.readJSON(SETTINGS_FILE, 1) || s; +const saved = settings || {}; +for (const key in saved) { +s[key] = saved[key]; +} - function save() { - settings = s; - storage.write(SETTINGS_FILE, settings); - } +function save() { + settings = s; + storage.write(SETTINGS_FILE, settings); +} - var color_options = ['Green','Orange','Cyan','Purple','Red','Blue']; - var fg_code = ['#0f0','#ff0','#0ff','#f0f','#f00','#00f']; - var gy_code = ['#020','#220','#022','#202','#200','#002']; - var ring_options = ['Hours', 'Minutes', 'Seconds', 'Steps', 'Battery', 'Sun']; - var step_options = [100, 1000, 5000, 10000, 15000, 20000]; +var color_options = ['Green','Orange','Cyan','Purple','Red','Blue']; +var fg_code = ['#0f0','#ff0','#0ff','#f0f','#f00','#00f']; +var gy_code = ['#020','#220','#022','#202','#200','#002']; +var ring_options = ['Hours', 'Minutes', 'Seconds', 'Day', 'Sun', 'Steps', 'Battery']; +var step_options = [100, 1000, 5000, 10000, 15000, 20000]; function showMainMenu() { let appMenu = { '': { 'title': 'Daisy Clock' }, '< Back': back, - 'Colour': { + 'Color': { value: 0 | color_options.indexOf(s.color), min: 0, max: color_options.length - 1, format: v => color_options[v], @@ -45,28 +44,7 @@ s.fg = fg_code[v]; s.gy = gy_code[v]; save(); - }, - }, - 'Idle Warning': { - value: !!s.idle_check, - onchange: v => { - s.idle_check = v; - save(); - }, - }, - 'Expected Battery Life In Days Not Percentage': { - value: !!s.batt_hours, - onchange: v => { - s.batt_hours = v; - save(); - }, - }, - '12 Hr Time': { - value: !!s.hr_12, - onchange: v => { - s.hr_12 = v; - save(); - }, + } }, 'Ring Display': { value: 0 | ring_options.indexOf(s.ring), @@ -94,7 +72,22 @@ save(); }, }; - } + } + appMenu['Idle Warning'] = { + value: !!s.idle_check, + onchange: v => { + s.idle_check = v; + save(); + }, + }; + appMenu['Battery Life Format'] = { + value: !!s.batt_hours, + format: value => value?"Days":"%", + onchange: v => { + s.batt_hours = v; + save(); + }, + }; E.showMenu(appMenu); } diff --git a/apps/gpsinfo/gps-info-icon.js b/apps/gpsinfo/gps-info-icon.js index b3f2dd3d4..a07d14ebd 100644 --- a/apps/gpsinfo/gps-info-icon.js +++ b/apps/gpsinfo/gps-info-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwghC/AH4AKg9wC6t3u4uVC6wWBI6t3uJeVuMQCqcBLisAi4XLxAABFxAXKgc4DBAuBRhQXEDAq7MmYXEwBHEXZYXFGAOqAAKDMmczC4mIC62CC50PC4JIBkQABiIvRmURAAUSjQXSFwMoxGKC6CRFwUSVYgXLPIgXXwMYegoXLJAYXCGBnzGA0hPQIwMgYwGC6gwCC4ZIMC4gYBC604C4ZISmcRVgapQAAMhC6GIJIwXCMBcIxGDDBAuLC4IwGAARGMAAQWGmAXPJQoWMC4pwCCpoXJAB4XXAH4A/ABQA=")) +require("heatshrink").decompress(atob("mEw4UA///31lAQM0hRL/ACED4ALJh4LKn4jK/iMLBRMMCxQuFgNUFxMFqtALoXwBYklqoYCNI0pqtVGpALCoE/Og2pBYNU/4uFgGqBYNVBZV/BZG//4ABBY+f6oYBHY2q0okBvpTG1RICvp6CAAcqHgQLJyoLBqALHGAN9VYgABhQwCuqSGBYIwBBY8CBYOVBYI8FBYWl+oLGMAIABBYJIGBYQjBBY1aEgQLHqwYCBY0BqtqBYOpHYoLBDAYiFgqaBpWq0ALILgwLZAH4AFA")) diff --git a/apps/meridian/metadata.json b/apps/meridian/metadata.json index ab4cdbed2..3ad1e78aa 100644 --- a/apps/meridian/metadata.json +++ b/apps/meridian/metadata.json @@ -3,12 +3,13 @@ "name": "Meridian Clock", "shortName": "Meridian", "version": "0.01", - "description": "An elegant clock", + "description": "An elegant clock with two ClockInfos", "screenshots": [{ "url": "screenshot.png" }], "icon": "icon.png", "type": "clock", - "tags": "clock", + "tags": "clock,clkinfo", "supports": ["BANGLEJS2"], + "dependencies" : { "clock_info":"module" }, "readme": "README.md", "storage": [ { "name": "meridian.app.js", "url": "app.js" }, diff --git a/apps/simplebgclock/metadata.json b/apps/simplebgclock/metadata.json index c0589cece..65af24a2e 100644 --- a/apps/simplebgclock/metadata.json +++ b/apps/simplebgclock/metadata.json @@ -6,7 +6,7 @@ "icon": "icon.png", "screenshots": [{"url":"screenshot.png"}], "type": "clock", - "tags": "clock,clkinfo,clockbg", + "tags": "clock,clockbg", "supports" : ["BANGLEJS2"], "dependencies" : { "clockbg":"module" }, "storage": [ diff --git a/apps/twotwoclock/ChangeLog b/apps/twotwoclock/ChangeLog index d3275c0e8..0b6e606c1 100644 --- a/apps/twotwoclock/ChangeLog +++ b/apps/twotwoclock/ChangeLog @@ -1,2 +1,3 @@ 0.01: New Clock! -0.02: Clockinfos now save under correct name, and wrap correctly to >1 line \ No newline at end of file +0.02: Clockinfos now save under correct name, and wrap correctly to >1 line +0.03: Fix time's border when drawing over a solid color background \ No newline at end of file diff --git a/apps/twotwoclock/app.js b/apps/twotwoclock/app.js index b2d5ea9fb..3ea357e46 100644 --- a/apps/twotwoclock/app.js +++ b/apps/twotwoclock/app.js @@ -73,10 +73,9 @@ for (var i=0;i<10;i++) var mn = d.getMinutes().toString().padStart(2,0); var date = require("locale").date(new Date()).split(" ").slice(0,2).join(" ").toUpperCase(); var x = 6, y = 16, w = 55, h = 67, datesz = 20, s=5; - g.reset(); background.fillRect(x, y, x + w*2, y + h*2 + datesz); var dx = x+w, dy = y+h+datesz-10; - g.setFont("LECO1976Regular").setFontAlign(0,0); + g.reset().setFont("LECO1976Regular").setFontAlign(0,0); g.setColor(g.theme.bg).drawString(date, dx+3,dy-3).drawString(date, dx+3,dy+3); g.drawString(date, dx-3,dy-3).drawString(date, dx-3,dy+3); g.drawString(date, dx,dy-3).drawString(date, dx,dy+3); diff --git a/apps/twotwoclock/metadata.json b/apps/twotwoclock/metadata.json index ae3b958ef..5bcff42c1 100644 --- a/apps/twotwoclock/metadata.json +++ b/apps/twotwoclock/metadata.json @@ -1,7 +1,7 @@ { "id": "twotwoclock", "name": "TwoTwo Clock", "shortName":"22 Clock", - "version":"0.02", + "version":"0.03", "description": "A clock with the time split over two lines, with custom backgrounds and two ClockInfos", "icon": "icon.png", "type": "clock", diff --git a/apps/umpire/20250511_210249-COLLAGE.jpg b/apps/umpire/20250511_210249-COLLAGE.jpg new file mode 100644 index 000000000..91622ad44 Binary files /dev/null and b/apps/umpire/20250511_210249-COLLAGE.jpg differ diff --git a/apps/umpire/ChangeLog b/apps/umpire/ChangeLog new file mode 100644 index 000000000..bffacca09 --- /dev/null +++ b/apps/umpire/ChangeLog @@ -0,0 +1 @@ +0.01: First release with heart rate monitoring diff --git a/apps/umpire/README.md b/apps/umpire/README.md new file mode 100644 index 000000000..9e7ed7318 --- /dev/null +++ b/apps/umpire/README.md @@ -0,0 +1,94 @@ +# Umpire Ball Counter +*Cricket umpire ball counter and match event logger for Bangle.js 2 hackable smart watch* + + + +## Background +There are a few ways to keep track of balls when umpiring a cricket match. These vary from the rudimentary - having some stones in your pockets and transferring them from one side to the other - to the "modern" umpire's clicker. The latter allows you to track fairly delivered balls, overs completed and wickets fallen. + +In addition, the umpire needs to know the time and to record events like when "Play" was called or when a fielder left the field. These events are written down on paper. + +The umpire also needs to assess whether either team is timewasting and whether a new batter has made it out to the crease on time, as required by the Laws. + +This app is designed to replace the umpire clicker, and keep track of events for analysis and reporting after each match. + +## Objectives ## + +The app has three objectives: +1. Count how many balls and wickets have been logged, show the current over number and the time. +2. Log these and other events with a timestamp and a duration since the last event. +3. Allow the data to be viewed on the watch and on a BLE-paired mobile device as structured data. + +## Modules ## + +The app is split into three screens shown to the umpire: +1. Main Menu +2. In-play Screen +3. Log viewer + +There is an additional module to allow settings to be changed via the watch Settings menu; the number of balls in the over and the number of overs in the innings are configurable, so that 15 eight-ball overs, T20, 40-50 overs and The Hundred can all be supported. + +A further module of the app is the web page used to view the log file in the app loader. The file can be deleted or a copy downloaded. + +## Interaction ## + +Bangle.js 2 app interactions are governed by the Bangle and Espruino (E) libraries. This app uses the following patterns: + +### Swipe and Button ### + +The in-play screen detects swipe and button events using Bangle.setUI. + +When in-play, use of the button increments the (fairly delivered) ball count. The app does not suppress the screen lock so by default it requires the umpire to press once to unlock the watch and a second press to log the ball. Bangle.buzz is used to give positive feedback when the app takes action so that the screen does not need to be looked at. + +After logging the third-to-last ball in the over, the app buzzes twice. This is typically when the umpire needs to check the balls remaining with the other umpire, in case they have not logged every fair delivery. + +After the second-to-last ball the app gives one long buzz to remind the umpire that the next ball will close the over (if fairly delivered). + +Logging the final ball triggers the change of over. + +When the last over of the innings is complete the screen shows "END" but allows play to continue to be logged until the new innings is triggered. + +Whilst the in-play screen is displayed swiping will cause the following actions to occur: +- **Swipe Up** performs the same action as the Button press, incrementing the ball count. +- **Swipe Down** decrements the ball count and logs the action as a "Correction". +- **Swipe Right** shows the log viewer. +- **Swipe Left** shows the main menu. + +### Confirmation Prompts ### + +When choosing "Wicket" or "Recall" from the main menu, E.showPrompt is used to ask for positive confirmation before incrementing or decrementing the wickets, respectively. Those functions also increment or decrement the ball count. + +### Scrollers ### + +The main menu and log viewer both use E.showScroller to display a scrollable list of tappable items. This is in preference to E.showMenu which has very small touch targets. + +Whenever a scroller is shown, the in-play swipe interactions are switched off. + +Tapping on the log viewer returns the umpire to the in-play screen. + +### Twist to refresh ### + +The app uses Bangle.twist to detect the umpire turning their wrist to view the screen and, when the in-play screen is active, will refresh the current time and elapsed time since the last ball. + +## Timing ## + +The app calculates three durations: +1. **Ball to ball** - When logging a ball the elapsed time since the last logged ball. +2. **Overs** - When logging the last ball of the over, the elapsed time since the last ball of the previous over was logged (or when "Play" was initially called). +3. **Lost Time** - When logging the next ball after a call of "Time" the elapsed time since the call of "Time". + +## Other Metadata Logging ## + +The app now includes logging of other sensor data from the watch. the following items have been added to the log function: + +1. **Steps** - When the app is launched it notes the current step count and then logs the number of steps the umpire has taken on every ball. +2. **Battery Level** - In order to assess whether the heart rate monitor is running the battery down, the battery percentage is logged on every ball. +3. **Heart Rate Monitoring** - The main menu includes an option to "Start HRM" and then the heart rate is logged on every ball. + +N.B. The HRM takes about 5 seconds to start measuring the umpire's heart rate. + +Once switched on, the HRM has two additional features: + +1. **Battery Saving** - After the Call of Time the HRM is powered down until (Play is called and) the next ball is logged. + +2. **Auto-logging** - The app Settings menu has an additional item which is a heartbeat rate over which the app should automatically log an event. Once the umpire's heart rate goes over the limit for 10 consecutive seconds an event will be logged and the in-play screen will show the heart rate on every refresh. Until the heart rate falls below the threshold no further heart events will be logged. A further event will not be triggered until the limit is exceeded again for 20 consecutive seconds. The HRM can be stopped from the main menu during the match. diff --git a/apps/umpire/app-icon.js b/apps/umpire/app-icon.js new file mode 100644 index 000000000..f54effcad --- /dev/null +++ b/apps/umpire/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///yn+hW803RBwUP/gUDlWAAoc//4FD1WgAoYiB4AEBgWq1QLD/1vBYUK0uqEgf9r4LClWVtQkDC4mqqtWBYYvBFwWpqtV1BHGFwILBHgcP+AuDBYNqPwwuBBZAuDEYgACFwdWHYaMDFwVaQgguGTggu/F34uxewguFEYguGHYYuHgEDKYIuHgEP9WVFw4LC0ouHgE//2qFw8A///1QuHgf//kqFxH/4EKFxH/BI4uC/gKIFwPwBZAuCBZAuWBYIuJEYIuJHYIKJgA")) diff --git a/apps/umpire/app.js b/apps/umpire/app.js new file mode 100644 index 000000000..e02932973 --- /dev/null +++ b/apps/umpire/app.js @@ -0,0 +1,432 @@ +// settings and environment +var settings = Object.assign({ + // default values + ballsPerOver: 6, + oversPerInnings: 40, + heartRateLimit: 130 +}, require('Storage').readJSON("umpire.json", true) || {}); +const BALLS_PER_OVER = settings.ballsPerOver; +const OVERS_PER_INNINGS = settings.oversPerInnings; +const HEART_RATE_LIMIT = settings.heartRateLimit; +delete settings; +const TIMEZONE_OFFSET_HOURS = (new Date()).getTimezoneOffset() / 60; +const STEP_COUNT_OFFSET = Bangle.getStepCount(); +const BALL_TO_COME_CHAR = '-'; +const BALL_FACED_CHAR = '='; + +// globals +var processing = true; //debounce to inhibit twist events +var wickets = 0; +var counter = 0; +var over = 0; +var ballTimes = []; +var overTimes = []; +var timeTimes = []; +var log = []; +var timeCalled = false; +var batteryPercents = []; +var battery = 0; +var heartRate = ''; +var heartRateEventSeconds = 0; +var HRM = false; + +function toggleHRM() { + if(HRM) { + Bangle.setHRMPower(0); + HRM = false; + heartRateEventSeconds = 0; + heartRate = ''; + } else { + Bangle.setHRMPower(1); + HRM = true; + } +} + +function getBattery() { + // calculate last 10 moving average % + batteryPercents.push(E.getBattery()); + if(batteryPercents.length > 10) batteryPercents.shift(); + return Math.round(batteryPercents.reduce((avg,e,i,arr)=>avg+e/arr.length,0)); +} + +// process heart rate monitor event +// each second (approx.) +function updateHeartRate(h) { + heartRate = h.bpm || 0; + if(heartRate >= HEART_RATE_LIMIT) { + heartRateEventSeconds++; + if(heartRateEventSeconds==10) + addLog((new Date()), over, counter, + "Heart Rate", ">" + HEART_RATE_LIMIT); + } + if(heartRateEventSeconds > 10 + && heartRate < HEART_RATE_LIMIT) + heartRateEventSeconds = -10; +} + +// write events to storage (csv, persistent) +// and memory (can be truncated while running) +function addLog(timeSig, over, ball, matchEvent, metaData) { + var steps = Bangle.getStepCount() - STEP_COUNT_OFFSET; + // write to storage + var csv = [ + formatTimeOfDay(timeSig), + over-1, ball, + matchEvent, metaData, + steps, battery, heartRate + ]; + file.write(csv.join(",")+"\n"); + // write to memory + log.unshift({ // in rev. chrono. order + time: formatTimeOfDay(timeSig), + over: over-1, + ball: ball, + matchEvent: matchEvent, + metaData: metaData, + steps: steps, + battery: battery, + heartRate: heartRate + }); +} + +// display log from memory (not csv) +function showLog() { + processing = true; + Bangle.setUI(); + return E.showScroller({ + h: 50, c: log.length, + draw: (idx, r) => { + g.setBgColor((idx&1)?"#000":"#112").clearRect(r.x,r.y,r.x+r.w-1,r.y+r.h-1); + if(log[idx].matchEvent==/*LANG*/"Over Duration" + || log[idx].matchEvent==/*LANG*/"Innings Duration") { + g.setFont("Vector", 22).drawString( + log[idx].matchEvent,r.x+6,r.y+2); + } else { + g.setFont("Vector", 22).drawString( + log[idx].over + "." + + log[idx].ball + " " + + log[idx].matchEvent,r.x+6,r.y+2); + } + g.setFont("Vector", 18).drawString( + log[idx].time + " " + + log[idx].metaData + " " + + log[idx].heartRate,r.x+6,r.y+27); + }, + select: (idx) => { + resumeGame(); + } + }); +} + +// format date (diff) as duration +function formatDuration(timeDate) { + return (timeDate.getHours() + TIMEZONE_OFFSET_HOURS) + ":" + + timeDate.getMinutes().toString().padStart(2, "0") + ":" + + timeDate.getSeconds().toString().padStart(2, "0") + ""; +} + +// format date as clock +function formatTimeOfDay(timeSig) { + return timeSig.getHours() + ":" + + timeSig.getMinutes().toString().padStart(2, "0"); +} + +// main ball counter logic +// and in-play screen +function countDown(dir) { + processing = true; + battery = getBattery(); // refresh battery + counter += dir; + // suppress correction on first ball of innings + if(over==1 && counter<0) { + counter=0; + processing = false; + return; + } + // Suppress dir when play after time + if(timeCalled) + counter -= dir; + // Correction to last ball of over + if(counter<0) { + counter = BALLS_PER_OVER -1; + over -= 1; + // use end of over time as last ball time + ballTimes.push(overTimes.pop()); + } + // create timestamp for log + var timeSig = new Date(); + // calculate elapsed since last ball + var lastBallTime = timeSig.getTime(); + if(ballTimes.length>0) { + lastBallTime = ballTimes[ballTimes.length - 1]; + } else if(overTimes.length>0) { + lastBallTime = overTimes[overTimes.length - 1]; + } + var deadDuration = new Date( + timeSig.getTime() - lastBallTime); + // process new (dead) ball + if(dir!=0) { + // call play after time? + if(timeCalled) { + timeCalled = false; + // resume heart rate monitoring + if(HRM) Bangle.setHRMPower(1); + // calculate time lost and log it + var lastTimeTime = timeTimes[timeTimes.length - 1]; + var timeDuration = new Date( + timeSig.getTime() - lastTimeTime); + addLog(timeSig, over, counter, + "Play", /*LANG*/"Lost:" + formatDuration(timeDuration)); + } else { + if(counter>0) // reset elapsed time + ballTimes.push(timeSig.getTime()); + Bangle.setLCDPower(1); //TODO need any more? + + if(dir>0) { // fairly delivered ball + addLog(timeSig, over, counter, + "Ball", formatDuration(deadDuration)); + } else { // +1 ball still to come + addLog(timeSig, over, counter, + /*LANG*/"Correction", formatDuration(deadDuration)); + } + } + // give haptic feedback + if(counter == BALLS_PER_OVER - 2) { + // buzz twice "2 to come" + Bangle.buzz(400).then(()=>{ + return new Promise( + resolve=>setTimeout(resolve,500)); + }).then(()=>{ + return Bangle.buzz(500); + }) + } else if(counter == BALLS_PER_OVER - 1) { + // long buzz "1 to come" + Bangle.buzz(800); + } else { + // otherwise short buzz + Bangle.buzz() + } + // Process end of over + if (counter == BALLS_PER_OVER) { + // calculate match time + var matchDuration = new Date( + timeSig.getTime() - overTimes[0]); + var matchMinutesString = formatDuration(matchDuration); + // calculate over time + var overDuration = new Date( + timeSig.getTime() - overTimes[overTimes.length - 1]); + var overMinutesString = formatDuration(overDuration); + // log end of over + addLog(timeSig, over + 1, 0, + /*LANG*/"Over Duration", overMinutesString); + addLog(timeSig, over + 1, 0, + /*LANG*/"Innings Duration", matchMinutesString); + overTimes.push(timeSig.getTime()); + // start new over + over += 1; + counter = 0; + ballTimes = []; + } + } + // refresh in-play screen + g.clear(1); + // draw wickets fallen (top-right) + g.setFontAlign(1,0); + g.setFont("Vector",26). + drawString(wickets, 162, 14, true); + g.setFont("Vector",12). + drawString('\¦\¦\¦', 173, 15, true); + // draw battery and heart rate (top-left) + g.setFontAlign(-1,0); + var heartRateString = 'HR:' + heartRate; + if(heartRateEventSeconds <= 0) heartRateString = ''; + g.setFont("Vector",16). + drawString(battery + '% ' + heartRateString, 5, 11, true); + // draw clock (upper-centre) + g.setFontAlign(0,0); + g.setFont("Vector",48). + drawString(formatTimeOfDay(timeSig), 93, 55, true); + // draw over.ball (centre) + var ballString = (over-1) + "." + counter; + if(over > OVERS_PER_INNINGS) + ballString = 'END'; + g.setFont("Vector",80). + drawString(ballString, 93, 120, true); + // draw ball graph and elapsed time + var ballGraph = + BALL_FACED_CHAR.repeat(counter) + + BALL_TO_COME_CHAR.repeat(BALLS_PER_OVER - counter); + if(timeCalled) ballGraph = '-TIME-'; + g.setFont("Vector",18).drawString( + ballGraph + ' ' + formatDuration(deadDuration), 93, 166, true); + // return to wait for next input + processing = false; +} + +function resumeGame(play) { + processing = true; + Bangle.buzz(); + Bangle.setUI({ + mode: "custom", + swipe: (directionLR, directionUD)=>{ + if (directionLR==-1) { + processing = true; + showMainMenu(); + } else if (directionLR==1) { + processing = true; + showLog(); + } else if (directionUD==-1) { + processing = true; + countDown(1); + } else { + processing = true; + countDown(-1); + } + }, + btn: ()=>{ + processing = true; + countDown(1); + } + }); + if(over==0) { // at start of innings + over += 1; // N.B. 1-based overs in code + counter = 0; // balls + ballTimes = []; + // set an inital time for comparison + var timeSig = new Date(); + overTimes.push(timeSig.getTime()); + addLog(timeSig, over, counter, + "Play", ""); + } + // load in-play screen + countDown(play? -1: 0); +} + +function incrementWickets(inc) { + processing = true; + E.showPrompt(/*LANG*/"Amend wickets by " + inc + "?"). + then(function(confirmed) { + if (confirmed) { + Bangle.buzz(); + wickets += inc; + var timeSig = new Date(); + if(inc>0) { + countDown(1); + addLog(timeSig, over, counter, + "Wicket", "Wickets: " + wickets); + } else { + addLog(timeSig, over, counter, + /*LANG*/"Recall Batter", "Wickets: " + wickets); + } + resumeGame(); + } else { + E.showPrompt(); + showMainMenu(); + } + }); +} + +function showMainMenu() { + processing = true; + Bangle.setUI(); + var scrollMenuItems = []; + // add menu items + if(over>0) + scrollMenuItems.push("« Back"); + if(over==0 || timeCalled) + scrollMenuItems.push("Call Play"); + if(over>0 && !timeCalled) { + scrollMenuItems.push("Wicket"); + if(wickets>0) + scrollMenuItems.push(/*LANG*/"Recall Batter"); + scrollMenuItems.push("Call Time"); + scrollMenuItems.push("New Innings"); + if(!HRM) + scrollMenuItems.push("Start HRM"); + } + if(HRM) scrollMenuItems.push("Stop HRM"); + // show menu + return E.showScroller({ + h: 80, c: scrollMenuItems.length, + draw: (idx, r) => { + g.setBgColor((idx&1)?"#000":"#121").clearRect(r.x,r.y,r.x+r.w-1,r.y+r.h-1); + g.setFont("Vector", 30).drawString(scrollMenuItems[idx],r.x+10,r.y+28); + }, + select: (idx) => { + if(scrollMenuItems[idx]=="Call Time") { + timeCalled = true; + // power down HRM until play + Bangle.setHRMPower(0); + heartRateEventSeconds = 0; + var timeSig = new Date(); + timeTimes.push(timeSig.getTime()); + addLog(timeSig, over, counter, + "Time", (HRM? "HRM Paused" : "")); + resumeGame(); + } + if(scrollMenuItems[idx]=="Call Play") + resumeGame(timeCalled); + if(scrollMenuItems[idx]=="« Back") + resumeGame(); + if(scrollMenuItems[idx]=="Wicket") + incrementWickets(1); + if(scrollMenuItems[idx]==/*LANG*/"Recall Batter") + incrementWickets(-1); + if(scrollMenuItems[idx]=="New Innings") + newInnings(); + if(scrollMenuItems[idx]=="Start HRM" + || scrollMenuItems[idx]=="Stop HRM") { + toggleHRM(); + resumeGame(); + } + } + }); +} + +function newInnings() { + var timeSig = new Date(); + if(over!=0) { // new innings + E.showPrompt(/*LANG*/"Start next innings?"). + then(function(confirmed) { + if (confirmed) { + Bangle.buzz(); + processing = true; //debounce to inhibit twist events + wickets = 0; + counter = 0; + over = 0; + ballTimes = []; + overTimes = []; + timeTimes = []; + log = []; + timeCalled = false; + addLog(timeSig, OVERS_PER_INNINGS + 1, BALLS_PER_OVER, + "New Innings", require("locale").date(new Date(), 1)); + resumeGame(); + } else { + E.showPrompt(); + showMainMenu(); + } + }); + } else { // resume innings or start app + addLog(timeSig, OVERS_PER_INNINGS + 1, BALLS_PER_OVER, + "New Innings", require("locale").date(new Date(), 1)); + } +} +// initialise file in storage +var file = require("Storage").open("matchlog.csv","a"); +// save state on exit TODO WIP +E.on("kill", function() { + console.log("Umpire app closed at " + (over-1) + "." + counter); +}); +// set up twist refresh once only +Bangle.on('twist', function() { + if(!processing) { + processing = true; // debounce + countDown(0); + } +}); +// set up HRM listener once only +Bangle.on('HRM', function(h) { + updateHeartRate(h)}); +newInnings(); // prepare 1st innings +showMainMenu(); // ready to play diff --git a/apps/umpire/app.png b/apps/umpire/app.png new file mode 100644 index 000000000..e9866f59d Binary files /dev/null and b/apps/umpire/app.png differ diff --git a/apps/umpire/interface.html b/apps/umpire/interface.html new file mode 100644 index 000000000..34dc5307d --- /dev/null +++ b/apps/umpire/interface.html @@ -0,0 +1,76 @@ + +
+ + + + + + + + + + + diff --git a/apps/umpire/metadata.json b/apps/umpire/metadata.json new file mode 100644 index 000000000..2cfdb8119 --- /dev/null +++ b/apps/umpire/metadata.json @@ -0,0 +1,18 @@ +{ "id": "umpire", + "name": "Umpire Ball Counter", + "shortName":"Umpire", + "icon": "app.png", + "version": "0.01", + "description": "Cricket umpire ball counter and match event logger", + "tags": "outdoors,tool,health,sports,referee,judge,assistant", + "readme":"README.md", + "interface": "interface.html", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"umpire.app.js","url":"app.js"}, + {"name":"umpire.settings.js","url":"settings.js"}, + {"name":"umpire.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"umpire.json"}] +} diff --git a/apps/umpire/settings.js b/apps/umpire/settings.js new file mode 100644 index 000000000..e40e656bc --- /dev/null +++ b/apps/umpire/settings.js @@ -0,0 +1,43 @@ +(function(back) { + var FILE = "umpire.json"; + // Load settings + var settings = Object.assign({ + ballsPerOver: 6, + oversPerInnings: 40, + heartRateLimit: 130 + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Show the menu + E.showMenu({ + "" : { "title" : "Umpire Ball Counter" }, + "< Back" : () => back(), + 'Balls per over': { + value: settings.ballsPerOver, + min: 4, max: 10, + onchange: v => { + settings.ballsPerOver = v; + writeSettings(); + } + }, + 'Overs per inn.': { + value: settings.oversPerInnings, + min: 12, max: 50, + onchange: v => { + settings.oversPerInnings = v; + writeSettings(); + } + }, + 'Auto-log HRM': { + value: settings.heartRateLimit, + min: 100, max: 200, + onchange: v => { + settings.heartRateLimit = v; + writeSettings(); + } + } + }); +}) diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index 75f740d4f..539e44ede 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -343,6 +343,10 @@ apps.forEach((app,appIdx) => { if (a>=0 && b>=0 && a