Monorepo for Aesthetic.Computer aesthetic.computer
core reports stample-layout-redesign.md
16 kB

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
  2. Current Stample Problems
  3. Notepat Layout Architecture
  4. Proposed Stample Redesign
  5. 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 #

// 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:

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:

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:

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:

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:

// 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:

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:

// 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 #

// 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 #

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:

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 Complete layout refactor


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.