Initial commit

This commit is contained in:
2024-06-27 12:29:50 +02:00
parent 6147bfe5db
commit d51c460fe7
10 changed files with 819 additions and 45 deletions

View File

@@ -23,9 +23,14 @@ kotlin {
sourceSets {
val commonMain by getting {
dependencies {
api("nl.astraeus:kotlin-css-generator:1.0.7")
}
}
val jsMain by getting {
dependencies {
implementation("nl.astraeus:kotlin-komponent-js:1.2.2")
}
}
val jsMain by getting
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))

View File

@@ -1,5 +1,5 @@
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
}
rootProject.name = "vst-worklet-base"
rootProject.name = "vst-ui-base"

View File

@@ -0,0 +1,86 @@
package nl.astraeus.vst.util
/**
* User: rnentjes
* Date: 28-3-16
* Time: 14:12
*/
fun formatFloat(value: Float, decimals: Int): String {
val valueString = value.toString()
var result = ""
var foundDot = false
var foundDecimals = 0
for (index in valueString.indices) {
if (foundDot) {
foundDecimals++
}
if (decimals == 0 && valueString[index] == '.') {
return result
}
if (foundDecimals > decimals) {
return result
} else {
result = result + valueString[index]
}
if (!foundDot) {
foundDot = valueString[index] == '.'
}
}
if (!foundDot && decimals > 0) {
result += "."
}
while (decimals > 0 && foundDecimals < decimals) {
result += "0"
foundDecimals++
}
return result
}
fun formatDouble(value: Double, decimals: Int): String {
val valueString = value.toString()
var result = ""
var foundDot = false
var foundDecimals = 0
for (index in valueString.indices) {
if (foundDot) {
foundDecimals++
}
if (decimals == 0 && valueString[index] == '.') {
return result
}
if (foundDecimals > decimals) {
return result
} else {
result = result + valueString[index]
}
if (!foundDot) {
foundDot = valueString[index] == '.'
}
}
if (!foundDot && decimals > 0) {
result += "."
}
while (decimals > 0 && foundDecimals < decimals) {
result += "0"
foundDecimals++
}
return result
}

View File

@@ -0,0 +1,158 @@
package nl.astraeus.vst.util
import kotlinx.html.SVG
import kotlinx.html.unsafe
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
fun SVG.width(width: Int) {
this.attributes["width"] = "$width"
}
fun SVG.height(height: Int) {
this.attributes["height"] = "$height"
}
fun SVG.svgStyle(
name: String,
vararg props: Pair<String, String>
) {
val result = StringBuilder()
result.append("<style>\n")
result.append(name)
result.append(" {\n")
for (prop in props) {
result.append(" ")
result.append(prop.first)
result.append(": ")
result.append(prop.second)
result.append(";\n")
}
result.append("}\n")
result.append("</style>\n")
unsafe {
+ "$result"
}
}
fun SVG.rect(
x: Int,
y: Int,
width: Int,
height: Int,
rx: Int,
cls: String
) {
this.unsafe {
+ """
<rect class="$cls" x="$x" y="$y" width="$width" height="$height" rx="$rx" />
""".trimIndent()
}
}
fun SVG.circle(
x: Int,
y: Int,
radius: Int,
cls: String
) {
this.unsafe {
+ """
<circle class="$cls" cx="$x" cy="$y" r="$radius" />
""".trimIndent()
}
}
fun SVG.control(
pnt: Pair<Int, Int>,
cls: String
) {
circle(pnt.first, pnt.second, 4, cls)
}
private val Pair<Int, Int>.crds: String
get() = "${this.first},${this.second}"
// https://svg-path-visualizer.netlify.app/bezier-curve/
fun SVG.curve(
start: Pair<Int, Int>,
control1: Pair<Int, Int>,
control2: Pair<Int, Int>,
end: Pair<Int, Int>,
smoothEnd: Pair<Int, Int>? = null,
vararg smooth: Pair<Int,Int>,
cls: String
) {
val smoothStr = StringBuilder()
for (crd in smooth) {
smoothStr.append("S ")
smoothStr.append(crd.crds)
smoothStr.append(" ")
}
val smoothEndStr = if (smoothEnd != null) {
smoothEnd.crds
} else {
""
}
this.unsafe {
+ """
<path class="$cls" d="M ${start.crds} C ${control1.crds} ${control2.crds} ${end.crds} $smoothStr $smoothEndStr " />
""".trimIndent()
}
this.control(start, cls = cls)
this.control(control1, cls = cls)
this.control(control2, cls = cls)
this.control(end, cls = cls)
for (sm in smooth) {
this.control(sm, cls = cls)
}
if (smoothEnd != null) {
this.control(smoothEnd, cls = cls)
}
}
/*
+"""<path d="${describeArc(middleX, middleY, radius, START_ANGLE_DEG, middle)}"
fill="none" stroke="${volumeColor.value}" stroke-width="10" />"""
*/
fun SVG.arc(
x: Int,
y: Int,
radius: Int,
startAngle: Int,
endAngle: Int,
cls: String
) {
val start = polarToCartesian(x, y, radius, endAngle)
val end = polarToCartesian(x, y, radius, startAngle)
val largeArcFlag = if (endAngle - startAngle <= 180) {
"0"
} else {
"1"
}
unsafe {
+"""<path class="$cls"
d="M ${start.first} ${start.second}
A $radius $radius 0 $largeArcFlag 0 ${end.first} ${end.second}" />"""
}
}
private fun polarToCartesian(
centerX: Int,
centerY: Int,
radius: Int,
angleInDegrees: Int
): Pair<Double, Double> {
val angleInRadians = (angleInDegrees - 90).toDouble() * PI / 180.0
return Pair(
centerX + (radius * cos(angleInRadians)),
centerY + (radius * sin(angleInRadians))
)
}

