Compare commits
4 Commits
538aa6b9ae
...
1d02a6ee16
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d02a6ee16 | |||
| 9c9962d7db | |||
| 5c16b57ae9 | |||
| 9ab909cf6c |
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user