2835 lines
59 KiB
JavaScript
2835 lines
59 KiB
JavaScript
|
|
|
|
|
|
// ===== Team Slider: True Infinite + Seamless Snap To Card Drag =====
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
if (typeof gsap === "undefined" || typeof Draggable === "undefined") return;
|
|
|
|
gsap.registerPlugin(Draggable);
|
|
|
|
const teamTrack = document.querySelector(".team-track");
|
|
if (!teamTrack || teamTrack.dataset.teamSliderInit === "true") return;
|
|
|
|
teamTrack.dataset.teamSliderInit = "true";
|
|
|
|
const originalItems = Array.from(teamTrack.children);
|
|
if (!originalItems.length) return;
|
|
|
|
originalItems.forEach((item) => {
|
|
item.dataset.teamOriginal = "true";
|
|
});
|
|
|
|
originalItems.forEach((item) => {
|
|
const clone = item.cloneNode(true);
|
|
clone.removeAttribute("data-team-original");
|
|
teamTrack.appendChild(clone);
|
|
});
|
|
|
|
const proxy = document.createElement("div");
|
|
|
|
let originalWidth = 0;
|
|
let wrapX;
|
|
let rawX = 0;
|
|
let lastX = 0;
|
|
let velocity = 0;
|
|
let autoSpeed = -0.28;
|
|
let targetSpeed = -0.28;
|
|
let isDragging = false;
|
|
let momentumTween = null;
|
|
|
|
function calculateOriginalWidth() {
|
|
const originals = teamTrack.querySelectorAll("[data-team-original='true']");
|
|
const styles = window.getComputedStyle(teamTrack);
|
|
const gap = parseFloat(styles.columnGap || styles.gap || 0);
|
|
|
|
originalWidth = 0;
|
|
|
|
originals.forEach((item, index) => {
|
|
originalWidth += item.offsetWidth;
|
|
|
|
if (index < originals.length - 1) {
|
|
originalWidth += gap;
|
|
}
|
|
});
|
|
|
|
wrapX = gsap.utils.wrap(-originalWidth, 0);
|
|
}
|
|
|
|
function setTrackX(x) {
|
|
rawX = x;
|
|
|
|
gsap.set(teamTrack, {
|
|
x: wrapX(rawX),
|
|
force3D: true
|
|
});
|
|
}
|
|
|
|
function getCardPositionsNearCurrent() {
|
|
const cards = Array.from(teamTrack.querySelectorAll(".team-card"));
|
|
const positions = [];
|
|
|
|
cards.forEach((card) => {
|
|
const cardX = -card.offsetLeft;
|
|
const loopsAround = Math.round((rawX - cardX) / originalWidth);
|
|
|
|
positions.push(cardX + loopsAround * originalWidth);
|
|
positions.push(cardX + (loopsAround - 1) * originalWidth);
|
|
positions.push(cardX + (loopsAround + 1) * originalWidth);
|
|
});
|
|
|
|
return [...new Set(positions.map((x) => Math.round(x)))]
|
|
.sort((a, b) => b - a);
|
|
}
|
|
|
|
function getClosestCardRawX() {
|
|
const positions = getCardPositionsNearCurrent();
|
|
|
|
let closestX = rawX;
|
|
let closestDistance = Infinity;
|
|
|
|
positions.forEach((candidate) => {
|
|
const distance = Math.abs(rawX - candidate);
|
|
|
|
if (distance < closestDistance) {
|
|
closestDistance = distance;
|
|
closestX = candidate;
|
|
}
|
|
});
|
|
|
|
return closestX;
|
|
}
|
|
|
|
function getNextCardRawX(direction) {
|
|
const positions = getCardPositionsNearCurrent();
|
|
const currentClosest = getClosestCardRawX();
|
|
|
|
let currentIndex = positions.findIndex((x) => {
|
|
return Math.abs(x - currentClosest) < 2;
|
|
});
|
|
|
|
if (currentIndex === -1) {
|
|
currentIndex = positions.findIndex((x) => {
|
|
return direction < 0 ? x < rawX : x > rawX;
|
|
});
|
|
}
|
|
|
|
if (direction < 0) {
|
|
return positions[currentIndex + 1] ?? currentClosest - 300;
|
|
}
|
|
|
|
return positions[currentIndex - 1] ?? currentClosest + 300;
|
|
}
|
|
|
|
calculateOriginalWidth();
|
|
|
|
gsap.set(proxy, { x: 0 });
|
|
setTrackX(0);
|
|
|
|
gsap.ticker.add(function () {
|
|
if (isDragging || !wrapX) return;
|
|
|
|
autoSpeed += (targetSpeed - autoSpeed) * 0.06;
|
|
setTrackX(rawX + autoSpeed);
|
|
gsap.set(proxy, { x: rawX });
|
|
});
|
|
|
|
Draggable.create(proxy, {
|
|
type: "x",
|
|
trigger: teamTrack,
|
|
dragResistance: 0,
|
|
minimumMovement: 1,
|
|
allowNativeTouchScrolling: true,
|
|
|
|
onPress(e) {
|
|
const pointer = e.touches ? e.touches[0] : e;
|
|
|
|
this.startPointerX = pointer.clientX || 0;
|
|
this.startPointerY = pointer.clientY || 0;
|
|
this.startRawX = rawX;
|
|
this.isHorizontalDrag = false;
|
|
|
|
if (momentumTween) momentumTween.kill();
|
|
|
|
isDragging = true;
|
|
|
|
gsap.set(proxy, { x: rawX });
|
|
|
|
lastX = rawX;
|
|
velocity = 0;
|
|
|
|
teamTrack.style.cursor = "grabbing";
|
|
},
|
|
|
|
onDrag(e) {
|
|
const pointer = e.touches ? e.touches[0] : e;
|
|
|
|
const pointerX = pointer.clientX || this.pointerX || 0;
|
|
const pointerY = pointer.clientY || this.pointerY || 0;
|
|
|
|
const diffX = Math.abs(pointerX - this.startPointerX);
|
|
const diffY = Math.abs(pointerY - this.startPointerY);
|
|
|
|
if (!this.isHorizontalDrag) {
|
|
if (diffY > diffX) return;
|
|
|
|
if (diffX > 1 && diffX > diffY) {
|
|
this.isHorizontalDrag = true;
|
|
}
|
|
}
|
|
|
|
if (!this.isHorizontalDrag) return;
|
|
|
|
const x = gsap.getProperty(proxy, "x");
|
|
|
|
velocity = x - lastX;
|
|
lastX = x;
|
|
|
|
setTrackX(x);
|
|
},
|
|
|
|
onRelease() {
|
|
teamTrack.style.cursor = "grab";
|
|
|
|
if (!this.isHorizontalDrag) {
|
|
isDragging = false;
|
|
return;
|
|
}
|
|
|
|
const dragAmount = rawX - this.startRawX;
|
|
const threshold = 5;
|
|
|
|
let targetX = getClosestCardRawX();
|
|
|
|
if (Math.abs(dragAmount) > threshold) {
|
|
targetX = getNextCardRawX(dragAmount);
|
|
}
|
|
|
|
momentumTween = gsap.to(proxy, {
|
|
x: targetX,
|
|
duration: 0.65,
|
|
ease: "power3.out",
|
|
|
|
onUpdate() {
|
|
setTrackX(gsap.getProperty(proxy, "x"));
|
|
},
|
|
|
|
onComplete() {
|
|
setTrackX(targetX);
|
|
gsap.set(proxy, { x: rawX });
|
|
isDragging = false;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
teamTrack.style.cursor = "grab";
|
|
|
|
teamTrack.addEventListener("mouseenter", function () {
|
|
targetSpeed = -0.08;
|
|
});
|
|
|
|
teamTrack.addEventListener("mouseleave", function () {
|
|
targetSpeed = -0.28;
|
|
});
|
|
|
|
window.addEventListener("resize", function () {
|
|
calculateOriginalWidth();
|
|
setTrackX(rawX);
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
// ===== Inspiration Overlay Slider: Infinite + Smooth Drag + Flip Cards =====
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
|
|
if (typeof gsap === "undefined" || typeof Draggable === "undefined") return;
|
|
|
|
gsap.registerPlugin(Draggable);
|
|
|
|
const slider = document.querySelector(".inspiration-overlay-slider");
|
|
const wrap = document.querySelector(".slider-wrapper");
|
|
|
|
if (!slider || !wrap || slider.dataset.inspirationInit === "true") return;
|
|
|
|
slider.dataset.inspirationInit = "true";
|
|
|
|
const originalCards = Array.from(slider.children);
|
|
if (!originalCards.length) return;
|
|
|
|
originalCards.forEach((card) => {
|
|
slider.appendChild(card.cloneNode(true));
|
|
});
|
|
|
|
const totalWidth = slider.scrollWidth / 2;
|
|
const wrapX = gsap.utils.wrap(-totalWidth, 0);
|
|
|
|
const proxy = document.createElement("div");
|
|
|
|
let currentX = 0;
|
|
let velocity = -0.28;
|
|
let dragVelocity = 0;
|
|
|
|
let isDragging = false;
|
|
let isHovering = false;
|
|
|
|
let lastProxyX = 0;
|
|
let momentumTween = null;
|
|
|
|
let tapStartX = 0;
|
|
let tapStartY = 0;
|
|
let tapTarget = null;
|
|
let didMove = false;
|
|
|
|
function setSliderX(x) {
|
|
currentX = wrapX(x);
|
|
|
|
gsap.set(slider, {
|
|
x: currentX
|
|
});
|
|
}
|
|
|
|
gsap.ticker.add(function () {
|
|
if (isDragging) return;
|
|
|
|
const targetSpeed = isHovering ? -0.06 : -0.28;
|
|
|
|
velocity += (targetSpeed - velocity) * 0.06;
|
|
|
|
setSliderX(currentX + velocity);
|
|
});
|
|
|
|
Draggable.create(proxy, {
|
|
type: "x",
|
|
trigger: wrap,
|
|
|
|
dragResistance: 0.08,
|
|
minimumMovement: 10,
|
|
allowNativeTouchScrolling: true,
|
|
|
|
onPress(e) {
|
|
const pointer = e.touches ? e.touches[0] : e;
|
|
|
|
this.startPointerX = pointer.clientX || 0;
|
|
this.startPointerY = pointer.clientY || 0;
|
|
this.isHorizontalDrag = false;
|
|
|
|
if (momentumTween) {
|
|
momentumTween.kill();
|
|
momentumTween = null;
|
|
}
|
|
|
|
isDragging = true;
|
|
dragVelocity = 0;
|
|
|
|
gsap.set(proxy, {
|
|
x: currentX
|
|
});
|
|
|
|
lastProxyX = currentX;
|
|
|
|
this.update();
|
|
|
|
wrap.classList.add("is-dragging");
|
|
},
|
|
|
|
onDrag(e) {
|
|
const pointer = e.touches ? e.touches[0] : e;
|
|
|
|
const pointerX = pointer.clientX || 0;
|
|
const pointerY = pointer.clientY || 0;
|
|
|
|
const diffX = Math.abs(pointerX - this.startPointerX);
|
|
const diffY = Math.abs(pointerY - this.startPointerY);
|
|
|
|
if (!this.isHorizontalDrag) {
|
|
if (diffY > diffX) return;
|
|
|
|
if (diffX > 10 && diffX > diffY) {
|
|
this.isHorizontalDrag = true;
|
|
didMove = true;
|
|
}
|
|
}
|
|
|
|
if (!this.isHorizontalDrag) return;
|
|
|
|
const x = gsap.getProperty(proxy, "x");
|
|
|
|
dragVelocity = x - lastProxyX;
|
|
lastProxyX = x;
|
|
|
|
setSliderX(x);
|
|
},
|
|
|
|
onRelease() {
|
|
wrap.classList.remove("is-dragging");
|
|
|
|
isDragging = false;
|
|
|
|
if (!this.isHorizontalDrag) return;
|
|
|
|
const startX = currentX;
|
|
const throwDistance = dragVelocity * 14;
|
|
|
|
momentumTween = gsap.to(
|
|
{ x: startX },
|
|
{
|
|
x: startX + throwDistance,
|
|
duration: 0.85,
|
|
ease: "power3.out",
|
|
|
|
onUpdate() {
|
|
setSliderX(this.targets()[0].x);
|
|
},
|
|
|
|
onComplete() {
|
|
momentumTween = null;
|
|
}
|
|
}
|
|
);
|
|
}
|
|
});
|
|
|
|
function startTap(e) {
|
|
const pointer = e.touches ? e.touches[0] : e;
|
|
const card = e.target.closest(".flip-card");
|
|
|
|
if (!card) return;
|
|
|
|
tapTarget = card;
|
|
tapStartX = pointer.clientX;
|
|
tapStartY = pointer.clientY;
|
|
didMove = false;
|
|
}
|
|
|
|
function endTap(e) {
|
|
if (!tapTarget) return;
|
|
|
|
const pointer = e.changedTouches ? e.changedTouches[0] : e;
|
|
|
|
const moveX = Math.abs(pointer.clientX - tapStartX);
|
|
const moveY = Math.abs(pointer.clientY - tapStartY);
|
|
|
|
if (moveX <= 8 && moveY <= 8 && !didMove) {
|
|
tapTarget.classList.toggle("active");
|
|
}
|
|
|
|
tapTarget = null;
|
|
}
|
|
|
|
slider.addEventListener("mousedown", startTap, true);
|
|
slider.addEventListener("mouseup", endTap, true);
|
|
|
|
slider.addEventListener("touchstart", startTap, { passive: true, capture: true });
|
|
slider.addEventListener("touchend", endTap, { passive: true, capture: true });
|
|
|
|
wrap.addEventListener("mouseenter", function () {
|
|
isHovering = true;
|
|
});
|
|
|
|
wrap.addEventListener("mouseleave", function () {
|
|
isHovering = false;
|
|
});
|
|
|
|
});
|
|
|
|
|
|
// ===== Brand Dock True Infinite Scroll + Mac Dock Effect + Drag =====
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
if (typeof gsap === "undefined") return;
|
|
|
|
const dock = document.querySelector(".brand-dock");
|
|
if (!dock || dock.dataset.brandDockInit === "true") return;
|
|
|
|
dock.dataset.brandDockInit = "true";
|
|
|
|
const originalItems = Array.from(dock.children);
|
|
if (!originalItems.length) return;
|
|
|
|
originalItems.forEach((item) => {
|
|
item.dataset.brandOriginal = "true";
|
|
});
|
|
|
|
originalItems.forEach((item) => {
|
|
const clone = item.cloneNode(true);
|
|
clone.removeAttribute("data-brand-original");
|
|
dock.appendChild(clone);
|
|
});
|
|
|
|
let originalWidth = 0;
|
|
let wrapX;
|
|
let rawX = 0;
|
|
let isDragging = false;
|
|
let startX = 0;
|
|
let dragStartX = 0;
|
|
|
|
let autoSpeed = -0.45;
|
|
let targetSpeed = -0.45;
|
|
|
|
const isMobile = window.innerWidth <= 767;
|
|
const maxScale = isMobile ? 1.1 : 1.25;
|
|
const maxDistance = isMobile ? 120 : 260;
|
|
|
|
function calculateOriginalWidth() {
|
|
const originals = dock.querySelectorAll("[data-brand-original='true']");
|
|
const styles = window.getComputedStyle(dock);
|
|
const gap = parseFloat(styles.columnGap || styles.gap || 0);
|
|
|
|
originalWidth = 0;
|
|
|
|
originals.forEach((item, index) => {
|
|
originalWidth += item.offsetWidth;
|
|
|
|
if (index < originals.length - 1) {
|
|
originalWidth += gap;
|
|
}
|
|
});
|
|
|
|
wrapX = gsap.utils.wrap(-originalWidth, 0);
|
|
}
|
|
|
|
function setDockX(x) {
|
|
if (!wrapX) return;
|
|
|
|
rawX = x;
|
|
|
|
gsap.set(dock, {
|
|
x: wrapX(rawX),
|
|
force3D: true
|
|
});
|
|
}
|
|
|
|
function getAllItems() {
|
|
return dock.querySelectorAll(".brand-dock-item");
|
|
}
|
|
|
|
calculateOriginalWidth();
|
|
setDockX(0);
|
|
|
|
gsap.ticker.add(function () {
|
|
if (isDragging || !wrapX) return;
|
|
|
|
autoSpeed += (targetSpeed - autoSpeed) * 0.06;
|
|
setDockX(rawX + autoSpeed);
|
|
});
|
|
|
|
dock.addEventListener("mousemove", function (event) {
|
|
if (isDragging || !wrapX) return;
|
|
|
|
getAllItems().forEach((item) => {
|
|
const rect = item.getBoundingClientRect();
|
|
const center = rect.left + rect.width / 2;
|
|
const distance = Math.abs(event.clientX - center);
|
|
|
|
const proximity = Math.max(0, 1 - distance / maxDistance);
|
|
const scale = 1 + proximity * (maxScale - 1);
|
|
|
|
gsap.to(item, {
|
|
scale: scale,
|
|
duration: 0.25,
|
|
ease: "power3.out",
|
|
overwrite: true
|
|
});
|
|
});
|
|
});
|
|
|
|
dock.addEventListener("mouseenter", function () {
|
|
targetSpeed = -0.12;
|
|
});
|
|
|
|
dock.addEventListener("mouseleave", function () {
|
|
targetSpeed = -0.45;
|
|
|
|
gsap.to(getAllItems(), {
|
|
scale: 1,
|
|
duration: 0.35,
|
|
ease: "power3.out",
|
|
overwrite: true
|
|
});
|
|
});
|
|
|
|
function startDrag(clientX) {
|
|
if (!wrapX) return;
|
|
|
|
isDragging = true;
|
|
startX = clientX;
|
|
dragStartX = rawX;
|
|
|
|
gsap.to(getAllItems(), {
|
|
scale: 1,
|
|
duration: 0.2,
|
|
overwrite: true
|
|
});
|
|
}
|
|
|
|
function moveDrag(clientX) {
|
|
if (!isDragging || !wrapX) return;
|
|
|
|
const delta = clientX - startX;
|
|
setDockX(dragStartX + delta);
|
|
}
|
|
|
|
function endDrag() {
|
|
isDragging = false;
|
|
}
|
|
|
|
dock.addEventListener("mousedown", function (e) {
|
|
e.preventDefault();
|
|
startDrag(e.clientX);
|
|
});
|
|
|
|
window.addEventListener("mousemove", function (e) {
|
|
moveDrag(e.clientX);
|
|
});
|
|
|
|
window.addEventListener("mouseup", endDrag);
|
|
|
|
dock.addEventListener("touchstart", function (e) {
|
|
startDrag(e.touches[0].clientX);
|
|
}, { passive: true });
|
|
|
|
dock.addEventListener("touchmove", function (e) {
|
|
moveDrag(e.touches[0].clientX);
|
|
}, { passive: true });
|
|
|
|
dock.addEventListener("touchend", endDrag);
|
|
dock.addEventListener("touchcancel", endDrag);
|
|
|
|
window.addEventListener("resize", function () {
|
|
calculateOriginalWidth();
|
|
setDockX(rawX);
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ===== Bunny Stream Video =====
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
|
|
const video = document.querySelector(".bunny-stream-video");
|
|
|
|
if (!video) return;
|
|
|
|
const streamUrl =
|
|
"TUKAJ_TVOJ_PLAYLIST.m3u8";
|
|
|
|
if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
|
|
|
video.src = streamUrl;
|
|
|
|
} else if (Hls.isSupported()) {
|
|
|
|
const hls = new Hls({
|
|
autoStartLoad: true
|
|
});
|
|
|
|
hls.loadSource(streamUrl);
|
|
hls.attachMedia(video);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
// ===== Black Hero Rotating Word Wave Animation =====
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
|
|
if (typeof gsap === "undefined") return;
|
|
|
|
const el = document.querySelector(".hero-rotating-word-black");
|
|
|
|
if (!el) return;
|
|
|
|
const words = el.dataset.words
|
|
.split(",")
|
|
.map(word => word.trim())
|
|
.filter(Boolean);
|
|
|
|
let currentWord = 0;
|
|
|
|
function buildWord(word) {
|
|
|
|
return word
|
|
.split("")
|
|
.map(char => {
|
|
|
|
const safeChar =
|
|
char === " "
|
|
? " "
|
|
: char;
|
|
|
|
return `<span class="black-char-wrap"><span class="black-char">${safeChar}</span></span>`;
|
|
|
|
})
|
|
.join("");
|
|
|
|
}
|
|
|
|
function animateWord() {
|
|
|
|
el.innerHTML = buildWord(words[currentWord]);
|
|
|
|
const chars = el.querySelectorAll(".black-char");
|
|
|
|
gsap.set(chars, {
|
|
yPercent: () => gsap.utils.random(120, 165),
|
|
xPercent: () => gsap.utils.random(-8, 8),
|
|
rotateZ: () => gsap.utils.random(-7, 7),
|
|
scaleY: () => gsap.utils.random(0.88, 1.08)
|
|
});
|
|
|
|
const tl = gsap.timeline({
|
|
|
|
onComplete() {
|
|
|
|
currentWord++;
|
|
|
|
if (currentWord >= words.length) {
|
|
currentWord = 0;
|
|
}
|
|
|
|
animateWord();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
// IN
|
|
tl.to(chars, {
|
|
yPercent: 0,
|
|
xPercent: 0,
|
|
rotateZ: 0,
|
|
scaleY: 1,
|
|
|
|
duration: () => gsap.utils.random(0.46, 0.68),
|
|
|
|
ease: "expo.out",
|
|
|
|
stagger: {
|
|
each: 0.010,
|
|
from: "start"
|
|
}
|
|
});
|
|
|
|
// HOLD
|
|
tl.to({}, {
|
|
duration: 0.85
|
|
});
|
|
|
|
// OUT
|
|
tl.to(chars, {
|
|
yPercent: () => gsap.utils.random(-120, -165),
|
|
xPercent: () => gsap.utils.random(-6, 6),
|
|
rotateZ: () => gsap.utils.random(-7, 7),
|
|
scaleY: () => gsap.utils.random(0.9, 1.08),
|
|
|
|
duration: () => gsap.utils.random(0.42, 0.62),
|
|
|
|
ease: "expo.in",
|
|
|
|
stagger: {
|
|
each: 0.010,
|
|
from: "start"
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
animateWord();
|
|
|
|
});
|
|
|
|
|
|
|
|
// ===== Pulse Services Stable Preview - DESKTOP ONLY =====
|
|
// No pin, no scrub. Preloaded videos stay alive and do not restart.
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
|
|
if (window.innerWidth <= 767) return;
|
|
|
|
const wrapper = document.querySelector(".pulse-services-section");
|
|
const serviceWords = Array.from(document.querySelectorAll(".pulse-service-word"));
|
|
const preview = document.querySelector(".pulse-preview-img");
|
|
|
|
if (!wrapper || !serviceWords.length || !preview) return;
|
|
|
|
document.body.appendChild(preview);
|
|
|
|
let activeIndex = -1;
|
|
let activeHref = null;
|
|
let ticking = false;
|
|
|
|
const preloadedVideos = {};
|
|
|
|
serviceWords.forEach((word) => {
|
|
if (word.dataset.previewType === "video" && word.dataset.previewSrc) {
|
|
const src = word.dataset.previewSrc;
|
|
|
|
const iframe = document.createElement("iframe");
|
|
iframe.src = src;
|
|
iframe.loading = "eager";
|
|
iframe.allow = "accelerometer;gyroscope;autoplay;encrypted-media;picture-in-picture;";
|
|
iframe.allowFullscreen = true;
|
|
iframe.tabIndex = -1;
|
|
iframe.dataset.videoSrc = src;
|
|
iframe.style.opacity = "0";
|
|
iframe.style.pointerEvents = "none";
|
|
|
|
preview.appendChild(iframe);
|
|
preloadedVideos[src] = iframe;
|
|
}
|
|
});
|
|
|
|
function hideVideos() {
|
|
Object.values(preloadedVideos).forEach((iframe) => {
|
|
iframe.style.opacity = "0";
|
|
});
|
|
}
|
|
|
|
function removeRenderedImages() {
|
|
preview.querySelectorAll(".pulse-rendered-image").forEach((img) => {
|
|
img.remove();
|
|
});
|
|
}
|
|
|
|
function clearActive() {
|
|
preview.classList.remove("is-visible", "is-video-preview");
|
|
|
|
hideVideos();
|
|
removeRenderedImages();
|
|
|
|
activeIndex = -1;
|
|
activeHref = null;
|
|
|
|
serviceWords.forEach((word) => {
|
|
word.classList.remove("is-active");
|
|
});
|
|
}
|
|
|
|
function renderPreview(activeItem) {
|
|
const previewType = activeItem.dataset.previewType || "image";
|
|
const previewSrc = activeItem.dataset.previewSrc || "";
|
|
const img = activeItem.querySelector("img");
|
|
const imageSrc = img ? img.getAttribute("src") : "";
|
|
|
|
hideVideos();
|
|
removeRenderedImages();
|
|
|
|
if (previewType === "video" && previewSrc && preloadedVideos[previewSrc]) {
|
|
preview.classList.add("is-video-preview");
|
|
preloadedVideos[previewSrc].style.opacity = "1";
|
|
return;
|
|
}
|
|
|
|
preview.classList.remove("is-video-preview");
|
|
|
|
if (imageSrc) {
|
|
const image = document.createElement("img");
|
|
image.className = "pulse-rendered-image";
|
|
image.src = imageSrc;
|
|
image.alt = "";
|
|
preview.appendChild(image);
|
|
}
|
|
}
|
|
|
|
function setActive(index) {
|
|
if (index === activeIndex) return;
|
|
|
|
activeIndex = index;
|
|
|
|
serviceWords.forEach((word, i) => {
|
|
word.classList.toggle("is-active", i === index);
|
|
});
|
|
|
|
const activeItem = serviceWords[index];
|
|
|
|
renderPreview(activeItem);
|
|
|
|
activeHref = activeItem.dataset.url || null;
|
|
|
|
preview.classList.add("is-visible");
|
|
}
|
|
|
|
function updateActiveWord() {
|
|
ticking = false;
|
|
|
|
const wrapperRect = wrapper.getBoundingClientRect();
|
|
|
|
if (
|
|
wrapperRect.bottom < window.innerHeight * 0.2 ||
|
|
wrapperRect.top > window.innerHeight * 0.65
|
|
) {
|
|
clearActive();
|
|
return;
|
|
}
|
|
|
|
const centerY = window.innerHeight * 0.38;
|
|
|
|
let closestIndex = 0;
|
|
let closestDistance = Infinity;
|
|
|
|
serviceWords.forEach((word, index) => {
|
|
const rect = word.getBoundingClientRect();
|
|
const wordCenter = rect.top + rect.height / 2;
|
|
const distance = Math.abs(centerY - wordCenter);
|
|
|
|
if (distance < closestDistance) {
|
|
closestDistance = distance;
|
|
closestIndex = index;
|
|
}
|
|
});
|
|
|
|
setActive(closestIndex);
|
|
}
|
|
|
|
function requestUpdate() {
|
|
if (ticking) return;
|
|
|
|
ticking = true;
|
|
requestAnimationFrame(updateActiveWord);
|
|
}
|
|
|
|
window.addEventListener("scroll", requestUpdate, { passive: true });
|
|
window.addEventListener("resize", requestUpdate);
|
|
|
|
document.addEventListener("visibilitychange", function () {
|
|
if (document.hidden) {
|
|
clearActive();
|
|
} else {
|
|
requestUpdate();
|
|
}
|
|
});
|
|
|
|
window.addEventListener("blur", clearActive);
|
|
|
|
preview.addEventListener("click", function () {
|
|
if (activeHref) {
|
|
window.location.href = activeHref;
|
|
}
|
|
});
|
|
|
|
serviceWords.forEach((word) => {
|
|
word.addEventListener("click", function () {
|
|
const url = word.dataset.url;
|
|
|
|
if (url) {
|
|
window.location.href = url;
|
|
}
|
|
});
|
|
});
|
|
|
|
requestUpdate();
|
|
|
|
});
|
|
|
|
|
|
// ===== Mobile Services Slow Story Scroll =====
|
|
// Supports image + preloaded Bunny video previews.
|
|
// Video starts once on page load and NEVER resets.
|
|
// We only fade it in/out with opacity.
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
|
|
if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") return;
|
|
if (window.innerWidth > 767) return;
|
|
|
|
const wrapper = document.querySelector(".services-story-mobile-wrap");
|
|
const section = document.querySelector(".services-word-cloud");
|
|
const serviceWords = gsap.utils.toArray(".service-word");
|
|
const preview = document.querySelector(".mobile-service-preview");
|
|
|
|
if (!wrapper || !section || !serviceWords.length || !preview) return;
|
|
|
|
document.body.appendChild(preview);
|
|
|
|
const scrollDistance = serviceWords.length * 400;
|
|
|
|
let activePreviewKey = null;
|
|
|
|
const preloadedVideos = {};
|
|
|
|
// ===== PRELOAD + AUTOPLAY VIDEO ON PAGE LOAD =====
|
|
|
|
serviceWords.forEach((word) => {
|
|
|
|
if (
|
|
word.dataset.previewType === "video" &&
|
|
word.dataset.previewSrc
|
|
) {
|
|
|
|
const src = word.dataset.previewSrc;
|
|
|
|
const iframe = document.createElement("iframe");
|
|
|
|
iframe.src = src;
|
|
iframe.loading = "eager";
|
|
|
|
iframe.allow =
|
|
"accelerometer;gyroscope;autoplay;encrypted-media;picture-in-picture;";
|
|
|
|
iframe.allowFullscreen = true;
|
|
|
|
iframe.setAttribute("tabindex", "-1");
|
|
iframe.dataset.videoSrc = src;
|
|
|
|
iframe.style.position = "absolute";
|
|
iframe.style.top = "50%";
|
|
iframe.style.left = "50%";
|
|
|
|
iframe.style.width = "185%";
|
|
iframe.style.height = "185%";
|
|
|
|
iframe.style.minWidth = "100%";
|
|
iframe.style.minHeight = "100%";
|
|
|
|
iframe.style.transform =
|
|
"translate(-50%, -50%)";
|
|
|
|
iframe.style.border = "0";
|
|
|
|
iframe.style.opacity = "0";
|
|
iframe.style.pointerEvents = "none";
|
|
|
|
preview.appendChild(iframe);
|
|
|
|
preloadedVideos[src] = iframe;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
function isWrapperVisible() {
|
|
|
|
const rect =
|
|
wrapper.getBoundingClientRect();
|
|
|
|
return (
|
|
rect.bottom > 0 &&
|
|
rect.top < window.innerHeight
|
|
);
|
|
|
|
}
|
|
|
|
function hideVideos() {
|
|
|
|
Object.values(preloadedVideos)
|
|
.forEach((iframe) => {
|
|
|
|
iframe.style.opacity = "0";
|
|
|
|
});
|
|
|
|
}
|
|
|
|
function removeImages() {
|
|
|
|
preview
|
|
.querySelectorAll(
|
|
".mobile-preview-rendered-img"
|
|
)
|
|
.forEach((img) => {
|
|
|
|
img.remove();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
function renderPreview(activeItem) {
|
|
|
|
const previewType =
|
|
activeItem.dataset.previewType || "image";
|
|
|
|
const previewSrc =
|
|
activeItem.dataset.previewSrc || "";
|
|
|
|
const img =
|
|
activeItem.querySelector("img");
|
|
|
|
const imageSrc =
|
|
img
|
|
? img.getAttribute("src")
|
|
: "";
|
|
|
|
const previewKey =
|
|
`${previewType}:${previewSrc}:${imageSrc}`;
|
|
|
|
if (
|
|
activePreviewKey === previewKey
|
|
) return;
|
|
|
|
activePreviewKey =
|
|
previewKey;
|
|
|
|
removeImages();
|
|
|
|
hideVideos();
|
|
|
|
if (
|
|
previewType === "video" &&
|
|
previewSrc &&
|
|
preloadedVideos[previewSrc]
|
|
) {
|
|
|
|
preloadedVideos[
|
|
previewSrc
|
|
].style.opacity = "1";
|
|
|
|
preview.classList.add(
|
|
"is-video-preview"
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
preview.classList.remove(
|
|
"is-video-preview"
|
|
);
|
|
|
|
if (imageSrc) {
|
|
|
|
const image =
|
|
document.createElement(
|
|
"img"
|
|
);
|
|
|
|
image.className =
|
|
"mobile-preview-rendered-img";
|
|
|
|
image.src =
|
|
imageSrc;
|
|
|
|
image.alt = "";
|
|
|
|
preview.appendChild(
|
|
image
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function clearActive() {
|
|
|
|
preview.classList.remove(
|
|
"is-visible",
|
|
"is-video-preview"
|
|
);
|
|
|
|
activePreviewKey =
|
|
null;
|
|
|
|
removeImages();
|
|
|
|
// IMPORTANT:
|
|
// videos stay alive
|
|
// only opacity changes
|
|
|
|
hideVideos();
|
|
|
|
serviceWords
|
|
.forEach((word) => {
|
|
|
|
word.classList.remove(
|
|
"is-active"
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
function setActive(index) {
|
|
|
|
if (
|
|
!isWrapperVisible()
|
|
) return;
|
|
|
|
serviceWords
|
|
.forEach(
|
|
(word, i) => {
|
|
|
|
word.classList.toggle(
|
|
"is-active",
|
|
i === index
|
|
);
|
|
|
|
}
|
|
);
|
|
|
|
renderPreview(
|
|
serviceWords[index]
|
|
);
|
|
|
|
preview.classList.add(
|
|
"is-visible"
|
|
);
|
|
|
|
}
|
|
|
|
const trigger =
|
|
ScrollTrigger.create({
|
|
|
|
trigger:
|
|
wrapper,
|
|
|
|
start:
|
|
"top 20%",
|
|
|
|
end:
|
|
"+=" +
|
|
scrollDistance,
|
|
|
|
pin:
|
|
section,
|
|
|
|
pinSpacing:
|
|
true,
|
|
|
|
scrub:
|
|
true,
|
|
|
|
anticipatePin:
|
|
1,
|
|
|
|
invalidateOnRefresh:
|
|
true,
|
|
|
|
onEnter:
|
|
() => {
|
|
|
|
if (
|
|
isWrapperVisible()
|
|
) {
|
|
|
|
setActive(0);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
onEnterBack:
|
|
() => {
|
|
|
|
if (
|
|
isWrapperVisible()
|
|
) {
|
|
|
|
setActive(0);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
onUpdate:
|
|
(self) => {
|
|
|
|
if (
|
|
!isWrapperVisible()
|
|
) {
|
|
|
|
clearActive();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const index =
|
|
Math.min(
|
|
|
|
serviceWords.length - 1,
|
|
|
|
Math.floor(
|
|
self.progress *
|
|
serviceWords.length
|
|
)
|
|
|
|
);
|
|
|
|
setActive(
|
|
index
|
|
);
|
|
|
|
},
|
|
|
|
onRefresh:
|
|
() => {
|
|
|
|
if (
|
|
!isWrapperVisible()
|
|
) {
|
|
|
|
clearActive();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
onLeave:
|
|
clearActive,
|
|
|
|
onLeaveBack:
|
|
clearActive
|
|
|
|
});
|
|
|
|
requestAnimationFrame(
|
|
() => {
|
|
|
|
if (
|
|
|
|
!isWrapperVisible() ||
|
|
|
|
!trigger.isActive
|
|
|
|
) {
|
|
|
|
clearActive();
|
|
|
|
}
|
|
|
|
}
|
|
);
|
|
|
|
setTimeout(
|
|
() => {
|
|
|
|
ScrollTrigger.refresh(
|
|
true
|
|
);
|
|
|
|
if (
|
|
|
|
!isWrapperVisible() ||
|
|
|
|
!trigger.isActive
|
|
|
|
) {
|
|
|
|
clearActive();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
400
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ===== Pink Hero Rotating Word Wave Animation =====
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
|
|
if (typeof gsap === "undefined") return;
|
|
|
|
const el = document.querySelector(".hero-rotating-word-pink");
|
|
|
|
if (!el) return;
|
|
|
|
const words = el.dataset.words
|
|
.split(",")
|
|
.map(word => word.trim())
|
|
.filter(Boolean);
|
|
|
|
let currentWord = 0;
|
|
|
|
|
|
function buildWord(word) {
|
|
|
|
return word
|
|
.split("")
|
|
.map(char => {
|
|
|
|
const safeChar =
|
|
char === " "
|
|
? " "
|
|
: char;
|
|
|
|
return `<span class="pink-char-wrap"><span class="pink-char">${safeChar}</span></span>`;
|
|
|
|
})
|
|
.join("");
|
|
|
|
}
|
|
|
|
|
|
function animateWord() {
|
|
|
|
el.innerHTML = buildWord(words[currentWord]);
|
|
|
|
const chars = el.querySelectorAll(".pink-char");
|
|
|
|
|
|
gsap.set(chars, {
|
|
yPercent: () => gsap.utils.random(120, 165),
|
|
xPercent: () => gsap.utils.random(-8, 8),
|
|
rotateZ: () => gsap.utils.random(-7, 7),
|
|
scaleY: () => gsap.utils.random(0.88, 1.08)
|
|
});
|
|
|
|
|
|
const tl = gsap.timeline({
|
|
|
|
onComplete() {
|
|
|
|
currentWord++;
|
|
|
|
if (currentWord >= words.length) {
|
|
currentWord = 0;
|
|
}
|
|
|
|
animateWord();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
tl.to(chars, {
|
|
yPercent: 0,
|
|
xPercent: 0,
|
|
rotateZ: 0,
|
|
scaleY: 1,
|
|
|
|
duration: () => gsap.utils.random(0.46, 0.68),
|
|
|
|
ease: "expo.out",
|
|
|
|
stagger: {
|
|
each: 0.010,
|
|
from: "start"
|
|
}
|
|
});
|
|
|
|
|
|
tl.to({}, {
|
|
duration: 0.85
|
|
});
|
|
|
|
|
|
tl.to(chars, {
|
|
yPercent: () => gsap.utils.random(-120, -165),
|
|
xPercent: () => gsap.utils.random(-6, 6),
|
|
rotateZ: () => gsap.utils.random(-7, 7),
|
|
scaleY: () => gsap.utils.random(0.9, 1.08),
|
|
|
|
duration: () => gsap.utils.random(0.42, 0.62),
|
|
|
|
ease: "expo.in",
|
|
|
|
stagger: {
|
|
each: 0.010,
|
|
from: "start"
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
|
|
animateWord();
|
|
|
|
});
|
|
|
|
|
|
|
|
// ===== Creative Area Rotating Word Wave Animation =====
|
|
// Static text + rotating animated phrase next to it.
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
|
|
if (typeof gsap === "undefined") return;
|
|
|
|
const el = document.querySelector(".ct-rotating-word");
|
|
if (!el) return;
|
|
|
|
const words = el.dataset.words
|
|
.split("|")
|
|
.map(word => word.trim())
|
|
.filter(Boolean);
|
|
|
|
let currentWord = 0;
|
|
|
|
function buildWord(word) {
|
|
return word
|
|
.split("")
|
|
.map(char => {
|
|
const safeChar = char === " " ? " " : char;
|
|
return `<span class="ct-char-wrap"><span class="ct-char">${safeChar}</span></span>`;
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
function animateWord() {
|
|
const word = words[currentWord];
|
|
|
|
el.innerHTML = buildWord(word);
|
|
|
|
const chars = el.querySelectorAll(".ct-char");
|
|
|
|
gsap.set(chars, {
|
|
yPercent: () => gsap.utils.random(120, 165),
|
|
xPercent: () => gsap.utils.random(-8, 8),
|
|
rotateZ: () => gsap.utils.random(-7, 7),
|
|
scaleY: () => gsap.utils.random(0.88, 1.08)
|
|
});
|
|
|
|
const tl = gsap.timeline({
|
|
onComplete() {
|
|
currentWord = (currentWord + 1) % words.length;
|
|
animateWord();
|
|
}
|
|
});
|
|
|
|
tl.to(chars, {
|
|
yPercent: 0,
|
|
xPercent: 0,
|
|
rotateZ: 0,
|
|
scaleY: 1,
|
|
duration: () => gsap.utils.random(0.46, 0.68),
|
|
ease: "expo.out",
|
|
stagger: {
|
|
each: 0.010,
|
|
from: "start"
|
|
}
|
|
});
|
|
|
|
tl.to({}, {
|
|
duration: 0.85
|
|
});
|
|
|
|
tl.to(chars, {
|
|
yPercent: () => gsap.utils.random(-120, -165),
|
|
xPercent: () => gsap.utils.random(-6, 6),
|
|
rotateZ: () => gsap.utils.random(-7, 7),
|
|
scaleY: () => gsap.utils.random(0.9, 1.08),
|
|
duration: () => gsap.utils.random(0.42, 0.62),
|
|
ease: "expo.in",
|
|
stagger: {
|
|
each: 0.010,
|
|
from: "start"
|
|
}
|
|
});
|
|
}
|
|
|
|
animateWord();
|
|
|
|
});
|
|
|
|
|
|
// ===== Hero Rotating Word Wave Animation =====
|
|
// Rotating words with per-letter wave animation.
|
|
// More flowy character movement, slower reveal, clear left-to-right wave.
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
|
|
if (typeof gsap === "undefined") return;
|
|
|
|
const el = document.querySelector(".hero-rotating-word");
|
|
if (!el) return;
|
|
|
|
const words = el.dataset.words
|
|
.split(",")
|
|
.map(word => word.trim())
|
|
.filter(Boolean);
|
|
|
|
let currentWord = 0;
|
|
|
|
function buildWord(word) {
|
|
return word
|
|
.split("")
|
|
.map(char => {
|
|
const safeChar = char === " " ? " " : char;
|
|
return `<span class="hero-char-wrap"><span class="hero-char">${safeChar}</span></span>`;
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
function animateWord() {
|
|
const word = words[currentWord];
|
|
|
|
el.innerHTML = buildWord(word);
|
|
|
|
const chars = el.querySelectorAll(".hero-char");
|
|
|
|
gsap.set(chars, {
|
|
yPercent: () => gsap.utils.random(120, 165),
|
|
xPercent: () => gsap.utils.random(-8, 8),
|
|
rotateZ: () => gsap.utils.random(-7, 7),
|
|
scaleY: () => gsap.utils.random(0.88, 1.08)
|
|
});
|
|
|
|
const tl = gsap.timeline({
|
|
onComplete() {
|
|
currentWord = (currentWord + 1) % words.length;
|
|
animateWord();
|
|
}
|
|
});
|
|
|
|
// IN animation
|
|
tl.to(chars, {
|
|
yPercent: 0,
|
|
xPercent: 0,
|
|
rotateZ: 0,
|
|
scaleY: 1,
|
|
|
|
duration: () => gsap.utils.random(0.46, 0.68),
|
|
ease: "expo.out",
|
|
|
|
stagger: {
|
|
each: 0.010,
|
|
from: "start"
|
|
}
|
|
});
|
|
|
|
// Hold
|
|
tl.to({}, {
|
|
duration: 0.85
|
|
});
|
|
|
|
// OUT animation
|
|
tl.to(chars, {
|
|
yPercent: () => gsap.utils.random(-120, -165),
|
|
xPercent: () => gsap.utils.random(-6, 6),
|
|
rotateZ: () => gsap.utils.random(-7, 7),
|
|
scaleY: () => gsap.utils.random(0.9, 1.08),
|
|
|
|
duration: () => gsap.utils.random(0.42, 0.62),
|
|
ease: "expo.in",
|
|
|
|
stagger: {
|
|
each: 0.010,
|
|
from: "start"
|
|
}
|
|
});
|
|
}
|
|
|
|
animateWord();
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ===== Portfolio Slider: Infinite + Seamless Snap To Card Drag =====
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
if (typeof gsap === "undefined" || typeof Draggable === "undefined") return;
|
|
|
|
const slider = document.querySelector(".portfolio-slider");
|
|
const wrap = document.querySelector(".portfolio-slider-wrap");
|
|
|
|
if (!slider || !wrap || slider.dataset.snapSliderInit === "true") return;
|
|
slider.dataset.snapSliderInit = "true";
|
|
|
|
const originalSlides = Array.from(slider.children);
|
|
if (!originalSlides.length) return;
|
|
|
|
originalSlides.forEach((slide) => {
|
|
slider.appendChild(slide.cloneNode(true));
|
|
});
|
|
|
|
const totalWidth = slider.scrollWidth / 2;
|
|
const wrapX = gsap.utils.wrap(-totalWidth, 0);
|
|
|
|
const proxy = document.createElement("div");
|
|
|
|
let rawX = 0;
|
|
let lastX = 0;
|
|
let velocity = 0;
|
|
let momentumTween = null;
|
|
let autoTween = null;
|
|
|
|
function setSliderX(x) {
|
|
rawX = x;
|
|
|
|
gsap.set(slider, {
|
|
x: wrapX(rawX),
|
|
force3D: true
|
|
});
|
|
}
|
|
|
|
function startAutoLoop() {
|
|
if (autoTween) autoTween.kill();
|
|
|
|
const pixelsPerSecond = 20;
|
|
const duration = totalWidth / pixelsPerSecond;
|
|
|
|
autoTween = gsap.to(proxy, {
|
|
x: rawX - totalWidth,
|
|
duration: duration,
|
|
ease: "none",
|
|
repeat: -1,
|
|
onUpdate() {
|
|
setSliderX(gsap.getProperty(proxy, "x"));
|
|
}
|
|
});
|
|
}
|
|
|
|
function pauseAutoLoop() {
|
|
if (autoTween) autoTween.pause();
|
|
}
|
|
|
|
function resumeAutoLoop() {
|
|
if (!autoTween) {
|
|
startAutoLoop();
|
|
return;
|
|
}
|
|
|
|
autoTween.play();
|
|
|
|
gsap.to(autoTween, {
|
|
timeScale: 1,
|
|
duration: 1.2,
|
|
ease: "power3.out"
|
|
});
|
|
}
|
|
|
|
function getSlidePositionsNearCurrent() {
|
|
const slides = Array.from(slider.querySelectorAll(".portfolio-slide"));
|
|
const positions = [];
|
|
|
|
slides.forEach((slide) => {
|
|
const slideX = -slide.offsetLeft;
|
|
const loopsAround = Math.round((rawX - slideX) / totalWidth);
|
|
|
|
positions.push(slideX + loopsAround * totalWidth);
|
|
positions.push(slideX + (loopsAround - 1) * totalWidth);
|
|
positions.push(slideX + (loopsAround + 1) * totalWidth);
|
|
});
|
|
|
|
return [...new Set(positions.map((x) => Math.round(x)))]
|
|
.sort((a, b) => b - a);
|
|
}
|
|
|
|
function getClosestSlideRawX() {
|
|
const positions = getSlidePositionsNearCurrent();
|
|
|
|
let closestX = rawX;
|
|
let closestDistance = Infinity;
|
|
|
|
positions.forEach((candidate) => {
|
|
const distance = Math.abs(rawX - candidate);
|
|
|
|
if (distance < closestDistance) {
|
|
closestDistance = distance;
|
|
closestX = candidate;
|
|
}
|
|
});
|
|
|
|
return closestX;
|
|
}
|
|
|
|
function getNextSlideRawX(direction) {
|
|
const positions = getSlidePositionsNearCurrent();
|
|
const currentClosest = getClosestSlideRawX();
|
|
|
|
let currentIndex = positions.findIndex((x) => {
|
|
return Math.abs(x - currentClosest) < 2;
|
|
});
|
|
|
|
if (currentIndex === -1) {
|
|
currentIndex = positions.findIndex((x) => {
|
|
return direction < 0 ? x < rawX : x > rawX;
|
|
});
|
|
}
|
|
|
|
if (direction < 0) {
|
|
return positions[currentIndex + 1] ?? currentClosest - 300;
|
|
}
|
|
|
|
return positions[currentIndex - 1] ?? currentClosest + 300;
|
|
}
|
|
|
|
gsap.set(proxy, { x: 0 });
|
|
setSliderX(0);
|
|
startAutoLoop();
|
|
|
|
Draggable.create(proxy, {
|
|
type: "x",
|
|
trigger: wrap,
|
|
dragResistance: 0,
|
|
minimumMovement: 1,
|
|
allowNativeTouchScrolling: true,
|
|
|
|
onPress(e) {
|
|
this.startPointerX = e.clientX || e.touches?.[0]?.clientX || 0;
|
|
this.startPointerY = e.clientY || e.touches?.[0]?.clientY || 0;
|
|
this.startRawX = rawX;
|
|
this.isHorizontalDrag = false;
|
|
|
|
if (momentumTween) momentumTween.kill();
|
|
|
|
pauseAutoLoop();
|
|
|
|
gsap.set(proxy, { x: rawX });
|
|
|
|
lastX = rawX;
|
|
velocity = 0;
|
|
},
|
|
|
|
onDrag(e) {
|
|
const pointerX = e.clientX || e.touches?.[0]?.clientX || this.pointerX || 0;
|
|
const pointerY = e.clientY || e.touches?.[0]?.clientY || this.pointerY || 0;
|
|
|
|
const diffX = Math.abs(pointerX - this.startPointerX);
|
|
const diffY = Math.abs(pointerY - this.startPointerY);
|
|
|
|
if (!this.isHorizontalDrag) {
|
|
if (diffY > diffX) return;
|
|
|
|
if (diffX > 1 && diffX > diffY) {
|
|
this.isHorizontalDrag = true;
|
|
wrap.classList.add("is-dragging");
|
|
}
|
|
}
|
|
|
|
if (!this.isHorizontalDrag) return;
|
|
|
|
const x = gsap.getProperty(proxy, "x");
|
|
|
|
velocity = x - lastX;
|
|
lastX = x;
|
|
|
|
setSliderX(x);
|
|
},
|
|
|
|
onRelease() {
|
|
wrap.classList.remove("is-dragging");
|
|
|
|
if (!this.isHorizontalDrag) {
|
|
resumeAutoLoop();
|
|
return;
|
|
}
|
|
|
|
const dragAmount = rawX - this.startRawX;
|
|
const threshold = 5;
|
|
|
|
let targetX = getClosestSlideRawX();
|
|
|
|
if (Math.abs(dragAmount) > threshold) {
|
|
targetX = getNextSlideRawX(dragAmount);
|
|
}
|
|
|
|
momentumTween = gsap.to(proxy, {
|
|
x: targetX,
|
|
duration: 0.65,
|
|
ease: "power3.out",
|
|
|
|
onUpdate() {
|
|
setSliderX(gsap.getProperty(proxy, "x"));
|
|
},
|
|
|
|
onComplete() {
|
|
setSliderX(targetX);
|
|
gsap.set(proxy, { x: rawX });
|
|
resumeAutoLoop();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
wrap.addEventListener("mouseenter", () => {
|
|
if (!autoTween) return;
|
|
|
|
gsap.to(autoTween, {
|
|
timeScale: 0.25,
|
|
duration: 0.45,
|
|
ease: "power3.out"
|
|
});
|
|
});
|
|
|
|
wrap.addEventListener("mouseleave", () => {
|
|
if (!autoTween) return;
|
|
|
|
gsap.to(autoTween, {
|
|
timeScale: 1,
|
|
duration: 0.65,
|
|
ease: "power3.out"
|
|
});
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ===== Hero Video Scale On Scroll =====
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
|
|
if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") return;
|
|
|
|
gsap.fromTo(".hero-video",
|
|
{
|
|
width: "70%",
|
|
height: "75vh"
|
|
},
|
|
{
|
|
width: "100%",
|
|
height: "100vh",
|
|
ease: "none",
|
|
|
|
scrollTrigger: {
|
|
trigger: ".hero-area",
|
|
|
|
start: "top 130%",
|
|
end: "top top",
|
|
|
|
scrub: 1
|
|
}
|
|
}
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ===== Awards Cursor Image Trail =====
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
|
|
if (typeof gsap === "undefined") return;
|
|
|
|
const section = document.querySelector(".about-area2");
|
|
if (!section) return;
|
|
|
|
const flair = gsap.utils.toArray(".about-area2 .flair");
|
|
if (!flair.length) return;
|
|
|
|
let gap = 100;
|
|
let index = 0;
|
|
let wrapper = gsap.utils.wrap(0, flair.length);
|
|
|
|
let mousePos = { x: 0, y: 0 };
|
|
let lastMousePos = { x: 0, y: 0 };
|
|
|
|
let isInside = false;
|
|
let idleTimeout;
|
|
|
|
function playAnimation(shape) {
|
|
|
|
gsap.timeline()
|
|
|
|
.fromTo(
|
|
shape,
|
|
{
|
|
opacity: 1,
|
|
scale: 0
|
|
},
|
|
{
|
|
opacity: 1,
|
|
scale: 1,
|
|
ease: "elastic.out(1,0.3)",
|
|
duration: 0.45
|
|
}
|
|
)
|
|
|
|
.to(shape, {
|
|
rotation: "random([-360,360])"
|
|
}, "<")
|
|
|
|
.to(shape, {
|
|
y: section.offsetHeight + 200,
|
|
opacity: 0,
|
|
ease: "power2.in",
|
|
duration: 2.1
|
|
}, 0.45);
|
|
}
|
|
|
|
function hideAllFlair() {
|
|
|
|
gsap.to(flair, {
|
|
opacity: 0,
|
|
duration: 0.25,
|
|
overwrite: true
|
|
});
|
|
|
|
}
|
|
|
|
section.addEventListener("mouseenter", function (e) {
|
|
|
|
isInside = true;
|
|
|
|
const rect = section.getBoundingClientRect();
|
|
|
|
mousePos = {
|
|
x: e.clientX - rect.left,
|
|
y: e.clientY - rect.top
|
|
};
|
|
|
|
lastMousePos = { ...mousePos };
|
|
|
|
});
|
|
|
|
section.addEventListener("mouseleave", function () {
|
|
|
|
isInside = false;
|
|
|
|
clearTimeout(idleTimeout);
|
|
|
|
hideAllFlair();
|
|
|
|
});
|
|
|
|
section.addEventListener("mousemove", function (e) {
|
|
|
|
clearTimeout(idleTimeout);
|
|
|
|
const rect = section.getBoundingClientRect();
|
|
|
|
mousePos = {
|
|
x: e.clientX - rect.left,
|
|
y: e.clientY - rect.top
|
|
};
|
|
|
|
idleTimeout = setTimeout(() => {
|
|
hideAllFlair();
|
|
}, 3050);
|
|
|
|
});
|
|
|
|
gsap.ticker.add(function () {
|
|
|
|
if (!isInside) return;
|
|
|
|
let travelDistance = Math.hypot(
|
|
lastMousePos.x - mousePos.x,
|
|
lastMousePos.y - mousePos.y
|
|
);
|
|
|
|
if (travelDistance > gap) {
|
|
|
|
let wrappedIndex = wrapper(index);
|
|
let img = flair[wrappedIndex];
|
|
|
|
gsap.killTweensOf(img);
|
|
|
|
gsap.set(img, {
|
|
clearProps: "all"
|
|
});
|
|
|
|
gsap.set(img, {
|
|
opacity: 1,
|
|
left: mousePos.x,
|
|
top: mousePos.y,
|
|
xPercent: -50,
|
|
yPercent: -50
|
|
});
|
|
|
|
playAnimation(img);
|
|
|
|
lastMousePos = { ...mousePos };
|
|
|
|
index++;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// ===== Global Smooth Scroll / Lenis - iframe safe =====
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
|
|
if (typeof Lenis === "undefined") return;
|
|
|
|
const lenis = new Lenis({
|
|
duration: 1.2,
|
|
|
|
smoothWheel: true,
|
|
smoothTouch: true,
|
|
|
|
touchMultiplier: 1.15,
|
|
wheelMultiplier: 1,
|
|
|
|
lerp: 0.08,
|
|
|
|
prevent: function (node) {
|
|
return node.closest && node.closest("iframe");
|
|
}
|
|
});
|
|
|
|
function raf(time) {
|
|
lenis.raf(time);
|
|
requestAnimationFrame(raf);
|
|
}
|
|
|
|
requestAnimationFrame(raf);
|
|
|
|
if (typeof ScrollTrigger !== "undefined") {
|
|
lenis.on("scroll", ScrollTrigger.update);
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
if (typeof gsap === "undefined") return;
|
|
|
|
const awardContainer = document.querySelector(".award-container");
|
|
const awardsWrap = document.querySelector(".awards-wrap");
|
|
const awards = document.querySelectorAll(".awards-wrap img");
|
|
|
|
if (!awardContainer || !awardsWrap || !awards.length) return;
|
|
|
|
gsap.set(awardContainer, {
|
|
perspective: 1400
|
|
});
|
|
|
|
gsap.set(awardsWrap, {
|
|
transformStyle: "preserve-3d"
|
|
});
|
|
|
|
gsap.set(awards, {
|
|
transformStyle: "preserve-3d"
|
|
});
|
|
|
|
const wrapRotateX = gsap.quickTo(awardsWrap, "rotationX", {
|
|
duration: 0.45,
|
|
ease: "power2.out"
|
|
});
|
|
|
|
const wrapRotateY = gsap.quickTo(awardsWrap, "rotationY", {
|
|
duration: 0.45,
|
|
ease: "power2.out"
|
|
});
|
|
|
|
const wrapX = gsap.quickTo(awardsWrap, "x", {
|
|
duration: 0.45,
|
|
ease: "power2.out"
|
|
});
|
|
|
|
const wrapY = gsap.quickTo(awardsWrap, "y", {
|
|
duration: 0.45,
|
|
ease: "power2.out"
|
|
});
|
|
|
|
awardContainer.addEventListener("pointermove", function (e) {
|
|
const rect = awardContainer.getBoundingClientRect();
|
|
|
|
const x = (e.clientX - rect.left) / rect.width;
|
|
const y = (e.clientY - rect.top) / rect.height;
|
|
|
|
const rotateY = gsap.utils.interpolate(-16, 16, x);
|
|
const rotateX = gsap.utils.interpolate(12, -12, y);
|
|
const moveX = gsap.utils.interpolate(-28, 28, x);
|
|
const moveY = gsap.utils.interpolate(-22, 22, y);
|
|
|
|
wrapRotateX(rotateX);
|
|
wrapRotateY(rotateY);
|
|
wrapX(moveX);
|
|
wrapY(moveY);
|
|
|
|
awards.forEach((award, i) => {
|
|
const baseRotate = i % 2 === 0 ? 15 : -15;
|
|
const strengthX = 8 + i * 2.5;
|
|
const strengthY = 6 + i * 2;
|
|
const depth = 20 + i * 6;
|
|
const extraRotate = gsap.utils.interpolate(-4, 4, x);
|
|
|
|
const driftX = gsap.utils.interpolate(-strengthX, strengthX, x);
|
|
const driftY = gsap.utils.interpolate(-strengthY, strengthY, y);
|
|
|
|
gsap.to(award, {
|
|
x: driftX,
|
|
y: driftY,
|
|
z: depth,
|
|
rotate: baseRotate + extraRotate,
|
|
duration: 0.45,
|
|
ease: "power2.out",
|
|
overwrite: true
|
|
});
|
|
});
|
|
});
|
|
|
|
awardContainer.addEventListener("pointerleave", function () {
|
|
wrapRotateX(0);
|
|
wrapRotateY(0);
|
|
wrapX(0);
|
|
wrapY(0);
|
|
|
|
awards.forEach((award, i) => {
|
|
const baseRotate = i % 2 === 0 ? 15 : -15;
|
|
|
|
gsap.to(award, {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0,
|
|
rotate: baseRotate,
|
|
duration: 0.7,
|
|
ease: "power3.out",
|
|
overwrite: true
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") return;
|
|
|
|
gsap.registerPlugin(ScrollTrigger);
|
|
|
|
const awards = document.querySelectorAll(".awards-wrap img");
|
|
if (!awards.length) return;
|
|
|
|
gsap.set(awards, {
|
|
y: -180,
|
|
z: -120,
|
|
opacity: 0,
|
|
rotate: (i) => (i % 2 === 0 ? 18 : -18),
|
|
filter: "blur(8px)",
|
|
force3D: true
|
|
});
|
|
|
|
gsap.to(awards, {
|
|
y: 0,
|
|
z: 0,
|
|
opacity: 1,
|
|
rotate: (i) => (i % 2 === 0 ? 15 : -15),
|
|
filter: "blur(0px)",
|
|
duration: 1.15,
|
|
ease: "power3.out",
|
|
stagger: 0.1,
|
|
force3D: true,
|
|
scrollTrigger: {
|
|
trigger: ".award-container",
|
|
start: "top 82%",
|
|
once: true
|
|
}
|
|
});
|
|
});
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const showcaseSlider = document.querySelector(".showcase-slider");
|
|
const sliderWrap = document.querySelector(".slider-wrap");
|
|
const showcaseItems = document.querySelectorAll(".slider-wrap .showcase-item");
|
|
|
|
if (!showcaseSlider || !sliderWrap || !showcaseItems.length) return;
|
|
|
|
if (window.innerWidth <= 991) return;
|
|
|
|
gsap.set(showcaseSlider, {
|
|
perspective: 1200
|
|
});
|
|
|
|
gsap.set(sliderWrap, {
|
|
transformStyle: "preserve-3d",
|
|
transformPerspective: 1200
|
|
});
|
|
|
|
gsap.set(showcaseItems, {
|
|
transformStyle: "preserve-3d"
|
|
});
|
|
|
|
const wrapRotateX = gsap.quickTo(sliderWrap, "rotationX", {
|
|
duration: 0.8,
|
|
ease: "power3.out"
|
|
});
|
|
|
|
const wrapRotateY = gsap.quickTo(sliderWrap, "rotationY", {
|
|
duration: 0.8,
|
|
ease: "power3.out"
|
|
});
|
|
|
|
const wrapY = gsap.quickTo(sliderWrap, "y", {
|
|
duration: 0.8,
|
|
ease: "power3.out"
|
|
});
|
|
|
|
function handleMove(e) {
|
|
const rect = showcaseSlider.getBoundingClientRect();
|
|
|
|
const x = (e.clientX - rect.left) / rect.width;
|
|
const y = (e.clientY - rect.top) / rect.height;
|
|
|
|
const rotateY = gsap.utils.interpolate(-8, 8, x);
|
|
const rotateX = gsap.utils.interpolate(6, -6, y);
|
|
const moveWrapY = gsap.utils.interpolate(8, -8, y);
|
|
|
|
wrapRotateX(rotateX);
|
|
wrapRotateY(rotateY);
|
|
wrapY(moveWrapY);
|
|
|
|
showcaseItems.forEach((item) => {
|
|
const img = item.querySelector("img");
|
|
if (!img) return;
|
|
|
|
const isCenter = item.classList.contains("center");
|
|
const strength = isCenter ? 22 : 10;
|
|
const zDepth = isCenter ? 35 : 0;
|
|
const scale = isCenter ? 1.03 : 1;
|
|
|
|
const moveX = gsap.utils.interpolate(-strength, strength, x);
|
|
const moveY = gsap.utils.interpolate(-strength, strength, y);
|
|
const imgRotate = isCenter
|
|
? gsap.utils.interpolate(-1.5, 1.5, x)
|
|
: gsap.utils.interpolate(-0.6, 0.6, x);
|
|
|
|
gsap.to(item, {
|
|
z: zDepth,
|
|
scale: scale,
|
|
duration: 0.7,
|
|
ease: "power3.out",
|
|
overwrite: true
|
|
});
|
|
|
|
gsap.to(img, {
|
|
x: moveX,
|
|
y: moveY,
|
|
rotateZ: imgRotate,
|
|
duration: 0.7,
|
|
ease: "power3.out",
|
|
overwrite: true
|
|
});
|
|
});
|
|
}
|
|
|
|
function handleLeave() {
|
|
wrapRotateX(0);
|
|
wrapRotateY(0);
|
|
wrapY(0);
|
|
|
|
showcaseItems.forEach((item) => {
|
|
const img = item.querySelector("img");
|
|
if (!img) return;
|
|
|
|
gsap.to(item, {
|
|
z: 0,
|
|
scale: 1,
|
|
duration: 0.9,
|
|
ease: "power3.out",
|
|
overwrite: true
|
|
});
|
|
|
|
gsap.to(img, {
|
|
x: 0,
|
|
y: 0,
|
|
rotateZ: 0,
|
|
duration: 0.9,
|
|
ease: "power3.out",
|
|
overwrite: true
|
|
});
|
|
});
|
|
}
|
|
|
|
showcaseSlider.addEventListener("pointermove", handleMove);
|
|
showcaseSlider.addEventListener("pointerleave", handleLeave);
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
(function ($) {
|
|
"use strict";
|
|
|
|
$(document).ready(function () {
|
|
|
|
// ===== Menu Toggle =====
|
|
$(".menu-trigger").click(() => $(".slide-menu").addClass("active"));
|
|
$(".menu-close").click(() => $(".slide-menu").removeClass("active"));
|
|
|
|
// ===== Sticky Header =====
|
|
|
|
|
|
|
|
// ===== Hide Header on Scroll Down / Show on Scroll Up =====
|
|
let lastScrollTop = 0;
|
|
let floatingShown = false;
|
|
|
|
$(window).on("scroll", () => {
|
|
|
|
const currentScroll = $(window).scrollTop();
|
|
const header = $(".header");
|
|
|
|
header.toggleClass("sticky", currentScroll >= 100);
|
|
|
|
if (currentScroll > lastScrollTop && currentScroll > 120) {
|
|
|
|
header.addClass("header-hidden");
|
|
|
|
if (!floatingShown) {
|
|
$(".lets-talk-btn").addClass("is-visible");
|
|
$(".language-action").addClass("is-visible");
|
|
floatingShown = true;
|
|
}
|
|
|
|
} else {
|
|
|
|
header.removeClass("header-hidden");
|
|
|
|
}
|
|
|
|
// hide floating buttons again at top
|
|
if (currentScroll <= 40) {
|
|
|
|
$(".lets-talk-btn").removeClass("is-visible");
|
|
$(".language-action").removeClass("is-visible");
|
|
|
|
floatingShown = false;
|
|
|
|
}
|
|
|
|
lastScrollTop = currentScroll <= 0 ? 0 : currentScroll;
|
|
|
|
});
|
|
|
|
|
|
|
|
// ===== Infinite Brand Slider =====
|
|
const brandTrack = document.querySelector(".slider-track");
|
|
if (brandTrack) {
|
|
brandTrack.innerHTML += brandTrack.innerHTML;
|
|
const totalBrandWidth = brandTrack.scrollWidth / 2;
|
|
|
|
gsap.to(brandTrack, {
|
|
x: -totalBrandWidth,
|
|
duration: 20,
|
|
ease: "none",
|
|
repeat: -1
|
|
});
|
|
}
|
|
|
|
// ===== Owl Carousel Sliders =====
|
|
// ===== Inspiration Overlay Slider =====
|
|
|
|
|
|
|
|
$('.showcase-gellary').owlCarousel({
|
|
loop: true,
|
|
center: true,
|
|
margin: 0,
|
|
nav: false,
|
|
dots: false,
|
|
smartSpeed: 850,
|
|
autoplay: false,
|
|
autoplayTimeout: 63200,
|
|
autoplayHoverPause: true,
|
|
mouseDrag: true,
|
|
touchDrag: true,
|
|
pullDrag: true,
|
|
responsive: {
|
|
0: {
|
|
items: 1.15,
|
|
stagePadding: 30
|
|
|
|
},
|
|
768: {
|
|
items: 2.2,
|
|
stagePadding: 40
|
|
|
|
},
|
|
1024: {
|
|
items: 3,
|
|
stagePadding: 50,
|
|
margin: 15
|
|
}
|
|
}
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ===== Portfolio Filter (Multi-Select) =====
|
|
const filterButtons = document.querySelectorAll(".portfolio-filter button");
|
|
const projectCards = document.querySelectorAll(".project-card");
|
|
let activeFilters = new Set();
|
|
|
|
filterButtons.forEach(btn => {
|
|
btn.addEventListener("click", () => {
|
|
const filter = btn.dataset.filter;
|
|
|
|
if (filter === "all") {
|
|
activeFilters.clear();
|
|
filterButtons.forEach(b => b.classList.remove("active"));
|
|
btn.classList.add("active");
|
|
projectCards.forEach(c => c.classList.remove("hide"));
|
|
return;
|
|
}
|
|
|
|
activeFilters.has(filter) ? activeFilters.delete(filter) : activeFilters.add(filter);
|
|
btn.classList.toggle("active");
|
|
|
|
document.querySelector(".portfolio-filter button[data-filter='all']").classList.remove("active");
|
|
|
|
projectCards.forEach(card => {
|
|
const matches = [...activeFilters].some(f => card.classList.contains(f));
|
|
card.classList.toggle("hide", activeFilters.size > 0 && !matches);
|
|
});
|
|
});
|
|
});
|
|
// ===== Portfolio Filter (Multi-Select) =====
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Beyond the Brief Card Infinite Slider Start
|
|
|
|
|
|
|
|
// ===== Flip Card =====
|
|
let currentCard = null;
|
|
function handleFlip(card) {
|
|
if (currentCard === card) return card.classList.remove("active"), currentCard = null;
|
|
|
|
if (currentCard) currentCard.classList.remove("active");
|
|
card.classList.add("active");
|
|
currentCard = card;
|
|
}
|
|
|
|
// ===== Branding Gallery Hover Effect =====
|
|
document.querySelectorAll(".branding-list h2").forEach(item => {
|
|
const hoverImage = document.querySelector(".hover-image");
|
|
const hoverImgTag = hoverImage.querySelector("img");
|
|
|
|
item.addEventListener("mouseenter", () => {
|
|
hoverImgTag.src = item.dataset.img;
|
|
hoverImage.style.opacity = 1;
|
|
});
|
|
item.addEventListener("mouseleave", () => hoverImage.style.opacity = 0);
|
|
item.addEventListener("mousemove", e => {
|
|
hoverImage.style.left = e.clientX + "px";
|
|
hoverImage.style.top = e.clientY + "px";
|
|
});
|
|
});
|
|
|
|
// ===== Text Vertical Slide =====
|
|
window.addEventListener("load", () => {
|
|
document.querySelectorAll(".shkVrtx91A").forEach(container => {
|
|
const items = container.querySelectorAll(".shk-vrtx-item-91A");
|
|
if (!items.length) return;
|
|
|
|
container.appendChild(items[0].cloneNode(true));
|
|
const itemHeight = items[0].offsetHeight;
|
|
const tl = gsap.timeline({ repeat: -1 });
|
|
|
|
items.forEach((_, i) => {
|
|
tl.to(container, { y: -itemHeight * (i + 1), duration: 0.5, ease: "power2.inOut" })
|
|
.to({}, { duration: 1.2 }); // pause
|
|
});
|
|
});
|
|
});
|
|
|
|
// ===== Scroll-triggered GSAP Animations =====
|
|
gsap.registerPlugin(ScrollTrigger, MorphSVGPlugin);
|
|
|
|
// Awards fade in
|
|
const awardsWrap = document.querySelector(".awards-wrap");
|
|
|
|
if (awardsWrap) {
|
|
|
|
gsap.to(awardsWrap, {
|
|
opacity: 1,
|
|
y: 0,
|
|
duration: 1,
|
|
ease: "power3.out",
|
|
scrollTrigger: {
|
|
trigger: awardsWrap,
|
|
start: "top 80%",
|
|
toggleActions: "play none none none"
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
// Showcase Items
|
|
gsap.utils.toArray(".showcase-item").forEach(item => {
|
|
gsap.set(item, { opacity: 0, y: 20 });
|
|
gsap.to(item, {
|
|
opacity: 1,
|
|
y: 0,
|
|
stagger: 0.12,
|
|
ease: "power1.out",
|
|
scrollTrigger: {
|
|
trigger: ".slider-wrap",
|
|
start: "top 75%",
|
|
end: "bottom 25%",
|
|
toggleActions: "play none none none"
|
|
}
|
|
});
|
|
});
|
|
|
|
// Footer Shake Animation
|
|
const down = 'M0-0.3C0-0.3,464,156,1139,156S2278-0.3,2278-0.3V683H0V-0.3z';
|
|
const center = 'M0-0.3C0-0.3,464,0,1139,0s1139-0.3,1139-0.3V683H0V-0.3z';
|
|
|
|
ScrollTrigger.create({
|
|
trigger: '.footer',
|
|
start: 'top bottom',
|
|
toggleActions: 'play pause resume reverse',
|
|
onEnter: self => {
|
|
const variation = self.getVelocity() / 10000;
|
|
gsap.fromTo('#bouncy-path', { morphSVG: down }, {
|
|
duration: 2,
|
|
morphSVG: center,
|
|
ease: `elastic.out(${1 + variation}, ${1 - variation})`,
|
|
overwrite: 'true'
|
|
});
|
|
}
|
|
});
|
|
|
|
// ===== Interactive Movement Effects =====
|
|
function addPointerEffect(elements, options) {
|
|
elements.forEach(el => {
|
|
const rotX = gsap.quickTo(el, "rotationX", options);
|
|
const rotY = gsap.quickTo(el, "rotationY", options);
|
|
const moveX = gsap.quickTo(el, "x", options);
|
|
const moveY = gsap.quickTo(el, "y", options);
|
|
const scale = gsap.quickTo(el, "scale", options);
|
|
|
|
el.addEventListener("pointermove", e => {
|
|
const rect = el.getBoundingClientRect();
|
|
const x = (e.clientX - rect.left) / rect.width;
|
|
const y = (e.clientY - rect.top) / rect.height;
|
|
|
|
rotX(gsap.utils.interpolate(options.rotXMin, options.rotXMax, y));
|
|
rotY(gsap.utils.interpolate(options.rotYMin, options.rotYMax, x));
|
|
moveX(gsap.utils.interpolate(options.moveXMin, options.moveXMax, x));
|
|
moveY(gsap.utils.interpolate(options.moveYMin, options.moveYMax, y));
|
|
scale(options.scaleValue);
|
|
});
|
|
|
|
el.addEventListener("pointerleave", () => {
|
|
rotX(0); rotY(0); moveX(0); moveY(0); scale(1);
|
|
});
|
|
});
|
|
}
|
|
|
|
addPointerEffect(document.querySelectorAll(".showcase-item"), {
|
|
duration: 0.6, ease: "power4.out",
|
|
rotXMin: 25, rotXMax: -25, rotYMin: -25, rotYMax: 25,
|
|
moveXMin: -40, moveXMax: 40, moveYMin: -40, moveYMax: 40,
|
|
scaleValue: 1.08
|
|
});
|
|
|
|
addPointerEffect(document.querySelectorAll(".awards-wrap img"), {
|
|
duration: 0.3, ease: "power3.out",
|
|
rotXMin: 35, rotXMax: -35, rotYMin: -35, rotYMax: 35,
|
|
moveXMin: -25, moveXMax: 25, moveYMin: -25, moveYMax: 25,
|
|
scaleValue: 1.12
|
|
});
|
|
|
|
addPointerEffect(document.querySelectorAll(".team-card"), {
|
|
duration: 0.2, ease: "power3.out",
|
|
rotXMin: 25, rotXMax: -25, rotYMin: -25, rotYMax: 25,
|
|
moveXMin: 0, moveXMax: 0, moveYMin: 0, moveYMax: 0,
|
|
scaleValue: 1.1
|
|
});
|
|
|
|
// ===== Falling Buttons Animation =====
|
|
document.querySelectorAll(".ct-falling-btn-wrap a").forEach(btn => {
|
|
const randomX = gsap.utils.random(-50, 50);
|
|
const randomRot = gsap.utils.random(-25, 25);
|
|
const randomDelay = gsap.utils.random(0, 0.5);
|
|
|
|
gsap.fromTo(btn,
|
|
{ y: -200, x: 0, rotation: 0, opacity: 0 },
|
|
{
|
|
y: 0,
|
|
x: randomX,
|
|
rotation: randomRot,
|
|
opacity: 1,
|
|
duration: 1.2,
|
|
ease: "power3.out",
|
|
delay: randomDelay,
|
|
scrollTrigger: {
|
|
trigger: ".branding-area",
|
|
start: "top 85%",
|
|
toggleActions: "play none none none"
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
})(jQuery);
|
|
|
|
|
|
|
|
|
|
// Text Vertical Slide Effect Start
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const container = document.getElementById("ctVertical");
|
|
const items = container.children;
|
|
|
|
container.appendChild(items[0].cloneNode(true));
|
|
|
|
const itemHeight = items[0].offsetHeight;
|
|
const tl = gsap.timeline({ repeat: -1 });
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
tl.to(container, {
|
|
y: -itemHeight * i,
|
|
duration: 0.6,
|
|
ease: "power2.inOut"
|
|
})
|
|
.to({}, { duration: 1.5 }); // ⏸ pause time
|
|
}
|
|
});
|
|
// Text Vertical Slide Effect End
|
|
|
|
|
|
// Falling buttons on About page
|
|
|
|
gsap.registerPlugin(ScrollTrigger);
|
|
|
|
ScrollTrigger.create({
|
|
trigger: ".branding-area",
|
|
start: "top 99%",
|
|
once: true,
|
|
onEnter: () => {
|
|
const buttons = document.querySelectorAll(".ct-falling-btn-wrap a");
|
|
|
|
buttons.forEach((btn) => {
|
|
const randomX = gsap.utils.random(-70, 70);
|
|
const randomY = gsap.utils.random(0, 20);
|
|
const randomRot = gsap.utils.random(-15, 15);
|
|
const randomDelay = gsap.utils.random(0, 0.4);
|
|
|
|
gsap.fromTo(
|
|
btn,
|
|
{
|
|
y: gsap.utils.random(-520, -400),
|
|
x: 0,
|
|
rotation: gsap.utils.random(-8, 8),
|
|
opacity: 0,
|
|
},
|
|
{
|
|
y: randomY,
|
|
x: randomX,
|
|
rotation: randomRot,
|
|
opacity: 1,
|
|
duration: gsap.utils.random(1.6, 2.1),
|
|
ease: "expo.out",
|
|
delay: randomDelay,
|
|
overwrite: "auto"
|
|
}
|
|
);
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
// ===== Separate H2 Word by Word Scroll Reveal =====
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
|
|
if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") return;
|
|
|
|
function wordReveal(selector, options = {}) {
|
|
|
|
const blocks = document.querySelectorAll(selector);
|
|
|
|
blocks.forEach((block) => {
|
|
|
|
// prevent double init
|
|
if (!block.dataset.wordRevealInit) {
|
|
|
|
const words = block.textContent.trim().split(" ");
|
|
|
|
block.innerHTML = words
|
|
.map(word => `<span class="word-reveal-word">${word} </span>`)
|
|
.join("");
|
|
|
|
block.dataset.wordRevealInit = "true";
|
|
}
|
|
|
|
const spans = block.querySelectorAll(".word-reveal-word");
|
|
|
|
gsap.set(spans, {
|
|
color: options.fromColor || "#F5F5F5"
|
|
});
|
|
|
|
gsap.to(spans, {
|
|
color: options.toColor || "#4050FF",
|
|
stagger: options.stagger || 0.08,
|
|
ease: "none",
|
|
|
|
scrollTrigger: {
|
|
trigger: block,
|
|
start: options.start || "top 100%",
|
|
end: options.end || "bottom 45%",
|
|
scrub: options.scrub || 1,
|
|
invalidateOnRefresh: true
|
|
}
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
// ===== PRVI + TRETJI BLOCK =====
|
|
// začne normalno
|
|
wordReveal(
|
|
".together-content h2, .together-content3 h2",
|
|
{
|
|
start: "top 100%",
|
|
end: "bottom 45%",
|
|
fromColor: "#F5F5F5",
|
|
toColor: "#4050FF",
|
|
scrub: 1
|
|
}
|
|
);
|
|
|
|
|
|
// ===== DRUGI BLOCK =====
|
|
// začne prej + back scroll takoj reagira
|
|
wordReveal(
|
|
".together-content2 h2",
|
|
{
|
|
start: "top 70%",
|
|
end: "bottom 30%",
|
|
fromColor: "#F5F5F5",
|
|
toColor: "#4050FF",
|
|
scrub: 0.4
|
|
}
|
|
);
|
|
|
|
}); |