View File

@@ -1,43 +0,0 @@
package nl.astraeus.vst
import org.khronos.webgl.Float32Array
import org.w3c.dom.MessagePort
enum class AutomationRate(
val rate: String
) {
A_RATE("a-rate"),
K_RATE("k-rate")
}
interface AudioParam {
var value: Double
var automationRate: AutomationRate
val defaultValue: Double
val minValue: Double
val maxValue: Double
}
interface AudioParamMap {
operator fun get(name: String): AudioParam
}
abstract external class AudioWorkletProcessor {
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AudioWorkletNode/parameters) */
//val parameters: AudioParamMap;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AudioWorkletNode/port) */
@JsName("port")
val port: MessagePort
@JsName("process")
open fun process (
inputs: Array<Array<Float32Array>>,
outputs: Array<Array<Float32Array>>,
parameters: dynamic
) : Boolean { definedExternally }
}
external fun registerProcessor(name: String, processorCtor: JsClass<*>)
external val sampleRate: Int
external val currentTime: Double

View File

@@ -0,0 +1,296 @@
package nl.astraeus.vst.ui.components
import kotlinx.html.classes
import kotlinx.html.js.onMouseDownFunction
import kotlinx.html.js.onMouseUpFunction
import kotlinx.html.js.onMouseWheelFunction
import kotlinx.html.span
import kotlinx.html.style
import kotlinx.html.svg
import nl.astraeus.css.properties.*
import nl.astraeus.css.style.cls
import nl.astraeus.komp.HtmlBuilder
import nl.astraeus.komp.Komponent
import nl.astraeus.vst.ui.css.ActiveCls
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.util.arc
import nl.astraeus.vst.util.height
import nl.astraeus.vst.util.width
import org.w3c.dom.events.MouseEvent
import org.w3c.dom.events.WheelEvent
import kotlin.math.PI
import kotlin.math.max
import kotlin.math.min
/**
* User: rnentjes
* Date: 26-11-17
* Time: 16:52
*/
private const val START_ANGLE_DEG = 230
private const val END_ANGLE_DEG = 130
private const val ANGLE_RANGE_DEG = 260
private const val START_ANGLE = PI * START_ANGLE_DEG / 180.toFloat() - PI / 2
private const val END_ANGLE = PI * END_ANGLE_DEG / 180.toFloat() - PI / 2
private const val ANGLE_RANGE = PI * ANGLE_RANGE_DEG / 180.toFloat()
open class BaseKnobComponent(
val value: Double,
val label: String,
val minValue: Double,
val maxValue: Double,
val step: Double,
val discrete: Boolean,
val width: Int,
val height: Int,
val pixelStep: Double,
val valueToActual: (Double) -> Double,
val actualToValue: (Double) -> Double,
val renderer: (Double) -> String,
val callback: (Double) -> Unit
) : Komponent() {
val actualMinimumValue = valueToActual(minValue)
val actualMaximumValue = valueToActual(maxValue)
var actualValue = valueToActual(value)
var activated = false
var mouseX = 0.0
var mouseY = 0.0
var startValue = 0.0
private fun getMiddleX() = width / 2
private fun getMiddleY() = ((height - 16) / 2) + 16
private fun getRadius() = min(getMiddleX(), getMiddleY() - 16) - 5
override fun HtmlBuilder.render() {
span(KnobCls.name) {
style = "width: ${width}px; height: ${height}px"
svg(KnobSvgCls.name) {
if (activated) {
classes = classes + ActiveCls.name
}
width(width)
height(height)
val middle = (
((ANGLE_RANGE_DEG.toFloat() *
(actualValue - actualMinimumValue)) /
(actualMaximumValue - actualMinimumValue)
+ START_ANGLE_DEG.toFloat()).toInt()
)
val middleX = getMiddleX()
val middleY = getMiddleY()
val radius = getRadius()
if (middle < 360) {
arc(
middleX,
middleY,
radius,
START_ANGLE_DEG,
middle,
KnobVolumeCls.name
)
arc(
middleX,
middleY,
radius,
middle,
360,
KnobVolumeBackgroundCls.name
)
arc(
middleX,
middleY,
radius,
0,
END_ANGLE_DEG,
KnobVolumeBackgroundCls.name
)
} else {
arc(
middleX,
middleY,
radius,
START_ANGLE_DEG,
middle,
KnobVolumeCls.name
)
arc(
middleX,
middleY,
radius,
middle,
END_ANGLE_DEG,
KnobVolumeBackgroundCls.name
)
}
}
span(KnobTextCls.name) {
+label
}
val renderedValue = renderer(actualToValue(actualValue))
span(KnobValueCls.name) {
+renderedValue
}
onMouseWheelFunction = {
if (it is WheelEvent) {
val delta = if (it.deltaY > 0) {
1.0
} else {
-1.0
} //it.deltaY / 250.0
var newValue = actualValue - delta * step
newValue = min(newValue, actualMaximumValue)
newValue = max(newValue, actualMinimumValue)
actualValue = newValue
callback(actualToValue(newValue))
requestUpdate()
it.preventDefault()
}
}
/* onMouseDownFunction = {
if (it is MouseEvent) {
activated = true
mouseX = it.clientX.toDouble()
mouseY = it.clientY.toDouble()
startValue = actualValue
mainView.globalMouseListener = { me ->
if (activated && me.buttons == 1.toShort()) {
setValueByMouseDelta(me, actualMinimumValue, actualMaximumValue, callback)
}
}
requestUpdate()
}
}
onMouseUpFunction = {
if (it is MouseEvent) {
activated = false
mainView.globalMouseListener = null
requestUpdate()
}
}*/
}
}
private fun setValueByMouseDelta(
it: MouseEvent,
minValue: Double = 0.0,
maxValue: Double = 5.0,
callback: (value: Double) -> Unit
) {
val deltaX = it.clientX.toDouble() - mouseX
val deltaY = it.clientY.toDouble() - mouseY
var length = - deltaX + deltaY
if (it.offsetX < mouseX || it.offsetY < mouseY) {
length = -length
}
var value = startValue + length * pixelStep
if (discrete) {
value -= (value % step)
}
value = max(value, minValue)
value = min(value, maxValue)
actualValue = value
callback(actualToValue(value))
requestUpdate()
}
companion object : CssId("knob") {
object KnobCls : CssName
object KnobSvgCls : CssName
object KnobTextCls : CssName
object KnobValueCls : CssName
object KnobBackgroundCls : CssName
object KnobVolumeCls : CssName
object KnobVolumeBackgroundCls : CssName
init {
defineCss {
select(cls(KnobCls)) {
position(Position.relative)
margin(5.px)
and(cls(ActiveCls)) {}
select(cls(KnobSvgCls)) {
plain("stroke", "none")
plain("stroke-opacity", "1.0")
plain("fill", "none")
plain("fill-opacity", "0.0")
position(Position.absolute)
backgroundColor(Color.transparent)
and(cls(ActiveCls)) {
color(Css.currentStyle.mainFontColor)
borderRadius(4.px)
}
}
select(cls(KnobTextCls)) {
position(Position.absolute)
width(100.prc)
textAlign(TextAlign.center)
fontSize(1.0.em)
color(Css.currentStyle.mainFontColor)
}
select(cls(KnobValueCls)) {
position(Position.absolute)
width(100.prc)
top((52).prc)
textAlign(TextAlign.center)
fontSize(0.75.em)
color(Css.currentStyle.mainFontColor)
}
}
select(cls(KnobVolumeCls)) {
plain("fill", Color.transparent)
color(Css.currentStyle.mainFontColor)
plain("stroke-width", "5")
//plain("stroke-dasharray", "4")
plain("fill-opacity", "0.5")
}
select(cls(KnobVolumeBackgroundCls)) {
plain("fill", Color.transparent)
color(Css.currentStyle.mainFontColor)
plain("stroke-width", "5")
//plain("stroke-dasharray", "4")
plain("fill-opacity", "0.5")
}
}
}
}
}

