define( function( require ) { 'use strict'; var inherit = require( 'PHET_CORE/inherit' ); var Vector2 = require( 'DOT/Vector2' ); var Bounds2 = require( 'DOT/Bounds2' ); var DotUtil = require( 'DOT/Util' ); // eslint-disable-line require-statement-match var kite = require( 'KITE/kite' ); var Segment = require( 'KITE/segments/Segment' ); /** * Creates a circular arc (or circle if the startAngle/endAngle difference is ~2pi). * See http://www.w3.org/TR/2dcontext/#dom-context-2d-arc for detailed information on the parameters. * * @param {Vector2} center - Center of the arc (every point on the arc is equally far from the center) * @param {number} radius - How far from the center the arc will be * @param {number} startAngle - Angle (radians) of the start of the arc * @param {number} endAngle - Angle (radians) of the end of the arc * @param {boolean} anticlockwise - Decides which direction the arc takes around the center * @constructor */ function Arc( center, radius, startAngle, endAngle, anticlockwise ) { Segment.call( this ); this._center = center; this._radius = radius; this._startAngle = startAngle; this._endAngle = endAngle; this._anticlockwise = anticlockwise; this.invalidate(); } kite.register( 'Arc', Arc ); inherit( Segment, Arc, { // @public - Clears cached information, should be called when any of the 'constructor arguments' are mutated. invalidate: function() { // Lazily-computed derived information this._start = null; // {Vector2 | null} this._end = null; // {Vector2 | null} this._startTangent = null; // {Vector2 | null} this._endTangent = null; // {Vector2 | null} this._actualEndAngle = null; // {number | null} - End angle in relation to our start angle (can get remapped) this._isFullPerimeter = null; // {boolean | null} - Whether it's a full circle (and not just an arc) this._angleDifference = null; // {number | null} this._bounds = null; // {Bounds2 | null} // Remap negative radius to a positive radius if ( this._radius < 0 ) { // support this case since we might actually need to handle it inside of strokes? this._radius = -this._radius; this._startAngle += Math.PI; this._endAngle += Math.PI; } // Constraints that should always be satisfied assert && assert( !( ( !this.anticlockwise && this.endAngle - this.startAngle <= -Math.PI * 2 ) || ( this.anticlockwise && this.startAngle - this.endAngle <= -Math.PI * 2 ) ), 'Not handling arcs with start/end angles that show differences in-between browser handling' ); assert && assert( !( ( !this.anticlockwise && this.endAngle - this.startAngle > Math.PI * 2 ) || ( this.anticlockwise && this.startAngle - this.endAngle > Math.PI * 2 ) ), 'Not handling arcs with start/end angles that show differences in-between browser handling' ); this.trigger0( 'invalidated' ); }, getStart: function() { if ( this._start === null ) { this._start = this.positionAtAngle( this._startAngle ); } return this._start; }, get start() { return this.getStart(); }, getEnd: function() { if ( this._end === null ) { this._end = this.positionAtAngle( this._endAngle ); } return this._end; }, get end() { return this.getEnd(); }, getStartTangent: function() { if ( this._startTangent === null ) { this._startTangent = this.tangentAtAngle( this._startAngle ); } return this._startTangent; }, get startTangent() { return this.getStartTangent(); }, getEndTangent: function() { if ( this._endTangent === null ) { this._endTangent = this.tangentAtAngle( this._endAngle ); } return this._endTangent; }, get endTangent() { return this.getEndTangent(); }, getActualEndAngle: function() { if ( this._actualEndAngle === null ) { // compute an actual end angle so that we can smoothly go from this._startAngle to this._actualEndAngle if ( this._anticlockwise ) { // angle is 'decreasing' // -2pi <= end - start < 2pi if ( this._startAngle > this._endAngle ) { this._actualEndAngle = this._endAngle; } else if ( this._startAngle < this._endAngle ) { this._actualEndAngle = this._endAngle - 2 * Math.PI; } else { // equal this._actualEndAngle = this._startAngle; } } else { // angle is 'increasing' // -2pi < end - start <= 2pi if ( this._startAngle < this._endAngle ) { this._actualEndAngle = this._endAngle; } else if ( this._startAngle > this._endAngle ) { this._actualEndAngle = this._endAngle + Math.PI * 2; } else { // equal this._actualEndAngle = this._startAngle; } } } return this._actualEndAngle; }, get actualEndAngle() { return this.getActualEndAngle(); }, getIsFullPerimeter: function() { if ( this._isFullPerimeter === null ) { this._isFullPerimeter = ( !this._anticlockwise && this._endAngle - this._startAngle >= Math.PI * 2 ) || ( this._anticlockwise && this._startAngle - this._endAngle >= Math.PI * 2 ); } return this._isFullPerimeter; }, get isFullPerimeter() { return this.getIsFullPerimeter(); }, getAngleDifference: function() { if ( this._angleDifference === null ) { // compute an angle difference that represents how "much" of the circle our arc covers this._angleDifference = this._anticlockwise ? this._startAngle - this._endAngle : this._endAngle - this._startAngle; if ( this._angleDifference < 0 ) { this._angleDifference += Math.PI * 2; } assert && assert( this._angleDifference >= 0 ); // now it should always be zero or positive } return this._angleDifference; }, get angleDifference() { return this.getAngleDifference(); }, getBounds: function() { if ( this._bounds === null ) { // acceleration for intersection this._bounds = Bounds2.NOTHING.copy().withPoint( this.getStart() ) .withPoint( this.getEnd() ); // if the angles are different, check extrema points if ( this._startAngle !== this._endAngle ) { // check all of the extrema points this.includeBoundsAtAngle( 0 ); this.includeBoundsAtAngle( Math.PI / 2 ); this.includeBoundsAtAngle( Math.PI ); this.includeBoundsAtAngle( 3 * Math.PI / 2 ); } } return this._bounds; }, get bounds() { return this.getBounds(); }, getNondegenerateSegments: function() { if ( this._radius <= 0 || this._startAngle === this._endAngle ) { return []; } else { return [ this ]; // basically, Arcs aren't really degenerate that easily } }, includeBoundsAtAngle: function( angle ) { if ( this.containsAngle( angle ) ) { // the boundary point is in the arc this._bounds = this._bounds.withPoint( this._center.plus( Vector2.createPolar( this._radius, angle ) ) ); } }, // maps a contained angle to between [startAngle,actualEndAngle), even if the end angle is lower. mapAngle: function( angle ) { // consider an assert that we contain that angle? return ( this._startAngle > this.getActualEndAngle() ) ? DotUtil.moduloBetweenUp( angle, this._startAngle - 2 * Math.PI, this._startAngle ) : DotUtil.moduloBetweenDown( angle, this._startAngle, this._startAngle + 2 * Math.PI ); }, tAtAngle: function( angle ) { return ( this.mapAngle( angle ) - this._startAngle ) / ( this.getActualEndAngle() - this._startAngle ); }, angleAt: function( t ) { return this._startAngle + ( this.getActualEndAngle() - this._startAngle ) * t; }, positionAt: function( t ) { return this.positionAtAngle( this.angleAt( t ) ); }, tangentAt: function( t ) { return this.tangentAtAngle( this.angleAt( t ) ); }, curvatureAt: function( t ) { return ( this._anticlockwise ? -1 : 1 ) / this._radius; }, positionAtAngle: function( angle ) { return this._center.plus( Vector2.createPolar( this._radius, angle ) ); }, tangentAtAngle: function( angle ) { var normal = Vector2.createPolar( 1, angle ); return this._anticlockwise ? normal.perpendicular() : normal.perpendicular().negated(); }, // TODO: refactor? shared with EllipticalArc (use this improved version) containsAngle: function( angle ) { // transform the angle into the appropriate coordinate form // TODO: check anticlockwise version! var normalizedAngle = this._anticlockwise ? angle - this._endAngle : angle - this._startAngle; // get the angle between 0 and 2pi var positiveMinAngle = DotUtil.moduloBetweenDown( normalizedAngle, 0, Math.PI * 2 ); return positiveMinAngle <= this.angleDifference; }, getSVGPathFragment: function() { // see http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands for more info // rx ry x-axis-rotation large-arc-flag sweep-flag x y var epsilon = 0.01; // allow some leeway to render things as 'almost circles' var sweepFlag = this._anticlockwise ? '0' : '1'; var largeArcFlag; if ( this.angleDifference < Math.PI * 2 - epsilon ) { largeArcFlag = this.angleDifference < Math.PI ? '0' : '1'; return 'A ' + kite.svgNumber( this._radius ) + ' ' + kite.svgNumber( this._radius ) + ' 0 ' + largeArcFlag + ' ' + sweepFlag + ' ' + kite.svgNumber( this.end.x ) + ' ' + kite.svgNumber( this.end.y ); } else { // circle (or almost-circle) case needs to be handled differently // since SVG will not be able to draw (or know how to draw) the correct circle if we just have a start and end, we need to split it into two circular arcs // get the angle that is between and opposite of both of the points var splitOppositeAngle = ( this._startAngle + this._endAngle ) / 2; // this _should_ work for the modular case? var splitPoint = this._center.plus( Vector2.createPolar( this._radius, splitOppositeAngle ) ); largeArcFlag = '0'; // since we split it in 2, it's always the small arc var firstArc = 'A ' + kite.svgNumber( this._radius ) + ' ' + kite.svgNumber( this._radius ) + ' 0 ' + largeArcFlag + ' ' + sweepFlag + ' ' + kite.svgNumber( splitPoint.x ) + ' ' + kite.svgNumber( splitPoint.y ); var secondArc = 'A ' + kite.svgNumber( this._radius ) + ' ' + kite.svgNumber( this._radius ) + ' 0 ' + largeArcFlag + ' ' + sweepFlag + ' ' + kite.svgNumber( this.end.x ) + ' ' + kite.svgNumber( this.end.y ); return firstArc + ' ' + secondArc; } }, strokeLeft: function( lineWidth ) { return [ new kite.Arc( this._center, this._radius + ( this._anticlockwise ? 1 : -1 ) * lineWidth / 2, this._startAngle, this._endAngle, this._anticlockwise ) ]; }, strokeRight: function( lineWidth ) { return [ new kite.Arc( this._center, this._radius + ( this._anticlockwise ? -1 : 1 ) * lineWidth / 2, this._endAngle, this._startAngle, !this._anticlockwise ) ]; }, // not including 0 and 1 getInteriorExtremaTs: function() { var that = this; var result = []; _.each( [ 0, Math.PI / 2, Math.PI, 3 * Math.PI / 2 ], function( angle ) { if ( that.containsAngle( angle ) ) { var t = that.tAtAngle( angle ); var epsilon = 0.0000000001; // TODO: general kite epsilon? if ( t > epsilon && t < 1 - epsilon ) { result.push( t ); } } } ); return result.sort(); // modifies original, which is OK }, subdivided: function( t ) { // TODO: verify that we don't need to switch anticlockwise here, or subtract 2pi off any angles var angle0 = this.angleAt( 0 ); var angleT = this.angleAt( t ); var angle1 = this.angleAt( 1 ); return [ new kite.Arc( this._center, this._radius, angle0, angleT, this._anticlockwise ), new kite.Arc( this._center, this._radius, angleT, angle1, this._anticlockwise ) ]; }, intersection: function( ray ) { var result = []; // hits in order // left here, if in the future we want to better-handle boundary points var epsilon = 0; // Run a general circle-intersection routine, then we can test the angles later. // Solves for the two solutions t such that ray.position + ray.direction * t is on the circle. // Then we check whether the angle at each possible hit point is in our arc. var centerToRay = ray.position.minus( this._center ); var tmp = ray.direction.dot( centerToRay ); var centerToRayDistSq = centerToRay.magnitudeSquared(); var discriminant = 4 * tmp * tmp - 4 * ( centerToRayDistSq - this._radius * this._radius ); if ( discriminant < epsilon ) { // ray misses circle entirely return result; } var base = ray.direction.dot( this._center ) - ray.direction.dot( ray.position ); var sqt = Math.sqrt( discriminant ) / 2; var ta = base - sqt; var tb = base + sqt; if ( tb < epsilon ) { // circle is behind ray return result; } var pointB = ray.pointAtDistance( tb ); var normalB = pointB.minus( this._center ).normalized(); if ( ta < epsilon ) { // we are inside the circle, so only one intersection is possible if ( this.containsAngle( normalB.angle() ) ) { result.push( { distance: tb, point: pointB, normal: normalB.negated(), // normal is towards the ray wind: this._anticlockwise ? -1 : 1 // since we are inside, wind this way } ); } } else { // two possible hits (outside circle) var pointA = ray.pointAtDistance( ta ); var normalA = pointA.minus( this._center ).normalized(); if ( this.containsAngle( normalA.angle() ) ) { result.push( { distance: ta, point: pointA, normal: normalA, wind: this._anticlockwise ? 1 : -1 // hit from outside } ); } if ( this.containsAngle( normalB.angle() ) ) { result.push( { distance: tb, point: pointB, normal: normalB.negated(), wind: this._anticlockwise ? -1 : 1 // this is the far hit, which winds the opposite way } ); } } return result; }, // returns the resultant winding number of this ray intersecting this segment. windingIntersection: function( ray ) { var wind = 0; var hits = this.intersection( ray ); _.each( hits, function( hit ) { wind += hit.wind; } ); return wind; }, writeToContext: function( context ) { context.arc( this._center.x, this._center.y, this._radius, this._startAngle, this._endAngle, this._anticlockwise ); }, // TODO: test various transform types, especially rotations, scaling, shears, etc. transformed: function( matrix ) { // so we can handle reflections in the transform, we do the general case handling for start/end angles var startAngle = matrix.timesVector2( Vector2.createPolar( 1, this._startAngle ) ).minus( matrix.timesVector2( Vector2.ZERO ) ).angle(); var endAngle = matrix.timesVector2( Vector2.createPolar( 1, this._endAngle ) ).minus( matrix.timesVector2( Vector2.ZERO ) ).angle(); // reverse the 'clockwiseness' if our transform includes a reflection var anticlockwise = matrix.getDeterminant() >= 0 ? this._anticlockwise : !this._anticlockwise; if ( Math.abs( this._endAngle - this._startAngle ) === Math.PI * 2 ) { endAngle = anticlockwise ? startAngle - Math.PI * 2 : startAngle + Math.PI * 2; } var scaleVector = matrix.getScaleVector(); if ( scaleVector.x !== scaleVector.y ) { var radiusX = scaleVector.x * this._radius; var radiusY = scaleVector.y * this._radius; return new kite.EllipticalArc( matrix.timesVector2( this._center ), radiusX, radiusY, 0, startAngle, endAngle, anticlockwise ); } else { var radius = scaleVector.x * this._radius; return new kite.Arc( matrix.timesVector2( this._center ), radius, startAngle, endAngle, anticlockwise ); } } } ); Segment.addInvalidatingGetterSetter( Arc, 'center' ); Segment.addInvalidatingGetterSetter( Arc, 'radius' ); Segment.addInvalidatingGetterSetter( Arc, 'startAngle' ); Segment.addInvalidatingGetterSetter( Arc, 'endAngle' ); Segment.addInvalidatingGetterSetter( Arc, 'anticlockwise' ); return Arc; } );
define( function( require ) { 'use strict'; var inherit = require( 'PHET_CORE/inherit' ); var Bounds2 = require( 'DOT/Bounds2' ); var Matrix3 = require( 'DOT/Matrix3' ); var Util = require( 'DOT/Util' ); var kite = require( 'KITE/kite' ); var Segment = require( 'KITE/segments/Segment' ); // constants var solveQuadraticRootsReal = Util.solveQuadraticRootsReal; var arePointsCollinear = Util.arePointsCollinear; function Quadratic( start, control, end ) { Segment.call( this ); this._start = start; this._control = control; this._end = end; this.invalidate(); } kite.register( 'Quadratic', Quadratic ); inherit( Segment, Quadratic, { degree: 2, // @public - Clears cached information, should be called when any of the 'constructor arguments' are mutated. invalidate: function() { // Lazily-computed derived information this._startTangent = null; // {Vector2 | null} this._endTangent = null; // {Vector2 | null} this._tCriticalX = null; // {number | null} T where x-derivative is 0 (replaced with NaN if not in range) this._tCriticalY = null; // {number | null} T where y-derivative is 0 (replaced with NaN if not in range) this._bounds = null; // {Bounds2 | null} this.trigger0( 'invalidated' ); }, getStartTangent: function() { if ( this._startTangent === null ) { var controlIsStart = this._start.equals( this._control ); // TODO: allocation reduction this._startTangent = controlIsStart ? this._end.minus( this._start ).normalized() : this._control.minus( this._start ).normalized(); } return this._startTangent; }, get startTangent() { return this.getStartTangent(); }, getEndTangent: function() { if ( this._endTangent === null ) { var controlIsEnd = this._end.equals( this._control ); // TODO: allocation reduction this._endTangent = controlIsEnd ? this._end.minus( this._start ).normalized() : this._end.minus( this._control ).normalized(); } return this._endTangent; }, get endTangent() { return this.getEndTangent(); }, getTCriticalX: function() { // compute x where the derivative is 0 (used for bounds and other things) if ( this._tCriticalX === null ) { this._tCriticalX = Quadratic.extremaT( this._start.x, this._control.x, this._end.x ); } return this._tCriticalX; }, get tCriticalX() { return this.getTCriticalX(); }, getTCriticalY: function() { // compute y where the derivative is 0 (used for bounds and other things) if ( this._tCriticalY === null ) { this._tCriticalY = Quadratic.extremaT( this._start.y, this._control.y, this._end.y ); } return this._tCriticalY; }, get tCriticalY() { return this.getTCriticalY(); }, getNondegenerateSegments: function() { var start = this._start; var control = this._control; var end = this._end; var startIsEnd = start.equals( end ); var startIsControl = start.equals( control ); var endIsControl = start.equals( control ); if ( startIsEnd && startIsControl ) { // all same points return []; } else if ( startIsEnd ) { // this is a special collinear case, we basically line out to the farthest point and back var halfPoint = this.positionAt( 0.5 ); return [ new kite.Line( start, halfPoint ), new kite.Line( halfPoint, end ) ]; } else if ( arePointsCollinear( start, control, end ) ) { // if they are collinear, we can reduce to start->control and control->end, or if control is between, just one line segment // also, start !== end (handled earlier) if ( startIsControl || endIsControl ) { // just a line segment! return [ new kite.Line( start, end ) ]; // no extra nondegenerate check since start !== end } // now control point must be unique. we check to see if our rendered path will be outside of the start->end line segment var delta = end.minus( start ); var p1d = control.minus( start ).dot( delta.normalized ) / delta.magnitude(); var t = Quadratic.extremaT( 0, p1d, 1 ); if ( !isNaN( t ) && t > 0 && t < 1 ) { // we have a local max inside the range, indicating that our extrema point is outside of start->end // we'll line to and from it var pt = this.positionAt( t ); return _.flatten( [ new kite.Line( start, pt ).getNondegenerateSegments(), new kite.Line( pt, end ).getNondegenerateSegments() ] ); } else { // just provide a line segment, our rendered path doesn't go outside of this return [ new kite.Line( start, end ) ]; // no extra nondegenerate check since start !== end } } else { return [ this ]; } }, getBounds: function() { // calculate our temporary guaranteed lower bounds based on the end points if ( this._bounds === null ) { this._bounds = new Bounds2( Math.min( this._start.x, this._end.x ), Math.min( this._start.y, this._end.y ), Math.max( this._start.x, this._end.x ), Math.max( this._start.y, this._end.y ) ); // compute x and y where the derivative is 0, so we can include this in the bounds var tCriticalX = this.getTCriticalX(); var tCriticalY = this.getTCriticalY(); if ( !isNaN( tCriticalX ) && tCriticalX > 0 && tCriticalX < 1 ) { this._bounds = this._bounds.withPoint( this.positionAt( tCriticalX ) ); } if ( !isNaN( tCriticalY ) && tCriticalY > 0 && tCriticalY < 1 ) { this._bounds = this._bounds.withPoint( this.positionAt( tCriticalY ) ); } } return this._bounds; }, get bounds() { return this.getBounds(); }, // can be described from t=[0,1] as: (1-t)^2 start + 2(1-t)t control + t^2 end positionAt: function( t ) { var mt = 1 - t; // TODO: allocation reduction return this._start.times( mt * mt ).plus( this._control.times( 2 * mt * t ) ).plus( this._end.times( t * t ) ); }, // derivative: 2(1-t)( control - start ) + 2t( end - control ) tangentAt: function( t ) { // TODO: allocation reduction return this._control.minus( this._start ).times( 2 * ( 1 - t ) ).plus( this._end.minus( this._control ).times( 2 * t ) ); }, curvatureAt: function( t ) { // see http://cagd.cs.byu.edu/~557/text/ch2.pdf p31 // TODO: remove code duplication with Cubic var epsilon = 0.0000001; if ( Math.abs( t - 0.5 ) > 0.5 - epsilon ) { var isZero = t < 0.5; var p0 = isZero ? this._start : this._end; var p1 = this._control; var p2 = isZero ? this._end : this._start; var d10 = p1.minus( p0 ); var a = d10.magnitude(); var h = ( isZero ? -1 : 1 ) * d10.perpendicular().normalized().dot( p2.minus( p1 ) ); return ( h * ( this.degree - 1 ) ) / ( this.degree * a * a ); } else { return this.subdivided( t, true )[ 0 ].curvatureAt( 1 ); } }, // see http://www.visgraf.impa.br/sibgrapi96/trabs/pdf/a14.pdf // and http://math.stackexchange.com/questions/12186/arc-length-of-bezier-curves for curvature / arc length offsetTo: function( r, reverse ) { // TODO: implement more accurate method at http://www.antigrain.com/research/adaptive_bezier/index.html // TODO: or more recently (and relevantly): http://www.cis.usouthal.edu/~hain/general/Publications/Bezier/BezierFlattening.pdf var curves = [ this ]; // subdivide this curve var depth = 5; // generates 2^depth curves for ( var i = 0; i < depth; i++ ) { curves = _.flatten( _.map( curves, function( curve ) { return curve.subdivided( 0.5, true ); } ) ); } var offsetCurves = _.map( curves, function( curve ) { return curve.approximateOffset( r ); } ); if ( reverse ) { offsetCurves.reverse(); offsetCurves = _.map( offsetCurves, function( curve ) { return curve.reversed( true ); } ); } return offsetCurves; }, subdivided: function( t ) { // de Casteljau method var leftMid = this._start.blend( this._control, t ); var rightMid = this._control.blend( this._end, t ); var mid = leftMid.blend( rightMid, t ); return [ new kite.Quadratic( this._start, leftMid, mid ), new kite.Quadratic( mid, rightMid, this._end ) ]; }, // elevation of this quadratic Bezier curve to a cubic Bezier curve degreeElevated: function() { // TODO: allocation reduction return new kite.Cubic( this._start, this._start.plus( this._control.timesScalar( 2 ) ).dividedScalar( 3 ), this._end.plus( this._control.timesScalar( 2 ) ).dividedScalar( 3 ), this._end ); }, reversed: function() { return new kite.Quadratic( this._end, this._control, this._start ); }, approximateOffset: function( r ) { return new kite.Quadratic( this._start.plus( ( this._start.equals( this._control ) ? this._end.minus( this._start ) : this._control.minus( this._start ) ).perpendicular().normalized().times( r ) ), this._control.plus( this._end.minus( this._start ).perpendicular().normalized().times( r ) ), this._end.plus( ( this._end.equals( this._control ) ? this._end.minus( this._start ) : this._end.minus( this._control ) ).perpendicular().normalized().times( r ) ) ); }, getSVGPathFragment: function() { return 'Q ' + kite.svgNumber( this._control.x ) + ' ' + kite.svgNumber( this._control.y ) + ' ' + kite.svgNumber( this._end.x ) + ' ' + kite.svgNumber( this._end.y ); }, strokeLeft: function( lineWidth ) { return this.offsetTo( -lineWidth / 2, false ); }, strokeRight: function( lineWidth ) { return this.offsetTo( lineWidth / 2, true ); }, getInteriorExtremaTs: function() { // TODO: we assume here we are reduce, so that a criticalX doesn't equal a criticalY? var result = []; var epsilon = 0.0000000001; // TODO: general kite epsilon? var criticalX = this.getTCriticalX(); var criticalY = this.getTCriticalY(); if ( !isNaN( criticalX ) && criticalX > epsilon && criticalX < 1 - epsilon ) { result.push( this.tCriticalX ); } if ( !isNaN( criticalY ) && criticalY > epsilon && criticalY < 1 - epsilon ) { result.push( this.tCriticalY ); } return result.sort(); }, // returns the resultant winding number of this ray intersecting this segment. intersection: function( ray ) { var self = this; var result = []; // find the rotation that will put our ray in the direction of the x-axis so we can only solve for y=0 for intersections var inverseMatrix = Matrix3.rotation2( -ray.direction.angle() ).timesMatrix( Matrix3.translation( -ray.position.x, -ray.position.y ) ); var p0 = inverseMatrix.timesVector2( this._start ); var p1 = inverseMatrix.timesVector2( this._control ); var p2 = inverseMatrix.timesVector2( this._end ); //(1-t)^2 start + 2(1-t)t control + t^2 end var a = p0.y - 2 * p1.y + p2.y; var b = -2 * p0.y + 2 * p1.y; var c = p0.y; var ts = solveQuadraticRootsReal( a, b, c ); _.each( ts, function( t ) { if ( t >= 0 && t <= 1 ) { var hitPoint = self.positionAt( t ); var unitTangent = self.tangentAt( t ).normalized(); var perp = unitTangent.perpendicular(); var toHit = hitPoint.minus( ray.position ); // make sure it's not behind the ray if ( toHit.dot( ray.direction ) > 0 ) { result.push( { distance: toHit.magnitude(), point: hitPoint, normal: perp.dot( ray.direction ) > 0 ? perp.negated() : perp, wind: ray.direction.perpendicular().dot( unitTangent ) < 0 ? 1 : -1 } ); } } } ); return result; }, windingIntersection: function( ray ) { var wind = 0; var hits = this.intersection( ray ); _.each( hits, function( hit ) { wind += hit.wind; } ); return wind; }, // assumes the current position is at start writeToContext: function( context ) { context.quadraticCurveTo( this._control.x, this._control.y, this._end.x, this._end.y ); }, transformed: function( matrix ) { return new kite.Quadratic( matrix.timesVector2( this._start ), matrix.timesVector2( this._control ), matrix.timesVector2( this._end ) ); }, // given the current curve parameterized by t, will return a curve parameterized by x where t = a * x + b reparameterized: function( a, b ) { // to the polynomial pt^2 + qt + r: var p = this._start.plus( this._end.plus( this._control.timesScalar( -2 ) ) ); var q = this._control.minus( this._start ).timesScalar( 2 ); var r = this._start; // to the polynomial alpha*x^2 + beta*x + gamma: var alpha = p.timesScalar( a * a ); var beta = p.timesScalar( a * b ).timesScalar( 2 ).plus( q.timesScalar( a ) ); var gamma = p.timesScalar( b * b ).plus( q.timesScalar( b ) ).plus( r ); // back to the form start,control,end return new kite.Quadratic( gamma, beta.timesScalar( 0.5 ).plus( gamma ), alpha.plus( beta ).plus( gamma ) ); } } ); Segment.addInvalidatingGetterSetter( Quadratic, 'start' ); Segment.addInvalidatingGetterSetter( Quadratic, 'control' ); Segment.addInvalidatingGetterSetter( Quadratic, 'end' ); // one-dimensional solution to extrema Quadratic.extremaT = function( start, control, end ) { // compute t where the derivative is 0 (used for bounds and other things) var divisorX = 2 * ( end - 2 * control + start ); if ( divisorX !== 0 ) { return -2 * ( control - start ) / divisorX; } else { return NaN; } }; return Quadratic; } );