Source: ./src/engine/core/render-manager.js

import SceneManager from "/src/engine/core/scene-manager.js";

import { typeCheckAndClamp } from "/src/engine/utils.js";

/**
 * 렌더링을 위해 HTML에서 제공하는 API중 하나인 <canvas>를 사용한다.
 * RenderManager는 Canvas를 다루는 기능을 제공한다.
 *
 * Canvas는 두 개가 있는데, 화면에 드러날 renderCanvas와
 * 이미지에 오버레이를 씌우기 위해 쓰는 bufferCanvas가 있다.
 *
 * Canvas의 크기, 해상도를 바꾸는 책임도 맡는다.
 */
class RenderManager {
  /**
   * renderCanvas의 id값
   *
   * @property {string}
   * @static
   */
  static renderCanvasId = "render-canvas";
  /**
   * renderCanvas의 Element요소
   *
   * @property {RenderCanvasContext2d}
   * @static
   */
  static renderCanvas = undefined;
  /**
   * renderCanvans의 가로 길이
   *
   * @property {number}
   * @static
   */
  static renderCanvasWidth;
  /**
   * renderCanvans의 세로 길이
   *
   * @property {number}
   * @static
   */
  static renderCanvasHeight;
  /**
   * renderCanvans의 최소 가로 길이
   *
   * @property {number}
   * @static
   */
  static renderCanvasMinWidth = 800;
  /**
   * renderCanvans의 최소 세로 길이
   *
   * @property {number}
   * @static
   */
  static renderCanvasMinHeight = 600;
  /**
   * bufferCanvas의 id값
   *
   * @property {string}
   * @static
   */
  static bufferCanvasId = "buffer-canvas";
  /**
   * bufferCanvas의 Element요소
   *
   * @property {RenderCanvasContext2d}
   * @static
   */
  static bufferCanvas = undefined;

  constructor() {}

  /**
   * 현재 씬을 렌더링한다.
   * 1. 먼저 게임화면의 크기를 브라우저의 크기에 맞춘다.
   * 2. 그다음 화면을 지운다.
   * 3. 깨끗해진 화면에 현재 프레임의 모든 오브젝트를 렌더링한다.
   */
  static render() {
    RenderManager.updateRenderCanvasSizeByWindowSize();
    RenderManager.clearScreen();
    SceneManager.getCurrentScene().render();
  }

