graphics/arc.js

'use strict';

/**
 * @namespace Arc
 */

var Thing = require('./thing.js');
var graphicsUtils = require('./graphics-utils.js');

/* The angles are always stored in radians.
 * Based on the unit (by default, degrees), the angles might be converted
 * when calling getters or setters on the start/end angles.
 */

/**
 * @class Arc
 * @augments Thing
 * @param {number} radius - Desired radius of the arc.
 * @param {number} startAngle - Start angle of the arc.
 * @param {number} endAngle - End angle of the arc.
 * @param {number} angleUnit - Integer representing unit.
 * Degrees ===0, Radians ===1
 */
function Arc(radius, startAngle, endAngle, angleUnit) {
    if (arguments.length !== 4) {
        throw new Error('You should pass exactly 4 arguments to <span ' +
            'class="code">new Arc(raduis, startAngle, endAngle, ' +
            'angleUnit)</span>');
    }
    if (typeof radius !== 'number' || !isFinite(radius)) {
        throw new TypeError('Invalid value for <span class="code">radius' +
            '</span>. Make sure you are passing finite numbers to <span ' +
            'class="code">new Arc(raduis, startAngle, endAngle, ' +
            'angleUnit)</span>');
    }
    if (typeof startAngle !== 'number' || !isFinite(startAngle)) {
        throw new TypeError('Invalid value for <span class="code">startAngle' +
            '</span>. Make sure you are passing finite numbers to <span ' +
            'class="code">new Arc(raduis, startAngle, endAngle, ' +
            'angleUnit)</span>');
    }
    if (typeof endAngle !== 'number' || !isFinite(endAngle)) {
        throw new TypeError('Invalid value for <span class="code">endAngle' +
            '</span>. Make sure you are passing finite numbers to <span ' +
            'class="code">new Arc(raduis, startAngle, endAngle, ' +
            'angleUnit)</span>');
    }
    if (typeof angleUnit !== 'number' || !isFinite(angleUnit)) {
        throw new TypeError('Invalid value for <span class="code">angleUnit' +
            '</span>. Make sure you are passing finite numbers to <span ' +
            'class="code">new Arc(raduis, startAngle, endAngle, ' +
            'angleUnit)</span>');
    }

    Thing.call(this);

    this.radius = radius;
    this.angleUnit = angleUnit == Arc.DEGREES ? Arc.DEGREES : Arc.RADIANS;

    this.counterclockwise = Arc.COUNTER_CLOCKWISE;
    this.type = 'Arc';

    if (this.angleUnit == Arc.DEGREES) {
        startAngle = degreesToRadians(startAngle);
        endAngle = degreesToRadians(endAngle);
    }

    this.startAngle = startAngle;
    this.endAngle = endAngle;
}

Arc.prototype = new Thing();
Arc.prototype.constructor = Arc;

// Constants for Arcs.
Arc.COUNTER_CLOCKWISE = true;
Arc.CLOCKWISE = false;
Arc.DEGREES = 0;
Arc.RADIANS = 1;

/**
 * Draws the arc in the canvas.
 *
 * @param {CodeHSGraphics} __graphics__ - Instance of the __graphics__ module.
 */
Arc.prototype.draw = function(__graphics__) {
    var context = __graphics__.getContext();
    // http://stackoverflow.com/questions/17125632/html5-canvas-rotate-object-without-moving-coordinates
    context.save();
    context.beginPath();
    context.translate(this.x, this.y);
    context.rotate(this.rotation);
    context.arc(0, 0, this.radius, prepareAngle(this.startAngle),
        prepareAngle(this.endAngle), this.counterclockwise);
    context.lineTo(0, 0);

    if (this.hasBorder) {
        context.lineWidth = this.lineWidth;
        context.strokeStyle = this.stroke.toString();
        context.stroke();
    }

    context.fillStyle = this.color.toString();
    context.fill();
    context.restore();
};

/* Sets the starting angle of the arc.
 * Note: All angles are stored in radians, so we must first convert
 * to radians (if the unit is degrees) before storing the new angle.
 * @param {number} angle - The desired start angle of the arc.
 */
Arc.prototype.setStartAngle = function(angle) {
    if (arguments.length !== 1) {
        throw new Error('You should pass exactly 1 argument to ' +
            '<span class="code">setStartAngle</span>');
    }
    if (typeof angle !== 'number' || !isFinite(angle)) {
        throw new Error('Invalid value passed to <span class="code">' +
            'setStartAngle</span>. Make sure you are passing a ' +
            'finite number.');
    }
    if (this.angleUnit == Arc.DEGREES) {
        angle = degreesToRadians(angle);
    }
    this.startAngle = angle;
};

