From d35b71afce8397221a105b8d18a01eae3c7840e8 Mon Sep 17 00:00:00 2001 From: Flaparoo <629229+flaparoo@users.noreply.github.com> Date: Tue, 30 Jan 2024 19:40:47 +0800 Subject: [PATCH 01/48] aviatorclk: added tap event to scroll METAR and toggle seconds display --- apps/aviatorclk/ChangeLog | 1 + apps/aviatorclk/README.md | 9 ++++-- apps/aviatorclk/aviatorclk.app.js | 47 +++++++++++++++++++++++++------ apps/aviatorclk/metadata.json | 2 +- 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/apps/aviatorclk/ChangeLog b/apps/aviatorclk/ChangeLog index 971e5b97e..929ee8387 100644 --- a/apps/aviatorclk/ChangeLog +++ b/apps/aviatorclk/ChangeLog @@ -1 +1,2 @@ 1.00: initial release +1.01: added tap event to scroll METAR and toggle seconds display diff --git a/apps/aviatorclk/README.md b/apps/aviatorclk/README.md index fe7376b5d..ac27b80d3 100644 --- a/apps/aviatorclk/README.md +++ b/apps/aviatorclk/README.md @@ -18,15 +18,20 @@ module after installing this app. - Latest METAR for the nearest airport (scrollable) Tap the screen in the top or bottom half to scroll the METAR text (in case not -the whole report fits on the screen). +the whole report fits on the screen). You can also tap the watch from the top +or bottom to scroll, which works even with the screen locked. The colour of the METAR text will change to orange if the report is more than 1h old, and red if it's older than 1.5h. +To toggle the seconds display, double tap the watch from either the left or +right. This only changes the display "temporarily" (ie. it doesn't change the +default configured through the settings). + ## Settings -- **Show Seconds**: to conserve battery power, you can turn the seconds display off +- **Show Seconds**: to conserve battery power, you can turn the seconds display off (as the default) - **Invert Scrolling**: swaps the METAR scrolling direction of the top and bottom taps diff --git a/apps/aviatorclk/aviatorclk.app.js b/apps/aviatorclk/aviatorclk.app.js index 1d99fdbde..33d671bc7 100644 --- a/apps/aviatorclk/aviatorclk.app.js +++ b/apps/aviatorclk/aviatorclk.app.js @@ -169,6 +169,7 @@ function drawSeconds() { let seconds = now.getSeconds().toString(); if (seconds.length == 1) seconds = '0' + seconds; let y = Bangle.appRect.y + mainTimeHeight - 3; + g.setBgColor(g.theme.bg); g.setFontAlign(-1, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_GREY); g.drawString(seconds, horizontalCenter + 54, y, true); } @@ -232,10 +233,10 @@ function draw() { // initialise g.clear(true); -// scroll METAR lines on taps -Bangle.setUI("clockupdown", action => { +// scroll METAR lines (either by touch or tap) +function scrollAVWX(action) { switch (action) { - case -1: // top tap + case -1: // top touch/tap if (settings.invertScrolling) { if (METARscollLines > 0) METARscollLines--; @@ -244,7 +245,7 @@ Bangle.setUI("clockupdown", action => { METARscollLines++; } break; - case 1: // bottom tap + case 1: // bottom touch/tap if (settings.invertScrolling) { if (METARscollLines < METARlinesCount - 4) METARscollLines++; @@ -254,11 +255,41 @@ Bangle.setUI("clockupdown", action => { } break; default: - // ignore + // ignore other actions } drawAVWX(); +} + +Bangle.on('tap', data => { + switch (data.dir) { + case 'top': + scrollAVWX(-1); + break; + case 'bottom': + scrollAVWX(1); + break; + case 'left': + case 'right': + // toggle seconds display on double taps left or right + if (data.double) { + if (settings.showSeconds) { + clearInterval(secondsInterval); + let y = Bangle.appRect.y + mainTimeHeight - 3; + g.clearRect(horizontalCenter + 54, y - secondaryFontHeight, g.getWidth(), y); + settings.showSeconds = false; + } else { + settings.showSeconds = true; + syncSecondsUpdate(); + } + } + break; + default: + // ignore other taps + } }); +Bangle.setUI("clockupdown", scrollAVWX); + // load widgets Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -276,8 +307,8 @@ updateAVWX(); // TMP for debugging: -//METAR = 'YAAA 011100Z 21014KT CAVOK 23/08 Q1018 RMK RF000/0000'; -//METAR = 'YAAA 150900Z 14012KT 9999 SCT045 BKN064 26/14 Q1012 RMK RF000/0000 DL-W/DL-NW'; -//METAR = 'YAAA 020030Z VRB CAVOK'; +//METAR = 'YAAA 011100Z 21014KT CAVOK 23/08 Q1018 RMK RF000/0000'; drawAVWX(); +//METAR = 'YAAA 150900Z 14012KT 9999 SCT045 BKN064 26/14 Q1012 RMK RF000/0000 DL-W/DL-NW'; drawAVWX(); +//METAR = 'YAAA 020030Z VRB CAVOK'; drawAVWX(); //METARts = new Date(Date.now() - 61 * 60000); // 61 to trigger warning, 91 to trigger alert diff --git a/apps/aviatorclk/metadata.json b/apps/aviatorclk/metadata.json index 6ae8c4a18..9d2b0beef 100644 --- a/apps/aviatorclk/metadata.json +++ b/apps/aviatorclk/metadata.json @@ -2,7 +2,7 @@ "id": "aviatorclk", "name": "Aviator Clock", "shortName":"AV8R Clock", - "version":"1.00", + "version":"1.01", "description": "A clock for aviators, with local time and UTC - and the latest METAR for the nearest airport", "icon": "aviatorclk.png", "screenshots": [{ "url": "screenshot.png" }, { "url": "screenshot2.png" }], From ed5aba215148616d14081be34521fd60c1f9e821 Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Sun, 14 Jan 2024 17:04:17 +0000 Subject: [PATCH 02/48] Add Warpdrive watchface --- apps/warpdrive/README.md | 5 + apps/warpdrive/app-icon.js | 1 + apps/warpdrive/app.js | 614 +++++++++++++++++++++++++++++++++++ apps/warpdrive/app.png | Bin 0 -> 5936 bytes apps/warpdrive/metadata.json | 16 + 5 files changed, 636 insertions(+) create mode 100644 apps/warpdrive/README.md create mode 100644 apps/warpdrive/app-icon.js create mode 100644 apps/warpdrive/app.js create mode 100644 apps/warpdrive/app.png create mode 100644 apps/warpdrive/metadata.json diff --git a/apps/warpdrive/README.md b/apps/warpdrive/README.md new file mode 100644 index 000000000..0f300c518 --- /dev/null +++ b/apps/warpdrive/README.md @@ -0,0 +1,5 @@ +# WarpDrive + +An animated watchface featuring 3D spaceships traveling just shy of ludicrous speed. + +WE BREAK FOR NOBODY. \ No newline at end of file diff --git a/apps/warpdrive/app-icon.js b/apps/warpdrive/app-icon.js new file mode 100644 index 000000000..1ac6aa67d --- /dev/null +++ b/apps/warpdrive/app-icon.js @@ -0,0 +1 @@ +atob("MDDD/wAA//+nMRF8r3PznG1znfdmmccUUUUUMMQQQQQQQQUdFFc8/66rKKLBBBBBBBBBBB6zyy+mnFcUUUUUMMQQQQMQMddFFdk3266rLKLLBBBBBBBBK6y6x/7FddcUUUUUMMMQQQQMcdUdd9e66q6rLJqLBBBBBBBSpyqze65dcUccUUUUUMMMMMNdccdd9ce66pqqppqLBBBBBBOp6qu/6qpcccccccUUUMMOsOccEdFlcUW5q66ppprLJBBDDWyr64866rIcUcccUUUUUMMUXfEGkGlddUWpqJ6pppqLLhCjO26u4867LLIcdFlFl0UUUXEMVdEEGndcUUVpjKs30330303ra1Ng8327LLIUW0VlnF9FEXcEEQUOnHdcUUVppppA0033u77sxBBA036pqrIUUUUUWHHHFHFHUYQUdENcEcVpppppBL373swzBBLB3rK4irAUUUUUYYGHGHEMMYUUcMMEdcVp5pprBDBWc2hBBBBLbC7Ba6ocUUUUMUYGmEUYQMUUMUUMFddpppprK5A82rDDBDBKLLDDK64UUcefEQGlcMMMQMQUcUMMVFdppp/2pA36BBBBDJDKpppjDKsUcedHccYQQQYUUUUFccUUVcdpr+38rBBBBBBDJBDBJ3KJqK4UEVelcYUYUQUQUMYYcdcdcUVoprc6rK6hJLLBDBBBBCqqqKIEUWkUUenUUVdUMYUUQQQcUUWpqfrDK22rK4+rBDDJBC5BJpIUW8UUUddcfGnUFFEUMdccUUVqeBLKKbrLY82q6yuBCa66ppAW0UUUXcUUGnddddUUVcdFUUWeLDDLdrDI+7a66ypLJpyyrLC0UMUV8UUe8VccddUUUUdcUUOLBBCdrBDfC66J6rJJqKKyBBAUMMV0UMW0cccUcccUUUVUUMNBBCdqBBKKKprLJ1pqLDLDBBAMMeUUMMUUccUUW0UUUUMUMMRBD1qJBDDKJrLLfLLLBBDLDLAUHUUMMUUUUUUW0UUUUMUUUUNAqKJBBBDKLLLXLLDJBDBBDLAEUUUMQMUUUcVUUUUUMUMMMUQqLKBBBDLLK6rLDBBBBDDBBBAUUUMQMUUUfcUMMMQMMUUQQQSKLBBBBDLL2rBBBBBBBBBBBBAUUMMMMUUfcUMMMMQMMMMQQQTLBBBBBDL2rBBBBBBBhBBBBBA=") diff --git a/apps/warpdrive/app.js b/apps/warpdrive/app.js new file mode 100644 index 000000000..990b6a961 --- /dev/null +++ b/apps/warpdrive/app.js @@ -0,0 +1,614 @@ +const gfx = E.compiledC(` +// void init(int, int, int) +// void clear() +// void render(int, int) +// void setCamera(int, int, int) +// void stars() + +unsigned char* fb; +int stride; +unsigned char* sint; + +const int near = 5 << 8; +int f = 0; + +typedef struct { + int x, y, z; +} Point; + +Point camera; +Point rotation; +Point scale; +Point position; + +const unsigned char ship[] = { +0,38,25,10,3,8,6,10,7,3,6,13,3,11,5,13,1,12,3,15,3,5,8,15,1,3,7,13,12,11,3,15,5,6,8,15,6,1,7,10,5,0,6,10,0,1,6,12,5,11,4,12,12,1,2,12,2,11,12,12,10,5,4,13,5,10,0,12,2,1,9,13,9,1,0,12,4,11,2,10,19,22,21,12,4,2,10,12,10,2,9,10,13,16,15,13,10,9,0,15,21,20,19,15,15,14,13,15,19,20,22,15,13,14,16,15,21,23,20,15,15,17,14,15,22,20,23,10,22,24,21,15,16,14,17,10,16,18,15,15,24,23,21,15,18,17,15,15,22,23,24,15,16,17,18,0,0,62,236,243,244,247,0,234,0,229,194,11,0,234,21,243,246,0,234,33,193,250,20,63,249,19,249,4,3,9,4,3,7,247,222,250,247,222,240,0,22,238,13,22,226,1,20,229,7,62,225,11,20,208,27,62,19,0,20,22,12,20,33,0,18,30,5,60,34,10,18,52,26,60 +}; + +unsigned int _rngState; +unsigned int rng() { + _rngState ^= _rngState << 17; + _rngState ^= _rngState >> 13; + _rngState ^= _rngState << 5; + return _rngState; +} + +void init(unsigned char* _fb, int _stride, unsigned char* _sint) { + fb = _fb; + stride = _stride; + sint = _sint; +} + +int sin(int angle) { + int a = (angle >> 7) & 0xFF; + if (angle & (1 << 15)) + a = 0xFF - a; + int v = sint[a]; + if (angle & (1 << 16)) + v = -v; + return v; +} + +int cos(int angle) { + return sin(angle + 0x8000); +} + +void clear() { + unsigned short* cursor = (unsigned short*) fb; + for (int y = 0; y < 176; ++y) { + for (int x = 0; x < 66/2; ++x) + *cursor++ = 0; + cursor++; + } +} + +void setCamera(int x, int y, int z) { + camera.x = x; + camera.y = y; + camera.z = z; +} + +unsigned int solid(unsigned int c) { + c &= 7; + c |= c << 3; + c |= c << 6; + c |= c << 12; + c |= c << 24; + return c; +} + +unsigned int alternate(unsigned int a, unsigned int b) { + unsigned int c = (a & 7) | ((b & 7) << 3); + c |= c << 6; + c |= c << 12; + c |= c << 24; + return c; +} + +void drawHLine(int x, unsigned int y, int l, unsigned int c) { + if (x < 0) { + l += x; + x = 0; + } + if (x + l >= 176) { + l = 176 - x; + } + if (l <= 0 || y >= 176) + return; + + if (y & 1) + c = alternate(c >> 3, c); + + int bitstart = x * 3; + int bitend = (x + l) * 3; + int wstart = bitstart >> 5; + int wend = bitend >> 5; + int padstart = bitstart & 31; + int padend = bitend & 31; + int maskstart = -1 << padstart; + int maskend = unsigned(-1) >> (32 - padend); + if (wstart == wend) { + maskstart &= maskend; + maskend = 0; + } + + int* row = (int*) &fb[y * stride]; + if (maskstart) { + row[wstart] = (row[wstart] & ~maskstart) | ((c << padstart) & maskstart); + while (bitstart >> 5 == wstart) + bitstart += 3; + } + if (maskend) + row[wend] = (row[wend] & ~maskend) | + (((c >> (30 - padend)) | (c >> (36 - padend))) & maskend); + bitend -= padend; + for (int x = bitstart; x < bitend; x += 10 * 3) { + unsigned int R = x & 31; + row[x >> 5] = (c << R) | (c >> (36 - R)) | (c >> (30 - R)) | (c << (R - 6)); + } +} + +void fillTriangle( int x0, int y0, + int x1, int y1, + int x2, int y2, + unsigned int col) { + int a, b, y, last, tmp; + + a = 176; + b = 176; + if( x0 < 0 && x1 < 0 && x2 < 0 ) return; + if( x0 >= a && x1 > a && x2 > a ) return; + if( y0 < 0 && y1 < 0 && y2 < 0 ) return; + if( y0 >= b && y1 > b && y2 > b ) return; + + // Sort coordinates by Y order (y2 >= y1 >= y0) + if (y0 > y1) { + tmp = y0; y0 = y1; y1 = tmp; + tmp = x0; x0 = x1; x1 = tmp; + } + if (y1 > y2) { + tmp = y2; y2 = y1; y1 = tmp; + tmp = x2; x2 = x1; x1 = tmp; + } + if (y0 > y1) { + tmp = y0; y0 = y1; y1 = tmp; + tmp = x0; x0 = x1; x1 = tmp; + } + + if (y0 == y2) { // Handle awkward all-on-same-line case as its own thing + a = b = x0; + if (x1 < a) a = x1; + else if (x1 > b) b = x1; + if (x2 < a) a = x2; + else if (x2 > b) b = x2; + drawHLine(a, y0, b - a + 1, col); + return; + } + + int dx01 = x1 - x0, + dx02 = x2 - x0, + dy02 = (1<<16) / (y2 - y0), + dx12 = x2 - x1, + sa = 0, + sb = 0; + + // For upper part of triangle, find scanline crossings for segments + // 0-1 and 0-2. If y1=y2 (flat-bottomed triangle), the scanline y1 + // is included here (and second loop will be skipped, avoiding a /0 + // error there), otherwise scanline y1 is skipped here and handled + // in the second loop...which also avoids a /0 error here if y0=y1 + // (flat-topped triangle). + if (y1 == y2) last = y1; // Include y1 scanline + else last = y1 - 1; // Skip it + + y = y0; + + if( y0 != y1 ){ + int dy01 = (1<<16) / (y1 - y0); + for (y = y0; y <= last; y++) { + a = x0 + ((sa * dy01) >> 16); + b = x0 + ((sb * dy02) >> 16); + sa += dx01; + sb += dx02; + /* longhand: + a = x0 + (x1 - x0) * (y - y0) / (y1 - y0); + b = x0 + (x2 - x0) * (y - y0) / (y2 - y0); + */ + if (a > b){ + tmp = a; + a = b; + b = tmp; + } + drawHLine(a, y, b - a + 1, col); + } + } + + // For lower part of triangle, find scanline crossings for segments + // 0-2 and 1-2. This loop is skipped if y1=y2. + if( y1 != y2 ){ + int dy12 = (1<<16) / (y2 - y1); + sa = dx12 * (y - y1); + sb = dx02 * (y - y0); + for (; y <= y2; y++) { + a = x1 + ((sa * dy12) >> 16); + b = x0 + ((sb * dy02) >> 16); + sa += dx12; + sb += dx02; + if (a > b){ + tmp = a; + a = b; + b = tmp; + } + drawHLine(a, y, b - a + 1, col); + } + } +} + +void v_project(Point* p){ + int fovz = ((90 << 16) / ((90 << 8) + p->z)); // 16:8 / 16:8 -> 16:8 + p->x = (p->x * fovz >> 8) + (176/2 << 8); // 16:8 * 16:8 = 16:16 -> 16:8 + p->y = (176/2 << 8) - (p->y * fovz >> 8); + p->z = fovz; +} + +void stars() { + f += 5; + _rngState = 1013904223; + + // rng(); rng(); rng(); + + for (int i = 0; i < 100; ++i) { + int s = rng(); + position.x = ((signed char)(s & 0xFF)) << 9; + s = rng(); + position.y = ((signed char)(s & 0xFF)) << 9; + s = rng(); + position.z = 0xFF - ((s + f) & 0xFF); + position.z <<= 12; + position.z -= 100 << 8; + + int light = position.z < (800 << 8); + int dark = position.z > ((800 + 500) << 8); + + scale = position; + scale.z += 30 << 10; + + v_project(&position); + v_project(&scale); + + s = (((s & 3) + 3) * position.z + 256) >> 7; + if (s < 1) s = 1; + if (s > 10) s = 10; + + position.x >>= 8; + position.y >>= 8; + scale.x >>= 8; + scale.y >>= 8; + + if (position.x < - 100 || position.x > 276) continue; + if (position.y < - 100 || position.y > 276) continue; + int color = 4 | (i & 1); + fillTriangle( + scale.x, scale.y, + position.x - s, position.y, + position.x + s, position.y, + light ? alternate(color, 7) : + dark ? alternate(color, 0) : + solid(color) + ); + } +} + +void transform(Point* p) { + int x = p->x; + int y = p->y; + int z = p->z; + int s, c; + if (rotation.z) { + s = sin(rotation.z); + c = cos(rotation.z); + p->x = (x*c>>8) - (y*s>>8); + p->y = (x*s>>8) + (y*c>>8); + x = p->x; + y = p->y; + } + + if (rotation.y) { + s = sin(rotation.y); + c = cos(rotation.y); + p->x = (x*c>>8) - (z*s>>8); + p->z = (x*s>>8) + (z*c>>8); + } + +// Scale + p->x = p->x * scale.x >> 8; + p->y = p->y * scale.y >> 8; + p->z = p->z * scale.z >> 8; + +// Translate + p->x += position.x; + p->y += position.y; + p->z += position.z; +} + +void render(int* n, const unsigned char* m){ + rotation.x = n[0]; + rotation.y = n[1]; + rotation.z = n[2]; + scale.x = n[3]; + scale.y = n[4]; + scale.z = n[5]; + position.x = n[6] - camera.x; + position.y = n[7] - camera.y; + position.z = n[8] - camera.z; + unsigned char tint = n[9]; + + if (position.z < near) + return; + + if (!m) + m = ship; + + int light = position.z < (800 << 8); + int dark = position.z > ((800 + 500) << 8); + + int faceCount = (((int)m[0]) << 8) + (int)m[1]; + // int vtxCount = m[2]; + const unsigned char* faceOffset = m + 3; + const unsigned char* vtxOffset = faceOffset + faceCount*4; + + Point pointA, pointB, pointC; + Point* A = &pointA; + unsigned char* Ai = 0; + Point* B = &pointB; + unsigned char* Bi = 0; + Point* C = &pointC; + unsigned char* Ci = 0; + bool Ab, Bb, Cb; + + for (int face = 0; facex = ((signed char)*indexA++) << 8; + A->y = ((signed char)*indexA++) << 8; + A->z = ((signed char)*indexA) << 8; + transform(A); + if(A->z <= near) continue; + v_project(A); + } + + if (!B) { + if (!Ab) { B = &pointA; Ab = true; } + else if (!Bb) { B = &pointB; Bb = true; } + else if (!Cb) { B = &pointC; Cb = true; } + B->x = ((signed char)*indexB++) << 8; + B->y = ((signed char)*indexB++) << 8; + B->z = ((signed char)*indexB) << 8; + transform(B); + if(B->z <= near) continue; + v_project(B); + } + + if (!C) { + if (!Ab) { C = &pointA; Ab = true; } + else if (!Bb) { C = &pointB; Bb = true; } + else if (!Cb) { C = &pointC; Cb = true; } + C->x = ((signed char)*indexC++) << 8; + C->y = ((signed char)*indexC++) << 8; + C->z = ((signed char)*indexC) << 8; + transform(C); + if(C->z <= near) continue; + v_project(C); + } + + if (((A->x - B->x) >> 8)*((A->y - C->y) >> 8) - + ((A->y - B->y) >> 8)*((A->x - C->x) >> 8) < 0) + continue; + + fillTriangle( + A->x >> 8, A->y >> 8, + B->x >> 8, B->y >> 8, + C->x >> 8, C->y >> 8, + light ? alternate(color, 7) : + dark ? alternate(color, 0) : + solid(color) + ); + } +} + +`); + +const nodeCount = 4; +const nodes = new Array(nodeCount); +const sintable = new Uint8Array(256); +const translation = new Uint32Array(10); +const BLACK = g.setColor.bind(g, 0); +const WHITE = g.setColor.bind(g, 0xFFFF); +let lcdBuffer = 0, + start = 0; + +let locked = false; +var interval; + +function setupInterval() { + if (interval) + clearInterval(interval); + if (!locked) + tick(); + interval = setInterval(tick.bind(null, !locked), locked ? 60 * 1000 : 70); +} + +function test(addr, y) { + BLACK().fillRect(0, y, 176, y); + if (peek8(addr)) return false; + WHITE().fillRect(0, y, 176, y); + let b = peek8(addr); + BLACK().fillRect(0, y, 176, y); + if (!b) return false; + return !peek8(addr); +} + +function probe() { + if (!start) + start = 0x20000000; + const end = Math.min(start + 0x800, 0x20038000); + + if (start >= end) { + print("Could not find framebuffer"); + return; + } + + BLACK().fillRect(0, 0, 176, 0); + // sampling every 64 bytes since a 176-pixel row is 66 bytes at 3bpp + for (; start < end; start += 64) { + if (peek8(start)) continue; + WHITE().fillRect(0, 0, 176, 0); + let b = peek8(start); + BLACK().fillRect(0, 0, 176, 0); + if (!b) continue; + if (!peek8(start)) break; + } + + if (start >= end) { + setTimeout(probe, 1); + return; + } + + // find the beginning of the row + while (test(start - 1, 0)) + start--; + + /* + let stride = (176 * 3 + 7) >> 3, + padding = 0; + for (let i = 0; i < 20; ++i, ++padding) { + if (test(start + stride + padding, 1)) { + break; + } + } + + stride += padding; + if (padding == 20) { + print("Warning: Could not calculate padding"); + stride = 68; + } + */ + stride = 68; + + lcdBuffer = start; + print('Found lcdBuffer at ' + lcdBuffer.toString(16) + ' stride=' + stride); + gfx.init(start, stride, E.getAddressOf(sintable, true)); + gfx.setCamera(0, 0, -300 << 8); + setupInterval(); +} + +function init() { + g.clear(); + g.setFont('6x8', 2); + g.setFontAlign(0, 0.5); + g.drawString("[LOADING]", 90, 66); + + // setup sin/cos table + for (let i = 0; i < sintable.length; ++i) + sintable[i] = Math.sin((i * Math.PI * 0.5) / sintable.length) * ((1 << 8) - 1); + + // setup nodes + let o = 0; + for (let i = 0; i < nodeCount; ++i) { + nodes[i] = { + rx: 0, + ry: 256, + rz: 0, + sx: 4, + sy: 4, + sz: 4, + vx: Math.random() * 20 - 10, + vy: Math.random() * 20 - 10, + vz: Math.random() * 5 - 2.5, + x: Math.random() * 2000 - 1000, + y: Math.random() * 2000 - 1000, + z: i * 500 + 500, + c: i + }; + } + setTimeout(probe, 1); +} + +function updateNode(index) { + let o = nodes[index]; + let x = o.x; + let y = o.y; + let z = o.z; + let tz = index * 500 + 500; + o.vx += (x < 0) * 10 - 5; + o.vy += (y < 0) * 10 - 5; + o.vz += (z < tz) * 1 - 0.5; + // lean into the curve + o.rz = o.vx * 0.5; + + x += o.vx; + y += o.vy; + z += o.vz; + + o.x = x; + o.y = y; + o.z = z; + + // iterative bubble sort + let p = nodes[index - 1]; + if (p && z > p.z) { + nodes[index - 1] = o; + nodes[index] = p; + } +} + +function drawNode(index) { + let o = nodes[index]; + let i = 0; + // float to 23.8 fixed + translation[i++] = o.rx * 256; + translation[i++] = o.ry * 256; + translation[i++] = o.rz * 256; + translation[i++] = o.sx * 256; + translation[i++] = o.sy * 256; + translation[i++] = o.sz * 256; + translation[i++] = o.x * 256; + translation[i++] = o.y * 256; + translation[i++] = o.z * 256; + translation[i++] = o.c; + gfx.render(E.getAddressOf(translation, true)); +} + +function tick(full) { + if (!lcdBuffer) + full = false; + + if (full) { + BLACK().drawRect(-1, -1, 0, 177); // dirty all the rows + gfx.clear(); + gfx.stars(); + // gfx.setCamera(0, 0, 0); + for (let i = 0; i < nodeCount; ++i) + updateNode(i); + for (let i = 0; i < nodeCount; ++i) + drawNode(i); + } + + var d = new Date(); + var h = d.getHours(), + m = d.getMinutes(); + var time = (" " + h).substr(-2) + ":" + m.toString().padStart(2, 0); + WHITE().drawString(time, 176 / 2, 176 - 16, true); +} + +init(); + +Bangle.on("lock", l => { + locked = l; + setupInterval(); +}); diff --git a/apps/warpdrive/app.png b/apps/warpdrive/app.png new file mode 100644 index 0000000000000000000000000000000000000000..ceef9691283ce5f2886a93a68bf7a95564163628 GIT binary patch literal 5936 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 SQ@D5l0000 Date: Sun, 14 Jan 2024 17:19:35 +0000 Subject: [PATCH 03/48] Fix incorrect file name in manifest --- apps/warpdrive/metadata.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/warpdrive/metadata.json b/apps/warpdrive/metadata.json index e4ca6a2af..6c01f18a2 100644 --- a/apps/warpdrive/metadata.json +++ b/apps/warpdrive/metadata.json @@ -10,7 +10,7 @@ "supports": ["BANGLEJS2"], "allow_emulator": false, "storage": [ - {"name":"pokewalk.app.js","url":"app.js"}, - {"name":"pokewalk.img","url":"app-icon.js","evaluate":true} + {"name":"warpdrive.app.js","url":"app.js"}, + {"name":"warpdrive.img","url":"app-icon.js","evaluate":true} ] } From f82a15abd6264336830c9596f20869f56f3607b2 Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Sun, 14 Jan 2024 17:33:53 +0000 Subject: [PATCH 04/48] Added screenshot --- apps/warpdrive/metadata.json | 1 + apps/warpdrive/screenshot.png | Bin 0 -> 3500 bytes 2 files changed, 1 insertion(+) create mode 100644 apps/warpdrive/screenshot.png diff --git a/apps/warpdrive/metadata.json b/apps/warpdrive/metadata.json index 6c01f18a2..c4fa1277e 100644 --- a/apps/warpdrive/metadata.json +++ b/apps/warpdrive/metadata.json @@ -5,6 +5,7 @@ "description": "A watchface with an animated 3D scene.", "readme": "README.md", "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], "type": "clock", "tags": "clock", "supports": ["BANGLEJS2"], diff --git a/apps/warpdrive/screenshot.png b/apps/warpdrive/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..110642499e82e1c0ec795126101c79023ee0e631 GIT binary patch literal 3500 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 Date: Mon, 15 Jan 2024 10:14:21 +0000 Subject: [PATCH 05/48] Make starfield into a tube --- apps/warpdrive/app.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/warpdrive/app.js b/apps/warpdrive/app.js index 990b6a961..760a5e02b 100644 --- a/apps/warpdrive/app.js +++ b/apps/warpdrive/app.js @@ -239,6 +239,12 @@ void stars() { for (int i = 0; i < 100; ++i) { int s = rng(); + int a = rng(); + int ca = cos(a); + int sa = sin(a); + int r = ((rng() & 0xFF) + 0xFF) << 9; + position.x = ca << 9; + position.y = sa << 9; position.x = ((signed char)(s & 0xFF)) << 9; s = rng(); position.y = ((signed char)(s & 0xFF)) << 9; @@ -260,6 +266,9 @@ void stars() { if (s < 1) s = 1; if (s > 10) s = 10; + int rx = s*sa >> 8; + int ry = s*ca >> 8; + position.x >>= 8; position.y >>= 8; scale.x >>= 8; @@ -270,8 +279,8 @@ void stars() { int color = 4 | (i & 1); fillTriangle( scale.x, scale.y, - position.x - s, position.y, - position.x + s, position.y, + position.x - rx, position.y - ry, + position.x + rx, position.y + ry, light ? alternate(color, 7) : dark ? alternate(color, 0) : solid(color) From d202e4c76e9ac041b26ca4fe722a11ff69717462 Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Mon, 15 Jan 2024 10:19:50 +0000 Subject: [PATCH 06/48] Track charging status --- apps/warpdrive/app.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/warpdrive/app.js b/apps/warpdrive/app.js index 760a5e02b..27c90e5fb 100644 --- a/apps/warpdrive/app.js +++ b/apps/warpdrive/app.js @@ -621,3 +621,8 @@ Bangle.on("lock", l => { locked = l; setupInterval(); }); + +Bangle.on('charging', c => { + charging = c; + setupInterval(); +}); From ca9ae0f7fe6ff633ed3ea33b227ef31679bd4799 Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Mon, 15 Jan 2024 11:17:56 +0000 Subject: [PATCH 07/48] Give the starfield a slight twist --- apps/warpdrive/app.js | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/apps/warpdrive/app.js b/apps/warpdrive/app.js index 27c90e5fb..e7291eadf 100644 --- a/apps/warpdrive/app.js +++ b/apps/warpdrive/app.js @@ -232,40 +232,30 @@ void v_project(Point* p){ } void stars() { - f += 5; + f += 7; _rngState = 1013904223; - // rng(); rng(); rng(); - for (int i = 0; i < 100; ++i) { - int s = rng(); - int a = rng(); + int a = rng() + ((i & 1 ? f : -f) << 7); int ca = cos(a); int sa = sin(a); - int r = ((rng() & 0xFF) + 0xFF) << 9; - position.x = ca << 9; - position.y = sa << 9; - position.x = ((signed char)(s & 0xFF)) << 9; - s = rng(); - position.y = ((signed char)(s & 0xFF)) << 9; - s = rng(); - position.z = 0xFF - ((s + f) & 0xFF); + int r = ((rng() & 0xFF) + 0xFF); + position.x = r*ca; + position.y = r*sa; + position.z = 0xFF - ((rng() + f) & 0xFF); position.z <<= 12; position.z -= 100 << 8; - int light = position.z < (800 << 8); int dark = position.z > ((800 + 500) << 8); - scale = position; - scale.z += 30 << 10; v_project(&position); + int s = 32 * position.z >> 8; + if (!s) + continue; + + scale.z += 30 << 10; v_project(&scale); - - s = (((s & 3) + 3) * position.z + 256) >> 7; - if (s < 1) s = 1; - if (s > 10) s = 10; - int rx = s*sa >> 8; int ry = s*ca >> 8; From d4f2ecd8feaaed5248bc4ea5d7c2a9e60d89832d Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Mon, 15 Jan 2024 11:33:55 +0000 Subject: [PATCH 08/48] Try to skip loading screen --- apps/warpdrive/app.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/warpdrive/app.js b/apps/warpdrive/app.js index e7291eadf..81cdda1d9 100644 --- a/apps/warpdrive/app.js +++ b/apps/warpdrive/app.js @@ -453,8 +453,11 @@ function test(addr, y) { } function probe() { - if (!start) + if (!start) { start = 0x20000000; + if (test(0x2002d3fe, 0)) // try to skip loading if possible + start = 0x2002d3fe; // FW=2v20 + } const end = Math.min(start + 0x800, 0x20038000); if (start >= end) { From 6477d4b60cf7d282aa77deddd90bde62533d6c2a Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Mon, 15 Jan 2024 11:34:25 +0000 Subject: [PATCH 09/48] Smooth speed transition between animating and frozen --- apps/warpdrive/app.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/apps/warpdrive/app.js b/apps/warpdrive/app.js index 81cdda1d9..92d62e486 100644 --- a/apps/warpdrive/app.js +++ b/apps/warpdrive/app.js @@ -431,15 +431,27 @@ const WHITE = g.setColor.bind(g, 0xFFFF); let lcdBuffer = 0, start = 0; -let locked = false; -var interval; +let locked = false, + charging = false; +var interval = 30, timeout; function setupInterval() { - if (interval) - clearInterval(interval); - if (!locked) - tick(); - interval = setInterval(tick.bind(null, !locked), locked ? 60 * 1000 : 70); + if (timeout) + clearTimeout(timeout); + if (!locked || charging) + tick(1); + let trigger; + let schedule = interval => { + timeout = setTimeout(trigger, interval | 0); + // print(interval); + }; + trigger = _ => { + if (!locked || charging) interval = 30; + else interval -= (interval - 130) * 0.15; + tick(1); + schedule(interval > 120 ? 60000 : interval); + }; + schedule(interval); } function test(addr, y) { From a11989315c1775e3e4906dc84e943a194727484c Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Mon, 15 Jan 2024 12:01:37 +0000 Subject: [PATCH 10/48] Add animated gif --- apps/warpdrive/README.md | 2 ++ apps/warpdrive/warpdrive.gif | Bin 0 -> 95494 bytes 2 files changed, 2 insertions(+) create mode 100644 apps/warpdrive/warpdrive.gif diff --git a/apps/warpdrive/README.md b/apps/warpdrive/README.md index 0f300c518..14fff1c97 100644 --- a/apps/warpdrive/README.md +++ b/apps/warpdrive/README.md @@ -2,4 +2,6 @@ An animated watchface featuring 3D spaceships traveling just shy of ludicrous speed. +![](warpdrive.gif) + WE BREAK FOR NOBODY. \ No newline at end of file diff --git a/apps/warpdrive/warpdrive.gif b/apps/warpdrive/warpdrive.gif new file mode 100644 index 0000000000000000000000000000000000000000..514edb24911ff39572e0c61e9805a57748e7822f GIT binary patch literal 95494 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 Date: Mon, 15 Jan 2024 15:10:15 +0000 Subject: [PATCH 11/48] Open launcher when button is pressed, show widgets when locked --- apps/warpdrive/app.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/warpdrive/app.js b/apps/warpdrive/app.js index 92d62e486..b66295d3a 100644 --- a/apps/warpdrive/app.js +++ b/apps/warpdrive/app.js @@ -439,7 +439,7 @@ function setupInterval() { if (timeout) clearTimeout(timeout); if (!locked || charging) - tick(1); + tick(locked); let trigger; let schedule = interval => { timeout = setTimeout(trigger, interval | 0); @@ -448,7 +448,7 @@ function setupInterval() { trigger = _ => { if (!locked || charging) interval = 30; else interval -= (interval - 130) * 0.15; - tick(1); + tick(locked); schedule(interval > 120 ? 60000 : interval); }; schedule(interval); @@ -598,11 +598,8 @@ function drawNode(index) { gfx.render(E.getAddressOf(translation, true)); } -function tick(full) { - if (!lcdBuffer) - full = false; - - if (full) { +function tick(widgets) { + if (lcdBuffer) { BLACK().drawRect(-1, -1, 0, 177); // dirty all the rows gfx.clear(); gfx.stars(); @@ -617,11 +614,19 @@ function tick(full) { var h = d.getHours(), m = d.getMinutes(); var time = (" " + h).substr(-2) + ":" + m.toString().padStart(2, 0); - WHITE().drawString(time, 176 / 2, 176 - 16, true); + WHITE() + .setFontAlign(0, 0.5) + .setFont('6x8', 2) + .drawString(time, 176 / 2, 176 - 16, true); + + if (widgets) + Bangle.drawWidgets(); } init(); +Bangle.setUI("clock"); +Bangle.loadWidgets(); Bangle.on("lock", l => { locked = l; setupInterval(); From 04c08de89aa6a7f6c4c36dcac717f1ae96870d3a Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Mon, 15 Jan 2024 15:23:39 +0000 Subject: [PATCH 12/48] Don't update animation when screen is locked --- apps/warpdrive/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/warpdrive/app.js b/apps/warpdrive/app.js index b66295d3a..9559de693 100644 --- a/apps/warpdrive/app.js +++ b/apps/warpdrive/app.js @@ -599,7 +599,7 @@ function drawNode(index) { } function tick(widgets) { - if (lcdBuffer) { + if (lcdBuffer && !widgets) { BLACK().drawRect(-1, -1, 0, 177); // dirty all the rows gfx.clear(); gfx.stars(); From 9a82a2e1eef6e55d03fe8b13626f954d20851a20 Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Mon, 15 Jan 2024 17:43:05 +0000 Subject: [PATCH 13/48] Ensure text is drawn with a black background --- apps/warpdrive/app.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/warpdrive/app.js b/apps/warpdrive/app.js index 9559de693..d3404276d 100644 --- a/apps/warpdrive/app.js +++ b/apps/warpdrive/app.js @@ -615,6 +615,7 @@ function tick(widgets) { m = d.getMinutes(); var time = (" " + h).substr(-2) + ":" + m.toString().padStart(2, 0); WHITE() + .setBgColor(0) .setFontAlign(0, 0.5) .setFont('6x8', 2) .drawString(time, 176 / 2, 176 - 16, true); From 84fd254d547443f03f02b876b2c6e3c05e0ac929 Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Mon, 15 Jan 2024 18:36:24 +0000 Subject: [PATCH 14/48] Implement full theme support --- apps/warpdrive/app.js | 95 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 79 insertions(+), 16 deletions(-) diff --git a/apps/warpdrive/app.js b/apps/warpdrive/app.js index d3404276d..3ceac4e89 100644 --- a/apps/warpdrive/app.js +++ b/apps/warpdrive/app.js @@ -1,6 +1,6 @@ const gfx = E.compiledC(` // void init(int, int, int) -// void clear() +// void clear(int) // void render(int, int) // void setCamera(int, int, int) // void stars() @@ -53,15 +53,6 @@ int cos(int angle) { return sin(angle + 0x8000); } -void clear() { - unsigned short* cursor = (unsigned short*) fb; - for (int y = 0; y < 176; ++y) { - for (int x = 0; x < 66/2; ++x) - *cursor++ = 0; - cursor++; - } -} - void setCamera(int x, int y, int z) { camera.x = x; camera.y = y; @@ -128,6 +119,76 @@ void drawHLine(int x, unsigned int y, int l, unsigned int c) { } } +void fillRect(int x, unsigned int y, int w, int h, unsigned int c) { + if (x < 0) { + w += x; + x = 0; + } + if (x + w >= 176) { + w = 176 - x; + } + if (w <= 0 || y >= 176) + return; + + if (y < 0) { + h += y; + y = 0; + } + if (y + h >= 176) { + h = 176 - y; + } + if (h <= 0 || y >= 176) + return; + + int bitstart = x * 3; + int bitend = (x + w) * 3; + int wstart = bitstart >> 5; + int wend = bitend >> 5; + int padstart = bitstart & 31; + int padend = bitend & 31; + int maskstart = -1 << padstart; + int maskend = unsigned(-1) >> (32 - padend); + if (wstart == wend) { + maskstart &= maskend; + maskend = 0; + } + + int* row = (int*) &fb[y * stride]; + if (maskstart) { + for (int i = 0; i < h; ++i) + row[wstart + (i*stride>>2)] = (row[wstart + (i*stride>>2)] & ~maskstart) | ((c << padstart) & maskstart); + while (bitstart >> 5 == wstart) + bitstart += 3; + } + if (maskend) { + for (int i = 0; i < h; ++i) + row[wend + (i*stride>>2)] = (row[wend + (i*stride>>2)] & ~maskend) | + (((c >> (30 - padend)) | (c >> (36 - padend))) & maskend); + } + bitend -= padend; + for (int x = bitstart; x < bitend; x += 10 * 3) { + unsigned int R = x & 31; + R = (c << R) | (c >> (36 - R)) | (c >> (30 - R)) | (c << (R - 6)); + for (int i = 0; i < h; ++i) + row[(x >> 5) + (i*stride>>2)] = R; + } +} + +void clear(int c) { + c &= 7; + if (!c || c==7) { + c = solid(c); + unsigned short* cursor = (unsigned short*) fb; + for (int y = 0; y < 176; ++y) { + for (int x = 0; x < 66/2; ++x) + *cursor++ = c; + cursor++; + } + } else { + fillRect(0, 0, 176, 176, solid(c)); + } +} + void fillTriangle( int x0, int y0, int x1, int y1, int x2, int y2, @@ -426,6 +487,7 @@ const nodeCount = 4; const nodes = new Array(nodeCount); const sintable = new Uint8Array(256); const translation = new Uint32Array(10); +let bgColor = 0; const BLACK = g.setColor.bind(g, 0); const WHITE = g.setColor.bind(g, 0xFFFF); let lcdBuffer = 0, @@ -433,7 +495,8 @@ let lcdBuffer = 0, let locked = false, charging = false; -var interval = 30, timeout; +var interval = 30, + timeout; function setupInterval() { if (timeout) @@ -522,6 +585,9 @@ function probe() { } function init() { + bgColor = g.theme.bg & 0x8410; + bgColor = ((bgColor >> 15) | (bgColor >> 9) | (bgColor >> 2)); + g.clear(); g.setFont('6x8', 2); g.setFontAlign(0, 0.5); @@ -601,9 +667,8 @@ function drawNode(index) { function tick(widgets) { if (lcdBuffer && !widgets) { BLACK().drawRect(-1, -1, 0, 177); // dirty all the rows - gfx.clear(); + gfx.clear(bgColor); gfx.stars(); - // gfx.setCamera(0, 0, 0); for (let i = 0; i < nodeCount; ++i) updateNode(i); for (let i = 0; i < nodeCount; ++i) @@ -614,9 +679,7 @@ function tick(widgets) { var h = d.getHours(), m = d.getMinutes(); var time = (" " + h).substr(-2) + ":" + m.toString().padStart(2, 0); - WHITE() - .setBgColor(0) - .setFontAlign(0, 0.5) + g.setFontAlign(0, 0.5) .setFont('6x8', 2) .drawString(time, 176 / 2, 176 - 16, true); From d41ffac16b648dc24dac8716f84df16e7546729a Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Mon, 15 Jan 2024 18:38:37 +0000 Subject: [PATCH 15/48] Update README.md --- apps/warpdrive/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/warpdrive/README.md b/apps/warpdrive/README.md index 14fff1c97..0f596a500 100644 --- a/apps/warpdrive/README.md +++ b/apps/warpdrive/README.md @@ -4,4 +4,6 @@ An animated watchface featuring 3D spaceships traveling just shy of ludicrous sp ![](warpdrive.gif) -WE BREAK FOR NOBODY. \ No newline at end of file +WE BREAK FOR NOBODY. + +Theme colors and widgets supported. Widgets only appear when the screen is locked. \ No newline at end of file From 7633f9e2752903177dee98a7dd84bf0a8c106f39 Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Mon, 15 Jan 2024 18:59:29 +0000 Subject: [PATCH 16/48] Make sure text is drawn with the foreground color --- apps/warpdrive/app.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/warpdrive/app.js b/apps/warpdrive/app.js index 3ceac4e89..d80356ecc 100644 --- a/apps/warpdrive/app.js +++ b/apps/warpdrive/app.js @@ -679,7 +679,8 @@ function tick(widgets) { var h = d.getHours(), m = d.getMinutes(); var time = (" " + h).substr(-2) + ":" + m.toString().padStart(2, 0); - g.setFontAlign(0, 0.5) + g.setColor(g.theme.fg) + .setFontAlign(0, 0.5) .setFont('6x8', 2) .drawString(time, 176 / 2, 176 - 16, true); From f5ae409a9212fea0f41879291c6d8b5dc849e18b Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Mon, 15 Jan 2024 19:35:23 +0000 Subject: [PATCH 17/48] Simplify scheduling code since it wasn't playing well with widgets --- apps/warpdrive/app.js | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/apps/warpdrive/app.js b/apps/warpdrive/app.js index d80356ecc..cf7d40b6e 100644 --- a/apps/warpdrive/app.js +++ b/apps/warpdrive/app.js @@ -498,23 +498,12 @@ let locked = false, var interval = 30, timeout; -function setupInterval() { +function setupInterval(force) { if (timeout) clearTimeout(timeout); - if (!locked || charging) - tick(locked); - let trigger; - let schedule = interval => { - timeout = setTimeout(trigger, interval | 0); - // print(interval); - }; - trigger = _ => { - if (!locked || charging) interval = 30; - else interval -= (interval - 130) * 0.15; - tick(locked); - schedule(interval > 120 ? 60000 : interval); - }; - schedule(interval); + let stopped = locked && !charging; + timeout = setTimeout(setupInterval, stopped ? 60000 : 30); + tick(stopped && !force); } function test(addr, y) { @@ -581,7 +570,7 @@ function probe() { print('Found lcdBuffer at ' + lcdBuffer.toString(16) + ' stride=' + stride); gfx.init(start, stride, E.getAddressOf(sintable, true)); gfx.setCamera(0, 0, -300 << 8); - setupInterval(); + setupInterval(true); } function init() { From a2c6f53ecebccf0b2b301a64326f096e0e3ae82b Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Tue, 16 Jan 2024 23:21:37 +0000 Subject: [PATCH 18/48] Add Synthwave variant of Warpdrive watchface --- apps/synthwave/README.md | 9 + apps/synthwave/app-icon.js | 1 + apps/synthwave/app.js | 724 ++++++++++++++++++++++++++++++++++ apps/synthwave/app.png | Bin 0 -> 4862 bytes apps/synthwave/metadata.json | 17 + apps/synthwave/screenshot.png | Bin 0 -> 3006 bytes apps/synthwave/theme.png | Bin 0 -> 3082 bytes apps/synthwave/widgets.png | Bin 0 -> 3171 bytes 8 files changed, 751 insertions(+) create mode 100644 apps/synthwave/README.md create mode 100644 apps/synthwave/app-icon.js create mode 100644 apps/synthwave/app.js create mode 100644 apps/synthwave/app.png create mode 100644 apps/synthwave/metadata.json create mode 100644 apps/synthwave/screenshot.png create mode 100644 apps/synthwave/theme.png create mode 100644 apps/synthwave/widgets.png diff --git a/apps/synthwave/README.md b/apps/synthwave/README.md new file mode 100644 index 000000000..9f92de33f --- /dev/null +++ b/apps/synthwave/README.md @@ -0,0 +1,9 @@ +# Synthwave Watch + +Fly towards the sunset in a 3D jet, cruising to the sound of futuristic synthesizers*. + +![](screenshot.png) ![](widgets.png) ![](theme.png) + +Theme colors and widgets supported. Widgets only appear when the screen is locked. + +* synthesizers not included diff --git a/apps/synthwave/app-icon.js b/apps/synthwave/app-icon.js new file mode 100644 index 000000000..5b46e62cf --- /dev/null +++ b/apps/synthwave/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4f/AoP///NjHvlGf/e4yMVzFf/cUqNHAQNFAQnVBAvPtu27fv2Vp1Wr12r3Vp00YsMEiEd/v3jv3798uIcBuMFEYtvEYP79++926AIPq9eq02atOGiEUqvf799+8RoscuPFikQEYlp0+uEYW6AQIFB12u3WqJQOaCgQeBAgPNmnTpoHBosEEYZlC3YmCSYP/3369YLBEYVBEoYgBEYQCBTQVBoo7BMoJEBEAPnz1//5QD3XrMoMUuYdCpl548dEYUc6P3+4TBMoX//1DhmSvIoBEYP69/+uPHIIhnB48cuhKE+47CMwXyEYMyvPnEYQOCWAIjD48avOGjFxBALhBu/XC4f//kwyVJkmTk4gCAQVfEYV9mOmjARB7lxcAQCCEAJoBvOMkgjBk88pMw/JKDCgP06dBoOECIMluFJkxuCEYQ7C/EaEYlK7OT8wOCNAcfeYIjCua9BqPx44OBaIdDzfACIOf82YtOmiFDEYcx48eot0ic161044jBocHEYI4B/3/+cY7MP1M/wEYEYMMz1//zLDpl9ilcq/sqHDg0evPHjpHC//tgkQpVI9EDmEwknDk4NB3/TrojC4sQEAJEBpOHzzaCI4IkC/XQgEGmFByZECBgX/CgXcqKJC4sWjBEBKYRrD73//P4tf2zEwIgOfEQYjDa4PFqtOuPHjhEDEYgVB9XyIAPnEAYLBa4vV6r+BDogCC6IqBDAP+12vDwX+XgQgCAQOu79Nj8UEA4CBJYIjBC4m/IIO/BAgCB9euuPNm5ELEYW6DQoCKqPHj/fo4aB5v0KAMcqP3EYf6LgP/HYJoC3Xr3369er3YjBEAQmB7l94/3793ml9osXuPFigaBEAW+34gBRIRoC1wFBC4PXudHfQNHj9d+v3JoPfI4MX3fu9w7BDQO/1eu3eu/XqEgPq91zoogB/vXrt3i9duvXqYvBqN17/7JAO7JQROC3XuEAIFB33u7oXBJQVR+omBEYIgBFgXX/Q+B9wdBxwjDAQPo9f6AoIA=")) diff --git a/apps/synthwave/app.js b/apps/synthwave/app.js new file mode 100644 index 000000000..ac277b739 --- /dev/null +++ b/apps/synthwave/app.js @@ -0,0 +1,724 @@ +const gfx = E.compiledC(` +// void init(int, int, int) +// void tick(int) +// void render(int) +// void setCamera(int, int, int) +// void bubble(int, int, int, int) + +unsigned char* fb; +int stride; +unsigned char* sint; + +const int near = 5 << 8; +int f = 0; + +typedef struct { + int x, y, z; +} Point; + +Point camera; +Point rotation; +Point scale; +Point position; +Point speed; + +const unsigned char ship[] = { +0,38,25,10,3,8,6,10,7,3,6,13,3,11,5,13,1,12,3,15,3,5,8,15,1,3,7,13,12,11,3,15,5,6,8,15,6,1,7,10,5,0,6,10,0,1,6,12,5,11,4,12,12,1,2,12,2,11,12,12,10,5,4,13,5,10,0,12,2,1,9,13,9,1,0,12,4,11,2,10,19,22,21,12,4,2,10,12,10,2,9,10,13,16,15,13,10,9,0,15,21,20,19,15,15,14,13,15,19,20,22,15,13,14,16,15,21,23,20,15,15,17,14,15,22,20,23,10,22,24,21,15,16,14,17,10,16,18,15,15,24,23,21,15,18,17,15,15,22,23,24,15,16,17,18,0,0,62,236,243,244,247,0,234,0,229,194,11,0,234,21,243,246,0,234,33,193,250,20,63,249,19,249,4,3,9,4,3,7,247,222,250,247,222,240,0,22,238,13,22,226,1,20,229,7,62,225,11,20,208,27,62,19,0,20,22,12,20,33,0,18,30,5,60,34,10,18,52,26,60 +}; + +const unsigned int terrainLength = 12; +const unsigned int terrainWidth = 12; +unsigned char terrain[terrainLength][terrainWidth]; +unsigned int travel = 0; + +unsigned int _rngState; +unsigned int rng() { + _rngState ^= _rngState << 17; + _rngState ^= _rngState >> 13; + _rngState ^= _rngState << 5; + return _rngState; +} + +void shiftTerrain() { + travel++; + for (int i = terrainLength - 1; i > 0; --i) { + for (int x = 0; x < terrainWidth; ++x) + terrain[i][x] = terrain[i-1][x]; + } + + for (int x = 0; x < terrainWidth; ++x) + terrain[0][x] = (rng() & 0x3F) + 0xF; + for (int x = 0; x < (terrainWidth >> 3); ++x) + terrain[0][((terrainWidth>>1)-(terrainWidth>>4)) + x] >>= 1; + for (int x = 0; x < (terrainWidth >> 2); ++x) + terrain[0][((terrainWidth>>1)-(terrainWidth>>5)) + x] = 0; +} + +void init(unsigned char* _fb, int _stride, unsigned char* _sint) { + fb = _fb; + stride = _stride; + sint = _sint; + _rngState = 1013904223; + for (int i = 0; i < terrainLength; ++i) + shiftTerrain(); + speed.x = 0; + speed.y = 0; + speed.z = 0; + position.x = 100 << 8; + position.y = -150 << 8; + position.z = 100 << 8; + rotation.x = 0; + rotation.y = 256 << 8; + rotation.z = 0; + scale.x = 1 << 8; + scale.y = 1 << 8; + scale.z = 1 << 8; +} + +int sin(int angle) { + int a = (angle >> 7) & 0xFF; + if (angle & (1 << 15)) + a = 0xFF - a; + int v = sint[a]; + if (angle & (1 << 16)) + v = -v; + return v; +} + +int cos(int angle) { + return sin(angle + 0x8000); +} + +void setCamera(int x, int y, int z) { + camera.x = x; + camera.y = y; + camera.z = z; +} + +unsigned int solid(unsigned int c) { + c &= 7; + c |= c << 3; + c |= c << 6; + c |= c << 12; + c |= c << 24; + return c; +} + +unsigned int alternate(unsigned int a, unsigned int b) { + unsigned int c = (a & 7) | ((b & 7) << 3); + c |= c << 6; + c |= c << 12; + c |= c << 24; + return c; +} + +void drawHLine(int x, unsigned int y, int l, unsigned int c) { + if (x < 0) { + l += x; + x = 0; + } + if (x + l >= 176) { + l = 176 - x; + } + if (l <= 0 || y >= 176) + return; + + if (y & 1) + c = alternate(c >> 3, c); + + int bitstart = x * 3; + int bitend = (x + l) * 3; + int wstart = bitstart >> 5; + int wend = bitend >> 5; + int padstart = bitstart & 31; + int padend = bitend & 31; + int maskstart = -1 << padstart; + int maskend = unsigned(-1) >> (32 - padend); + if (wstart == wend) { + maskstart &= maskend; + maskend = 0; + } + + int* row = (int*) &fb[y * stride]; + if (maskstart) { + row[wstart] = (row[wstart] & ~maskstart) | ((c << padstart) & maskstart); + while (bitstart >> 5 == wstart) + bitstart += 3; + } + if (maskend) + row[wend] = (row[wend] & ~maskend) | + (((c >> (30 - padend)) | (c >> (36 - padend))) & maskend); + bitend -= padend; + for (int x = bitstart; x < bitend; x += 10 * 3) { + unsigned int R = x & 31; + row[x >> 5] = (c << R) | (c >> (36 - R)) | (c >> (30 - R)) | (c << (R - 6)); + } +} + +void fillRect(int x, unsigned int y, int w, int h, unsigned int c) { + if (x < 0) { + w += x; + x = 0; + } + if (x + w >= 176) { + w = 176 - x; + } + if (w <= 0 || y >= 176) + return; + + if (y < 0) { + h += y; + y = 0; + } + if (y + h >= 176) { + h = 176 - y; + } + if (h <= 0 || y >= 176) + return; + + int bitstart = x * 3; + int bitend = (x + w) * 3; + int wstart = bitstart >> 5; + int wend = bitend >> 5; + int padstart = bitstart & 31; + int padend = bitend & 31; + int maskstart = -1 << padstart; + int maskend = unsigned(-1) >> (32 - padend); + if (wstart == wend) { + maskstart &= maskend; + maskend = 0; + } + + int* row = (int*) &fb[y * stride]; + if (maskstart) { + for (int i = 0; i < h; ++i) + row[wstart + (i*stride>>2)] = (row[wstart + (i*stride>>2)] & ~maskstart) | ((c << padstart) & maskstart); + while (bitstart >> 5 == wstart) + bitstart += 3; + } + if (maskend) { + for (int i = 0; i < h; ++i) + row[wend + (i*stride>>2)] = (row[wend + (i*stride>>2)] & ~maskend) | + (((c >> (30 - padend)) | (c >> (36 - padend))) & maskend); + } + bitend -= padend; + for (int x = bitstart; x < bitend; x += 10 * 3) { + unsigned int R = x & 31; + R = (c << R) | (c >> (36 - R)) | (c >> (30 - R)) | (c << (R - 6)); + for (int i = 0; i < h; ++i) + row[(x >> 5) + (i*stride>>2)] = R; + } +} + +void fillTriangle( int x0, int y0, + int x1, int y1, + int x2, int y2, + unsigned int col) { + int a, b, y, last, tmp; + + a = 176; + b = 176; + if( x0 < 0 && x1 < 0 && x2 < 0 ) return; + if( x0 >= a && x1 > a && x2 > a ) return; + if( y0 < 0 && y1 < 0 && y2 < 0 ) return; + if( y0 >= b && y1 > b && y2 > b ) return; + + // Sort coordinates by Y order (y2 >= y1 >= y0) + if (y0 > y1) { + tmp = y0; y0 = y1; y1 = tmp; + tmp = x0; x0 = x1; x1 = tmp; + } + if (y1 > y2) { + tmp = y2; y2 = y1; y1 = tmp; + tmp = x2; x2 = x1; x1 = tmp; + } + if (y0 > y1) { + tmp = y0; y0 = y1; y1 = tmp; + tmp = x0; x0 = x1; x1 = tmp; + } + + if (y0 == y2) { // Handle awkward all-on-same-line case as its own thing + a = b = x0; + if (x1 < a) a = x1; + else if (x1 > b) b = x1; + if (x2 < a) a = x2; + else if (x2 > b) b = x2; + drawHLine(a, y0, b - a + 1, col); + return; + } + + int dx01 = x1 - x0, + dx02 = x2 - x0, + dy02 = (1<<16) / (y2 - y0), + dx12 = x2 - x1, + sa = 0, + sb = 0; + + // For upper part of triangle, find scanline crossings for segments + // 0-1 and 0-2. If y1=y2 (flat-bottomed triangle), the scanline y1 + // is included here (and second loop will be skipped, avoiding a /0 + // error there), otherwise scanline y1 is skipped here and handled + // in the second loop...which also avoids a /0 error here if y0=y1 + // (flat-topped triangle). + if (y1 == y2) last = y1; // Include y1 scanline + else last = y1 - 1; // Skip it + + y = y0; + + if( y0 != y1 ){ + int dy01 = (1<<16) / (y1 - y0); + for (y = y0; y <= last; y++) { + a = x0 + ((sa * dy01) >> 16); + b = x0 + ((sb * dy02) >> 16); + sa += dx01; + sb += dx02; + /* longhand: + a = x0 + (x1 - x0) * (y - y0) / (y1 - y0); + b = x0 + (x2 - x0) * (y - y0) / (y2 - y0); + */ + if (a > b){ + tmp = a; + a = b; + b = tmp; + } + drawHLine(a, y, b - a + 1, col); + } + } + + // For lower part of triangle, find scanline crossings for segments + // 0-2 and 1-2. This loop is skipped if y1=y2. + if( y1 != y2 ){ + int dy12 = (1<<16) / (y2 - y1); + sa = dx12 * (y - y1); + sb = dx02 * (y - y0); + for (; y <= y2; y++) { + a = x1 + ((sa * dy12) >> 16); + b = x0 + ((sb * dy02) >> 16); + sa += dx12; + sb += dx02; + if (a > b){ + tmp = a; + a = b; + b = tmp; + } + drawHLine(a, y, b - a + 1, col); + } + } +} + +void v_project(Point* p){ + int fovz = ((90 << 16) / ((90 << 8) + p->z)); // 16:16 / 16:8 -> 16:8 + p->x = (p->x * fovz >> 8) + (176/2 << 8); // 16:8 * 16:8 = 16:16 -> 16:8 + p->y = (176/2 << 8) - (p->y * fovz >> 8); + p->z = fovz; +} + +void drawTerrain() { + const int tileSize = 40 << 8; + camera.x = (terrainWidth + 2) * tileSize / 2; + camera.y = 60 << 8; + camera.z += 6 << 8; + if (camera.z > tileSize * 3) { + camera.z -= tileSize; + shiftTerrain(); + } + + int dist[] = { + solid(7), + alternate(5, 7), + solid(5), + solid(5), + alternate(5, 0), + solid(0) + }; + int line = solid(5); + + int fovz; + int prvz = ((90 << 16) / ((90 << 8) + ((terrainLength) * tileSize - camera.z))); // 16:16 / 16:8 = 16:8 + for (int i = 0; i < terrainLength - 1; ++i, prvz = fovz) { + fovz = ((90 << 16) / ((90 << 8) + ((terrainLength - (i + 1)) * tileSize - camera.z))); // 16:16 / 16:8 = 16:8 + int lum = i < 3 ? i - 3 : 3; + for (int x = 0; x < terrainWidth - 1; ++x) { + int ax = (((((x ) * tileSize - camera.x) >> 8) * prvz) >> 8) + (176/2); // int * 16:8 = 16:8 -> int + int bx = (((((x + 1) * tileSize - camera.x) >> 8) * prvz) >> 8) + (176/2); // int * 16:8 = 16:8 -> int + int cx = (((((x ) * tileSize - camera.x) >> 8) * fovz) >> 8) + (176/2); // int * 16:8 = 16:8 -> int + int dx = (((((x + 1) * tileSize - camera.x) >> 8) * fovz) >> 8) + (176/2); // int * 16:8 = 16:8 -> int + + int ay = (176/2) - ((((terrain[i ][x ] << 8) - camera.y) >> 8) * prvz >> 8); + int by = (176/2) - ((((terrain[i ][x + 1] << 8) - camera.y) >> 8) * prvz >> 8); + int cy = (176/2) - ((((terrain[i + 1][x ] << 8) - camera.y) >> 8) * fovz >> 8); + int dy = (176/2) - ((((terrain[i + 1][x + 1] << 8) - camera.y) >> 8) * fovz >> 8); + + int na = (ax - bx)*(ay - cy) - (ay - by)*(ax - cx); + if (na > 0) { + int c = (lum + (na >> 8)); + if (c < 0) c = 0; + else if (c > 5) c = 5; + c = dist[c]; + fillTriangle(ax, ay, bx, by, cx, cy, c); + } + + na = (bx - dx)*(by - cy) - (by - dy)*(bx - cx); + if (na > 0) { + int c = (lum + (na >> 8)); + if (c < 0) c = 0; + else if (c > 5) c = 5; + c = dist[c]; + fillTriangle(bx, by, cx, cy, dx, dy, c); + if (!c) { + fillTriangle(ax, ay, bx, by, bx, by - 1, line); + fillTriangle(ax, ay, cx, cy, cx, cy - 1, line); + } + } + } + } +} + +void transform(Point* p) { + int x = p->x; + int y = p->y; + int z = p->z; + int s, c; + + if (rotation.x) { + s = sin(rotation.x); + c = cos(rotation.x); + p->y = (y*c>>8) - (z*s>>8); + p->z = (y*s>>8) + (z*c>>8); + y = p->y; + z = p->z; + } + + if (rotation.z) { + s = sin(rotation.z); + c = cos(rotation.z); + p->x = (x*c>>8) - (y*s>>8); + p->y = (x*s>>8) + (y*c>>8); + x = p->x; + y = p->y; + } + + if (rotation.y) { + s = sin(rotation.y); + c = cos(rotation.y); + p->x = (x*c>>8) - (z*s>>8); + p->z = (x*s>>8) + (z*c>>8); + } + +// Scale + p->x = p->x * scale.x >> 8; + p->y = p->y * scale.y >> 8; + p->z = p->z * scale.z >> 8; + +// Translate + p->x += position.x; + p->y += position.y; + p->z += position.z; +} + +void fillCircleInternal(int xc, int yc, int x, int y, int c) { + drawHLine(xc - x, yc - y, x * 2, c); + drawHLine(xc - x, yc + y, x * 2, c); + drawHLine(xc - y, yc - x, y * 2, c); + drawHLine(xc - y, yc + x, y * 2, c); +} + +void fillCircle(int xc, int yc, int r, int color) { + if (r < 1 || xc + r < 0 || xc - r >= 176 || yc + r < 0 || yc - r >= 176) + return; + int x = 0, y = r; + int d = 3 - 2 * r; + fillCircleInternal(xc, yc, x, y, color); + while (y >= x) { + x++; + if (d > 0) { + y--; + d = d + 4 * (x - y) + 10; + } else { + d = d + 4 * x + 6; + } + fillCircleInternal(xc, yc, x, y, color); + } +} + +void bubble(int x, int y, int r, int c) { + fillCircle(x, y, r + 3, alternate(7, 4)); + fillCircle(x, y, r, alternate(c, 0)); + int rs = r * 0xE666 >> 16; + int off = (r - rs) * 0x9696 >> 16; + fillCircle(x + off, y - off, rs, solid(c)); + rs = r * 0x4CCC >> 16; + off = (r - rs) * 0x9696 >> 16; + fillCircle(x + off, y - off, rs, alternate(c, 7)); + rs = r * 0x1999 >> 16; + off = (r - rs) * 0x8E38 >> 16; + fillCircle(x + off, y - off, rs, solid(7)); +} + +void render(const unsigned char* m){ + if (position.z < near) + return; + + if (!m) + m = ship; + + int faceCount = (((int)m[0]) << 8) + (int)m[1]; + const unsigned char* faceOffset = m + 3; + const unsigned char* vtxOffset = faceOffset + faceCount*4; + + Point pointA, pointB, pointC; + Point* A = &pointA; + unsigned char* Ai = 0; + Point* B = &pointB; + unsigned char* Bi = 0; + Point* C = &pointC; + unsigned char* Ci = 0; + bool Ab, Bb, Cb; + + for (int face = 0; face> 2) & 1; + + const unsigned char* indexA = vtxOffset + ((int)*faceOffset++) * 3; + const unsigned char* indexB = vtxOffset + ((int)*faceOffset++) * 3; + const unsigned char* indexC = vtxOffset + ((int)*faceOffset++) * 3; + + if( indexA == Ai ){ Ab = true; } + else if( indexA == Bi ){ A = &pointB; Bb = true; } + else if( indexA == Ci ){ A = &pointC; Cb = true; } + else A = 0; + + if (indexB == Bi) { Bb = true; } + else if (indexB == Ai) { B = &pointA; Ab = true; } + else if (indexB == Ci) { B = &pointC; Cb = true; } + else B = 0; + + if (indexC == Ci) { Cb = true; } + else if (indexC == Bi) { C = &pointB; Bb = true; } + else if (indexC == Ai) { C = &pointA; Ab = true; } + else C = 0; + + if (!A) { + if (!Ab) { A = &pointA; Ab = true; } + else if (!Bb) { A = &pointB; Bb = true; } + else if (!Cb) { A = &pointC; Cb = true; } + A->x = ((signed char)*indexA++) << 8; + A->y = ((signed char)*indexA++) << 8; + A->z = ((signed char)*indexA) << 8; + transform(A); + if(A->z <= near) continue; + v_project(A); + } + + if (!B) { + if (!Ab) { B = &pointA; Ab = true; } + else if (!Bb) { B = &pointB; Bb = true; } + else if (!Cb) { B = &pointC; Cb = true; } + B->x = ((signed char)*indexB++) << 8; + B->y = ((signed char)*indexB++) << 8; + B->z = ((signed char)*indexB) << 8; + transform(B); + if(B->z <= near) continue; + v_project(B); + } + + if (!C) { + if (!Ab) { C = &pointA; Ab = true; } + else if (!Bb) { C = &pointB; Bb = true; } + else if (!Cb) { C = &pointC; Cb = true; } + C->x = ((signed char)*indexC++) << 8; + C->y = ((signed char)*indexC++) << 8; + C->z = ((signed char)*indexC) << 8; + transform(C); + if(C->z <= near) continue; + v_project(C); + } + + int cross = (A->x - B->x)*(A->y - C->y) - (A->y - B->y)*(A->x - C->x); + if (cross < 0) + continue; + + cross >>= 8; + int light = cross > (20000 << 3); + int dark = cross < (5000 << 2); + + fillTriangle( + A->x >> 8, A->y >> 8, + B->x >> 8, B->y >> 8, + C->x >> 8, C->y >> 8, + light ? alternate(color, 7) : + dark ? alternate(color, 0) : + solid(color) + ); + } +} + +void tick(int c) { + c &= 7; + if (!c || c==7) { + c = solid(c); + unsigned short* cursor = (unsigned short*) fb; + for (int y = 0; y < 176; ++y) { + for (int x = 0; x < 66/2; ++x) + *cursor++ = c; + cursor++; + } + } else { + fillRect(0, 0, 176, 176, solid(c)); + } + + + fillCircle(88, 110, 35, alternate(5,0)); + fillCircle(88, 110, 27, alternate(5,7)); + fillCircle(88, 110, 20, solid(7)); + drawTerrain(); + + speed.x += ((position.x < 0) ? 1 : -1) << 8; + speed.y += ((position.y < (-80 << 8)) ? 1 : -1) << 8; + rotation.x = speed.y; + rotation.z = speed.x; + position.y += speed.y >> 1; + position.x += speed.x >> 1; + + render(ship); +} + + +`); + +const sintable = new Uint8Array(256); +let bgColor = 0; +const BLACK = g.setColor.bind(g, 0); +const WHITE = g.setColor.bind(g, 0xFFFF); +let lcdBuffer = 0, + start = 0; + +let locked = false, + charging = false; +var interval = 30; +var timeout; + +function setupInterval(force) { + if (timeout) + clearTimeout(timeout); + let stopped = locked && !charging; + timeout = setTimeout(setupInterval, stopped ? 60000 : 60); + tick(stopped && !force); +} + +function test(addr, y) { + BLACK().fillRect(0, y, 176, y); + if (peek8(addr)) return false; + WHITE().fillRect(0, y, 176, y); + let b = peek8(addr); + BLACK().fillRect(0, y, 176, y); + if (!b) return false; + return !peek8(addr); +} + +function probe() { + if (!start) { + start = 0x20000000; + if (test(0x2002d3fe, 0)) // try to skip loading if possible + start = 0x2002d3fe; // FW=2v20 + } + const end = Math.min(start + 0x800, 0x20038000); + + if (start >= end) { + print("Could not find framebuffer"); + return; + } + + BLACK().fillRect(0, 0, 176, 0); + // sampling every 64 bytes since a 176-pixel row is 66 bytes at 3bpp + for (; start < end; start += 64) { + if (peek8(start)) continue; + WHITE().fillRect(0, 0, 176, 0); + let b = peek8(start); + BLACK().fillRect(0, 0, 176, 0); + if (!b) continue; + if (!peek8(start)) break; + } + + if (start >= end) { + setTimeout(probe, 1); + return; + } + + // find the beginning of the row + while (test(start - 1, 0)) + start--; + + /* + let stride = (176 * 3 + 7) >> 3, + padding = 0; + for (let i = 0; i < 20; ++i, ++padding) { + if (test(start + stride + padding, 1)) { + break; + } + } + + stride += padding; + if (padding == 20) { + print("Warning: Could not calculate padding"); + stride = 68; + } + */ + stride = 68; + + lcdBuffer = start; + print('Found lcdBuffer at ' + lcdBuffer.toString(16) + ' stride=' + stride); + gfx.init(start, stride, E.getAddressOf(sintable, true)); + gfx.setCamera(0, 0, 0); + setupInterval(true); +} + +function init() { + require("Font5x9Numeric7Seg").add(Graphics); + g.setFont("5x9Numeric7Seg"); + bgColor = g.theme.bg & 0x8410; + bgColor = ((bgColor >> 15) | (bgColor >> 9) | (bgColor >> 2)); + + g.clear(); + g.setFontAlign(0, 0.5); + g.drawString("[LOADING]", 90, 66); + + // setup sin/cos table + for (let i = 0; i < sintable.length; ++i) + sintable[i] = Math.sin((i * Math.PI * 0.5) / sintable.length) * ((1 << 8) - 1); + setTimeout(probe, 1); +} + +function tick(widgets) { + if (lcdBuffer && !widgets) { + BLACK().drawRect(-1, -1, 0, 177); // dirty all the rows + gfx.tick(bgColor); + } + + var d = new Date(); + var h = d.getHours(), + m = d.getMinutes(); + var time = (" " + h).substr(-2) + ":" + m.toString().padStart(2, 0); + g.setColor(g.theme.fg) + .setFontAlign(0, 0.5) + .setFont('5x9Numeric7Seg', 5) + .drawString(time, 176 / 2, 50, true); + + if (widgets) + Bangle.drawWidgets(); +} + +init(); + +Bangle.setUI("clock"); +Bangle.loadWidgets(); +Bangle.on("lock", l => { + locked = l; + setupInterval(); +}); + +Bangle.on('charging', c => { + charging = c; + setupInterval(); +}); diff --git a/apps/synthwave/app.png b/apps/synthwave/app.png new file mode 100644 index 0000000000000000000000000000000000000000..b85c1ac62d3d576ba8c42e0061143982aaf82254 GIT binary patch literal 4862 zcmV&=~bszBWT5Iq9JkNVM@0>Gp=Hi*L$Jg!N&G@#`cUo_j7x1&t>nmR)1*! zjn9YftR9_*5)mhC-HOKCBh^uL2m?eQM99$-lQR&AK_NLafg#KkOaM0+ zftY+!CfhbWSN0a+VBlhI230e2D9oTBa&v_#4DN7e08ZgJP0lzUws54xBhn;eX1jvn zNT?pQLnD%7qwT$R{=N1_=II}eUMxb)m;?;T-^Xh!QA>j3@;O6_}WaP)xO0Yc_CM!?N~+Yv(0vDbT@G zVFRnFI=r|QFXrYjH8;4r%dEObh@Hs;pAW0UyuH(;JAM1K%Vw;%xIV#?5@X#|4~`E# zS>`3sNYW(4F*h4wG>OeLjb^bKrDh~4g%~+B90NsR4irSBM#U_Vr9z;9L?RN%5GWET z5EBal$~ zjT^O&q;TwX2&6?Ucu{aK+1w0H;4nrFG9peKlZ%ivSQ(%hd7x6v zideQ1Y6;AoC?*794kR@bxik`L4;(l-1qvgkI@OI+Ez(HbkTfQ#V>OOp6nMlE9qv}L zuO%)K8Y(?eTPP_FE(66@hOCwgYPz7o3k(`2l*A<=MGlcEAuw?b z9)T-_NHHSTBElGh8dK z7kBN4z7sv|>7whgk~%Fl;z5v)w6{B3ILwFKDNzW_wNn*1P^g?Lmx?5^)Zki@FxN!2 z(1^mA<20rR&t3!^q;Llj$l(ej=<@DUguXs^VQYkX&ShKU0oT`My#jKfqB1Sklh2Gg zDK(`BhUwoMj-8A97@XdJ^rIwRu_WRkT#*EDqEJI4NhN`qY6>H83Q`F-98M&<{p>4H z0x3`sBC=u-C_xpOp@J63Zn^%}qFyMqW}Ry1rSpV2_BvDNVjPx9Z= z-+qW^ug=TywVV!zKs=_{fHzd?Sj|FxvL7{Gl2ao*0}ds+>z-GDf-pcbf@x#3e9S@StfrgfY-X^t^DP$8<)&JN=Di=x-Y;i98PV z$v8R8qlI??c8EAg@MJrAWx>Fu8l{tOd{|G?lA-m0ZYZQMi$(R zxQOo6$raY~mX~F?MbjMlDtG&2a}4UfLpht8p}2-OH2vLjlsECr!43s_wOxIi)(g+k zc@+5!r8i)*)u5^a3&)3FMo`27V?(6~gpnZtVn_sLP>pqi;J9Ge_wr&oxQb=Rb8DAw z1==|8YTEpefRcX3#NxdHEUvkz=;KLl?td8C{rvp zy`Isopj(1?b#3pd<<4_7Ka=HJ>sL|^PhVZGNmsllj2ejq8wj_C(Op$SA>v$FF?^>!5PIqE|1joMF*Cb}NHhr~ z;s}OOMpBG{sl4fP|NHu=)ay{i^z7Fj#u3Gs(2S5~2pf>&pfOC}f6E`B9G?e{R=*c_-LiS+1CMTDFbqrNg2CZ6jC>8%K=b=P8KMZBg@{Qi zhXBD59Zf;am>4Gb#GgI+2S2>_7Ra+96k9%Z?r&n8kfw+yfRhN5{rB!3EH2b9-1X5{ zzx>VkMCJ8RO3SV2*j+b>T0*Bo!7w0abdDSle)+Zses(x}q8jInl}ahZ4Mxp)6c7V~ zfS>`)h=RZnf*67!*vH=eNQ3IHu==SoOV&Jc=eMvthW_x^@4fzo2OfX(@sIxTU(Vn6 z=YM+aADnvI?AW8zhkmHvxUA3rc)nGvXZRuwgDw?&8G8jgz_LcS_t-!E*pY`%%_d&Q zP{oQZq(EOf_3HqEs7BZ_y1it{YU?k8{J!}rVmevHyq=VheR z%GUq$%=_;9&3C`&mwtIC@BjMUpL+C(w+QO{aqH7~wnms>^9rutCilJknP0-S7Ar%` zXbCGtXXt@!=m-k*?wysQts=bGFB?jM`4}o<66Q)0QLL);fvf-ZFK>Q-gy2Z)XL0~X zK!^1dqyP0Y|M~vS+4<+6|H)tY#Z&U;uPi?O_9q@1WA;v*U6EH_8*n#HPH6PP@BAa| zEno(941|ucA{@9bd9{xFJ?&-<2(NX+L(`FIM}Kj?k&M9)I&+T>0t-jz7=eb}wsR5;S3a3|j?#k1)k4L%#5( z_hDbLUog*T4V_~RtORqzfe#B=cWGX5P|#&O_H3YBmIR~7ZRS6 z;&}O7doJ1J?6Eh$eC?^*u)U812Ra-Kn7x%}Pm^uoq@z3T_0yk!5B3#ee`7KGq&ThhlDzjtqc;(e!ox{;oR zNl=GooZ_Ec{?5OB@v984G5Xp1k@uIo-d(=XgDW6m{nUp)fTSQrHuQ?tu_CM;ebALH z+qh7y3iSo+?CqefS?4s6J2(=Kvh=P>kv@0pZ&#$`0Jsh*MP_DkPApZBnvs}){@kB! z=W18?YN>)$?tO*y+mr3r{rJ6N9Z*0H0@&aVbL0RgbZ(1kr?Z2>R|UO#=RT+xr{XYG z0~L5NKTq9lUI)QYD5!UqWkpt1v2rWLMLfdEIT;7gky*7R5Tvd63?_;(8=js{Z`2zCDf#>C#Ay3jtc5?-nB- ztz~$cmWPM5hjnj`+o zB9Kd-v+4G}oyP#^l;Yqh@XWL>IXes}4k9N6N9Wn0;ii?l z*J(WB@0NpIvWbST`K#y3^&jk>H$J!)cRqRK@2vWvmJm>SD+e|tmX_?0(a5|pWYO<4 zySuANLhV=>nF8)cq;LWekkrSSpR3jiQ^3s~aJZRgWH>3z5a<&}e{-P+E?^Qmgu}a| z+HEDMm;xvgrjW6SLTia*mw-}?4`|p&CZ<}sB91Jb=FDzT1DjJ+7w67>NoH) zg)uT}jE}oy(wpW?G~vX`7pPmhX61!gi_)vQ6Io|W#)uAs4dx<5N`c7W1~+nH3LP2P z66l6ea2tbf27@p_2_)_^&{n9-d>ynn+;M=ipfXW9s$&g)+46#*=Ip*exxuBiLT=0C zSJ74~4FZVRMcv6=LBS;|v*eHeFgp3BY9ZZ37B88;o@*eGKQM@%n!m!a8JNu(GR#E2`t zygk>i)$2n(%}@uN99$gaMnue8eRZ8nS9xk;75r?OWwMw;$0a+jvmVLS(08$q%VEeM zWAg!#;!~I#;=Q?XE%UbgaAXTxJ)RbOp?{tZ4hZ+^D8nx1yI5<`=fnDn z4GZgw+hNC|Eh7tq*_dSFBLAiN$B*^f-Ja}8wqRGauQ{Ig#fxG63Kol07F>$x3LO&d zqjxB>!>}amc~^wrh1uKu_{-i0sm=^F8@uzIhst=L^nh1$oiDizti5)E;4m`Z3X{{* z>SkZODtnY$Xvh7+i)cFgWxDu6ym>%95pNBn9bTW>9nGq^P0i2McU`b$Vf%^kvDE-% zA(MBEPrTNzZ*td@AGn@X`c~B?rTxYagPr(B(iO{&*e#_#lG2*OVN6f-O~*9hM9xN7 z>hjXM6$*3HBVm-xD>N%~@<{yAhT%d6LZ~L4Ux*7NIAlgutHZnP*01CG?oiH^oi@;l z^t|rhL^kFl18t<)6IJ_-eD#H}|7w}nMZRHIui4_U?&vMj%=-KswS921JWFYo)5KF9 zLRQ-xs>z{QE`ExeXwo_jrQPTz^19h08vMA{XVYd^=p6Q3FhON-)4b#Xy`AmC^HXS7 z@{oJvHI?F8+-|kQH^Sb_Wt~yr?(R2e7@%NpB)p`{tH9gC(Pp)Lx@y~XcRtN;P`mVg z75p+S_lMPBR;6lR+pD%(iFs}E&gLC|1Diy)>%)t*-%8GI2h{cC3pO75E3~|xe4fjJ zg+(~ffZCXeUhmtx^-2vpwgTMX4iqHYst3c$>r+~MY~W}WAIhZC3TgB!B`LA4qQRzz6}K+)0D za7kAKv`BeNee80-HfBeV*V7xK)Zf8PA~xt$dBd8clup`c(hgrt-Ax(hXy>+s>1*iE zUBCIE(+@n??lxBK$V%0eh|L>H24=wEfD-_+tMMSa4w1$d&MvB6m#cPri@C#a!lwzc zjnNXrsE^lVz4Vo$@OFLph*uk2{XZ%kQB0)q_Ahf`g8*CV%m z;>NeXOIKcyo5i%SyK7-vV}KhuoEh#8uTWJcL`Flup!Fe{{>um76ekBr97^?2>to@o k(i-EUEM06s*}>xf0#0esOW!qP(e%6@l7}v0NTicEQJ@nU{oTKfwCani(@+m{n#lw;km zjtTCzdFY2TdWX3FwdMc2>fc_^FwpPp89{LYFFroWd9pYvGRot!gTs@RNm=Gsp}FI~ z)51eKQy5%v!!t;y>Yt&PIe+UmW)qHCYr*SoxQ#@SRqE zZtb)GXS=VD^*SvjlA9+uGDCe+YpS^(k6-X=SJ%XbFfP@7Wrl{A<$q3P&OTzCmsCzW zS{#)0-uTg5Zyz|RVM1v?TC`^TWv%#=!&DjG|;_aKW?+)k1ygLzw>D1 zdWA)+`)+PzC5_=7ALSP<@|GYAohEm~W{(7bX^)|bcvz&so}}vXnPp|D(PNcwQa}y)jYxe2S6fK1+{ps2Jd#6JQ z1b=hNf$AVTVgK6ImdnW8!BnGtGu?t%Fbbmt+t?R3*V41ja5k>6i8y2z`fqgi9@@7? zgqi1$c89d&VQpD;%f=PQMTHjbf?!31^0xNQA>)QG61$#iW*JT}hsH?~xAM_1H%d@a zcs6ZQXurTTY=$E8p%E;x&wBUKmIA9y+y}Stv;Q=c$shU4vHi2moE9qZ#K5vamG8l% zj(HU@d@;ZS+|M;-QBItr={48hm%mdUKA^E6Kd zyW28LSuID$wd@=2+ms@#VhzvHNP(}W-){8H8sSjowUj9A2-(>=X8|TpTms|ul0CK9 zdsj=D`{#BM%>QJXGA{G0AyX>_R4@qZCfLBs06S7d<6vi$Ji^juNLB6mQpa+bo>SJQbso!o2g9UzW3;kLAri^g@q(m%ngJP54qH z5OUH&)cu>DE*+{nuoADxnb2W^m`>*x_| zX`SW#dT%e7;_Mv7%d4&msNSb;H|?p?AvL4kntm~2k5$@RrS`=)#>~N!`Ug^{eEaZ2 ztwgp3supe&5#G;HAKZCMb|}N&E(9kqdOpl8tTbgQYy!Q)ZOe?K#;8y@i|Gc`UW3Of zCBxtcW&N$F@|vQimk{)1w%9?JaYvuJpt$}D@s%iM95P{8Ci@dxndY-Lp5;HrXju#j z(2pX}yDun`U8Qdj>of04q7h5;@vQeMit{@Q@F}^C{HHpT7GX*GT{1m2IOAKpT4#LM zCP0W!NTaoYcYU1npibu%rU)hcDcChpt)@1K6xqpX$jVWt0InnHu*&<#(YW#kBi>cy z+17%v%wWC!fn>l3oH9_`3(~a3& zMN~94)?=l||3fM7>`bruY zg47%%;KXX*H}%aDk_D!IIz(u_76f{^D;W8yx$pvZ+My+rm%uspt2HuC0Dy5zT*)xsl?7mygm+=%+8VtSZ zodFk-JP`TA3Vy5epzJQs>h(84PTr;nk?SpaRdJaZiTfj<0MISUwOH1dHR2>Wfzpbt z!72+1XK^iTnZk9Di!v(}U|%&D60sr^(?66tUg~zm!}kA>RHE z)pMU1t{QqRjLUGs?k_gEW-7`@$5bvN^8qEitR`=(U3&y@^cpn@7n`-$-c2y4c{cxr z-ElW;fGOe~EH4%f;*!+v*>{|4?X%G=UUdy*E0Y&_&k{iwbZ*@2QHVg!15Sj(v@Rjr zP`h0Iucg2xUW_?tXX@785EM#n6zy!?i>NmxKpc8Dki}Y9y2%E`4KSaFka@AS$3(ui zbD7d5d6QFAmBpi=ZzAo5Kc&9u22Pxj`-M+_GWR(Y%ToV3qMi|XQ{UF8)l=6bP~J+0 zD4%Ju*?)b$_k%%F!+SY9AMA^V%n0%1wgPI_R?*u5l^u5eq3dG%mPWINc`_ro39ji?Z_wUb~9rZl@LwnFBEdx@;LpKmk0GKGzhU_)n ziefXwiBkehjqDum(eo;~!;8HbLt^RGs--5*^Xw(;;PW5aPAmTJTy`Y%NQ>Xjg~y+Z zU@b|QCo~ciD(K};K>A~j*BnQ_+PXY?QvUqmJ2M2(**6Yjj<+$AOGmm#wFo)6n!#J~ zibcmp6s%l3$2a1KTvV*6&RQi2}^A)Kh&l?Ah2Tq{AIwh8VGw5+02fLH)%8b&3# zzmUj(B5q;F_yFvc#uVF`1bVwKKcs)r7>})<)~{0v&`N9H3yiLaQBuFCCa`?o)?giRsMUA<*y&U5DuZ0UvPuU(27MXjo+Qd?DPCPu3ysS&Fsir7I_RLzi5O0>4By+=f3>! zbCLesmeqWRQBhqU(AUy5^-JAMGeqzyvxjBV%3w(M?U^6HHqIh7=Tq@8WgRvgT&hoR z(7k;tQy#*d(R`oZZtgPojG2tQX_dqoZO3(he}O(X_;s+;bsHPr|9|K&r`keBjJ-niDIkh=m);of9`9lJC$Ycp{TI)_!w}yE`jsy2 zfZx>@{t5ea_^_3l_1{UliB06*(OHEbt>^tF{?G-r8G+TG?wQ78V-vqlyk33{qGn6e zysPDXZ${vK`{^0!h{t!2!n77lBlr8zt+Byonm<1;oz1%$$0D+pf&_9b-DBzaad|-ud6SPAN1v$$EGSdU1tyWhC-tgLZ5ir zB8@rv6?drt|N*%hM1rJ31-uDFy5ucQa zs79pVo$r4VA5bR+eNK^4w9`#O09yqcPoIrwhrA02U0)njIv<`j<&R^v>kNS<9D8{Q1hr$9%^e0xQ4vZ7TEr}D-&tLu(4KGUMuo5{V zi~Xt0lbc90kF}npl$E_^)!1j><|3GLc3Ig1PY$Yx);DC1O(#X#)+3kL$bw2MU#sAk zWOSa0bdJYKE&TLVv_?dRu5mjTfgi^rt(Yg;)Z)WHm+mZ)8jhUy7*nO1<5}%i;hFfr zX_|udjkTbDOuW&&All-K{eC)tUIJeugD_fnumyGA)7C z2u*(_#QNYiXRpLccaEyC)=eDI8vD@$Aw9Aldds00#(fmNPn01Z?8`FC@1Yfg`>7`3_6EA#;@!P zHJUy09ZR$%Mq$$1k$PrI*qE+}I$l8PM!&@Ofoedh=SnH6DH@Q+_a&bbs0?v(f-*h8 zc53kLc9X+y2#iB6`X7^6*&!~gCxWvYkesXQ)zdE>qup~i0t_4yuNiZ?_iYw3BtiNL zYj_@DyYmh-L2Jy)Q*ZNvECTzQ)3CDLlUW2l@U+sa({KsF2N=d}omf*ox7c-s!(Uc$ zAScr={FI~P#Kf|2+v`p@THeffcDpZ%USM9CQRY1H4(wsyo}XXZdY99|Zn&7tek2!{ zQ|{!Qc6twcr4r=)k!CC^Tu!~)!zRPj#U&0(6cvk-_wrLJCJPdlHE-^l-)#>*J;4Ku zrGcCBMV4BNqtQjtxpUzh zMQ+(rdbl*7ov&*-I!(3`Uw9rGpx@UCK$9mWHv~MdIuxIcwyj?r0*lyuI9FI6itBpdJGsZ!iLd!>ZJqfiW-lU~-M=EqbZXHP?}hKJ(Wa)P zEvi&*HxXaiUF{s@w#wsK=&{1A(HgXReN27##KOZ<+_2h2m6=4hqhjK3>gsam8sY@& z{4F^S4MV-6sHriJ-s!vps?Vv-_qGv?Dff&~8$#pMT3xm4`Ak)BKemWyDk%usqh!t8xB|bMcS(%=o&gN=8Ydt-#<1>53F|&E;dTFO~uzJGaMB7 zq@OoGt8X@8`Ru)vN0+LFQKq$~1n>uC-ZmM`Byi`nb2W1$_PU1A`0|I0_=f}DP zzjh690E@ZYoX;HS4EAaw)9xGR)m^Um8VVX-&dugK@~#bF(@{E^_263Nz@hzcegU_o zY{ZRL42;g9$Zd^JRz?{;LuuMO`xie+m&rS^x*79yUs75sB+(?zp#aEqS`^`(7kOMl zt2d0SVzZ^4safA(sn7BSBP?pEYh*b;YtBb7+q2~@aL>9TFicv3tU;s~Hl^DWL0VFx z^T!z?#}>tBvTBWBjBT=XC<`3dp4j% z?9-70U{E&+A1k&4OjwH2Ln4b9Vy0>%ss(mNm7FACTSP#w^jiRi-L2Rdhlad~{zx!0 zn&zFt=XSL9)h(CK*=@LEZ>_|~FYH5lf5X{i;-=g7>~8w!MvC%Pidks{Zo%oq)!Uw z_L=-(RSg<8NTba*ETCWYywRAG?;xumGukFs@_P@3Uz&eZ<;QPwDrv?gSt;kC4k?wf zbmi}sW)asJ11t7m2$!$q#an2rRoUl&u^taDcfA>51?tUGL!E`>P1Pc*T8V*56Dz zZ}`sA#yf%t^cws72LaG;IWN`A71(IV7-rQJu4-r29f`w0LzlL&I=d83S(saTyzJJG zN^`&>rq;K?q`DbhGsn$aF`6cn~uoxfXD zqfk_hKa7gluw}j_WQREyyw3dA{)T~9 z>T}S;`}Hbwdo7)h+$IkS@79T32!wAY75t$|(!%s>iW5;5rRcmO}OR9VLTQybI&g`QVe6pPZ;Avgm1V`c1^0>L<$Ry}#EkwF(Cg7sMBv- z`;Q0-{MI#;Pu@H{Vj35qmd;V+wTG87-&3Vt_h(RqF!(N00-jtttgSPWV5!Ze3<zs;uBj?=pyqa)KU5D(X{${l~P?^WNym@ zKhLA&gGva`m${$pK`aIS(|u|%JpYuxM9AEqkL=2Hcl+@0stBkipVDf?X+DeM9EaAu z!i?6#2xIM~b2Df-E>37$pvGQLz@fn{#Z^a4{a9hJ$Bm5?eAOIXxEhD6x-4>7M2=X+ z$Lk4JWhopzJqrJXQG1aU?7DqBxNy256(JF6e|qYcqZoONL`Yi42)@AsKUmR3%)?#WJr%Vl>2@VF{a#56ieGU3!=EAsFXTo(V$Qn>s!z+cjuw_pIUvn9Ag1`oXzc#@}{h(H_`g%~dEW+Wpzn(OB0(V;BQ( zlmJk9_(vGh5Y*~#MLpQh1|*jouX{0Ot0|T855RK>U(->gA|Q+J6XHg-Wc%Iyuy>P* zfZ&4|&Bh3>J#nWypFOd(&bSk6gudANOJ4zkjMTnX_H+#;^DS#>>66aIOz&IgjOTZH zBds(YRX#eKhY*95xdTd?syB#ny*E))n49VlHHNH@x`Rl3hmR08*2r6U%*$>2-TRJ@ zV`xfXgcqco&9&Mz4OPW#6=>u~a29F9K75O*hCee!RgH8)0^iIt7H8Y96VR%1XL2*w zl-~wB&(9EFo+s1=1y;WmbMh6Fld>YPK1)hNe#TD|S>R~PThIqm3cck`C8_Tlr!1|z z-SDCkC?DGVa>#@r2NXG1&`AZS3hS-XVWx+sgozu#)nDFwanxe(TeLhypSGcpg~Hb? zF)ipgzB`9hMsSudPjZr@f^UigP&Yxrt1%g23%UKz7%m!F1pqkFFwCcXx-!ht2vr%5 zfc%+Z$%wv-kt>zB*8X<+duH3|01kUQYjlJ7vrJdbyR_Y5GUdGt#px-*IMvYy;23xO zk8p*2sX$2H1CzLXz!G6brPI+IwI1ZCVWHF|w6rzvm*%H(4Dg(;`A2KsZr`X?fX|c7 zJv!psaC}@wQlu`!8rr7?&B6JnL^?}Q{W=PvTJEH=@YMaau$cVTw(ZV`#3lL5um&8K zmFMT{Jyv2#&-R@Y$<)0+{_alvC!@czD<@C-hCCWSv>iEp1I^QOj^H;MF076kF_S zvJ?~x{rP)ZHE7bM&7QG=u>(Pp@n&Z!J)Mf2OrVlp*cZjebbSwM+e1h`|6IysPd|B~ zf!cJ*Kw$9*(vN9+J+>%8I+<)4B&PVfqU87$9c)?@@eB|l)qh(j0OJFPJybRnurO;I z@FXV2>U8}`$xrzLi8!7a<@)GEm7h3~90XtO&q%`-W2vGxT}Rm@av*$(r4sGkpc9fY zN8CBPC*EYYI4^R=TG8Z%m?vhNlzuLA|JPqi=cx|@AD@x2aCtSVmok-?0Q+c6If(>E zgRmd!Uq|n+edq`*lYcY9XA>&blOgksBHx*->;V&ypsamJ?l1Z_{xS=E#Y5J{24nfgn0~!Y1)m%%W}*8D7er<>WIPuYMDhiaj~3-f;Q=JES7{ zMBe3$Xo>!M2B;$4dACZB;XyOIy@J&ZA(g^hJ6gM=l5cV zi{{PL6w7N)bdX}dr?;Hjd^K91R#2wpK-}nsWS_l1(!!Q@ReG4UnBF)j;?sOAL`h+J zj8;})o_FwlITRUxpop(Law+{?FFVevWzjMVVKYghESA=rgg8`^Wz2_gy1hPXs2bT3 z4OD4yk4KJX#0qmKZa64+g<8J-DWGEe==$ls^_j=C$lWAg69KmW51IHo#I5w}iCjgm ztnxFJ)J9g#yD@p0mJPyZ7QG&d0U{1B?5!{lDTy1;y7pdV&fOXQ>vRLYC@-7n|9f`OiyV z9;HijObiqhFxYbRJGS!ax}v9>WXE{!{$mFA!PicUiUm}1v2p~ZZ~qqvt@!QNpl0~c zirYtqZT+hJ_SV4~!i|xk-+VP_KcLTuPVxn=%7SF3=xy@{-9sYG9sa8I7DOj>!6Y({ zW**5L^M_JYKh=J$Pt7me4BE;jfYDX;OhrP$qwCfb=(25YvwGgFNMR5B2_1hohG}%f zFZK5wbv1o8O#0yVUOSXHk3HcL5Q;JkTnURYv`4BMHi8c&C)Re^J_MNFdKlFa-^%>q z-@HI#MulH|#oxfE)+`&?}5DIMpYPDo}Lc*|x0RBcc~ zc3?jU_rt$G3w;kxr>g@iRxtCnu}z}qL_hnN}z z1uB$P?4cQ&41{$41IkYP1QooHa)LRfA^4F2Bj;g~>_U;tK)2iR4Xm}JtuDPE8dh zGIyhvMXT&HYY%DgVir0p(LIwJr)21FM4~>oPHqQ#d&%3aZ~{97Ij={xSd}RgedNa?8P#v zTBv3+toPCAUE&d8x6<&(Zw;g~+=cb|)V>COmRwM*C%Pr#Jgt%^Iy!kw+#qvDO8{T2 zb3{+eXoIW-({6$|PQ2TGO2XU6hVZr^Q6+(blY9tb|Sn2{VY T4`BR$9(XR;I6$kdkoW!vCi?}6 literal 0 HcmV?d00001 From 72edbf964e9587933e72572e9a5da64118fc290e Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Wed, 17 Jan 2024 11:29:38 +0000 Subject: [PATCH 19/48] Improve terrain lighting calculation --- apps/synthwave/app.js | 82 ++++++++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/apps/synthwave/app.js b/apps/synthwave/app.js index ac277b739..8ac8d259b 100644 --- a/apps/synthwave/app.js +++ b/apps/synthwave/app.js @@ -325,6 +325,8 @@ void drawTerrain() { int dist[] = { solid(7), + solid(7), + alternate(5, 7), alternate(5, 7), solid(5), solid(5), @@ -333,39 +335,63 @@ void drawTerrain() { }; int line = solid(5); - int fovz; - int prvz = ((90 << 16) / ((90 << 8) + ((terrainLength) * tileSize - camera.z))); // 16:16 / 16:8 = 16:8 - for (int i = 0; i < terrainLength - 1; ++i, prvz = fovz) { - fovz = ((90 << 16) / ((90 << 8) + ((terrainLength - (i + 1)) * tileSize - camera.z))); // 16:16 / 16:8 = 16:8 - int lum = i < 3 ? i - 3 : 3; + int fovz, fz; + int pz = (terrainLength) * tileSize - camera.z; + int prvz = ((90 << 16) / ((90 << 8) + pz)); // 16:16 / 16:8 = 16:8 + for (int i = 0; i < terrainLength - 1; ++i, prvz = fovz, pz = fz) { + fz = (terrainLength - (i + 1)) * tileSize - camera.z; + fovz = ((90 << 16) / ((90 << 8) + fz)); // 16:16 / 16:8 = 16:8 + int lum = i < 7 ? i : 7; for (int x = 0; x < terrainWidth - 1; ++x) { - int ax = (((((x ) * tileSize - camera.x) >> 8) * prvz) >> 8) + (176/2); // int * 16:8 = 16:8 -> int - int bx = (((((x + 1) * tileSize - camera.x) >> 8) * prvz) >> 8) + (176/2); // int * 16:8 = 16:8 -> int - int cx = (((((x ) * tileSize - camera.x) >> 8) * fovz) >> 8) + (176/2); // int * 16:8 = 16:8 -> int - int dx = (((((x + 1) * tileSize - camera.x) >> 8) * fovz) >> 8) + (176/2); // int * 16:8 = 16:8 -> int + int ax = ((x ) * tileSize - camera.x) >> 8; + int bx = ((x + 1) * tileSize - camera.x) >> 8; + int cx = ((x ) * tileSize - camera.x) >> 8; + int dx = ((x + 1) * tileSize - camera.x) >> 8; - int ay = (176/2) - ((((terrain[i ][x ] << 8) - camera.y) >> 8) * prvz >> 8); - int by = (176/2) - ((((terrain[i ][x + 1] << 8) - camera.y) >> 8) * prvz >> 8); - int cy = (176/2) - ((((terrain[i + 1][x ] << 8) - camera.y) >> 8) * fovz >> 8); - int dy = (176/2) - ((((terrain[i + 1][x + 1] << 8) - camera.y) >> 8) * fovz >> 8); + int ay = ((terrain[i ][x ] << 8) - camera.y) >> 8; + int by = ((terrain[i ][x + 1] << 8) - camera.y) >> 8; + int cy = ((terrain[i + 1][x ] << 8) - camera.y) >> 8; + int dy = ((terrain[i + 1][x + 1] << 8) - camera.y) >> 8; - int na = (ax - bx)*(ay - cy) - (ay - by)*(ax - cx); - if (na > 0) { - int c = (lum + (na >> 8)); - if (c < 0) c = 0; - else if (c > 5) c = 5; - c = dist[c]; - fillTriangle(ax, ay, bx, by, cx, cy, c); + int na = ((ax - bx)*(ay - cy) - (ay - by)*(ax - cx)) >> 8; + int nb = ((bx - dx)*(by - cy) - (by - dy)*(bx - cx)) >> 8; + int ca = lum - na; + int cb = lum - nb; + + ax = 88 + (ax * prvz >> 8); + bx = 88 + (bx * prvz >> 8); + cx = 88 + (cx * fovz >> 8); + dx = 88 + (dx * fovz >> 8); + ay = 88 - (ay * prvz >> 8); + by = 88 - (by * prvz >> 8); + cy = 88 - (cy * fovz >> 8); + dy = 88 - (dy * fovz >> 8); + + int av = (ax - bx)*(ay - cy) - (ay - by)*(ax - cx); + int bv = (bx - dx)*(by - cy) - (by - dy)*(bx - cx); + + if (av > 0) { + if (ca < 0) ca = 0; + else if (ca >= 7) ca = 7; + if (ca >= 6 && x >= terrainWidth/2 && x < terrainWidth/2+2) ca = 6; + ca = dist[ca]; + fillTriangle(ax, ay, bx, by, cx, cy, ca); } - na = (bx - dx)*(by - cy) - (by - dy)*(bx - cx); - if (na > 0) { - int c = (lum + (na >> 8)); - if (c < 0) c = 0; - else if (c > 5) c = 5; - c = dist[c]; - fillTriangle(bx, by, cx, cy, dx, dy, c); - if (!c) { + if (bv > 0) { + int hasLine = false; + if (cb < 0) cb = 0; + else if (cb >= 7) { + cb = 7; + hasLine = true; + } + if (cb >= 6 && x >= terrainWidth/2 && x < terrainWidth/2+2) { + cb = 6; + hasLine = true; + } + cb = dist[cb]; + fillTriangle(bx, by, cx, cy, dx, dy, cb); + if (hasLine) { fillTriangle(ax, ay, bx, by, bx, by - 1, line); fillTriangle(ax, ay, cx, cy, cx, cy - 1, line); } From f450062cfb322f9287c87e7862ec866a9d38ba79 Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Wed, 17 Jan 2024 11:32:14 +0000 Subject: [PATCH 20/48] Use 93dub font with no separator --- apps/synthwave/app.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/synthwave/app.js b/apps/synthwave/app.js index 8ac8d259b..5365ff52e 100644 --- a/apps/synthwave/app.js +++ b/apps/synthwave/app.js @@ -613,6 +613,9 @@ void tick(int c) { `); +// font from 93dub +var fontNum = atob("AAAAAAAAAAAAAA//8D//g//8P/+I//8//44//w//j4//A/+P4/8A/4/4AAAAD/4AAAAP/wAAAAf/gAAAA//AAAAB/+AAAAD/8AAAAH/4AAAAP/wAAAAf/gAAAA//AAAAB/+AAAAD/8AAAAH/wAAAAH/H/gH/H8f/gf/Hx//h//HH//n//Ef/+H//B//4H//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wB/4AP/4H/4A//4f/4D//5//4P//h//4//+B//4AAAAAAAAAAAAAAAAAf/+AAAB//4gAAD//jgAAD/+PgABj/4/gAHj/j/gAfgAP/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/AA/AAf8f88AAfx/8wAAfH/8AAAcf/8AAAR//4AAAH//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAA4AAAAAD4AAYAAP4AD8AA/4AH4AD/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/wAHgAH/H/GH/H8f/gf/Hx//h//HH//n//Ef/+H//B//4H//AAAAAAAAAAAAAAP//AAAAP//AAAAP//AAAAP/8AAAAP/2AAAAP/eAAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB+AAAAAD8AAAB/7x/4AH/7H/4Af/4f/4B//5//4H//h//4f/+B//4AAAAAAAAAAAAAD//wAAAD//wAAAj//gAADj/+AAAPj/5gAA/j/ngAD/gAfgAP/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/AA/AAf8AA8f8fwAAx/8fAAAH/8cAAAf/8QAAA//8AAAA//8AAAAAAAAAAAAAA//8D//g//8P/+I//8//44//0//j4//Y/+P4/94/4/4AH4AD/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/wAPwAH/AAPH/H8AAMf/HwAAB//HAAAH//EAAAH//AAAAH//AAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAGAAAAAAOAAAAAAeAAAAAA+AAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB8AAAAADx/4B/4HH/4H/4Mf/4f/4R//5//4H//h//4f/+B//4AAAAAAAAAAAAAD//wP/+D//w//4j//z//jj//T/+Pj/9j/4/j/3j/j/gAfgAP/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/AA/AAf8f+8f8fx/+x/8fH/+H/8cf/+f/8R//4f/8H//gf/8AAAAAAAAAAAAAA//8AAAA//8AAAI//8AAA4//0AAD4//YAAP4/94AA/4AH4AD/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/wAPwAH/H/vH/H8f/sf/Hx//h//HH//n//Ef/+H//B//4H//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + const sintable = new Uint8Array(256); let bgColor = 0; const BLACK = g.setColor.bind(g, 0); @@ -723,13 +726,11 @@ function tick(widgets) { } var d = new Date(); - var h = d.getHours(), - m = d.getMinutes(); - var time = (" " + h).substr(-2) + ":" + m.toString().padStart(2, 0); - g.setColor(g.theme.fg) - .setFontAlign(0, 0.5) - .setFont('5x9Numeric7Seg', 5) - .drawString(time, 176 / 2, 50, true); + var h = d.getHours(), m = d.getMinutes(); + g.setColor(widgets ? g.theme.fg : g.toColor(1,0,1)) + .setFontCustom(fontNum, 48, 28, 41) + .setFontAlign(-1, -1) + .drawString(("0" + h).substr(-2) + ("0"+m).substr(-2), 30, 30, true); if (widgets) Bangle.drawWidgets(); From 189e1888bf86656945dc8922a6752ea46d772069 Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Wed, 17 Jan 2024 11:41:16 +0000 Subject: [PATCH 21/48] Update Synthwave screenshots --- apps/synthwave/screenshot.png | Bin 3006 -> 3921 bytes apps/synthwave/theme.png | Bin 3082 -> 3304 bytes apps/synthwave/widgets.png | Bin 3171 -> 4170 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/apps/synthwave/screenshot.png b/apps/synthwave/screenshot.png index 60e3c18aade61b86ffd3d28cd569c20da0f301dd..e8697bd0a1710afaf25e9a582f75550f5b308682 100644 GIT binary patch literal 3921 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 VIrAUU`}-;b40KHHR%kgq{SQCYh>idN literal 3006 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 IiTjiP0Lb4~`v3p{ literal 3082 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 DcU>iA literal 3171 zcmZ8kc{CJ?_s3*s7?oXWgdW~R_O&oFH7Fw6(3nD!!HhjIld+7oXBqM)qK)i@89QSn zW0x&WVHC+S4aqXexA*(wcg}l%+sMBv- z`;Q0-{MI#;Pu@H{Vj35qmd;V+wTG87-&3Vt_h(RqF!(N00-jtttgSPWV5!Ze3<zs;uBj?=pyqa)KU5D(X{${l~P?^WNym@ zKhLA&gGva`m${$pK`aIS(|u|%JpYuxM9AEqkL=2Hcl+@0stBkipVDf?X+DeM9EaAu z!i?6#2xIM~b2Df-E>37$pvGQLz@fn{#Z^a4{a9hJ$Bm5?eAOIXxEhD6x-4>7M2=X+ z$Lk4JWhopzJqrJXQG1aU?7DqBxNy256(JF6e|qYcqZoONL`Yi42)@AsKUmR3%)?#WJr%Vl>2@VF{a#56ieGU3!=EAsFXTo(V$Qn>s!z+cjuw_pIUvn9Ag1`oXzc#@}{h(H_`g%~dEW+Wpzn(OB0(V;BQ( zlmJk9_(vGh5Y*~#MLpQh1|*jouX{0Ot0|T855RK>U(->gA|Q+J6XHg-Wc%Iyuy>P* zfZ&4|&Bh3>J#nWypFOd(&bSk6gudANOJ4zkjMTnX_H+#;^DS#>>66aIOz&IgjOTZH zBds(YRX#eKhY*95xdTd?syB#ny*E))n49VlHHNH@x`Rl3hmR08*2r6U%*$>2-TRJ@ zV`xfXgcqco&9&Mz4OPW#6=>u~a29F9K75O*hCee!RgH8)0^iIt7H8Y96VR%1XL2*w zl-~wB&(9EFo+s1=1y;WmbMh6Fld>YPK1)hNe#TD|S>R~PThIqm3cck`C8_Tlr!1|z z-SDCkC?DGVa>#@r2NXG1&`AZS3hS-XVWx+sgozu#)nDFwanxe(TeLhypSGcpg~Hb? zF)ipgzB`9hMsSudPjZr@f^UigP&Yxrt1%g23%UKz7%m!F1pqkFFwCcXx-!ht2vr%5 zfc%+Z$%wv-kt>zB*8X<+duH3|01kUQYjlJ7vrJdbyR_Y5GUdGt#px-*IMvYy;23xO zk8p*2sX$2H1CzLXz!G6brPI+IwI1ZCVWHF|w6rzvm*%H(4Dg(;`A2KsZr`X?fX|c7 zJv!psaC}@wQlu`!8rr7?&B6JnL^?}Q{W=PvTJEH=@YMaau$cVTw(ZV`#3lL5um&8K zmFMT{Jyv2#&-R@Y$<)0+{_alvC!@czD<@C-hCCWSv>iEp1I^QOj^H;MF076kF_S zvJ?~x{rP)ZHE7bM&7QG=u>(Pp@n&Z!J)Mf2OrVlp*cZjebbSwM+e1h`|6IysPd|B~ zf!cJ*Kw$9*(vN9+J+>%8I+<)4B&PVfqU87$9c)?@@eB|l)qh(j0OJFPJybRnurO;I z@FXV2>U8}`$xrzLi8!7a<@)GEm7h3~90XtO&q%`-W2vGxT}Rm@av*$(r4sGkpc9fY zN8CBPC*EYYI4^R=TG8Z%m?vhNlzuLA|JPqi=cx|@AD@x2aCtSVmok-?0Q+c6If(>E zgRmd!Uq|n+edq`*lYcY9XA>&blOgksBHx*->;V&ypsamJ?l1Z_{xS=E#Y5J{24nfgn0~!Y1)m%%W}*8D7er<>WIPuYMDhiaj~3-f;Q=JES7{ zMBe3$Xo>!M2B;$4dACZB;XyOIy@J&ZA(g^hJ6gM=l5cV zi{{PL6w7N)bdX}dr?;Hjd^K91R#2wpK-}nsWS_l1(!!Q@ReG4UnBF)j;?sOAL`h+J zj8;})o_FwlITRUxpop(Law+{?FFVevWzjMVVKYghESA=rgg8`^Wz2_gy1hPXs2bT3 z4OD4yk4KJX#0qmKZa64+g<8J-DWGEe==$ls^_j=C$lWAg69KmW51IHo#I5w}iCjgm ztnxFJ)J9g#yD@p0mJPyZ7QG&d0U{1B?5!{lDTy1;y7pdV&fOXQ>vRLYC@-7n|9f`OiyV z9;HijObiqhFxYbRJGS!ax}v9>WXE{!{$mFA!PicUiUm}1v2p~ZZ~qqvt@!QNpl0~c zirYtqZT+hJ_SV4~!i|xk-+VP_KcLTuPVxn=%7SF3=xy@{-9sYG9sa8I7DOj>!6Y({ zW**5L^M_JYKh=J$Pt7me4BE;jfYDX;OhrP$qwCfb=(25YvwGgFNMR5B2_1hohG}%f zFZK5wbv1o8O#0yVUOSXHk3HcL5Q;JkTnURYv`4BMHi8c&C)Re^J_MNFdKlFa-^%>q z-@HI#MulH|#oxfE)+`&?}5DIMpYPDo}Lc*|x0RBcc~ zc3?jU_rt$G3w;kxr>g@iRxtCnu}z}qL_hnN}z z1uB$P?4cQ&41{$41IkYP1QooHa)LRfA^4F2Bj;g~>_U;tK)2iR4Xm}JtuDPE8dh zGIyhvMXT&HYY%DgVir0p(LIwJr)21FM4~>oPHqQ#d&%3aZ~{97Ij={xSd}RgedNa?8P#v zTBv3+toPCAUE&d8x6<&(Zw;g~+=cb|)V>COmRwM*C%Pr#Jgt%^Iy!kw+#qvDO8{T2 zb3{+eXoIW-({6$|PQ2TGO2XU6hVZr^Q6+(blY9tb|Sn2{VY T4`BR$9(XR;I6$kdkoW!vCi?}6 From ea112f26a8c204ff4edb2da0df8240d232070917 Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Wed, 17 Jan 2024 14:24:19 +0000 Subject: [PATCH 22/48] Simplify terrain generation --- apps/synthwave/app.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/synthwave/app.js b/apps/synthwave/app.js index 5365ff52e..9edd2abff 100644 --- a/apps/synthwave/app.js +++ b/apps/synthwave/app.js @@ -48,10 +48,12 @@ void shiftTerrain() { for (int x = 0; x < terrainWidth; ++x) terrain[0][x] = (rng() & 0x3F) + 0xF; - for (int x = 0; x < (terrainWidth >> 3); ++x) - terrain[0][((terrainWidth>>1)-(terrainWidth>>4)) + x] >>= 1; - for (int x = 0; x < (terrainWidth >> 2); ++x) - terrain[0][((terrainWidth>>1)-(terrainWidth>>5)) + x] = 0; + int mid = terrainWidth >> 1; + terrain[0][mid-1] >>= 1; + terrain[0][mid ] = 0; + terrain[0][mid+1] = 0; + terrain[0][mid+2] = 0; + terrain[0][mid+3] >>= 1; } void init(unsigned char* _fb, int _stride, unsigned char* _sint) { From 16c08d0cf38bc12658dfe87b5c53abf9e2b68472 Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Wed, 17 Jan 2024 14:30:43 +0000 Subject: [PATCH 23/48] Generate larger/smoother hills --- apps/synthwave/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/synthwave/app.js b/apps/synthwave/app.js index 9edd2abff..a04300f44 100644 --- a/apps/synthwave/app.js +++ b/apps/synthwave/app.js @@ -47,7 +47,7 @@ void shiftTerrain() { } for (int x = 0; x < terrainWidth; ++x) - terrain[0][x] = (rng() & 0x3F) + 0xF; + terrain[0][x] = (int(terrain[0][x]) + ((rng() & 0x7F) + 0x7)) >> 1; int mid = terrainWidth >> 1; terrain[0][mid-1] >>= 1; terrain[0][mid ] = 0; From 2304572e7f1f8fe725f5f2b37b28e403839b6786 Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Wed, 17 Jan 2024 20:31:53 +0000 Subject: [PATCH 24/48] Add support for FW2v21 lcdBufferPtr --- apps/synthwave/app.js | 7 +++---- apps/warpdrive/app.js | 4 +++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/synthwave/app.js b/apps/synthwave/app.js index a04300f44..891008df0 100644 --- a/apps/synthwave/app.js +++ b/apps/synthwave/app.js @@ -29,8 +29,6 @@ const unsigned char ship[] = { const unsigned int terrainLength = 12; const unsigned int terrainWidth = 12; unsigned char terrain[terrainLength][terrainWidth]; -unsigned int travel = 0; - unsigned int _rngState; unsigned int rng() { _rngState ^= _rngState << 17; @@ -40,7 +38,6 @@ unsigned int rng() { } void shiftTerrain() { - travel++; for (int i = terrainLength - 1; i > 0; --i) { for (int x = 0; x < terrainWidth; ++x) terrain[i][x] = terrain[i-1][x]; @@ -651,7 +648,9 @@ function test(addr, y) { function probe() { if (!start) { start = 0x20000000; - if (test(0x2002d3fe, 0)) // try to skip loading if possible + if (test(Bangle.getOptions().lcdBufferPtr, 0)) + start = Bangle.getOptions().lcdBufferPtr; // FW=2v21 + else if (test(0x2002d3fe, 0)) // try to skip loading if possible start = 0x2002d3fe; // FW=2v20 } const end = Math.min(start + 0x800, 0x20038000); diff --git a/apps/warpdrive/app.js b/apps/warpdrive/app.js index cf7d40b6e..46b998379 100644 --- a/apps/warpdrive/app.js +++ b/apps/warpdrive/app.js @@ -519,7 +519,9 @@ function test(addr, y) { function probe() { if (!start) { start = 0x20000000; - if (test(0x2002d3fe, 0)) // try to skip loading if possible + if (test(Bangle.getOptions().lcdBufferPtr, 0)) + start = Bangle.getOptions().lcdBufferPtr; // FW=2v21 + else if (test(0x2002d3fe, 0)) // try to skip loading if possible start = 0x2002d3fe; // FW=2v20 } const end = Math.min(start + 0x800, 0x20038000); From 3bfe4448fb16a59d4fe617eb6c7cfd38527b735f Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Thu, 18 Jan 2024 15:23:57 +0000 Subject: [PATCH 25/48] Ensure text uses theme background color --- apps/synthwave/app.js | 7 ++++--- apps/warpdrive/app.js | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/synthwave/app.js b/apps/synthwave/app.js index 891008df0..6e99865a7 100644 --- a/apps/synthwave/app.js +++ b/apps/synthwave/app.js @@ -729,9 +729,10 @@ function tick(widgets) { var d = new Date(); var h = d.getHours(), m = d.getMinutes(); g.setColor(widgets ? g.theme.fg : g.toColor(1,0,1)) - .setFontCustom(fontNum, 48, 28, 41) - .setFontAlign(-1, -1) - .drawString(("0" + h).substr(-2) + ("0"+m).substr(-2), 30, 30, true); + .setBgColor(g.theme.bg) + .setFontCustom(fontNum, 48, 28, 41) + .setFontAlign(-1, -1) + .drawString(("0" + h).substr(-2) + ("0"+m).substr(-2), 30, 30, true); if (widgets) Bangle.drawWidgets(); diff --git a/apps/warpdrive/app.js b/apps/warpdrive/app.js index 46b998379..4f55db43d 100644 --- a/apps/warpdrive/app.js +++ b/apps/warpdrive/app.js @@ -671,6 +671,7 @@ function tick(widgets) { m = d.getMinutes(); var time = (" " + h).substr(-2) + ":" + m.toString().padStart(2, 0); g.setColor(g.theme.fg) + .setBgColor(g.theme.bg) .setFontAlign(0, 0.5) .setFont('6x8', 2) .drawString(time, 176 / 2, 176 - 16, true); From 417b434cf182e65d7a6aa2b0ffd05f9eb381c09c Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Thu, 18 Jan 2024 19:09:53 +0000 Subject: [PATCH 26/48] Ensure graphics is in a known state when drawing --- apps/synthwave/app.js | 2 ++ apps/warpdrive/app.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/apps/synthwave/app.js b/apps/synthwave/app.js index 6e99865a7..cbc6b8794 100644 --- a/apps/synthwave/app.js +++ b/apps/synthwave/app.js @@ -721,6 +721,8 @@ function init() { } function tick(widgets) { + g.reset(); + if (lcdBuffer && !widgets) { BLACK().drawRect(-1, -1, 0, 177); // dirty all the rows gfx.tick(bgColor); diff --git a/apps/warpdrive/app.js b/apps/warpdrive/app.js index 4f55db43d..ad9b88223 100644 --- a/apps/warpdrive/app.js +++ b/apps/warpdrive/app.js @@ -656,6 +656,8 @@ function drawNode(index) { } function tick(widgets) { + g.reset(); + if (lcdBuffer && !widgets) { BLACK().drawRect(-1, -1, 0, 177); // dirty all the rows gfx.clear(bgColor); From 58e8c032c7260b59f66e46452e775518ca0749a9 Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Fri, 19 Jan 2024 11:48:21 +0000 Subject: [PATCH 27/48] Call widget_utils' show/hide instead of Bangle.drawWidgets() --- apps/synthwave/app.js | 33 +++++++++++++++++++-------------- apps/warpdrive/app.js | 33 +++++++++++++++++++-------------- 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/apps/synthwave/app.js b/apps/synthwave/app.js index cbc6b8794..85fbfe7ad 100644 --- a/apps/synthwave/app.js +++ b/apps/synthwave/app.js @@ -623,16 +623,23 @@ let lcdBuffer = 0, start = 0; let locked = false, - charging = false; -var interval = 30; -var timeout; +let charging = false; +let stopped = true; +let interval = 30; +let timeout; function setupInterval(force) { if (timeout) clearTimeout(timeout); - let stopped = locked && !charging; - timeout = setTimeout(setupInterval, stopped ? 60000 : 60); - tick(stopped && !force); + let stop = locked && !charging; + timeout = setTimeout(setupInterval, stop ? 60000 : 60); + tick(stop && !force); + if (stop != stopped) { + stopped = stop; + let widget_utils = require("widget_utils"); + if (stop) widget_utils.show(); + else if (widget_utils.hide) widget_utils.hide(); + } } function test(addr, y) { @@ -720,30 +727,26 @@ function init() { setTimeout(probe, 1); } -function tick(widgets) { +function tick(locked) { g.reset(); - if (lcdBuffer && !widgets) { + if (lcdBuffer && !locked) { BLACK().drawRect(-1, -1, 0, 177); // dirty all the rows gfx.tick(bgColor); } var d = new Date(); var h = d.getHours(), m = d.getMinutes(); - g.setColor(widgets ? g.theme.fg : g.toColor(1,0,1)) + g.setColor(locked ? g.theme.fg : g.toColor(1,0,1)) .setBgColor(g.theme.bg) .setFontCustom(fontNum, 48, 28, 41) .setFontAlign(-1, -1) .drawString(("0" + h).substr(-2) + ("0"+m).substr(-2), 30, 30, true); - - if (widgets) - Bangle.drawWidgets(); } -init(); - Bangle.setUI("clock"); Bangle.loadWidgets(); + Bangle.on("lock", l => { locked = l; setupInterval(); @@ -753,3 +756,5 @@ Bangle.on('charging', c => { charging = c; setupInterval(); }); + +init(); diff --git a/apps/warpdrive/app.js b/apps/warpdrive/app.js index ad9b88223..855b86c78 100644 --- a/apps/warpdrive/app.js +++ b/apps/warpdrive/app.js @@ -493,17 +493,24 @@ const WHITE = g.setColor.bind(g, 0xFFFF); let lcdBuffer = 0, start = 0; -let locked = false, - charging = false; -var interval = 30, - timeout; +let locked = false; +let charging = false; +let stopped = true; +let interval = 30; +let timeout; function setupInterval(force) { if (timeout) clearTimeout(timeout); - let stopped = locked && !charging; - timeout = setTimeout(setupInterval, stopped ? 60000 : 30); - tick(stopped && !force); + let stop = locked && !charging; + timeout = setTimeout(setupInterval, stop ? 60000 : 60); + tick(stop && !force); + if (stop != stopped) { + stopped = stop; + let widget_utils = require("widget_utils"); + if (stop) widget_utils.show(); + else if (widget_utils.hide) widget_utils.hide(); + } } function test(addr, y) { @@ -655,10 +662,10 @@ function drawNode(index) { gfx.render(E.getAddressOf(translation, true)); } -function tick(widgets) { +function tick(locked) { g.reset(); - if (lcdBuffer && !widgets) { + if (lcdBuffer && !locked) { BLACK().drawRect(-1, -1, 0, 177); // dirty all the rows gfx.clear(bgColor); gfx.stars(); @@ -677,15 +684,11 @@ function tick(widgets) { .setFontAlign(0, 0.5) .setFont('6x8', 2) .drawString(time, 176 / 2, 176 - 16, true); - - if (widgets) - Bangle.drawWidgets(); } -init(); - Bangle.setUI("clock"); Bangle.loadWidgets(); + Bangle.on("lock", l => { locked = l; setupInterval(); @@ -695,3 +698,5 @@ Bangle.on('charging', c => { charging = c; setupInterval(); }); + +init(); From c4aebc6a8c8a017cb868ba6023027ac36f9723b7 Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Fri, 19 Jan 2024 11:54:13 +0000 Subject: [PATCH 28/48] Fix typo --- apps/synthwave/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/synthwave/app.js b/apps/synthwave/app.js index 85fbfe7ad..61a0a1e79 100644 --- a/apps/synthwave/app.js +++ b/apps/synthwave/app.js @@ -622,7 +622,7 @@ const WHITE = g.setColor.bind(g, 0xFFFF); let lcdBuffer = 0, start = 0; -let locked = false, +let locked = false; let charging = false; let stopped = true; let interval = 30; From 646b24e090841a90ad37e88722590f08aa05f417 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 29 Jan 2024 22:27:11 +0000 Subject: [PATCH 29/48] Improve boolean formatter check for showMenu() calls See also #3149 --- bin/sanitycheck.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index 4e6662e4a..78cadc34c 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -263,7 +263,7 @@ apps.forEach((app,appIdx) => { WARN(`App ${app.id} has a setting file but no corresponding data entry (add \`"data":[{"name":"${app.id}.settings.json"}]\`)`, {file:appDirRelative+file.url}); } // check for manual boolean formatter - const m = fileContents.match(/format: *\(\) *=>.*["'](yes|on)["']/i); + const m = fileContents.match(/format: *\(?\w*\)? *=>.*["'](yes|on)["']/i); if (m) { WARN(`Settings for ${app.id} has a boolean formatter - this is handled automatically, the line can be removed`, {file:appDirRelative+file.url, line: fileContents.substr(0, m.index).split("\n").length}); } From e17a24e48f335b1fb51e65985efa3afb24974edc Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Mon, 29 Jan 2024 22:27:24 +0000 Subject: [PATCH 30/48] Drop more unnecessary format arguments --- apps/mywelcome/settings.js | 1 - apps/shadowclk/settings.js | 2 -- 2 files changed, 3 deletions(-) diff --git a/apps/mywelcome/settings.js b/apps/mywelcome/settings.js index cf7208d65..e8d294cd4 100644 --- a/apps/mywelcome/settings.js +++ b/apps/mywelcome/settings.js @@ -5,7 +5,6 @@ '': { 'title': 'Welcome App' }, 'Run next boot': { value: !settings.welcomed, - format: v => v ? 'Yes' : 'No', onchange: v => require('Storage').write('mywelcome.json', {welcomed: !v}), }, 'Run Now': () => load('mywelcome.app.js'), diff --git a/apps/shadowclk/settings.js b/apps/shadowclk/settings.js index 3fb774892..1472cb099 100644 --- a/apps/shadowclk/settings.js +++ b/apps/shadowclk/settings.js @@ -130,7 +130,6 @@ }, 'Date Suffix:': { value: appSettings.enableSuffix, - format: v => v ? 'Yes' : 'No', onchange: v => { appSettings.enableSuffix = v; writeSettings(); @@ -138,7 +137,6 @@ }, 'Lead Zero:': { value: appSettings.enableLeadingZero, - format: v => v ? 'Yes' : 'No', onchange: v => { appSettings.enableLeadingZero = v; writeSettings(); From 2c96f33f00a7cedf23b940c7e604355988c1c32f Mon Sep 17 00:00:00 2001 From: Logan B <3870583+thinkpoop@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:39:56 -0600 Subject: [PATCH 31/48] android: Fix alarms created in Gadgetbridge not repeating --- apps/android/ChangeLog | 1 + apps/android/boot.js | 8 +++++++- apps/android/metadata.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index d531e43a9..108242825 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -32,3 +32,4 @@ Allow alarm enable/disable 0.31: Implement API for activity fetching 0.32: Added support for loyalty cards from gadgetbridge +0.33: Fix alarms created in Gadgetbridge not repeating diff --git a/apps/android/boot.js b/apps/android/boot.js index 846fc40a8..63f9b2883 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -81,7 +81,12 @@ for (var j = 0; j < event.d.length; j++) { // prevents all alarms from going off at once?? var dow = event.d[j].rep; - if (!dow) dow = 127; //if no DOW selected, set alarm to all DOW + var rp = false; + if (!dow) { + dow = 127; //if no DOW selected, set alarm to all DOW + } else { + rp = true; + } var last = (event.d[j].h * 3600000 + event.d[j].m * 60000 < currentTime) ? (new Date()).getDate() : 0; var a = require("sched").newDefaultAlarm(); a.id = "gb"+j; @@ -89,6 +94,7 @@ a.on = event.d[j].on !== undefined ? event.d[j].on : true; a.t = event.d[j].h * 3600000 + event.d[j].m * 60000; a.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format + a.rp = rp; a.last = last; alarms.push(a); } diff --git a/apps/android/metadata.json b/apps/android/metadata.json index 68bd946c5..5babc520b 100644 --- a/apps/android/metadata.json +++ b/apps/android/metadata.json @@ -2,7 +2,7 @@ "id": "android", "name": "Android Integration", "shortName": "Android", - "version": "0.32", + "version": "0.33", "description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.", "icon": "app.png", "tags": "tool,system,messages,notifications,gadgetbridge", From 69071891f1deef3a1485f4a7822b6570a688c6bf Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Fri, 2 Feb 2024 09:34:36 +0000 Subject: [PATCH 32/48] Tweaks to speed up track rendering --- apps/openstmap/ChangeLog | 3 +- apps/openstmap/openstmap.js | 22 +- apps/recorder/ChangeLog | 1 + apps/recorder/app.js | 414 ++++++++++++++++++------------------ apps/recorder/metadata.json | 2 +- 5 files changed, 221 insertions(+), 221 deletions(-) diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog index 98d01d5e9..f0a1e5c5a 100644 --- a/apps/openstmap/ChangeLog +++ b/apps/openstmap/ChangeLog @@ -33,4 +33,5 @@ 0.26: Ensure that when redrawing, we always cancel any in-progress track draw 0.27: Display message if no map is installed 0.28: Fix rounding errors -0.29: move exit to bottom of menu +0.29: Keep exit at bottom of menu + Speed up latLonToXY for track rendering \ No newline at end of file diff --git a/apps/openstmap/openstmap.js b/apps/openstmap/openstmap.js index 7e84f66e5..eb1aeaf3c 100644 --- a/apps/openstmap/openstmap.js +++ b/apps/openstmap/openstmap.js @@ -38,17 +38,17 @@ if (m.map) { m.lat = m.map.lat; // position of middle of screen m.lon = m.map.lon; // position of middle of screen } +var CX = g.getWidth()/2; +var CY = g.getHeight()/2; // return number of tiles drawn exports.draw = function() { - var cx = g.getWidth()/2; - var cy = g.getHeight()/2; var p = Bangle.project({lat:m.lat,lon:m.lon}); let count = 0; m.maps.forEach((map,idx) => { var d = map.scale/m.scale; - var ix = (p.x-map.center.x)/m.scale + (map.imgx*d/2) - cx; - var iy = (map.center.y-p.y)/m.scale + (map.imgy*d/2) - cy; + var ix = (p.x-map.center.x)/m.scale + (map.imgx*d/2) - CX; + var iy = (map.center.y-p.y)/m.scale + (map.imgy*d/2) - CY; var o = {}; var s = map.tilesize; if (d!=1) { // if the two are different, add scaling @@ -85,14 +85,12 @@ exports.draw = function() { }; /// Convert lat/lon to pixels on the screen -exports.latLonToXY = function(lat, lon) { - var p = Bangle.project({lat:m.lat,lon:m.lon}); - var q = Bangle.project({lat:lat, lon:lon}); - var cx = g.getWidth()/2; - var cy = g.getHeight()/2; +exports.latLonToXY = function(lat, lon) { "ram" + var p = Bangle.project({lat:m.lat,lon:m.lon}), + q = Bangle.project({lat:lat, lon:lon}); return { - x : Math.round((q.x-p.x)/m.scale + cx), - y : Math.round(cy - (q.y-p.y)/m.scale) + x : Math.round((q.x-p.x)/m.scale + CX), + y : Math.round(CY - (q.y-p.y)/m.scale) }; }; @@ -102,4 +100,4 @@ exports.scroll = function(x,y) { var b = Bangle.project({lat:m.lat+1,lon:m.lon+1}); this.lon += x * m.scale / (a.x-b.x); this.lat -= y * m.scale / (a.y-b.y); -}; +}; \ No newline at end of file diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog index dadd3fbcb..0e20a13fc 100644 --- a/apps/recorder/ChangeLog +++ b/apps/recorder/ChangeLog @@ -45,3 +45,4 @@ 0.36: When recording with 1 second periods, log time with one decimal. 0.37: 1 second periods + gps log => log when gps event is received, not with setInterval. +0.38: Tweaks to speed up track rendering \ No newline at end of file diff --git a/apps/recorder/app.js b/apps/recorder/app.js index a2218420a..c02a800d0 100644 --- a/apps/recorder/app.js +++ b/apps/recorder/app.js @@ -213,230 +213,230 @@ function viewTrack(filename, info) { }); }; menu['< Back'] = () => { viewTracks(); }; + return E.showMenu(menu); +} - function plotTrack(info) { "ram" - function distance(lat1,long1,lat2,long2) { "ram" - var x = (long1-long2) * Math.cos((lat1+lat2)*Math.PI/360); - var y = lat2 - lat1; - return Math.sqrt(x*x + y*y) * 6371000 * Math.PI / 180; - } +function plotTrack(info) { "ram" + function distance(lat1,long1,lat2,long2) { "ram" + var x = (long1-long2) * Math.cos((lat1+lat2)*Math.PI/360); + var y = lat2 - lat1; + return Math.sqrt(x*x + y*y) * 6371000 * Math.PI / 180; + } - // Function to convert lat/lon to XY - var getMapXY; - if (info.qOSTM) { - // scale map to view full track - const max = Bangle.project({lat: info.maxLat, lon: info.maxLong}); - const min = Bangle.project({lat: info.minLat, lon: info.minLong}); - const scaleX = (max.x-min.x)/Bangle.appRect.w; - const scaleY = (max.y-min.y)/Bangle.appRect.h; - osm.scale = Math.ceil((scaleX > scaleY ? scaleX : scaleY)*1.1); // add 10% margin - getMapXY = osm.latLonToXY.bind(osm); - } else { - getMapXY = function(lat, lon) { "ram" - return {x:cx + Math.round((long - info.lon)*info.lfactor*info.scale), - y:cy + Math.round((info.lat - lat)*info.scale)}; - }; - } + // Function to convert lat/lon to XY + var XY; + if (info.qOSTM) { + // scale map to view full track + const max = Bangle.project({lat: info.maxLat, lon: info.maxLong}); + const min = Bangle.project({lat: info.minLat, lon: info.minLong}); + const scaleX = (max.x-min.x)/Bangle.appRect.w; + const scaleY = (max.y-min.y)/Bangle.appRect.h; + osm.scale = Math.ceil((scaleX > scaleY ? scaleX : scaleY)*1.1); // add 10% margin + XY = osm.latLonToXY.bind(osm); + } else { + XY = function(lat, lon) { "ram" + return {x:cx + Math.round((long - info.lon)*info.lfactor*info.scale), + y:cy + Math.round((info.lat - lat)*info.scale)}; + }; + } - E.showMenu(); // remove menu - E.showMessage(/*LANG*/"Drawing...",/*LANG*/"Track "+info.fn); - g.flip(); // on buffered screens, draw a not saying we're busy - g.clear(1); - var s = require("Storage"); - var W = g.getWidth(); - var H = g.getHeight(); - var cx = W/2; - var cy = 24 + (H-24)/2; - if (!info.qOSTM) { - g.setColor("#f00").fillRect(9,80,11,120).fillPoly([9,60,19,80,0,80]); - g.setColor(g.theme.fg).setFont("6x8").setFontAlign(0,0).drawString("N",10,50); - } else { - osm.lat = info.lat; - osm.lon = info.lon; - osm.draw(); - g.setColor("#000"); + E.showMenu(); // remove menu + E.showMessage(/*LANG*/"Drawing...",/*LANG*/"Track "+info.fn); + g.flip(); // on buffered screens, draw a not saying we're busy + g.clear(1); + var s = require("Storage"); + var G = g; + var W = g.getWidth(); + var H = g.getHeight(); + var cx = W/2; + var cy = 24 + (H-24)/2; + if (!info.qOSTM) { + g.setColor("#f00").fillRect(9,80,11,120).fillPoly([9,60,19,80,0,80]); + g.setColor(g.theme.fg).setFont("6x8").setFontAlign(0,0).drawString("N",10,50); + } else { + osm.lat = info.lat; + osm.lon = info.lon; + osm.draw(); + g.setColor("#000"); + } + var latIdx = info.fields.indexOf("Latitude"); + var lonIdx = info.fields.indexOf("Longitude"); + g.drawString(asTime(info.duration),10,220); + var f = require("Storage").open(info.filename,"r"); + if (f===undefined) return; + var l = f.readLine(f); + l = f.readLine(f); // skip headers + var ox=0; + var oy=0; + var olat,olong,dist=0; + var i=0, c = l.split(","); + // skip until we find our first data + while(l!==undefined && c[latIdx]=="") { + c = l.split(","); + l = f.readLine(f); + } + // now start plotting + var lat = +c[latIdx]; + var long = +c[lonIdx]; + var mp = XY(lat, long); + g.moveTo(mp.x,mp.y); + g.setColor("#0f0"); + g.fillCircle(mp.x,mp.y,5); + if (info.qOSTM) g.setColor("#f09"); + else g.setColor(g.theme.fg); + l = f.readLine(f); + g.flip(); // force update + while(l!==undefined) { + c = l.split(",");l = f.readLine(f); + if (c[latIdx]=="")continue; + lat = +c[latIdx]; + long = +c[lonIdx]; + mp = XY(lat, long); + G.lineTo(mp.x,mp.y); + if (info.qOSTM) G.fillCircle(mp.x,mp.y,2); // make the track more visible + var d = distance(olat,olong,lat,long); + if (!isNaN(d)) dist+=d; + olat = lat; + olong = long; + ox = mp.x; + oy = mp.y; + if (++i > 100) { G.flip();i=0; } + } + g.setColor("#f00"); + g.fillCircle(ox,oy,5); + if (info.qOSTM) g.setColor("#000"); + else g.setColor(g.theme.fg); + g.drawString(require("locale").distance(dist,2),g.getWidth() / 2, g.getHeight() - 20); + g.setFont("6x8",2); + g.setFontAlign(0,0,3); + var isBTN3 = "BTN3" in global; + g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2)); + setWatch(function() { + viewTrack(info.fn, info); + }, isBTN3?BTN3:BTN1); + Bangle.drawWidgets(); + g.flip(); +} + +function plotGraph(info, style) { "ram" + E.showMenu(); // remove menu + E.showMessage(/*LANG*/"Calculating...",/*LANG*/"Track "+info.fn); + var filename = info.filename; + var infn = new Float32Array(80); + var infc = new Uint16Array(80); + var title; + var lt = 0; // last time + var tn = 0; // count for each time period + var strt, dur = info.duration; + var f = require("Storage").open(filename,"r"); + if (f===undefined) return; + var l = f.readLine(f); + l = f.readLine(f); // skip headers + var nl = 0, c, i; + var factor = 1; // multiplier used for values when graphing + var timeIdx = info.fields.indexOf("Time"); + if (l!==undefined) { + c = l.split(","); + strt = c[timeIdx]; + } + if (style=="Heartrate") { + title = /*LANG*/"Heartrate (bpm)"; + var hrmIdx = info.fields.indexOf("Heartrate"); + while(l!==undefined) { + ++nl;c=l.split(",");l = f.readLine(f); + if (c[hrmIdx]=="") continue; + i = Math.round(80*(c[timeIdx] - strt)/dur); + infn[i]+=+c[hrmIdx]; + infc[i]++; } + } else if (style=="Altitude") { + title = /*LANG*/"Altitude (m)"; + var altIdx = info.fields.indexOf("Barometer Altitude"); + if (altIdx<0) altIdx = info.fields.indexOf("Altitude"); + while(l!==undefined) { + ++nl;c=l.split(",");l = f.readLine(f); + if (c[altIdx]=="") continue; + i = Math.round(80*(c[timeIdx] - strt)/dur); + infn[i]+=+c[altIdx]; + infc[i]++; + } + } else if (style=="Speed") { + // use locate to work out units + var localeStr = require("locale").speed(1,5); // get what 1kph equates to + let units = localeStr.replace(/[0-9.]*/,""); + factor = parseFloat(localeStr)*3.6; // m/sec to whatever out units are + // title + title = /*LANG*/"Speed"+` (${units})`; var latIdx = info.fields.indexOf("Latitude"); var lonIdx = info.fields.indexOf("Longitude"); - g.drawString(asTime(info.duration),10,220); - var f = require("Storage").open(info.filename,"r"); - if (f===undefined) return; - var l = f.readLine(f); - l = f.readLine(f); // skip headers - var ox=0; - var oy=0; - var olat,olong,dist=0; - var i=0, c = l.split(","); // skip until we find our first data while(l!==undefined && c[latIdx]=="") { c = l.split(","); l = f.readLine(f); } - // now start plotting - var lat = +c[latIdx]; - var long = +c[lonIdx]; - var mp = getMapXY(lat, long); - g.moveTo(mp.x,mp.y); - g.setColor("#0f0"); - g.fillCircle(mp.x,mp.y,5); - if (info.qOSTM) g.setColor("#f09"); - else g.setColor(g.theme.fg); - l = f.readLine(f); - g.flip(); // force update + // now iterate + var p,lp = Bangle.project({lat:c[1],lon:c[2]}); + var t,dx,dy,d,lt = c[timeIdx]; while(l!==undefined) { - c = l.split(",");l = f.readLine(f); - if (c[latIdx]=="")continue; - lat = +c[latIdx]; - long = +c[lonIdx]; - mp = getMapXY(lat, long); - g.lineTo(mp.x,mp.y); - if (info.qOSTM) g.fillCircle(mp.x,mp.y,2); // make the track more visible - var d = distance(olat,olong,lat,long); - if (!isNaN(d)) dist+=d; - olat = lat; - olong = long; - ox = mp.x; - oy = mp.y; - if (++i > 100) { g.flip();i=0; } - } - g.setColor("#f00"); - g.fillCircle(ox,oy,5); - if (info.qOSTM) g.setColor("#000"); - else g.setColor(g.theme.fg); - g.drawString(require("locale").distance(dist,2),g.getWidth() / 2, g.getHeight() - 20); - g.setFont("6x8",2); - g.setFontAlign(0,0,3); - var isBTN3 = "BTN3" in global; - g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2)); - setWatch(function() { - viewTrack(info.fn, info); - }, isBTN3?BTN3:BTN1); - Bangle.drawWidgets(); - g.flip(); - } - - function plotGraph(info, style) { "ram" - E.showMenu(); // remove menu - E.showMessage(/*LANG*/"Calculating...",/*LANG*/"Track "+info.fn); - var filename = info.filename; - var infn = new Float32Array(80); - var infc = new Uint16Array(80); - var title; - var lt = 0; // last time - var tn = 0; // count for each time period - var strt, dur = info.duration; - var f = require("Storage").open(filename,"r"); - if (f===undefined) return; - var l = f.readLine(f); - l = f.readLine(f); // skip headers - var nl = 0, c, i; - var factor = 1; // multiplier used for values when graphing - var timeIdx = info.fields.indexOf("Time"); - if (l!==undefined) { - c = l.split(","); - strt = c[timeIdx]; - } - if (style=="Heartrate") { - title = /*LANG*/"Heartrate (bpm)"; - var hrmIdx = info.fields.indexOf("Heartrate"); - while(l!==undefined) { - ++nl;c=l.split(",");l = f.readLine(f); - if (c[hrmIdx]=="") continue; - i = Math.round(80*(c[timeIdx] - strt)/dur); - infn[i]+=+c[hrmIdx]; + ++nl;c=l.split(","); + l = f.readLine(f); + if (c[latIdx] == "") { + continue; + } + t = c[timeIdx]; + i = Math.round(80*(t - strt)/dur); + p = Bangle.project({lat:c[latIdx],lon:c[lonIdx]}); + dx = p.x-lp.x; + dy = p.y-lp.y; + d = Math.sqrt(dx*dx+dy*dy); + if (t!=lt) { + infn[i]+=d / (t-lt); // speed infc[i]++; } - } else if (style=="Altitude") { - title = /*LANG*/"Altitude (m)"; - var altIdx = info.fields.indexOf("Barometer Altitude"); - if (altIdx<0) altIdx = info.fields.indexOf("Altitude"); - while(l!==undefined) { - ++nl;c=l.split(",");l = f.readLine(f); - if (c[altIdx]=="") continue; - i = Math.round(80*(c[timeIdx] - strt)/dur); - infn[i]+=+c[altIdx]; - infc[i]++; - } - } else if (style=="Speed") { - // use locate to work out units - var localeStr = require("locale").speed(1,5); // get what 1kph equates to - let units = localeStr.replace(/[0-9.]*/,""); - factor = parseFloat(localeStr)*3.6; // m/sec to whatever out units are - // title - title = /*LANG*/"Speed"+` (${units})`; - var latIdx = info.fields.indexOf("Latitude"); - var lonIdx = info.fields.indexOf("Longitude"); - // skip until we find our first data - while(l!==undefined && c[latIdx]=="") { - c = l.split(","); - l = f.readLine(f); - } - // now iterate - var p,lp = Bangle.project({lat:c[1],lon:c[2]}); - var t,dx,dy,d,lt = c[timeIdx]; - while(l!==undefined) { - ++nl;c=l.split(","); - l = f.readLine(f); - if (c[latIdx] == "") { - continue; - } - t = c[timeIdx]; - i = Math.round(80*(t - strt)/dur); - p = Bangle.project({lat:c[latIdx],lon:c[lonIdx]}); - dx = p.x-lp.x; - dy = p.y-lp.y; - d = Math.sqrt(dx*dx+dy*dy); - if (t!=lt) { - infn[i]+=d / (t-lt); // speed - infc[i]++; - } - lp = p; - lt = t; - } - } else throw new Error("Unknown type "+style); - var min=100000,max=-100000; - for (var i=0;i0) infn[i]=factor*infn[i]/infc[i]; - else { // no data - search back and see if we can find something - for (var j=i-1;j>=0;j--) - if (infc[j]) { infn[i]=infn[j]; break; } - } - var n = infn[i]; - if (n>max) max=n; - if (n 8) { - grid*=2; + } else throw new Error("Unknown type "+style); + var min=100000,max=-100000; + for (var i=0;i0) infn[i]=factor*infn[i]/infc[i]; + else { // no data - search back and see if we can find something + for (var j=i-1;j>=0;j--) + if (infc[j]) { infn[i]=infn[j]; break; } } - // draw - g.clear(1).setFont("6x8",1); - var r = require("graph").drawLine(g, infn, { - x:4,y:24, - width: g.getWidth()-24, - height: g.getHeight()-(24+8), - axes : true, - gridy : grid, - gridx : infn.length / 3, - title: title, - miny: min, - maxy: max, - xlabel : x=>Math.round(x*dur/(60*infn.length))+/*LANG*/" min" // minutes - }); - g.setFont("6x8",2); - g.setFontAlign(0,0,3); - var isBTN3 = "BTN3" in global; - g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2)); - setWatch(function() { - viewTrack(info.filename, info); - }, isBTN3?BTN3:BTN1); - g.flip(); + var n = infn[i]; + if (n>max) max=n; + if (n 8) { + grid*=2; + } + // draw + g.clear(1).setFont("6x8",1); + var r = require("graph").drawLine(g, infn, { + x:4,y:24, + width: g.getWidth()-24, + height: g.getHeight()-(24+8), + axes : true, + gridy : grid, + gridx : infn.length / 3, + title: title, + miny: min, + maxy: max, + xlabel : x=>Math.round(x*dur/(60*infn.length))+/*LANG*/" min" // minutes + }); + g.setFont("6x8",2); + g.setFontAlign(0,0,3); + var isBTN3 = "BTN3" in global; + g.drawString(/*LANG*/"Back",g.getWidth() - 10, isBTN3 ? (g.getHeight() - 40) : (g.getHeight()/2)); + setWatch(function() { + viewTrack(info.filename, info); + }, isBTN3?BTN3:BTN1); + g.flip(); } -showMainMenu(); +showMainMenu(); \ No newline at end of file diff --git a/apps/recorder/metadata.json b/apps/recorder/metadata.json index b0f42e1b4..a231a98e9 100644 --- a/apps/recorder/metadata.json +++ b/apps/recorder/metadata.json @@ -2,7 +2,7 @@ "id": "recorder", "name": "Recorder", "shortName": "Recorder", - "version": "0.37", + "version": "0.38", "description": "Record GPS position, heart rate and more in the background, then download to your PC.", "icon": "app.png", "tags": "tool,outdoors,gps,widget,clkinfo", From feea7df3eb3c01bfa9a615c019528f956b3762a1 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Mon, 5 Feb 2024 10:33:20 +0000 Subject: [PATCH 33/48] legoremote 0.02: Add more control styles, and linear control of motors --- apps/legoremote/ChangeLog | 1 + apps/legoremote/README.md | 17 +++- apps/legoremote/app.js | 171 ++++++++++++++++++++++++---------- apps/legoremote/metadata.json | 2 +- 4 files changed, 139 insertions(+), 52 deletions(-) diff --git a/apps/legoremote/ChangeLog b/apps/legoremote/ChangeLog index 5560f00bc..b86638553 100644 --- a/apps/legoremote/ChangeLog +++ b/apps/legoremote/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Add more control styles diff --git a/apps/legoremote/README.md b/apps/legoremote/README.md index a95b7b154..b6cd82979 100644 --- a/apps/legoremote/README.md +++ b/apps/legoremote/README.md @@ -10,8 +10,21 @@ in the future this app will be able to support other types of remote (see below) ## Usage -Run the app, and ensure you're not connected to your watch via Bluetooth -(a warning will pop up if so). +Run the app, then choose the type of controls you want and ensure you're not connected +to your watch via Bluetooth (a warning will pop up if so). + +Linear mode controls A/B axes individually, and allows you to vary the speed of the +motors based on the distance you drag from the centre. Other modes just use on/off +buttons. + +| Mode | up | down | left | right | +|------------|------|------|------|-------| +| **Linear** | +A | -A | -B | +B | +| **Normal** | +A | -A | -B | +B | +| **Tank** | -A+B | +A-B | +A+B | -A-B | +| **Merged** | -A-B | +A+B | +A-B | -A+B | + +In all cases pressing the C/D buttons will turn on C/D outputs Now press the arrow keys on the screen to control the robot. diff --git a/apps/legoremote/app.js b/apps/legoremote/app.js index 1c76a54a8..40935cabf 100644 --- a/apps/legoremote/app.js +++ b/apps/legoremote/app.js @@ -1,5 +1,4 @@ var lego = require("mouldking"); -lego.start(); E.on('kill', () => { // return to normal Bluetooth advertising NRF.setAdvertising({},{showName:true}); @@ -12,59 +11,133 @@ var controlState = ""; Bangle.loadWidgets(); Bangle.drawWidgets(); var R = Bangle.appRect; -// we'll divide up into 3x3 -function getBoxCoords(x,y) { - return { - x : R.x + R.w*x/3, - y : R.y + R.h*y/3 - }; -} -function draw() { - g.reset().clearRect(R); - var c, ninety = Math.PI/2; - var colOn = "#f00", colOff = g.theme.fg; - c = getBoxCoords(1.5, 0.5); - g.setColor(controlState=="up"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:0}); - c = getBoxCoords(2.5, 1.5); - g.setColor(controlState=="right"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety}); - c = getBoxCoords(0.5, 1.5); - g.setColor(controlState=="left"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:-ninety}); - c = getBoxCoords(1.5, 1.5); - g.setColor(controlState=="down"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety*2}); - if (NRF.getSecurityStatus().connected) { - c = getBoxCoords(1.5, 2.5); - g.setFontAlign(0,0).setFont("6x8").drawString("WARNING:\nBluetooth Connected\nYou must disconnect\nbefore LEGO will work",c.x,c.y); +function startLegoButtons(controls) { + // we'll divide up into 3x3 + function getBoxCoords(x,y) { + return { + x : R.x + R.w*x/3, + y : R.y + R.h*y/3 + }; } -} -draw(); -NRF.on('connect', draw); -NRF.on('disconnect', draw); -function setControlState(s) { - controlState = s; - var c = {}; - var speed = 3; - if (s=="up") c={a:-speed,b:-speed}; - if (s=="down") c={a:speed,b:speed}; - if (s=="left") c={a:speed,b:-speed}; - if (s=="right") c={a:-speed,b:speed}; + function draw() { + g.reset().clearRect(R); + var c, ninety = Math.PI/2; + var colOn = "#f00", colOff = g.theme.fg; + c = getBoxCoords(1.5, 0.5); + g.setColor(controlState=="up"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:0}); + c = getBoxCoords(2.5, 1.5); + g.setColor(controlState=="right"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety}); + c = getBoxCoords(0.5, 1.5); + g.setColor(controlState=="left"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:-ninety}); + c = getBoxCoords(1.5, 1.5); + g.setColor(controlState=="down"?colOn:colOff).drawImage(arrowIcon,c.x,c.y,{rotate:ninety*2}); + if (NRF.getSecurityStatus().connected) { + c = getBoxCoords(1.5, 2.5); + g.setFontAlign(0,0).setFont("6x8").drawString("WARNING:\nBluetooth Connected\nYou must disconnect\nbefore LEGO will work",c.x,c.y); + } + g.setFont("6x8:3").setFontAlign(0,0); + c = getBoxCoords(0.5, 0.5); + g.setColor(controlState=="c"?colOn:colOff).drawString("C",c.x,c.y); + c = getBoxCoords(2.5, 0.5); + g.setColor(controlState=="d"?colOn:colOff).drawString("D",c.x,c.y); + } + + function setControlState(s) { + controlState = s; + var c = {}; + if (s in controls) + c = controls[s]; + draw(); + lego.set(c); + } + + lego.start(); + Bangle.setUI({mode:"custom", drag : e => { + var x = Math.floor(E.clip((e.x - R.x) * 3 / R.w,0,2.99)); + var y = Math.floor(E.clip((e.y - R.y) * 3 / R.h,0,2.99)); + if (!e.b) { + setControlState(""); + return; + } + if (y==0) { // top row + if (x==0) setControlState("c"); + if (x==1) setControlState("up"); + if (x==2) setControlState("d"); + } else if (y==1) { + if (x==0) setControlState("left"); + if (x==1) setControlState("down"); + if (x==2) setControlState("right"); + } + }}); + draw(); - lego.set(c); + NRF.on('connect', draw); + NRF.on('disconnect', draw); } -Bangle.on('drag',e => { - var x = Math.floor(E.clip((e.x - R.x) * 3 / R.w,0,2.99)); - var y = Math.floor(E.clip((e.y - R.y) * 3 / R.h,0,2.99)); - if (!e.b) { - setControlState(""); - return; - } - if (y==0) { // top row - if (x==1) setControlState("up"); - } else if (y==1) { - if (x==0) setControlState("left"); - if (x==1) setControlState("down"); - if (x==2) setControlState("right"); +function startLegoLinear() { + var mx = R.x+R.w/2; + var my = R.y+R.h/2; + var x=0,y=0; + var scale = 10; + + function draw() { + g.reset().clearRect(R); + for (var i=3;i<60;i+=10) + g.drawCircle(mx,my,i); + g.setColor("#F00"); + var px = E.clip(mx + x*scale, R.x+20, R.x2-20); + var py = E.clip(my + y*scale, R.y+20, R.y2-20); + g.fillCircle(px, py, 20); } + + lego.start(); + Bangle.setUI({mode:"custom", drag : e => { + x = Math.round((e.x - mx) / scale); + y = Math.round((e.y - my) / scale); + if (!e.b) { + x=0; y=0; + } + lego.set({a:x, b:y}); + draw(); + }}); + + draw(); + NRF.on('connect', draw); + NRF.on('disconnect', draw); +} + +// Mappings of button to output +const CONTROLS = { + normal : { + up : {a: 7,b: 0}, + down : {a:-7,b: 0}, + left : {a: 0,b:-7}, + right: {a: 0,b: 7}, + c : {c:7}, + d : {d:7} + }, tank : { + up : {a:-7,b:7}, + down : {a: 7,b:-7}, + left : {a: 7,b:7}, + right: {a:-7,b:-7}, + c : {c:7}, + d : {d:7} + }, merged : { + up : {a: 7,b: 7}, + down : {a:-7,b:-7}, + left : {a: 7,b:-7}, + right: {a:-7,b: 7}, + c : {c:7}, + d : {d:7} + } + }; + +E.showMenu({ "" : {title:"LEGO Remote", back:()=>load()}, + "Linear" : () => startLegoLinear(), + "Normal" : () => startLego(CONTROLS.normal), + "Tank" : () => startLego(CONTROLS.tank), + "Marged" : () => startLego(CONTROLS.merged), }); diff --git a/apps/legoremote/metadata.json b/apps/legoremote/metadata.json index c86251860..8fe4c4b44 100644 --- a/apps/legoremote/metadata.json +++ b/apps/legoremote/metadata.json @@ -1,7 +1,7 @@ { "id": "legoremote", "name": "LEGO Remote control", "shortName":"LEGO Remote", - "version":"0.01", + "version":"0.02", "description": "Use your Bangle.js to control LEGO models. See the README for compatibility", "icon": "app.png", "tags": "toy,lego,bluetooth", From a9d135f616b1dbb015b554500e3b61d045718d79 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Mon, 5 Feb 2024 11:02:19 +0000 Subject: [PATCH 34/48] change app descriptions slightly --- apps/bthometemp/metadata.json | 2 +- apps/ha/metadata.json | 2 +- apps/hasensors/metadata.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/bthometemp/metadata.json b/apps/bthometemp/metadata.json index 8ffb22c83..fc6804f17 100644 --- a/apps/bthometemp/metadata.json +++ b/apps/bthometemp/metadata.json @@ -2,7 +2,7 @@ "name": "BTHome Temperature and Pressure", "shortName":"BTHome T", "version":"0.02", - "description": "Displays temperature and pressure, and advertises them over bluetooth using BTHome.io standard", + "description": "Displays temperature and pressure, and advertises them over bluetooth for Home Assistant using BTHome.io standard", "icon": "app.png", "tags": "bthome,bluetooth,temperature", "supports" : ["BANGLEJS2"], diff --git a/apps/ha/metadata.json b/apps/ha/metadata.json index a34621b04..c58e5e486 100644 --- a/apps/ha/metadata.json +++ b/apps/ha/metadata.json @@ -2,7 +2,7 @@ "id": "ha", "name": "Home Assistant", "version": "0.10", - "description": "Integrates your Bangle.js into Home Assistant.", + "description": "Integrates your Bangle.js into Home Assistant using Android Integration/Gadgetbridge", "icon": "ha.png", "type": "app", "tags": "tool,clkinfo,bluetooth", diff --git a/apps/hasensors/metadata.json b/apps/hasensors/metadata.json index 106f11407..5764c6100 100644 --- a/apps/hasensors/metadata.json +++ b/apps/hasensors/metadata.json @@ -3,7 +3,7 @@ "name": "Home Assistant Sensors", "shortName": "HA sensors", "version": "0.02", - "description": "Send sensor values to Home Assistant using the Android Integration.", + "description": "Send sensor values to Home Assistant using Android Integration/Gadgetbridge", "icon": "ha.png", "type": "bootloader", "tags": "tool,sensors", From d5360755bdd0e0887e2d5f0e9b6c87c3eec7c07d Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Mon, 5 Feb 2024 14:00:59 +0000 Subject: [PATCH 35/48] add example clock app --- apps/_example_clkinfo/ChangeLog | 2 +- apps/_example_clock/ChangeLog | 1 + apps/_example_clock/README.md | 25 +++++++++++++++++ apps/_example_clock/app-icon.js | 1 + apps/_example_clock/clock.js | 44 ++++++++++++++++++++++++++++++ apps/_example_clock/icon.png | Bin 0 -> 1620 bytes apps/_example_clock/metadata.json | 15 ++++++++++ 7 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 apps/_example_clock/ChangeLog create mode 100644 apps/_example_clock/README.md create mode 100644 apps/_example_clock/app-icon.js create mode 100644 apps/_example_clock/clock.js create mode 100644 apps/_example_clock/icon.png create mode 100644 apps/_example_clock/metadata.json diff --git a/apps/_example_clkinfo/ChangeLog b/apps/_example_clkinfo/ChangeLog index 4c21f3ace..78ba28f3b 100644 --- a/apps/_example_clkinfo/ChangeLog +++ b/apps/_example_clkinfo/ChangeLog @@ -1 +1 @@ -0.01: New Widget! +0.01: New Clock Info! diff --git a/apps/_example_clock/ChangeLog b/apps/_example_clock/ChangeLog new file mode 100644 index 000000000..09953593e --- /dev/null +++ b/apps/_example_clock/ChangeLog @@ -0,0 +1 @@ +0.01: New Clock! diff --git a/apps/_example_clock/README.md b/apps/_example_clock/README.md new file mode 100644 index 000000000..5d750a965 --- /dev/null +++ b/apps/_example_clock/README.md @@ -0,0 +1,25 @@ +# Clock Name + +More info on making Clock Faces: https://www.espruino.com/Bangle.js+Clock + +Describe the Clock... + +## Usage + +Describe how to use it + +## Features + +Name the function + +## Controls + +Name the buttons and what they are used for + +## Requests + +Name who should be contacted for support/update requests + +## Creator + +Your name diff --git a/apps/_example_clock/app-icon.js b/apps/_example_clock/app-icon.js new file mode 100644 index 000000000..49232b838 --- /dev/null +++ b/apps/_example_clock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA==")) diff --git a/apps/_example_clock/clock.js b/apps/_example_clock/clock.js new file mode 100644 index 000000000..7e97cf758 --- /dev/null +++ b/apps/_example_clock/clock.js @@ -0,0 +1,44 @@ +// timeout used to update every minute +var drawTimeout; + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + +function draw() { + // queue next draw in one minute + queueDraw(); + // Work out where to draw... + var x = g.getWidth()/2; + var y = g.getHeight()/2; + g.reset(); + // work out locale-friendly date/time + var date = new Date(); + var timeStr = require("locale").time(date,1); + var dateStr = require("locale").date(date); + // draw time + g.setFontAlign(0,0).setFont("Vector",48); + g.clearRect(0,y-15,g.getWidth(),y+25); // clear the background + g.drawString(timeStr,x,y); + // draw date + y += 35; + g.setFontAlign(0,0).setFont("6x8"); + g.clearRect(0,y-4,g.getWidth(),y+4); // clear the background + g.drawString(dateStr,x,y); +} + +// Clear the screen once, at startup +g.clear(); +// draw immediately at first, queue update +draw(); + +// Show launcher when middle button pressed +Bangle.setUI("clock"); +// Load widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); \ No newline at end of file diff --git a/apps/_example_clock/icon.png b/apps/_example_clock/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..582cb2e0853a5a2899a3afbd7eb19cde2ee7f6a0 GIT binary patch literal 1620 zcmV-a2CMmrP)1gXjloC|3_d8m;N2OpV(|i0q4YwBna<2! zK9thw%-*|urnNbV{Gax^?eD+#{x0kLJ~)lj_;W+1>qV*k8akT^^dvctZccUyj4}H~#M%Wwee_v` zHMv7o%BM8@dBrLshn{wGD9BDl?^eV5vSM3T96;NnHvtc6La=(qzq)xrX1d8bK-TN- zrd_f$_O`9nEmS+_S7HTXK<&u;LDIW|qlN&KJvM}tt6TVVqL-AvNv`B*{NzNpBfSQwQP5~Sf(Dp@Vq1+3Q`N9wBQN2`J_?M^u0FIMlt?p^8 z%U3%80kIwg!T{E9<8J18S&$k1`eO)@HP+=TZKo(z3_A3VFYJB=sn`2^Q$mRE>02(+W)np;)L1!GUvU2{O{<&F_nE6Qe#D~Xf|dD z+?d3-D1(IUiL`C2;PPv4CKw8H)v7h8^obJ&Z6D0CjVUe8Xq_NAymxUyPAMU^CCrIu z%1M71EC`5o2if_~7E&h??0jeQ1Y3N6p?}G72FmS*)xQD)%wBE=2tW6@(+MTi!fk9H1pWKew2(jTXVu4%vk26QvSQCbGmk`Z)Y! zBIhh)6vG2)h6mF8wC^|l$M(Eo9D?JiW}=_T2jUA>LC80foTera{^p)Wi`>}Gf;(|ZwEZQ zS^k|*9wyt=f4ZOo!xty7{%}HKD9tBZ50g$=%v&&vMa!#@Nsf>EkEEDA*ST6fiC+An zsNK1#>!x0obq@j$QqYU-ad3ZvbjqUU+%iw(0WahgmHV6yeLWqoYkSl4pzFQ(_Vp&I ztO{WI-48rGLwQb?#vgVvduyd9_6W)rFRoQJq3I(J?{Xmin45#=3l9BmL6Bp<*MZej zrsWN7oRPUr7IvrHoIHOjS=gPTCw>d)^LQK+B|=f2qbGjrWaOd5D<<9Dv>MTW0X3z> zyPy}9`<>1~?NCx@m8G$_@rRTy5zH12YM&P)=tU+L^fgY z^0Z&_6^qdVuwgN3wt_Ze(10?J@%{C2grBk42hsu74qEo^nd&v`X`IHN9lrxzS~GeF S(*#!l0000 Date: Mon, 5 Feb 2024 22:31:45 +0000 Subject: [PATCH 36/48] Drop more format arguments --- apps/aviatorclk/aviatorclk.settings.js | 2 -- apps/gassist/settings.js | 1 - apps/welcome/settings.js | 1 - 3 files changed, 4 deletions(-) diff --git a/apps/aviatorclk/aviatorclk.settings.js b/apps/aviatorclk/aviatorclk.settings.js index 6db212ef1..d3ffbaad2 100644 --- a/apps/aviatorclk/aviatorclk.settings.js +++ b/apps/aviatorclk/aviatorclk.settings.js @@ -17,7 +17,6 @@ "< Back" : () => back(), 'Show Seconds': { value: !!settings.showSeconds, // !! converts undefined to false - format: v => v ? "On" : "Off", onchange: v => { settings.showSeconds = v; writeSettings(); @@ -25,7 +24,6 @@ }, 'Invert Scrolling': { value: !!settings.invertScrolling, // !! converts undefined to false - format: v => v ? "On" : "Off", onchange: v => { settings.invertScrolling = v; writeSettings(); diff --git a/apps/gassist/settings.js b/apps/gassist/settings.js index 987c3fdfc..20634ed5e 100644 --- a/apps/gassist/settings.js +++ b/apps/gassist/settings.js @@ -20,7 +20,6 @@ "< Back": () => back(), 'Front Tap:': { value: (appSettings.enableTap === true), - format: v => v ? "On" : "Off", onchange: v => { appSettings.enableTap = v; writeSettings(); diff --git a/apps/welcome/settings.js b/apps/welcome/settings.js index 27a322c7f..aa374694d 100644 --- a/apps/welcome/settings.js +++ b/apps/welcome/settings.js @@ -5,7 +5,6 @@ '': { 'title': 'Welcome App' }, 'Run next boot': { value: !settings.welcomed, - format: v => v ? 'Yes' : 'No', onchange: v => require('Storage').write('welcome.json', {welcomed: !v}), }, 'Run Now': () => load('welcome.app.js'), From f30f7d450ccddef9d6674e0de7fd72ac70f29066 Mon Sep 17 00:00:00 2001 From: Logan B <3870583+thinkpoop@users.noreply.github.com> Date: Mon, 5 Feb 2024 21:41:17 -0600 Subject: [PATCH 37/48] fwupdate: Add target=_blank on links leaving the page --- apps/fwupdate/custom.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/fwupdate/custom.html b/apps/fwupdate/custom.html index b917da87b..c2a12bb91 100644 --- a/apps/fwupdate/custom.html +++ b/apps/fwupdate/custom.html @@ -3,7 +3,7 @@ -

This tool allows you to update the firmware on Bangle.js 2 devices +

This tool allows you to update the firmware on Bangle.js 2 devices from within the App Loader.

@@ -32,7 +32,7 @@ bit of code that runs when Bangle.js starts, and it is able to update the Bangle.js firmware. Normally you would update firmware via this Firmware Updater app, but if for some reason Bangle.js will not boot, you can - always use DFU to do the update manually. + always use DFU to do the update manually. On DFU 2v19 and earlier, iOS devices could have issues updating firmware - 2v20 fixes this.

DFU is itself a bootloader, but here we're calling it DFU to avoid confusion with the Bootloader app in the app loader (which prepares Bangle.js for running apps).

@@ -42,7 +42,7 @@