sandbox/studio: knobs in charcoal black, neutral palette, drop sidebar

Aligning the mockup more closely with the reference image:

- Knob body is now a pure charcoal/black radial gradient (no warm
  brown tint). Top-left specular dim, top crescent gloss, outer dark
  ring. Drop shadow underneath so the knob "sits on" the panel.
- Value readouts ("120.0", "2.50", ...) in cream/white, no inset
  amber LCD frame. Amber is reserved for arcs / LEDs / bullet labels
  exclusively.
- Panel palette pulled toward neutral dark (less brown saturation).
- Sidebar with the description bullets removed; layout is just the
  hardware unit at full width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jose Luis Montañes
2026-05-01 17:56:39 +02:00
parent 9b0c7c0cac
commit 64620ef75a

View File

@@ -4,293 +4,309 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>code-sinth — studio hardware emulation</title> <title>code-sinth — studio hardware emulation</title>
<style> <style>
/* === warm palette inspired by Moog / Roland / Korg vintage rack gear === */
:root { :root {
--hw-bg: #1a1612; --bg: #131210;
--hw-panel: #2b2620; --hw-bg: #1a1916; /* panel: neutral dark, very faint warm tint */
--hw-panel-hi: #342f28; --hw-bg-hi: #232220;
--hw-panel-lo: #1f1b16; --hw-bg-lo: #0e0d0b;
--hw-divider: #11100d; --hw-edge: #050505;
--hw-screw: #6a635a; --hw-screw: #5a5550;
--hw-screw-dk: #2a2520; --hw-amber: #e8a050; /* used ONLY for arcs / LEDs / labels */
--hw-amber: #ffae5c; --hw-amber-hi: #f4c890;
--hw-amber-hi: #ffd5a0; --hw-amber-mut: rgba(232, 160, 80, 0.18);
--hw-amber-mut: #5a3d20; --hw-amber-glow: rgba(232, 160, 80, 0.45);
--hw-amber-off: #2a2118; --hw-amber-off: #28201a;
--hw-amber-glow: rgba(255, 174, 92, 0.55); --hw-fg: #a8a39a; /* general text: warm-neutral cream */
--hw-fg: #b9a98c; --hw-fg-hi: #d8d0c0;
--hw-fg-bright: #d8c9a8; --hw-fg-dim: #6c6660;
--hw-fg-dim: #786a55; --hw-engrave: #8a847a;
--hw-engrave: #8c7d62; --hw-screen: #161412;
--hw-screen-bg: #1d1814; --hw-led-red: #c84838;
--hw-syn-com: #5a4a3a;
--hw-syn-kw: #e0916a;
--hw-syn-num: #d8a060;
--hw-syn-fn: #c8a878;
--hw-syn-atom: #b8a070;
--hw-syn-str: #a08862;
--hw-led-red: #e85a3a;
--hw-led-green: #6aca8a; --hw-led-green: #6aca8a;
--hw-syn-com: #5a554f;
--hw-syn-kw: #d68868;
--hw-syn-num: #d6a268;
--hw-syn-fn: #c8a878;
--hw-syn-id: #b8b0a0;
--hw-syn-op: #6c6660;
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
html, body { height: 100%; margin: 0; html, body { height: 100%; margin: 0; background: var(--bg); color: var(--hw-fg);
background:
radial-gradient(circle at 30% 20%, #221c16 0%, #14110d 70%),
var(--hw-bg);
color: var(--hw-fg);
font-family: 'JetBrains Mono', 'Cascadia Code', Consolas, monospace; font-family: 'JetBrains Mono', 'Cascadia Code', Consolas, monospace;
font-size: 13px; } font-size: 12px; }
body { padding: 28px; min-height: 100vh; } body { padding: 32px; min-height: 100vh; display: flex; align-items: flex-start; justify-content: center; }
/* ===================================================================== */ /* ===================================================================== */
/* Panel chrome — beveled metal panels with corner screws */ /* Outer layout: sidebar (description) + hardware unit */
/* ===================================================================== */ /* ===================================================================== */
.panel { .layout {
width: 100%;
max-width: 980px;
}
/* ===================================================================== */
/* Hardware unit — single large rounded panel containing everything */
/* ===================================================================== */
.hardware {
position: relative; position: relative;
background: background:
linear-gradient(180deg, var(--hw-panel-hi) 0%, var(--hw-panel) 50%, var(--hw-panel-lo) 100%); radial-gradient(ellipse at 30% 0%, var(--hw-bg-hi) 0%, var(--hw-bg) 50%, var(--hw-bg-lo) 100%);
border: 1px solid #0a0805; border: 1px solid var(--hw-edge);
border-radius: 6px; border-radius: 10px;
box-shadow: box-shadow:
inset 0 1px 0 rgba(255, 220, 180, 0.04), inset 0 1px 0 rgba(255, 220, 180, 0.05),
inset 0 -1px 0 rgba(0, 0, 0, 0.6), inset 0 -1px 0 rgba(0, 0, 0, 0.7),
0 1px 0 rgba(255, 220, 180, 0.03), inset 0 0 0 1px rgba(232, 160, 80, 0.04),
0 6px 16px rgba(0, 0, 0, 0.45); 0 1px 0 rgba(255, 220, 180, 0.02),
} 0 8px 24px rgba(0, 0, 0, 0.55);
.panel::before, .panel::after, .panel > .screw-tl, .panel > .screw-tr, padding: 12px;
.panel > .screw-bl, .panel > .screw-br { display: grid;
/* nothing; pseudo-elements not enough — using real spans below */ grid-template-columns: 1fr 220px;
grid-template-rows: auto 1fr;
gap: 10px;
} }
/* corner screws — only on the outermost frame */
.screw { .screw {
position: absolute; width: 10px; height: 10px; border-radius: 50%; position: absolute;
width: 8px; height: 8px; border-radius: 50%;
background: background:
radial-gradient(circle at 30% 25%, #9a8e7c 0%, #4a4138 55%, #1a1510 100%); radial-gradient(circle at 30% 25%, #8a7d6a 0%, #3a3025 60%, #100c08 100%);
box-shadow: box-shadow:
inset 0 -1px 1px rgba(0,0,0,0.7), inset 0 -1px 1px rgba(0,0,0,0.7),
inset 0 1px 0 rgba(255,220,180,0.2), inset 0 1px 0 rgba(255,220,180,0.18),
0 1px 1px rgba(0,0,0,0.6); 0 1px 1px rgba(0,0,0,0.6);
pointer-events: none; pointer-events: none;
z-index: 5;
} }
.screw::after { .screw::after {
content: ''; position: absolute; inset: 0; content: ''; position: absolute; inset: 0;
background: linear-gradient(45deg, transparent 44%, rgba(0,0,0,0.65) 47%, background: linear-gradient(45deg, transparent 44%, rgba(0,0,0,0.55) 47%,
rgba(0,0,0,0.65) 53%, transparent 56%); rgba(0,0,0,0.55) 53%, transparent 56%);
border-radius: 50%; border-radius: 50%;
} }
.screw-tl { top: 6px; left: 6px; } .hardware > .screw.tl { top: 8px; left: 8px; }
.screw-tr { top: 6px; right: 6px; } .hardware > .screw.tr { top: 8px; right: 8px; }
.screw-bl { bottom: 6px; left: 6px; } .hardware > .screw.bl { bottom: 8px; left: 8px; }
.screw-br { bottom: 6px; right: 6px; } .hardware > .screw.br { bottom: 8px; right: 8px; }
/* ===================================================================== */ /* ===================================================================== */
/* Layout */ /* Header bar inside the hardware unit */
/* ===================================================================== */ /* ===================================================================== */
.frame { display: grid; grid-template-rows: auto 1fr; gap: 14px; .hw-header {
max-width: 1100px; margin: 0 auto; } grid-column: 1 / -1;
display: flex;
/* --- header bar --- */ align-items: center;
header.panel { display: flex; align-items: center; gap: 18px; padding: 10px 36px; height: 50px; } gap: 14px;
.hw-btn { background: linear-gradient(180deg, #3a342c 0%, #1f1b16 100%); padding: 4px 26px;
border: 1px solid #0a0805; border-radius: 4px; height: 36px;
color: var(--hw-fg-bright); padding: 6px 14px; color: var(--hw-fg-dim);
font-family: inherit; font-size: 11px; cursor: pointer; font-size: 10px;
letter-spacing: 0.12em; text-transform: uppercase; letter-spacing: 0.08em;
box-shadow: inset 0 1px 0 rgba(255,220,180,0.1), }
inset 0 -1px 0 rgba(0,0,0,0.6), .hw-btn {
0 1px 2px rgba(0,0,0,0.5); background: linear-gradient(180deg, #2c261f 0%, #14110d 100%);
transition: all 80ms; } border: 1px solid var(--hw-edge);
border-radius: 12px;
color: var(--hw-fg-hi);
padding: 4px 12px;
font-family: inherit;
font-size: 10px;
cursor: pointer;
letter-spacing: 0.1em;
text-transform: uppercase;
box-shadow:
inset 0 1px 0 rgba(255,220,180,0.08),
inset 0 -1px 0 rgba(0,0,0,0.55),
0 1px 1px rgba(0,0,0,0.4);
display: inline-flex; align-items: center; gap: 5px;
transition: color 80ms;
}
.hw-btn:hover { color: var(--hw-amber); } .hw-btn:hover { color: var(--hw-amber); }
.hw-btn:active { transform: translateY(1px); .hw-btn:active { transform: translateY(1px);
box-shadow: inset 0 1px 4px rgba(0,0,0,0.6); } box-shadow: inset 0 1px 3px rgba(0,0,0,0.6); }
.hw-btn .ico { display: inline-block; width: 0; height: 0; .hw-btn .led-red {
border-left: 7px solid var(--hw-led-green); width: 6px; height: 6px; border-radius: 50%;
border-top: 5px solid transparent; background: var(--hw-led-red);
border-bottom: 5px solid transparent; box-shadow: 0 0 4px var(--hw-led-red);
vertical-align: middle; margin-right: 6px; animation: led-pulse 2.2s ease-in-out infinite;
filter: drop-shadow(0 0 4px var(--hw-led-green)); } }
.hw-led { display: inline-block; width: 7px; height: 7px; border-radius: 50%; .hw-btn .ico-play {
background: var(--hw-led-red); width: 0; height: 0;
box-shadow: 0 0 6px var(--hw-led-red), border-left: 6px solid var(--hw-led-green);
inset 0 0 2px rgba(0,0,0,0.7); border-top: 4px solid transparent;
vertical-align: middle; margin-right: 6px; border-bottom: 4px solid transparent;
animation: led-pulse 2s ease-in-out infinite; } filter: drop-shadow(0 0 3px var(--hw-led-green));
}
@keyframes led-pulse { @keyframes led-pulse {
0%, 100% { box-shadow: 0 0 4px var(--hw-led-red); } 0%, 100% { box-shadow: 0 0 3px var(--hw-led-red); }
50% { box-shadow: 0 0 10px var(--hw-led-red); } 50% { box-shadow: 0 0 8px var(--hw-led-red); }
} }
.engrave { color: var(--hw-engrave); font-size: 10px; .hw-header .num { color: var(--hw-amber); font-variant-numeric: tabular-nums; }
letter-spacing: 0.18em; text-transform: uppercase; .hw-header .right { margin-left: auto; color: var(--hw-engrave); letter-spacing: 0.18em; }
text-shadow: 0 1px 0 rgba(0,0,0,0.7), 0 -1px 0 rgba(255,220,180,0.04); }
.stat { color: var(--hw-fg-dim); font-size: 11px; letter-spacing: 0.05em; }
.stat strong { color: var(--hw-fg-bright); font-weight: 500;
font-variant-numeric: tabular-nums; }
/* --- main split --- */ /* ===================================================================== */
main.row { display: grid; grid-template-columns: 1fr 320px; gap: 14px; /* Code "screen" — left, big */
min-height: 0; } /* ===================================================================== */
.screen {
/* --- code area (the "screen") --- */ background: var(--hw-screen);
.screen.panel { padding: 14px 18px 26px; min-height: 460px; border: 1px solid var(--hw-edge);
display: flex; flex-direction: column; border-radius: 6px;
background: padding: 14px 18px 10px;
linear-gradient(180deg, #181410 0%, var(--hw-screen-bg) 12%,
var(--hw-screen-bg) 88%, #181410 100%);
border-color: #050402; }
.screen .glass {
flex: 1; padding: 14px 18px;
background: var(--hw-screen-bg);
border: 1px solid #0a0805;
border-radius: 4px;
box-shadow: box-shadow:
inset 0 0 18px rgba(0,0,0,0.6), inset 0 0 0 1px rgba(232,160,80,0.04),
inset 0 0 1px rgba(255, 174, 92, 0.06), inset 0 1px 8px rgba(0,0,0,0.5);
inset 0 0 60px rgba(255, 174, 92, 0.02); display: flex; flex-direction: column;
font-size: 13px; line-height: 22px; min-height: 320px;
overflow: auto; overflow: hidden;
} }
.code-line { display: flex; gap: 18px; white-space: pre; } .glass {
.ln { color: var(--hw-syn-com); user-select: none; min-width: 22px; text-align: right; } flex: 1; overflow: auto;
font-size: 12px; line-height: 19px;
}
.code-line { display: flex; gap: 14px; white-space: pre; }
.ln { color: var(--hw-syn-com); user-select: none; min-width: 16px; text-align: right; }
.syn-com { color: var(--hw-syn-com); font-style: italic; }
.syn-kw { color: var(--hw-syn-kw); } .syn-kw { color: var(--hw-syn-kw); }
.syn-num { color: var(--hw-syn-num); } .syn-num { color: var(--hw-syn-num); }
.syn-fn { color: var(--hw-syn-fn); } .syn-fn { color: var(--hw-syn-fn); }
.syn-atom{ color: var(--hw-syn-atom); } .syn-id { color: var(--hw-syn-id); }
.syn-str { color: var(--hw-syn-str); } .syn-op { color: var(--hw-syn-op); }
.syn-com { color: var(--hw-syn-com); font-style: italic; } .syn-arrow { color: var(--hw-amber); }
.syn-id { color: var(--hw-fg-bright); }
.syn-op { color: var(--hw-fg-dim); }
.screen .status-line { .status-line {
margin-top: 10px; padding: 0 4px; margin-top: 8px;
font-size: 11px; color: var(--hw-amber); color: var(--hw-amber);
font-size: 11px;
letter-spacing: 0.16em;
text-shadow: 0 0 6px var(--hw-amber-glow); text-shadow: 0 0 6px var(--hw-amber-glow);
letter-spacing: 0.12em; } }
.blink { animation: blink 1.1s step-end infinite; } .blink { animation: blink 1.1s step-end infinite; }
@keyframes blink { 50% { opacity: 0; } } @keyframes blink { 50% { opacity: 0; } }
/* --- right column --- */ /* ===================================================================== */
aside.col { display: flex; flex-direction: column; gap: 14px; } /* Right column */
/* ===================================================================== */
.right-col {
display: flex; flex-direction: column; gap: 10px;
min-width: 0;
}
/* --- knobs panel --- */ /* knobs sub-panel inside hardware */
.knobs.panel { padding: 22px 14px; } .knobs {
.knobs-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } background: linear-gradient(180deg, #1f1a14 0%, #14110d 100%);
.knob-cell { display: flex; flex-direction: column; align-items: center; gap: 6px; } border: 1px solid var(--hw-edge);
.knob-cell .label { font-size: 9px; letter-spacing: 0.18em; border-radius: 6px;
text-transform: uppercase; color: var(--hw-engrave); padding: 14px 12px;
text-shadow: 0 1px 0 rgba(0,0,0,0.7), box-shadow:
0 -1px 0 rgba(255,220,180,0.05); } inset 0 1px 0 rgba(255,220,180,0.04),
inset 0 -1px 0 rgba(0,0,0,0.6);
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px 10px;
}
.knob-cell {
display: flex; flex-direction: column; align-items: center; gap: 4px;
}
.knob-cell .label {
font-size: 9px; letter-spacing: 0.16em; text-transform: uppercase;
color: var(--hw-engrave);
text-shadow: 0 1px 0 rgba(0,0,0,0.6), 0 -1px 0 rgba(255,220,180,0.04);
}
.knob-cell .value { .knob-cell .value {
font-size: 12px; font-variant-numeric: tabular-nums; font-size: 10px; font-variant-numeric: tabular-nums;
color: var(--hw-amber); text-shadow: 0 0 6px var(--hw-amber-glow); color: var(--hw-fg-hi); letter-spacing: 0.05em;
letter-spacing: 0.04em; background: transparent;
/* slight inset / engraved feel for the digital readout */ padding: 0; min-width: 46px; text-align: center;
background: var(--hw-screen-bg);
border: 1px solid #0a0805;
border-radius: 3px;
padding: 1px 8px; min-width: 56px; text-align: center;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.7);
} }
.hw-knob { cursor: grab; touch-action: none; } .hw-knob { cursor: grab; touch-action: none; }
.hw-knob.dragging { cursor: grabbing; } .hw-knob.dragging { cursor: grabbing; }
/* --- sequencer panel --- */ /* sequencer sub-panel */
.seq.panel { padding: 16px 14px 18px; } .seq {
.seq-row { display: flex; align-items: center; gap: 10px; padding: 4px 4px; background: linear-gradient(180deg, #1f1a14 0%, #14110d 100%);
border-top: 1px solid #0a0805; } border: 1px solid var(--hw-edge);
.seq-row:first-of-type { border-top: none; } border-radius: 6px;
.seq-row .label { width: 56px; font-size: 9px; letter-spacing: 0.18em; padding: 8px 10px;
text-transform: uppercase; color: var(--hw-engrave); box-shadow:
text-shadow: 0 1px 0 rgba(0,0,0,0.7), inset 0 1px 0 rgba(255,220,180,0.04),
0 -1px 0 rgba(255,220,180,0.05); } inset 0 -1px 0 rgba(0,0,0,0.6);
.leds { display: grid; grid-template-columns: repeat(16, 1fr); gap: 3px; flex: 1; } display: flex; flex-direction: column; gap: 5px;
.led {
height: 14px; border-radius: 2px;
background:
linear-gradient(180deg, #100c08 0%, var(--hw-amber-off) 100%);
border: 1px solid #050402;
box-shadow: inset 0 1px 1px rgba(0,0,0,0.7),
inset 0 -1px 0 rgba(255,220,180,0.03);
cursor: pointer;
transition: background 80ms, box-shadow 80ms;
} }
.led:hover { background: linear-gradient(180deg, #1c160e 0%, #3a2a18 100%); } .seq-row {
display: flex; align-items: center; gap: 8px;
}
.seq-row .label {
width: 44px;
font-size: 8px; letter-spacing: 0.16em; text-transform: uppercase;
color: var(--hw-engrave);
text-shadow: 0 1px 0 rgba(0,0,0,0.6);
}
.leds {
display: grid; grid-template-columns: repeat(16, 1fr); gap: 2px;
flex: 1;
}
.led {
height: 12px; border-radius: 1px;
background: linear-gradient(180deg, #0a0805 0%, var(--hw-amber-off) 100%);
border: 1px solid var(--hw-edge);
box-shadow: inset 0 1px 1px rgba(0,0,0,0.7);
cursor: pointer; position: relative;
transition: background 60ms;
}
.led:hover { background: linear-gradient(180deg, #181208 0%, #382818 100%); }
.led.on { .led.on {
background: background:
radial-gradient(circle at 50% 30%, var(--hw-amber-hi) 0%, radial-gradient(circle at 50% 25%, var(--hw-amber-hi) 0%,
var(--hw-amber) 40%, #c87830 100%); var(--hw-amber) 55%, #a06820 100%);
box-shadow: box-shadow:
inset 0 0 4px rgba(255,255,255,0.4), inset 0 0 2px rgba(255,255,255,0.4),
inset 0 -1px 1px rgba(0,0,0,0.3), 0 0 5px var(--hw-amber-glow);
0 0 8px var(--hw-amber-glow);
} }
.led.beat { border-left: 1px solid #281e12; } .led.beat::before {
.led.playhead::after { content: ''; position: absolute; left: -1px; top: -3px; bottom: -3px;
content: ''; position: absolute; inset: -2px; width: 1px; background: var(--hw-amber-mut);
border: 1px solid rgba(255, 220, 180, 0.4); }
border-radius: 3px; pointer-events: none; .led.playhead {
outline: 1px solid rgba(232, 160, 80, 0.6);
outline-offset: 1px;
} }
.led { position: relative; }
/* --- title scaffold --- */
.titlecard { color: var(--hw-fg-dim); font-size: 11px; line-height: 1.6;
max-width: 240px; padding: 18px 14px;
border-left: 2px solid var(--hw-amber-mut); }
.titlecard h1 { color: var(--hw-fg-bright); font-size: 13px; font-weight: 500;
letter-spacing: 0.18em; text-transform: uppercase;
margin: 0 0 8px;
text-shadow: 0 1px 0 rgba(0,0,0,0.7),
0 -1px 0 rgba(255,220,180,0.05); }
.titlecard ul { padding-left: 14px; margin: 8px 0 0; }
.titlecard li { margin-bottom: 3px; }
</style> </style>
</head> </head>
<body> <body>
<div class="frame"> <div class="layout">
<!-- ===================== header bar ===================== --> <!-- ===================== hardware unit ===================== -->
<header class="panel"> <div class="hardware">
<span class="screw screw-tl"></span> <span class="screw tl"></span>
<span class="screw screw-tr"></span> <span class="screw tr"></span>
<span class="screw screw-bl"></span> <span class="screw bl"></span>
<span class="screw screw-br"></span> <span class="screw br"></span>
<button class="hw-btn"><span class="hw-led"></span>STOP</button> <!-- header bar, full width -->
<button class="hw-btn"><span class="ico"></span>RUN</button> <div class="hw-header">
<span class="engrave">48 kHz</span> <button class="hw-btn"><span class="led-red"></span>STOP</button>
<span class="stat">TAPS: <strong>14</strong></span> <button class="hw-btn"><span class="ico-play"></span>RUN</button>
<span class="stat">CPU: <strong>9%</strong></span> <span>48 KHZ</span>
<span style="flex: 1"></span> <span>TAPS: <span class="num">14</span></span>
<span class="engrave">code · sinth</span> <span>CPU: <span class="num">9%</span></span>
</header> <span class="right">CODE · SINTH</span>
</div>
<main class="row"> <!-- code area -->
<section class="screen">
<div class="glass" id="glass"></div>
<div class="status-line">&gt; RUNNING<span class="blink">_</span></div>
</section>
<!-- ===================== code area ===================== --> <!-- right column with knobs + seq -->
<section class="screen panel"> <div class="right-col">
<span class="screw screw-tl"></span>
<span class="screw screw-tr"></span>
<span class="screw screw-bl"></span>
<span class="screw screw-br"></span>
<div class="glass" id="glass"></div> <section class="knobs">
<div class="status-line">&gt; RUNNING<span class="blink">_</span></div>
</section>
<!-- ===================== right column ===================== -->
<aside class="col">
<!-- knobs -->
<section class="knobs panel">
<span class="screw screw-tl"></span>
<span class="screw screw-tr"></span>
<span class="screw screw-bl"></span>
<span class="screw screw-br"></span>
<div class="knobs-grid">
<div class="knob-cell"> <div class="knob-cell">
<span class="label">tempo</span> <span class="label">tempo</span>
<canvas class="hw-knob" data-min="60" data-max="200" data-value="120" data-unit=""></canvas> <canvas class="hw-knob" data-min="60" data-max="200" data-value="120" data-decimals="1"></canvas>
<span class="value">120.0</span> <span class="value">120.0</span>
</div> </div>
<div class="knob-cell"> <div class="knob-cell">
@@ -308,80 +324,77 @@
<canvas class="hw-knob" data-min="0" data-max="1" data-value="0.35" data-decimals="2"></canvas> <canvas class="hw-knob" data-min="0" data-max="1" data-value="0.35" data-decimals="2"></canvas>
<span class="value">0.35</span> <span class="value">0.35</span>
</div> </div>
</div> </section>
</section>
<!-- step sequencers --> <section class="seq">
<section class="seq panel"> <div class="seq-row" data-pattern="1000100010001000">
<span class="screw screw-tl"></span> <span class="label">kicks</span>
<span class="screw screw-tr"></span> <div class="leds"></div>
<span class="screw screw-bl"></span> </div>
<span class="screw screw-br"></span> <div class="seq-row" data-pattern="0010001000100010">
<span class="label">hat</span>
<div class="leds"></div>
</div>
<div class="seq-row" data-pattern="1001010001010010">
<span class="label">melody</span>
<div class="leds"></div>
</div>
</section>
<div class="seq-row" data-pattern="1000100010001000"> </div>
<span class="label">kicks</span> </div>
<div class="leds"></div>
</div>
<div class="seq-row" data-pattern="0010001000100010">
<span class="label">hat</span>
<div class="leds"></div>
</div>
<div class="seq-row" data-pattern="1001010001010010">
<span class="label">melody</span>
<div class="leds"></div>
</div>
</section>
</aside>
</main>
</div> </div>
<script> <script>
// =========================================================================== // ===========================================================================
// syntax-highlighted snippet rendering (static — this is a visual mockup) // static syntax-highlighted code (matches reference layout)
// =========================================================================== // ===========================================================================
const PATCH_LINES = [ const PATCH = [
['com', '# studio hardware emulation — visual exploration'], ['# studio hardware emulation — visual exploration', 'com'],
null, null,
['mix', ['kw','node'], ['id','kick'], ['op','='], ['fn','step_seq'], ['op','('], [['kw','node'], ['id','kick'], ['op','='], ['fn','step_seq'], ['op','('],
['id','rate'], ['op','='], ['id','tempo'], ['op',','], ['id','steps'], ['op','='], ['num','16'], ['op',','] ], ['id','rate'], ['op','='], ['id','tempo'], ['op',','], ['id','steps'], ['op','='], ['num','16'], ['op',',']],
['mix', ['op',' '], ['id','default'], ['op','='], ['op','['], [['op',' '], ['id','default'], ['op','='], ['op','['],
['num','1'], ['op',','], ['num','0'], ['op',','], ['num','0'], ['op',','], ['num','0'], ['op',', '], ['num','1'], ['op',','], ['num','0'], ['op',','], ['num','0'], ['op',','], ['num','0'], ['op',', '],
['num','1'], ['op',','], ['num','0'], ['op',','], ['num','0'], ['op',','], ['num','0'], ['op',', '], ['num','1'], ['op',','], ['num','0'], ['op',','], ['num','0'], ['op',','], ['num','0'], ['op',', '],
['num','1'], ['op',','], ['num','0'], ['op',','], ['num','0'], ['op',','], ['num','0'], ['op',', '], ['num','1'], ['op',','], ['num','0'], ['op',','], ['num','0'], ['op',','], ['num','0'], ['op',', '],
['num','1'], ['op',','], ['num','0'], ['op',','], ['num','0'], ['op',','], ['num','0'], ['op',']'], ['op',')'] ], ['num','1'], ['op',','], ['num','0'], ['op',','], ['num','0'], ['op',','], ['num','0'], ['op',']'], ['op',')']],
null, null,
['mix', ['kw','node'], ['id','bass'], ['op','='], ['fn','osc'], ['op','('], [['kw','node'], ['id','bass'], ['op','='], ['fn','osc'], ['op','('],
['atom','saw'], ['op',', '], ['id','freq'], ['op','='], ['num','55'], ['op',')'] ], ['id','saw'], ['op',','], ['id','freq'], ['op','='], ['num','55'], ['op',')']],
['mix', ['kw','node'], ['id','lp'], ['op','='], ['fn','filter'], ['op','('], [['kw','node'], ['id','lp'], ['op','='], ['fn','filter'], ['op','('],
['id','bass'], ['op',', '], ['id','cutoff'], ['op','='], ['num','800'], ['op',', '], ['id','q'], ['op','='], ['num','2.5'], ['op',')'] ], ['id','bass'], ['op',','], ['id','cutoff'], ['op','='], ['num','800'], ['op',','],
['id','q'], ['op','='], ['num','2.5'], ['op',')']],
null, null,
['mix', ['kw','out'], ['op','<- '], ['id','bass'], ['op',' * '], ['num','0.6'], [['kw','out'], ['arrow',' '], ['id','bass'], ['op','*'], ['num','0.6'],
['op',' + '], ['id','kick'], ['op',' * '], ['num','0.4'] ], ['op','+'], ['id','kick'], ['op','*'], ['num','0.4']],
null, null,
['mix', ['kw','node'], ['id','melody'], ['op','='], ['fn','piano_roll'], ['op','('], [['kw','node'], ['id','melody'], ['op','='], ['fn','piano_roll'], ['op','('],
['id','voice'], ['op','='], ['id','synth'], ['op',','] ], ['id','voice'], ['op','='], ['id','synth'], ['op',',']],
['mix', ['op',' '], ['id','rate'], ['op','='], ['id','tempo'], ['op',', '], ['id','length'], ['op','='], ['num','16'], ['op',','] ], [['op',' '], ['id','rate'], ['op','='], ['id','tempo'], ['op',','],
['mix', ['op',' '], ['id','octaves'], ['op','='], ['num','2'], ['op',', '], ['id','gate'], ['op','='], ['num','0.18'], ['op',')'] ], ['id','length'], ['op','='], ['num','16'], ['op',',']],
[['op',' '], ['id','octaves'], ['op','='], ['num','2'], ['op',','],
['id','gate'], ['op','='], ['num','0.18'], ['op',')']],
]; ];
const glass = document.getElementById('glass'); const glass = document.getElementById('glass');
let lineNum = 0; let lineNum = 0;
for (const line of PATCH_LINES) { for (const line of PATCH) {
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'code-line'; div.className = 'code-line';
if (line === null) { if (line === null) {
lineNum++; lineNum++;
div.innerHTML = `<span class="ln">${lineNum}</span><span>&nbsp;</span>`; div.innerHTML = `<span class="ln">${lineNum}</span><span> </span>`;
} else if (line[0] === 'com') { } else if (Array.isArray(line) && typeof line[0] === 'string' && line[1] === 'com') {
lineNum++; lineNum++;
div.innerHTML = `<span class="ln">${lineNum}</span><span class="syn-com">${line[1]}</span>`; div.innerHTML = `<span class="ln">${lineNum}</span><span class="syn-com">${line[0]}</span>`;
} else if (line[0] === 'mix') { } else {
lineNum++; lineNum++;
let html = `<span class="ln">${lineNum}</span><span>`; let html = `<span class="ln">${lineNum}</span><span>`;
for (let i = 1; i < line.length; i++) { for (const [cls, txt] of line) {
const [cls, txt] = line[i]; const sep = cls === 'op' ? '' : ' ';
html += `<span class="syn-${cls}">${txt}</span>${cls !== 'op' ? ' ' : ''}`; const klass = cls === 'arrow' ? 'syn-arrow' : `syn-${cls}`;
html += `<span class="${klass}">${txt}</span>${sep}`;
} }
html += `</span>`; html += `</span>`;
div.innerHTML = html; div.innerHTML = html;
@@ -390,9 +403,10 @@ for (const line of PATCH_LINES) {
} }
// =========================================================================== // ===========================================================================
// knob drawing — metallic radial gradient + amber indicator // knob — restrained: dark gradient + thin amber arc + single tickmark.
// No knurled ridges, no brushed metal. Closer to the reference image.
// =========================================================================== // ===========================================================================
const KNOB_SIZE = 64; const KNOB_SIZE = 50;
function setupCanvas(canvas, size) { function setupCanvas(canvas, size) {
const dpr = window.devicePixelRatio || 1; const dpr = window.devicePixelRatio || 1;
canvas.style.width = size + 'px'; canvas.style.width = size + 'px';
@@ -404,107 +418,74 @@ function setupCanvas(canvas, size) {
return ctx; return ctx;
} }
function drawHwKnob(ctx, size, norm) { function drawKnob(ctx, size, norm) {
const cx = size / 2, cy = size / 2 + 1; const cx = size / 2, cy = size / 2 + 1;
const outerR = size * 0.46; const dialR = size * 0.40;
const bezelR = size * 0.42;
const dialR = size * 0.34;
ctx.clearRect(0, 0, size, size); ctx.clearRect(0, 0, size, size);
// outer bezel (darker ring) // soft outer drop shadow under the knob (sits ON the panel)
const bezel = ctx.createRadialGradient(cx - outerR*0.4, cy - outerR*0.5, 0, cx, cy, outerR); ctx.save();
bezel.addColorStop(0, '#3a342c'); ctx.shadowColor = 'rgba(0,0,0,0.6)';
bezel.addColorStop(0.6, '#1f1b16'); ctx.shadowBlur = 6;
bezel.addColorStop(1, '#0c0a07'); ctx.shadowOffsetY = 2;
ctx.fillStyle = bezel; ctx.fillStyle = '#000';
ctx.beginPath(); ctx.beginPath();
ctx.arc(cx, cy, outerR, 0, Math.PI * 2); ctx.arc(cx, cy, dialR, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
ctx.restore();
// value arc — amber, with glow // value arc — amber, only the active portion is drawn (no background track)
const startA = Math.PI * 0.78, endA = Math.PI * 2.22; const startA = Math.PI * 0.78, endA = Math.PI * 2.22;
ctx.lineCap = 'round'; ctx.lineCap = 'round';
ctx.lineWidth = 2; ctx.lineWidth = 1.6;
ctx.strokeStyle = 'rgba(255,174,92,0.18)'; ctx.strokeStyle = 'rgba(232,160,80,0.10)'; // very faint full track
ctx.beginPath(); ctx.beginPath();
ctx.arc(cx, cy, bezelR + 2, startA, endA); ctx.arc(cx, cy, dialR + 2.5, startA, endA);
ctx.stroke(); ctx.stroke();
ctx.strokeStyle = '#ffae5c'; ctx.strokeStyle = '#e8a050';
ctx.shadowBlur = 5; ctx.shadowBlur = 3;
ctx.shadowColor = 'rgba(255,174,92,0.7)'; ctx.shadowColor = 'rgba(232,160,80,0.5)';
ctx.beginPath(); ctx.beginPath();
ctx.arc(cx, cy, bezelR + 2, startA, startA + norm * (endA - startA)); ctx.arc(cx, cy, dialR + 2.5, startA, startA + norm * (endA - startA));
ctx.stroke(); ctx.stroke();
ctx.shadowBlur = 0; ctx.shadowBlur = 0;
// inner dial — brushed metal radial gradient // dial body — neutral charcoal black, NOT warm brown
const dial = ctx.createRadialGradient( const grad = ctx.createRadialGradient(
cx - dialR * 0.45, cy - dialR * 0.55, dialR * 0.05, cx - dialR * 0.35, cy - dialR * 0.5, 0,
cx, cy, dialR * 1.1 cx, cy, dialR * 1.1
); );
dial.addColorStop(0, '#5a5246'); grad.addColorStop(0, '#2a2a28'); // top-left highlight
dial.addColorStop(0.45, '#2c2820'); grad.addColorStop(0.5, '#141312');
dial.addColorStop(1, '#0a0805'); grad.addColorStop(1, '#040404');
ctx.fillStyle = dial; ctx.fillStyle = grad;
ctx.beginPath(); ctx.beginPath();
ctx.arc(cx, cy, dialR, 0, Math.PI * 2); ctx.arc(cx, cy, dialR, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
// brushed metal striations (faint concentric arcs) // outer dark ring (separates dial from panel)
ctx.strokeStyle = 'rgba(255,220,180,0.04)'; ctx.strokeStyle = 'rgba(0,0,0,0.85)';
ctx.lineWidth = 0.5;
for (let i = 0; i < 5; i++) {
ctx.beginPath();
ctx.arc(cx, cy, dialR * (0.55 + i * 0.07), -0.3, 0.3 + Math.PI);
ctx.stroke();
}
// knurled edge (small ridges around perimeter)
ctx.strokeStyle = 'rgba(0,0,0,0.55)';
ctx.lineWidth = 1; ctx.lineWidth = 1;
for (let i = 0; i < 36; i++) {
const a = (i / 36) * Math.PI * 2;
const r1 = dialR * 0.94, r2 = dialR * 1.0;
ctx.beginPath();
ctx.moveTo(cx + Math.cos(a) * r1, cy + Math.sin(a) * r1);
ctx.lineTo(cx + Math.cos(a) * r2, cy + Math.sin(a) * r2);
ctx.stroke();
}
ctx.strokeStyle = 'rgba(255,220,180,0.08)';
for (let i = 0; i < 36; i++) {
const a = (i / 36 + 0.5/36) * Math.PI * 2;
const r1 = dialR * 0.94, r2 = dialR * 1.0;
ctx.beginPath();
ctx.moveTo(cx + Math.cos(a) * r1, cy + Math.sin(a) * r1);
ctx.lineTo(cx + Math.cos(a) * r2, cy + Math.sin(a) * r2);
ctx.stroke();
}
// specular highlight from NW
const hl = ctx.createRadialGradient(
cx - dialR * 0.4, cy - dialR * 0.55, 0,
cx - dialR * 0.4, cy - dialR * 0.55, dialR
);
hl.addColorStop(0, 'rgba(255,220,180,0.18)');
hl.addColorStop(0.5, 'rgba(255,220,180,0.04)');
hl.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = hl;
ctx.beginPath(); ctx.beginPath();
ctx.arc(cx, cy, dialR, 0, Math.PI * 2); ctx.arc(cx, cy, dialR, 0, Math.PI * 2);
ctx.fill(); ctx.stroke();
// indicator notch (engraved, white-warm) // glossy top crescent — short bright arc on the upper half
ctx.strokeStyle = 'rgba(255,255,255,0.08)';
ctx.lineWidth = 0.8;
ctx.beginPath();
ctx.arc(cx, cy, dialR - 1.0, Math.PI * 1.18, Math.PI * 1.82);
ctx.stroke();
// indicator: short cream tick from edge inward
const ang = startA + norm * (endA - startA); const ang = startA + norm * (endA - startA);
ctx.strokeStyle = 'rgba(255,235,210,0.85)'; ctx.strokeStyle = 'rgba(245,235,215,0.92)';
ctx.shadowBlur = 3; ctx.lineWidth = 1.6;
ctx.shadowColor = 'rgba(0,0,0,0.7)';
ctx.lineWidth = 2.2;
ctx.lineCap = 'round'; ctx.lineCap = 'round';
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(cx + Math.cos(ang) * dialR * 0.45, cy + Math.sin(ang) * dialR * 0.45); ctx.moveTo(cx + Math.cos(ang) * dialR * 0.55, cy + Math.sin(ang) * dialR * 0.55);
ctx.lineTo(cx + Math.cos(ang) * dialR * 0.88, cy + Math.sin(ang) * dialR * 0.88); ctx.lineTo(cx + Math.cos(ang) * dialR * 0.88, cy + Math.sin(ang) * dialR * 0.88);
ctx.stroke(); ctx.stroke();
ctx.shadowBlur = 0;
} }
document.querySelectorAll('canvas.hw-knob').forEach((canvas) => { document.querySelectorAll('canvas.hw-knob').forEach((canvas) => {
@@ -515,9 +496,9 @@ document.querySelectorAll('canvas.hw-knob').forEach((canvas) => {
let value = parseFloat(canvas.dataset.value); let value = parseFloat(canvas.dataset.value);
const ctx = setupCanvas(canvas, KNOB_SIZE); const ctx = setupCanvas(canvas, KNOB_SIZE);
const fmt = (v) => v.toFixed(dec); const fmt = (v) => v.toFixed(dec);
const norm = () => (value - min) / (max - min);
const redraw = () => { const redraw = () => {
drawHwKnob(ctx, KNOB_SIZE, Math.max(0, Math.min(1, norm()))); const norm = Math.max(0, Math.min(1, (value - min) / (max - min)));
drawKnob(ctx, KNOB_SIZE, norm);
if (valueEl) valueEl.textContent = fmt(value); if (valueEl) valueEl.textContent = fmt(value);
}; };
redraw(); redraw();
@@ -555,7 +536,7 @@ document.querySelectorAll('canvas.hw-knob').forEach((canvas) => {
}); });
// =========================================================================== // ===========================================================================
// step LEDs — render from data-pattern, click to toggle, animated playhead // step LEDs
// =========================================================================== // ===========================================================================
document.querySelectorAll('.seq-row').forEach((row) => { document.querySelectorAll('.seq-row').forEach((row) => {
const pattern = row.dataset.pattern.split('').map((c) => c === '1' ? 1 : 0); const pattern = row.dataset.pattern.split('').map((c) => c === '1' ? 1 : 0);
@@ -574,7 +555,6 @@ document.querySelectorAll('.seq-row').forEach((row) => {
row._cells = cells; row._cells = cells;
}); });
// global playhead at 120 BPM 1/16 (≈ 8 steps/sec)
let phStep = 0; let phStep = 0;
const allRows = document.querySelectorAll('.seq-row'); const allRows = document.querySelectorAll('.seq-row');
setInterval(() => { setInterval(() => {