Wright's Ferry Mansion

μ „ν˜œλ¦°Β·2024λ…„ 4μ›” 8일
0

Portfolio

λͺ©λ‘ 보기
7/11

πŸ’» Wright's Ferry Mansion 클둠 μ½”λ”©

  • μ‚¬μ΄νŠΈλͺ…: Wright's Ferry Mansion
  • μ œμž‘κΈ°κ°„: 24.03.12 ~ 24.03.15(3일 μ†Œμš”)
  • μ‚¬μš©μ–Έμ–΄: html, css, js
  • λΆ„λ₯˜: λ°˜μ‘ν˜•

πŸ” Main Point

  • GSAP을 μ‚¬μš©ν•˜μ—¬ κ°€λ‘œ 슀크둀 νŽ˜μ΄μ§€ λ§Œλ“€κΈ°
  • κ°€λ‘œ 슀크둀 μ• λ‹ˆλ©”μ΄μ…˜ 연동
  • GSAP νΌμ„ΌνŠΈ 값이 ν”½μ…€λ‘œ λ³€ν™˜λ˜λŠ” 것을 λ°©μ§€ν•˜κ³  싢을 λ•Œ
  • CSS clamp() ν•¨μˆ˜

GSAP을 μ‚¬μš©ν•˜μ—¬ κ°€λ‘œ 슀크둀 νŽ˜μ΄μ§€ λ§Œλ“€κΈ°

Wright's Ferry MansionλŠ” λͺ¨λ“  컨텐츠가 κ°€λ‘œλ‘œ 흐λ₯΄λŠ” 횑 슀크둀 μ‚¬μ΄νŠΈλ‹€. μΈν„°λž™μ…˜ κ΅¬ν˜„μ„ μœ„ν•΄ GSAPκ³Ό sticky 속성을 μ΄μš©ν–ˆλŠ”λ°, 방법은 μ•„λž˜μ™€ κ°™λ‹€.

HTML

  • horizontal-container -> sticky 속성을 μ‚¬μš©ν•  μš”μ†Œμ˜ λΆ€λͺ¨. νŽ˜μ΄μ§€μ˜ 슀크둀 값을 κ°€μ§ˆ μš”μ†Œλ‘œ, μ»¨ν…μΈ μ˜ κ°€λ‘œλ„ˆλΉ„μ˜ 총합을 높이 κ°’μœΌλ‘œ 쀄 μ˜ˆμ •μ΄λ‹€.(높이 κ°’ = 슀크둀 μ–‘)
  • horizontal -> stickyκ°€ 될 μš”μ†Œ
  • content-wrap -> flex-container. ν•΄λ‹Ή μš”μ†Œμ˜ κ°€λ‘œ λ„ˆλΉ„ = horizontal-container의 height
  • content -> flex-item.
<div class="horizontal-container">
  <div class="horizontal">
    <div class="content-wrap">
      <div class="content">content1</div>
      <div class="content">content2</div>
      <div class="content">content3</div>
      <div class="content">content4</div>
      <div class="content">content5</div>
      <div class="content">content6</div>
    </div>
  </div>
</div>

CSS

.horizontal-container {
  position: relative;
}
.horizontal {
  position: sticky;
  top: 0;
  display: flex; /* content-wrap이 content의 총 κ°€λ‘œ λ„ˆλΉ„ 인식할 수 μžˆλ„λ‘ λΆ€λͺ¨μ—λ„ flex 속성 μ£ΌκΈ° */
  height: 100vh;
  overflow: hidden; /* κ°€λ‘œ 슀크둀 제거 */
}
.content-wrap {
  display: flex;
}
.content {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100vw;
  height: 100%;
  font-size: 5vw;
  font-weight: 700;
  color: #fff;
}

JS

  • 슀크둀 μ‹œ μ• λ‹ˆλ©”μ΄μ…˜ 싀행될 μš”μ†Œ -> content-wrap / 전체 컨텐츠 μ˜μ—­μ˜ κ°€λ‘œ λ„ˆλΉ„λ₯Ό 가지고 μžˆλŠ” μš”μ†Œ
  • κΈ°μ€€ -> horizontal-container / νŽ˜μ΄μ§€ 전체 슀크둀 μ–‘
  • 슀크둀 이벀트 -> xPercent: -100 / x: window.innerWidth
