From ed5aba215148616d14081be34521fd60c1f9e821 Mon Sep 17 00:00:00 2001 From: Felipe Manga Date: Sun, 14 Jan 2024 17:04:17 +0000 Subject: [PATCH] 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