parallaxBanner.vue 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. <template>
  2. <section class="prallax--banner--wrapper" ref="parallaxWrapper">
  3. <div class="prallax--banner" ref="parallaxBanner">
  4. <picture>
  5. <source
  6. media="(min-width:1440px)"
  7. :srcset="`${images.xl} 1920w, ${images.xl2x || images.xl} 3840w`"
  8. sizes="(min-width:1920px) 1920px, 100vw"
  9. width="1920"
  10. height="2000"
  11. />
  12. <source
  13. media="(min-width:768px)"
  14. :srcset="`${images.m} 1439w, ${images.m2x || images.m} 2878w`"
  15. sizes="100vw"
  16. width="1439"
  17. height="1750"
  18. />
  19. <source
  20. media="(min-width:375px)"
  21. :srcset="`${images.s} 787w, ${images.s2x || images.s} 1574w`"
  22. sizes="100vw"
  23. width="787"
  24. height="1250"
  25. />
  26. <img
  27. :alt="imageAlt"
  28. loading="lazy"
  29. :src="images.xs"
  30. :srcset="`${images.xs} 374w, ${images.xs2x || images.xs} 748w`"
  31. />
  32. </picture>
  33. </div>
  34. <div class="text--box--wrapper" ref="textWrapper">
  35. <div class="text--container">
  36. <h2>{{ title }}</h2>
  37. <p>{{ description }}</p>
  38. <a v-if="linkUrl && linkText" :href="linkUrl">{{ linkText }}</a>
  39. </div>
  40. </div>
  41. </section>
  42. </template>
  43. <script setup>
  44. // Props 정의
  45. const props = defineProps({
  46. // 이미지 경로들
  47. images: {
  48. type: Object,
  49. default: () => ({
  50. xl:
  51. "https://media.audi.com/is/image/audi/nemo/misc/audi-exclusive-one-audi-ready/parallax-teaser/02_r8_2021_3104_58056-XL.jpg?auto=webp&width=1920",
  52. xl2x:
  53. "https://media.audi.com/is/image/audi/nemo/misc/audi-exclusive-one-audi-ready/parallax-teaser/02_r8_2021_3104_58056-XL.jpg?auto=webp&width=3840",
  54. m:
  55. "https://media.audi.com/is/image/audi/nemo/misc/audi-exclusive-one-audi-ready/parallax-teaser/02_r8_2021_3104_58056-M.jpg?auto=webp&width=1439",
  56. m2x:
  57. "https://media.audi.com/is/image/audi/nemo/misc/audi-exclusive-one-audi-ready/parallax-teaser/02_r8_2021_3104_58056-M.jpg?auto=webp&width=2878",
  58. s:
  59. "https://media.audi.com/is/image/audi/nemo/misc/audi-exclusive-one-audi-ready/parallax-teaser/02_r8_2021_3104_58056-S.jpg?auto=webp&width=787",
  60. s2x:
  61. "https://media.audi.com/is/image/audi/nemo/misc/audi-exclusive-one-audi-ready/parallax-teaser/02_r8_2021_3104_58056-S.jpg?auto=webp&width=1574",
  62. xs:
  63. "https://media.audi.com/is/image/audi/nemo/misc/audi-exclusive-one-audi-ready/parallax-teaser/02_r8_2021_3104_58056-XS.jpg?auto=webp&width=374",
  64. xs2x:
  65. "https://media.audi.com/is/image/audi/nemo/misc/audi-exclusive-one-audi-ready/parallax-teaser/02_r8_2021_3104_58056-XS.jpg?auto=webp&width=748",
  66. }),
  67. },
  68. // 이미지 alt 텍스트
  69. imageAlt: {
  70. type: String,
  71. default: "Audi Exclusive",
  72. },
  73. // 제목
  74. title: {
  75. type: String,
  76. default: "Audi exclusive order 주문하기",
  77. },
  78. // 설명
  79. description: {
  80. type: String,
  81. default:
  82. "당신만의 특별한 아우디 주문은 아우디 딜러샵을 통해 신청 가능하며, 자세한 딜러 네트워크 정보는 아래 버튼을 통해 확인하실 수 있습니다.",
  83. },
  84. // 링크 URL
  85. linkUrl: {
  86. type: String,
  87. default: "/",
  88. },
  89. // 링크 텍스트
  90. linkText: {
  91. type: String,
  92. default: "아우디 딜러 네트워크 알아보기",
  93. },
  94. });
  95. // Refs
  96. const parallaxWrapper = ref(null);
  97. const parallaxBanner = ref(null);
  98. const textWrapper = ref(null);
  99. // Easing 함수 - 부드러운 가속/감속 효과
  100. const easeOutQuart = (t) => {
  101. return 1 - Math.pow(1 - t, 4);
  102. };
  103. // 패럴렉스 효과 함수
  104. const handleScroll = () => {
  105. if (!parallaxWrapper.value || !parallaxBanner.value || !textWrapper.value) return;
  106. const rect = parallaxWrapper.value.getBoundingClientRect();
  107. const windowHeight = window.innerHeight;
  108. // 요소가 화면에 보이는지 확인
  109. if (rect.bottom >= 0 && rect.top <= windowHeight) {
  110. // 스크롤 진행률 계산 (0 ~ 1.5) - 50% 추가 범위
  111. const scrollProgress = Math.max(
  112. 0,
  113. Math.min(1.5, (windowHeight - rect.top) / (windowHeight + rect.height))
  114. );
  115. // Easing 적용
  116. const easedProgress = easeOutQuart(scrollProgress);
  117. // 배경 이미지 패럴렉스 효과 (아우디 실제 로직: 스크롤 거리의 25%만큼 움직임) - translate3d로 GPU 가속
  118. const scrollDistance = (windowHeight - rect.top);
  119. const bgTranslateY = scrollDistance * 0.25;
  120. parallaxBanner.value.style.transform = `translate3d(-50%, ${bgTranslateY}px, 0)`;
  121. parallaxBanner.value.style.webkitTransform = `translate3d(-50%, ${bgTranslateY}px, 0)`;
  122. // 텍스트 박스 패럴렉스 효과 (아우디 실제 로직: 스크롤 거리의 -12.5%만큼 움직임) - translate3d로 GPU 가속
  123. const textTranslateY = scrollDistance * -0.125;
  124. const textOpacity = Math.max(
  125. 0.5,
  126. Math.min(1, 1.3 - Math.abs(scrollProgress - 0.5) * 1.2)
  127. );
  128. textWrapper.value.style.transform = `translate3d(0, ${textTranslateY}px, 0)`;
  129. textWrapper.value.style.webkitTransform = `translate3d(0, ${textTranslateY}px, 0)`;
  130. textWrapper.value.style.opacity = textOpacity;
  131. }
  132. };
  133. // Intersection Observer를 사용한 성능 최적화
  134. let observer = null;
  135. onMounted(() => {
  136. // 스크롤 이벤트를 throttle하여 성능 최적화
  137. let ticking = false;
  138. const throttledScroll = () => {
  139. if (!ticking) {
  140. requestAnimationFrame(() => {
  141. handleScroll();
  142. ticking = false;
  143. });
  144. ticking = true;
  145. }
  146. };
  147. // Intersection Observer로 뷰포트 진입 감지
  148. observer = new IntersectionObserver(
  149. (entries) => {
  150. entries.forEach((entry) => {
  151. if (entry.isIntersecting) {
  152. window.addEventListener("scroll", throttledScroll, { passive: true });
  153. } else {
  154. window.removeEventListener("scroll", throttledScroll);
  155. }
  156. });
  157. },
  158. {
  159. rootMargin: "100px 0px",
  160. }
  161. );
  162. if (parallaxWrapper.value) {
  163. observer.observe(parallaxWrapper.value);
  164. }
  165. // 초기 실행
  166. handleScroll();
  167. });
  168. onUnmounted(() => {
  169. if (observer) {
  170. observer.disconnect();
  171. }
  172. window.removeEventListener("scroll", handleScroll);
  173. });
  174. </script>