Home Reference Source

source/modules/Polygon.js

const Body = require('./Body')

/**
 * A polygon used to detect collisions
 * @class
 */
class Polygon extends Body {
  /**
   * @constructor
   * @param {Number} [x = 0] The starting X coordinate
   * @param {Number} [y = 0] The starting Y coordinate
   * @param {Array<Number[]>} [points = []] An array of coordinate pairs making up the polygon - [[x1, y1], [x2, y2], ...]
   * @param {Number} [angle = 0] The starting rotation in radians
   * @param {Number} [scale_x = 1] The starting scale along the X axis
   * @param {Number} [scale_y = 1] The starting scale long the Y axis
   * @param {Number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions
   */
  constructor (x = 0, y = 0, points = [], angle = 0, scale_x = 1, scale_y = 1, padding = 0) {
    super(x, y, padding)

    /**
     * @desc The angle of the body in radians
     * @type {Number}
     */
    this.angle = angle

    /**
     * @desc The scale of the body along the X axis
     * @type {Number}
     */
    this.scale_x = scale_x

    /**
     * @desc The scale of the body along the Y axis
     * @type {Number}
     */
    this.scale_y = scale_y

    /** @private */
    this._polygon = true

    /** @private */
    this._x = x

    /** @private */
    this._y = y

    /** @private */
    this._angle = angle

    /** @private */
    this._scale_x = scale_x

    /** @private */
    this._scale_y = scale_y

    /** @private */
    this._min_x = 0

    /** @private */
    this._min_y = 0

    /** @private */
    this._max_x = 0

    /** @private */
    this._max_y = 0

    /** @private */
    this._points = null

    /** @private */
    this._coords = null

    /** @private */
    this._edges = null

    /** @private */
    this._normals = null

    /** @private */
    this._dirty_coords = true

    /** @private */
    this._dirty_normals = true

    Polygon.prototype.setPoints.call(this, points)
  }

  /**
   * Draws the polygon to a CanvasRenderingContext2D's current path
   * @param {CanvasRenderingContext2D} context The context to add the shape to
   */
  draw (context) {
    if (
      this._dirty_coords ||
      this.x !== this._x ||
      this.y !== this._y ||
      this.angle !== this._angle ||
      this.scale_x !== this._scale_x ||
      this.scale_y !== this._scale_y
    ) {
      this._calculateCoords()
    }

    const coords = this._coords

    if (coords.length === 2) {
      context.moveTo(coords[0], coords[1])
      context.arc(coords[0], coords[1], 1, 0, Math.PI * 2)
    } else {
      context.moveTo(coords[0], coords[1])

      for (let i = 2; i < coords.length; i += 2) {
        context.lineTo(coords[i], coords[i + 1])
      }

      if (coords.length > 4) {
        context.lineTo(coords[0], coords[1])
      }
    }
  }

  /**
   * Sets the points making up the polygon. It's important to use this function when changing the polygon's shape to ensure internal data is also updated.
   * @param {Array<Number[]>} new_points An array of coordinate pairs making up the polygon - [[x1, y1], [x2, y2], ...]
   */
  setPoints (new_points) {
    const count = new_points.length

    this._points = new Float64Array(count * 2)
    this._coords = new Float64Array(count * 2)
    this._edges = new Float64Array(count * 2)
    this._normals = new Float64Array(count * 2)

    const points = this._points

    for (let i = 0, ix = 0, iy = 1; i < count; ++i, ix += 2, iy += 2) {
      const new_point = new_points[i]

      points[ix] = new_point[0]
      points[iy] = new_point[1]
    }

    this._dirty_coords = true
  }

  /**
   * Calculates and caches the polygon's world coordinates based on its points, angle, and scale
   */
  _calculateCoords () {
    const x = this.x
    const y = this.y
    const angle = this.angle
    const scale_x = this.scale_x
    const scale_y = this.scale_y
    const points = this._points
    const coords = this._coords
    const count = points.length

    let min_x
    let max_x
    let min_y
    let max_y

    for (let ix = 0, iy = 1; ix < count; ix += 2, iy += 2) {
      let coord_x = points[ix] * scale_x
      let coord_y = points[iy] * scale_y

      if (angle) {
        const cos = Math.cos(angle)
        const sin = Math.sin(angle)
        const tmp_x = coord_x
        const tmp_y = coord_y

        coord_x = tmp_x * cos - tmp_y * sin
        coord_y = tmp_x * sin + tmp_y * cos
      }

      coord_x += x
      coord_y += y

      coords[ix] = coord_x
      coords[iy] = coord_y

      if (ix === 0) {
        min_x = max_x = coord_x
        min_y = max_y = coord_y
      } else {
        if (coord_x < min_x) {
          min_x = coord_x
        } else if (coord_x > max_x) {
          max_x = coord_x
        }

        if (coord_y < min_y) {
          min_y = coord_y
        } else if (coord_y > max_y) {
          max_y = coord_y
        }
      }
    }

    this._x = x
    this._y = y
    this._angle = angle
    this._scale_x = scale_x
    this._scale_y = scale_y
    this._min_x = min_x
    this._min_y = min_y
    this._max_x = max_x
    this._max_y = max_y
    this._dirty_coords = false
    this._dirty_normals = true
  }

  /**
   * Calculates the normals and edges of the polygon's sides
   */
  _calculateNormals () {
    const coords = this._coords
    const edges = this._edges
    const normals = this._normals
    const count = coords.length

    for (let ix = 0, iy = 1; ix < count; ix += 2, iy += 2) {
      const next = ix + 2 < count ? ix + 2 : 0
      const x = coords[next] - coords[ix]
      const y = coords[next + 1] - coords[iy]
      const length = x || y ? Math.sqrt(x * x + y * y) : 0

      edges[ix] = x
      edges[iy] = y
      normals[ix] = length ? y / length : 0
      normals[iy] = length ? -x / length : 0
    }

    this._dirty_normals = false
  }
};

module.exports = Polygon

module.exports.default = module.exports