-
Notifications
You must be signed in to change notification settings - Fork 1
/
photos.elm
170 lines (146 loc) · 6.93 KB
/
photos.elm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
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})