거의 알고리즘 일기장

html canvas + ts 로 유성효과 만들기 (canvas animation - 2편) 본문

web

html canvas + ts 로 유성효과 만들기 (canvas animation - 2편)

건우권 2023. 3. 11. 17:54

이번에는 눈효과에 이어 유성효과이다. 유성효과는 

https://kunkunwoo.tistory.com/295

 

html canvas + ts로 눈효과 만들기

예전에 재밌게 했던 html canvas와 바닐라 js 를 이용해 효과들을 만든 기억이 떠올라 다시 만들어보았다. 워밍업으로 간단한 눈효과를 만들어보았다. 데모 세팅 vite, ts, pure css 사용 자세한 내용은

kunkunwoo.tistory.com

이 눈효과에서 조금만 바꾸면 된다.

 

1. 유성처럼 보이려면 일단 유성에는 꼬리가 있다.

유성 꼬리

2. 유성 대가리가 꼬리보다 밝다. (꼬리로 갈수록 서서히 색이 옅어지는 구조)

3. 안보이면 다시 재활용한다 (안보이면 -> init function 실행하여 새로나오는것처럼 구현, 음.. 약간 구현사항이 다르긴 한데 약간 게임개발시에 object pool처럼 instance를 재활용함으로써 비용을 아끼도록 구현) 

 

자 이제 ㄱㄱ하기전에 데모부터 보자


데모

데모 영상

음.. 저 gif로 봐서 좀 유성같이 안보이는데, 실제 60프레임에서의 웹사이트에서는 꽤나 유성처럼 보인다.


구조

구조는 눈 구현에서의 구조와 거의 동일하다. 거의 x, y의 스피드와 color 그리고 canvas의 덮어쓰는 도형의 opacity 정도만 다르다.

여기서 snow-> meteor로만 바꾸면됨

IntroApp class로 dependency injection 하는것도 동일하다 (snow에서 meteor class의 인스턴스를 넣는것만 다르다.)

snow -> meteor로만 바꾸면됨


전체코드

interface (snow 게시물과 동일)

/**
 * @description animated object interface
 */
export interface AnimatedObject {
  init(stageWidth: number, stageHeight: number): void;
  update(): void;
  draw(ctx: CanvasRenderingContext2D): void;
}

/**
 * @description particle interface
 */
export interface Particle extends AnimatedObject {
  x: number;
  y: number;
  radius: number;
  speedX: number;
  speedY: number;
  stageWidth: number;
  stageHeight: number;
}

 

meteor class

import { getRandomArbitrary } from '@/utils';
import { Particle } from '@/src/intro/particles/particles.interface';

/**
 * @description 유성 class
 */
export class Meteor implements Particle {
  x!: number;
  y!: number;
  radius!: number;
  speedX!: number;
  speedY!: number;
  opacity!: number;
  opacityMinus!: number;
  pixelRatio!: number;
  stageWidth!: number;
  stageHeight!: number;

  constructor(stageWidth: number, stageHeight: number) {
    this.init(stageWidth, stageHeight);
  }

  init(stageWidth: number, stageHeight: number): void {
    this.x = getRandomArbitrary(0, stageWidth);
    this.y = getRandomArbitrary(0, stageHeight / 3);
    this.radius = 2;
    this.speedX = getRandomArbitrary(-7, -5);
    this.speedY = getRandomArbitrary(1, 5);
    this.opacity = getRandomArbitrary(0.8, 1);
    this.opacityMinus = getRandomArbitrary(0.001, 0.005);
    this.pixelRatio = window.devicePixelRatio > 1 ? 2 : 1;
    this.stageWidth = stageWidth;
    this.stageHeight = stageHeight;
  }

  update(): void {
    this.x += this.speedX;
    this.y += this.speedY;
    this.opacity -= this.opacityMinus;
  }

