Tadpoles
This is the original ‘tadpole’ drawing which was used on the early Coracle homepage. It uses the old trick of drawing a semi-transparent rectangle over the frame at the end of each draw iteration, this gives the effect of the agents/boids having tails, though they really only ever draw a single circle.
import coracle.*
class TadpolesOriginal: Drawing() {
private val tadpoles = mutableListOf()
private val cycleOffspring = mutableListOf()
private val touchOffspring = mutableListOf()
private val maxRelationshipMemory = 20
private val count = 40
private val maxPopulation = 70
private var offspring = 0
private var maxSize = 8f
private var tadpoleColour = 0x000000
private var inited = false
override fun setup() {
size(450, 450)
noStroke()
if(isAndroid()) maxSize = 24f
}
override fun draw() {
matchWidth()
if(!inited){
repeat(count){ index ->
tadpoles.add(Tadpole(index))
}
inited = true
}
tadpoles.forEach { tadpole ->
tadpole.update().draw()
}
tadpoles.removeAll { tadpole ->
!tadpole.alive
}
when (tadpoles.size) {
1 -> {
repeat(count){ index ->
tadpoles.add(Tadpole(index))
}
}
}
tadpoles.addAll(cycleOffspring)
cycleOffspring.clear()
tadpoles.addAll(touchOffspring)
touchOffspring.clear()
foreground(0xf5f2f0, 0.28f)
}
inner class Tadpole(private val id: Int, spawnLocation: Vector?, private val parentId: Int?){
constructor(id: Int) : this(id, null, null)
private var location: Vector = when {
spawnLocation != null -> spawnLocation
else -> Vector(random(width), random(height))
}
private var velocity = Vector(0f, 0f)
private var acceleration: Vector? = null
private var maxSpeed = 3.5f
private var relationshipLength = random(400, 900)
private var allowedCycles = random(900, 2000).toInt()
private var closestDistance = Float.MAX_VALUE
private val exes = mutableListOf()
private var currentCompanion = -1
private var cycles = 0
private var cyclesAttached = 0
private var inRelationship = false
private var hasOffspring = false
var alive = true
fun update(): Tadpole {
if(tadpoles.size == 1) return this
cycles++
if(cycles == allowedCycles){
//die
alive = false
}
val distances = HashMap()
tadpoles.forEach { tadpole ->
val distance = location.distance(tadpole.location)
distances[tadpole.id] = distance
}
//Sort - self will be index 0
val sortedDistances = distances.toList().sortedBy { (_, value) -> value}
//Closest neighbour
val closestTadpole = tadpoles.find { tadpole -> tadpole.id == sortedDistances[1].first }
closestDistance = sortedDistances[1].second
var directionToTadpole = closestTadpole!!.location - location
directionToTadpole.normalize()
//Is the closest tadpole the same as last cycle?
if(currentCompanion == closestTadpole.id && closestDistance < 2.5){
cyclesAttached++
}else{
//This is more correct for what we're trying to achieve, but the result is more boring movement,
//so remove it:
//cyclesAttached = 0
}
inRelationship = cyclesAttached > 100
//Update closest tadpole id after we've checked if they were previous closest
currentCompanion = closestTadpole.id
//Have they been together too long?
when {
cyclesAttached > relationshipLength -> {
exes.add(currentCompanion)
currentCompanion = -1
cyclesAttached = 0
relationshipLength = random(100, 500)
inRelationship = false
}
}
//If closest neighbour is an ex then move away,
//if it's a parent or siblings move away quickly,
//otherwise stay close to current companion
directionToTadpole *= when {
exes.contains(closestTadpole.id) -> -0.6f
(parentId != null && (parentId == closestTadpole.id)) -> -2.4f
(parentId != null && (parentId == closestTadpole.parentId)) -> -2.4f
else -> 0.2f
}
acceleration = directionToTadpole
//Block only applies to tadpoles in a relationship
if(inRelationship){
//Find nearest single tadpole, index 0 is self, index 1 is partner
var foundThreat = false
for(index in 2 until tadpoles.size){
val tadpole = tadpoles.find { tadpole ->
tadpole.id == sortedDistances[index].first
}
if(!tadpole!!.inRelationship && !foundThreat){
//Single - is a threat, move away
val directionToThreat = tadpole.location - location
directionToThreat.normalise()
acceleration = directionToTadpole + (directionToThreat * -0.4f)
foundThreat = true
}
if(foundThreat){
break
}
}
//Arbitary max population count
if(!hasOffspring && cyclesAttached > relationshipLength/2 && tadpoles.size < maxPopulation){
if(random(100) < 8){
hasOffspring = true
val numberOfOffspring = random(1, 2).toInt()
repeat(numberOfOffspring){
cycleOffspring.add(Tadpole(count + offspring, location, id))
}
offspring += numberOfOffspring
}
}
}
velocity += acceleration!!
if(inRelationship){
val blackHole = Vector(width/2, height/2)
var directionToBlackHole = blackHole - location
directionToBlackHole.normalize()
directionToBlackHole *= 0.3f
velocity += directionToBlackHole
velocity.limit(maxSpeed / 1.25f)
}else{
velocity.limit(maxSpeed)
}
location += velocity
if (exes.size == maxRelationshipMemory) exes.removeAt(0)//Forget oldest relationships
return this
}
fun draw() {
val diam = if(cycles < allowedCycles/2){
Math.map(cycles.toFloat(), 0f, allowedCycles.toFloat(), 2f, maxSize)
}else{
Math.map(cycles.toFloat(), 0f, allowedCycles.toFloat(), maxSize, 2f)
}
checkBounds(diam)
fill(tadpoleColour)
circle(location.x, location.y, diam.toInt())
}
private fun checkBounds(diam: Float) {
when {
location.x > width +diam -> location.x = -diam
location.x < -diam -> location.x = width.toFloat() + diam
}
when {
location.y > (height +diam) -> location.y = -diam
location.y < -diam -> location.y = height.toFloat() + diam
}
}
}
}