Permalink
Browse files

Make Transition Easier On The Eyes

+ 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 2eb2e66f120426ef46b10b6b04aea0b177f3c221
Showing with 121 additions and 16 deletions.
  1. +7 −0 src/Common.elm
  2. +90 −0 src/Console.elm
  3. +7 −11 src/GameWorld.elm
  4. +16 −5 src/app.elm
  5. +1 −0 src/elm-package.json
@@ -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
@@ -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 }
@@ -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 Point2d exposing (Point2d)
@@ -93,18 +93,11 @@ view state =
let
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" ]

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
[ state.visuals |> viewVisuals
, edgesView
, nodesView
] |> Svg.g [ SA.transform (Visuals.svgTransformTranslate viewportOffset)]
] |> Svg.g []

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

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

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

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

viewVisuals : GameWorldVisuals -> Svg.Svg event
@@ -1,17 +1,22 @@
import GameWorld
import Console
import Visuals
import BoundingBox2d exposing (BoundingBox2d)
import Html exposing (Html)
import Html.Attributes as HA
import Svg
import Svg.Attributes as SA
import AnimationFrame


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

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


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

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 stateBefore =
case event of
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.none
subscriptions state = AnimationFrame.diffs AnimationFrameDiff

view : State -> Html.Html Event
view state =
@@ -42,6 +53,6 @@ view state =
BoundingBox2d.fromExtrema { minX = -300, minY = -200, maxX = 300, maxY = 200 }
|> Visuals.svgViewBoxFromBoundingBox
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")]]

@@ -10,6 +10,7 @@
"dependencies": {
"Fresheyeball/elm-tuple-extra": "3.0.0 <= v < 4.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/html": "2.0.0 <= v < 3.0.0",
"elm-lang/svg": "2.0.0 <= v < 3.0.0",

0 comments on commit 2eb2e66

Please sign in to comment.