From c705e31a6dc567beca784c9b9c034390afe6da19 Mon Sep 17 00:00:00 2001 From: Janis Date: Sun, 23 Nov 2025 16:10:55 +0100 Subject: [PATCH] feat(websocket)!: Implement WebSocket connection and event handling --- .../app/assets/stylesheets/login.less | 2 +- .../app/assets/stylesheets/main.less | 257 +- knockoutwhistweb/app/auth/AuthAction.scala | 14 +- .../WebApplicationConfiguration.scala | 1 + .../app/controllers/IngameController.scala | 85 +- .../JavaScriptRoutingController.scala | 27 +- .../app/controllers/MainMenuController.scala | 6 +- .../app/controllers/PollingController.scala | 152 - .../app/controllers/WebsocketController.scala | 13 +- knockoutwhistweb/app/logic/PodManager.scala | 31 +- .../app/logic/game/GameLobby.scala | 228 +- .../app/logic/game/PollingEvents.scala | 10 - .../app/logic/user/SessionManager.scala | 4 +- .../app/logic/user/UserManager.scala | 4 + .../app/logic/user/impl/StubUserManager.scala | 4 +- .../app/model/sessions/InteractionType.scala | 2 +- .../app/model/sessions/PlayerSession.scala | 6 +- .../app/model/sessions/SimpleSession.scala | 4 +- .../app/model/sessions/UserSession.scala | 12 +- .../model/sessions/UserWebsocketActor.scala | 18 +- knockoutwhistweb/app/model/users/User.scala | 8 +- .../app/services/JwtKeyProvider.scala | 53 +- knockoutwhistweb/app/util/GameUtil.scala | 2 +- .../app/views/ingame/finishedMatch.scala.html | 2 +- .../app/views/ingame/ingame.scala.html | 47 +- .../app/views/ingame/selecttrump.scala.html | 27 +- .../app/views/ingame/tie.scala.html | 85 +- .../app/views/lobby/lobby.scala.html | 8 +- knockoutwhistweb/app/views/main.scala.html | 16 +- .../app/views/mainmenu/creategame.scala.html | 24 +- .../app/views/mainmenu/navbar.scala.html | 106 +- .../app/views/mainmenu/rules.scala.html | 332 +- knockoutwhistweb/conf/logback.xml | 72 +- knockoutwhistweb/conf/routes | 40 +- knockoutwhistweb/public/javascripts/main.js | 456 +-- .../public/javascripts/particles.js | 2723 ++++++++--------- .../javascripts/{ingame.js => websocket.js} | 23 +- .../test/controllers/HomeControllerSpec.scala | 58 +- 38 files changed, 2176 insertions(+), 2786 deletions(-) delete mode 100644 knockoutwhistweb/app/controllers/PollingController.scala delete mode 100644 knockoutwhistweb/app/logic/game/PollingEvents.scala rename knockoutwhistweb/public/javascripts/{ingame.js => websocket.js} (89%) diff --git a/knockoutwhistweb/app/assets/stylesheets/login.less b/knockoutwhistweb/app/assets/stylesheets/login.less index 2e3bb1e..67e85b6 100644 --- a/knockoutwhistweb/app/assets/stylesheets/login.less +++ b/knockoutwhistweb/app/assets/stylesheets/login.less @@ -17,7 +17,7 @@ width: 100%; border: none; border-radius: 1rem; - box-shadow: 0 4px 20px rgba(0,0,0,0.1); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); position: relative; z-index: 3; /* ensure card sits above the particles */ } diff --git a/knockoutwhistweb/app/assets/stylesheets/main.less b/knockoutwhistweb/app/assets/stylesheets/main.less index b9713a8..b321c74 100644 --- a/knockoutwhistweb/app/assets/stylesheets/main.less +++ b/knockoutwhistweb/app/assets/stylesheets/main.less @@ -14,37 +14,44 @@ --bs-border-color: rgba(0, 0, 0, 0.125) !important; --bs-heading-color: var(--color) !important; } + @background-color: var(--background-color); @highlightcolor: var(--highlightscolor); @background-image: var(--background-image); @color: var(--color); @keyframes slideIn { - 0% { transform: translateX(-100vw); } - 100% { transform: translateX(0); } + 0% { + transform: translateX(-100vw); + } + 100% { + transform: translateX(0); + } } .game-field-background { - background-image: @background-image; - background-repeat: no-repeat; - background-size: cover; - max-width: 1400px; - margin: 0 auto; - min-height: 100vh; -} -.lobby-background { - background-color: @background-color; - width: 100%; - height: 100vh; + background-image: @background-image; + background-repeat: no-repeat; + background-size: cover; + max-width: 1400px; + margin: 0 auto; + min-height: 100vh; } -.navbar-header{ - text-align:center; +.lobby-background { + background-color: @background-color; + width: 100%; + height: 100vh; +} + +.navbar-header { + text-align: center; } .navbar-toggle { - float: none; - margin-right:0; + float: none; + margin-right: 0; } + .handcard :hover { box-shadow: 3px 3px 3px @highlightcolor; } @@ -52,7 +59,7 @@ .inactive::after { content: ""; position: absolute; - inset: 0; /* cover the whole container */ + inset: 0; /* cover the whole container */ background: rgba(0, 0, 0, 0.50); z-index: 10; border-radius: 6px; @@ -73,26 +80,26 @@ /* Ensure body text color follows theme variable and works with Bootstrap */ body { - color: @color; + color: @color; } .footer { - width: 100%; - text-align: center; - font-size: 12px; - color: @color; - padding: 0.5rem 0; - flex-grow: 1; /* fill remaining vertical space as visual footer background */ + width: 100%; + text-align: center; + font-size: 12px; + color: @color; + padding: 0.5rem 0; + flex-grow: 1; /* fill remaining vertical space as visual footer background */ } .game-field { - position: fixed; - inset: 0; - overflow: auto; + position: fixed; + inset: 0; + overflow: auto; } .navbar-drop-shadow { - box-shadow: 0 1px 15px 0 #000000 + box-shadow: 0 1px 15px 0 #000000 } .ingame-side-shadow { @@ -100,126 +107,164 @@ body { } #sessions { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: center; - text-align: center; - h1 { - animation: slideIn 0.5s ease-out forwards; - animation-fill-mode: backwards; - } + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + text-align: center; + + h1 { + animation: slideIn 0.5s ease-out forwards; + animation-fill-mode: backwards; + } } + #textanimation { - animation: slideIn 0.5s ease-out forwards; - animation-fill-mode: backwards; - animation-delay: 1s; + animation: slideIn 0.5s ease-out forwards; + animation-fill-mode: backwards; + animation-delay: 1s; } #sessions a, #sessions h1, #sessions p { - color: @color; - font-size: 40px; - font-family: Arial, serif; + color: @color; + font-size: 40px; + font-family: Arial, serif; } + #ingame { - display: flex; - flex-direction: column; - align-items: center; - justify-content: flex-end; - height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + height: 100%; } + #ingame a, #ingame h1, #ingame p { - color: @color; - font-size: 40px; - font-family: Arial, serif; + color: @color; + font-size: 40px; + font-family: Arial, serif; } .ingame-cards-slide { div { animation: slideIn 0.5s ease-out forwards; animation-fill-mode: backwards; - &:nth-child(1) { animation-delay: 0.5s; } - &:nth-child(2) { animation-delay: 1s; } - &:nth-child(3) { animation-delay: 1.5s; } - &:nth-child(4) { animation-delay: 2s; } - &:nth-child(5) { animation-delay: 2.5s; } - &:nth-child(6) { animation-delay: 3s; } - &:nth-child(7) { animation-delay: 3.5s; } + + &:nth-child(1) { + animation-delay: 0.5s; + } + + &:nth-child(2) { + animation-delay: 1s; + } + + &:nth-child(3) { + animation-delay: 1.5s; + } + + &:nth-child(4) { + animation-delay: 2s; + } + + &:nth-child(5) { + animation-delay: 2.5s; + } + + &:nth-child(6) { + animation-delay: 3s; + } + + &:nth-child(7) { + animation-delay: 3.5s; + } } } #playedcardplayer { - display: flex; - flex-direction: column; - justify-content: flex-end; + display: flex; + flex-direction: column; + justify-content: flex-end; } + #playedcardplayer p { - font-size: 12px; - height: 4%; + font-size: 12px; + height: 4%; } + #playedcardplayer img { - height: 90%; + height: 90%; } #firstCard { - display: flex; - flex-direction: row; - height: 20%; - width: 100%; - justify-content: space-between; + display: flex; + flex-direction: row; + height: 20%; + width: 100%; + justify-content: space-between; } + #firstCardObject { - display: flex; - flex-direction: column; - margin-right: 4%; + display: flex; + flex-direction: column; + margin-right: 4%; } -#firstCardObject img{ - height: 90%; + +#firstCardObject img { + height: 90%; } -#firstCardObject p{ - height: 10%; - font-size: 20px; + +#firstCardObject p { + height: 10%; + font-size: 20px; } + #nextPlayers { - display: flex; - flex-direction: column; - align-items: center; - height: 0; - p { - margin-top: 0; - margin-bottom: 0; - } + display: flex; + flex-direction: column; + align-items: center; + height: 0; + + p { + margin-top: 0; + margin-bottom: 0; + } } + #invisible { - visibility: hidden; + visibility: hidden; } + #selecttrumpsuit { - display: flex; - flex-direction: column; - align-items: center; - height: 100%; + display: flex; + flex-direction: column; + align-items: center; + height: 100%; } + #rules { - color: @color; - font-size: 1.5em; - font-family: Arial, serif; + color: @color; + font-size: 1.5em; + font-family: Arial, serif; } + .score-table { - background-color: rgba(255, 255, 255, 0.1); - border-radius: 8px; - padding: 10px; - margin-bottom: 20px; - backdrop-filter: blur(8px); - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + background-color: rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 10px; + margin-bottom: 20px; + backdrop-filter: blur(8px); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } + .score-header { - font-weight: bold; - color: #000000; - border-bottom: 1px solid rgba(255, 255, 255, 0.3); + font-weight: bold; + color: #000000; + border-bottom: 1px solid rgba(255, 255, 255, 0.3); } + .score-row { - color: #000000; + color: #000000; } /* In-game centered stage and blurred sides overlay */ @@ -243,12 +288,12 @@ body { inset: 0; pointer-events: none; /* fallback: subtle vignette if backdrop-filter unsupported */ - background: radial-gradient(ellipse at center, rgba(0,0,0,0) 30%, rgba(0,0,0,0.35) 100%); + background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0) 30%, rgba(0, 0, 0, 0.35) 100%); } @supports ((-webkit-backdrop-filter: blur(8px)) or (backdrop-filter: blur(8px))) { .blur-sides::before { - background: rgba(0,0,0,0.08); + background: rgba(0, 0, 0, 0.08); -webkit-backdrop-filter: blur(10px) saturate(110%); backdrop-filter: blur(10px) saturate(110%); } diff --git a/knockoutwhistweb/app/auth/AuthAction.scala b/knockoutwhistweb/app/auth/AuthAction.scala index 30add86..c14b1dc 100644 --- a/knockoutwhistweb/app/auth/AuthAction.scala +++ b/knockoutwhistweb/app/auth/AuthAction.scala @@ -15,13 +15,6 @@ class AuthAction @Inject()(val sessionManager: SessionManager, val parser: BodyP override def executionContext: ExecutionContext = ec - protected def getUserFromSession(request: RequestHeader): Option[User] = { - val session = request.cookies.get("sessionId") - if (session.isDefined) - return sessionManager.getUserBySession(session.get.value) - None - } - override def invokeBlock[A]( request: Request[A], block: AuthenticatedRequest[A] => Future[Result] @@ -33,5 +26,12 @@ class AuthAction @Inject()(val sessionManager: SessionManager, val parser: BodyP Future.successful(Results.Redirect(routes.UserController.login())) } } + + protected def getUserFromSession(request: RequestHeader): Option[User] = { + val session = request.cookies.get("sessionId") + if (session.isDefined) + return sessionManager.getUserBySession(session.get.value) + None + } } diff --git a/knockoutwhistweb/app/components/WebApplicationConfiguration.scala b/knockoutwhistweb/app/components/WebApplicationConfiguration.scala index e70c069..65944ee 100644 --- a/knockoutwhistweb/app/components/WebApplicationConfiguration.scala +++ b/knockoutwhistweb/app/components/WebApplicationConfiguration.scala @@ -8,6 +8,7 @@ import de.knockoutwhist.utils.events.EventListener class WebApplicationConfiguration extends DefaultConfiguration { override def uis: Set[UI] = Set() + override def listener: Set[EventListener] = Set(DelayHandler) } diff --git a/knockoutwhistweb/app/controllers/IngameController.scala b/knockoutwhistweb/app/controllers/IngameController.scala index 5638ae1..e8588ba 100644 --- a/knockoutwhistweb/app/controllers/IngameController.scala +++ b/knockoutwhistweb/app/controllers/IngameController.scala @@ -1,7 +1,7 @@ package controllers import auth.{AuthAction, AuthenticatedRequest} -import de.knockoutwhist.control.GameState.{FinishedMatch, InGame, Lobby, SelectTrump, TieBreak} +import de.knockoutwhist.control.GameState.* import exceptions.* import logic.PodManager import logic.game.GameLobby @@ -18,39 +18,11 @@ import scala.concurrent.ExecutionContext import scala.util.Try @Singleton -class IngameController @Inject() ( - val cc: ControllerComponents, - val authAction: AuthAction, - implicit val ec: ExecutionContext - ) extends AbstractController(cc) { - - def returnInnerHTML(gameLobby: GameLobby, user: User): Html = { - gameLobby.logic.getCurrentState match { - case Lobby => views.html.lobby.lobby(Some(user), gameLobby) - case InGame => - views.html.ingame.ingame( - gameLobby.getPlayerByUser(user), - gameLobby - ) - case SelectTrump => - views.html.ingame.selecttrump( - gameLobby.getPlayerByUser(user), - gameLobby - ) - case TieBreak => - views.html.ingame.tie( - gameLobby.getPlayerByUser(user), - gameLobby - ) - case FinishedMatch => - views.html.ingame.finishedMatch( - Some(user), - gameLobby - ) - case _ => - throw new IllegalStateException(s"Invalid game state for in-game view. GameId: ${gameLobby.id}" + s" State: ${gameLobby.logic.getCurrentState}") - } - } +class IngameController @Inject()( + val cc: ControllerComponents, + val authAction: AuthAction, + implicit val ec: ExecutionContext + ) extends AbstractController(cc) { def game(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = PodManager.getGame(gameId) @@ -68,6 +40,35 @@ class IngameController @Inject() ( Redirect(routes.MainMenuController.mainMenu()) } } + + def returnInnerHTML(gameLobby: GameLobby, user: User): Html = { + gameLobby.logic.getCurrentState match { + case Lobby => views.html.lobby.lobby(Some(user), gameLobby) + case InGame => + views.html.ingame.ingame( + gameLobby.getPlayerByUser(user), + gameLobby + ) + case SelectTrump => + views.html.ingame.selecttrump( + gameLobby.getPlayerByUser(user), + gameLobby + ) + case TieBreak => + views.html.ingame.tie( + gameLobby.getPlayerByUser(user), + gameLobby + ) + case FinishedMatch => + views.html.ingame.finishedMatch( + Some(user), + gameLobby + ) + case _ => + throw new IllegalStateException(s"Invalid game state for in-game view. GameId: ${gameLobby.id}" + s" State: ${gameLobby.logic.getCurrentState}") + } + } + def startGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = PodManager.getGame(gameId) val result = Try { @@ -109,13 +110,14 @@ class IngameController @Inject() ( } } } + def kickPlayer(gameId: String, playerToKick: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = PodManager.getGame(gameId) val playerToKickUUID = UUID.fromString(playerToKick) val result = Try { game.get.leaveGame(playerToKickUUID) } - if(result.isSuccess) { + if (result.isSuccess) { Ok(Json.obj( "status" -> "success", "redirectUrl" -> routes.IngameController.game(gameId).url @@ -127,6 +129,7 @@ class IngameController @Inject() ( )) } } + def leaveGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = PodManager.getGame(gameId) val result = Try { @@ -135,7 +138,8 @@ class IngameController @Inject() ( if (result.isSuccess) { Ok(Json.obj( "status" -> "success", - "redirectUrl" -> routes.MainMenuController.mainMenu().url + "redirectUrl" -> routes.MainMenuController.mainMenu().url, + "content" -> views.html.mainmenu.creategame(Some(request.user)).toString )) } else { InternalServerError(Json.obj( @@ -144,7 +148,7 @@ class IngameController @Inject() ( )) } } - + def playCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => { val game = PodManager.getGame(gameId) game match { @@ -216,6 +220,7 @@ class IngameController @Inject() ( } } } + def playDogCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => { val game = PodManager.getGame(gameId) game match { @@ -280,6 +285,7 @@ class IngameController @Inject() ( } } } + def playTrump(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = PodManager.getGame(gameId) game match { @@ -332,6 +338,7 @@ class IngameController @Inject() ( NotFound("Game not found") } } + def playTie(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = PodManager.getGame(gameId) game match { @@ -384,8 +391,8 @@ class IngameController @Inject() ( NotFound("Game not found") } } - - + + def returnToLobby(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = PodManager.getGame(gameId) game match { diff --git a/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala b/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala index 9d2937e..5b03508 100644 --- a/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala +++ b/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala @@ -6,28 +6,19 @@ import play.api.routing.JavaScriptReverseRouter import javax.inject.Inject -class JavaScriptRoutingController @Inject()( - val controllerComponents: ControllerComponents, - val authAction: AuthAction, - ) extends BaseController { +class JavaScriptRoutingController @Inject()( + val controllerComponents: ControllerComponents, + val authAction: AuthAction, + ) extends BaseController { def javascriptRoutes(): Action[AnyContent] = Action { implicit request => - Ok( - JavaScriptReverseRouter("jsRoutes")( + Ok( + JavaScriptReverseRouter("jsRoutes")( routes.javascript.MainMenuController.createGame, routes.javascript.MainMenuController.joinGame, routes.javascript.MainMenuController.navSPA, - routes.javascript.IngameController.startGame, - routes.javascript.IngameController.kickPlayer, - routes.javascript.IngameController.leaveGame, - routes.javascript.IngameController.playCard, - routes.javascript.IngameController.playDogCard, - routes.javascript.IngameController.playTrump, - routes.javascript.IngameController.playTie, - routes.javascript.IngameController.returnToLobby, - routes.javascript.PollingController.polling, routes.javascript.UserController.login_Post - ) - ).as("text/javascript") - } + ) + ).as("text/javascript") + } } diff --git a/knockoutwhistweb/app/controllers/MainMenuController.scala b/knockoutwhistweb/app/controllers/MainMenuController.scala index 912b4a8..4a3eeb7 100644 --- a/knockoutwhistweb/app/controllers/MainMenuController.scala +++ b/knockoutwhistweb/app/controllers/MainMenuController.scala @@ -54,9 +54,9 @@ class MainMenuController @Inject()( "errorMessage" -> "Invalid form submission" )) } - + } - + def joinGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val jsonBody = request.body.asJson val gameId: Option[String] = jsonBody.flatMap { jsValue => @@ -90,7 +90,7 @@ class MainMenuController @Inject()( Ok(views.html.main("Knockout Whist - Rules")(views.html.mainmenu.rules(Some(request.user)))) } - def navSPA(location: String) : Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => + def navSPA(location: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => location match { case "0" => // Main Menu Ok(Json.obj( diff --git a/knockoutwhistweb/app/controllers/PollingController.scala b/knockoutwhistweb/app/controllers/PollingController.scala deleted file mode 100644 index 7308766..0000000 --- a/knockoutwhistweb/app/controllers/PollingController.scala +++ /dev/null @@ -1,152 +0,0 @@ -package controllers - -import auth.{AuthAction, AuthenticatedRequest} -import controllers.PollingController.{scheduler, timeoutDuration} -import de.knockoutwhist.cards.Hand -import de.knockoutwhist.player.AbstractPlayer -import logic.PodManager -import logic.game.{GameLobby, PollingEvents} -import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, NewTrick, ReloadEvent} -import model.sessions.UserSession -import model.users.User -import play.api.libs.json.{JsArray, JsValue, Json} -import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents, Result} -import util.WebUIUtils - -import javax.inject.{Inject, Singleton} -import scala.concurrent.{ExecutionContext, Future} -import java.util.concurrent.{Executors, ScheduledExecutorService, TimeUnit} -import scala.concurrent.duration.* -object PollingController { - private val scheduler: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor() - private val timeoutDuration = 25.seconds -} -@Singleton -class PollingController @Inject() ( - val cc: ControllerComponents, - val authAction: AuthAction, - val ingameController: IngameController, - implicit val ec: ExecutionContext - ) extends AbstractController(cc) { - - private def buildCardPlayResponse(game: GameLobby, hand: Option[Hand], player: AbstractPlayer, newRound: Boolean): JsValue = { - val currentRound = game.logic.getCurrentRound.get - val currentTrick = game.logic.getCurrentTrick.get - - val trickCardsJson = Json.toJson( - currentTrick.cards.map { case (card, player) => - Json.obj("cardId" -> WebUIUtils.cardtoString(card), "player" -> player.name) - } - ) - val scoreTableJson = Json.toJson( - game.getLogic.getPlayerQueue.get.toList.map { player => - Json.obj( - "name" -> player.name, - "tricks" -> currentRound.tricklist.count(_.winner.contains(player)) - ) - } - ) - - val stringHand = hand.map { h => - val cardStrings = h.cards.map(WebUIUtils.cardtoString) - Json.toJson(cardStrings).as[JsArray] - }.getOrElse(Json.arr()) - - val firstCardId = currentTrick.firstCard.map(WebUIUtils.cardtoString).getOrElse("BLANK") - val nextPlayer = game.getLogic.getPlayerQueue.get.duplicate().nextPlayer().name - Json.obj( - "status" -> "cardPlayed", - "animation" -> newRound, - "handData" -> stringHand, - "dog" -> player.isInDogLife, - "currentPlayerName" -> game.logic.getCurrentPlayer.get.name, - "trumpSuit" -> currentRound.trumpSuit.toString, - "trickCards" -> trickCardsJson, - "scoreTable" -> scoreTableJson, - "firstCardId" -> firstCardId, - "nextPlayer" -> nextPlayer, - "yourTurn" -> (game.logic.getCurrentPlayer.get == player) - ) - } - - private def buildLobbyUsersResponse(game: GameLobby, userSession: UserSession): JsValue = { - Json.obj( - "status" -> "lobbyUpdate", - "host" -> userSession.host, - "users" -> game.getUsers.map(u => Json.obj( - "name" -> u.name, - "id" -> u.id, - "self" -> (u.id == userSession.id) - )), - "maxPlayers" -> game.maxPlayers - ) - } - - - def handleEvent(event: PollingEvents, game: GameLobby, userSession: UserSession): Result = { - event match { - case NewRound => - val player = game.getPlayerByUser(userSession.user) - val hand = player.currentHand() - val jsonResponse = buildCardPlayResponse(game, hand, player, true) - Ok(jsonResponse) - case NewTrick => - val player = game.getPlayerByUser(userSession.user) - val hand = player.currentHand() - val jsonResponse = buildCardPlayResponse(game, hand, player, false) - Ok(jsonResponse) - case CardPlayed => - val player = game.getPlayerByUser(userSession.user) - val hand = player.currentHand() - val jsonResponse = buildCardPlayResponse(game, hand, player, false) - Ok(jsonResponse) - case LobbyUpdate => - Ok(buildLobbyUsersResponse(game, userSession)) - case ReloadEvent => - val jsonResponse = Json.obj( - "status" -> "reloadEvent", - "redirectUrl" -> routes.IngameController.game(game.id).url, - "content" -> ingameController.returnInnerHTML(game, userSession.user).toString - ) - Ok(jsonResponse) - } - } - - def polling(gameId: String): Action[AnyContent] = authAction.async { implicit request: AuthenticatedRequest[AnyContent] => - - val playerId = request.user.id - - PodManager.getGame(gameId) match { - case Some(game) => - val playerEventQueue = game.getEventsOfPlayer(playerId) - if (playerEventQueue.nonEmpty) { - val event = playerEventQueue.dequeue() - Future.successful(handleEvent(event, game, game.getUserSession(playerId))) - } else { - val eventPromise = game.registerWaiter(playerId) - val scheduledFuture = scheduler.schedule( - new Runnable { - override def run(): Unit = - eventPromise.tryFailure(new java.util.concurrent.TimeoutException("Polling Timeout")) - }, - timeoutDuration.toMillis, - TimeUnit.MILLISECONDS - ) - eventPromise.future.map { event => - scheduledFuture.cancel(false) - game.removeWaiter(playerId) - handleEvent(event, game, game.getUserSession(playerId)) - }.recover { - case _: Throwable => - game.removeWaiter(playerId) - NoContent - } - } - - case None => - Future.successful(NotFound("Game not found.")) - } - } - - -} diff --git a/knockoutwhistweb/app/controllers/WebsocketController.scala b/knockoutwhistweb/app/controllers/WebsocketController.scala index 2cf702a..0a9825e 100644 --- a/knockoutwhistweb/app/controllers/WebsocketController.scala +++ b/knockoutwhistweb/app/controllers/WebsocketController.scala @@ -20,13 +20,6 @@ class WebsocketController @Inject()( val sessionManger: SessionManager, )(implicit system: ActorSystem, mat: Materializer) extends AbstractController(cc) { - object KnockOutWebSocketActorFactory { - def create(out: ActorRef, userSession: UserSession): Props = { - Props(new UserWebsocketActor(out, userSession)) - } - } - - def socket(): WebSocket = WebSocket.accept[String, String] { request => val session = request.cookies.get("sessionId") if (session.isEmpty) throw new Exception("No session cookie found") @@ -42,5 +35,11 @@ class WebsocketController @Inject()( } } + object KnockOutWebSocketActorFactory { + def create(out: ActorRef, userSession: UserSession): Props = { + Props(new UserWebsocketActor(out, userSession)) + } + } + } \ No newline at end of file diff --git a/knockoutwhistweb/app/logic/PodManager.scala b/knockoutwhistweb/app/logic/PodManager.scala index 123e018..e00979c 100644 --- a/knockoutwhistweb/app/logic/PodManager.scala +++ b/knockoutwhistweb/app/logic/PodManager.scala @@ -16,15 +16,15 @@ object PodManager { val TTL: Long = System.currentTimeMillis() + 86400000L // 24 hours in milliseconds val podIp: String = System.getenv("POD_IP") val podName: String = System.getenv("POD_NAME") - + private val sessions: mutable.Map[String, GameLobby] = mutable.Map() private val userSession: mutable.Map[User, String] = mutable.Map() private val injector: Injector = Guice.createInjector(KnockOutWebConfigurationModule()) - + def createGame( - host: User, - name: String, - maxPlayers: Int + host: User, + name: String, + maxPlayers: Int ): GameLobby = { val gameLobby = GameLobby( logic = BaseGameLogic(injector.getInstance(classOf[Configuration])), @@ -42,13 +42,7 @@ object PodManager { def getGame(gameId: String): Option[GameLobby] = { sessions.get(gameId) } - - private[logic] def removeGame(gameId: String): Unit = { - sessions.remove(gameId) - // Also remove all user sessions associated with this game - userSession.filterInPlace((_, v) => v != gameId) - } - + def registerUserToGame(user: User, gameId: String): Boolean = { if (sessions.contains(gameId)) { userSession += (user -> gameId) @@ -57,18 +51,23 @@ object PodManager { false } } - + def unregisterUserFromGame(user: User): Unit = { userSession.remove(user) } - + def identifyGameOfUser(user: User): Option[GameLobby] = { userSession.get(user) match { case Some(gameId) => sessions.get(gameId) case None => None } } - - + + private[logic] def removeGame(gameId: String): Unit = { + sessions.remove(gameId) + // Also remove all user sessions associated with this game + userSession.filterInPlace((_, v) => v != gameId) + } + } diff --git a/knockoutwhistweb/app/logic/game/GameLobby.scala b/knockoutwhistweb/app/logic/game/GameLobby.scala index afc954e..2f96d1f 100644 --- a/knockoutwhistweb/app/logic/game/GameLobby.scala +++ b/knockoutwhistweb/app/logic/game/GameLobby.scala @@ -2,67 +2,36 @@ package logic.game import de.knockoutwhist.cards.{Hand, Suit} import de.knockoutwhist.control.GameLogic -import de.knockoutwhist.control.GameState.{InGame, Lobby, MainMenu} +import de.knockoutwhist.control.GameState.{Lobby, MainMenu} import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil} -import de.knockoutwhist.events.global.tie.TieTurnEvent -import de.knockoutwhist.events.global.{CardPlayedEvent, GameStateChangeEvent, NewTrickEvent, SessionClosed} -import de.knockoutwhist.events.player.{PlayerEvent, ReceivedHandEvent} +import de.knockoutwhist.events.global.{GameStateChangeEvent, SessionClosed} +import de.knockoutwhist.events.player.PlayerEvent import de.knockoutwhist.player.Playertype.HUMAN import de.knockoutwhist.player.{AbstractPlayer, PlayerFactory} import de.knockoutwhist.rounds.{Match, Round, Trick} import de.knockoutwhist.utils.events.{EventListener, SimpleEvent} import exceptions.* import logic.PodManager -import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, NewTrick, ReloadEvent} import model.sessions.{InteractionType, UserSession} import model.users.User +import play.api.libs.json.{JsObject, Json} import java.util.UUID import scala.collection.mutable import scala.collection.mutable.ListBuffer -import scala.concurrent.Promise as ScalaPromise class GameLobby private( - val logic: GameLogic, - val id: String, - val internalId: UUID, - val name: String, - val maxPlayers: Int - ) extends EventListener { - + val logic: GameLogic, + val id: String, + val internalId: UUID, + val name: String, + val maxPlayers: Int + ) extends EventListener { private val users: mutable.Map[UUID, UserSession] = mutable.Map() - private val eventsPerPlayer: mutable.Map[UUID, mutable.Queue[PollingEvents]] = mutable.Map() - private val waitingPromises: mutable.Map[UUID, ScalaPromise[PollingEvents]] = mutable.Map() - private val lock = new Object - lock.synchronized { - logic.addListener(this) - logic.createSession() - } - - - def registerWaiter(playerId: UUID): ScalaPromise[PollingEvents] = { - val promise = ScalaPromise[PollingEvents]() - lock.synchronized { - val queue = eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue()) - - if (queue.nonEmpty) { - val evt = queue.dequeue() - promise.success(evt) - promise - } else { - waitingPromises.put(playerId, promise) - promise - } - } - } - - def removeWaiter(playerId: UUID): Unit = { - lock.synchronized { - waitingPromises.remove(playerId) - } - } + logic.addListener(this) + logic.createSession() def addUser(user: User): UserSession = { if (users.size >= maxPlayers) throw new GameFullException("The game is full!") @@ -75,57 +44,27 @@ class GameLobby private( ) users += (user.id -> userSession) PodManager.registerUserToGame(user, id) - addToQueue(LobbyUpdate) + //TODO : transmit Lobby Update transmitToAll() userSession } override def listen(event: SimpleEvent): Unit = { event match { - case event: ReceivedHandEvent => - addToQueue(NewRound) - users.get(event.playerId).foreach(session => session.updatePlayer(event)) - case event: CardPlayedEvent => - addToQueue(CardPlayed) - case event: TieTurnEvent => - addToQueue(ReloadEvent) - users.get(event.player.id).foreach(session => session.updatePlayer(event)) case event: PlayerEvent => users.get(event.playerId).foreach(session => session.updatePlayer(event)) - case event: NewTrickEvent => - addToQueue(NewTrick) case event: GameStateChangeEvent => if (event.oldState == MainMenu && event.newState == Lobby) { return } - addToQueue(ReloadEvent) - users.values.foreach(session => session.updatePlayer(event)) - case event: SessionClosed => users.values.foreach(session => session.updatePlayer(event)) case event: SimpleEvent => users.values.foreach(session => session.updatePlayer(event)) } } - private def addToQueue(event: PollingEvents): Unit = { - lock.synchronized { - users.keys.foreach { playerId => - val q = eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue()) - q.enqueue(event) - } - val waiterIds = waitingPromises.keys.toList - waiterIds.foreach { playerId => - val q = eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue()) - if (q.nonEmpty) { - val evt = q.dequeue() - val p = waitingPromises.remove(playerId) - p.foreach(_.success(evt)) - } - } - } - } - /** * Start the game if the user is the host. + * * @param user the user who wants to start the game. */ def startGame(user: User): Unit = { @@ -152,6 +91,7 @@ class GameLobby private( /** * Remove the user from the game lobby. + * * @param user the user who wants to leave the game. */ def leaveGame(userId: UUID): Unit = { @@ -165,15 +105,23 @@ class GameLobby private( PodManager.removeGame(id) return } + sessionOpt.get.websocketActor.foreach(act => act.transmitJsonToClient(Json.obj( + "id" -> "-1", + "event" -> "SessionClosed", + "data" -> Json.obj( + "reason" -> "You left the game (or got kicked)." + ) + ))) users.remove(userId) PodManager.unregisterUserFromGame(sessionOpt.get.user) - addToQueue(LobbyUpdate) + //TODO: transmit Lobby Update transmitToAll() } /** * Play a card from the player's hand. + * * @param userSession the user session of the player. - * @param cardIndex the index of the card in the player's hand. + * @param cardIndex the index of the card in the player's hand. */ def playCard(userSession: UserSession, cardIndex: Int): Unit = { val player = getPlayerInteractable(userSession, InteractionType.Card) @@ -189,10 +137,35 @@ class GameLobby private( logic.playerInputLogic.receivedCard(card) } + private def getHand(player: AbstractPlayer): Hand = { + val handOption = player.currentHand() + if (handOption.isEmpty) { + throw new IllegalStateException("You have no cards!") + } + handOption.get + } + + private def getRound: Round = { + val roundOpt = logic.getCurrentRound + if (roundOpt.isEmpty) { + throw new IllegalStateException("No round is currently running!") + } + roundOpt.get + } + + private def getTrick: Trick = { + val trickOpt = logic.getCurrentTrick + if (trickOpt.isEmpty) { + throw new IllegalStateException("No trick is currently running!") + } + trickOpt.get + } + /** * Play a card from the player's hand while in dog life or skip the round. + * * @param userSession the user session of the player. - * @param cardIndex the index of the card in the player's hand or -1 if the player wants to skip the round. + * @param cardIndex the index of the card in the player's hand or -1 if the player wants to skip the round. */ def playDogCard(userSession: UserSession, cardIndex: Int): Unit = { val player = getPlayerInteractable(userSession, InteractionType.DogCard) @@ -214,8 +187,9 @@ class GameLobby private( /** * Select the trump suit for the round. + * * @param userSession the user session of the player. - * @param trumpIndex the index of the trump suit. + * @param trumpIndex the index of the trump suit. */ def selectTrump(userSession: UserSession, trumpIndex: Int): Unit = { val player = getPlayerInteractable(userSession, InteractionType.TrumpSuit) @@ -225,8 +199,21 @@ class GameLobby private( logic.playerInputLogic.receivedTrumpSuit(selectedTrump) } + + //------------------- + + private def getPlayerInteractable(userSession: UserSession, iType: InteractionType): AbstractPlayer = { + if (!userSession.lock.isHeldByCurrentThread) { + throw new IllegalStateException("The user session is not locked!") + } + if (userSession.canInteract.isEmpty || userSession.canInteract.get != iType) { + throw new NotInteractableException("You can't play a card!") + } + getPlayerBySession(userSession) + } + /** - * + * * @param userSession * @param tieNumber */ @@ -249,9 +236,10 @@ class GameLobby private( logic.createSession() } - - //------------------- - + def getPlayerByUser(user: User): AbstractPlayer = { + getPlayerBySession(getUserSession(user.id)) + } + def getUserSession(userId: UUID): UserSession = { val sessionOpt = users.get(userId) if (sessionOpt.isEmpty) { @@ -260,8 +248,20 @@ class GameLobby private( sessionOpt.get } - def getPlayerByUser(user: User): AbstractPlayer = { - getPlayerBySession(getUserSession(user.id)) + private def getPlayerBySession(userSession: UserSession): AbstractPlayer = { + val playerOption = getMatch.totalplayers.find(_.id == userSession.id) + if (playerOption.isEmpty) { + throw new NotInThisGameException("You are not in this game!") + } + playerOption.get + } + + private def getMatch: Match = { + val matchOpt = logic.getCurrentMatch + if (matchOpt.isEmpty) { + throw new IllegalStateException("No match is currently running!") + } + matchOpt.get } def getPlayers: mutable.Map[UUID, UserSession] = { @@ -271,63 +271,17 @@ class GameLobby private( def getLogic: GameLogic = { logic } - def getEventsOfPlayer(playerId: UUID): mutable.Queue[PollingEvents] = { - eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue()) - } - private def getPlayerBySession(userSession: UserSession): AbstractPlayer = { - val playerOption = getMatch.totalplayers.find(_.id == userSession.id) - if (playerOption.isEmpty) { - throw new NotInThisGameException("You are not in this game!") - } - playerOption.get - } - - private def getPlayerInteractable(userSession: UserSession, iType: InteractionType): AbstractPlayer = { - if (!userSession.lock.isHeldByCurrentThread) { - throw new IllegalStateException("The user session is not locked!") - } - if (userSession.canInteract.isEmpty || userSession.canInteract.get != iType) { - throw new NotInteractableException("You can't play a card!") - } - getPlayerBySession(userSession) - } - - private def getHand(player: AbstractPlayer): Hand = { - val handOption = player.currentHand() - if (handOption.isEmpty) { - throw new IllegalStateException("You have no cards!") - } - handOption.get - } - - private def getMatch: Match = { - val matchOpt = logic.getCurrentMatch - if (matchOpt.isEmpty) { - throw new IllegalStateException("No match is currently running!") - } - matchOpt.get - } - - private def getRound: Round = { - val roundOpt = logic.getCurrentRound - if (roundOpt.isEmpty) { - throw new IllegalStateException("No round is currently running!") - } - roundOpt.get - } - - private def getTrick: Trick = { - val trickOpt = logic.getCurrentTrick - if (trickOpt.isEmpty) { - throw new IllegalStateException("No trick is currently running!") - } - trickOpt.get - } def getUsers: Set[User] = { users.values.map(d => d.user).toSet } + private def transmitToAll(event: JsObject): Unit = { + users.values.foreach(session => { + session.websocketActor.foreach(act => act.transmitJsonToClient(event)) + }) + } + } object GameLobby { diff --git a/knockoutwhistweb/app/logic/game/PollingEvents.scala b/knockoutwhistweb/app/logic/game/PollingEvents.scala deleted file mode 100644 index cc1117e..0000000 --- a/knockoutwhistweb/app/logic/game/PollingEvents.scala +++ /dev/null @@ -1,10 +0,0 @@ -package logic.game - -enum PollingEvents { - case CardPlayed - case NewRound - case NewTrick - case ReloadEvent - case LobbyUpdate - case LobbyCreation -} \ No newline at end of file diff --git a/knockoutwhistweb/app/logic/user/SessionManager.scala b/knockoutwhistweb/app/logic/user/SessionManager.scala index aad7472..4096ffb 100644 --- a/knockoutwhistweb/app/logic/user/SessionManager.scala +++ b/knockoutwhistweb/app/logic/user/SessionManager.scala @@ -6,9 +6,11 @@ import model.users.User @ImplementedBy(classOf[BaseSessionManager]) trait SessionManager { - + def createSession(user: User): String + def getUserBySession(sessionId: String): Option[User] + def invalidateSession(sessionId: String): Unit } diff --git a/knockoutwhistweb/app/logic/user/UserManager.scala b/knockoutwhistweb/app/logic/user/UserManager.scala index ecf3a8d..98e1912 100644 --- a/knockoutwhistweb/app/logic/user/UserManager.scala +++ b/knockoutwhistweb/app/logic/user/UserManager.scala @@ -8,9 +8,13 @@ import model.users.User trait UserManager { def addUser(name: String, password: String): Boolean + def authenticate(name: String, password: String): Option[User] + def userExists(name: String): Option[User] + def userExistsById(id: Long): Option[User] + def removeUser(name: String): Boolean } diff --git a/knockoutwhistweb/app/logic/user/impl/StubUserManager.scala b/knockoutwhistweb/app/logic/user/impl/StubUserManager.scala index 398a908..a50bd58 100644 --- a/knockoutwhistweb/app/logic/user/impl/StubUserManager.scala +++ b/knockoutwhistweb/app/logic/user/impl/StubUserManager.scala @@ -9,7 +9,7 @@ import javax.inject.{Inject, Singleton} @Singleton class StubUserManager @Inject()(val config: Config) extends UserManager { - + private val user: Map[String, User] = Map( "Janis" -> User( internalId = 1L, @@ -53,5 +53,5 @@ class StubUserManager @Inject()(val config: Config) extends UserManager { override def removeUser(name: String): Boolean = { throw new NotImplementedError("StubUserManager.removeUser is not implemented") } - + } diff --git a/knockoutwhistweb/app/model/sessions/InteractionType.scala b/knockoutwhistweb/app/model/sessions/InteractionType.scala index e265edb..db6fedc 100644 --- a/knockoutwhistweb/app/model/sessions/InteractionType.scala +++ b/knockoutwhistweb/app/model/sessions/InteractionType.scala @@ -1,7 +1,7 @@ package model.sessions enum InteractionType { - + case TrumpSuit case Card case DogCard diff --git a/knockoutwhistweb/app/model/sessions/PlayerSession.scala b/knockoutwhistweb/app/model/sessions/PlayerSession.scala index 95c39f5..e7ba0b1 100644 --- a/knockoutwhistweb/app/model/sessions/PlayerSession.scala +++ b/knockoutwhistweb/app/model/sessions/PlayerSession.scala @@ -5,9 +5,11 @@ import de.knockoutwhist.utils.events.SimpleEvent import java.util.UUID trait PlayerSession { - + def id: UUID + def name: String + def updatePlayer(event: SimpleEvent): Unit - + } diff --git a/knockoutwhistweb/app/model/sessions/SimpleSession.scala b/knockoutwhistweb/app/model/sessions/SimpleSession.scala index a4c7007..6f31820 100644 --- a/knockoutwhistweb/app/model/sessions/SimpleSession.scala +++ b/knockoutwhistweb/app/model/sessions/SimpleSession.scala @@ -6,9 +6,9 @@ import de.knockoutwhist.utils.events.SimpleEvent import java.util.UUID case class SimpleSession(id: UUID, player: AbstractPlayer) extends PlayerSession { - + def name: String = player.name - + override def updatePlayer(event: SimpleEvent): Unit = { } } diff --git a/knockoutwhistweb/app/model/sessions/UserSession.scala b/knockoutwhistweb/app/model/sessions/UserSession.scala index d26abcc..ed249c9 100644 --- a/knockoutwhistweb/app/model/sessions/UserSession.scala +++ b/knockoutwhistweb/app/model/sessions/UserSession.scala @@ -4,18 +4,16 @@ import de.knockoutwhist.events.player.{RequestCardEvent, RequestTieChoiceEvent, import de.knockoutwhist.utils.events.SimpleEvent import logic.game.GameLobby import model.users.User -import org.apache.pekko.actor.{Actor, ActorRef} -import play.api.libs.json.{JsObject, JsValue, Json} -import util.WebsocketEventMapper +import play.api.libs.json.JsObject import java.util.UUID import java.util.concurrent.locks.ReentrantLock -import scala.util.{Failure, Success, Try} +import scala.util.Try class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) extends PlayerSession { + val lock: ReentrantLock = ReentrantLock() var canInteract: Option[InteractionType] = None var websocketActor: Option[UserWebsocketActor] = None - val lock: ReentrantLock = ReentrantLock() override def updatePlayer(event: SimpleEvent): Unit = { event match { @@ -33,7 +31,7 @@ class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) e override def id: UUID = user.id override def name: String = user.name - + def resetCanInteract(): Unit = { canInteract = None } @@ -49,5 +47,5 @@ class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) e } lock.unlock() } - + } diff --git a/knockoutwhistweb/app/model/sessions/UserWebsocketActor.scala b/knockoutwhistweb/app/model/sessions/UserWebsocketActor.scala index 0262ce1..fba8100 100644 --- a/knockoutwhistweb/app/model/sessions/UserWebsocketActor.scala +++ b/knockoutwhistweb/app/model/sessions/UserWebsocketActor.scala @@ -37,15 +37,6 @@ class UserWebsocketActor( case other => } - def transmitEventToClient(event: SimpleEvent): Unit = { - val jsonString = WebsocketEventMapper.toJsonString(event) - out ! jsonString - } - - private def transmitJsonToClient(jsonObj: JsObject): Unit = { - out ! jsonObj.toString() - } - private def transmitTextToClient(text: String): Unit = { out ! text } @@ -91,4 +82,13 @@ class UserWebsocketActor( } } + def transmitJsonToClient(jsonObj: JsObject): Unit = { + out ! jsonObj.toString() + } + + def transmitEventToClient(event: SimpleEvent): Unit = { + val jsonString = WebsocketEventMapper.toJsonString(event) + out ! jsonString + } + } diff --git a/knockoutwhistweb/app/model/users/User.scala b/knockoutwhistweb/app/model/users/User.scala index e56c048..f418618 100644 --- a/knockoutwhistweb/app/model/users/User.scala +++ b/knockoutwhistweb/app/model/users/User.scala @@ -3,10 +3,10 @@ package model.users import java.util.UUID case class User( - internalId: Long, - id: UUID, - name: String, - passwordHash: String + internalId: Long, + id: UUID, + name: String, + passwordHash: String ) { def withName(newName: String): User = { diff --git a/knockoutwhistweb/app/services/JwtKeyProvider.scala b/knockoutwhistweb/app/services/JwtKeyProvider.scala index f1fb46f..bb05df9 100644 --- a/knockoutwhistweb/app/services/JwtKeyProvider.scala +++ b/knockoutwhistweb/app/services/JwtKeyProvider.scala @@ -12,10 +12,28 @@ import javax.inject.* @Singleton class JwtKeyProvider @Inject()(config: Configuration) { - private def cleanPem(pem: String): String = - pem.replaceAll("-----BEGIN (.*)-----", "") - .replaceAll("-----END (.*)-----", "") - .replaceAll("\\s", "") + val publicKey: RSAPublicKey = { + val pemOpt = config.getOptional[String]("auth.publicKeyPem") + val fileOpt = config.getOptional[String]("auth.publicKeyFile") + + pemOpt.orElse(fileOpt.map { path => + new String(Files.readAllBytes(Paths.get(path))) + }) match { + case Some(pem) => loadPublicKeyFromPem(pem) + case None => throw new RuntimeException("No RSA public key configured.") + } + } + val privateKey: RSAPrivateKey = { + val pemOpt = config.getOptional[String]("auth.privateKeyPem") + val fileOpt = config.getOptional[String]("auth.privateKeyFile") + + pemOpt.orElse(fileOpt.map { path => + new String(Files.readAllBytes(Paths.get(path))) + }) match { + case Some(pem) => loadPrivateKeyFromPem(pem) + case None => throw new RuntimeException("No RSA private key configured.") + } + } private def loadPublicKeyFromPem(pem: String): RSAPublicKey = { val decoded = Base64.getDecoder.decode(cleanPem(pem)) @@ -29,28 +47,9 @@ class JwtKeyProvider @Inject()(config: Configuration) { KeyFactory.getInstance("RSA").generatePrivate(spec).asInstanceOf[RSAPrivateKey] } - val publicKey: RSAPublicKey = { - val pemOpt = config.getOptional[String]("auth.publicKeyPem") - val fileOpt = config.getOptional[String]("auth.publicKeyFile") + private def cleanPem(pem: String): String = + pem.replaceAll("-----BEGIN (.*)-----", "") + .replaceAll("-----END (.*)-----", "") + .replaceAll("\\s", "") - pemOpt.orElse(fileOpt.map { path => - new String(Files.readAllBytes(Paths.get(path))) - }) match { - case Some(pem) => loadPublicKeyFromPem(pem) - case None => throw new RuntimeException("No RSA public key configured.") - } - } - - val privateKey: RSAPrivateKey = { - val pemOpt = config.getOptional[String]("auth.privateKeyPem") - val fileOpt = config.getOptional[String]("auth.privateKeyFile") - - pemOpt.orElse(fileOpt.map { path => - new String(Files.readAllBytes(Paths.get(path))) - }) match { - case Some(pem) => loadPrivateKeyFromPem(pem) - case None => throw new RuntimeException("No RSA private key configured.") - } - } - } diff --git a/knockoutwhistweb/app/util/GameUtil.scala b/knockoutwhistweb/app/util/GameUtil.scala index b2caf50..a250603 100644 --- a/knockoutwhistweb/app/util/GameUtil.scala +++ b/knockoutwhistweb/app/util/GameUtil.scala @@ -25,5 +25,5 @@ object GameUtil { code.toString() } - + } diff --git a/knockoutwhistweb/app/views/ingame/finishedMatch.scala.html b/knockoutwhistweb/app/views/ingame/finishedMatch.scala.html index 13de1f4..85fc7cf 100644 --- a/knockoutwhistweb/app/views/ingame/finishedMatch.scala.html +++ b/knockoutwhistweb/app/views/ingame/finishedMatch.scala.html @@ -34,5 +34,5 @@ }); } waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id')); - connectWebSocket() + connectWebSocket() \ No newline at end of file diff --git a/knockoutwhistweb/app/views/ingame/ingame.scala.html b/knockoutwhistweb/app/views/ingame/ingame.scala.html index 1310bf2..9770a8d 100644 --- a/knockoutwhistweb/app/views/ingame/ingame.scala.html +++ b/knockoutwhistweb/app/views/ingame/ingame.scala.html @@ -12,10 +12,10 @@

Current Player

@gamelobby.getLogic.getCurrentPlayer.get.name

@if(!TrickUtil.isOver(gamelobby.getLogic.getCurrentMatch.get, gamelobby.getLogic.getPlayerQueue.get)) { -

Next Player

- @for(nextplayer <- gamelobby.getLogic.getPlayerQueue.get.duplicate()) { -

@nextplayer

- } +

Next Player

+ @for(nextplayer <- gamelobby.getLogic.getPlayerQueue.get.duplicate()) { +

@nextplayer

+ } } @@ -30,21 +30,22 @@ @for(player <- gamelobby.getLogic.getPlayerQueue.get.toList.sortBy { p => - -(gamelobby.getLogic.getCurrentRound.get.tricklist.filter { trick => trick.winner.contains(p) }.size) + -(gamelobby.getLogic.getCurrentRound.get.tricklist.filter { trick => trick.winner.contains(p) }.size) }) { -
-
@player.name
-
+
+
@player.name
+
@(gamelobby.getLogic.getCurrentRound.get.tricklist.filter { trick => trick.winner.contains(player) }.size) +
-
}
- @for((cardplayed, player) <- gamelobby.getLogic.getCurrentTrick.get.cards) { + @for((cardplayed, player) <- gamelobby.getLogic.getCurrentTrick.get.cards) {
-
+
@util.WebUIUtils.cardtoImage(cardplayed) width="100%"/>
@@ -53,7 +54,7 @@
- } + }
@@ -62,31 +63,35 @@
First Card
- @if(gamelobby.getLogic.getCurrentTrick.get.firstCard.isDefined) { - @util.WebUIUtils.cardtoImage(gamelobby.getLogic.getCurrentTrick.get.firstCard.get) width="80px"/> - } else { + @if(gamelobby.getLogic.getCurrentTrick.get.firstCard.isDefined) { + @util.WebUIUtils.cardtoImage(gamelobby.getLogic.getCurrentTrick.get.firstCard.get) + width="80px"/> + } else { @views.html.render.card.apply("images/cards/1B.png")("Blank Card") width="80px"/> - } + }
-
+
- @for(i <- player.currentHand().get.cards.indices) { + @for(i <- player.currentHand().get.cards.indices) {
@util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/>
@if(player.isInDogLife) {
- +
}
- } + }
@@ -104,5 +109,5 @@ }); } waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id')); - connectWebSocket() + connectWebSocket() diff --git a/knockoutwhistweb/app/views/ingame/selecttrump.scala.html b/knockoutwhistweb/app/views/ingame/selecttrump.scala.html index 2b9c267..c42c2be 100644 --- a/knockoutwhistweb/app/views/ingame/selecttrump.scala.html +++ b/knockoutwhistweb/app/views/ingame/selecttrump.scala.html @@ -18,35 +18,40 @@
- @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Spades)) width="120px" style="border-radius: 6px"/> + @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Spades)) + width="120px" style="border-radius: 6px"/>
- @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Hearts)) width="120px" style="border-radius: 6px"/> + @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Hearts)) + width="120px" style="border-radius: 6px"/>
- @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Diamonds)) width="120px" style="border-radius: 6px"/> + @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Diamonds)) + width="120px" style="border-radius: 6px"/>
- @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Clubs)) width="120px" style="border-radius: 6px"/> + @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Clubs)) + width="120px" style="border-radius: 6px"/>
- @for(i <- player.currentHand().get.cards.indices) { -
- @util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/> -
- } + @for(i <- player.currentHand().get.cards.indices) { +
+ @util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/> +
+ }
} else { } @@ -68,5 +73,5 @@ }); } waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id')); - connectWebSocket() + connectWebSocket() \ No newline at end of file diff --git a/knockoutwhistweb/app/views/ingame/tie.scala.html b/knockoutwhistweb/app/views/ingame/tie.scala.html index 20bd17e..57e4826 100644 --- a/knockoutwhistweb/app/views/ingame/tie.scala.html +++ b/knockoutwhistweb/app/views/ingame/tie.scala.html @@ -13,9 +13,9 @@

