camera.js

var inherit = require('./inherit');
var object = require('./object');
var util = require('./util');
var mat3 = require('gl-mat3');
var vec3 = require('gl-vec3');
var mat4 = require('gl-mat4');
var vec4 = require('gl-vec4');

/**
 * This class defines the raw interface for a camera.  At a low level, the
 * camera provides a methods for converting between a map's coordinate system
 * to display pixel coordinates.
 *
 * For the moment, all camera transforms are assumed to be expressible as
 * 4x4 matrices.  More general cameras may follow that break this assumption.
 *
 * The interface for the camera is relatively stable for "map-like" views,
 * e.g. when the camera is pointing in the direction [0, 0, -1], and placed
 * above the z=0 plane.  More general view changes and events have not yet
 * been defined.
 *
 * The camera emits the following events when the view changes:
 *
 *   * {@link geo.event.camera.pan} when the camera is translated in the
 *       x/y plane
 *   * {@link geo.event.camera.zoom} when the camera is changed in a way
 *       that modifies the current zoom level
 *   * {@link geo.event.camera.view} when the visible bounds change for
 *       any reason
 *   * {@link geo.event.camera.projection} when the projection type changes
 *   * {@link geo.event.camera.viewport} when the viewport changes
 *
 * By convention, protected methods do not update the internal matrix state,
 * public methods do.  There are a few primary methods that are intended to
 * be used by external classes to mutate the internal state:
 *
 *   * bounds: Set the visible bounds (for initialization and zooming)
 *   * pan: Translate the camera in x/y by an offset (for panning)
 *   * viewFromCenterSizeRotation: set the camera view based on a center
 *        point, boundary size, and rotation angle.
 *
 * @class
 * @alias geo.camera
 * @extends geo.object
 * @param {object?} spec Options argument
 * @param {string} spec.projection One of the supported
 *    {@link geo.camera.projection}.
 * @param {object} spec.viewport The initial camera viewport
 * @param {object} spec.viewport.width
 * @param {object} spec.viewport.height
 * @returns {geo.camera}
 */
