Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

notepat-remote: online-first bootstrap + notepat-native palette

Offline amxd now probes aesthetic.computer on boot and routes jweb~ to
the live URL when reachable; the chunked bundle is only decompressed
as a fallback. Fixes the OFFLINE-transport lock where relay WS refused
to open from a data: URI's opaque origin.

Visual pass ported from notepat.mjs/native:
• responsive backdrop (wipe bgColor that tracks the last note)
• BAR_BG top strip + BAR_BORDER separator, no more Live-orange accent
• black-key pad fill = PAD_SHARP; held-pad border glows its own note
• 1px inset margin between pads with full-rect borders
• red X overlay when unfocused (replaces "TAP ME!")
• label/hint contrast fixed for held black keys

+147 -79
+19 -6
oven/bundler.mjs
··· 1023 1023 const title = pieceName.replace(/[<>&"']/g, ""); 1024 1024 // Compact JS so the whole document stays well under the 24 KB ceiling 1025 1025 // the `url` data: URI attribute can hold. 1026 + // Online-first: on boot the bootstrap probes aesthetic.computer. On 1027 + // success it tells Max to navigate jweb~ to the live URL (via a 1028 + // `goonline` message); on failure it emits `ready` and lets the chunk 1029 + // messages reassemble the embedded offline bundle. 1026 1030 return `<!DOCTYPE html> 1027 1031 <html lang="en"><head><meta charset="utf-8"><title>${title} · loading</title> 1028 1032 <style>html,body{margin:0;padding:0;width:100%;height:100%;background:#0e1012;color:#4f9;font:12px -apple-system,monospace;overflow:hidden}pre{margin:0;padding:10px 12px;white-space:pre-wrap;word-break:break-all}</style> ··· 1039 1043 }catch(e){log("render failed: "+e);}}}; 1040 1044 window.addEventListener("error",function(e){log("[err] "+e.message);}); 1041 1045 window.addEventListener("unhandledrejection",function(e){log("[rej] "+(e.reason&&e.reason.message||e.reason));}); 1046 + function probe(){return new Promise(function(res){if(typeof navigator!=="undefined"&&navigator.onLine===false){return res(false);}var done=false;var to=setTimeout(function(){if(!done){done=true;res(false);}},2500);try{fetch("https://aesthetic.computer/favicon.ico",{mode:"no-cors",cache:"no-store"}).then(function(){if(!done){done=true;clearTimeout(to);res(true);}}).catch(function(){if(!done){done=true;clearTimeout(to);res(false);}});}catch(e){if(!done){done=true;clearTimeout(to);res(false);}}});} 1042 1047 log("alive, polling for window.max"); 1043 - var tries=0;var iv=setInterval(function(){if(window.max&&typeof window.max.outlet==="function"){clearInterval(iv);log("window.max bound ("+tries+" tries), sending ready");try{window.max.outlet("ready",1);}catch(e){log("ready send failed: "+e);}}else if(++tries>100){clearInterval(iv);log("gave up waiting for window.max");}},50); 1048 + var tries=0;var iv=setInterval(function(){if(window.max&&typeof window.max.outlet==="function"){clearInterval(iv);log("window.max bound ("+tries+" tries), probing network");probe().then(function(online){if(online){log("online — sending goonline");try{window.max.outlet("goonline",1);}catch(e){log("goonline send failed: "+e);}}else{log("offline — sending ready");try{window.max.outlet("ready",1);}catch(e){log("ready send failed: "+e);}}});}else if(++tries>100){clearInterval(iv);log("gave up waiting for window.max");}},50); 1044 1049 })(); 1045 1050 </script></body></html>`; 1046 1051 } ··· 1062 1067 // the MIDI router. Everything else matches the hand-rolled notepat device. 1063 1068 function generateChunkedNotepatM4DPatcher(pieceName, bootstrapDataUri, chunks) { 1064 1069 const W = 360, H = 169; 1070 + const liveUrl = "https://aesthetic.computer/" + pieceName + "?daw=1&nogap=1&density=1"; 1065 1071 const boxes = [ 1066 1072 { box: { disablefind: 0, id: "obj-jweb", latency: 0, maxclass: "jweb~", numinlets: 1, numoutlets: 3, outlettype: ["signal","signal",""], patching_rect: [10,10,W,H], presentation: 1, presentation_rect: [0,0,W,H], rendermode: 1, url: bootstrapDataUri } }, 1067 1073 // Split jweb outlet 2 first by handshake/log symbols, then pass anything 1068 1074 // else to the MIDI router. `route` has (N matched + 1 unmatched) outlets. 1069 - { box: { id: "obj-route-top", maxclass: "newobj", numinlets: 1, numoutlets: 5, outlettype: ["","","","",""], patching_rect: [10,200,400,22], text: "route ready log error warn" } }, 1075 + // `goonline` means the bootstrap's network probe succeeded — swap 1076 + // jweb~'s URL to the live piece and skip chunk reassembly entirely. 1077 + { box: { id: "obj-route-top", maxclass: "newobj", numinlets: 1, numoutlets: 6, outlettype: ["","","","","",""], patching_rect: [10,200,400,22], text: "route ready goonline log error warn" } }, 1078 + { box: { id: "obj-goonline-msg", maxclass: "message", numinlets: 2, numoutlets: 1, outlettype: [""], patching_rect: [10,160,560,22], text: "url " + liveUrl } }, 1070 1079 { box: { id: "obj-print-log", maxclass: "newobj", numinlets: 1, numoutlets: 0, patching_rect: [100,230,200,22], text: "print [AC-LOG]" } }, 1071 1080 { box: { id: "obj-print-error", maxclass: "newobj", numinlets: 1, numoutlets: 0, patching_rect: [200,230,200,22], text: "print [AC-ERROR]" } }, 1072 1081 { box: { id: "obj-print-warn", maxclass: "newobj", numinlets: 1, numoutlets: 0, patching_rect: [300,230,200,22], text: "print [AC-WARN]" } }, ··· 1080 1089 const lines = [ 1081 1090 { patchline: { source: ["obj-jweb", 2], destination: ["obj-route-top", 0] } }, 1082 1091 // Handshake "ready" fans out to every chunk message (see below). 1083 - { patchline: { source: ["obj-route-top", 1], destination: ["obj-print-log", 0] } }, 1084 - { patchline: { source: ["obj-route-top", 2], destination: ["obj-print-error", 0] } }, 1085 - { patchline: { source: ["obj-route-top", 3], destination: ["obj-print-warn", 0] } }, 1092 + // `goonline` fires the `url …` message into jweb~, which navigates to 1093 + // the live piece and abandons the bootstrap (chunks never fire). 1094 + { patchline: { source: ["obj-route-top", 1], destination: ["obj-goonline-msg", 0] } }, 1095 + { patchline: { source: ["obj-goonline-msg", 0], destination: ["obj-jweb", 0] } }, 1096 + { patchline: { source: ["obj-route-top", 2], destination: ["obj-print-log", 0] } }, 1097 + { patchline: { source: ["obj-route-top", 3], destination: ["obj-print-error", 0] } }, 1098 + { patchline: { source: ["obj-route-top", 4], destination: ["obj-print-warn", 0] } }, 1086 1099 // Unmatched messages (notedown/noteup/octave/focus/ping) → MIDI router. 1087 - { patchline: { source: ["obj-route-top", 4], destination: ["obj-route", 0] } }, 1100 + { patchline: { source: ["obj-route-top", 5], destination: ["obj-route", 0] } }, 1088 1101 { patchline: { source: ["obj-route", 0], destination: ["obj-noteout", 0] } }, 1089 1102 { patchline: { source: ["obj-route", 1], destination: ["obj-noteout", 1] } }, 1090 1103 { patchline: { source: ["obj-route", 2], destination: ["obj-pack-on", 0] } },
+128 -73
system/public/aesthetic.computer/disks/notepat-remote.mjs
··· 96 96 let _send = null; 97 97 let frame = 0; 98 98 99 + // Smoothly-lerped background color so the notepat-native "color-reactive 100 + // backdrop" vibe carries into the M4L device. Target is derived from the 101 + // most recent note (darkened) and decays back to idle when nothing is held. 102 + const bgColor = [4, 2, 6]; 103 + 99 104 // Button grid layout (recomputed on each paint in case screen size changes). 100 105 let buttons = []; 101 106 ··· 345 350 const H = screen.height; 346 351 347 352 // ── Palette ────────────────────────────────────────────────────────── 348 - // Ableton-tasteful: graphite background, amber/orange accent (Live's 349 - // MIDI color), muted grays. Unfocused = deeply dimmed + angry red blink 350 - // so you can't miss it. 353 + // Ported from notepat.mjs (web) + fedac/native/pieces/notepat.mjs so the 354 + // M4L device shares the "notepat look" instead of its own Ableton-orange 355 + // identity: warm near-black backdrop that color-reacts to incoming notes 356 + // (native does this on its full screen via `wipe(bgColor)`), burgundy- 357 + // graphite bar tones, and off-white chrome. Held pads glow in their own 358 + // rainbow note color — no global accent color to clash with Live's UI. 351 359 const blinkPhase = (sin(frame * 0.14) + 1) / 2; // 0..1 sinusoidal 352 360 const blinkOn = blinkPhase > 0.5; 353 361 354 - // Focused theme (always-on) 355 - const focusedBg = [16, 18, 22]; 356 - const focusedAccent = [255, 156, 60]; // Live orange 357 - const focusedAccentBright = [255, 196, 110]; 358 - const focusedFg = [212, 216, 224]; 359 - const focusedDim = [110, 116, 130]; 360 - const focusedKeyWhite = [38, 42, 50]; 361 - const focusedKeyBlack = [22, 24, 30]; 362 - const focusedOutline = [70, 76, 88]; 362 + // Native's dark palette (fedac/native/pieces/notepat.mjs paint()). 363 + const BAR_BG = [35, 20, 30]; 364 + const BAR_BORDER = [55, 35, 45]; 365 + const PAD_SHARP = [18, 18, 20]; // black-key rest fill 366 + const BG_IDLE = [4, 2, 6]; // near-black when nothing playing 367 + 368 + // Focused theme — notepat palette, rainbow accent per note. 369 + const focusedFg = [220, 220, 220]; 370 + const focusedDim = [130, 125, 130]; 363 371 364 372 // Unfocused theme — dark + red; flashes for attention. 365 373 const unfocusedBg = blinkOn ? [48, 12, 12] : [22, 6, 6]; 366 374 const unfocusedAccent = blinkOn ? [255, 70, 70] : [180, 45, 45]; 367 375 const unfocusedFg = [180, 150, 150]; 368 376 const unfocusedDim = [100, 70, 70]; 369 - const unfocusedKeyWhite = [40, 18, 18]; 370 377 const unfocusedKeyBlack = [24, 10, 10]; 371 - const unfocusedOutline = [80, 30, 30]; 378 + const unfocusedKeyWhite = [40, 18, 18]; 372 379 373 - const bgBase = focused ? focusedBg : unfocusedBg; 374 - const accent = focused ? focusedAccent : unfocusedAccent; 375 - const accentBright = focused ? focusedAccentBright : unfocusedAccent; 380 + const sinceNote = frame - lastNoteFrame; 381 + 382 + // Compute the target backdrop color (notepat-native style: darkened note 383 + // color when a note's active/fresh, otherwise fade toward BG_IDLE). 384 + const BG_DECAY = 48; // frames a note keeps tinting the backdrop 385 + let bgTarget; 386 + if (focused && lastNote && sinceNote < BG_DECAY) { 387 + const lastPitchName = pitchNameShort(lastNote.pitch).toLowerCase(); 388 + const lastOct = pitchOctave(lastNote.pitch); 389 + const lastColor = getNoteColorForOctave(lastPitchName, lastOct, baseOctave); 390 + // Darker when old, brighter when fresh; caps at 0.42 so the bg never 391 + // competes with pad legibility. 392 + const freshness = max(0, 1 - sinceNote / BG_DECAY); 393 + const darken = 0.18 + freshness * 0.24; 394 + bgTarget = [ 395 + floor(BG_IDLE[0] + (lastColor[0] * darken - BG_IDLE[0]) * freshness), 396 + floor(BG_IDLE[1] + (lastColor[1] * darken - BG_IDLE[1]) * freshness), 397 + floor(BG_IDLE[2] + (lastColor[2] * darken - BG_IDLE[2]) * freshness), 398 + ]; 399 + } else if (!focused) { 400 + bgTarget = unfocusedBg; 401 + } else { 402 + bgTarget = BG_IDLE; 403 + } 404 + // Smooth lerp into target — snap on held (currently-pressed) notes. 405 + const hasHeld = focused && (heldKeys.size > 0 || !!tappedButton); 406 + const lerp = hasHeld ? 1 : 0.25; 407 + bgColor[0] += (bgTarget[0] - bgColor[0]) * lerp; 408 + bgColor[1] += (bgTarget[1] - bgColor[1]) * lerp; 409 + bgColor[2] += (bgTarget[2] - bgColor[2]) * lerp; 410 + const bgBase = [floor(bgColor[0]), floor(bgColor[1]), floor(bgColor[2])]; 411 + 412 + // Fallback-ish "accent" — used only where chrome needs a non-fg color 413 + // (held pad borders); derived from the last note so it still tracks the 414 + // rainbow vibe. When nothing's been played, use a warm off-white. 415 + const lastNoteColor = lastNote 416 + ? getNoteColorForOctave(pitchNameShort(lastNote.pitch).toLowerCase(), pitchOctave(lastNote.pitch), baseOctave) 417 + : focusedFg; 418 + const accent = focused ? lastNoteColor : unfocusedAccent; 376 419 const fg = focused ? focusedFg : unfocusedFg; 377 420 const dim = focused ? focusedDim : unfocusedDim; 378 - const keyWhite = focused ? focusedKeyWhite : unfocusedKeyWhite; 379 - const keyBlack = focused ? focusedKeyBlack : unfocusedKeyBlack; 380 - const outline = focused ? focusedOutline : unfocusedOutline; 421 + const keyBlack = focused ? PAD_SHARP : unfocusedKeyBlack; 381 422 382 423 wipe(...bgBase); 383 424 384 - const sinceNote = frame - lastNoteFrame; 385 - if (lastNote && sinceNote < 10 && focused) { 386 - const f = 1 - sinceNote / 10; 387 - ink(...accentBright, floor(40 * f)).box(0, 0, W, H, "fill"); 425 + // ── Top bar: notepat-native BAR_BG strip across the full width ─────── 426 + // Keeps header chrome legible while the responsive backdrop still shows 427 + // through the 1px gaps between pads below. 428 + const topBarH = 22; 429 + if (focused) { 430 + ink(...BAR_BG).box(0, 0, W, topBarH, "fill"); 431 + ink(...BAR_BORDER).line(0, topBarH, W - 1, topBarH); 388 432 } 389 433 390 434 // ── Header row: piece name + transport state ───────────────────────── 391 435 let y = 2; 392 - ink(...accent).write("notepat-remote", { x: 4, y }); 436 + ink(...fg).write("notepat-remote", { x: 4, y }); 393 437 // Transport indicator mirrors arena.mjs: UDP (green) > WS (yellow) > 394 438 // OFFLINE (red). UDP means net.udp is live (session geckos channel up); 395 439 // WS means the raw notepat:midi subscription socket is open. ··· 471 515 // piece's pad palette. Sharps/flats render black. 472 516 const nameShort = pitchNameShort(pitch); 473 517 const noteOctave = pitchOctave(pitch); 518 + // Black keys use native's PAD_SHARP so the "sharp" rest tone is 519 + // tuned to the rest of the palette; white keys keep the rainbow. 474 520 const baseColor = black 475 - ? [20, 22, 28] 521 + ? PAD_SHARP 476 522 : getNoteColorForOctave(nameShort.toLowerCase(), noteOctave, baseOctave); 477 523 478 524 let fill; ··· 496 542 floor(bgBase[2] + (baseColor[2] - bgBase[2]) * 0.35), 497 543 ]; 498 544 } else { 499 - fill = black ? keyBlack : keyWhite; 545 + fill = black ? keyBlack : unfocusedKeyWhite; 500 546 } 501 - ink(...fill).box(b.x, b.y, b.w, b.h, "fill"); 502 - // Draw a thin separator on the top and left edges using a darker 503 - // shade of the pad's own color. This gives a consistent 1px grid 504 - // line without the awkward outline-over-fill double-draw artifact. 505 - // Rightmost / bottommost pads skip the edge since the device 506 - // frame handles that boundary. 507 - const edgeDark = [ 508 - floor(fill[0] * 0.55), 509 - floor(fill[1] * 0.55), 510 - floor(fill[2] * 0.55), 547 + // Inset the drawn pad inside the cell so adjacent pads show a 1px 548 + // background gap between them. Hit area stays the full cell. 549 + const gap = 1; 550 + const px = b.x + gap; 551 + const py = b.y + gap; 552 + const pw = max(1, b.w - gap * 2); 553 + const ph = max(1, b.h - gap * 2); 554 + 555 + ink(...fill).box(px, py, pw, ph, "fill"); 556 + // Pad border: darker shade of fill, or bright accent when held. 557 + const borderColor = held && focused ? accent : [ 558 + floor(fill[0] * 0.5), 559 + floor(fill[1] * 0.5), 560 + floor(fill[2] * 0.5), 511 561 ]; 512 - if (b.x > 0) ink(...edgeDark).line(b.x, b.y, b.x, b.y + b.h - 1); 513 - if (b.y > gridTop) ink(...edgeDark).line(b.x, b.y, b.x + b.w - 1, b.y); 514 - // Held state: bright accent border fully around the pad. 515 - if (held && focused) { 516 - ink(...accent).box(b.x, b.y, b.w, b.h, "outline"); 517 - } 562 + ink(...borderColor).box(px, py, pw, ph, "outline"); 518 563 519 564 // Main label = note name (C, C#, D…), small hint = keyboard key. 565 + // Measured against AC's default 6×10 glyph grid. 566 + const charW = 6; 567 + const charH = 10; 520 568 const label = nameShort; 521 - const labelX = b.x + floor(b.w / 2) - floor(label.length * 6 / 2); 522 - const labelY = b.y + floor(b.h / 2) - 7; 569 + const labelW = label.length * charW; 570 + const labelX = px + floor((pw - labelW) / 2); 571 + const labelY = py + floor((ph - charH) / 2); 572 + // Black keys stay dark when held (fill ≈ graphite), so dark label 573 + // text disappears. Keep the light label/hint for black keys even 574 + // in the held state; only white keys (bright rainbow fill) flip 575 + // to dark text on press. 523 576 const labelColor = 524 - held && focused ? [10, 10, 14] : 577 + held && focused && !black ? [10, 10, 14] : 525 578 black ? [220, 225, 235] : [20, 22, 28]; 526 579 ink(...labelColor).write(label, { x: labelX, y: labelY }); 527 - // Keyboard key hint (1 char), bottom-right corner of the pad. 528 - const hintX = b.x + b.w - 7; 529 - const hintY = b.y + b.h - 8; 580 + // Keyboard key hint, inset from the bottom-right corner. 581 + const hintInset = 2; 582 + const hintX = px + pw - charW - hintInset; 583 + const hintY = py + ph - charH - hintInset + 1; 530 584 const hintColor = 531 - held && focused ? [10, 10, 14, 200] : 585 + held && focused && !black ? [10, 10, 14, 200] : 532 586 black ? [180, 180, 190, 180] : [40, 44, 52, 180]; 533 587 ink(...hintColor).write(key.toUpperCase(), { x: hintX, y: hintY }); 534 588 } 535 589 } 536 590 } 537 591 538 - // ── Unfocused overlay: big blinking "TAP ME!" over everything ──────── 592 + // ── Unfocused overlay: big red X spanning the device ───────────────── 593 + // When the mjs piece doesn't have focus, the keyboard won't drive 594 + // notes — the red X is the at-a-glance signal for that. 539 595 if (!focused) { 540 - // Semi-opaque scrim so the grid visibly darkens 541 - ink(4, 0, 0, 160).box(0, 0, W, H, "fill"); 596 + // Darken the grid so the X reads as "inactive" not "on top of art" 597 + ink(4, 0, 0, 150).box(0, 0, W, H, "fill"); 542 598 543 - // Thick pulsing red border that can't be missed 599 + // Pulsing red border — same cue scheme as before, just thinner 544 600 const borderAlpha = floor(140 + blinkPhase * 115); 545 - for (let i = 0; i < 3; i += 1) { 601 + for (let i = 0; i < 2; i += 1) { 546 602 ink(255, 40, 40, borderAlpha).box(i, i, W - i * 2, H - i * 2, "outline"); 547 603 } 548 604 549 - // Huge "TAP ME!" centered in the device 550 - const msg = "TAP ME!"; 551 - const msgSize = 2; // AC text scaling 552 - const charW = 6 * msgSize; 553 - const charH = 10 * msgSize; 554 - const msgW = msg.length * charW; 555 - const msgX = floor((W - msgW) / 2); 556 - const msgY = floor((H - charH) / 2) - 4; 557 - // Drop shadow 558 - ink(0, 0, 0, 180).write(msg, { x: msgX + 2, y: msgY + 2, size: msgSize }); 559 - ink(255, blinkOn ? 80 : 40, blinkOn ? 80 : 40) 560 - .write(msg, { x: msgX, y: msgY, size: msgSize }); 605 + // Thick red X from corner to corner. AC's line() is 1px, so stack 606 + // a handful of parallel lines to build a visible stroke. 607 + const xAlpha = floor(180 + blinkPhase * 75); 608 + const xThick = 2; // gives a 5px-equivalent X across (t from -2..2) 609 + for (let t = -xThick; t <= xThick; t += 1) { 610 + ink(255, 60, 60, xAlpha).line(0, t, W - 1, H - 1 + t); 611 + ink(255, 60, 60, xAlpha).line(W - 1, t, 0, H - 1 + t); 612 + } 561 613 562 - // Subtitle — smaller, italic-y hint 563 - const sub = "click me to play"; 564 - const subX = floor((W - sub.length * 6) / 2); 565 - const subY = msgY + charH + 4; 566 - ink(blinkOn ? 220 : 140, 120, 120) 567 - .write(sub, { x: subX, y: subY }); 614 + // Small hint badge at the top so the reason is readable 615 + const hint = "click to activate keys"; 616 + const hintW = hint.length * 6; 617 + const hintBoxX = floor((W - hintW) / 2) - 4; 618 + const hintBoxY = 2; 619 + ink(0, 0, 0, 200).box(hintBoxX, hintBoxY, hintW + 8, 12, "fill"); 620 + ink(255, 80, 80, borderAlpha).box(hintBoxX, hintBoxY, hintW + 8, 12, "outline"); 621 + ink(blinkOn ? 255 : 200, 140, 140) 622 + .write(hint, { x: floor((W - hintW) / 2), y: hintBoxY + 2 }); 568 623 } 569 624 } 570 625
system/public/m4l/notepat-remote.amxd

This is a binary file and will not be displayed.