The last round was tied between: - @for(players <- gamelobby.logic.playerTieLogic.getTiedPlayers) { - @players - } + @for(players <- gamelobby.logic.playerTieLogic.getTiedPlayers) { + @players + }

@@ -23,7 +23,10 @@ @if(player.equals(gamelobby.logic.playerTieLogic.currentTiePlayer())) { @defining(gamelobby.logic.playerTieLogic.highestAllowedNumber()) { maxNum =>
@@ -31,49 +34,21 @@
- +
- +
Currently Picked Cards
- @if(gamelobby.logic.playerTieLogic.getSelectedCard.nonEmpty) { - @for((player, card) <- gamelobby.logic.playerTieLogic.getSelectedCard) { -
-
-
-

@player

-
- @util.WebUIUtils.cardtoImage(card) -
-
-
-
- } - } else { -
- -
- } -
- } - } else { - - -
Currently Picked Cards
- -
@if(gamelobby.logic.playerTieLogic.getSelectedCard.nonEmpty) { @for((player, card) <- gamelobby.logic.playerTieLogic.getSelectedCard) { -
+

@player

@@ -92,6 +67,38 @@
} +
+ } + } else { + + +
Currently Picked Cards
+ +
+ @if(gamelobby.logic.playerTieLogic.getSelectedCard.nonEmpty) { + @for((player, card) <- gamelobby.logic.playerTieLogic.getSelectedCard) { +
+
+
+

@player

+
+ @util.WebUIUtils.cardtoImage(card) +
+
+
+
+ } + } else { +
+ +
+ }
} @@ -114,5 +121,5 @@ }); } waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id')); - connectWebSocket() + connectWebSocket() \ No newline at end of file diff --git a/knockoutwhistweb/app/views/lobby/lobby.scala.html b/knockoutwhistweb/app/views/lobby/lobby.scala.html index 85bda17..f3f6346 100644 --- a/knockoutwhistweb/app/views/lobby/lobby.scala.html +++ b/knockoutwhistweb/app/views/lobby/lobby.scala.html @@ -14,7 +14,8 @@
-
Playeramount: @gamelobby.getPlayers.size / @gamelobby.maxPlayers
+
+ Playeramount: @gamelobby.getPlayers.size / @gamelobby.maxPlayers
@@ -30,7 +31,8 @@ Remove } else {
@playersession.name
-
Remove
+
+ Remove
}
@@ -78,5 +80,5 @@ }); } waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id')); - connectWebSocket() + connectWebSocket() \ No newline at end of file diff --git a/knockoutwhistweb/app/views/main.scala.html b/knockoutwhistweb/app/views/main.scala.html index 824bf53..72e8799 100644 --- a/knockoutwhistweb/app/views/main.scala.html +++ b/knockoutwhistweb/app/views/main.scala.html @@ -1,8 +1,8 @@ @* - * This template is called from the `index` template. This template - * handles the rendering of the page header and body tags. It takes - * two arguments, a `String` for the title of the page and an `Html` - * object to insert into the body of the page. +* This template is called from the `index` template. This template +* handles the rendering of the page header and body tags. It takes +* two arguments, a `String` for the title of the page and an `Html` +* object to insert into the body of the page. *@ @(title: String)(content: Html) @@ -19,13 +19,13 @@ - @* And here's where we render the `Html` object containing - * the page content. *@ - @content + @* And here's where we render the `Html` object containing + * the page content. *@ + @content - + diff --git a/knockoutwhistweb/app/views/mainmenu/creategame.scala.html b/knockoutwhistweb/app/views/mainmenu/creategame.scala.html index 958b835..e521320 100644 --- a/knockoutwhistweb/app/views/mainmenu/creategame.scala.html +++ b/knockoutwhistweb/app/views/mainmenu/creategame.scala.html @@ -11,23 +11,23 @@ -
- - -
- 2 - 3 - 4 - 5 - 6 - 7 -
+
+ + +
+ 2 + 3 + 4 + 5 + 6 + 7
+
Create Game
- \ No newline at end of file diff --git a/knockoutwhistweb/app/views/mainmenu/navbar.scala.html b/knockoutwhistweb/app/views/mainmenu/navbar.scala.html index 5eb501a..6ddc4c0 100644 --- a/knockoutwhistweb/app/views/mainmenu/navbar.scala.html +++ b/knockoutwhistweb/app/views/mainmenu/navbar.scala.html @@ -1,57 +1,61 @@ @(user: Option[model.users.User]) - + + diff --git a/knockoutwhistweb/app/views/mainmenu/rules.scala.html b/knockoutwhistweb/app/views/mainmenu/rules.scala.html index 989611b..e4ff1a4 100644 --- a/knockoutwhistweb/app/views/mainmenu/rules.scala.html +++ b/knockoutwhistweb/app/views/mainmenu/rules.scala.html @@ -1,180 +1,180 @@ @(user: Option[model.users.User]) - @navbar(user) +@navbar(user) -
-
-
-
-

