├── .gitignore
├── example.gif
├── test
├── index.js
└── index.html
├── package.json
├── dist
├── index.html
├── index.es.js
├── index.umd.js
├── index.js.map
├── index.es.js.map
├── index.umd.js.map
├── index.js
├── test.e31bb0bc.js
└── test.e31bb0bc.js.map
├── README.md
└── src
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache
3 |
--------------------------------------------------------------------------------
/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aholachek/scrollbounce/HEAD/example.gif
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | import initBounce from "../src/index"
2 |
3 | const ul = document.querySelector("ul")
4 |
5 | ;[...new Array(200).keys()].forEach(i => {
6 | const li = document.createElement("li")
7 | li.dataset.bounceId = `bounce-id-${i}`
8 | ul.appendChild(li)
9 | })
10 |
11 | initBounce({ effectMultiplier: 3 })
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scrollbounce",
3 | "version": "0.0.5",
4 | "description": "Add a subtle bounce effect on mobile when the user scrolls",
5 | "source": "src/index.js",
6 | "main": "dist/index.js",
7 | "module": "dist/index.es.js",
8 | "unpkg": "dist/index.umd.js",
9 | "scripts": {
10 | "test": "parcel test/index.html",
11 | "build": "microbundle",
12 | "dev": "microbundle watch",
13 | "prepare": "yarn run build",
14 | "postpublish": "git push origin --tags"
15 | },
16 | "author": "Alex Holachek",
17 | "license": "ISC",
18 | "dependencies": {
19 | "rebound": "^0.1.0"
20 | },
21 | "devDependencies": {
22 | "microbundle": "^0.11.0",
23 | "parcel": "^1.12.4"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Document
9 |
10 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Document
9 |
10 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # scrollbounce
2 | [](https://bundlephobia.com/result?p=scrollbounce)
3 | [](https://npmjs.org/package/scrollbounce "View this project on npm")
4 |
5 |
6 |
7 |
8 |
9 |
10 | [➡ Live demo on CodeSandbox](https://codesandbox.io/s/scrollbounce-demo-ofxn8)
11 |
12 |
13 | ## Quickstart
14 |
15 | `npm install scrollbounce`
16 |
17 | or
18 |
19 | `yarn add scrollbounce`
20 |
21 | ### 1. Give animated elements unique `data-bounce-id` attributes:
22 |
23 | ```html
24 |
29 | ```
30 |
31 | ### 2. Init the animation:
32 |
33 | ```js
34 | import bounce from 'scrollbounce'
35 |
36 | const stopBounce = bounce()
37 |
38 | // if you want to remove the effect later:
39 | stopBounce()
40 | ```
41 |
42 | ## Options
43 |
44 | The default effect is pretty subtle. To crank it up you can pass in an `effectMultiplier` option.
45 |
46 | ```js
47 | bounce({ effectMultiplier: 3 })
48 | ```
49 |
50 | ## Coming soon
51 |
52 | - [ ] Improved edge case handling
53 | - [ ] Performance optimizations
54 | - [ ] More spring customization
55 | - [ ] Support horizontal scroll
56 |
57 | ## Details
58 |
59 | - [Inspired by the "BouncyLayout" library for iOS ](https://github.com/roberthein/BouncyLayout)
60 | - This library is targeted towards touch devices and won't have any effect on desktop.
61 |
--------------------------------------------------------------------------------
/dist/index.es.js:
--------------------------------------------------------------------------------
1 | import{SpringSystem as e}from"rebound";var t=function(e,t){var n=t.height;return e.top<3*n&&e.bottom>2*-n};export default function(n){void 0===n&&(n={});var o=n.effectMultiplier;void 0===o&&(o=2);var r=new e,i=Array.from(document.querySelectorAll("[data-bounce-id]")),d=window.pageYOffset,c=i.map(function(e){var t=r.createSpring();return t.addListener({onSpringUpdate:function(t){var n=t.getCurrentValue();e.style.transform="translateY("+n+"px)"}}),[e,t]}).reduce(function(e,t){return e[t[0].dataset.bounceId]=t[1],e},{}),a={},u=function(e){return e.top+e.height/2},l=function(){if(a.scrollHeight||(a.scrollHeight=Math.max(document.body.scrollHeight,document.documentElement.scrollHeight,document.body.offsetHeight,document.documentElement.offsetHeight,document.body.clientHeight,document.documentElement.clientHeight)),!(Math.abs(n)>a.viewportCoords.height)){var e=window.pageYOffset;if(!(e<=0||e>=a.scrollHeight-a.viewportCoords.height)){var n=Math.max(-40,Math.min(d-e,40)),r=document.querySelector(['[data-bounce-id="'+a.closestBounceId+'"]']),l=i.indexOf(r),s={},f=l-1,h=l+1;if(n>0)for(;;){var v=i[f];if(!v)break;var m=v.getBoundingClientRect();if(!t(m,a.viewportCoords))break;s[v.dataset.bounceId]=m,f-=1}else for(;;){var g=i[h];if(!g)break;var w=g.getBoundingClientRect();if(!t(w,a.viewportCoords))break;s[g.dataset.bounceId]=w,h+=1}var b=i.filter(function(e){return s[e.dataset.bounceId]}).map(function(e){return[e,s[e.dataset.bounceId]]});i.filter(function(e){return!s[e.dataset.bounceId]}).forEach(function(e){e.style.willChange="",c[e.dataset.bounceId].setEndValue(0)}),b.forEach(function(e){var t=e[0],r=e[1];t.style.willChange="transform";var i=c[t.dataset.bounceId],d=Math.abs(a.clientY-u(r))/a.viewportCoords.height;i.setEndValue(-n*(d*=o))}),d=e}}},s=!1,f=function(e){s||(s=window.addEventListener("scroll",l)),a.clientY=e.targetTouches[0].clientY,a.viewportCoords={height:document.documentElement.clientHeight,width:document.documentElement.clientWidth};var t=i.reduce(function(e,t,n,o){if(e.length&&e[0]!==o[n-1])return e;var r,i,d=t.getBoundingClientRect();return(r=d).top<(i=a.viewportCoords).height&&r.bottom>0&&r.left0&&(0===e.length||Math.abs(a.clientY-u(d))2*-n};return function(n){void 0===n&&(n={});var o=n.effectMultiplier;void 0===o&&(o=2);var r=new e.SpringSystem,i=Array.from(document.querySelectorAll("[data-bounce-id]")),d=window.pageYOffset,c=i.map(function(e){var t=r.createSpring();return t.addListener({onSpringUpdate:function(t){var n=t.getCurrentValue();e.style.transform="translateY("+n+"px)"}}),[e,t]}).reduce(function(e,t){return e[t[0].dataset.bounceId]=t[1],e},{}),u={},a=function(e){return e.top+e.height/2},s=function(){if(u.scrollHeight||(u.scrollHeight=Math.max(document.body.scrollHeight,document.documentElement.scrollHeight,document.body.offsetHeight,document.documentElement.offsetHeight,document.body.clientHeight,document.documentElement.clientHeight)),!(Math.abs(n)>u.viewportCoords.height)){var e=window.pageYOffset;if(!(e<=0||e>=u.scrollHeight-u.viewportCoords.height)){var n=Math.max(-40,Math.min(d-e,40)),r=document.querySelector(['[data-bounce-id="'+u.closestBounceId+'"]']),s=i.indexOf(r),l={},f=s-1,h=s+1;if(n>0)for(;;){var m=i[f];if(!m)break;var v=m.getBoundingClientRect();if(!t(v,u.viewportCoords))break;l[m.dataset.bounceId]=v,f-=1}else for(;;){var g=i[h];if(!g)break;var w=g.getBoundingClientRect();if(!t(w,u.viewportCoords))break;l[g.dataset.bounceId]=w,h+=1}var b=i.filter(function(e){return l[e.dataset.bounceId]}).map(function(e){return[e,l[e.dataset.bounceId]]});i.filter(function(e){return!l[e.dataset.bounceId]}).forEach(function(e){e.style.willChange="",c[e.dataset.bounceId].setEndValue(0)}),b.forEach(function(e){var t=e[0],r=e[1];t.style.willChange="transform";var i=c[t.dataset.bounceId],d=Math.abs(u.clientY-a(r))/u.viewportCoords.height;i.setEndValue(-n*(d*=o))}),d=e}}},l=!1,f=function(e){l||(l=window.addEventListener("scroll",s)),u.clientY=e.targetTouches[0].clientY,u.viewportCoords={height:document.documentElement.clientHeight,width:document.documentElement.clientWidth};var t=i.reduce(function(e,t,n,o){if(e.length&&e[0]!==o[n-1])return e;var r,i,d=t.getBoundingClientRect();return(r=d).top<(i=u.viewportCoords).height&&r.bottom>0&&r.left0&&(0===e.length||Math.abs(u.clientY-a(d)) {
4 | return {
5 | height: document.documentElement.clientHeight,
6 | width: document.documentElement.clientWidth
7 | }
8 | }
9 |
10 | const getScrollHeight = () => {
11 | // insane code courtesy of https://javascript.info/size-and-scroll-window
12 | return Math.max(
13 | document.body.scrollHeight,
14 | document.documentElement.scrollHeight,
15 | document.body.offsetHeight,
16 | document.documentElement.offsetHeight,
17 | document.body.clientHeight,
18 | document.documentElement.clientHeight
19 | )
20 | }
21 |
22 | const rectInViewport = (
23 | { top, bottom, left, right },
24 | { width: windowWidth, height: windowHeight }
25 | ) => {
26 | return top < windowHeight && bottom > 0 && left < windowWidth && right > 0
27 | }
28 |
29 | const rectCloseToViewport = ({ top, bottom }, { height: windowHeight }) => {
30 | return top < windowHeight * 3 && bottom > -windowHeight * 2
31 | }
32 |
33 | const initScrollBounce = ({ effectMultiplier = 2 } = {}) => {
34 | const springSystem = new SpringSystem()
35 |
36 | const bounceChildren = Array.from(
37 | document.querySelectorAll("[data-bounce-id]")
38 | )
39 |
40 | let offset = window.pageYOffset
41 |
42 | const springs = bounceChildren
43 | .map(child => {
44 | const spring = springSystem.createSpring()
45 |
46 | spring.addListener({
47 | onSpringUpdate(_spring) {
48 | const val = _spring.getCurrentValue()
49 | child.style.transform = `translateY(${val}px)`
50 | }
51 | })
52 | return [child, spring]
53 | })
54 | .reduce((acc, curr) => {
55 | acc[curr[0].dataset.bounceId] = curr[1]
56 | return acc
57 | }, {})
58 |
59 | const resetSprings = () => {
60 | Object.keys(springs).forEach(s => {
61 | springs[s].setEndValue(0)
62 | })
63 | }
64 |
65 | let cache = {}
66 |
67 | const getCenter = ({ top, height }) => top + height / 2
68 |
69 | const onScroll = () => {
70 | const fastScroll = Math.abs(diff) > cache.viewportCoords.height
71 | if (fastScroll) return
72 |
73 | const newOffset = window.pageYOffset
74 |
75 | if (newOffset <= 0) return
76 |
77 | if (newOffset >= cache.scrollHeight - cache.viewportCoords.height) {
78 | return
79 | }
80 |
81 | const scrollDiffLimit = 40
82 | const diff = Math.max(
83 | -scrollDiffLimit,
84 | Math.min(offset - newOffset, scrollDiffLimit)
85 | )
86 |
87 | const closestChild = document.querySelector([
88 | `[data-bounce-id="${cache.closestBounceId}"]`
89 | ])
90 |
91 | const closestChildIndex = bounceChildren.indexOf(closestChild)
92 |
93 | const animatedChildrenDict = {}
94 |
95 | let animatedAboveIndex = closestChildIndex - 1
96 | let animatedBelowIndex = closestChildIndex + 1
97 |
98 | const scrollDown = diff > 0
99 |
100 | if (scrollDown) {
101 | while (true) {
102 | const el = bounceChildren[animatedAboveIndex]
103 | if (!el) break
104 | const bounding = el.getBoundingClientRect()
105 | const isAnimated = rectCloseToViewport(bounding, cache.viewportCoords)
106 | if (!isAnimated) break
107 | animatedChildrenDict[el.dataset.bounceId] = bounding
108 | animatedAboveIndex -= 1
109 | }
110 | } else {
111 | while (true) {
112 | const el = bounceChildren[animatedBelowIndex]
113 | if (!el) break
114 | const bounding = el.getBoundingClientRect()
115 | const isAnimated = rectCloseToViewport(bounding, cache.viewportCoords)
116 | if (!isAnimated) break
117 | animatedChildrenDict[el.dataset.bounceId] = bounding
118 | animatedBelowIndex += 1
119 | }
120 | }
121 |
122 | const animatedChildren = bounceChildren
123 | .filter(c => {
124 | return animatedChildrenDict[c.dataset.bounceId]
125 | })
126 | .map(c => {
127 | return [c, animatedChildrenDict[c.dataset.bounceId]]
128 | })
129 |
130 | bounceChildren
131 | .filter(c => {
132 | return !animatedChildrenDict[c.dataset.bounceId]
133 | })
134 | .forEach(c => {
135 | c.style.willChange = ""
136 | springs[c.dataset.bounceId].setEndValue(0)
137 | })
138 |
139 | animatedChildren.forEach(([child, bounding]) => {
140 | child.style.willChange = "transform"
141 |
142 | const spring = springs[child.dataset.bounceId]
143 |
144 | let resistance =
145 | Math.abs(cache.clientY - getCenter(bounding)) /
146 | cache.viewportCoords.height
147 |
148 | resistance = resistance * effectMultiplier
149 |
150 | spring.setEndValue(-diff * resistance)
151 | })
152 |
153 | offset = newOffset
154 | }
155 |
156 | const onTouchStart = event => {
157 | window.addEventListener("scroll", onScroll)
158 | cache.clientY = event.targetTouches[0].clientY
159 |
160 | cache.viewportCoords = getViewportCoords()
161 | cache.scrollHeight = getScrollHeight()
162 |
163 | const closestElTuple = bounceChildren.reduce((acc, curr, i, source) => {
164 | if (acc.length && acc[0] !== source[i - 1]) return acc
165 | const bounding = curr.getBoundingClientRect()
166 | if (!rectInViewport(bounding, cache.viewportCoords)) return acc
167 | if (
168 | acc.length === 0 ||
169 | Math.abs(cache.clientY - getCenter(bounding)) <
170 | Math.abs(cache.clientY - getCenter(acc[1]))
171 | ) {
172 | return [curr, bounding]
173 | }
174 | return acc
175 | }, [])
176 |
177 | cache.closestBounceId = closestElTuple[0].dataset.bounceId
178 | }
179 |
180 | const onTouchEnd = () => {
181 | window.removeEventListener("scroll", onScroll)
182 | resetSprings()
183 | cache = {}
184 | }
185 |
186 | const onTouchMove = event => {
187 | cache.clientY = event.targetTouches[0].clientY
188 | }
189 |
190 | window.addEventListener("touchstart", onTouchStart, false)
191 | window.addEventListener("touchmove", onTouchMove, false)
192 | window.addEventListener("touchend", onTouchEnd, false)
193 |
194 | return () => {
195 | window.removeEventListener("touchstart", onTouchStart, false)
196 | window.removeEventListener("touchmove", onTouchMove, false)
197 | window.removeEventListener("touchend", onTouchEnd, false)
198 | }
199 | }
200 |
201 | export default initScrollBounce
202 |
--------------------------------------------------------------------------------
/dist/index.es.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"index.es.js","sources":["../src/index.js"],"sourcesContent":["import { SpringSystem } from \"rebound\"\n\nconst getViewportCoords = () => {\n return {\n height: document.documentElement.clientHeight,\n width: document.documentElement.clientWidth\n }\n}\n\nconst getScrollHeight = () => {\n // insane code courtesy of https://javascript.info/size-and-scroll-window\n return Math.max(\n document.body.scrollHeight,\n document.documentElement.scrollHeight,\n document.body.offsetHeight,\n document.documentElement.offsetHeight,\n document.body.clientHeight,\n document.documentElement.clientHeight\n )\n}\n\nconst rectInViewport = (\n { top, bottom, left, right },\n { width: windowWidth, height: windowHeight }\n) => {\n return top < windowHeight && bottom > 0 && left < windowWidth && right > 0\n}\n\nconst rectCloseToViewport = ({ top, bottom }, { height: windowHeight }) => {\n return top < windowHeight * 3 && bottom > -windowHeight * 2\n}\n\nconst initScrollBounce = ({ effectMultiplier = 2 } = {}) => {\n const springSystem = new SpringSystem()\n\n const bounceChildren = Array.from(\n document.querySelectorAll(\"[data-bounce-id]\")\n )\n\n let offset = window.pageYOffset\n\n const springs = bounceChildren\n .map(child => {\n const spring = springSystem.createSpring()\n\n spring.addListener({\n onSpringUpdate(_spring) {\n const val = _spring.getCurrentValue()\n child.style.transform = `translateY(${val}px)`\n }\n })\n return [child, spring]\n })\n .reduce((acc, curr) => {\n acc[curr[0].dataset.bounceId] = curr[1]\n return acc\n }, {})\n\n const resetSprings = () => {\n Object.keys(springs).forEach(s => {\n springs[s].setEndValue(0)\n })\n }\n\n let cache = {}\n\n const getCenter = bounding => {\n return bounding.top + bounding.height / 2\n }\n\n const onScroll = () => {\n if (!cache.scrollHeight) cache.scrollHeight = getScrollHeight()\n\n const fastScroll = Math.abs(diff) > cache.viewportCoords.height\n if (fastScroll) return\n\n const newOffset = window.pageYOffset\n\n if (newOffset <= 0) return\n\n if (newOffset >= cache.scrollHeight - cache.viewportCoords.height) {\n return\n }\n\n const scrollDiffLimit = 40\n const diff = Math.max(\n -scrollDiffLimit,\n Math.min(offset - newOffset, scrollDiffLimit)\n )\n\n const closestChild = document.querySelector([\n `[data-bounce-id=\"${cache.closestBounceId}\"]`\n ])\n\n const closestChildIndex = bounceChildren.indexOf(closestChild)\n\n const animatedChildrenDict = {}\n\n let animatedAboveIndex = closestChildIndex - 1\n let animatedBelowIndex = closestChildIndex + 1\n\n const scrollDown = diff > 0\n\n if (scrollDown) {\n while (true) {\n const el = bounceChildren[animatedAboveIndex]\n if (!el) break\n const bounding = el.getBoundingClientRect()\n const isAnimated = rectCloseToViewport(bounding, cache.viewportCoords)\n if (!isAnimated) break\n animatedChildrenDict[el.dataset.bounceId] = bounding\n animatedAboveIndex -= 1\n }\n } else {\n while (true) {\n const el = bounceChildren[animatedBelowIndex]\n if (!el) break\n const bounding = el.getBoundingClientRect()\n const isAnimated = rectCloseToViewport(bounding, cache.viewportCoords)\n if (!isAnimated) break\n animatedChildrenDict[el.dataset.bounceId] = bounding\n animatedBelowIndex += 1\n }\n }\n\n const animatedChildren = bounceChildren\n .filter(c => {\n return animatedChildrenDict[c.dataset.bounceId]\n })\n .map(c => {\n return [c, animatedChildrenDict[c.dataset.bounceId]]\n })\n\n bounceChildren\n .filter(c => {\n return !animatedChildrenDict[c.dataset.bounceId]\n })\n .forEach(c => {\n c.style.willChange = \"\"\n springs[c.dataset.bounceId].setEndValue(0)\n })\n\n animatedChildren.forEach(([child, bounding]) => {\n child.style.willChange = \"transform\"\n\n const spring = springs[child.dataset.bounceId]\n\n let resistance =\n Math.abs(cache.clientY - getCenter(bounding)) /\n cache.viewportCoords.height\n\n resistance = resistance * effectMultiplier\n\n spring.setEndValue(-diff * resistance)\n })\n\n offset = newOffset\n }\n\n let scrollListener = false\n\n const onTouchStart = event => {\n if (!scrollListener)\n scrollListener = window.addEventListener(\"scroll\", onScroll)\n cache.clientY = event.targetTouches[0].clientY\n\n cache.viewportCoords = getViewportCoords()\n\n const closestElTuple = bounceChildren.reduce((acc, curr, i, source) => {\n if (acc.length && acc[0] !== source[i - 1]) return acc\n const bounding = curr.getBoundingClientRect()\n if (!rectInViewport(bounding, cache.viewportCoords)) return acc\n if (\n acc.length === 0 ||\n Math.abs(cache.clientY - getCenter(bounding)) <\n Math.abs(cache.clientY - getCenter(acc[1]))\n ) {\n return [curr, bounding]\n }\n return acc\n }, [])\n\n cache.closestBounceId = closestElTuple[0].dataset.bounceId\n }\n\n const onTouchEnd = () => {\n window.removeEventListener(\"scroll\", onScroll)\n resetSprings()\n cache = {}\n }\n\n const onTouchMove = event => {\n cache.clientY = event.targetTouches[0].clientY\n }\n\n window.addEventListener(\"touchstart\", onTouchStart, false)\n window.addEventListener(\"touchmove\", onTouchMove, false)\n window.addEventListener(\"touchend\", onTouchEnd, false)\n\n return () => {\n window.removeEventListener(\"touchstart\", onTouchStart, false)\n window.removeEventListener(\"touchmove\", onTouchMove, false)\n window.removeEventListener(\"touchend\", onTouchEnd, false)\n }\n}\n\nexport default initScrollBounce\n"],"names":["const","rectCloseToViewport","ref","ref$1","windowHeight","springSystem","SpringSystem","bounceChildren","Array","from","document","querySelectorAll","offset","window","pageYOffset","springs","map","child","spring","createSpring","addListener","onSpringUpdate","_spring","val","getCurrentValue","style","transform","reduce","acc","curr","dataset","bounceId","cache","getCenter","bounding","top","height","onScroll","scrollHeight","Math","max","body","documentElement","offsetHeight","clientHeight","abs","diff","viewportCoords","newOffset","min","closestChild","querySelector","closestChildIndex","indexOf","animatedChildrenDict","animatedAboveIndex","animatedBelowIndex","el","getBoundingClientRect","animatedChildren","filter","c","forEach","willChange","setEndValue","resistance","clientY","effectMultiplier","scrollListener","onTouchStart","event","addEventListener","targetTouches","width","clientWidth","closestElTuple","i","source","length","closestBounceId","onTouchEnd","removeEventListener","Object","keys","s","onTouchMove"],"mappings":"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"}
--------------------------------------------------------------------------------
/dist/index.umd.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"index.umd.js","sources":["../src/index.js"],"sourcesContent":["import { SpringSystem } from \"rebound\"\n\nconst getViewportCoords = () => {\n return {\n height: document.documentElement.clientHeight,\n width: document.documentElement.clientWidth\n }\n}\n\nconst getScrollHeight = () => {\n // insane code courtesy of https://javascript.info/size-and-scroll-window\n return Math.max(\n document.body.scrollHeight,\n document.documentElement.scrollHeight,\n document.body.offsetHeight,\n document.documentElement.offsetHeight,\n document.body.clientHeight,\n document.documentElement.clientHeight\n )\n}\n\nconst rectInViewport = (\n { top, bottom, left, right },\n { width: windowWidth, height: windowHeight }\n) => {\n return top < windowHeight && bottom > 0 && left < windowWidth && right > 0\n}\n\nconst rectCloseToViewport = ({ top, bottom }, { height: windowHeight }) => {\n return top < windowHeight * 3 && bottom > -windowHeight * 2\n}\n\nconst initScrollBounce = ({ effectMultiplier = 2 } = {}) => {\n const springSystem = new SpringSystem()\n\n const bounceChildren = Array.from(\n document.querySelectorAll(\"[data-bounce-id]\")\n )\n\n let offset = window.pageYOffset\n\n const springs = bounceChildren\n .map(child => {\n const spring = springSystem.createSpring()\n\n spring.addListener({\n onSpringUpdate(_spring) {\n const val = _spring.getCurrentValue()\n child.style.transform = `translateY(${val}px)`\n }\n })\n return [child, spring]\n })\n .reduce((acc, curr) => {\n acc[curr[0].dataset.bounceId] = curr[1]\n return acc\n }, {})\n\n const resetSprings = () => {\n Object.keys(springs).forEach(s => {\n springs[s].setEndValue(0)\n })\n }\n\n let cache = {}\n\n const getCenter = bounding => {\n return bounding.top + bounding.height / 2\n }\n\n const onScroll = () => {\n if (!cache.scrollHeight) cache.scrollHeight = getScrollHeight()\n\n const fastScroll = Math.abs(diff) > cache.viewportCoords.height\n if (fastScroll) return\n\n const newOffset = window.pageYOffset\n\n if (newOffset <= 0) return\n\n if (newOffset >= cache.scrollHeight - cache.viewportCoords.height) {\n return\n }\n\n const scrollDiffLimit = 40\n const diff = Math.max(\n -scrollDiffLimit,\n Math.min(offset - newOffset, scrollDiffLimit)\n )\n\n const closestChild = document.querySelector([\n `[data-bounce-id=\"${cache.closestBounceId}\"]`\n ])\n\n const closestChildIndex = bounceChildren.indexOf(closestChild)\n\n const animatedChildrenDict = {}\n\n let animatedAboveIndex = closestChildIndex - 1\n let animatedBelowIndex = closestChildIndex + 1\n\n const scrollDown = diff > 0\n\n if (scrollDown) {\n while (true) {\n const el = bounceChildren[animatedAboveIndex]\n if (!el) break\n const bounding = el.getBoundingClientRect()\n const isAnimated = rectCloseToViewport(bounding, cache.viewportCoords)\n if (!isAnimated) break\n animatedChildrenDict[el.dataset.bounceId] = bounding\n animatedAboveIndex -= 1\n }\n } else {\n while (true) {\n const el = bounceChildren[animatedBelowIndex]\n if (!el) break\n const bounding = el.getBoundingClientRect()\n const isAnimated = rectCloseToViewport(bounding, cache.viewportCoords)\n if (!isAnimated) break\n animatedChildrenDict[el.dataset.bounceId] = bounding\n animatedBelowIndex += 1\n }\n }\n\n const animatedChildren = bounceChildren\n .filter(c => {\n return animatedChildrenDict[c.dataset.bounceId]\n })\n .map(c => {\n return [c, animatedChildrenDict[c.dataset.bounceId]]\n })\n\n bounceChildren\n .filter(c => {\n return !animatedChildrenDict[c.dataset.bounceId]\n })\n .forEach(c => {\n c.style.willChange = \"\"\n springs[c.dataset.bounceId].setEndValue(0)\n })\n\n animatedChildren.forEach(([child, bounding]) => {\n child.style.willChange = \"transform\"\n\n const spring = springs[child.dataset.bounceId]\n\n let resistance =\n Math.abs(cache.clientY - getCenter(bounding)) /\n cache.viewportCoords.height\n\n resistance = resistance * effectMultiplier\n\n spring.setEndValue(-diff * resistance)\n })\n\n offset = newOffset\n }\n\n let scrollListener = false\n\n const onTouchStart = event => {\n if (!scrollListener)\n scrollListener = window.addEventListener(\"scroll\", onScroll)\n cache.clientY = event.targetTouches[0].clientY\n\n cache.viewportCoords = getViewportCoords()\n\n const closestElTuple = bounceChildren.reduce((acc, curr, i, source) => {\n if (acc.length && acc[0] !== source[i - 1]) return acc\n const bounding = curr.getBoundingClientRect()\n if (!rectInViewport(bounding, cache.viewportCoords)) return acc\n if (\n acc.length === 0 ||\n Math.abs(cache.clientY - getCenter(bounding)) <\n Math.abs(cache.clientY - getCenter(acc[1]))\n ) {\n return [curr, bounding]\n }\n return acc\n }, [])\n\n cache.closestBounceId = closestElTuple[0].dataset.bounceId\n }\n\n const onTouchEnd = () => {\n window.removeEventListener(\"scroll\", onScroll)\n resetSprings()\n cache = {}\n }\n\n const onTouchMove = event => {\n cache.clientY = event.targetTouches[0].clientY\n }\n\n window.addEventListener(\"touchstart\", onTouchStart, false)\n window.addEventListener(\"touchmove\", onTouchMove, false)\n window.addEventListener(\"touchend\", onTouchEnd, false)\n\n return () => {\n window.removeEventListener(\"touchstart\", onTouchStart, false)\n window.removeEventListener(\"touchmove\", onTouchMove, false)\n window.removeEventListener(\"touchend\", onTouchEnd, false)\n }\n}\n\nexport default initScrollBounce\n"],"names":["const","rectCloseToViewport","ref","ref$1","windowHeight","springSystem","SpringSystem","bounceChildren","Array","from","document","querySelectorAll","offset","window","pageYOffset","springs","map","child","spring","createSpring","addListener","onSpringUpdate","_spring","val","getCurrentValue","style","transform","reduce","acc","curr","dataset","bounceId","cache","getCenter","bounding","top","height","onScroll","scrollHeight","Math","max","body","documentElement","offsetHeight","clientHeight","abs","diff","viewportCoords","newOffset","min","closestChild","querySelector","closestChildIndex","indexOf","animatedChildrenDict","animatedAboveIndex","animatedBelowIndex","el","getBoundingClientRect","animatedChildren","filter","c","forEach","willChange","setEndValue","resistance","clientY","effectMultiplier","scrollListener","onTouchStart","event","addEventListener","targetTouches","width","clientWidth","closestElTuple","i","source","length","closestBounceId","onTouchEnd","removeEventListener","Object","keys","s","onTouchMove"],"mappings":"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"}
--------------------------------------------------------------------------------
/dist/index.js:
--------------------------------------------------------------------------------
1 | // modules are defined as an array
2 | // [ module function, map of requires ]
3 | //
4 | // map of requires is short require name -> numeric require
5 | //
6 | // anything defined in a previous bundle is accessed via the
7 | // orig method which is the require for previous bundles
8 | parcelRequire = (function (modules, cache, entry, globalName) {
9 | // Save the require from previous bundle to this closure if any
10 | var previousRequire = typeof parcelRequire === 'function' && parcelRequire;
11 | var nodeRequire = typeof require === 'function' && require;
12 |
13 | function newRequire(name, jumped) {
14 | if (!cache[name]) {
15 | if (!modules[name]) {
16 | // if we cannot find the module within our internal map or
17 | // cache jump to the current global require ie. the last bundle
18 | // that was added to the page.
19 | var currentRequire = typeof parcelRequire === 'function' && parcelRequire;
20 | if (!jumped && currentRequire) {
21 | return currentRequire(name, true);
22 | }
23 |
24 | // If there are other bundles on this page the require from the
25 | // previous one is saved to 'previousRequire'. Repeat this as
26 | // many times as there are bundles until the module is found or
27 | // we exhaust the require chain.
28 | if (previousRequire) {
29 | return previousRequire(name, true);
30 | }
31 |
32 | // Try the node require function if it exists.
33 | if (nodeRequire && typeof name === 'string') {
34 | return nodeRequire(name);
35 | }
36 |
37 | var err = new Error('Cannot find module \'' + name + '\'');
38 | err.code = 'MODULE_NOT_FOUND';
39 | throw err;
40 | }
41 |
42 | localRequire.resolve = resolve;
43 | localRequire.cache = {};
44 |
45 | var module = cache[name] = new newRequire.Module(name);
46 |
47 | modules[name][0].call(module.exports, localRequire, module, module.exports, this);
48 | }
49 |
50 | return cache[name].exports;
51 |
52 | function localRequire(x){
53 | return newRequire(localRequire.resolve(x));
54 | }
55 |
56 | function resolve(x){
57 | return modules[name][1][x] || x;
58 | }
59 | }
60 |
61 | function Module(moduleName) {
62 | this.id = moduleName;
63 | this.bundle = newRequire;
64 | this.exports = {};
65 | }
66 |
67 | newRequire.isParcelRequire = true;
68 | newRequire.Module = Module;
69 | newRequire.modules = modules;
70 | newRequire.cache = cache;
71 | newRequire.parent = previousRequire;
72 | newRequire.register = function (id, exports) {
73 | modules[id] = [function (require, module) {
74 | module.exports = exports;
75 | }, {}];
76 | };
77 |
78 | var error;
79 | for (var i = 0; i < entry.length; i++) {
80 | try {
81 | newRequire(entry[i]);
82 | } catch (e) {
83 | // Save first error but execute all entries
84 | if (!error) {
85 | error = e;
86 | }
87 | }
88 | }
89 |
90 | if (entry.length) {
91 | // Expose entry point to Node, AMD or browser globals
92 | // Based on https://github.com/ForbesLindesay/umd/blob/master/template.js
93 | var mainExports = newRequire(entry[entry.length - 1]);
94 |
95 | // CommonJS
96 | if (typeof exports === "object" && typeof module !== "undefined") {
97 | module.exports = mainExports;
98 |
99 | // RequireJS
100 | } else if (typeof define === "function" && define.amd) {
101 | define(function () {
102 | return mainExports;
103 | });
104 |
105 | //