Created
February 12, 2019 03:03
-
-
Save KableM/6dd5c552093223a0b4332d98833de037 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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