Game Rules Overview

-
+
+
+
+
+

Game Rules Overview

+
-
- + -
-
-

- -

-
-
- Two to seven players. The aim is to be the last player left in the game. -
+
+
+

+ +

+
+
+ Two to seven players. The aim is to be the last player left in the game.
- -
-

- -

-
-
- To be the last player left at the end of the game, with the object in each hand being to win a majority of tricks. -
-
-
- -
-

- -

-
-
- A standard 52-card pack is used. -
-
-
- -
-

- -

-
-
- In each suit, cards rank from highest to lowest: A K Q J 10 9 8 7 6 5 4 3 2. -
-
-
- -
-

- -

-
-
- The dealer deals seven cards to each player. One card is turned up to determine the trump suit for the round. -
-
-
- -
-

- -

-
-
- The deal rotates clockwise. The player who took the most tricks in the previous hand selects the trump suit. If there's a tie, players cut cards to decide who calls trumps. One fewer card is dealt in each successive hand until the final hand consists of one card each. -
-
-
- -
-

- -

-
-
- The player to the dealer's left (eldest hand) leads the first trick. Any card can be led. Other players must follow suit if possible. A player with no cards of the suit led may play any card. -
-
-
- -
-

- -

-
-
- The highest card of the suit led wins, unless a trump is played, in which case the highest trump wins. The winner of the trick leads the next. -
-
-
- -
-

- -

-
-
- Some rules disallow leading trumps before the suit has been 'broken' (a trump has been played to the lead of another suit). Leading trumps is always permissible if a player holds only trumps. -
-
-
- -
-

- -

-
-
- At the end of each hand, any player who took no tricks is knocked out and takes no further part in the game. -
-
-
- -
-

- -

-
-
- The game is won when a player takes all the tricks in a round, as all other players are knocked out, leaving only one player remaining. -
-
-
- -
-

- -

-
-
- The first player who takes no tricks is awarded a \"dog's life\". In the next hand, that player is dealt one card and can decide which trick to play it to. Each time a trick is played the dog may either play the card or knock on the table and wait to play it later. If the dog wins a trick, the player to the left leads the next and the dog re-enters the game properly in the next hand. If the dog fails, they are knocked out. -
-
-
-
+ +
+

+ +

+
+
+ To be the last player left at the end of the game, with the object in each hand being to win a majority of tricks. +
+
+
+ +
+

+ +

+
+
+ A standard 52-card pack is used. +
+
+
+ +
+

+ +

+
+
+ In each suit, cards rank from highest to lowest: A K Q J 10 9 8 7 6 5 4 3 2. +
+
+
+ +
+

+ +

+
+
+ The dealer deals seven cards to each player. One card is turned up to determine the trump suit for the round. +
+
+
+ +
+

+ +

+
+
+ The deal rotates clockwise. The player who took the most tricks in the previous hand selects the trump suit. If there's a tie, players cut cards to decide who calls trumps. One fewer card is dealt in each successive hand until the final hand consists of one card each. +
+
+
+ +
+

+ +

+
+
+ The player to the dealer's left (eldest hand) leads the first trick. Any card can be led. Other players must follow suit if possible. A player with no cards of the suit led may play any card. +
+
+
+ +
+

+ +

+
+
+ The highest card of the suit led wins, unless a trump is played, in which case the highest trump wins. The winner of the trick leads the next. +
+
+
+ +
+

+ +

+
+
+ Some rules disallow leading trumps before the suit has been 'broken' (a trump has been played to the lead of another suit). Leading trumps is always permissible if a player holds only trumps. +
+
+
+ +
+

+ +

+
+
+ At the end of each hand, any player who took no tricks is knocked out and takes no further part in the game. +
+
+
+ +
+

+ +

+
+
+ The game is won when a player takes all the tricks in a round, as all other players are knocked out, leaving only one player remaining. +
+
+
+ +
+

+ +

