alongslide 0.9.1 → 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +10 -0
- data/Gemfile.lock +103 -0
- data/alongslide.gemspec +18 -0
- data/app/assets/javascripts/alongslide/alongslide.coffee +107 -0
- data/app/assets/javascripts/alongslide/layout.coffee +504 -0
- data/app/assets/javascripts/alongslide/parser.coffee +125 -0
- data/app/assets/javascripts/alongslide/scrolling.coffee +423 -0
- data/app/assets/javascripts/alongslide.coffee +19 -0
- data/app/assets/stylesheets/alongslide.sass +444 -0
- data/app/views/panel/panel.haml +8 -0
- data/app/views/panel/unpin.haml +4 -0
- data/app/views/section/exit.haml +4 -0
- data/app/views/section/section.haml +4 -0
- data/grammar/alongslide.treetop +162 -0
- data/grammar/panel.treetop +67 -0
- data/vendor/assets/javascripts/jquery.fitvids.js +74 -0
- data/vendor/assets/javascripts/prefix.js +18 -0
- data/vendor/assets/javascripts/regionFlow.coffee +305 -0
- data/vendor/assets/javascripts/skrollr.js +1716 -0
- data/vendor/assets/javascripts/tether.js +1357 -0
- metadata +23 -3
@@ -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
|
+
#
|