Compare commits

...

4 Commits

Author SHA1 Message Date
1d02a6ee16 Refactor KeyboardComponent to use immutable state for octave and pressed notes
Replaced mutable state with an immutable `KeyboardState` data class to track octave and pressed notes. Updated state management logic with functional updates for improved consistency and immutability. Simplified note handling and rendering to reference the unified state object.
2025-06-07 13:12:03 +02:00
9c9962d7db Refactor KeyboardComponent key rendering logic
Extracted white and black key rendering into separate `renderWhiteKeys` and `renderBlackKeys` functions for improved readability and modularity. Simplified drawing logic by delegating key rendering to these helper functions.
2025-06-07 11:57:17 +02:00
5c16b57ae9 Refactor KeyboardComponent with constants for note and key handling
Replaced hardcoded values for octaves, keys, and dimensions with named constants for improved readability and maintainability. Simplified calculations and loops using these constants. Enhanced clarity in key rendering and MIDI note calculations.
2025-06-07 11:39:21 +02:00
9ab909cf6c Refactor KeyboardComponent note calculation logic
Extracted MIDI note calculation into a reusable `getMidiNoteFromMousePosition` function. Replaced redundant inline logic in mouse event handlers and key drawing sections for improved readability and maintainability.
2025-06-07 11:24:30 +02:00

View File

