Skip to content

Commit

Permalink
Created a small JavaFX frontend.
Browse files Browse the repository at this point in the history
  • Loading branch information
Dirk Groot committed May 11, 2014
1 parent 56c9fee commit 35c8878
Show file tree
Hide file tree
Showing 9 changed files with 341 additions and 2 deletions.
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
</properties>

<dependencies>
<dependency>
<groupId>com.airhacks</groupId>
<artifactId>afterburner.fx</artifactId>
<version>1.4.4</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/nl/dricus/gameoflife/app/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package nl.dricus.gameoflife.app;

import nl.dricus.gameoflife.presentation.display.DisplayView;

import com.airhacks.afterburner.views.FXMLView;

public class Application extends FXMLApplication {

@Override
protected String getTitle() {
return "Conway's Game of Life";
}

@Override
protected FXMLView createAppRootView() {
return new DisplayView();
}

public static void main(String[] args) {
launch(args);
}

}
90 changes: 90 additions & 0 deletions src/main/java/nl/dricus/gameoflife/app/FXMLApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package nl.dricus.gameoflife.app;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;

import com.airhacks.afterburner.injection.InjectionProvider;
import com.airhacks.afterburner.views.FXMLView;

/**
* Base class for JavaFX applications. Initializes the {@link Scene}, and the primary {@link Stage}.
* Provides several "hooks" so subclasses can do extra initialization if needed/wanted.
*
* @author Dirk
*/
public abstract class FXMLApplication extends Application {

public static final String DEFAULT_APP_CSS_FILENAME = "app.css";

@Override
public void start(Stage primaryStage) throws Exception {
FXMLView appView = createAppRootView();
Scene primaryScene = createPrimaryScene(appView);

initializePrimaryScene(primaryScene);
initializePrimaryStage(primaryStage);

primaryStage.setScene(primaryScene);
primaryStage.show();
}

@Override
public void stop() throws Exception {
InjectionProvider.forgetAll();
}

/**
* @return the title of the main window of the application.
*/
protected abstract String getTitle();

/**
* @return a new {@link FXMLView}, which will be the main view of the application.
*/
protected abstract FXMLView createAppRootView();

/**
* @return a new {@link Scene} with <code>appView</code> as it's root node.
*/
protected Scene createPrimaryScene(FXMLView appView) {
return new Scene(appView.getView());
}

/**
* Adds the gobal CSS file to the scene. Called by {@link #start(Stage)} before initializing the
* primary {@link Stage}. Override this if you want to do more initialization.
*/
protected void initializePrimaryScene(Scene scene) {
scene.getStylesheets().add(getAppCssPath());
}

/**
* Sets the title of the primary stage (and thus of the main window of the application). Called
* by {@link #start(Stage)} before setting the scene and showing the {@link Stage}. Override
* this if you want to do more initialization.
*/
protected void initializePrimaryStage(Stage primaryStage) {
primaryStage.setTitle(getTitle());
}

/**
* Override this if you want to use a global CSS file which is not in the same package as your
* application class.
*
* @return the absolute path to the file returned by {@link #getAppCssFileName()}.
*/
protected String getAppCssPath() {
return getClass().getResource(getAppCssFileName()).toExternalForm();
}

/**
* Override this is you want to use a different filename than {@link #DEFAULT_APP_CSS_FILENAME}.
*
* @return the filename of the global CSS file.
*/
protected String getAppCssFileName() {
return DEFAULT_APP_CSS_FILENAME;
}

}
Empty file.
29 changes: 27 additions & 2 deletions src/main/java/nl/dricus/gameoflife/boundary/Game.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
package nl.dricus.gameoflife.boundary;

import java.util.Random;

import nl.dricus.gameoflife.entity.Generation;

