Download step_sequencer.html from zxbc2023/Toybox: direct link, hf CLI and curl.
- Browser
- Download file 11.9 kB
-
https://huggingface.co/spaces/zxbc2023/Toybox/resolve/main/step_sequencer.html
- Command line
-
hf download hf://spaces/zxbc2023/Toybox/step_sequencer.html
-
curl -L -o step_sequencer.html https://huggingface.co/spaces/zxbc2023/Toybox/resolve/main/step_sequencer.html
11.9 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>16-step drum sequencer</title> | |
| <style> | |
| /* Explicit dark palette (overrides app theme on purpose) */ | |
| .seq { | |
| --bg: #131519; --panel: #191c23; --line: #2a2f3a; | |
| --cell: #20242e; --cell-beat: #282d39; | |
| --fg: #e8eaef; --mut: #98a0ae; --acc: #4f7cff; | |
| display: inline-flex; flex-direction: column; gap: 18px; | |
| padding: 22px 24px 24px; border: 1px solid #262a33; | |
| border-radius: 16px; background: var(--bg); | |
| color: var(--fg); font-family: inherit; font-size: 14px; | |
| box-shadow: 0 10px 34px rgba(0,0,0,.55); | |
| } | |
| .seq button { font: inherit; cursor: pointer; user-select: none; } | |
| /* ---- transport ---- */ | |
| .transport { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; } | |
| .play-btn { | |
| display: inline-flex; align-items: center; justify-content: center; gap: 10px; | |
| min-width: 132px; min-height: 48px; padding: 12px 26px; | |
| font-size: 15px; font-weight: 600; letter-spacing: .03em; | |
| color: #fff; background: var(--acc); | |
| border: none; border-radius: 12px; | |
| box-shadow: 0 2px 14px rgba(79,124,255,.45); | |
| transition: transform .06s ease, filter .15s ease; | |
| } | |
| .play-btn:hover { filter: brightness(1.1); } | |
| .play-btn:active { transform: scale(.97); } | |
| .play-btn svg { width: 18px; height: 18px; fill: currentColor; } | |
| .knob { | |
| display: inline-flex; flex-direction: column; align-items: flex-start; gap: 6px; | |
| color: var(--mut); font-size: 11px; text-transform: uppercase; letter-spacing: .12em; | |
| } | |
| .knob input[type=range] { width: 170px; accent-color: var(--acc); cursor: pointer; } | |
| .knob .value { | |
| font-size: 18px; font-weight: 650; color: var(--fg); | |
| font-variant-numeric: tabular-nums; letter-spacing: 0; | |
| } | |
| .knob .value small { font-size: 11px; font-weight: 500; color: var(--mut); } | |
| .ghost-btn { | |
| min-height: 48px; padding: 12px 20px; font-size: 14px; | |
| color: var(--mut); | |
| background: transparent; border: 1px solid #333a48; border-radius: 12px; | |
| transition: color .15s, border-color .15s; | |
| } | |
| .ghost-btn:hover { color: var(--fg); border-color: var(--fg); } | |
| /* ---- step grid ---- */ | |
| .grid { | |
| display: grid; grid-template-columns: 140px repeat(16, 40px); | |
| gap: 2px; align-items: center; | |
| touch-action: none; | |
| } | |
| .lbl { display: flex; flex-direction: column; gap: 4px; padding: 2px 0; } | |
| .lbl .top { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 550; color: var(--mut); } | |
| .lbl .dot { width: 9px; height: 9px; border-radius: 50%; background: var(--tc); flex: none; | |
| box-shadow: 0 0 8px color-mix(in srgb, var(--tc) 70%, transparent); } | |
| .lbl input[type=range] { | |
| width: 108px; height: 18px; accent-color: var(--tc); cursor: pointer; | |
| } | |
| .lbl input[type=range]:hover { filter: brightness(1.2); } | |
| .cell { | |
| width: 40px; height: 40px; padding: 0; | |
| border-radius: 6px; border: 1px solid var(--line); | |
| background: var(--cell); | |
| transition: background .08s, border-color .08s, transform .05s; | |
| } | |
| .cell:hover { border-color: #4a5262; } | |
| .cell:active { transform: scale(.95); } | |
| .cell.beat { background: var(--cell-beat); } | |
| .cell.on { | |
| background: var(--tc); border-color: var(--tc); | |
| box-shadow: 0 0 10px color-mix(in srgb, var(--tc) 55%, transparent), | |
| inset 0 0 5px rgba(255,255,255,.3); | |
| } | |
| /* playhead: whole column */ | |
| .cell.playhead { border-color: #5a6478; } | |
| .cell.playhead.on { | |
| box-shadow: 0 0 0 2px var(--fg), | |
| 0 0 14px color-mix(in srgb, var(--tc) 70%, transparent); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="seq" id="seq"></div> | |
| <script> | |
| (function () { | |
| var STEPS = 16; | |
| var TRACKS = [ | |
| { name: 'Kick', color: '#ff5d5d' }, | |
| { name: 'Snare', color: '#ffb020' }, | |
| { name: 'Clap', color: '#c084fc' }, | |
| { name: 'Hi-hat', color: '#2dd4bf' } | |
| ]; | |
| var grid = TRACKS.map(function () { return new Array(STEPS).fill(false); }); | |
| // Default groove: four-on-the-floor kick, clap on 5 & 13, hats on eighths | |
| [0, 4, 8, 12].forEach(function (s) { grid[0][s] = true; }); | |
| [4, 12].forEach(function (s) { grid[2][s] = true; }); | |
| for (var s = 0; s < STEPS; s += 2) grid[3][s] = true; | |
| var el = document.getElementById('seq'); | |
| /* ---------- build UI ---------- */ | |
| var transport = document.createElement('div'); | |
| transport.className = 'transport'; | |
| var playBtn = document.createElement('button'); | |
| playBtn.className = 'play-btn'; | |
| var ICON_PLAY = '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>'; | |
| var ICON_STOP = '<svg viewBox="0 0 24 24"><path d="M6 6h12v12H6z"/></svg>'; | |
| playBtn.innerHTML = ICON_PLAY + '<span>Play</span>'; | |
| var clearBtn = document.createElement('button'); | |
| clearBtn.className = 'ghost-btn'; | |
| clearBtn.textContent = 'Clear pattern'; | |
| var bpmKnob = document.createElement('div'); | |
| bpmKnob.className = 'knob'; | |
| bpmKnob.innerHTML = 'Tempo' + | |
| '<input id="bpm" type="range" min="60" max="200" value="120">' + | |
| '<span class="value" id="bpmVal">120 <small>BPM</small></span>'; | |
| var volKnob = document.createElement('div'); | |
| volKnob.className = 'knob'; | |
| volKnob.innerHTML = 'Master' + | |
| '<input id="vol" type="range" min="0" max="100" value="85">'; | |
| transport.appendChild(playBtn); | |
| transport.appendChild(bpmKnob); | |
| transport.appendChild(volKnob); | |
| transport.appendChild(clearBtn); | |
| el.appendChild(transport); | |
| var gridEl = document.createElement('div'); | |
| gridEl.className = 'grid'; | |
| var cells = []; | |
| TRACKS.forEach(function (tr, r) { | |
| var lbl = document.createElement('div'); | |
| lbl.className = 'lbl'; | |
| lbl.style.setProperty('--tc', tr.color); | |
| lbl.innerHTML = | |
| '<div class="top"><span class="dot"></span>' + tr.name + '</div>' + | |
| '<input type="range" min="0" max="100" value="100" data-trk="' + r + '" ' + | |
| 'aria-label="' + tr.name + ' volume">'; | |
| gridEl.appendChild(lbl); | |
| cells[r] = []; | |
| for (var s = 0; s < STEPS; s++) { | |
| var b = document.createElement('button'); | |
| b.className = 'cell' + (s % 4 === 0 ? ' beat' : ''); | |
| b.style.setProperty('--tc', tr.color); | |
| b.dataset.r = r; b.dataset.s = s; | |
| b.setAttribute('aria-label', tr.name + ' step ' + (s + 1)); | |
| if (grid[r][s]) b.classList.add('on'); | |
| gridEl.appendChild(b); | |
| cells[r][s] = b; | |
| } | |
| }); | |
| el.appendChild(gridEl); | |
| var bpmInput = bpmKnob.querySelector('#bpm'); | |
| var bpmVal = bpmKnob.querySelector('#bpmVal'); | |
| var volInput = volKnob.querySelector('#vol'); | |
| bpmInput.addEventListener('input', function () { | |
| bpmVal.innerHTML = bpmInput.value + ' <small>BPM</small>'; | |
| }); | |
| /* ---------- Web Audio synthesis (no samples) ---------- */ | |
| var ctx = null, master = null, noiseBuf = null; | |
| var trackGains = []; | |
| var trackVols = TRACKS.map(function () { return 1; }); | |
| var masterVol = +volInput.value / 100; | |
| function ac() { | |
| if (!ctx) { | |
| ctx = new (window.AudioContext || window.webkitAudioContext)(); | |
| master = ctx.createGain(); | |
| master.gain.value = masterVol; | |
| master.connect(ctx.destination); | |
| trackGains = TRACKS.map(function (_, i) { | |
| var g = ctx.createGain(); | |
| g.gain.value = trackVols[i]; | |
| g.connect(master); | |
| return g; | |
| }); | |
| noiseBuf = ctx.createBuffer(1, ctx.sampleRate, ctx.sampleRate); | |
| var d = noiseBuf.getChannelData(0); | |
| for (var i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1; | |
| } | |
| return ctx; | |
| } | |
| volInput.addEventListener('input', function () { | |
| masterVol = +volInput.value / 100; | |
| if (master) master.gain.value = masterVol; | |
| }); | |
| gridEl.querySelectorAll('input[type=range][data-trk]').forEach(function (f) { | |
| f.addEventListener('input', function () { | |
| var r = +f.dataset.trk; | |
| trackVols[r] = +f.value / 100; | |
| if (trackGains[r]) trackGains[r].gain.value = trackVols[r]; | |
| }); | |
| }); | |
| function env(g, t, peak, decay) { | |
| g.gain.setValueAtTime(peak, t); | |
| g.gain.exponentialRampToValueAtTime(0.0001, t + decay); | |
| } | |
| function noise(t, filterType, freq, q, peak, decay, dest) { | |
| var src = ctx.createBufferSource(); src.buffer = noiseBuf; | |
| var f = ctx.createBiquadFilter(); f.type = filterType; f.frequency.value = freq; if (q) f.Q.value = q; | |
| var g = ctx.createGain(); env(g, t, peak, decay); | |
| src.connect(f); f.connect(g); g.connect(dest); | |
| src.start(t); src.stop(t + decay + 0.05); | |
| } | |
| var SOUNDS = { | |
| Kick: function (t, dest) { | |
| var o = ctx.createOscillator(), g = ctx.createGain(); | |
| o.type = 'sine'; | |
| o.frequency.setValueAtTime(150, t); | |
| o.frequency.exponentialRampToValueAtTime(45, t + 0.12); | |
| env(g, t, 0.9, 0.3); | |
| o.connect(g); g.connect(dest); | |
| o.start(t); o.stop(t + 0.35); | |
| }, | |
| Snare: function (t, dest) { | |
| noise(t, 'bandpass', 1800, 0.8, 0.5, 0.18, dest); | |
| var o = ctx.createOscillator(), g = ctx.createGain(); | |
| o.type = 'triangle'; o.frequency.value = 190; | |
| env(g, t, 0.35, 0.1); | |
| o.connect(g); g.connect(dest); | |
| o.start(t); o.stop(t + 0.15); | |
| }, | |
| Clap: function (t, dest) { | |
| for (var i = 0; i < 3; i++) noise(t + i * 0.012, 'bandpass', 1200, 1.2, 0.5 - i * 0.1, 0.12, dest); | |
| }, | |
| 'Hi-hat': function (t, dest) { | |
| noise(t, 'highpass', 7000, 1, 0.35, 0.05, dest); | |
| } | |
| }; | |
| /* ---------- cell painting: click + drag ---------- */ | |
| function setCell(b, on) { | |
| grid[+b.dataset.r][+b.dataset.s] = on; | |
| b.classList.toggle('on', on); | |
| } | |
| var painting = null; | |
| gridEl.addEventListener('pointerdown', function (e) { | |
| var b = e.target.closest('.cell'); | |
| if (!b) return; | |
| e.preventDefault(); | |
| painting = { state: !grid[+b.dataset.r][+b.dataset.s] }; | |
| setCell(b, painting.state); | |
| }); | |
| gridEl.addEventListener('pointermove', function (e) { | |
| if (!painting) return; | |
| var hit = document.elementFromPoint(e.clientX, e.clientY); | |
| var b = hit && hit.closest('.cell'); | |
| if (b && grid[+b.dataset.r][+b.dataset.s] !== painting.state) setCell(b, painting.state); | |
| }); | |
| window.addEventListener('pointerup', function () { painting = null; }); | |
| window.addEventListener('pointercancel', function () { painting = null; }); | |
| // Keyboard access: Enter/Space on a focused cell (native click is not used) | |
| gridEl.addEventListener('keydown', function (e) { | |
| var b = e.target.closest('.cell'); | |
| if (!b || (e.key !== ' ' && e.key !== 'Enter')) return; | |
| e.preventDefault(); | |
| setCell(b, !grid[+b.dataset.r][+b.dataset.s]); | |
| }); | |
| /* ---------- lookahead scheduler (stable timing, survives tab throttling) ---------- */ | |
| var playing = false, step = 0, nextTime = 0, timer = null; | |
| function markPlayhead(s) { | |
| for (var c = 0; c < STEPS; c++) | |
| for (var r = 0; r < TRACKS.length; r++) | |
| cells[r][c].classList.toggle('playhead', c === s); | |
| } | |
| function scheduleStep(s, t) { | |
| TRACKS.forEach(function (tr, r) { | |
| if (grid[r][s]) SOUNDS[tr.name](t, trackGains[r]); | |
| }); | |
| setTimeout(function () { if (playing) markPlayhead(s); }, Math.max(0, (t - ctx.currentTime) * 1000)); | |
| } | |
| function loop() { | |
| while (nextTime < ctx.currentTime + 0.12) { | |
| scheduleStep(step, nextTime); | |
| step = (step + 1) % STEPS; | |
| nextTime += 60 / +bpmInput.value / 4; | |
| } | |
| } | |
| playBtn.addEventListener('click', function () { | |
| if (playing) { | |
| playing = false; | |
| clearInterval(timer); | |
| playBtn.innerHTML = ICON_PLAY + '<span>Play</span>'; | |
| markPlayhead(-1); | |
| } else { | |
| ac().resume(); | |
| playing = true; | |
| step = 0; | |
| nextTime = ctx.currentTime + 0.06; | |
| playBtn.innerHTML = ICON_STOP + '<span>Stop</span>'; | |
| timer = setInterval(loop, 25); | |
| } | |
| }); | |
| clearBtn.addEventListener('click', function () { | |
| grid.forEach(function (row) { row.fill(false); }); | |
| cells.forEach(function (row) { row.forEach(function (b) { b.classList.remove('on'); }); }); | |
| }); | |
| // Debug/test handle | |
| window.__seq = { grid: grid, trackGains: function () { return trackGains; }, master: function () { return master; } }; | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |