Skip to content

Commit

Permalink
Make Transition Easier On The Eyes
Browse files Browse the repository at this point in the history
+ Make it easier to follow movement by avoiding abrupt movement. Model camera following player.
+ Increase interactive area to input command to move.
  • Loading branch information
Viir committed Jul 25, 2018
1 parent 5d1a429 commit 2eb2e66
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 16 deletions.
7 changes: 7 additions & 0 deletions src/Common.elm
@@ -0,0 +1,7 @@
module Common exposing (..)

withListTransformApplied : List (a -> a) -> a -> a
withListTransformApplied listTransform original =
case listTransform |> List.head of
Just transform -> withListTransformApplied (listTransform |> List.tail |> Maybe.withDefault []) (transform original)
Nothing -> original
90 changes: 90 additions & 0 deletions src/Console.elm
@@ -0,0 +1,90 @@
module Console exposing (State, init, updateForChangedGame, updateForTimeProgress, applyCameraTransformToSvg)

import GameWorld
import Visuals
import Common exposing (..)
import Point2d exposing (Point2d)
import Vector2d exposing (Vector2d)
import Dict
import Tuple2
import Svg
import Svg.Attributes as SA


type alias State =
{ knownGame : GameWorld.State
, cameraVelocity : Vector2d
, cameraOffset : Vector2d
}


init : GameWorld.State -> State
init game =
{ knownGame = game
, cameraOffset = game |> defaultCameraOffsetFromGame |> Maybe.withDefault Vector2d.zero
, cameraVelocity = Vector2d.zero
}

applyCameraTransformToSvg : State -> Svg.Svg event -> Svg.Svg event
applyCameraTransformToSvg state =
List.singleton >>
(Svg.g [ SA.transform (Visuals.svgTransformTranslate (state.cameraOffset |> Vector2d.components |> Tuple2.mapBoth negate))])

defaultCameraOffsetFromGame : GameWorld.State -> Maybe Vector2d
defaultCameraOffsetFromGame game =
let
playerVisualLocation =
case game.playerLocation of
GameWorld.OnNode nodeId -> game.nodes |> Dict.get nodeId |> Maybe.map .visualLocation
in
playerVisualLocation
|> Maybe.map (Point2d.coordinates >> Vector2d.fromComponents)

updateForChangedGame : GameWorld.State -> State -> State
updateForChangedGame gameState stateBefore = { stateBefore | knownGame = gameState }

updateForTimeProgress : Int -> State -> State
updateForTimeProgress = updateAnimateCamera

