Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

notepat-remote: live track color + stronger focus + narrower

Max patcher now observes the hosting track's color via
live.thisdevice → live.path (canonical_parent) → live.observer, and
pushes it into jweb as window.acSetLiveTrackColor(int). Piece decodes
the 0xRRGGBB int and uses it as the held-black-key tint, so pressed
sharps glow in the track's own color. Piece re-requests on boot (the
observer fires before boot runs) via a new `requestTrackColor`
outlet on route-top.

Focus detection got three backup paths since Max-for-Live jweb doesn't
reliably forward DOM events: native window blur/focus + document
visibilitychange listeners in boot, plus a 250ms `document.hasFocus()`
poll in sim. Also exposes `window.acSetLiveFocus` for future Max-side
pushes.

Black keys now visibly change fill on press (used to stay on PAD_SHARP
regardless of held state). QWERTY hint returns in the bottom-right of
each pad in MatrixChunky8, gated on pad width >= 14 so it hides
automatically on ultra-tight layouts.

Device shrinks once more: W 180 → 140 so the rack footprint halves
again. Square pads land at 17×17, still enough for the note label +
tiny QWERTY hint.

+113 -8
+29 -6
oven/bundler.mjs
··· 1066 1066 // octave, focus, ping from the daw bridge) pass through the final outlet to 1067 1067 // the MIDI router. Everything else matches the hand-rolled notepat device. 1068 1068 function generateChunkedNotepatM4DPatcher(pieceName, bootstrapDataUri, chunks) { 1069 - const W = 180, H = 169; 1069 + const W = 140, H = 169; 1070 1070 const liveUrl = "https://aesthetic.computer/" + pieceName + "?daw=1&nogap=1&density=1"; 1071 1071 const boxes = [ 1072 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 } }, ··· 1074 1074 // else to the MIDI router. `route` has (N matched + 1 unmatched) outlets. 1075 1075 // `goonline` means the bootstrap's network probe succeeded — swap 1076 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" } }, 1077 + // `requestTrackColor` is emitted by the piece post-boot to pull the 1078 + // current Live track color (observer's initial fire happens before 1079 + // the piece has mounted its handler). 1080 + { box: { id: "obj-route-top", maxclass: "newobj", numinlets: 1, numoutlets: 7, outlettype: ["","","","","","",""], patching_rect: [10,200,500,22], text: "route ready goonline requestTrackColor log error warn" } }, 1078 1081 { box: { id: "obj-goonline-msg", maxclass: "message", numinlets: 2, numoutlets: 1, outlettype: [""], patching_rect: [10,160,560,22], text: "url " + liveUrl } }, 1079 1082 { box: { id: "obj-print-log", maxclass: "newobj", numinlets: 1, numoutlets: 0, patching_rect: [100,230,200,22], text: "print [AC-LOG]" } }, 1080 1083 { box: { id: "obj-print-error", maxclass: "newobj", numinlets: 1, numoutlets: 0, patching_rect: [200,230,200,22], text: "print [AC-ERROR]" } }, 1081 1084 { box: { id: "obj-print-warn", maxclass: "newobj", numinlets: 1, numoutlets: 0, patching_rect: [300,230,200,22], text: "print [AC-WARN]" } }, 1085 + // ── Live track color integration ──────────────────────────────── 1086 + // live.thisdevice → canonical_parent resolves the track hosting 1087 + // this device; live.observer watches its `color` property and re- 1088 + // emits on change. Route the color int through sprintf into jweb 1089 + // as window.acSetLiveTrackColor(N). Piece decodes 0xRRGGBB itself. 1090 + { box: { id: "obj-live-thisdevice", maxclass: "newobj", numinlets: 1, numoutlets: 3, outlettype: ["bang","int","int"], patching_rect: [10,540,110,22], text: "live.thisdevice" } }, 1091 + { box: { id: "obj-live-path-parent", maxclass: "newobj", numinlets: 1, numoutlets: 3, outlettype: ["","",""], patching_rect: [10,570,280,22], text: "live.path this_device canonical_parent" } }, 1092 + { box: { id: "obj-live-observe-color", maxclass: "newobj", numinlets: 2, numoutlets: 3, outlettype: ["","",""], patching_rect: [10,600,220,22], text: "live.observer @property color" } }, 1093 + { box: { id: "obj-route-track-color", maxclass: "newobj", numinlets: 1, numoutlets: 2, outlettype: ["",""], patching_rect: [10,630,120,22], text: "route color" } }, 1094 + { box: { id: "obj-sprintf-track-color", maxclass: "newobj", numinlets: 1, numoutlets: 1, outlettype: [""], patching_rect: [10,660,480,22], text: "sprintf executejavascript window.acSetLiveTrackColor(%ld)" } }, 1082 1095 // MIDI routing (downstream of the unmatched outlet). 1083 1096 { box: { id: "obj-route", maxclass: "newobj", numinlets: 1, numoutlets: 7, outlettype: ["","","","","","",""], patching_rect: [10,300,560,22], text: "route note channel notedown noteup octave focus ping" } }, 1084 1097 { box: { id: "obj-noteout", maxclass: "newobj", numinlets: 2, numoutlets: 0, patching_rect: [10,450,60,22], text: "noteout" } }, ··· 1093 1106 // the live piece and abandons the bootstrap (chunks never fire). 1094 1107 { patchline: { source: ["obj-route-top", 1], destination: ["obj-goonline-msg", 0] } }, 1095 1108 { 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] } }, 1109 + // `requestTrackColor` from piece: bang the observer so it re-emits 1110 + // the current value to jweb (the initial fire happens before the 1111 + // piece is mounted, so without this the color never lands). 1112 + { patchline: { source: ["obj-route-top", 2], destination: ["obj-live-observe-color", 0] } }, 1113 + { patchline: { source: ["obj-route-top", 3], destination: ["obj-print-log", 0] } }, 1114 + { patchline: { source: ["obj-route-top", 4], destination: ["obj-print-error", 0] } }, 1115 + { patchline: { source: ["obj-route-top", 5], destination: ["obj-print-warn", 0] } }, 1099 1116 // Unmatched messages (notedown/noteup/octave/focus/ping) → MIDI router. 1100 - { patchline: { source: ["obj-route-top", 5], destination: ["obj-route", 0] } }, 1117 + { patchline: { source: ["obj-route-top", 6], destination: ["obj-route", 0] } }, 1118 + // Live track color chain: 1119 + { patchline: { source: ["obj-live-thisdevice", 0], destination: ["obj-live-path-parent", 0] } }, 1120 + { patchline: { source: ["obj-live-path-parent", 0], destination: ["obj-live-observe-color", 0] } }, 1121 + { patchline: { source: ["obj-live-observe-color", 0], destination: ["obj-route-track-color", 0] } }, 1122 + { patchline: { source: ["obj-route-track-color", 0], destination: ["obj-sprintf-track-color", 0] } }, 1123 + { patchline: { source: ["obj-sprintf-track-color", 0], destination: ["obj-jweb", 0] } }, 1101 1124 { patchline: { source: ["obj-route", 0], destination: ["obj-noteout", 0] } }, 1102 1125 { patchline: { source: ["obj-route", 1], destination: ["obj-noteout", 1] } }, 1103 1126 { patchline: { source: ["obj-route", 2], destination: ["obj-pack-on", 0] } },
+84 -2
system/public/aesthetic.computer/disks/notepat-remote.mjs
··· 101 101 // most recent note (darkened) and decays back to idle when nothing is held. 102 102 const bgColor = [4, 2, 6]; 103 103 104 + // Live track color, pushed in by the Max patcher via 105 + // `window.acSetLiveTrackColor(int)` once `live.observer` resolves the 106 + // device's parent track. Null until that lands; paint falls back to the 107 + // rainbow/ambient scheme when it's null so this stays optional. 108 + let liveTrackColor = null; 109 + let lastFocusPollFrame = -9999; 110 + 104 111 // Button grid layout (recomputed on each paint in case screen size changes). 105 112 let buttons = []; 106 113 ··· 204 211 hud?.label?.(""); 205 212 _send = send; 206 213 connectWs(); 214 + 215 + // ── Focus detection ──────────────────────────────────────────────── 216 + // jweb inside Max for Live doesn't always surface DOM blur/focus as AC 217 + // events. Hook the native listeners ourselves so the red-X unfocused 218 + // overlay actually tracks "can my keyboard drive this right now?". 219 + if (typeof window !== "undefined") { 220 + const setFocus = (f) => { 221 + if (focused !== f) { 222 + focused = f; 223 + focusedChangedFrame = frame; 224 + if (!f) heldKeys.clear(); 225 + } 226 + }; 227 + try { 228 + window.addEventListener("blur", () => setFocus(false)); 229 + window.addEventListener("focus", () => setFocus(true)); 230 + document.addEventListener?.("visibilitychange", () => setFocus(!document.hidden)); 231 + } catch {} 232 + // Max side pushes focus + theme state via these globals. Defining 233 + // them unconditionally lets the patcher call them whenever it likes. 234 + window.acSetLiveFocus = (f) => setFocus(!!f); 235 + window.acSetLiveTrackColor = (colorInt) => { 236 + const n = Number(colorInt) >>> 0; 237 + if (!Number.isFinite(n)) return; 238 + liveTrackColor = [(n >> 16) & 255, (n >> 8) & 255, n & 255]; 239 + }; 240 + // Pull the current track color now — the Max patcher's 241 + // `live.observer` fires once on device load (before this boot 242 + // runs), so without an explicit re-request we'd never see it. 243 + try { window.max?.outlet?.("requestTrackColor", 1); } catch {} 244 + } 207 245 // Also open AC's session-scoped socket + udp purely so the transport 208 246 // status indicator can show UDP connectivity. Callbacks are no-ops — 209 247 // the notepat:midi subscription flows through the raw WS above. ··· 233 271 function sim() { 234 272 frame += 1; 235 273 if (wsState === "closed" && frame >= reconnectAt) connectWs(); 274 + 275 + // Poll document.hasFocus() every ~250ms as a safety net — some jweb 276 + // focus transitions (user clicks the Live mixer header, browses a 277 + // menu) don't dispatch blur/focus events. Polling catches those. 278 + if (frame - lastFocusPollFrame > 15) { 279 + lastFocusPollFrame = frame; 280 + if (typeof document !== "undefined" && typeof document.hasFocus === "function") { 281 + const hf = document.hasFocus(); 282 + if (hf !== focused) { 283 + focused = hf; 284 + focusedChangedFrame = frame; 285 + if (!hf) heldKeys.clear(); 286 + } 287 + } 288 + } 236 289 237 290 // Subscribe over UDP once the geckos channel comes up. Lost connection 238 291 // resets the flag so we re-subscribe on reconnect. ··· 525 578 526 579 let fill; 527 580 if (held && focused) { 528 - // Fully saturated note color when pressed. 529 - fill = baseColor; 581 + if (black) { 582 + // Black keys use PAD_SHARP at rest — if we kept that for the 583 + // held state they'd never visibly change. Brighten + warm- 584 + // tint using the live track color (when Max has pushed it) 585 + // or a muted purple fallback so the press actually reads. 586 + const tint = liveTrackColor || [120, 90, 140]; 587 + fill = [ 588 + floor(55 + tint[0] * 0.35), 589 + floor(55 + tint[1] * 0.35), 590 + floor(55 + tint[2] * 0.35), 591 + ]; 592 + } else { 593 + // White keys: fully saturated rainbow note color on press. 594 + fill = baseColor; 595 + } 530 596 } else if (recentFlash && focused) { 531 597 // Note color blended in at recent-flash intensity. 532 598 const f = 1 - sinceNote / 18; ··· 595 661 const shadowColor = labelColor[0] < 128 ? [240, 240, 240, 140] : [0, 0, 0, 180]; 596 662 ink(...shadowColor).write(label, { x: labelX + shakeX + 1, y: labelY + shakeY + 1 }); 597 663 ink(...labelColor).write(label, { x: labelX + shakeX, y: labelY + shakeY }); 664 + 665 + // QWERTY hint — tiny MatrixChunky8 glyph pinned to the bottom- 666 + // right of the pad so you still see which key drives which note 667 + // even after we shrank the pads. Only drawn when the pad has 668 + // room (>= 14px) so we don't paint on top of the note label. 669 + if (pw >= 14 && ph >= 14) { 670 + const hintColor = 671 + held && focused && !black ? [10, 10, 14, 220] : 672 + black ? [210, 210, 220, 180] : [30, 30, 40, 190]; 673 + ink(...hintColor).write( 674 + key.toUpperCase(), 675 + { x: px + pw - 6, y: py + ph - 7 }, 676 + undefined, undefined, false, 677 + "MatrixChunky8", 678 + ); 679 + } 598 680 } 599 681 } 600 682 }
system/public/m4l/notepat.com.amxd

This is a binary file and will not be displayed.