+
+
+ The first player who takes no tricks is awarded a \"dog's life\". In the next hand, that player is dealt one card and can decide which trick to play it to. Each time a trick is played the dog may either play the card or knock on the table and wait to play it later. If the dog wins a trick, the player to the left leads the next and the dog re-enters the game properly in the next hand. If the dog fails, they are knocked out. +
+
+
+
-
+
+
diff --git a/knockoutwhistweb/conf/logback.xml b/knockoutwhistweb/conf/logback.xml index ab6c2b1..6126790 100644 --- a/knockoutwhistweb/conf/logback.xml +++ b/knockoutwhistweb/conf/logback.xml @@ -5,46 +5,48 @@ - - - - + + + + - - ${application.home:-.}/logs/application.log - - UTF-8 - %d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{pekkoSource}) %msg%n - - + + ${application.home:-.}/logs/application.log + + UTF-8 + %d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{pekkoSource}) %msg%n + + + - - - - - UTF-8 - %d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{pekkoSource}) %msg%n - - + + + + + UTF-8 + %d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{pekkoSource}) %msg%n + + + - - - + + + - - - + + + - - + + - - - - + + + + diff --git a/knockoutwhistweb/conf/routes b/knockoutwhistweb/conf/routes index 73a8569..ac9c094 100644 --- a/knockoutwhistweb/conf/routes +++ b/knockoutwhistweb/conf/routes @@ -4,41 +4,27 @@ # ~~~~ # For the javascript routing -GET /assets/js/routes controllers.JavaScriptRoutingController.javascriptRoutes() +GET /assets/js/routes controllers.JavaScriptRoutingController.javascriptRoutes() # Primary routes -GET / controllers.MainMenuController.index() -GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) +GET / controllers.MainMenuController.index() +GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) # Main menu routes -GET /mainmenu controllers.MainMenuController.mainMenu() -GET /rules controllers.MainMenuController.rules() -GET /navSPA/:pType controllers.MainMenuController.navSPA(pType) +GET /mainmenu controllers.MainMenuController.mainMenu() +GET /rules controllers.MainMenuController.rules() +GET /navSPA/:pType controllers.MainMenuController.navSPA(pType) -POST /createGame controllers.MainMenuController.createGame() -POST /joinGame controllers.MainMenuController.joinGame() +POST /createGame controllers.MainMenuController.createGame() +POST /joinGame controllers.MainMenuController.joinGame() # User authentication routes -GET /login controllers.UserController.login() -POST /login controllers.UserController.login_Post() +GET /login controllers.UserController.login() +POST /login controllers.UserController.login_Post() -GET /logout controllers.UserController.logout() +GET /logout controllers.UserController.logout() # In-game routes -GET /game/:id controllers.IngameController.game(id: String) -POST /game/:id/start controllers.IngameController.startGame(id: String) -POST /game/:id/kickPlayer/:playerToKick controllers.IngameController.kickPlayer(id: String, playerToKick: String) - -POST /game/:id/trump controllers.IngameController.playTrump(id: String) -POST /game/:id/tie controllers.IngameController.playTie(id: String) - -GET /game/:id/leaveGame controllers.IngameController.leaveGame(id: String) -POST /game/:id/playCard controllers.IngameController.playCard(id: String) -POST /game/:id/dogPlayCard controllers.IngameController.playDogCard(id: String) - -POST /game/:id/returnToLobby controllers.IngameController.returnToLobby(id: String) - -# Polling -GET /polling/:gameId controllers.PollingController.polling(gameId: String) +GET /game/:id controllers.IngameController.game(id: String) # Websocket -GET /websocket controllers.WebsocketController.socket() \ No newline at end of file +GET /websocket controllers.WebsocketController.socket() \ No newline at end of file diff --git a/knockoutwhistweb/public/javascripts/main.js b/knockoutwhistweb/public/javascripts/main.js index 1f8f830..d843bd3 100644 --- a/knockoutwhistweb/public/javascripts/main.js +++ b/knockoutwhistweb/public/javascripts/main.js @@ -79,206 +79,6 @@ }) })() -let polling = false; -function pollForUpdates(gameId) { - if (polling) { - console.log("[DEBUG] Polling already in progress. Skipping this cycle."); - return; - } - polling = true; - console.log(`[DEBUG] Starting poll cycle for Game ID: ${gameId} at ${new Date().toISOString()}`); - if (!gameId) { - console.error("[DEBUG] Game ID is missing. Stopping poll."); - return; - } - const $handElement = $('#card-slide'); - const $lobbyElement = $('#lobbybackground'); - const $mainmenuElement = $('#main-menu-screen') - const $mainbody = $('#main-body') - if (!$handElement.length && !$lobbyElement.length && !$mainmenuElement.length && !$mainbody.length) { - setTimeout(() => { polling = false; pollForUpdates(gameId) }, 1000); - return; - } - const route = jsRoutes.controllers.PollingController.polling(gameId); - $.ajax({ - url: route.url, - type: 'GET', - dataType: 'json', - - success: (data => { - if (!data) { - console.log("[DEBUG] Received 204 No Content (Timeout). Restarting poll."); - return; - } - if (data.status === "cardPlayed" && data.handData) { - console.log("Event received: Card played. Redrawing hand."); - const newHand = data.handData; - let newHandHTML = ''; - $handElement.empty(); - - if(data.animation) { - $handElement.addClass('ingame-cards-slide'); - } else { - $handElement.removeClass('ingame-cards-slide'); - } - - const dog = data.dog; - - newHand.forEach((cardId, index) => { - const cardHtml = ` -
-
- - -
-
- `; - newHandHTML += cardHtml; - }); - - if (dog) { - newHandHTML += ` -
- -
- `; - } - - $handElement.html(newHandHTML); - - if (data.yourTurn) { - $handElement.removeClass('inactive'); - } else { - $handElement.addClass('inactive'); - } - - $('#current-player-name').text(data.currentPlayerName) - if (data.nextPlayer) { - $('#next-player-name').text(data.nextPlayer); - } else if (nextPlayerElement) { - $('#next-player-name').text(""); - } else { - console.warn("[DEBUG] 'current-player-name' element missing in DOM"); - } - $('#trump-suit').text(data.trumpSuit); - if ($('#trick-cards-container').length) { - let trickHTML = ''; - - data.trickCards.forEach(trickCard => { - trickHTML += ` -
-
-
- -
-
- ${trickCard.player} -
-
-
- `; - }); - $('#trick-cards-container').html(trickHTML); - } - if ($('#score-table-body').length && data.scoreTable) { - let scoreHTML = ''; - scoreHTML += `

Tricks Won

