INSTRUCTION
Webflow Template User Guide
Webflow Template User Guide
<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 Text Bg Animation
gsap.to(".big-text.v1", {
backgroundPosition: "200% center",
duration: 20,
ease: "none",
repeat: -1,
yoyo: true
});
gsap.to(".big-text.v2", {
backgroundPosition: "200% center",
duration: 20,
ease: "nonet",
repeat: -1,
yoyo: true
});
gsap.to(".big-text.tight.v-1", {
backgroundPosition: "200% center",
duration: 20,
ease: "none",
repeat: -1,
yoyo: true
});
gsap.to(".big-text.tight.v-2", {
backgroundPosition: "200% center",
duration: 20,
ease: "none",
repeat: -1,
yoyo: true
});
gsap.to(".big-text.tight.v-3", {
backgroundPosition: "200% center",
duration: 20,
ease: "none",
repeat: -1,
yoyo: true
});
</script>
<script>
// Hero Image Trail Effect (Mouse + Touch Support)
document.addEventListener("DOMContentLoaded", () => {
// --- ELEMENTS & INITIAL SETUP ---
const wrapper = document.querySelector(".image-wrap");
if (!wrapper) return console.error("Missing .image-wrap container");
const images = gsap.utils.toArray(".content-img-wrap");
let mouse = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
let last = { x: mouse.x, y: mouse.y };
let cache = { x: mouse.x, y: mouse.y };
let index = 0;
let threshold = 80;
let activeImages = 0;
let idle = true;
// --- MOUSE MOVEMENT TRACKING ---
window.addEventListener("mousemove", (e) => {
mouse.x = e.clientX;
mouse.y = e.clientY;
});
// --- TOUCH SUPPORT: TAP + SWIPE ---
window.addEventListener("touchstart", (e) => {
const touch = e.touches[0];
mouse.x = touch.clientX;
mouse.y = touch.clientY;
showNextImage(); // trigger image on tap
});
window.addEventListener("touchmove", (e) => {
const touch = e.touches[0];
mouse.x = touch.clientX;
mouse.y = touch.clientY;
});
// --- GSAP TICKER LOOP ---
gsap.ticker.add(() => {
// Smooth follow (lerp effect)
cache.x = gsap.utils.interpolate(cache.x, mouse.x, 0.1);
cache.y = gsap.utils.interpolate(cache.y, mouse.y, 0.1);
// Distance check
const dist = Math.hypot(mouse.x - last.x, mouse.y - last.y);
if (dist > threshold) {
showNextImage();
last.x = mouse.x;
last.y = mouse.y;
}
// Reset index when idle
if (idle && index !== 1) index = 1;
});
// --- SHOW NEXT IMAGE FUNCTION ---
function showNextImage() {
index++;
const img = images[index % images.length];
const rect = img.getBoundingClientRect();
gsap.killTweensOf(img);
gsap.set(img, {
opacity: 0,
scale: 0,
zIndex: index,
x: cache.x - rect.width / 2,
y: cache.y - rect.height / 2
});
// Image trail animation
gsap.timeline({
onStart: () => { activeImages++; idle = false; },
onComplete: () => {
activeImages--;
if (activeImages === 0) idle = true;
}
})
.to(img, {
duration: 0.4,
opacity: 1,
scale: 1,
x: mouse.x - rect.width / 2,
y: mouse.y - rect.height / 2,
ease: "power2.out"
})
.to(img, {
duration: 0.8,
opacity: 0,
scale: 0.2,
ease: "power2.in",
x: cache.x - rect.width / 2,
y: cache.y - rect.height / 2
}, "+=0.3");
}
});
</script><!--SVG Filter CODE-->
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
<filter id="cyberGlitch">
<feTurbulence id="glitchNoise"
type="fractalNoise"
baseFrequency="0.02 0.8"
numOctaves="1"
seed="3"
result="noise" />
<feDisplacementMap id="glitchDisp"
in="SourceGraphic"
in2="noise"
scale="0"
xChannelSelector="R"
yChannelSelector="G"
result="displaced" />
<feOffset id="glitchRGB"
in="displaced"
dx="0"
dy="0"
result="rgbShift" />
<feBlend in="displaced" in2="rgbShift" mode="lighten" result="blended" />
<feComponentTransfer in="blended" result="final">
<feFuncR type="linear" slope="1.0" />
<feFuncG type="linear" slope="1.0" />
<feFuncB type="linear" slope="1.0" />
</feComponentTransfer>
</filter>
</svg>
<script>
//Glitch Effect
document.addEventListener("DOMContentLoaded", () => {
const cursor = document.querySelector('.cursor-pointer');
const cards = document.querySelectorAll('.single-project-card');
// SVG filter nodes
const turb = document.querySelector('#glitchNoise');
const disp = document.querySelector('#glitchDisp');
const rgb = document.querySelector('#glitchRGB');
if (!cursor || !turb || !disp) {
console.warn('Missing selector: .cursor-pointer or #glitchNoise/#glitchDisp. Check HTML.');
return;
}
// create a reusable timeline (paused)
const glitchTL = gsap.timeline({ paused: true, defaults: { ease: "power2.inOut" } })
.to(turb, { attr: { baseFrequency: "0.05 1.2" }, duration: 0.12 })
.to(disp, { attr: { scale: 60 }, duration: 0.12 }, "<")
.to(rgb, { attr: { dx: 6 }, duration: 0.12 }, "<")
.to(turb, { attr: { baseFrequency: "0.02 0.6" }, duration: 0.2 })
.to(disp, { attr: { scale: 30 }, duration: 0.2 }, "<")
.to(rgb, { attr: { dx: 3 }, duration: 0.2 }, "<")
.to(turb, { attr: { baseFrequency: "0 0" }, duration: 0.25 })
.to(disp, { attr: { scale: 0 }, duration: 0.25 }, "<")
.to(rgb, { attr: { dx: 0 }, duration: 0.25 }, "<");
// helper: reset filter attributes to "rest" state
function resetFilterAttrs() {
gsap.set(turb, { attr: { baseFrequency: "0 0" } });
gsap.set(disp, { attr: { scale: 0 } });
gsap.set(rgb, { attr: { dx: 0 } });
}
// ensure initial state
resetFilterAttrs();
cards.forEach(card => {
card.addEventListener('mouseenter', () => {
// 1) Make sure cursor has no filter (force reflow)
cursor.style.filter = 'none';
// Force reflow/read of layout to ensure removal is processed
// eslint-disable-next-line no-unused-expressions
cursor.offsetWidth;
// 2) Reset filter attributes (so animation always starts from same baseline)
resetFilterAttrs();
// 3) Re-apply the filter to the cursor
cursor.style.filter = 'url(#cyberGlitch)';
// 4) If timeline is active, restart it (restart(true) forces immediate restart)
// If it's currently playing, this will jump it back to start cleanly.
glitchTL.restart(true);
});
card.addEventListener('mouseleave', () => {
// optional: remove filter when leaving (so it only shows while hovered / animating)
// small timeout helps the timeline finish smoothing (tweak or remove as you like)
gsap.to({}, { duration: 0.05, onComplete: () => cursor.style.filter = 'none' });
});
});
// Optional: stop everything on page visibility change to avoid stuck state
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
glitchTL.pause(0);
resetFilterAttrs();
if (cursor) cursor.style.filter = 'none';
}
});
});
</script>
<script>
// TOUCHSCREEN CYBERGLITCH
document.addEventListener("DOMContentLoaded", () => {
// ====== ELEMENT REFERENCES ======
const triggers = document.querySelectorAll(".single-project-wrap");
const turb = document.querySelector("#glitchNoise");
const disp = document.querySelector("#glitchDisp");
const rgb = document.querySelector("#glitchRGB");
// ====== CHECK SVG FILTER NODES ======
if (!turb || !disp || !rgb) {
console.warn("⚠️ Missing SVG filter parts (#glitchNoise, #glitchDisp, #glitchRGB)");
return;
}
// ====== GLITCH TIMELINE ======
const glitchTL = gsap.timeline({
paused: true,
defaults: { ease: "power2.inOut" }
})
.to(turb, { attr: { baseFrequency: "0.05 1.2" }, duration: 0.12 })
.to(disp, { attr: { scale: 60 }, duration: 0.12 }, "<")
.to(rgb, { attr: { dx: 6 }, duration: 0.12 }, "<")
.to(turb, { attr: { baseFrequency: "0.02 0.6" }, duration: 0.2 })
.to(disp, { attr: { scale: 30 }, duration: 0.2 }, "<")
.to(rgb, { attr: { dx: 3 }, duration: 0.2 }, "<")
.to(turb, { attr: { baseFrequency: "0 0" }, duration: 0.25 })
.to(disp, { attr: { scale: 0 }, duration: 0.25 }, "<")
.to(rgb, { attr: { dx: 0 }, duration: 0.25 }, "<");
// ====== RESET FILTER ATTRIBUTES ======
function resetFilterAttrs() {
gsap.set(turb, { attr: { baseFrequency: "0 0" } });
gsap.set(disp, { attr: { scale: 0 } });
gsap.set(rgb, { attr: { dx: 0 } });
}
resetFilterAttrs();
// ====== PLAY GLITCH ON TARGET ELEMENT ======
function playGlitch(el) {
el.style.filter = "url(#cyberGlitch)";
glitchTL.restart(true);
gsap.delayedCall(glitchTL.duration(), () => {
el.style.filter = "none";
resetFilterAttrs();
});
}
// ====== VIEWPORT CHECK ======
function isTabletOrBelow() {
return window.innerWidth <= 991;
}
// ====== EVENT LISTENERS ======
triggers.forEach(trigger => {
const mobileButton = trigger.querySelector(".mobile-button");
if (!mobileButton) return; // Skip if no button found
// Touch devices
trigger.addEventListener("touchstart", e => {
if (isTabletOrBelow()) playGlitch(mobileButton);
}, { passive: true });
// Fallback for desktop testing
trigger.addEventListener("click", e => {
if (isTabletOrBelow()) playGlitch(mobileButton);
});
});
});
</script>