Compare commits

...

9 Commits

Author SHA1 Message Date
Matthew Griffith
caa5520580 incorrect type signature 2024-05-04 08:35:12 -04:00
Matthew Griffith
07a390de66 Remove Animator.Watcher from the exposed list 2024-05-04 08:00:14 -04:00
Matthew Griffith
963d415161 A bunch of elm-review cleanup 2024-05-04 08:00:01 -04:00
Matthew Griffith
65b635e9ef Remove some elm-review rules 2024-05-04 07:59:39 -04:00
Matthew Griffith
8d9af0afaa Update new examples 2024-05-03 16:43:57 -04:00
Matthew Griffith
1309108299 Remove old examples 2024-05-03 16:43:24 -04:00
Matthew Griffith
441177c59b Remove old Basic example 2024-05-03 16:24:17 -04:00
Matthew Griffith
f5d75fee5f Update basic example 2024-05-03 16:24:05 -04:00
Matthew Griffith
aa0aad7b01 Remove Animator.Watcher as it seems like its too much work 2024-05-03 16:23:51 -04:00
34 changed files with 834 additions and 5081 deletions

View File

@ -8,17 +8,17 @@
"Animator",
"Animator.Timeline",
"Animator.Value",
"Animator.Watcher",
"Animator.Transition"
],
"elm-version": "0.19.0 <= v < 0.20.0",
"dependencies": {
"avh4/elm-color": "1.0.0 <= v < 2.0.0",
"elm/browser": "1.0.0 <= v < 2.0.0",
"elm/core": "1.0.0 <= v < 2.0.0",
"elm/html": "1.0.0 <= v < 2.0.0",
"elm/time": "1.0.0 <= v < 2.0.0",
"mdgriffith/elm-bezier": "1.0.0 <= v < 2.0.0"
},
"test-dependencies": { "elm-explorations/test": "2.2.0 <= v < 3.0.0" }
"test-dependencies": {
"elm-explorations/test": "2.2.0 <= v < 3.0.0"
}
}

View File