View File

@@ -0,0 +1,37 @@
package nl.astraeus.vst.ui.components
import nl.astraeus.vst.util.formatDouble
import kotlin.math.log10
import kotlin.math.pow
/**
* User: rnentjes
* Date: 26-11-17
* Time: 16:52
*/
class ExpKnobComponent(
value: Double = 1.0,
label: String = "",
minValue: Double = 0.0,
maxValue: Double = 5.0,
step: Double = 0.1,
width: Int = 50,
height: Int = 60,
renderer: (Double) -> String = { nv -> formatDouble(nv, 2) },
callback: (Double) -> Unit = {}
) : BaseKnobComponent(
value,
label,
minValue,
maxValue,
log10(maxValue / (maxValue - step)),
false,
width,
height,
0.005,
{ log10(it) },
{ 10.0.pow(it) },
renderer,
callback
)

View File

@@ -0,0 +1,36 @@
package nl.astraeus.vst.ui.components
import nl.astraeus.vst.util.formatDouble
/**
* User: rnentjes
* Date: 26-11-17
* Time: 16:52
*/
class KnobComponent(
value: Double = 1.0,
label: String = "",
minValue: Double = 0.0,
maxValue: Double = 5.0,
step: Double = 0.1,
pixelStep: Double = step / 25.0,
width: Int = 50,
height: Int = 60,
renderer: (Double) -> String = { nv -> formatDouble(nv, 2) },
callback: (Double) -> Unit = {}
) : BaseKnobComponent(
value,
label,
minValue,
maxValue,
step,
true,
width,
height,
pixelStep,
{ it },
{ it },
renderer,
callback
)

