ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [ JS / Canvas ] 비 내리는 효과 - rain effect
    Javascript/실습 2023. 2. 19. 17:53
    반응형

     

      이번에 만든 것은 canvas를 통해 구현한 비 내리는 효과입니다. 클래스를 통해 구현했으며, 총 두 가지 느낌을 표현해봤습니다. 인스턴스 생성 시 총 세 가지의 인자(효과를 입힐 대상 요소, 빗줄기 수, 빗줄기의 속도)를 받도록 했으며 이 중 빗줄기 수와 빗줄기의 속도는 디폴트값을 주어 필요시 조절하도록 했습니다.

     

    See the Pen rainEffect by OnnJE (@ogtitle) on CodePen.

     

     

     

    1. Constructor

    constructor(target, intensity = 5, speed = 5) {
        //add canvas that same size as target element
        this.target = target;
        this.targetStyle = getComputedStyle(this.target);
        this.width = this.targetStyle.getPropertyValue("width").slice(0, -2);
        this.height = this.targetStyle.getPropertyValue("height").slice(0, -2);
        this.canvasID = `canvas_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
        this.target.innerHTML = `<canvas id = "${this.canvasID}" width = "${this.width}px" height = "${this.width}px"></canvas>`;
        this.canvas = document.getElementById(this.canvasID);
        this.ctx = this.canvas.getContext("2d");
    
        //set canvas properties
        this.target.style.position = "relative";
        this.canvas.style.position = "absolute";
    
        //variable initialization
        this.intensity = intensity;
        this.speed = speed;
        this.cx = new Array(this.intensity)
          .fill()
          .map(() => this.width * Math.random());
        this.cy = new Array(this.intensity)
          .fill()
          .map(() => this.height * Math.random());
        this.l = new Array(this.intensity)
          .fill()
          .map(() => this.height / 20 + (this.height / 30) * Math.random());
        this.v = new Array(this.intensity)
          .fill()
          .map(() => this.speed + this.speed * Math.random());
      }

     

      생성자는 총 두가지 부분으로 나누어집니다. 먼저 효과를 입힐 대상 요소의 자식요소로 canvas 태그를 추가합니다. 이때 canvas 태그의 크기는 대상요소의 동일하게 합니다. 또한 각 캔버스가 생성될 때 각각 다른  id를 적용할 수 있도록 date 클래스를 사용하였고 추가적으로 0~1000 사이의 랜덤값을 부여했습니다. 캔버스 태그의 생성 및 적용은 innerHTML 메서드를 이용했습니다.(createElement를 사용하는 방법도 고려해 보았지만 createElement 사용 시 실제 canvas크기와 draw 효과가 적용되는 범위가 달라지는 문제점이 있었습니다.) 또한 캔버스가 대상 요소의 다른 자식요소들과 간섭하지 않도록 position을 absolute로 적용하였고, 이의 올바른 적용을 위해 부모요소(대상요소) 또한 position : relative로 설정해 주었습니다.

      다음으로 기타 빗줄기를 그리는 데 사용할 변수를 정의합니다. 인자로 받은 빗줄기 수를 저장할 intensity를 초기화하며, Array클래스와 앞서 초기화한 intensity를 통해 빗줄기의 시작점에 대한 정보를 담을 cx, cy와 길이와 속도에 대한 정보를 담을 l, v를 동적으로 초기화합니다. 이때 각 빗줄기에 대한 시작점과 길이 속도 등은 Math 클래스의 메서드 random을 통해 일정 범위 안에서 랜덤값을 가지도록 했습니다.

     

     

    2. Method

    1) draw

    draw(color = "#999999") {
        this.ctx.strokeStyle = color;
    
        if (!this.cx.length) {
          this.ctx.beginPath();
          this.ctx.moveTo(this.cx, this.cy);
          this.ctx.lineTo(this.cx, this.cy + this.height * 0.1);
          this.ctx.stroke();
          return;
        }
    
        for (let i = 0; i < this.intensity; i++) {
          this.ctx.beginPath();
          this.ctx.moveTo(this.cx[i], this.cy[i]);
          this.ctx.lineTo(this.cx[i], this.cy[i] + this.l[i]);
          this.ctx.stroke();
        }
        return;
      }

     

      draw 메서드는 fallC, fallD에 사용되는 메서드입니다. 어느 메서드에서 호출되었는지에 따라 cx, cy는 다른 동작을 해야했습니다.(fallD 에서 cx, cy의 값이 단일숫자로 랜덤 하게 바뀌게 됩니다.) 따라서, cx.length를 조건으로 두어(fallD의 경우 cx를 숫자형으로 바꾸기 때문에 cx.length는 undefined로 falsy로 취급됩니다.) fallD에서 호출되었을 경우 통과하도록 하였고 그 외의 경우엔 조건문 내부 함수를 실행하지 않도록 했습니다.

     

     

    2) fallC

    fallC() {
        if (!this.cx.length) {
          this.cx = new Array(this.intensity)
            .fill()
            .map(() => this.width * Math.random());
          this.cy = new Array(this.intensity)
            .fill()
            .map(() => this.height * Math.random());
        }
    
        this.ctx.clearRect(0, 0, this.width, this.height);
        this.draw();
    
        this.cy = this.cy.map((ele, idx) => {
          ele += this.v[idx];
    
          if (ele >= this.height) {
            this.cx[idx] = this.width * Math.random();
            return 0;
          }
    
          return ele;
        });
    
        requestAnimationFrame(this.fallC.bind(this));
      }

     

      fallC는 연속적으로 이어지는 빗줄기를 표현합니다. 먼저 if문을 통해 cx의 상태를 확인하고 fallD에서 사용되 단일 숫자형으로 바뀌었다면 다시 랜덤값이 담긴  intensity에 맞는 길이의 배열로 바꾸어줍니다. cx값을 확인한 후에는 clearRect를 통해 canvas를 깨끗하게 지웁니다. 그 이후 현재 cx, cy, l을 바탕으로 다시 빗줄기를 그린 후, cy값을 업데이트합니다.(만약 이러한 함수를 연속적으로 호출해 canvas상에서 빗줄기가 y축을 따라 연속적으로 움직이는 것처럼 보이게 합니다. 함수의 호출은 RAF을 통해 이루어지며 이때 인자로 this.fallC가 아닌 this.fallC.bind(this)를 주는 것은 RAF를 통해 호출이 반복되는 메서드 fallC가 현재 Rain 클래스의 인스턴스를 지속적으로 참조하도록 해야 하기 때문입니다. 만약 bind(this)를 사용하지 않을 시 호출된 fallC는 RAF의 기본값으로 세팅된 global object에 bind 될 것입니다.

     

     

    3) fallD

    fallD() {
        this.ctx.clearRect(0, 0, this.width, this.height);
        this.draw();
        this.cx = this.width * Math.random();
        this.cy = this.height * Math.random();
    
        requestAnimationFrame(this.fallD.bind(this));
      }

      fallD의 동작은 더욱 간단합니다. fallD에서는 fallC와 동일하게 draw() 메서드를 통해 canvas상에 빗줄기를 그리고 cx, cy를 RAF를 통해 지속적으로 업데이트하지만 cx를 유지하지도, cy를 일정거리로 업데이트해 움직임이 이어지도록 하지도 않습니다. 또한 cx, cy를 단일한 숫자형으로 빠꾸어 한번 그려질 때 단 하나의 빗줄기만 그려지도록 합니다. 즉, 단 하나의 빗줄기가 랜덤 한 위치에 생성되는 것을 RAF를 통해 반복합니다.

    반응형

    'Javascript > 실습' 카테고리의 다른 글

    [ JS ] Layered illust  (0) 2023.03.01
    [ JS ] Pixel animaiton  (0) 2023.03.01

    댓글

Designed by Tistory.