Canvas Fireworks

kimbyungchanยท2020๋…„ 11์›” 12์ผ
6

canvas

๋ชฉ๋ก ๋ณด๊ธฐ
3/4
post-thumbnail

๐ŸŽ‰ ๊ฐ„๋‹จํ•œ ํญ์ฃฝ ํšจ๊ณผ๋ฅผ Canvas๋ฅผ ์ด์šฉํ•ด ๊ตฌํ˜„ํ•ด๋ด…์‹œ๋‹ค.

์ด์ „ ํฌ์ŠคํŒ…์„ ์ฐธ๊ณ ํ•˜์‹œ๋ฉด ๊ตฌ์กฐ๋ฅผ ์ดํ•ดํ•˜๋Š”๋ฐ ๋„์›€์ด๋ฉ๋‹ˆ๋‹ค.
https://velog.io/@kimbyungchan/canvas-animation
https://velog.io/@kimbyungchan/canvas-mouse-interaction


// Time.ts
export default class Time {
  static delta: number = 0;
  static startTime: number = 0;

  static start() {
    Time.startTime = Date.now();
  }

  static update() {
    const currentTime = Date.now();
    Time.delta = (currentTime - Time.startTime) * 0.001;
    Time.startTime = currentTime;
  }
}

์‹œ๊ฐ„์„ ๊ด€๋ฆฌํ•  Time ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•˜๊ณ 
update ํ•จ์ˆ˜์—์„œ ์ž์‹ ์˜ static ๋ณ€์ˆ˜์ธ delta, startTime์„ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.
delta๋Š” ์ด์ „ ํ”„๋ ˆ์ž„๊ณผ ํ˜„์žฌ ํ”„๋ ˆ์ž„์˜ ์‹œ๊ฐ„์ฐจ์ด์ž…๋‹ˆ๋‹ค.

