alongslide 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ #