Skip to content

Instantly share code, notes, and snippets.

@Roland09
Created July 26, 2015 07:08
Show Gist options
  • Save Roland09/71ef45f14d0ec2a353e6 to your computer and use it in GitHub Desktop.
Save Roland09/71ef45f14d0ec2a353e6 to your computer and use it in GitHub Desktop.
Particle system with a GUI for parameter modification.
package application;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.Text;
import javafx.scene.text.TextBoundsType;
/**
* A node which calculates a attraction force for particles
*/
public class Attractor extends Sprite {
double factor = 1.0; // similar to repeller, but with +1 factor
public Attractor( Vector2D location, Vector2D velocity, Vector2D acceleration, double width, double height) {
super( location, velocity, acceleration, width, height);
}
/**
* Circle with a label
*/
@Override
public Node createView() {
Group group = new Group();
double radius = width / 2;
Circle circle = new Circle( radius);
circle.setCenterX(radius);
circle.setCenterY(radius);
circle.setStroke(Color.RED);
circle.setFill(Color.RED.deriveColor(1, 1, 1, 0.3));
group.getChildren().add( circle);
Text text = new Text( "Attractor");
text.setStroke(Color.RED);
text.setFill(Color.RED);
text.setBoundsType(TextBoundsType.VISUAL);
text.relocate(radius - text.getLayoutBounds().getWidth() / 2, radius - text.getLayoutBounds().getHeight() / 2);
group.getChildren().add( text);
return group;
}
/**
* Attraction force
*/
public Vector2D getForce(Particle particle) {
// calculate direction of force
Vector2D dir = Vector2D.subtract(location, particle.location);
// get distance (constrain distance)
double distance = dir.magnitude(); // distance between objects
dir.normalize(); // normalize vector (distance doesn't matter here, we just want this vector for direction)
distance = Utils.clamp(distance, 5, 1000); // keep distance within a reasonable range
// calculate magnitude
double force = factor * Settings.get().getAttractorStrength() / (distance * distance); // repelling force is inversely proportional to distance
// make a vector out of direction and magnitude
dir.multiply(force); // get force vector => magnitude * direction
return dir;
}
}
package application;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class Main extends Application {
private static Random random = new Random();
Canvas canvas;
GraphicsContext graphicsContext;
/**
* Container for canvas and other nodes like attractors and repellers
*/
Pane layerPane;
List<Attractor> allAttractors = new ArrayList<>();
List<Repeller> allRepellers = new ArrayList<>();
List<Particle> allParticles = new ArrayList<>();
AnimationTimer animationLoop;
Scene scene;
MouseGestures mouseGestures = new MouseGestures();
/**
* Container for pre-created images which have color and size depending on
* the particle's lifespan
*/
Image[] images;
@Override
public void start(Stage primaryStage) {
BorderPane root = new BorderPane();
canvas = new Canvas(Settings.get().getCanvasWidth(), Settings.get().getCanvasHeight());
graphicsContext = canvas.getGraphicsContext2D();
layerPane = new Pane();
layerPane.getChildren().addAll(canvas);
canvas.widthProperty().bind(layerPane.widthProperty());
root.setCenter(layerPane);
Node toolbar = Settings.get().createToolbar();
root.setRight(toolbar);
scene = new Scene(root, Settings.get().getSceneWidth(), Settings.get().getSceneHeight(), Settings.get().getSceneColor());
primaryStage.setScene(scene);
primaryStage.setTitle("Particles");
primaryStage.show();
// initialize content
preCreateImages();
// add content
prepareObjects();
// listeners for settings
addSettingsListeners();
// add mouse location listener
addInputListeners();
// add context menus
addContextMenu( canvas);
// run animation loop
startAnimation();
}
private void preCreateImages() {
this.images = Utils.preCreateImages();
}
private void prepareObjects() {
// add attractors
for (int i = 0; i < Settings.get().getAttractorCount(); i++) {
addAttractor();
}
// add repellers
for (int i = 0; i < Settings.get().getRepellerCount(); i++) {
addRepeller();
}
}
private void startAnimation() {
// start game
animationLoop = new AnimationTimer() {
FpsCounter fpsCounter = new FpsCounter();
@Override
public void handle(long now) {
// update fps
fpsCounter.update( now);
// add new particles
for (int i = 0; i < Settings.get().getEmitterFrequency(); i++) {
addParticle();
}
// apply force: gravity
Vector2D forceGravity = Settings.get().getForceGravity();
allParticles.forEach(sprite -> {
sprite.applyForce(forceGravity);
});
// apply force: attractor
for (Attractor attractor: allAttractors) {
allParticles.stream().parallel().forEach(sprite -> {
Vector2D force = attractor.getForce(sprite);
sprite.applyForce(force);
});
}
// apply force: repeller
for (Repeller repeller : allRepellers) {
allParticles.stream().parallel().forEach(sprite -> {
Vector2D force = repeller.getForce(sprite);
sprite.applyForce(force);
});
}
// move sprite: apply acceleration, calculate velocity and location
allParticles.stream().parallel().forEach(Sprite::move);
// update in fx scene
allAttractors.forEach(Sprite::display);
allRepellers.forEach(Sprite::display);
// draw all particles on canvas
// -----------------------------------------
// graphicsContext.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
graphicsContext.setFill(Color.BLACK);
graphicsContext.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
// TODO: parallel?
double particleSizeHalf = Settings.get().getParticleWidth() / 2;
allParticles.stream().forEach(particle -> {
Image img = images[particle.getLifeSpan()];
graphicsContext.drawImage(img, particle.getLocation().x - particleSizeHalf, particle.getLocation().y - particleSizeHalf);
});
// life span of particle
allParticles.stream().parallel().forEach(Sprite::decreaseLifeSpan);
// remove all particles that aren't visible anymore
removeDeadParticles();
// show number of particles
graphicsContext.setFill(Color.WHITE);
graphicsContext.fillText("Particles: " + allParticles.size() + ", fps: " + fpsCounter.getFrameRate(), 1, 10);
}
};
animationLoop.start();
}
private void removeDeadParticles() {
Iterator<Particle> iter = allParticles.iterator();
while (iter.hasNext()) {
Particle particle = iter.next();
if (particle.isDead()) {
// remove from particle list
iter.remove();
}
}
}
private void addParticle() {
// random location
double x = Settings.get().getCanvasWidth() / 2 + random.nextDouble() * Settings.get().getEmitterWidth() - Settings.get().getEmitterWidth() / 2;
double y = Settings.get().getEmitterLocationY();
// dimensions
double width = Settings.get().getParticleWidth();
double height = Settings.get().getParticleHeight();
// create motion data
Vector2D location = new Vector2D(x, y);
double vx = random.nextGaussian() * 0.3;
double vy = random.nextGaussian() * 0.3 - 1.0;
Vector2D velocity = new Vector2D(vx, vy);
Vector2D acceleration = new Vector2D(0, 0);
// create sprite and add to layer
Particle sprite = new Particle(location, velocity, acceleration, width, height);
// register sprite
allParticles.add(sprite);
}
private void addAttractor() {
// center node
double x = Settings.get().getCanvasWidth() / 2;
double y = Settings.get().getCanvasHeight() - Settings.get().getCanvasHeight() / 4;
// dimensions
double width = 100;
double height = 100;
// create motion data
Vector2D location = new Vector2D(x, y);
Vector2D velocity = new Vector2D(0, 0);
Vector2D acceleration = new Vector2D(0, 0);
// create sprite and add to layer
Attractor attractor = new Attractor(location, velocity, acceleration, width, height);
// register sprite
allAttractors.add(attractor);
layerPane.getChildren().add(attractor);
// allow moving via mouse
mouseGestures.makeDraggable(attractor);
}
private void addRepeller() {
// center node
double x = Settings.get().getCanvasWidth() / 2;
double y = Settings.get().getCanvasHeight() - Settings.get().getCanvasHeight() / 4 + 110;
// dimensions
double width = 100;
double height = 100;
// create motion data
Vector2D location = new Vector2D(x, y);
Vector2D velocity = new Vector2D(0, 0);
Vector2D acceleration = new Vector2D(0, 0);
// create sprite and add to layer
Repeller repeller = new Repeller(location, velocity, acceleration, width, height);
// register sprite
allRepellers.add(repeller);
layerPane.getChildren().add(repeller);
// allow moving via mouse
mouseGestures.makeDraggable(repeller);
}
private void addInputListeners() {
}
private void addSettingsListeners() {
// particle size
Settings.get().particleWidthProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
preCreateImages();
}
});
}
public void addContextMenu( Node node) {
MenuItem menuItem;
// create context menu
ContextMenu contextMenu = new ContextMenu();
// add attractor
menuItem = new MenuItem("Add Attractor");
menuItem.setOnAction(e -> addAttractor());
contextMenu.getItems().add( menuItem);
// add repeller
menuItem = new MenuItem("Add Repeller");
menuItem.setOnAction(e -> addRepeller());
contextMenu.getItems().add( menuItem);
// context menu listener
node.setOnMousePressed(event -> {
if (event.isSecondaryButtonDown()) {
contextMenu.show(node, event.getScreenX(), event.getScreenY());
}
});
}
public static void main(String[] args) {
launch(args);
}
/**
* Helper class for frame rate calculation
*/
private static class FpsCounter {
final long[] frameTimes = new long[100];
int frameTimeIndex = 0;
boolean arrayFilled = false;
double frameRate;
double decimalsFactor = 1000; // we want 3 decimals
public void update(long now) {
long oldFrameTime = frameTimes[frameTimeIndex];
frameTimes[frameTimeIndex] = now;
frameTimeIndex = (frameTimeIndex + 1) % frameTimes.length;
if (frameTimeIndex == 0) {
arrayFilled = true;
}
if (arrayFilled) {
long elapsedNanos = now - oldFrameTime;
long elapsedNanosPerFrame = elapsedNanos / frameTimes.length;
frameRate = 1_000_000_000.0 / elapsedNanosPerFrame;
}
}
public double getFrameRate() {
// return frameRate;
return ((int) (frameRate * decimalsFactor)) / decimalsFactor; // reduce to n decimals
}
}
}
package application;
import javafx.event.EventHandler;
import javafx.scene.input.MouseEvent;
/**
* Allow dragging of attractors and repellers
*/
public class MouseGestures {
final DragContext dragContext = new DragContext();
public void makeDraggable(final Sprite sprite) {
sprite.setOnMousePressed(onMousePressedEventHandler);
sprite.setOnMouseDragged(onMouseDraggedEventHandler);
sprite.setOnMouseReleased(onMouseReleasedEventHandler);
}
EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
dragContext.x = event.getSceneX();
dragContext.y = event.getSceneY();
}
};
EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
Sprite sprite = (Sprite) event.getSource();
double offsetX = event.getSceneX() - dragContext.x;
double offsetY = event.getSceneY() - dragContext.y;
sprite.setLocationOffset(offsetX, offsetY);
dragContext.x = event.getSceneX();
dragContext.y = event.getSceneY();
}
};
EventHandler<MouseEvent> onMouseReleasedEventHandler = new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
}
};
class DragContext {
double x;
double y;
}
}
package application;
import javafx.scene.Node;
/**
* A single particle with a per-frame reduced lifespan and now view. The particle is drawn on a canvas, it isn't actually a node
*/
public class Particle extends Sprite {
public Particle( Vector2D location, Vector2D velocity, Vector2D acceleration, double width, double height) {
super( location, velocity, acceleration, width, height);
}
@Override
public Node createView() {
return null;
}
public void decreaseLifeSpan() {
lifeSpan--;
}
}
package application;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.Text;
import javafx.scene.text.TextBoundsType;
/**
* A node which calculates a repelling force for particles
*/
public class Repeller extends Sprite {
double factor = -1.0; // similar to attractor, but with -1 factor
public Repeller( Vector2D location, Vector2D velocity, Vector2D acceleration, double width, double height) {
super( location, velocity, acceleration, width, height);
}
/**
* Circle with a label
*/
@Override
public Node createView() {
Group group = new Group();
double radius = width / 2;
Circle circle = new Circle( radius);
circle.setCenterX(radius);
circle.setCenterY(radius);
circle.setStroke(Color.YELLOW);
circle.setFill(Color.YELLOW.deriveColor(1, 1, 1, 0.3));
group.getChildren().add( circle);
Text text = new Text( "Repeller");
text.setStroke(Color.YELLOW);
text.setFill(Color.YELLOW);
text.setBoundsType(TextBoundsType.VISUAL);
text.relocate(radius - text.getLayoutBounds().getWidth() / 2, radius - text.getLayoutBounds().getHeight() / 2);
group.getChildren().add( text);
return group;
}
/**
* Repel force
*/
public Vector2D getForce(Particle particle) {
// calculate direction of force
Vector2D dir = Vector2D.subtract(location, particle.location);
// get distance (constrain distance)
double distance = dir.magnitude(); // distance between objects
dir.normalize(); // normalize vector (distance doesn't matter here, we just want this vector for direction)
distance = Utils.clamp(distance, 5, 1000); // keep distance within a reasonable range
// calculate magnitude
double force = factor * Settings.get().getRepellerStrength() / (distance * distance); // repelling force is inversely proportional to distance
// make a vector out of direction and magnitude
dir.multiply(force); // get force vector => magnitude * direction
return dir;
}
}
package application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.control.Slider;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
/**
* Application settings
*/
public class Settings {
// scene settings
// -------------------------------
private DoubleProperty sceneWidth = new SimpleDoubleProperty(1280);
private DoubleProperty sceneHeight = new SimpleDoubleProperty(720);
private ObjectProperty<Color> sceneColor = new SimpleObjectProperty<>( Color.BLACK);
private DoubleProperty toolbarWidth = new SimpleDoubleProperty(250);
private DoubleProperty canvasWidth = new SimpleDoubleProperty(sceneWidth.doubleValue()-toolbarWidth.doubleValue());
private DoubleProperty canvasHeight = new SimpleDoubleProperty(sceneHeight.doubleValue());
// forces
// -------------------------------
// number of forces
private IntegerProperty attractorCount = new SimpleIntegerProperty(1);
private IntegerProperty repellerCount = new SimpleIntegerProperty(1);
// just some artificial strength value that matches our needs
private DoubleProperty repellerStrength = new SimpleDoubleProperty( 500);
private DoubleProperty attractorStrength = new SimpleDoubleProperty( 500);
// just some artificial strength value that matches our needs.
private ObjectProperty<Vector2D> forceGravity = new SimpleObjectProperty<>( new Vector2D(0,0));
private DoubleProperty gravityX = new SimpleDoubleProperty( forceGravity.getValue().x);
private DoubleProperty gravityY = new SimpleDoubleProperty( forceGravity.getValue().y);
// emitter
// -------------------------------
private IntegerProperty emitterFrequency = new SimpleIntegerProperty(100); // particles per frame
private DoubleProperty emitterWidth = new SimpleDoubleProperty(canvasWidth.doubleValue());
private DoubleProperty emitterLocationY = new SimpleDoubleProperty(sceneHeight.doubleValue() / 2.0);
// particles
// -------------------------------
private DoubleProperty particleWidth = new SimpleDoubleProperty( 5);
private DoubleProperty particleHeight = new SimpleDoubleProperty( particleWidth.doubleValue());
private DoubleProperty particleLifeSpanMax = new SimpleDoubleProperty( 256);
private DoubleProperty particleMaxSpeed = new SimpleDoubleProperty( 4);
// instance handling
// ----------------------------------------
private static Settings settings = new Settings();
private Settings() {
}
/**
* Return the one instance of this class
*/
public static Settings get() {
return settings;
}
// user interface
// ----------------------------------------
public Node createToolbar() {
GridPane gp = new GridPane();
// gridpane layout
gp.setPrefWidth( Settings.get().getToolbarWidth());
gp.setHgap(1);
gp.setVgap(1);
gp.setPadding(new Insets(8));
// set column size in percent
ColumnConstraints column = new ColumnConstraints();
column.setPercentWidth(30);
gp.getColumnConstraints().add(column);
column = new ColumnConstraints();
column.setPercentWidth(70);
gp.getColumnConstraints().add(column);
// add components for settings to gridpane
Slider slider;
int rowIndex = 0;
// emitter
gp.addRow(rowIndex++, createSeparator( "Emitter"));
slider = createNumberSlider( emitterFrequency, 1, 150);
gp.addRow(rowIndex++, new Label("Frequency"), slider);
slider = createNumberSlider( emitterWidth, 0, getCanvasWidth());
gp.addRow(rowIndex++, new Label("Width"), slider);
slider = createNumberSlider( emitterLocationY, 0, getCanvasHeight());
gp.addRow(rowIndex++, new Label("Location Y"), slider);
// particles
gp.addRow(rowIndex++, createSeparator( "Particles"));
slider = createNumberSlider( particleWidth, 1, 60);
gp.addRow(rowIndex++, new Label("Size"), slider);
slider = createNumberSlider( particleMaxSpeed, 0, 10);
gp.addRow(rowIndex++, new Label("Max Speed"), slider);
// attractors
gp.addRow(rowIndex++, createSeparator( "Attractors"));
slider = createNumberSlider( attractorStrength, 0, 2000);
gp.addRow(rowIndex++, new Label("Strength"), slider);
// repellers
gp.addRow(rowIndex++, createSeparator( "Repellers"));
slider = createNumberSlider( repellerStrength, 0, 2000);
gp.addRow(rowIndex++, new Label("Strength"), slider);
// forces
gp.addRow(rowIndex++, createSeparator( "Forces"));
// gravity
// update gravity vector value when gravity value changes
gravityX.addListener( (ChangeListener<Number>) (observable, oldValue, newValue) -> {
forceGravity.getValue().set(newValue.doubleValue(),gravityY.doubleValue());
});
gravityY.addListener( (ChangeListener<Number>) (observable, oldValue, newValue) -> {
forceGravity.getValue().set(gravityX.doubleValue(), newValue.doubleValue());
});
slider = createNumberSlider( gravityX, -0.5, 0.5);
gp.addRow(rowIndex++, new Label("Gravity X"), slider);
slider = createNumberSlider( gravityY, -0.5, 0.5);
gp.addRow(rowIndex++, new Label("Gravity Y"), slider);
return gp;
}
private Node createSeparator( String text) {
VBox box = new VBox();
Label label = new Label( text);
label.setFont(Font.font(null, FontWeight.BOLD, 14));
Separator separator = new Separator();
box.getChildren().addAll(separator, label);
box.setFillWidth(true);
GridPane.setColumnSpan(box, 2);
GridPane.setFillWidth(box, true);
GridPane.setHgrow(box, Priority.ALWAYS);
return box;
}
private Slider createNumberSlider( Property<Number> observable, double min, double max) {
Slider slider = new Slider( min, max, observable.getValue().doubleValue());
slider.setShowTickLabels(true);
slider.setShowTickMarks(true);
slider.valueProperty().bindBidirectional(observable);
return slider;
}
// -------------------------------
// auto-generated begin
// -------------------------------
public final DoubleProperty sceneWidthProperty() {
return this.sceneWidth;
}
public final double getSceneWidth() {
return this.sceneWidthProperty().get();
}
public final void setSceneWidth(final double sceneWidth) {
this.sceneWidthProperty().set(sceneWidth);
}
public final DoubleProperty sceneHeightProperty() {
return this.sceneHeight;
}
public final double getSceneHeight() {
return this.sceneHeightProperty().get();
}
public final void setSceneHeight(final double sceneHeight) {
this.sceneHeightProperty().set(sceneHeight);
}
public final ObjectProperty<Color> sceneColorProperty() {
return this.sceneColor;
}
public final javafx.scene.paint.Color getSceneColor() {
return this.sceneColorProperty().get();
}
public final void setSceneColor(final javafx.scene.paint.Color sceneColor) {
this.sceneColorProperty().set(sceneColor);
}
public final IntegerProperty attractorCountProperty() {
return this.attractorCount;
}
public final int getAttractorCount() {
return this.attractorCountProperty().get();
}
public final void setAttractorCount(final int attractorCount) {
this.attractorCountProperty().set(attractorCount);
}
public final IntegerProperty repellerCountProperty() {
return this.repellerCount;
}
public final int getRepellerCount() {
return this.repellerCountProperty().get();
}
public final void setRepellerCount(final int repellerCount) {
this.repellerCountProperty().set(repellerCount);
}
public final DoubleProperty repellerStrengthProperty() {
return this.repellerStrength;
}
public final double getRepellerStrength() {
return this.repellerStrengthProperty().get();
}
public final void setRepellerStrength(final double repellerStrength) {
this.repellerStrengthProperty().set(repellerStrength);
}
public final ObjectProperty<Vector2D> forceGravityProperty() {
return this.forceGravity;
}
public final application.Vector2D getForceGravity() {
return this.forceGravityProperty().get();
}
public final void setForceGravity(final application.Vector2D forceGravity) {
this.forceGravityProperty().set(forceGravity);
}
public final IntegerProperty emitterFrequencyProperty() {
return this.emitterFrequency;
}
public final int getEmitterFrequency() {
return this.emitterFrequencyProperty().get();
}
public final void setEmitterFrequency(final int emitterFrequency) {
this.emitterFrequencyProperty().set(emitterFrequency);
}
public final DoubleProperty emitterWidthProperty() {
return this.emitterWidth;
}
public final double getEmitterWidth() {
return this.emitterWidthProperty().get();
}
public final void setEmitterWidth(final double emitterWidth) {
this.emitterWidthProperty().set(emitterWidth);
}
public final DoubleProperty emitterLocationYProperty() {
return this.emitterLocationY;
}
public final double getEmitterLocationY() {
return this.emitterLocationYProperty().get();
}
public final void setEmitterLocationY(final double emitterLocationY) {
this.emitterLocationYProperty().set(emitterLocationY);
}
public final DoubleProperty particleWidthProperty() {
return this.particleWidth;
}
public final double getParticleWidth() {
return this.particleWidthProperty().get();
}
public final void setParticleWidth(final double particleWidth) {
this.particleWidthProperty().set(particleWidth);
}
public final DoubleProperty particleHeightProperty() {
return this.particleHeight;
}
public final double getParticleHeight() {
return this.particleHeightProperty().get();
}
public final void setParticleHeight(final double particleHeight) {
this.particleHeightProperty().set(particleHeight);
}
public final DoubleProperty particleLifeSpanMaxProperty() {
return this.particleLifeSpanMax;
}
public final double getParticleLifeSpanMax() {
return this.particleLifeSpanMaxProperty().get();
}
public final void setParticleLifeSpanMax(final double particleLifeSpanMax) {
this.particleLifeSpanMaxProperty().set(particleLifeSpanMax);
}
public final DoubleProperty particleMaxSpeedProperty() {
return this.particleMaxSpeed;
}
public final double getParticleMaxSpeed() {
return this.particleMaxSpeedProperty().get();
}
public final void setParticleMaxSpeed(final double particleMaxSpeed) {
this.particleMaxSpeedProperty().set(particleMaxSpeed);
}
public final DoubleProperty toolbarWidthProperty() {
return this.toolbarWidth;
}
public final double getToolbarWidth() {
return this.toolbarWidthProperty().get();
}
public final void setToolbarWidth(final double toolbarWidth) {
this.toolbarWidthProperty().set(toolbarWidth);
}
public final DoubleProperty canvasWidthProperty() {
return this.canvasWidth;
}
public final double getCanvasWidth() {
return this.canvasWidthProperty().get();
}
public final void setCanvasWidth(final double canvasWidth) {
this.canvasWidthProperty().set(canvasWidth);
}
public final DoubleProperty canvasHeightProperty() {
return this.canvasHeight;
}
public final double getCanvasHeight() {
return this.canvasHeightProperty().get();
}
public final void setCanvasHeight(final double canvasHeight) {
this.canvasHeightProperty().set(canvasHeight);
}
public final DoubleProperty attractorStrengthProperty() {
return this.attractorStrength;
}
public final double getAttractorStrength() {
return this.attractorStrengthProperty().get();
}
public final void setAttractorStrength(final double attractorStrength) {
this.attractorStrengthProperty().set(attractorStrength);
}
// -------------------------------
// auto-generated end
// -------------------------------
}
package application;
import javafx.scene.Node;
import javafx.scene.layout.Region;
/**
* Sprite base class
*/
public abstract class Sprite extends Region {
Vector2D location;
Vector2D velocity;
Vector2D acceleration;
double maxSpeed = Settings.get().getParticleMaxSpeed();
double radius;
Node view;
double width;
double height;
double centerX;
double centerY;
double angle;
double lifeSpanMax = Settings.get().getParticleLifeSpanMax() - 1; // -1 because we want [0..255] for an amount of 256; otherwise we'd get DivByZero in the logic; will fix it later
double lifeSpan = lifeSpanMax;
public Sprite( Vector2D location, Vector2D velocity, Vector2D acceleration, double width, double height) {
this.location = location;
this.velocity = velocity;
this.acceleration = acceleration;
this.width = width;
this.height = height;
this.centerX = width / 2;
this.centerY = height / 2;
this.radius = width / 2;
this.view = createView();
setPrefSize(width, height);
if( this.view != null) {
getChildren().add( view);
}
}
public abstract Node createView();
public void applyForce(Vector2D force) {
acceleration.add(force);
}
/**
* Standard movement method: calculate valocity depending on accumulated acceleration force, then calculate the location.
* Reset acceleration so that it can be recalculated in the next animation step.
*/
public void move() {
// set velocity depending on acceleration
velocity.add(acceleration);
// limit velocity to max speed
velocity.limit(maxSpeed);
// change location depending on velocity
location.add(velocity);
// angle: towards velocity (ie target)
angle = velocity.angle();
// clear acceleration
acceleration.multiply(0);
}
/**
* Update node position
*/
public void display() {
// location
relocate(location.x - centerX, location.y - centerY);
// rotation
setRotate(Math.toDegrees( angle));
}
public Vector2D getVelocity() {
return velocity;
}
public Vector2D getLocation() {
return location;
}
public void setLocation( double x, double y) {
location.x = x;
location.y = y;
}
public void setLocationOffset( double x, double y) {
location.x += x;
location.y += y;
}
public void decreaseLifeSpan() {
}
public boolean isDead() {
if (lifeSpan <= 0.0) {
return true;
} else {
return false;
}
}
public int getLifeSpan() {
return (int) lifeSpan;
}
}
package application;
import javafx.scene.Node;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
public class Utils {
/**
* Clamp value between min and max
* @param value
* @param min
* @param max
* @return
*/
public static double clamp(double value, double min, double max) {
if (value < min)
return min;
if (value > max)
return max;
return value;
}
/**
* Map value of a given range to a target range
* @param value
* @param currentRangeStart
* @param currentRangeStop
* @param targetRangeStart
* @param targetRangeStop
* @return
*/
public static double map(double value, double currentRangeStart, double currentRangeStop, double targetRangeStart, double targetRangeStop) {
return targetRangeStart + (targetRangeStop - targetRangeStart) * ((value - currentRangeStart) / (currentRangeStop - currentRangeStart));
}
/**
* Snapshot an image out of a node, consider transparency.
*
* @param node
* @return
*/
public static Image createImage(Node node) {
WritableImage wi;
SnapshotParameters parameters = new SnapshotParameters();
parameters.setFill(Color.TRANSPARENT);
int imageWidth = (int) node.getBoundsInLocal().getWidth();
int imageHeight = (int) node.getBoundsInLocal().getHeight();
wi = new WritableImage(imageWidth, imageHeight);
node.snapshot(parameters, wi);
return wi;
}
/**
* Pre-create images with various gradient colors and sizes.
*
* @return
*/
public static Image[] preCreateImages() {
// get number of images
int count = (int) Settings.get().getParticleLifeSpanMax();
// create linear gradient lookup image: lifespan 0 -> lifespan max
double width = count;
Stop[] stops = new Stop[] { new Stop(0, Color.BLACK.deriveColor(1, 1, 1, 0.0)), new Stop(0.3, Color.RED), new Stop(0.9, Color.YELLOW), new Stop(1, Color.WHITE)};
LinearGradient linearGradient = new LinearGradient(0, 0, width, 0, false, CycleMethod.NO_CYCLE, stops);
Rectangle rectangle = new Rectangle(width,1);
rectangle.setFill( linearGradient);
Image lookupImage = createImage(rectangle);
// pre-create images
Image[] list = new Image[count];
double radius = Settings.get().getParticleWidth();
for (int i = 0; i < count; i++) {
// get color depending on lifespan
Color color = lookupImage.getPixelReader().getColor( i, 0);
// create gradient image with given color
Circle ball = new Circle(radius);
RadialGradient gradient1 = new RadialGradient(0, 0, 0, 0, radius, false, CycleMethod.NO_CYCLE, new Stop(0, color.deriveColor(1, 1, 1, 1)), new Stop(1, color.deriveColor(1, 1, 1, 0)));
ball.setFill(gradient1);
// create image
list[i] = Utils.createImage(ball);
}
return list;
}
}
package application;
public class Vector2D {
public double x;
public double y;
public Vector2D(double x, double y) {
this.x = x;
this.y = y;
}
public void set(double x, double y) {
this.x = x;
this.y = y;
}
public double magnitude() {
return (double) Math.sqrt(x * x + y * y);
}
public void add(Vector2D v) {
x += v.x;
y += v.y;
}
public void add(double x, double y) {
this.x += x;
this.y += y;
}
public void multiply(double n) {
x *= n;
y *= n;
}
public void div(double n) {
x /= n;
y /= n;
}
public void normalize() {
double m = magnitude();
if (m != 0 && m != 1) {
div(m);
}
}
public void limit(double max) {
if (magnitude() > max) {
normalize();
multiply(max);
}
}
public double angle() {
double angle = (double) Math.atan2(-y, x);
return -1 * angle;
}
static public Vector2D subtract(Vector2D v1, Vector2D v2) {
return new Vector2D(v1.x - v2.x, v1.y - v2.y);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment