Home Reference Source

src/components/Camera2D.js

import Camera from './Camera';
import System from '../systems/System';
import { mat4 } from '../utils/gl-matrix';

const ZoomMode = {
  PIXEL_PERFECT: 'pixel-perfect',
  KEEP_ASPECT: 'keep-aspect'
};

/**
 * Camera used to view 2D scene.
 *
 * @example
 * const component = new Camera2D();
 * component.deserialize({ zoomOut: 1024, zoomMode: 'keep-aspect' });
 */
export default class Camera2D extends Camera {

  /**
   * Component factory.
   *
   * @return {Camera2D} Component instance.
   */
  static factory() {
    return new Camera2D();
  }

  /** @type {*} */
  static get propsTypes() {
    return {
      ...Camera.propsTypes,
      zoom: 'number',
      zoomOut: 'number',
      near: 'number',
      far: 'number',
      zoomMode: 'enum(pixel-perfect, keep-aspect)'
    };
  }

  /** @type {*} */
  static get ZoomMode() {
    return ZoomMode;
  }

  /** @type {number} */
  get zoom() {
    return this._zoom;
  }

  /** @type {number} */
  set zoom(value) {
    if (typeof value !== 'number') {
      throw new Error('`value` is not type of Number');
    }

    this._zoom = value;
    this._dirty = true;
  }

  /** @type {number} */
  get zoomOut() {
    const { _zoom } = this;
    return _zoom !== 0 ? 1 / _zoom : 1;
  }

  /** @type {number} */
  set zoomOut(value) {
    if (typeof value !== 'number') {
      throw new Error('`value` is not type of Number');
    }

    this._zoom = value !== 0 ? 1 / value : 1;
    this._dirty = true;
  }

  /** @type {number} */
  get near() {
    return this._near;
  }

  /** @type {number} */
  set near(value) {
    if (typeof value !== 'number') {
      throw new Error('`value` is not type of Number');
    }

    this._near = value;
    this._dirty = true;
  }

  /** @type {number} */
  get far() {
    return this._far;
  }

  /** @type {number} */
  set far(value) {
    if (typeof value !== 'number') {
      throw new Error('`value` is not type of Number');
    }

    this._far = value;
    this._dirty = true;
  }

  /** @type {string} */
  get zoomMode() {
    return this._zoomMode;
  }

  /** @type {string} */
  set zoomMode(value) {
    if (typeof value !== 'string') {
      throw new Error('`value` is not type of String');
    }

    this._zoomMode = value;
    this._dirty = true;
  }

  /** @type {number} */
  get cachedWorldWidth() {
    return this._cachedWorldWidth;
  }

  /** @type {number} */
  get cachedWorldHeight() {
    return this._cachedWorldHeight;
  }

  /**
   * Constructor.
   */
  constructor() {
    super();

    this._zoom = 1;
    this._near = -1;
    this._far = 1;
    this._zoomMode = ZoomMode.PIXEL_PERFECT;
    this._cachedWorldWidth = 0;
    this._cachedWorldHeight = 0;
  }

  /**
   * @override
   */
  buildCameraMatrix(target, width, height) {
    const { _zoom, _zoomMode } = this;
    const scale = _zoom > 0 ? 1 / _zoom : 0;

    if (_zoomMode === ZoomMode.KEEP_ASPECT) {
      if (width >= height) {
        width = width / height;
        height = 1;
      } else {
        height = height / width;
        width = 1;
      }
    }

    const ww = this._cachedWorldWidth = width * scale;
    const wh = this._cachedWorldHeight = height * scale;
    const halfWidth = ww * 0.5;
    const halfHeight = wh * 0.5;

    mat4.ortho(
      target,
      -halfWidth,
      halfWidth,
      halfHeight,
      -halfHeight,
      this._near,
      this._far
    );
  }

  /**
   * @override
   */
  onPropertySerialize(name, value) {
    if (this.zoomOut > this.zoom ? name === 'zoom' : name === 'zoomOut') {
      return;
    } else {
      return super.onPropertySerialize(name, value);
    }
  }

}