├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── demo ├── 3d.js ├── camera.js └── index.js ├── function.js ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | test 7 | test.js 8 | demo/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Modified BSD License 2 | ==================================================== 3 | Anti-Grain Geometry - Version 2.4 4 | Copyright (C) 2002-2005 Maxim Shemanarev (McSeem) 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in 15 | the documentation and/or other materials provided with the 16 | distribution. 17 | 18 | 3. The name of the author may not be used to endorse or promote 19 | products derived from this software without specific prior 20 | written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 23 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 25 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, 26 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 29 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 30 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 31 | IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # adaptive-bezier-curve 2 | 3 | [![stable](http://badges.github.io/stability-badges/dist/stable.svg)](http://github.com/badges/stability-badges) 4 | 5 | Builds a bezier curve that is adaptive; that is to say, it has more points along curved corners, and less points along straight lines. This can be used to produce scalable curves that are consistently smooth, while using a small number of steps. Based on [AntiGrain](http://antigrain.com/research/adaptive_bezier/). 6 | 7 | Also see [adaptive-quadratic-curve](https://nodei.co/npm/adaptive-quadratic-curve/). 8 | 9 | ```js 10 | var bezier = require('adaptive-bezier-curve') 11 | 12 | var start = [20, 20], 13 | c1 = [100, 159], 14 | c2 = [50, 200], 15 | end = [200, 20], 16 | scale = 2 17 | 18 | var points = bezier(start, c1, c2, end, scale) 19 | 20 | //returns a list of 2d points: [ [x,y], [x,y], [x,y] ... ] 21 | ``` 22 | 23 | See [demo/index.js](demo/index.js) for an example with HTML5 canvas. 24 | 25 | ![img](http://i.imgur.com/iEQCFY3.png) 26 | 27 | ## Usage 28 | 29 | [![NPM](https://nodei.co/npm/adaptive-bezier-curve.png)](https://nodei.co/npm/adaptive-bezier-curve/) 30 | 31 | #### `bezier(start, c1, c2, end[, scale, points])` 32 | 33 | Returns an adaptive bezier curve for the given four control points. You can specify a `scale` to produce better smoothing for scaled contexts, otherwise it defaults to 1.0. 34 | 35 | If you specify a `points` array, the new points will be pushed onto that array (useful for building paths). If you don't specify `points`, a new array will be used. 36 | 37 | ## License 38 | 39 | The AntiGrain 2.4 code is licensed under BSD-3-Clause, see [LICENSE.md](http://github.com/mattdesl/adaptive-bezier-curve/blob/master/LICENSE.md) for details. 40 | -------------------------------------------------------------------------------- /demo/3d.js: -------------------------------------------------------------------------------- 1 | require('canvas-testbed')(render, start) 2 | 3 | 4 | function render(ctx, width, height) { 5 | 6 | } 7 | 8 | function start(ctx, width, height) { 9 | 10 | } -------------------------------------------------------------------------------- /demo/camera.js: -------------------------------------------------------------------------------- 1 | var mat4 = { 2 | create: require('gl-mat4/create'), 3 | multiply: require('gl-mat4/multiply') 4 | } 5 | 6 | var tmpMat = mat4.create() 7 | 8 | function Projector(opt) { 9 | opt = opt||{} 10 | this.combined = mat4.create() 11 | this.view = mat4.create 12 | this.invCombined = mat4.create() 13 | } 14 | 15 | 16 | Projector.prototype.build = function(projection, position, direction, up) { 17 | //build the view matrix 18 | mat4.add(tmpMat, position, direction) 19 | mat4.lookAt() 20 | tmpVec3.copy(this.position).add(this.direction); 21 | this.view.lookAt(this.position, tmpVec3, this.up); 22 | 23 | //projection * view matrix 24 | this.combined.copy(this.projection).mul(this.view); 25 | 26 | //invert combined matrix, used for unproject 27 | this.invProjectionView.copy(this.combined).invert(); 28 | } 29 | 30 | function project(vec3d, out) { 31 | if (!out) 32 | out = new Vector4(); 33 | 34 | var viewportWidth = this.viewportWidth, 35 | viewportHeight = this.viewportHeight, 36 | n = 1, 37 | f = 0; 38 | 39 | // for useful Z and W values we should do the usual steps... 40 | // clip space -> NDC -> window coords 41 | 42 | //implicit 1.0 for w component 43 | tmpVec4.set(vec.x, vec.y, vec.z, 1.0); 44 | 45 | //transform into clip space 46 | tmpVec4.transformMat4(this.combined); 47 | 48 | //now into NDC 49 | tmpVec4.x = tmpVec4.x / tmpVec4.w; 50 | tmpVec4.y = tmpVec4.y / tmpVec4.w; 51 | tmpVec4.z = tmpVec4.z / tmpVec4.w; 52 | 53 | //and finally into window coordinates 54 | out.x = viewportWidth / 2 * tmpVec4.x + (0 + viewportWidth / 2); 55 | out.y = viewportHeight / 2 * tmpVec4.y + (0 + viewportHeight / 2); 56 | out.z = (f - n) / 2 * tmpVec4.z + (f + n) / 2; 57 | 58 | //if the out vector has a fourth component, we also store (1/clip.w) 59 | //same idea as gl_FragCoord.w 60 | if (out.w === 0 || out.w) 61 | out.w = 1 / tmpVec4.w; 62 | 63 | return out; 64 | }, -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | var adaptive = require('../') 2 | 3 | require('canvas-testbed')(function(ctx, width, height) { 4 | var scale = 2 5 | var path = adaptive([20, 20], [100, 159], [50, 200], [200, 20], scale) 6 | 7 | ctx.save() 8 | ctx.scale(scale, scale) 9 | 10 | ctx.beginPath() 11 | ctx.moveTo(20, 20) 12 | ctx.bezierCurveTo(100,159, 50,200, 200,20) 13 | ctx.lineWidth = 4 14 | ctx.strokeStyle = 'red' 15 | ctx.stroke() 16 | 17 | ctx.strokeStyle = '#000' 18 | path.forEach(function(p) { 19 | var r = 3 20 | ctx.fillRect(p[0]-r/2,p[1]-r/2, r, r) 21 | }) 22 | ctx.restore() 23 | }) 24 | 25 | 26 | /* 27 | //This is how a typical (incremental) bezier curve might look 28 | var bezier = require('bezier') 29 | var tmpX = [0, 0, 0, 0] 30 | var tmpY = [0, 0, 0, 0] 31 | 32 | function createBezier(start, c1, c2, end, points) { 33 | if (!points) 34 | points = [] 35 | 36 | tmpX[0] = start[0] 37 | tmpX[1] = c1[0] 38 | tmpX[2] = c2[0] 39 | tmpX[3] = end[0] 40 | 41 | tmpY[0] = start[1] 42 | tmpY[1] = c1[1] 43 | tmpY[2] = c2[1] 44 | tmpY[3] = end[1] 45 | for (var t = 0; t < 1; t += 0.01) { 46 | points.push([bezier(tmpX, t), bezier(tmpY, t)]) 47 | } 48 | return points 49 | } 50 | */ -------------------------------------------------------------------------------- /function.js: -------------------------------------------------------------------------------- 1 | function clone(point) { //TODO: use gl-vec2 for this 2 | return [point[0], point[1]] 3 | } 4 | 5 | function vec2(x, y) { 6 | return [x, y] 7 | } 8 | 9 | module.exports = function createBezierBuilder(opt) { 10 | opt = opt||{} 11 | 12 | var RECURSION_LIMIT = typeof opt.recursion === 'number' ? opt.recursion : 8 13 | var FLT_EPSILON = typeof opt.epsilon === 'number' ? opt.epsilon : 1.19209290e-7 14 | var PATH_DISTANCE_EPSILON = typeof opt.pathEpsilon === 'number' ? opt.pathEpsilon : 1.0 15 | 16 | var curve_angle_tolerance_epsilon = typeof opt.angleEpsilon === 'number' ? opt.angleEpsilon : 0.01 17 | var m_angle_tolerance = opt.angleTolerance || 0 18 | var m_cusp_limit = opt.cuspLimit || 0 19 | 20 | return function bezierCurve(start, c1, c2, end, scale, points) { 21 | if (!points) 22 | points = [] 23 | 24 | scale = typeof scale === 'number' ? scale : 1.0 25 | var distanceTolerance = PATH_DISTANCE_EPSILON / scale 26 | distanceTolerance *= distanceTolerance 27 | begin(start, c1, c2, end, points, distanceTolerance) 28 | return points 29 | } 30 | 31 | 32 | ////// Based on: 33 | ////// https://github.com/pelson/antigrain/blob/master/agg-2.4/src/agg_curves.cpp 34 | 35 | function begin(start, c1, c2, end, points, distanceTolerance) { 36 | points.push(clone(start)) 37 | var x1 = start[0], 38 | y1 = start[1], 39 | x2 = c1[0], 40 | y2 = c1[1], 41 | x3 = c2[0], 42 | y3 = c2[1], 43 | x4 = end[0], 44 | y4 = end[1] 45 | recursive(x1, y1, x2, y2, x3, y3, x4, y4, points, distanceTolerance, 0) 46 | points.push(clone(end)) 47 | } 48 | 49 | function recursive(x1, y1, x2, y2, x3, y3, x4, y4, points, distanceTolerance, level) { 50 | if(level > RECURSION_LIMIT) 51 | return 52 | 53 | var pi = Math.PI 54 | 55 | // Calculate all the mid-points of the line segments 56 | //---------------------- 57 | var x12 = (x1 + x2) / 2 58 | var y12 = (y1 + y2) / 2 59 | var x23 = (x2 + x3) / 2 60 | var y23 = (y2 + y3) / 2 61 | var x34 = (x3 + x4) / 2 62 | var y34 = (y3 + y4) / 2 63 | var x123 = (x12 + x23) / 2 64 | var y123 = (y12 + y23) / 2 65 | var x234 = (x23 + x34) / 2 66 | var y234 = (y23 + y34) / 2 67 | var x1234 = (x123 + x234) / 2 68 | var y1234 = (y123 + y234) / 2 69 | 70 | if(level > 0) { // Enforce subdivision first time 71 | // Try to approximate the full cubic curve by a single straight line 72 | //------------------ 73 | var dx = x4-x1 74 | var dy = y4-y1 75 | 76 | var d2 = Math.abs((x2 - x4) * dy - (y2 - y4) * dx) 77 | var d3 = Math.abs((x3 - x4) * dy - (y3 - y4) * dx) 78 | 79 | var da1, da2 80 | 81 | if(d2 > FLT_EPSILON && d3 > FLT_EPSILON) { 82 | // Regular care 83 | //----------------- 84 | if((d2 + d3)*(d2 + d3) <= distanceTolerance * (dx*dx + dy*dy)) { 85 | // If the curvature doesn't exceed the distanceTolerance value 86 | // we tend to finish subdivisions. 87 | //---------------------- 88 | if(m_angle_tolerance < curve_angle_tolerance_epsilon) { 89 | points.push(vec2(x1234, y1234)) 90 | return 91 | } 92 | 93 | // Angle & Cusp Condition 94 | //---------------------- 95 | var a23 = Math.atan2(y3 - y2, x3 - x2) 96 | da1 = Math.abs(a23 - Math.atan2(y2 - y1, x2 - x1)) 97 | da2 = Math.abs(Math.atan2(y4 - y3, x4 - x3) - a23) 98 | if(da1 >= pi) da1 = 2*pi - da1 99 | if(da2 >= pi) da2 = 2*pi - da2 100 | 101 | if(da1 + da2 < m_angle_tolerance) { 102 | // Finally we can stop the recursion 103 | //---------------------- 104 | points.push(vec2(x1234, y1234)) 105 | return 106 | } 107 | 108 | if(m_cusp_limit !== 0.0) { 109 | if(da1 > m_cusp_limit) { 110 | points.push(vec2(x2, y2)) 111 | return 112 | } 113 | 114 | if(da2 > m_cusp_limit) { 115 | points.push(vec2(x3, y3)) 116 | return 117 | } 118 | } 119 | } 120 | } 121 | else { 122 | if(d2 > FLT_EPSILON) { 123 | // p1,p3,p4 are collinear, p2 is considerable 124 | //---------------------- 125 | if(d2 * d2 <= distanceTolerance * (dx*dx + dy*dy)) { 126 | if(m_angle_tolerance < curve_angle_tolerance_epsilon) { 127 | points.push(vec2(x1234, y1234)) 128 | return 129 | } 130 | 131 | // Angle Condition 132 | //---------------------- 133 | da1 = Math.abs(Math.atan2(y3 - y2, x3 - x2) - Math.atan2(y2 - y1, x2 - x1)) 134 | if(da1 >= pi) da1 = 2*pi - da1 135 | 136 | if(da1 < m_angle_tolerance) { 137 | points.push(vec2(x2, y2)) 138 | points.push(vec2(x3, y3)) 139 | return 140 | } 141 | 142 | if(m_cusp_limit !== 0.0) { 143 | if(da1 > m_cusp_limit) { 144 | points.push(vec2(x2, y2)) 145 | return 146 | } 147 | } 148 | } 149 | } 150 | else if(d3 > FLT_EPSILON) { 151 | // p1,p2,p4 are collinear, p3 is considerable 152 | //---------------------- 153 | if(d3 * d3 <= distanceTolerance * (dx*dx + dy*dy)) { 154 | if(m_angle_tolerance < curve_angle_tolerance_epsilon) { 155 | points.push(vec2(x1234, y1234)) 156 | return 157 | } 158 | 159 | // Angle Condition 160 | //---------------------- 161 | da1 = Math.abs(Math.atan2(y4 - y3, x4 - x3) - Math.atan2(y3 - y2, x3 - x2)) 162 | if(da1 >= pi) da1 = 2*pi - da1 163 | 164 | if(da1 < m_angle_tolerance) { 165 | points.push(vec2(x2, y2)) 166 | points.push(vec2(x3, y3)) 167 | return 168 | } 169 | 170 | if(m_cusp_limit !== 0.0) { 171 | if(da1 > m_cusp_limit) 172 | { 173 | points.push(vec2(x3, y3)) 174 | return 175 | } 176 | } 177 | } 178 | } 179 | else { 180 | // Collinear case 181 | //----------------- 182 | dx = x1234 - (x1 + x4) / 2 183 | dy = y1234 - (y1 + y4) / 2 184 | if(dx*dx + dy*dy <= distanceTolerance) { 185 | points.push(vec2(x1234, y1234)) 186 | return 187 | } 188 | } 189 | } 190 | } 191 | 192 | // Continue subdivision 193 | //---------------------- 194 | recursive(x1, y1, x12, y12, x123, y123, x1234, y1234, points, distanceTolerance, level + 1) 195 | recursive(x1234, y1234, x234, y234, x34, y34, x4, y4, points, distanceTolerance, level + 1) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./function')() -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adaptive-bezier-curve", 3 | "version": "1.0.3", 4 | "description": "adaptive and scalable 2D bezier curves", 5 | "main": "index.js", 6 | "license": "BSD-3-Clause", 7 | "author": { 8 | "name": "Matt DesLauriers", 9 | "email": "dave.des@gmail.com", 10 | "url": "https://github.com/mattdesl" 11 | }, 12 | "dependencies": {}, 13 | "devDependencies": { 14 | "canvas-testbed": "^1.0.2", 15 | "tape": "^3.0.1" 16 | }, 17 | "scripts": { 18 | "test": "node test.js" 19 | }, 20 | "keywords": [ 21 | "bezier", 22 | "quadratic", 23 | "curve", 24 | "adaptive", 25 | "approximate", 26 | "quadraticCurveTo", 27 | "bezierCurveTo", 28 | "line", 29 | "path", 30 | "rendering", 31 | "canvas", 32 | "draw", 33 | "drawing", 34 | "webgl", 35 | "segments" 36 | ], 37 | "repository": { 38 | "type": "git", 39 | "url": "git://github.com/mattdesl/adaptive-bezier-curve.git" 40 | }, 41 | "homepage": "https://github.com/mattdesl/adaptive-bezier-curve", 42 | "bugs": { 43 | "url": "https://github.com/mattdesl/adaptive-bezier-curve/issues" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var bezierCurveTo = require('./') 2 | var test = require('tape').test 3 | 4 | test('generates points for a bezier curve in 2D', function(t) { 5 | 6 | t.end() 7 | }) --------------------------------------------------------------------------------