From a2c6f53ecebccf0b2b301a64326f096e0e3ae82b Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Tue, 16 Jan 2024 23:21:37 +0000 Subject: [PATCH] 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 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 T4`BR$9(XR;I6$kdkoW!vCi?}6 literal 0 HcmV?d00001