Compare commits
4 Commits
538aa6b9ae
...
1d02a6ee16
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d02a6ee16 | |||
| 9c9962d7db | |||
| 5c16b57ae9 | |||
| 9ab909cf6c |
@@ -1,5 +1,6 @@
|
|||||||
package nl.astraeus.vst.ui.components
|
package nl.astraeus.vst.ui.components
|
||||||
|
|
||||||
|
import kotlinx.html.SVG
|
||||||
import kotlinx.html.div
|
import kotlinx.html.div
|
||||||
import kotlinx.html.js.onMouseDownFunction
|
import kotlinx.html.js.onMouseDownFunction
|
||||||
import kotlinx.html.js.onMouseLeaveFunction
|
import kotlinx.html.js.onMouseLeaveFunction
|
||||||
@@ -41,30 +42,36 @@ class KeyboardComponent(
|
|||||||
val onNoteUp: (Int) -> Unit = {}
|
val onNoteUp: (Int) -> Unit = {}
|
||||||
) : Komponent() {
|
) : 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
|
// Current octave with range validation
|
||||||
private var _octave: Int = initialOctave
|
|
||||||
var octave: Int
|
var octave: Int
|
||||||
get() = _octave
|
get() = state.octave
|
||||||
set(value) {
|
set(value) {
|
||||||
_octave = when {
|
updateOctave(value)
|
||||||
value < 0 -> 0
|
|
||||||
value > 9 -> 9
|
|
||||||
else -> value
|
|
||||||
}
|
|
||||||
requestUpdate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set to track which notes are currently pressed
|
// Update state with functions that return new state
|
||||||
private val pressedNotes = mutableSetOf<Int>()
|
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)
|
// MIDI note numbers for one octave
|
||||||
private val whiteKeys = listOf(60, 62, 64, 65, 67, 69, 71)
|
private val whiteKeys = BASE_WHITE_KEYS
|
||||||
private val blackKeys = listOf(61, 63, 66, 68, 70)
|
private val blackKeys = BASE_BLACK_KEYS
|
||||||
|
|
||||||
// Key dimensions
|
// Key dimensions
|
||||||
private val whiteKeyWidth = keyboardWidth / 7
|
private val whiteKeyWidth = keyboardWidth / WHITE_KEYS_PER_OCTAVE
|
||||||
private val blackKeyWidth = (keyboardWidth / 9)
|
private val blackKeyWidth = (keyboardWidth / BLACK_KEY_WIDTH_DIVISOR)
|
||||||
private val blackKeyHeight = keyboardHeight * 60 / 100
|
private val blackKeyHeight = keyboardHeight * BLACK_KEY_HEIGHT_PERCENTAGE / 100
|
||||||
|
|
||||||
// Calculate positions for black keys
|
// Calculate positions for black keys
|
||||||
private val blackKeyPositions = listOf(
|
private val blackKeyPositions = listOf(
|
||||||
@@ -76,30 +83,56 @@ class KeyboardComponent(
|
|||||||
)
|
)
|
||||||
|
|
||||||
fun noteDown(midiNote: Int) {
|
fun noteDown(midiNote: Int) {
|
||||||
pressedNotes.add(midiNote)
|
state = state.copy(pressedNotes = state.pressedNotes + midiNote)
|
||||||
onNoteDown(midiNote)
|
onNoteDown(midiNote)
|
||||||
requestUpdate()
|
requestUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun noteUp(midiNote: Int) {
|
fun noteUp(midiNote: Int) {
|
||||||
pressedNotes.remove(midiNote)
|
state = state.copy(pressedNotes = state.pressedNotes - midiNote)
|
||||||
onNoteUp(midiNote)
|
onNoteUp(midiNote)
|
||||||
requestUpdate()
|
requestUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun releaseAllNotes() {
|
private fun releaseAllNotes() {
|
||||||
// Create a copy of the set to avoid concurrent modification
|
// Create a copy of the set to avoid concurrent modification
|
||||||
val notesToRelease = pressedNotes.toSet()
|
val notesToRelease = state.pressedNotes.toSet()
|
||||||
for (note in notesToRelease) {
|
for (note in notesToRelease) {
|
||||||
noteUp(note)
|
noteUp(note)
|
||||||
}
|
}
|
||||||
// Clear the set just to be safe
|
// Just to be safe, clear all pressed notes
|
||||||
pressedNotes.clear()
|
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() {
|
override fun HtmlBuilder.render() {
|
||||||
div(KeyboardCls.name) {
|
div(KeyboardCls.name) {
|
||||||
style = "width: ${keyboardWidth}px; height: ${keyboardHeight + 60}px"
|
style = "width: ${keyboardWidth}px; height: ${keyboardHeight + KEYBOARD_CONTROLS_HEIGHT}px"
|
||||||
|
|
||||||
onMouseLeaveFunction = { event ->
|
onMouseLeaveFunction = { event ->
|
||||||
if (event is MouseEvent) {
|
if (event is MouseEvent) {
|
||||||
@@ -115,7 +148,7 @@ class KeyboardComponent(
|
|||||||
+"<"
|
+"<"
|
||||||
onMouseDownFunction = { event ->
|
onMouseDownFunction = { event ->
|
||||||
if (event is MouseEvent) {
|
if (event is MouseEvent) {
|
||||||
octave--
|
updateOctave(octave - 1)
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,7 +173,7 @@ class KeyboardComponent(
|
|||||||
+">"
|
+">"
|
||||||
onMouseDownFunction = { event ->
|
onMouseDownFunction = { event ->
|
||||||
if (event is MouseEvent) {
|
if (event is MouseEvent) {
|
||||||
octave++
|
updateOctave(octave + 1)
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,109 +190,62 @@ class KeyboardComponent(
|
|||||||
// Define mouse event handlers at the SVG level
|
// Define mouse event handlers at the SVG level
|
||||||
onMouseDownFunction = { event ->
|
onMouseDownFunction = { event ->
|
||||||
if (event is MouseEvent) {
|
if (event is MouseEvent) {
|
||||||
val x = event.offsetX
|
getMidiNoteFromMousePosition(event.offsetX, event.offsetY)?.let { noteDown(it) }
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseUpFunction = { event ->
|
onMouseUpFunction = { event ->
|
||||||
if (event is MouseEvent) {
|
if (event is MouseEvent) {
|
||||||
val x = event.offsetX
|
getMidiNoteFromMousePosition(event.offsetX, event.offsetY)?.let { noteUp(it) }
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw white keys
|
// Draw white keys
|
||||||
for (i in 0 until 7) {
|
this.renderWhiteKeys()
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw black keys
|
// Draw black keys
|
||||||
for (i in 0 until 5) {
|
this.renderBlackKeys()
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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") {
|
companion object : CssId("keyboard") {
|
||||||
|
// CSS class names
|
||||||
object KeyboardCls : CssName()
|
object KeyboardCls : CssName()
|
||||||
object KeyboardControlsCls : CssName()
|
object KeyboardControlsCls : CssName()
|
||||||
object KeyboardInfoCls : CssName()
|
object KeyboardInfoCls : CssName()
|
||||||
@@ -272,6 +258,21 @@ class KeyboardComponent(
|
|||||||
object WhiteKeyPressedCls : CssName()
|
object WhiteKeyPressedCls : CssName()
|
||||||
object BlackKeyPressedCls : 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 {
|
init {
|
||||||
defineCss {
|
defineCss {
|
||||||
select(cls(KeyboardCls)) {
|
select(cls(KeyboardCls)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user