Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

notepat-remote: fit 169px panel, dual 4x3 chromatic octave blocks (base + base+1)

+115 -119
+4 -4
ac-m4l/AC-NotepatRemote.amxd.json
··· 4 4 "appversion": { "major": 9, "minor": 0, "revision": 7, "architecture": "x64", "modernui": 1 }, 5 5 "classnamespace": "box", 6 6 "rect": [100, 100, 900, 520], 7 - "openrect": [0, 0, 360, 220], 7 + "openrect": [0, 0, 360, 169], 8 8 "openinpresentation": 1, 9 9 "gridsize": [15, 15], 10 10 "enablehscroll": 0, ··· 21 21 "numinlets": 1, 22 22 "numoutlets": 3, 23 23 "outlettype": ["signal", "signal", ""], 24 - "patching_rect": [10, 10, 360, 220], 24 + "patching_rect": [10, 10, 360, 169], 25 25 "presentation": 1, 26 - "presentation_rect": [0, 0, 360, 220], 26 + "presentation_rect": [0, 0, 360, 169], 27 27 "rendermode": 1, 28 - "url": "https://aesthetic.computer/notepat-remote?daw=1&density=1&nogap&v=9" 28 + "url": "https://aesthetic.computer/notepat-remote?daw=1&density=1&nogap&v=10" 29 29 } 30 30 }, 31 31 {
+1 -1
ac-m4l/devices.json
··· 63 63 "piece": "notepat-remote", 64 64 "description": "Relay MIDI from ac-native notepat (ThinkPad) to this track via session-server", 65 65 "width": 360, 66 - "height": 220, 66 + "height": 169, 67 67 "type": "midi", 68 68 "source": "AC-NotepatRemote.amxd.json", 69 69 "version": "0.1.0"
+110 -114
system/public/aesthetic.computer/disks/notepat-remote.mjs
··· 29 29 ";": 24, "'": 25, "]": 26, 30 30 }; 31 31 32 - // Rows laid out like a QWERTY keyboard (for physical muscle memory). 33 - const KEY_ROWS = [ 34 - ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"], 35 - ["a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "'"], 36 - ["z", "x", "c", "v", "b", "n", "m"], 32 + // Chromatic octave blocks — 4 cols × 3 rows = 12 notes each. 33 + // Matches notepat.mjs pad layout. Row 0 = low (C..D#), row 2 = high (G#..B). 34 + // Two blocks render side-by-side (base octave left, +1 right) when the 35 + // device is wide enough (always 360px in M4L). 36 + const OCTAVE_GRIDS = [ 37 + // Base octave (offsets 0-11 → C..B of baseOctave) 38 + [ 39 + ["c", "v", "d", "s"], 40 + ["e", "f", "w", "g"], 41 + ["r", "a", "q", "b"], 42 + ], 43 + // +1 octave (offsets 12-23 → C..B of baseOctave+1) 44 + [ 45 + ["h", "t", "i", "y"], 46 + ["j", "k", "u", "l"], 47 + ["o", "m", "p", "n"], 48 + ], 37 49 ]; 38 50 39 51 const PITCH_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; ··· 255 267 const W = screen.width; 256 268 const H = screen.height; 257 269 258 - // Attract mode: blink brightness between 0.4 and 1 every ~30 frames. 259 - const attract = !focused; 260 - const blinkPhase = (sin(frame * 0.1) + 1) / 2; // 0..1 261 - const attractPulse = attract ? 0.4 + blinkPhase * 0.6 : 1; 270 + // Attract mode: blink brightness between 0.4 and 1. 271 + const blinkPhase = (sin(frame * 0.12) + 1) / 2; // 0..1 272 + const attractPulse = focused ? 1 : 0.5 + blinkPhase * 0.5; 262 273 263 - // Palette — active uses lime/green, attract uses warning red. 264 274 const accent = focused 265 - ? [floor(140 + blinkPhase * 30), 255, floor(180 + blinkPhase * 40)] 266 - : [255, floor(80 + blinkPhase * 100), floor(80 + blinkPhase * 40)]; 275 + ? [floor(130 + blinkPhase * 40), 255, floor(170 + blinkPhase * 50)] // lime 276 + : [255, floor(70 + blinkPhase * 100), floor(70 + blinkPhase * 40)]; // red 267 277 const dim = [130, 140, 170]; 268 278 const fg = [220, 225, 255]; 269 279 const bgBase = focused ? [10, 20, 18] : [22, 10, 10]; 270 - wipe(floor(bgBase[0] * attractPulse), floor(bgBase[1] * attractPulse), floor(bgBase[2] * attractPulse)); 280 + wipe( 281 + floor(bgBase[0] * attractPulse), 282 + floor(bgBase[1] * attractPulse), 283 + floor(bgBase[2] * attractPulse), 284 + ); 271 285 286 + const sinceNote = frame - lastNoteFrame; 272 287 // Flash overlay on recent note-on. 273 - const sinceNote = frame - lastNoteFrame; 274 - if (lastNote && sinceNote < 12) { 275 - const f = 1 - sinceNote / 12; 276 - const alpha = floor(50 * f); 288 + if (lastNote && sinceNote < 10) { 289 + const f = 1 - sinceNote / 10; 290 + const alpha = floor(45 * f); 277 291 ink(...accent, alpha).box(0, 0, W, H, "fill"); 278 292 } 279 293 280 - // Title bar 281 - let y = 3; 294 + // ── Header row: piece name + ws state 295 + let y = 2; 282 296 ink(...accent).write("notepat-remote", { x: 4, y }); 283 297 const wsColor = 284 298 wsState === "open" ? [140, 255, 180] : ··· 287 301 ink(...wsColor).write(wsState, { x: W - wsState.length * 6 - 4, y }); 288 302 y += 10; 289 303 290 - // Focus state pill / attract message 304 + // ── Status row: ACTIVE / TAP ME + octave + last note 291 305 if (focused) { 292 - ink(...accent).box(4, y, 8, 8, "fill"); 293 - ink(...fg).write("ACTIVE", { x: 16, y }); 294 - ink(...dim).write(`oct ${baseOctave}`, { x: 64, y }); 295 - if (sources.length > 0) { 296 - const src = sources 297 - .slice(0, 2) 298 - .map((s) => s.handle ? "@" + s.handle : s.machineId.slice(0, 6)) 299 - .join(" "); 300 - ink(...dim).write(`relay ${src}`, { x: W - (src.length + 7) * 5, y }); 301 - } else if (relayCount > 0) { 302 - ink(...dim).write(`relay ${relayCount}`, { x: W - 60, y }); 303 - } 306 + ink(...accent).box(4, y + 1, 6, 6, "fill"); 307 + ink(...fg).write("ACTIVE", { x: 14, y }); 304 308 } else { 305 - // Blink red "TAP ME!" in the middle of the header area. 306 - const blinkOn = blinkPhase > 0.3; 307 - if (blinkOn) { 308 - ink(255, 40, 40).write("TAP ME!", { x: 16, y }); 309 - } else { 310 - ink(100, 20, 20).write("TAP ME!", { x: 16, y }); 311 - } 312 - ink(...dim).write(`oct ${baseOctave}`, { x: 64, y }); 309 + const blinkOn = blinkPhase > 0.35; 310 + ink(blinkOn ? 255 : 110, blinkOn ? 40 : 20, blinkOn ? 40 : 20) 311 + .write("TAP ME!", { x: 4, y }); 313 312 } 314 - y += 10; 315 - 316 - // Last note readout 313 + ink(...dim).write(`oct ${baseOctave}`, { x: 70, y }); 314 + // Right side: compact last-note readout 317 315 if (lastNote) { 318 316 const noteFresh = sinceNote < 30; 317 + const pn = pitchName(lastNote.pitch); 319 318 const noteColor = noteFresh ? accent : fg; 320 - const pn = pitchName(lastNote.pitch); 321 - const src = lastNote.source === "relay" 322 - ? `@${lastNote.handle || "?"}` 323 - : lastNote.source; 324 - const arrow = lastNote.vel === 0 ? "▽" : "▲"; 325 - ink(...noteColor).write(`${arrow} ${pn} (${lastNote.pitch}) ${src}`, { x: 4, y }); 326 - } else { 327 - ink(...dim).write("(no notes yet)", { x: 4, y }); 319 + const srcTag = 320 + lastNote.source === "relay" ? "@" + (lastNote.handle || "?") : 321 + lastNote.source === "tap" ? "tap" : "kbd"; 322 + const label = `${lastNote.vel === 0 ? "v" : "^"} ${pn} ${srcTag}`; 323 + ink(...noteColor).write(label, { x: W - label.length * 6 - 4, y }); 328 324 } 329 - y += 12; 325 + y += 10; 330 326 331 - // Layout the button grid — QWERTY rows mirroring a physical keyboard. 332 - const gridTop = y; 333 - const gridBottom = H - 12; 334 - const rowCount = KEY_ROWS.length; 335 - const rowH = floor((gridBottom - gridTop) / rowCount) - 2; 336 - const maxRowLen = KEY_ROWS.reduce((m, r) => Math.max(m, r.length), 0); 337 - const gridW = W - 8; 338 - const btnW = floor(gridW / maxRowLen); 327 + // ── Button grid area: two 4×3 octave blocks side-by-side 328 + const gridTop = y + 2; 329 + const gridBottom = H - 4; // leave 4px safe margin at bottom 330 + const gap = 6; 331 + const blockW = floor((W - 8 - gap) / 2); 332 + const blockH = gridBottom - gridTop; 333 + const cellW = floor(blockW / 4); 334 + const cellH = floor(blockH / 3); 339 335 340 336 buttons = []; 341 - for (let r = 0; r < rowCount; r += 1) { 342 - const row = KEY_ROWS[r]; 343 - const rowW = row.length * btnW; 344 - const rowX = 4 + floor((gridW - rowW) / 2) + r * 4; // slight indent per row for keyboard feel 345 - const rowY = gridTop + r * (rowH + 2); 346 - for (let i = 0; i < row.length; i += 1) { 347 - const key = row[i]; 348 - const pitch = pitchForKey(key); 349 - if (pitch === null) continue; 350 - const b = { 351 - x: rowX + i * btnW, 352 - y: rowY, 353 - w: btnW - 1, 354 - h: rowH, 355 - key, 356 - pitch, 357 - }; 358 - buttons.push(b); 337 + for (let octIdx = 0; octIdx < OCTAVE_GRIDS.length; octIdx += 1) { 338 + const grid = OCTAVE_GRIDS[octIdx]; 339 + const blockX = 4 + octIdx * (blockW + gap); 340 + // Octave number label in the corner of each block 341 + const octNum = baseOctave + octIdx; 342 + ink(...dim).write(`o${octNum}`, { x: blockX + 1, y: gridTop - 1 }); 343 + 344 + for (let rowIdx = 0; rowIdx < grid.length; rowIdx += 1) { 345 + const row = grid[rowIdx]; 346 + for (let colIdx = 0; colIdx < row.length; colIdx += 1) { 347 + const key = row[colIdx]; 348 + const offset = rowIdx * 4 + colIdx; 349 + const pitch = (baseOctave + 1 + octIdx) * 12 + offset; 350 + const b = { 351 + x: blockX + colIdx * cellW, 352 + y: gridTop + rowIdx * cellH, 353 + w: cellW - 1, 354 + h: cellH - 1, 355 + key, 356 + pitch, 357 + }; 358 + buttons.push(b); 359 + 360 + const held = 361 + heldKeys.has(key) || (tappedButton && tappedButton.key === key); 362 + const recentFlash = 363 + lastNote && lastNote.pitch === pitch && sinceNote < 18; 364 + const black = isBlackKey(pitch); 359 365 360 - // Draw 361 - const held = 362 - heldKeys.has(key) || 363 - (tappedButton && tappedButton.key === key); 364 - const recentFlash = lastNote && lastNote.pitch === pitch && sinceNote < 20; 365 - const black = isBlackKey(pitch); 366 + let fill; 367 + if (held) { 368 + fill = accent; 369 + } else if (recentFlash) { 370 + const f = 1 - sinceNote / 18; 371 + fill = [ 372 + floor(bgBase[0] + (accent[0] - bgBase[0]) * f * 0.6), 373 + floor(bgBase[1] + (accent[1] - bgBase[1]) * f * 0.6), 374 + floor(bgBase[2] + (accent[2] - bgBase[2]) * f * 0.6), 375 + ]; 376 + } else if (black) { 377 + fill = focused ? [22, 32, 28] : [48, 18, 18]; 378 + } else { 379 + fill = focused ? [42, 52, 48] : [70, 28, 28]; 380 + } 381 + ink(...fill).box(b.x, b.y, b.w, b.h, "fill"); 382 + ink(...(held ? accent : [80, 90, 100])).box(b.x, b.y, b.w, b.h, "outline"); 366 383 367 - let fill; 368 - if (held) { 369 - fill = accent; 370 - } else if (recentFlash) { 371 - const f = 1 - sinceNote / 20; 372 - fill = [ 373 - floor(bgBase[0] + (accent[0] - bgBase[0]) * f * 0.7), 374 - floor(bgBase[1] + (accent[1] - bgBase[1]) * f * 0.7), 375 - floor(bgBase[2] + (accent[2] - bgBase[2]) * f * 0.7), 376 - ]; 377 - } else if (black) { 378 - fill = focused ? [28, 38, 34] : [50, 20, 20]; 379 - } else { 380 - fill = focused ? [48, 58, 55] : [72, 30, 30]; 384 + // Letter centered in cell 385 + const labelX = b.x + floor(b.w / 2) - 2; 386 + const labelY = b.y + floor(b.h / 2) - 4; 387 + const labelColor = held 388 + ? [10, 20, 10] 389 + : black 390 + ? [200, 210, 220] 391 + : fg; 392 + ink(...labelColor).write(key.toUpperCase(), { x: labelX, y: labelY }); 381 393 } 382 - ink(...fill).box(b.x, b.y, b.w, b.h, "fill"); 383 - // Outline 384 - ink(...(held ? accent : [90, 100, 110])).box(b.x, b.y, b.w, b.h, "outline"); 385 - // Letter label centered 386 - const letterX = b.x + floor(b.w / 2) - 2; 387 - const letterY = b.y + floor(b.h / 2) - 4; 388 - const letterColor = held ? [10, 20, 10] : (black ? [200, 210, 220] : fg); 389 - ink(...letterColor).write(key.toUpperCase(), { x: letterX, y: letterY }); 390 394 } 391 - } 392 - 393 - // Footer hint 394 - const hintY = H - 8; 395 - if (focused) { 396 - ink(...dim).write(`1-9=oct ${baseOctave}`, { x: 4, y: hintY }); 397 - } else { 398 - ink(255, 80, 80).write("click device to play!", { x: 4, y: hintY }); 399 395 } 400 396 } 401 397
system/public/m4l/notepat-remote.amxd

This is a binary file and will not be displayed.