var camera = function (spec) {
  if (!(this instanceof camera)) {
    return new camera(spec);
  }

  var geo_event = require('./event');

  spec = spec || {};
  object.call(this, spec);

  /**
   * The view matrix
   * @protected
   */
  this._view = util.mat4AsArray();

  /**
   * The projection matrix
   * @protected
   */
  this._proj = util.mat4AsArray();

  /**
   * The projection type (one of `this.constructor.projection`)
   * @protected
   */
  this._projection = null;

  /**
   * The transform matrix (view * proj)
   * @protected
   */
  this._transform = util.mat4AsArray();

  /**
   * The inverse transform matrix (view * proj)^-1
   * @protected
   */
  this._inverse = util.mat4AsArray();

  /**
   * Cached bounds object recomputed on demand.
   * @protected
   */
  this._bounds = null;

  /**
   * Cached "display" matrix recomputed on demand.
   * @see {@link geo.camera.display}
   * @protected
   */
  this._display = null;

  /**
   * Cached "world" matrix recomputed on demand.
   * @see {@link geo.camera.world}
   * @protected
   */
  this._world = null;

  /**
   * The viewport parameters size and offset.
   * @property {number} height Viewport height in pixels
   * @property {number} width Viewport width in pixels
   * @protected
   */
  this._viewport = {width: 1, height: 1};

  /**
   * Set up the projection matrix for the current projection type.
   * @protected
   */
  this._createProj = function () {
    var func = this._projection === 'perspective' ? mat4.frustum : mat4.ortho,
        clipbounds = this._clipbounds[this._projection];
    func(this._proj, clipbounds.left, clipbounds.right, clipbounds.bottom,
         clipbounds.top, clipbounds.near, clipbounds.far);
  };

  /**
   * Update the internal state of the camera on change to camera
   * parameters.
   * @protected
   * @fires geo.event.camera.view
   */
  this._update = function () {
    this._bounds = null;
    this._display = null;
    this._world = null;
    this._transform = mat4.multiply(util.mat4AsArray(), this._proj, this._view);
    mat4.invert(this._inverse, this._transform);
    this.geoTrigger(geo_event.camera.view, {
      camera: this
    });
  };

  /**
   * Getter/setter for the view matrix.
   * @note copies the matrix value on set.
   * @property {mat4} view The view matrix.
   * @name geo.camera#view
   */
  Object.defineProperty(this, 'view', {
    get: function () {
      return this._view;
    },
    set: function (view) {
      mat4.copy(this._view, view);
      this._update();
    },
    configurable: true
  });

  /**
   * Getter/setter for the view bounds.
   *
   * @property {object} bounds The view bounds.
   * @property {number} bounds.left
   * @property {number} bounds.top
   * @property {number} bounds.right
   * @property {number} bounds.bottom
   * @name geo.camera#bounds
   */
  Object.defineProperty(this, 'bounds', {
    get: function () {
      if (this._bounds === null) {
        this._bounds = this._getBounds();
      }
      return this._bounds;
    },
    set: function (bounds) {
      this._setBounds(bounds);
      this._update();
    }
  });

  /**
   * Getter/setter for the render clipbounds.  Opposite bounds must have
   * different values.  There are independent clipbounds for each projection
   * (parallel and perspective); switching the projection will switch to the
   * clipbounds.  Individual values of the clipbounds can be set either via a
   * command like `camera.clipbounds = {near: 3, far: 1}` or
   * `camera.clipbounds.near = 3`.  In the second example, no check is made to
   * ensure a non-zero volume clipbounds.
   *
   * @property {object} clipbounds The clipbounds for the current projection.
   * @name geo.camera#clipbounds
   */
  Object.defineProperty(this, 'clipbounds', {
    get: function () {
      return this._clipbounds[this._projection];
    },
    set: function (bounds) {
      var clipbounds = this._clipbounds[this._projection];
      bounds = {
        left: bounds.left === undefined ? clipbounds.left : bounds.left,
        right: bounds.right === undefined ? clipbounds.right : bounds.right,
        top: bounds.top === undefined ? clipbounds.top : bounds.top,
        bottom: bounds.bottom === undefined ? clipbounds.bottom : bounds.bottom,
        near: bounds.near === undefined ? clipbounds.near : bounds.near,
        far: bounds.far === undefined ? clipbounds.far : bounds.far
      };
      if (bounds.left === bounds.right) {
        throw new Error('Left and right values must be different');
      }
      if (bounds.top === bounds.bottom) {
        throw new Error('Top and bottom values must be different');
      }
      if (bounds.near === bounds.far) {
        throw new Error('Near and far values must be different');
      }
      this._clipbounds[this._projection] = bounds;
      this._createProj();
      this._update();
    }
  });

  /**
   * Getter for the "display" matrix.  This matrix converts from world
   * coordinates into display coordinates.  Read only.
   *
   * @property {mat4} display The display matrix.
   * @name geo.camera#display
   */
  Object.defineProperty(this, 'display', {
    get: function () {
      if (this._display === null) {
        var b = this._clipbounds[this._projection];
        var mat = util.mat4AsArray();
        mat4.translate(mat, mat, [
          this.viewport.width / 2,
          this.viewport.height / 2,
          0]);
        mat4.scale(mat, mat, [
          this.viewport.width / (b.right - b.left),
          this.viewport.height / (b.bottom - b.top),
          1]);
        mat4.translate(mat, mat, [
          -(b.left + b.right) / 2,
          -(b.top + b.bottom) / 2,
          0]);
        mat4.multiply(mat, mat, this._transform);
        this._display = mat;
      }
      return this._display;
    }
  });

  /**
   * Getter for the "world" matrix.  This matrix converts from display
   * coordinates into world coordinates.  This is the inverse of the "display"
   * matrix.  Read only.
   *
   * @property {mat4} world The world matrix.
   * @name geo.camera#world
   */
  Object.defineProperty(this, 'world', {
    get: function () {
      if (this._world === null) {
        this._world = mat4.invert(
          util.mat4AsArray(),
          this.display
        );
      }
      return this._world;
    }
  });

  /**
   * Getter/setter for the projection type.
   *
   * @property {string} projection The projection type.  One of `parallel` or
   *    `perspective`.
   * @name geo.camera#projection
   * @fires geo.event.camera.projection
   */
  Object.defineProperty(this, 'projection', {
    get: function () {
      return this._projection;
    },
    set: function (type) {
      if (!this.constructor.projection[type]) {
        throw new Error('Unsupported projection type: ' + type);
      }
      if (type !== this._projection) {
        this._projection = type;
        this._createProj();
        this._update();
        this.geoTrigger(geo_event.camera.projection, {
          camera: this,
          projection: type
        });
      }
    }
  });

  /**
   * Getter for the projection matrix.  Read only.
   *
   * @property {mat4} projectionMatrix The projection matrix.
   * @name geo.camera#projectionMatrix
   */
  Object.defineProperty(this, 'projectionMatrix', {
    get: function () {
      return this._proj;
    }
  });

  /**
   * Getter for the transform matrix.  This is the projection multiplied by the
   * view matrix.  Read only.
   *
   * @property {mat4} transform The transform matrix.
   * @name geo.camera#transform
   */
  Object.defineProperty(this, 'transform', {
    get: function () {
      return this._transform;
    }
  });

  /**
   * Getter for the inverse transform matrix.  Read only.
   *
   * @property {mat4} inverse The inverse transform matrix.
   * @name geo.camera#inverse
   */
  Object.defineProperty(this, 'inverse', {
    get: function () {
      return this._inverse;
    }
  });

  /**
   * Getter/setter for the viewport.
   *
   * The viewport consists of a width and height in pixels, plus a left and
   * top offset in pixels.  The offsets are only used to determine if pixel
   * alignment is possible.
   *
   * @property {object} viewport The viewport in pixels.
   * @property {number} viewport.width
   * @property {number} viewport.height
   * @property {number} viewport.top
   * @property {number} viewport.left
   * @name geo.camera#viewport
   * @fires geo.event.camera.viewport
   */
  Object.defineProperty(this, 'viewport', {
    get: function () {
      return {
        width: this._viewport.width,
        height: this._viewport.height,
        left: this._viewport.left,
        top: this._viewport.top
      };
    },
    set: function (viewport) {
      if (!(viewport.width > 0 &&
            viewport.height > 0)) {
        throw new Error('Invalid viewport dimensions');
      }
      if (viewport.width === this._viewport.width &&
          viewport.height === this._viewport.height) {
        return;
      }

      // apply scaling to the view matrix to account for the new aspect ratio
      // without changing the apparent zoom level
      if (this._viewport.width && this._viewport.height) {
        this._scale([
          this._viewport.width / viewport.width,
          this._viewport.height / viewport.height,
          1
        ]);

        // translate by half the difference to keep the center the same
        this._translate([
          (viewport.width - this._viewport.width) / 2,
          (viewport.height - this._viewport.height) / 2,
          0
        ]);
      }

      this._viewport = {
        width: viewport.width,
        height: viewport.height,
        left: viewport.left,
        top: viewport.top
      };
      this._update();
      this.geoTrigger(geo_event.camera.viewport, {
        camera: this,
        viewport: this.viewport
      });
    }
  });

  /**
   * Reset the view matrix to its initial (identity) state.
   * @protected
   * @returns {this} Chainable.
   */
  this._resetView = function () {
    mat4.identity(this._view);
    return this;
  };

  /**
   * Uses `mat4.translate` to translate the camera by the given vector amount.
   * @protected
   * @param {vec3|Array} offset The camera translation vector.
   * @returns {this} Chainable.
   */
  this._translate = function (offset) {
    mat4.translate(this._view, this._view, offset);
    return this;
  };

  /**
   * Uses `mat4.scale` to scale the camera by the given vector amount.
   * @protected
   * @param {vec3|Array} scale The scaling vector.
   * @returns {this} Chainable.
   */
  this._scale = function (scale) {
    mat4.scale(this._view, this._view, scale);
    return this;
  };

  /**
   * Project a vec4 from world space into clipped space [-1, 1] in place.
   * @protected
   * @param {vec4} point The point in world coordinates (mutated).
   * @returns {vec4} The point in clip space coordinates.
   */
  this._worldToClip4 = function (point) {
    return vec4.transformMat4(point, point, this._transform);
  };

  /**
   * Project a vec4 from clipped space into world space in place.
   * @protected
   * @param {vec4} point The point in clipped coordinates (mutated).
   * @returns {vec4} The point in world space coordinates.
   */
  this._clipToWorld4 = function (point) {
    return vec4.transformMat4(point, point, this._inverse);
  };

  /**
   * Apply the camera's projection transform to the given point.
   * @param {vec4} pt a point in clipped coordinates.
   * @returns {vec4} the point in normalized coordinates.
   */
  this.applyProjection = function (pt) {
    var w;
    if (this._projection === 'perspective') {
      w = 1 / (pt[3] || 1);
      pt[0] = w * pt[0];
      pt[1] = w * pt[1];
      pt[2] = w * pt[2];
      pt[3] = w;
    } else {
      pt[3] = 1;
    }
    return pt;
  };

  /**
   * Unapply the camera's projection transform from the given point.
   * @param {vec4} pt a point in normalized coordinates.
   * @returns {vec4} the point in clipped coordinates.
   */
  this.unapplyProjection = function (pt) {
    var w;
    if (this._projection === 'perspective') {
      w = pt[3] || 1;
      pt[0] = w * pt[0];
      pt[1] = w * pt[1];
      pt[2] = w * pt[2];
      pt[3] = w;
    } else {
      pt[3] = 1;
    }
    return pt;
  };

  /**
   * Project a vector from world space into viewport (display) space.  The
   * resulting vector always has the last component (`w`) equal to 1.
   *
   * @param {vec3|vec4} point The point in world coordinates.
   * @returns {vec4} The point in display coordinates.
   */
  this.worldToDisplay4 = function (point) {
    point = [point[0], point[1], point[2] || 0, point[3] || 1];
    point = vec4.transformMat4(point, point, this.display);
    if (point[3] && point[3] !== 1) {
      point = [point[0] / point[3], point[1] / point[3], point[2] / point[3], 1];
    }
    return point;
  };

  /**
   * Project a vector from viewport (display) space into world space.  The
   * resulting vector always has the last component (`w`) equal to 1.
   *
   * @param {vec3|vec4} point The point in display coordinates.
   * @returns {vec4} The point in world space coordinates.
   */
  this.displayToWorld4 = function (point) {
    point = [point[0], point[1], point[2] || 0, point[3] || 1];
    point = vec4.transformMat4(point, point, this.world);
    if (point[3] && point[3] !== 1) {
      point = [point[0] / point[3], point[1] / point[3], point[2] / point[3], 1];
    }
    return point;
  };

  /**
   * Project a 2D point object from world space into viewport space.  `z` is
   * set to `-this.clipbounds.near` to scale with the clip space.
   *
   * @param {object} point The point in world coordinates.
   * @param {number} point.x
   * @param {number} point.y
   * @returns {object} The point in display coordinates.
   */
  this.worldToDisplay = function (point) {
    var b = this._clipbounds[this._projection];
    point = this.worldToDisplay4([point.x, point.y, -b.near]);
    return {x: point[0], y: point[1]};
  };

  /**
   * Project a 2D point object from viewport space into world space.  `z` is
   * set to -1 to scale with the clip space.
   *
   * @param {object} point The point in display coordinates.
   * @param {number} point.x
   * @param {number} point.y
   * @returns {object} The point in world coordinates.
   */
  this.displayToWorld = function (point) {
    point = this.displayToWorld4([point.x, point.y, -1]);
    return {x: point[0], y: point[1]};
  };

  /**
   * Calculate the current bounds in world coordinates from the
   * current view matrix.  This computes a matrix vector multiplication
   * so the result is cached for public facing methods.
   *
   * @protected
   * @returns {object} bounds object.
   */
  this._getBounds = function () {
    var ul, ur, ll, lr, bds = {};

    // get corners
    ul = this.displayToWorld({x: 0, y: 0});
    ur = this.displayToWorld({x: this._viewport.width, y: 0});
    ll = this.displayToWorld({x: 0, y: this._viewport.height});
    lr = this.displayToWorld({x: this._viewport.width, y: this._viewport.height});

    bds.left = Math.min(ul.x, ur.x, ll.x, lr.x);
    bds.bottom = Math.min(ul.y, ur.y, ll.y, lr.y);
    bds.right = Math.max(ul.x, ur.x, ll.x, lr.x);
    bds.top = Math.max(ul.y, ur.y, ll.y, lr.y);

    return bds;
  };

  /**
   * Sets the view matrix so that the given world bounds
   * are in view.  To account for the viewport aspect ratio,
   * the resulting bounds may be larger in width or height than
   * the requested bound, but should be centered in the frame.
   *
   * @protected
   * @param {object} bounds
   * @param {number} bounds.left
   * @param {number} bounds.right
   * @param {number} bounds.bottom
   * @param {number} bounds.top
   * @param {number?} bounds.near Currently ignored.
   * @param {number?} bounds.far Currently ignored.
   * @returns {this} Chainable.
   */
  this._setBounds = function (bounds) {
    var size = {
      width: bounds.right - bounds.left,
      height: bounds.top - bounds.bottom
    };
    var center = {
      x: (bounds.left + bounds.right) / 2,
      y: (bounds.bottom + bounds.top) / 2
    };

    this._viewFromCenterSizeRotation(center, size, 0);
    return this;
  };

  /**
   * Sets the view matrix so that the given world center is centered, at
   * least a certain width and height are visible, and a rotation is applied.
   * The resulting bounds may be larger in width or height than the values if
   * the viewport is a different aspect ratio.
   *
   * @protected
   * @param {object} center Center of the view in gcs coordinates.
   * @param {number} center.x
   * @param {number} center.y
   * @param {object} size Minimum size of the view in gcs units.
   * @param {number} size.width
   * @param {number} size.height
   * @param {number} rotation in clockwise radians.  Optional.
   * @returns {this} Chainable.
   */
  this._viewFromCenterSizeRotation = function (center, size, rotation) {
    var translate = util.vec3AsArray(),
        scale = util.vec3AsArray(),
        c_ar, v_ar, w, h;

    // reset view to the identity
    this._resetView();

    w = Math.abs(size.width);
    h = Math.abs(size.height);
    c_ar = w / h;
    v_ar = this._viewport.width / this._viewport.height;

    if (c_ar >= v_ar) {
      // grow camera bounds vertically
      h = w / v_ar;
      scale[0] = 2 / w;
      scale[1] = 2 / h;
    } else {
      // grow bounds horizontally
      w = h * v_ar;
      scale[0] = 2 / w;
      scale[1] = 2 / h;
    }

    scale[2] = 1;
    this._scale(scale);

    if (rotation) {
      this._rotate(rotation);
    }

    // translate to the new center.
    translate[0] = -center.x;
    translate[1] = -center.y;
    translate[2] = 0;

    this._translate(translate);

    return this;
  };

  /**
   * Sets the view matrix so that the given world center is centered, at
   * least a certain width and height are visible, and a rotation is applied.
   * The resulting bounds may be larger in width or height than the values if
   * the viewport is a different aspect ratio.
   *
   * @param {object} center Center of the view in gcs coordinates.
   * @param {number} center.x
   * @param {number} center.y
   * @param {object} size Minimum size of the view in gcs units.
   * @param {number} size.width
   * @param {number} size.height
   * @param {number} rotation in clockwise radians.  Optional.
   * @returns {this} Chainable.
   */
  this.viewFromCenterSizeRotation = function (center, size, rotation) {
    this._viewFromCenterSizeRotation(center, size, rotation);
    this._update();
    return this;
  };

  /**
   * Pans the view matrix by the given amount.
   *
   * @param {object} offset The delta in world space coordinates.
   * @param {number} offset.x
   * @param {number} offset.y
   * @param {number} [offset.z=0]
   * @returns {this} Chainable.
   */
  this.pan = function (offset) {
    if (!offset.x && !offset.y && !offset.z) {
      return;
    }
    this._translate([
      offset.x,
      offset.y,
      offset.z || 0
    ]);
    this._update();
    return this;
  };

  /**
   * Zooms the view matrix by the given amount.
   *
   * @param {number} zoom The zoom scale to apply
   * @returns {this} Chainable.
   */
  this.zoom = function (zoom) {
    if (zoom === 1) {
      return;
    }
    mat4.scale(this._view, this._view, [
      zoom,
      zoom,
      zoom
    ]);
    this._update();
    return this;
  };

  /**
   * Rotate the view matrix by the given amount.
   *
   * @param {number} rotation Counter-clockwise rotation angle in radians.
   * @param {object} center Center of rotation in world space coordinates.
   * @param {vec3} [axis=[0, 0, -1]] axis of rotation.
   * @returns {this} Chainable.
   */
  this._rotate = function (rotation, center, axis) {
    if (!rotation) {
      return;
    }
    axis = axis || [0, 0, -1];
    if (!center) {
      center = [0, 0, 0];
    } else if (center.x !== undefined) {
      center = [center.x || 0, center.y || 0, center.z || 0];
    }
    var invcenter = [-center[0], -center[1], -center[2]];
    mat4.translate(this._view, this._view, center);
    mat4.rotate(this._view, this._view, rotation, axis);
    mat4.translate(this._view, this._view, invcenter);
    return this;
  };

  /**
   * Returns a CSS transform that converts (by default) from world coordinates
   * into display coordinates.  This allows users of this module to position
   * elements using world coordinates directly inside DOM elements.  This
   * expects that the transform-origin is 0 0.
   *
   * @param {string} [transform='display'] The transform to return.  One of
   *   `display` or `world`.
   * @returns {string} The css transform string.
   */
  this.css = function (transform) {
    var m;
    switch ((transform || '').toLowerCase()) {
      case 'display':
      case '':
        m = this.display;
        break;
      case 'world':
        m = this.world;
        break;
      default:
        throw new Error('Unknown transform ' + transform);
    }
    return camera.css(m);
  };

  /**
   * Represent a glmatrix as a pretty-printed string.
   * @param {mat4} mat A 4 x 4 matrix.
   * @param {number} [prec] The number of decimal places.
   * @returns {string}
   */
  this.ppMatrix = function (mat, prec) {
    var t = mat;
    prec = prec || 2;
    function f(i) {
      var d = t[i], s = d.toExponential(prec);
      if (d >= 0) {
        s = ' ' + s;
      }
      return s;
    }
    return [
      [f(0), f(4), f(8), f(12)].join(' '),
      [f(1), f(5), f(9), f(13)].join(' '),
      [f(2), f(6), f(10), f(14)].join(' '),
      [f(3), f(7), f(11), f(15)].join(' ')
    ].join('\n');
  };

  /**
   * Pretty print the transform matrix.
   * @returns {string} A string representation of the matrix.
   */
  this.toString = function () {
    return this.ppMatrix(this._transform);
  };

  /**
   * Return a debugging string of the current camera state.
   * @returns {string} A string with the camera state.
   */
  this.debug = function () {
    return [
      'bounds',
      JSON.stringify(this.bounds),
      'view:',
      this.ppMatrix(this._view),
      'projection:',
      this.ppMatrix(this._proj),
      'transform:',
      this.ppMatrix(this._transform)
    ].join('\n');
  };

  /**
   * Represent the value of the camera as its transform matrix.
   * @returns {mat4} The transform matrix.
   */
  this.valueOf = function () {
    return this._transform;
  };

  this._clipbounds = this.constructor.clipbounds;
  // initialize the view matrix
  this._resetView();

  // set up the projection matrix
  this.projection = spec.projection || 'parallel';

  // initialize the viewport
  if (spec.viewport) {
    this.viewport = spec.viewport;
  }

  // trigger an initial update to set up the camera state
  this._update();

  return this;
};

