[VanillaJS]生長したツリーを作成-2


Demo
第1編では、スクリーンの真ん中に木を描きました.
この編では、クリックすることで、木の描画活動と、下から徐々に成長する効果を実現しましょう!

クリックイベント


クリック活動は簡単です.
アプリケーションがTreeオブジェクトを自動的に作成すると、Treeのdraw()関数が自動的に実行され、クリックイベントが発生すると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セグメントに分割し、ブランチを描画し続けます.

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で枝が生成され、それらを1つの配列に配置し、その後、すべての枝に対してdraw()関数を呼び出すので、問題は枝がすべて同時に成長することである.

▼▼木が育つ効果


コードを修正して、枝を深さで挿入し、深さの枝を描き終わってから、次の深さを描きます.

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に深さ別にブランチを格納し、draw関数に深さ別にブランチを描画します.現在、深さでブランチをすべて描画します.pass == trueになると、次の深さが行われますが、描画が完了しません.pass == falseであれば、draw()関数を終了し、次の深さのブランチを描画できなくなります.
最後に、すべての樹木を描画すると、cancelAnimationFrame()が呼び出され、不要なアニメーションがメモリ漏れを繰り返すことを回避します.
このようにコードを修正した結果!

スピードが遅すぎるので、branch.jsframeを10程度に修正しましょう.

樹木の生長効果を達成した😀
好みで色を変えてもいいです.

筆者はいくつかの色を決め、ランダムに色を指定した後、深さと白を混ぜ合わせ、Interactive Developer金鍾民の作品に似た効果を生み出した.

後記


Interactive Developer金鍾民(キム・ジョンミン)の動画Googleから入社提案を受けたポートフォリオのPlant Treeを見て、想像以上に難易度が高かった.しかし、金鍾民の動画を見て学んだのか、問題に直面した時も難なく解決できた.
終わってから、コードはあまり長くありませんでしたが、6~7時間くらいしました.😂
お尻が重すぎて、最初から仕事をしていたので、そのせいか完成するとぐったりしていました.
それでも、仕事の中で生活しているうちに、久しぶりに芸術作品の仕事をしたり、頭を働かせたり、途中で成果を見たりして、仕事が面白かったです.
これからもたまにやります.😃
下一篇:[VanillaJS]生長したツリーを作成-1