Boids and orbs

Full screen version

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

class BoidsAndOrbs: Drawing() {
    val spatialHash = SpatialHash(10, 10)
    val orbs = mutableListOf()
    val agentCount = 1400
    val orbCount = 8
    var scale = 0.01f
    var elapsed = 0

    var frame = 0

    override fun setup() {
        size(450, 450)

        repeat(orbCount){
            orbs.add(Orb(random(width), random(height)))
        }
        repeat(agentCount){
            spatialHash.add(Agent(random(width), random(height)))
        }
    }

    override fun draw() {
        frame++

        fill(0xffffff, 0.2f)
        noStroke()
        orbs.forEach { orb ->
            orb.updateFlowField().grow().checkBounds().draw()
        }

        stroke(0xffffff, 0.75f)
        spatialHash
            .iterate()
            .remap()

        elapsed++
        if(elapsed > 350){
            Perlin.newSeed()
            scale = random(0.008f, 0.02f)
            elapsed = 0
        }

        foreground(0x000000, 0.025f)
    }

    inner class SpatialHash(private val columns: Int, private val rows: Int){
        val cellPopulations = HashMap>()
        var initialised = false

        init {
            var index = -1
            repeat(rows * columns){
                index++
                cellPopulations[index] = mutableListOf()
            }
        }

        fun add(c: Agent): SpatialHash {
            val index = getIndexHash(c.x.toInt(), c.y.toInt())
            val population = cellPopulations[index]
            population?.add(c)

            initialised = true
            return this
        }

        fun getIndexHash(x: Int, y: Int): Int {
            val col = (x * columns / (width + 1))
            val row = (y * rows / (height + 1))
            return row * columns + col
        }

        fun iterate(): SpatialHash {
            cellPopulations.forEach { cellCollection ->
                cellCollection.value.forEach { agent ->
                    agent
                        .avoidOrbs()
                        .updateFlowField()
                        .checkBounds()
                        .draw()
                }
            }

            return this
        }

        fun remap(){
            cellPopulations.values.forEachIndexed { index, agents ->
                agents.indices.reversed().forEach { i ->
                    val agent = agents[i]
                    val correctIndex = getIndexHash(agent.x.toInt(), agent.y.toInt())
                    if(correctIndex != index){
                        agents.removeAt(i)
                        add(agent)
                    }
                }
            }
        }
    }

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

        var size = 1f
        var targetSize = random(20, 55)
        var growthRate = random(0.08f, 0.4f)
        val orbSpeed = 0.3f

        var age = 0
        var deathAge = randomInt(100, 400)


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

        fun grow(): Orb{


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

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

            r = size
            age++

            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(20, 55)
            size = 1f
            r = size
            growthRate = random(0.08f, 0.4f)
            age = 0
            deathAge = randomInt(100, 400)
        }
    }

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

        var age = 0
        var deathAge = random(100, 340)
        private var velocity = Vector(0f, 0f)
        private var maxSpeed = random(2.2f, 3f)

        fun updateFlowField(): Agent {
            if(frame % 3 != 0) return this
            age++
            val a = TAU * Perlin.noise(x * scale, y * scale)
            val direction = direction(Vector( x + (cos(a)).toFloat(), y + (sin(a) ).toFloat()))
            velocity += direction
            velocity.limit(maxSpeed)
            this.x += velocity.x
            this.y += velocity.y

            return this
        }

        fun avoidOrbs(): Agent{
            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
        }

        fun checkBounds(): Agent {
            if(age >= deathAge || x < 0 || x > width || y < 0 || y > height ){
                x = random(width)
                y = random(height)
                age = 0
                deathAge = random(100, 340)
            }
            return this
        }
    }
}