[이전 애니메이션] 3D 카드 회전 애니메이션
미리보기
해당 과정을 따라오시면 아래와 같은 놀라운 자석 애니메이션을 구현하실 수 있는 능력을 가지게 됩니다. 이번에 자바스크립트에서 투자되는 코드가 비중이 큰 편입니다. 요소를 동적으로 생성하고, 생성된 각 요소에 각도 변환을 이용한 자석 효과를 표현하기 위한 조치가 필요하기 때문입니다.
[버전1] gap 과 자석의 수를 적당히
[버전2] 빼곡한 버전, 웅장함을 느낄 수 있음
HTML
이번에는 HTML 마크업 구조를 설정하지 않습니다. 이 부분은 자바스크립트에서 동적으로 요소를 생성하여 추가할 예정입니다.
CSS
우선 CSS 전체 코드 입니다. 각 속성에 대해서는 하단에서 이어 설명합니다.
* {
margin:0;
padding:0;
box-sizing:border-box;
}
body {
overflow:hidden;
width:100%;
height:100vh;
background:#333;
gap:40px;
display:flex;
justify-content:center;
align-items:center;
flex-wrap:wrap;
}
.magnet {
width:50px;
height:3px;
background:white;
box-shadow:inset -10px 0 0 0 blue, inset 10px 0 0 0 red;
border-radius:5px;
transform-origin:90px;
}
body
body {
overflow:hidden;
width:100%;
height:100vh;
background:#333;
gap:40px;
display:flex;
justify-content:center;
align-items:center;
flex-wrap:wrap;
}
body 는 현재 애니메이션의 배경이 되는 영역입니다. 따라서 뷰포트의 전체 사이즈를 차지하도록 width와 height를 각각 100%, 100vh 로 지정합니다. 또한 자석 역할을 하는 .magnet 요소가 동적으로 생성될 때 body 태그 바깥으로 벗어나는 요소는 보이지 않도록하여 보기 싫은 스크롤바가 생기는 것을 방지하기 위해 overflow:hidden 을 지정해줍니다.
또한 .magnet 요소가 브라우저 내에 균일한 간격으로 수직-수평이 정렬되면서 생성이 되도록 display:flex , align-items:center, justify-content:center; 를 지정해줍니다.
gap의 경우 40px 를 지정하여 .magnet 요소 사이에 40px 간격으로 배치가 되도록 해줍니다. 또한, body 태그를 벗어나려는 요소는 자연스럽게 행 전환이 될 수 있도록 flex-wrap:wrap 을 설정해줍니다.
magnet | 자석
다음은 이 친구를 만들어주는 코드입니다.
.magnet {
width:50px;
height:3px;
background:white;
box-shadow:inset -10px 0 0 0 blue, inset 10px 0 0 0 red;
border-radius:5px;
transform-origin:90px;
}
앞서 이미지와 같이 납작하면서도 긴 막대 형태를 만들어주기 위해 width와 height 를 각각 50px, 3px 를 설정해줍니다.
막대의 하얀색을 표현하기 위해 background를 white로 지정해 주었습니다.
N극과 S극을 표현하기 위해 box-shadow 를 사용하여 색상을 표현해줍니다. 참고로 inset 를 사용하면 해당 요소의 안쪽으로 그림자를 표현할 수 있습니다.
border-radius 를 5px 로 지정하여 약간의 둥근 모양을 만들어주고, transform-origin을 90px 지정해 줍니다. 아래 예시 자료를 보시면, 90px 로 지정하니, 요소의 중앙에서 약간 오른쪽 지점으로 transform 적용 시 기준이 되는 좌표가 변경된 것을 볼 수 있습니다.
JS
const container = document.body;
const totalElement = new Array(300).fill('div')
// 커스텀 map 함수
const map = (fn, iter)=> {
const result = []
for (const n of iter) {
result.push(fn(n))
}
return result
}
// 요소 생성 함수
function createElement(elName){
const element = document.createElement(elName)
element.classList.add('magnet')
return element
}
const magnets = map(createElement,totalElement)
// 자석 렌더링
function render(container,magnets){
for(const b of magnets) {
container.append(b)
}
}
render(container,magnets)
document.addEventListener('mousemove',(e)=>{
const {clientX, clientY} = e;
magnets.forEach((magnet,i)=> {
const magnetRect = magnet.getBoundingClientRect();
const magnetCenterX = magnetRect.left + magnetRect.width/2; // 각 박스 X축 중앙
const magnetCenterY = magnetRect.top + magnetRect.height/2; // 각 박스Y축 중앙
const deltaX = clientX - magnetCenterX; // (0,0) 지점에서 각 박스의 X축 중앙까지의 거리
const deltaY = clientY - magnetCenterY; // (0,0) 지점에서 각 박스의 Y축 중앙까지의 거리
const angle = Math.atan2(deltaY, deltaX) * (180/Math.PI)
magnet.style.transform= `rotate(${angle}deg)`
})
})
document.addEventListener('mouseleave',()=>{
magnets.forEach(magnet=> {
magnet.style.transform = `rotate(0)`
})
})
컨테이너 요소와 생성할 자석 요소 설정
우선 생성할 자석들을 추가할 컨테이너를 선언하고, 생성할 총 자석의 수를 배열 형태로 할당하는 totalElement 변수를 초기화 해줍니다.
const container = document.body;
const totalElement = new Array(300).fill('div')
커스텀 map 고차함수 정의
이번에는 map 고차함수를 커스텀하여 만든 map 함수를 정의해줍니다. 해당 함수를 커스텀하여 만드는 이유는 NodeList 등과 같은 유사배열이라 하더라도 순회가능한 형태로 만들기 위해서 입니다.
// 커스텀 map 함수
const map = (fn, iter)=> {
const result = []
for (const n of iter) {
result.push(fn(n))
}
return result
}
해당함수의 첫 번째 매개변수는 콜백함수를 받고, 두번째 매개변수는 순회 가능한 이터레이터 객체를 뜻하는 iter 를 받습니다. 쉽게 말해 배열이나 객체 등을 받는다고 보면 됩니다.
즉, 이 함수는 함수와 배열을 받아서 for ~ of 문으로 배열을 순회하고, 각 요소에 조작을 콜백함수인 fn 내 에서 처리합니다. 그리고 fn 함수는 내부적으로 처리한 결과를 return 하고, 그 결과를 result 배열에 push 메소드를 사용하여 추가해줍니다.
마지막으로 생성된 result 를 return 할 수 있도록 코드를 작성해줍니다.
map 함수에 전달할 요소 생성 함수와 map 함수 호출
이번에는 앞서 만든 map 함수의 첫 번째 매개변수로 전달할 요소를 생성하는 createElement 함수를 선언해줍니다.
// 요소 생성 함수
function createElement(elName){
const element = document.createElement(elName)
element.classList.add('magnet')
return element
}
const magnets = map(createElement,totalElement)
해당 함수는 내부적으로 매개변수로 전달받은 element 문자열을 document.createElement의 인자로 전달하여 실제 element로 생성하고, class 속성을 추가한 후 return 으로 반환하는 기능을 수행합니다.
그 후 map(createElement, totalElement) 를 호출하게 되면, totalElement 로 전달된 요소의 수 만큼 createElement 가 호출되고 그 결과가 배열에 담겨서 magents 변수에 담기게 됩니다.
앞서 생성했던 map 함수를 보면 앞서 const totalElement = new Array(300).fill('div') 로 생성한 300개의 div 문자열이 담긴 배열을 map의 인자 -> 매개변수 로 전달하게 되고, 이를 for ~ of 로 순회하고 있습니다. 그리고 이렇게 생성된 300개의 요소는 return 으로 반환하고, 이를 할당 받은 magnets 변수를 재사용하는 것 입니다.
// 커스텀 map 함수
const map = (fn, iter)=> {
const result = []
for (const n of iter) {
result.push(fn(n))
}
return result
}
생성한 요소 브라우저에 그리기
앞서 생성한 요소들을 브라우저에 그려주기 위해 render 함수를 생성해줍니다. 매개변수로 container, magnets 을 받고, 이를 for ~ of 로 순회하여 container 요소의 자식요소로 추가해줍니다.
// 자석 렌더링
function render(container,magnets){
for(const b of magnets) {
container.append(b)
}
}
render(container,magnets)
위에서 for ~ of 로 각각 순회를 돌고 있는데, 사실 이렇게 할 필요는 없습니다. 바로 ... 전개 연산자를 사용하여 순회하지 않고도 바로 추가할 수 있습니다. 그 이유는 .append 메소드가 추가할 자식 요소의 갯수에 제한을 두지 않기 때문 입니다.
예를 들어, [1,2,3,4,5] 를 ...[1,2,3,4,5] 를 한다면 1,2,3,4,5 형태가 되기 때문애 append(1,2,3,4,5) 와 동일하게 동작합니다.
따라서 위와 같이 말고 아래와 같이도 작성할 수 있습니다.
// 박스 렌더링
function render(container,magnets){
// for(const b of magnets) {
// container.append(b)
// }
container.append(...magnets)
}
mousemove 이벤트 등록 및 각도 계산
이제 mousemove 이벤트를 등록하고, 콜백함수 내에 각 magnet 요소가 마우스 포인터가 움직이는 방향과 각도로 바라보는 작업을 처리해줍니다.
document.addEventListener('mousemove',(e)=>{
const {clientX, clientY} = e;
magnets.forEach((magnet,i)=> {
const magnetRect = magnet.getBoundingClientRect();
const magnetCenterX = magnetRect.left + magnetRect.width/2; // 각 박스 X축 중앙
const magnetCenterY = magnetRect.top + magnetRect.height/2; // 각 박스Y축 중앙
const deltaX = clientX - magnetCenterX; // (0,0) 지점에서 각 박스의 X축 중앙까지의 거리
const deltaY = clientY - magnetCenterY; // (0,0) 지점에서 각 박스의 Y축 중앙까지의 거리
const angle = Math.atan2(deltaY, deltaX) * (180/Math.PI)
magnet.style.transform= `rotate(${angle}deg)`
})
})
document.addEventListener('mouseleave',()=>{
magnets.forEach(magnet=> {
magnet.style.transform = `rotate(0)`
})
})
clientX, clientY
우선 document 객체에 이벤트 리스너를 등록하고, 콜백함수의 매개변수로 전달받은 이벤트 객체로부터 해당 document 객체의 전체 넓이에 대한 (0,0) 좌표 정보를 나타내는 clientX 와 clientY 를 가져와 줍니다.
document.addEventListener('mousemove',(e)=>{
const {clientX, clientY} = e;
// ...
}
document의 (0,0) 좌표(->좌측 최상단 꼭짓점)에서 자석 요소의 중심좌표 까지 거리 계산하기
그 다음에는 magents 배열을 순회하여 각 자석(magent) 요소의 중심좌표(magentCenterX, magentCenterY)가 document 요소 위에서 마우스포인터가 움직일 때 마다 측정되는 상대적인 (clientX,clientY) 좌표로 부터 얼마나 떨어져 있는지를 계산하는 deltaX 와 deltaY 를 구해줍니다.
const magnetRect = magnet.getBoundingClientRect();
const magnetCenterX = magnetRect.left + magnetRect.width/2; // 각 박스 X축 중앙
const magnetCenterY = magnetRect.top + magnetRect.height/2; // 각 박스Y축 중앙
const deltaX = clientX - magnetCenterX; // (0,0) 지점에서 각 박스의 X축 중앙까지의 거리
const deltaY = clientY - magnetCenterY; // (0,0) 지점에서 각 박스의 Y축 중앙까지의 거리
각도 계산하기
마지막으로 앞서 구한 값들을 이용해서 마우스 포인터가 위치한 상대적인 좌표가 현재 브라우저의 중심좌표(0,0) 간의 절대 각도를 계산해줍니다. 그리고 해당 각도를 magnet 요소의 transform:rotate() 에 전달해주기만 하면 끝입니다.
const angle = Math.atan2(deltaY, deltaX) * (180/Math.PI)
magnet.style.transform= `rotate(${angle}deg)`
여기서 조금만 더 부연설명을 하자면, 앞서 구한 deltaY 와 deltaX 는 크기가 동적으로 변하는 상대적인 좌표라고 한다면,
각 각의 자석은 절대적인 좌표(중심좌표)가 됩니다. 즉, 각 자석 마다 고유한 (magentCenterX,magentCenterY) 가 되는 지점이 고정된 중심좌표 즉 절대좌표가 됩니다. 다시 말해, 마우스 포인트가 움직일 때 마다 변동되는 상대적인 위치인 (deltaX, deltaY) 와 (magentCenterX,magentCenterY) 간에 떨어진 거리에서 역시계 방향으로 아래와 같이 호를 그릴 때의 각도를 구하는 것이 atan2() 정적 메소드입니다.
여기서 사용된 atan2 정적 메소드의 사용 사례는 MDN(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2) 에서 라이브 코드로 바로 확인할 수 있습니다.
깊게 들어간다면, 삼각함수에 대한 부분 까지 상세하게 설명해야 할 만큼 요구되는 수학적 이해가 필요하기 때문에, 해당 메소드를 사용하면 중심좌표와 상대적인 좌표 간에 절대 각도를 구할 수 있으며, 해당 각도는 각 요소가 움직이는 마우스를 바라보는 각도가 된다 라고 이해하고 넘어가면 좋을 것 같습니다.
저도 상세하게는 모르기 때문에, 원리를 이해하고, 향후 재사용하거나 응용할 때 까먹지 않고 쓸 수 있을 정도만 공부해두는 편입니다. 이상 글을 줄이도록 하겠습니다.
구현결과
See the Pen 자석 by youngwan2 (@youngwan2) on CodePen.
참고자료
- 어찌보면 젤 핵심이 되는 자료( https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2 )
다음 애니메이션
'UI 디자인 애니메이션 연구소' 카테고리의 다른 글
HTML, CSS 로 불타는 입력창(Input) 만들기 (0) | 2024.06.20 |
---|---|
HTML/CSS/JS 를 이용한 스트리밍효과 토글 UI 만들기 (0) | 2024.06.16 |
HTML,CSS 를 사용한 3D 카드 회전 애니메이션 만들기 (0) | 2024.06.01 |
HTML,CSS,JS 로 간단한 무한 슬라이드 만들기 3탄 (2) | 2024.05.31 |
HTML+CSS+JS로 초간단 무한 슬라이드 만들기 [1탄] (0) | 2024.05.28 |