3 Commits

Author SHA1 Message Date
b7449c86fd Kotlin v. 2.0.0 2024-06-10 21:32:31 +02:00
223ff91dde Fixes 2024-06-10 21:20:57 +02:00
6f03f71b15 Communication iframe -> audio worklet etc. 2023-09-10 15:35:46 +02:00
13 changed files with 571 additions and 278 deletions

View File

@@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType.IR
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput.Target.VAR
plugins {
kotlin("multiplatform") version "1.9.0"
kotlin("multiplatform") version "2.0.0"
application
}
@@ -38,6 +38,15 @@ kotlin {
binaries.executable()
browser()
}
js("jsWorklet", jsMode) {
binaries.executable()
browser {
commonWebpackConfig {
outputFileName = "html-worklet.js"
}
}
}
js("jsAudioWorklet", jsMode) {
binaries.executable()
@@ -72,9 +81,10 @@ application {
}
tasks.named<Copy>("jvmProcessResources") {
val jsBrowserDistribution = tasks.named("jsBrowserDevelopmentWebpack")
val jsBrowserDistribution = tasks.named("jsBrowserDistribution")
from(jsBrowserDistribution)
from(tasks.named("jsAudioWorkletBrowserDistribution"))
from(tasks.named("jsWorkletBrowserDistribution"))
}
tasks.named<JavaExec>("run") {

View File

@@ -1,165 +0,0 @@
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
/**
* User: rnentjes
* Date: 14-11-15
* Time: 11:50
*/
enum class Note(
val description: String
) {
C0("C-0"),
C0s("C#0"),
D0("D-0"),
D0s("D#0"),
E0("E-0"),
F0("F-0"),
F0s("F#0"),
G0("G-0"),
G0s("G#0"),
A0("A-0"),
A0s("A#0"),
B0("B-0"),
C1("C-1"),
C1s("C#1"),
D1("D-1"),
D1s("D#1"),
E1("E-1"),
F1("F-1"),
F1s("F#1"),
G1("G-1"),
G1s("G#1"),
A1("A-1"),
A1s("A#1"),
B1("B-1"),
C2("C-2"),
C2s("C#2"),
D2("D-2"),
D2s("D#2"),
E2("E-2"),
F2("F-2"),
F2s("F#2"),
G2("G-2"),
G2s("G#2"),
A2("A-2"),
A2s("A#2"),
B2("B-2"),
C3("C-3"),
C3s("C#3"),
D3("D-3"),
D3s("D#3"),
E3("E-3"),
F3("F-3"),
F3s("F#3"),
G3("G-3"),
G3s("G#3"),
A3("A-3"),
A3s("A#3"),
B3("B-3"),
C4("C-4"),
C4s("C#4"),
D4("D-4"),
D4s("D#4"),
E4("E-4"),
F4("F-4"),
F4s("F#4"),
G4("G-4"),
G4s("G#4"),
A4("A-4"),
A4s("A#4"),
B4("B-4"),
C5("C-5"),
C5s("C#5"),
D5("D-5"),
D5s("D#5"),
E5("E-5"),
F5("F-5"),
F5s("F#5"),
G5("G-5"),
G5s("G#5"),
A5("A-5"),
A5s("A#5"),
B5("B-5"),
C6("C-6"),
C6s("C#6"),
D6("D-6"),
D6s("D#6"),
E6("E-6"),
F6("F-6"),
F6s("F#6"),
G6("G-6"),
G6s("G#6"),
A6("A-6"),
A6s("A#6"),
B6("B-6"),
C7("C-7"),
C7s("C#7"),
D7("D-7"),
D7s("D#7"),
E7("E-7"),
F7("F-7"),
F7s("F#7"),
G7("G-7"),
G7s("G#7"),
A7("A-7"),
A7s("A#7"),
B7("B-7"),
C8("C-8"),
C8s("C#8"),
D8("D-8"),
D8s("D#8"),
E8("E-8"),
F8("F-8"),
F8s("F#8"),
G8("G-8"),
G8s("G#8"),
A8("A-8"),
A8s("A#8"),
B8("B-8"),
NONE("---"),
END("XXX"),
UP("^^^"),
;
val freq: Double by lazy {
val ordinal = ordinal
val relNote = ordinal - A4.ordinal
440.0 * 2.0.pow(relNote/12.0)
}
val cycleLength: Double by lazy { 1.0 / freq }
val sampleDelta: Double by lazy { (1.0 / sampleRate.toDouble()) / cycleLength }
fun transpose(semiNotes: Int): Note = if (ordinal >= C0.ordinal && ordinal <= B8.ordinal) {
var result = this.ordinal + semiNotes
result = min(result, B8.ordinal)
result = max(result, C0.ordinal)
values().firstOrNull { it.ordinal == result } ?: this
} else {
this
}
companion object {
var sampleRate: Int = 44100
}
/*
* Amount of one cycle to advance per sample
*/
/*
fun sampleDelta(): Double {
// 44100
val sampleRate = sampleRate
val time = 1f / sampleRate.toDouble()
return time / cycleLength
}
*/
}

View File

@@ -0,0 +1,179 @@
package common
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
/**
* User: rnentjes
* Date: 14-11-15
* Time: 11:50
*/
var sampleRate = 44100
set(value) {
field = value
for (note in Note.values()) {
note.sampleDelta = (1.0 / sampleRate.toDouble()) / note.cycleLength
}
}
//var samplesPerEntry = 5512
enum class Note(
val sharp: String,
val flat: String
) {
NONE("---", "---"),
NO02("",""),
NO03("",""),
NO04("",""),
NO05("",""),
NO06("",""),
NO07("",""),
NO08("",""),
NO09("",""),
NO10("",""),
NO11("",""),
NO12("",""),
C0("C-0","C-0"),
C0s("C#0","Db0"),
D0("D-0","D-0"),
D0s("D#0","Eb0"),
E0("E-0","E-0"),
F0("F-0","F-0"),
F0s("F#0","Gb0"),
G0("G-0","G-0"),
G0s("G#0","Ab0"),
A0("A-0","A-0"),
A0s("A#0","Bb0"),
B0("B-0","B-0"),
C1("C-1","C-1"),
C1s("C#1","Db1"),
D1("D-1","D-1"),
D1s("D#1","Eb1"),
E1("E-1","E-1"),
F1("F-1","F-1"),
F1s("F#1","Gb1"),
G1("G-1","G-1"),
G1s("G#1","Ab1"),
A1("A-1","A-1"),
A1s("A#1","Bb1"),
B1("B-1","B-1"),
C2("C-2","C-2"),
C2s("C#2","Db2"),
D2("D-2","D-2"),
D2s("D#2","Eb2"),
E2("E-2","E-2"),
F2("F-2","F-2"),
F2s("F#2","Gb2"),
G2("G-2","G-2"),
G2s("G#2","Ab2"),
A2("A-2","A-2"),
A2s("A#2","Bb2"),
B2("B-2","B-2"),
C3("C-3","C-3"),
C3s("C#3","Db3"),
D3("D-3","D-3"),
D3s("D#3","Eb3"),
E3("E-3","E-3"),
F3("F-3","F-3"),
F3s("F#3","Gb3"),
G3("G-3","G-3"),
G3s("G#3","Ab3"),
A3("A-3","A-3"),
A3s("A#3","Bb3"),
B3("B-3","B-3"),
C4("C-4","C-4"),
C4s("C#4","Db4"),
D4("D-4","D-4"),
D4s("D#4","Eb4"),
E4("E-4","E-4"),
F4("F-4","F-4"),
F4s("F#4","Gb4"),
G4("G-4","G-4"),
G4s("G#4","Ab4"),
A4("A-4","A-4"),
A4s("A#4","Bb4"),
B4("B-4","B-4"),
C5("C-5","C-5"),
C5s("C#5","Db5"),
D5("D-5","D-5"),
D5s("D#5","Eb5"),
E5("E-5","E-5"),
F5("F-5","F-5"),
F5s("F#5","Gb5"),
G5("G-5","G-5"),
G5s("G#5","Ab5"),
A5("A-5","A-5"),
A5s("A#5","Bb5"),
B5("B-5","B-5"),
C6("C-6","C-6"),
C6s("C#6","Db6"),
D6("D-6","D-6"),
D6s("D#6","Eb6"),
E6("E-6","E-6"),
F6("F-6","F-6"),
F6s("F#6","Gb6"),
G6("G-6","G-6"),
G6s("G#6","Ab6"),
A6("A-6","A-6"),
A6s("A#6","Bb6"),
B6("B-6","B-6"),
C7("C-7","C-7"),
C7s("C#7","Db7"),
D7("D-7","D-7"),
D7s("D#7","Eb7"),
E7("E-7","E-7"),
F7("F-7","F-7"),
F7s("F#7","Gb7"),
G7("G-7","G-7"),
G7s("G#7","Ab7"),
A7("A-7","A-7"),
A7s("A#7","Bb7"),
B7("B-7","B-7"),
C8("C-8","C-8"),
C8s("C#8","Db8"),
D8("D-8","D-8"),
D8s("D#8","Eb8"),
E8("E-8","E-8"),
F8("F-8","F-8"),
F8s("F#8","Gb8"),
G8("G-8","G-8"),
G8s("G#8","Ab8"),
A8("A-8","A-8"),
A8s("A#8","Bb8"),
B8("B-8","B-8"),
C9("C-9","C-9"),
C9s("C#9","Db9"),
D9("D-9","D-9"),
D9s("D#9","Eb9"),
E9("E-9","E-9"),
F9("F-9","F-9"),
F9s("F#9","Gb9"),
G9("G-9","G-9"),
// out of midi range
//G9s("G#9","Ab9"),
//A9("A-9","A-9"),
//A9s("A#9","Bb9"),
//B9("B-9","B-9"),
UP("^^^","^^^"),
END("XXX","XXX"),
;
// 69 = A4.ordinal
val freq: Double = 440.0 * 2.0.pow((ordinal - 69)/12.0)
val cycleLength: Double = 1.0 / freq
var sampleDelta: Double = (1.0 / sampleRate.toDouble()) / cycleLength
fun transpose(semiNotes: Int): Note = if (ordinal >= C0.ordinal && ordinal <= G9.ordinal) {
var result = this.ordinal + semiNotes
result = min(result, G9.ordinal)
result = max(result, C0.ordinal)
values().firstOrNull { it.ordinal == result } ?: this
} else {
this
}
}

View File

@@ -1,38 +1,49 @@
import common.Note
import org.khronos.webgl.Float32Array
import org.khronos.webgl.set
import org.w3c.dom.MessageEvent
import org.w3c.dom.MessagePort
import kotlin.math.PI
import kotlin.math.sin
const val PI2 = PI * 2
class NoteState(
note: Note,
counter: Int
)
@ExperimentalJsExport
@JsExport
class AudioProcessor : AudioWorkletProcessor() {
private var started = true
private var counter: Int = 0
private var note = Note.C2
private var note: Note? = null
private var offset = 0.0
private var note_length = 2500
private var harmonics = 3
private var transpose = 0
private var workletPort: MessagePort? = null
init {
this.port.onmessage = ::handleMessage
common.sampleRate = sampleRate
}
private fun handleMessage(message: MessageEvent) {
console.log("WorkletProcessor: Received message", message)
console.log("AudioProcessor: Received message", message)
val data = message.data
val data: Any? = message.data
if (data is String) {
val parts = data.split("\n")
when (parts[0]) {
"start" -> {
println("Start worklet!")
Note.sampleRate = sampleRate
common.sampleRate = sampleRate
started = true
}
"stop" -> {
@@ -49,9 +60,24 @@ class AudioProcessor : AudioWorkletProcessor() {
"transpose" -> {
transpose = parts[1].toInt()
}
"play" -> {
note = Note.valueOf(parts[1])
counter=1
offset = 0.0
}
else ->
console.error("Don't kow how to handle message", message)
}
} else {
val dynData: dynamic = message.data
if (dynData.command == "audio-iframe-message-port") {
workletPort = dynData.port
workletPort?.onmessage = {
console.log("AudioProcessor: Received message from iframe", it)
handleMessage(it)
}
}
}
}
@@ -61,9 +87,6 @@ class AudioProcessor : AudioWorkletProcessor() {
parameters: dynamic
) : Boolean {
if (started) {
//console.log("process called", inputs, outputs, parameters, port)
//console.log("sample rate", sampleRate)//console.log("WorkletProcessor: process", samples, left, right)
check(outputs.size == 1) {
"Expected 1 output got ${outputs.size}"
}
@@ -71,37 +94,33 @@ class AudioProcessor : AudioWorkletProcessor() {
"Expected 2 output channels, got ${outputs.size}"
}
var delta = note.sampleDelta
val samples = outputs[0][0].length
val left = outputs[0][0]
val right = outputs[0][1]
note?.also { activeNote ->
val delta = activeNote.sampleDelta
val samples = outputs[0][0].length
val left = outputs[0][0]
val right = outputs[0][1]
//console.log("left/right", left, right)
for (sample in 0..<samples) {
var value = sin(offset * PI2)
for (sample in 0..<samples) {
var value = sin(offset * PI2)
for(index in 0..<harmonics) {
value += sin(offset * (index + 2) * PI2) * (3.0 / (index+2))
}
offset += delta
// new note every NOTE_LENGTH samples
val noteProgress = counter % note_length
if (noteProgress == 0) {
note = note.transpose(1)
if (note.ordinal >= Note.C7.transpose(transpose).ordinal) {
note = Note.C2.transpose(transpose)
for (index in 0..<harmonics) {
value += sin(offset * (index + 2) * PI2) * (3.0 / (index + 2))
}
delta = note.sampleDelta
offset += delta
// new note every NOTE_LENGTH samples
val noteProgress = counter % note_length
// simple envelop from max to 0 every note
value *= (1.0 - noteProgress / note_length.toDouble())
left[sample] = value.toFloat()
right[sample] = value.toFloat()
if (counter >= note_length) {
note = null
}
counter++
}
// simple envelop from max to 0 every note
value *= (1.0 - noteProgress / note_length.toDouble())
left[sample] = value.toFloat()
right[sample] = value.toFloat()
counter++
}
}

View File

@@ -1,6 +1,26 @@
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;
@@ -19,3 +39,4 @@ abstract external class AudioWorkletProcessor {
external fun registerProcessor(name: String, processorCtor: JsClass<*>)
external val sampleRate: Int
external val currentTime: Double

View File

@@ -3,18 +3,19 @@ import org.khronos.webgl.get
import org.khronos.webgl.set
import org.w3c.dom.MessageEvent
@ExperimentalJsExport
@JsExport
class MixerProcessor : AudioWorkletProcessor() {
var started = false
val leftBuffer = Float32Array(sampleRate * 2)
val rightBuffer = Float32Array(sampleRate * 2)
init {
this.port.onmessage = ::handleMessage
}
private fun handleMessage(message: MessageEvent) {
console.log("WorkletProcessor: Received message", message)
console.log("MixerProcessor: Received message", message)
val data = message.data
if (data is String) {

View File

@@ -1,7 +1,9 @@
package nl.astraeus
import common.Note
import nl.astraeus.handler.AudioNode
import org.w3c.dom.MessageEvent
import org.w3c.dom.MessagePort
class AudioProcessorNode(
audioModule: dynamic,
@@ -52,4 +54,18 @@ class AudioProcessorNode(
fun length(i: Int) {
node?.port.postMessage("set_note_length\n$i")
}
}
fun play(note: Note) {
node?.port.postMessage("play\n$note")
}
fun setWorkletPort(port2: MessagePort) {
node?.port.postMessage(
createWorkletSetPortMessage(
"audio-iframe-message-port",
port2
),
arrayOf(port2)
)
}
}

View File

@@ -1,15 +1,52 @@
package nl.astraeus
import common.Note
import kotlinx.browser.document
import kotlinx.browser.window
import nl.astraeus.handler.AudioModule
import org.w3c.dom.HTMLIFrameElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.MessageChannel
import org.w3c.dom.MessagePort
fun createWorkletSetPortMessage(
command: String,
port: MessagePort
): dynamic {
val result = js("{}")
result.command = command
result.port = port
return result
}
fun main() {
val audioModule = AudioModule("static/audio-worklet.js")
val mixer = MixerProcessorNode(audioModule)
var node1: AudioProcessorNode? = null
var node2: AudioProcessorNode? = null
val iframeWorkletChannel = MessageChannel()
window.addEventListener("message", { event ->
console.log("Main Received message: ", event)
}, "")
//window.postMessage("Hello from Main 1", window.location.origin + "/worklet.html")
document.getElementById("iframe")?.also {
val iframe = it as? HTMLIFrameElement
iframe?.also {
it.contentWindow?.postMessage("Hello from Main 2", window.location.origin + "/worklet.html")
it.contentWindow?.postMessage(
createWorkletSetPortMessage(
"audio-processor-message-port",
iframeWorkletChannel.port1
),
window.location.origin + "/worklet.html",
arrayOf(iframeWorkletChannel.port1)
)
}
}
document.getElementById("createButton")?.also {
it.addEventListener("click", {
@@ -19,6 +56,7 @@ fun main() {
node1?.create {
println("node 1 created")
node1?.setWorkletPort(iframeWorkletChannel.port2)
}
node2?.create {
println("node 2 created")
@@ -50,14 +88,44 @@ fun main() {
}
}, "")
}
document.getElementById("harmonics")?.also {
it.addEventListener("change", {
val target = it.target
if (target is HTMLInputElement) {
node1?.harmonic(target.value.toInt())
node2?.harmonic(target.value.toInt())
}
}, "")
}
document.getElementById("note_c3_1")?.also {
it.addEventListener("click", {
node1?.play(Note.C3)
}, "")
}
document.getElementById("note_e3_1")?.also {
it.addEventListener("click", {
node1?.play(Note.E3)
}, "")
}
document.getElementById("note_g3_1")?.also {
it.addEventListener("click", {
node1?.play(Note.G3)
}, "")
}
document.getElementById("note_c3_2")?.also {
it.addEventListener("click", {
node2?.play(Note.C3)
}, "")
}
document.getElementById("note_e3_2")?.also {
it.addEventListener("click", {
node2?.play(Note.E3)
}, "")
}
document.getElementById("note_g3_2")?.also {
it.addEventListener("click", {
node2?.play(Note.G3)
}, "")
}
}

View File

@@ -3,14 +3,6 @@ package nl.astraeus.handler
import org.w3c.dom.MessageEvent
import org.w3c.dom.MessagePort
private val audioModules = mutableMapOf<String, AudioModule>()
fun loadAudioModule(jsFile: String) {
val module = audioModules.getOrPut(jsFile) {
AudioModule(jsFile)
}
}
enum class ModuleStatus {
INIT,
LOADING,
@@ -77,7 +69,7 @@ abstract class AudioNode(
node.connect(module.audioContext.destination)
} else {
node.connect(destination, outputIndex, inputIndex)
}
}
node.port.onmessage = ::onMessage
@@ -88,5 +80,4 @@ abstract class AudioNode(
done(node)
}
}
}
}

View File

@@ -0,0 +1,43 @@
package nl.astraeus.worklet
import kotlinx.browser.document
import kotlinx.browser.window
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.MessageEvent
import org.w3c.dom.MessagePort
fun main() {
var port: MessagePort? = null
console.log("Worklet", document.location)
window.addEventListener("message", { event ->
console.log("Worklet xxx Received message: ", event)
if (event is MessageEvent) {
val data: dynamic = event.data
console.log("WL DATA: ", data, data?.command == "audio-processor-message-port")
if (data?.command == "audio-processor-message-port") {
port = data.port
port?.onmessage = {
console.log("Worklet Received message from audio worklet: ", it)
}
}
}
}, "")
window.parent.postMessage(
"Hello from Worklet",
window.location.origin + "/index.html"
)
document.getElementById("harmonics")?.also {
it.addEventListener("change", {
val target = it.target
if (target is HTMLInputElement) {
console.log("Sending: ", target.value, port)
port?.postMessage("harmonics\n${target.value}")
}
}, "")
}
}

View File

@@ -0,0 +1,125 @@
import kotlinx.html.HTML
import kotlinx.html.InputType
import kotlinx.html.body
import kotlinx.html.div
import kotlinx.html.h3
import kotlinx.html.head
import kotlinx.html.id
import kotlinx.html.iframe
import kotlinx.html.input
import kotlinx.html.label
import kotlinx.html.link
import kotlinx.html.script
import kotlinx.html.span
import kotlinx.html.style
import kotlinx.html.title
fun HTML.index() {
head {
title("Hello from Ktor!")
link("static/worklet.css", "stylesheet" ,"text/css")
}
body {
div {
+"We need a button to start because we can only start audio from a user event:"
}
div("button_div") {
span("button") {
id = "createButton"
+"Create"
}
span("button") {
id = "startButton"
+"Start"
}
span("button") {
id = "stopButton"
+"Stop"
}
}
div {
+ "An example of how to interact with the audioworklet:"
}
div {
label {
htmlFor = "noteLength"
+"Note length (in samples):"
}
input {
id = "noteLength"
type = InputType.number
value = "2500"
min = "1"
max = "100000"
step = "100"
}
}
div {
label {
htmlFor = "harmonics"
+"Number of harmonics:"
}
input {
id = "harmonics"
type = InputType.number
value = "3"
min = "0"
max = "10"
}
}
h3 {
+ "Node 1"
}
div {
input {
id = "note_c3_1"
type = InputType.button
value = "C3"
}
input {
id = "note_e3_1"
type = InputType.button
value = "E3"
}
input {
id = "note_g3_1"
type = InputType.button
value = "G3"
}
}
h3 {
+ "Node 2"
}
div {
input {
id = "note_c3_2"
type = InputType.button
value = "C3"
}
input {
id = "note_e3_2"
type = InputType.button
value = "E3"
}
input {
id = "note_g3_2"
type = InputType.button
value = "G3"
}
}
iframe {
id = "iframe"
src = "/worklet.html"
style = "width: 100%; height: 500px; background-color: #ddf;"
}
script(src = "/static/kotlin-audioworklet.js") {}
}
}

View File

@@ -4,77 +4,22 @@ import io.ktor.server.engine.embeddedServer
import io.ktor.server.html.*
import io.ktor.server.http.content.*
import io.ktor.server.netty.Netty
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.html.*
fun HTML.index() {
head {
title("Hello from Ktor!")
link("static/worklet.css", "stylesheet" ,"text/css")
}
body {
div {
+"We need a button to start because we can only start audio from a user event:"
}
div("button_div") {
span("button") {
id = "createButton"
+"Create"
}
span("button") {
id = "startButton"
+"Start"
}
span("button") {
id = "stopButton"
+"Stop"
}
}
div {
+ "An example of how to interact with the audioworklet:"
}
div {
label {
htmlFor = "noteLength"
+"Note length (in samples):"
}
input {
id = "noteLength"
type = InputType.number
value = "2500"
min = "1"
max = "100000"
step = "100"
}
}
div {
label {
htmlFor = "harmonics"
+"Number of harmonics:"
}
input {
id = "harmonics"
type = InputType.number
value = "3"
min = "1"
max = "10"
}
}
script(src = "/static/kotlin-audioworklet.js") {}
}
}
fun main() {
embeddedServer(Netty, port = 8080, host = "127.0.0.1") {
routing {
get("/") {
call.respondRedirect("/index.html")
}
get("/index.html") {
call.respondHtml(HttpStatusCode.OK, HTML::index)
}
get("/worklet.html") {
call.respondHtml(HttpStatusCode.OK, HTML::worklet)
}
static("/static") {
resources()
}

View File

@@ -0,0 +1,40 @@
import kotlinx.html.HTML
import kotlinx.html.InputType
import kotlinx.html.body
import kotlinx.html.div
import kotlinx.html.head
import kotlinx.html.id
import kotlinx.html.iframe
import kotlinx.html.input
import kotlinx.html.label
import kotlinx.html.link
import kotlinx.html.script
import kotlinx.html.span
import kotlinx.html.title
fun HTML.worklet() {
head {
title("Hello from Ktor!")
link("static/worklet.css", "stylesheet" ,"text/css")
}
body {
div {
+"IFrame js set worklet parameters:"
}
div {
label {
htmlFor = "harmonics"
+"Number of harmonics:"
}
input {
id = "harmonics"
type = InputType.number
value = "3"
min = "0"
max = "10"
}
}
script(src = "/static/html-worklet.js") {}
}
}