├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── package.json ├── rollup.config.js ├── src └── index.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | cjs/ 2 | dist/ 3 | modules/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .travis.yml 3 | rollup.config.js 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - ~/.npm 5 | notifications: 6 | email: false 7 | node_js: 8 | - '10' 9 | - '8' 10 | script: 11 | - npm run lint 12 | after_success: 13 | - npm run travis-deploy-once "npm run semantic-release" 14 | branches: 15 | except: 16 | - /^v\d+\.\d+\.\d+$/ 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Internet Systems Consortium license 2 | =================================== 3 | 4 | Copyright (c) `2017`, `Colin Meinke` 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any purpose 7 | with or without fee is hereby granted, provided that the above copyright notice 8 | and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 11 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 12 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 13 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 14 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 15 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 16 | THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SVG arc to cubic bezier 2 | 3 | A function that takes an SVG arc curve as input, and maps it to 4 | one or more cubic bezier curves. 5 | 6 | I extracted the `a2c` function from 7 | [SVG path](https://github.com/fontello/svgpath), as I wanted to use it on its own. 8 | 9 | All credit, thanks and respect goes to: 10 | 11 | - Sergey Batishchev – [@snb2013](https://github.com/snb2013) 12 | - Vitaly Puzrin – [@puzrin](https://github.com/puzrin) 13 | - Alex Kocharin – [@rlidwka](https://github.com/rlidwka) 14 | 15 | It blew my mind. Thank you! 16 | 17 | ## Installation 18 | 19 | ``` 20 | npm install svg-arc-to-cubic-bezier 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```js 26 | import arcToBezier from 'svg-arc-to-cubic-bezier'; 27 | 28 | const previousPoint = { x: 100, y: 100 } 29 | 30 | const currentPoint = { 31 | x: 700, 32 | y: 100, 33 | curve: { 34 | type: 'arc', 35 | rx: 300, 36 | ry: 200, 37 | largeArcFlag: 30, 38 | sweepFlag: 0, 39 | xAxisRotation: 0, 40 | }, 41 | }; 42 | 43 | const curves = arcToBezier({ 44 | px: previousPoint.x, 45 | py: previousPoint.y, 46 | cx: currentPoint.x, 47 | cy: currentPoint.y, 48 | rx: currentPoint.curve.rx, 49 | ry: currentPoint.curve.ry, 50 | xAxisRotation: currentPoint.curve.xAxisRotation, 51 | largeArcFlag: currentPoint.curve.largeArcFlag, 52 | sweepFlag: currentPoint.curve.sweepFlag, 53 | }); 54 | 55 | curves.forEach( c => console.log( c )); 56 | 57 | // [ 58 | // { 59 | // x1: 159.7865795437111, 60 | // y1: 244.97474575043722, 61 | // x2: 342.5677510865157, 62 | // y2: 362.49999701503634, 63 | // x: 508.253174689854, 64 | // y: 362.4999967447917, 65 | // }, 66 | // { 67 | // x1: 673.9385982931924, 68 | // y1: 362.49999647454695, 69 | // x2: 759.7865756485664, 70 | // y2: 244.97474477179443, 71 | // x: 699.9999995964145, 72 | // y: 99.99999902135724, 73 | // }, 74 | // ] 75 | ``` 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Colin Meinke", 4 | "email": "hello@colinmeinke.com", 5 | "url": "https://colinmeinke.com" 6 | }, 7 | "babel": { 8 | "env": { 9 | "cjs": { 10 | "plugins": [ 11 | "transform-object-rest-spread", 12 | "add-module-exports" 13 | ], 14 | "presets": [ 15 | "es2015" 16 | ] 17 | }, 18 | "modules": { 19 | "plugins": [ 20 | "transform-object-rest-spread" 21 | ], 22 | "presets": [ 23 | [ 24 | "es2015", 25 | { 26 | "modules": false 27 | } 28 | ] 29 | ] 30 | }, 31 | "umd": { 32 | "plugins": [ 33 | "transform-object-rest-spread" 34 | ], 35 | "presets": [ 36 | [ 37 | "es2015", 38 | { 39 | "modules": false 40 | } 41 | ] 42 | ] 43 | } 44 | } 45 | }, 46 | "bugs": { 47 | "url": "https://github.com/colinmeinke/svg-arc-to-cubic-bezier/issues" 48 | }, 49 | "config": { 50 | "commitizen": { 51 | "path": "node_modules/cz-conventional-changelog" 52 | } 53 | }, 54 | "description": "A function that takes an SVG arc curve as input, and maps it to one or more cubic bezier curves", 55 | "devDependencies": { 56 | "babel-cli": "^6.26.0", 57 | "babel-core": "^6.26.3", 58 | "babel-plugin-add-module-exports": "^0.3.3", 59 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 60 | "babel-preset-es2015": "^6.24.1", 61 | "commitizen": "^3.1.1", 62 | "cz-conventional-changelog": "^2.1.0", 63 | "rimraf": "^2.6.3", 64 | "rollup": "^1.16.6", 65 | "rollup-plugin-babel": "^3.0.7", 66 | "rollup-plugin-commonjs": "^10.0.1", 67 | "rollup-plugin-uglify": "^6.0.2", 68 | "snazzy": "^8.0.0", 69 | "standard": "^12.0.1", 70 | "travis-deploy-once": "^5.0.11", 71 | "semantic-release": "^15.13.18" 72 | }, 73 | "keywords": [ 74 | "arc", 75 | "bezier", 76 | "convert", 77 | "cubic", 78 | "curve", 79 | "path", 80 | "svg" 81 | ], 82 | "license": "ISC", 83 | "main": "cjs/index.js", 84 | "module": "modules/index.js", 85 | "name": "svg-arc-to-cubic-bezier", 86 | "repository": { 87 | "type": "git", 88 | "url": "https://github.com/colinmeinke/svg-arc-to-cubic-bezier" 89 | }, 90 | "scripts": { 91 | "build": "npm run build:modules && npm run build:cjs && npm run build:umd", 92 | "build:cjs": "BABEL_ENV=cjs babel src --out-dir cjs", 93 | "build:modules": "BABEL_ENV=modules babel src --out-dir modules", 94 | "build:umd": "npm run build:umd:dev && npm run build:umd:pro", 95 | "build:umd:dev": "BABEL_ENV=umd rollup -c", 96 | "build:umd:pro": "NODE_ENV=production BABEL_ENV=umd rollup -c", 97 | "commit": "git-cz", 98 | "fix": "standard --fix", 99 | "lint": "standard --verbose | snazzy", 100 | "prepublish": "npm run tidy && npm run build", 101 | "semantic-release": "semantic-release", 102 | "tidy": "rimraf modules cjs dist", 103 | "travis-deploy-once": "travis-deploy-once" 104 | }, 105 | "version": "0.0.0-development" 106 | } 107 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import commonJs from 'rollup-plugin-commonjs' 3 | import { uglify } from 'rollup-plugin-uglify' 4 | 5 | const config = { 6 | input: 'src/index.js', 7 | plugins: [ 8 | babel({ exclude: 'node_modules/**' }), 9 | commonJs() 10 | ], 11 | output: { 12 | name: 'SVGArcToCubicBezier', 13 | sourcemap: false, 14 | format: 'umd' 15 | } 16 | } 17 | 18 | if (process.env.NODE_ENV === 'production') { 19 | config.output.file = 'dist/svg-points-to-cubic-bezier.min.js' 20 | config.plugins.push(uglify()) 21 | } else { 22 | config.output.file = 'dist/svg-points-to-cubic-bezier.js' 23 | } 24 | 25 | export default config 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const TAU = Math.PI * 2 2 | 3 | const mapToEllipse = ({ x, y }, rx, ry, cosphi, sinphi, centerx, centery) => { 4 | x *= rx 5 | y *= ry 6 | 7 | const xp = cosphi * x - sinphi * y 8 | const yp = sinphi * x + cosphi * y 9 | 10 | return { 11 | x: xp + centerx, 12 | y: yp + centery 13 | } 14 | } 15 | 16 | const approxUnitArc = (ang1, ang2) => { 17 | // If 90 degree circular arc, use a constant 18 | // as derived from http://spencermortensen.com/articles/bezier-circle 19 | const a = ang2 === 1.5707963267948966 20 | ? 0.551915024494 21 | : ang2 === -1.5707963267948966 22 | ? -0.551915024494 23 | : 4 / 3 * Math.tan(ang2 / 4) 24 | 25 | const x1 = Math.cos(ang1) 26 | const y1 = Math.sin(ang1) 27 | const x2 = Math.cos(ang1 + ang2) 28 | const y2 = Math.sin(ang1 + ang2) 29 | 30 | return [ 31 | { 32 | x: x1 - y1 * a, 33 | y: y1 + x1 * a 34 | }, 35 | { 36 | x: x2 + y2 * a, 37 | y: y2 - x2 * a 38 | }, 39 | { 40 | x: x2, 41 | y: y2 42 | } 43 | ] 44 | } 45 | 46 | const vectorAngle = (ux, uy, vx, vy) => { 47 | const sign = (ux * vy - uy * vx < 0) ? -1 : 1 48 | 49 | let dot = ux * vx + uy * vy 50 | 51 | if (dot > 1) { 52 | dot = 1 53 | } 54 | 55 | if (dot < -1) { 56 | dot = -1 57 | } 58 | 59 | return sign * Math.acos(dot) 60 | } 61 | 62 | const getArcCenter = ( 63 | px, 64 | py, 65 | cx, 66 | cy, 67 | rx, 68 | ry, 69 | largeArcFlag, 70 | sweepFlag, 71 | sinphi, 72 | cosphi, 73 | pxp, 74 | pyp 75 | ) => { 76 | const rxsq = Math.pow(rx, 2) 77 | const rysq = Math.pow(ry, 2) 78 | const pxpsq = Math.pow(pxp, 2) 79 | const pypsq = Math.pow(pyp, 2) 80 | 81 | let radicant = (rxsq * rysq) - (rxsq * pypsq) - (rysq * pxpsq) 82 | 83 | if (radicant < 0) { 84 | radicant = 0 85 | } 86 | 87 | radicant /= (rxsq * pypsq) + (rysq * pxpsq) 88 | radicant = Math.sqrt(radicant) * (largeArcFlag === sweepFlag ? -1 : 1) 89 | 90 | const centerxp = radicant * rx / ry * pyp 91 | const centeryp = radicant * -ry / rx * pxp 92 | 93 | const centerx = cosphi * centerxp - sinphi * centeryp + (px + cx) / 2 94 | const centery = sinphi * centerxp + cosphi * centeryp + (py + cy) / 2 95 | 96 | const vx1 = (pxp - centerxp) / rx 97 | const vy1 = (pyp - centeryp) / ry 98 | const vx2 = (-pxp - centerxp) / rx 99 | const vy2 = (-pyp - centeryp) / ry 100 | 101 | let ang1 = vectorAngle(1, 0, vx1, vy1) 102 | let ang2 = vectorAngle(vx1, vy1, vx2, vy2) 103 | 104 | if (sweepFlag === 0 && ang2 > 0) { 105 | ang2 -= TAU 106 | } 107 | 108 | if (sweepFlag === 1 && ang2 < 0) { 109 | ang2 += TAU 110 | } 111 | 112 | return [ centerx, centery, ang1, ang2 ] 113 | } 114 | 115 | const arcToBezier = ({ 116 | px, 117 | py, 118 | cx, 119 | cy, 120 | rx, 121 | ry, 122 | xAxisRotation = 0, 123 | largeArcFlag = 0, 124 | sweepFlag = 0 125 | }) => { 126 | const curves = [] 127 | 128 | if (rx === 0 || ry === 0) { 129 | return [] 130 | } 131 | 132 | const sinphi = Math.sin(xAxisRotation * TAU / 360) 133 | const cosphi = Math.cos(xAxisRotation * TAU / 360) 134 | 135 | const pxp = cosphi * (px - cx) / 2 + sinphi * (py - cy) / 2 136 | const pyp = -sinphi * (px - cx) / 2 + cosphi * (py - cy) / 2 137 | 138 | if (pxp === 0 && pyp === 0) { 139 | return [] 140 | } 141 | 142 | rx = Math.abs(rx) 143 | ry = Math.abs(ry) 144 | 145 | const lambda = 146 | Math.pow(pxp, 2) / Math.pow(rx, 2) + 147 | Math.pow(pyp, 2) / Math.pow(ry, 2) 148 | 149 | if (lambda > 1) { 150 | rx *= Math.sqrt(lambda) 151 | ry *= Math.sqrt(lambda) 152 | } 153 | 154 | let [ centerx, centery, ang1, ang2 ] = getArcCenter( 155 | px, 156 | py, 157 | cx, 158 | cy, 159 | rx, 160 | ry, 161 | largeArcFlag, 162 | sweepFlag, 163 | sinphi, 164 | cosphi, 165 | pxp, 166 | pyp 167 | ) 168 | 169 | // If 'ang2' == 90.0000000001, then `ratio` will evaluate to 170 | // 1.0000000001. This causes `segments` to be greater than one, which is an 171 | // unecessary split, and adds extra points to the bezier curve. To alleviate 172 | // this issue, we round to 1.0 when the ratio is close to 1.0. 173 | let ratio = Math.abs(ang2) / (TAU / 4) 174 | if (Math.abs(1.0 - ratio) < 0.0000001) { 175 | ratio = 1.0 176 | } 177 | 178 | const segments = Math.max(Math.ceil(ratio), 1) 179 | 180 | ang2 /= segments 181 | 182 | for (let i = 0; i < segments; i++) { 183 | curves.push(approxUnitArc(ang1, ang2)) 184 | ang1 += ang2 185 | } 186 | 187 | return curves.map(curve => { 188 | const { x: x1, y: y1 } = mapToEllipse(curve[ 0 ], rx, ry, cosphi, sinphi, centerx, centery) 189 | const { x: x2, y: y2 } = mapToEllipse(curve[ 1 ], rx, ry, cosphi, sinphi, centerx, centery) 190 | const { x, y } = mapToEllipse(curve[ 2 ], rx, ry, cosphi, sinphi, centerx, centery) 191 | 192 | return { x1, y1, x2, y2, x, y } 193 | }) 194 | } 195 | 196 | export default arcToBezier 197 | --------------------------------------------------------------------------------