[Vanilla JS] 자라나는 나무 만들기 - 2
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.js
의 draw()
함수에서 가지가 다 그려지면 true
, 그렇지 않으면 false
를 반환한다.
tree.js
에서 depth별로 가지를 저장해두고 draw
함수에서 depth별 가지를 그리는데, 현재 depth에서 가지들이 전부 다 그려져 pass == true
가 되면 다음 depth로 for문이 진행되지만, 다 그려지지 않아 pass == false
인 경우 draw()
함수를 종료해 다음 depth의 가지들이 그려지지 않게 했다.
마지막으로 나무가 전부 그려지면 cancelAnimationFrame()
을 호출해 불필요한 애니메이션의 반복으로 메모리누수가 일어나지 않도록 한다.
이렇게 코드를 수정한 결과!
속도가 너무 느리니 branch.js
의 frame
을 10정도로 수정해보자
나무가 자라는 효과를 완성했다 😀
취향에 따라 나무 색도 한번 바꿔보는 것도 좋을 것 같다.
필자는 색 몇개를 정해놓고 랜덤으로 색이 지정되게 한 후, depth별로 흰 색과 섞어 Interactive Developer 김종민 님의 작품과 비슷한 효과를 내고자 했다.
✏️ 후기
Interactive Developer 김종민 님의 영상 구글에서 입사 제의 받은 포트폴리오에 나오는 Plant tree라는 작품을 보고 따라 만들어보았는데, 생각보다 꽤 난이도가 있었다. 그래도 김종민님의 영상들을 보고 배운 덕분인지, 문제에 직면할 때 마다 큰 난관 없이 해결 할 수 있었다.
다 만들고나니 코드는 그리 길지 않았지만 대략 6~7시간정도 만든거같다 😂
엉덩이가 무거운 탓에 한번 작업을 시작하면 쉬지않고 하는 경향이 있는데, 그 탓인지 완성하자마자 진이 빠져버렸다..
그래도 일에 치여 살다가 오랜만에 아트웍 작업을 하며 머리도 굴려보고, 중간중간에 결과물도 보며 재미있게 작업했던 것 같다.
앞으로도 가끔씩 만들어야지 😃
Author And Source
이 문제에 관하여([Vanilla JS] 자라나는 나무 만들기 - 2), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@heekang/Vanilla-JS-자라나는-나무-만들기-2저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)