Canvas Animation

๊น€๋ณ‘์ฐฌยท2020๋…„ 11์›” 11์ผ
54

canvas

๋ชฉ๋ก ๋ณด๊ธฐ
1/3
post-custom-banner

ํ˜„์žฌ ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๋กœ ์ผํ•˜๊ณ  ์žˆ์œผ๋ฉฐ
๊ธ€์„ ์“ฐ๊ฒŒ ๋œ ์ด์œ ๋Š” ์บ”๋ฒ„์Šค ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ข‹์•„ํ•˜๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๋งŽ์•„์กŒ์œผ๋ฉด ํ•˜๋Š” ๋ฐ”๋žŒ์ž…๋‹ˆ๋‹ค.
ํ•ด๋‹น ๋‚ด์šฉ์€ ์ˆœ์ˆ˜ํ•˜๊ฒŒ TypeScript๋ฅผ ๊ฐ€์ง€๊ณ  ๋งŒ๋“ค์—ˆ์œผ๋ฉฐ ๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์‚ฌ์šฉํ•˜์ง€์•Š์•˜์Šต๋‹ˆ๋‹ค.


// index.ts
class App {
  canvas: HTMLCanvasElement;
  context: CanvasRenderingContext2D;
  delta: number = 0;
  startTime: number;
  frameRequestHandle: number;

  constructor() {
    this.canvas = document.createElement('canvas');
    this.context = this.canvas.getContext('2d')!;
    this.startTime = Date.now();
    this.frameRequestHandle = window.requestAnimationFrame(this.frameRequest);
    document.body.appendChild(this.canvas);
  }

  frameRequest = () => {
    this.frameRequestHandle = window.requestAnimationFrame(this.frameRequest);

    const currentTime = Date.now();
    this.delta = (currentTime - this.startTime) * 0.001;
    this.startTime = currentTime;
  }
}

window.addEventListener('load', () => {
  new App();
});

๊ธฐ๋ณธ์ ์œผ๋กœ ์บ”๋ฒ„์Šค๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๋ Œ๋”๋ง ๋ฃจํ”„๋ฅผ ๊ตฌํ˜„์„ ํ•ฉ๋‹ˆ๋‹ค

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

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

๋„ํ˜•์„ ์–ด๋””์— ๊ทธ๋ฆด์ง€ ํ™œ์šฉํ•  Vector ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•ด๋ด…์‹œ๋‹ค.

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

export default class Shape {
  position: Vector;

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

  update(delta: number) {

  }

  render(context: CanvasRenderingContext2D) {
    
  }
}

Shape ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ํ•ด๋‹น ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›์•„ ๋‹ค๋ฅธ ๋„ํ˜•์„ ๋งŒ๋“ค๊ฒ๋‹ˆ๋‹ค.

// Circle.ts
import Shape from './Shape';
import Vector from './Vector';

export default class Circle extends Shape {
  radius: number;

  constructor(position: Vector, radius: number) {
    super(position);
    this.radius = radius
  }

  update(delta: number) {

  }

  render(context: CanvasRenderingContext2D) {
    context.beginPath();
    context.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2);
    context.fill();
  }
}

๋ฐฉ๊ธˆ์ „์— ๋งŒ๋“  Shapeํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›์•„ Circleํด๋ž˜์Šค๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
์ด์ œ ํ•ด๋‹น ํด๋ž˜์Šค๋ฅผ ๊ฐ€์ง€๊ณ  frameLoop์—์„œ ๋ Œ๋”๋ง ํ•˜๋Š” ๋กœ์ง์„ ์ž‘์„ฑํ• ๊ฒ๋‹ˆ๋‹ค.

// index.ts
import Shape from './Shape';
import Vector from './Vector';
import Circle from './Circle';

class App {
  canvas: HTMLCanvasElement;
  context: CanvasRenderingContext2D;
  delta: number = 0;
  startTime: number;
  frameRequestHandle: number;
  shapes: Array<Shape> = [];

  constructor() {
    this.canvas = document.createElement('canvas');
    this.context = this.canvas.getContext('2d')!;
    this.startTime = Date.now();
    this.frameRequestHandle = window.requestAnimationFrame(this.frameRequest);
    document.body.appendChild(this.canvas);

    this.shapes.push(
      new Circle(new Vector(100, 100), 10)
    )
  }

