Source: Arrow.js

/**
 * Represents one Arrow with: position, time lived, time till death, length, forward speed, width
 *
 * @alias Arrow
 * @constructor
 *
 * @param {TrajectoryHelper} trajectoryHelper TrajectoryHelper to be used for this arrow
 */
function Arrow(trajectoryHelper) {
    this._trajectoryHelper = trajectoryHelper;

    /**
     * List of positions over time
     * @member
     * @type {list}
     * @readonly
     */
    this.position = []; //Position list based on timestep

    /**
     * List of head/tail positions and length over time
     * @member
     * @type {list}
     * @readonly
     */
    this.timeData = []; //Stores timedependent values: length, head, tail

    /**
     * Width of this arrow
     * @member
     * @type {number}
     * @readonly
     */
    this.width = 0;

    /**
     * Timestep when this arrow was born
     * @member
     * @type {int}
     * @readonly
     */
    this.timeBorn = 0;

    /**
     * Timestep when this arrow dies
     * @member
     * @type {int}
     * @readonly
     */
    this.timeDeath = 0;

    this.polygon = []; //Saves the polygons of timesteps for speed up
}

/**
 * Returns a polygon that is defined by the linesegment
 * from tail -> position -> head with the width of this.width
 * @param {int} time time for calculation
 * @param {float} [border] border to add around polygon, default 0
 * @return {SAT.Polygon} Polygon approximation
 */
Arrow.prototype.getPolygon = function(time, border){
    border = border | 0;
    //If we have a polygon cached, return that
    if (this.polygon[time] !== undefined &&
        this.polygon[time][border] !== undefined){
        return this.polygon[time][border];
    }

    var dHead = Point.sub(this.timeData[time].head,this.position[time]);
    var dTail = Point.sub(this.position[time],this.timeData[time].tail);
    // Calculate normals
    var normalHead = new Point(-dHead.y,dHead.x);
    var normalTail = new Point(-dTail.y,dTail.x);
    // Calculate interpolated normal for position
    var normalPosition = new Point((normalHead.x+normalTail.x)/2, (normalHead.y+normalTail.y)/2);
    // Normalize
    normalHead.normalize();
    normalTail.normalize();
    normalPosition.normalize();
    // Add arrow width and border offset to head/position/tail offset
    normalHead = Point.add(normalHead.clone().scale(this.width/8), normalHead.scale(border));
    normalPosition = Point.add(normalPosition.clone().scale(this.width/4), normalPosition.scale(border));
    normalTail = Point.add(normalTail.clone().scale(this.width/2), normalTail.scale(border));
    // Create points
    var headPoint1 = Point.add(this.timeData[time].head, normalHead);
    var headPoint2 = Point.add(this.timeData[time].head, normalHead.reverse());
    var positionPoint1 = Point.add(this.position[time], normalPosition);
    var positionPoint2 = Point.add(this.position[time], normalPosition.reverse());
    var tailPoint1 = Point.add(this.timeData[time].tail, normalTail);
    var tailPoint2 = Point.add(this.timeData[time].tail, normalTail.reverse());

    return new SAT.Polygon(new SAT.Vector(), [
            headPoint1, positionPoint1, tailPoint1, tailPoint2, positionPoint2, headPoint2
    ]);
}

/**
 * Returns true if distance is kept.
 * @param {Arrow} other The arrow to calculate the distance to
 * @param {int} time time for calculation
 * @param {float} dist the dist to check for
 * @return {boolean} true if distance is greater than dist
 */
Arrow.prototype.distance = function(other, time, dist){

	// three points for line segments of this arrow
	var hp1 = this.timeData[time].head;
	var p1 = this.position[time];
	var tp1 = this.timeData[time].tail;

	// three points for line segments of other arrow
	var hp2 = other.timeData[time].head;
	var p2 = other.position[time];
	var tp2 = other.timeData[time].tail;

	// calculate the distances from every point of an arrow to every
	// line segment of the other arrow
	//
	// line segments are:
	//   segment 1: from headpoint to point
	//   segment 2: from point to tailpoint

	minDist = Number.MAX_SAFE_INTEGER;

	// distances for head point 1
	minDist = Math.min(this._distancePointLine(hp1, hp2, p2), minDist);
	minDist = Math.min(this._distancePointLine(hp1, p2, tp2), minDist);

	// distances for point 1
	minDist = Math.min(this._distancePointLine(p1, hp2, p2), minDist);
	minDist = Math.min(this._distancePointLine(p1, p2, tp2), minDist);

	// distances for tail point 1
	minDist = Math.min(this._distancePointLine(tp1, hp2, p2), minDist);
	minDist = Math.min(this._distancePointLine(tp1, p2, tp2), minDist);

	// distances for head point 2
	minDist = Math.min(this._distancePointLine(hp2, hp1, p1), minDist);
	minDist = Math.min(this._distancePointLine(hp2, p1, tp1), minDist);

	// distances for point 2
	minDist = Math.min(this._distancePointLine(p2, hp1, p1), minDist);
	minDist = Math.min(this._distancePointLine(p2, p1, tp1), minDist);

	// distances for tail point 2
	minDist = Math.min(this._distancePointLine(tp2, hp1, p1), minDist);
	minDist = Math.min(this._distancePointLine(tp2, p1, tp1), minDist);

	// return true if the minimum distance from all points to all other
	// line segments is larger than the allowed distance
	return (minDist > dist);
}

/**
 * Returns the distance from a point to a line segment
 * @private
 * @param {Point} point Point
 * @param {Point} lineStart Start point of line
 * @param {Point} lineEnd End point of line
 * @return {number} distance
 */
Arrow.prototype._distancePointLine = function(point, lineStart, lineEnd){
	// from http://paulbourke.net/geometry/pointlineplane/

	var lineMag = (Point.sub(lineStart,lineEnd)).len();

	var u = ( ( ( point.x - lineStart.x ) * ( lineEnd.x - lineStart.x ) ) +
			( ( point.y - lineStart.y ) * ( lineEnd.y - lineStart.y ) ) ) /
			( lineMag * lineMag );

	if (u < 0.0 || u > 1.0){
		return Math.min(Point.sub(point,lineStart).len(), Point.sub(point,lineEnd).len());
	}

	var iX = lineStart.x + u * ( lineEnd.x - lineStart.x );
	var iY = lineStart.y + u * ( lineEnd.y - lineStart.y );

	return (Point.sub(point, new Point(iX,iY))).len();
}

/**
 * Draws the arrow on a canvas
 * @param {Context2D} ctx Context for drawing
 * @param {number} time Timestep for drawing (including fraction times)
 * @param {number} scale scales the whole arrow by scale
 */
Arrow.prototype.draw = function(ctx, time, scale){

    // Default use 1 as scale
    scale = scale || 1;

    var alphaBegin = Math.min((time-this.timeBorn),1);
    var alphaEnd = Math.min((this.timeDeath-time),1);
    var alpha = Math.min(alphaBegin, alphaEnd);
    ctx.fillStyle = 'rgba(255, 255, 255, ' + alpha + ')';
    ctx.strokeStyle = 'rgba(96, 215, 235, ' + alpha + ')';
	ctx.lineWidth = 1;
	
    var tp = undefined;
    var pos = undefined;
    var hp = undefined;
    var length = undefined;
	var width = this.width*scale;// * 3;
    var flooredTime = Math.floor(time);
    var fracTime = time % 1;	

    if (Math.abs(fracTime) > 0 && time <= this.timeDeath){
        //We are drawing a inbetween arrow...
        tp = Point.interpolate(this.timeData[flooredTime].tail,this.timeData[flooredTime+1].tail,fracTime).scale(scale);
        pos = Point.interpolate(this.position[flooredTime],this.position[flooredTime+1],fracTime).scale(scale);
        hp = Point.interpolate(this.timeData[flooredTime].head,this.timeData[flooredTime+1].head,fracTime).scale(scale);
        length = (this.timeData[flooredTime].length*(1-fracTime)+this.timeData[flooredTime+1].length*(fracTime))*scale;
    } else {
        tp = Point.scale(this.timeData[flooredTime].tail, scale);
        pos = Point.scale(this.position[flooredTime], scale);
        hp = Point.scale(this.timeData[flooredTime].head, scale);
        length = this.timeData[flooredTime].length*scale;
    }
	
	if (length < width * 2 || Point.angle(Point.sub(tp, pos), Point.sub(hp, pos)) < 0.9) {
		// draw circle
		
		ctx.beginPath();
		ctx.arc(pos.x,pos.y,width * 1.5,0,2*Math.PI);
		ctx.stroke();	
		ctx.fill();
		
	} else {
		// draw arrow
		var linePoints = [tp, pos, hp];
		
		// arrowhead width factor, depending on width of the arrow
		var awf = 2;

		// arrowhead length
		var ahl = 0.2 * length;

		// list of point coordinates left to the line, with distance this.width, going from tail to head
		var l = [];

		// list of point coordinates right to the line, with distance this.width, going from tail to head
		var r = [];

		// arrowhead left, right and front point coordinates
		var al, ar, af;

		// temporal variables
		var v, v2, p1, p2, p3, p, angle, wFactor, len;

		for (i = 0; i < linePoints.length; i++){

			if (i == 0){
				p1 = linePoints[i];
				p2 = linePoints[i+1];

				// vector from first to second point
				v = Point.sub(p2, p1).normalize().scale(width);

				// rotate vector by 90 degrees
				p = Point.add(p1, v.rotate(-Math.PI/2));
				l.push({"x": p.x, "y": p.y});

				// rotate vector by 180 degrees in other direction
				p = Point.add(p1, v.rotate(Math.PI));
				r.push({"x": p.x, "y": p.y});

			} else if (i < linePoints.length-1) {
				p1 = linePoints[i];
				p2 = linePoints[i-1];
				p3 = linePoints[i+1];

				// vector from current point to previous point
				v = Point.sub(p2, p1);

				// vector from current point to next point
				v2 = Point.sub(p3, p1);

				// angle and sign between vectors v and v2
				aSign = (v.x * v2.y - v.y * v2.x) < 0 ? -1 : 1;
				angle = aSign * Point.angle(v, v2)/2;

				// width changes based on the angle between the vectors v and v2
				wfactor = -(width / Math.cos(Math.abs(Math.PI/2 - angle))) / width;

				// scale to width
				v = v.normalize().scale(width * -wfactor);

				// rotate vector
				p = Point.add(p1, v.rotate(angle));
				l.push({"x": p.x, "y": p.y});

				// rotate vector by 180 degrees in other direction
				p = Point.add(p1, v.rotate(-Math.PI));
				r.push({"x": p.x, "y": p.y});
			} else {
				p1 = linePoints[i];
				p2 = linePoints[i-1];

				// vector from current point to previous point
				v = Point.sub(p2, p1);
				len = v.len();

				// position at half distance
				p1 = Point.add(p1, v.normalize().scale(len/2));

				// normalize and scale to width
				v = v.normalize().scale(width);

				// rotate vector by 90 degrees
				p = Point.add(p1, v.rotate(Math.PI/2));
				l.push({"x": p.x, "y": p.y});

				// rotate vector by 180 degrees in other direction
				p = Point.add(p1, v.rotate(-Math.PI));
				r.push({"x": p.x, "y": p.y});
			}
		}

		// draw arrowhead at the end of the line
		p = Point.add(p1, v.scale(awf));
		ar = {"x": p.x, "y": p.y};

		p = Point.add(p1, v.rotate(-Math.PI));
		al = {"x": p.x, "y": p.y};

		p = Point.add(p1, v.rotate(Math.PI/2).normalize().scale(len/2));
		af = {"x": p.x, "y": p.y};

		var lineFuncInterpolate = d3.svg.line()
									.x(function(d) { return d.x; })
									.y(function(d) { return d.y; })
									.interpolate("basis");	// other interpolation might work better

		var lineFuncLinear = d3.svg.line()
									.x(function(d) { return d.x; })
									.y(function(d) { return d.y; });

		var path1 = lineFuncInterpolate(l);
		var path2 = lineFuncLinear([l[l.length-1], al, af, ar, r[r.length-1]]);
		var path3 = lineFuncInterpolate(r.reverse());

		var path = path1 + "L" + path2.substr(1, path2.length-1)
						 + "L" + path3.substr(1, path3.length-1)
						 + "Z";

		var p = new Path2D(path);
		ctx.stroke(p);
		ctx.fill(p);
	}

	
}