/* Sets the ending angle of the arc.
 * Note: All angles are stored in radians, so we must first convert
 * to radians (if the unit is degrees) before storing the new angle.
 * @param {number} angle - The desired end angle of the arc.
 */
Arc.prototype.setEndAngle = function(angle) {
    if (arguments.length !== 1) {
        throw new Error('You should pass exactly 1 argument to ' +
            '<span class="code">setEndAngle</span>');
    }
    if (typeof angle !== 'number' || !isFinite(angle)) {
        throw new Error('Invalid value passed to <span class="code">' +
            'setEndAngle</span>. Make sure you are passing a ' +
            'finite number.');
    }
    if (this.angleUnit == Arc.DEGREES) {
        angle = degreesToRadians(angle);
    }
    this.endAngle = angle;
};

/* Gets the starting angle of the arc.
 * @returns {number} The start angle of the arc.
 */
Arc.prototype.getStartAngle = function() {
    var angle = this.startAngle;
    if (this.angleUnit == Arc.DEGREES) {
        angle = radiansToDegrees(this.startAngle);
    }

    return Math.round(angle);
};

/* Gets the starting angle of the arc.
 * @returns {number} The start angle of the arc.
 */
Arc.prototype.getEndAngle = function() {
    var angle = this.endAngle;
    if (this.angleUnit == Arc.DEGREES) {
        angle = radiansToDegrees(this.endAngle);
    }
    return Math.round(angle);
};

/* Gets the direction of the arc (CW or CCW).
 * @param {boolean} val - Boolean representing CW or CCW.
 * `True` sets counterclockwise to true.
 */
Arc.prototype.setDirection = function(val) {
    if (arguments.length !== 1) {
        throw new Error('You should pass exactly 1 argument to ' +
            '<span class="code">setDirection</span>');
    }
    if (typeof val !== 'boolean') {
        throw new Error('Invalid value passed to <span class="code">' +
            'setDirection</span>. Make sure you are passing a ' +
            'boolean value. true for counterclockwise, false for clockwise.');
    }
    this.counterclockwise = val;
};

/**
 * Checks if a given point is contained within the arc. We always fill the arc
 * so it is technically a segment of the circle
 *
 * @param {number} x - x coordinate of the point being tested.
 * @param {number} y - y coordinate of the point being tested.
 */
Arc.prototype.containsPoint = function(x, y) {
    // First check whether the point is in the circle
    var dist = graphicsUtils.getDistance(this.x, this.y, x, y);
    if (dist > this.radius) {
        return false;
    }

    // Get vector/ angle for the point
    var vx = x - this.x;
    var vy = this.y - y;
    var theta = Math.atan(vy / vx)

    // Adjust the arctan based on the quadran the point is in using the
    // position of the arc as the origin
    // Quadrant II and III
    if (vx < 0) {
        theta += Math.PI;
    // Quadrant IV
    } else if (vy < 0) {
        theta += 2 * Math.PI
    }

    // Check whether angle is between start and end, take into account fill
    // direction
    var betweenCCW = theta >= this.startAngle && theta <= this.endAngle;
    if (this.counterclockwise) {
        return betweenCCW;
    } else {
        return !betweenCCW;
    }
};

/**
 * Prepares an angle to be drawn.
 *
 * @memberof Arc
 * @param {number} angle - The angle to be prepared.
 * @returns {number} The prepared angle.
 */
var prepareAngle = function(angle) {
    // First, convert to degrees (may lose some accuracy)
    angle = radiansToDegrees(angle);
    angle = Math.round(angle);

    // The canvas arc angles go clockwise, but we want them
    // to go counterclockwise (like the unit circle). Here,
    // we adjust the angle for that.
    angle = (360 - angle) % 360;
    angle = degreesToRadians(angle);

    return angle;
};

/**
 * Helper to convert degrees to radians.
 *
 * @memberof Arc
 * @param {number} angleInDegrees - The angle represented as degrees.
 * @returns {number} The angle represented as radians.
 */
var degreesToRadians = function(angleInDegrees) {
    return angleInDegrees / 180 * Math.PI;
};

/**
 * Helper to convert radians to degrees.
 *
 * @memberof Arc
 * @param {number} angleInRadians - The angle represented as radians.
 * @returns {number} The angle represented as degrees.
 */
var radiansToDegrees = function(angleInRadians) {
    return angleInRadians / Math.PI * 180;
};

module.exports = Arc;