forked from bfl-itp/react
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjquery.scrollmagic.js
More file actions
executable file
·2428 lines (2302 loc) · 91.3 KB
/
jquery.scrollmagic.js
File metadata and controls
executable file
·2428 lines (2302 loc) · 91.3 KB
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
ScrollMagic v1.1.2
The jQuery plugin for doing magical scroll interactions.
(c) 2014 Jan Paepke (@janpaepke)
License & Info: http://janpaepke.github.io/ScrollMagic
Inspired by and partially based on SUPERSCROLLORAMA by John Polacek (@johnpolacek)
http://johnpolacek.github.com/superscrollorama/
Powered by the Greensock Tweening Platform (GSAP): http://www.greensock.com/js
Greensock License info at http://www.greensock.com/licensing/
*/
/**
@overview ##Info
@version 1.1.2
@license Dual licensed under MIT license and GPL.
@author Jan Paepke - e-mail@janpaepke.de
@todo: enhancement: remove dependencies and move to plugins -> 2.0
@todo: bug: when cascading pins (pinning one element multiple times) and later removing them without reset, positioning errors occur.
@todo: bug: having multiple scroll directions with cascaded pins doesn't work (one scroll vertical, one horizontal)
@todo: feature: optimize performance on debug plugin (huge drawbacks, when using many scenes)
*/
(function($, window) {
"use strict";
/**
* The main class that is needed once per scroll container.
*
* @class
* @global
*
* @example
* // basic initialization
* var controller = new ScrollMagic();
*
* // passing options
* var controller = new ScrollMagic({container: "#myContainer", loglevel: 3});
*
* @param {object} [options] - An object containing one or more options for the controller.
* @param {(string|object)} [options.container=window] - A selector, DOM object or a jQuery object that references the main container for scrolling.
* @param {boolean} [options.vertical=true] - Sets the scroll mode to vertical (`true`) or horizontal (`false`) scrolling.
* @param {object} [options.globalSceneOptions={}] - These options will be passed to every Scene that is added to the controller using the addScene method. For more information on Scene options see {@link ScrollScene}.
* @param {number} [options.loglevel=2] Loglevel for debugging. Note that logging is disabled in the minified version of ScrollMagic.
** `0` => silent
** `1` => errors
** `2` => errors, warnings
** `3` => errors, warnings, debuginfo
* @param {boolean} [options._refreshInterval=100] - Some changes don't call events by default, like changing the container size or moving a scene trigger element.
This interval polls these parameters to fire the necessary events.
If you don't use custom containers, trigger elements or have static layouts, where the positions of the trigger elements don't change, you can set this to 0 disable interval checking and improve performance.
*
*/
var ScrollMagic = function(options) {
/*
* ----------------------------------------------------------------
* settings
* ----------------------------------------------------------------
*/
var
NAMESPACE = "ScrollMagic",
DEFAULT_OPTIONS = {
container: window,
vertical: true,
globalSceneOptions: {},
loglevel: 2,
refreshInterval: 100
};
/*
* ----------------------------------------------------------------
* private vars
* ----------------------------------------------------------------
*/
var
ScrollMagic = this,
_options = $.extend({}, DEFAULT_OPTIONS, options),
_sceneObjects = [],
_updateScenesOnNextTick = false, // can be boolean (true => all scenes) or an array of scenes to be updated
_scrollPos = 0,
_scrollDirection = "PAUSED",
_isDocument = true,
_viewPortSize = 0,
_tickerUsed = false,
_enabled = true,
_refreshInterval;
/*
* ----------------------------------------------------------------
* private functions
* ----------------------------------------------------------------
*/
/**
* Internal constructor function of ScrollMagic
* @private
*/
var construct = function () {
$.each(_options, function (key, value) {
if (!DEFAULT_OPTIONS.hasOwnProperty(key)) {
log(2, "WARNING: Unknown option \"" + key + "\"");
delete _options[key];
}
});
_options.container = $(_options.container).first();
// check ScrollContainer
if (_options.container.length === 0) {
log(1, "ERROR creating object " + NAMESPACE + ": No valid scroll container supplied");
throw NAMESPACE + " init failed."; // cancel
}
_isDocument = !$.contains(document, _options.container.get(0));
// update container size immediately
_viewPortSize = _options.vertical ? _options.container.height() : _options.container.width();
// set event handlers
_options.container.on("scroll resize", onChange);
try {
TweenLite.ticker.addEventListener("tick", onTick); // prefer TweenMax Ticker, but don't rely on it for basic functionality
_tickerUsed = true;
} catch (e) {
_options.container.on("scroll resize", onTick); // okay then just update on scroll/resize...
_tickerUsed = false;
}
_options.refreshInterval = parseInt(_options.refreshInterval);
if (_options.refreshInterval > 0) {
_refreshInterval = window.setInterval(refresh, _options.refreshInterval);
}
log(3, "added new " + NAMESPACE + " controller (v" + ScrollMagic.version + ")");
};
/**
* Default function to get scroll pos - overwriteable using `ScrollMagic.scrollPos(newFunction)`
* @private
*/
var getScrollPos = function () {
return _options.vertical ? _options.container.scrollTop() : _options.container.scrollLeft();
};
/**
* Default function to set scroll pos - overwriteable using `ScrollMagic.scrollTo(newFunction)`
* @private
*/
var setScrollPos = function (pos) {
if (_options.vertical) {
_options.container.scrollTop(pos);
} else {
_options.container.scrollLeft(pos);
}
};
/**
* Handle updates on tick instead of on scroll (performance)
* @private
*/
var onTick = function (e) {
if (_updateScenesOnNextTick && _enabled) {
var
scenesToUpdate = $.isArray(_updateScenesOnNextTick) ? _updateScenesOnNextTick : _sceneObjects.slice(0),
oldScrollPos = _scrollPos;
// update scroll pos & direction
_scrollPos = ScrollMagic.scrollPos();
var deltaScroll = _scrollPos - oldScrollPos;
_scrollDirection = (deltaScroll === 0) ? "PAUSED" : (deltaScroll > 0) ? "FORWARD" : "REVERSE";
if (deltaScroll < 0) { // reverse order if scrolling reverse
scenesToUpdate.reverse();
}
// update scenes
$.each(scenesToUpdate, function (index, scene) {
log(3, "updating Scene " + (index + 1) + "/" + scenesToUpdate.length + " (" + _sceneObjects.length + " total)");
scene.update(true);
});
if (scenesToUpdate.length === 0 && _options.loglevel >= 3) {
log(3, "updating 0 Scenes (nothing added to controller)");
}
_updateScenesOnNextTick = false;
}
};
/**
* Handles Container changes
* @private
*/
var onChange = function (e) {
if (e.type == "resize") {
_viewPortSize = _options.vertical ? _options.container.height() : _options.container.width();
}
_updateScenesOnNextTick = true;
};
var refresh = function () {
if (!_isDocument) {
if (_viewPortSize != (_options.vertical ? _options.container.height() : _options.container.width())) {
_options.container.trigger("resize");
}
}
$.each(_sceneObjects, function (index, scene) {// refresh all scenes
scene.refresh();
});
};
/**
* Send a debug message to the console.
* @private
*
* @param {number} loglevel - The loglevel required to initiate output for the message.
* @param {...mixed} output - One or more variables that should be passed to the console.
*/
var log = function (loglevel, output) {
if (_options.loglevel >= loglevel) {
var
prefix = "(" + NAMESPACE + ") ->",
args = Array.prototype.splice.call(arguments, 1);
args.unshift(loglevel, prefix);
debug.apply(window, args);
}
};
/**
* Sort scenes in ascending order of their start offset.
* @private
*
* @param {array} ScrollScenesArray - an array of ScrollScenes that should be sorted
* @return {array} The sorted array of ScrollScenes.
*/
var sortScenes = function (ScrollScenesArray) {
if (ScrollScenesArray.length <= 1) {
return ScrollScenesArray;
} else {
var scenes = ScrollScenesArray.slice(0);
scenes.sort(function(a, b) {
return a.scrollOffset() > b.scrollOffset() ? 1 : -1;
});
return scenes;
}
};
/*
* ----------------------------------------------------------------
* public functions
* ----------------------------------------------------------------
*/
/**
* Add one ore more scene(s) to the controller.
* This is the equivalent to `ScrollScene.addTo(controller)`.
* @public
* @example
* // with a previously defined scene
* controller.addScene(scene);
*
* // with a newly created scene.
* controller.addScene(new ScrollScene({duration : 0}));
*
* // adding multiple scenes
* controller.addScene([scene, scene2, new ScrollScene({duration : 0})]);
*
* @param {(ScrollScene|array)} ScrollScene - ScrollScene or Array of ScrollScenes to be added to the controller.
* @return {ScrollMagic} Parent object for chaining.
*/
this.addScene = function (newScene) {
if ($.isArray(newScene)) {
$.each(newScene, function (index, scene) {
ScrollMagic.addScene(scene);
});
} else if (newScene instanceof ScrollScene) {
if (newScene.parent() != ScrollMagic) {
newScene.addTo(ScrollMagic);
} else if ($.inArray(newScene, _sceneObjects) < 0){
// new scene
_sceneObjects.push(newScene); // add to array
_sceneObjects = sortScenes(_sceneObjects); // sort
newScene.on("shift." + NAMESPACE + "_sort", function() { // resort whenever scene moves
_sceneObjects = sortScenes(_sceneObjects);
});
// insert Global defaults.
$.each(_options.globalSceneOptions, function (key, value) {
if (newScene[key]) {
newScene[key].call(newScene, value);
}
});
log(3, "added Scene (" + _sceneObjects.length + " total)");
}
} else {
log(1, "ERROR: invalid argument supplied for '.addScene()'");
}
return ScrollMagic;
};
/**
* Remove one ore more scene(s) from the controller.
* This is the equivalent to `ScrollScene.remove()`.
* @public
* @example
* // remove a scene from the controller
* controller.removeScene(scene);
*
* // remove multiple scenes from the controller
* controller.removeScene([scene, scene2, scene3]);
*
* @param {(ScrollScene|array)} ScrollScene - ScrollScene or Array of ScrollScenes to be removed from the controller.
* @returns {ScrollMagic} Parent object for chaining.
*/
this.removeScene = function (ScrollScene) {
if ($.isArray(ScrollScene)) {
$.each(ScrollScene, function (index, scene) {
ScrollMagic.removeScene(scene);
});
} else {
var index = $.inArray(ScrollScene, _sceneObjects);
if (index > -1) {
ScrollScene.off("shift." + NAMESPACE + "_sort");
_sceneObjects.splice(index, 1);
ScrollScene.remove();
log(3, "removed Scene (" + _sceneObjects.length + " total)");
}
}
return ScrollMagic;
};
/**
* Update one ore more scene(s) according to the scroll position of the container.
* This is the equivalent to `ScrollScene.update()`.
* The update method calculates the scene's start and end position (based on the trigger element, trigger hook, duration and offset) and checks it against the current scroll position of the container.
* It then updates the current scene state accordingly (or does nothing, if the state is already correct) – Pins will be set to their correct position and tweens will be updated to their correct progress.
* _**Note:** This method gets called constantly whenever ScrollMagic detects a change. The only application for you is if you change something outside of the realm of ScrollMagic, like moving the trigger or changing tween parameters._
* @public
* @example
* // update a specific scene on next tick
* controller.updateScene(scene);
*
* // update a specific scene immediately
* controller.updateScene(scene, true);
*
* // update multiple scenes scene on next tick
* controller.updateScene([scene1, scene2, scene3]);
*
* @param {ScrollScene} ScrollScene - ScrollScene or Array of ScrollScenes that is/are supposed to be updated.
* @param {boolean} [immediately=false] - If `true` the update will be instant, if `false` it will wait until next tweenmax tick.
This is useful when changing multiple properties of the scene - this way it will only be updated once all new properties are set (onTick).
* @return {ScrollMagic} Parent object for chaining.
*/
this.updateScene = function (ScrollScene, immediately) {
if ($.isArray(ScrollScene)) {
$.each(ScrollScene, function (index, scene) {
ScrollMagic.updateScene(scene, immediately);
});
} else {
if (immediately) {
ScrollScene.update(true);
} else {
// prep array for next update cycle
if (!$.isArray(_updateScenesOnNextTick)) {
_updateScenesOnNextTick = [];
}
if ($.inArray(ScrollScene, _updateScenesOnNextTick) == -1) {
_updateScenesOnNextTick.push(ScrollScene);
}
_updateScenesOnNextTick = sortScenes(_updateScenesOnNextTick); // sort
}
}
return ScrollMagic;
};
/**
* Updates the controller params and calls updateScene on every scene, that is attached to the controller.
* See `ScrollMagic.updateScene()` for more information about what this means.
* In most cases you will not need this function, as it is called constantly, whenever ScrollMagic detects a state change event, like resize or scroll.
* The only application for this method is when ScrollMagic fails to detect these events.
* One application is with some external scroll libraries (like iScroll) that move an internal container to a negative offset instead of actually scrolling. In this case the update on the controller needs to be called whenever the child container's position changes.
* For this case there will also be the need to provide a custom function to calculate the correct scroll position. See `ScrollMagic.scrollPos()` for details.
* @public
* @example
* // update the controller on next tick (saves performance)
* controller.update();
*
* // update the controller immediately
* controller.update(true);
*
* @param {boolean} [immediately=false] - If `true` the update will be instant, if `false` it will wait until next tweenmax tick (better performance)
* @return {ScrollMagic} Parent object for chaining.
*/
this.update = function (immediately) {
onChange({type: "resize"}); // will update size and set _updateScenesOnNextTick to true
if (immediately) {
onTick();
}
return ScrollMagic;
};
/**
* Scroll to a numeric scroll offset, a DOM element, the start of a scene or provide an alternate method for scrolling.
* For vertical controllers it will change the top scroll offset and for horizontal applications it will change the left offset.
* @public
*
* @since 1.1.0
* @example
* // scroll to an offset of 100
* controller.scrollTo(100);
*
* // scroll to a DOM element
* controller.scrollTo("#anchor");
*
* // scroll to the beginning of a scene
* var scene = new ScrollScene({offset: 200});
* controller.scrollTo(scene);
*
* // define a new scroll position modification function (animate instead of jump)
* controller.scrollTo(function (newScrollPos) {
* $("body").animate({scrollTop: newScrollPos});
* });
*
* @param {mixed} [scrollTarget] - The supplied argument can be one of these types:
* 1. `number` -> The container will scroll to this new scroll offset.
* 2. `string` or `object` -> Can be a selector, a DOM object or a jQuery element.
* The container will scroll to the position of this element.
* 3. `ScrollScene` -> The container will scroll to the start of this scene.
* 4. `function` -> This function will be used as a callback for future scroll position modifications.
* This provides a way for you to change the behaviour of scrolling and adding new behaviour like animation. The callback receives the new scroll position as a parameter and a reference to the container element using `this`.
* _**NOTE:** All other options will still work as expected, using the new function to scroll._
* @returns {ScrollMagic} Parent object for chaining.
*/
this.scrollTo = function (scrollTarget) {
if (scrollTarget instanceof ScrollScene) {
if (scrollTarget.parent() === ScrollMagic) { // check if this controller is the parent
ScrollMagic.scrollTo(scrollTarget.scrollOffset());
} else {
log (2, "scrollTo(): The supplied scene does not belong to this controller. Scroll cancelled.", scrollTarget);
}
} else if ($.type(scrollTarget) === "string" || isDomElement(scrollTarget) || scrollTarget instanceof $) {
var $elm = $(scrollTarget).first();
if ($elm[0]) {
var
param = _options.vertical ? "top" : "left", // which param is of interest ?
containerOffset = getOffset(_options.container), // container position is needed because element offset is returned in relation to document, not in relation to container.
elementOffset = getOffset($elm);
if (!_isDocument) { // container is not the document root, so substract scroll Position to get correct trigger element position relative to scrollcontent
containerOffset[param] -= ScrollMagic.scrollPos();
}
ScrollMagic.scrollTo(elementOffset[param] - containerOffset[param]);
} else {
log (2, "scrollTo(): The supplied element could not be found. Scroll cancelled.", scrollTarget);
}
} else if ($.isFunction(scrollTarget)) {
setScrollPos = scrollTarget;
} else {
setScrollPos.call(_options.container[0], scrollTarget);
}
return ScrollMagic;
};
/**
* **Get** the current scrollPosition or **Set** a new method to calculate it.
* -> **GET**:
* When used as a getter this function will return the current scroll position.
* To get a cached value use ScrollMagic.info("scrollPos"), which will be updated on tick to save on performance.
* For vertical controllers it will return the top scroll offset and for horizontal applications it will return the left offset.
*
* -> **SET**:
* When used as a setter this method prodes a way to permanently overwrite the controller's scroll position calculation.
* A typical usecase is when the scroll position is not reflected by the containers scrollTop or scrollLeft values, but for example by the inner offset of a child container.
* Moving a child container inside a parent is a commonly used method for several scrolling frameworks, including iScroll.
* By providing an alternate calculation function you can make sure ScrollMagic receives the correct scroll position.
* Please also bear in mind that your function should return y values for vertical scrolls an x for horizontals.
*
* To change the current scroll position please use `ScrollMagic.scrollTo()`.
* @public
*
* @example
* // get the current scroll Position
* var scrollPos = controller.scrollPos();
*
* // set a new scroll position calculation method
* controller.scrollPos(function () {
* return this.info("vertical") ? -$mychildcontainer.y : -$mychildcontainer.x
* });
*
* @param {function} [scrollPosMethod] - The function to be used for the scroll position calculation of the container.
* @returns {(number|ScrollMagic)} Current scroll position or parent object for chaining.
*/
this.scrollPos = function (scrollPosMethod) {
if (!arguments.length) { // get
return getScrollPos.call(ScrollMagic);
} else { // set
if ($.isFunction(scrollPosMethod)) {
getScrollPos = scrollPosMethod;
} else {
log(2, "Provided value for method 'scrollPos' is not a function. To change the current scroll position use 'scrollTo()'.");
}
}
return ScrollMagic;
};
/**
* **Get** all infos or one in particular about the controller.
* @public
* @example
* // returns the current scroll position (number)
* var scrollPos = controller.info("scrollPos");
*
* // returns all infos as an object
* var infos = controller.info();
*
* @param {string} [about] - If passed only this info will be returned instead of an object containing all.
Valid options are:
** `"size"` => the current viewport size of the container
** `"vertical"` => true if vertical scrolling, otherwise false
** `"scrollPos"` => the current scroll position
** `"scrollDirection"` => the last known direction of the scroll
** `"container"` => the container element
** `"isDocument"` => true if container element is the document.
* @returns {(mixed|object)} The requested info(s).
*/
this.info = function (about) {
var values = {
size: _viewPortSize, // contains height or width (in regard to orientation);
vertical: _options.vertical,
scrollPos: _scrollPos,
scrollDirection: _scrollDirection,
container: _options.container,
isDocument: _isDocument
};
if (!arguments.length) { // get all as an object
return values;
} else if (values[about] !== undefined) {
return values[about];
} else {
log(1, "ERROR: option \"" + about + "\" is not available");
return;
}
};
/**
* **Get** or **Set** the current loglevel option value.
* @public
*
* @example
* // get the current value
* var loglevel = controller.loglevel();
*
* // set a new value
* controller.loglevel(3);
*
* @param {number} [newLoglevel] - The new loglevel setting of the ScrollMagic controller. `[0-3]`
* @returns {(number|ScrollMagic)} Current loglevel or parent object for chaining.
*/
this.loglevel = function (newLoglevel) {
if (!arguments.length) { // get
return _options.loglevel;
} else if (_options.loglevel != newLoglevel) { // set
_options.loglevel = newLoglevel;
}
return ScrollMagic;
};
/**
* **Get** or **Set** the current enabled state of the controller.
* This can be used to disable all Scenes connected to the controller without destroying or removing them.
* @public
*
* @example
* // get the current value
* var enabled = controller.enabled();
*
* // disable the controller
* controller.enabled(false);
*
* @param {boolean} [newState] - The new enabled state of the controller `true` or `false`.
* @returns {(boolean|ScrollMagic)} Current enabled state or parent object for chaining.
*/
this.enabled = function (newState) {
if (!arguments.length) { // get
return _enabled;
} else if (_enabled != newState) { // set
_enabled = !!newState;
ScrollMagic.updateScene(_sceneObjects, true);
}
return ScrollMagic;
};
/**
* Destroy the Controller, all Scenes and everything.
* @public
*
* @example
* // without resetting the scenes
* controller = controller.destroy();
*
* // with scene reset
* controller = controller.destroy(true);
*
* @param {boolean} [resetScenes=false] - If `true` the pins and tweens (if existent) of all scenes will be reset.
* @returns {null} Null to unset handler variables.
*/
this.destroy = function (resetScenes) {
window.clearTimeout(_refreshInterval);
var i = _sceneObjects.length;
while (i--) {
_sceneObjects[i].destroy(resetScenes);
}
_options.container.off("scroll resize", onChange);
if (_tickerUsed) {
TweenLite.ticker.removeEventListener("tick", onTick);
} else {
_options.container.off("scroll resize", onTick);
}
log(3, "destroyed " + NAMESPACE + " (reset: " + (resetScenes ? "true" : "false") + ")");
return null;
};
// INIT
construct();
return ScrollMagic;
};
/**
* A ScrollScene defines where the controller should react and how.
*
* @class
* @global
*
* @example
* // create a standard scene and add it to a controller
* new ScrollScene()
* .addTo(controller);
*
* // create a scene with custom options and assign a handler to it.
* var scene = new ScrollScene({
* duration: 100,
* offset: 200,
* triggerHook: "onEnter",
* reverse: false
* });
*
* @param {object} [options] - Options for the Scene. The options can be updated at any time.
Instead of setting the options for each scene individually you can also set them globally in the controller as the controllers `globalSceneOptions` option. The object accepts the same properties as the ones below.
When a scene is added to the controller the options defined using the ScrollScene constructor will be overwritten by those set in `globalSceneOptions`.
* @param {(number|function)} [options.duration=0] - The duration of the scene.
If `0` tweens will auto-play when reaching the scene start point, pins will be pinned indefinetly starting at the start position.
A function retuning the duration value is also supported. Please see `ScrollScene.duration()` for details.
* @param {number} [options.offset=0] - Offset Value for the Trigger Position. If no triggerElement is defined this will be the scroll distance from the start of the page, after which the scene will start.
* @param {(string|object)} [options.triggerElement=null] - Selector, DOM object or jQuery Object that defines the start of the scene. If undefined the scene will start right at the start of the page (unless an offset is set).
* @param {(number|string)} [options.triggerHook="onCenter"] - Can be a number between 0 and 1 defining the position of the trigger Hook in relation to the viewport.
Can also be defined using a string:
** `"onEnter"` => `1`
** `"onCenter"` => `0.5`
** `"onLeave"` => `0`
* @param {boolean} [options.reverse=true] - Should the scene reverse, when scrolling up?
* @param {boolean} [options.tweenChanges=false] - Tweens Animation to the progress target instead of setting it.
Does not affect animations where duration is `0`.
* @param {number} [options.loglevel=2] - Loglevel for debugging. Note that logging is disabled in the minified version of ScrollMagic.
** `0` => silent
** `1` => errors
** `2` => errors, warnings
** `3` => errors, warnings, debuginfo
*
*/
var ScrollScene = function (options) {
/*
* ----------------------------------------------------------------
* settings
* ----------------------------------------------------------------
*/
var
TRIGGER_HOOK_VALUES = {"onCenter" : 0.5, "onEnter" : 1, "onLeave" : 0},
NAMESPACE = "ScrollScene",
DEFAULT_OPTIONS = {
duration: 0,
offset: 0,
triggerElement: null,
triggerHook: "onCenter",
reverse: true,
tweenChanges: false,
loglevel: 2
};
/*
* ----------------------------------------------------------------
* private vars
* ----------------------------------------------------------------
*/
var
ScrollScene = this,
_options = $.extend({}, DEFAULT_OPTIONS, options),
_state = 'BEFORE',
_progress = 0,
_scrollOffset = {start: 0, end: 0}, // reflects the parent's scroll position for the start and end of the scene respectively
_triggerPos = 0,
_enabled = true,
_durationUpdateMethod,
_parent,
_tween,
_pin,
_pinOptions,
_cssClasses,
_cssClassElm;
// object containing validator functions for various options
var _validate = {
"unknownOptionSupplied" : function () {
$.each(_options, function (key, value) {
if (!DEFAULT_OPTIONS.hasOwnProperty(key)) {
log(2, "WARNING: Unknown option \"" + key + "\"");
delete _options[key];
}
});
},
"duration" : function () {
if ($.isFunction(_options.duration)) {
_durationUpdateMethod = _options.duration;
try {
_options.duration = parseFloat(_durationUpdateMethod());
} catch (e) {
log(1, "ERROR: Invalid return value of supplied function for option \"duration\":", _options.duration);
_durationUpdateMethod = undefined;
_options.duration = DEFAULT_OPTIONS.duration;
}
} else {
_options.duration = parseFloat(_options.duration);
if (!$.isNumeric(_options.duration) || _options.duration < 0) {
log(1, "ERROR: Invalid value for option \"duration\":", _options.duration);
_options.duration = DEFAULT_OPTIONS.duration;
}
}
},
"offset" : function () {
_options.offset = parseFloat(_options.offset);
if (!$.isNumeric(_options.offset)) {
log(1, "ERROR: Invalid value for option \"offset\":", _options.offset);
_options.offset = DEFAULT_OPTIONS.offset;
}
},
"triggerElement" : function () {
if (_options.triggerElement !== null && $(_options.triggerElement).length === 0) {
log(1, "ERROR: Element defined in option \"triggerElement\" was not found:", _options.triggerElement);
_options.triggerElement = DEFAULT_OPTIONS.triggerElement;
}
},
"triggerHook" : function () {
if (!(_options.triggerHook in TRIGGER_HOOK_VALUES)) {
if ($.isNumeric(_options.triggerHook)) {
_options.triggerHook = Math.max(0, Math.min(parseFloat(_options.triggerHook), 1)); // make sure its betweeen 0 and 1
} else {
log(1, "ERROR: Invalid value for option \"triggerHook\": ", _options.triggerHook);
_options.triggerHook = DEFAULT_OPTIONS.triggerHook;
}
}
},
"reverse" : function () {
_options.reverse = !!_options.reverse; // force boolean
},
"tweenChanges" : function () {
_options.tweenChanges = !!_options.tweenChanges; // force boolean
},
"loglevel" : function () {
_options.loglevel = parseInt(_options.loglevel);
if (!$.isNumeric(_options.loglevel) || _options.loglevel < 0 || _options.loglevel > 3) {
var wrongval = _options.loglevel;
_options.loglevel = DEFAULT_OPTIONS.loglevel;
log(1, "ERROR: Invalid value for option \"loglevel\":", wrongval);
}
},
"checkIfTriggerElementIsTweened" : function () {
// check if there are position tweens defined for the trigger and warn about it :)
if (_tween && _parent && _options.triggerElement && _options.loglevel >= 2) {// parent is needed to know scroll direction.
var
triggerTweens = _tween.getTweensOf($(_options.triggerElement)),
vertical = _parent.info("vertical");
$.each(triggerTweens, function (index, value) {
var
tweenvars = value.vars.css || value.vars,
condition = vertical ? (tweenvars.top !== undefined || tweenvars.bottom !== undefined) : (tweenvars.left !== undefined || tweenvars.right !== undefined);
if (condition) {
log(2, "WARNING: Tweening the position of the trigger element affects the scene timing and should be avoided!");
return false;
}
});
}
},
};
/*
* ----------------------------------------------------------------
* private functions
* ----------------------------------------------------------------
*/
/**
* Internal constructor function of ScrollMagic
* @private
*/
var construct = function () {
validateOption();
// event listeners
ScrollScene
.on("change.internal", function (e) {
if (e.what !== "loglevel" && e.what !== "tweenChanges") { // no need for a scene update scene with these options...
if (e.what === "triggerElement") {
updateTriggerElementPosition();
} else if (e.what === "reverse") { // the only property left that may have an impact on the current scene state. Everything else is handled by the shift event.
ScrollScene.update();
}
}
})
.on("shift.internal", function (e) {
updateScrollOffset();
ScrollScene.update(); // update scene to reflect new position
if ((_state === "AFTER" && e.reason === "duration") || (_state === 'DURING' && _options.duration === 0)) {
// if [duration changed after a scene (inside scene progress updates pin position)] or [duration is 0, we are in pin phase and some other value changed].
updatePinState();
}
})
.on("progress.internal", function (e) {
updateTweenProgress();
updatePinState();
})
.on("destroy", function (e) {
e.preventDefault(); // otherwise jQuery would call target.destroy() by default.
});
};
/**
* Send a debug message to the console.
* @private
*
* @param {number} loglevel - The loglevel required to initiate output for the message.
* @param {...mixed} output - One or more variables that should be passed to the console.
*/
var log = function (loglevel, output) {
if (_options.loglevel >= loglevel) {
var
prefix = "(" + NAMESPACE + ") ->",
args = Array.prototype.splice.call(arguments, 1);
args.unshift(loglevel, prefix);
debug.apply(window, args);
}
};
/**
* Checks the validity of a specific or all options and reset to default if neccessary.
* @private
*/
var validateOption = function (check) {
if (!arguments.length) {
check = [];
for (var key in _validate){
check.push(key);
}
} else if (!$.isArray(check)) {
check = [check];
}
$.each(check, function (key, value) {
if (_validate[value]) {
_validate[value]();
}
});
};
/**
* Helper used by the setter/getters for scene options
* @private
*/
var changeOption = function(varname, newval) {
var
changed = false,
oldval = _options[varname];
if (_options[varname] != newval) {
_options[varname] = newval;
validateOption(varname); // resets to default if necessary
changed = oldval != _options[varname];
}
return changed;
};
/**
* Update the start and end scrollOffset of the container.
* The positions reflect what the parent's scroll position will be at the start and end respectively.
* Is called, when:
* - ScrollScene event "change" is called with: offset, triggerHook, duration
* - scroll container event "resize" is called
* - the position of the triggerElement changes
* - the parent changes -> addTo()
* @private
*/
var updateScrollOffset = function () {
_scrollOffset = {start: _triggerPos + _options.offset};
if (_parent && _options.triggerElement) {
// take away triggerHook portion to get relative to top
_scrollOffset.start -= _parent.info("size") * ScrollScene.triggerHook();
}
_scrollOffset.end = _scrollOffset.start + _options.duration;
};
/**
* Updates the duration if set to a dynamic function.
* This method is called when the scene is added to a controller and in regular intervals from the controller through scene.refresh().
*
* @fires {@link ScrollScene.change}, if the duration changed
* @fires {@link ScrollScene.shift}, if the duration changed
*
* @param {boolean} [suppressEvents=false] - If true the shift event will be suppressed.
* @private
*/
var updateDuration = function (suppressEvents) {
// update duration
if (_durationUpdateMethod) {
var varname = "duration";
if (changeOption(varname, _durationUpdateMethod.call(ScrollScene)) && !suppressEvents) { // set
ScrollScene.trigger("change", {what: varname, newval: _options[varname]});
ScrollScene.trigger("shift", {reason: varname});
}
}
};
/**
* Updates the position of the triggerElement, if present.
* This method is called ...
* - ... when the triggerElement is changed
* - ... when the scene is added to a (new) controller
* - ... in regular intervals from the controller through scene.refresh().
*
* @fires {@link ScrollScene.shift}, if the position changed
*
* @param {boolean} [suppressEvents=false] - If true the shift event will be suppressed.
* @private
*/
var updateTriggerElementPosition = function (suppressEvents) {
var elementPos = 0;
if (_parent && _options.triggerElement) {
var
element = $(_options.triggerElement).first(),
controllerInfo = _parent.info(),
containerOffset = getOffset(controllerInfo.container), // container position is needed because element offset is returned in relation to document, not in relation to container.
param = controllerInfo.vertical ? "top" : "left"; // which param is of interest ?
// if parent is spacer, use spacer position instead so correct start position is returned for pinned elements.
while (element.parent().data("ScrollMagicPinSpacer")) {
element = element.parent();
}
var elementOffset = getOffset(element);
if (!controllerInfo.isDocument) { // container is not the document root, so substract scroll Position to get correct trigger element position relative to scrollcontent
containerOffset[param] -= _parent.scrollPos();
}
elementPos = elementOffset[param] - containerOffset[param];
}
var changed = elementPos != _triggerPos;
_triggerPos = elementPos;
if (changed && !suppressEvents) {
ScrollScene.trigger("shift", {reason: "triggerElementPosition"});
}
};
/**
* Update the tween progress.
* @private
*
* @param {number} [to] - If not set the scene Progress will be used. (most cases)
* @return {boolean} true if the Tween was updated.
*/
var updateTweenProgress = function (to) {
if (_tween) {
var progress = (to >= 0 && to <= 1) ? to : _progress;
if (_tween.repeat() === -1) {
// infinite loop, so not in relation to progress
if (_state === "DURING" && _tween.paused()) {
_tween.play();
} else if (_state !== "DURING" && !_tween.paused()) {
_tween.pause();
} else {
return false;
}
} else if (progress != _tween.progress()) { // do we even need to update the progress?
// no infinite loop - so should we just play or go to a specific point in time?
if (_options.duration === 0) {
// play the animation
if (_state === "DURING") { // play from 0 to 1
_tween.play();
} else { // play from 1 to 0
_tween.reverse();
}
} else {
// go to a specific point in time
if (_options.tweenChanges) {
// go smooth
_tween.tweenTo(progress * _tween.duration());
} else {
// just hard set it
_tween.progress(progress).pause();
}