graphics/polygon.js

import Thing from './thing.js';

/**
 * A polygon is a shape with any number of points, and will
 * be drawn as a continuous shape contained by those points.
 * @extends Thing
 */
class Polygon extends Thing {
    type = 'Polygon';

    /**
     * Constructs a new Polygon.
     * The Polygon constructor takes no arguments, and only prepares a Polygon to have points
     * added later with {@link addPoint}.
     * @constructor
     */
    constructor() {
        super();
        if (arguments.length !== 0) {
            throw new Error('You should pass exactly 0 arguments to `new Polygon()`');
        }
        /**
         * An array of points in the Polygon.
         * @example
         * const p = new Polygon();
         * for (let i = 0; i < p.points.length; i++) {
         *     println(p.points[i]);
         * }
         * @type {Array.<{x: number, y: number}>}
         * @public
         */
        this.points = [];
        this.width = 0; // max x-distance of points in the polygon
        this.height = 0; // max y-distance of points in the polygon
    }

    /**
     * Draws the polygon in the canvas.
     *
     * @private
     * @param {CanvasRenderingContext2D} context - Context to draw on.
     */
    draw(context) {
        if (this.points.length === 0) {
            return;
        }

        super.draw(context, () => {
            context.save();
            context.translate(-this.x, -this.y);
            context.beginPath();
            const first = this.points[0];
            let current;
            context.moveTo(first.x, first.y);
            for (let i = 1; i < this.points.length; i++) {
                current = this.points[i];
                context.lineTo(current.x, current.y);
            }
            context.closePath();
            context.restore();
        });
    }

    /**
     * Checks if the coordinates are contained in the Polygon.
     * @alias Polygon#containsPoint
     * @example
     * const p = new Polygon();
     * p.addPoint(10, 10);
     * p.addPoint(20, 10);
     * p.addPoint(15, 15);
     * if (p.containsPoint(12, 12)) {
     *     p.setColor(Color.RED);
     * }
     *
     * @param {number} x - The x coordinate of the point being tested.
     * @param {number} y - The y coordinate of the point being tested.
     * @returns {boolean}
     */
    _containsPoint(x, y) {
        x += this.width * this.anchor.horizontal;
        y += this.height * this.anchor.vertical;
        // https://www.eecs.umich.edu/courses/eecs380/HANDOUTS/PROJ2/InsidePoly.html
        // solution 3 from above
        let previousOrientation = -1;
        let x1, x2, y1, y2;
        for (let i = 0; i < this.points.length; i++) {
            x1 = this.points[i].x;
            y1 = this.points[i].y;
            x2 = this.points[(i + 1) % this.points.length].x;
            y2 = this.points[(i + 1) % this.points.length].y;
            let orientation = (y - y1) * (x2 - x1) - (x - x1) * (y2 - y1) <= 0;
            if (previousOrientation < 0) {
                previousOrientation = orientation;
            } else {
                if (previousOrientation !== orientation) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Gets the width of the Polygon.
     * The width is the greatest distance between two points in the Polygon along the x axis.
     * @example
     * const p = new Polygon();
     * p.addPoint(0, 50);
     * p.addPoint(200, 30);
     * p.getWidth() === 200;
     *
     * @returns {number} Width of the rectangle.
     */
    getWidth() {
        return this.width;
    }

    /**
     * Gets the width of the Polygon.
     * The width is the greatest distance between two points in the Polygon along the x axis.
     * @example
     * const p = new Polygon();
     * p.addPoint(50, 0);
     * p.addPoint(30, 100);
     * p.addPoint(10, 200);
     * p.getHeight() === 200;
     *
     * @returns {number} height of the rectangle.
     */
    getHeight() {
        return this.height;
    }

    /**
     * Adds a vertex to the polygon.
     * This also updates the width and height of the Polygon, expanding the width and height
     * to the maximum distance between two points along the x and y axes, respectively.
     * @example
     * const p = new Polygon();
     * p.addPoint(10, 10);
     *
     * @param {number} x - The x coordinate of the desired new vertex.
     * @param {number} y - The y coordinate of the desired new vertex.
     */
    addPoint(x, y) {
        if (arguments.length !== 2) {
            throw new Error('You should pass exactly 2 arguments to `addPoint(x, y)`');
        }
        if (typeof x !== 'number' || !isFinite(x)) {
            throw new TypeError(
                'Invalid value for x-coordinate. Make sure you are passing finite numbers to `addPoint(x, y)`.'
            );
        }
        if (typeof y !== 'number' || !isFinite(y)) {
            throw new TypeError(
                'Invalid value for y-coordinate. Make sure you are passing finite numbers to `addPoint(x, y)`.'
            );
        }

        this.points.push({ x: x, y: y });
        for (let i = 0; i < this.points.length; i++) {
            if (Math.abs(x - this.points[i].x) > this.width) {
                this.width = Math.abs(x - this.points[i].x);
            }
            if (Math.abs(y - this.points[i].y) > this.height) {
                this.height = Math.abs(y - this.points[i].y);
            }
        }
    }

    /**
     * Moves the entire polygon.
     * This shifts each vertex in the Polygon by dx, dy.
     * @example
     * const p = new Polygon();
     * p.addPoint(20, 20);
     * p.move(10, 10);
     * p.points[0] === {x: 30, y: 30};
     *
     * @param {number} dx - The change in x coordinate of all starting and ending points.
     * @param {number} dy - The change in y coordinate of all starting and ending points.
     */
    move(dx, dy) {
        if (arguments.length !== 2) {
            throw new Error('You should pass exactly 2 arguments to `move(dx, dy).`');
        }
        if (typeof dx !== 'number' || !isFinite(dx)) {
            throw new TypeError(
                'Invalid number passed for `dx`. Make sure you are passing finite numbers to `move(dx, dy)`.'
            );
        }
        if (typeof dy !== 'number' || !isFinite(dy)) {
            throw new TypeError(
                'Invalid number passed for `dy`. Make sure you are passing finite numbers to `move(dx, dy)`.'
            );
        }

        for (let i = 0; i < this.points.length; i++) {
            this.points[i].x += dx;
            this.points[i].y += dy;
        }
        this.x += dx;
        this.y += dy;
    }

    /**
     * Set the position of the polygon by moving all of its points.
     * @param {number} x
     * @param {number} y
     */
    setPosition(x, y) {
        const dx = x - this.x;
        const dy = y - this.y;
        this.move(dx, dy);
    }

    /**
     * Polygons manually calculate their bounds with their own implementation of _updateBounds
     * (rather than the implementation in the Thing superclass) because Polygon's can have
     * negative points which draw to the left of their x value or above their y value.
     * @private
     */
    _updateBounds() {
        let minX = Infinity;
        let maxX = -Infinity;
        let minY = Infinity;
        let maxY = -Infinity;
        this.points.forEach(({ x, y }) => {
            minX = Math.min(minX, x);
            maxX = Math.max(maxX, x);
            minY = Math.min(minY, y);
            maxY = Math.max(maxY, y);
        });
        const width = maxX - minX;
        const height = maxY - minY;
        this.bounds = {
            left: minX - this.anchor.horizontal * width,
            right: maxX - this.anchor.horizontal * width,
            top: minY - this.anchor.vertical * height,
            bottom: maxY - this.anchor.vertical * height,
        };
        this._boundsInvalidated = false;
        this._lastCalculatedBoundsID++;
    }
}

export default Polygon;