# Stample Layout Redesign — Notepat Comparison Report **Date:** January 29, 2025 **Author:** Generated by Copilot **Status:** ✅ **IMPLEMENTED** --- ## Implementation Summary The responsive layout system has been implemented in `stample.mjs`: ### Key Changes - Added `getLayoutMetrics()` function for centralized layout computation - Added `getCachedLayout()` for performance optimization - Updated `boot()`, `paint()`, `genPats()`, `layoutBitmapUI()`, and `act()` to use layout metrics - Removed all hardcoded layout constants (`labelHeight`, `menuHeight`, `bitmapColumnWidth`, etc.) ### Layout Modes 1. **Recording Mode**: Full-screen centered preview with responsive stop button 2. **Normal Mode (Bitmap Beside)**: Strip buttons on left, bitmap column on right (when `width >= 280`) 3. **Normal Mode (Bitmap Overlay)**: Full-width strips, bitmap overlaid in corner (portrait/narrow) ### Responsive Thresholds - `COMPACT_HEIGHT = 160` — triggers compact mode with smaller controls - `NARROW_WIDTH = 180` — triggers narrow mode - `BITMAP_BESIDE_MIN_WIDTH = 280` — minimum width for side-by-side layout --- ## Table of Contents 1. [Executive Summary](#executive-summary) 2. [Current Stample Problems](#current-stample-problems) 3. [Notepat Layout Architecture](#notepat-layout-architecture) 4. [Proposed Stample Redesign](#proposed-stample-redesign) 5. [Implementation Plan](#implementation-plan) --- ## Executive Summary **Stample's current layout** is ad-hoc with hardcoded values that don't adapt well to different screen sizes. Elements overlap, the bitmap preview column steals space awkwardly, and the BPM/controls have no proper home. **Notepat's layout system** is sophisticated and responsive: - Uses a **centralized metrics function** (`getButtonLayoutMetrics()`) that computes all dimensions - Has **multiple layout modes** (compact, split, horizontal, vertical) - Uses **caching** to avoid recalculating on every frame - Calculates **reserved zones** for different UI elements - Responds intelligently to **screen aspect ratio** --- ## Current Stample Problems ### 1. Hardcoded Layout Constants ```javascript // Current stample.mjs - hardcoded values scattered throughout const menuHeight = 32; const labelHeight = 24; const bitmapPreviewSize = 120; const bitmapPreviewMargin = 6; const bitmapColumnWidth = 148; // ❌ Fixed column steals space ``` **Problems:** - `bitmapColumnWidth = 148` is always reserved, even when no bitmap exists - Button widths calculated as `Math.max(32, screen.width - bitmapColumnWidth)` — awkward subtraction - No consideration for portrait vs landscape - No adaptation for small screens ### 2. Split Layout is Inflexible Current approach creates a rigid 2-column layout: ``` ┌────────────────────────┬────────────────┐ │ Header (24px) │ │ ├────────────────────────┤ Bitmap │ │ │ Preview │ │ Vertical │ (148px wide) │ │ Strip Buttons │ │ │ ├────────────────┤ │ │ Paint Button │ │ │ Loop Button │ ├────────────────────────┴────────────────┤ │ Record Button (64px wide) │ └─────────────────────────────────────────┘ ``` **Issues:** - Bitmap column is always 148px even with small/no bitmap - Strip buttons get compressed on narrow screens - No reflow for portrait mode - Wasted space when bitmap is smaller than column width ### 3. No Responsive Breakpoints The `genPats()` function is simplistic: ```javascript function genPats({ screen, ui }) { for (let i = 0; i < pats; i += 1) { const strip = (screen.height - menuHeight - labelHeight) / pats; const width = Math.max(32, screen.width - bitmapColumnWidth); // ... } } ``` - No compact mode for small screens - No consideration of whether bitmap exists - Width calculation is subtraction-based, not allocation-based ### 4. Recording Mode Layout is Separate Recording mode has its own layout logic that duplicates calculations: ```javascript if (isRecording) { const margin = 20; const stopBtnH = 40; const livePreviewW = Math.min(screen.width - margin * 2, 300); // ... completely different coordinate system } ``` --- ## Notepat Layout Architecture ### 1. Centralized Metrics Function Notepat uses `getButtonLayoutMetrics()` (lines 1871-2140) to compute ALL layout values: ```javascript function getButtonLayoutMetrics(screen, options) { const reservedSide = sidePanelWidth || 0; const hudReserved = SECONDARY_BAR_BOTTOM; // Calculate usable space const usableWidth = max(0, screen.width - reservedSide); // Detect mode based on screen size const compactMode = screen.height < 200; const isLandscape = screen.width > screen.height; if (compactMode && isLandscape) { // Split layout: buttons on sides, piano in center return { splitLayout: true, ... }; } // Standard layout calculations return { buttonWidth, buttonHeight, topButtonY, totalRows, buttonsPerRow, margin, bottomPadding, hudReserved, ... }; } ``` ### 2. Layout Caching Notepat caches layout to avoid recomputation: ```javascript function getCachedLayout(screen, options) { const key = [ screen.width, screen.height, songMode ? 1 : 0, pictureOverlay ? 1 : 0, // ... all relevant state ].join("|"); if (layoutCache.key === key && layoutCache.layout) { return layoutCache; } // Recompute and cache layoutCache = { key, layout, midiMetrics }; return layoutCache; } ``` ### 3. Responsive Breakpoints Notepat has intelligent breakpoints: ```javascript // Compact mode for DAW/small screens const compactMode = screen.height < 200; const isLandscape = screen.width > screen.height; // Check if mini inputs fit horizontally vs vertically const horizontalSpaceForMini = usableWidth - gridWidthEstimate - pianoWidth > 10; const narrowVerticalSpace = !horizontalSpaceForMini && usableWidth - gridWidthEstimate - rotatedPianoWidth > 4; // Decide layout style const showMiniInputs = miniInputsEnabled && canFitMini; ``` ### 4. Dynamic Reserved Zones Notepat calculates reserved zones dynamically: ```javascript const hudReserved = SECONDARY_BAR_BOTTOM; // Top bar const trackHeight = songMode ? TRACK_HEIGHT : 0; // Optional track const bottomPadding = pictureOverlay ? baseBottomPadding : songMode ? baseBottomPadding + badgeMetrics.height + MIDI_BADGE_MARGIN + aliasPadding : baseBottomPadding; ``` ### 5. Aspect Ratio Constraints Notepat keeps button aspect ratios reasonable: ```javascript // Keep aspect ratio between 0.5 and 2.0 const aspectRatio = buttonWidth / buttonHeight; if (aspectRatio > 1.5) { buttonWidth = floor(buttonHeight * 1.5); } else if (aspectRatio < 0.67) { buttonHeight = floor(buttonWidth * 1.5); } ``` --- ## Proposed Stample Redesign ### New Layout Zones ``` ┌─────────────────────────────────────────┐ │ TOP BAR (24px) - Mode indicators, pats │ ├─────────────────────────────────────────┤ │ │ │ MAIN AREA │ │ (Responsive: bitmap + strips or just │ │ strips when no bitmap) │ │ │ ├─────────────────────────────────────────┤ │ BOTTOM BAR (32px) - Record, Loop, Paint │ └─────────────────────────────────────────┘ ``` ### Responsive Modes #### Mode 1: Portrait / No Bitmap ``` ┌────────────────────┐ │ pats: 4 pat→ │ ← Top bar ├────────────────────┤ │ │ │ STRIP BUTTONS │ ← Full width │ (vertical stack) │ │ │ ├────────────────────┤ │ [Record] [Loop] │ ← Bottom bar └────────────────────┘ ``` #### Mode 2: Landscape with Bitmap ``` ┌───────────────────────────────────────┐ │ pats: 4 pat→ │ ├─────────────────────────┬─────────────┤ │ │ ┌─────────┐ │ │ STRIP BUTTONS │ │ BITMAP │ │ │ (fills remaining) │ │ PREVIEW │ │ │ │ └─────────┘ │ │ │ [Loop][Pnt] │ ├─────────────────────────┴─────────────┤ │ [Record] waveform │ └───────────────────────────────────────┘ ``` #### Mode 3: Compact / Small Screen ``` ┌─────────────────────┐ │ 4 pats [rec] [loop]│ ← Combined bar ├─────────────────────┤ │ │ │ STRIP BUTTONS │ │ (maximize space) │ │ │ ├─────────────────────┤ │ [bitmap preview] │ ← Only if exists └─────────────────────┘ ``` ### New Layout Constants ```javascript // Layout zones const TOP_BAR_HEIGHT = 24; const BOTTOM_BAR_HEIGHT = 36; const MIN_STRIP_WIDTH = 48; const MIN_STRIP_HEIGHT = 32; // Bitmap preview sizing const BITMAP_PREVIEW_MIN = 64; const BITMAP_PREVIEW_MAX = 160; const BITMAP_PREVIEW_MARGIN = 8; // Responsive breakpoints const COMPACT_HEIGHT_THRESHOLD = 180; const NARROW_WIDTH_THRESHOLD = 200; const SHOW_BITMAP_BESIDE_THRESHOLD = 320; // Width needed to show bitmap beside strips ``` ### New Metrics Function ```javascript function getStampleLayoutMetrics(screen, { hasBitmap = false, pats = 4 } = {}) { const isCompact = screen.height < COMPACT_HEIGHT_THRESHOLD; const isNarrow = screen.width < NARROW_WIDTH_THRESHOLD; const isLandscape = screen.width > screen.height; // Calculate available space const topReserved = isCompact ? 20 : TOP_BAR_HEIGHT; const bottomReserved = isCompact ? 28 : BOTTOM_BAR_HEIGHT; const availableHeight = screen.height - topReserved - bottomReserved; // Decide if bitmap goes beside or below strips const bitmapBeside = hasBitmap && isLandscape && screen.width >= SHOW_BITMAP_BESIDE_THRESHOLD; // Calculate bitmap preview size (scales with available space) let bitmapSize = 0; let bitmapColumnWidth = 0; if (hasBitmap) { if (bitmapBeside) { // Beside: column to the right bitmapSize = Math.min(BITMAP_PREVIEW_MAX, Math.floor(availableHeight * 0.6)); bitmapSize = Math.max(BITMAP_PREVIEW_MIN, bitmapSize); bitmapColumnWidth = bitmapSize + BITMAP_PREVIEW_MARGIN * 2; } else { // Below or overlaid: smaller thumbnail bitmapSize = Math.min(80, Math.floor(screen.width * 0.3)); } } // Strip button dimensions const stripWidth = screen.width - bitmapColumnWidth; const stripHeight = Math.floor(availableHeight / pats); // Button positions const recordButtonWidth = isCompact ? 48 : 72; const recordButtonHeight = isCompact ? 24 : 32; return { // Mode flags isCompact, isNarrow, isLandscape, bitmapBeside, // Zones topReserved, bottomReserved, availableHeight, // Strip buttons stripWidth, stripHeight, stripTop: topReserved, stripLeft: 0, // Bitmap hasBitmap, bitmapSize, bitmapColumnWidth, bitmapX: bitmapBeside ? screen.width - bitmapColumnWidth + BITMAP_PREVIEW_MARGIN : null, bitmapY: bitmapBeside ? topReserved + 8 : null, // Bottom bar bottomBarY: screen.height - bottomReserved, recordButtonWidth, recordButtonHeight, }; } ``` ### Recording Mode Integration Instead of separate layout code, recording mode becomes a layout flag: ```javascript function getStampleLayoutMetrics(screen, { isRecording = false, ... }) { if (isRecording) { // Full-screen recording preview with centered stop button const previewMargin = 20; const stopButtonHeight = 48; return { isRecording: true, previewX: previewMargin, previewY: TOP_BAR_HEIGHT + previewMargin, previewWidth: screen.width - previewMargin * 2, previewHeight: screen.height - TOP_BAR_HEIGHT - stopButtonHeight - previewMargin * 3, stopButtonX: (screen.width - 120) / 2, stopButtonY: screen.height - stopButtonHeight - previewMargin, stopButtonWidth: 120, stopButtonHeight: stopButtonHeight, }; } // ... normal layout } ``` --- ## Implementation Plan ### Phase 1: Extract Layout Metrics 1. Create `getStampleLayoutMetrics()` function 2. Replace hardcoded values with metrics calls 3. Add layout caching like notepat ### Phase 2: Refactor `genPats()` 1. Use metrics for button positioning 2. Remove `bitmapColumnWidth` subtraction 3. Add responsive strip sizing ### Phase 3: Unify Recording Mode 1. Move recording layout into metrics function 2. Remove duplicate coordinate calculations 3. Use same `paint()` function with mode flag ### Phase 4: Add Responsive Breakpoints 1. Implement compact mode 2. Implement portrait vs landscape switching 3. Implement bitmap-beside vs bitmap-below ### Phase 5: Polish 1. Smooth transitions between modes 2. Test on various screen sizes 3. Ensure touch targets are sufficient --- ## Key Takeaways from Notepat | Feature | Notepat | Current Stample | Proposed Stample | |---------|---------|-----------------|------------------| | Layout metrics | Centralized function | Scattered hardcodes | Centralized function | | Layout caching | ✅ Yes | ❌ No | ✅ Yes | | Aspect ratio limits | ✅ Yes | ❌ No | ✅ Yes | | Compact mode | ✅ Yes (split) | ❌ No | ✅ Yes | | Reserved zones | ✅ Dynamic | ❌ Fixed | ✅ Dynamic | | Landscape adaptation | ✅ Yes | ❌ Partial | ✅ Yes | | Button size limits | min=20, max=64 | min=32 only | min/max | | Recording mode | N/A | Separate code path | Integrated | --- ## Files to Modify | File | Changes | |------|---------| | [stample.mjs](../system/public/aesthetic.computer/disks/stample.mjs) | Complete layout refactor | --- ## Related Files - [notepat.mjs](../system/public/aesthetic.computer/disks/notepat.mjs) — Reference implementation - [clock.mjs](../system/public/aesthetic.computer/disks/clock.mjs) — Another piece with responsive layout --- ## Summary Stample needs a complete layout overhaul using notepat's architectural patterns: 1. **Centralized metrics** — One function computes all layout values 2. **Responsive modes** — Detect screen size and adapt 3. **Dynamic allocation** — Don't subtract, allocate 4. **Caching** — Don't recompute every frame 5. **Unified recording** — Same system, different mode flag The result will be a clean, responsive layout that works on phones, tablets, DAWs, and desktops without overlapping elements or wasted space.