aboutsummaryrefslogtreecommitdiff
path: root/src/view
diff options
context:
space:
mode:
authorJoris2022-06-11 16:42:33 +0200
committerJoris2022-06-11 16:42:33 +0200
commit03197b1ab992540b951fcbc6f841cfcd42a757f3 (patch)
tree2eb5277462b8dfef41e901a945f251725fb7ad8f /src/view
parent70c672535f36edaeaf1d63d4637830b564271c34 (diff)
Add kick sequencer
Diffstat (limited to 'src/view')
-rw-r--r--src/view/sequencer.ts60
-rw-r--r--src/view/sequencer/addRemoveBeat.ts32
-rw-r--r--src/view/sequencer/block.ts28
-rw-r--r--src/view/sequencer/play.ts68
4 files changed, 188 insertions, 0 deletions
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
+ )
+ }
+ )
+ )
+ )
+}