Skip to content

3D Animations

3D animations are GPU-accelerated and enable rich, immersive effects.

Example

Rotating cube with expanding sides.

View Examples

View Source Code
module Animation.Keyframe.Animate3D.Main exposing (main)

import Anim.Builder exposing (AnimBuilder, ForKeyframe)
import Anim.Engine.Keyframe as Keyframe
import Anim.Extra.View3D as View3D
import Anim.Property.Rotate as Rotate
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser exposing (Document)
import Html exposing (Html, div, text)
import Html.Attributes exposing (id, style)
import Motion.Easing as Easing exposing (Easing(..))
import Process
import Task



-- MAIN


main : Program () Model Msg
main =
    Browser.document
        { init = \_ -> init
        , view = view
        , update = update
        , subscriptions = always Sub.none
        }



-- MODEL


type alias Model =
    { animState : Keyframe.AnimState
    , state : State
    }





init : ( Model, Cmd Msg )
init =
    let
        initialAnimState =
            Keyframe.init <|
                [ -- Bring the cube forward on the Z axis
                  -- so that it doesn't get clipped by the
                  -- z=0 clipping plane when we expand the
                  -- sides and rotate. This is a fixed 3D depth
                  -- offset, unrelated to layout, so it stays
                  -- in pixels.
                  Translate.initZ cubeGroupName 200 >> Translate.cssUnit Px

                -- Position each face in 3D space along the axis it faces.
                -- Face offsets use `Cqmin` so the cube scales proportionally
                -- with the stage's new dimensions on resize.
                -- Front/Back faces move on Z (forward/backward)
                -- Left/Right faces move on X (sideways)
                -- Top/Bottom faces move on Y (up/down)
                , Translate.initZ frontFace.groupName depth >> Translate.cssUnit Cqmin
                , Translate.initZ backFace.groupName (depth * -1)
                    >> Translate.cssUnit Cqmin
                    -- Rotate each face into position to build the cube
                    -- Front face is not rotated due to facing forward by default
                    >> Rotate.initY backFace.groupName 180
                , Translate.initX rightFace.groupName depth
                    >> Translate.cssUnit Cqmin
                    >> Rotate.initY rightFace.groupName 90
                , Translate.initX leftFace.groupName (-1 * depth)
                    >> Translate.cssUnit Cqmin
                    >> Rotate.initY leftFace.groupName -90
                , Translate.initY topFace.groupName (-1 * depth)
                    >> Translate.cssUnit Cqmin
                    >> Rotate.initX topFace.groupName 90
                , Translate.initY bottomFace.groupName depth
                    >> Translate.cssUnit Cqmin
                    >> Rotate.initX bottomFace.groupName -90

                -- The text labels all start on the same plane as their faces
                -- at z=0, which is the default starting value, so we don't need
                -- to initialize them
                ]
    in
    ( { animState = initialAnimState
      , state = Opening
      }
    , Process.sleep 0
        |> Task.perform (always TriggerAnimation)
    )





type State
    = Opening
    | Closing
    | RotatingOpen
    | RotatingClosed



-- Cube configuration


cubeGroupName : String
cubeGroupName =
    "cubeAnim"


{-| Cube edge length expressed in `Cqmin` (% of the stage's smaller
dimension). The stage applies `container-type: size`, so the cube
resizes automatically with the viewport without any Elm-side
measurement.
-}
cubeSize : Float
cubeSize =
    18


{-| Distance each face sits in front of / behind the cube centre.
Half the cube edge so adjacent faces meet at the cube corners.
-}
depth : Float
depth =
    cubeSize / 2



-- Face configuration


type alias TextConfig =
    { id : String
    , groupName : String
    , label : String
    , color : String
    }


type alias FaceConfig =
    { id : String
    , groupName : String
    , label : String
    , background : String
    , borderColor : String
    , text : TextConfig
    }


frontFace : FaceConfig
frontFace =
    { id = "front-face"
    , groupName = "frontFaceAnim"
    , label = "FRONT"
    , background = "rgb(52, 152, 219)"
    , borderColor = "rgb(41, 128, 185)"
    , text =
        { id = "front-face-text"
        , groupName = "frontFaceTextAnim"
        , label = "FRONT"
        , color = "rgb(0,0,0)"
        }
    }


backFace : FaceConfig
backFace =
    { id = "back-face"
    , groupName = "backFaceAnim"
    , label = "BACK"
    , background = "rgb(41, 128, 185)"
    , borderColor = "rgb(33, 97, 140)"
    , text =
        { id = "back-face-text"
        , groupName = "backFaceTextAnim"
        , label = "BACK"
        , color = "rgb(0,0,0)"
        }
    }


rightFace : FaceConfig
rightFace =
    { id = "right-face"
    , groupName = "rightFaceAnim"
    , label = "RIGHT"
    , background = "rgb(231, 76, 60)"
    , borderColor = "rgb(192, 57, 43)"
    , text =
        { id = "right-face-text"
        , groupName = "rightFaceTextAnim"
        , label = "RIGHT"
        , color = "rgb(0,0,0)"
        }
    }


leftFace : FaceConfig
leftFace =
    { id = "left-face"
    , groupName = "leftFaceAnim"
    , label = "LEFT"
    , background = "rgb(230, 126, 34)"
    , borderColor = "rgb(211, 84, 0)"
    , text =
        { id = "left-face-text"
        , groupName = "leftFaceTextAnim"
        , label = "LEFT"
        , color = "rgb(0,0,0)"
        }
    }


topFace : FaceConfig
topFace =
    { id = "top-face"
    , groupName = "topFaceAnim"
    , label = "TOP"
    , background = "rgb(46, 204, 113)"
    , borderColor = "rgb(39, 174, 96)"
    , text =
        { id = "top-face-text"
        , groupName = "topFaceTextAnim"
        , label = "TOP"
        , color = "rgb(0,0,0)"
        }
    }


bottomFace : FaceConfig
bottomFace =
    { id = "bottom-face"
    , groupName = "bottomFaceAnim"
    , label = "BOTTOM"
    , background = "rgb(155, 89, 182)"
    , borderColor = "rgb(142, 68, 173)"
    , text =
        { id = "bottom-face-text"
        , groupName = "bottomFaceTextAnim"
        , label = "BOTTOM"
        , color = "rgb(0,0,0)"
        }
    }



-- ANIMATIONS
--
-- CUBE ROTATION
--
-- We only rotate the cube, not individual faces, they maintain their
-- position in 3D space because we use `View3D.transformStyle View3D.Preserve3D`
-- on the cube container


rotateCube : Float -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
rotateCube to =
    Keyframe.for cubeGroupName
        >> Rotate.begin
        >> Rotate.toXYZ to to to
        >> Rotate.easing BackInOut
        >> Rotate.duration 8000
        >> Rotate.end