const horizontalMotion = gsap.to('.content-wrap', {
  ease: 'none',
  xPercent: -100, // μŒμˆ˜κ°’μ„ μ£Όμ–΄ μ™Όμͺ½μœΌλ‘œ 밀어버림. 슀크둀 μ‹œ μš”μ†Œκ°€ μ™Όμͺ½μœΌλ‘œ 이동
  x: function() {
    return window.innerWidth; // λΈŒλΌμš°μ € λ„ˆλΉ„ 만큼 xPercent둜 밀어버린 값을 λ‹€μ‹œ λ‹Ήκ²¨μ˜΄
  },
  scrollTrigger: {
    trigger: '.horizontal-container',
    start: '0% 0%',
    end: '100% 100%',
    scrub: 1, // ν•„μˆ˜! 슀크둀이 μ‚¬μš©λ  λ•Œλ§Œ μ΄λ²€νŠΈκ°€ μž¬μƒ λ˜λ„λ‘ λ§Œλ“€μ–΄μ£ΌλŠ” 속성
    invalidateOnRefresh: true, // ν™”λ©΄ 리사이징 μ‹œ μœˆλ„μš° λ„ˆλΉ„ μž¬κ³„μ‚°
    // markers: true,
  }
})

// 슀크둀 κ°’ λ™μ μœΌλ‘œ λ„£μ–΄μ£ΌκΈ°
setContentHeight();
window.addEventListener('resize', setContentHeight);

function setContentHeight() {
  const content = document.querySelector('.content-wrap');
  const contentWidth = content.getBoundingClientRect().width;
  const contentWrapper = document.querySelector('.horizontal-container');
  const windowWidth = window.innerWidth;

  contentWrapper.style.height = contentWidth + 'px';
}

μš°μ„ , content-wrap 전체λ₯Ό -100% 만큼 μ™Όμͺ½μœΌλ‘œ 밀어버린닀. 그럼 ν™”λ©΄μ—μ„œ 보이지 μ•Šκ²Œ 되며, μš”μ†Œμ˜ 끝점이 ν™”λ©΄ κΈ°μ€€ μ™Όμͺ½ 끝에 λΆ™μ–΄μžˆλŠ” μƒνƒœκ°€ λœλ‹€.

const horizontalMotion = gsap.to('.content-wrap', {
  ease: 'none',
  xPercent: -100, // μŒμˆ˜κ°’μ„ μ£Όμ–΄ μ™Όμͺ½μœΌλ‘œ 밀어버림. 슀크둀 μ‹œ μš”μ†Œκ°€ μ™Όμͺ½μœΌλ‘œ 이동
})

κ·Έκ±Έ.. μ–΄λ–»κ²Œ ν™•μΈν•˜μ§€?
개발자 λ„κ΅¬λ‘œ -100%둜 μ΄λ™ν•œ 값을 -99%둜 변경해보면 μ•„λž˜μ™€ 같이 ν™”λ©΄ μ™Όμͺ½μ— λΉΌκΌΌ 보인닀!

κ·Έ μƒνƒœμ—μ„œ λΈŒλΌμš°μ € λ„ˆλΉ„ 만큼 였λ₯Έμͺ½μœΌλ‘œ λ‹€μ‹œ λ‹ΉκΈ°κ²Œ 되면 μš”μ†Œ 맨 λ’€ μ˜μ—­μ΄ 화면에 λ³΄μ΄λŠ” μƒνƒœκ°€ λœλ‹€.

