Book a Session
<link rel="stylesheet" href="https://unpkg.com/lenis@1.3.15/dist/lenis.css" />
<script src="https://unpkg.com/lenis@1.3.15/dist/lenis.min.js"></script>
<script>
const lenis = new Lenis({ duration: 1.4 });
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
gsap.ticker.lagSmoothing(0);
</script><script>
document.addEventListener("DOMContentLoaded", () => {
gsap.registerPlugin(ScrollTrigger);
const mm = gsap.matchMedia();
mm.add(
{
mobile: "(max-width: 479px)",
mobileLandscape: "(min-width: 480px) and (max-width: 767px)",
tablet: "(min-width: 768px) and (max-width: 991px)",
desktop: "(min-width: 992px)"
},
(context) => {
const {
mobile,
mobileLandscape,
tablet,
desktop
} = context.conditions;
document
.querySelectorAll('[data-parallax="trigger"]')
.forEach((trigger) => {
// Disable parallax on selected breakpoints
const disable = trigger.getAttribute("data-parallax-disable");
if (
(disable === "mobile" && mobile) ||
(disable === "mobileLandscape" && mobileLandscape) ||
(disable === "tablet" && tablet) ||
(disable === "desktop" && desktop)
) {
return;
}
// Target element
const target =
trigger.querySelector('[data-parallax="target"]') || trigger;
// Direction
const direction =
trigger.getAttribute("data-parallax-direction") || "vertical";
const property =
direction === "horizontal" ? "xPercent" : "yPercent";
// Parallax values
const startValue = parseFloat(
trigger.getAttribute("data-parallax-start")
) || -5;
const endValue = parseFloat(
trigger.getAttribute("data-parallax-end")
) || -20;
// Scrub
const scrubAttribute =
trigger.getAttribute("data-parallax-scrub");
const scrub =
scrubAttribute !== null
? parseFloat(scrubAttribute)
: true;
// ScrollTrigger positions
const scrollStart =
trigger.getAttribute("data-parallax-scroll-start") ||
"top bottom";
const scrollEnd =
trigger.getAttribute("data-parallax-scroll-end") ||
"bottom top";
gsap.fromTo(
target,
{
[property]: startValue
},
{
[property]: endValue,
ease: "none",
scrollTrigger: {
trigger: trigger,
start: `clamp(${scrollStart})`,
end: `clamp(${scrollEnd})`,
scrub: scrub,
invalidateOnRefresh: true
}
}
);
});
ScrollTrigger.refresh();
}
);
});
</script><script>
gsap.registerPlugin(ScrollTrigger, MotionPathPlugin);
function initImagesOnPathScroll() {
const wrap = document.querySelector('[data-motionpath="wrap"]');
const path = wrap.querySelector('[data-motionpath="path"]');
const items = wrap.querySelectorAll('[data-motionpath="item"]');
const itemDetails = wrap.querySelectorAll('[data-motionpath="item-details"]');
// Set z-index on items, to make sure the 1st item is on top
gsap.set(items, {
zIndex: (i, target, all) => all.length - i,
});
// if there’s an old timeline, grab its progress, reset it, then kill it
const oldTl = initImagesOnPathScroll.tl;
let progress = 0;
if (oldTl) {
progress = oldTl.progress();
oldTl.progress(0).kill();
}
// create a new timeline + ScrollTrigger
const tl = gsap.timeline({
scrollTrigger: {
trigger: wrap,
start: 'top top',
end: 'bottom bottom',
scrub: true,
},
defaults: {
ease: 'none',
stagger: 0.3, // Define the space between each item
},
});
tl.to(items, {
duration: 1,
motionPath: { path, align: path, curviness: 2, alignOrigin: [0.5, 0.5] },
})
.fromTo(items, { autoAlpha: 0 }, { autoAlpha: 1, duration: 0.1 }, 0)
.fromTo(items, { filter: 'blur(1.5em)' }, { filter: 'blur(0em)', duration: 0.5 }, 0)
.fromTo(itemDetails, { autoAlpha: 0, yPercent: 25 }, { autoAlpha: 1, yPercent: 0, duration: 0.1 }, 0.5)
.fromTo(items, { scale: 0.4 }, { scale: 1, duration: 0.65 }, 0)
.to(items, { autoAlpha: 0, filter: 'blur(1em)', duration: 0.15 }, 0.85)
.to(itemDetails, { autoAlpha: 0, duration: 0.05 }, 0.9);
// jump back to previous spot and refresh
tl.progress(progress);
ScrollTrigger.refresh();
// store it on the function so we can grab it next time
initImagesOnPathScroll.tl = tl;
// on first run bind a single debounced resize listener
if (!initImagesOnPathScroll.resizeHandler) {
initImagesOnPathScroll.resizeHandler = debounce(() => {
initImagesOnPathScroll();
}, 200);
window.addEventListener('resize', initImagesOnPathScroll.resizeHandler);
}
return tl;
}
function debounce(fn, delay = 200) {
let timeout;
return () => {
clearTimeout(timeout);
timeout = setTimeout(fn, delay);
};
}
// Initialize Follow SVG Path on Scroll
document.addEventListener('DOMContentLoaded', () => {
initImagesOnPathScroll();
});
</script>