diff --git a/src/assets/css/portfolio.css b/src/assets/css/portfolio.css index a8ca5ef..d0a5749 100644 --- a/src/assets/css/portfolio.css +++ b/src/assets/css/portfolio.css @@ -643,7 +643,7 @@ section[class*='-section'] { } .scroll-track { width: 5px; - height: 93px; + height: 86px; background: #dfdfdf; border-radius: 26.6px; position: relative; @@ -672,7 +672,7 @@ body.dragging-scroll { align-items: flex-start; gap: 48px; position: relative; - /* top: 69px; */ + top: 69px; transition: transform 0.3s ease; } .side-buttons { @@ -1504,7 +1504,7 @@ textarea { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 389px)); gap: 16px; - +margin-top: 24px; justify-content: center; } .related-card { @@ -1733,6 +1733,7 @@ textarea { top: 0; left: 0; width: 100%; + transition: transform 0.6s cubic-bezier(0.25, 1, 0.5, 1); } .text-item { height: 60dvh; diff --git a/src/assets/js/portfolio.js b/src/assets/js/portfolio.js index a5e0e55..eddc005 100644 --- a/src/assets/js/portfolio.js +++ b/src/assets/js/portfolio.js @@ -611,11 +611,126 @@ function updateScrollScene() { setActiveSlide(current); } -window.addEventListener('scroll', updateScrollScene); +let currentSlideIndex = 0; +let isAnimating = false; -window.addEventListener('resize', updateScrollScene); +function goToSlide(index) { + if (index < 0 || index >= totalSlides) return; + + currentSlideIndex = index; + isAnimating = true; + + const progress = index / (totalSlides - 1); + const exactIndex = index; + + const isMobile = window.innerWidth <= 768; + + if (isMobile) { + textTrack.style.transform = 'translateY(0)'; + resetTextItemInlineStyles(); + setActiveSlide(currentSlideIndex); + setTimeout(() => { + isAnimating = false; + }, 500); + return; + } + + textTrack.style.transform = `translateY(-${exactIndex * 60}vh)`; + + textItems.forEach((item) => { + item.classList.remove('active'); + }); + + textItems.forEach((item, idx) => { + const offset = idx - exactIndex; + const absOffset = Math.abs(offset); + + let blur = 0; + let opacity = 1; + + if (offset > 0) { + blur = absOffset > 0.1 ? Math.min((absOffset - 0.1) * 25, 20) : 0; + opacity = Math.max(1 - absOffset * 2.5, 0); + } + + item.style.filter = `blur(${blur}px)`; + item.style.opacity = opacity; + }); + + setActiveSlide(currentSlideIndex); + + setTimeout(() => { + isAnimating = false; + }, 600); +} + +let lastScrollTime = 0; +const scrollCooldown = 800; + +showcase.addEventListener('wheel', function(e) { + const currentTime = new Date().getTime(); + const isAtEnd = currentSlideIndex === totalSlides - 1; + const isAtStart = currentSlideIndex === 0; + + + if ((e.deltaY > 0 && !isAtEnd) || (e.deltaY < 0 && !isAtStart)) { + e.preventDefault(); + } + + + if (isAnimating || (currentTime - lastScrollTime < scrollCooldown)) { + return; + } + + if (Math.abs(e.deltaY) < 10) return; + + if (e.deltaY > 0) { + if (!isAtEnd) { + lastScrollTime = currentTime; + goToSlide(currentSlideIndex + 1); + } + } else { + if (!isAtStart) { + lastScrollTime = currentTime; + goToSlide(currentSlideIndex - 1); + } + } +}, { passive: false }); + +showcase.addEventListener('touchmove', function(e) { + const currentTime = new Date().getTime(); + const isAtEnd = currentSlideIndex === totalSlides - 1; + const isAtStart = currentSlideIndex === 0; + + const currentTouchY = e.touches[0].clientY; + const diffY = touchStartY - currentTouchY; + + + if ((diffY > 0 && !isAtEnd) || (diffY < 0 && !isAtStart)) { + e.preventDefault(); + } + + if (isAnimating || (currentTime - lastScrollTime < scrollCooldown)) { + return; + } + + if (Math.abs(diffY) > 50) { + if (diffY > 0) { + if (!isAtEnd) { + lastScrollTime = currentTime; + goToSlide(currentSlideIndex + 1); + } + } else { + if (!isAtStart) { + lastScrollTime = currentTime; + goToSlide(currentSlideIndex - 1); + } + } + } +}, { passive: false }); + +goToSlide(0); -updateScrollScene(); const portfolioNavConfig = { actionType: 'project', @@ -971,25 +1086,27 @@ document.addEventListener('DOMContentLoaded', () => { updateUI(); }); - const relatedCards = [ ...document.querySelectorAll('[data-related-section="gallery"] .related-card'), ]; +const hasGalleryThumbnail = document.querySelector('.gallery-thumbnail') !== null; + let currentPopupIndex = 0; -const galleryItems = [ - { +const galleryItems = []; + +if (hasGalleryThumbnail) { + galleryItems.push({ type: 'project', - }, - - ...[...document.querySelectorAll('[data-related-section="gallery"] .related-card')].map( - (card) => ({ - type: 'card', - element: card, - }) - ), -]; + }); +} +relatedCards.forEach((card) => { + galleryItems.push({ + type: 'card', + element: card, + }); +}); function openPopupByIndex(index) { const item = galleryItems[index]; @@ -998,7 +1115,6 @@ function openPopupByIndex(index) { currentPopupIndex = index; - // اول هر دو پاپ‌آپ بسته شوند overlay.classList.remove('active'); imagePopup.classList.remove('active'); @@ -1024,13 +1140,11 @@ function openPopupByIndex(index) { function updatePopupButtons() { document.querySelectorAll('.next-popup').forEach((btn) => { btn.disabled = currentPopupIndex === galleryItems.length - 1; - btn.classList.toggle('disabled', currentPopupIndex === galleryItems.length - 1); }); document.querySelectorAll('.prev-popup').forEach((btn) => { btn.disabled = currentPopupIndex === 0; - btn.classList.toggle('disabled', currentPopupIndex === 0); }); } @@ -1040,15 +1154,18 @@ document.querySelector('.gallery-thumbnail .wrapper')?.addEventListener('click', openPopupByIndex(0); }); -relatedCards.forEach((card, index) => { +relatedCards.forEach((card) => { card.addEventListener('click', (e) => { if (e.target.closest('.play-btn')) return; - openPopupByIndex(index + 1); + const targetIndex = galleryItems.findIndex(item => item.element === card); + + if (targetIndex !== -1) { + openPopupByIndex(targetIndex); + } }); }); - document.querySelectorAll('.next-popup').forEach((btn) => { btn.addEventListener('click', () => { if (currentPopupIndex < galleryItems.length - 1) { @@ -1064,6 +1181,8 @@ document.querySelectorAll('.prev-popup').forEach((btn) => { } }); }); + + window.addEventListener('keydown', (e) => { if (!imagePopup.classList.contains('active')) return;