This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / pages / profile-fx / gdorsi.bsky.social.js
10 kB 313 lines
1// Super Mario Bros. World 1-1, rendered onto the punchcard. 2// 3// The punchcard is a grid of small dots (one per day of the year). We treat it 4// as a low-res dot-matrix display and side-scroll World 1-1 across it: ground, 5// bricks, ? blocks, pipes, Goombas, hills, bushes, clouds, a flagpole and a 6// castle, with Mario auto-running and hopping over the obstacles. It loops. 7// 8// Textures are base64: every sprite is an array of strings, and each character 9// is one pixel whose value is its index in the base64 alphabet (A=0, B=1, ...). 10// That index selects an entry from PAL below. Index 0 (`A`) is transparent. 11 12const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 13 14// Palette. Index lines up with the base64 alphabet so a texture char maps 15// straight to a colour: A->sky, D->ground, M->mario-red, and so on. 16const PAL = [ 17 null, // 0 A transparent 18 "#5c94fc", // 1 B sky 19 "#ffffff", // 2 C white (clouds, flag) 20 "#c84c0c", // 3 D ground 21 "#8a3b08", // 4 E ground / seam dark 22 "#e39d5b", // 5 F ground top light / block edge 23 "#b8560f", // 6 G brick 24 "#fac000", // 7 H ? block yellow 25 "#7a3b08", // 8 I shadow / mortar 26 "#00a800", // 9 J pipe green 27 "#5fdd5f", // 10 K pipe light 28 "#006000", // 11 L pipe dark 29 "#e21b0c", // 12 M mario red 30 "#ffa060", // 13 N mario skin 31 "#2038ec", // 14 O mario overalls blue 32 "#6a2a00", // 15 P brown (mario hair, goomba feet) 33 "#b06a3c", // 16 Q goomba tan 34 "#000000", // 17 R black 35 "#3ca03c", // 18 S hill / bush green 36 "#78d060", // 19 T hill light 37 "#b0b0b0", // 20 U castle grey 38 "#707070", // 21 V castle dark 39]; 40const SKY = PAL[1]; 41 42// Decode a base64 texture (array of rows) into a {w,h,d} sprite of palette ids. 43const spr = (rows) => ({ 44 w: rows[0].length, 45 h: rows.length, 46 d: rows.map((r) => Array.prototype.map.call(r, (c) => B64.indexOf(c))), 47}); 48 49const HILL = spr(["AASAA", "ASTSA", "SSSSS"]); 50const BUSH = spr(["ASSSA", "SSSSS"]); 51const CLOUD = spr(["ACCCA", "CCCCC"]); 52 53// Mario's three fixed top rows (hat, face, body); the fourth row is his legs, 54// swapped per animation frame below. 55const MTOP = ["AMM", "PNN", "MMM"]; 56const LEGS = ["OAO", "AOO", "OAO", "OOA"]; // running cycle 57const LEGS_AIR = "OOA"; // tucked while jumping 58 59// --- Level layout (one looping period, measured in dot-columns) -------------- 60const P = 72; // period width 61const SCROLL = 5.5; // dot-columns per second 62 63const CLOUDS = [ 64 { x: 8, f: 0.18 }, 65 { x: 26, f: 0.1 }, 66 { x: 42, f: 0.24 }, 67 { x: 62, f: 0.14 }, 68]; 69const HILLS = [3, 46]; 70const BUSHES = [14, 58]; 71const QSINGLE = [{ x: 10, u: 4 }]; // lone ? block 72const HIGHQ = { x: 21, u: 8 }; // high ? block 73const RUN = [ 74 // the classic brick / ? / brick / ? / brick row, 4 tiles up 75 { x: 18, t: "b" }, 76 { x: 19, t: "q" }, 77 { x: 20, t: "b" }, 78 { x: 21, t: "q" }, 79 { x: 22, t: "b" }, 80]; 81const PIPES = [ 82 { x: 28, h: 2 }, 83 { x: 34, h: 3 }, 84 { x: 48, h: 4 }, 85 { x: 56, h: 2 }, 86]; 87const PITS = [[44, 45]]; // inclusive column ranges with no ground 88const GOOMBAS = [24, 40, 52]; // spawn columns; they walk left 89const STAIRS = [ 90 { x: 61, h: 1 }, 91 { x: 62, h: 2 }, 92 { x: 63, h: 3 }, 93 { x: 64, h: 4 }, 94]; 95const FLAGX = 68; 96const CASTLEX = 70; 97 98// Scheduled jumps (column ranges + peak height) that carry Mario over each 99// obstacle. Between them he runs along the ground. 100const JUMPS = [ 101 { a: 25, b: 31, p: 4 }, // pipe @28 102 { a: 31.5, b: 38, p: 5 }, // pipe @34 103 { a: 42.5, b: 52, p: 7 }, // pit @44-45 + pipe @48 104 { a: 53.5, b: 59, p: 4 }, // pipe @56 105 { a: 60, b: 65, p: 5 }, // staircase 106]; 107 108const isPit = (local) => PITS.some(([a, b]) => local >= a && local <= b); 109const jumpY = (local) => { 110 let y = 0; 111 for (const j of JUMPS) { 112 if (local > j.a && local < j.b) { 113 const u = (local - j.a) / (j.b - j.a); 114 const h = 4 * j.p * u * (1 - u); // parabola, peak at the middle 115 if (h > y) y = h; 116 } 117 } 118 return y; 119}; 120 121// --- Renderer ---------------------------------------------------------------- 122const run = (card) => { 123 const dots = Array.from(card.children, (c) => c.firstElementChild).filter(Boolean); 124 const count = dots.length; 125 if (!count) return () => {}; 126 127 const cols = matchMedia("(min-width: 768px)").matches ? 14 : 28; 128 const rows = Math.max(1, Math.ceil(count / cols)); 129 const W = cols; 130 const G = Math.max(2, Math.round(rows * 0.16)); // ground thickness 131 const horizon = rows - G; // first ground row; sky is above 132 const MSC = Math.max(2, Math.round(W * 0.32)); // Mario's fixed screen column 133 const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches; 134 135 // Fill the cells so the picture reads as a solid dot-matrix screen. 136 for (const d of dots) { 137 d.style.transition = "none"; 138 d.style.transform = "scale(2)"; 139 d.style.border = "none"; 140 d.style.willChange = "background-color"; 141 } 142 143 const buf = new Uint8Array(W * rows); 144 const last = new Array(count).fill(null); 145 146 const setPx = (x, y, idx) => { 147 if (idx > 0 && x >= 0 && x < W && y >= 0 && y < rows) buf[y * W + x] = idx; 148 }; 149 const blit = (s, x, y) => { 150 for (let ry = 0; ry < s.h; ry++) { 151 const row = s.d[ry]; 152 for (let cx = 0; cx < s.w; cx++) setPx(x + cx, y + ry, row[cx]); 153 } 154 }; 155 const blitStr = (str, x, y) => { 156 for (let k = 0; k < str.length; k++) setPx(x + k, y, B64.indexOf(str[k])); 157 }; 158 159 // Draw one instance of a level feature per visible period. 160 let camCol = 0; 161 const eachBase = (cb) => { 162 const start = Math.floor((camCol - 8) / P) * P; 163 for (let b = start; b <= camCol + W + 8; b += P) cb(b); 164 }; 165 const at = (lx, draw) => 166 eachBase((b) => { 167 const sx = b + lx - camCol; 168 if (sx > -8 && sx < W + 2) draw(Math.round(sx)); 169 }); 170 171 const drawPipe = (sx, h) => { 172 const top = horizon - h; 173 for (let y = top; y <= horizon - 1; y++) { 174 setPx(sx, y, 10); 175 setPx(sx + 1, y, 9); 176 setPx(sx + 2, y, 11); 177 } 178 setPx(sx + 1, top, 10); // brighten the cap lip 179 }; 180 const drawStep = (sx, h) => { 181 for (let y = horizon - h; y <= horizon - 1; y++) setPx(sx, y, y === horizon - h ? 5 : 3); 182 }; 183 const drawFlag = (sx) => { 184 const top = horizon - 9; 185 for (let y = top; y <= horizon - 1; y++) setPx(sx, y, 20); 186 setPx(sx, top - 1, 2); // ball 187 setPx(sx - 1, top, 18); 188 setPx(sx - 2, top + 1, 18); 189 setPx(sx - 1, top + 1, 18); 190 setPx(sx - 1, top + 2, 18); 191 }; 192 const drawCastle = (sx) => { 193 const top = horizon - 4; 194 for (let ry = 0; ry < 4; ry++) { 195 for (let cx = 0; cx < 5; cx++) { 196 if (ry === 0 && cx % 2 === 1) continue; // crenellations 197 setPx(sx + cx, top + ry, 20); 198 } 199 } 200 setPx(sx + 2, horizon - 1, 17); // door 201 setPx(sx + 2, horizon - 2, 17); 202 setPx(sx + 1, top + 1, 17); // windows 203 setPx(sx + 3, top + 1, 17); 204 }; 205 206 let marioY = 0; // set each frame, read by the Goomba stomp check 207 const drawGoomba = (sx, t) => { 208 if ((sx === MSC || sx === MSC + 1) && marioY <= 1) { 209 blitStr("PPP", sx, horizon - 1); // squished flat 210 return; 211 } 212 blitStr("QQQ", sx, horizon - 2); 213 blitStr(Math.floor(t / 200) % 2 ? "APA" : "PAP", sx, horizon - 1); // waddle 214 }; 215 216 const render = (t) => { 217 const camX = (t / 1000) * SCROLL; 218 camCol = Math.floor(camX); 219 buf.fill(1); // sky 220 221 for (const c of CLOUDS) at(c.x, (sx) => blit(CLOUD, sx, Math.round(horizon * c.f))); 222 for (const hx of HILLS) at(hx, (sx) => blit(HILL, sx, horizon - HILL.h)); 223 for (const bx of BUSHES) at(bx, (sx) => blit(BUSH, sx, horizon - BUSH.h)); 224 225 // Ground, per screen column, with a pit here and there. 226 for (let x = 0; x < W; x++) { 227 const local = ((camCol + x) % P + P) % P; 228 if (isPit(local)) continue; 229 const seam = local % 4 === 0; 230 setPx(x, horizon, seam ? 4 : 5); 231 for (let y = horizon + 1; y < rows; y++) setPx(x, y, (local + y) % 4 === 0 ? 4 : 3); 232 } 233 234 const blink = Math.floor(t / 350) % 4 === 0; 235 for (const p of PIPES) at(p.x, (sx) => drawPipe(sx, p.h)); 236 for (const q of QSINGLE) at(q.x, (sx) => setPx(sx, horizon - q.u, blink ? 5 : 7)); 237 at(HIGHQ.x, (sx) => setPx(sx, horizon - HIGHQ.u, blink ? 5 : 7)); 238 for (const r of RUN) at(r.x, (sx) => setPx(sx, horizon - 4, r.t === "q" ? (blink ? 5 : 7) : 6)); 239 for (const s of STAIRS) at(s.x, (sx) => drawStep(sx, s.h)); 240 at(FLAGX, drawFlag); 241 at(CASTLEX, drawCastle); 242 243 // Goombas walk left; a full period of travel loops seamlessly. 244 const phase = ((t / 1000) * 3) % P; 245 eachBase((b) => { 246 for (const s of GOOMBAS) { 247 const sx = Math.round(b + s - phase - camCol); 248 if (sx > -3 && sx < W + 1) drawGoomba(sx, t); 249 } 250 }); 251 252 // Mario, pinned to screen column MSC, hopping the level as it scrolls by. 253 const local = ((camCol + MSC) % P + P) % P; 254 marioY = Math.min(horizon - 4, Math.round(jumpY(local))); 255 const legs = marioY > 0 ? LEGS_AIR : LEGS[Math.floor(t / 90) % LEGS.length]; 256 const topY = horizon - 1 - marioY - 3; 257 const body = [MTOP[0], MTOP[1], MTOP[2], legs]; 258 for (let r = 0; r < 4; r++) blitStr(body[r], MSC, topY + r); 259 260 // Commit only the dots that changed. 261 for (let i = 0; i < count; i++) { 262 const hex = PAL[buf[(i / W | 0) * W + (i % W)]] || SKY; 263 if (hex !== last[i]) { 264 dots[i].style.backgroundColor = hex; 265 last[i] = hex; 266 } 267 } 268 }; 269 270 if (reduce) { 271 render(2600); // one static frame, no animation 272 return () => {}; 273 } 274 275 let raf = 0; 276 let running = false; 277 const loop = (t) => { 278 render(t); 279 raf = requestAnimationFrame(loop); 280 }; 281 const start = () => { 282 if (running) return; 283 running = true; 284 raf = requestAnimationFrame(loop); 285 }; 286 const stop = () => { 287 running = false; 288 cancelAnimationFrame(raf); 289 }; 290 291 // Only animate while the punchcard is on screen. 292 const io = new IntersectionObserver((es) => (es.some((e) => e.isIntersecting) ? start() : stop())); 293 io.observe(card); 294 start(); 295 296 return () => { 297 stop(); 298 io.disconnect(); 299 }; 300}; 301 302// Boot, and re-bind across htmx navigations (matching the reference effect). 303let target = null; 304let teardown = null; 305const boot = () => { 306 const el = document.querySelector("[data-punchcard]"); 307 if (el === target) return; 308 if (teardown) teardown(); 309 target = el; 310 teardown = el ? run(el) : null; 311}; 312boot(); 313document.addEventListener("htmx:load", boot);