new app: tinyheads clock

pull/3611/head
Ian Ward 2024-09-15 11:20:17 +01:00
parent 4d8c46ba51
commit 98de95415f
14 changed files with 779 additions and 0 deletions

1
apps/tinyheads/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New app!

68
apps/tinyheads/README.md Normal file
View File

@ -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)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4n/oH9A4P/AoMFgH4Fj0zAAgX/C+MAgYVBmCQTC9IRCLI4JDDpAXvAH4AHgUiAAQKFBIcgC+wLDC5YL6ogABBYoICC/4X/AA8BiIOMC/URiAX/C/4AEEx6wHC95ZDABYmIC95iCiUiABAX7MRazOC9oAQ"))

295
apps/tinyheads/app.js Normal file
View File

@ -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();
}

BIN
apps/tinyheads/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

BIN
apps/tinyheads/editing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

143
apps/tinyheads/lib.js Normal file
View File

@ -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});
};

View File

@ -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"}
]
}

BIN
apps/tinyheads/palette.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

249
apps/tinyheads/settings.js Normal file
View File

@ -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);
}
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB