diff --git a/pom.xml b/pom.xml
index 39c3428..2af83d2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,6 +28,7 @@
0.0.4
3.8.1
+ ThreadRipper
launcher
nl.andrewlalis.threadripper.ThreadRipperApplication
diff --git a/src/main/java/nl/andrewlalis/threadripper/ThreadRipperApplication.java b/src/main/java/nl/andrewlalis/threadripper/ThreadRipperApplication.java
index a039366..e95b392 100644
--- a/src/main/java/nl/andrewlalis/threadripper/ThreadRipperApplication.java
+++ b/src/main/java/nl/andrewlalis/threadripper/ThreadRipperApplication.java
@@ -1,17 +1,20 @@
package nl.andrewlalis.threadripper;
import javafx.application.Application;
-import javafx.scene.Group;
+import javafx.beans.value.ChangeListener;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
-import javafx.scene.canvas.GraphicsContext;
-import javafx.scene.paint.Color;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j;
import nl.andrewlalis.threadripper.engine.ParticleChamber;
import nl.andrewlalis.threadripper.engine.Vec2;
-import nl.andrewlalis.threadripper.particle.Particle;
import nl.andrewlalis.threadripper.particle.ParticleFactory;
+import nl.andrewlalis.threadripper.render.ParticleChamberRenderer;
/**
* Main application starting point.
@@ -20,46 +23,34 @@ import nl.andrewlalis.threadripper.particle.ParticleFactory;
public class ThreadRipperApplication extends Application {
private final Canvas canvas;
+
private final ParticleChamber chamber;
+ private final Thread chamberThread;
+ private final ParticleChamberRenderer renderer;
+ private final Thread renderThread;
public ThreadRipperApplication() {
- this.canvas = new Canvas(1000, 1000);
- this.chamber = new ParticleChamber(canvas);
+ this.canvas = new Canvas(800, 800);
+ this.chamber = new ParticleChamber();
+ this.renderer = new ParticleChamberRenderer(chamber, canvas);
+
+
ParticleFactory factory = new ParticleFactory(
- 10000.0,
- 10000000.0,
- 0,
- 0,
- new Vec2(350, 250),
- new Vec2(450, 400),
- new Vec2(10, -2),
- new Vec2(25, 2)
+ 0.1, 100000000000000.0,
+ 0, 0,
+ 0.5, 5,
+ new Vec2(0, 0), new Vec2(800, 800),
+ new Vec2(-50, -50), new Vec2(50, 50)
);
- for (int i = 0; i < 100; i++) {
+ for (int i = 0; i < 50; i++) {
this.chamber.addParticle(factory.build());
}
-// this.chamber.addParticle(new Particle(new Vec2(400, 298), new Vec2(19, 0), 100000.0, 0.0));
-// this.chamber.addParticle(new Particle(new Vec2(400, 300), new Vec2(20, 0), 10000000000.0, 0.0));
- this.chamber.addParticle(new Particle(new Vec2(400, 500), new Vec2(0, 0), 1000000000000000.0, 0.0));
+ this.chamberThread = new Thread(this.chamber);
+ this.chamberThread.start();
-// ParticleFactory factory1 = new ParticleFactory(
-// 1000000000.0,
-// 10000000000000.0,
-// 0,
-// 0,
-// new Vec2(700, 700),
-// new Vec2(900, 900),
-// new Vec2(0, 0),
-// new Vec2(0, 0)
-// );
-// for (int i = 0; i < 500; i++) {
-// this.chamber.addParticle(factory1.build());
-// }
-
- Thread particleChamberThread = new Thread(chamber);
- particleChamberThread.setName("ParticleChamber");
- particleChamberThread.start();
+ this.renderThread = new Thread(this.renderer);
+ this.renderThread.start();
}
public static void main(String[] args) {
@@ -71,22 +62,47 @@ public class ThreadRipperApplication extends Application {
public void start(Stage stage) throws Exception {
stage.setTitle("ThreadRipper");
- GraphicsContext gc = this.canvas.getGraphicsContext2D();
+ BorderPane borderPane = new BorderPane();
+ borderPane.setCenter(this.canvas);
- gc.setFill(Color.GREEN);
- gc.fillOval(10, 60, 30, 30);
+ TextField simRateField = new TextField(Double.toString(this.chamber.getSimulationRate()));
+ Button simRateUpdate = new Button("Apply");
+ simRateUpdate.setOnMouseClicked(mouseEvent -> {
+ double simRate = this.chamber.getSimulationRate();
+ try {
+ simRate = Double.parseDouble(simRateField.getText().trim());
+ } catch (NumberFormatException e) {
+ simRateField.clear();
+ }
+ this.chamber.setSimulationRate(simRate);
+ });
+ HBox bottomPanel = new HBox(
+ new Label("Simulation Rate"),
+ simRateField,
+ simRateUpdate
+ );
+ borderPane.setBottom(bottomPanel);
+
+ Scene scene = new Scene(borderPane);
+
+ ChangeListener sceneSizeListener = (observable, oldValue, newValue) -> {
+ this.canvas.setWidth(scene.getWidth());
+ this.canvas.setHeight(scene.getHeight());
+ };
+ scene.widthProperty().addListener(sceneSizeListener);
+ scene.heightProperty().addListener(sceneSizeListener);
- Group root = new Group();
- root.getChildren().add(this.canvas);
- Scene scene = new Scene(root);
stage.setScene(scene);
+ stage.setMaximized(true);
stage.show();
}
@Override
public void stop() throws Exception {
this.chamber.setRunning(false);
+ this.chamberThread.join();
+ this.renderer.setRunning(false);
+ this.renderThread.join();
super.stop();
- System.exit(0);
}
}
diff --git a/src/main/java/nl/andrewlalis/threadripper/engine/ParticleChamber.java b/src/main/java/nl/andrewlalis/threadripper/engine/ParticleChamber.java
index ee1d4ab..a9412c7 100644
--- a/src/main/java/nl/andrewlalis/threadripper/engine/ParticleChamber.java
+++ b/src/main/java/nl/andrewlalis/threadripper/engine/ParticleChamber.java
@@ -1,9 +1,5 @@
package nl.andrewlalis.threadripper.engine;
-import javafx.application.Platform;
-import javafx.scene.canvas.Canvas;
-import javafx.scene.canvas.GraphicsContext;
-import javafx.scene.paint.Color;
import lombok.extern.slf4j.Slf4j;
import nl.andrewlalis.threadripper.particle.Particle;
@@ -13,28 +9,28 @@ import java.util.concurrent.*;
@Slf4j
public class ParticleChamber implements Runnable {
private static final int DEFAULT_THREAD_POOL = 100;
- private static final double DEFAULT_UPDATE_FPS = 60;
+ private static final double DEFAULT_UPDATES_PER_SECOND = 60;
private final Set particles;
+ private double simulationRate = 1.0;
+ private boolean allowCollision = true;
+
private int threadCount;
- private ExecutorService executorService;
- private CompletionService particleUpdateService;
+ private final ExecutorService executorService;
+ private final CompletionService particleUpdateService;
private boolean running;
private double updateFps;
+ private double secondsSinceLastUpdate;
- private final Canvas canvas;
-
- public ParticleChamber(Canvas canvas) {
+ public ParticleChamber() {
this.particles = new HashSet<>();
this.threadCount = DEFAULT_THREAD_POOL;
this.executorService = Executors.newFixedThreadPool(this.threadCount);
this.particleUpdateService = new ExecutorCompletionService<>(this.executorService);
- this.updateFps = DEFAULT_UPDATE_FPS;
-
- this.canvas = canvas;
+ this.updateFps = DEFAULT_UPDATES_PER_SECOND;
}
/**
@@ -49,6 +45,14 @@ public class ParticleChamber implements Runnable {
this.running = running;
}
+ public synchronized void setSimulationRate(double simulationRate) {
+ this.simulationRate = simulationRate;
+ }
+
+ public synchronized void setAllowCollision(boolean allowCollision) {
+ this.allowCollision = allowCollision;
+ }
+
@Override
public void run() {
this.running = true;
@@ -61,6 +65,7 @@ public class ParticleChamber implements Runnable {
final long elapsedMilliseconds = currentTimeMilliseconds - previousTimeMilliseconds;
millisecondsSinceLastUpdate += elapsedMilliseconds;
+ this.secondsSinceLastUpdate = millisecondsSinceLastUpdate / 1000.0;
final double millisecondsPerFrame = 1000.0 / this.updateFps;
@@ -68,12 +73,12 @@ public class ParticleChamber implements Runnable {
final double secondsSinceLastUpdate = millisecondsSinceLastUpdate / 1000.0;
millisecondsSinceLastUpdate = 0L;
//log.info("Updating particles after {} seconds elapsed.", secondsSinceLastUpdate);
- this.updateParticles(secondsSinceLastUpdate);
- this.drawParticles();
+ this.updateParticles(secondsSinceLastUpdate * this.simulationRate);
}
previousTimeMilliseconds = currentTimeMilliseconds;
}
+ this.executorService.shutdown();
log.info("Particle chamber stopped.");
}
@@ -84,7 +89,7 @@ public class ParticleChamber implements Runnable {
private void updateParticles(double deltaTime) {
// First submit a new callable task for each particle.
for (Particle particle : this.particles) {
- this.particleUpdateService.submit(new ParticleUpdater(particle, this.particles));
+ this.particleUpdateService.submit(new ParticleUpdater(particle, this.particles, this.allowCollision));
}
int updatesReceived = 0;
@@ -103,28 +108,41 @@ public class ParticleChamber implements Runnable {
}
}
+ Set particlesToRemove = new HashSet<>(this.particles.size());
+ Set particlesToAdd = new HashSet<>(this.particles.size());
// Implement the updates for each particle.
for (ParticleUpdate update : updates) {
update.getFocusParticle().updateVelocity(update.getAcceleration(), deltaTime);
update.getFocusParticle().updatePosition(deltaTime);
- //log.info("Particle updated: {}", update.getFocusParticle().toString());
+ if (!update.getCollidesWith().isEmpty() && !particlesToRemove.contains(update.getFocusParticle())) {
+ particlesToRemove.addAll(update.getCollidesWith());
+ particlesToRemove.add(update.getFocusParticle());
+ // Create a new particle as combination.
+ Particle p = update.getFocusParticle();
+ for (Particle collided : update.getCollidesWith()) {
+ p = p.combine(collided);
+ }
+ particlesToAdd.add(p);
+ }
}
+
+ this.particles.removeAll(particlesToRemove);
+ this.particles.addAll(particlesToAdd);
}
- /**
- * Draws all particles on the canvas.
- */
- private void drawParticles() {
- Platform.runLater(() -> {
- GraphicsContext gc = this.canvas.getGraphicsContext2D();
- gc.setFill(Color.WHITE);
- gc.clearRect(0, 0, this.canvas.getWidth(), this.canvas.getHeight());
- gc.setFill(Color.BLACK);
- gc.setStroke(Color.BLUE);
- for (Particle particle : this.particles) {
- Vec2 pos = particle.getPosition();
- gc.fillOval(pos.getX() - 3, pos.getY() - 3, 6, 6);
- }
- });
+ public double getSecondsSinceLastUpdate() {
+ return this.secondsSinceLastUpdate;
+ }
+
+ public double getSimulationRate() {
+ return simulationRate;
+ }
+
+ public Set getCopyOfParticles() {
+ Set set = new HashSet<>(this.particles.size());
+ for (Particle p : this.particles) {
+ set.add(p.getCopy());
+ }
+ return set;
}
}
diff --git a/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdate.java b/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdate.java
index 7b117ee..b7169e2 100644
--- a/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdate.java
+++ b/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdate.java
@@ -3,6 +3,8 @@ package nl.andrewlalis.threadripper.engine;
import lombok.Getter;
import nl.andrewlalis.threadripper.particle.Particle;
+import java.util.Set;
+
/**
* Describes an update to a particle's motion.
*/
@@ -19,8 +21,14 @@ public class ParticleUpdate {
*/
private final Vec2 acceleration;
- public ParticleUpdate(Particle focusParticle, Vec2 acceleration) {
+ /**
+ * A set of particles that this one has collided with.
+ */
+ private final Set collidesWith;
+
+ public ParticleUpdate(Particle focusParticle, Vec2 acceleration, Set collidesWith) {
this.focusParticle = focusParticle;
this.acceleration = acceleration;
+ this.collidesWith = collidesWith;
}
}
diff --git a/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdater.java b/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdater.java
index 17b7def..d1698fd 100644
--- a/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdater.java
+++ b/src/main/java/nl/andrewlalis/threadripper/engine/ParticleUpdater.java
@@ -3,6 +3,7 @@ package nl.andrewlalis.threadripper.engine;
import lombok.extern.slf4j.Slf4j;
import nl.andrewlalis.threadripper.particle.Particle;
+import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Callable;
@@ -14,24 +15,40 @@ import java.util.concurrent.Callable;
public class ParticleUpdater implements Callable {
private final Particle focusParticle;
private final Set particles;
+ private final boolean allowCollision;
- public ParticleUpdater(Particle focusParticle, Set particles) {
+ public ParticleUpdater(
+ Particle focusParticle,
+ Set particles,
+ boolean allowCollision
+ ) {
this.focusParticle = focusParticle;
this.particles = particles;
+ this.allowCollision = allowCollision;
}
@Override
public ParticleUpdate call() throws Exception {
double accelerationX = 0L;
double accelerationY = 0L;
+ Set collidesWith = new HashSet<>();
for (Particle particle : this.particles) {
if (!particle.equals(this.focusParticle)) {
Vec2 partialAcceleration = this.computeAcceleration(particle);
accelerationX += partialAcceleration.getX();
accelerationY += partialAcceleration.getY();
+
+ // Collision detection:
+ final double distance = this.focusParticle.getPosition().distance(particle.getPosition());
+ if (
+ Math.abs(this.focusParticle.getRadius() - particle.getRadius()) <= distance
+ && distance <= (this.focusParticle.getRadius() + particle.getRadius())
+ ) {
+ collidesWith.add(particle);
+ }
}
}
- return new ParticleUpdate(this.focusParticle, new Vec2(accelerationX, accelerationY));
+ return new ParticleUpdate(this.focusParticle, new Vec2(accelerationX, accelerationY), collidesWith);
}
/**
@@ -51,17 +68,13 @@ public class ParticleUpdater implements Callable {
final double gravityNewtons = Constants.G * (this.focusParticle.getMass() * other.getMass()) / Math.pow(radius, 2);
final double gravityAcceleration = gravityNewtons / this.focusParticle.getMass();
final Vec2 gravityAccelerationVector = Vec2.fromPolar(gravityAcceleration, angle);
-// log.info(
-// "Force of gravity between particles {} and {} is {} Newtons.\nCauses {} m/s^2 acceleration on particle {}.",
-// this.focusParticle.getId(),
-// other.getId(),
-// gravityNewtons,
-// gravityAcceleration,
-// this.focusParticle.getId()
-// );
final double emNewtons = Constants.Ke * (this.focusParticle.getCharge() * other.getCharge()) / Math.pow(radius, 2);
- final double emAcceleration = emNewtons / this.focusParticle.getMass();
+ final boolean isRepulsion = emNewtons < 0.0;
+ double emAcceleration = emNewtons / this.focusParticle.getMass();
+ if (isRepulsion) {
+ emAcceleration *= -1.0;
+ }
final Vec2 emAccelerationVector = Vec2.fromPolar(emAcceleration, angle);
final Vec2 totalAcceleration = gravityAccelerationVector.add(emAccelerationVector);
diff --git a/src/main/java/nl/andrewlalis/threadripper/engine/Vec2.java b/src/main/java/nl/andrewlalis/threadripper/engine/Vec2.java
index d316d31..9fd8d5e 100644
--- a/src/main/java/nl/andrewlalis/threadripper/engine/Vec2.java
+++ b/src/main/java/nl/andrewlalis/threadripper/engine/Vec2.java
@@ -50,4 +50,8 @@ public class Vec2 {
public String toString() {
return String.format("[%f, %f]", this.getX(), this.getY());
}
+
+ public Vec2 getCopy() {
+ return new Vec2(this.getX(), this.getY());
+ }
}
diff --git a/src/main/java/nl/andrewlalis/threadripper/particle/Particle.java b/src/main/java/nl/andrewlalis/threadripper/particle/Particle.java
index 2bc3d6d..b34c501 100644
--- a/src/main/java/nl/andrewlalis/threadripper/particle/Particle.java
+++ b/src/main/java/nl/andrewlalis/threadripper/particle/Particle.java
@@ -15,7 +15,7 @@ public class Particle {
/**
* Unique id for this particle.
*/
- private long id;
+ private final long id;
/**
* The particle's position, in meters.
@@ -37,15 +37,25 @@ public class Particle {
*/
private double charge;
+ /**
+ * The particle's radius, in meters.
+ */
+ private double radius;
+
public Particle(Vec2 position) {
- this(position, new Vec2(0, 0), 1, 0);
+ this(position, new Vec2(0, 0), 1, 0, 1);
}
- public Particle(Vec2 position, Vec2 velocity, double mass, double charge) {
+ public Particle(Vec2 position, double mass, double charge, double radius) {
+ this(position, new Vec2(0, 0), mass, charge, radius);
+ }
+
+ public Particle(Vec2 position, Vec2 velocity, double mass, double charge, double radius) {
this.position = position;
this.velocity = velocity;
this.mass = mass;
this.charge = charge;
+ this.radius = radius;
this.id = NEXT_PARTICLE_ID;
NEXT_PARTICLE_ID++;
@@ -95,4 +105,32 @@ public class Particle {
this.getVelocity()
);
}
+
+ public Particle getCopy() {
+ return new Particle(
+ this.getPosition().getCopy(),
+ this.getVelocity().getCopy(),
+ this.getMass(),
+ this.getCharge(),
+ this.getRadius()
+ );
+ }
+
+ public Particle combine(Particle other) {
+ final Vec2 position = new Vec2(
+ (this.getPosition().getX() + other.getPosition().getX()) / 2.0,
+ (this.getPosition().getY() + other.getPosition().getY()) / 2.0
+ );
+ final Vec2 velocity = new Vec2(
+ (this.getVelocity().getX() + other.getVelocity().getX()) / 2.0,
+ (this.getVelocity().getY() + other.getVelocity().getY()) / 2.0
+ );
+ return new Particle(
+ position,
+ velocity,
+ this.getMass() + other.getMass(),
+ this.getCharge() + other.getCharge(),
+ this.getRadius() + other.getRadius()
+ );
+ }
}
diff --git a/src/main/java/nl/andrewlalis/threadripper/particle/ParticleFactory.java b/src/main/java/nl/andrewlalis/threadripper/particle/ParticleFactory.java
index eb2a57e..f3d6669 100644
--- a/src/main/java/nl/andrewlalis/threadripper/particle/ParticleFactory.java
+++ b/src/main/java/nl/andrewlalis/threadripper/particle/ParticleFactory.java
@@ -9,6 +9,8 @@ public class ParticleFactory {
private final double maxMass;
private final double minCharge;
private final double maxCharge;
+ private final double minRadius;
+ private final double maxRadius;
private final Vec2 minPosition;
private final Vec2 maxPosition;
private final Vec2 minVelocity;
@@ -19,6 +21,8 @@ public class ParticleFactory {
double maxMass,
double minCharge,
double maxCharge,
+ double minRadius,
+ double maxRadius,
Vec2 minPosition,
Vec2 maxPosition,
Vec2 minVelocity,
@@ -28,6 +32,8 @@ public class ParticleFactory {
this.maxMass = maxMass;
this.minCharge = minCharge;
this.maxCharge = maxCharge;
+ this.minRadius = minRadius;
+ this.maxRadius = maxRadius;
this.minPosition = minPosition;
this.maxPosition = maxPosition;
this.minVelocity = minVelocity;
@@ -45,14 +51,20 @@ public class ParticleFactory {
random.nextDouble(this.minVelocity.getY(), this.maxVelocity.getY())
);
final double mass = random.nextDouble(this.minMass, this.maxMass);
-// final double charge = random.nextDouble(this.minCharge, this.maxCharge);
+ final double charge;
+ if (this.minCharge == 0.0 && this.maxCharge == 0.0) {
+ charge = 0.0;
+ } else {
+ charge = random.nextDouble(this.minCharge, this.maxCharge);
+ }
+ final double radius = random.nextDouble(this.minRadius, this.maxRadius);
- final double charge = 0;
return new Particle(
position,
velocity,
mass,
- charge
+ charge,
+ radius
);
}
diff --git a/src/main/java/nl/andrewlalis/threadripper/render/ParticleChamberRenderer.java b/src/main/java/nl/andrewlalis/threadripper/render/ParticleChamberRenderer.java
new file mode 100644
index 0000000..4d7a685
--- /dev/null
+++ b/src/main/java/nl/andrewlalis/threadripper/render/ParticleChamberRenderer.java
@@ -0,0 +1,140 @@
+package nl.andrewlalis.threadripper.render;
+
+import javafx.application.Platform;
+import javafx.scene.canvas.Canvas;
+import javafx.scene.canvas.GraphicsContext;
+import javafx.scene.paint.Color;
+import lombok.extern.slf4j.Slf4j;
+import nl.andrewlalis.threadripper.engine.ParticleChamber;
+import nl.andrewlalis.threadripper.engine.Vec2;
+import nl.andrewlalis.threadripper.particle.Particle;
+
+import java.util.LinkedList;
+import java.util.Set;
+
+/**
+ * The renderer is responsible for drawing a particle chamber's contents to
+ * a canvas.
+ */
+@Slf4j
+public class ParticleChamberRenderer implements Runnable {
+ private static final double DEFAULT_FPS = 60.0;
+ private static final int FPS_READING_COUNT = 360;
+
+ private final ParticleChamber chamber;
+ private final Canvas canvas;
+
+ private double targetFps;
+ private boolean running;
+
+ private Vec2 offset;
+ private double scale;
+
+ private LinkedList recentFpsReadings;
+
+ public ParticleChamberRenderer(ParticleChamber chamber, Canvas canvas) {
+ this.chamber = chamber;
+ this.canvas = canvas;
+
+ this.targetFps = DEFAULT_FPS;
+ this.recentFpsReadings = new LinkedList<>();
+
+ this.offset = new Vec2(0, 0);
+ this.scale = 1.0;
+ }
+
+ public synchronized void setRunning(boolean running) {
+ this.running = running;
+ }
+
+ public synchronized void setTargetFps(double targetFps) {
+ if (targetFps > 0) {
+ this.targetFps = targetFps;
+ }
+ }
+
+ public synchronized void setOffset(Vec2 offset) {
+ this.offset = offset;
+ }
+
+ public synchronized void setScale(double scale) {
+ if (scale > 0) {
+ this.scale = scale;
+ }
+ }
+
+ @Override
+ public void run() {
+ this.running = true;
+ long previousTimeMilliseconds = System.currentTimeMillis();
+ long millisecondsSinceLastFrame = 0L;
+
+ log.info("Starting particle chamber renderer.");
+ while (this.running) {
+ final long currentTimeMilliseconds = System.currentTimeMillis();
+ final long elapsedMilliseconds = currentTimeMilliseconds - previousTimeMilliseconds;
+
+ millisecondsSinceLastFrame += elapsedMilliseconds;
+
+ final double millisecondsPerFrame = 1000.0 / this.targetFps;
+
+ if (millisecondsSinceLastFrame > millisecondsPerFrame) {
+ final double currentFps = 1000.0 / millisecondsSinceLastFrame;
+ this.updateFpsReading(currentFps);
+ this.draw();
+ millisecondsSinceLastFrame = 0L;
+ }
+
+ previousTimeMilliseconds = currentTimeMilliseconds;
+ }
+ log.info("Particle chamber renderer stopped.");
+ }
+
+ private void draw() {
+ final double secondsSinceLastUpdate = this.chamber.getSecondsSinceLastUpdate();
+ final Set particles = this.chamber.getCopyOfParticles();
+ Platform.runLater(() -> {
+ GraphicsContext gc = this.canvas.getGraphicsContext2D();
+ gc.setFill(Color.WHITE);
+ gc.clearRect(0, 0, this.canvas.getWidth(), this.canvas.getHeight());
+
+
+ for (Particle particle : particles) {
+ particle.updatePosition(secondsSinceLastUpdate);
+ Vec2 pos = particle.getPosition().add(this.offset);
+ if (particle.getCharge() > 0) {
+ gc.setFill(Color.BLUE);
+ } else {
+ gc.setFill(Color.RED);
+ }
+ gc.fillOval(
+ pos.getX() - particle.getRadius(),
+ pos.getY() - particle.getRadius(),
+ particle.getRadius() * 2,
+ particle.getRadius() * 2
+ );
+ }
+
+ gc.setFill(Color.BLACK);
+ gc.fillText(String.format("FPS: %.2f", this.getAverageFps()), 10, 10);
+ });
+ }
+
+ private void updateFpsReading(double recentFpsReading) {
+ this.recentFpsReadings.addFirst(recentFpsReading);
+ if (this.recentFpsReadings.size() > FPS_READING_COUNT) {
+ this.recentFpsReadings.removeLast();
+ }
+ }
+
+ public double getAverageFps() {
+ if (this.recentFpsReadings.size() == 0) {
+ return 0.0;
+ }
+ double sum = 0.0;
+ for (double reading : this.recentFpsReadings) {
+ sum += reading;
+ }
+ return sum / this.recentFpsReadings.size();
+ }
+}