sandbox: studio hardware emulation mockup (variant D)
A fourth aesthetic direction beyond the three knob variants — based on the user's reference image (Moog/Roland/Korg vintage rack gear). - Warm amber+charcoal palette (replaces the cyan phosphor accent). - Beveled metal panels with corner screws (radial-gradient circles with a slot mark). - Knobs: brushed-metal radial gradient, knurled perimeter ridges, specular highlight, white-warm engraved indicator, amber arc with glow. - LEDs for step cells: dim amber-off when inactive, full amber-on with radial glow when active. Animated playhead outline at 8 Hz. - Code "screen" with engraved-screen feel (inset shadow, faint amber bloom). Status line "> RUNNING_" with blinking cursor. - Engraved labels (small caps, double text-shadow for inset). - Tabular numeric value displays in their own dark inset readouts (LCD-style). - Header buttons (STOP / RUN) with green run LED, pulsing red status. Static mockup — no audio engine wired. Knobs are draggable to evaluate the feel; step LEDs toggle on click. Decision pending: pick this or one of A/B/C from knobs.html and align all surfaces to that language.
This commit is contained in:
588
web/sandbox/studio.html
Normal file
588
web/sandbox/studio.html
Normal file
@@ -0,0 +1,588 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>code-sinth — studio hardware emulation</title>
|
||||||
|
<style>
|
||||||
|
/* === warm palette inspired by Moog / Roland / Korg vintage rack gear === */
|
||||||
|
:root {
|
||||||
|
--hw-bg: #1a1612;
|
||||||
|
--hw-panel: #2b2620;
|
||||||
|
--hw-panel-hi: #342f28;
|
||||||
|
--hw-panel-lo: #1f1b16;
|
||||||
|
--hw-divider: #11100d;
|
||||||
|
--hw-screw: #6a635a;
|
||||||
|
--hw-screw-dk: #2a2520;
|
||||||
|
--hw-amber: #ffae5c;
|
||||||
|
--hw-amber-hi: #ffd5a0;
|
||||||
|
--hw-amber-mut: #5a3d20;
|
||||||
|
--hw-amber-off: #2a2118;
|
||||||
|
--hw-amber-glow: rgba(255, 174, 92, 0.55);
|
||||||
|
--hw-fg: #b9a98c;
|
||||||
|
--hw-fg-bright: #d8c9a8;
|
||||||
|
--hw-fg-dim: #786a55;
|
||||||
|
--hw-engrave: #8c7d62;
|
||||||
|
--hw-screen-bg: #1d1814;
|
||||||
|
--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;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { height: 100%; margin: 0;
|
||||||
|
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-size: 13px; }
|
||||||
|
body { padding: 28px; min-height: 100vh; }
|
||||||
|
|
||||||
|
/* ===================================================================== */
|
||||||
|
/* Panel chrome — beveled metal panels with corner screws */
|
||||||
|
/* ===================================================================== */
|
||||||
|
.panel {
|
||||||
|
position: relative;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, var(--hw-panel-hi) 0%, var(--hw-panel) 50%, var(--hw-panel-lo) 100%);
|
||||||
|
border: 1px solid #0a0805;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 220, 180, 0.04),
|
||||||
|
inset 0 -1px 0 rgba(0, 0, 0, 0.6),
|
||||||
|
0 1px 0 rgba(255, 220, 180, 0.03),
|
||||||
|
0 6px 16px rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
.panel::before, .panel::after, .panel > .screw-tl, .panel > .screw-tr,
|
||||||
|
.panel > .screw-bl, .panel > .screw-br {
|
||||||
|
/* nothing; pseudo-elements not enough — using real spans below */
|
||||||
|
}
|
||||||
|
.screw {
|
||||||
|
position: absolute; width: 10px; height: 10px; border-radius: 50%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 30% 25%, #9a8e7c 0%, #4a4138 55%, #1a1510 100%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 -1px 1px rgba(0,0,0,0.7),
|
||||||
|
inset 0 1px 0 rgba(255,220,180,0.2),
|
||||||
|
0 1px 1px rgba(0,0,0,0.6);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.screw::after {
|
||||||
|
content: ''; position: absolute; inset: 0;
|
||||||
|
background: linear-gradient(45deg, transparent 44%, rgba(0,0,0,0.65) 47%,
|
||||||
|
rgba(0,0,0,0.65) 53%, transparent 56%);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.screw-tl { top: 6px; left: 6px; }
|
||||||
|
.screw-tr { top: 6px; right: 6px; }
|
||||||
|
.screw-bl { bottom: 6px; left: 6px; }
|
||||||
|
.screw-br { bottom: 6px; right: 6px; }
|
||||||
|
|
||||||
|
/* ===================================================================== */
|
||||||
|
/* Layout */
|
||||||
|
/* ===================================================================== */
|
||||||
|
.frame { display: grid; grid-template-rows: auto 1fr; gap: 14px;
|
||||||
|
max-width: 1100px; margin: 0 auto; }
|
||||||
|
|
||||||
|
/* --- header bar --- */
|
||||||
|
header.panel { display: flex; align-items: center; gap: 18px; padding: 10px 36px; height: 50px; }
|
||||||
|
.hw-btn { background: linear-gradient(180deg, #3a342c 0%, #1f1b16 100%);
|
||||||
|
border: 1px solid #0a0805; border-radius: 4px;
|
||||||
|
color: var(--hw-fg-bright); padding: 6px 14px;
|
||||||
|
font-family: inherit; font-size: 11px; cursor: pointer;
|
||||||
|
letter-spacing: 0.12em; text-transform: uppercase;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255,220,180,0.1),
|
||||||
|
inset 0 -1px 0 rgba(0,0,0,0.6),
|
||||||
|
0 1px 2px rgba(0,0,0,0.5);
|
||||||
|
transition: all 80ms; }
|
||||||
|
.hw-btn:hover { color: var(--hw-amber); }
|
||||||
|
.hw-btn:active { transform: translateY(1px);
|
||||||
|
box-shadow: inset 0 1px 4px rgba(0,0,0,0.6); }
|
||||||
|
.hw-btn .ico { display: inline-block; width: 0; height: 0;
|
||||||
|
border-left: 7px solid var(--hw-led-green);
|
||||||
|
border-top: 5px solid transparent;
|
||||||
|
border-bottom: 5px solid transparent;
|
||||||
|
vertical-align: middle; margin-right: 6px;
|
||||||
|
filter: drop-shadow(0 0 4px var(--hw-led-green)); }
|
||||||
|
.hw-led { display: inline-block; width: 7px; height: 7px; border-radius: 50%;
|
||||||
|
background: var(--hw-led-red);
|
||||||
|
box-shadow: 0 0 6px var(--hw-led-red),
|
||||||
|
inset 0 0 2px rgba(0,0,0,0.7);
|
||||||
|
vertical-align: middle; margin-right: 6px;
|
||||||
|
animation: led-pulse 2s ease-in-out infinite; }
|
||||||
|
@keyframes led-pulse {
|
||||||
|
0%, 100% { box-shadow: 0 0 4px var(--hw-led-red); }
|
||||||
|
50% { box-shadow: 0 0 10px var(--hw-led-red); }
|
||||||
|
}
|
||||||
|
.engrave { color: var(--hw-engrave); font-size: 10px;
|
||||||
|
letter-spacing: 0.18em; text-transform: uppercase;
|
||||||
|
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;
|
||||||
|
min-height: 0; }
|
||||||
|
|
||||||
|
/* --- code area (the "screen") --- */
|
||||||
|
.screen.panel { padding: 14px 18px 26px; min-height: 460px;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
background:
|
||||||
|
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:
|
||||||
|
inset 0 0 18px rgba(0,0,0,0.6),
|
||||||
|
inset 0 0 1px rgba(255, 174, 92, 0.06),
|
||||||
|
inset 0 0 60px rgba(255, 174, 92, 0.02);
|
||||||
|
font-size: 13px; line-height: 22px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.code-line { display: flex; gap: 18px; white-space: pre; }
|
||||||
|
.ln { color: var(--hw-syn-com); user-select: none; min-width: 22px; text-align: right; }
|
||||||
|
.syn-kw { color: var(--hw-syn-kw); }
|
||||||
|
.syn-num { color: var(--hw-syn-num); }
|
||||||
|
.syn-fn { color: var(--hw-syn-fn); }
|
||||||
|
.syn-atom{ color: var(--hw-syn-atom); }
|
||||||
|
.syn-str { color: var(--hw-syn-str); }
|
||||||
|
.syn-com { color: var(--hw-syn-com); font-style: italic; }
|
||||||
|
.syn-id { color: var(--hw-fg-bright); }
|
||||||
|
.syn-op { color: var(--hw-fg-dim); }
|
||||||
|
|
||||||
|
.screen .status-line {
|
||||||
|
margin-top: 10px; padding: 0 4px;
|
||||||
|
font-size: 11px; color: var(--hw-amber);
|
||||||
|
text-shadow: 0 0 6px var(--hw-amber-glow);
|
||||||
|
letter-spacing: 0.12em; }
|
||||||
|
.blink { animation: blink 1.1s step-end infinite; }
|
||||||
|
@keyframes blink { 50% { opacity: 0; } }
|
||||||
|
|
||||||
|
/* --- right column --- */
|
||||||
|
aside.col { display: flex; flex-direction: column; gap: 14px; }
|
||||||
|
|
||||||
|
/* --- knobs panel --- */
|
||||||
|
.knobs.panel { padding: 22px 14px; }
|
||||||
|
.knobs-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||||
|
.knob-cell { display: flex; flex-direction: column; align-items: center; gap: 6px; }
|
||||||
|
.knob-cell .label { font-size: 9px; 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.05); }
|
||||||
|
.knob-cell .value {
|
||||||
|
font-size: 12px; font-variant-numeric: tabular-nums;
|
||||||
|
color: var(--hw-amber); text-shadow: 0 0 6px var(--hw-amber-glow);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
/* slight inset / engraved feel for the digital readout */
|
||||||
|
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.dragging { cursor: grabbing; }
|
||||||
|
|
||||||
|
/* --- sequencer panel --- */
|
||||||
|
.seq.panel { padding: 16px 14px 18px; }
|
||||||
|
.seq-row { display: flex; align-items: center; gap: 10px; padding: 4px 4px;
|
||||||
|
border-top: 1px solid #0a0805; }
|
||||||
|
.seq-row:first-of-type { border-top: none; }
|
||||||
|
.seq-row .label { width: 56px; font-size: 9px; 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.05); }
|
||||||
|
.leds { display: grid; grid-template-columns: repeat(16, 1fr); gap: 3px; flex: 1; }
|
||||||
|
.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%); }
|
||||||
|
.led.on {
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 50% 30%, var(--hw-amber-hi) 0%,
|
||||||
|
var(--hw-amber) 40%, #c87830 100%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 4px rgba(255,255,255,0.4),
|
||||||
|
inset 0 -1px 1px rgba(0,0,0,0.3),
|
||||||
|
0 0 8px var(--hw-amber-glow);
|
||||||
|
}
|
||||||
|
.led.beat { border-left: 1px solid #281e12; }
|
||||||
|
.led.playhead::after {
|
||||||
|
content: ''; position: absolute; inset: -2px;
|
||||||
|
border: 1px solid rgba(255, 220, 180, 0.4);
|
||||||
|
border-radius: 3px; pointer-events: none;
|
||||||
|
}
|
||||||
|
.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>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="frame">
|
||||||
|
|
||||||
|
<!-- ===================== header bar ===================== -->
|
||||||
|
<header class="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>
|
||||||
|
|
||||||
|
<button class="hw-btn"><span class="hw-led"></span>STOP</button>
|
||||||
|
<button class="hw-btn"><span class="ico"></span>RUN</button>
|
||||||
|
<span class="engrave">48 kHz</span>
|
||||||
|
<span class="stat">TAPS: <strong>14</strong></span>
|
||||||
|
<span class="stat">CPU: <strong>9%</strong></span>
|
||||||
|
<span style="flex: 1"></span>
|
||||||
|
<span class="engrave">code · sinth</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="row">
|
||||||
|
|
||||||
|
<!-- ===================== code area ===================== -->
|
||||||
|
<section class="screen 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="glass" id="glass"></div>
|
||||||
|
|
||||||
|
<div class="status-line">> 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">
|
||||||
|
<span class="label">tempo</span>
|
||||||
|
<canvas class="hw-knob" data-min="60" data-max="200" data-value="120" data-unit=""></canvas>
|
||||||
|
<span class="value">120.0</span>
|
||||||
|
</div>
|
||||||
|
<div class="knob-cell">
|
||||||
|
<span class="label">res</span>
|
||||||
|
<canvas class="hw-knob" data-min="0.5" data-max="8" data-value="2.5" data-decimals="2"></canvas>
|
||||||
|
<span class="value">2.50</span>
|
||||||
|
</div>
|
||||||
|
<div class="knob-cell">
|
||||||
|
<span class="label">cutoff</span>
|
||||||
|
<canvas class="hw-knob" data-min="200" data-max="4000" data-value="800" data-decimals="0"></canvas>
|
||||||
|
<span class="value">800</span>
|
||||||
|
</div>
|
||||||
|
<div class="knob-cell">
|
||||||
|
<span class="label">drive</span>
|
||||||
|
<canvas class="hw-knob" data-min="0" data-max="1" data-value="0.35" data-decimals="2"></canvas>
|
||||||
|
<span class="value">0.35</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- step sequencers -->
|
||||||
|
<section class="seq 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="seq-row" data-pattern="1000100010001000">
|
||||||
|
<span class="label">kicks</span>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ===========================================================================
|
||||||
|
// syntax-highlighted snippet rendering (static — this is a visual mockup)
|
||||||
|
// ===========================================================================
|
||||||
|
const PATCH_LINES = [
|
||||||
|
['com', '# studio hardware emulation — visual exploration'],
|
||||||
|
null,
|
||||||
|
['mix', ['kw','node'], ['id','kick'], ['op','='], ['fn','step_seq'], ['op','('],
|
||||||
|
['id','rate'], ['op','='], ['id','tempo'], ['op',','], ['id','steps'], ['op','='], ['num','16'], ['op',','] ],
|
||||||
|
['mix', ['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',']'], ['op',')'] ],
|
||||||
|
null,
|
||||||
|
['mix', ['kw','node'], ['id','bass'], ['op','='], ['fn','osc'], ['op','('],
|
||||||
|
['atom','saw'], ['op',', '], ['id','freq'], ['op','='], ['num','55'], ['op',')'] ],
|
||||||
|
['mix', ['kw','node'], ['id','lp'], ['op','='], ['fn','filter'], ['op','('],
|
||||||
|
['id','bass'], ['op',', '], ['id','cutoff'], ['op','='], ['num','800'], ['op',', '], ['id','q'], ['op','='], ['num','2.5'], ['op',')'] ],
|
||||||
|
null,
|
||||||
|
['mix', ['kw','out'], ['op','<- '], ['id','bass'], ['op',' * '], ['num','0.6'],
|
||||||
|
['op',' + '], ['id','kick'], ['op',' * '], ['num','0.4'] ],
|
||||||
|
null,
|
||||||
|
['mix', ['kw','node'], ['id','melody'], ['op','='], ['fn','piano_roll'], ['op','('],
|
||||||
|
['id','voice'], ['op','='], ['id','synth'], ['op',','] ],
|
||||||
|
['mix', ['op',' '], ['id','rate'], ['op','='], ['id','tempo'], ['op',', '], ['id','length'], ['op','='], ['num','16'], ['op',','] ],
|
||||||
|
['mix', ['op',' '], ['id','octaves'], ['op','='], ['num','2'], ['op',', '], ['id','gate'], ['op','='], ['num','0.18'], ['op',')'] ],
|
||||||
|
];
|
||||||
|
const glass = document.getElementById('glass');
|
||||||
|
let lineNum = 0;
|
||||||
|
for (const line of PATCH_LINES) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'code-line';
|
||||||
|
if (line === null) {
|
||||||
|
lineNum++;
|
||||||
|
div.innerHTML = `<span class="ln">${lineNum}</span><span> </span>`;
|
||||||
|
} else if (line[0] === 'com') {
|
||||||
|
lineNum++;
|
||||||
|
div.innerHTML = `<span class="ln">${lineNum}</span><span class="syn-com">${line[1]}</span>`;
|
||||||
|
} else if (line[0] === 'mix') {
|
||||||
|
lineNum++;
|
||||||
|
let html = `<span class="ln">${lineNum}</span><span>`;
|
||||||
|
for (let i = 1; i < line.length; i++) {
|
||||||
|
const [cls, txt] = line[i];
|
||||||
|
html += `<span class="syn-${cls}">${txt}</span>${cls !== 'op' ? ' ' : ''}`;
|
||||||
|
}
|
||||||
|
html += `</span>`;
|
||||||
|
div.innerHTML = html;
|
||||||
|
}
|
||||||
|
glass.appendChild(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// knob drawing — metallic radial gradient + amber indicator
|
||||||
|
// ===========================================================================
|
||||||
|
const KNOB_SIZE = 64;
|
||||||
|
function setupCanvas(canvas, size) {
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
canvas.style.width = size + 'px';
|
||||||
|
canvas.style.height = size + 'px';
|
||||||
|
canvas.width = Math.round(size * dpr);
|
||||||
|
canvas.height = Math.round(size * dpr);
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.scale(dpr, dpr);
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawHwKnob(ctx, size, norm) {
|
||||||
|
const cx = size / 2, cy = size / 2 + 1;
|
||||||
|
const outerR = size * 0.46;
|
||||||
|
const bezelR = size * 0.42;
|
||||||
|
const dialR = size * 0.34;
|
||||||
|
ctx.clearRect(0, 0, size, size);
|
||||||
|
|
||||||
|
// outer bezel (darker ring)
|
||||||
|
const bezel = ctx.createRadialGradient(cx - outerR*0.4, cy - outerR*0.5, 0, cx, cy, outerR);
|
||||||
|
bezel.addColorStop(0, '#3a342c');
|
||||||
|
bezel.addColorStop(0.6, '#1f1b16');
|
||||||
|
bezel.addColorStop(1, '#0c0a07');
|
||||||
|
ctx.fillStyle = bezel;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(cx, cy, outerR, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// value arc — amber, with glow
|
||||||
|
const startA = Math.PI * 0.78, endA = Math.PI * 2.22;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeStyle = 'rgba(255,174,92,0.18)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(cx, cy, bezelR + 2, startA, endA);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.strokeStyle = '#ffae5c';
|
||||||
|
ctx.shadowBlur = 5;
|
||||||
|
ctx.shadowColor = 'rgba(255,174,92,0.7)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(cx, cy, bezelR + 2, startA, startA + norm * (endA - startA));
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
|
||||||
|
// inner dial — brushed metal radial gradient
|
||||||
|
const dial = ctx.createRadialGradient(
|
||||||
|
cx - dialR * 0.45, cy - dialR * 0.55, dialR * 0.05,
|
||||||
|
cx, cy, dialR * 1.1
|
||||||
|
);
|
||||||
|
dial.addColorStop(0, '#5a5246');
|
||||||
|
dial.addColorStop(0.45, '#2c2820');
|
||||||
|
dial.addColorStop(1, '#0a0805');
|
||||||
|
ctx.fillStyle = dial;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(cx, cy, dialR, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// brushed metal striations (faint concentric arcs)
|
||||||
|
ctx.strokeStyle = 'rgba(255,220,180,0.04)';
|
||||||
|
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;
|
||||||
|
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.arc(cx, cy, dialR, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// indicator notch (engraved, white-warm)
|
||||||
|
const ang = startA + norm * (endA - startA);
|
||||||
|
ctx.strokeStyle = 'rgba(255,235,210,0.85)';
|
||||||
|
ctx.shadowBlur = 3;
|
||||||
|
ctx.shadowColor = 'rgba(0,0,0,0.7)';
|
||||||
|
ctx.lineWidth = 2.2;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(cx + Math.cos(ang) * dialR * 0.45, cy + Math.sin(ang) * dialR * 0.45);
|
||||||
|
ctx.lineTo(cx + Math.cos(ang) * dialR * 0.88, cy + Math.sin(ang) * dialR * 0.88);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('canvas.hw-knob').forEach((canvas) => {
|
||||||
|
const min = parseFloat(canvas.dataset.min);
|
||||||
|
const max = parseFloat(canvas.dataset.max);
|
||||||
|
const dec = canvas.dataset.decimals !== undefined ? +canvas.dataset.decimals : 1;
|
||||||
|
const valueEl = canvas.parentNode.querySelector('.value');
|
||||||
|
let value = parseFloat(canvas.dataset.value);
|
||||||
|
const ctx = setupCanvas(canvas, KNOB_SIZE);
|
||||||
|
const fmt = (v) => v.toFixed(dec);
|
||||||
|
const norm = () => (value - min) / (max - min);
|
||||||
|
const redraw = () => {
|
||||||
|
drawHwKnob(ctx, KNOB_SIZE, Math.max(0, Math.min(1, norm())));
|
||||||
|
if (valueEl) valueEl.textContent = fmt(value);
|
||||||
|
};
|
||||||
|
redraw();
|
||||||
|
|
||||||
|
canvas.addEventListener('pointerdown', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
canvas.setPointerCapture(e.pointerId);
|
||||||
|
canvas.classList.add('dragging');
|
||||||
|
document.body.style.cursor = 'grabbing';
|
||||||
|
const startY = e.clientY;
|
||||||
|
const startV = value;
|
||||||
|
const span = max - min;
|
||||||
|
const onMove = (ev) => {
|
||||||
|
const dy = startY - ev.clientY;
|
||||||
|
const factor = ev.shiftKey ? 4 : 1;
|
||||||
|
const delta = (dy / 200) * span / factor;
|
||||||
|
value = Math.max(min, Math.min(max, startV + delta));
|
||||||
|
redraw();
|
||||||
|
};
|
||||||
|
const onUp = () => {
|
||||||
|
canvas.classList.remove('dragging');
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
canvas.removeEventListener('pointermove', onMove);
|
||||||
|
canvas.removeEventListener('pointerup', onUp);
|
||||||
|
canvas.removeEventListener('pointercancel', onUp);
|
||||||
|
};
|
||||||
|
canvas.addEventListener('pointermove', onMove);
|
||||||
|
canvas.addEventListener('pointerup', onUp);
|
||||||
|
canvas.addEventListener('pointercancel', onUp);
|
||||||
|
});
|
||||||
|
canvas.addEventListener('dblclick', () => {
|
||||||
|
value = parseFloat(canvas.dataset.value);
|
||||||
|
redraw();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// step LEDs — render from data-pattern, click to toggle, animated playhead
|
||||||
|
// ===========================================================================
|
||||||
|
document.querySelectorAll('.seq-row').forEach((row) => {
|
||||||
|
const pattern = row.dataset.pattern.split('').map((c) => c === '1' ? 1 : 0);
|
||||||
|
const container = row.querySelector('.leds');
|
||||||
|
const cells = [];
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
const led = document.createElement('div');
|
||||||
|
led.className = 'led' + (pattern[i] ? ' on' : '') + (i % 4 === 0 ? ' beat' : '');
|
||||||
|
led.addEventListener('click', () => {
|
||||||
|
pattern[i] = pattern[i] ? 0 : 1;
|
||||||
|
led.classList.toggle('on', !!pattern[i]);
|
||||||
|
});
|
||||||
|
container.appendChild(led);
|
||||||
|
cells.push(led);
|
||||||
|
}
|
||||||
|
row._cells = cells;
|
||||||
|
});
|
||||||
|
|
||||||
|
// global playhead at 120 BPM 1/16 (≈ 8 steps/sec)
|
||||||
|
let phStep = 0;
|
||||||
|
const allRows = document.querySelectorAll('.seq-row');
|
||||||
|
setInterval(() => {
|
||||||
|
allRows.forEach((row) => {
|
||||||
|
row._cells.forEach((c, i) => c.classList.toggle('playhead', i === phStep));
|
||||||
|
});
|
||||||
|
phStep = (phStep + 1) % 16;
|
||||||
|
}, 125);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user