├── .gitignore ├── LICENSE-MIT ├── README.md ├── bower.json ├── changelog.txt ├── js ├── jsbezier.d.ts └── jsbezier.js ├── package.json └── test └── lineTest.html /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 - 2016 jsPlumb, https://jsplumbtoolkit.com/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsBezier 2 | 3 | This is a set of Bezier curve functions that deal with Beziers, used by jsPlumb, and perhaps useful for other 4 | people. These functions work with Bezier curves of arbitrary degree. 5 | 6 | ## Installation 7 | 8 | ``` 9 | npm install jsbezier 10 | ``` 11 | 12 | ## Notes 13 | 14 | - all input points should be in the format {x:.., y:..}. all output points are in this format too. 15 | - all input curves should be in the format [ {x:.., y:..}, {x:.., y:..}, {x:.., y:..}, {x:.., y:..} ] 16 | - The order of the points is [ start, control point 1, ..., control point N, end ]. `location` as used as an 17 | input here refers to a decimal in the range [0-1], which indicates a point some proportion along the 18 | length of the curve. `location` as output has the same format and meaning. 19 | 20 | ## Function List 21 | 22 | ### Functions are all in the 'jsBezier' namespace. 23 | 24 | - `distanceFromCurve(point, curve)` 25 | 26 | Calculates the distance that the given point lies from the given Bezier. Note that it is computed 27 | relative to the center of the Bezier, so if you have stroked the curve with a wide pen you may wish 28 | to take that into account! The distance returned is relative to the values of the curve and the point - 29 | it will most likely be pixels. 30 | 31 | - `gradientAtPoint(curve, location)` 32 | 33 | Calculates the gradient to the curve at the given location, as a decimal between 0 and 1 inclusive. 34 | 35 | - `gradientAtPointAlongCurveFrom(location, distance)` 36 | 37 | Calculates the gradient to the curve at the point which is 'distance' units from the given location. 38 | See `pointAlongCurveFrom`. 39 | 40 | - `nearestPointOnCurve(point, curve)` 41 | 42 | Calculates the nearest point to the given point on the given curve. The return value of this is a JS object 43 | literal, containing both the point's coordinates and also the location of the point (see above), for example: 44 | 45 | { point:{ x:551,y:150 }, location:0.263365 }. 46 | 47 | - `pointOnCurve(curve, location)` 48 | 49 | Calculates the coordinates of the point on the given Bezier curve at the given location. 50 | 51 | - `pointAlongCurveFrom(curve, location, distance)` 52 | 53 | Calculates the coordinates of the point on the given curve that is `distance` units from `location`. `distance` 54 | should be in the same coordinate space as that used to construct the Bezier curve. For an HTML Canvas usage, 55 | for example, `distance` would be a measure of pixels. The return value is a point in the form: 56 | 57 | { x:..., y:... } 58 | 59 | - `locationAlongCurveFrom(curve, location, distance)` 60 | 61 | Calculates the location of the point on the given curve that is `distance` units from `location`. 62 | `distance` should be in the same coordinate space as that used to construct the Bezier curve. For an 63 | HTML Canvas usage, for example, distance would be a measure of pixels. The return value is a float 64 | in the range [0,1]. 65 | 66 | - `pointAlongCurve(curve, location, distance)` 67 | 68 | Calculates both the coordinates and location of the point on the given curve that is `distance` units 69 | from `location`. This function is the function that both `locationAlongCurveFrom` and `pointAlongCurveFrom` 70 | call. The return value is a JS object containing: 71 | 72 | { point:the new point, location: location in range [0,1], originalPoint:the reference point } 73 | 74 | - `perpendicularToCurveAt(curve, location, length, distance)` 75 | 76 | Calculates the perpendicular to the given curve at the given location. `length` is the length of the line 77 | you wish for (it will be centered on the point at location). `distance` is optional, and allows you to 78 | specify a point along the path from the given `location` as the center of the perpendicular returned. 79 | The return value of this is an array of two points: 80 | 81 | [ {x:...,y:...}, {x:...,y:...} ]. -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsbezier", 3 | "version": "0.9.1", 4 | "homepage": "https://github.com/jsplumb/jsBezier", 5 | "authors": [ 6 | "jsPlumb " 7 | ], 8 | "description": "Bezier curve helper functions for JavaScript. Used by jsPlumb; perhaps useful for others.", 9 | "main": "js/jsbezier.js", 10 | "keywords": [ 11 | "bezier" 12 | ], 13 | "license": "MIT", 14 | "ignore": [ 15 | "**/.*", 16 | "README.md" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | 0.9.4 2 | 3 | December 5th 2018 4 | 5 | - fix for cubic root calculation method 6 | 7 | 0.9.3 8 | 9 | December 3rd 2018 10 | 11 | - add line/box cubic curve intersection methods. 12 | 13 | 0.9.2 14 | 15 | December 3rd 2018 16 | 17 | - a misfire. 18 | 19 | 0.9.1 20 | 21 | January 29 2017 22 | 23 | - register jsBezier on 'exports' if found 24 | 25 | 0.9.0 26 | 27 | January 26 2017 28 | 29 | - publish to npm 30 | - remove version suffix from filename -------------------------------------------------------------------------------- /js/jsbezier.d.ts: -------------------------------------------------------------------------------- 1 | interface Point { 2 | x: number; 3 | y: number; 4 | } 5 | 6 | interface Curve { 7 | [index: number]: Point; 8 | } 9 | 10 | interface JSBezier { 11 | distanceFromCurve(curve: Curve, point: Point): number; 12 | gradientAtPoint(curve: Curve, location: number): number; 13 | gradientAtPointAlongCurveFrom(curve: Curve, location: number, distance: number) : number; 14 | nearestPointOnCurve(point: Point, curve: Curve) : Point; 15 | pointOnCurve(curve: Curve, location: number): Point; 16 | pointAlongCurveFrom(curve: Curve, location: number, distance: number); 17 | perpendicularToCurveAt(curve: Curve, location: number, length: number, distance: number): Curve; 18 | locationAlongCurveFrom(curve: Curve, location, number, distance: number): number; 19 | getLength(curve: Curve): number; 20 | } 21 | 22 | declare var jsBezier: JSBezier; -------------------------------------------------------------------------------- /js/jsbezier.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jsBezier 3 | * 4 | * Copyright (c) 2010 - 2017 jsPlumb (hello@jsplumbtoolkit.com) 5 | * 6 | * licensed under the MIT license. 7 | * 8 | * a set of Bezier curve functions that deal with Beziers, used by jsPlumb, and perhaps useful for other people. These functions work with Bezier 9 | * curves of arbitrary degree. 10 | * 11 | * - functions are all in the 'jsBezier' namespace. 12 | * 13 | * - all input points should be in the format {x:.., y:..}. all output points are in this format too. 14 | * 15 | * - all input curves should be in the format [ {x:.., y:..}, {x:.., y:..}, {x:.., y:..}, {x:.., y:..} ] 16 | * 17 | * - 'location' as used as an input here refers to a decimal in the range 0-1 inclusive, which indicates a point some proportion along the length 18 | * of the curve. location as output has the same format and meaning. 19 | * 20 | * 21 | * Function List: 22 | * -------------- 23 | * 24 | * distanceFromCurve(point, curve) 25 | * 26 | * Calculates the distance that the given point lies from the given Bezier. Note that it is computed relative to the center of the Bezier, 27 | * so if you have stroked the curve with a wide pen you may wish to take that into account! The distance returned is relative to the values 28 | * of the curve and the point - it will most likely be pixels. 29 | * 30 | * gradientAtPoint(curve, location) 31 | * 32 | * Calculates the gradient to the curve at the given location, as a decimal between 0 and 1 inclusive. 33 | * 34 | * gradientAtPointAlongCurveFrom (curve, location) 35 | * 36 | * Calculates the gradient at the point on the given curve that is 'distance' units from location. 37 | * 38 | * nearestPointOnCurve(point, curve) 39 | * 40 | * Calculates the nearest point to the given point on the given curve. The return value of this is a JS object literal, containing both the 41 | *point's coordinates and also the 'location' of the point (see above), for example: { point:{x:551,y:150}, location:0.263365 }. 42 | * 43 | * pointOnCurve(curve, location) 44 | * 45 | * Calculates the coordinates of the point on the given Bezier curve at the given location. 46 | * 47 | * pointAlongCurveFrom(curve, location, distance) 48 | * 49 | * Calculates the coordinates of the point on the given curve that is 'distance' units from location. 'distance' should be in the same coordinate 50 | * space as that used to construct the Bezier curve. For an HTML Canvas usage, for example, distance would be a measure of pixels. 51 | * 52 | * locationAlongCurveFrom(curve, location, distance) 53 | * 54 | * Calculates the location on the given curve that is 'distance' units from location. 'distance' should be in the same coordinate 55 | * space as that used to construct the Bezier curve. For an HTML Canvas usage, for example, distance would be a measure of pixels. 56 | * 57 | * perpendicularToCurveAt(curve, location, length, distance) 58 | * 59 | * Calculates the perpendicular to the given curve at the given location. length is the length of the line you wish for (it will be centered 60 | * on the point at 'location'). distance is optional, and allows you to specify a point along the path from the given location as the center of 61 | * the perpendicular returned. The return value of this is an array of two points: [ {x:...,y:...}, {x:...,y:...} ]. 62 | * 63 | * 64 | */ 65 | 66 | (function() { 67 | 68 | var root = this; 69 | 70 | if(typeof Math.sgn == "undefined") { 71 | Math.sgn = function(x) { return x == 0 ? 0 : x > 0 ? 1 :-1; }; 72 | } 73 | 74 | var Vectors = { 75 | subtract : function(v1, v2) { return {x:v1.x - v2.x, y:v1.y - v2.y }; }, 76 | dotProduct : function(v1, v2) { return (v1.x * v2.x) + (v1.y * v2.y); }, 77 | square : function(v) { return Math.sqrt((v.x * v.x) + (v.y * v.y)); }, 78 | scale : function(v, s) { return {x:v.x * s, y:v.y * s }; } 79 | }, 80 | 81 | maxRecursion = 64, 82 | flatnessTolerance = Math.pow(2.0,-maxRecursion-1); 83 | 84 | /** 85 | * Calculates the distance that the point lies from the curve. 86 | * 87 | * @param point a point in the form {x:567, y:3342} 88 | * @param curve a Bezier curve in the form [{x:..., y:...}, {x:..., y:...}, {x:..., y:...}, {x:..., y:...}]. note that this is currently 89 | * hardcoded to assume cubiz beziers, but would be better off supporting any degree. 90 | * @return a JS object literal containing location and distance, for example: {location:0.35, distance:10}. Location is analogous to the location 91 | * argument you pass to the pointOnPath function: it is a ratio of distance travelled along the curve. Distance is the distance in pixels from 92 | * the point to the curve. 93 | */ 94 | var _distanceFromCurve = function(point, curve) { 95 | var candidates = [], 96 | w = _convertToBezier(point, curve), 97 | degree = curve.length - 1, higherDegree = (2 * degree) - 1, 98 | numSolutions = _findRoots(w, higherDegree, candidates, 0), 99 | v = Vectors.subtract(point, curve[0]), dist = Vectors.square(v), t = 0.0; 100 | 101 | for (var i = 0; i < numSolutions; i++) { 102 | v = Vectors.subtract(point, _bezier(curve, degree, candidates[i], null, null)); 103 | var newDist = Vectors.square(v); 104 | if (newDist < dist) { 105 | dist = newDist; 106 | t = candidates[i]; 107 | } 108 | } 109 | v = Vectors.subtract(point, curve[degree]); 110 | newDist = Vectors.square(v); 111 | if (newDist < dist) { 112 | dist = newDist; 113 | t = 1.0; 114 | } 115 | return {location:t, distance:dist}; 116 | }; 117 | /** 118 | * finds the nearest point on the curve to the given point. 119 | */ 120 | var _nearestPointOnCurve = function(point, curve) { 121 | var td = _distanceFromCurve(point, curve); 122 | return {point:_bezier(curve, curve.length - 1, td.location, null, null), location:td.location}; 123 | }; 124 | var _convertToBezier = function(point, curve) { 125 | var degree = curve.length - 1, higherDegree = (2 * degree) - 1, 126 | c = [], d = [], cdTable = [], w = [], 127 | z = [ [1.0, 0.6, 0.3, 0.1], [0.4, 0.6, 0.6, 0.4], [0.1, 0.3, 0.6, 1.0] ]; 128 | 129 | for (var i = 0; i <= degree; i++) c[i] = Vectors.subtract(curve[i], point); 130 | for (var i = 0; i <= degree - 1; i++) { 131 | d[i] = Vectors.subtract(curve[i+1], curve[i]); 132 | d[i] = Vectors.scale(d[i], 3.0); 133 | } 134 | for (var row = 0; row <= degree - 1; row++) { 135 | for (var column = 0; column <= degree; column++) { 136 | if (!cdTable[row]) cdTable[row] = []; 137 | cdTable[row][column] = Vectors.dotProduct(d[row], c[column]); 138 | } 139 | } 140 | for (i = 0; i <= higherDegree; i++) { 141 | if (!w[i]) w[i] = []; 142 | w[i].y = 0.0; 143 | w[i].x = parseFloat(i) / higherDegree; 144 | } 145 | var n = degree, m = degree-1; 146 | for (var k = 0; k <= n + m; k++) { 147 | var lb = Math.max(0, k - m), 148 | ub = Math.min(k, n); 149 | for (i = lb; i <= ub; i++) { 150 | var j = k - i; 151 | w[i+j].y += cdTable[j][i] * z[j][i]; 152 | } 153 | } 154 | return w; 155 | }; 156 | /** 157 | * counts how many roots there are. 158 | */ 159 | var _findRoots = function(w, degree, t, depth) { 160 | var left = [], right = [], 161 | left_count, right_count, 162 | left_t = [], right_t = []; 163 | 164 | switch (_getCrossingCount(w, degree)) { 165 | case 0 : { 166 | return 0; 167 | } 168 | case 1 : { 169 | if (depth >= maxRecursion) { 170 | t[0] = (w[0].x + w[degree].x) / 2.0; 171 | return 1; 172 | } 173 | if (_isFlatEnough(w, degree)) { 174 | t[0] = _computeXIntercept(w, degree); 175 | return 1; 176 | } 177 | break; 178 | } 179 | } 180 | _bezier(w, degree, 0.5, left, right); 181 | left_count = _findRoots(left, degree, left_t, depth+1); 182 | right_count = _findRoots(right, degree, right_t, depth+1); 183 | for (var i = 0; i < left_count; i++) t[i] = left_t[i]; 184 | for (var i = 0; i < right_count; i++) t[i+left_count] = right_t[i]; 185 | return (left_count+right_count); 186 | }; 187 | var _getCrossingCount = function(curve, degree) { 188 | var n_crossings = 0, sign, old_sign; 189 | sign = old_sign = Math.sgn(curve[0].y); 190 | for (var i = 1; i <= degree; i++) { 191 | sign = Math.sgn(curve[i].y); 192 | if (sign != old_sign) n_crossings++; 193 | old_sign = sign; 194 | } 195 | return n_crossings; 196 | }; 197 | var _isFlatEnough = function(curve, degree) { 198 | var error, 199 | intercept_1, intercept_2, left_intercept, right_intercept, 200 | a, b, c, det, dInv, a1, b1, c1, a2, b2, c2; 201 | a = curve[0].y - curve[degree].y; 202 | b = curve[degree].x - curve[0].x; 203 | c = curve[0].x * curve[degree].y - curve[degree].x * curve[0].y; 204 | 205 | var max_distance_above, max_distance_below; 206 | max_distance_above = max_distance_below = 0.0; 207 | 208 | for (var i = 1; i < degree; i++) { 209 | var value = a * curve[i].x + b * curve[i].y + c; 210 | if (value > max_distance_above) 211 | max_distance_above = value; 212 | else if (value < max_distance_below) 213 | max_distance_below = value; 214 | } 215 | 216 | a1 = 0.0; b1 = 1.0; c1 = 0.0; a2 = a; b2 = b; 217 | c2 = c - max_distance_above; 218 | det = a1 * b2 - a2 * b1; 219 | dInv = 1.0/det; 220 | intercept_1 = (b1 * c2 - b2 * c1) * dInv; 221 | a2 = a; b2 = b; c2 = c - max_distance_below; 222 | det = a1 * b2 - a2 * b1; 223 | dInv = 1.0/det; 224 | intercept_2 = (b1 * c2 - b2 * c1) * dInv; 225 | left_intercept = Math.min(intercept_1, intercept_2); 226 | right_intercept = Math.max(intercept_1, intercept_2); 227 | error = right_intercept - left_intercept; 228 | return (error < flatnessTolerance)? 1 : 0; 229 | }; 230 | var _computeXIntercept = function(curve, degree) { 231 | var XLK = 1.0, YLK = 0.0, 232 | XNM = curve[degree].x - curve[0].x, YNM = curve[degree].y - curve[0].y, 233 | XMK = curve[0].x - 0.0, YMK = curve[0].y - 0.0, 234 | det = XNM*YLK - YNM*XLK, detInv = 1.0/det, 235 | S = (XNM*YMK - YNM*XMK) * detInv; 236 | return 0.0 + XLK * S; 237 | }; 238 | var _bezier = function(curve, degree, t, left, right) { 239 | var temp = [[]]; 240 | for (var j =0; j <= degree; j++) temp[0][j] = curve[j]; 241 | for (var i = 1; i <= degree; i++) { 242 | for (var j =0 ; j <= degree - i; j++) { 243 | if (!temp[i]) temp[i] = []; 244 | if (!temp[i][j]) temp[i][j] = {}; 245 | temp[i][j].x = (1.0 - t) * temp[i-1][j].x + t * temp[i-1][j+1].x; 246 | temp[i][j].y = (1.0 - t) * temp[i-1][j].y + t * temp[i-1][j+1].y; 247 | } 248 | } 249 | if (left != null) 250 | for (j = 0; j <= degree; j++) left[j] = temp[j][0]; 251 | if (right != null) 252 | for (j = 0; j <= degree; j++) right[j] = temp[degree-j][j]; 253 | 254 | return (temp[degree][0]); 255 | }; 256 | 257 | var _curveFunctionCache = {}; 258 | var _getCurveFunctions = function(order) { 259 | var fns = _curveFunctionCache[order]; 260 | if (!fns) { 261 | fns = []; 262 | var f_term = function() { return function(t) { return Math.pow(t, order); }; }, 263 | l_term = function() { return function(t) { return Math.pow((1-t), order); }; }, 264 | c_term = function(c) { return function(t) { return c; }; }, 265 | t_term = function() { return function(t) { return t; }; }, 266 | one_minus_t_term = function() { return function(t) { return 1-t; }; }, 267 | _termFunc = function(terms) { 268 | return function(t) { 269 | var p = 1; 270 | for (var i = 0; i < terms.length; i++) p = p * terms[i](t); 271 | return p; 272 | }; 273 | }; 274 | 275 | fns.push(new f_term()); // first is t to the power of the curve order 276 | for (var i = 1; i < order; i++) { 277 | var terms = [new c_term(order)]; 278 | for (var j = 0 ; j < (order - i); j++) terms.push(new t_term()); 279 | for (var j = 0 ; j < i; j++) terms.push(new one_minus_t_term()); 280 | fns.push(new _termFunc(terms)); 281 | } 282 | fns.push(new l_term()); // last is (1-t) to the power of the curve order 283 | 284 | _curveFunctionCache[order] = fns; 285 | } 286 | 287 | return fns; 288 | }; 289 | 290 | 291 | /** 292 | * calculates a point on the curve, for a Bezier of arbitrary order. 293 | * @param curve an array of control points, eg [{x:10,y:20}, {x:50,y:50}, {x:100,y:100}, {x:120,y:100}]. For a cubic bezier this should have four points. 294 | * @param location a decimal indicating the distance along the curve the point should be located at. this is the distance along the curve as it travels, taking the way it bends into account. should be a number from 0 to 1, inclusive. 295 | */ 296 | var _pointOnPath = function(curve, location) { 297 | var cc = _getCurveFunctions(curve.length - 1), 298 | _x = 0, _y = 0; 299 | for (var i = 0; i < curve.length ; i++) { 300 | _x = _x + (curve[i].x * cc[i](location)); 301 | _y = _y + (curve[i].y * cc[i](location)); 302 | } 303 | 304 | return {x:_x, y:_y}; 305 | }; 306 | 307 | var _dist = function(p1,p2) { 308 | return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); 309 | }; 310 | 311 | var _isPoint = function(curve) { 312 | return curve[0].x === curve[1].x && curve[0].y === curve[1].y; 313 | }; 314 | 315 | /** 316 | * finds the point that is 'distance' along the path from 'location'. this method returns both the x,y location of the point and also 317 | * its 'location' (proportion of travel along the path); the method below - _pointAlongPathFrom - calls this method and just returns the 318 | * point. 319 | */ 320 | var _pointAlongPath = function(curve, location, distance) { 321 | 322 | if (_isPoint(curve)) { 323 | return { 324 | point:curve[0], 325 | location:location 326 | }; 327 | } 328 | 329 | var prev = _pointOnPath(curve, location), 330 | tally = 0, 331 | curLoc = location, 332 | direction = distance > 0 ? 1 : -1, 333 | cur = null; 334 | 335 | while (tally < Math.abs(distance)) { 336 | curLoc += (0.005 * direction); 337 | cur = _pointOnPath(curve, curLoc); 338 | tally += _dist(cur, prev); 339 | prev = cur; 340 | } 341 | return {point:cur, location:curLoc}; 342 | }; 343 | 344 | var _length = function(curve) { 345 | if (_isPoint(curve)) return 0; 346 | 347 | var prev = _pointOnPath(curve, 0), 348 | tally = 0, 349 | curLoc = 0, 350 | direction = 1, 351 | cur = null; 352 | 353 | while (curLoc < 1) { 354 | curLoc += (0.005 * direction); 355 | cur = _pointOnPath(curve, curLoc); 356 | tally += _dist(cur, prev); 357 | prev = cur; 358 | } 359 | return tally; 360 | }; 361 | 362 | /** 363 | * finds the point that is 'distance' along the path from 'location'. 364 | */ 365 | var _pointAlongPathFrom = function(curve, location, distance) { 366 | return _pointAlongPath(curve, location, distance).point; 367 | }; 368 | 369 | /** 370 | * finds the location that is 'distance' along the path from 'location'. 371 | */ 372 | var _locationAlongPathFrom = function(curve, location, distance) { 373 | return _pointAlongPath(curve, location, distance).location; 374 | }; 375 | 376 | /** 377 | * returns the gradient of the curve at the given location, which is a decimal between 0 and 1 inclusive. 378 | * 379 | * thanks // http://bimixual.org/AnimationLibrary/beziertangents.html 380 | */ 381 | var _gradientAtPoint = function(curve, location) { 382 | var p1 = _pointOnPath(curve, location), 383 | p2 = _pointOnPath(curve.slice(0, curve.length - 1), location), 384 | dy = p2.y - p1.y, dx = p2.x - p1.x; 385 | return dy === 0 ? Infinity : Math.atan(dy / dx); 386 | }; 387 | 388 | /** 389 | returns the gradient of the curve at the point which is 'distance' from the given location. 390 | if this point is greater than location 1, the gradient at location 1 is returned. 391 | if this point is less than location 0, the gradient at location 0 is returned. 392 | */ 393 | var _gradientAtPointAlongPathFrom = function(curve, location, distance) { 394 | var p = _pointAlongPath(curve, location, distance); 395 | if (p.location > 1) p.location = 1; 396 | if (p.location < 0) p.location = 0; 397 | return _gradientAtPoint(curve, p.location); 398 | }; 399 | 400 | /** 401 | * calculates a line that is 'length' pixels long, perpendicular to, and centered on, the path at 'distance' pixels from the given location. 402 | * if distance is not supplied, the perpendicular for the given location is computed (ie. we set distance to zero). 403 | */ 404 | var _perpendicularToPathAt = function(curve, location, length, distance) { 405 | distance = distance == null ? 0 : distance; 406 | var p = _pointAlongPath(curve, location, distance), 407 | m = _gradientAtPoint(curve, p.location), 408 | _theta2 = Math.atan(-1 / m), 409 | y = length / 2 * Math.sin(_theta2), 410 | x = length / 2 * Math.cos(_theta2); 411 | return [{x:p.point.x + x, y:p.point.y + y}, {x:p.point.x - x, y:p.point.y - y}]; 412 | }; 413 | 414 | /** 415 | * Calculates all intersections of the given line with the given curve. 416 | * @param x1 417 | * @param y1 418 | * @param x2 419 | * @param y2 420 | * @param curve 421 | * @returns {Array} 422 | */ 423 | var _lineIntersection = function(x1, y1, x2, y2, curve) { 424 | var a = y2 - y1, 425 | b = x1 - x2, 426 | c = (x1 * (y1 - y2)) + (y1 * (x2-x1)), 427 | coeffs = _computeCoefficients(curve), 428 | p = [ 429 | (a*coeffs[0][0]) + (b * coeffs[1][0]), 430 | (a*coeffs[0][1])+(b*coeffs[1][1]), 431 | (a*coeffs[0][2])+(b*coeffs[1][2]), 432 | (a*coeffs[0][3])+(b*coeffs[1][3]) + c 433 | ], 434 | r = _cubicRoots.apply(null, p), 435 | intersections = []; 436 | 437 | if (r != null) { 438 | 439 | for (var i = 0; i < 3; i++) { 440 | var t = r[i], 441 | t2 = Math.pow(t, 2), 442 | t3 = Math.pow(t, 3), 443 | x = [ 444 | (coeffs[0][0] * t3) + (coeffs[0][1] * t2) + (coeffs[0][2] * t) + coeffs[0][3], 445 | (coeffs[1][0] * t3) + (coeffs[1][1] * t2) + (coeffs[1][2] * t) + coeffs[1][3] 446 | ]; 447 | 448 | // check bounds of the line 449 | var s; 450 | if ((x2 - x1) !== 0) { 451 | s = (x[0] - x1) / (x2 - x1); 452 | } 453 | else { 454 | s = (x[1] - y1) / (y2 - y1); 455 | } 456 | 457 | if (t >= 0 && t <= 1.0 && s >= 0 && s <= 1.0) { 458 | intersections.push(x); 459 | } 460 | } 461 | } 462 | 463 | return intersections; 464 | }; 465 | 466 | /** 467 | * Calculates all intersections of the given box with the given curve. 468 | * @param x X position of top left corner of box 469 | * @param y Y position of top left corner of box 470 | * @param w width of box 471 | * @param h height of box 472 | * @param curve 473 | * @returns {Array} 474 | */ 475 | var _boxIntersection = function(x, y, w, h, curve) { 476 | var i = []; 477 | i.push.apply(i, _lineIntersection(x, y, x + w, y, curve)); 478 | i.push.apply(i, _lineIntersection(x + w, y, x + w, y + h, curve)); 479 | i.push.apply(i, _lineIntersection(x + w, y + h, x, y + h, curve)); 480 | i.push.apply(i, _lineIntersection(x, y + h, x, y, curve)); 481 | return i; 482 | }; 483 | 484 | /** 485 | * Calculates all intersections of the given bounding box with the given curve. 486 | * @param boundingBox Bounding box, in { x:.., y:..., w:..., h:... } format. 487 | * @param curve 488 | * @returns {Array} 489 | */ 490 | var _boundingBoxIntersection = function(boundingBox, curve) { 491 | var i = []; 492 | i.push.apply(i, _lineIntersection(boundingBox.x, boundingBox.y, boundingBox.x + boundingBox.w, boundingBox.y, curve)); 493 | i.push.apply(i, _lineIntersection(boundingBox.x + boundingBox.w, boundingBox.y, boundingBox.x + boundingBox.w, boundingBox.y + boundingBox.h, curve)); 494 | i.push.apply(i, _lineIntersection(boundingBox.x + boundingBox.w, boundingBox.y + boundingBox.h, boundingBox.x, boundingBox.y + boundingBox.h, curve)); 495 | i.push.apply(i, _lineIntersection(boundingBox.x, boundingBox.y + boundingBox.h, boundingBox.x, boundingBox.y, curve)); 496 | return i; 497 | }; 498 | 499 | 500 | function _computeCoefficientsForAxis(curve, axis) { 501 | return [ 502 | -(curve[0][axis]) + (3*curve[1][axis]) + (-3 * curve[2][axis]) + curve[3][axis], 503 | (3*(curve[0][axis])) - (6*(curve[1][axis])) + (3*(curve[2][axis])), 504 | -3*curve[0][axis] + 3*curve[1][axis], 505 | curve[0][axis] 506 | ]; 507 | } 508 | 509 | function _computeCoefficients(curve) 510 | { 511 | return [ 512 | _computeCoefficientsForAxis(curve, "x"), 513 | _computeCoefficientsForAxis(curve, "y") 514 | ]; 515 | } 516 | 517 | function sgn(x) { 518 | return x < 0 ? -1 : x > 0 ? 1 : 0; 519 | } 520 | 521 | function _cubicRoots(a, b, c, d) { 522 | var A = b / a, 523 | B = c / a, 524 | C = d / a, 525 | Q = (3*B - Math.pow(A, 2))/9, 526 | R = (9*A*B - 27*C - 2*Math.pow(A, 3))/54, 527 | D = Math.pow(Q, 3) + Math.pow(R, 2), 528 | S, 529 | T, 530 | t = []; 531 | 532 | if (D >= 0) // complex or duplicate roots 533 | { 534 | S = sgn(R + Math.sqrt(D))*Math.pow(Math.abs(R + Math.sqrt(D)),(1/3)); 535 | T = sgn(R - Math.sqrt(D))*Math.pow(Math.abs(R - Math.sqrt(D)),(1/3)); 536 | 537 | t[0] = -A/3 + (S + T); 538 | t[1] = -A/3 - (S + T)/2; 539 | t[2] = -A/3 - (S + T)/2; 540 | 541 | /*discard complex roots*/ 542 | if (Math.abs(Math.sqrt(3)*(S - T)/2) !== 0) { 543 | t[1] = -1; 544 | t[2] = -1; 545 | } 546 | } 547 | else // distinct real roots 548 | { 549 | var th = Math.acos(R/Math.sqrt(-Math.pow(Q, 3))); 550 | t[0] = 2*Math.sqrt(-Q)*Math.cos(th/3) - A/3; 551 | t[1] = 2*Math.sqrt(-Q)*Math.cos((th + 2*Math.PI)/3) - A/3; 552 | t[2] = 2*Math.sqrt(-Q)*Math.cos((th + 4*Math.PI)/3) - A/3; 553 | } 554 | 555 | // discard out of spec roots 556 | for (var i = 0; i < 3; i++) { 557 | if (t[i] < 0 || t[i] > 1.0) { 558 | t[i] = -1; 559 | } 560 | } 561 | 562 | return t; 563 | } 564 | 565 | var jsBezier = this.jsBezier = { 566 | distanceFromCurve : _distanceFromCurve, 567 | gradientAtPoint : _gradientAtPoint, 568 | gradientAtPointAlongCurveFrom : _gradientAtPointAlongPathFrom, 569 | nearestPointOnCurve : _nearestPointOnCurve, 570 | pointOnCurve : _pointOnPath, 571 | pointAlongCurveFrom : _pointAlongPathFrom, 572 | perpendicularToCurveAt : _perpendicularToPathAt, 573 | locationAlongCurveFrom:_locationAlongPathFrom, 574 | getLength:_length, 575 | lineIntersection:_lineIntersection, 576 | boxIntersection:_boxIntersection, 577 | boundingBoxIntersection:_boundingBoxIntersection, 578 | version:"0.9.0" 579 | }; 580 | 581 | if (typeof exports !== "undefined") { 582 | exports.jsBezier = jsBezier; 583 | } 584 | 585 | }).call(typeof window !== 'undefined' ? window : this); 586 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsbezier", 3 | "version": "0.9.5", 4 | "description": "Bezier curve helper functions for JavaScript. Used by jsPlumb; perhaps useful for others.", 5 | "main": "js/jsbezier.js", 6 | "types": "js/jsbezier.d.ts", 7 | "files":[ 8 | "js/jsbezier.js", 9 | "js/jsbezier.d.ts" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/jsplumb/jsBezier.git" 14 | }, 15 | "author": "jsPlumb", 16 | "license": "(MIT OR GPL-2.0)", 17 | "devDependencies": { 18 | "grunt": "~0.4.1", 19 | "grunt-contrib-uglify": "~0.2.2", 20 | "grunt-contrib-yuidoc": "0.5.2", 21 | "grunt-contrib-qunit": "~0.2.2", 22 | "grunt-contrib-jshint": "~0.6.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/lineTest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 45 | 46 | 47 | --------------------------------------------------------------------------------