Files
vst-chip/src/jsMain/kotlin/nl/astraeus/vst/chip/view/MainView.kt
2024-06-30 20:32:43 +02:00

502 lines
14 KiB
Kotlin

package nl.astraeus.vst.chip.view
import kotlinx.browser.window
import kotlinx.html.InputType
import kotlinx.html.canvas
import kotlinx.html.classes
import kotlinx.html.div
import kotlinx.html.h1
import kotlinx.html.input
import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onClickFunction
import kotlinx.html.js.onInputFunction
import kotlinx.html.option
import kotlinx.html.select
import kotlinx.html.span
import nl.astraeus.css.properties.AlignItems
import nl.astraeus.css.properties.BoxSizing
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.Transform
import nl.astraeus.css.properties.em
import nl.astraeus.css.properties.hsla
import nl.astraeus.css.properties.prc
import nl.astraeus.css.properties.px
import nl.astraeus.css.properties.rem
import nl.astraeus.css.properties.vh
import nl.astraeus.css.properties.vw
import nl.astraeus.css.style.Style
import nl.astraeus.css.style.cls
import nl.astraeus.komp.HtmlBuilder
import nl.astraeus.komp.Komponent
import nl.astraeus.komp.currentElement
import nl.astraeus.vst.chip.audio.VstChipWorklet
import nl.astraeus.vst.chip.audio.VstChipWorklet.midiChannel
import nl.astraeus.vst.chip.midi.Midi
import nl.astraeus.vst.chip.ws.WebsocketClient
import nl.astraeus.vst.ui.components.KnobComponent
import nl.astraeus.vst.ui.css.Css
import nl.astraeus.vst.ui.css.Css.defineCss
import nl.astraeus.vst.ui.css.Css.noTextSelect
import nl.astraeus.vst.ui.css.CssName
import nl.astraeus.vst.ui.css.hover
import nl.astraeus.vst.ui.util.uInt8ArrayOf
import org.khronos.webgl.get
import org.w3c.dom.CanvasRenderingContext2D
import org.w3c.dom.HTMLCanvasElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLSelectElement
object WaveformView: Komponent() {
init {
window.requestAnimationFrame(::onAnimationFrame)
}
fun onAnimationFrame(time: Double) {
if (MainView.started) {
VstChipWorklet.postMessage("start_recording")
}
window.requestAnimationFrame(::onAnimationFrame)
}
override fun HtmlBuilder.render() {
div {
if (VstChipWorklet.recording != null) {
canvas {
width = "1000"
height = "400"
val ctx = (currentElement() as? HTMLCanvasElement)?.getContext("2d") as? CanvasRenderingContext2D
val data = VstChipWorklet.recording
if (ctx != null && data != null) {
val width = ctx.canvas.width.toDouble()
val height = ctx.canvas.height.toDouble()
val halfHeight = height / 2.0
ctx.lineWidth = 2.0
ctx.clearRect(0.0, 0.0, width, height)
val step = 1000.0 / data.length
ctx.beginPath()
ctx.strokeStyle = "rgba(0, 255, 255, 0.5)"
ctx.moveTo(0.0, halfHeight)
for (i in 0 until data.length) {
ctx.lineTo(i * step, halfHeight - data[i] * halfHeight)
}
ctx.stroke()
}
}
}
}
}
}
object MainView : Komponent(), CssName {
private var messages: MutableList<String> = ArrayList()
var started = false
init {
css()
}
fun addMessage(message: String) {
messages.add(message)
while (messages.size > 10) {
messages.removeAt(0)
}
requestUpdate()
}
override fun HtmlBuilder.render() {
div(MainDivCss.name) {
if (!started) {
div(StartSplashCss.name) {
div(StartBoxCss.name) {
div(StartButtonCss.name) {
+"START"
onClickFunction = {
VstChipWorklet.create {
started = true
requestUpdate()
WebsocketClient.send("LOAD\n")
}
}
}
}
}
}
h1 {
+"VST Chip"
}
div {
span {
+"Midi input: "
select {
option {
+"None"
value = "none"
}
for (mi in Midi.inputs) {
option {
+mi.name
value = mi.id
selected = mi.id == Midi.currentInput?.id
}
}
onChangeFunction = { event ->
val target = event.target as HTMLSelectElement
if (target.value == "none") {
Midi.setInput(null)
} else {
Midi.setInput(target.value)
}
}
}
}
span {
+"channel:"
input {
type = InputType.number
value = VstChipWorklet.midiChannel.toString()
onInputFunction = { event ->
val target = event.target as HTMLInputElement
println("onInput channel: $target")
VstChipWorklet.midiChannel = target.value.toInt()
}
}
}
}
div {
span(ButtonBarCss.name) {
+"SAVE"
onClickFunction = {
val patch = VstChipWorklet.save().copy(midiId = Midi.currentInput?.id ?: "")
WebsocketClient.send("SAVE\n${JSON.stringify(patch)}")
}
}
span(ButtonBarCss.name) {
+"STOP"
onClickFunction = {
VstChipWorklet.postDirectlyToWorklet(
uInt8ArrayOf(0xb0 + midiChannel, 123, 0)
)
}
}
}
div {
span(ButtonBarCss.name) {
+"Sine"
if (VstChipWorklet.waveform == 0) {
classes += SelectedCss.name
}
onClickFunction = {
VstChipWorklet.waveform = 0
requestUpdate()
}
}
span(ButtonBarCss.name) {
+"Square"
if (VstChipWorklet.waveform == 1) {
classes += SelectedCss.name
}
onClickFunction = {
VstChipWorklet.waveform = 1
requestUpdate()
}
}
span(ButtonBarCss.name) {
+"Triangle"
if (VstChipWorklet.waveform == 2) {
classes += SelectedCss.name
}
onClickFunction = {
VstChipWorklet.waveform = 2
requestUpdate()
}
}
span(ButtonBarCss.name) {
+"Sawtooth"
if (VstChipWorklet.waveform == 3) {
classes += SelectedCss.name
}
onClickFunction = {
VstChipWorklet.waveform = 3
requestUpdate()
}
}
}
div(ControlsCss.name) {
include(
KnobComponent(
value = VstChipWorklet.volume,
label = "Volume",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.volume = value
}
)
include(
KnobComponent(
value = VstChipWorklet.dutyCycle,
label = "Duty cycle",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.dutyCycle = value
}
)
include(
KnobComponent(
value = VstChipWorklet.fmModFreq,
label = "FM Freq",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.fmModFreq = value
}
)
include(
KnobComponent(
value = VstChipWorklet.fmModAmp,
label = "FM Ampl",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.fmModAmp = value
}
)
include(
KnobComponent(
value = VstChipWorklet.amModFreq,
label = "AM Freq",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.amModFreq = value
}
)
include(
KnobComponent(
value = VstChipWorklet.amModAmp,
label = "AM Ampl",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.amModAmp = value
}
)
}
div(ControlsCss.name) {
include(
KnobComponent(
value = VstChipWorklet.attack,
label = "Attack",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.attack = value
}
)
include(
KnobComponent(
value = VstChipWorklet.decay,
label = "Decay",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.decay = value
}
)
include(
KnobComponent(
value = VstChipWorklet.sustain,
label = "Sustain",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.sustain = value
}
)
include(
KnobComponent(
value = VstChipWorklet.release,
label = "Release",
minValue = 0.0,
maxValue = 1.0,
step = 2.0 / 127.0,
width = 100,
height = 120,
) { value ->
VstChipWorklet.release = value
}
)
}
include(WaveformView)
}
}
object MainDivCss : CssName
object ActiveCss : CssName
object ButtonCss : CssName
object ButtonBarCss : CssName
object SelectedCss : CssName
object NoteBarCss : CssName
object StartSplashCss : CssName
object StartBoxCss : CssName
object StartButtonCss : CssName
object ControlsCss : CssName
private fun css() {
defineCss {
select("*") {
select("*:before") {
select("*:after") {
boxSizing(BoxSizing.borderBox)
}
}
}
select("html", "body") {
margin(0.px)
padding(0.px)
height(100.prc)
}
select("html", "body") {
backgroundColor(Css.currentStyle.mainBackgroundColor)
color(Css.currentStyle.mainFontColor)
fontFamily("JetbrainsMono, monospace")
fontSize(14.px)
fontWeight(FontWeight.bold)
//transition()
noTextSelect()
}
select("input", "textarea") {
backgroundColor(Css.currentStyle.inputBackgroundColor)
color(Css.currentStyle.mainFontColor)
border("none")
}
select(cls(ButtonCss)) {
margin(1.rem)
commonButton()
}
select(cls(ButtonBarCss)) {
margin(1.rem, 0.px)
commonButton()
}
select(cls(ActiveCss)) {
//backgroundColor(Css.currentStyle.selectedBackgroundColor)
}
select(cls(NoteBarCss)) {
minHeight(4.rem)
}
select(cls(MainDivCss)) {
margin(1.rem)
}
select("select") {
plain("appearance", "none")
border("0")
outline("0")
width(20.rem)
padding(0.5.rem, 2.rem, 0.5.rem, 0.5.rem)
backgroundImage("url('https://upload.wikimedia.org/wikipedia/commons/9/9d/Caret_down_font_awesome_whitevariation.svg')")
background("right 0.8em center/1.4em")
backgroundColor(Css.currentStyle.inputBackgroundColor)
color(Css.currentStyle.mainFontColor)
borderRadius(0.25.em)
}
select(cls(StartSplashCss)) {
position(Position.fixed)
left(0.px)
top(0.px)
width(100.vw)
height(100.vh)
zIndex(100)
backgroundColor(hsla(32, 0, 5, 0.65))
select(cls(StartBoxCss)) {
position(Position.relative)
left(25.vw)
top(25.vh)
width(50.vw)
height(50.vh)
backgroundColor(hsla(239, 50, 10, 1.0))
borderColor(Css.currentStyle.mainFontColor)
borderWidth(2.px)
select(cls(StartButtonCss)) {
position(Position.absolute)
left(50.prc)
top(50.prc)
transform(Transform("translate(-50%, -50%)"))
padding(1.rem)
backgroundColor(Css.currentStyle.buttonBackgroundColor)
cursor("pointer")
}
}
}
select(ControlsCss.cls()) {
display(Display.flex)
flexDirection(FlexDirection.row)
justifyContent(JustifyContent.flexStart)
alignItems(AlignItems.center)
margin(1.rem)
padding(1.rem)
backgroundColor(Css.currentStyle.mainBackgroundColor)
}
}
}
private fun Style.commonButton() {
display(Display.inlineBlock)
padding(1.rem)
backgroundColor(Css.currentStyle.buttonBackgroundColor)
borderColor(Css.currentStyle.buttonBorderColor)
borderWidth(Css.currentStyle.buttonBorderWidth)
color(Css.currentStyle.mainFontColor)
hover {
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover())
}
and(SelectedCss.cls()) {
backgroundColor(Css.currentStyle.buttonBackgroundColor.hover().hover().hover())
}
}
}