[Vanilla JS] 평창올림픽 드론쇼 만들기

신희강·2022년 3월 9일
146
post-thumbnail

Demo: https://heekang2271.github.io/artwork/Olympic/

지난 2월 20일, 2022 베이징 올림픽이 폐막했습니다.
저는 올림픽 시즌에 선수들이 메달을 따고 기뻐하는 모습을 보면 한껏 국뽕(?)에 취해 지난 올림픽 경기들도 찾아보고 그러는데요. 😛
이번에도 지난 올림픽 영상들을 보던 중 평창올림픽 개막식을 보게되었어요.

평창올림픽 드론쇼

출처: 동아사이언스 - 평창 하늘에 뜬 드론 1218대의 비밀...인텔은 왜?

올림픽 개막 당시엔 군대에 있어서 보지 못했는데, 개막식에서 하늘을 수놓은 드론들이 모여 오륜기를 만드는 장면은 4년이 지난 지금 봐도 감탄이 절로나왔어요.
이 장면을 라이브로 못본게 너무 아쉽고 그 때의 전율을 간접적으로나마 느껴보고 싶은 마음에 드론쇼를 재현해보자! 라는 욕심이 생겼어요.
그래서 바로 vscode를 열어 작업을 해보았습니다.

해결해야 할 문제

해결해야 할 문제는 크게 두가지였어요.

1. 오륜기를 그릴 좌표를 어떻게 딸 수 있을까?
2. 흩어져 있는 드론들이 어떻게 오륜기 모양으로 모이게 할 수 있을까?

이 문제들만 해결하면 제가 재현하려 했던 드론쇼의 구색은 갖출 수 있을 것 같았거든요.

오륜기를 그릴 좌표를 어떻게 딸 수 있을까?

처음 든 생각은 동그란 원을 만들 수 있는 좌표 그룹을 구성해놓고 5개 원을 만들어 배치를 잘 계산하면 되지 않을까? 였어요. 하지만 이 방법은 어찌됐건 동그란 원의 좌표를 수동으로 일일히 따내야 했고, 이는 전혀 문제를 해결했다고 볼 수 없다고 생각이 들었어요.

완전 Automatic한 방법으로 좌표를 따고 싶어 canvas의 메소드들 중 쓸만한 메소드가 있는지 html 레퍼런스를 살펴보다가 getImageData() 라는 메소드가 있었고 다음과 같이 소개되고 있었어요.

캔버스의 지정된 사각형에 대한 픽셀 데이터를 복사하는 ImageData 객체를 반환합니다.
ImageData 객체의 모든 픽셀에는 4가지 정보가 있습니다. (RGBA 값)

캔버스에서 픽셀 데이터를 추출할 사각형의 영역을 지정하면 해당 영역의 모든 픽셀들의 rgba 값을 얻을 수 있다고 해요. 이를 사용해서 캔버스에 오륜기 이미지를 삽입하고 rgba 값이 있는 픽셀들만 추출해 좌표를 따는 방식으로 문제를 해결 할 수 있을 것 같았어요.

흩어져 있는 드론들이 어떻게 오륜기 모양으로 모이게 할 수 있을까?

흩어져 있는 드론이야 드론을 생성하면서 x, y좌표를 랜덤하게 부여하면 쉽게 구현할 수 있을 것 같았어요.
하지만 흩어져 있는 각각의 드론들에 어떻게 오륜기에 해당하는 좌표를 부여할지가 문제였어요.
어떻게 해결할지 고민하고 있었던 찰나에 순서를 바꿔보자는 생각이 들었어요.

흩어진 드론을 모은다 ❌
모여진 드론을 흩어지게 했다가 다시 모은다 ⭕️

이미 흩어져 있는 드론들에 오륜기 좌표를 부여하는게 어렵다면, 오륜기 좌표대로 드론을 생성해 그 값을 저장해 두고 랜덤한 x, y 좌표를 생성해 흩어지도록 하자 였어요.
이렇게 각 드론들이 오륜기의 좌표, 랜덤한 좌표 두 개를 갖도록 하면 인터랙션이 일어날 때마다 출발 좌표와 도착 좌표만 바꿔주어 모이고 흩어지는 효과를 구현할 수 있을 것 같았어요.

큰 문제들의 해결 방향을 잡았으니 본격적으로 구현을 해보도록 할게요.

구현 과정

오륜기의 좌표 구하기

