Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

merge stoic-meninsky: notepat m4l/os button positioning + drop piano roll

+50 -338
+50 -338
system/public/aesthetic.computer/disks/notepat.mjs
··· 524 524 const SECONDARY_BAR_HEIGHT = 12; 525 525 const SECONDARY_BAR_BOTTOM = SECONDARY_BAR_TOP + SECONDARY_BAR_HEIGHT; 526 526 527 - // OS bar — thin strip below the secondary bar for the "x86 os" button 528 - const OS_BAR_TOP = SECONDARY_BAR_BOTTOM; 529 - const OS_BAR_HEIGHT = 12; 530 - const OS_BAR_BOTTOM = OS_BAR_TOP + OS_BAR_HEIGHT; 531 - 532 527 const TOGGLE_BTN_PADDING_X = 2; 533 528 const TOGGLE_BTN_PADDING_Y = 2; 534 529 const TOGGLE_BTN_GAP = 3; // At least 1px visible gap between buttons ··· 676 671 function getTopBarPianoMetrics(screen) { 677 672 const topPianoY = 3; 678 673 const topPianoHeight = 15; 679 - // Push piano right when .com superscript is shown to avoid overlap with HUD label 680 - const topPianoStartX = dotComMode ? 75 : 54; 681 - // Cap the piano's right edge at the leftmost top-bar button (os/m4l/wave/oct) 682 - // so keys never slide under the button strip on narrow screens. 683 - const leftmostButtonX = Math.min( 684 - osBtn?.box?.x ?? Infinity, 685 - abletonBtn?.box?.x ?? Infinity, 674 + // Piano begins after the m4l/os buttons (which sit right of notepat.com). 675 + const leftSideEnd = Math.max( 676 + abletonBtn?.box ? abletonBtn.box.x + abletonBtn.box.w : 0, 677 + osBtn?.box ? osBtn.box.x + osBtn.box.w : 0, 678 + ); 679 + const defaultPianoStart = dotComMode ? 75 : 54; 680 + const topPianoStartX = Math.max(defaultPianoStart, leftSideEnd + 3); 681 + // Cap the piano's right edge at the leftmost right-side button (wave/oct). 682 + const leftmostRightButtonX = Math.min( 686 683 waveBtn?.box?.x ?? Infinity, 687 684 octBtn?.box?.x ?? Infinity, 688 685 ); 689 - const rightEdge = Number.isFinite(leftmostButtonX) 690 - ? leftmostButtonX - 3 686 + const rightEdge = Number.isFinite(leftmostRightButtonX) 687 + ? leftmostRightButtonX - 3 691 688 : screen.width; 692 689 const availableWidth = Math.max(0, rightEdge - topPianoStartX); 693 690 ··· 1081 1078 1082 1079 const trail = {}; 1083 1080 1084 - // 🎹 Piano roll history - pixel timeline of held notes 1085 - const PIANO_ROLL_WIDTH = 400; // pixels of history (larger buffer for more context) 1086 - const PIANO_ROLL_SCROLL_DIVISOR = 2; // Only scroll every N frames (lower = faster scrolling) 1087 - let pianoRollFrameCounter = 0; // Frame counter for scroll timing 1088 - let pianoRollScrollPosition = 0; // Total pixels scrolled (for beat marker sync) 1089 - const pianoRollHistory = buttonNotes.map(() => new Uint8Array(PIANO_ROLL_WIDTH)); // 1 row per note, 0=off, 1=on 1090 - // 🥁 Beat marker history - 0=no beat, 1=regular beat, 2=downbeat (every 4) 1091 - const pianoRollBeatHistory = new Uint8Array(PIANO_ROLL_WIDTH); 1092 - 1093 1081 // 🔬 Telemetry: Expose trail for stability testing 1094 1082 if (typeof window !== 'undefined') { 1095 1083 window.__notepat_trail = trail; ··· 1822 1810 metronomeVisualPhase = 1.0; // Flash on beat 1823 1811 metronomeFlash = 1.0; // Trigger peripheral screen flash 1824 1812 1825 - // 🥁 Record beat in piano roll history (downbeat=2, regular=1) 1826 - const isDownbeat = (beatNumber % 4) === 0; 1827 - pianoRollBeatHistory[PIANO_ROLL_WIDTH - 1] = isDownbeat ? 2 : 1; 1828 - 1829 1813 // Play metronome click sound 1830 1814 // Accent on beat 1 of each measure (every 4 beats) 1815 + const isDownbeat = (beatNumber % 4) === 0; 1831 1816 const clickFreq = isDownbeat ? 1200 : 800; // Higher pitch for downbeat 1832 1817 const clickVol = isDownbeat ? 0.4 : 0.25; 1833 1818 ··· 2138 2123 (qKeyWidth + qKeySpacing); 2139 2124 const qwertyHeight = QWERTY_MINIMAP_HEIGHT; 2140 2125 2141 - let pianoY = OS_BAR_BOTTOM; 2126 + let pianoY = SECONDARY_BAR_BOTTOM; 2142 2127 let pianoStartX = 58; 2143 2128 const centerX = layout?.centerX ?? (effectiveWidth - pianoWidth) / 2; 2144 2129 const centerWidth = layout?.centerAreaWidth ?? pianoWidth; ··· 2155 2140 const rotatedPianoWidth = whiteKeyHeight + 2; // Keys drawn horizontally but stacked vertically 2156 2141 const rotatedPianoHeight = MINI_PIANO_WHITE_KEYS.length * whiteKeyWidth; // Full piano height when rotated 2157 2142 pianoStartX = gridLeft + gridWidth + 4; // Gap from grid 2158 - pianoY = layout?.topButtonY || OS_BAR_BOTTOM; 2143 + pianoY = layout?.topButtonY || SECONDARY_BAR_BOTTOM; 2159 2144 2160 2145 // Check if rotated piano fits horizontally (x + width within screen) 2161 2146 const pianoRight = pianoStartX + rotatedPianoWidth; ··· 2199 2184 const gridWidth = (layout?.buttonsPerRow || 4) * (layout?.buttonWidth || 20) + (layout?.margin || 2) * 2; 2200 2185 const gridLeft = layout?.margin || 2; 2201 2186 pianoStartX = gridLeft + gridWidth + 8; // 8px gap from grid 2202 - pianoY = layout?.topButtonY || OS_BAR_BOTTOM; 2187 + pianoY = layout?.topButtonY || SECONDARY_BAR_BOTTOM; 2203 2188 2204 2189 // Check if piano fits horizontally 2205 2190 const pianoRight = pianoStartX + pianoWidth; ··· 2237 2222 } 2238 2223 2239 2224 if (isCompact) { 2240 - pianoY = OS_BAR_BOTTOM + 2; 2225 + pianoY = SECONDARY_BAR_BOTTOM + 2; 2241 2226 // Check if piano fits in center area - if not, skip it (return hidden flag) 2242 2227 if (centerWidth < pianoWidth + 4) { 2243 2228 // Piano doesn't fit - hide it but still compute QWERTY position for center ··· 2264 2249 const maxX = Math.max(minX, centerRight - pianoWidth); 2265 2250 pianoStartX = clamp(idealX, minX, maxX); 2266 2251 } else if (song) { 2267 - const effectiveTrackY = trackY ?? OS_BAR_BOTTOM; 2252 + const effectiveTrackY = trackY ?? SECONDARY_BAR_BOTTOM; 2268 2253 pianoY = effectiveTrackY + (trackHeight || 0) + 2; 2269 2254 pianoStartX = effectiveWidth - pianoWidth - 2; 2270 2255 } else { ··· 2518 2503 const notesPerSide = 12; 2519 2504 const buttonsPerRow = 4; // 4 notes per row on each side 2520 2505 const totalRows = Math.ceil(notesPerSide / buttonsPerRow); // 3 rows 2521 - const hudReserved = OS_BAR_BOTTOM; 2506 + const hudReserved = SECONDARY_BAR_BOTTOM; 2522 2507 2523 2508 // Piano dimensions (extended mini layout) 2524 2509 const whiteKeyWidth = getMiniPianoWhiteKeyWidth(true); ··· 2658 2643 }; 2659 2644 } 2660 2645 2661 - const hudReserved = OS_BAR_BOTTOM; 2646 + const hudReserved = SECONDARY_BAR_BOTTOM; 2662 2647 const trackHeight = songMode ? TRACK_HEIGHT : 0; 2663 2648 const trackSpacing = songMode ? TRACK_GAP : 0; 2664 2649 const baseReservedTop = hudReserved + trackHeight + trackSpacing; ··· 2679 2664 // Rotated piano: width = MINI_KEYBOARD_HEIGHT, height = pianoWidth (all keys stacked) 2680 2665 const rotatedPianoWidth = MINI_KEYBOARD_HEIGHT + 4; // Piano keys become vertical 2681 2666 const rotatedPianoHeight = pianoWidth; // Height needed to fit ALL white keys 2682 - const availableHeightForRotated = screen.height - OS_BAR_BOTTOM - 4; // Leave some margin 2667 + const availableHeightForRotated = screen.height - SECONDARY_BAR_BOTTOM - 4; // Leave some margin 2683 2668 // Only show rotated piano if ALL keys fit vertically 2684 2669 const narrowVerticalSpace = !horizontalSpaceForMini && 2685 2670 usableWidth - gridWidthEstimate - rotatedPianoWidth > 4 && ··· 3121 3106 wipe(0, 0, 0, 0); 3122 3107 } else if (kidlispBgEnabled && kidlispBackground && !paintPictureOverlay) { 3123 3108 wipe(bg); // Base background first 3124 - const klY = OS_BAR_BOTTOM; 3109 + const klY = SECONDARY_BAR_BOTTOM; 3125 3110 const klBottom = earlyLayout.topButtonY; 3126 3111 const klH = klBottom - klY; 3127 3112 if (klH > 10) { ··· 3399 3384 // 🎚️ Room parameter bar - appears when room mode is enabled 3400 3385 if (roomMode && roomBtn?.box) { 3401 3386 const barHeight = 6; 3402 - const barY = OS_BAR_BOTTOM + 2; 3387 + const barY = SECONDARY_BAR_BOTTOM + 2; 3403 3388 const barX = roomBtn.box.x; 3404 3389 const barWidth = Math.max(40, roomBtn.box.w * 2); 3405 3390 const fillWidth = Math.floor(barWidth * roomAmount); ··· 3706 3691 // In single-column mode (non-split), hide horizontal track entirely - only show vertical track if available 3707 3692 const showHorizontalTrack = showTrack && !useVerticalTrack && layout.splitLayout; 3708 3693 const trackHeight = showHorizontalTrack ? TRACK_HEIGHT : 0; 3709 - const trackY = showHorizontalTrack ? OS_BAR_BOTTOM : null; 3694 + const trackY = showHorizontalTrack ? SECONDARY_BAR_BOTTOM : null; 3710 3695 3711 3696 3712 3697 if (showHorizontalTrack) { ··· 5176 5161 // Use layout metrics to find a safe spot 5177 5162 const padTop = layout?.topButtonY || (screen.height - 120); 5178 5163 const osdX = screen.width - osdWidth - 4; 5179 - const osdY = Math.max(OS_BAR_BOTTOM + 2, padTop - osdHeight - 4); 5164 + const osdY = Math.max(SECONDARY_BAR_BOTTOM + 2, padTop - osdHeight - 4); 5180 5165 5181 5166 // Semi-transparent background 5182 5167 ink(0, 0, 0, 210).box(osdX - 2, osdY - 2, osdWidth, osdHeight); ··· 5229 5214 writeRow("white", `TOTAL EST: ${totalStr}`); 5230 5215 } 5231 5216 5232 - // 🎹 Piano Roll Timeline - pixel-by-pixel held note history 5233 - // Can be horizontal (bottom-right) or vertical (center/right side based on layout) 5234 - // In single-column portrait mode, skip the horizontal piano roll entirely 5235 - if (!paintPictureOverlay && !projector && !recitalMode) { 5236 - const noteCount = buttonNotes.length; // 24 notes 5237 - 5238 - // Increment frame counter and check if we should scroll this frame 5239 - pianoRollFrameCounter = (pianoRollFrameCounter + 1) % PIANO_ROLL_SCROLL_DIVISOR; 5240 - const shouldScrollPianoRoll = pianoRollFrameCounter === 0; 5241 - 5242 - // Determine if we should use vertical layout based on available space 5243 - // In landscape mode, always prefer vertical roll on the right side 5244 - const isLandscapeScreen = screen.width > screen.height; 5245 - const useVerticalRoll = layout.splitLayout || layout.verticalTrack || isLandscapeScreen; 5246 - 5247 - // In single-column mode (non-split, portrait), hide piano roll entirely 5248 - const isSingleColumnPortrait = !layout.splitLayout && !isLandscapeScreen; 5249 - 5250 - if (useVerticalRoll) { 5251 - // 🎹 VERTICAL Piano Roll - time flows downward, notes spread horizontally 5252 - // Position: center of split layout, or right of single column 5253 - let rollX, rollY, rollW, rollH; 5254 - let skipRoll = false; 5255 - 5256 - if (layout.splitLayout) { 5257 - // In split layout: ONLY draw in center area, never on right side 5258 - if (layout.centerAreaWidth > noteCount + 4) { 5259 - // Expand to fill center area width 5260 - rollW = Math.max(noteCount, layout.centerAreaWidth - 8); 5261 - rollH = Math.min(PIANO_ROLL_WIDTH, layout.verticalTrackHeight || (screen.height - layout.hudReserved - layout.bottomPadding - 4)); 5262 - rollX = layout.centerX + Math.floor((layout.centerAreaWidth - rollW) / 2); 5263 - rollY = layout.hudReserved + 2; 5264 - } else { 5265 - // Not enough space in center - skip drawing entirely in split mode 5266 - skipRoll = true; 5267 - } 5268 - } else if (layout.verticalTrack) { 5269 - // Single column with vertical track space 5270 - rollW = Math.min(noteCount, layout.verticalTrackWidth || 36); 5271 - rollH = Math.min(PIANO_ROLL_WIDTH, screen.height - layout.hudReserved - layout.bottomPadding - 4); 5272 - rollX = layout.verticalTrackX || (screen.width - rollW - 2); 5273 - rollY = layout.verticalTrackY || layout.hudReserved + 2; 5274 - } else { 5275 - // Fallback for landscape: right side vertical roll 5276 - // Calculate available space on the right of the button grid 5277 - const gridWidth = layout.buttonsPerRow * layout.buttonWidth; 5278 - const availableRight = screen.width - gridWidth - layout.margin * 3; 5279 - rollW = Math.min(noteCount, Math.max(24, availableRight)); 5280 - rollH = Math.min(PIANO_ROLL_WIDTH, screen.height - (layout.hudReserved || 34) - (layout.bottomPadding || 2) - 4); 5281 - rollX = screen.width - rollW - 2; 5282 - rollY = (layout.hudReserved || 34) + 2; 5283 - } 5284 - 5285 - // Skip all drawing if there's no space (split mode with narrow center) 5286 - if (skipRoll) { 5287 - // Still update history even when not drawing 5288 - if (shouldScrollPianoRoll) { 5289 - pianoRollScrollPosition++; 5290 - for (let y = 0; y < PIANO_ROLL_WIDTH - 1; y++) { 5291 - pianoRollBeatHistory[y] = pianoRollBeatHistory[y + 1]; 5292 - } 5293 - pianoRollBeatHistory[PIANO_ROLL_WIDTH - 1] = 0; 5294 - for (let noteIdx = 0; noteIdx < noteCount; noteIdx++) { 5295 - const row = pianoRollHistory[noteIdx]; 5296 - for (let y = 0; y < PIANO_ROLL_WIDTH - 1; y++) { 5297 - row[y] = row[y + 1]; 5298 - } 5299 - const note = buttonNotes[noteIdx]; 5300 - row[PIANO_ROLL_WIDTH - 1] = sounds[note] !== undefined ? 1 : 0; 5301 - } 5302 - } else { 5303 - for (let noteIdx = 0; noteIdx < noteCount; noteIdx++) { 5304 - const note = buttonNotes[noteIdx]; 5305 - if (sounds[note] !== undefined) { 5306 - pianoRollHistory[noteIdx][PIANO_ROLL_WIDTH - 1] = 1; 5307 - } 5308 - } 5309 - } 5310 - } else { 5311 - // Update history: shift all rows up, add current state at bottom 5312 - // Only scroll on designated frames for slower movement 5313 - if (shouldScrollPianoRoll) { 5314 - pianoRollScrollPosition++; // Track total scroll for beat marker sync 5315 - 5316 - // Shift beat history 5317 - for (let y = 0; y < PIANO_ROLL_WIDTH - 1; y++) { 5318 - pianoRollBeatHistory[y] = pianoRollBeatHistory[y + 1]; 5319 - } 5320 - pianoRollBeatHistory[PIANO_ROLL_WIDTH - 1] = 0; // Clear newest slot (will be set by metronome tick) 5321 - 5322 - for (let noteIdx = 0; noteIdx < noteCount; noteIdx++) { 5323 - const row = pianoRollHistory[noteIdx]; 5324 - // Shift up by 1 pixel (older history moves toward index 0) 5325 - for (let y = 0; y < PIANO_ROLL_WIDTH - 1; y++) { 5326 - row[y] = row[y + 1]; 5327 - } 5328 - // Set bottom pixel based on current note state 5329 - const note = buttonNotes[noteIdx]; 5330 - row[PIANO_ROLL_WIDTH - 1] = sounds[note] !== undefined ? 1 : 0; 5331 - } 5332 - } else { 5333 - // Even when not scrolling, update the current pixel to reflect held notes 5334 - for (let noteIdx = 0; noteIdx < noteCount; noteIdx++) { 5335 - const note = buttonNotes[noteIdx]; 5336 - if (sounds[note] !== undefined) { 5337 - pianoRollHistory[noteIdx][PIANO_ROLL_WIDTH - 1] = 1; 5338 - } 5339 - } 5340 - } 5341 - 5342 - // 🎹 Draw mini piano keys above the track to show note relationship 5343 - const miniKeyH = 4; // Height of mini piano keys 5344 - const pianoY = rollY; // Piano at top of roll area 5345 - const trackStartY = rollY + miniKeyH + 1; // Track starts below piano 5346 - const actualRollH = rollH - miniKeyH - 1; // Adjust roll height for piano 5347 - 5348 - // Calculate note width to fill the available roll width 5349 - const noteWidth = Math.max(1, Math.floor(rollW / noteCount)); 5350 - const actualNoteAreaW = noteWidth * noteCount; // Actual width used by notes 5351 - const noteAreaX = rollX + Math.floor((rollW - actualNoteAreaW) / 2); // Center notes in roll 5352 - 5353 - // Draw subtle background for the whole area 5354 - ink(10, 10, 15, 180).box(rollX, rollY, rollW, rollH); 5355 - 5356 - // Draw mini piano keys at the top (white keys full color, black keys lighter) 5357 - for (let noteIdx = 0; noteIdx < noteCount; noteIdx++) { 5358 - const note = buttonNotes[noteIdx]; 5359 - const x = noteAreaX + noteIdx * noteWidth; 5360 - const baseColor = getCachedColor(note, num); 5361 - const isActive = sounds[note] !== undefined; 5362 - const isBlack = note.includes('#'); 5363 - 5364 - if (isBlack) { 5365 - // Black keys: lighter version (0.55 instead of 0.3 for better visibility) 5366 - const lite = [Math.floor(baseColor[0] * 0.55), Math.floor(baseColor[1] * 0.55), Math.floor(baseColor[2] * 0.55)]; 5367 - ink(lite[0], lite[1], lite[2], isActive ? 255 : 200).box(x, pianoY, noteWidth, miniKeyH); 5368 - } else { 5369 - // White keys: full or slightly dimmed 5370 - ink(baseColor[0], baseColor[1], baseColor[2], isActive ? 255 : 140).box(x, pianoY, noteWidth, miniKeyH); 5371 - } 5372 - // Flash bright when active 5373 - if (isActive) { 5374 - ink(255, 255, 255, 120).box(x, pianoY, noteWidth, miniKeyH); 5375 - } 5376 - } 5377 - 5378 - // 🎨 Draw colored "groove" indicators - full height desaturated note colors 5379 - for (let noteIdx = 0; noteIdx < noteCount; noteIdx++) { 5380 - const note = buttonNotes[noteIdx]; 5381 - const x = noteAreaX + noteIdx * noteWidth; 5382 - const baseColor = getCachedColor(note, num); 5383 - // Desaturate: blend toward gray (reduce saturation by ~70%) 5384 - const gray = (baseColor[0] + baseColor[1] + baseColor[2]) / 3; 5385 - const desat = [ 5386 - Math.round(baseColor[0] * 0.3 + gray * 0.7), 5387 - Math.round(baseColor[1] * 0.3 + gray * 0.7), 5388 - Math.round(baseColor[2] * 0.3 + gray * 0.7), 5389 - ]; 5390 - // Full height groove for each note lane 5391 - ink(desat[0], desat[1], desat[2], 35).box(x, trackStartY, noteWidth, actualRollH); 5392 - } 5393 - 5394 - // Draw each pixel - vertical layout (time = Y axis, notes = X axis) 5395 - // Optimized: pre-calculate base offset and skip empty pixels efficiently 5396 - const histBaseIdx = PIANO_ROLL_WIDTH - actualRollH; 5397 - for (let noteIdx = 0; noteIdx < noteCount; noteIdx++) { 5398 - const row = pianoRollHistory[noteIdx]; 5399 - const x = noteAreaX + noteIdx * noteWidth; 5400 - // Cache color once per note column 5401 - const baseColor = getCachedColor(buttonNotes[noteIdx], num); 5402 - const r = baseColor[0], g = baseColor[1], b = baseColor[2]; 5403 - 5404 - // Only iterate if this note has any history 5405 - for (let yOff = 0; yOff < actualRollH; yOff++) { 5406 - if (row[histBaseIdx + yOff]) { 5407 - // Fade older notes (top = older) - simplified alpha calc 5408 - const alpha = Math.max(80, 255 - ((actualRollH - yOff) << 1)); 5409 - ink(r, g, b, alpha).box(x, trackStartY + yOff, noteWidth, 1); 5410 - } 5411 - } 5412 - } 5413 - 5414 - // Draw subtle grid lines for octave separation (vertical lines at note 12) 5415 - ink(40, 40, 50, 100).line(noteAreaX + 12 * noteWidth, trackStartY, noteAreaX + 12 * noteWidth, trackStartY + actualRollH - 1); 5416 - 5417 - // 🥁 Draw horizontal beat markers from recorded history 5418 - // These are actual metronome beats, not calculated intervals 5419 - // (reuse histBaseIdx from above) 5420 - for (let yOff = 0; yOff < actualRollH; yOff++) { 5421 - const beatValue = pianoRollBeatHistory[histBaseIdx + yOff]; 5422 - if (beatValue > 0) { 5423 - const lineY = trackStartY + yOff; 5424 - const isDownbeat = beatValue === 2; 5425 - // Current beat (at bottom) blinks 5426 - const isCurrentBeat = yOff === actualRollH - 1 && beatValue > 0; 5427 - const blinkAlpha = isCurrentBeat ? Math.floor(140 + Math.sin(metronomeVisualPhase * Math.PI * 2) * 80) : 0; 5428 - const baseAlpha = isDownbeat ? 120 : 55; 5429 - const alpha = isCurrentBeat ? Math.max(blinkAlpha, baseAlpha) : baseAlpha; 5430 - const color = isDownbeat ? [140, 160, 200] : [90, 100, 130]; 5431 - ink(color[0], color[1], color[2], alpha).line(rollX, lineY, rollX + rollW - 1, lineY); 5432 - } 5433 - } 5434 - } // End of skipRoll else block 5435 - 5436 - } else if (!isSingleColumnPortrait) { 5437 - // 🎹 HORIZONTAL Piano Roll (original) - time flows left-to-right, notes stacked vertically 5438 - // Skip in single-column portrait mode to avoid cluttering the compact layout 5439 - const rollHeight = noteCount; 5440 - const rollWidth = Math.min(PIANO_ROLL_WIDTH, screen.width); 5441 - const rollY = screen.height - rollHeight - 1; 5442 - const rollX = screen.width - rollWidth; 5443 - 5444 - // Update history: shift all columns left, add current state on right 5445 - // Only scroll on designated frames for slower movement 5446 - if (shouldScrollPianoRoll) { 5447 - for (let noteIdx = 0; noteIdx < noteCount; noteIdx++) { 5448 - const row = pianoRollHistory[noteIdx]; 5449 - // Shift left by 1 pixel 5450 - for (let x = 0; x < PIANO_ROLL_WIDTH - 1; x++) { 5451 - row[x] = row[x + 1]; 5452 - } 5453 - // Set rightmost pixel based on current note state 5454 - const note = buttonNotes[noteIdx]; 5455 - row[PIANO_ROLL_WIDTH - 1] = sounds[note] !== undefined ? 1 : 0; 5456 - } 5457 - } else { 5458 - // Even when not scrolling, update the current pixel to reflect held notes 5459 - for (let noteIdx = 0; noteIdx < noteCount; noteIdx++) { 5460 - const note = buttonNotes[noteIdx]; 5461 - if (sounds[note] !== undefined) { 5462 - pianoRollHistory[noteIdx][PIANO_ROLL_WIDTH - 1] = 1; 5463 - } 5464 - } 5465 - } 5466 - 5467 - // Draw subtle background 5468 - ink(10, 10, 15, 180).box(rollX, rollY, rollWidth, rollHeight); 5469 - 5470 - // 🎨 Draw colored "groove" indicators - full length desaturated note colors 5471 - for (let noteIdx = 0; noteIdx < noteCount; noteIdx++) { 5472 - const note = buttonNotes[noteIdx]; 5473 - const y = rollY + noteIdx; 5474 - const baseColor = getCachedColor(note, num); 5475 - // Desaturate: blend toward gray 5476 - const gray = (baseColor[0] + baseColor[1] + baseColor[2]) / 3; 5477 - const desat = [ 5478 - Math.round(baseColor[0] * 0.4 + gray * 0.6), 5479 - Math.round(baseColor[1] * 0.4 + gray * 0.6), 5480 - Math.round(baseColor[2] * 0.4 + gray * 0.6), 5481 - ]; 5482 - // Full length groove for each note row 5483 - ink(desat[0], desat[1], desat[2], 40).box(rollX, y, rollWidth, 1); 5484 - } 5485 - 5486 - // Draw each pixel 5487 - for (let noteIdx = 0; noteIdx < noteCount; noteIdx++) { 5488 - const note = buttonNotes[noteIdx]; 5489 - const row = pianoRollHistory[noteIdx]; 5490 - const y = rollY + noteIdx; 5491 - const baseColor = getCachedColor(note, num); 5492 - 5493 - for (let x = 0; x < rollWidth; x++) { 5494 - const histIdx = PIANO_ROLL_WIDTH - rollWidth + x; 5495 - if (row[histIdx]) { 5496 - // Fade older notes slightly 5497 - const age = rollWidth - x; 5498 - const alpha = Math.max(80, 255 - age * 2); 5499 - ink(baseColor[0], baseColor[1], baseColor[2], alpha).box(rollX + x, y, 1, 1); 5500 - } 5501 - } 5502 - } 5503 - 5504 - // Draw subtle grid lines for octave separation 5505 - ink(40, 40, 50, 100).line(rollX, rollY + 12, rollX + rollWidth, rollY + 12); 5506 - } 5507 - } 5508 - 5509 5217 // 🥁 Metronome pulse post-process (sharpen) 5510 5218 if (metronomeEnabled && metronomeFlash > 0 && !paintPictureOverlay && !projector) { 5511 5219 const sharpenAmount = 0.8 * metronomeFlash; ··· 6069 5777 screen, 6070 5778 painting, 6071 5779 api, 5780 + jump, 6072 5781 }) { 6073 5782 setSoundContext({ synth, play, freq, num }); 6074 5783 if (pendingAudioReinit && !audioReinitRequested && api?.send) { ··· 6189 5898 // Only in visualizer area (between piano end and waveBtn), not on piano keys 6190 5899 if (e.is("touch") && e.y < TOP_BAR_BOTTOM && !projector && !paintPictureOverlay && !recitalMode) { 6191 5900 // Check that tap is in the visualizer area (after piano, before waveBtn) 6192 - const topBarBase = dotComMode ? 75 : 54; 5901 + const leftSideEnd = Math.max( 5902 + abletonBtn?.box ? abletonBtn.box.x + abletonBtn.box.w : 0, 5903 + osBtn?.box ? osBtn.box.x + osBtn.box.w : 0, 5904 + ); 5905 + const topBarBase = Math.max(dotComMode ? 75 : 54, leftSideEnd + 3); 6193 5906 const topPianoWidth = Math.min(140, Math.floor((screen.width - topBarBase) * 0.5)); 6194 5907 const topPianoEndX = topBarBase + topPianoWidth; 6195 5908 const vizLeft = topPianoEndX; // Start after piano 6196 - const vizRight = Math.min( 6197 - osBtn?.box?.x ?? Infinity, 6198 - abletonBtn?.box?.x ?? Infinity, 6199 - waveBtn?.box?.x ?? screen.width, 6200 - ) - 1; 5909 + const vizRight = (waveBtn?.box?.x ?? screen.width) - 1; 6201 5910 if (e.x >= vizLeft && e.x <= vizRight) { 6202 5911 recitalMode = true; 6203 5912 recitalBlinkPhase = 0; ··· 6221 5930 if (layout.miniInputsEnabled && !recitalMode) { 6222 5931 6223 5932 const trackHeight = showTrack ? TRACK_HEIGHT : 0; 6224 - const trackY = showTrack ? OS_BAR_BOTTOM : null; 5933 + const trackY = showTrack ? SECONDARY_BAR_BOTTOM : null; 6225 5934 const pianoGeometry = getMiniPianoGeometry({ 6226 5935 screen, 6227 5936 layout, ··· 7169 6878 7170 6879 // 🎚️ Room parameter bar interaction (drag to adjust room amount) 7171 6880 if (roomMode && roomBtn?.box && (e.is("touch") || e.is("draw"))) { 7172 - const barY = OS_BAR_BOTTOM + 2; 6881 + const barY = SECONDARY_BAR_BOTTOM + 2; 7173 6882 const barHeight = 6; 7174 6883 const barX = roomBtn.box.x; 7175 6884 const barWidth = Math.max(40, roomBtn.box.w * 2); ··· 8268 7977 octBtn.isNarrow = isNarrow; 8269 7978 } 8270 7979 8271 - // OS bar (m4l / os / drum) — dedicated row below the secondary bar so the 8272 - // top bar stays uncluttered. Buttons are right-aligned and chain leftward; 8273 - // when the screen is narrow they collapse widths/labels so they still fit. 8274 - const OS_BAR_BTN_GAP = 3; 8275 - const OS_BAR_RIGHT_MARGIN = 4; 7980 + // Top-bar side buttons (m4l / os) — sit in the HUD row immediately right 7981 + // of the "notepat.com" corner label. Buttons chain left-to-right so the 7982 + // piano strip begins after the os button. 7983 + const TOP_BAR_SIDE_BTN_GAP = 3; 7984 + const TOP_BAR_SIDE_BTN_MARGIN = 3; // Gap between HUD label and first button 8276 7985 8277 - function osBarButtonMetrics({ screen }) { 7986 + function topBarSideButtonMetrics({ screen }) { 8278 7987 const isNarrow = screen.width < 240; 8279 7988 const isVeryNarrow = screen.width < 180; 8280 7989 const padX = isVeryNarrow ? 2 : isNarrow ? 3 : 4; 8281 7990 const glyph = 6; 7991 + const hudLabelEnd = dotComMode ? 75 : 54; 8282 7992 return { 8283 7993 isNarrow, 8284 7994 isVeryNarrow, 8285 7995 padX, 8286 7996 glyph, 8287 - y: OS_BAR_TOP + 1, 8288 - h: OS_BAR_HEIGHT - 2, 7997 + y: 2, 7998 + h: 12, 7999 + startX: hudLabelEnd + TOP_BAR_SIDE_BTN_MARGIN, 8289 8000 labels: { 8290 8001 ableton: "m4l", 8291 8002 os: "os", ··· 8294 8005 } 8295 8006 8296 8007 function buildAbletonButton({ ui, screen }) { 8297 - const m = osBarButtonMetrics({ screen }); 8008 + const m = topBarSideButtonMetrics({ screen }); 8298 8009 const w = m.labels.ableton.length * m.glyph + m.padX * 2; 8299 - const x = screen.width - OS_BAR_RIGHT_MARGIN - w; 8300 - abletonBtn = new ui.Button(x, m.y, w, m.h); 8010 + abletonBtn = new ui.Button(m.startX, m.y, w, m.h); 8301 8011 abletonBtn.id = "ableton-button"; 8302 8012 abletonBtn.label = m.labels.ableton; 8303 8013 } 8304 8014 8305 8015 function buildOsButton({ ui, screen }) { 8306 - const m = osBarButtonMetrics({ screen }); 8016 + const m = topBarSideButtonMetrics({ screen }); 8307 8017 const w = m.labels.os.length * m.glyph + m.padX * 2; 8308 - const anchorX = abletonBtn?.box?.x ?? (screen.width - OS_BAR_RIGHT_MARGIN); 8309 - osBtn = new ui.Button(anchorX - w - OS_BAR_BTN_GAP, m.y, w, m.h); 8018 + const anchorX = abletonBtn?.box 8019 + ? abletonBtn.box.x + abletonBtn.box.w + TOP_BAR_SIDE_BTN_GAP 8020 + : m.startX; 8021 + osBtn = new ui.Button(anchorX, m.y, w, m.h); 8310 8022 osBtn.id = "os-button"; 8311 8023 osBtn.label = m.labels.os; 8312 8024 }