public class Game {

private final int width;
private final int height;
private int width;
private int height;

private Generation currentGeneration;
private Generation previousGeneration;

public Game() {
this(800, 600);
}

public Game(int width, int height) {
initializeGeneration(width, height);
}

public void setDimensions(int width, int height) {
initializeGeneration(width, height);
}

private void initializeGeneration(int width, int height) {
this.width = width;
this.height = height;

Expand Down Expand Up @@ -63,4 +77,15 @@ private boolean shouldBeResurrected(int row, int col) {
return previousGeneration.getLiveNeighborCount(col, row) == 3;
}

public void randomizeCurrentGeneration() {
Random random = new Random();

for (int row = 0; row < currentGeneration.getHeight(); row++) {
for (int col = 0; col < currentGeneration.getWidth(); col++) {
if (random.nextBoolean())
currentGeneration.resurrectCell(col, row);
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package nl.dricus.gameoflife.presentation.display;

import javafx.animation.Animation.Status;
import javafx.animation.AnimationTimer;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Bounds;
import javafx.scene.canvas.Canvas;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.PixelWriter;
import javafx.scene.layout.AnchorPane;
import javafx.util.Duration;

import javax.inject.Inject;

import nl.dricus.gameoflife.boundary.Game;
import nl.dricus.gameoflife.entity.Generation;

public class DisplayPresenter {

private static final int SCALE_FACTOR = 16;

@Inject
Game game;

@FXML
private Canvas canvas;

@FXML
private AnchorPane anchorPane;

private AnimationTimer timer;
private Timeline timeline;

@FXML
public void initialize() {
initializeGame();

createAnimation();
createTimeline();

adjustCanvasToGameDimensions();
updateCanvasSizeOnWindowResize();

changeGameDimensionsWhenCanvasSizeChanges();

startRendering();
}

private void initializeGame() {
game.setDimensions((int) canvas.getWidth() / SCALE_FACTOR, (int) canvas.getHeight() / SCALE_FACTOR);
game.randomizeCurrentGeneration();
}

private void createAnimation() {
timer = new AnimationTimer() {
@Override
public void handle(long now) {
render();
}
};
}

private void createTimeline() {
timeline = new Timeline();

timeline.setCycleCount(Timeline.INDEFINITE);
timeline.getKeyFrames().add(createTickKeyFrame());
}

private KeyFrame createTickKeyFrame() {
return new KeyFrame(Duration.millis(100), new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
game.tick();
}
});
}

private void adjustCanvasToGameDimensions() {
canvas.setWidth(game.getCurrentGeneration().getWidth() * SCALE_FACTOR);
canvas.setHeight(game.getCurrentGeneration().getHeight() * SCALE_FACTOR);
}

private void updateCanvasSizeOnWindowResize() {
anchorPane.widthProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
canvas.setWidth(newValue.doubleValue());
}
});
anchorPane.heightProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
canvas.setHeight(newValue.doubleValue());
}
});
}

private void changeGameDimensionsWhenCanvasSizeChanges() {
canvas.boundsInLocalProperty().addListener(new ChangeListener<Bounds>() {
@Override
public void changed(ObservableValue<? extends Bounds> observable, Bounds oldValue, Bounds newValue) {
game.setDimensions((int) newValue.getWidth() / SCALE_FACTOR, (int) newValue.getHeight() / SCALE_FACTOR);
game.randomizeCurrentGeneration();
}
});
}

private void startRendering() {
timer.start();
}

@FXML
public void onMouseClicked() {
if (timeline.getStatus() == Status.RUNNING)
timeline.pause();
else
timeline.play();
}

private void render() {
int[] pixels = renderGenerationToPixelArray(game.getCurrentGeneration());

renderPixelsToCanvas(pixels);
}

private int[] renderGenerationToPixelArray(Generation currentGeneration) {
int scaledHeight = getScaledHeight(currentGeneration);
int scaledWidth = getScaledWidth(currentGeneration);
int[] pixels = new int[scaledHeight * scaledWidth];

for (int row = 0; row < scaledHeight; row++) {
for (int col = 0; col < scaledWidth; col++) {
int index = (row * scaledWidth) + col;

if (currentGeneration.isCellAlive(col / SCALE_FACTOR, row / SCALE_FACTOR))
pixels[index] = 0xffffffff;
else
pixels[index] = 0xff000000;
}
}

return pixels;
}

private void renderPixelsToCanvas(int[] pixels) {
Generation currentGeneration = game.getCurrentGeneration();
int scaledHeight = getScaledHeight(currentGeneration);
int scaledWidth = getScaledWidth(currentGeneration);
PixelWriter pixelWriter = getPixelWriter();

pixelWriter
.setPixels(0, 0, scaledWidth, scaledHeight, PixelFormat.getIntArgbInstance(), pixels, 0, scaledWidth);
}

private int getScaledHeight(Generation currentGeneration) {
return currentGeneration.getHeight() * SCALE_FACTOR;
}

private int getScaledWidth(Generation currentGeneration) {
return currentGeneration.getWidth() * SCALE_FACTOR;
}

private PixelWriter getPixelWriter() {
return canvas.getGraphicsContext2D().getPixelWriter();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package nl.dricus.gameoflife.presentation.display;

import com.airhacks.afterburner.views.FXMLView;

public class DisplayView extends FXMLView {

}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import javafx.scene.canvas.*?>
<?import javafx.scene.canvas.Canvas?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.shape.*?>

<AnchorPane id="pane" fx:id="anchorPane" pickOnBounds="true" scaleX="1.0" scaleY="1.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2" fx:controller="nl.dricus.gameoflife.presentation.display.DisplayPresenter">
<children>
<Canvas fx:id="canvas" height="600.0" layoutX="0.0" layoutY="0.0" onMouseClicked="#onMouseClicked" scaleX="1.0" scaleY="1.0" width="800.0" />
</children>
</AnchorPane>

0 comments on commit 35c8878

Please sign in to comment.