new app: tinyheads clock
|
@ -0,0 +1 @@
|
|||
0.01: New app!
|
|
@ -0,0 +1,68 @@
|
|||
#  Tinyheads
|
||||
|
||||
### Which Tinyhead will you create?
|
||||
|
||||
Choose from a variety of hairstyles, eyes, noses, and mouths to customize your pixel art style Tinyhead.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||

|
||||
|
||||
**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)
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4n/oH9A4P/AoMFgH4Fj0zAAgX/C+MAgYVBmCQTC9IRCLI4JDDpAXvAH4AHgUiAAQKFBIcgC+wLDC5YL6ogABBYoICC/4X/AA8BiIOMC/URiAX/C/4AEEx6wHC95ZDABYmIC95iCiUiABAX7MRazOC9oAQ"))
|
|
@ -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();
|
||||
|
||||
}
|
After Width: | Height: | Size: 378 B |
After Width: | Height: | Size: 3.0 KiB |
|
@ -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});
|
||||
};
|
|
@ -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"}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 1.8 KiB |
|
@ -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);
|
||||
}
|
||||
})
|
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.3 KiB |