본문 바로가기

UI 디자인 애니메이션 연구소

CSS, JS를 이용한 웅장한 N-S(자석) 애니메이션 제작

반응형

[이전 애니메이션] 3D 카드 회전 애니메이션

 

HTML,CSS 를 사용한 3D 카드 회전 애니메이션 만들기

미리보기이번에 만들어볼 애니메이션은 원의 중심축을 기준으로 카드가 회전하는 애니메이션을 3D 로 구현해 봅니다. HTML 우선 HTML 을 아래와 같이 구성해줍니다.   부연설명을 해보자면, 3D 효

duklook.tistory.com

 


미리보기

해당 과정을 따라오시면 아래와 같은 놀라운 자석 애니메이션을 구현하실 수 있는 능력을 가지게 됩니다. 이번에 자바스크립트에서 투자되는 코드가 비중이 큰 편입니다. 요소를 동적으로 생성하고, 생성된 각 요소에 각도 변환을 이용한 자석 효과를 표현하기 위한 조치가 필요하기 때문입니다. 

 

[버전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 적용 시 기준이 되는 좌표가 변경된 것을 볼 수 있습니다. 

https://developer.mozilla.org/en-US/docs/Web/CSS/transform-origin


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 )

 

다음 애니메이션

 

HTML/CSS/JS 를 이용한 스트리밍효과 토글 UI 만들기

이전 애니메이션 CSS, JS를 이용한 웅장한 N-S(자석) 애니메이션 제작[이전 애니메이션] 3D 카드 회전 애니메이션 HTML,CSS 를 사용한 3D 카드 회전 애니메이션 만들기미리보기이번에 만들어볼 애니

duklook.tistory.com

 

반응형