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 = 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()) } } }