mirror of https://github.com/espruino/BangleApps
494 lines
12 KiB
JavaScript
494 lines
12 KiB
JavaScript
/**
|
|
* BangleJS MARIO CLOCK
|
|
*
|
|
* + Original Author: Paul Cockrell https://github.com/paulcockrell
|
|
* + Created: April 2020
|
|
* + Based on Espruino Mario Clock V3 https://github.com/paulcockrell/espruino-mario-clock
|
|
* + Online Image convertor: https://www.espruino.com/Image+Converter, Use transparency + compression + 8bit Web + export as Image String
|
|
* + Images must be drawn as PNGs with transparent backgrounds
|
|
*/
|
|
|
|
const locale = require("locale");
|
|
const storage = require('Storage');
|
|
const settings = (storage.readJSON('setting.json', 1) || {});
|
|
const timeout = settings.timeout || 10;
|
|
const is12Hour = settings["12hour"] || false;
|
|
|
|
// Screen dimensions
|
|
let W, H;
|
|
|
|
let intervalRef, displayTimeoutRef = null;
|
|
|
|
// Colours
|
|
const LIGHTEST = "#effedd";
|
|
const LIGHT = "#add795";
|
|
const DARK = "#588d77";
|
|
const DARKEST = "#122d3e";
|
|
const NIGHT = "#001818";
|
|
|
|
// Character names
|
|
const DAISY = "daisy";
|
|
const TOAD = "toad";
|
|
const MARIO = "mario";
|
|
|
|
const characterSprite = {
|
|
frameIdx: 0,
|
|
x: 33,
|
|
y: 55,
|
|
jumpCounter: 0,
|
|
jumpIncrement: Math.PI / 6,
|
|
isJumping: false,
|
|
character: MARIO,
|
|
};
|
|
|
|
const coinSprite = {
|
|
frameIdx: 0,
|
|
x: 34,
|
|
y: 18,
|
|
isAnimating: false,
|
|
yDefault: 18,
|
|
};
|
|
|
|
const pyramidSprite = {
|
|
x: 90,
|
|
height: 34,
|
|
};
|
|
|
|
const ONE_SECOND = 1000;
|
|
const DATE_MODE = "date";
|
|
const BATT_MODE = "batt";
|
|
const TEMP_MODE = "temp";
|
|
|
|
let timer = 0;
|
|
let backgroundArr = [];
|
|
let nightMode = false;
|
|
let infoMode = DATE_MODE;
|
|
|
|
// Used to stop values flapping when displayed on screen
|
|
let lastBatt = 0;
|
|
let lastTemp = 0;
|
|
|
|
function genRanNum(min, max) {
|
|
return Math.floor(Math.random() * (max - min + 1) + min);
|
|
}
|
|
|
|
function switchCharacter() {
|
|
const curChar = characterSprite.character;
|
|
|
|
let newChar;
|
|
switch(curChar) {
|
|
case DAISY:
|
|
newChar = MARIO;
|
|
break;
|
|
case TOAD:
|
|
newChar = DAISY;
|
|
break;
|
|
case MARIO:
|
|
default:
|
|
newChar = TOAD;
|
|
}
|
|
|
|
characterSprite.character = newChar;
|
|
}
|
|
|
|
function toggleNightMode() {
|
|
nightMode = !nightMode;
|
|
}
|
|
|
|
function incrementTimer() {
|
|
if (timer > 1000) {
|
|
timer = 0;
|
|
}
|
|
else {
|
|
timer += 50;
|
|
}
|
|
}
|
|
|
|
function drawBackground() {
|
|
// Clear screen
|
|
const bgColor = (nightMode) ? NIGHT : LIGHTEST;
|
|
g.setColor(bgColor);
|
|
g.fillRect(0, 10, W, H);
|
|
|
|
// set cloud colors and draw clouds
|
|
const cloudColor = (nightMode) ? DARK : LIGHT;
|
|
g.setColor(cloudColor);
|
|
g.fillRect(0, 10, g.getWidth(), 15);
|
|
g.fillRect(0, 17, g.getWidth(), 17);
|
|
g.fillRect(0, 19, g.getWidth(), 19);
|
|
g.fillRect(0, 21, g.getWidth(), 21);
|
|
|
|
// Date bar
|
|
g.setColor(DARKEST);
|
|
g.fillRect(0, 0, W, 9);
|
|
}
|
|
|
|
function drawFloor() {
|
|
const fImg = require("heatshrink").decompress(atob("ikDxH+rgATCoIBQAQYDP")); // Floor image
|
|
for (let x = 0; x < 4; x++) {
|
|
g.drawImage(fImg, x * 20, g.getHeight() - 5);
|
|
}
|
|
}
|
|
|
|
function drawPyramid() {
|
|
const pPol = [pyramidSprite.x + 10, H - 6, pyramidSprite.x + 50, pyramidSprite.height, pyramidSprite.x + 90, H - 6]; // Pyramid poly
|
|
|
|
const color = (nightMode) ? DARK : LIGHT;
|
|
g.setColor(color);
|
|
g.fillPoly(pPol);
|
|
|
|
pyramidSprite.x -= 1;
|
|
// Reset and randomize pyramid if off-screen
|
|
if (pyramidSprite.x < - 100) {
|
|
pyramidSprite.x = 90;
|
|
pyramidSprite.height = genRanNum(25, 60);
|
|
}
|
|
}
|
|
|
|
function drawTreesFrame(x, y) {
|
|
const tImg = require("heatshrink").decompress(atob("h8GxH+AAMHAAIFCAxADEBYgDCAQYAFCwobOAZAEFBxo=")); // Tree image
|
|
|
|
g.drawImage(tImg, x, y);
|
|
g.setColor(DARKEST);
|
|
g.drawLine(x + 6 /* Match stalk to palm tree */, y + 6 /* Match stalk to palm tree */, x + 6, H - 6);
|
|
}
|
|
|
|
function generateTreeSprite() {
|
|
return {
|
|
x: 90,
|
|
y: genRanNum(30, 60)
|
|
};
|
|
}
|
|
|
|
function drawTrees() {
|
|
// remove first sprite if offscreen
|
|
let firstBackgroundSprite = backgroundArr[0];
|
|
if (firstBackgroundSprite) {
|
|
if (firstBackgroundSprite.x < -15) backgroundArr.splice(0, 1);
|
|
}
|
|
|
|
// set background sprite if array empty
|
|
let lastBackgroundSprite = backgroundArr[backgroundArr.length - 1];
|
|
if (!lastBackgroundSprite) {
|
|
const newSprite = generateTreeSprite();
|
|
lastBackgroundSprite = newSprite;
|
|
backgroundArr.push(lastBackgroundSprite);
|
|
}
|
|
|
|
// add random sprites
|
|
if (backgroundArr.length < 2 && lastBackgroundSprite.x < (16 * 7)) {
|
|
const randIdx = Math.floor(Math.random() * 25);
|
|
if (randIdx < 2) {
|
|
const newSprite = generateTreeSprite();
|
|
backgroundArr.push(newSprite);
|
|
}
|
|
}
|
|
|
|
for (x = 0; x < backgroundArr.length; x++) {
|
|
let scenerySprite = backgroundArr[x];
|
|
scenerySprite.x -= 5;
|
|
drawTreesFrame(scenerySprite.x, scenerySprite.y);
|
|
}
|
|
}
|
|
|
|
function drawCoinFrame(x, y) {
|
|
const cImg = require("heatshrink").decompress(atob("hkPxH+AAcHAAQIEBIXWAAQNEBIWHAAdcBgQLBA4IODBYQKEBAQMDBelcBaJUBM4QRBNYx1EBQILDR4QHBBISdIBIoA==")); // Coin image
|
|
g.drawImage(cImg, x, y);
|
|
}
|
|
|
|
function drawCoin() {
|
|
if (!coinSprite.isAnimating) return;
|
|
|
|
coinSprite.y -= 8;
|
|
if (coinSprite.y < (0 - 15 /*Coin sprite height*/)) {
|
|
coinSprite.isAnimating = false;
|
|
coinSprite.y = coinSprite.yDefault;
|
|
return;
|
|
}
|
|
|
|
drawCoinFrame(coinSprite.x, coinSprite.y);
|
|
}
|
|
|
|
function drawDaisyFrame(idx, x, y) {
|
|
switch(idx) {
|
|
case 0:
|
|
const dFr1 = require("heatshrink").decompress(atob("h8UxH+AAsHAIgAI60HAIQOJBYIABDpMHAAwNNB4wOJB4gIEHgQBBBxYQCBwYLDDhIaEBxApEw4qDAgIOHDwiIEBwtcFIRWIUgWHw6TIAQXWrlcWZAqBDQIeBBxQaBDxIcCHIQ8JDAIAFWJLPHA=="));
|
|
g.drawImage(dFr1, x, y);
|
|
break;
|
|
case 1:
|
|
default:
|
|
const dFr2 = require("heatshrink").decompress(atob("h8UxH+AAsHAIgAI60HAIQOJBYIABDpMHAAwNNB4wOJB4gIEHgQBBBxYQCBwYLDDhIaEBxApEw4qDAgIOHDwiIEBwtcFIRWIUgQvBSZACCBwNcWZQcCAAIPIDgYACFw4YBDYIOCD4waEDYI+HaBQ="));
|
|
g.drawImage(dFr2, x, y);
|
|
}
|
|
}
|
|
|
|
function drawMarioFrame(idx, x, y) {
|
|
switch(idx) {
|
|
case 0:
|
|
const mFr1 = require("heatshrink").decompress(atob("h8UxH+AAkHAAYKFBolcAAIPIBgYPDBpgfGFIY7EA4YcEBIPWAAYdDC4gLDAII5ECoYOFDogODFgoJCBwYZCAQYOFBAhAFFwZKGHQpMDw52FSg2HAAIoDAgIOMB5AAFGQTtKeBLuNcQwOJFwgJFA=")); // Mario Frame 1
|
|
g.drawImage(mFr1, x, y);
|
|
break;
|
|
case 1:
|
|
default:
|
|
const mFr2 = require("heatshrink").decompress(atob("h8UxH+AAkHAAYKFBolcAAIPIBgYPDBpgfGFIY7EA4YcEBIPWAAYdDC4gLDAII5ECoYOFDogODFgoJCBwYZCAQYOFBAhAFFwZKGHQpMDw+HCQYEBSowOBBQIdCCgTOIFgiVHFwYCBUhA9FBwz8HAo73GACQA=")); // Mario frame 2
|
|
g.drawImage(mFr2, x, y);
|
|
}
|
|
}
|
|
|
|
function drawToadFrame(idx, x, y) {
|
|
switch(idx) {
|
|
case 0:
|
|
const tFr1 = require("heatshrink").decompress(atob("iEUxH+ACkHAAoNJrnWAAQRGg/WrgACB4QEBCAYOBB44QFB4QICAg4QBBAQbDEgwPCHpAGCGAQ9KAYQPKCYg/EJAoADAwaKFw4BEP4YQCBIIABB468EB4QADYIoQGDwQOGBYYrCCAwbFFwgQEM4gAEeA4OIH4ghFAAYLD")); // Toad Frame 1
|
|
g.drawImage(tFr1, x, y);
|
|
break;
|
|
case 1:
|
|
default:
|
|
const tFr2 = require("heatshrink").decompress(atob("iEUxH+ACkHAAoNJrnWAAQRGg/WrgACB4QEBCAYOBB44QFB4QICAg4QBBAQbDEgwPCHpAGCGAQ9KAYQPKCYg/EJAoADAwaKFw4BEP4YQCBIIABB468EB4QADYIoQGDwQOGBYQrDb4wcGFxYLDMoYgHRYgwKABAMBA")); // Mario frame 2
|
|
g.drawImage(tFr2, x, y);
|
|
}
|
|
}
|
|
|
|
function drawCharacter(date, character) {
|
|
// calculate jumping
|
|
const seconds = date.getSeconds(),
|
|
milliseconds = date.getMilliseconds();
|
|
|
|
if (seconds == 59 && milliseconds > 800 && !characterSprite.isJumping) {
|
|
characterSprite.isJumping = true;
|
|
}
|
|
|
|
if (characterSprite.isJumping) {
|
|
characterSprite.y = (Math.sin(characterSprite.jumpCounter) * -12) + 50 /* Character Y base value */;
|
|
characterSprite.jumpCounter += characterSprite.jumpIncrement;
|
|
|
|
if (parseInt(characterSprite.jumpCounter) === 2 && !coinSprite.isAnimating) {
|
|
coinSprite.isAnimating = true;
|
|
}
|
|
|
|
if (characterSprite.jumpCounter.toFixed(1) >= 4) {
|
|
characterSprite.jumpCounter = 0;
|
|
characterSprite.isJumping = false;
|
|
}
|
|
}
|
|
|
|
// calculate animation timing
|
|
if (timer % 50 === 0) {
|
|
// shift to next frame
|
|
characterSprite.frameIdx ^= 1;
|
|
}
|
|
|
|
switch(characterSprite.character) {
|
|
case DAISY:
|
|
drawDaisyFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y);
|
|
break;
|
|
case TOAD:
|
|
drawToadFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y);
|
|
break;
|
|
case MARIO:
|
|
default:
|
|
drawMarioFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y);
|
|
}
|
|
}
|
|
|
|
function drawBrickFrame(x, y) {
|
|
const brk = require("heatshrink").decompress(atob("ikQxH+/0HACASB6wAQCoPWw4AOrgT/Cf4T/Cb1cAB8H/wVBAB/+A"));
|
|
g.drawImage(brk, x, y);
|
|
}
|
|
|
|
function drawTime(date) {
|
|
// draw hour brick
|
|
drawBrickFrame(20, 25);
|
|
// draw minute brick
|
|
drawBrickFrame(42, 25);
|
|
|
|
const h = date.getHours();
|
|
const hours = ("0" + ((is12Hour && h > 12) ? h - 12 : h)).substr(-2);
|
|
const mins = ("0" + date.getMinutes()).substr(-2);
|
|
|
|
g.setFont("6x8");
|
|
g.setColor(DARKEST);
|
|
g.drawString(hours, 25, 29);
|
|
g.drawString(mins, 47, 29);
|
|
}
|
|
|
|
function buildDateStr(date) {
|
|
let dateStr = locale.date(date, true);
|
|
dateStr = dateStr.replace(date.getFullYear(), "").trim().replace(/\/$/i,"");
|
|
dateStr = locale.dow(date, true) + " " + dateStr;
|
|
|
|
return dateStr;
|
|
}
|
|
|
|
function buildBatStr() {
|
|
let batt = parseInt(E.getBattery());
|
|
const battDiff = Math.abs(lastBatt - batt);
|
|
|
|
// Suppress flapping values
|
|
// Only update batt if it moves greater than +-2
|
|
if (battDiff > 2) {
|
|
lastBatt = batt;
|
|
} else {
|
|
batt = lastBatt;
|
|
}
|
|
|
|
const battStr = `Bat: ${batt}%`;
|
|
|
|
return battStr;
|
|
}
|
|
|
|
function buildTempStr() {
|
|
let temp = parseInt(E.getTemperature());
|
|
const tempDiff = Math.abs(lastTemp - temp);
|
|
|
|
// Suppress flapping values
|
|
// Only update temp if it moves greater than +-2
|
|
if (tempDiff > 2) {
|
|
lastTemp = temp;
|
|
} else {
|
|
temp = lastTemp;
|
|
}
|
|
const tempStr = `Temp: ${temp}'c`;
|
|
|
|
return tempStr;
|
|
}
|
|
|
|
function drawInfo(date) {
|
|
let str = "";
|
|
switch(infoMode) {
|
|
case TEMP_MODE:
|
|
str = buildTempStr();
|
|
break;
|
|
case BATT_MODE:
|
|
str = buildBatStr();
|
|
break;
|
|
case DATE_MODE:
|
|
default:
|
|
str = buildDateStr(date);
|
|
}
|
|
|
|
g.setFont("6x8");
|
|
g.setColor(LIGHTEST);
|
|
g.drawString(str, (W - g.stringWidth(str))/2, 1);
|
|
}
|
|
|
|
function changeInfoMode() {
|
|
switch(infoMode) {
|
|
case BATT_MODE:
|
|
infoMode = TEMP_MODE;
|
|
break;
|
|
case TEMP_MODE:
|
|
infoMode = DATE_MODE;
|
|
break;
|
|
case DATE_MODE:
|
|
default:
|
|
infoMode = BATT_MODE;
|
|
}
|
|
}
|
|
|
|
function redraw() {
|
|
const date = new Date();
|
|
|
|
// Update timers
|
|
incrementTimer();
|
|
|
|
// Draw frame
|
|
drawBackground();
|
|
drawFloor();
|
|
drawPyramid();
|
|
drawTrees();
|
|
drawTime(date);
|
|
drawInfo(date);
|
|
drawCharacter(date);
|
|
drawCoin();
|
|
|
|
// Render new frame
|
|
g.flip();
|
|
}
|
|
|
|
function clearTimers(){
|
|
if(intervalRef) {
|
|
clearInterval(intervalRef);
|
|
intervalRef = null;
|
|
}
|
|
|
|
if(displayTimeoutRef) {
|
|
clearInterval(displayTimeoutRef);
|
|
displayTimeoutRef = null;
|
|
}
|
|
}
|
|
|
|
function resetDisplayTimeout() {
|
|
if (displayTimeoutRef) clearInterval(displayTimeoutRef);
|
|
|
|
displayTimeoutRef = setInterval(() => {
|
|
if (Bangle.isLCDOn()) Bangle.setLCDPower(false);
|
|
clearTimers();
|
|
}, ONE_SECOND * timeout);
|
|
}
|
|
|
|
function startTimers(){
|
|
if(intervalRef) clearTimers();
|
|
intervalRef = setInterval(redraw, 50);
|
|
|
|
resetDisplayTimeout();
|
|
|
|
redraw();
|
|
}
|
|
|
|
// Main
|
|
function init() {
|
|
clearInterval();
|
|
|
|
// Initialise display
|
|
Bangle.setLCDMode("80x80");
|
|
|
|
// Store screen dimensions
|
|
W = g.getWidth();
|
|
H = g.getHeight();
|
|
|
|
// Get Mario to jump!
|
|
setWatch(() => {
|
|
if (intervalRef && !characterSprite.isJumping) characterSprite.isJumping = true;
|
|
resetDisplayTimeout();
|
|
}, BTN3, {repeat: true});
|
|
|
|
// Close watch and load launcher app
|
|
setWatch(() => {
|
|
Bangle.setLCDMode();
|
|
Bangle.showLauncher();
|
|
}, BTN2, {repeat: false, edge: "falling"});
|
|
|
|
// Change info mode
|
|
setWatch(() => {
|
|
changeInfoMode();
|
|
}, BTN1, {repeat: true});
|
|
|
|
Bangle.on('lcdPower', (on) => on ? startTimers() : clearTimers());
|
|
|
|
Bangle.on('faceUp', (up) => {
|
|
if (up && !Bangle.isLCDOn()) {
|
|
clearTimers();
|
|
Bangle.setLCDPower(true);
|
|
}
|
|
});
|
|
|
|
Bangle.on('swipe', (sDir) => {
|
|
resetDisplayTimeout();
|
|
|
|
switch(sDir) {
|
|
// Swipe right (1) - change character (on a loop)
|
|
case 1:
|
|
switchCharacter();
|
|
break;
|
|
// Swipe left (-1) - change day/night mode (on a loop)
|
|
case -1:
|
|
default:
|
|
toggleNightMode();
|
|
}
|
|
});
|
|
|
|
startTimers();
|
|
}
|
|
|
|
// Initialise!
|
|
init(); |