PEDAL-UI: Helix Stadium-style redesign — block chain, big touch sliders, footswitch bar with scribble strips
This commit is contained in:
@@ -0,0 +1,811 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NAM Pedal — Helix Stadium UX Concept</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; user-select: none; -webkit-tap-highlight-color: transparent; }
|
||||
|
||||
:root {
|
||||
--bg: #0A0A0C;
|
||||
--panel: #141418;
|
||||
--surface: #1C1C22;
|
||||
--border: #2A2A32;
|
||||
--amber: #E8A030;
|
||||
--amberDim: #7A5218;
|
||||
--blue: #3A7BA8;
|
||||
--blueDim: #1E4060;
|
||||
--green: #3AB87A;
|
||||
--red: #C84040;
|
||||
--text: #F0EDE6;
|
||||
--textSec: #8888A0;
|
||||
--textDim: #444458;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: 'Inter', sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Main Layout ── */
|
||||
.pedal {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
/* ── Status Bar ── */
|
||||
.status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px 14px;
|
||||
background: var(--panel);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.status-left { display: flex; align-items: center; gap: 10px; }
|
||||
.status-led {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
background: var(--green);
|
||||
box-shadow: 0 0 8px var(--green);
|
||||
}
|
||||
.status-text { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--textSec); letter-spacing: .08em; }
|
||||
.status-right { display: flex; align-items: center; gap: 14px; }
|
||||
.status-chip {
|
||||
font-family: 'JetBrains Mono', monospace; font-size: 10px;
|
||||
padding: 3px 8px; border-radius: 4px;
|
||||
background: rgba(58,123,168,.15); color: var(--blue);
|
||||
border: 1px solid rgba(58,123,168,.3);
|
||||
}
|
||||
|
||||
/* ── Scribble Strip / Info Bar ── */
|
||||
.scribble-bar {
|
||||
display: flex;
|
||||
padding: 8px 14px;
|
||||
background: #0F0F14;
|
||||
border-bottom: 1px solid var(--border);
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.scribble {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
border-radius: 5px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
text-align: center;
|
||||
}
|
||||
.scribble .label { font-size: 9px; color: var(--textDim); letter-spacing: .06em; text-transform: uppercase; }
|
||||
.scribble .value { font-family: 'JetBrains Mono', monospace; font-size: 13px; font-weight: 600; color: var(--amber); margin-top: 1px; }
|
||||
|
||||
/* ── Block Chain ── */
|
||||
.chain-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chain-scroll {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 14px 12px;
|
||||
overflow-x: auto;
|
||||
flex-shrink: 0;
|
||||
align-items: stretch;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border) transparent;
|
||||
}
|
||||
.chain-scroll::-webkit-scrollbar { height: 4px; }
|
||||
.chain-scroll::-webkit-scrollbar-track { background: transparent; }
|
||||
.chain-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||
|
||||
.chain-term {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 12px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .1em;
|
||||
color: var(--blue);
|
||||
background: var(--blueDim);
|
||||
border: 1px solid rgba(58,123,168,.4);
|
||||
border-radius: 8px;
|
||||
min-height: 72px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chain-block {
|
||||
min-width: 110px;
|
||||
max-width: 140px;
|
||||
flex-shrink: 0;
|
||||
background: var(--panel);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
transition: all .15s;
|
||||
position: relative;
|
||||
}
|
||||
.chain-block.selected {
|
||||
border-color: var(--amber);
|
||||
box-shadow: 0 0 12px rgba(232,160,48,.3);
|
||||
}
|
||||
.chain-block.bypassed { opacity: .45; }
|
||||
|
||||
.chain-block .block-led {
|
||||
width: 6px; height: 6px; border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 8px; right: 8px;
|
||||
}
|
||||
.block-led.on { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
||||
.block-led.off { background: var(--textDim); }
|
||||
|
||||
.chain-block .block-scribble {
|
||||
padding: 8px 10px 4px;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.block-scribble .block-type {
|
||||
font-size: 9px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
color: var(--textDim);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.block-scribble .block-name {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.block-scribble .block-sub {
|
||||
font-size: 10px;
|
||||
color: var(--textSec);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.chain-block .block-indicator {
|
||||
height: 3px;
|
||||
border-radius: 0 0 8px 8px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.chain-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--textDim);
|
||||
font-size: 18px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
/* ── Parameter Panel ── */
|
||||
.param-panel {
|
||||
background: var(--panel);
|
||||
border-top: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 12px 14px 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.param-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.param-header h3 {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
color: var(--textSec);
|
||||
}
|
||||
.param-header .block-label {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--amber);
|
||||
}
|
||||
|
||||
.param-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px 20px;
|
||||
}
|
||||
|
||||
.param-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.param-group .param-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 11px;
|
||||
}
|
||||
.param-label .name { color: var(--textSec); text-transform: uppercase; letter-spacing: .05em; }
|
||||
.param-label .value { font-family: 'JetBrains Mono', monospace; color: var(--amber); font-weight: 600; }
|
||||
|
||||
.touch-slider {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: border-color .15s;
|
||||
}
|
||||
.touch-slider::-webkit-slider-runnable-track {
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(to right, var(--amberDim) 0%, var(--amber) 100%);
|
||||
background-size: var(--pct, 50%) 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-color: var(--surface);
|
||||
}
|
||||
.touch-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
background: var(--amber);
|
||||
border: 2px solid var(--bg);
|
||||
box-shadow: 0 1px 6px rgba(0,0,0,.5);
|
||||
margin-top: 1px;
|
||||
cursor: pointer;
|
||||
transition: transform .1s;
|
||||
}
|
||||
.touch-slider::-webkit-slider-thumb:active {
|
||||
transform: scale(1.12);
|
||||
}
|
||||
|
||||
.touch-slider::-moz-range-track {
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background: var(--surface);
|
||||
}
|
||||
.touch-slider::-moz-range-thumb {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
background: var(--amber);
|
||||
border: 2px solid var(--bg);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ── Footswitch Bar ── */
|
||||
.footswitch-bar {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 10px 12px 14px;
|
||||
background: var(--panel);
|
||||
border-top: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.footswitch {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 6px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid var(--border);
|
||||
background: var(--surface);
|
||||
cursor: pointer;
|
||||
transition: all .12s;
|
||||
position: relative;
|
||||
min-height: 68px;
|
||||
}
|
||||
.footswitch:active {
|
||||
transform: scale(.95);
|
||||
border-color: var(--amber);
|
||||
}
|
||||
.footswitch.active {
|
||||
border-color: var(--amber);
|
||||
background: rgba(232,160,48,.1);
|
||||
}
|
||||
.footswitch.preset {
|
||||
border-color: var(--blueDim);
|
||||
}
|
||||
.footswitch.preset.active {
|
||||
border-color: var(--blue);
|
||||
}
|
||||
|
||||
.footswitch .fs-scribble {
|
||||
font-size: 9px;
|
||||
color: var(--textDim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .06em;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.footswitch .fs-label {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
.footswitch .fs-sub {
|
||||
font-size: 9px;
|
||||
color: var(--textSec);
|
||||
margin-top: 1px;
|
||||
}
|
||||
.footswitch .fs-led {
|
||||
width: 5px; height: 5px; border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 6px; right: 6px;
|
||||
}
|
||||
.fs-led.green { background: var(--green); }
|
||||
.fs-led.amber { background: var(--amber); }
|
||||
.fs-led.off { background: var(--textDim); }
|
||||
|
||||
.footswitch .fs-badge {
|
||||
position: absolute;
|
||||
top: 5px; left: 5px;
|
||||
font-size: 8px;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
background: rgba(58,123,168,.2);
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
/* ── Animations ── */
|
||||
@keyframes pulse-amber { 0%,100%{opacity:1} 50%{opacity:.6} }
|
||||
.recording { animation: pulse-amber 1s infinite; }
|
||||
|
||||
/* ── VU Meter ── */
|
||||
.vu-strip {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
align-items: flex-end;
|
||||
height: 28px;
|
||||
}
|
||||
.vu-seg {
|
||||
width: 4px;
|
||||
border-radius: 1px;
|
||||
transition: background .05s;
|
||||
}
|
||||
|
||||
/* ── Focus / Active styles for touch ── */
|
||||
.touch-target {
|
||||
cursor: pointer;
|
||||
transition: transform .1s;
|
||||
}
|
||||
.touch-target:active {
|
||||
transform: scale(.96);
|
||||
}
|
||||
|
||||
/* ── Preset Selector ── */
|
||||
.preset-strip {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
flex-shrink: 0;
|
||||
background: rgba(0,0,0,.3);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.preset-chip {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all .12s;
|
||||
flex: 1;
|
||||
}
|
||||
.preset-chip.active {
|
||||
border-color: var(--amber);
|
||||
background: rgba(232,160,48,.1);
|
||||
}
|
||||
.preset-chip .pc-name { font-size: 11px; font-weight: 500; }
|
||||
.preset-chip .pc-num { font-family: 'JetBrains Mono', monospace; font-size: 9px; color: var(--textDim); }
|
||||
|
||||
/* ── Responsive: slim down for smaller screens ── */
|
||||
@media (max-width: 480px) {
|
||||
.chain-block { min-width: 90px; max-width: 110px; }
|
||||
.footswitch { min-height: 58px; padding: 8px 4px; }
|
||||
.footswitch .fs-label { font-size: 11px; }
|
||||
.param-grid { grid-template-columns: 1fr; gap: 8px; }
|
||||
.preset-chip .pc-name { font-size: 10px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="pedal">
|
||||
|
||||
<!-- Status Bar -->
|
||||
<div class="status-bar">
|
||||
<div class="status-left">
|
||||
<div class="status-led"></div>
|
||||
<span class="status-text">CONNECTED</span>
|
||||
<span class="status-chip">● Plexi Crunch</span>
|
||||
</div>
|
||||
<div class="status-right">
|
||||
<div class="vu-strip" id="vuL"></div>
|
||||
<span class="status-text" style="color:var(--textDim);font-size:10px;">CPU 23%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scribble Strip Info Bar -->
|
||||
<div class="scribble-bar">
|
||||
<div class="scribble"><div class="label">Preset</div><div class="value">Plexi Crunch</div></div>
|
||||
<div class="scribble"><div class="label">Bank</div><div class="value">A · 01</div></div>
|
||||
<div class="scribble"><div class="label">Routing</div><div class="value">MONO</div></div>
|
||||
<div class="scribble"><div class="label">Master</div><div class="value">72%</div></div>
|
||||
</div>
|
||||
|
||||
<!-- Preset Strip -->
|
||||
<div class="preset-strip">
|
||||
<div class="preset-chip" onclick="selectPreset(this)">
|
||||
<div class="pc-name">Plexi Crunch</div>
|
||||
<div class="pc-num">01</div>
|
||||
</div>
|
||||
<div class="preset-chip active" onclick="selectPreset(this)">
|
||||
<div class="pc-name">Modern Hi-Gain</div>
|
||||
<div class="pc-num">02</div>
|
||||
</div>
|
||||
<div class="preset-chip" onclick="selectPreset(this)">
|
||||
<div class="pc-name">Clean Chime</div>
|
||||
<div class="pc-num">03</div>
|
||||
</div>
|
||||
<div class="preset-chip" onclick="selectPreset(this)">
|
||||
<div class="pc-name">Blues Drive</div>
|
||||
<div class="pc-num">04</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Block Chain Area -->
|
||||
<div class="chain-area">
|
||||
<div class="chain-scroll" id="chainScroll">
|
||||
<div class="chain-term">IN</div>
|
||||
<div class="chain-arrow">▶</div>
|
||||
|
||||
<div class="chain-block selected" data-block="gate" onclick="selectBlock(this)">
|
||||
<div class="block-led on"></div>
|
||||
<div class="block-scribble">
|
||||
<div class="block-type">Dynamics</div>
|
||||
<div class="block-name">Noise Gate</div>
|
||||
<div class="block-sub">Threshold 40%</div>
|
||||
</div>
|
||||
<div class="block-indicator" style="background:var(--green);"></div>
|
||||
</div>
|
||||
|
||||
<div class="chain-arrow">▶</div>
|
||||
|
||||
<div class="chain-block" data-block="drive" onclick="selectBlock(this)">
|
||||
<div class="block-led on"></div>
|
||||
<div class="block-scribble">
|
||||
<div class="block-type">Drive</div>
|
||||
<div class="block-name">OD-808</div>
|
||||
<div class="block-sub">Drive 65%</div>
|
||||
</div>
|
||||
<div class="block-indicator" style="background:var(--amber);"></div>
|
||||
</div>
|
||||
|
||||
<div class="chain-arrow">▶</div>
|
||||
|
||||
<div class="chain-block bypassed" data-block="dist" onclick="selectBlock(this)">
|
||||
<div class="block-led off"></div>
|
||||
<div class="block-scribble">
|
||||
<div class="block-type">Distortion</div>
|
||||
<div class="block-name">RAT</div>
|
||||
<div class="block-sub">— Bypassed —</div>
|
||||
</div>
|
||||
<div class="block-indicator" style="background:var(--textDim);"></div>
|
||||
</div>
|
||||
|
||||
<div class="chain-arrow">▶</div>
|
||||
|
||||
<div class="chain-block" data-block="mod" onclick="selectBlock(this)">
|
||||
<div class="block-led on"></div>
|
||||
<div class="block-scribble">
|
||||
<div class="block-type">Modulation</div>
|
||||
<div class="block-name">Phase 90</div>
|
||||
<div class="block-sub">Rate 35%</div>
|
||||
</div>
|
||||
<div class="block-indicator" style="background:var(--blue);"></div>
|
||||
</div>
|
||||
|
||||
<div class="chain-arrow">▶</div>
|
||||
|
||||
<div class="chain-block" data-block="delay" onclick="selectBlock(this)">
|
||||
<div class="block-led on"></div>
|
||||
<div class="block-scribble">
|
||||
<div class="block-type">Delay</div>
|
||||
<div class="block-name">Digital Delay</div>
|
||||
<div class="block-sub">450ms · 25%</div>
|
||||
</div>
|
||||
<div class="block-indicator" style="background:#A060E0;"></div>
|
||||
</div>
|
||||
|
||||
<div class="chain-arrow">▶</div>
|
||||
|
||||
<div class="chain-block" data-block="reverb" onclick="selectBlock(this)">
|
||||
<div class="block-led on"></div>
|
||||
<div class="block-scribble">
|
||||
<div class="block-type">Reverb</div>
|
||||
<div class="block-name">Hall Reverb</div>
|
||||
<div class="block-sub">Decay 60%</div>
|
||||
</div>
|
||||
<div class="block-indicator" style="background:#60A0E0;"></div>
|
||||
</div>
|
||||
|
||||
<div class="chain-arrow">▶</div>
|
||||
<div class="chain-term">OUT</div>
|
||||
</div>
|
||||
|
||||
<!-- Parameter Panel -->
|
||||
<div class="param-panel" id="paramPanel">
|
||||
<div class="param-header">
|
||||
<h3>Noise Gate <span class="block-label">· Dynamics</span></h3>
|
||||
<span style="color:var(--green);font-size:11px;display:flex;align-items:center;gap:4px;">
|
||||
<span style="width:6px;height:6px;border-radius:50%;background:var(--green);display:inline-block;"></span> Active
|
||||
</span>
|
||||
</div>
|
||||
<div class="param-grid">
|
||||
<div class="param-group">
|
||||
<div class="param-label">
|
||||
<span class="name">Threshold</span>
|
||||
<span class="value" id="val_thresh">40</span>
|
||||
</div>
|
||||
<input type="range" class="touch-slider" min="0" max="100" value="40" oninput="updateSlider(this,'val_thresh')" style="--pct:40%">
|
||||
</div>
|
||||
<div class="param-group">
|
||||
<div class="param-label">
|
||||
<span class="name">Release</span>
|
||||
<span class="value" id="val_rel">25</span>
|
||||
</div>
|
||||
<input type="range" class="touch-slider" min="0" max="100" value="25" oninput="updateSlider(this,'val_rel')" style="--pct:25%">
|
||||
</div>
|
||||
<div class="param-group">
|
||||
<div class="param-label">
|
||||
<span class="name">Depth</span>
|
||||
<span class="value" id="val_dep">70</span>
|
||||
</div>
|
||||
<input type="range" class="touch-slider" min="0" max="100" value="70" oninput="updateSlider(this,'val_dep')" style="--pct:70%">
|
||||
</div>
|
||||
<div class="param-group">
|
||||
<div class="param-label">
|
||||
<span class="name">Mix</span>
|
||||
<span class="value" id="val_mix">100</span>
|
||||
</div>
|
||||
<input type="range" class="touch-slider" min="0" max="100" value="100" oninput="updateSlider(this,'val_mix')" style="--pct:100%">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footswitch Bar -->
|
||||
<div class="footswitch-bar">
|
||||
<div class="footswitch preset" onclick="toggleFS(this)">
|
||||
<span class="fs-scribble">BANK</span>
|
||||
<span class="fs-label">▲ A</span>
|
||||
<span class="fs-sub">Bank Up</span>
|
||||
</div>
|
||||
|
||||
<div class="footswitch active" data-block="gate" onclick="toggleBlock(this,'gate')">
|
||||
<span class="fs-scribble">GATE</span>
|
||||
<span class="fs-label">Noise Gate</span>
|
||||
<span class="fs-sub">Threshold 40%</span>
|
||||
<div class="fs-led green"></div>
|
||||
</div>
|
||||
|
||||
<div class="footswitch" data-block="drive" onclick="toggleBlock(this,'drive')">
|
||||
<span class="fs-scribble">DRIVE</span>
|
||||
<span class="fs-label">OD-808</span>
|
||||
<span class="fs-sub">Drive 65%</span>
|
||||
<div class="fs-led green"></div>
|
||||
</div>
|
||||
|
||||
<div class="footswitch" data-block="dist" onclick="toggleBlock(this,'dist')">
|
||||
<span class="fs-scribble">DIST</span>
|
||||
<span class="fs-label" style="color:var(--textDim);">RAT</span>
|
||||
<span class="fs-sub" style="color:var(--textDim);">Bypassed</span>
|
||||
<div class="fs-led off"></div>
|
||||
</div>
|
||||
|
||||
<div class="footswitch" data-block="mod" onclick="selectBlockFromFS(this,'mod')">
|
||||
<span class="fs-scribble">MOD</span>
|
||||
<span class="fs-label">Phase 90</span>
|
||||
<span class="fs-sub">Rate 35%</span>
|
||||
<div class="fs-led green"></div>
|
||||
</div>
|
||||
|
||||
<div class="footswitch preset" onclick="toggleFS(this)">
|
||||
<span class="fs-scribble">BANK</span>
|
||||
<span class="fs-label">▼ A</span>
|
||||
<span class="fs-sub">Bank Down</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── Block Chain Selection ──
|
||||
function selectBlock(el) {
|
||||
document.querySelectorAll('.chain-block').forEach(b => b.classList.remove('selected'));
|
||||
el.classList.add('selected');
|
||||
|
||||
// Update footswitch bar
|
||||
document.querySelectorAll('.footswitch[data-block]').forEach(f => f.classList.remove('active'));
|
||||
const blockId = el.dataset.block;
|
||||
const fs = document.querySelector(`.footswitch[data-block="${blockId}"]`);
|
||||
if (fs) fs.classList.add('active');
|
||||
|
||||
// Update parameter panel (simple demo)
|
||||
const name = el.querySelector('.block-name').textContent;
|
||||
const type = el.querySelector('.block-type').textContent;
|
||||
const header = document.querySelector('.param-header h3');
|
||||
header.innerHTML = `${name} <span class="block-label">· ${type}</span>`;
|
||||
|
||||
// Update param panel content based on block
|
||||
const params = {
|
||||
gate: [
|
||||
['Threshold', '40', 0, 100],
|
||||
['Release', '25', 0, 100],
|
||||
['Depth', '70', 0, 100],
|
||||
['Mix', '100', 0, 100],
|
||||
],
|
||||
drive: [
|
||||
['Drive', '65', 0, 100],
|
||||
['Tone', '55', 0, 100],
|
||||
['Level', '70', 0, 100],
|
||||
['Tight', '30', 0, 100],
|
||||
],
|
||||
dist: [
|
||||
['Distortion', '75', 0, 100],
|
||||
['Filter', '50', 0, 100],
|
||||
['Volume', '65', 0, 100],
|
||||
['Gain', '80', 0, 100],
|
||||
],
|
||||
mod: [
|
||||
['Rate', '35', 0, 100],
|
||||
['Depth', '60', 0, 100],
|
||||
['Feedback', '40', 0, 100],
|
||||
['Mix', '50', 0, 100],
|
||||
],
|
||||
delay: [
|
||||
['Time', '450', 20, 2000],
|
||||
['Feedback', '25', 0, 100],
|
||||
['Mix', '40', 0, 100],
|
||||
['Low Cut', '80', 20, 500],
|
||||
],
|
||||
reverb: [
|
||||
['Decay', '60', 0, 100],
|
||||
['Pre-Delay', '30', 0, 200],
|
||||
['Damping', '50', 0, 100],
|
||||
['Mix', '35', 0, 100],
|
||||
],
|
||||
};
|
||||
|
||||
const p = params[blockId] || params.gate;
|
||||
const grid = document.querySelector('.param-grid');
|
||||
grid.innerHTML = p.map(([name, val, min, max], i) => {
|
||||
const id = `p${i}`;
|
||||
const pct = Math.round((val / max) * 100);
|
||||
return `<div class="param-group">
|
||||
<div class="param-label">
|
||||
<span class="name">${name}</span>
|
||||
<span class="value" id="${id}">${val}</span>
|
||||
</div>
|
||||
<input type="range" class="touch-slider" min="${min}" max="${max}" value="${val}" oninput="updateSlider(this,'${id}')" style="--pct:${pct}%">
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function updateSlider(el, valId) {
|
||||
const val = el.value;
|
||||
document.getElementById(valId).textContent = val;
|
||||
const min = parseInt(el.min);
|
||||
const max = parseInt(el.max);
|
||||
const pct = Math.round(((val - min) / (max - min)) * 100);
|
||||
el.style.setProperty('--pct', pct + '%');
|
||||
}
|
||||
|
||||
// ── Footswitch Interaction ──
|
||||
function toggleBlock(el, blockId) {
|
||||
// Toggle bypass
|
||||
const chainBlock = document.querySelector(`.chain-block[data-block="${blockId}"]`);
|
||||
if (chainBlock) {
|
||||
chainBlock.classList.toggle('bypassed');
|
||||
const led = chainBlock.querySelector('.block-led');
|
||||
const indicator = chainBlock.querySelector('.block-indicator');
|
||||
if (chainBlock.classList.contains('bypassed')) {
|
||||
led.className = 'block-led off';
|
||||
indicator.style.background = 'var(--textDim)';
|
||||
el.querySelector('.fs-label').style.color = 'var(--textDim)';
|
||||
el.querySelector('.fs-sub').textContent = 'Bypassed';
|
||||
el.querySelector('.fs-sub').style.color = 'var(--textDim)';
|
||||
el.querySelector('.fs-led').className = 'fs-led off';
|
||||
} else {
|
||||
led.className = 'block-led on';
|
||||
// Restore original indicator color
|
||||
const colors = {gate:'var(--green)',drive:'var(--amber)',dist:'var(--red)',mod:'var(--blue)',delay:'#A060E0',reverb:'#60A0E0'};
|
||||
indicator.style.background = colors[blockId] || 'var(--green)';
|
||||
el.querySelector('.fs-label').style.color = '';
|
||||
el.querySelector('.fs-sub').style.color = '';
|
||||
// Restore sub text from chain block
|
||||
const sub = chainBlock.querySelector('.block-sub');
|
||||
el.querySelector('.fs-sub').textContent = sub.textContent;
|
||||
el.querySelector('.fs-led').className = 'fs-led green';
|
||||
}
|
||||
}
|
||||
el.classList.toggle('active');
|
||||
}
|
||||
|
||||
function selectBlockFromFS(el, blockId) {
|
||||
// Select block in chain
|
||||
const chainBlock = document.querySelector(`.chain-block[data-block="${blockId}"]`);
|
||||
if (chainBlock) selectBlock(chainBlock);
|
||||
}
|
||||
|
||||
function toggleFS(el) {
|
||||
el.classList.toggle('active');
|
||||
}
|
||||
|
||||
function selectPreset(el) {
|
||||
document.querySelectorAll('.preset-chip').forEach(c => c.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
}
|
||||
|
||||
// ── VU Meter Animation ──
|
||||
(function animateVU() {
|
||||
const vu = document.getElementById('vuL');
|
||||
const segs = 8;
|
||||
vu.innerHTML = '';
|
||||
for (let i = 0; i < segs; i++) {
|
||||
const seg = document.createElement('div');
|
||||
seg.className = 'vu-seg';
|
||||
seg.style.height = '100%';
|
||||
seg.style.flex = '1';
|
||||
vu.appendChild(seg);
|
||||
}
|
||||
function tick() {
|
||||
const level = Math.random();
|
||||
const active = Math.floor(level * segs);
|
||||
const segs_ = vu.querySelectorAll('.vu-seg');
|
||||
segs_.forEach((seg, i) => {
|
||||
const on = i < active;
|
||||
const color = i >= 6 ? '#C84040' : i >= 4 ? '#E8C030' : '#3AB87A';
|
||||
seg.style.background = on ? color : '#2A2A32';
|
||||
seg.style.boxShadow = on ? `0 0 4px ${color}` : 'none';
|
||||
});
|
||||
}
|
||||
setInterval(tick, 100);
|
||||
})();
|
||||
|
||||
// ── Auto-scroll to show chain fully ──
|
||||
window.addEventListener('load', () => {
|
||||
const scroll = document.getElementById('chainScroll');
|
||||
if (scroll) scroll.scrollLeft = 120;
|
||||
});
|
||||
|
||||
// ── Touch feedback ──
|
||||
document.addEventListener('touchstart', () => {}, {passive: true});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,617 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Pi Multi-FX — Helix Stadium Mockup</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; user-select: none; }
|
||||
body { background: #0A0A0C; color: #F0EDE6; font-family: 'Inter', sans-serif; display: flex; justify-content: center; min-height: 100vh; }
|
||||
|
||||
.device {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #0A0A0C;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Status Bar ─────────────────────────────── */
|
||||
.status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 16px 6px;
|
||||
background: #141418;
|
||||
border-bottom: 1px solid #2A2A32;
|
||||
height: 38px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.status-left { display: flex; align-items: center; gap: 8px; }
|
||||
.status-dot { width: 7px; height: 7px; border-radius: 50%; background: #3AB87A; box-shadow: 0 0 8px #3AB87A; }
|
||||
.status-label { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: #8888A0; letter-spacing: .08em; }
|
||||
.status-right { display: flex; align-items: center; gap: 14px; }
|
||||
.status-preset { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: #8888A0; }
|
||||
.status-cpu { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: #444458; }
|
||||
.status-sig { font-size: 13px; color: #E8A030; }
|
||||
|
||||
/* ── MAIN AREA ───────────────────────────────── */
|
||||
.main-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ── Block Chain ─────────────────────────────── */
|
||||
.chain-section {
|
||||
padding: 12px 10px 10px;
|
||||
background: #0A0A0C;
|
||||
border-bottom: 1px solid #2A2A32;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.chain-label {
|
||||
font-size: 9px;
|
||||
letter-spacing: .12em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
color: #444458;
|
||||
margin-bottom: 8px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.chain-scroll {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
padding: 4px 0;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
.chain-scroll::-webkit-scrollbar { display: none; }
|
||||
|
||||
.terminator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 44px;
|
||||
height: 56px;
|
||||
padding: 0 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.terminator-in { background: #1E4060; color: #3A7BA8; border: 1px solid #3A7BA855; }
|
||||
.terminator-out { background: #1E4060; color: #3A7BA8; border: 1px solid #3A7BA855; }
|
||||
|
||||
.connector {
|
||||
width: 18px;
|
||||
height: 2px;
|
||||
background: #2A2A32;
|
||||
border-radius: 1px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
.connector::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -2px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-right: 2px solid #2A2A32;
|
||||
border-top: 2px solid #2A2A32;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.block-chip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
min-width: 120px;
|
||||
height: 56px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
background: #1C1C22;
|
||||
border: 1px solid #2A2A32;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: all .12s;
|
||||
position: relative;
|
||||
}
|
||||
.block-chip.active { border-color: #E8A030; background: #1C1C22; box-shadow: 0 0 12px #E8A03033; }
|
||||
.block-chip.bypassed { opacity: .4; }
|
||||
.block-chip.drag-over { border-color: #3AB87A; border-style: dashed; }
|
||||
|
||||
.block-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.block-led {
|
||||
width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0;
|
||||
}
|
||||
.block-led.on { background: #3AB87A; box-shadow: 0 0 6px #3AB87A; }
|
||||
.block-led.off { background: #444458; }
|
||||
|
||||
.block-name {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #F0EDE6;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.block-type {
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
background: #7A521888;
|
||||
color: #E8A030;
|
||||
}
|
||||
.block-drag {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 4px;
|
||||
cursor: grab;
|
||||
opacity: .3;
|
||||
}
|
||||
.block-drag::before, .block-drag::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
border-radius: 50%;
|
||||
background: #8888A0;
|
||||
}
|
||||
|
||||
/* ── PARAMS PANEL ────────────────────────────── */
|
||||
.params-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 12px;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.params-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.params-title {
|
||||
font-size: 10px;
|
||||
letter-spacing: .12em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
color: #8888A0;
|
||||
}
|
||||
.param-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.param-action {
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
background: #1C1C22;
|
||||
border: 1px solid #2A2A32;
|
||||
color: #8888A0;
|
||||
cursor: pointer;
|
||||
transition: all .1s;
|
||||
}
|
||||
.param-action:active { transform: scale(.95); }
|
||||
|
||||
.knob-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px 20px;
|
||||
justify-content: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.knob-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.knob-shell {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
touch-action: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.knob-bg {
|
||||
width: 100%; height: 100%;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 35% 30%, #3A3A48, #1A1A22);
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.07);
|
||||
}
|
||||
.knob-dot {
|
||||
position: absolute;
|
||||
width: 3px;
|
||||
height: 22px;
|
||||
border-radius: 2px;
|
||||
background: #E8A030;
|
||||
left: 50%;
|
||||
bottom: 8px;
|
||||
transform-origin: bottom center;
|
||||
transform: translateX(-50%) rotate(30deg);
|
||||
box-shadow: 0 0 6px #E8A030;
|
||||
}
|
||||
.knob-value {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: #E8A030;
|
||||
}
|
||||
.knob-label {
|
||||
font-size: 9px;
|
||||
color: #8888A0;
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* slider */
|
||||
.slider-group {
|
||||
padding: 0 4px;
|
||||
}
|
||||
.slider-label-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 10px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.slider-label { color: #8888A0; letter-spacing: .06em; text-transform: uppercase; }
|
||||
.slider-value { font-family: 'JetBrains Mono', monospace; color: #E8A030; }
|
||||
.slider-track {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: #2A2A32;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
.slider-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
background: #E8A030;
|
||||
width: 65%;
|
||||
box-shadow: 0 0 6px #E8A03066;
|
||||
}
|
||||
.slider-thumb {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 65%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 20px; height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #F0EDE6;
|
||||
border: 2px solid #0A0A0C;
|
||||
box-shadow: 0 1px 6px rgba(0,0,0,.5);
|
||||
}
|
||||
|
||||
.master-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.master-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #F0EDE6;
|
||||
}
|
||||
.master-sub {
|
||||
font-size: 11px;
|
||||
color: #8888A0;
|
||||
}
|
||||
|
||||
/* ── FOOTSWITCH BAR ───────────────────────────── */
|
||||
.footswitch-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
background: #141418;
|
||||
border-top: 1px solid #2A2A32;
|
||||
height: 72px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.bank-btn {
|
||||
width: 32px;
|
||||
height: 56px;
|
||||
border-radius: 6px;
|
||||
background: #1C1C22;
|
||||
border: 1px solid #2A2A32;
|
||||
color: #8888A0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
transition: all .1s;
|
||||
}
|
||||
.bank-btn:active { background: #2A2A32; transform: scale(.95); }
|
||||
|
||||
.footswitch {
|
||||
flex: 1;
|
||||
height: 56px;
|
||||
border-radius: 6px;
|
||||
background: #1C1C22;
|
||||
border: 1px solid #2A2A32;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
transition: all .12s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.footswitch.active { border-color: #E8A03066; box-shadow: 0 0 10px #E8A03022; }
|
||||
.footswitch:active { transform: scale(.96); }
|
||||
.fs-number {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #E8A030;
|
||||
}
|
||||
.fs-label {
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
color: #F0EDE6;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 90%;
|
||||
text-align: center;
|
||||
}
|
||||
.fs-sub {
|
||||
font-size: 7px;
|
||||
color: #444458;
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
|
||||
/* ── Add Block Button ── */
|
||||
.add-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 36px;
|
||||
height: 56px;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed #2A2A32;
|
||||
background: transparent;
|
||||
color: #444458;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: all .12s;
|
||||
}
|
||||
.add-block:hover { border-color: #3A7BA8; color: #3A7BA8; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="device">
|
||||
|
||||
<!-- ── Status Bar ──────────────────────────── -->
|
||||
<div class="status-bar">
|
||||
<div class="status-left">
|
||||
<div class="status-dot"></div>
|
||||
<span class="status-label">CONNECTED</span>
|
||||
</div>
|
||||
<div class="status-right">
|
||||
<span class="status-preset">Plexi Crunch</span>
|
||||
<span class="status-cpu">CPU 12%</span>
|
||||
<span class="status-sig">▃▅▇█</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Main Area ───────────────────────────── -->
|
||||
<div class="main-area">
|
||||
|
||||
<!-- ── Block Chain ────────────────────────── -->
|
||||
<div class="chain-section">
|
||||
<div class="chain-label">Signal Chain</div>
|
||||
<div class="chain-scroll">
|
||||
<div class="terminator terminator-in">IN</div>
|
||||
|
||||
<div class="connector"></div>
|
||||
|
||||
<div class="block-chip active" onclick="selectBlock(this, 0)">
|
||||
<div class="block-top">
|
||||
<div class="block-led on"></div>
|
||||
<span class="block-name">Compressor</span>
|
||||
<span class="block-type">DYN</span>
|
||||
</div>
|
||||
<div class="block-drag"></div>
|
||||
</div>
|
||||
|
||||
<div class="connector"></div>
|
||||
|
||||
<div class="block-chip" onclick="selectBlock(this, 1)">
|
||||
<div class="block-top">
|
||||
<div class="block-led on"></div>
|
||||
<span class="block-name">Overdrive</span>
|
||||
<span class="block-type">DRV</span>
|
||||
</div>
|
||||
<div class="block-drag"></div>
|
||||
</div>
|
||||
|
||||
<div class="connector"></div>
|
||||
|
||||
<div class="block-chip bypassed" onclick="selectBlock(this, 2)">
|
||||
<div class="block-top">
|
||||
<div class="block-led off"></div>
|
||||
<span class="block-name">Chorus</span>
|
||||
<span class="block-type">MOD</span>
|
||||
</div>
|
||||
<div class="block-drag"></div>
|
||||
</div>
|
||||
|
||||
<div class="connector"></div>
|
||||
|
||||
<div class="block-chip" onclick="selectBlock(this, 3)">
|
||||
<div class="block-top">
|
||||
<div class="block-led on"></div>
|
||||
<span class="block-name">Delay</span>
|
||||
<span class="block-type">TIME</span>
|
||||
</div>
|
||||
<div class="block-drag"></div>
|
||||
</div>
|
||||
|
||||
<div class="connector"></div>
|
||||
|
||||
<div class="block-chip" onclick="selectBlock(this, 4)">
|
||||
<div class="block-top">
|
||||
<div class="block-led on"></div>
|
||||
<span class="block-name">Reverb</span>
|
||||
<span class="block-type">AMB</span>
|
||||
</div>
|
||||
<div class="block-drag"></div>
|
||||
</div>
|
||||
|
||||
<div class="connector"></div>
|
||||
|
||||
<div class="terminator terminator-out">OUT</div>
|
||||
|
||||
<div class="add-block" title="Add block">+</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Params Panel ───────────────────────── -->
|
||||
<div class="params-panel" id="paramsPanel">
|
||||
<div class="params-header">
|
||||
<div class="params-title" id="paramsTitle">Parameters — Compressor</div>
|
||||
<div class="param-actions">
|
||||
<div class="param-action" style="color:#3AB87A">○ Bypass</div>
|
||||
<div class="param-action" style="color:#C84040">✕ Delete</div>
|
||||
<div class="param-action">↻ Reset</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="knob-grid">
|
||||
<div class="knob-wrap">
|
||||
<div class="knob-shell">
|
||||
<div class="knob-bg"></div>
|
||||
<div class="knob-dot" style="transform: translateX(-50%) rotate(-30deg)"></div>
|
||||
</div>
|
||||
<div class="knob-value" style="color: #E8A030">-20</div>
|
||||
<div class="knob-label">Threshold</div>
|
||||
</div>
|
||||
<div class="knob-wrap">
|
||||
<div class="knob-shell">
|
||||
<div class="knob-bg"></div>
|
||||
<div class="knob-dot" style="transform: translateX(-50%) rotate(45deg)"></div>
|
||||
</div>
|
||||
<div class="knob-value" style="color: #3A7BA8">3:1</div>
|
||||
<div class="knob-label">Ratio</div>
|
||||
</div>
|
||||
<div class="knob-wrap">
|
||||
<div class="knob-shell">
|
||||
<div class="knob-bg"></div>
|
||||
<div class="knob-dot" style="transform: translateX(-50%) rotate(-15deg)"></div>
|
||||
</div>
|
||||
<div class="knob-value" style="color: #E8A030">5</div>
|
||||
<div class="knob-label">Attack</div>
|
||||
</div>
|
||||
<div class="knob-wrap">
|
||||
<div class="knob-shell">
|
||||
<div class="knob-bg"></div>
|
||||
<div class="knob-dot" style="transform: translateX(-50%) rotate(20deg)"></div>
|
||||
</div>
|
||||
<div class="knob-value" style="color: #E8A030">100</div>
|
||||
<div class="knob-label">Release</div>
|
||||
</div>
|
||||
<div class="knob-wrap">
|
||||
<div class="knob-shell">
|
||||
<div class="knob-bg"></div>
|
||||
<div class="knob-dot" style="transform: translateX(-50%) rotate(60deg)"></div>
|
||||
</div>
|
||||
<div class="knob-value" style="color: #E8A030">1.0</div>
|
||||
<div class="knob-label">Makeup</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="slider-group">
|
||||
<div class="slider-label-row">
|
||||
<span class="slider-label">Mix</span>
|
||||
<span class="slider-value">65%</span>
|
||||
</div>
|
||||
<div class="slider-track">
|
||||
<div class="slider-fill" style="width:65%"></div>
|
||||
<div class="slider-thumb" style="left:65%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Footswitch Bar ────────────────────────── -->
|
||||
<div class="footswitch-bar">
|
||||
<div class="bank-btn">◀</div>
|
||||
|
||||
<div class="footswitch active">
|
||||
<div class="fs-number">1</div>
|
||||
<div class="fs-label">Plexi Crunch</div>
|
||||
<div class="fs-sub">Preset</div>
|
||||
</div>
|
||||
<div class="footswitch">
|
||||
<div class="fs-number">2</div>
|
||||
<div class="fs-label">Mod Hi-Gain</div>
|
||||
<div class="fs-sub">Preset</div>
|
||||
</div>
|
||||
<div class="footswitch">
|
||||
<div class="fs-number">3</div>
|
||||
<div class="fs-label">Clean Chimey</div>
|
||||
<div class="fs-sub">Preset</div>
|
||||
</div>
|
||||
<div class="footswitch">
|
||||
<div class="fs-number">4</div>
|
||||
<div class="fs-label">Soul Blues</div>
|
||||
<div class="fs-sub">Preset</div>
|
||||
</div>
|
||||
|
||||
<div class="bank-btn">▶</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let selectedIdx = 0;
|
||||
const chips = document.querySelectorAll('.block-chip');
|
||||
const paramsTitle = document.getElementById('paramsTitle');
|
||||
|
||||
function selectBlock(el, idx) {
|
||||
chips.forEach(c => c.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
selectedIdx = idx;
|
||||
const name = el.querySelector('.block-name').textContent;
|
||||
paramsTitle.textContent = 'Parameters — ' + name;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,397 @@
|
||||
# Helix Stadium UX Redesign — Spec & Mockup
|
||||
|
||||
> **Task:** Redesign the Pi Multi-FX Pedal UI from a mobile-first vertical tab layout to a
|
||||
> Helix Stadium-style horizontal block chain with footswitch navigation.
|
||||
> **Status:** Research / Spec Complete
|
||||
> **Date:** 2026-06-12
|
||||
|
||||
---
|
||||
|
||||
## 1. Design Philosophy
|
||||
|
||||
The Helix Stadium is designed for **stage use**: large touch targets, high contrast at
|
||||
arm's length, minimal scrolling, instant access to block placement and parameters.
|
||||
Our redesign translates this philosophy into a legally distinct UI using the existing
|
||||
amber/blue/green palette and JetBrains Mono + Inter typography.
|
||||
|
||||
### Key UX Principles
|
||||
|
||||
| Principle | Application |
|
||||
|-----------|-------------|
|
||||
| One primary view | Block chain is the main screen — IN → blocks → OUT |
|
||||
| Touch-first | 60-80px minimum tap targets, no hover-dependent controls |
|
||||
| Context-sensitive | Select a block → knob/slider panel shows its params |
|
||||
| Visual feedback | Bypass LED, bypassed opacity, active block highlight |
|
||||
| No dead space | Every pixel is a control or shows status |
|
||||
|
||||
---
|
||||
|
||||
## 2. Screen Layout (Top → Bottom)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ STATUS BAR │
|
||||
│ ● CONNECTED Preset Name CPU 12% ▂▃▅▇ │ ~36px
|
||||
├─────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ BLOCK CHAIN (horizontal scroll) │
|
||||
│ │ ~80-100px
|
||||
│ [IN] → [Comp] → [OD] → [Rev] → [OUT] │
|
||||
│ ↑ selected │
|
||||
│ ───────────────────────────────────── │
|
||||
│ ⬡ BYPASS ⬡ DRIVE ⬡ TONE ⬡ GAIN │
|
||||
│ PARAMETER KNOBS / SLIDERS │ flex
|
||||
│ │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ FOOTSWITCH BAR │
|
||||
│ [1 PlexiC] [2 ModHi] [3 Clean] [4 Soul] │ ~64-72px
|
||||
│ [5 Metal] [6 Studio] [7 Acous] [8 Twang]│
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.1 Status Bar (36px)
|
||||
|
||||
- **Left:** Green/red connection dot, "CONNECTED" / "OFFLINE" label
|
||||
- **Right:** Current preset name (if loaded), CPU %, audio signal meter
|
||||
- Background: `#141418` (panel) with `#2A2A32` bottom border
|
||||
- Same as current, but more compact
|
||||
|
||||
### 2.2 Block Chain (80-100px)
|
||||
|
||||
- Horizontal scrolling flex container with overflow-x: auto
|
||||
- Left terminators: **IN** chip (blue) — fixed, always first
|
||||
- Right terminator: **OUT** chip (blue) — fixed, always last
|
||||
- Blocks between: **amber** chips, min 48px height (target 64px)
|
||||
- Gap between blocks shown as a connector line (`→` or border-based line)
|
||||
- Scrollable via horizontal swipe or footswitch left/right buttons
|
||||
- Visual distinction between bypassed (opacity 0.4) and active blocks
|
||||
|
||||
**Block Chip Spec:**
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ ██ Compressor │ ← green LED when active, dim when bypassed
|
||||
│ ODRIVE │ ← subtype badge (small)
|
||||
│ ▾ │ ← expand indicator (tap to select)
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
#### Interaction:
|
||||
- **Tap:** Select block → parameter panel updates below
|
||||
- **Long press / drag handle:** Reorder (drag-and-drop within chain)
|
||||
- **Double tap:** Toggle bypass
|
||||
- **Swipe left on block:** Delete (with undo toast)
|
||||
|
||||
### 2.3 Parameters Panel (flex, fills remaining space)
|
||||
|
||||
When a block is selected, the panel below the chain shows its editable parameters.
|
||||
Each param renders as either a **big knob** (continuous, like drive, tone, rate) or
|
||||
a **big slider** (linear range, like volume, mix, feedback) or a **toggle** (bypass, enable).
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ PARAMETERS — Compressor │ ~24px section label
|
||||
│ │
|
||||
│ ┌──────┐ ┌──────┐ ┌──────┐ │
|
||||
│ │ ╭──╮ │ │ ╭──╮ │ │ ╭──╮ │ │
|
||||
│ │ ╰──╯ │ │ ╰──╯ │ │ ╰──╯ │ │ knobs 52-64px
|
||||
│ │ -24 │ │ 3:1 │ │ 5ms │ │
|
||||
│ │THRESH│ │RATIO │ │ATTACK│ │
|
||||
│ └──────┘ └──────┘ └──────┘ │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────┐ │
|
||||
│ │ ○ MASTER VOLUME ▓▓▓▓░░│ │ slider
|
||||
│ └───────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [○ BYPASS BLOCK] [✕ DELETE] [↻ RESET] │ action buttons
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
When no block is selected, this panel shows **master volume**, **global bypass**,
|
||||
and **active preset** info (like current RigScreen).
|
||||
|
||||
### 2.4 Footswitch Bar (64-72px)
|
||||
|
||||
- **Fixed at bottom** — never scrolls away
|
||||
- **4 footswitch buttons** shown at a time (with bank left/right pagination for 8+)
|
||||
- Each button = big rounded rect with **scribble strip** (number + short label)
|
||||
- Colors match preset/bank colors (amber, red, blue, green)
|
||||
|
||||
```
|
||||
┌──────────┬──────────┬──────────┬──────────┐
|
||||
│ 1 │ 2 │ 3 │ 4 │
|
||||
│ Plexi │ Modern │ Clean │ Southern │ ← scribble strip
|
||||
│ Crunch │ Hi-Gain │ Chimey │ Blues │
|
||||
├──────────┼──────────┼──────────┼──────────┤
|
||||
│ ◀ │ │ │ ▶│ ← bank nav
|
||||
└──────────┴──────────┴──────────┴──────────┘
|
||||
```
|
||||
|
||||
#### Footswitch Interactions:
|
||||
- **Tap:** Load that preset
|
||||
- **Long press:** Save current chain to that slot
|
||||
- **Bank nav (◀ ▶):** Switch between banks (pages of 4)
|
||||
- **Scribble strip:** Shows preset name (truncated to ~8-10 chars) rendered in amber/green
|
||||
|
||||
---
|
||||
|
||||
## 3. Component Tree
|
||||
|
||||
```
|
||||
App
|
||||
├── <style>{CSS}</style>
|
||||
├── Shell (100vh, flex column)
|
||||
│ ├── StatusBar
|
||||
│ │ ├── ConnectionDot
|
||||
│ │ ├── PresetLabel
|
||||
│ │ └── SystemStats (CPU + signal)
|
||||
│ ├── ChainPanel (flex: 1)
|
||||
│ │ ├── BlockChain (horizontal scroll)
|
||||
│ │ │ ├── ChainTerminator("IN", fixed, blue)
|
||||
│ │ │ ├── BlockChip × N (draggable, amber)
|
||||
│ │ │ │ ├── BypassLED
|
||||
│ │ │ │ ├── BlockName
|
||||
│ │ │ │ ├── TypeBadge
|
||||
│ │ │ │ └── DragHandle
|
||||
│ │ │ ├── ChainConnector × N-1 (→ line)
|
||||
│ │ │ └── ChainTerminator("OUT", fixed, blue)
|
||||
│ │ └── ParamsPanel
|
||||
│ │ ├── (if block selected) KnobGrid
|
||||
│ │ │ └── BigKnob × N
|
||||
│ │ ├── (if block selected) SliderList
|
||||
│ │ │ └── BigSlider × N
|
||||
│ │ ├── (if block selected) BlockActions
|
||||
│ │ │ └── BypassBtn / DeleteBtn / ResetBtn
|
||||
│ │ └── (if none selected) MasterVolume + PresetInfo
|
||||
│ │ ├── VolumeSlider (existing)
|
||||
│ │ └── PresetSummary
|
||||
│ └── FootswitchBar (fixed 64-72px)
|
||||
│ ├── BankLeftBtn (◀)
|
||||
│ ├── FootswitchBtn × 4
|
||||
│ │ └── ScribbleStrip (number + label)
|
||||
│ └── BankRightBtn (▶)
|
||||
```
|
||||
|
||||
### State Shape (React)
|
||||
|
||||
```js
|
||||
const [selectedBlock, setSelectedBlock] = useState(null); // block id or null
|
||||
const [blocks, setBlocks] = useState([]); // current block chain
|
||||
const [activePreset, setActivePreset] = useState(null);
|
||||
const [footswitchBank, setFootswitchBank] = useState(0); // 0-based
|
||||
const [params, setParams] = useState({}); // fetched from API when block selected
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Color & Typography (Existing Design Tokens)
|
||||
|
||||
Use the existing `T` object from App.jsx unchanged. No new colors needed —
|
||||
the amber/blue/green palette maps directly to Helix-style needs:
|
||||
|
||||
| Token | Hex | Usage |
|
||||
|-------|-----|-------|
|
||||
| `T.amber` | `#E8A030` | Active block, selected state, knobs, scribble strip text |
|
||||
| `T.amberDim` | `#7A5218` | Block chip background |
|
||||
| `T.blue` | `#3A7BA8` | IN/OUT terminators, connection dots |
|
||||
| `T.blueDim` | `#1E4060` | Terminator backgrounds |
|
||||
| `T.green` | `#3AB87A` | Bypass LED on, signal OK |
|
||||
| `T.bg` | `#0A0A0C` | Page background |
|
||||
| `T.panel` | `#141418` | Card/section backgrounds |
|
||||
| `T.surface` | `#1C1C22` | Input fields, button surfaces |
|
||||
| `T.border` | `#2A2A32` | Lines, dividers |
|
||||
|
||||
Typography unchanged: **Inter** for body/UI text, **JetBrains Mono** for values/labels.
|
||||
|
||||
---
|
||||
|
||||
## 5. API Integration
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
Mount → GET /api/state
|
||||
├── connected, master_volume, bypass
|
||||
└── current_preset (name, bank, program)
|
||||
|
||||
On mount of preset: GET /api/presets/{bank}/{program}
|
||||
└── chain[].blocks → populates BlockChain
|
||||
|
||||
On block select: GET /api/block-params/{fx_type}
|
||||
└── params[] → renders knobs/sliders
|
||||
|
||||
Param change: PATCH /api/block-params { id, key, value }
|
||||
Toggle bypass: PATCH /api/blocks { id, enabled }
|
||||
Master volume: POST /api/volume { volume }
|
||||
Global bypass: POST /api/bypass/toggle
|
||||
Load preset: POST /api/presets/{bank}/{program}/activate
|
||||
Save preset: PUT /api/presets/{bank}/{program}
|
||||
```
|
||||
|
||||
### Backend Gap: Blocks in `/api/state`
|
||||
|
||||
The current `_gather_state()` does NOT return `blocks[]` — but the UI needs the
|
||||
current block chain to render. Options ranked by preference:
|
||||
|
||||
1. **Extend `/api/state` to include `blocks`** — Add the current preset's chain
|
||||
to the state response. Most efficient: single fetch on mount + WebSocket push.
|
||||
Requires adding ~5 lines to `_gather_state()`.
|
||||
2. **Load preset chain separately** — On mount, fetch `GET /api/presets/{bank}/{program}`
|
||||
to get the chain. Needs bank/program from state. Works without backend change
|
||||
but adds an extra round trip.
|
||||
3. **WebSocket-first** — `/ws` already pushes state on connect. Extend the push
|
||||
to include blocks.
|
||||
|
||||
**Recommendation:** Option 1 (backend change) — it's trivial and eliminates an
|
||||
extra HTTP round trip on every page load.
|
||||
|
||||
---
|
||||
|
||||
## 6. Drag-and-Drop Reordering
|
||||
|
||||
For reordering blocks in the chain, use the **HTML5 Drag and Drop API**
|
||||
(native, no library needed in modern React):
|
||||
|
||||
```jsx
|
||||
// BlockChip
|
||||
<div
|
||||
draggable
|
||||
onDragStart={(e) => e.dataTransfer.setData('text/plain', blockId)}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
const draggedId = e.dataTransfer.getData('text/plain');
|
||||
reorderBlocks(draggedId, blockId); // swap or insert
|
||||
}}
|
||||
>
|
||||
```
|
||||
|
||||
After reorder: `PATCH /api/blocks/reorder { ids: [new order] }` — a new endpoint
|
||||
to persist the sequence. For MVP, keep the reorder local-only (state-only) and
|
||||
save with the preset.
|
||||
|
||||
---
|
||||
|
||||
## 7. Responsive Behavior
|
||||
|
||||
| Viewport | Layout |
|
||||
|----------|--------|
|
||||
| < 480px (phone) | Single column, chain scrolls horizontally, param panel below, footswitch at bottom |
|
||||
| 480-768px (tablet) | Same layout, more knobs visible per row |
|
||||
| > 768px (desktop) | Optional: chain + params side-by-side. Footswitch always at bottom. |
|
||||
|
||||
The current app is capped at `maxWidth: 440px` — Stage UI should use **full width**
|
||||
on desktop (for the pedal's browser UI) while remaining touch-friendly on tablets.
|
||||
|
||||
---
|
||||
|
||||
## 8. Implementation Phases
|
||||
|
||||
### Phase 1: Block Chain View
|
||||
- [ ] Extract block rendering into `ChainPanel` + `BlockChain` + `BlockChip` components
|
||||
- [ ] Build horizontal scroll layout with IN/OUT terminators
|
||||
- [ ] Wire block select → highlight state
|
||||
- [ ] Bypass toggle on double-tap
|
||||
- [ ] Fetch and display blocks from `/api/state` (requires backend change #1)
|
||||
|
||||
### Phase 2: Parameters Panel
|
||||
- [ ] Build `ParamsPanel` component with conditional rendering
|
||||
- [ ] Adapt existing `Knob` component for bigger stage-friendly size
|
||||
- [ ] Add `BigSlider` component for linear params
|
||||
- [ ] Wire param change → `PATCH /api/block-params`
|
||||
- [ ] Show master volume / preset info when no block selected
|
||||
|
||||
### Phase 3: Footswitch Bar
|
||||
- [ ] Build `FootswitchBar` component
|
||||
- [ ] Scribble strip rendering (number + truncated name)
|
||||
- [ ] Bank pagination (◀ ▶)
|
||||
- [ ] Tap → load preset via API
|
||||
- [ ] Long press → save current chain as preset
|
||||
|
||||
### Phase 4: Drag-and-Drop Reorder
|
||||
- [ ] Implement HTML5 DnD on `BlockChip`
|
||||
- [ ] Reorder state on drop
|
||||
- [ ] Persist to API
|
||||
|
||||
### Phase 5: Polish & Legal Distinctiveness
|
||||
- [ ] Custom block icon SVGs per FX type
|
||||
- [ ] Scribble strip font rendering optimization
|
||||
- [ ] Touch sensitivity tuning
|
||||
- [ ] Build to `dist/` and verify backend serves at `/`
|
||||
|
||||
---
|
||||
|
||||
## 9. Size & Layout Constants
|
||||
|
||||
```css
|
||||
:root {
|
||||
--block-height: 64px;
|
||||
--footswitch-height: 68px;
|
||||
--statusbar-height: 36px;
|
||||
--knob-size: 56px;
|
||||
--slider-height: 36px;
|
||||
--min-tap-target: 48px;
|
||||
--border-radius-block: 8px;
|
||||
--border-radius-footswitch: 6px;
|
||||
--chain-gap: 8px;
|
||||
--connector-width: 16px;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Existing Components to Reuse
|
||||
|
||||
| Component | Status | Changes Needed |
|
||||
|-----------|--------|----------------|
|
||||
| `Knob` | Already exists (SVG arc, touch drag) | Increase default size to 56px, adjustable sizing |
|
||||
| `VUMeter` | Already exists (vertical + horizontal) | None — fits in status bar |
|
||||
| `FXBlock` | Currently vertical card | Rewrite as horizontal `BlockChip` |
|
||||
| `T` design tokens | Already defined | Use unchanged |
|
||||
| CSS styles | Currently in tagged template | Extract into separate `.module.css` or keep inline |
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Backend Changes Required
|
||||
|
||||
### 1. Add `blocks` to `/api/state`
|
||||
|
||||
In `_gather_state()`, add after line 1163:
|
||||
|
||||
```python
|
||||
# Current block chain from active preset
|
||||
if preset:
|
||||
state["blocks"] = [
|
||||
{
|
||||
"id": f"block_{i}",
|
||||
"fx_type": b.fx_type.value,
|
||||
"name": b.fx_type.value.replace("_", " ").title(),
|
||||
"enabled": b.enabled,
|
||||
"bypass": b.bypass,
|
||||
"params": dict(b.params),
|
||||
"nam_model_path": b.nam_model_path,
|
||||
"ir_file_path": b.ir_file_path,
|
||||
}
|
||||
for i, b in enumerate(preset.chain)
|
||||
]
|
||||
else:
|
||||
state["blocks"] = []
|
||||
```
|
||||
|
||||
### 2. (Optional) `/api/blocks/reorder` endpoint
|
||||
|
||||
For persistable drag-and-drop ordering:
|
||||
|
||||
```python
|
||||
@app.put("/api/blocks/reorder")
|
||||
async def reorder_blocks(data: dict):
|
||||
"""Reorder blocks in the current preset chain."""
|
||||
new_ids = data.get("ids", [])
|
||||
# Map old block order to new order based on block ids
|
||||
pm = self.deps.presets
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*End of spec. Ready for handoff to implementation.*
|
||||
Reference in New Issue
Block a user