aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin
diff options
context:
space:
mode:
authorAkshay Nair <phenax5@gmail.com>2026-10-24 12:09:08 +0530
committerAkshay Nair <phenax5@gmail.com>2026-10-24 13:05:25 +0530
commite251d719710a951954e2dd9f93544afc601a6600 (patch)
tree1140d17fd3e4da4cfba0a33edb1b4f79d15a357b /src/main/kotlin
parentbfd7af6c4772c68f001c5c121b62b134bbca04e4 (diff)
downloadart-playground-openrndr-e251d719710a951954e2dd9f93544afc601a6600.tar.gz
art-playground-openrndr-e251d719710a951954e2dd9f93544afc601a6600.zip
Add blood reaction diffusion
Diffstat (limited to 'src/main/kotlin')
-rw-r--r--src/main/kotlin/blood/BloodConfigs.kt106
-rw-r--r--src/main/kotlin/blood/BloodProgram.kt120
2 files changed, 226 insertions, 0 deletions
diff --git a/src/main/kotlin/blood/BloodConfigs.kt b/src/main/kotlin/blood/BloodConfigs.kt
new file mode 100644
index 0000000..d6630e6
--- /dev/null
+++ b/src/main/kotlin/blood/BloodConfigs.kt
@@ -0,0 +1,106 @@
+package blood
+
+import kotlin.random.Random
+import org.openrndr.math.IntVector2
+import org.openrndr.math.Vector2
+
+typealias Cells = Array<Array<Vector2>>
+
+class BloodConfigs {
+ abstract class Base {
+ abstract val feedRate: Double
+ abstract val killRate: Double
+ open val iterationsPerFrame: Int = 40
+ open val diffusion: Vector2 = Vector2(1.0, 0.5)
+ open val random: Random = Random(100)
+ open val initialSize: IntVector2 = IntVector2(20, 20)
+ open val noiseProbability: Double = 1.0
+
+ open fun initializeState(cells: Cells) {
+ val x = cells[0].size / 2 - initialSize.x / 2
+ val y = cells.size / 2 - initialSize.y / 2
+ for (i in y until (y + initialSize.y)) {
+ for (j in x until (x + initialSize.x)) {
+ val v = random.nextDouble(1.0) <= noiseProbability
+ cells[i][j] = if (v) Vector2(0.0, 1.0) else Vector2(1.0, 0.0)
+ }
+ }
+ }
+ }
+
+ class mitosis : Base() {
+ override val feedRate = 0.0367
+ override val killRate = 0.0649
+ override val noiseProbability = 0.1
+ override val random = Random(0)
+ }
+
+ class coral : Base() {
+ override val feedRate = 0.055
+ override val killRate = 0.062
+ }
+
+ class holey : Base() {
+ override val feedRate = 0.043
+ override val killRate = 0.059
+ }
+
+ class pulseSettled1 : Base() {
+ override val feedRate = 0.037
+ override val killRate = 0.059
+ }
+
+ class pulseSettled2 : Base() {
+ override val feedRate = 0.029
+ override val killRate = 0.057
+ }
+
+ class bubbles : Base() {
+ override val feedRate = 0.014
+ override val killRate = 0.054
+ override val noiseProbability = 0.2
+ }
+
+ class worms : Base() {
+ override val feedRate = 0.078
+ override val killRate = 0.061
+ override val random = Random(200)
+ override val noiseProbability = 0.1
+ override val initialSize = IntVector2(200, 50)
+ }
+
+ class fingies : Base() {
+ override val feedRate = 0.046
+ override val killRate = 0.063
+ }
+
+ class maze : Base() {
+ override val feedRate = 0.029
+ override val killRate = 0.057
+ override val noiseProbability = 0.2
+ }
+
+ class explosions : Base() {
+ override val feedRate = 0.018
+ override val killRate = 0.051
+ override val noiseProbability = 0.2
+ override val iterationsPerFrame = 18
+ }
+
+ class waves : Base() {
+ override val feedRate = 0.014
+ override val killRate = 0.045
+ override val iterationsPerFrame = 18
+ }
+}
+
+fun getConfig(name: String): BloodConfigs.Base {
+ val cls = BloodConfigs::class.nestedClasses.first { it.simpleName == name }
+ val ctor = cls.constructors.first()
+ val inst = ctor.call() as BloodConfigs.Base
+ return inst
+}
+
+fun getConfig(): BloodConfigs.Base {
+ return getConfig(System.getenv("PHENAX_FILE_SUFFIX") ?: "pulseSettled2")
+}
diff --git a/src/main/kotlin/blood/BloodProgram.kt b/src/main/kotlin/blood/BloodProgram.kt
new file mode 100644
index 0000000..ed70213
--- /dev/null
+++ b/src/main/kotlin/blood/BloodProgram.kt
@@ -0,0 +1,120 @@
+package blood
+
+// import org.openrndr.extra.timer.repeat
+
+import blood.*
+import org.openrndr.WindowMultisample
+import org.openrndr.application
+import org.openrndr.color.*
+import org.openrndr.draw.*
+import org.openrndr.extra.noise.perlinLinear
+import org.openrndr.extra.shadestyles.fills.gradients.gradient
+import org.openrndr.math.IntVector2
+import org.openrndr.math.Vector2
+import org.openrndr.shape.Rectangle
+import phenax.utils.*
+
+fun main() = application {
+ recordDuration(15.seconds())
+
+ if (captureEnabled) {
+ configure {
+ width = 1920
+ height = 1080
+ windowResizable = false
+ multisample = WindowMultisample.SampleCount(8)
+ }
+ } else {
+ configure {
+ width = 640
+ height = 360
+ windowResizable = false
+ }
+ }
+
+ oliveProgram {
+ val cell_size = 3.0
+ val cell_count = IntVector2(width, height) / cell_size.toInt()
+ val config = getConfig()
+
+ var cells: Cells = Array(cell_count.y) { Array(cell_count.x) { Vector2(1.0, 0.0) } }
+ config.initializeState(cells)
+
+ val feedRate = config.feedRate
+ val killRate = config.killRate
+ val diffusion = config.diffusion
+
+ fun laplaceThyNeighbour(row: Int, col: Int): Vector2 {
+ var d = Vector2.ZERO
+ for (i in -1 until 2) {
+ for (j in -1 until 2) {
+ val r = row + i
+ val c = col + j
+ val weight = if (i == 0 && j == 0) -1.0 else if (i == 0 || j == 0) 0.2 else 0.05
+ if (r >= 0 && r < cell_count.y && c >= 0 && c < cell_count.x) d += cells[r][c] * weight
+ }
+ }
+ return d
+ }
+
+ fun coerceIn(v: Vector2, min: Double, max: Double): Vector2 {
+ return Vector2(v.x.coerceIn(min, max), v.y.coerceIn(min, max))
+ }
+
+ fun nextState(dt: Double): Cells {
+ return Array(cell_count.y) { row ->
+ Array(cell_count.x) { col ->
+ val d = cells[row][col]
+ val lapl = laplaceThyNeighbour(row, col)
+ val u = -d.x * d.y * d.y + feedRate * (1 - d.x)
+ val v = d.x * d.y * d.y - (feedRate + killRate) * d.y
+ var change = lapl * diffusion + Vector2(u, v)
+ coerceIn(d + change * dt, 0.0, 1.0)
+ }
+ }
+ }
+
+ val col_flesh = ColorRGBa(0.53, 0.20, 0.20)
+ val col_blood = ColorRGBa(0.34, 0.00, 0.00)
+
+ fun drawCell(pos: Vector2, amounts: Vector2) {
+ var color = col_flesh.opacify(0.0)
+
+ run {
+ val noiseScale = 0.99
+ val noise = perlinLinear(100, pos.x * noiseScale, pos.y * noiseScale)
+ color = mix(color, col_flesh.shade(1.1).opacify(0.14), noise)
+ }
+
+ color = mix(color, col_blood, (amounts.y * 3).coerceIn(0.0, 1.0))
+ if (amounts.y > 0.38) color = mix(color, ColorRGBa.BLACK, amounts.y * 0.08)
+ if (amounts.y > 0.40) color = mix(color, ColorRGBa.WHITE, amounts.y * 0.28)
+
+ drawer.fill = color
+ drawer.stroke = drawer.fill
+ drawer.rectangle(pos.x * cell_size, pos.y * cell_size, cell_size, cell_size)
+ }
+
+ extend { repeat(config.iterationsPerFrame) { cells = nextState(0.8) } }
+
+ extend {
+ drawer.clear(col_flesh)
+ drawer.shadeStyle =
+ gradient<ColorRGBa> {
+ stops[0.4] = col_flesh
+ stops[0.9] = col_flesh.shade(0.65)
+ radial {}
+ }
+ drawer.rectangle(
+ Rectangle.fromCenter(drawer.bounds.center, width.toDouble(), height.toDouble())
+ )
+ drawer.shadeStyle = null
+ drawer.strokeWeight = 0.0
+ for ((row, rowdiff) in cells.withIndex()) {
+ for ((col, diff) in rowdiff.withIndex()) {
+ drawCell(Vector2(col.toDouble(), row.toDouble()), diff)
+ }
+ }
+ }
+ }
+}