// Vector.ts
export default class Vector {
  x: number;
  y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

์ขŒํ‘œ๋ฅผ ๊ด€๋ฆฌํ•  Vector ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
(๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด๋„ ์ข‹์Šต๋‹ˆ๋‹ค. https://www.npmjs.com/package/fast-vector ์ œ๊ฐ€๋งŒ๋“ ๊ฑฐ๋ผ์„œ ํ™๋ณดํ•˜๋Š”๊ฑด ์•„๋‹™๋‹ˆ๋‹ค. ๐Ÿคฃ)

//Entity.ts
import Vector from './Vector';

export default class Entity {
  position: Vector;

  constructor(position: Vector) {
    this.position = position;
  }

  update() {

  }

  render(context: CanvasRenderingContext2D) {

  }
}

์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›์•„ ๋‹ค๋ฅธ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ตฌํ˜„ํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.

// EntityManager.ts
import Entity from './Entity';

export default class EntityManager {
  entities: Entity[] = [];

  update() {
    for (let i = 0; i < this.entities.length; i++) {
      this.entities[i].update();
    }
  }

  render(context: CanvasRenderingContext2D) {
    context.clearRect(0, 0, context.canvas.width, context.canvas.height);

    for (let i = 0; i < this.entities.length; i++) {
      this.entities[i].render(context);
    }
  }

  addEntity(entity: Entity) {
    this.entities.push(entity);
  }

  removeEntity(entity: Entity) {
    const entityIndex = this.entities.indexOf(entity);
    if (entityIndex > -1) {
      this.entities.splice(entityIndex, 1);
    }
  }
}

์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ด€๋ฆฌํ•  EntityManager ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
addEntity, removeEntity ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด entity๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// index.ts
import Time from './Time';
import EntityManager from './EntityManager';

class App {
  ref: HTMLElement;
  canvas: HTMLCanvasElement;
  context: CanvasRenderingContext2D;
  handleRequestFrame: number | null = null;
  entityManager: EntityManager;

  constructor(ref: HTMLElement) {
    this.ref = ref;
    this.canvas = document.createElement('canvas');
    this.canvas.width = 700;
    this.canvas.height = 700;
    this.context = this.canvas.getContext('2d')!;
    this.entityManager = new EntityManager();
    this.ref.appendChild(this.canvas);
  }

  play = () => {
    Time.start();
    this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
  }

  pause = () => {
    if (this.handleRequestFrame === null) {
      return;
    }

    window.cancelAnimationFrame(this.handleRequestFrame);
  }

  onEnterFrame = () => {
    Time.update();
    this.entityManager.update();
    this.entityManager.render(this.context);
    this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
  }
}

window.addEventListener('load', () => {
  const app = new App(document.body);
  app.play();
});

์บ”๋ฒ„์Šค, EntityManager๋ฅผ ์—…๋ฐ์ดํŠธํ•ด์ค„ App ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
onEnterFrameํ•จ์ˆ˜์—์„œ entityManager์˜ update, renderํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
๊ธฐ์กด ๊ธ€์—์„œ ์‚ฌ์šฉํ•˜๋˜ ๊ตฌ์กฐ๋ณด๋‹ค ์กฐ๊ธˆ๋” ๊ณ ๋„ํ™” ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

// Firework.ts
import Entity from './Entity';
import Vector from './Vector';
import Time from './Time';

const PI2 = Math.PI * 2;

function createRandomColor(): string {
  const r = Math.round(Math.random() * 120) + 120;
  const g = Math.round(Math.random() * 120) + 120;
  const b = Math.round(Math.random() * 120) + 120;
  return `rgb(${r}, ${g}, ${b})`;
}

export default class Firework extends Entity {
  angle: number;
  speed: number;
  radius: number;
  color: string;

  constructor(position: Vector) {
    super(position);

    this.angle = -Math.PI * 0.5;
	this.speed = Math.random() * 15 + 15;
	this.radius = Math.random() + 1;
    this.color = createRandomColor();
  }

  update() {
    this.position.x += Math.cos(this.angle) * this.speed * Time.delta;
    this.position.y += Math.sin(this.angle) * this.speed * Time.delta;
  }

  render(context: CanvasRenderingContext2D) {
    context.beginPath();
    context.fillStyle = this.color;
    context.arc(this.position.x, this.position.y, this.radius, 0, PI2);
    context.fill();
  }
}

Entity ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›์•„ Firework ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
update ํ•จ์ˆ˜์—์„  angle๋ณ€์ˆ˜์˜ ๊ฐ’์—๋”ฐ๋ผ position์„ ์—…๋ฐ์ดํŠธํ•ด์ค๋‹ˆ๋‹ค.
render ํ•จ์ˆ˜์—์„  ๊ธฐ๋ณธ์ ์œผ๋กœ ์›์„ ๊ทธ๋ ค๋ด…๋‹ˆ๋‹ค.

// index.ts
import Time from './Time';
import EntityManager from './EntityManager';
import Firework from './Firework';
import Vector from './Vector';

class App {
  ref: HTMLElement;
  canvas: HTMLCanvasElement;
  context: CanvasRenderingContext2D;
  handleRequestFrame: number | null = null;
  entityManager: EntityManager;

  constructor(ref: HTMLElement) {
    this.ref = ref;
    this.canvas = document.createElement('canvas');
    this.canvas.width = 700;
    this.canvas.height = 700;
    this.context = this.canvas.getContext('2d')!;
    this.entityManager = new EntityManager();
    this.ref.appendChild(this.canvas);
  }

  play = () => {
    Time.start();
    this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
  }

  pause = () => {
    if (this.handleRequestFrame === null) {
      return;
    }

    window.cancelAnimationFrame(this.handleRequestFrame);
  }

  onEnterFrame = () => {
    Time.update();
    this.entityManager.update();
    this.entityManager.render(this.context);
    this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
  }
}

window.addEventListener('load', () => {
  const app = new App(document.body);

  const firework = new Firework(
    new Vector(app.canvas.width * 0.5, app.canvas.height * 0.5)
  );

  app.entityManager.addEntity(firework);
  app.play();
});

window load ํ•จ์ˆ˜์—์„œ firework๋ฅผ ์ƒ์„ฑํ•˜๊ณ  entityManager๋ฅผ ํ†ตํ•ด ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

์ƒˆ๋กœ๊ณ ์นจ ํ• ๋•Œ๋งˆ๋‹ค. ์œ„๋กœ ์˜ฌ๋ผ๊ฐ€๋Š” ์›์ด ๋ณด์ž…๋‹ˆ๋‹ค.
๊ฐ€์†๋„๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ๋ฐฐ๊ฒฝ์ƒ‰์„ ๋„ฃ๊ณ , clearํ•˜๋Š” ๋กœ์ง์„ ์ˆ˜์ •ํ•ด ๊ผฌ๋ฆฌ๊ฐ€ ๋‹ฌ๋ฆฐ๊ฒƒ์ฒ˜๋Ÿผ ํ‘œํ˜„ํ•ด๋ด…์‹œ๋‹ค.

// Entity.ts
import Entity from './Entity';

export default class EntityManager {
  entities: Entity[] = [];

  update() {
    for (let i = 0; i < this.entities.length; i++) {
      this.entities[i].update();
    }
  }

  render(context: CanvasRenderingContext2D) {
    context.beginPath();
    context.fillStyle = 'rgba(0, 0, 0, .05)';
    context.fillRect(0, 0, context.canvas.width, context.canvas.height);
    context.fill();

    for (let i = 0; i < this.entities.length; i++) {
      this.entities[i].render(context);
    }
  }

  addEntity(entity: Entity) {
    this.entities.push(entity);
  }

  removeEntity(entity: Entity) {
    const entityIndex = this.entities.indexOf(entity);
    if (entityIndex > -1) {
      this.entities.splice(entityIndex, 1);
    }
  }
}

context.clearํ•˜๋Š” ๋กœ์ง์„ ์ œ๊ฑฐํ•˜๊ณ  canvas ์‚ฌ์ด์ฆˆ ๋งŒํ•œ opacity๊ฐ€ 0.1์ธ ๊ฒ€์€์ƒ‰ ์‚ฌ๊ฐํ˜•์„ ๊ทธ๋ฆฌ๊ฒŒ ๋ณ€๊ฒฝํ•˜์˜€์Šต๋‹ˆ๋‹ค.

// Firework.ts
import Entity from './Entity';
import Vector from './Vector';
import Time from './Time';

const PI2 = Math.PI * 2;

function createRandomColor(): string {
  const r = Math.round(Math.random() * 120) + 120;
  const g = Math.round(Math.random() * 120) + 120;
  const b = Math.round(Math.random() * 120) + 120;
  return `rgb(${r}, ${g}, ${b})`;
}

export default class Firework extends Entity {
  angle: number;
  speed: number;
  radius: number;
  color: string;
  velocity: number;

  constructor(position: Vector) {
    super(position);

    this.angle = -Math.PI * 0.5;
    this.speed = Math.random() * 15 + 15;
    this.radius = Math.random() + 1;
    this.velocity = Math.random() * 15 + 5;
    this.color = createRandomColor();
  }

  update() {
    const speedVelocity = this.speed * this.velocity * Time.delta;
    this.position.x += Math.cos(this.angle) * speedVelocity;
    this.position.y += Math.sin(this.angle) * speedVelocity;
    this.velocity *= 0.98;
  }

  render(context: CanvasRenderingContext2D) {
    context.beginPath();
    context.fillStyle = this.color;
    context.arc(this.position.x, this.position.y, this.radius, 0, PI2);
    context.fill();
  }
}

๊ฐ€์†๋„ ๋ณ€์ˆ˜์ธ velocity๋ฅผ ์ถ”๊ฐ€ํ•˜์˜€๊ณ  updateํ• ๋•Œ speed๋ณ€์ˆ˜์™€ ๊ณฑํ•ด์„œ ์‚ฌ์šฉํ•˜๊ณ 
๊ฐ€์†๋„๊ฐ€ 0์— ์ˆ˜๋ ดํ•˜๋„๋ก 0.98์„ ๊ณฑํ•ด์ค๋‹ˆ๋‹ค.

์ด์ œ ๊ฐ€์†๋„๊ฐ€ 0์— ๊ฐ€๊นŒ์›Œ์งˆ๋•Œ ํ„ฐ์ง€๋Š” ํšจ๊ณผ๋ฅผ ์ถ”๊ฐ€ํ•ด๋ด…์‹œ๋‹ค.
๊ทธ ์ „์— Firework(Entity) ํด๋ž˜์Šค์—์„œ EntityManager์— ์ ‘๊ทผํ•ด ํ„ฐ์ง€๋Š” ํšจ๊ณผ๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผํ•˜๋‹ˆ ์‹ฑ๊ธ€ํ†ค ํŒจํ„ด์œผ๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ์ ‘๊ทผํ• ์ˆ˜์žˆ๋„๋ก ํ•ด๋ด…์‹œ๋‹ค.

// EntityManager.ts
import Entity from './Entity';

export default class EntityManager {
  static instance: EntityManager;

  static addEntity(entity: Entity) {
    EntityManager.instance.addEntity(entity);
  }

  static removeEntity(entity: Entity) {
    EntityManager.instance.removeEntity(entity);
  }
  
  constructor() {
    EntityManager.instance = this;
  }

  entities: Entity[] = [];

  update() {
    for (let i = 0; i < this.entities.length; i++) {
      this.entities[i].update();
    }
  }

  render(context: CanvasRenderingContext2D) {
    context.beginPath();
    context.fillStyle = 'rgba(0, 0, 0, .05)';
    context.fillRect(0, 0, context.canvas.width, context.canvas.height);
    context.fill();

    for (let i = 0; i < this.entities.length; i++) {
      this.entities[i].render(context);
    }
  }

  addEntity(entity: Entity) {
    this.entities.push(entity);
  }

  removeEntity(entity: Entity) {
    const entityIndex = this.entities.indexOf(entity);
    if (entityIndex > -1) {
      this.entities.splice(entityIndex, 1);
    }
  }
}

static ๋ณ€์ˆ˜๋ฅผ ํ†ตํ•ด ์ ‘๊ทผํ• ์ˆ˜์žˆ๊ฒŒ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

// Firework.ts
import Entity from './Entity';
import Vector from './Vector';
import Time from './Time';
import EntityManager from './EntityManager';

const PI2 = Math.PI * 2;

function createRandomColor(): string {
  const r = Math.round(Math.random() * 120) + 120;
  const g = Math.round(Math.random() * 120) + 120;
  const b = Math.round(Math.random() * 120) + 120;
  return `rgb(${r}, ${g}, ${b})`;
}

export default class Firework extends Entity {
  angle: number;
  speed: number;
  radius: number;
  color: string;
  velocity: number;
  isBurst: boolean = false;

  constructor(position: Vector) {
    super(position);

    this.angle = -Math.PI * 0.5;
    this.speed = Math.random() * 15 + 15;
    this.radius = Math.random() + 1;
    this.velocity = Math.random() * 15 + 5;
    this.color = createRandomColor();
  }

  createFireworks() {
    const size = 50;
    const angle = PI2 / size;
    const speed = Math.random() * 20 + 10;
    const velocity = Math.random() * 10 + 5;

    for (let i = 0; i < size; i++) {
      const firework = new Firework(
        new Vector(this.position.x, this.position.y)
      );

      firework.angle = angle * i;
      firework.isBurst = true;
      firework.speed = speed;
      firework.velocity = velocity;

      EntityManager.addEntity(firework);
    }
  }

  update() {
    const speedVelocity = this.speed * this.velocity * Time.delta;
    this.position.x += Math.cos(this.angle) * speedVelocity;
    this.position.y += Math.sin(this.angle) * speedVelocity;
    this.velocity *= 0.98;

    if (!this.isBurst && this.velocity <= 1) {
      this.isBurst = true;
      this.createFireworks();
    } else if (this.velocity <= 1) {
      EntityManager.removeEntity(this);
    }
  }

  render(context: CanvasRenderingContext2D) {
    context.beginPath();
    context.fillStyle = this.color;
    context.arc(this.position.x, this.position.y, this.radius, 0, PI2);
    context.fill();
  }
}

isBurst ๋ณ€์ˆ˜๋ฅผ ํ†ตํ•ด ํ„ฐ์กŒ๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๊ณ  false์ด๋ฉด
velocity๊ฐ€ 1๋ณด๋‹ค ์ž‘๊ฑฐ๋‚˜ ๊ฐ™์„๋•Œ creeateFireworksํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
creeateFireworksํ•จ์ˆ˜์—์„  ์ž๊ธฐ ์ž์‹ ์„ ๋‹ค์‹œ ์ƒ์„ฑํ•˜๋Š”๋ฐ ์ฒ˜์Œ ์ƒ์„ฑํ• ๋•Œ ์ƒ์„ฑ์ž์—์„œ ๋žœ๋ค์œผ๋กœ ์ƒ์„ฑ์‹œํ‚ค์ง€ ์•Š๊ณ  ๊ฐ’์„ ์ผ์ •ํ•˜๊ฒŒ ์ •ํ•œ๋’ค ์—ฌ๋Ÿฌ๊ฐœ๋ฅผ ์ƒ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ  isBurst === true ์ด๋ฉฐ velocity๊ฐ€ 1๋ณด๋‹ค ์ž‘๊ฑฐ๋‚˜ ๊ฐ™์€ Firework๋Š” removeEntity๋ฅผ ํ†ตํ•ด ์ œ๊ฑฐํ•˜์˜€์Šต๋‹ˆ๋‹ค.
์—ฌ๊ธฐ์—์„œ creeateFireworksํ•จ์ˆ˜์—์„œ isBurst๋ฅผ true๋กœ ์ƒ์„ฑํ•˜์ง€์•Š์œผ๋ฉด ์ƒˆ๋กœ ์ƒ์„ฑ๋œ firework๊ฐ€ ๋‹ค์‹œ firework๋ฅผ ์ƒ์„ฑํ•˜๊ณ .. ๋˜ ์ƒ์„ฑํ•˜๊ณ  ๋ฌด์ˆ˜ํžˆ ๋งŽ์•„์ง‘๋‹ˆ๋‹ค.

์ด์ œ ๊ทธ๋Ÿด๋“ฏํ•ด์กŒ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์กฐ๊ธˆ๋” ๊ฐ€๊ฟ”๋ณด๋„๋ก ํ•˜์ฃ  ์ด์ œ ์—ฌ๋Ÿฌ๋ฒˆ ํ„ฐ์ง€๋Š” ํšจ๊ณผ๋ฅผ ์ถ”๊ฐ€ํ• ๊ฒ๋‹ˆ๋‹ค.
createFireworks๋ฅผ ์‹œ๊ฐ„์ฐจ๋กœ ์—ฌ๋Ÿฌ๋ฒˆ ํ˜ธ์ถœํ•˜๋ฉด ๋˜๊ฒ ์ฃ ?

// Firework.ts
import Entity from './Entity';
import Vector from './Vector';
import Time from './Time';
import EntityManager from './EntityManager';

const PI2 = Math.PI * 2;

function createRandomColor(): string {
  const r = Math.round(Math.random() * 120) + 120;
  const g = Math.round(Math.random() * 120) + 120;
  const b = Math.round(Math.random() * 120) + 120;
  return `rgb(${r}, ${g}, ${b})`;
}

function sleep(ms: number) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve();
    }, ms);
  })
}