@ -1,43 +0,0 @@
{
"type": "application",
"source-directories": [
"src",
"../src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"avh4/elm-color": "1.0.0",
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
"elm/json": "1.1.3",
"elm/svg": "1.0.1",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"ianmackenzie/elm-geometry": "4.0.0",
"ianmackenzie/elm-units": "2.10.0",
"mdgriffith/elm-bezier": "1.0.0",
"terezka/line-charts": "2.0.2"
},
"indirect": {
"debois/elm-dom": "1.3.0",
"elm/parser": "1.1.0",
"elm/random": "1.0.0",
"elm/virtual-dom": "1.0.3",
"ianmackenzie/elm-1d-parameter": "1.0.1",
"ianmackenzie/elm-float-extra": "1.1.0",
"ianmackenzie/elm-interval": "3.1.0",
"ianmackenzie/elm-triangular-mesh": "1.1.0",
"ianmackenzie/elm-units-interval": "3.2.0",
"justinmimbs/date": "4.1.0",
"justinmimbs/time-extra": "1.2.0",
"myrho/elm-round": "1.0.5",
"ryan-haskell/date-format": "1.0.0"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}

View File

@ -1,92 +0,0 @@
module AnimationX exposing (main)
import Animator
import Animator.Timeline
import Animator.Transition
import Animator.Value
import Browser
import Browser.Events
import Html
import Html.Events
import Time
main : Program () Model Msg
main =
Browser.element
{ init = always init
, update = update
, subscriptions = subscriptions
, view = view
}
type alias Model =
{ timeline : Animator.Timeline.Timeline Float }
type Msg
= NewPosix Time.Posix
| Start
init : ( Model, Cmd Msg )
init =
let
initialTimeline : Animator.Timeline.Timeline Float
initialTimeline =
Animator.Timeline.init 10
queuedSteps : List (Animator.Timeline.Step Float)
queuedSteps =
[ Animator.Timeline.transitionTo (Animator.ms 1000) 100
, Animator.Timeline.transitionTo (Animator.ms 1000) 50
, Animator.Timeline.transitionTo (Animator.ms 1000) 5
]
timelineWithSteps : Animator.Timeline.Timeline Float
timelineWithSteps =
Animator.Timeline.scale 3 <| Animator.Timeline.queue queuedSteps initialTimeline
in
( { timeline = timelineWithSteps }
, Cmd.none
)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NewPosix posix ->
( { model | timeline = Animator.Timeline.update posix model.timeline }, Cmd.none )
Start ->
init
subscriptions : Model -> Sub Msg
subscriptions model =
if Animator.Timeline.isRunning model.timeline then
Browser.Events.onAnimationFrame NewPosix
else
Sub.none
view : Model -> Html.Html Msg
view model =
let
positionStandard : Float
positionStandard =
Animator.Value.float model.timeline Animator.Value.to
positionLinear : Float
positionLinear =
Animator.Value.float model.timeline (Animator.Value.withTransition Animator.Transition.linear << Animator.Value.to)
in
Html.div []
[ Html.div []
[ Html.div [] [ Html.text ("Position Standard: " ++ String.fromFloat positionStandard) ]
, Html.div [] [ Html.text ("Position Linear: " ++ String.fromFloat positionLinear) ]
]
, Html.button [ Html.Events.onClick Start ] [ Html.text "Re-Start" ]
]

View File

@ -1,83 +0,0 @@
module Basic exposing (main)
{-| -}
import Animator
import Browser
import Color
import Html exposing (..)
import Html.Attributes as Attr
import Html.Events as Events
import Time
type alias Model =
{}
main =
Browser.sandbox
{ init =
{}
, view = view
, update = update
}
type Msg
= Check Bool
update : Msg -> Model -> Model
update msg model =
case msg of
Check newChecked ->
model
view : Model -> Html Msg
view model =
div
[ Attr.class "root"
]
[ stylesheet
, box (Animator.spinning (Animator.ms 2000))
, box (Animator.spinning (Animator.ms 1500))
, box (Animator.spinning (Animator.ms 1000))
]
box animation =
Animator.div animation
[ Attr.class "box"
]
[]
stylesheet : Html msg
stylesheet =
Html.node "style"
[]
[ text """@import url('https://fonts.googleapis.com/css?family=Roboto&display=swap');
.root {
display: flex;
}
.root > * {
margin-right: 20px;
}
.box {
box-sizing: border-box;
position: relative;
width: 100px;
height: 100px;
border-radius: 20%;
background-color:red;
border-color: black;
border-style: solid;
}
"""
]

View File

@ -1,251 +0,0 @@
module Checkbox exposing (main)
{-| As you've probably figured out, this is a checkbox example!
The main purpose of this example is to cover the basics of Elm Animator.
You should checkout these places in the code, which are marked so you can search for them.
It looks like a lot, but much of it is only needed once per project!
- (1) - Instead of a `Bool`, we store an `Animator Bool` in our model.
- (2) - The `Animator Model`, which is the piece of code that knows how to update your model when a timeline changes.
- (3) - Start a timeline by using `Animator.init`
- (4) - Turning out `Timeline` into a subscription using `Animator.toSubscription`.
- (5) - Updating our model using our animator and the current time.
- (6) - Starting an animation
- (7) - Turning our timeline into inline styles.
-}
import Animator
import Animator.Inline
import Browser
import Color
import Html exposing (..)
import Html.Attributes as Attr
import Html.Events as Events
import Time
{-| (1) - In our model we'd normally store just a `Bool`.
However now we have an `Animator.Timeline Bool`
-}
type alias Model =
{ checked : Animator.Timeline Bool
}
{-| (2) - The first thing we do is create an `Animator Model`.
It's job is to reach into our model and update our timelines when they need to be updated
Notice you could add any number of timelines to this animator.
**Note:** You likely only need one animator for a given project.
-}
animator : Animator.Animator Model
animator =
Animator.animator
|> Animator.watching
-- we tell the animator how
-- to get the checked timeline using .checked
.checked
-- and we tell the animator how
-- to update that timeline as well
(\newChecked model ->
{ model | checked = newChecked }
)
main =
Browser.document
{ init =
\() ->
-- (3) - How we create our timeline
( { checked = Animator.init False
}
, Cmd.none
)
, view = view
, update = update
, subscriptions =
\model ->
-- (4) - turning out Animator into a subscription
-- this is where the animator will decide to have a subscription to AnimationFrame or not.
animator
|> Animator.toSubscription Tick model
}
type Msg
= Tick Time.Posix
| Check Bool
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Tick newTime ->
( model
|> Animator.update newTime animator
-- (5) - Updating our model using our animator and the current time.
, Cmd.none
)
Check newChecked ->
( { model
| checked =
-- (6) - Here we're adding a new state to our timeline.
model.checked
|> Animator.go Animator.slowly newChecked
}
, Cmd.none
)
view : Model -> Browser.Document Msg
view model =
{ title = "Animator - Checkbox"
, body =
[ stylesheet
, div
[ Attr.class "root"
]
[ div
[ Attr.class "viewport"
]
[ viewHugeCheckbox model.checked
]
]
]
}
viewHugeCheckbox : Animator.Timeline Bool -> Html Msg
viewHugeCheckbox checked =
div
[ Attr.style "display" "flex"
, Attr.style "align-items" "center"
, Attr.style "flex-direction" "column"
]
[ div
[ Attr.style "display" "flex"
, Attr.style "align-items" "center"
, Attr.style "cursor" "pointer"
, Events.onClick (Check (not (Animator.current checked)))
]
[ div
-- (7) - Rendering our timeline as inline styles.
-- What we're doing here is mapping our timeline states
-- to what values they should be in the view.
-- Elm animator then uses these to interpolate where we should be.
[ Animator.Inline.backgroundColor checked <|
\state ->
if state then
Color.rgb255 255 96 96
else
Color.white
, Animator.Inline.borderColor checked <|
\state ->
if state then
Color.rgb255 255 96 96
else
Color.black
, Attr.class "checkbox"
]
[ div
[ Animator.Inline.opacity checked <|
\state ->
if state then
Animator.at 1
else
Animator.at 0
, Animator.Inline.transform
{ position = { x = 0, y = 0 }
, rotate =
Animator.move checked <|
\state ->
if state then
Animator.at (turns 0)
else
Animator.at (turns 0.05)
, scale =
Animator.move checked <|
\state ->
if state then
Animator.at 1
else
Animator.at 0.8
}
]
[ text "!" ]
]
, span
[ Attr.style "margin-left" "32px"
, Attr.style "font-size" "190px"
]
[ text "Click me" ]
]
, div
[ Animator.Inline.opacity checked <|
\state ->
if state then
Animator.at 1
else
Animator.at 0
]
[ text "Great job "
, span
[ Attr.style "display" "inline-block"
]
[ text "👍" ]
]
]
stylesheet : Html msg
stylesheet =
Html.node "style"
[]
[ text """@import url('https://fonts.googleapis.com/css?family=Roboto&display=swap');
.root {
width: 100%;
height: 1000px;
font-size: 48px;
user-select: none;
padding: 50px;
font-family: 'Roboto', sans-serif;
}
.viewport {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 200px;
}
.checkbox {
border-width: 10px;
border-style: solid;
color: #000;
width: 160px;
height: 160px;
border-radius: 20px;
font-size: 160px;
line-height: 1.0;
text-align: center;
}
"""
]

View File

@ -1,530 +0,0 @@
module Loading exposing (main)
{-| Animating loading states!
It's pretty common to have a type, usually called RemoteData, to represent a piece of data that's been requested from the server.
It generally looks something like this:
type RemoteData error data
= NotAsked
| Loading
| Failure error
| Success data
This example will show you how to:
1. Animate a "resting" state, so that when we're at a state of `Loading`, a a loading animation is occurring.
2. Animate content that's already been deleted! In this case we can show the previously retrieved comment, even whil we're in a loading state.
3. How to debug animations with `Animator.Css.explain`. This will show you a bounding box for an element, as well as it's center of rotation.
Search for (1), (2), and (3) respectively to see the code!
-}
import Animator
import Animator.Css
import Browser
import Color
import Html exposing (..)
import Html.Attributes as Attr
import Html.Events as Events
import Process
import Task
import Time
type RemoteData error data
= NotAsked
| Loading
| Failure error
| Success data
type alias Model =
{ comment :
Animator.Timeline (RemoteData String String)
}
main =
Browser.document
{ init =
\() ->
( { comment =
Animator.init NotAsked
}
, Cmd.none
)
, view = view
, update = update
, subscriptions =
\model ->
animator
|> Animator.toSubscription Tick model
}
{-| -}
animator : Animator.Animator Model
animator =
Animator.animator
-- *NOTE* We're using `the Animator.Css.watching` instead of `Animator.watching`.
-- Instead of asking for a constant stream of animation frames, it'll only ask for one
-- and we'll render the entire css animation in that frame.
|> Animator.Css.watching
.comment
(\newComment model ->
{ model | comment = newComment }
)
type Msg
= Tick Time.Posix
| AskServerForNewComment
| NewCommentFromServer String
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Tick newTime ->
( model
|> Animator.update newTime animator
, Cmd.none
)
AskServerForNewComment ->
( { model
| comment =
model.comment
|> Animator.go Animator.quickly Loading
}
, Task.perform (always (NewCommentFromServer "Howdy partner!"))
(Process.sleep (2 * 1000))
)
NewCommentFromServer comment ->
( { model
| comment =
model.comment
|> Animator.go Animator.quickly (Success comment)
}
, Cmd.none
)
view : Model -> Browser.Document Msg
view model =
{ title = "Animator - Loading"
, body =
[ stylesheet
, div [ Attr.class "root" ]
[ div
[ Attr.class "viewport"
]
[ viewComment model.comment
, Html.div
[ Attr.style "display" "flex"
, Attr.style "flex-direction" "row"
, Attr.style "justify-content" "space-between"
, Attr.style "width" "100%"
]
[ Html.button
[ Attr.class "button"
, Events.onClick AskServerForNewComment
]
[ Html.text "Load comment" ]
]
]
]
]
}
viewComment : Animator.Timeline (RemoteData String String) -> Html Msg
viewComment comment =
Animator.Css.div comment
[ Animator.Css.backgroundColor <|
\state ->
case state of
Loading ->
Color.rgb 0.9 0.9 0.9
_ ->
Color.rgb 1 1 1
, Animator.Css.fontColor <|
\state ->
case state of
Loading ->
Color.rgb 0.5 0.5 0.5
_ ->
Color.rgb 0 0 0
, Animator.Css.borderColor <|
\state ->
case state of
Loading ->
Color.rgb 0.5 0.5 0.5
_ ->
Color.rgb 0 0 0
]
[ Attr.style "position" "relative"
, Attr.style "display" "flex"
, Attr.style "flex-direction" "column"
, Attr.style "justify-content" "space-between"
, Attr.style "font-size" "16px"
, Attr.style "width" "100%"
, Attr.style "height" "100px"
, Attr.style "padding" "24px"
, Attr.style "box-sizing" "border-box"
, Attr.style "border" "1px solid black"
, Attr.style "border-radius" "3px"
]
[ loadingIndicator comment
, case Animator.current comment of
NotAsked ->
Html.div [ Attr.style "color" "#CCC" ] [ Html.text "No comment loaded" ]
Loading ->
Html.div []
-- (2) - We can still show the previous comment!
-- If we're loading a new one, we'll grey it out.
[ case Animator.previous comment of
Success text ->
Html.div
[ Attr.style "display" "flex"
, Attr.style "flex-direction" "row"
, Attr.style "filter" "grayscale(1)"
]
[ viewThinking
, Html.text text
]
_ ->
Html.div [] [ Html.text "Loading..." ]
]
Failure error ->
Html.div [] [ Html.text error ]
Success text ->
Html.div
[ Attr.style "display" "flex"
, Attr.style "flex-direction" "row"
]
[ viewCowboy
-- (3) - This `debug` field will turn on `Animator.Css.explain` for the elements animated in the `viewCowboy` function.
-- See the other note labeled (3) to get a better idea of what that means.
{ debug = False
, timeline = comment
}
, Html.text text
]
]
loadingIndicator : Animator.Timeline (RemoteData error success) -> Html msg
loadingIndicator loading =
Html.div
[ Attr.class "loading-indicator"
]
[ viewBlinkingCircle 0.2 loading
, viewBlinkingCircle 0.3 loading
, viewBlinkingCircle 0.4 loading
]
viewBlinkingCircle offset timeline =
Animator.Css.div timeline
[ Animator.Css.opacity <|
\state ->
case state of
Loading ->
-- (1) - When we're loading, use a sine wave to oscillate between 0.05 and 1
-- Becase we want multiple blinking circles, we can als "shift" this wave over by some amount.
-- A shift of 0 means the value starts at 0.05.
-- A shift of 1 would mean the value starts at the "end", at 1.
-- Then we tell it to take 700ms for a full loop
Animator.wave 0.05 1
|> Animator.shift offset
|> Animator.loop (Animator.millis 700)
_ ->
Animator.at 0
]
[ Attr.class "circle"
]
[]
viewThinking : Html msg
viewThinking =
Html.div
[ Attr.class "thinking"
]
[ Html.text "\u{1F914}"
]
viewCowboy : { debug : Bool, timeline : Animator.Timeline (RemoteData error value) } -> Html msg
viewCowboy { debug, timeline } =
Html.div [ Attr.class "cowboy" ]
[ Animator.Css.div timeline
-- (3) - Animator.Css.explain True will show you
-- -> the bounding box for this element
-- -> the center of rotation, which you can change by setting the origin with transformWith
-- -> The coordinate system for this element.
[ Animator.Css.explain debug
, Animator.Css.opacity <|
\state ->
case state of
Success _ ->
Animator.at 1
_ ->
Animator.at 0
, Animator.Css.transformWith
{ rotationAxis =
{ x = 0
, y = 0
, z = 1
}
, origin =
Animator.Css.offset
-5
3
}
<|
\state ->
case state of
NotAsked ->
Animator.Css.xy
{ x = 0
, y = 0
}
Loading ->
Animator.Css.rotating (Animator.seconds 5)
Failure error ->
Animator.Css.xy
{ x = 0
, y = 0
}
Success text ->
fingerGuns 0.5
]
[]
[ Html.text "👉"
]
, Animator.Css.div timeline
[ Animator.Css.explain debug
, Animator.Css.transformWith
{ rotationAxis =
{ x = 0
, y = 0
, z = 1
}
, origin =
Animator.Css.offset
0
0
}
<|
\state ->
case state of
NotAsked ->
Animator.Css.xy
{ x = 0
, y = 0
}
Loading ->
Animator.Css.rotating (Animator.seconds 5)
Failure error ->
Animator.Css.xy
{ x = 0
, y = 0
}
Success text ->
Animator.Css.xy
{ x = 0
, y = 0
}
|> Animator.Css.lookAt
{ x = 0.7
, y = 0.2
, z = 0.8
}
]
[]
[ case Animator.current timeline of
NotAsked ->
Html.text ""
Loading ->
-- thinking face
Html.text "\u{1F914}"
Failure error ->
-- sad face
Html.text "😞"
Success text ->
-- cowboy
Html.text "\u{1F920}"
]
, Animator.Css.div timeline
[ Animator.Css.explain debug
, Animator.Css.opacity <|
\state ->
case state of
Success _ ->
Animator.at 1
_ ->
Animator.at 0
, Animator.Css.transformWith
{ rotationAxis =
{ x = 0
, y = 0
, z = 1
}
, origin =
Animator.Css.offset
-5
5
}
<|
\state ->
case state of
NotAsked ->
Animator.Css.xy
{ x = 0
, y = 0
}
Loading ->
Animator.Css.rotating (Animator.seconds 5)
Failure error ->
Animator.Css.xy
{ x = 0
, y = 0
}
Success text ->
fingerGuns 0
]
[]
[ Html.div
-- this is an adjustment to get this hand to point in the desired direction.
[ Attr.style "transform" "translateY(3px) scaleX(-1) rotate(270deg)"
]
[ Html.text "" ]
]
]
fingerGuns : Float -> Animator.Css.Transform
fingerGuns offset =
Animator.Css.in2d
(Animator.Css.repeat 3 (Animator.millis 300))
{ x = Animator.Css.resting 0
, y = Animator.Css.resting 0
, rotate =
Animator.zigzag (turns 0) (turns -0.1)
|> Animator.shift offset
, scaleX = Animator.Css.resting 1
, scaleY = Animator.Css.resting 1
}
{- STYLESHEET -}
pink : String
pink =
"rgb(240, 0, 245)"
stylesheet : Html msg
stylesheet =
Html.node "style"
[]
[ text """@import url('https://fonts.googleapis.com/css?family=Roboto&display=swap');
.circle {
width: 12px;
height: 12px;
border-radius: 6px;
background-color: rgb(240, 0, 245);
margin-right: 8px;
}
.thinking {
display: flex;
flex-direction: center;
transform: translateY(-6px);
width: 72px;
font-size: 24px;
margin-right: 12px;
justify-content: center;
}
.loading-indicator {
display: flex;
width: 100%;
height: 100%;
left: 0;
top: 0;
position: absolute;
box-sizing: border-box;
flex-direction: row;
justify-content: center;
align-items: center;
}
.root {
width: 100%;
font-size: 48px;
user-select: none;
padding: 50px;
box-sizing: border-box;
font-family: 'Roboto', sans-serif;
padding: 200px;
}
.viewport {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
width: 400px;
}
.button {
background-color: rgb(240, 0, 245);
padding: 8px 12px;
border: none;
border-radius: 2px;
color: white;
cursor: pointer;
margin-top: 20px;
}
.cowboy {
display: flex;
transform: translateY(-6px);
width: 72px;
font-size: 24px;
margin-right: 12px;
}
"""
]

View File

@ -1,804 +0,0 @@
module Mario exposing (main)
{-|
# Run around as Mario!
This example is primarily here to show how the sprite system works.
Essentially we're going to be animating like you would with a flip book!
But we're also going to be doing some physics calculations directly ourselves, so this example shows how you'd use `elm-animator` side-by-side with other manual animations if you need to.
(1) **Sprite Sheet** - A sprite sheet is single image file with a bunch of images at different positions in the file. [Here's the one we're using](https://github.com/mdgriffith/elm-animator/blob/master/examples/images/mario-sprites.png)
To animate this, we need to pick out our subimages from our image file by knowing their bounding boxes.
(2) **Animating the flipbook** - Here's the new place where we're animating Mario'd state.
-}
import Animator
import Browser
import Browser.Dom
import Browser.Events
import Html exposing (Html)
import Html.Attributes as Attr
import Json.Decode as Decode
import Task
import Time
{--}
type alias Model =
{ x : Float
, y : Float
, vx : Float
, vy : Float
, window : Window
, gamepad : GamePad
, mario : Animator.Timeline Mario
}
type alias GamePad =
{ left : Pressed
, right : Pressed
, jump : Pressed
, run : Pressed
, duck : Pressed
}
type Pressed
= NotPressed
| StartPressed
| HeldFor Milliseconds
type alias Milliseconds =
Float
type alias Keys =
{ x : Int
, y : Int
}
type Arrow
= ArrowDown
| ArrowUp
| ArrowLeft
| ArrowRight
type alias Window =
{ width : Int
, height : Int
}
type Mario
= Mario Action Direction
type Action
= Running
| Walking
| Standing
| Ducking
| Jumping
type Direction
= Left
| Right
type Msg
= Tick Time.Posix
| Frame Float
| Pressed Button
| Released Button
| WindowSize Int Int
type Button
= GoLeft
| GoRight
| Duck
| Jump
| Run
main =
Browser.document
{ init =
\() ->
( init
, Browser.Dom.getViewport
|> Task.attempt
(\viewportResult ->
case viewportResult of
Ok viewport ->
WindowSize
(round viewport.scene.width)
(round viewport.scene.height)
Err err ->
WindowSize
(round 800)
(round 600)
)
)
, view = view
, update = update
, subscriptions = subscriptions
}
init : Model
init =
{ x = 0
, y = 0
, vx = 0
, vy = 0
, window = { width = 800, height = 500 }
, gamepad =
{ left = NotPressed
, right = NotPressed
, jump = NotPressed
, run = NotPressed
, duck = NotPressed
}
, mario = Animator.init (Mario Standing Right)
}
update : Msg -> Model -> ( Model, Cmd Msg )
update msg mario =
case msg of
Tick newTime ->
( mario
|> Animator.update newTime animator
, Cmd.none
)
Frame dt ->
( mario
|> holdButtons dt
|> gravity (dt / 10)
|> jump mario.gamepad
|> walk mario.gamepad
|> physics (dt / 10)
|> updateSprites
, Cmd.none
)
Pressed button ->
( { mario
| gamepad =
applyButtonToGamepad button True mario.gamepad
}
, Cmd.none
)
Released button ->
( { mario
| gamepad =
applyButtonToGamepad button False mario.gamepad
}
, Cmd.none
)
WindowSize width height ->
( { mario
| window =
{ width = width
, height = height
}
}
, Cmd.none
)
{-| -}
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ animator
|> Animator.toSubscription Tick model
, Browser.Events.onResize WindowSize
, Browser.Events.onKeyDown (Decode.map Pressed decodeButton)
, Browser.Events.onKeyUp (Decode.map Released decodeButton)
, Browser.Events.onAnimationFrameDelta Frame
]
animator : Animator.Animator Model
animator =
Animator.animator
-- we tell the animator how to get the checked timeline using .checked
-- and we tell the animator how to update that timeline with updateChecked
|> Animator.watching .mario (\mario m -> { m | mario = mario })
decodeButton : Decode.Decoder Button
decodeButton =
Decode.andThen toButton
(Decode.field "key" Decode.string)
toButton : String -> Decode.Decoder Button
toButton string =
case string of
"ArrowLeft" ->
Decode.succeed GoLeft
"ArrowRight" ->
Decode.succeed GoRight
" " ->
Decode.succeed Jump
"ArrowDown" ->
Decode.succeed Duck
"Shift" ->
Decode.succeed Run
_ ->
Decode.fail "Skip"
holdButtons : Float -> Model -> Model
holdButtons dt model =
let
gamepad =
model.gamepad
in
{ model
| gamepad = holdButtonsOnGamepad dt gamepad
}
holdButtonsOnGamepad dt gamepad =
{ left = hold dt gamepad.left
, right = hold dt gamepad.right
, jump = hold dt gamepad.jump
, run = hold dt gamepad.run
, duck = hold dt gamepad.duck
}
hold dt pressed =
case pressed of
NotPressed ->
NotPressed
StartPressed ->
HeldFor dt
HeldFor existingDt ->
HeldFor (existingDt + dt)
applyButtonToGamepad : Button -> Bool -> GamePad -> GamePad
applyButtonToGamepad button pressed gamepad =
case button of
GoLeft ->
{ gamepad
| left =
if pressed then
StartPressed
else
NotPressed
}
GoRight ->
{ gamepad
| right =
if pressed then
StartPressed
else
NotPressed
}
Duck ->
{ gamepad
| duck =
if pressed then
StartPressed
else
NotPressed
}
Jump ->
{ gamepad
| jump =
if pressed then
StartPressed
else
NotPressed
}
Run ->
{ gamepad
| run =
if pressed then
StartPressed
else
NotPressed
}
view : Model -> Browser.Document Msg
view model =
{ title = "Mario - Elm Animator"
, body =
[ stylesheet
, Html.div
[ Attr.style "position" "fixed"
, Attr.style "left" "0"
, Attr.style "top" "0"
, Attr.style "width" (String.fromInt model.window.width ++ "px")
, Attr.style "height" (String.fromInt model.window.height ++ "px")
]
[ Html.div
[ Attr.style "position" "absolute"
, Attr.style "top" "80px"
, Attr.style "left" "80px"
, Attr.style "user-select" "none"
, Attr.style "font-family" "'Roboto', sans-serif"
]
[ Html.h1 [] [ Html.text "Mario" ]
, Html.div [] [ Html.text "Arrows to move, shift to run, space to jump!" ]
]
, Html.div
[ Attr.class "positioner"
, Attr.style "position" "absolute"
, Attr.style "top" (String.fromFloat ((toFloat model.window.height / 2) - model.y) ++ "px")
, Attr.style "left" (String.fromFloat model.x ++ "px")
]
-- (2) - Animating Mario's state with sprites
-- We're watching te model.mario timeline, which has both a direction and an action that mario is currently doing.
--
[ viewSprite
(Animator.step model.mario <|
\(Mario action direction) ->
let
-- this is where we decide to show the left or the right sprite.
--
frame mySprite =
case direction of
Left ->
Animator.frame mySprite
Right ->
Animator.frame { mySprite | flipX = True }
in
case action of
-- for these first three states, we only have a single frame we care about.
Standing ->
frame sprite.tail.stand
Ducking ->
frame sprite.tail.duck
Jumping ->
frame sprite.tail.jump
Walking ->
-- when we're in a `Walking` state, we want to cycle through 3 frames.
-- And we can also specify our frames per secton
Animator.framesWith
-- `transition` are the frames we'd want to take when transitioning to this state.
{ transition = frame sprite.tail.stand
-- `resting` is what we want to do while we're in this state.
, resting =
Animator.cycle
(Animator.fps 15)
[ frame sprite.tail.step1
, frame sprite.tail.step2
, frame sprite.tail.stand
]
}
Running ->
-- In order to make mario go faster, we're upping the fps
-- and we're also changing the frames so that he puts his arms out.
Animator.framesWith
{ transition = frame sprite.tail.standArms
, resting =
Animator.cycle
(Animator.fps 30)
[ frame sprite.tail.runStep1
, frame sprite.tail.runStep2
, frame sprite.tail.standArms
]
}
)
]
]
]
}
viewSprite : Box -> Html msg
viewSprite box =
Html.div []
[ Html.div
[ Attr.style "position" "absolute"
, Attr.style "top" (String.fromInt box.adjustY ++ "px")
, Attr.style "left" (String.fromInt box.adjustX ++ "px")
, Attr.style "width" (String.fromInt box.width ++ "px")
, Attr.style "height" (String.fromInt box.height ++ "px")
, Attr.style "background-image" "url('http://mdgriffith.github.io/elm-animator/images/mario-sprites.png')"
, Attr.style "background-repeat" "no-repeat"
, Attr.style "transform-origin" "30% 50%"
, Attr.style "transform"
(if box.flipX then
"scaleX(-1) scale(2)"
else
"scaleX(1) scale(2)"
)
, Attr.style "background-position"
("-"
++ (String.fromInt box.x ++ "px -")
++ (String.fromInt box.y ++ "px")
)
-- we need to tell the browser to render our image and leave the pixels pixelated.
, Attr.class "pixel-art"
]
[]
]
isHeld : Pressed -> Bool
isHeld pressed =
case pressed of
NotPressed ->
False
StartPressed ->
True
HeldFor _ ->
True
walk : GamePad -> Model -> Model
walk pad mario =
let
run yes x =
if yes then
x * 2.0
else
x
newVx =
if isHeld pad.left && isHeld pad.right then
0
else if isHeld pad.left then
run (isHeld pad.run) -1.8
else if isHeld pad.right then
run (isHeld pad.run) 1.8
else
0
in
{ mario
| vx = newVx
}
jump : GamePad -> Model -> Model
jump pad mario =
if pad.jump == StartPressed && mario.vy == 0 then
{ mario | vy = 6.0 }
else
mario
gravity : Float -> Model -> Model
gravity dt mario =
{ mario
| vy =
if mario.y > 0 then
mario.vy - dt / 4
else
0
}
physics : Float -> Model -> Model
physics dt mario =
{ mario
| x =
(mario.x + dt * mario.vx)
|> min (toFloat mario.window.width - 40)
|> max 0
, y = max 0 (mario.y + dt * mario.vy)
}
updateSprites : Model -> Model
updateSprites model =
let
current =
Animator.current model.mario
direction =
if model.vx > 0 then
Right
else if model.vx < 0 then
Left
else
case current of
Mario _ currentDirection ->
currentDirection
action =
if model.y /= 0 then
Jumping
else if model.vx /= 0 then
if abs model.vx > 2 then
Running
else
Walking
else if isHeld model.gamepad.duck then
Ducking
else
Standing
newMario =
Mario action direction
in
if current /= newMario then
{ model
| mario =
model.mario
|> Animator.go Animator.immediately newMario
}
else
model
{- (1) - Sprite Sheet
x, y -> the coordinates of the image on the sprite sheet
width, height -> the size of the image I want
adjustX, adjustY -> adjustX and adjustY move the position of the rendered image so that we can line it up with the previous frames.
flipX, flipY -> The sprite sheet only shows mario looking in one direction. Though we can flip that image if we need to!
-}
type alias Box =
{ x : Int
, y : Int
, width : Int
, height : Int
, adjustX : Int
, adjustY : Int
, flipX : Bool
, flipY : Bool
}
sprite =
{ tail =
{ stand =
{ x = 0
, y = 240
, width = 27
, height = 30
, adjustX = 4
, adjustY = 0
, flipX = False
, flipY = False
}
, step1 =
{ x = 30
, y = 240
, width = 27
, height = 30
, adjustX = 3
, adjustY = 0
, flipX = False
, flipY = False
}
, step2 =
{ x = 60
, y = 240
, width = 27
, height = 30
, adjustX = 4
, adjustY = 0
, flipX = False
, flipY = False
}
, jump =
{ x = 90
, y = 240
, width = 27
, height = 30
, adjustX = 4
, adjustY = 0
, flipX = False
, flipY = False
}
, duck =
{ x = 120
, y = 235
, width = 27
, height = 30
, adjustX = 5
, adjustY = 0
, flipX = False
, flipY = False
}
, pivot =
{ x = 150
, y = 240
, width = 27
, height = 30
, adjustX = 0
, adjustY = 0
, flipX = False
, flipY = False
}
, kick =
{ x = 180
, y = 240
, width = 27
, height = 30
, adjustX = -1
, adjustY = 0
, flipX = False
, flipY = False
}
, bum =
{ x = 208
, y = 239
, width = 20
, height = 30
, adjustX = 3
, adjustY = 0
, flipX = False
, flipY = False
}
, standArms =
{ x = 0
, y = 280
, width = 25
, height = 30
, adjustX = 4
, adjustY = 0
, flipX = False
, flipY = False
}
, runStep1 =
{ x = 25
, y = 280
, width = 27
, height = 30
, adjustX = 3
, adjustY = 0
, flipX = False
, flipY = False
}
, runStep2 =
{ x = 52
, y = 280
, width = 25
, height = 30
, adjustX = 4
, adjustY = 0
, flipX = False
, flipY = False
}
, runJump1 =
{ x = 329
, y = 280
, width = 27
, height = 30
, adjustX = 3
, adjustY = 0
, flipX = False
, flipY = False
}
, runJump2 =
{ x = 359
, y = 280
, width = 27
, height = 30
, adjustX = 3
, adjustY = 0
, flipX = False
, flipY = False
}
, runJump3 =
{ x = 389
, y = 280
, width = 27
, height = 30
, adjustX = 3
, adjustY = 0
, flipX = False
, flipY = False
}
, fall1 =
{ x = 268
, y = 280
, width = 27
, height = 30
, adjustX = 3
, adjustY = 0
, flipX = False
, flipY = False
}
, fall2 =
{ x = 300
, y = 280
, width = 27
, height = 30
, adjustX = 3
, adjustY = 0
, flipX = False
, flipY = False
}
}
}
stylesheet : Html msg
stylesheet =
Html.node "style"
[]
[ Html.text """@import url('https://fonts.googleapis.com/css?family=Roboto&display=swap');
body, html {
margin: 0;
padding:0;
border:0;
display:block;
position: relative;
width: 100%;
height: 100%;
}
.pixel-art {
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
}
"""
]

View File

@ -1,28 +0,0 @@
# Examples
Hello and welcome to `elm-animator`!
Your first step is to read the high-level overview of how this library works in the `README.md`.
Then, we can dig in to some examples.
**Note** if you clone this library to play with the code locally, make sure to `cd examples` and run `elm make` from there!
1. [**Checkbox**](https://github.com/mdgriffith/elm-animator/blob/master/examples/Checkbox.elm) - Animate a checkbox as it's checked. It covers:
- *How to **get started** with `elm-animator`*
- *An idea of how to **incrementally upgrade** existing code to use animations*
2. [**Page transitions**](https://github.com/mdgriffith/elm-animator/blob/master/examples/Pages.elm) - Transition between pages 3d page transition with routing.
- *Set up routing so there are no page **reloads*** (actually this is standard for SPAs in general).
- *Animate the transition between pages*
- How to do _**CSS Keyframe** generation_
3. [**Loading**](https://github.com/mdgriffith/elm-animator/blob/master/examples/Loading.elm) - Animate the loading state of a piece of content.
- *How to animate **resting** states such as a loading spinner*
- _Animate with content that's already been **deleted**_ (whaaaa?!)
4. [**Mario**](https://github.com/mdgriffith/elm-animator/blob/master/examples/Mario.elm) - The classic Mario example! Run around and jump.
- *How to get started with **sprite animation***
- *How to interact with a separate animation system such as physics code!*

View File

@ -1,359 +0,0 @@
module Pages exposing (main)
{-| Animated page transitions!
This example is meant to show a few things.
1. That page transitions are just like animating any other state, we'll just create an `Animator.Timeline Page` and animate with that.
2. How to use CSS keyframes by using the `Animator.Css` module
3. How to handle routing so that the url changes as your transition.
-}
import Animator
import Animator.Css
import Animator.Inline
import Browser
import Browser.Events
import Browser.Navigation
import Color
import Html exposing (..)
import Html.Attributes as Attr
import Html.Events as Events
import Time
import Url
import Url.Builder
import Url.Parser exposing ((</>))
{-| -}
type alias Model =
{ page : Animator.Timeline Page
, navKey : Browser.Navigation.Key
, needsUpdate : Bool
}
main =
Browser.application
{ init =
\() url navKey ->
let
initialPage =
Url.Parser.parse urlParser url
|> Maybe.withDefault NotFound
in
( { page = Animator.init initialPage
, navKey = navKey
, needsUpdate = False
}
, Cmd.none
)
, view = view
, update = update
, subscriptions =
\model ->
Sub.batch
[ animator
|> Animator.toSubscription Tick model
]
, onUrlRequest = ClickedLink
, onUrlChange = UrlChanged
}
{- URL Handling -}
type Page
= Home
| About
| Blog
| NotFound
urlParser : Url.Parser.Parser (Page -> a) a
urlParser =
Url.Parser.oneOf
[ Url.Parser.map Home Url.Parser.top
, Url.Parser.map Blog (Url.Parser.s "blog")
, Url.Parser.map About (Url.Parser.s "about")
]
pageToUrl : Page -> String
pageToUrl page =
case page of
Home ->
Url.Builder.absolute [] []
About ->
Url.Builder.absolute [ "about" ] []
Blog ->
Url.Builder.absolute [ "blog" ] []
NotFound ->
Url.Builder.absolute [ "notfound" ] []
animator : Animator.Animator Model
animator =
Animator.animator
-- *NOTE* We're using `the Animator.Css.watching` instead of `Animator.watching`.
-- Instead of asking for a constant stream of animation frames, it'll only ask for one
-- and we'll render the entire css animation in that frame.
|> Animator.Css.watching .page
(\newPage model ->
{ model | page = newPage }
)
{- UPDATING -}
type Msg
= Tick Time.Posix
| ClickedLink Browser.UrlRequest
| UrlChanged Url.Url
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Tick newTime ->
( Animator.update newTime animator model
, Cmd.none
)
ClickedLink request ->
case request of
Browser.InternalAnim url ->
-- Note - Ideally, starting a new animation with `toNewPage` would only happen in `UrlChanged`
-- which occurs immediately after this message if we use `Browser.Navigation.pushUrl`
--
-- However there seems to be a bug in elm where a subscription to animationFrame fails to fire
-- if we start a new animation just in `UrlChanged`.
-- Note, this seems to be a sepcial case with routing
( toNewPage url model
, Browser.Navigation.pushUrl model.navKey (Url.toString url)
)
Browser.External url ->
( model
, Browser.Navigation.load url
)
UrlChanged url ->
-- This should be te only place we need to use `toNewPage`. See above note.
( toNewPage url model
, Cmd.none
)
toNewPage : Url.Url -> Model -> Model
toNewPage url model =
let
newPage =
Url.Parser.parse urlParser url
|> Maybe.withDefault NotFound
in
{ model
| page =
model.page
-- full page animations involve moving some large stuff.
-- in that case using a slower duration than normal is a good place to start.
|> Animator.go Animator.verySlowly newPage
}
{- Actually viewing our pages! -}
view : Model -> Browser.Document Msg
view model =
{ title = "Animator - Page Transitions"
, body =
[ stylesheet
, div
[ Attr.class "root"
]
[ nav []
[ link Home "Home"
, link Blog "Blog"
, link About "About"
]
, div
[ Attr.class "page-row"
]
[ viewPage model.page
Home
{ title = "The home page"
, content = loremIpsum
}
, viewPage model.page
Blog
{ title = "Blog"
, content = loremIpsum
}
, viewPage model.page
About
{ title = "About"
, content = loremIpsum
}
]
]
]
}
viewPage : Animator.Timeline Page -> Page -> { title : String, content : Html msg } -> Html Msg
viewPage timeline page { title, content } =
let
wrapInLink html =
if Animator.current timeline == page then
html
else
Html.a
[ Attr.href (pageToUrl page)
, Attr.style "cursor" "pointer"
]
[ html ]
in
Animator.Css.div timeline
(pageAnimation page)
[ Attr.class "page" ]
[ Html.h2 [] [ Html.text title ]
, loremIpsum
]
|> wrapInLink
pageAnimation : Page -> List (Animator.Css.Attribute Page)
pageAnimation page =
[ Animator.Css.opacity <|
\currentPage ->
if currentPage == page then
Animator.at 1
else
Animator.at 0.4
, Animator.Css.transform <|
\currentPage ->
if currentPage == page then
Animator.Css.scale 5
else
Animator.Css.scale 1
-- Here we're animating a style that's not directly supported by elm-animator.
, Animator.Css.style "margin"
(\float ->
let
str =
String.fromFloat float
in
"0px " ++ str ++ "px"
)
(\currentPage ->
if currentPage == page then
Animator.at 1800
else
Animator.at 0
)
-- because we're zooming everywhere, we can adjust the border width so it still looks nice
-- even when the page thumbnail is small.
, Animator.Css.style "border-width"
(\float ->
String.fromFloat float ++ "px"
)
(\currentPage ->
if currentPage == page then
Animator.at 1
else
Animator.at 5
)
]
{- Less Exciting Stuff
Below here is some content and a stylesheet.
-}
loremIpsum : Html msg
loremIpsum =
Html.div []
[ Html.div []
[ Html.text "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
]
, Html.div []
[ Html.text "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of \"de Finibus Bonorum et Malorum\" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, \"Lorem ipsum dolor sit amet..\", comes from a line in section 1.10.32."
]
, Html.div []
[ Html.text "The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from \"de Finibus Bonorum et Malorum\" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham."
]
]
link : Page -> String -> Html msg
link page label =
Html.a
[ Attr.href (pageToUrl page)
, Attr.style "margin-right" "12px"
]
[ Html.text label ]
stylesheet : Html msg
stylesheet =
Html.node "style"
[]
[ text """@import url('https://fonts.googleapis.com/css?family=Roboto&display=swap');
a {
text-decoration: none;
color: black;
}
a:visited {
text-decoration: none;
color: black;
}
.root {
width: 100%;
height: 1000px;
font-size: 16px;
user-select: none;
padding: 50px;
font-family: 'Roboto', sans-serif;
}
.page-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 100px;
transform: scale(0.2);
}
.page {
width: 500px;
padding: 48px;
border: 1px solid black;
border-radius: 2px;
flex-shrink: 0;
background-color: white;
}
"""
]

View File

@ -1,7 +1,7 @@
{
"type": "application",
"source-directories": [
".",
"src",
"../src"
],
"elm-version": "0.19.1",
@ -9,33 +9,35 @@
"direct": {
"avh4/elm-color": "1.0.0",
"elm/browser": "1.0.2",
"elm/core": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
"elm/json": "1.1.3",
"elm/svg": "1.0.1",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"ianmackenzie/elm-geometry": "2.0.0",
"ianmackenzie/elm-units": "2.2.0",
"terezka/line-charts": "2.0.0"
"ianmackenzie/elm-geometry": "4.0.0",
"ianmackenzie/elm-units": "2.10.0",
"mdgriffith/elm-bezier": "1.0.0",
"terezka/line-charts": "2.0.2"
},
"indirect": {
"debois/elm-dom": "1.3.0",
"elm/parser": "1.1.0",
"elm/virtual-dom": "1.0.2",
"elm/random": "1.0.0",
"elm/virtual-dom": "1.0.3",
"ianmackenzie/elm-1d-parameter": "1.0.1",
"ianmackenzie/elm-float-extra": "1.1.0",
"ianmackenzie/elm-interval": "2.0.0",
"ianmackenzie/elm-triangular-mesh": "1.0.2",
"ianmackenzie/elm-units-interval": "1.0.0",
"justinmimbs/date": "3.2.0",
"justinmimbs/time-extra": "1.1.0",
"myrho/elm-round": "1.0.4",
"ryannhg/date-format": "2.3.0"
"ianmackenzie/elm-interval": "3.1.0",
"ianmackenzie/elm-triangular-mesh": "1.1.0",
"ianmackenzie/elm-units-interval": "3.2.0",
"justinmimbs/date": "4.1.0",
"justinmimbs/time-extra": "1.2.0",
"myrho/elm-round": "1.0.5",
"ryan-haskell/date-format": "1.0.0"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}
}

View File

@ -1,25 +0,0 @@
{
"type": "application",
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
"elm/json": "1.1.3",
"elm/svg": "1.0.1"
},
"indirect": {
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"elm/virtual-dom": "1.0.2"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}

View File

@ -1,438 +0,0 @@
module Bezier exposing (main)
{-| -}
import Browser
import Browser.Events
import Html
import Html.Attributes as Attr
import Html.Events
import Json.Decode as Decode
import Svg exposing (..)
import Svg.Attributes exposing (..)
main =
Browser.element
{ init =
\() ->
( { moving = NoMovement
, one = Point 0 0.5
, controlOne = Point 0.4 0.5
, controlTwo = Point 0.2 0
, two = Point 1 0
}
, Cmd.none
)
, view = view
, update =
update
, subscriptions =
subscriptions
}
subscriptions model =
case model.moving of
NoMovement ->
Sub.none
_ ->
Sub.batch
[ Browser.Events.onMouseUp (Decode.succeed EndMovement)
, Browser.Events.onMouseMove
(Decode.map MouseCoords
(Decode.map2
Point
(Decode.field "pageX" Decode.float)
(Decode.field "pageY" Decode.float)
)
)
]
controlOneFull =
Point 1 0.5
controlTwoFull =
Point 0 0
type Movement
= NoMovement
| Start Entity
| Moving
{ entity : Entity
, startedAt : Point
, currentlyAt : Point
}
type Entity
= Top
| Bottom
type Msg
= StartMovementForTop
| StartMovementForBottom
| MouseCoords Point
| EndMovement
| ResetTo Coords
type alias Coords =
{ one : Point
, controlOne : Point
, controlTwo : Point
, two : Point
}
linear =
{ one = Point 0 0.5
, controlOne = Point 0 0.5
, controlTwo = Point 1 0
, two = Point 1 0
}
default =
{ one = Point 0 0.5
, controlOne = Point 0.4 0.5
, controlTwo = Point 0.2 0
, two = Point 1 0
}
type alias Point =
{ x : Float
, y : Float
}
update msg model =
case msg of
StartMovementForTop ->
( { model | moving = Start Top }
, Cmd.none
)
StartMovementForBottom ->
( { model | moving = Start Bottom }
, Cmd.none
)
MouseCoords coords ->
case model.moving of
NoMovement ->
( model
, Cmd.none
)
Start entity ->
( { model
| moving =
Moving
{ entity = entity
, startedAt = coords
, currentlyAt = coords
}
}
, Cmd.none
)
Moving xy ->
( { model
| moving =
Moving { xy | currentlyAt = coords }
}
, Cmd.none
)
EndMovement ->
( { model
| moving = NoMovement
, controlOne = adjust Bottom model.moving model.controlOne
, controlTwo = adjust Top model.moving model.controlTwo
}
, Cmd.none
)
ResetTo coords ->
( { model
| one = coords.one
, controlOne = coords.controlOne
, controlTwo = coords.controlTwo
, two = coords.two
}
, Cmd.none
)
view model =
let
controlOne =
adjust Bottom model.moving model.controlOne
controlTwo =
adjust Top model.moving model.controlTwo
in
Html.div [ Attr.style "user-select" "none" ]
[ Html.div
[ Attr.style "display" "flex"
, Attr.style "flex-direction" "row"
, Attr.style "width" "600px"
, Attr.style "box-sizing" "border-box"
, Attr.style "justify-content" "center"
, Attr.style "padding-top" "50px"
]
[ Html.button
[ Attr.style "border" "2px solid black"
, Attr.style "border-radius" "3px"
, Attr.style "background-color" "white"
, Attr.style "cursor" "pointer"
, Attr.style "padding" "8px 12px"
, Html.Events.onClick (ResetTo linear)
]
[ Html.text "Linear" ]
, Html.button
[ Attr.style "border" "2px solid black"
, Attr.style "border-radius" "3px"
, Attr.style "background-color" "white"
, Attr.style "cursor" "pointer"
, Attr.style "padding" "8px 12px"
, Html.Events.onClick (ResetTo default)
, Attr.style "margin-left" "20px"
]
[ Html.text "Elm Animator Default" ]
]
, svg
[ width "800"
, height "1000"
, viewBox "-0.2 -0.2 1.8 1.8"
]
[ Svg.path
[ fill "none"
, stroke "black"
, strokeWidth "0.005"
, d (renderPath { model | controlOne = controlOne, controlTwo = controlTwo })
]
[]
, viewLine model.one controlOne
, viewLine model.two controlTwo
, viewScale model.one controlOneFull "Leave Smoothly"
|> move 0 0.15
, viewScale model.two controlTwoFull "Arrive Smoothly"
|> move 0 -0.15
, viewCircle model.one "Start"
, viewMovableCircle
{ onMouseDown = StartMovementForBottom
, isMoving = somethingMoving model.moving
, point = controlOne
, toLabel = fromFloat << .x
}
, viewCircle model.two "End"
, viewMovableCircle
{ onMouseDown = StartMovementForTop
, isMoving = somethingMoving model.moving
, point = controlTwo
, toLabel = \p -> fromFloat (1 - p.x)
}
]
]
somethingMoving moving =
case moving of
Moving _ ->
True
_ ->
False
fromFloat f =
String.fromFloat f
|> String.left 4
adjust label moving point =
case moving of
NoMovement ->
point
Start _ ->
point
Moving movement ->
if movement.entity == label then
{ x =
point.x
- (factor * (movement.startedAt.x - movement.currentlyAt.x))
|> Basics.max 0
|> Basics.min 1
, y = point.y
}
else
point
factor =
1.8 / 800
viewCircle point label =
g []
[ circle
[ cx (String.fromFloat point.x)
, cy (String.fromFloat point.y)
, r "0.02"
]
[]
, text_
[ fontSize "0.05"
, x (String.fromFloat point.x)
, y (String.fromFloat (point.y + 0.065))
, textAnchor "middle"
]
[ text label ]
]
viewMovableCircle details =
g
[ Html.Events.onMouseDown details.onMouseDown
, Attr.style "cursor"
(if details.isMoving then
"grabbing"
else
"grab"
)
]
[ circle
[ cx (String.fromFloat details.point.x)
, cy (String.fromFloat details.point.y)
, r "0.02"
, fill "red"
]
[]
, text_
[ fontSize "0.05"
, x (String.fromFloat details.point.x)
, y (String.fromFloat (details.point.y - 0.04))
, textAnchor "middle"
]
[ text (details.toLabel details.point) ]
]
viewLine one two =
line
[ x1 (String.fromFloat one.x)
, y1 (String.fromFloat one.y)
, x2 (String.fromFloat two.x)
, y2 (String.fromFloat two.y)
, stroke "black"
, strokeWidth "0.005"
, strokeDasharray "0.01 0.01"
]
[]
move x y content =
g
[ transform
("translate(" ++ String.fromFloat x ++ "," ++ String.fromFloat y ++ ")")
]
[ content ]
viewScale one two label =
g
[]
[ line
[ x1 (String.fromFloat one.x)
, y1 (String.fromFloat one.y)
, x2 (String.fromFloat two.x)
, y2 (String.fromFloat two.y)
, stroke "#EEE"
, strokeWidth "0.005"
]
[]
, viewText
{ width = 0.35
, height = 0.05
, label = label
, x = abs (two.x - one.x) / 2
, y = one.y
}
, viewText
{ width = 0.05
, height = 0.05
, label = "0"
, x = one.x
, y = one.y
}
, viewText
{ width = 0.05
, height = 0.05
, label = "1"
, x = two.x
, y = two.y
}
]
viewText details =
g []
[ rect
[ x
(String.fromFloat
(details.x
- (details.width / 2)
)
)
, y
(String.fromFloat
(details.y - (details.height / 2))
)
, width (String.fromFloat details.width)
, height (String.fromFloat details.height)
, fill "white"
]
[]
, text_
[ fontSize "0.05"
, x
(String.fromFloat
details.x
)
, y
(String.fromFloat
((details.y + (details.height / 2)) - 0.01)
)
, textAnchor "middle"
]
[ text details.label ]
]
renderPath points =
("M " ++ renderPoint points.one)
++ (" C "
++ ([ points.controlOne
, points.controlTwo
, points.two
]
|> List.map renderPoint
|> String.join " "
)
)
renderPoint point =
String.fromFloat point.x ++ "," ++ String.fromFloat point.y

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

343
examples/src/Loading.elm Normal file
View File

@ -0,0 +1,343 @@
module Loading exposing (main)
{-| Animating loading states!
It's pretty common to have a type, usually called RemoteData, to represent a piece of data that's been requested from the server.
It generally looks something like this:
type RemoteData error data
= NotAsked
| Loading
| Failure error
| Success data
This example will show you how to:
1. Animate a "resting" state, so that when we're at a state of `Loading`, a a loading animation is occurring.
2. Animate content that's already been deleted! In this case we can show the previously retrieved comment, even whil we're in a loading state.
3. How to debug animations with `Animator.Css.explain`. This will show you a bounding box for an element, as well as it's center of rotation.
Search for (1), (2), and (3) respectively to see the code!
-}
import Animator
import Animator.Timeline as Timeline
import Browser
import Browser.Events
import Color
import Html exposing (..)
import Html.Attributes as Attr
import Html.Events as Events
import Process
import Task
import Time
type RemoteData error data
= NotAsked
| Loading
| Failure error
| Success data
type alias Model =
{ comment :
Timeline.Timeline (RemoteData String String)
}
main =
Browser.document
{ init =
\() ->
( { comment =
Timeline.init NotAsked
}
, Cmd.none
)
, view = view
, update = update
, subscriptions =
\model ->
if Timeline.isRunning model.comment then
Browser.Events.onAnimationFrame Tick
else
Sub.none
}
type Msg
= Tick Time.Posix
| AskServerForNewComment
| NewCommentFromServer String
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Tick newTime ->
( { model | comment = Timeline.update newTime model.comment }
, Cmd.none
)
AskServerForNewComment ->
( { model
| comment =
model.comment
|> Timeline.to (Animator.ms 300) Loading
}
, Task.perform (always (NewCommentFromServer "Howdy partner!"))
(Process.sleep (2 * 1000))
)
NewCommentFromServer comment ->
( { model
| comment =
model.comment
|> Timeline.to (Animator.ms 300) (Success comment)
}
, Cmd.none
)
view : Model -> Browser.Document Msg
view model =
{ title = "Animator - Loading"
, body =
[ stylesheet
, div [ Attr.class "root" ]
[ div
[ Attr.class "viewport"
]
[ viewComment model.comment
, Html.div
[ Attr.style "display" "flex"
, Attr.style "flex-direction" "row"
, Attr.style "justify-content" "space-between"
, Attr.style "width" "100%"
]
[ Html.button
[ Attr.class "button"
, Events.onClick AskServerForNewComment
]
[ Html.text "Load comment" ]
]
]
]
]
}
viewComment : Timeline.Timeline (RemoteData String String) -> Html Msg
viewComment commentTimeline =
Animator.div
(Animator.onTimeline commentTimeline
(\state ->
case state of
Loading ->
[ Animator.color "background-color"
(Color.rgb 0.9 0.9 0.9)
, Animator.color "color"
(Color.rgb 0.5 0.5 0.5)
, Animator.color "border-color"
(Color.rgb 0.5 0.5 0.5)
]
_ ->
[ Animator.color "background-color"
(Color.rgb 1 1 1)
, Animator.color "color"
(Color.rgb 0 0 0)
, Animator.color "border-color"
(Color.rgb 0 0 0)
]
)
)
[ Attr.style "position" "relative"
, Attr.style "display" "flex"
, Attr.style "flex-direction" "column"
, Attr.style "justify-content" "space-between"
, Attr.style "font-size" "16px"
, Attr.style "width" "100%"
, Attr.style "height" "100px"
, Attr.style "padding" "24px"
, Attr.style "box-sizing" "border-box"
, Attr.style "border" "1px solid black"
, Attr.style "border-radius" "3px"
]
[ loadingIndicator commentTimeline
, case Timeline.current commentTimeline of
NotAsked ->
Html.div [ Attr.style "color" "#CCC" ] [ Html.text "No comment loaded" ]
Loading ->
Html.div []
-- (2) - We can still show the previous comment!
-- If we're loading a new one, we'll grey it out.
[ case Timeline.previous commentTimeline of
Success text ->
Html.div
[ Attr.style "display" "flex"
, Attr.style "flex-direction" "row"
, Attr.style "filter" "grayscale(1)"
]
[ viewThinking
, Html.text text
]
_ ->
Html.div [] [ Html.text "Loading..." ]
]
Failure error ->
Html.div [] [ Html.text error ]
Success text ->
Html.div
[ Attr.style "display" "flex"
, Attr.style "flex-direction" "row"
]
[ -- viewCowboy
-- -- (3) - This `debug` field will turn on `Animator.Css.explain` for the elements animated in the `viewCowboy` function.
-- -- See the other note labeled (3) to get a better idea of what that means.
-- { debug = False
-- , timeline = comment
-- }
-- ,
Html.text text
]
]
loadingIndicator : Timeline.Timeline (RemoteData error success) -> Html msg
loadingIndicator loading =
Html.div
[ Attr.class "loading-indicator"
]
[ viewBlinkingCircle 0.2 loading
, viewBlinkingCircle 0.3 loading
, viewBlinkingCircle 0.4 loading
]
viewBlinkingCircle offset timeline =
Animator.div
(Animator.onTimeline timeline <|
\state ->
case state of
Loading ->
-- (1) - When we're loading, use a sine wave to oscillate between 0.05 and 1
-- Becase we want multiple blinking circles, we can als "shift" this wave over by some amount.
-- A shift of 0 means the value starts at 0.05.
-- A shift of 1 would mean the value starts at the "end", at 1.
-- Then we tell it to take 700ms for a full loop
[ -- Animator.wave 0.05 1
-- |> Animator.shift offset
-- |> Animator.loop (Animator.millis 700)
Animator.opacity 1
]
_ ->
[ Animator.opacity 0
]
)
[ Attr.class "circle"
]
[]
viewThinking : Html msg
viewThinking =
Html.div
[ Attr.class "thinking"
]
[ Html.text "🤔"
]
{- STYLESHEET -}
pink : String
pink =
"rgb(240, 0, 245)"
stylesheet : Html msg
stylesheet =
Html.node "style"
[]
[ text """@import url('https://fonts.googleapis.com/css?family=Roboto&display=swap');
.circle {
width: 12px;
height: 12px;
border-radius: 6px;
background-color: rgb(240, 0, 245);
margin-right: 8px;
}
.thinking {
display: flex;
flex-direction: center;
transform: translateY(-6px);
width: 72px;
font-size: 24px;
margin-right: 12px;
justify-content: center;
}
.loading-indicator {
display: flex;
width: 100%;
height: 100%;
left: 0;
top: 0;
position: absolute;
box-sizing: border-box;
flex-direction: row;
justify-content: center;
align-items: center;
}
.root {
width: 100%;
font-size: 48px;
user-select: none;
padding: 50px;
box-sizing: border-box;
font-family: 'Roboto', sans-serif;
padding: 200px;
}
.viewport {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
width: 400px;
}
.button {
background-color: rgb(240, 0, 245);
padding: 8px 12px;
border: none;
border-radius: 2px;
color: white;
cursor: pointer;
margin-top: 20px;
}
.cowboy {
display: flex;
transform: translateY(-6px);
width: 72px;
font-size: 24px;
margin-right: 12px;
}
"""
]

View File

@ -0,0 +1,152 @@
module TimelineValue exposing (main)
import Animator
import Animator.Timeline
import Animator.Transition
import Animator.Value
import Browser
import Browser.Events
import Html
import Html.Attributes as Attr
import Html.Events
import Time
main : Program () Model Msg
main =
Browser.element
{ init = always init
, update = update
, subscriptions = subscriptions
, view = view
}
type alias Model =
{ timeline : Animator.Timeline.Timeline Float
, historyOne : List Float
, historyTwo : List Float
}
type Msg
= NewPosix Time.Posix
| Start
init : ( Model, Cmd Msg )
init =
let
initialTimeline : Animator.Timeline.Timeline Float
initialTimeline =
Animator.Timeline.init 10
queuedSteps : List (Animator.Timeline.Step Float)
queuedSteps =
[ Animator.Timeline.transitionTo (Animator.ms 1000) 100
, Animator.Timeline.transitionTo (Animator.ms 1000) 50
, Animator.Timeline.transitionTo (Animator.ms 1000) 5
]
timelineWithSteps : Animator.Timeline.Timeline Float
timelineWithSteps =
Animator.Timeline.scale 3 <| Animator.Timeline.queue queuedSteps initialTimeline
in
( { timeline = timelineWithSteps
, historyOne = []
, historyTwo = []
}
, Cmd.none
)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NewPosix posix ->
( { model
| timeline = Animator.Timeline.update posix model.timeline
, historyOne = Animator.Value.float model.timeline Animator.Value.to :: model.historyOne
, historyTwo = Animator.Value.float model.timeline (Animator.Value.withTransition Animator.Transition.linear << Animator.Value.to) :: model.historyTwo
}
, Cmd.none
)
Start ->
init
subscriptions : Model -> Sub Msg
subscriptions model =
if Animator.Timeline.isRunning model.timeline then
Browser.Events.onAnimationFrame NewPosix
else
Sub.none
view : Model -> Html.Html Msg
view model =
let
positionStandard : Float
positionStandard =
Animator.Value.float model.timeline Animator.Value.to
positionLinear : Float
positionLinear =
Animator.Value.float model.timeline (Animator.Value.withTransition Animator.Transition.linear << Animator.Value.to)
in
Html.div []
[ row [ Attr.style "width" "600px", Attr.style "justify-content" "center", Attr.style "align-items" "center" ]
[ column [ Attr.style "align-self" "flex-start", Attr.style "width" "200px", Attr.style "margin-top" "50px" ]
[ Html.div []
[ Html.div [] [ Html.text ("Position Standard: " ++ String.fromFloat positionStandard) ]
, Html.div [] [ Html.text ("Position Linear: " ++ String.fromFloat positionLinear) ]
]
, Html.button [ Html.Events.onClick Start ] [ Html.text "Re-Start" ]
]
, column [ Attr.style "width" "200px" ]
(List.map (\x -> circle [ positionX x ] []) model.historyOne)
, column [ Attr.style "width" "200px" ]
(List.map (\x -> circle [ positionX x ] []) model.historyTwo)
]
]
row : List (Html.Attribute msg) -> List (Html.Html msg) -> Html.Html msg
row attributes columns =
Html.div
(Attr.style "display" "flex"
:: Attr.style "flex-direction" "row"
:: Attr.style "gap" "10px"
:: attributes
)
columns
column : List (Html.Attribute msg) -> List (Html.Html msg) -> Html.Html msg
column attributes columns =
Html.div
(Attr.style "display" "flex"
:: Attr.style "flex-direction" "column"
:: Attr.style "gap" "10px"
:: attributes
)
columns
circle : List (Html.Attribute msg) -> List (Html.Html msg) -> Html.Html msg
circle attributes content =
Html.div
(Attr.style "width" "2px"
:: Attr.style "height" "2px"
:: Attr.style "border-radius" "50%"
:: Attr.style "background-color" "black"
:: attributes
)
content
positionX : Float -> Html.Attribute msg
positionX x =
Attr.style "transform" ("translateX(" ++ String.fromFloat x ++ "px)")

View File

@ -45,7 +45,8 @@ config =
-- }
-- ,
Docs.ReviewLinksAndSections.rule
, Docs.ReviewAtDocs.rule
-- , Docs.ReviewAtDocs.rule
, Docs.UpToDateReadmeLinks.rule
, NoConfusingPrefixOperator.rule
, NoDebug.Log.rule
@ -54,8 +55,6 @@ config =
, NoExposingEverything.rule
, NoImportingEverything.rule []
, NoMissingTypeAnnotation.rule
-- , NoMissingTypeAnnotationInLetIn.rule
, NoMissingTypeExpose.rule
, NoSimpleLetBody.rule
, NoPrematureLetComputation.rule
@ -63,7 +62,6 @@ config =
, NoUnused.CustomTypeConstructorArgs.rule
, NoUnused.Dependencies.rule
, NoUnused.Exports.rule
, NoUnused.Parameters.rule
, NoUnused.Patterns.rule
, NoUnused.Variables.rule
, Simplify.rule Simplify.defaults

View File

@ -118,16 +118,6 @@ opacity o =
InternalAnim.Css.Props.float
{-| -}
xAsSingleProp : Float -> Attribute
xAsSingleProp o =
Css.Prop
InternalAnim.Css.Props.ids.opacity
"transform"
(Move.to o)
InternalAnim.Css.Props.translateX
{-| -}
scale : Float -> Attribute
scale s =
@ -189,6 +179,7 @@ rotationAround axis n =
(InternalAnim.Css.Props.turns axis)
zAxis : { x : Float, y : Float, z : Float }
zAxis =
{ x = 0, y = 0, z = 1 }
@ -261,21 +252,6 @@ ms =
Duration.milliseconds
{-| When transitioning to this state, start with a little extra velocity!
This takes a number from 0-1.
-}
withImpulse : Float -> Attribute -> Attribute
withImpulse impulse prop =
case prop of
Css.Prop id name move format ->
Css.Prop id name (Move.withVelocities impulse 0 move) format
Css.ColorProp name move ->
Css.ColorProp name (Move.withVelocities impulse 0 move)
{-| -}
delay : Duration -> Animation -> Animation
delay dur (Animation now attrs) =
@ -340,7 +316,7 @@ keyframes steps =
[] ->
[]
(Step dur props) :: _ ->
(Step _ props) :: _ ->
props
, now = imminent
, delay = Time.zeroDuration
@ -508,12 +484,12 @@ formatColorSteps steps prop pastSteps =
Nothing ->
List.reverse pastSteps
Just (Css.Prop id name _ format) ->
Just (Css.Prop _ _ _ _) ->
formatColorSteps next
prop
pastSteps
Just (Css.ColorProp name (Move.Pos trans value _)) ->
Just (Css.ColorProp _ (Move.Pos trans value _)) ->
formatColorSteps next
prop
(Move.stepWith dur trans value :: pastSteps)
@ -534,12 +510,12 @@ formatSteps steps prop pastSteps =
Nothing ->
List.reverse pastSteps
Just (Css.Prop id name (Move.Pos trans value _) format) ->
Just (Css.Prop _ _ (Move.Pos trans value _) _) ->
formatSteps next
prop
(Move.stepWith dur trans value :: pastSteps)
Just (Css.ColorProp name movement) ->
Just (Css.ColorProp _ _) ->
formatSteps next
prop
pastSteps
@ -683,7 +659,7 @@ pinging dur =
]
{-| Animate an element on a specific timeline. Check out [`Animator.Timeline`](#Animator/Timeline) for more details.
{-| Animate an element on a specific timeline. Check out [`Animator.Timeline`](https://package.elm-lang.org/packages/mdgriffith/elm-animator/latest/Animator-Timeline) for more details.
This will

View File

@ -6,6 +6,7 @@ module Animator.Timeline exposing
, Step, wait, transitionTo
, scale, delay
, current, previous, upcoming, upcomingWith, arrived, arrivedAt, arrivedAtWith
, Duration
)
{-|
@ -408,9 +409,6 @@ update =
{-| Does this timeline have upcoming events?
**Note** this is only useful if you're not using a `Animator.Watcher`
-}
isRunning : Timeline state -> Bool
isRunning (Timeline.Timeline tl) =

View File

@ -93,11 +93,8 @@ movement : Timeline state -> (state -> Movement) -> { position : Float, velocity
movement timeline lookup =
Timeline.foldpAll lookup
Move.init
(\_ prev target now startTransition interruptedOrEnd future state ->
(\_ _ target now startTransition interruptedOrEnd future state ->
let
arrived =
Timeline.startTime target
isHappening =
Time.thisAfterOrEqualThat now startTransition
|| (List.isEmpty future
@ -106,6 +103,9 @@ movement timeline lookup =
in
if isHappening then
let
arrived =
Timeline.startTime target
progress =
Time.progress startTransition arrived now

View File

@ -1,224 +0,0 @@
module Animator.Watcher exposing
( Watching
, init, watching, list
, update, toSubscription
)
{-| If you're tracking a number of timelines in your model, you may decide to use a `Watching` to simplify adding new timelines.
@docs Watching
@docs init, watching, list
@docs update, toSubscription
-}
import Animator.Timeline exposing (Timeline)
import Browser.Events
import InternalAnim.Time as Time
import InternalAnim.Timeline as Timeline
import Time
{-| An `Animator` knows how to read and write all the `Timelines` within your `Model`.
Here's an animator from the [Checkbox.elm example](https://github.com/mdgriffith/elm-animator/blob/master/examples/Checkbox.elm),
import Animator.Watcher as Watcher
animator : Watcher.Watching Model
animator =
Watcher.init
|> Watcher.watching
-- we tell the animator how
-- to get the checked timeline using .checked
.checked
-- and we tell the animator how
-- to update that timeline as well
(\newChecked model ->
{ model | checked = newChecked }
)
Notice you could add any number of timelines to this animator:
animator : Watcher.Watching Model
animator =
Watcher.init
|> Watcher.watching
.checked
(\newChecked model ->
{ model | checked = newChecked }
)
|> Watcher.watching
.anotherChecked
(\anotherCheckboxState ->
{ model | anotherChecked = anotherCheckboxState }
)
**Note** You likely only need one animator for a given project.
**Note 2** Once we have an `Animator Model`, we have two more steps in order to set things up:
- [create a _subscription_](#toSubscription)
- [_update_ our model](#update)
-}
type alias Watching model =
Timeline.Animator model
{-| -}
init : Watching model
init =
Timeline.Animator (\_ -> { running = False, ping = Nothing }) (\now model -> model)
{-| -}
watching :
(model -> Timeline state)
-> (Timeline state -> model -> model)
-> Watching model
-> Watching model
watching get setValue (Timeline.Animator isRunning updateModel) =
Timeline.Animator
(\model ->
let
prev =
isRunning model
timeline =
get model
ping =
case Timeline.sendPing timeline of
Nothing ->
prev.ping
Just currentPing ->
case prev.ping of
Nothing ->
Just currentPing
Just prevPing ->
if prevPing.delay < currentPing.delay then
Just prevPing
else
Just currentPing
running =
if prev.running then
True
else
Timeline.hasChanged timeline
|| Timeline.justInitialized timeline
in
{ running = running
, ping = ping
}
)
(\now model ->
let
newModel =
updateModel now model
in
setValue (Timeline.update now (get newModel)) newModel
)
{-| -}
list :
(model -> List item)
-> (List item -> model -> model)
-> Watching item
-> Watching model
-> Watching model
list getItems setItems (Timeline.Animator getItemRunning updateItem) (Timeline.Animator getModelRunning updateModel) =
Timeline.Animator
(\model ->
let
modelRunning =
getModelRunning model
in
List.foldl
(\item running ->
Timeline.combineRunning
(getItemRunning item)
running
)
modelRunning
(getItems model)
)
(\now model ->
let
newModel =
updateModel now model
in
setItems
(List.map
(updateItem now)
(getItems newModel)
)
newModel
)
{-| Convert an `Animator` to a subscription.
This is where the animator will decide if a running animation needs another frame or not.
subscriptions model =
Animator.toSubscription Tick model animator
-}
toSubscription : (Time.Posix -> msg) -> model -> Watching model -> Sub msg
toSubscription toMsg model (Timeline.Animator getContext _) =
let
context =
getContext model
in
if context.running || not (context.ping == Nothing) then
Sub.batch
[ if context.running then
Browser.Events.onAnimationFrame
toMsg
else
Sub.none
, case context.ping of
Just ping ->
Time.every ping.delay
(\time ->
toMsg ping.target
)
Nothing ->
Sub.none
]
else
Sub.none
{-| When new messages come in, we then need to update our model. This looks something like this:
type Msg
= Tick Time.Posix
update msg model =
case msg of
Tick newTime ->
( Animator.update newTime animator model
, Cmd.none
)
And voilà, we can begin animating!
**Note** To animate more things, all you need to do is add a new `with` to your `Animator`.
-}
update : Time.Posix -> Watching model -> model -> model
update newTime (Timeline.Animator _ updateModel) model =
updateModel newTime model

View File

@ -1,4 +1,4 @@
module InternalAnim.Bits exposing (Bits, has, init, off, on, store4, store4Float, value, zeroes)
module InternalAnim.Bits exposing (Bits, store4Float, value)
{-| Let's make storing values within a single Int a bit easier to do while not compromising performance.
@ -24,14 +24,6 @@ type Bits bits
= Bits Int
type Size
= Flag
type Offset
= Offset
-- flip : Int -> Bool -> Bits bits -> Bits bits
-- flip offset on (Bits b) =
@ -40,43 +32,6 @@ type Offset
-- |> Bits
init : Bits bits
init =
Bits zeroes
has : Int -> Bits bits -> Bool
has offset (Bits b) =
let
target =
zeroes |> store offset 1 ones
in
Bitwise.and target b - target == 0
on : Int -> Bits bits -> Bits bits
on offset (Bits b) =
b
|> store offset 1 ones
|> Bits
off : Int -> Bits bits -> Bits bits
off offset (Bits b) =
b
|> store offset 1 zeroes
|> Bits
bool : Bool -> Int
bool yes =
if yes then
ones
else
zeroes
store4Float : Float -> Float -> Float -> Float -> Bits bits
store4Float one two three four =
Bitwise.and top8 (round one)
@ -89,28 +44,6 @@ store4Float one two three four =
|> Bits
store4 : Int -> Int -> Int -> Int -> Bits bits
store4 one two three four =
Bitwise.and top8 one
|> Bitwise.or
(Bitwise.shiftLeftBy 8 (Bitwise.and top8 two))
|> Bitwise.or
(Bitwise.shiftLeftBy 16 (Bitwise.and top8 three))
|> Bitwise.or
(Bitwise.shiftLeftBy 24 (Bitwise.and top8 four))
|> Bits
store : Int -> Int -> Int -> Int -> Int
store offset length val target =
target
get : Int -> Int -> Int -> Int
get offset length val =
val
{-| -}
ones : Int
ones =
@ -124,21 +57,6 @@ zeroes =
Bitwise.or 0 0
top10 : Int
top10 =
Bitwise.shiftRightZfBy (32 - 10) ones
top8 : Int
top8 =
Bitwise.shiftRightZfBy (32 - 8) ones
top6 : Int
top6 =
Bitwise.shiftRightZfBy (32 - 6) ones
top5 : Int
top5 =
Bitwise.shiftRightZfBy (32 - 5) ones

View File

@ -1,7 +1,6 @@
module InternalAnim.Css exposing
( Prop(..)
, RenderedProp(..)
, cssFromProps
, match
, propsToRenderedProps
, toCss
@ -9,21 +8,15 @@ module InternalAnim.Css exposing
{-| -}
import Bezier
import Color
import Html
import Html.Attributes as Attr exposing (id)
import InternalAnim.Bits as Bits
import InternalAnim.Css.Props as Props
import InternalAnim.Duration as Duration
import InternalAnim.Hash as Hash
import InternalAnim.Move as Move
import InternalAnim.Quantity as Quantity
import InternalAnim.Time as Time
import InternalAnim.Timeline as Timeline
import InternalAnim.Transition as Transition
import InternalAnim.Units as Units
import Set exposing (Set)
{-| An id representing a prop type.
@ -68,16 +61,6 @@ type Prop
| ColorProp String (Move.Move Color.Color)
isTransformProp : Prop -> Bool
isTransformProp prop =
case prop of
Prop id name _ _ ->
Props.isTransformId id
ColorProp _ _ ->
False
isGroupProp : Id -> Prop -> Bool
isGroupProp groupId prop =
case prop of
@ -162,21 +145,6 @@ toCss now renderedProps =
}
cssFromProps : Timeline.Timeline state -> (state -> List Prop) -> CssAnim
cssFromProps timeline lookup =
let
present =
getInitial timeline lookup
renderedProps =
Timeline.foldpAll lookup
(\_ -> present)
toPropCurves2
timeline
in
props2Css (Timeline.getCurrentTime timeline) renderedProps emptyAnim
getInitial : Timeline.Timeline event -> (event -> List Prop) -> List RenderedProp
getInitial timeline lookup =
let
@ -185,7 +153,7 @@ getInitial timeline lookup =
(\props ->
toInitialProps props { props = [], translation = Nothing, scale = Nothing }
)
(\get prev target now startTime endTime future cursor ->
(\get _ target _ _ _ _ cursor ->
addInitialProps (get (Timeline.getEvent target)) cursor
)
timeline
@ -205,6 +173,7 @@ addMaybeVector maybeVector renderedProps =
VectorProp vector :: renderedProps
initState : Float -> Move.State
initState x =
{ position =
Units.pixels x
@ -233,12 +202,7 @@ toInitialProps props rendered =
[] ->
rendered
(Prop id name movement format) :: remaining ->
let
state =
Move.init
(Props.default id)
in
(Prop id name _ format) :: remaining ->
toInitialProps remaining
(if Props.isTranslateId id then
case rendered.translation of
@ -279,6 +243,11 @@ toInitialProps props rendered =
rendered
else
let
state =
Move.init
(Props.default id)
in
{ props =
RenderedProp
{ id = id
@ -313,29 +282,23 @@ matchProp id renderedProp =
RenderedProp details ->
details.id - id == 0
RenderedColorProp details ->
RenderedColorProp _ ->
False
TransformProp details ->
False
VectorProp details ->
VectorProp _ ->
False
matchColor : String -> RenderedProp -> Bool
matchColor name renderedProp =
case renderedProp of
RenderedProp details ->
RenderedProp _ ->
False
RenderedColorProp details ->
details.name == name
TransformProp details ->
False
VectorProp details ->
VectorProp _ ->
False
@ -347,7 +310,7 @@ addInitialProps props rendered =
[] ->
rendered
(Prop id name movement format) :: remaining ->
(Prop id name _ format) :: remaining ->
let
new =
if Props.isTranslateId id then
@ -537,36 +500,6 @@ props2Css now renderedProps anim =
|> combine anim
)
(TransformProp details) :: remain ->
props2Css now
remain
(case details.sections of
[] ->
{ anim
| hash = transformToHash (stateToTransform details.state) ++ anim.hash
, props =
( "transform"
, renderTransformState details.state
)
:: anim.props
}
_ ->
Move.cssForSections now
(stateToTransform details.state)
"transform"
(\t one two ->
"transform: "
++ transformToString
(Move.lerpTransform t one two)
)
transformToString
transformToHash
(List.reverse details.sections)
emptyAnim
|> combine anim
)
vectorToHash : Props.Id -> Vector -> String
vectorToHash group vec =
@ -594,38 +527,6 @@ vectorToHash group vec =
++ Hash.float vec.z
transformToHash : { a | x : Float, y : Float, rotation : Float, scale : Float } -> String
transformToHash trans =
let
scaleStr =
if trans.scale == 1 then
""
else
"s" ++ String.fromInt (round (trans.scale * 100))
rotationStr =
if trans.rotation == 0 then
""
else
"r" ++ String.fromInt (round (trans.rotation * 100))
translateStr =
if trans.x == 0 && trans.y == 0 then
""
else
"t"
++ String.fromInt (round trans.x)
++ "-"
++ String.fromInt (round trans.y)
in
translateStr
++ rotationStr
++ scaleStr
vectorStateToVector : VectorState -> Vector
vectorStateToVector state =
{ x =
@ -637,99 +538,6 @@ vectorStateToVector state =
}
stateToTransform : TransformState -> Transform
stateToTransform state =
{ x =
Units.inPixels state.x.position
, y =
Units.inPixels state.y.position
, scale =
Units.inPixels state.scale.position
, rotation =
Units.inPixels state.rotation.position
}
renderTransformState :
{ a
| x : { b | position : Units.Pixels }
, y : { c | position : Units.Pixels }
, rotation : { d | position : Units.Pixels }
, scale : { e | position : Units.Pixels }
}
-> String
renderTransformState state =
"translate("
++ pixelsToString state.x.position
++ "px, "
++ pixelsToString state.y.position
++ "px) rotate("
++ pixelsToString state.rotation.position
++ "turn)"
++ " scale("
++ pixelsToString state.scale.position
++ ")"
pixelsToString : Units.Pixels -> String
pixelsToString pixels =
floatToString (Units.inPixels pixels)
floatToString : Float -> String
floatToString plusMinus =
let
float =
abs plusMinus
base =
floor float
decimal =
floor (100 * (float - toFloat base))
toString f =
if plusMinus < 0 then
"-" ++ String.fromInt f
else
String.fromInt f
in
if decimal == 0 then
toString base
else
toString base ++ "." ++ String.fromInt decimal
transformToString trans =
"translate("
++ floatToString trans.x
++ "px, "
++ floatToString trans.y
++ "px) rotate("
++ floatToString trans.rotation
++ "turn)"
++ " scale("
++ floatToString trans.scale
++ ")"
infinite : String
infinite =
"infinite"
isEmptyAnim : { css | keyframes : String } -> Bool
isEmptyAnim anim =
case anim.keyframes of
"" ->
True
_ ->
False
emptyAnim : CssAnim
emptyAnim =
{ hash = ""
@ -824,37 +632,6 @@ toPropCurves2 lookup prev target now startTime endTime future cursor =
|> lookup
|> stateOrDefault rendered.id
rendered.name
finalProp =
-- this is the check for being a transition
if not (Time.equal (Timeline.endTime prev) startTime) then
-- adjust the transition by taking into account
-- the intro and exit velocity
-- but only if this is an interruption
targetProp
|> Move.withVelocities
(normalizeVelocity
startTime
targetTime
startPosition
targetPosition
rendered.state.velocity
)
-- If we do any transition smoothing
-- we'll need to normalize this velocity too
--Estimation.velocityAtTarget lookupState target future
0
else
targetProp
startPosition =
Units.inPixels rendered.state.position
targetPosition =
case targetProp of
Move.Pos _ x _ ->
x
in
RenderedProp
{ id = rendered.id
@ -865,6 +642,39 @@ toPropCurves2 lookup prev target now startTime endTime future cursor =
rendered.sections
else
let
finalProp =
-- this is the check for being a transition
if not (Time.equal (Timeline.endTime prev) startTime) then
-- adjust the transition by taking into account
-- the intro and exit velocity
-- but only if this is an interruption
let
startPosition =
Units.inPixels rendered.state.position
targetPosition =
case targetProp of
Move.Pos _ x _ ->
x
in
targetProp
|> Move.withVelocities
(normalizeVelocity
startTime
targetTime
startPosition
targetPosition
rendered.state.velocity
)
-- If we do any transition smoothing
-- we'll need to normalize this velocity too
--Estimation.velocityAtTarget lookupState target future
0
else
targetProp
in
Move.sequences
startTime
targetTime
@ -933,11 +743,6 @@ toPropCurves2 lookup prev target now startTime endTime future cursor =
targetProps
Transition.standard
commonSequence =
getCommonVectorSequence details.group
targetProps
[]
targets =
{ x =
getVectorSlot details.group Props.X targetProps
@ -946,11 +751,6 @@ toPropCurves2 lookup prev target now startTime endTime future cursor =
, z =
getVectorSlot details.group Props.Z targetProps
}
commonMovement =
Move.move commonTransition
targets
commonSequence
in
VectorProp
{ group = details.group
@ -961,6 +761,17 @@ toPropCurves2 lookup prev target now startTime endTime future cursor =
details.sections
else
let
commonSequence =
getCommonVectorSequence details.group
targetProps
[]
commonMovement =
Move.move commonTransition
targets
commonSequence
in
Move.sequences
startTime
targetTime
@ -995,139 +806,6 @@ toPropCurves2 lookup prev target now startTime endTime future cursor =
details.state.z
}
}
TransformProp details ->
-- for each prop
-- calculate a new state
-- calculate a new transition
-- (for now), take the most "different" curve
-- Compose a new `Move Transform` with the transition
--
let
targetProps : List Prop
targetProps =
Timeline.getEvent target
|> lookup
|> List.filter isTransformProp
commonTransition =
if not (Time.equal (Timeline.endTime prev) startTime) then
let
fastestVelocity =
firstNonZero
[ normalizeVelocity
startTime
targetTime
(Units.inPixels details.state.x.position)
targets.x
details.state.x.velocity
, normalizeVelocity
startTime
targetTime
(Units.inPixels details.state.y.position)
targets.y
details.state.y.velocity
, normalizeVelocity
startTime
targetTime
(Units.inPixels details.state.rotation.position)
targets.rotation
details.state.rotation.velocity
, normalizeVelocity
startTime
targetTime
(Units.inPixels details.state.scale.position)
targets.scale
details.state.scale.velocity
]
in
getCommonTransformTransition
targetProps
Transition.standard
|> Transition.withVelocities fastestVelocity
-- If we do any transition smoothing
-- we'll need to normalize this velocity too
--Estimation.velocityAtTarget lookupState target future
0
else
getCommonTransformTransition
targetProps
Transition.standard
commonSequence =
getCommonTransformSequence
targetProps
[]
targets =
{ x =
transformOrDefault Props.ids.x
targetProps
, y =
transformOrDefault Props.ids.y
targetProps
, scale =
transformOrDefault Props.ids.scale
targetProps
, rotation =
transformOrDefault Props.ids.rotation
targetProps
}
commonMovement =
Move.move commonTransition
targets
commonSequence
in
TransformProp
{ sections =
if finished then
details.sections
else
Move.sequences
startTime
targetTime
now
endTime
commonMovement
details.sections
, state =
{ x =
Move.at progress
startTime
targetTime
(Move.toWith commonTransition
targets.x
)
details.state.x
, y =
Move.at progress
startTime
targetTime
(Move.toWith commonTransition
targets.y
)
details.state.y
, scale =
Move.at progress
startTime
targetTime
(Move.toWith commonTransition
targets.scale
)
details.state.scale
, rotation =
Move.at progress
startTime
targetTime
(Move.toWith commonTransition
targets.rotation
)
details.state.rotation
}
}
)
cursor
@ -1158,7 +836,7 @@ getCommonVectorSequence :
-> List (Move.Sequence Vector)
getCommonVectorSequence groupId props sequences =
case props of
(Prop id name (Move.Pos trans v propSeq) format) :: _ ->
(Prop _ _ (Move.Pos _ _ propSeq) _) :: _ ->
-- sequences
vectorSeq groupId props propSeq 0 []
@ -1210,7 +888,7 @@ gatherVectorSteps groupId seqLevel stepLevel steps props transforms =
[] ->
transforms
(Move.Step dur trans target) :: remainingSteps ->
(Move.Step dur trans _) :: remainingSteps ->
gatherVectorSteps groupId
seqLevel
(stepLevel + 1)
@ -1221,6 +899,7 @@ gatherVectorSteps groupId seqLevel stepLevel steps props transforms =
)
getVectorStepAt : Id -> Duration.Duration -> Transition.Transition -> Int -> Int -> List Prop -> Move.Step Vector
getVectorStepAt groupId dur trans seqLevel stepLevel props =
let
x =
@ -1262,109 +941,6 @@ getVectorStepAt groupId dur trans seqLevel stepLevel props =
{- END VECTOR -}
{-|
*warning! this need to be called with pre-filtered props that are only transform props!
-}
getCommonTransformSequence :
List Prop
-> List (Move.Sequence Transform)
-> List (Move.Sequence Transform)
getCommonTransformSequence props sequences =
case props of
(Prop id name (Move.Pos trans v propSeq) format) :: _ ->
-- sequences
transformSeq props propSeq 0 []
_ ->
sequences
transformSeq :
List Prop
-> List (Move.Sequence Float)
-> Int
-> List (Move.Sequence Transform)
-> List (Move.Sequence Transform)
transformSeq props pilotSequence seqLevel renderedTransforms =
case pilotSequence of
[] ->
renderedTransforms
(Move.Sequence n delay dur steps) :: remain ->
transformSeq props
remain
(seqLevel + 1)
(Move.Sequence n
delay
dur
(gatherSequenceSteps seqLevel
0
steps
props
[]
)
:: renderedTransforms
)
gatherSequenceSteps :
Int
-> Int
-> List (Move.Step Float)
-> List Prop
-> List (Move.Step Transform)
-> List (Move.Step Transform)
gatherSequenceSteps seqLevel stepLevel steps props transforms =
case steps of
[] ->
transforms
(Move.Step dur trans target) :: remainingSteps ->
gatherSequenceSteps seqLevel
(stepLevel + 1)
remainingSteps
props
(getTransformStepAt dur trans seqLevel stepLevel props
:: transforms
)
getTransformStepAt dur trans seqLevel stepLevel props =
Move.Step dur
trans
{ x =
getTransformSequenceValueAt seqLevel
stepLevel
Nothing
Props.ids.x
props
Nothing
, y =
getTransformSequenceValueAt seqLevel
stepLevel
Nothing
Props.ids.y
props
Nothing
, scale =
getTransformSequenceValueAt seqLevel
stepLevel
Nothing
Props.ids.scale
props
Nothing
, rotation =
getTransformSequenceValueAt seqLevel
stepLevel
Nothing
Props.ids.rotation
props
Nothing
}
getTransformSequenceValueAt : Int -> Int -> Maybe Props.Id -> Props.Id -> List Prop -> Maybe Float -> Float
getTransformSequenceValueAt seqLevel stepLevel maybeDefaultId targetId props defaultValue =
case props of
@ -1376,7 +952,7 @@ getTransformSequenceValueAt seqLevel stepLevel maybeDefaultId targetId props def
Just default ->
default
(Prop id name move _) :: remain ->
(Prop id _ move _) :: remain ->
if id - targetId == 0 then
case move of
Move.Pos _ v seq ->
@ -1420,7 +996,7 @@ getTransformSequenceValueAt seqLevel stepLevel maybeDefaultId targetId props def
else
getTransformSequenceValueAt seqLevel stepLevel maybeDefaultId targetId remain defaultValue
(ColorProp name movement) :: remain ->
(ColorProp _ _) :: remain ->
getTransformSequenceValueAt seqLevel stepLevel maybeDefaultId targetId remain defaultValue
@ -1447,7 +1023,7 @@ getCommonTransformTransition props currentTrans =
[] ->
currentTrans
(Prop id _ (Move.Pos trans _ _) _) :: remain ->
(Prop _ _ (Move.Pos trans _ _) _) :: remain ->
if Transition.isStandard trans then
getCommonTransformTransition remain currentTrans
@ -1478,7 +1054,7 @@ valueOrDefault maybeDefaultid targetId props defaultVal =
Just val ->
val
(Prop id name move _) :: remain ->
(Prop id _ move _) :: remain ->
if id - targetId == 0 then
case move of
Move.Pos _ v _ ->
@ -1498,30 +1074,10 @@ valueOrDefault maybeDefaultid targetId props defaultVal =
else
valueOrDefault maybeDefaultid targetId remain defaultVal
(ColorProp name movement) :: remain ->
(ColorProp _ _) :: remain ->
valueOrDefault maybeDefaultid targetId remain defaultVal
{-| -}
transformOrDefault : Id -> List Prop -> Float
transformOrDefault targetId props =
case props of
[] ->
Props.defaultPosition targetId
(Prop id name move _) :: remain ->
if id - targetId == 0 then
case move of
Move.Pos _ v _ ->
v
else
transformOrDefault targetId remain
(ColorProp name movement) :: remain ->
transformOrDefault targetId remain
{-| -}
stateOrDefault : Id -> String -> List Prop -> Move.Move Float
stateOrDefault targetId targetName props =
@ -1543,7 +1099,7 @@ stateOrDefault targetId targetName props =
else
stateOrDefault targetId targetName remain
(ColorProp name movement) :: remain ->
(ColorProp _ _) :: remain ->
stateOrDefault targetId targetName remain
@ -1554,7 +1110,7 @@ colorOrDefault targetName default props =
[] ->
default
(Prop id _ move _) :: remain ->
(Prop _ _ _ _) :: remain ->
colorOrDefault targetName default remain
(ColorProp name (Move.Pos _ clr _)) :: remain ->
@ -1565,30 +1121,6 @@ colorOrDefault targetName default props =
colorOrDefault targetName default remain
matchForMovement : Id -> String -> List Prop -> Maybe (Move.Move Float)
matchForMovement onlyId onlyName props =
case props of
[] ->
Nothing
(ColorProp name move) :: remain ->
matchForMovement onlyId onlyName remain
((Prop id name movement _) as top) :: remain ->
if id + 1 == 0 then
if name == onlyName then
Just movement
else
matchForMovement onlyId onlyName remain
else if id - onlyId == 0 then
Just movement
else
matchForMovement onlyId onlyName remain
{-| A group of curves represents the trail of one scalar property
(Scalar property meaning something like opacity, or just the `R` channel of rgb.)
@ -1597,7 +1129,6 @@ matchForMovement onlyId onlyName props =
type RenderedProp
= RenderedProp RenderedPropDetails
| RenderedColorProp RenderedColorPropDetails
| TransformProp TransformPropDetails
-- transform can now be deconstructed into its parts
-- This is for translation and scaling
-- Rotation is a RenderedProp
@ -1645,33 +1176,6 @@ type alias RenderedColorPropDetails =
}
type alias TransformPropDetails =
{ sections :
List (Move.Sequence Transform)
, state : TransformState
}
type alias TransformState =
{ x : Move.State
, y : Move.State
, scale : Move.State
, rotation : Move.State
}
type alias Transform =
{ x : Float
, y : Float
, scale : Float
, rotation : Float
}
type alias TransformPresence =
Bits.Bits Transform
{-| Slightly different than CssAnim in that we can also have style properties
This is for when a property has not changed and so a full animation is not necessary
-}

View File

@ -1,22 +1,19 @@
module InternalAnim.Css.Props exposing
( Id, ids, hash, default, defaultPosition, groups
, isTransformId, isTranslateId, isRotateId, isScaleId
, isTranslateId, isScaleId
, Format, format, float, int, px, turns
, roundFloat, floatToString
, isGroup
, VectorSlot(..), colorHash, groupToCompoundId, name, noId, toStr, translateX, transparent, vectorSlotToId, vectorToString, zero
, VectorSlot(..), colorHash, groupToCompoundId, noId, transparent, vectorSlotToId, vectorToString
)
{-|
@docs Id, ids, hash, default, defaultPosition, groups
@docs isTransformId, isTranslateId, isRotateId, isScaleId
@docs isTranslateId, isScaleId
@docs Format, format, float, int, px, turns
@docs roundFloat, floatToString
@docs translateToString, isGroup
-}
@ -139,12 +136,9 @@ hashFormat form num =
Px ->
String.fromInt (round num) ++ "px"
Turns vec ->
Turns _ ->
String.fromInt (round num)
TranslateX ->
"translateX(" ++ String.fromInt (round num) ++ "px)"
format : Format -> Float -> String
format form num =
@ -162,15 +156,11 @@ format form num =
-- Number here is 1/1000 of a turn
vectorToCssString vec ++ " " ++ String.fromFloat (num / 1000) ++ "turn"
TranslateX ->
"translateX(" ++ String.fromInt (round num) ++ "px)"
type Format
= AsFloat
| AsInt
| Px
| TranslateX
| Turns Vector
@ -194,11 +184,6 @@ px =
Px
translateX : Format
translateX =
TranslateX
{-| We make this huge because we want it last.
The order of the ids matters, as it's the order that they're rendered in.
@ -249,12 +234,24 @@ isGroup groupId id =
False
groups : { scaling : Id, translation : Id }
groups =
{ scaling = 20
, translation = 10
}
ids :
{ x : Id
, y : Id
, z : Id
, rotation : Id
, scale : Id
, scaleX : Id
, scaleY : Id
, scaleZ : Id
, opacity : Id
}
ids =
{ x = 0
, y = 1
@ -268,30 +265,15 @@ ids =
}
firstTransform : Id
firstTransform =
ids.x
type alias Id =
Int
isTransformId : Id -> Bool
isTransformId id =
id < 12
isTranslateId : Id -> Bool
isTranslateId id =
id < 3
isRotateId : Id -> Bool
isRotateId id =
id == 3
isScaleId : Id -> Bool
isScaleId id =
id == 4 || id == 5 || id == 6 || id == 7
@ -382,60 +364,6 @@ hashId id =
"unknown"
name : Id -> String
name id =
case id of
13 ->
"opacity"
14 ->
"background-color"
_ ->
"unknown"
toStr : Id -> (Float -> String)
toStr id =
case id of
0 ->
\f ->
"translateX(" ++ String.fromFloat f ++ "px)"
1 ->
\f ->
"translateY(" ++ String.fromFloat f ++ "px)"
2 ->
\f ->
"translateZ(" ++ String.fromFloat f ++ "px)"
3 ->
\f ->
"rotate(" ++ String.fromFloat f ++ "rad)"
4 ->
\f ->
"scale(" ++ String.fromFloat f ++ ")"
5 ->
\f ->
"scaleX(" ++ String.fromFloat f ++ ")"
6 ->
\f ->
"scaleY(" ++ String.fromFloat f ++ ")"
13 ->
-- opacity
\f ->
String.fromFloat f
_ ->
\f ->
String.fromFloat f
defaultPosition : Id -> Float
defaultPosition id =
case id of

View File

@ -1,130 +0,0 @@
module InternalAnim.Estimation exposing (velocity, velocityAtTarget)
{-| -}
import Animator
import Animator.Timeline
import Animator.Value
import InternalAnim.Duration as Duration
import InternalAnim.Move as Move
import InternalAnim.Quantity as Quantity
import InternalAnim.Time as Time
import InternalAnim.Timeline as Timeline
import InternalAnim.Units as Units exposing (Pixels, PixelsPerSecond)
import Time
mapTime fn time =
Time.millisToPosix (fn (Time.posixToMillis time))
{-| Estimate velocity in pixels/second
-}
velocity : Int -> Time.Posix -> Animator.Timeline.Timeline event -> (event -> Animator.Value.Movement) -> Float
velocity resolution time timeline toPosition =
let
before =
mapTime (\t -> t - resolution) time
after =
mapTime (\t -> t + resolution) time
zero =
Animator.Value.movement (Timeline.atTime before timeline) toPosition
one =
Animator.Value.movement (Timeline.atTime time timeline) toPosition
two =
Animator.Value.movement (Timeline.atTime after timeline) toPosition
first =
(one.position - zero.position) / toFloat resolution
second =
(two.position - one.position) / toFloat resolution
expected =
-- 1000 * avg first second
1000 * (two.position - zero.position) / (2 * toFloat resolution)
in
expected
{-| -}
velocityAtTarget :
(state -> Move.Move Float)
-> Timeline.Occurring state
-> List (Timeline.Occurring state)
-> PixelsPerSecond
velocityAtTarget lookup target future =
let
movement =
lookup (Timeline.getEvent target)
in
case future of
[] ->
case movement of
Move.Pos _ _ [] ->
zeroVelocity
Move.Pos _ _ (seq :: _) ->
Move.initialSequenceVelocity seq
next :: _ ->
let
targetPosition =
case movement of
Move.Pos _ x _ ->
Units.pixels x
in
case lookup (Timeline.getEvent next) of
Move.Pos _ aheadPosition [] ->
-- our target velocity is the linear velocity between target and lookahead
velocityBetween
targetPosition
(Timeline.endTime target)
(Units.pixels aheadPosition)
(Timeline.startTime next)
Move.Pos _ aheadPosition (seq :: _) ->
if Timeline.isResting target then
Move.initialSequenceVelocity seq
else
velocityBetween
targetPosition
(Timeline.endTime target)
(Units.pixels aheadPosition)
(Timeline.startTime next)
velocityBetween :
Pixels
-> Time.Absolute
-> Pixels
-> Time.Absolute
-> PixelsPerSecond
velocityBetween one oneTime two twoTime =
let
distance =
two
|> Quantity.minus one
duration =
Time.duration oneTime twoTime
vel =
Units.inPixels distance
/ Duration.inSeconds duration
in
if isNaN vel || isInfinite vel then
Quantity.zero
else
Units.pixelsPerSecond vel
zeroVelocity : PixelsPerSecond
zeroVelocity =
Units.pixelsPerSecond 0

View File

@ -1,15 +1,14 @@
module InternalAnim.Move exposing
( Move(..), to, toWith
, State, init
, lerpColor, lerpFloat, lerpTransform, lerpVector
, lerpColor, lerpFloat, lerpVector
, Sequence(..)
, Step(..), step, stepWith, set
, Step(..), stepWith
, sequences
, addSequence, cssForSections
, withTransition, withVelocities
, at, atX, transitionTo
, denormalize, normalizeOver, toReal
, floatToString, initialSequenceVelocity, move
, at, transitionTo
, move
)
{-|
@ -18,10 +17,10 @@ module InternalAnim.Move exposing
@docs State, init
@docs lerpColor, lerpFloat, lerpTransform, lerpVector
@docs lerpColor, lerpFloat, lerpVector
@docs Sequence
@docs Step, step, stepWith, set
@docs Step, stepWith
@docs sequences, goto
@ -29,9 +28,7 @@ module InternalAnim.Move exposing
@docs withTransition, withVelocities
@docs at, atX, transitionTo
@docs denormalize, normalizeOver, toReal
@docs at, transitionTo
-}
@ -121,6 +118,7 @@ toWith t v =
Pos t v []
move : Transition.Transition -> value -> List (Sequence value) -> Move value
move =
Pos
@ -140,27 +138,12 @@ withSequenceDelay delay (Sequence i _ dur steps) =
Sequence i delay dur steps
set : value -> Step value
set =
Step zeroDuration Transition.standard
step : Duration.Duration -> value -> Step value
step dur value =
Step dur Transition.standard value
stepWith : Duration.Duration -> Transition.Transition -> value -> Step value
stepWith =
Step
{--}
zeroVelocity : Units.PixelsPerSecond
zeroVelocity =
Units.pixelsPerSecond 0
type alias State =
{ position : Units.Pixels
, velocity : Units.PixelsPerSecond
@ -185,14 +168,6 @@ addDelayToSequence delay seqs captured =
)
type alias Transform =
{ x : Float
, y : Float
, scale : Float
, rotation : Float
}
lerpFloat : Float -> Float -> Float -> Float
lerpFloat t one two =
one + ((two - one) * t)
@ -237,30 +212,6 @@ lerpVector t one two =
}
lerpTransform : Float -> Transform -> Transform -> Transform
lerpTransform t one two =
{ x =
lerpFloat t one.x two.x
, y =
lerpFloat t one.y two.y
, scale =
lerpFloat t one.scale two.scale
, rotation =
lerpFloat t one.rotation two.rotation
}
atX :
Float
-> Move Float
->
{ position : Bezier.Point
, velocity : Bezier.Point
}
atX progress (Pos trans value dwell) =
Transition.atX2 progress trans
at :
Float
-> Time.Absolute
@ -273,7 +224,7 @@ at progress startTime targetTime (Pos transition targetPosition dwell) startingS
startPosition =
Units.inPixels startingState.position
in
Transition.atX2 progress transition
Transition.atX progress transition
|> denormalize startTime
targetTime
startPosition
@ -313,7 +264,7 @@ transitionTo progress startTime targetTime (Pos trans targetPosition dwell) star
else
Transition.withVelocities introVelocity 0 trans
in
Transition.atX2 progress transition
Transition.atX progress transition
|> denormalize startTime
targetTime
startPosition
@ -389,26 +340,6 @@ scaleXYBy { x, y } point =
{ x = point.x * x, y = point.y * y }
{-|
Map a value to 0:1 given a range it should be in.
-}
normalizeOver : Float -> Float -> Float -> Float
normalizeOver start end current =
let
total =
abs (end - start)
in
if total == 0 then
0
else
((current - start) / total)
|> max 0
|> min 1
{-| The opposite of `normalizeOver`.
I guess this is denormalization? Though i was always confused by that term :/
@ -481,162 +412,160 @@ sequences :
-> List (Sequence value)
-> List (Sequence value)
sequences startTime targetTime now stopTime movement existingSequence =
let
durationToNow =
Time.duration startTime now
in
if Time.equal now stopTime && not (Time.equal targetTime stopTime) then
-- We've probably been interrupted
[]
else if Time.thisAfterOrEqualThat startTime now then
-- We've definitely started, so we want to report the full sequence
-- most common case will be startTime == now
case movement of
Pos trans value [] ->
let
transitionDuration =
Time.duration startTime targetTime
seq =
Sequence 1
durationToNow
transitionDuration
[ Step transitionDuration trans value
]
in
push seq existingSequence
Pos trans value [ Sequence 1 delay dur steps ] ->
let
stepDuration =
Time.duration startTime targetTime
transitionDuration =
stepDuration
|> Time.expand dur
transitionSequence =
Sequence 1
(Time.expand durationToNow delay)
transitionDuration
(Step stepDuration trans value
:: steps
)
in
push transitionSequence
existingSequence
Pos trans value dwell ->
let
transitionDuration =
Time.duration startTime targetTime
transitionSequence =
Sequence 1
durationToNow
transitionDuration
[ Step transitionDuration trans value
]
in
if Time.isZeroDuration transitionDuration && not (List.isEmpty dwell) then
existingSequence
|> append (addDelayToSequence durationToNow dwell [])
else
existingSequence
|> push transitionSequence
|> append (addDelayToSequence (Time.expand durationToNow transitionDuration) dwell [])
else if after startTime stopTime now movement then
-- we've completely passed this state, no splines are returned
[]
else
-- now is during the new sequence
-- so let's compose the new sequence and then split it at the new time
-- we also know that existingSequence should be [] here
case movement of
Pos trans value [] ->
let
splitTime =
Time.progress startTime targetTime now
transitionDuration =
Time.duration startTime targetTime
newSequence =
Sequence 1 Quantity.zero transitionDuration [ Step transitionDuration trans value ]
|> takeAfter durationToNow
in
case newSequence.following of
Nothing ->
[ newSequence.base ]
Just following ->
[ newSequence.base ]
|> push
(following
|> withSequenceDelay (getSequenceDuration newSequence.base)
)
Pos trans value [ Sequence 1 delay dur steps ] ->
let
stepDuration =
Time.duration startTime targetTime
transitionDuration =
stepDuration
|> Time.expand dur
new =
Sequence 1 Quantity.zero transitionDuration (Step stepDuration trans value :: steps)
|> takeAfter durationToNow
in
case new.following of
Nothing ->
[ new.base
]
Just following ->
[ new.base ]
|> push
(following |> withSequenceDelay (getSequenceDuration new.base))
Pos trans value dwell ->
let
transitionDuration =
Time.duration startTime targetTime
in
if Time.thisAfterThat now targetTime then
takeAfterSequenceList durationToNow dwell
else
let
durationToNow =
Time.duration startTime now
in
if Time.thisAfterOrEqualThat startTime now then
-- We've definitely started, so we want to report the full sequence
-- most common case will be startTime == now
case movement of
Pos trans value [] ->
let
new =
transitionDuration =
Time.duration startTime targetTime
seq =
Sequence 1
durationToNow
transitionDuration
[ Step transitionDuration trans value
]
in
push seq existingSequence
Pos trans value [ Sequence 1 delay dur steps ] ->
let
stepDuration =
Time.duration startTime targetTime
transitionDuration =
stepDuration
|> Time.expand dur
transitionSequence =
Sequence 1
(Time.expand durationToNow delay)
transitionDuration
(Step stepDuration trans value
:: steps
)
in
push transitionSequence
existingSequence
Pos trans value dwell ->
let
transitionDuration =
Time.duration startTime targetTime
in
if Time.isZeroDuration transitionDuration && not (List.isEmpty dwell) then
existingSequence
|> append (addDelayToSequence durationToNow dwell [])
else
let
transitionSequence =
Sequence 1
durationToNow
transitionDuration
[ Step transitionDuration trans value
]
in
existingSequence
|> push transitionSequence
|> append (addDelayToSequence (Time.expand durationToNow transitionDuration) dwell [])
else if after startTime stopTime now movement then
-- we've completely passed this state, no splines are returned
[]
else
-- now is during the new sequence
-- so let's compose the new sequence and then split it at the new time
-- we also know that existingSequence should be [] here
case movement of
Pos trans value [] ->
let
transitionDuration =
Time.duration startTime targetTime
newSequence =
Sequence 1 Quantity.zero transitionDuration [ Step transitionDuration trans value ]
|> takeAfter durationToNow
in
case newSequence.following of
Nothing ->
[ newSequence.base ]
Just following ->
[ newSequence.base ]
|> push
(following
|> withSequenceDelay (getSequenceDuration newSequence.base)
)
Pos trans value [ Sequence 1 _ dur steps ] ->
let
stepDuration =
Time.duration startTime targetTime
transitionDuration =
stepDuration
|> Time.expand dur
new =
Sequence 1 Quantity.zero transitionDuration (Step stepDuration trans value :: steps)
|> takeAfter durationToNow
in
case new.following of
Nothing ->
new.base
:: addDelayToSequence (getSequenceDuration new.base) dwell []
[ new.base
]
Just following ->
let
delayToFollowing =
getSequenceDuration new.base
in
[ new.base ]
|> push (following |> withSequenceDelay delayToFollowing)
|> append
(addDelayToSequence
(Time.expand (getSequenceDuration following)
delayToFollowing
|> push
(following |> withSequenceDelay (getSequenceDuration new.base))
Pos trans value dwell ->
if Time.thisAfterThat now targetTime then
takeAfterSequenceList durationToNow dwell
else
let
transitionDuration =
Time.duration startTime targetTime
new =
Sequence 1 Quantity.zero transitionDuration [ Step transitionDuration trans value ]
|> takeAfter durationToNow
in
case new.following of
Nothing ->
new.base
:: addDelayToSequence (getSequenceDuration new.base) dwell []
Just following ->
let
delayToFollowing =
getSequenceDuration new.base
in
[ new.base ]
|> push (following |> withSequenceDelay delayToFollowing)
|> append
(addDelayToSequence
(Time.expand (getSequenceDuration following)
delayToFollowing
)
dwell
[]
)
dwell
[]
)
takeAfterSequenceList :
@ -648,7 +577,7 @@ takeAfterSequenceList durationToNow seqs =
[] ->
[]
((Sequence n delay duration steps) as top) :: remain ->
((Sequence n _ duration _) as top) :: remain ->
let
floatN =
toFloat n
@ -824,7 +753,7 @@ afterSequenceList durationTillNow seq remaining =
[] ->
True
((Sequence n delay duration steps) as top) :: rest ->
((Sequence n _ duration _) as top) :: rest ->
let
durationOfUnrolledSeq =
duration |> Quantity.multiplyBy (toFloat n)
@ -838,6 +767,7 @@ afterSequenceList durationTillNow seq remaining =
False
afterSequence : Duration.Duration -> Sequence value -> Bool
afterSequence durationTillNow (Sequence n delay duration steps) =
let
floatN =
@ -863,21 +793,6 @@ zeroDuration =
{- CSS KEYFRAMES -}
initialSequenceVelocity : Sequence value -> Units.PixelsPerSecond
initialSequenceVelocity seq =
case seq of
Sequence 0 _ _ _ ->
zeroVelocity
Sequence _ _ _ [] ->
zeroVelocity
Sequence n _ _ ((Step dur trans _) :: _) ->
Transition.initialVelocity trans
|> (*) 1000
|> Units.pixelsPerSecond
hash : Time.Absolute -> String -> Sequence value -> (value -> String) -> String
hash now name (Sequence n delay dur steps) toString =
-- we need to encode the current time in the animations name so the browser doesn't cache anything
@ -914,16 +829,6 @@ stepHash steps toString hashed =
)
roundFloat : Float -> Float
roundFloat f =
toFloat (round (f * 100)) / 100
floatToString : Float -> String
floatToString f =
String.fromFloat (roundFloat f)
hashDuration : String -> Duration.Duration -> String
hashDuration prefix dur =
let
@ -939,38 +844,6 @@ hashDuration prefix dur =
(round (Duration.inSeconds dur))
lastPosOr : value -> Sequence value -> value
lastPosOr x (Sequence _ _ _ steps) =
case steps of
[] ->
x
_ ->
lastPosOrHelper x steps
lastPosOrHelper : value -> List (Step value) -> value
lastPosOrHelper x steps =
case steps of
[] ->
x
(Step _ _ v) :: [] ->
v
(Step _ _ _) :: (Step _ _ v) :: [] ->
v
(Step _ _ _) :: (Step _ _ _) :: (Step _ _ v) :: [] ->
v
(Step _ _ _) :: (Step _ _ _) :: (Step _ _ _) :: (Step _ _ v) :: [] ->
v
(Step _ _ _) :: (Step _ _ _) :: (Step _ _ _) :: (Step _ _ v) :: remain ->
lastPosOrHelper v remain
type alias CssAnim =
{ hash : String
, animation : String
@ -1005,7 +878,7 @@ cssForSections now startPos name lerp toString toHashString sections anim =
[] ->
anim
[ (Sequence 1 delay dur [ Step stepDur (Transition.Transition spline) v ]) as seq ] ->
[ (Sequence 1 delay _ [ Step stepDur (Transition.Transition spline) v ]) as seq ] ->
-- NOTE, we're not using the above `dur` because it's the total duration of the sequence
-- and therefore equal to stepDur in this case
{ anim
@ -1156,7 +1029,7 @@ initialProps name toString startPos (Sequence _ _ _ steps) =
[] ->
[]
(Step dur transition val) :: _ ->
(Step dur _ val) :: _ ->
if Duration.isZero dur then
[ ( name, toString val ) ]
@ -1219,16 +1092,6 @@ keyframeHelper name lerp startPos sequenceDuration currentDur steps rendered =
(rendered ++ frames)
emptyAnim : CssAnim
emptyAnim =
{ hash = ""
, animation = ""
, transition = ""
, keyframes = ""
, props = []
}
combine : CssAnim -> CssAnim -> CssAnim
combine one two =
if String.isEmpty one.hash && List.isEmpty one.props then

View File

@ -4,12 +4,7 @@ module InternalAnim.Quantity exposing
, equalWithin
, greaterThan
, greaterThanOrEqualTo
, greaterThanOrEqualToZero
, greaterThanZero
, lessThan
, lessThanOrEqualTo
, lessThanOrEqualToZero
, lessThanZero
, max
, minus
, multiplyBy
@ -38,12 +33,6 @@ minus (Quantity y) (Quantity x) =
Quantity (x - y)
{-| -}
lessThan : Quantity number units -> Quantity number units -> Bool
lessThan (Quantity y) (Quantity x) =
x < y
{-| -}
greaterThan : Quantity number units -> Quantity number units -> Bool
greaterThan (Quantity y) (Quantity x) =
@ -62,30 +51,6 @@ greaterThanOrEqualTo (Quantity y) (Quantity x) =
x >= y
{-| -}
lessThanZero : Quantity number units -> Bool
lessThanZero (Quantity x) =
x < 0
{-| -}
greaterThanZero : Quantity number units -> Bool
greaterThanZero (Quantity x) =
x > 0
{-| -}
lessThanOrEqualToZero : Quantity number units -> Bool
lessThanOrEqualToZero (Quantity x) =
x <= 0
{-| -}
greaterThanOrEqualToZero : Quantity number units -> Bool
greaterThanOrEqualToZero (Quantity x) =
x >= 0
max : Quantity number units -> Quantity number units -> Quantity number units
max (Quantity x) (Quantity y) =
Quantity (Basics.max x y)

View File

@ -3,6 +3,7 @@ module InternalAnim.Random exposing (random)
{-| -}
fract : Float -> Float
fract x =
x - toFloat (floor x)
@ -20,4 +21,4 @@ random seed low high =
0
else
(fract (sin seed * 100000.0) + 1.5707) / pi
low + (((fract (sin seed * 100000.0) + 1.5707) / pi) * (high - low))

View File

@ -2,8 +2,8 @@ module InternalAnim.Time exposing
( thisBeforeOrEqualThat, thisAfterOrEqualThat, equal
, Absolute, AbsoluteTime(..), Duration, absolute, duration, progress
, inMilliseconds
, latest, earliest, toPosix, durationToString, reduceDurationBy
, advanceBy, equalDuration, expand, isZeroDuration, maxDuration, millis, numberOfFrames, positiveDuration, progressWithin, rollbackBy, scaleDuration, thisAfterThat, thisBeforeThat, zeroDuration
, latest, toPosix, durationToString
, advanceBy, expand, isZeroDuration, maxDuration, millis, positiveDuration, progressWithin, rollbackBy, scaleDuration, thisAfterThat, thisBeforeThat, zeroDuration
)
{-|
@ -14,7 +14,7 @@ module InternalAnim.Time exposing
@docs inMilliseconds
@docs latest, earliest, toPosix, durationToString, reduceDurationBy
@docs latest, toPosix, durationToString
-}
@ -74,11 +74,6 @@ expand one two =
Quantity.plus one two
reduceDurationBy : Duration -> Duration -> Duration
reduceDurationBy one two =
Quantity.minus one two
advanceBy : Duration -> Absolute -> Absolute
advanceBy dur time =
Quantity.plus time (Quantity.Quantity (Duration.inMilliseconds dur))
@ -147,15 +142,6 @@ latest ((Quantity.Quantity one) as oneQty) ((Quantity.Quantity two) as twoQty) =
oneQty
earliest : Absolute -> Absolute -> Absolute
earliest ((Quantity.Quantity one) as oneQty) ((Quantity.Quantity two) as twoQty) =
if (one - two) >= 0 then
twoQty
else
oneQty
thisBeforeThat : Absolute -> Absolute -> Bool
thisBeforeThat (Quantity.Quantity this) (Quantity.Quantity that) =
(this - that) < 0
@ -189,34 +175,3 @@ isZeroDuration (Quantity.Quantity dur) =
equal : Absolute -> Absolute -> Bool
equal (Quantity.Quantity this) (Quantity.Quantity that) =
(this - that) == 0
equalDuration : Duration -> Duration -> Bool
equalDuration (Quantity.Quantity this) (Quantity.Quantity that) =
(this - that) == 0
{-| The number of frames, and the offset that's needed to preserve the framerate.
Offset
-}
numberOfFrames : Float -> Absolute -> Absolute -> Absolute -> ( Float, Int )
numberOfFrames fps lastFrameTime startAt endAt =
let
millisecondsPerFrame =
1000 / fps
totalDurationInMs =
Duration.inMilliseconds (duration startAt endAt)
framesSinceLastFrame =
max 0 (Duration.inMilliseconds (duration lastFrameTime startAt))
/ millisecondsPerFrame
offset =
1 - (framesSinceLastFrame - toFloat (floor framesSinceLastFrame))
in
( offset * millisecondsPerFrame
, max 1 (round (totalDurationInMs / millisecondsPerFrame))
)

View File

@ -1,35 +1,40 @@
module InternalAnim.Timeline exposing
( Timeline(..), TimelineDetails, Occurring(..), getEvents
( Timeline(..), TimelineDetails, Occurring(..)
, Schedule(..), Event(..)
, needsUpdate, update, updateWith
, startTime, endTime, getEvent, extendEventDwell, hasDwell, isResting
, update, updateWith
, startTime, endTime, getEvent, extendEventDwell
, addToDwell
, sendPing
, current, arrivedAt, arrived, previous, upcoming
, progress
, Line(..), Timetable(..)
, foldpAll, captureTimeline
, ActualDuration(..), Animator(..), Description(..), Frame(..), Frames(..), FramesSummary, Interp, LookAhead, Period(..), Previous(..), Resting(..), Summary, SummaryEvent(..), Transition, atTime, combineRunning, dwellingTime, gc, getCurrentTime, hasChanged, justInitialized, linesAreActive, periodDuration, progress
, foldpAll
, gc, atTime, dwellingTime, getCurrentTime, linesAreActive
, Transition
)
{-|
@docs Timeline, TimelineDetails, Occurring, getEvents
@docs Timeline, TimelineDetails, Occurring
@docs Schedule, Event
@docs needsUpdate, update, updateWith
@docs update, updateWith
@docs startTime, endTime, getEvent, extendEventDwell, hasDwell, isResting
@docs startTime, endTime, getEvent, extendEventDwell
@docs addToDwell
@docs sendPing
@docs current, arrivedAt, arrived, previous, upcoming
@docs progress
@docs Line, Timetable
@docs foldpAll, captureTimeline
@docs foldpAll
@docs gc, atTime, dwellingTime, getCurrentTime, linesAreActive
@docs Transition
-}
@ -50,21 +55,6 @@ type Event event
= Event Time.Duration event (Maybe Time.Duration)
type Period
= Loop Time.Duration
| Repeat Int Time.Duration
periodDuration : Period -> Time.Duration
periodDuration per =
case per of
Loop dur ->
dur
Repeat i dur ->
dur
getScheduledEvent : Event event -> event
getScheduledEvent (Event _ ev _) =
ev
@ -121,29 +111,6 @@ type Occurring event
{- TYPES FOR INTERPOLATION -}
{-| First, let's cover what the type parameters are
Examples:
The symbolic state of the timeline entry.
state -> MenuOpen
The description of how to animate that symbolic state
anchor ->
= Osc Personality Period (Float -> Float)
| Pos Personality Float
The actual value that's moving
motion -> { x:34, y: 34 }
-}
type alias Interp state anchor motion =
{ start : anchor -> motion
, visit : Visit state anchor motion
, transition : Lerp anchor motion
}
type alias Transition state anchor motion =
(state -> anchor)
-- previous event
@ -167,74 +134,6 @@ type alias Transition state anchor motion =
-> motion
type alias Visit state anchor motion =
(state -> anchor)
-> Occurring state
-> Time.Absolute
-> Maybe (LookAhead anchor)
-> motion
-> motion
type alias LookAhead state =
{ anchor : state
, time : Time.Absolute
, resting : Bool
}
mapLookAhead : (a -> b) -> LookAhead a -> LookAhead b
mapLookAhead fn look =
{ anchor = fn look.anchor
, time = look.time
, resting = look.resting
}
type alias Lerp anchor motion =
Time.Absolute
-> anchor
-> anchor
-> Time.Absolute
-> Time.Absolute
-> Maybe (LookAhead anchor)
-> motion
-> motion
type Previous event
= Previous (Occurring event)
| PreviouslyInterrupted Time.Absolute
mapTable : (Occurring a -> Occurring b) -> Timetable a -> Timetable b
mapTable fn (Timetable lines) =
Timetable (List.map (mapLine fn) lines)
mapLine : (Occurring a -> Occurring b) -> Line a -> Line b
mapLine fn (Line t startEvent els) =
Line t (fn startEvent) (List.map fn els)
mapLineWith : (Occurring a -> state -> ( Occurring b, state )) -> state -> Line a -> ( Line b, state )
mapLineWith fn initial (Line start startingEvent remaining) =
let
onLine occur ( events, state ) =
let
( newOccur, newState ) =
fn occur state
in
( newOccur :: events, newState )
( newStartingEvent, startingState ) =
fn startingEvent initial
in
case List.foldl onLine ( [], startingState ) remaining of
( reversedEvents, newState ) ->
( Line start newStartingEvent (List.reverse reversedEvents), newState )
getEvent : Occurring event -> event
getEvent (Occurring ev _ _) =
ev
@ -249,16 +148,6 @@ extendEventDwell extendBy ((Event at ev maybeDwell) as thisEvent) =
Event at ev (addToDwell extendBy maybeDwell)
hasDwell : Occurring event -> Bool
hasDwell (Occurring _ (Quantity.Quantity start) (Quantity.Quantity end)) =
(start - end) /= 0
isResting : Occurring event -> Bool
isResting (Occurring _ (Quantity.Quantity start) (Quantity.Quantity end)) =
(end - start) == 0
startTime : Occurring event -> Time.Absolute
startTime (Occurring _ time _) =
time
@ -269,26 +158,6 @@ endTime (Occurring _ _ end) =
end
type Description event
= DescribeStartTransition Time.Posix
| DescribeEvent Time.Posix event
| DescribeInterruption
{ interruption : Time.Posix
, target : event
, newTarget : event
, newTargetTime : Time.Posix
}
getEvents : Timeline event -> List (List ( Time.Posix, event ))
getEvents (Timeline timeline) =
case timeline.events of
Timetable lines ->
lines
|> List.map (\(Line _ start ev) -> start :: ev)
|> List.map (List.map (\(Occurring evt time _) -> ( Time.toPosix time, evt )))
atTime : Time.Posix -> Timeline event -> Timeline event
atTime now (Timeline timeline) =
Timeline { timeline | now = Time.absolute now }
@ -334,11 +203,6 @@ updateWith withGC possiblyNow (Timeline timeline) =
clean : Bool -> TimelineDetails event -> TimelineDetails event
clean runGC details =
let
events =
case details.events of
Timetable evs ->
evs
running =
case details.events of
Timetable lines ->
@ -349,6 +213,12 @@ clean runGC details =
running
, events =
if runGC then
let
events =
case details.events of
Timetable evs ->
evs
in
Timetable (garbageCollectOldEvents details.now [] events)
else
@ -597,7 +467,7 @@ scaleEvent scale (Event dur event maybeDur) =
-}
scheduleMatchesExisting : Schedule event -> Line event -> Bool
scheduleMatchesExisting (Schedule scheduleDelay event schedulUpcoming) (Line lineStart lineStartEvent lineUpcoming) =
scheduleMatchesExisting (Schedule _ event schedulUpcoming) (Line _ lineStartEvent lineUpcoming) =
let
equalStartEvent =
scheduledEventEqual event lineStartEvent
@ -619,7 +489,7 @@ scheduleMatchesExisting (Schedule scheduleDelay event schedulUpcoming) (Line lin
scheduledEventEqual : Event event -> Occurring event -> Bool
scheduledEventEqual (Event dur schedEvent maybeDwell) (Occurring occurEvent occurStart occurEnd) =
scheduledEventEqual (Event _ schedEvent _) (Occurring occurEvent _ _) =
schedEvent == occurEvent
@ -685,17 +555,17 @@ interrupt timeline scheduled =
interruptLines : Time.Absolute -> Schedule event -> List (Line event) -> List (Line event) -> Maybe (List (Line event))
interruptLines now scheduled pastLines lines =
let
startInterruption =
case scheduled of
Schedule scheduleDelay _ _ ->
Time.advanceBy scheduleDelay now
in
case lines of
[] ->
Nothing
startLine :: remaining ->
let
startInterruption =
case scheduled of
Schedule scheduleDelay _ _ ->
Time.advanceBy scheduleDelay now
in
if interruptionHappensLater startInterruption remaining then
interruptLines now scheduled (startLine :: pastLines) remaining
@ -731,14 +601,14 @@ interruptionHappensLater startInterruption remaining =
interruptLine : Time.Absolute -> Schedule event -> Line event -> List (Line event) -> Maybe (List (Line event))
interruptLine now scheduled line future =
let
startInterruption =
case scheduled of
Schedule scheduleDelay _ _ ->
Time.advanceBy scheduleDelay now
in
case line of
Line start startEvent trailing ->
let
startInterruption =
case scheduled of
Schedule scheduleDelay _ _ ->
Time.advanceBy scheduleDelay now
in
if Time.thisAfterOrEqualThat startInterruption start then
-- this line starts before the interruption
case future of
@ -803,18 +673,19 @@ getTransitionAt interruptionTime prev trailing =
interruptAtExactly : Time.Absolute -> Schedule event -> LastTwoEvents event -> Line event
interruptAtExactly now scheduled ((LastTwoEvents penultimateTime penultimate lastEventTime lastEvent) as last) =
interruptAtExactly now scheduled (LastTwoEvents penultimateTime penultimate lastEventTime _) =
case scheduled of
Schedule delay_ startingEvent reverseQueued ->
let
amountProgress =
Time.progress penultimateTime
lastEventTime
(Time.advanceBy delay_ now)
newStartingEvent =
-- we apply the discount if we are returning to a state
if penultimate == getScheduledEvent startingEvent then
let
amountProgress =
Time.progress penultimateTime
lastEventTime
(Time.advanceBy delay_ now)
in
startingEvent
|> adjustScheduledDuration (Quantity.multiplyBy amountProgress)
@ -1008,33 +879,6 @@ addToDwell duration maybeDwell =
Just (Quantity.plus duration existing)
rescale : Time.Absolute -> Float -> List (Line event) -> List (Line event)
rescale now scale lines =
if scale == 1 then
lines
else
List.map (rescaleLine now scale) lines
rescaleLine : Time.Absolute -> Float -> Line event -> Line event
rescaleLine now scale (Line lineStart firstEvent remain) =
Line
(rescaleTime now scale lineStart)
(rescaleEvent now scale firstEvent)
(List.map (rescaleEvent now scale) remain)
rescaleTime : Time.Absolute -> Float -> Time.Absolute -> Time.Absolute
rescaleTime (Quantity.Quantity now) scale (Quantity.Quantity time) =
Time.millis (now + ((time - now) * scale))
rescaleEvent : Time.Absolute -> Float -> Occurring event -> Occurring event
rescaleEvent now scale (Occurring event start end) =
Occurring event (rescaleTime now scale start) (rescaleTime now scale end)
foldpAll :
(state -> anchor)
-> (anchor -> motion)
@ -1052,7 +896,7 @@ foldpAll lookup toStart transitionTo (Timeline timelineDetails) =
[] ->
start
(Line lineStart firstEvent remain) :: _ ->
(Line lineStart _ _) :: _ ->
visitAll2
lookup
transitionTo
@ -1238,176 +1082,6 @@ visitAll2 toAnchor transitionTo details prev queue future state =
new
type alias FramesSummary motion =
{ frames : List (Frame motion)
, duration : Time.Duration
, dwell :
Maybe
{ period : Period
, frames : List (Frame motion)
}
}
type Frame motion
= Frame Float motion
type alias Summary event =
{ events : List (SummaryEvent event)
, now : Time.Absolute
, startTime : Time.Absolute
}
type SummaryEvent event
= EventSummary event Time.Absolute ActualDuration
| InterruptionSummary
{ target : event
, targetTime : Time.Absolute
, interruptedAt : Time.Absolute
, newTarget : event
, newTargetTime : Time.Absolute
, newTargetDuration : ActualDuration
}
type ActualDuration
= OpenDuration
| KnownDuration Time.Duration
{-| -}
type Frames item
= Single item
| Hold Int item
| Walk item (List (Frames item))
| WithRest (Resting item) (Frames item)
{-| -}
type Resting item
= Cycle Period (List (Frames item))
captureTimeline :
(state -> anchor)
-> Timeline state
-> Summary anchor
captureTimeline lookup (Timeline timelineDetails) =
case timelineDetails.events of
Timetable timetable ->
case timetable of
[] ->
-- I believe this case is fleeting because as soon as we have a real time,
-- we add a line to the timetable.
-- However, maybe it is awkwardly rendered once?
{ events =
[ EventSummary
(lookup timelineDetails.initial)
timelineDetails.now
OpenDuration
]
, now = timelineDetails.now
, startTime = timelineDetails.now
}
(Line start startEv remain) :: remainingLines ->
let
events =
captureTimelineHelper lookup
(startEv :: remain)
remainingLines
[]
in
{ events = List.reverse events
, now = timelineDetails.now
, startTime = start
}
{-| Summarize all the events on the current timeline.
Note, this does not take into account time adjustments!
Essentially this is only used for sprite animation, which currently dont have leaveLate or arriveEarly.
-}
captureTimelineHelper :
(state -> anchor)
-> List (Occurring state)
-> List (Line state)
-> List (SummaryEvent anchor)
-> List (SummaryEvent anchor)
captureTimelineHelper lookup events futureLines summary =
-- futureStart starts a new line.
-- if an interruption occurs, we want to interpolate to the point of the interruption
-- then transition over to the new line.
case events of
[] ->
case futureLines of
[] ->
summary
(Line futureStart futureStartEv futureRemain) :: restOfFuture ->
captureTimelineHelper lookup (futureStartEv :: futureRemain) restOfFuture summary
(Occurring event start eventEnd) :: remain ->
case futureLines of
[] ->
case remain of
[] ->
let
newEvent =
EventSummary (lookup event) start OpenDuration
in
newEvent :: summary
_ ->
let
newEvent =
EventSummary (lookup event) start (KnownDuration (Time.duration start eventEnd))
in
captureTimelineHelper lookup remain futureLines (newEvent :: summary)
(Line futureStart futureStartEv futureRemain) :: restOfFuture ->
if Time.thisBeforeOrEqualThat futureStart start then
-- interruption
let
newEvent =
InterruptionSummary
{ target = lookup event
, targetTime = start
, interruptedAt = futureStart
, newTarget = lookup (getEvent futureStartEv)
, newTargetTime = startTime futureStartEv
, newTargetDuration =
case futureRemain of
[] ->
case restOfFuture of
[] ->
OpenDuration
_ ->
KnownDuration (Time.duration start eventEnd)
_ ->
KnownDuration (Time.duration start eventEnd)
}
in
captureTimelineHelper lookup futureRemain restOfFuture (newEvent :: summary)
else
-- queue up new events
let
newEvent =
EventSummary (lookup event)
start
(KnownDuration (Time.duration start eventEnd))
in
captureTimelineHelper lookup remain futureLines (newEvent :: summary)
{- BOOKKEEPING -}
@ -1418,10 +1092,10 @@ type Status
status : Timeline event -> Status
status ((Timeline details) as timeline) =
status timeline =
foldpAll identity
(\_ -> Dwelling Time.zeroDuration)
(\lookup prev target now start end future state ->
(\_ _ _ now start end _ _ ->
if Time.thisAfterThat now end then
Dwelling (Time.duration now end)
@ -1466,7 +1140,7 @@ arrived : Timeline event -> event
arrived ((Timeline details) as timeline) =
foldpAll identity
(\_ -> details.initial)
(\lookup prev target now start end future state ->
(\_ _ target now _ end _ state ->
-- This is the current event when
-- we have started toward an event or arrived at it.
-- A tricky aspect is that css timelines are only updated on transition
@ -1484,7 +1158,7 @@ current : Timeline event -> event
current ((Timeline details) as timeline) =
foldpAll identity
(\_ -> details.initial)
(\lookup prev target now start end future state ->
(\_ _ target now start end future state ->
-- This is the current event when
-- we have started toward an event or arrived at it.
-- A tricky aspect is that css timelines are only updated on transition
@ -1508,7 +1182,7 @@ previous : Timeline event -> event
previous ((Timeline details) as timeline) =
foldpAll identity
(\_ -> details.initial)
(\lookup prev target now start end future state ->
(\_ _ target now _ _ future state ->
if Time.thisAfterThat now (endTime target) then
case future of
[] ->
@ -1527,7 +1201,7 @@ arrivedAt : (event -> Bool) -> Time.Posix -> Timeline event -> Bool
arrivedAt matches newTime ((Timeline details) as tl) =
foldpAll identity
(\_ -> False)
(\lookup prev target now start end future state ->
(\_ _ target _ _ end _ state ->
state
|| (matches (getEvent target)
&& Time.thisBeforeOrEqualThat details.now end
@ -1553,7 +1227,7 @@ matchesEvent matches (Event _ event _) =
anyScheduled : (event -> Bool) -> Schedule event -> Bool
anyScheduled matches (Schedule dur startEvent remainingEvents) =
anyScheduled matches (Schedule _ startEvent remainingEvents) =
if matchesEvent matches startEvent then
True
@ -1576,149 +1250,10 @@ upcoming matches ((Timeline details) as tl) =
else
foldpAll identity
(\_ -> False)
(\lookup prev target now start end future state ->
(\_ _ target now _ end _ state ->
state
|| (matches (getEvent target)
&& Time.thisBeforeThat now end
)
)
tl
{- ANIMATOR -}
{- The animator checks to see if any timelines are running and also has the ability to update the animation state.
Different animators can do different things
- Normal -> always on
- Inline -> on when moving (require anotation of dwelling events)
- CSS -> single update when timeline is updated
-}
{-| -}
type Animator model
= Animator (model -> Running) (Time.Posix -> model -> model)
type alias Running =
{ running : Bool
, ping : Maybe { delay : Float, target : Time.Posix }
}
combineRunning : Running -> Running -> Running
combineRunning one two =
{ running = one.running || two.running
, ping =
case two.ping of
Nothing ->
one.ping
Just twoPing ->
case one.ping of
Nothing ->
Just twoPing
Just onePing ->
if onePing.delay < twoPing.delay then
Just onePing
else
Just twoPing
}
sendPing : Timeline event -> Maybe { delay : Float, target : Time.Posix }
sendPing ((Timeline details) as timeline) =
Maybe.andThen (encodeStamp details.now) (findNextTransitionTime timeline)
findNextTransitionTime : Timeline event -> Maybe Time.Absolute
findNextTransitionTime ((Timeline details) as timeline) =
foldpAll identity
(\_ -> Nothing)
(\lookup prev target now start end future state ->
if Time.thisBeforeThat now end && (startTime target == end) then
Just (startTime target)
else
state
)
timeline
{-| This is to account for a bug in `Time.every` where it uses the provided delay time as a key to keep track of a time.
To get around this, we encode the current time as a really small part of the given time.
-}
encodeStamp : Time.Absolute -> Time.Absolute -> Maybe { delay : Float, target : Time.Posix }
encodeStamp now target =
let
millis =
Time.inMilliseconds target - nowInMillis
nowInMillis =
Time.inMilliseconds now
nowTail =
nowInMillis / 1000000
pingDelay =
millis + (1 / nowTail)
in
if pingDelay <= 0 then
Nothing
else
Just
{ delay = pingDelay
, target =
Time.millisToPosix (round (Time.inMilliseconds target + 1))
}
{-| -}
needsUpdate : Timeline event -> Bool
needsUpdate ((Timeline timeline) as tl) =
case timeline.queued of
Nothing ->
case timeline.interruption of
[] ->
timeline.running
_ ->
True
Just _ ->
True
{-| -}
hasChanged : Timeline event -> Bool
hasChanged (Timeline timeline) =
case timeline.queued of
Nothing ->
case timeline.interruption of
[] ->
False
_ ->
True
Just _ ->
True
{-| -}
justInitialized : Timeline event -> Bool
justInitialized (Timeline timeline) =
case timeline.now of
Quantity.Quantity qty ->
qty == 0

View File

@ -1,10 +1,10 @@
module InternalAnim.Transition exposing
( Transition(..)
, linear, standard, wobble, bezier
, initialVelocity, atX
, split, before
, hash, keyframes
, atX2, isStandard, takeAfter, withVelocities
, atX
, isStandard
, takeAfter, withVelocities
)
{-|
@ -13,12 +13,14 @@ module InternalAnim.Transition exposing
@docs linear, standard, wobble, bezier
@docs initialVelocity, atX
@docs split, before
@docs hash, keyframes
@docs atX
@docs isStandard
@docs takeAfter, withVelocities
Current bezier formats for elm-animator
Standard:
@ -62,11 +64,7 @@ Goals:
import Bezier
import Bezier.Spring as Spring
import InternalAnim.Duration as Duration
import InternalAnim.Hash as Hash
import InternalAnim.Quantity as Quantity
import InternalAnim.Time as Time
import InternalAnim.Units as Units
{-| A transition are all the bezier curves between A and B that we want to transition through.
@ -197,76 +195,15 @@ isStandard trans =
False
type alias TimeDomain =
{ start : PointInTime
, end : PointInTime
}
type alias PointInTime =
{ x : Time.Absolute
, y : Units.Pixels
}
{-| -}
atX :
Float
-> TimeDomain
-> Units.PixelsPerSecond
-> Units.PixelsPerSecond
-> Transition
->
{ position : Units.Pixels
, velocity : Units.PixelsPerSecond
}
atX progress domain introVelocity exitVelocity transition =
case transition of
Transition spline ->
if domain.start.x == domain.end.x then
{ position = domain.end.y
, velocity =
exitVelocity
}
else
spline
|> inTimeDomain domain introVelocity exitVelocity
|> posVel (toTimeProgress domain progress)
Wobble wob ->
let
totalX =
Time.inMilliseconds domain.end.x - Time.inMilliseconds domain.start.x
params =
Spring.new
{ wobble = wob.wobble
, quickness = wob.quickness
, settleMax = totalX
}
in
Spring.at
{ spring = params
, initial =
{ position = Units.inPixels domain.start.y
, velocity = Units.inPixelsPerMs introVelocity
}
, target = Units.inPixels domain.end.y
}
(totalX * progress)
|> wrapUnits
{-| -}
atX2 :
Float
-> Transition
->
{ position : Bezier.Point
, velocity : Bezier.Point
}
atX2 progress transition =
atX progress transition =
case transition of
Transition spline ->
let
@ -327,146 +264,6 @@ withVelocities intro exit transition =
}
toTimeProgress :
TimeDomain
-> Float
-> Float
toTimeProgress domain factor =
let
start =
Time.inMilliseconds domain.start.x
end =
Time.inMilliseconds domain.end.x
in
((end - start) * factor) + start
wrapUnits state =
{ position =
Units.pixels state.position
, velocity =
Units.pixelsPerSecond (state.velocity * 1000)
}
posVel :
Float
-> Bezier.Spline
->
{ position : Units.Pixels
, velocity : Units.PixelsPerSecond
}
posVel progress spline =
let
current =
Bezier.atX progress spline
firstDeriv =
Bezier.firstDerivative spline current.t
in
{ position =
Units.pixels current.point.y
, velocity =
Units.pixelsPerSecond ((firstDeriv.y / firstDeriv.x) * 1000)
}
zeroVelocity : Float
zeroVelocity =
0
initialVelocity : Transition -> Float
initialVelocity transition =
case transition of
Transition spline ->
let
firstDeriv =
-- at t == 0, the first derivative vector will always be 0,0
-- so we cheat in slightly.
Bezier.firstDerivative spline 0.001
in
if firstDeriv.x == 0 then
zeroVelocity
else
firstDeriv.y / firstDeriv.x
Wobble wob ->
zeroVelocity
type alias Domain =
{ start : Bezier.Point
, end : Bezier.Point
}
third : Float
third =
1 / 3
negativeThird : Float
negativeThird =
-1 / 3
{-| Note, we only rotate the control point to match the desired velocity.
However, there is the question of the magnitude of the control point.
I _think_ the magnitude is roughly equivalent to momentum.
It's possible that we override the built-in control points when there is a non-0 intro/exit Velocity.
Maybe it's a constant like 1/3 or something....
-}
toDomain : Domain -> Float -> Float -> Bezier.Spline -> Bezier.Spline
toDomain domain introVelocity exitVelocity spline =
let
{ two, three } =
toBezierPoints spline
totalX =
domain.end.x - domain.start.x
totalY =
domain.end.y - domain.start.y
ctrl1 =
let
angle =
atan2 introVelocity 1
in
{ x =
(totalX * two.x) + domain.start.x
, y =
(totalY * two.y) + domain.start.y
}
|> rotateAround angle domain.start
ctrl2 =
let
angle =
atan2 exitVelocity 1
in
{ x =
(totalX * three.x) + domain.start.x
, y =
(totalY * three.y) + domain.start.y
}
|> rotateAround angle domain.end
in
Bezier.fromPoints
domain.start
ctrl1
ctrl2
domain.end
toBezierPoints : Bezier.Spline -> { one : Bezier.Point, two : Bezier.Point, three : Bezier.Point, four : Bezier.Point }
toBezierPoints spline =
{ one = Bezier.first spline
@ -476,136 +273,6 @@ toBezierPoints spline =
}
inTimeDomain : TimeDomain -> Units.PixelsPerSecond -> Units.PixelsPerSecond -> Bezier.Spline -> Bezier.Spline
inTimeDomain domain introVelocity exitVelocity spline =
let
{ one, two, three, four } =
toBezierPoints spline
totalX =
Time.inMilliseconds domain.end.x - Time.inMilliseconds domain.start.x
totalY =
Units.inPixels domain.end.y - Units.inPixels domain.start.y
ctrl1 =
let
angle =
atan2 (Units.inPixelsPerMs introVelocity) 1
in
{ x =
(totalX * two.x) + Time.inMilliseconds domain.start.x
, y =
(totalY * two.y) + Units.inPixels domain.start.y
}
|> rotateAroundTimePoint angle domain.start
ctrl2 =
let
angle =
atan2 (Units.inPixelsPerMs exitVelocity) 1
in
{ x =
(totalX * three.x) + Time.inMilliseconds domain.start.x
, y =
(totalY * three.y) + Units.inPixels domain.start.y
}
|> rotateAroundTimePoint angle domain.end
in
Bezier.fromPoints
{ x = Time.inMilliseconds domain.start.x
, y = Units.inPixels domain.start.y
}
ctrl1
ctrl2
{ x = Time.inMilliseconds domain.end.x
, y = Units.inPixels domain.end.y
}
{-| -}
rotateAroundTimePoint : Float -> PointInTime -> Bezier.Point -> Bezier.Point
rotateAroundTimePoint radians center point =
let
centerX =
Time.inMilliseconds center.x
centerY =
Units.inPixels center.y
in
{ x = cos radians * (point.x - centerX) - sin radians * (point.y - centerY) + centerX
, y = sin radians * (point.x - centerX) + cos radians * (point.y - centerY) + centerY
}
{-| -}
rotateAround : Float -> Bezier.Point -> Bezier.Point -> Bezier.Point
rotateAround radians center point =
{ x = cos radians * (point.x - center.x) - sin radians * (point.y - center.y) + center.x
, y = sin radians * (point.x - center.x) + cos radians * (point.y - center.y) + center.y
}
translateBy : Bezier.Point -> Bezier.Point -> Bezier.Point
translateBy one two =
{ x = one.x + two.x
, y = one.y + two.y
}
scaleBy : Float -> Bezier.Point -> Bezier.Point
scaleBy k v =
{ x = k * v.x
, y = k * v.y
}
scaleAbout : Bezier.Point -> Float -> Bezier.Point -> Bezier.Point
scaleAbout p0 k p =
{ x = p0.x + k * (p.x - p0.x)
, y = p0.y + k * (p.y - p0.y)
}
{-| We are vector v and want the component in the direction d.
-}
componentIn : Bezier.Point -> Bezier.Point -> Float
componentIn d v =
v.x * d.x + v.y * d.y
zeroPoint : Bezier.Point
zeroPoint =
{ x = 0
, y = 0
}
scaleTo : Float -> Bezier.Point -> Bezier.Point
scaleTo q v =
let
largestComponent =
max (abs v.x) (abs v.y)
in
if largestComponent == 0 then
zeroPoint
else
let
scaledX =
v.x / largestComponent
scaledY =
v.y / largestComponent
scaledLength =
sqrt (scaledX * scaledX + scaledY * scaledY)
in
{ x = q * scaledX / scaledLength
, y = q * scaledY / scaledLength
}
{-| -}
hash : Transition -> String
hash transition =
@ -788,20 +455,6 @@ keyframeListFromNonNormalizedBezier steps toString str =
)
{-| -}
before : Float -> Transition -> Transition
before t transition =
transition
{-| -}
split : Float -> Transition -> { before : Transition, after : Transition }
split t transition =
{ before = transition
, after = transition
}
takeAfter : Float -> Transition -> Transition
takeAfter t transition =
case transition of

View File

@ -2,11 +2,9 @@ module InternalAnim.Units exposing
( Pixels
, PixelsPerSecond
, inPixels
, inPixelsPerMs
, inPixelsPerSecond
, pixels
, pixelsPerSecond
, zero
)
{-| -}
@ -22,30 +20,6 @@ type InPixels
= InPixels
zero =
Quantity.Quantity 0
type Unit guard
= Unit Float
type alias Position =
Unit Pos
type Pos
= Pos
type alias Velocity =
Unit Vel
type Vel
= Vel
type alias Pixels =
Quantity.Quantity Float InPixels
@ -59,11 +33,6 @@ inPixelsPerSecond (Quantity.Quantity pps) =
pps
inPixelsPerMs : PixelsPerSecond -> Float
inPixelsPerMs (Quantity.Quantity pps) =
1000 * pps
inPixels : Pixels -> Float
inPixels (Quantity.Quantity pixs) =
pixs