Path: blob/trunk/third_party/closure/goog/math/path.js
2868 views
// Copyright 2007 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.131415/**16* @fileoverview Represents a path used with a Graphics implementation.17* @author [email protected] (Erik Arvidsson)18*/1920goog.provide('goog.math.Path');21goog.provide('goog.math.Path.Segment');2223goog.require('goog.array');24goog.require('goog.math');25goog.require('goog.math.AffineTransform');26272829/**30* Creates a path object. A path is a sequence of segments and may be open or31* closed. Path uses the EVEN-ODD fill rule for determining the interior of the32* path. A path must start with a moveTo command.33*34* A "simple" path does not contain any arcs and may be transformed using35* the {@code transform} method.36*37* @struct38* @constructor39* @final40*/41goog.math.Path = function() {42/**43* The segment types that constitute this path.44* @private {!Array<goog.math.Path.Segment>}45*/46this.segments_ = [];4748/**49* The number of repeated segments of the current type.50* @type {!Array<number>}51* @private52*/53this.count_ = [];5455/**56* The arguments corresponding to each of the segments.57* @type {!Array<number>}58* @private59*/60this.arguments_ = [];6162/**63* The coordinates of the point which closes the path (the point of the64* last moveTo command).65* @type {Array<number>?}66* @private67*/68this.closePoint_ = null;6970/**71* The coordinates most recently added to the end of the path.72* @type {Array<number>?}73* @private74*/75this.currentPoint_ = null;7677/**78* Flag for whether this is a simple path (contains no arc segments).79* @type {boolean}80* @private81*/82this.simple_ = true;83};848586/**87* Path segment types.88* @enum {number}89*/90goog.math.Path.Segment = {91MOVETO: 0,92LINETO: 1,93CURVETO: 2,94ARCTO: 3,95CLOSE: 496};979899/**100* The number of points for each segment type.101* @type {!Array<number>}102* @private103*/104goog.math.Path.segmentArgCounts_ = (function() {105var counts = [];106counts[goog.math.Path.Segment.MOVETO] = 2;107counts[goog.math.Path.Segment.LINETO] = 2;108counts[goog.math.Path.Segment.CURVETO] = 6;109counts[goog.math.Path.Segment.ARCTO] = 6;110counts[goog.math.Path.Segment.CLOSE] = 0;111return counts;112})();113114115/**116* Returns an array of the segment types in this path, in the order of their117* appearance. Adjacent segments of the same type are collapsed into a single118* entry in the array. The returned array is a copy; modifications are not119* reflected in the Path object.120* @return {!Array<number>}121*/122goog.math.Path.prototype.getSegmentTypes = function() {123return this.segments_.concat();124};125126127/**128* Returns an array of the number of times each segment type repeats in this129* path, in order. The returned array is a copy; modifications are not reflected130* in the Path object.131* @return {!Array<number>}132*/133goog.math.Path.prototype.getSegmentCounts = function() {134return this.count_.concat();135};136137138/**139* Returns an array of all arguments for the segments of this path object, in140* order. The returned array is a copy; modifications are not reflected in the141* Path object.142* @return {!Array<number>}143*/144goog.math.Path.prototype.getSegmentArgs = function() {145return this.arguments_.concat();146};147148149/**150* Returns the number of points for a segment type.151*152* @param {number} segment The segment type.153* @return {number} The number of points.154*/155goog.math.Path.getSegmentCount = function(segment) {156return goog.math.Path.segmentArgCounts_[segment];157};158159160/**161* Appends another path to the end of this path.162*163* @param {!goog.math.Path} path The path to append.164* @return {!goog.math.Path} This path.165*/166goog.math.Path.prototype.appendPath = function(path) {167if (path.currentPoint_) {168Array.prototype.push.apply(this.segments_, path.segments_);169Array.prototype.push.apply(this.count_, path.count_);170Array.prototype.push.apply(this.arguments_, path.arguments_);171this.currentPoint_ = path.currentPoint_.concat();172this.closePoint_ = path.closePoint_.concat();173this.simple_ = this.simple_ && path.simple_;174}175return this;176};177178179/**180* Clears the path.181*182* @return {!goog.math.Path} The path itself.183*/184goog.math.Path.prototype.clear = function() {185this.segments_.length = 0;186this.count_.length = 0;187this.arguments_.length = 0;188this.closePoint_ = null;189this.currentPoint_ = null;190this.simple_ = true;191return this;192};193194195/**196* Adds a point to the path by moving to the specified point. Repeated moveTo197* commands are collapsed into a single moveTo.198*199* @param {number} x X coordinate of destination point.200* @param {number} y Y coordinate of destination point.201* @return {!goog.math.Path} The path itself.202*/203goog.math.Path.prototype.moveTo = function(x, y) {204if (goog.array.peek(this.segments_) == goog.math.Path.Segment.MOVETO) {205this.arguments_.length -= 2;206} else {207this.segments_.push(goog.math.Path.Segment.MOVETO);208this.count_.push(1);209}210this.arguments_.push(x, y);211this.currentPoint_ = this.closePoint_ = [x, y];212return this;213};214215216/**217* Adds points to the path by drawing a straight line to each point.218*219* @param {...number} var_args The coordinates of each destination point as x, y220* value pairs.221* @return {!goog.math.Path} The path itself.222*/223goog.math.Path.prototype.lineTo = function(var_args) {224return this.lineTo_(arguments);225};226227228/**229* Adds points to the path by drawing a straight line to each point.230*231* @param {!Array<number>} coordinates The coordinates of each232* destination point as x, y value pairs.233* @return {!goog.math.Path} The path itself.234*/235goog.math.Path.prototype.lineToFromArray = function(coordinates) {236return this.lineTo_(coordinates);237};238239240/**241* Adds points to the path by drawing a straight line to each point.242*243* @param {!Array<number>|Arguments} coordinates The coordinates of each244* destination point as x, y value pairs.245* @return {!goog.math.Path} The path itself.246* @private247*/248goog.math.Path.prototype.lineTo_ = function(coordinates) {249var lastSegment = goog.array.peek(this.segments_);250if (lastSegment == null) {251throw Error('Path cannot start with lineTo');252}253if (lastSegment != goog.math.Path.Segment.LINETO) {254this.segments_.push(goog.math.Path.Segment.LINETO);255this.count_.push(0);256}257for (var i = 0; i < coordinates.length; i += 2) {258var x = coordinates[i];259var y = coordinates[i + 1];260this.arguments_.push(x, y);261}262this.count_[this.count_.length - 1] += i / 2;263this.currentPoint_ = [x, y];264return this;265};266267268/**269* Adds points to the path by drawing cubic Bezier curves. Each curve is270* specified using 3 points (6 coordinates) - two control points and the end271* point of the curve.272*273* @param {...number} var_args The coordinates specifying each curve in sets of274* 6 points: {@code [x1, y1]} the first control point, {@code [x2, y2]} the275* second control point and {@code [x, y]} the end point.276* @return {!goog.math.Path} The path itself.277*/278goog.math.Path.prototype.curveTo = function(var_args) {279return this.curveTo_(arguments);280};281282283/**284* Adds points to the path by drawing cubic Bezier curves. Each curve is285* specified using 3 points (6 coordinates) - two control points and the end286* point of the curve.287*288* @param {!Array<number>} coordinates The coordinates specifying289* each curve in sets of 6 points: {@code [x1, y1]} the first control point,290* {@code [x2, y2]} the second control point and {@code [x, y]} the end291* point.292* @return {!goog.math.Path} The path itself.293*/294goog.math.Path.prototype.curveToFromArray = function(coordinates) {295return this.curveTo_(coordinates);296};297298299/**300* Adds points to the path by drawing cubic Bezier curves. Each curve is301* specified using 3 points (6 coordinates) - two control points and the end302* point of the curve.303*304* @param {!Array<number>|Arguments} coordinates The coordinates specifying305* each curve in sets of 6 points: {@code [x1, y1]} the first control point,306* {@code [x2, y2]} the second control point and {@code [x, y]} the end307* point.308* @return {!goog.math.Path} The path itself.309* @private310*/311goog.math.Path.prototype.curveTo_ = function(coordinates) {312var lastSegment = goog.array.peek(this.segments_);313if (lastSegment == null) {314throw Error('Path cannot start with curve');315}316if (lastSegment != goog.math.Path.Segment.CURVETO) {317this.segments_.push(goog.math.Path.Segment.CURVETO);318this.count_.push(0);319}320for (var i = 0; i < coordinates.length; i += 6) {321var x = coordinates[i + 4];322var y = coordinates[i + 5];323this.arguments_.push(324coordinates[i], coordinates[i + 1], coordinates[i + 2],325coordinates[i + 3], x, y);326}327this.count_[this.count_.length - 1] += i / 6;328this.currentPoint_ = [x, y];329return this;330};331332333/**334* Adds a path command to close the path by connecting the335* last point to the first point.336*337* @return {!goog.math.Path} The path itself.338*/339goog.math.Path.prototype.close = function() {340var lastSegment = goog.array.peek(this.segments_);341if (lastSegment == null) {342throw Error('Path cannot start with close');343}344if (lastSegment != goog.math.Path.Segment.CLOSE) {345this.segments_.push(goog.math.Path.Segment.CLOSE);346this.count_.push(1);347this.currentPoint_ = this.closePoint_;348}349return this;350};351352353/**354* Adds a path command to draw an arc centered at the point {@code (cx, cy)}355* with radius {@code rx} along the x-axis and {@code ry} along the y-axis from356* {@code startAngle} through {@code extent} degrees. Positive rotation is in357* the direction from positive x-axis to positive y-axis.358*359* @param {number} cx X coordinate of center of ellipse.360* @param {number} cy Y coordinate of center of ellipse.361* @param {number} rx Radius of ellipse on x axis.362* @param {number} ry Radius of ellipse on y axis.363* @param {number} fromAngle Starting angle measured in degrees from the364* positive x-axis.365* @param {number} extent The span of the arc in degrees.366* @param {boolean} connect If true, the starting point of the arc is connected367* to the current point.368* @return {!goog.math.Path} The path itself.369* @deprecated Use {@code arcTo} or {@code arcToAsCurves} instead.370*/371goog.math.Path.prototype.arc = function(372cx, cy, rx, ry, fromAngle, extent, connect) {373var startX = cx + goog.math.angleDx(fromAngle, rx);374var startY = cy + goog.math.angleDy(fromAngle, ry);375if (connect) {376if (!this.currentPoint_ || startX != this.currentPoint_[0] ||377startY != this.currentPoint_[1]) {378this.lineTo(startX, startY);379}380} else {381this.moveTo(startX, startY);382}383return this.arcTo(rx, ry, fromAngle, extent);384};385386387/**388* Adds a path command to draw an arc starting at the path's current point,389* with radius {@code rx} along the x-axis and {@code ry} along the y-axis from390* {@code startAngle} through {@code extent} degrees. Positive rotation is in391* the direction from positive x-axis to positive y-axis.392*393* This method makes the path non-simple.394*395* @param {number} rx Radius of ellipse on x axis.396* @param {number} ry Radius of ellipse on y axis.397* @param {number} fromAngle Starting angle measured in degrees from the398* positive x-axis.399* @param {number} extent The span of the arc in degrees.400* @return {!goog.math.Path} The path itself.401*/402goog.math.Path.prototype.arcTo = function(rx, ry, fromAngle, extent) {403var cx = this.currentPoint_[0] - goog.math.angleDx(fromAngle, rx);404var cy = this.currentPoint_[1] - goog.math.angleDy(fromAngle, ry);405var ex = cx + goog.math.angleDx(fromAngle + extent, rx);406var ey = cy + goog.math.angleDy(fromAngle + extent, ry);407this.segments_.push(goog.math.Path.Segment.ARCTO);408this.count_.push(1);409this.arguments_.push(rx, ry, fromAngle, extent, ex, ey);410this.simple_ = false;411this.currentPoint_ = [ex, ey];412return this;413};414415416/**417* Same as {@code arcTo}, but approximates the arc using bezier curves.418.* As a result, this method does not affect the simplified status of this path.419* The algorithm is adapted from {@code java.awt.geom.ArcIterator}.420*421* @param {number} rx Radius of ellipse on x axis.422* @param {number} ry Radius of ellipse on y axis.423* @param {number} fromAngle Starting angle measured in degrees from the424* positive x-axis.425* @param {number} extent The span of the arc in degrees.426* @return {!goog.math.Path} The path itself.427*/428goog.math.Path.prototype.arcToAsCurves = function(rx, ry, fromAngle, extent) {429var cx = this.currentPoint_[0] - goog.math.angleDx(fromAngle, rx);430var cy = this.currentPoint_[1] - goog.math.angleDy(fromAngle, ry);431var extentRad = goog.math.toRadians(extent);432var arcSegs = Math.ceil(Math.abs(extentRad) / Math.PI * 2);433var inc = extentRad / arcSegs;434var angle = goog.math.toRadians(fromAngle);435for (var j = 0; j < arcSegs; j++) {436var relX = Math.cos(angle);437var relY = Math.sin(angle);438var z = 4 / 3 * Math.sin(inc / 2) / (1 + Math.cos(inc / 2));439var c0 = cx + (relX - z * relY) * rx;440var c1 = cy + (relY + z * relX) * ry;441angle += inc;442relX = Math.cos(angle);443relY = Math.sin(angle);444this.curveTo(445c0, c1, cx + (relX + z * relY) * rx, cy + (relY - z * relX) * ry,446cx + relX * rx, cy + relY * ry);447}448return this;449};450451452/**453* Iterates over the path calling the supplied callback once for each path454* segment. The arguments to the callback function are the segment type and455* an array of its arguments.456*457* The {@code LINETO} and {@code CURVETO} arrays can contain multiple458* segments of the same type. The number of segments is the length of the459* array divided by the segment length (2 for lines, 6 for curves).460*461* As a convenience the {@code ARCTO} segment also includes the end point as the462* last two arguments: {@code rx, ry, fromAngle, extent, x, y}.463*464* @param {function(!goog.math.Path.Segment, !Array<number>)} callback465* The function to call with each path segment.466*/467goog.math.Path.prototype.forEachSegment = function(callback) {468var points = this.arguments_;469var index = 0;470for (var i = 0, length = this.segments_.length; i < length; i++) {471var seg = this.segments_[i];472var n = goog.math.Path.segmentArgCounts_[seg] * this.count_[i];473callback(seg, points.slice(index, index + n));474index += n;475}476};477478479/**480* Returns the coordinates most recently added to the end of the path.481*482* @return {Array<number>?} An array containing the ending coordinates of the483* path of the form {@code [x, y]}.484*/485goog.math.Path.prototype.getCurrentPoint = function() {486return this.currentPoint_ && this.currentPoint_.concat();487};488489490/**491* @return {!goog.math.Path} A copy of this path.492*/493goog.math.Path.prototype.clone = function() {494var path = new goog.math.Path();495path.segments_ = this.segments_.concat();496path.count_ = this.count_.concat();497path.arguments_ = this.arguments_.concat();498path.closePoint_ = this.closePoint_ && this.closePoint_.concat();499path.currentPoint_ = this.currentPoint_ && this.currentPoint_.concat();500path.simple_ = this.simple_;501return path;502};503504505/**506* Returns true if this path contains no arcs. Simplified paths can be507* created using {@code createSimplifiedPath}.508*509* @return {boolean} True if the path contains no arcs.510*/511goog.math.Path.prototype.isSimple = function() {512return this.simple_;513};514515516/**517* A map from segment type to the path function to call to simplify a path.518* @private {!Object<goog.math.Path.Segment, function(this: goog.math.Path)>}519*/520goog.math.Path.simplifySegmentMap_ = (function() {521var map = {};522map[goog.math.Path.Segment.MOVETO] = goog.math.Path.prototype.moveTo;523map[goog.math.Path.Segment.LINETO] = goog.math.Path.prototype.lineTo;524map[goog.math.Path.Segment.CLOSE] = goog.math.Path.prototype.close;525map[goog.math.Path.Segment.CURVETO] = goog.math.Path.prototype.curveTo;526map[goog.math.Path.Segment.ARCTO] = goog.math.Path.prototype.arcToAsCurves;527return map;528})();529530531/**532* Creates a copy of the given path, replacing {@code arcTo} with533* {@code arcToAsCurves}. The resulting path is simplified and can534* be transformed.535*536* @param {!goog.math.Path} src The path to simplify.537* @return {!goog.math.Path} A new simplified path.538*/539goog.math.Path.createSimplifiedPath = function(src) {540if (src.isSimple()) {541return src.clone();542}543var path = new goog.math.Path();544src.forEachSegment(function(segment, args) {545goog.math.Path.simplifySegmentMap_[segment].apply(path, args);546});547return path;548};549550551// TODO(chrisn): Delete this method552/**553* Creates a transformed copy of this path. The path is simplified554* {@see #createSimplifiedPath} prior to transformation.555*556* @param {!goog.math.AffineTransform} tx The transformation to perform.557* @return {!goog.math.Path} A new, transformed path.558*/559goog.math.Path.prototype.createTransformedPath = function(tx) {560var path = goog.math.Path.createSimplifiedPath(this);561path.transform(tx);562return path;563};564565566/**567* Transforms the path. Only simple paths are transformable. Attempting568* to transform a non-simple path will throw an error.569*570* @param {!goog.math.AffineTransform} tx The transformation to perform.571* @return {!goog.math.Path} The path itself.572*/573goog.math.Path.prototype.transform = function(tx) {574if (!this.isSimple()) {575throw Error('Non-simple path');576}577tx.transform(578this.arguments_, 0, this.arguments_, 0, this.arguments_.length / 2);579if (this.closePoint_) {580tx.transform(this.closePoint_, 0, this.closePoint_, 0, 1);581}582if (this.currentPoint_ && this.closePoint_ != this.currentPoint_) {583tx.transform(this.currentPoint_, 0, this.currentPoint_, 0, 1);584}585return this;586};587588589/**590* @return {boolean} Whether the path is empty.591*/592goog.math.Path.prototype.isEmpty = function() {593return this.segments_.length == 0;594};595596597