path.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  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.math.Path');
  19. goog.provide('goog.math.Path.Segment');
  20. goog.require('goog.array');
  21. goog.require('goog.math');
  22. goog.require('goog.math.AffineTransform');
  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. * @struct
  32. * @constructor
  33. * @final
  34. */
  35. goog.math.Path = function() {
  36. /**
  37. * The segment types that constitute this path.
  38. * @private {!Array<goog.math.Path.Segment>}
  39. */
  40. this.segments_ = [];
  41. /**
  42. * The number of repeated segments of the current type.
  43. * @type {!Array<number>}
  44. * @private
  45. */
  46. this.count_ = [];
  47. /**
  48. * The arguments corresponding to each of the segments.
  49. * @type {!Array<number>}
  50. * @private
  51. */
  52. this.arguments_ = [];
  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. this.closePoint_ = null;
  60. /**
  61. * The coordinates most recently added to the end of the path.
  62. * @type {Array<number>?}
  63. * @private
  64. */
  65. this.currentPoint_ = null;
  66. /**
  67. * Flag for whether this is a simple path (contains no arc segments).
  68. * @type {boolean}
  69. * @private
  70. */
  71. this.simple_ = true;
  72. };
  73. /**
  74. * Path segment types.
  75. * @enum {number}
  76. */
  77. goog.math.Path.Segment = {
  78. MOVETO: 0,
  79. LINETO: 1,
  80. CURVETO: 2,
  81. ARCTO: 3,
  82. CLOSE: 4
  83. };
  84. /**
  85. * The number of points for each segment type.
  86. * @type {!Array<number>}
  87. * @private
  88. */
  89. goog.math.Path.segmentArgCounts_ = (function() {
  90. var counts = [];
  91. counts[goog.math.Path.Segment.MOVETO] = 2;
  92. counts[goog.math.Path.Segment.LINETO] = 2;
  93. counts[goog.math.Path.Segment.CURVETO] = 6;
  94. counts[goog.math.Path.Segment.ARCTO] = 6;
  95. counts[goog.math.Path.Segment.CLOSE] = 0;
  96. return counts;
  97. })();
  98. /**
  99. * Returns an array of the segment types in this path, in the order of their
  100. * appearance. Adjacent segments of the same type are collapsed into a single
  101. * entry in the array. The returned array is a copy; modifications are not
  102. * reflected in the Path object.
  103. * @return {!Array<number>}
  104. */
  105. goog.math.Path.prototype.getSegmentTypes = function() {
  106. return this.segments_.concat();
  107. };
  108. /**
  109. * Returns an array of the number of times each segment type repeats in this
  110. * path, in order. The returned array is a copy; modifications are not reflected
  111. * in the Path object.
  112. * @return {!Array<number>}
  113. */
  114. goog.math.Path.prototype.getSegmentCounts = function() {
  115. return this.count_.concat();
  116. };
  117. /**
  118. * Returns an array of all arguments for the segments of this path object, in
  119. * order. The returned array is a copy; modifications are not reflected in the
  120. * Path object.
  121. * @return {!Array<number>}
  122. */
  123. goog.math.Path.prototype.getSegmentArgs = function() {
  124. return this.arguments_.concat();
  125. };
  126. /**
  127. * Returns the number of points for a segment type.
  128. *
  129. * @param {number} segment The segment type.
  130. * @return {number} The number of points.
  131. */
  132. goog.math.Path.getSegmentCount = function(segment) {
  133. return goog.math.Path.segmentArgCounts_[segment];
  134. };
  135. /**
  136. * Appends another path to the end of this path.
  137. *
  138. * @param {!goog.math.Path} path The path to append.
  139. * @return {!goog.math.Path} This path.
  140. */
  141. goog.math.Path.prototype.appendPath = function(path) {
  142. if (path.currentPoint_) {
  143. Array.prototype.push.apply(this.segments_, path.segments_);
  144. Array.prototype.push.apply(this.count_, path.count_);
  145. Array.prototype.push.apply(this.arguments_, path.arguments_);
  146. this.currentPoint_ = path.currentPoint_.concat();
  147. this.closePoint_ = path.closePoint_.concat();
  148. this.simple_ = this.simple_ && path.simple_;
  149. }
  150. return this;
  151. };
  152. /**
  153. * Clears the path.
  154. *
  155. * @return {!goog.math.Path} The path itself.
  156. */
  157. goog.math.Path.prototype.clear = function() {
  158. this.segments_.length = 0;
  159. this.count_.length = 0;
  160. this.arguments_.length = 0;
  161. this.closePoint_ = null;
  162. this.currentPoint_ = null;
  163. this.simple_ = true;
  164. return this;
  165. };
  166. /**
  167. * Adds a point to the path by moving to the specified point. Repeated moveTo
  168. * commands are collapsed into a single moveTo.
  169. *
  170. * @param {number} x X coordinate of destination point.
  171. * @param {number} y Y coordinate of destination point.
  172. * @return {!goog.math.Path} The path itself.
  173. */
  174. goog.math.Path.prototype.moveTo = function(x, y) {
  175. if (goog.array.peek(this.segments_) == goog.math.Path.Segment.MOVETO) {
  176. this.arguments_.length -= 2;
  177. } else {
  178. this.segments_.push(goog.math.Path.Segment.MOVETO);
  179. this.count_.push(1);
  180. }
  181. this.arguments_.push(x, y);
  182. this.currentPoint_ = this.closePoint_ = [x, y];
  183. return this;
  184. };
  185. /**
  186. * Adds points to the path by drawing a straight line to each point.
  187. *
  188. * @param {...number} var_args The coordinates of each destination point as x, y
  189. * value pairs.
  190. * @return {!goog.math.Path} The path itself.
  191. */
  192. goog.math.Path.prototype.lineTo = function(var_args) {
  193. return this.lineTo_(arguments);
  194. };
  195. /**
  196. * Adds points to the path by drawing a straight line to each point.
  197. *
  198. * @param {!Array<number>} coordinates The coordinates of each
  199. * destination point as x, y value pairs.
  200. * @return {!goog.math.Path} The path itself.
  201. */
  202. goog.math.Path.prototype.lineToFromArray = function(coordinates) {
  203. return this.lineTo_(coordinates);
  204. };
  205. /**
  206. * Adds points to the path by drawing a straight line to each point.
  207. *
  208. * @param {!Array<number>|Arguments} coordinates The coordinates of each
  209. * destination point as x, y value pairs.
  210. * @return {!goog.math.Path} The path itself.
  211. * @private
  212. */
  213. goog.math.Path.prototype.lineTo_ = function(coordinates) {
  214. var lastSegment = goog.array.peek(this.segments_);
  215. if (lastSegment == null) {
  216. throw Error('Path cannot start with lineTo');
  217. }
  218. if (lastSegment != goog.math.Path.Segment.LINETO) {
  219. this.segments_.push(goog.math.Path.Segment.LINETO);
  220. this.count_.push(0);
  221. }
  222. for (var i = 0; i < coordinates.length; i += 2) {
  223. var x = coordinates[i];
  224. var y = coordinates[i + 1];
  225. this.arguments_.push(x, y);
  226. }
  227. this.count_[this.count_.length - 1] += i / 2;
  228. this.currentPoint_ = [x, y];
  229. return this;
  230. };
  231. /**
  232. * Adds points to the path by drawing cubic Bezier curves. Each curve is
  233. * specified using 3 points (6 coordinates) - two control points and the end
  234. * point of the curve.
  235. *
  236. * @param {...number} var_args The coordinates specifying each curve in sets of
  237. * 6 points: {@code [x1, y1]} the first control point, {@code [x2, y2]} the
  238. * second control point and {@code [x, y]} the end point.
  239. * @return {!goog.math.Path} The path itself.
  240. */
  241. goog.math.Path.prototype.curveTo = function(var_args) {
  242. return this.curveTo_(arguments);
  243. };
  244. /**
  245. * Adds points to the path by drawing cubic Bezier curves. Each curve is
  246. * specified using 3 points (6 coordinates) - two control points and the end
  247. * point of the curve.
  248. *
  249. * @param {!Array<number>} coordinates The coordinates specifying
  250. * each curve in sets of 6 points: {@code [x1, y1]} the first control point,
  251. * {@code [x2, y2]} the second control point and {@code [x, y]} the end
  252. * point.
  253. * @return {!goog.math.Path} The path itself.
  254. */
  255. goog.math.Path.prototype.curveToFromArray = function(coordinates) {
  256. return this.curveTo_(coordinates);
  257. };
  258. /**
  259. * Adds points to the path by drawing cubic Bezier curves. Each curve is
  260. * specified using 3 points (6 coordinates) - two control points and the end
  261. * point of the curve.
  262. *
  263. * @param {!Array<number>|Arguments} coordinates The coordinates specifying
  264. * each curve in sets of 6 points: {@code [x1, y1]} the first control point,
  265. * {@code [x2, y2]} the second control point and {@code [x, y]} the end
  266. * point.
  267. * @return {!goog.math.Path} The path itself.
  268. * @private
  269. */
  270. goog.math.Path.prototype.curveTo_ = function(coordinates) {
  271. var lastSegment = goog.array.peek(this.segments_);
  272. if (lastSegment == null) {
  273. throw Error('Path cannot start with curve');
  274. }
  275. if (lastSegment != goog.math.Path.Segment.CURVETO) {
  276. this.segments_.push(goog.math.Path.Segment.CURVETO);
  277. this.count_.push(0);
  278. }
  279. for (var i = 0; i < coordinates.length; i += 6) {
  280. var x = coordinates[i + 4];
  281. var y = coordinates[i + 5];
  282. this.arguments_.push(
  283. coordinates[i], coordinates[i + 1], coordinates[i + 2],
  284. coordinates[i + 3], x, y);
  285. }
  286. this.count_[this.count_.length - 1] += i / 6;
  287. this.currentPoint_ = [x, y];
  288. return this;
  289. };
  290. /**
  291. * Adds a path command to close the path by connecting the
  292. * last point to the first point.
  293. *
  294. * @return {!goog.math.Path} The path itself.
  295. */
  296. goog.math.Path.prototype.close = function() {
  297. var lastSegment = goog.array.peek(this.segments_);
  298. if (lastSegment == null) {
  299. throw Error('Path cannot start with close');
  300. }
  301. if (lastSegment != goog.math.Path.Segment.CLOSE) {
  302. this.segments_.push(goog.math.Path.Segment.CLOSE);
  303. this.count_.push(1);
  304. this.currentPoint_ = this.closePoint_;
  305. }
  306. return this;
  307. };
  308. /**
  309. * Adds a path command to draw an arc centered at the point {@code (cx, cy)}
  310. * with radius {@code rx} along the x-axis and {@code ry} along the y-axis from
  311. * {@code startAngle} through {@code extent} degrees. Positive rotation is in
  312. * the direction from positive x-axis to positive y-axis.
  313. *
  314. * @param {number} cx X coordinate of center of ellipse.
  315. * @param {number} cy Y coordinate of center of ellipse.
  316. * @param {number} rx Radius of ellipse on x axis.
  317. * @param {number} ry Radius of ellipse on y axis.
  318. * @param {number} fromAngle Starting angle measured in degrees from the
  319. * positive x-axis.
  320. * @param {number} extent The span of the arc in degrees.
  321. * @param {boolean} connect If true, the starting point of the arc is connected
  322. * to the current point.
  323. * @return {!goog.math.Path} The path itself.
  324. * @deprecated Use {@code arcTo} or {@code arcToAsCurves} instead.
  325. */
  326. goog.math.Path.prototype.arc = function(
  327. cx, cy, rx, ry, fromAngle, extent, connect) {
  328. var startX = cx + goog.math.angleDx(fromAngle, rx);
  329. var startY = cy + goog.math.angleDy(fromAngle, ry);
  330. if (connect) {
  331. if (!this.currentPoint_ || startX != this.currentPoint_[0] ||
  332. startY != this.currentPoint_[1]) {
  333. this.lineTo(startX, startY);
  334. }
  335. } else {
  336. this.moveTo(startX, startY);
  337. }
  338. return this.arcTo(rx, ry, fromAngle, extent);
  339. };
  340. /**
  341. * Adds a path command to draw an arc starting at the path's current point,
  342. * with radius {@code rx} along the x-axis and {@code ry} along the y-axis from
  343. * {@code startAngle} through {@code extent} degrees. Positive rotation is in
  344. * the direction from positive x-axis to positive y-axis.
  345. *
  346. * This method makes the path non-simple.
  347. *
  348. * @param {number} rx Radius of ellipse on x axis.
  349. * @param {number} ry Radius of ellipse on y axis.
  350. * @param {number} fromAngle Starting angle measured in degrees from the
  351. * positive x-axis.
  352. * @param {number} extent The span of the arc in degrees.
  353. * @return {!goog.math.Path} The path itself.
  354. */
  355. goog.math.Path.prototype.arcTo = function(rx, ry, fromAngle, extent) {
  356. var cx = this.currentPoint_[0] - goog.math.angleDx(fromAngle, rx);
  357. var cy = this.currentPoint_[1] - goog.math.angleDy(fromAngle, ry);
  358. var ex = cx + goog.math.angleDx(fromAngle + extent, rx);
  359. var ey = cy + goog.math.angleDy(fromAngle + extent, ry);
  360. this.segments_.push(goog.math.Path.Segment.ARCTO);
  361. this.count_.push(1);
  362. this.arguments_.push(rx, ry, fromAngle, extent, ex, ey);
  363. this.simple_ = false;
  364. this.currentPoint_ = [ex, ey];
  365. return this;
  366. };
  367. /**
  368. * Same as {@code arcTo}, but approximates the arc using bezier curves.
  369. .* As a result, this method does not affect the simplified status of this path.
  370. * The algorithm is adapted from {@code java.awt.geom.ArcIterator}.
  371. *
  372. * @param {number} rx Radius of ellipse on x axis.
  373. * @param {number} ry Radius of ellipse on y axis.
  374. * @param {number} fromAngle Starting angle measured in degrees from the
  375. * positive x-axis.
  376. * @param {number} extent The span of the arc in degrees.
  377. * @return {!goog.math.Path} The path itself.
  378. */
  379. goog.math.Path.prototype.arcToAsCurves = function(rx, ry, fromAngle, extent) {
  380. var cx = this.currentPoint_[0] - goog.math.angleDx(fromAngle, rx);
  381. var cy = this.currentPoint_[1] - goog.math.angleDy(fromAngle, ry);
  382. var extentRad = goog.math.toRadians(extent);
  383. var arcSegs = Math.ceil(Math.abs(extentRad) / Math.PI * 2);
  384. var inc = extentRad / arcSegs;
  385. var angle = goog.math.toRadians(fromAngle);
  386. for (var j = 0; j < arcSegs; j++) {
  387. var relX = Math.cos(angle);
  388. var relY = Math.sin(angle);
  389. var z = 4 / 3 * Math.sin(inc / 2) / (1 + Math.cos(inc / 2));
  390. var c0 = cx + (relX - z * relY) * rx;
  391. var c1 = cy + (relY + z * relX) * ry;
  392. angle += inc;
  393. relX = Math.cos(angle);
  394. relY = Math.sin(angle);
  395. this.curveTo(
  396. c0, c1, cx + (relX + z * relY) * rx, cy + (relY - z * relX) * ry,
  397. cx + relX * rx, cy + relY * ry);
  398. }
  399. return this;
  400. };
  401. /**
  402. * Iterates over the path calling the supplied callback once for each path
  403. * segment. The arguments to the callback function are the segment type and
  404. * an array of its arguments.
  405. *
  406. * The {@code LINETO} and {@code CURVETO} arrays can contain multiple
  407. * segments of the same type. The number of segments is the length of the
  408. * array divided by the segment length (2 for lines, 6 for curves).
  409. *
  410. * As a convenience the {@code ARCTO} segment also includes the end point as the
  411. * last two arguments: {@code rx, ry, fromAngle, extent, x, y}.
  412. *
  413. * @param {function(!goog.math.Path.Segment, !Array<number>)} callback
  414. * The function to call with each path segment.
  415. */
  416. goog.math.Path.prototype.forEachSegment = function(callback) {
  417. var points = this.arguments_;
  418. var index = 0;
  419. for (var i = 0, length = this.segments_.length; i < length; i++) {
  420. var seg = this.segments_[i];
  421. var n = goog.math.Path.segmentArgCounts_[seg] * this.count_[i];
  422. callback(seg, points.slice(index, index + n));
  423. index += n;
  424. }
  425. };
  426. /**
  427. * Returns the coordinates most recently added to the end of the path.
  428. *
  429. * @return {Array<number>?} An array containing the ending coordinates of the
  430. * path of the form {@code [x, y]}.
  431. */
  432. goog.math.Path.prototype.getCurrentPoint = function() {
  433. return this.currentPoint_ && this.currentPoint_.concat();
  434. };
  435. /**
  436. * @return {!goog.math.Path} A copy of this path.
  437. */
  438. goog.math.Path.prototype.clone = function() {
  439. var path = new goog.math.Path();
  440. path.segments_ = this.segments_.concat();
  441. path.count_ = this.count_.concat();
  442. path.arguments_ = this.arguments_.concat();
  443. path.closePoint_ = this.closePoint_ && this.closePoint_.concat();
  444. path.currentPoint_ = this.currentPoint_ && this.currentPoint_.concat();
  445. path.simple_ = this.simple_;
  446. return path;
  447. };
  448. /**
  449. * Returns true if this path contains no arcs. Simplified paths can be
  450. * created using {@code createSimplifiedPath}.
  451. *
  452. * @return {boolean} True if the path contains no arcs.
  453. */
  454. goog.math.Path.prototype.isSimple = function() {
  455. return this.simple_;
  456. };
  457. /**
  458. * A map from segment type to the path function to call to simplify a path.
  459. * @private {!Object<goog.math.Path.Segment, function(this: goog.math.Path)>}
  460. */
  461. goog.math.Path.simplifySegmentMap_ = (function() {
  462. var map = {};
  463. map[goog.math.Path.Segment.MOVETO] = goog.math.Path.prototype.moveTo;
  464. map[goog.math.Path.Segment.LINETO] = goog.math.Path.prototype.lineTo;
  465. map[goog.math.Path.Segment.CLOSE] = goog.math.Path.prototype.close;
  466. map[goog.math.Path.Segment.CURVETO] = goog.math.Path.prototype.curveTo;
  467. map[goog.math.Path.Segment.ARCTO] = goog.math.Path.prototype.arcToAsCurves;
  468. return map;
  469. })();
  470. /**
  471. * Creates a copy of the given path, replacing {@code arcTo} with
  472. * {@code arcToAsCurves}. The resulting path is simplified and can
  473. * be transformed.
  474. *
  475. * @param {!goog.math.Path} src The path to simplify.
  476. * @return {!goog.math.Path} A new simplified path.
  477. */
  478. goog.math.Path.createSimplifiedPath = function(src) {
  479. if (src.isSimple()) {
  480. return src.clone();
  481. }
  482. var path = new goog.math.Path();
  483. src.forEachSegment(function(segment, args) {
  484. goog.math.Path.simplifySegmentMap_[segment].apply(path, args);
  485. });
  486. return path;
  487. };
  488. // TODO(chrisn): Delete this method
  489. /**
  490. * Creates a transformed copy of this path. The path is simplified
  491. * {@see #createSimplifiedPath} prior to transformation.
  492. *
  493. * @param {!goog.math.AffineTransform} tx The transformation to perform.
  494. * @return {!goog.math.Path} A new, transformed path.
  495. */
  496. goog.math.Path.prototype.createTransformedPath = function(tx) {
  497. var path = goog.math.Path.createSimplifiedPath(this);
  498. path.transform(tx);
  499. return path;
  500. };
  501. /**
  502. * Transforms the path. Only simple paths are transformable. Attempting
  503. * to transform a non-simple path will throw an error.
  504. *
  505. * @param {!goog.math.AffineTransform} tx The transformation to perform.
  506. * @return {!goog.math.Path} The path itself.
  507. */
  508. goog.math.Path.prototype.transform = function(tx) {
  509. if (!this.isSimple()) {
  510. throw Error('Non-simple path');
  511. }
  512. tx.transform(
  513. this.arguments_, 0, this.arguments_, 0, this.arguments_.length / 2);
  514. if (this.closePoint_) {
  515. tx.transform(this.closePoint_, 0, this.closePoint_, 0, 1);
  516. }
  517. if (this.currentPoint_ && this.closePoint_ != this.currentPoint_) {
  518. tx.transform(this.currentPoint_, 0, this.currentPoint_, 0, 1);
  519. }
  520. return this;
  521. };
  522. /**
  523. * @return {boolean} Whether the path is empty.
  524. */
  525. goog.math.Path.prototype.isEmpty = function() {
  526. return this.segments_.length == 0;
  527. };