Add 'clock backgrounds' app that other clocks can use to provide custom backgrounds - fix #3287

pull/3328/head
Gordon Williams 2024-04-04 12:56:26 +01:00
parent 21a17cc751
commit fdab58c5c0
45 changed files with 400 additions and 34 deletions

1
apps/clockbg/ChangeLog Normal file
View File

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

40
apps/clockbg/README.md Normal file
View File

@ -0,0 +1,40 @@
# Clock Backgrounds
This app provides a library (`clockbg`) that can be used by clocks to
provide different backgrounds for them.
## Usage
By default the app provides just a red/green/blue background but it can easily be configured.
You can either:
* Go to [the Clock Backgrounds app](https://banglejs.com/apps/?id=clockbg) in the App Loader and upload backgrounds
* Go to the `Backgrounds` app on the Bangle itself, and choose between solid color, random colors, or any uploaded images.
## Usage in code
Just use the following to use this library within your code:
```JS
// once at the start
let background = require("clockbg");
// to fill the whole area
background.fillRect(Bangle.appRect);
// to fill just one part of the screen
background.fillRect(x1, y1, x2, y2);
```
You should also add `"dependencies" : { "clockbg":"module" },` to your app's metadata to
ensure that the clock background library is automatically loaded.
## Features to be added
This library/app is still pretty basic right now, but a few features could be added that would really improve functionality:
* Support for >1 image, and choosing randomly between them
* Support for gradients (random colors)
* Storing 'clear' areas of uploaded images so clocks can easily position themselves

1
apps/clockbg/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA///8H55lCsHZHmEKgEVqoLIBQNVqgLGh4KBAANQBAUQBY1VoApBB4QLFDYoL/Bf4L/BbLrBBZAKBgEBBY0KAQIABgoLCCYQLEgEVBQYLGAAoL/Bf4LPAFw"))

88
apps/clockbg/app.js Normal file
View File

@ -0,0 +1,88 @@
let settings = Object.assign({
style : "randomcolor",
colors : ["#F00","#0F0","#00F"]
},require("Storage").readJSON("clockbg.json")||{});
function saveSettings() {
if (settings.style!="image")
delete settings.fn;
if (settings.style!="color")
delete settings.color;
if (settings.style!="randomcolor")
delete settings.colors;
require("Storage").writeJSON("clockbg.json", settings);
}
function getColorsImage(cols) {
var b = Graphics.createArrayBuffer(16*cols.length,16,16);
cols.forEach((c,i)=>{
b.setColor(c).fillRect(i*16,0,i*16+15,15);
});
return "\0"+b.asImage("string");
}
function showModeMenu() {
E.showMenu({
"" : {title:/*LANG*/"Background", back:showMainMenu},
/*LANG*/"Solid Color" : function() {
var cols = ["#F00","#0F0","#FF0",
"#00F","#F0F","#0FF",
"#000","#888","#fff",];
var menu = {"":{title:/*LANG*/"Colors", back:showModeMenu}};
cols.forEach(col => {
menu["-"+getColorsImage([col])] = () => {
settings.style = "color";
settings.color = col;
saveSettings();
showMainMenu();
};
});
E.showMenu(menu);
},
/*LANG*/"Random Color" : function() {
var cols = [
["#F00","#0F0","#FF0","#00F","#F0F","#0FF"],
["#F00","#0F0","#00F"],
];
var menu = {"":{title:/*LANG*/"Colors", back:showModeMenu}};
cols.forEach(col => {
menu[getColorsImage(col)] = () => {
settings.style = "randomcolor";
settings.colors = col;
saveSettings();
showMainMenu();
};
});
E.showMenu(menu);
},
/*LANG*/"Image" : function() {
let images = require("Storage").list(/clockbg\..*\.img/);
if (images.length) {
var menu = {"":{title:/*LANG*/"Images", back:showModeMenu}};
images.forEach(im => {
menu[im.slice(8,-4)] = () => {
settings.style = "image";
settings.fn = im;
saveSettings();
showMainMenu();
};
});
E.showMenu(menu);
} else {
E.showAlert("Please use App Loader to upload images").then(showModeMenu);
}
},
});
}
function showMainMenu() {
E.showMenu({
"" : {title:/*LANG*/"Clock Background", back:load},
/*LANG*/"Mode" : {
value : settings.style,
onchange : showModeMenu
}
});
}
showMainMenu();

BIN
apps/clockbg/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,19 @@
Clock Images
=============
If you want to add your own images ensure they're in the same style and then also list the image file in custom.html in the root directory.
## Flags
The flags come from https://icons8.com/icon/set/flags/color and are 480x480px
If your flag is listed in https://icons8.com/icon/set/flags/color and you can't download it in the right size, please file an issue and we'll download it with our account.
## Other backgrounds
Backgrounds prefixed `ai_` are generated by the AI [Bing Image Creator](https://www.bing.com/images/create)

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

179
apps/clockbg/interface.html Normal file
View File

@ -0,0 +1,179 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
<style>
.flag {
width : 100px;
cursor: pointer;
}
#preview {
width : 176px;
height: 176px;
border: 1px solid black;
}
</style>
</head>
<body>
<p>Upload an image:</p>
<div id="flaglist"></div>
<p>If you'd like to contribute images you can <a href="https://github.com/espruino/BangleApps/tree/master/apps/patriotclk/img" target="_blank">add them on GitHub</a>!</p>
<div style="float:right">Preview:<br/><canvas width="176" height="176" id="preview"></canvas></div>
<div class="form-group">
<label class="form-switch">
<input type="checkbox" id="box_zoom">
<i class="form-icon"></i> Zoom
</label>
<label class="form-switch">
<input type="checkbox" id="box_mirror">
<i class="form-icon"></i> Mirror
</label>
<select class="form-select" id="box_bright" style="width:inherit">
<option value="0">Normal Brightness</option>
<option value="1">Brighten</option>
<option value="-1">Darken</option>
</select><br/>
<select class="form-select" id="box_dither" style="width:inherit">
<option value="">Default Dither</option>
<option value="no">No Dither</option>
<option value="bayer2">Bayer Dither</option>
<option value="error">Diffusion Dither</option>
<option value="random1">Random Dither</option>
<option value="comic">'Comic' Dither</option>
</select>
</div>
<p>Click <button id="upload" class="btn btn-primary disabled" style>Upload</button></p>
<script src="../../core/lib/interface.js"></script>
<script src="../../webtools/imageconverter.js"></script>
<script>
const IMAGES =[
{"path":"img/ai_rock.jpeg","dither":true},
{"path":"img/ai_robot.jpeg","dither":true},
{"path":"img/ai_eye.jpeg","dither":true},
{"path":"img/ai_hero.jpeg","dither":true},
{"path":"img/icons8-australia-480.png","dither":false},
{"path":"img/icons8-austria-480.png","dither":false},
{"path":"img/icons8-belgium-480.png","dither":false},
{"path":"img/icons8-brazil-480.png","dither":false},
{"path":"img/icons8-canada-480.png","dither":false},
{"path":"img/icons8-china-480.png","dither":false},
{"path":"img/icons8-denmark-480.png","dither":false},
{"path":"img/icons8-england-480.png","dither":false},
{"path":"img/icons8-flag-of-europe-480.png","dither":false},
{"path":"img/icons8-france-480.png","dither":false},
{"path":"img/icons8-germany-480.png","dither":false},
{"path":"img/icons8-great-britain-480.png","dither":false},
{"path":"img/icons8-greece-480.png","dither":false},
{"path":"img/icons8-hungary-480.png","dither":false},
{"path":"img/icons8-italy-480.png","dither":false},
{"path":"img/icons8-netherlands-480.png","dither":false},
{"path":"img/icons8-new-zealand-480.png","dither":false},
{"path":"img/icons8-norway-480.png","dither":false},
{"path":"img/icons8-scotland-480.png","dither":false},
{"path":"img/icons8-spain-480.png","dither":false},
{"path":"img/icons8-sweden-480.png","dither":false},
{"path":"img/icons8-switzerland-480.png","dither":false},
{"path":"img/icons8-usa-480.png","dither":false},
{"path":"img/icons8-wales-480.png","dither":false},
{"path":"img/icons8-lgbt-flag-480.png","dither":true},
{"path":"img/icons8-ukraine-480.png","dither":true}];
var selectedImage;
var bgImageData;
document.getElementById("flaglist").innerHTML =
IMAGES.map(f => `<img class="flag" src="${f.path}" data-file="${f.path}"/>`).join("\n");
var elements = document.querySelectorAll(".flag");
for (var i=0;i<elements.length;i++)
elements[i].addEventListener("click", function(e) {
selectedImage = e.target;
drawPreview();
document.getElementById("upload").classList.remove("disabled")
});
function drawPreview() {
if (!selectedImage) return;
var imgPath = selectedImage.getAttribute("data-file");
var img = IMAGES.find(img => img.path == imgPath);
var zoom = document.getElementById("box_zoom").checked;
var dither = document.getElementById("box_dither").value;
if (dither=="" && img.dither) dither="bayer2";
if (dither=="no" || dither=="") dither=undefined;
var mirror = document.getElementById("box_mirror").checked;
var brightness = 0|document.getElementById("box_bright").value;
const canvas = document.getElementById("preview");
canvas.width = 176; // setting size clears canvas
canvas.height = 176;
const ctx = canvas.getContext("2d");
var y = 0;
console.log(selectedImage);
let imgW = selectedImage.naturalWidth;
let imgH = selectedImage.naturalHeight;
let border = 0;
if (imgW > 400) border = 20;
if (zoom) border = (border*5) >> 1;
ctx.save(); // Save the current state
if (mirror) {
ctx.translate(canvas.width, 0);
ctx.scale(-1, 1);
}
ctx.drawImage(selectedImage, border, border, imgW-border*2, imgH-border*2, 0, y, canvas.width, canvas.height);
ctx.restore();
var options = {
mode:"3bit",
output:"raw",
compression:false,
updateCanvas:true,
transparent:false,
diffusion:dither,
contrast: brightness ? -64 : 64,
brightness:64*brightness
};
bgImageData = imageconverter.canvastoString(canvas, options);
}
// If options changed
document.getElementById("box_zoom").addEventListener("click", function() {
drawPreview();
});
document.getElementById("box_dither").addEventListener("click", function() {
drawPreview();
});
document.getElementById("box_mirror").addEventListener("click", function() {
drawPreview();
});
document.getElementById("box_bright").addEventListener("click", function() {
drawPreview();
});
// When the 'upload' button is clicked...
document.getElementById("upload").addEventListener("click", function() {
let settings = {
style : "image",
fn : "clockbg.bg0.img"
};
Util.showModal("Uploading Image...");
Util.writeStorage(settings.fn, bgImageData, function(data) {
Util.writeStorage("clockbg.json", JSON.stringify(settings), function(data) {
Util.hideModal()
});
});
});
function onInit() {
Util.readStorageJSON("clockbg.json", function(data) {
let settings = Object.assign({
style : "randomcolor",
colors : ["#F00","#0F0","#00F"]
},data);
console.log(settings);
});
}
</script>
</body>
</html>

25
apps/clockbg/lib.js Normal file
View File

@ -0,0 +1,25 @@
let settings = Object.assign({
style : "randomcolor",
colors : ["#F00","#0F0","#00F"]
},require("Storage").readJSON("clockbg.json")||{});
if (settings.style=="image")
settings.img = require("Storage").read(settings.fn);
if (settings.style=="randomcolor") {
settings.style = "color";
var n = (0|(Math.random()*settings.colors.length)) % settings.colors.length;
settings.color = settings.colors[n];
}
// Fill a rectangle with the current background style, rect = {x,y,w,h}
// eg require("clockbg").fillRect({x:10,y:10,w:50,h:50})
// require("clockbg").fillRect(Bangle.appRect)
exports.fillRect = function(rect,y,x2,y2) {
if ("object"!=typeof rect) rect = {x:rect,y:y,w:1+x2-rect,h:1+y2-y};
if (settings.img) {
g.setClipRect(rect.x, rect.y, rect.x+rect.w-1, rect.y+rect.h-1).drawImage(settings.img).setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
} else if (settings.style == "color") {
g.setBgColor(settings.color).clearRect(rect);
} else
console.log("No background set");
};

View File

@ -0,0 +1,22 @@
{ "id": "clockbg",
"name": "Clock Backgrounds",
"shortName":"Backgrounds",
"version": "0.01",
"description": "Library that allows clocks to include a custom background, from a library or uploaded.",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "app",
"readme": "README.md",
"provides_modules" : ["clockbg"],
"tags": "module,background",
"supports" : ["BANGLEJS2"],
"interface": "interface.html",
"storage": [
{"name":"clockbg","url":"lib.js"},
{"name":"clockbg.app.js","url":"app.js"},
{"name":"clockbg.img","url":"app-icon.js","evaluate":true}
], "data": [
{"wildcard":"clockbg.bg*.img"},
{"name":"clockbg.json"}
]
}

BIN
apps/clockbg/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -4,3 +4,4 @@
0.03: Use smaller font if clock_info test doesn't fit in area
0.04: Ensure we only scale down clockinfo text if it really won't fit
0.05: Minor code improvements
0.06: Use the clockbg library to allow custom image backgrounds

View File

@ -20,7 +20,8 @@ Graphics.prototype.setFontLECO1976Regular14 = function() {
{
const SETTINGS_FILE = "pebblepp.json";
let settings = require("Storage").readJSON(SETTINGS_FILE,1)|| {'bg': '#0f0', 'color': 'Green', 'theme':'System', 'showlock':false};
let settings = require("Storage").readJSON(SETTINGS_FILE,1)|| {'theme':'System', 'showlock':false};
let background = require("clockbg");
let theme;
let drawTimeout;
@ -50,7 +51,7 @@ let draw = function() {
};
let loadThemeColors = function() {
theme = {fg: g.theme.fg, bg: g.theme.bg, day: g.toColor(0,0,0)};
theme = {fg: g.theme.fg, bg: g.theme.bg };
if (settings.theme === "Dark") {
theme.fg = g.toColor(1,1,1);
theme.bg = g.toColor(0,0,0);
@ -58,13 +59,15 @@ let loadThemeColors = function() {
theme.fg = g.toColor(0,0,0);
theme.bg = g.toColor(1,1,1);
}
// day and steps
if (settings.color == 'Blue' || settings.color == 'Red')
theme.day = g.toColor(1,1,1); // white on blue or red best contrast
};
loadThemeColors();
// Load the clock infos
let clockInfoW = 0|(w/2);
let clockInfoH = 0|(h/2);
let clockInfoG = Graphics.createArrayBuffer(25, 25, 2, {msb:true});
clockInfoG.transparent = 3;
clockInfoG.palette = new Uint16Array([g.theme.bg, g.theme.fg, g.toColor("#888"), g.toColor("#888")]);
let clockInfoItems = require("clock_info").load();
let clockInfoDraw = (itm, info, options) => {
// itm: the item containing name/hasRange/etc
@ -72,25 +75,26 @@ let clockInfoDraw = (itm, info, options) => {
// options: options passed into addInteractive
// Clear the background - if focussed, add a border
g.reset().setBgColor(theme.bg).setColor(theme.fg);
var b = 0; // border
var y,b = 0; // border
if (options.focus) { // white border
b = 4;
g.clearRect(options.x, options.y, options.x+options.w-1, options.y+options.h-1);
}
g.setBgColor(settings.bg).clearRect(options.x+b, options.y+b, options.x+options.w-1-b, options.y+options.h-1-b);
background.fillRect(options.x+b, options.y+b, options.x+options.w-1-b, options.y+options.h-1-b);
// we're drawing center-aligned here
var midx = options.x+options.w/2;
if (info.img) { // draw the image
// TODO: we could replace certain images with our own ones here...
var y = options.y+8;
y = options.y+8;
if (g.floodFill) {
/* img is (usually) a black and white transparent image. But we really would like the bits in
the middle of it to be white. So what we do is we draw a slightly bigger rectangle in white,
draw the image, and then flood-fill the rectangle back to the background color. floodFill
was only added in 2v18 so we have to check for it and fallback if not. */
g.setBgColor(theme.bg).clearRect(midx-25,y-1,midx+24,y+48);
g.drawImage(info.img, midx-24,y,{scale:2});
g.floodFill(midx-25,y,settings.bg);
clockInfoG.setBgColor(0).clearRect(0,0,24,24);
clockInfoG.setColor(1).drawImage(info.img, 0,0);
clockInfoG.floodFill(24,24,3);
g.drawImage(clockInfoG, midx-24,y,{scale:2});
} else { // fallback
g.drawImage(info.img, midx-24,y,{scale:2});
}
@ -103,17 +107,18 @@ let clockInfoDraw = (itm, info, options) => {
var l = g.wrapString(txt, options.w);
txt = l.slice(0,2).join("\n") + (l.length>2)?"...":"";
}
g.drawString(txt, midx,options.y+options.h-12); // draw the text
y = options.y+options.h-12;
g.drawString(txt, midx, y); // draw the text
};
let clockInfoMenuA = require("clock_info").addInteractive(clockInfoItems, {
app:"pebblepp",
x : 0, y: 0, w: w/2, h:h/2,
x : 0, y: 0, w: clockInfoW, h:clockInfoH,
draw : clockInfoDraw
});
let clockInfoMenuB = require("clock_info").addInteractive(clockInfoItems, {
app:"pebblepp",
x : w/2, y: 0, w: w/2, h:h/2,
x : w/2, y: 0, w: clockInfoW, h:clockInfoH,
draw : clockInfoDraw
});
@ -134,7 +139,7 @@ Bangle.setUI({
Bangle.loadWidgets();
require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe
g.setBgColor(settings.bg).clear(); // start off with completely clear background
background.fillRect(Bangle.appRect); // start off with completely clear background
// contrast bar (top)
g.setColor(theme.fg).fillRect(0, h2 - 6, w, h2);
// contrast bar (bottom)

View File

@ -2,15 +2,14 @@
"id": "pebblepp",
"name": "Pebble++ Clock",
"shortName": "Pebble++",
"version": "0.05",
"version": "0.06",
"description": "A pebble style clock (based on the 'Pebble Clock' app) but with two configurable ClockInfo items at the top",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock,clkinfo",
"supports": ["BANGLEJS2"],
"dependencies" : { "clock_info":"module" },
"allow_emulator": true,
"dependencies" : { "clock_info":"module", "clockbg":"module" },
"storage": [
{"name":"pebblepp.app.js","url":"app.js"},
{"name":"pebblepp.settings.js","url":"settings.js"},

View File

@ -1,10 +1,8 @@
(function(back) {
const SETTINGS_FILE = "pebblepp.json";
// TODO Only the color/theme indices should be written in the settings file so the labels can be translated
// Initialize with default settings...
let s = {'bg': '#0f0', 'color': 'Green', 'theme':'System', 'showlock':false}
let s = {'theme':'System', 'showlock':false}
// ...and overwrite them with any saved values
// This way saved values are preserved if a new version adds more settings
@ -20,23 +18,11 @@
storage.write(SETTINGS_FILE, settings);
}
var color_options = ['Green','Orange','Cyan','Purple','Red','Blue'];
var bg_code = ['#0f0','#ff0','#0ff','#f0f','#f00','#00f'];
var theme_options = ['System', 'Light', 'Dark'];
E.showMenu({
'': { 'title': 'Pebble++ Clock' },
/*LANG*/'< Back': back,
/*LANG*/'Colour': {
value: 0 | color_options.indexOf(s.color),
min: 0, max: 5,
format: v => color_options[v],
onchange: v => {
s.color = color_options[v];
s.bg = bg_code[v];
save();
}
},
/*LANG*/'Theme': {
value: 0 | theme_options.indexOf(s.theme),
min: 0, max: theme_options.length - 1,

@ -1 +1 @@
Subproject commit 7e4283948f68b1dd14a59a73e544c537a26c800a
Subproject commit 3a55438189240c0f40cf280128d663806d04a5be