[My Toy] 혼자서 즐겨보는 벚꽃 축제 - JavaScript

MandarinePunch·2022년 3월 14일
17

토이 프로젝트

목록 보기
2/6
post-thumbnail

벚나무 페이지 링크

✍ 만들게 된 계기

여느 때와 다를 것 없이 Java로 코딩테스트를 연습 중이었다.
그 후 블로그 작성을 위해 velog를 켰는데 우연히 보게 된 신희강님의 작업물들 중 하나를 보고 적지 않은 충격을 받았다.
Interactive Developer 김종민님의 영상을 참고해 만든 Tree Artwork이었다.
정말... 정말 실사 같았다. 그리고 무엇보다도 작품이 아름다웠다.
어떻게 JavaScript로 이런 작업물을 만들 수 있는건지 너무 감탄했다.
이걸 본 이상 안 만들어 볼 수는 없어 코드도 참고하고, canvas에 대해 이것저것 찾아보며 나만의 나무를 만들어봤다.


🔨 작업 코드

먼저 canvas를 html코드에 넣고 작업을 시작했다.

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

그 후, 캔버스를 js로 가져오고 캔버스의 크기를 현재 내가 보고있는 창의 크기로 맞춰줬다.

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

오랜만에 JS를 만지는 거라 뭐가 안될 때 마다 계속 console창을 들여다 봤다. ㅋㅋ (게다가 캔버스는 거의 초면 수준인지라....)
도화지는 만들어졌으니 이제 나무를 그려야 한다.
그리기 위해서는 좌표가 필요한데 그 식은 삼각함수를 이용하여 비교적 간단히 구현할 수 있었다.

const draw = (startX, startY, startDepth, angle, branchWidth) => {

  ctx.beginPath();

  let len = 100;
  const endX =
    // 빗변 길이와 cosθ을 곱해 x좌표를
    startX + Math.cos((angle / 180) * Math.PI) * len;
  const endY =
    // 빗변 길이에 sinθ를 곱해 y좌표를 구한다.
    startY + Math.sin((angle / 180) * Math.PI) * len;

  // moveTo로 위치를 잡아주고
  ctx.moveTo(startX, startY);
  // lineTo의 좌표까지 직선을 잇는다.
  ctx.lineTo(endX, endY);

  ctx.lineWidth = branchWidth;
  ctx.stroke();

};
// 첫 각도를 -90도로 맞춰야 이 친구가 화면 위로 우뚝 선다.
draw(canvas.width/2 ,canvas.height, 0, -90, 15);

짜잔! 귀여운 나무 밑둥이 만들어졌다! 이제 가지를 뻗어나가는 일만 남았다.
열심히 찾고 생각한 결과 각도를 좌우로 틀고 재귀 함수를 호출해주는 것이 가장 간단히 구현할 수 있는 방법이었다. (DFS 공부가 도움이 됐다!)

const depth = 4;

const draw = (startX, startY, startDepth, angle, branchWidth) => {
  // 뻗는 가지 갯수를 depth로 조정한다.
  if (startDepth === depth) {
  	return;
  }
  
  ctx.beginPath();

  let len = 100;
  const endX =
    startX + Math.cos((angle / 180) * Math.PI) * len;
  const endY =
    startY + Math.sin((angle / 180) * Math.PI) * len;

  ctx.moveTo(startX, startY);
  ctx.lineTo(endX, endY);

  ctx.lineWidth = branchWidth;
  ctx.stroke();
  // 재귀 함수 호출
  draw(endX, endY, startDepth + 1, angle - 30, branchWidth);
  draw(endX, endY, startDepth + 1, angle + 30, branchWidth);

};

draw(canvas.width / 2 ,canvas.height, 0, -90, 15);

제법 예쁘게 갈라진 모습이 매력적이다.
이제 갈라지는 횟수를 담당하는 depth를 늘려주고 30°로 정해놓은 각도도 랜덤하게 바꾼다.
branchWidth도 가지가 뻗어갈수록 얇아지게하면 더욱 그럴싸한 나무가 된다.

const depth = 11;

const draw = (startX, startY, startDepth, angle, branchWidth) => {
	
  ...
  // 첫 밑둥을 만들 때는 어느정도 고정길이를 주고, 그 이후에 뻗어나오는 가지는 랜덤하게 길이를 주었다.
  let len = startDepth === 0 ? random(10, 14) : random(0, 10);
  const endX =
    // 첫 밑둥을 최대 길이로 하고 가지가 늘어날수록 길이를 작아지게 하기위해 len뒤에 식을 넣어줬다.
    startX + Math.cos((angle / 180) * Math.PI) * len * (depth - startDepth);
  const endY =
    startY + Math.sin((angle / 180) * Math.PI) * len * (depth - startDepth);
  
  ...
  // 재귀를 할때마다 branchWidth를 0.8배씩 조정하여 굵기를 줄였다.
  draw(endX, endY, startDepth + 1, angle - random(15, 25), branchWidth * 0.8);
  draw(endX, endY, startDepth + 1, angle + random(15, 25), branchWidth * 0.8);

};
// Math.random() MDN을 보면 나오는 사잇값 구하는 방법에 1 이상이 나오도록 뒤에 1을 더해줬다.
const random = (min, max) => {
  return min + Math.floor(Math.random() * (max - min) + 1);
};