const horizontalMotion = gsap.to('.content-wrap', {
  ease: 'none',
  xPercent: -100, // μŒμˆ˜κ°’μ„ μ£Όμ–΄ μ™Όμͺ½μœΌλ‘œ 밀어버림. 슀크둀 μ‹œ μš”μ†Œκ°€ μ™Όμͺ½μœΌλ‘œ 이동
  x: function() {
    return window.innerWidth; // λΈŒλΌμš°μ € λ„ˆλΉ„ 만큼 xPercent둜 밀어버린 값을 λ‹€μ‹œ λ‹Ήκ²¨μ˜΄
  },
})

그리고 scrollTrigger에 μ‹œμž‘μ κ³Ό 끝점을 각각 '0% 0%', '100% 100%'둜 주게 되면 ν™”λ©΄μ—λŠ” μ˜μ—­ 맨 μ•žλΆ€λΆ„(content1)이 λ³΄μ—¬μ§€κ²Œ λœλ‹€. μΆ”κ°€λ‘œ μ• λ‹ˆλ©”μ΄μ…˜ μž¬μ‚¬μš©μ΄ κ°€λŠ₯ν•œ scrub 속성을 μΆ”κ°€ν•˜λ©΄ 완성이닀.
scrub 속성은 슀크둀이 μš”μ†Œ μ΄μ „μœΌλ‘œ λŒμ•„κ°€λ©΄ μ• λ‹ˆλ©”μ΄μ…˜ μ—­μ‹œ λ˜λŒμ•„κ°€λŠ” κΈ°λŠ₯으둜, μΌνšŒμ„± μ• λ‹ˆλ©”μ΄μ…˜ νš¨κ³Όκ°€ μ•„λ‹Œ κ²½μš°μ— μ‚¬μš©ν•  수 μžˆλ‹€. scrub 속성을 주지 μ•Šμ„ 경우, 슀크둀 μ‹œ start 지점뢀터 end μ§€μ κΉŒμ§€ ν•œλ²ˆμ— μ΄λ™ν•΄λ²„λ¦¬λŠ” μ΄μŠˆκ°€ λ°œμƒν•˜λ―€λ‘œ λ‚˜μ—κ² ν•„μˆ˜ 속성이닀!

const horizontalMotion = gsap.to('.content-wrap', {
  ease: 'none',
  xPercent: -100, // μŒμˆ˜κ°’μ„ μ£Όμ–΄ μ™Όμͺ½μœΌλ‘œ 밀어버림. 슀크둀 μ‹œ μš”μ†Œκ°€ μ™Όμͺ½μœΌλ‘œ 이동
  x: function() {
    return window.innerWidth; // λΈŒλΌμš°μ € λ„ˆλΉ„ 만큼 xPercent둜 밀어버린 값을 λ‹€μ‹œ λ‹Ήκ²¨μ˜΄
  },
  scrollTrigger: {
    trigger: '.horizontal-container',
    start: '0% 0%',
    end: '100% 100%',
    scrub: 1, // ν•„μˆ˜! 슀크둀이 μ‚¬μš©λ  λ•Œλ§Œ μ΄λ²€νŠΈκ°€ μž¬μƒ λ˜λ„λ‘ λ§Œλ“€μ–΄μ£ΌλŠ” 속성
    invalidateOnRefresh: true, // ν™”λ©΄ 리사이징 μ‹œ μœˆλ„μš° λ„ˆλΉ„ μž¬κ³„μ‚°
  }
})

그리고 invalidateOnRefresh: true 값을 μ£Όμ–΄μ•Ό ν•˜λŠ”λ° true둜 μ„€μ •ν•  경우 ν™”λ©΄ 리사이징 μ‹œ window.innerWidth 값이 κ°±μ‹ λœλ‹€. ν•΄λ‹Ή 속성은 scrollTrigger μ•ˆμ— μ„ μ–Έν•΄μ•Όν•œλ‹€!

참고둜, css의 sticky λŒ€μ‹  gsap의 μš”μ†Œλ₯Ό κ³ μ •ν•˜λŠ” pin 속성을 μ‚¬μš©ν•΄λ„ κ²°κ³ΌλŠ” λ™μΌν•˜λ‹€. ex) pin: '.horizontal'

πŸ“‚ μ™„μ„±μ½”λ“œ



