Monorepo for Tangled
0

Configure Feed

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

profile-fx: add Super Mario Bros. World 1-1 punchcard effect

Adds a self-contained profile effect that side-scrolls World 1-1 across
the punchcard dot grid, and registers it for gdorsi.bsky.social.

authored by

Guido D'Orsi and committed by
Tangled
(Jul 14, 2026, 2:40 PM +0300) f2770fc2 229a640a

+313
+313
appview/pages/profile-fx/gdorsi.bsky.social.js
··· 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 + 12 + const 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. 16 + const 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 + ]; 40 + const SKY = PAL[1]; 41 + 42 + // Decode a base64 texture (array of rows) into a {w,h,d} sprite of palette ids. 43 + const 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 + 49 + const HILL = spr(["AASAA", "ASTSA", "SSSSS"]); 50 + const BUSH = spr(["ASSSA", "SSSSS"]); 51 + const 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. 55 + const MTOP = ["AMM", "PNN", "MMM"]; 56 + const LEGS = ["OAO", "AOO", "OAO", "OOA"]; // running cycle 57 + const LEGS_AIR = "OOA"; // tucked while jumping 58 + 59 + // --- Level layout (one looping period, measured in dot-columns) -------------- 60 + const P = 72; // period width 61 + const SCROLL = 5.5; // dot-columns per second 62 + 63 + const 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 + ]; 69 + const HILLS = [3, 46]; 70 + const BUSHES = [14, 58]; 71 + const QSINGLE = [{ x: 10, u: 4 }]; // lone ? block 72 + const HIGHQ = { x: 21, u: 8 }; // high ? block 73 + const 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 + ]; 81 + const PIPES = [ 82 + { x: 28, h: 2 }, 83 + { x: 34, h: 3 }, 84 + { x: 48, h: 4 }, 85 + { x: 56, h: 2 }, 86 + ]; 87 + const PITS = [[44, 45]]; // inclusive column ranges with no ground 88 + const GOOMBAS = [24, 40, 52]; // spawn columns; they walk left 89 + const STAIRS = [ 90 + { x: 61, h: 1 }, 91 + { x: 62, h: 2 }, 92 + { x: 63, h: 3 }, 93 + { x: 64, h: 4 }, 94 + ]; 95 + const FLAGX = 68; 96 + const CASTLEX = 70; 97 + 98 + // Scheduled jumps (column ranges + peak height) that carry Mario over each 99 + // obstacle. Between them he runs along the ground. 100 + const 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 + 108 + const isPit = (local) => PITS.some(([a, b]) => local >= a && local <= b); 109 + const 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 ---------------------------------------------------------------- 122 + const 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). 303 + let target = null; 304 + let teardown = null; 305 + const 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 + }; 312 + boot(); 313 + document.addEventListener("htmx:load", boot);