ํ์ฌ ํ๋ก ํธ์๋ ๊ฐ๋ฐ์๋ก ์ผํ๊ณ ์์ผ๋ฉฐ
๊ธ์ ์ฐ๊ฒ ๋ ์ด์ ๋ ์บ๋ฒ์ค ์ ๋๋ฉ์ด์
์ ์ข์ํ๋ ๊ฐ๋ฐ์๊ฐ ๋ง์์ก์ผ๋ฉด ํ๋ ๋ฐ๋์
๋๋ค.
ํด๋น ๋ด์ฉ์ ์์ํ๊ฒ 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์ด ์์ต๋๋ค.
์ข์ ๊ธ ๊ฐ์ฌ๋๋ฆฝ๋๋ค :) ์ ๋ ์บ๋ฒ์ค๋ฅผ ์ด์ฉํ ์ธํฐ๋ํฐ๋ธ ์น ๊ฐ๋ฐ์ ๊ด์ฌ์ด ๋ง์๋ฐ ํน์ ์บ๋ฒ์ค์ ๋ํด ๊ณต๋ถํ๊ธฐ ์ข์ ์ฑ ์ด๋ ํํ ๋ฆฌ์ผ ๊ฐ์ ๊ฒ์ด ์์๊น์? ์ด๋ป๊ฒ ๊ณต๋ถ๋ฅผ ์์ํ๋ฉด ์ข์์ง ์๊ณ ๊ณ์ ๋ค๋ฉด ์กฐ์ธ ์กฐ๊ธ ๋ถํ๋๋ฆฝ๋๋ค ใ ใ ๊ฐ์ฌํฉ๋๋ค!
์ค ํฅ๋ฏธ๋ก์ด ๊ธ์ด๋ค์! ๊ฐ์ฌํฉ๋๋ค ^^
์ ๋ ์ ๋๋ฉ์ด์ ๊ณต๋ถํ๊ณ ์ถ์๋ฐ, ํด๋น ๊ธ์ ๋ํ ์ ์ฒด ์ฝ๋๋ฅผ ๋ณผ ์ ์์๊น์?