View File

@@ -0,0 +1,121 @@
package nl.astraeus.vst.ui.css
import kotlinx.browser.document
import nl.astraeus.css.properties.*
import nl.astraeus.css.style
import nl.astraeus.css.style.ConditionalStyle
import nl.astraeus.css.style.DescriptionProvider
import nl.astraeus.css.style.Style
class StyleDefinition(
val mainFontColor: Color = hsla(178, 70, 55, 1.0),
val mainBackgroundColor: Color = hsl(239, 50, 10),
//val entryFontColor: Color = hsl(Css.mainFontColorNumber, 70, 55),
val inputBackgroundColor : Color = mainBackgroundColor.lighten(15),
val buttonBackgroundColor : Color = mainBackgroundColor.lighten(15),
val buttonBorderColor : Color = mainFontColor.changeAlpha(0.25),
val buttonBorderWidth : Measurement = 1.px,
)
object NoTextSelectCls : CssName {
override val name = "no-text-select"
}
object SelectedCls : CssName {
override val name = "selected"
}
object ActiveCls : CssName {
override val name = "active"
}
fun Color.hover(): Color = if (Css.currentStyle == Css.darkStyle) {
this.lighten(15)
} else {
this.darken(15)
}
object Css {
var dynamicStyles = mutableMapOf<DescriptionProvider, ConditionalStyle.() -> Unit>()
fun DescriptionProvider.defineCss(conditionalStyle: ConditionalStyle.() -> Unit) {
check(!dynamicStyles.containsKey(this)) {
"CssId with name ${this.description()} already defined!"
}
updateCss(conditionalStyle)
}
private fun DescriptionProvider.updateCss(conditionalStyle: ConditionalStyle.() -> Unit) {
val elementId = this.description()
var dynamicStyleElement = document.getElementById(elementId)
dynamicStyles[this] = conditionalStyle
if (dynamicStyleElement == null) {
dynamicStyleElement = document.createElement("style")
dynamicStyleElement.id = elementId
document.head?.append(dynamicStyleElement)
}
val css = style(conditionalStyle)
dynamicStyleElement.innerHTML = css.generateCss(minified = CssSettings.minified)
}
val darkStyle = StyleDefinition(
)
val lightStyle = StyleDefinition(
mainBackgroundColor = hsl(239+180, 50, 15),
)
var currentStyle: StyleDefinition = darkStyle
fun updateStyle() {
for ((cssId, dynStyle) in dynamicStyles) {
cssId.apply {
updateCss(dynStyle)
}
}
}
fun switchLayout() {
currentStyle = if (currentStyle == darkStyle) {
lightStyle
} else {
darkStyle
}
updateStyle()
}
fun Style.transition() {
transition("all 0.5s ease")
}
fun Style.noTextSelect() {
plain("-webkit-touch-callout", "none")
plain("-webkit-user-select", "none")
plain("-moz-user-select", "none")
plain("-ms-user-select", "none")
userSelect(UserSelect.none)
select("::selection") {
background("none")
}
}
object GenericCss : CssId("generic") {
init {
fun generateStyle(): String {
val css = style {
}
return css.generateCss(minified = CssSettings.minified)
}
}
}
}

