diff --git a/apps/tinyheads/ChangeLog b/apps/tinyheads/ChangeLog new file mode 100644 index 000000000..1a3bc1757 --- /dev/null +++ b/apps/tinyheads/ChangeLog @@ -0,0 +1 @@ +0.01: New app! diff --git a/apps/tinyheads/README.md b/apps/tinyheads/README.md new file mode 100644 index 000000000..f463e73d8 --- /dev/null +++ b/apps/tinyheads/README.md @@ -0,0 +1,68 @@ +# ![Image](app.png "icon") Tinyheads + +### Which Tinyhead will you create? + +Choose from a variety of hairstyles, eyes, noses, and mouths to customize your pixel art style Tinyhead. + +![Image](tinyhead1.png "screenshot") +![Image](tinyhead2.png "screenshot") +![Image](tinyhead3.png "screenshot") +![Image](tinyhead4.png "screenshot") + +## Features + +* **Facial Features:** + * A diverse selection of hairstyles, eyes, noses, and mouths. + * Choose from 27 different colours. + * Adjust everything on the Bangle.js itself. +* **Optional Widgets:** Display widgets for added functionality. +* **Clock Options:** Include analog clock hands and a digital clock. +* **Device Status:** Eyes will indicate charging status or Bluetooth connection loss. + +## Usage + +* Install the Tinyheads Clock via the Bangle.js app loader. +* Configure settings through the Bangle.js configuration menu (Settings app > "Apps" > "Tinyheads clock") or by long-pressing the screen. +* To set as your default watch face, navigate to the Settings app > "System" > "Clock" > select "Tinyheads clock." +* If your Tinyhead appears drowsy, it means the battery is low (<10%). +* While charging, your Tinyhead will "sleep" (eyes will remain closed). + +## Configuration + +Accesing settings via the standard method (settings app) to go to the main configuration options. Alternatively a long press on the Tinyhead Clock will go directly to the face editing screen. Pressing the button while on this screen will return to the configuration screen, a second press will return to the clock. + +### Configuration options + +* **Face:** Choose facial features and colours (see below). +* **Analog Clock:** Display analog clock hands. Default: On. + * Options: On/Off/Unlocked (only visible when screen is unlocked). +* **Analog Colour:** Choose the colour of the analog hands (Black, White, Red, Green, Blue, Yellow, Cyan, Magenta). Default: White. +* **Digital Clock:** Show a digital clock area in 12 or 24-hour format (system setting). Default: Off. + * Options: On/Off/Unlocked (only visible when screen is unlocked). +* **Digital Position:** Choose the position on the screen (Top/Bottom). Default: Bottom. +* **Show Widgets:** Display the widget area. Default: Off. + * Options: On/Off/Unlocked (only visible when screen is unlocked). +* **BT Status Eyes:** Eyes indicate Bluetooth connection loss. Default: On. + +### Face editing + +To edit the face, select "Face" from settings or long-press the Tinyhead. + +![Image](editing.png "screenshot") +![Image](palette.png "screenshot") + +**Face Features:** +* Hair +* Eyes +* Nose +* Mouth + +Use the arrows next to each feature to change them. Tap on a feature to open the color selector. For skin color, long-press anywhere on the face. Tap a color to select it; if you make a mistake, quickly tap the correct color to change it. The selected color will apply after a brief pause. + +Use the button to exit the color selector without changes. Pressing the button while on the face editing screen will save your changes and return to the main settings. + +Depending on how you accessed settings, pressing the button will take you back to either the Bangle.js settings app or the Tinyheads clock. + +## Author + +Woogal [github](https://github.com/retcurve) diff --git a/apps/tinyheads/app-icon.js b/apps/tinyheads/app-icon.js new file mode 100644 index 000000000..7cf100bd8 --- /dev/null +++ b/apps/tinyheads/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4n/oH9A4P/AoMFgH4Fj0zAAgX/C+MAgYVBmCQTC9IRCLI4JDDpAXvAH4AHgUiAAQKFBIcgC+wLDC5YL6ogABBYoICC/4X/AA8BiIOMC/URiAX/C/4AEEx6wHC95ZDABYmIC95iCiUiABAX7MRazOC9oAQ")) diff --git a/apps/tinyheads/app.js b/apps/tinyheads/app.js new file mode 100644 index 000000000..a3f79c081 --- /dev/null +++ b/apps/tinyheads/app.js @@ -0,0 +1,295 @@ +{ + // Shared library for face drawing and settings loading. + let lib = require('tinyheads.lib.js'); + + // Read 12/24 from system settings + const is12Hour=(require("Storage").readJSON("setting.json",1)||{})["12hour"] || false; + + // Tinyhead features are stored at a resolution of 18x21, this scales them to the best fit for the Banglejs2 screen + const scale=9; + + const closedEyes = 25; + const scaredEyes = 26; + const scaredMouth = 4; + const disconnectedEyes = 3; + const glassesEyes = [18, 23, 24]; + + let drawTimeout, blinkTimeout, tapTimeout; + let activeEyesNum = lib.settings.eyesNum; + let eyesNum = activeEyesNum; + let activeMouthNum = lib.settings.mouthNum; + let mouthNum = activeMouthNum; + let helpShown = false; + let tapCount = 0; + let centerX, centerY, minuteHandLength, hourHandLength, handOutline; + + // Open the eyes and schedule the next blink + let blinkOpen = function blinkOpen() { + if (blinkTimeout) clearTimeout(blinkTimeout); + blinkTimeout = setTimeout(function() { + blinkTimeout = undefined; + blinkClose(); + }, 3000 + (Math.random() * 10000)); + eyesNum = activeEyesNum; + mouthNum = activeMouthNum; + drawTinyhead(); + }; + + // Close the eyes and schedule the next open + let blinkClose = function blinkClose() { + if (!glassesEyes.includes(activeEyesNum)) { + if (blinkTimeout) clearTimeout(blinkTimeout); + if (!Bangle.isCharging()) { // Keep eyes shut while charging + blinkTimeout = setTimeout(function() { + blinkTimeout = undefined; + blinkOpen(); + }, E.getBattery() < 10 ? 6000 + (Math.random() * 6000) : 150); // Doze if battery low, otherwise quick blink + } + eyesNum = closedEyes; // Closed eyes + drawTinyhead(); + } + }; + + // Tinyhead is scared + let scared = function scared() { + if (!glassesEyes.includes(activeEyesNum)) { + if (blinkTimeout) clearTimeout(blinkTimeout); + blinkTimeout = setTimeout(function() { + blinkTimeout = undefined; + peek(); + }, 4000); // Terrified for 4 seconds + eyesNum = scaredEyes; + } + if (mouthNum < 10) { + mouthNum = scaredMouth; + } + drawTinyhead(); + }; + + // See if it's safe to open eyes + let peek = function peek() { + if (blinkTimeout) clearTimeout(blinkTimeout); + blinkTimeout = setTimeout(function() { + blinkTimeout = undefined; + blinkOpen(); + }, 3000); // Peek for 3 seconds + drawTinyhead(true); + }; + + // Draw the tinyhead + let drawTinyhead = function drawTinyhead(peek) { + // Set background to black, the tinyhead is slightly narrower than the banglejs2 screen + g.setBgColor(0, 0, 0); + g.clearRect(Bangle.appRect); + + let offset = 0; + if (shouldDrawDigital()) { + offset = 30; // Move the head by half the clock height to keep more of the features in view + if (lib.settings.digitalPosition == 'bottom') { + offset = offset * -1; // Move the head up if clock is at the bottom + } + } + + lib.drawFace(scale, eyesNum, mouthNum, peek, offset); + // Draw widgets again as the face will have been drawn above them + + drawClocks(); + }; + + // Draw analog and digital clocks + let drawClocks = function drawClocks() { + if (shouldDrawAnalog()) { + drawAnalog(); + } + if (shouldDrawDigital()) { + drawDigital(); + } + queueClocksDraw(); + }; + + // Draw digital clock + let drawDigital = function drawDigital() { + let width = lib.faceW * scale; // Set width to face width, which is slightly narrower than screen width + let height = 60; + let yOffset = Bangle.appRect.y; + let xOffset = (Bangle.appRect.w - width) / 2; + if (lib.settings.digitalPosition == 'bottom') { + yOffset = g.getHeight() - height; + } + + g.setColor(0, 0, 0); + g.fillRect(xOffset, yOffset, width+xOffset, height+yOffset); + g.setColor(1, 1, 1); + g.fillRect(xOffset+10, yOffset+10, width-10+xOffset, height-10+yOffset); + + let d = new Date(); + let h = d.getHours(), m = ("0"+d.getMinutes()).substr(-2); + if (is12Hour){ + h = h - 12; + } + h = ("0"+h).substr(-2); + + g.setColor(0, 0, 0); + g.setFont("6x8:4x6"); + g.drawString(h+":"+m, 22+xOffset, 7+yOffset); + }; + + // Draw analog clock hands + let drawAnalog = function drawAnalog() { + let thickness = 4; + + let d = new Date(); + let h = d.getHours(); + let m = d.getMinutes(); + let hRot = (h + m/60) * Math.PI / 6; // Angle of hour hand + let mRot = m * Math.PI / 30; // Angle of minute hand + let offset = 0; + + if (shouldDrawDigital()) { + offset = 30; // Adjust the analog center to match head position + if (lib.settings.digitalPosition == 'bottom') { + offset = offset * -1; + } + } + + g.setColor(lib.settings.analogColour); + g.fillPolyAA(g.transformVertices([-thickness, 0, thickness, 0, thickness, -minuteHandLength, -thickness, -minuteHandLength ], {x: centerX, y: centerY+offset, rotate: mRot})); + g.setColor(handOutline); + g.drawPolyAA(g.transformVertices([-thickness, 0, thickness, 0, thickness, -minuteHandLength, -thickness, -minuteHandLength ], {x: centerX, y: centerY+offset, rotate: mRot}), true); + g.setColor(lib.settings.analogColour); + g.fillPolyAA(g.transformVertices([-thickness, 0, thickness, 0, thickness, -hourHandLength, -thickness, -hourHandLength ], {x: centerX, y: centerY+offset, rotate: hRot})); + g.setColor(handOutline); + g.drawPolyAA(g.transformVertices([-thickness, 0, thickness, 0, thickness, -hourHandLength, -thickness, -hourHandLength ], {x: centerX, y: centerY+offset, rotate: hRot}), true); + }; + + // Schedule a redraw of the clocks + let queueClocksDraw = function queueClocksDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + drawTinyhead(); + }, (60000) - (Date.now() % (60000))); + }; + + // Show clocks on unlock + let lockHandler = (on, reason) => { + if (lib.settings.showWidgets == 'unlock' && !on) { + require("widget_utils").show(); + Bangle.drawWidgets(); + } + if (lib.settings.showWidgets == 'unlock' && on) { + require("widget_utils").hide(); + } + if (lib.settings.analogClock == 'unlock' || lib.settings.digitalClock == 'unlock' || lib.settings.showWidgets == 'unlock') { + drawTinyhead(); + } + }; + + // Sleep while charging + let chargingHandler = charging => { + if (charging) { + blinkClose(); + } else { + blinkOpen(); + } + }; + + // Status eyes on bt disconnect + let btDisconnectHandler = () => { + activeEyesNum = disconnectedEyes; + blinkOpen(); + }; + + // reset eyes to normal + let btConnectHandler = () => { + activeEyesNum = lib.settings.eyesNum; + blinkOpen(); + }; + + let shouldDrawAnalog = function() { + return lib.settings.analogClock == 'on' || (lib.settings.analogClock == 'unlock' && !Bangle.isLocked()); + }; + let shouldDrawDigital = function() { + return lib.settings.digitalClock == 'on' || (lib.settings.digitalClock == 'unlock' && !Bangle.isLocked()); + }; + + let init = function init() { + Bangle.on('lock', lockHandler); + Bangle.on('charging', chargingHandler); + if (lib.settings.btStatusEyes) { + NRF.on('connect', btConnectHandler); + NRF.on('disconnect', btDisconnectHandler); + } + + activeEyesNum = lib.settings.eyesNum; + activeMouthNum = lib.settings.mouthNum; + if (!NRF.getSecurityStatus().connected && lib.settings.btStatusEyes) { + activeEyesNum = disconnectedEyes; + } + + Bangle.setUI({ + mode:"custom", + clock: true, + touch: (button, xy) => { + // Go direct to feature select in settings on long screen press + if (xy.type == 2) { + eval(require("Storage").read("tinyheads.settings.js"))(()=> { + E.showMenu(); + init(); + }, true, helpShown); + helpShown = true; // Only trigger the help screen once per run + } else { + if (tapTimeout) clearTimeout(tapTimeout); + tapTimeout = setTimeout(function() { + tapTimeout = undefined; + tapCount = 0; + }, 1500); + tapCount++; + if (tapCount == 3) { + scared(); + } + } + }, + remove: function() { + // Clear timeouts and listeners for fast loading + if (drawTimeout) clearTimeout(drawTimeout); + if (blinkTimeout) clearTimeout(blinkTimeout); + Bangle.removeListener("charging", chargingHandler); + Bangle.removeListener("lock", lockHandler); + if (lib.settings.btStatusEyes) { + NRF.removeListener('connect', btConnectHandler); + NRF.removeListener('disconnect', btDisconnectHandler); + } + require("widget_utils").show(); + Bangle.drawWidgets(); + } + }); + + // Always load widgets (needed for fast loading) and display if option selected + Bangle.loadWidgets(); + if (lib.settings.showWidgets == 'on' || (lib.settings.showWidgets == 'unlock' && !Bangle.isLocked())) { + Bangle.drawWidgets(); + } else { + require("widget_utils").hide(); + } + + centerX = (Bangle.appRect.w / 2) + Bangle.appRect.x; + centerY = (Bangle.appRect.h / 2) + Bangle.appRect.y; + + minuteHandLength = Math.floor(Math.min(Bangle.appRect.w, Bangle.appRect.h) * 0.45); + hourHandLength = Math.floor(Math.min(Bangle.appRect.w, Bangle.appRect.h) * 0.30); + + handOutline = g.toColor( // Calculate a contrasting colour for the hands edge + lib.settings.analogColour.substring(1,2)=='f' ? 0 : 1, + lib.settings.analogColour.substring(2,3)=='f' ? 0 : 1, + lib.settings.analogColour.substring(3,4)=='f' ? 0 : 1 + ); + + // Opening the eyes triggers a face redraw and also starts the blink and clock timers + blinkOpen(); + + }; + + init(); + +} diff --git a/apps/tinyheads/app.png b/apps/tinyheads/app.png new file mode 100644 index 000000000..d1772b411 Binary files /dev/null and b/apps/tinyheads/app.png differ diff --git a/apps/tinyheads/editing.png b/apps/tinyheads/editing.png new file mode 100644 index 000000000..d43577e7d Binary files /dev/null and b/apps/tinyheads/editing.png differ diff --git a/apps/tinyheads/lib.js b/apps/tinyheads/lib.js new file mode 100644 index 000000000..cbb139d8c --- /dev/null +++ b/apps/tinyheads/lib.js @@ -0,0 +1,143 @@ +exports.maxMouth = 13; +exports.maxNose = 6; +exports.maxHair = 12; +exports.maxEyes = 25; +exports.faceW = 18; +exports.faceH = 21; + +exports.settingsFile = 'tinyheads.json'; + +let faceCanvas; + +let features = { + 'mouth': [ + require("heatshrink").decompress(atob("iURwUBqoA/AAlUitVJwMFqA=")), + + require("heatshrink").decompress(atob("iURwUBqoA/AA9UJQNQA=")), + require("heatshrink").decompress(atob("iUSwUBqoA9qgCBqNVqBKBAwNP//FAgMAJ4I=")), + require("heatshrink").decompress(atob("iUSwUBqoA/AA9AgEVqtQAIQ=")), + require("heatshrink").decompress(atob("iUSwUBqoA/AA1UikUioEEA==")), + require("heatshrink").decompress(atob("iUSwUBqoA/AAVAJQMVqn///xBAg=")), + require("heatshrink").decompress(atob("iUSwUBqoA/AAVAJQMVqnz/nxBAg=")), + require("heatshrink").decompress(atob("iUSwUBqoA/AAdAioDBqEABAo")), + require("heatshrink").decompress(atob("iURwUBqoA/AAVQAIVVoEAitQ")), + require("heatshrink").decompress(atob("iUSwUBqoA/AAVQAIVVp8PioEB6t1qoA=")), + require("heatshrink").decompress(atob("iUUwUBqoA/AAVUgBGCJwMFqEQn/woEBnkAgkAiEFqtAA")), + require("heatshrink").decompress(atob("iUTwUBqoA/AAVBoEUitUiEAoNVoEFgEVqsAqEFqA")), + require("heatshrink").decompress(atob("iUVwUBqoAZ+oDC/4DCr4Ia/4AD+ABC//AgE/BggAM+A=")) + ], + 'nose': [ + require("heatshrink").decompress(atob("iUOwUBqoAtooDCqIIDioDCqgCBA=")), + require("heatshrink").decompress(atob("iUOwUBqoA1qlRAgVQAQI")), + require("heatshrink").decompress(atob("iUOwUBqoAxqIEDgoDCqACBA=")), + require("heatshrink").decompress(atob("iUOwUBqoAoqAEDgoIGqg4DoEVqo=")), + require("heatshrink").decompress(atob("iUMwUBqoAtgoDCqACBA=")), + require("heatshrink").decompress(atob("iUMwUBqoAxqACBA=")) + ], + 'hair': [ + require("heatshrink").decompress(atob("iUHwUBCyVVqtQgEQitAoEFBANVgA")), + require("heatshrink").decompress(atob("iUGwUBqtUAQIABoEVAYIIIqsFAQNQitAqoA=")), + require("heatshrink").decompress(atob("iURwUBCR0VoADBqtVqEAqAIBqEFBANVBgQNBBCsBAYVUioECoIIHoAA=")), + require("heatshrink").decompress(atob("iUVwUBCR0VoADBqtVqEAqAIBqEFBANVBgQNBBBEVAgVBAYVUBCIjEBoQ/BHwYAIA")), + require("heatshrink").decompress(atob("iUGwUBBpNUgNQgEVqFVoEBqtVqkAqEVoFQA=")), + require("heatshrink").decompress(atob("iUHwUBgtVAAMAioDBoAECAYINCAYYEBqEVoACBBAVAA=")), + require("heatshrink").decompress(atob("iUVwUBCJtUgEBqEFqoABgFQitABAoDCqkVAgVBBA8BAYQaEoARDBAYaIBAY4BGo4jEA=")), + require("heatshrink").decompress(atob("iUNwUBCJsFoADBgNVqtUgEQitAoEFBANVgADCqAIRioECoA=")), + require("heatshrink").decompress(atob("iUVwUBCR0FAYdVAYMBqtUAYQEBAYQEBioECoIInHoIABgANCoEAAYNQgAA==")), + require("heatshrink").decompress(atob("iUVwUBDzUVqoABoIDCqgIsgoECHQdAgADBqEAA==")), + require("heatshrink").decompress(atob("iUHwUBBhEEiFBgsAqNFilQgtVAAMAqEVoAIF")), + require("heatshrink").decompress(atob("iUPwUBCqP/AAPwh4EC4FVAAUFAYVQBClU+oECp4RDgA=")) + ], + 'eyes': [ + require("heatshrink").decompress(atob("iUMwUBqoAev/VAINXhoBBqtw6oBBq/9AIIVE")), + require("heatshrink").decompress(atob("iUMwUBqoAer/Vv9Vq/9AINVvkVofVq/Bqk9CogA=")), + require("heatshrink").decompress(atob("iUMwUBqoAeq/VvoEB/tX+tVuHVAINXhoBBCogA==")), + require("heatshrink").decompress(atob("iUMwUBqoAer/VofVq/9qk9qt8it/BAPBq/1CogA=")), + require("heatshrink").decompress(atob("iUMwUBqoAeq/VvoEB/tX+tVvkVofVq/Bqk9CogA=")), + require("heatshrink").decompress(atob("iUMwUBqoAeq/VvoEB/tX+tVvkVAINX4IBBCogA==")), + require("heatshrink").decompress(atob("iUMwUBqoAeq/VvoEB/tX+tVofVAINUnoBBCogA==")), + require("heatshrink").decompress(atob("iUMwUBqoAer/Vv9Vq/9AINVofVAINUnoBBCogA==")), + require("heatshrink").decompress(atob("iUMwUBqoAer/Vv9Vq/9AINVvkVAINX4IBBCogA==")), + require("heatshrink").decompress(atob("iUMwUBqoAer/Vv9Vq/9AINVuHVAINXhoBBCogA==")), + require("heatshrink").decompress(atob("iUMwUBqoAevkVAINX4IBBqt/6oBBqv9AIQAD")), + require("heatshrink").decompress(atob("iUMwUBqoAeofVAINUnoBBqt/6oBBq/1AIIVE")), + require("heatshrink").decompress(atob("iUMwUBqoAeuHVAINXhoBBqt/6oBBqv9q/1CogA==")), + require("heatshrink").decompress(atob("iUMwUBqoAeqfVvlVqn9q/xqsw6tw4tXhoBBCogA=")), + require("heatshrink").decompress(atob("iUMwUBqoAep/Vv8Vqf9q/8qt8ioBBq/BAIIVEA==")), + require("heatshrink").decompress(atob("iUMwUBqoAep/Vv8Vqf9q/8qtD6oBBqk9AIIVEA==")), + require("heatshrink").decompress(atob("iUMwUBqoAeuEVAINXgIBBBAv9AIIVEA=")), + require("heatshrink").decompress(atob("iUMwUBqoAev/VAINXgIBBqtwioBBBAgADA==")), + require("heatshrink").decompress(atob("iUMwUBqoAeoEVAIIrDgoIDqkBAIIVEA=")), + require("heatshrink").decompress(atob("iULwUBqoAeqEVoFVqkBAINVoAIBitUgtVgNQ")), + require("heatshrink").decompress(atob("iULwUBqoAeqABCqsFAIVAioBBqkBAINQ")), + require("heatshrink").decompress(atob("iULwUBqoAeoEVAINU+IBBqtPioBBqkBAINQA")), + require("heatshrink").decompress(atob("iULwUBqoAevtVq/Vq/1qv9qtw6oBBq8NAINQ")), + require("heatshrink").decompress(atob("iUVwUBqoAeoEFgEVFYcFBAdVgNUgowfABlQA")), + require("heatshrink").decompress(atob("iUVwUBqoAev/VAIMDhgBBgtw6oBBq/9AIIwfABlQ")), + require("heatshrink").decompress(atob("iUMwUBqoAnosVAINVgoBCAAY")), // closed + require("heatshrink").decompress(atob("iUMwUBqoAjgoBCosVAIIQIA=")) // scared + ] +}; + +// Load settings +exports.settings = Object.assign({ + eyesNum: Math.floor(Math.random() * exports.maxEyes), + noseNum: Math.floor(Math.random() * exports.maxNose), + hairNum: Math.floor(Math.random() * exports.maxHair), + mouthNum: Math.floor(Math.random() * exports.maxMouth), + eyesColour: '#00f', + hairColour: '#000', + faceColour: '#fa0', + mouthColour: '#000', + noseColour: '#000', + digitalClock: 'off', + digitalPosition: "bottom", + analogClock: 'on', + analogColour: '#fff', + showWidgets: 'off', + btStatusEyes: true +}, require('Storage').readJSON(exports.settingsFile, true) || {}); + +// Draw a facial feature +let drawFeature = function drawFeature(feature, colour, lr) { + faceCanvas.setColor(1, 1, 1); // fg is white of eyes + faceCanvas.setBgColor(colour); // bg is feature colour + if (lr == 'l') { // Only draw left side (for scared/peeking eyes) + faceCanvas.setClipRect(0, 0, (exports.faceW/2)-1, exports.faceH-1); + } else if (lr == 'r') { // Only draw right side (for scared/peeking eyes) + faceCanvas.setClipRect(9, 0, exports.faceW-1, exports.faceH-1); + } + faceCanvas.drawImage(feature); +}; + +exports.drawFace = function(scale, eyesNum, mouthNum, peek, offset) { + if (faceCanvas == undefined) { + faceCanvas = Graphics.createArrayBuffer(exports.faceW, exports.faceH, 8, {msb:true}); + faceCanvas.transparent = 0; + } + + // Face background + faceCanvas.setClipRect(0, 0, exports.faceW-1, exports.faceH-1); + faceCanvas.setColor(exports.settings.faceColour); + faceCanvas.fillRect(0, 0, exports.faceW, exports.faceH); + + // Face features + drawFeature(features.mouth[mouthNum ? mouthNum : exports.settings.mouthNum], exports.settings.mouthColour); + drawFeature(features.nose[exports.settings.noseNum], exports.settings.noseColour); + drawFeature(features.hair[exports.settings.hairNum], exports.settings.hairColour); + if (peek) { + drawFeature(features.eyes[eyesNum], exports.settings.eyesColour, 'l'); // Left eye still closed + drawFeature(features.eyes[exports.settings.eyesNum], exports.settings.eyesColour, 'r'); // Right eye normal + } else { + drawFeature(features.eyes[eyesNum ? eyesNum : exports.settings.eyesNum], exports.settings.eyesColour); + } + + // Draw face + let xOffset = (g.getWidth() - (exports.faceW * scale)) / 2; + let yOffset = (offset ? offset : 0) + ((g.getHeight() - (exports.faceH * scale)) / 2); + g.setBgColor(0, 0, 0); + g.clearRect(Bangle.appRect); + g.setClipRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y2); + + g.drawImage(faceCanvas, xOffset, yOffset, {scale: scale}); +}; \ No newline at end of file diff --git a/apps/tinyheads/metadata.json b/apps/tinyheads/metadata.json new file mode 100644 index 000000000..3f0e7abf9 --- /dev/null +++ b/apps/tinyheads/metadata.json @@ -0,0 +1,22 @@ +{ + "id": "tinyheads", + "name": "Tinyheads Clock", + "shortName":"Tinyheads", + "icon": "app.png", + "screenshots" : [ { "url":"tinyhead1.png" }, {"url":"tinyhead2.png"}, {"url":"tinyhead3.png"}, {"url":"tinyhead4.png"}, {"url":"editing.png"} ], + "version":"0.01", + "description": "Choose from a variety of hairstyles, eyes, noses, and mouths to customize your pixel art style Tinyhead.", + "readme":"README.md", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"tinyheads.app.js","url":"app.js"}, + {"name":"tinyheads.settings.js","url":"settings.js"}, + {"name":"tinyheads.lib.js","url":"lib.js"}, + {"name":"tinyheads.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"tinyheads.json"} + ] +} diff --git a/apps/tinyheads/palette.png b/apps/tinyheads/palette.png new file mode 100644 index 000000000..4defa3439 Binary files /dev/null and b/apps/tinyheads/palette.png differ diff --git a/apps/tinyheads/settings.js b/apps/tinyheads/settings.js new file mode 100644 index 000000000..716f20561 --- /dev/null +++ b/apps/tinyheads/settings.js @@ -0,0 +1,249 @@ +(function(back, faceEdit, helpShown) { + // Shared library for face drawing and settings loading. + let lib = require('tinyheads.lib.js'); + + let paletteCanvas; + let featureColour = 'faceColour'; + let colourSelectTimeout; + + let scale = 6; // Smaller scale than on the clock itself, so that selection arrows can be shown down the sides + + // 27 colours + let colours = [ + '#000', '#008', '#00f', + '#800', '#808', '#80f', + '#080', '#088', '#08f', + '#880', '#888', '#88f', + '#0f0', '#0f8', '#0ff', + '#8f0', '#8f8', '#8ff', + '#f00', '#f08', '#f0f', + '#001', '#001', '#001', + '#f80', '#f88', '#f8f', + '#001', '#001', '#001', + '#ff0', '#ff8', '#fff', + '#001', '#001', '#001' + ]; + + let colorW = 6; + let colorScale = Math.floor(176/colorW); + + function writeSettings() { + require('Storage').writeJSON(lib.settingsFile, lib.settings); + } + + function setSetting(key,value) { + lib.settings[key] = value; + writeSettings(); + } + + // Helper method which uses int-based menu item for set of string values and their labels + function stringItems(key, startvalue, values, labels) { + return { + value: (startvalue === undefined ? 0 : values.indexOf(startvalue)), + format: v => labels[v], + min: 0, + max: values.length - 1, + wrap: true, + step: 1, + onchange: v => { + setSetting(key,values[v]); + } + }; + } + + // Helper method which breaks string set settings down to local settings object + function stringInSettings(name, values, labels) { + return stringItems(name,lib.settings[name], values, labels); + } + + // Colour selection mode + function colourSelect(index) { + Bangle.setUI({ + mode: "custom", + touch: colourTouchHandler, + btn: () => { // On button press write colour settings and return to feature selection + colourSelectTimeout = null; + writeSettings(); + featureSelect(); + } + }); + + // Create a canvas for the palette and set each pixel + if (paletteCanvas === undefined) { + paletteCanvas = Graphics.createArrayBuffer(colorW, colorW, 8, {msb:true}); + paletteCanvas.setBgColor(0, 0, 0); + paletteCanvas.clear(); + + + for (let i=0; i<(colorW*colorW); i++) { + let x = (i % colorW); + let y = Math.floor(i / colorW); + + paletteCanvas.setPixel(x, y, colours[i]); + } + } + + // Scale the canvas to full screen size + g.setBgColor(0, 0, 0); + g.clear(); + g.drawImage(paletteCanvas, 0, 0, {scale: colorScale}); + g.setColor(1, 1, 1); + g.setFontAlign(0, 0); + g.setFont("6x8:2"); + c = featureColour.split('Colour')[0]; + g.drawString(c[0].toUpperCase() + c.slice(1), 132, 132); + + if (index !== undefined) { // If a colour has been selected draw it in a larger box + x = (index % colorW) * colorScale; + y = Math.floor((index / colorW)) * colorScale; + g.setColor(0, 0, 0); + g.fillRect(x-(colorScale/2), y-(colorScale/2), x+(colorScale/2)+colorScale, y+(colorScale/2)+colorScale); + g.setColor(colours[index]); + g.fillRect(x-(colorScale/2)-2, y-(colorScale/2)-2, x+(colorScale/2)+colorScale-2, y+(colorScale/2)+colorScale-2); + } + } + + // Feature selection mode + function featureSelect() { + E.showMenu(); // Remove previous menu + Bangle.setUI({ + mode: "custom", + touch: featureTouchHandler, + btn: () => { // On button press write settings and return to the main menu + writeSettings(); + require("widget_utils").show(); + E.showMenu(mainMenu); + } + }); + + lib.drawFace(scale); + + // Arrows + for (let i=0; i<4; i++) { + g.setColor(0, 0, 0); + g.fillPolyAA([0, 22+(i*44), 34, 5+(i*44), 34, 39+(i*44)]); + g.fillPolyAA([175, 22+(i*44), 141, 5+(i*44), 141, 39+(i*44)]); + g.setColor(1, 1, 0); + g.fillPolyAA([5, 22+(i*44), 31, 10+(i*44), 31, 34+(i*44)]); + g.fillPolyAA([170, 22+(i*44), 144, 10+(i*44), 144, 34+(i*44)]); + } + + } + + // Cycle between features + function modifyFeature(feature, inc, max) { + lib.settings[feature] += inc; + if (lib.settings[feature] < 0) { + lib.settings[feature] = max - 1; + } else if (lib.settings[feature] >= max) { + lib.settings[feature] = 0; + } + } + + let featureTouchHandler = (button, xy) => { + let inc = 0; + // Left size decrements feature, right side increments + if (xy.x < 45) { + inc = -1; + } + if (xy.x > 130) { + inc = 1; + } + + // Center selects feature for colour changing + if (xy.x>44 && xy.x<132) { + let yOffset = (g.getHeight() - (lib.faceH * scale)) / 2; + let featureHeight = (lib.faceH * scale) / 4; // All features are considered to be of equal heights when selecting + if (xy.y > yOffset && xy.y < yOffset + (lib.faceH * scale)) { + if (xy.type == 0) { // Short press, select feature + if (xy.y < yOffset + featureHeight) { + featureColour = 'hairColour'; + } else if (xy.y < yOffset + featureHeight*2) { + featureColour = 'eyesColour'; + } else if (xy.y < yOffset + featureHeight*3) { + featureColour = 'noseColour'; + } else { + featureColour = 'mouthColour'; + } + } else { // Long press, select skin + featureColour = 'faceColour'; + } + // Show colour palette + colourSelect(); + } + } else { // Which arrow was pressed + if (xy.y < 44) { + modifyFeature('hairNum', inc, lib.maxHair); + } else if (xy.y < 88) { + modifyFeature('eyesNum', inc, lib.maxEyes); + } else if (xy.y < 132) { + modifyFeature('noseNum', inc, lib.maxNose); + } else { + modifyFeature('mouthNum', inc, lib.maxMouth); + } + if (inc !== 0) { // Redraw if feature has been altered + featureSelect(); + } + } + }; + + let colourTouchHandler = (button, xy) => { + let index = Math.floor(xy.x / colorScale) + (Math.floor(xy.y / colorScale) * colorW); + if (colours[index] !== '#001') { + lib.settings[featureColour] = colours[index]; + // Redraw the palette with the chosen colour enlarged for a few ms + colourSelect(index); + if (colourSelectTimeout) clearTimeout(colourSelectTimeout); + colourSelectTimeout = setTimeout(function() { + colourSelectTimeout = undefined; + // If colour choice not quickly changed, save settings and return to feature selection + writeSettings(); + featureSelect(); + }, 700); + } + }; + + function editFace() { + require("widget_utils").hide(); + if (! helpShown) { // Don't show the help text every time + E.showPrompt('Editing a face -\nUse arrows to cycle through facial features.', {buttons : {"Ok":true}}).then(function(v) { + E.showPrompt('Tap feature to change colour, long press the face to change skin colour.', {buttons : {"Ok":true}}).then(function(v) { + helpShown = true; + featureSelect(); + }); + }); + } else { + featureSelect(); + } + } + + let mainMenu = { + '' : { + 'title' : 'Tinyheads', + back: () => { + back(); + }, + }, + 'Face': () => { + editFace(); + }, + 'Analog Clock': stringInSettings('analogClock', ['off', 'on', 'unlock'], ['Off', 'On', 'Unlocked']), + 'Analog Colour': stringInSettings('analogColour', ['#000', '#fff', '#f00', '#0f0', '#00f', '#ff0', '#0ff', '#f0f'], ['Black', 'White', 'Red', 'Green', 'Blue', 'Yellow', 'Cyan', 'Magenta']), + 'Digital Clock': stringInSettings('digitalClock', ['off', 'on', 'unlock'], ['Off', 'On', 'Unlocked']), + 'Digital Position': stringInSettings('digitalPosition', ['bottom', 'top'], ['Bottom', 'Top']), + 'Show Widgets': stringInSettings('showWidgets', ['off', 'on', 'unlock'], ['Off', 'On', 'Unlocked']), + 'BT Status Eyes': { + value: !!lib.settings.btStatusEyes, + onchange: v => { + setSetting('btStatusEyes', v); + } + } + }; + + if (faceEdit) { // faceEdit passed from main clock so we're taken directly to the feature selection + E.showMenu(); + editFace(); + } else { // Otherwise if entered from settings display main menu + E.showMenu(mainMenu); + } +}) diff --git a/apps/tinyheads/tinyhead1.png b/apps/tinyheads/tinyhead1.png new file mode 100644 index 000000000..6d0234138 Binary files /dev/null and b/apps/tinyheads/tinyhead1.png differ diff --git a/apps/tinyheads/tinyhead2.png b/apps/tinyheads/tinyhead2.png new file mode 100644 index 000000000..bbf401b7b Binary files /dev/null and b/apps/tinyheads/tinyhead2.png differ diff --git a/apps/tinyheads/tinyhead3.png b/apps/tinyheads/tinyhead3.png new file mode 100644 index 000000000..f172ad4c4 Binary files /dev/null and b/apps/tinyheads/tinyhead3.png differ diff --git a/apps/tinyheads/tinyhead4.png b/apps/tinyheads/tinyhead4.png new file mode 100644 index 000000000..e19daf3cb Binary files /dev/null and b/apps/tinyheads/tinyhead4.png differ