  draw(ctx: CanvasRenderingContext2D): void {
    //비가 화면 밖으로 나가던가 opacity 가 0 이하면 다시 init 해줘야함
    if (this.isDead()) {
      //ratio 에 따라 다르게 넣어주었기 때문에.. 이렇게 해야함
      this.init(
        ctx.canvas.width / this.pixelRatio,
        ctx.canvas.height / this.pixelRatio,
      );
    }

    this.update();
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
    ctx.fillStyle = `rgba(100, 255, 255, ${this.opacity})`;
    ctx.fill();
    ctx.closePath();
  }

  //보이지 않거나 시야에서 벗어나면 true
  isDead(): boolean {
    return (
      this.x < 0 ||
      this.x > this.stageWidth ||
      this.y < 0 ||
      this.y > this.stageHeight ||
      this.opacity <= 0
    );
  }
}

 

IntroApp class

import '@/styles/globals.css';
import '@/styles/intro.css';
import { Snow } from '@/src/intro/particles/Snow';
import { Meteor } from '@/src/intro/particles/Meteor';
import { AnimatedObject } from '@/src/intro/particles/particles.interface';
import { GradationBall } from '@/src/intro/particles/GradationBall';

/**
 * TODO: canvas 를 여러개로 두는 형태로 하자
 * canvas 마다 크게보기 기능을 넣고 크게 볼때 animation 이 동작하게 하자
 */
class IntroApp {
  canvas: HTMLCanvasElement;
  ctx: CanvasRenderingContext2D;
  stageWidth: number;
  stageHeight: number;
  pixelRatio: number;
  animatedObjects: AnimatedObject[] = [];

  constructor(animatedObjects: AnimatedObject[] = []) {
    //init
    this.canvas = document.createElement('canvas');
    document.body.appendChild(this.canvas);
    this.ctx = this.canvas.getContext('2d')!;
    this.stageWidth = document.body.clientWidth;
    this.stageHeight = document.body.clientHeight;
    this.pixelRatio = window.devicePixelRatio > 1 ? 2 : 1;
    this.animatedObjects = animatedObjects;

    //resize 한번 호출
    this.resize();

    //event
    window.addEventListener('resize', this.resize.bind(this));
    window.requestAnimationFrame(this.animate.bind(this));
  }

  //화면 resize 시마다 호출되는 함수, canvas 재계산
  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);

    //resize 되었으므로 object init 해줘야함
    this.animatedObjects.forEach((animatedObject) => {
      animatedObject.init(this.stageWidth, this.stageHeight);
    });
  }

  animate() {
    // this.ctx.clearRect(0, 0, this.stageWidth, this.stageHeight);
    //!!! 여기서 덮어쓰는 rect의 opacity가 달라짐!!
    this.ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
    this.ctx.fillRect(0, 0, this.stageWidth, this.stageHeight);

    //animate object draw
    this.animatedObjects.forEach((animatedObject) => {
      animatedObject.draw(this.ctx);
    });

    window.requestAnimationFrame(this.animate.bind(this));
  }
}

window.onload = () => {
  console.log('window onload');

  const meteors = [];
  for (let i = 0; i < 10; i++) {
    meteors.push(new Meteor(0, 0));
  }

  new IntroApp([...meteors]);
};

particle animation에 대한 전체적인 interface 설계를 좀 해놨더니 비슷한걸 만들때는 참으로 편하구먼

 

이 글을 읽고 만약에 canvas animation에 관심이 많이 생긴다면, 이 유튜버를 보면 많은 도움이 될것이다.
(필자도 처음 개발시작할때 이 유튜버의 영상을 보고 프론트 개발에 관심을 가지게 되었다! 실무에 쓸일은.. 많이없지만, 그래도 재밌으니까ㅎㅎ)

https://www.youtube.com/@cmiscm/videos

 

Interactive Developer

코드로 만드는 애니메이션, 영감, 실리콘밸리의 생활과 해외취업에 대해 이야기 합니다. https://blog.cmiscm.com/

www.youtube.com

 

반응형
Comments