draw(canvas.width / 2 ,canvas.height, 0, -90, 15);

감동... 너무 예쁜 나무가 나왔다.
여기서 더 뭘해줄까 하다가, 이제 날도 따뜻해지는 것 같고 봄이 오는 느낌이 슬슬 들기에 코로나를 의식한 조금 이른 벚꽃 축제를 열어보기로 했다. ㅋㅋ

일단 나무가 많아야 하므로 나무 호출식(draw)의 x좌표를 clientX를 이용하여, 마우스 클릭시 그 곳에서 나무가 나오게 수정하였다.

const handleClick = (event) => {
  const { clientX } = event;
  draw(clientX, canvas.height, 0, -90, 15);
};

window.addEventListener("click", handleClick);

메인 주제인 벚꽃을 표현해 줘야 하므로, 처음에는 분홍색 단색만 써서 출력해봤다.
예쁘긴한데 뭔가 밋밋한 느낌이 있어, 배열을 선언하여 조금씩 다른 분홍들을 몇가지 넣고
가지가 끝에 다다랐을 때 랜덤하게 배열에서 뽑아 색을 입혀주었다.

const draw = (startX, startY, startDepth, angle, branchWidth) => {
  ...
  // 나무 굵기가 일정 수치 이하로 떨어지면
  if (branchWidth < 2) {
    // 꽃의 크기를 랜덤하게 부여하고
    branchWidth = random(3, 5);
    // 색도 랜덤하게 넣는다.
    ctx.strokeStyle = color[Math.floor(Math.random() * 4)];
  } else {
    ctx.strokeStyle = "black";
  }
  ctx.lineWidth = branchWidth;
  ctx.stroke();
  
  draw(endX, endY, startDepth + 1, angle - random(15, 25), branchWidth * 0.8);
  draw(endX, endY, startDepth + 1, angle + random(15, 25), branchWidth * 0.8);
}

마지막으로, 창이 늘고 줄때마다 나무 위치가 이상해지길래 그냥 창 사이즈가 바뀔때마다 캔버스를 지우고 바뀐 창 사이즈에 새로 나무를 만들게끔 resize 함수도 구현했다.

const handleResize = () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
};

window.addEventListener("resize", handleResize);

🎉 그 결과...

생각보다 너무 잘 나온것 같아 뿌듯하다.
여담으로 나무를 빽빽히 찍어내면 이런 장관도 볼수 있다.

(얼핏 보면 사진같다. ㅎㅎ)


📌 깨달은 점

canvas라는 태그 자체가 굉장히 생소해서 이것저것 꽤 오래 찾아본 것 같다.

  • canvas를 window사이즈로 맞추는 방법
  • moveTo로 시작 위치를 잡아야 한다는 것 <- (이거 깨닫는게 너무 오래 걸렸음)
  • lineTo로 선을 이을 지점을 잡아야 한다는 것
  • stroke로 직접 선을 그려줘야 한다는 것

stroke로 선을 그려줘야 한다는 것도 몰라서 moveTo, lineTo만 선언해 놓고 어..? 왜 선이 안나오지? 이러고 있었다. ㅋㅋ

stroke를 쓰고도 난관이 있었는데... 분명 초기 위치를 moveTo로 잘 잡아주었음에도 불구하고, 첫 밑둥을 만드는데 선이 중구난방으로 튀었었다. ㅋㅋ (moveTo(0, 0)으로 시작 좌표를 잡으면 화면 좌측 상단부터 선이 시작된다.)

재귀함수로 가지를 뻗어나가게 구현할 때도 문제가 있었는데,
코드가 뭔가 이상했는지 오른쪽 왼쪽으로 재귀를 2개를 써줬음에도 불구하고, 왼쪽 가지만 엄청 늘어났었다. (오른쪽 가지 어디갔냐고!)
결국 코드 선언 순서가 문제긴 했다.

정말 오랜만에 모르는 분야를 찾아가며 구현한 것 같다.
생소한 만큼 정보를 찾느라 힘들었지만, 이 프로젝트 전후로 검색 실력이 달라진건 확실하다. ㅋㅋ
이렇게 느낌이 오는 날 종종 다른 것도 만들어야겠다. (JS..최고야...)
너무 재밌었던 나만의 벚꽃 축제였다!

profile
개발을 좋아하는 귤나라 사람입니다.

6개의 댓글

comment-user-thumbnail
2022년 3월 15일

언급해주셔서 감사합니다!
제 포스팅이 계기가 되셨다니 정말 뿌듯하고 감사하네요 😁

저는 나뭇잎을 넣게되면 어떻게 넣을까 하고 막연하게 생각만 했었는데,
다양한 색으로 벚꽃을 표현하신 아이디어에 감탄했습니다ㅎㅎ

좋은 작업물 보여주셔서 감사해요!

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

마지막 장관도 너무 멋지네요! 좋은 포스팅 잘 봤습니다 :)

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

잘봤습니다 너무예쁘네요!

1개의 답글