우선 밤하늘의 색을 보여주기 위해 css의 linear-gradient 속성을 사용해서 배경색을 설정하고,
캔버스의 drawImage() 함수를 사용해 오륜기 이미지를 로드했어요. 이 때 오륜기의 좌표는 현재 윈도우의 width와 오륜기의 width를 계산해 가운대로 오게 했어요.
이 후 위에서 설명한대로 getImageData() 함수를 사용해 픽셀 값들을 불러왔어요.

다음과 같이 모든 픽셀들의 rgba 값이 순서대로 담긴 데이터를 얻을 수 있었어요.
이제 이 값들을 사용해 픽셀별 x,y좌표와 rgb 컬러 값을 가진 픽셀 배열을 만들어 보았어요.

const particles = [];

for (let h = 0; h < this.stageHeight; h += this.density) {
	for (let w = 0; w < this.stageWidth; w += this.density) {
        const idx = (h * this.stageWidth + w) * 4;

        const r = imageData.data[idx];
        const g = imageData.data[idx + 1];
        const b = imageData.data[idx + 2];

        if (r != 0) {
          const color = `#${this.decToHex(r)}${this.decToHex(g)}${this.decToHex(
            b
          )}`;

          particles.push({
			x: w,
            y: h,
            color,
          });
		}
	}
}

이 데이터를 다음과 같이 화면의 width, height를 변수로 하는 이중 for 문을 사용해 4개씩 끊어 처리해 픽셀별로 x, y좌표와 컬러값을 얻어 저장했어요.
for문의 증감값으로 density 를 넣어주었는데, 픽셀단위가 워낙 촘촘하다보니까 드론의 크기를 고려해서 density 만큼의 간격을 주어 픽셀을 추출했어요.

드론 생성

이렇게 픽셀 정보들을 추출하면 clearRect()를 사용해 오륜기 이미지를 지워주고, 픽셀들의 정보를 이용해 노란색 원 형태의 드론을 찍어보면 다음과 같아요.

생각했던 것 보다 만들려고 하는 드론쇼의 이미지와 비슷하게 나와 굉장히 만족했어요 😊

오륜기 모양이 잘 찍히는걸 확인했으니 페이지가 로드되면 오륜기 모양으로 드론이 배치되는 것이 아닌 랜덤한 x, y 좌표를 생성해 그 위치로 드론을 배치했어요.

이렇게 자유분방(?) 하게 흩어져 있는 드론을 확인할 수 있어요.

모이는 효과

requestAnimation() 함수를 사용해 캔버스의 이미지를 지웠다가 그렸다가를 반복하고,
그 반복 안에서 좌표위치를 계속 수정하면 점들이 움직이는 효과를 줄 수 있어요.
만화영화를 만들 때, 수많은 양의 그림들을 빠르게 넘겨 움직이는 효과를 내는 것과 같은 원리에요.

프레임을 설정한 후, 드론들이 가진 랜덤좌표, 오륜기좌표의 x, y별 거리를 구하고 설정한 프레임으로 나누면 한 프레임당 이동할 거리가 구해져요.

프레임당 이동할 x 거리 = (랜덤좌표.x - 오륜기좌표.x) / 프레임
프레임당 이동할 y 거리 = (랜덤좌표.y - 오륜기좌표.y) / 프레임

현재 좌표를 따로 변수로 만들어 초기값으로 랜덤 좌표를 주고,
requestAnimation() 함수가 호출 될 때마다 위에서 계산한 거리만큼 현재 좌표에 더해주면 랜덤좌표에서 출발해 오륜기좌표로 도착할 수 있어요.

모이는 효과를 언제 줄까 고민하다가 사용자가 직접 인터랙션을 주는것이 재밌을 것 같다고 생각했어요. 그래서 마우스 왼쪽 버튼을 꾹 누르고 있으면 오륜기 모양으로 모이도록 구현했습니다.

결과는 다음과 같아요.

모이긴 모이는데 뭔가 모르게 이질감이 들죠?
아무래도 일정한 거리를 계속해서 더해주는, 등속운동의 방식이기 때문에 현실적인 드론과 다르게 느껴져 어색한 것 같아요.

그래서 프레임당 이동할 거리를 현재 좌표에 더해줄 때 가속도를 곱해서 넣어주기로 했어요.
그리고 목적지까지 이동할 때 절반 이상 이동했으면 마찰력을 곱해서 넣어주어 출발할 때는 가속이 붙었다가 도착하면서 느려지며 자리를 찾아가는 느낌을 내고자 했어요.

결과는 다음과 같습니다.

이제 좀 자연스러워 진 것 같네요. 😀

이렇게 평창올림픽 드론쇼를 재현해보았는데, 시간도 좀 남고 뭔가를 더 해보고 싶은 욕심이 있어서 좀 더 드론의 특성을 살릴 수 있는 무언가를 만들어보고 싶었어요.
지금은 그냥 하늘에 찍혀있는 점에 불과하거든요.