- -
-
PLAYER
-
TRICKS
-
` - data.scoreTable.forEach(score => { - scoreHTML += ` -
-
${score.name}
-
${score.tricks}
-
- `; - }); - $('#score-table-body').html(scoreHTML); - } - const cardId = data.firstCardId; - if ($('#first-card-container').length) { - let imageSrc = ''; - let altText = 'First Card'; - - if (cardId === "BLANK") { - imageSrc = "/assets/images/cards/1B.png"; - altText = "Blank Card"; - } else { - imageSrc = `/assets/images/cards/${cardId}.png`; - } - - const newImageHTML = ` - ${altText} - `; - - $('#first-card-container').html(newImageHTML); - } - } else if (data.status === "reloadEvent") { - console.log("[DEBUG] Reload event received. Redirecting..."); - exchangeBody(data.content, "Knockout Whist - Ingame", data.redirectUrl); - } - else if (data.status === "lobbyUpdate") { - console.log("[DEBUG] Entering 'lobbyUpdate' logic."); - let newHtml = '' - - if (data.host) { - data.users.forEach(user => { - - const inner = user.self ? `
${user.name} (You)
- Remove` - : `
${user.name}
-
Remove
` - - newHtml += `
-
- Profile -
- ${inner} -
-
-
` - }) - } else { - data.users.forEach(user => { - - const inner = user.self ? `
${user.name} (You)
` : `
${user.name}
` - - newHtml += `
-
- Profile -
- ${inner} -
-
-
` - }) - } - $("#players").html(newHtml); - $('#playerAmount').text(`Playeramount: ${data.users.length} / ${data.maxPlayers}`); - } else { - console.warn(`[DEBUG] Received unknown status: ${data.status}`); - } - }), - error: ((jqXHR, textStatus, errorThrown) => { - if (jqXHR.status >= 400) { - console.error(`Server error: ${jqXHR.status}, ${errorThrown}`); - } - else { - console.error(`Something unexpected happened while polling. ${jqXHR.status}, ${errorThrown}`) - } - }), - complete: (() => { - setTimeout(() => { polling = false; pollForUpdates(gameId) }, 200); - }) - }) -} - function createGameJS() { let lobbyName = $('#lobbyname').val(); if ($.trim(lobbyName) === "") { @@ -291,26 +91,6 @@ function createGameJS() { sendGameCreationRequest(jsonObj); } -function backToLobby(gameId) { - const route = jsRoutes.controllers.IngameController.returnToLobby(gameId); - - $.ajax({ - url: route.url, - type: route.type, - contentType: 'application/json', - dataType: 'json', - error: ((jqXHR) => { - const errorData = JSON.parse(jqXHR.responseText); - if (errorData && errorData.errorMessage) { - alert(`${errorData.errorMessage}`); - } else { - alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`); - } - }) - }) - -} - function sendGameCreationRequest(dataObject) { const route = jsRoutes.controllers.MainMenuController.createGame(); @@ -344,60 +124,6 @@ function exchangeBody(content, title = "Knockout Whist", url = null) { document.title = title; } -function startGame(gameId) { - sendGameStartRequest(gameId) -} - -function sendGameStartRequest(gameId) { - const route = jsRoutes.controllers.IngameController.startGame(gameId); - - $.ajax({ - url: route.url, - type: route.type, - dataType: 'json', - success: (data => { - if (data.status === 'success') { - window.location.href = data.redirectUrl; - } - }), - error: ((jqXHR) => { - const errorData = JSON.parse(jqXHR.responseText); - if (errorData && errorData.errorMessage) { - alert(`${errorData.errorMessage}`); - } else { - alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`); - } - }) - }) -} -function removePlayer(gameid, playersessionId) { - sendRemovePlayerRequest(gameid, playersessionId) -} - -function sendRemovePlayerRequest(gameId, playersessionId) { - const route = jsRoutes.controllers.IngameController.kickPlayer(gameId, playersessionId); - - $.ajax({ - url: route.url, - type: route.type, - contentType: 'application/json', - dataType: 'json', - success: (data => { - if (data.status === 'success') { - window.location.href = data.redirectUrl; - } - }), - error: ((jqXHR) => { - const errorData = JSON.parse(jqXHR.responseText); - if (errorData && errorData.errorMessage) { - alert(`${errorData.errorMessage}`); - } else { - alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`); - } - }) - }) -} - function login() { const username = $('#username').val(); const password = $('#password').val(); @@ -423,7 +149,7 @@ function login() { }), error: ((jqXHR) => { const errorData = JSON.parse(jqXHR.responseText); - if (errorData && errorData.errorMessage) { + if (errorData?.errorMessage) { alert(`${errorData.errorMessage}`); } else { alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`); @@ -455,7 +181,7 @@ function joinGame() { }), error: ((jqXHR) => { const errorData = JSON.parse(jqXHR.responseText); - if (errorData && errorData.errorMessage) { + if (errorData?.errorMessage) { alert(`${errorData.errorMessage}`); } else { alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`); @@ -482,7 +208,7 @@ function navSpa(page, title) { }), error: ((jqXHR) => { const errorData = JSON.parse(jqXHR.responseText); - if (errorData && errorData.errorMessage) { + if (errorData?.errorMessage) { alert(`${errorData.errorMessage}`); } else { alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`); @@ -490,178 +216,4 @@ function navSpa(page, title) { }) }); return false -} - - -function selectTie(gameId) { - const route = jsRoutes.controllers.IngameController.playTie(gameId); - const jsonObj = { - tie: $('#tieNumber').val() - }; - - $.ajax({ - url: route.url, - type: route.type, - contentType: 'application/json', - dataType: 'json', - data: JSON.stringify(jsonObj), - error: (jqXHR => { - let error; - try { - error = JSON.parse(jqXHR.responseText); - } catch (e) { - console.error("Failed to parse error response:", e); - } - if (error?.errorMessage) { - alert(`${error.errorMessage}`); - } else { - alert('An unexpected error occurred. Please try again.'); - } - }) - }) -} - -function leaveGame(gameId) { - sendLeavePlayerRequest(gameId) -} - -function sendLeavePlayerRequest(gameId) { - const route = jsRoutes.controllers.IngameController.leaveGame(gameId); - $.ajax({ - url: route.url, - type: route.type, - dataType: 'json', - error: ((jqXHR) => { - const errorData = JSON.parse(jqXHR.responseText); - if (errorData && errorData.errorMessage) { - alert(`${errorData.errorMessage}`); - } else { - alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`); - } - }) - }) -} - -function handleTrumpSelection(cardobject, gameId) { - const trumpId = cardobject.dataset.trump; - const jsonObj = { - trump: trumpId - } - - const route = jsRoutes.controllers.IngameController.playTrump(gameId); - - $.ajax({ - url: route.url, - type: route.type, - contentType: 'application/json', - dataType: 'json', - data: JSON.stringify(jsonObj), - error: (jqXHR => { - let error; - try { - error = JSON.parse(jqXHR.responseText); - } catch (e) { - console.error("Failed to parse error response:", e); - } - if (error?.errorMessage) { - alert(`${error.errorMessage}`); - } else { - alert('An unexpected error occurred. Please try again.'); - } - }) - }) -} - -function handlePlayCard(cardobject, gameId, dog = false) { - const cardId = cardobject.dataset.cardId; - const jsonObj = { - cardID: cardId - } - sendPlayCardRequest(jsonObj, gameId, cardobject, dog) -} - -function handleSkipDogLife(cardobject, gameId) { - - const wiggleKeyframes = [ - { transform: 'translateX(0)' }, - { transform: 'translateX(-5px)' }, - { transform: 'translateX(5px)' }, - { transform: 'translateX(-5px)' }, - { transform: 'translateX(0)' } - ]; - - const wiggleTiming = { - duration: 400, - iterations: 1, - easing: 'ease-in-out', - fill: 'forwards' - }; - - const route = jsRoutes.controllers.IngameController.playDogCard(gameId); - - $.ajax({ - url: route.url, - type: route.type, - contentType: 'application/json', - dataType: 'json', - data: JSON.stringify({ - cardID: 'skip' - }), - error: (jqXHR => { - let error; - try { - error = JSON.parse(jqXHR.responseText); - } catch (e) { - console.error("Failed to parse error response:", e); - } - if (error?.errorMessage.includes("You can't skip this round!")) { - cardobject.parentElement.animate(wiggleKeyframes, wiggleTiming); - } else if (error?.errorMessage) { - alert(`${error.errorMessage}`); - } else { - alert('An unexpected error occurred. Please try again.'); - } - }) - }) -} - -function sendPlayCardRequest(jsonObj, gameId, cardobject, dog) { - const wiggleKeyframes = [ - { transform: 'translateX(0)' }, - { transform: 'translateX(-5px)' }, - { transform: 'translateX(5px)' }, - { transform: 'translateX(-5px)' }, - { transform: 'translateX(0)' } - ]; - - const wiggleTiming = { - duration: 400, - iterations: 1, - easing: 'ease-in-out', - fill: 'forwards' - }; - const route = dog === "true" ? jsRoutes.controllers.IngameController.playDogCard(gameId) : jsRoutes.controllers.IngameController.playCard(gameId); - - $.ajax({ - url: route.url, - type: route.type, - contentType: 'application/json', - dataType: 'json', - data: JSON.stringify(jsonObj), - error: (jqXHR => { - try { - error = JSON.parse(jqXHR.responseText); - } catch (e) { - console.error("Failed to parse error response:", e); - } - if (error?.errorMessage.includes("You can't play this card!")) { - cardobject.parentElement.animate(wiggleKeyframes, wiggleTiming); - } else if (error?.errorMessage) { - alert(`${error.errorMessage}`); - } else { - alert('An unexpected error occurred. Please try again.'); - } - }) - }) -} - +} \ No newline at end of file diff --git a/knockoutwhistweb/public/javascripts/particles.js b/knockoutwhistweb/public/javascripts/particles.js index 325d834..2a80d26 100644 --- a/knockoutwhistweb/public/javascripts/particles.js +++ b/knockoutwhistweb/public/javascripts/particles.js @@ -7,1466 +7,1449 @@ /* v2.0.0 /* ----------------------------------------------- */ -var pJS = function(tag_id, params){ +var pJS = function (tag_id, params) { - var canvas_el = document.querySelector('#'+tag_id+' > .particles-js-canvas-el'); + var canvas_el = document.querySelector('#' + tag_id + ' > .particles-js-canvas-el'); - /* particles.js variables with default values */ - this.pJS = { - canvas: { - el: canvas_el, - w: canvas_el.offsetWidth, - h: canvas_el.offsetHeight - }, - particles: { - number: { - value: 400, - density: { - enable: true, - value_area: 800 - } - }, - color: { - value: '#fff' - }, - shape: { - type: 'circle', - stroke: { - width: 0, - color: '#ff0000' + /* particles.js variables with default values */ + this.pJS = { + canvas: { + el: canvas_el, + w: canvas_el.offsetWidth, + h: canvas_el.offsetHeight }, - polygon: { - nb_sides: 5 + particles: { + number: { + value: 400, + density: { + enable: true, + value_area: 800 + } + }, + color: { + value: '#fff' + }, + shape: { + type: 'circle', + stroke: { + width: 0, + color: '#ff0000' + }, + polygon: { + nb_sides: 5 + }, + image: { + src: '', + width: 100, + height: 100 + } + }, + opacity: { + value: 1, + random: false, + anim: { + enable: false, + speed: 2, + opacity_min: 0, + sync: false + } + }, + size: { + value: 20, + random: false, + anim: { + enable: false, + speed: 20, + size_min: 0, + sync: false + } + }, + line_linked: { + enable: true, + distance: 100, + color: '#fff', + opacity: 1, + width: 1 + }, + move: { + enable: true, + speed: 2, + direction: 'none', + random: false, + straight: false, + out_mode: 'out', + bounce: false, + attract: { + enable: false, + rotateX: 3000, + rotateY: 3000 + } + }, + array: [] }, - image: { - src: '', - width: 100, - height: 100 - } - }, - opacity: { - value: 1, - random: false, - anim: { - enable: false, - speed: 2, - opacity_min: 0, - sync: false - } - }, - size: { - value: 20, - random: false, - anim: { - enable: false, - speed: 20, - size_min: 0, - sync: false - } - }, - line_linked: { - enable: true, - distance: 100, - color: '#fff', - opacity: 1, - width: 1 - }, - move: { - enable: true, - speed: 2, - direction: 'none', - random: false, - straight: false, - out_mode: 'out', - bounce: false, - attract: { - enable: false, - rotateX: 3000, - rotateY: 3000 - } - }, - array: [] - }, - interactivity: { - detect_on: 'canvas', - events: { - onhover: { - enable: true, - mode: 'grab' + interactivity: { + detect_on: 'canvas', + events: { + onhover: { + enable: true, + mode: 'grab' + }, + onclick: { + enable: true, + mode: 'push' + }, + resize: true + }, + modes: { + grab: { + distance: 100, + line_linked: { + opacity: 1 + } + }, + bubble: { + distance: 200, + size: 80, + duration: 0.4 + }, + repulse: { + distance: 200, + duration: 0.4 + }, + push: { + particles_nb: 4 + }, + remove: { + particles_nb: 2 + } + }, + mouse: {} }, - onclick: { - enable: true, - mode: 'push' + retina_detect: false, + fn: { + interact: {}, + modes: {}, + vendors: {} }, - resize: true - }, - modes: { - grab:{ - distance: 100, - line_linked:{ - opacity: 1 - } - }, - bubble:{ - distance: 200, - size: 80, - duration: 0.4 - }, - repulse:{ - distance: 200, - duration: 0.4 - }, - push:{ - particles_nb: 4 - }, - remove:{ - particles_nb: 2 - } - }, - mouse:{} - }, - retina_detect: false, - fn: { - interact: {}, - modes: {}, - vendors:{} - }, - tmp: {} - }; + tmp: {} + }; - var pJS = this.pJS; + var pJS = this.pJS; - /* params settings */ - if(params){ - Object.deepExtend(pJS, params); - } - - pJS.tmp.obj = { - size_value: pJS.particles.size.value, - size_anim_speed: pJS.particles.size.anim.speed, - move_speed: pJS.particles.move.speed, - line_linked_distance: pJS.particles.line_linked.distance, - line_linked_width: pJS.particles.line_linked.width, - mode_grab_distance: pJS.interactivity.modes.grab.distance, - mode_bubble_distance: pJS.interactivity.modes.bubble.distance, - mode_bubble_size: pJS.interactivity.modes.bubble.size, - mode_repulse_distance: pJS.interactivity.modes.repulse.distance - }; - - - pJS.fn.retinaInit = function(){ - - if(pJS.retina_detect && window.devicePixelRatio > 1){ - pJS.canvas.pxratio = window.devicePixelRatio; - pJS.tmp.retina = true; - } - else{ - pJS.canvas.pxratio = 1; - pJS.tmp.retina = false; + /* params settings */ + if (params) { + Object.deepExtend(pJS, params); } - pJS.canvas.w = pJS.canvas.el.offsetWidth * pJS.canvas.pxratio; - pJS.canvas.h = pJS.canvas.el.offsetHeight * pJS.canvas.pxratio; - - pJS.particles.size.value = pJS.tmp.obj.size_value * pJS.canvas.pxratio; - pJS.particles.size.anim.speed = pJS.tmp.obj.size_anim_speed * pJS.canvas.pxratio; - pJS.particles.move.speed = pJS.tmp.obj.move_speed * pJS.canvas.pxratio; - pJS.particles.line_linked.distance = pJS.tmp.obj.line_linked_distance * pJS.canvas.pxratio; - pJS.interactivity.modes.grab.distance = pJS.tmp.obj.mode_grab_distance * pJS.canvas.pxratio; - pJS.interactivity.modes.bubble.distance = pJS.tmp.obj.mode_bubble_distance * pJS.canvas.pxratio; - pJS.particles.line_linked.width = pJS.tmp.obj.line_linked_width * pJS.canvas.pxratio; - pJS.interactivity.modes.bubble.size = pJS.tmp.obj.mode_bubble_size * pJS.canvas.pxratio; - pJS.interactivity.modes.repulse.distance = pJS.tmp.obj.mode_repulse_distance * pJS.canvas.pxratio; - - }; + pJS.tmp.obj = { + size_value: pJS.particles.size.value, + size_anim_speed: pJS.particles.size.anim.speed, + move_speed: pJS.particles.move.speed, + line_linked_distance: pJS.particles.line_linked.distance, + line_linked_width: pJS.particles.line_linked.width, + mode_grab_distance: pJS.interactivity.modes.grab.distance, + mode_bubble_distance: pJS.interactivity.modes.bubble.distance, + mode_bubble_size: pJS.interactivity.modes.bubble.size, + mode_repulse_distance: pJS.interactivity.modes.repulse.distance + }; + pJS.fn.retinaInit = function () { - /* ---------- pJS functions - canvas ------------ */ - - pJS.fn.canvasInit = function(){ - pJS.canvas.ctx = pJS.canvas.el.getContext('2d'); - }; - - pJS.fn.canvasSize = function(){ - - pJS.canvas.el.width = pJS.canvas.w; - pJS.canvas.el.height = pJS.canvas.h; - - if(pJS && pJS.interactivity.events.resize){ - - window.addEventListener('resize', function(){ - - pJS.canvas.w = pJS.canvas.el.offsetWidth; - pJS.canvas.h = pJS.canvas.el.offsetHeight; - - /* resize canvas */ - if(pJS.tmp.retina){ - pJS.canvas.w *= pJS.canvas.pxratio; - pJS.canvas.h *= pJS.canvas.pxratio; - } - - pJS.canvas.el.width = pJS.canvas.w; - pJS.canvas.el.height = pJS.canvas.h; - - /* repaint canvas on anim disabled */ - if(!pJS.particles.move.enable){ - pJS.fn.particlesEmpty(); - pJS.fn.particlesCreate(); - pJS.fn.particlesDraw(); - pJS.fn.vendors.densityAutoParticles(); - } - - /* density particles enabled */ - pJS.fn.vendors.densityAutoParticles(); - - }); - - } - - }; - - - pJS.fn.canvasPaint = function(){ - pJS.canvas.ctx.fillRect(0, 0, pJS.canvas.w, pJS.canvas.h); - }; - - pJS.fn.canvasClear = function(){ - pJS.canvas.ctx.clearRect(0, 0, pJS.canvas.w, pJS.canvas.h); - }; - - - /* --------- pJS functions - particles ----------- */ - - pJS.fn.particle = function(color, opacity, position){ - - /* size */ - this.radius = (pJS.particles.size.random ? Math.random() : 1) * pJS.particles.size.value; - if(pJS.particles.size.anim.enable){ - this.size_status = false; - this.vs = pJS.particles.size.anim.speed / 100; - if(!pJS.particles.size.anim.sync){ - this.vs = this.vs * Math.random(); - } - } - - /* position */ - this.x = position ? position.x : Math.random() * pJS.canvas.w; - this.y = position ? position.y : Math.random() * pJS.canvas.h; - - /* check position - into the canvas */ - if(this.x > pJS.canvas.w - this.radius*2) this.x = this.x - this.radius; - else if(this.x < this.radius*2) this.x = this.x + this.radius; - if(this.y > pJS.canvas.h - this.radius*2) this.y = this.y - this.radius; - else if(this.y < this.radius*2) this.y = this.y + this.radius; - - /* check position - avoid overlap */ - if(pJS.particles.move.bounce){ - pJS.fn.vendors.checkOverlap(this, position); - } - - /* color */ - this.color = {}; - if(typeof(color.value) == 'object'){ - - if(color.value instanceof Array){ - var color_selected = color.value[Math.floor(Math.random() * pJS.particles.color.value.length)]; - this.color.rgb = hexToRgb(color_selected); - }else{ - if(color.value.r != undefined && color.value.g != undefined && color.value.b != undefined){ - this.color.rgb = { - r: color.value.r, - g: color.value.g, - b: color.value.b - } - } - if(color.value.h != undefined && color.value.s != undefined && color.value.l != undefined){ - this.color.hsl = { - h: color.value.h, - s: color.value.s, - l: color.value.l - } - } - } - - } - else if(color.value == 'random'){ - this.color.rgb = { - r: (Math.floor(Math.random() * (255 - 0 + 1)) + 0), - g: (Math.floor(Math.random() * (255 - 0 + 1)) + 0), - b: (Math.floor(Math.random() * (255 - 0 + 1)) + 0) - } - } - else if(typeof(color.value) == 'string'){ - this.color = color; - this.color.rgb = hexToRgb(this.color.value); - } - - /* opacity */ - this.opacity = (pJS.particles.opacity.random ? Math.random() : 1) * pJS.particles.opacity.value; - if(pJS.particles.opacity.anim.enable){ - this.opacity_status = false; - this.vo = pJS.particles.opacity.anim.speed / 100; - if(!pJS.particles.opacity.anim.sync){ - this.vo = this.vo * Math.random(); - } - } - - /* animation - velocity for speed */ - var velbase = {} - switch(pJS.particles.move.direction){ - case 'top': - velbase = { x:0, y:-1 }; - break; - case 'top-right': - velbase = { x:0.5, y:-0.5 }; - break; - case 'right': - velbase = { x:1, y:-0 }; - break; - case 'bottom-right': - velbase = { x:0.5, y:0.5 }; - break; - case 'bottom': - velbase = { x:0, y:1 }; - break; - case 'bottom-left': - velbase = { x:-0.5, y:1 }; - break; - case 'left': - velbase = { x:-1, y:0 }; - break; - case 'top-left': - velbase = { x:-0.5, y:-0.5 }; - break; - default: - velbase = { x:0, y:0 }; - break; - } - - if(pJS.particles.move.straight){ - this.vx = velbase.x; - this.vy = velbase.y; - if(pJS.particles.move.random){ - this.vx = this.vx * (Math.random()); - this.vy = this.vy * (Math.random()); - } - }else{ - this.vx = velbase.x + Math.random()-0.5; - this.vy = velbase.y + Math.random()-0.5; - } - - // var theta = 2.0 * Math.PI * Math.random(); - // this.vx = Math.cos(theta); - // this.vy = Math.sin(theta); - - this.vx_i = this.vx; - this.vy_i = this.vy; - - - - /* if shape is image */ - - var shape_type = pJS.particles.shape.type; - if(typeof(shape_type) == 'object'){ - if(shape_type instanceof Array){ - var shape_selected = shape_type[Math.floor(Math.random() * shape_type.length)]; - this.shape = shape_selected; - } - }else{ - this.shape = shape_type; - } - - if(this.shape == 'image'){ - var sh = pJS.particles.shape; - this.img = { - src: sh.image.src, - ratio: sh.image.width / sh.image.height - } - if(!this.img.ratio) this.img.ratio = 1; - if(pJS.tmp.img_type == 'svg' && pJS.tmp.source_svg != undefined){ - pJS.fn.vendors.createSvgImg(this); - if(pJS.tmp.pushing){ - this.img.loaded = false; - } - } - } - - - - }; - - - pJS.fn.particle.prototype.draw = function() { - - var p = this; - - if(p.radius_bubble != undefined){ - var radius = p.radius_bubble; - }else{ - var radius = p.radius; - } - - if(p.opacity_bubble != undefined){ - var opacity = p.opacity_bubble; - }else{ - var opacity = p.opacity; - } - - if(p.color.rgb){ - var color_value = 'rgba('+p.color.rgb.r+','+p.color.rgb.g+','+p.color.rgb.b+','+opacity+')'; - }else{ - var color_value = 'hsla('+p.color.hsl.h+','+p.color.hsl.s+'%,'+p.color.hsl.l+'%,'+opacity+')'; - } - - pJS.canvas.ctx.fillStyle = color_value; - pJS.canvas.ctx.beginPath(); - - switch(p.shape){ - - case 'circle': - pJS.canvas.ctx.arc(p.x, p.y, radius, 0, Math.PI * 2, false); - break; - - case 'edge': - pJS.canvas.ctx.rect(p.x-radius, p.y-radius, radius*2, radius*2); - break; - - case 'triangle': - pJS.fn.vendors.drawShape(pJS.canvas.ctx, p.x-radius, p.y+radius / 1.66, radius*2, 3, 2); - break; - - case 'polygon': - pJS.fn.vendors.drawShape( - pJS.canvas.ctx, - p.x - radius / (pJS.particles.shape.polygon.nb_sides/3.5), // startX - p.y - radius / (2.66/3.5), // startY - radius*2.66 / (pJS.particles.shape.polygon.nb_sides/3), // sideLength - pJS.particles.shape.polygon.nb_sides, // sideCountNumerator - 1 // sideCountDenominator - ); - break; - - case 'star': - pJS.fn.vendors.drawShape( - pJS.canvas.ctx, - p.x - radius*2 / (pJS.particles.shape.polygon.nb_sides/4), // startX - p.y - radius / (2*2.66/3.5), // startY - radius*2*2.66 / (pJS.particles.shape.polygon.nb_sides/3), // sideLength - pJS.particles.shape.polygon.nb_sides, // sideCountNumerator - 2 // sideCountDenominator - ); - break; - - case 'image': - - function draw(){ - pJS.canvas.ctx.drawImage( - img_obj, - p.x-radius, - p.y-radius, - radius*2, - radius*2 / p.img.ratio - ); + if (pJS.retina_detect && window.devicePixelRatio > 1) { + pJS.canvas.pxratio = window.devicePixelRatio; + pJS.tmp.retina = true; + } else { + pJS.canvas.pxratio = 1; + pJS.tmp.retina = false; } - if(pJS.tmp.img_type == 'svg'){ - var img_obj = p.img.obj; - }else{ - var img_obj = pJS.tmp.img_obj; - } + pJS.canvas.w = pJS.canvas.el.offsetWidth * pJS.canvas.pxratio; + pJS.canvas.h = pJS.canvas.el.offsetHeight * pJS.canvas.pxratio; - if(img_obj){ - draw(); - } + pJS.particles.size.value = pJS.tmp.obj.size_value * pJS.canvas.pxratio; + pJS.particles.size.anim.speed = pJS.tmp.obj.size_anim_speed * pJS.canvas.pxratio; + pJS.particles.move.speed = pJS.tmp.obj.move_speed * pJS.canvas.pxratio; + pJS.particles.line_linked.distance = pJS.tmp.obj.line_linked_distance * pJS.canvas.pxratio; + pJS.interactivity.modes.grab.distance = pJS.tmp.obj.mode_grab_distance * pJS.canvas.pxratio; + pJS.interactivity.modes.bubble.distance = pJS.tmp.obj.mode_bubble_distance * pJS.canvas.pxratio; + pJS.particles.line_linked.width = pJS.tmp.obj.line_linked_width * pJS.canvas.pxratio; + pJS.interactivity.modes.bubble.size = pJS.tmp.obj.mode_bubble_size * pJS.canvas.pxratio; + pJS.interactivity.modes.repulse.distance = pJS.tmp.obj.mode_repulse_distance * pJS.canvas.pxratio; - break; - - } - - pJS.canvas.ctx.closePath(); - - if(pJS.particles.shape.stroke.width > 0){ - pJS.canvas.ctx.strokeStyle = pJS.particles.shape.stroke.color; - pJS.canvas.ctx.lineWidth = pJS.particles.shape.stroke.width; - pJS.canvas.ctx.stroke(); - } - - pJS.canvas.ctx.fill(); - - }; + }; - pJS.fn.particlesCreate = function(){ - for(var i = 0; i < pJS.particles.number.value; i++) { - pJS.particles.array.push(new pJS.fn.particle(pJS.particles.color, pJS.particles.opacity.value)); - } - }; + /* ---------- pJS functions - canvas ------------ */ - pJS.fn.particlesUpdate = function(){ + pJS.fn.canvasInit = function () { + pJS.canvas.ctx = pJS.canvas.el.getContext('2d'); + }; - for(var i = 0; i < pJS.particles.array.length; i++){ + pJS.fn.canvasSize = function () { - /* the particle */ - var p = pJS.particles.array[i]; + pJS.canvas.el.width = pJS.canvas.w; + pJS.canvas.el.height = pJS.canvas.h; - // var d = ( dx = pJS.interactivity.mouse.click_pos_x - p.x ) * dx + ( dy = pJS.interactivity.mouse.click_pos_y - p.y ) * dy; - // var f = -BANG_SIZE / d; - // if ( d < BANG_SIZE ) { - // var t = Math.atan2( dy, dx ); - // p.vx = f * Math.cos(t); - // p.vy = f * Math.sin(t); - // } + if (pJS && pJS.interactivity.events.resize) { - /* move the particle */ - if(pJS.particles.move.enable){ - var ms = pJS.particles.move.speed/2; - p.x += p.vx * ms; - p.y += p.vy * ms; - } + window.addEventListener('resize', function () { - /* change opacity status */ - if(pJS.particles.opacity.anim.enable) { - if(p.opacity_status == true) { - if(p.opacity >= pJS.particles.opacity.value) p.opacity_status = false; - p.opacity += p.vo; - }else { - if(p.opacity <= pJS.particles.opacity.anim.opacity_min) p.opacity_status = true; - p.opacity -= p.vo; - } - if(p.opacity < 0) p.opacity = 0; - } + pJS.canvas.w = pJS.canvas.el.offsetWidth; + pJS.canvas.h = pJS.canvas.el.offsetHeight; - /* change size */ - if(pJS.particles.size.anim.enable){ - if(p.size_status == true){ - if(p.radius >= pJS.particles.size.value) p.size_status = false; - p.radius += p.vs; - }else{ - if(p.radius <= pJS.particles.size.anim.size_min) p.size_status = true; - p.radius -= p.vs; - } - if(p.radius < 0) p.radius = 0; - } + /* resize canvas */ + if (pJS.tmp.retina) { + pJS.canvas.w *= pJS.canvas.pxratio; + pJS.canvas.h *= pJS.canvas.pxratio; + } - /* change particle position if it is out of canvas */ - if(pJS.particles.move.out_mode == 'bounce'){ - var new_pos = { - x_left: p.radius, - x_right: pJS.canvas.w, - y_top: p.radius, - y_bottom: pJS.canvas.h - } - }else{ - var new_pos = { - x_left: -p.radius, - x_right: pJS.canvas.w + p.radius, - y_top: -p.radius, - y_bottom: pJS.canvas.h + p.radius - } - } + pJS.canvas.el.width = pJS.canvas.w; + pJS.canvas.el.height = pJS.canvas.h; - if(p.x - p.radius > pJS.canvas.w){ - p.x = new_pos.x_left; - p.y = Math.random() * pJS.canvas.h; - } - else if(p.x + p.radius < 0){ - p.x = new_pos.x_right; - p.y = Math.random() * pJS.canvas.h; - } - if(p.y - p.radius > pJS.canvas.h){ - p.y = new_pos.y_top; - p.x = Math.random() * pJS.canvas.w; - } - else if(p.y + p.radius < 0){ - p.y = new_pos.y_bottom; - p.x = Math.random() * pJS.canvas.w; - } + /* repaint canvas on anim disabled */ + if (!pJS.particles.move.enable) { + pJS.fn.particlesEmpty(); + pJS.fn.particlesCreate(); + pJS.fn.particlesDraw(); + pJS.fn.vendors.densityAutoParticles(); + } - /* out of canvas modes */ - switch(pJS.particles.move.out_mode){ - case 'bounce': - if (p.x + p.radius > pJS.canvas.w) p.vx = -p.vx; - else if (p.x - p.radius < 0) p.vx = -p.vx; - if (p.y + p.radius > pJS.canvas.h) p.vy = -p.vy; - else if (p.y - p.radius < 0) p.vy = -p.vy; - break; - } + /* density particles enabled */ + pJS.fn.vendors.densityAutoParticles(); - /* events */ - if(isInArray('grab', pJS.interactivity.events.onhover.mode)){ - pJS.fn.modes.grabParticle(p); - } - - if(isInArray('bubble', pJS.interactivity.events.onhover.mode) || isInArray('bubble', pJS.interactivity.events.onclick.mode)){ - pJS.fn.modes.bubbleParticle(p); - } - - if(isInArray('repulse', pJS.interactivity.events.onhover.mode) || isInArray('repulse', pJS.interactivity.events.onclick.mode)){ - pJS.fn.modes.repulseParticle(p); - } - - /* interaction auto between particles */ - if(pJS.particles.line_linked.enable || pJS.particles.move.attract.enable){ - for(var j = i + 1; j < pJS.particles.array.length; j++){ - var p2 = pJS.particles.array[j]; - - /* link particles */ - if(pJS.particles.line_linked.enable){ - pJS.fn.interact.linkParticles(p,p2); - } - - /* attract particles */ - if(pJS.particles.move.attract.enable){ - pJS.fn.interact.attractParticles(p,p2); - } - - /* bounce particles */ - if(pJS.particles.move.bounce){ - pJS.fn.interact.bounceParticles(p,p2); - } + }); } - } + + }; - } + pJS.fn.canvasPaint = function () { + pJS.canvas.ctx.fillRect(0, 0, pJS.canvas.w, pJS.canvas.h); + }; - }; - - pJS.fn.particlesDraw = function(){ - - /* clear canvas */ - pJS.canvas.ctx.clearRect(0, 0, pJS.canvas.w, pJS.canvas.h); - - /* update each particles param */ - pJS.fn.particlesUpdate(); - - /* draw each particle */ - for(var i = 0; i < pJS.particles.array.length; i++){ - var p = pJS.particles.array[i]; - p.draw(); - } - - }; - - pJS.fn.particlesEmpty = function(){ - pJS.particles.array = []; - }; - - pJS.fn.particlesRefresh = function(){ - - /* init all */ - cancelRequestAnimFrame(pJS.fn.checkAnimFrame); - cancelRequestAnimFrame(pJS.fn.drawAnimFrame); - pJS.tmp.source_svg = undefined; - pJS.tmp.img_obj = undefined; - pJS.tmp.count_svg = 0; - pJS.fn.particlesEmpty(); - pJS.fn.canvasClear(); - - /* restart */ - pJS.fn.vendors.start(); - - }; + pJS.fn.canvasClear = function () { + pJS.canvas.ctx.clearRect(0, 0, pJS.canvas.w, pJS.canvas.h); + }; - /* ---------- pJS functions - particles interaction ------------ */ + /* --------- pJS functions - particles ----------- */ - pJS.fn.interact.linkParticles = function(p1, p2){ + pJS.fn.particle = function (color, opacity, position) { - var dx = p1.x - p2.x, - dy = p1.y - p2.y, - dist = Math.sqrt(dx*dx + dy*dy); + /* size */ + this.radius = (pJS.particles.size.random ? Math.random() : 1) * pJS.particles.size.value; + if (pJS.particles.size.anim.enable) { + this.size_status = false; + this.vs = pJS.particles.size.anim.speed / 100; + if (!pJS.particles.size.anim.sync) { + this.vs = this.vs * Math.random(); + } + } - /* draw a line between p1 and p2 if the distance between them is under the config distance */ - if(dist <= pJS.particles.line_linked.distance){ + /* position */ + this.x = position ? position.x : Math.random() * pJS.canvas.w; + this.y = position ? position.y : Math.random() * pJS.canvas.h; - var opacity_line = pJS.particles.line_linked.opacity - (dist / (1/pJS.particles.line_linked.opacity)) / pJS.particles.line_linked.distance; + /* check position - into the canvas */ + if (this.x > pJS.canvas.w - this.radius * 2) this.x = this.x - this.radius; + else if (this.x < this.radius * 2) this.x = this.x + this.radius; + if (this.y > pJS.canvas.h - this.radius * 2) this.y = this.y - this.radius; + else if (this.y < this.radius * 2) this.y = this.y + this.radius; - if(opacity_line > 0){ - - /* style */ - var color_line = pJS.particles.line_linked.color_rgb_line; - pJS.canvas.ctx.strokeStyle = 'rgba('+color_line.r+','+color_line.g+','+color_line.b+','+opacity_line+')'; - pJS.canvas.ctx.lineWidth = pJS.particles.line_linked.width; - //pJS.canvas.ctx.lineCap = 'round'; /* performance issue */ - - /* path */ + /* check position - avoid overlap */ + if (pJS.particles.move.bounce) { + pJS.fn.vendors.checkOverlap(this, position); + } + + /* color */ + this.color = {}; + if (typeof (color.value) == 'object') { + + if (color.value instanceof Array) { + var color_selected = color.value[Math.floor(Math.random() * pJS.particles.color.value.length)]; + this.color.rgb = hexToRgb(color_selected); + } else { + if (color.value.r != undefined && color.value.g != undefined && color.value.b != undefined) { + this.color.rgb = { + r: color.value.r, + g: color.value.g, + b: color.value.b + } + } + if (color.value.h != undefined && color.value.s != undefined && color.value.l != undefined) { + this.color.hsl = { + h: color.value.h, + s: color.value.s, + l: color.value.l + } + } + } + + } else if (color.value == 'random') { + this.color.rgb = { + r: (Math.floor(Math.random() * (255 - 0 + 1)) + 0), + g: (Math.floor(Math.random() * (255 - 0 + 1)) + 0), + b: (Math.floor(Math.random() * (255 - 0 + 1)) + 0) + } + } else if (typeof (color.value) == 'string') { + this.color = color; + this.color.rgb = hexToRgb(this.color.value); + } + + /* opacity */ + this.opacity = (pJS.particles.opacity.random ? Math.random() : 1) * pJS.particles.opacity.value; + if (pJS.particles.opacity.anim.enable) { + this.opacity_status = false; + this.vo = pJS.particles.opacity.anim.speed / 100; + if (!pJS.particles.opacity.anim.sync) { + this.vo = this.vo * Math.random(); + } + } + + /* animation - velocity for speed */ + var velbase = {} + switch (pJS.particles.move.direction) { + case 'top': + velbase = {x: 0, y: -1}; + break; + case 'top-right': + velbase = {x: 0.5, y: -0.5}; + break; + case 'right': + velbase = {x: 1, y: -0}; + break; + case 'bottom-right': + velbase = {x: 0.5, y: 0.5}; + break; + case 'bottom': + velbase = {x: 0, y: 1}; + break; + case 'bottom-left': + velbase = {x: -0.5, y: 1}; + break; + case 'left': + velbase = {x: -1, y: 0}; + break; + case 'top-left': + velbase = {x: -0.5, y: -0.5}; + break; + default: + velbase = {x: 0, y: 0}; + break; + } + + if (pJS.particles.move.straight) { + this.vx = velbase.x; + this.vy = velbase.y; + if (pJS.particles.move.random) { + this.vx = this.vx * (Math.random()); + this.vy = this.vy * (Math.random()); + } + } else { + this.vx = velbase.x + Math.random() - 0.5; + this.vy = velbase.y + Math.random() - 0.5; + } + + // var theta = 2.0 * Math.PI * Math.random(); + // this.vx = Math.cos(theta); + // this.vy = Math.sin(theta); + + this.vx_i = this.vx; + this.vy_i = this.vy; + + + /* if shape is image */ + + var shape_type = pJS.particles.shape.type; + if (typeof (shape_type) == 'object') { + if (shape_type instanceof Array) { + var shape_selected = shape_type[Math.floor(Math.random() * shape_type.length)]; + this.shape = shape_selected; + } + } else { + this.shape = shape_type; + } + + if (this.shape == 'image') { + var sh = pJS.particles.shape; + this.img = { + src: sh.image.src, + ratio: sh.image.width / sh.image.height + } + if (!this.img.ratio) this.img.ratio = 1; + if (pJS.tmp.img_type == 'svg' && pJS.tmp.source_svg != undefined) { + pJS.fn.vendors.createSvgImg(this); + if (pJS.tmp.pushing) { + this.img.loaded = false; + } + } + } + + + }; + + + pJS.fn.particle.prototype.draw = function () { + + var p = this; + + if (p.radius_bubble != undefined) { + var radius = p.radius_bubble; + } else { + var radius = p.radius; + } + + if (p.opacity_bubble != undefined) { + var opacity = p.opacity_bubble; + } else { + var opacity = p.opacity; + } + + if (p.color.rgb) { + var color_value = 'rgba(' + p.color.rgb.r + ',' + p.color.rgb.g + ',' + p.color.rgb.b + ',' + opacity + ')'; + } else { + var color_value = 'hsla(' + p.color.hsl.h + ',' + p.color.hsl.s + '%,' + p.color.hsl.l + '%,' + opacity + ')'; + } + + pJS.canvas.ctx.fillStyle = color_value; pJS.canvas.ctx.beginPath(); - pJS.canvas.ctx.moveTo(p1.x, p1.y); - pJS.canvas.ctx.lineTo(p2.x, p2.y); - pJS.canvas.ctx.stroke(); + + switch (p.shape) { + + case 'circle': + pJS.canvas.ctx.arc(p.x, p.y, radius, 0, Math.PI * 2, false); + break; + + case 'edge': + pJS.canvas.ctx.rect(p.x - radius, p.y - radius, radius * 2, radius * 2); + break; + + case 'triangle': + pJS.fn.vendors.drawShape(pJS.canvas.ctx, p.x - radius, p.y + radius / 1.66, radius * 2, 3, 2); + break; + + case 'polygon': + pJS.fn.vendors.drawShape( + pJS.canvas.ctx, + p.x - radius / (pJS.particles.shape.polygon.nb_sides / 3.5), // startX + p.y - radius / (2.66 / 3.5), // startY + radius * 2.66 / (pJS.particles.shape.polygon.nb_sides / 3), // sideLength + pJS.particles.shape.polygon.nb_sides, // sideCountNumerator + 1 // sideCountDenominator + ); + break; + + case 'star': + pJS.fn.vendors.drawShape( + pJS.canvas.ctx, + p.x - radius * 2 / (pJS.particles.shape.polygon.nb_sides / 4), // startX + p.y - radius / (2 * 2.66 / 3.5), // startY + radius * 2 * 2.66 / (pJS.particles.shape.polygon.nb_sides / 3), // sideLength + pJS.particles.shape.polygon.nb_sides, // sideCountNumerator + 2 // sideCountDenominator + ); + break; + + case 'image': + + function draw() { + pJS.canvas.ctx.drawImage( + img_obj, + p.x - radius, + p.y - radius, + radius * 2, + radius * 2 / p.img.ratio + ); + } + + if (pJS.tmp.img_type == 'svg') { + var img_obj = p.img.obj; + } else { + var img_obj = pJS.tmp.img_obj; + } + + if (img_obj) { + draw(); + } + + break; + + } + pJS.canvas.ctx.closePath(); - } - - } - - }; - - - pJS.fn.interact.attractParticles = function(p1, p2){ - - /* condensed particles */ - var dx = p1.x - p2.x, - dy = p1.y - p2.y, - dist = Math.sqrt(dx*dx + dy*dy); - - if(dist <= pJS.particles.line_linked.distance){ - - var ax = dx/(pJS.particles.move.attract.rotateX*1000), - ay = dy/(pJS.particles.move.attract.rotateY*1000); - - p1.vx -= ax; - p1.vy -= ay; - - p2.vx += ax; - p2.vy += ay; - - } - - - } - - - pJS.fn.interact.bounceParticles = function(p1, p2){ - - var dx = p1.x - p2.x, - dy = p1.y - p2.y, - dist = Math.sqrt(dx*dx + dy*dy), - dist_p = p1.radius+p2.radius; - - if(dist <= dist_p){ - p1.vx = -p1.vx; - p1.vy = -p1.vy; - - p2.vx = -p2.vx; - p2.vy = -p2.vy; - } - - } - - - /* ---------- pJS functions - modes events ------------ */ - - pJS.fn.modes.pushParticles = function(nb, pos){ - - pJS.tmp.pushing = true; - - for(var i = 0; i < nb; i++){ - pJS.particles.array.push( - new pJS.fn.particle( - pJS.particles.color, - pJS.particles.opacity.value, - { - 'x': pos ? pos.pos_x : Math.random() * pJS.canvas.w, - 'y': pos ? pos.pos_y : Math.random() * pJS.canvas.h - } - ) - ) - if(i == nb-1){ - if(!pJS.particles.move.enable){ - pJS.fn.particlesDraw(); + if (pJS.particles.shape.stroke.width > 0) { + pJS.canvas.ctx.strokeStyle = pJS.particles.shape.stroke.color; + pJS.canvas.ctx.lineWidth = pJS.particles.shape.stroke.width; + pJS.canvas.ctx.stroke(); } - pJS.tmp.pushing = false; - } - } - }; + pJS.canvas.ctx.fill(); + + }; - pJS.fn.modes.removeParticles = function(nb){ + pJS.fn.particlesCreate = function () { + for (var i = 0; i < pJS.particles.number.value; i++) { + pJS.particles.array.push(new pJS.fn.particle(pJS.particles.color, pJS.particles.opacity.value)); + } + }; - pJS.particles.array.splice(0, nb); - if(!pJS.particles.move.enable){ - pJS.fn.particlesDraw(); - } + pJS.fn.particlesUpdate = function () { - }; + for (var i = 0; i < pJS.particles.array.length; i++) { + /* the particle */ + var p = pJS.particles.array[i]; - pJS.fn.modes.bubbleParticle = function(p){ + // var d = ( dx = pJS.interactivity.mouse.click_pos_x - p.x ) * dx + ( dy = pJS.interactivity.mouse.click_pos_y - p.y ) * dy; + // var f = -BANG_SIZE / d; + // if ( d < BANG_SIZE ) { + // var t = Math.atan2( dy, dx ); + // p.vx = f * Math.cos(t); + // p.vy = f * Math.sin(t); + // } - /* on hover event */ - if(pJS.interactivity.events.onhover.enable && isInArray('bubble', pJS.interactivity.events.onhover.mode)){ - - var dx_mouse = p.x - pJS.interactivity.mouse.pos_x, - dy_mouse = p.y - pJS.interactivity.mouse.pos_y, - dist_mouse = Math.sqrt(dx_mouse*dx_mouse + dy_mouse*dy_mouse), - ratio = 1 - dist_mouse / pJS.interactivity.modes.bubble.distance; - - function init(){ - p.opacity_bubble = p.opacity; - p.radius_bubble = p.radius; - } - - /* mousemove - check ratio */ - if(dist_mouse <= pJS.interactivity.modes.bubble.distance){ - - if(ratio >= 0 && pJS.interactivity.status == 'mousemove'){ - - /* size */ - if(pJS.interactivity.modes.bubble.size != pJS.particles.size.value){ - - if(pJS.interactivity.modes.bubble.size > pJS.particles.size.value){ - var size = p.radius + (pJS.interactivity.modes.bubble.size*ratio); - if(size >= 0){ - p.radius_bubble = size; - } - }else{ - var dif = p.radius - pJS.interactivity.modes.bubble.size, - size = p.radius - (dif*ratio); - if(size > 0){ - p.radius_bubble = size; - }else{ - p.radius_bubble = 0; - } + /* move the particle */ + if (pJS.particles.move.enable) { + var ms = pJS.particles.move.speed / 2; + p.x += p.vx * ms; + p.y += p.vy * ms; } - } - - /* opacity */ - if(pJS.interactivity.modes.bubble.opacity != pJS.particles.opacity.value){ - - if(pJS.interactivity.modes.bubble.opacity > pJS.particles.opacity.value){ - var opacity = pJS.interactivity.modes.bubble.opacity*ratio; - if(opacity > p.opacity && opacity <= pJS.interactivity.modes.bubble.opacity){ - p.opacity_bubble = opacity; - } - }else{ - var opacity = p.opacity - (pJS.particles.opacity.value-pJS.interactivity.modes.bubble.opacity)*ratio; - if(opacity < p.opacity && opacity >= pJS.interactivity.modes.bubble.opacity){ - p.opacity_bubble = opacity; - } + /* change opacity status */ + if (pJS.particles.opacity.anim.enable) { + if (p.opacity_status == true) { + if (p.opacity >= pJS.particles.opacity.value) p.opacity_status = false; + p.opacity += p.vo; + } else { + if (p.opacity <= pJS.particles.opacity.anim.opacity_min) p.opacity_status = true; + p.opacity -= p.vo; + } + if (p.opacity < 0) p.opacity = 0; } - } - - } - - }else{ - init(); - } - - - /* mouseleave */ - if(pJS.interactivity.status == 'mouseleave'){ - init(); - } - - } - - /* on click event */ - else if(pJS.interactivity.events.onclick.enable && isInArray('bubble', pJS.interactivity.events.onclick.mode)){ - - - if(pJS.tmp.bubble_clicking){ - var dx_mouse = p.x - pJS.interactivity.mouse.click_pos_x, - dy_mouse = p.y - pJS.interactivity.mouse.click_pos_y, - dist_mouse = Math.sqrt(dx_mouse*dx_mouse + dy_mouse*dy_mouse), - time_spent = (new Date().getTime() - pJS.interactivity.mouse.click_time)/1000; - - if(time_spent > pJS.interactivity.modes.bubble.duration){ - pJS.tmp.bubble_duration_end = true; - } - - if(time_spent > pJS.interactivity.modes.bubble.duration*2){ - pJS.tmp.bubble_clicking = false; - pJS.tmp.bubble_duration_end = false; - } - } - - - function process(bubble_param, particles_param, p_obj_bubble, p_obj, id){ - - if(bubble_param != particles_param){ - - if(!pJS.tmp.bubble_duration_end){ - if(dist_mouse <= pJS.interactivity.modes.bubble.distance){ - if(p_obj_bubble != undefined) var obj = p_obj_bubble; - else var obj = p_obj; - if(obj != bubble_param){ - var value = p_obj - (time_spent * (p_obj - bubble_param) / pJS.interactivity.modes.bubble.duration); - if(id == 'size') p.radius_bubble = value; - if(id == 'opacity') p.opacity_bubble = value; - } - }else{ - if(id == 'size') p.radius_bubble = undefined; - if(id == 'opacity') p.opacity_bubble = undefined; + /* change size */ + if (pJS.particles.size.anim.enable) { + if (p.size_status == true) { + if (p.radius >= pJS.particles.size.value) p.size_status = false; + p.radius += p.vs; + } else { + if (p.radius <= pJS.particles.size.anim.size_min) p.size_status = true; + p.radius -= p.vs; + } + if (p.radius < 0) p.radius = 0; } - }else{ - if(p_obj_bubble != undefined){ - var value_tmp = p_obj - (time_spent * (p_obj - bubble_param) / pJS.interactivity.modes.bubble.duration), - dif = bubble_param - value_tmp; - value = bubble_param + dif; - if(id == 'size') p.radius_bubble = value; - if(id == 'opacity') p.opacity_bubble = value; + + /* change particle position if it is out of canvas */ + if (pJS.particles.move.out_mode == 'bounce') { + var new_pos = { + x_left: p.radius, + x_right: pJS.canvas.w, + y_top: p.radius, + y_bottom: pJS.canvas.h + } + } else { + var new_pos = { + x_left: -p.radius, + x_right: pJS.canvas.w + p.radius, + y_top: -p.radius, + y_bottom: pJS.canvas.h + p.radius + } } - } + + if (p.x - p.radius > pJS.canvas.w) { + p.x = new_pos.x_left; + p.y = Math.random() * pJS.canvas.h; + } else if (p.x + p.radius < 0) { + p.x = new_pos.x_right; + p.y = Math.random() * pJS.canvas.h; + } + if (p.y - p.radius > pJS.canvas.h) { + p.y = new_pos.y_top; + p.x = Math.random() * pJS.canvas.w; + } else if (p.y + p.radius < 0) { + p.y = new_pos.y_bottom; + p.x = Math.random() * pJS.canvas.w; + } + + /* out of canvas modes */ + switch (pJS.particles.move.out_mode) { + case 'bounce': + if (p.x + p.radius > pJS.canvas.w) p.vx = -p.vx; + else if (p.x - p.radius < 0) p.vx = -p.vx; + if (p.y + p.radius > pJS.canvas.h) p.vy = -p.vy; + else if (p.y - p.radius < 0) p.vy = -p.vy; + break; + } + + /* events */ + if (isInArray('grab', pJS.interactivity.events.onhover.mode)) { + pJS.fn.modes.grabParticle(p); + } + + if (isInArray('bubble', pJS.interactivity.events.onhover.mode) || isInArray('bubble', pJS.interactivity.events.onclick.mode)) { + pJS.fn.modes.bubbleParticle(p); + } + + if (isInArray('repulse', pJS.interactivity.events.onhover.mode) || isInArray('repulse', pJS.interactivity.events.onclick.mode)) { + pJS.fn.modes.repulseParticle(p); + } + + /* interaction auto between particles */ + if (pJS.particles.line_linked.enable || pJS.particles.move.attract.enable) { + for (var j = i + 1; j < pJS.particles.array.length; j++) { + var p2 = pJS.particles.array[j]; + + /* link particles */ + if (pJS.particles.line_linked.enable) { + pJS.fn.interact.linkParticles(p, p2); + } + + /* attract particles */ + if (pJS.particles.move.attract.enable) { + pJS.fn.interact.attractParticles(p, p2); + } + + /* bounce particles */ + if (pJS.particles.move.bounce) { + pJS.fn.interact.bounceParticles(p, p2); + } + + } + } + } - } + }; + + pJS.fn.particlesDraw = function () { + + /* clear canvas */ + pJS.canvas.ctx.clearRect(0, 0, pJS.canvas.w, pJS.canvas.h); + + /* update each particles param */ + pJS.fn.particlesUpdate(); + + /* draw each particle */ + for (var i = 0; i < pJS.particles.array.length; i++) { + var p = pJS.particles.array[i]; + p.draw(); + } + + }; + + pJS.fn.particlesEmpty = function () { + pJS.particles.array = []; + }; + + pJS.fn.particlesRefresh = function () { + + /* init all */ + cancelRequestAnimFrame(pJS.fn.checkAnimFrame); + cancelRequestAnimFrame(pJS.fn.drawAnimFrame); + pJS.tmp.source_svg = undefined; + pJS.tmp.img_obj = undefined; + pJS.tmp.count_svg = 0; + pJS.fn.particlesEmpty(); + pJS.fn.canvasClear(); + + /* restart */ + pJS.fn.vendors.start(); + + }; + + + /* ---------- pJS functions - particles interaction ------------ */ + + pJS.fn.interact.linkParticles = function (p1, p2) { + + var dx = p1.x - p2.x, + dy = p1.y - p2.y, + dist = Math.sqrt(dx * dx + dy * dy); + + /* draw a line between p1 and p2 if the distance between them is under the config distance */ + if (dist <= pJS.particles.line_linked.distance) { + + var opacity_line = pJS.particles.line_linked.opacity - (dist / (1 / pJS.particles.line_linked.opacity)) / pJS.particles.line_linked.distance; + + if (opacity_line > 0) { + + /* style */ + var color_line = pJS.particles.line_linked.color_rgb_line; + pJS.canvas.ctx.strokeStyle = 'rgba(' + color_line.r + ',' + color_line.g + ',' + color_line.b + ',' + opacity_line + ')'; + pJS.canvas.ctx.lineWidth = pJS.particles.line_linked.width; + //pJS.canvas.ctx.lineCap = 'round'; /* performance issue */ + + /* path */ + pJS.canvas.ctx.beginPath(); + pJS.canvas.ctx.moveTo(p1.x, p1.y); + pJS.canvas.ctx.lineTo(p2.x, p2.y); + pJS.canvas.ctx.stroke(); + pJS.canvas.ctx.closePath(); + + } + + } + + }; + + + pJS.fn.interact.attractParticles = function (p1, p2) { + + /* condensed particles */ + var dx = p1.x - p2.x, + dy = p1.y - p2.y, + dist = Math.sqrt(dx * dx + dy * dy); + + if (dist <= pJS.particles.line_linked.distance) { + + var ax = dx / (pJS.particles.move.attract.rotateX * 1000), + ay = dy / (pJS.particles.move.attract.rotateY * 1000); + + p1.vx -= ax; + p1.vy -= ay; + + p2.vx += ax; + p2.vy += ay; + + } - if(pJS.tmp.bubble_clicking){ - /* size */ - process(pJS.interactivity.modes.bubble.size, pJS.particles.size.value, p.radius_bubble, p.radius, 'size'); - /* opacity */ - process(pJS.interactivity.modes.bubble.opacity, pJS.particles.opacity.value, p.opacity_bubble, p.opacity, 'opacity'); - } } - }; + pJS.fn.interact.bounceParticles = function (p1, p2) { - pJS.fn.modes.repulseParticle = function(p){ + var dx = p1.x - p2.x, + dy = p1.y - p2.y, + dist = Math.sqrt(dx * dx + dy * dy), + dist_p = p1.radius + p2.radius; - if(pJS.interactivity.events.onhover.enable && isInArray('repulse', pJS.interactivity.events.onhover.mode) && pJS.interactivity.status == 'mousemove') { + if (dist <= dist_p) { + p1.vx = -p1.vx; + p1.vy = -p1.vy; - var dx_mouse = p.x - pJS.interactivity.mouse.pos_x, - dy_mouse = p.y - pJS.interactivity.mouse.pos_y, - dist_mouse = Math.sqrt(dx_mouse*dx_mouse + dy_mouse*dy_mouse); + p2.vx = -p2.vx; + p2.vy = -p2.vy; + } - var normVec = {x: dx_mouse/dist_mouse, y: dy_mouse/dist_mouse}, - repulseRadius = pJS.interactivity.modes.repulse.distance, - velocity = 100, - repulseFactor = clamp((1/repulseRadius)*(-1*Math.pow(dist_mouse/repulseRadius,2)+1)*repulseRadius*velocity, 0, 50); - - var pos = { - x: p.x + normVec.x * repulseFactor, - y: p.y + normVec.y * repulseFactor - } - - if(pJS.particles.move.out_mode == 'bounce'){ - if(pos.x - p.radius > 0 && pos.x + p.radius < pJS.canvas.w) p.x = pos.x; - if(pos.y - p.radius > 0 && pos.y + p.radius < pJS.canvas.h) p.y = pos.y; - }else{ - p.x = pos.x; - p.y = pos.y; - } - } - else if(pJS.interactivity.events.onclick.enable && isInArray('repulse', pJS.interactivity.events.onclick.mode)) { + /* ---------- pJS functions - modes events ------------ */ - if(!pJS.tmp.repulse_finish){ - pJS.tmp.repulse_count++; - if(pJS.tmp.repulse_count == pJS.particles.array.length){ - pJS.tmp.repulse_finish = true; + pJS.fn.modes.pushParticles = function (nb, pos) { + + pJS.tmp.pushing = true; + + for (var i = 0; i < nb; i++) { + pJS.particles.array.push( + new pJS.fn.particle( + pJS.particles.color, + pJS.particles.opacity.value, + { + 'x': pos ? pos.pos_x : Math.random() * pJS.canvas.w, + 'y': pos ? pos.pos_y : Math.random() * pJS.canvas.h + } + ) + ) + if (i == nb - 1) { + if (!pJS.particles.move.enable) { + pJS.fn.particlesDraw(); + } + pJS.tmp.pushing = false; + } } - } - if(pJS.tmp.repulse_clicking){ + }; - var repulseRadius = Math.pow(pJS.interactivity.modes.repulse.distance/6, 3); - var dx = pJS.interactivity.mouse.click_pos_x - p.x, - dy = pJS.interactivity.mouse.click_pos_y - p.y, - d = dx*dx + dy*dy; + pJS.fn.modes.removeParticles = function (nb) { - var force = -repulseRadius / d * 1; + pJS.particles.array.splice(0, nb); + if (!pJS.particles.move.enable) { + pJS.fn.particlesDraw(); + } - function process(){ + }; - var f = Math.atan2(dy,dx); - p.vx = force * Math.cos(f); - p.vy = force * Math.sin(f); - if(pJS.particles.move.out_mode == 'bounce'){ + pJS.fn.modes.bubbleParticle = function (p) { + + /* on hover event */ + if (pJS.interactivity.events.onhover.enable && isInArray('bubble', pJS.interactivity.events.onhover.mode)) { + + var dx_mouse = p.x - pJS.interactivity.mouse.pos_x, + dy_mouse = p.y - pJS.interactivity.mouse.pos_y, + dist_mouse = Math.sqrt(dx_mouse * dx_mouse + dy_mouse * dy_mouse), + ratio = 1 - dist_mouse / pJS.interactivity.modes.bubble.distance; + + function init() { + p.opacity_bubble = p.opacity; + p.radius_bubble = p.radius; + } + + /* mousemove - check ratio */ + if (dist_mouse <= pJS.interactivity.modes.bubble.distance) { + + if (ratio >= 0 && pJS.interactivity.status == 'mousemove') { + + /* size */ + if (pJS.interactivity.modes.bubble.size != pJS.particles.size.value) { + + if (pJS.interactivity.modes.bubble.size > pJS.particles.size.value) { + var size = p.radius + (pJS.interactivity.modes.bubble.size * ratio); + if (size >= 0) { + p.radius_bubble = size; + } + } else { + var dif = p.radius - pJS.interactivity.modes.bubble.size, + size = p.radius - (dif * ratio); + if (size > 0) { + p.radius_bubble = size; + } else { + p.radius_bubble = 0; + } + } + + } + + /* opacity */ + if (pJS.interactivity.modes.bubble.opacity != pJS.particles.opacity.value) { + + if (pJS.interactivity.modes.bubble.opacity > pJS.particles.opacity.value) { + var opacity = pJS.interactivity.modes.bubble.opacity * ratio; + if (opacity > p.opacity && opacity <= pJS.interactivity.modes.bubble.opacity) { + p.opacity_bubble = opacity; + } + } else { + var opacity = p.opacity - (pJS.particles.opacity.value - pJS.interactivity.modes.bubble.opacity) * ratio; + if (opacity < p.opacity && opacity >= pJS.interactivity.modes.bubble.opacity) { + p.opacity_bubble = opacity; + } + } + + } + + } + + } else { + init(); + } + + + /* mouseleave */ + if (pJS.interactivity.status == 'mouseleave') { + init(); + } + + } + + /* on click event */ + else if (pJS.interactivity.events.onclick.enable && isInArray('bubble', pJS.interactivity.events.onclick.mode)) { + + + if (pJS.tmp.bubble_clicking) { + var dx_mouse = p.x - pJS.interactivity.mouse.click_pos_x, + dy_mouse = p.y - pJS.interactivity.mouse.click_pos_y, + dist_mouse = Math.sqrt(dx_mouse * dx_mouse + dy_mouse * dy_mouse), + time_spent = (new Date().getTime() - pJS.interactivity.mouse.click_time) / 1000; + + if (time_spent > pJS.interactivity.modes.bubble.duration) { + pJS.tmp.bubble_duration_end = true; + } + + if (time_spent > pJS.interactivity.modes.bubble.duration * 2) { + pJS.tmp.bubble_clicking = false; + pJS.tmp.bubble_duration_end = false; + } + } + + + function process(bubble_param, particles_param, p_obj_bubble, p_obj, id) { + + if (bubble_param != particles_param) { + + if (!pJS.tmp.bubble_duration_end) { + if (dist_mouse <= pJS.interactivity.modes.bubble.distance) { + if (p_obj_bubble != undefined) var obj = p_obj_bubble; + else var obj = p_obj; + if (obj != bubble_param) { + var value = p_obj - (time_spent * (p_obj - bubble_param) / pJS.interactivity.modes.bubble.duration); + if (id == 'size') p.radius_bubble = value; + if (id == 'opacity') p.opacity_bubble = value; + } + } else { + if (id == 'size') p.radius_bubble = undefined; + if (id == 'opacity') p.opacity_bubble = undefined; + } + } else { + if (p_obj_bubble != undefined) { + var value_tmp = p_obj - (time_spent * (p_obj - bubble_param) / pJS.interactivity.modes.bubble.duration), + dif = bubble_param - value_tmp; + value = bubble_param + dif; + if (id == 'size') p.radius_bubble = value; + if (id == 'opacity') p.opacity_bubble = value; + } + } + + } + + } + + if (pJS.tmp.bubble_clicking) { + /* size */ + process(pJS.interactivity.modes.bubble.size, pJS.particles.size.value, p.radius_bubble, p.radius, 'size'); + /* opacity */ + process(pJS.interactivity.modes.bubble.opacity, pJS.particles.opacity.value, p.opacity_bubble, p.opacity, 'opacity'); + } + + } + + }; + + + pJS.fn.modes.repulseParticle = function (p) { + + if (pJS.interactivity.events.onhover.enable && isInArray('repulse', pJS.interactivity.events.onhover.mode) && pJS.interactivity.status == 'mousemove') { + + var dx_mouse = p.x - pJS.interactivity.mouse.pos_x, + dy_mouse = p.y - pJS.interactivity.mouse.pos_y, + dist_mouse = Math.sqrt(dx_mouse * dx_mouse + dy_mouse * dy_mouse); + + var normVec = {x: dx_mouse / dist_mouse, y: dy_mouse / dist_mouse}, + repulseRadius = pJS.interactivity.modes.repulse.distance, + velocity = 100, + repulseFactor = clamp((1 / repulseRadius) * (-1 * Math.pow(dist_mouse / repulseRadius, 2) + 1) * repulseRadius * velocity, 0, 50); + var pos = { - x: p.x + p.vx, - y: p.y + p.vy + x: p.x + normVec.x * repulseFactor, + y: p.y + normVec.y * repulseFactor } - if (pos.x + p.radius > pJS.canvas.w) p.vx = -p.vx; - else if (pos.x - p.radius < 0) p.vx = -p.vx; - if (pos.y + p.radius > pJS.canvas.h) p.vy = -p.vy; - else if (pos.y - p.radius < 0) p.vy = -p.vy; - } - } - - // default - if(d <= repulseRadius){ - process(); - } - - // bang - slow motion mode - // if(!pJS.tmp.repulse_finish){ - // if(d <= repulseRadius){ - // process(); - // } - // }else{ - // process(); - // } - - - }else{ - - if(pJS.tmp.repulse_clicking == false){ - - p.vx = p.vx_i; - p.vy = p.vy_i; - - } - - } - - } - - } - - - pJS.fn.modes.grabParticle = function(p){ - - if(pJS.interactivity.events.onhover.enable && pJS.interactivity.status == 'mousemove'){ - - var dx_mouse = p.x - pJS.interactivity.mouse.pos_x, - dy_mouse = p.y - pJS.interactivity.mouse.pos_y, - dist_mouse = Math.sqrt(dx_mouse*dx_mouse + dy_mouse*dy_mouse); - - /* draw a line between the cursor and the particle if the distance between them is under the config distance */ - if(dist_mouse <= pJS.interactivity.modes.grab.distance){ - - var opacity_line = pJS.interactivity.modes.grab.line_linked.opacity - (dist_mouse / (1/pJS.interactivity.modes.grab.line_linked.opacity)) / pJS.interactivity.modes.grab.distance; - - if(opacity_line > 0){ - - /* style */ - var color_line = pJS.particles.line_linked.color_rgb_line; - pJS.canvas.ctx.strokeStyle = 'rgba('+color_line.r+','+color_line.g+','+color_line.b+','+opacity_line+')'; - pJS.canvas.ctx.lineWidth = pJS.particles.line_linked.width; - //pJS.canvas.ctx.lineCap = 'round'; /* performance issue */ - - /* path */ - pJS.canvas.ctx.beginPath(); - pJS.canvas.ctx.moveTo(p.x, p.y); - pJS.canvas.ctx.lineTo(pJS.interactivity.mouse.pos_x, pJS.interactivity.mouse.pos_y); - pJS.canvas.ctx.stroke(); - pJS.canvas.ctx.closePath(); - - } - - } - - } - - }; - - - - /* ---------- pJS functions - vendors ------------ */ - - pJS.fn.vendors.eventsListeners = function(){ - - /* events target element */ - if(pJS.interactivity.detect_on == 'window'){ - pJS.interactivity.el = window; - }else{ - pJS.interactivity.el = pJS.canvas.el; - } - - - /* detect mouse pos - on hover / click event */ - if(pJS.interactivity.events.onhover.enable || pJS.interactivity.events.onclick.enable){ - - /* el on mousemove */ - pJS.interactivity.el.addEventListener('mousemove', function(e){ - - if(pJS.interactivity.el == window){ - var pos_x = e.clientX, - pos_y = e.clientY; - } - else{ - var pos_x = e.offsetX || e.clientX, - pos_y = e.offsetY || e.clientY; - } - - pJS.interactivity.mouse.pos_x = pos_x; - pJS.interactivity.mouse.pos_y = pos_y; - - if(pJS.tmp.retina){ - pJS.interactivity.mouse.pos_x *= pJS.canvas.pxratio; - pJS.interactivity.mouse.pos_y *= pJS.canvas.pxratio; - } - - pJS.interactivity.status = 'mousemove'; - - }); - - /* el on onmouseleave */ - pJS.interactivity.el.addEventListener('mouseleave', function(e){ - - pJS.interactivity.mouse.pos_x = null; - pJS.interactivity.mouse.pos_y = null; - pJS.interactivity.status = 'mouseleave'; - - }); - - } - - /* on click event */ - if(pJS.interactivity.events.onclick.enable){ - - pJS.interactivity.el.addEventListener('click', function(){ - - pJS.interactivity.mouse.click_pos_x = pJS.interactivity.mouse.pos_x; - pJS.interactivity.mouse.click_pos_y = pJS.interactivity.mouse.pos_y; - pJS.interactivity.mouse.click_time = new Date().getTime(); - - if(pJS.interactivity.events.onclick.enable){ - - switch(pJS.interactivity.events.onclick.mode){ - - case 'push': - if(pJS.particles.move.enable){ - pJS.fn.modes.pushParticles(pJS.interactivity.modes.push.particles_nb, pJS.interactivity.mouse); - }else{ - if(pJS.interactivity.modes.push.particles_nb == 1){ - pJS.fn.modes.pushParticles(pJS.interactivity.modes.push.particles_nb, pJS.interactivity.mouse); - } - else if(pJS.interactivity.modes.push.particles_nb > 1){ - pJS.fn.modes.pushParticles(pJS.interactivity.modes.push.particles_nb); - } - } - break; - - case 'remove': - pJS.fn.modes.removeParticles(pJS.interactivity.modes.remove.particles_nb); - break; - - case 'bubble': - pJS.tmp.bubble_clicking = true; - break; - - case 'repulse': - pJS.tmp.repulse_clicking = true; - pJS.tmp.repulse_count = 0; - pJS.tmp.repulse_finish = false; - setTimeout(function(){ - pJS.tmp.repulse_clicking = false; - }, pJS.interactivity.modes.repulse.duration*1000) - break; - - } - - } - - }); - - } - - - }; - - pJS.fn.vendors.densityAutoParticles = function(){ - - if(pJS.particles.number.density.enable){ - - /* calc area */ - var area = pJS.canvas.el.width * pJS.canvas.el.height / 1000; - if(pJS.tmp.retina){ - area = area/(pJS.canvas.pxratio*2); - } - - /* calc number of particles based on density area */ - var nb_particles = area * pJS.particles.number.value / pJS.particles.number.density.value_area; - - /* add or remove X particles */ - var missing_particles = pJS.particles.array.length - nb_particles; - if(missing_particles < 0) pJS.fn.modes.pushParticles(Math.abs(missing_particles)); - else pJS.fn.modes.removeParticles(missing_particles); - - } - - }; - - - pJS.fn.vendors.checkOverlap = function(p1, position){ - for(var i = 0; i < pJS.particles.array.length; i++){ - var p2 = pJS.particles.array[i]; - - var dx = p1.x - p2.x, - dy = p1.y - p2.y, - dist = Math.sqrt(dx*dx + dy*dy); - - if(dist <= p1.radius + p2.radius){ - p1.x = position ? position.x : Math.random() * pJS.canvas.w; - p1.y = position ? position.y : Math.random() * pJS.canvas.h; - pJS.fn.vendors.checkOverlap(p1); - } - } - }; - - - pJS.fn.vendors.createSvgImg = function(p){ - - /* set color to svg element */ - var svgXml = pJS.tmp.source_svg, - rgbHex = /#([0-9A-F]{3,6})/gi, - coloredSvgXml = svgXml.replace(rgbHex, function (m, r, g, b) { - if(p.color.rgb){ - var color_value = 'rgba('+p.color.rgb.r+','+p.color.rgb.g+','+p.color.rgb.b+','+p.opacity+')'; - }else{ - var color_value = 'hsla('+p.color.hsl.h+','+p.color.hsl.s+'%,'+p.color.hsl.l+'%,'+p.opacity+')'; - } - return color_value; - }); - - /* prepare to create img with colored svg */ - var svg = new Blob([coloredSvgXml], {type: 'image/svg+xml;charset=utf-8'}), - DOMURL = window.URL || window.webkitURL || window, - url = DOMURL.createObjectURL(svg); - - /* create particle img obj */ - var img = new Image(); - img.addEventListener('load', function(){ - p.img.obj = img; - p.img.loaded = true; - DOMURL.revokeObjectURL(url); - pJS.tmp.count_svg++; - }); - img.src = url; - - }; - - - pJS.fn.vendors.destroypJS = function(){ - cancelAnimationFrame(pJS.fn.drawAnimFrame); - canvas_el.remove(); - pJSDom = null; - }; - - - pJS.fn.vendors.drawShape = function(c, startX, startY, sideLength, sideCountNumerator, sideCountDenominator){ - - // By Programming Thomas - https://programmingthomas.wordpress.com/2013/04/03/n-sided-shapes/ - var sideCount = sideCountNumerator * sideCountDenominator; - var decimalSides = sideCountNumerator / sideCountDenominator; - var interiorAngleDegrees = (180 * (decimalSides - 2)) / decimalSides; - var interiorAngle = Math.PI - Math.PI * interiorAngleDegrees / 180; // convert to radians - c.save(); - c.beginPath(); - c.translate(startX, startY); - c.moveTo(0,0); - for (var i = 0; i < sideCount; i++) { - c.lineTo(sideLength,0); - c.translate(sideLength,0); - c.rotate(interiorAngle); - } - //c.stroke(); - c.fill(); - c.restore(); - - }; - - pJS.fn.vendors.exportImg = function(){ - window.open(pJS.canvas.el.toDataURL('image/png'), '_blank'); - }; - - - pJS.fn.vendors.loadImg = function(type){ - - pJS.tmp.img_error = undefined; - - if(pJS.particles.shape.image.src != ''){ - - if(type == 'svg'){ - - var xhr = new XMLHttpRequest(); - xhr.open('GET', pJS.particles.shape.image.src); - xhr.onreadystatechange = function (data) { - if(xhr.readyState == 4){ - if(xhr.status == 200){ - pJS.tmp.source_svg = data.currentTarget.response; - pJS.fn.vendors.checkBeforeDraw(); - }else{ - console.log('Error pJS - Image not found'); - pJS.tmp.img_error = true; + if (pJS.particles.move.out_mode == 'bounce') { + if (pos.x - p.radius > 0 && pos.x + p.radius < pJS.canvas.w) p.x = pos.x; + if (pos.y - p.radius > 0 && pos.y + p.radius < pJS.canvas.h) p.y = pos.y; + } else { + p.x = pos.x; + p.y = pos.y; } - } + + } else if (pJS.interactivity.events.onclick.enable && isInArray('repulse', pJS.interactivity.events.onclick.mode)) { + + if (!pJS.tmp.repulse_finish) { + pJS.tmp.repulse_count++; + if (pJS.tmp.repulse_count == pJS.particles.array.length) { + pJS.tmp.repulse_finish = true; + } + } + + if (pJS.tmp.repulse_clicking) { + + var repulseRadius = Math.pow(pJS.interactivity.modes.repulse.distance / 6, 3); + + var dx = pJS.interactivity.mouse.click_pos_x - p.x, + dy = pJS.interactivity.mouse.click_pos_y - p.y, + d = dx * dx + dy * dy; + + var force = -repulseRadius / d * 1; + + function process() { + + var f = Math.atan2(dy, dx); + p.vx = force * Math.cos(f); + p.vy = force * Math.sin(f); + + if (pJS.particles.move.out_mode == 'bounce') { + var pos = { + x: p.x + p.vx, + y: p.y + p.vy + } + if (pos.x + p.radius > pJS.canvas.w) p.vx = -p.vx; + else if (pos.x - p.radius < 0) p.vx = -p.vx; + if (pos.y + p.radius > pJS.canvas.h) p.vy = -p.vy; + else if (pos.y - p.radius < 0) p.vy = -p.vy; + } + + } + + // default + if (d <= repulseRadius) { + process(); + } + + // bang - slow motion mode + // if(!pJS.tmp.repulse_finish){ + // if(d <= repulseRadius){ + // process(); + // } + // }else{ + // process(); + // } + + + } else { + + if (pJS.tmp.repulse_clicking == false) { + + p.vx = p.vx_i; + p.vy = p.vy_i; + + } + + } + } - xhr.send(); - }else{ + } + + pJS.fn.modes.grabParticle = function (p) { + + if (pJS.interactivity.events.onhover.enable && pJS.interactivity.status == 'mousemove') { + + var dx_mouse = p.x - pJS.interactivity.mouse.pos_x, + dy_mouse = p.y - pJS.interactivity.mouse.pos_y, + dist_mouse = Math.sqrt(dx_mouse * dx_mouse + dy_mouse * dy_mouse); + + /* draw a line between the cursor and the particle if the distance between them is under the config distance */ + if (dist_mouse <= pJS.interactivity.modes.grab.distance) { + + var opacity_line = pJS.interactivity.modes.grab.line_linked.opacity - (dist_mouse / (1 / pJS.interactivity.modes.grab.line_linked.opacity)) / pJS.interactivity.modes.grab.distance; + + if (opacity_line > 0) { + + /* style */ + var color_line = pJS.particles.line_linked.color_rgb_line; + pJS.canvas.ctx.strokeStyle = 'rgba(' + color_line.r + ',' + color_line.g + ',' + color_line.b + ',' + opacity_line + ')'; + pJS.canvas.ctx.lineWidth = pJS.particles.line_linked.width; + //pJS.canvas.ctx.lineCap = 'round'; /* performance issue */ + + /* path */ + pJS.canvas.ctx.beginPath(); + pJS.canvas.ctx.moveTo(p.x, p.y); + pJS.canvas.ctx.lineTo(pJS.interactivity.mouse.pos_x, pJS.interactivity.mouse.pos_y); + pJS.canvas.ctx.stroke(); + pJS.canvas.ctx.closePath(); + + } + + } + + } + + }; + + + /* ---------- pJS functions - vendors ------------ */ + + pJS.fn.vendors.eventsListeners = function () { + + /* events target element */ + if (pJS.interactivity.detect_on == 'window') { + pJS.interactivity.el = window; + } else { + pJS.interactivity.el = pJS.canvas.el; + } + + + /* detect mouse pos - on hover / click event */ + if (pJS.interactivity.events.onhover.enable || pJS.interactivity.events.onclick.enable) { + + /* el on mousemove */ + pJS.interactivity.el.addEventListener('mousemove', function (e) { + + if (pJS.interactivity.el == window) { + var pos_x = e.clientX, + pos_y = e.clientY; + } else { + var pos_x = e.offsetX || e.clientX, + pos_y = e.offsetY || e.clientY; + } + + pJS.interactivity.mouse.pos_x = pos_x; + pJS.interactivity.mouse.pos_y = pos_y; + + if (pJS.tmp.retina) { + pJS.interactivity.mouse.pos_x *= pJS.canvas.pxratio; + pJS.interactivity.mouse.pos_y *= pJS.canvas.pxratio; + } + + pJS.interactivity.status = 'mousemove'; + + }); + + /* el on onmouseleave */ + pJS.interactivity.el.addEventListener('mouseleave', function (e) { + + pJS.interactivity.mouse.pos_x = null; + pJS.interactivity.mouse.pos_y = null; + pJS.interactivity.status = 'mouseleave'; + + }); + + } + + /* on click event */ + if (pJS.interactivity.events.onclick.enable) { + + pJS.interactivity.el.addEventListener('click', function () { + + pJS.interactivity.mouse.click_pos_x = pJS.interactivity.mouse.pos_x; + pJS.interactivity.mouse.click_pos_y = pJS.interactivity.mouse.pos_y; + pJS.interactivity.mouse.click_time = new Date().getTime(); + + if (pJS.interactivity.events.onclick.enable) { + + switch (pJS.interactivity.events.onclick.mode) { + + case 'push': + if (pJS.particles.move.enable) { + pJS.fn.modes.pushParticles(pJS.interactivity.modes.push.particles_nb, pJS.interactivity.mouse); + } else { + if (pJS.interactivity.modes.push.particles_nb == 1) { + pJS.fn.modes.pushParticles(pJS.interactivity.modes.push.particles_nb, pJS.interactivity.mouse); + } else if (pJS.interactivity.modes.push.particles_nb > 1) { + pJS.fn.modes.pushParticles(pJS.interactivity.modes.push.particles_nb); + } + } + break; + + case 'remove': + pJS.fn.modes.removeParticles(pJS.interactivity.modes.remove.particles_nb); + break; + + case 'bubble': + pJS.tmp.bubble_clicking = true; + break; + + case 'repulse': + pJS.tmp.repulse_clicking = true; + pJS.tmp.repulse_count = 0; + pJS.tmp.repulse_finish = false; + setTimeout(function () { + pJS.tmp.repulse_clicking = false; + }, pJS.interactivity.modes.repulse.duration * 1000) + break; + + } + + } + + }); + + } + + + }; + + pJS.fn.vendors.densityAutoParticles = function () { + + if (pJS.particles.number.density.enable) { + + /* calc area */ + var area = pJS.canvas.el.width * pJS.canvas.el.height / 1000; + if (pJS.tmp.retina) { + area = area / (pJS.canvas.pxratio * 2); + } + + /* calc number of particles based on density area */ + var nb_particles = area * pJS.particles.number.value / pJS.particles.number.density.value_area; + + /* add or remove X particles */ + var missing_particles = pJS.particles.array.length - nb_particles; + if (missing_particles < 0) pJS.fn.modes.pushParticles(Math.abs(missing_particles)); + else pJS.fn.modes.removeParticles(missing_particles); + + } + + }; + + + pJS.fn.vendors.checkOverlap = function (p1, position) { + for (var i = 0; i < pJS.particles.array.length; i++) { + var p2 = pJS.particles.array[i]; + + var dx = p1.x - p2.x, + dy = p1.y - p2.y, + dist = Math.sqrt(dx * dx + dy * dy); + + if (dist <= p1.radius + p2.radius) { + p1.x = position ? position.x : Math.random() * pJS.canvas.w; + p1.y = position ? position.y : Math.random() * pJS.canvas.h; + pJS.fn.vendors.checkOverlap(p1); + } + } + }; + + + pJS.fn.vendors.createSvgImg = function (p) { + + /* set color to svg element */ + var svgXml = pJS.tmp.source_svg, + rgbHex = /#([0-9A-F]{3,6})/gi, + coloredSvgXml = svgXml.replace(rgbHex, function (m, r, g, b) { + if (p.color.rgb) { + var color_value = 'rgba(' + p.color.rgb.r + ',' + p.color.rgb.g + ',' + p.color.rgb.b + ',' + p.opacity + ')'; + } else { + var color_value = 'hsla(' + p.color.hsl.h + ',' + p.color.hsl.s + '%,' + p.color.hsl.l + '%,' + p.opacity + ')'; + } + return color_value; + }); + + /* prepare to create img with colored svg */ + var svg = new Blob([coloredSvgXml], {type: 'image/svg+xml;charset=utf-8'}), + DOMURL = window.URL || window.webkitURL || window, + url = DOMURL.createObjectURL(svg); + + /* create particle img obj */ var img = new Image(); - img.addEventListener('load', function(){ - pJS.tmp.img_obj = img; - pJS.fn.vendors.checkBeforeDraw(); + img.addEventListener('load', function () { + p.img.obj = img; + p.img.loaded = true; + DOMURL.revokeObjectURL(url); + pJS.tmp.count_svg++; }); - img.src = pJS.particles.shape.image.src; + img.src = url; - } - - }else{ - console.log('Error pJS - No image.src'); - pJS.tmp.img_error = true; - } - - }; + }; - pJS.fn.vendors.draw = function(){ + pJS.fn.vendors.destroypJS = function () { + cancelAnimationFrame(pJS.fn.drawAnimFrame); + canvas_el.remove(); + pJSDom = null; + }; - if(pJS.particles.shape.type == 'image'){ - if(pJS.tmp.img_type == 'svg'){ + pJS.fn.vendors.drawShape = function (c, startX, startY, sideLength, sideCountNumerator, sideCountDenominator) { - if(pJS.tmp.count_svg >= pJS.particles.number.value){ - pJS.fn.particlesDraw(); - if(!pJS.particles.move.enable) cancelRequestAnimFrame(pJS.fn.drawAnimFrame); - else pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw); - }else{ - //console.log('still loading...'); - if(!pJS.tmp.img_error) pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw); + // By Programming Thomas - https://programmingthomas.wordpress.com/2013/04/03/n-sided-shapes/ + var sideCount = sideCountNumerator * sideCountDenominator; + var decimalSides = sideCountNumerator / sideCountDenominator; + var interiorAngleDegrees = (180 * (decimalSides - 2)) / decimalSides; + var interiorAngle = Math.PI - Math.PI * interiorAngleDegrees / 180; // convert to radians + c.save(); + c.beginPath(); + c.translate(startX, startY); + c.moveTo(0, 0); + for (var i = 0; i < sideCount; i++) { + c.lineTo(sideLength, 0); + c.translate(sideLength, 0); + c.rotate(interiorAngle); + } + //c.stroke(); + c.fill(); + c.restore(); + + }; + + pJS.fn.vendors.exportImg = function () { + window.open(pJS.canvas.el.toDataURL('image/png'), '_blank'); + }; + + + pJS.fn.vendors.loadImg = function (type) { + + pJS.tmp.img_error = undefined; + + if (pJS.particles.shape.image.src != '') { + + if (type == 'svg') { + + var xhr = new XMLHttpRequest(); + xhr.open('GET', pJS.particles.shape.image.src); + xhr.onreadystatechange = function (data) { + if (xhr.readyState == 4) { + if (xhr.status == 200) { + pJS.tmp.source_svg = data.currentTarget.response; + pJS.fn.vendors.checkBeforeDraw(); + } else { + console.log('Error pJS - Image not found'); + pJS.tmp.img_error = true; + } + } + } + xhr.send(); + + } else { + + var img = new Image(); + img.addEventListener('load', function () { + pJS.tmp.img_obj = img; + pJS.fn.vendors.checkBeforeDraw(); + }); + img.src = pJS.particles.shape.image.src; + + } + + } else { + console.log('Error pJS - No image.src'); + pJS.tmp.img_error = true; } - }else{ + }; - if(pJS.tmp.img_obj != undefined){ - pJS.fn.particlesDraw(); - if(!pJS.particles.move.enable) cancelRequestAnimFrame(pJS.fn.drawAnimFrame); - else pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw); - }else{ - if(!pJS.tmp.img_error) pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw); + + pJS.fn.vendors.draw = function () { + + if (pJS.particles.shape.type == 'image') { + + if (pJS.tmp.img_type == 'svg') { + + if (pJS.tmp.count_svg >= pJS.particles.number.value) { + pJS.fn.particlesDraw(); + if (!pJS.particles.move.enable) cancelRequestAnimFrame(pJS.fn.drawAnimFrame); + else pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw); + } else { + //console.log('still loading...'); + if (!pJS.tmp.img_error) pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw); + } + + } else { + + if (pJS.tmp.img_obj != undefined) { + pJS.fn.particlesDraw(); + if (!pJS.particles.move.enable) cancelRequestAnimFrame(pJS.fn.drawAnimFrame); + else pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw); + } else { + if (!pJS.tmp.img_error) pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw); + } + + } + + } else { + pJS.fn.particlesDraw(); + if (!pJS.particles.move.enable) cancelRequestAnimFrame(pJS.fn.drawAnimFrame); + else pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw); } - } - - }else{ - pJS.fn.particlesDraw(); - if(!pJS.particles.move.enable) cancelRequestAnimFrame(pJS.fn.drawAnimFrame); - else pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw); - } - - }; + }; - pJS.fn.vendors.checkBeforeDraw = function(){ + pJS.fn.vendors.checkBeforeDraw = function () { - // if shape is image - if(pJS.particles.shape.type == 'image'){ + // if shape is image + if (pJS.particles.shape.type == 'image') { - if(pJS.tmp.img_type == 'svg' && pJS.tmp.source_svg == undefined){ - pJS.tmp.checkAnimFrame = requestAnimFrame(check); - }else{ - //console.log('images loaded! cancel check'); - cancelRequestAnimFrame(pJS.tmp.checkAnimFrame); - if(!pJS.tmp.img_error){ - pJS.fn.vendors.init(); - pJS.fn.vendors.draw(); + if (pJS.tmp.img_type == 'svg' && pJS.tmp.source_svg == undefined) { + pJS.tmp.checkAnimFrame = requestAnimFrame(check); + } else { + //console.log('images loaded! cancel check'); + cancelRequestAnimFrame(pJS.tmp.checkAnimFrame); + if (!pJS.tmp.img_error) { + pJS.fn.vendors.init(); + pJS.fn.vendors.draw(); + } + + } + + } else { + pJS.fn.vendors.init(); + pJS.fn.vendors.draw(); } - - } - }else{ - pJS.fn.vendors.init(); - pJS.fn.vendors.draw(); - } - - }; + }; - pJS.fn.vendors.init = function(){ + pJS.fn.vendors.init = function () { - /* init canvas + particles */ - pJS.fn.retinaInit(); - pJS.fn.canvasInit(); - pJS.fn.canvasSize(); - pJS.fn.canvasPaint(); - pJS.fn.particlesCreate(); - pJS.fn.vendors.densityAutoParticles(); + /* init canvas + particles */ + pJS.fn.retinaInit(); + pJS.fn.canvasInit(); + pJS.fn.canvasSize(); + pJS.fn.canvasPaint(); + pJS.fn.particlesCreate(); + pJS.fn.vendors.densityAutoParticles(); - /* particles.line_linked - convert hex colors to rgb */ - pJS.particles.line_linked.color_rgb_line = hexToRgb(pJS.particles.line_linked.color); + /* particles.line_linked - convert hex colors to rgb */ + pJS.particles.line_linked.color_rgb_line = hexToRgb(pJS.particles.line_linked.color); - }; + }; - pJS.fn.vendors.start = function(){ + pJS.fn.vendors.start = function () { - if(isInArray('image', pJS.particles.shape.type)){ - pJS.tmp.img_type = pJS.particles.shape.image.src.substr(pJS.particles.shape.image.src.length - 3); - pJS.fn.vendors.loadImg(pJS.tmp.img_type); - }else{ - pJS.fn.vendors.checkBeforeDraw(); - } + if (isInArray('image', pJS.particles.shape.type)) { + pJS.tmp.img_type = pJS.particles.shape.image.src.substr(pJS.particles.shape.image.src.length - 3); + pJS.fn.vendors.loadImg(pJS.tmp.img_type); + } else { + pJS.fn.vendors.checkBeforeDraw(); + } - }; + }; + /* ---------- pJS - start ------------ */ - /* ---------- pJS - start ------------ */ + pJS.fn.vendors.eventsListeners(); - - pJS.fn.vendors.eventsListeners(); - - pJS.fn.vendors.start(); - + pJS.fn.vendors.start(); }; /* ---------- global functions - vendors ------------ */ -Object.deepExtend = function(destination, source) { - for (var property in source) { - if (source[property] && source[property].constructor && - source[property].constructor === Object) { - destination[property] = destination[property] || {}; - arguments.callee(destination[property], source[property]); - } else { - destination[property] = source[property]; +Object.deepExtend = function (destination, source) { + for (var property in source) { + if (source[property] && source[property].constructor && + source[property].constructor === Object) { + destination[property] = destination[property] || {}; + arguments.callee(destination[property], source[property]); + } else { + destination[property] = source[property]; + } } - } - return destination; + return destination; }; -window.requestAnimFrame = (function(){ - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function(callback){ - window.setTimeout(callback, 1000 / 60); - }; +window.requestAnimFrame = (function () { + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function (callback) { + window.setTimeout(callback, 1000 / 60); + }; })(); -window.cancelRequestAnimFrame = ( function() { - return window.cancelAnimationFrame || - window.webkitCancelRequestAnimationFrame || - window.mozCancelRequestAnimationFrame || - window.oCancelRequestAnimationFrame || - window.msCancelRequestAnimationFrame || - clearTimeout -} )(); +window.cancelRequestAnimFrame = (function () { + return window.cancelAnimationFrame || + window.webkitCancelRequestAnimationFrame || + window.mozCancelRequestAnimationFrame || + window.oCancelRequestAnimationFrame || + window.msCancelRequestAnimationFrame || + clearTimeout +})(); -function hexToRgb(hex){ - // By Tim Down - http://stackoverflow.com/a/5624139/3493650 - // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") - var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; - hex = hex.replace(shorthandRegex, function(m, r, g, b) { - return r + r + g + g + b + b; - }); - var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16) - } : null; +function hexToRgb(hex) { + // By Tim Down - http://stackoverflow.com/a/5624139/3493650 + // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") + var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + hex = hex.replace(shorthandRegex, function (m, r, g, b) { + return r + r + g + g + b + b; + }); + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; }; function clamp(number, min, max) { - return Math.min(Math.max(number, min), max); + return Math.min(Math.max(number, min), max); }; function isInArray(value, array) { - return array.indexOf(value) > -1; + return array.indexOf(value) > -1; } @@ -1474,68 +1457,68 @@ function isInArray(value, array) { window.pJSDom = []; -window.particlesJS = function(tag_id, params){ +window.particlesJS = function (tag_id, params) { - //console.log(params); + //console.log(params); - /* no string id? so it's object params, and set the id with default id */ - if(typeof(tag_id) != 'string'){ - params = tag_id; - tag_id = 'particles-js'; - } - - /* no id? set the id to default id */ - if(!tag_id){ - tag_id = 'particles-js'; - } - - /* pJS elements */ - var pJS_tag = document.getElementById(tag_id), - pJS_canvas_class = 'particles-js-canvas-el', - exist_canvas = pJS_tag.getElementsByClassName(pJS_canvas_class); - - /* remove canvas if exists into the pJS target tag */ - if(exist_canvas.length){ - while(exist_canvas.length > 0){ - pJS_tag.removeChild(exist_canvas[0]); + /* no string id? so it's object params, and set the id with default id */ + if (typeof (tag_id) != 'string') { + params = tag_id; + tag_id = 'particles-js'; } - } - /* create canvas element */ - var canvas_el = document.createElement('canvas'); - canvas_el.className = pJS_canvas_class; + /* no id? set the id to default id */ + if (!tag_id) { + tag_id = 'particles-js'; + } - /* set size canvas */ - canvas_el.style.width = "100%"; - canvas_el.style.height = "100%"; + /* pJS elements */ + var pJS_tag = document.getElementById(tag_id), + pJS_canvas_class = 'particles-js-canvas-el', + exist_canvas = pJS_tag.getElementsByClassName(pJS_canvas_class); - /* append canvas */ - var canvas = document.getElementById(tag_id).appendChild(canvas_el); + /* remove canvas if exists into the pJS target tag */ + if (exist_canvas.length) { + while (exist_canvas.length > 0) { + pJS_tag.removeChild(exist_canvas[0]); + } + } - /* launch particle.js */ - if(canvas != null){ - pJSDom.push(new pJS(tag_id, params)); - } + /* create canvas element */ + var canvas_el = document.createElement('canvas'); + canvas_el.className = pJS_canvas_class; + + /* set size canvas */ + canvas_el.style.width = "100%"; + canvas_el.style.height = "100%"; + + /* append canvas */ + var canvas = document.getElementById(tag_id).appendChild(canvas_el); + + /* launch particle.js */ + if (canvas != null) { + pJSDom.push(new pJS(tag_id, params)); + } }; -window.particlesJS.load = function(tag_id, path_config_json, callback){ +window.particlesJS.load = function (tag_id, path_config_json, callback) { - /* load json config */ - var xhr = new XMLHttpRequest(); - xhr.open('GET', path_config_json); - xhr.onreadystatechange = function (data) { - if(xhr.readyState == 4){ - if(xhr.status == 200){ - var params = JSON.parse(data.currentTarget.response); - window.particlesJS(tag_id, params); - if(callback) callback(); - }else{ - console.log('Error pJS - XMLHttpRequest status: '+xhr.status); - console.log('Error pJS - File config not found'); - } - } - }; - xhr.send(); + /* load json config */ + var xhr = new XMLHttpRequest(); + xhr.open('GET', path_config_json); + xhr.onreadystatechange = function (data) { + if (xhr.readyState == 4) { + if (xhr.status == 200) { + var params = JSON.parse(data.currentTarget.response); + window.particlesJS(tag_id, params); + if (callback) callback(); + } else { + console.log('Error pJS - XMLHttpRequest status: ' + xhr.status); + console.log('Error pJS - File config not found'); + } + } + }; + xhr.send(); }; \ No newline at end of file diff --git a/knockoutwhistweb/public/javascripts/ingame.js b/knockoutwhistweb/public/javascripts/websocket.js similarity index 89% rename from knockoutwhistweb/public/javascripts/ingame.js rename to knockoutwhistweb/public/javascripts/websocket.js index d44e04d..744c37a 100644 --- a/knockoutwhistweb/public/javascripts/ingame.js +++ b/knockoutwhistweb/public/javascripts/websocket.js @@ -5,10 +5,11 @@ const handlers = new Map(); // eventType -> handler(data) -> (value|Promise) let timer = null; + // helper to attach message/error/close handlers to a socket function setupSocketHandlers(socket) { socket.onmessage = (event) => { - console.debug("SERVER RESPONSE:", event.data); + console.debug("SERVER MESSAGE:", event.data); let msg; try { msg = JSON.parse(event.data); @@ -39,7 +40,7 @@ function setupSocketHandlers(socket) { if (id && eventType) { const handler = handlers.get(eventType); const sendResponse = (respData) => { - const response = { id: id, event: eventType, data: respData === undefined ? {} : respData }; + const response = {id: id, event: eventType, data: respData === undefined ? {} : respData}; if (socket && socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify(response)); } else { @@ -49,16 +50,16 @@ function setupSocketHandlers(socket) { if (!handler) { // no handler: respond with an error object in data so server can fail it - sendResponse({ error: "No handler for event: " + eventType }); + sendResponse({error: "No handler for event: " + eventType}); return; } try { Promise.resolve(handler(data === undefined ? {} : data)) .then(result => sendResponse(result)) - .catch(err => sendResponse({ error: err?.message ? err.message : String(err) })); + .catch(err => sendResponse({error: err?.message ? err.message : String(err)})); } catch (err) { - sendResponse({ error: err?.message ? err.message : String(err) }); + sendResponse({error: err?.message ? err.message : String(err)}); } } }; @@ -140,7 +141,10 @@ function disconnectWebSocket(code = 1000, reason = "Client disconnect") { timer = null; } if (ws) { - try { ws.close(code, reason); } catch (e) {} + try { + ws.close(code, reason); + } catch (e) { + } ws = null; } } @@ -151,7 +155,7 @@ function sendEvent(eventType, eventData) { return; } const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 9); - const message = { id: id, event: eventType, data: eventData }; + const message = {id: id, event: eventType, data: eventData}; ws.send(JSON.stringify(message)); console.debug("SENT:", message); } @@ -161,7 +165,7 @@ function sendEventAndWait(eventType, eventData, timeoutMs = 10000) { return Promise.reject(new Error("WebSocket is not open")); } const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 9); - const message = { id: id, event: eventType, data: eventData }; + const message = {id: id, event: eventType, data: eventData}; const p = new Promise((resolve, reject) => { const timerId = setTimeout(() => { if (pending.has(id)) { @@ -169,12 +173,13 @@ function sendEventAndWait(eventType, eventData, timeoutMs = 10000) { reject(new Error(`No response within ${timeoutMs}ms for id=${id}`)); } }, timeoutMs); - pending.set(id, { resolve, reject, timer: timerId }); + pending.set(id, {resolve, reject, timer: timerId}); }); ws.send(JSON.stringify(message)); console.debug("SENT (await):", message); return p; } + function onEvent(eventType, handler) { handlers.set(eventType, handler); } diff --git a/knockoutwhistweb/test/controllers/HomeControllerSpec.scala b/knockoutwhistweb/test/controllers/HomeControllerSpec.scala index db9c4b9..12dc14b 100644 --- a/knockoutwhistweb/test/controllers/HomeControllerSpec.scala +++ b/knockoutwhistweb/test/controllers/HomeControllerSpec.scala @@ -13,33 +13,33 @@ import play.api.test.Helpers.* */ class HomeControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting { -// "HomeController GET" should { -// -// "render the index page from a new instance of controller" in { -// val controller = new HomeController(stubControllerComponents()) -// val home = controller.index().apply(FakeRequest(GET, "/")) -// -// status(home) mustBe OK -// contentType(home) mustBe Some("text/html") -// contentAsString(home) must include ("Welcome to Play") -// } -// -// "render the index page from the application" in { -// val controller = inject[HomeController] -// val home = controller.index().apply(FakeRequest(GET, "/")) -// -// status(home) mustBe OK -// contentType(home) mustBe Some("text/html") -// contentAsString(home) must include ("Welcome to Play") -// } -// -// "render the index page from the router" in { -// val request = FakeRequest(GET, "/") -// val home = route(app, request).get -// -// status(home) mustBe OK -// contentType(home) mustBe Some("text/html") -// contentAsString(home) must include ("Welcome to Play") -// } -// } + // "HomeController GET" should { + // + // "render the index page from a new instance of controller" in { + // val controller = new HomeController(stubControllerComponents()) + // val home = controller.index().apply(FakeRequest(GET, "/")) + // + // status(home) mustBe OK + // contentType(home) mustBe Some("text/html") + // contentAsString(home) must include ("Welcome to Play") + // } + // + // "render the index page from the application" in { + // val controller = inject[HomeController] + // val home = controller.index().apply(FakeRequest(GET, "/")) + // + // status(home) mustBe OK + // contentType(home) mustBe Some("text/html") + // contentAsString(home) must include ("Welcome to Play") + // } + // + // "render the index page from the router" in { + // val request = FakeRequest(GET, "/") + // val home = route(app, request).get + // + // status(home) mustBe OK + // contentType(home) mustBe Some("text/html") + // contentAsString(home) must include ("Welcome to Play") + // } + // } }