Initial commit
This commit is contained in:
@@ -23,9 +23,14 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
dependencies {
|
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 {
|
val jsTest by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("test-js"))
|
implementation(kotlin("test-js"))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
|
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
|
||||||
}
|
}
|
||||||
rootProject.name = "vst-worklet-base"
|
rootProject.name = "vst-ui-base"
|
||||||
|
|
||||||
|
|||||||
86
src/commonMain/kotlin/nl/astraeus/vst/util/FormatFloat.kt
Normal file
86
src/commonMain/kotlin/nl/astraeus/vst/util/FormatFloat.kt
Normal 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
|
||||||
|
}
|
||||||
158
src/commonMain/kotlin/nl/astraeus/vst/util/SVGFunctions.kt
Normal file
158
src/commonMain/kotlin/nl/astraeus/vst/util/SVGFunctions.kt
Normal 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))
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
)
|
||||||
121
src/jsMain/kotlin/nl/astraeus/vst/ui/css/Css.kt
Normal file
121
src/jsMain/kotlin/nl/astraeus/vst/ui/css/Css.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
78
src/jsMain/kotlin/nl/astraeus/vst/ui/css/CssName.kt
Normal file
78
src/jsMain/kotlin/nl/astraeus/vst/ui/css/CssName.kt
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user