export default class Firework extends Entity {
  angle: number;
  speed: number;
  radius: number;
  color: string;
  velocity: number;
  isBurst: boolean = false;

  constructor(position: Vector) {
    super(position);

    this.angle = -Math.PI * 0.5;
    this.speed = Math.random() * 15 + 15;
    this.radius = Math.random() + 1;
    this.velocity = Math.random() * 15 + 5;
    this.color = createRandomColor();
  }

  createFireworks() {
    const size = 50;
    const angle = PI2 / size;
    const speed = 20;
    const velocity = 10;

    for (let i = 0; i < size; i++) {
      const firework = new Firework(
        new Vector(this.position.x, this.position.y)
      );

      firework.angle = angle * i;
      firework.isBurst = true;
      firework.speed = speed;
      firework.velocity = velocity;

      EntityManager.addEntity(firework);
    }
  }

  async update() {
    const speedVelocity = this.speed * this.velocity * Time.delta;
    this.position.x += Math.cos(this.angle) * speedVelocity;
    this.position.y += Math.sin(this.angle) * speedVelocity;
    this.velocity *= 0.98;

    if (!this.isBurst && this.velocity <= 1) {
      this.isBurst = true;

      this.createFireworks();
      await sleep(300);
      this.createFireworks();
      await sleep(300);
      this.createFireworks();
      
    } else if (this.velocity <= 1) {
      EntityManager.removeEntity(this);
    }
  }