  /**
   * renderCanvas를 깨끗이 지운다.
   */
  static clearScreen() {
    const canvas = RenderManager.getRenderCanvas();
    canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height);
  }

  /**
   * renderCanvas의 크기(width, height)를 변경한다.
   * 크기를 변경하면 style에도 영향이 있기 때문에 renderCanvas의 CSS도 변경한다.
   *
   * @param {number} width - 가로 크기
   * @param {number} height - 세로 크기
   */
  static changeResolution(width, height) {
    RenderManager.renderCanvasWidth = typeCheckAndClamp(
      width,
      "number",
      1280,
      RenderManager.renderCanvasMinWidth,
      Number.MAX_VALUE
    );
    RenderManager.renderCanvasHeight = typeCheckAndClamp(
      height,
      "number",
      720,
      RenderManager.renderCanvasMinHeight,
      Number.MAX_VALUE
    );

    const renderCanvas = RenderManager.getRenderCanvas();
    renderCanvas.width = RenderManager.renderCanvasWidth;
    renderCanvas.height = RenderManager.renderCanvasHeight;
    RenderManager.changeRenderCanvasStyleVariable(
      RenderManager.renderCanvasWidth,
      RenderManager.renderCanvasHeight
    );
  }

  /**
   * 브라우저의 크기에 따라 canvas의 크기를 조절한다.
   * 조절한 크기를 renderCanvas의 style에 적용한다.
   */
  static updateRenderCanvasSizeByWindowSize() {
    const canvasRatio =
      RenderManager.renderCanvasWidth / RenderManager.renderCanvasHeight;

    let renderCanvasStyleWidth = RenderManager.renderCanvasWidth;
    let renderCanvasStyleHeight = RenderManager.renderCanvasHeight;
    // 화면의 세로 길이가 canvas의 세로 길이보다 작다면
    // canvas의 가로 길이를 화면의 가로 길이로 설정한다.
    if (window.innerHeight < RenderManager.renderCanvasHeight) {
      renderCanvasStyleHeight = window.innerHeight;
    }
    // 화면의 가로 길이가 canvas의 가로 길이보다 작다면
    // canvas의 가로 길이를 화면의 가로 길이로 설정한다.
    if (window.innerWidth < RenderManager.renderCanvasWidth) {
      renderCanvasStyleWidth = window.innerWidth;
    }

    const newRatio = renderCanvasStyleWidth / renderCanvasStyleHeight;
    // w : h = newW : newH
    // newW가 더 길다면 newW = w * newH/h = newH * w/h
    // newH가 더 길다면 newH = h * newW/w = newW * h/w
    if (newRatio > canvasRatio) {
      renderCanvasStyleWidth = renderCanvasStyleHeight * canvasRatio;
    } else if (newRatio < canvasRatio) {
      renderCanvasStyleHeight = renderCanvasStyleWidth / canvasRatio;
    }

    // 완성된 크기를 canvas의 style에 업데이트한다.
    RenderManager.changeRenderCanvasStyleVariable(
      renderCanvasStyleWidth,
      renderCanvasStyleHeight
    );
  }

  /**
   * renderCanvas의 style에 사용되는 변수를 업데이트하여
   * 화면에 나타나는 renderCanvans의 크기를 변경한다.
   *
   * @param {number} width - 가로 크기
   * @param {number} height - 세로 크기
   */
  static changeRenderCanvasStyleVariable(width, height) {
    const root = document.querySelector(":root");
    root.style.setProperty("--render-canvas-width", width);
    root.style.setProperty("--render-canvas-height", height);
  }

  /**
   * bufferCanvas의 크기(width, height)를 변경한다.
   *
   * @param {number} width - 가로 크기
   * @param {number} height - 세로 크기
   */
  static changeBufferCanvasResolution(width, height) {
    const bufferCanvas = RenderManager.getBufferCanvas();
    bufferCanvas.width = width;
    bufferCanvas.height = height;
  }

  /**
   * renderCanvas를 getElementById로 찾아 반환한다.
   * 만약 존재하지 않는 element라면 새로 element를 생성해 반환한다.
   *
   * @returns {RenderCanvasContext2d}
   */
  static getRenderCanvas() {
    if (RenderManager.renderCanvas === undefined) {
      RenderManager.renderCanvas = document.getElementById(
        RenderManager.renderCanvasId
      );
    }

    if (RenderManager.renderCanvas === undefined) {
      RenderManager.renderCanvas = document.createElement("canvas");
      RenderManager.renderCanvas.id = RenderManager.renderCanvasId;
    }

    return RenderManager.renderCanvas;
  }

  /**
   * bufferCanvas를 getElementById로 찾아 반환한다.
   * 만약 존재하지 않는 element라면 새로 element를 생성해 반환한다.
   *
   * @returns {RenderCanvasContext2d}
   */
  static getBufferCanvas() {
    if (RenderManager.bufferCanvas === undefined) {
      RenderManager.bufferCanvas = document.getElementById(
        RenderManager.bufferCanvasId
      );
    }

    if (
      RenderManager.bufferCanvas === undefined ||
      RenderManager.bufferCanvas === null
    ) {
      const bufferCanvas = document.createElement("canvas");
      bufferCanvas.id = RenderManager.bufferCanvasId;
      RenderManager.bufferCanvas = bufferCanvas;
    }
    return RenderManager.bufferCanvas;
  }
}

export default RenderManager;