path.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. // Copyright 2007 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Represents a path used with a Graphics implementation.
  16. * @author arv@google.com (Erik Arvidsson)
  17. */
  18. goog.provide('goog.graphics.Path');
  19. goog.provide('goog.graphics.Path.Segment');
  20. goog.require('goog.array');
  21. goog.require('goog.graphics.AffineTransform');
  22. goog.require('goog.math');
  23. /**
  24. * Creates a path object. A path is a sequence of segments and may be open or
  25. * closed. Path uses the EVEN-ODD fill rule for determining the interior of the
  26. * path. A path must start with a moveTo command.
  27. *
  28. * A "simple" path does not contain any arcs and may be transformed using
  29. * the {@code transform} method.
  30. *
  31. * @constructor
  32. */
  33. goog.graphics.Path = function() {
  34. /**
  35. * The segment types that constitute this path.
  36. * @type {!Array<number>}
  37. * @private
  38. */
  39. this.segments_ = [];
  40. /**
  41. * The number of repeated segments of the current type.
  42. * @type {!Array<number>}
  43. * @private
  44. */
  45. this.count_ = [];
  46. /**
  47. * The arguments corresponding to each of the segments.
  48. * @type {!Array<number>}
  49. * @private
  50. */
  51. this.arguments_ = [];
  52. };
  53. /**
  54. * The coordinates of the point which closes the path (the point of the
  55. * last moveTo command).
  56. * @type {Array<number>?}
  57. * @private
  58. */
  59. goog.graphics.Path.prototype.closePoint_ = null;
  60. /**
  61. * The coordinates most recently added to the end of the path.
  62. * @type {Array<number>?}
  63. * @private
  64. */
  65. goog.graphics.Path.prototype.currentPoint_ = null;
  66. /**
  67. * Flag for whether this is a simple path (contains no arc segments).
  68. * @type {boolean}
  69. * @private
  70. */
  71. goog.graphics.Path.prototype.simple_ = true;
  72. /**
  73. * Path segment types.
  74. * @enum {number}
  75. */
  76. goog.graphics.Path.Segment = {
  77. MOVETO: 0,
  78. LINETO: 1,
  79. CURVETO: 2,
  80. ARCTO: 3,
  81. CLOSE: 4
  82. };
  83. /**
  84. * The number of points for each segment type.
  85. * @type {!Array<number>}
  86. * @private
  87. */
  88. goog.graphics.Path.segmentArgCounts_ = (function() {
  89. var counts = [];
  90. counts[goog.graphics.Path.Segment.MOVETO] = 2;
  91. counts[goog.graphics.Path.Segment.LINETO] = 2;
  92. counts[goog.graphics.Path.Segment.CURVETO] = 6;
  93. counts[goog.graphics.Path.Segment.ARCTO] = 6;
  94. counts[goog.graphics.Path.Segment.CLOSE] = 0;
  95. return counts;
  96. })();
  97. /**
  98. * Returns the number of points for a segment type.
  99. *
  100. * @param {number} segment The segment type.
  101. * @return {number} The number of points.
  102. */
  103. goog.graphics.Path.getSegmentCount = function(segment) {
  104. return goog.graphics.Path.segmentArgCounts_[segment];
  105. };
  106. /**
  107. * Appends another path to the end of this path.
  108. *
  109. * @param {!goog.graphics.Path} path The path to append.
  110. * @return {!goog.graphics.Path} This path.
  111. */
  112. goog.graphics.Path.prototype.appendPath = function(path) {
  113. if (path.currentPoint_) {
  114. Array.prototype.push.apply(this.segments_, path.segments_);
  115. Array.prototype.push.apply(this.count_, path.count_);
  116. Array.prototype.push.apply(this.arguments_, path.arguments_);
  117. this.currentPoint_ = path.currentPoint_.concat();
  118. this.closePoint_ = path.closePoint_.concat();
  119. this.simple_ = this.simple_ && path.simple_;
  120. }
  121. return this;
  122. };
  123. /**
  124. * Clears the path.
  125. *
  126. * @return {!goog.graphics.Path} The path itself.
  127. */
  128. goog.graphics.Path.prototype.clear = function() {
  129. this.segments_.length = 0;
  130. this.count_.length = 0;
  131. this.arguments_.length = 0;
  132. delete this.closePoint_;
  133. delete this.currentPoint_;
  134. delete this.simple_;
  135. return this;
  136. };
  137. /**
  138. * Adds a point to the path by moving to the specified point. Repeated moveTo
  139. * commands are collapsed into a single moveTo.
  140. *
  141. * @param {number} x X coordinate of destination point.
  142. * @param {number} y Y coordinate of destination point.
  143. * @return {!goog.graphics.Path} The path itself.
  144. */
  145. goog.graphics.Path.prototype.moveTo = function(x, y) {
  146. if (goog.array.peek(this.segments_) == goog.graphics.Path.Segment.MOVETO) {
  147. this.arguments_.length -= 2;
  148. } else {
  149. this.segments_.push(goog.graphics.Path.Segment.MOVETO);
  150. this.count_.push(1);
  151. }
  152. this.arguments_.push(x, y);
  153. this.currentPoint_ = this.closePoint_ = [x, y];
  154. return this;
  155. };
  156. /**
  157. * Adds points to the path by drawing a straight line to each point.
  158. *
  159. * @param {...number} var_args The coordinates of each destination point as x, y
  160. * value pairs.
  161. * @return {!goog.graphics.Path} The path itself.
  162. */
  163. goog.graphics.Path.prototype.lineTo = function(var_args) {
  164. var lastSegment = goog.array.peek(this.segments_);
  165. if (lastSegment == null) {
  166. throw Error('Path cannot start with lineTo');
  167. }
  168. if (lastSegment != goog.graphics.Path.Segment.LINETO) {
  169. this.segments_.push(goog.graphics.Path.Segment.LINETO);
  170. this.count_.push(0);
  171. }
  172. for (var i = 0; i < arguments.length; i += 2) {
  173. var x = arguments[i];
  174. var y = arguments[i + 1];
  175. this.arguments_.push(x, y);
  176. }
  177. this.count_[this.count_.length - 1] += i / 2;
  178. this.currentPoint_ = [x, y];
  179. return this;
  180. };
  181. /**
  182. * Adds points to the path by drawing cubic Bezier curves. Each curve is
  183. * specified using 3 points (6 coordinates) - two control points and the end
  184. * point of the curve.
  185. *
  186. * @param {...number} var_args The coordinates specifying each curve in sets of
  187. * 6 points: {@code [x1, y1]} the first control point, {@code [x2, y2]} the
  188. * second control point and {@code [x, y]} the end point.
  189. * @return {!goog.graphics.Path} The path itself.
  190. */
  191. goog.graphics.Path.prototype.curveTo = function(var_args) {
  192. var lastSegment = goog.array.peek(this.segments_);
  193. if (lastSegment == null) {
  194. throw Error('Path cannot start with curve');
  195. }
  196. if (lastSegment != goog.graphics.Path.Segment.CURVETO) {
  197. this.segments_.push(goog.graphics.Path.Segment.CURVETO);
  198. this.count_.push(0);
  199. }
  200. for (var i = 0; i < arguments.length; i += 6) {
  201. var x = arguments[i + 4];
  202. var y = arguments[i + 5];
  203. this.arguments_.push(
  204. arguments[i], arguments[i + 1], arguments[i + 2], arguments[i + 3], x,
  205. y);
  206. }
  207. this.count_[this.count_.length - 1] += i / 6;
  208. this.currentPoint_ = [x, y];
  209. return this;
  210. };
  211. /**
  212. * Adds a path command to close the path by connecting the
  213. * last point to the first point.
  214. *
  215. * @return {!goog.graphics.Path} The path itself.
  216. */
  217. goog.graphics.Path.prototype.close = function() {
  218. var lastSegment = goog.array.peek(this.segments_);
  219. if (lastSegment == null) {
  220. throw Error('Path cannot start with close');
  221. }
  222. if (lastSegment != goog.graphics.Path.Segment.CLOSE) {
  223. this.segments_.push(goog.graphics.Path.Segment.CLOSE);
  224. this.count_.push(1);
  225. this.currentPoint_ = this.closePoint_;
  226. }
  227. return this;
  228. };
  229. /**
  230. * Adds a path command to draw an arc centered at the point {@code (cx, cy)}
  231. * with radius {@code rx} along the x-axis and {@code ry} along the y-axis from
  232. * {@code startAngle} through {@code extent} degrees. Positive rotation is in
  233. * the direction from positive x-axis to positive y-axis.
  234. *
  235. * @param {number} cx X coordinate of center of ellipse.
  236. * @param {number} cy Y coordinate of center of ellipse.
  237. * @param {number} rx Radius of ellipse on x axis.
  238. * @param {number} ry Radius of ellipse on y axis.
  239. * @param {number} fromAngle Starting angle measured in degrees from the
  240. * positive x-axis.
  241. * @param {number} extent The span of the arc in degrees.
  242. * @param {boolean} connect If true, the starting point of the arc is connected
  243. * to the current point.
  244. * @return {!goog.graphics.Path} The path itself.
  245. * @deprecated Use {@code arcTo} or {@code arcToAsCurves} instead.
  246. */
  247. goog.graphics.Path.prototype.arc = function(
  248. cx, cy, rx, ry, fromAngle, extent, connect) {
  249. var startX = cx + goog.math.angleDx(fromAngle, rx);
  250. var startY = cy + goog.math.angleDy(fromAngle, ry);
  251. if (connect) {
  252. if (!this.currentPoint_ || startX != this.currentPoint_[0] ||
  253. startY != this.currentPoint_[1]) {
  254. this.lineTo(startX, startY);
  255. }
  256. } else {
  257. this.moveTo(startX, startY);
  258. }
  259. return this.arcTo(rx, ry, fromAngle, extent);
  260. };
  261. /**
  262. * Adds a path command to draw an arc starting at the path's current point,
  263. * with radius {@code rx} along the x-axis and {@code ry} along the y-axis from
  264. * {@code startAngle} through {@code extent} degrees. Positive rotation is in
  265. * the direction from positive x-axis to positive y-axis.
  266. *
  267. * This method makes the path non-simple.
  268. *
  269. * @param {number} rx Radius of ellipse on x axis.
  270. * @param {number} ry Radius of ellipse on y axis.
  271. * @param {number} fromAngle Starting angle measured in degrees from the
  272. * positive x-axis.
  273. * @param {number} extent The span of the arc in degrees.
  274. * @return {!goog.graphics.Path} The path itself.
  275. */
  276. goog.graphics.Path.prototype.arcTo = function(rx, ry, fromAngle, extent) {
  277. var cx = this.currentPoint_[0] - goog.math.angleDx(fromAngle, rx);
  278. var cy = this.currentPoint_[1] - goog.math.angleDy(fromAngle, ry);
  279. var ex = cx + goog.math.angleDx(fromAngle + extent, rx);
  280. var ey = cy + goog.math.angleDy(fromAngle + extent, ry);
  281. this.segments_.push(goog.graphics.Path.Segment.ARCTO);
  282. this.count_.push(1);
  283. this.arguments_.push(rx, ry, fromAngle, extent, ex, ey);
  284. this.simple_ = false;
  285. this.currentPoint_ = [ex, ey];
  286. return this;
  287. };
  288. /**
  289. * Same as {@code arcTo}, but approximates the arc using bezier curves.
  290. .* As a result, this method does not affect the simplified status of this path.
  291. * The algorithm is adapted from {@code java.awt.geom.ArcIterator}.
  292. *
  293. * @param {number} rx Radius of ellipse on x axis.
  294. * @param {number} ry Radius of ellipse on y axis.
  295. * @param {number} fromAngle Starting angle measured in degrees from the
  296. * positive x-axis.
  297. * @param {number} extent The span of the arc in degrees.
  298. * @return {!goog.graphics.Path} The path itself.
  299. */
  300. goog.graphics.Path.prototype.arcToAsCurves = function(
  301. rx, ry, fromAngle, extent) {
  302. var cx = this.currentPoint_[0] - goog.math.angleDx(fromAngle, rx);
  303. var cy = this.currentPoint_[1] - goog.math.angleDy(fromAngle, ry);
  304. var extentRad = goog.math.toRadians(extent);
  305. var arcSegs = Math.ceil(Math.abs(extentRad) / Math.PI * 2);
  306. var inc = extentRad / arcSegs;
  307. var angle = goog.math.toRadians(fromAngle);
  308. for (var j = 0; j < arcSegs; j++) {
  309. var relX = Math.cos(angle);
  310. var relY = Math.sin(angle);
  311. var z = 4 / 3 * Math.sin(inc / 2) / (1 + Math.cos(inc / 2));
  312. var c0 = cx + (relX - z * relY) * rx;
  313. var c1 = cy + (relY + z * relX) * ry;
  314. angle += inc;
  315. relX = Math.cos(angle);
  316. relY = Math.sin(angle);
  317. this.curveTo(
  318. c0, c1, cx + (relX + z * relY) * rx, cy + (relY - z * relX) * ry,
  319. cx + relX * rx, cy + relY * ry);
  320. }
  321. return this;
  322. };
  323. /**
  324. * Iterates over the path calling the supplied callback once for each path
  325. * segment. The arguments to the callback function are the segment type and
  326. * an array of its arguments.
  327. *
  328. * The {@code LINETO} and {@code CURVETO} arrays can contain multiple
  329. * segments of the same type. The number of segments is the length of the
  330. * array divided by the segment length (2 for lines, 6 for curves).
  331. *
  332. * As a convenience the {@code ARCTO} segment also includes the end point as the
  333. * last two arguments: {@code rx, ry, fromAngle, extent, x, y}.
  334. *
  335. * @param {function(number, Array)} callback The function to call with each
  336. * path segment.
  337. */
  338. goog.graphics.Path.prototype.forEachSegment = function(callback) {
  339. var points = this.arguments_;
  340. var index = 0;
  341. for (var i = 0, length = this.segments_.length; i < length; i++) {
  342. var seg = this.segments_[i];
  343. var n = goog.graphics.Path.segmentArgCounts_[seg] * this.count_[i];
  344. callback(seg, points.slice(index, index + n));
  345. index += n;
  346. }
  347. };
  348. /**
  349. * Returns the coordinates most recently added to the end of the path.
  350. *
  351. * @return {Array<number>?} An array containing the ending coordinates of the
  352. * path of the form {@code [x, y]}.
  353. */
  354. goog.graphics.Path.prototype.getCurrentPoint = function() {
  355. return this.currentPoint_ && this.currentPoint_.concat();
  356. };
  357. /**
  358. * @return {!goog.graphics.Path} A copy of this path.
  359. */
  360. goog.graphics.Path.prototype.clone = function() {
  361. var path = new this.constructor();
  362. path.segments_ = this.segments_.concat();
  363. path.count_ = this.count_.concat();
  364. path.arguments_ = this.arguments_.concat();
  365. path.closePoint_ = this.closePoint_ && this.closePoint_.concat();
  366. path.currentPoint_ = this.currentPoint_ && this.currentPoint_.concat();
  367. path.simple_ = this.simple_;
  368. return path;
  369. };
  370. /**
  371. * Returns true if this path contains no arcs. Simplified paths can be
  372. * created using {@code createSimplifiedPath}.
  373. *
  374. * @return {boolean} True if the path contains no arcs.
  375. */
  376. goog.graphics.Path.prototype.isSimple = function() {
  377. return this.simple_;
  378. };
  379. /**
  380. * A map from segment type to the path function to call to simplify a path.
  381. * @type {!Object}
  382. * @private
  383. * @suppress {deprecated} goog.graphics.Path is deprecated.
  384. */
  385. goog.graphics.Path.simplifySegmentMap_ = (function() {
  386. var map = {};
  387. map[goog.graphics.Path.Segment.MOVETO] = goog.graphics.Path.prototype.moveTo;
  388. map[goog.graphics.Path.Segment.LINETO] = goog.graphics.Path.prototype.lineTo;
  389. map[goog.graphics.Path.Segment.CLOSE] = goog.graphics.Path.prototype.close;
  390. map[goog.graphics.Path.Segment.CURVETO] =
  391. goog.graphics.Path.prototype.curveTo;
  392. map[goog.graphics.Path.Segment.ARCTO] =
  393. goog.graphics.Path.prototype.arcToAsCurves;
  394. return map;
  395. })();
  396. /**
  397. * Creates a copy of the given path, replacing {@code arcTo} with
  398. * {@code arcToAsCurves}. The resulting path is simplified and can
  399. * be transformed.
  400. *
  401. * @param {!goog.graphics.Path} src The path to simplify.
  402. * @return {!goog.graphics.Path} A new simplified path.
  403. * @suppress {deprecated} goog.graphics is deprecated.
  404. */
  405. goog.graphics.Path.createSimplifiedPath = function(src) {
  406. if (src.isSimple()) {
  407. return src.clone();
  408. }
  409. var path = new goog.graphics.Path();
  410. src.forEachSegment(function(segment, args) {
  411. goog.graphics.Path.simplifySegmentMap_[segment].apply(path, args);
  412. });
  413. return path;
  414. };
  415. // TODO(chrisn): Delete this method
  416. /**
  417. * Creates a transformed copy of this path. The path is simplified
  418. * {@see #createSimplifiedPath} prior to transformation.
  419. *
  420. * @param {!goog.graphics.AffineTransform} tx The transformation to perform.
  421. * @return {!goog.graphics.Path} A new, transformed path.
  422. */
  423. goog.graphics.Path.prototype.createTransformedPath = function(tx) {
  424. var path = goog.graphics.Path.createSimplifiedPath(this);
  425. path.transform(tx);
  426. return path;
  427. };
  428. /**
  429. * Transforms the path. Only simple paths are transformable. Attempting
  430. * to transform a non-simple path will throw an error.
  431. *
  432. * @param {!goog.graphics.AffineTransform} tx The transformation to perform.
  433. * @return {!goog.graphics.Path} The path itself.
  434. */
  435. goog.graphics.Path.prototype.transform = function(tx) {
  436. if (!this.isSimple()) {
  437. throw Error('Non-simple path');
  438. }
  439. tx.transform(
  440. this.arguments_, 0, this.arguments_, 0, this.arguments_.length / 2);
  441. if (this.closePoint_) {
  442. tx.transform(this.closePoint_, 0, this.closePoint_, 0, 1);
  443. }
  444. if (this.currentPoint_ && this.closePoint_ != this.currentPoint_) {
  445. tx.transform(this.currentPoint_, 0, this.currentPoint_, 0, 1);
  446. }
  447. return this;
  448. };
  449. /**
  450. * @return {boolean} Whether the path is empty.
  451. */
  452. goog.graphics.Path.prototype.isEmpty = function() {
  453. return this.segments_.length == 0;
  454. };