  render(context: CanvasRenderingContext2D) {
    context.beginPath();
    context.fillStyle = this.color;
    context.arc(this.position.x, this.position.y, this.radius, 0, PI2);
    context.fill();
  }
}

๋จผ์ € sleep ์ด๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค๊ณ 
async, await ๊ตฌ๋ฌธ์„ ์ด์šฉํ•ด updateํ•จ์ˆ˜์—์„œ createFireworks๋ฅผ 300ms ๋งˆ๋‹ค ํ•œ๋ฒˆ์”ฉ ์ด ์„ธ๋ฒˆ ํ˜ธ์ถœํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

๋˜ createFireworksํ•จ์ˆ˜์—์„œ ๋žœ๋ค์œผ๋กœ speed, velocity๋ฅผ ์ฃผ์—ˆ๋Š”๋ฐ ์ด๋ถ€๋ถ„์„ ๊ณ ์ •๊ฐ’์œผ๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

์กฐ๊ธˆ๋” ์ด์œ ์ƒ‰์ƒ๊ณผ, ๋ชจ์–‘, ์ค‘๋ ฅ๋„ ์ ์šฉํ•ด๋ด…์‹œ๋‹ค.

// EntitiyManager
context.beginPath();
context.fillStyle = 'rgba(0, 0, 0, .2)';
context.fillRect(0, 0, context.canvas.width, context.canvas.height);
context.fill();

