이 글을 코드로 간편하게 보고싶나요? 다음을 참고하시길 바라요!
일단 먼저 백그라운드를 설명 드리자면, 저는 javascript-utils
라는 레포지토리에서 작업 중이에요. 이 레포지토리는 일련의 자바스크립트 소스들을 모아놓는 역할을 하고 있죠.
따라서, 캔버스의 경우 제가 추후에도 2D 애니메이션을 만들 때 적용할 일들이 많아요.
그렇기에 작업에 앞서, 귀찮더라도 재사용성과 객체 지향적인 설계를 위해 추상클래스 및 클래스를 확장하는 방식으로 MetaballCanvas
라는 것을 설계할 것입니다.
따라서 다음과 같은 결과물이 나오겠어요.
Canvas
: 가장 추상적인 인터페이스GradientCanvas
: Canvas
추상 클래스와 공통 인터페이스를 가지며, 추가적으로 캔버스의 배경을 Gradient
으로 적용할 수 있는 추상 클래스MetaballCanvas
: GradientCanvas
를 상속받는, 실질적으로 이번 프로젝트에서 사용할 서브 클래스따라서, 결과적으로 MetaballCanvas
라는 서브 클래스에서 작업할 거에요. 이를 통해 개방-폐쇄 원칙을 지키며 상위 클래스에서의 인터페이스 변경을 최소화하는 방식으로 구현할 예정입니다. 😉
먼저 두 개의 추상 클래스는 다음과 같이 정의했어요.
export interface CanvasShape {
width: number;
height: number;
}
export enum ECanvasGradientType {
'linear' = 'linear',
'radial' = 'radial',
}
export abstract class Canvas implements CanvasShape {
// 타입을 정의합니다.
abstract type: ECanvasGradientType;
// canvas 엘리먼트를 생성 및 정의합니다.
abstract $canvas: HTMLCanvasElement;
// canvas의 컨텍스트를 메모리에 저장합니다.
abstract ctx: CanvasRenderingContext2D;
// canvas 너비를 조정합니다.
abstract width: CanvasShape['width'];
// canvas 높이를 조정합니다.
abstract height: CanvasShape['height'];
// canvas를 그려냅니다.
abstract draw(background: CanvasGradient | string | CanvasPattern): void;
// 초기에 상위 엘리먼트에 추가합니다. 이후 rendering을 진행합니다.
abstract mount($target: Element): void;
// rendering을 진행합니다.
abstract render(): void;
}
// radialGradient를 생성할 때 발생하는 추가적인 옵션을 지정합니다.
export interface IRadialGradientOptions {
r0?: number;
r1?: number;
}
interface MetaballCanvasOptions {
radialGradient?: IRadialGradientOptions;
}
export abstract class GradientCanvas extends Canvas {
abstract gradients: string[];
abstract options?: MetaballCanvasOptions;
abstract draw(background: CanvasGradient): void;
abstract getLinearGradient(): CanvasGradient;
abstract getRadialGradient(options: IRadialGradientOptions): CanvasGradient;
}
생각보다 간결하죠?
여기서 옵션의 경우, 다음과 같은 의미를 지닙니다.
gradient
방식을 현재는linear
하게 주지만, 언젠가radial
하게 바꾼다면?- 결과적으로 이를 외부에서 바꿔줄 수 있게 옵셔널하게 주는 건 어떨까?
물론 RadialGradientCanvas
LinearGradientCanvas
등으로 나누는 게 더 좋을 것 같기는 하나, 생각보다 Linear
, Radial
여부에 따라 파생되는 파급효과가 크지는 않을 것 같다고 생각했어요. (단순히 배경의 차이라 생각하고 있습니다.) 또한, 전자에 대해 많이 쓸지에 대한 여부가 의문이었습니다.
따라서 어느정도의 유도리(?)를 가지고 당장 필요한 것들만 설계했어요! 😉
이제 추상 클래스를 구현했으니, 메타볼에서는 어떻게 정의할지를 살펴보죠!
import {GradientCanvas, IRadialGradientOptions} from './types';
export class MetaballCanvas implements GradientCanvas {
$canvas: GradientCanvas['$canvas'];
ctx: GradientCanvas['ctx'];
type: GradientCanvas['type'];
width: GradientCanvas['width'];
height: GradientCanvas['height'];
gradients: GradientCanvas['gradients'];
constructor({
type,
width,
height,
gradients,
}: Omit<
GradientCanvas,
| '$canvas'
| 'ctx'
| 'render'
| 'mount'
| 'draw'
| 'getLinearGradient'
| 'getRadialGradient'
>) {
this.$canvas = document.createElement('canvas');
this.ctx = this.$canvas.getContext('2d') as CanvasRenderingContext2D;
this.type = type;
this.width = width;
this.height = height;
this.gradients = gradients;
}
getLinearGradient() {
const result = this.ctx.createLinearGradient(0, 0, 0, this.height);
this.gradients.forEach((gradient, idx) => {
result.addColorStop(idx, gradient);
});
return result;
}
getRadialGradient({r0 = 0, r1 = 0}: IRadialGradientOptions) {
const result = this.ctx.createRadialGradient(0, 0, r0, 0, this.height, r1);
this.gradients.forEach((gradient, idx) => {
result.addColorStop(idx, gradient);
});
return result;
}
draw(background: CanvasGradient) {
this.ctx.clearRect(0, 0, this.width, this.height);
this.ctx.fillStyle = background;
this.ctx.fillRect(0, 0, this.width, this.height);
}
mount($target: Element) {
$target.appendChild(this.$canvas);
this.render();
}
render() {
const canvasGradiation = this.getLinearGradient();
this.draw(canvasGradiation);
}
}
제대로 렌더링이 되는지 결과를 살펴볼까요?
const $target = document.body;
const app = new MetaballAnimation({
canvas: new MetaballCanvas({
gradients: ['#123141', '#235234'],
width: 400,
height: 400,
type: ECanvasGradientType.linear,
}),
});
app.mount($target);
오! 멋진 밤하늘을 닮은 캔버스가 생성되는군요. 🙆🏻🙆🏻♀️
지금까지 메타볼 애니메이션을 구현하기에 앞서 캔버스를 설계 및 구현하는 과정을 만들었어요.
글을 쓰는 과정은 꽤나 호흡이 길어서, 자칫 페이스를 잃기도 하지만 요새는 점차 이런 글쓰기에 갈증이 나기 시작했어요.
개발에 있어서 기록은 분명, 가장 제 성장을 객관적으로 살펴볼 수 있는 지표라는 것을 이번 시리즈를 연재하며 다시금 느끼네요. 🥰
이 시리즈 글들이 캔버스를 생성하거나, 추후 메타볼 애니메이션에 관심을 가지실 분들에게 도움이 되었으면 좋겠네요. 그럼, 다음에는
Metaballs
설계에 대해 살펴볼게요. 다시 만나요! 🎉