updateAnimateCamera : Int -> State -> State
updateAnimateCamera progressAmountMilli consoleBefore =
let
stepSizes =
(animateCameraStepSizeMax |> List.repeat (progressAmountMilli // animateCameraStepSizeMax)) ++
[ progressAmountMilli % animateCameraStepSizeMax ]
in
consoleBefore
|> withListTransformApplied (stepSizes |> List.map updateAnimateCameraSingleStep)

animateCameraStepSizeMax : Int
animateCameraStepSizeMax = 5

updateAnimateCameraSingleStep : Int -> State -> State
updateAnimateCameraSingleStep progressAmountMilli consoleBefore =
if progressAmountMilli < 1
then consoleBefore
else
let
cameraDestinationOffset = (init consoleBefore.knownGame).cameraOffset

progressSeconds = (progressAmountMilli |> toFloat) * 1e-3

cameraVelocityAcceleratedToDestination =
{ offset = consoleBefore.cameraOffset, velocity = consoleBefore.cameraVelocity }
|> accelerateCameraTowardsOffset (progressSeconds * 3) cameraDestinationOffset
|> .velocity

dampFactor = 0.9 ^ (progressSeconds * 30)

cameraVelocity = cameraVelocityAcceleratedToDestination |> Vector2d.scaleBy dampFactor

cameraOffset = cameraVelocity |> Vector2d.scaleBy progressSeconds |> Vector2d.sum consoleBefore.cameraOffset
in
{ consoleBefore | cameraOffset = cameraOffset, cameraVelocity = cameraVelocity }

accelerateCameraTowardsOffset : Float -> Vector2d -> { offset: Vector2d, velocity : Vector2d } -> { velocity: Vector2d }
accelerateCameraTowardsOffset accelerationFactor cameraDestinationOffset cameraBefore =
let
offsetAcceleration = Vector2d.difference cameraDestinationOffset cameraBefore.offset |> Vector2d.scaleBy accelerationFactor
in
{ velocity = cameraBefore.velocity |> Vector2d.sum offsetAcceleration }
18 changes: 7 additions & 11 deletions src/GameWorld.elm
@@ -1,4 +1,4 @@
module GameWorld exposing (Node, State, FromPlayerInput, init, updateForPlayerInput, view) module GameWorld exposing (Node, State, Location(..), FromPlayerInput, init, updateForPlayerInput, view)


import Visuals import Visuals
import Point2d exposing (Point2d) import Point2d exposing (Point2d)
Expand Down Expand Up @@ -93,18 +93,11 @@ view state =
let let
nodesView = state.nodes |> Dict.toList |> List.map (viewNode state) |> Svg.g [] nodesView = state.nodes |> Dict.toList |> List.map (viewNode state) |> Svg.g []
edgesView = state.edges |> Set.toList |> List.map (viewEdge state) |> Svg.g [ SA.opacity "0.5" ] edgesView = state.edges |> Set.toList |> List.map (viewEdge state) |> Svg.g [ SA.opacity "0.5" ]

playerVisualLocation =
case state.playerLocation of
OnNode nodeId -> state.nodes |> Dict.get nodeId |> Maybe.map .visualLocation |> Maybe.withDefault Point2d.origin

viewportOffset =
playerVisualLocation |> Point2d.coordinates |> Tuple2.mapBoth negate
in in
[ state.visuals |> viewVisuals [ state.visuals |> viewVisuals
, edgesView , edgesView
, nodesView , nodesView
] |> Svg.g [ SA.transform (Visuals.svgTransformTranslate viewportOffset)] ] |> Svg.g []


edgeViewWidth : Float edgeViewWidth : Float
edgeViewWidth = 2 edgeViewWidth = 2
Expand Down Expand Up @@ -156,7 +149,7 @@ viewNode worldState (nodeId, node) =
canPlayerGetHereDirectly = worldAfterPointerDownEvent.playerLocation == (OnNode nodeId) canPlayerGetHereDirectly = worldAfterPointerDownEvent.playerLocation == (OnNode nodeId)


nodeBaseView = nodeBaseView =
Svg.circle [ SA.r (nodeViewRadius |> toString), SA.fill "grey" ] [] svgCircleFromRadiusAndFillAndStroke (nodeViewRadius, "grey") Nothing


playerView = playerView =
if isPlayerLocatedHere if isPlayerLocatedHere
Expand All @@ -175,8 +168,11 @@ viewNode worldState (nodeId, node) =
(if indicateEffectForInput (if indicateEffectForInput
then [ HA.style [("cursor","pointer")] ] then [ HA.style [("cursor","pointer")] ]
else []) else [])

additionalInputArea =
svgCircleFromRadiusAndFillAndStroke (nodeViewRadius * 2, "transparent") Nothing
in in
[ nodeBaseView, playerView ] [ additionalInputArea, nodeBaseView, playerView ]
|> Svg.g (inputAttributes ++ [ transformAttribute, SA.opacity (opacity |> toString) ]) |> Svg.g (inputAttributes ++ [ transformAttribute, SA.opacity (opacity |> toString) ])


viewVisuals : GameWorldVisuals -> Svg.Svg event viewVisuals : GameWorldVisuals -> Svg.Svg event
Expand Down
21 changes: 16 additions & 5 deletions src/app.elm
@@ -1,17 +1,22 @@
import GameWorld import GameWorld
import Console
import Visuals import Visuals
import BoundingBox2d exposing (BoundingBox2d) import BoundingBox2d exposing (BoundingBox2d)
import Html exposing (Html) import Html exposing (Html)
import Html.Attributes as HA import Html.Attributes as HA
import Svg import Svg
import Svg.Attributes as SA import Svg.Attributes as SA
import AnimationFrame




type alias State = type alias State =
{ gameWorld : GameWorld.State { gameWorld : GameWorld.State
, console : Console.State
} }


type Event = PlayerInput GameWorld.FromPlayerInput type Event
= PlayerInput GameWorld.FromPlayerInput
| AnimationFrameDiff Float




main : Program Never State Event main : Program Never State Event
Expand All @@ -24,16 +29,22 @@ main =
} }