@@ -1,5 +1,6 @@
package nl.astraeus.vst.ui.components
import kotlinx.html.SVG
import kotlinx.html.div
import kotlinx.html.js.onMouseDownFunction
import kotlinx.html.js.onMouseLeaveFunction
@@ -41,30 +42,36 @@ class KeyboardComponent(
val onNoteUp: (Int) -> Unit = {}
) : Komponent() {
// Define a data class for keyboard state
data class KeyboardState(
val octave: Int,
val pressedNotes: Set<Int> = emptySet()
)
// Use immutable state
private var state = KeyboardState(initialOctave)
// Current octave with range validation
private var _octave: Int = initialOctave
var octave: Int
get() = _octave
get() = state.octave
set(value) {
_octave = when {
value < 0 -> 0
value > 9 -> 9
else -> value
}
requestUpdate()
updateOctave(value)
}
// Set to track which notes are currently pressed
private val pressedNotes = mutableSetOf<Int>()
// Update state with functions that return new state
private fun updateOctave(newOctave: Int) {
state = state.copy(octave = newOctave.coerceIn(MIN_OCTAVE, MAX_OCTAVE))
requestUpdate()
}
// 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)
// MIDI note numbers for one octave
private val whiteKeys = BASE_WHITE_KEYS
private val blackKeys = BASE_BLACK_KEYS
// Key dimensions
private val whiteKeyWidth = keyboardWidth / 7
private val blackKeyWidth = (keyboardWidth / 9)
private val blackKeyHeight = keyboardHeight * 60 / 100
private val whiteKeyWidth = keyboardWidth / WHITE_KEYS_PER_OCTAVE
private val blackKeyWidth = (keyboardWidth / BLACK_KEY_WIDTH_DIVISOR)
private val blackKeyHeight = keyboardHeight * BLACK_KEY_HEIGHT_PERCENTAGE / 100
// Calculate positions for black keys
private val blackKeyPositions = listOf(
@@ -76,30 +83,56 @@ class KeyboardComponent(
)
fun noteDown(midiNote: Int) {
pressedNotes.add(midiNote)
state = state.copy(pressedNotes = state.pressedNotes + midiNote)
onNoteDown(midiNote)
requestUpdate()
}
fun noteUp(midiNote: Int) {
pressedNotes.remove(midiNote)
state = state.copy(pressedNotes = state.pressedNotes - midiNote)
onNoteUp(midiNote)
requestUpdate()
}
private fun releaseAllNotes() {
// Create a copy of the set to avoid concurrent modification
val notesToRelease = pressedNotes.toSet()
val notesToRelease = state.pressedNotes.toSet()
for (note in notesToRelease) {
noteUp(note)
}
// Clear the set just to be safe
pressedNotes.clear()
// Just to be safe, clear all pressed notes
state = state.copy(pressedNotes = emptySet())
}
private fun getOctaveOffset(): Int = (octave - OCTAVE_BASE) * NOTES_PER_OCTAVE
private fun getMidiNoteFromMousePosition(x: Double, y: Double): Int? {
// Check if click is on a black key (black keys are on top of white keys)
if (y <= blackKeyHeight) {
for (j in 0 until BLACK_KEYS_PER_OCTAVE) {
if (x >= blackKeyPositions[j] && x <= blackKeyPositions[j] + blackKeyWidth) {
return blackKeys[j] + getOctaveOffset()
}
}
// If no black key was pressed, check for white key
val keyIndex = (x / whiteKeyWidth).toInt()
if (keyIndex in 0 until WHITE_KEYS_PER_OCTAVE) {
return whiteKeys[keyIndex] + getOctaveOffset()
}
} else {
// If y > blackKeyHeight, it's definitely a white key
val keyIndex = (x / whiteKeyWidth).toInt()
if (keyIndex in 0 until WHITE_KEYS_PER_OCTAVE) {
return whiteKeys[keyIndex] + getOctaveOffset()
}
}
return null
}
override fun HtmlBuilder.render() {
div(KeyboardCls.name) {
style = "width: ${keyboardWidth}px; height: ${keyboardHeight + 60}px"
style = "width: ${keyboardWidth}px; height: ${keyboardHeight + KEYBOARD_CONTROLS_HEIGHT}px"
onMouseLeaveFunction = { event ->
if (event is MouseEvent) {
@@ -115,7 +148,7 @@ class KeyboardComponent(
+"<"
onMouseDownFunction = { event ->
if (event is MouseEvent) {
octave--
updateOctave(octave - 1)
event.preventDefault()
}
}
@@ -140,7 +173,7 @@ class KeyboardComponent(
+">"
onMouseDownFunction = { event ->
if (event is MouseEvent) {
octave++
updateOctave(octave + 1)
event.preventDefault()
}
}
@@ -157,109 +190,62 @@ class KeyboardComponent(
// 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 - 5) * 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 - 5) * 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 - 5) * 12)
}
}
getMidiNoteFromMousePosition(event.offsetX, event.offsetY)?.let { noteDown(it) }
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 - 5) * 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 - 5) * 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 - 5) * 12)
}
}
getMidiNoteFromMousePosition(event.offsetX, event.offsetY)?.let { noteUp(it) }
event.preventDefault()
}
}
// Draw white keys
for (i in 0 until 7) {
val midiNote = whiteKeys[i] + (octave - 5) * 12
val isPressed = pressedNotes.contains(midiNote)
rect(
i * whiteKeyWidth,
0,
whiteKeyWidth,
keyboardHeight,
0,
if (isPressed) WhiteKeyPressedCls.name else WhiteKeyCls.name
)
}
this.renderWhiteKeys()
// Draw black keys
for (i in 0 until 5) {
val midiNote = blackKeys[i] + (octave - 5) * 12
val isPressed = pressedNotes.contains(midiNote)
rect(
blackKeyPositions[i],
0,
blackKeyWidth,
blackKeyHeight,
0,
if (isPressed) BlackKeyPressedCls.name else BlackKeyCls.name
)
}
this.renderBlackKeys()
}
}
}
}
// Render white keys
private fun SVG.renderWhiteKeys() {
for (i in 0 until WHITE_KEYS_PER_OCTAVE) {
val midiNote = whiteKeys[i] + getOctaveOffset()
val isPressed = state.pressedNotes.contains(midiNote)
rect(
i * whiteKeyWidth,
0,
whiteKeyWidth,
keyboardHeight,
0,
if (isPressed) WhiteKeyPressedCls.name else WhiteKeyCls.name
)
}
}
// Render black keys
private fun SVG.renderBlackKeys() {
for (i in 0 until BLACK_KEYS_PER_OCTAVE) {
val midiNote = blackKeys[i] + getOctaveOffset()
val isPressed = state.pressedNotes.contains(midiNote)
rect(
blackKeyPositions[i],
0,
blackKeyWidth,
blackKeyHeight,
0,
if (isPressed) BlackKeyPressedCls.name else BlackKeyCls.name
)
}
}
companion object : CssId("keyboard") {
// CSS class names
object KeyboardCls : CssName()
object KeyboardControlsCls : CssName()
object KeyboardInfoCls : CssName()
@@ -272,6 +258,21 @@ class KeyboardComponent(
object WhiteKeyPressedCls : CssName()
object BlackKeyPressedCls : CssName()
// Constants
private const val OCTAVE_BASE = 5
private const val NOTES_PER_OCTAVE = 12
private const val BLACK_KEY_HEIGHT_PERCENTAGE = 60
private const val MIN_OCTAVE = 0
private const val MAX_OCTAVE = 9
private const val WHITE_KEYS_PER_OCTAVE = 7
private const val BLACK_KEYS_PER_OCTAVE = 5
private const val BLACK_KEY_WIDTH_DIVISOR = 9
private const val KEYBOARD_CONTROLS_HEIGHT = 60
// MIDI note numbers for C5 to B5 (one octave)
private val BASE_WHITE_KEYS = listOf(60, 62, 64, 65, 67, 69, 71) // C, D, E, F, G, A, B
private val BASE_BLACK_KEYS = listOf(61, 63, 66, 68, 70) // C#, D#, F#, G#, A#
init {
defineCss {
select(cls(KeyboardCls)) {