  frameRequest = () => {
    this.frameRequestHandle = window.requestAnimationFrame(this.frameRequest);

    const currentTime = Date.now();
    this.delta = (currentTime - this.startTime) * 0.001;
    this.startTime = currentTime;

    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);

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

window.addEventListener('load', () => {
  new App();
});

shapes ๋ผ๋Š” ๋ฐฐ์—ด์—๋‹ค Circle์„ ํ•˜๋‚˜ ์ƒ์„ฑํ•œ๋’ค์—
frameRequestํ•จ์ˆ˜์—์„œ update, render๋ฅผ ํ˜ธ์ถœํ•ด์คฌ์Šต๋‹ˆ๋‹ค.
์•„๋ž˜ ๊ทธ๋ฆผ์ฒ˜๋Ÿผ Circle์ด ๋‚˜์˜ค๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์ด์ œ Circle์˜ update๋ถ€๋ถ„์„ ์ˆ˜์ •ํ•ด์„œ
์–ด๋– ํ•œ ๊ฐ๋„๋กœ ์›€์ง์ด๊ฒŒ ํ•ด๋ด…์‹œ๋‹ค.

// Circle.ts
import Shape from './Shape';
import Vector from './Vector';

const PI2 = Math.PI * 2;

export default class Circle extends Shape {
  radius: number;
  angle: number;
  speed: number;

  constructor(position: Vector, radius: number) {
    super(position);
    this.radius = radius;
    this.angle = PI2 * Math.random();
    this.speed = 100 * Math.random();
  }

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

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

Circle property์— angle, speed ๋ณ€์ˆ˜๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์œผ๋ฉฐ
updateํ•จ์ˆ˜์—์„  ํ•ด๋‹น ๋ณ€์ˆ˜๋ฅผ ๊ฐ€์ง€๊ณ  position์„ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋กœ์ง์„ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

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

// Circle.ts
import Shape from './Shape';
import Vector from './Vector';

const PI2 = Math.PI * 2;

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

export default class Circle extends Shape {
  radius: number;
  angle: number;
  speed: number;
  color: string;

  constructor(position: Vector) {
    super(position);
    this.radius = 10 * Math.random();
    this.angle = PI2 * Math.random();
    this.speed = 100 * Math.random();
    this.color = createRandomColor();
  }

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

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

์ƒ‰์ƒ์„ ๋žœ๋ค์œผ๋กœ ์ƒ์„ฑํ•˜๋Š”ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•˜๊ณ 
color ๋ณ€์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ  renderํ•จ์ˆ˜์—์„œ context.fillStyle์— color๋ณ€์ˆ˜๋ฅผ ๋„ฃ์–ด์ค๋‹ˆ๋‹ค.
์›๋ž˜ ์ƒ์„ฑ์ž๋กœ ๋ฐ›๋˜ radius๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ๋žœ๋คํ•œ ์ˆ˜์น˜๋ฅผ ์ค๋‹ˆ๋‹ค.

// index.ts
import Shape from './Shape';
import Vector from './Vector';
import Circle from './Circle';

class App {
  canvas: HTMLCanvasElement;
  context: CanvasRenderingContext2D;
  delta: number = 0;
  startTime: number;
  frameRequestHandle: number;
  shapes: Array<Shape> = [];

  constructor() {
    this.canvas = document.createElement('canvas');
    this.context = this.canvas.getContext('2d')!;
    this.startTime = Date.now();
    this.frameRequestHandle = window.requestAnimationFrame(this.frameRequest);
    document.body.appendChild(this.canvas);

    for (let i = 0; i < 100; i++) {
      this.shapes.push(
        new Circle(new Vector(this.canvas.width * 0.5, this.canvas.height * 0.5))
      )
    }
  }

  frameRequest = () => {
    this.frameRequestHandle = window.requestAnimationFrame(this.frameRequest);

    const currentTime = Date.now();
    this.delta = (currentTime - this.startTime) * 0.001;
    this.startTime = currentTime;

    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);

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

window.addEventListener('load', () => {
  new App();
});

for๋ฌธ์„ ์‚ฌ์šฉํ•˜์—ฌ Circle์„ 100๊ฐœ ์ƒ์„ฑํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

์ด์ œ ์›์ด ์ฒ˜์Œ ๋“ฑ์žฅํ• ๋•Œ ๋ก ํ•˜๊ณ ๋‚˜์˜ค๋Š”๊ฒŒ ์•„๋‹Œ radius๊ฐ€ ์ž‘์•˜๋‹ค๊ฐ€ ์ ์  ์ปค์ง€๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์ƒ‰์ƒ์ด ๋„ˆ๋ฌด ๋‚œ์žกํ•œ๊ฒƒ๊ฐ™์œผ๋‹ˆ ๊น”๋”ํ•˜๊ฒŒ ๋ณ€๊ฒฝํ•ด๋ด…์‹œ๋‹ค.

// AnimatedValue.ts
function linear(t: number) {
  return t;
}

export default class AnimatedValue {
  from: number;
  to: number;
  time: number = 0;
  elapsedTime: number = 0;
  duration: number;
  delay: number;
  easingFunction: (t: number) => number;

