diff --git a/apps/bmoface/ChangeLog b/apps/bmoface/ChangeLog new file mode 100644 index 0000000000..c503e7b873 --- /dev/null +++ b/apps/bmoface/ChangeLog @@ -0,0 +1,14 @@ +0.0.01: Initial release with BMO character +0.0.02: Added Finn and Jake characters +0.0.03: Added settings menu for character selection +0.0.04: Added temperature unit toggle (C/F) +0.0.05: Fixed settings menu crash, added charging status indicators +0.0.06: Fixed "Invalid Settings!" error with proper settings file handling +0.0.07: Added character randomizer feature with multiple intervals +0.0.08: Fixed lock screen character variable error, separated lock screen logic +0.0.09: Improved lock screen character-specific drawing and positioning +0.0.1: Major release with all core features complete +0.11: Code refactoring and lock screen improvements + +## Attribution +Based on the Advanced Casio Clock by dotgreg (https://github.com/dotgreg/advCasioBangleClock) \ No newline at end of file diff --git a/apps/bmoface/README.md b/apps/bmoface/README.md new file mode 100644 index 0000000000..13a46d8f3b --- /dev/null +++ b/apps/bmoface/README.md @@ -0,0 +1,53 @@ +BMO Face + +A playful Bangle.js watchface inspired by BMO from Adventure Time. Features three selectable characters (BMO, Finn, Jake) with dynamic expressions based on watch state. + +Features +- **Three Characters**: BMO (green), Finn (blue), Jake (yellow) +- **Dynamic Expressions**: + - Normal face when unlocked + - Sleeping face (`-_-`) when locked + - Lightning bolt eyes when charging +- **Information Display**: + - Time (top-center) using `7x11Numeric7Seg` font + - Temperature (upper-left) with C/F toggle + - Steps (bottom-right) + - Heart rate (above steps) +- **Settings Menu**: + - Character selection (BMO, Finn, Jake) + - Temperature unit toggle (Celsius/Fahrenheit) + - Character randomizer (Off, 5min, 10min, 30min, On Wake) +- **Lock Screen**: Light gray background with character-specific sleeping expressions +- **Charging Indicator**: Lightning bolt eyes for all characters + +Character Details +- **BMO**: Green background, black circular eyes, complex layered mouth, dark teal borders +- **Finn**: Light blue background, flesh-colored face, white hood with ears, simple curved smile +- **Jake**: Yellow background, white eyes with black outlines, horizontal pointed jowls, oval nose + +Testing Commands +Use in emulator console: +```javascript +// Test lock state +Bangle.setLocked(true); +Bangle.setLocked(false); + +// Test charging state +Bangle.setCharging(true); +Bangle.setCharging(false); + +// Test character randomizer +Bangle.emit("lock"); // Triggers "On Wake" randomizer +``` + +Installation +Upload via Bangle.js App Loader or manually install the files in the `bmoface` folder. + +## Attribution + +**Character Inspiration**: BMO, Finn, and Jake from Adventure Time (Cartoon Network) + +**Code Base**: Based on the Advanced Casio Clock by [dotgreg](https://github.com/dotgreg/advCasioBangleClock) +- Original template: [Advanced Casio Clock](https://github.com/dotgreg/advCasioBangleClock) +- Creator: [dotgreg](https://github.com/dotgreg) + diff --git a/apps/bmoface/app-icon.js b/apps/bmoface/app-icon.js new file mode 100644 index 0000000000..b69774dd79 --- /dev/null +++ b/apps/bmoface/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgJC/AD8f/F4vE/wEf/l8vF/4Ef/18/H/8Een18/PzAoMeAoPh+Eenl+/PA+AdBv/7Aom/AoX+u4FCEYIFEjwFEh9zj4LDuEd8I7C+EdHYIjB+Ec/5NB/gFE/AFBn6k/ADQA==")) \ No newline at end of file diff --git a/apps/bmoface/app.js b/apps/bmoface/app.js new file mode 100644 index 0000000000..088a9b2b29 --- /dev/null +++ b/apps/bmoface/app.js @@ -0,0 +1,552 @@ +const storage = require('Storage'); + +// Try to load fonts, but don't fail if they're not available (emulator compatibility) +try { require("Font6x12").add(Graphics); } catch(e) {} +try { require("Font8x12").add(Graphics); } catch(e) {} +try { require("Font7x11Numeric7Seg").add(Graphics); } catch(e) {} + +function bigThenSmall(big, small, x, y) { + g.setFont("6x8", 2); + g.drawString(big, x, y); + x += g.stringWidth(big); + g.setFont("6x8", 2); + g.drawString(small, x, y); +} + +function getBackgroundImage() { + // Cartoon face background - we'll create this + return null; // Placeholder for now +} + +function drawSmileShape(x, y, width, height, thickness) { + // New approach: stamp small circles along an ellipse arc to get + // naturally rounded ends (no polygons changed elsewhere) + var startAngle = Math.PI / 5; + var endAngle = (4 * Math.PI) / 5; + var step = Math.PI / 40; // small, keeps change minimal + var rx = width/1.57; // match previous horizontal scale + var ry = height/2; + var r = Math.max(1, thickness/2); + for (var a=startAngle; a<=endAngle; a+=step) { + var px = x + rx * Math.cos(a); + var py = y + ry * Math.sin(a); + g.fillCircle(px, py, r); + } +} + +function drawLightningBolt(x, y, width, height) { + // Draw lightning bolt using two opposing acute triangles + // x, y = center point + // width = how wide the bolt is + // height = how tall the bolt is + g.setColor(0x000000); + + var halfWidth = width / 2.5; + var halfHeight = height / 1; + + // Upper triangle (pointing down-right) + var upperTriangle = [ + x, y - halfHeight, // Top center point + x + halfWidth, y, // Right middle point + x - halfWidth/2, y + halfHeight/2 // Left lower point + ]; + g.fillPoly(upperTriangle); + + // Lower triangle (pointing up-left) + var lowerTriangle = [ + x, y + halfHeight, // Bottom center point + x - halfWidth, y, // Left middle point + x + halfWidth/2, y - halfHeight/2 // Right upper point + ]; + g.fillPoly(lowerTriangle); +} + +function drawFinnFace() { + var isCharging = Bangle.isCharging(); + + // White hood ears + g.setColor(0xFFFFFF); + g.fillCircle(30, 20, 22); // Left ear (x, y, radius) + g.fillCircle(140, 20, 22); // Right ear (x, y, radius) + + // White hood behind face + g.setColor(0xFFFFFF); // White + g.fillCircle(85, 82, 85); // Hood circle (x, y, radius) + + // Finn's face (flesh colored circle) + g.setColor(0.95, 0.8, 0.7); // Flesh color + g.fillEllipse(150, 100, 20, 10); // Face circle (x, y, radius) + + // Outlines + g.setColor(0x000000); + g.drawEllipse(150, 100, 20, 10); // Face outline + g.drawCircle(85, 85, 85); // Hood outline + + // White squared bottom for hood (behind everything) + g.setColor(0xFFFFFF); // White + g.fillRect(2, 102, 168, 180); // Squared hood bottom (x1, y1, x2, y2) + + if (isCharging) { + // Lightning bolt eyes when charging + drawLightningBolt(32, 55, 12, 20); // Left lightning bolt + drawLightningBolt(139, 55, 12, 20); // Right lightning bolt + } else { + // Normal circular eyes + g.setColor(0x000000); + g.fillCircle(35, 55, 10); // Left eye (x, y, radius) + g.fillCircle(135, 55, 10); // Right eye (x, y, radius) + } + + // Curved smile using arc + g.setColor(0x000000); + // Draw curved smile: center at (85, 100), radius 20, from 0.2*PI to 0.8*PI + var smilePoints = []; + for (var angle = 0.2 * Math.PI; angle <= 0.8 * Math.PI; angle += 0.1) { + var x = 85 + 20 * Math.cos(angle); + var y = 60 + 20 * Math.sin(angle); + smilePoints.push(x, y); + } + g.drawPoly(smilePoints); +} + +function drawBMOFace() { + var isCharging = Bangle.isCharging(); + + if (isCharging) { + // Lightning bolt eyes when charging + drawLightningBolt(32, 55, 12, 20); // Left lightning bolt (x, y, width, height) + drawLightningBolt(139, 55, 12, 20); // Right lightning bolt (x, y, width, height) + } else { + // Normal circular eyes + g.setColor(0x000000); + g.fillCircle(32, 55, 10); // Left eye - moved up and left + g.fillCircle(139, 55, 10); // Right eye - moved up and left + } + + // BMO mouth structure - all elements follow the same calculated curve + // Black mouth outline + g.setColor(0x424242); + drawSmileShape(85, 86, 40, 20, 29); // Black smile outline + + // Inside of mouth (dark green) + g.setColor(0x225c27); // Dark green + drawSmileShape(85, 85, 43, 20, 20); // Dark green inside smile + + // Tongue (medium green) + g.setColor(0x474747); // Medium green + drawSmileShape(85, 99, 40, 10, 6); // Green tongue smile + + // Curved white tooth line (smile) + g.setColor(0xFFFFFF); + drawSmileShape(85, 80, 50, 12, 4); // White tooth line smile +} + +function drawJakeFace() { + var isCharging = Bangle.isCharging(); + + // Black circles behind Jake's eyes + g.setColor(0x000000); + g.fillCircle(45, 63, 30); // Left black eye background (x, y, radius) + g.fillCircle(115, 63, 30); // Right black eye background (x, y, radius) + + // Jake's white eyes on top of black circles + g.setColor(0xFFFFFF); // White + g.fillCircle(50, 60, 25); // Left eye (x, y, radius) + g.fillCircle(120, 60, 25); // Right eye (x, y, radius) + + // Eye outlines + g.setColor(0x000000); + g.drawCircle(50, 60, 25); // Left eye outline + g.drawCircle(120, 60, 25); // Right eye outline + + if (isCharging) { + // Lightning bolt eyes when charging (inside the white circles) + drawLightningBolt(50, 60, 8, 15); // Left lightning bolt (x, y, width, height) + drawLightningBolt(120, 60, 8, 15); // Right lightning bolt (x, y, width, height) + } + + // Jake's jowls - horizontal pointed oval (like an eye shape) + g.setColor(0xFFFF00); // Yellow + g.fillEllipse(130, 140, 45, 65); // Main jowl oval (center x, center y, width, height) + + // Jowl outline + g.setColor(0x000000); + g.drawEllipse(130, 120, 45, 65); // Main jowl outline (center x, center y, width, height) + g.drawEllipse(45, 130, 70, 77); // Left droop outline + g.drawEllipse(105, 130, 130, 77); // Right droop outline + + g.setColor(0xFFFF00); + g.fillEllipse(47, 125, 68, 75); // Inner left droop oval (center x, center y, width, height) + g.fillEllipse(107, 125, 128, 75); // Inner right droop oval (center x, center y, width, height) + + // Black horizontal oval nose + g.setColor(0x000000); + g.fillEllipse(107, 105, 68, 80); // Nose oval (center x, center y, width, height) +} + // g.fillEllipse(105, 105, 68, 80); // Nose oval (center x, center y, width, height) + +function drawCartoonFace() { + var settings = require("Storage").readJSON("bmoface.settings.json", 1) || {}; + var character = settings.character || "BMO"; + + if (character === "Finn") { + drawFinnFace(); + } else if (character === "Jake") { + drawJakeFace(); + } else { + drawBMOFace(); // Default BMO face + } +} + +// Global variables for randomizer +var randomizerTimeout = null; +var currentCharacter = null; + +// schedule a draw for the next minute +var drawTimeout; +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + +function clearIntervals() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + if (randomizerTimeout) { + clearTimeout(randomizerTimeout); + randomizerTimeout = null; + } +} + +// Start character randomizer +function startCharacterRandomizer() { + clearIntervals(); + + var settings = require("Storage").readJSON("bmoface.settings.json", 1) || {}; + var interval = settings.randomizerInterval || 0; + + if (interval === 0) return; // Off + if (interval === 4) return; // On Wake - handled in lock event + + var intervals = [0, 5, 10, 30]; // minutes + var intervalMs = intervals[interval] * 60 * 1000; + + if (intervalMs > 0) { + randomizerTimeout = setTimeout(function() { + cycleCharacter(); + startCharacterRandomizer(); // Restart timer + }, intervalMs); + } +} + +// Cycle to next character +function cycleCharacter() { + var characters = ["BMO", "Finn", "Jake"]; + var settings = require("Storage").readJSON("bmoface.settings.json", 1) || {}; + var currentIndex = characters.indexOf(currentCharacter || settings.character || "BMO"); + var nextIndex = (currentIndex + 1) % characters.length; + currentCharacter = characters[nextIndex]; + + // Update settings + settings.character = currentCharacter; + require("Storage").writeJSON("bmoface.settings.json", settings); + + // Redraw + if (Bangle.isLocked()) { + drawLockedScreen(); + } else { + draw(); + } +} + +function drawClock() { + g.setFont("7x11Numeric7Seg", 3); + g.setColor(0, 0, 0); // Black text directly on green background + // Top-center time + var t = require("locale").time(new Date(), 1); + var tx = (g.getWidth() - g.stringWidth(t)) / 2; + g.drawString(t, tx, 8); + g.setFont("6x8", 2); + g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 18, 140); + g.setFont("6x8", 2); + g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 77, 126); + g.setFont("6x8", 2); + const time = new Date().getDate(); + g.drawString(time < 10 ? "0" + time : time, 78, 145); +} + +function drawBattery() { + bigThenSmall(E.getBattery(), "%", 146, 8); +} + +function getTemperature(){ + try { + var temperature = E.getTemperature(); + var settings = require("Storage").readJSON("bmoface.settings.json", 1) || {}; + var useFahrenheit = settings.tempUnit === "F"; + + if (useFahrenheit) { + temperature = (temperature * 9/5) + 32; + return Math.round(temperature) + "F"; + } else { + var formatted = require("locale").temp(temperature).replace(/[^\d-]/g, ''); + return formatted; + } + } catch(ex) { + print(ex) + return "--" + } +} + +function getSteps() { + var steps = Bangle.getHealthStatus("day").steps; + steps = Math.round(steps/1000); + return steps + "k"; +} + +function drawBorders() { + // Top border - thin dark teal/green line + g.setColor(0.1, 0.4, 0.3); // Dark teal/green + g.fillRect(0, 0, g.getWidth(), 6); + + // Bottom border - thicker bar (no progress indicator) + g.fillRect(0, g.getHeight() - 8, g.getWidth(), g.getHeight()); +} + +function draw() { + queueDraw(); + + // Clear to character-appropriate background + g.clear(); + var settings = require("Storage").readJSON("bmoface.settings.json", 1) || {}; + var character = settings.character || "BMO"; + + if (character === "Finn") { + g.setColor(0.1, 0.3, 1.0); // Light blue for Finn + } else if (character === "Jake") { + g.setColor(1.0, 1.0, 0.0); // Yellow for Jake + } else { + g.setColor(0.35, 0.78, 0.45); // Light green for BMO + } + g.fillRect(0, 0, g.getWidth(), g.getHeight()); + + // Draw borders (only for BMO, not Finn or Jake) + if (character === "BMO") { + drawBorders(); + } + + // Draw cartoon face + drawCartoonFace(); + + // Draw AdvCasio information like the original + g.setColor(0x000000); // Black text + + g.setFontAlign(-1,-1); + g.setFont("6x8", 2); + // Temperature - upper left + g.drawString(getTemperature(), 6, 6); + + // Steps - bottom right + var stepsStr = getSteps(); + var sx = g.getWidth() - g.stringWidth(stepsStr) - 6; + var sy = g.getHeight() - g.getFontHeight() - 6; + g.drawString(stepsStr, sx, sy); + + // Heart rate just above steps + var hr = Bangle.getHealthStatus().bpm || Bangle.getHealthStatus("last").bpm; + var hrStr = (hr && isFinite(hr)) ? String(Math.round(hr)) : "--"; + var hx = g.getWidth() - g.stringWidth(hrStr) - 6; + var hy = sy - g.getFontHeight() - 2; + g.drawString(hrStr, hx, hy); + + g.setFontAlign(-1,-1); + drawClock(); + drawBattery(); + + // Hide widgets + for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} +} + +// Draw BMO's locked face +function drawBMOLockedFace() { + // BMO horizontal mouth slit + g.setColor(0x000000); + g.fillRect(60, 90, 120, 83); +} + +// Draw Finn's locked face +function drawFinnLockedFace() { + // Black outlines on top + g.setColor(0x000000); + g.drawEllipse(150, 100, 20, 10); // Face outline + g.drawCircle(85, 85, 85); // Hood outline + + // Gray hood bottom rectangle (same as white one but gray) + g.setColor(0.8, 0.8, 0.8); // Same gray as lock screen background + g.fillRect(2, 102, 168, 180); // Squared hood bottom (x1, y1, x2, y2) + + // Finn's shorter horizontal mouth slit + g.setColor(0x000000); + g.fillRect(70, 85, 105, 83); +} + +// Draw Jake's locked face +function drawJakeLockedFace() { + // Black jowl outlines on top + g.setColor(0x000000); + g.drawEllipse(130, 120, 45, 65); // Main jowl outline + + g.setColor(0.8, 0.8, 0.8); // Same gray as lock screen background + g.fillEllipse(130, 140, 45, 65); // Main jowl oval + + g.setColor(0x000000); + g.drawEllipse(45, 130, 70, 77); // Left droop outline + g.drawEllipse(105, 130, 130, 77); // Right droop outline + g.setColor(0.8, 0.8, 0.8); // Same gray as lock screen background + g.fillEllipse(47, 125, 68, 75); // Inner left droop oval + g.fillEllipse(107, 125, 128, 75); // Inner right droop oval + + // Jake's upside-down V mouth (^) - two intersecting lines, centered on nose + g.setColor(0x000000); + g.drawLine(65, 120, 85, 85); // Left line: bottom-left to apex + g.drawLine(105, 120, 85, 85); // Right line: bottom-right to apex + + // Black horizontal oval nose + g.setColor(0x000000); + g.fillEllipse(95, 95, 75, 80); // Nose oval (center x, center y, width, height) +} + +// Draw the sleeping overlay version when locked +function drawLockedScreen() { + // Light gray background like LCD + g.clear(); + g.setColor(0.8, 0.8, 0.8); + g.fillRect(0, 0, g.getWidth(), g.getHeight()); + + // Get character setting first + var settings = require("Storage").readJSON("bmoface.settings.json", 1) || {}; + var character = settings.character || "BMO"; + + // Draw borders only for BMO + if (character === "BMO") { + drawBorders(); + } + + // Schedule next update for time refresh + queueDraw(); + + var isCharging = Bangle.isCharging(); + + if (isCharging) { + // Lightning bolt eyes when charging (even when locked) + if (character === "Jake") { + // Jake's lightning bolts in white eye circles + g.setColor(0xFFFFFF); // White eye background + g.fillCircle(50, 60, 25); // Left eye + g.fillCircle(120, 60, 25); // Right eye + g.setColor(0x000000); + g.drawCircle(50, 60, 25); // Left eye outline + g.drawCircle(120, 60, 25); // Right eye outline + drawLightningBolt(50, 60, 8, 15); // Left lightning bolt + drawLightningBolt(120, 60, 8, 15); // Right lightning bolt + } else { + // BMO/Finn lightning bolts + drawLightningBolt(32, 55, 12, 20); // Left lightning bolt (x, y, width, height) + drawLightningBolt(139, 55, 12, 20); // Right lightning bolt (x, y, width, height) + } + } else { + // Sleeping face: horizontal slits + g.setColor(0x000000); + if (character === "Jake") { + // Jake's sleeping eyes in white circles + g.setColor(0xFFFFFF); // White eye background + g.fillCircle(50, 60, 25); // Left eye + g.fillCircle(120, 60, 25); // Right eye + g.setColor(0x000000); + g.drawCircle(50, 60, 25); // Left eye outline + g.drawCircle(120, 60, 25); // Right eye outline + // Horizontal slits inside the white circles + g.fillRect(30, 60, 70, 63); // left slit + g.fillRect(100, 60, 140, 63); // right slit + } else { + // BMO/Finn sleeping eyes + g.fillRect(22, 55, 42, 58); // left slit: y fixed by height of 3 px + g.fillRect(129, 55, 149, 58); // right slit + } +} + + // Draw character-specific locked faces + if (character === "Finn") { + drawFinnLockedFace(); + } else if (character === "Jake") { + drawJakeLockedFace(); + } else { + drawBMOLockedFace(); + } + + // Redraw information in black at same positions + g.setColor(0x000000); + g.setFontAlign(-1,-1); + g.setFont("6x8", 2); + g.drawString(getTemperature(), 6, 6); + + var stepsStr = getSteps(); + var sx = g.getWidth() - g.stringWidth(stepsStr) - 6; + var sy = g.getHeight() - g.getFontHeight() - 6; + g.drawString(stepsStr, sx, sy); + + var hr = Bangle.getHealthStatus().bpm || Bangle.getHealthStatus("last").bpm; + var hrStr = (hr && isFinite(hr)) ? String(Math.round(hr)) : "--"; + var hx = g.getWidth() - g.stringWidth(hrStr) - 6; + var hy = sy - g.getFontHeight() - 2; + g.drawString(hrStr, hx, hy); + + g.setFontAlign(-1,-1); + drawClock(); + drawBattery(); + + // Keep widgets hidden + for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} +} + +Bangle.on("lcdPower", (on) => { + if (on) { + draw(); + } else { + clearIntervals(); + } +}); + +Bangle.on("lock", (locked) => { + clearIntervals(); + if (locked) { + drawLockedScreen(); + } else { + // Check if "On Wake" randomizer is enabled + var settings = require("Storage").readJSON("bmoface.settings.json", 1) || {}; + if (settings.randomizerInterval === 4) { + cycleCharacter(); + } + draw(); + startCharacterRandomizer(); + } +}); + +Bangle.setUI("clock"); + +// Load widgets, but don't show them +Bangle.loadWidgets(); +require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe + +// Initialize current character from settings +var settings = require("Storage").readJSON("bmoface.settings.json", 1) || {}; +currentCharacter = settings.character || "BMO"; + +// Start character randomizer +startCharacterRandomizer(); + +g.clear(); +draw(); \ No newline at end of file diff --git a/apps/bmoface/app.png b/apps/bmoface/app.png new file mode 100644 index 0000000000..cb52e9f1b7 Binary files /dev/null and b/apps/bmoface/app.png differ diff --git a/apps/bmoface/metadata.json b/apps/bmoface/metadata.json new file mode 100644 index 0000000000..5dbda351b9 --- /dev/null +++ b/apps/bmoface/metadata.json @@ -0,0 +1,27 @@ +{ + "id": "bmoface", + "name": "BMO Face", + "shortName": "BMO", + "version": "0.11", + "description": "A watch face inspired by BMO that shows time, temp, steps and HR. Sleeps to -_- when locked.", + "icon": "app.png", + "tags": "clock", + "type": "clock", + "supports": ["BANGLEJS", "BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "screenshots": [ + {"url": "screenshot1.png"}, + {"url": "screenshot2.png"}, + {"url": "screenshot3.png"}, + {"url": "screenshot4.png"} + ], + "storage": [ + { "name": "bmoface.app.js", "url": "app.js" }, + { "name": "bmoface.img", "url": "app-icon.js", "evaluate": true }, + { "name": "bmoface.settings.js", "url": "settings.js" } + ], + "data": [ + { "name": "bmoface.settings.json" } + ] +} \ No newline at end of file diff --git a/apps/bmoface/screenshot1.png b/apps/bmoface/screenshot1.png new file mode 100644 index 0000000000..b12ff07ad2 Binary files /dev/null and b/apps/bmoface/screenshot1.png differ diff --git a/apps/bmoface/screenshot2.png b/apps/bmoface/screenshot2.png new file mode 100644 index 0000000000..5834256113 Binary files /dev/null and b/apps/bmoface/screenshot2.png differ diff --git a/apps/bmoface/screenshot3.png b/apps/bmoface/screenshot3.png new file mode 100644 index 0000000000..f1cbc68721 Binary files /dev/null and b/apps/bmoface/screenshot3.png differ diff --git a/apps/bmoface/screenshot4.png b/apps/bmoface/screenshot4.png new file mode 100644 index 0000000000..f2ba4fd1d3 Binary files /dev/null and b/apps/bmoface/screenshot4.png differ diff --git a/apps/bmoface/settings.js b/apps/bmoface/settings.js new file mode 100644 index 0000000000..c2341d0934 --- /dev/null +++ b/apps/bmoface/settings.js @@ -0,0 +1,47 @@ +(function(back) { + var FILE = "bmoface.settings.json"; + + // Load settings with proper defaults + var settings = Object.assign({ + character: "BMO", + tempUnit: "F", + randomizerInterval: 0 // 0=off, 1=5min, 2=10min, 3=30min + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Show the menu + E.showMenu({ + "" : { "title" : "BMO Face Settings" }, + "< Back" : back, + 'Character': { + value: 0 | ["BMO", "Finn", "Jake"].indexOf(settings.character), + min: 0, max: 2, + format: v => ["BMO", "Finn", "Jake"][v], + onchange: v => { + settings.character = ["BMO", "Finn", "Jake"][v]; + writeSettings(); + } + }, + 'Temperature Unit': { + value: settings.tempUnit === "F" ? 1 : 0, + min: 0, max: 1, + format: v => v ? "Fahrenheit" : "Celsius", + onchange: v => { + settings.tempUnit = v ? "F" : "C"; + writeSettings(); + } + }, + 'Character Randomizer': { + value: settings.randomizerInterval, + min: 0, max: 4, + format: v => ["Off", "5 min", "10 min", "30 min", "On Wake"][v], + onchange: v => { + settings.randomizerInterval = v; + writeSettings(); + } + } + }); +})(back) \ No newline at end of file diff --git a/apps/doomguy/ChangeLog b/apps/doomguy/ChangeLog new file mode 100644 index 0000000000..f5332531a2 --- /dev/null +++ b/apps/doomguy/ChangeLog @@ -0,0 +1,13 @@ +0.01: Initial Doomguy watch face +0.02: Added animated Doomguy face that changes with battery level +0.03: Face animation - Doomguy looks left, right, and center +0.04: Added heart icon in lower right corner +0.05: Added yellow lightning bolt charging indicators in lower left +0.06: Added "BATT" label above battery percentage in white text +0.07: Changed date text color to yellow +0.08: Optimized memory usage with 4-bit color sprites and heatshrink compression +0.09: Added interactive tap feature - tap Doomguy's face to flash yellow and show damage, daily hit counter with persistent storage +0.11: Added temperature unit settings (Fahrenheit/Celsius toggle) + +## Attribution +Based on the Advanced Casio Clock by dotgreg (https://github.com/dotgreg/advCasioBangleClock) diff --git a/apps/doomguy/README.md b/apps/doomguy/README.md new file mode 100644 index 0000000000..ddcc9010e8 --- /dev/null +++ b/apps/doomguy/README.md @@ -0,0 +1,78 @@ +# Doomguy Clock + + + +A DOOM-inspired watch face featuring the iconic Doomguy face that reacts to your battery level! + +## Features + +### Dynamic Doomguy Face +- **Battery-Reactive**: Doomguy's face changes based on your battery level: + - 80-100%: Normal face + - 60-80%: Slightly damaged + - 40-60%: More damaged + - 20-40%: Heavily damaged + - 0-20%: Critical damage + - **Charging**: God mode (invincibility face)! + +### Face Animation +- Doomguy periodically looks left, right, and center +- Animation occurs every 2-3 seconds when unlocked +- Pauses when watch is locked to save battery + +### HUD Display +- **Time**: Large red digital time display +- **Date**: Yellow date text (day of week, month, day) +- **Battery**: Shows percentage with "BATT" label +- **Heart Rate**: Current BPM displayed with heart icon +- **Steps**: Daily step count +- **Temperature**: Current watch temperature +- **Charging Indicator**: Yellow lightning bolts appear when charging + +### Visual Elements +- **Heart Icon**: Red heart in lower right corner +- **Lightning Bolts**: Two yellow triangles in lower left when charging +- **Gray HUD Panels**: Left (battery) and right (stats) panels +- **Hit Counter**: Displays daily tap count + +## Interactive Features + +### Tap to Hit +- **Tap Doomguy's Face**: Interactive hit counter + - Screen flashes yellow twice + - Face shows damage reaction (damaged2_center) + - Hit counter increments + - Counter resets automatically each day + - Hit count persists through app restarts + +## Controls + +- **Tap Face**: Trigger hit animation and increment daily counter +- **Swipe Down**: Show widgets +- **Lock Watch**: Pauses face animation to save battery +- **Unlock Watch**: Resumes face animation + +## Technical Details + +- Uses 4-bit color depth for memory efficiency +- Heatshrink compression for sprite storage +- 16 different face sprites (5 damage levels × 3 directions + 1 god mode) +- Each sprite: 60×60 pixels +- Optimized for Bangle.js 2 + +## Memory Optimization + +This watch face uses advanced memory optimization techniques: +- Sprites stored as compressed 4-bit palette images +- On-demand decompression +- Efficient animation timer management +- Persistent storage for daily hit counter +- Minimal memory footprint for smooth operation + +## Credits + +**Character Inspiration**: Inspired by the classic DOOM game's status bar face by id Software. + +**Code Base**: Based on the Advanced Casio Clock by [dotgreg](https://github.com/dotgreg/advCasioBangleClock) +- Original template: [Advanced Casio Clock](https://github.com/dotgreg/advCasioBangleClock) +- Creator: [dotgreg](https://github.com/dotgreg) diff --git a/apps/doomguy/app-icon.js b/apps/doomguy/app-icon.js new file mode 100644 index 0000000000..400739092e --- /dev/null +++ b/apps/doomguy/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4n/80Zpsb+9XylVBoNNlexoksjdMjdzquUrtPomTqvGiMvom0rtEAC1RiIAVC7EWswAEtwOFjYNFswXBieqAAOvAYWxC4lj1/61X///6nIXC2CjErU2CwdZnNwBomWC5EHzWWHwWTsDJFC5MAgw+BAAP7cIwXKABgXX1IX5gu7AAO2RwwXL8UiAAYXQhwWEkUgC54uFGA4XCjQXEFwwwHC4UZ1wuLkUvfYk7C4MV0dQFxQwF2dhC4MR9WVBANSC5JhChxGBC4UbnYJBCxUi4EAh+VC4cRC6JGBC/4XL1YX/C4sVC6EKC4kZ1wXB+QXK8EAg2rC4dq2AXBh4WJlwNBgFj8IXBi02BAQwKFwIABg0+C4MayAXDGBAuDAAOmC4RGCGBQuDC4kTC4owGFwoXKGAwuFC4cfC4wwElYLFL5QANI5QXPF64Xfgu7AAQjHC5Xj1Wq0cznJ3Qh2pqMRiMVseXC5OWs1r3dm3djyoWBAAMW1RNDAAJHDHoJBBAAIJBAAhNEmejmwXBB4oAQC64=")) \ No newline at end of file diff --git a/apps/doomguy/app.js b/apps/doomguy/app.js new file mode 100644 index 0000000000..7698c6b20d --- /dev/null +++ b/apps/doomguy/app.js @@ -0,0 +1,499 @@ +const storage = require('Storage'); + +require("Font6x12").add(Graphics); +require("Font8x12").add(Graphics); +require("Font7x11Numeric7Seg").add(Graphics); + +// Load settings from storage +function loadSettings() { + return storage.readJSON("doomguy.settings.json", true) || { + faceMetric: "battery", + tempUnit: "C" + }; +} + +// Reset hit counter +function resetHitCounter() { + hitCount = 0; + saveHitCounter(); +} + +function bigThenSmall(big, small, x, y) { + g.setFont("7x11Numeric7Seg", 2); + g.drawString(big, x, y); + x += g.stringWidth(big); + g.setFont("8x12"); + g.drawString(small, x, y); +} + + +// schedule a draw for the next minute +var drawTimeout; +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + + +function clearIntervals() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + stopFaceAnimation(); +} + +function drawClock() { + g.setFont("7x11Numeric7Seg", 3); + g.setColor(1, 0, 0); + g.drawString(require("locale").time(new Date(), 1), 70, 10); + g.setFont("8x12", 2); + // Yellow color for date + g.setColor(1, 1, 0); + g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 8, 8); + g.setFont("8x12"); + const time = new Date().getDate(); + g.drawString(time < 10 ? "0" + time : time, 50, 8); + g.setFont("8x12", 1); + g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 8, 30); + g.setFont("8x12", 2); +} + +function drawBattery() { + // Draw "Batt" label in white + g.setFont("6x8", 2); + g.setColor(1, 1, 1); + g.drawString("BATT", 5, 95); + + // Draw battery percentage in red + g.setColor(1, 0, 0); + g.setFont("8x12", 3); + bigThenSmall(E.getBattery(), "%", 8, 120); + + // Draw lightning bolt (two yellow triangles) for charging indicator + if (Bangle.isCharging()) { + g.setColor(1, 1, 0); // Yellow + // First triangle (left bolt) + g.fillPoly([ + 20, 155, // Top vertex + 5, 155, // Bottom left vertex + 20, 165 // Bottom right vertex + ]); + // Second triangle (right bolt) + g.fillPoly([ + 20, 148, // Top vertex + 20, 160, // Bottom left vertex + 35, 160 // Bottom right vertex + ]); + } +} + + +function getTemperature(){ + try { + var temperature = E.getTemperature(); + var settings = require("Storage").readJSON("doomguy.settings.json", 1) || {}; + var useFahrenheit = settings.tempUnit === "F"; + + if (useFahrenheit) { + temperature = (temperature * 9/5) + 32; + return Math.round(temperature) + "F"; + } else { + var formatted = require("locale").temp(temperature).replace(/[^\d-]/g, ''); + return formatted || "--"; + } + } catch(ex) { + print(ex) + return "--" + } +} + +function getSteps() { + var steps = Bangle.getHealthStatus("day").steps; + steps = Math.round(steps/1000); + return steps + "k"; +} + + +// Doomguy face sprites - stored as base64 strings (NOT decompressed yet!) +// We'll decompress ONE at a time when drawing to save memory +var doomguySprites = { + normal_center: { + width : 60, height : 60, bpp : 4, + transparent : 7, + buffer : require("heatshrink").decompress(atob("u4A/AH4A/AG91ioABoNFAIVRqNXDqNxqlEAAtUisXDZ9yGINBDAMtnstklEHgVRDp0RqI1CDYYACotFisSDptRK4VNDgtBHgUCDpsEig8BDYNNDwUUBINRgodNOwNUoo2CDgR1DqEWPBsBCYSwEiJzBAAMBgsAahlQDocUAIVVDgRYBiEVg47OorRBoNVoMUqhaBHwMFeRtQGIIaCAINUXQRBBJAQ7PHII2BLIIAEHwNQO5w0BZQNEAgNCDoaZCHZxSDAANCpYeEHgNnDpZYBqlC6XSd4O0olNkmyBIJ3OgtkilDDoPS6gcConT6fUT4IdMiCUBpsz6Y8BHoND6c9nohBaJrgBoktmYWBmgeB6Q6BAwKWBHZtUik9mfjDAIZBIIMzmZDBO5lyqg7BAAJ5BLwIBBloDBWYVSm4dJuhKBoruBppYBkhyBkb4BYAIBBo4dKFgNEotRiNVIIImBqrdCSoMUDpYNBJoMBiK5CC4IFBiRABiNEHZgWBkQdCisj6gbBgMiMINBLJd1KQIvBnapBonUmhjCDgNCBQNXHZRSCok9EAIDBnoDBpYIBpaZBi4dKilC2fSZ4LOBdwIFCoj2BoizMHYI1Bnstmcz6YeBAwQdCiqzMpoTDAAXjAgZfCHZd3cINC6g2CAQIjBAoRbBoJ2KAAKzBOYR0CGwMtMQSeCWRQABWYU0GwPUAgLLB6c+EIMkoIdMu8UVAU9mqvCoK3EoQcMu9EklDGYIaBDwVEPAM0Dp9UOQPTAQIbCoXT7sz6lLogdNig7BDwNDPQNDewQ7SO4KsCeYT2D6ktiQdNudEltDolUWgNNLgTRCDht3uVEoYaBAAPU6geC6SzBDp9UppvBAIK3BmgFBOwNFDp13uhQBKINCHAIkDkgcPLQZRBLgS7C6SxOAAZPBWoI3BAQQ6Bo4dRu61CZ4LQBSQIcTu8VOYLwBHgVEi4dTeYTuDoIbUAAk3DTIA/AH4AKA=")) + }, + normal_left: { + width : 60, height : 60, bpp : 4, + transparent : 7, + buffer : require("heatshrink").decompress(atob("u4A/AH4A/AHFRAANUigBBAwVXDaFykNUogAB6QDColRqUnDp91HANEoXTofTD4NBHgNQDh8RDgfS6g7DokVisXDptwqlBC4ckAYUUio7BPJ1xqIVBDYlBqlULIMBg6wPoodBHAdFiIKBgsFsoeNgprBLYglCBIMBso8OiAWBN4KOBSAUVBQMFgNRDpocBqlFDQLMBLodFAwKWOLINBR4IBBDgZgBIAQ7NB4KPBK4TWED4I7BeBo7BC4lE2lCLYg7PZ4ND6VElskoXU2gEBkiFBaJwdB6QABGoICC6fT6lBLJztBoU9mYeCklD6c9nskLJ8BR4JZB6YeB6lNEgM9mieBDptVitQgtEGoIABmZYBmZZBO5osBO4NFgtDG4QDBltViLcCDptBosRgFEmndHQQLBDYNRqg7PqNQgEEaANQDYILCAQIdLNANEoMAkURgsViIiBgMiktEigdMBwJ2BqMioMBqhxBEIMikgqBO5hnBojRCIANEltBOgM9olCFQIdLosUDYPSDYIYBmYECoVCBQJ3NHIPTpskofUWQQZBpr1BoKzMHYPS6YTBnv9mYeBAoLVDDpl0SoM9HoIWCAQI+EosXDpdySoQVBAAQbCHgMtWgIdPodDDINEPQJ1Bmc9DgI7Nu9xCoMj6fUWASxBn09mlEo4cMPALpBGYR9BGoJhBmYlBDp11lstN4Q0BMAPS6g7B6dXDp1CoSqBVoS4BbARZQuNNpoWCLgLyBAwU9WRoACkkkVQVNSwaBBBAIcODoNNCgQeBD4a7CDp8loVDRwM9ZoKbEoIdPu43BNwIAEDwMkDiDxCGwJbCXQK4BoQdRu8UC4IABLAQjBdpw8Fig3BmczHINEqYcSu9ykXSHgctkUnDqaYCAAdHDaoA/AH4A/AEoA=")) + }, + normal_right: { + width : 60, height : 60, bpp : 4, + transparent : 7, + buffer : require("heatshrink").decompress(atob("u4A/AH4A/AHFRAANUigBCisVi4dSitBogADoNFoNXDaF1isUDQUtlskAgI8BisnDhtyDgdD6fSHoY8BqMSDptwqNFCwPS6g6DokUqNVqAdNuNUCgNCO4o5BqNQiAdNkpZBD4KUFitFiEQqwdNiBZBSoYcBojYCgsBgsCDpkFU4MUDIIBBV4QABHQNRgIdMGISqBDYMUIINUDoI9CiIdMgNRqgBBDgZeDHwRZNFwKUBipzBTIp7DO5yRDAAW0W4kQLJpLBGgjyBkkkAQIeBqqzPoVC6QbBHIPS6ktlskosRLJqQBonT6fUHgM9HAO0ns9HaAdBCgIABDYIGDAoI7PqNEmcz6fSAARCBMAKWBHZsBolNnszHoYABmlFQgLRNolVipMBptDHoMz6U0oD1BHYMVo4dLiNEqNVig+BofTpodBQYQgBDph2BojyBisBqlNisAgAaBSoQdMiqJBkUhiBwBoNRgIHBQgNEigdLoNUVAMimkQgNRDoVCkRIDDpYVBCAND6VUKQNNklUij1DLJlFigbB6QTBki1BAgImB6gBBHZhZBV4MtGQPSWYPUmhFDO4MXHZdEls9dQMzd4XTnoADmhZMuSVB6YAE8YkBAoRkCDhQdBigPBLYIYCAQhIBLIIdLu6tBHgIWB6lCoYkBLYT8BqQdMkI7BPAQiBSQR9BLAUnDpl0GYMzc4L1CKYPdn0tEgIcMu91oaWC6XUDoR0BMINNkodNuNNoTGBZIRfBAAb5BHZ1CWQM9OQIDBAQQjCoIdNPARYC7qtBnpzBofS6SUNDoR3BOgdNoh7BEwNEm4dOuIYBNwJzE6VNklFDhwABVYMtPAPUXAPToZEBo4dQigVBaIoGCi4dQu9UDwJ7BoYcBHSaXCaYQAEoixPS4o3B78zWATsPAA0kkiYBmkkK6YdESgZXUAH4A/AFY")) + }, + damaged1_center: { + width : 60, height : 60, bpp : 4, + transparent : 7, + buffer : require("heatshrink").decompress(atob("u4A/AH4A/AG91ioABoNFAIVRqMnDiEUiNEAAVD6gDBosVqNBDp44Cok9AAMkmgeBisUiocOqNUCoNNlskAgI8CqgpBg4dNgsUilEoXSppcCBAIcBgNXLBwxBDwIADilUSoNRqFnDhlwCIJ2BDwIDBG4MRBQMFgMFLRlxCQNVip6BR4LPBA4JYBssQi47MqDtCDQVRZwIACHYNQPBg7CogBBagMFgjQCqMViFRLJh3BqhXBHYKRCAwI/BIYR3Pc4VFSwNRgK2CLwTwNGYUkC4QBB6LWDLgJZNSQLtEAAIjCDoUQSpl1ioSBloYED4hgBSp9C6c9kgfDls9ndEPAKVOolDnoABHAVL6YlBmg7RpszmYXBnYbC+YdBolQO5g7CkgYCHYM0EgPdDgNFiIdLuhoBoLSBLYMkpofBIIL4BihKBDpbDBU4MVqtRqNBoNVqrsBFALgBDpY5CoMRiMQqo6BiERgMSXQIsBLJkUkkSiEhiEBls0qESgMjPAItBShoADAoPT6RFCAAZZMJIMtWAQABeQYABoXTUoIdLGoKvBlpuBDgMz6QcD2S2BSplE2buBdQIDB8czEwIjBntBLJdyDoIYBHAQBCA4IJCltEq4dKu91NoQVCHQXTEAJ7CoMXDpdxCANDDgM0PYM0ps9nzQBWQI7MukUV4Y0BkiZBEoPT6g7Bo4dLu47CKgNFL4XBohaBmlCDht3qkkWIIyBAAVS6g7DDp0U6kj6ZTBdgXCWAMzPgKUMHYh3CLgPSAwfUkgcNPAQ5B6g8C6W0oXS6ZBBDqA0BOwdMAYSVBDqEUGYNCSAPdKwMtlsk6lBDp7oBCwM0OYU0K4KbBk4dPLQLJDolUEgMzDoIcQu90N4JVBkQCBno6Tu9yWoQACWINNkk3DqJ5CHoR4CokXDiR6DZgVCOiQdHAAVFDq4A/AH4A/AAo")) + }, + damaged1_left: { + width : 60, height : 60, bpp : 4, + transparent : 7, + buffer : require("heatshrink").decompress(atob("u4A/AH4A/AHFRqNUigBCitBqNXDqUVokUogADotBDqN1isVDgdD6QDBihFBHKA6BAAM9mlNkgGCDwI9Pgo6DHAQACPANFi47QHgQ4DoNBotFiodOgsBWQQfBqKUCEwMQqMFg46OLQVBDoNUiIlBAAUBPBo7BspbCoNQiFFWIQ6BqBaNiFQHQNFKwIgBgNULQUVsJZNgo6BqiPBOga6BBIJ5BLJsQCwdFkgDBKwLvCEgI7OKILqDAgIfBBAbSOOwVEoQfDAAodOgMFDRMkegQdNNwVC6cz6QbCoW0mc9kh3PLANEls9ns0LoQFBnstoKzORYNBHwPT6m9mm+mcz6fSLJtyZAYfBKwIZBolNA4IoBgQdLulFLIIBBHoc9WAUVqkVgodMcoVRgMRC4NE6gjCEgICBDpo5BisDmcRghUBiNQgMSHwQdLuNBSoUSDoNRotEqsQqNCkg/BDpd1DgMtkk7EANUoXSAYNLmh7BLJsRokj6RyBeYYEEmlFDpg7B6fToVEofS6czAYIKC6h3MuiUBok9AAMzAAIFCno7BkhZMu6NBloTBAAfzHAJXBIoMXDpjRCoQXCHoICC6aBCq4dMiqMCkczOQNEV4J6BLANBDpo8BZYI7BWwgeBPANHDht3ilNkgzCDYVRTwU0Dp9UklNVgRXBolblsjBAKUNaYTuBLIJyB6bzB6fdHYIcODoMtCwIACns7obwDDqFEprwBO4NFqiVCIAIdPDwJZBofUH4I3BofS6U3DqI8CHoPzXAIfBnocQeIUkRoJZBLAK7BkgdSu54CHoNNdwM064dTogdBnpYCEQMnDqZ5BeQTxB6VHDiY9DHIR7BDiwdBHoPS6gdYAH4A/AH4AiA")) + }, + damaged1_right: { + width : 60, height : 60, bpp : 4, + transparent : 7, + buffer : require("heatshrink").decompress(atob("u4A/AH4A/AH4AWqIABotBAIUVisXDaF1CgNEAAtBEwVHDp1xqNUigaCoQCBoo9SqFFDYo7CLgUQDhtygNBLA46CAAJYQDwMkHgVBqgaBHYU3LBqUCO4dESQQABgsVgIdMiASBio9BqMEohWDgNRgNQdhouBqMUKIMBqg8Eq1VSxsFKwIBBdIYGBSoQ6BDpp2Cog3BK4YjBHwUFgo7PSoY3BAgL4BilRHZ7rED4YHDaINWSptUdQIAGeoJaDDpg1CoYbEEgO0PwazOoktns9GoKRB2k9mfTHgQ7NZIIVBmYeB2kkps9IIdFHZsEonTAAIdBH4M9mhXBfIQ7MqNBqgTColNmfSKgI7DoNRo7QLWYKJBaoNC6fUokRAwI7CogdLqNFiguBGoNUoklisRoERIoQdMG4UiiEAgIkBokAiczkAkBAAMXHZdUilCklFPQcViUybAMkoNFHZcUC4U7LYNEoaUD2b0BEgI7LqsUoaQCpstZ4NNHIL6BaoKzNdwQ4B6dDmfTmfUXAPTa4JZMqNSofSCgIACnofBAgU9a4IdLu6mCDAIVB+c9DQQbBoQNBDhaWBOgIWCkczmgjCmlCQQIdNHYMkCwMk6iTCPIPSDgK+BDplyqnSVIQADps+EoIjBDhgdBoVNGYNEL4NFoktIYNEBIIdOrskO4QzBpkjWYQhBoIdOigbBlqRB6UkdYIdC6UnDpt3RoIACdgPbd4ZYBm4dOujvBSIKsBotNPYIICHZ9xZgSvB6nTEAIIDDp91OIJZCZoIAClskoNHDpxaBCwTvDoZ2CDiA8BoQWBpocBXAM0olCDiB4CHgSxBaAJ5BqYdRu6sBKgMzEIKTBoIcSPIQ9CK4R0RDoskoXSAIIdXAH4A/AH4AGA=")) + }, + damaged2_center: { + width : 60, height : 60, bpp : 4, + transparent : 7, + buffer : require("heatshrink").decompress(atob("u4A/AH4A/AH4AWusVitBooBCqNRq4dSuNUogAEqkVi4cRGQNEilEoQcBoI8CqI5QDgIABns9lskHoZbQOoIVBpocBDwg9Bg4dOuFUK4Mk6fT6XSDgVFiodOuFRDgJZCohZEilRqCYNuMFgiyCSgQcFHh1wq0VHgVND4JZBDgIABgIdNusBgodBoI7DDYQ7BqC0NuEQCYNUoNEgLXBSQIABgNWHZ4dBVYIaCoNUAYJ3CSptwGIRxBDYIjBO4ZIBHZ0FSoQ7BigeBXQJDBeB47CDYLMCEIJ8BAAIMBHZw6BDIMkDAQcDAgJZOuBRCAAokBpacCDpsgSYNEkfT2g9D2k0mlBqEDDplFHYXTns9loeB2XT6fUPAMEDphpBHQMzmfj6Q5BnwGBmlEFYI7NdQNVonUmdNkfSmfUopGBUYIdMoAdCqtD6ZdBps9mlRAAYdMcIMViIwBog1BoICBDgZZOilQqkAgtQiNRqoEBgtAQgJZNoNFZQMRilVHAIfBoJCBih6BHZocBoUkXAIYBTwUiBYIhBDplxokiZoNEltDmfSAwc9olXDpl1iisBCgMz6YABAgIgBoVBDpo7BAAMzDgICBEIb4BosXDpl3kgdBnpUBDwIaBoc9BYIcOu8lNoMzEANNn0tqlNmfUklHDpwvBofT6lEkY6B4hDBmlEDqdCVYNNpcUTgM0ppZQoSNB6h2CoS7BAANCWRoACHYXS2jVBldC6QHBok3DqDmBplVokUqnEkizCDh93iiNBkm0d4Q+BofUoIdQujsB6VC7ocBki6CDiF3uTwBGwJTBoidBoYdSHgQ3BTIL2DWKC1FVwLSBTgNCDiaXC6c9SoUkHShbD6XUdaQA/AH4A/AFQA=")) + }, + damaged2_left: { + width : 60, height : 60, bpp : 4, + transparent : 7, + buffer : require("heatshrink").decompress(atob("u4A/AH4A/AG91isVoNFAIVRqNXDqVxCwNUqlEAAIfBDqVQHQMUqlBDoQ+BBIMnHKNFDQQAFqNQDp1wGAIVBoVEls0AYNEigLBSaFRGgVNmlNnpcDHZ1wiFWWII7B6fUkhACLINRg46NgsBqkUCwMtknSHQUUilQWxo7BiEVZQIZCHYIcBqg7RssBoLQEDIIADDpo7Bd4NUHoKPBa4KwBAAMFi47NgMVgIxBiNEisQAoNVDoMQHZwcBgsUoo9Ba4NRL4JZCSpo7BgoZBK4J1BdQK6BLQQ7PsrvCSoY9BqlEXoI7PJoIaCZoIhEEQI7PLAVCDAYAEHZ52BGgNNlskDYkrDoI7NoDJBNwMj6fS6QbBoW0ns0orwNVgKtBqlNns9HgW0n0z6VBgJaMJYNUosQilDHgWy8cz6c9osVHZkUSgNBopcBmYfCnvSosBawI7MqhZBF4MVHgPUodEmk0qNVqI7NaAUViFQCwLvBotE6IpBHZx2BolAgEEoofCisAgFEqsBDptFGgIABiklLYNEIYMRoSCBoI7NKAIABkKbBLIQDBawR3NulEkk9mgUBpstmdE6QGBlskHRgdBilNknSc4LqBdgUyegPRo4dMuqVBHoUzmYCBEIU9LQJYMAANxKwUz6lMoYbBA4RcBDhp4CNoIdBolD8fS4lDQINFDp11DoMjGwIdBAYVE6YlBDp13ok06ZQBnstVwPEokzmlBDqHUG4PUKYM92lCnoHBk4dQkgdB7dLSAM03oHB6U3Dp4eBcwNEqlFotMO4JfBHaF3qQVBkm0oY4BoXSHYIdRilNSQUzmc9AAIGBDqLTBSYLKBAAVDMIIcQAAN0LQXUD4IcBXoIdSutNkfSkczawUko4dSPIc9O4VEoQcTagVC6avBOqg8ESgckoIdWAH4A/AH4AFA")) + }, + damaged2_right: { + width : 60, height : 60, bpp : 4, + transparent : 7, + buffer : require("heatshrink").decompress(atob("u4A/AH4A/AHEikkiAQskDqdVitFotEAQNBA4IbRusVitBogADqIACq4cOuURDQgADoI+BqMnHR45CoVD6QcFqMHDp0UDgXS6ktlskD4cVi4dNiNVDwU9DYNCDgkVgQdNJgJxC6XSpo6CogcBiFQDpsEilUig3CAAUUitRqFRgodNCQNFijtBG4bRDgsBDpsBCQNUiskoKuCAAVQLJ9Rqo1Bio5BgoFCAAVRiAdNCwJYCCoIYBqixBHoVWO5wdBoLLBqlRgNULgLRCO5w7BqgcBV4NEbIcVEAJ3OOgQACEIIfBXAkVHZ0RojuFAAdFoI7OiA2BAA8kMQJZOZQVC6c9mgYBIIO0AwJaCojuMVYND6cz6XUkgcB6XT6SbDHZgOBnvz6YXBpdNn0koEEHYKcBHZgABHoUzH4NDKwL4CqNUWhjDCqsUps9ofUAYKSBqoCBqNFHZxrCoK4BoNAgsAFQRaBHZrSBoEAXAMUiEFokAgC5BSptUqg5CGIVFTwNLiJ3BoNFLJikCokhNwJZCqkRegQGBHZl3ilCcoIABWoIECbYMtEAIcMLQITBkXTnoABls9mi5B6Y7BDppZC789mffmYAC6XUBYNVDpt1KoM9CgMznzwBppdDo4dNukUK4JtB6YZCEoM0AgMXDpx3BCgVNEIPESgQ7QuNUSIY9B6kUEIM0oQ7PO4PT6SrBEIMrpazBmlNoQcNHYNEkfT7ZyBHoKVBAgNCLBx4C6XSovFosU4m0ki7CDh4dBOoNEoVDoR5B6RiBDqVC6VLlczmfT6dNXYNBDqLSC6hTBAALQCDiAABWgTvBDgLWCSaAACqg1CLAJ1B6dEDqd3igeBO4JeBolXDiZ6CG4IAB6gbVTAZ4BaIIdXAH4A/AH4AGA=")) + }, + damaged3_center: { + width : 60, height : 60, bpp : 4, + transparent : 7, + buffer : require("heatshrink").decompress(atob("u4A/AH4A/AEdyCidRqNUoICBokVAAIbRuoUBilBolFogACEYNXDp8hGwQADkgiBqlFiMnDp0FiocF6Q6BokUHiBXBDYVD6QcBHQNRPYMHLCVC6XT6YeCop4BqSUPK4ctmYdB6lBTgUQDptwqITBoVNnoACkgbBaiFxqh3BD4NDLIjZBHZ5ZBNoIXBHgMtHQIABqo7QVAQ9Bmi0CHgMUiJ3RAAI7CHINC6jSBHYMFHZ9RqsUpZVCEAURBYNQHZ1QiDTEHYZ1BgMBHZ0FspZCPYchoJkCHZ11sISBqjTBoIDBoruBiALBSp9QG4LTBAgYcDHZw5CDANRHQJ8BHQIKCiFCDhd0F4NEEAVFEANUAQIDBX4IdNCoQ0Bio7CSwKxBH4MRDpoZBWAQABOoNUpdD6XSHYNEDplQilC6SsCqjOBok9AAIFBiTuMLINDCYKxCokEonTAAJGBqhZNC4JaEAoQACHYKVPoq0BqFVisRooZBqT3BXgIdLuQdBoOyqsAZgIfBLwNbIoUiDpd3FwIAB2lLqqzBrkcpckLQQcMu52BloSBCoPTmfSogKBoRZBDpoZB6ckGYMjV4QeBoVNkg7PqVN+c9mY6BmYEE6kVDpt3GIIVB6fULIQCBnphBq4dOqlEoZVBAQM+klNns9MQMXHZ4dBns8TQIZBpYHBBYNHDpypBGgNEqo8BotU7s0XoJZPukUoStBqI1BqtUkfUQYM3Dp11oI7BknFqkUqtLpstkh2PHgVC2m0qNVqPB4lLoVFDiA8BKoJWBPoMcim0OwIdRuNEpYACldEDgMkHaTxCnpYBqtVoVE6KTPeQvTplCoScBkjsPDo0k4Wynu02XUDqqXBqlNmlFokTDijUCrkU6jRBDiy2BqPCoNciIdXAH4A/AH4AFA")) + }, + damaged3_left: { + width : 60, height : 60, bpp : 4, + transparent : 7, + buffer : require("heatshrink").decompress(atob("u4A/AH4A/ADV1isUqkVoNFoNRqIcSqNUolEoICDosVisXDqEUDQQADHgIBBq5XQDYs9ktEEwNVHh91igaCoVElpaCotEisQk4dNuJ1CoXTAAIjCoKbBiA7PKAIXBoYeDPwMUqNVgIdNio0Cls9AARbBoq0BgNQDpsEO4dNmY6B6SVCqNQiDtOKAIBBknT6XSoYkBd4MQqI7ORQI9B2dNlpYB6VUoNQqMFHZxsBWYPUaQNC6VLaQI7BO50BqNRqlNklNKwMkolVLAVhHZwABDAQAC6jQCqEVLJ0FgAdBAANBihbBSgTQBWZ0QCYIdCTIVBZ4QeBqqzOqx3BDAVVDoNUDgLSCO6EUHAIECWAIABPIMFm4cMgsUio4BG4JeBdgMVgghCoQdLCII3CdAI0BTYJeBMQICBDplRDoKSBDQMEHoLVDAoQ7NKQMkoAEBjslitLls0DoNEmlSShtFF4NFiCXBSIM9noeBBQI7PC4NBqodBAAVC6RbCHZo5BZAgdDPAY7OUwMAgtVgsQiIGBplFW4JACDpd3HYMFpbmBLINR4JiCSwQcMHgNEqvCDoIjBilMpdR4g7CDprnCiJOB6cz6kkE4MjSwMVDpsRoUznsjDgPTAAQIBklFoIdNolNmYaDAYM9A4IdBoIdOulDC4IABmfj6hdB7skolFk4dNuNEDQPTogiBPwM9+XUWYNHHZwVBKAIdCSAJeBWQQ7OuoVC6lVps9agNC6Q7Bi4cNLIUtmjyBa4MVrdLIYMUq4dQonS2nBqMVrkUA4NEqI7PPANC2g7BgsVivEpYnBigcPHge0qm0AAMbMQMlDiC0Cog1BoVCpgFDDqN3ZwXF4NUqtTaAJ1QDwg9Cmg5BltHDid3kh4BoTNBDwMnDql1olU3stpkUoIcUAAVVqlCqNXDi93kQABoRXVAH4A/AH4AJA")) + }, + damaged3_right: { + width : 60, height : 60, bpp : 4, + transparent : 7, + buffer : require("heatshrink").decompress(atob("u4A/AH4A/AHNRqNUoNUogCBi4aRuQbBqNEilEAAVBolRDqFxioaBHAIABkgCBIIIdQisVoo4DonT6VFHgNXDp8FKoktmg7CPINBDx8VOoJWCoQ6B6R5DTB1wSYtNns9loHBqNFo6UOgsVLAc9mY8BAwNFilAHaBPBDwc9kk9aQQdOusBZwRaBoY6BptCA4MVHZ7RBCYNEpaTBpq0B2g7BqI7PRQKXBkgaBoR3CDgIdOHYNlHoKyBPQQ8CBIMQHaEFikUOQLzCWQVRqA7OiFQHYQABAYdBFAI7PLAI9BDoa3BitFLAJ3QDgNVZQI2BbANRisFO51yF4IABiNUigGCbANRiAGBiQdLuinCoNVilUOwIBBH4IIBIQIdNcQIWCqNBqhaCTwUQiIdLuIaBCoNEWwNRoVRilLTgVAHZl1oKRBok0HgVNitUoXT6QrBig7MHINUlszKYMQC4MVofTns0opZMuiTBJ4VEZQMReYaCBSp4ACH4MBqFQA4VVFQNFLJgdBHYJxB4MAgJ5BqFVqEAbIIdMHgbnCpjtDqPCooFBDhgdBWQMtkpvBokkmlEKoJHBoodNuoYBnskGYOzV4M9MQKYCDptxG4PTmcz2fkmc9AIUz2hZOu4vBd4I3Bls+mfdmYmB6lHDpx3BoQVCoZZBmgGB6dEDp7nCkYVBPgKVBlohBokXDpyvCHANFqlD6iaBMAMkq5ZQilNnodBps0ovEEoQ7PuVUqlC6VEqsUqtRoW9lsVDp48BitLpdMitRitcQISUPAAMVoIVBoNVitFoO02ktHSF3uKXBHgNbpcrpg7CDqI8BZYKSBolFqOyPwIcRu8iZwXEiJcBlskoQdSHgRbCnu06Y6TPIlULgIiBq4dUuUiolboXUqsyk4dUAANRqqVBqobWAH4A/AH4AG")) + }, + critical_center: { + width : 60, height : 60, bpp : 4, + transparent : 7, + buffer : require("heatshrink").decompress(atob("u4A/AH4A/AHFRqNUoICCosViobRuoUBilBolFolEAgIjBq4dPuI2BDIIACkghCisXK6I1CAAXSDgMULgMHDp0FigaCoXSoZaCD4JaQOoIdC6YAB6gFBqNBoNSSh46DoYdB6RgCqlRqAdNuFRVwVNnszDwgLBiKyOqhZBoJ2BDYVNSoJZBHZxZBCQJaCDoKXDqNViruPNoI9BnstAAJ/CFIJ3QAANUKgI+Com0ioABgI7PAANEkQdCpskolFDoMQHZ1QCQMUHYYcBLAIcBLJ1xgtgHYIABoJ7BaAQACWZ1hVANUZYI/BDgJYBqFRHZxLBgsRc4QiBolAWAJ3PugQBoI7Big2BiNUA4IiBHYNCDplQiNEgMVqIdBgpABgsFqo7BoYdMiFUqgxBDoT0Bio7CLgNEDpjtCqsFLwMFgIdBgtUpgIBLJo7BcwLICEAMUqPBqlKIYMUaBgOBoXBqsQLYUEitFqVLFIMSLJqwBWYI7CogABBQLzBM4LRNVYNMiNQVgK6BEoVBHYJ3NGAISBqEAHgNQioFBrY5BoodMu4bBoSUCKgQDBaIJdBoIcMu8UokkovBCoMkmXUbINUoQdPHYRsBolD6cz6dE4PBBQNFHZ1UnsrkfTnocBAQOzoc9kkVDpt3KoO+2c9HYPz6XT6W9mlEo4dOPAU9nstmc+mhaBoR7Bq47QppQBAYQHB2nCAYMXDp10olLofUqhaBoscDYIdTltNpi1BnsVqnB4XSoR3PuIwBpcVdQNRqtRDoJ2QHgdUpdVqtcqvB2gnBDiA8C4W0qPKokcXgPCqIdRutEC4PE2my2gDBigdSu8V4mzoNEplUawPBDiV3uUUWgNLa4VEWB4AFGwNLoQeBptLWKJ5FqPEHYNCDioABktcjdbqjOSag1R4LuBWCYA/AH4A/ABQ")) + }, + critical_left: { + width : 60, height : 60, bpp : 4, + transparent : 7, + buffer : require("heatshrink").decompress(atob("u4A/AH4A/ADN1isVoNFilUilRAAIcRCYNUolEoICBooEBqsVi4dPisUDQQADilFIQIdPK4IbFmgeDMYMnDptxK4VEklC6XUDgJ6BisQDp11HAfTAAJaDqlRqBaNuBYBig6Bls9AAMkS4I7BgNQHRsUDgNEps9mfT6XSSgRZBio7NOwlDDgVNA4KUBqMAShrtCHoJ2B6VEobYBoNRgo7PoJ5BWINNPINEpdUHYMQsA7OHgMUpdElrTBMIUVqC0BWZwABDYMtTIUk6QoBWYMFWZsBHYQbBOYNC6kkilVgNViA7ORAJTCDoICCHYNQiDvOgNlDoTpBH4LtEAQI7NiAxBDALKBAgIcCqoCBqw7NC4J4CDIJABEIIcBioMBm4dLJgIVBijzBEQVBSINVqgCBoQdLcINFAQNRoFVgsUbQUVigpBDpgQBolVLgVQDAQ5BotBqjZBDpYTBolQDQLWBLIJzB4NMikdmlSHZsVoo8BD4R/B4NLmjbBoo7NJYKqCWQSzBrhkBokUoJ3NC4NcWwUBDANEFAQABio7NB4MAiu0gpaBiEAqu0oI7BSpt1HAKOBokReIdVLIQABDhd3uIPBZIMVqgYBEoNVDwcUDpl1HANFPQMtmc9nskrkbofSPgI7Ooc7oez6fTmYCB2VDEINFoodMukUCYM9n09mfjDoPS2YdBoNBDpgeBCwIACHQPUklE7s0olUk4dNLQJXBRgPTGwNE2lC6lEoIdOuh4B6fbSwIdCpdEAYNFHZ9D6k9oNUVoNFrlFog7BiodOuozCdINUAYNR4PCMINHDhp3COAPBqsViodC2h2QAANElgeBDQIbB4m0IANCDh93ihvBokbLoO1oIkBokSDqCnB2m04lCDIIFBqlBi4dQu8Vok0oPEqnEoi6BqIcReIVE5dC2VLpsk4KTQAAVyDoNElZYBkiTSagu1D4KbBoNXDql3otV4nE4PBDapbCkQADDq4A/AH4A/AAw=")) + }, + critical_right: { + width : 60, height : 60, bpp : 4, + transparent : 7, + buffer : require("heatshrink").decompress(atob("u4A/AH4A/ADVxoNBokkilCokUigdTksRqkUD4IABikVDiVRqIcBqgcColFokVi4cRCoIADlskH4VRk4dOgNRDQR0BDgPSolBqkVDp9Rqo5DpodBklEPwQdOuEQiqOBK4Uz6fULAQBBDpt1godBLAU9AAJ4DqkQHZ0VcwJYCno7B6YGBoKhBHZyVBGQNBoQeBSwTSCLJw7BO4UUps06fSO4O0ilRoA7QoqMBoXSpskPgIABFQI7Pso9CdYJ7CaQJYBqA7PWgMUqgYBAAIDBSgIABO6BbBDYSaBAANFilQaJ11g0BqFUOIcUpcUHYILBHZ5NBDINBooDBolRqsQqqVOupZBD4NUGwVVWAQABLJt0RIQCBqlUHYK4BqMFHYUXDpgxColQisRoAXBii8BipbBq4dNGQNEqpWBoNQIAJhBBANRDplxRgIBBqFQiNQqsBqkBrkV4IdNujrDoo0BKgJ9BotFiIEBDpo5BAAPBKgMQaIVEqlV2qVOopZCjkUgJYBEoVViKjBWZ1BDwNLosAgKPBqG0iEAqhZNWgYwBDQJ+Be4VVqgrBDhh4ESoIiCkgkBqsUoNFDpt1C4M7SwIEBmc9loGB4ghBDptxG4PTmdDnoABmYGB2kzkhZOu60BlobCDgIDClsz6lDDpyzBolD6YCCHYPSpYCBk4dOqiWB2k9SQI5BolLIoMkDp8UYwNNkfEqnT6hEBpkjogcOu7NCnlNoNUls0qvE4lNaBwABirrCpdV4NRqtViscklHDp4eCWwNBqodBjkcpXSDqcsjdMiFRovB4W1ogdRuMVHYOx4my2lEjksoQcQDwVF2jRBAAO12QDBDqVyqtD6XE2tMijxBHad3iK2Dkm06VMDiZaBqtE5dEO4KTSAAtEawXBm4cWaoNV4PB4ocXAH4A/AH4AjA=")) + }, + godMode: { + width : 60, height : 60, bpp : 4, + transparent : 7, + buffer : require("heatshrink").decompress(atob("u4A/AH4A8qIABqkUAIUVAAIbQuVCisUogABloDCosVkUXDp1wio5CoXT6VCDoI8CDp11io0Cps9AYUkAYNBqoeOuEUAAJWBkg5CDYIcCgIdNutRolUogbDoiWBToNWiAdNgJ1CDYYcBXQVRgFVgLOQDgdBaAcQqMBqAdMgoTBigZCijNBAAVQiABBDpguBNwJbBAIJeCoqWBJANVHZw6CoMVoqcBAASXCsAdMJwNBoggBDYiYBBIMVqI7ODAgABpabEqMFHZwbEoVLeYMkewS2BHZ8tnsklstolNknS7c9olRaJpzBDoU9HAUkogGBlskJQI7Pnsz6fULgXTAAI7BLJw7CmYdBGwIACA4JiBopZNgodB78+KYQBBEYPfDoL5BDhbDBqkUitUN4JZCps9otBotFNAIdLLITPCeYzRCDptRqgUDAoVBignDoNRDpgSCp//OgNEndE6nv/80BgIPBDpQrColDn8+OgJ6Bp0j+bRBQ4QdKDgVEp3eG4NCeQXepoLBaQMRDpVFilDofSCYLsCAQMkoXSofUiJZMqlL6bMDnvj6YkBogGBmlFSptD6fTHIQcBAYYAB6lBDpd3ZQMtDghZBLYS7BklBDhd3aIQ9B8ez6ffEYRcDoodMHYKuBKoIEBXYRhCklEWRYABuIVGkixBIQKgCo4dMug2BKQPSIIIABqhhBIYIdOutC6hvBOAadCBANEmkXLKIYBLQK6CoY7B6g7OLISuCGwLQDEgMlHZp4BptD6gyB6XULoVNDoI6NDoSrBOoYADoYjBDp54CGQJVDpstlp9BLBwABKAKQBZgPeAQJDB6VCDh4dBHIMtH4NBLAXTLCAeDGwYBCDgNEDiN3kirBlsjZ4kiDqV3iiXBeIM9mhdBDid3uq2CDgSwRAAlwiruEoIdVAH4A/AH4AkA")) + } + } + +// Animation state +var faceDirection = "center"; // current direction: "center", "left", or "right" +var faceAnimationTimer; + +// Hit counter state +var hitCount = 0; +var hitCountDate = ""; + +// Get today's date as a simple string (YYYY-MM-DD) +function getTodayString() { + var d = new Date(); + return d.getFullYear() + "-" + d.getMonth() + "-" + d.getDate(); +} + +// Load hit counter from storage +function loadHitCounter() { + var today = getTodayString(); + var stored = storage.readJSON("doomguy.hits.json", true) || {}; + + if (stored.date === today) { + // Same day, restore the count + hitCount = stored.count || 0; + } else { + // New day, reset counter + hitCount = 0; + saveHitCounter(); + } + hitCountDate = today; +} + +// Save hit counter to storage +function saveHitCounter() { + var today = getTodayString(); + storage.writeJSON("doomguy.hits.json", { + date: today, + count: hitCount + }); +} + +function animateFace() { + // Randomly look left or right occasionally + var rand = Math.random(); + if (rand < 0.1) { + faceDirection = "left"; + } else if (rand < 0.2) { + faceDirection = "right"; + } else { + faceDirection = "center"; + } +} + +// Helper functions for different metrics +function getBatteryLevel() { + return E.getBattery(); +} + +function getHeartRateLevel() { + var hr = Bangle.getHealthStatus().bpm || Bangle.getHealthStatus("last").bpm; + if (!hr || !isFinite(hr)) return 0; + // Normalize heart rate to 0-100 scale (inverted - lower HR = more damage) + // Assuming normal range is 60-180 bpm + var normalized = Math.max(0, Math.min(100, ((hr - 60) / 120) * 100)); + return 100 - normalized; // Invert the scale +} + +function getTemperatureLevel() { + try { + var temp = E.getTemperature(); + var settings = loadSettings(); + var useFahrenheit = settings.tempUnit === "F"; + + if (useFahrenheit) { + temp = (temp * 9/5) + 32; + } + + // Normalize temperature to 0-100 scale (inverted - extreme temps = more damage) + // Assuming normal range is 20-40°C (68-104°F) + var minTemp = useFahrenheit ? 68 : 20; + var maxTemp = useFahrenheit ? 104 : 40; + var normalized = Math.max(0, Math.min(100, ((temp - minTemp) / (maxTemp - minTemp)) * 100)); + + // Invert so that extreme temperatures (both high and low) cause more damage + // Normal temp (around 50% of range) = healthy, extremes = damaged + var distanceFromCenter = Math.abs(normalized - 50); + return distanceFromCenter * 2; // Scale to 0-100 + } catch(ex) { + return 50; // Default middle value + } +} + +function getStepsLevel() { + var steps = Bangle.getHealthStatus("day").steps; + var stepGoal = 10000; // Default step goal + return Math.min(100, (steps / stepGoal) * 100); +} + +function getHitsLevel() { + // Normalize hit count to 0-100 scale + // More hits = more damage (inverted scale) + return Math.max(0, 100 - Math.min(100, hitCount * 2)); +} + +function getMetricValue() { + var settings = loadSettings(); + switch(settings.faceMetric) { + case "battery": return getBatteryLevel(); + case "heartrate": return getHeartRateLevel(); + case "temperature": return getTemperatureLevel(); + case "steps": return getStepsLevel(); + case "hits": return getHitsLevel(); + default: return getBatteryLevel(); + } +} + +function drawDoomguyFace() { + var isCharging = Bangle.isCharging(); + var faceImage; + + // God mode when charging + if (isCharging) { + faceImage = doomguySprites.godMode; + } else { + // Select face based on selected metric + var metricValue = getMetricValue(); + var spriteKey; + + if (metricValue > 80) spriteKey = "normal"; + else if (metricValue > 60) spriteKey = "damaged1"; + else if (metricValue > 40) spriteKey = "damaged2"; + else if (metricValue > 20) spriteKey = "damaged3"; + else spriteKey = "critical"; + + faceImage = doomguySprites[spriteKey + "_" + faceDirection]; + } + // Draw the face in the middle section (between the HUD panels) + // Adjust x, y coordinates to center the face + g.drawImage(faceImage, 60, 105); +} + +function startFaceAnimation() { + if (faceAnimationTimer) clearInterval(faceAnimationTimer); + // Animate face every 2-3 seconds + faceAnimationTimer = setInterval(function() { + animateFace(); + draw(); + }, 2500); +} + +function stopFaceAnimation() { + if (faceAnimationTimer) clearInterval(faceAnimationTimer); + faceAnimationTimer = undefined; +} + +function flashYellow() { + // Flash between two background colors twice + var flashCount = 0; + var flashInterval = setInterval(function() { + if (flashCount % 2 === 0) { + // First color flash - red background + g.setBgColor(1, 0, 0); // Red background + g.clear(); + // Draw damaged2_center face during flash + g.drawImage(doomguySprites.damaged2_center, 60, 105); + } else { + // Second color flash - yellow background + g.setBgColor(1, 1, 0); // Yellow background + g.clear(); + // Draw damaged2_center face during flash + g.drawImage(doomguySprites.damaged2_center, 60, 105); + } + flashCount++; + if (flashCount >= 4) { // 2 flashes = 4 toggles + clearInterval(flashInterval); + draw(); // Ensure we end on normal display + } + }, 100); // Flash every 100ms +} + +function onFaceTap() { + // Check if we've moved to a new day + var today = getTodayString(); + if (hitCountDate !== today) { + hitCount = 0; + hitCountDate = today; + } + + hitCount++; + saveHitCounter(); + flashYellow(); +} + +function drawHeart(x, y, size) { + // Draw a heart using filled circles and triangle + g.setColor(1, 0, 0); // Red + + // Left circle (top-left lobe) + g.fillCircle(x - 5, y, 5); + + // Right circle (top-right lobe) + g.fillCircle(x + 5, y, 5); + + // Bottom triangle (point of heart) + g.fillPoly([ + x - 8, y, // Left top corner + x + 8, y, // Right top corner + x, y + 16 // Bottom point + ]); +} + +function drawHUD() { + // Left section - Battery area + g.setColor(0.4, 0.4, 0.4); // Solid gray for battery section + g.fillRect(0, 95, 50, 176); + + // Right section - Heart rate area + g.setColor(0.4, 0.4, 0.4); // Solid gray for BPM section + g.fillRect(180, 95, 130, 176); + // Connecting line across the top + g.setColor(0.3, 0.3, 0.3); + g.fillRect(0, 95, g.getWidth(), 93); + + // Draw heart in lower right corner + drawHeart(150, 155, 16); + + // Draw hit counter + g.setFont("8x12", 1); + g.setColor(1, 1, 1); // White + g.drawString("Hit:" + hitCount, 60, 98); +} + +function draw() { + queueDraw(); + + g.clear(1); + g.setColor(0, 0, 0); + g.fillRect(0, 0, g.getWidth(), g.getHeight()); + + // Draw HUD panel first + drawHUD(); + + // Draw Doomguy face + drawDoomguyFace(); + + g.setFontAlign(1,1); + g.setFont("8x12", 2); + g.setColor(1, 0, 0); + var hr = Bangle.getHealthStatus().bpm || Bangle.getHealthStatus("last").bpm; + var hrStr = (hr && isFinite(hr)) ? String(Math.round(hr)) : "--"; + g.drawString(hrStr, 165, 140); + g.drawString(getSteps(), 170, 85); + g.drawString(getTemperature(), 35, 85); + + g.setFontAlign(-1,-1); + drawClock(); + drawBattery(); + + // Hide widgets + for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} +} + +Bangle.on("lcdPower", (on) => { + if (on) { + draw(); + } else { + clearIntervals(); + } +}); + + +Bangle.on("lock", (locked) => { + clearIntervals(); + draw(); + if (!locked) { + startFaceAnimation(); + } +}); + +Bangle.setUI("clock"); + +// Set up touch handler for double-tap on Doomguy face +Bangle.on('touch', function(button, xy) { + // Check if tap is within Doomguy face area (60x60 sprite at position 60, 105) + if (xy && xy.x >= 60 && xy.x <= 120 && xy.y >= 105 && xy.y <= 165) { + onFaceTap(); + } +}); + +// Load hit counter from storage +loadHitCounter(); + +// Load widgets, but don't show them +Bangle.loadWidgets(); +require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe +g.clear(1); +draw(); +startFaceAnimation(); // Start the face animation diff --git a/apps/doomguy/app.png b/apps/doomguy/app.png new file mode 100644 index 0000000000..9fc952f77a Binary files /dev/null and b/apps/doomguy/app.png differ diff --git a/apps/doomguy/data.json b/apps/doomguy/data.json new file mode 100644 index 0000000000..0fcf7dd7dd --- /dev/null +++ b/apps/doomguy/data.json @@ -0,0 +1 @@ +{"tasks":"", "weather":[]}; diff --git a/apps/doomguy/doomguy.settings.js b/apps/doomguy/doomguy.settings.js new file mode 100644 index 0000000000..94b3648fa4 --- /dev/null +++ b/apps/doomguy/doomguy.settings.js @@ -0,0 +1,66 @@ +(function(back) { + var FILE = "doomguy.settings.json"; + + // Load settings with proper defaults + var settings = Object.assign({ + faceMetric: "battery", // Default to battery + tempUnit: "F" // Default to Fahrenheit + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + function showSettingsMenu() { + E.showMenu({ + "" : { "title" : "Doomguy Settings" }, + "< Back" : back, + 'Face Metric': { + value: ["battery", "heartrate", "temperature", "steps", "hits"].indexOf(settings.faceMetric), + min: 0, max: 4, + format: function(v) { + var options = ["Battery", "Heart Rate", "Temperature", "Steps", "Hit Counter"]; + return options[v]; + }, + onchange: function(v) { + var options = ["battery", "heartrate", "temperature", "steps", "hits"]; + settings.faceMetric = options[v]; + writeSettings(); + } + }, + 'Temperature Unit': { + value: settings.tempUnit === "F" ? 1 : 0, + min: 0, max: 1, + format: function(v) { return v ? "Fahrenheit" : "Celsius"; }, + onchange: function(v) { + settings.tempUnit = v ? "F" : "C"; + writeSettings(); + } + }, + 'Reset Hit Counter': { + value: false, + onchange: function() { + // Reset hit counter + require('Storage').writeJSON("doomguy.hits.json", { + date: new Date().getFullYear() + "-" + new Date().getMonth() + "-" + new Date().getDate(), + count: 0 + }); + // Show brief confirmation message + g.clear(); + g.setFont("6x8", 2); + g.setColor(0, 1, 0); // Green color + g.drawString("Hit Counter", 20, 50); + g.drawString("Reset!", 20, 80); + g.flip(); + // Auto-return to settings after 1 second + setTimeout(function() { + showSettingsMenu(); + }, 1000); + } + } + }); + } + + // Show the menu + showSettingsMenu(); +})(back) diff --git a/apps/doomguy/metadata.json b/apps/doomguy/metadata.json new file mode 100644 index 0000000000..c965894c10 --- /dev/null +++ b/apps/doomguy/metadata.json @@ -0,0 +1,23 @@ +{ "id": "doomguy", + "name": "Doomguy Clock", + "shortName":"Doomguy", + "version":"0.11", + "description": "DOOM-inspired watch face with animated Doomguy face that reacts to battery level. Interactive tap feature lets you hit Doomguy with yellow flash effects and damage reactions. Features daily hit counter, battery-reactive faces, animated glances, heart rate, steps, temperature, and charging indicators.", + "icon": "app.png", + "tags": "clock,retro,doom", + "type": "clock", + "screenshots": [ + { "url": "screenshot01.png" } + ], + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "allow_emulator":true, + "storage": [ + {"name":"doomguy.app.js","url":"app.js"}, + {"name":"doomguy.img","url":"app-icon.js","evaluate":true}, + {"name":"doomguy.settings.js","url":"doomguy.settings.js"} + ], + "data": [ + { "name": "doomguy.settings.json" } + ] +} diff --git a/apps/doomguy/screenshot01.png b/apps/doomguy/screenshot01.png new file mode 100644 index 0000000000..f834a3935c Binary files /dev/null and b/apps/doomguy/screenshot01.png differ diff --git a/apps/meseeks/ChangeLog b/apps/meseeks/ChangeLog new file mode 100644 index 0000000000..07131b672f --- /dev/null +++ b/apps/meseeks/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial release with Mr Meeseeks character faces +0.02: Added battery-dependent aging spots overlay, tap-to-cycle faces, temperature in Fahrenheit diff --git a/apps/meseeks/README.md b/apps/meseeks/README.md new file mode 100644 index 0000000000..ecc79412f1 --- /dev/null +++ b/apps/meseeks/README.md @@ -0,0 +1,59 @@ +# Mr Meeseeks Clock + +A Rick and Morty inspired watch face featuring Mr Meeseeks with multiple expressions and battery-dependent aging effects! + +## Features + +### Dynamic Mr Meeseeks Faces +- **12 Different Expressions**: Various Mr Meeseeks faces to cycle through +- **Tap to Cycle**: Tap anywhere on the face to cycle through expressions +- **Swipe/BTN1 Fallback**: Alternative controls for devices with touch issues + +### Battery-Dependent Aging +- **Aging Spots**: Blue spots appear on screen as battery decreases +- **Progressive Aging**: + - 100% battery: No spots + - 80% battery: Few spots + - 50% battery: Moderate spots + - 20% battery: Many spots +- **Transparent Overlay**: Spots are drawn over background but under the face + +### Information Display +- **Time**: Large digital time display +- **Date**: Current date +- **Battery**: Battery percentage with charging indicator +- **Heart Rate**: Current BPM +- **Steps**: Daily step count +- **Temperature**: Current watch temperature in Fahrenheit + +### Visual Elements +- **Transparent Sprites**: Proper transparency handling for clean appearance +- **Stippled Spots**: Light stipple pattern for semi-transparent aging effect +- **Charging State**: Different behavior when charging (no aging spots, no face cycling) + +## Controls + +- **Tap Face**: Cycle through Mr Meeseeks expressions +- **Swipe**: Alternative face cycling (if tap doesn't work) +- **BTN1**: Physical button fallback for face cycling +- **Swipe Down**: Show widgets + +## Technical Details + +- Uses raw Image Object format for optimal transparency +- 4-bit color depth with custom palettes +- Cached spot generation to prevent flicker +- Multiple input methods for maximum compatibility +- Optimized for Bangle.js 2 + +## Attribution + +**Character Inspiration**: Mr Meeseeks from Rick and Morty (Adult Swim) + +**Code Base**: Based on the Advanced Casio Clock by [dotgreg](https://github.com/dotgreg/advCasioBangleClock) +- Original template: [Advanced Casio Clock](https://github.com/dotgreg/advCasioBangleClock) +- Creator: [dotgreg](https://github.com/dotgreg) + +## Installation + +Upload via Bangle.js App Loader or manually install the files in the `meseeks` folder. diff --git a/apps/meseeks/app-icon.js b/apps/meseeks/app-icon.js new file mode 100644 index 0000000000..400739092e --- /dev/null +++ b/apps/meseeks/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4n/80Zpsb+9XylVBoNNlexoksjdMjdzquUrtPomTqvGiMvom0rtEAC1RiIAVC7EWswAEtwOFjYNFswXBieqAAOvAYWxC4lj1/61X///6nIXC2CjErU2CwdZnNwBomWC5EHzWWHwWTsDJFC5MAgw+BAAP7cIwXKABgXX1IX5gu7AAO2RwwXL8UiAAYXQhwWEkUgC54uFGA4XCjQXEFwwwHC4UZ1wuLkUvfYk7C4MV0dQFxQwF2dhC4MR9WVBANSC5JhChxGBC4UbnYJBCxUi4EAh+VC4cRC6JGBC/4XL1YX/C4sVC6EKC4kZ1wXB+QXK8EAg2rC4dq2AXBh4WJlwNBgFj8IXBi02BAQwKFwIABg0+C4MayAXDGBAuDAAOmC4RGCGBQuDC4kTC4owGFwoXKGAwuFC4cfC4wwElYLFL5QANI5QXPF64Xfgu7AAQjHC5Xj1Wq0cznJ3Qh2pqMRiMVseXC5OWs1r3dm3djyoWBAAMW1RNDAAJHDHoJBBAAIJBAAhNEmejmwXBB4oAQC64=")) \ No newline at end of file diff --git a/apps/meseeks/app.js b/apps/meseeks/app.js new file mode 100644 index 0000000000..70a16ce64a --- /dev/null +++ b/apps/meseeks/app.js @@ -0,0 +1,385 @@ +const storage = require('Storage'); + +require("Font6x12").add(Graphics); +require("Font8x12").add(Graphics); +require("Font7x11Numeric7Seg").add(Graphics); + +// Meeseeks face sprites - stored as base64 strings (decompressed on demand to save memory) +var meeseeksSprites = { + face1: { + width : 117, height : 104, bpp : 4, + transparent : 0, + palette : new Uint16Array([65535,24579,24580,22531,65399,192,1,22532,22530,47112,39904,20482,0,9,45952,8]), + buffer : require("heatshrink").decompress(atob("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")) + }, + face2: { + width : 115, height : 111, bpp : 4, + transparent : 0, + palette : new Uint16Array([65535,24579,65534,65532,65502,6368,24580,22531,10,0,22532,29280,27232,1,9,65435]), + buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/AH4AShGABzY4bxAOOHMwoRxBJNHFKDPVMoAF93gHEMOLyntHMMOESpzhHCw5B6DjzVolQHGznfHDI5BOTo4Zfy4A/AH4A/AH4A/AH4A/AH4A/AC8FqvuGFuIwAHFzuZzve6A3q93e9vtqA4ErNd7vdHNVd71d9vdHIfd7kMzPZHNXu647BNII5C93VgcFrKtBHFEO6sCkA5BF4UO7uyn+1OQPuHE8NcINEguZHIfd65yBzOd7vgHM/e6twhdZyrcChvdqvFrNZOVXd6tXM4IBBOQXdrOZzJCBOVPd7ItByudOQMJyo3BHQOdWoQAlh3dzNdGQRyC9vZG4JBBHFAvBN4I4DuAJCAwIICPYKsoOQNdUQIvDPgOZ7PdqA4oF4IADyAJD7OdBAI4qgGV7wvBUItVyvVHFatB73ubVIA/AH4A/AH4A/AH4Ak7Xu93qxXgGl0I93d73e9Xa9XuAoIGBG1MFzvt9Xn7Vau+q09XAYOt1vt92KG8vd9ta7QzB1QCBvQEBu45BBAVe9vpN0Xe7vXFwIAKvV6u+nu9d73QHD9d9uuNIIuBNwI2Fi8XBgIOCvWq9Xl0AdCxBwZrqaBGAenFoURiMXuIDBiK1CuMXHIOlfAIeBhA5UCgcN87fCHAWqUIUXGoQACi0R0IFCHoNn1vtHKoTEryoBOIl2s6kBGwhzCu1204MBuOqsN60vlqAlBwBxVvvnVQZmCszaCHQKjBHYS5DAQQKBjWl1pfC8A4U9ulHAOnMoUWUAI+COAg4CW4OmAAOqCgI5B7oiBxw4RQoXacQOqtRwDHIMWGgVxWwdxi12BgNms1hCgTHB6pgFABkOQgVeDQJbBjQlCsMRAIMWsMXHAayBB4IQCG4QGB1Wt6DmRHAdd7WniOhiNnEYSrBHIMRi45BVQZBBJQoUBs967zmCOR/uHgSpBFQOhuwhBE4IACGggzBWII3DAAQGBixyBLwQCCOR8FcYOni9h0NntQuCVAI4GGwSCCB4KECsOn1XlqAoBOR7kDOQNxGAOhOAyrBu43DAAMXuNxuzzBiI/BjQ5B9pyRVYRyBMgMXs9htUas2mdgQ3Bq5xEAAIVBAAMaJANmjV6coJyRhBJCrQhCi2h0JbBix5BLoN1urvBAAll0ulRYI0B01qvWncqeOAQMN844B1TiBs9xMYet9Xe72qGwWGs2OBAPt9XaHYI5B1Wu7QlBxByS891HAVh042CNwItB92gDY8O13q9xGC1Q+B7AoCwByRgvVu9xbgLdBu9a7pbBGxAAFHQPtxwDB9xFCVZ5yDh3au7MBG4NVrvu9QdOAAeKxHqEYUNHB68E7zLC1Wt7vu1w3SW4/VCSCsCgCMB13tNwPQG7I4B9ISQhDBCgGoxHu1DeOS5rnDHKC+QESOIxyOTHMWIxDAV7wwYKD0O93tRSY4CwCLffgIAB9A4zHYY4JhyiHHEgALhAxFAwI3uNYYACx3uHGQ6Dxw30AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/ADg")) + }, + face3: { + width : 125, height : 99, bpp : 4, + transparent : 0, + palette : new Uint16Array([65535,22531,65400,0,24579,160,24580,8,4,32,9,27264,5,3,2,1]), + buffer : require("heatshrink").decompress(atob("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")) + }, + face4: { + width : 106, height : 85, bpp : 4, + transparent : 0, + palette : new Uint16Array([65535,2,65534,65533,65340,23008,65437,37664,8416,5,31264,0,14528,9,32,12736]), + buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A2zIAB8AqmjPu9GQBAkBj3hjMe+EA6EN6AzghOR93uzoIDhGR9GO8MR98A7sJGkORjG+90RSoY9BqEP30Rzw0BNYJogiNQhhqB9wJDjkCkGLiMRGgQBBADUNyENgHZFQMw9BrBiByCjlCkG4jERiHd6A0eDwPZ35fB9w0BwA0CjeghmL3eLGgIVCGjZTCzMbuyeB3Y0D3e+3g9C32wNMGdNIMRxnO8O+3ewBoMRjcb33rboJnBGkOZiJhB92O8LTCBIXr2Ph8I0g6A0EGYPuFQINCyIKBwMRiMAGQPQab2QhI0CAAPoBoUJH4IzC94wbGg3QGgMbGYPhjAODzI1BBgPwGj6JBGwKfCAAMZwANDhPZzMZjgzgXwiVBAAQPGzPMGcKkGGZAA/AH4A/AH4A/AH4A/ADndGmnQGn4Anho00alsBGmcJyDUFTFo0zNIcNAQOZGmBmCGmXQgEbT2OQh2RGmEJzOJGmMNzOZiA0xzgztGgnd6AztMoI007o0CGdwxEGmWQhudGmHZzPZaV41DTuIA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AhA==")) + }, + face5: { + width : 111, height : 99, bpp : 4, + transparent : 0, + palette : new Uint16Array([65535,24579,65503,22531,61343,7,24580,6,160,47112,9,5,8,10,4,3]), + buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/AH4A/hva1va7vQGt8K1Xa03atpsy6EM1nK1Rvw7sK4t3u915Xd7o3vqu+mAGDOEIiB1XK5gMI7SuGG8EN1orB1XM1otG6wWH5nATsmtLwjVKqo3kSyPsb8BwF6EFCBq4FG8SiJQAhulVIYOMhQ2mMB5uh7poFNxrZj1WtAwesAgeAhGP/EwA4UOgHMHEOq0oECgtwAgUIxGO9e73wHB/fwhmgG8MG096GIP4GwmL/2+u9Vvd3vBuj2tT3d84o2Bh++v42B/83rnF4tVq8AhlQGr4uB/3v/d8FIMAutX32DweACQcD0EN6AzchkA6EI90///4mAMD4uwCw7uB7o2cguw8sA90EgeIx4WOqBue98IuH+8EgnGIxwWNGwJudh+A/cO/8wGwOP8AND1QWH46legH4h+z///xAAB/wMDTRHFIJJuW+HnnA2D/+wG5d8uEKbjsAu8M1mI/H4GwP/hVwG4d/Cgl1qEH0A2dgBWB4Y3C//vhlXBocHu93mEAgvF4EAAIIAghGPNgP7gFaBgus1Wq5lcqEKNr4AEvfv/EA5jOH7vaqEAhWtGsUN7oECrRgLgoRDAEHd1kAh98GxfKNsY3C4u7rg2L1SxHAD0Mq+wBxfaGsoA/AH4AJxGAEL8IxGO3YAF2973/4nAUG/3AGjna4/vxGI+f/n//AAQGC/GP/dc6A3E2+gNDM3utVrc+n8zweDAYIAGBQP//d8GIcHq43XMwP+907nc3mc4mcxGo44Dmfz9ZxDgtVU50NGo+InE/MwM2scziMTGxQAC8c//0wN4V1N5sN7oFDg/o/CaEsw2BmI3BAII6N/+4EIMMqo2N7QFDrfvTgIAFm02iIABHQIACG5foEQMK542LhXMAgV13fj+Y1GNwNhGYY4BcYoHBi1mG4eDEgPauA3Ldgf+9wiETQQrBsY2BUYIACBoKsBV4cWAwcz9/wEoPV2A2Ku9QVAPvNYhhCAYM2G4MWG4oADG4R8Ei1jn3gFQNXGxUOIYMNn0/NooADi0xAAM2NIYAEmxDBAILqCs0+/0wE4NbG5Xub4N7NgpZCNoUTsYHBAgNhAAJBCAgMWG4axCeYP+UwSZBABGrdQNfwY3DAAJkBmw3BAQQmBIgJzDNgTaCJgJBBYAU/3orBqo2Jht1gEHGweDnEzcQgACLwIIGJAI2BNgI3CCQI3B/3QgEFNxXXboPoxH4////GIAAWPHgKwDIoQADAwJyBAQI1BW4MxHoM//YrBuo6BNxFXgEIxH//273w3DAYP/+Z0DHoUxs0TweBIIQxBOAgCBnBfBg9dNxPc2wDBhE6sALEgeD/++9e3m6vCi2ZzOWyJ5DGIQABsw7BiymBD4O1NxMA4oLKAAXG6t79/zwNpGwIAByM4VAIwCAAsTn/ggELNxUFvg3NgGqq/v/9mG4mWVALWCUoQCDn+ObphDButeG5qsBre1G47cGHgc4NwMH5olKhHr9167o4Nhd80tWHAeWnDeCAAIzBHQU4xAXBrlgG5e7uvKVJw4Bnk8OIbfBmcRicxmMzm02mcz/GAgGsExsNNpwTE6+2G4SmCNwQ6Bs1jmf/+EAhVaE6IAQ4fHm1py0YG4szs07/7cBgFVG0S9ButT4ymCU4JuCAoJtCgF3qA3jgEM4ezmeDwYyBAAWD/+DB4W7G0gABr3/FwI1DnA2B2A2CrY2mVII3Bx+IAAX4/8wGwXFHYQAmweO/w6BAQKjCAAPF0A2oAAXj2dz2YIEq/AG1axI242Y/A2b9bbYr3wGzNcq/gDbHvDLEOqtcqAcYh6mXhXM5mq0CKZxyJXGwPdfDUA9w3WhnAGrY3CuAfdAC0I/dVG+kAuu3wAHEhVV9A3s4u/+9XAAN+89XrSoux+3ut3u+72utVOEM1ut7vQcmoA/AH4A/AH4A/AH4A/ADIA=")) + }, + face6: { + width : 108, height : 72, bpp : 4, + transparent : 0, + palette : new Uint16Array([65535,8,65501,29248,27296,25184,12512,224,10464,32,0,8416,4576,6624,7,9]), + buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH3u92q9w0whw0BzOa0AKE32uIII1m9Hq1W73OZBIcKBIOu12wGsuu1erqEFzNwBIWaGgOo3WpGkkLMAOmgUgGomu1OamEG3Rskh2q1WoGoWrBIepzIJB1XgGsmuGwNQg2a9wJC9WpzA1BzXvNc2q1eZUoIJBhWqzepoEGvQ1lNYLOBGoOoH4eZzN7vOZGskK9RqBZ4OZyAJD3IzBy41lMII2BGgOauAJBhOaGQI2BzPwGscA3yiBzOe1IIChp0CzWp13gGsiYB9WZ9wqE3w+B1Xu1w0kAAPu1W59w/GOgOuiI1mgGL5gIGx2X13uiA1nABPdxYzxAEf/+AecDqkP///KmXuGjxWCGyMO9zBgh8eCJ8HGkIAB8KOPhPgGsTaBap40jGu0f+DoNIp4AViLHNGsIhBM4URIp4ZETbX/K4fhCp0edghPDM6nx8IZD3ygNDAQvEGwI3RGQIADBQnuiAcPbAwfBiMecZIwEGQyPCj6FRMg/u8I4BAII7BA4MeA4P/jxeJh77ENh5+KiPxGgIACLZvxap4ADgJ/RRhpqTNgSjHACoeWh71HDtpscDjJsbDbMPbLoA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/ACo")) + }, + face7: { + width : 132, height : 97, bpp : 4, + transparent : 0, + palette : new Uint16Array([65535,24579,65433,65400,65367,7,1,128,22530,8,22531,3,2,35552,0,192]), + buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/AH4A/AH4AuyAMLhINMAE2ZIP40NJxhBzQWh3MzJB1PBTE0G5bE2XZSC3PQI5GJJAAxrJCEJA7H4IAKC5ABH733vG+kG83/BAuP5m83lvIGUL4xBBuoJE3lmAANrIOV+93ut3FqAICgu292AgHrrKCx933vsA9hBD3ZBBwUi7dVIOFc8/n+Ui7nFIIfOs5BC2uQIN++u9+6RBB9ZBD41nYoOM2qODAFi6Bv3AkGPvY3CIINuv2Hs3LIOFct3uu+v+y7D3dm93nv3utmwIOHO8/n+/sIIYJB9wACthAvPIPOw9392H3YJD21mAANu5hBy8/u9F83gJChe2tnMs1s3RBwgvGXQXr34KDu3mQQNrqBBwgHGXANmG4v/9m8514IGMAg+73e28wKF/dV3HwIOUA2tVr430AH4A/AH4A/AH4A/AH4A/ADEF/4ACs/Gs93u/m5e13fL34MB+A+srdb3+P7/f7v3vvdAAX3tvd7HfKAd3H00PFYWPOIvX+EgAgMC7+AkAGBhv97vf73m94hEhOQHKtVqAGE3fvx/d6EAx+NmU0ocgx99ocikUt739oUiknf+kikEN7H+LImZzJCUhOVCwcP23svp1Bkct//96lEmVNvHu6A3B7H3xxRBhuPwEiohMB/7NCIS8FIAt3xsI/p2BkgEBQgI1B72Hx197vY/+IvHo/H+x/9olEkbKBCgJCEqtbIKOZrhGD2+NgUI/EAmkN7+IxH3x4/BAoPux2PAgOIBAWI9CIBbYJGBmBCBrQpC/byFABeVCQe7wEykUPOYPdx/4GQV4vAECxGHxBCCwJFD8+Px/3xtDRIMN2ugNYVZIB8JyoECg1tgT2BFYOP9w6DAAeq1AECwMa1Wh1WKBQRSC/8EolDkGG36zDegbEMCAfM9/QkjEBHYh1BxWo1QABIIWKHgIADiMaSImNQYMIvFs+ByCIKADCgvI9H47BAFxERAAQ/DHIMYY4JDDIIKPDxH4aYP4vG72BBRhJBDze6YAPnEgMYIQ4yB9C3BOwPo93oJAOqIwIBB0INCIAP4BgNbGIWbIKMJrQmBHYiFFAAOO92OHYJFB7vdx9+w4ABBQTaBAAhBCYwJBQSYUFqpBExSDDu9+PoV47vY+4EB6kikUNx6MB/oDBIYWOB4OHaQW7wAxBzRBRhYaCQYhqBvA8BO4WNoc07vnx8DkciknY//dgkN7w/BSYLQDAANc4AxBypBS3WpPwYAD9HQklA73v6dEokwx99oczmcjnv4gSJBhH3w+IxuH9hAC0uL2EAhT3CABcFIImq0Oq12HP4LDBIAJ+BhvogckmUih/wkdCIQMg/sCmdDJoP46lEpuPxWhjTFB0BzEIJlZIIXKIIOK0/o++ONQIpBokikH/gQYCh/9DwcN/4DBI4Pf/oWBokI/CDC2q1FABcOIIe11WoX4QuB+9/ggpBoEAx8AFoP/AAP3/93v/vAQONJoWAIIUt/HB0u7qAMBYhwcBKoUI3eqxGOxp9CHAMDmh2C/+73/4/Hf7+PAIX42tV24PB6/wgFEocA/GHxm12CCBIJ8A4pWCqu7xF4X4R9B//dPYQABMh3Xs1nv/9/vf+/M5egYgRBQ+pBChdb5lnX4MA7vfHgP9/5vBACELxlvK4W724aD/OQDp8FqoEChlbraxBEgX4HySQOtITRyqECTYOpq6oBXpwAThNbuAURQgJCDAEsJqtZCydVrewIE2ZyryDACW1IYL/ZyA/JAAQlXrdV3a3YGhJAaAAMFqtQXC6DJAGxB/TgSC/rbdXADkJBRMLQWsJzIKI2uQIOxCH+qC1IQWVIQsF2tQIOyFCqp9CzOVQW6FEqqHBzObIHLACrJABZQoA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AxA==")) + }, + face8: { + width : 109, height : 92, bpp : 4, + transparent : 0, + palette : new Uint16Array([65535,24580,65470,65468,24579,22531,0,33440,6304,22532,47112,29408,6272,8,6368,25024]), + buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4Ag7vQFlfAA41mtts8A0n91t5ttGo2Gs1sGtHms1m6xjD7vO/1ss1oGs3O91/GwPdBAXmtcDhAJBszWmt0Cl9m5qaCg3WgUj9lus1gGskN43gkUMtnG6AJBs1rmcM4xsBVoYAh61ogZsBFoIsBhtm9+1GgNttpsls1o//mFgKjCGQIHB6wJB4A2kGAIuBw2GtqjBH4IACOoPONk3GAANoTIY9DHAQ1kgHNsxtBG4LPDtptD7p1BUco2BAAKiCgEMawIABbA48fhieBFgVgVoY2CNY/dBQQ2chw2C6wsFFYNtyAVH91t7o3UYZHdGwPgBQwHHC4o2RhoTBGxHZQ64hIGpLxfGwg1QF7vN7vcAwduPjwAM5PcAAPZ6wiChwmNhoOChud7udyAzShHM7ts6vJzNc5ojDGxihD7vd7ICB43uGh4aB5vF5OVrOZyuZ5raQBoUNzIACy3Ws2O9ozMQANZNAOZqtUog4B6xsPGoXuso2DyvJ41mOgPc5ngCwi0B7PZ6ouBAANUotJAgOdUZ4MCh1sqqHCqtFrOVzmcfwXWAIPWttt5mdJAQ2CoNBjI7C5jLFNhnd4o2BquVKoNZLgeZ5IAFBQYAByMUikRiORHAJoC7psOhvVqo2CiMUio2DOwQ8EGgoyBoICDjPNbKXZGoKJBogCBpI1DOwQOBHIo0BoJqBGYIYBimcNgY1LNgtEoiLBAAIfBoMVyuVMwlBrMZMoJnCimRCYIFByttNiXdqtUNYI2DomRqJdBymUTAjQCiNRoIBBAAcV5vAEoI1MIYUJ7lFGgSlCfgVByheBAwQAGpIMBrJrCotWGoSiMUYnFqhrDKoVRqlUqtFotUO4YRBisUIQJ4BO4UV5hoCURpEDww2BNIInCotJqmUqIABHINEFgIACOYUZymRC4MZrlsGqDaE4ooBAAVJqriBAQNEIIJyBWAQCBUgRNDqudGoaiNIonc5OVGAIeBGYQAEGoIrBF4Q1CJINBBwPN5hcFGyFdtosCK4TVBHYIAEBYVZGwLfCokVGoOQGqQQBGwSFB4vEMYJgBGwIEBFYJwGU4QKBzOdGqoREhilBzOZyKSBFoOqAAOkotVAII+Ca4MUpOc7gwEho1QAAvN7vZ6KTBGogABGgJtCNQVEzNt5vVGCxzH7udytcGoNKGwdV4rfE5nM63NqA1dQwOd7rfBoJsE0lcAAQzBAAXAGjw3Es3Nb4YABzmWGQfc4ozhAAkOOAOdAAbmB7nc8AzmAH4A/AH4AphvQG2ndGv41rUWg11hpr/Gv6ZZ7oGGNdovBGAQECTmAADsDUxUwwA/AH4A/AH4A/AH4A/AH4A/AH4A/ADIA=")) + }, + face9: { + width : 113, height : 101, bpp : 4, + transparent : 0, + palette : new Uint16Array([65535,0,65534,65469,24580,31392,29344,65404,3,32,4,9,27232,1,5,8]), + buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/AH4A/AHW72ANLhf/3Y2mE5/7HEu7+ARPhaBNACsP/43QHEkO/aEU8A3fLYIWU92AG+sAzy+RG8kAz7jdG7EA/Y4cG7MADLIddKTQA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4ARhe73YJGhOa8A3qhFP93u+AJEzOZzWNG9OZzsR7vY0AJD9o4BzPYOVEIzPArIuF/Wc5g4BxQ4nh/p48jrMdxyqC32pg81HAOuOE/q6kzkuRiOvIIXo4cjhOZzGJG83+7OQuFJFwKfC9WJgU8OAPd6DgmxuZygtBzOOBIMOBILhCzuZOEwqB7I3CzJwCGQIAEyA3laYOROIZwChJwBjI4DOE0IxIsDxXwBIOe1p6DxA3mgGa7MdzudxCoCgHu1o3C7HQHE+ZGoPY7BmE91NOIPa0A3nHAOq1WozgJE/2oN4J5DAE0O///oAJFpztBBIwA/AH4A/AH4A/AH4A/AFu0G2sP//6xQ3zhuupWI1w3yhOU6lE6mIG+WZogAB6mNHGPpG4VExHgG+Hqyg4DxXwG98O0g3DomOOGGNG4naVOEJpJw27o3EpugG98Kwg3DpNIOGHoOAlJOGEOxo3ExRww9RwEpGAG98JOAmY1JwwhvZN4eOgEL2A4uzOUptEptK0EA3Y1s/Y4CztNpGKcAO7OFkLMwWYzPd1VAVF5mDzOZxRBCPQQ4vAAn+G9qpDG4ugHGvu8A3tHAP4Awn/+A3ugG+1IECh/vG+EA13u/e73/vU95sD/4ACG2LjC3e72A3zAH4A/AH4A/AH4A/AH4A/ADgA=")) + }, + face10: { + width : 156, height : 116, bpp : 4, + transparent : 0, + palette : new Uint16Array([65535,10,53856,6304,9,11,41664,65533,1,51712,25280,0,2,8,29344,7]), + buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AHcwIH4AKh8zmfUGFn/GAJ/Z4c+8czJlcEmfjGDMNnOZzOeTdnpGAPjTi8PDgVuJtnmzOWyZNayxNvzMun4bWhhNC9xNty2Sz3DDa2jn2Wy3gJtmmtOZ8bpWgczmfunxNt11u90zdK0zn3pnJpQqoNMqAdOh3u92e8czTSuZAAQVPJpkFJqAyDmbrTnfpDIWeCp5AMJqEKtIzCsacTnZnDJqCcMexqbDJoeWnhNS4eZyUpJqaPJgpNQTYmWn5NVkUiJqMBJpNBdB5NFzPsJqXTCwKbBtwXRSBENoobQJoaBBdKddn3p93uTaLfBSI8EoBNRs3u9Oe8czJqUEmZOBJ4IYSqpOFgtUDSKbC90zmcwGiUA8c59OZTaRGBdYgFBUY7pO8ZMUgEzJgOWtwYTiMQUJJNRyc6JqnfJoOZJqkEJoYAC//9JqeemZNUgfmyycBCqP/4YIFh88xk4HB5NCyWTJqsI31ps3jJiE85n8Fws/nG7AAOwJp3uTYNjmBNUgGDmYABCZ5MB5k73nAKwfzxez804/5rO4cz8czJq0M+YZQgf85cz90755WE2cwu8DBIYALP4RMWACcPTIM2IYPMJofInZNC5lAHdIAR/nMwZDBueM6AJBnGIxE+u9rxhN8//I3ZDBsZNDnnD5HDnU7BIYA5+fD3czn0z3nwJoXMxnMxGzBIYA5nmLnezme73BDC/+7BIOz3e8wBN8ne4nE7xZNDh+D3YABneImBN7hnIxGI5E8xhND5G7TYOI5hM7IYPMxHDAQJDE5nIxgIB+ZN8gHf3eMnG4wamE/nM5nPJnpDBxeM5E7mAKE5//5jxCAHsz//8JgoA/AAsPIH4A/AH4A/AH4A/AH4A/AGUFqpB/ABlVqBB/Tn4AaoicvPzkBoLM/ABkU7qa/dZtFJlUBqANL6kUogAFiIgJ6ghMTU/0///5/RiIAEilBiMVqNRiNEC4kEipMogp4H5n//pNB/4XJqtRUQf/NganJJj6aGqtE7pJJABNRqtVNoUEdU6aFhv0bAIhXEAS/IAD5EE3n8xhyiAEMPooECxn8mDAjAEMNiAECqIlfJs9NEslVJkpNlqqamgFUEcUFTU7DkTVB3BTkMFJlAAB//wFdIAhh//IP4AMhvfTqrgrABVP/8QCydVTu1BHClFen4ALh/xIP4AL//QIP6aL+BB/ABfdJv4ALqsRIP5FFqBM/ABUFqNEond/tEoJTEAH4AC//9JoPUoBF/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AlA=")) + }, + face11: { + width : 109, height : 111, bpp : 4, + transparent : 0, + palette : new Uint16Array([65535,24579,65271,24580,22531,7,35552,47112,2240,65431,65434,39776,22532,43872,65335,35712]), + buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AH4A/AH4A/AGVVBpkFqA0kgtVE5tVIppqXGpxGQGqwSRG0IiTG0LGUgo2ffio2ehA1VGwIXWGro2erAYXUjlVDjA2bDbSjcAH4A/AH4A/AH4A/AH4A/AGNVqtQFlOIxAHFhFYGwNVGk8IGoJjFhGFBII4BGtOIF4IIEowKCG00FxGAsg2BwqqDtWgIATbmquA1RmBFgdYxeq0g2orGE1RjFOoJsEUcpoBxuqtDQEAgOEcgZslFguIFgI/BAAZsnrAtENgg2EbMwtFbIQJBOwVVGsosCGooJBwp0DxA1kGw7jEBQLYmFgQ2CMwIJFA4I1ngGFrAACO4tVquAGs8AhFYqo1FAH4A/AH4A/AH4ABrGIAAY0uxA1BwmEww2BwuAGlUIF4OAh2ZzPqtA4CG9NVTYOGl3p9Xq82A0EGG1NVoEK1GAkUil0gxEJzOQotcUE1YgGq0FIsUu93gpGGlWqhGFqo1kguIoHqkFoxEdgFmxFEoEig2EplVUseFxmIpfWpGEpAABwlEG4IEBpnMrA2iGoNUxGEFwIABGgIABGgIABpnI4o1hrHFiItBwIxCAAgLConM5GFGsFVqgqEAA1BigNBiczolVGsGM5nBFQIxEHocRmMUiIOBoo2fa4PM4YpBTwo2CikTodDpjgCqA1dxA1BGwIvBAA0Uikcjk8jhEBpnFGzsIa4NMGpMxNIPDAQUzJIKjdgtUwjRBiY0GiczNAIADikx4dMGztY4g1BZoJrHFwPBNgIBBmkxmlVUblV5lEMgQ2FAwMzmdDGwQABmMzolFGzEFAQWFoJkDGwgrBGoQ2DAIPBidFUbEFJ4VYUIIwDNQY2CibZEikUBoNEpBsYJ4UFwiiDbQjWBiMzGQI3Bmg6CocRphsdLAQzBogACA4bbBAAMUmkUoKABmnFNjguDSAQABHwLfBG4LWCoNDoPB4JsBwpSCbLQ0BijdBmYDCHwVBUwJsECgMUNgVVN6psDwIsCpEzu93OgKYBFgIxBmkzA4IACIoIcCNjNUNgVEi41Bug2DNgKkBjg4BmlENwVIDgIfDGysFwgrBGod3pA2CbgJsBmJsDGwMUpBsBgpvCUa2IFoNEo41CHYLaDNgMUjgABNQSiBovAbK5sChA2CoI1Cu4nBAAdMpjZBNAI9BBINIrhsYUYlIwI1DumBFIOEAANE5kzmc0dYQ1BGQQ1WC4dYwg1EuJsBxHFxFYrFU5nM4PBiJ5CqqiBKgRsVbQeEi5sEwlYqtcqlV4o3CWYI1BpFYQ4RsXbQjYEuNIwtIPwlVrHFcIWExBoCGq4YBGwVYoY1CmfFqmACQtcqoyBpGIxANCwqiWJ4kFSgMzmfMqvICZHFOANVxAHCGrI2EEgIoBrgVMF4iICGzZeCDKlUGrL0YQQJraGwQ3VCwI1cD6QRBAAY0cRgY4OCAZqeAAeILMAA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AFg=")) + }, + face12: { + width : 136, height : 135, bpp : 4, + transparent : 0, + palette : new Uint16Array([65535,24579,24580,22531,65400,8,65270,22532,65369,22530,47112,9,11,20482,0,10]), + buffer : require("heatshrink").decompress(atob("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")) + }, + charge: { + width : 176, height : 176, bpp : 4, + transparent : 0, + palette : new Uint16Array([65535,65535,0,19967,2040,61309,2041,17919,44373,2048,19935,40607,2008,38559,17918,2009]), + buffer : require("heatshrink").decompress(atob("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")) + } + } + +// Current face state +var currentFace = "face1"; +// Ordered list of faces for cycling +var faceNumbers = [ + "face1", "face2", "face3", "face4", "face5", "face6", + "face7", "face8", "face9", "face10", "face11", "face12" +]; + +// Get a random Meeseeks face +function getRandomFace() { + return faceNumbers[Math.floor(Math.random() * faceNumbers.length)]; +} + +// Set a new random face +function setRandomFace() { + currentFace = getRandomFace(); +} + +// Cycle to the next face in order +function setNextFace() { + var idx = faceNumbers.indexOf(currentFace); + if (idx < 0) idx = 0; + currentFace = faceNumbers[(idx + 1) % faceNumbers.length]; +} + +// Decompress sprite on demand to save memory +function getSprite(spriteName) { + var sprite = meeseeksSprites[spriteName]; + if (!sprite.buffer) { + // Support legacy heatshrink-compressed base64 (sprite.data) + if (sprite.data) { + sprite.buffer = require("heatshrink").decompress(atob(sprite.data)); + // Support raw base64 pixel data from Espruino Image Converter (Image Object, no compression) + } else if (sprite.raw) { + sprite.buffer = E.toArrayBuffer(atob(sprite.raw)); + } + } + return sprite; +} + +// Draw the Meeseeks face +function drawMeeseeksFace() { + var isCharging = Bangle.isCharging(); + var faceImage; + + // Show charge face when charging + if (isCharging) { + faceImage = getSprite("charge"); + } else { + faceImage = getSprite(currentFace); + } + + // Draw the face centered on screen based on actual sprite dimensions + var centerX = (g.getWidth() - faceImage.width) / 2; + var centerY = (g.getHeight() - faceImage.height) / 2; + g.drawImage(faceImage, centerX, centerY, {transparent: faceImage.transparent}); +} + +// ------- Aging spots overlay (more as battery goes down) ------- +var spotCache = { batteryBucket : -1, w:0, h:0, spots : [] }; + +function randomBetween(min, max) { + return Math.floor(min + Math.random() * (max - min + 1)); +} + +function computeSpots(centerX, centerY, width, height, count) { + var spots = []; + var margin = 6; // keep a small inset from edges + for (var i = 0; i < count; i++) { + var x = randomBetween(centerX + margin, centerX + width - margin); + var y = randomBetween(centerY + margin, centerY + height - margin); + var baseR = randomBetween(1, 3); // base radius + var blobs = randomBetween(1, 3); // draw 1-3 small blobs around base + var blobList = []; + for (var b = 0; b < blobs; b++) { + var ox = randomBetween(-2, 2); + var oy = randomBetween(-2, 2); + var r = Math.max(1, baseR + randomBetween(-1, 1)); + blobList.push({ x:x+ox, y:y+oy, r:r }); + } + spots.push(blobList); + } + return spots; +} + +// Lightweight stipple fill to simulate translucency +function fillCircleStipple(cx, cy, r, spacing, phase) { + if (r <= 0) return; + var rr = r*r; + for (var dy = -r; dy <= r; dy++) { + var y = cy + dy; + var dxMax = Math.floor(Math.sqrt(rr - dy*dy)); + for (var dx = -dxMax; dx <= dxMax; dx++) { + var x = cx + dx; + if (((dx + dy + phase) % spacing) === 0) g.setPixel(x, y); + } + } +} + +function drawSpotsOverlay() { + if (Bangle.isCharging()) return; // no aging while charging + var w = g.getWidth(); + var h = g.getHeight(); + var centerX = 0; + var centerY = 0; + var b = E.getBattery(); + // Bucketize to reduce flicker/regen + var bucket = Math.max(0, Math.min(20, Math.floor((100 - b) / 5))); // 0..20 + var maxSpots = 60; // many at low battery + var spotsWanted = Math.round(maxSpots * Math.min(1, Math.max(0, (100 - b) / 80))); // 100->0, 20->~max + + if (spotCache.batteryBucket !== bucket || spotCache.w !== w || spotCache.h !== h) { + spotCache.batteryBucket = bucket; + spotCache.w = w; + spotCache.h = h; + spotCache.spots = computeSpots(centerX, centerY, w, h, spotsWanted); + } + // Color: dev blue; outlines + stipple for quasi-transparency + g.setColor(0, 0, 0.5); + for (var i = 0; i < spotCache.spots.length; i++) { + var cluster = spotCache.spots[i]; + for (var j = 0; j < cluster.length; j++) { + var c = cluster[j]; + g.drawCircle(c.x, c.y, c.r); + // sparse interior points; spacing 3 gives light fill + fillCircleStipple(c.x, c.y, c.r-1, 3, (i*7 + j*3) % 3); + } + } +} + +function bigThenSmall(big, small, x, y) { + g.setFont("7x11Numeric7Seg", 2); + g.drawString(big, x, y); + x += g.stringWidth(big); + g.setFont("8x12"); + g.drawString(small, x, y); +} + + + + +// schedule a draw for the next minute +var drawTimeout; +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + + +function clearIntervals() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; +} + +function drawClock() { + g.setFont("7x11Numeric7Seg", 3); + g.setColor(1, 1, 1); + g.drawString(require("locale").time(new Date(), 1), 75, 135); + g.setFont("8x12", 2); + g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 8, 145); + g.setFont("8x12"); + g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 8, 130); + g.setFont("8x12", 2); + const time = new Date().getDate(); + g.drawString(time < 10 ? "0" + time : time, 28, 120); +} + +function drawBattery() { + bigThenSmall(E.getBattery(), "%", 10, 10); +} + + +function getTemperature(){ + try { + var temperature = E.getTemperature(); + if (!temperature || !isFinite(temperature)) return "--"; + // Show Fahrenheit + var f = (temperature * 9/5) + 32; + return Math.round(f) + "F"; + + } catch(ex) { + print(ex) + return "--" + } +} + +function getSteps() { + var steps = Bangle.getHealthStatus("day").steps; + steps = Math.round(steps/1000); + return steps + "k"; +} + + +function draw() { + queueDraw(); + + g.clear(1); + g.setColor(0, 0.7, 1); + g.fillRect(0, 0, g.getWidth(), g.getHeight()); + + // Spots should be above background but below the face + drawSpotsOverlay(); + // Draw the Meeseeks face on top of spots + drawMeeseeksFace(); + + // Foreground metrics + g.setFontAlign(0,-1); + g.setFont("8x12", 2); + g.setColor(1, 1, 1); + g.drawString(getTemperature(), 20, 40); + var hr = Bangle.getHealthStatus().bpm || Bangle.getHealthStatus("last").bpm; + var hrStr = (hr && isFinite(hr)) ? String(Math.round(hr)) : "--"; + g.drawString(hrStr, 15, 70); + g.drawString(getSteps(), 160, 10); + + g.setFontAlign(-1,-1); + drawClock(); + drawBattery(); + + // Hide widgets + for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} +} + +Bangle.on("lcdPower", (on) => { + if (on) { + // Set a random face when waking up + setRandomFace(); + draw(); + } else { + clearIntervals(); + } +}); + + +Bangle.on("lock", (locked) => { + clearIntervals(); + draw(); +}); + +Bangle.setUI("clock"); + +// Touch debouncing to prevent rapid face changes +var lastTouchTime = 0; +var touchDebounceMs = 500; // 500ms debounce + +// Independent touch listener so it's not overridden by UI helpers +Bangle.on('touch', function(button, xy) { + var now = Date.now(); + + // Debounce rapid touches + if (now - lastTouchTime < touchDebounceMs) { + return; + } + + // Get current sprite to check its dimensions + var currentSprite = Bangle.isCharging() ? getSprite("charge") : getSprite(currentFace); + var centerX = (g.getWidth() - currentSprite.width) / 2; + var centerY = (g.getHeight() - currentSprite.height) / 2; + + // Define touch zones - exclude top area for widget bar access + var widgetBarHeight = 40; // Reserve top 40px for widget bar gestures + var faceTouchMargin = 20; // Reduce touch area to center of face + + // Check if tap is within the center area of Meeseeks face (excluding top for widget bar) + if (xy && xy.x >= centerX + faceTouchMargin && xy.x <= centerX + currentSprite.width - faceTouchMargin && + xy.y >= centerY + faceTouchMargin && xy.y <= centerY + currentSprite.height - faceTouchMargin && + xy.y > widgetBarHeight) { // Exclude top area for widget bar access + // Only change face if not charging + if (!Bangle.isCharging()) { + lastTouchTime = now; + setNextFace(); + draw(); + } + } +}); + +// Fallbacks for devices/firmware without touch delivery +if (typeof BTN1 !== 'undefined') { + setWatch(function() { + if (!Bangle.isCharging()) { setNextFace(); draw(); } + }, BTN1, {repeat:true, edge:'falling'}); +} + +Bangle.on('swipe', function(LR, UD) { + if (!Bangle.isCharging()) { setNextFace(); draw(); } +}); + +// Load widgets, but don't show them +Bangle.loadWidgets(); +require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe +g.clear(1); +// Set initial random face +setRandomFace(); +draw(); diff --git a/apps/meseeks/app.png b/apps/meseeks/app.png new file mode 100644 index 0000000000..e54ea5cc6c Binary files /dev/null and b/apps/meseeks/app.png differ diff --git a/apps/meseeks/data.json b/apps/meseeks/data.json new file mode 100644 index 0000000000..0fcf7dd7dd --- /dev/null +++ b/apps/meseeks/data.json @@ -0,0 +1 @@ +{"tasks":"", "weather":[]}; diff --git a/apps/meseeks/metadata.json b/apps/meseeks/metadata.json new file mode 100644 index 0000000000..d3fc54a439 --- /dev/null +++ b/apps/meseeks/metadata.json @@ -0,0 +1,17 @@ +{ "id": "meseeks", + "name": "Mr Meeseeks", + "shortName":"MeSeeks", + "version":"0.02", + "description": "Mr Meeseeks clock with random faces and battery-dependent freckles overlay.", + "icon": "app.png", + "tags": "clock", + "type": "clock", + "supports" : ["BANGLEJS", "BANGLEJS2"], + "screenshots": [{"url":"screenshot01.png"}, {"url":"screenshot02.png"}, {"url":"screenshot03.png"}], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"meseeks.app.js","url":"app.js"}, + {"name":"meseeks.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/meseeks/screenshot01.png b/apps/meseeks/screenshot01.png new file mode 100644 index 0000000000..9c23d3e23f Binary files /dev/null and b/apps/meseeks/screenshot01.png differ diff --git a/apps/meseeks/screenshot02.png b/apps/meseeks/screenshot02.png new file mode 100644 index 0000000000..6665f949cd Binary files /dev/null and b/apps/meseeks/screenshot02.png differ diff --git a/apps/meseeks/screenshot03.png b/apps/meseeks/screenshot03.png new file mode 100644 index 0000000000..93c5fd6e9c Binary files /dev/null and b/apps/meseeks/screenshot03.png differ