bezier.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  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 cubic Bezier curve.
  16. *
  17. * Uses the deCasteljau algorithm to compute points on the curve.
  18. * http://en.wikipedia.org/wiki/De_Casteljau's_algorithm
  19. *
  20. * Currently it uses an unrolled version of the algorithm for speed. Eventually
  21. * it may be useful to use the loop form of the algorithm in order to support
  22. * curves of arbitrary degree.
  23. *
  24. * @author robbyw@google.com (Robby Walker)
  25. */
  26. goog.provide('goog.math.Bezier');
  27. goog.require('goog.math');
  28. goog.require('goog.math.Coordinate');
  29. /**
  30. * Object representing a cubic bezier curve.
  31. * @param {number} x0 X coordinate of the start point.
  32. * @param {number} y0 Y coordinate of the start point.
  33. * @param {number} x1 X coordinate of the first control point.
  34. * @param {number} y1 Y coordinate of the first control point.
  35. * @param {number} x2 X coordinate of the second control point.
  36. * @param {number} y2 Y coordinate of the second control point.
  37. * @param {number} x3 X coordinate of the end point.
  38. * @param {number} y3 Y coordinate of the end point.
  39. * @struct
  40. * @constructor
  41. * @final
  42. */
  43. goog.math.Bezier = function(x0, y0, x1, y1, x2, y2, x3, y3) {
  44. /**
  45. * X coordinate of the first point.
  46. * @type {number}
  47. */
  48. this.x0 = x0;
  49. /**
  50. * Y coordinate of the first point.
  51. * @type {number}
  52. */
  53. this.y0 = y0;
  54. /**
  55. * X coordinate of the first control point.
  56. * @type {number}
  57. */
  58. this.x1 = x1;
  59. /**
  60. * Y coordinate of the first control point.
  61. * @type {number}
  62. */
  63. this.y1 = y1;
  64. /**
  65. * X coordinate of the second control point.
  66. * @type {number}
  67. */
  68. this.x2 = x2;
  69. /**
  70. * Y coordinate of the second control point.
  71. * @type {number}
  72. */
  73. this.y2 = y2;
  74. /**
  75. * X coordinate of the end point.
  76. * @type {number}
  77. */
  78. this.x3 = x3;
  79. /**
  80. * Y coordinate of the end point.
  81. * @type {number}
  82. */
  83. this.y3 = y3;
  84. };
  85. /**
  86. * Constant used to approximate ellipses.
  87. * See: http://canvaspaint.org/blog/2006/12/ellipse/
  88. * @type {number}
  89. */
  90. goog.math.Bezier.KAPPA = 4 * (Math.sqrt(2) - 1) / 3;
  91. /**
  92. * @return {!goog.math.Bezier} A copy of this curve.
  93. */
  94. goog.math.Bezier.prototype.clone = function() {
  95. return new goog.math.Bezier(
  96. this.x0, this.y0, this.x1, this.y1, this.x2, this.y2, this.x3, this.y3);
  97. };
  98. /**
  99. * Test if the given curve is exactly the same as this one.
  100. * @param {goog.math.Bezier} other The other curve.
  101. * @return {boolean} Whether the given curve is the same as this one.
  102. */
  103. goog.math.Bezier.prototype.equals = function(other) {
  104. return this.x0 == other.x0 && this.y0 == other.y0 && this.x1 == other.x1 &&
  105. this.y1 == other.y1 && this.x2 == other.x2 && this.y2 == other.y2 &&
  106. this.x3 == other.x3 && this.y3 == other.y3;
  107. };
  108. /**
  109. * Modifies the curve in place to progress in the opposite direction.
  110. */
  111. goog.math.Bezier.prototype.flip = function() {
  112. var temp = this.x0;
  113. this.x0 = this.x3;
  114. this.x3 = temp;
  115. temp = this.y0;
  116. this.y0 = this.y3;
  117. this.y3 = temp;
  118. temp = this.x1;
  119. this.x1 = this.x2;
  120. this.x2 = temp;
  121. temp = this.y1;
  122. this.y1 = this.y2;
  123. this.y2 = temp;
  124. };
  125. /**
  126. * Computes the curve's X coordinate at a point between 0 and 1.
  127. * @param {number} t The point on the curve to find.
  128. * @return {number} The computed coordinate.
  129. */
  130. goog.math.Bezier.prototype.getPointX = function(t) {
  131. // Special case start and end.
  132. if (t == 0) {
  133. return this.x0;
  134. } else if (t == 1) {
  135. return this.x3;
  136. }
  137. // Step one - from 4 points to 3
  138. var ix0 = goog.math.lerp(this.x0, this.x1, t);
  139. var ix1 = goog.math.lerp(this.x1, this.x2, t);
  140. var ix2 = goog.math.lerp(this.x2, this.x3, t);
  141. // Step two - from 3 points to 2
  142. ix0 = goog.math.lerp(ix0, ix1, t);
  143. ix1 = goog.math.lerp(ix1, ix2, t);
  144. // Final step - last point
  145. return goog.math.lerp(ix0, ix1, t);
  146. };
  147. /**
  148. * Computes the curve's Y coordinate at a point between 0 and 1.
  149. * @param {number} t The point on the curve to find.
  150. * @return {number} The computed coordinate.
  151. */
  152. goog.math.Bezier.prototype.getPointY = function(t) {
  153. // Special case start and end.
  154. if (t == 0) {
  155. return this.y0;
  156. } else if (t == 1) {
  157. return this.y3;
  158. }
  159. // Step one - from 4 points to 3
  160. var iy0 = goog.math.lerp(this.y0, this.y1, t);
  161. var iy1 = goog.math.lerp(this.y1, this.y2, t);
  162. var iy2 = goog.math.lerp(this.y2, this.y3, t);
  163. // Step two - from 3 points to 2
  164. iy0 = goog.math.lerp(iy0, iy1, t);
  165. iy1 = goog.math.lerp(iy1, iy2, t);
  166. // Final step - last point
  167. return goog.math.lerp(iy0, iy1, t);
  168. };
  169. /**
  170. * Computes the curve at a point between 0 and 1.
  171. * @param {number} t The point on the curve to find.
  172. * @return {!goog.math.Coordinate} The computed coordinate.
  173. */
  174. goog.math.Bezier.prototype.getPoint = function(t) {
  175. return new goog.math.Coordinate(this.getPointX(t), this.getPointY(t));
  176. };
  177. /**
  178. * Changes this curve in place to be the portion of itself from [t, 1].
  179. * @param {number} t The start of the desired portion of the curve.
  180. */
  181. goog.math.Bezier.prototype.subdivideLeft = function(t) {
  182. if (t == 1) {
  183. return;
  184. }
  185. // Step one - from 4 points to 3
  186. var ix0 = goog.math.lerp(this.x0, this.x1, t);
  187. var iy0 = goog.math.lerp(this.y0, this.y1, t);
  188. var ix1 = goog.math.lerp(this.x1, this.x2, t);
  189. var iy1 = goog.math.lerp(this.y1, this.y2, t);
  190. var ix2 = goog.math.lerp(this.x2, this.x3, t);
  191. var iy2 = goog.math.lerp(this.y2, this.y3, t);
  192. // Collect our new x1 and y1
  193. this.x1 = ix0;
  194. this.y1 = iy0;
  195. // Step two - from 3 points to 2
  196. ix0 = goog.math.lerp(ix0, ix1, t);
  197. iy0 = goog.math.lerp(iy0, iy1, t);
  198. ix1 = goog.math.lerp(ix1, ix2, t);
  199. iy1 = goog.math.lerp(iy1, iy2, t);
  200. // Collect our new x2 and y2
  201. this.x2 = ix0;
  202. this.y2 = iy0;
  203. // Final step - last point
  204. this.x3 = goog.math.lerp(ix0, ix1, t);
  205. this.y3 = goog.math.lerp(iy0, iy1, t);
  206. };
  207. /**
  208. * Changes this curve in place to be the portion of itself from [0, t].
  209. * @param {number} t The end of the desired portion of the curve.
  210. */
  211. goog.math.Bezier.prototype.subdivideRight = function(t) {
  212. this.flip();
  213. this.subdivideLeft(1 - t);
  214. this.flip();
  215. };
  216. /**
  217. * Changes this curve in place to be the portion of itself from [s, t].
  218. * @param {number} s The start of the desired portion of the curve.
  219. * @param {number} t The end of the desired portion of the curve.
  220. */
  221. goog.math.Bezier.prototype.subdivide = function(s, t) {
  222. this.subdivideRight(s);
  223. this.subdivideLeft((t - s) / (1 - s));
  224. };
  225. /**
  226. * Computes the position t of a point on the curve given its x coordinate.
  227. * That is, for an input xVal, finds t s.t. getPointX(t) = xVal.
  228. * As such, the following should always be true up to some small epsilon:
  229. * t ~ solvePositionFromXValue(getPointX(t)) for t in [0, 1].
  230. * @param {number} xVal The x coordinate of the point to find on the curve.
  231. * @return {number} The position t.
  232. */
  233. goog.math.Bezier.prototype.solvePositionFromXValue = function(xVal) {
  234. // Desired precision on the computation.
  235. var epsilon = 1e-6;
  236. // Initial estimate of t using linear interpolation.
  237. var t = (xVal - this.x0) / (this.x3 - this.x0);
  238. if (t <= 0) {
  239. return 0;
  240. } else if (t >= 1) {
  241. return 1;
  242. }
  243. // Try gradient descent to solve for t. If it works, it is very fast.
  244. var tMin = 0;
  245. var tMax = 1;
  246. var value = 0;
  247. for (var i = 0; i < 8; i++) {
  248. value = this.getPointX(t);
  249. var derivative = (this.getPointX(t + epsilon) - value) / epsilon;
  250. if (Math.abs(value - xVal) < epsilon) {
  251. return t;
  252. } else if (Math.abs(derivative) < epsilon) {
  253. break;
  254. } else {
  255. if (value < xVal) {
  256. tMin = t;
  257. } else {
  258. tMax = t;
  259. }
  260. t -= (value - xVal) / derivative;
  261. }
  262. }
  263. // If the gradient descent got stuck in a local minimum, e.g. because
  264. // the derivative was close to 0, use a Dichotomy refinement instead.
  265. // We limit the number of interations to 8.
  266. for (var i = 0; Math.abs(value - xVal) > epsilon && i < 8; i++) {
  267. if (value < xVal) {
  268. tMin = t;
  269. t = (t + tMax) / 2;
  270. } else {
  271. tMax = t;
  272. t = (t + tMin) / 2;
  273. }
  274. value = this.getPointX(t);
  275. }
  276. return t;
  277. };
  278. /**
  279. * Computes the y coordinate of a point on the curve given its x coordinate.
  280. * @param {number} xVal The x coordinate of the point on the curve.
  281. * @return {number} The y coordinate of the point on the curve.
  282. */
  283. goog.math.Bezier.prototype.solveYValueFromXValue = function(xVal) {
  284. return this.getPointY(this.solvePositionFromXValue(xVal));
  285. };