rotateCubeClockwise : Keyframe.EngineBuilder -> Keyframe.EngineBuilder
rotateCubeClockwise =
    rotateCube 360


rotateCubeAntiClockwise : Keyframe.EngineBuilder -> Keyframe.EngineBuilder
rotateCubeAntiClockwise =
    rotateCube 0



-- SIDES


moveSidesOut : Float -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveSidesOut targetAmount =
    moveFrontFaceOut targetAmount
        >> moveBackFaceOut targetAmount
        >> moveRightFaceOut targetAmount
        >> moveLeftFaceOut targetAmount
        >> moveTopFaceOut targetAmount
        >> moveBottomFaceOut targetAmount


moveSidesIn : Float -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveSidesIn targetAmount =
    moveFrontFaceIn targetAmount
        >> moveBackFaceIn targetAmount
        >> moveRightFaceIn targetAmount
        >> moveLeftFaceIn targetAmount
        >> moveTopFaceIn targetAmount
        >> moveBottomFaceIn targetAmount



-- Each face moves along the axis it faces by a `moveAmount` (expressed in
-- `Cqmin`, so it scales with the stage) when the cube expands, and moves
-- back to its original position when the cube closes.
--
-- Front/Back faces move on Z (forward/backward)
-- Left/Right faces move on X (sideways)
-- Top/Bottom faces move on Y (up/down)


sharedTiming : Keyframe.EngineBuilder -> Keyframe.EngineBuilder
sharedTiming =
    Keyframe.duration 1000
        >> Keyframe.easing CircInOut


moveAmount : Float
moveAmount =
    10


moveFace : FaceConfig -> (Translate.Builder ForKeyframe -> Translate.Builder ForKeyframe) -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveFace config moveToBuilder =
    sharedTiming
        >> Keyframe.for config.groupName
        >> Translate.begin
        >> moveToBuilder
        >> Translate.end


moveFrontFaceOut : Float -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveFrontFaceOut toZ =
    moveFace frontFace <|
        Translate.toZ (toZ + moveAmount)


moveFrontFaceIn : Float -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveFrontFaceIn toZ =
    moveFace frontFace <|
        Translate.toZ toZ


moveBackFaceOut : Float -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveBackFaceOut toZ =
    moveFace backFace <|
        Translate.toZ (-1 * toZ - moveAmount)


moveBackFaceIn : Float -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveBackFaceIn toZ =
    moveFace backFace <|
        Translate.toZ (-1 * toZ)


moveRightFaceOut : Float -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveRightFaceOut toX =
    moveFace rightFace <|
        Translate.toX (toX + moveAmount)


moveRightFaceIn : Float -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveRightFaceIn toX =
    moveFace rightFace <|
        Translate.toX toX


moveLeftFaceOut : Float -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveLeftFaceOut toX =
    moveFace leftFace <|
        Translate.toX (-1 * toX - moveAmount)


moveLeftFaceIn : Float -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveLeftFaceIn toX =
    moveFace leftFace <|
        Translate.toX (-1 * toX)


moveTopFaceOut : Float -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveTopFaceOut toY =
    moveFace topFace <|
        Translate.toY (-1 * toY - moveAmount)


moveTopFaceIn : Float -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveTopFaceIn toY =
    moveFace topFace <|
        Translate.toY (-1 * toY)


moveBottomFaceOut : Float -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveBottomFaceOut toY =
    moveFace bottomFace <|
        Translate.toY (toY + moveAmount)


moveBottomFaceIn : Float -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveBottomFaceIn toY =
    moveFace bottomFace <|
        Translate.toY toY



-- TEXT
--
-- Text moves forward (Z+4cqmin) and rotates (to Z=360deg) when sides expand,
-- and then moves back (to Z=0cqmin) and rotates back (to Z=0deg) when sides close


textMoveAmount : Float
textMoveAmount =
    14


moveText : TextConfig -> Float -> Float -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveText config toZ toRotate =
    sharedTiming
        >> Keyframe.for config.groupName
        >> Translate.begin
        >> Translate.toZ toZ
        >> Translate.end
        >> Keyframe.for config.groupName
        >> Rotate.begin
        >> Rotate.toZ toRotate
        >> Rotate.end


moveTextsOut : Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveTextsOut =
    moveText frontFace.text textMoveAmount 360
        >> moveText backFace.text textMoveAmount 360
        >> moveText rightFace.text textMoveAmount 360
        >> moveText leftFace.text textMoveAmount 360
        >> moveText topFace.text textMoveAmount 360
        >> moveText bottomFace.text textMoveAmount 360


moveTextsIn : Keyframe.EngineBuilder -> Keyframe.EngineBuilder
moveTextsIn =
    moveText frontFace.text 0 0
        >> moveText backFace.text 0 0
        >> moveText rightFace.text 0 0
        >> moveText leftFace.text 0 0
        >> moveText topFace.text 0 0
        >> moveText bottomFace.text 0 0





selectAnimation : Float -> State -> Keyframe.EngineBuilder -> Keyframe.EngineBuilder
selectAnimation targetAmount state =
    case state of
        Opening ->
            moveSidesOut targetAmount
                >> moveTextsOut

        Closing ->
            moveSidesIn targetAmount
                >> moveTextsIn

        RotatingOpen ->
            rotateCubeClockwise

        RotatingClosed ->
            rotateCubeAntiClockwise



-- UPDATE


type Msg
    = NoOp
    | GotKeyframeMsg Keyframe.AnimMsg
    | TriggerAnimation





update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        NoOp ->
            ( model, Cmd.none )

        TriggerAnimation ->
            let
                animState =
                    Keyframe.animate model.animState <|
                        selectAnimation depth model.state
            in
            ( { model | animState = animState }
            , Cmd.none
            )

        GotKeyframeMsg animMsg ->
            let
                ( animState, maybeEvent ) =
                    Keyframe.update animMsg model.animState

                nextModel =
                    { model | animState = animState }
            in
            ( case maybeEvent of
                Just animEvent ->
                    handleEvent animEvent nextModel

                Nothing ->
                    nextModel
            , Cmd.none
            )


handleEvent : Keyframe.AnimEvent -> Model -> Model
handleEvent animEvent model =
    case animEvent of
        Keyframe.Ended _ _ "cubeAnim" ->
            cubeRotationEnded model

        Keyframe.Ended _ _ "frontFaceAnim" ->
            sidesMovementEnded model

        _ ->
            model


cubeRotationEnded : Model -> Model
cubeRotationEnded model =
    case model.state of
        RotatingOpen ->
            stateChanged Closing model

        RotatingClosed ->
            stateChanged Opening model

        _ ->
            model


