Java tutorial
package com.github.vatbub.tictactoe.view; /*- * #%L * tictactoe * %% * Copyright (C) 2016 - 2017 Frederik Kammel * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import com.github.vatbub.common.core.Common; import com.github.vatbub.common.core.Config; import com.github.vatbub.common.core.logging.FOKLogger; import com.github.vatbub.common.view.core.ExceptionAlert; import com.github.vatbub.tictactoe.Board; import com.github.vatbub.tictactoe.NameList; import com.github.vatbub.tictactoe.Player; import com.github.vatbub.tictactoe.PlayerMode; import com.github.vatbub.tictactoe.common.Move; import com.github.vatbub.tictactoe.common.OnlineMultiplayerRequestOpponentResponse; import com.github.vatbub.tictactoe.common.ResponseCode; import com.github.vatbub.tictactoe.kryo.KryoGameConnections; import com.github.vatbub.tictactoe.kryo.OnOpponentFoundRunnable; import com.github.vatbub.tictactoe.view.refreshables.Refreshable; import com.github.vatbub.tictactoe.view.refreshables.RefreshableNodeList; import com.sun.javafx.tk.Toolkit; import javafx.animation.*; import javafx.application.Application; import javafx.application.Platform; import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.*; import javafx.scene.control.*; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.effect.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.MouseEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import javafx.scene.shape.*; import javafx.scene.shape.Rectangle; import javafx.scene.shape.Shape; import javafx.scene.text.Font; import javafx.scene.text.Text; import javafx.stage.Stage; import javafx.util.Callback; import javafx.util.Duration; import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; import org.controlsfx.control.ToggleSwitch; import java.awt.*; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.*; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; /** * The main game view */ @SuppressWarnings({ "JavaDoc", "WeakerAccess" }) public class Main extends Application { public static final double animationSpeed = 0.3; private static final String windowTitle = "Tic Tac Toe"; private static final int gameRows = 3; private static final int gameCols = 3; private static final String player1Letter = "X"; private static final String player2Letter = "O"; private static final double opponentsTurnLabelShownForAtLeastSeconds = 2; public static Main currentMainWindowInstance; private static Config applicationConfiguration; private static Stage stage; final StringProperty style = new SimpleStringProperty(""); private final AnimationThreadPoolExecutor guiAnimationQueue = new AnimationThreadPoolExecutor(2); private final RefreshableNodeList refreshedNodes = new RefreshableNodeList(); private final Map<String, Timer> loadTimerMap = new HashMap<>(); private final DoubleProperty aiLevelLabelPositionProperty = new SimpleDoubleProperty(); private final String player1SampleName = NameList.getNextName(); private final String player2SampleName = NameList.getNextName(); private final Timer runLaterTimer = new Timer(); private Board board; private ObjectProperty<Font> rowFont; private Rectangle aiLevelLabelClipRectangle; private boolean blockedForInput; private Calendar opponentsTurnLabelLastShown = Calendar.getInstance(); private ResourceBundle accessibilityBundle = ResourceBundle .getBundle("com.github.vatbub.tictactoe.view.accessibleDescriptions"); /** * We need to save this manually since {@code aiLevelSlider.isVisible} is delayed due to the animation */ private boolean aiLevelSliderVisible = true; @FXML private AnchorPane root; @FXML private AnchorPane gamePane; @FXML private TableView<Row> gameTable; @FXML private VBox menuBox; @FXML private AnchorPane menuBackground; @FXML private TextField player1Name; @FXML private ToggleSwitch player1AIToggle; @FXML private TextField player2Name; @FXML private ToggleSwitch player2AIToggle; @FXML private Group winLineGroup; @FXML private AnchorPane looserPane; @FXML private ImageView looseImage; @FXML private AnchorPane looseMessage; @FXML private AnchorPane tieMessage; @FXML private Label looserText; @FXML private Label currentPlayerLabel; @FXML private AnchorPane tiePane; @FXML private AnchorPane winPane; @FXML private ImageView confetti; @FXML private ImageView winningGirl; @FXML private AnchorPane winMessage; @FXML private Label winnerText; @FXML private ImageView bowTie; @FXML private Slider aiLevelSlider; @FXML private Pane aiLevelLabelPane; @FXML private HBox aiLevelLabelHBox; @FXML private Label aiLevelTitleLabel; @FXML private VBox menuSubBox; @FXML private Line aiLevelCenterLine; @FXML private AnchorPane playOnlineAnchorPane; @FXML private Hyperlink playOnlineHyperlink; @FXML private AnchorPane loadingBackground; @FXML private HBox loadingBox; @FXML private Label loadingStatusText; @FXML private VBox errorBox; @FXML private Label errorReasonLabel; @FXML private Label errorMessageLabel; @FXML private VBox onlineMenuBox; @FXML private VBox getOnlineMenuSubBox; @FXML private TextField onlineMyUsername; @FXML private TextField onlineDesiredOpponentName; @FXML private HBox opponentsTurnHBox; @FXML private AnchorPane opponentsTurnAnchorPane; @FXML private Label opponentsTurnLabel; @FXML private AnchorPane twoHumansWinnerPane; @FXML private ImageView twoHumansWinnerImage; @FXML private AnchorPane twoHumansWinMessage; @FXML private Label twoHumansWinnerText; @FXML private AnchorPane playOnlineClipAnchorPane; private Timeline updateMenuHeightTimeline; public Main() { super(); currentMainWindowInstance = this; } public static Config getApplicationConfiguration() { return applicationConfiguration; } public static void main(String[] args) { Common.getInstance().setAppName("tictactoev2"); FOKLogger.enableLoggingOfUncaughtExceptions(); try { applicationConfiguration = new Config(new URL( "https://raw.githubusercontent.com/vatbub/tictactoe/master/remoteconfig/client.properties"), Main.class.getResource("fallbackConfig.properties"), true, "tictactoeClientConfigCache.properties", true); } catch (IOException e) { FOKLogger.log(Main.class.getName(), Level.SEVERE, "Could not read the remote config", e); } for (String arg : args) { if (arg.toLowerCase().matches("mockappversion=.*")) { // Set the mock version String version = arg.substring(arg.toLowerCase().indexOf('=') + 1); Common.getInstance().setMockAppVersion(version); } else if (arg.toLowerCase().matches("mockbuildnumber=.*")) { // Set the mock build number String buildnumber = arg.substring(arg.toLowerCase().indexOf('=') + 1); Common.getInstance().setMockBuildNumber(buildnumber); } else if (arg.toLowerCase().matches("mockpackaging=.*")) { // Set the mock packaging String packaging = arg.substring(arg.toLowerCase().indexOf('=') + 1); Common.getInstance().setMockPackaging(packaging); } else if (arg.toLowerCase().matches("locale=.*")) { // set the gui language String guiLanguageCode = arg.substring(arg.toLowerCase().indexOf('=') + 1); FOKLogger.info(Main.class.getName(), "Setting language: " + guiLanguageCode); Locale.setDefault(new Locale(guiLanguageCode)); } } launch(args); } private String getWindowTitle() { String res = windowTitle; if (board != null && (board.getCurrentPlayer().getPlayerMode().equals(PlayerMode.internetHuman) || board.getOpponent(board.getCurrentPlayer()).getPlayerMode().equals(PlayerMode.internetHuman))) { Player internetPlayer; if (board.getCurrentPlayer().getPlayerMode().equals(PlayerMode.internetHuman)) { internetPlayer = board.getCurrentPlayer(); } else { internetPlayer = board.getOpponent(board.getCurrentPlayer()); } res = res + ": Playing against " + internetPlayer.getName(); if (board.getCurrentPlayer() == internetPlayer) { res = res + " (Opponent's turn)"; } else { res = res + " (Your turn)"; } } return res; } @FXML void onlineStartButtonOnAction(ActionEvent event) { hideOnlineMenu(); setLoadingStatusText("Searching for an opponent...", true); showLoadingScreen(); String clientIdentifier = onlineMyUsername.getText(); if (clientIdentifier.equals("")) { clientIdentifier = onlineMyUsername.getPromptText(); } String desiredOpponentIdentifier = null; if (!onlineDesiredOpponentName.getText().equals("")) { desiredOpponentIdentifier = onlineDesiredOpponentName.getText(); } KryoGameConnections.requestOpponent(clientIdentifier, desiredOpponentIdentifier, new OnOpponentFoundRunnable() { private boolean inversePlayerOrder = true; @Override public void run(OnlineMultiplayerRequestOpponentResponse response) { if (response.getResponseCode() == ResponseCode.WaitForOpponent) { inversePlayerOrder = false; } else { Main.this.setLoadingStatusText("Waiting for the opponent..."); Platform.runLater(() -> Main.this.startGame(response.getOpponentIdentifier(), inversePlayerOrder)); } } }, () -> Platform.runLater( () -> showErrorMessage("The game was cancelled.", "Disconnected from the server."))); } public void showErrorMessage(Throwable e) { showErrorMessage("Something went wrong.", e); } public void showErrorMessage(@SuppressWarnings("SameParameterValue") String errorMessage, Throwable e) { Throwable finalException; if (ExceptionUtils.getRootCause(e) != null) { finalException = ExceptionUtils.getRootCause(e); } else { finalException = e; } String errorText = finalException.getClass().getSimpleName(); if (finalException.getLocalizedMessage() != null) { errorText = errorText + ": " + finalException.getLocalizedMessage(); } showErrorMessage(errorMessage, errorText); } public void showErrorMessage(String errorMessage, String errorReason) { String finalErrorMessage = errorMessage + "\nPlease try again later.\nReason:"; errorMessageLabel.setText(finalErrorMessage); errorReasonLabel.setText(errorReason); showErrorScreen(); } @FXML void playOnlineHyperlinkOnAction(ActionEvent event) { if (!onlineMenuBox.isVisible()) { connectToRelayServer(); } else { guiAnimationQueue.submit(() -> { guiAnimationQueue.setBlocked(true); playOnlineHyperlink.setText("Play online"); fadeNode(onlineMenuBox, 0, () -> fadeNode(menuBox, 1, () -> guiAnimationQueue.setBlocked(false))); }); } } private void connectToRelayServer() { setLoadingStatusText("The server is waking up, hang tight...", true); showLoadingScreen(); Thread connectionThread = new Thread(() -> { int maxRetries = 10; int remainingRetries = maxRetries; AtomicBoolean readyWithoutException = new AtomicBoolean(false); Exception lastException = null; while (remainingRetries > 0 && !readyWithoutException.get()) { try { int finalRemainingRetries = remainingRetries; KryoGameConnections.connect(() -> { if (maxRetries == finalRemainingRetries) // should only appear the first time setLoadingStatusText("Connecting to the server..."); }, () -> Platform.runLater(() -> { readyWithoutException.set(true); hideLoadingScreen(); showOnlineMenu(); })); } catch (Exception e) { remainingRetries--; setLoadingStatusText("This is taking longer than usual, hang tight (Retry " + (maxRetries - remainingRetries) + " of " + maxRetries + ")..."); FOKLogger.log(Main.class.getName(), Level.SEVERE, "Could not connect to the relay server: " + e.getMessage(), e); lastException = e; } } if (!readyWithoutException.get()) { if (lastException == null) { Platform.runLater(() -> showErrorMessage("Something went wrong.", "Unknown")); } else { Exception finalLastException = lastException; Platform.runLater(() -> showErrorMessage(finalLastException)); } } }); connectionThread.setName("connectionThread"); connectionThread.start(); } private void showErrorScreen() { blurGamePane(); fadeNode(loadingBackground, 0.8); fadeNode(errorBox, 1, true); } private void hideErrorScreen() { fadeNode(errorBox, 0, true); } private void showLoadingScreen() { guiAnimationQueue.submitWaitForUnlock(() -> { guiAnimationQueue.setBlocked(true); fadeNode(loadingBackground, 0.8); fadeNode(loadingBox, 1, () -> guiAnimationQueue.setBlocked(false)); }); } private void hideLoadingScreen() { guiAnimationQueue.submitWaitForUnlock(() -> { guiAnimationQueue.setBlocked(true); fadeNode(loadingBackground, 0); fadeNode(loadingBox, 0, () -> guiAnimationQueue.setBlocked(false)); }); } private void setLoadingStatusText(@SuppressWarnings("SameParameterValue") String textToSet) { setLoadingStatusText(textToSet, false); } private void setLoadingStatusText(String textToSet, boolean noAnimation) { if (!loadingStatusText.getText().equals(textToSet) && !noAnimation) { KeyValue keyValueTranslation1 = new KeyValue(loadingStatusText.translateYProperty(), -loadingStatusText.getHeight()); KeyValue keyValueOpacity1 = new KeyValue(loadingStatusText.opacityProperty(), 0); KeyFrame keyFrame1 = new KeyFrame(Duration.seconds(animationSpeed), keyValueOpacity1, keyValueTranslation1); Timeline timeline1 = new Timeline(keyFrame1); timeline1.setOnFinished((event) -> { loadingStatusText.setText(textToSet); loadingStatusText.setTranslateY(loadingStatusText.getHeight()); KeyValue keyValueTranslation2 = new KeyValue(loadingStatusText.translateYProperty(), 0); KeyValue keyValueOpacity2 = new KeyValue(loadingStatusText.opacityProperty(), 1); KeyFrame keyFrame2 = new KeyFrame(Duration.seconds(animationSpeed), keyValueOpacity2, keyValueTranslation2); Timeline timeline2 = new Timeline(keyFrame2); timeline2.play(); }); timeline1.play(); } else { loadingStatusText.setText(textToSet); } } private void showOnlineMenu() { guiAnimationQueue.submit(() -> { guiAnimationQueue.setBlocked(true); playOnlineHyperlink.setText("Play offline"); fadeNode(menuBox, 0); showMenuBackground(); fadeNode(onlineMenuBox, 1, () -> guiAnimationQueue.setBlocked(false)); }); } private void hideOnlineMenu() { guiAnimationQueue.submit(() -> { guiAnimationQueue.setBlocked(true); playOnlineHyperlink.setText("Play online"); fadeNode(onlineMenuBox, 0, () -> guiAnimationQueue.setBlocked(false)); }); } @FXML void errorRetryOnAction(ActionEvent event) { hideErrorScreen(); connectToRelayServer(); } @FXML void errorPlayOfflineOnAction(ActionEvent event) { hideErrorScreen(); hideLoadingScreen(); showMenu(); } /** * The main entry point for all JavaFX applications. * The start method is called after the init method has returned, * and after the system is ready for the application to begin running. * <p> * NOTE: This method is called on the JavaFX Application Thread. * </p> * * @param primaryStage the primary stage for this application, onto which * the application scene can be set. The primary stage will be embedded in * the browser if the application was launched as an applet. * Applications may create other stages, if needed, but they will not be * primary stages and will not be embedded in the browser. */ @Override public void start(Stage primaryStage) throws Exception { Parent root = FXMLLoader.load(getClass().getResource("View.fxml")); Scene scene = new Scene(root); stage = primaryStage; primaryStage.setMinWidth(scene.getRoot().minWidth(0) + 70); primaryStage.setMinHeight(scene.getRoot().minHeight(0) + 70); primaryStage.setScene(scene); // Set Icon primaryStage.getIcons().add(new Image(Main.class.getResourceAsStream("icon.png"))); primaryStage.setTitle(getWindowTitle()); primaryStage.show(); } @Override public void stop() { try { if (KryoGameConnections.isGameConnected()) { KryoGameConnections.sendCancelGameRequest(); } KryoGameConnections.resetConnections(); } catch (Exception e) { FOKLogger.log(Main.class.getName(), Level.SEVERE, "Exception in the application stop method", e); } System.exit(0); } @FXML // This method is called by the FXMLLoader when initialization is complete void initialize() { // modify the default exception handler to show a good error message on every uncaught exception final Thread.UncaughtExceptionHandler currentUncaughtExceptionHandler = Thread .getDefaultUncaughtExceptionHandler(); Thread.setDefaultUncaughtExceptionHandler((thread, exception) -> { if (currentUncaughtExceptionHandler != null) { // execute current handler as we only want to append it currentUncaughtExceptionHandler.uncaughtException(thread, exception); } Platform.runLater(() -> new ExceptionAlert(exception).showAndWait()); }); opponentsTurnHBox.heightProperty() .addListener((observable, oldValue, newValue) -> updateOpponentsTurnHBox(false)); aiLevelLabelClipRectangle = new Rectangle(0, 0, 0, 0); aiLevelLabelClipRectangle.setEffect(new MotionBlur(0, 10)); aiLevelLabelPane.setClip(aiLevelLabelClipRectangle); aiLevelLabelClipRectangle.heightProperty().bind(aiLevelLabelPane.heightProperty()); aiLevelLabelPane.widthProperty().addListener((observable, oldValue, newValue) -> updateAILevelLabel(true)); Rectangle menuSubBoxClipRectangle = new Rectangle(0, 0, 0, 0); menuSubBox.setClip(menuSubBoxClipRectangle); menuSubBoxClipRectangle.heightProperty().bind(menuSubBox.heightProperty()); menuSubBoxClipRectangle.widthProperty().bind(menuSubBox.widthProperty()); Rectangle playOnlineClipRectangle = new Rectangle(0, 0, 0, 0); playOnlineClipAnchorPane.setClip(playOnlineClipRectangle); playOnlineClipRectangle.heightProperty().bind(playOnlineClipAnchorPane.heightProperty()); playOnlineClipRectangle.widthProperty().bind(playOnlineClipAnchorPane.widthProperty()); player1SetSampleName(); player2SetSampleName(); gameTable.heightProperty() .addListener((observable, oldValue, newValue) -> refreshedNodes.refreshAll(gameTable.getWidth(), oldValue.doubleValue(), gameTable.getWidth(), newValue.doubleValue())); gameTable.widthProperty() .addListener((observable, oldValue, newValue) -> refreshedNodes.refreshAll(oldValue.doubleValue(), gameTable.getHeight(), newValue.doubleValue(), gameTable.getHeight())); player1AIToggle.selectedProperty().addListener((observable, oldValue, newValue) -> { showHideAILevelSlider(newValue, player2AIToggle.isSelected()); player1SetSampleName(); }); player2AIToggle.selectedProperty().addListener((observable, oldValue, newValue) -> { showHideAILevelSlider(player1AIToggle.isSelected(), newValue); player2SetSampleName(); }); gameTable.setSelectionModel(null); gameTable.heightProperty().addListener((observable, oldValue, newValue) -> { Pane header = (Pane) gameTable.lookup("TableHeaderRow"); if (header.isVisible()) { header.setMaxHeight(0); header.setMinHeight(0); header.setPrefHeight(0); header.setVisible(false); } renderRows(); }); gameTable.setRowFactory(param -> { TableRow<Row> row = new TableRow<>(); row.styleProperty().bind(style); if (rowFont == null) { rowFont = new SimpleObjectProperty<>(); rowFont.bind(row.fontProperty()); } return row; }); looseImage.fitHeightProperty().bind(looserPane.heightProperty()); looseImage.fitWidthProperty().bind(looserPane.widthProperty()); looseImage.fitHeightProperty().addListener((observable, oldValue, newValue) -> reloadImage(looseImage, getClass().getResource("loose.png").toString(), looseImage.getFitWidth(), newValue.doubleValue())); looseImage.fitWidthProperty().addListener((observable, oldValue, newValue) -> reloadImage(looseImage, getClass().getResource("loose.png").toString(), newValue.doubleValue(), looseImage.getFitWidth())); confetti.fitHeightProperty().bind(winPane.heightProperty()); confetti.fitWidthProperty().bind(winPane.widthProperty()); confetti.fitHeightProperty().addListener((observable, oldValue, newValue) -> reloadImage(confetti, getClass().getResource("confetti.png").toString(), confetti.getFitWidth(), newValue.doubleValue())); confetti.fitWidthProperty().addListener((observable, oldValue, newValue) -> reloadImage(confetti, getClass().getResource("confetti.png").toString(), newValue.doubleValue(), confetti.getFitWidth())); aiLevelSlider.valueProperty().addListener((observable, oldValue, newValue) -> updateAILevelLabel()); playOnlineHyperlink.widthProperty().addListener((observable, oldValue, newValue) -> { if (playOnlineAnchorPane.isVisible()) { setLowerRightAnchorPaneDimensions(playOnlineHyperlink, currentPlayerLabel, true); } }); playOnlineHyperlink.heightProperty().addListener((observable, oldValue, newValue) -> { if (playOnlineAnchorPane.isVisible()) { setLowerRightAnchorPaneDimensions(playOnlineHyperlink, currentPlayerLabel, true); } }); playOnlineHyperlink.textProperty().addListener((observable, oldValue, newValue) -> { if (!newValue.equals(oldValue)) { if (newValue.contains("ff")) { setLowerRightAnchorPaneDimensions(playOnlineHyperlink, currentPlayerLabel, true, 1); } else { setLowerRightAnchorPaneDimensions(playOnlineHyperlink, currentPlayerLabel, true, -1); } } }); // Kunami code root.setOnKeyPressed(event -> { if (KunamiCode.isCompleted(event.getCode())) { if (root.getEffect() != null && root.getEffect() instanceof Blend) { BlendMode currentMode = ((Blend) root.getEffect()).getMode(); BlendMode nextMode; if (currentMode == BlendMode.values()[BlendMode.values().length - 1]) { nextMode = BlendMode.values()[0]; } else { nextMode = BlendMode.values()[Arrays.asList(BlendMode.values()).indexOf(currentMode) + 1]; } ((Blend) root.getEffect()).setMode(nextMode); } else { root.setEffect(new Blend(BlendMode.EXCLUSION)); } } }); // prompt text of the my username field in the online multiplayer menu onlineMyUsername.promptTextProperty().bind(player1Name.promptTextProperty()); onlineMyUsername.textProperty().bindBidirectional(player1Name.textProperty()); setAccessibleTextsForNodesThatDoNotChange(); updateAccessibleTexts(); initBoard(); initNewGame(); } private void player1SetSampleName() { player1Name.setPromptText(player1AIToggle.isSelected() ? player1SampleName + " (AI)" : player1SampleName); } private void player2SetSampleName() { player2Name.setPromptText(player2AIToggle.isSelected() ? player2SampleName + " (AI)" : player2SampleName); } private void reloadImage(ImageView imageView, String imageURL, double newWidth, double newHeight) { if (loadTimerMap.get(imageURL) != null) { loadTimerMap.get(imageURL).cancel(); } Timer loadTimer = new Timer(); loadTimerMap.put(imageURL, loadTimer); loadTimer.schedule(new TimerTask() { @Override public void run() { Image image = new Image(imageURL, newWidth, newHeight, false, true); Platform.runLater(() -> imageView.setImage(image)); } }, 300); } @FXML void startButtonOnAction(ActionEvent event) { startGame(); } @FXML void newGameOnAction(ActionEvent event) { initNewGame(); } @FXML void thinkOnAction(ActionEvent event) { if (!isBlockedForInput()) { setBlockedForInput(true); boolean opponentIsInternetPlayer = board.getOpponent(board.getCurrentPlayer()).getPlayerMode() .equals(PlayerMode.internetHuman); board.getCurrentPlayer().doAiTurn(board, AILevel.UNBEATABLE); updateCurrentPlayerLabel(false, opponentIsInternetPlayer); renderRows(); } else { flashOpponentsTurnHBox(); } } @FXML void player1AIToggleOnClick(MouseEvent event) { updateAccessibleTexts(); player1AIToggle.requestFocus(); player1AIToggle.notifyAccessibleAttributeChanged(AccessibleAttribute.SELECTED); } @FXML void player2AIToggleOnClick(MouseEvent event) { updateAccessibleTexts(); player2AIToggle.requestFocus(); player2AIToggle.notifyAccessibleAttributeChanged(AccessibleAttribute.SELECTED); } private void setAccessibleTextsForNodesThatDoNotChange() { player1AIToggle.setAccessibleRole(AccessibleRole.TOGGLE_BUTTON); player2AIToggle.setAccessibleRole(AccessibleRole.TOGGLE_BUTTON); aiLevelLabelHBox.getChildren().get(0).setAccessibleText(accessibilityBundle.getString("aiStupid")); aiLevelLabelHBox.getChildren().get(1).setAccessibleText(accessibilityBundle.getString("aiMedium")); aiLevelLabelHBox.getChildren().get(2).setAccessibleText(accessibilityBundle.getString("aiGood")); aiLevelLabelHBox.getChildren().get(3).setAccessibleText(accessibilityBundle.getString("aiUnbeatable")); } @FXML void updateAccessibleTexts() { String name1; if (player1Name.getText().equals("")) { name1 = player1Name.getPromptText(); } else { name1 = player1Name.getText(); } player1Name.setAccessibleText( accessibilityBundle.getString("playerNameTextField").replace("%n", "1") + " " + name1); String name2; if (player2Name.getText().equals("")) { name2 = player2Name.getPromptText(); } else { name2 = player2Name.getText(); } player2Name.setAccessibleText( accessibilityBundle.getString("playerNameTextField").replace("%n", "2") + " " + name2); player1AIToggle.setAccessibleText(getAccessibilityAIToggleText(1, player1AIToggle.isSelected())); player2AIToggle.setAccessibleText(getAccessibilityAIToggleText(2, player2AIToggle.isSelected())); String aiAccessibilityText = accessibilityBundle.getString("aiLevelPrefix") + " "; int sliderPos = (int) Math.round(aiLevelSlider.getValue() * 3.0 / 100.0); switch (sliderPos) { case 0: aiAccessibilityText += accessibilityBundle.getString("aiStupid"); break; case 1: aiAccessibilityText += accessibilityBundle.getString("aiMedium"); break; case 2: aiAccessibilityText += accessibilityBundle.getString("aiGood"); break; case 3: aiAccessibilityText += accessibilityBundle.getString("aiUnbeatable"); break; } aiLevelSlider.setAccessibleText(aiAccessibilityText); } private String getAccessibilityAIToggleText(int playerNumber, boolean status) { String statusText; if (status) { statusText = accessibilityBundle.getString("ai"); } else { statusText = accessibilityBundle.getString("human"); } return accessibilityBundle.getString("playerAIStatus").replace("%n", Integer.toString(playerNumber)) + " " + statusText; } private double getAILevelLabelCenter(int labelIndex) { double res = 0; for (int i = 0; i < labelIndex; i++) { res = res + ((Label) aiLevelLabelHBox.getChildren().get(i)).getWidth(); } res = res + labelIndex * aiLevelLabelHBox.getSpacing(); res = res - aiLevelLabelPane.getWidth() / 2; res = res + ((Label) aiLevelLabelHBox.getChildren().get(labelIndex)).getWidth() / 2; return res; } private void updateAILevelLabel() { updateAILevelLabel(false); } private void updateAILevelLabel(boolean forceUpdate) { double sliderPos = 100 * Math.round(aiLevelSlider.getValue() * 3.0 / 100.0) / 3.0; if (sliderPos != aiLevelLabelPositionProperty.get() || forceUpdate) { aiLevelLabelPositionProperty.set(sliderPos); // request focus of the current ai label for accessibility aiLevelLabelHBox.getChildren().get((int) (sliderPos * 3 / 100)).requestFocus(); updateAccessibleTexts(); // get the slider position double[] xDouble = new double[] { 0, 100.0 / 3.0, 200.0 / 3.0, 300.0 / 3.0 }; double[] translationYDouble = new double[4]; double[] widthYDouble = new double[4]; double[] trueWidthYDouble = new double[4]; for (int i = 0; i < translationYDouble.length; i++) { // {-getAILevelLabelCenter(0), -getAILevelLabelCenter(1), -getAILevelLabelCenter(2), -getAILevelLabelCenter(3)}; translationYDouble[i] = -getAILevelLabelCenter(i); widthYDouble[i] = Math.max(90, ((Label) aiLevelLabelHBox.getChildren().get(i)).getWidth() + 8 * aiLevelLabelHBox.getSpacing()); trueWidthYDouble[i] = ((Label) aiLevelLabelHBox.getChildren().get(i)).getWidth(); } SplineInterpolator splineInterpolator = new SplineInterpolator(); PolynomialSplineFunction translateFunction = splineInterpolator.interpolate(xDouble, translationYDouble); PolynomialSplineFunction widthFunction = splineInterpolator.interpolate(xDouble, widthYDouble); PolynomialSplineFunction trueWidthFunction = splineInterpolator.interpolate(xDouble, trueWidthYDouble); KeyValue hBoxLayoutXKeyValue1 = new KeyValue(aiLevelLabelHBox.layoutXProperty(), aiLevelLabelHBox.getLayoutX(), Interpolator.EASE_BOTH); KeyValue aiLevelLabelClipRectangleWidthKeyValue1 = new KeyValue( aiLevelLabelClipRectangle.widthProperty(), aiLevelLabelClipRectangle.getWidth(), Interpolator.EASE_BOTH); KeyValue aiLevelLabelClipRectangleXKeyValue1 = new KeyValue(aiLevelLabelClipRectangle.xProperty(), aiLevelLabelClipRectangle.getX(), Interpolator.EASE_BOTH); KeyValue aiLevelCenterLineStartXKeyValue1 = new KeyValue(aiLevelCenterLine.startXProperty(), aiLevelCenterLine.getStartX(), Interpolator.EASE_BOTH); KeyValue aiLevelCenterLineEndXKeyValue1 = new KeyValue(aiLevelCenterLine.endXProperty(), aiLevelCenterLine.getEndX(), Interpolator.EASE_BOTH); KeyFrame keyFrame1 = new KeyFrame(Duration.seconds(0), hBoxLayoutXKeyValue1, aiLevelLabelClipRectangleWidthKeyValue1, aiLevelLabelClipRectangleXKeyValue1, aiLevelCenterLineStartXKeyValue1, aiLevelCenterLineEndXKeyValue1); double interpolatedLabelWidth = trueWidthFunction.value(sliderPos); KeyValue hBoxLayoutXKeyValue2 = new KeyValue(aiLevelLabelHBox.layoutXProperty(), translateFunction.value(sliderPos), Interpolator.EASE_BOTH); KeyValue aiLevelLabelClipRectangleWidthKeyValue2 = new KeyValue( aiLevelLabelClipRectangle.widthProperty(), widthFunction.value(sliderPos), Interpolator.EASE_BOTH); KeyValue aiLevelLabelClipRectangleXKeyValue2 = new KeyValue(aiLevelLabelClipRectangle.xProperty(), aiLevelLabelPane.getWidth() / 2 - widthFunction.value(sliderPos) / 2, Interpolator.EASE_BOTH); KeyValue aiLevelCenterLineStartXKeyValue2 = new KeyValue(aiLevelCenterLine.startXProperty(), (aiLevelLabelPane.getWidth() - interpolatedLabelWidth) / 2, Interpolator.EASE_BOTH); KeyValue aiLevelCenterLineEndXKeyValue2 = new KeyValue(aiLevelCenterLine.endXProperty(), (aiLevelLabelPane.getWidth() + interpolatedLabelWidth) / 2, Interpolator.EASE_BOTH); KeyFrame keyFrame2 = new KeyFrame(Duration.seconds(animationSpeed * 0.9), hBoxLayoutXKeyValue2, aiLevelLabelClipRectangleWidthKeyValue2, aiLevelLabelClipRectangleXKeyValue2, aiLevelCenterLineStartXKeyValue2, aiLevelCenterLineEndXKeyValue2); Timeline timeline = new Timeline(keyFrame1, keyFrame2); timeline.play(); } } @FXML void aboutLinkOnAction(ActionEvent event) { try { Desktop.getDesktop().browse(new URI("https://github.com/vatbub/tictactoe#tictactoe")); } catch (URISyntaxException | IOException e) { FOKLogger.log(Main.class.getName(), Level.SEVERE, "Typo in a hardcoded value", e); } } public void initNewGame() { if (KryoGameConnections.isGameConnected()) { KryoGameConnections.sendCancelGameRequest(); } KryoGameConnections.resetConnections(); guiAnimationQueue.submit(() -> { if (looserPane.isVisible()) { blurLooserPane(); } if (tiePane.isVisible()) { blurTiePane(); } if (winPane.isVisible()) { blurWinPane(); } if (twoHumansWinnerPane.isVisible()) { blurTwoHumansWinnerPane(); } updateAILevelLabel(true); if (!isMenuShown()) { showMenu(); } }); } private void startGame() { startGame(null, false); } private void startGame(String onlineOpponentName, boolean inversePlayerOrderForOnlineGame) { initBoard(); if (looserPane.isVisible()) { fadeNode(looserPane, 0, true); } if (tiePane.isVisible()) { fadeNode(tiePane, 0); } if (winPane.isVisible()) { fadeNode(winPane, 0); } if (twoHumansWinnerPane.isVisible()) { fadeNode(twoHumansWinnerPane, 0); } hideMenu(); hideOnlineMenu(); hideLoadingScreen(); fadeWinLineGroup(); if (onlineOpponentName != null) { guiAnimationQueue.submit(() -> { String finalOnlineUsername = onlineMyUsername.getText(); if (finalOnlineUsername.equals("")) { finalOnlineUsername = onlineMyUsername.getPromptText(); } if (!inversePlayerOrderForOnlineGame) { board.setPlayer1(new Player(PlayerMode.localHuman, finalOnlineUsername, player1Letter)); board.setPlayer2(new Player(PlayerMode.internetHuman, onlineOpponentName, player2Letter)); } else { board.setPlayer1(new Player(PlayerMode.internetHuman, onlineOpponentName, player1Letter)); board.setPlayer2(new Player(PlayerMode.localHuman, finalOnlineUsername, player2Letter)); } updateCurrentPlayerLabel(true, inversePlayerOrderForOnlineGame); KryoGameConnections.setConnectedBoard(board); }); } else { guiAnimationQueue.submit(() -> { String finalPlayerName1 = player1Name.getText(); if (finalPlayerName1.equals("")) { finalPlayerName1 = player1Name.getPromptText(); } String finalPlayerName2 = player2Name.getText(); if (finalPlayerName2.equals("")) { finalPlayerName2 = player2Name.getPromptText(); } board.setPlayer1(new Player(player1AIToggle.isSelected() ? PlayerMode.ai : PlayerMode.localHuman, finalPlayerName1, player1Letter)); board.setPlayer2(new Player(player2AIToggle.isSelected() ? PlayerMode.ai : PlayerMode.localHuman, finalPlayerName2, player2Letter)); updateCurrentPlayerLabel(true); if (board.getPlayer1().isAi()) { board.getPlayer1().doAiTurn(board); } }); } } public void updateCurrentPlayerLabel() { updateCurrentPlayerLabel(false); } public void updateCurrentPlayerLabel(boolean noAnimation) { updateCurrentPlayerLabel(noAnimation, false); } public void updateCurrentPlayerLabel(boolean noAnimation, boolean setBlockedValueAfterAnimation) { Platform.runLater(() -> stage.setTitle(getWindowTitle())); if (board.getCurrentPlayer() != null) { if (!board.getCurrentPlayer().getLetter().equals(currentPlayerLabel.getText())) { if (noAnimation) { setCurrentPlayerValue(); } else { guiAnimationQueue.submitWaitForUnlock(() -> { guiAnimationQueue.setBlocked(true); GaussianBlur blur = (GaussianBlur) currentPlayerLabel.getEffect(); if (blur == null) { blur = new GaussianBlur(0); } Calendar changeLabelTextDate = Calendar.getInstance(); changeLabelTextDate.add(Calendar.MILLISECOND, (int) (animationSpeed * 1000)); runLaterTimer.schedule(new TimerTask() { @Override public void run() { Platform.runLater(() -> setCurrentPlayerValue()); } }, changeLabelTextDate.getTime()); currentPlayerLabel.setEffect(blur); Timeline timeline = new Timeline(); KeyValue keyValue1 = new KeyValue(blur.radiusProperty(), 20); KeyFrame keyFrame1 = new KeyFrame(Duration.seconds(animationSpeed), keyValue1); KeyValue keyValue2 = new KeyValue(blur.radiusProperty(), 0); KeyFrame keyFrame2 = new KeyFrame(Duration.seconds(2 * animationSpeed), keyValue2); timeline.getKeyFrames().addAll(keyFrame1, keyFrame2); timeline.setOnFinished((event) -> { currentPlayerLabel.setEffect(null); guiAnimationQueue.setBlocked(false); setBlockedForInput(setBlockedValueAfterAnimation); }); timeline.play(); }); return; } } } guiAnimationQueue.setBlocked(false); setBlockedForInput(setBlockedValueAfterAnimation); } private void setCurrentPlayerValue() { Player currentPlayer = board.getCurrentPlayer(); if (currentPlayer != null) { currentPlayerLabel.setText(currentPlayer.getLetter()); } } private void initBoard() { guiAnimationQueue.submit(() -> { board = new Board(gameRows, gameCols); // set the ai level int sliderPos = (int) Math.round(aiLevelSlider.getValue() * 3.0 / 100.0); switch (sliderPos) { case 0: board.setAiLevel(AILevel.COMPLETELY_STUPID); break; case 1: board.setAiLevel(AILevel.SOMEWHAT_GOOD); break; case 2: board.setAiLevel(AILevel.GOOD); break; case 3: board.setAiLevel(AILevel.UNBEATABLE); break; } board.setGameEndCallback((winnerInfo) -> guiAnimationQueue.submit(() -> { // disconnect after ending the game if (board.getCurrentPlayer().getPlayerMode().equals(PlayerMode.internetHuman) || board .getOpponent(board.getCurrentPlayer()).getPlayerMode().equals(PlayerMode.internetHuman)) { KryoGameConnections.resetConnections(); } updateOpponentsTurnHBox(false, false); FOKLogger.info(Main.class.getName(), "The winner is: " + winnerInfo.winningPlayer.getName()); if (winnerInfo.isTie()) { showTie(); } else if (winnerInfo.winningPlayer.getPlayerMode().equals(PlayerMode.localHuman) && !board .getOpponent(winnerInfo.winningPlayer).getPlayerMode().equals(PlayerMode.localHuman)) { showWinner(winnerInfo); } else if (winnerInfo.winningPlayer.getPlayerMode().equals(PlayerMode.localHuman) && board .getOpponent(winnerInfo.winningPlayer).getPlayerMode().equals(PlayerMode.localHuman)) { showWinnerWithTwoHumanPlayers(winnerInfo); } else { showLooser(winnerInfo); } })); while (gameTable.getColumns().size() > 0) { gameTable.getColumns().remove(0); } for (int i = 0; i < gameCols; i++) { TableColumn<Row, String> column = new TableColumn<>(Integer.toString(i + 1)); //noinspection Convert2Lambda int finalI = i; column.setCellValueFactory(p -> new SimpleStringProperty(p.getValue().getValues().get(finalI))); column.setCellFactory(new Callback<TableColumn<Row, String>, TableCell<Row, String>>() { @Override public TableCell<Row, String> call(TableColumn col) { TableCell<Row, String> cell = new TableCell<Row, String>() { // The updateItem method is what is called when setting the cell's text. You can customize formatting here @Override protected void updateItem(String item, boolean empty) { // calling super here is very important - don't skip this! super.updateItem(item, empty); if (item != null) { setText(item); } } }; cell.setOnMouseClicked(event -> { if (board.getPlayerAt(cell.getIndex(), gameTable.getColumns().indexOf(col)) == null && !isBlockedForInput()) { setBlockedForInput(true); boolean opponentIsInternetPlayer = board.getOpponent(board.getCurrentPlayer()) .getPlayerMode().equals(PlayerMode.internetHuman); board.doTurn(new Move(cell.getIndex(), gameTable.getColumns().indexOf(col))); updateCurrentPlayerLabel(false, opponentIsInternetPlayer); renderRows(); } else if (isBlockedForInput()) { flashOpponentsTurnHBox(); } }); return cell; } }); column.setStyle("-fx-alignment: CENTER; -fx-padding: 0;"); gameTable.getColumns().add(column); } renderRows(); }); } public void renderRows() { guiAnimationQueue.submit(() -> { ObservableList<Row> generatedRows = FXCollections.observableArrayList(); for (int r = 0; r < board.getRowCount(); r++) { List<String> values = new ArrayList<>(); for (int c = 0; c < board.getColumnCount(); c++) { if (board.getPlayerAt(r, c) == null) { values.add(""); } else if (board.getPlayerAt(r, c) == board.getPlayer1()) { values.add(player1Letter); } else if (board.getPlayerAt(r, c) == board.getPlayer2()) { values.add(player2Letter); } } generatedRows.add(new Row(values)); } gameTable.setItems(generatedRows); double effectiveHeight = gameTable.getHeight() - 5; long fontSize = Math.round((effectiveHeight - 250) / board.getRowCount()); // get letter widths; if (rowFont != null) { Font font = new Font(rowFont.getName(), fontSize); double player1SymbolWidth = Toolkit.getToolkit().getFontLoader().computeStringWidth(player1Letter, font); double player2SymbolWidth = Toolkit.getToolkit().getFontLoader().computeStringWidth(player2Letter, font); // make the font smaller so that it fits the cell even if the width is very small while (player1SymbolWidth > (gameTable.getWidth() / board.getColumnCount()) || player2SymbolWidth + 10 > (gameTable.getWidth() / board.getColumnCount())) { fontSize = fontSize - 1; font = new Font(rowFont.getName(), fontSize); player1SymbolWidth = Toolkit.getToolkit().getFontLoader().computeStringWidth(player1Letter, font); player2SymbolWidth = Toolkit.getToolkit().getFontLoader().computeStringWidth(player2Letter, font); } style.set("-fx-font-size:" + fontSize + "px; -fx-padding: 0;"); } gameTable.setFixedCellSize(effectiveHeight / board.getRowCount()); gameTable.refresh(); }); } private boolean isMenuShown() { return menuBox.isVisible(); } private void showMenu() { guiAnimationQueue.submit(() -> { fadeNode(menuBox, 1); menuBox.setVisible(true); }); showMenuBackground(); } private void showMenuBackground() { guiAnimationQueue.submit(() -> { fadeNode(menuBackground, 0.12); if (currentPlayerLabel.isVisible()) { setLowerRightAnchorPaneDimensions(playOnlineHyperlink, currentPlayerLabel); } blurGamePane(); menuBackground.setVisible(true); }); } private void setLowerRightAnchorPaneDimensions(Labeled nodeToShow, Labeled nodeToHide) { setLowerRightAnchorPaneDimensions(nodeToShow, nodeToHide, false); } private void setLowerRightAnchorPaneDimensions(Labeled nodeToShow, Labeled nodeToHide, boolean noAnimation) { setLowerRightAnchorPaneDimensions(nodeToShow, nodeToHide, noAnimation, 0); } private double computeTextWidth(Font font, String text) { Text sampleText = new Text(text); sampleText.setFont(font); return sampleText.getLayoutBounds().getWidth(); } private void setLowerRightAnchorPaneDimensions(Labeled nodeToShow, Labeled nodeToHide, boolean noAnimation, double widthOffset) { final double secondAnimationOffset = 0.1; if (widthOffset > 0) { nodeToShow.setPrefWidth(nodeToShow.getWidth() + widthOffset); } if (noAnimation) { nodeToShow.setOpacity(1); nodeToShow.setVisible(true); nodeToHide.setVisible(false); playOnlineAnchorPane.setPrefHeight(nodeToShow.getHeight()); playOnlineAnchorPane.setPrefWidth(nodeToShow.getWidth()); } else { nodeToShow.setOpacity(0); nodeToShow.setVisible(true); nodeToShow.setPrefWidth(nodeToShow.getWidth()); nodeToShow.setPrefHeight(nodeToShow.getHeight()); DoubleProperty firstProperty; DoubleProperty secondProperty; double firstPropertyValue; double secondPropertyValue; double secondPropertyInitialValue; if (nodeToShow.getHeight() > nodeToShow.getWidth()) { firstProperty = playOnlineAnchorPane.prefHeightProperty(); firstPropertyValue = nodeToShow.getHeight() + AnchorPane.getBottomAnchor(nodeToShow); secondProperty = playOnlineAnchorPane.prefWidthProperty(); secondPropertyValue = nodeToShow.getWidth() + AnchorPane.getRightAnchor(nodeToShow); secondPropertyInitialValue = nodeToHide.getWidth(); } else { firstProperty = playOnlineAnchorPane.prefWidthProperty(); firstPropertyValue = nodeToShow.getWidth() + AnchorPane.getRightAnchor(nodeToShow); secondProperty = playOnlineAnchorPane.prefHeightProperty(); secondPropertyValue = nodeToShow.getHeight() + AnchorPane.getBottomAnchor(nodeToShow); secondPropertyInitialValue = nodeToHide.getHeight(); } KeyValue keyValueFirstProperty = new KeyValue(firstProperty, firstPropertyValue, Interpolator.EASE_BOTH); KeyValue keyValueNodeToHideOpacity1 = new KeyValue(nodeToHide.opacityProperty(), 0, Interpolator.EASE_IN); KeyFrame keyFrame1 = new KeyFrame(Duration.seconds(animationSpeed), keyValueFirstProperty, keyValueNodeToHideOpacity1); KeyValue keyValueSecondProperty1 = new KeyValue(secondProperty, secondPropertyInitialValue); KeyValue keyValueNodeToShowOpacity1 = new KeyValue(nodeToShow.opacityProperty(), nodeToShow.getOpacity()); KeyFrame keyFrame2 = new KeyFrame(Duration.seconds(secondAnimationOffset * animationSpeed), keyValueSecondProperty1, keyValueNodeToShowOpacity1); KeyValue keyValueSecondProperty2 = new KeyValue(secondProperty, secondPropertyValue, Interpolator.EASE_BOTH); KeyValue keyValueNodeToShowOpacity2 = new KeyValue(nodeToShow.opacityProperty(), 1, Interpolator.EASE_OUT); KeyFrame keyFrame3 = new KeyFrame(Duration.seconds((1 + secondAnimationOffset) * animationSpeed), keyValueSecondProperty2, keyValueNodeToShowOpacity2); Timeline timeline = new Timeline(keyFrame1, keyFrame2, keyFrame3); timeline.setOnFinished((event) -> nodeToHide.setVisible(false)); timeline.play(); } } private void hideMenu() { guiAnimationQueue.submit(() -> { fadeNode(menuBackground, 0); fadeNode(menuBox, 0); setLowerRightAnchorPaneDimensions(currentPlayerLabel, playOnlineHyperlink); unblurGamePane(); }); } private void showWinner(Board.WinnerInfo winnerInfo) { guiAnimationQueue.submitWaitForUnlock( () -> addWinLineOnWin(winnerInfo, new Color(1.0, 145.0 / 255.0, 30.0 / 255.0, 1.0), () -> { winnerText.setText(winnerInfo.winningPlayer.getName() + " won :)"); double endX = winningGirl.getX(); double endY = winningGirl.getY(); double confettiOffset = 30; double confettiX = confetti.getX(); double confettiY = confetti.getY(); AnchorPane.clearConstraints(winningGirl); winningGirl.setX(endX); winningGirl.setY(root.getHeight() + 140); blurGamePane(); winMessage.setOpacity(0); confetti.setOpacity(0); winPane.setOpacity(1); winPane.setVisible(true); winningGirl.setVisible(true); Timeline timeline = new Timeline(); double S4 = 1.45; double x0 = 0.33; KeyValue confettiKeyValue1x = new KeyValue(confetti.xProperty(), confettiX); KeyValue confettiKeyValue1y = new KeyValue(confetti.yProperty(), confettiY - confettiOffset); KeyValue confettiKeyValue1opacity = new KeyValue(confetti.opacityProperty(), 0); KeyFrame confettiKeyFrame1 = new KeyFrame(Duration.seconds(0), confettiKeyValue1x, confettiKeyValue1y, confettiKeyValue1opacity); KeyValue confettiKeyValue2x = new KeyValue(confetti.xProperty(), confettiX); KeyValue confettiKeyValue2y = new KeyValue(confetti.yProperty(), confettiY - confettiOffset); KeyValue confettiKeyValue2opacity = new KeyValue(confetti.opacityProperty(), 0); KeyFrame confettiKeyFrame2 = new KeyFrame(Duration.millis(500), confettiKeyValue2x, confettiKeyValue2y, confettiKeyValue2opacity); KeyValue confettiKeyValue3x = new KeyValue(confetti.xProperty(), confettiX, new CustomEaseOutInterpolator(S4, x0)); KeyValue confettiKeyValue3y = new KeyValue(confetti.yProperty(), confettiY, new CustomEaseOutInterpolator(S4, x0)); KeyValue confettiKeyValue3opacity = new KeyValue(confetti.opacityProperty(), 1); KeyFrame confettiKeyFrame3 = new KeyFrame(Duration.millis(1100), confettiKeyValue3x, confettiKeyValue3y, confettiKeyValue3opacity); KeyValue winningGirlKeyValue1x = new KeyValue(winningGirl.xProperty(), endX, new CustomEaseOutInterpolator(S4, x0)); KeyValue winningGirlKeyValue1y = new KeyValue(winningGirl.yProperty(), endY, new CustomEaseOutInterpolator(S4, x0)); KeyFrame winningGirlKeyFrame1 = new KeyFrame(Duration.seconds(1), winningGirlKeyValue1x, winningGirlKeyValue1y); timeline.getKeyFrames().addAll(winningGirlKeyFrame1, confettiKeyFrame1, confettiKeyFrame2, confettiKeyFrame3); timeline.setOnFinished((event) -> fadeNode(winMessage, 1, () -> { AnchorPane.setRightAnchor(winningGirl, 0.0); AnchorPane.setBottomAnchor(winningGirl, 0.0); })); timeline.play(); })); } private void showWinnerWithTwoHumanPlayers(Board.WinnerInfo winnerInfo) { guiAnimationQueue.submitWaitForUnlock(() -> addWinLineOnWin(winnerInfo, Color.YELLOW, () -> { twoHumansWinnerText.setText(winnerInfo.winningPlayer.getName() + " won :)"); double endX = twoHumansWinnerImage.getX(); double endY = twoHumansWinnerImage.getY(); AnchorPane.clearConstraints(twoHumansWinnerImage); twoHumansWinnerImage.setX(endX); twoHumansWinnerImage.setY(twoHumansWinnerImage.getY() + 300); // blurGamePane(); blurGamePane(10.0); twoHumansWinMessage.setOpacity(0); twoHumansWinnerImage.setOpacity(0); twoHumansWinnerPane.setOpacity(1); twoHumansWinnerPane.setVisible(true); twoHumansWinnerImage.setVisible(true); Timeline timeline = new Timeline(); double S4 = 1.45; double x0 = 0.33; KeyValue twoHumansWinnerImageKeyValue1x = new KeyValue(twoHumansWinnerImage.xProperty(), endX, new CustomEaseOutInterpolator(S4, x0)); KeyValue twoHumansWinnerImageKeyValue1y = new KeyValue(twoHumansWinnerImage.yProperty(), endY, new CustomEaseOutInterpolator(S4, x0)); KeyValue twoHumansWinnerImageKeyValue1Opacity = new KeyValue(twoHumansWinnerImage.opacityProperty(), 1); KeyFrame twoHumansWinnerImageKeyFrame1 = new KeyFrame(Duration.millis(600), twoHumansWinnerImageKeyValue1x, twoHumansWinnerImageKeyValue1y, twoHumansWinnerImageKeyValue1Opacity); timeline.getKeyFrames().addAll(twoHumansWinnerImageKeyFrame1); timeline.setOnFinished((event) -> fadeNode(twoHumansWinMessage, 1, () -> { AnchorPane.setLeftAnchor(twoHumansWinnerImage, 0.0); AnchorPane.setBottomAnchor(twoHumansWinnerImage, 0.0); })); timeline.play(); })); } private void addWinLineOnWin(Board.WinnerInfo winnerInfo, Paint color, Runnable onFinished) { Line originLine = new Line(0, 0, 0, 0); winLineGroup.getChildren().add(originLine); WinLine winLine = new WinLine(winnerInfo); double winLineEndX = winLine.endArc.getCenterX(); double winLineEndY = winLine.endArc.getCenterY(); winLine.startArc.setFill(color); winLine.startArc.setStrokeWidth(0); winLine.endArc.setFill(color); winLine.endArc.setStrokeWidth(0); winLine.rightLine.setStrokeWidth(0); winLine.leftLine.setStrokeWidth(0); winLine.centerLine.setStroke(color); winLine.centerLine.strokeWidthProperty().bind(winLine.startArc.radiusXProperty().multiply(2)); winLineGroup.getChildren().addAll(winLine.getAll()); blurNode(gamePane, 4); winLineGroup.setOpacity(0); GaussianBlur blur = new GaussianBlur(100); winLineGroup.setEffect(blur); winLineGroup.setBlendMode(BlendMode.DARKEN); winLineGroup.setVisible(true); double winLineAnimationX1 = 0.2; double winLineAnimationX2 = 0.5; KeyValue stretchKeyValue1x = new KeyValue(winLine.endArc.centerXProperty(), winLine.startArc.getCenterX()); KeyValue stretchKeyValue1y = new KeyValue(winLine.endArc.centerYProperty(), winLine.startArc.getCenterY()); KeyFrame stretchKeyFrame1 = new KeyFrame(Duration.millis(0), stretchKeyValue1x, stretchKeyValue1y); KeyValue stretchKeyValue2x = new KeyValue(winLine.endArc.centerXProperty(), winLine.startArc.getCenterX(), new CustomEaseBothInterpolator(winLineAnimationX1, winLineAnimationX2)); KeyValue stretchKeyValue2y = new KeyValue(winLine.endArc.centerYProperty(), winLine.startArc.getCenterY(), new CustomEaseBothInterpolator(winLineAnimationX1, winLineAnimationX2)); KeyFrame stretchKeyFrame2 = new KeyFrame(Duration.millis(100), stretchKeyValue2x, stretchKeyValue2y); KeyValue stretchKeyValue3x = new KeyValue(winLine.endArc.centerXProperty(), winLineEndX, new CustomEaseBothInterpolator(winLineAnimationX1, winLineAnimationX2)); KeyValue stretchKeyValue3y = new KeyValue(winLine.endArc.centerYProperty(), winLineEndY, new CustomEaseBothInterpolator(winLineAnimationX1, winLineAnimationX2)); KeyFrame stretchKeyFrame3 = new KeyFrame(Duration.millis(800), stretchKeyValue3x, stretchKeyValue3y); KeyValue opacityKeyValue1 = new KeyValue(winLineGroup.opacityProperty(), 0.8); KeyFrame opacityKeyFrame1 = new KeyFrame(Duration.millis(400), opacityKeyValue1); Timeline timeline = new Timeline(); timeline.getKeyFrames().addAll(stretchKeyFrame1, stretchKeyFrame2, stretchKeyFrame3, opacityKeyFrame1); timeline.play(); timeline.setOnFinished((event) -> onFinished.run()); } private void showTie() { guiAnimationQueue.submitWaitForUnlock(() -> { double endX = tiePane.getWidth() - 230; double endY = 90; AnchorPane.clearConstraints(bowTie); bowTie.setX(endX); bowTie.setY(-150); blurGamePane(); tieMessage.setOpacity(0); tiePane.setOpacity(1); tiePane.setVisible(true); bowTie.setVisible(true); Timeline timeline = new Timeline(); double S4 = 1.45; double x0 = 0.33; KeyValue keyValue1x = new KeyValue(bowTie.xProperty(), endX, new CustomEaseOutInterpolator(S4, x0)); KeyValue keyValue1y = new KeyValue(bowTie.yProperty(), endY, new CustomEaseOutInterpolator(S4, x0)); KeyFrame keyFrame1 = new KeyFrame(Duration.seconds(1), keyValue1x, keyValue1y); timeline.getKeyFrames().add(keyFrame1); timeline.setOnFinished((event) -> fadeNode(tieMessage, 1, () -> { AnchorPane.setRightAnchor(bowTie, tiePane.getWidth() - bowTie.getFitWidth() - endX); AnchorPane.setTopAnchor(bowTie, endY); })); timeline.play(); }); } private void showLooser(Board.WinnerInfo winnerInfo) { String looserName = board.getOpponent(winnerInfo.winningPlayer).getName(); guiAnimationQueue.submitWaitForUnlock(() -> { ShakeTransition anim = new ShakeTransition(gamePane, null); anim.playFromStart(); Timeline timeline = new Timeline(); Circle c1 = new Circle((452 / 600.0) * looserPane.getWidth(), (323 / 640.0) * looserPane.getHeight(), 0); GaussianBlur circleBlur = new GaussianBlur(30); c1.setEffect(circleBlur); looseImage.setClip(c1); addWinLineOnLoose(winnerInfo); KeyValue keyValue1 = new KeyValue(c1.radiusProperty(), 0); KeyFrame keyFrame1 = new KeyFrame(Duration.millis(800), keyValue1); KeyValue keyValue2 = new KeyValue(c1.radiusProperty(), (500 / 640.0) * looserPane.getHeight()); KeyFrame keyFrame2 = new KeyFrame(Duration.millis(900), keyValue2); timeline.getKeyFrames().addAll(keyFrame1, keyFrame2); looseMessage.setOpacity(0); looserText.setText(looserName + " lost :("); looserPane.setVisible(true); looserPane.setOpacity(1); timeline.setOnFinished((event) -> { looseImage.setClip(null); winLineGroup.setClip(null); blurGamePane(); PauseTransition wait = new PauseTransition(); wait.setDuration(Duration.seconds(1)); wait.setOnFinished((event2) -> { FadeTransition looseMessageTransition = new FadeTransition(); looseMessageTransition.setNode(looseMessage); looseMessageTransition.setFromValue(0); looseMessageTransition.setToValue(1); looseMessageTransition.setDuration(Duration.millis(500)); looseMessageTransition.setAutoReverse(false); looseMessageTransition.play(); }); wait.play(); }); timeline.play(); }); } private void addWinLineOnLoose(Board.WinnerInfo winnerInfo) { final double strokeWidth = 2; Line originLine = new Line(0, 0, 0, 0); winLineGroup.getChildren().add(originLine); WinLine winLine = new WinLine(winnerInfo); winLine.startArc.setFill(Color.TRANSPARENT); winLine.startArc.setStroke(Color.BLACK); winLine.startArc.setStrokeWidth(strokeWidth); winLine.endArc.setFill(Color.TRANSPARENT); winLine.endArc.setStroke(Color.BLACK); winLine.endArc.setStrokeWidth(strokeWidth); winLine.rightLine.setStrokeWidth(strokeWidth); winLine.leftLine.setStrokeWidth(strokeWidth); winLine.centerLine.setStrokeWidth(0); winLineGroup.getChildren().addAll(winLine.getAll()); winLineGroup.setOpacity(0); GaussianBlur blur = new GaussianBlur(7); winLineGroup.setEffect(blur); winLineGroup.setVisible(true); KeyValue keyValue1 = new KeyValue(winLineGroup.opacityProperty(), 0); KeyFrame keyFrame1 = new KeyFrame(Duration.millis(900), keyValue1); KeyValue keyValue2 = new KeyValue(winLineGroup.opacityProperty(), 1); KeyFrame keyFrame2 = new KeyFrame(Duration.millis(950), keyValue2); Timeline timeline = new Timeline(); timeline.getKeyFrames().addAll(keyFrame1, keyFrame2); timeline.play(); } private void fadeWinLineGroup() { fadeNode(winLineGroup, 0, () -> { winLineGroup.setBlendMode(BlendMode.SRC_OVER); //noinspection SuspiciousMethodCalls refreshedNodes.removeAll(winLineGroup.getChildren()); winLineGroup.getChildren().clear(); }); } private void blurGamePane() { blurGamePane(7.0); } private void unblurGamePane() { blurGamePane(0.0); } private void blurGamePane(double toValue) { blurNode(gamePane, toValue); } private void blurLooserPane() { blurNode(looserPane, 7); } private void blurTiePane() { blurNode(tiePane, 7); } private void blurWinPane() { blurNode(winPane, 7); } private void blurTwoHumansWinnerPane() { blurNode(twoHumansWinnerPane, 7); } private void blurNode(Node node, double toValue) { blurNode(node, toValue, null); } private void blurNode(Node node, double toValue, @SuppressWarnings("SameParameterValue") Runnable onFinish) { guiAnimationQueue.submit(() -> { GaussianBlur blur = (GaussianBlur) node.getEffect(); if (blur == null) { blur = new GaussianBlur(0); node.setEffect(blur); } node.setEffect(blur); Timeline timeline = new Timeline(); KeyValue keyValue = new KeyValue(blur.radiusProperty(), toValue); KeyFrame keyFrame = new KeyFrame(Duration.seconds(animationSpeed), keyValue); timeline.getKeyFrames().add(keyFrame); timeline.setOnFinished((event) -> { if (toValue == 0) { node.setEffect(null); } if (onFinish != null) { onFinish.run(); } }); timeline.play(); }); } /** * Shows or hides the ai level slider according to the current ai configuration */ private void showHideAILevelSlider(boolean player1IsAI, boolean player2IsAI) { if ((player1IsAI || player2IsAI) && !aiLevelSliderVisible) { aiLevelSliderVisible = true; fadeAILevelSliderIn(); } else if (!player1IsAI && !player2IsAI && aiLevelSliderVisible) { aiLevelSliderVisible = false; fadeAILevelSliderOut(); } } private void fadeAILevelSliderOut() { guiAnimationQueue.submit(() -> { menuSubBox.setPrefHeight(menuSubBox.getHeight()); updateMenuHeight(false); }); } private void fadeAILevelSliderIn() { guiAnimationQueue.submit(() -> { menuSubBox.setPrefHeight(menuSubBox.getHeight()); updateMenuHeight(true); }); } private void updateMenuHeight(boolean includeAILevelSlider) { double toHeight = 0; int effectiveChildCount = 0; for (Node child : menuSubBox.getChildren()) { if (child.isVisible() && child != aiLevelTitleLabel && child != aiLevelSlider && child != aiLevelLabelPane) { toHeight = toHeight + child.getBoundsInParent().getHeight(); effectiveChildCount = effectiveChildCount + 1; } } if (includeAILevelSlider) { toHeight = toHeight + aiLevelLabelPane.getPrefHeight(); toHeight = toHeight + aiLevelSlider.getPrefHeight(); toHeight = toHeight + aiLevelTitleLabel.getPrefHeight(); effectiveChildCount = effectiveChildCount + 3; } toHeight = toHeight + menuSubBox.getSpacing() * (effectiveChildCount - 1); if (updateMenuHeightTimeline != null && updateMenuHeightTimeline.getStatus() == Animation.Status.RUNNING) { updateMenuHeightTimeline.stop(); } updateMenuHeightTimeline = new Timeline(); KeyValue keyValue0 = new KeyValue(menuSubBox.prefHeightProperty(), toHeight, Interpolator.EASE_BOTH); KeyFrame keyFrame0 = new KeyFrame(Duration.seconds(animationSpeed), keyValue0); updateMenuHeightTimeline.getKeyFrames().add(keyFrame0); updateMenuHeightTimeline.play(); } private void fadeNode(Node node, double toValue) { fadeNode(node, toValue, false); } private void fadeNode(Node node, double toValue, @SuppressWarnings("SameParameterValue") boolean block) { fadeNode(node, toValue, block, null); } private void fadeNode(Node node, @SuppressWarnings("SameParameterValue") double toValue, Runnable onFinish) { fadeNode(node, toValue, false, onFinish); } private void fadeNode(Node node, double toValue, boolean block, Runnable onFinish) { guiAnimationQueue.submit(() -> { if (block) { guiAnimationQueue.setBlocked(true); } if (!node.isVisible()) { node.setOpacity(0); node.setVisible(true); } FadeTransition fadeTransition = new FadeTransition(); fadeTransition.setNode(node); fadeTransition.setFromValue(node.getOpacity()); fadeTransition.setToValue(toValue); fadeTransition.setDuration(Duration.seconds(animationSpeed)); fadeTransition.setAutoReverse(false); fadeTransition.setOnFinished((event) -> { if (toValue == 0) { node.setEffect(null); node.setVisible(false); } if (block) { guiAnimationQueue.setBlocked(false); } if (onFinish != null) { onFinish.run(); } }); fadeTransition.play(); }); } public boolean isBlockedForInput() { return blockedForInput; } public void setBlockedForInput(boolean blockedForInput) { this.blockedForInput = blockedForInput; updateOpponentsTurnHBox(); } public void updateOpponentsTurnHBox() { updateOpponentsTurnHBox(false); } public void updateOpponentsTurnHBox(@SuppressWarnings("SameParameterValue") boolean noAnimation) { updateOpponentsTurnHBox(noAnimation, isBlockedForInput()); } public void updateOpponentsTurnHBox(boolean noAnimation, boolean isShown) { double destinationTranslate; double animationOffsetInSeconds = 0; if (!isShown) { destinationTranslate = opponentsTurnHBox.getHeight() + 3; double timeSinceLastShownInMillis = Calendar.getInstance().getTime().getTime() - opponentsTurnLabelLastShown.getTime().getTime(); double timeSinceLastShownInSeconds = timeSinceLastShownInMillis / 1000; animationOffsetInSeconds = Math .max(opponentsTurnLabelShownForAtLeastSeconds - timeSinceLastShownInSeconds, 0); } else { destinationTranslate = 0; String opponentsName; if (board.getCurrentPlayer() == null || board.getOpponent(board.getCurrentPlayer()) == null) { opponentsName = "Opponent"; } else { Player player; if (board.getCurrentPlayer().getPlayerMode() == PlayerMode.localHuman) { player = board.getOpponent(board.getCurrentPlayer()); } else { player = board.getCurrentPlayer(); } opponentsName = player.getName(); } opponentsTurnLabel.setText(opponentsName + "'s turn..."); } if (noAnimation) { opponentsTurnHBox.setTranslateY(destinationTranslate); } else { KeyValue keyValue1 = new KeyValue(opponentsTurnHBox.translateYProperty(), opponentsTurnHBox.getTranslateY()); KeyFrame keyFrame1 = new KeyFrame(Duration.seconds(animationOffsetInSeconds), keyValue1); KeyValue keyValue2 = new KeyValue(opponentsTurnHBox.translateYProperty(), destinationTranslate); KeyFrame keyFrame2 = new KeyFrame(Duration.seconds(animationSpeed + animationOffsetInSeconds), keyValue2); Timeline timeline = new Timeline(keyFrame1, keyFrame2); timeline.setOnFinished((event) -> { if (isShown) { opponentsTurnLabelLastShown = Calendar.getInstance(); } }); timeline.play(); } } public void flashOpponentsTurnHBox() { KeyValue keyValue1Color = new KeyValue(((DropShadow) opponentsTurnAnchorPane.getEffect()).colorProperty(), Color.RED); KeyValue keyValue1Width = new KeyValue(((DropShadow) opponentsTurnAnchorPane.getEffect()).widthProperty(), 30); KeyValue keyValue1Height = new KeyValue(((DropShadow) opponentsTurnAnchorPane.getEffect()).heightProperty(), 30); KeyFrame keyFrame1 = new KeyFrame(Duration.seconds(animationSpeed / 2), keyValue1Color, keyValue1Width, keyValue1Height); KeyValue keyValue2Color = new KeyValue(((DropShadow) opponentsTurnAnchorPane.getEffect()).colorProperty(), Color.BLACK); KeyValue keyValue2Width = new KeyValue(((DropShadow) opponentsTurnAnchorPane.getEffect()).widthProperty(), 12); KeyValue keyValue2Height = new KeyValue(((DropShadow) opponentsTurnAnchorPane.getEffect()).heightProperty(), 12); KeyFrame keyFrame2 = new KeyFrame(Duration.seconds(animationSpeed), keyValue2Color, keyValue2Width, keyValue2Height); Timeline timeline = new Timeline(keyFrame1, keyFrame2); // play it twice timeline.setOnFinished((event -> new Timeline(keyFrame1, keyFrame2).play())); timeline.play(); } private static class KunamiCode { private static final KeyCode[] kunamiCodes = { KeyCode.U, KeyCode.U, KeyCode.D, KeyCode.D, KeyCode.L, KeyCode.R, KeyCode.L, KeyCode.R, KeyCode.B, KeyCode.A }; private static int currentIndex = 0; private static boolean isCompleted(KeyCode nextKeyCode) { if (nextKeyCode.equals(kunamiCodes[currentIndex])) { if (currentIndex == kunamiCodes.length - 1) { // we're at the end currentIndex = 0; return true; } else { // The key was correct but we're not yet at the end currentIndex++; return false; } } else { // the key was false currentIndex = 0; return false; } } } private class WinLineGeometry { final double winLineWidth; final double startX; final double startY; final double endX; final double endY; double startAngle; WinLineGeometry(Board.WinnerInfo winnerInfo, double newHeight, double newWidth) { double cellWidth = newWidth / board.getColumnCount(); double cellHeight = newHeight / board.getRowCount(); winLineWidth = Math.min(cellHeight, cellWidth) / 2; startX = (winnerInfo.winLineStartColumn * cellWidth) + (cellWidth / 2.0); startY = (winnerInfo.winLineStartRow * cellHeight) + (cellHeight / 2.0); endX = (winnerInfo.winLineEndColumn * cellWidth) + (cellWidth / 2.0); endY = (winnerInfo.winLineEndRow * cellHeight) + (cellHeight / 2.0); startAngle = Math.atan2(endX - startX, endY - startY) * 180 / Math.PI; } } private class WinLine implements Refreshable { final Arc startArc; final Line leftLine; final Line centerLine; final Line rightLine; final Arc endArc; final Rectangle clipRectangle = new Rectangle(); private final Board.WinnerInfo winnerInfo; private double lastWinLineWidth = 0; WinLine(Board.WinnerInfo winnerInfo) { this.winnerInfo = winnerInfo; startArc = new Arc(); startArc.setType(ArcType.OPEN); //noinspection SuspiciousNameCombination endArc = new Arc(); endArc.setType(ArcType.OPEN); leftLine = new Line(); centerLine = new Line(); rightLine = new Line(); centerLine.setClip(clipRectangle); startArc.centerXProperty().addListener((observable, oldValue, newValue) -> { double startAngle = new WinLineGeometry(winnerInfo, gameTable.getHeight(), gameTable.getWidth()).startAngle; internalRefresh(newValue.doubleValue(), startArc.getCenterY(), endArc.getCenterX(), endArc.getCenterY(), lastWinLineWidth, startAngle); }); startArc.centerYProperty().addListener((observable, oldValue, newValue) -> { double startAngle = new WinLineGeometry(winnerInfo, gameTable.getHeight(), gameTable.getWidth()).startAngle; internalRefresh(startArc.getCenterX(), newValue.doubleValue(), endArc.getCenterX(), endArc.getCenterY(), lastWinLineWidth, startAngle); }); endArc.centerXProperty().addListener((observable, oldValue, newValue) -> { double startAngle = new WinLineGeometry(winnerInfo, gameTable.getHeight(), gameTable.getWidth()).startAngle; internalRefresh(startArc.getCenterX(), startArc.getCenterY(), newValue.doubleValue(), endArc.getCenterY(), lastWinLineWidth, startAngle); }); endArc.centerYProperty().addListener((observable, oldValue, newValue) -> { double startAngle = new WinLineGeometry(winnerInfo, gameTable.getHeight(), gameTable.getWidth()).startAngle; internalRefresh(startArc.getCenterX(), startArc.getCenterY(), endArc.getCenterX(), newValue.doubleValue(), lastWinLineWidth, startAngle); }); refreshedNodes.add(this); refreshedNodes.refreshAll(gameTable.getWidth(), gameTable.getHeight(), gameTable.getWidth(), gameTable.getHeight()); } private void internalRefresh(double startX, double startY, double endX, double endY, double winLineWidth, double startAngle) { leftLine.setStartX(startX - Math.cos(startAngle * Math.PI / 180) * winLineWidth); leftLine.setStartY(startY + Math.sin(startAngle * Math.PI / 180) * winLineWidth); leftLine.setEndX(endX - Math.cos(startAngle * Math.PI / 180) * winLineWidth); leftLine.setEndY(endY + Math.sin(startAngle * Math.PI / 180) * winLineWidth); double tempStartAngle = -startAngle; clipRectangle.setWidth(winLineWidth * 2); clipRectangle.setHeight(Math.sqrt(Math.pow(startX - endX, 2) + Math.pow(startY - endY, 2))); clipRectangle.setRotate(tempStartAngle); // calculate the coordinates of the center of the rectangle double centerX = (startX + endX) / 2; double centerY = (startY + endY) / 2; // now convert that into the coordinates of the upper left corner of the rectangle if the rotation was 0 (that's how javaFX needs them... :/ ) clipRectangle.setX(centerX - clipRectangle.getWidth() / 2); clipRectangle.setY(centerY - clipRectangle.getHeight() / 2); centerLine.setStartX(startX); centerLine.setStartY(startY); centerLine.setEndX(endX); centerLine.setEndY(endY); rightLine.setStartX(startX + Math.cos(startAngle * Math.PI / 180) * winLineWidth); rightLine.setStartY(startY - Math.sin(startAngle * Math.PI / 180) * winLineWidth); rightLine.setEndX(endX + Math.cos(startAngle * Math.PI / 180) * winLineWidth); rightLine.setEndY(endY - Math.sin(startAngle * Math.PI / 180) * winLineWidth); } List<Shape> getAll() { List<Shape> res = new ArrayList<>(5); res.add(startArc); res.add(leftLine); res.add(centerLine); res.add(rightLine); res.add(endArc); return res; } /** * Called when the window is resized * * @param oldWindowWidth The width of the window prior to resizing * @param oldWindowHeight The height of the window prior to resizing * @param newWindowWidth The width of the window after to resizing * @param newWindowHeight The height of the window after to resizing */ @Override public void refresh(double oldWindowWidth, double oldWindowHeight, double newWindowWidth, double newWindowHeight) { WinLineGeometry geometry = new WinLineGeometry(winnerInfo, newWindowHeight, newWindowWidth); lastWinLineWidth = geometry.winLineWidth; startArc.setCenterX(geometry.startX); startArc.setCenterY(geometry.startY); startArc.setRadiusX(geometry.winLineWidth); startArc.setRadiusY(geometry.winLineWidth); startArc.setStartAngle(geometry.startAngle); startArc.setLength(180); double tempStartAngle = geometry.startAngle = geometry.startAngle + 180; if (tempStartAngle > 360) { tempStartAngle = tempStartAngle - 360; } endArc.setCenterX(geometry.endX); endArc.setCenterY(geometry.endY); endArc.setRadiusX(geometry.winLineWidth); endArc.setRadiusY(geometry.winLineWidth); endArc.setStartAngle(tempStartAngle); endArc.setLength(180); } } }