parallaxBanner.vue 6.3 KB

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