그래서 마우스 포인터로 드론을 건드렸을 때, 드론이 밀렸다가 다시 돌아오는 효과를 구현하고자 했어요.

드론이 밀리는 효과

드론이 밀리는 효과는 수학적 지식이 살짝 필요했어요.

우선 마우스 포인터에 반경을 주어 마우스가 움직일 때 마다 드론과의 거리를 계산했어요. 계산한 거리가 마우스 반경과 같거나 짧다면 충돌이 일어난 것으로 보고 밀리는 효과를 주었어요.

충돌이 일어났을 땐 마우스가 드론에게 향하는 방향을 구해주어야 하는데, 삼각함수 중 탄젠트의 역함수인 아크탄젠트를 사용했어요.
마우스의 x, y좌표와 드론의 x, y좌표를 Math.atan2 함수에 넣어 방향을 구하고 이 방향대로 움직이도록 드론의 좌표를 변경해주었어요.

그 결과 다음과 같이 드론이 밀리는 효과를 주었어요.

시간이 지나면 다시 원래자리로 돌아오는 효과까지 주어,
바람같은 외부 자극(마우스 포인터)에 의해 위치가 바뀌면 다시 제자리로 찾아가는 드론의 모습을 구현했어요.

마무리

지금은 드론이 단순히 색이 채워진 원의 형태지만 불빛과 같은 효과를 주고싶었어요.
그래서 캔버스의 createRadialGradient() 함수를 사용해 그라데이션을 주어 최대한 불빛과 비슷한 효과를 주었고, 드론들은 랜덤한 위치에서 제자리에 있을 때 깜빡거리는 효과를 추가했어요.

마지막으로 노란색으로만 이루어진 오륜기 보다는 원래 오륜기의 색을 주는건 어떨까 했어요.
그래서 본문의 앞 부분에 이미지에서 좌표를 추출할 때 저장해두었던 컬러값을 이용해 오륜기 모양으로 모여지면 컬러를 바꾸는걸로 적용했습니다.

최종 결과는 다음과 같아요.

이상으로 평창올림픽 드론쇼를 재현해 보았어요.

사실 좀 아이디어가 떠오르는대로 막 만들다보니 정돈도 잘 되지 않아 공개하기 부끄럽지만, 혹시라도 코드가 궁금하실 수 있어 링크 남기고 마무리 하도록 하겠습니다.😀

Demo: https://heekang2271.github.io/artwork/Olympic/
Code: https://github.com/heekang2271/artwork/tree/main/Olympic

※ 화면 크기에 따라 매우 큰 화면에서는 짤려보일 수도 있어요!
※ 디스플레이 픽셀 비율에 따라 속도가 차이날 수 있어요!

위 버그는 해결중이에요. 양해 해주시면 감사하겠습니다ㅠㅠ

profile
끄적

20개의 댓글

comment-user-thumbnail
2022년 3월 9일

오륜기 너무 이쁘네용ㅎㅎㅎㅎ
다음 글도 기대하겠씁니다!!!👍👍

1개의 답글
comment-user-thumbnail
2022년 3월 9일

너무 예뻐요🥰🥰 다음 작업도 기대할께요 :)

1개의 답글
comment-user-thumbnail
2022년 3월 10일

너무 멋져요 희강님!

1개의 답글
comment-user-thumbnail
2022년 3월 10일

금손이세요..👍

1개의 답글
comment-user-thumbnail
2022년 3월 11일

너무 잘봤습니다 👍👍

1개의 답글
comment-user-thumbnail
2022년 3월 14일

우와아.. 멋졍요!

1개의 답글
comment-user-thumbnail
2022년 3월 15일

어딘가 접근 방법이 예전에 봤던 나무그리기랑 비슷하다 생각했더니 역시 같은 분 이셨군요!
이번에도 멋진 작품 잘 보고갑니다!

1개의 답글
comment-user-thumbnail
2022년 3월 16일

정말 잘보고 갑니다..!! 대단쓰

1개의 답글
comment-user-thumbnail
2022년 5월 12일

와 좋은 작업 공유 감사드립니다! 그런데 코드 다운받아서 실행시켜보려는데 왜 점들이 안생길까요.?ㅜ

1개의 답글
comment-user-thumbnail
2022년 8월 5일

다른 차원의 질문일 수 있겠지만 현실성을 고려했을 때 드론끼리의 충돌의 위험성은 어떻게 구현할 수 있을까요?

답글 달기