/**
 * Supported projection types.
 * @enum {boolean}
 */
camera.projection = {
  perspective: true,
  parallel: true
};

/**
 * Default camera clipping bounds.  Some features and renderers may rely on the
 * far clip value being more positive than the near clip value.
 * @enum {number}
 */
camera.clipbounds = {
  perspective: {
    left: -1,
    right: 1,
    top: 1,
    bottom: -1,
    far: 2000,
    near: 0.01
  },
  parallel: {
    left: -1,
    right: 1,
    top: 1,
    bottom: -1,
    far: -1,
    near: 1
  }
};

/**
 * Output a mat4 as a css transform.  This expects that the transform-origin is
 * 0 0.
 *
 * @variation 2
 * @param {mat4} t A matrix transform.
 * @returns {string} A css transform string.
 */
camera.css = function (t) {
  return 'matrix3d(' +
      t.map(function (val) {
        /* Format each value with a certain precision, but don't use scientific
         * notation or keep needless trailing zeroes. */
        val = (+val).toPrecision(15);
        if (val.indexOf('e') >= 0) {
          val = (+val).toString();
        } else if (val.indexOf('.') >= 0) {
          val = val.replace(/(\.|)0+$/, '');
        }
        return val;
      }).join(',') +
    ')';
};

// expose the vector and matrix functions for convenience
camera.vec3 = vec3;
camera.mat3 = mat3;
camera.vec4 = vec4;
camera.mat4 = mat4;

inherit(camera, object);
module.exports = camera;