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   Lee</h1>
</section>
<section>
<h1>About</h1>
</section>
<section>
<h1>Contact</h1>
</section>
<script src="app.js"></script>
</body>
</html>
* {
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;
}
대문에 박을 문구를 h1에 작성하고 이를 배열에 담는다. h1Array
그리고 해당 문구를 보여주기 전에 파친코처럼 돌아갈 문구를 specialChars
에 담는다.
Header 클래스를 생성한다. 이 클래스는 문장을 한 글자씩 분해해주는 클래스이다.
나중에 DOMContentLoaded 이벤트가 발생하면, h1Elements에 한 글자씩 담아줄 것이다.
그리고 DOMContentLoaded 이벤트가 발생하면
IntersectionObserver를 사용해서 실행해줄건데,
++Intersection Observer API는
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
})
})
})