[2D 메타볼 애니메이션 구현] 2. 확장성을 고려하며 Canvas 설계하기

young_pallete·2023년 3월 23일
0
post-thumbnail

이 글을 코드로 간편하게 보고싶나요? 다음을 참고하시길 바라요!

캔버스를 어떻게 설계할 것인가

일단 먼저 백그라운드를 설명 드리자면, 저는 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 여부에 따라 파생되는 파급효과가 크지는 않을 것 같다고 생각했어요. (단순히 배경의 차이라 생각하고 있습니다.) 또한, 전자에 대해 많이 쓸지에 대한 여부가 의문이었습니다.

따라서 어느정도의 유도리(?)를 가지고 당장 필요한 것들만 설계했어요! 😉

MetaballCanvas 인터페이스 설계

이제 추상 클래스를 구현했으니, 메타볼에서는 어떻게 정의할지를 살펴보죠!

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 설계에 대해 살펴볼게요. 다시 만나요! 🎉

profile
People are scared of falling to the bottom but born from there. What they've lost is nth. 😉

0개의 댓글