alongslide 0.9.1 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,423 @@
1
+ #
2
+ # alongslide/scrolling.coffee: Skrollr wrapper.
3
+ #
4
+ # Copyright 2013 Canopy Canopy Canopy, Inc.
5
+ # Authors Adam Florin & Anthony Tran
6
+ #
7
+ class Alongslide::Scrolling
8
+
9
+ # For applyTransition.
10
+ #
11
+ TRANSITIONS:
12
+ in: [-1..0]
13
+ out: [0..1]
14
+
15
+ FLIP_THRESHOLD: 0.04
16
+ WAIT_BEFORE_RESET_MS: 250
17
+ SLIDE_DURATION_MS: 250
18
+ FORCE_SLIDE_DURATION_MS: 100
19
+ NUM_WHEEL_HISTORY_EVENTS: 10
20
+ MAGNITUDE_THRESHOLD: 2.2
21
+
22
+ currentPosition: 0
23
+ indexedTransitions: {}
24
+
25
+ # For desktop scroll throttling.
26
+ #
27
+ wheelHistory: []
28
+ lastAverageMagnitude: 0
29
+ ignoreScroll: false
30
+ lastRequestedPosition: 0
31
+
32
+ mouseDown: false
33
+
34
+ constructor: (options= {}) ->
35
+ {@frames} = options
36
+
37
+ @skrollr = skrollr.init
38
+ emitEvents: true
39
+ horizontal: true
40
+ edgeStrategy: 'set'
41
+ render: @snap
42
+ easing:
43
+ easeInOutQuad: (p) ->
44
+ if p < 0.5
45
+ Math.pow(p*2, 1.5) / 2
46
+ else
47
+ 1 - Math.pow((p * -2 + 2), 1.5) / 2;
48
+ easeOutQuad: (p) ->
49
+ 1 - Math.pow(1 - p, 2)
50
+
51
+ @arrowKeys()
52
+
53
+ @events()
54
+
55
+ unless @skrollr.isMobile()
56
+ @throttleScrollEvents()
57
+ @monitorScroll()
58
+ @monitorMouse()
59
+
60
+ # Init skrollr once content data attributes are populated.
61
+ #
62
+ # @param - frameAspect - percentage horizontal offset (0.-1.)
63
+ # to factor in when applying scroll transitions
64
+ #
65
+ render: (@frameAspect, lastFramePosition) ->
66
+ @applyTransitions(lastFramePosition)
67
+ @skrollr.refresh()
68
+
69
+ # TODO: write API that injects functions
70
+ # i.e. functionName(event, frame number, function) `('before', 3, animateTable)`
71
+ events: =>
72
+ @frames.on 'skrollrBefore', (e) -> e.target
73
+ @frames.on 'skrollrBetween', (e) -> e.target
74
+ @frames.on 'skrollrAfter', (e) -> e.target
75
+
76
+ # Apply skrollr-style attrs based on Alongslide attrs.
77
+ #
78
+ #
79
+ applyTransitions: (lastFramePosition) ->
80
+ @indexedTransitions = {}
81
+
82
+ @frames.find('.frame').each (index, frameEl) =>
83
+ frame = $(frameEl)
84
+
85
+ keyframes =
86
+ in: parseInt(frame.data alongslide.layout.IN_POINT_KEY)
87
+ out: parseInt(frame.data alongslide.layout.OUT_POINT_KEY)
88
+
89
+ if (keyframes.in is lastFramePosition) or (keyframes.out is -1)
90
+ keyframes.out = null
91
+ else
92
+ keyframes.out ||= keyframes.in
93
+
94
+ @indexedTransitions[keyframes.in] ?= {in: [], out: []}
95
+ @indexedTransitions[keyframes.in].in.push frame
96
+ if keyframes.out?
97
+ @indexedTransitions[keyframes.out] ?= {in: [], out: []}
98
+ @indexedTransitions[keyframes.out].out.push frame
99
+
100
+ @applyTransition frame, _(keyframes).extend lastFramePosition: lastFramePosition
101
+
102
+ if @skrollr.isMobile()
103
+ # For mobile, stage positions 0 and 1
104
+ _.each [0, 1], (position) =>
105
+ _.each @indexedTransitions[position].in, (frame) ->
106
+ frame.removeClass('unstaged')
107
+ else
108
+ # For desktop, stage all
109
+ @frames.find('.frame').each (index, frameEl) =>
110
+ $(frameEl).removeClass('unstaged')
111
+
112
+
113
+ # Write skrollr-style scroll transitions into jQuery DOM element.
114
+ #
115
+ # Note that, since a frame may enter and exit with different transitions,
116
+ # the CSS snippets for each transition should zero out effects of other
117
+ # transitions. (That's why the "slide" transition sets opacity.)
118
+ #
119
+ # Also, frames may scroll over a shorter distance ('scale') if they
120
+ # are with horizontally pinned panels. The code below must be context-aware
121
+ # enough to know when to do a normal-length scroll and when to shorten it.
122
+ #
123
+ # @param frame: jQuery object wrapping new DOM frame
124
+ # @param options: hash containing:
125
+ # - in: percentage of total page width when frame should enter
126
+ # - out (optional): percentage of total page width when frame should exit
127
+ # - lastFramePosition: last position of any frame in DOM.
128
+ #
129
+ # @return frame
130
+ #
131
+ applyTransition: (frame, options = {}) ->
132
+ # fuzzy logic coefficient. see below.
133
+ A_LITTLE_MORE = 2
134
+
135
+ options = @transitionOptions frame, options
136
+
137
+ # Flow frames have a left offset; panels and backgrounds do not.
138
+ offset = if frame.parent().hasClass('flow') then @frameAspect.left else 0
139
+
140
+ # if frame is partial width, scale the scroll down
141
+ frameScale = alongslide.layout.framePartialWidth(frame)
142
+
143
+ # transition is either 'in' or 'out'
144
+ for transition, directions of @TRANSITIONS
145
+ if options[transition]?
146
+
147
+ # direction is either -1, 0, or 1.
148
+ for direction in directions
149
+
150
+ # Set framescale for horizontal panels if there's another horizontal
151
+ # panel at the opposite edge before AND after the transition.
152
+ #
153
+ if Math.abs(direction) > 0
154
+ if frame.parent().hasClass('panels')
155
+ panelAlignment = alongslide.layout.panelAlignment(frame)
156
+ if _(alongslide.layout.HORIZONTAL_EDGES).contains panelAlignment
157
+ oppositeEdge =
158
+ if frame.hasClass('left') then 'right'
159
+ else if frame.hasClass('right') then 'left'
160
+ if (alongslide.layout.horizontalPanelAt(options[transition], oppositeEdge) and
161
+ alongslide.layout.horizontalPanelAt(options[transition] + direction, oppositeEdge))
162
+ frameScale = 0.495
163
+
164
+ # In certain cases, we need more than one keypoint per direction.
165
+ #
166
+ keypoints =
167
+ if frameScale?
168
+ # if frameScale is set, use it--but add an additional keypoint
169
+ # at 1.0 to make sure these closely-packed frames are out of
170
+ # the visible frame when they need to be be.
171
+ _.map [frameScale, 1.0], (scale, index) ->
172
+ magnitude = direction * (parseInt(index)+1)
173
+ position = options[transition] + magnitude
174
+
175
+ # if there isn't a horizontally pinned panel here, then scroll normally.
176
+ scale = 1.0 unless alongslide.layout.horizontalPanelAt(position)
177
+
178
+ # Double keypoints in order to keep the frame out of the visible
179
+ # window until absolutely necessary so that it doesn't sit atop
180
+ # the visible frame (and consume link clicks).
181
+ #
182
+ [ {magnitude: magnitude, scale: scale * A_LITTLE_MORE},
183
+ {magnitude: magnitude * 0.99, scale: scale}]
184
+
185
+ else if options.transition[transition] is "fade" and direction isnt 0
186
+ # fade transitions need a secret keypoint so that they fade
187
+ # in place but also don't hang out with opacity: 0 on top of
188
+ # other content when they're not supposed to be there.
189
+ [ {magnitude: direction * 0.99, scale: 0.0},
190
+ {magnitude: direction, scale: 1.0}]
191
+
192
+ else
193
+ # default: one keypoint
194
+ [{magnitude: direction, scale: 1.0}]
195
+
196
+ # apply Skrollr transitions for each keypoint.
197
+ #
198
+ for keypoint in _.flatten(keypoints)
199
+ {magnitude, scale} = keypoint
200
+ position = options[transition] + magnitude
201
+
202
+ # Don't write extra transitions beyond the end of the piece
203
+ unless position > options.lastFramePosition
204
+ styles = {}
205
+
206
+ # x translate
207
+ translateBy = (offset - direction) * scale
208
+ translateByPx = Math.round(translateBy * Math.max($(window).width(), 980))
209
+ styles["#{prefix.css}transform"] =
210
+ "translate(#{translateByPx}px, 0) translateZ(0)"
211
+
212
+ # opacity
213
+ styles.opacity = if options.transition[transition] is "fade"
214
+ 1.0 - Math.abs(direction)
215
+ else
216
+ 1.0
217
+
218
+ # apply the data for Skrollr.
219
+ frame.attr "data-#{Math.round(position * 100)}p",
220
+ (_.map styles, (value, key) -> "#{key}: #{value}").join("; ")
221
+
222
+ # Check frame's CSS classes for transition cues in the format
223
+ # `*-in` or `*-out`.
224
+ #
225
+ # Currently defaults to "slide", and does no validation.
226
+ #
227
+ transitionOptions: (frame, options = {}) ->
228
+ frameClass = frame.get(0).className
229
+
230
+ options.transition =
231
+ _.object _.map @TRANSITIONS, (directions, transition) ->
232
+ effectMatch = frameClass.match(new RegExp("(\\S+)-#{transition}"))
233
+ effect = if effectMatch then effectMatch[1] else "slide"
234
+ [transition, effect]
235
+
236
+ return options
237
+
238
+ # If we're in the refractory period, extinguish all scroll events immediately.
239
+ #
240
+ # Desktop only.
241
+ #
242
+ throttleScrollEvents: ->
243
+ $(window).on 'wheel mousewheel DOMMouseScroll MozMousePixelScroll', (e) =>
244
+
245
+ deltaX = e.originalEvent.deltaX || e.originalEvent.wheelDeltaX || 0
246
+ deltaY = e.originalEvent.deltaY || e.originalEvent.wheelDeltaY || 0
247
+
248
+ averageMagnitude = @updateWheelHistory(deltaX)
249
+
250
+ if @ignoreScroll
251
+ if averageMagnitude > @lastAverageMagnitude * @MAGNITUDE_THRESHOLD
252
+ @ignoreScroll = false
253
+ else if Math.abs(deltaX) > Math.abs(deltaY)
254
+ e.preventDefault()
255
+
256
+ @lastAverageMagnitude = averageMagnitude
257
+
258
+ # To gauge scroll inertia on desktop, need to constantly populate our
259
+ # wheelHistory array with zeroes to mark time.
260
+ #
261
+ # Desktop only.
262
+ #
263
+ monitorScroll: ->
264
+ # zero handler
265
+ zeroHistory = => @lastAverageMagnitude = @updateWheelHistory(0)
266
+
267
+ # init wheel history
268
+ _(@NUM_WHEEL_HISTORY_EVENTS).times -> zeroHistory()
269
+
270
+ # repeat forever.
271
+ setInterval zeroHistory, 5
272
+
273
+ # Add the latest delta to the running history, enforce max length.
274
+ #
275
+ # Returns average after updating.
276
+ #
277
+ updateWheelHistory: (delta) ->
278
+ # add delta to history
279
+ @wheelHistory.unshift(delta)
280
+
281
+ # trim history
282
+ @wheelHistory.pop() while @wheelHistory.length > @NUM_WHEEL_HISTORY_EVENTS
283
+
284
+ # return average
285
+ sum = _.reduce(@wheelHistory, ((memo, num) -> memo + num), 0)
286
+ average = sum / @wheelHistory.length
287
+ return Math.abs(average)
288
+
289
+ # Monitor mousedown state on desktop to separate scrollbar from mousewheel
290
+ #
291
+ monitorMouse: ->
292
+ $(document).mousedown =>
293
+ @mouseDown = true
294
+ $(document).mouseup =>
295
+ @mouseDown = false
296
+ requestedPosition = $(window).scrollLeft() / $(window).width()
297
+ window.alongslide?.scrolling.scrollToPosition(requestedPosition)
298
+
299
+ # Scroll to requested frame.
300
+ #
301
+ # Don't scroll below zero, and don't do redundant scrolls.
302
+ #
303
+ # @param position - ALS position (= frame number). May be floating point!
304
+ #
305
+ # @option skrollr - caller may pass in skrollr if our variable hasn't been
306
+ # set yet
307
+ # @option force - if true, force a corrective scroll, even if we think we're
308
+ # at the position we think we're supposed to be at.
309
+ # @option scrollMethod - is this a "scroll", a "touch", or "keys"?
310
+ #
311
+ scrollToPosition: (requestedPosition, options = {}) =>
312
+ # use our stored copy of skrollr instance if available
313
+ skrollr = @skrollr || options.skrollr
314
+
315
+ clearTimeout @resetTimeout
316
+
317
+ # round floating point position up or down based on thresholds
318
+ deltaRequestedPosition = requestedPosition - @lastRequestedPosition
319
+ deltaPosition = requestedPosition - @currentPosition
320
+ position =
321
+ if deltaRequestedPosition > 0
322
+ if deltaPosition > @FLIP_THRESHOLD
323
+ Math.ceil requestedPosition
324
+ else if deltaRequestedPosition < 0
325
+ if deltaPosition < -@FLIP_THRESHOLD
326
+ Math.floor requestedPosition
327
+ position ?= @currentPosition
328
+
329
+ # contain within bounds
330
+ position = Math.max(0, position)
331
+ position = Math.min(position, alongslide?.layout.lastFramePosition()) if alongslide?
332
+
333
+ if position isnt @currentPosition
334
+ # scroll to new (integer) position
335
+ scrollTo = position
336
+
337
+ duration = @SLIDE_DURATION_MS
338
+ if alongslide.layout.horizontalPanelAt(position) and alongslide.layout.horizontalPanelAt(@currentPosition)
339
+ duration /= 2
340
+
341
+ else if requestedPosition isnt @currentPosition
342
+ # didn't quite land on a new frame. revert back to current position after timeout.
343
+ @resetTimeout = setTimeout((=>
344
+ @scrollToPosition(@currentPosition, force: true)
345
+ ), @WAIT_BEFORE_RESET_MS)
346
+
347
+ else if options.force
348
+ if (position * $(window).width()) isnt skrollr.getScrollPosition()
349
+ scrollTo = @currentPosition
350
+ duration = @FORCE_SLIDE_DURATION_MS
351
+
352
+ @doScroll(scrollTo, skrollr, duration, options) if scrollTo?
353
+
354
+ @lastRequestedPosition = requestedPosition
355
+
356
+ #
357
+ #
358
+ doScroll: (scrollTo, skrollr, duration, options) ->
359
+ scrollDelta = scrollTo - @currentPosition
360
+ @currentPosition = scrollTo
361
+
362
+ # Block all future scrolls until new scroll has begun.
363
+ # See throttleScrollEvents().
364
+ unless skrollr.isMobile() or options.scrollMethod is "keys"
365
+ @ignoreScroll = true
366
+
367
+ # Do scroll
368
+ skrollr.animateTo scrollTo * $(window).width(),
369
+ duration: duration
370
+ easing: 'easeOutQuad'
371
+ done: (skrollr) ->
372
+
373
+ # For mobile, stage/unstage frames after transition
374
+ if @skrollr.isMobile()
375
+ setTimeout((=>
376
+ if Math.abs(scrollDelta) > 0
377
+ stagePosition = scrollTo + scrollDelta
378
+ stageTransition = if scrollDelta > 0 then 'in' else 'out'
379
+ _.each @indexedTransitions[stagePosition]?[stageTransition], (frame) ->
380
+ frame.removeClass('unstaged').hide()
381
+ setTimeout((-> frame.show()), 0)
382
+
383
+ unstagePosition = @currentPosition - 2 * scrollDelta
384
+ unstageTransition = if scrollDelta > 0 then 'out' else 'in'
385
+ _.each @indexedTransitions[unstagePosition]?[unstageTransition], (frame) ->
386
+ frame.addClass('unstaged')
387
+ ), duration)
388
+
389
+ # Snap-to-content scrolling, implemented as a skrollr callback, called after
390
+ # each frame in the animation loop.
391
+ #
392
+ # Bias the scroll so that it moves in the same direction as the user's input
393
+ # (i.e., use floor()/ceil() rather than round(), so that scroll never
394
+ # snaps BACK, which can feel disheartening as a user experience).
395
+ #
396
+ snap: (info) ->
397
+
398
+ # don't do anything if skrollr is animating
399
+ return if @isAnimatingTo()
400
+
401
+ # don't scroll past the document end
402
+ return if info.curTop > info.maxTop
403
+
404
+ # don't animate if user is clicking scrollbar
405
+ return if window.alongslide?.scrolling.mouseDown
406
+
407
+ # see how far the user has scrolled, scroll to the next frame.
408
+ requestedPosition = info.curTop / $(window).width()
409
+ window.alongslide?.scrolling.scrollToPosition(requestedPosition, skrollr: @)
410
+
411
+ # Listen to left/right arrow (unless modifier keys are pressed),
412
+ # and scroll accordingly.
413
+ #
414
+ arrowKeys: ->
415
+ $(document).keydown (event) =>
416
+ if event.altKey or event.shiftKey or event.ctrlKey or event.metaKey
417
+ return true
418
+ else
419
+ switch event.keyCode
420
+ when 37 then @scrollToPosition(@currentPosition - 1, scrollMethod: "keys")
421
+ when 39 then @scrollToPosition(@currentPosition + 1, scrollMethod: "keys")
422
+ else propagate_event = true
423
+ return propagate_event?
@@ -0,0 +1,19 @@
1
+ #
2
+ # alongslide.js: Require Alongslide libraries.
3
+ #
4
+ # Vendor
5
+ #= require regionFlow
6
+ #= require skrollr
7
+ #= require tether
8
+ #= require prefix
9
+ #= require jquery.fitvids
10
+ #
11
+ # Core
12
+ #= require alongslide/alongslide
13
+ #= require alongslide/parser
14
+ #= require alongslide/layout
15
+ #= require alongslide/scrolling
16
+ #
17
+ # Copyright 2013 Canopy Canopy Canopy, Inc.
18
+ # Authors Adam Florin & Anthony Tran
19
+ #