/**
 * Returns true if distance to all is kept
 * @param {Arrow} others The arrows to calculate the distance to
 * @param {int} time time for calculation
 * @param {float} dist the dist to check for
 * @return {boolean} true if distance is greater than dist
 */
Arrow.prototype.distanceToMultiple = function(others, time, dist){
    var that = this;
    // Array.every will stop if any return false...
    return others.every(elem => that.distance(elem, time, dist));
}

/**
 * Calculates the length, and head and tail of this arrow
 * this.position has to be set
 * @param {int} time The time to calculate this values for
 */
Arrow.prototype.calcDataValues = function(time){
    this.timeData[time] = this._trajectoryHelper.getTimeData(this, time);
}

/**
 * returns wether this arrow is alive at the given time
 * @param {int} time asked for timestep
 * @return {boolean} true if arrow is alive at given position
 */
Arrow.prototype.isAlive = function(time){
    return this.timeBorn <= time && this.timeDeath >= time;
}

/**
 * adds a position for the arrow at "time" timestep,
 * based on the position of startTime and calculates the Data values
 * for this timestep
 * @param {int} startTime known timestep
 * @param {int} time asked for timestep
 */
Arrow.prototype.propagateToTime = function(startTime, time){
    if (time == startTime) return;
    if (time > startTime){
        // forward pass
        this.position[time] = Point.interpolate(this.position[startTime], this.timeData[startTime].head, 0.4);
        this.calcDataValues(time);
        // use old tail as new tail, to avoid jumping based on changed streamline integration
        this.timeData[time].tail = Point.interpolate(this.position[startTime], this.timeData[startTime].tail, 0.6);
    } else {
        // Backward pass
        this.position[time] = Point.interpolate(this.position[startTime], this.timeData[startTime].tail, 0.4);
        this.calcDataValues(time);
        this.timeData[time].head = Point.interpolate(this.position[startTime], this.timeData[startTime].head, 0.6);
    }

}