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() {
(450, 450)
size()
noStrokeif(isAndroid()) maxSize = 24f
}
override fun draw() {
()
matchWidth
if(!inited){
(count){ index ->
repeat.add(Tadpole(index))
tadpoles}
= true
inited }
.forEach { tadpole ->
tadpoles.update().draw()
tadpole}
.removeAll { tadpole ->
tadpoles!tadpole.alive
}
when (tadpoles.size) {
1 -> {
(count){ index ->
repeat.add(Tadpole(index))
tadpoles}
}
}
.addAll(cycleOffspring)
tadpoles.clear()
cycleOffspring.addAll(touchOffspring)
tadpoles.clear()
touchOffspring
(0xf5f2f0, 0.28f)
foreground}
class Tadpole(private val id: Int, spawnLocation: Vector?, private val parentId: Int?){
inner
constructor(id: Int) : this(id, null, null)
private var location: Vector = when {
!= null -> spawnLocation
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
++
cyclesif(cycles == allowedCycles){
//die
= false
alive }
val distances = HashMap()
.forEach { tadpole ->
tadpolesval distance = location.distance(tadpole.location)
[tadpole.id] = distance
distances}
//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 }
= sortedDistances[1].second
closestDistance
var directionToTadpole = closestTadpole!!.location - location
.normalize()
directionToTadpole
//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
}
= cyclesAttached > 100
inRelationship
//Update closest tadpole id after we've checked if they were previous closest
= closestTadpole.id
currentCompanion
//Have they been together too long?
when {
> relationshipLength -> {
cyclesAttached .add(currentCompanion)
exes= -1
currentCompanion = 0
cyclesAttached = random(100, 500)
relationshipLength = false
inRelationship }
}
//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
*= when {
directionToTadpole .contains(closestTadpole.id) -> -0.6f
exes(parentId != null && (parentId == closestTadpole.id)) -> -2.4f
(parentId != null && (parentId == closestTadpole.parentId)) -> -2.4f
else -> 0.2f
}
= directionToTadpole
acceleration
//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 ->
.id == sortedDistances[index].first
tadpole}
if(!tadpole!!.inRelationship && !foundThreat){
//Single - is a threat, move away
val directionToThreat = tadpole.location - location
.normalise()
directionToThreat= directionToTadpole + (directionToThreat * -0.4f)
acceleration = true
foundThreat }
if(foundThreat){
break
}
}
//Arbitary max population count
if(!hasOffspring && cyclesAttached > relationshipLength/2 && tadpoles.size < maxPopulation){
if(random(100) < 8){
= true
hasOffspring val numberOfOffspring = random(1, 2).toInt()
(numberOfOffspring){
repeat.add(Tadpole(count + offspring, location, id))
cycleOffspring}
+= numberOfOffspring
offspring }
}
}
+= acceleration!!
velocity
if(inRelationship){
val blackHole = Vector(width/2, height/2)
var directionToBlackHole = blackHole - location
.normalize()
directionToBlackHole*= 0.3f
directionToBlackHole += directionToBlackHole
velocity
.limit(maxSpeed / 1.25f)
velocity}else{
.limit(maxSpeed)
velocity}
+= velocity
location
if (exes.size == maxRelationshipMemory) exes.removeAt(0)//Forget oldest relationships
return this
}
fun draw() {
val diam = if(cycles < allowedCycles/2){
.map(cycles.toFloat(), 0f, allowedCycles.toFloat(), 2f, maxSize)
Math}else{
.map(cycles.toFloat(), 0f, allowedCycles.toFloat(), maxSize, 2f)
Math}
(diam)
checkBounds(tadpoleColour)
fill
(location.x, location.y, diam.toInt())
circle}
private fun checkBounds(diam: Float) {
when {
.x > width +diam -> location.x = -diam
location.x < -diam -> location.x = width.toFloat() + diam
location}
when {
.y > (height +diam) -> location.y = -diam
location.y < -diam -> location.y = height.toFloat() + diam
location}
}
}
}