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 = keyboardWidth / 7 private val blackKeyWidth = whiteKeyWidth * 3 / 2 private val blackKeyHeight = keyboardHeight * 60 / 100 // 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) { pressedNotes.add(midiNote) onNoteDown(midiNote) } fun noteUp(midiNote: Int) { 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) { rect( i * whiteKeyWidth, 0, whiteKeyWidth, keyboardHeight, 0, WhiteKeyCls.name ) } // Draw black keys for (i in 0 until 5) { 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") } } } } }