  constructor(from: number = 0, to: number = 1, duration: number = 1000, delay: number = 0, easingFunction: (t: number) => number = linear) {
    this.from = from;
    this.to = to;
    this.duration = duration;
    this.delay = delay * 0.001;
    this.easingFunction = easingFunction;
  }

  get value() {
    return this.from + (this.to - this.from) * this.easingFunction.call(null, this.elapsedTime);
  }

  update(delta: number) {
    this.time += delta;

    if (this.time < this.delay) {
      return;
    }

    this.elapsedTime += delta * (1000 / this.duration);
    if (this.elapsedTime >= 1) {
      this.elapsedTime = 1;
    }
  }
}

easing function์„ ํ†ตํ•ด animation์„ ํŽธํ•˜๊ฒŒ ์“ธ์ˆ˜์žˆ๋Š” AnimatedValue ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

import Shape from './Shape';
import Vector from './Vector';
import AnimatedValue from './AnimatedValue';

const PI2 = Math.PI * 2;

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

export default class Circle extends Shape {
  radius: number;
  radiusAnimatedValue: AnimatedValue;
  angle: number;
  speed: number;
  color: string;

  constructor(position: Vector) {
    super(position);
    this.radius = 10 * Math.random();
    this.angle = PI2 * Math.random();
    this.speed = 100 * Math.random();
    this.color = createRandomColor();

    this.radiusAnimatedValue = new AnimatedValue(0, 1, 300, this.speed * 10);
  }

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

    this.radiusAnimatedValue.update(delta);
  }

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

์ƒ‰์ƒ ํ•จ์ˆ˜๋ฅผ ๋นจ๊ฐ„์ƒ‰์„ ์ค‘์ ์œผ๋กœ ๋žœ๋คํ•˜๊ฒŒ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
Circleํด๋ž˜์Šค์—์„œ animatedValue๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋ฉฐ renderํ•จ์ˆ˜์—์„œ radius์— value๋ฅผ ๊ณฑํ•ด์ค๋‹ˆ๋‹ค.

์—ฌ๊ธฐ๊นŒ์ง€ ์•„์ฃผ ๊ฐ„๋‹จํ•œ Canvas Animation์ด ์˜€์Šต๋‹ˆ๋‹ค.

profile
๐Ÿ‘€ ์‹œ๊ฐ์ ์ธ ์š”์†Œ๋ฅผ ์ค‘์š”ํ•˜๊ฒŒ ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.
post-custom-banner

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

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

์˜ค ํฅ๋ฏธ๋กœ์šด ๊ธ€์ด๋„ค์š”! ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค ^^
์ €๋„ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ์€๋ฐ, ํ•ด๋‹น ๊ธ€์— ๋Œ€ํ•œ ์ „์ฒด ์ฝ”๋“œ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์„๊นŒ์š”?

1๊ฐœ์˜ ๋‹ต๊ธ€
comment-user-thumbnail
2020๋…„ 11์›” 16์ผ

์žฌ๋ฐŒ์–ด์š” ์ž˜ ์ฝ๊ณ  ๊ฐ‘๋‹ˆ๋‹ค

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ
comment-user-thumbnail
2021๋…„ 5์›” 5์ผ

์ข‹์€ ๊ธ€ ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค :) ์ €๋„ ์บ”๋ฒ„์Šค๋ฅผ ์ด์šฉํ•œ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์›น ๊ฐœ๋ฐœ์— ๊ด€์‹ฌ์ด ๋งŽ์€๋ฐ ํ˜น์‹œ ์บ”๋ฒ„์Šค์— ๋Œ€ํ•ด ๊ณต๋ถ€ํ•˜๊ธฐ ์ข‹์€ ์ฑ…์ด๋‚˜ ํŠœํ† ๋ฆฌ์–ผ ๊ฐ™์€ ๊ฒƒ์ด ์žˆ์„๊นŒ์š”? ์–ด๋–ป๊ฒŒ ๊ณต๋ถ€๋ฅผ ์‹œ์ž‘ํ•˜๋ฉด ์ข‹์„์ง€ ์•Œ๊ณ  ๊ณ„์‹ ๋‹ค๋ฉด ์กฐ์–ธ ์กฐ๊ธˆ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค ใ…Žใ…Ž ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!

1๊ฐœ์˜ ๋‹ต๊ธ€
comment-user-thumbnail
2022๋…„ 2์›” 15์ผ

์ž˜๋ณด๊ณ ๊ฐ‘๋‹ˆ๋‹ค!!

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ
comment-user-thumbnail
2022๋…„ 4์›” 26์ผ

์žฌ๋ฐŒ๊ฒŒ๋ดค์Šต๋‹ˆ๋‹ค!

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