Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

add morpho piece — pixel sorting as a model of morphogenesis

Laboratory/sandbox for exploring how classical sorting algorithms
(bubble, selection, insertion, quick) model biological pattern
formation. Supports multiple sources (noise, gradients, camera,
drawing), sort keys, threshold-based interval sorting, and a
damage simulation mode inspired by Levin's self-sorting research.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+684
+684
system/public/aesthetic.computer/disks/morpho.mjs
··· 1 + // Morpho, 2026.3.02 2 + // Pixel sorting as a model of morphogenesis — a laboratory / sandbox. 3 + 4 + /* 📝 Engineering Notes 5 + Classical sorting algorithms transform disorder into order through iterated 6 + local operations — the same principle by which embryos self-organize from 7 + homogeneous cell masses. This piece lets you watch that process unfold 8 + pixel by pixel. 9 + 10 + Controls: 11 + S — cycle source (noise / gradient-h / gradient-v / camera / draw) 12 + A — cycle algorithm (bubble / selection / insertion / quick) 13 + K — cycle sort key (brightness / hue / red / green / blue) 14 + D — cycle direction (rows / columns / both) 15 + T / G — threshold up / down (0 = sort everything) 16 + + / - — speed up / slow down 17 + X — toggle damage (frozen pixels that refuse to swap) 18 + R — reset / regenerate 19 + Space — pause / resume 20 + */ 21 + 22 + const { floor, min, max, abs, random, round } = Math; 23 + 24 + // --- Source --- 25 + const SOURCES = ["noise", "gradient-h", "gradient-v", "camera", "draw"]; 26 + let sourceIdx = 0; 27 + 28 + // --- Algorithm --- 29 + const ALGORITHMS = ["bubble", "selection", "insertion", "quick"]; 30 + let algoIdx = 0; 31 + 32 + // --- Sort key --- 33 + const KEYS = ["brightness", "hue", "red", "green", "blue"]; 34 + let keyIdx = 0; 35 + 36 + // --- Direction --- 37 + const DIRECTIONS = ["rows", "columns", "both"]; 38 + let dirIdx = 0; 39 + 40 + // --- Parameters --- 41 + let threshold = 0; // 0–255; 0 = sort all, >0 = interval sorting 42 + let speed = 4; // comparison steps per row/col per frame 43 + let paused = false; 44 + let damageEnabled = false; 45 + 46 + // --- Pixel buffer (separate from screen so HUD text doesn't contaminate) --- 47 + let buf = null; 48 + let bufW = 0, 49 + bufH = 0; 50 + 51 + // --- Per-row / per-column sort state --- 52 + let rowStates = []; 53 + let colStates = []; 54 + let sortComplete = false; 55 + 56 + // --- Damage map --- 57 + let damageMap = null; // Uint8Array, 1 = frozen 58 + 59 + // --- Camera --- 60 + let vid = null; 61 + let cameraReady = false; 62 + 63 + // --- Drawing --- 64 + let drawColor = [255, 100, 50]; 65 + let penDown = false; 66 + let lastPenX = -1, 67 + lastPenY = -1; 68 + 69 + // ------------------------------------------------------------------------- 70 + // Utilities 71 + // ------------------------------------------------------------------------- 72 + 73 + function clamp8(v) { 74 + return max(0, min(255, round(v))); 75 + } 76 + 77 + function hslToRgb(h, s, l) { 78 + let r, g, b; 79 + if (s === 0) { 80 + r = g = b = l; 81 + } else { 82 + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; 83 + const p = 2 * l - q; 84 + const hue2rgb = (pp, qq, t) => { 85 + if (t < 0) t += 1; 86 + if (t > 1) t -= 1; 87 + if (t < 1 / 6) return pp + (qq - pp) * 6 * t; 88 + if (t < 1 / 2) return qq; 89 + if (t < 2 / 3) return pp + (qq - pp) * (2 / 3 - t) * 6; 90 + return pp; 91 + }; 92 + r = hue2rgb(p, q, h + 1 / 3); 93 + g = hue2rgb(p, q, h); 94 + b = hue2rgb(p, q, h - 1 / 3); 95 + } 96 + return [round(r * 255), round(g * 255), round(b * 255)]; 97 + } 98 + 99 + function rgbToHue(r, g, b) { 100 + r /= 255; 101 + g /= 255; 102 + b /= 255; 103 + const cmax = max(r, g, b), 104 + cmin = min(r, g, b); 105 + const d = cmax - cmin; 106 + if (d === 0) return 0; 107 + let h; 108 + if (cmax === r) h = ((g - b) / d) % 6; 109 + else if (cmax === g) h = (b - r) / d + 2; 110 + else h = (r - g) / d + 4; 111 + h *= 60; 112 + if (h < 0) h += 360; 113 + return h; // 0–360 114 + } 115 + 116 + // ------------------------------------------------------------------------- 117 + // Sort key extraction 118 + // ------------------------------------------------------------------------- 119 + 120 + function getSortKey(idx) { 121 + const r = buf[idx], 122 + g = buf[idx + 1], 123 + b = buf[idx + 2]; 124 + const key = KEYS[keyIdx]; 125 + if (key === "brightness") return r * 0.299 + g * 0.587 + b * 0.114; 126 + if (key === "red") return r; 127 + if (key === "green") return g; 128 + if (key === "blue") return b; 129 + if (key === "hue") return rgbToHue(r, g, b); 130 + return 0; 131 + } 132 + 133 + // ------------------------------------------------------------------------- 134 + // Source generation 135 + // ------------------------------------------------------------------------- 136 + 137 + function generateSource() { 138 + const src = SOURCES[sourceIdx]; 139 + 140 + if (src === "noise") { 141 + for (let i = 0; i < bufW * bufH; i++) { 142 + const idx = i * 4; 143 + buf[idx] = floor(random() * 256); 144 + buf[idx + 1] = floor(random() * 256); 145 + buf[idx + 2] = floor(random() * 256); 146 + buf[idx + 3] = 255; 147 + } 148 + } else if (src === "gradient-h") { 149 + for (let y = 0; y < bufH; y++) { 150 + for (let x = 0; x < bufW; x++) { 151 + const hue = x / bufW; 152 + const [r, g, b] = hslToRgb(hue, 0.8, 0.5); 153 + const n = (random() - 0.5) * 80; 154 + const idx = (y * bufW + x) * 4; 155 + buf[idx] = clamp8(r + n); 156 + buf[idx + 1] = clamp8(g + n); 157 + buf[idx + 2] = clamp8(b + n); 158 + buf[idx + 3] = 255; 159 + } 160 + } 161 + } else if (src === "gradient-v") { 162 + for (let y = 0; y < bufH; y++) { 163 + for (let x = 0; x < bufW; x++) { 164 + const hue = y / bufH; 165 + const [r, g, b] = hslToRgb(hue, 0.8, 0.5); 166 + const n = (random() - 0.5) * 80; 167 + const idx = (y * bufW + x) * 4; 168 + buf[idx] = clamp8(r + n); 169 + buf[idx + 1] = clamp8(g + n); 170 + buf[idx + 2] = clamp8(b + n); 171 + buf[idx + 3] = 255; 172 + } 173 + } 174 + } else if (src === "draw") { 175 + for (let i = 0; i < bufW * bufH; i++) { 176 + const idx = i * 4; 177 + buf[idx] = 15; 178 + buf[idx + 1] = 10; 179 + buf[idx + 2] = 20; 180 + buf[idx + 3] = 255; 181 + } 182 + } 183 + // Camera source is handled separately in paint(). 184 + 185 + damageMap.fill(0); 186 + } 187 + 188 + // ------------------------------------------------------------------------- 189 + // Sort state 190 + // ------------------------------------------------------------------------- 191 + 192 + function createSortState(algo, length) { 193 + if (algo === "bubble") 194 + return { type: "bubble", n: length, i: 0, pass: 0, done: false }; 195 + if (algo === "selection") 196 + return { 197 + type: "selection", 198 + n: length, 199 + current: 0, 200 + scanPos: 1, 201 + minIdx: 0, 202 + done: false, 203 + }; 204 + if (algo === "insertion") 205 + return { type: "insertion", n: length, current: 1, j: 1, done: false }; 206 + if (algo === "quick") 207 + return { 208 + type: "quick", 209 + n: length, 210 + stack: [[0, length - 1]], 211 + ps: null, 212 + done: false, 213 + }; 214 + return { done: true }; 215 + } 216 + 217 + function initSortState() { 218 + sortComplete = false; 219 + rowStates = []; 220 + colStates = []; 221 + 222 + const dir = DIRECTIONS[dirIdx]; 223 + const algo = ALGORITHMS[algoIdx]; 224 + 225 + if (dir === "rows" || dir === "both") { 226 + for (let y = 0; y < bufH; y++) 227 + rowStates.push(createSortState(algo, bufW)); 228 + } 229 + if (dir === "columns" || dir === "both") { 230 + for (let x = 0; x < bufW; x++) 231 + colStates.push(createSortState(algo, bufH)); 232 + } 233 + } 234 + 235 + // ------------------------------------------------------------------------- 236 + // Pixel helpers 237 + // ------------------------------------------------------------------------- 238 + 239 + function rowIdx(y, x) { 240 + return (y * bufW + x) * 4; 241 + } 242 + function colIdx(x, y) { 243 + return (y * bufW + x) * 4; 244 + } 245 + 246 + function swapPixels(a, b) { 247 + for (let c = 0; c < 4; c++) { 248 + const tmp = buf[a + c]; 249 + buf[a + c] = buf[b + c]; 250 + buf[b + c] = tmp; 251 + } 252 + } 253 + 254 + function inThreshold(flatIdx) { 255 + if (threshold === 0) return true; 256 + const bright = 257 + buf[flatIdx] * 0.299 + buf[flatIdx + 1] * 0.587 + buf[flatIdx + 2] * 0.114; 258 + return bright > threshold; 259 + } 260 + 261 + function isDamaged(flatIdx) { 262 + if (!damageEnabled) return false; 263 + return damageMap[flatIdx >> 2] === 1; // flatIdx is byte offset, /4 for pixel index 264 + } 265 + 266 + // ------------------------------------------------------------------------- 267 + // Sort step functions — one comparison/swap per call 268 + // ------------------------------------------------------------------------- 269 + 270 + function stepBubble(st, getIdx) { 271 + if (st.done) return; 272 + const bound = st.n - 1 - st.pass; 273 + if (bound <= 0) { 274 + st.done = true; 275 + return; 276 + } 277 + if (st.i >= bound) { 278 + st.pass++; 279 + st.i = 0; 280 + if (st.n - 1 - st.pass <= 0) st.done = true; 281 + return; 282 + } 283 + const a = getIdx(st.i), 284 + b = getIdx(st.i + 1); 285 + if (inThreshold(a) && inThreshold(b) && !isDamaged(a) && !isDamaged(b)) { 286 + if (getSortKey(a) > getSortKey(b)) swapPixels(a, b); 287 + } 288 + st.i++; 289 + } 290 + 291 + function stepSelection(st, getIdx) { 292 + if (st.done) return; 293 + if (st.current >= st.n - 1) { 294 + st.done = true; 295 + return; 296 + } 297 + const idxScan = getIdx(st.scanPos); 298 + const idxMin = getIdx(st.minIdx); 299 + if ( 300 + inThreshold(idxScan) && 301 + inThreshold(idxMin) && 302 + !isDamaged(idxScan) 303 + ) { 304 + if (getSortKey(idxScan) < getSortKey(idxMin)) st.minIdx = st.scanPos; 305 + } 306 + st.scanPos++; 307 + if (st.scanPos >= st.n) { 308 + const idxCur = getIdx(st.current); 309 + const idxMinF = getIdx(st.minIdx); 310 + if ( 311 + !isDamaged(idxCur) && 312 + !isDamaged(idxMinF) && 313 + st.minIdx !== st.current 314 + ) 315 + swapPixels(idxCur, idxMinF); 316 + st.current++; 317 + st.scanPos = st.current + 1; 318 + st.minIdx = st.current; 319 + if (st.current >= st.n - 1) st.done = true; 320 + } 321 + } 322 + 323 + function stepInsertion(st, getIdx) { 324 + if (st.done) return; 325 + if (st.current >= st.n) { 326 + st.done = true; 327 + return; 328 + } 329 + if (st.j > 0) { 330 + const a = getIdx(st.j), 331 + b = getIdx(st.j - 1); 332 + if (inThreshold(a) && inThreshold(b) && !isDamaged(a) && !isDamaged(b)) { 333 + if (getSortKey(b) > getSortKey(a)) { 334 + swapPixels(b, a); 335 + st.j--; 336 + return; 337 + } 338 + } 339 + st.current++; 340 + st.j = st.current; 341 + } else { 342 + st.current++; 343 + st.j = st.current; 344 + } 345 + } 346 + 347 + function stepQuick(st, getIdx) { 348 + if (st.done) return; 349 + if (st.stack.length === 0 && !st.ps) { 350 + st.done = true; 351 + return; 352 + } 353 + if (!st.ps) { 354 + const [lo, hi] = st.stack.pop(); 355 + if (lo >= hi) return; 356 + const pivotKey = getSortKey(getIdx(hi)); 357 + st.ps = { lo, hi, pivotKey, i: lo, store: lo }; 358 + } 359 + const p = st.ps; 360 + if (p.i < p.hi) { 361 + const idxI = getIdx(p.i), 362 + idxS = getIdx(p.store); 363 + if (inThreshold(idxI) && !isDamaged(idxI) && !isDamaged(idxS)) { 364 + if (getSortKey(idxI) <= p.pivotKey) { 365 + if (p.i !== p.store) swapPixels(idxI, idxS); 366 + p.store++; 367 + } 368 + } 369 + p.i++; 370 + } else { 371 + const idxS = getIdx(p.store), 372 + idxH = getIdx(p.hi); 373 + if (!isDamaged(idxS) && !isDamaged(idxH)) swapPixels(idxS, idxH); 374 + const piv = p.store; 375 + if (piv - 1 > p.lo) st.stack.push([p.lo, piv - 1]); 376 + if (piv + 1 < p.hi) st.stack.push([piv + 1, p.hi]); 377 + st.ps = null; 378 + } 379 + } 380 + 381 + function stepSort(st, getIdx) { 382 + if (st.type === "bubble") stepBubble(st, getIdx); 383 + else if (st.type === "selection") stepSelection(st, getIdx); 384 + else if (st.type === "insertion") stepInsertion(st, getIdx); 385 + else if (st.type === "quick") stepQuick(st, getIdx); 386 + } 387 + 388 + // ------------------------------------------------------------------------- 389 + // Advance all rows / columns by `speed` steps 390 + // ------------------------------------------------------------------------- 391 + 392 + function advanceSort() { 393 + if (paused || sortComplete) return; 394 + let allDone = true; 395 + 396 + for (let y = 0; y < rowStates.length; y++) { 397 + const st = rowStates[y]; 398 + if (st.done) continue; 399 + allDone = false; 400 + const getIdx = (pos) => rowIdx(y, pos); 401 + for (let s = 0; s < speed; s++) { 402 + stepSort(st, getIdx); 403 + if (st.done) break; 404 + } 405 + } 406 + 407 + for (let x = 0; x < colStates.length; x++) { 408 + const st = colStates[x]; 409 + if (st.done) continue; 410 + allDone = false; 411 + const getIdx = (pos) => colIdx(x, pos); 412 + for (let s = 0; s < speed; s++) { 413 + stepSort(st, getIdx); 414 + if (st.done) break; 415 + } 416 + } 417 + 418 + if (allDone && (rowStates.length > 0 || colStates.length > 0)) 419 + sortComplete = true; 420 + } 421 + 422 + // ------------------------------------------------------------------------- 423 + // Drawing helpers (for "draw" source mode) 424 + // ------------------------------------------------------------------------- 425 + 426 + function drawPixel(x, y) { 427 + x = floor(x); 428 + y = floor(y); 429 + if (x < 0 || x >= bufW || y < 0 || y >= bufH) return; 430 + const radius = 3; 431 + for (let dy = -radius; dy <= radius; dy++) { 432 + for (let dx = -radius; dx <= radius; dx++) { 433 + const px = x + dx, 434 + py = y + dy; 435 + if ( 436 + px >= 0 && 437 + px < bufW && 438 + py >= 0 && 439 + py < bufH && 440 + dx * dx + dy * dy <= radius * radius 441 + ) { 442 + const idx = (py * bufW + px) * 4; 443 + buf[idx] = drawColor[0]; 444 + buf[idx + 1] = drawColor[1]; 445 + buf[idx + 2] = drawColor[2]; 446 + buf[idx + 3] = 255; 447 + } 448 + } 449 + } 450 + } 451 + 452 + function drawLineOnBuf(x0, y0, x1, y1) { 453 + x0 = floor(x0); 454 + y0 = floor(y0); 455 + x1 = floor(x1); 456 + y1 = floor(y1); 457 + const dx = abs(x1 - x0), 458 + dy = abs(y1 - y0); 459 + const sx = x0 < x1 ? 1 : -1, 460 + sy = y0 < y1 ? 1 : -1; 461 + let err = dx - dy; 462 + while (true) { 463 + drawPixel(x0, y0); 464 + if (x0 === x1 && y0 === y1) break; 465 + const e2 = 2 * err; 466 + if (e2 > -dy) { 467 + err -= dy; 468 + x0 += sx; 469 + } 470 + if (e2 < dx) { 471 + err += dx; 472 + y0 += sy; 473 + } 474 + } 475 + } 476 + 477 + // ------------------------------------------------------------------------- 478 + // Lifecycle 479 + // ------------------------------------------------------------------------- 480 + 481 + function boot({ screen, params }) { 482 + bufW = screen.width; 483 + bufH = screen.height; 484 + buf = new Uint8ClampedArray(bufW * bufH * 4); 485 + damageMap = new Uint8Array(bufW * bufH); 486 + 487 + // Parse params: morpho:camera, morpho:gradient-h, etc. 488 + if (params[0]) { 489 + const p = params[0].toLowerCase(); 490 + const idx = SOURCES.indexOf(p); 491 + if (idx >= 0) sourceIdx = idx; 492 + if (p === "cam") sourceIdx = SOURCES.indexOf("camera"); 493 + } 494 + 495 + generateSource(); 496 + initSortState(); 497 + } 498 + 499 + function paint({ screen, ink, video }) { 500 + const { width: w, height: h, pixels } = screen; 501 + 502 + // Reinitialize on resize 503 + if (w !== bufW || h !== bufH) { 504 + bufW = w; 505 + bufH = h; 506 + buf = new Uint8ClampedArray(bufW * bufH * 4); 507 + damageMap = new Uint8Array(bufW * bufH); 508 + vid = null; 509 + cameraReady = false; 510 + generateSource(); 511 + initSortState(); 512 + } 513 + 514 + // Camera source: capture frame into buf 515 + if (SOURCES[sourceIdx] === "camera") { 516 + if (!vid) { 517 + vid = video("camera", { width: w, height: h, facing: "environment" }); 518 + } 519 + if (!cameraReady) { 520 + const frame = vid(function shader(_, c) {}); 521 + if (frame && frame.pixels && frame.pixels.length === buf.length) { 522 + buf.set(frame.pixels); 523 + cameraReady = true; 524 + initSortState(); 525 + } else if (frame && frame.pixels) { 526 + // Size mismatch — copy what we can 527 + const len = min(buf.length, frame.pixels.length); 528 + for (let i = 0; i < len; i++) buf[i] = frame.pixels[i]; 529 + cameraReady = true; 530 + initSortState(); 531 + } 532 + } 533 + } 534 + 535 + // Advance sorting 536 + advanceSort(); 537 + 538 + // Copy buffer to screen 539 + if (buf.length === pixels.length) { 540 + pixels.set(buf); 541 + } 542 + 543 + // --- HUD --- 544 + const algo = ALGORITHMS[algoIdx]; 545 + const src = SOURCES[sourceIdx]; 546 + const key = KEYS[keyIdx]; 547 + const dir = DIRECTIONS[dirIdx]; 548 + 549 + ink(255, 255, 255, 200).write(algo, { x: 6, y: 6 }); 550 + ink(180, 180, 200, 180).write(`${src} / ${key} / ${dir}`, { x: 6, y: 18 }); 551 + ink(180, 180, 200, 180).write(`threshold:${threshold} speed:${speed}`, { 552 + x: 6, 553 + y: 30, 554 + }); 555 + 556 + if (paused) ink(255, 200, 0).write("PAUSED", { x: 6, y: 42 }); 557 + if (sortComplete) ink(0, 255, 120).write("SORTED", { x: 6, y: 42 }); 558 + if (damageEnabled) ink(255, 80, 80).write("DAMAGE", { x: 6, y: 54 }); 559 + 560 + // Bottom hint 561 + ink(100, 100, 120, 160).write( 562 + "S:src A:algo K:key D:dir T/G:thresh +/-:spd X:dmg R:reset", 563 + { x: 6, y: h - 12 }, 564 + ); 565 + } 566 + 567 + function act({ event: e, screen }) { 568 + // Algorithm 569 + if (e.is("keyboard:down:a")) { 570 + algoIdx = (algoIdx + 1) % ALGORITHMS.length; 571 + initSortState(); 572 + } 573 + 574 + // Source 575 + if (e.is("keyboard:down:s")) { 576 + sourceIdx = (sourceIdx + 1) % SOURCES.length; 577 + vid = null; 578 + cameraReady = false; 579 + generateSource(); 580 + initSortState(); 581 + } 582 + 583 + // Sort key 584 + if (e.is("keyboard:down:k")) { 585 + keyIdx = (keyIdx + 1) % KEYS.length; 586 + initSortState(); 587 + } 588 + 589 + // Direction 590 + if (e.is("keyboard:down:d")) { 591 + dirIdx = (dirIdx + 1) % DIRECTIONS.length; 592 + initSortState(); 593 + } 594 + 595 + // Threshold 596 + if (e.is("keyboard:down:t")) { 597 + threshold = min(255, threshold + 16); 598 + initSortState(); 599 + } 600 + if (e.is("keyboard:down:g")) { 601 + threshold = max(0, threshold - 16); 602 + initSortState(); 603 + } 604 + 605 + // Speed 606 + if (e.is("keyboard:down:=")) { 607 + speed = min(128, speed * 2); 608 + } 609 + if (e.is("keyboard:down:-")) { 610 + speed = max(1, floor(speed / 2)); 611 + } 612 + 613 + // Pause 614 + if (e.is("keyboard:down:space")) { 615 + paused = !paused; 616 + } 617 + 618 + // Damage 619 + if (e.is("keyboard:down:x")) { 620 + damageEnabled = !damageEnabled; 621 + if (damageEnabled) { 622 + const count = floor(bufW * bufH * 0.02); 623 + for (let i = 0; i < count; i++) { 624 + damageMap[floor(random() * bufW * bufH)] = 1; 625 + } 626 + } else { 627 + damageMap.fill(0); 628 + } 629 + initSortState(); 630 + } 631 + 632 + // Reset 633 + if (e.is("keyboard:down:r")) { 634 + vid = null; 635 + cameraReady = false; 636 + generateSource(); 637 + initSortState(); 638 + } 639 + 640 + // Drawing (in draw mode) 641 + if (SOURCES[sourceIdx] === "draw") { 642 + if (e.is("touch")) { 643 + penDown = true; 644 + // Randomize brush color per stroke 645 + drawColor = hslToRgb(random(), 0.7 + random() * 0.3, 0.4 + random() * 0.3); 646 + lastPenX = e.x; 647 + lastPenY = e.y; 648 + drawPixel(e.x, e.y); 649 + } 650 + if (e.is("draw") && penDown) { 651 + drawLineOnBuf(lastPenX, lastPenY, e.x, e.y); 652 + lastPenX = e.x; 653 + lastPenY = e.y; 654 + } 655 + if (e.is("lift")) { 656 + penDown = false; 657 + } 658 + } 659 + 660 + // Touch to place damage 661 + if (damageEnabled && SOURCES[sourceIdx] !== "draw") { 662 + if (e.is("touch") || e.is("draw")) { 663 + const r = 6; 664 + for (let dy = -r; dy <= r; dy++) { 665 + for (let dx = -r; dx <= r; dx++) { 666 + const px = floor(e.x) + dx, 667 + py = floor(e.y) + dy; 668 + if ( 669 + px >= 0 && 670 + px < bufW && 671 + py >= 0 && 672 + py < bufH && 673 + dx * dx + dy * dy <= r * r 674 + ) { 675 + damageMap[py * bufW + px] = 1; 676 + } 677 + } 678 + } 679 + } 680 + } 681 + } 682 + 683 + export { boot, paint, act }; 684 + export const noBios = true;