From 03197b1ab992540b951fcbc6f841cfcd42a757f3 Mon Sep 17 00:00:00 2001 From: Joris Date: Sat, 11 Jun 2022 16:42:33 +0200 Subject: Add kick sequencer --- src/view/sequencer.ts | 60 ++++++++++++++++++++++++++++++++ src/view/sequencer/addRemoveBeat.ts | 32 +++++++++++++++++ src/view/sequencer/block.ts | 28 +++++++++++++++ src/view/sequencer/play.ts | 68 +++++++++++++++++++++++++++++++++++++ 4 files changed, 188 insertions(+) create mode 100644 src/view/sequencer.ts create mode 100644 src/view/sequencer/addRemoveBeat.ts create mode 100644 src/view/sequencer/block.ts create mode 100644 src/view/sequencer/play.ts (limited to 'src/view') diff --git a/src/view/sequencer.ts b/src/view/sequencer.ts new file mode 100644 index 0000000..150f89b --- /dev/null +++ b/src/view/sequencer.ts @@ -0,0 +1,60 @@ +import h, { classNames } from 'lib/h' +import * as soundsLib from 'sounds' +import * as play from 'view/sequencer/play' +import * as addRemoveBeat from 'view/sequencer/addRemoveBeat' +import * as block from 'view/sequencer/block' + +export function view() { + let index = -1 + let blocks = [true] + let blocksNode = h('div', + { className: 'g-Sequencer__Blocks' }, + block.view({ + checked: true, + onCheck: checked => blocks[0] = checked + }) + ) + + let onNextStep = (sounds: soundsLib.Sounds) => { + let oldIndex = index + let newIndex = (index + 1) % blocks.length + index = newIndex + + let oldBlock = blocksNode.childNodes[oldIndex] as HTMLElement + if (oldBlock !== undefined) oldBlock.classList.remove('g-Sequencer__Block--Beat') + + let newBlock = blocksNode.childNodes[newIndex] as HTMLElement + newBlock.classList.add('g-Sequencer__Block--Beat') + + if (blocks[newIndex]) soundsLib.playKick(sounds) + } + + let sequencer = h('div', { className: 'g-Sequencer' }, + play.view({ + onNextStep, + onStop: () => { + let block = blocksNode.childNodes[index] as HTMLElement + block.classList.remove('g-Sequencer__Block--Beat') + index = -1 + } + }), + addRemoveBeat.view({ + initBeats: 1, + onRemove: index => { + let lastBlock = blocksNode.childNodes[index] + blocksNode.removeChild(lastBlock) + blocks.pop() + }, + onAdd: index => { + blocks.push(false) + blocksNode.appendChild(block.view({ + checked: false, + onCheck: checked => blocks[index] = checked + })) + } + }), + blocksNode + ) + + return sequencer +} diff --git a/src/view/sequencer/addRemoveBeat.ts b/src/view/sequencer/addRemoveBeat.ts new file mode 100644 index 0000000..e991d3f --- /dev/null +++ b/src/view/sequencer/addRemoveBeat.ts @@ -0,0 +1,32 @@ +import h, { classNames } from 'lib/h' + +interface Params { + initBeats: number, + onRemove: (index: number) => void, + onAdd: (index: number) => void +} + +export function view({ initBeats, onRemove, onAdd }: Params) { + let beats = initBeats + + return h('div', { className: 'g-Sequencer__Buttons' }, + h('button', + { onclick: () => { + if (beats > 1) { + beats -= 1 + onRemove(beats) + } + } + }, + 'Remove Beat' + ), + h('button', + { onclick: () => { + onAdd(beats) + beats += 1 + } + }, + 'Add Beat' + ) + ) +} diff --git a/src/view/sequencer/block.ts b/src/view/sequencer/block.ts new file mode 100644 index 0000000..5776120 --- /dev/null +++ b/src/view/sequencer/block.ts @@ -0,0 +1,28 @@ +import h, { classNames } from 'lib/h' + +interface Params { + checked: boolean, + onCheck: (checked: boolean) => void +} + +export function view({ checked, onCheck }: Params) { + return h('div', + { className: classNames({ + 'g-Sequencer__Block': true, + 'g-Sequencer__Block--Checked': checked + }), + onclick: (e: Event) => { + checked = !checked + onCheck(checked) + let target = e.target as HTMLElement + if (target !== undefined) { + if (checked) { + target.classList.add('g-Sequencer__Block--Checked') + } else { + target.classList.remove('g-Sequencer__Block--Checked') + } + } + } + } + ) +} diff --git a/src/view/sequencer/play.ts b/src/view/sequencer/play.ts new file mode 100644 index 0000000..9ff9c81 --- /dev/null +++ b/src/view/sequencer/play.ts @@ -0,0 +1,68 @@ +import h, { classNames } from 'lib/h' +import * as time from 'lib/time' +import * as soundsLib from 'sounds' + +const MIN_BPM: number = 1 +const MAX_BPM: number = 1000 + +interface Params { + onNextStep: (sounds: soundsLib.Sounds) => void, + onStop: () => void +} + +export function view({ onNextStep, onStop }: Params) { + let bpm = 60 + let isPlaying = false + let lastBeat: undefined | number = undefined + + return h('div', {}, + h('button', + { className: 'g-PlayStop', + onclick: async (e: Event) => { + const target = e.target as HTMLButtonElement + isPlaying = !isPlaying + target.innerText = isPlaying ? '■' : '▶' + + let sounds = await soundsLib.load() + let step = (timestamp: number) => { + if (lastBeat === undefined || timestamp - lastBeat > 1000 * 60 / bpm) { + lastBeat = timestamp + onNextStep(sounds) + } + + if (isPlaying) window.requestAnimationFrame(step) + } + + if (isPlaying) { + window.requestAnimationFrame(step) + } else { + onStop() + } + } + }, + '▶' + ) + , h('label', + { className: 'g-Bpm' }, + 'BPM', + h('input', + { className: 'g-Input', + type: 'number', + value: bpm, + min: MIN_BPM, + max: MAX_BPM, + oninput: time.debounce( + (e: Event) => { + const target = e.target as HTMLInputElement + const n = parseInt(target.value) + if (n >= MIN_BPM && n <= MAX_BPM) { + bpm = n + } + }, + 1000 + ) + } + ) + ) + ) +} -- cgit v1.2.3