├── .babelrc.js
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .npmignore
├── .prettierrc
├── .release-it.json
├── .storybook
├── addons.js
├── config.js
├── preview-head.html
└── webpack.config.js
├── LICENSE
├── README.md
├── package.json
├── src
├── ScrollObserver.ts
├── ScrollScene.ts
├── helpers
│ ├── createArray.ts
│ ├── errorLog.ts
│ ├── index.ts
│ ├── isFunc.ts
│ ├── isObject.ts
│ ├── isString.ts
│ ├── roundOff.ts
│ ├── scrollAnimationInit.ts
│ └── stringContains.ts
├── index.ts
├── plugins
│ └── debug.addIndicators.ts
├── scrollmagic-add-indicators.ts
├── scrollmagic-with-ssr.ts
└── stories
│ ├── Stylesheet.js
│ └── story.js
├── tsconfig.json
├── tsconfig.types.cjs.json
├── tsconfig.types.esm.json
├── tslint.json
└── yarn.lock
/.babelrc.js:
--------------------------------------------------------------------------------
1 | const tsconfig = require('./tsconfig.json')
2 | const tsPaths = tsconfig.compilerOptions.paths
3 |
4 | /*
5 | Convert tsconfig path
6 | {
7 | '@components/*': [ './src/components/*' ],
8 | }
9 |
10 | To babelrc aliases
11 |
12 | {
13 | @components': './src/components',
14 | }
15 | */
16 |
17 | const webpackTsPaths = {}
18 |
19 | Object.keys(tsPaths).forEach(function(key) {
20 | const newKey = key.replace('/*', '')
21 |
22 | // this only support single src aliases, not arrays of aliases
23 | // might need to upgrade later if it's really that necessary
24 | const value = tsPaths[key][0].replace('/*', '')
25 |
26 | webpackTsPaths[newKey] = value
27 | })
28 |
29 | module.exports = {
30 | presets: ['@babel/preset-env', '@babel/preset-react', '@babel/typescript'],
31 | plugins: [
32 | '@babel/plugin-proposal-class-properties',
33 | '@babel/proposal-object-rest-spread',
34 | '@babel/plugin-proposal-export-default-from',
35 | '@babel/plugin-transform-runtime',
36 | 'babel-plugin-add-react-displayname',
37 | [
38 | 'module-resolver',
39 | {
40 | root: ['.'],
41 | alias: webpackTsPaths,
42 | },
43 | ],
44 | ],
45 | comments: false,
46 | env: {
47 | commonjs: {
48 | presets: [
49 | [
50 | '@babel/preset-env',
51 | {
52 | useBuiltIns: false,
53 | },
54 | ],
55 | ],
56 | },
57 | esm: {
58 | presets: [['@babel/preset-env', { modules: false }]],
59 | plugins: ['dynamic-import-node', ['@babel/plugin-transform-runtime', { useESModules: true }]],
60 | },
61 | },
62 | }
63 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build
2 | dist
3 | node_modules
4 | .cache
5 | ./src/lib
6 | /src/**/stories/*
7 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | commonjs: true,
5 | es6: true
6 | },
7 | parser: 'babel-eslint',
8 | parserOptions: {
9 | ecmaVersion: 6,
10 | sourceType: 'module',
11 | ecmaFeatures: {
12 | jsx: true,
13 | modules: true,
14 | experimentalObjectRestSpread: true
15 | }
16 | },
17 | extends: ['eslint:recommended', 'plugin:react/recommended'],
18 | plugins: ['react'],
19 | rules: {
20 | indent: ['error', 2],
21 | 'linebreak-style': ['error', 'unix'],
22 | quotes: ['error', 'single'],
23 | semi: ['error', 'never']
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | _wip
2 | .cache
3 | .npmrc
4 | .out
5 | build
6 | dist
7 | node_modules
8 | package
9 | package-lock.json
10 | todo
11 | yarn-error.log
12 | public
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jonkwheeler/ScrollScene/9861e17c66c0f6a29ecd7777b6bdff2f1cadb6d0/.npmignore
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": false,
3 | "tabWidth": 2,
4 | "singleQuote": true,
5 | "semi": false,
6 | "jsxBracketSameLine": true,
7 | "trailingComma": "all",
8 | "bracketSpacing": true,
9 | "arrowParens": "avoid",
10 | "printWidth": 120
11 | }
12 |
--------------------------------------------------------------------------------
/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "non-interactive": false,
3 | "requireCleanWorkingDir": true,
4 | "buildCommand": "yarn nuke:build:rebuild"
5 | }
6 |
--------------------------------------------------------------------------------
/.storybook/addons.js:
--------------------------------------------------------------------------------
1 | import '@storybook/addon-knobs/register'
2 | import '@storybook/addon-notes/register'
3 | import '@storybook/addon-storysource/register'
4 |
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure, addDecorator } from '@storybook/react'
2 | import { withKnobs } from '@storybook/addon-knobs'
3 |
4 | addDecorator(
5 | withKnobs({
6 | escapeHTML: false,
7 | }),
8 | )
9 |
10 | function loadStories() {
11 | const req = require.context('../src', true, /story.(js|jsx|ts|tsx)$|stories\/*.(js|jsx|ts|tsx)/)
12 | req.keys().forEach(filename => req(filename))
13 | }
14 |
15 | configure(loadStories, module)
16 |
--------------------------------------------------------------------------------
/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 |
2 |
40 |
--------------------------------------------------------------------------------
/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
3 | const tsconfig = require('../tsconfig.json')
4 | const tsPaths = tsconfig.compilerOptions.paths
5 |
6 | /*
7 | Convert tsconfig path
8 | {
9 | '@storybook-helpers/*': [ '../.storybook/helpers/*' ]
10 | }
11 |
12 | To webpack aliases
13 |
14 | {
15 | '@components': path.resolve(__dirname, '../src/components')
16 | }
17 | */
18 |
19 | const webpackTsPaths = {}
20 |
21 | Object.keys(tsPaths).forEach(function(key) {
22 | const newKey = key.replace('/*', '')
23 | const regex = /^.{0,2}/
24 |
25 | // this only support single src aliases, not arrays of aliases
26 | // might need to upgrade later if it's really that necessary
27 | const value = tsPaths[key][0]
28 | const returnedValue = (regex.test('./') ? value.replace(regex, '../') : value).replace('/*', '')
29 |
30 | webpackTsPaths[newKey] = path.resolve(__dirname, returnedValue)
31 | })
32 |
33 | const extensions = ['.ts', '.tsx', '.js', '.jsx', '.json']
34 |
35 | module.exports = ({ config, mode }) => {
36 | // config
37 | config.module.rules.push({
38 | test: /\.(ts|tsx)$/,
39 | use: {
40 | loader: require.resolve('ts-loader'),
41 | options: {
42 | // disable checker as it's handled by the checker plugin in parallel
43 | transpileOnly: true,
44 | },
45 | },
46 | })
47 |
48 | // runs tslint and typechecks
49 | config.plugins.push(new ForkTsCheckerWebpackPlugin())
50 |
51 | // Uncomment this later. Causing build issues now.
52 | // config.plugins.push(new TSDocgenPlugin())
53 |
54 | // make storybook recognize ts and tsx files
55 | config.resolve.extensions.push(...extensions)
56 |
57 | // do not use TsconfigPathsPlugin, it will not resolve correctly when webpack has a different root from tsconfig, and baseUrl is bs
58 | // this shiz works 🎉
59 | config.resolve.alias = webpackTsPaths
60 |
61 | config.resolve.modules = ['node_modules', path.resolve(__dirname, '../src')]
62 |
63 | return config
64 | }
65 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Jon Wheeler
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://img.shields.io/github/v/tag/jonkwheeler/ScrollScene?color=forest&label=latest%20release)
2 |
3 | # ScrollScene
4 |
5 | ScrollScene is an extra layer on top of ScrollMagic as well as using IntersectionObserver to achieve similar effects.
6 |
7 | ## Examples
8 |
9 | View the online [Storybook](https://scrollscene.jonkwheeler.now.sh).
10 |
11 | ## Install
12 |
13 | ```js
14 | yarn add scrollscene scrollmagic
15 | ```
16 |
17 | or
18 |
19 | ```js
20 | npm install scrollscene scrollmagic
21 | ```
22 |
23 | If plan to use only the ScrollObserver, currently you can
24 |
25 | ```js
26 | yarn add scrollscene && yarn add -D scrollmagic
27 | ```
28 |
29 | to avoid import errors.
30 |
31 | ## Import
32 |
33 | ### ScrollScene (uses ScrollMagic)
34 |
35 | ```js
36 | import { ScrollScene } from 'scrollscene'
37 | ```
38 |
39 | ### ScrollObserver (uses IntersectionObserver)
40 |
41 | ```js
42 | import { ScrollObserver } from 'scrollscene'
43 | ```
44 |
45 | ### ScrollMagic (with SSR catch)
46 |
47 | ```js
48 | import { ScrollMagic } from 'scrollscene'
49 | ```
50 |
51 | or
52 |
53 | ```js
54 | import { ScrollMagicSsr } from 'scrollscene'
55 | ```
56 |
57 | `ScrollMagic` and `ScrollMagicSsr` are the exact same thing.
58 |
59 | ## Options
60 |
61 | ### ScrollScene Options (uses ScrollMagic)
62 |
63 | | option | Description / Example |
64 | | ---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
65 | | `breakpoints` | `breakpoints: { 0: false, 768: true }` is used to set responsiveness of the new ScrollMagic.Scene, mobile-first. |
66 | | `controller` | `controller: { vertical: false }`. Add anything from [new ScrollMagic.Controller(options)](http://scrollmagic.io/docs/ScrollMagic.Controller.html#constructor). |
67 | | `duration` | `duration: '100%'` OR `duration: { 0: '50%', 768: '100% }` is used to set responsiveness of the new ScrollMagic.Scene, mobile-first. OR set as a dom node (HTMLElement) `duration: triggerElement` and the scene will last as long as the height of the element. |
68 | | `gsap` | Init a Gsap timeline with `gsap: { timeline: myTimeline, reverseSpeed: 2, yoyo: true, delay: 2 }`. |
69 | | `offset` | Used to change the ScrollMagic `offset`. |
70 | | `scene` | `scene: { loglevel: 1 }`. Add anything from [new ScrollMagic.Scene(options)](http://scrollmagic.io/docs/ScrollMagic.Scene.html#constructor). |
71 | | `toggle` | Toggle a className on an element with `toggle: { element: containerRef.current, className: 'lets-do-this' }`. The `element` key does _not_ accept string; eg: `.className`. Use a dom node selector instead. |
72 | | `triggerElement` | `triggerElement: document.querySelector('#element')` is used to set the element you wish to trigger events based upon. Does _not_ accept string; eg: `.className`. Use a dom node selector instead. Optional: If left blank, will use top of page. |
73 | | `triggerHook` | Used to change the ScrollMagic `triggerHook`. |
74 | | methods | You can actually apply all the ScrollMagic.Scene methods to `ScrollScene`, like `const scrollScene = new ScrollScene({...}); scrollScene.Scene.on('enter', ())` or `setPin`. See all the options here: http://scrollmagic.io/docs/ScrollMagic.Scene.html. The same goes for ScrollMagic.Controller, `scrollScene.Controller.destroy(true)`, but be careful if you're using the built-in `globalController`. |
75 |
76 | ### ScrollObserver Options (uses IntersectionObserver)
77 |
78 | | option | Description / Example |
79 | | -------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
80 | | `breakpoints` | `breakpoints: { 0: false, 768: true }` is used to set responsiveness of the new ScrollMagic.Scene, mobile-first. | |
81 | | `gsap` | Init a Gsap timeline with `gsap: { timeline: myTimeline, reverseSpeed: 2, yoyo: true, delay: 2 }`. |
82 | | `observer` | `observer: { rootMargin: '-50% 0%' }` is used to pass extra options to pass the IntersectionObserver, like `root`, `rootMargin`, or `threshold` (to override the thresholds option). `observer: { rootMargin: '0px', threshold: 1.0 }` |
83 | | `offset` | Used to change the `rootMargin` easy. `offset: '-10%` will be `rootMargin: '-10% 0%'`. This is a bit wonky and needs more testing. |
84 | | `thresholds` | `thresholds: 1` is to set the number of thresholds you want. `thresholds: 100 = [0, 0.1, 0.2, ... 0.98, 0.99, 1]`. It's easy to use `whenVisible`. |
85 | | `toggle` | Toggle a className on an element with `toggle: { element: containerRef.current, className: 'lets-do-this' }`. The `element` key does _not_ accept string; eg: `.className`. Use a dom node selector instead. |
86 | | `triggerElement` | `triggerElement: document.querySelector('#element')` is used to set the element you wish to trigger events based upon. Does _not_ accept string; eg: `.className`. Use a dom node selector instead. |
87 | | `useDuration` | `useDuration: true` to use the percentage of element visibility to scrub the gsap timeline. Similar to ScrollMagic Duration on a Gsap timeline, but not quite the same if the element is longer than the viewport height, thus the element visibility will never reach 100%, thus the gsap timeline will never reach 100%. |
88 | | `destroyImmediately` | `destroyImmediately: true` to destroy the scene immediately after firing once the element is visible. |
89 | | `whenVisible` | `whenVisible: '50%'` make the scene active when the triggerElement is visible whatever percentage you set. "50%" means to fire the event when the element is 50% in the viewport. This will override `thresholds`. |
90 | | `callback` | For adding callback functions. Make sure you pass functions. You can supply one or both callbacks. `callback: { active: () => (), notActive: () => () }` |
91 |
92 | See below for examples.
93 |
94 | ## Key Notes
95 |
96 | - This project sought out to overcome the issues of using Gsap3 with ScrollMagic, as well as ESM related problems. In the end it added more features, like video playback, scene init breakpoints, scene duration breakpoints, gsap speed controls, and using an IntersectionObserver when it fits your usecase better.
97 | - Is written in TypeScript so you have access to the types.
98 | - This does not include `gsap` or `scrollmagic`. If you plan to use them, you'll need to install them in addition to `scrollscene`. If you don't plan to use ScrollScene, you currently still have to install `scrollmagic`, but can so as a dev dependency. `yarn add -D scrollmagic`,
99 | - This works with Gsap without having to import the extra `animation.gsap.js` file from ScrollMagic (though you'll have to install in yourself `yarn add gsap` or `npm install gsap`). In turn this is smaller than using ScrollMagic and animation.gsap.js.
100 | - It allows for scene init breakpoints, and for scene duration breakpoints. This will also will on SSR if implemented correctly.
101 | - You do not need to create a ScrollMagic controller. It is done for you.
102 | - This will Tree Shake if your webpack is set up correctly. Next.js, for example, does this for you. Thus you can just `ScrollObserver` and not `ScrollScene` if you wanted and your build should exclude `ScrollScene` and `scrollmagic` (as long as you did import them).
103 | - Does not work with `jQuery`. You need to provide a domNodeSelector. Whether a `document.querySelector('#element')` or React ref `myRef.current`.
104 | - You can add all the methods from ScrollMagic.Scene directly onto the `scrollScene`. See options here http://scrollmagic.io/docs/ScrollMagic.Scene.html. You can do a `setPin`, or `on` event handler.
105 |
106 | ## Next Steps
107 |
108 | - Add native setPin functionality using ScrollObserver to deprecate using it with ScrollMagic.
109 | - Add out-of-the-box split text animations, svg line draw, etc...
110 | - Move ScrollObserver to a separate package.
111 |
112 | ## Usage
113 |
114 | ### ScrollScene (uses ScrollMagic)
115 |
116 | ```js
117 | import { ScrollScene } from 'scrollscene'
118 |
119 | const myElement = document.querySelector('#element')
120 |
121 | const scrollScene = new ScrollScene({
122 | triggerElement: myElement,
123 | })
124 | ```
125 |
126 | #### Toggle a className
127 |
128 | ```js
129 | import { ScrollScene } from 'scrollscene'
130 |
131 | const domNode = document.querySelector('#element')
132 | const anotherDomNode = document.querySelector('#element2')
133 |
134 | const scrollScene = new ScrollScene({
135 | triggerElement: domNode,
136 | toggle: {
137 | element: anotherDomNode,
138 | className: 'turn-blue',
139 | },
140 | })
141 | ```
142 |
143 | #### Toggle a className on a duration
144 |
145 | ```js
146 | import { ScrollScene } from 'scrollscene'
147 |
148 | const domNode = document.querySelector('#element')
149 | const anotherDomNode = document.querySelector('#element2')
150 |
151 | const scrollScene = new ScrollScene({
152 | triggerElement: domNode,
153 | toggle: {
154 | element: anotherDomNode,
155 | className: 'turn-blue',
156 | reverse: true,
157 | },
158 | triggerHook: 1,
159 | duration: '100%',
160 | })
161 | ```
162 |
163 | #### Add extra options from ScrollMagic (like a triggerHook or offset)
164 |
165 | ```js
166 | import { ScrollScene } from 'scrollscene'
167 |
168 | const domNode = document.querySelector('#element')
169 | const anotherDomNode = document.querySelector('#element2')
170 |
171 | const scrollScene = new ScrollScene({
172 | triggerElement: domNode,
173 | toggle: {
174 | element: anotherDomNode,
175 | className: 'turn-blue',
176 | },
177 | offset: 50,
178 | triggerHook: 0.5,
179 | })
180 | ```
181 |
182 | or anything from [new ScrollMagic.Scene(options)](http://scrollmagic.io/docs/ScrollMagic.Scene.html#constructor) under the `scene` key to contain those options.
183 |
184 | ```js
185 | import { ScrollScene } from 'scrollscene'
186 |
187 | const domNode = document.querySelector('#element')
188 | const anotherDomNode = document.querySelector('#element2')
189 |
190 | const scrollScene = new ScrollScene({
191 | triggerElement: domNode,
192 | toggle: {
193 | element: anotherDomNode,
194 | className: 'turn-blue',
195 | },
196 | scene: {
197 | logLevel: 1,
198 | },
199 | })
200 | ```
201 |
202 | Same for [new ScrollMagic.Controller(options)](http://scrollmagic.io/docs/ScrollMagic.Controller.html#constructor) under the `controller` key to contain those options.
203 |
204 | ```js
205 | import { ScrollScene } from 'scrollscene'
206 |
207 | const domNode = document.querySelector('#element')
208 | const anotherDomNode = document.querySelector('#element2')
209 |
210 | const scrollScene = new ScrollScene({
211 | triggerElement: domNode,
212 | toggle: {
213 | element: anotherDomNode,
214 | className: 'turn-blue',
215 | },
216 | controller: {
217 | logLevel: 3,
218 | },
219 | })
220 | ```
221 |
222 | Use a new local controller
223 |
224 | ```js
225 | import { ScrollScene } from 'scrollscene'
226 |
227 | const domNode = document.querySelector('#element')
228 | const anotherDomNode = document.querySelector('#element2')
229 |
230 | const scrollScene = new ScrollScene({
231 | triggerElement: domNode,
232 | toggle: {
233 | element: anotherDomNode,
234 | className: 'turn-blue',
235 | },
236 | useGlobalController: false,
237 | })
238 | ```
239 |
240 | Add event handlers (`on`) or `setPin`. See options here http://scrollmagic.io/docs/ScrollMagic.Scene.html.
241 |
242 | ```js
243 | import { ScrollScene } from 'scrollscene'
244 |
245 | const domNode = document.querySelector('#element')
246 |
247 | const scrollScene = new ScrollScene({
248 | triggerElement: domNode,
249 | })
250 |
251 | scrollScene.Scene.on('enter', function(event) {
252 | console.log('Scene entered.')
253 | })
254 | ```
255 |
256 | Add methods to the controller. See options here http://scrollmagic.io/docs/ScrollMagic.Controller.html. But be careful if you're using the built-in `globalController`, as it'll impact all the scenes you have.
257 |
258 | ```js
259 | import { ScrollScene } from 'scrollscene'
260 |
261 | const domNode = document.querySelector('#element')
262 |
263 | const scrollScene = new ScrollScene({
264 | triggerElement: domNode,
265 | })
266 |
267 | scrollScene.Controller.destroy(true)
268 | ```
269 |
270 | #### Using GSAP (Greensock)
271 |
272 | ```js
273 | import { ScrollScene } from 'scrollscene'
274 | import { gsap } from 'gsap'
275 |
276 | // create a timeline and add a tween
277 | const myTimeline = gsap.timeline({ paused: true })
278 | const domNode = document.querySelector('#element')
279 | const scrollTrigger = document.querySelector('.scroll-trigger-01')
280 |
281 | myTimeline.to(domNode, {
282 | x: -200,
283 | duration: 1,
284 | ease: 'power2.out',
285 | })
286 |
287 | const scrollScene = new ScrollScene({
288 | triggerElement: scrollTrigger,
289 | gsap: {
290 | timeline: myTimeline,
291 | },
292 | })
293 | ```
294 |
295 | #### Using GSAP (Greensock), and setting the reserveSpeed
296 |
297 | ```js
298 | import { ScrollScene } from 'scrollscene'
299 | import { gsap } from 'gsap'
300 |
301 | // create a timeline and add a tween
302 | const myTimeline = gsap.timeline({ paused: true })
303 | const domNode = document.querySelector('#element')
304 | const scrollTrigger = document.querySelector('.scroll-trigger-01')
305 |
306 | myTimeline.to(domNode, {
307 | x: -200,
308 | duration: 1,
309 | ease: 'power2.out',
310 | })
311 |
312 | const scrollScene = new ScrollScene({
313 | triggerElement: scrollTrigger,
314 | gsap: {
315 | timeline: tl,
316 | reverseSpeed: 4,
317 | },
318 | })
319 | ```
320 |
321 | #### Using GSAP (Greensock), and tying it to the user scrolling with a duration
322 |
323 | ```js
324 | import { ScrollScene } from 'scrollscene'
325 | import { gsap } from 'gsap'
326 |
327 | // create a timeline and add a tween
328 | const myTimeline = gsap.timeline({ paused: true })
329 | const domNode = document.querySelector('#element')
330 | const scrollTrigger = document.querySelector('.scroll-trigger-01')
331 |
332 | myTimeline.to(domNode, {
333 | x: -200,
334 | duration: 1,
335 | ease: 'power2.out',
336 | })
337 |
338 | const scrollScene = new ScrollScene({
339 | triggerElement: scrollTrigger,
340 | gsap: {
341 | timeline: tl,
342 | },
343 | duration: 500,
344 | })
345 | ```
346 |
347 | #### Add Indicators (Using the addIndicators plugin from ScrollMagic)
348 |
349 | I added to this package a modified version of the addIndicators plugin. It's easy to use. Just remember to remove it after you're done testing so it doesn't go into production.
350 |
351 | ```js
352 | import { ScrollScene, addIndicators } from 'scrollscene'
353 |
354 | const domNode = document.querySelector('#element')
355 | const anotherDomNode = document.querySelector('#element2')
356 |
357 | const scrollScene = new ScrollScene({
358 | triggerElement: domNode,
359 | toggle: {
360 | element: anotherDomNode,
361 | className: 'turn-blue',
362 | reverse: true,
363 | },
364 | triggerHook: 1,
365 | duration: '100%',
366 | })
367 |
368 | scrollScene.Scene.addIndicators({ name: 'pin scene', colorEnd: '#FFFFFF' })
369 | ```
370 |
371 | _Note_: Notice that it's `scrollScene.Scene`. `scrollScene` actually returns `Scene` and `Controller` methods, which you can then modify. `scrollScene.addIndicators` will not work.
372 |
373 | Alternatively you could do this and it'll apply to the built-in globalController...
374 |
375 | ```js
376 | import { ScrollScene, addIndicators } from 'scrollscene'
377 |
378 | const scrollScene = new ScrollScene({
379 | controller: {
380 | addIndicators: true,
381 | },
382 | })
383 | ```
384 |
385 | or
386 |
387 | ```js
388 | import { ScrollScene, addIndicators } from 'scrollscene'
389 |
390 | const scrollScene = new ScrollScene({
391 | ...options,
392 | })
393 |
394 | scrollScene.Controller({ addIndicators: true })
395 | ```
396 |
397 | ### ScrollObserver (uses IntersectionObserver)
398 |
399 | #### Toggle a className while element is visible on the page
400 |
401 | ```js
402 | import { ScrollObserver } from 'scrollscene'
403 | import { gsap } from 'gsap'
404 |
405 | const domNode = document.querySelector('#element')
406 | const anotherDomNode = document.querySelector('#element2')
407 |
408 | const scrollObserver = new ScrollObserver({
409 | triggerElement: domNode,
410 | toggle: {
411 | element: anotherDomNode,
412 | className: 'turn-blue',
413 | },
414 | })
415 | ```
416 |
417 | #### Toggle a Gsap animation while element is visible on the page
418 |
419 | ```js
420 | import { ScrollObserver } from 'scrollscene'
421 | import { gsap } from 'gsap'
422 |
423 | // create a timeline and add a tween
424 | const tl = gsap.timeline({ paused: true })
425 | const domNode = document.querySelector('#element')
426 | const squareElement = document.querySelector('#square')
427 |
428 | tl.to(squareElement, {
429 | x: -200,
430 | duration: 1,
431 | ease: 'power2.out',
432 | })
433 |
434 | const scrollObserver = new ScrollObserver({
435 | triggerElement: domNode,
436 | gsap: {
437 | timeline: tl,
438 | },
439 | })
440 | ```
441 |
442 | #### Toggle a Gsap animation while element is visible on the page with a yoyo effect and repeat delay of 0
443 |
444 | ```js
445 | import { ScrollObserver } from 'scrollscene'
446 | import { gsap } from 'gsap'
447 |
448 | // create a timeline and add a tween
449 | const tl = gsap.timeline({ paused: true })
450 | const domNode = document.querySelector('#element')
451 | const squareElement = document.querySelector('#square')
452 |
453 | tl.to(squareElement, {
454 | x: -200,
455 | duration: 1,
456 | ease: 'power2.out',
457 | })
458 |
459 | const scrollObserver = new ScrollObserver({
460 | triggerElement: domNode,
461 | gsap: {
462 | timeline: tl,
463 | yoyo: true,
464 | delay: 0,
465 | },
466 | })
467 | ```
468 |
469 | #### Scrub a Gsap timeline based on element visibility
470 |
471 | ```js
472 | import { ScrollObserver } from 'scrollscene'
473 | import { gsap } from 'gsap'
474 |
475 | // create a timeline and add a tween
476 | const tl = gsap.timeline({ paused: true })
477 | const domNode = document.querySelector('#element')
478 | const squareElement = document.querySelector('#square')
479 |
480 | tl.to(squareElement, {
481 | x: -200,
482 | duration: 1,
483 | ease: 'power2.out',
484 | })
485 |
486 | const scrollObserver = new ScrollObserver({
487 | triggerElement: domNode,
488 | gsap: {
489 | timeline: tl,
490 | },
491 | useDuration: true,
492 | })
493 | ```
494 |
495 | #### Start a video when an element is visible and pause the video when it's not
496 |
497 | ```js
498 | import { ScrollObserver } from 'scrollscene'
499 |
500 | const domNode = document.querySelector('#element')
501 | const videoTagDomNode = document.querySelector('#video')
502 |
503 | const scrollObserver = new ScrollObserver({
504 | triggerElement: domNode,
505 | video: {
506 | element: videoTagDomNode,
507 | playingClassName: 'is-playing',
508 | pausedClassName: 'is-paused',
509 | },
510 | })
511 | ```
512 |
513 | #### Using a scene once
514 |
515 | ```js
516 | import { ScrollObserver } from 'scrollscene'
517 |
518 | const domNode = document.querySelector('#element')
519 |
520 | const scrollObserver = new ScrollObserver({
521 | triggerElement: domNode,
522 | destroyImmediately: true,
523 | })
524 | ```
525 |
526 | #### Set a percentage for the visibility threshold
527 |
528 | ```js
529 | import { ScrollObserver } from 'scrollscene'
530 |
531 | const domNode = document.querySelector('#element')
532 |
533 | const scrollObserver = new ScrollObserver({
534 | triggerElement: domNode,
535 | whenVisible: '50%',
536 | })
537 | ```
538 |
539 | #### Set a different threshold
540 |
541 | The below would create an array of 100 thresholds ([0, 0.1, 0.2, ... 0.98, 0.99, 1]), effectively says any percent from 1 to 100 of the element intersecting the viewport should trigger the scene.
542 |
543 | ```js
544 | import { ScrollObserver } from 'scrollscene'
545 |
546 | const domNode = document.querySelector('#element')
547 |
548 | const scrollObserver = new ScrollObserver({
549 | triggerElement: domNode,
550 | thresholds: 100,
551 | })
552 | ```
553 |
554 | #### Extra observer options
555 |
556 | The below adds extra options to the IntersectionObserver. See others properities you could add [here](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver).
557 |
558 | ```js
559 | import { ScrollObserver } from 'scrollscene'
560 |
561 | const domNode = document.querySelector('#element')
562 |
563 | const scrollObserver = new ScrollObserver({
564 | triggerElement: domNode,
565 | observer: { rootMargin: '-50% 0%' },
566 | })
567 | ```
568 |
569 | or
570 |
571 | ```js
572 | import { ScrollObserver } from 'scrollscene'
573 |
574 | const domNode = document.querySelector('#element')
575 |
576 | const scrollObserver = new ScrollObserver({
577 | triggerElement: domNode,
578 | observer: {
579 | rootMargin: '0px',
580 | threshold: 1.0,
581 | },
582 | })
583 | ```
584 |
585 | ### Destroy the scene
586 |
587 | Whatever you've named your scene, whether `const scrollScene` or `const scrollObserver`, you can destroy it with...
588 |
589 | ```js
590 | scrollScene.destroy()
591 | ```
592 |
593 | ```js
594 | scrollObserver.destroy()
595 | ```
596 |
597 | ### Using React?
598 |
599 | With React it's best to do this inside either a `useEffect` hook or using the `componentDidMount` and `componentWillUnmount` lifecycle. Whatever you choose, make sure to destroy the scene on the unmount.
600 |
601 | See the Storybook source for good examples (story.js) [found here](https://github.com/jonkwheeler/ScrollScene/blob/master/src/stories/story.js).
602 |
603 | ```js
604 | import { ScrollScene } from 'scrollscene'
605 |
606 | const MyComponent = () => {
607 | // init ref
608 | const containerRef = React.useRef(null)
609 | const triggerRef = React.useRef(null)
610 |
611 | React.useEffect(() => {
612 | const { current: containerElement } = containerRef
613 | const { current: triggerElement } = triggerRef
614 |
615 | if (!containerElement && !triggerElement) {
616 | return undefined
617 | }
618 |
619 | const scrollScene = new ScrollScene({
620 | triggerElement: triggerElement,
621 | toggle: {
622 | element: containerElement,
623 | className: 'turn-blue',
624 | },
625 | })
626 |
627 | // destroy on unmount
628 | return () => {
629 | scrollScene.destroy()
630 | }
631 | })
632 |
633 | return (
634 |
635 |
636 |
637 |
Basic Example
638 |
Scroll Down
639 |
640 |
641 |
642 |
When this hits the top the page will turn blue
643 |
644 |
645 |
646 | )
647 | }
648 | ```
649 |
650 | ### Other options
651 |
652 | You can now set `breakpoints` so you scene is more responsive. They work mobile first. The below would set up a scene on tablet, but not mobile, and resizing will init and destroy.
653 |
654 | ```js
655 | const scrollScene = new ScrollScene({
656 | breakpoints: { 0: false, 768: true },
657 | })
658 | ```
659 |
660 | `duration` also can be responsive, finally! The below would set up a scene that lasts 50vh on mobile, 100% after.
661 |
662 | ```js
663 | const scrollScene = new ScrollScene({
664 | duration: { 0: '50%', 768: '100%' },
665 | })
666 | ```
667 |
668 | ## Polyfill
669 |
670 | In order to use ScrollObserver on IE, you'll need a polyfill IntersectionObserver. You can do this with loading a polyfill script from https://polyfill.io/.
671 |
672 | ### Using Next.js or Webpack?
673 |
674 | With Next.js you can load polyfills another way. See https://github.com/zeit/next.js/blob/canary/examples/with-polyfills/client/polyfills.js.
675 |
676 | Add following to `client/polyfills.js`
677 |
678 | ```js
679 | /*
680 | * This files runs at the very beginning (even before React and Next.js core)
681 | * https://github.com/zeit/next.js/blob/canary/examples/with-polyfills/client/polyfills.js
682 | */
683 |
684 | // https://www.npmjs.com/package/intersection-observer
685 | import 'intersection-observer'
686 | ```
687 |
688 | And then modify the `next.config.js`
689 |
690 | ```js
691 | // next.config.js
692 |
693 | const nextConfig = {
694 | webpack(config) {
695 | /*
696 | * Add polyfills
697 | * https://github.com/zeit/next.js/blob/canary/examples/with-polyfills/next.config.js
698 | */
699 |
700 | const originalEntry = config.entry
701 | config.entry = async () => {
702 | const entries = await originalEntry()
703 |
704 | if (entries['main.js'] && !entries['main.js'].includes('./client/polyfills.js')) {
705 | entries['main.js'].unshift('./client/polyfills.js')
706 | }
707 |
708 | return entries
709 | }
710 |
711 | return config
712 | },
713 | }
714 |
715 | module.exports = nextConfig
716 | ```
717 |
718 | For more on ScrollMagic, hit up [scrollmagic.io/](https://www.scrollmagic.io/) and [https://github.com/janpaepke/ScrollMagic](https://github.com/janpaepke/ScrollMagic)
719 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scrollscene",
3 | "title": "ScrollScene",
4 | "version": "0.0.24",
5 | "description": "ScrollScene is an extra layer on top of ScrollMagic as well as using IntersectionObserver to achieve similar effects.",
6 | "author": "Jon K. Wheeler",
7 | "license": "MIT",
8 | "main": "./dist/commonjs/index.js",
9 | "module": "./dist/esm/index.js",
10 | "types": "./dist/commonjs/index.d.ts",
11 | "files": [
12 | "dist"
13 | ],
14 | "sideEffects": false,
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/jonkwheeler/ScrollScene.git"
18 | },
19 | "publishConfig": {
20 | "scope": "jonkwheeler",
21 | "registry": "https://registry.npmjs.org/"
22 | },
23 | "config": {
24 | "dirBuild": "./dist",
25 | "dirCommonjs": "./dist/commonjs",
26 | "dirEsm": "./dist/esm",
27 | "dirSrc": "./src",
28 | "dirStorybook": "./.storybook",
29 | "dirStorybookBuild": "./public",
30 | "portStorybook": "8888"
31 | },
32 | "scripts": {
33 | "build:commonjs": "yarn nuke:commonjs && cross-env BABEL_ENV=commonjs babel $npm_package_config_dirSrc --out-dir $npm_package_config_dirCommonjs --extensions \".ts,.tsx,.js,.jsx\" --ignore \"**/*.stories.js\",\"**/story.js\",\"**/stories\",\"**/*.test.js\",\"test.js\",\"**/test\",\"**/*.md\",\"**/docs\" --source-maps inline",
34 | "build:esm": "yarn nuke:esm && cross-env BABEL_ENV=esm babel $npm_package_config_dirSrc --out-dir $npm_package_config_dirEsm --extensions \".ts,.tsx,.js,.jsx\" --ignore \"**/*.stories.js\",\"**/story.js\",\"**/stories\",\"**/*.test.js\",\"test.js\",\"**/test\",\"**/*.md\",\"**/docs\" --source-maps inline",
35 | "build:package": "yarn build:commonjs && yarn build:esm && yarn build:types",
36 | "build:storybook": "build-storybook -s $npm_package_config_dirStorybook -o $npm_package_config_dirStorybookBuild",
37 | "build:types:commonjs": "tsc --project tsconfig.types.cjs.json && tsconfig-replace-paths --project tsconfig.types.cjs.json",
38 | "build:types:esm": "tsc --project tsconfig.types.esm.json && tsconfig-replace-paths --project tsconfig.types.esm.json",
39 | "build:types": "yarn build:types:commonjs && yarn build:types:esm",
40 | "build": "yarn build:storybook",
41 | "dev": "yarn start:storybook",
42 | "lint": "eslint .",
43 | "nuke:all:install:dev": "yarn nuke:all && yarn && yarn dev",
44 | "nuke:all:install": "yarn nuke:all && yarn",
45 | "nuke:all": "rm -rf node_modules .cache $npm_package_config_dirBuild yarn.lock yarn-error.log package-lock.json",
46 | "nuke:build:rebuild": "yarn nuke:build && yarn build:package",
47 | "nuke:build": "rm -rf $npm_package_config_dirBuild",
48 | "nuke:commonjs": "rm -rf $npm_package_config_dirBuild",
49 | "nuke:esm": "rm -rf $npm_package_config_dirEsm",
50 | "prepublishOnly": "yarn nuke:build:rebuild",
51 | "release": "yarn release-it",
52 | "start:storybook": "start-storybook -p $npm_package_config_portStorybook -c $npm_package_config_dirStorybook",
53 | "storybook": "yarn start:storybook"
54 | },
55 | "dependencies": {
56 | "@babel/runtime": "^7.8.4",
57 | "is-client": "^0.0.2",
58 | "lodash.throttle": "^4.1.1"
59 | },
60 | "devDependencies": {
61 | "@babel/cli": "^7.7.7",
62 | "@babel/core": "^7.7.7",
63 | "@babel/plugin-proposal-class-properties": "^7.7.4",
64 | "@babel/plugin-proposal-export-default-from": "^7.7.4",
65 | "@babel/plugin-proposal-object-rest-spread": "^7.7.7",
66 | "@babel/plugin-transform-runtime": "^7.8.3",
67 | "@babel/preset-env": "^7.7.7",
68 | "@babel/preset-react": "^7.7.4",
69 | "@babel/preset-typescript": "^7.7.7",
70 | "@storybook/addon-knobs": "^5.2.8",
71 | "@storybook/addon-notes": "^5.2.8",
72 | "@storybook/addon-storysource": "^5.3.6",
73 | "@storybook/react": "^5.2.8",
74 | "babel-eslint": "^10.0.3",
75 | "babel-loader": "^8.0.6",
76 | "babel-plugin-add-react-displayname": "^0.0.5",
77 | "babel-plugin-module-resolver": "^4.0.0",
78 | "cross-env": "^6.0.3",
79 | "eslint": "^6.8.0",
80 | "eslint-plugin-react": "^7.17.0",
81 | "fork-ts-checker-webpack-plugin": "^3.1.1",
82 | "gsap": "^3.0.4",
83 | "prettier": "^1.19.1",
84 | "prop-types": "^15.7.2",
85 | "react": "^16.12.0",
86 | "react-dom": "^16.12.0",
87 | "release-it": "^12.4.3",
88 | "scrollmagic": "^2.0.7",
89 | "storybook": "^5.1.11",
90 | "styled-components": "^4.4.1",
91 | "ts-loader": "^6.2.1",
92 | "tsconfig-replace-paths": "^0.0.5",
93 | "tslint": "^5.20.1",
94 | "tslint-config-prettier": "^1.18.0",
95 | "tslint-eslint-rules": "^5.4.0",
96 | "types-installer": "^1.6.3",
97 | "typescript": "^3.7.4",
98 | "webpack": "^4.41.5"
99 | },
100 | "peerDependencies": {
101 | "scrollmagic": "^2.0.7"
102 | },
103 | "bugs": {
104 | "url": "https://github.com/jonkwheeler/ScrollScene/issues"
105 | },
106 | "homepage": "https://github.com/jonkwheeler/ScrollScene#readme",
107 | "keywords": [
108 | "scrollmagic",
109 | "scroll",
110 | "scrollscene",
111 | "gsap",
112 | "greensock"
113 | ]
114 | }
115 |
--------------------------------------------------------------------------------
/src/ScrollObserver.ts:
--------------------------------------------------------------------------------
1 | import { errorLog, isFunc, isObject, scrollAnimationInit, createArray, isString, stringContains } from './helpers'
2 |
3 | const nameSpace = 'ScrollObserver'
4 |
5 | // @ts-ignore
6 | const state = function(visible, alreadyFired) {
7 | this.visible = false
8 | this.alreadyFired = false
9 | }
10 |
11 | const setClassName = function(this: any, options) {
12 | const toggle = {
13 | element: null,
14 | className: null,
15 | ...options,
16 | }
17 |
18 | if (!toggle.element) {
19 | errorLog(
20 | nameSpace,
21 | `Be sure to set a const toggleElement = (reactRef.current or document.querySelector) in the new ${nameSpace}({ toggle: { element: toggleElement } })`,
22 | )
23 | }
24 |
25 | if (!toggle.className) {
26 | errorLog(
27 | nameSpace,
28 | `Be sure to set the className you want to toggle in the new ${nameSpace}({ toggle: { className: "my-class" } })`,
29 | )
30 | }
31 |
32 | this.add = function() {
33 | /* add className if it's not already there */
34 | !toggle.element.classList.contains(toggle.className) && toggle.element.classList.add(toggle.className)
35 | }
36 |
37 | this.remove = function() {
38 | /* remove className if it's there */
39 | toggle.element.classList.contains(toggle.className) && toggle.element.classList.remove(toggle.className)
40 | }
41 |
42 | this.update = function(setState) {
43 | if (!setState.alreadyFired && setState.visible) {
44 | this.add()
45 | }
46 |
47 | if (setState.alreadyFired && !setState.visible) {
48 | this.remove()
49 | }
50 | }
51 | }
52 |
53 | const setTween = function(this: any, options) {
54 | const gsap = {
55 | timeline: null,
56 | yoyo: false,
57 | speed: 1,
58 | reverseSpeed: 1,
59 | delay: 2,
60 | ...options,
61 | }
62 |
63 | if (!gsap.timeline) {
64 | errorLog(
65 | nameSpace,
66 | `Be sure to set a const tl = gsap.timeline({ paused: true }) in the new ${nameSpace}({ gsap: { timeline: tl } })`,
67 | )
68 | }
69 |
70 | const tl = gsap.timeline
71 |
72 | if (gsap.yoyo) {
73 | tl.repeat(-1)
74 | .yoyo(gsap.yoyo)
75 | .repeatDelay(gsap.delay)
76 | }
77 |
78 | this.play = function() {
79 | tl.timeScale(gsap.speed).play()
80 | }
81 |
82 | this.pause = function() {
83 | tl.pause()
84 | }
85 |
86 | this.reverse = function() {
87 | tl.timeScale(gsap.reverseSpeed).reverse()
88 | }
89 |
90 | this.kill = function() {
91 | if (tl) {
92 | tl.pause(0)
93 | tl.kill()
94 | }
95 | }
96 |
97 | this.update = function(setState) {
98 | if (!setState.alreadyFired && setState.visible) {
99 | this.play()
100 | }
101 |
102 | if (setState.alreadyFired && !setState.visible) {
103 | gsap.yoyo ? this.pause() : this.reverse()
104 | }
105 | }
106 |
107 | this.scrub = function(intersectionRatio) {
108 | tl.progress(intersectionRatio)
109 | }
110 | }
111 |
112 | const setPlayer = function(this: any, options) {
113 | const video = {
114 | element: null,
115 | playingClassName: null,
116 | pausedClassName: null,
117 | ...options,
118 | }
119 |
120 | if (!video.element) {
121 | errorLog(
122 | nameSpace,
123 | `Be sure to set a video element in the new ${nameSpace}({ video: { element: videoRef.current } })`,
124 | )
125 | }
126 |
127 | function handlePlay() {
128 | if (video.element.src) {
129 | video.element.play()
130 | video.playingClassName && video.element.classList.add(video.playingClassName)
131 | video.pausedClassName && video.element.classList.remove(video.pausedClassName)
132 | }
133 | }
134 | function handlePause() {
135 | if (video.element.src) {
136 | video.element.pause()
137 | video.pausedClassName && video.element.classList.add(video.pausedClassName)
138 | video.playingClassName && video.element.classList.remove(video.playingClassName)
139 | }
140 | }
141 |
142 | function tryPlay() {
143 | try {
144 | handlePlay()
145 | } catch (error) {
146 | handlePause()
147 | }
148 | }
149 |
150 | function tryPause() {
151 | try {
152 | handlePause()
153 | } catch (error) {}
154 | }
155 |
156 | this.play = function() {
157 | tryPlay()
158 | }
159 |
160 | this.pause = function() {
161 | tryPause()
162 | }
163 |
164 | this.kill = function() {
165 | tryPause()
166 | }
167 |
168 | this.update = function(setState) {
169 | if (!setState.alreadyFired && setState.visible) {
170 | this.play()
171 | }
172 |
173 | if (setState.alreadyFired && !setState.visible) {
174 | this.pause()
175 | }
176 | }
177 | }
178 |
179 | const setFunction: any = function(this: any, options) {
180 | const callback = {
181 | active: null,
182 | notActive: null,
183 | ...options,
184 | }
185 |
186 | if (!callback.active && !callback.notActive) {
187 | errorLog(
188 | nameSpace,
189 | `Be sure to set a callback active or notActive function in the new ${nameSpace}({ callback: { active: () => () } })`,
190 | )
191 | }
192 |
193 | if ((callback.active && !isFunc(callback.active)) || (callback.notActive && !isFunc(callback.notActive))) {
194 | errorLog(nameSpace, `Be sure to set the callback as a function `)
195 | }
196 |
197 | this.update = function(setState) {
198 | if (!setState.alreadyFired && setState.visible && callback.active) {
199 | callback.active()
200 | }
201 |
202 | if (setState.alreadyFired && !setState.visible && callback.notActive) {
203 | callback.notActive()
204 | }
205 | }
206 | }
207 |
208 | export interface IScrollObserverGsap {
209 | /**
210 | * timeline
211 | * @desc Insert your gsap.timeline object var
212 | * @type object
213 | * @example
214 | * const myTimeline = gsap.timeline()
215 | * gsap: { timeline: myTimeline }
216 | */
217 | timeline: any
218 |
219 | /**
220 | * yoyo
221 | * @desc Do you want gsap animation to loop back and forth?
222 | * @type boolean
223 | * @default false
224 | * @example
225 | * gsap: { yoyo: true }
226 | */
227 | yoyo?: boolean
228 |
229 | /**
230 | * speed
231 | * @desc The speed at which the gsap animation will play relative to it's current speed.
232 | * @type number
233 | * @default 1
234 | * @example
235 | * gsap: { speed: 2 } // = twice as fast
236 | */
237 | speed?: number
238 |
239 | /**
240 | * speed
241 | * @desc The speed at which the gsap animation will reverse relative to it's current speed.
242 | * @type number
243 | * @default 4
244 | * @example
245 | * gsap: { speed: 4 } // = 4x as fast
246 | */
247 | reverseSpeed?: number
248 |
249 | /**
250 | * delay
251 | * @desc The delay (in seconds) at which the gsap animation wait before reversing.
252 | * @type number
253 | * @default 2
254 | * @example
255 | * gsap: { delay: 3 } // = Wait 3 seconds
256 | */
257 | delay?: number
258 | }
259 |
260 | export interface IScrollObserverToggle {
261 | /**
262 | * className
263 | * @desc Specify the className you wish to toggle.
264 | * @type string
265 | * @example
266 | * toggle: { className: 'turn-me-blue-baby' }
267 | */
268 | className: string
269 |
270 | /**
271 | * element
272 | * @desc Specify the element you wish to toggle the className on.
273 | * @type HTMLElement | any
274 | * @example
275 | * toggle: { element: containerRef.current }
276 | */
277 | element: HTMLElement | any
278 | }
279 |
280 | export interface IScrollObserverVideo {
281 | /**
282 | * element
283 | * @desc Specify the video element you wish to interact with.
284 | * @type HTMLElement | any
285 | * @example
286 | * video: { element: videoRef.current }
287 | */
288 | element: HTMLElement | any
289 | }
290 |
291 | interface IScrollObserver {
292 | /**
293 | * breakpoints
294 | * @desc Use to set responsiveness of the ScrollObserver, mobile-first
295 | * @type object
296 | * @example
297 | * breakpoints: { 0: false, 768: true }
298 | */
299 | breakpoints?: object
300 |
301 | /**
302 | * callback
303 | * @desc Use to set a callback when the scene is active.
304 | * @type object
305 | * @example
306 | * callback: { active: () => (), notActive: () => () }
307 | */
308 | callback?: object
309 |
310 | /**
311 | * destroyImmediately
312 | * @desc Use to destroy the scene immediately after firing once the element is visible.
313 | * @type boolean
314 | * @example
315 | * destroyImmediately: true
316 | */
317 | destroyImmediately?: boolean
318 |
319 | /**
320 | * gsap
321 | * @desc Use to set options for the gsap animation of the ScrollObserver.
322 | * @type object
323 | * @example
324 | * gsap: { timeline: myTimeline, yoyo: true, reverseSpeed: 2 }
325 | */
326 | gsap?: IScrollObserverGsap
327 |
328 | /**
329 | * observer
330 | * @desc Extra options to pass the IntersectionObserver, like root, rootMargin, threshold (to override the thresholds option)
331 | * @type object
332 | * @example
333 | * observer: { rootMargin: '-50% 0%' }
334 | */
335 | observer?: object
336 |
337 | /**
338 | * offset
339 | * @desc Change the offset. This uses rootMargin thus only works as a negative offset.
340 | * @type number | string
341 | * @defaultvalue '0% 0%'
342 | * @example
343 | * offset: 500 // this will be rootMargin: '-500px 0%'
344 | */
345 | offset?: number | string
346 |
347 | /**
348 | * thresholds
349 | * @desc Set the number of thresholds you want.
350 | * @returns An Array of number from 0 to 1. Add 1 to your number to account for 0.
351 | * @type number
352 | * @example
353 | * thresholds: 1 = [0, 1]
354 | * thresholds: 2 = [0, 0.5, 1]
355 | * thresholds: 3 = [0, 0.33, 0.67, 1]
356 | * thresholds: 100 = [0, 0.1, 0.2, ... 0.98, 0.99, 1]
357 | */
358 | thresholds?: number
359 |
360 | /**
361 | * toggle
362 | * @desc Use to set the options for the toggling of a className
363 | * @type object
364 | * @example
365 | * toggle: { element: containerRef.current, className: 'lets-do-this' }
366 | */
367 | toggle?: IScrollObserverToggle
368 |
369 | /**
370 | * triggerElement
371 | * @desc Set the element you wish to trigger events based upon, the observed element.
372 | * @type HTMLElement | any
373 | * @example
374 | * triggerElement: triggerRef.current
375 | */
376 | triggerElement: HTMLElement | any
377 |
378 | /**
379 | * useDuration
380 | * @desc Use the percentage of element visibility to scrub the gsap timeline.
381 | * @type boolean
382 | * @example
383 | * useDuration: true
384 | */
385 | useDuration?: boolean
386 |
387 | /**
388 | * video
389 | * @desc Use to set the options for playing and pausing of the video.
390 | * @type object
391 | * @example
392 | * video: { element: videoRef.current }
393 | */
394 | video?: IScrollObserverVideo
395 |
396 | /**
397 | * whenVisible
398 | * @desc Set when the scene should be active based on the percentage of the element visible
399 | * @type number
400 | * @example
401 | * whenVisible: '50%'
402 | */
403 | whenVisible?: string
404 | }
405 |
406 | const ScrollObserver = function(
407 | this: any,
408 | {
409 | breakpoints,
410 | callback,
411 | destroyImmediately,
412 | gsap,
413 | observer,
414 | offset,
415 | whenVisible,
416 | thresholds,
417 | toggle,
418 | triggerElement,
419 | useDuration,
420 | video,
421 | }: IScrollObserver,
422 | ) {
423 | if (!triggerElement) {
424 | errorLog(
425 | nameSpace,
426 | 'Be sure to set a const triggerElement = (reactRef.current or document.querySelector) in the new ScrollScene({ triggerElement: triggerElement })',
427 | )
428 | }
429 |
430 | const $this = this
431 | let setToggle
432 | let setGsap
433 | let setVideo
434 | let setCallback
435 | let ratio
436 | let setRootMargin = '0% 0%'
437 | let setState = new state(false, false)
438 |
439 | if (typeof offset === 'number') {
440 | // protect against positive px values, for now
441 | setRootMargin = `-${Math.abs(offset)}px 0%`
442 | } else if (typeof offset === 'string') {
443 | // protect against positive percentage values, for now
444 | setRootMargin = `-${Math.abs(parseFloat(offset))}% 0%`
445 | }
446 |
447 | if (toggle && isObject(toggle)) {
448 | setToggle = new setClassName(toggle)
449 | }
450 |
451 | if (gsap && isObject(gsap)) {
452 | setGsap = new setTween(gsap)
453 | }
454 |
455 | if (video && isObject(video)) {
456 | setVideo = new setPlayer(video)
457 | }
458 |
459 | if (callback && isObject(callback)) {
460 | setCallback = new setFunction(callback)
461 | }
462 |
463 | const observerCallback = function(entries) {
464 | entries.forEach(({ isIntersecting, intersectionRatio }) => {
465 | /*
466 | * To help the wonkiness of IntersectionObserver, isIntersecting firing true when it's really false
467 | */
468 | if (ratio) {
469 | setState.visible = intersectionRatio >= ratio
470 | } else if (isIntersecting && !setState.visible) {
471 | /*
472 | * To help with setCallback and ignoring refiring extra function
473 | * calls due to intersectionRatio
474 | */
475 | setState.visible = true
476 | } else if (!isIntersecting && setState.visible) {
477 | setState.visible = false
478 | }
479 |
480 | setToggle && setToggle.update(setState)
481 | setGsap && (!useDuration ? setGsap.update(setState) : setGsap.scrub(intersectionRatio))
482 | setVideo && setVideo.update(setState)
483 | setCallback && setCallback.update(setState)
484 |
485 | /*
486 | * To help with ignoring refiring extra function
487 | */
488 | if (!setState.alreadyFired && setState.visible) {
489 | setState.alreadyFired = true
490 | }
491 |
492 | if (setState.alreadyFired && !setState.visible) {
493 | setState.alreadyFired = false
494 | }
495 |
496 | if (isIntersecting && destroyImmediately) {
497 | /*
498 | * destroy the scene after used once
499 | */
500 | $this.destroy()
501 | }
502 | })
503 | }
504 |
505 | const getPercentage = value => {
506 | if (!isString(whenVisible) && !stringContains(whenVisible, '%')) {
507 | errorLog(nameSpace, 'Be sure to set a percentage as a string. { whenVisible: "50%" }')
508 | }
509 |
510 | const parsed = parseInt(value.replace('%', '')) / 100
511 |
512 | ratio = parsed
513 |
514 | return parsed
515 | }
516 |
517 | const getThresolds = () => {
518 | const defaults = {
519 | one: [0, 1],
520 | gsap: createArray(199),
521 | }
522 |
523 | let returnedThresholds: any = defaults.one
524 |
525 | if (whenVisible) {
526 | returnedThresholds = getPercentage(whenVisible)
527 | }
528 |
529 | if (useDuration) {
530 | returnedThresholds = defaults.gsap
531 | }
532 |
533 | if (thresholds) {
534 | returnedThresholds = createArray(thresholds)
535 | }
536 |
537 | return returnedThresholds
538 | }
539 |
540 | const Observer = new IntersectionObserver(observerCallback, {
541 | threshold: getThresolds(),
542 | rootMargin: setRootMargin,
543 | ...observer,
544 | })
545 |
546 | this.init = function() {
547 | Observer.observe(triggerElement)
548 | }
549 |
550 | this.destroy = function() {
551 | if (triggerElement && Observer) {
552 | if (setToggle) {
553 | setToggle.remove()
554 | }
555 |
556 | if (setGsap) {
557 | setGsap.kill()
558 | }
559 |
560 | if (setVideo) {
561 | setVideo.kill()
562 | }
563 |
564 | Observer.unobserve(triggerElement)
565 | }
566 | }
567 |
568 | scrollAnimationInit(breakpoints, this.init, this.destroy)
569 | }
570 |
571 | export { ScrollObserver }
572 |
--------------------------------------------------------------------------------
/src/ScrollScene.ts:
--------------------------------------------------------------------------------
1 | import ScrollMagic from './scrollmagic-with-ssr'
2 | import throttle from 'lodash.throttle'
3 | import { errorLog, isObject, scrollAnimationInit } from './helpers'
4 | import { IScrollObserverToggle, IScrollObserverGsap } from './ScrollObserver'
5 |
6 | const nameSpace = 'ScrollScene'
7 |
8 | const updateTweenProgress = function(Scene, Tween, gsapForwardSpeed, gsapReverseSpeed) {
9 | if (Tween) {
10 | const progress = Scene.progress()
11 | const state = Scene.state()
12 | if (Tween.repeat && Tween.repeat() === -1) {
13 | // infinite loop, so not in relation to progress
14 | if (state === 'DURING' && Tween.paused()) {
15 | Tween.timeScale(gsapForwardSpeed).play()
16 | } else if (state !== 'DURING' && !Tween.paused()) {
17 | Tween.pause()
18 | }
19 | } else if (progress != Tween.progress()) {
20 | // do we even need to update the progress?
21 | // no infinite loop - so should we just play or go to a specific point in time?
22 | if (Scene.duration() === 0) {
23 | // play the animation
24 | if (progress > 0) {
25 | // play from 0 to 1
26 | Tween.timeScale(gsapForwardSpeed).play()
27 | } else {
28 | // play from 1 to 0
29 | Tween.timeScale(gsapReverseSpeed).reverse()
30 | }
31 | } else {
32 | // go to a specific point in time
33 | // just hard set it
34 | Tween.progress(progress).pause()
35 | }
36 | }
37 | }
38 | }
39 |
40 | const removeTween = function(Tween) {
41 | if (Tween) {
42 | Tween.pause(0)
43 | Tween.kill()
44 | }
45 | }
46 |
47 | const setDuration = (Scene, duration) => {
48 | /* check if duration is set as an HTMLElement */
49 | if (duration instanceof HTMLElement) {
50 | let previousHeight
51 | let currentHeight
52 |
53 | const getHeight = () => duration.offsetHeight
54 |
55 | const update = () => {
56 | Scene.duration(getHeight())
57 | previousHeight = getHeight()
58 | }
59 |
60 | const fn = () => {
61 | /* set duration to match height of element */
62 | currentHeight = getHeight()
63 |
64 | if (currentHeight !== previousHeight) {
65 | update()
66 | }
67 | }
68 |
69 | fn()
70 |
71 | window.addEventListener('resize', throttle(fn, 700))
72 |
73 | currentHeight = getHeight()
74 |
75 | update()
76 | } else if (isObject(duration)) {
77 | /* if an object, make breakpoints */
78 | const keys = Object.keys(duration).reverse()
79 |
80 | const fn = () => {
81 | for (let index = 0; index < keys.length; index++) {
82 | const breakpoint = parseFloat(keys[index])
83 |
84 | if (breakpoint <= window.innerWidth) {
85 | Scene.duration(duration[breakpoint])
86 | break
87 | }
88 | }
89 | }
90 |
91 | fn()
92 |
93 | window.addEventListener('resize', throttle(fn, 700))
94 | } else {
95 | /* nothing of the above? just set it */
96 | Scene.duration(duration)
97 | }
98 | }
99 |
100 | const setClassName = (Scene, options, duration) => {
101 | const toggle = {
102 | className: null,
103 | element: null,
104 | reverse: false,
105 | ...options,
106 | }
107 |
108 | if (!toggle.className) {
109 | errorLog(nameSpace, `Be sure to set a className in the new ${nameSpace}({ toggle: { className: "my-class" } })`)
110 | }
111 |
112 | if (!toggle.element) {
113 | errorLog(
114 | nameSpace,
115 | `Be sure to set a const toggleElement = (reactRef.current or document.querySelector) in the new ${nameSpace}({ toggle: { element: toggleElement } })`,
116 | )
117 | }
118 |
119 | const addClassName = () =>
120 | !toggle.element.classList.contains(toggle.className) && toggle.element.classList.add(toggle.className)
121 |
122 | const removeClassName = () =>
123 | toggle.element.classList.contains(toggle.className) && toggle.element.classList.remove(toggle.className)
124 |
125 | Scene.on('enter', function() {
126 | addClassName()
127 | })
128 |
129 | Scene.on('add', function() {
130 | if (Scene.state() === 'DURING') {
131 | addClassName()
132 | }
133 | })
134 |
135 | Scene.on('leave', function(event) {
136 | if (!toggle.reverse && duration) {
137 | // needs to be based on whether or not we have a duration
138 | event.scrollDirection === 'REVERSE' && removeClassName()
139 | } else {
140 | removeClassName()
141 | }
142 | })
143 |
144 | Scene.on('remove', function() {
145 | removeClassName()
146 | })
147 | }
148 |
149 | const setTween = (Scene, options) => {
150 | const gsap = {
151 | forwardSpeed: 1,
152 | reverseSpeed: 1,
153 | timeline: null,
154 | ...options,
155 | }
156 |
157 | if (!gsap.timeline) {
158 | errorLog(
159 | nameSpace,
160 | `Be sure to set a const tl = gsap.timeline({ paused: true }) in the new ${nameSpace}({ gsap: { timeline: tl } })`,
161 | )
162 | }
163 |
164 | Scene.on('progress', function() {
165 | updateTweenProgress(Scene, gsap.timeline, gsap.forwardSpeed, gsap.reverseSpeed)
166 | })
167 |
168 | Scene.on('remove', function() {
169 | removeTween(gsap.timeline)
170 | })
171 | }
172 |
173 | interface IScrollSceneToggle extends IScrollObserverToggle {
174 | /**
175 | * reverse
176 | * @desc Specify the className should be removed after the duration of scene is met. Only applies if scene has duration.
177 | * @type boolean
178 | * @default false
179 | * @example
180 | * toggle: { reverse: true }
181 | */
182 | reverse?: boolean
183 | }
184 |
185 | interface IScrollScene {
186 | /**
187 | * breakpoints
188 | * @desc Use to set responsiveness of the new ScrollMagic.Scene, mobile-first
189 | * @type object
190 | * @example
191 | * breakpoints: { 0: false, 768: true }
192 | */
193 | breakpoints?: object
194 |
195 | /**
196 | * controller
197 | * @desc Extra options to pass the new ScrollMagic.Controller, like vertical, etc.
198 | * @type object
199 | * @example
200 | * controller: { vertical: false }
201 | */
202 | controller?: object
203 |
204 | /**
205 | * duration
206 | * @desc Use to set responsiveness of the new ScrollMagic.Scene, mobile-first (if setting breakpoints)
207 | * Must be string for percentage, and number for pixel.
208 | * @type object
209 | * @example
210 | * duration: '100%' = 100vh
211 | * duration: 100 = 100px
212 | * duration: { 0: '50%', 768: '100% } // = ScrollScene lasts 50vh on mobile, 100% after
213 | */
214 | duration?: string | number | object
215 |
216 | /**
217 | * gsap
218 | * @desc Use to set options for the gsap animation of the ScrollObserver.
219 | * @type object
220 | * @example
221 | * gsap: { timeline: myTimeline, yoyo: true, reverseSpeed: 2 }
222 | */
223 | gsap?: IScrollObserverGsap
224 |
225 | /**
226 | * triggerHook
227 | * @desc Set the offset of the ScrollMagic scene.
228 | * @type number | string
229 | * @defaultvalue 0
230 | * @example
231 | * offset: 100
232 | * offset: '10%'
233 | */
234 | offset?: number | string
235 |
236 | /**
237 | * scene
238 | * @desc Extra options to pass the new ScrollMagic.Scene, like logLevel, etc.
239 | * @type object
240 | * @example
241 | * scene: { logLevel: 2 }
242 | */
243 | scene?: object
244 |
245 | /**
246 | * toggle
247 | * @desc Use to set the options for the toggling of a className
248 | * @type object
249 | * @example
250 | * toggle: { element: containerRef.current, className: 'lets-do-this' }
251 | */
252 | toggle?: IScrollSceneToggle
253 |
254 | /**
255 | * triggerElement
256 | * @desc Set the element you wish to trigger events based upon, the observed element.
257 | * @type HTMLElement | any
258 | * @example
259 | * triggerElement: triggerRef.current
260 | */
261 | triggerElement: HTMLElement | any
262 |
263 | /**
264 | * triggerHook
265 | * @desc Set the triggerHook of the ScrollMagic scene.
266 | * @type number
267 | * @defaultvalue 'onEnter'
268 | * @example
269 | * triggerHook: 0.5
270 | */
271 | triggerHook?: number | string
272 |
273 | /**
274 | * useGlobalController
275 | * @desc Chose whether or not to use the globalController provided for you, or a fresh new ScrollMagic.Controller instance.
276 | * @type boolean
277 | * @defaultValue true
278 | * @example
279 | * useGlobalController: false
280 | */
281 | useGlobalController?: boolean
282 | }
283 |
284 | // add controller var
285 | let globalController
286 |
287 | const ScrollScene = function(
288 | this: any,
289 | {
290 | breakpoints,
291 | controller = {},
292 | duration,
293 | gsap,
294 | offset = 0,
295 | scene = {},
296 | toggle,
297 | triggerElement,
298 | triggerHook = 'onEnter',
299 | useGlobalController = true,
300 | }: IScrollScene,
301 | ) {
302 | let localController
303 |
304 | // check if using a local controller
305 | if (!useGlobalController) {
306 | localController = new ScrollMagic.Controller(controller)
307 | }
308 |
309 | // mount controller
310 | if (!globalController && useGlobalController) {
311 | globalController = new ScrollMagic.Controller(controller)
312 | }
313 |
314 | const controllerIsUse = localController ? localController : globalController
315 |
316 | if (!triggerElement) {
317 | errorLog(
318 | nameSpace,
319 | `Be sure to set a const triggerElement = (reactRef.current or document.querySelector) in the new ${nameSpace}({ triggerElement: triggerElement }).`,
320 | )
321 | }
322 |
323 | const Scene = new ScrollMagic.Scene({
324 | triggerElement,
325 | triggerHook,
326 | offset,
327 | ...scene,
328 | })
329 |
330 | if (duration) {
331 | setDuration(Scene, duration)
332 | }
333 |
334 | if (toggle && isObject(toggle)) {
335 | setClassName(Scene, toggle, duration)
336 | }
337 |
338 | if (gsap && isObject(gsap)) {
339 | setTween(Scene, gsap)
340 | }
341 |
342 | this.init = function() {
343 | controllerIsUse && Scene.addTo(controllerIsUse)
344 | }
345 |
346 | this.destroy = function() {
347 | Scene.remove()
348 | }
349 |
350 | this.Scene = Scene
351 | this.Controller = controllerIsUse
352 |
353 | scrollAnimationInit(breakpoints, this.init, this.destroy)
354 | }
355 |
356 | export { ScrollScene }
357 |
--------------------------------------------------------------------------------
/src/helpers/createArray.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * createArray
3 | * Set performance using thresholds
4 | * @desc Building an array of numbers starting at 0.00 and incrementing at every 0.01
5 | * @example
6 | * thresholds = 2 [0, 0.5, 1], thresholds = 3 [0, 0.33, 0.67, 1]
7 | */
8 | import { roundOff } from './roundOff'
9 |
10 | export const createArray = value =>
11 | Array.apply(null, new Array(value + 1))
12 | // @ts-ignore
13 | .map((n, i) => roundOff(i / value))
14 |
--------------------------------------------------------------------------------
/src/helpers/errorLog.ts:
--------------------------------------------------------------------------------
1 | export const errorLog = (nameSpace, msg) => {
2 | throw new Error(`${nameSpace} -> ${msg}`)
3 | }
4 |
--------------------------------------------------------------------------------
/src/helpers/index.ts:
--------------------------------------------------------------------------------
1 | export { createArray } from './createArray'
2 | export { errorLog } from './errorLog'
3 | export { isFunc } from './isFunc'
4 | export { isObject } from './isObject'
5 | export { isString } from './isString'
6 | export { roundOff } from './roundOff'
7 | export { scrollAnimationInit } from './scrollAnimationInit'
8 | export { stringContains } from './stringContains'
9 |
--------------------------------------------------------------------------------
/src/helpers/isFunc.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * isFunc
3 | * @returns boolean
4 | * @example
5 | * isFunc(function(){}) => true
6 | * isFunc('hi') => false
7 | */
8 | export const isFunc = value => typeof value === 'function'
9 |
--------------------------------------------------------------------------------
/src/helpers/isObject.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * isObject
3 | * @returns boolean
4 | * @example
5 | * isObject({foo: 'bar'}) => true
6 | * isObject('yo') => false
7 | */
8 | export const isObject = x => typeof x === 'object' && !Array.isArray(x) && x !== null
9 |
--------------------------------------------------------------------------------
/src/helpers/isString.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * isString
3 | * @returns boolean
4 | * @example
5 | * isString('yo') => true
6 | * isString({foo: 'bar'}) => false
7 | */
8 | export const isString = x => typeof x === 'string'
9 |
--------------------------------------------------------------------------------
/src/helpers/roundOff.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * roundOff
3 | * @desc round a number off to nearest 2 decimal places
4 | * roundOff(5) => 5
5 | * roundOff(100) => 100
6 | * roundOff(0.23432) => 0.23
7 | * roundOff(0.897) => 0.9
8 | */
9 |
10 | export const roundOff = value => Math.round(value * 100) / 100
11 |
--------------------------------------------------------------------------------
/src/helpers/scrollAnimationInit.ts:
--------------------------------------------------------------------------------
1 | import throttle from 'lodash.throttle'
2 | import { isObject } from './isObject'
3 |
4 | export const scrollAnimationInit = (breakpoints, init, destroy) => {
5 | if (isObject(breakpoints)) {
6 | const keys = Object.keys(breakpoints).reverse()
7 |
8 | const fn = () => {
9 | for (let index = 0; index < keys.length; index += 1) {
10 | const breakpoint = parseFloat(keys[index])
11 |
12 | if (breakpoint <= window.innerWidth) {
13 | if (breakpoints[breakpoint]) {
14 | init()
15 | } else {
16 | destroy()
17 | }
18 |
19 | break
20 | }
21 | }
22 | }
23 |
24 | fn()
25 |
26 | window.addEventListener('resize', throttle(fn, 700))
27 | } else {
28 | init()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/helpers/stringContains.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * stringContains
3 | * @returns boolean
4 | * @example
5 | * stringContains('yoyoyo', 'yo') => true
6 | * stringContains('Hello', 'World') => false
7 | */
8 | export const stringContains = (doesThis: string, containThis: string) => doesThis.indexOf(containThis) !== -1
9 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { ScrollScene } from './ScrollScene'
2 | export { ScrollObserver } from './ScrollObserver'
3 |
4 | import scrollmagic from './scrollmagic-with-ssr'
5 | export { scrollmagic as ScrollMagic }
6 | export { scrollmagic as ScrollMagicSsr }
7 |
8 | import addindicators from './scrollmagic-add-indicators'
9 | export { addindicators as addIndicators }
10 |
--------------------------------------------------------------------------------
/src/plugins/debug.addIndicators.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /* prettier-ignore */
3 |
4 | /*
5 | Slightly modified version of this: https://github.com/janpaepke/ScrollMagic/blob/master/dev/src/plugins/debug.addIndicators.js
6 | */
7 |
8 | import ScrollMagic from '../scrollmagic-with-ssr'
9 |
10 | var NAMESPACE = 'debug.addIndicators'
11 |
12 | var console = window.console || {},
13 | //@ts-ignore
14 | err = Function.prototype.bind.call(console.error || console.log || function() {}, console)
15 | if (!ScrollMagic) {
16 | err(
17 | '(' +
18 | NAMESPACE +
19 | ") -> ERROR: The ScrollMagic main module could not be found. Please make sure it's loaded before this plugin or use an asynchronous loader like requirejs.",
20 | )
21 | }
22 |
23 | // plugin settings
24 | var FONT_SIZE = '0.85em',
25 | ZINDEX = '9999',
26 | EDGE_OFFSET = 15 // minimum edge distance, added to indentation
27 |
28 | // overall vars
29 | var _util = ScrollMagic._util,
30 | _autoindex = 0
31 |
32 | ScrollMagic.Scene.extend(function() {
33 | var Scene = this,
34 | _indicator
35 | //@ts-ignore
36 | var log = function() {
37 | if (Scene._log) {
38 | // not available, when main source minified
39 | Array.prototype.splice.call(arguments, 1, 0, '(' + NAMESPACE + ')', '->')
40 | Scene._log.apply(this, arguments)
41 | }
42 | }
43 |
44 | /**
45 | * Add visual indicators for a ScrollMagic.Scene.
46 | * @memberof! debug.addIndicators#
47 | *
48 | * @example
49 | * // add basic indicators
50 | * scene.addIndicators()
51 | *
52 | * // passing options
53 | * scene.addIndicators({name: "pin scene", colorEnd: "#FFFFFF"});
54 | *
55 | * @param {object} [options] - An object containing one or more options for the indicators.
56 | * @param {(string|object)} [options.parent] - A selector, DOM Object or a jQuery object that the indicators should be added to.
57 | If undefined, the controller's container will be used.
58 | * @param {number} [options.name=""] - This string will be displayed at the start and end indicators of the scene for identification purposes. If no name is supplied an automatic index will be used.
59 | * @param {number} [options.indent=0] - Additional position offset for the indicators (useful, when having multiple scenes starting at the same position).
60 | * @param {string} [options.colorStart=green] - CSS color definition for the start indicator.
61 | * @param {string} [options.colorEnd=red] - CSS color definition for the end indicator.
62 | * @param {string} [options.colorTrigger=blue] - CSS color definition for the trigger indicator.
63 | */
64 | Scene.addIndicators = function(options) {
65 | if (!_indicator) {
66 | var DEFAULT_OPTIONS = {
67 | name: '',
68 | indent: 0,
69 | parent: undefined,
70 | colorStart: 'green',
71 | colorEnd: 'red',
72 | colorTrigger: 'blue',
73 | }
74 |
75 | options = _util.extend({}, DEFAULT_OPTIONS, options)
76 |
77 | _autoindex++
78 | _indicator = new Indicator(Scene, options)
79 |
80 | Scene.on('add.plugin_addIndicators', _indicator.add)
81 | Scene.on('remove.plugin_addIndicators', _indicator.remove)
82 | Scene.on('destroy.plugin_addIndicators', Scene.removeIndicators)
83 |
84 | // it the scene already has a controller we can start right away.
85 | if (Scene.controller()) {
86 | _indicator.add()
87 | }
88 | }
89 | return Scene
90 | }
91 |
92 | /**
93 | * Removes visual indicators from a ScrollMagic.Scene.
94 | * @memberof! debug.addIndicators#
95 | *
96 | * @example
97 | * // remove previously added indicators
98 | * scene.removeIndicators()
99 | *
100 | */
101 | Scene.removeIndicators = function() {
102 | if (_indicator) {
103 | _indicator.remove()
104 | this.off('*.plugin_addIndicators')
105 | _indicator = undefined
106 | }
107 | return Scene
108 | }
109 | })
110 |
111 | /*
112 | * ----------------------------------------------------------------
113 | * Extension for controller to store and update related indicators
114 | * ----------------------------------------------------------------
115 | */
116 | // add option to globally auto-add indicators to scenes
117 | /**
118 | * Every ScrollMagic.Controller instance now accepts an additional option.
119 | * See {@link ScrollMagic.Controller} for a complete list of the standard options.
120 | * @memberof! debug.addIndicators#
121 | * @method new ScrollMagic.Controller(options)
122 | * @example
123 | * // make a controller and add indicators to all scenes attached
124 | * var controller = new ScrollMagic.Controller({addIndicators: true});
125 | * // this scene will automatically have indicators added to it
126 | * new ScrollMagic.Scene()
127 | * .addTo(controller);
128 | *
129 | * @param {object} [options] - Options for the Controller.
130 | * @param {boolean} [options.addIndicators=false] - If set to `true` every scene that is added to the controller will automatically get indicators added to it.
131 | */
132 | ScrollMagic.Controller.addOption('addIndicators', false)
133 | // extend Controller
134 | ScrollMagic.Controller.extend(function() {
135 | var Controller = this,
136 | _info = Controller.info(),
137 | _container = _info.container,
138 | _isDocument = _info.isDocument,
139 | _vertical = _info.vertical,
140 | _indicators = {
141 | // container for all indicators and methods
142 | groups: [],
143 | }
144 |
145 | var log = function() {
146 | if (Controller._log) {
147 | // not available, when main source minified
148 | Array.prototype.splice.call(arguments, 1, 0, '(' + NAMESPACE + ')', '->')
149 | Controller._log.apply(this, arguments)
150 | }
151 | }
152 | if (Controller._indicators) {
153 | //@ts-ignore
154 | log(2, "WARNING: Scene already has a property '_indicators', which will be overwritten by plugin.")
155 | }
156 |
157 | // add indicators container
158 | this._indicators = _indicators
159 | /*
160 | needed updates:
161 | +++++++++++++++
162 | start/end position on scene shift (handled in Indicator class)
163 | trigger parameters on triggerHook value change (handled in Indicator class)
164 | bounds position on container scroll or resize (to keep alignment to bottom/right)
165 | trigger position on container resize, window resize (if container isn't document) and window scroll (if container isn't document)
166 | */
167 |
168 | // event handler for when associated bounds markers need to be repositioned
169 | var handleBoundsPositionChange = function() {
170 | //@ts-ignore
171 | _indicators.updateBoundsPositions()
172 | }
173 |
174 | // event handler for when associated trigger groups need to be repositioned
175 | var handleTriggerPositionChange = function() {
176 | //@ts-ignore
177 | _indicators.updateTriggerGroupPositions()
178 | }
179 |
180 | _container.addEventListener('resize', handleTriggerPositionChange)
181 | if (!_isDocument) {
182 | window.addEventListener('resize', handleTriggerPositionChange)
183 | window.addEventListener('scroll', handleTriggerPositionChange)
184 | }
185 | // update all related bounds containers
186 | _container.addEventListener('resize', handleBoundsPositionChange)
187 | _container.addEventListener('scroll', handleBoundsPositionChange)
188 |
189 | // updates the position of the bounds container to aligned to the right for vertical containers and to the bottom for horizontal
190 | this._indicators.updateBoundsPositions = function(specificIndicator) {
191 | var // constant for all bounds
192 | groups = specificIndicator
193 | ? [
194 | _util.extend({}, specificIndicator.triggerGroup, {
195 | members: [specificIndicator],
196 | }),
197 | ] // create a group with only one element
198 | : _indicators.groups, // use all
199 | g = groups.length,
200 | css = {},
201 | paramPos = _vertical ? 'left' : 'top',
202 | paramDimension = _vertical ? 'width' : 'height',
203 | edge = _vertical
204 | ? _util.get.scrollLeft(_container) + _util.get.width(_container) - EDGE_OFFSET
205 | : _util.get.scrollTop(_container) + _util.get.height(_container) - EDGE_OFFSET,
206 | b,
207 | triggerSize,
208 | group
209 | while (g--) {
210 | // group loop
211 | group = groups[g]
212 | b = group.members.length
213 | triggerSize = _util.get[paramDimension](group.element.firstChild)
214 | while (b--) {
215 | // indicators loop
216 | css[paramPos] = edge - triggerSize
217 | _util.css(group.members[b].bounds, css)
218 | }
219 | }
220 | }
221 |
222 | // updates the positions of all trigger groups attached to a controller or a specific one, if provided
223 | this._indicators.updateTriggerGroupPositions = function(specificGroup) {
224 | var // constant vars
225 | groups = specificGroup ? [specificGroup] : _indicators.groups,
226 | i = groups.length,
227 | container = _isDocument ? document.body : _container,
228 | containerOffset = _isDocument
229 | ? {
230 | top: 0,
231 | left: 0,
232 | }
233 | : _util.get.offset(container, true),
234 | edge = _vertical ? _util.get.width(_container) - EDGE_OFFSET : _util.get.height(_container) - EDGE_OFFSET,
235 | paramDimension = _vertical ? 'width' : 'height',
236 | paramTransform = _vertical ? 'Y' : 'X'
237 | var // changing vars
238 | group,
239 | elem,
240 | pos,
241 | elemSize,
242 | transform
243 | while (i--) {
244 | group = groups[i]
245 | elem = group.element
246 | pos = group.triggerHook * Controller.info('size')
247 | elemSize = _util.get[paramDimension](elem.firstChild.firstChild)
248 | transform = pos > elemSize ? 'translate' + paramTransform + '(-100%)' : ''
249 |
250 | _util.css(elem, {
251 | top: containerOffset.top + (_vertical ? pos : edge - group.members[0].options.indent),
252 | left: containerOffset.left + (_vertical ? edge - group.members[0].options.indent : pos),
253 | })
254 | _util.css(elem.firstChild.firstChild, {
255 | '-ms-transform': transform,
256 | '-webkit-transform': transform,
257 | transform: transform,
258 | })
259 | }
260 | }
261 |
262 | // updates the label for the group to contain the name, if it only has one member
263 | this._indicators.updateTriggerGroupLabel = function(group) {
264 | var text = 'trigger' + (group.members.length > 1 ? '' : ' ' + group.members[0].options.name),
265 | elem = group.element.firstChild.firstChild,
266 | doUpdate = elem.textContent !== text
267 | if (doUpdate) {
268 | elem.textContent = text
269 | if (_vertical) {
270 | // bounds position is dependent on text length, so update
271 | //@ts-ignore
272 | _indicators.updateBoundsPositions()
273 | }
274 | }
275 | }
276 |
277 | // add indicators if global option is set
278 | this.addScene = function(newScene) {
279 | if (this._options.addIndicators && newScene instanceof ScrollMagic.Scene && newScene.controller() === Controller) {
280 | newScene.addIndicators()
281 | }
282 | // call original destroy method
283 | this.$super.addScene.apply(this, arguments)
284 | }
285 |
286 | // remove all previously set listeners on destroy
287 | this.destroy = function() {
288 | _container.removeEventListener('resize', handleTriggerPositionChange)
289 | if (!_isDocument) {
290 | window.removeEventListener('resize', handleTriggerPositionChange)
291 | window.removeEventListener('scroll', handleTriggerPositionChange)
292 | }
293 | _container.removeEventListener('resize', handleBoundsPositionChange)
294 | _container.removeEventListener('scroll', handleBoundsPositionChange)
295 | // call original destroy method
296 | this.$super.destroy.apply(this, arguments)
297 | }
298 | return Controller
299 | })
300 |
301 | /*
302 | * ----------------------------------------------------------------
303 | * Internal class for the construction of Indicators
304 | * ----------------------------------------------------------------
305 | */
306 | var Indicator = function(Scene, options) {
307 | var Indicator = this,
308 | _elemBounds = TPL.bounds(),
309 | _elemStart = TPL.start(options.colorStart),
310 | _elemEnd = TPL.end(options.colorEnd),
311 | _boundsContainer = options.parent && _util.get.elements(options.parent)[0],
312 | _vertical,
313 | _ctrl
314 |
315 | var log = function() {
316 | if (Scene._log) {
317 | // not available, when main source minified
318 | Array.prototype.splice.call(arguments, 1, 0, '(' + NAMESPACE + ')', '->')
319 | Scene._log.apply(this, arguments)
320 | }
321 | }
322 |
323 | options.name = options.name || _autoindex
324 |
325 | // prepare bounds elements
326 | _elemStart.firstChild.textContent += ' ' + options.name
327 | _elemEnd.textContent += ' ' + options.name
328 | _elemBounds.appendChild(_elemStart)
329 | _elemBounds.appendChild(_elemEnd)
330 |
331 | // set public variables
332 | Indicator.options = options
333 | Indicator.bounds = _elemBounds
334 | // will be set later
335 | Indicator.triggerGroup = undefined
336 |
337 | // add indicators to DOM
338 | this.add = function() {
339 | _ctrl = Scene.controller()
340 | _vertical = _ctrl.info('vertical')
341 |
342 | var isDocument = _ctrl.info('isDocument')
343 |
344 | if (!_boundsContainer) {
345 | // no parent supplied or doesnt exist
346 | _boundsContainer = isDocument ? document.body : _ctrl.info('container') // check if window/document (then use body)
347 | }
348 | if (!isDocument && _util.css(_boundsContainer, 'position') === 'static') {
349 | // position mode needed for correct positioning of indicators
350 | _util.css(_boundsContainer, {
351 | position: 'relative',
352 | })
353 | }
354 |
355 | // add listeners for updates
356 | Scene.on('change.plugin_addIndicators', handleTriggerParamsChange)
357 | Scene.on('shift.plugin_addIndicators', handleBoundsParamsChange)
358 |
359 | // updates trigger & bounds (will add elements if needed)
360 | updateTriggerGroup()
361 | updateBounds()
362 |
363 | setTimeout(function() {
364 | // do after all execution is finished otherwise sometimes size calculations are off
365 | _ctrl._indicators.updateBoundsPositions(Indicator)
366 | }, 0)
367 | //@ts-ignore
368 | log(3, 'added indicators')
369 | }
370 |
371 | // remove indicators from DOM
372 | this.remove = function() {
373 | if (Indicator.triggerGroup) {
374 | // if not set there's nothing to remove
375 | Scene.off('change.plugin_addIndicators', handleTriggerParamsChange)
376 | Scene.off('shift.plugin_addIndicators', handleBoundsParamsChange)
377 |
378 | if (Indicator.triggerGroup.members.length > 1) {
379 | // just remove from memberlist of old group
380 | var group = Indicator.triggerGroup
381 | group.members.splice(group.members.indexOf(Indicator), 1)
382 | _ctrl._indicators.updateTriggerGroupLabel(group)
383 | _ctrl._indicators.updateTriggerGroupPositions(group)
384 | Indicator.triggerGroup = undefined
385 | } else {
386 | // remove complete group
387 | removeTriggerGroup()
388 | }
389 | removeBounds()
390 | //@ts-ignore
391 | log(3, 'removed indicators')
392 | }
393 | }
394 |
395 | /*
396 | * ----------------------------------------------------------------
397 | * internal Event Handlers
398 | * ----------------------------------------------------------------
399 | */
400 |
401 | // event handler for when bounds params change
402 | var handleBoundsParamsChange = function() {
403 | updateBounds()
404 | }
405 |
406 | // event handler for when trigger params change
407 | var handleTriggerParamsChange = function(e) {
408 | if (e.what === 'triggerHook') {
409 | updateTriggerGroup()
410 | }
411 | }
412 |
413 | /*
414 | * ----------------------------------------------------------------
415 | * Bounds (start / stop) management
416 | * ----------------------------------------------------------------
417 | */
418 |
419 | // adds an new bounds elements to the array and to the DOM
420 | var addBounds = function() {
421 | var v = _ctrl.info('vertical')
422 | // apply stuff we didn't know before...
423 | _util.css(_elemStart.firstChild, {
424 | 'border-bottom-width': v ? 1 : 0,
425 | 'border-right-width': v ? 0 : 1,
426 | bottom: v ? -1 : options.indent,
427 | right: v ? options.indent : -1,
428 | padding: v ? '0 8px' : '2px 4px',
429 | })
430 | _util.css(_elemEnd, {
431 | 'border-top-width': v ? 1 : 0,
432 | 'border-left-width': v ? 0 : 1,
433 | top: v ? '100%' : '',
434 | right: v ? options.indent : '',
435 | bottom: v ? '' : options.indent,
436 | left: v ? '' : '100%',
437 | padding: v ? '0 8px' : '2px 4px',
438 | })
439 | // append
440 | _boundsContainer.appendChild(_elemBounds)
441 | }
442 |
443 | // remove bounds from list and DOM
444 | var removeBounds = function() {
445 | _elemBounds.parentNode.removeChild(_elemBounds)
446 | }
447 |
448 | // update the start and end positions of the scene
449 | var updateBounds = function() {
450 | if (_elemBounds.parentNode !== _boundsContainer) {
451 | addBounds() // Add Bounds elements (start/end)
452 | }
453 | var css = {}
454 | css[_vertical ? 'top' : 'left'] = Scene.triggerPosition()
455 | css[_vertical ? 'height' : 'width'] = Scene.duration()
456 | _util.css(_elemBounds, css)
457 | _util.css(_elemEnd, {
458 | display: Scene.duration() > 0 ? '' : 'none',
459 | })
460 | }
461 |
462 | /*
463 | * ----------------------------------------------------------------
464 | * trigger and trigger group management
465 | * ----------------------------------------------------------------
466 | */
467 |
468 | // adds an new trigger group to the array and to the DOM
469 | var addTriggerGroup = function() {
470 | var triggerElem = TPL.trigger(options.colorTrigger) // new trigger element
471 | var css = {}
472 | css[_vertical ? 'right' : 'bottom'] = 0
473 | css[_vertical ? 'border-top-width' : 'border-left-width'] = 1
474 | _util.css(triggerElem.firstChild, css)
475 | _util.css(triggerElem.firstChild.firstChild, {
476 | padding: _vertical ? '0 8px 3px 8px' : '3px 4px',
477 | })
478 | document.body.appendChild(triggerElem) // directly add to body
479 | var newGroup = {
480 | triggerHook: Scene.triggerHook(),
481 | element: triggerElem,
482 | members: [Indicator],
483 | }
484 | _ctrl._indicators.groups.push(newGroup)
485 | Indicator.triggerGroup = newGroup
486 | // update right away
487 | _ctrl._indicators.updateTriggerGroupLabel(newGroup)
488 | _ctrl._indicators.updateTriggerGroupPositions(newGroup)
489 | }
490 |
491 | var removeTriggerGroup = function() {
492 | _ctrl._indicators.groups.splice(_ctrl._indicators.groups.indexOf(Indicator.triggerGroup), 1)
493 | Indicator.triggerGroup.element.parentNode.removeChild(Indicator.triggerGroup.element)
494 | Indicator.triggerGroup = undefined
495 | }
496 |
497 | // updates the trigger group -> either join existing or add new one
498 | /*
499 | * Logic:
500 | * 1 if a trigger group exist, check if it's in sync with Scene settings – if so, nothing else needs to happen
501 | * 2 try to find an existing one that matches Scene parameters
502 | * 2.1 If a match is found check if already assigned to an existing group
503 | * If so:
504 | * A: it was the last member of existing group -> kill whole group
505 | * B: the existing group has other members -> just remove from member list
506 | * 2.2 Assign to matching group
507 | * 3 if no new match could be found, check if assigned to existing group
508 | * A: yes, and it's the only member -> just update parameters and positions and keep using this group
509 | * B: yes but there are other members -> remove from member list and create a new one
510 | * C: no, so create a new one
511 | */
512 | var updateTriggerGroup = function() {
513 | var triggerHook = Scene.triggerHook(),
514 | closeEnough = 0.0001
515 |
516 | // Have a group, check if it still matches
517 | if (Indicator.triggerGroup) {
518 | if (Math.abs(Indicator.triggerGroup.triggerHook - triggerHook) < closeEnough) {
519 | // _util.log(0, "trigger", options.name, "->", "no need to change, still in sync");
520 | return // all good
521 | }
522 | }
523 | // Don't have a group, check if a matching one exists
524 | // _util.log(0, "trigger", options.name, "->", "out of sync!");
525 | var groups = _ctrl._indicators.groups,
526 | group,
527 | i = groups.length
528 | while (i--) {
529 | group = groups[i]
530 | if (Math.abs(group.triggerHook - triggerHook) < closeEnough) {
531 | // found a match!
532 | // _util.log(0, "trigger", options.name, "->", "found match");
533 | if (Indicator.triggerGroup) {
534 | // do I have an old group that is out of sync?
535 | if (Indicator.triggerGroup.members.length === 1) {
536 | // is it the only remaining group?
537 | // _util.log(0, "trigger", options.name, "->", "kill");
538 | // was the last member, remove the whole group
539 | removeTriggerGroup()
540 | } else {
541 | Indicator.triggerGroup.members.splice(Indicator.triggerGroup.members.indexOf(Indicator), 1) // just remove from memberlist of old group
542 | _ctrl._indicators.updateTriggerGroupLabel(Indicator.triggerGroup)
543 | _ctrl._indicators.updateTriggerGroupPositions(Indicator.triggerGroup)
544 | // _util.log(0, "trigger", options.name, "->", "removing from previous member list");
545 | }
546 | }
547 | // join new group
548 | group.members.push(Indicator)
549 | Indicator.triggerGroup = group
550 | _ctrl._indicators.updateTriggerGroupLabel(group)
551 | return
552 | }
553 | }
554 |
555 | // at this point I am obviously out of sync and don't match any other group
556 | if (Indicator.triggerGroup) {
557 | if (Indicator.triggerGroup.members.length === 1) {
558 | // _util.log(0, "trigger", options.name, "->", "updating existing");
559 | // out of sync but i'm the only member => just change and update
560 | Indicator.triggerGroup.triggerHook = triggerHook
561 | _ctrl._indicators.updateTriggerGroupPositions(Indicator.triggerGroup)
562 | return
563 | } else {
564 | // _util.log(0, "trigger", options.name, "->", "removing from previous member list");
565 | Indicator.triggerGroup.members.splice(Indicator.triggerGroup.members.indexOf(Indicator), 1) // just remove from memberlist of old group
566 | _ctrl._indicators.updateTriggerGroupLabel(Indicator.triggerGroup)
567 | _ctrl._indicators.updateTriggerGroupPositions(Indicator.triggerGroup)
568 | Indicator.triggerGroup = undefined // need a brand new group...
569 | }
570 | }
571 | // _util.log(0, "trigger", options.name, "->", "add a new one");
572 | // did not find any match, make new trigger group
573 | addTriggerGroup()
574 | }
575 | }
576 |
577 | /*
578 | * ----------------------------------------------------------------
579 | * Templates for the indicators
580 | * ----------------------------------------------------------------
581 | */
582 | var TPL = {
583 | start: function(color) {
584 | // inner element (for bottom offset -1, while keeping top position 0)
585 | var inner = document.createElement('div')
586 | inner.textContent = 'start'
587 | _util.css(inner, {
588 | position: 'absolute',
589 | overflow: 'visible',
590 | 'border-width': 0,
591 | 'border-style': 'solid',
592 | color: color,
593 | 'border-color': color,
594 | })
595 | var e = document.createElement('div')
596 | // wrapper
597 | _util.css(e, {
598 | position: 'absolute',
599 | overflow: 'visible',
600 | width: 0,
601 | height: 0,
602 | })
603 | e.appendChild(inner)
604 | return e
605 | },
606 | end: function(color) {
607 | var e = document.createElement('div')
608 | e.textContent = 'end'
609 | _util.css(e, {
610 | position: 'absolute',
611 | overflow: 'visible',
612 | 'border-width': 0,
613 | 'border-style': 'solid',
614 | color: color,
615 | 'border-color': color,
616 | })
617 | return e
618 | },
619 | bounds: function() {
620 | var e = document.createElement('div')
621 | _util.css(e, {
622 | position: 'absolute',
623 | overflow: 'visible',
624 | 'white-space': 'nowrap',
625 | 'pointer-events': 'none',
626 | 'font-size': FONT_SIZE,
627 | })
628 | e.style.zIndex = ZINDEX
629 | return e
630 | },
631 | trigger: function(color) {
632 | // inner to be above or below line but keep position
633 | var inner = document.createElement('div')
634 | inner.textContent = 'trigger'
635 | _util.css(inner, {
636 | position: 'relative',
637 | })
638 | // inner wrapper for right: 0 and main element has no size
639 | var w = document.createElement('div')
640 | _util.css(w, {
641 | position: 'absolute',
642 | overflow: 'visible',
643 | 'border-width': 0,
644 | 'border-style': 'solid',
645 | color: color,
646 | 'border-color': color,
647 | })
648 | w.appendChild(inner)
649 | // wrapper
650 | var e = document.createElement('div')
651 | _util.css(e, {
652 | position: 'fixed',
653 | overflow: 'visible',
654 | 'white-space': 'nowrap',
655 | 'pointer-events': 'none',
656 | 'font-size': FONT_SIZE,
657 | })
658 | e.style.zIndex = ZINDEX
659 | e.appendChild(w)
660 | return e
661 | },
662 | }
663 |
--------------------------------------------------------------------------------
/src/scrollmagic-add-indicators.ts:
--------------------------------------------------------------------------------
1 | var detect = require('is-client')
2 | var addindicators = undefined
3 | if (detect()) {
4 | addindicators = require('./plugins/debug.addIndicators')
5 | }
6 |
7 | export default addindicators
8 |
--------------------------------------------------------------------------------
/src/scrollmagic-with-ssr.ts:
--------------------------------------------------------------------------------
1 | /* https://github.com/epranka/scrollmagic-with-ssr/blob/master/index.js */
2 | var detect = require('is-client')
3 | var scrollmagic = undefined
4 | if (detect()) {
5 | scrollmagic = require('scrollmagic')
6 | }
7 |
8 | export default scrollmagic
9 |
--------------------------------------------------------------------------------
/src/stories/Stylesheet.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export const Stylesheet = () => (
4 |
83 | )
84 |
--------------------------------------------------------------------------------
/src/stories/story.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import { ScrollScene } from '../ScrollScene'
4 | import { ScrollObserver } from '../ScrollObserver'
5 | import { gsap } from 'gsap'
6 | import { addIndicators } from '../index'
7 | import { Stylesheet } from './Stylesheet'
8 |
9 | import notes from '../../README.md'
10 |
11 | storiesOf('ScrollScene|toggle (className)', module)
12 | .add(
13 | 'triggerHook: 0',
14 | () => {
15 | // init ref
16 | const containerRef = React.useRef(null)
17 | const triggerRef = React.useRef(null)
18 |
19 | React.useEffect(() => {
20 | const { current: containerElement } = containerRef
21 | const { current: triggerElement } = triggerRef
22 |
23 | if (!containerElement && !triggerElement) {
24 | return undefined
25 | }
26 |
27 | const scrollScene = new ScrollScene({
28 | triggerElement,
29 | toggle: {
30 | element: containerElement,
31 | className: 'turn-blue',
32 | },
33 | triggerHook: 0,
34 | })
35 |
36 | scrollScene.Scene.addIndicators({ name: 'pin scene', colorEnd: '#FFFFFF' })
37 |
38 | return () => {
39 | scrollScene.destroy()
40 | }
41 | })
42 |
43 | return (
44 |
45 |
46 |
47 |
triggerHook: 0
48 |
Scroll Down
49 |
50 |
51 |
52 |
When this hits the top the page will turn blue
53 |
54 |
55 |
56 | )
57 | },
58 | {
59 | notes: { markdown: notes },
60 | },
61 | )
62 | .add(
63 | 'triggerHook: 0.5',
64 | () => {
65 | // init ref
66 | const containerRef = React.useRef(null)
67 | const triggerRef = React.useRef(null)
68 |
69 | React.useEffect(() => {
70 | const { current: containerElement } = containerRef
71 | const { current: triggerElement } = triggerRef
72 |
73 | if (!containerElement && !triggerElement) {
74 | return undefined
75 | }
76 |
77 | const scrollScene = new ScrollScene({
78 | triggerElement,
79 | toggle: {
80 | element: containerElement,
81 | className: 'turn-blue',
82 | },
83 | triggerHook: 0.5,
84 | })
85 |
86 | scrollScene.Scene.addIndicators({ name: 'pin scene', colorEnd: '#FFFFFF' })
87 |
88 | return () => {
89 | scrollScene.destroy()
90 | }
91 | })
92 |
93 | return (
94 |
95 |
96 |
97 |
triggerHook: 0.5
98 |
Scroll Down
99 |
100 |
101 |
102 |
When this the middle of the page the page will turn blue
103 |
104 |
105 |
106 | )
107 | },
108 | {
109 | notes: { markdown: notes },
110 | },
111 | )
112 | .add(
113 | 'triggerHook: 1',
114 | () => {
115 | // init ref
116 | const containerRef = React.useRef(null)
117 | const triggerRef = React.useRef(null)
118 |
119 | React.useEffect(() => {
120 | const { current: containerElement } = containerRef
121 | const { current: triggerElement } = triggerRef
122 |
123 | if (!containerElement && !triggerElement) {
124 | return undefined
125 | }
126 |
127 | const scrollScene = new ScrollScene({
128 | triggerElement,
129 | toggle: {
130 | element: containerElement,
131 | className: 'turn-blue',
132 | },
133 | triggerHook: 1,
134 | })
135 |
136 | return () => {
137 | scrollScene.destroy()
138 | }
139 | })
140 |
141 | return (
142 |
143 |
144 |
145 |
triggerHook: 1
146 |
Scroll Down
147 |
148 |
149 |
150 |
151 | When this appears on page the page will turn blue
152 |
153 |
154 |
155 |
156 | )
157 | },
158 | {
159 | notes: { markdown: notes },
160 | },
161 | )
162 | .add(
163 | 'duration: "100%", reverse: true',
164 | () => {
165 | // init ref
166 | const containerRef = React.useRef(null)
167 | const triggerRef = React.useRef(null)
168 |
169 | React.useEffect(() => {
170 | const { current: containerElement } = containerRef
171 | const { current: triggerElement } = triggerRef
172 |
173 | if (!containerElement && !triggerElement) {
174 | return undefined
175 | }
176 |
177 | const scrollScene = new ScrollScene({
178 | triggerElement,
179 | toggle: {
180 | element: containerElement,
181 | className: 'turn-blue',
182 | reverse: true,
183 | },
184 | triggerHook: 1,
185 | duration: '100%',
186 | })
187 |
188 | return () => {
189 | scrollScene.destroy()
190 | }
191 | })
192 |
193 | return (
194 |
195 |
196 |
197 |
duration: "100%", reverse: true
198 |
Scroll Down
199 |
200 |
201 |
202 |
203 | When this hits the top of the page the page will turn blue and it will last for 100% of the viewport height
204 |
205 |
206 |
207 |
208 |
209 | When this hits the top of the page the page will reverse the scene
210 |
211 |
212 |
213 |
214 | )
215 | },
216 | {
217 | notes: { markdown: notes },
218 | },
219 | )
220 | .add(
221 | 'offset: 100, triggerHook: 0.5',
222 | () => {
223 | // init ref
224 | const containerRef = React.useRef(null)
225 | const triggerRef = React.useRef(null)
226 |
227 | React.useEffect(() => {
228 | const { current: containerElement } = containerRef
229 | const { current: triggerElement } = triggerRef
230 |
231 | if (!containerElement && !triggerElement) {
232 | return undefined
233 | }
234 |
235 | const scrollScene = new ScrollScene({
236 | controller: {
237 | loglevel: 3,
238 | },
239 | triggerElement: triggerElement,
240 | toggle: {
241 | element: containerElement,
242 | className: 'turn-blue',
243 | },
244 | offset: 100,
245 | triggerHook: 0.5,
246 | })
247 |
248 | scrollScene.Scene.addIndicators({ name: 'pin scene', colorEnd: '#FFFFFF' })
249 |
250 | return () => {
251 | scrollScene.destroy()
252 | }
253 | })
254 |
255 | return (
256 |
257 |
258 |
259 |
offset: 100, triggerHook: 0.5
260 |
Scroll Down
261 |
262 |
263 |
264 |
When this hits the middle of the page plus 100 pixels, the page with turn blue.
265 |
266 |
267 |
268 |
offset: 100, triggerHook: 0.5,
269 |
270 |
271 |
272 |
273 |
274 | )
275 | },
276 | {
277 | notes: { markdown: notes },
278 | },
279 | )
280 |
281 | storiesOf('ScrollScene|events', module)
282 | .add(
283 | 'on enter, on leave',
284 | () => {
285 | // init ref
286 | const containerRef = React.useRef(null)
287 | const triggerRef = React.useRef(null)
288 |
289 | React.useEffect(() => {
290 | const { current: containerElement } = containerRef
291 | const { current: triggerElement } = triggerRef
292 |
293 | if (!containerElement && !triggerElement) {
294 | return undefined
295 | }
296 |
297 | const scrollScene = new ScrollScene({
298 | triggerElement,
299 | toggle: {
300 | element: containerElement,
301 | className: 'turn-blue',
302 | reverse: true,
303 | },
304 | triggerHook: 1,
305 | duration: '100%',
306 | })
307 |
308 | scrollScene.Scene.on('enter', function(event) {
309 | console.log('Scene entered.')
310 | })
311 |
312 | scrollScene.Scene.on('leave', function(event) {
313 | console.log('Scene left.')
314 | })
315 |
316 | return () => {
317 | scrollScene.destroy()
318 | }
319 | })
320 |
321 | return (
322 |
323 |
324 |
325 |
{`scrollScene.Scene.on('enter', function(event) {console.log('Scene entered.')})`.toString()}
326 |
{`scrollScene.Scene.on('leave', function(event) {console.log('Scene left.')})`.toString()}
327 |
Scroll Down
328 |
329 |
330 |
331 |
When this hits the top of the page, console.log('Scene entered.')
332 |
333 |
334 |
335 |
336 | When this hits the top of the page, console.log("Scene left.");
337 |
338 |
339 |
340 |
341 | )
342 | },
343 | {
344 | notes: { markdown: notes },
345 | },
346 | )
347 | .add(
348 | 'on progress',
349 | () => {
350 | // init ref
351 | const containerRef = React.useRef(null)
352 | const triggerRef = React.useRef(null)
353 |
354 | React.useEffect(() => {
355 | const { current: containerElement } = containerRef
356 | const { current: triggerElement } = triggerRef
357 |
358 | if (!containerElement && !triggerElement) {
359 | return undefined
360 | }
361 |
362 | const scrollScene = new ScrollScene({
363 | triggerElement,
364 | toggle: {
365 | element: containerElement,
366 | className: 'turn-blue',
367 | reverse: true,
368 | },
369 | triggerHook: 1,
370 | duration: '100%',
371 | })
372 |
373 | scrollScene.Scene.on('progress', function(event) {
374 | console.log('Scene progress changed to ' + event.progress)
375 | })
376 |
377 | return () => {
378 | scrollScene.destroy()
379 | }
380 | })
381 |
382 | return (
383 |
384 |
385 |
386 |
387 | {`scrollScene.Scene.on('progress', function(event) {
388 | console.log('Scene progress changed to ' + event.progress)
389 | })`.toString()}
390 |
391 |
Scroll Down
392 |
393 |
394 |
395 |
396 | When this hits the top of the page, console.log the progress duration the active scene
397 |
398 |
399 |
400 |
401 |
402 | When this hits the top of the page, the progress will stop console logging
403 |
404 |
405 |
406 |
407 | )
408 | },
409 | {
410 | notes: { markdown: notes },
411 | },
412 | )
413 |
414 | storiesOf('ScrollScene|gsap', module)
415 | .add(
416 | 'Basic Example',
417 | () => {
418 | // init ref
419 | const containerRef = React.useRef(null)
420 | const triggerRef = React.useRef(null)
421 | const squareRef = React.useRef(null)
422 |
423 | React.useEffect(() => {
424 | const { current: containerElement } = containerRef
425 | const { current: triggerElement } = triggerRef
426 | const { current: squareElement } = squareRef
427 |
428 | if (!containerElement && !triggerElement && !squareElement) {
429 | return undefined
430 | }
431 |
432 | // create a timeline and add a tween
433 | const tl = gsap.timeline({ paused: true })
434 |
435 | tl.to(squareElement, {
436 | x: -200,
437 | duration: 1,
438 | ease: 'power2.out',
439 | })
440 |
441 | const scrollScene = new ScrollScene({
442 | triggerElement,
443 | gsap: {
444 | timeline: tl,
445 | },
446 | triggerHook: 0,
447 | })
448 |
449 | return () => {
450 | scrollScene.destroy()
451 | }
452 | })
453 |
454 | return (
455 |
456 |
457 |
458 |
Basic Example
459 |
Scroll Down
460 |
461 |
462 |
463 |
464 | When this hits the top the page the container will move 20%, and scrolling back up will reverse it
465 | automatically
466 |
467 |
468 |
472 |
473 |
474 |
475 | )
476 | },
477 | {
478 | notes: { markdown: notes },
479 | },
480 | )
481 | .add(
482 | 'reverseSpeed: 4, triggerHook: 0',
483 | () => {
484 | // init ref
485 | const containerRef = React.useRef(null)
486 | const triggerRef = React.useRef(null)
487 | const squareRef = React.useRef(null)
488 |
489 | React.useEffect(() => {
490 | const { current: containerElement } = containerRef
491 | const { current: triggerElement } = triggerRef
492 | const { current: squareElement } = squareRef
493 |
494 | if (!containerElement && !triggerElement && !squareElement) {
495 | return undefined
496 | }
497 |
498 | // create a timeline and add a tween
499 | const tl = gsap.timeline({ paused: true })
500 |
501 | tl.to(squareElement, {
502 | x: -200,
503 | duration: 1,
504 | ease: 'power2.out',
505 | })
506 |
507 | const scrollScene = new ScrollScene({
508 | triggerElement,
509 | gsap: {
510 | timeline: tl,
511 | reverseSpeed: 4,
512 | },
513 | triggerHook: 0,
514 | })
515 |
516 | return () => {
517 | scrollScene.destroy()
518 | }
519 | })
520 |
521 | return (
522 |
523 |
524 |
525 |
reverseSpeed: 4, triggerHook: 0
526 |
Scroll Down
527 |
528 |
529 |
530 |
531 | When this hits the top the page the container will move 20%, and scrolling back up will reverse it 4 times
532 | faster
533 |
534 |
535 |
539 |
540 |
541 |
542 | )
543 | },
544 | {
545 | notes: { markdown: notes },
546 | },
547 | )
548 | .add(
549 | 'forwardSpeed: 2, reverseSpeed: 0.5',
550 | () => {
551 | // init ref
552 | const containerRef = React.useRef(null)
553 | const triggerRef = React.useRef(null)
554 | const squareRef = React.useRef(null)
555 |
556 | React.useEffect(() => {
557 | const { current: containerElement } = containerRef
558 | const { current: triggerElement } = triggerRef
559 | const { current: squareElement } = squareRef
560 |
561 | if (!containerElement && !triggerElement && !squareElement) {
562 | return undefined
563 | }
564 |
565 | // create a timeline and add a tween
566 | const tl = gsap.timeline({ paused: true })
567 |
568 | tl.to(squareElement, {
569 | x: -200,
570 | duration: 1,
571 | ease: 'power2.out',
572 | })
573 |
574 | const scrollScene = new ScrollScene({
575 | triggerElement,
576 | gsap: {
577 | timeline: tl,
578 | forwardSpeed: 2,
579 | reverseSpeed: 0.5,
580 | },
581 | triggerHook: 0,
582 | })
583 |
584 | return () => {
585 | scrollScene.destroy()
586 | }
587 | })
588 |
589 | return (
590 |
591 |
592 |
593 |
forwardSpeed: 2, reverseSpeed: 1
594 |
Scroll Down
595 |
596 |
597 |
598 |
599 | When this hits the top the page the container will move 20% and 2 times faster than normal, and scrolling
600 | back up will reverse it half it's normal speed
601 |
602 |
603 |
607 |
608 |
609 |
610 | )
611 | },
612 | {
613 | notes: { markdown: notes },
614 | },
615 | )
616 | .add(
617 | 'duration: 500',
618 | () => {
619 | // init ref
620 | const containerRef = React.useRef(null)
621 | const triggerRef = React.useRef(null)
622 | const squareRef = React.useRef(null)
623 |
624 | React.useEffect(() => {
625 | const { current: containerElement } = containerRef
626 | const { current: triggerElement } = triggerRef
627 | const { current: squareElement } = squareRef
628 |
629 | if (!containerElement && !triggerElement && !squareElement) {
630 | return undefined
631 | }
632 |
633 | // create a timeline and add a tween
634 | const tl = gsap.timeline({ paused: true })
635 |
636 | tl.to(squareElement, {
637 | x: -200,
638 | duration: 1,
639 | ease: 'none',
640 | })
641 |
642 | const scrollScene = new ScrollScene({
643 | triggerElement,
644 | gsap: {
645 | timeline: tl,
646 | },
647 | duration: 500,
648 | triggerHook: 0,
649 | })
650 |
651 | return () => {
652 | scrollScene.destroy()
653 | }
654 | })
655 |
656 | return (
657 |
658 |
659 |
660 |
duration: 500
661 |
Scroll Down
662 |
663 |
664 |
665 |
666 | When this hits the top the page the container will move 20% but attached to a 500px duration
667 |
668 |
669 |
673 |
674 |
675 |
676 | )
677 | },
678 | {
679 | notes: { markdown: notes },
680 | },
681 | )
682 |
683 | storiesOf('ScrollScene|duration', module).add(
684 | 'duration: domNode',
685 | () => {
686 | // init ref
687 | const containerRef = React.useRef(null)
688 | const triggerRef = React.useRef(null)
689 |
690 | React.useEffect(() => {
691 | const { current: containerElement } = containerRef
692 | const { current: triggerElement } = triggerRef
693 |
694 | if (!containerElement && !triggerElement) {
695 | return undefined
696 | }
697 |
698 | const scrollScene = new ScrollScene({
699 | triggerElement,
700 | toggle: {
701 | element: containerElement,
702 | className: 'turn-blue',
703 | reverse: true,
704 | },
705 | triggerHook: 0.25,
706 | duration: triggerElement,
707 | })
708 |
709 | scrollScene.Scene.addIndicators({ name: 'duration: domNode', colorEnd: '#FFFFFF' })
710 |
711 | return () => {
712 | scrollScene.destroy()
713 | }
714 | })
715 |
716 | return (
717 |
718 |
719 |
720 |
triggerHook: 0
721 |
Scroll Down
722 |
723 |
724 |
725 |
726 | When this hits the top the page will turn blue
727 |
728 |
729 |
730 |
731 | )
732 | },
733 | {
734 | notes: { markdown: notes },
735 | },
736 | )
737 |
738 | storiesOf('ScrollScene|controller', module).add(
739 | 'add options',
740 | () => {
741 | // init ref
742 | const containerRef = React.useRef(null)
743 | const triggerRef = React.useRef(null)
744 |
745 | React.useEffect(() => {
746 | const { current: containerElement } = containerRef
747 | const { current: triggerElement } = triggerRef
748 |
749 | if (!containerElement && !triggerElement) {
750 | return undefined
751 | }
752 |
753 | const scrollScene = new ScrollScene({
754 | triggerElement,
755 | toggle: {
756 | element: containerElement,
757 | className: 'turn-blue',
758 | reverse: true,
759 | },
760 | triggerHook: 1,
761 | duration: '100%',
762 | controller: { loglevel: 3 },
763 | useGlobalController: false,
764 | })
765 |
766 | console.log(scrollScene.Controller.info())
767 |
768 | return () => {
769 | scrollScene.destroy()
770 | }
771 | })
772 |
773 | return (
774 |
775 |
776 |
777 |
{`controller: { loglevel: 3 } will log all the goods`.toString()}
778 |
Scroll and refresh page after leaving.
779 |
Otherwise you'll get annoyed with the amount of console.logging
780 |
781 |
782 |
783 |
Scroll and the console.logging will begin.
784 |
This page is using a local controller. Refresh page after leaving.
785 |
786 |
787 |
788 | )
789 | },
790 | {
791 | notes: { markdown: notes },
792 | },
793 | )
794 |
795 | storiesOf('ScrollObserver|toggle (className)', module).add(
796 | 'Basic Example',
797 | () => {
798 | // init ref
799 | const containerRef = React.useRef(null)
800 | const triggerRef = React.useRef(null)
801 |
802 | React.useEffect(() => {
803 | const { current: containerElement } = containerRef
804 | const { current: triggerElement } = triggerRef
805 |
806 | if (!containerElement && !triggerElement) {
807 | return undefined
808 | }
809 |
810 | const scrollObserver = new ScrollObserver({
811 | triggerElement,
812 | toggle: {
813 | element: containerElement,
814 | className: 'turn-blue',
815 | },
816 | })
817 |
818 | return () => {
819 | scrollObserver.destroy()
820 | }
821 | })
822 |
823 | return (
824 |
825 |
826 |
827 |
Basic Example
828 |
Scroll Down
829 |
830 |
831 |
832 |
833 | While this is visible on the page the page will turn blue
834 |
835 |
836 |
837 |
838 | )
839 | },
840 | {
841 | notes: { markdown: notes },
842 | },
843 | )
844 |
845 | storiesOf('ScrollObserver|gsap', module)
846 | .add(
847 | 'Basic Example',
848 | () => {
849 | // init ref
850 | const containerRef = React.useRef(null)
851 | const triggerRef = React.useRef(null)
852 | const squareRef = React.useRef(null)
853 |
854 | React.useEffect(() => {
855 | const { current: containerElement } = containerRef
856 | const { current: triggerElement } = triggerRef
857 | const { current: squareElement } = squareRef
858 |
859 | if (!containerElement && !triggerElement && !squareElement) {
860 | return undefined
861 | }
862 |
863 | // create a timeline and add a tween
864 | const tl = gsap.timeline({ paused: true })
865 |
866 | tl.to(squareElement, {
867 | x: -200,
868 | duration: 1,
869 | ease: 'power2.out',
870 | })
871 |
872 | const scrollObserver = new ScrollObserver({
873 | triggerElement,
874 | gsap: {
875 | timeline: tl,
876 | },
877 | })
878 |
879 | return () => {
880 | scrollObserver.destroy()
881 | }
882 | })
883 |
884 | return (
885 |
886 |
887 |
888 |
Basic Example
889 |
Scroll Down
890 |
891 |
892 |
893 |
While this is visible on the page the container will move 20%
894 |
895 |
899 |
900 |
901 |
902 | )
903 | },
904 | {
905 | notes: { markdown: notes },
906 | },
907 | )
908 | .add(
909 | 'offset: "-10%"',
910 | () => {
911 | // init ref
912 | const containerRef = React.useRef(null)
913 | const triggerRef = React.useRef(null)
914 | const squareRef = React.useRef(null)
915 |
916 | React.useEffect(() => {
917 | const { current: containerElement } = containerRef
918 | const { current: triggerElement } = triggerRef
919 | const { current: squareElement } = squareRef
920 |
921 | if (!containerElement && !triggerElement && !squareElement) {
922 | return undefined
923 | }
924 |
925 | // create a timeline and add a tween
926 | const tl = gsap.timeline({ paused: true })
927 |
928 | tl.to(squareElement, {
929 | x: -200,
930 | duration: 1,
931 | ease: 'power2.out',
932 | })
933 |
934 | const scrollObserver = new ScrollObserver({
935 | triggerElement,
936 | gsap: {
937 | timeline: tl,
938 | },
939 | offset: '-10%',
940 | })
941 |
942 | return () => {
943 | scrollObserver.destroy()
944 | }
945 | })
946 |
947 | return (
948 |
949 |
950 |
951 |
offset: '-10%',
952 |
Scroll Down
953 |
954 |
955 |
956 |
957 | While this container is visible on the page the container will move 20%, but using a -10% offset of the
958 | container
959 |
960 |
961 |
965 |
966 |
967 |
968 | )
969 | },
970 | {
971 | notes: { markdown: notes },
972 | },
973 | )
974 | .add(
975 | 'yoyo: true, delay: 0',
976 | () => {
977 | // init ref
978 | const containerRef = React.useRef(null)
979 | const triggerRef = React.useRef(null)
980 | const squareRef = React.useRef(null)
981 |
982 | React.useEffect(() => {
983 | const { current: containerElement } = containerRef
984 | const { current: triggerElement } = triggerRef
985 | const { current: squareElement } = squareRef
986 |
987 | if (!containerElement && !triggerElement && !squareElement) {
988 | return undefined
989 | }
990 |
991 | // create a timeline and add a tween
992 | const tl = gsap.timeline({ paused: true })
993 |
994 | tl.to(squareElement, {
995 | x: -200,
996 | duration: 1,
997 | ease: 'power2.out',
998 | })
999 |
1000 | const scrollObserver = new ScrollObserver({
1001 | triggerElement,
1002 | gsap: {
1003 | timeline: tl,
1004 | yoyo: true,
1005 | delay: 0,
1006 | },
1007 | })
1008 |
1009 | return () => {
1010 | scrollObserver.destroy()
1011 | }
1012 | })
1013 |
1014 | return (
1015 |
1016 |
1017 |
1018 |
yoyo: true, delay: 1
1019 |
Scroll Down
1020 |
1021 |
1022 |
1023 |
1024 | While this is visible on the page the container will move 20%, and the animation will yoyo with a delay of 0
1025 |
1026 |
1027 |
1031 |
1032 |
1033 |
1034 | )
1035 | },
1036 | {
1037 | notes: { markdown: notes },
1038 | },
1039 | )
1040 | .add(
1041 | 'useDuration: true',
1042 | () => {
1043 | // init ref
1044 | const containerRef = React.useRef(null)
1045 | const triggerRef = React.useRef(null)
1046 | const squareRef = React.useRef(null)
1047 |
1048 | React.useEffect(() => {
1049 | const { current: containerElement } = containerRef
1050 | const { current: triggerElement } = triggerRef
1051 | const { current: squareElement } = squareRef
1052 |
1053 | if (!containerElement && !triggerElement && !squareElement) {
1054 | return undefined
1055 | }
1056 |
1057 | // create a timeline and add a tween
1058 | const tl = gsap.timeline({ paused: true })
1059 |
1060 | tl.to(squareElement, {
1061 | x: -200,
1062 | duration: 1,
1063 | ease: 'none',
1064 | })
1065 |
1066 | const scrollObserver = new ScrollObserver({
1067 | triggerElement,
1068 | gsap: {
1069 | timeline: tl,
1070 | },
1071 | useDuration: true,
1072 | thresholds: 100,
1073 | })
1074 |
1075 | return () => {
1076 | scrollObserver.destroy()
1077 | }
1078 | })
1079 |
1080 | return (
1081 |
1082 |
1083 |
1084 |
useDuration: true
1085 |
Scroll Down
1086 |
1087 |
1088 |
1089 |
1090 | While this container is visible on the page the container will move 20%, but tied to the visibility of the
1091 | container
1092 |
1093 |
1094 |
1098 |
1099 |
1100 |
1101 | )
1102 | },
1103 | {
1104 | notes: { markdown: notes },
1105 | },
1106 | )
1107 |
1108 | storiesOf('ScrollObserver|video', module)
1109 | .add(
1110 | 'whenVisible: "50%"',
1111 | () => {
1112 | // init ref
1113 | const videoRef = React.useRef(null)
1114 |
1115 | React.useEffect(() => {
1116 | const { current: videoElement } = videoRef
1117 |
1118 | if (!videoElement) {
1119 | return undefined
1120 | }
1121 |
1122 | const scrollObserver = new ScrollObserver({
1123 | triggerElement: videoElement,
1124 | video: {
1125 | element: videoElement,
1126 | playingClassName: 'is-playing',
1127 | pausedClassName: 'is-paused',
1128 | },
1129 | whenVisible: '50%',
1130 | })
1131 |
1132 | return () => {
1133 | scrollObserver.destroy()
1134 | }
1135 | })
1136 |
1137 | return (
1138 |
1139 |
1140 |
1141 |
whenVisible: '50%'
1142 |
Scroll Down
1143 |
1144 |
1145 |
1146 |
While the video is 50% visible on the page the video will play, and pause when not 50%
1147 |
1148 |
1159 |
1160 |
1161 |
1162 | )
1163 | },
1164 | {
1165 | notes: { markdown: notes },
1166 | },
1167 | )
1168 | .add(
1169 | 'whenVisible: "80%"',
1170 | () => {
1171 | // init ref
1172 | const videoRef = React.useRef(null)
1173 |
1174 | React.useEffect(() => {
1175 | const { current: videoElement } = videoRef
1176 |
1177 | if (!videoElement) {
1178 | return undefined
1179 | }
1180 |
1181 | const scrollObserver = new ScrollObserver({
1182 | triggerElement: videoElement,
1183 | video: {
1184 | element: videoElement,
1185 | },
1186 | whenVisible: '80%',
1187 | })
1188 |
1189 | return () => {
1190 | scrollObserver.destroy()
1191 | }
1192 | })
1193 |
1194 | return (
1195 |
1196 |
1197 |
1198 |
whenVisible: '80%'
1199 |
Scroll Down
1200 |
1201 |
1202 |
1203 |
While the video is 80% visible on the page the video will play, and pause when not 80%
1204 |
1205 |
1216 |
1217 |
1218 |
1219 | )
1220 | },
1221 | {
1222 | notes: { markdown: notes },
1223 | },
1224 | )
1225 |
1226 | storiesOf('ScrollObserver|callback', module)
1227 | .add(
1228 | 'console.log("active")',
1229 | () => {
1230 | // init ref
1231 | const triggerRef = React.useRef(null)
1232 |
1233 | React.useEffect(() => {
1234 | const { current: triggerElement } = triggerRef
1235 |
1236 | if (!triggerElement) {
1237 | return undefined
1238 | }
1239 |
1240 | const scrollObserver = new ScrollObserver({
1241 | triggerElement,
1242 | callback: {
1243 | active: () => console.log('active'),
1244 | notActive: () => console.log('notActive'),
1245 | },
1246 | })
1247 |
1248 | return () => {
1249 | scrollObserver.destroy()
1250 | }
1251 | })
1252 |
1253 | return (
1254 |
1255 |
1256 |
1257 |
{`callback: {
1258 | active: () => console.log('active'),
1259 | notActive: () => console.log('notActive'),
1260 | }`}
1261 |
1262 |
Scroll Down
1263 |
1264 |
1265 |
1266 |
1267 | While this is visible on the page, the console.log("active") will fire, and while it's not,
1268 | console.log("notActive") will fire
1269 |
1270 |
1271 |
1272 |
1273 |
1274 | )
1275 | },
1276 | {
1277 | notes: { markdown: notes },
1278 | },
1279 | )
1280 | .add(
1281 | 'destroyImmediately: true',
1282 | () => {
1283 | // init ref
1284 | const triggerRef = React.useRef(null)
1285 |
1286 | React.useEffect(() => {
1287 | const { current: triggerElement } = triggerRef
1288 |
1289 | if (!triggerElement) {
1290 | return undefined
1291 | }
1292 |
1293 | const scrollObserver = new ScrollObserver({
1294 | triggerElement,
1295 | callback: {
1296 | active: () => console.log('active'),
1297 | },
1298 | destroyImmediately: true,
1299 | })
1300 |
1301 | return () => {
1302 | scrollObserver.destroy()
1303 | }
1304 | })
1305 |
1306 | return (
1307 |
1308 |
1309 |
1310 |
console.log("hello-world")
1311 |
Scroll Down
1312 |
1313 |
1314 |
1315 |
1316 | While this is visible on the page, the console.log("hello-world") will fire once, and then the scene will
1317 | destory itself
1318 |
1319 |
1320 |
1321 |
1322 |
1323 | )
1324 | },
1325 | {
1326 | notes: { markdown: notes },
1327 | },
1328 | )
1329 | .add(
1330 | 'whenVisible: "50%"',
1331 | () => {
1332 | // init ref
1333 | const triggerRef = React.useRef(null)
1334 |
1335 | React.useEffect(() => {
1336 | const { current: triggerElement } = triggerRef
1337 |
1338 | if (!triggerElement) {
1339 | return undefined
1340 | }
1341 |
1342 | const scrollObserver = new ScrollObserver({
1343 | triggerElement,
1344 | callback: {
1345 | active: () => console.log('active'),
1346 | notActive: () => console.log('notActive'),
1347 | },
1348 | whenVisible: '50%',
1349 | })
1350 |
1351 | return () => {
1352 | scrollObserver.destroy()
1353 | }
1354 | })
1355 |
1356 | return (
1357 |
1358 |
1359 |
1360 |
whenVisible: "50%"
1361 |
Scroll Down
1362 |
1363 |
1364 |
1365 |
1366 | While this is visible on the page, the console.log("hello-world") will fire once, and then the scene will
1367 | destory itself
1368 |
1369 |
1370 |
1371 |
1372 |
1373 | )
1374 | },
1375 | {
1376 | notes: { markdown: notes },
1377 | },
1378 | )
1379 | .add(
1380 | 'multiple triggers',
1381 | () => {
1382 | // init ref
1383 | const triggerRef1 = React.useRef(null)
1384 | const triggerRef2 = React.useRef(null)
1385 |
1386 | React.useEffect(() => {
1387 | const { current: triggerElement1 } = triggerRef1
1388 | const { current: triggerElement2 } = triggerRef2
1389 |
1390 | if (!triggerElement1 && !triggerElement2) {
1391 | return undefined
1392 | }
1393 |
1394 | const scrollObserver1 = new ScrollObserver({
1395 | triggerElement: triggerElement1,
1396 | callback: {
1397 | active: () => console.log('trigger 1 active'),
1398 | notActive: () => console.log('trigger 1 notActive'),
1399 | },
1400 | })
1401 |
1402 | const scrollObserver2 = new ScrollObserver({
1403 | triggerElement: triggerElement2,
1404 | callback: {
1405 | active: () => console.log('trigger 2 active'),
1406 | notActive: () => console.log('trigger 2 notActive'),
1407 | },
1408 | })
1409 |
1410 | return () => {
1411 | scrollObserver1.destroy()
1412 | scrollObserver2.destroy()
1413 | }
1414 | })
1415 |
1416 | return (
1417 |
1418 |
1419 |
1420 |
multiple triggers
1421 |
1422 |
Scroll Down
1423 |
1424 |
1425 |
1426 |
1427 | While this is visible on the page, the console.log("trigger 1 active") will fire, and while it's not,
1428 | console.log("trigger 1 notActive") will fire
1429 |
1430 |
1431 |
1432 | While this is visible on the page, the console.log("trigger 2 active") will fire, and while it's not,
1433 | console.log("trigger 2 notActive") will fire
1434 |
1435 |
1436 |
1437 |
1438 | )
1439 | },
1440 | {
1441 | notes: { markdown: notes },
1442 | },
1443 | )
1444 |
1445 | storiesOf('ScrollObserver|multiple events', module).add(
1446 | 'toggle + video + callback',
1447 | () => {
1448 | // init ref
1449 | const triggerRef = React.useRef(null)
1450 | const containerRef = React.useRef(null)
1451 | const videoRef = React.useRef(null)
1452 |
1453 | React.useEffect(() => {
1454 | const { current: triggerElement } = triggerRef
1455 | const { current: containerElement } = containerRef
1456 | const { current: videoElement } = videoRef
1457 |
1458 | if (!triggerElement) {
1459 | return undefined
1460 | }
1461 |
1462 | const scrollObserver = new ScrollObserver({
1463 | triggerElement,
1464 | toggle: {
1465 | element: containerElement,
1466 | className: 'turn-blue',
1467 | },
1468 | callback: {
1469 | active: () => console.log('active'),
1470 | notActive: () => console.log('notActive'),
1471 | },
1472 | video: {
1473 | element: videoElement,
1474 | },
1475 | })
1476 |
1477 | return () => {
1478 | scrollObserver.destroy()
1479 | }
1480 | })
1481 |
1482 | return (
1483 |
1484 |
1485 |
1486 |
{`callback: {
1487 | active: () => console.log('active'),
1488 | notActive: () => console.log('notActive'),
1489 | }`}
1490 |
1491 |
Scroll Down
1492 |
1493 |
1494 |
1495 |
1496 | While this is visible on the page, the console.log("active") will fire, and while it's not,
1497 | console.log("notActive") will fire
1498 |
1499 |
1510 |
1511 |
1512 |
1513 |
1514 | )
1515 | },
1516 | {
1517 | notes: { markdown: notes },
1518 | },
1519 | )
1520 |
1521 | storiesOf('ScrollScene|Examples', module).add(
1522 | 'mimic pinning',
1523 | () => {
1524 | // init ref
1525 | const screen02trigger = React.useRef(null)
1526 | const scaledBox = React.useRef(null)
1527 | const screen03trigger = React.useRef(null)
1528 | const heroScreen = React.useRef(null)
1529 |
1530 | const [LOADED, SET_LOADED] = React.useState(false)
1531 |
1532 | React.useEffect(() => {
1533 | const scaleBoxTimeline = gsap.timeline({ paused: true })
1534 |
1535 | scaleBoxTimeline.fromTo(
1536 | scaledBox.current,
1537 | {
1538 | scale: 4,
1539 | },
1540 | { scale: 1, ease: 'power4.out' },
1541 | )
1542 |
1543 | const scaleBoxScene = new ScrollScene({
1544 | triggerElement: screen02trigger.current,
1545 | gsap: {
1546 | timeline: scaleBoxTimeline,
1547 | },
1548 | useDuration: true,
1549 | duration: '100%',
1550 | triggerHook: 1,
1551 | })
1552 |
1553 | const moveOutTimeline = gsap.timeline({ paused: true })
1554 |
1555 | moveOutTimeline.fromTo(
1556 | heroScreen.current,
1557 | {
1558 | y: 0,
1559 | },
1560 | { y: '-100%', ease: 'none' },
1561 | )
1562 |
1563 | const moveOutScene = new ScrollScene({
1564 | triggerElement: screen03trigger.current,
1565 | gsap: {
1566 | timeline: moveOutTimeline,
1567 | },
1568 | useDuration: true,
1569 | duration: '100%',
1570 | triggerHook: 1,
1571 | })
1572 |
1573 | SET_LOADED(true)
1574 |
1575 | return () => {
1576 | scaleBoxScene.destroy()
1577 | moveOutScene.destroy()
1578 | }
1579 | })
1580 |
1581 | return (
1582 | <>
1583 |
1584 |
1585 |
1586 |
1587 |
1588 |
Scroll Down
1589 |
1590 | imagine a computer screen here
1591 |
1592 |
1593 |
1594 |
1595 |
1596 |
1597 |
1598 |
1599 | I can't get involved! I've got work to do! It's not that I like the Empire, I hate it, but there's
1600 | nothing I can do about it right now. It's such a long way from here.
1601 |
1602 |
1603 | The Force is strong with this one. I have you now. Remember, a Jedi can feel the Force flowing through
1604 | him. Red Five standing by. I call it luck. What good is a reward if you ain't around to use it?
1605 | Besides, attacking that battle station ain't my idea of courage. It's more like…suicide.
1606 |
1607 |
1608 | Look, I can take you as far as Anchorhead.{' '}
1609 | You can get a transport there to Mos Eisley or wherever you're going. {' '}
1610 | I suggest you try it again, Luke. This time, let go your conscious self and act on instinct.
1611 |
1612 |
You are a part of the Rebel Alliance and a traitor! Take her away!
1613 |
1614 | You are a part of the Rebel Alliance and a traitor! Take her away! I call it luck. Don't underestimate
1615 | the Force. Still, she's got a lot of spirit. I don't know, what do you think?
1616 |
1617 |
1618 | Ye-ha!
1619 | As you wish.
1620 | What?!
1621 |
1622 |
1623 |
I find your lack of faith disturbing.
1624 |
1625 | I'm surprised you had the courage to take the responsibility yourself. As you wish. What?! Look, I can
1626 | take you as far as Anchorhead. You can get a transport there to Mos Eisley or wherever you're going.
1627 |
1628 |
1629 |
1630 | Leave that to me. Send a distress signal, and inform the Senate that all on board were killed.
1631 |
1632 | I suggest you try it again, Luke. This time, let go your conscious self and act on instinct.
1633 | Hey, Luke! May the Force be with you.
1634 |
1635 |
1636 |
1637 | What!? I need your help, Luke. She needs your help. I'm getting too old for this sort of thing. Hokey
1638 | religions and ancient weapons are no match for a good blaster at your side, kid. I can't get involved!
1639 | I've got work to do! It's not that I like the Empire, I hate it, but there's nothing I can do about it
1640 | right now. It's such a long way from here.
1641 |
1642 |
1643 | Still, she's got a lot of spirit. I don't know, what do you think? You don't believe in the Force, do
1644 | you? I want to come with you to Alderaan. There's nothing for me here now. I want to learn the ways of
1645 | the Force and be a Jedi, like my father before me.
1646 |
1647 |
1648 | You don't believe in the Force, do you? You mean it controls your actions? I find your lack of faith
1649 | disturbing. Look, I can take you as far as Anchorhead. You can get a transport there to Mos Eisley or
1650 | wherever you're going.
1651 |
1652 |
1653 | Don't be too proud of this technological terror you've constructed. The ability to destroy a planet is
1654 | insignificant next to the power of the Force. Look, I can take you as far as Anchorhead. You can get a
1655 | transport there to Mos Eisley or wherever you're going.
1656 |
1657 |
1658 | I can't get involved! I've got work to do! It's not that I like the Empire, I hate it, but there's
1659 | nothing I can do about it right now. It's such a long way from here. Don't act so surprised, Your
1660 | Highness. You weren't on any mercy mission this time. Several transmissions were beamed to this ship
1661 | by Rebel spies. I want to know what happened to the plans they sent you.
1662 |
1663 |
1664 | The plans you refer to will soon be back in our hands. What!? I'm trying not to, kid. Leave that to
1665 | me. Send a distress signal, and inform the Senate that all on board were killed.
1666 |
1667 |
1668 | You don't believe in the Force, do you? The more you tighten your grip, Tarkin, the more star systems
1669 | will slip through your fingers. He is here. Remember, a Jedi can feel the Force flowing through him.
1670 |
1671 |
1672 | You don't believe in the Force, do you? I suggest you try it again, Luke. This time, let go your
1673 | conscious self and act on instinct. I suggest you try it again, Luke. This time, let go your conscious
1674 | self and act on instinct.
1675 |
1676 |
1677 | Leave that to me. Send a distress signal, and inform the Senate that all on board were killed. I need
1678 | your help, Luke. She needs your help. I'm getting too old for this sort of thing. No! Alderaan is
1679 | peaceful. We have no weapons. You can't possibly…
1680 |
1681 |
1682 | The more you tighten your grip, Tarkin, the more star systems will slip through your fingers. Hey,
1683 | Luke! May the Force be with you. Leave that to me. Send a distress signal, and inform the Senate that
1684 | all on board were killed.
1685 |
1686 |
1687 | I need your help, Luke. She needs your help. I'm getting too old for this sort of thing. I have traced
1688 | the Rebel spies to her. Now she is my only link to finding their secret base. She must have hidden the
1689 | plans in the escape pod. Send a detachment down to retrieve them, and see to it personally, Commander.
1690 | There'll be no one to stop us this time!
1691 |
1692 |
1693 |
1694 |
1695 |
1696 |
1697 |
1700 |
1704 |
1708 |
1709 |
1710 | >
1711 | )
1712 | },
1713 | {
1714 | notes: { markdown: notes },
1715 | },
1716 | )
1717 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "moduleResolution": "node",
5 | "allowJs": false,
6 | "noEmit": false,
7 | "strict": false,
8 | "isolatedModules": false,
9 | "esModuleInterop": true,
10 | "module": "commonjs",
11 | "outDir": "dist",
12 | "declaration": false,
13 | "checkJs": false,
14 | "removeComments": false,
15 | "lib": ["es5", "es6", "es7", "es2017", "dom"],
16 | "typeRoots": ["node_modules/@types", "./types"],
17 | "sourceMap": true,
18 | "jsx": "react",
19 | "forceConsistentCasingInFileNames": true,
20 | "noImplicitAny": false,
21 | "suppressImplicitAnyIndexErrors": true,
22 | "allowSyntheticDefaultImports": true,
23 | "experimentalDecorators": true,
24 | "rootDir": "./src",
25 | "baseUrl": ".",
26 | "paths": {
27 | "@src": ["./src"],
28 | "@src/*": ["./src/*"]
29 | },
30 | "noUnusedLocals": true,
31 | "noUnusedParameters": true,
32 | "skipLibCheck": true
33 | },
34 | "include": ["src"],
35 | "exclude": ["node_modules/**", "build", "scripts"]
36 | }
37 |
--------------------------------------------------------------------------------
/tsconfig.types.cjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "rootDir": "./src",
6 | "outDir": "dist/commonjs",
7 | "declaration": true,
8 | "declarationMap": false,
9 | "isolatedModules": false,
10 | "noEmit": false,
11 | "allowJs": false,
12 | "emitDeclarationOnly": true
13 | },
14 | "exclude": ["**/*.test.ts"]
15 | }
--------------------------------------------------------------------------------
/tsconfig.types.esm.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "compilerOptions": {
4 | "module": "es6",
5 | "rootDir": "./src",
6 | "outDir": "dist/esm",
7 | "declaration": true,
8 | "declarationMap": false,
9 | "isolatedModules": false,
10 | "noEmit": false,
11 | "allowJs": false,
12 | "emitDeclarationOnly": true
13 | },
14 | "exclude": ["**/*.test.ts"]
15 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": ["tslint:recommended", "tslint-eslint-rules", "tslint-config-prettier"],
4 | "jsRules": {},
5 | "rules": {
6 | "no-empty": false,
7 | "no-console": false,
8 | "no-bitwise": false,
9 | "no-var-requires": false,
10 | "interface-name": false,
11 | "object-literal-sort-keys": false,
12 | "quotemark": [true, "single", "jsx-double", "avoid-escape"],
13 | "jsdoc-format": [true, "check-multiline-start"]
14 | },
15 | "rulesDirectory": []
16 | }
17 |
--------------------------------------------------------------------------------