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();
});
});