EntityManager ํด๋ž˜์Šค์˜ renderํ•จ์ˆ˜์˜ clear๋ถ€๋ถ„์„ ์ด๋ ‡๊ฒŒ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

// Firework.ts
import Entity from './Entity';
import Vector from './Vector';
import Time from './Time';
import EntityManager from './EntityManager';

const PI2 = Math.PI * 2;

function createRandomColor(): string {
  const r = Math.round(Math.random() * 120) + 170;
  const g = Math.round(Math.random() * 120) + 170;
  const b = Math.round(Math.random() * 120) + 170;
  return `rgb(${r}, ${g}, ${b})`;
}

function sleep(ms: number) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve();
    }, ms);
  })
}

export default class Firework extends Entity {
  angle: number;
  speed: number;
  radius: number;
  color: string;
  velocity: number;
  isBurst: boolean = false;

  constructor(position: Vector) {
    super(position);

    this.angle = -Math.PI * 0.5;
    this.speed = Math.random() * 15 + 25;
    this.radius = Math.random() + 1;
    this.velocity = Math.random() * 15 + 5;
    this.color = createRandomColor();
  }

  createFireworks(speed: number, size = 50) {
    const angle = PI2 / size;
    const velocity = 10;
    const color = this.color;

    for (let i = 0; i < size; i++) {
      const firework = new Firework(
        new Vector(this.position.x, this.position.y)
      );

      firework.angle = angle * i;
      firework.isBurst = true;
      firework.speed = speed;
      firework.velocity = velocity;
      firework.color = color;
      firework.radius = this.radius;

      EntityManager.addEntity(firework);
    }
  }