sidesMovementEnded : Model -> Model
sidesMovementEnded model =
    case model.state of
        Opening ->
            stateChanged RotatingOpen model

        Closing ->
            stateChanged RotatingClosed model

        _ ->
            model


stateChanged : State -> Model -> Model
stateChanged state model =
    let
        animState =
            Keyframe.animate model.animState <|
                selectAnimation depth state
    in
    { model
        | state = state
        , animState = animState
    }



-- VIEW


view : Model -> Document Msg
view model =
    { title = "Keyframe 3D Example"
    , body =
        [ Keyframe.styleNode model.animState
        , div
            [ Html.Attributes.class "example-stage"
            , id "example-stage"
            , style "width" "min(90vw, 90vh)"
            , style "height" "min(90vw, 90vh)"
            , style "container-type" "size"
            ]
            [ viewAnimationArea model
            ]
        ]
    }


viewAnimationArea : Model -> Html Msg
viewAnimationArea model =
    div
        [ -- Perspective container
          View3D.perspective 1000
        , View3D.perspectiveOrigin View3D.Center

        --
        -- Hack for Chrome on macOS GPU compositing issues with 3D transforms.
        -- Setting opacity: 0.99 forces a new compositing layer, which prevents
        -- colored rectangle artifacts that can appear during complex 3D animations.
        -- It's not perfect, some flickering can still occur.
        , View3D.opacityHack
        , id "animation-area"
        , style "display" "flex"
        , style "justify-content" "center"
        , style "align-items" "center"
        , style "width" "100%"
        , style "height" "100%"
        , style "flex" "0 0 auto"
        ]
        [ viewCube model ]





cubeSizeCss : String
cubeSizeCss =
    String.fromFloat cubeSize ++ "cqmin"


viewCube : Model -> Html Msg
viewCube model =
    div
        (Keyframe.attributes cubeGroupName model.animState
            ++ Keyframe.events GotKeyframeMsg
            ++ [ View3D.transformStyle View3D.Preserve3D
               , id "cube"
               , style "width" cubeSizeCss
               , style "height" cubeSizeCss
               , style "position" "relative"
               ]
        )
        [ viewFace model.animState frontFace
        , viewFace model.animState backFace
        , viewFace model.animState rightFace
        , viewFace model.animState leftFace
        , viewFace model.animState topFace
        , viewFace model.animState bottomFace
        ]


viewFace : Keyframe.AnimState -> FaceConfig -> Html Msg
viewFace animState config =
    div
        (Keyframe.attributes config.groupName animState
            ++ [ View3D.transformStyle View3D.Preserve3D
               , id config.id
               , style "position" "absolute"
               , style "width" cubeSizeCss
               , style "height" cubeSizeCss
               , style "background-color" config.background
               , style "border" ("2px solid " ++ config.borderColor)
               , style "box-sizing" "border-box"
               , style "display" "flex"
               , style "justify-content" "center"
               , style "align-items" "center"
               , style "font-weight" "bold"
               , style "font-size" ("calc(" ++ cubeSizeCss ++ " * 0.13)")
               ]
        )
        [ div
            [ style "color" "#ffffff"
            , style "position" "absolute"
            ]
            [ text config.label ]
        , div
            (Keyframe.attributes config.text.groupName animState
                ++ [ id config.text.id
                   , style "color" config.text.color
                   , style "position" "absolute"
                   ]
            )
            [ text config.text.label ]
        ]
module Animation.Sub.Animate3D.Main exposing (main)

import Anim.Builder exposing (AnimBuilder, ForSub)
import Anim.Engine.Sub as Sub
import Anim.Extra.View3D as View3D
import Anim.Property.Rotate as Rotate
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser exposing (Document)
import Html exposing (Html, div, text)
import Html.Attributes exposing (class, id, style)
import Json.Encode as Encode
import Motion.Easing as Easing exposing (Easing(..))
import Process
import Task



-- MAIN


main : Program { window : { width : Int, height : Int } } Model Msg
main =
    Browser.document
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }



-- MODEL


type alias Model =
    { animState : Sub.AnimState
    , state : State
    }





init : { window : { width : Int, height : Int } } -> ( Model, Cmd Msg )
init _ =
    let
        initialAnimState =
            Sub.init <|
                [ -- Bring the cube forward on the Z axis
                  -- so that it doesn't get clipped by the
                  -- z=0 clipping plane when we expand the
                  -- sides and rotate. This is a fixed 3D depth
                  -- offset, unrelated to layout, so it stays
                  -- in pixels.
                  Translate.initZ cubeGroupName 200 >> Translate.cssUnit Px

                -- Position each face in 3D space along the axis it faces.
                -- Face offsets use `Cqmin` so the cube scales proportionally
                -- with the stage's new dimensions on resize.
                -- Front/Back faces move on Z (forward/backward)
                -- Left/Right faces move on X (sideways)
                -- Top/Bottom faces move on Y (up/down)
                , Translate.initZ frontFace.groupName depth >> Translate.cssUnit Cqmin
                , Translate.initZ backFace.groupName (depth * -1)
                    >> Translate.cssUnit Cqmin
                    -- Rotate each face into position to build the cube
                    -- Front face is not rotated due to facing forward by default
                    >> Rotate.initY backFace.groupName 180
                , Translate.initX rightFace.groupName depth
                    >> Translate.cssUnit Cqmin
                    >> Rotate.initY rightFace.groupName 90
                , Translate.initX leftFace.groupName (-1 * depth)
                    >> Translate.cssUnit Cqmin
                    >> Rotate.initY leftFace.groupName -90
                , Translate.initY topFace.groupName (-1 * depth)
                    >> Translate.cssUnit Cqmin
                    >> Rotate.initX topFace.groupName 90
                , Translate.initY bottomFace.groupName depth
                    >> Translate.cssUnit Cqmin
                    >> Rotate.initX bottomFace.groupName -90

                -- The text labels all start on the same plane as their faces
                -- at z=0, which is the default starting value, so we don't need
                -- to initialize them
                ]
    in
    ( { animState = initialAnimState
      , state = Opening
      }
    , Process.sleep 0
        |> Task.perform (always TriggerAnimation)
    )





type State
    = Opening
    | Closing
    | RotatingOpen
    | RotatingClosed



-- Cube configuration


cubeGroupName : String
cubeGroupName =
    "cubeAnim"


{-| Cube edge length expressed in `Cqmin` (% of the stage's smaller
dimension). The stage applies `container-type: size`, so the cube
resizes automatically with the viewport without any Elm-side
measurement.
-}
cubeSize : Float
cubeSize =
    18


cubeSizeCss : String
cubeSizeCss =
    String.fromFloat cubeSize ++ "cqmin"


