Updated key size calculations to be dynamic based on keyboard dimensions for improved scalability. Removed unnecessary `println` calls and redundant MIDI note calculations in drawing logic for better performance and code clarity.
265 lines
7.8 KiB
Kotlin
265 lines
7.8 KiB
Kotlin
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 = 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")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|