[Vanilla JS] 자라나는 나무 만들기 - 2

신희강·2022년 2월 15일
69
post-thumbnail

Demo

1편에서 화면의 가운데에 나무를 그려주는 작업을 했다.
이번 편에선 클릭시 나무가 그려지는 이벤트아래에서부터 서서히 자라나는 효과를 구현해보자!

✏️ 클릭 이벤트

클릭 이벤트는 간단하다.
지금은 App에서 Tree 객체 하나가 자동으로 생성되면 Tree의 draw() 함수가 자동으로 실행되어 나무가 그려지게끔 동작하는데, 클릭 이벤트가 발생했을 때 App에서 Tree 객체가 생성되게 하면 된다.

App.js

import { Tree } from './tree.js';

class App {
  constructor() {
    this.canvas = document.createElement('canvas');
    document.body.appendChild(this.canvas);

    this.ctx = this.canvas.getContext('2d');
    this.pixelRatio = window.devicePixelRatio > 1 ? 2 : 1;

    // click이벤트 추가
    window.addEventListener('resize', this.resize.bind(this), false);
    window.addEventListener('click', this.click.bind(this), false);
    this.resize();
  }

  resize() {
    this.stageWidth = document.body.clientWidth;
    this.stageHeight = document.body.clientHeight;

    this.canvas.width = this.stageWidth * this.pixelRatio;
    this.canvas.height = this.stageHeight * this.pixelRatio;
    this.ctx.scale(this.pixelRatio, this.pixelRatio);

    this.ctx.clearRect(0, 0, this.stageWidth, this.stageHeight);
  }

  // click 함수 추가
  click(event) {
    const { clientX } = event;
    new Tree(this.ctx, clientX, this.stageHeight);
  }
}

window.onload = () => {
  new App();
};

App에 click() 함수를 구현하고 마우스의 x좌표와 화면의 가장 밑 좌표인 this.stageHeight를 사용해 Tree 객체를 생성한다.

화면을 클릭하면 클릭한 위치에 나무가 자라는 것을 볼 수 있다. 😀

클릭시 나무가 자라는데, 이번엔 밑에서부터 서서히 자라나는 작업을 해보자.

✏️ 가지가 자라나는 효과

가지가 자라나는 효과를 주려면 어떻게 해야할까?

그림처럼 구간을 나누고 requestAnimationFrame() 함수를 사용해 draw()를 호출해서 Gap에 해당하는 길이만큼 계속 그려준다면 자라나는 효과를 나타낼 수 있을 것 같다.

branch.js

export class Branch {
  constructor(startX, startY, endX, endY, lineWidth) {
    this.startX = startX;
    this.startY = startY;
    this.endX = endX;
    this.endY = endY;
    this.color = '#000000';
    this.lineWidth = lineWidth;

    this.frame = 100; // 가지를 100등분으로 나누기 위한 변수 frame 선언
    this.cntFrame = 0; // 현재 frame
    
    // 가지의 길이를 frame으로 나누어 구간별 길이를 구함
    this.gapX = (this.endX - this.startX) / this.frame;
    this.gapY = (this.endY - this.startY) / this.frame;

    // 구간별 가지가 그려질 때 끝 좌표
    this.currentX = this.startX;
    this.currentY = this.startY;
  }

  draw(ctx) {
    // 현재 frame인 cntFrame이 설정한 frame과 같다면 draw를 하지 않는다.
    if (this.cntFrame === this.frame) return;

    ctx.beginPath();

    // 구간별 길이를 더해주어 다음 구간의 끝 좌표를 구함
    this.currentX += this.gapX; 
    this.currentY += this.gapY;

    ctx.moveTo(this.startX, this.startY); 
    ctx.lineTo(this.currentX, this.currentY); // 끝 좌표를 currentX,Y로 

    if (this.lineWidth < 3) {
      ctx.lineWidth = 0.5;
    } else if (this.lineWidth < 7) {
      ctx.lineWidth = this.lineWidth * 0.7;
    } else if (this.lineWidth < 10) {
      ctx.lineWidth = this.lineWidth * 0.9;
    } else {
      ctx.lineWidth = this.lineWidth;
    }
    
    ctx.fillStyle = this.color;
    ctx.strokeStyle = this.color;

    ctx.stroke();
    ctx.closePath();

    this.cntFrame++; // 현재 프레임수 증가
  }
}

다음과 같이 100개의 구간으로 나누어 branch가 계속 그려지게 한다.

tree.js

import { Branch } from './branch.js';

export class Tree {
  ...
  
  draw() {
    for (let i = 0; i < this.branches.length; i++) {
      this.branches[i].draw(this.ctx);
    }

    requestAnimationFrame(this.draw.bind(this));
  }

  ...
}

다음으로 tree.js에서 draw() 함수 밑에 requestAnimationFrame() 함수를 사용해 draw()를 재귀호출 해보면 가지가 자라나는 효과를 구현 할 수 있다.

오잉?! 가지가 자라긴 자라는데 나무가 자란다는 느낌이 아니다 🥲
생각해보니 tree.js에서 가지들을 생성하고 하나의 배열 안에 다 집어넣은 후, 모든 가지들에 대해 draw() 함수를 호출하다보니 가지들이 전부 동시에 자라는게 문제인 것 같다.

✏️ 나무가 자라나는 효과

가지를 depth별로 집어넣고 한 depth의 가지들이 끝까지 그려진 후에 다음 depth를 그리도록 코드를 수정해야겠다.

tree.js

import { Branch } from './branch.js';

