OPENRNDR Code
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.math.Vector2
import kotlin.math.*
import kotlin.random.Random
data class LineSegment(
val start: Vector2,
val direction: Vector2,
val length: Double,
val birthTime: Double,
val lifetime: Double = 2.0,
val depth: Int = 0,
val fadeInDuration: Double = 1.5
)
data class Branch(
val startPoint: Vector2,
val lines: MutableList<LineSegment> = mutableListOf(),
val birthTime: Double,
val fadeInDuration: Double = 1.5
)
fun getLineAlpha(age: Double, lifetime: Double, fadeInDuration: Double): Double {
return when {
age < fadeInDuration -> age / fadeInDuration
age < lifetime -> 1.0
else -> {
val fadeOutDuration = 1.5
val fadeOutAge = age - lifetime
(1.0 - (fadeOutAge / fadeOutDuration)).coerceIn(0.0, 1.0)
}
}
}
fun main() = application {
configure {
width = 1000
height = 1000
title = "Interactive Recursive Lines"
}
program {
val branches = mutableListOf<Branch>()
var currentTime = 0.0
val maxDepth = 2
mouse.buttonDown.listen { event ->
val clickPos = event.position
val newBranch = Branch(clickPos, mutableListOf(), currentTime)
val lineCount = Random.nextInt(13, 16)
repeat(lineCount) {
val angle = (it.toDouble() / lineCount) * 2 * PI + Random.nextDouble(-0.2, 0.2)
val direction = Vector2(cos(angle), sin(angle))
val lineLength = Random.nextDouble(40.0, 60.0)
newBranch.lines.add(
LineSegment(
start = clickPos,
direction = direction,
length = lineLength,
birthTime = currentTime,
depth = 0
)
)
}
branches.add(newBranch)
}
extend {
drawer.clear(ColorRGBa.BLACK)
currentTime += 0.016
drawer.stroke = ColorRGBa.WHITE
drawer.strokeWeight = 1.2
drawer.fill = ColorRGBa.WHITE
val linesToRemove = mutableListOf<Pair<Branch, LineSegment>>()
val newLines = mutableListOf<LineSegment>()
for (branch in branches) {
for (line in branch.lines) {
val age = currentTime - line.birthTime
val alpha = getLineAlpha(age, line.lifetime, line.fadeInDuration)
if (alpha > 0.0) {
drawer.stroke = ColorRGBa(1.0, 1.0, 1.0, alpha)
val endPoint = line.start + line.direction * line.length
drawer.lineSegment(line.start, endPoint)
drawer.fill = ColorRGBa(1.0, 1.0, 1.0, alpha)
drawer.circle(endPoint, 1.5)
if (line.depth < maxDepth && age > line.lifetime * 0.35 && age <= line.lifetime * 0.36) {
val branchCount = Random.nextInt(2, 4)
repeat(branchCount) {
val childAngle = atan2(line.direction.y, line.direction.x) +
Random.nextDouble(-PI / 4, PI / 4)
val childDirection = Vector2(cos(childAngle), sin(childAngle))
val childLength = line.length * Random.nextDouble(0.6, 0.75)
newLines.add(
LineSegment(
start = endPoint,
direction = childDirection,
length = childLength,
birthTime = currentTime,
depth = line.depth + 1
)
)
}
}
}
if (age > line.lifetime + 0.3) {
linesToRemove.add(branch to line)
}
}
}
for ((branch, line) in linesToRemove) {
branch.lines.remove(line)
}
if (newLines.isNotEmpty()) {
val existingBranches = branches.filter { it.lines.isNotEmpty() }
if (existingBranches.isNotEmpty()) {
existingBranches.last().lines.addAll(newLines)
}
}
drawer.fill = ColorRGBa.WHITE
drawer.stroke = null
val branchesToRemove = mutableListOf<Branch>()
for (branch in branches) {
val branchAge = currentTime - branch.birthTime
val branchAlpha = if (branch.lines.isNotEmpty()) {
when {
branchAge < branch.fadeInDuration -> branchAge / branch.fadeInDuration
else -> 1.0
}
} else {
val fadeOutStartTime = branch.birthTime + 1.5 + 0.3
val fadeOutDuration = 1.0
if (currentTime < fadeOutStartTime + fadeOutDuration) {
val fadeProgress = ((currentTime - fadeOutStartTime) / fadeOutDuration).coerceIn(0.0, 1.0)
(1.0 - fadeProgress).coerceIn(0.0, 1.0)
} else {
0.0
}
}
if (branchAlpha > 0.0) {
drawer.fill = ColorRGBa(1.0, 1.0, 1.0, branchAlpha)
drawer.circle(branch.startPoint, 3.5)
} else if (branchAlpha == 0.0 && branch.lines.isEmpty()) {
branchesToRemove.add(branch)
}
}
branches.removeAll(branchesToRemove)
}
}
}