Tails and orbs

Full screen version

import coracle.*
import coracle.shapes.Circle
import kotlin.math.cos
import kotlin.math.sin

class TailsAndOrbs: Drawing() {
    val w = 450
    val h = 450

    val minOrbSize = 6
    val maxOrbSize = 24
    val orbs = Array(24){ Orb(random(w), random(h))}

    val boids = Array(350){ Boid(random(w), random(h))}
    val boidColour = 0x000000
    val worldColour = 0xf5f2f0
    val orbColour = 0xe5e2e0

    var epochElapsed = 0
    var epochLength = 800

    var minNoiseScale = 0.025f
    var maxNoiseScale = 0.06f
    var noiseScale = random(minNoiseScale, maxNoiseScale)

    var frame = 0

    var inited = false

    override fun setup() {
        size(w, h)

        stroke(boidColour)
        noFill()
    }

    override fun draw() {
        frame++
        background(worldColour)

        orbs.forEach { orb ->
            orb.updateFlowField().grow().checkBounds().draw()
        }

        boids.forEach { boid ->
            boid
                .iterate()
                .avoidOrbs()
                .checkBounds()
                .draw()
        }

        epochElapsed++
        if(epochElapsed >= epochLength){
            Perlin.newSeed()
            noiseScale = random(minNoiseScale, maxNoiseScale)
            epochElapsed = 0
        }
    }

    inner class Boid(x: Float, y: Float): Vector(x, y) {

        var age = 0
        var deathAge = random(100, 340)

        var velocity = Vector(0f, 0f)
        var maxSpeed = 1.7f

        var tailLength = randomInt(10, 30)
        var tailX = FloatArray(tailLength)
        var tailY = FloatArray(tailLength)

        fun iterate(): Boid {
            val a = TAU * Perlin.noise(x * noiseScale, y * noiseScale)
            var direction = direction(Vector( x + (cos(a)).toFloat(), y + (sin(a) ).toFloat()))
            direction *= 0.35f

            velocity += direction

            velocity.limit(maxSpeed)

            x += velocity.x
            y += velocity.y

            val tailIndex = frame % tailLength
            tailX[tailIndex] = x
            tailY[tailIndex] = y

            age++

            return this
        }

        fun avoidOrbs(): Boid {
            orbs.forEach { orb ->
                if(distance(orb) < orb.r + 10){
                    var direction = direction(orb)
                    direction *= -0.8f
                    velocity += direction
                    velocity.limit(maxSpeed)
                    this.x += velocity.x
                    this.y += velocity.y
                }
            }
            return this
        }

        override fun draw(){
            repeat(tailLength){ tailIndex ->
                stroke(boidColour, 0.4f)
                point(tailX[tailIndex], tailY[tailIndex])

                if(tailIndex == tailLength - 1){
                    noStroke()
                    fill(boidColour)
                    circle(x, y, 1)
                }
            }

        }

        fun checkBounds(): Boid {
            if(age >= deathAge || x < 0 || x > width || y < 0 || y > height ){
                x = random(width)
                y = random(height)
                tailLength = randomInt(10, 30)
                tailX = FloatArray(tailLength)
                tailY = FloatArray(tailLength)
                age = 0
                deathAge = random(100, 340)
            }
            return this
        }
    }

    inner class Orb(x: Float, y: Float): Circle(x, y, 1){

        var size = 1f
        var targetSize = random(minOrbSize, maxOrbSize)
        var growthRate = 0.6f

        var velocity = Vector(0f, 0f)
        var maxSpeed = 0.25f

        var age = 0
        var deathAge = randomInt(300, 800)

        val count = randomInt(8, 32)
        var rotate = 0f
        var rotateDirection = when {
            random(100) < 50 -> -1
            else -> 1
        }

        fun updateFlowField(): Orb {
            val a = TAU * Perlin.noise(x * noiseScale, y * noiseScale)
            var direction = Vector(x, y).direction(Vector( x + (cos(a)).toFloat(), y + (sin(a) ).toFloat()))
            direction *= 0.4f

            velocity += direction

            velocity.limit(maxSpeed)

            x += velocity.x
            y += velocity.y

            return this
        }

        fun grow(): Orb {

            if(age < deathAge){
                if(size < targetSize){
                    size += growthRate
                }

            }else{
                if(size > 1){
                    size -= growthRate
                }else{
                    respawn()
                }
            }

            r = size
            age++

            when (rotateDirection) {
                1 -> rotate += 0.01f
                else -> rotate -= 0.01f
            }

            return this
        }

        fun checkBounds(): Orb {
            if(x < 0 - r || x > width + r || y < 0 - r || y > height + r ){
                respawn()
            }
            return this
        }

        private fun respawn(){
            x = random(width)
            y = random(height)
            targetSize = random(minOrbSize, maxOrbSize)
            size = 1f
            r = size
            age = 0
            deathAge = randomInt(100, 400)
        }

        val parametricDraw = true

        override fun draw(){
            when {
                parametricDraw -> parametricDraw()
                else -> {
                    fill(orbColour)
                    stroke(boidColour, 0.8f)
                    circle(x, y, r)
                }
            }
        }

        private fun parametricDraw(){
            stroke(boidColour)
            repeat(count){ i ->
                val angle = (i * TWO_PI / count).toFloat()
                point(x + cos(angle + rotate) * r, y + sin(angle + rotate) * r)
            }
        }
    }
}