UNPKG

3d-slider

Version:

A customizable 3D cylindrical slider component for web projects.

403 lines (368 loc) 13.2 kB
import "./3d-slider.css"; import nextBtnImg from "./images/slider-next-btn.png"; import prevBtnImg from "./images/slider-prev-btn.png"; import nextGroupBtnImg from "./images/slider-next-group-btn.png"; import prevGroupBtnImg from "./images/slider-prev-group-btn.png"; // Initialize 3D Slider function initializeSlider({ sliderContainerClass, perspective, transitionDuration, alwaysOnMode, alwaysOnDesktopDuration, alwaysOnMobileDuration, cardsToShowLargeScreen, cardsToShowMediumScreen, cardsToShowSmallScreen, cardsToShowMobile, dotsMode, dotColor, }) { // Slider values let visibleCardsArray = []; let dotsArray = []; let isLargeScreen = null; let isMediumScreen = null; let isSmallScreen = null; let isXSmallScreen = null; let cardsToShow = null; let perspectiveValue = null; let translateZValue = null; let currentTranslateZValue = null; let isAlwaysOnMode = null; let translateZInterval = null; let currentRotateAngle = 0; let initialIndex = 0; let currentIndex = 0; let groupIndex = 0; let xSmallMode = 0; let startX = 0; let endX = 0; let isTouching = false; // Check device const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( navigator.userAgent ); const sliderContainer = document.querySelector(`.${sliderContainerClass}`); if (!sliderContainer) { console.error( `No element found with class name "${sliderContainerClass}".` ); return; } perspective = perspective || 1; if (!transitionDuration) { transitionDuration = 2; } isAlwaysOnMode = alwaysOnMode; if (cardsToShowMobile >= 3 && isMobile) { cardsToShowMobile = 3; xSmallMode = 0; } else if (cardsToShowMobile < 3 && isMobile) { cardsToShowMobile = 2; xSmallMode = 1; } sliderContainer.classList.add("slider-container-3d"); const cardsArray = Array.from(sliderContainer.children).filter( (child) => child.tagName === "DIV" ); // Generate slider elements function createElement(tagName, className) { const element = document.createElement(tagName); if (className) element.className = className; return element; } const slider = createElement("div", "slider-3d"); const sliderPrevGroupBtn = createElement("div", "slider-prev-group-btn"); sliderPrevGroupBtn.style.backgroundImage = `url(${prevGroupBtnImg})`; const sliderNextGroupBtn = createElement("div", "slider-next-group-btn"); sliderNextGroupBtn.style.backgroundImage = `url(${nextGroupBtnImg})`; const sliderPrevBtn = createElement("div", "slider-prev-btn"); sliderPrevBtn.style.backgroundImage = `url(${prevBtnImg})`; const sliderNextBtn = createElement("div", "slider-next-btn"); sliderNextBtn.style.backgroundImage = `url(${nextBtnImg})`; const navigateContainer = createElement("div", "navigate-container"); const dotsContainer = createElement("div", "dots-container"); sliderContainer.innerHTML = ""; sliderContainer.appendChild(slider); // Reset previous interval if (translateZInterval) clearInterval(translateZInterval); // Adjust sizes function adjustBtnSize() { let btnWidth = parseInt(sliderContainer.clientWidth * 0.025); isMobile ? (btnWidth = btnWidth * 3) : ""; sliderContainer.style.setProperty("--btn-width", btnWidth + "px"); } function setSize() { isLargeScreen = sliderContainer.clientWidth >= 1280; isMediumScreen = sliderContainer.clientWidth >= 1024 && sliderContainer.clientWidth < 1280; isSmallScreen = sliderContainer.clientWidth >= 768 && sliderContainer.clientWidth < 1024; isXSmallScreen = sliderContainer.clientWidth < 768; setCardsToShowNumber(); setVisibleCardsArray(initialIndex, initialIndex + cardsToShow); adjustBtnSize(); setControls(); } setSize(); // Set cards to show function setCardsToShowNumber() { if (isLargeScreen) { cardsToShow = cardsToShowLargeScreen || 9; } else if (isMediumScreen) { cardsToShow = cardsToShowMediumScreen || 7; } else if (isSmallScreen) { cardsToShow = cardsToShowSmallScreen || 4; } else if (isXSmallScreen) { cardsToShow = cardsToShowMobile; } const sliderHeight = slider.clientHeight; const sliderWidth = slider.clientWidth; perspectiveValue = sliderWidth * perspective; translateZValue = sliderWidth * 0.347; currentTranslateZValue = sliderWidth * 0.245; const sliderContainerProperties = { "--slides": cardsToShow, "--perspective": `${perspectiveValue}px`, "--dot-color": `${dotColor}5e`, "--active-dot": dotColor, }; for (const [property, value] of Object.entries(sliderContainerProperties)) { sliderContainer.style.setProperty(property, value); } const sliderProperties = { "--slides": cardsToShow, "--always-rotate-on-duration": isMobile ? alwaysOnMobileDuration + "s" : alwaysOnDesktopDuration + "s", "--translate-z": `${currentTranslateZValue}px`, "--transition": `${transitionDuration}s`, "--card-width": `${ (sliderWidth / cardsToShow) * (1.6 - xSmallMode / 2) }px`, "--card-height": `${ isMobile ? sliderHeight * 0.5 : sliderHeight * 0.57 }px`, }; for (const [property, value] of Object.entries(sliderProperties)) { slider.style.setProperty(property, value); } } // Set dots function setControls() { if (dotsMode) { dotsContainer.innerHTML = ""; dotsArray = []; const dotsNumber = Math.ceil(cardsArray.length / cardsToShow); for (let index = 0; index < dotsNumber; index++) { const dot = document.createElement("div"); dotsArray.push(dot); dot.classList.add("dot"); dotsContainer.appendChild(dot); if (index === 0) dot.classList.add("active-dot"); } } let navigateWidth = isMobile ? 75 : 25; sliderContainer.style.setProperty("--nav-width", navigateWidth + "%"); navigateContainer.appendChild(sliderPrevBtn); navigateContainer.appendChild(sliderNextBtn); sliderContainer.appendChild(navigateContainer); dotsContainer.appendChild(sliderPrevGroupBtn); dotsContainer.appendChild(sliderNextGroupBtn); sliderContainer.appendChild(dotsContainer); } function updateActiveDot() { dotsArray.forEach((dot, index) => { dot.classList.toggle( "active-dot", index === Math.floor(currentIndex / cardsToShow) ); }); } // Set visible cards function setVisibleCardsArray(startIndex, lastIndex) { slider.innerHTML = ""; visibleCardsArray = cardsArray.slice(startIndex, lastIndex).reverse(); visibleCardsArray.forEach((card, index) => { card.classList.add("card-3d"); card.style.setProperty("--card-group-index", index); slider.appendChild(card); slider.style.setProperty("--visible-cards", visibleCardsArray.length); slider.style.setProperty( "--card-rotate", `${360 / (visibleCardsArray.length + xSmallMode)}deg` ); }); // Slider rotate function adjustSliderRotate() { const rotateAngel = 360 / (visibleCardsArray.length + xSmallMode); const additionalRotate = currentRotateAngle % rotateAngel; currentRotateAngle === 0 ? (currentRotateAngle = rotateAngel) : (currentRotateAngle = currentRotateAngle - additionalRotate + rotateAngel); slider.style.transform = `rotateY(${currentRotateAngle}deg)`; resetRotateDeg(currentRotateAngle, transitionDuration); if (isAlwaysOnMode) { slider.classList.add("always-rotate-on"); resetBtnOpacity(0.5); } } adjustSliderRotate(); setTimeout(() => { openCards(); }, 2000); } // Open cards animation function openCards() { translateZInterval = setInterval(() => { if (currentTranslateZValue >= translateZValue) { clearInterval(translateZInterval); currentTranslateZValue = translateZValue; } else { currentTranslateZValue += 1; slider.style.setProperty( "--translate-z", `${currentTranslateZValue}px` ); } }, 5); } // Reset slider properties function resetRotateDeg(rotateValue, transitionValue) { slider.style.setProperty("--current-rotate-angel", rotateValue + "deg"); slider.style.setProperty("--full-round", 360 + rotateValue + "deg"); slider.style.setProperty("--transition", transitionValue + "s"); } // Get slider current rotate function getCurrentRotateY() { const computedStyle = window.getComputedStyle(slider); const transformMatrix = computedStyle.transform; if (transformMatrix && transformMatrix !== "none") { const values = transformMatrix.match(/matrix3d\((.+)\)/); if (values) { const matrix = values[1].split(", ").map(parseFloat); const m11 = matrix[0]; const m13 = matrix[2]; const angle = Math.atan2(-m13, m11) * (180 / Math.PI); return angle >= 0 ? angle : angle + 360; } } return 0; } // Button opacity handler function resetBtnOpacity(opacityValue) { sliderNextBtn.style.setProperty("--btn-opacity", opacityValue); sliderPrevBtn.style.setProperty("--btn-opacity", opacityValue); } // Cards navigate function rotateSlider(direction) { if (!isAlwaysOnMode) { if (direction === "next") { currentRotateAngle += 360 / (visibleCardsArray.length + xSmallMode); } else if (direction === "prev") { currentRotateAngle += -(360 / (visibleCardsArray.length + xSmallMode)); } resetRotateDeg(currentRotateAngle, transitionDuration); slider.style.transform = `rotateY(${currentRotateAngle}deg)`; sliderNextBtn.disabled = true; sliderPrevBtn.disabled = true; setTimeout(() => { sliderNextBtn.disabled = false; sliderPrevBtn.disabled = false; }, transitionDuration * 500); } } function updateCurrentIndex(step) { currentIndex += step; if (currentIndex >= cardsArray.length) { currentIndex = 0; } else if (currentIndex < 0) { currentIndex = Math.max(cardsArray.length - cardsToShow, 0); } } // Groups navigate function navigateGroup(direction) { if (!isTouching && visibleCardsArray.length !== cardsArray.length) { if (direction === "next") { groupIndex++; if (groupIndex * cardsToShow >= cardsArray.length) { groupIndex = 0; } updateCurrentIndex(cardsToShow); setVisibleCardsArray(currentIndex, currentIndex + cardsToShow); updateActiveDot(); } else if (direction === "prev") { if (groupIndex === 0) { groupIndex = Math.ceil(cardsArray.length / cardsToShow) - 1; currentIndex = cardsArray.length - (cardsArray.length % cardsToShow || cardsToShow); } else { groupIndex--; updateCurrentIndex(-cardsToShow); } setVisibleCardsArray(currentIndex, currentIndex + cardsToShow); updateActiveDot(); } } } // Slider events sliderNextBtn.addEventListener("click", () => rotateSlider("next")); sliderPrevBtn.addEventListener("click", () => rotateSlider("prev")); sliderNextGroupBtn.addEventListener("click", () => navigateGroup("next")); sliderPrevGroupBtn.addEventListener("click", () => navigateGroup("prev")); sliderContainer.addEventListener("touchstart", (e) => { isTouching = true; startX = e.touches[0].clientX; }); sliderContainer.addEventListener("touchmove", (e) => { endX = e.touches[0].clientX; }); sliderContainer.addEventListener("touchend", () => { if (endX !== 0) { const swipeDistance = endX - startX; const minSwipeDistance = 50; if (swipeDistance > minSwipeDistance) { rotateSlider("next"); } else if (swipeDistance < -minSwipeDistance) { rotateSlider("prev"); } startX = 0; endX = 0; } isTouching = false; }); slider.addEventListener("click", () => { const toggleAlwaysOnClass = (enable) => { slider.classList.toggle("always-rotate-on", enable); slider.classList.toggle("always-rotate-off", !enable); }; if (alwaysOnMode) { if (isAlwaysOnMode) { const rotateY = getCurrentRotateY(); const stepAngle = 360 / (visibleCardsArray.length + xSmallMode); const additionalRotate = rotateY % stepAngle; const nearestAngle = additionalRotate > stepAngle / 2 ? rotateY + (stepAngle - additionalRotate) : rotateY - additionalRotate; currentRotateAngle = nearestAngle; slider.style.setProperty("--slider-current-rotate", rotateY + "deg"); resetRotateDeg(currentRotateAngle, 0); resetBtnOpacity(1); toggleAlwaysOnClass(false); setTimeout(() => { slider.style.transform = `rotateY(${currentRotateAngle}deg)`; }, 1000); } else { toggleAlwaysOnClass(true); resetBtnOpacity(0.5); } isAlwaysOnMode = !isAlwaysOnMode; } }); } export { initializeSlider };