Deleted unused `KeyboardInputComponent`. Added file upload functionality to `MainView` with WebSocket event handling. Introduced `WebsocketClient` class for managing server connection and file transmission. Enhanced `KeyboardComponent` with configurable key rounding. Updated `WebsocketHandler` for improved binary message handling and storage.
364 lines
11 KiB
Kotlin
364 lines
11 KiB
Kotlin
package nl.astraeus.vst.ui.components
|
|
|
|
import kotlinx.html.SVG
|
|
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.AlignItems
|
|
import nl.astraeus.css.properties.Display
|
|
import nl.astraeus.css.properties.FlexDirection
|
|
import nl.astraeus.css.properties.FontWeight
|
|
import nl.astraeus.css.properties.JustifyContent
|
|
import nl.astraeus.css.properties.Position
|
|
import nl.astraeus.css.properties.TextAlign
|
|
import nl.astraeus.css.properties.prc
|
|
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",
|
|
initialOctave: Int = 4,
|
|
val keyboardWidth: Int = 210,
|
|
val keyboardHeight: Int = keyboardWidth / 2,
|
|
val rounding: Int = 4,
|
|
val onNoteDown: (Int) -> Unit = {},
|
|
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
|
|
var octave: Int
|
|
get() = state.octave
|
|
set(value) {
|
|
updateOctave(value)
|
|
}
|
|
|
|
// 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 one octave
|
|
private val whiteKeys = BASE_WHITE_KEYS
|
|
private val blackKeys = BASE_BLACK_KEYS
|
|
|
|
// Key dimensions
|
|
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(
|
|
whiteKeyWidth - blackKeyWidth/2,
|
|
whiteKeyWidth*2 - blackKeyWidth/2,
|
|
whiteKeyWidth*4 - blackKeyWidth/2,
|
|
whiteKeyWidth*5 - blackKeyWidth/2,
|
|
whiteKeyWidth*6 - blackKeyWidth/2
|
|
)
|
|
|
|
fun noteDown(midiNote: Int) {
|
|
state = state.copy(pressedNotes = state.pressedNotes + midiNote)
|
|
onNoteDown(midiNote)
|
|
requestUpdate()
|
|
}
|
|
|
|
fun noteUp(midiNote: Int) {
|
|
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 = state.pressedNotes.toSet()
|
|
for (note in notesToRelease) {
|
|
noteUp(note)
|
|
}
|
|
// 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 + KEYBOARD_CONTROLS_HEIGHT}px"
|
|
|
|
onMouseLeaveFunction = { event ->
|
|
if (event is MouseEvent) {
|
|
releaseAllNotes()
|
|
event.preventDefault()
|
|
}
|
|
}
|
|
|
|
div(KeyboardControlsCls.name) {
|
|
// Decrease octave button
|
|
div(OctaveButtonCls.name) {
|
|
style = "width: ${whiteKeyWidth}px"
|
|
+"<"
|
|
onMouseDownFunction = { event ->
|
|
if (event is MouseEvent) {
|
|
updateOctave(octave - 1)
|
|
event.preventDefault()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Title and octave display container
|
|
div(KeyboardInfoCls.name) {
|
|
div(KeyboardTitleCls.name) {
|
|
// Show title of the keyboard
|
|
+title
|
|
}
|
|
|
|
div(KeyboardOctaveCls.name) {
|
|
// Show current octave the piano is being played at
|
|
+"Octave: $octave"
|
|
}
|
|
}
|
|
|
|
// Increase octave button
|
|
div(OctaveButtonCls.name) {
|
|
style = "width: ${whiteKeyWidth}px"
|
|
+">"
|
|
onMouseDownFunction = { event ->
|
|
if (event is MouseEvent) {
|
|
updateOctave(octave + 1)
|
|
event.preventDefault()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
getMidiNoteFromMousePosition(event.offsetX, event.offsetY)?.let { noteDown(it) }
|
|
event.preventDefault()
|
|
}
|
|
}
|
|
|
|
onMouseUpFunction = { event ->
|
|
if (event is MouseEvent) {
|
|
getMidiNoteFromMousePosition(event.offsetX, event.offsetY)?.let { noteUp(it) }
|
|
event.preventDefault()
|
|
}
|
|
}
|
|
|
|
// Draw white keys
|
|
this.renderWhiteKeys()
|
|
|
|
// Draw black keys
|
|
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,
|
|
rounding,
|
|
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,
|
|
rounding,
|
|
if (isPressed) BlackKeyPressedCls.name else BlackKeyCls.name
|
|
)
|
|
}
|
|
}
|
|
|
|
companion object : CssId("keyboard") {
|
|
// CSS class names
|
|
object KeyboardCls : CssName()
|
|
object KeyboardControlsCls : CssName()
|
|
object KeyboardInfoCls : CssName()
|
|
object KeyboardTitleCls : CssName()
|
|
object KeyboardOctaveCls : CssName()
|
|
object KeyboardKeysCls : CssName()
|
|
object OctaveButtonCls : CssName()
|
|
object WhiteKeyCls : CssName()
|
|
object BlackKeyCls : CssName()
|
|
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)) {
|
|
position(Position.relative)
|
|
margin(5.px)
|
|
|
|
select(cls(KeyboardControlsCls)) {
|
|
position(Position.relative)
|
|
display(Display.flex)
|
|
flexDirection(FlexDirection.row)
|
|
justifyContent(JustifyContent.spaceBetween)
|
|
alignItems(AlignItems.center)
|
|
width(100.prc)
|
|
height(50.px)
|
|
marginBottom(10.px)
|
|
}
|
|
|
|
select(cls(KeyboardInfoCls)) {
|
|
display(Display.flex)
|
|
flexDirection(FlexDirection.column)
|
|
alignItems(AlignItems.center)
|
|
justifyContent(JustifyContent.center)
|
|
flex("1")
|
|
}
|
|
|
|
select(cls(KeyboardTitleCls)) {
|
|
textAlign(TextAlign.center)
|
|
fontSize(1.2.rem)
|
|
color(Css.currentStyle.mainFontColor)
|
|
}
|
|
|
|
select(cls(KeyboardOctaveCls)) {
|
|
textAlign(TextAlign.center)
|
|
fontSize(1.0.rem)
|
|
color(Css.currentStyle.mainFontColor)
|
|
marginTop(5.px)
|
|
}
|
|
|
|
select(cls(OctaveButtonCls)) {
|
|
height(50.px)
|
|
plain("background-color", Css.currentStyle.buttonBackgroundColor.toString())
|
|
color(Css.currentStyle.mainFontColor)
|
|
border("1px solid ${Css.currentStyle.buttonBorderColor}")
|
|
textAlign(TextAlign.center)
|
|
lineHeight(50.px)
|
|
fontSize(1.5.rem)
|
|
fontWeight(FontWeight.bold)
|
|
cursor("pointer")
|
|
plain("-webkit-touch-callout", "none")
|
|
plain("-webkit-user-select", "none")
|
|
plain("-moz-user-select", "none")
|
|
plain("-ms-user-select", "none")
|
|
plain("user-select", "none")
|
|
}
|
|
|
|
select(cls(KeyboardKeysCls)) {
|
|
position(Position.relative)
|
|
}
|
|
}
|
|
|
|
select(cls(WhiteKeyCls)) {
|
|
plain("fill", "#FFFFFF")
|
|
plain("stroke", "#000000")
|
|
plain("stroke-width", "1")
|
|
}
|
|
|
|
select(cls(WhiteKeyPressedCls)) {
|
|
plain("fill", "#E6E6E6") // 10% darker than white
|
|
plain("stroke", "#000000")
|
|
plain("stroke-width", "1")
|
|
}
|
|
|
|
select(cls(BlackKeyCls)) {
|
|
plain("fill", "#000000")
|
|
plain("stroke", "#000000")
|
|
plain("stroke-width", "1")
|
|
}
|
|
|
|
select(cls(BlackKeyPressedCls)) {
|
|
plain("fill", "#333333") // 10% lighter than black
|
|
plain("stroke", "#000000")
|
|
plain("stroke-width", "1")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|