mirror of https://github.com/espruino/BangleApps
customclock - Initial work on easily customizable clock
parent
ac5c80ff38
commit
f9001e1969
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwIdah/wAof//4ECgYFB4AFBg4FB8AFBj/wh/4AoM/wEB/gFBvwCEBAU/AQP4gfAj8AgPwAoMPwED8AFBg/AAYIBDA4ngg4TB4EBApkPKgJSBJQIFTMgIFCJIIFDKoIFEvgFBGoMAnw7DP4IFEh+BAoItBg+DNIQwBMIaeCKoKxCPoIzCEgKVHUIqtFXIrFFaIrdFdIwAV"))
|
|
@ -0,0 +1,361 @@
|
|||
var face = require("Storage").readJSON("imageclock.face.json");
|
||||
var resources = require("Storage").readJSON("imageclock.resources.json");
|
||||
|
||||
function getImg(resource){
|
||||
//print("getImg: ", resource);
|
||||
var buffer;
|
||||
if (resource.img){
|
||||
buffer = E.toArrayBuffer(atob(resource.img));
|
||||
//print("buffer from img");
|
||||
} else if (resource.file){
|
||||
buffer = E.toArrayBuffer(atob(require("Storage").read(resource.file)));
|
||||
//print("buffer from file");
|
||||
}
|
||||
|
||||
var result = {
|
||||
width: resource.width,
|
||||
height: resource.height,
|
||||
bpp: resource.bpp,
|
||||
buffer: buffer
|
||||
};
|
||||
if (resource.transparent) result.transparent = resource.transparent;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getByPath(object, path, lastElem){
|
||||
var current = object;
|
||||
for (var c of path){
|
||||
if (!current[c]) return undefined;
|
||||
current = current[c];
|
||||
}
|
||||
if (lastElem!==undefined){
|
||||
if (!current["" + lastElem]) return undefined;
|
||||
current = current["" + lastElem];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function splitNumberToDigits(num){
|
||||
return String(num).split('').map(item => Number(item));
|
||||
}
|
||||
|
||||
function drawNumber(element, offset, number){
|
||||
//print("drawNumber: ", element, number);
|
||||
var isNegative;
|
||||
var digits;
|
||||
if (number == undefined){
|
||||
isNegative = false;
|
||||
digits = ["minus","minus","minus"];
|
||||
} else {
|
||||
isNegative = number < 0;
|
||||
if (isNegative) number *= -1;
|
||||
digits = splitNumberToDigits(number);
|
||||
}
|
||||
|
||||
//print("digits: ", digits);
|
||||
var numberOfDigits = element.Digits;
|
||||
if (!numberOfDigits) numberOfDigits = digits.length;
|
||||
var firstDigitX = element.TopLeftX;
|
||||
var firstDigitY = element.TopLeftY;
|
||||
var firstImage = getByPath(resources, element.ImagePath, 0);
|
||||
|
||||
if (element.Alignment == "BottomRight"){
|
||||
var digitWidth = firstImage.width + element.Spacing;
|
||||
var numberWidth = (numberOfDigits * digitWidth);
|
||||
if (isNegative){
|
||||
numberWidth += firstImage.width + element.Spacing;
|
||||
}
|
||||
//print("Number width: ", numberWidth, firstImage.width, element.Spacing);
|
||||
firstDigitX = element.BottomRightX - numberWidth + 1;
|
||||
firstDigitY = element.BottomRightY - firstImage.height + 1;
|
||||
//print("Calculated start " + firstDigitX + "," + firstDigitY + " From:" + element.BottomRightX + " " + firstImage.width + " " + element.Spacing);
|
||||
}
|
||||
var currentX = firstDigitX;
|
||||
|
||||
if (isNegative){
|
||||
drawElement({X:currentX,Y:firstDigitY}, offset, element.ImagePath, "minus");
|
||||
currentX += firstImage.width + element.Spacing;
|
||||
}
|
||||
|
||||
for (var d = 0; d < numberOfDigits; d++){
|
||||
var currentDigit;
|
||||
var difference = numberOfDigits - digits.length;
|
||||
if (d >= difference){
|
||||
currentDigit = digits[d-difference];
|
||||
} else {
|
||||
currentDigit = 0;
|
||||
}
|
||||
//print("Digit " + currentDigit + " " + currentX);
|
||||
drawElement({X:currentX,Y:firstDigitY}, offset, element.ImagePath, currentDigit);
|
||||
currentX += firstImage.width + element.Spacing;
|
||||
}
|
||||
}
|
||||
|
||||
function setColors(properties){
|
||||
if (properties.fg) g.setColor(properties.fg);
|
||||
if (properties.bg) g.setBgColor(properties.bg);
|
||||
}
|
||||
|
||||
function drawElement(pos, offset, path, lastElem){
|
||||
//print("drawElement ",pos, offset, path, lastElem);
|
||||
var image = getByPath(resources, path, lastElem);
|
||||
if (image){
|
||||
setColors(offset);
|
||||
g.drawImage(getImg(image),offset.X + pos.X,offset.Y + pos.Y);
|
||||
} else {
|
||||
print("Could not create image from", path, lastElem);
|
||||
}
|
||||
}
|
||||
|
||||
function drawScale(scale, offset, value){
|
||||
var segments = scale.Segments;
|
||||
for (var i = 0; i < value * segments.length; i++){
|
||||
drawElement(segments[i], offset, scale.ImagePath, i);
|
||||
}
|
||||
}
|
||||
|
||||
function drawDigit(element, offset, digit){
|
||||
drawElement(element, offset, element.ImagePath, digit);
|
||||
}
|
||||
|
||||
function drawMonthAndDay(element, offset){
|
||||
var date = new Date();
|
||||
|
||||
var dateOffset = updateOffset(element, offset);
|
||||
|
||||
if (element.Separate){
|
||||
var separateOffset = updateOffset(element.Separate, dateOffset);
|
||||
drawNumber(element.Separate.Month, separateOffset, date.getMonth() + 1);
|
||||
drawNumber(element.Separate.Day, separateOffset, date.getDate());
|
||||
}
|
||||
}
|
||||
|
||||
function drawImage(image, offset, name){
|
||||
if (image.ImagePath) {
|
||||
//print("drawImage", image, offset, name);
|
||||
drawElement(image, offset, image.ImagePath, name ? "" + name: undefined);
|
||||
} else if (image.ImageFile) {
|
||||
var file = require("Storage").readJSON(image.ImageFile);
|
||||
setColors(offset);
|
||||
g.drawImage(getImg(file),image.X + offset.X, image.Y + offsetY);
|
||||
}
|
||||
}
|
||||
|
||||
function drawWeather(element, offset){
|
||||
var jsonWeather = require("Storage").readJSON('weather.json');
|
||||
var weather = jsonWeather && jsonWeather.weather ? jsonWeather.weather : undefined;
|
||||
|
||||
var weatherOffset = updateOffset(element, offset);
|
||||
|
||||
var iconOffset = updateOffset(element.Icon, weatherOffset);
|
||||
if (weather && weather.code && element.Icon){
|
||||
var weathercode = weather.code;
|
||||
//print(getByPath(resources, element.Icon.CustomIcon.ImagePath, weathercode));
|
||||
if (!getByPath(resources, element.Icon.CustomIcon.ImagePath, weathercode)){
|
||||
weathercode = Math.floor(weathercode/10)*10;
|
||||
//print("Weathercode ", weathercode);
|
||||
}
|
||||
if (!getByPath(resources, element.Icon.CustomIcon.ImagePath, weathercode)){
|
||||
weathercode = Math.floor(weathercode/100)*100;
|
||||
//print("Weathercode ", weathercode);
|
||||
}
|
||||
if (getByPath(resources, element.Icon.CustomIcon.ImagePath, weathercode)){
|
||||
//print("Weathercode ", weathercode);
|
||||
drawImage(element.Icon.CustomIcon, offset, weathercode);
|
||||
}
|
||||
} else if (getByPath(resources, element.Icon.CustomIcon.ImagePath, "000")) {
|
||||
drawImage(element.Icon.CustomIcon, iconOffset, "000");
|
||||
}
|
||||
|
||||
if (element.Temperature){
|
||||
var tempOffset = updateOffset(element.Temperature, weatherOffset);
|
||||
if (weather && weather.temp && element.Temperature){
|
||||
drawNumber(element.Temperature.Current.Number, tempOffset, (weather.temp - 273.15).toFixed(0));
|
||||
} else {
|
||||
drawNumber(element.Temperature.Current.Number, tempOffset);
|
||||
}
|
||||
drawImage(element.Temperature.Current, tempOffset, "centigrade");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function updateOffset(element, offset){
|
||||
var newOffset = { X: offset.X, Y: offset.Y };
|
||||
if (element.X) newOffset.X += element.X;
|
||||
if (element.Y) newOffset.Y += element.Y;
|
||||
newOffset.fg = element.ForegroundColor ? element.ForegroundColor: offset.fg;
|
||||
newOffset.bg = element.BackgroundColor ? element.BackgroundColor: offset.bg;
|
||||
//print("Updated offset from ", offset, "to", newOffset);
|
||||
return newOffset;
|
||||
}
|
||||
|
||||
function drawTime(element, offset){
|
||||
var date = new Date();
|
||||
var hours = date.getHours();
|
||||
var minutes = date.getMinutes();
|
||||
|
||||
var offsetTime = updateOffset(element, offset);
|
||||
|
||||
var offsetHours = updateOffset(element.Hours, offsetTime);
|
||||
if (element.Hours.Tens) {
|
||||
drawDigit(element.Hours.Tens, offsetHours, Math.floor(hours/10));
|
||||
}
|
||||
|
||||
if (element.Hours.Ones) {
|
||||
drawDigit(element.Hours.Ones, offsetHours, hours % 10);
|
||||
}
|
||||
|
||||
var offsetMinutes = updateOffset(element.Minutes, offsetTime);
|
||||
if (element.Minutes.Tens) {
|
||||
drawDigit(element.Minutes.Tens, offsetMinutes, Math.floor(minutes/10));
|
||||
}
|
||||
|
||||
if (element.Minutes.Ones) {
|
||||
drawDigit(element.Minutes.Ones, offsetMinutes, minutes % 10);
|
||||
}
|
||||
}
|
||||
|
||||
function drawSteps(element, offset){
|
||||
//print("drawSteps", element, offset);
|
||||
if (Bangle.getHealthStatus) {
|
||||
drawNumber(element.Number, offset, Bangle.getHealthStatus("day").steps);
|
||||
} else {
|
||||
drawNumber(element.Number, offset);
|
||||
}
|
||||
}
|
||||
|
||||
function drawBattery(element, offset){
|
||||
if (element.Scale){
|
||||
drawScale(element.Scale, offset, E.getBattery()/100);
|
||||
}
|
||||
}
|
||||
|
||||
function drawStatus(element, offset){
|
||||
var statusOffset = updateOffset(element, offset);
|
||||
if (element.Lock) drawImage(element.Lock, statusOffset, Bangle.isLocked() ? "on" : "off");
|
||||
if (element.Charge) drawImage(element.Charge, statusOffset, Bangle.isCharging() ? "on" : "off");
|
||||
if (element.Bluetooth) drawImage(element.Bluetooth, statusOffset, NRF.getSecurityStatus().connected ? "on" : "off");
|
||||
if (element.Alarm) drawImage(element.Alarm, statusOffset, (require('Storage').readJSON('alarm.json',1)||[]).some(alarm=>alarm.on) ? "on" : "off");
|
||||
if (element.Notifications) drawImage(element.Notifications, statusOffset, ((require("Storage").readJSON("setting.json", 1) || {}).quiet|0) ? "soundoff" : "vibrate");
|
||||
}
|
||||
|
||||
function draw(element, offset){
|
||||
if (!element){
|
||||
element = face;
|
||||
g.clear();
|
||||
}
|
||||
g.setColor(g.theme.fg);
|
||||
g.setBgColor(g.theme.bg);
|
||||
|
||||
var elementOffset = updateOffset(element, offset);
|
||||
setColors(elementOffset);
|
||||
//print("Using offset", elementOffset);
|
||||
|
||||
//print("Starting drawing loop", element);
|
||||
for (var current in element){
|
||||
//print("Handling ", current, " with offset ", elementOffset);
|
||||
var currentElement = element[current];
|
||||
switch(current){
|
||||
case "X":
|
||||
case "Y":
|
||||
case "Properties":
|
||||
case "ForegroundColor":
|
||||
case "BackgroundColor":
|
||||
//Nothing to draw for these
|
||||
break;
|
||||
case "Background":
|
||||
drawImage(currentElement, elementOffset);
|
||||
break;
|
||||
case "Time":
|
||||
drawTime(currentElement, elementOffset);
|
||||
break;
|
||||
case "Battery":
|
||||
drawBattery(currentElement, elementOffset);
|
||||
break;
|
||||
case "Steps":
|
||||
drawSteps(currentElement, elementOffset);
|
||||
break;
|
||||
case "Pulse":
|
||||
if (pulse) drawNumber(currentElement.Number, elementOffset, pulse);
|
||||
break;
|
||||
case "Pressure":
|
||||
if (press) drawNumber(currentElement.Number, elementOffset, press.toFixed(0));
|
||||
break;
|
||||
case "Altitude":
|
||||
if (alt) drawNumber(currentElement.Number, elementOffset, alt.toFixed(0));
|
||||
break;
|
||||
case "Temperature":
|
||||
if (temp) drawNumber(currentElement.Number, elementOffset, temp.toFixed(0));
|
||||
break;
|
||||
case "MonthAndDay":
|
||||
drawMonthAndDay(currentElement, elementOffset);
|
||||
break;
|
||||
case "Weather":
|
||||
drawWeather(currentElement, elementOffset);
|
||||
break;
|
||||
case "Status":
|
||||
drawStatus(currentElement, elementOffset);
|
||||
break;
|
||||
default:
|
||||
//print("Enter next level", currentElement, elementOffset);
|
||||
draw(currentElement, elementOffset);
|
||||
}
|
||||
}
|
||||
//print("Finished drawing loop");
|
||||
}
|
||||
|
||||
var pulse,alt,temp,press;
|
||||
|
||||
|
||||
var zeroOffset={X:0,Y:0};
|
||||
|
||||
|
||||
function handleHrm(e){
|
||||
if (e.confidence > 70){
|
||||
pulse = e.bpm;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePressure(e){
|
||||
alt = e.altitude;
|
||||
temp = e.temperature;
|
||||
press = e.pressure;
|
||||
}
|
||||
|
||||
|
||||
var unlockedDrawInterval;
|
||||
|
||||
var lockedRedraw = getByPath(face, ["Properties","Redraw","Locked"]);
|
||||
var unlockedRedraw = getByPath(face, ["Properties","Redraw","Unlocked"]);
|
||||
|
||||
function handleLock(isLocked){
|
||||
if (!isLocked){
|
||||
Bangle.setHRMPower(1, "imageclock");
|
||||
Bangle.setBarometerPower(1, 'imageclock');
|
||||
unlockedDrawInterval = setInterval(()=>{
|
||||
draw(face, zeroOffset);
|
||||
},unlockedRedraw?unlockedRedraw:1000);
|
||||
draw(face, zeroOffset);
|
||||
} else {
|
||||
Bangle.setHRMPower(0, "imageclock");
|
||||
Bangle.setBarometerPower(0, 'imageclock');
|
||||
clearInterval(unlockedDrawInterval);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Bangle.setUI("clock");
|
||||
|
||||
Bangle.on('pressure', handlePressure);
|
||||
Bangle.on('HRM', handleHrm);
|
||||
Bangle.on('lock', handleLock);
|
||||
|
||||
|
||||
|
||||
draw(face, zeroOffset);
|
||||
|
||||
|
||||
setInterval(()=>{
|
||||
draw(face, zeroOffset);
|
||||
}, lockedRedraw ? lockedRedraw : 6000);
|
Binary file not shown.
After Width: | Height: | Size: 929 B |
|
@ -0,0 +1,174 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script src="../../core/lib/heatshrink.js"></script>
|
||||
<script src="../../core/lib/imageconverter.js"></script>
|
||||
<script src="../../core/lib/customize.js"></script>
|
||||
|
||||
<p>Upload watchface</p>
|
||||
|
||||
|
||||
<input type="file" id="fileLoader" name="files[]" multiple directory="" webkitdirectory="" moxdirectory="" /><br/>
|
||||
<button id="btnUpload">Upload to watch</button></br>
|
||||
<button id="btnSave">Download resources file</button></br>
|
||||
<canvas id="canvas" style="display:none;"></canvas></br>
|
||||
|
||||
<script>
|
||||
var result = "";
|
||||
var resultJson = {};
|
||||
|
||||
function imageLoaded() {
|
||||
var options = {};
|
||||
|
||||
options.diffusion = infoJson.diffusion ? infoJson.diffusion : "none";
|
||||
options.compression = false;
|
||||
options.alphaToColor = false;
|
||||
options.transparent = this.path.match(/\.t[^.]*\./);
|
||||
options.inverted = false;
|
||||
options.autoCrop = false;
|
||||
options.brightness = 0;
|
||||
options.contrast = 0;
|
||||
options.mode = infoJson.color ? infoJson.color : "1bpp";
|
||||
options.output = "jsonobject";
|
||||
|
||||
var canvas = document.getElementById("canvas")
|
||||
canvas.width = this.width*2;
|
||||
canvas.height = this.height;
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(this,0,0);
|
||||
|
||||
var imgstr = "";
|
||||
|
||||
|
||||
var imageData = ctx.getImageData(0, 0, this.width, this.height);
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(options.width, 0, this.width, this.height);
|
||||
var rgba = imageData.data;
|
||||
options.rgbaOut = rgba;
|
||||
options.width = this.width;
|
||||
options.height = this.height;
|
||||
imgstr = imageconverter.RGBAtoString(rgba, options);
|
||||
var outputImageData = new ImageData(options.rgbaOut, options.width, options.height);
|
||||
ctx.putImageData(outputImageData,this.width,0);
|
||||
|
||||
|
||||
// checkerboard for transparency on original image
|
||||
var imageData = ctx.getImageData(0, 0, this.width, this.height);
|
||||
imageconverter.RGBAtoCheckerboard(imageData.data, {width:this.width,height:this.height});
|
||||
ctx.putImageData(imageData,0,0);
|
||||
|
||||
console.log("result is ", imgstr);
|
||||
|
||||
var faceJson;
|
||||
var jsonPath = this.path.replace(/^[^\/\\]*[\/\\](.*)(\.t[^.]){0,1}\.[^.]+/, "$1").split("/");
|
||||
|
||||
var currentElement = resultJson;
|
||||
|
||||
for (var i = 0; i < jsonPath.length; i++){
|
||||
if (i == jsonPath.length - 1){
|
||||
currentElement[jsonPath[i]] = JSON.parse(imgstr);
|
||||
} else {
|
||||
if (!currentElement[jsonPath[i]]) currentElement[jsonPath[i]] = {};
|
||||
currentElement = currentElement[jsonPath[i]];
|
||||
}
|
||||
}
|
||||
|
||||
result += '"' + jsonPath.join(".") + '":"' + imgstr + '"' + ",\n";
|
||||
|
||||
console.log("json is ", result);
|
||||
}
|
||||
|
||||
function handleWatchFace(infoFile, faceFile, resourceFiles){
|
||||
var reader = new FileReader();
|
||||
reader.path = infoFile.webkitRelativePath;
|
||||
reader.onload = function(event) {
|
||||
infoJson = JSON.parse(reader.result);
|
||||
handleFaceJson(faceFile, resourceFiles);
|
||||
};
|
||||
reader.readAsText(infoFile);
|
||||
}
|
||||
|
||||
function handleFaceJson(faceFile, resourceFiles){
|
||||
var reader = new FileReader();
|
||||
reader.path = faceFile.webkitRelativePath;
|
||||
reader.onload = function(event) {
|
||||
faceJson = JSON.parse(reader.result);
|
||||
handleResourceFiles(resourceFiles);
|
||||
};
|
||||
reader.readAsText(faceFile);
|
||||
}
|
||||
|
||||
function handleResourceFiles(files){
|
||||
for (var current of files){
|
||||
console.log('Handle resource file ', current);
|
||||
var reader = new FileReader();
|
||||
reader.path = current.webkitRelativePath.replace("resources/","");
|
||||
reader.onload = function(event) {
|
||||
var img = new Image();
|
||||
img.path = this.path;
|
||||
img.onload = imageLoaded;
|
||||
img.src = event.target.result;
|
||||
};
|
||||
reader.readAsDataURL(current);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(event) {
|
||||
console.log("File select event", event);
|
||||
if (event.target.files.length == 0) return;
|
||||
result = "";
|
||||
resultJson= {};
|
||||
|
||||
var resourceFiles = [];
|
||||
var faceFile;
|
||||
var infoFile;
|
||||
|
||||
for (var current of event.target.files){
|
||||
console.log('Handle file ', current);
|
||||
if (current.webkitRelativePath.split("/")[1].startsWith("resources")){
|
||||
console.log('Found resource file', current.name);
|
||||
resourceFiles.push(current);
|
||||
} else if (current.name == "face.json"){
|
||||
console.log('Found face file', current.name);
|
||||
faceFile = current;
|
||||
} else if (current.name == "info.json"){
|
||||
console.log('Found info file', current.name);
|
||||
infoFile = current;
|
||||
} else {
|
||||
console.log('Found unsupported file', current.name);
|
||||
}
|
||||
}
|
||||
handleWatchFace(infoFile, faceFile, resourceFiles);
|
||||
|
||||
};
|
||||
document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false);
|
||||
|
||||
document.getElementById("btnSave").addEventListener("click", function() {
|
||||
var h = document.createElement('a');
|
||||
h.href = 'data:text/json;charset=utf-8,' + encodeURI(JSON.stringify(resultJson));
|
||||
h.target = '_blank';
|
||||
h.download = "imageclock.resources.json";
|
||||
h.click();
|
||||
});
|
||||
document.getElementById("btnUpload").addEventListener("click", function() {
|
||||
var appDef = {
|
||||
id : "imageclock",
|
||||
storage:[
|
||||
{name:"imageclock.app.js", url:"app.js"},
|
||||
{name:"imageclock.face.json", content: JSON.stringify(faceJson)},
|
||||
{name:"imageclock.resources.json", content: JSON.stringify(resultJson)},
|
||||
{name:"imageclock.img", url:"app-icon.js", evaluate:true},
|
||||
]
|
||||
};
|
||||
sendCustomizedApp(appDef);
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"id": "imageclock",
|
||||
"name": "imageclock",
|
||||
"shortName": "imageclock",
|
||||
"version": "0.01",
|
||||
"type": "clock",
|
||||
"description": "",
|
||||
"icon": "app.png",
|
||||
"tags": "clock",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"custom": "custom.html",
|
||||
"customConnect": false,
|
||||
"storage": [
|
||||
{"name":"imageclock.app.js","url":"app.js"},
|
||||
{"name":"imageclock.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue