├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── package.json ├── packages ├── docz │ ├── doczrc.js │ ├── gatsby-node.js │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── Animation.tsx │ │ │ ├── Controls.mdx │ │ │ ├── Reveal.mdx │ │ │ ├── ScrollTrigger.mdx │ │ │ ├── SplitChars.mdx │ │ │ ├── SplitWords.mdx │ │ │ ├── Timeline.mdx │ │ │ ├── Timeline.tsx │ │ │ ├── Tween.mdx │ │ │ └── Tween.tsx │ │ ├── index.mdx │ │ ├── instructions │ │ │ ├── Easing.mdx │ │ │ ├── LowLevelAccess.mdx │ │ │ ├── Migration.mdx │ │ │ └── PlayState.mdx │ │ └── plugins │ │ │ ├── CountPlugin.mdx │ │ │ ├── GSAP.mdx │ │ │ └── SvgDrawPlugin.mdx │ └── tsconfig.json ├── next │ ├── .gitignore │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ └── index.tsx │ └── tsconfig.json ├── playground │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── examples │ │ │ ├── Animation.tsx │ │ │ ├── ManualControl.tsx │ │ │ ├── Reveal.tsx │ │ │ ├── ScrollTrigger.tsx │ │ │ ├── Scroller.tsx │ │ │ ├── Svg.tsx │ │ │ ├── Timeline.tsx │ │ │ ├── Transition.tsx │ │ │ └── Tween.tsx │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ ├── serviceWorker.ts │ │ └── setupTests.ts │ ├── tsconfig.json │ └── yarn.lock └── react-gsap │ ├── .babelrc │ ├── .github │ └── workflows │ │ └── main.yml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── example │ ├── .npmignore │ ├── index.html │ ├── index.tsx │ ├── package.json │ └── tsconfig.json │ ├── package.json │ ├── src │ ├── Provider.tsx │ ├── Timeline.tsx │ ├── Tween.tsx │ ├── helper.ts │ ├── index.tsx │ ├── plugins │ │ ├── PlugInCount.ts │ │ └── PlugInSvgDraw.ts │ ├── tools │ │ ├── Controls.tsx │ │ ├── Reveal.tsx │ │ ├── ScrollTrigger.tsx │ │ ├── Scroller.tsx │ │ └── SplitText.tsx │ └── types.ts │ ├── test │ └── blah.test.tsx │ └── tsconfig.json └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitworking/react-gsap/b6180f5a3ccd243611c8e891c8932dbf3b1d1d87/.eslintrc -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | .pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # builds 13 | build 14 | dist 15 | .rpt2_cache 16 | 17 | # misc 18 | .DS_Store 19 | .env 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | .eslintcache 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | .docz 31 | 32 | # editors 33 | /.vscode 34 | /.idea 35 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.json 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "trailingComma": "es5", 5 | "semi": true, 6 | "singleQuote": true, 7 | "endOfLine": "lf" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Plugin License Information: 2 | 3 | The Greensock Animation Platform (GSAP) is used per its "No Charge" license. 4 | https://greensock.com/standard-license 5 | 6 | For some cases the extended "Business Green" license will be necessary. 7 | https://greensock.com/club 8 | 9 | 10 | MIT License 11 | 12 | Copyright (c) 2020 Jan Fischer 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a copy 15 | of this software and associated documentation files (the "Software"), to deal 16 | in the Software without restriction, including without limitation the rights 17 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | copies of the Software, and to permit persons to whom the Software is 19 | furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in all 22 | copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30 | SOFTWARE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-gsap 2 | 3 | > React components for GSAP 4 | 5 | [![NPM](https://img.shields.io/npm/v/react-gsap.svg)](https://www.npmjs.com/package/react-gsap) 6 | ![npm type definitions](https://img.shields.io/npm/types/react-gsap) 7 | ![npm bundle size (version)](https://img.shields.io/bundlephobia/minzip/react-gsap/^2) 8 | 9 | # Introduction 10 | 11 | `react-gsap` lets you use the GreenSock Animation Platform (GSAP) in React in a fully declarative way. 12 | It abstracts away the direct use of the GSAP [Tween](https://greensock.com/docs/v3/GSAP/Tween) and [Timeline](https://greensock.com/docs/v3/GSAP/Timeline) functions. 13 | 14 | If you need the full control it's possible by getting low level access to the underlying objects. 15 | 16 | In addition to that it ships some GSAP Plugins and useful helper components. 17 | 18 | From version 2 on it's build for GSAP 3 and only has `gsap` as a peer dependency. In this way you can update `gsap` separately from `react-gsap`. 19 | 20 | It's built with TypeScript and ships the types directly in the package. 21 | 22 | Documentation and examples are here: https://bitworking.github.io/react-gsap/ 23 | 24 | ##### The examples on the documentation pages are all editable directly in the browser. So play with it! 25 | 26 | ## Installation 27 | 28 | ```bash 29 | npm install gsap react-gsap 30 | ``` 31 | 32 | ## About GSAP 33 | 34 | GreenSock Animation Platform (GSAP) is a set of some JavaScript functions which let you tween a value/attribute/css property over time and insert these tweens into a timeline for more complex animations. 35 | 36 | `react-gsap` just adds some React wrapper components for these functions, so also read the official GreenSock documentation to know how to do things: 37 | 38 | [GreenSock Docs](https://greensock.com/docs/) 39 | 40 | 41 | ## License 42 | 43 | MIT © [bitworking](https://github.com/bitworking) 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "packages/*" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/docz/doczrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | typescript: true, 3 | repository: 'https://github.com/bitworking/react-gsap', 4 | base: '/react-gsap/', 5 | title: 'react-gsap Docs', 6 | description: 7 | 'react-gsap lets you use the GreenSock Animation Platform (GSAP) in React in a fully declarative way. It abstracts away the direct use of the GSAP Tween and Timeline functions.', 8 | menu: [ 9 | 'Introduction', 10 | { 11 | name: 'Components', 12 | menu: [ 13 | 'Tween', 14 | 'Timeline', 15 | 'ScrollTrigger', 16 | 'Reveal', 17 | 'SplitChars', 18 | 'SplitWords', 19 | 'Controls', 20 | ], 21 | }, 22 | { name: 'Plugins', menu: ['GSAP Plugins', 'CountPlugin', 'SvgDrawPlugin'] }, 23 | 'Instructions', 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /packages/docz/gatsby-node.js: -------------------------------------------------------------------------------- 1 | exports.onCreateBabelConfig = ({ actions }) => { 2 | actions.setBabelPlugin({ 3 | name: `@babel/plugin-transform-typescript`, 4 | options: { 5 | allowDeclareFields: true, 6 | }, 7 | }) 8 | } -------------------------------------------------------------------------------- /packages/docz/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "react-gsap-docz", 4 | "version": "2.0.0-rc.41", 5 | "license": "MIT", 6 | "files": [ 7 | "src/", 8 | "doczrc.js", 9 | "tsconfig.json", 10 | "package.json" 11 | ], 12 | "scripts": { 13 | "dev": "docz dev", 14 | "build": "docz build", 15 | "serve": "docz build && docz serve", 16 | "predeploy": "yarn build", 17 | "deploy": "gh-pages -d .docz/public" 18 | }, 19 | "dependencies": { 20 | "@emotion/core": "10.1.1", 21 | "@emotion/styled": "^11.10.4", 22 | "docz": "2.4.0", 23 | "gsap": "^3.11.1", 24 | "react": "^18.2.0", 25 | "react-dom": "^18.2.0", 26 | "react-gsap": "3.3.0" 27 | }, 28 | "devDependencies": { 29 | "@babel/plugin-transform-typescript": "^7.19.1", 30 | "@types/react": "^18.0.20", 31 | "@types/react-dom": "^18.0.6", 32 | "gh-pages": "^4.0.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/docz/src/components/Animation.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, ReactComponentElement, useEffect, useRef, useState } from 'react'; 2 | import { gsap } from 'gsap'; 3 | import { SplitChars, SplitWords, Timeline, Tween } from './../../../react-gsap/src'; 4 | 5 | export const FadeIn = ({ 6 | children, 7 | ...rest 8 | }: { 9 | children: React.ReactNode; 10 | [key: string]: any; 11 | }) => ( 12 | 13 | {children} 14 | 15 | ); 16 | 17 | export const FadeInLeft = ({ 18 | children, 19 | ...rest 20 | }: { 21 | children: React.ReactNode; 22 | [key: string]: any; 23 | }) => ( 24 | 29 | {children} 30 | 31 | ); 32 | 33 | export const RubberBand = ({ 34 | children, 35 | ...rest 36 | }: { 37 | children: React.ReactNode; 38 | [key: string]: any; 39 | }) => ( 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | 50 | export const FadeInLeftChars = ({ 51 | children, 52 | wrapper, 53 | ...rest 54 | }: { 55 | children: React.ReactNode; 56 | wrapper: ReactComponentElement; 57 | [key: string]: any; 58 | }) => ( 59 | 60 | {children} 61 | 62 | ); 63 | 64 | export const FadeInLeftWords = ({ 65 | children, 66 | wrapper, 67 | ...rest 68 | }: { 69 | children: React.ReactNode; 70 | wrapper: ReactComponentElement; 71 | [key: string]: any; 72 | }) => ( 73 | 74 | {children} 75 | 76 | ); 77 | 78 | export const CutText = ({ 79 | children, 80 | numberSlices = 4, 81 | type = 0, 82 | ...rest 83 | }: { 84 | children: string; 85 | numberSlices?: number; 86 | type?: number; 87 | [key: string]: any; 88 | }) => { 89 | const textRef = useRef(null); 90 | const [viewBox, setViewBox] = useState({ x: 0, y: 0, width: 0, height: 0 }); 91 | const [sliceHeight, setSliceHeight] = useState(0); 92 | 93 | useEffect(() => { 94 | const boundingBox = textRef.current 95 | ? textRef.current.getBBox() 96 | : { x: 0, y: 0, width: 0, height: 0 }; 97 | const { x, y, width, height } = boundingBox; 98 | setViewBox({ x, y, width, height }); 99 | 100 | setSliceHeight(height / numberSlices); 101 | }, []); 102 | 103 | return ( 104 | 110 | 111 | 119 | 120 | {children} 121 | 122 | 123 | 124 | } 126 | target={ 127 | 128 | {Array.from({ length: numberSlices }).map((_, index) => ( 129 | 136 | ))} 137 | 138 | } 139 | {...rest} 140 | > 141 | {type === 0 && ( 142 | 149 | )} 150 | 151 | {type === 1 && ( 152 | 164 | )} 165 | {type === 2 && ( 166 | 177 | )} 178 | 179 | 180 | ); 181 | }; 182 | 183 | export const AnimationTrigger = React.forwardRef(({ children }, ref) => ( 184 |
193 | {children} 194 |
195 | )); 196 | -------------------------------------------------------------------------------- /packages/docz/src/components/Controls.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Controls 3 | menu: Components 4 | --- 5 | 6 | import { Fragment } from 'react'; 7 | import { Playground, Props } from 'docz' 8 | import { Controls, PlayState } from './../../../react-gsap/src/' 9 | import { Tween } from './Tween' 10 | 11 | # Controls 12 | 13 | The Controls component is a helper that you can use to debug your animations. 14 | Just wrap it around a Tween or Timeline component and you'll get control buttons and a slider. 15 | 16 | With the optional `playState` prop you can set an initial play state. 17 | 18 | You can use the `PlayState` enum/object or just the strings "play", "restart", "reverse", "restartReverse", "stop", "stopEnd", "pause" or "resume". 19 | 20 | import { Controls, PlayState, Tween } from 'react-gsap'; 21 | 22 | ### Usage with Tween 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /packages/docz/src/components/Reveal.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Reveal 3 | menu: Components 4 | --- 5 | 6 | import { Fragment } from 'react'; 7 | import { Playground, Props } from 'docz' 8 | import { Reveal, SplitLetters } from './../../../react-gsap/src/' 9 | import { Tween, Comment } from './Tween' 10 | import { FadeInLeft, AnimationTrigger, RubberBand } from './Animation' 11 | 12 | 13 | # Reveal 14 | 15 | The Reveal component is basically an Intersection Observer and a Timeline component combined. 16 | 17 | All children Tween and Timeline components get played when they are visible in viewport. 18 | 19 | 20 | import { Reveal, Tween } from 'react-gsap'; 21 | 22 | ## Usage with Tween 23 | 24 | Add a little space for testing 25 | 26 | 27 | 28 | 29 | 30 | | 31 | 32 | so scroll down 33 | 34 | | 35 | 36 | | 37 | 38 | | 39 | 40 | | 41 | 42 | | 43 | 44 | | 45 | 46 | | 47 | 48 | | 49 | 50 | | 51 | 52 | | 53 | 54 | 55 | 56 | 57 |

This headline is fading in

58 |
59 |
60 |
61 | 62 | ## Fade in from left example 63 | 64 | Of course you can outsource animations to separate components. In this example a FadeInLeft animation: 65 | 66 | ```javascript 67 | const FadeInLeft = ({ children }) => ( 68 | 72 | {children} 73 | 74 | ); 75 | ``` 76 | 77 | Also note the use of the `trigger` prop. 78 | In this case the `

` wouldn't trigger because it's out of the viewport initially: `translate3d(-100vw, 0, 0)` 79 | If no trigger is passed, the first element from any child Tween or Timeline that gets beyond the threshold is used. 80 | 81 | 82 | }> 83 | 84 |

This headline is coming from left

85 |
86 |
87 |
88 | 89 | ## forwardRef trigger 90 | 91 | You can also create a forwardRef component as trigger if you need a more complex wrapper or trigger. 92 | 93 | ```javascript 94 | export const AnimationTrigger = React.forwardRef(({ children }, ref) => ( 95 |
104 | {children} 105 |
106 | )); 107 | 108 | ``` 109 | 110 | 111 | }> 112 | 113 |

I get triggered later

114 |
115 |
116 |
117 | 118 | ## Props 119 | 120 | | Name | Type | Default | Description | 121 | | :-- | :-- | :-- | :-- | 122 | | children | React.ReactNode | | | 123 | | trigger? | React.ReactElement \| null | | This element triggers the animation if it gets into view. Needs to be a "refable" element like HTML element or forwardRef component | 124 | | repeat? | boolean | false | Trigger the animation again and again? | 125 | | root? | Element \| null | null | | 126 | | rootMargin? | string | 0px | | 127 | | threshold? | number | 0.66 | As opposed to the threshold value from IntersectionObserver options this is just a single number | 128 | -------------------------------------------------------------------------------- /packages/docz/src/components/ScrollTrigger.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: ScrollTrigger 3 | menu: Components 4 | --- 5 | 6 | import { Fragment } from 'react'; 7 | import { Playground, Props } from 'docz' 8 | import { Controls, PlayState, ScrollTrigger } from './../../../react-gsap/src/' 9 | import { Tween } from './Tween' 10 | import { Timeline, TargetWithNames } from './Timeline' 11 | 12 | # ScrollTrigger 13 | 14 | The ScrollTrigger component is a small helper for the ScrollTrigger plugin. 15 | 16 | Read the official documentation: https://greensock.com/docs/v3/Plugins/ScrollTrigger 17 | 18 | It's available since version 3.2.0. 19 | Before you also could use the ScrollTrigger plugin by importing/registering and using it in a Tween or Timeline by yourself: 20 | 21 | import { Tween } from 'react-gsap'; 22 | 23 | import { ScrollTrigger } from 'gsap/dist/ScrollTrigger'; 24 | gsap.registerPlugin(ScrollTrigger); 25 | 26 | 38 |
39 | 40 | 41 | ## Basic usage 42 | 43 | With the ScrollTrigger component, it looks like the following. If you don't add a trigger prop, it will use the ref from the Tween target. 44 | 45 | 46 | 52 | 57 |
58 | 59 | 64 |
65 | 66 | 67 | 68 | 69 | ## Use "trigger" prop 70 | 71 | Currently it's not possible to change the props on the fly. **So this will not work for the `trigger` prop**: 72 | 73 | const triggerRef = useRef(null); 74 | const [trigger, setTrigger] = useState(triggerRef.current); 75 | 76 | useEffect(() => { 77 | setTrigger(triggerRef.current); 78 | }, []); 79 | 80 | return ( 81 | <> 82 | 83 | 88 |
This element gets not tweened by ref
89 |
90 |
91 | 92 | 93 | This element is the trigger 94 | 95 | 96 | ); 97 | 98 | If you want to target a ref directly instead of using a CSS selector you can use a Timeline with a forwardRef target: 99 | 100 | // This is the target component which "exports" 4 refs 101 | const TargetWithNames = forwardRef((props, ref: any) => { 102 | const div1 = useRef(null); 103 | const div2 = useRef[]>([]); 104 | const div3 = useRef(null); 105 | const trigger = useRef(null); 106 | 107 | useImperativeHandle(ref, () => ({ 108 | div1, 109 | div2, 110 | div3, 111 | trigger, 112 | })); 113 | 114 | return ( 115 |
116 |

THIS

117 | ) => div2.current.push(charRef)} 119 | wrapper={

} 120 | > 121 | TEST 122 | 123 |

IS A

124 |
125 | ); 126 | }); 127 | 128 | You can then use the key of the exported refs in the `trigger` or `target` props. 129 | If it doesn't find a ref with this key it will use the `trigger` string as CSS selector. 130 | 131 | 132 | 140 | }> 141 | 147 | 153 | 160 | 161 | 162 | 163 | 164 | ## Standalone 165 | 166 | If you don't pass children to the component a GSAP ScrollTrigger instance will be created and can be used standalone. 167 | 168 | You can get the instance by calling `getGSAP()` on the ref. 169 | 170 | 171 | {() => { 172 | const scrollTrigger = React.useRef(null); 173 | return ( 174 | 175 |
176 |
177 | console.log("toggled, isActive:", self.isActive)} 185 | onUpdate={self => { 186 | console.log("progress:", self.progress.toFixed(3), "direction:", self.direction, "velocity", self.getVelocity()); 187 | }} 188 | /> 189 | 190 | 191 | ) 192 | }} 193 | 194 | -------------------------------------------------------------------------------- /packages/docz/src/components/SplitChars.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: SplitChars 3 | menu: Components 4 | --- 5 | 6 | import { Fragment } from 'react'; 7 | import { Playground, Props } from 'docz' 8 | import { Controls, PlayState, SplitChars } from './../../../react-gsap/src/' 9 | import { Tween } from './Tween' 10 | 11 | # SplitChars 12 | 13 | The SplitChars component is a small helper that splits a text by chars and returns one component per char. 14 | 15 | import { Controls, PlayState, Tween, SplitChars } from 'react-gsap'; 16 | 17 | ## Usage with Tween 18 | 19 | 20 | 21 | 22 | }> 23 | This text gets splitted by chars. 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /packages/docz/src/components/SplitWords.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: SplitWords 3 | menu: Components 4 | --- 5 | 6 | import { Fragment } from 'react'; 7 | import { Playground, Props } from 'docz' 8 | import { Controls, PlayState, SplitWords } from './../../../react-gsap/src/' 9 | import { Tween } from './Tween' 10 | import { Timeline } from './Timeline' 11 | 12 | # SplitWords 13 | 14 | The SplitWords component is a small helper that splits a text by words and returns one component per word. 15 | 16 | import { Controls, PlayState, Timeline, Tween, SplitWords } from 'react-gsap'; 17 | 18 | ## Usage with Tween 19 | 20 | 21 | 22 | 23 | }> 24 | This text gets splitted by words. 25 | 26 | 27 | 28 | 29 | 30 | ## Usage with Timeline 31 | 32 | 33 | 34 | 36 | }> 37 | This text gets splitted by words. 38 | 39 | 40 | }> 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ## With custom delimiter 49 | 50 | You can use the `delimiter` prop to use a custom delimiter string. 51 | 52 | 53 | 54 | 55 | } 58 | > 59 | This|text|gets|splitted|by|custom|delimiter 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /packages/docz/src/components/Timeline.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Timeline 3 | menu: Components 4 | --- 5 | 6 | import { Fragment } from 'react'; 7 | import { Playground, Props } from 'docz' 8 | import { Controls, PlayState } from './../../../react-gsap/src/' 9 | import { Timeline, TimelinePropsDummy, TargetWithNames } from './Timeline' 10 | import { Tween } from './Tween' 11 | 12 | # Timeline 13 | 14 | The Timeline component uses the [gsap.timeline()](https://greensock.com/docs/v3/GSAP/Timeline) function internally. 15 | 16 | import { Controls, PlayState, Timeline, Tween } from 'react-gsap'; 17 | 18 | ## Basic usage 19 | 20 | You can add a target and control it with childless Tween components. The target needs to be a "refable" component. 21 | So it can be a HTML element or a forwardRef component like a styled-components component. 22 | 23 | 24 | 25 | 28 | } 29 | > 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ## Other Tweens 38 | 39 | You can also add other normal Tweens. 40 | 41 | 42 | 43 | 46 | } 47 | > 48 | 49 | 50 | 51 |
52 | 53 | 54 | 55 | 56 | 57 | ## Multiple targets 58 | 59 | You can wrap multiple target components in a Fragment and target them with the array index with the `target` prop from the Tween component. 60 | If you don't add a target you transform all target components. 61 | 62 | 63 | 64 | 67 |
68 |
69 | 70 | } 71 | > 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | ## Advanced multiple targets 82 | 83 | If you need to target individual elements you can use a special forwardRef component with useImperativeHandle hook. 84 | 85 | In this way these component can be better reused and the refs not only work in a Timeline target context. 86 | 87 | You can also pass an array ref like seen with div2. In this way you can use the `stagger` prop. 88 | 89 | ```javascript 90 | const TargetWithNames = forwardRef((props, ref) => { 91 | const div1 = useRef(null); 92 | const div2 = useRef([]); 93 | const div3 = useRef(null); 94 | useImperativeHandle(ref, () => ({ 95 | div1, 96 | div2, 97 | div3, 98 | })); 99 | return ( 100 |
101 |

THIS

102 | div2.current.push(charRef)} 104 | wrapper={

} 105 | > 106 | TEST 107 | 108 |

IS A

109 |
110 | ); 111 | }); 112 | 113 | ``` 114 | 115 | If you want to combine multiple of those named components, you can do it like this: 116 | 117 | ```javascript 118 | const TargetWithNamesCombined = forwardRef((props, ref) => { 119 | const target1 = useRef({}); 120 | const target2 = useRef({}); 121 | useImperativeHandle(ref, () => ({ 122 | ...target1.current, 123 | ...target2.current, 124 | })); 125 | return ( 126 | <> 127 | 128 | 129 | 130 | ); 131 | }); 132 | 133 | ``` 134 | 135 | For version < 3: 136 | 137 | If you need to target individual elements you can use a special forwardRef function. 138 | The `targets` parameter provide the `set` function, which you can use to set a ref to a certain key. 139 | 140 | If you use an array as value, as seen in the example, you can save multiple elements as array under one key and use e.g. the `stagger` prop. 141 | 142 | ```javascript 143 | const TargetWithNames = forwardRef((props, targets) => ( 144 |
145 |

targets.set('div1', div)}>THIS

146 | targets.set('div2', [div])} 148 | wrapper={

} 149 | > 150 | TEST 151 | 152 |

targets.set('div3', div)}>IS A

153 |
154 | )); 155 | 156 | ``` 157 | 158 | 159 | 160 | }> 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | ## Nested Timelines 169 | 170 | You can nest other Timelines or HTML structures. 171 | 172 | 173 | 174 | 177 | } 178 | > 179 | 180 | 181 |
182 |

Other Timeline:

183 | 186 | } 187 | > 188 | 189 | 190 | 191 |
192 |
193 |
194 |
195 | 196 | ## Label support 197 | 198 | You can add labels to the timeline and use them in the position prop of the Tweens or nested Timelines. 199 | 200 | More info: [https://greensock.com/docs/v3/GSAP/Timeline/addLabel()](https://greensock.com/docs/v3/GSAP/Timeline/addLabel()) 201 | 202 | 203 | 204 | 207 | } 208 | labels={[ 209 | { 210 | label: 'sec4', 211 | position: 4, 212 | }, 213 | { 214 | label: 'sec6', 215 | position: 6, 216 | }, 217 | ]} 218 | > 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | ## Props 228 | 229 | The question mark means it's an optional prop. 230 | 231 | For all available props check out the documentation: [vars](https://greensock.com/docs/v3/GSAP/Timeline/vars) 232 | 233 | | Name | Type | Description | 234 | | :-- | :-- | :-- | 235 | | children | React.ReactNode | Can be any component type. But Tween and other Timeline components are controlled by this Timeline | 236 | | wrapper? | React.ReactElement | This component gets wrapped around the Timeline component | 237 | | target? | React.ReactElement \| null | The target component that gets outputted and tweened from all childless Tween child components | 238 | | position? | string \| number | If this Timeline is a child from another Timeline it's added at this position | 239 | | labels? | Label[] | Can be used to give the positions a name | 240 | | duration? | number | Adjusts the Timeline's timeScale to fit it within the specified duration (Can be changed on-the-fly) | 241 | | progress? | number | 0 - 1 (Can be changed on-the-fly) | 242 | | totalProgress? | number | 0 - 1 (Can be changed on-the-fly) | 243 | | playState? | PlayState | Use it to control the playing state (Can be changed on-the-fly) | 244 | | [prop: string] | any | All other props are added to the vars object for the gsap.timeline function | 245 | 246 | ## Type Label 247 | 248 | More info: [https://greensock.com/docs/v3/GSAP/Timeline/addLabel()](https://greensock.com/docs/v3/GSAP/Timeline/addLabel()) 249 | 250 | | Name | Type | 251 | | :-- | :-- | 252 | | label | string | 253 | | position | string \| number | 254 | 255 | ## Enum PlayState 256 | 257 | | Field | As string | 258 | | :-- | :-- | 259 | | play | "play" | 260 | | restart | "restart" | 261 | | reverse | "reverse" | 262 | | restartReverse | "restartReverse" | 263 | | stop | "stop" | 264 | | stopEnd | "stopEnd" | 265 | | pause | "pause" | 266 | | resume | "resume" | 267 | -------------------------------------------------------------------------------- /packages/docz/src/components/Timeline.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | forwardRef, 3 | MutableRefObject, 4 | ReactElement, 5 | useImperativeHandle, 6 | useRef, 7 | } from 'react'; 8 | import Timeline, { TimelineProps } from './../../../react-gsap/src/Timeline'; 9 | import { SplitChars } from './../../../react-gsap/src'; 10 | 11 | export const TimelinePropsDummy: React.FunctionComponent = props => ( 12 |
13 | ); 14 | 15 | const TargetWithNames = forwardRef((props, ref: any) => { 16 | const div1 = useRef(null); 17 | const div2 = useRef[]>([]); 18 | const div3 = useRef(null); 19 | const trigger = useRef(null); 20 | useImperativeHandle(ref, () => ({ 21 | div1, 22 | div2, 23 | div3, 24 | trigger, 25 | })); 26 | return ( 27 |
28 |

THIS

29 | ) => div2.current.push(charRef)} 31 | wrapper={

} 32 | > 33 | TEST 34 | 35 |

IS A

36 |
37 | ); 38 | }); 39 | 40 | export { Timeline, TargetWithNames }; 41 | -------------------------------------------------------------------------------- /packages/docz/src/components/Tween.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Tween 3 | menu: Components 4 | --- 5 | 6 | import { Fragment } from 'react' 7 | import { Playground, Props } from 'docz' 8 | import { Controls, PlayState } from './../../../react-gsap/src/' 9 | import { Tween, TweenPropsDummy, StaggerPropsDummy } from './Tween' 10 | 11 | # Tween 12 | 13 | The Tween component uses the [gsap.to()](https://greensock.com/docs/v3/GSAP/gsap.to()), 14 | [gsap.from()](https://greensock.com/docs/v3/GSAP/gsap.from()) and 15 | [gsap.fromTo()](https://greensock.com/docs/v3/GSAP/gsap.fromTo()) functions internally. 16 | 17 | The children need to be "refable" components. So they can be HTML elements or forwardRef components like styled-components components. 18 | 19 | import { Controls, PlayState, Tween } from 'react-gsap'; 20 | 21 | ## Use "to" prop 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | ## Use "from" prop 32 | 33 | 34 | 35 | 36 |
37 | 38 | 39 | 40 | 41 | ## Use "from" and "to" prop 42 | 43 | Internally the [gsap.fromTo()](https://greensock.com/docs/v3/GSAP/gsap.fromTo()) function will be called. 44 | 45 | 46 | 47 | 48 |
49 | 50 | 51 | 52 | 53 | ## Use stagger prop 54 | 55 | If you have multiple children you can make use of the stagger prop. 56 | 57 | More info: https://greensock.com/docs/v3/Staggers 58 | 59 | 60 | 61 | 62 |
63 |
64 |
65 |
66 |
67 | 68 | 69 | 70 | 71 | ### Advanced stagger 72 | 73 | 74 | 75 |
76 | 77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | 87 |
88 | 89 | 90 | 91 | ## Props 92 | 93 | The question mark means it's an optional prop. 94 | 95 | For a documentation of all possible props or from and to values check out the documentations: 96 | [vars](https://greensock.com/docs/v3/GSAP/Tween/vars), [gsap.to()](https://greensock.com/docs/v3/GSAP/gsap.to()), 97 | [gsap.from()](https://greensock.com/docs/v3/GSAP/gsap.from()) and [gsap.fromTo()](https://greensock.com/docs/v3/GSAP/gsap.fromTo()) 98 | 99 | | Name | Type | Description | 100 | | :-- | :-- | :-- | 101 | | children? | React.ReactNode | Need to be "refable" components. So they can be HTML elements or forwardRef components like styled-components components | 102 | | wrapper? | any | This component gets wrapped around the Tween component | 103 | | target? | number \| string | The target index or key of the Timeline targets. Used if Tween is childless and child of a Timeline | 104 | | position? | string \| number | If this Tween is a child from a Timeline it's added at this position | 105 | | from? | any | The vars object for the [gsap.from()](https://greensock.com/docs/v3/GSAP/gsap.from()) or [gsap.fromTo()](https://greensock.com/docs/v3/GSAP/gsap.fromTo()) function | 106 | | to? | any | The vars object for the [gsap.to()](https://greensock.com/docs/v3/GSAP/gsap.to()) or [gsap.fromTo()](https://greensock.com/docs/v3/GSAP/gsap.fromTo()) function | 107 | | stagger? | number \| Stagger \| (index: number, target: any, list: any) => number | If multiple children are added, you can stagger the start times for each | 108 | | duration? | number | The duration of the animation (in seconds) (Can be changed on-the-fly) | 109 | | progress? | number | 0 - 1 (Can be changed on-the-fly) | 110 | | totalProgress? | number | 0 - 1 (Can be changed on-the-fly) | 111 | | playState? | PlayState | Use it to control the playing state (Can be changed on-the-fly) | 112 | | disabled? | boolean | on-the-fly changes and are no more possible | 113 | | onlyInvalidateTo? | boolean | | 114 | | [prop: string] | any | All other props are added to the vars object for the Tween functions | 115 | 116 | ## Type Stagger 117 | 118 | More info: https://greensock.com/docs/v3/Staggers 119 | 120 | | Name | Type | 121 | | :-- | :-- | 122 | | amount? | number | 123 | | each? | number | 124 | | from? | 'start' \| 'center' \| 'edges' \| 'random' \| 'end' \| number \| [number, number] | 125 | | grid? | [number, number] \| 'auto' | 126 | | axis? | 'x' \| 'y' | 127 | | ease? | string \| (value: number) => number | 128 | | repeat? | number | 129 | | yoyo? | boolean | 130 | | [prop: string] | any | 131 | 132 | ## Enum PlayState 133 | 134 | | Field | As string | 135 | | :-- | :-- | 136 | | play | "play" | 137 | | restart | "restart" | 138 | | reverse | "reverse" | 139 | | restartReverse | "restartReverse" | 140 | | stop | "stop" | 141 | | stopEnd | "stopEnd" | 142 | | pause | "pause" | 143 | | resume | "resume" | 144 | -------------------------------------------------------------------------------- /packages/docz/src/components/Tween.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Tween, { TweenProps, Stagger } from './../../../react-gsap/src/Tween'; 3 | 4 | export const TweenPropsDummy: React.FunctionComponent = props =>
; 5 | export const StaggerPropsDummy: React.FunctionComponent = props =>
; 6 | export { Tween }; 7 | 8 | export const Comment = (props: any) =>
{props.children}
; 9 | -------------------------------------------------------------------------------- /packages/docz/src/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Introduction 3 | route: / 4 | --- 5 | 6 | [![NPM](https://img.shields.io/npm/v/react-gsap.svg)](https://www.npmjs.com/package/react-gsap) 7 | ![npm type definitions](https://img.shields.io/npm/types/react-gsap) 8 | ![npm bundle size (version)](https://img.shields.io/bundlephobia/minzip/react-gsap/^2) 9 | 10 | # Introduction 11 | 12 | `react-gsap` lets you use the GreenSock Animation Platform (GSAP) in React in a fully declarative way. 13 | It abstracts away the direct use of the GSAP [Tween](https://greensock.com/docs/v3/GSAP/Tween) and [Timeline](https://greensock.com/docs/v3/GSAP/Timeline) functions. 14 | 15 | If you need the full control it's possible by getting low level access to the underlying objects. 16 | 17 | In addition to that it ships some GSAP Plugins and useful helper components. 18 | 19 | From version 2 on it's build for GSAP 3 and only has `gsap` as a peer dependency. In this way you can update `gsap` separately from `react-gsap`. 20 | 21 | It's built with TypeScript and ships the types directly in the package. 22 | 23 | Documentation and examples are here: https://bitworking.github.io/react-gsap/ 24 | 25 | ##### The examples on the documentation pages are all editable directly in the browser. So play with it! 26 | 27 | ## Installation 28 | 29 | ```bash 30 | npm install gsap react-gsap 31 | ``` 32 | 33 | ## About GSAP 34 | 35 | GreenSock Animation Platform (GSAP) is a set of some JavaScript functions which let you tween a value/attribute/css property over time and insert these tweens into a timeline for more complex animations. 36 | 37 | `react-gsap` just adds some React wrapper components for these functions, so also read the official GreenSock documentation to know how to do things: 38 | 39 | [GreenSock Docs](https://greensock.com/docs/) 40 | 41 | 42 | ## License 43 | 44 | MIT © [bitworking](https://github.com/bitworking) 45 | -------------------------------------------------------------------------------- /packages/docz/src/instructions/Easing.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Easing 3 | menu: Instructions 4 | --- 5 | 6 | import { Fragment } from 'react'; 7 | import { Playground } from 'docz' 8 | import { Controls, PlayState, Tween } from './../../../react-gsap/src/' 9 | 10 | import { gsap } from 'gsap' 11 | import { SlowMo } from 'gsap/EasePack' 12 | gsap.registerPlugin(SlowMo) 13 | 14 | # Easing 15 | 16 | With GSAP 3 the easing functions are more easily usable as string without importing. They can be used in the "ease" prop of the `Tween`. 17 | 18 | Have a look at the docs: https://greensock.com/docs/v3/Eases 19 | 20 | The default is `power1.out`. You can overide the defaults if you want: https://greensock.com/docs/v3/GSAP/gsap.defaults(). 21 | 22 | import { Controls, PlayState, Tween } from 'react-gsap'; 23 | 24 | ## Ease function with parameters 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | ## SlowMo ease 35 | 36 | This is an extra ease that has to be registered first: 37 | 38 | import { gsap } from 'gsap'; 39 | import { SlowMo } from 'gsap/EasePack'; 40 | 41 | gsap.registerPlugin(SlowMo); 42 | 43 | 44 | 45 | 46 |
47 | 48 | 49 | 50 | 51 | ## Custom easing function 52 | 53 | You can also pass a function to the ease prop. In this way you can have your custom easing functions. 54 | 55 | In the example it's an `easeInOutQuart`. Take a look at this resource for inspiration: https://easings.net/ 56 | 57 | 58 | 59 | x < 0.5 ? 8 * x * x * x * x : 1 - Math.pow(-2 * x + 2, 4) / 2}> 60 |
61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /packages/docz/src/instructions/LowLevelAccess.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Low-level access 3 | menu: Instructions 4 | --- 5 | 6 | import { Fragment } from 'react'; 7 | import { Playground } from 'docz' 8 | import { PlayState, Tween } from './../../../react-gsap/src/' 9 | 10 | # Low-level access 11 | 12 | You are able to use the complete API of the underlying GSAP objects. 13 | Just add a reference to the React `Tween` or `Timeline` components and get the `Tween` or `Timeline` objects from GSAP by calling the getGSAP() method on it: 14 | 15 | import { PlayState, Tween } from 'react-gsap'; 16 | 17 | ### Usage with Tween 18 | 19 | 20 | {() => { 21 | const tween = React.useRef(null) 22 | return ( 23 | 24 | 25 |
26 | 27 | 30 | 33 | 36 | 37 | ) 38 | }} 39 | 40 | 41 | -------------------------------------------------------------------------------- /packages/docz/src/instructions/Migration.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Migration from version 1 3 | menu: Instructions 4 | --- 5 | 6 | import { Fragment } from 'react'; 7 | import { Playground } from 'docz' 8 | import { PlayState, Tween } from './../../../react-gsap/src/' 9 | 10 | # Migration from version 1 11 | 12 | `react-gsap` version 2 is mostly backwards compatible with version 1. 13 | 14 | But there are 2 changes: 15 | 16 | 1. You need to install `gsap` separately. So if you upgrade `react-gsap` you need to upgrade `gsap` to a version >= 3. 17 | 18 | 2. The `Tween` component props `staggerFrom` and `staggerTo` are removed. 19 | In `gsap` version >=3 you can "stagger" the normal 20 | [gsap.to()](https://greensock.com/docs/v3/GSAP/gsap.to()), [gsap.from()](https://greensock.com/docs/v3/GSAP/gsap.from()) and 21 | [gsap.fromTo()](https://greensock.com/docs/v3/GSAP/gsap.fromTo()) functions. 22 | 23 | So replace this: 24 | 25 | 26 |
27 |
28 | 29 | 30 | With this: 31 | 32 | 33 |
34 |
35 | 36 | 37 | 38 | There are possible more changes in `gsap` version 3 that leads to a different behaviour. Have also a look at https://greensock.com/3-migration/. 39 | If you find something please open an issue in github. 40 | -------------------------------------------------------------------------------- /packages/docz/src/instructions/PlayState.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Use "playState" prop 3 | menu: Instructions 4 | --- 5 | 6 | import { Fragment } from 'react'; 7 | import { Playground } from 'docz' 8 | import { PlayState, Tween } from './../../../react-gsap/src/' 9 | 10 | # Use playState and totalProgress props 11 | 12 | With the playState and progress/totalProgress props you can control a Tween or a Timeline. 13 | So you don't need low-level access to play/reverse/pause/stop or seek to a position. 14 | 15 | From version 3.2.0 on the `playState` prop also works for the initial state and the following states were added: 16 | `restartReverse`, `stopEnd`, `resume`. 17 | 18 | The following gsap functions are called internally, if the `playState` prop change: 19 | 20 | if (playState === PlayState.play) { 21 | gsap.play(); 22 | } else if (playState === PlayState.restart) { 23 | gsap.restart(true); 24 | } else if (playState === PlayState.reverse) { 25 | gsap.reverse(); 26 | } else if (playState === PlayState.restartReverse) { 27 | gsap.reverse(0); 28 | } else if (playState === PlayState.stop) { 29 | gsap.pause(0); 30 | } else if (playState === PlayState.stopEnd) { 31 | gsap.reverse(0); 32 | gsap.pause(); 33 | } else if (playState === PlayState.pause) { 34 | gsap.pause(); 35 | } else if (playState === PlayState.resume) { 36 | gsap.resume(); 37 | } 38 | 39 | 40 | {() => { 41 | const [playState, setPlayState] = React.useState(PlayState.pause); 42 | const [totalProgress, setTotalProgress] = React.useState(0) 43 | return ( 44 | 45 | 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 | setTotalProgress(event.target.value)} /> 58 |
59 | 60 | ) 61 | }} 62 | 63 | 64 | -------------------------------------------------------------------------------- /packages/docz/src/plugins/CountPlugin.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: CountPlugin 3 | menu: Plugins 4 | --- 5 | 6 | import { Fragment } from 'react'; 7 | import { Playground, Props } from 'docz' 8 | import { Controls, PlayState, Timeline, Tween } from './../../../react-gsap/src/' 9 | 10 | # CountPlugin 11 | 12 | The CountPlugin animates an number counting. It uses the innerText from the target elements. (children) 13 | 14 | It can be called with the "count" property which takes an number or an object with the following properties: 15 | 16 | | Name | Type | 17 | | :-- | :-- | 18 | | value | number | 19 | | format | () => (value) => string | 20 | 21 | import { Controls, PlayState, Tween } from 'react-gsap'; 22 | 23 | ## Integer 24 | 25 | By default the count value will be parsed to an integer. 26 | 27 | 28 | 29 |
30 | 37 |
0
38 |
39 |
40 |
41 |
42 | 43 | ## Custom format function 44 | 45 | You can use your own custom format function. 46 | 47 | 48 | 49 |
50 | (value) => value.toFixed(2), 55 | }, 56 | }} 57 | ease="none" 58 | duration={5} 59 | > 60 |
10.00
61 |
62 |
63 |
64 |
65 | 66 | ## Percentage Bar 67 | 68 | 69 | 70 | 71 | (value) => `${parseInt(value, 10)} %`, 76 | }, 77 | }} 78 | ease="none" 79 | duration={5} 80 | position={0} 81 | > 82 |
0 %
83 |
84 |
85 | 94 |
95 | 96 |
97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /packages/docz/src/plugins/GSAP.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: GSAP Plugins 3 | menu: Plugins 4 | --- 5 | 6 | import { Fragment } from 'react'; 7 | import { Playground } from 'docz' 8 | import { Controls, PlayState, Tween } from './../../../react-gsap/src/' 9 | 10 | import { gsap } from 'gsap' 11 | import { TextPlugin } from 'gsap/TextPlugin' 12 | gsap.registerPlugin(TextPlugin) 13 | 14 | # GSAP Plugins 15 | 16 | There are some Core Plugins that are already included in GSAP: https://greensock.com/docs/v3/GSAP/CorePlugins 17 | 18 | And there are some free Plugins that you can use to extend the functionality. Here is the list: https://greensock.com/docs/v3/Plugins. 19 | 20 | To use them with `react-gsap` you first have to register them. 21 | 22 | ### Register the TextPlugin for example: 23 | 24 | import { gsap } from 'gsap'; 25 | import { TextPlugin } from 'gsap/TextPlugin'; 26 | 27 | # if your build step doesn't transpile inside node_modules you can try this import: 28 | import { TextPlugin } from 'gsap/dist/TextPlugin'; 29 | 30 | gsap.registerPlugin(TextPlugin); 31 | 32 | ### And use it: 33 | 34 | 35 | 36 | 37 | This is a text. 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /packages/docz/src/plugins/SvgDrawPlugin.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: SvgDrawPlugin 3 | menu: Plugins 4 | --- 5 | 6 | import { Fragment } from 'react'; 7 | import { Playground, Props } from 'docz' 8 | import { Controls, PlayState, Tween } from './../../../react-gsap/src/' 9 | 10 | # SvgDrawPlugin 11 | 12 | The SvgDrawPlugin is shipped with react-gsap and lets you animate SVG elements. 13 | You can draw the following SVG elements: path, circle, rect, line, polyline and polygon. 14 | 15 | It works similar to the [DrawSVGPlugin](https://greensock.com/drawSVG) from GreenSock but the parameters are a little different. 16 | 17 | It can be called with the "svgDraw" property and takes a single number (0-1) value or an array with two numbers (Tuple) ([(0-1), (0-1)]). 18 | 19 | The single or first number is the length of the stroke. 20 | 21 | The second value is the position/offset on the path. (default = 0) 22 | 23 | So you can animate a line drawing from start to end like that: 24 | 25 | import { Controls, PlayState, Tween } from 'react-gsap'; 26 | 27 | ## Single Number 28 | 29 | 30 | 31 | 38 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ## Tuple 53 | 54 | An animation from the middle to the outside: 55 | 56 | 57 | 58 | 65 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | ## All possible SVG elements 80 | 81 | 82 | 83 | 84 | 93 | 102 | 113 | 114 | 115 | 116 | 122 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /packages/docz/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "moduleResolution": "node", 5 | "strict": true, 6 | "resolveJsonModule": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": false, 9 | "noEmit": true, 10 | "jsx": "react", 11 | "allowSyntheticDefaultImports": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/next/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | -------------------------------------------------------------------------------- /packages/next/README.md: -------------------------------------------------------------------------------- 1 | # TypeScript Next.js example 2 | 3 | This is a really simple project that shows the usage of Next.js with TypeScript. 4 | 5 | ## Deploy your own 6 | 7 | Deploy the example using [Vercel](https://vercel.com): 8 | 9 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/zeit/next.js/tree/canary/examples/with-typescript) 10 | 11 | ## How to use it? 12 | 13 | ### Using `create-next-app` 14 | 15 | Execute [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: 16 | 17 | ```bash 18 | npm init next-app --example with-typescript with-typescript-app 19 | # or 20 | yarn create next-app --example with-typescript with-typescript-app 21 | ``` 22 | 23 | ### Download manually 24 | 25 | Download the example: 26 | 27 | ```bash 28 | curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-typescript 29 | cd with-typescript 30 | ``` 31 | 32 | Install it and run: 33 | 34 | ```bash 35 | npm install 36 | npm run dev 37 | # or 38 | yarn 39 | yarn dev 40 | ``` 41 | 42 | Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). 43 | 44 | ## Notes 45 | 46 | This example shows how to integrate the TypeScript type system into Next.js. Since TypeScript is supported out of the box with Next.js, all we have to do is to install TypeScript. 47 | 48 | ``` 49 | npm install --save-dev typescript 50 | ``` 51 | 52 | To enable TypeScript's features, we install the type declarations for React and Node. 53 | 54 | ``` 55 | npm install --save-dev @types/react @types/react-dom @types/node 56 | ``` 57 | 58 | When we run `next dev` the next time, Next.js will start looking for any `.ts` or `.tsx` files in our project and builds it. It even automatically creates a `tsconfig.json` file for our project with the recommended settings. 59 | 60 | Next.js has built-in TypeScript declarations, so we'll get autocompletion for Next.js' modules straight away. 61 | 62 | A `type-check` script is also added to `package.json`, which runs TypeScript's `tsc` CLI in `noEmit` mode to run type-checking separately. You can then include this, for example, in your `test` scripts. 63 | -------------------------------------------------------------------------------- /packages/next/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /packages/next/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } -------------------------------------------------------------------------------- /packages/next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-typescript", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "next", 6 | "build": "next build", 7 | "start": "next start", 8 | "type-check": "tsc" 9 | }, 10 | "dependencies": { 11 | "gsap": "^3.11.1", 12 | "next": "^12.3.0", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-gsap": "3.3.0" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^18.7.18", 19 | "@types/react": "^18.0.20", 20 | "@types/react-dom": "^18.0.6", 21 | "typescript": "4.8.3" 22 | }, 23 | "license": "ISC" 24 | } 25 | -------------------------------------------------------------------------------- /packages/next/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, forwardRef } from 'react'; 2 | import { SplitChars, Timeline, Tween, Controls, PlayState } from 'react-gsap'; 3 | 4 | const IndexPage = () => ( 5 | <> 6 | 7 |
15 | 22 |
0
23 |
24 |
25 |
26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | ); 34 | 35 | export default IndexPage; 36 | -------------------------------------------------------------------------------- /packages/next/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "alwaysStrict": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "isolatedModules": true, 8 | "jsx": "preserve", 9 | "lib": [ 10 | "dom", 11 | "es2017" 12 | ], 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "noEmit": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "resolveJsonModule": true, 20 | "skipLibCheck": true, 21 | "strict": true, 22 | "target": "esnext", 23 | "incremental": true 24 | }, 25 | "exclude": [ 26 | "node_modules" 27 | ], 28 | "include": [ 29 | "**/*.ts", 30 | "**/*.tsx" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /packages/playground/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /packages/playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-gsap-playground", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^14.4.3", 9 | "@types/jest": "^29.0.3", 10 | "@types/node": "^18.7.18", 11 | "@types/react": "^18.0.20", 12 | "@types/react-dom": "^18.0.6", 13 | "@types/react-router-dom": "^5.3.3", 14 | "@types/react-transition-group": "^4.4.5", 15 | "@types/styled-components": "^5.1.26", 16 | "@types/uuid": "^8.3.4", 17 | "gsap": "^3.11.1", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "react-gsap": "3.3.0", 21 | "react-router-dom": "^6.4.0", 22 | "react-scripts": "5.0.1", 23 | "react-transition-group": "^4.4.5", 24 | "styled-components": "^5.3.5", 25 | "typescript": "~4.8.3", 26 | "uuid": "^9.0.0" 27 | }, 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "build": "react-scripts build", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject" 33 | }, 34 | "eslintConfig": { 35 | "extends": "react-app" 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.2%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/playground/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitworking/react-gsap/b6180f5a3ccd243611c8e891c8932dbf3b1d1d87/packages/playground/public/favicon.ico -------------------------------------------------------------------------------- /packages/playground/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /packages/playground/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitworking/react-gsap/b6180f5a3ccd243611c8e891c8932dbf3b1d1d87/packages/playground/public/logo192.png -------------------------------------------------------------------------------- /packages/playground/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitworking/react-gsap/b6180f5a3ccd243611c8e891c8932dbf3b1d1d87/packages/playground/public/logo512.png -------------------------------------------------------------------------------- /packages/playground/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/playground/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/playground/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/playground/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Route, Link } from 'react-router-dom'; 3 | 4 | import Tween from './examples/Tween'; 5 | import Timeline from './examples/Timeline'; 6 | import Svg from './examples/Svg'; 7 | import Transition from './examples/Transition'; 8 | import Reveal from './examples/Reveal'; 9 | import ScrollTrigger from './examples/ScrollTrigger'; 10 | import ManualControl from './examples/ManualControl'; 11 | // import Scroller from './examples/Scroller'; 12 | 13 | function App() { 14 | return ( 15 |
16 | 17 |
18 | 46 | 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {/**/} 57 |
58 |
59 |
60 | ); 61 | } 62 | 63 | export default App; 64 | -------------------------------------------------------------------------------- /packages/playground/src/examples/Animation.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, ReactComponentElement, useEffect, useRef, useState } from 'react'; 2 | import { SplitChars, SplitWords, Timeline, Tween } from 'react-gsap'; 3 | import { gsap } from 'gsap'; 4 | 5 | export const FadeIn = ({ 6 | children, 7 | ...rest 8 | }: { 9 | children: React.ReactNode; 10 | [key: string]: any; 11 | }) => ( 12 | 13 | {children} 14 | 15 | ); 16 | 17 | export const FadeInLeft = ({ 18 | children, 19 | ...rest 20 | }: { 21 | children: React.ReactNode; 22 | [key: string]: any; 23 | }) => ( 24 | 29 | {children} 30 | 31 | ); 32 | 33 | export const RubberBand = ({ 34 | children, 35 | ...rest 36 | }: { 37 | children: React.ReactElement; 38 | [key: string]: any; 39 | }) => ( 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | 50 | export const FadeInLeftChars = ({ 51 | children, 52 | wrapper, 53 | ...rest 54 | }: { 55 | children: React.ReactNode; 56 | wrapper: ReactComponentElement; 57 | [key: string]: any; 58 | }) => ( 59 | 60 | {children} 61 | 62 | ); 63 | 64 | export const FadeInLeftWords = ({ 65 | children, 66 | wrapper, 67 | ...rest 68 | }: { 69 | children: React.ReactNode; 70 | wrapper: ReactComponentElement; 71 | [key: string]: any; 72 | }) => ( 73 | 74 | {children} 75 | 76 | ); 77 | 78 | export const CutText = ({ 79 | children, 80 | numberSlices = 4, 81 | type = 0, 82 | ...rest 83 | }: { 84 | children: string; 85 | numberSlices?: number; 86 | type?: number; 87 | [key: string]: any; 88 | }) => { 89 | const textRef = useRef(null); 90 | const [viewBox, setViewBox] = useState({ x: 0, y: 0, width: 0, height: 0 }); 91 | const [sliceHeight, setSliceHeight] = useState(0); 92 | 93 | useEffect(() => { 94 | const boundingBox = textRef.current 95 | ? textRef.current.getBBox() 96 | : { x: 0, y: 0, width: 0, height: 0 }; 97 | const { x, y, width, height } = boundingBox; 98 | setViewBox({ x, y, width, height }); 99 | 100 | setSliceHeight(height / numberSlices); 101 | }, []); 102 | 103 | return ( 104 | 110 | 111 | 119 | 120 | {children} 121 | 122 | 123 | 124 | } 126 | target={ 127 | 128 | {Array.from({ length: numberSlices }).map((_, index) => ( 129 | 136 | ))} 137 | 138 | } 139 | {...rest} 140 | > 141 | {type === 0 && ( 142 | 149 | )} 150 | 151 | {type === 1 && ( 152 | 164 | )} 165 | {type === 2 && ( 166 | 177 | )} 178 | 179 | 180 | ); 181 | }; 182 | -------------------------------------------------------------------------------- /packages/playground/src/examples/ManualControl.tsx: -------------------------------------------------------------------------------- 1 | import React, { useImperativeHandle, useMemo, useRef } from 'react'; 2 | // import './styles.css'; 3 | import { PlayState, Timeline, Tween } from 'react-gsap'; 4 | import styled from 'styled-components'; 5 | 6 | // const CurriedForwardedTimelineTarget = React.forwardRef( 7 | // ({ text }: any, ref: any): JSX.Element => { 8 | // const Kekeeeeee = useRef(null); 9 | // useImperativeHandle(ref, () => ({ 10 | // Kekeeeeee, 11 | // })); 12 | // 13 | // return ( 14 | //
15 | //

{text}

16 | //
17 | // ); 18 | // } 19 | // ); 20 | 21 | const CurriedForwardedTimelineTarget: ({ 22 | text, 23 | }: { 24 | text: string; 25 | }) => React.ForwardRefExoticComponent> = ({ text }: { text: string }) => 26 | React.forwardRef( 27 | (_props: any, ref: any): JSX.Element => { 28 | const Kekeeeeee = useRef(null); 29 | useImperativeHandle(ref, () => ({ 30 | Kekeeeeee, 31 | })); 32 | return ( 33 |
34 |

{text}

35 |
36 | ); 37 | } 38 | ); 39 | 40 | const StyledH2 = styled.h2` 41 | color: red; 42 | `; 43 | 44 | export function InlineTimeline({ position }: { position: string | number }) { 45 | let h2Ref: React.MutableRefObject = React.useRef(null); 46 | 47 | return ( 48 | 49 |
50 | 51 | Sliiiide Inline 52 | 53 |
54 |
55 | ); 56 | } 57 | 58 | const ForwardedTimelineTarget: React.ForwardRefExoticComponent> = React.forwardRef( 61 | (_props: any, ref: any): JSX.Element => { 62 | const Sliiiide = useRef(null); 63 | useImperativeHandle(ref, () => ({ 64 | Sliiiide, 65 | })); 66 | return ( 67 |
68 |

Sliiiide Forwarded

69 |
70 | ); 71 | } 72 | ); 73 | 74 | export function ForwardedTimeline({ position }: { position: string | number }) { 75 | return ( 76 | <> 77 | } 79 | position={position} 80 | // playState={playing ? PlayState.play : PlayState.pause} 81 | // totalProgress={progress} 82 | // paused={true} 83 | > 84 | 91 | 92 | 93 | 94 | 95 | ); 96 | } 97 | 98 | export function CurriedForwardedTimeline({ 99 | position, 100 | text, 101 | totalProgress, 102 | }: { 103 | position: string | number; 104 | text: string; 105 | totalProgress?: number; 106 | }) { 107 | const CurriedForwardedTimelineTargetMarkup: React.ForwardRefExoticComponent> = useMemo(() => CurriedForwardedTimelineTarget({ text }), [text]); 110 | 111 | // const CurriedForwardedTimelineTargetMarkup: React.ForwardRefExoticComponent> = CurriedForwardedTimelineTarget({ text }); 114 | 115 | return ( 116 | <> 117 | } 119 | target={} 120 | position={position} 121 | // playState={playing ? PlayState.play : PlayState.pause} 122 | // totalProgress={totalProgress} 123 | // paused={true} 124 | > 125 | 132 | 133 | 134 | 135 | 136 | ); 137 | } 138 | 139 | export default function App() { 140 | const [playState, setPlayState] = React.useState(PlayState.pause); 141 | const [progress, setProgress] = React.useState(0); 142 | 143 | return ( 144 | <> 145 | {/**/} 146 | 147 |
Tween
148 |
149 | 159 |
160 | {/**/} 161 | {/**/} 162 | 163 |
164 |
165 | {/*
*/} 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 |
175 | setProgress(parseFloat(event.target.value))} 182 | /> 183 |
184 | 185 | ); 186 | } 187 | -------------------------------------------------------------------------------- /packages/playground/src/examples/Reveal.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Reveal } from 'react-gsap'; 4 | import { 5 | FadeIn, 6 | FadeInLeft, 7 | FadeInLeftChars, 8 | FadeInLeftWords, 9 | RubberBand, 10 | CutText, 11 | } from './Animation'; 12 | 13 | const RevealStyled = styled.div` 14 | padding-top: 1000px; 15 | padding-bottom: 1000px; 16 | overflow: hidden; 17 | 18 | text-align: center; 19 | font-family: arial; 20 | font-size: 80px; 21 | 22 | h1 { 23 | font-size: 80px; 24 | font-weight: normal; 25 | margin: 0; 26 | padding: 60px 0; 27 | } 28 | 29 | svg { 30 | padding: 60px 0; 31 | } 32 | `; 33 | 34 | const Wrapper = forwardRef((props, ref: any) =>
{props.children}
); 35 | 36 | const RevealComponent = () => ( 37 | 38 | 39 | 40 |

REACT-GSAP

41 |
42 |
43 | }> 44 | 45 |

AIIIIIIGHT

46 |
47 |
48 | 49 | 50 |

ONE MORE

51 |
52 |
53 | }> 54 | }> 55 | SPLIT IT UP 56 | 57 | 58 | }> 59 | }> 60 | SPLIT IT UP 61 | 62 | 63 | }> 64 | 65 | CUT ME PLEASE 66 | 67 | 68 | }> 69 | 70 | CUT ME PLEASE 71 | 72 | 73 | }> 74 | 75 | CUT ME PLEASE 76 | 77 | 78 |
79 | ); 80 | 81 | export default RevealComponent; 82 | -------------------------------------------------------------------------------- /packages/playground/src/examples/ScrollTrigger.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | forwardRef, 3 | MutableRefObject, 4 | useEffect, 5 | useImperativeHandle, 6 | useRef, 7 | useState, 8 | } from 'react'; 9 | import styled from 'styled-components'; 10 | import { 11 | Tween, 12 | Timeline, 13 | SplitWords, 14 | SplitLetters, 15 | Controls, 16 | ScrollTrigger, 17 | PlayState, 18 | SplitChars, 19 | } from 'react-gsap'; 20 | 21 | const Container = styled.div` 22 | width: 100%; 23 | height: 100vh; 24 | position: relative; 25 | `; 26 | 27 | const Square = styled.div` 28 | background-color: red; 29 | width: 100px; 30 | height: 100px; 31 | position: relative; 32 | `; 33 | 34 | const TweenStyled = styled.div` 35 | padding: 2000px 0; 36 | `; 37 | 38 | const FadeIn = ({ children }: { children: React.ReactNode }) => ( 39 | {children} 40 | ); 41 | 42 | const TargetWithNames = forwardRef((props, ref: any) => { 43 | const div1 = useRef(null); 44 | const div2 = useRef[]>([]); 45 | const div3 = useRef(null); 46 | const trigger = useRef(null); 47 | useImperativeHandle(ref, () => ({ 48 | div1, 49 | div2, 50 | div3, 51 | trigger, 52 | })); 53 | return ( 54 |
55 |
first
56 | ) => div2.current.push(charRef)} 58 | wrapper={} 59 | > 60 | second 61 | 62 |
third
63 |
64 | ); 65 | }); 66 | 67 | const TweenComponent = () => { 68 | const triggerRef = useRef(null); 69 | const [trigger, setTrigger] = useState(triggerRef.current); 70 | 71 | useEffect(() => { 72 | setTrigger(triggerRef.current); 73 | }, []); 74 | 75 | return ( 76 | <> 77 | 78 | 79 | 84 | This element gets tweened 85 | 86 | 87 | 88 | 89 | 94 | This element gets tweened by ref 95 | 96 | 97 | 98 | 106 | 111 | 112 | This element is the trigger 113 | 114 | 115 | 116 | 117 | 118 | 123 | This element gets tweened 124 | 125 | 130 | This element gets tweened 131 | 132 | 133 | 134 | 142 | }> 143 | 149 | 156 | 162 | 163 | 164 | 165 | 166 | 167 | {/**/} 168 | {/* */} 181 | {/* This element gets tweened*/} 182 | {/* */} 183 | {/**/} 184 | 185 | 186 | ); 187 | }; 188 | 189 | export default TweenComponent; 190 | -------------------------------------------------------------------------------- /packages/playground/src/examples/Scroller.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Fragment, 3 | ReactComponentElement, 4 | ReactHTMLElement, 5 | useEffect, 6 | useRef, 7 | useState, 8 | } from 'react'; 9 | import styled from 'styled-components'; 10 | import { Tween, Timeline, SplitChars, SplitWords } from 'react-gsap'; 11 | // import { Scroller } from 'react-gsap'; 12 | import { gsap } from 'gsap'; 13 | import { CutText } from './Animation'; 14 | 15 | const RevealStyled = styled.div` 16 | overflow: hidden; 17 | margin-bottom: 100vh; 18 | margin-top: 100vh; 19 | 20 | text-align: center; 21 | font-family: arial; 22 | font-size: 80px; 23 | 24 | h1 { 25 | font-size: 80px; 26 | font-weight: normal; 27 | margin: 0; 28 | padding: 60px 0; 29 | } 30 | 31 | svg { 32 | padding: 60px 0; 33 | } 34 | 35 | .fixed { 36 | position: fixed; 37 | top: 0; 38 | bottom: 0; 39 | left: 0; 40 | right: 0; 41 | display: flex; 42 | justify-content: center; 43 | align-items: center; 44 | } 45 | `; 46 | 47 | /* 48 |
0.3 && progress < 0.7 ? 'fixed' : undefined}> 49 | 50 | SCROLLREVEAL RULES 51 | 52 | {progress} 53 |
54 | */ 55 | 56 | const ScrollerComponent = () => ( 57 | 58 | {/* 59 | {(progress: number) =>
{progress}
} 60 |
*/} 61 | {/* 62 | 63 | {(progress: number) => ( 64 | <> 65 | {progress >= 0 && ( 66 |
67 | 68 | SCROLLREVEAL RULES 69 | 70 |
71 | )} 72 | 73 | )} 74 |
75 | */} 76 |
77 | ); 78 | 79 | export default ScrollerComponent; 80 | -------------------------------------------------------------------------------- /packages/playground/src/examples/Svg.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Tween, Timeline, Controls } from 'react-gsap'; 4 | 5 | const SvgStyled = styled.div``; 6 | 7 | const SvgComponent = () => ( 8 | 9 | Play with these example on{' '} 10 | 11 | StackBlitz.io 12 | 13 |
SvgDraw PlugIn
14 | 15 | } 17 | from={{ 18 | svgDraw: 0, 19 | }} 20 | to={{ 21 | svgDraw: 1, 22 | }} 23 | duration={2} 24 | > 25 | 34 | 45 | 46 | 47 | 48 | 49 | 50 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 77 | } 78 | target={ 79 | 80 | } 81 | duration={2} 82 | > 83 | 92 | 98 | 103 | 108 | 113 | 118 | 119 | 120 | 121 | 130 | } 131 | from={{ 132 | svgDraw: 0, 133 | }} 134 | to={{ 135 | svgDraw: 1, 136 | }} 137 | duration={5} 138 | > 139 | 145 | 146 | 147 |
148 | ); 149 | 150 | export default SvgComponent; 151 | -------------------------------------------------------------------------------- /packages/playground/src/examples/Timeline.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | forwardRef, 3 | Fragment, 4 | useRef, 5 | useImperativeHandle, 6 | ReactElement, 7 | ReactHTMLElement, 8 | useCallback, 9 | useEffect, 10 | MutableRefObject, 11 | } from 'react'; 12 | import styled from 'styled-components'; 13 | import { Tween, Timeline, SplitWords, SplitChars, Controls, PlayState } from 'react-gsap'; 14 | 15 | const TimelineStyled = styled.div``; 16 | 17 | const StyledTarget1 = styled.div` 18 | height: 200px; 19 | background-color: #accef7; 20 | `; 21 | 22 | const StyledTarget2 = styled.div` 23 | height: 50px; 24 | background-color: #ff4757; 25 | padding: 50px; 26 | `; 27 | 28 | const Inline = styled.div` 29 | display: inline-block; 30 | font-size: 40px; 31 | `; 32 | 33 | const TimelinePlayState = () => { 34 | const [playing, setPlaying] = React.useState(false); 35 | const [progress, setProgress] = React.useState(0); 36 | 37 | return ( 38 | <> 39 | } 41 | playState={playing ? PlayState.play : PlayState.pause} 42 | totalProgress={progress} 43 | paused={false} 44 | > 45 | 46 | 47 | 48 | 49 | 50 |
51 | setProgress(parseFloat(event.target.value))} 58 | /> 59 |
60 | 61 | ); 62 | }; 63 | 64 | const TimelineComponent = () => ( 65 | 66 | Play with these example on{' '} 67 | 68 | StackBlitz.io 69 | 70 |
Nested Timeline
71 | 72 | 83 | } 84 | target={ 85 | 86 | 89 | } 90 | > 91 | This is a Timeline 92 | 93 | 94 | } 95 | repeat={0} 96 | > 97 | 104 | 105 | 112 | } 114 | target={ 115 | 116 | }> 117 | AIIIIGHHT 118 | 119 | 120 | } 121 | labels={[ 122 | { 123 | label: 'sec4', 124 | position: 4, 125 | }, 126 | { 127 | label: 'sec6', 128 | position: 6, 129 | }, 130 | ]} 131 | > 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 |
141 | ); 142 | 143 | const TargetWithNames = forwardRef((props, ref: any) => { 144 | const div1 = useRef(null); 145 | const div2 = useRef[]>([]); 146 | const div3 = useRef(null); 147 | useImperativeHandle(ref, () => ({ 148 | div1, 149 | div2, 150 | div3, 151 | })); 152 | return ( 153 |
154 |
first
155 | ) => div2.current.push(charRef)} 157 | wrapper={} 158 | > 159 | second 160 | 161 |
third
162 |
163 | ); 164 | }); 165 | 166 | const TargetWithNames2 = forwardRef((props, ref: any) => { 167 | const div4 = useRef(null); 168 | const div5 = useRef[]>([]); 169 | const div6 = useRef(null); 170 | useImperativeHandle(ref, () => ({ 171 | div4, 172 | div5, 173 | div6, 174 | })); 175 | return ( 176 |
177 |
first
178 | ) => div5.current.push(charRef)} 180 | wrapper={} 181 | > 182 | second 183 | 184 |
third
185 |
186 | ); 187 | }); 188 | 189 | const TargetWithNamesCombined = forwardRef((props, ref: any) => { 190 | const target1 = useRef({}); 191 | const target2 = useRef({}); 192 | useImperativeHandle(ref, () => ({ 193 | ...target1.current, 194 | ...target2.current, 195 | })); 196 | return ( 197 | <> 198 | 199 | 200 | 201 | ); 202 | }); 203 | 204 | const TimelineTargets = () => { 205 | return ( 206 | }> 207 | 208 | 209 | 210 | 211 | ); 212 | }; 213 | 214 | //export default TimelineTargets; 215 | 216 | const ForwardRefComponent = forwardRef(({ children }, ref: any) => { 217 | return ( 218 |
219 | 220 | {children} 221 | 222 |
223 | ); 224 | }); 225 | 226 | const Component = forwardRef(({ children }, ref?) => { 227 | const div1 = useRef(null); 228 | const div2 = useRef(null); 229 | useImperativeHandle(ref, () => ({ 230 | div1, 231 | div2, 232 | test: () => { 233 | console.log('run test'); 234 | }, 235 | })); 236 | return ( 237 |
238 |
239 | {children} 240 |
241 |
Div 2
242 |
243 | ); 244 | }); 245 | 246 | const AnimatedComponent = ({ children, ...props }) => { 247 | const component = useRef(null); 248 | 249 | useEffect(() => { 250 | console.log('component', component); 251 | }, []); 252 | 253 | return ( 254 | 257 | {children} 258 | 259 | } 260 | > 261 | 262 | 263 | 264 | ); 265 | }; 266 | 267 | // const Out = () => { 268 | // return ( 269 | // <> 270 | // Not animated 271 | // Animated 272 | // 273 | // ); 274 | // }; 275 | 276 | const Out = () => { 277 | const divRef1 = useCallback(ref => { 278 | if (ref !== null) { 279 | // Ref never updates 280 | // console.log(ref); 281 | } 282 | }, []); 283 | 284 | const divRef2 = useRef(null); 285 | 286 | useEffect(() => { 287 | // Ref never updates 288 | // console.log(divRef2.current); 289 | }, []); 290 | 291 | return ( 292 |
293 | 296 | // 297 | // 298 | // 299 | // 300 | // 301 | // 302 | // 303 | // } 304 | // target={
} 305 | // target={} 306 | target={ 307 | <> 308 | 309 | 310 | 311 | } 312 | // target={} 313 | // target={ 314 | // <> 315 | // 316 | // ForwardRefComponent 1 317 | // ForwardRefComponent 2 318 | // 319 | // } 320 | > 321 | {/**/} 322 | {/*
*/} 323 | {/**/} 324 | 325 | {/**/} 326 | {/*
*/} 327 | {/**/} 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | {/**/} 338 | {/**/} 339 | 340 |
341 | ); 342 | }; 343 | 344 | //export default Out; 345 | 346 | const Test = () => { 347 | // the array gets filled up with every new render! 348 | // can SplitWords outputs it's refs as array, so that we don't need to push into? 349 | const ref = useRef[]>([]); 350 | 351 | useEffect(() => { 352 | console.log(ref); 353 | }, []); 354 | 355 | return ( 356 | 357 | 360 | ref.current.push(charRef)} wrapper={}> 361 | This text gets splitted by words. 362 | 363 | 364 | } 365 | > 366 | 367 | 368 | 369 | 370 | 371 | ); 372 | }; 373 | 374 | export default Test; 375 | -------------------------------------------------------------------------------- /packages/playground/src/examples/Transition.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Tween, Timeline, PlayState } from 'react-gsap'; 4 | import { Transition, TransitionGroup } from 'react-transition-group'; 5 | import { v4 as uuid } from 'uuid'; 6 | 7 | const Item = styled.div` 8 | background-color: #f0f0f0; 9 | border: 1px solid #999; 10 | margin-bottom: 5px; 11 | padding: 10px; 12 | cursor: pointer; 13 | `; 14 | 15 | const TransitionItem = ({ 16 | children, 17 | onClick, 18 | ...props 19 | }: { 20 | children: React.ReactNode; 21 | onClick: Function; 22 | }) => ( 23 | 24 | {state => ( 25 |
26 | 46 | onClick()}> 47 | {children} {state} 48 | 49 | 50 |
51 | )} 52 |
53 | ); 54 | 55 | const TransitionStyled = styled.div` 56 | button { 57 | margin-bottom: 30px; 58 | } 59 | 60 | span { 61 | margin-left: 30px; 62 | } 63 | `; 64 | 65 | type State = { 66 | items: { id: string; text: string | number }[]; 67 | }; 68 | 69 | class TransitionComponent extends React.Component<{}, State> { 70 | state = { 71 | items: [{ id: uuid(), text: '0' }], 72 | }; 73 | 74 | removeItem(id: string) { 75 | this.setState(state => ({ 76 | items: state.items.filter(item => item.id !== id), 77 | })); 78 | } 79 | 80 | render() { 81 | const { items } = this.state; 82 | 83 | return ( 84 | 85 | Play with these example on{' '} 86 | 87 | StackBlitz.io 88 | 89 |
Mount/Unmount Animation - built with React Transition Group
90 | 100 | Click on item to remove 101 | 102 | {items.map(({ id, text }) => ( 103 | this.removeItem(id)}> 104 | {id} 105 | 106 | ))} 107 | 108 |
109 | ); 110 | } 111 | } 112 | 113 | export default TransitionComponent; 114 | -------------------------------------------------------------------------------- /packages/playground/src/examples/Tween.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Tween, SplitWords, SplitLetters, Controls } from 'react-gsap'; 4 | 5 | const Square = styled.div` 6 | background-color: red; 7 | width: 100px; 8 | height: 100px; 9 | position: relative; 10 | `; 11 | 12 | const TweenStyled = styled.div``; 13 | 14 | const FadeIn = ({ children }: { children: React.ReactNode }) => ( 15 | {children} 16 | ); 17 | 18 | const TweenComponent = () => ( 19 | 20 | Play with these examples on{' '} 21 | 22 | StackBlitz.io 23 | 24 |
Basic tween
25 | 26 | 27 |
This element gets tweened
28 |
29 |
30 |
FadeIn component
31 | 32 |
FadeIn Test
33 |
34 |
SplitWords component
35 | 36 | 37 | }> 38 | This is a Test 39 | 40 | 41 | 42 |
SplitLetters component + staggerFrom + ease
43 | 44 | 45 | }> 46 | This is a Test 47 | 48 | 49 | 50 |
Nice list animation
51 | 52 | } 54 | from={{ 55 | opacity: 0, 56 | rotationX: (index: number) => (index % 2 ? -90 : 90), 57 | transformOrigin: (index: number) => (index % 2 ? '50% top -100' : '50% bottom 100'), 58 | }} 59 | duration={1} 60 | stagger={0.1} 61 | > 62 |
  • Rich Harris
  • 63 |
  • Dan Abramov
  • 64 |
  • Kyle Simpson
  • 65 |
  • Gregory Brown
  • 66 |
  • Addy Osmani
  • 67 |
  • Evan You
  • 68 |
  • Axel Rauschmayer
  • 69 |
  • Sarah Drasner
  • 70 |
  • André Staltz
  • 71 |
    72 |
    73 |
    Styled components
    74 | 75 | 84 | 85 | 86 | 87 | 88 |
    89 | ); 90 | 91 | export default TweenComponent; 92 | -------------------------------------------------------------------------------- /packages/playground/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import * as serviceWorker from './serviceWorker'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | 13 | // If you want your app to work offline and load faster, you can change 14 | // unregister() to register() below. Note this comes with some pitfalls. 15 | // Learn more about service workers: https://bit.ly/CRA-PWA 16 | serviceWorker.unregister(); 17 | -------------------------------------------------------------------------------- /packages/playground/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/playground/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready 142 | .then(registration => { 143 | registration.unregister(); 144 | }) 145 | .catch(error => { 146 | console.error(error.message); 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /packages/playground/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /packages/playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react", 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /packages/react-gsap/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "@babel/plugin-transform-typescript", 5 | { 6 | "allowDeclareFields": true 7 | } 8 | ] 9 | ] 10 | } -------------------------------------------------------------------------------- /packages/react-gsap/.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | 7 | steps: 8 | - name: Begin CI... 9 | uses: actions/checkout@v2 10 | 11 | - name: Use Node 12 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: 12.x 15 | 16 | - name: Use cached node_modules 17 | uses: actions/cache@v1 18 | with: 19 | path: node_modules 20 | key: nodeModules-${{ hashFiles('**/yarn.lock') }} 21 | restore-keys: | 22 | nodeModules- 23 | 24 | - name: Install dependencies 25 | run: yarn install --frozen-lockfile 26 | env: 27 | CI: true 28 | 29 | - name: Lint 30 | run: yarn lint 31 | env: 32 | CI: true 33 | 34 | - name: Test 35 | run: yarn test --ci --coverage --maxWorkers=2 36 | env: 37 | CI: true 38 | 39 | - name: Build 40 | run: yarn build 41 | env: 42 | CI: true 43 | -------------------------------------------------------------------------------- /packages/react-gsap/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | dist 6 | -------------------------------------------------------------------------------- /packages/react-gsap/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jan Fischer 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. -------------------------------------------------------------------------------- /packages/react-gsap/README.md: -------------------------------------------------------------------------------- 1 | # react-gsap 2 | 3 | > React components for GSAP 4 | 5 | [![NPM](https://img.shields.io/npm/v/react-gsap.svg)](https://www.npmjs.com/package/react-gsap) 6 | ![npm type definitions](https://img.shields.io/npm/types/react-gsap) 7 | ![npm bundle size (version)](https://img.shields.io/bundlephobia/minzip/react-gsap/^2) 8 | 9 | # Introduction 10 | 11 | `react-gsap` lets you use the GreenSock Animation Platform (GSAP) in React in a fully declarative way. 12 | It abstracts away the direct use of the GSAP [Tween](https://greensock.com/docs/v3/GSAP/Tween) and [Timeline](https://greensock.com/docs/v3/GSAP/Timeline) functions. 13 | 14 | If you need the full control it's possible by getting low level access to the underlying objects. 15 | 16 | In addition to that it ships some GSAP Plugins and useful helper components. 17 | 18 | From version 2 on it's build for GSAP 3 and only has `gsap` as a peer dependency. In this way you can update `gsap` separately from `react-gsap`. 19 | 20 | It's built with TypeScript and ships the types directly in the package. 21 | 22 | Documentation and examples are here: https://bitworking.github.io/react-gsap/ 23 | 24 | ##### The examples on the documentation pages are all editable directly in the browser. So play with it! 25 | 26 | ## Installation 27 | 28 | ```bash 29 | npm install gsap react-gsap 30 | ``` 31 | 32 | ## About GSAP 33 | 34 | GreenSock Animation Platform (GSAP) is a set of some JavaScript functions which let you tween a value/attribute/css property over time and insert these tweens into a timeline for more complex animations. 35 | 36 | `react-gsap` just adds some React wrapper components for these functions, so also read the official GreenSock documentation to know how to do things: 37 | 38 | [GreenSock Docs](https://greensock.com/docs/) 39 | 40 | 41 | ## License 42 | 43 | MIT © [bitworking](https://github.com/bitworking) 44 | -------------------------------------------------------------------------------- /packages/react-gsap/example/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | dist -------------------------------------------------------------------------------- /packages/react-gsap/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Playground 8 | 9 | 10 | 11 |
    12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/react-gsap/example/index.tsx: -------------------------------------------------------------------------------- 1 | import 'react-app-polyfill/ie11'; 2 | import * as React from 'react'; 3 | import * as ReactDOM from 'react-dom'; 4 | import { Thing } from '../.'; 5 | 6 | const App = () => { 7 | return ( 8 |
    9 | 10 |
    11 | ); 12 | }; 13 | 14 | ReactDOM.render(, document.getElementById('root')); 15 | -------------------------------------------------------------------------------- /packages/react-gsap/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "parcel index.html", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "react-app-polyfill": "^1.0.0" 12 | }, 13 | "alias": { 14 | "react": "../node_modules/react", 15 | "react-dom": "../node_modules/react-dom/profiling", 16 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^16.9.11", 20 | "@types/react-dom": "^16.8.4", 21 | "parcel": "^1.12.3", 22 | "typescript": "^3.4.5" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/react-gsap/example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": false, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "jsx": "react", 7 | "moduleResolution": "node", 8 | "noImplicitAny": false, 9 | "noUnusedLocals": false, 10 | "noUnusedParameters": false, 11 | "removeComments": true, 12 | "strictNullChecks": true, 13 | "preserveConstEnums": true, 14 | "sourceMap": true, 15 | "lib": ["es2015", "es2016", "dom"], 16 | "baseUrl": ".", 17 | "types": ["node"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/react-gsap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-gsap", 3 | "version": "3.3.0", 4 | "description": "React components for GSAP", 5 | "author": "bitworking", 6 | "license": "MIT", 7 | "repository": "bitworking/react-gsap", 8 | "main": "dist/index.js", 9 | "typings": "dist/index.d.ts", 10 | "module": "dist/react-gsap.esm.js", 11 | "files": [ 12 | "dist", 13 | "src" 14 | ], 15 | "engines": { 16 | "node": ">=10" 17 | }, 18 | "scripts": { 19 | "start": "tsdx watch", 20 | "build": "tsdx build --format cjs,esm,umd", 21 | "test": "tsdx test --passWithNoTests", 22 | "lint": "tsdx lint", 23 | "prepare": "yarn build", 24 | "typecheck": "tsc --project tsconfig.json -noEmit" 25 | }, 26 | "peerDependencies": { 27 | "gsap": ">=3", 28 | "react": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0", 29 | "react-dom": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0" 30 | }, 31 | "husky": { 32 | "hooks": { 33 | "pre-commit": "tsdx lint" 34 | } 35 | }, 36 | "prettier": { 37 | "printWidth": 100, 38 | "tabWidth": 2, 39 | "trailingComma": "es5", 40 | "semi": true, 41 | "singleQuote": true, 42 | "endOfLine": "lf" 43 | }, 44 | "devDependencies": { 45 | "@babel/plugin-transform-typescript": "^7.19.1", 46 | "@types/react": "^18.0.20", 47 | "@types/react-dom": "^18.0.6", 48 | "@types/react-is": "^17.0.3", 49 | "gsap": "^3.11.1", 50 | "husky": "^8.0.1", 51 | "react": "^18.2.0", 52 | "react-dom": "^18.2.0", 53 | "tsdx": "^0.14.1", 54 | "tslib": "^2.4.0", 55 | "typescript": "^4.8.3" 56 | }, 57 | "dependencies": { 58 | "react-is": "^18.2.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/react-gsap/src/Provider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type RegisteredPlugins = 'scrollTrigger'; 4 | type Plugin = (targets: any) => any; 5 | type Plugins = { [key in RegisteredPlugins]: Plugin } | {}; 6 | 7 | export type ContextProps = { 8 | registerConsumer: (consumer: any) => void; 9 | plugins?: Plugins; 10 | getPlugins: (plugins?: Plugins, targets?: any) => any; 11 | }; 12 | 13 | export const Context = React.createContext({ 14 | registerConsumer: () => {}, 15 | getPlugins: () => {}, 16 | plugins: {}, 17 | }); 18 | 19 | abstract class Provider extends React.Component { 20 | static contextType = Context; 21 | declare context: React.ContextType; 22 | 23 | consumers: any[]; 24 | plugins?: Plugins; 25 | 26 | constructor(props: T) { 27 | super(props); 28 | this.consumers = []; 29 | this.plugins = {}; 30 | 31 | this.registerConsumer = this.registerConsumer.bind(this); 32 | this.getContextValue = this.getContextValue.bind(this); 33 | this.getPlugin = this.getPlugin.bind(this); 34 | this.getPlugins = this.getPlugins.bind(this); 35 | this.renderWithProvider = this.renderWithProvider.bind(this); 36 | } 37 | 38 | registerConsumer(consumer: any) { 39 | this.consumers.push(consumer); 40 | } 41 | 42 | getContextValue(plugin: Plugins = {}) { 43 | return { 44 | registerConsumer: this.registerConsumer, 45 | // plugins: { ...this.context.plugins, ...plugin }, 46 | plugins: plugin, 47 | getPlugins: this.getPlugins, 48 | }; 49 | } 50 | 51 | getPlugin(props: any, targets: any) { 52 | return {}; 53 | } 54 | 55 | getPlugins(plugins?: Plugins, targets?: any) { 56 | return Object.keys(plugins ?? {}).reduce((acc, plugin) => { 57 | if (Object.prototype.hasOwnProperty.call(plugins, plugin)) { 58 | // @ts-ignore 59 | return { ...acc, [plugin]: this.getPlugin(plugins[plugin], targets) }; 60 | } 61 | return acc; 62 | }, {}); 63 | } 64 | 65 | renderWithProvider(output: any, plugin?: Plugins) { 66 | return {output}; 67 | } 68 | } 69 | 70 | export default Provider; 71 | -------------------------------------------------------------------------------- /packages/react-gsap/src/Timeline.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, ReactElement, ReactNode } from 'react'; 2 | import { gsap } from 'gsap'; 3 | import { isForwardRef, isFragment } from 'react-is'; 4 | import { PlayState } from './types'; 5 | import { 6 | getInitialPaused, 7 | getTargetRefProp, 8 | getTweenFunction, 9 | nullishCoalescing, 10 | setInitialPlayState, 11 | setPlayState, 12 | setProps, 13 | } from './helper'; 14 | import Provider, { Context } from './Provider'; 15 | import { TweenProps } from './Tween'; 16 | 17 | type Label = { 18 | label: string; 19 | position: string | number; 20 | }; 21 | 22 | export type Targets = Map; 23 | export type TargetsRef = { 24 | set: (key: string, target: any) => void; 25 | }; 26 | 27 | export type Target = ReactElement | null; 28 | 29 | export type TimelineProps = { 30 | children: ReactNode; 31 | wrapper?: ReactElement; 32 | target?: Target; 33 | position?: string | number; 34 | labels?: Label[]; 35 | 36 | duration?: number; 37 | progress?: number; 38 | totalProgress?: number; 39 | playState?: PlayState; 40 | 41 | [prop: string]: any; 42 | }; 43 | 44 | class Timeline extends Provider { 45 | static displayName = 'Timeline'; 46 | 47 | timeline: any; 48 | targets: Targets = new Map(); 49 | 50 | static defaultProps = { 51 | playState: PlayState.play, 52 | }; 53 | 54 | constructor(props: TimelineProps) { 55 | super(props); 56 | 57 | this.addTarget = this.addTarget.bind(this); 58 | this.setTarget = this.setTarget.bind(this); 59 | } 60 | 61 | componentDidMount() { 62 | this.createTimeline(); 63 | 64 | // props at mount 65 | setProps(this.timeline, this.props); 66 | setInitialPlayState(this.timeline, this.props); 67 | 68 | this.context.registerConsumer(this); 69 | } 70 | 71 | componentWillUnmount() { 72 | this.timeline.kill(); 73 | } 74 | 75 | getSnapshotBeforeUpdate() { 76 | this.targets = new Map(); 77 | return null; 78 | } 79 | 80 | componentDidUpdate(prevProps: TimelineProps) { 81 | const { children, duration, progress, totalProgress, playState, target } = this.props; 82 | 83 | // if children change create a new timeline 84 | // TODO: replace easy length check with fast equal check 85 | // TODO: same for props.target? 86 | if (React.Children.count(prevProps.children) !== React.Children.count(children)) { 87 | this.createTimeline(); 88 | } 89 | 90 | // execute function calls 91 | setProps(this.timeline, this.props, prevProps); 92 | 93 | // TODO: need rerender or something if target change? 94 | // if (target !== prevProps.target) { 95 | // this.forceUpdate(); 96 | // } 97 | 98 | setPlayState(playState, prevProps.playState, this.timeline); 99 | } 100 | 101 | createTimeline() { 102 | const { 103 | children, 104 | target, 105 | duration, 106 | progress, 107 | totalProgress, 108 | playState, 109 | labels, 110 | position, 111 | ...vars 112 | } = this.props; 113 | 114 | if (this.timeline) { 115 | this.timeline.kill(); 116 | } 117 | 118 | const plugins = this.context?.getPlugins(this.context?.plugins, this.targets) ?? {}; 119 | 120 | // init timeline 121 | this.timeline = gsap.timeline({ 122 | smoothChildTiming: true, 123 | paused: getInitialPaused(playState), 124 | ...vars, 125 | ...plugins, 126 | }); 127 | 128 | if (labels) { 129 | labels.forEach(label => { 130 | this.timeline.addLabel(label.label, label.position); 131 | }); 132 | } 133 | 134 | // add tweens or nested timelines to timeline 135 | this.consumers.forEach(consumer => { 136 | // Tween with no children -> control Timeline target 137 | if (consumer.tween && !consumer.props.children) { 138 | const { position, target, stagger, ...vars } = consumer.props as TweenProps; 139 | 140 | // get target if not nullish 141 | let targets = null; 142 | if (target !== null && typeof target !== 'undefined') { 143 | targets = this.targets.get(target); 144 | } 145 | 146 | // if no target found -> take all Timeline targets as target 147 | const tween = getTweenFunction( 148 | // @ts-ignore 149 | nullishCoalescing(targets, Array.from(this.targets.values())), 150 | { 151 | stagger, 152 | ...vars, 153 | } 154 | ); 155 | this.timeline.add(tween, nullishCoalescing(position, '+=0')); 156 | consumer.setGSAP(tween); 157 | } else { 158 | const { position } = consumer.props; 159 | this.timeline.add(consumer.getGSAP(), nullishCoalescing(position, '+=0')); 160 | } 161 | }); 162 | } 163 | 164 | getGSAP() { 165 | return this.timeline; 166 | } 167 | 168 | addTarget(target: any) { 169 | if (target !== null) { 170 | this.targets.set(this.targets.size, target); 171 | } 172 | } 173 | 174 | setTarget(key: string, target: any) { 175 | if (target !== null) { 176 | if (this.targets.has(key)) { 177 | const targets = this.targets.get(key); 178 | if (Array.isArray(targets)) { 179 | this.targets.set(key, [...targets, target]); 180 | } else { 181 | this.targets.set(key, [targets, target]); 182 | } 183 | } else { 184 | this.targets.set(key, target); 185 | } 186 | } 187 | } 188 | 189 | setTargets(targets: Targets) { 190 | this.targets = targets; 191 | } 192 | 193 | getTargets() { 194 | return this.targets; 195 | } 196 | 197 | cloneElement(child: any) { 198 | // @ts-ignore 199 | return React.cloneElement(child, getTargetRefProp(child, this.setTarget, this.addTarget)); 200 | } 201 | 202 | renderTarget(target?: Target): ReactNode { 203 | if (!target) { 204 | return null; 205 | } 206 | 207 | // if is forwardRef clone and pass targets as ref 208 | if (isForwardRef(target)) { 209 | return this.cloneElement(target); 210 | } 211 | 212 | // else iterate the first level of children and set targets 213 | return ( 214 | 215 | {/* First render the target */} 216 | {React.Children.map(target, child => { 217 | if (isFragment(child)) { 218 | return React.Children.map(child.props.children, fragmentChild => { 219 | return this.cloneElement(fragmentChild); 220 | }); 221 | } 222 | return this.cloneElement(child); 223 | })} 224 | 225 | ); 226 | } 227 | 228 | render() { 229 | let { target, children, wrapper } = this.props; 230 | 231 | const renderedTarget = this.renderTarget(target); 232 | 233 | let output = ( 234 | 235 | {renderedTarget} 236 | {children} 237 | 238 | ); 239 | 240 | if (wrapper) { 241 | output = React.cloneElement(wrapper, [], output); 242 | } 243 | 244 | return this.renderWithProvider(output); 245 | } 246 | } 247 | 248 | export default Timeline; 249 | -------------------------------------------------------------------------------- /packages/react-gsap/src/Tween.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, ReactElement } from 'react'; 2 | import { gsap } from 'gsap'; 3 | import { PlayState } from './types'; 4 | import { 5 | getTweenFunction, 6 | setPlayState, 7 | isEqual, 8 | getRefProp, 9 | setProps, 10 | setInitialPlayState, 11 | } from './helper'; 12 | import { Context } from './Provider'; 13 | 14 | import SvgDrawPlugin from './plugins/PlugInSvgDraw'; 15 | import CountPlugin from './plugins/PlugInCount'; 16 | 17 | // @ts-ignore 18 | gsap.registerPlugin(SvgDrawPlugin); 19 | 20 | // @ts-ignore 21 | gsap.registerPlugin(CountPlugin); 22 | 23 | type StaggerFunction = (index: number, target: any, list: any) => number; 24 | type StaggerFromValues = 'start' | 'center' | 'edges' | 'random' | 'end'; 25 | type EaseFunction = (value: number) => number; 26 | 27 | export type Stagger = 28 | | { 29 | amount?: number; 30 | each?: number; 31 | from?: StaggerFromValues | number | [number, number]; 32 | grid?: [number, number] | 'auto'; 33 | axis?: 'x' | 'y'; 34 | ease?: string | EaseFunction; 35 | repeat?: number; 36 | yoyo?: boolean; 37 | [prop: string]: any; 38 | } 39 | | number 40 | | StaggerFunction; 41 | 42 | export type TweenProps = { 43 | /** One or multiple "refable" components */ 44 | children?: React.ReactNode; 45 | wrapper?: React.ReactElement; 46 | target?: number | string; 47 | position?: string | number; 48 | 49 | from?: any; 50 | to?: any; 51 | stagger?: Stagger; 52 | 53 | duration?: number; 54 | progress?: number; 55 | totalProgress?: number; 56 | playState?: PlayState; 57 | 58 | disabled?: boolean; 59 | onlyInvalidateTo?: boolean; 60 | 61 | [prop: string]: any; 62 | }; 63 | 64 | class Tween extends React.Component { 65 | static displayName = 'Tween'; 66 | static contextType = Context; 67 | declare context: React.ContextType; 68 | 69 | tween: any; 70 | targets: any[] = []; 71 | 72 | constructor(props: TweenProps) { 73 | super(props); 74 | 75 | this.addTarget = this.addTarget.bind(this); 76 | } 77 | 78 | componentDidMount() { 79 | this.createTween(); 80 | 81 | // props at mount 82 | setProps(this.tween, this.props); 83 | setInitialPlayState(this.tween, this.props); 84 | 85 | this.context.registerConsumer(this); 86 | } 87 | 88 | componentWillUnmount() { 89 | if (this.tween) { 90 | this.tween.kill(); 91 | } 92 | } 93 | 94 | getSnapshotBeforeUpdate() { 95 | this.targets = []; 96 | return null; 97 | } 98 | 99 | componentDidUpdate(prevProps: TweenProps) { 100 | const { 101 | children, 102 | wrapper, 103 | 104 | duration, 105 | from, 106 | to, 107 | stagger, 108 | 109 | progress, 110 | totalProgress, 111 | playState, 112 | disabled, 113 | onlyInvalidateTo, 114 | 115 | onCompleteAll, 116 | onCompleteAllParams, 117 | onCompleteAllScope, 118 | onStartAll, 119 | 120 | position, 121 | target, 122 | 123 | ...vars 124 | } = this.props; 125 | 126 | // if children change create a new tween 127 | // TODO: replace easy length check with fast equal check 128 | if (React.Children.count(prevProps.children) !== React.Children.count(children)) { 129 | this.createTween(); 130 | } 131 | 132 | if (disabled) { 133 | return; 134 | } 135 | 136 | // execute function calls 137 | setProps(this.tween, this.props, prevProps); 138 | 139 | // if "to" props are changed: reinit and restart tween 140 | if (!isEqual(to, prevProps.to)) { 141 | // is Tween 142 | if (!this.tween.getChildren) { 143 | this.tween.vars = { ...to, ...vars }; 144 | 145 | if (onlyInvalidateTo) { 146 | var progressTmp = this.tween.progress(); 147 | this.tween 148 | .progress(0) 149 | .invalidate() 150 | .progress(progressTmp); 151 | } else { 152 | this.tween.invalidate(); 153 | } 154 | } 155 | // is Timeline 156 | // TODO: not yet ready 157 | else { 158 | let delay = 0; 159 | this.tween.getChildren(false, true, false).forEach((tween: any) => { 160 | tween.vars = { ...to, ...vars, ...{ delay } }; 161 | tween.invalidate(); 162 | // delay += stagger || 0; 163 | }); 164 | } 165 | 166 | if (!this.tween.paused()) { 167 | this.tween.restart(); 168 | } 169 | } 170 | 171 | setPlayState(playState, prevProps.playState, this.tween); 172 | } 173 | 174 | createTween() { 175 | if (this.tween) { 176 | this.tween.kill(); 177 | } 178 | 179 | if (this.props.children) { 180 | this.tween = getTweenFunction(this.targets, this.props, this.context); 181 | } else { 182 | // why this is needed? 183 | this.tween = () => {}; 184 | } 185 | } 186 | 187 | getGSAP() { 188 | return this.tween; 189 | } 190 | 191 | setGSAP(tween: any) { 192 | this.tween = tween; 193 | } 194 | 195 | addTarget(target: any) { 196 | // target is null at unmount 197 | if (target !== null) { 198 | this.targets.push(target); 199 | } 200 | } 201 | 202 | getTargets() { 203 | return this.targets; 204 | } 205 | 206 | render() { 207 | let { children, wrapper } = this.props; 208 | 209 | const output = ( 210 | 211 | {React.Children.map(children, child => { 212 | return React.cloneElement(child as ReactElement, getRefProp(child, this.addTarget)); 213 | })} 214 | 215 | ); 216 | 217 | if (wrapper) { 218 | return React.cloneElement(wrapper, [], output); 219 | } 220 | 221 | return output; 222 | } 223 | } 224 | 225 | export default Tween; 226 | -------------------------------------------------------------------------------- /packages/react-gsap/src/helper.ts: -------------------------------------------------------------------------------- 1 | import { gsap } from 'gsap'; 2 | import React from 'react'; 3 | import { PlayState } from './types'; 4 | import { TimelineProps } from 'Timeline'; 5 | import { TweenProps } from 'Tween'; 6 | import { ContextProps } from 'Provider'; 7 | 8 | if (!String.prototype.startsWith) { 9 | String.prototype.startsWith = function(searchString, position) { 10 | position = position || 0; 11 | return this.indexOf(searchString, position) === position; 12 | }; 13 | } 14 | 15 | const setPlayState = ( 16 | playState?: PlayState, 17 | prevPlayState?: PlayState | null, 18 | tween: any = null 19 | ) => { 20 | if (tween && playState && playState !== prevPlayState) { 21 | if (playState === PlayState.play) { 22 | tween.play(); 23 | } else if (playState === PlayState.restart) { 24 | tween.restart(true); 25 | } else if (playState === PlayState.reverse) { 26 | tween.reverse(); 27 | } else if (playState === PlayState.restartReverse) { 28 | tween.reverse(0); 29 | } else if (playState === PlayState.stop) { 30 | tween.pause(0); 31 | } else if (playState === PlayState.stopEnd) { 32 | tween.reverse(0); 33 | tween.pause(); 34 | } else if (playState === PlayState.pause) { 35 | tween.pause(); 36 | } else if (playState === PlayState.resume) { 37 | tween.resume(); 38 | } 39 | } 40 | }; 41 | 42 | const setInitialPlayState = (tweenOrTimeline: any, props: TimelineProps | TweenProps) => { 43 | const { playState } = props; 44 | if (playState) { 45 | setPlayState(playState, PlayState.play, tweenOrTimeline); 46 | } 47 | }; 48 | 49 | const getInitialPaused = (playState?: PlayState) => { 50 | return ( 51 | playState && 52 | (playState === PlayState.stop || 53 | playState === PlayState.stopEnd || 54 | playState === PlayState.pause) 55 | ); 56 | }; 57 | 58 | const getTweenFunction = ( 59 | targets: any, 60 | props: TweenProps | TimelineProps, 61 | context?: ContextProps 62 | ): gsap.core.Tween | gsap.core.Timeline => { 63 | const { 64 | children, 65 | wrapper, 66 | 67 | duration = 1, 68 | from, 69 | to, 70 | 71 | stagger, 72 | 73 | progress, 74 | totalProgress, 75 | playState, 76 | disabled, 77 | onlyInvalidateTo, 78 | 79 | onCompleteAll, 80 | onCompleteAllParams, 81 | onCompleteAllScope, 82 | onStartAll, 83 | 84 | position, 85 | target, 86 | 87 | ...vars 88 | } = props; 89 | 90 | let tweenFunction: gsap.core.Tween | gsap.core.Timeline; 91 | const paused = getInitialPaused(playState); 92 | const plugins = context?.getPlugins(context?.plugins, targets) ?? {}; 93 | 94 | if (from && to) { 95 | // special props like paused always go in the toVars parameter 96 | tweenFunction = gsap.fromTo(targets, from, { 97 | stagger, 98 | duration, 99 | paused, 100 | ...to, 101 | ...vars, 102 | ...plugins, 103 | }); 104 | } else if (to) { 105 | tweenFunction = gsap.to(targets, { stagger, duration, paused, ...to, ...vars, ...plugins }); 106 | } else { 107 | tweenFunction = gsap.from(targets, { stagger, duration, paused, ...from, ...vars, ...plugins }); 108 | } 109 | 110 | // if multiple tweens (stagger), wrap them in a timeline 111 | // TODO: if it's already an timeline add event handlers 112 | if (Array.isArray(tweenFunction)) { 113 | tweenFunction.forEach(t => { 114 | t.paused(false); 115 | }); 116 | tweenFunction = gsap.timeline({ 117 | ...vars, 118 | tweens: tweenFunction, 119 | smoothChildTiming: true, 120 | onComplete: onCompleteAll, 121 | onCompleteParams: onCompleteAllParams, 122 | onCompleteScope: onCompleteAllScope, 123 | onStart: onStartAll, 124 | }); 125 | } 126 | 127 | return tweenFunction; 128 | }; 129 | 130 | const callTweenFunction = ( 131 | tweenFunction: any, 132 | functionName: string, 133 | params: Array | undefined = undefined, 134 | returnFunction: string | undefined = undefined 135 | ): void => { 136 | if (Array.isArray(tweenFunction)) { 137 | tweenFunction.forEach(tween => { 138 | if (!params && returnFunction) { 139 | params = [tween[returnFunction].apply(tween)]; 140 | } 141 | tween[functionName].apply(tween, params); 142 | }); 143 | } else { 144 | if (!params && returnFunction) { 145 | params = [tweenFunction[returnFunction].apply(tweenFunction)]; 146 | } 147 | tweenFunction[functionName].apply(tweenFunction, params); 148 | } 149 | }; 150 | 151 | const isEqual = (obj1: any, obj2: any) => { 152 | // very easy equal check 153 | // attention: if the order of properties are different it returns false 154 | return JSON.stringify(obj1) === JSON.stringify(obj2); 155 | }; 156 | 157 | const refOrInnerRef = (child: any) => { 158 | if (child.type.$$typeof && child.type.$$typeof.toString() === 'Symbol(react.forward_ref)') { 159 | return 'ref'; 160 | } 161 | 162 | // styled-components < 4 163 | if (child.type.styledComponentId) { 164 | return 'innerRef'; 165 | } 166 | 167 | return 'ref'; 168 | }; 169 | 170 | function isElement(element: any) { 171 | return React.isValidElement(element); 172 | } 173 | 174 | function isDOMTypeElement(element: any) { 175 | return isElement(element) && typeof element.type === 'string'; 176 | } 177 | 178 | // https://stackoverflow.com/a/39165137 179 | function getReactNode(dom: any, traverseUp = 0) { 180 | const key = Object.keys(dom ?? {}).find( 181 | key => key.startsWith('__reactInternalInstance$') || key.startsWith('__reactFiber$') 182 | ); 183 | 184 | const domFiber = key && dom[key]; 185 | if (!domFiber) return null; 186 | 187 | // react <16 188 | if (domFiber._currentElement) { 189 | let compFiber = domFiber._currentElement._owner; 190 | for (let i = 0; i < traverseUp; i++) { 191 | compFiber = compFiber._currentElement._owner; 192 | } 193 | return compFiber._instance; 194 | } 195 | 196 | // react 16+ 197 | if (domFiber.stateNode) { 198 | return domFiber.stateNode; 199 | } 200 | 201 | const getCompFiber = (fiber: any) => { 202 | //return fiber._debugOwner; // this also works, but is __DEV__ only 203 | let parentFiber = fiber.return; 204 | while (typeof parentFiber.type == 'string') { 205 | parentFiber = parentFiber.return; 206 | } 207 | return parentFiber; 208 | }; 209 | let compFiber = getCompFiber(domFiber); 210 | for (let i = 0; i < traverseUp; i++) { 211 | compFiber = getCompFiber(compFiber); 212 | } 213 | return compFiber.stateNode; 214 | } 215 | 216 | const getRefProp = (child: any, addTarget: (target: any) => void) => { 217 | // has to be tested if it works, which lib does still use innerRef? 218 | if (child.props.innerRef) { 219 | return { 220 | innerRef: (target: any) => { 221 | addTarget(target); 222 | const { innerRef } = child.props; 223 | if (typeof innerRef === 'function') innerRef(target); 224 | else if (innerRef) innerRef.current = target; 225 | }, 226 | }; 227 | } 228 | 229 | return { 230 | ref: (target: any) => { 231 | addTarget(target); 232 | const { ref } = child; 233 | if (typeof ref === 'function') ref(target); 234 | else if (ref) ref.current = target; 235 | }, 236 | }; 237 | }; 238 | 239 | const setOrAddTarget = ( 240 | target: any, 241 | setTarget: (key: string, target: any) => void, 242 | addTarget: (target: any) => void 243 | ) => { 244 | const reactNode = getReactNode(target); 245 | 246 | if (reactNode) { 247 | addTarget(reactNode); 248 | } else if (target) { 249 | Object.keys(target).forEach(key => { 250 | const elementRef = target[key]; 251 | if (typeof elementRef === 'object' && elementRef.current) { 252 | if (Array.isArray(elementRef.current)) { 253 | elementRef.current.forEach((singleRef: React.RefObject) => { 254 | setTarget(key, singleRef); 255 | }); 256 | } else { 257 | setTarget(key, elementRef.current); 258 | } 259 | } 260 | }); 261 | } 262 | }; 263 | 264 | const getTargetRefProp = ( 265 | child: any, 266 | setTarget: (key: string, target: any) => void, 267 | addTarget: (target: any) => void 268 | ) => { 269 | // has to be tested if it works, which lib does still use innerRef? 270 | if (child.props.innerRef) { 271 | return { 272 | innerRef: (target: any) => { 273 | setOrAddTarget(target, setTarget, addTarget); 274 | // merge refs 275 | const { innerRef } = child.props; 276 | if (typeof innerRef === 'function') innerRef(target); 277 | else if (innerRef) innerRef.current = target; 278 | }, 279 | }; 280 | } 281 | 282 | return { 283 | ref: (target: any) => { 284 | setOrAddTarget(target, setTarget, addTarget); 285 | // merge refs 286 | const { ref } = child; 287 | if (typeof ref === 'function') ref(target); 288 | else if (ref) ref.current = target; 289 | }, 290 | }; 291 | }; 292 | 293 | const nullishCoalescing = (value: T, ifNullish: R): T | R => { 294 | if (value === null || typeof value === 'undefined') { 295 | return ifNullish; 296 | } 297 | return value; 298 | }; 299 | 300 | const setProps = ( 301 | tweenOrTimeline: any, 302 | props: TimelineProps | TweenProps, 303 | prevProps?: TimelineProps | TweenProps 304 | ) => { 305 | if (props.progress !== undefined && props.progress !== prevProps?.progress) { 306 | tweenOrTimeline.progress(props.progress); 307 | } 308 | if (props.totalProgress !== undefined && props.totalProgress !== prevProps?.totalProgress) { 309 | tweenOrTimeline.totalProgress(props.totalProgress); 310 | } 311 | if ( 312 | tweenOrTimeline.duration !== undefined && 313 | props.duration && 314 | props.duration !== prevProps?.duration 315 | ) { 316 | tweenOrTimeline.duration(props.duration); 317 | } 318 | }; 319 | 320 | export { 321 | getTweenFunction, 322 | callTweenFunction, 323 | setPlayState, 324 | isEqual, 325 | refOrInnerRef, 326 | getRefProp, 327 | getTargetRefProp, 328 | nullishCoalescing, 329 | setProps, 330 | setInitialPlayState, 331 | getInitialPaused, 332 | }; 333 | -------------------------------------------------------------------------------- /packages/react-gsap/src/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Tween } from './Tween'; 2 | export { default as Timeline } from './Timeline'; 3 | export { default as Reveal } from './tools/Reveal'; 4 | // export { default as Scroller } from './tools/Scroller'; 5 | export { default as ScrollTrigger } from './tools/ScrollTrigger'; 6 | export { SplitWords, SplitChars, SplitLetters } from './tools/SplitText'; 7 | export { default as Controls } from './tools/Controls'; 8 | export { PlayState } from './types'; 9 | -------------------------------------------------------------------------------- /packages/react-gsap/src/plugins/PlugInCount.ts: -------------------------------------------------------------------------------- 1 | import { nullishCoalescing } from '../helper'; 2 | 3 | let gsap: any; 4 | let _interpolate: any; 5 | let _format: any; 6 | const _getGSAP = () => 7 | gsap || (typeof window !== 'undefined' && (gsap = window.gsap) && gsap.registerPlugin && gsap); 8 | 9 | type Prop = 10 | | { 11 | value: string | number; 12 | format: () => (value: string | number) => number | string; 13 | } 14 | | number 15 | | string; 16 | 17 | export const CountPlugin = { 18 | version: '1.0.0', 19 | name: 'count', 20 | register(core: any, Plugin: any, propTween: any) { 21 | gsap = core; 22 | _interpolate = gsap.utils.interpolate; 23 | _format = (value: string | number) => parseInt(value.toString(), 10); 24 | }, 25 | init(target: any, value: Prop, _tween: any, index: number, targets: any) { 26 | let inputValue = value; 27 | let format = _format; 28 | if (typeof value === 'object') { 29 | inputValue = nullishCoalescing(value.value, 0); 30 | if (value.format) { 31 | format = value.format; 32 | } 33 | } 34 | 35 | const initialCount = parseFloat(target.innerText); 36 | 37 | let data = this; 38 | data.target = target; 39 | data.count = _interpolate(initialCount, parseFloat(inputValue.toString())); 40 | data.format = format; 41 | }, 42 | render(progress: number, data: any) { 43 | data.target.innerText = data.format(data.count(progress)); 44 | }, 45 | }; 46 | 47 | _getGSAP() && gsap.registerPlugin(CountPlugin); 48 | 49 | export { CountPlugin as default }; 50 | -------------------------------------------------------------------------------- /packages/react-gsap/src/plugins/PlugInSvgDraw.ts: -------------------------------------------------------------------------------- 1 | import { nullishCoalescing } from '../helper'; 2 | 3 | let gsap: any; 4 | let _interpolate: any; 5 | let _getProp: any; 6 | const _getGSAP = () => 7 | gsap || (typeof window !== 'undefined' && (gsap = window.gsap) && gsap.registerPlugin && gsap); 8 | 9 | type Point = { 10 | x: number; 11 | y: number; 12 | }; 13 | 14 | function getDistance(p1: DOMPoint | Point, p2: DOMPoint | Point) { 15 | return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); 16 | } 17 | 18 | function getCircleLength(el: SVGCircleElement) { 19 | return 2 * Math.PI * parseFloat(nullishCoalescing(el.getAttribute('r'), '1') || '0'); 20 | } 21 | 22 | function getRectLength(el: SVGRectElement) { 23 | return ( 24 | parseFloat(nullishCoalescing(el.getAttribute('width'), '1') || '0') * 2 + 25 | parseFloat(nullishCoalescing(el.getAttribute('height'), '1') || '0') * 2 26 | ); 27 | } 28 | 29 | function getLineLength(el: SVGLineElement) { 30 | return getDistance( 31 | { 32 | x: parseFloat(nullishCoalescing(el.getAttribute('x1'), '1') || '0'), 33 | y: parseFloat(nullishCoalescing(el.getAttribute('y1'), '1') || '0'), 34 | }, 35 | { 36 | x: parseFloat(nullishCoalescing(el.getAttribute('x2'), '1') || '0'), 37 | y: parseFloat(nullishCoalescing(el.getAttribute('y2'), '1') || '0'), 38 | } 39 | ); 40 | } 41 | 42 | function getPolylineLength(el: SVGPolylineElement) { 43 | const points = el.points; 44 | let totalLength = 0; 45 | let previousPos: DOMPoint | undefined = undefined; 46 | for (let i = 0; i < points.numberOfItems; i++) { 47 | const currentPos = points.getItem(i); 48 | if (previousPos) totalLength += getDistance(previousPos, currentPos); 49 | previousPos = currentPos; 50 | } 51 | return totalLength; 52 | } 53 | 54 | function getPolygonLength(el: SVGPolylineElement) { 55 | const points = el.points; 56 | return ( 57 | getPolylineLength(el) + getDistance(points.getItem(points.numberOfItems - 1), points.getItem(0)) 58 | ); 59 | } 60 | 61 | // if path is splitted into multiple move commands then return longest path 62 | function getPathLength(el: SVGPathElement) { 63 | if (!el.hasAttribute('d')) { 64 | return el.getTotalLength(); 65 | } 66 | const d = el.getAttribute('d'); 67 | const pathString = d ? d.replace(/m/gi, 'M') : null; 68 | 69 | if (!pathString) { 70 | return el.getTotalLength(); 71 | } 72 | 73 | const paths = pathString 74 | .split('M') 75 | .filter(path => path !== '') 76 | .map(path => `M${path}`); 77 | 78 | if (paths.length === 1) { 79 | return el.getTotalLength(); 80 | } 81 | 82 | let maxLength = 0; 83 | 84 | paths.forEach(path => { 85 | const pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 86 | pathElement.setAttribute('d', path); 87 | maxLength = Math.max(maxLength, pathElement.getTotalLength()); 88 | }); 89 | 90 | return maxLength; 91 | } 92 | 93 | function getTotalLength(el: any) { 94 | if (el.getTotalLength) { 95 | return getPathLength(el); 96 | } 97 | switch (el.tagName.toLowerCase()) { 98 | case 'circle': 99 | return getCircleLength(el); 100 | case 'rect': 101 | return getRectLength(el); 102 | case 'line': 103 | return getLineLength(el); 104 | case 'polyline': 105 | return getPolylineLength(el); 106 | case 'polygon': 107 | return getPolygonLength(el); 108 | default: 109 | return 0; 110 | } 111 | } 112 | 113 | export const SvgDrawPlugin = { 114 | version: '2.0.0', 115 | name: 'svgDraw', 116 | register(core: any, Plugin: any, propTween: any) { 117 | gsap = core; 118 | _interpolate = gsap.utils.interpolate; 119 | _getProp = gsap.getProperty; 120 | }, 121 | init(target: any, value: number, _tween: any, index: number, targets: any) { 122 | const length = getTotalLength(target); 123 | 124 | let lengthParam = value; 125 | let offsetParam = 0; 126 | 127 | if (Array.isArray(value)) { 128 | lengthParam = value[0]; 129 | if (value.length >= 2) { 130 | offsetParam = value[1] * -1; 131 | } 132 | } 133 | 134 | let data = this; 135 | data.target = target; 136 | data.strokeDashoffset = _interpolate( 137 | _getProp(target, 'stroke-dashoffset'), 138 | length * offsetParam 139 | ); 140 | data.strokeDasharray = _interpolate(_getProp(target, 'stroke-dasharray'), [ 141 | lengthParam * length, 142 | length, 143 | ]); 144 | }, 145 | render(progress: number, data: any) { 146 | data.target.setAttribute('stroke-dashoffset', data.strokeDashoffset(progress)); 147 | data.target.setAttribute('stroke-dasharray', data.strokeDasharray(progress)); 148 | }, 149 | }; 150 | 151 | _getGSAP() && gsap.registerPlugin(SvgDrawPlugin); 152 | 153 | export { SvgDrawPlugin as default }; 154 | -------------------------------------------------------------------------------- /packages/react-gsap/src/tools/Controls.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react'; 2 | import { PlayState } from './../types'; 3 | import { setPlayState } from './../helper'; 4 | import Provider from '../Provider'; 5 | 6 | type ControlsProps = { 7 | playState?: PlayState; 8 | children: ReactNode; 9 | }; 10 | 11 | type ControlsState = { 12 | totalProgress: number; 13 | playState?: PlayState; 14 | prevPlayState?: PlayState; 15 | }; 16 | 17 | class Controls extends Provider { 18 | gsap: any; 19 | slider: any; 20 | sliderTouched: boolean = false; 21 | 22 | state = { 23 | totalProgress: 0, 24 | playState: undefined, 25 | prevPlayState: undefined, 26 | }; 27 | 28 | containerStyle = { 29 | backgroundColor: '#f0f0f0', 30 | padding: '10px 10px 0 10px', 31 | marginTop: '10px', 32 | position: 'relative' as 'relative', 33 | zIndex: 2, 34 | fontFamily: 'verdana, sans-serif', 35 | fontSize: '16px', 36 | border: '1px solid #ccc', 37 | }; 38 | 39 | buttonContainerStyle = { 40 | margin: '0', 41 | display: 'flex', 42 | flexWrap: 'wrap' as 'wrap', 43 | justifyContent: 'space-between', 44 | }; 45 | 46 | buttonStyle = { 47 | border: '1px solid #999', 48 | backgroundColor: '#f0f0f0', 49 | padding: '5px', 50 | margin: '10px 10px 10px 0', 51 | cursor: 'pointer', 52 | }; 53 | 54 | sliderStyle = { 55 | margin: '0', 56 | width: '100%', 57 | }; 58 | 59 | playStateStyle = { 60 | color: '#999', 61 | margin: '10px 0', 62 | fontSize: '14px', 63 | }; 64 | 65 | componentDidMount() { 66 | if (this.consumers.length) { 67 | this.gsap = this.consumers[0]; 68 | 69 | const gsap = this.gsap.getGSAP(); 70 | 71 | if (gsap) { 72 | gsap.eventCallback('onUpdate', this.onUpdate); 73 | 74 | if (this.props.playState) { 75 | this.setPlayState(this.props.playState); 76 | } else { 77 | // get child initial state 78 | if (gsap.paused()) { 79 | this.setPlayState(PlayState.pause); 80 | } else if (gsap.reversed()) { 81 | this.setPlayState(PlayState.reverse); 82 | } else { 83 | this.setPlayState(PlayState.play); 84 | } 85 | } 86 | 87 | const totalProgress = gsap.totalProgress(); 88 | this.slider.value = totalProgress * 100; 89 | } 90 | } 91 | } 92 | 93 | componentDidUpdate() { 94 | this.onUpdate(); 95 | } 96 | 97 | onUpdate = () => { 98 | if (this.gsap && this.slider && !this.sliderTouched) { 99 | const totalProgress = this.gsap.getGSAP().totalProgress(); 100 | this.slider.value = totalProgress * 100; 101 | } 102 | }; 103 | 104 | onChange = (event: any) => { 105 | if (this.gsap && this.gsap.getGSAP()) { 106 | this.gsap.getGSAP().totalProgress(event.target.value / 100); 107 | } 108 | }; 109 | 110 | setPlayState = (state: PlayState) => { 111 | this.setState(prevState => { 112 | return { 113 | playState: state, 114 | prevPlayState: prevState.playState, 115 | }; 116 | }); 117 | }; 118 | 119 | getControls = (_totalProgress: any, playState: PlayState | undefined) => ( 120 |
    121 | (this.slider = el)} 123 | type="range" 124 | style={this.sliderStyle} 125 | step="0.001" 126 | onChange={e => this.onChange(e)} 127 | onMouseDown={() => (this.sliderTouched = true)} 128 | onMouseUp={() => (this.sliderTouched = false)} 129 | /> 130 |
    131 |
    132 | 139 | 146 | 153 | 160 |
    161 | {playState} 162 |
    163 |
    164 | ); 165 | 166 | render() { 167 | const { children } = this.props; 168 | const { totalProgress, playState, prevPlayState } = this.state; 169 | 170 | if (this.gsap) { 171 | setPlayState(playState, prevPlayState, this.gsap.getGSAP()); 172 | } 173 | 174 | return this.renderWithProvider( 175 |
    176 | {children} 177 | {this.getControls(totalProgress, playState)} 178 |
    179 | ); 180 | } 181 | } 182 | 183 | export default Controls; 184 | -------------------------------------------------------------------------------- /packages/react-gsap/src/tools/Reveal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { gsap } from 'gsap'; 3 | import { nullishCoalescing } from '../helper'; 4 | import Provider from '../Provider'; 5 | 6 | export type RevealProps = { 7 | children: React.ReactNode; 8 | trigger: React.ReactElement | null; 9 | repeat: boolean; 10 | root: Element | null; 11 | rootMargin: string; 12 | threshold: number; 13 | }; 14 | 15 | enum EntryState { 16 | unknown, 17 | entered, 18 | exited, 19 | } 20 | 21 | class Reveal extends Provider { 22 | static displayName = 'Reveal'; 23 | 24 | static defaultProps = { 25 | trigger: null, 26 | repeat: false, 27 | root: null, 28 | rootMargin: '0px', 29 | threshold: 0.66, 30 | }; 31 | 32 | timeline: any; 33 | triggerRef: HTMLElement | null = null; 34 | observer: IntersectionObserver | null = null; 35 | 36 | init() { 37 | this.createTimeline(); 38 | this.createIntersectionObserver(); 39 | } 40 | 41 | kill() { 42 | this.killTimeline(); 43 | this.killIntersectionObserver(); 44 | } 45 | 46 | componentDidMount() { 47 | this.init(); 48 | } 49 | 50 | componentWillUnmount() { 51 | this.kill(); 52 | } 53 | 54 | componentDidUpdate(prevProps: RevealProps) { 55 | const { children, trigger } = this.props; 56 | 57 | // if children change create a new timeline 58 | // TODO: replace easy length check with fast equal check 59 | // TODO: same for props.target? 60 | if (React.Children.count(prevProps.children) !== React.Children.count(children)) { 61 | this.init(); 62 | } 63 | 64 | if (prevProps.trigger !== trigger) { 65 | this.init(); 66 | } 67 | } 68 | 69 | createTimeline() { 70 | this.killTimeline(); 71 | 72 | // init timeline 73 | this.timeline = gsap.timeline({ 74 | smoothChildTiming: true, 75 | paused: true, 76 | }); 77 | 78 | // add consumers 79 | this.consumers.forEach(consumer => { 80 | const { position } = consumer.props; 81 | this.timeline.add(consumer.getGSAP().play(), nullishCoalescing(position, 0)); 82 | }); 83 | } 84 | 85 | killTimeline() { 86 | if (this.timeline) { 87 | this.timeline.kill(); 88 | } 89 | } 90 | 91 | createIntersectionObserver() { 92 | let { root, rootMargin, threshold } = this.props; 93 | 94 | const options = { 95 | root, 96 | rootMargin, 97 | threshold: [0, threshold], 98 | }; 99 | 100 | this.observer = new IntersectionObserver(this.intersectionObserverCallback, options); 101 | 102 | // It would be better if we wouldn't need an extra wrapper. 103 | // But it can be problematic for example with a fadeInLeft animation 104 | // were the element is out of the viewport in the initial state. 105 | // In this case there wouldn't be an intersection.. 106 | if (!this.triggerRef) { 107 | this.consumers.forEach(consumer => { 108 | consumer.getTargets().forEach((target: any) => { 109 | this.observer && this.observer.observe(target); 110 | }); 111 | }); 112 | } else { 113 | this.observer && this.observer.observe(this.triggerRef); 114 | } 115 | } 116 | 117 | killIntersectionObserver() { 118 | this.unobserveAll(); 119 | this.observer = null; 120 | } 121 | 122 | unobserveAll() { 123 | if (this.observer) { 124 | if (!this.triggerRef) { 125 | this.consumers.forEach(consumer => { 126 | consumer.getTargets().forEach((target: any) => { 127 | this.observer && this.observer.unobserve(target); 128 | }); 129 | }); 130 | } else { 131 | this.observer && this.observer.unobserve(this.triggerRef); 132 | } 133 | } 134 | } 135 | 136 | intersectionObserverCallback = (entries: any) => { 137 | let { repeat, threshold } = this.props; 138 | let state: EntryState = EntryState.unknown; 139 | 140 | for (const entry of entries) { 141 | if (entry.isIntersecting && entry.intersectionRatio >= threshold) { 142 | this.timeline.play(); 143 | state = EntryState.entered; 144 | break; 145 | } else if (!entry.isIntersecting) { 146 | state = EntryState.exited; 147 | break; 148 | } 149 | } 150 | 151 | if (!repeat && state === EntryState.entered) { 152 | this.killIntersectionObserver(); 153 | } else if (repeat && state === EntryState.exited) { 154 | this.timeline.pause(0); 155 | } 156 | }; 157 | 158 | getGSAP() { 159 | return this.timeline; 160 | } 161 | 162 | render() { 163 | let { children, trigger } = this.props; 164 | 165 | let output = trigger ? ( 166 | (this.triggerRef = trigger)}> 167 | {children} 168 | 169 | ) : ( 170 | children 171 | ); 172 | 173 | return this.renderWithProvider(output); 174 | } 175 | } 176 | 177 | export default Reveal; 178 | -------------------------------------------------------------------------------- /packages/react-gsap/src/tools/ScrollTrigger.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { gsap } from 'gsap'; 3 | import { ScrollTrigger as ScrollTriggerPlugin } from 'gsap/dist/ScrollTrigger'; 4 | import Provider, { Context } from '../Provider'; 5 | 6 | gsap.registerPlugin(ScrollTriggerPlugin); 7 | 8 | export type ScrollTriggerProps = { 9 | children?: React.ReactNode; 10 | } & gsap.plugins.ScrollTriggerInstanceVars; 11 | 12 | class ScrollTrigger extends Provider { 13 | static displayName = 'ScrollTrigger'; 14 | 15 | scrollTrigger: any | null = null; 16 | targets: any = {}; 17 | 18 | constructor(props: ScrollTriggerProps) { 19 | super(props); 20 | 21 | this.getPlugin = this.getPlugin.bind(this); 22 | } 23 | 24 | // override and pass registerConsumer to next parent provider 25 | registerConsumer(consumer: any) { 26 | this.context.registerConsumer(consumer); 27 | } 28 | 29 | componentDidMount() { 30 | const { children, ...scrollTrigger } = this.props; 31 | 32 | if (!children) { 33 | this.scrollTrigger = ScrollTriggerPlugin.create(scrollTrigger); 34 | } 35 | } 36 | 37 | componentWillUnmount() { 38 | if (this.scrollTrigger) { 39 | this.scrollTrigger.kill(); 40 | } 41 | } 42 | 43 | // componentDidUpdate(prevProps: ScrollTriggerProps) { 44 | // const { trigger } = this.props; 45 | // 46 | // if (trigger !== prevProps.trigger) { 47 | // console.log('prevProps.trigger', prevProps.trigger); 48 | // console.log('trigger', trigger); 49 | // } 50 | // } 51 | 52 | getGSAP() { 53 | return this.scrollTrigger; 54 | } 55 | 56 | getPlugin(props: any, targets: any) { 57 | let { children, trigger: triggerProp, ...scrollTrigger } = props; 58 | 59 | let trigger = triggerProp; 60 | 61 | if (targets instanceof Map) { 62 | if (trigger) { 63 | const target = targets.get(trigger); 64 | if (target) { 65 | trigger = target; 66 | } 67 | } else { 68 | trigger = Array.from(targets.values()); 69 | } 70 | } else if (!trigger) { 71 | trigger = targets; 72 | } 73 | 74 | return { 75 | trigger, 76 | ...scrollTrigger, 77 | }; 78 | } 79 | 80 | render() { 81 | const { children, ...scrollTrigger } = this.props; 82 | if (!children) { 83 | return null; 84 | } 85 | return this.renderWithProvider(children, { 86 | scrollTrigger, 87 | }); 88 | } 89 | } 90 | 91 | export default ScrollTrigger; 92 | -------------------------------------------------------------------------------- /packages/react-gsap/src/tools/Scroller.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { gsap } from 'gsap'; 3 | import { nullishCoalescing } from '../helper'; 4 | import Provider from '../Provider'; 5 | 6 | export enum TriggerPosition { 7 | top = 'top', 8 | bottom = 'bottom', 9 | } 10 | 11 | export type ScrollerProps = { 12 | children: (progress: number) => React.ReactNode; 13 | heightVh: number; 14 | resolution: number; 15 | triggerPosition: TriggerPosition; 16 | }; 17 | 18 | type ScrollerState = { 19 | progress: number; 20 | }; 21 | 22 | class Scroller extends Provider { 23 | static displayName = 'ScrollReveal'; 24 | 25 | static defaultProps = { 26 | heightVh: 100, 27 | resolution: 100, 28 | triggerPosition: TriggerPosition.bottom, 29 | }; 30 | 31 | state: ScrollerState = { 32 | progress: 0, 33 | }; 34 | 35 | timeline: any; 36 | heights: number[] = []; 37 | targetRefs: HTMLElement[] = []; 38 | observer: IntersectionObserver | null = null; 39 | 40 | constructor(props: ScrollerProps) { 41 | super(props); 42 | this.heights = this.getTargetHeights(this.props.heightVh); 43 | } 44 | 45 | getTargetHeights(heightVh: number) { 46 | const numberTimes = heightVh / 100; 47 | const numberTargets = Math.ceil(numberTimes); 48 | const numberFull = Math.floor(numberTimes); 49 | const lastHeight = numberTargets === numberFull ? 100 : (numberTimes % numberFull) * 100; 50 | const heights = Array.from({ length: numberTargets }, () => 100); 51 | heights[numberTargets - 1] = lastHeight; 52 | return heights; 53 | } 54 | 55 | componentDidMount() { 56 | this.createTimeline(); 57 | this.createIntersectionObserver(); 58 | } 59 | 60 | componentWillUnmount() { 61 | this.timeline.kill(); 62 | } 63 | 64 | getSnapshotBeforeUpdate() { 65 | // this.targets = []; 66 | return null; 67 | } 68 | 69 | componentDidUpdate(prevProps: ScrollerProps) { 70 | const { children } = this.props; 71 | 72 | // if children change create a new timeline 73 | // TODO: replace easy length check with fast equal check 74 | // TODO: same for props.target? 75 | if (React.Children.count(prevProps.children) !== React.Children.count(children)) { 76 | this.createTimeline(); 77 | } 78 | } 79 | 80 | createTimeline() { 81 | if (this.timeline) { 82 | this.timeline.kill(); 83 | } 84 | 85 | // init timeline 86 | this.timeline = gsap.timeline({ 87 | smoothChildTiming: true, 88 | paused: true, 89 | }); 90 | 91 | // add consumers 92 | this.consumers.forEach(consumer => { 93 | const { position } = consumer.props; 94 | this.timeline.add(consumer.getGSAP().play(), nullishCoalescing(position, 0)); 95 | }); 96 | } 97 | 98 | createIntersectionObserver() { 99 | const { resolution } = this.props; 100 | 101 | const options = { 102 | // root: this.fixedWrapperRef, 103 | root: null, 104 | rootMargin: '0px', 105 | threshold: Array.from({ length: resolution + 1 }, (v, i) => i / resolution), 106 | }; 107 | 108 | this.observer = new IntersectionObserver(this.intersectionObserverCallback, options); 109 | 110 | this.targetRefs.forEach(target => { 111 | this.observer && this.observer.observe(target); 112 | }); 113 | } 114 | 115 | unobserveAll() { 116 | this.targetRefs.forEach(target => { 117 | this.observer && this.observer.unobserve(target); 118 | }); 119 | } 120 | 121 | intersectionObserverCallback = (entries: any) => { 122 | const { triggerPosition } = this.props; 123 | const progresses = Array.from({ length: this.heights.length }, () => 0); 124 | const { heightVh } = this.props; 125 | 126 | for (const entry of entries) { 127 | console.log('rootBounds.height', entry.rootBounds.height); 128 | console.log('boundingClientRect.top', entry.boundingClientRect.top); 129 | console.log('boundingClientRect.height', entry.boundingClientRect.height); 130 | console.log('intersectionRatio', entry.intersectionRatio); 131 | console.log('intersectionRect.top', entry.intersectionRect.top); 132 | console.log('intersectionRect.height', entry.intersectionRect.height); 133 | console.log('isIntersecting', entry.isIntersecting); 134 | 135 | let progress = 0; 136 | 137 | if (triggerPosition === TriggerPosition.top) { 138 | const height = entry.boundingClientRect.height; 139 | const top = entry.boundingClientRect.top; 140 | const position = top <= 0 ? -top : 0; 141 | progress = position / height; 142 | } else if (triggerPosition === TriggerPosition.bottom) { 143 | const height = entry.boundingClientRect.height; 144 | const position = height - Math.max(Math.min(entry.boundingClientRect.top, height), 0); 145 | progress = position / height; 146 | } 147 | 148 | // console.log('progress', progress); 149 | 150 | const key = entry.target.dataset.key; 151 | 152 | progresses[parseInt(key, 10)] = progress; 153 | } 154 | 155 | if (this.emptyProgresses(progresses)) { 156 | // this.setState({ progress: 0 }); 157 | return; 158 | } 159 | 160 | const totalProgress = this.getTotalProgress(progresses); 161 | const progress = (totalProgress * 100) / heightVh; 162 | 163 | console.log('progresses', progresses); 164 | console.log('totalProgress', totalProgress); 165 | console.log('progress', progress); 166 | 167 | this.setState({ progress }); 168 | }; 169 | 170 | getTotalProgress(progresses: number[]) { 171 | // const length = progresses.length; 172 | return progresses.reduceRight((previousValue, currentValue) => { 173 | if (previousValue) { 174 | return Math.min(currentValue || 1, 1) + previousValue; 175 | } 176 | return currentValue; 177 | }); 178 | } 179 | 180 | emptyProgresses(progresses: number[]) { 181 | for (const progress of progresses) { 182 | if (progress) { 183 | return false; 184 | } 185 | } 186 | return true; 187 | } 188 | 189 | getGSAP() { 190 | return this.timeline; 191 | } 192 | 193 | render() { 194 | const { children } = this.props; 195 | const { progress } = this.state; 196 | 197 | const wrapper = ( 198 | <> 199 | {this.heights.map((height: number, index: number) => ( 200 |
    this.targetRefs.push(target)} 203 | key={index} 204 | data-key={index} 205 | > 206 | {index === 0 ? children(progress) : null} 207 |
    208 | ))} 209 | 210 | ); 211 | 212 | return this.renderWithProvider(wrapper); 213 | } 214 | } 215 | 216 | export default Scroller; 217 | -------------------------------------------------------------------------------- /packages/react-gsap/src/tools/SplitText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type SplitWordsProps = { 4 | children: React.ReactNode; 5 | wrapper: React.ReactElement; 6 | delimiter?: string; 7 | }; 8 | 9 | type SplitCharsProps = { 10 | children: React.ReactNode; 11 | wrapper: React.ReactElement; 12 | }; 13 | 14 | const escapeRegExp = (regExp: string) => { 15 | var specialChars = ['$', '^', '*', '(', ')', '+', '[', ']', '{', '}', '\\', '|', '.', '?', '/']; 16 | var regex = new RegExp('(\\' + specialChars.join('|\\') + ')', 'g'); 17 | return regExp.replace(regex, '\\$1'); 18 | }; 19 | 20 | // TODO: possible or better to output all the refs as one array? 21 | export const SplitWords = React.forwardRef( 22 | ({ children, wrapper, delimiter = ' ' }, ref) => { 23 | if (typeof children !== 'string') { 24 | throw new Error('SplitWords only accepts a string as child.'); 25 | } 26 | const words = children.split(new RegExp(`(${escapeRegExp(delimiter)})`, 'g')); 27 | return ( 28 | <> 29 | {words.map((word: string, i: number) => { 30 | if (delimiter === ' ' && word === delimiter) { 31 | return ; 32 | } 33 | return React.cloneElement(wrapper, { ref, key: i }, word); 34 | })} 35 | 36 | ); 37 | } 38 | ); 39 | 40 | // TODO: possible or better to output all the refs as one array? 41 | export const SplitChars = React.forwardRef(({ children, wrapper }, ref) => { 42 | if (typeof children !== 'string') { 43 | throw new Error('SplitChars only accepts a string as child.'); 44 | } 45 | return ( 46 | <> 47 | {children 48 | .split( 49 | /(?=(?:[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))/ 50 | ) 51 | .map((char: string, i: number) => { 52 | // TODO: enhance check for space 53 | if (char === ' ') { 54 | return ; 55 | } 56 | return React.cloneElement(wrapper, { ref, key: i }, char); 57 | })} 58 | 59 | ); 60 | }); 61 | 62 | export const SplitLetters = React.forwardRef((props: any, ref) => { 63 | console.warn('Deprecation warning: Use SplitChars instead of SplitLetters'); 64 | return ; 65 | }); 66 | -------------------------------------------------------------------------------- /packages/react-gsap/src/types.ts: -------------------------------------------------------------------------------- 1 | export enum PlayState { 2 | play = 'play', 3 | restart = 'restart', 4 | reverse = 'reverse', 5 | restartReverse = 'restartReverse', 6 | stop = 'stop', 7 | stopEnd = 'stopEnd', 8 | pause = 'pause', 9 | resume = 'resume', 10 | } 11 | -------------------------------------------------------------------------------- /packages/react-gsap/test/blah.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { Thing } from '../src'; 4 | 5 | describe('it', () => { 6 | it('renders without crashing', () => { 7 | const div = document.createElement('div'); 8 | ReactDOM.render(, div); 9 | ReactDOM.unmountComponentAtNode(div); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/react-gsap/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "types"], 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "importHelpers": true, 7 | "declaration": true, 8 | "sourceMap": true, 9 | "rootDir": "./src", 10 | "strict": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "moduleResolution": "node", 14 | "baseUrl": "./", 15 | "paths": { 16 | "*": ["src/*", "node_modules/*"] 17 | }, 18 | "jsx": "react", 19 | "esModuleInterop": true, 20 | "noImplicitThis": false, 21 | "noUnusedLocals": false, 22 | "noUnusedParameters": false, 23 | "allowSyntheticDefaultImports": true 24 | } 25 | } 26 | --------------------------------------------------------------------------------