Refactor 'neler-yapariz' Template and Introduce New Sections: Updated the 'neler-yapariz' template to enhance its structure by adding new sections for footer, hero, inertia, marquee, and scrolltrigger. Introduced external CSS and JS files for improved styling and functionality. This update aims to enhance user experience through better organization and dynamic content integration.

This commit is contained in:
Ümit Tunç
2026-01-09 21:16:13 +03:00
parent b00ca5ea78
commit 3b852512f0
27 changed files with 6297 additions and 5696 deletions
File diff suppressed because one or more lines are too long
@@ -0,0 +1,132 @@
/** Text Highlight */
::selection {
background: var(--primary);
color: var(--secondary);
text-shadow: none;
}
::-moz-selection {
background: var(--primary);
color: var(--secondary);
text-shadow: none;
}
[data-select="inverse"] ::selection {
background: var(--secondary);
color: var(--primary);
text-shadow: none;
}
[data-select="inverse"] ::-moz-selection {
background: var(--secondary);
color: var(--primary);
text-shadow: none;
}
/** Font Smoothing */
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/** Toggle States */
[data-css="toggle"] {
.toggle-ball {
transform: translateX(0%);
transition: transform .3s ease-out;
}
}
[data-css="toggle"]:has(input:checked) {
.toggle-ball {
transform: translateX(100%);
background-color: #10CD00;
border-color: #10CD00;
}
}
img {
user-drag: none;
-webkit-user-drag: none;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
/* -- explainer css */
/* -- explainer css */
[data-css="explainer"] {
.explainer-expandable-w {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 0fr;
transition: grid-template-rows 0.5s;
}
.plus-icon-w {
transform: rotate(0deg);
transition: transform 0.4s ease-in-out;
}
.plus-icon-w > div:nth-child(1) {
transform: scaleX(1);
transition: transform 0.4s ease-in-out;
}
}
[data-css="explainer"]:has(input:checked) {
.explainer-expandable-w {
display: grid;
grid-template-rows: 1fr;
}
.plus-icon-w {
transform: rotate(90deg);
}
.plus-icon-w > div:nth-child(1) {
transform: scaleX(0);
}
}
@media (max-width: 599px)
{
[data-css="explainer"] {
.explainer-expandable-w {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
transition: grid-template-rows 0.5s;
}
}
}
/** -- toggle styles */
[data-css="toggle"] {
position: relative;
.toggle-slider {
box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.25);
}
.toggle-ball {
box-shadow: inset 0px 2px 4px rgba(0, 0, 0, 0.15);
}
}
/* -- sky hero mask */
.sky-hero-w {
clip-path: polygon(20% 13%, 65% 14%, 63% 90%, 20% 84%);
}
/* -- slider current style */
[data-animate="slider"] {
.current > div {
background-color: var(--green);
transition: background-color 0.3s ease-in-out 0.2s;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
<link href="https://cdn.prod.website-files.com/67fea42b19018db93e3fe132/css/gsap-webflow-offbrand.webflow.shared.1d2cc8aa4.min.css" rel="stylesheet" type="text/css"/><script type="text/javascript">!function(o,c){var n=c.documentElement,t=" w-mod-";n.className+=t+"js",("ontouchstart"in o||o.DocumentTouch&&c instanceof DocumentTouch)&&(n.className+=t+"touch")}(window,document);</script><link href="https://cdn.prod.website-files.com/67fea42b19018db93e3fe132/681222b3ec7ebf1b78b5aac3_favicon.jpg" rel="shortcut icon" type="image/x-icon"/><link href="https://cdn.prod.website-files.com/67fea42b19018db93e3fe132/68122359a3aa462b4e80111c_webclip.jpg" rel="apple-touch-icon"/><script>
history.scrollRestoration = "manual";
</script>
<link rel="stylesheet" href="https://unpkg.com/lenis@1.3.1/dist/lenis.css">
@@ -0,0 +1,10 @@
<!-- GSAP Core -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<!-- GSAP ScrollTrigger Plugin -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>
<!-- GSAP ScrollToPlugin -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollToPlugin.min.js"></script>
<!-- SplitType for text splitting -->
<script src="https://cdn.jsdelivr.net/npm/split-type@0.3.4/dist/index.umd.min.js"></script>
<script src="https://cdn.prod.website-files.com/gsap/3.13.0/SplitText.min.js" type="text/javascript"></script>
<script src="https://d3e54v103j8qbb.cloudfront.net/js/jquery-3.5.1.min.dc5e7f18c8.js?site=67fea42b19018db93e3fe132" type="text/javascript" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script><script src="https://cdn.prod.website-files.com/67fea42b19018db93e3fe132/js/webflow.751e0867.7204fca6be7878de.js" type="text/javascript"></script><script src="https://cdn.prod.website-files.com/gsap/3.13.0/gsap.min.js" type="text/javascript"></script><script src="https://cdn.prod.website-files.com/gsap/3.13.0/SplitText.min.js" type="text/javascript"></script><script src="https://cdn.prod.website-files.com/gsap/3.13.0/ScrollTrigger.min.js" type="text/javascript"></script><script src="https://cdn.prod.website-files.com/gsap/3.13.0/ScrollSmoother.min.js" type="text/javascript"></script><script src="https://cdn.prod.website-files.com/gsap/3.13.0/Flip.min.js" type="text/javascript"></script><script src="https://cdn.prod.website-files.com/gsap/3.13.0/ScrollToPlugin.min.js" type="text/javascript"></script><script src="https://cdn.prod.website-files.com/gsap/3.13.0/InertiaPlugin.min.js" type="text/javascript"></script><script src="https://cdn.prod.website-files.com/gsap/3.13.0/ScrambleTextPlugin.min.js" type="text/javascript"></script><script src="https://cdn.prod.website-files.com/gsap/3.13.0/Observer.min.js" type="text/javascript"></script><script src="https://cdn.prod.website-files.com/gsap/3.13.0/Draggable.min.js" type="text/javascript"></script><script src="https://cdn.prod.website-files.com/gsap/3.13.0/MotionPathPlugin.min.js" type="text/javascript"></script><script src="https://cdn.prod.website-files.com/gsap/3.13.0/MotionPathHelper.min.js" type="text/javascript"></script><script type="text/javascript">gsap.registerPlugin(SplitText,ScrollTrigger,ScrollSmoother,Flip,ScrollToPlugin,InertiaPlugin,ScrambleTextPlugin,Observer,Draggable,MotionPathPlugin,MotionPathHelper);</script><script src="https://unpkg.com/lenis@1.3.1/dist/lenis.min.js"></script>
@@ -0,0 +1,126 @@
{
/** -- Copy Button */
const copybtn = () => {
const copybtn = [...document.querySelectorAll("[data-module='copybtn']")];
// Create a map to store data for each button
const buttonDataMap = new Map();
// Single event listener that checks which button's data to use
document.addEventListener("copy", (event) => {
const activeButtonData = buttonDataMap.get("active");
if (activeButtonData) {
event.preventDefault();
event.clipboardData.setData(
"application/json",
JSON.stringify(activeButtonData)
);
event.clipboardData.setData(
"text/plain",
JSON.stringify(activeButtonData)
);
buttonDataMap.delete("active"); // Clear after copying
}
});
copybtn.forEach((btn) => {
const url = btn.getAttribute("data-copy");
const text = btn.children[0].children[0];
const icon = btn.querySelector("svg");
let isAnimating = false;
btn.onmouseenter = async () => {
if (isAnimating) return;
isAnimating = true;
await gsap.to(icon, {
rotate: "+=360",
duration: 0.3,
ease: "expo.out",
});
isAnimating = false;
};
btn.onclick = async (evt) => {
try {
const res = await fetch(url);
let data = await res.text();
// Try to parse as JSON in case it's JSON data
try {
const jsonData = JSON.parse(data);
buttonDataMap.set("active", jsonData);
} catch {
buttonDataMap.set("active", data);
}
// Trigger the copy command
document.execCommand("copy");
text.textContent = "Copied!";
console.log("copied successfully");
setTimeout(() => {
text.textContent = "Copy this";
}, 1000);
} catch (error) {
console.error("Failed to copy:", error);
text.textContent = "Failed to copy";
setTimeout(() => {
text.textContent = "Copy this";
}, 1000);
}
};
});
};
/** -- Split Text Button */
const split = (text, type = "chars") => {
text.setAttribute("aria-label", text.textContent);
const splits = new SplitText(text, { type });
splits[type].forEach((char) => char.setAttribute("aria-hidden", "true"));
return splits[type];
};
const buttons = () => {
const buttons = [...document.querySelectorAll("[data-module='btn']")];
buttons.forEach((btn) => {
const overflow = btn.children[0];
const text = overflow.children[0];
overflow.appendChild(text.cloneNode(true));
const splitText1 = split(overflow.children[0], "chars");
const splitText2 = split(overflow.children[1], "chars");
const animation = {
yPercent: "-=100",
duration: 0.8,
ease: "expo.out",
stagger: 0.02,
};
btn.onmouseenter = () => {
gsap.to(splitText1, { ...animation, yPercent: -100 });
gsap.to(splitText2, { ...animation, yPercent: -100 });
};
btn.onmouseleave = () => {
gsap.to(splitText1, { ...animation, yPercent: 0 });
gsap.to(splitText2, { ...animation, yPercent: 0 });
};
});
};
/** -- Controller */
addEventListener("GSAPReady", (event) => {
copybtn();
// desktop only animations
if (window.matchMedia("(min-width: 1024px)").matches) {
buttons();
}
});
}
@@ -0,0 +1,30 @@
// https://codepen.io/GreenSock/pen/RwLepdQ?editors=1010
addEventListener("GSAPReady", (event) => {
const wrapper = document.querySelector("[data-animate='flip']");
const toggles = [...wrapper.querySelector("[data-flip='toggle']").children];
const items = wrapper.querySelector("[data-flip='item']");
// console.log("flip", wrapper, toggles, items);
const flipGridList = () => {
const state = Flip.getState(items.children);
items.classList.toggle("list");
Flip.from(state, {
duration: 0.8,
ease: "expo.out",
onComplete: () => {
ScrollTrigger.refresh();
}
});
};
toggles.forEach((toggle, index) => {
toggle.addEventListener("click", () => {
toggles[0].classList.toggle("current");
toggles[1].classList.toggle("current");
flipGridList();
});
});
});
@@ -0,0 +1,120 @@
// Single event listener that checks which button's data to use
document.addEventListener("copy", (event) => {
const activeButtonData = buttonDataMap.get("active");
if (activeButtonData) {
event.preventDefault();
event.clipboardData.setData(
"application/json",
JSON.stringify(activeButtonData)
);
event.clipboardData.setData(
"text/plain",
JSON.stringify(activeButtonData)
);
buttonDataMap.delete("active"); // Clear after copying
}
});
copybtn.forEach((btn) => {
const url = btn.getAttribute("data-copy");
const text = btn.children[0].children[0];
const icon = btn.querySelector("svg");
let isAnimating = false;
btn.onmouseenter = async () => {
if (isAnimating) return;
isAnimating = true;
await gsap.to(icon, {
rotate: "+=360",
duration: 0.3,
ease: "expo.out",
});
isAnimating = false;
};
btn.onclick = async (evt) => {
try {
const res = await fetch(url);
let data = await res.text();
// Try to parse as JSON in case it's JSON data
try {
const jsonData = JSON.parse(data);
buttonDataMap.set("active", jsonData);
} catch {
buttonDataMap.set("active", data);
}
// Trigger the copy command
document.execCommand("copy");
text.textContent = "Copied!";
console.log("copied successfully");
setTimeout(() => {
text.textContent = "Copy this";
}, 1000);
} catch (error) {
console.error("Failed to copy:", error);
text.textContent = "Failed to copy";
setTimeout(() => {
text.textContent = "Copy this";
}, 1000);
}
};
});
};
/** -- Split Text Button */
const split = (text, type = "chars") => {
text.setAttribute("aria-label", text.textContent);
const splits = new SplitText(text, { type });
splits[type].forEach((char) => char.setAttribute("aria-hidden", "true"));
return splits[type];
};
const buttons = () => {
const buttons = [...document.querySelectorAll("[data-module='btn']")];
buttons.forEach((btn) => {
const overflow = btn.children[0];
const text = overflow.children[0];
overflow.appendChild(text.cloneNode(true));
const splitText1 = split(overflow.children[0], "chars");
const splitText2 = split(overflow.children[1], "chars");
const animation = {
yPercent: "-=100",
duration: 0.8,
ease: "expo.out",
stagger: 0.02,
};
btn.onmouseenter = () => {
gsap.to(splitText1, { ...animation, yPercent: -100 });
gsap.to(splitText2, { ...animation, yPercent: -100 });
};
btn.onmouseleave = () => {
gsap.to(splitText1, { ...animation, yPercent: 0 });
gsap.to(splitText2, { ...animation, yPercent: 0 });
};
});
};
/** -- Controller */
addEventListener("GSAPReady", (event) => {
copybtn();
// desktop only animations
if (window.matchMedia("(min-width: 1024px)").matches) {
buttons();
}
});
}
</div>
@@ -0,0 +1,69 @@
{
addEventListener("GSAPReady", (event) => {
const wrapper = document.querySelector("[data-animate='footerlogo']");
if (!wrapper) return;
const paths = [...wrapper.querySelectorAll("path")].reverse();
const lastPath = paths[paths.length - 1];
// Configuration for the footer logo animation
const FOOTER_ANIMATION_CONFIG = {
STAGGER: 0.05,
VISIBILITY_DURATION: 0.1,
};
// Set initial state
gsap.set(paths, {
autoAlpha: 0,
});
const animateFooterLogo = () => {
// Reset the last path visibility before starting the animation
gsap.set(lastPath, { autoAlpha: 0 });
const tl = gsap.timeline();
paths.forEach((path, index) => {
const baseDelay = index * FOOTER_ANIMATION_CONFIG.STAGGER;
const isLastPath = index === paths.length - 1;
// Flash animation
tl.to(
path,
{
autoAlpha: 1,
duration: 0.1,
ease: "none",
},
baseDelay
);
// Only fade out if it's not the last path
if (!isLastPath) {
tl.to(
path,
{
autoAlpha: 0,
duration: 0.1,
ease: "none",
},
baseDelay + FOOTER_ANIMATION_CONFIG.VISIBILITY_DURATION
);
}
});
return tl;
};
// Create the scroll trigger
ScrollTrigger.create({
trigger: wrapper,
start: "top bottom",
end: "bottom top",
onEnter: () => animateFooterLogo(),
onEnterBack: () => animateFooterLogo(),
});
// console.log(event.detail);
});
}
@@ -0,0 +1,79 @@
{
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());
}
}
@@ -0,0 +1,263 @@
{
const split = (text, type = "chars") => {
// Handle both single elements and arrays of elements
const elements = Array.isArray(text) ? text : [text];
// Set aria labels
elements.forEach((element) => {
if (element && typeof element.setAttribute === "function") {
element.setAttribute("aria-label", element.textContent);
}
});
// Split text and set aria-hidden
const splits = new SplitText(text, { type });
splits[type].forEach((char) => {
if (char && typeof char.setAttribute === "function") {
char.setAttribute("aria-hidden", "true");
}
});
return splits[type];
};
/** -- Setup and Utils */
let titleSplit,
parSplit,
layerItems,
isIn = false;
const animateIn = async (delay = 0) => {
isIn = true;
animateStripesOut();
gsap.to(parSplit, {
autoAlpha: 1,
yPercent: 0,
duration: 0.8,
delay: 0.65,
ease: "expo.out",
stagger: {
each: 0.08,
},
});
await gsap.to(titleSplit.flat(), {
yPercent: 0,
duration: 1.2,
delay: delay,
ease: "expo.out",
stagger: {
each: 0.01,
from: "end",
},
});
};
const animateOut = async () => {
isIn = false;
animateStripesIn();
gsap.to(parSplit, {
autoAlpha: 0,
yPercent: 100,
duration: 0.8,
ease: "expo.out",
stagger: {
each: 0.08,
},
});
await gsap.to(titleSplit.flat(), {
yPercent: 110,
duration: 0.8,
ease: "expo.out",
delay: 0.1,
stagger: {
each: 0.01,
from: "center",
},
});
};
/** -- Scribble Animaiton */
let isScribbling = false;
const SCRIBBLE_CONFIG = {
ROTATION_RANGE: 20,
TIME_BETWEEN_SCRIBBLES: 0.015,
VISIBILITY_DURATION: 0.01,
REVERSE_ORDER: true,
};
const animateScribble = (scribble) => {
if (isScribbling) return;
isScribbling = true;
gsap.set(scribble, { autoAlpha: 0, rotation: 0 });
const tl = gsap.timeline();
const scribblesToAnimate = [...scribble].reverse();
scribblesToAnimate.forEach((item, index) => {
const randomRotation =
Math.random() * SCRIBBLE_CONFIG.ROTATION_RANGE -
SCRIBBLE_CONFIG.ROTATION_RANGE / 2;
const baseDelay = index * SCRIBBLE_CONFIG.TIME_BETWEEN_SCRIBBLES;
tl.add(
gsap.set(item, {
autoAlpha: 1,
rotation: randomRotation,
delay: baseDelay,
})
);
tl.add(
gsap.set(item, {
autoAlpha: 0,
delay: baseDelay + SCRIBBLE_CONFIG.VISIBILITY_DURATION,
})
);
});
tl.call(() => {
isScribbling = false;
});
};
// hero stripes animation
let heroStripes,
stripes,
stripesAnimation = null;
const calcX = (i) => {
return i % 2 === 0
? -heroStripes[i].scrollWidth
: heroStripes[i].scrollWidth;
};
const animateStripesIn = () => {
if (stripesAnimation) {
stripesAnimation.kill();
}
// play video on the hero
document.querySelector("video").play()
gsap.set(heroStripes, {
autoAlpha: 1,
x: (i) => calcX(i),
yPercent: (i) => (heroStripes.length / 2 - i) * 80,
});
gsap.set(stripes, {
rotation: (i) => Math.random() * 30,
ease: "expo.out",
});
stripesAnimation = gsap.to(heroStripes, {
x: 0,
delay: 0.2,
duration: (i) => Math.random() + 1.2,
ease: "expo.out",
});
};
const animateStripesOut = () => {
if (stripesAnimation) {
stripesAnimation.kill();
}
stripesAnimation = gsap.to(heroStripes, {
x: (i) => calcX(i),
duration: 0.8,
ease: "expo.out",
});
};
addEventListener("GSAPReady", (event) => {
const wrapper = document.querySelector("[data-module='hero']");
if (!wrapper) return;
const layer = wrapper.querySelector("[data-hero='layer']");
layerItems = layer.children;
const title = wrapper.querySelector("[data-hero='title']");
const toggle = wrapper.querySelector("[data-hero='toggle']");
const alpha = document.querySelectorAll("[data-hero='alpha']");
const par = document.querySelectorAll("[data-hero='par']");
const scribble = [
...document.querySelector("[data-hero='scribble']").children[0].children,
];
stripes = [...layer.children];
heroStripes = stripes.map((item) => item.children[0]);
gsap.set(heroStripes, { autoAlpha: 0 });
gsap.to(layer, {
yPercent: 10,
scrollTrigger: {
trigger: layer,
start: "top top",
end: "+=100%",
scrub: true,
},
});
// split
titleSplit = split(title.children, "chars");
parSplit = split(par, "lines");
// initial state
gsap.set(titleSplit.flat(), { yPercent: -110 });
gsap.set(layer, { autoAlpha: 1 });
gsap.set(toggle, { scale: 3, rotation: 360 });
gsap.set(parSplit, { autoAlpha: 0, yPercent: 100 });
gsap.set(alpha, { autoAlpha: 0, scale: 0.7 });
gsap.set(scribble, { autoAlpha: 0 });
animateIn(0.6);
gsap.to(toggle, {
scale: 1,
rotation: 0,
delay: 0,
duration: 1.2,
ease: "expo.inOut",
});
gsap.to(parSplit, {
autoAlpha: 1,
yPercent: 0,
duration: 0.8,
delay: 0.65,
ease: "expo.out",
stagger: {
each: 0.08,
},
});
gsap.to(alpha, {
autoAlpha: 1,
scale: 1,
duration: 0.8,
delay: 0.9,
ease: "expo.out",
});
toggle.querySelector("input").addEventListener("change", (event) => {
if (event.target.checked) {
animateOut();
} else {
animateIn();
}
});
toggle.onmouseenter = () => {
if (!isIn) return;
// console.log("mouseenter");
animateScribble(scribble);
};
});
}
@@ -0,0 +1,165 @@
{
/** Utility Functions */
const split = (text, type = "chars") => {
text.setAttribute("aria-label", text.textContent);
const splits = new SplitText(text, { type });
splits[type].forEach((char) => char.setAttribute("aria-hidden", "true"));
return splits[type];
};
const getParams = (item) => {
const { duration, delay, stagger, ease, type } = item.dataset;
const params = {
duration: 1.2,
delay: 0,
type: "chars",
};
if (duration) params.duration = parseFloat(duration);
if (delay) params.delay = parseFloat(delay);
if (type && (type === "chars" || type === "words" || type === "lines"))
params.type = type;
return params;
};
const baseAnimation = (item) => {
const params = getParams(item);
return {
duration: params.duration,
delay: params.delay,
ease: "expo.out",
// stagger each item so the letters come in one after the other
stagger: {
each: 0.04,
from: "start",
},
scrollTrigger: {
trigger: item,
// use scrolltrigger to trigger on enter and reset on exit
toggleActions: "play reset play reset",
},
};
};
/** (1) Split Reveal Animation */
function createSplitRevealAnimation(item) {
const params = getParams(item); // 1. get params from attributes
const text = split(item, params.type); // 2. split the text
item.style.overflow = "hidden"; // 3. set container to overflow
// 4. create the animation
return gsap.fromTo(
text,
{ yPercent: 120 },
{
...baseAnimation(item),
yPercent: 0,
}
);
}
function createScrambledAnimation(item) {
const text = item.textContent;
const randomText = Array(text.length)
.fill()
.map(() => String.fromCharCode(Math.floor(Math.random() * (90 - 65) + 65)))
.join("")
.toLowerCase();
return gsap.fromTo(
item.children[0],
{
scrambleText: randomText,
},
{
...baseAnimation(item),
scrambleText: text,
chars: text,
}
);
}
function createAlphaAnimation(item) {
const params = getParams(item); // 1. get params from attributes
const text = split(item, "chars"); // 2. split the text
return gsap.fromTo(
text,
{ autoAlpha: 0 },
{
...baseAnimation(item),
autoAlpha: 1,
stagger: {
each: 0.02,
from: "random",
},
}
);
}
function createSideAnimation(item) {
const params = getParams(item);
const outer = split(item, "chars");
outer.forEach((char) => {
char.style.overflow = "hidden";
});
const inner = split(item.children[0], "chars");
return gsap.fromTo(
inner,
{ x: 100 },
{
...baseAnimation(item),
x: 0,
stagger: {
each: 0.02,
from: "end",
},
}
);
}
function createUpDownAnimation(item) {
const params = getParams(item);
const outer = split(item, "chars");
outer.forEach((char) => {
char.style.overflow = "hidden";
});
const inner = split(item.children[0], "chars");
return gsap.fromTo(
inner,
{
yPercent: (i) => {
return i % 2 === 0 ? 100 : -100;
},
},
{
...baseAnimation(item),
yPercent: 0,
stagger: {
each: 0.02,
from: "random",
},
}
);
}
/** Main Text Effect Wrapper */
addEventListener("GSAPReady", (event) => {
const texts = [...document.querySelectorAll('[data-animate="text"]')];
texts.forEach((item) => {
if (item.dataset.type === "chars") createSplitRevealAnimation(item);
else if (item.dataset.type === "words") createSplitRevealAnimation(item);
else if (item.dataset.type === "scramble") createScrambledAnimation(item);
else if (item.dataset.type === "alpha") createAlphaAnimation(item);
else if (item.dataset.type === "side") createSideAnimation(item);
else if (item.dataset.type === "updown") createUpDownAnimation(item);
});
});
}
@@ -0,0 +1,30 @@
addEventListener("GSAPReady", (event) => {
// console.log("marquee");
const wrapper = document.querySelector("[data-animate='marquee']");
if (!wrapper) return;
const sections = [...wrapper.querySelectorAll("[data-marquee='line']")];
gsap.fromTo(
sections,
{
x: (i) =>
i % 2 === 0
? sections[i].offsetWidth / 4
: -sections[i].offsetWidth / 4,
},
{
x: (i) =>
i % 2 === 0
? -sections[i].offsetWidth / 4
: sections[i].offsetWidth / 4,
scrollTrigger: {
trigger: wrapper,
start: "top bottom",
end: "bottom top",
scrub: true,
// invalidateOnRefresh: true,
},
}
);
});
@@ -0,0 +1,44 @@
{
const params = {
duration: 0.8,
ease: "expo.out",
};
const handleSecondaryVisibility = (secondary) => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
gsap.to(secondary, {
yPercent: 200,
...params,
});
} else {
gsap.to(secondary, {
yPercent: 0,
...params,
});
}
});
},
{
threshold: 0.02,
rootMargin: "-10% 0px",
}
);
const contentElement = document.getElementById("content");
if (contentElement) {
observer.observe(contentElement);
}
};
addEventListener("GSAPReady", (event) => {
const wrapper = document.querySelector("[data-module='nav']");
if (!wrapper) return;
const main = wrapper.children[0];
const secondary = wrapper.children[1].children[0];
handleSecondaryVisibility(secondary);
});
}
@@ -0,0 +1,108 @@
const split = (text, type = "chars") => {
text.setAttribute("aria-label", text.textContent);
const splits = new SplitText(text, { type });
splits[type].forEach((char) => char.setAttribute("aria-hidden", "true"));
return splits[type];
};
addEventListener("GSAPReady", (event) => {
const section = document.querySelector("[data-animate='videogrow']");
const video = section.querySelector("[data-videogrow='video']");
const img = video.children[0];
const title = split(
section.querySelector("[data-videogrow='title']"),
"chars"
);
const tl = gsap.timeline({
scrollTrigger: {
trigger: section,
start: "top center",
end: "bottom bottom+=100%",
invalidateOnRefresh: true,
scrub: 1,
},
});
gsap.set(
title,
{
scale: 0,
rotation: (i) => Math.random() * 360 - 180,
})
tl.to(
title,
{
scale: 1,
duration: 0.2,
rotation: 0,
ease: "expo.out",
stagger: {
each: 0.05,
from: "random",
},
}
);
tl.fromTo(
video,
{
clipPath: "inset(10% 50% 10% 50%)",
yPercent: 100,
},
{
ease: "power3",
clipPath: "inset(0% 0% 0% 0%)",
duration: 1,
yPercent: 0,
},
".3"
);
tl.fromTo(
video,
{
scale: 0.5,
// rotation: 180,
},
{
ease: "back.inOut(.2)",
scale: 1,
duration: 0.8,
// rotation: 0,
},
"<"
);
tl.fromTo(
img,
{
scale: 2.8,
yPercent: 40,
},
{
scale: 1.2,
duration: 0.8,
delay: 0.2,
yPercent: 0,
},
"<"
);
tl.to(video, {
scale: 0.9,
ease: "linear",
});
tl.to(
img,
{
scale: 1,
ease: "linear",
},
"<"
);
});
@@ -0,0 +1,61 @@
{
/** Section Selector */
const createObserver = (section, cb) => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && entry.intersectionRatio >= 0.1) {
cb(entry.target);
}
});
}, {
threshold: 0.2 // This means the callback will fire when at least 10% is visible
});
observer.observe(section);
};
addEventListener("GSAPReady", (event) => {
const { lenis } = event.detail;
const wrapper = document.querySelector("[data-animate='sectionselector']");
const items = [...wrapper.children];
const highlight = wrapper.querySelector("[data-selector='highlight']");
let currentIndex = 0;
const sections = [...document.querySelectorAll("section")];
sections.forEach((section, index) => {
createObserver(section, () => {
items[currentIndex].classList.remove("current");
currentIndex = index;
items[currentIndex].classList.add("current");
moveHighlightTo(index);
});
});
let flipAnimation = null
const moveHighlightTo = (index) => {
const state = Flip.getState(highlight);
items[index].appendChild(highlight);
if (flipAnimation) flipAnimation.kill()
flipAnimation = Flip.from(state, {
duration: 0.5,
ease: "expo.out",
});
};
items.forEach((item, index) => {
item.addEventListener("click", () => {
lenis.scrollTo("#" + items[index].dataset.to);
});
item.addEventListener("mouseenter", () => {
moveHighlightTo(index);
});
item.addEventListener("mouseleave", () => {
moveHighlightTo(currentIndex);
});
});
});
}
@@ -0,0 +1,378 @@
// https://codepen.io/GreenSock/pen/gOvvJee?editors=1010
// https://gsap.com/docs/v3/HelperFunctions/helpers/seamlessLoop/
// https://codepen.io/GreenSock/pen/gOvvJee?editors=1010
// https://gsap.com/docs/v3/HelperFunctions/helpers/seamlessLoop/
function horizontalLoop(items, config) {
let timeline;
items = gsap.utils.toArray(items);
config = config || {};
gsap.context(() => {
// use a context so that if this is called from within another context or a gsap.matchMedia(), we can perform proper cleanup like the "resize" event handler on the window
let onChange = config.onChange,
lastIndex = 0,
tl = gsap.timeline({
repeat: config.repeat,
onUpdate:
onChange &&
function () {
let i = tl.closestIndex();
if (lastIndex !== i) {
lastIndex = i;
onChange(items[i], i);
}
},
paused: config.paused,
defaults: { ease: "none" },
onReverseComplete: () =>
tl.totalTime(tl.rawTime() + tl.duration() * 100),
}),
length = items.length,
startX = items[0].offsetLeft,
times = [],
widths = [],
spaceBefore = [],
xPercents = [],
curIndex = 0,
indexIsDirty = false,
center = config.center,
pixelsPerSecond = (config.speed || 1) * 100,
snap =
config.snap === false ? (v) => v : gsap.utils.snap(config.snap || 1), // some browsers shift by a pixel to accommodate flex layouts, so for example if width is 20% the first element's width might be 242px, and the next 243px, alternating back and forth. So we snap to 5 percentage points to make things look more natural
timeOffset = 0,
container =
center === true
? items[0].parentNode
: gsap.utils.toArray(center)[0] || items[0].parentNode,
totalWidth,
getTotalWidth = () =>
items[length - 1].offsetLeft +
(xPercents[length - 1] / 100) * widths[length - 1] -
startX +
spaceBefore[0] +
items[length - 1].offsetWidth *
gsap.getProperty(items[length - 1], "scaleX") +
(parseFloat(config.paddingRight) || 0),
populateWidths = () => {
let b1 = container.getBoundingClientRect(),
b2;
// Reset all items to their natural position first
gsap.set(items, {
x: 0,
xPercent: 0,
clearProps: "transform",
});
// Force a reflow
items[0].offsetWidth;
// Get the container's new position after reset
startX = items[0].offsetLeft;
items.forEach((el, i) => {
// Get the natural width without any transforms
widths[i] = el.offsetWidth;
// Calculate the space between items
b2 = el.getBoundingClientRect();
spaceBefore[i] = b2.left - (i ? b1.right : b1.left);
b1 = b2;
// Reset xPercent to 0 for accurate calculations
xPercents[i] = 0;
});
// Set all items to their calculated positions
gsap.set(items, {
xPercent: (i) => xPercents[i],
});
// Calculate total width after all items are positioned
totalWidth = getTotalWidth();
},
timeWrap,
populateOffsets = () => {
timeOffset = center
? (tl.duration() * (container.offsetWidth / 2)) / totalWidth
: 0;
center &&
times.forEach((t, i) => {
times[i] = timeWrap(
tl.labels["label" + i] +
(tl.duration() * widths[i]) / 2 / totalWidth -
timeOffset
);
});
},
getClosest = (values, value, wrap) => {
let i = values.length,
closest = 1e10,
index = 0,
d;
while (i--) {
d = Math.abs(values[i] - value);
if (d > wrap / 2) {
d = wrap - d;
}
if (d < closest) {
closest = d;
index = i;
}
}
return index;
},
populateTimeline = () => {
let i, item, curX, distanceToStart, distanceToLoop;
tl.clear();
for (i = 0; i < length; i++) {
item = items[i];
curX = (xPercents[i] / 100) * widths[i];
distanceToStart = item.offsetLeft + curX - startX + spaceBefore[0];
distanceToLoop =
distanceToStart + widths[i] * gsap.getProperty(item, "scaleX");
tl.to(
item,
{
xPercent: snap(((curX - distanceToLoop) / widths[i]) * 100),
duration: distanceToLoop / pixelsPerSecond,
},
0
)
.fromTo(
item,
{
xPercent: snap(
((curX - distanceToLoop + totalWidth) / widths[i]) * 100
),
},
{
xPercent: xPercents[i],
duration:
(curX - distanceToLoop + totalWidth - curX) / pixelsPerSecond,
immediateRender: false,
},
distanceToLoop / pixelsPerSecond
)
.add("label" + i, distanceToStart / pixelsPerSecond);
times[i] = distanceToStart / pixelsPerSecond;
}
timeWrap = gsap.utils.wrap(0, tl.duration());
},
refresh = (deep) => {
let progress = tl.progress();
tl.progress(0, true);
// Reset only the x transforms while preserving other properties
gsap.set(items, {
x: 0,
xPercent: 0,
});
// Force a reflow
items[0].offsetWidth;
populateWidths();
deep && populateTimeline();
populateOffsets();
if (deep && tl.draggable && tl.paused()) {
tl.time(times[curIndex], true);
} else {
tl.progress(progress, true);
}
},
onResize = () => {
if (window.resizeTimeout) clearTimeout(window.resizeTimeout);
window.resizeTimeout = setTimeout(() => {
refresh(true);
}, 100);
},
proxy;
gsap.set(items, { x: 0 });
populateWidths();
populateTimeline();
populateOffsets();
window.addEventListener("resize", onResize);
function toIndex(index, vars) {
vars = vars || {};
Math.abs(index - curIndex) > length / 2 &&
(index += index > curIndex ? -length : length); // always go in the shortest direction
let newIndex = gsap.utils.wrap(0, length, index),
time = times[newIndex];
if (time > tl.time() !== index > curIndex && index !== curIndex) {
// if we're wrapping the timeline's playhead, make the proper adjustments
time += tl.duration() * (index > curIndex ? 1 : -1);
}
if (time < 0 || time > tl.duration()) {
vars.modifiers = { time: timeWrap };
}
curIndex = newIndex;
vars.overwrite = true;
gsap.killTweensOf(proxy);
return vars.duration === 0
? tl.time(timeWrap(time))
: tl.tweenTo(time, vars);
}
tl.toIndex = (index, vars) => toIndex(index, vars);
tl.closestIndex = (setCurrent) => {
let index = getClosest(times, tl.time(), tl.duration());
if (setCurrent) {
curIndex = index;
indexIsDirty = false;
}
return index;
};
tl.current = () => (indexIsDirty ? tl.closestIndex(true) : curIndex);
tl.next = (vars) => toIndex(tl.current() + 1, vars);
tl.previous = (vars) => toIndex(tl.current() - 1, vars);
tl.times = times;
tl.progress(1, true).progress(0, true); // pre-render for performance
if (config.reversed) {
tl.vars.onReverseComplete();
tl.reverse();
}
if (config.draggable && typeof Draggable === "function") {
proxy = document.createElement("div");
let wrap = gsap.utils.wrap(0, 1),
ratio,
startProgress,
draggable,
dragSnap,
lastSnap,
initChangeX,
wasPlaying,
align = () =>
tl.progress(
wrap(startProgress + (draggable.startX - draggable.x) * ratio)
),
syncIndex = () => tl.closestIndex(true);
typeof InertiaPlugin === "undefined" &&
console.warn(
"InertiaPlugin required for momentum-based scrolling and snapping. https://greensock.com/club"
);
draggable = Draggable.create(proxy, {
trigger: items[0].parentNode,
type: "x",
onPressInit() {
let x = this.x;
gsap.killTweensOf(tl);
wasPlaying = !tl.paused();
tl.pause();
startProgress = tl.progress();
refresh();
ratio = 1 / totalWidth;
initChangeX = startProgress / -ratio - x;
gsap.set(proxy, { x: startProgress / -ratio });
},
onDrag: align,
onThrowUpdate: align,
overshootTolerance: 0,
inertia: true,
snap(value) {
//note: if the user presses and releases in the middle of a throw, due to the sudden correction of proxy.x in the onPressInit(), the velocity could be very large, throwing off the snap. So sense that condition and adjust for it. We also need to set overshootTolerance to 0 to prevent the inertia from causing it to shoot past and come back
if (Math.abs(startProgress / -ratio - this.x) < 10) {
return lastSnap + initChangeX;
}
let time = -(value * ratio) * tl.duration(),
wrappedTime = timeWrap(time),
snapTime = times[getClosest(times, wrappedTime, tl.duration())],
dif = snapTime - wrappedTime;
Math.abs(dif) > tl.duration() / 2 &&
(dif += dif < 0 ? tl.duration() : -tl.duration());
lastSnap = (time + dif) / tl.duration() / -ratio;
return lastSnap;
},
onRelease() {
syncIndex();
draggable.isThrowing && (indexIsDirty = true);
},
onThrowComplete: () => {
syncIndex();
wasPlaying && tl.play();
},
})[0];
tl.draggable = draggable;
}
tl.closestIndex(true);
lastIndex = curIndex;
onChange && onChange(items[curIndex], curIndex);
timeline = tl;
return () => window.removeEventListener("resize", onResize); // cleanup
});
return timeline;
}
/* -- Global Params */
const sliderParams = {
duration: 0.5,
ease: "power2.out",
};
addEventListener("GSAPReady", (event) => {
const wrapper = document.querySelector("[data-animate='slider']");
const slides = [...wrapper.querySelector("[data-slider='slides']").children];
/* -- Setup Slider UI */
const progress = wrapper.querySelector("[data-slider='progress']");
const next = wrapper.querySelector("[data-slider='next']");
const prev = wrapper.querySelector("[data-slider='prev']");
let currentIndex = 0;
const dots = [...wrapper.querySelector("[data-slider='dots']").children].map(
(item, index) => {
const btn = item.children[0];
btn.addEventListener("click", () => {
loop.toIndex(index, sliderParams);
});
return btn;
}
);
/* -- Helper Functions */
const updateCurrent = (index) => {
// handle dots
dots[currentIndex].classList.remove("current");
dots[index].classList.add("current");
slides[currentIndex].classList.remove("current");
slides[index].classList.add("current");
// handle slides
// ...
gsap.to(progress.children[0], {
width: `${(index / (slides.length - 1)) * 100}%`,
duration: 0.6,
ease: "expo.out",
});
// save the previous position
currentIndex = index;
};
/* -- Setup Slider */
let loop;
setTimeout(() => {
loop = horizontalLoop(slides, {
paused: true,
draggable: true,
// draggableOptions: {
// dragResistance: 10,
// resistance: 10,
// },
center: true,
onChange: (element, index) => {
updateCurrent(index);
},
});
}, 500);
// (*) THIS IS JUST TO CHECK PLEASE REMOVE
next.addEventListener("click", () => {
loop.next(sliderParams);
});
prev.addEventListener("click", () => {
loop.previous(sliderParams);
});
});
@@ -0,0 +1,165 @@
{
/** Utility Functions */
const split = (text, type = "chars") => {
text.setAttribute("aria-label", text.textContent);
const splits = new SplitText(text, { type });
splits[type].forEach((char) => char.setAttribute("aria-hidden", "true"));
return splits[type];
};
const getParams = (item) => {
const { duration, delay, stagger, ease, type } = item.dataset;
const params = {
duration: 1.2,
delay: 0,
type: "chars",
};
if (duration) params.duration = parseFloat(duration);
if (delay) params.delay = parseFloat(delay);
if (type && (type === "chars" || type === "words" || type === "lines"))
params.type = type;
return params;
};
const baseAnimation = (item) => {
const params = getParams(item);
return {
duration: params.duration,
delay: params.delay,
ease: "expo.out",
// stagger each item so the letters come in one after the other
stagger: {
each: 0.04,
from: "start",
},
scrollTrigger: {
trigger: item,
// use scrolltrigger to trigger on enter and reset on exit
toggleActions: "play reset play reset",
},
};
};
/** (1) Split Reveal Animation */
function createSplitRevealAnimation(item) {
const params = getParams(item); // 1. get params from attributes
const text = split(item, params.type); // 2. split the text
item.style.overflow = "hidden"; // 3. set container to overflow
// 4. create the animation
return gsap.fromTo(
text,
{ yPercent: 120 },
{
...baseAnimation(item),
yPercent: 0,
}
);
}
function createScrambledAnimation(item) {
const text = item.textContent;
const randomText = Array(text.length)
.fill()
.map(() => String.fromCharCode(Math.floor(Math.random() * (90 - 65) + 65)))
.join("")
.toLowerCase();
return gsap.fromTo(
item.children[0],
{
scrambleText: randomText,
},
{
...baseAnimation(item),
scrambleText: text,
chars: text,
}
);
}
function createAlphaAnimation(item) {
const params = getParams(item); // 1. get params from attributes
const text = split(item, "chars"); // 2. split the text
return gsap.fromTo(
text,
{ autoAlpha: 0 },
{
...baseAnimation(item),
autoAlpha: 1,
stagger: {
each: 0.02,
from: "random",
},
}
);
}
function createSideAnimation(item) {
const params = getParams(item);
const outer = split(item, "chars");
outer.forEach((char) => {
char.style.overflow = "hidden";
});
const inner = split(item.children[0], "chars");
return gsap.fromTo(
inner,
{ x: 100 },
{
...baseAnimation(item),
x: 0,
stagger: {
each: 0.02,
from: "end",
},
}
);
}
function createUpDownAnimation(item) {
const params = getParams(item);
const outer = split(item, "chars");
outer.forEach((char) => {
char.style.overflow = "hidden";
});
const inner = split(item.children[0], "chars");
return gsap.fromTo(
inner,
{
yPercent: (i) => {
return i % 2 === 0 ? 100 : -100;
},
},
{
...baseAnimation(item),
yPercent: 0,
stagger: {
each: 0.02,
from: "random",
},
}
);
}
/** Main Text Effect Wrapper */
addEventListener("GSAPReady", (event) => {
const texts = [...document.querySelectorAll('[data-animate="text"]')];
texts.forEach((item) => {
if (item.dataset.type === "chars") createSplitRevealAnimation(item);
else if (item.dataset.type === "words") createSplitRevealAnimation(item);
else if (item.dataset.type === "scramble") createScrambledAnimation(item);
else if (item.dataset.type === "alpha") createAlphaAnimation(item);
else if (item.dataset.type === "side") createSideAnimation(item);
else if (item.dataset.type === "updown") createUpDownAnimation(item);
});
});
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long