{-| Distance each face sits in front of / behind the cube centre.
Half the cube edge so adjacent faces meet at the cube corners.
-}
depth : Float
depth =
    cubeSize / 2



-- Face configuration


type alias TextConfig =
    { id : String
    , groupName : String
    , label : String
    , color : String
    }


type alias FaceConfig =
    { id : String
    , groupName : String
    , label : String
    , background : String
    , borderColor : String
    , text : TextConfig
    }


frontFace : FaceConfig
frontFace =
    { id = "front-face"
    , groupName = "frontFaceAnim"
    , label = "FRONT"
    , background = "rgb(52, 152, 219)"
    , borderColor = "rgb(41, 128, 185)"
    , text =
        { id = "front-face-text"
        , groupName = "frontFaceTextAnim"
        , label = "FRONT"
        , color = "rgb(0,0,0)"
        }
    }


backFace : FaceConfig
backFace =
    { id = "back-face"
    , groupName = "backFaceAnim"
    , label = "BACK"
    , background = "rgb(41, 128, 185)"
    , borderColor = "rgb(33, 97, 140)"
    , text =
        { id = "back-face-text"
        , groupName = "backFaceTextAnim"
        , label = "BACK"
        , color = "rgb(0,0,0)"
        }
    }


rightFace : FaceConfig
rightFace =
    { id = "right-face"
    , groupName = "rightFaceAnim"
    , label = "RIGHT"
    , background = "rgb(231, 76, 60)"
    , borderColor = "rgb(192, 57, 43)"
    , text =
        { id = "right-face-text"
        , groupName = "rightFaceTextAnim"
        , label = "RIGHT"
        , color = "rgb(0,0,0)"
        }
    }


leftFace : FaceConfig
leftFace =
    { id = "left-face"
    , groupName = "leftFaceAnim"
    , label = "LEFT"
    , background = "rgb(230, 126, 34)"
    , borderColor = "rgb(211, 84, 0)"
    , text =
        { id = "left-face-text"
        , groupName = "leftFaceTextAnim"
        , label = "LEFT"
        , color = "rgb(0,0,0)"
        }
    }


topFace : FaceConfig
topFace =
    { id = "top-face"
    , groupName = "topFaceAnim"
    , label = "TOP"
    , background = "rgb(46, 204, 113)"
    , borderColor = "rgb(39, 174, 96)"
    , text =
        { id = "top-face-text"
        , groupName = "topFaceTextAnim"
        , label = "TOP"
        , color = "rgb(0,0,0)"
        }
    }


bottomFace : FaceConfig
bottomFace =
    { id = "bottom-face"
    , groupName = "bottomFaceAnim"
    , label = "BOTTOM"
    , background = "rgb(155, 89, 182)"
    , borderColor = "rgb(142, 68, 173)"
    , text =
        { id = "bottom-face-text"
        , groupName = "bottomFaceTextAnim"
        , label = "BOTTOM"
        , color = "rgb(0,0,0)"
        }
    }



-- ANIMATIONS
--
-- CUBE
--
-- We only rotate the cube, not individual faces, they maintain their
-- position in 3D space because we use `View3D.transformStyle View3D.Preserve3D`
-- on the cube container


rotateCube : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
rotateCube to =
    Sub.for cubeGroupName
        >> Rotate.begin
        >> Rotate.toXYZ to to to
        >> Rotate.easing BackInOut
        >> Rotate.duration 8000
        >> Rotate.end


rotateCubeClockwise : Sub.EngineBuilder -> Sub.EngineBuilder
rotateCubeClockwise =
    rotateCube 360


rotateCubeAntiClockwise : Sub.EngineBuilder -> Sub.EngineBuilder
rotateCubeAntiClockwise =
    rotateCube 0



-- SIDES


moveSidesOut : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
moveSidesOut targetAmount =
    moveFrontFaceOut targetAmount
        >> moveBackFaceOut targetAmount
        >> moveRightFaceOut targetAmount
        >> moveLeftFaceOut targetAmount
        >> moveTopFaceOut targetAmount
        >> moveBottomFaceOut targetAmount


moveSidesIn : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
moveSidesIn targetAmount =
    moveFrontFaceIn targetAmount
        >> moveBackFaceIn targetAmount
        >> moveRightFaceIn targetAmount
        >> moveLeftFaceIn targetAmount
        >> moveTopFaceIn targetAmount
        >> moveBottomFaceIn targetAmount


sharedTiming : Sub.EngineBuilder -> Sub.EngineBuilder
sharedTiming =
    Sub.duration 1000
        >> Sub.easing CircInOut


moveFace : FaceConfig -> (Translate.Builder ForSub -> Translate.Builder ForSub) -> Sub.EngineBuilder -> Sub.EngineBuilder
moveFace config moveToBuilder =
    sharedTiming
        >> Sub.for config.groupName
        >> Translate.begin
        >> moveToBuilder
        >> Translate.end



-- Each face moves along the axis it faces by a `moveAmount` (expressed in
-- `Cqmin`, so it scales with the stage) when the cube expands, and moves
-- back to its original position when the cube closes.
--
-- Front/Back faces move on Z (forward/backward)
-- Left/Right faces move on X (sideways)
-- Top/Bottom faces move on Y (up/down)


moveAmount : Float
moveAmount =
    10


moveFrontFaceOut : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
moveFrontFaceOut toZ =
    moveFace frontFace <|
        Translate.toZ (toZ + moveAmount)


moveFrontFaceIn : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
moveFrontFaceIn toZ =
    moveFace frontFace <|
        Translate.toZ toZ


moveBackFaceOut : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
moveBackFaceOut toZ =
    moveFace backFace <|
        Translate.toZ (-1 * toZ - moveAmount)


moveBackFaceIn : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
moveBackFaceIn toZ =
    moveFace backFace <|
        Translate.toZ (-1 * toZ)


moveRightFaceOut : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
moveRightFaceOut toX =
    moveFace rightFace <|
        Translate.toX (toX + moveAmount)


moveRightFaceIn : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
moveRightFaceIn toX =
    moveFace rightFace <|
        Translate.toX toX


moveLeftFaceOut : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
moveLeftFaceOut toX =
    moveFace leftFace <|
        Translate.toX (-1 * toX - moveAmount)


moveLeftFaceIn : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
moveLeftFaceIn toX =
    moveFace leftFace <|
        Translate.toX (-1 * toX)


moveTopFaceOut : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
moveTopFaceOut toY =
    moveFace topFace <|
        Translate.toY (-1 * toY - moveAmount)


moveTopFaceIn : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
moveTopFaceIn toY =
    moveFace topFace <|
        Translate.toY (-1 * toY)


moveBottomFaceOut : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
moveBottomFaceOut toY =
    moveFace bottomFace <|
        Translate.toY (toY + moveAmount)