κ°€λ‘œ 슀크둀 μ• λ‹ˆλ©”μ΄μ…˜ 연동

HTML

데이터 μ†μ„±μœΌλ‘œ λ°˜λ³΅λ¬Έμ„ 돌렀 효과λ₯Ό μ μš©ν•  μ˜ˆμ •μœΌλ‘œ μ• λ‹ˆλ©”μ΄μ…˜ 이름과 μ μš©ν•  값을 속성에 μΆ”κ°€ν•œλ‹€.

<div class="horizontal-container">
  <div class="horizontal">
    <div class="content-wrap">
      <div class="content">
        <span class="text" data-motion="scale" data-motion-value="2">Hello !</span>
      </div>
      <div class="content">
        <span class="text" data-motion="rotate" data-motion-value="360">Hello !</span>
      </div>
      <div class="content">
        <div class="box" data-motion="scale" data-motion-value="4"></div>
      </div>
      <div class="content">
        <span class="text" data-motion="fade-out">Hello !</span>
      </div>
      <div class="content">
        <span class="text" data-motion="fade-in">Hello !</span>
      </div>
      <div class="content">
        <span class="text" data-motion="rotate" data-motion-value="360">Hello !</span>
      </div>
    </div>
  </div>
</div>

JS

[data-motion] 속성을 가지고 μžˆλŠ” μš”μ†Œλ₯Ό μ°Ύμ•„ λ°˜λ³΅λ¬Έμ„ 돌린 ν›„ κ·Έ μ•ˆμ—μ„œ λ‹€μ‹œ 쑰건문을 λ§Œλ“€μ–΄ 효과λ₯Ό μ£Όμ—ˆλ‹€.
data-motion-value 값이 없을 κ²½μš°μ—λŠ” κΈ°λ³Έ κ°’μœΌλ‘œ 0이 μ μš©λ˜λ„λ‘ ν•˜μ˜€λ‹€.
μ—¬κΈ°μ„œ κ°€μž₯ μ€‘μš”ν•œ 것은, containerAnimation이닀. ν•΄λ‹Ή 속성은 수직 μŠ€ν¬λ‘€μ„ μˆ˜ν‰μœΌλ‘œ μ• λ‹ˆλ©”μ΄μ…˜ν™” ν•  수 있게 ν•΄μ€€λ‹€. 속성 값에 λΆ€λͺ¨ μ• λ‹ˆλ©”μ΄μ…˜(horizontalMotion)을 κ±Έμ–΄μ£Όλ©΄ "연동"이 λ˜μ–΄ 이제 μˆ˜ν‰ 이동에 λŒ€ν•΄ scrollTrigger 섀정이 κ°€λŠ₯ν•˜λ‹€.
μ—¬κΈ°μ„œ markersλ₯Ό μ„€μ •ν•˜λ©΄ κ°€λ‘œ λͺ¨λ“œμ— λ§žμΆ”μ–΄ ν‘œμ‹œλœλ‹€.