init : (State, Cmd Event) init : (State, Cmd Event)
init = ({ gameWorld = GameWorld.init }, Cmd.none) init = ({ gameWorld = GameWorld.init, console = Console.init GameWorld.init }, Cmd.none)


update : Event -> State -> (State, Cmd Event) update : Event -> State -> (State, Cmd Event)
update event stateBefore = update event stateBefore =
case event of case event of
PlayerInput playerInput -> PlayerInput playerInput ->
( { stateBefore | gameWorld = stateBefore.gameWorld |> GameWorld.updateForPlayerInput playerInput } , Cmd.none) let
gameWorld = stateBefore.gameWorld |> GameWorld.updateForPlayerInput playerInput
console = stateBefore.console |> Console.updateForChangedGame gameWorld
in
( { stateBefore | gameWorld = gameWorld, console = console }, Cmd.none)
AnimationFrameDiff diff ->
( { stateBefore | console = stateBefore.console |> Console.updateForTimeProgress (diff |> round) } , Cmd.none)


subscriptions : State -> Sub Event subscriptions : State -> Sub Event
subscriptions state = Sub.none subscriptions state = AnimationFrame.diffs AnimationFrameDiff


view : State -> Html.Html Event view : State -> Html.Html Event
view state = view state =
Expand All @@ -42,6 +53,6 @@ view state =
BoundingBox2d.fromExtrema { minX = -300, minY = -200, maxX = 300, maxY = 200 } BoundingBox2d.fromExtrema { minX = -300, minY = -200, maxX = 300, maxY = 200 }
|> Visuals.svgViewBoxFromBoundingBox |> Visuals.svgViewBoxFromBoundingBox
in in
[ state.gameWorld |> GameWorld.view |> Html.map PlayerInput ] [ state.gameWorld |> GameWorld.view |> Html.map PlayerInput |> Console.applyCameraTransformToSvg state.console ]
|> Svg.svg [ SA.viewBox viewbox, HA.style [("width","100%"),("height","96vh")]] |> Svg.svg [ SA.viewBox viewbox, HA.style [("width","100%"),("height","96vh")]]


1 change: 1 addition & 0 deletions src/elm-package.json
Expand Up @@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"Fresheyeball/elm-tuple-extra": "3.0.0 <= v < 4.0.0", "Fresheyeball/elm-tuple-extra": "3.0.0 <= v < 4.0.0",
"elm-community/maybe-extra": "4.0.0 <= v < 5.0.0", "elm-community/maybe-extra": "4.0.0 <= v < 5.0.0",
"elm-lang/animation-frame": "1.0.1 <= v < 2.0.0",
"elm-lang/core": "5.1.1 <= v < 6.0.0", "elm-lang/core": "5.1.1 <= v < 6.0.0",
"elm-lang/html": "2.0.0 <= v < 3.0.0", "elm-lang/html": "2.0.0 <= v < 3.0.0",
"elm-lang/svg": "2.0.0 <= v < 3.0.0", "elm-lang/svg": "2.0.0 <= v < 3.0.0",
Expand Down

0 comments on commit 2eb2e66

Please sign in to comment.