Skip to content

Instantly share code, notes, and snippets.

@KableM
Created February 12, 2019 03:03
Show Gist options
  • Save KableM/6dd5c552093223a0b4332d98833de037 to your computer and use it in GitHub Desktop.
Save KableM/6dd5c552093223a0b4332d98833de037 to your computer and use it in GitHub Desktop.
import javax.swing.*;
import java.awt.*;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
*
* Run on the command line with
* > javac FishPond.java
* > java FishPond
*
* @author @kable_codes
*/
public class FishPond {
private final int width;
private final int height;
private final Color backgroundColor;
private float scale;
private float radiusDecay;
private Random random = new Random(2);
private List<Fish> fish = new ArrayList<>();
private float maxDistanceFromCenter;
private float maxInfluenceDistance;
public FishPond(int width, int height) {
this.width = width;
this.height = height;
scale = width / 256f;
radiusDecay = 0.005f * scale;
backgroundColor = new Color(0x112233);
maxDistanceFromCenter = (float) Math.sqrt(width * width + height * height) * 0.8f;
maxInfluenceDistance = 100000 * scale;
for (int s = 0; s < 2; s++) {
spawnSchool();
}
}
private void spawnSchool() {
float fishSpread = 50 * scale;
float spawnRadius = (float) (Math.sqrt(width * width + height * height) / 2 + fishSpread * 2);
float spawnAngle = (float) (random.nextFloat() * Math.PI * 2);
float schoolX = (float) (width / 2 + Math.cos(spawnAngle) * spawnRadius);
float schoolY = (float) (height / 2 + Math.sin(spawnAngle) * spawnRadius);
float targetX = random.nextFloat() * width;
float targetY = random.nextFloat() * height;
float schoolAngle = (float) Math.atan2(targetY - schoolY, targetX - schoolX);
fish.add(new Fish(schoolX, schoolY, (random.nextFloat() / 40f + 0.01f) * scale, schoolAngle, 3 * scale, true));
int schoolSize = random.nextInt(80) + 20;
for (int i = 0; i < schoolSize; i++) {
fish.add(new Fish(schoolX + random.nextFloat() * fishSpread,
schoolY + random.nextFloat() * fishSpread,
random.nextFloat() * scale / 40f,
schoolAngle,
(random.nextFloat() * 1 + 0.5f) * scale, false));
}
}
protected void step(float dt) {
fish.forEach(fish -> fish.step(dt));
fish.removeIf(fish -> fish.distanceFromCenter > maxDistanceFromCenter);
if (random.nextFloat() < dt * 1 / 500) {
spawnSchool();
}
}
protected void draw(Graphics2D graphics) {
Rectangle2D background = new Rectangle2D.Float(0, 0, width, height);
graphics.setColor(backgroundColor);
graphics.fill(background);
fish.forEach(fish -> {
if (!fish.leader) {
fish.draw(graphics);
}
});
fish.forEach(fish -> {
if (fish.leader) {
fish.draw(graphics);
}
});
}
class Fish {
private float x;
private float y;
private float speed;
private float angle;
private float dAngle = 0;
private float size;
private boolean leader;
private float distanceFromCenter;
private float timer = random.nextFloat() * 1000f;
private float pAnglePeriod = 700f;
private float magnitude = scale * 1f;
private float pSpeed = scale / 40f;
private List<Particle> particles = new ArrayList<>();
private Color color;
public Fish(float x, float y, float speed, float angle, float size, boolean leader) {
this.x = x;
this.y = y;
this.speed = speed;
this.angle = angle;
this.size = size;
this.leader = leader;
if (leader) {
color = new Color(0xFFFF00);
} else {
color = new Color(0x99FFFCF2, true);
}
}
private void step(float dt) {
particles.forEach(particle -> particle.step(dt));
particles.removeIf(particle -> particle.finished);
timer += dt;
if (fish.size() > 1 && !leader) {
float daTotal = 0;
float dsTotal = 0;
float weightTotal = 0;
for (Fish otherFish : fish) {
if (otherFish.leader) {
float xd = otherFish.x - x;
float yd = otherFish.y - y;
float distance = (float) Math.sqrt(xd * xd + yd * yd);
if (distance < maxInfluenceDistance) {
float weight = 1 / (distance * distance * scale);
daTotal += normalize(otherFish.angle - angle) * weight;
dsTotal += normalize(otherFish.speed - speed) * weight;
weightTotal += weight;
}
}
}
float daAverage = daTotal / weightTotal;
float dsAverage = dsTotal / weightTotal;
angle = normalize(angle + daAverage / 10f);
speed = speed + dsAverage / 10f;
}
if (leader) {
angle += dAngle * dt;
if (random.nextFloat() < dt * 1 / 2000) {
// change speed
speed = (random.nextFloat() / 20f + 0.01f) * scale;
dAngle = (float) ((random.nextFloat() - 0.5f) * Math.toRadians(0.1));
}
}
x += Math.cos(angle) * speed * dt;
y += Math.sin(angle) * speed * dt;
float cxd = width / 2f - x;
float cyd = height / 2f - y;
distanceFromCenter = (float) Math.sqrt(cxd * cxd + cyd * cyd);
float pOffsetAngle = (float) (angle - Math.PI / 2);
// pAnglePeriod = 20 / speed;
float pOffset = (float) (Math.sin(2 * Math.PI * timer / pAnglePeriod) * magnitude);
float pX = (float) (x + Math.cos(pOffsetAngle) * pOffset);
float pY = (float) (y + Math.sin(pOffsetAngle) * pOffset);
float pAngle = (float) (angle + Math.PI);
float xSpeed = (float) (Math.cos(pAngle) * pSpeed);
float ySpeed = (float) (Math.sin(pAngle) * pSpeed);
Particle particle = new Particle(pX, pY, xSpeed, ySpeed, size, color, leader);
particles.add(particle);
if (leader) {
pX = x;
pY = y;
pAngle = (float) (angle + Math.PI + Math.PI / 4);
xSpeed = (float) (Math.cos(pAngle) * pSpeed);
ySpeed = (float) (Math.sin(pAngle) * pSpeed);
particle = new Particle(pX, pY, xSpeed, ySpeed, size * 3f / 4, color, true);
particles.add(particle);
pAngle = (float) (angle + Math.PI - Math.PI / 4);
xSpeed = (float) (Math.cos(pAngle) * pSpeed);
ySpeed = (float) (Math.sin(pAngle) * pSpeed);
particle = new Particle(pX, pY, xSpeed, ySpeed, size * 3f / 4, color, true);
particles.add(particle);
}
}
private void draw(Graphics2D graphics) {
particles.forEach(particle -> particle.draw(graphics));
}
}
private float normalize(float angle) {
while (angle > Math.PI) {
angle -= Math.PI * 2;
}
while (angle < -Math.PI) {
angle += Math.PI * 2;
}
return angle;
}
class Particle {
private float x;
private float y;
private float xSpeed;
private float ySpeed;
private float radius;
private Color color;
private boolean leader;
private boolean finished = false;
public Particle(float x, float y, float xSpeed, float ySpeed, float radius, Color color, boolean leader) {
this.x = x;
this.y = y;
this.xSpeed = xSpeed;
this.ySpeed = ySpeed;
this.radius = radius;
this.color = color;
this.leader = leader;
}
private void step(float dt) {
if (!finished) {
if (leader) {
color = setHue(color, getHue(color) - 0.07f / 1000 * dt);
} else {
color = setSaturation(color, getSaturation(color) + 1f / 500 * dt);
}
x += xSpeed * dt;
y += ySpeed * dt;
radius -= radiusDecay * dt;
if (radius <= 0) {
finished = true;
}
}
}
private void draw(Graphics2D graphics) {
graphics.setColor(color);
Ellipse2D circle = new Ellipse2D.Float(x - radius, y - radius, radius * 2, radius * 2);
graphics.fill(circle);
}
}
private float getSaturation(Color color) {
return Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null)[1];
}
private Color setSaturation(Color color, float saturation) {
float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null);
return new Color(Color.HSBtoRGB(hsb[0], saturation, hsb[2]));
}
private float getHue(Color color) {
return Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null)[0];
}
private Color setHue(Color color, float hue) {
float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null);
return new Color(Color.HSBtoRGB(hue, hsb[1], hsb[2]));
}
public static void main(String[] args) {
int size = 512;
FishPond fishPond = new FishPond(size, size);
Preview preview = new Preview("FishPond", fishPond, size, size, 30, true);
preview.start();
}
static class Preview extends JFrame {
private final FishPond fishPond;
private final int width;
private final int height;
private Timer timer;
public Preview(String title,
FishPond fishPond,
int width,
int height,
int frameRate,
boolean anitalias) throws HeadlessException {
super(title);
this.width = width;
this.height = height;
this.fishPond = fishPond;
float delay = 1000f / frameRate;
timer = new Timer((int) delay, e -> step(delay));
JPanel container = new JPanel() {
@Override
protected void paintComponent(Graphics g) {
if (anitalias) {
RenderingHints hints = new RenderingHints(null);
hints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
((Graphics2D) g).setRenderingHints(hints);
}
fishPond.draw((Graphics2D) g);
}
};
getContentPane().add(BorderLayout.CENTER, container);
}
public void start() {
EventQueue.invokeLater(() -> {
setLocationRelativeTo(null);
setDefaultCloseOperation(EXIT_ON_CLOSE);
timer.start();
setVisible(true);
setPreferredSize(new Dimension(width, height + getInsets().top));
pack();
});
}
private void step(float dt) {
fishPond.step(dt);
repaint();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment