document.addEventListener("DOMContentLoaded", () => { const section = document.querySelector(".gallery"); const mainItems = Array.from(document.querySelectorAll(".gallery_list .gallery_item")); const viewport = document.querySelector(".list_thumbs"); const numberHost = document.querySelector(".number_active"); const activeSmallEl = document.querySelector(".active_small > div"); const allSmallEl = document.querySelector(".all_small > div"); const illImg = document.querySelector(".ill_middle img"); if (!section || !viewport || !mainItems.length) return; const initialThumbs = Array.from(viewport.querySelectorAll(".thumb_item")); const count = Math.min(mainItems.length, initialThumbs.length); if (!count) return; const DIGITS = 3; const fmt = (n) => String(n).padStart(DIGITS, "0"); const mod = (n, m) => ((n % m) + m) % m; const isDesktop = () => window.matchMedia("(min-width: 992px)").matches; let logicalIndex = 0; let current = 0; let wheelLocked = false; let isAnimating = false; let unlockTimer = 0; let bigValue = 1; let numTimer = null; function buildRoll(text) { const roll = document.createElement("div"); roll.className = "num_roll"; for (const ch of text) { const col = document.createElement("span"); col.className = "digit"; col.innerHTML = ` ${ch} ${ch} `; roll.appendChild(col); } return roll; } function animateBigNumber(nextValue) { if (!numberHost) return; if (nextValue === bigValue) return; if (numTimer) clearTimeout(numTimer); const prev = fmt(bigValue); const next = fmt(nextValue); const cols = Array.from(numberHost.querySelectorAll(".digit")); cols.forEach((col, i) => { const currEl = col.querySelector(".digit_curr"); const nextEl = col.querySelector(".digit_next"); currEl.textContent = prev[i]; nextEl.textContent = next[i]; currEl.style.transition = "none"; nextEl.style.transition = "none"; currEl.style.transform = "translateY(0)"; nextEl.style.transform = "translateY(0)"; nextEl.style.top = "100%"; }); requestAnimationFrame(() => { cols.forEach((col, i) => { if (prev[i] === next[i]) return; const currEl = col.querySelector(".digit_curr"); const nextEl = col.querySelector(".digit_next"); currEl.style.transition = "transform .24s ease"; nextEl.style.transition = "transform .24s ease"; currEl.style.transform = "translateY(-100%)"; nextEl.style.transform = "translateY(-100%)"; }); }); numTimer = setTimeout(() => { bigValue = nextValue; numberHost.innerHTML = ""; numberHost.appendChild(buildRoll(fmt(bigValue))); }, 260); } if (numberHost) { numberHost.innerHTML = ""; numberHost.appendChild(buildRoll(fmt(bigValue))); } const base = initialThumbs.slice(0, count); const inner = document.createElement("div"); inner.className = "list_thumbs_inner"; base.forEach((el) => inner.appendChild(el)); base.forEach((el) => inner.appendChild(el.cloneNode(true))); viewport.innerHTML = ""; viewport.appendChild(inner); const thumbs = Array.from(inner.querySelectorAll(".thumb_item")); function getStepW() { if (thumbs.length < 2) return 144; return thumbs[1].offsetLeft - thumbs[0].offsetLeft; } function getCycleW() { return getStepW() * count; } let x = 0; let isPointerDown = false; let isFreeDragging = false; let pointerId = null; let dragStartX = 0; let dragStartY = 0; let dragStartTranslate = 0; let dragMoved = false; let suppressClick = false; let lastDragDx = 0; const DRAG_THRESHOLD = 8; function applyTransform(withAnim) { inner.style.transition = withAnim ? "transform .26s ease" : "none"; inner.style.transform = `translate3d(${x}px,0,0)`; } function setNumbers() { animateBigNumber(current + 1); const text = fmt(current + 1); if (activeSmallEl) activeSmallEl.textContent = text; if (allSmallEl) allSmallEl.textContent = `- ${fmt(count)}`; } function getVisibleActiveThumbIndex() { const leftEdge = viewport.getBoundingClientRect().left + 6; const candidates = [current, current + count].filter((idx) => thumbs[idx]); let bestIdx = candidates[0]; let bestDist = Infinity; candidates.forEach((idx) => { const d = Math.abs(thumbs[idx].getBoundingClientRect().left - leftEdge); if (d < bestDist) { bestDist = d; bestIdx = idx; } }); return bestIdx; } // Desktop: pick the visible copy of current (not left-edge biased) function getDesktopActiveThumbIndex() { const candidates = [current, current + count].filter((idx) => thumbs[idx]); if (!candidates.length) return current; const vRect = viewport.getBoundingClientRect(); let bestIdx = candidates[0]; let bestVisible = -1; candidates.forEach((idx) => { const r = thumbs[idx].getBoundingClientRect(); const visible = Math.min(r.right, vRect.right) - Math.max(r.left, vRect.left); if (visible > bestVisible) { bestVisible = visible; bestIdx = idx; } }); return bestIdx; } function setSingleActiveThumb() { if (isFreeDragging) return; thumbs.forEach((el) => el.classList.remove("is-active")); const idx = isDesktop() ? getDesktopActiveThumbIndex() : getVisibleActiveThumbIndex(); const best = thumbs[idx]; if (!best) return; best.classList.add("is-active"); } function paintActive() { mainItems.forEach((el, i) => el.classList.toggle("is-active", i === current)); setSingleActiveThumb(); setNumbers(); } function animateIll(dir) { if (!illImg || typeof gsap === "undefined") return; gsap.killTweensOf(illImg); gsap.timeline() .to(illImg, { scale: 1.03, rotate: dir > 0 ? 1.2 : -1.2, y: -4, duration: 0.18, ease: "power2.out" }) .to(illImg, { scale: 1, rotate: 0, y: 0, duration: 0.34, ease: "power3.out" }); } function normalizeForDirection(dir) { const cycle = getCycleW(); if (!cycle) return; if (dir > 0 && x <= -cycle + 0.5) { x += cycle; applyTransform(false); setSingleActiveThumb(); } if (dir < 0 && x >= -0.5) { x -= cycle; applyTransform(false); setSingleActiveThumb(); } } function normalizeXToNearestCycle() { const cycle = getCycleW(); if (!cycle) return; while (x <= -cycle) x += cycle; while (x > 0) x -= cycle; } function syncIndexFromX() { const stepW = getStepW(); if (!stepW) return { changed: false, dir: 0 }; const prevCurrent = current; normalizeXToNearestCycle(); logicalIndex = Math.round(-x / stepW); current = mod(logicalIndex, count); x = -logicalIndex * stepW; normalizeXToNearestCycle(); applyTransform(false); paintActive(); if (current === prevCurrent) return { changed: false, dir: 0 }; const dir = lastDragDx < 0 ? 1 : -1; return { changed: true, dir }; } function finishStep() { clearTimeout(unlockTimer); isAnimating = false; setSingleActiveThumb(); } function step(dir) { if (isAnimating) return; isAnimating = true; const stepW = getStepW(); if (!stepW) { isAnimating = false; return; } normalizeForDirection(dir); logicalIndex += dir; current = mod(logicalIndex, count); paintActive(); animateIll(dir); x -= dir * stepW; applyTransform(true); const onEnd = () => { inner.removeEventListener("transitionend", onEnd); finishStep(); }; inner.addEventListener("transitionend", onEnd, { once: true }); unlockTimer = setTimeout(() => { inner.removeEventListener("transitionend", onEnd); finishStep(); }, 700); } function goToLogical(nextLogical) { if (isAnimating) return; const target = mod(nextLogical, count); if (target === current) return; let delta = target - current; if (delta > count / 2) delta -= count; if (delta < -count / 2) delta += count; logicalIndex += delta; current = target; paintActive(); animateIll(delta > 0 ? 1 : -1); } function jumpToPhysical(thumbPhysicalIndex) { if (isAnimating) return; const from = getVisibleActiveThumbIndex(); const delta = thumbPhysicalIndex - from; if (delta === 0) return; const stepW = getStepW(); if (!stepW) return; isAnimating = true; const dir = delta > 0 ? 1 : -1; normalizeForDirection(dir); logicalIndex += delta; current = mod(logicalIndex, count); paintActive(); animateIll(dir); x -= delta * stepW; applyTransform(true); const onEnd = () => { inner.removeEventListener("transitionend", onEnd); clearTimeout(unlockTimer); isAnimating = false; setSingleActiveThumb(); }; inner.addEventListener("transitionend", onEnd, { once: true }); unlockTimer = setTimeout(() => { inner.removeEventListener("transitionend", onEnd); isAnimating = false; setSingleActiveThumb(); }, 700); } thumbs.forEach((thumb, i) => { thumb.addEventListener("click", (e) => { if (suppressClick) { e.preventDefault(); e.stopPropagation(); return; } if (isDesktop()) { goToLogical(mod(i, count)); return; } jumpToPhysical(i); }); }); viewport.addEventListener("pointerdown", (e) => { if (e.pointerType === "mouse") return; if (isAnimating) return; isPointerDown = true; isFreeDragging = true; pointerId = e.pointerId; dragStartX = e.clientX; dragStartY = e.clientY; dragStartTranslate = x; dragMoved = false; suppressClick = false; lastDragDx = 0; viewport.setPointerCapture(pointerId); inner.style.transition = "none"; }); viewport.addEventListener("pointermove", (e) => { if (!isPointerDown || e.pointerId !== pointerId) return; const dx = e.clientX - dragStartX; const dy = e.clientY - dragStartY; lastDragDx = dx; if (!dragMoved) { if (Math.abs(dx) < DRAG_THRESHOLD) return; if (Math.abs(dy) > Math.abs(dx)) return; dragMoved = true; suppressClick = true; } x = dragStartTranslate + dx; applyTransform(false); e.preventDefault(); }, { passive: false }); function endDrag(e) { if (!isPointerDown || e.pointerId !== pointerId) return; isPointerDown = false; try { viewport.releasePointerCapture(pointerId); } catch (_) {} pointerId = null; isFreeDragging = false; if (dragMoved) { const result = syncIndexFromX(); if (result.changed) animateIll(result.dir); setTimeout(() => { suppressClick = false; }, 0); } else { setSingleActiveThumb(); } } viewport.addEventListener("pointerup", endDrag); viewport.addEventListener("pointercancel", endDrag); window.addEventListener("wheel", (e) => { const rect = section.getBoundingClientRect(); const inView = rect.top < window.innerHeight && rect.bottom > 0; const inside = e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom; if (!inView || !inside) return; e.preventDefault(); if (wheelLocked || isAnimating || isPointerDown) return; wheelLocked = true; step(e.deltaY > 0 ? 1 : -1); setTimeout(() => { wheelLocked = false; }, 190); }, { passive: false }); logicalIndex = 0; current = 0; x = 0; applyTransform(false); paintActive(); setSingleActiveThumb(); window.addEventListener("resize", () => { const stepW = getStepW(); x = -logicalIndex * stepW; applyTransform(false); setSingleActiveThumb(); }); });