Created
July 26, 2015 07:08
-
-
Save Roland09/71ef45f14d0ec2a353e6 to your computer and use it in GitHub Desktop.
Particle system with a GUI for parameter modification.
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
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; | |
} | |
} |
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
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 | |
} | |
} | |
} |
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
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; | |
} | |
} |
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
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--; | |
} | |
} |
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
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; | |
} | |
} |
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
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 | |
// ------------------------------- | |
} |
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
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; | |
} | |
} |
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
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; | |
} | |
} |
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
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