[vanila.js] JavaScript Random Text Reveal Effect

woolee의 기록보관소·2023년 3월 1일
0

FE 기능구현 연습

목록 보기
31/33

출처 : JavaScript Random Text Reveal Effect

index.html

js에서 해당 문구를 배열로 만들면 단순 띄어쓰기는 생략되므로
(no-break space)를 사용해서 공백을 표현해줘야 한다.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="style.css">
  <title>Document</title>
</head>
<body>

  <section>
    <h1>Woo &nbsp Lee</h1>
  </section>
  <section>
    <h1>About</h1>
  </section>
  <section>
    <h1>Contact</h1>
  </section>
  

  <script src="app.js"></script>
</body>
</html>

style.css

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: sans-serif;
  font-weight: 400;
  letter-spacing: -.08em;
  color: rgb(90, 90, 90);
}
section {
  position: relative;
  left: 50%;
  transform: translateX(-50%);
  height: 100vh;
  width: 95%;
  /* border: 1px solid black; */
}
h1 {
  position: absolute;
  bottom: 1rem;
  /* 
  clamp(min, prefered, max) 
  부모 요소를 기준으로 하는 상대 단위 사용함 

  prefered 크기를 갖되, min 만큼 작아질 수도 있고 max만큼 커질 수도 있다. 
  이 구간에서 웹 브라우저의 너비가 변하는 만큼 선형으로 글자 크기도 변하는 거임.
  */
  font-size: clamp(1px, 13vw, 100px);
  opacity: 0;
}
span {
  display: inline-block;
  transform: translateX(-10px);
  opacity: 0;
  transition: opacity .3s ease-in-out, transform .3s ease-in-out;
}

app.js

대문에 박을 문구를 h1에 작성하고 이를 배열에 담는다. h1Array
그리고 해당 문구를 보여주기 전에 파친코처럼 돌아갈 문구를 specialChars에 담는다.

Header 클래스를 생성한다. 이 클래스는 문장을 한 글자씩 분해해주는 클래스이다.

나중에 DOMContentLoaded 이벤트가 발생하면, h1Elements에 한 글자씩 담아줄 것이다.

그리고 DOMContentLoaded 이벤트가 발생하면
IntersectionObserver를 사용해서 실행해줄건데,

++Intersection Observer API는

  • 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법이다.
  • 사용법은 이렇다. new IntersectionObserver(callback, options)

options의 threshold를 0.0으로 부여해서 요소가 보이지 않아도 바로 동작할 수 있게 설정해둔다.

observer로 h1Elements 요소들을 하나하나 관찰을 시작함과 동시에 opacity를 1로 잡아줘 하나씩 보여줄 것이다.

entry.isIntersecting이 true일 때
h1Elements에 대해 animate 함수를 실행할 것이다.

animate 함수는 다음과 같다.
idx가 원본 문장의 끝에 오기 전까지
변화를 주는데 frame이 %3으로 나눠떨어질 때 랜덤한 specialChars를 보여주고
%9로 나눠떨어질 때 원본 문자 1개를 보여준다. 그리고 이때는 idx를 전진시켜 다음 문구에서 또 다시 파친코가 돌아가게 둔다.

bind는 새로운 함수를 만들어낸다. 첫번째 인자로 대상 객체를 설정하는데,
requestAnimationFrame에 바로 this.animate 함수를 넣기 싫어서 bind로 새로운 함수를 만들어 넣는다.

const h1Elements = []
const h1Array = [...document.querySelectorAll('h1')]
const specialChars = [...'!#@$%^&*()_-+={}[];<>?qwertyuiopasdfghjklzxcvbnm'.split('')]

class Header {
  constructor(id, element){
    this.id = id 
    this.idx = 0
    this.frame = 0 
    this.element = element 
    this.element.className = `${id}`
    this.originalString = element.innerText 
    this.innerHTML = ''
    this.intersecting = false 
    this.createSpans()
  }

  createSpans(){
    for(let i=0; i<this.originalString.length; i++){
      this.innerHTML += `<span>${this.originalString[i]}</span>`
    }
    this.element.innerHTML = this.innerHTML 
    this.spans = [...this.element.querySelectorAll('span')]
  }

  animate(){
    // idx가 계속 증가하다가 원본 문자열의 길이에 도달하면 종료 
    if(this.idx !== this.originalString.length && this.intersecting){
      this.spans[this.idx].style.opacity = 1
      this.spans[this.idx].style.transform = `translateX(0)`

      if(this.frame % 3 == 0 && this.spans[this.idx].innerText !== ' '){
        // 랜덤한 문자들을 반복해준다.
        this.spans[this.idx].innerText = specialChars[Math.floor(Math.random() * specialChars.length)]
      }
      if(this.frame % 15 == 0 && this.frame !== 0){
        // 원래 문자열을 돌려주고 한칸 이동한다.
        this.spans[this.idx].innerText = this.originalString[this.idx]
        this.idx++
      }
      this.frame++ 
      // bind()는 새로운 함수 반환, 
      // Function.bind(thisArg, [arg1, arg2, ...])
      // 첫번째 인자는 this가 가리킬 객체를 지정한다. 
      // 두번째 인자부터는 함수의 인자로 전달할 값들이다. 
      // 여기에 this를 삽입하면, 새로운 함수가 이 Header라는 클래스를 똑같이 가리키도록 할 수 있다.
      // 함수를 실행하지 않고 함수를 생성한 후 반환한다는 점에서 call(), apply()와 차이점을 지닌다.
      // 예를 들어 react에서 props로 함수를 전달해야 할 때, 함수와 인자를 모두 전달해야 하기 위해 불필요하게 함수를 추가하지 않아도 됨
      requestAnimationFrame(this.animate.bind(this))
    } else {
      console.log('done')
    }
  }

  reset(){
    this.idx = 0 
    this.frame = 0 
    this.intersecting = false 
    this.spans.forEach(span => {
      span.style.opacity = 0 
      span.style.transform = `translateX(-10px)`
    })
  }
}

window.addEventListener('DOMContentLoaded', () => {
  setTimeout(() => {
    h1Array.forEach((header, idx) => {
      h1Elements[idx] = new Header(idx, header)
    })

    let options = {
      // root의 기본값은 브라우저 뷰포트 
      rootMargin: '0px', // root가 가진 여백 
      threshold: 0.0 // observer의 콜백이 실행될 대상의 요소 가시성 퍼센티지. 
      // 50%만큼 요소가 보였을 때 탐지하고 싶으면 값을 0.5로 하면 됨(0.0~1.0) 

    }
    let callback = (entries) => {
      entries.forEach(entry => {
        // Each entry describes an intersection change for one observed
        // target element:
        //   entry.boundingClientRect
        //   entry.intersectionRatio
        //   entry.intersectionRect
        //   entry.isIntersecting
        //   entry.rootBounds
        //   entry.target
        //   entry.time
        if(entry.isIntersecting){
          h1Elements[+entry.target.className].intersecting = true 
          h1Elements[+entry.target.className].animate()
        } else {
          h1Elements[+entry.target.className].reset()
        }
      })
    }

    /**
     * Intersection Observer API는 
     * 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법이다.
     * new IntersectionObserver(callback, options)
     * 
     * https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API 
     */
    let observer = new IntersectionObserver(callback, options)

    h1Elements.forEach(instance => {
      observer.observe(instance.element)
      instance.element.style.opacity = 1 
    })
  })
})
profile
https://medium.com/@wooleejaan

0개의 댓글