export class Tree {
  constructor(ctx, posX, posY) {
    this.ctx = ctx;
    this.posX = posX;
    this.posY = posY;
    this.branches = [];
    this.depth = 11;

    this.cntDepth = 0; // depth별로 그리기 위해 현재 depth 변수 선언
    this.animation = null; // 현재 동작하는 애니메이션

    this.init();
  }

  init() {
    // depth별로 가지를 저장하기 위해 branches에 depth만큼 빈배열 추가
    for (let i = 0; i < this.depth; i++) {
      this.branches.push([]);
    }

    this.createBranch(this.posX, this.posY, -90, 0);
    this.draw();
  }

  createBranch(startX, startY, angle, depth) {
    if (depth === this.depth) return;

    const len = depth === 0 ? this.random(10, 13) : this.random(0, 11);

    const endX = startX + this.cos(angle) * len * (this.depth - depth);
    const endY = startY + this.sin(angle) * len * (this.depth - depth);

    // depth에 해당하는 위치의 배열에 가지를 추가
    this.branches[depth].push(
      new Branch(startX, startY, endX, endY, this.depth - depth)
    );

    this.createBranch(endX, endY, angle - this.random(15, 23), depth + 1);
    this.createBranch(endX, endY, angle + this.random(15, 23), depth + 1);
  }

  draw() {
    // 다 그렸으면 requestAnimationFrame을 중단해 메모리 누수가 없게 함.
    if (this.cntDepth === this.depth) {
      cancelAnimationFrame(this.animation);
    }

    // depth별로 가지를 그리기
    for (let i = this.cntDepth; i < this.branches.length; i++) {
      let pass = true;

      for (let j = 0; j < this.branches[i].length; j++) {
        pass = this.branches[i][j].draw(this.ctx);
      }

      if (!pass) break;
      this.cntDepth++;
    }

    this.animation = requestAnimationFrame(this.draw.bind(this));
  }
}

branch.js

export class Branch {
  ...
  
  draw(ctx) {
    // 가지를 다 그리면 true 리턴
    if (this.cntFrame === this.frame) return true;

    ctx.beginPath();

    this.currentX += this.gapX;
    this.currentY += this.gapY;

    ctx.moveTo(this.startX, this.startY);
    ctx.lineTo(this.currentX, this.currentY);

    ctx.lineWidth = this.lineWidth;
    ctx.fillStyle = this.color;
    ctx.strokeStyle = this.color;

    ctx.stroke();
    ctx.closePath();

    this.cntFrame++;

    // 다 안그렸으면 false를 리턴
    return false;
  }
}

branch.jsdraw() 함수에서 가지가 다 그려지면 true, 그렇지 않으면 false를 반환한다.

tree.js에서 depth별로 가지를 저장해두고 draw 함수에서 depth별 가지를 그리는데, 현재 depth에서 가지들이 전부 다 그려져 pass == true가 되면 다음 depth로 for문이 진행되지만, 다 그려지지 않아 pass == false인 경우 draw() 함수를 종료해 다음 depth의 가지들이 그려지지 않게 했다.

마지막으로 나무가 전부 그려지면 cancelAnimationFrame()을 호출해 불필요한 애니메이션의 반복으로 메모리누수가 일어나지 않도록 한다.

이렇게 코드를 수정한 결과!

속도가 너무 느리니 branch.jsframe을 10정도로 수정해보자

나무가 자라는 효과를 완성했다 😀

취향에 따라 나무 색도 한번 바꿔보는 것도 좋을 것 같다.

필자는 색 몇개를 정해놓고 랜덤으로 색이 지정되게 한 후, depth별로 흰 색과 섞어 Interactive Developer 김종민 님의 작품과 비슷한 효과를 내고자 했다.

✏️ 후기

Interactive Developer 김종민 님의 영상 구글에서 입사 제의 받은 포트폴리오에 나오는 Plant tree라는 작품을 보고 따라 만들어보았는데, 생각보다 꽤 난이도가 있었다. 그래도 김종민님의 영상들을 보고 배운 덕분인지, 문제에 직면할 때 마다 큰 난관 없이 해결 할 수 있었다.

다 만들고나니 코드는 그리 길지 않았지만 대략 6~7시간정도 만든거같다 😂
엉덩이가 무거운 탓에 한번 작업을 시작하면 쉬지않고 하는 경향이 있는데, 그 탓인지 완성하자마자 진이 빠져버렸다..
그래도 일에 치여 살다가 오랜만에 아트웍 작업을 하며 머리도 굴려보고, 중간중간에 결과물도 보며 재미있게 작업했던 것 같다.

앞으로도 가끔씩 만들어야지 😃

이전편: [Vanilla JS] 자라나는 나무 만들기 - 1

profile
끄적

17개의 댓글

comment-user-thumbnail
2022년 2월 15일

좋은 글 너무 잘봤습니당~~👍👍👍

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

잎이 생기면 더 멋질거같네요 ! 잘봤습니다

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

너무 아름다운 트리 감사합니다!

1개의 답글
comment-user-thumbnail
알 수 없음
2022년 2월 21일
수정삭제

삭제된 댓글입니다.

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

너무 멋있네요. 좋은 글 공유 감사합니다.

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

덕분에 프론트엔드 입문했습니다. 감사합니다^^

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

만들어 보고 싶다는 동기가 생길 만큼 정말 멋진 결과물이네요!
저도 글 보면서 한 번 시도해봐야겠어요. 좋은 글 써주셔서 진심으로 감사 드립니다🙏🏻

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

신기해용...

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

The person who made the work is so wonderful. The growth rate of the tree is also very smooth. Perfect. drift f1

답글 달기
comment-user-thumbnail
2022년 10월 12일

great entertainment klondike solitaire

답글 달기