@file:OptIn(ExperimentalJsExport::class) 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.midi.message.TimedMidiMessage import nl.astraeus.midi.message.getCurrentTime import nl.astraeus.vst.chip.Views 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.ExpKnobComponent 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 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 (Views.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() } } } } } } class MainView : Komponent() { private var messages: MutableList = ArrayList() var started = false init { css() } fun addMessage(message: String) { messages.add(message) while (messages.size > 10) { messages.removeAt(0) } requestUpdate() } override fun renderUpdate() { println("Rendering MainView") super.renderUpdate() } 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 ?: "", midiName = Midi.currentInput?.name ?: "" ) WebsocketClient.send("SAVE\n${JSON.stringify(patch)}") } } span(ButtonBarCss.name) { +"STOP" onClickFunction = { VstChipWorklet.postDirectlyToWorklet( TimedMidiMessage(getCurrentTime(), (0xb0 + midiChannel).toByte(), 123, 0) .data.buffer.data ) } } } 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( ExpKnobComponent( value = VstChipWorklet.volume, label = "Volume", minValue = 0.0, maxValue = 1.0, step = 5.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( ExpKnobComponent( value = VstChipWorklet.fmModFreq, label = "FM Freq", minValue = 0.0, maxValue = 2.0, step = 5.0 / 127.0, width = 100, height = 120, ) { value -> VstChipWorklet.fmModFreq = value } ) include( ExpKnobComponent( value = VstChipWorklet.fmModAmp, label = "FM Ampl", minValue = 0.0, maxValue = 1.0, step = 5.0 / 127.0, width = 100, height = 120, ) { value -> VstChipWorklet.fmModAmp = value } ) include( ExpKnobComponent( value = VstChipWorklet.amModFreq, label = "AM Freq", minValue = 0.0, maxValue = 1.0, step = 5.0 / 127.0, width = 100, height = 120, ) { value -> VstChipWorklet.amModFreq = value } ) include( ExpKnobComponent( value = VstChipWorklet.amModAmp, label = "AM Ampl", minValue = 0.0, maxValue = 1.0, step = 5.0 / 127.0, width = 100, height = 120, ) { value -> VstChipWorklet.amModAmp = value } ) include( ExpKnobComponent( value = VstChipWorklet.feedback, label = "Feedback", minValue = 0.0, maxValue = 1.0, step = 5.0 / 127.0, width = 100, height = 120, ) { value -> VstChipWorklet.feedback = value } ) include( ExpKnobComponent( value = VstChipWorklet.delay, label = "Delay", minValue = 0.0, maxValue = 1.0, step = 5.0 / 127.0, width = 100, height = 120, ) { value -> VstChipWorklet.delay = value } ) include( ExpKnobComponent( value = VstChipWorklet.delayDepth, label = "Delay depth", minValue = 0.0, maxValue = 1.0, step = 5.0 / 127.0, width = 100, height = 120, ) { value -> VstChipWorklet.delayDepth = value } ) } div(ControlsCss.name) { include( ExpKnobComponent( value = VstChipWorklet.attack, label = "Attack", minValue = 0.0, maxValue = 5.0, step = 25.0 / 127.0, width = 100, height = 120, ) { value -> VstChipWorklet.attack = value / 5.0 } ) include( ExpKnobComponent( value = VstChipWorklet.decay, label = "Decay", minValue = 0.0, maxValue = 5.0, step = 25.0 / 127.0, width = 100, height = 120, ) { value -> VstChipWorklet.decay = value / 5.0 } ) 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( ExpKnobComponent( value = VstChipWorklet.release, label = "Release", minValue = 0.0, maxValue = 5.0, step = 25.0 / 127.0, width = 100, height = 120, ) { value -> VstChipWorklet.release = value / 5.0 } ) } include(WaveformView) } } companion object MainViewCss : CssName() { 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()) } } } }