From f163a24f62843f72228f7940fa4ae774f4d07f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Luis=20Monta=C3=B1es?= Date: Fri, 1 May 2026 18:15:48 +0200 Subject: [PATCH] sandbox/studio: add faders sub-panel between knobs and seq MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four channel-volume faders matching the patch (KICK/HAT/MEL/MIX), in their own sub-panel with the same brushed-metal treatment as .knobs and .seq. - Track is a deep slot cut into the panel — heavy inset shadow, thin highlight at the bottom edge for chamfer feel. - Side ticks drawn with repeating-linear-gradient (1px on / 6px off). - Cap uses the same metal language as the knob disc: brushed stripes + vertical light gradient + grip line in the middle + 4-edge bevel via inset shadows + drop shadow underneath. - Click anywhere on the track jumps the cap to that value, then dragging continues. Pointer capture on the track itself so the cap follows even outside the bounds. Co-Authored-By: Claude Opus 4.7 (1M context) --- web/sandbox/studio.html | 164 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 162 insertions(+), 2 deletions(-) diff --git a/web/sandbox/studio.html b/web/sandbox/studio.html index 5a93bff..109dadb 100644 --- a/web/sandbox/studio.html +++ b/web/sandbox/studio.html @@ -85,7 +85,7 @@ gap: 12px; } .left-stack { display: grid; grid-template-rows: auto 1fr auto; gap: 8px; min-width: 0; } - .right-stack { display: grid; grid-template-rows: auto auto; gap: 8px; min-width: 0; } + .right-stack { display: grid; grid-template-rows: auto auto auto; gap: 8px; min-width: 0; } /* corner screws — only on the outermost frame */ .screw { @@ -235,7 +235,7 @@ /* ===================================================================== */ /* shared brushed-metal panel treatment for sub-panels inside the rack */ - .knobs, .seq { + .knobs, .seq, .faders { border: 1px solid var(--hw-edge); border-radius: 6px; background: @@ -274,6 +274,82 @@ .hw-knob { cursor: grab; touch-action: none; } .hw-knob.dragging { cursor: grabbing; } + /* faders sub-panel */ + .faders { + padding: 10px 10px 14px; + display: flex; + justify-content: space-around; + align-items: flex-start; + gap: 8px; + } + .fader-cell { + display: flex; flex-direction: column; align-items: center; + gap: 4px; flex: 1; min-width: 0; + } + .fader-cell .label { + font-size: 8px; letter-spacing: 0.18em; text-transform: uppercase; + color: var(--hw-engrave); + text-shadow: 0 1px 0 rgba(0,0,0,0.7), 0 -1px 0 rgba(255,220,180,0.04); + } + /* track is a deep slot cut into the panel */ + .fd-track { + position: relative; + width: 5px; height: 78px; + background: + linear-gradient(180deg, #050402 0%, #0a0808 50%, #060403 100%); + border-radius: 3px; + box-shadow: + inset 0 1px 3px rgba(0,0,0,0.85), + inset 0 0 0 1px rgba(0,0,0,0.6), + inset 0 -1px 0 rgba(255,220,180,0.05), + 0 1px 0 rgba(255,220,180,0.04); + cursor: pointer; touch-action: none; + } + /* tick marks on either side of the track */ + .fd-ticks { + position: absolute; top: 4px; bottom: 4px; + width: 16px; pointer-events: none; + background: repeating-linear-gradient( + 180deg, + rgba(255,220,180,0.20) 0px, + rgba(255,220,180,0.20) 1px, + transparent 1px, + transparent 7px); + } + .fd-ticks.left { left: -18px; } + .fd-ticks.right { right: -18px; } + /* the cap — metallic, sits on the track */ + .fd-cap { + position: absolute; + left: 50%; transform: translate(-50%, -50%); + width: 26px; height: 14px; + border-radius: 2px; + background: + /* horizontal grip line in middle */ + linear-gradient(180deg, + transparent 38%, rgba(0,0,0,0.55) 46%, rgba(0,0,0,0.55) 54%, + transparent 62%), + /* very fine brushed stripes */ + repeating-linear-gradient(0deg, + rgba(255,250,235,0.04) 0px, rgba(255,250,235,0.04) 1px, + rgba(0,0,0,0.05) 1px, rgba(0,0,0,0.05) 2px), + /* vertical light gradient */ + linear-gradient(180deg, #b6b0a4 0%, #807a6e 45%, #3a352e 80%, #1a1612 100%); + box-shadow: + inset 0 1px 0 rgba(255,252,240,0.55), + inset 0 -1px 0 rgba(0,0,0,0.7), + inset 1px 0 0 rgba(255,250,235,0.15), + inset -1px 0 0 rgba(0,0,0,0.45), + 0 2px 3px rgba(0,0,0,0.6); + cursor: grab; touch-action: none; + } + .fd-cap.dragging { cursor: grabbing; } + .fader-cell .value { + font-size: 10px; font-variant-numeric: tabular-nums; + color: var(--hw-fg-hi); letter-spacing: 0.05em; + margin-top: 2px; + } + /* sequencer sub-panel — inherits brushed-metal background from .knobs,.seq */ .seq { padding: 8px 10px; @@ -380,6 +456,45 @@ +
+
+ kick +
+
+
+
+
+ 0.78 +
+
+ hat +
+
+
+
+
+ 0.55 +
+
+ mel +
+
+
+
+
+ 0.62 +
+
+ mix +
+
+
+
+
+ 0.85 +
+
+
kicks @@ -677,6 +792,51 @@ document.querySelectorAll('canvas.hw-knob').forEach((canvas) => { }); }); +// =========================================================================== +// faders — drag the cap or click anywhere on the track to jump +// =========================================================================== +document.querySelectorAll('.fader-cell').forEach((cell) => { + const track = cell.querySelector('.fd-track'); + const cap = cell.querySelector('.fd-cap'); + const valueEl = cell.querySelector('.value'); + let value = parseFloat(track.dataset.value || '0.5'); + + function paint() { + const trackH = track.clientHeight; + cap.style.top = ((1 - value) * trackH) + 'px'; + if (valueEl) valueEl.textContent = value.toFixed(2); + } + // wait for layout + requestAnimationFrame(paint); + + function valueAt(clientY) { + const rect = track.getBoundingClientRect(); + const norm = 1 - (clientY - rect.top) / rect.height; + return Math.max(0, Math.min(1, norm)); + } + + function startDrag(e, jumpToPointer) { + e.preventDefault(); + cap.classList.add('dragging'); + track.setPointerCapture(e.pointerId); + document.body.style.cursor = 'grabbing'; + if (jumpToPointer) { value = valueAt(e.clientY); paint(); } + const onMove = (ev) => { value = valueAt(ev.clientY); paint(); }; + const onUp = () => { + cap.classList.remove('dragging'); + document.body.style.cursor = ''; + track.removeEventListener('pointermove', onMove); + track.removeEventListener('pointerup', onUp); + track.removeEventListener('pointercancel', onUp); + }; + track.addEventListener('pointermove', onMove); + track.addEventListener('pointerup', onUp); + track.addEventListener('pointercancel', onUp); + } + // click on track jumps to that position; drag continues from there + track.addEventListener('pointerdown', (e) => startDrag(e, true)); +}); + // =========================================================================== // step LEDs // ===========================================================================