Multiple programs in one openrndr-js-template

In openrndr-template it’s very easy to have multiple programs in one project: we clone that repo, then have multiple Kotlin programs under under src/main/kotlin/. Each main method shows a green clickable triangle in IntelliJ Idea. Click it and the program runs.

But what about openrndr-js-template? With the JS template there is no green triangle next to the main method. The way to run the program is to execute ./gradlew browserDevelopmentRun --continuous or ./gradlew jsRun -t, depending on the version you’re using. Once you execute that command, the build system keeps running and any time you save changes to your program, the web page displaying your OPENRNDR program reloads automatically in the web browser.

What if you would like to have several programs? The obvious approach is to clone the repo again for each new program you want to create. But that feels less convenient than having several programs in a folder.

Easy switching of programs with Kotlin / JS

Here one approach I came up with: change TemplateProgram.kt to be just one line:

fun main() = p1

Then create your first program (p1.kt):

import org.openrndr.application
import org.openrndr.color.ColorRGBa

val p1 = application {
    program {
        extend {
            drawer.clear(ColorRGBa.PINK)
        }
    }
}

and your second program (p2.kt):

import org.openrndr.application
import org.openrndr.color.ColorRGBa

val p2 = application {
    program {
        extend {
            drawer.clear(ColorRGBa.GREEN)
        }
    }
}

Now, to decide which program runs, you only need to change the main method to choose between p1 and p2.

This way you could have as many programs as you want and quickly jump between them.

Alternative solution

An approach proposed by user ephemient in the Kotlin Slack:

I’ve ended up creating separate Gradle subprojects, each depending on the common library. then :foo:jsRun :bar:jsRun etc. work as expected

2 Likes

How to choose which JS program to run from HTML?

I would like to improve this so the user can choose which program runs from the HTML page. There could be a clickable menu or a dropdown, which would reload the page passing the name of the function to run to the Kotlin program.

I figured out I can pass information from HTML to the program like this: in the HTML I add a data attribute:

<canvas id="openrndr-canvas" data-program="p2"></canvas>

In Kotlin I can read it like this:

val program = document.getElementById("openrndr-canvas")!!.getAttribute("data-program")!!

But I don’t know what to do with that string. I tried the following:

    println(program) // p2
    println(eval(program)) // null
    println(window[program]) // null
    println(p2) // kotlin.Unit

But I suspect the eval and window return null because p1 and p2 are not known in the JavaScript side of things. Using @JsExport didn’t help either.

I also don’t know if the dead code removal is actually getting rid of the p1 and p2 functions because they are not called.

If anyone has ideas I’m all ears :slight_smile:

Hey @abe, here’s my attempt at that this morning. I expect there’s a better way of reloading the script in index.html than the way I’ve done it!

1 Like

Nice! :slight_smile: That’s definitely progress :chart_with_upwards_trend:

I have a few ideas to try now… Thank you!

This is what I came up with. I moved all the logic into the main method, which takes care of running whatever program you specify in a GET parameter in the URL, and also creates a clickable menu of programs you can run.

import kotlinx.browser.document
import kotlinx.browser.window
import org.w3c.dom.url.URLSearchParams

// Add your application functions here
val myApps = listOf(
    ::bouncyBubbles,
    ::justGreen,
)

fun main() {
    // Create a map where function names point to the actual functions
    val appMap = myApps.associateBy { it.toString().lines()[1].trim().dropLast(3) }

    // Take the GET argument from the URL specifying which program to run. If missing take the first.
    val currentProgram = URLSearchParams(window.location.search).get("program") ?: appMap.keys.first()

    // Launch the selected program
    appMap[currentProgram]!!.invoke()

    // Create a div with clickable links
    val div = document.createElement("div")
    div.innerHTML = appMap.keys.joinToString(" ") { programName ->
        if (programName != currentProgram)
            """<a href="?program=$programName">$programName</a>"""
        else
            """<span>$programName</span>"""
    }
    // Prepend the div at the top of the HTML
    document.body?.prepend(div)
}

The user needs to update the myApps list, and create functions as in @bluehut 's example (ideally one per file).

fun bouncyBubbles() = application { ... }
fun justGreen() = application { ... }

It’s also pretty close to live coding. When I save my changes to my OPENRNDR Kotlin programs the browser refreshes in ~2 seconds (on my 2014 laptop).

It refreshes twice though. I wish I know why. Workaround: add sourceMaps = false inside commonWebpackConfig in build.gradle.kts. Then it refreshes just once :tada: But source maps are gone, which may be useful for debugging.

1 Like

Next, I’m adding those links as an overlay on top of the visuals (so one can use the full window for visuals), and a source button to view the syntax-highlighted source code of the running program :slight_smile:

2 Likes

Latest iteration:

I will post the actual code online :slight_smile:

1 Like

And here the live demo that gets built automatically by a workflow on GitHub any time I commit changes :slight_smile: :tada:

ps. I really wanted to have

val myApps = listOf(
    ::bouncyBubbles,
    ::justGreen,
    ::fabulousPink,
)

instead of

val myApps = mapOf(
    "bouncyBubbles" to ::bouncyBubbles,
    "justGreen" to ::justGreen,
    "fabulousPink" to ::fabulousPink,
)

because repeating words is a potential source of typos. I did have a working version looking like the first solution, but when doing a production release, the system renames variables and methods to one-character names, and the whole thing falls apart because function names are no longer found.

Even better would be not needing that list/map at all, so the user could create programs, and they would be somehow detected.

Maybe doable with Gradle, but auto-generating source code files sounds a bit too magical :magic_wand: , even to me XD. Another option would be to add tweak openrndr-js to enable multi-app usage…

Lets see :slight_smile:

1 Like

I couldn’t resist and wrote a Gradle task that generates that list of a apps :slight_smile:

Since it only runs when request by the user, I think this should be ok.

Any time the user adds an app they should double click on the task to update the list. I’ll use it and see how it goes.

1 Like