I would like to implement the 4 callbacks of gsap's ScrollTrigger: onEnter
, onEnterBack
, onLeave
, and onLeaveBack
, with IntersectionObserver.
https://greensock.com/docs/v3/Plugins/ScrollTrigger
(ScrollTrigger notifies 4 events because it takes into account the scroll direction in addition to visible and invisible state transitions).
function createScrollTrigger({
element,
onStateChange,
root,
marginTop = "0px",
marginBottom = "0px"
}) {
let state = undefined;
const getDirection = ({ rootBounds, boundingClientRect }) => {
return boundingClientRect.bottom < rootBounds.top
? "leave"
: boundingClientRect.top > rootBounds.bottom
? "beforeEnter"
: "enter";
};
const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
const oldState = state;
const newState = getDirection(entry);
if (newState !== oldState) {
state = newState;
onStateChange(newState, oldState);
}
},
{
root,
rootMargin: `${marginTop} 0px ${marginBottom} 0px`,
threshold: 0
}
);
observer.observe(element);
return () => {
observer.disconnect();
};
}
createScrollTrigger({
element: document.querySelector(".box"),
root: document,
marginTop: "-20%",
marginBottom: "-20%",
onStateChange: (newState, oldState) => {
console.log({ newState, oldState });
if (newState === "enter") {
if (oldState === "beforeEnter") {
console.log("→ onEnter()");
} else {
console.log("→ onEnterBack()");
}
} else {
if (newState === "leave") {
console.log("→ onLeave()");
} else {
console.log("→ onLeaveBack()");
}
}
}
});
body {
font-family: sans-serif;
}
.spacer {
height: 400vh;
background-color: #ffffff;
opacity: 0.8;
background-image: repeating-linear-gradient(
45deg,
#c4c4c4 25%,
transparent 25%,
transparent 75%,
#c4c4c4 75%,
#c4c4c4
),
repeating-linear-gradient(
45deg,
#c4c4c4 25%,
#ffffff 25%,
#ffffff 75%,
#c4c4c4 75%,
#c4c4c4
);
background-position: 0 0, 10px 10px;
background-size: 20px 20px;
background-repeat: repeat;
}
.container {
display: flex;
align-items: center;
justify-content: center;
}
.box {
background: #000;
width: 50vw;
}
.box::before {
content: "";
display: block;
padding-top: 100%;
}
.root-margin-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
border: solid rgba(0, 0, 0, 0.5);
border-width: 20vh 0;
pointer-events: none;
}
.nav {
position: fixed;
top: 0;
left: 0;
}
.nav li {
color: #fff;
text-decoration: underline;
cursor: pointer;
}
<!DOCTYPE html>
<html>
<head>
<title>Sandbox</title>
<meta charset="UTF-8" />
</head>
<body>
<div class="spacer container">
<div class="box"></div>
</div>
<div class="root-margin-overlay"></div>
<nav class="nav">
<button onclick="window.scrollTo(0, 0)">Go to top of page</button>
<button onclick="window.scrollTo(0, document.body.scrollHeight)">
Go to end of page
</button>
</nav>
</body>
</html>
It almost works, but there is one problem.
How to reproduce:
- Scroll back to the top of the page
- Press the "Go to end of page" button
- Scroll back to where the element is visible
- Application state is not updated and incorrect callback
onEnter()
is called.onEnterBack()
should be called.
IntersectionObserver notifies state transitions between invisible and visible, but not from invisible (element is above viewport) to invisible (element is below viewport).
Therefore, it is difficult for me to reproduce the ScrollTrigger's callbacks in IntersectionObserver.
I think just knowing if an element is visible or not would be of limited use, but is there any workaround?
Is there any use for IntersectionObserver beyond stopping requestAnimationFrame() when it is off-screen or triggering an animation only once?
Edit: What I end up wanting to do is an animation that is triggered by scrolling, but is replayable, such as the following:
Start animation mid-viewport, but reset it offscreen
However, I would like to do this without installing gsap & ScrollTrigger if possible; ScrollTrigger is great, but when using other animation libraries like framer-motion
or simply with CSS Transition, it is too much.
Can I make good use of IntersectionObserver for this, or should I use Scroll event and Element.getBoundingClinetRect()?