diff --git a/README.md b/README.md index 2c2ed7be..07e05448 100644 --- a/README.md +++ b/README.md @@ -5,19 +5,24 @@ Just some Processing sketches. Source code for visuals we use at [Soul Ex Machin ![](demo-gif.gif) -The project is divided into 3 modules, `:core` module cointains the core stuff like audio processing, tools, remote control handlers, extensions, etc. Then there are two application modules - the `:playground` and `:visuals` module. +The project is divided into multiple modules. -The `:playground` module serves as, well... playground. You can quickly create a new sketch and play around. I'm using the [Koin](https://insert-koin.io/) DI framework, so you can inject here whatever is defined in the `CoreModule`. Have a look around. +The `:core` module contains the core stuff like audio processing, tools, remote control handlers, extensions, etc. + +The `:playground` module serves as, well... playground. Used to quickly create a new sketch and play around. I'm using the [Koin](https://insert-koin.io/) DI framework, so you can inject here whatever is defined in the `CoreModule`. Have a look around. The `:visuals` module is meant to be used in live environment at the parties. There is an abstraction layer in form of `Mixer` and `Layer`s, which allows me to blend multiple scenes together. Also, have a look around, proceed at your own risk, ignore `legacy` package 😅 (I like to change things, API is generally unstable). +The `:raspberrypi` module contains standalone RPi application that can be distributed using the [Application Gradle plugin](https://docs.gradle.org/current/userguide/application_plugin.html). + ## How to build This project depends on local [Processing 4](https://processing.org) installation, so go ahead and install it if you haven't already. Then create a `local.properties` file in project's root directory and configure the core library and contributed libraries' paths: ``` -processing.core.jars=/path/to/your/processing/libraries/dir -processing.core.natives=/path/to/your/processing/libraries/dir/ +processing.core.jars=/path/to/core/processing/libraries +processing.core.natives=/path/to/core/processing/libraries/ +processing.core.natives.rpi=/path/to/core/processing/libraries/ processing.libs.jars=/path/to/core/processing/libraries ``` @@ -26,8 +31,11 @@ On macOS it might look like this: ``` processing.core.jars=/Applications/Processing.app/Contents/Java/core/library processing.core.natives=/Applications/Processing.app/Contents/Java/core/library/macos-x86_64 +processing.core.natives.rpi=/Applications/Processing.app/Contents/Java/core/library/linux-aarch64 processing.libs.jars=/Users/matsem/Documents/Processing/libraries ``` +Note the difference between `processing.core.natives` and `processing.core.natives.rpi`. +The Raspberry Pi libs have to be configured if you wish to use the `:raspberrypi` module. The Gradle buildscript will look for Processing dependencies at these two paths. Dependencies are defined in CommonDependencies gradle plugin. Open it up, and you can notice that this project depends on some 3rd party libraries, which need to be installed at `processing.libs.jars` path. Open your Processing library manager (Sketch > Import Library > Add library) and install whatever libraries are specified in the `build.gradle` file. @@ -49,6 +57,11 @@ val processingLibs = listOf( ) ``` + +### :raspberrypi module +The Raspberry Pi app can be installed using `./gradlew raspberrypi:installDist` task and zipped using `./gradlew raspberrypi:distZip` task. +See the [Application Plugin](https://docs.gradle.org/current/userguide/application_plugin.html) docs for more info. + ## How to run You can run the project with Gradle `run` task. Be sure to include the `--sketch-path` argument so sketches can properly resolve the data folder containing resources needed by some Sketches. diff --git a/buildSrc/src/main/kotlin/ProjectSettings.kt b/buildSrc/src/main/kotlin/ProjectSettings.kt index c10358eb..651f0883 100644 --- a/buildSrc/src/main/kotlin/ProjectSettings.kt +++ b/buildSrc/src/main/kotlin/ProjectSettings.kt @@ -1,4 +1,4 @@ object ProjectSettings { - const val version = "2.1.0" + const val version = "2.2.0" const val group = "dev.matsem" } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/dev/matsem/astral/CommonDependencies.kt b/buildSrc/src/main/kotlin/dev/matsem/astral/CommonDependencies.kt index b13e7e15..8e86f85e 100644 --- a/buildSrc/src/main/kotlin/dev/matsem/astral/CommonDependencies.kt +++ b/buildSrc/src/main/kotlin/dev/matsem/astral/CommonDependencies.kt @@ -41,7 +41,7 @@ internal fun Project.configureCommonDependencies() { fileTree( mapOf( "dir" to "$processingLibsDir/$libName/library", - "include" to listOf("*.jar") + "include" to listOf("*.jar", "shader/*.glsl") ) ) ) diff --git a/data/images/semlogo_bottom.svg b/data/images/semlogo_bottom.svg new file mode 100644 index 00000000..b82efc39 --- /dev/null +++ b/data/images/semlogo_bottom.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/data/images/semlogo_top.svg b/data/images/semlogo_top.svg new file mode 100644 index 00000000..ebf13435 --- /dev/null +++ b/data/images/semlogo_top.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/raspberrypi/.gitignore b/raspberrypi/.gitignore new file mode 100644 index 00000000..f46534a2 --- /dev/null +++ b/raspberrypi/.gitignore @@ -0,0 +1,3 @@ +build/ +out/ +*.log \ No newline at end of file diff --git a/raspberrypi/build.gradle.kts b/raspberrypi/build.gradle.kts new file mode 100644 index 00000000..df53c61c --- /dev/null +++ b/raspberrypi/build.gradle.kts @@ -0,0 +1,50 @@ +import java.util.* + +plugins { + kotlin("jvm") + application +} + +apply() + +application { + val props = Properties().apply { + load(file("${rootDir}/local.properties").inputStream()) + } + val nativesDir = props["processing.core.natives.rpi"] + + mainClass.set("dev.matsem.astral.raspberrypi.RaspberryApp") + applicationDefaultJvmArgs = listOf( + "-Djava.library.path=$nativesDir" + ) + applicationName = "visuals" +} + +repositories { + mavenCentral() + jcenter() +} + +dependencies { + implementation(project(":core")) +} + +group = ProjectSettings.group +version = ProjectSettings.version + +tasks { + compileKotlin { + kotlinOptions.jvmTarget = "11" + } + compileTestKotlin { + kotlinOptions.jvmTarget = "11" + } +} + +tasks.getByName("distZip") { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +tasks.getByName("installDist") { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} \ No newline at end of file diff --git a/raspberrypi/src/dist/fonts/JetBrainsMono-Bold.ttf b/raspberrypi/src/dist/fonts/JetBrainsMono-Bold.ttf new file mode 100644 index 00000000..c2075fc3 Binary files /dev/null and b/raspberrypi/src/dist/fonts/JetBrainsMono-Bold.ttf differ diff --git a/raspberrypi/src/dist/images/semlogo_bottom.svg b/raspberrypi/src/dist/images/semlogo_bottom.svg new file mode 100644 index 00000000..b82efc39 --- /dev/null +++ b/raspberrypi/src/dist/images/semlogo_bottom.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/raspberrypi/src/dist/images/semlogo_top.svg b/raspberrypi/src/dist/images/semlogo_top.svg new file mode 100644 index 00000000..ebf13435 --- /dev/null +++ b/raspberrypi/src/dist/images/semlogo_top.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/raspberrypi/src/dist/setup.sh b/raspberrypi/src/dist/setup.sh new file mode 100755 index 00000000..30fac8e8 --- /dev/null +++ b/raspberrypi/src/dist/setup.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +mkdir lib/shader +mv lib/*.glsl lib/shader/ +mv images/ bin/images +mv fonts/ bin/fonts \ No newline at end of file diff --git a/raspberrypi/src/main/kotlin/dev/matsem/astral/raspberrypi/RaspberryApp.kt b/raspberrypi/src/main/kotlin/dev/matsem/astral/raspberrypi/RaspberryApp.kt new file mode 100644 index 00000000..5c01482b --- /dev/null +++ b/raspberrypi/src/main/kotlin/dev/matsem/astral/raspberrypi/RaspberryApp.kt @@ -0,0 +1,35 @@ +package dev.matsem.astral.raspberrypi + +import dev.matsem.astral.core.di.coreModule +import dev.matsem.astral.raspberrypi.sketches.NeonLogo +import org.koin.core.KoinComponent +import org.koin.core.context.startKoin +import org.koin.core.inject +import org.koin.core.logger.Level +import processing.core.PApplet + +class RaspberryApp : KoinComponent { + + companion object { + @JvmStatic + fun main(args: Array) { + RaspberryApp().run(args) + } + } + + private val sketch: PApplet by inject() + + /** + * Launches PApplet with specified arguments. Be sure to include --sketch-path argument for proper data + * folder resolution (dir containing your Processing data folder), + * @see https://processing.github.io/processing-javadocs/core/ + */ + fun run(processingArgs: Array) { + startKoin { + printLogger(Level.ERROR) + modules(coreModule + raspberryModule { NeonLogo() }) + } + + PApplet.runSketch(processingArgs + arrayOf("RaspberryVisuals"), sketch) + } +} \ No newline at end of file diff --git a/raspberrypi/src/main/kotlin/dev/matsem/astral/raspberrypi/RaspberryModule.kt b/raspberrypi/src/main/kotlin/dev/matsem/astral/raspberrypi/RaspberryModule.kt new file mode 100644 index 00000000..4e5be79b --- /dev/null +++ b/raspberrypi/src/main/kotlin/dev/matsem/astral/raspberrypi/RaspberryModule.kt @@ -0,0 +1,9 @@ +package dev.matsem.astral.raspberrypi + +import org.koin.dsl.bind +import org.koin.dsl.module +import processing.core.PApplet + +fun raspberryModule(providePApplet: () -> PApplet) = module { + single { providePApplet() } bind PApplet::class +} \ No newline at end of file diff --git a/raspberrypi/src/main/kotlin/dev/matsem/astral/raspberrypi/sketches/Blank.kt b/raspberrypi/src/main/kotlin/dev/matsem/astral/raspberrypi/sketches/Blank.kt new file mode 100644 index 00000000..2ab90a94 --- /dev/null +++ b/raspberrypi/src/main/kotlin/dev/matsem/astral/raspberrypi/sketches/Blank.kt @@ -0,0 +1,21 @@ +package dev.matsem.astral.raspberrypi.sketches + +import dev.matsem.astral.core.tools.extensions.colorModeHsb +import org.koin.core.KoinComponent +import processing.core.PApplet +import processing.core.PConstants + +class Blank : PApplet(), KoinComponent { + + override fun settings() { + size(420, 420, PConstants.P2D) + } + + override fun setup() { + colorModeHsb() + } + + override fun draw() { + background(0f, 100f, 100f) + } +} \ No newline at end of file diff --git a/raspberrypi/src/main/kotlin/dev/matsem/astral/raspberrypi/sketches/NeonLogo.kt b/raspberrypi/src/main/kotlin/dev/matsem/astral/raspberrypi/sketches/NeonLogo.kt new file mode 100644 index 00000000..df4ad319 --- /dev/null +++ b/raspberrypi/src/main/kotlin/dev/matsem/astral/raspberrypi/sketches/NeonLogo.kt @@ -0,0 +1,314 @@ +package dev.matsem.astral.raspberrypi.sketches + +import ch.bildspur.postfx.builder.PostFX +import dev.matsem.astral.core.Files +import dev.matsem.astral.core.tools.animations.AnimationHandler +import dev.matsem.astral.core.tools.animations.radianSeconds +import dev.matsem.astral.core.tools.extensions.colorModeHsb +import dev.matsem.astral.core.tools.extensions.draw +import dev.matsem.astral.core.tools.extensions.heightF +import dev.matsem.astral.core.tools.extensions.pushPop +import dev.matsem.astral.core.tools.extensions.shorterDimension +import dev.matsem.astral.core.tools.extensions.translate +import dev.matsem.astral.core.tools.extensions.translateCenter +import dev.matsem.astral.core.tools.extensions.widthF +import dev.matsem.astral.core.tools.extensions.withAlpha +import extruder.extruder +import geomerative.RG +import geomerative.RShape +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import org.koin.core.KoinComponent +import org.koin.core.inject +import processing.core.PApplet +import processing.core.PConstants +import processing.core.PFont +import processing.core.PGraphics +import processing.core.PShape +import processing.core.PVector +import java.io.File +import java.util.* + +/** + * The standalone raspberry pi sketch. + * + * Uses geomerative library to convert an SVG logo into 2D shape. Extruder library is then used to extrude the + * logo into 3D shape. Renders with oldskool playstation-one-like effect using PostFX shaders. + * + * This sketch creates lineup.txt and render.properties files on your desktop. Use them to display text and modify render + * settings of the scene. The files are watched and the sketch content will be updated upon modification of these files. + */ +class NeonLogo : PApplet(), AnimationHandler, KoinComponent { + + override fun provideMillis(): Int = millis() + + private lateinit var fx: PostFX + private val ex: extruder by inject() + private lateinit var font: PFont + private val coroutineScope = CoroutineScope(Dispatchers.Default) + private lateinit var textFile: File + private lateinit var displayedText: String + + private val logoToScreenScale = 0.35f + private val shapeDepth = 100 + private var sclOff = 0f + private var rotYOff = 0f + private val starCount = 3000 + private val fps = 20f + private lateinit var starsCanvas: PGraphics + private lateinit var stars: List + private val logoPosition = PVector(0f, 0f) + private val logoPositionTarget = PVector(0f, 0f) + private val textPosition = PVector(0f, 0f) + private val textPositionTarget = PVector(0f, 0f) + private var lerpSpeed = 0.2f + private val renderStyles = arrayOf( + RenderStyle(fillColor = 0x00ffc8, strokeColor = 0x000000, strokeWeight = 10f), + RenderStyle(fillColor = 0x000000, strokeColor = 0x00ffc8, strokeWeight = 10f), + RenderStyle(fillColor = null, strokeColor = 0x00ffc8, strokeWeight = 10f) + ) + private var renderStyle = renderStyles.first() + + private val props: Properties = Properties() + private var logoActiveIntervalMs = 1000L + private var textActiveIntervalMs = 1000L + private var renderStyleSwitchIntervalMs = 1000L + private val fileReadIntervalMs = 5_000L + + companion object { + const val LineupFile = "lineup.txt" + const val PropsFile = "render.properties" + } + + data class Chunk( + val originalShape: RShape, + val extrudedShape: List, + val shapeWidth: Float, + val shapeHeight: Float + ) + + data class RenderStyle( + val fillColor: Int?, + val strokeColor: Int, + val strokeWeight: Float + ) + + private lateinit var chunks: List + override fun settings() { + fullScreen(PConstants.P3D) +// size(1024, 768, PConstants.P3D) + } + + override fun setup() { + colorModeHsb() + surface.setTitle("Futured") + surface.setResizable(true) + surface.hideCursor() + surface.setAlwaysOnTop(true) + frameRate(fps) + + RG.init(this) + RG.setPolygonizer(RG.UNIFORMSTEP) + RG.setPolygonizerStep(1f) + + chunks = listOf("images/semlogo_top.svg", "images/semlogo_bottom.svg") + .map { uri -> RG.loadShape(uri) } + .map { rshape -> RG.polygonize(rshape) } + .map { rshape -> + Chunk( + originalShape = rshape, + shapeWidth = rshape.width, + shapeHeight = rshape.height, + extrudedShape = rshape.children + .asSequence() + .map { it.points } + .map { points -> + createShape().apply { + beginShape() + for (point in points) { + vertex(point.x, point.y) + } + endShape(PApplet.CLOSE) + } + } + .flatMap { ex.extrude(it, 100, "box").toList() } + .onEach { + it.enableStyle() + it.translate(-rshape.width / 2f, -rshape.height / 2f, -shapeDepth / 2f) + } + .toList() + ) + } + + starsCanvas = createGraphics(width, height, PConstants.P3D) + stars = generateSequence { + PVector( + random(-width.toFloat(), width.toFloat()), + random(-width.toFloat(), width.toFloat()), + random(-width.toFloat(), width.toFloat()) + ) + }.take(starCount).toList() + + font = createFont(Files.Font.JETBRAINS_MONO, height / 20f, false) + textFile = desktopFile(LineupFile) + fx = PostFX(this) + + makeFiles() + + coroutineScope.launch { + while (isActive) { + logoPositionTarget.set(0f, 0f) + textPositionTarget.set(widthF, 0f) + kotlinx.coroutines.delay(logoActiveIntervalMs) + + logoPositionTarget.set(-widthF * 0.4f, 0f) + textPositionTarget.set(-widthF * 0.15f, 0f) + kotlinx.coroutines.delay(textActiveIntervalMs) + } + } + + coroutineScope.launch(Dispatchers.IO) { + while (isActive) { + displayedText = textFile.readText().trimIndent() + + props.load(desktopFile(PropsFile).inputStream()) + logoActiveIntervalMs = props["visuals.logo.duration_ms"].toString().toLongOrNull() ?: 35_000L + textActiveIntervalMs = props["visuals.text.duration_ms"].toString().toLongOrNull() ?: 60_000L + renderStyleSwitchIntervalMs = + props["visuals.logo.style.duration_ms"].toString().toLongOrNull() ?: 120_000L + + kotlinx.coroutines.delay(fileReadIntervalMs) + } + } + } + + private fun makeFiles() { + val textFile = desktopFile(LineupFile) + val propsFile = desktopFile(PropsFile) + if (textFile.exists().not()) { + textFile.createNewFile() + textFile.writeText( + """ + ~/Desktop/lineup.txt + """.trimIndent() + ) + } + + if (propsFile.exists().not()) { + propsFile.createNewFile() + propsFile.writeText( + """ + visuals.logo.style.duration_ms=5000 + visuals.logo.duration_ms=5000 + visuals.text.duration_ms=5000 + """.trimIndent() + ) + } + } + + override fun draw() { + background(0) + ortho() + + // Update props + + stars.forEach { + it.z += 2f + if (it.z > width) { + it.z = random(-width.toFloat(), 0f) + } + } + + logoPosition.lerp(logoPositionTarget, lerpSpeed) + textPosition.lerp(textPositionTarget, lerpSpeed) + + if (millis() % renderStyleSwitchIntervalMs in 0 until 1000) { + renderStyle = renderStyles.random() + } + + // endregion + + // region Starfield + + starsCanvas.draw { + fill(0x000000.withAlpha(32)) + rect(0f, 0f, widthF, heightF) + + pushPop { + noStroke() + fill(0x00ffc8.withAlpha(128)) + translateCenter() + + stars.forEach { + pushPop { + translate(it.x, it.y, it.z) + circle(0f, 0f, 3f) + } + } + } + } + + pushPop { + translate(0f, 0f, -400f) + image(starsCanvas, 0f, 0f) + } + + // endregion + + // region Logo + + pushPop { + translateCenter() + translate(logoPosition) + + val renderStyle = renderStyle + + for (chunk in chunks) { + pushPop { + scale( + width / chunk.shapeWidth * logoToScreenScale + sclOff, + width / chunk.shapeWidth * logoToScreenScale + sclOff + ) + + rotateX(PI * 0.1f * sin(radianSeconds(60f))) + rotateY(-radianSeconds(30f) + rotYOff) + chunk.extrudedShape.forEach { + it.setFill(true) + if (renderStyle.fillColor != null) { + it.setFill(renderStyle.fillColor.withAlpha()) + } else { + it.setFill(0x00000000) + } + it.setStroke(true) + it.setStroke(renderStyle.strokeColor.withAlpha()) + it.setStrokeWeight(renderStyle.strokeWeight) + shape(it) + } + } + } + } + + // endregion + + // region Info text + + pushPop { + translateCenter() + translate(textPosition) + textFont(font) + textAlign(LEFT, CENTER) + noStroke() + fill(0x00ffc8.withAlpha()) + text(displayedText, 0f, 0f) + } + + // endregion + + fx.render().apply { + noise(0.3f, 0.1f) + pixelate(shorterDimension() / 2.4f) + }.compose() + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 47e15258..9011ccf3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,3 +2,4 @@ rootProject.name = "astral-visuals" include("core") include("visuals") include("playground") +include("raspberrypi")