Path: blob/trunk/third_party/closure/goog/graphics/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.graphics.Path');21goog.provide('goog.graphics.Path.Segment');2223goog.require('goog.array');24goog.require('goog.graphics.AffineTransform');25goog.require('goog.math');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* @constructor38*/39goog.graphics.Path = function() {40/**41* The segment types that constitute this path.42* @type {!Array<number>}43* @private44*/45this.segments_ = [];4647/**48* The number of repeated segments of the current type.49* @type {!Array<number>}50* @private51*/52this.count_ = [];5354/**55* The arguments corresponding to each of the segments.56* @type {!Array<number>}57* @private58*/59this.arguments_ = [];60};616263/**64* The coordinates of the point which closes the path (the point of the65* last moveTo command).66* @type {Array<number>?}67* @private68*/69goog.graphics.Path.prototype.closePoint_ = null;707172/**73* The coordinates most recently added to the end of the path.74* @type {Array<number>?}75* @private76*/77goog.graphics.Path.prototype.currentPoint_ = null;787980/**81* Flag for whether this is a simple path (contains no arc segments).82* @type {boolean}83* @private84*/85goog.graphics.Path.prototype.simple_ = true;868788/**89* Path segment types.90* @enum {number}91*/92goog.graphics.Path.Segment = {93MOVETO: 0,94LINETO: 1,95CURVETO: 2,96ARCTO: 3,97CLOSE: 498};99100101/**102* The number of points for each segment type.103* @type {!Array<number>}104* @private105*/106goog.graphics.Path.segmentArgCounts_ = (function() {107var counts = [];108counts[goog.graphics.Path.Segment.MOVETO] = 2;109counts[goog.graphics.Path.Segment.LINETO] = 2;110counts[goog.graphics.Path.Segment.CURVETO] = 6;111counts[goog.graphics.Path.Segment.ARCTO] = 6;112counts[goog.graphics.Path.Segment.CLOSE] = 0;113return counts;114})();115116117/**118* Returns the number of points for a segment type.119*120* @param {number} segment The segment type.121* @return {number} The number of points.122*/123goog.graphics.Path.getSegmentCount = function(segment) {124return goog.graphics.Path.segmentArgCounts_[segment];125};126127128/**129* Appends another path to the end of this path.130*131* @param {!goog.graphics.Path} path The path to append.132* @return {!goog.graphics.Path} This path.133*/134goog.graphics.Path.prototype.appendPath = function(path) {135if (path.currentPoint_) {136Array.prototype.push.apply(this.segments_, path.segments_);137Array.prototype.push.apply(this.count_, path.count_);138Array.prototype.push.apply(this.arguments_, path.arguments_);139this.currentPoint_ = path.currentPoint_.concat();140this.closePoint_ = path.closePoint_.concat();141this.simple_ = this.simple_ && path.simple_;142}143return this;144};145146147/**148* Clears the path.149*150* @return {!goog.graphics.Path} The path itself.151*/152goog.graphics.Path.prototype.clear = function() {153this.segments_.length = 0;154this.count_.length = 0;155this.arguments_.length = 0;156delete this.closePoint_;157delete this.currentPoint_;158delete this.simple_;159return this;160};161162163/**164* Adds a point to the path by moving to the specified point. Repeated moveTo165* commands are collapsed into a single moveTo.166*167* @param {number} x X coordinate of destination point.168* @param {number} y Y coordinate of destination point.169* @return {!goog.graphics.Path} The path itself.170*/171goog.graphics.Path.prototype.moveTo = function(x, y) {172if (goog.array.peek(this.segments_) == goog.graphics.Path.Segment.MOVETO) {173this.arguments_.length -= 2;174} else {175this.segments_.push(goog.graphics.Path.Segment.MOVETO);176this.count_.push(1);177}178this.arguments_.push(x, y);179this.currentPoint_ = this.closePoint_ = [x, y];180return this;181};182183184/**185* Adds points to the path by drawing a straight line to each point.186*187* @param {...number} var_args The coordinates of each destination point as x, y188* value pairs.189* @return {!goog.graphics.Path} The path itself.190*/191goog.graphics.Path.prototype.lineTo = function(var_args) {192var lastSegment = goog.array.peek(this.segments_);193if (lastSegment == null) {194throw Error('Path cannot start with lineTo');195}196if (lastSegment != goog.graphics.Path.Segment.LINETO) {197this.segments_.push(goog.graphics.Path.Segment.LINETO);198this.count_.push(0);199}200for (var i = 0; i < arguments.length; i += 2) {201var x = arguments[i];202var y = arguments[i + 1];203this.arguments_.push(x, y);204}205this.count_[this.count_.length - 1] += i / 2;206this.currentPoint_ = [x, y];207return this;208};209210211/**212* Adds points to the path by drawing cubic Bezier curves. Each curve is213* specified using 3 points (6 coordinates) - two control points and the end214* point of the curve.215*216* @param {...number} var_args The coordinates specifying each curve in sets of217* 6 points: {@code [x1, y1]} the first control point, {@code [x2, y2]} the218* second control point and {@code [x, y]} the end point.219* @return {!goog.graphics.Path} The path itself.220*/221goog.graphics.Path.prototype.curveTo = function(var_args) {222var lastSegment = goog.array.peek(this.segments_);223if (lastSegment == null) {224throw Error('Path cannot start with curve');225}226if (lastSegment != goog.graphics.Path.Segment.CURVETO) {227this.segments_.push(goog.graphics.Path.Segment.CURVETO);228this.count_.push(0);229}230for (var i = 0; i < arguments.length; i += 6) {231var x = arguments[i + 4];232var y = arguments[i + 5];233this.arguments_.push(234arguments[i], arguments[i + 1], arguments[i + 2], arguments[i + 3], x,235y);236}237this.count_[this.count_.length - 1] += i / 6;238this.currentPoint_ = [x, y];239return this;240};241242243/**244* Adds a path command to close the path by connecting the245* last point to the first point.246*247* @return {!goog.graphics.Path} The path itself.248*/249goog.graphics.Path.prototype.close = function() {250var lastSegment = goog.array.peek(this.segments_);251if (lastSegment == null) {252throw Error('Path cannot start with close');253}254if (lastSegment != goog.graphics.Path.Segment.CLOSE) {255this.segments_.push(goog.graphics.Path.Segment.CLOSE);256this.count_.push(1);257this.currentPoint_ = this.closePoint_;258}259return this;260};261262263/**264* Adds a path command to draw an arc centered at the point {@code (cx, cy)}265* with radius {@code rx} along the x-axis and {@code ry} along the y-axis from266* {@code startAngle} through {@code extent} degrees. Positive rotation is in267* the direction from positive x-axis to positive y-axis.268*269* @param {number} cx X coordinate of center of ellipse.270* @param {number} cy Y coordinate of center of ellipse.271* @param {number} rx Radius of ellipse on x axis.272* @param {number} ry Radius of ellipse on y axis.273* @param {number} fromAngle Starting angle measured in degrees from the274* positive x-axis.275* @param {number} extent The span of the arc in degrees.276* @param {boolean} connect If true, the starting point of the arc is connected277* to the current point.278* @return {!goog.graphics.Path} The path itself.279* @deprecated Use {@code arcTo} or {@code arcToAsCurves} instead.280*/281goog.graphics.Path.prototype.arc = function(282cx, cy, rx, ry, fromAngle, extent, connect) {283var startX = cx + goog.math.angleDx(fromAngle, rx);284var startY = cy + goog.math.angleDy(fromAngle, ry);285if (connect) {286if (!this.currentPoint_ || startX != this.currentPoint_[0] ||287startY != this.currentPoint_[1]) {288this.lineTo(startX, startY);289}290} else {291this.moveTo(startX, startY);292}293return this.arcTo(rx, ry, fromAngle, extent);294};295296297/**298* Adds a path command to draw an arc starting at the path's current point,299* with radius {@code rx} along the x-axis and {@code ry} along the y-axis from300* {@code startAngle} through {@code extent} degrees. Positive rotation is in301* the direction from positive x-axis to positive y-axis.302*303* This method makes the path non-simple.304*305* @param {number} rx Radius of ellipse on x axis.306* @param {number} ry Radius of ellipse on y axis.307* @param {number} fromAngle Starting angle measured in degrees from the308* positive x-axis.309* @param {number} extent The span of the arc in degrees.310* @return {!goog.graphics.Path} The path itself.311*/312goog.graphics.Path.prototype.arcTo = function(rx, ry, fromAngle, extent) {313var cx = this.currentPoint_[0] - goog.math.angleDx(fromAngle, rx);314var cy = this.currentPoint_[1] - goog.math.angleDy(fromAngle, ry);315var ex = cx + goog.math.angleDx(fromAngle + extent, rx);316var ey = cy + goog.math.angleDy(fromAngle + extent, ry);317this.segments_.push(goog.graphics.Path.Segment.ARCTO);318this.count_.push(1);319this.arguments_.push(rx, ry, fromAngle, extent, ex, ey);320this.simple_ = false;321this.currentPoint_ = [ex, ey];322return this;323};324325326/**327* Same as {@code arcTo}, but approximates the arc using bezier curves.328.* As a result, this method does not affect the simplified status of this path.329* The algorithm is adapted from {@code java.awt.geom.ArcIterator}.330*331* @param {number} rx Radius of ellipse on x axis.332* @param {number} ry Radius of ellipse on y axis.333* @param {number} fromAngle Starting angle measured in degrees from the334* positive x-axis.335* @param {number} extent The span of the arc in degrees.336* @return {!goog.graphics.Path} The path itself.337*/338goog.graphics.Path.prototype.arcToAsCurves = function(339rx, ry, fromAngle, extent) {340var cx = this.currentPoint_[0] - goog.math.angleDx(fromAngle, rx);341var cy = this.currentPoint_[1] - goog.math.angleDy(fromAngle, ry);342var extentRad = goog.math.toRadians(extent);343var arcSegs = Math.ceil(Math.abs(extentRad) / Math.PI * 2);344var inc = extentRad / arcSegs;345var angle = goog.math.toRadians(fromAngle);346for (var j = 0; j < arcSegs; j++) {347var relX = Math.cos(angle);348var relY = Math.sin(angle);349var z = 4 / 3 * Math.sin(inc / 2) / (1 + Math.cos(inc / 2));350var c0 = cx + (relX - z * relY) * rx;351var c1 = cy + (relY + z * relX) * ry;352angle += inc;353relX = Math.cos(angle);354relY = Math.sin(angle);355this.curveTo(356c0, c1, cx + (relX + z * relY) * rx, cy + (relY - z * relX) * ry,357cx + relX * rx, cy + relY * ry);358}359return this;360};361362363/**364* Iterates over the path calling the supplied callback once for each path365* segment. The arguments to the callback function are the segment type and366* an array of its arguments.367*368* The {@code LINETO} and {@code CURVETO} arrays can contain multiple369* segments of the same type. The number of segments is the length of the370* array divided by the segment length (2 for lines, 6 for curves).371*372* As a convenience the {@code ARCTO} segment also includes the end point as the373* last two arguments: {@code rx, ry, fromAngle, extent, x, y}.374*375* @param {function(number, Array)} callback The function to call with each376* path segment.377*/378goog.graphics.Path.prototype.forEachSegment = function(callback) {379var points = this.arguments_;380var index = 0;381for (var i = 0, length = this.segments_.length; i < length; i++) {382var seg = this.segments_[i];383var n = goog.graphics.Path.segmentArgCounts_[seg] * this.count_[i];384callback(seg, points.slice(index, index + n));385index += n;386}387};388389390/**391* Returns the coordinates most recently added to the end of the path.392*393* @return {Array<number>?} An array containing the ending coordinates of the394* path of the form {@code [x, y]}.395*/396goog.graphics.Path.prototype.getCurrentPoint = function() {397return this.currentPoint_ && this.currentPoint_.concat();398};399400401/**402* @return {!goog.graphics.Path} A copy of this path.403*/404goog.graphics.Path.prototype.clone = function() {405var path = new this.constructor();406path.segments_ = this.segments_.concat();407path.count_ = this.count_.concat();408path.arguments_ = this.arguments_.concat();409path.closePoint_ = this.closePoint_ && this.closePoint_.concat();410path.currentPoint_ = this.currentPoint_ && this.currentPoint_.concat();411path.simple_ = this.simple_;412return path;413};414415416/**417* Returns true if this path contains no arcs. Simplified paths can be418* created using {@code createSimplifiedPath}.419*420* @return {boolean} True if the path contains no arcs.421*/422goog.graphics.Path.prototype.isSimple = function() {423return this.simple_;424};425426427/**428* A map from segment type to the path function to call to simplify a path.429* @type {!Object}430* @private431* @suppress {deprecated} goog.graphics.Path is deprecated.432*/433goog.graphics.Path.simplifySegmentMap_ = (function() {434var map = {};435map[goog.graphics.Path.Segment.MOVETO] = goog.graphics.Path.prototype.moveTo;436map[goog.graphics.Path.Segment.LINETO] = goog.graphics.Path.prototype.lineTo;437map[goog.graphics.Path.Segment.CLOSE] = goog.graphics.Path.prototype.close;438map[goog.graphics.Path.Segment.CURVETO] =439goog.graphics.Path.prototype.curveTo;440map[goog.graphics.Path.Segment.ARCTO] =441goog.graphics.Path.prototype.arcToAsCurves;442return map;443})();444445446/**447* Creates a copy of the given path, replacing {@code arcTo} with448* {@code arcToAsCurves}. The resulting path is simplified and can449* be transformed.450*451* @param {!goog.graphics.Path} src The path to simplify.452* @return {!goog.graphics.Path} A new simplified path.453* @suppress {deprecated} goog.graphics is deprecated.454*/455goog.graphics.Path.createSimplifiedPath = function(src) {456if (src.isSimple()) {457return src.clone();458}459var path = new goog.graphics.Path();460src.forEachSegment(function(segment, args) {461goog.graphics.Path.simplifySegmentMap_[segment].apply(path, args);462});463return path;464};465466467// TODO(chrisn): Delete this method468/**469* Creates a transformed copy of this path. The path is simplified470* {@see #createSimplifiedPath} prior to transformation.471*472* @param {!goog.graphics.AffineTransform} tx The transformation to perform.473* @return {!goog.graphics.Path} A new, transformed path.474*/475goog.graphics.Path.prototype.createTransformedPath = function(tx) {476var path = goog.graphics.Path.createSimplifiedPath(this);477path.transform(tx);478return path;479};480481482/**483* Transforms the path. Only simple paths are transformable. Attempting484* to transform a non-simple path will throw an error.485*486* @param {!goog.graphics.AffineTransform} tx The transformation to perform.487* @return {!goog.graphics.Path} The path itself.488*/489goog.graphics.Path.prototype.transform = function(tx) {490if (!this.isSimple()) {491throw Error('Non-simple path');492}493tx.transform(494this.arguments_, 0, this.arguments_, 0, this.arguments_.length / 2);495if (this.closePoint_) {496tx.transform(this.closePoint_, 0, this.closePoint_, 0, 1);497}498if (this.currentPoint_ && this.closePoint_ != this.currentPoint_) {499tx.transform(this.currentPoint_, 0, this.currentPoint_, 0, 1);500}501return this;502};503504505/**506* @return {boolean} Whether the path is empty.507*/508goog.graphics.Path.prototype.isEmpty = function() {509return this.segments_.length == 0;510};511512513