Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

notepat-remote: rainbow pad colors + pixel-perfect grid

Pad fills now use the shared ROYGBIV palette from lib/note-colors.mjs so
the M4L remote matches notepat.mjs visually. Labels show note names (C,
C#, D…) centered, with a small keyboard key hint in the bottom-right of
each pad. Grid math switched to pre-computed col/row edges (floor(i*W/N))
so adjacent pads share exact boundaries — no 1px gaps or outer margins.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+81 -28
+81 -28
system/public/aesthetic.computer/disks/notepat-remote.mjs
··· 14 14 // • When the iframe loses focus, Max's key listener stops receiving, so 15 15 // the UI goes red + "TAP ME!" attract mode to prompt a click. 16 16 17 + import { getNoteColorForOctave } from "../lib/note-colors.mjs"; 18 + 17 19 const { floor, min, max, abs, sin, PI } = Math; 18 20 19 21 const WS_URL = "wss://session-server.aesthetic.computer/"; ··· 51 53 const PITCH_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; 52 54 function pitchName(p) { 53 55 return PITCH_NAMES[((p % 12) + 12) % 12] + (floor(p / 12) - 1); 56 + } 57 + // Short pitch name (no octave suffix) for tight pad labels. 58 + function pitchNameShort(p) { 59 + return PITCH_NAMES[((p % 12) + 12) % 12]; 60 + } 61 + // MIDI octave number (C4 = 60 in standard MIDI). 62 + function pitchOctave(p) { 63 + return floor(p / 12) - 1; 54 64 } 55 65 function isBlackKey(p) { 56 66 return [1, 3, 6, 8, 10].includes(((p % 12) + 12) % 12); ··· 418 428 y += 10; 419 429 420 430 // ── Button grid area: two 4×3 octave blocks side-by-side ───────────── 421 - const gridTop = y + 2; 422 - const gridBottom = H - 4; 423 - const gap = 6; 424 - const blockW = floor((W - 8 - gap) / 2); 425 - const blockH = gridBottom - gridTop; 426 - const cellW = floor(blockW / 4); 427 - const cellH = floor(blockH / 3); 431 + // Pixel-perfect: spans full width (0..W), fills from gridTop down to H. 432 + // Pads butt up against each other — rainbow palette gives enough natural 433 + // contrast that explicit separators add noise. 434 + const gridTop = y + 1; 435 + // Pre-compute row and column extents so every pixel is accounted for and 436 + // adjacent pads share the same boundary line (no 1px gaps or overlaps). 437 + const cols = 8; // 4 pads × 2 octave blocks 438 + const rows = 3; 439 + const colEdges = []; 440 + for (let c = 0; c <= cols; c += 1) colEdges.push(floor((c * W) / cols)); 441 + const rowEdges = []; 442 + for (let r = 0; r <= rows; r += 1) rowEdges.push(gridTop + floor((r * (H - gridTop)) / rows)); 428 443 429 444 buttons = []; 430 445 for (let octIdx = 0; octIdx < OCTAVE_GRIDS.length; octIdx += 1) { 431 446 const grid = OCTAVE_GRIDS[octIdx]; 432 - const blockX = 4 + octIdx * (blockW + gap); 433 447 const octNum = baseOctave + octIdx; 434 - ink(...dim).write(`o${octNum}`, { x: blockX + 1, y: gridTop - 1 }); 435 448 436 449 for (let rowIdx = 0; rowIdx < grid.length; rowIdx += 1) { 437 450 const row = grid[rowIdx]; ··· 439 452 const key = row[colIdx]; 440 453 const offset = rowIdx * 4 + colIdx; 441 454 const pitch = (baseOctave + 1 + octIdx) * 12 + offset; 442 - const b = { 443 - x: blockX + colIdx * cellW, 444 - y: gridTop + rowIdx * cellH, 445 - w: cellW - 1, 446 - h: cellH - 1, 447 - key, 448 - pitch, 449 - }; 455 + const globalCol = octIdx * 4 + colIdx; 456 + const x0 = colEdges[globalCol]; 457 + const x1 = colEdges[globalCol + 1]; 458 + const y0 = rowEdges[rowIdx]; 459 + const y1 = rowEdges[rowIdx + 1]; 460 + const b = { x: x0, y: y0, w: x1 - x0, h: y1 - y0, key, pitch }; 450 461 buttons.push(b); 451 462 452 463 const held = ··· 455 466 lastNote && lastNote.pitch === pitch && sinceNote < 18; 456 467 const black = isBlackKey(pitch); 457 468 469 + // Rainbow ROYGBIV per note from note-colors.mjs — matches 470 + // notepat.mjs so the packed remote visually maps onto the main 471 + // piece's pad palette. Sharps/flats render black. 472 + const nameShort = pitchNameShort(pitch); 473 + const noteOctave = pitchOctave(pitch); 474 + const baseColor = black 475 + ? [20, 22, 28] 476 + : getNoteColorForOctave(nameShort.toLowerCase(), noteOctave, baseOctave); 477 + 458 478 let fill; 459 479 if (held && focused) { 460 - fill = accent; 480 + // Fully saturated note color when pressed. 481 + fill = baseColor; 461 482 } else if (recentFlash && focused) { 483 + // Note color blended in at recent-flash intensity. 462 484 const f = 1 - sinceNote / 18; 463 485 fill = [ 464 - floor(bgBase[0] + (accent[0] - bgBase[0]) * f * 0.5), 465 - floor(bgBase[1] + (accent[1] - bgBase[1]) * f * 0.5), 466 - floor(bgBase[2] + (accent[2] - bgBase[2]) * f * 0.5), 486 + floor(bgBase[0] + (baseColor[0] - bgBase[0]) * (0.35 + f * 0.5)), 487 + floor(bgBase[1] + (baseColor[1] - bgBase[1]) * (0.35 + f * 0.5)), 488 + floor(bgBase[2] + (baseColor[2] - bgBase[2]) * (0.35 + f * 0.5)), 489 + ]; 490 + } else if (focused) { 491 + // Resting state: muted version of note color so the rainbow is 492 + // still readable without overwhelming the unpressed pads. 493 + fill = [ 494 + floor(bgBase[0] + (baseColor[0] - bgBase[0]) * 0.35), 495 + floor(bgBase[1] + (baseColor[1] - bgBase[1]) * 0.35), 496 + floor(bgBase[2] + (baseColor[2] - bgBase[2]) * 0.35), 467 497 ]; 468 498 } else { 469 499 fill = black ? keyBlack : keyWhite; 470 500 } 471 501 ink(...fill).box(b.x, b.y, b.w, b.h, "fill"); 472 - ink(...(held && focused ? accent : outline)) 473 - .box(b.x, b.y, b.w, b.h, "outline"); 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), 511 + ]; 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 + } 474 518 475 - const labelX = b.x + floor(b.w / 2) - 2; 476 - const labelY = b.y + floor(b.h / 2) - 4; 519 + // Main label = note name (C, C#, D…), small hint = keyboard key. 520 + 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; 477 523 const labelColor = 478 - held && focused ? [10, 16, 10] : 479 - black ? [200, 210, 220] : fg; 480 - ink(...labelColor).write(key.toUpperCase(), { x: labelX, y: labelY }); 524 + held && focused ? [10, 10, 14] : 525 + black ? [220, 225, 235] : [20, 22, 28]; 526 + 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; 530 + const hintColor = 531 + held && focused ? [10, 10, 14, 200] : 532 + black ? [180, 180, 190, 180] : [40, 44, 52, 180]; 533 + ink(...hintColor).write(key.toUpperCase(), { x: hintX, y: hintY }); 481 534 } 482 535 } 483 536 }
system/public/m4l/notepat-remote.amxd

This is a binary file and will not be displayed.