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.
- 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
|
+
#
|