├── .babelrc ├── .bithoundrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── dist ├── svg-path-properties.cjs.js ├── svg-path-properties.esm.js ├── svg-path-properties.min.js └── types │ ├── arc.d.ts │ ├── bezier-functions.d.ts │ ├── bezier-values.d.ts │ ├── bezier.d.ts │ ├── index.d.ts │ ├── linear.d.ts │ ├── parse.d.ts │ ├── svg-path-properties.d.ts │ └── types.d.ts ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── arc.ts ├── bezier-functions.ts ├── bezier-values.ts ├── bezier.ts ├── index.ts ├── linear.ts ├── parse.ts ├── svg-path-properties.ts └── types.ts ├── test ├── bezier-test.ts ├── get-parts-test.ts ├── inDelta.ts ├── index-test.ts ├── length-test.ts ├── parse-test.ts ├── point-at-test.ts ├── tangent-test.ts └── test-getLength.html └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env", "@babel/typescript"], 3 | "plugins": [ 4 | "@babel/proposal-class-properties", 5 | "@babel/proposal-object-rest-spread", 6 | "module:babel6-plugin-strip-class-callcheck" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.bithoundrc: -------------------------------------------------------------------------------- 1 | "ignore": [ 2 | "**/deps/**", 3 | "**/node_modules/**", 4 | "**/thirdparty/**", 5 | "**/third_party/**", 6 | "**/build/**" 7 | ] 8 | 9 | "critics": { 10 | "lint": {"engine": "eslint"} 11 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": 3 | {"sourceType": "module"}, 4 | 5 | "env": { 6 | "es6": true, 7 | "browser": true 8 | }, 9 | 10 | "extends": "eslint:recommended", 11 | 12 | "rules": { 13 | "no-cond-assign": 0, 14 | "no-constant-condition": 0 15 | } 16 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - v10.2.1 5 | env: 6 | - CXX=g++-4.8 7 | before_script: 8 | - npm install -g istanbul 9 | - npm install coveralls 10 | after_success: 11 | - istanbul cover --report lcovonly ./node_modules/tape/bin/tape './test/*-test.js' && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 12 | addons: 13 | apt: 14 | sources: 15 | - ubuntu-toolchain-r-test 16 | packages: 17 | - g++-4.8 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "[javascript]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at rveciana@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Roger Veciana i Rovira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/rveciana/svg-path-properties.svg?branch=master)](https://travis-ci.org/rveciana/svg-path-properties) 2 | [![Coverage Status](https://coveralls.io/repos/github/rveciana/svg-path-properties/badge.svg?branch=master)](https://coveralls.io/github/rveciana/svg-path-properties?branch=master) 3 | 4 | # svg-path-properties 5 | 6 | Pure Javascript alternative to getPointAtLength(t) and getTotalLength() functions. Works with Canvas objects and when Node 7 | 8 | JavaScript can access to path elements properties in a browser, such as its length and the point at a given length. Unfortunately, this can't be achieved using a Canvas element or when working with node. This library can be used to replace this need. It has no dependencies on other JavaScript libraries. 9 | 10 | # INSTALL 11 | 12 | To use with npm, just type 13 | 14 | npm install svg-path-properties 15 | 16 | You can use it int he browser directly by including _svg-path-properties.min.js_ from the dist directory 17 | 18 | 19 | 20 | # USAGE 21 | 22 | The available methods are: 23 | 24 | const path = require("svg-path-properties"); 25 | const properties = new path.svgPathProperties("M0,100 Q50,-50 100,100 T200,100"); 26 | const length = properties.getTotalLength(); 27 | const point = properties.getPointAtLength(200); 28 | const tangent = properties.getTangentAtLength(200); 29 | const allProperties = properties.getPropertiesAtLength(200); 30 | const parts = properties.getParts(); 31 | 32 | ## Node: 33 | 34 | const path = require("svg-path-properties"); 35 | const properties = new path.svgPathProperties("M0,100 Q50,-50 100,100 T200,100"); 36 | 37 | ## Including it from an import: 38 | 39 | import { svgPathProperties } from "svg-path-properties"; 40 | const properties = new svgPathProperties("M0,100 Q50,-50 100,100 T200,100"); 41 | 42 | ## Including the script in the browser 43 | 44 | Once the _script_ tag has been included, 45 | 46 | const properties = new svgPathProperties.svgPathProperties("M0,100 Q50,-50 100,100 T200,100"); 47 | 48 | ## Using _new_ 49 | 50 | Since _svgPathProperties_ is a class, using _new_ is the correct way to initilize it. For backwards compatibility reasons, the object can be get without it: 51 | 52 | const properties = svgPathProperties("M0,100 Q50,-50 100,100 T200,100"); 53 | 54 | # Some usage examples 55 | 56 | - [Drawing an animated path](http://bl.ocks.org/rveciana/209fa7efeb01f05fa4a544a76ac8ed91) 57 | - [Label positioning](http://bl.ocks.org/rveciana/bef48021e38a77a520109d2088bff9eb) 58 | - [Drawing stramlines arrows](http://bl.ocks.org/rveciana/edb1dd43f3edc5d16ecaf4839c032dec) 59 | - [Using it to get a length when in node instead of the browser](https://github.com/veltman/flubber/blob/master/src/svg.js), as in the [Flubber library](https://github.com/veltman/flubber) 60 | - [SVG animations in React Native](https://bitbucket.org/ingenuityph/react-native-svg-animations/src/master/) 61 | 62 | # Typescript 63 | 64 | The TypeScript declaration file is available too, since version 0.5.0 From version 1.0.0, the whole library has been rewritten using TypeScript, and the types are auto-generated. 65 | 66 | # CREDITS 67 | 68 | Some parts of the code are taken from other libraries or questions at StackOverflow: 69 | 70 | For Bézier curves: 71 | 72 | - http://bl.ocks.org/hnakamur/e7efd0602bfc15f66fc5, https://gist.github.com/tunght13488/6744e77c242cc7a94859 73 | - http://stackoverflow.com/questions/11854907/calculate-the-length-of-a-segment-of-a-quadratic-bezier 74 | - http://pomax.github.io/bezierinfo 75 | - Arc elements calculation: https://github.com/MadLittleMods/svg-curve-lib/tree/f07d6008a673816f4cb74a3269164b430c3a95cb 76 | 77 | For path parsing: 78 | 79 | - https://github.com/jkroso/parse-svg-path 80 | -------------------------------------------------------------------------------- /dist/svg-path-properties.cjs.js: -------------------------------------------------------------------------------- 1 | // http://geoexamples.com/path-properties/ v1.2.0 Copyright 2023 Roger Veciana i Rovira 2 | "use strict";function t(t,n){for(var e=0;et.length)&&(n=t.length);for(var e=0,i=new Array(n);eu.length&&(t=u.length);var n=c({x:u.x0,y:u.y0},u.rx,u.ry,u.xAxisRotate,u.LargeArcFlag,u.SweepFlag,{x:u.x1,y:u.y1},t/u.length);return{x:n.x,y:n.y}})),e(this,"getTangentAtLength",(function(t){t<0?t=0:t>u.length&&(t=u.length);var n,e=.05,i=u.getPointAtLength(t);t<0?t=0:t>u.length&&(t=u.length);var r=(n=t1&&(n=Math.sqrt(c)*n,e=Math.sqrt(c)*e);var f=(Math.pow(n,2)*Math.pow(e,2)-Math.pow(n,2)*Math.pow(l.y,2)-Math.pow(e,2)*Math.pow(l.x,2))/(Math.pow(n,2)*Math.pow(l.y,2)+Math.pow(e,2)*Math.pow(l.x,2));f=f<0?0:f;var x=(r!==h?1:-1)*Math.sqrt(f),v=x*(n*l.y/e),w=x*(-e*l.x/n),L={x:Math.cos(o)*v-Math.sin(o)*w+(t.x+s.x)/2,y:Math.sin(o)*v+Math.cos(o)*w+(t.y+s.y)/2},A={x:(l.x-v)/n,y:(l.y-w)/e},d=M({x:1,y:0},A),b=M(A,{x:(-l.x-v)/n,y:(-l.y-w)/e});!h&&b>0?b-=2*Math.PI:h&&b<0&&(b+=2*Math.PI);var P=d+(b%=2*Math.PI)*a,m=n*Math.cos(P),T=e*Math.sin(P);return{x:Math.cos(o)*m-Math.sin(o)*T+L.x,y:Math.sin(o)*m+Math.cos(o)*T+L.y,ellipticalArcStartAngle:d,ellipticalArcEndAngle:d+b,ellipticalArcAngle:P,ellipticalArcCenter:L,resultantRx:n,resultantRy:e}},f=function(t,n){t=t||500;for(var e,i=0,r=[],h=[],s=n(0),a=0;a0?Math.sqrt(l*l+c):0,y=u*u+c>0?Math.sqrt(u*u+c):0,p=u+Math.sqrt(u*u+c)!==0&&(l+f)/(u+y)!=0?c*Math.log(Math.abs((l+f)/(u+y))):0;return Math.sqrt(a)/2*(l*f-u*y+p)},q=function(t,n,e){return{x:2*(1-e)*(t[1]-t[0])+2*e*(t[2]-t[1]),y:2*(1-e)*(n[1]-n[0])+2*e*(n[2]-n[1])}};function _(t,n,e){var i=S(1,e,t),r=S(1,e,n),h=i*i+r*r;return Math.sqrt(h)}var S=function t(n,e,i){var r,h,s=i.length-1;if(0===s)return 0;if(0===n){h=0;for(var a=0;a<=s;a++)h+=A[s][a]*Math.pow(1-e,s-a)*Math.pow(e,a)*i[a];return h}r=new Array(s);for(var o=0;o.001;){var a=e(r+h),o=Math.abs(t-a)/n;if(o500)break}return r},C=n((function(t,n,i,r,h,s,a,o){var g=this;e(this,"a",void 0),e(this,"b",void 0),e(this,"c",void 0),e(this,"d",void 0),e(this,"length",void 0),e(this,"getArcLength",void 0),e(this,"getPoint",void 0),e(this,"getDerivative",void 0),e(this,"getTotalLength",(function(){return g.length})),e(this,"getPointAtLength",(function(t){var n=[g.a.x,g.b.x,g.c.x,g.d.x],e=[g.a.y,g.b.y,g.c.y,g.d.y],i=N(t,g.length,(function(t){return g.getArcLength(n,e,t)}));return g.getPoint(n,e,i)})),e(this,"getTangentAtLength",(function(t){var n=[g.a.x,g.b.x,g.c.x,g.d.x],e=[g.a.y,g.b.y,g.c.y,g.d.y],i=N(t,g.length,(function(t){return g.getArcLength(n,e,t)})),r=g.getDerivative(n,e,i),h=Math.sqrt(r.x*r.x+r.y*r.y);return h>0?{x:r.x/h,y:r.y/h}:{x:0,y:0}})),e(this,"getPropertiesAtLength",(function(t){var n,e=[g.a.x,g.b.x,g.c.x,g.d.x],i=[g.a.y,g.b.y,g.c.y,g.d.y],r=N(t,g.length,(function(t){return g.getArcLength(e,i,t)})),h=g.getDerivative(e,i,r),s=Math.sqrt(h.x*h.x+h.y*h.y);n=s>0?{x:h.x/s,y:h.y/s}:{x:0,y:0};var a=g.getPoint(e,i,r);return{x:a.x,y:a.y,tangentX:n.x,tangentY:n.y}})),e(this,"getC",(function(){return g.c})),e(this,"getD",(function(){return g.d})),this.a={x:t,y:n},this.b={x:i,y:r},this.c={x:h,y:s},void 0!==a&&void 0!==o?(this.getArcLength=P,this.getPoint=d,this.getDerivative=b,this.d={x:a,y:o}):(this.getArcLength=T,this.getPoint=m,this.getDerivative=q,this.d={x:0,y:0}),this.length=this.getArcLength([this.a.x,this.b.x,this.c.x,this.d.x],[this.a.y,this.b.y,this.c.y,this.d.y],1)})),O=n((function(t){var n=this;e(this,"length",0),e(this,"partial_lengths",[]),e(this,"functions",[]),e(this,"initial_point",null),e(this,"getPartAtLength",(function(t){t<0?t=0:t>n.length&&(t=n.length);for(var e=n.partial_lengths.length-1;n.partial_lengths[e]>=t&&e>0;)e--;return e++,{fraction:t-n.partial_lengths[e-1],i:e}})),e(this,"getTotalLength",(function(){return n.length})),e(this,"getPointAtLength",(function(t){var e=n.getPartAtLength(t),i=n.functions[e.i];if(i)return i.getPointAtLength(e.fraction);if(n.initial_point)return n.initial_point;throw new Error("Wrong function at this part.")})),e(this,"getTangentAtLength",(function(t){var e=n.getPartAtLength(t),i=n.functions[e.i];if(i)return i.getTangentAtLength(e.fraction);if(n.initial_point)return{x:0,y:0};throw new Error("Wrong function at this part.")})),e(this,"getPropertiesAtLength",(function(t){var e=n.getPartAtLength(t),i=n.functions[e.i];if(i)return i.getPropertiesAtLength(e.fraction);if(n.initial_point)return{x:n.initial_point.x,y:n.initial_point.y,tangentX:0,tangentY:0};throw new Error("Wrong function at this part.")})),e(this,"getParts",(function(){for(var t=[],e=0;e0?t:"M0,0").match(a);if(!n)throw new Error("No path elements found in string ".concat(t));return n.reduce((function(t,n){var e=n.charAt(0),r=e.toLowerCase(),h=g(n.substring(1));if("m"===r&&h.length>2&&(t.push([e].concat(i(h.splice(0,2)))),r="l",e="m"===e?"l":"L"),"a"===r.toLowerCase()&&(5===h.length||6===h.length)){var a=n.substring(1).trim().split(" ");h=[Number(a[0]),Number(a[1]),Number(a[2]),Number(a[3].charAt(0)),Number(a[3].charAt(1)),Number(a[3].substring(2)),Number(a[4])]}for(;h.length>=0;){if(h.length===s[r]){t.push([e].concat(i(h.splice(0,s[r]))));break}if(h.length0?(this.length+=r.getTotalLength(),this.functions.push(r),o=[h[y][5]+o[0],h[y][6]+o[1]]):this.functions.push(new u(o[0],o[0],o[1],o[1]));else if("S"===h[y][0]){if(y>0&&["C","c","S","s"].indexOf(h[y-1][0])>-1){if(r){var p=r.getC();r=new C(o[0],o[1],2*o[0]-p.x,2*o[1]-p.y,h[y][1],h[y][2],h[y][3],h[y][4])}}else r=new C(o[0],o[1],o[0],o[1],h[y][1],h[y][2],h[y][3],h[y][4]);r&&(this.length+=r.getTotalLength(),o=[h[y][3],h[y][4]],this.functions.push(r))}else if("s"===h[y][0]){if(y>0&&["C","c","S","s"].indexOf(h[y-1][0])>-1){if(r){var x=r.getC(),v=r.getD();r=new C(o[0],o[1],o[0]+v.x-x.x,o[1]+v.y-x.y,o[0]+h[y][1],o[1]+h[y][2],o[0]+h[y][3],o[1]+h[y][4])}}else r=new C(o[0],o[1],o[0],o[1],o[0]+h[y][1],o[1]+h[y][2],o[0]+h[y][3],o[1]+h[y][4]);r&&(this.length+=r.getTotalLength(),o=[h[y][3]+o[0],h[y][4]+o[1]],this.functions.push(r))}else if("Q"===h[y][0]){if(o[0]==h[y][1]&&o[1]==h[y][2]){var M=new u(h[y][1],h[y][3],h[y][2],h[y][4]);this.length+=M.getTotalLength(),this.functions.push(M)}else r=new C(o[0],o[1],h[y][1],h[y][2],h[y][3],h[y][4],void 0,void 0),this.length+=r.getTotalLength(),this.functions.push(r);o=[h[y][3],h[y][4]],c=[h[y][1],h[y][2]]}else if("q"===h[y][0]){if(0!=h[y][1]||0!=h[y][2])r=new C(o[0],o[1],o[0]+h[y][1],o[1]+h[y][2],o[0]+h[y][3],o[1]+h[y][4],void 0,void 0),this.length+=r.getTotalLength(),this.functions.push(r);else{var w=new u(o[0]+h[y][1],o[0]+h[y][3],o[1]+h[y][2],o[1]+h[y][4]);this.length+=w.getTotalLength(),this.functions.push(w)}c=[o[0]+h[y][1],o[1]+h[y][2]],o=[h[y][3]+o[0],h[y][4]+o[1]]}else if("T"===h[y][0]){if(y>0&&["Q","q","T","t"].indexOf(h[y-1][0])>-1)r=new C(o[0],o[1],2*o[0]-c[0],2*o[1]-c[1],h[y][1],h[y][2],void 0,void 0),this.functions.push(r),this.length+=r.getTotalLength();else{var L=new u(o[0],h[y][1],o[1],h[y][2]);this.functions.push(L),this.length+=L.getTotalLength()}c=[2*o[0]-c[0],2*o[1]-c[1]],o=[h[y][1],h[y][2]]}else if("t"===h[y][0]){if(y>0&&["Q","q","T","t"].indexOf(h[y-1][0])>-1)r=new C(o[0],o[1],2*o[0]-c[0],2*o[1]-c[1],o[0]+h[y][1],o[1]+h[y][2],void 0,void 0),this.length+=r.getTotalLength(),this.functions.push(r);else{var A=new u(o[0],o[0]+h[y][1],o[1],o[1]+h[y][2]);this.length+=A.getTotalLength(),this.functions.push(A)}c=[2*o[0]-c[0],2*o[1]-c[1]],o=[h[y][1]+o[0],h[y][2]+o[1]]}else if("A"===h[y][0]){var d=new l(o[0],o[1],h[y][1],h[y][2],h[y][3],1===h[y][4],1===h[y][5],h[y][6],h[y][7]);this.length+=d.getTotalLength(),o=[h[y][6],h[y][7]],this.functions.push(d)}else if("a"===h[y][0]){var b=new l(o[0],o[1],h[y][1],h[y][2],h[y][3],1===h[y][4],1===h[y][5],o[0]+h[y][6],o[1]+h[y][7]);this.length+=b.getTotalLength(),o=[o[0]+h[y][6],o[1]+h[y][7]],this.functions.push(b)}this.partial_lengths.push(this.length)}})),j=n((function(t){var n=this;if(e(this,"inst",void 0),e(this,"getTotalLength",(function(){return n.inst.getTotalLength()})),e(this,"getPointAtLength",(function(t){return n.inst.getPointAtLength(t)})),e(this,"getTangentAtLength",(function(t){return n.inst.getTangentAtLength(t)})),e(this,"getPropertiesAtLength",(function(t){return n.inst.getPropertiesAtLength(t)})),e(this,"getParts",(function(){return n.inst.getParts()})),this.inst=new O(t),!(this instanceof j))return new j(t)}));exports.svgPathProperties=j; 3 | -------------------------------------------------------------------------------- /dist/svg-path-properties.esm.js: -------------------------------------------------------------------------------- 1 | // http://geoexamples.com/path-properties/ v1.2.0 Copyright 2023 Roger Veciana i Rovira 2 | function t(t,n){for(var e=0;et.length)&&(n=t.length);for(var e=0,i=new Array(n);eu.length&&(t=u.length);var n=c({x:u.x0,y:u.y0},u.rx,u.ry,u.xAxisRotate,u.LargeArcFlag,u.SweepFlag,{x:u.x1,y:u.y1},t/u.length);return{x:n.x,y:n.y}})),e(this,"getTangentAtLength",(function(t){t<0?t=0:t>u.length&&(t=u.length);var n,e=.05,i=u.getPointAtLength(t);t<0?t=0:t>u.length&&(t=u.length);var r=(n=t1&&(n=Math.sqrt(c)*n,e=Math.sqrt(c)*e);var f=(Math.pow(n,2)*Math.pow(e,2)-Math.pow(n,2)*Math.pow(l.y,2)-Math.pow(e,2)*Math.pow(l.x,2))/(Math.pow(n,2)*Math.pow(l.y,2)+Math.pow(e,2)*Math.pow(l.x,2));f=f<0?0:f;var x=(r!==h?1:-1)*Math.sqrt(f),v=x*(n*l.y/e),w=x*(-e*l.x/n),L={x:Math.cos(o)*v-Math.sin(o)*w+(t.x+a.x)/2,y:Math.sin(o)*v+Math.cos(o)*w+(t.y+a.y)/2},A={x:(l.x-v)/n,y:(l.y-w)/e},d=M({x:1,y:0},A),b=M(A,{x:(-l.x-v)/n,y:(-l.y-w)/e});!h&&b>0?b-=2*Math.PI:h&&b<0&&(b+=2*Math.PI);var m=d+(b%=2*Math.PI)*s,P=n*Math.cos(m),T=e*Math.sin(m);return{x:Math.cos(o)*P-Math.sin(o)*T+L.x,y:Math.sin(o)*P+Math.cos(o)*T+L.y,ellipticalArcStartAngle:d,ellipticalArcEndAngle:d+b,ellipticalArcAngle:m,ellipticalArcCenter:L,resultantRx:n,resultantRy:e}},f=function(t,n){t=t||500;for(var e,i=0,r=[],h=[],a=n(0),s=0;s0?Math.sqrt(l*l+c):0,y=u*u+c>0?Math.sqrt(u*u+c):0,p=u+Math.sqrt(u*u+c)!==0&&(l+f)/(u+y)!=0?c*Math.log(Math.abs((l+f)/(u+y))):0;return Math.sqrt(s)/2*(l*f-u*y+p)},q=function(t,n,e){return{x:2*(1-e)*(t[1]-t[0])+2*e*(t[2]-t[1]),y:2*(1-e)*(n[1]-n[0])+2*e*(n[2]-n[1])}};function _(t,n,e){var i=S(1,e,t),r=S(1,e,n),h=i*i+r*r;return Math.sqrt(h)}var S=function t(n,e,i){var r,h,a=i.length-1;if(0===a)return 0;if(0===n){h=0;for(var s=0;s<=a;s++)h+=A[a][s]*Math.pow(1-e,a-s)*Math.pow(e,s)*i[s];return h}r=new Array(a);for(var o=0;o.001;){var s=e(r+h),o=Math.abs(t-s)/n;if(o500)break}return r},C=n((function(t,n,i,r,h,a,s,o){var g=this;e(this,"a",void 0),e(this,"b",void 0),e(this,"c",void 0),e(this,"d",void 0),e(this,"length",void 0),e(this,"getArcLength",void 0),e(this,"getPoint",void 0),e(this,"getDerivative",void 0),e(this,"getTotalLength",(function(){return g.length})),e(this,"getPointAtLength",(function(t){var n=[g.a.x,g.b.x,g.c.x,g.d.x],e=[g.a.y,g.b.y,g.c.y,g.d.y],i=N(t,g.length,(function(t){return g.getArcLength(n,e,t)}));return g.getPoint(n,e,i)})),e(this,"getTangentAtLength",(function(t){var n=[g.a.x,g.b.x,g.c.x,g.d.x],e=[g.a.y,g.b.y,g.c.y,g.d.y],i=N(t,g.length,(function(t){return g.getArcLength(n,e,t)})),r=g.getDerivative(n,e,i),h=Math.sqrt(r.x*r.x+r.y*r.y);return h>0?{x:r.x/h,y:r.y/h}:{x:0,y:0}})),e(this,"getPropertiesAtLength",(function(t){var n,e=[g.a.x,g.b.x,g.c.x,g.d.x],i=[g.a.y,g.b.y,g.c.y,g.d.y],r=N(t,g.length,(function(t){return g.getArcLength(e,i,t)})),h=g.getDerivative(e,i,r),a=Math.sqrt(h.x*h.x+h.y*h.y);n=a>0?{x:h.x/a,y:h.y/a}:{x:0,y:0};var s=g.getPoint(e,i,r);return{x:s.x,y:s.y,tangentX:n.x,tangentY:n.y}})),e(this,"getC",(function(){return g.c})),e(this,"getD",(function(){return g.d})),this.a={x:t,y:n},this.b={x:i,y:r},this.c={x:h,y:a},void 0!==s&&void 0!==o?(this.getArcLength=m,this.getPoint=d,this.getDerivative=b,this.d={x:s,y:o}):(this.getArcLength=T,this.getPoint=P,this.getDerivative=q,this.d={x:0,y:0}),this.length=this.getArcLength([this.a.x,this.b.x,this.c.x,this.d.x],[this.a.y,this.b.y,this.c.y,this.d.y],1)})),O=n((function(t){var n=this;e(this,"length",0),e(this,"partial_lengths",[]),e(this,"functions",[]),e(this,"initial_point",null),e(this,"getPartAtLength",(function(t){t<0?t=0:t>n.length&&(t=n.length);for(var e=n.partial_lengths.length-1;n.partial_lengths[e]>=t&&e>0;)e--;return e++,{fraction:t-n.partial_lengths[e-1],i:e}})),e(this,"getTotalLength",(function(){return n.length})),e(this,"getPointAtLength",(function(t){var e=n.getPartAtLength(t),i=n.functions[e.i];if(i)return i.getPointAtLength(e.fraction);if(n.initial_point)return n.initial_point;throw new Error("Wrong function at this part.")})),e(this,"getTangentAtLength",(function(t){var e=n.getPartAtLength(t),i=n.functions[e.i];if(i)return i.getTangentAtLength(e.fraction);if(n.initial_point)return{x:0,y:0};throw new Error("Wrong function at this part.")})),e(this,"getPropertiesAtLength",(function(t){var e=n.getPartAtLength(t),i=n.functions[e.i];if(i)return i.getPropertiesAtLength(e.fraction);if(n.initial_point)return{x:n.initial_point.x,y:n.initial_point.y,tangentX:0,tangentY:0};throw new Error("Wrong function at this part.")})),e(this,"getParts",(function(){for(var t=[],e=0;e0?t:"M0,0").match(s);if(!n)throw new Error("No path elements found in string ".concat(t));return n.reduce((function(t,n){var e=n.charAt(0),r=e.toLowerCase(),h=g(n.substring(1));if("m"===r&&h.length>2&&(t.push([e].concat(i(h.splice(0,2)))),r="l",e="m"===e?"l":"L"),"a"===r.toLowerCase()&&(5===h.length||6===h.length)){var s=n.substring(1).trim().split(" ");h=[Number(s[0]),Number(s[1]),Number(s[2]),Number(s[3].charAt(0)),Number(s[3].charAt(1)),Number(s[3].substring(2)),Number(s[4])]}for(;h.length>=0;){if(h.length===a[r]){t.push([e].concat(i(h.splice(0,a[r]))));break}if(h.length0?(this.length+=r.getTotalLength(),this.functions.push(r),o=[h[y][5]+o[0],h[y][6]+o[1]]):this.functions.push(new u(o[0],o[0],o[1],o[1]));else if("S"===h[y][0]){if(y>0&&["C","c","S","s"].indexOf(h[y-1][0])>-1){if(r){var p=r.getC();r=new C(o[0],o[1],2*o[0]-p.x,2*o[1]-p.y,h[y][1],h[y][2],h[y][3],h[y][4])}}else r=new C(o[0],o[1],o[0],o[1],h[y][1],h[y][2],h[y][3],h[y][4]);r&&(this.length+=r.getTotalLength(),o=[h[y][3],h[y][4]],this.functions.push(r))}else if("s"===h[y][0]){if(y>0&&["C","c","S","s"].indexOf(h[y-1][0])>-1){if(r){var x=r.getC(),v=r.getD();r=new C(o[0],o[1],o[0]+v.x-x.x,o[1]+v.y-x.y,o[0]+h[y][1],o[1]+h[y][2],o[0]+h[y][3],o[1]+h[y][4])}}else r=new C(o[0],o[1],o[0],o[1],o[0]+h[y][1],o[1]+h[y][2],o[0]+h[y][3],o[1]+h[y][4]);r&&(this.length+=r.getTotalLength(),o=[h[y][3]+o[0],h[y][4]+o[1]],this.functions.push(r))}else if("Q"===h[y][0]){if(o[0]==h[y][1]&&o[1]==h[y][2]){var M=new u(h[y][1],h[y][3],h[y][2],h[y][4]);this.length+=M.getTotalLength(),this.functions.push(M)}else r=new C(o[0],o[1],h[y][1],h[y][2],h[y][3],h[y][4],void 0,void 0),this.length+=r.getTotalLength(),this.functions.push(r);o=[h[y][3],h[y][4]],c=[h[y][1],h[y][2]]}else if("q"===h[y][0]){if(0!=h[y][1]||0!=h[y][2])r=new C(o[0],o[1],o[0]+h[y][1],o[1]+h[y][2],o[0]+h[y][3],o[1]+h[y][4],void 0,void 0),this.length+=r.getTotalLength(),this.functions.push(r);else{var w=new u(o[0]+h[y][1],o[0]+h[y][3],o[1]+h[y][2],o[1]+h[y][4]);this.length+=w.getTotalLength(),this.functions.push(w)}c=[o[0]+h[y][1],o[1]+h[y][2]],o=[h[y][3]+o[0],h[y][4]+o[1]]}else if("T"===h[y][0]){if(y>0&&["Q","q","T","t"].indexOf(h[y-1][0])>-1)r=new C(o[0],o[1],2*o[0]-c[0],2*o[1]-c[1],h[y][1],h[y][2],void 0,void 0),this.functions.push(r),this.length+=r.getTotalLength();else{var L=new u(o[0],h[y][1],o[1],h[y][2]);this.functions.push(L),this.length+=L.getTotalLength()}c=[2*o[0]-c[0],2*o[1]-c[1]],o=[h[y][1],h[y][2]]}else if("t"===h[y][0]){if(y>0&&["Q","q","T","t"].indexOf(h[y-1][0])>-1)r=new C(o[0],o[1],2*o[0]-c[0],2*o[1]-c[1],o[0]+h[y][1],o[1]+h[y][2],void 0,void 0),this.length+=r.getTotalLength(),this.functions.push(r);else{var A=new u(o[0],o[0]+h[y][1],o[1],o[1]+h[y][2]);this.length+=A.getTotalLength(),this.functions.push(A)}c=[2*o[0]-c[0],2*o[1]-c[1]],o=[h[y][1]+o[0],h[y][2]+o[1]]}else if("A"===h[y][0]){var d=new l(o[0],o[1],h[y][1],h[y][2],h[y][3],1===h[y][4],1===h[y][5],h[y][6],h[y][7]);this.length+=d.getTotalLength(),o=[h[y][6],h[y][7]],this.functions.push(d)}else if("a"===h[y][0]){var b=new l(o[0],o[1],h[y][1],h[y][2],h[y][3],1===h[y][4],1===h[y][5],o[0]+h[y][6],o[1]+h[y][7]);this.length+=b.getTotalLength(),o=[o[0]+h[y][6],o[1]+h[y][7]],this.functions.push(b)}this.partial_lengths.push(this.length)}})),j=n((function(t){var n=this;if(e(this,"inst",void 0),e(this,"getTotalLength",(function(){return n.inst.getTotalLength()})),e(this,"getPointAtLength",(function(t){return n.inst.getPointAtLength(t)})),e(this,"getTangentAtLength",(function(t){return n.inst.getTangentAtLength(t)})),e(this,"getPropertiesAtLength",(function(t){return n.inst.getPropertiesAtLength(t)})),e(this,"getParts",(function(){return n.inst.getParts()})),this.inst=new O(t),!(this instanceof j))return new j(t)}));export{j as svgPathProperties}; 3 | -------------------------------------------------------------------------------- /dist/svg-path-properties.min.js: -------------------------------------------------------------------------------- 1 | // http://geoexamples.com/path-properties/ v1.2.0 Copyright 2023 Roger Veciana i Rovira 2 | !function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((t="undefined"!=typeof globalThis?globalThis:t||self).svgPathProperties={})}(this,(function(t){"use strict";function n(t,n){for(var e=0;et.length)&&(n=t.length);for(var e=0,i=new Array(n);eu.length&&(t=u.length);var n=f({x:u.x0,y:u.y0},u.rx,u.ry,u.xAxisRotate,u.LargeArcFlag,u.SweepFlag,{x:u.x1,y:u.y1},t/u.length);return{x:n.x,y:n.y}})),i(this,"getTangentAtLength",(function(t){t<0?t=0:t>u.length&&(t=u.length);var n,e=.05,i=u.getPointAtLength(t);t<0?t=0:t>u.length&&(t=u.length);var r=(n=t1&&(n=Math.sqrt(c)*n,e=Math.sqrt(c)*e);var f=(Math.pow(n,2)*Math.pow(e,2)-Math.pow(n,2)*Math.pow(l.y,2)-Math.pow(e,2)*Math.pow(l.x,2))/(Math.pow(n,2)*Math.pow(l.y,2)+Math.pow(e,2)*Math.pow(l.x,2));f=f<0?0:f;var y=(r!==h?1:-1)*Math.sqrt(f),v=y*(n*l.y/e),M=y*(-e*l.x/n),L={x:Math.cos(o)*v-Math.sin(o)*M+(t.x+s.x)/2,y:Math.sin(o)*v+Math.cos(o)*M+(t.y+s.y)/2},d={x:(l.x-v)/n,y:(l.y-M)/e},A=w({x:1,y:0},d),b=w(d,{x:(-l.x-v)/n,y:(-l.y-M)/e});!h&&b>0?b-=2*Math.PI:h&&b<0&&(b+=2*Math.PI);var P=A+(b%=2*Math.PI)*a,m=n*Math.cos(P),T=e*Math.sin(P);return{x:Math.cos(o)*m-Math.sin(o)*T+L.x,y:Math.sin(o)*m+Math.cos(o)*T+L.y,ellipticalArcStartAngle:A,ellipticalArcEndAngle:A+b,ellipticalArcAngle:P,ellipticalArcCenter:L,resultantRx:n,resultantRy:e}},y=function(t,n){t=t||500;for(var e,i=0,r=[],h=[],s=n(0),a=0;a0?Math.sqrt(l*l+c):0,y=u*u+c>0?Math.sqrt(u*u+c):0,p=u+Math.sqrt(u*u+c)!==0&&(l+f)/(u+y)!=0?c*Math.log(Math.abs((l+f)/(u+y))):0;return Math.sqrt(a)/2*(l*f-u*y+p)},_=function(t,n,e){return{x:2*(1-e)*(t[1]-t[0])+2*e*(t[2]-t[1]),y:2*(1-e)*(n[1]-n[0])+2*e*(n[2]-n[1])}};function S(t,n,e){var i=N(1,e,t),r=N(1,e,n),h=i*i+r*r;return Math.sqrt(h)}var N=function t(n,e,i){var r,h,s=i.length-1;if(0===s)return 0;if(0===n){h=0;for(var a=0;a<=s;a++)h+=A[s][a]*Math.pow(1-e,s-a)*Math.pow(e,a)*i[a];return h}r=new Array(s);for(var o=0;o.001;){var a=e(r+h),o=Math.abs(t-a)/n;if(o500)break}return r},j=e((function(t,n,e,r,h,s,a,o){var g=this;i(this,"a",void 0),i(this,"b",void 0),i(this,"c",void 0),i(this,"d",void 0),i(this,"length",void 0),i(this,"getArcLength",void 0),i(this,"getPoint",void 0),i(this,"getDerivative",void 0),i(this,"getTotalLength",(function(){return g.length})),i(this,"getPointAtLength",(function(t){var n=[g.a.x,g.b.x,g.c.x,g.d.x],e=[g.a.y,g.b.y,g.c.y,g.d.y],i=C(t,g.length,(function(t){return g.getArcLength(n,e,t)}));return g.getPoint(n,e,i)})),i(this,"getTangentAtLength",(function(t){var n=[g.a.x,g.b.x,g.c.x,g.d.x],e=[g.a.y,g.b.y,g.c.y,g.d.y],i=C(t,g.length,(function(t){return g.getArcLength(n,e,t)})),r=g.getDerivative(n,e,i),h=Math.sqrt(r.x*r.x+r.y*r.y);return h>0?{x:r.x/h,y:r.y/h}:{x:0,y:0}})),i(this,"getPropertiesAtLength",(function(t){var n,e=[g.a.x,g.b.x,g.c.x,g.d.x],i=[g.a.y,g.b.y,g.c.y,g.d.y],r=C(t,g.length,(function(t){return g.getArcLength(e,i,t)})),h=g.getDerivative(e,i,r),s=Math.sqrt(h.x*h.x+h.y*h.y);n=s>0?{x:h.x/s,y:h.y/s}:{x:0,y:0};var a=g.getPoint(e,i,r);return{x:a.x,y:a.y,tangentX:n.x,tangentY:n.y}})),i(this,"getC",(function(){return g.c})),i(this,"getD",(function(){return g.d})),this.a={x:t,y:n},this.b={x:e,y:r},this.c={x:h,y:s},void 0!==a&&void 0!==o?(this.getArcLength=m,this.getPoint=b,this.getDerivative=P,this.d={x:a,y:o}):(this.getArcLength=q,this.getPoint=T,this.getDerivative=_,this.d={x:0,y:0}),this.length=this.getArcLength([this.a.x,this.b.x,this.c.x,this.d.x],[this.a.y,this.b.y,this.c.y,this.d.y],1)})),O=e((function(t){var n=this;i(this,"length",0),i(this,"partial_lengths",[]),i(this,"functions",[]),i(this,"initial_point",null),i(this,"getPartAtLength",(function(t){t<0?t=0:t>n.length&&(t=n.length);for(var e=n.partial_lengths.length-1;n.partial_lengths[e]>=t&&e>0;)e--;return e++,{fraction:t-n.partial_lengths[e-1],i:e}})),i(this,"getTotalLength",(function(){return n.length})),i(this,"getPointAtLength",(function(t){var e=n.getPartAtLength(t),i=n.functions[e.i];if(i)return i.getPointAtLength(e.fraction);if(n.initial_point)return n.initial_point;throw new Error("Wrong function at this part.")})),i(this,"getTangentAtLength",(function(t){var e=n.getPartAtLength(t),i=n.functions[e.i];if(i)return i.getTangentAtLength(e.fraction);if(n.initial_point)return{x:0,y:0};throw new Error("Wrong function at this part.")})),i(this,"getPropertiesAtLength",(function(t){var e=n.getPartAtLength(t),i=n.functions[e.i];if(i)return i.getPropertiesAtLength(e.fraction);if(n.initial_point)return{x:n.initial_point.x,y:n.initial_point.y,tangentX:0,tangentY:0};throw new Error("Wrong function at this part.")})),i(this,"getParts",(function(){for(var t=[],e=0;e0?t:"M0,0").match(o);if(!n)throw new Error("No path elements found in string ".concat(t));return n.reduce((function(t,n){var e=n.charAt(0),i=e.toLowerCase(),h=u(n.substring(1));if("m"===i&&h.length>2&&(t.push([e].concat(r(h.splice(0,2)))),i="l",e="m"===e?"l":"L"),"a"===i.toLowerCase()&&(5===h.length||6===h.length)){var s=n.substring(1).trim().split(" ");h=[Number(s[0]),Number(s[1]),Number(s[2]),Number(s[3].charAt(0)),Number(s[3].charAt(1)),Number(s[3].substring(2)),Number(s[4])]}for(;h.length>=0;){if(h.length===a[i]){t.push([e].concat(r(h.splice(0,a[i]))));break}if(h.length0?(this.length+=e.getTotalLength(),this.functions.push(e),s=[h[y][5]+s[0],h[y][6]+s[1]]):this.functions.push(new l(s[0],s[0],s[1],s[1]));else if("S"===h[y][0]){if(y>0&&["C","c","S","s"].indexOf(h[y-1][0])>-1){if(e){var p=e.getC();e=new j(s[0],s[1],2*s[0]-p.x,2*s[1]-p.y,h[y][1],h[y][2],h[y][3],h[y][4])}}else e=new j(s[0],s[1],s[0],s[1],h[y][1],h[y][2],h[y][3],h[y][4]);e&&(this.length+=e.getTotalLength(),s=[h[y][3],h[y][4]],this.functions.push(e))}else if("s"===h[y][0]){if(y>0&&["C","c","S","s"].indexOf(h[y-1][0])>-1){if(e){var x=e.getC(),v=e.getD();e=new j(s[0],s[1],s[0]+v.x-x.x,s[1]+v.y-x.y,s[0]+h[y][1],s[1]+h[y][2],s[0]+h[y][3],s[1]+h[y][4])}}else e=new j(s[0],s[1],s[0],s[1],s[0]+h[y][1],s[1]+h[y][2],s[0]+h[y][3],s[1]+h[y][4]);e&&(this.length+=e.getTotalLength(),s=[h[y][3]+s[0],h[y][4]+s[1]],this.functions.push(e))}else if("Q"===h[y][0]){if(s[0]==h[y][1]&&s[1]==h[y][2]){var M=new l(h[y][1],h[y][3],h[y][2],h[y][4]);this.length+=M.getTotalLength(),this.functions.push(M)}else e=new j(s[0],s[1],h[y][1],h[y][2],h[y][3],h[y][4],void 0,void 0),this.length+=e.getTotalLength(),this.functions.push(e);s=[h[y][3],h[y][4]],g=[h[y][1],h[y][2]]}else if("q"===h[y][0]){if(0!=h[y][1]||0!=h[y][2])e=new j(s[0],s[1],s[0]+h[y][1],s[1]+h[y][2],s[0]+h[y][3],s[1]+h[y][4],void 0,void 0),this.length+=e.getTotalLength(),this.functions.push(e);else{var w=new l(s[0]+h[y][1],s[0]+h[y][3],s[1]+h[y][2],s[1]+h[y][4]);this.length+=w.getTotalLength(),this.functions.push(w)}g=[s[0]+h[y][1],s[1]+h[y][2]],s=[h[y][3]+s[0],h[y][4]+s[1]]}else if("T"===h[y][0]){if(y>0&&["Q","q","T","t"].indexOf(h[y-1][0])>-1)e=new j(s[0],s[1],2*s[0]-g[0],2*s[1]-g[1],h[y][1],h[y][2],void 0,void 0),this.functions.push(e),this.length+=e.getTotalLength();else{var L=new l(s[0],h[y][1],s[1],h[y][2]);this.functions.push(L),this.length+=L.getTotalLength()}g=[2*s[0]-g[0],2*s[1]-g[1]],s=[h[y][1],h[y][2]]}else if("t"===h[y][0]){if(y>0&&["Q","q","T","t"].indexOf(h[y-1][0])>-1)e=new j(s[0],s[1],2*s[0]-g[0],2*s[1]-g[1],s[0]+h[y][1],s[1]+h[y][2],void 0,void 0),this.length+=e.getTotalLength(),this.functions.push(e);else{var d=new l(s[0],s[0]+h[y][1],s[1],s[1]+h[y][2]);this.length+=d.getTotalLength(),this.functions.push(d)}g=[2*s[0]-g[0],2*s[1]-g[1]],s=[h[y][1]+s[0],h[y][2]+s[1]]}else if("A"===h[y][0]){var A=new c(s[0],s[1],h[y][1],h[y][2],h[y][3],1===h[y][4],1===h[y][5],h[y][6],h[y][7]);this.length+=A.getTotalLength(),s=[h[y][6],h[y][7]],this.functions.push(A)}else if("a"===h[y][0]){var b=new c(s[0],s[1],h[y][1],h[y][2],h[y][3],1===h[y][4],1===h[y][5],s[0]+h[y][6],s[1]+h[y][7]);this.length+=b.getTotalLength(),s=[s[0]+h[y][6],s[1]+h[y][7]],this.functions.push(b)}this.partial_lengths.push(this.length)}})),E=e((function(t){var n=this;if(i(this,"inst",void 0),i(this,"getTotalLength",(function(){return n.inst.getTotalLength()})),i(this,"getPointAtLength",(function(t){return n.inst.getPointAtLength(t)})),i(this,"getTangentAtLength",(function(t){return n.inst.getTangentAtLength(t)})),i(this,"getPropertiesAtLength",(function(t){return n.inst.getPropertiesAtLength(t)})),i(this,"getParts",(function(){return n.inst.getParts()})),this.inst=new O(t),!(this instanceof E))return new E(t)}));t.svgPathProperties=E})); 3 | -------------------------------------------------------------------------------- /dist/types/arc.d.ts: -------------------------------------------------------------------------------- 1 | import { Properties, Point, PointProperties } from "./types"; 2 | export declare class Arc implements Properties { 3 | private x0; 4 | private y0; 5 | private rx; 6 | private ry; 7 | private xAxisRotate; 8 | private LargeArcFlag; 9 | private SweepFlag; 10 | private x1; 11 | private y1; 12 | private length; 13 | constructor(x0: number, y0: number, rx: number, ry: number, xAxisRotate: number, LargeArcFlag: boolean, SweepFlag: boolean, x1: number, y1: number); 14 | getTotalLength: () => number; 15 | getPointAtLength: (fractionLength: number) => Point; 16 | getTangentAtLength: (fractionLength: number) => Point; 17 | getPropertiesAtLength: (fractionLength: number) => PointProperties; 18 | } 19 | -------------------------------------------------------------------------------- /dist/types/bezier-functions.d.ts: -------------------------------------------------------------------------------- 1 | import { Point } from "./types"; 2 | export declare const cubicPoint: (xs: number[], ys: number[], t: number) => Point; 3 | export declare const cubicDerivative: (xs: number[], ys: number[], t: number) => Point; 4 | export declare const getCubicArcLength: (xs: number[], ys: number[], t: number) => number; 5 | export declare const quadraticPoint: (xs: number[], ys: number[], t: number) => Point; 6 | export declare const getQuadraticArcLength: (xs: number[], ys: number[], t: number) => number; 7 | export declare const quadraticDerivative: (xs: number[], ys: number[], t: number) => { 8 | x: number; 9 | y: number; 10 | }; 11 | export declare const t2length: (length: number, totalLength: number, func: (t: number) => number) => number; 12 | -------------------------------------------------------------------------------- /dist/types/bezier-values.d.ts: -------------------------------------------------------------------------------- 1 | export declare const tValues: number[][]; 2 | export declare const cValues: number[][]; 3 | export declare const binomialCoefficients: number[][]; 4 | -------------------------------------------------------------------------------- /dist/types/bezier.d.ts: -------------------------------------------------------------------------------- 1 | import { Properties, Point } from "./types"; 2 | export declare class Bezier implements Properties { 3 | private a; 4 | private b; 5 | private c; 6 | private d; 7 | private length; 8 | private getArcLength; 9 | private getPoint; 10 | private getDerivative; 11 | constructor(ax: number, ay: number, bx: number, by: number, cx: number, cy: number, dx: number | undefined, dy: number | undefined); 12 | getTotalLength: () => number; 13 | getPointAtLength: (length: number) => Point; 14 | getTangentAtLength: (length: number) => Point; 15 | getPropertiesAtLength: (length: number) => { 16 | x: number; 17 | y: number; 18 | tangentX: number; 19 | tangentY: number; 20 | }; 21 | getC: () => Point; 22 | getD: () => Point; 23 | } 24 | -------------------------------------------------------------------------------- /dist/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import SVGPathProperties from "./svg-path-properties"; 2 | declare class _svgPathProperties { 3 | inst: SVGPathProperties; 4 | constructor(svgPath: string); 5 | getTotalLength: () => number; 6 | getPointAtLength: (fractionLength: number) => import("./types").Point; 7 | getTangentAtLength: (fractionLength: number) => import("./types").Point; 8 | getPropertiesAtLength: (fractionLength: number) => import("./types").PointProperties; 9 | getParts: () => import("./types").PartProperties[]; 10 | } 11 | type svgPathProperties = _svgPathProperties; 12 | export declare const svgPathProperties: typeof _svgPathProperties & ((val: string) => svgPathProperties); 13 | export {}; 14 | -------------------------------------------------------------------------------- /dist/types/linear.d.ts: -------------------------------------------------------------------------------- 1 | import { Properties, Point, PointProperties } from "./types"; 2 | export declare class LinearPosition implements Properties { 3 | private x0; 4 | private x1; 5 | private y0; 6 | private y1; 7 | constructor(x0: number, x1: number, y0: number, y1: number); 8 | getTotalLength: () => number; 9 | getPointAtLength: (pos: number) => Point; 10 | getTangentAtLength: (_: number) => Point; 11 | getPropertiesAtLength: (pos: number) => PointProperties; 12 | } 13 | -------------------------------------------------------------------------------- /dist/types/parse.d.ts: -------------------------------------------------------------------------------- 1 | declare const _default: (path: string) => [string, ...number[]][]; 2 | export default _default; 3 | -------------------------------------------------------------------------------- /dist/types/svg-path-properties.d.ts: -------------------------------------------------------------------------------- 1 | import { Properties, PartProperties, Point } from "./types"; 2 | export default class SVGPathProperties implements Properties { 3 | private length; 4 | private partial_lengths; 5 | private functions; 6 | private initial_point; 7 | constructor(source: string | [string, ...Array][]); 8 | private getPartAtLength; 9 | getTotalLength: () => number; 10 | getPointAtLength: (fractionLength: number) => Point; 11 | getTangentAtLength: (fractionLength: number) => Point; 12 | getPropertiesAtLength: (fractionLength: number) => import("./types").PointProperties; 13 | getParts: () => PartProperties[]; 14 | } 15 | -------------------------------------------------------------------------------- /dist/types/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Properties { 2 | getTotalLength(): number; 3 | getPointAtLength(pos: number): Point; 4 | getTangentAtLength(pos: number): Point; 5 | getPropertiesAtLength(pos: number): PointProperties; 6 | } 7 | export interface PartProperties { 8 | start: Point; 9 | end: Point; 10 | length: number; 11 | getPointAtLength(pos: number): Point; 12 | getTangentAtLength(pos: number): Point; 13 | getPropertiesAtLength(pos: number): PointProperties; 14 | } 15 | export interface Point { 16 | x: number; 17 | y: number; 18 | } 19 | export type PointArray = [number, number]; 20 | export interface PointProperties { 21 | x: number; 22 | y: number; 23 | tangentX: number; 24 | tangentY: number; 25 | } 26 | export type pathOrders = "a" | "c" | "h" | "l" | "m" | "q" | "s" | "t" | "v" | "z"; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svg-path-properties", 3 | "version": "1.3.0", 4 | "description": "Calculate the length for an SVG path, to use it with node or a Canvas element", 5 | "keywords": [ 6 | "path", 7 | "getPointAtLength", 8 | "length", 9 | "canvas", 10 | "svg" 11 | ], 12 | "homepage": "http://geoexamples.com/path-properties/", 13 | "main": "dist/svg-path-properties.cjs.js", 14 | "module": "dist/svg-path-properties.esm.js", 15 | "unpkg": "dist/svg-path-properties.min.js", 16 | "types": "dist/types/index.d.ts", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/rveciana/svg-path-properties.git" 20 | }, 21 | "scripts": { 22 | "type-check": "tsc --noEmit", 23 | "type-check:watch": "npm run type-check -- --watch", 24 | "build:types": "tsc --emitDeclarationOnly", 25 | "build:js": "rollup -c --bundleConfigAsCjs", 26 | "build": "npm run build:types && npm run build:js", 27 | "test": "ts-node --skip-project node_modules/tape/bin/tape test/**/*-test.ts", 28 | "preversion": "npm run build && git add dist && git commit --allow-empty -am \"new version\"", 29 | "postpublish": "git push && git push --tags" 30 | }, 31 | "author": { 32 | "name": "Roger Veciana i Rovira", 33 | "url": "https://geoexamples.com" 34 | }, 35 | "license": "ISC", 36 | "devDependencies": { 37 | "@babel/core": "^7.23.5", 38 | "@babel/plugin-proposal-class-properties": "^7.18.6", 39 | "@babel/plugin-proposal-object-rest-spread": "^7.20.7", 40 | "@babel/preset-env": "^7.23.5", 41 | "@babel/preset-typescript": "^7.23.3", 42 | "@types/tape": "^5.6.4", 43 | "babel6-plugin-strip-class-callcheck": "^6.0.0", 44 | "eslint": "^8.55.0", 45 | "prettier": "^3.1.0", 46 | "rollup": "^4.6.1", 47 | "rollup-plugin-babel": "^4.4.0", 48 | "rollup-plugin-commonjs": "^10.1.0", 49 | "rollup-plugin-node-resolve": "^5.2.0", 50 | "rollup-plugin-terser": "^7.0.2", 51 | "tape": "^5.7.2", 52 | "ts-node": "^10.9.1", 53 | "typescript": "^5.3.2" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from "rollup-plugin-commonjs"; 2 | import resolve from "rollup-plugin-node-resolve"; 3 | import babel from "rollup-plugin-babel"; 4 | import { terser } from "rollup-plugin-terser"; 5 | 6 | import pkg from "./package.json"; 7 | const extensions = [".js", ".jsx", ".ts", ".tsx"]; 8 | 9 | const name = "svgPathProperties"; 10 | const banner = `// ${pkg.homepage} v${ 11 | pkg.version 12 | } Copyright ${new Date().getFullYear()} ${pkg.author.name}`; 13 | 14 | export default { 15 | input: "./src/index.ts", 16 | 17 | // Specify here external modules which you don't want to include in your bundle (for instance: 'lodash', 'moment' etc.) 18 | // https://rollupjs.org/guide/en#external-e-external 19 | external: [], 20 | 21 | plugins: [ 22 | // Allows node_modules resolution 23 | resolve({ extensions }), 24 | 25 | // Allow bundling cjs modules. Rollup doesn't understand cjs 26 | commonjs(), 27 | 28 | // Compile TypeScript/JavaScript files 29 | babel({ extensions, include: ["src/**/*"] }), 30 | terser({ 31 | output: { 32 | preamble: banner 33 | } 34 | }) 35 | ], 36 | 37 | output: [ 38 | { 39 | file: pkg.main, 40 | format: "cjs" 41 | }, 42 | { 43 | file: pkg.module, 44 | format: "es" 45 | }, 46 | { 47 | file: pkg.unpkg, 48 | format: "umd", 49 | name, 50 | 51 | // https://rollupjs.org/guide/en#output-globals-g-globals 52 | globals: {} 53 | } 54 | ] 55 | }; 56 | -------------------------------------------------------------------------------- /src/arc.ts: -------------------------------------------------------------------------------- 1 | import { Properties, Point, PointProperties } from "./types"; 2 | 3 | export class Arc implements Properties { 4 | private x0: number; 5 | private y0: number; 6 | private rx: number; 7 | private ry: number; 8 | private xAxisRotate: number; 9 | private LargeArcFlag: boolean; 10 | private SweepFlag: boolean; 11 | private x1: number; 12 | private y1: number; 13 | private length: number; 14 | constructor( 15 | x0: number, 16 | y0: number, 17 | rx: number, 18 | ry: number, 19 | xAxisRotate: number, 20 | LargeArcFlag: boolean, 21 | SweepFlag: boolean, 22 | x1: number, 23 | y1: number 24 | ) { 25 | this.x0 = x0; 26 | this.y0 = y0; 27 | this.rx = rx; 28 | this.ry = ry; 29 | this.xAxisRotate = xAxisRotate; 30 | this.LargeArcFlag = LargeArcFlag; 31 | this.SweepFlag = SweepFlag; 32 | this.x1 = x1; 33 | this.y1 = y1; 34 | 35 | const lengthProperties = approximateArcLengthOfCurve(300, function(t: number) { 36 | return pointOnEllipticalArc( 37 | { x: x0, y: y0 }, 38 | rx, 39 | ry, 40 | xAxisRotate, 41 | LargeArcFlag, 42 | SweepFlag, 43 | { x: x1, y: y1 }, 44 | t 45 | ); 46 | }); 47 | this.length = lengthProperties.arcLength; 48 | } 49 | 50 | public getTotalLength = () => { 51 | return this.length; 52 | }; 53 | 54 | public getPointAtLength = (fractionLength: number): Point => { 55 | if (fractionLength < 0) { 56 | fractionLength = 0; 57 | } else if (fractionLength > this.length) { 58 | fractionLength = this.length; 59 | } 60 | 61 | const position = pointOnEllipticalArc( 62 | { x: this.x0, y: this.y0 }, 63 | this.rx, 64 | this.ry, 65 | this.xAxisRotate, 66 | this.LargeArcFlag, 67 | this.SweepFlag, 68 | { x: this.x1, y: this.y1 }, 69 | fractionLength / this.length 70 | ); 71 | 72 | return { x: position.x, y: position.y }; 73 | }; 74 | 75 | public getTangentAtLength = (fractionLength: number): Point => { 76 | if (fractionLength < 0) { 77 | fractionLength = 0; 78 | } else if (fractionLength > this.length) { 79 | fractionLength = this.length; 80 | } 81 | const point_dist = 0.05; // needs testing 82 | const p1 = this.getPointAtLength(fractionLength); 83 | let p2: Point; 84 | 85 | if (fractionLength < 0) { 86 | fractionLength = 0; 87 | } else if (fractionLength > this.length) { 88 | fractionLength = this.length; 89 | } 90 | 91 | if (fractionLength < this.length - point_dist) { 92 | p2 = this.getPointAtLength(fractionLength + point_dist); 93 | } else { 94 | p2 = this.getPointAtLength(fractionLength - point_dist); 95 | } 96 | 97 | const xDist = p2.x - p1.x; 98 | const yDist = p2.y - p1.y; 99 | const dist = Math.sqrt(xDist * xDist + yDist * yDist); 100 | 101 | if (fractionLength < this.length - point_dist) { 102 | return { x: -xDist / dist, y: -yDist / dist }; 103 | } else { 104 | return { x: xDist / dist, y: yDist / dist }; 105 | } 106 | }; 107 | 108 | public getPropertiesAtLength = (fractionLength: number): PointProperties => { 109 | const tangent = this.getTangentAtLength(fractionLength); 110 | const point = this.getPointAtLength(fractionLength); 111 | return { x: point.x, y: point.y, tangentX: tangent.x, tangentY: tangent.y }; 112 | }; 113 | } 114 | 115 | interface PointOnEllipticalArc { 116 | x: number; 117 | y: number; 118 | ellipticalArcAngle: number; 119 | } 120 | 121 | const pointOnEllipticalArc = ( 122 | p0: Point, 123 | rx: number, 124 | ry: number, 125 | xAxisRotation: number, 126 | largeArcFlag: boolean, 127 | sweepFlag: boolean, 128 | p1: Point, 129 | t: number 130 | ): PointOnEllipticalArc => { 131 | // In accordance to: http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters 132 | rx = Math.abs(rx); 133 | ry = Math.abs(ry); 134 | xAxisRotation = mod(xAxisRotation, 360); 135 | const xAxisRotationRadians = toRadians(xAxisRotation); 136 | // If the endpoints are identical, then this is equivalent to omitting the elliptical arc segment entirely. 137 | if (p0.x === p1.x && p0.y === p1.y) { 138 | return { x: p0.x, y: p0.y, ellipticalArcAngle: 0 }; // Check if angle is correct 139 | } 140 | 141 | // If rx = 0 or ry = 0 then this arc is treated as a straight line segment joining the endpoints. 142 | if (rx === 0 || ry === 0) { 143 | //return this.pointOnLine(p0, p1, t); 144 | return { x: 0, y: 0, ellipticalArcAngle: 0 }; // Check if angle is correct 145 | } 146 | 147 | // Following "Conversion from endpoint to center parameterization" 148 | // http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter 149 | 150 | // Step #1: Compute transformedPoint 151 | const dx = (p0.x - p1.x) / 2; 152 | const dy = (p0.y - p1.y) / 2; 153 | const transformedPoint = { 154 | x: Math.cos(xAxisRotationRadians) * dx + Math.sin(xAxisRotationRadians) * dy, 155 | y: -Math.sin(xAxisRotationRadians) * dx + Math.cos(xAxisRotationRadians) * dy 156 | }; 157 | // Ensure radii are large enough 158 | const radiiCheck = 159 | Math.pow(transformedPoint.x, 2) / Math.pow(rx, 2) + 160 | Math.pow(transformedPoint.y, 2) / Math.pow(ry, 2); 161 | if (radiiCheck > 1) { 162 | rx = Math.sqrt(radiiCheck) * rx; 163 | ry = Math.sqrt(radiiCheck) * ry; 164 | } 165 | 166 | // Step #2: Compute transformedCenter 167 | const cSquareNumerator = 168 | Math.pow(rx, 2) * Math.pow(ry, 2) - 169 | Math.pow(rx, 2) * Math.pow(transformedPoint.y, 2) - 170 | Math.pow(ry, 2) * Math.pow(transformedPoint.x, 2); 171 | const cSquareRootDenom = 172 | Math.pow(rx, 2) * Math.pow(transformedPoint.y, 2) + 173 | Math.pow(ry, 2) * Math.pow(transformedPoint.x, 2); 174 | let cRadicand = cSquareNumerator / cSquareRootDenom; 175 | // Make sure this never drops below zero because of precision 176 | cRadicand = cRadicand < 0 ? 0 : cRadicand; 177 | const cCoef = (largeArcFlag !== sweepFlag ? 1 : -1) * Math.sqrt(cRadicand); 178 | const transformedCenter = { 179 | x: cCoef * ((rx * transformedPoint.y) / ry), 180 | y: cCoef * (-(ry * transformedPoint.x) / rx) 181 | }; 182 | 183 | // Step #3: Compute center 184 | const center = { 185 | x: 186 | Math.cos(xAxisRotationRadians) * transformedCenter.x - 187 | Math.sin(xAxisRotationRadians) * transformedCenter.y + 188 | (p0.x + p1.x) / 2, 189 | y: 190 | Math.sin(xAxisRotationRadians) * transformedCenter.x + 191 | Math.cos(xAxisRotationRadians) * transformedCenter.y + 192 | (p0.y + p1.y) / 2 193 | }; 194 | 195 | // Step #4: Compute start/sweep angles 196 | // Start angle of the elliptical arc prior to the stretch and rotate operations. 197 | // Difference between the start and end angles 198 | const startVector = { 199 | x: (transformedPoint.x - transformedCenter.x) / rx, 200 | y: (transformedPoint.y - transformedCenter.y) / ry 201 | }; 202 | const startAngle = angleBetween( 203 | { 204 | x: 1, 205 | y: 0 206 | }, 207 | startVector 208 | ); 209 | 210 | const endVector = { 211 | x: (-transformedPoint.x - transformedCenter.x) / rx, 212 | y: (-transformedPoint.y - transformedCenter.y) / ry 213 | }; 214 | let sweepAngle = angleBetween(startVector, endVector); 215 | 216 | if (!sweepFlag && sweepAngle > 0) { 217 | sweepAngle -= 2 * Math.PI; 218 | } else if (sweepFlag && sweepAngle < 0) { 219 | sweepAngle += 2 * Math.PI; 220 | } 221 | // We use % instead of `mod(..)` because we want it to be -360deg to 360deg(but actually in radians) 222 | sweepAngle %= 2 * Math.PI; 223 | 224 | // From http://www.w3.org/TR/SVG/implnote.html#ArcParameterizationAlternatives 225 | const angle = startAngle + sweepAngle * t; 226 | const ellipseComponentX = rx * Math.cos(angle); 227 | const ellipseComponentY = ry * Math.sin(angle); 228 | 229 | const point = { 230 | x: 231 | Math.cos(xAxisRotationRadians) * ellipseComponentX - 232 | Math.sin(xAxisRotationRadians) * ellipseComponentY + 233 | center.x, 234 | y: 235 | Math.sin(xAxisRotationRadians) * ellipseComponentX + 236 | Math.cos(xAxisRotationRadians) * ellipseComponentY + 237 | center.y, 238 | ellipticalArcStartAngle: startAngle, 239 | ellipticalArcEndAngle: startAngle + sweepAngle, 240 | ellipticalArcAngle: angle, 241 | ellipticalArcCenter: center, 242 | resultantRx: rx, 243 | resultantRy: ry 244 | }; 245 | 246 | return point; 247 | }; 248 | 249 | const approximateArcLengthOfCurve = ( 250 | resolution: number, 251 | pointOnCurveFunc: (t: number) => Point 252 | ) => { 253 | // Resolution is the number of segments we use 254 | resolution = resolution ? resolution : 500; 255 | 256 | let resultantArcLength = 0; 257 | const arcLengthMap = []; 258 | const approximationLines = []; 259 | 260 | let prevPoint = pointOnCurveFunc(0); 261 | let nextPoint; 262 | for (let i = 0; i < resolution; i++) { 263 | const t = clamp(i * (1 / resolution), 0, 1); 264 | nextPoint = pointOnCurveFunc(t); 265 | resultantArcLength += distance(prevPoint, nextPoint); 266 | approximationLines.push([prevPoint, nextPoint]); 267 | 268 | arcLengthMap.push({ 269 | t: t, 270 | arcLength: resultantArcLength 271 | }); 272 | 273 | prevPoint = nextPoint; 274 | } 275 | // Last stretch to the endpoint 276 | nextPoint = pointOnCurveFunc(1); 277 | approximationLines.push([prevPoint, nextPoint]); 278 | resultantArcLength += distance(prevPoint, nextPoint); 279 | arcLengthMap.push({ 280 | t: 1, 281 | arcLength: resultantArcLength 282 | }); 283 | 284 | return { 285 | arcLength: resultantArcLength, 286 | arcLengthMap: arcLengthMap, 287 | approximationLines: approximationLines 288 | }; 289 | }; 290 | 291 | const mod = (x: number, m: number) => { 292 | return ((x % m) + m) % m; 293 | }; 294 | 295 | const toRadians = (angle: number) => { 296 | return angle * (Math.PI / 180); 297 | }; 298 | 299 | const distance = (p0: Point, p1: Point) => { 300 | return Math.sqrt(Math.pow(p1.x - p0.x, 2) + Math.pow(p1.y - p0.y, 2)); 301 | }; 302 | 303 | const clamp = (val: number, min: number, max: number) => { 304 | return Math.min(Math.max(val, min), max); 305 | }; 306 | 307 | const angleBetween = (v0: Point, v1: Point) => { 308 | const p = v0.x * v1.x + v0.y * v1.y; 309 | const n = Math.sqrt( 310 | (Math.pow(v0.x, 2) + Math.pow(v0.y, 2)) * (Math.pow(v1.x, 2) + Math.pow(v1.y, 2)) 311 | ); 312 | const sign = v0.x * v1.y - v0.y * v1.x < 0 ? -1 : 1; 313 | const angle = sign * Math.acos(p / n); 314 | 315 | return angle; 316 | }; 317 | -------------------------------------------------------------------------------- /src/bezier-functions.ts: -------------------------------------------------------------------------------- 1 | import { tValues, cValues, binomialCoefficients } from "./bezier-values"; 2 | import { Point } from "./types"; 3 | export const cubicPoint = (xs: number[], ys: number[], t: number): Point => { 4 | const x = 5 | (1 - t) * (1 - t) * (1 - t) * xs[0] + 6 | 3 * (1 - t) * (1 - t) * t * xs[1] + 7 | 3 * (1 - t) * t * t * xs[2] + 8 | t * t * t * xs[3]; 9 | const y = 10 | (1 - t) * (1 - t) * (1 - t) * ys[0] + 11 | 3 * (1 - t) * (1 - t) * t * ys[1] + 12 | 3 * (1 - t) * t * t * ys[2] + 13 | t * t * t * ys[3]; 14 | 15 | return { x: x, y: y }; 16 | }; 17 | 18 | export const cubicDerivative = (xs: number[], ys: number[], t: number) => { 19 | const derivative = quadraticPoint( 20 | [3 * (xs[1] - xs[0]), 3 * (xs[2] - xs[1]), 3 * (xs[3] - xs[2])], 21 | [3 * (ys[1] - ys[0]), 3 * (ys[2] - ys[1]), 3 * (ys[3] - ys[2])], 22 | t 23 | ); 24 | return derivative; 25 | }; 26 | 27 | export const getCubicArcLength = (xs: number[], ys: number[], t: number) => { 28 | let z: number; 29 | let sum: number; 30 | let correctedT: number; 31 | 32 | /*if (xs.length >= tValues.length) { 33 | throw new Error('too high n bezier'); 34 | }*/ 35 | 36 | const n = 20; 37 | 38 | z = t / 2; 39 | sum = 0; 40 | for (let i = 0; i < n; i++) { 41 | correctedT = z * tValues[n][i] + z; 42 | sum += cValues[n][i] * BFunc(xs, ys, correctedT); 43 | } 44 | return z * sum; 45 | }; 46 | 47 | export const quadraticPoint = ( 48 | xs: number[], 49 | ys: number[], 50 | t: number 51 | ): Point => { 52 | const x = (1 - t) * (1 - t) * xs[0] + 2 * (1 - t) * t * xs[1] + t * t * xs[2]; 53 | const y = (1 - t) * (1 - t) * ys[0] + 2 * (1 - t) * t * ys[1] + t * t * ys[2]; 54 | return { x: x, y: y }; 55 | }; 56 | 57 | export const getQuadraticArcLength = ( 58 | xs: number[], 59 | ys: number[], 60 | t: number 61 | ) => { 62 | if (t === undefined) { 63 | t = 1; 64 | } 65 | const ax = xs[0] - 2 * xs[1] + xs[2]; 66 | const ay = ys[0] - 2 * ys[1] + ys[2]; 67 | const bx = 2 * xs[1] - 2 * xs[0]; 68 | const by = 2 * ys[1] - 2 * ys[0]; 69 | 70 | const A = 4 * (ax * ax + ay * ay); 71 | const B = 4 * (ax * bx + ay * by); 72 | const C = bx * bx + by * by; 73 | 74 | if (A === 0) { 75 | return ( 76 | t * Math.sqrt(Math.pow(xs[2] - xs[0], 2) + Math.pow(ys[2] - ys[0], 2)) 77 | ); 78 | } 79 | const b = B / (2 * A); 80 | const c = C / A; 81 | const u = t + b; 82 | const k = c - b * b; 83 | 84 | const uuk = u * u + k > 0 ? Math.sqrt(u * u + k) : 0; 85 | const bbk = b * b + k > 0 ? Math.sqrt(b * b + k) : 0; 86 | const term = 87 | b + Math.sqrt(b * b + k) !== 0 && ((u + uuk) / (b + bbk)) != 0 88 | ? k * Math.log(Math.abs((u + uuk) / (b + bbk))) 89 | : 0; 90 | 91 | return (Math.sqrt(A) / 2) * (u * uuk - b * bbk + term); 92 | }; 93 | 94 | export const quadraticDerivative = (xs: number[], ys: number[], t: number) => { 95 | return { 96 | x: (1 - t) * 2 * (xs[1] - xs[0]) + t * 2 * (xs[2] - xs[1]), 97 | y: (1 - t) * 2 * (ys[1] - ys[0]) + t * 2 * (ys[2] - ys[1]), 98 | }; 99 | }; 100 | 101 | function BFunc(xs: number[], ys: number[], t: number) { 102 | const xbase = getDerivative(1, t, xs); 103 | const ybase = getDerivative(1, t, ys); 104 | const combined = xbase * xbase + ybase * ybase; 105 | return Math.sqrt(combined); 106 | } 107 | 108 | /** 109 | * Compute the curve derivative (hodograph) at t. 110 | */ 111 | const getDerivative = (derivative: number, t: number, vs: number[]): number => { 112 | // the derivative of any 't'-less function is zero. 113 | const n = vs.length - 1; 114 | let _vs; 115 | let value; 116 | 117 | if (n === 0) { 118 | return 0; 119 | } 120 | 121 | // direct values? compute! 122 | if (derivative === 0) { 123 | value = 0; 124 | for (let k = 0; k <= n; k++) { 125 | value += 126 | binomialCoefficients[n][k] * 127 | Math.pow(1 - t, n - k) * 128 | Math.pow(t, k) * 129 | vs[k]; 130 | } 131 | return value; 132 | } else { 133 | // Still some derivative? go down one order, then try 134 | // for the lower order curve's. 135 | _vs = new Array(n); 136 | for (let k = 0; k < n; k++) { 137 | _vs[k] = n * (vs[k + 1] - vs[k]); 138 | } 139 | return getDerivative(derivative - 1, t, _vs); 140 | } 141 | }; 142 | 143 | export const t2length = ( 144 | length: number, 145 | totalLength: number, 146 | func: (t: number) => number 147 | ): number => { 148 | let error = 1; 149 | let t = length / totalLength; 150 | let step = (length - func(t)) / totalLength; 151 | 152 | let numIterations = 0; 153 | while (error > 0.001) { 154 | const increasedTLength = func(t + step); 155 | const increasedTError = Math.abs(length - increasedTLength) / totalLength; 156 | if (increasedTError < error) { 157 | error = increasedTError; 158 | t += step; 159 | } else { 160 | const decreasedTLength = func(t - step); 161 | const decreasedTError = Math.abs(length - decreasedTLength) / totalLength; 162 | if (decreasedTError < error) { 163 | error = decreasedTError; 164 | t -= step; 165 | } else { 166 | step /= 2; 167 | } 168 | } 169 | 170 | numIterations++; 171 | if (numIterations > 500) { 172 | break; 173 | } 174 | } 175 | 176 | return t; 177 | }; 178 | -------------------------------------------------------------------------------- /src/bezier-values.ts: -------------------------------------------------------------------------------- 1 | // Legendre-Gauss abscissae (xi values, defined at i=n as the roots of the nth order Legendre polynomial Pn(x)) 2 | export const tValues = [ 3 | [], 4 | [], 5 | [ 6 | -0.5773502691896257645091487805019574556476, 7 | 0.5773502691896257645091487805019574556476 8 | ], 9 | [ 10 | 0, 11 | -0.7745966692414833770358530799564799221665, 12 | 0.7745966692414833770358530799564799221665 13 | ], 14 | [ 15 | -0.3399810435848562648026657591032446872005, 16 | 0.3399810435848562648026657591032446872005, 17 | -0.8611363115940525752239464888928095050957, 18 | 0.8611363115940525752239464888928095050957 19 | ], 20 | [ 21 | 0, 22 | -0.5384693101056830910363144207002088049672, 23 | 0.5384693101056830910363144207002088049672, 24 | -0.9061798459386639927976268782993929651256, 25 | 0.9061798459386639927976268782993929651256 26 | ], 27 | [ 28 | 0.6612093864662645136613995950199053470064, 29 | -0.6612093864662645136613995950199053470064, 30 | -0.2386191860831969086305017216807119354186, 31 | 0.2386191860831969086305017216807119354186, 32 | -0.9324695142031520278123015544939946091347, 33 | 0.9324695142031520278123015544939946091347 34 | ], 35 | [ 36 | 0, 37 | 0.4058451513773971669066064120769614633473, 38 | -0.4058451513773971669066064120769614633473, 39 | -0.7415311855993944398638647732807884070741, 40 | 0.7415311855993944398638647732807884070741, 41 | -0.9491079123427585245261896840478512624007, 42 | 0.9491079123427585245261896840478512624007 43 | ], 44 | [ 45 | -0.1834346424956498049394761423601839806667, 46 | 0.1834346424956498049394761423601839806667, 47 | -0.5255324099163289858177390491892463490419, 48 | 0.5255324099163289858177390491892463490419, 49 | -0.7966664774136267395915539364758304368371, 50 | 0.7966664774136267395915539364758304368371, 51 | -0.9602898564975362316835608685694729904282, 52 | 0.9602898564975362316835608685694729904282 53 | ], 54 | [ 55 | 0, 56 | -0.8360311073266357942994297880697348765441, 57 | 0.8360311073266357942994297880697348765441, 58 | -0.9681602395076260898355762029036728700494, 59 | 0.9681602395076260898355762029036728700494, 60 | -0.3242534234038089290385380146433366085719, 61 | 0.3242534234038089290385380146433366085719, 62 | -0.6133714327005903973087020393414741847857, 63 | 0.6133714327005903973087020393414741847857 64 | ], 65 | [ 66 | -0.1488743389816312108848260011297199846175, 67 | 0.1488743389816312108848260011297199846175, 68 | -0.4333953941292471907992659431657841622, 69 | 0.4333953941292471907992659431657841622, 70 | -0.6794095682990244062343273651148735757692, 71 | 0.6794095682990244062343273651148735757692, 72 | -0.8650633666889845107320966884234930485275, 73 | 0.8650633666889845107320966884234930485275, 74 | -0.9739065285171717200779640120844520534282, 75 | 0.9739065285171717200779640120844520534282 76 | ], 77 | [ 78 | 0, 79 | -0.2695431559523449723315319854008615246796, 80 | 0.2695431559523449723315319854008615246796, 81 | -0.5190961292068118159257256694586095544802, 82 | 0.5190961292068118159257256694586095544802, 83 | -0.7301520055740493240934162520311534580496, 84 | 0.7301520055740493240934162520311534580496, 85 | -0.8870625997680952990751577693039272666316, 86 | 0.8870625997680952990751577693039272666316, 87 | -0.9782286581460569928039380011228573907714, 88 | 0.9782286581460569928039380011228573907714 89 | ], 90 | [ 91 | -0.1252334085114689154724413694638531299833, 92 | 0.1252334085114689154724413694638531299833, 93 | -0.3678314989981801937526915366437175612563, 94 | 0.3678314989981801937526915366437175612563, 95 | -0.587317954286617447296702418940534280369, 96 | 0.587317954286617447296702418940534280369, 97 | -0.7699026741943046870368938332128180759849, 98 | 0.7699026741943046870368938332128180759849, 99 | -0.9041172563704748566784658661190961925375, 100 | 0.9041172563704748566784658661190961925375, 101 | -0.9815606342467192506905490901492808229601, 102 | 0.9815606342467192506905490901492808229601 103 | ], 104 | [ 105 | 0, 106 | -0.2304583159551347940655281210979888352115, 107 | 0.2304583159551347940655281210979888352115, 108 | -0.4484927510364468528779128521276398678019, 109 | 0.4484927510364468528779128521276398678019, 110 | -0.6423493394403402206439846069955156500716, 111 | 0.6423493394403402206439846069955156500716, 112 | -0.8015780907333099127942064895828598903056, 113 | 0.8015780907333099127942064895828598903056, 114 | -0.9175983992229779652065478365007195123904, 115 | 0.9175983992229779652065478365007195123904, 116 | -0.9841830547185881494728294488071096110649, 117 | 0.9841830547185881494728294488071096110649 118 | ], 119 | [ 120 | -0.1080549487073436620662446502198347476119, 121 | 0.1080549487073436620662446502198347476119, 122 | -0.3191123689278897604356718241684754668342, 123 | 0.3191123689278897604356718241684754668342, 124 | -0.5152486363581540919652907185511886623088, 125 | 0.5152486363581540919652907185511886623088, 126 | -0.6872929048116854701480198030193341375384, 127 | 0.6872929048116854701480198030193341375384, 128 | -0.8272013150697649931897947426503949610397, 129 | 0.8272013150697649931897947426503949610397, 130 | -0.928434883663573517336391139377874264477, 131 | 0.928434883663573517336391139377874264477, 132 | -0.986283808696812338841597266704052801676, 133 | 0.986283808696812338841597266704052801676 134 | ], 135 | [ 136 | 0, 137 | -0.2011940939974345223006283033945962078128, 138 | 0.2011940939974345223006283033945962078128, 139 | -0.3941513470775633698972073709810454683627, 140 | 0.3941513470775633698972073709810454683627, 141 | -0.5709721726085388475372267372539106412383, 142 | 0.5709721726085388475372267372539106412383, 143 | -0.7244177313601700474161860546139380096308, 144 | 0.7244177313601700474161860546139380096308, 145 | -0.8482065834104272162006483207742168513662, 146 | 0.8482065834104272162006483207742168513662, 147 | -0.9372733924007059043077589477102094712439, 148 | 0.9372733924007059043077589477102094712439, 149 | -0.9879925180204854284895657185866125811469, 150 | 0.9879925180204854284895657185866125811469 151 | ], 152 | [ 153 | -0.0950125098376374401853193354249580631303, 154 | 0.0950125098376374401853193354249580631303, 155 | -0.281603550779258913230460501460496106486, 156 | 0.281603550779258913230460501460496106486, 157 | -0.45801677765722738634241944298357757354, 158 | 0.45801677765722738634241944298357757354, 159 | -0.6178762444026437484466717640487910189918, 160 | 0.6178762444026437484466717640487910189918, 161 | -0.7554044083550030338951011948474422683538, 162 | 0.7554044083550030338951011948474422683538, 163 | -0.8656312023878317438804678977123931323873, 164 | 0.8656312023878317438804678977123931323873, 165 | -0.9445750230732325760779884155346083450911, 166 | 0.9445750230732325760779884155346083450911, 167 | -0.9894009349916499325961541734503326274262, 168 | 0.9894009349916499325961541734503326274262 169 | ], 170 | [ 171 | 0, 172 | -0.1784841814958478558506774936540655574754, 173 | 0.1784841814958478558506774936540655574754, 174 | -0.3512317634538763152971855170953460050405, 175 | 0.3512317634538763152971855170953460050405, 176 | -0.5126905370864769678862465686295518745829, 177 | 0.5126905370864769678862465686295518745829, 178 | -0.6576711592166907658503022166430023351478, 179 | 0.6576711592166907658503022166430023351478, 180 | -0.7815140038968014069252300555204760502239, 181 | 0.7815140038968014069252300555204760502239, 182 | -0.8802391537269859021229556944881556926234, 183 | 0.8802391537269859021229556944881556926234, 184 | -0.9506755217687677612227169578958030214433, 185 | 0.9506755217687677612227169578958030214433, 186 | -0.9905754753144173356754340199406652765077, 187 | 0.9905754753144173356754340199406652765077 188 | ], 189 | [ 190 | -0.0847750130417353012422618529357838117333, 191 | 0.0847750130417353012422618529357838117333, 192 | -0.2518862256915055095889728548779112301628, 193 | 0.2518862256915055095889728548779112301628, 194 | -0.4117511614628426460359317938330516370789, 195 | 0.4117511614628426460359317938330516370789, 196 | -0.5597708310739475346078715485253291369276, 197 | 0.5597708310739475346078715485253291369276, 198 | -0.6916870430603532078748910812888483894522, 199 | 0.6916870430603532078748910812888483894522, 200 | -0.8037049589725231156824174550145907971032, 201 | 0.8037049589725231156824174550145907971032, 202 | -0.8926024664975557392060605911271455154078, 203 | 0.8926024664975557392060605911271455154078, 204 | -0.9558239495713977551811958929297763099728, 205 | 0.9558239495713977551811958929297763099728, 206 | -0.9915651684209309467300160047061507702525, 207 | 0.9915651684209309467300160047061507702525 208 | ], 209 | [ 210 | 0, 211 | -0.1603586456402253758680961157407435495048, 212 | 0.1603586456402253758680961157407435495048, 213 | -0.3165640999636298319901173288498449178922, 214 | 0.3165640999636298319901173288498449178922, 215 | -0.4645707413759609457172671481041023679762, 216 | 0.4645707413759609457172671481041023679762, 217 | -0.6005453046616810234696381649462392798683, 218 | 0.6005453046616810234696381649462392798683, 219 | -0.7209661773352293786170958608237816296571, 220 | 0.7209661773352293786170958608237816296571, 221 | -0.8227146565371428249789224867127139017745, 222 | 0.8227146565371428249789224867127139017745, 223 | -0.9031559036148179016426609285323124878093, 224 | 0.9031559036148179016426609285323124878093, 225 | -0.960208152134830030852778840687651526615, 226 | 0.960208152134830030852778840687651526615, 227 | -0.9924068438435844031890176702532604935893, 228 | 0.9924068438435844031890176702532604935893 229 | ], 230 | [ 231 | -0.0765265211334973337546404093988382110047, 232 | 0.0765265211334973337546404093988382110047, 233 | -0.227785851141645078080496195368574624743, 234 | 0.227785851141645078080496195368574624743, 235 | -0.3737060887154195606725481770249272373957, 236 | 0.3737060887154195606725481770249272373957, 237 | -0.5108670019508270980043640509552509984254, 238 | 0.5108670019508270980043640509552509984254, 239 | -0.6360536807265150254528366962262859367433, 240 | 0.6360536807265150254528366962262859367433, 241 | -0.7463319064601507926143050703556415903107, 242 | 0.7463319064601507926143050703556415903107, 243 | -0.8391169718222188233945290617015206853296, 244 | 0.8391169718222188233945290617015206853296, 245 | -0.9122344282513259058677524412032981130491, 246 | 0.9122344282513259058677524412032981130491, 247 | -0.963971927277913791267666131197277221912, 248 | 0.963971927277913791267666131197277221912, 249 | -0.9931285991850949247861223884713202782226, 250 | 0.9931285991850949247861223884713202782226 251 | ], 252 | [ 253 | 0, 254 | -0.1455618541608950909370309823386863301163, 255 | 0.1455618541608950909370309823386863301163, 256 | -0.288021316802401096600792516064600319909, 257 | 0.288021316802401096600792516064600319909, 258 | -0.4243421202074387835736688885437880520964, 259 | 0.4243421202074387835736688885437880520964, 260 | -0.551618835887219807059018796724313286622, 261 | 0.551618835887219807059018796724313286622, 262 | -0.667138804197412319305966669990339162597, 263 | 0.667138804197412319305966669990339162597, 264 | -0.7684399634756779086158778513062280348209, 265 | 0.7684399634756779086158778513062280348209, 266 | -0.8533633645833172836472506385875676702761, 267 | 0.8533633645833172836472506385875676702761, 268 | -0.9200993341504008287901871337149688941591, 269 | 0.9200993341504008287901871337149688941591, 270 | -0.9672268385663062943166222149076951614246, 271 | 0.9672268385663062943166222149076951614246, 272 | -0.9937521706203895002602420359379409291933, 273 | 0.9937521706203895002602420359379409291933 274 | ], 275 | [ 276 | -0.0697392733197222212138417961186280818222, 277 | 0.0697392733197222212138417961186280818222, 278 | -0.2078604266882212854788465339195457342156, 279 | 0.2078604266882212854788465339195457342156, 280 | -0.3419358208920842251581474204273796195591, 281 | 0.3419358208920842251581474204273796195591, 282 | -0.4693558379867570264063307109664063460953, 283 | 0.4693558379867570264063307109664063460953, 284 | -0.5876404035069115929588769276386473488776, 285 | 0.5876404035069115929588769276386473488776, 286 | -0.6944872631866827800506898357622567712673, 287 | 0.6944872631866827800506898357622567712673, 288 | -0.7878168059792081620042779554083515213881, 289 | 0.7878168059792081620042779554083515213881, 290 | -0.8658125777203001365364256370193787290847, 291 | 0.8658125777203001365364256370193787290847, 292 | -0.9269567721871740005206929392590531966353, 293 | 0.9269567721871740005206929392590531966353, 294 | -0.9700604978354287271239509867652687108059, 295 | 0.9700604978354287271239509867652687108059, 296 | -0.994294585482399292073031421161298980393, 297 | 0.994294585482399292073031421161298980393 298 | ], 299 | [ 300 | 0, 301 | -0.1332568242984661109317426822417661370104, 302 | 0.1332568242984661109317426822417661370104, 303 | -0.264135680970344930533869538283309602979, 304 | 0.264135680970344930533869538283309602979, 305 | -0.390301038030290831421488872880605458578, 306 | 0.390301038030290831421488872880605458578, 307 | -0.5095014778460075496897930478668464305448, 308 | 0.5095014778460075496897930478668464305448, 309 | -0.6196098757636461563850973116495956533871, 310 | 0.6196098757636461563850973116495956533871, 311 | -0.7186613631319501944616244837486188483299, 312 | 0.7186613631319501944616244837486188483299, 313 | -0.8048884016188398921511184069967785579414, 314 | 0.8048884016188398921511184069967785579414, 315 | -0.8767523582704416673781568859341456716389, 316 | 0.8767523582704416673781568859341456716389, 317 | -0.9329710868260161023491969890384229782357, 318 | 0.9329710868260161023491969890384229782357, 319 | -0.9725424712181152319560240768207773751816, 320 | 0.9725424712181152319560240768207773751816, 321 | -0.9947693349975521235239257154455743605736, 322 | 0.9947693349975521235239257154455743605736 323 | ], 324 | [ 325 | -0.0640568928626056260850430826247450385909, 326 | 0.0640568928626056260850430826247450385909, 327 | -0.1911188674736163091586398207570696318404, 328 | 0.1911188674736163091586398207570696318404, 329 | -0.3150426796961633743867932913198102407864, 330 | 0.3150426796961633743867932913198102407864, 331 | -0.4337935076260451384870842319133497124524, 332 | 0.4337935076260451384870842319133497124524, 333 | -0.5454214713888395356583756172183723700107, 334 | 0.5454214713888395356583756172183723700107, 335 | -0.6480936519369755692524957869107476266696, 336 | 0.6480936519369755692524957869107476266696, 337 | -0.7401241915785543642438281030999784255232, 338 | 0.7401241915785543642438281030999784255232, 339 | -0.8200019859739029219539498726697452080761, 340 | 0.8200019859739029219539498726697452080761, 341 | -0.8864155270044010342131543419821967550873, 342 | 0.8864155270044010342131543419821967550873, 343 | -0.9382745520027327585236490017087214496548, 344 | 0.9382745520027327585236490017087214496548, 345 | -0.9747285559713094981983919930081690617411, 346 | 0.9747285559713094981983919930081690617411, 347 | -0.9951872199970213601799974097007368118745, 348 | 0.9951872199970213601799974097007368118745 349 | ] 350 | ]; 351 | 352 | // Legendre-Gauss weights (wi values, defined by a function linked to in the Bezier primer article) 353 | export const cValues = [ 354 | [], 355 | [], 356 | [1.0, 1.0], 357 | [ 358 | 0.8888888888888888888888888888888888888888, 359 | 0.5555555555555555555555555555555555555555, 360 | 0.5555555555555555555555555555555555555555 361 | ], 362 | [ 363 | 0.6521451548625461426269360507780005927646, 364 | 0.6521451548625461426269360507780005927646, 365 | 0.3478548451374538573730639492219994072353, 366 | 0.3478548451374538573730639492219994072353 367 | ], 368 | [ 369 | 0.5688888888888888888888888888888888888888, 370 | 0.4786286704993664680412915148356381929122, 371 | 0.4786286704993664680412915148356381929122, 372 | 0.2369268850561890875142640407199173626432, 373 | 0.2369268850561890875142640407199173626432 374 | ], 375 | [ 376 | 0.3607615730481386075698335138377161116615, 377 | 0.3607615730481386075698335138377161116615, 378 | 0.4679139345726910473898703439895509948116, 379 | 0.4679139345726910473898703439895509948116, 380 | 0.1713244923791703450402961421727328935268, 381 | 0.1713244923791703450402961421727328935268 382 | ], 383 | [ 384 | 0.4179591836734693877551020408163265306122, 385 | 0.3818300505051189449503697754889751338783, 386 | 0.3818300505051189449503697754889751338783, 387 | 0.2797053914892766679014677714237795824869, 388 | 0.2797053914892766679014677714237795824869, 389 | 0.1294849661688696932706114326790820183285, 390 | 0.1294849661688696932706114326790820183285 391 | ], 392 | [ 393 | 0.3626837833783619829651504492771956121941, 394 | 0.3626837833783619829651504492771956121941, 395 | 0.3137066458778872873379622019866013132603, 396 | 0.3137066458778872873379622019866013132603, 397 | 0.2223810344533744705443559944262408844301, 398 | 0.2223810344533744705443559944262408844301, 399 | 0.1012285362903762591525313543099621901153, 400 | 0.1012285362903762591525313543099621901153 401 | ], 402 | [ 403 | 0.3302393550012597631645250692869740488788, 404 | 0.1806481606948574040584720312429128095143, 405 | 0.1806481606948574040584720312429128095143, 406 | 0.0812743883615744119718921581105236506756, 407 | 0.0812743883615744119718921581105236506756, 408 | 0.3123470770400028400686304065844436655987, 409 | 0.3123470770400028400686304065844436655987, 410 | 0.2606106964029354623187428694186328497718, 411 | 0.2606106964029354623187428694186328497718 412 | ], 413 | [ 414 | 0.295524224714752870173892994651338329421, 415 | 0.295524224714752870173892994651338329421, 416 | 0.2692667193099963550912269215694693528597, 417 | 0.2692667193099963550912269215694693528597, 418 | 0.2190863625159820439955349342281631924587, 419 | 0.2190863625159820439955349342281631924587, 420 | 0.1494513491505805931457763396576973324025, 421 | 0.1494513491505805931457763396576973324025, 422 | 0.0666713443086881375935688098933317928578, 423 | 0.0666713443086881375935688098933317928578 424 | ], 425 | [ 426 | 0.272925086777900630714483528336342189156, 427 | 0.2628045445102466621806888698905091953727, 428 | 0.2628045445102466621806888698905091953727, 429 | 0.2331937645919904799185237048431751394317, 430 | 0.2331937645919904799185237048431751394317, 431 | 0.1862902109277342514260976414316558916912, 432 | 0.1862902109277342514260976414316558916912, 433 | 0.1255803694649046246346942992239401001976, 434 | 0.1255803694649046246346942992239401001976, 435 | 0.0556685671161736664827537204425485787285, 436 | 0.0556685671161736664827537204425485787285 437 | ], 438 | [ 439 | 0.2491470458134027850005624360429512108304, 440 | 0.2491470458134027850005624360429512108304, 441 | 0.2334925365383548087608498989248780562594, 442 | 0.2334925365383548087608498989248780562594, 443 | 0.2031674267230659217490644558097983765065, 444 | 0.2031674267230659217490644558097983765065, 445 | 0.160078328543346226334652529543359071872, 446 | 0.160078328543346226334652529543359071872, 447 | 0.1069393259953184309602547181939962242145, 448 | 0.1069393259953184309602547181939962242145, 449 | 0.047175336386511827194615961485017060317, 450 | 0.047175336386511827194615961485017060317 451 | ], 452 | [ 453 | 0.2325515532308739101945895152688359481566, 454 | 0.2262831802628972384120901860397766184347, 455 | 0.2262831802628972384120901860397766184347, 456 | 0.2078160475368885023125232193060527633865, 457 | 0.2078160475368885023125232193060527633865, 458 | 0.1781459807619457382800466919960979955128, 459 | 0.1781459807619457382800466919960979955128, 460 | 0.1388735102197872384636017768688714676218, 461 | 0.1388735102197872384636017768688714676218, 462 | 0.0921214998377284479144217759537971209236, 463 | 0.0921214998377284479144217759537971209236, 464 | 0.0404840047653158795200215922009860600419, 465 | 0.0404840047653158795200215922009860600419 466 | ], 467 | [ 468 | 0.2152638534631577901958764433162600352749, 469 | 0.2152638534631577901958764433162600352749, 470 | 0.2051984637212956039659240656612180557103, 471 | 0.2051984637212956039659240656612180557103, 472 | 0.1855383974779378137417165901251570362489, 473 | 0.1855383974779378137417165901251570362489, 474 | 0.1572031671581935345696019386238421566056, 475 | 0.1572031671581935345696019386238421566056, 476 | 0.1215185706879031846894148090724766259566, 477 | 0.1215185706879031846894148090724766259566, 478 | 0.0801580871597602098056332770628543095836, 479 | 0.0801580871597602098056332770628543095836, 480 | 0.0351194603317518630318328761381917806197, 481 | 0.0351194603317518630318328761381917806197 482 | ], 483 | [ 484 | 0.2025782419255612728806201999675193148386, 485 | 0.1984314853271115764561183264438393248186, 486 | 0.1984314853271115764561183264438393248186, 487 | 0.1861610000155622110268005618664228245062, 488 | 0.1861610000155622110268005618664228245062, 489 | 0.1662692058169939335532008604812088111309, 490 | 0.1662692058169939335532008604812088111309, 491 | 0.1395706779261543144478047945110283225208, 492 | 0.1395706779261543144478047945110283225208, 493 | 0.1071592204671719350118695466858693034155, 494 | 0.1071592204671719350118695466858693034155, 495 | 0.0703660474881081247092674164506673384667, 496 | 0.0703660474881081247092674164506673384667, 497 | 0.0307532419961172683546283935772044177217, 498 | 0.0307532419961172683546283935772044177217 499 | ], 500 | [ 501 | 0.1894506104550684962853967232082831051469, 502 | 0.1894506104550684962853967232082831051469, 503 | 0.1826034150449235888667636679692199393835, 504 | 0.1826034150449235888667636679692199393835, 505 | 0.1691565193950025381893120790303599622116, 506 | 0.1691565193950025381893120790303599622116, 507 | 0.1495959888165767320815017305474785489704, 508 | 0.1495959888165767320815017305474785489704, 509 | 0.1246289712555338720524762821920164201448, 510 | 0.1246289712555338720524762821920164201448, 511 | 0.0951585116824927848099251076022462263552, 512 | 0.0951585116824927848099251076022462263552, 513 | 0.0622535239386478928628438369943776942749, 514 | 0.0622535239386478928628438369943776942749, 515 | 0.0271524594117540948517805724560181035122, 516 | 0.0271524594117540948517805724560181035122 517 | ], 518 | [ 519 | 0.1794464703562065254582656442618856214487, 520 | 0.1765627053669926463252709901131972391509, 521 | 0.1765627053669926463252709901131972391509, 522 | 0.1680041021564500445099706637883231550211, 523 | 0.1680041021564500445099706637883231550211, 524 | 0.1540457610768102880814315948019586119404, 525 | 0.1540457610768102880814315948019586119404, 526 | 0.1351363684685254732863199817023501973721, 527 | 0.1351363684685254732863199817023501973721, 528 | 0.1118838471934039710947883856263559267358, 529 | 0.1118838471934039710947883856263559267358, 530 | 0.0850361483171791808835353701910620738504, 531 | 0.0850361483171791808835353701910620738504, 532 | 0.0554595293739872011294401653582446605128, 533 | 0.0554595293739872011294401653582446605128, 534 | 0.0241483028685479319601100262875653246916, 535 | 0.0241483028685479319601100262875653246916 536 | ], 537 | [ 538 | 0.1691423829631435918406564701349866103341, 539 | 0.1691423829631435918406564701349866103341, 540 | 0.1642764837458327229860537764659275904123, 541 | 0.1642764837458327229860537764659275904123, 542 | 0.1546846751262652449254180038363747721932, 543 | 0.1546846751262652449254180038363747721932, 544 | 0.1406429146706506512047313037519472280955, 545 | 0.1406429146706506512047313037519472280955, 546 | 0.1225552067114784601845191268002015552281, 547 | 0.1225552067114784601845191268002015552281, 548 | 0.1009420441062871655628139849248346070628, 549 | 0.1009420441062871655628139849248346070628, 550 | 0.0764257302548890565291296776166365256053, 551 | 0.0764257302548890565291296776166365256053, 552 | 0.0497145488949697964533349462026386416808, 553 | 0.0497145488949697964533349462026386416808, 554 | 0.0216160135264833103133427102664524693876, 555 | 0.0216160135264833103133427102664524693876 556 | ], 557 | [ 558 | 0.1610544498487836959791636253209167350399, 559 | 0.1589688433939543476499564394650472016787, 560 | 0.1589688433939543476499564394650472016787, 561 | 0.152766042065859666778855400897662998461, 562 | 0.152766042065859666778855400897662998461, 563 | 0.1426067021736066117757461094419029724756, 564 | 0.1426067021736066117757461094419029724756, 565 | 0.1287539625393362276755157848568771170558, 566 | 0.1287539625393362276755157848568771170558, 567 | 0.1115666455473339947160239016817659974813, 568 | 0.1115666455473339947160239016817659974813, 569 | 0.0914900216224499994644620941238396526609, 570 | 0.0914900216224499994644620941238396526609, 571 | 0.0690445427376412265807082580060130449618, 572 | 0.0690445427376412265807082580060130449618, 573 | 0.0448142267656996003328381574019942119517, 574 | 0.0448142267656996003328381574019942119517, 575 | 0.0194617882297264770363120414644384357529, 576 | 0.0194617882297264770363120414644384357529 577 | ], 578 | [ 579 | 0.1527533871307258506980843319550975934919, 580 | 0.1527533871307258506980843319550975934919, 581 | 0.1491729864726037467878287370019694366926, 582 | 0.1491729864726037467878287370019694366926, 583 | 0.1420961093183820513292983250671649330345, 584 | 0.1420961093183820513292983250671649330345, 585 | 0.1316886384491766268984944997481631349161, 586 | 0.1316886384491766268984944997481631349161, 587 | 0.118194531961518417312377377711382287005, 588 | 0.118194531961518417312377377711382287005, 589 | 0.1019301198172404350367501354803498761666, 590 | 0.1019301198172404350367501354803498761666, 591 | 0.0832767415767047487247581432220462061001, 592 | 0.0832767415767047487247581432220462061001, 593 | 0.0626720483341090635695065351870416063516, 594 | 0.0626720483341090635695065351870416063516, 595 | 0.040601429800386941331039952274932109879, 596 | 0.040601429800386941331039952274932109879, 597 | 0.0176140071391521183118619623518528163621, 598 | 0.0176140071391521183118619623518528163621 599 | ], 600 | [ 601 | 0.1460811336496904271919851476833711882448, 602 | 0.1445244039899700590638271665537525436099, 603 | 0.1445244039899700590638271665537525436099, 604 | 0.1398873947910731547221334238675831108927, 605 | 0.1398873947910731547221334238675831108927, 606 | 0.132268938633337461781052574496775604329, 607 | 0.132268938633337461781052574496775604329, 608 | 0.1218314160537285341953671771257335983563, 609 | 0.1218314160537285341953671771257335983563, 610 | 0.1087972991671483776634745780701056420336, 611 | 0.1087972991671483776634745780701056420336, 612 | 0.0934444234560338615532897411139320884835, 613 | 0.0934444234560338615532897411139320884835, 614 | 0.0761001136283793020170516533001831792261, 615 | 0.0761001136283793020170516533001831792261, 616 | 0.0571344254268572082836358264724479574912, 617 | 0.0571344254268572082836358264724479574912, 618 | 0.0369537897708524937999506682993296661889, 619 | 0.0369537897708524937999506682993296661889, 620 | 0.0160172282577743333242246168584710152658, 621 | 0.0160172282577743333242246168584710152658 622 | ], 623 | [ 624 | 0.1392518728556319933754102483418099578739, 625 | 0.1392518728556319933754102483418099578739, 626 | 0.1365414983460151713525738312315173965863, 627 | 0.1365414983460151713525738312315173965863, 628 | 0.1311735047870623707329649925303074458757, 629 | 0.1311735047870623707329649925303074458757, 630 | 0.1232523768105124242855609861548144719594, 631 | 0.1232523768105124242855609861548144719594, 632 | 0.1129322960805392183934006074217843191142, 633 | 0.1129322960805392183934006074217843191142, 634 | 0.1004141444428809649320788378305362823508, 635 | 0.1004141444428809649320788378305362823508, 636 | 0.0859416062170677274144436813727028661891, 637 | 0.0859416062170677274144436813727028661891, 638 | 0.0697964684245204880949614189302176573987, 639 | 0.0697964684245204880949614189302176573987, 640 | 0.0522933351526832859403120512732112561121, 641 | 0.0522933351526832859403120512732112561121, 642 | 0.0337749015848141547933022468659129013491, 643 | 0.0337749015848141547933022468659129013491, 644 | 0.0146279952982722006849910980471854451902, 645 | 0.0146279952982722006849910980471854451902 646 | ], 647 | [ 648 | 0.1336545721861061753514571105458443385831, 649 | 0.132462039404696617371642464703316925805, 650 | 0.132462039404696617371642464703316925805, 651 | 0.1289057221880821499785953393997936532597, 652 | 0.1289057221880821499785953393997936532597, 653 | 0.1230490843067295304675784006720096548158, 654 | 0.1230490843067295304675784006720096548158, 655 | 0.1149966402224113649416435129339613014914, 656 | 0.1149966402224113649416435129339613014914, 657 | 0.1048920914645414100740861850147438548584, 658 | 0.1048920914645414100740861850147438548584, 659 | 0.0929157660600351474770186173697646486034, 660 | 0.0929157660600351474770186173697646486034, 661 | 0.0792814117767189549228925247420432269137, 662 | 0.0792814117767189549228925247420432269137, 663 | 0.0642324214085258521271696151589109980391, 664 | 0.0642324214085258521271696151589109980391, 665 | 0.0480376717310846685716410716320339965612, 666 | 0.0480376717310846685716410716320339965612, 667 | 0.0309880058569794443106942196418845053837, 668 | 0.0309880058569794443106942196418845053837, 669 | 0.0134118594871417720813094934586150649766, 670 | 0.0134118594871417720813094934586150649766 671 | ], 672 | [ 673 | 0.1279381953467521569740561652246953718517, 674 | 0.1279381953467521569740561652246953718517, 675 | 0.1258374563468282961213753825111836887264, 676 | 0.1258374563468282961213753825111836887264, 677 | 0.121670472927803391204463153476262425607, 678 | 0.121670472927803391204463153476262425607, 679 | 0.1155056680537256013533444839067835598622, 680 | 0.1155056680537256013533444839067835598622, 681 | 0.1074442701159656347825773424466062227946, 682 | 0.1074442701159656347825773424466062227946, 683 | 0.0976186521041138882698806644642471544279, 684 | 0.0976186521041138882698806644642471544279, 685 | 0.086190161531953275917185202983742667185, 686 | 0.086190161531953275917185202983742667185, 687 | 0.0733464814110803057340336152531165181193, 688 | 0.0733464814110803057340336152531165181193, 689 | 0.0592985849154367807463677585001085845412, 690 | 0.0592985849154367807463677585001085845412, 691 | 0.0442774388174198061686027482113382288593, 692 | 0.0442774388174198061686027482113382288593, 693 | 0.0285313886289336631813078159518782864491, 694 | 0.0285313886289336631813078159518782864491, 695 | 0.0123412297999871995468056670700372915759, 696 | 0.0123412297999871995468056670700372915759 697 | ] 698 | ]; 699 | 700 | // LUT for binomial coefficient arrays per curve order 'n' 701 | export const binomialCoefficients = [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1]]; 702 | -------------------------------------------------------------------------------- /src/bezier.ts: -------------------------------------------------------------------------------- 1 | import { Properties, Point } from "./types"; 2 | 3 | import { 4 | cubicPoint, 5 | getCubicArcLength, 6 | cubicDerivative, 7 | getQuadraticArcLength, 8 | quadraticPoint, 9 | quadraticDerivative, 10 | t2length 11 | } from "./bezier-functions"; 12 | 13 | export class Bezier implements Properties { 14 | private a: Point; 15 | private b: Point; 16 | private c: Point; 17 | private d: Point; 18 | private length: number; 19 | private getArcLength: (xs: number[], ys: number[], t: number) => number; 20 | private getPoint: (xs: number[], ys: number[], t: number) => Point; 21 | private getDerivative: (xs: number[], ys: number[], t: number) => Point; 22 | constructor( 23 | ax: number, 24 | ay: number, 25 | bx: number, 26 | by: number, 27 | cx: number, 28 | cy: number, 29 | dx: number | undefined, 30 | dy: number | undefined 31 | ) { 32 | this.a = { x: ax, y: ay }; 33 | this.b = { x: bx, y: by }; 34 | this.c = { x: cx, y: cy }; 35 | 36 | if (dx !== undefined && dy !== undefined) { 37 | this.getArcLength = getCubicArcLength; 38 | this.getPoint = cubicPoint; 39 | this.getDerivative = cubicDerivative; 40 | this.d = { x: dx, y: dy }; 41 | } else { 42 | this.getArcLength = getQuadraticArcLength; 43 | this.getPoint = quadraticPoint; 44 | this.getDerivative = quadraticDerivative; 45 | this.d = { x: 0, y: 0 }; 46 | } 47 | this.length = this.getArcLength( 48 | [this.a.x, this.b.x, this.c.x, this.d.x], 49 | [this.a.y, this.b.y, this.c.y, this.d.y], 50 | 1 51 | ); 52 | } 53 | public getTotalLength = () => { 54 | return this.length; 55 | }; 56 | public getPointAtLength = (length: number) => { 57 | const xs = [this.a.x, this.b.x, this.c.x, this.d.x]; 58 | const xy = [this.a.y, this.b.y, this.c.y, this.d.y]; 59 | const t = t2length(length, this.length, i => this.getArcLength(xs, xy, i)); 60 | 61 | return this.getPoint(xs, xy, t); 62 | }; 63 | public getTangentAtLength = (length: number) => { 64 | const xs = [this.a.x, this.b.x, this.c.x, this.d.x]; 65 | const xy = [this.a.y, this.b.y, this.c.y, this.d.y]; 66 | const t = t2length(length, this.length, i => this.getArcLength(xs, xy, i)); 67 | 68 | const derivative = this.getDerivative(xs, xy, t); 69 | const mdl = Math.sqrt(derivative.x * derivative.x + derivative.y * derivative.y); 70 | let tangent: Point; 71 | if (mdl > 0) { 72 | tangent = { x: derivative.x / mdl, y: derivative.y / mdl }; 73 | } else { 74 | tangent = { x: 0, y: 0 }; 75 | } 76 | return tangent; 77 | }; 78 | public getPropertiesAtLength = (length: number) => { 79 | const xs = [this.a.x, this.b.x, this.c.x, this.d.x]; 80 | const xy = [this.a.y, this.b.y, this.c.y, this.d.y]; 81 | const t = t2length(length, this.length, i => this.getArcLength(xs, xy, i)); 82 | 83 | const derivative = this.getDerivative(xs, xy, t); 84 | const mdl = Math.sqrt(derivative.x * derivative.x + derivative.y * derivative.y); 85 | let tangent: Point; 86 | if (mdl > 0) { 87 | tangent = { x: derivative.x / mdl, y: derivative.y / mdl }; 88 | } else { 89 | tangent = { x: 0, y: 0 }; 90 | } 91 | const point = this.getPoint(xs, xy, t); 92 | return { x: point.x, y: point.y, tangentX: tangent.x, tangentY: tangent.y }; 93 | }; 94 | 95 | public getC = () => { 96 | return this.c; 97 | }; 98 | public getD = () => { 99 | return this.d; 100 | }; 101 | } 102 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import SVGPathProperties from "./svg-path-properties"; 2 | 3 | //https://stackoverflow.com/a/48362715/1086633 4 | class _svgPathProperties { 5 | inst: SVGPathProperties; 6 | 7 | constructor(svgPath: string) { 8 | this.inst = new SVGPathProperties(svgPath); 9 | if (!(this instanceof svgPathProperties)) { 10 | return new svgPathProperties(svgPath); 11 | } 12 | } 13 | 14 | public getTotalLength = () => this.inst.getTotalLength(); 15 | public getPointAtLength = (fractionLength: number) => 16 | this.inst.getPointAtLength(fractionLength); 17 | public getTangentAtLength = (fractionLength: number) => 18 | this.inst.getTangentAtLength(fractionLength); 19 | public getPropertiesAtLength = (fractionLength: number) => 20 | this.inst.getPropertiesAtLength(fractionLength); 21 | public getParts = () => this.inst.getParts(); 22 | } 23 | type svgPathProperties = _svgPathProperties; 24 | export const svgPathProperties = _svgPathProperties as typeof _svgPathProperties & 25 | ((val: string) => svgPathProperties); 26 | -------------------------------------------------------------------------------- /src/linear.ts: -------------------------------------------------------------------------------- 1 | import { Properties, Point, PointProperties } from "./types"; 2 | 3 | export class LinearPosition implements Properties { 4 | private x0: number; 5 | private x1: number; 6 | private y0: number; 7 | private y1: number; 8 | 9 | constructor(x0: number, x1: number, y0: number, y1: number) { 10 | this.x0 = x0; 11 | this.x1 = x1; 12 | this.y0 = y0; 13 | this.y1 = y1; 14 | } 15 | 16 | public getTotalLength = () => { 17 | return Math.sqrt( 18 | Math.pow(this.x0 - this.x1, 2) + Math.pow(this.y0 - this.y1, 2) 19 | ); 20 | }; 21 | 22 | public getPointAtLength = (pos: number): Point => { 23 | let fraction = 24 | pos / 25 | Math.sqrt( 26 | Math.pow(this.x0 - this.x1, 2) + Math.pow(this.y0 - this.y1, 2) 27 | ); 28 | 29 | fraction = Number.isNaN(fraction) ? 1 : fraction; 30 | const newDeltaX = (this.x1 - this.x0) * fraction; 31 | const newDeltaY = (this.y1 - this.y0) * fraction; 32 | 33 | return { x: this.x0 + newDeltaX, y: this.y0 + newDeltaY }; 34 | }; 35 | 36 | public getTangentAtLength = (_: number): Point => { 37 | const module = Math.sqrt( 38 | (this.x1 - this.x0) * (this.x1 - this.x0) + 39 | (this.y1 - this.y0) * (this.y1 - this.y0) 40 | ); 41 | return { 42 | x: (this.x1 - this.x0) / module, 43 | y: (this.y1 - this.y0) / module, 44 | }; 45 | }; 46 | 47 | public getPropertiesAtLength = (pos: number): PointProperties => { 48 | const point = this.getPointAtLength(pos); 49 | const tangent = this.getTangentAtLength(pos); 50 | return { x: point.x, y: point.y, tangentX: tangent.x, tangentY: tangent.y }; 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /src/parse.ts: -------------------------------------------------------------------------------- 1 | import { pathOrders } from "./types"; 2 | 3 | const length: { [key in pathOrders]: number } = { 4 | a: 7, 5 | c: 6, 6 | h: 1, 7 | l: 2, 8 | m: 2, 9 | q: 4, 10 | s: 4, 11 | t: 2, 12 | v: 1, 13 | z: 0 14 | }; 15 | const segmentRegExp = /([astvzqmhlc])([^astvzqmhlc]*)/gi; 16 | const numberRegExp = /-?[0-9]*\.?[0-9]+(?:e[-+]?\d+)?/gi; 17 | 18 | export default (path: string) => { 19 | const segments = (path && path.length > 0 ? path : "M0,0").match(segmentRegExp); 20 | if (!segments) { 21 | throw new Error(`No path elements found in string ${path}`); 22 | } 23 | return segments.reduce((segmentsArray: [string, ...Array][], segmentString: string) => { 24 | let command = segmentString.charAt(0); 25 | let type: pathOrders = command.toLowerCase() as pathOrders; 26 | let args = parseValues(segmentString.substring(1)); 27 | 28 | // overloaded moveTo 29 | if (type === "m" && args.length > 2) { 30 | segmentsArray.push([command, ...args.splice(0, 2)]); 31 | type = "l"; 32 | command = command === "m" ? "l" : "L"; 33 | } 34 | 35 | // overloaded arcTo 36 | if (type.toLowerCase() === "a" && (args.length === 5 || args.length === 6)) { 37 | const aArgs = segmentString.substring(1).trim().split(' '); 38 | args = [ 39 | Number(aArgs[0]), 40 | Number(aArgs[1]), 41 | Number(aArgs[2]), 42 | Number(aArgs[3].charAt(0)), 43 | Number(aArgs[3].charAt(1)), 44 | Number(aArgs[3].substring(2)), 45 | Number(aArgs[4]), 46 | ]; 47 | } 48 | 49 | while (args.length >= 0) { 50 | if (args.length === length[type]) { 51 | segmentsArray.push([command, ...args.splice(0, length[type])]); 52 | break; 53 | } 54 | if (args.length < length[type]) { 55 | throw new Error( 56 | `Malformed path data: "${command}" must have ${length[type]} elements and has ${args.length}: ${segmentString}` 57 | ); 58 | } 59 | segmentsArray.push([command, ...args.splice(0, length[type])]); 60 | } 61 | 62 | return segmentsArray; 63 | }, []); 64 | }; 65 | 66 | const parseValues = (args: string) => { 67 | const numbers = args.match(numberRegExp); 68 | return numbers ? numbers.map(Number) : []; 69 | }; 70 | -------------------------------------------------------------------------------- /src/svg-path-properties.ts: -------------------------------------------------------------------------------- 1 | import parse from "./parse"; 2 | import { PointArray, Properties, PartProperties, Point } from "./types"; 3 | import { LinearPosition } from "./linear"; 4 | import { Arc } from "./arc"; 5 | import { Bezier } from "./bezier"; 6 | 7 | export default class SVGPathProperties implements Properties { 8 | private length: number = 0; 9 | private partial_lengths: number[] = []; 10 | private functions: (null | Properties)[] = []; 11 | private initial_point: null | Point = null; 12 | constructor(source: string | [string, ...Array][]) { 13 | const parsed = Array.isArray(source) ? source : parse(source); 14 | let cur: PointArray = [0, 0]; 15 | let prev_point: PointArray = [0, 0]; 16 | let curve: Bezier | undefined; 17 | let ringStart: PointArray = [0, 0]; 18 | for (let i = 0; i < parsed.length; i++) { 19 | //moveTo 20 | if (parsed[i][0] === "M") { 21 | cur = [parsed[i][1], parsed[i][2]]; 22 | ringStart = [cur[0], cur[1]]; 23 | this.functions.push(null); 24 | if (i === 0) { 25 | this.initial_point = { x: parsed[i][1], y: parsed[i][2] }; 26 | } 27 | } else if (parsed[i][0] === "m") { 28 | cur = [parsed[i][1] + cur[0], parsed[i][2] + cur[1]]; 29 | ringStart = [cur[0], cur[1]]; 30 | this.functions.push(null); 31 | //lineTo 32 | } else if (parsed[i][0] === "L") { 33 | this.length += Math.sqrt( 34 | Math.pow(cur[0] - parsed[i][1], 2) + 35 | Math.pow(cur[1] - parsed[i][2], 2) 36 | ); 37 | this.functions.push( 38 | new LinearPosition(cur[0], parsed[i][1], cur[1], parsed[i][2]) 39 | ); 40 | cur = [parsed[i][1], parsed[i][2]]; 41 | } else if (parsed[i][0] === "l") { 42 | this.length += Math.sqrt( 43 | Math.pow(parsed[i][1], 2) + Math.pow(parsed[i][2], 2) 44 | ); 45 | this.functions.push( 46 | new LinearPosition( 47 | cur[0], 48 | parsed[i][1] + cur[0], 49 | cur[1], 50 | parsed[i][2] + cur[1] 51 | ) 52 | ); 53 | cur = [parsed[i][1] + cur[0], parsed[i][2] + cur[1]]; 54 | } else if (parsed[i][0] === "H") { 55 | this.length += Math.abs(cur[0] - parsed[i][1]); 56 | this.functions.push( 57 | new LinearPosition(cur[0], parsed[i][1], cur[1], cur[1]) 58 | ); 59 | cur[0] = parsed[i][1]; 60 | } else if (parsed[i][0] === "h") { 61 | this.length += Math.abs(parsed[i][1]); 62 | this.functions.push( 63 | new LinearPosition(cur[0], cur[0] + parsed[i][1], cur[1], cur[1]) 64 | ); 65 | cur[0] = parsed[i][1] + cur[0]; 66 | } else if (parsed[i][0] === "V") { 67 | this.length += Math.abs(cur[1] - parsed[i][1]); 68 | this.functions.push( 69 | new LinearPosition(cur[0], cur[0], cur[1], parsed[i][1]) 70 | ); 71 | cur[1] = parsed[i][1]; 72 | } else if (parsed[i][0] === "v") { 73 | this.length += Math.abs(parsed[i][1]); 74 | this.functions.push( 75 | new LinearPosition(cur[0], cur[0], cur[1], cur[1] + parsed[i][1]) 76 | ); 77 | cur[1] = parsed[i][1] + cur[1]; 78 | //Close path 79 | } else if (parsed[i][0] === "z" || parsed[i][0] === "Z") { 80 | this.length += Math.sqrt( 81 | Math.pow(ringStart[0] - cur[0], 2) + 82 | Math.pow(ringStart[1] - cur[1], 2) 83 | ); 84 | this.functions.push( 85 | new LinearPosition(cur[0], ringStart[0], cur[1], ringStart[1]) 86 | ); 87 | cur = [ringStart[0], ringStart[1]]; 88 | //Cubic Bezier curves 89 | } else if (parsed[i][0] === "C") { 90 | curve = new Bezier( 91 | cur[0], 92 | cur[1], 93 | parsed[i][1], 94 | parsed[i][2], 95 | parsed[i][3], 96 | parsed[i][4], 97 | parsed[i][5], 98 | parsed[i][6] 99 | ); 100 | this.length += curve.getTotalLength(); 101 | cur = [parsed[i][5], parsed[i][6]]; 102 | this.functions.push(curve); 103 | } else if (parsed[i][0] === "c") { 104 | curve = new Bezier( 105 | cur[0], 106 | cur[1], 107 | cur[0] + parsed[i][1], 108 | cur[1] + parsed[i][2], 109 | cur[0] + parsed[i][3], 110 | cur[1] + parsed[i][4], 111 | cur[0] + parsed[i][5], 112 | cur[1] + parsed[i][6] 113 | ); 114 | if (curve.getTotalLength() > 0) { 115 | this.length += curve.getTotalLength(); 116 | this.functions.push(curve); 117 | cur = [parsed[i][5] + cur[0], parsed[i][6] + cur[1]]; 118 | } else { 119 | this.functions.push( 120 | new LinearPosition(cur[0], cur[0], cur[1], cur[1]) 121 | ); 122 | } 123 | } else if (parsed[i][0] === "S") { 124 | if (i > 0 && ["C", "c", "S", "s"].indexOf(parsed[i - 1][0]) > -1) { 125 | if (curve) { 126 | const c = curve.getC(); 127 | curve = new Bezier( 128 | cur[0], 129 | cur[1], 130 | 2 * cur[0] - c.x, 131 | 2 * cur[1] - c.y, 132 | parsed[i][1], 133 | parsed[i][2], 134 | parsed[i][3], 135 | parsed[i][4] 136 | ); 137 | } 138 | } else { 139 | curve = new Bezier( 140 | cur[0], 141 | cur[1], 142 | cur[0], 143 | cur[1], 144 | parsed[i][1], 145 | parsed[i][2], 146 | parsed[i][3], 147 | parsed[i][4] 148 | ); 149 | } 150 | if (curve) { 151 | this.length += curve.getTotalLength(); 152 | cur = [parsed[i][3], parsed[i][4]]; 153 | this.functions.push(curve); 154 | } 155 | } else if (parsed[i][0] === "s") { 156 | //240 225 157 | if (i > 0 && ["C", "c", "S", "s"].indexOf(parsed[i - 1][0]) > -1) { 158 | if (curve) { 159 | const c = curve.getC(); 160 | const d = curve.getD(); 161 | curve = new Bezier( 162 | cur[0], 163 | cur[1], 164 | cur[0] + d.x - c.x, 165 | cur[1] + d.y - c.y, 166 | cur[0] + parsed[i][1], 167 | cur[1] + parsed[i][2], 168 | cur[0] + parsed[i][3], 169 | cur[1] + parsed[i][4] 170 | ); 171 | } 172 | } else { 173 | curve = new Bezier( 174 | cur[0], 175 | cur[1], 176 | cur[0], 177 | cur[1], 178 | cur[0] + parsed[i][1], 179 | cur[1] + parsed[i][2], 180 | cur[0] + parsed[i][3], 181 | cur[1] + parsed[i][4] 182 | ); 183 | } 184 | if (curve) { 185 | this.length += curve.getTotalLength(); 186 | cur = [parsed[i][3] + cur[0], parsed[i][4] + cur[1]]; 187 | this.functions.push(curve); 188 | } 189 | } 190 | //Quadratic Bezier curves 191 | else if (parsed[i][0] === "Q") { 192 | if (cur[0] == parsed[i][1] && cur[1] == parsed[i][2]) { 193 | let linearCurve = new LinearPosition( 194 | parsed[i][1], 195 | parsed[i][3], 196 | parsed[i][2], 197 | parsed[i][4] 198 | ); 199 | this.length += linearCurve.getTotalLength(); 200 | this.functions.push(linearCurve); 201 | } else { 202 | curve = new Bezier( 203 | cur[0], 204 | cur[1], 205 | parsed[i][1], 206 | parsed[i][2], 207 | parsed[i][3], 208 | parsed[i][4], 209 | undefined, 210 | undefined 211 | ); 212 | this.length += curve.getTotalLength(); 213 | this.functions.push(curve); 214 | } 215 | 216 | cur = [parsed[i][3], parsed[i][4]]; 217 | prev_point = [parsed[i][1], parsed[i][2]]; 218 | } else if (parsed[i][0] === "q") { 219 | if (!(parsed[i][1] == 0 && parsed[i][2] == 0)) { 220 | curve = new Bezier( 221 | cur[0], 222 | cur[1], 223 | cur[0] + parsed[i][1], 224 | cur[1] + parsed[i][2], 225 | cur[0] + parsed[i][3], 226 | cur[1] + parsed[i][4], 227 | undefined, 228 | undefined 229 | ); 230 | this.length += curve.getTotalLength(); 231 | this.functions.push(curve); 232 | } else { 233 | let linearCurve = new LinearPosition( 234 | cur[0] + parsed[i][1], 235 | cur[0] + parsed[i][3], 236 | cur[1] + parsed[i][2], 237 | cur[1] + parsed[i][4] 238 | ); 239 | this.length += linearCurve.getTotalLength(); 240 | this.functions.push(linearCurve); 241 | } 242 | 243 | prev_point = [cur[0] + parsed[i][1], cur[1] + parsed[i][2]]; 244 | cur = [parsed[i][3] + cur[0], parsed[i][4] + cur[1]]; 245 | } else if (parsed[i][0] === "T") { 246 | if (i > 0 && ["Q", "q", "T", "t"].indexOf(parsed[i - 1][0]) > -1) { 247 | curve = new Bezier( 248 | cur[0], 249 | cur[1], 250 | 2 * cur[0] - prev_point[0], 251 | 2 * cur[1] - prev_point[1], 252 | parsed[i][1], 253 | parsed[i][2], 254 | undefined, 255 | undefined 256 | ); 257 | this.functions.push(curve); 258 | this.length += curve.getTotalLength(); 259 | } else { 260 | let linearCurve = new LinearPosition( 261 | cur[0], 262 | parsed[i][1], 263 | cur[1], 264 | parsed[i][2] 265 | ); 266 | this.functions.push(linearCurve); 267 | this.length += linearCurve.getTotalLength(); 268 | } 269 | 270 | prev_point = [2 * cur[0] - prev_point[0], 2 * cur[1] - prev_point[1]]; 271 | cur = [parsed[i][1], parsed[i][2]]; 272 | } else if (parsed[i][0] === "t") { 273 | if (i > 0 && ["Q", "q", "T", "t"].indexOf(parsed[i - 1][0]) > -1) { 274 | curve = new Bezier( 275 | cur[0], 276 | cur[1], 277 | 2 * cur[0] - prev_point[0], 278 | 2 * cur[1] - prev_point[1], 279 | cur[0] + parsed[i][1], 280 | cur[1] + parsed[i][2], 281 | undefined, 282 | undefined 283 | ); 284 | this.length += curve.getTotalLength(); 285 | this.functions.push(curve); 286 | } else { 287 | let linearCurve = new LinearPosition( 288 | cur[0], 289 | cur[0] + parsed[i][1], 290 | cur[1], 291 | cur[1] + parsed[i][2] 292 | ); 293 | this.length += linearCurve.getTotalLength(); 294 | this.functions.push(linearCurve); 295 | } 296 | 297 | prev_point = [2 * cur[0] - prev_point[0], 2 * cur[1] - prev_point[1]]; 298 | cur = [parsed[i][1] + cur[0], parsed[i][2] + cur[1]]; 299 | } else if (parsed[i][0] === "A") { 300 | const arcCurve = new Arc( 301 | cur[0], 302 | cur[1], 303 | parsed[i][1], 304 | parsed[i][2], 305 | parsed[i][3], 306 | parsed[i][4] === 1, 307 | parsed[i][5] === 1, 308 | parsed[i][6], 309 | parsed[i][7] 310 | ); 311 | 312 | this.length += arcCurve.getTotalLength(); 313 | cur = [parsed[i][6], parsed[i][7]]; 314 | this.functions.push(arcCurve); 315 | } else if (parsed[i][0] === "a") { 316 | const arcCurve = new Arc( 317 | cur[0], 318 | cur[1], 319 | parsed[i][1], 320 | parsed[i][2], 321 | parsed[i][3], 322 | parsed[i][4] === 1, 323 | parsed[i][5] === 1, 324 | cur[0] + parsed[i][6], 325 | cur[1] + parsed[i][7] 326 | ); 327 | 328 | this.length += arcCurve.getTotalLength(); 329 | cur = [cur[0] + parsed[i][6], cur[1] + parsed[i][7]]; 330 | this.functions.push(arcCurve); 331 | } 332 | this.partial_lengths.push(this.length); 333 | } 334 | } 335 | 336 | private getPartAtLength = (fractionLength: number) => { 337 | if (fractionLength < 0) { 338 | fractionLength = 0; 339 | } else if (fractionLength > this.length) { 340 | fractionLength = this.length; 341 | } 342 | 343 | let i = this.partial_lengths.length - 1; 344 | 345 | while (this.partial_lengths[i] >= fractionLength && i > 0) { 346 | i--; 347 | } 348 | i++; 349 | return { fraction: fractionLength - this.partial_lengths[i - 1], i: i }; 350 | }; 351 | 352 | public getTotalLength = () => { 353 | return this.length; 354 | }; 355 | 356 | public getPointAtLength = (fractionLength: number) => { 357 | const fractionPart = this.getPartAtLength(fractionLength); 358 | const functionAtPart = this.functions[fractionPart.i]; 359 | 360 | if (functionAtPart) { 361 | return functionAtPart.getPointAtLength(fractionPart.fraction); 362 | } else if (this.initial_point) { 363 | return this.initial_point; 364 | } 365 | throw new Error("Wrong function at this part."); 366 | }; 367 | 368 | public getTangentAtLength = (fractionLength: number) => { 369 | const fractionPart = this.getPartAtLength(fractionLength); 370 | const functionAtPart = this.functions[fractionPart.i]; 371 | if (functionAtPart) { 372 | return functionAtPart.getTangentAtLength(fractionPart.fraction); 373 | } else if (this.initial_point) { 374 | return { x: 0, y: 0 }; 375 | } 376 | throw new Error("Wrong function at this part."); 377 | }; 378 | 379 | public getPropertiesAtLength = (fractionLength: number) => { 380 | const fractionPart = this.getPartAtLength(fractionLength); 381 | const functionAtPart = this.functions[fractionPart.i]; 382 | if (functionAtPart) { 383 | return functionAtPart.getPropertiesAtLength(fractionPart.fraction); 384 | } else if (this.initial_point) { 385 | return { 386 | x: this.initial_point.x, 387 | y: this.initial_point.y, 388 | tangentX: 0, 389 | tangentY: 0, 390 | }; 391 | } 392 | throw new Error("Wrong function at this part."); 393 | }; 394 | 395 | public getParts = () => { 396 | const parts: PartProperties[] = []; 397 | for (var i = 0; i < this.functions.length; i++) { 398 | if (this.functions[i] !== null) { 399 | this.functions[i] = this.functions[i] as Properties; 400 | const properties: PartProperties = { 401 | start: this.functions[i]!.getPointAtLength(0), 402 | end: this.functions[i]!.getPointAtLength( 403 | this.partial_lengths[i] - this.partial_lengths[i - 1] 404 | ), 405 | length: this.partial_lengths[i] - this.partial_lengths[i - 1], 406 | getPointAtLength: this.functions[i]!.getPointAtLength, 407 | getTangentAtLength: this.functions[i]!.getTangentAtLength, 408 | getPropertiesAtLength: this.functions[i]!.getPropertiesAtLength, 409 | }; 410 | parts.push(properties); 411 | } 412 | } 413 | 414 | return parts; 415 | }; 416 | } 417 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Properties { 2 | getTotalLength(): number; 3 | getPointAtLength(pos: number): Point; 4 | getTangentAtLength(pos: number): Point; 5 | getPropertiesAtLength(pos: number): PointProperties; 6 | } 7 | 8 | export interface PartProperties { 9 | start: Point; 10 | end: Point; 11 | length: number; 12 | getPointAtLength(pos: number): Point; 13 | getTangentAtLength(pos: number): Point; 14 | getPropertiesAtLength(pos: number): PointProperties; 15 | } 16 | export interface Point { 17 | x: number; 18 | y: number; 19 | } 20 | export type PointArray = [number, number]; 21 | 22 | export interface PointProperties { 23 | x: number; 24 | y: number; 25 | tangentX: number; 26 | tangentY: number; 27 | } 28 | 29 | export type pathOrders = 30 | | "a" 31 | | "c" 32 | | "h" 33 | | "l" 34 | | "m" 35 | | "q" 36 | | "s" 37 | | "t" 38 | | "v" 39 | | "z"; 40 | -------------------------------------------------------------------------------- /test/bezier-test.ts: -------------------------------------------------------------------------------- 1 | import test from "tape"; 2 | import { Bezier } from "../src/bezier"; 3 | import { inDelta } from "./inDelta"; 4 | 5 | test("Testing Length Quadratic", function (test) { 6 | var curve = new Bezier(200, 300, 400, 50, 600, 300, undefined, undefined); 7 | test.true(inDelta(curve.getTotalLength(), 487.77, 0.1)); 8 | test.end(); 9 | }); 10 | test("Testing Length Cubic", function (test) { 11 | var curve = new Bezier(200, 200, 275, 100, 575, 100, 500, 200); 12 | test.true(inDelta(curve.getTotalLength(), 383.44, 0.1)); 13 | test.end(); 14 | }); 15 | 16 | test("Testing getPointAtLength Quadratic", function (test) { 17 | var curve = new Bezier(200, 300, 400, 50, 600, 300, undefined, undefined); 18 | var point = curve.getPointAtLength(487.77 / 6); 19 | test.true(inDelta(point.x, 255.24, 1)); 20 | test.true(inDelta(point.y, 240.47, 1)); 21 | test.end(); 22 | }); 23 | 24 | test("Testing getPointAtLength Cubic", function (test) { 25 | var curve = new Bezier(200, 200, 275, 100, 575, 100, 500, 200); 26 | var point = curve.getPointAtLength(383.44 / 6); 27 | test.true(inDelta(point.x, 249.48, 1)); 28 | test.true(inDelta(point.y, 160.37, 1)); 29 | test.end(); 30 | }); 31 | 32 | test("Testing getTangentAtLength", function (test) { 33 | var curve = new Bezier(200, 200, 275, 100, 575, 100, 500, 200); 34 | 35 | test.end(); 36 | }); 37 | 38 | test("Testing pull request #16 solution", function (test) { 39 | var curve = new Bezier( 40 | 640.48, 41 | 1285.21, 42 | 642.39, 43 | 644.73, 44 | 642.39, 45 | 644.73, 46 | undefined, 47 | undefined 48 | ); 49 | var tangent = curve.getTangentAtLength(curve.getTotalLength() / 2); 50 | test.true(inDelta(tangent.y, 0, 1)); 51 | test.true(inDelta(tangent.x, 0, 1)); 52 | 53 | test.end(); 54 | }); 55 | 56 | test("Testing for infinite length (issue #61)", function (test) { 57 | var curve = new Bezier(267, 0, 391, 0, 512, 0, undefined, undefined); 58 | var length = curve.getTotalLength(); 59 | test.true(length != Infinity); 60 | 61 | test.end(); 62 | }); 63 | -------------------------------------------------------------------------------- /test/get-parts-test.ts: -------------------------------------------------------------------------------- 1 | import test from "tape"; 2 | import SVGPathProperties from "../src/svg-path-properties"; 3 | 4 | test("Testing the getParts with simple path", function (test) { 5 | var properties = new SVGPathProperties("m10,0l10,0"); 6 | var parts = properties.getParts(); 7 | 8 | test.equal(parts.length, 1, "A one part path must give one element array"); 9 | test.equal("start" in parts[0], true, "Elements must have all properties"); 10 | test.equal("end" in parts[0], true, "Elements must have all properties"); 11 | test.equal("length" in parts[0], true, "Elements must have all properties"); 12 | test.equal( 13 | "getPointAtLength" in parts[0], 14 | true, 15 | "Elements must have all properties" 16 | ); 17 | test.equal( 18 | "getTangentAtLength" in parts[0], 19 | true, 20 | "Elements must have all properties" 21 | ); 22 | test.equal( 23 | "getPropertiesAtLength" in parts[0], 24 | true, 25 | "Elements must have all properties" 26 | ); 27 | 28 | properties = new SVGPathProperties("m10,0l10,0l10,0"); 29 | parts = properties.getParts(); 30 | 31 | test.deepEqual(parts[0].start, { x: 10, y: 0 }, "testing start points"); 32 | test.deepEqual(parts[1].start, { x: 20, y: 0 }, "testing start points"); 33 | 34 | test.deepEqual(parts[0].end, { x: 20, y: 0 }, "testing end points"); 35 | test.deepEqual(parts[1].end, { x: 30, y: 0 }, "testing end points"); 36 | 37 | test.equal(parts[0].length, 10, "testing lengths"); 38 | test.equal(parts[1].length, 10, "testing lengths"); 39 | 40 | //Testing functions 41 | test.deepEqual( 42 | parts[0].getPointAtLength(5), 43 | { x: 15, y: 0 }, 44 | "testing getPointAtLength" 45 | ); 46 | test.deepEqual( 47 | parts[1].getPointAtLength(5), 48 | { x: 25, y: 0 }, 49 | "testing getPointAtLength" 50 | ); 51 | 52 | test.deepEqual( 53 | parts[0].getTangentAtLength(5), 54 | { x: 1, y: 0 }, 55 | "testing getTangentAtLength" 56 | ); 57 | test.deepEqual( 58 | parts[1].getTangentAtLength(5), 59 | { x: 1, y: 0 }, 60 | "testing getTangentAtLength" 61 | ); 62 | 63 | test.deepEqual( 64 | parts[0].getPropertiesAtLength(5), 65 | { tangentX: 1, tangentY: 0, x: 15, y: 0 }, 66 | "testing getPropertiesAtLength" 67 | ); 68 | test.deepEqual( 69 | parts[1].getPropertiesAtLength(5), 70 | { tangentX: 1, tangentY: 0, x: 25, y: 0 }, 71 | "testing getPropertiesAtLength" 72 | ); 73 | 74 | test.end(); 75 | }); 76 | 77 | test("Testing the getParts with simple path", function (test) { 78 | var properties = new SVGPathProperties( 79 | "M100,200 C100,100 250,100 250,200 S400,300 400,200" 80 | ); 81 | var parts = properties.getParts(); 82 | test.equal(parts.length, 2, "Correct number of parts"); 83 | test.deepEqual( 84 | parts[0].getPointAtLength(5), 85 | properties.getPointAtLength(5), 86 | "First part must have equal distances" 87 | ); 88 | test.deepEqual( 89 | parts[1].getPointAtLength(5), 90 | properties.getPointAtLength(parts[0]["length"] + 5), 91 | "Second part must have equal distances" 92 | ); 93 | test.end(); 94 | }); 95 | 96 | test("Issue 15", function (test) { 97 | var def = "M0,0 c 0.025,-0.052 0.081,-0.1387 0.2031,-0.2598 0,0 0,0 0,0"; 98 | var properties = new SVGPathProperties(def); 99 | properties.getParts(); //The above path used to hang the programd 100 | 101 | def = 102 | "M0,0 c 0.025,-0.052 0.081,-0.1387 0.2031,-0.2598 0,0 0,0 0,0 c 0.1865,-0.31055 0.3632,-0.71289 0.5371,-1.22266 0.1963,-0.40625 0.3261,-0.78516 0.3857,-1.13184 0,-0.008 0,-0.0156 0,-0.0225"; 103 | properties = new SVGPathProperties(def); 104 | properties.getParts(); 105 | 106 | test.end(); 107 | }); 108 | -------------------------------------------------------------------------------- /test/inDelta.ts: -------------------------------------------------------------------------------- 1 | export const inDelta = ( 2 | actual: number[] | number, 3 | expected: number[] | number, 4 | delta: number 5 | ) => { 6 | if (Array.isArray(actual) && Array.isArray(expected)) 7 | return inDeltaArray(actual, expected, delta); 8 | else if (!Array.isArray(actual) && !Array.isArray(expected)) 9 | return inDeltaNumber(actual, expected, delta); 10 | else throw new Error("Both elements should be either arrays or numbers"); 11 | }; 12 | 13 | const inDeltaArray = (actual: number[], expected: number[], delta: number) => { 14 | const n = expected.length; 15 | let i = -1; 16 | if (actual.length !== n) return false; 17 | while (++i < n) if (!inDelta(actual[i], expected[i], delta)) return false; 18 | return true; 19 | }; 20 | 21 | const inDeltaNumber = (actual: number, expected: number, delta: number) => { 22 | return actual >= expected - delta && actual <= expected + delta; 23 | }; 24 | -------------------------------------------------------------------------------- /test/index-test.ts: -------------------------------------------------------------------------------- 1 | import test from "tape"; 2 | import { svgPathProperties } from "../src/index"; 3 | 4 | test("Creation with different styles test", function (test) { 5 | const svgPath = "M0,100 q50,-150 100,0 t100,0"; 6 | const a = new svgPathProperties(svgPath); // MyClass 7 | const b = new svgPathProperties(svgPath); // also MyClass 8 | 9 | test.equal( 10 | a.getTotalLength(), 11 | b.getTotalLength(), 12 | "Both methods must return the same and work: getTotalLength" 13 | ); 14 | test.deepEqual( 15 | a.getPointAtLength(50), 16 | b.getPointAtLength(50), 17 | "Both methods must return the same and work: getPointAtLength" 18 | ); 19 | test.deepEqual( 20 | a.getTangentAtLength(50), 21 | b.getTangentAtLength(50), 22 | "Both methods must return the same and work: getTangentAtLength" 23 | ); 24 | test.deepEqual( 25 | a.getPropertiesAtLength(50), 26 | b.getPropertiesAtLength(50), 27 | "Both methods must return the same and work: getPropertiesAtLength" 28 | ); 29 | 30 | test.deepEqual(a.getParts()[0].start, b.getParts()[0].start); 31 | test.end(); 32 | }); 33 | -------------------------------------------------------------------------------- /test/length-test.ts: -------------------------------------------------------------------------------- 1 | import test from "tape"; 2 | import { LinearPosition } from "../src/linear"; 3 | import SVGPathProperties from "../src/svg-path-properties"; 4 | import { inDelta } from "./inDelta"; 5 | 6 | test("Testing LinearPosition directly", function (test) { 7 | let properties = new LinearPosition(0, 0, 10, 0); 8 | test.equal(properties.getTotalLength(), 10, "Length with m and l"); 9 | test.end(); 10 | }); 11 | 12 | test("Testing the lineTo", function (test) { 13 | let properties = new SVGPathProperties("m0,0l10,0"); 14 | 15 | test.equal(properties.getTotalLength(), 10, "Length with m and l"); 16 | 17 | properties = new SVGPathProperties("M0,0L10,0"); 18 | test.equal(properties.getTotalLength(), 10, "Length with M and L"); 19 | properties = new SVGPathProperties("M0,0L10,0M0,0L10,0"); 20 | test.equal(properties.getTotalLength(), 20, "Length with M and L"); 21 | properties = new SVGPathProperties("M0,0L10,0m0,0L10,0"); 22 | test.equal(properties.getTotalLength(), 10, "Length with m, M and L"); 23 | 24 | properties = new SVGPathProperties("M0,0L10,0l10,0"); 25 | test.equal(properties.getTotalLength(), 20, "Length with M, l and L"); 26 | test.end(); 27 | }); 28 | 29 | test("Testing the H and V", function (test) { 30 | let properties = new SVGPathProperties("m0,0h10"); 31 | test.equal(properties.getTotalLength(), 10, "Length with m and h"); 32 | 33 | properties = new SVGPathProperties("M50,0H40"); 34 | test.equal(properties.getTotalLength(), 10, "Length with m and H"); 35 | 36 | properties = new SVGPathProperties("m0,0v10"); 37 | test.equal(properties.getTotalLength(), 10, "Length with m and v"); 38 | 39 | properties = new SVGPathProperties("M0,50V40"); 40 | test.equal(properties.getTotalLength(), 10, "Length with m and V"); 41 | 42 | test.end(); 43 | }); 44 | 45 | test("Testing z and Z", function (test) { 46 | let properties = new SVGPathProperties("m0,0h10z"); 47 | test.equal(properties.getTotalLength(), 20, "Length with z"); 48 | 49 | properties = new SVGPathProperties("m0,0h10Z"); 50 | test.equal(properties.getTotalLength(), 20, "Length with Z"); 51 | test.end(); 52 | }); 53 | 54 | test("Testing Cubic Bézier", function (test) { 55 | //C & c 56 | let properties = new SVGPathProperties("M100,25C10,90,110,100,150,195"); 57 | test.true(inDelta(properties.getTotalLength(), 213.8, 0.1)); 58 | 59 | properties = new SVGPathProperties("m100,25c-90,65,10,75,50,170"); 60 | test.true(inDelta(properties.getTotalLength(), 213.8, 0.1)); 61 | 62 | //S & s 63 | properties = new SVGPathProperties( 64 | "M100,200 C100,100 250,100 250,200 S400,300 400,200" 65 | ); 66 | test.true(inDelta(properties.getTotalLength(), 475.746, 0.1)); 67 | 68 | properties = new SVGPathProperties( 69 | "M100,200 c0,-100 150,-100 150,0 s150,100 150,0" 70 | ); 71 | test.true(inDelta(properties.getTotalLength(), 475.746, 0.1)); 72 | 73 | //S & s without previous C or c 74 | properties = new SVGPathProperties("M100,200 S400,300 400,200"); 75 | test.true(inDelta(properties.getTotalLength(), 327.9618, 0.1)); 76 | 77 | properties = new SVGPathProperties("M100,200 s300,100 300,0"); 78 | test.true(inDelta(properties.getTotalLength(), 327.9618, 0.1)); 79 | 80 | test.end(); 81 | }); 82 | 83 | test("Testing Quadratic Bézier", function (test) { 84 | //Q & q 85 | let properties = new SVGPathProperties("M200,300 Q400,50 600,300"); 86 | test.true(inDelta(properties.getTotalLength(), 487.77, 0.1)); 87 | 88 | properties = new SVGPathProperties("M200,300 q200,-250 400,0"); 89 | test.true(inDelta(properties.getTotalLength(), 487.77, 0.1)); 90 | 91 | //T & t 92 | properties = new SVGPathProperties("M0,100 Q50,-50 100,100 T200,100"); 93 | test.true(inDelta(properties.getTotalLength(), 376.84, 0.1)); 94 | 95 | properties = new SVGPathProperties("M0,100 q50,-150 100,0 t100,0"); 96 | test.true(inDelta(properties.getTotalLength(), 376.84, 0.1)); 97 | 98 | properties = new SVGPathProperties( 99 | "M0,100 Q50,-50 100,100 T200,100 T300,100" 100 | ); 101 | test.true(inDelta(properties.getTotalLength(), 565.26, 0.1)); 102 | 103 | //T & t without previous values 104 | properties = new SVGPathProperties("M0,100 T200,100"); 105 | test.true(inDelta(properties.getTotalLength(), 200, 0.1)); 106 | 107 | properties = new SVGPathProperties("M0,100 t200,100"); 108 | test.true(inDelta(properties.getTotalLength(), 223.606, 0.1)); 109 | 110 | test.end(); 111 | }); 112 | 113 | test("Testing Arcs", function (test) { 114 | //A & a 115 | let properties = new SVGPathProperties("M50,20A50,50,0,0,0,150,20"); 116 | test.true(inDelta(properties.getTotalLength(), 157.1, 0.1)); 117 | 118 | properties = new SVGPathProperties("M50,20A50,50,0,0,0,150,20Z"); 119 | test.true(inDelta(properties.getTotalLength(), 257.1, 0.1)); 120 | 121 | properties = new SVGPathProperties("M50,20a50,50,0,0,0,100,0"); 122 | test.true(inDelta(properties.getTotalLength(), 157.1, 0.1)); 123 | test.end(); 124 | }); 125 | 126 | test("Some complex examples", function (test) { 127 | let properties = new SVGPathProperties( 128 | "M137.69692698614858,194.75002119995685L140.5811864522362,200.02784443179866L145.21300688556522,205.5730786360974L151.96589957664872,210.57916233863872L157.11811791245674,216.958427402148L160.38007797705498,217.5517159659712L170.86150068075614,226.50677931755828L184.78753673995035,229.40372164152683L188.48682846625186,231.74464203758626L194.96220985606624,232.24831761753774L199.0151340580992,235.98908347947008L200.33619274822317,239.1501414459547L208.1352797340722,240.97174662891314L214.55451361971706,243.72269753526453L217.92992784370034,242.79750552259512L222.422382828094,245.95312239185364L226.33834281296274,246.6562900586742L232.1785094475572,250.37579609444018L247.67126011118384,253.41216989328635L249.86860925383274,259.67235659237457L258.0102758151366,263.53584756964034L265.7094539012957,271.9301187141604L275.3442092382522,280.797134878233L292.5367640425162,281.439215857073L300.3900165167456,283.19277126134665L317.1541418598862,288.08140107614616L325.68746219694265,282.98731281377525L334.20900545032936,279.42687578910136L341.89090086141164,279.65662234387565L344.6975683081848,280.71420717321774L352.73368224017975,278.81635544720564L357.8378453664788,280.8621873013037L360.27780217558785,280.351713437805L366.10835670115375,282.6140677325477L369.09298803246423,282.32880268111796L376.79699044083907,278.5755589629451L382.0884404158815,278.74374570898004L386.6969703376813,280.7868194847831L391.5118882394122,287.6851129793625L401.6043570144851,289.4523241399227L418.32264375071753,303.60974325767233L416.56748832810626,308.8321991418072L421.85304030224415,309.8073672357337L426.9233662531078,306.30064325383734L428.39794675453993,303.9729502861741L433.7178516894217,301.12745610964237L435.55518815288303,303.2790040699963L429.98849506106274,310.0981677440247L430.3920258191735,315.904266873991L431.8697365975619,320.41310652120495L431.51963155330213,325.7229788905284L437.6672507546333,329.58621381302714L437.3918696288182,334.8637567665635L439.98603260092784,334.44629338092415L446.1764597142119,341.8547790472293L453.6668527230894,346.9381545890387L457.5294853076264,347.9669234517022L462.48118856871827,352.94569484976665L466.87142760911547,353.62325409732335L470.1647323309724,356.65500849656917L478.52329558789495,361.73028232300277L486.88560554821527,370.7823973990582L489.73056770534674,376.3046557640006L489.2413765676388,379.0217789927731L492.6796339000674,384.9123226146289L500.3373626256565,376.6596349946864L507.84942333888387,380.4063594074064L511.8061547036337,380.01502900094323" 129 | ); 130 | test.true(inDelta(properties.getTotalLength(), 498.031, 0.1)); 131 | properties = new SVGPathProperties( 132 | "M240,100C290,100,240,225,290,200S290,75,340,50S515,100,390,150S215,200,90,150S90,25,140,50S140,175,190,200S190,100,240,100" 133 | ); 134 | test.true(inDelta(properties.getTotalLength(), 1329.45, 0.1)); 135 | properties = new SVGPathProperties( 136 | "m240,100c50,0,0,125,50,100s0,-125,50,-150s175,50,50,100s-175,50,-300,0s0,-125,50,-100s0,125,50,150s0,-100,50,-100" 137 | ); 138 | test.true(inDelta(properties.getTotalLength(), 1329.45, 0.1)); 139 | properties = new SVGPathProperties("M100,100h100v100h-100Z m200,0h1v1h-1z"); 140 | test.true(inDelta(properties.getTotalLength(), 404, 0.1)); 141 | //https://github.com/rveciana/svg-path-properties/issues/12. getQuadraticArcLength zero division 142 | properties = new SVGPathProperties("M470,623Q468,627,467,629"); 143 | test.true(inDelta(properties.getTotalLength(), 6.71, 0.1)); 144 | properties = new SVGPathProperties( 145 | "M 408 666 Q 408 569 276 440 Q 270 435 270 429 Q 269 427 270 426 Q 271 426 274 426 Q 347 445 443 578 Q 456 598 466 609 Q 472 616 470 623 Q 468 627 467 629" 146 | ); 147 | test.true(inDelta(properties.getTotalLength(), 580, 0.1)); 148 | 149 | //https://github.com/rveciana/svg-path-properties/issues/14 150 | properties = new SVGPathProperties( 151 | "M0,0L31.081620209059235,726.1062992125984Q41.44216027874565,726.1062992125984,41.44216027874565,726.1062992125984" 152 | ); 153 | test.true(inDelta(properties.getTotalLength(), 737.13, 0.1)); 154 | 155 | //https://github.com/rveciana/svg-path-properties/issues/32 156 | properties = new SVGPathProperties("M270 830 Q270 830 270 830"); 157 | test.true(inDelta(properties.getTotalLength(), 0, 0.1)); 158 | 159 | test.end(); 160 | }); 161 | -------------------------------------------------------------------------------- /test/parse-test.ts: -------------------------------------------------------------------------------- 1 | import test from "tape"; 2 | import parse from "../src/parse"; 3 | 4 | test("overloaded moveTo", (test) => { 5 | test.deepEqual(parse("m 12.5,52 39,0 0,-40 -39,0 z"), [ 6 | ["m", 12.5, 52], 7 | ["l", 39, 0], 8 | ["l", 0, -40], 9 | ["l", -39, 0], 10 | ["z"], 11 | ]); 12 | 13 | test.deepEqual(parse("M 12.5,52 39,0 0,-40 -39,0 z"), [ 14 | ["M", 12.5, 52], 15 | ["L", 39, 0], 16 | ["L", 0, -40], 17 | ["L", -39, 0], 18 | ["z"], 19 | ]); 20 | 21 | test.deepEqual(parse("z"), [["z"]]); 22 | test.end(); 23 | }); 24 | 25 | test("curveTo", function (test) { 26 | const a = parse("c 50,0 50,100 100,100 50,0 50,-100 100,-100"); 27 | const b = parse("c 50,0 50,100 100,100 c 50,0 50,-100 100,-100"); 28 | 29 | test.deepEqual( 30 | a, 31 | [ 32 | ["c", 50, 0, 50, 100, 100, 100], 33 | ["c", 50, 0, 50, -100, 100, -100], 34 | ], 35 | "Correct parse c" 36 | ); 37 | test.deepEqual(a, b, "Testing repeated"); 38 | test.end(); 39 | }); 40 | 41 | test("lineTo, h, v, l", function (test) { 42 | test.throws(function () { 43 | parse("l 10 10 0"); 44 | }, /Malformed/); 45 | test.deepEqual(parse("l 10,10"), [["l", 10, 10]]); 46 | test.deepEqual(parse("L 10,10"), [["L", 10, 10]]); 47 | test.deepEqual(parse("l10 10 10 10"), [ 48 | ["l", 10, 10], 49 | ["l", 10, 10], 50 | ]); 51 | 52 | test.deepEqual(parse("h 10.5"), [["h", 10.5]]); 53 | test.deepEqual(parse("v 10.5"), [["v", 10.5]]); 54 | 55 | test.end(); 56 | }); 57 | 58 | test("arcTo, quadratic curveTo, smooth curveTo, smooth quadratic curveTo", function (test) { 59 | test.deepEqual(parse("A 30 50 0 0 1 162.55 162.45"), [ 60 | ["A", 30, 50, 0, 0, 1, 162.55, 162.45], 61 | ]); 62 | 63 | test.deepEqual(parse('a30 50 0 01162.55 162.45'), [ 64 | ['a', 30, 50, 0, 0, 1, 162.55, 162.45], 65 | ]); 66 | 67 | test.deepEqual(parse('a95500 95500 0 01-219.59 1128.84'), [ 68 | ['a', 95500, 95500, 0, 0, 1, -219.59, 1128.84], 69 | ]); 70 | 71 | test.deepEqual(parse("M10 80 Q 95 10 180 80"), [ 72 | ["M", 10, 80], 73 | ["Q", 95, 10, 180, 80], 74 | ]); 75 | test.deepEqual(parse("S 1 2, 3 4"), [["S", 1, 2, 3, 4]]); 76 | test.deepEqual(parse("T 1 -2e2"), [["T", 1, -2e2]]); 77 | 78 | test.throws(function () { 79 | parse("t 1 2 3"); 80 | }, Error); 81 | test.end(); 82 | }); 83 | 84 | test("Empty string", (test) => { 85 | test.deepEqual(parse(""), [["M", 0, 0]]); 86 | 87 | test.end(); 88 | }); 89 | -------------------------------------------------------------------------------- /test/point-at-test.ts: -------------------------------------------------------------------------------- 1 | import test from "tape"; 2 | import SVGPathProperties from "../src/svg-path-properties"; 3 | import { inDelta } from "./inDelta"; 4 | 5 | test("getPointAtLength testing lineTo", function (test) { 6 | const paths = [ 7 | { 8 | path: "M0,50L500,50", 9 | xValues: [0, 100, 200, 300, 400, 500], 10 | yValues: [50, 50, 50, 50, 50, 50], 11 | }, 12 | { 13 | path: "M0,50L300,300", 14 | xValues: [ 15 | 0, 16 | 59.999996185302734, 17 | 119.99999237060547, 18 | 180, 19 | 239.99998474121094, 20 | 300, 21 | ], 22 | yValues: [50, 100, 150, 200, 249.99998474121094, 300], 23 | }, 24 | { 25 | path: "M0,50H300", 26 | xValues: [0, 50, 100, 150, 200, 250, 300], 27 | yValues: [50, 50, 50, 50, 50, 50, 50], 28 | }, 29 | { 30 | path: "M50,50h300", 31 | xValues: [50, 100, 150, 200, 250, 300, 350], 32 | yValues: [50, 50, 50, 50, 50, 50, 50], 33 | }, 34 | { 35 | path: "M50,0V200", 36 | xValues: [50, 50, 50, 50, 50, 50, 50], 37 | yValues: [ 38 | 0, 39 | 33.33333206176758, 40 | 66.66666412353516, 41 | 100, 42 | 133.3333282470703, 43 | 166.6666717529297, 44 | 200, 45 | ], 46 | }, 47 | { 48 | path: "M50,10v200", 49 | xValues: [50, 50, 50, 50, 50, 50, 50], 50 | yValues: [ 51 | 10, 52 | 43.33333206176758, 53 | 76.66666412353516, 54 | 110, 55 | 143.3333282470703, 56 | 176.6666717529297, 57 | 210, 58 | ], 59 | }, 60 | { 61 | path: "M50,50H300V200H50Z", 62 | xValues: [50, 183.3333282470703, 300, 300, 166.66668701171875, 50, 50], 63 | yValues: [50, 50, 66.66665649414062, 200, 200, 183.33331298828125, 50], 64 | }, 65 | ]; 66 | let properties: SVGPathProperties; 67 | for (let i = 0; i < paths.length; i++) { 68 | properties = new SVGPathProperties(paths[i].path); 69 | for (let j = 0; j < paths[i].xValues.length; j++) { 70 | const position = properties.getPointAtLength( 71 | (j * properties.getTotalLength()) / (paths[i].xValues.length - 1) 72 | ); 73 | test.true(inDelta(position.x, paths[i].xValues[j], 0.1)); 74 | test.true(inDelta(position.y, paths[i].yValues[j], 0.1)); 75 | } 76 | test.deepEqual( 77 | properties.getPointAtLength(10000000), 78 | properties.getPointAtLength(properties.getTotalLength()) 79 | ); 80 | test.deepEqual( 81 | properties.getPointAtLength(-1), 82 | properties.getPointAtLength(0) 83 | ); 84 | } 85 | 86 | test.end(); 87 | }); 88 | 89 | test("getPointAtLength testing Quadratic Bézier", function (test) { 90 | const paths = [ 91 | { 92 | path: "M200,300 Q400,50 600,300", 93 | xValues: [ 94 | 200, 95 | 255.24655151367188, 96 | 321.72381591796875, 97 | 400.0000305175781, 98 | 478.2762756347656, 99 | 544.75341796875, 100 | 600, 101 | ], 102 | yValues: [ 103 | 300, 104 | 240.47999572753906, 105 | 194.14747619628906, 106 | 175.0000762939453, 107 | 194.1474609375, 108 | 240.47999572753906, 109 | 300, 110 | ], 111 | }, 112 | { 113 | path: "M0,100 Q50,-50 100,100 T200,100", 114 | xValues: [ 115 | 0, 116 | 25.60834312438965, 117 | 74.3916015625, 118 | 99.99996948242188, 119 | 125.60824584960938, 120 | 174.39163208007812, 121 | 200, 122 | ], 123 | yValues: [ 124 | 100, 125 | 42.84862518310547, 126 | 42.84857940673828, 127 | 99.99991607666016, 128 | 157.15122985839844, 129 | 157.15139770507812, 130 | 100, 131 | ], 132 | }, 133 | { 134 | path: "M0,100 q50,-150 100,0 t100,0", 135 | xValues: [ 136 | 0, 137 | 25.60834312438965, 138 | 74.3916015625, 139 | 99.99996948242188, 140 | 125.60824584960938, 141 | 174.39163208007812, 142 | 200, 143 | ], 144 | yValues: [ 145 | 100, 146 | 42.84862518310547, 147 | 42.84857940673828, 148 | 99.99991607666016, 149 | 157.15122985839844, 150 | 157.15139770507812, 151 | 100, 152 | ], 153 | }, 154 | { 155 | path: "M0,100 T200,100", 156 | xValues: [ 157 | 0, 158 | 33.33333206176758, 159 | 66.66666412353516, 160 | 100, 161 | 133.3333282470703, 162 | 166.6666717529297, 163 | 200, 164 | ], 165 | yValues: [100, 100, 100, 100, 100, 100, 100], 166 | }, 167 | { 168 | path: "M0,100 Q50,-50 100,100 T200,100 T300,100", 169 | xValues: [ 170 | 0, 171 | 50.00000762939453, 172 | 99.99998474121094, 173 | 149.9999542236328, 174 | 200.0000457763672, 175 | 250.00059509277344, 176 | 300, 177 | ], 178 | yValues: [ 179 | 100, 180 | 25.000080108642578, 181 | 99.99996185302734, 182 | 174.9999237060547, 183 | 99.99983978271484, 184 | 25.00008201599121, 185 | 100, 186 | ], 187 | }, 188 | ]; 189 | let properties: SVGPathProperties; 190 | for (let i = 0; i < paths.length; i++) { 191 | properties = new SVGPathProperties(paths[i].path); 192 | for (let j = 0; j < paths[i].xValues.length; j++) { 193 | const position = properties.getPointAtLength( 194 | (j * properties.getTotalLength()) / (paths[i].xValues.length - 1) 195 | ); 196 | test.true(inDelta(position.x, paths[i].xValues[j], 1)); 197 | test.true(inDelta(position.y, paths[i].yValues[j], 1)); 198 | } 199 | test.deepEqual( 200 | properties.getPointAtLength(10000000), 201 | properties.getPointAtLength(properties.getTotalLength()) 202 | ); 203 | test.deepEqual( 204 | properties.getPointAtLength(-1), 205 | properties.getPointAtLength(0) 206 | ); 207 | } 208 | test.end(); 209 | }); 210 | 211 | test("getPointAtLength testing Cubic Bézier", function (test) { 212 | const paths = [ 213 | { 214 | path: "M200,200 C275,100 575,100 500,200", 215 | xValues: [ 216 | 200, 217 | 249.48426818847656, 218 | 309.1169738769531, 219 | 371.97515869140625, 220 | 435.7851257324219, 221 | 496.41815185546875, 222 | 500.0001220703125, 223 | ], 224 | yValues: [ 225 | 200, 226 | 160.3770294189453, 227 | 137.765380859375, 228 | 126.64154052734375, 229 | 126.40363311767578, 230 | 144.5059051513672, 231 | 199.99981689453125, 232 | ], 233 | }, 234 | { 235 | path: "M100,200 C100,100 250,100 250,200 S400,300 400,200", 236 | xValues: [ 237 | 100, 238 | 136.8885955810547, 239 | 213.11134338378906, 240 | 250, 241 | 286.88836669921875, 242 | 363.11114501953125, 243 | 400, 244 | ], 245 | yValues: [ 246 | 200, 247 | 134.37181091308594, 248 | 134.3717498779297, 249 | 199.99984741210938, 250 | 265.6280517578125, 251 | 265.62835693359375, 252 | 200, 253 | ], 254 | }, 255 | { 256 | path: "M100,200 S400,300 400,200", 257 | xValues: [ 258 | 100, 259 | 152.38723754882812, 260 | 205.42906188964844, 261 | 259.1198425292969, 262 | 313.48455810546875, 263 | 367.6199951171875, 264 | 400, 265 | ], 266 | yValues: [ 267 | 200, 268 | 215.58023071289062, 269 | 228.76190185546875, 270 | 238.95660400390625, 271 | 244.3085174560547, 272 | 238.78338623046875, 273 | 200, 274 | ], 275 | }, 276 | { 277 | path: 278 | "M240,100C290,100,240,225,290,200S290,75,340,50S515,100,390,150S215,200,90,150S90,25,140,50S140,175,190,200S190,100,240,100", 279 | xValues: [ 280 | 240, 281 | 315.0015563964844, 282 | 441.4165954589844, 283 | 240.0000762939453, 284 | 38.58317947387695, 285 | 164.99853515625, 286 | 240, 287 | ], 288 | yValues: [ 289 | 100, 290 | 121.3836898803711, 291 | 111.11810302734375, 292 | 187.49990844726562, 293 | 111.11775207519531, 294 | 121.38365936279297, 295 | 100, 296 | ], 297 | }, 298 | { 299 | path: 300 | "m240,100c50,0,0,125,50,100s0,-125,50,-150s175,50,50,100s-175,50,-300,0s0,-125,50,-100s0,125,50,150s0,-100,50,-100", 301 | xValues: [ 302 | 240, 303 | 315.0015563964844, 304 | 441.4165954589844, 305 | 240.0000762939453, 306 | 38.58317947387695, 307 | 164.99853515625, 308 | 240, 309 | ], 310 | yValues: [ 311 | 100, 312 | 121.3836898803711, 313 | 111.11810302734375, 314 | 187.49990844726562, 315 | 111.11775207519531, 316 | 121.38365936279297, 317 | 100, 318 | ], 319 | }, 320 | ]; 321 | let properties: SVGPathProperties; 322 | for (let i = 0; i < paths.length; i++) { 323 | properties = new SVGPathProperties(paths[i].path); 324 | for (let j = 0; j < paths[i].xValues.length; j++) { 325 | const position = properties.getPointAtLength( 326 | (j * properties.getTotalLength()) / (paths[i].xValues.length - 1) 327 | ); 328 | test.true(inDelta(position.x, paths[i].xValues[j], 1)); 329 | test.true(inDelta(position.y, paths[i].yValues[j], 1)); 330 | } 331 | test.deepEqual( 332 | properties.getPointAtLength(10000000), 333 | properties.getPointAtLength(properties.getTotalLength()) 334 | ); 335 | test.deepEqual( 336 | properties.getPointAtLength(-1), 337 | properties.getPointAtLength(0) 338 | ); 339 | } 340 | test.end(); 341 | }); 342 | 343 | test("getPointAtLength bug testing", function (test) { 344 | //Testing https://github.com/rveciana/svg-path-properties/issues/1 345 | const properties = new SVGPathProperties( 346 | "M 211.6687111164928,312.6478542077994 C 211.6687111164928,312.6478542077994 211.6687111164928,312.6478542077994 219,293" 347 | ); 348 | const pos1 = properties.getPointAtLength(12); 349 | const pos2 = properties.getPointAtLength(11.95); 350 | const pos3 = properties.getPointAtLength(12.05); 351 | test.true(inDelta(pos1.x, pos2.x, 0.1)); 352 | test.true(inDelta(pos1.x, pos3.x, 0.1)); 353 | test.true(inDelta(pos1.y, pos2.y, 0.1)); 354 | test.true(inDelta(pos1.y, pos3.y, 0.1)); 355 | 356 | test.end(); 357 | }); 358 | 359 | test("Testing getPointAtLength with straigh line bezier curve (bug)", function (test) { 360 | //https://github.com/rveciana/svg-path-properties/issues/4 361 | const pathData = new SVGPathProperties("M500,300Q425,325 350,350"); 362 | const pathLen = pathData.getTotalLength(); 363 | test.true(inDelta(pathLen, 158.11, 0.1)); //Gave undefined 364 | 365 | let pos = pathData.getPointAtLength(0); 366 | test.true(inDelta(pos.x, 500, 0.00001)); 367 | test.true(inDelta(pos.y, 300, 0.00001)); 368 | pos = pathData.getPointAtLength(pathLen); 369 | test.true(inDelta(pos.x, 350, 0.00001)); 370 | test.true(inDelta(pos.y, 350, 0.00001)); 371 | 372 | test.end(); 373 | }); 374 | 375 | test("Testing with multiple rings", function (test) { 376 | let properties = new SVGPathProperties( 377 | "M100,100h100v100h-100Z m200,0h1v1h-1z" 378 | ); 379 | test.deepEqual(properties.getPointAtLength(0), { x: 100, y: 100 }); 380 | test.deepEqual(properties.getPointAtLength(401), { x: 301, y: 100 }); 381 | 382 | properties = new SVGPathProperties("M100,100L200,100 M300,100L400,100"); 383 | test.deepEqual(properties.getPointAtLength(0), { x: 100, y: 100 }); 384 | test.deepEqual(properties.getPointAtLength(100), { x: 200, y: 100 }); 385 | test.deepEqual(properties.getPointAtLength(200), { x: 400, y: 100 }); 386 | test.deepEqual( 387 | properties.getPointAtLength(200), 388 | properties.getPointAtLength(500) 389 | ); 390 | properties = new SVGPathProperties( 391 | "M100,100 L101,100 M200,0 M500,600 M0,0L1,0L1,1L0,1Z" 392 | ); 393 | test.deepEqual(properties.getPointAtLength(0), { x: 100, y: 100 }); 394 | test.deepEqual(properties.getPointAtLength(1), { x: 101, y: 100 }); 395 | test.deepEqual(properties.getPointAtLength(2), { x: 1, y: 0 }); 396 | test.end(); 397 | }); 398 | 399 | test("TestingDegenerated quadratic curves, issue 9", function (test) { 400 | let properties = new SVGPathProperties("M60,20Q60,20 150,20"); 401 | test.deepEqual(properties.getPointAtLength(2), { x: 62, y: 20 }); 402 | test.equal(properties.getTotalLength(), 90); 403 | properties = new SVGPathProperties("M60,20q0,0 90,0"); 404 | test.deepEqual(properties.getPointAtLength(2), { x: 62, y: 20 }); 405 | test.equal(properties.getTotalLength(), 90); 406 | test.end(); 407 | }); 408 | 409 | test("Check null path, issue 35", function (test) { 410 | let properties = new SVGPathProperties("M0, 0"); 411 | test.deepEqual(properties.getPointAtLength(0), { x: 0, y: 0 }); 412 | test.deepEqual(properties.getTangentAtLength(0), { x: 0, y: 0 }); 413 | test.deepEqual(properties.getPropertiesAtLength(0), { 414 | x: 0, 415 | y: 0, 416 | tangentX: 0, 417 | tangentY: 0, 418 | }); 419 | 420 | properties = new SVGPathProperties(""); 421 | test.deepEqual(properties.getPointAtLength(0), { x: 0, y: 0 }); 422 | test.deepEqual(properties.getTangentAtLength(0), { x: 0, y: 0 }); 423 | test.deepEqual(properties.getPropertiesAtLength(0), { 424 | x: 0, 425 | y: 0, 426 | tangentX: 0, 427 | tangentY: 0, 428 | }); 429 | properties = new SVGPathProperties("M10, 10"); 430 | test.deepEqual(properties.getPointAtLength(0), { x: 10, y: 10 }); 431 | test.deepEqual(properties.getTangentAtLength(0), { x: 0, y: 0 }); 432 | test.deepEqual(properties.getPropertiesAtLength(0), { 433 | x: 10, 434 | y: 10, 435 | tangentX: 0, 436 | tangentY: 0, 437 | }); 438 | 439 | test.end(); 440 | }); 441 | test("TestingDegenerated quadratic curves, issue 43", function (test) { 442 | let properties = new SVGPathProperties( 443 | "M224,32C153,195,69,366,76,544C77,567,97,585,105,606C133,683,137,768,175,840C193,875,225,902,250,932" 444 | ); 445 | test.deepEqual(properties.getPointAtLength(300), { 446 | x: 111.7377391058728, 447 | y: 310.0179550672576, 448 | }); 449 | 450 | test.deepEqual(properties.getTangentAtLength(300), { 451 | x: -0.29770950875462776, 452 | y: 0.9546565080682572, 453 | }); 454 | 455 | test.deepEqual(properties.getPropertiesAtLength(300), { 456 | x: 111.7377391058728, 457 | y: 310.0179550672576, 458 | tangentX: -0.29770950875462776, 459 | tangentY: 0.9546565080682572, 460 | }); 461 | 462 | test.end(); 463 | }); 464 | 465 | //https://github.com/rveciana/svg-path-properties/pull/44/files 466 | test("Testing first point of zero length fraction", function (test) { 467 | let properties = new SVGPathProperties("M 0,0 l 0 0 l 10 10"); 468 | test.deepEqual(properties.getPointAtLength(0), { x: 0, y: 0 }); 469 | 470 | properties = new SVGPathProperties("M 1,1 L 1 1"); 471 | test.deepEqual(properties.getPointAtLength(0), { x: 1, y: 1 }); 472 | 473 | properties = new SVGPathProperties("M 1,1 l 0 0"); 474 | test.deepEqual(properties.getPointAtLength(0), { x: 1, y: 1 }); 475 | 476 | test.end(); 477 | }); 478 | -------------------------------------------------------------------------------- /test/tangent-test.ts: -------------------------------------------------------------------------------- 1 | import test from "tape"; 2 | import SVGPathProperties from "../src/svg-path-properties"; 3 | import { inDelta } from "./inDelta"; 4 | 5 | test("getTangentAtLength testing", function (test) { 6 | const paths = [ 7 | { 8 | path: "M0,50L500,50", 9 | xValues: [1, 1, 1, 1, 1, 1, 1], 10 | yValues: [0, 0, 0, 0, 0, 0, 0], 11 | }, 12 | { 13 | path: "M0,50L300,300", 14 | xValues: [ 15 | 0.7682212795973759, 16 | 0.7682212795973759, 17 | 0.7682212795973759, 18 | 0.7682212795973759, 19 | 0.7682212795973759, 20 | 0.7682212795973759, 21 | 0.7682212795973759, 22 | ], 23 | yValues: [ 24 | 0.6401843996644798, 25 | 0.6401843996644798, 26 | 0.6401843996644798, 27 | 0.6401843996644798, 28 | 0.6401843996644798, 29 | 0.6401843996644798, 30 | 0.6401843996644798, 31 | ], 32 | }, 33 | { 34 | path: "M0,50H300", 35 | xValues: [1, 1, 1, 1, 1, 1, 1], 36 | yValues: [0, 0, 0, 0, 0, 0, 0], 37 | }, 38 | { 39 | path: "M50,50h300", 40 | xValues: [1, 1, 1, 1, 1, 1, 1], 41 | yValues: [0, 0, 0, 0, 0, 0, 0], 42 | }, 43 | { 44 | path: "M50,0V200", 45 | xValues: [0, 0, 0, 0, 0, 0, 0], 46 | yValues: [1, 1, 1, 1, 1, 1, 1], 47 | }, 48 | { 49 | path: "M50,10v200", 50 | xValues: [0, 0, 0, 0, 0, 0, 0], 51 | yValues: [1, 1, 1, 1, 1, 1, 1], 52 | }, 53 | { 54 | path: "M50,50H300V200H50Z", 55 | xValues: [1, 1, 0, 0, -1, 0, 0], 56 | yValues: [0, 0, 1, 1, 0, -1, -1], 57 | }, 58 | { 59 | path: "M200,300 Q400,50 600,300", 60 | xValues: [ 61 | 0.6246950475544243, 62 | 0.7418002077808687, 63 | 0.8984910301093634, 64 | 0.9999999999999247, 65 | 0.8984912864152661, 66 | 0.7418004257503856, 67 | 0.6246951852934535, 68 | ], 69 | yValues: [ 70 | -0.7808688094430304, 71 | -0.6706209448982786, 72 | -0.43899187784401555, 73 | -3.8801383766437346e-7, 74 | 0.4389913532586264, 75 | 0.6706207037935429, 76 | 0.7808686992517869, 77 | ], 78 | }, 79 | { 80 | path: "M0,100 Q50,-50 100,100 T200,100", 81 | xValues: [ 82 | 0.3162277660168379, 83 | 0.564254701134953, 84 | 0.5642557779681502, 85 | 0.3162278830087659, 86 | 0.5642536243071706, 87 | 0.5642568548067621, 88 | 0.3162280000014775, 89 | ], 90 | yValues: [ 91 | -0.9486832980505138, 92 | -0.8256007704981294, 93 | 0.8256000345382487, 94 | 0.9486832590531965, 95 | 0.8256015064522486, 96 | -0.8255992985726069, 97 | -0.948683220055602, 98 | ], 99 | }, 100 | { 101 | path: "M0,100 q50,-150 100,0 t100,0", 102 | xValues: [ 103 | 0.3162277660168379, 104 | 0.564254701134953, 105 | 0.5642557779681502, 106 | 0.3162278830087659, 107 | 0.5642536243071706, 108 | 0.5642568548067621, 109 | 0.3162280000014775, 110 | ], 111 | yValues: [ 112 | -0.9486832980505138, 113 | -0.8256007704981294, 114 | 0.8256000345382487, 115 | 0.9486832590531965, 116 | 0.8256015064522486, 117 | -0.8255992985726069, 118 | -0.948683220055602, 119 | ], 120 | }, 121 | { 122 | path: "M0,100 T200,100", 123 | xValues: [1, 1, 1, 1, 1, 1, 1], 124 | yValues: [0, 0, 0, 0, 0, 0, 0], 125 | }, 126 | { 127 | path: "M0,100 Q50,-50 100,100 T200,100 T300,100", 128 | xValues: [ 129 | 0.3162277660168379, 130 | 0.9999999999982623, 131 | 0.3162278434645575, 132 | 0.9999999999843601, 133 | 0.3162279209126205, 134 | 0.999999999956556, 135 | 0.31622799836102694, 136 | ], 137 | yValues: [ 138 | -0.9486832980505138, 139 | -0.000001864275757835856, 140 | 0.9486832722346038, 141 | 0.000005592827273145598, 142 | -0.9486832464185723, 143 | -0.000009321378787369434, 144 | 0.948683220602419, 145 | ], 146 | }, 147 | { 148 | path: "M200,200 C275,100 575,100 500,200", 149 | xValues: [ 150 | 0.6, 151 | 0.8855412510377398, 152 | 0.9662834030858953, 153 | 0.9955306269200779, 154 | 0.9950635163752912, 155 | 0.8102625798004855, 156 | -0.5999966647640088, 157 | ], 158 | yValues: [ 159 | -0.8, 160 | -0.4645607524431165, 161 | -0.25748084379374897, 162 | -0.09443924430085592, 163 | 0.09924010468979105, 164 | 0.5860670198663817, 165 | 0.8000025014161303, 166 | ], 167 | }, 168 | { 169 | path: "M100,200 C100,100 250,100 250,200 S400,300 400,200", 170 | xValues: [ 171 | 0, 172 | 0.8804617844502954, 173 | 0.8804628147711906, 174 | 0.0000014914995832878143, 175 | 0.8804607541243551, 176 | 0.8804638450870454, 177 | 0.0000029830040755489834, 178 | ], 179 | yValues: [ 180 | -1, 181 | -0.47411712278993, 182 | 0.47411520942192104, 183 | 0.9999999999988878, 184 | 0.47411903615734735, 185 | -0.47411329605331204, 186 | -0.9999999999955509, 187 | ], 188 | }, 189 | { 190 | path: "M100,200 S400,300 400,200", 191 | xValues: [ 192 | 0, 193 | 0.9647425496827284, 194 | 0.9760648482761272, 195 | 0.9886843239589528, 196 | 0.9995932816004552, 197 | 0.953018740113177, 198 | 2.2070215559197298e-7, 199 | ], 200 | yValues: [ 201 | 0, 202 | 0.26319538907752243, 203 | 0.21747968171693818, 204 | 0.15001102478760836, 205 | 0.028517913304325987, 206 | -0.30291134180332835, 207 | -0.9999999999999756, 208 | ], 209 | }, 210 | { 211 | path: 212 | "M240,100C290,100,240,225,290,200S290,75,340,50S515,100,390,150S215,200,90,150S90,25,140,50S140,175,190,200S190,100,240,100", 213 | xValues: [ 214 | 1, 215 | 0.0011574896425055297, 216 | -0.3871202612806902, 217 | -0.999999999999897, 218 | -0.3871144077881202, 219 | 0.001157352564866387, 220 | 1, 221 | ], 222 | yValues: [ 223 | 0, 224 | -0.9999993301086394, 225 | 0.9220292312643728, 226 | -4.5391975842908476e-7, 227 | -0.9220316888712953, 228 | 0.999999330267296, 229 | -2.2204460492503162e-15, 230 | ], 231 | }, 232 | { 233 | path: 234 | "m240,100c50,0,0,125,50,100s0,-125,50,-150s175,50,50,100s-175,50,-300,0s0,-125,50,-100s0,125,50,150s0,-100,50,-100", 235 | xValues: [ 236 | 1, 237 | 0.0011574896425055297, 238 | -0.3871202612806902, 239 | -0.999999999999897, 240 | -0.3871144077881202, 241 | 0.001157352564866387, 242 | 1, 243 | ], 244 | yValues: [ 245 | 0, 246 | -0.9999993301086394, 247 | 0.9220292312643728, 248 | -4.5391975842908476e-7, 249 | -0.9220316888712953, 250 | 0.999999330267296, 251 | -2.2204460492503162e-15, 252 | ], 253 | }, 254 | ]; 255 | 256 | for (let i = 0; i < paths.length; i++) { 257 | let properties = new SVGPathProperties(paths[i].path); 258 | for (let j = 0; j < paths[i].xValues.length; j++) { 259 | var tangent = properties.getTangentAtLength( 260 | (j * properties.getTotalLength()) / (paths[i].xValues.length - 1) 261 | ); 262 | 263 | test.true(inDelta(tangent.x, paths[i].xValues[j], 0.1)); 264 | test.true(inDelta(tangent.y, paths[i].yValues[j], 0.1)); 265 | } 266 | 267 | test.deepEqual( 268 | properties.getTangentAtLength(10000000), 269 | properties.getTangentAtLength(properties.getTotalLength()) 270 | ); 271 | test.deepEqual( 272 | properties.getTangentAtLength(-1), 273 | properties.getTangentAtLength(0) 274 | ); 275 | } 276 | 277 | test.end(); 278 | }); 279 | -------------------------------------------------------------------------------- /test/test-getLength.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 164 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "noFallthroughCasesInSwitch": true, 5 | "noUnusedParameters": true, 6 | "noImplicitReturns": true, 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "noUnusedLocals": true, 10 | "noImplicitAny": true, 11 | "declarationDir": "dist/types", 12 | "declaration": true, 13 | "target": "es2015", 14 | "module": "es2015", 15 | "strict": true 16 | }, 17 | "include": ["src/**/*"], 18 | "exclude": ["node_modules", "dist"] 19 | } 20 | --------------------------------------------------------------------------------