Add: FX and music to GUI
This commit is contained in:
@@ -25,6 +25,11 @@
|
|||||||
<artifactId>javafx-fxml</artifactId>
|
<artifactId>javafx-fxml</artifactId>
|
||||||
<version>21.0.6</version>
|
<version>21.0.6</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-media</artifactId>
|
||||||
|
<version>21.0.6</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.controlsfx</groupId>
|
<groupId>org.controlsfx</groupId>
|
||||||
<artifactId>controlsfx</artifactId>
|
<artifactId>controlsfx</artifactId>
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
package it.polimi.ingsw.gc14.View.GUI;
|
||||||
|
|
||||||
|
import javafx.animation.AnimationTimer;
|
||||||
|
import javafx.scene.canvas.Canvas;
|
||||||
|
import javafx.scene.canvas.GraphicsContext;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class FireParticleSystem {
|
||||||
|
|
||||||
|
private static final int MAX_PARTICLES = 20;
|
||||||
|
private static final long SPAWN_INTERVAL_NS = 100_000_000L; // ~10 spawns/sec
|
||||||
|
|
||||||
|
private final Canvas canvas;
|
||||||
|
private final List<Particle> particles = new ArrayList<>();
|
||||||
|
private final Random rnd = new Random();
|
||||||
|
private AnimationTimer timer;
|
||||||
|
private long lastSpawn = 0;
|
||||||
|
private boolean warmedUp = false;
|
||||||
|
|
||||||
|
public FireParticleSystem(Scene scene) {
|
||||||
|
canvas = new Canvas();
|
||||||
|
canvas.widthProperty().bind(scene.widthProperty());
|
||||||
|
canvas.heightProperty().bind(scene.heightProperty());
|
||||||
|
canvas.setMouseTransparent(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Canvas getCanvas() {
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
timer = new AnimationTimer() {
|
||||||
|
@Override
|
||||||
|
public void handle(long now) {
|
||||||
|
tick(now);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
timer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
if (timer != null) timer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tick(long now) {
|
||||||
|
double w = canvas.getWidth();
|
||||||
|
double h = canvas.getHeight();
|
||||||
|
if (w == 0 || h == 0) return;
|
||||||
|
|
||||||
|
if (!warmedUp) {
|
||||||
|
warmedUp = true;
|
||||||
|
// Pre-populate: spawn a full batch and advance each particle a random
|
||||||
|
// number of frames so they're spread across the screen from frame 1.
|
||||||
|
for (int i = 0; i < MAX_PARTICLES; i++) {
|
||||||
|
Particle p = spawnParticle(w, h);
|
||||||
|
int advance = rnd.nextInt(800) + 100; // 100-900 frames ahead
|
||||||
|
for (int f = 0; f < advance; f++) p.update();
|
||||||
|
if (!p.isDead(w, h)) particles.add(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now - lastSpawn > SPAWN_INTERVAL_NS && particles.size() < MAX_PARTICLES) {
|
||||||
|
lastSpawn = now;
|
||||||
|
int count = rnd.nextInt(2) + 1; // 1-2 per spawn
|
||||||
|
for (int i = 0; i < count && particles.size() < MAX_PARTICLES; i++) {
|
||||||
|
particles.add(spawnParticle(w, h));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GraphicsContext gc = canvas.getGraphicsContext2D();
|
||||||
|
gc.clearRect(0, 0, w, h);
|
||||||
|
|
||||||
|
Iterator<Particle> it = particles.iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
Particle p = it.next();
|
||||||
|
p.update();
|
||||||
|
if (p.isDead(w, h)) {
|
||||||
|
it.remove();
|
||||||
|
} else {
|
||||||
|
p.draw(gc, w, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Particle spawnParticle(double w, double h) {
|
||||||
|
// One of 4 corners, with a small random offset inside the corner area
|
||||||
|
int corner = rnd.nextInt(4);
|
||||||
|
double margin = 0.12;
|
||||||
|
double x, y;
|
||||||
|
switch (corner) {
|
||||||
|
case 0 -> { x = rnd.nextDouble() * w * margin; y = h - rnd.nextDouble() * h * margin; } // bottom-left
|
||||||
|
case 1 -> { x = w - rnd.nextDouble() * w * margin; y = h - rnd.nextDouble() * h * margin; } // bottom-right
|
||||||
|
case 2 -> { x = rnd.nextDouble() * w * margin; y = rnd.nextDouble() * h * margin; } // top-left
|
||||||
|
default -> { x = w - rnd.nextDouble() * w * margin; y = rnd.nextDouble() * h * margin; } // top-right
|
||||||
|
}
|
||||||
|
return new Particle(x, y, w, h, corner, rnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Particle {
|
||||||
|
double x, y;
|
||||||
|
double vx, vy;
|
||||||
|
double life; // only sparks use this for burnout
|
||||||
|
double size;
|
||||||
|
double wobblePhase;
|
||||||
|
double wobbleSpeed;
|
||||||
|
double wobbleAmp;
|
||||||
|
final int type;
|
||||||
|
|
||||||
|
Particle(double x, double y, double w, double h, int corner, Random rnd) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.life = 1.0;
|
||||||
|
this.type = weightedType(rnd);
|
||||||
|
|
||||||
|
// Direction: roughly toward the opposite corner, with angular spread
|
||||||
|
double targetX = (corner == 0 || corner == 2) ? w * 0.75 : w * 0.25;
|
||||||
|
double targetY = (corner == 0 || corner == 1) ? h * 0.25 : h * 0.75;
|
||||||
|
double dx = targetX - x;
|
||||||
|
double dy = targetY - y;
|
||||||
|
double len = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
double baseAngle = Math.atan2(dy, dx);
|
||||||
|
double spread = Math.PI / 3.5; // ~51 degree spread
|
||||||
|
double angle = baseAngle + (rnd.nextDouble() - 0.5) * spread;
|
||||||
|
|
||||||
|
double speed;
|
||||||
|
if (type == 0) { // ember
|
||||||
|
speed = 0.5 + rnd.nextDouble() * 0.7;
|
||||||
|
size = rnd.nextDouble() * 3 + 2;
|
||||||
|
} else if (type == 1) { // spark
|
||||||
|
speed = 1.5 + rnd.nextDouble() * 2.0;
|
||||||
|
size = rnd.nextDouble() * 1.5 + 0.5;
|
||||||
|
} else { // dust
|
||||||
|
speed = 0.2 + rnd.nextDouble() * 0.35;
|
||||||
|
size = rnd.nextDouble() * 8 + 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
vx = Math.cos(angle) * speed;
|
||||||
|
vy = Math.sin(angle) * speed;
|
||||||
|
|
||||||
|
wobblePhase = rnd.nextDouble() * Math.PI * 2;
|
||||||
|
wobbleSpeed = 0.025 + rnd.nextDouble() * 0.04;
|
||||||
|
wobbleAmp = 0.1 + rnd.nextDouble() * 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int weightedType(Random rnd) {
|
||||||
|
double r = rnd.nextDouble();
|
||||||
|
if (r < 0.55) return 0;
|
||||||
|
if (r < 0.78) return 1;
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
void update() {
|
||||||
|
wobblePhase += wobbleSpeed;
|
||||||
|
vx += Math.sin(wobblePhase) * wobbleAmp * 0.05;
|
||||||
|
vy += Math.cos(wobblePhase) * wobbleAmp * 0.02;
|
||||||
|
x += vx;
|
||||||
|
y += vy;
|
||||||
|
if (type == 1) life -= 0.008 + Math.random() * 0.006;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isDead(double w, double h) {
|
||||||
|
double pad = size * 4;
|
||||||
|
boolean offScreen = x < -pad || x > w + pad || y < -pad || y > h + pad;
|
||||||
|
return offScreen || (type == 1 && life <= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw(GraphicsContext gc, double w, double h) {
|
||||||
|
// Fade only within 5% of any edge
|
||||||
|
double edge = Math.min(w, h) * 0.05;
|
||||||
|
double fadeX = Math.min(x / edge, Math.min((w - x) / edge, 1.0));
|
||||||
|
double fadeY = Math.min(y / edge, Math.min((h - y) / edge, 1.0));
|
||||||
|
double posAlpha = Math.max(0, Math.min(fadeX, fadeY));
|
||||||
|
|
||||||
|
double lifeAlpha = (type == 1) ? Math.min(life * 2.0, 1.0) : 1.0;
|
||||||
|
double alpha = posAlpha * lifeAlpha;
|
||||||
|
if (alpha <= 0.01) return;
|
||||||
|
|
||||||
|
if (type == 0) { // ember: orange, glows
|
||||||
|
double dist = Math.sqrt((x - w / 2) * (x - w / 2) + (y - h / 2) * (y - h / 2));
|
||||||
|
double hotness = Math.max(0, 1.0 - dist / (Math.sqrt(w * w + h * h) * 0.4));
|
||||||
|
double g = 0.3 + hotness * 0.5;
|
||||||
|
gc.setFill(Color.color(1.0, g * 0.35, 0.0, alpha * 0.10));
|
||||||
|
gc.fillOval(x - size * 2.2, y - size * 2.2, size * 4.4, size * 4.4);
|
||||||
|
gc.setFill(Color.color(1.0, g, 0.0, alpha * 0.88));
|
||||||
|
gc.fillOval(x - size / 2, y - size / 2, size, size);
|
||||||
|
} else if (type == 1) { // spark: white-yellow
|
||||||
|
double brightness = Math.min(life * 2.5, 1.0);
|
||||||
|
gc.setFill(Color.color(1.0, brightness * 0.85 + 0.15, brightness * 0.15, alpha));
|
||||||
|
gc.fillOval(x - size / 2, y - size / 2, size, size);
|
||||||
|
} else { // dust: gray-brown translucent
|
||||||
|
gc.setFill(Color.color(0.55, 0.38, 0.22, alpha * 0.20));
|
||||||
|
gc.fillOval(x - size, y - size * 0.5, size * 2, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,16 +4,24 @@ import it.polimi.ingsw.gc14.Controller.ClientController;
|
|||||||
import it.polimi.ingsw.gc14.ErrorType;
|
import it.polimi.ingsw.gc14.ErrorType;
|
||||||
import it.polimi.ingsw.gc14.Model.MiniModel;
|
import it.polimi.ingsw.gc14.Model.MiniModel;
|
||||||
import it.polimi.ingsw.gc14.View.IView;
|
import it.polimi.ingsw.gc14.View.IView;
|
||||||
|
import javafx.animation.FadeTransition;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
import javafx.geometry.Rectangle2D;
|
import javafx.geometry.Rectangle2D;
|
||||||
|
import javafx.scene.Parent;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.input.KeyCode;
|
import javafx.scene.input.KeyCode;
|
||||||
import javafx.scene.input.KeyEvent;
|
import javafx.scene.input.KeyEvent;
|
||||||
import javafx.scene.input.KeyCombination;
|
import javafx.scene.input.KeyCombination;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.media.Media;
|
||||||
|
import javafx.scene.media.MediaPlayer;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
|
import javafx.scene.shape.Rectangle;
|
||||||
import javafx.stage.Screen;
|
import javafx.stage.Screen;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
import javafx.util.Duration;
|
||||||
|
|
||||||
import static it.polimi.ingsw.gc14.Model.GamePackage.GameStages.ENDED;
|
import static it.polimi.ingsw.gc14.Model.GamePackage.GameStages.ENDED;
|
||||||
import static it.polimi.ingsw.gc14.Model.GamePackage.GameStages.TOTEM_CHOICE;
|
import static it.polimi.ingsw.gc14.Model.GamePackage.GameStages.TOTEM_CHOICE;
|
||||||
@@ -46,6 +54,8 @@ public class GUI extends Application implements IView {
|
|||||||
|
|
||||||
private ClientController controller;
|
private ClientController controller;
|
||||||
private boolean autoReenterFullscreen = true;
|
private boolean autoReenterFullscreen = true;
|
||||||
|
private boolean isFading = false;
|
||||||
|
private MediaPlayer bgMusic;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,14 +90,23 @@ public class GUI extends Application implements IView {
|
|||||||
controllerLeaderboard = loaderLeaderboard.getController();
|
controllerLeaderboard = loaderLeaderboard.getController();
|
||||||
controllerLeaderboard.setController(controller, () -> {
|
controllerLeaderboard.setController(controller, () -> {
|
||||||
controllerLogin.updateLoginButton(true);
|
controllerLogin.updateLoginButton(true);
|
||||||
primaryStage.setScene(loginScene);
|
|
||||||
controllerLogin.showError("");
|
controllerLogin.showError("");
|
||||||
|
fadeToScene(loginScene);
|
||||||
}, loginScene);
|
}, loginScene);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
wrapScene(loginScene);
|
||||||
|
wrapScene(totemScene);
|
||||||
|
wrapScene(mainScene);
|
||||||
|
wrapScene(leaderboardScene);
|
||||||
|
|
||||||
|
FireParticleSystem fireParticles = new FireParticleSystem(mainScene);
|
||||||
|
((StackPane) mainScene.getRoot()).getChildren().add(fireParticles.getCanvas());
|
||||||
|
fireParticles.start();
|
||||||
|
|
||||||
primaryStage.setScene(loginScene);
|
primaryStage.setScene(loginScene);
|
||||||
Rectangle2D tmp = Screen.getPrimary().getVisualBounds();
|
Rectangle2D tmp = Screen.getPrimary().getVisualBounds();
|
||||||
primaryStage.setWidth(tmp.getWidth());
|
primaryStage.setWidth(tmp.getWidth());
|
||||||
@@ -123,6 +142,12 @@ public class GUI extends Application implements IView {
|
|||||||
});
|
});
|
||||||
primaryStage.show();
|
primaryStage.show();
|
||||||
Platform.runLater(() -> primaryStage.setFullScreen(true));
|
Platform.runLater(() -> primaryStage.setFullScreen(true));
|
||||||
|
|
||||||
|
String musicUrl = getClass().getResource("/Audio/Music.mp3").toExternalForm();
|
||||||
|
bgMusic = new MediaPlayer(new Media(musicUrl));
|
||||||
|
bgMusic.setCycleCount(MediaPlayer.INDEFINITE);
|
||||||
|
bgMusic.setVolume(0.4);
|
||||||
|
bgMusic.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -154,18 +179,64 @@ public class GUI extends Application implements IView {
|
|||||||
synchronized (miniModel) {
|
synchronized (miniModel) {
|
||||||
if (controller.miniModel.currentState.getGameStage() == TOTEM_CHOICE) {
|
if (controller.miniModel.currentState.getGameStage() == TOTEM_CHOICE) {
|
||||||
controllerTotem.render();
|
controllerTotem.render();
|
||||||
primaryStage.setScene(totemScene);
|
fadeToScene(totemScene);
|
||||||
} else if (controller.miniModel.currentState.getGameStage() == ENDED) {
|
} else if (controller.miniModel.currentState.getGameStage() == ENDED) {
|
||||||
controllerLeaderboard.render();
|
controllerLeaderboard.render();
|
||||||
primaryStage.setScene(leaderboardScene);
|
fadeToScene(leaderboardScene);
|
||||||
} else {
|
} else {
|
||||||
primaryStage.setScene(mainScene);
|
|
||||||
controllerMain.render();
|
controllerMain.render();
|
||||||
|
fadeToScene(mainScene);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void wrapScene(Scene scene) {
|
||||||
|
Parent root = scene.getRoot();
|
||||||
|
scene.setRoot(new StackPane(root));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fadeToScene(Scene newScene) {
|
||||||
|
Scene currentScene = primaryStage.getScene();
|
||||||
|
if (currentScene == newScene || isFading) return;
|
||||||
|
isFading = true;
|
||||||
|
|
||||||
|
StackPane currentRoot = (StackPane) currentScene.getRoot();
|
||||||
|
Rectangle overlay = new Rectangle();
|
||||||
|
overlay.setFill(Color.BLACK);
|
||||||
|
overlay.setOpacity(0);
|
||||||
|
overlay.widthProperty().bind(currentRoot.widthProperty());
|
||||||
|
overlay.heightProperty().bind(currentRoot.heightProperty());
|
||||||
|
currentRoot.getChildren().add(overlay);
|
||||||
|
|
||||||
|
FadeTransition fadeOut = new FadeTransition(Duration.millis(500), overlay);
|
||||||
|
fadeOut.setFromValue(0);
|
||||||
|
fadeOut.setToValue(1);
|
||||||
|
fadeOut.setOnFinished(e -> {
|
||||||
|
currentRoot.getChildren().remove(overlay);
|
||||||
|
|
||||||
|
StackPane newRoot = (StackPane) newScene.getRoot();
|
||||||
|
Rectangle newOverlay = new Rectangle();
|
||||||
|
newOverlay.setFill(Color.BLACK);
|
||||||
|
newOverlay.setOpacity(1);
|
||||||
|
newOverlay.widthProperty().bind(newRoot.widthProperty());
|
||||||
|
newOverlay.heightProperty().bind(newRoot.heightProperty());
|
||||||
|
newRoot.getChildren().add(newOverlay);
|
||||||
|
|
||||||
|
primaryStage.setScene(newScene);
|
||||||
|
|
||||||
|
FadeTransition fadeIn = new FadeTransition(Duration.millis(500), newOverlay);
|
||||||
|
fadeIn.setFromValue(1);
|
||||||
|
fadeIn.setToValue(0);
|
||||||
|
fadeIn.setOnFinished(ev -> {
|
||||||
|
newRoot.getChildren().remove(newOverlay);
|
||||||
|
isFading = false;
|
||||||
|
});
|
||||||
|
fadeIn.play();
|
||||||
|
});
|
||||||
|
fadeOut.play();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays an error on the JavaFX application thread.
|
* Displays an error on the JavaFX application thread.
|
||||||
*
|
*
|
||||||
@@ -180,8 +251,8 @@ public class GUI extends Application implements IView {
|
|||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if (error == ErrorType.SERVER_CRASHED) {
|
if (error == ErrorType.SERVER_CRASHED) {
|
||||||
controllerLogin.updateLoginButton(true);
|
controllerLogin.updateLoginButton(true);
|
||||||
primaryStage.setScene(loginScene);
|
|
||||||
controllerLogin.showError(error);
|
controllerLogin.showError(error);
|
||||||
|
fadeToScene(loginScene);
|
||||||
} else {
|
} else {
|
||||||
synchronized (miniModel) {
|
synchronized (miniModel) {
|
||||||
controllerMain.isError = true;
|
controllerMain.isError = true;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
module it.polimi.ingsw.gc14 {
|
module it.polimi.ingsw.gc14 {
|
||||||
requires javafx.controls;
|
requires javafx.controls;
|
||||||
requires javafx.fxml;
|
requires javafx.fxml;
|
||||||
|
requires javafx.media;
|
||||||
requires org.controlsfx.controls;
|
requires org.controlsfx.controls;
|
||||||
requires java.rmi;
|
requires java.rmi;
|
||||||
requires java.smartcardio;
|
requires java.smartcardio;
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 MiB After Width: | Height: | Size: 1.7 MiB |
@@ -146,4 +146,4 @@
|
|||||||
-fx-padding: 13 48 13 48; -fx-background-radius: 2;
|
-fx-padding: 13 48 13 48; -fx-background-radius: 2;
|
||||||
-fx-cursor: hand;"/>
|
-fx-cursor: hand;"/>
|
||||||
</VBox>
|
</VBox>
|
||||||
</StackPane>
|
</StackPane>
|
||||||
|
|||||||
Reference in New Issue
Block a user