document.querySelectorAll('[data-motion]').forEach(function(element, index) {
  const value = element.dataset.motionValue ? element.dataset.motionValue : 0;

  if(element.dataset.motion === 'rotate') {
    gsap.to(element, {
      rotate: value,
      duration: 1,
      scrollTrigger: {
        trigger: element.parentElement,
        start: 'left center',
        end: 'right left',
        scrub: 0,
        containerAnimation: horizontalMotion, // λΆ€λͺ¨ μ• λ‹ˆλ©”μ΄μ…˜ μ—°κ²°
        // markers: true
      }
    })
  }
  if(element.dataset.motion === 'fade-in') {
    gsap.from(element, {
      opacity: value,
      duration: 1,
      scrollTrigger: {
        trigger: element.parentElement,
        start: 'left center',
        end: 'center center',
        scrub: 0,
        containerAnimation: horizontalMotion, // λΆ€λͺ¨ μ• λ‹ˆλ©”μ΄μ…˜ μ—°κ²°
        // markers: true
      }
    })
  }
  // ...μ΄ν•˜ μƒλž΅

πŸ“‚ μ™„μ„±μ½”λ“œ



GSAP νΌμ„ΌνŠΈ 값이 ν”½μ…€λ‘œ λ³€ν™˜λ˜λŠ” 것을 λ°©μ§€ν•˜κ³  μ‹ΆμŠ΅λ‹ˆλ‹€.

μš°μ„  μ™„μ„± 화면을 λ¨Όμ € 보자.

μœ„μ™€ 같이 햄버거 λ²„νŠΌμ„ ν† κΈ€ν•  λ•Œλ§ˆλ‹€ λ„€λΉ„κ²Œμ΄μ…˜ μ˜μ—­μ΄ xμΆ• 100% <-> 0%둜 λ³€κ²½ λ˜λ„λ‘ μ„€μ • ν–ˆλ‹€.
ν•˜μ§€λ§Œ λ¬Έμ œκ°€ μžˆμ—ˆλŠ”λ°, ν™”λ©΄ μ‚¬μ΄μ¦ˆκ°€ 변경될 경우 xPercent 값이 μ—…λ°μ΄νŠΈ λ˜μ–΄ λ°˜μ‘ν˜• λŒ€μ‘μ΄ λ˜μ§€ μ•Šμ•˜λ‹€.

μˆ˜μ • μ „ μ½”λ“œ

const navTl = gsap.timeline({
  paused: true,
  defaults : {
    ease: 'none',
    duration: 0.3
  },
});
navTl
  .to('.gnb', {x: 0, xPercent: 0, 'pointer-events': 'auto'}, 'a') // 문제의 μ½”λ“œ !
  .to('.btn-nav .bar:nth-child(1)', {y: 6, rotate: 45}, 'a')
  .to('.btn-nav .bar:nth-child(3)', {y: -6, rotate: -45}, 'a')
  .to('.btn-nav .bar:nth-child(2)', {opacity: 0}, 'a')
  .to('.gnb-container .dim', {opacity: 1, 'pointer-events': 'auto'}, 'a')

const navBtn = document.querySelector('.btn-nav');

navBtn.addEventListener('click', function() {
  const headerEl = document.querySelector('.header');
  const CLASSNAME = 'is-open';

  headerEl.classList.toggle(CLASSNAME);
  if(headerEl.classList.contains(CLASSNAME)) {
    navTl.play();
  } else {
    navTl.reverse();
  }
})

예λ₯Ό λ“€μ–΄, 1200px ν™”λ©΄μ—μ„œ λ²„νŠΌμ„ μ—΄κ³  λ‹€μ‹œ λ‹«μ•˜λ‹€. 그리고 λ‚˜μ„œ 개발자 λ„κ΅¬λ‘œ ν™•μΈν•΄λ³΄λ‹ˆ xPercent 값이 μ•„λž˜ 이미지와 같이 px둜 λ³€κ²½λ˜μ–΄ μžˆλ‹€.

그리고 λ‚˜μ„œ 화면을 1920px둜 늘린 ν›„ λ‹€μ‹œ λ²„νŠΌμ„ ν† κΈ€ν–ˆμ§€λ§Œ, 값은 κ°±μ‹ λ˜μ§€ μ•Šκ³  처음의 600px κ°’μœΌλ‘œ κ³ μ •λ˜μ–΄ λ„€λΉ„κ²Œμ΄μ…˜ μ˜μ—­μ΄ 계속 보여지고 μžˆλ‹€. (transform: translateX(100%) κ°€ μ μš©λ˜μ§€ μ•Šμ•„ 화면에 계속 보여지고 있음)

문제 해결을 μœ„ν•΄ ꡬ글링을 ν•˜μ˜€λŠ”λ°, μ•„λž˜μ™€ 같이 해닡을 μ°Ύμ•˜λ‹€!

μ²˜μŒμ— set으둜 초기 μ„ΈνŒ…ν•˜λŠ” 것이닀. 그럼 toggle ν–ˆμ„ 경우 px둜 λ³€ν™˜λ˜μ§€ μ•Šκ³  100%둜 μ μš©λ˜μ–΄ 리사이징 μ‹œμ—λ„ μ΄μŠˆκ°€ λ°œμƒν•˜μ§€ μ•ŠλŠ”λ‹€.

gsap.set('.gnb', {x: 0, xPercent: 100});

μˆ˜μ • ν›„ μ½”λ“œ

gsap.set('.gnb', {x: 0, xPercent: 100}); // λ°˜μ‘ν˜• λŒ€μ‘μ„ μœ„ν•΄ μ΄ˆκΈ°μ„ΈνŒ…

const navTl = gsap.timeline({
  paused: true,
  defaults : {
    ease: 'none',
    duration: 0.3
  },
});
navTl
  .to('.gnb', {xPercent: 0, 'pointer-events': 'auto'}, 'a')
  .to('.btn-nav .bar:nth-child(1)', {y: 6, rotate: 45}, 'a')
  .to('.btn-nav .bar:nth-child(3)', {y: -6, rotate: -45}, 'a')
  .to('.btn-nav .bar:nth-child(2)', {opacity: 0}, 'a')
  .to('.gnb-container .dim', {opacity: 1, 'pointer-events': 'auto'}, 'a')

const navBtn = document.querySelector('.btn-nav');

navBtn.addEventListener('click', function() {
  const headerEl = document.querySelector('.header');
  const CLASSNAME = 'is-open';

  headerEl.classList.toggle(CLASSNAME);
  if(headerEl.classList.contains(CLASSNAME)) {
    navTl.play();
  } else {
    navTl.reverse();
  }
})


CSS clamp() ν•¨μˆ˜

clamp()κ°€ ν•˜λŠ” 일은 μ •μ˜λœ 두 값인 μ΅œμ†Ÿκ°’κ³Ό μ΅œλŒ“κ°’ μ‚¬μ΄μ˜ 값을 μ„ νƒν•œλ‹€.
3개의 λ§€κ°œλ³€μˆ˜(μ΅œμ†Ÿκ°’, μ„ ν˜Έκ°’, μ΅œλŒ“κ°’)λ₯Ό μ‚¬μš©ν•œλ‹€.

.item {
  width: clamp(200px, 50%, 1000px);
}

μœ„ μ½”λ“œμ—μ„œ μ΅œμ†Ÿκ°’μ€ 200px, μ΅œλŒ“κ°’μ€ 1000px이며, μ„ ν˜Έν•˜λŠ” 값은 width의 50%이닀.
ν•΄λ‹Ή κ°’(50%)은 200px ~ 1000px μ‚¬μ΄μ—λ§Œ μ‘΄μž¬ν•˜κ²Œ λœλ‹€.

clamp()λŠ” μ–΄λ–»κ²Œ κ³„μ‚°λ˜λŠ” 걸까?

MDN은 μ•„λž˜μ™€ 같이 μ„€λͺ…ν•œλ‹€.

.item {
  width: clamp(200px, 50%, 1000px);
  /* Is equivalent to the below */
  width: max(200px, min(50%, 1000px));
}

50%λŠ” 뷰포트의 λ„ˆλΉ„μ— μ˜ν•΄ κ²°μ •λœλ‹€.
뷰포트 λ„ˆλΉ„κ°€ 1150px 이라고 ν•˜λ©΄ λ‹€μŒκ³Ό 같은 λ‹¨κ³„λ‘œ κ³„μ‚°λœλ‹€.

.item {
  width: max(200px, min(50%, 1000px));
  /* Assuming the viewport width is 1150px */
  width: max(200px, min(575px, 1000px));
  /* Resolves to */
  width: max(200px, 575px);
  /* Resolves to */
  width: 575px;
}


μ°Έκ³  μ‚¬μ΄νŠΈ
https://gsap.com/community/forums/topic/28215-prevent-gsap-from-converting-percentage-values-to-pixels/#comment-139360
https://itchallenger.tistory.com/921

profile
μ½”λ”©μͺΌμ•„

0개의 λŒ“κΈ€