"광주 문화예술 인문스토리 플랫폼2"의 두 판 사이의 차이
광주문화예술인문스토리플랫폼
(같은 사용자의 중간 판 4개는 보이지 않습니다) | |||
1번째 줄: | 1번째 줄: | ||
{{#tag:html| | {{#tag:html| | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
<!-- 에디터 컨텐츠 등록 영역 --> | <!-- 에디터 컨텐츠 등록 영역 --> | ||
<div class="gwangju-main-wrap"> | <div class="gwangju-main-wrap"> | ||
554번째 줄: | 459번째 줄: | ||
<div class="main-header"> | <div class="main-header"> | ||
<p class="main-subtit">3D ASSET</p> | <p class="main-subtit">3D ASSET</p> | ||
− | <h2 class="main-tit">3D로 보는 광주 인문 아카이브</h2> | + | <h2 class="main-tit">3D로 보는<br />광주 인문 아카이브</h2> |
</div> | </div> | ||
<div class="asset-swiper"> | <div class="asset-swiper"> | ||
709번째 줄: | 614번째 줄: | ||
<!-- //AR Heritage 영역 --> | <!-- //AR Heritage 영역 --> | ||
</div> | </div> | ||
− | < | + | <link rel="stylesheet" href="https://unpkg.com/swiper/swiper-bundle.min.css" /> |
− | + | <link | |
− | + | rel="stylesheet" | |
+ | href="https://cdn.jsdelivr.net/npm/keen-slider@6.8.5/keen-slider.min.css" | ||
+ | /> | ||
+ | <script src="https://unpkg.com/swiper/swiper-bundle.min.js"></script> | ||
+ | <script src="https://cdn.jsdelivr.net/npm/keen-slider@6.8.5/keen-slider.min.js"></script> | ||
+ | <script> | ||
+ | let swiperInstance = null; | ||
− | + | const initSwiper = () => { | |
− | + | // 모바일에서만 Swiper 초기화 | |
− | + | if (window.innerWidth < 768) { | |
− | + | swiperInstance = new Swiper('.main-visual-swiper', { | |
− | + | direction: 'horizontal', | |
− | + | slidesPerView: 1, | |
− | + | spaceBetween: 0, | |
− | + | pagination: { | |
− | + | el: '.swiper-fraction', | |
− | + | type: 'bullets', | |
− | + | clickable: true, // Bullet 클릭 가능 | |
− | + | }, | |
− | + | }); | |
− | + | } | |
− | + | }; | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | < | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | const destroySwiper = () => { | |
− | + | if (swiperInstance) { | |
− | + | swiperInstance.destroy(true, true); | |
− | + | swiperInstance = null; | |
− | + | } | |
− | + | }; | |
− | |||
− | |||
− | + | // 초기화 | |
− | + | initSwiper(); | |
− | |||
− | |||
− | |||
− | + | // 윈도우 리사이즈 시 Swiper 재설정 | |
− | + | window.addEventListener('resize', () => { | |
− | + | if (window.innerWidth < 768) { | |
− | + | if (!swiperInstance) { | |
− | + | initSwiper(); | |
− | + | } | |
− | + | } else if (window.innerWidth >= 768) { | |
− | + | if (swiperInstance) { | |
− | + | destroySwiper(); | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
} | } | ||
} | } | ||
− | } | + | }); |
+ | document.addEventListener('DOMContentLoaded', function () { | ||
+ | const storyLinks = document.querySelectorAll('.story-link-item'); | ||
+ | const imageContainer = document.querySelector('.image-scroll-container'); | ||
+ | const breakpoint = window.matchMedia('(max-width: 991px)'); | ||
+ | let swiperInstance; | ||
− | + | const initDesktop = () => { | |
− | + | if (swiperInstance) { | |
− | + | swiperInstance.destroy(true, true); | |
+ | swiperInstance = null; | ||
+ | } | ||
− | + | storyLinks.forEach(link => { | |
− | + | link.addEventListener('click', function (e) { | |
− | + | e.preventDefault(); | |
− | + | storyLinks.forEach(l => l.classList.remove('active')); | |
+ | document | ||
+ | .querySelectorAll('.swiper-slide') | ||
+ | .forEach(i => i.classList.remove('active')); | ||
− | + | this.classList.add('active'); | |
− | + | const targetId = this.getAttribute('data-target'); | |
− | + | const targetImage = document.getElementById(targetId); | |
− | + | if (targetImage) { | |
− | + | targetImage.classList.add('active'); | |
− | + | const containerRect = imageContainer.getBoundingClientRect(); | |
− | + | const imageRect = targetImage.getBoundingClientRect(); | |
− | + | const scrollTop = | |
− | + | imageContainer.scrollTop + | |
− | + | (imageRect.top - containerRect.top) - | |
− | + | containerRect.height / 2 + | |
− | + | imageRect.height / 2; | |
− | |||
− | + | imageContainer.scrollTo({ | |
− | + | top: scrollTop + 0, | |
− | + | behavior: 'smooth', | |
− | + | }); | |
− | + | } | |
− | + | }); | |
− | + | }); | |
− | + | }; | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | const initMobile = () => { | |
− | + | swiperInstance = new Swiper('.image-scroll-container.swiper', { | |
− | + | direction: 'horizontal', | |
− | + | slidesPerView: 1, | |
− | + | spaceBetween: 12, | |
− | + | pagination: { | |
+ | el: '.swiper-fraction', | ||
+ | type: 'fraction', | ||
+ | formatFractionCurrent: number => number, | ||
+ | formatFractionTotal: number => number, | ||
+ | }, | ||
+ | on: { | ||
+ | init: () => { | ||
+ | updateProgress(); | ||
+ | }, | ||
+ | slideChange: () => { | ||
+ | updateProgress(); | ||
+ | }, | ||
+ | }, | ||
+ | }); | ||
+ | }; | ||
− | + | const updateProgress = () => { | |
− | + | if (!swiperInstance) return; | |
+ | const current = swiperInstance.realIndex + 1; | ||
+ | const total = swiperInstance.slides.length; | ||
+ | const percent = (current / total) * 100; | ||
+ | const progressBar = document.querySelector('.swiper-progressbar .progress-fill'); | ||
+ | if (progressBar) { | ||
+ | progressBar.style.width = `${percent}%`; | ||
+ | } | ||
+ | }; | ||
− | + | const handleResize = () => { | |
− | + | if (breakpoint.matches) { | |
− | + | initMobile(); | |
− | + | } else { | |
− | + | initDesktop(); | |
− | + | } | |
− | + | }; | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | breakpoint.addEventListener('change', handleResize); | |
− | + | handleResize(); | |
− | + | }); | |
− | + | document.addEventListener('DOMContentLoaded', function () { | |
− | + | // 공통 브레이크포인트 기준 | |
+ | const breakpoint = window.matchMedia('(max-width: 991px)'); | ||
− | + | // Story Swiper (모바일 전용 터치) | |
− | + | let storySwiper; | |
− | |||
− | |||
− | |||
− | + | const initStorySwiper = () => { | |
− | + | const isMobile = breakpoint.matches; | |
− | |||
− | |||
− | |||
− | + | if (storySwiper) storySwiper.destroy(true, true); | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | storySwiper = new Swiper('.story-swiper .swiper', { | |
− | + | slidesPerView: 1, | |
− | + | spaceBetween: 24, | |
− | }); | + | grabCursor: isMobile, |
− | } | + | simulateTouch: isMobile, |
+ | allowTouchMove: isMobile, | ||
+ | pagination: { | ||
+ | el: '.swiper-pagination', | ||
+ | clickable: true, | ||
+ | }, | ||
+ | navigation: { | ||
+ | nextEl: '.swiper-button-next', | ||
+ | prevEl: '.swiper-button-prev', | ||
+ | }, | ||
+ | mousewheel: isMobile | ||
+ | ? { | ||
+ | forceToAxis: true, | ||
+ | invert: false, | ||
+ | } | ||
+ | : false, | ||
+ | on: { | ||
+ | progress: function (swiper, progress) { | ||
+ | const indicatorBar = swiper.el.querySelector('.indicator-bar'); | ||
+ | if (indicatorBar) { | ||
+ | indicatorBar.style.width = progress * 100 + '%'; | ||
+ | } | ||
+ | }, | ||
+ | }, | ||
}); | }); | ||
+ | }; | ||
+ | |||
+ | breakpoint.addEventListener('change', initStorySwiper); | ||
+ | initStorySwiper(); | ||
+ | |||
+ | // Asset Swiper type01 | ||
+ | const swiper01 = new Swiper('.asset-swiper .type01', { | ||
+ | loop: true, | ||
+ | slidesPerView: 'auto', | ||
+ | spaceBetween: 8, | ||
+ | autoplay: { | ||
+ | delay: 0, | ||
+ | disableOnInteraction: false, | ||
+ | }, | ||
+ | speed: 5000, | ||
+ | freeMode: true, | ||
+ | breakpoints: { | ||
+ | 991: { | ||
+ | spaceBetween: 28, | ||
+ | }, | ||
+ | }, | ||
}); | }); | ||
− | |||
− | + | // Asset Swiper type02 | |
− | + | const swiper02 = new Swiper('.asset-swiper .type02', { | |
− | + | loop: true, | |
− | slidesPerView: | + | slidesPerView: 'auto', |
− | spaceBetween: | + | spaceBetween: 8, |
− | + | autoplay: { | |
− | + | delay: 0, | |
− | + | disableOnInteraction: false, | |
− | + | reverseDirection: true, | |
− | |||
}, | }, | ||
− | + | speed: 5000, | |
− | + | freeMode: true, | |
− | + | breakpoints: { | |
− | + | 991: { | |
− | + | spaceBetween: 28, | |
− | |||
}, | }, | ||
}, | }, | ||
}); | }); | ||
− | } | + | }); |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | document.addEventListener('DOMContentLoaded', function () { | |
− | + | const progressBar = document.querySelector('.gwangju-main-section04 .progress-bar'); | |
− | + | const metaverseSlider = new Swiper('.gwangju-metaverse-slider', { | |
− | + | loop: true, | |
− | + | slidesPerView: 1.2, | |
− | + | spaceBetween: 15, | |
pagination: { | pagination: { | ||
el: '.swiper-pagination', | el: '.swiper-pagination', | ||
− | + | type: 'fraction', | |
− | |||
− | |||
− | |||
− | |||
}, | }, | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
on: { | on: { | ||
− | + | slideChange: function () { | |
− | const | + | const totalSlides = this.slides.length - this.loopedSlides * 2; |
− | if ( | + | const progress = (this.realIndex + 1) / totalSlides; |
− | + | if (progressBar) { | |
+ | progressBar.style.width = progress * 100 + '%'; | ||
} | } | ||
+ | }, | ||
+ | }, | ||
+ | breakpoints: { | ||
+ | 769: { | ||
+ | slidesPerView: 3, | ||
+ | spaceBetween: 30, | ||
+ | }, | ||
+ | 1024: { | ||
+ | slidesPerView: 4, | ||
+ | spaceBetween: 30, | ||
}, | }, | ||
}, | }, | ||
}); | }); | ||
+ | }); | ||
+ | var animation = { | ||
+ | duration: (document.querySelectorAll('.keen-slider__slide').length / 2) * 1500, | ||
+ | easing: t => t, | ||
}; | }; | ||
− | + | // 변수 초기화 | |
− | + | let slider = null; | |
− | + | let slider2 = null; | |
− | // | + | let autoplayActive = true; |
− | + | let autoplayTimeoutId = null; | |
+ | let isSyncingSlider1 = false; | ||
+ | let isSyncingSlider2 = false; | ||
+ | let isMouseOverContainer = false; // 컨테이너 마우스 상태 추적 변수 | ||
+ | // 슬라이더 2 먼저 초기화 | ||
+ | slider2 = new KeenSlider('#my-keen-slider2', { | ||
loop: true, | loop: true, | ||
− | + | renderMode: 'performance', | |
− | + | mode: 'free', | |
− | + | rtl: true, | |
− | + | slides: { | |
− | + | perView: 'auto', | |
+ | spacing: 8, | ||
}, | }, | ||
− | |||
− | |||
breakpoints: { | breakpoints: { | ||
− | + | '(min-width: 500px)': { | |
− | + | slides: { | |
+ | perView: 'auto', | ||
+ | spacing: 28, | ||
+ | }, | ||
}, | }, | ||
+ | }, | ||
+ | created(s) { | ||
+ | if (!isMouseOverContainer) { | ||
+ | s.moveToIdx(5, true, animation); | ||
+ | } | ||
+ | }, | ||
+ | animationEnded(s) { | ||
+ | // if (!isMouseOverContainer) { | ||
+ | slider2.endTimer = setTimeout(() => { | ||
+ | slider?.moveToIdx(slider.track.details.abs + 4, true, animation); | ||
+ | s.moveToIdx(s.track.details.abs + 5, true, animation); | ||
+ | }, 1000); | ||
+ | // } | ||
+ | }, | ||
+ | detailsChanged(s) { | ||
+ | // 마우스가 컨테이너 위에 있을 때만 동기화 작동 | ||
+ | if (slider2?.endTimer) clearTimeout(slider2?.endTimer); | ||
+ | if (!isSyncingSlider1) { | ||
+ | if (slider) { | ||
+ | if (slider.track) { | ||
+ | slider.animator.stop(); | ||
+ | isSyncingSlider2 = true; | ||
+ | // 위치 동기화 | ||
+ | const position = s.track.details.position; | ||
+ | slider.track.to(position); | ||
+ | setTimeout(() => { | ||
+ | isSyncingSlider2 = false; | ||
+ | }, 30); | ||
+ | } | ||
+ | } | ||
+ | } | ||
}, | }, | ||
}); | }); | ||
− | // | + | // slider1 초기화 |
− | + | slider = new KeenSlider('#my-keen-slider', { | |
loop: true, | loop: true, | ||
− | + | renderMode: 'performance', | |
− | + | mode: 'free', | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
breakpoints: { | breakpoints: { | ||
− | + | '(min-width: 500px)': { | |
− | + | slides: { | |
+ | perView: 'auto', | ||
+ | spacing: 28, | ||
+ | }, | ||
}, | }, | ||
}, | }, | ||
− | + | slides: { | |
− | + | perView: 'auto', | |
− | + | spacing: 8, | |
− | + | }, | |
− | + | created(s) { | |
− | + | if (!isMouseOverContainer) { | |
− | + | s.moveToIdx(5, true, animation); | |
− | + | } | |
− | + | }, | |
− | + | animationEnded(s) { | |
− | + | // if (!isMouseOverContainer) { | |
− | + | slider.endTimer = setTimeout(() => { | |
− | + | slider2?.moveToIdx(slider2.track.details.abs + 4, true, animation); | |
+ | s.moveToIdx(s.track.details.abs + 5, true, animation); | ||
+ | }, 1000); | ||
+ | // } | ||
}, | }, | ||
− | + | detailsChanged(s) { | |
− | + | if (slider?.endTimer) clearTimeout(slider?.endTimer); | |
− | + | // 마우스가 컨테이너 위에 있을 때만 동기화 작동 | |
− | + | if (!isSyncingSlider2) { | |
− | + | if (slider2) { | |
− | + | if (slider2.track) { | |
+ | slider2.animator.stop(); | ||
+ | isSyncingSlider1 = true; | ||
+ | slider2.track.to(s.track.details.position); | ||
+ | setTimeout(() => { | ||
+ | isSyncingSlider1 = false; | ||
+ | }, 30); | ||
+ | } | ||
} | } | ||
− | } | + | } |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
}, | }, | ||
}); | }); | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | // 컨테이너 하나에만 마우스 이벤트 적용 | |
− | + | document.querySelector('.asset-swiper').addEventListener('pointerdown', () => { | |
− | + | // 타임아웃 삭제 | |
− | + | if (autoplayTimeoutId) clearTimeout(autoplayTimeoutId); | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | // | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | if ( | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | // 현재 진행 중인 애니메이션 중단 | |
− | + | if (slider) { | |
− | + | if (slider.animator) { | |
− | + | slider.animator.stop(); | |
− | + | } | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
} | } | ||
− | + | if (slider2) { | |
− | + | if (slider2.animator) { | |
− | + | slider2.animator.stop(); | |
− | + | } | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | } | ||
} | } | ||
− | + | }); | |
− | + | </script> | |
− | + | <!-- // 에디터 컨텐츠 등록 영역 -->}} | |
− | / | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | }} |
2025년 7월 18일 (금) 17:57 기준 최신판
MEMORIAL SITE
장소로 알아보는
열흘 간의 항쟁 기록
그날의 기억이 머문 장소들을 따라가며, 시간 속에 남은 이야기를 천천히 만나보세요.
번호를 따라가며 클릭하면, 장소마다 담긴 역사적 의미와 당시의 이야기를 자세히 살펴볼
수 있습니다. 광주의 골목골목에 스며든 기억과 그날의 진실과 마주해보세요.





STORY DATA
카테고리별로 만나는
인문 스토리
PLATFORM
AR Heritage
광주의 근현대 역사와 문화를 엿볼 수 있는 유산들을 증강현실
광주의 역사적 명소와 문화예술 거점을 AR 콘텐츠로 체험할 수 있는 투어입니다.
실시간 위치 정보와 장소의 맥락을 제공하여 시민들이 지역 인문자원을 향유하도록
기획되었습니다.
AR을 통해 광주 시민들이 공유하는 기억과 감성을 생생하게 느낄 수 있습니다.