Add KeyboardComponent

This commit is contained in:
2025-06-05 21:26:07 +02:00
parent b94a50d6d4
commit 70a8b55c47
2 changed files with 274 additions and 1 deletions

View File

@@ -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<Int>()
// 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")
}
}
}
}
}

View File

@@ -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() {