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:
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
Reference in New Issue
Block a user