moveBottomFaceIn : Float -> Sub.EngineBuilder -> Sub.EngineBuilder
moveBottomFaceIn toY =
    moveFace bottomFace <|
        Translate.toY toY



-- TEXT
--
-- Text moves forward (Z+4cqmin) and rotates (to Z=360deg) when sides expand,
-- and then moves back (to Z=0cqmin) and rotates back (to Z=0deg) when sides close


textMoveAmount : Float
textMoveAmount =
    14


moveText : TextConfig -> Float -> Float -> Sub.EngineBuilder -> Sub.EngineBuilder
moveText config toZ toRotate =
    sharedTiming
        >> Sub.for config.groupName
        >> Translate.begin
        >> Translate.toZ toZ
        >> Translate.end
        >> Sub.for config.groupName
        >> Rotate.begin
        >> Rotate.toZ toRotate
        >> Rotate.end


moveTextsOut : Sub.EngineBuilder -> Sub.EngineBuilder
moveTextsOut =
    moveText frontFace.text textMoveAmount 360
        >> moveText backFace.text textMoveAmount 360
        >> moveText rightFace.text textMoveAmount 360
        >> moveText leftFace.text textMoveAmount 360
        >> moveText topFace.text textMoveAmount 360
        >> moveText bottomFace.text textMoveAmount 360


moveTextsIn : Sub.EngineBuilder -> Sub.EngineBuilder
moveTextsIn =
    moveText frontFace.text 0 0
        >> moveText backFace.text 0 0
        >> moveText rightFace.text 0 0
        >> moveText leftFace.text 0 0
        >> moveText topFace.text 0 0
        >> moveText bottomFace.text 0 0





selectAnimation : Float -> State -> Sub.EngineBuilder -> Sub.EngineBuilder
selectAnimation targetAmount state =
    case state of
        Opening ->
            moveSidesOut targetAmount
                >> moveTextsOut

        Closing ->
            moveSidesIn targetAmount
                >> moveTextsIn

        RotatingOpen ->
            rotateCubeClockwise

        RotatingClosed ->
            rotateCubeAntiClockwise



-- UPDATE


type Msg
    = NoOp
    | GotSubMsg Sub.AnimMsg
    | TriggerAnimation





update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        NoOp ->
            ( model, Cmd.none )

        TriggerAnimation ->
            ( { model
                | animState =
                    Sub.animate model.animState <|
                        selectAnimation depth model.state
              }
            , Cmd.none
            )

        GotSubMsg animMsg ->
            let
                ( animState, animEvents ) =
                    Sub.update animMsg model.animState
            in
            ( List.foldl handleMotionEvent { model | animState = animState } animEvents
            , Cmd.none
            )


handleMotionEvent : Sub.AnimEvent -> Model -> Model
handleMotionEvent animEvent model =
    case animEvent of
        Sub.Ended "cubeAnim" ->
            cubeRotationEnded model

        Sub.Ended "frontFaceAnim" ->
            sidesMovementEnded model

        _ ->
            model


cubeRotationEnded : Model -> Model
cubeRotationEnded model =
    case model.state of
        RotatingOpen ->
            stateChanged Closing model

        RotatingClosed ->
            stateChanged Opening model

        _ ->
            model


sidesMovementEnded : Model -> Model
sidesMovementEnded model =
    case model.state of
        Opening ->
            stateChanged RotatingOpen model

        Closing ->
            stateChanged RotatingClosed model

        _ ->
            model


stateChanged : State -> Model -> Model
stateChanged state model =
    { model
        | state = state
        , animState =
            Sub.animate model.animState <|
                selectAnimation depth state
    }



-- SUBSCRIPTIONS


subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.subscriptions GotSubMsg model.animState



-- VIEW


view : Model -> Document Msg
view model =
    { title = "Sub 3D Example"
    , body =
        [ div
            [ class "example-stage"
            , id "example-stage"
            , style "width" "min(90vw, 90vh)"
            , style "height" "min(90vw, 90vh)"
            , style "container-type" "size"
            ]
            [ viewAnimationArea model
            ]
        ]
    }


viewAnimationArea : Model -> Html Msg
viewAnimationArea model =
    div
        [ -- Perspective container
          View3D.perspective 1000
        , View3D.perspectiveOrigin View3D.Center

        --
        -- Hack for Chrome on macOS GPU compositing issues with 3D transforms.
        -- Setting opacity: 0.99 forces a new compositing layer, which prevents
        -- the colored rectangle artifacts that can appear during complex 3D animations.
        -- It's not perfect, some flickering can still occur.
        , View3D.opacityHack
        , id "animation-area"
        , style "display" "flex"
        , style "justify-content" "center"
        , style "align-items" "center"
        , style "width" "100%"
        , style "height" "100%"
        , style "flex" "0 0 auto"
        ]
        [ viewCube model ]





viewCube : Model -> Html Msg
viewCube model =
    div
        (Sub.attributes cubeGroupName model.animState
            ++ [ View3D.transformStyle View3D.Preserve3D
               , id "cube"
               , style "width" cubeSizeCss
               , style "height" cubeSizeCss
               , style "position" "relative"
               ]
        )
        [ viewFace model.animState frontFace
        , viewFace model.animState backFace
        , viewFace model.animState rightFace
        , viewFace model.animState leftFace
        , viewFace model.animState topFace
        , viewFace model.animState bottomFace
        ]


viewFace : Sub.AnimState -> FaceConfig -> Html Msg
viewFace animState config =
    div
        (Sub.attributes config.groupName animState
            ++ [ View3D.transformStyle View3D.Preserve3D
               , id config.id
               , style "position" "absolute"
               , style "width" cubeSizeCss
               , style "height" cubeSizeCss
               , style "background-color" config.background
               , style "border" ("2px solid " ++ config.borderColor)
               , style "box-sizing" "border-box"
               , style "display" "flex"
               , style "justify-content" "center"
               , style "align-items" "center"
               , style "font-weight" "bold"
               , style "font-size" ("calc(" ++ cubeSizeCss ++ " * 0.13)")
               ]
        )
        [ div
            [ style "color" "#ffffff"
            , style "position" "absolute"
            ]
            [ text config.label ]
        , div
            (Sub.attributes config.text.groupName animState
                ++ [ id config.text.id
                   , style "color" config.text.color
                   , style "position" "absolute"
                   ]
            )
            [ text config.text.label ]
        ]
port module Animation.WAAPI.Animate3D.Main exposing (main)

import Anim.Builder exposing (AnimBuilder, ForWAAPI)
import Anim.Engine.WAAPI as WAAPI
import Anim.Extra.View3D as View3D
import Anim.Property.Rotate as Rotate
import Anim.Property.Translate as Translate
import Anim.Unit exposing (Unit(..))
import Browser exposing (Document)
import Html exposing (Html, div, text)
import Html.Attributes exposing (id, style)
import Json.Encode as Encode
import Motion.Easing as Easing exposing (Easing(..))
import Process
import Task



-- PORTS


port motionCmd : Encode.Value -> Cmd msg


port motionMsg : (Encode.Value -> msg) -> Sub msg



-- MAIN


main : Program () Model Msg
main =
    Browser.document
        { init = \_ -> init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }



-- MODEL


type alias Model =
    { animState : WAAPI.AnimState Msg
    , state : State
    }





init : ( Model, Cmd Msg )
init =
    let
        initialAnimState =
            WAAPI.init motionCmd motionMsg <|
                [ -- Bring the cube forward on the Z axis
                  -- so that it doesn't get clipped by the
                  -- z=0 clipping plane when we expand the
                  -- sides and rotate. This is a fixed 3D depth
                  -- offset, unrelated to layout, so it stays
                  -- in pixels.
                  Translate.initZ cubeGroupName 200 >> Translate.cssUnit Px

                -- Position each face in 3D space along the axis it faces.
                -- Face offsets use `Cqmin` so the cube scales proportionally
                -- with the stage's new dimensions on resize.
                -- Front/Back faces move on Z (forward/backward)
                -- Left/Right faces move on X (sideways)
                -- Top/Bottom faces move on Y (up/down)
                , Translate.initZ frontFace.groupName depth >> Translate.cssUnit Cqmin
                , Translate.initZ backFace.groupName (depth * -1)
                    >> Translate.cssUnit Cqmin
                    -- Rotate each face into position to build the cube
                    -- Front face is not rotated due to facing forward by default
                    >> Rotate.initY backFace.groupName 180
                , Translate.initX rightFace.groupName depth
                    >> Translate.cssUnit Cqmin
                    >> Rotate.initY rightFace.groupName 90
                , Translate.initX leftFace.groupName (-1 * depth)
                    >> Translate.cssUnit Cqmin
                    >> Rotate.initY leftFace.groupName -90
                , Translate.initY topFace.groupName (-1 * depth)
                    >> Translate.cssUnit Cqmin
                    >> Rotate.initX topFace.groupName 90
                , Translate.initY bottomFace.groupName depth
                    >> Translate.cssUnit Cqmin
                    >> Rotate.initX bottomFace.groupName -90

                -- The text labels all start on the same plane as their faces
                -- at z=0, which is the default starting value, so we don't need
                -- to initialize them
                ]
    in
    ( { animState = initialAnimState
      , state = Opening
      }
    , Process.sleep 0
        |> Task.perform (always TriggerAnimation)
    )





type State
    = Opening
    | Closing
    | RotatingOpen
    | RotatingClosed



-- Cube configuration


cubeGroupName : String
cubeGroupName =
    "cubeAnim"


{-| Cube edge length expressed in `Cqmin` (% of the stage's smaller
dimension). The stage applies `container-type: size`, so the cube
resizes automatically with the viewport without any Elm-side
measurement.
-}
cubeSize : Float
cubeSize =
    18


cubeSizeCss : String
cubeSizeCss =
    String.fromFloat cubeSize ++ "cqmin"


{-| Distance each face sits in front of / behind the cube centre.
Half the cube edge so adjacent faces meet at the cube corners.
-}
depth : Float
depth =
    cubeSize / 2



-- Face configuration


type alias TextConfig =
    { id : String
    , groupName : String
    , label : String
    , color : String
    }


type alias FaceConfig =
    { id : String
    , groupName : String
    , label : String
    , background : String
    , borderColor : String
    , text : TextConfig
    }


frontFace : FaceConfig
frontFace =
    { id = "front-face"
    , groupName = "frontFaceAnim"
    , label = "FRONT"
    , background = "rgb(52, 152, 219)"
    , borderColor = "rgb(41, 128, 185)"
    , text =
        { id = "front-face-text"
        , groupName = "frontFaceTextAnim"
        , label = "FRONT"
        , color = "rgb(0,0,0)"
        }
    }


backFace : FaceConfig
backFace =
    { id = "back-face"
    , groupName = "backFaceAnim"
    , label = "BACK"
    , background = "rgb(41, 128, 185)"
    , borderColor = "rgb(33, 97, 140)"
    , text =
        { id = "back-face-text"
        , groupName = "backFaceTextAnim"
        , label = "BACK"
        , color = "rgb(0,0,0)"
        }
    }


rightFace : FaceConfig
rightFace =
    { id = "right-face"
    , groupName = "rightFaceAnim"
    , label = "RIGHT"
    , background = "rgb(231, 76, 60)"
    , borderColor = "rgb(192, 57, 43)"
    , text =
        { id = "right-face-text"
        , groupName = "rightFaceTextAnim"
        , label = "RIGHT"
        , color = "rgb(0,0,0)"
        }
    }


leftFace : FaceConfig
leftFace =
    { id = "left-face"
    , groupName = "leftFaceAnim"
    , label = "LEFT"
    , background = "rgb(230, 126, 34)"
    , borderColor = "rgb(211, 84, 0)"
    , text =
        { id = "left-face-text"
        , groupName = "leftFaceTextAnim"
        , label = "LEFT"
        , color = "rgb(0,0,0)"
        }
    }


topFace : FaceConfig
topFace =
    { id = "top-face"
    , groupName = "topFaceAnim"
    , label = "TOP"
    , background = "rgb(46, 204, 113)"
    , borderColor = "rgb(39, 174, 96)"
    , text =
        { id = "top-face-text"
        , groupName = "topFaceTextAnim"
        , label = "TOP"
        , color = "rgb(0,0,0)"
        }
    }


bottomFace : FaceConfig
bottomFace =
    { id = "bottom-face"
    , groupName = "bottomFaceAnim"
    , label = "BOTTOM"
    , background = "rgb(155, 89, 182)"
    , borderColor = "rgb(142, 68, 173)"
    , text =
        { id = "bottom-face-text"
        , groupName = "bottomFaceTextAnim"
        , label = "BOTTOM"
        , color = "rgb(0,0,0)"
        }
    }



-- ANIMATIONS
--
-- CUBE
--
-- We only rotate the cube, not individual faces, they maintain their
-- position in 3D space because we use `View3D.transformStyle View3D.Preserve3D`
-- on the cube container


rotateCube : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
rotateCube to =
    WAAPI.for cubeGroupName
        >> Rotate.begin
        >> Rotate.toXYZ to to to
        >> Rotate.easing BackInOut
        >> Rotate.duration 8000
        >> Rotate.end


rotateCubeClockwise : WAAPI.EngineBuilder -> WAAPI.EngineBuilder
rotateCubeClockwise =
    rotateCube 360


rotateCubeAntiClockwise : WAAPI.EngineBuilder -> WAAPI.EngineBuilder
rotateCubeAntiClockwise =
    rotateCube 0



-- SIDES


moveSidesOut : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveSidesOut targetAmount =
    moveFrontFaceOut targetAmount
        >> moveBackFaceOut targetAmount
        >> moveRightFaceOut targetAmount
        >> moveLeftFaceOut targetAmount
        >> moveTopFaceOut targetAmount
        >> moveBottomFaceOut targetAmount


moveSidesIn : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveSidesIn targetAmount =
    moveFrontFaceIn targetAmount
        >> moveBackFaceIn targetAmount
        >> moveRightFaceIn targetAmount
        >> moveLeftFaceIn targetAmount
        >> moveTopFaceIn targetAmount
        >> moveBottomFaceIn targetAmount


sharedTiming : WAAPI.EngineBuilder -> WAAPI.EngineBuilder
sharedTiming =
    WAAPI.duration 1000
        >> WAAPI.easing CircInOut


moveFace : FaceConfig -> (Translate.Builder ForWAAPI -> Translate.Builder ForWAAPI) -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveFace config moveToBuilder =
    sharedTiming
        >> WAAPI.for config.groupName
        >> Translate.begin
        >> moveToBuilder
        >> Translate.end



-- Each face moves along the axis it faces by a `moveAmount` (expressed in
-- `Cqmin`, so it scales with the stage) when the cube expands, and moves
-- back to its original position when the cube closes.
--
-- Front/Back faces move on Z (forward/backward)
-- Left/Right faces move on X (sideways)
-- Top/Bottom faces move on Y (up/down)


moveAmount : Float
moveAmount =
    10


moveFrontFaceOut : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveFrontFaceOut toZ =
    moveFace frontFace <|
        Translate.toZ (toZ + moveAmount)


moveFrontFaceIn : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveFrontFaceIn toZ =
    moveFace frontFace <|
        Translate.toZ toZ


moveBackFaceOut : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveBackFaceOut toZ =
    moveFace backFace <|
        Translate.toZ (-1 * toZ - moveAmount)


moveBackFaceIn : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveBackFaceIn toZ =
    moveFace backFace <|
        Translate.toZ (-1 * toZ)


moveRightFaceOut : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveRightFaceOut toX =
    moveFace rightFace <|
        Translate.toX (toX + moveAmount)


moveRightFaceIn : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveRightFaceIn toX =
    moveFace rightFace <|
        Translate.toX toX


moveLeftFaceOut : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveLeftFaceOut toX =
    moveFace leftFace <|
        Translate.toX (-1 * toX - moveAmount)


moveLeftFaceIn : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveLeftFaceIn toX =
    moveFace leftFace <|
        Translate.toX (-1 * toX)


moveTopFaceOut : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveTopFaceOut toY =
    moveFace topFace <|
        Translate.toY (-1 * toY - moveAmount)


moveTopFaceIn : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveTopFaceIn toY =
    moveFace topFace <|
        Translate.toY (-1 * toY)


moveBottomFaceOut : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveBottomFaceOut toY =
    moveFace bottomFace <|
        Translate.toY (toY + moveAmount)


moveBottomFaceIn : Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveBottomFaceIn toY =
    moveFace bottomFace <|
        Translate.toY toY



-- TEXT
--
-- Text moves forward (Z+4cqmin) and rotates (to Z=360deg) when sides expand,
-- and then moves back (to Z=0cqmin) and rotates back (to Z=0deg) when sides close


textMoveAmount : Float
textMoveAmount =
    14


moveText : TextConfig -> Float -> Float -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveText config toZ toRotate =
    sharedTiming
        >> WAAPI.for config.groupName
        >> Translate.begin
        >> Translate.toZ toZ
        >> Translate.end
        >> Rotate.begin
        >> Rotate.toZ toRotate
        >> Rotate.end


moveTextsOut : WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveTextsOut =
    moveText frontFace.text textMoveAmount 360
        >> moveText backFace.text textMoveAmount 360
        >> moveText rightFace.text textMoveAmount 360
        >> moveText leftFace.text textMoveAmount 360
        >> moveText topFace.text textMoveAmount 360
        >> moveText bottomFace.text textMoveAmount 360


moveTextsIn : WAAPI.EngineBuilder -> WAAPI.EngineBuilder
moveTextsIn =
    moveText frontFace.text 0 0
        >> moveText backFace.text 0 0
        >> moveText rightFace.text 0 0
        >> moveText leftFace.text 0 0
        >> moveText topFace.text 0 0
        >> moveText bottomFace.text 0 0





selectAnimation : Float -> State -> WAAPI.EngineBuilder -> WAAPI.EngineBuilder
selectAnimation targetAmount state =
    case state of
        Opening ->
            moveSidesOut targetAmount
                >> moveTextsOut

        Closing ->
            moveSidesIn targetAmount
                >> moveTextsIn

        RotatingOpen ->
            rotateCubeClockwise

        RotatingClosed ->
            rotateCubeAntiClockwise



-- UPDATE


type Msg
    = NoOp
    | GotWaapiMsg WAAPI.AnimMsg
    | TriggerAnimation





update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        NoOp ->
            ( model, Cmd.none )

        TriggerAnimation ->
            let
                ( animState, cmd ) =
                    WAAPI.animate model.animState <|
                        selectAnimation depth model.state
            in
            ( { model | animState = animState }
            , cmd
            )

        GotWaapiMsg animMsg ->
            let
                ( animState, maybeAnimEvent ) =
                    WAAPI.update animMsg model.animState
            in
            case maybeAnimEvent of
                Just animEvent ->
                    handleMotionEvent animEvent { model | animState = animState }

                Nothing ->
                    ( { model | animState = animState }, Cmd.none )


handleMotionEvent : WAAPI.AnimEvent -> Model -> ( Model, Cmd Msg )
handleMotionEvent animEvent model =
    case animEvent of
        WAAPI.Ended "cubeAnim" ->
            cubeRotationEnded model

        WAAPI.Ended "frontFaceAnim" ->
            sidesMovementEnded model

        _ ->
            ( model, Cmd.none )


cubeRotationEnded : Model -> ( Model, Cmd Msg )
cubeRotationEnded model =
    case model.state of
        RotatingOpen ->
            stateChanged Closing model

        RotatingClosed ->
            stateChanged Opening model

        _ ->
            ( model, Cmd.none )


sidesMovementEnded : Model -> ( Model, Cmd Msg )
sidesMovementEnded model =
    case model.state of
        Opening ->
            stateChanged RotatingOpen model

        Closing ->
            stateChanged RotatingClosed model

        _ ->
            ( model, Cmd.none )


stateChanged : State -> Model -> ( Model, Cmd Msg )
stateChanged state model =
    let
        ( animState, cmd ) =
            WAAPI.animate model.animState <|
                selectAnimation depth state
    in
    ( { model
        | state = state
        , animState = animState
      }
    , cmd
    )



-- SUBSCRIPTIONS


subscriptions : Model -> Sub Msg
subscriptions model =
    WAAPI.subscriptions GotWaapiMsg model.animState



-- VIEW


view : Model -> Document Msg
view model =
    { title = "WAAPI 3D Example"
    , body =
        [ div
            [ Html.Attributes.class "example-stage"
            , id "example-stage"
            , style "width" "min(90vw, 90vh)"
            , style "height" "min(90vw, 90vh)"
            , style "container-type" "size"
            ]
            [ viewAnimationArea model
            ]
        ]
    }


viewAnimationArea : Model -> Html Msg
viewAnimationArea model =
    div
        [ -- Perspective container
          View3D.perspective 1000
        , View3D.perspectiveOrigin View3D.Center

        --
        -- Hack for Chrome on macOS GPU compositing issues with 3D transforms.
        -- Setting opacity: 0.99 forces a new compositing layer, which prevents
        -- the colored rectangle artifacts that can appear during complex 3D animations.
        -- It's not perfect, some flickering can still occur.
        , View3D.opacityHack
        , id "animation-area"
        , style "display" "flex"
        , style "justify-content" "center"
        , style "align-items" "center"
        , style "width" "100%"
        , style "height" "100%"
        , style "flex" "0 0 auto"
        ]
        [ viewCube model ]





viewCube : Model -> Html Msg
viewCube model =
    div
        (WAAPI.attributes cubeGroupName model.animState
            ++ [ View3D.transformStyle View3D.Preserve3D
               , id "cube"
               , style "width" cubeSizeCss
               , style "height" cubeSizeCss
               , style "position" "relative"
               ]
        )
        [ viewFace model.animState frontFace
        , viewFace model.animState backFace
        , viewFace model.animState rightFace
        , viewFace model.animState leftFace
        , viewFace model.animState topFace
        , viewFace model.animState bottomFace
        ]


viewFace : WAAPI.AnimState Msg -> FaceConfig -> Html Msg
viewFace animState config =
    div
        (WAAPI.attributes config.groupName animState
            ++ [ View3D.transformStyle View3D.Preserve3D
               , id config.id
               , style "position" "absolute"
               , style "width" cubeSizeCss
               , style "height" cubeSizeCss
               , style "background-color" config.background
               , style "border" ("2px solid " ++ config.borderColor)
               , style "box-sizing" "border-box"
               , style "display" "flex"
               , style "justify-content" "center"
               , style "align-items" "center"
               , style "font-weight" "bold"
               , style "font-size" ("calc(" ++ cubeSizeCss ++ " * 0.13)")
               ]
        )
        [ div
            [ style "color" "#ffffff"
            , style "position" "absolute"
            ]
            [ text config.label ]
        , div
            (WAAPI.attributes config.text.groupName animState
                ++ [ id config.text.id
                   , style "color" config.text.color
                   , style "position" "absolute"
                   ]
            )
            [ text config.text.label ]
        ]

Setting Up 3D

Everything you need for 3D animations is in the Anim.Extra.View3D module. All that is required to create a 3D scene is perspective.

View3D Module

The Anim.Extra.View3D module provides four key functions, all of which operate on your view layer. perspective is always required - the others depend on your use case:

perspective : Float -> Attribute msg

Set the perspective depth on a container element.

3D animations require perspective to create depth. Without perspective, they will appear flat. When setting perspective, you set the distance between the viewer and the z=0 plane (where elements sit by default). Lower values create more dramatic 3D effects because the viewer is closer to the z=0 plane; higher values are more subtle (further away).

Value Effect
500-800px Dramatic, close-up 3D effect
800-1500px Natural, balanced perspective (recommended)
1500px+ Subtle, distant 3D effect
View Source Code
View3D.perspective 1000  -- 1000px perspective depth

Perspective defines where the 3D scene begins and applies directly to an element's children. For nested 3D scenes see transformStyle.


transformStyle : TransformStyle -> Attribute msg

Set a child elements 3D positioning relative to its parent.

By default, child elements are flattened into their parent's plane — if a parent rotates in 3D, children don't maintain their own 3D positioning relative to the parent. Use Preserve3D when you need children to exist in the same 3D space as their parent, such as building a 3D cube from six faces or creating layered card stacks.

For nested 3D animations, all elements must Preserve3D, any element that does not Preserve3D will break the 3D context, and child elements will then be flattened into 2D space.

View Source Code
View3D.transformStyle View3D.Preserve3D  -- Children maintain 3D positioning
View3D.transformStyle View3D.Flat        -- Children flattened (default)

perspectiveOrigin : PerspectiveOrigin -> Attribute msg

Control where the viewer is looking from.

View Source Code
View3D.perspectiveOrigin Center           -- center/middle of scene (default)
View3D.perspectiveOrigin TopLeft          -- top-left, (0,0) coordinate
View3D.perspectiveOrigin (Px 100 50)      -- X and Y coordinate in pixels
View3D.perspectiveOrigin (Percent 25 75)  -- X and Y coordinate as a percentage of the axes

Available positions: Center, TopLeft, TopCenter, TopRight, LeftMiddle, RightMiddle, BottomLeft, BottomCenter, BottomRight, Percent Float Float, Px Float Float


backfaceVisibility : BackfaceVisibility -> Attribute msg

Show or hide the back face of your element.

View Source Code
View3D.backfaceVisibility View3D.Hidden   -- Hide when facing away
View3D.backfaceVisibility View3D.Visible  -- Always visible (default)

When an element rotates past 90° on the X or Y axis, you're looking at its "back face" — like flipping a playing card. By default, the back face shows a mirrored version of the front. Set it to Hidden to make elements invisible when facing away, which is essential for card-flip effects with separate front and back elements.


Performance Tips

GPU Acceleration

All 3D transforms are GPU-accelerated. They're as performant as 2D transforms.

Avoid layout thrashing

Keep perspective containers stable. Changing their dimensions during animation can cause performance issues.

Use transform-style

For nested 3D elements, use View3D.transformStyle View3D.Preserve3D to maintain 3D context.

Chrome & 3D rendering

Complex 3D animations may cause rendering artifacts in Chrome on macOS (colored rectangles appearing over the page). Apply View3D.opacityHack to the direct parent of the animated element to fix this.

Next Steps

Play with, and learn from the examples.

Examples →

Or, learn how to scroll with Elm Motion.

Scrolling →