Merge branch 'espruino:master' into master

master
awkirk71 2021-09-27 22:10:49 +01:00 committed by GitHub
commit 292bf52fa0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 208 additions and 85 deletions

View File

@ -2522,7 +2522,7 @@
"name": "Cycling speed sensor",
"shortName":"CSCSensor",
"icon": "icons8-cycling-48.png",
"version":"0.04",
"version":"0.05",
"description": "Read BLE enabled cycling speed and cadence sensor and display readings on watch",
"tags": "outdoors,exercise,ble,bluetooth",
"readme": "README.md",
@ -3540,7 +3540,8 @@
"icon": "app.png",
"version":"0.01",
"description": "A simple clock using the bold Anton font.",
"tags": "clock,b2",
"tags":"clock,b2",
"type":"clock",
"storage": [
{"name":"antonclk.app.js","url":"app.js"},
{"name":"antonclk.img","url":"app-icon.js","evaluate":true}
@ -3551,7 +3552,8 @@
"icon": "app.png",
"version":"0.01",
"description": "A clock using a wave image by [Lillith May](https://www.instagram.com/_lilustrations_/). **Note: This requires a bugfix for #2049 on Bangle.js 1**",
"tags": "clock,b2",
"tags":"clock,b2",
"type":"clock",
"storage": [
{"name":"waveclk.app.js","url":"app.js"},
{"name":"waveclk.img","url":"app-icon.js","evaluate":true}

View File

@ -1 +1 @@
require("heatshrink").decompress(atob("mEwge27dtAX4C+/dt+wFB/wCECIu3/dvBYNv34RC7/tCIu//99EYN9C4IpB74jG3379ovDFIIRBEYxHD/47D2wjHCIX+AQJHBCIIXBNZt/+5QBEZIgBAQX///9EZRWBBARHDEwhlC/9/EAJoBDQIOBNwyPCEYYCDJQ4CJSQ4CB0O2lojL2lwBIXFiwsK0f/KgUbuwRJo6cBPAO34cUmJHH7U/97tBgEGBIODEY/RXoOw7cAgHbtlxoojGx7hCjAjD20ANA1378MEIIAB4d0u5HGNAPYCAYAB2n2SQSPDjv3CIsF2lxEYto//+CoOGCIUt0O3EYtHvqMBvlw4UAgJQBqIjERgQDBsO+7FAhaMH64DB+4qB+3AgARG9uhIgQJD4dghd+7dLBQZoBaISwC4cArf27dpCIf/23f9uHCIQABhoNClsl20ttuwgYKBEAIAChOmCIOH/vx9ttwB2BDgMBAoIRBmnbpkbtk2ltsgAMCJQOwAgMBk+eq3AhiSBsE2GAX//4WCAAOBAoVbtt8mCJBgHHfYMHdgoRBott+zmDsEAn/tEwkC7UAVoYACgPbv4REAASTBEYY0BPIPwCJAjEu3Dvq8BCAnbsEwGgm2jbAC8EAjFvEAQREjuwDQXbvvx7cd2K/EgEb9oRCAoLOBjEAgk/A="))
require("heatshrink").decompress(atob("mEwgX/AH4A/AAf+14BBAoPq/Wq1QFB+EP+kLAoNA+CdB3//yEP6ALB/sABYf8gEMgALB6ALJqoLB+tVgH//kQhkQhUABYImCIwPwh3whcA94UCgJHD+AXB/4LCcQQLCKYQLB+EDBZQFCBY/Qh4LJ4ALLl4LFioLCgE/KZMAv4XFBYimBC4/+BYw7DRwQLIV4ILWTQQLDOIUA/ALDAC+t1uv/263/6/Pq/YLC3vv//vM4Oq33rBYP6/u//2/C4Pr1YXC12vBwO+BYO69XmJDQA/ACIA=="))

View File

@ -2,4 +2,4 @@
0.02: Add wheel circumference settings dialog
0.03: Save total distance traveled
0.04: Add sensor battery level indicator
0.05: Add cadence sensor support

View File

@ -13,6 +13,6 @@ Currently the app displays the following data:
Button 1 resets all measurements except total distance traveled. The latter gets preserved by being written to storage every 0.1 miles and upon exiting the app.
If the watch app has not received an update from the sensor for at least 10 seconds, pushing button 3 will attempt to reconnect to the sensor.
Button 2 switches between the display for cycling speed and cadence.
I do not have access to a cadence sensor at the moment, so only the speed part is currently implemented. Values displayed are imperial or metric (depending on locale),
the wheel circumference can be adjusted in the global settings app.
Values displayed are imperial or metric (depending on locale), cadence is in RPM, the wheel circumference can be adjusted in the global settings app.

View File

@ -28,6 +28,10 @@ class CSCSensor {
this.distFactor = this.qMetric ? 1.609344 : 1;
this.screenInit = true;
this.batteryLevel = -1;
this.lastCrankTime = 0;
this.lastCrankRevs = 0;
this.showCadence = false;
this.cadence = 0;
}
reset() {
@ -40,6 +44,11 @@ class CSCSensor {
this.screenInit = true;
}
toggleDisplayCadence() {
this.showCadence = !this.showCadence;
this.screenInit = true;
}
setBatteryLevel(level) {
if (level!=this.batteryLevel) {
this.batteryLevel = level;
@ -62,7 +71,7 @@ class CSCSensor {
else g.setFontVector(14).setFontAlign(0, 0, 0).setColor(0xffff).drawString("?", 16, 66);
}
updateScreen() {
updateScreenRevs() {
var dist = this.distFactor*(this.lastRevs-this.lastRevsStart)*this.wheelCirc/63360.0;
var ddist = Math.round(100*dist)/100;
var tdist = Math.round(this.distFactor*this.totaldist*10)/10;
@ -108,45 +117,88 @@ class CSCSensor {
g.setColor(0).fillRect(88, 209, 238, 238);
g.setColor(0xffff).drawString(tdist + " " + this.distUnit, 92, 226);
}
updateScreenCadence() {
if (this.screenInit) {
for (var i=0; i<2; ++i) {
if ((i&1)==0) g.setColor(0, 0, 0);
else g.setColor(0x30cd);
g.fillRect(0, 48+i*32, 86, 48+(i+1)*32);
if ((i&1)==1) g.setColor(0);
else g.setColor(0x30cd);
g.fillRect(87, 48+i*32, 239, 48+(i+1)*32);
g.setColor(0.5, 0.5, 0.5).drawRect(87, 48+i*32, 239, 48+(i+1)*32).drawLine(0, 239, 239, 239);//.drawRect(0, 48, 87, 239);
g.moveTo(0, 80).lineTo(30, 80).lineTo(30, 48).lineTo(87, 48).lineTo(87, 239).lineTo(0, 239).lineTo(0, 80);
}
g.setFontAlign(1, 0, 0).setFontVector(19).setColor(1, 1, 0);
g.drawString("Cadence:", 87, 98);
this.drawBatteryIcon();
this.screenInit = false;
}
g.setFontAlign(-1, 0, 0).setFontVector(26);
g.setColor(0).fillRect(88, 81, 238, 111);
g.setColor(0xffff).drawString(Math.round(this.cadence), 92, 98);
}
updateScreen() {
if (!this.showCadence) {
this.updateScreenRevs();
} else {
this.updateScreenCadence();
}
}
updateSensor(event) {
var qChanged = false;
if (event.target.uuid == "0x2a5b") {
var wheelRevs = event.target.value.getUint32(1, true);
var dRevs = (this.lastRevs>0 ? wheelRevs-this.lastRevs : 0);
if (dRevs>0) {
qChanged = true;
this.totaldist += dRevs*this.wheelCirc/63360.0;
if ((this.totaldist-this.settings.totaldist)>0.1) {
this.settings.totaldist = this.totaldist;
storage.writeJSON(SETTINGS_FILE, this.settings);
if (event.target.value.getUint8(0, true) & 0x2) {
// crank revolution
const crankRevs = event.target.value.getUint16(1, true);
const crankTime = event.target.value.getUint16(3, true);
if (crankTime > this.lastCrankTime) {
this.cadence = (crankRevs-this.lastCrankRevs)/(crankTime-this.lastCrankTime)*(60*1024);
qChanged = true;
}
}
this.lastRevs = wheelRevs;
if (this.lastRevsStart<0) this.lastRevsStart = wheelRevs;
var wheelTime = event.target.value.getUint16(5, true);
var dT = (wheelTime-this.lastTime)/1024;
var dBT = (Date.now()-this.lastBangleTime)/1000;
this.lastBangleTime = Date.now();
if (dT<0) dT+=64;
if (Math.abs(dT-dBT)>3) dT = dBT;
this.lastTime = wheelTime;
this.speed = this.lastSpeed;
if (dRevs>0 && dT>0) {
this.speed = (dRevs*this.wheelCirc/63360.0)*3600/dT;
this.speedFailed = 0;
this.movingTime += dT;
}
else {
this.speedFailed++;
qChanged = false;
if (this.speedFailed>3) {
this.speed = 0;
qChanged = (this.lastSpeed>0);
this.lastCrankRevs = crankRevs;
this.lastCrankTime = crankTime;
} else {
// wheel revolution
var wheelRevs = event.target.value.getUint32(1, true);
var dRevs = (this.lastRevs>0 ? wheelRevs-this.lastRevs : 0);
if (dRevs>0) {
qChanged = true;
this.totaldist += dRevs*this.wheelCirc/63360.0;
if ((this.totaldist-this.settings.totaldist)>0.1) {
this.settings.totaldist = this.totaldist;
storage.writeJSON(SETTINGS_FILE, this.settings);
}
}
this.lastRevs = wheelRevs;
if (this.lastRevsStart<0) this.lastRevsStart = wheelRevs;
var wheelTime = event.target.value.getUint16(5, true);
var dT = (wheelTime-this.lastTime)/1024;
var dBT = (Date.now()-this.lastBangleTime)/1000;
this.lastBangleTime = Date.now();
if (dT<0) dT+=64;
if (Math.abs(dT-dBT)>3) dT = dBT;
this.lastTime = wheelTime;
this.speed = this.lastSpeed;
if (dRevs>0 && dT>0) {
this.speed = (dRevs*this.wheelCirc/63360.0)*3600/dT;
this.speedFailed = 0;
this.movingTime += dT;
}
else {
this.speedFailed++;
qChanged = false;
if (this.speedFailed>3) {
this.speed = 0;
qChanged = (this.lastSpeed>0);
}
}
this.lastSpeed = this.speed;
if (this.speed>this.maxSpeed && (this.movingTime>3 || this.speed<20) && this.speed<50) this.maxSpeed = this.speed;
}
this.lastSpeed = this.speed;
if (this.speed>this.maxSpeed && (this.movingTime>3 || this.speed<20) && this.speed<50) this.maxSpeed = this.speed;
}
if (qChanged && this.qUpdateScreen) this.updateScreen();
}
@ -199,6 +251,7 @@ connection_setup();
setWatch(function() { mySensor.reset(); g.clearRect(0, 48, 239, 239); mySensor.updateScreen(); }, BTN1, {repeat:true, debounce:20});
E.on('kill',()=>{ if (gatt!=undefined) gatt.disconnect(); mySensor.settings.totaldist = mySensor.totaldist; storage.writeJSON(SETTINGS_FILE, mySensor.settings); });
setWatch(function() { if (Date.now()-mySensor.lastBangleTime>10000) connection_setup(); }, BTN3, {repeat:true, debounce:20});
setWatch(function() { mySensor.toggleDisplayCadence(); g.clearRect(0, 48, 239, 239); mySensor.updateScreen(); }, BTN2, {repeat:true, debounce:20});
NRF.on('disconnect', connection_setup);
Bangle.loadWidgets();

View File

@ -1 +1 @@
require("heatshrink").decompress(atob("mEwgX/AH4A/AAf+14BBAoPq/Wq1QFB+EP+kLAoNA+CdB3//yEP6ALB/sABYf8gEMgALB6ALJqoLB+tVgH//kQhkQhUABYImCIwPwh3whcA94UCgJHD+AXB/4LCcQQLCKYQLB+EDBZQFCBY/Qh4LJ4ALLl4LFioLCgE/KZMAv4XFBYimBC4/+BYw7DRwQLIV4ILWTQQLDOIUA/ALDAC+t1uv/263/6/Pq/YLC3vv//vM4Oq33rBYP6/u//2/C4Pr1YXC12vBwO+BYO69XmJDQA/ACIA=="))
require("heatshrink").decompress(atob("mEwge27dtAX4C+/dt+wFB/wCECIu3/dvBYNv34RC7/tCIu//99EYN9C4IpB74jG3379ovDFIIRBEYxHD/47D2wjHCIX+AQJHBCIIXBNZt/+5QBEZIgBAQX///9EZRWBBARHDEwhlC/9/EAJoBDQIOBNwyPCEYYCDJQ4CJSQ4CB0O2lojL2lwBIXFiwsK0f/KgUbuwRJo6cBPAO34cUmJHH7U/97tBgEGBIODEY/RXoOw7cAgHbtlxoojGx7hCjAjD20ANA1378MEIIAB4d0u5HGNAPYCAYAB2n2SQSPDjv3CIsF2lxEYto//+CoOGCIUt0O3EYtHvqMBvlw4UAgJQBqIjERgQDBsO+7FAhaMH64DB+4qB+3AgARG9uhIgQJD4dghd+7dLBQZoBaISwC4cArf27dpCIf/23f9uHCIQABhoNClsl20ttuwgYKBEAIAChOmCIOH/vx9ttwB2BDgMBAoIRBmnbpkbtk2ltsgAMCJQOwAgMBk+eq3AhiSBsE2GAX//4WCAAOBAoVbtt8mCJBgHHfYMHdgoRBott+zmDsEAn/tEwkC7UAVoYACgPbv4REAASTBEYY0BPIPwCJAjEu3Dvq8BCAnbsEwGgm2jbAC8EAjFvEAQREjuwDQXbvvx7cd2K/EgEb9oRCAoLOBjEAgk/A="))

View File

@ -154,16 +154,20 @@ function touchHandler(l,e) {
if (l.c) l.c.forEach(n => touchHandler(n,e));
}
function prepareLazyRender(l, rectsToClear, drawList, rects, bgCol) {
if ((l.bgCol != null && l.bgCol != bgCol) || l.type == "txt" || l.type == "btn" || l.type == "img" || l.type == "custom") {
function prepareLazyRender(l, rectsToClear, drawList, rects, parentBg) {
var bgCol = l.bgCol == null ? parentBg : g.toColor(l.bgCol);
if (bgCol != parentBg || l.type == "txt" || l.type == "btn" || l.type == "img" || l.type == "custom") {
// Hash the layoutObject without including its children
let c = l.c;
var c = l.c;
delete l.c;
let hash = "H"+E.CRC32(E.toJS(l)); // String keys maintain insertion order
var hash = "H"+E.CRC32(E.toJS(l)); // String keys maintain insertion order
if (c) l.c = c;
if (!delete rectsToClear[hash]) {
rects[hash] = {bg: bgCol, r: [l.x,l.y,l.x+l.w-1,l.y+l.h-1]};
rects[hash] = {
bg: parentBg == null ? g.theme.bg : parentBg,
r: [l.x,l.y,l.x+l.w-1,l.y+l.h-1]
};
if (drawList) {
drawList.push(l);
drawList = null; // Prevent children from being redundantly added to the drawList
@ -171,7 +175,7 @@ function prepareLazyRender(l, rectsToClear, drawList, rects, bgCol) {
}
}
if (l.c) for (let ch of l.c) prepareLazyRender(ch, rectsToClear, drawList, rects, l.bgCol == null ? bgCol : l.bgCol);
if (l.c) for (var ch of l.c) prepareLazyRender(ch, rectsToClear, drawList, rects, bgCol);
}
Layout.prototype.render = function (l) {
@ -189,20 +193,24 @@ Layout.prototype.render = function (l) {
"txt":function(l){
g.setFont(l.font,l.fsz).setFontAlign(0,0,l.r).drawString(l.label, l.x+(l.w>>1), l.y+(l.h>>1));
}, "btn":function(l){
var x = l.x+(0|l.pad);
var y = l.y+(0|l.pad);
var w = l.w-(l.pad<<1);
var h = l.h-(l.pad<<1);
var poly = [
l.x,l.y+4,
l.x+4,l.y,
l.x+l.w-5,l.y,
l.x+l.w-1,l.y+4,
l.x+l.w-1,l.y+l.h-5,
l.x+l.w-5,l.y+l.h-1,
l.x+4,l.y+l.h-1,
l.x,l.y+l.h-5,
l.x,l.y+4
x,y+4,
x+4,y,
x+w-5,y,
x+w-1,y+4,
x+w-1,y+h-5,
x+w-5,y+h-1,
x+4,y+h-1,
x,y+h-5,
x,y+4
];
g.setColor(g.theme.bgH).fillPoly(poly).setColor(l.selected ? g.theme.fgH : g.theme.fg).drawPoly(poly).setFont("4x6",2).setFontAlign(0,0,l.r).drawString(l.label,l.x+l.w/2,l.y+l.h/2);
}, "img":function(l){
g.drawImage(l.src(), l.x, l.y);
g.drawImage(l.src(), l.x + (0|l.pad), l.y + (0|l.pad));
}, "custom":function(l){
l.render(l);
},"h":function(l) { l.c.forEach(render); },
@ -216,7 +224,7 @@ Layout.prototype.render = function (l) {
if (!this.rects) this.rects = {};
var rectsToClear = this.rects.clone();
var drawList = [];
prepareLazyRender(l, rectsToClear, drawList, this.rects, g.getBgColor());
prepareLazyRender(l, rectsToClear, drawList, this.rects, null);
for (var h in rectsToClear) delete this.rects[h];
var clearList = Object.keys(rectsToClear).map(k=>rectsToClear[k]).reverse(); // Rects are cleared in reverse order so that the original bg color is restored
for (var r of clearList) g.setBgColor(r.bg).clearRect.apply(g, r.r);
@ -231,36 +239,28 @@ Layout.prototype.layout = function (l) {
// exw,exh = extra width/height available
switch (l.type) {
case "h": {
let x = l.x + (l.w-l._w)/2;
var x = l.x + (0|l.pad);
var fillx = l.c && l.c.reduce((a,l)=>a+(0|l.fillx),0);
if (fillx) { x = l.x; }
if (!fillx) { x += (l.w-l._w)/2; }
l.c.forEach(c => {
c.w = c._w + ((0|c.fillx)*(l.w-l._w)/(fillx||1));
c.h = c.filly ? l.h : c._h;
if (c.pad) {
c.w += c.pad*2;
c.h += c.pad*2;
}
c.h = c.filly ? l.h - (l.pad<<1) : c._h;
c.x = x;
c.y = l.y + (1+(0|c.valign))*(l.h-c.h)/2;
c.y = l.y + (0|l.pad) + (1+(0|c.valign))*(l.h-(l.pad<<1)-c.h)/2;
x += c.w;
if (c.c) this.layout(c);
});
break;
}
case "v": {
let y = l.y + (l.h-l._h)/2;
var y = l.y + (0|l.pad);;
var filly = l.c && l.c.reduce((a,l)=>a+(0|l.filly),0);
if (filly) { y = l.y; }
if (!filly) { y += (l.h-l._h)/2 }
l.c.forEach(c => {
c.w = c.fillx ? l.w : c._w;
c.w = c.fillx ? l.w - (l.pad<<1) : c._w;
c.h = c._h + ((0|c.filly)*(l.h-l._h)/(filly||1));
if (c.pad) {
c.w += c.pad*2;
c.h += c.pad*2;
}
c.y = y;
c.x = l.x + (1+(0|c.halign))*(l.w-c.w)/2;
c.x = l.x + (0|l.pad) + (1+(0|c.halign))*(l.w-(l.pad<<1)-c.w)/2;
y += c.h;
if (c.c) this.layout(c);
});
@ -288,8 +288,8 @@ Layout.prototype.update = function() {
if (l.r&1) { // rotation
var t = l._w;l._w=l._h;l._h=t;
}
l._w = Math.max(l._w, 0|l.width);
l._h = Math.max(l._h, 0|l.height);
l._w = Math.max(l._w + (l.pad<<1), 0|l.width);
l._h = Math.max(l._h + (l.pad<<1), 0|l.height);
}
var cb = {
"txt" : function(l) {
@ -327,16 +327,16 @@ Layout.prototype.update = function() {
l._h = 0;
}, "h": function(l) {
l.c.forEach(updateMin);
l._h = l.c.reduce((a,b)=>Math.max(a,b._h+(b.pad<<1)),0);
l._w = l.c.reduce((a,b)=>a+b._w+(b.pad<<1),0);
if (l.c.some(c=>c.fillx)) l.fillx = 1;
if (l.c.some(c=>c.filly)) l.filly = 1;
l._h = l.c.reduce((a,b)=>Math.max(a,b._h),0);
l._w = l.c.reduce((a,b)=>a+b._w,0);
if (l.fillx == null && l.c.some(c=>c.fillx)) l.fillx = 1;
if (l.filly == null && l.c.some(c=>c.filly)) l.filly = 1;
}, "v": function(l) {
l.c.forEach(updateMin);
l._h = l.c.reduce((a,b)=>a+b._h+(b.pad<<1),0);
l._w = l.c.reduce((a,b)=>Math.max(a,b._w+(b.pad<<1)),0);
if (l.c.some(c=>c.fillx)) l.fillx = 1;
if (l.c.some(c=>c.filly)) l.filly = 1;
l._h = l.c.reduce((a,b)=>a+b._h,0);
l._w = l.c.reduce((a,b)=>Math.max(a,b._w),0);
if (l.fillx == null && l.c.some(c=>c.fillx)) l.fillx = 1;
if (l.filly == null && l.c.some(c=>c.filly)) l.filly = 1;
}
};
updateMin(l);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,9 @@
g.clear();
var layout = new Layout({type:"h", filly: 0, c: [
{type: "txt", font: "50%", label: "A"},
{type:"v", c: [
{type: "txt", font: "10%", label: "B"},
{filly: 1},
{type: "txt", font: "10%", label: "C"},
]},
]});

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,26 @@
var img = () => ({
width : 8, height : 8, bpp : 4,
transparent : 1,
buffer : E.toArrayBuffer(atob("Ee7uER7u7uHuDuDu7u7u7u7u7u7g7u4OHgAA4RHu7hE="))
});
var layout = new Layout({type: "v", c: [
{type: "txt", font: "6x8", bgCol: "#F00", pad: 5, label: "TEXT"},
{type: "img", font: "6x8", bgCol: "#0F0", pad: 5, src: img},
{type: "btn", font: "6x8", bgCol: "#00F", pad: 5, label: "BTN"},
{type: "v", bgCol: "#F0F", pad: 2, c: [
{type: "txt", font: "6x8", bgCol: "#F00", label: "v with children"},
{type: "txt", font: "6x8", bgCol: "#0F0", halign: -1, label: "halign -1"},
{type: "txt", font: "6x8", bgCol: "#00F", halign: 1, label: "halign 1"},
]},
{type: "h", bgCol: "#0FF", pad: 2, c: [
{type: "txt", font: "6x8:2", bgCol: "#F00", label: "h"},
{type: "txt", font: "6x8", bgCol: "#0F0", valign: -1, label: "valign -1"},
{type: "txt", font: "6x8", bgCol: "#00F", valign: 1, label: "valign 1"},
]},
{type: "h", bgCol: "#FF0", pad: 2, c: [
{type: "v", bgCol: "#0F0", pad: 2, c: [
{type: "txt", font: "6x8", bgCol: "#F00", pad: 2, label: "nested"},
]},
]},
]});

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,7 @@
var layout = new Layout({type: "v", c: [
{type: "h", pad: 20, c: [
{type: "txt", font: "10%", label: "abcd"},
{fillx: 1},
{type: "txt", font: "10%", label: "1234"},
]},
]});

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,26 @@
var img = () => ({
width : 8, height : 8, bpp : 4,
transparent : 1,
buffer : E.toArrayBuffer(atob("Ee7uER7u7uHuDuDu7u7u7u7u7u7g7u4OHgAA4RHu7hE="))
});
var layout = new Layout({type: "v", c: [
{type: "txt", font: "6x8", bgCol: "#F00", fillx: 1, pad: 5, label: "TEXT"},
{type: "img", font: "6x8", bgCol: "#0F0", filly: 1, fillx: 1,pad: 5, src: img},
{type: "btn", font: "6x8", bgCol: "#00F", fillx: 1, pad: 5, label: "BTN"},
{type: "v", bgCol: "#F0F", pad: 2, c: [
{type: "txt", font: "6x8", bgCol: "#F00", fillx: 1, filly: 1, label: "v with children"},
{type: "txt", font: "6x8", bgCol: "#0F0", halign: -1, label: "halign -1"},
{type: "txt", font: "6x8", bgCol: "#00F", halign: 1, label: "halign 1"},
]},
{type: "h", bgCol: "#0FF", pad: 2, c: [
{type: "txt", font: "6x8:2", bgCol: "#F00", fillx: 1, filly: 1, label: "h"},
{type: "txt", font: "6x8", bgCol: "#0F0", valign: -1, label: "valign -1"},
{type: "txt", font: "6x8", bgCol: "#00F", valign: 1, label: "valign 1"},
]},
{type: "h", bgCol: "#FF0", pad: 2, c: [
{type: "v", bgCol: "#0F0", pad: 2, c: [
{type: "txt", font: "6x8", bgCol: "#F00", fillx: 1, filly: 1, pad: 2, label: "nested"},
]},
]},
]});