From 70a8b55c47297e6cc0e8dd9d119b811ea46adeee Mon Sep 17 00:00:00 2001 From: rnentjes Date: Thu, 5 Jun 2025 21:26:07 +0200 Subject: [PATCH] Add KeyboardComponent --- .../vst/ui/components/KeyboardComponent.kt | 269 ++++++++++++++++++ .../nl/astraeus/vst/ui/view/MainView.kt | 6 +- 2 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 src/jsMain/kotlin/nl/astraeus/vst/ui/components/KeyboardComponent.kt diff --git a/src/jsMain/kotlin/nl/astraeus/vst/ui/components/KeyboardComponent.kt b/src/jsMain/kotlin/nl/astraeus/vst/ui/components/KeyboardComponent.kt new file mode 100644 index 0000000..a1eea60 --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/vst/ui/components/KeyboardComponent.kt @@ -0,0 +1,269 @@ +package nl.astraeus.vst.ui.components + +import kotlinx.html.div +import kotlinx.html.js.onMouseDownFunction +import kotlinx.html.js.onMouseLeaveFunction +import kotlinx.html.js.onMouseUpFunction +import kotlinx.html.style +import kotlinx.html.svg +import nl.astraeus.css.properties.Position +import nl.astraeus.css.properties.TextAlign +import nl.astraeus.css.properties.px +import nl.astraeus.css.properties.rem +import nl.astraeus.css.style.cls +import nl.astraeus.komp.HtmlBuilder +import nl.astraeus.komp.Komponent +import nl.astraeus.vst.ui.css.Css +import nl.astraeus.vst.ui.css.Css.defineCss +import nl.astraeus.vst.ui.css.CssId +import nl.astraeus.vst.ui.css.CssName +import nl.astraeus.vst.ui.util.height +import nl.astraeus.vst.ui.util.rect +import nl.astraeus.vst.ui.util.width +import org.w3c.dom.events.MouseEvent + +/** + * The keyboard component shows 1 octabe of a (piano) keyboard and + * calls the noteDown and noteUp methods when the keys are clicked + */ +class KeyboardComponent( + val title: String = "Keyboard", + val octave: Int = 4, + val onNoteDown: (Int) -> Unit = {}, + val onNoteUp: (Int) -> Unit = {} +) : Komponent() { + + // Set to track which notes are currently pressed + private val pressedNotes = mutableSetOf() + + // MIDI note numbers for C4 to B4 (one octave) + private val whiteKeys = listOf(60, 62, 64, 65, 67, 69, 71) + private val blackKeys = listOf(61, 63, 66, 68, 70) + + // Key dimensions + private val keyboardWidth = 210 + private val keyboardHeight = 100 + private val whiteKeyWidth = 30 + private val whiteKeyHeight = 100 + private val blackKeyWidth = 20 + private val blackKeyHeight = 60 + + // Calculate positions for black keys + private val blackKeyPositions = listOf( + whiteKeyWidth - blackKeyWidth/2, + whiteKeyWidth*2 - blackKeyWidth/2, + whiteKeyWidth*4 - blackKeyWidth/2, + whiteKeyWidth*5 - blackKeyWidth/2, + whiteKeyWidth*6 - blackKeyWidth/2 + ) + + fun noteDown(midiNote: Int) { + println("noteDown $midiNote") + pressedNotes.add(midiNote) + onNoteDown(midiNote) + } + + fun noteUp(midiNote: Int) { + println("noteUp $midiNote") + pressedNotes.remove(midiNote) + onNoteUp(midiNote) + } + + private fun releaseAllNotes() { + // Create a copy of the set to avoid concurrent modification + val notesToRelease = pressedNotes.toSet() + for (note in notesToRelease) { + noteUp(note) + } + // Clear the set just to be safe + pressedNotes.clear() + } + + override fun HtmlBuilder.render() { + div(KeyboardCls.name) { + style = "width: ${keyboardWidth}px; height: ${keyboardHeight + 60}px" + + onMouseLeaveFunction = { event -> + if (event is MouseEvent) { + releaseAllNotes() + event.preventDefault() + } + } + + div(KeyboardTitleCls.name) { + // Show title of the keyboard + +title + } + + div(KeyboardOctaveCls.name) { + // Show current octave the piano is being played at + +"Octave: $octave" + } + + div(KeyboardKeysCls.name) { + // Draw the keyboard with SVG, and mousedown and mouseup methods + // that call noteDown and noteUp + svg { + width(keyboardWidth) + height(keyboardHeight) + + // Define mouse event handlers at the SVG level + onMouseDownFunction = { event -> + if (event is MouseEvent) { + val x = event.offsetX + val y = event.offsetY + + // Check if click is on a black key (black keys are on top of white keys) + if (y <= blackKeyHeight) { + var blackKeyPressed = false + for (j in 0 until 5) { + if (x >= blackKeyPositions[j] && x <= blackKeyPositions[j] + blackKeyWidth) { + noteDown(blackKeys[j] + (octave - 4) * 12) + blackKeyPressed = true + break + } + } + + // If no black key was pressed, check for white key + if (!blackKeyPressed) { + // Check if click is on a white key + val keyIndex = (x / whiteKeyWidth).toInt() + if (keyIndex in 0..6) { + noteDown(whiteKeys[keyIndex] + (octave - 4) * 12) + } + } + } else { + // If y > blackKeyHeight, it's definitely a white key + val keyIndex = (x / whiteKeyWidth).toInt() + if (keyIndex in 0..6) { + noteDown(whiteKeys[keyIndex] + (octave - 4) * 12) + } + } + + event.preventDefault() + } + } + + onMouseUpFunction = { event -> + if (event is MouseEvent) { + val x = event.offsetX + val y = event.offsetY + + // Check if release is on a black key (black keys are on top of white keys) + if (y <= blackKeyHeight) { + var blackKeyReleased = false + for (j in 0 until 5) { + if (x >= blackKeyPositions[j] && x <= blackKeyPositions[j] + blackKeyWidth) { + noteUp(blackKeys[j] + (octave - 4) * 12) + blackKeyReleased = true + break + } + } + + // If no black key was released, check for white key + if (!blackKeyReleased) { + // Check if release is on a white key + val keyIndex = (x / whiteKeyWidth).toInt() + if (keyIndex in 0..6) { + noteUp(whiteKeys[keyIndex] + (octave - 4) * 12) + } + } + } else { + // If y > blackKeyHeight, it's definitely a white key + val keyIndex = (x / whiteKeyWidth).toInt() + if (keyIndex in 0..6) { + noteUp(whiteKeys[keyIndex] + (octave - 4) * 12) + } + } + + event.preventDefault() + } + } + + // Draw white keys + for (i in 0 until 7) { + val midiNote = whiteKeys[i] + (octave - 4) * 12 + rect( + i * whiteKeyWidth, + 0, + whiteKeyWidth, + whiteKeyHeight, + 0, + WhiteKeyCls.name + ) + } + + // Draw black keys + for (i in 0 until 5) { + val midiNote = blackKeys[i] + (octave - 4) * 12 + rect( + blackKeyPositions[i], + 0, + blackKeyWidth, + blackKeyHeight, + 0, + BlackKeyCls.name + ) + } + } + } + } + } + + companion object : CssId("keyboard") { + object KeyboardCls : CssName() + object KeyboardTitleCls : CssName() + object KeyboardOctaveCls : CssName() + object KeyboardKeysCls : CssName() + object WhiteKeyCls : CssName() + object BlackKeyCls : CssName() + + init { + defineCss { + select(cls(KeyboardCls)) { + position(Position.relative) + margin(5.px) + + select(cls(KeyboardTitleCls)) { + position(Position.absolute) + width(100.px) + textAlign(TextAlign.center) + fontSize(1.2.rem) + color(Css.currentStyle.mainFontColor) + top(5.px) + left(0.px) + right(0.px) + } + + select(cls(KeyboardOctaveCls)) { + position(Position.absolute) + width(100.px) + textAlign(TextAlign.center) + fontSize(1.0.rem) + color(Css.currentStyle.mainFontColor) + top(30.px) + left(0.px) + right(0.px) + } + + select(cls(KeyboardKeysCls)) { + position(Position.absolute) + top(60.px) + } + } + + select(cls(WhiteKeyCls)) { + plain("fill", "#FFFFFF") + plain("stroke", "#000000") + plain("stroke-width", "1") + } + + select(cls(BlackKeyCls)) { + plain("fill", "#000000") + plain("stroke", "#000000") + plain("stroke-width", "1") + } + } + } + } +} diff --git a/test-app/src/jsMain/kotlin/nl/astraeus/vst/ui/view/MainView.kt b/test-app/src/jsMain/kotlin/nl/astraeus/vst/ui/view/MainView.kt index 9f14247..ee39569 100644 --- a/test-app/src/jsMain/kotlin/nl/astraeus/vst/ui/view/MainView.kt +++ b/test-app/src/jsMain/kotlin/nl/astraeus/vst/ui/view/MainView.kt @@ -28,6 +28,7 @@ import nl.astraeus.css.style.cls import nl.astraeus.komp.HtmlBuilder import nl.astraeus.komp.Komponent import nl.astraeus.vst.ui.components.ExpKnobComponent +import nl.astraeus.vst.ui.components.KeyboardComponent import nl.astraeus.vst.ui.components.KnobComponent import nl.astraeus.vst.ui.css.Css import nl.astraeus.vst.ui.css.Css.defineCss @@ -110,6 +111,10 @@ class MainView : Komponent() { */ } } + } + div { + include(KeyboardComponent()) + } /* div { span(ButtonBarCss.name) { @@ -176,7 +181,6 @@ class MainView : Komponent() { ) } } - } } companion object MainViewCss : CssName() {