  async update() {
    const speedVelocity = this.speed * this.velocity * Time.delta;
    this.position.x += Math.cos(this.angle) * speedVelocity;
    this.position.y += Math.sin(this.angle) * speedVelocity;
    this.velocity *= 0.98;

    if (!this.isBurst && this.velocity <= 1) {
      this.isBurst = true;

      this.createFireworks(16, 30);
      await sleep(150);
      this.createFireworks(12, 20);
      await sleep(150);
      this.createFireworks(10, 10);

    } else if (this.isBurst) {
      this.position.y += this.speed * Time.delta + 0.98;
      this.position.y *= 1.0005;

      if (this.velocity <= 1) {
        EntityManager.removeEntity(this);
      }
    }
  }

  render(context: CanvasRenderingContext2D) {
    context.beginPath();
    context.fillStyle = this.color;
    context.arc(this.position.x, this.position.y, this.radius, 0, PI2);
    context.fill();
  }
}

createFireworks ์ƒ์„ฑํ•˜๋Š” ํ•จ์ˆ˜์— speed, size์ธ์ž๋ฅผ ๋ฐ›๊ฒŒ ๋ณ€๊ฒฝํ–ˆ์œผ๋ฉฐ
isBurst === true ์ธ Firework๋Š” position.y๊ฐ€ ์ฆ๊ฐ€ํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

