Skip to content

Instantly share code, notes, and snippets.

@KrabCode
Created July 11, 2025 13:18
Show Gist options
  • Save KrabCode/547e7c8e03397c8acc5c34830e29520b to your computer and use it in GitHub Desktop.
Save KrabCode/547e7c8e03397c8acc5c34830e29520b to your computer and use it in GitHub Desktop.
float minFishSize = 10;
float maxFishSize = 20;
int fishCount = 50;
final float animationSpeedIdle = radians(0.5f); // Speed of the fish animation
final float animationSpeedFast = radians(1f); // Speed of the fish animation
float alignRadius = 200;
float alignWeight = 0.5f;
float centralizeRadius = 283;
float centralizeWeight = 0.5f;
float avoidRadius = 30;
float avoidWeight = 5f;
float minSpeed = 6;
float maxSpeed = 16;
float globalDrag = 0.95f;
float randomMag = 0.5f;
final ArrayList<Dust> dusts = new ArrayList<Dust>();
final ArrayList<Dust> dustsToRemove = new ArrayList<Dust>();
int dustCount = 300;
float accSmoothing = 0.1f; // Smoothing factor for acceleration
float dustMinLife = 30; // Lifetime of dust particles
float dustMaxLife = 100; // Lifetime of dust particles
float dustMinSize = 2; // Minimum size of dust particles
float dustMaxSize = 5; // Maximum size of dust particles
float strokeWeightPlayer = 1;
float strokeWeightFish = 1;
int fishColorA, fishColorB, colorPlayer, dustColor;
PVector target, cameraOffset;
float centerToCornerDistance, farSpawnDistance;
Fish player;
ArrayList<Fish> allFish = new ArrayList<Fish>();
ArrayList<Fish> fishToRemove = new ArrayList<Fish>();
boolean targetActive = false;
// LazyGui gui;
/*
public static void main(String[] args) {
PApplet.main(java.lang.invoke.MethodHandles.lookup().lookupClass());
}
public void settings(){
fullScreen(P3D);
orientation(PORTRAIT);
}
*/
public void setup() {
// gui = new LazyGui(this);
fullScreen(P3D);
orientation(PORTRAIT);
cameraOffset = new PVector(width * .5f, height * .5f);
player = new Fish();
player.pos = new PVector(0, 0);
player.spd = new PVector(minSpeed, 0);
allFish.add(player);
target = new PVector(player.pos.x, player.pos.y);
initColorsWithoutGui();
}
void initColorsWithoutGui() {
colorPlayer = 0xFFFFFFFF;
fishColorA = 0xFF8364FF;
fishColorB = 0xFFFF9AA1;
dustColor = color(128, 128);
}
public void draw() {
drawSliders();
centerToCornerDistance = dist(0, 0, width * .5f, height * .5f);
farSpawnDistance = centerToCornerDistance * 1.5f;
background(0);
pushMatrix();
translate(cameraOffset.x, cameraOffset.y);
updateMouseTarget();
movePlayerTowardsTarget();
moveCameraTowardsPlayer();
spawnNewDust();
drawDust();
drawAllFish();
spawnNewFish();
popMatrix();
}
void spawnNewDust() {
while (dusts.size() < dustCount) {
// base location is the cameraOffset
PVector pos = new PVector(
-cameraOffset.x + random(-farSpawnDistance, farSpawnDistance),
-cameraOffset.y + random(-farSpawnDistance, farSpawnDistance)
);
dusts.add(new Dust(pos));
}
}
void drawDust() {
for (Dust d : dusts) {
d.update();
if (d.toRemove) {
dustsToRemove.add(d);
} else {
d.draw();
}
}
dusts.removeAll(dustsToRemove);
dustsToRemove.clear();
}
void spawnNewFish() {
while (allFish.size() < fishCount) {
allFish.add(new Fish());
}
}
void drawSliders() {
/*
fishCount = gui.sliderInt("fish count", 50);
minFishSize = gui.slider("min fish size", 10);
maxFishSize = gui.slider("max fish size", 20);
gui.pushFolder("flocking");
alignRadius = gui.slider("align radius", alignRadius);
avoidRadius = gui.slider("avoid radius", avoidRadius);
centralizeRadius = gui.slider("centralize radius", centralizeRadius);
alignWeight = gui.slider("align weight", alignWeight);
avoidWeight = gui.slider("avoid weight", avoidWeight);
centralizeWeight = gui.slider("centralize weight", centralizeWeight);
maxSpeed = gui.slider("max speed", maxSpeed);
minSpeed = gui.slider("min speed", minSpeed);
globalDrag = gui.slider("global drag", globalDrag);
randomMag = gui.slider("random mag", randomMag, 0, 20);
accSmoothing = gui.slider("acc smoothing", 0.1f, 0, 1);
gui.popFolder();
gui.pushFolder("dust");
dustCount = gui.sliderInt("dust count", dustCount);
dustMinLife = gui.slider("dust min life", dustMinLife, 0, 500);
dustMaxLife = gui.slider("dust max life", dustMaxLife, 0, 500);
dustMinSize = gui.slider("dust min size", dustMinSize, 0, 10);
dustMaxSize = gui.slider("dust max size", dustMaxSize, 0, 20);
gui.popFolder();
gui.pushFolder("visuals");
strokeWeightPlayer = gui.slider("weight (p)", 1);
strokeWeightFish = gui.slider("weight (f)", 1);
colorPlayer = gui.colorPicker("stroke (p)", color(255, 255, 255, 150)).hex;
fishColorA = gui.colorPicker("fish color A", fishColorA).hex; // 0xFF8364FF
fishColorB = gui.colorPicker("fish color B", fishColorB).hex; // 0xFFFF9AA1
dustColor = gui.colorPicker("dust color", color(128, 255)).hex;
gui.popFolder();
*/
}
void drawAllFish() {
noFill();
for (Fish f : allFish) {
f.update();
if (f != player && f.toRemove) {
fishToRemove.add(f);
}
}
allFish.removeAll(fishToRemove);
fishToRemove.clear();
for (Fish f : allFish) {
if (f == player) {
stroke(colorPlayer);
strokeWeight(strokeWeightPlayer);
} else {
stroke(lerpColor(fishColorA, fishColorB, f.colorRand));
strokeWeight(strokeWeightFish);
}
f.drawFish();
}
}
void updateMouseTarget() {
if (mousePressed && isMouseOutsideGui()) {
target.x = mouseX - cameraOffset.x;
target.y = mouseY - cameraOffset.y;
targetActive = true;
}
if (dist(player.pos.x, player.pos.y, target.x, target.y) < 10) {
targetActive = false;
}
}
boolean isMouseOutsideGui(){
return true;
// gui.isMouseOutsideGui()
}
float distanceFromPlayerToTarget() {
return (target.x == player.pos.x && target.y == player.pos.y) ? 0 : dist(player.pos.x, player.pos.y, target.x, target.y);
}
void movePlayerTowardsTarget() {
if (!targetActive) {
return;
}
player.spd.x = lerp(player.pos.x, target.x, .1f);
player.spd.y = lerp(player.pos.y, target.y, .1f);
player.spd.sub(player.pos);
player.spd.limit(10);
player.spd.mult(.9f);
player.pos.add(player.spd);
}
void moveCameraTowardsPlayer() {
// PVector playerCoordinateOnScreen = new PVector(modelX(player.pos.x, player.pos.y, player.pos.z), modelY(player.pos.x, player.pos.y, player.pos.z), 0);
cameraOffset.x = lerp(cameraOffset.x, width * .5f - player.pos.x, .1f);
cameraOffset.y = lerp(cameraOffset.y, height * .5f - player.pos.y, .1f);
}
boolean isPointInRect(float px, float py, float rx, float ry, float rw, float rh) {
return px >= rx && px <= rx + rw && py >= ry && py <= ry + rh;
}
class Fish {
PVector pos;
PVector spd = new PVector();
PVector acc = new PVector();
float radius;
boolean toRemove = false;
PVector centralizeAvgPos = new PVector();
PVector fishAvoidance = new PVector();
PVector alignableAvgSpd = new PVector();
float animationTime = 0;
float colorRand;
Fish() {
if (player == null) {
pos = new PVector(width * .5f, height * .5f);
radius = 20;
} else {
pos = randomPositionOffscreenInFrontOfPlayer();
radius = random(minFishSize, maxFishSize);
colorRand = random(1);
}
}
void update() {
acc.mult(0);
centralizeAvgPos.mult(0);
alignableAvgSpd.mult(0);
fishAvoidance.mult(0);
int alignableFishCount = 0;
int centralizableFishCount = 0;
debugFishRanges();
for (Fish f : allFish) {
if (this.equals(f)) {
continue;
}
float d = dist(f.pos.x, f.pos.y, pos.x, pos.y);
if (d < alignRadius) {
alignableAvgSpd.add(f.spd);
alignableFishCount++;
}
if (d < avoidRadius) {
// friendAvoidance.add(PVector.sub(pos, f.pos).normalize());
float avoidDist = 1 - norm(d, 0, avoidRadius);
float avoidCloserMore = 1 / max(avoidDist, 0.01f); // Prevent division by zero
PVector fishAvoidVector = PVector.sub(pos, f.pos).normalize().mult(avoidCloserMore);
fishAvoidance.add(fishAvoidVector);
}
if (d < centralizeRadius) {
centralizeAvgPos.add(f.pos); // Prevent division by zero
centralizableFishCount++;
}
}
if (centralizableFishCount > 0) {
centralizeAvgPos.div(centralizableFishCount);
}
if (alignableFishCount > 0) {
alignableAvgSpd.div(alignableFishCount);
}
PVector centralize = PVector.sub(centralizeAvgPos, pos).normalize().mult(centralizeWeight);
PVector avoid = fishAvoidance.normalize().mult(avoidWeight);
PVector align = PVector.sub(alignableAvgSpd, spd).normalize().mult(alignWeight);
debugFishVectors(avoid, align, centralize);
acc.add(centralize);
acc.add(avoid);
acc.add(align);
acc.add(PVector.random2D().mult(randomMag));
spd.lerp(acc, accSmoothing); // Smoothly apply acceleration to speed
spd.mult(globalDrag);
spd.limit(maxSpeed);
if (spd.mag() < minSpeed) {
spd.setMag(minSpeed);
}
pos.add(spd);
}
void debugFishRanges() {
/*
gui.pushFolder("debug");
if (gui.toggle("avoid circle")) {
pushStyle();
stroke(255, 0, 0, 100);
noFill();
ellipse(pos.x, pos.y, avoidRadius * 2, avoidRadius * 2);
popStyle();
}
if (gui.toggle("align circle")) {
pushStyle();
stroke(0, 255, 0, 100);
noFill();
ellipse(pos.x, pos.y, alignRadius * 2, alignRadius * 2);
popStyle();
}
if (gui.toggle("centralize circle")) {
pushStyle();
stroke(0, 0, 255, 100);
noFill();
ellipse(pos.x, pos.y, centralizeRadius * 2, centralizeRadius * 2);
popStyle();
}
gui.popFolder();
*/
}
void debugFishVectors(PVector avoid, PVector align, PVector centralize) {
/*
gui.pushFolder("debug");
pushStyle();
pushMatrix();
strokeWeight(2);
translate(pos.x, pos.y);
if (gui.toggle("avoid vector")) {
stroke(255, 0, 0, 150);
line(0, 0, avoid.x * avoidRadius, avoid.y * avoidRadius);
}
if (gui.toggle("align vector")) {
stroke(0, 255, 0, 150);
line(0, 0, align.x * alignRadius, align.y * alignRadius);
}
if (gui.toggle("centralize vector")) {
stroke(0, 0, 255, 150);
line(0, 0, centralize.x * centralizeRadius, centralize.y * centralizeRadius);
}
popMatrix();
popStyle();
gui.popFolder();
*/
}
boolean isPlayer() {
return this == player;
}
void drawFish() {
pushMatrix();
translate(pos.x, pos.y);
rotate(spd.heading());
float distanceToPlayer = dist(pos.x, pos.y, player.pos.x, player.pos.y);
if (distanceToPlayer > farSpawnDistance && isBehindPlayer(pos)) {
this.toRemove = true;
}
beginShape(TRIANGLE_STRIP);
int vertexCount = 12;
animationTime += map(spd.mag(), minSpeed, maxSpeed, animationSpeedIdle, animationSpeedFast);
for (float i = -vertexCount * .25f; i < vertexCount; i++) {
boolean tail = i < 0;
float iN = map(i, 0, vertexCount - 1, 0, 1);
float x = map(iN, 0, 1, -radius * 2, radius);
float y0 = radius * .5f * sin(iN * 3.f) + radius * .1f * sin(iN * 2 - animationTime * 25) * (tail ? 2 : 1);
float y1 = -radius * .5f * sin(iN * 3.f) + radius * .1f * sin(iN * 2 - animationTime * 25) * (tail ? 2 : 1);
vertex(x, y0);
vertex(x, y1);
}
endShape(CLOSE);
popMatrix();
}
boolean isBehindPlayer(PVector pos) {
float angleToPlayer = atan2(player.pos.y - pos.y, player.pos.x - pos.x);
float normalizedAngleToPlayer = normalizeAngle(angleToPlayer, PI);
float normalizedPlayerHeading = normalizeAngle(player.spd.heading(), PI);
float angleToPlayerVsHeading = normalizeAngle(normalizedAngleToPlayer - normalizedPlayerHeading, 0);
return abs(angleToPlayerVsHeading) < HALF_PI;
}
PVector randomPositionOffscreenInFrontOfPlayer() {
float angle = random(player.spd.heading() - HALF_PI, player.spd.heading() + HALF_PI);
float distance = random(centerToCornerDistance, farSpawnDistance);
return new PVector(player.pos.x + distance * cos(angle), player.pos.y + distance * sin(angle));
}
public float normalizeAngle(float a, float center) {
return a - TWO_PI * floor((a + PI - center) / TWO_PI);
}
boolean isPointInRect(float px, float py, float rx, float ry, float rw, float rh) {
return px >= rx && px <= rx + rw && py >= ry && py <= ry + rh;
}
boolean isPointInRectCenterMode(float px, float py, float rx, float ry, float rw, float rh) {
return px >= rx - rw * .5f && px <= rx + rw * .5f && py >= ry - rh * .5f && py <= ry + rh * .5f;
}
float sign(float value) {
return (value < 0) ? -1 : 1;
}
}
class Dust {
PVector pos;
float radius;
float lifeTimeTotal, lifeTime;
boolean toRemove = false;
Dust(PVector pos) {
this.pos = pos;
this.radius = random(dustMinSize, dustMaxSize);
this.lifeTimeTotal = random(dustMinLife, dustMaxLife);
lifeTime = lifeTimeTotal;
}
void update() {
lifeTime -= 1;
if (lifeTime <= 0) {
toRemove = true;
}
}
void draw() {
float alpha;
if (lifeTime > lifeTimeTotal * 0.66f) { // First third of the lifetime
alpha = map(lifeTime, lifeTimeTotal, lifeTimeTotal * 0.66f, 0, 255);
} else if (lifeTime < lifeTimeTotal * 0.33f) { // Last third of the lifetime
alpha = map(lifeTime, lifeTimeTotal * 0.33f, 0, 255, 0);
} else { // Middle third of the lifetime
alpha = 255;
}
fill(dustColor, alpha);
noStroke();
ellipse(pos.x, pos.y, radius * 2, radius * 2);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment