module Photos where import Array exposing (..) import Html exposing (..) import Html.Events exposing (..) import Html.Attributes exposing (..) import Anima exposing (..) import Graphics.Element as El import Graphics.Collage as Co import Graphics.Input as In import Touch import Color import Window import Signal import Debug exposing (..) import Time import Automaton as Auto exposing ((>>>)) import Focus exposing ((=>)) {- A simple viewer for a list of photos. Clicking on the currently displayed photo will switch to the next one using a 3D transition. The animation is interruptible and continuous. -} type Input = Quiet | Click | Next | Prev {- We display one photo at a time. currentPhoto indicates which photo as well as how we got to it. The actual photo index is obtained by taking modulo numberOfPhotos. -} type alias Model = { photoURLs : Array String, currentPhoto : Int } {- Each photo is shown for a certain range of angles. The mapping from angles to the photo to be shown is given by `angleToPhotoIndex`. The `angle` is the stable angle at which the `currentPhoto` as indicated by the model should be shown. The photo index provided by the `angleToPhotoIndex` function is to be interpreted modulo the number of photos, so this record holds this modulus as well. -} type alias Direction = { angle : Float, numPhotos : Int, angleToPhotoIndex : Float -> Int } {- The view state can often be the same structure as the direction, but in this case, it is a little different, as the viewer needs to know which photo to display and how to display it. Which photo is given by `photoIndex` and the "how" part is jointly indicated by `angle` and `flip`. While `angle` looks like the same value in the direction, this is the *instantaneous* angle, as opposed to the stable angle indicated in the direction. `flip` tells the viewer whether the image is to be flipped horizontally. -} type alias ViewState = { angle : Float, photoIndex : Int, flip : Bool } clicks = Signal.mailbox Quiet input = clicks.signal photoURLs = [ "http://sriku.org/demos/elm-anima/pictures/k1.jpg" , "http://sriku.org/demos/elm-anima/pictures/k2.jpg" , "http://sriku.org/demos/elm-anima/pictures/k3.jpg" , "http://sriku.org/demos/elm-anima/pictures/k4.jpg" , "http://sriku.org/demos/elm-anima/pictures/k5.jpg" , "http://sriku.org/demos/elm-anima/pictures/k6.jpg" , "http://sriku.org/demos/elm-anima/pictures/k7.jpg" , "http://sriku.org/demos/elm-anima/pictures/k8.jpg" ] initial = { model = { photoURLs = Array.fromList photoURLs, currentPhoto = 0 } , direction = { angle = 0.0 , numPhotos = List.length photoURLs , angleToPhotoIndex = \angle -> floor ((angle + 90) / 180) } , viewstate = { angle = 0.0, photoIndex = 0, flip = False } , view = hiddenImageLoaders } app : OpinionatedApp Input Model Direction ViewState Html app = { {- The modeller looks after changing the "current photo" in response to user input. -} modeller = \input model -> case input of Next -> { model | currentPhoto = model.currentPhoto + 1 } Prev -> { model | currentPhoto = model.currentPhoto - 1 } Click -> app.modeller Next model _ -> model {- The director gives the angle at which the current photo should be displayed in the stable state. -} , director = \(input, model) dir -> let data = dir.data in {dir | data = {data | angle = 180.0 * toFloat model.currentPhoto}} {- The animator moves the angle from the current value to the value desired by the director. The animator also decides, based on the angle it computes, which image is to be shown at that angle. Depending on the angle, the image may need to be flipped along the x axis. So the animator also computes that information. -} , animator = filterProp angle_ (springy 1.0 1.2 initial.direction.angle) >>> Anima.pureData (\a -> let ix = a.angleToPhotoIndex a.angle in { angle = a.angle , photoIndex = ix % a.numPhotos , flip = ix % 2 == 1 }) {- The view renders the will of the animator. In this case, the animator has computed for us the photo to be shown, the angle at which it should be shown, and whether we should flip it along the x axis. -} , viewer = \(model, vs) -> case (Array.get vs.data.photoIndex model.photoURLs) of Just url -> box vs.env.width vs.env.height [ hiddenImageLoaders , let dx = 0.25 * toFloat vs.env.width dy = 0.15 * toFloat vs.env.height in picture url 512 512 dx dy vs.data.angle vs.data.flip [ onClick clicks.address Click ] ] Nothing -> text "No such image" , initial = initial } main = let (app', _) = Anima.runOpinionatedApp app input in app' -- Puts some content within a black box of given dimensions. box width height contents = let widthStr = toString width ++ "px" heightStr = toString height ++ "px" in div [ style [ ("width", widthStr) , ("height", heightStr) , ("background", "black") ] ] contents {- Shows a picture drawn from the given url, at the given dimensions fitting it in such a way that the picture's aspect ratio is preserved. The whole picture is translated and rotated according to the given dx,dy and angle. -} picture url width height dx dy angle flip attrs = let transform = perspective 1024 ++ translate3d dx dy 0.0 ++ rotateY angle ++ flipX flip in div ([ style [ ("width", toString width ++ "px") , ("height", toString height ++ "px") , ("position", "absolute") , ("background-image", "url(\"" ++ url ++ "\")") , ("background-size", "contain") , ("background-position", "center") , ("background-repeat", "no-repeat") , ("-webkit-transform", transform) , ("-moz-transform", transform) , ("transform", transform) ] ] ++ attrs) [] -- Useful to trigger loading of images which will otherwise be loaded only one by one. hiddenImageLoaders = div [hidden True] (List.map (\p -> img [src p] []) photoURLs) -- Utility functions for calculating the transform css string. perspective n = "perspective(" ++ toString n ++ "px)" translate3d dx dy dz = "translate3d(" ++ toString dx ++ "px," ++ toString dy ++ "px," ++ toString dz ++ "px)" rotateY deg = "rotateY(" ++ toString deg ++ "deg)" flipX flip = if flip then "scale3d(-1.0,1.0,1.0)" else "" angle_ = data_ => Focus.create .angle (\f rec -> {rec | angle = f rec.angle})