Guide for Mellio Webflow Template
<script src="https://unpkg.com/lenis@1.3.1/dist/lenis.min.js"></script>
<script>
// lenis smooth scroll
{
let lenis;
const initScroll = () => {
lenis = new Lenis({});
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);
};
function initGsapGlobal() {
/** Do everything that needs to happen
* before triggering all
* the gsap animations */
initScroll();
// match reduced motion media
// const media = gsap.matchMedia();
/** Send a custom
* event to all your
* gsap animations
* to start them */
const sendGsapEvent = () => {
window.dispatchEvent(
new CustomEvent("GSAPReady", {
detail: {
lenis,
},
})
);
};
// Check if fonts are already loaded
if (document.fonts.status === "loaded") {
sendGsapEvent();
} else {
document.fonts.ready.then(() => {
sendGsapEvent();
});
}
/** We need specific handling because the
* grid/list changes the scroll height of the whole container
*/
let resizeTimeout;
const onResize = () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
ScrollTrigger.refresh();
}, 50);
};
window.addEventListener("resize", () => onResize());
const resizeObserver = new ResizeObserver((entries) => onResize());
resizeObserver.observe(document.body);
queueMicrotask(() => {
gsap.to("[data-start='hidden']", {
autoAlpha: 1,
duration: 0.1,
delay: 0.2,
});
});
}
// this only for dev
const documentReady =
document.readyState === "complete" || document.readyState === "interactive";
if (documentReady) {
initGsapGlobal();
} else {
addEventListener("DOMContentLoaded", (event) => initGsapGlobal());
}
}
</script>
<script>
// -----HERO SECTION------
// ---Hero title animation---
const split = new SplitText(".hero-title", { type: "chars" });
// Set initial opacity to 0 for all characters
gsap.set(split.chars, {
opacity: 0,
filter : "blur(1vw)"
});
// Animate characters in
gsap.to(split.chars, {
opacity: 1,
filter : "blur(0vw)",
duration: 1,
stagger: {
each: 0.8 / split.chars.length,
from: "random"
},
ease: "power2.out"
});
// END hero title animation'
// Hero Image Flicker
document.addEventListener("DOMContentLoaded", () => {
const frame = document.querySelector(".background-image-frame");
const images = gsap.utils.toArray(".hero-background-image");
// Show first image initially
gsap.set(images, { autoAlpha: 0 });
gsap.set(images[0], { autoAlpha: 1 });
// ---- Animation 1: Image sequence ----
gsap.to(images, {
delay: 0.75, // wait before animation starts
autoAlpha: 1,
duration: 0.15,
stagger: { each: 0.15, from: "start" },
ease: "power1.inOut",
onStart: () => {
// Hide first image just before starting sequence
gsap.set(images[0], { autoAlpha: 0 });
},
onComplete: () => {
gsap.set(images.slice(0, -1), { autoAlpha: 0 });
gsap.set(images[images.length - 1], { autoAlpha: 1 });
}
});
// ---- Animation 2: Frame growth ----
gsap.set(frame, { width: "20vw", height: "20vw" });
gsap.to(frame, {
delay: 0.75, // same delay
width: "100vw",
height: "100vh",
duration: 2,
ease: "power2.inOut"
});
});
// END Hero Image Flicker
</script>
<style>
/* SplitText character style Bug fix for word wrapping */
.split-about-word {
display: inline-block;
white-space: normal;
word-break: normal;
}
.split-about-char {
display: inline-block;
white-space: normal;
word-break: normal;
}
/* Ensure parent allows wrapping to fix splittext Bug*/
.medium-text.about-text {
white-space: normal !important;
overflow-wrap: break-word;
word-wrap: break-word;
}
</style>
<script>
// -----ABOUT SECTION------
// about text animation
// Create SplitText instance
const splitAbout = new SplitText(".medium-text.about-text", {
type: "words,chars",
charsClass: "split-about-char",
wordsClass: "split-about-word"
});
// Set initial styles
gsap.set(splitAbout.chars, {
opacity: 0,
filter: "blur(1vw)"
});
// Animate on scroll in and reverse on scroll out (up)
ScrollTrigger.create({
trigger: ".section-main.about-section",
start: "top top",
end: "bottom top",
onEnter: () => {
gsap.to(splitAbout.chars, {
opacity: 1,
filter: "blur(0vw)",
duration: 1,
stagger: {
each: 0.8 / splitAbout.chars.length,
from: "random"
},
ease: "power2.out"
});
},
onLeaveBack: () => {
gsap.to(splitAbout.chars, {
opacity: 0,
filter: "blur(1vw)",
duration: 0.5,
stagger: {
each: 0.8 / splitAbout.chars.length,
from: "random"
},
ease: "power2.in"
});
}
});
// END about text animation
// about image trail
const trailEls = gsap.utils.toArray(".trail-image-wrap");
let imgIndex = 0;
let mouse = { x: 0, y: 0 };
let lastMouse = { x: 0, y: 0 };
let smoothed = { x: 0, y: 0 };
const threshold = 130;
// Capture mouse position
window.addEventListener("mousemove", (e) => {
mouse.x = e.clientX;
mouse.y = e.clientY;
});
// Distance check
const getDistance = (a, b) => {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
};
function animate() {
smoothed.x += (mouse.x - smoothed.x) * 0.15;
smoothed.y += (mouse.y - smoothed.y) * 0.15;
const dist = getDistance(mouse, lastMouse);
if (dist > threshold) {
showNextImage(smoothed.x, smoothed.y);
lastMouse.x = mouse.x;
lastMouse.y = mouse.y;
}
requestAnimationFrame(animate);
}
function showNextImage(x, y) {
const el = trailEls[imgIndex % trailEls.length];
imgIndex++;
gsap.killTweensOf(el);
// Determine size based on viewport
let imgSize = "15vw";
if (window.innerWidth >= 256 && window.innerWidth <= 428) {
imgSize = "30vw";
}
// Bring it to the front
gsap.set(el, {
x: x,
y: y,
width: "0vw",
height: "0vw",
opacity: 0,
zIndex: imgIndex // increase with each image
});
// Animate in
gsap.to(el, {
duration: 0.4,
ease: "power3.out",
opacity: 1,
width: imgSize,
height: imgSize,
x: x,
y: y,
scale: 1
});
// Animate out
gsap.to(el, {
delay: 0.75,
duration: 0.7,
ease: "power2.inOut",
opacity: 0,
width: "0vw",
height: "0vw",
scale: 0.3
});
}
animate(); // Start the loop
// END about image trail
</script>
<script>
// -----PROJECT SECTION------
// DRAGGABLE Project
document.querySelectorAll('#container-draggable .drag-image').forEach((el, index) => {
// Set initial opacity and scale using GSAP (no need for random positioning anymore)
gsap.set(el, {
opacity: 0, // Start with opacity 0
scale: 0.9, // Start with scale 0.9
rotation: gsap.utils.random(-20, 20) // Optional: add random rotation for variety
});
// Create timeline for the appearance animation
const appearTimeline = gsap.timeline({ paused: true });
appearTimeline.to(el, {
opacity: 1, // Fade in to opacity 1
scale: 1, // Scale up to normal size
rotation: gsap.utils.random(-20, 20), // Random rotation for variety
duration: 1, // Duration of the appearance animation
ease: "back.out(1.7)" // Bounce ease for that small bounce effect
});
// ScrollTrigger to start the animation when the container comes into view
ScrollTrigger.create({
trigger: el,
start: "top 80%",
once: true,
onEnter: () => {
gsap.delayedCall(index * 0.2, () => { // Staggered start for each element
appearTimeline.play();
});
// Re-enable Draggable after the animation starts
Draggable.create(el, {
bounds: "#container-draggable",
inertia: true,
type: "x,y",
edgeResistance: 0.9,
throwProps: true,
throwResistance: 3000,
zIndexBoost: false
});
// π Add toggle-expand interaction
let isExpanded = false; // Track expanded state
gsap.set(el, { filter: "brightness(100%)" });
el.addEventListener("click", () => {
if (!isExpanded) {
// Expand to full screen
gsap.to(el, {
width: "100vw",
height: "100vh",
x: "0%",
y: "0%",
filter: "brightness(75%)",
top: "0%",
left: "0%",
bottom: "auto",
right: "auto",
position: "absolute",
margin: 0,
zIndex: 90,
rotation: 0,
duration: 0.6,
ease: "power3.inOut"
});
} else {
// Shrink back β decide size dynamically based on current viewport
let targetWidth = "20vw";
let targetHeight = "20vw";
if (window.innerWidth >= 768 && window.innerWidth <= 1024) {
targetWidth = "30vw";
targetHeight = "30vw";
} else if (window.innerWidth >= 480 && window.innerWidth < 720) {
targetWidth = "35vw";
targetHeight = "35vw";
} else if (window.innerWidth >= 256 && window.innerWidth <= 428) {
targetWidth = "65vw";
targetHeight = "65vw";
}
gsap.to(el, {
width: targetWidth,
height: targetHeight,
position: "absolute",
filter: "brightness(100%)",
top: "",
left: "",
bottom: "",
right: "",
margin: "auto",
zIndex: 1,
rotation: gsap.utils.random(-20, 20), // Random rotation each time
duration: 0.6,
ease: "power3.inOut"
});
}
isExpanded = !isExpanded;
});
}
});
});
// END DRAGGABLE
// Dragable image reveal text
// --- Hide all target texts on load ---
document.querySelectorAll(".drag-title, .drag-detail-service, .drag-case").forEach((text) => {
gsap.set(text, { opacity: 1 });
text.style.visibility = "hidden";
});
// Store SplitText states
const splitTextStates = new Map();
document.querySelectorAll(".drag-image").forEach((image) => {
image.addEventListener("click", () => {
const card = image.closest(".drag-card");
// Select all text elements
const textElements = card.querySelectorAll(".drag-title, .drag-detail-service, .drag-case");
// π Get the corresponding drag-link inside this card
const dragLink = card.querySelector(".drag-link");
textElements.forEach((el) => {
const state = splitTextStates.get(el) || { active: false, split: null };
if (!state.active) {
el.style.visibility = "visible";
const split = new SplitText(el, {
type: "chars",
charsClass: "split-char"
});
splitTextStates.set(el, { split, active: true });
gsap.set(split.chars, {
opacity: 0,
filter: "blur(1vw)"
});
// π Stagger logic
let staggerOptions;
if (el.classList.contains("drag-title")) {
// Random for title
staggerOptions = {
each: 1 / split.chars.length,
from: "random"
};
} else {
// Left to right for detail-service and case
staggerOptions = 0.02;
}
gsap.to(split.chars, {
opacity: 1,
filter: "blur(0vw)",
duration: 1,
color: "#FFFFFF",
stagger: staggerOptions,
ease: "power2.out"
});
// π Show the drag-link (link block)
if (dragLink) {
gsap.to(dragLink, {
opacity: 1,
pointerEvents: "auto",
duration: 0
});
}
} else {
gsap.to(el, {
opacity: 0,
duration: 0,
onComplete: () => {
state.split?.revert();
splitTextStates.set(el, { active: false, split: null });
el.style.visibility = "hidden";
gsap.set(el, { opacity: 1 });
}
});
// π Hide the drag-link again
if (dragLink) {
gsap.to(dragLink, {
opacity: 0,
pointerEvents: "none",
duration: 0
});
}
}
});
});
});
// END Dragable image reveal text
// LINK inside project card animation
gsap.registerPlugin(TextPlugin);
document.querySelectorAll(".drag-case").forEach((el) => {
const originalText = el.textContent;
let isTyping = false; // π flag to prevent repeat typing
el.addEventListener("mouseenter", () => {
if (isTyping) return; // β
Skip if already typing
isTyping = true;
el.textContent = "";
gsap.to(el, {
duration: 1,
text: originalText,
ease: "none",
onComplete: () => {
isTyping = false; // β
allow replay next time
}
});
});
el.addEventListener("mouseleave", () => {
// Optional: reset text if you want to replay each hover
// el.textContent = originalText;
// isTyping = false;
});
});
//END LInk inside project animation
</script>
<script>
// -----SERVICE SECTION------
// Service slider
gsap.registerPlugin(Draggable);
const gallery = document.querySelector(".service-gallery-slide");
Draggable.create(gallery, {
type: "x",
bounds: ".service-gallery", // container element
inertia: true
});
// END Service slider
// Service Counter
gsap.registerPlugin(ScrollTrigger);
ScrollTrigger.create({
trigger: ".service-counter",
start: "top 80%", // starts when the top of .service-counter is 80% down the viewport
once: true, // run only once
onEnter: () => {
document.querySelectorAll(".number").forEach(counter => {
let finalValue = parseInt(counter.textContent, 10); // read existing number
gsap.fromTo(counter,
{ innerText: 0 },
{
innerText: finalValue,
duration: 2,
ease: "power1.out",
snap: { innerText: 1 }
}
);
});
}
});
// END Service Counter
</script>
<script>
// ----- Testimonial Section -------
// Bouncy Slider for Testimonials
document.addEventListener("DOMContentLoaded", () => {
const track = document.querySelector(".testimonial-track");
const slides = document.querySelectorAll(".single-testimonial");
const prevBtn = document.querySelector(".previous-button");
const nextBtn = document.querySelector(".next-button");
let currentIndex = 0;
const totalSlides = slides.length;
function showSlide(index) {
gsap.to(track, {
x: `${-index * 100}%`,
duration: 0.6,
ease: "power2.inOut"
});
}
function bounce(direction) {
let overshoot = direction === "next"
? (-currentIndex * 100 - 5) // bounce left
: (-currentIndex * 100 + 5); // bounce right
gsap.to(track, {
x: `${overshoot}%`,
duration: 0.3,
ease: "power1.out",
onComplete: () => {
gsap.to(track, {
x: `${-currentIndex * 100}%`,
duration: 0.4,
ease: "power2.out"
});
}
});
}
nextBtn.addEventListener("click", () => {
if (currentIndex < totalSlides - 1) {
currentIndex++;
showSlide(currentIndex);
} else {
bounce("next");
}
});
prevBtn.addEventListener("click", () => {
if (currentIndex > 0) {
currentIndex--;
showSlide(currentIndex);
} else {
bounce("prev");
}
});
});
// END Bouncy Slider for Testimonials
</script>
<script>
// ----- Testimonial Section -------
// Bouncy Slider for Testimonials
document.addEventListener("DOMContentLoaded", () => {
const track = document.querySelector(".testimonial-track");
const slides = document.querySelectorAll(".single-testimonial");
const prevBtn = document.querySelector(".previous-button");
const nextBtn = document.querySelector(".next-button");
let currentIndex = 0;
const totalSlides = slides.length;
function showSlide(index) {
gsap.to(track, {
x: `${-index * 100}%`,
duration: 0.6,
ease: "power2.inOut"
});
}
function bounce(direction) {
let overshoot = direction === "next"
? (-currentIndex * 100 - 5) // bounce left
: (-currentIndex * 100 + 5); // bounce right
gsap.to(track, {
x: `${overshoot}%`,
duration: 0.3,
ease: "power1.out",
onComplete: () => {
gsap.to(track, {
x: `${-currentIndex * 100}%`,
duration: 0.4,
ease: "power2.out"
});
}
});
}
nextBtn.addEventListener("click", () => {
if (currentIndex < totalSlides - 1) {
currentIndex++;
showSlide(currentIndex);
} else {
bounce("next");
}
});
prevBtn.addEventListener("click", () => {
if (currentIndex > 0) {
currentIndex--;
showSlide(currentIndex);
} else {
bounce("prev");
}
});
});
// END Bouncy Slider for Testimonials
</script>
<script>
//-----CTA SECTION------
// CTA Title Animation
const splitContact = new SplitText(".contact-title", { type: "chars" });
// Set initial opacity & blur
gsap.set(splitContact.chars, {
opacity: 0,
filter: "blur(1vw)"
});
gsap.to(splitContact.chars, {
scrollTrigger: {
trigger: ".cta-wrap",
start: "top 25%", // when .cta-wrap center hits viewport center
end: "bottom top",
toggleActions: "play none none none" // play on enter, reverse on leave
},
opacity: 1,
filter: "blur(0vw)",
duration: 1,
stagger: {
each: 0.8 / splitContact.chars.length,
from: "random"
},
ease: "power2.out"
});
// END CTA Title Animation
//CTA Following button text type effect
document.addEventListener("DOMContentLoaded", () => {
const pointer = document.querySelector(".cursor-pointer");
const textEl = document.querySelector(".pointer-text");
const arrow = document.querySelector(".arrow-icon");
const textContent = textEl.textContent;
let typingTween;
// Initial state
textEl.textContent = "";
pointer.addEventListener("mouseenter", () => {
gsap.killTweensOf([arrow, textEl]);
if (typingTween) typingTween.kill();
// Fade out arrow
gsap.to(arrow, { opacity: 0, duration: 0.2, ease: "power2.out", delay: 0.25 });
// Typing effect
textEl.textContent = "";
typingTween = gsap.to({}, {
delay: 0.25,
duration: textContent.length * 0.05,
onUpdate: function() {
let progress = this.progress();
let chars = Math.floor(progress * textContent.length);
textEl.textContent = textContent.substring(0, chars);
}
});
});
pointer.addEventListener("mouseleave", () => {
gsap.killTweensOf([arrow, textEl]);
if (typingTween) typingTween.kill();
// Timeline with slight offset
const tl = gsap.timeline({ delay: 0.25 });
tl.to(textEl, { textContent: "", duration: 0.3, ease: "power1.inOut" }, 0)
.to(arrow, { opacity: 1, duration: 0.2, ease: "power2.inOut" }, 0.05); // arrow starts 0.05s after text fade begins
});
});
// END Following button text type effect
</script>