๐Ÿ–ผ๏ธ Artwork 01. Recursive Lines

BamgasiJMยท6์ผ ์ „

OPENRNDR

๋ชฉ๋ก ๋ณด๊ธฐ
16/17
post-thumbnail

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 {
                    // ๋ผ์ธ์ด ์—†์œผ๋ฉด: ํŽ˜์ด๋“œ ์•„์›ƒ (๋ผ์ธ์ด ๋ชจ๋‘ ์‚ฌ๋ผ์ง„ ํ›„ 1์ดˆ)
                    val fadeOutStartTime = branch.birthTime + 1.5 + 0.3  // lifetime + fadeOut
                    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()) {
                    // ์•ŒํŒŒ๊ฐ€ 0์ด ๋˜๋ฉด ๋ธŒ๋žœ์น˜ ์ œ๊ฑฐ
                    branchesToRemove.add(branch)
                }
            }

            branches.removeAll(branchesToRemove)
        }
    }
}

profile
Coding Art with Blender / Three.js / p5.js / GLSL Shader / Processing / nannou

0๊ฐœ์˜ ๋Œ“๊ธ€