View File

@@ -0,0 +1,78 @@
package nl.astraeus.vst.ui.css
import nl.astraeus.css.style.DescriptionProvider
import nl.astraeus.css.style.cls
private val CAPITAL_LETTER = Regex("[A-Z]")
fun String.hyphenize(): String {
var result = replace(CAPITAL_LETTER) {
"-${it.value.lowercase()}"
}
if (result.startsWith('-')) {
result = result.substring(1)
}
return result
}
private var nextCssId = 1
object CssSettings {
var preFix = "css"
var shortId = false
var minified = false
}
private fun nextShortId(): String {
var id = nextCssId++
val result = StringBuilder()
while(id > 0) {
val ch = ((id % 26) + 'a'.code).toChar()
result.append(ch)
id /= 26
}
return result.toString()
}
interface CssName : DescriptionProvider {
val name: String
get() = if (CssSettings.shortId) {
nextShortId()
} else {
"${CssSettings.preFix}-${this::class.simpleName?.hyphenize() ?: this::class}"
}
fun cls() : DescriptionProvider = cls(this)
fun cssName(): String = "${this::class.simpleName?.hyphenize() ?: this::class}"
override fun description() = name
}
open class CssId(name: String) : DescriptionProvider {
val name: String = if (CssSettings.shortId) {
nextShortId()
} else {
"daw-$name-css"
}
override fun description() = name
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is CssId) return false
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
return name.hashCode()
}
}