// index.ts
import Time from './Time';
import EntityManager from './EntityManager';
import Firework from './Firework';
import Vector from './Vector';

class App {
  ref: HTMLElement;
  canvas: HTMLCanvasElement;
  context: CanvasRenderingContext2D;
  handleRequestFrame: number | null = null;
  entityManager: EntityManager;

  constructor(ref: HTMLElement) {
    this.ref = ref;
    this.canvas = document.createElement('canvas');
    this.canvas.width = 700;
    this.canvas.height = 700;
    this.context = this.canvas.getContext('2d')!;
    this.entityManager = new EntityManager();
    this.ref.appendChild(this.canvas);
  }

  play = () => {
    Time.start();
    this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
  }

  pause = () => {
    if (this.handleRequestFrame === null) {
      return;
    }

    window.cancelAnimationFrame(this.handleRequestFrame);
  }

  onEnterFrame = () => {
    Time.update();
    this.entityManager.update();
    this.entityManager.render(this.context);
    this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
  }
}

window.addEventListener('load', () => {
  const app = new App(document.body);

  function createFirework() {
    const firework = new Firework(
      new Vector(app.canvas.width * Math.random(), app.canvas.height)
    );

    app.entityManager.addEntity(firework);
  }

  createFirework();
  setInterval(() => {
    createFirework();
  }, 600);

  app.play();
});

๊ทธ๋ฆฌ๊ณ  600ms ๋งˆ๋‹ค ๋žœ๋ค x์ขŒํ‘œ์—์„œ Firework๋ฅผ ์ƒ์„ฑํ•˜๊ฒŒ ๋ณ€๊ฒฝํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๊ฐ„๋‹จํ•˜๊ฒŒ Canvas๋ฅผ ์ด์šฉํ•ด Fireworkํšจ๊ณผ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์•˜์Šต๋‹ˆ๋‹ค.

profile
๊ฐ„๋‹จํ•œ๊ฑธ ์ข‹์•„ํ•˜๋Š” ๊ฐœ๋ฐœ์ž์ž…๋‹ˆ๋‹ค

1๊ฐœ์˜ ๋Œ“๊ธ€

comment-user-thumbnail
2020๋…„ 11์›” 13์ผ

๐Ÿ‘€๐Ÿ‘

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