├── .eslintignore ├── .vscode └── settings.json ├── typedoc.json ├── _config.yml ├── docs ├── _config.yml ├── .nojekyll ├── assets │ └── highlight.css ├── types │ ├── _internal_.Vector.html │ ├── _internal_.NumArray4.html │ └── _internal_.SegmentFunction.html ├── interfaces │ ├── _internal_.BBox.html │ ├── _internal_.CurveParameters.html │ └── _internal_.SplineCurveOptions.html ├── index.html ├── modules │ └── _internal_.html └── variables │ └── EPS.html ├── .mocharc.json ├── tsconfig.json ├── src ├── curve-mappers │ ├── index.ts │ ├── abstract-curve-mapper.spec.ts │ ├── segmented-curve-mapper.ts │ ├── abstract-curve-mapper.ts │ ├── segmented-curve-mapper.spec.ts │ ├── numerical-curve-mapper.spec.ts │ ├── numerical-curve-mapper.ts │ └── gauss.ts ├── index.ts └── core │ ├── interfaces.spec.ts │ ├── point.ts │ ├── utils.spec.ts │ ├── spline-curve.ts │ ├── interfaces.ts │ ├── spline-curve.spec.ts │ ├── utils.ts │ ├── math.spec.ts │ ├── spline-segment.ts │ ├── spline-segment.spec.ts │ └── math.ts ├── .gitignore ├── test ├── test-data.ts └── test-utils.ts ├── .editorconfig ├── .github └── workflows │ ├── npmpublish.yml │ └── nodejs.yml ├── .gitattributes ├── .eslintrc ├── rollup.config.mjs ├── LICENSE ├── README.md └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test 3 | testfiles 4 | dist 5 | **.spec.ts 6 | 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "binormals" 4 | ] 5 | } -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "excludePrivate": false, 3 | "exclude": ["**/legacy/*"] 4 | } 5 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | include: 3 | - "_*_.html" 4 | - "_*_.*.html" 5 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | include: 3 | - "_*_.html" 4 | - "_*_.*.html" 5 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["ts"], 3 | "spec": "src/**/*.spec.ts", 4 | "require": "ts-node/register" 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "declaration": true, 5 | "outDir": "dist" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /src/curve-mappers/index.ts: -------------------------------------------------------------------------------- 1 | export { SegmentedCurveMapper as LinearCurveMapper } from './segmented-curve-mapper'; 2 | export { NumericalCurveMapper } from './numerical-curve-mapper'; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files 3 | 4 | coverage 5 | dist 6 | docs/dist 7 | node_modules 8 | npm-debug.log 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /test/test-data.ts: -------------------------------------------------------------------------------- 1 | export const points = [[1, 18],[2, 13],[2.5, 10],[4, 7.5],[5, 8.5],[7, 9],[8, 8],[10, 8.5],[11, 8],[12, 7],[14, 5],[18, 6],[19, 2],[14, 1.5],[10, 3],[10, 10],[14, 12],[14.5, 11.5],[12, 10]]; 2 | export const points3d = [[1,0,1],[1,-1,1],[1,-2,2],[1.2,-3,1],[2,-5,5],[10,-5,4.5],[12,-5,7.5],[10,-4.8,10]]; 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CurveInterpolator } from './curve-interpolator'; 2 | export { default as Point } from './core/point'; 3 | 4 | export * from './core/math'; 5 | export * from './core/utils'; 6 | export * from './core/spline-curve'; 7 | export * from './core/spline-segment'; 8 | export * from './curve-mappers/index'; 9 | -------------------------------------------------------------------------------- /test/test-utils.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | export function compareNumArrays(result: any, expected: any, delta = 0.00001) : boolean { 4 | return result.every(((d, i) => expect(d).to.be.closeTo(expected[i], delta))); 5 | } 6 | 7 | export function compareNumArraysUnordered(result: any, expected: any, delta = 0.00001) : Chai.Assertion { 8 | const res = result.every(d => expected.some(e => Math.abs(d - e) <= delta)); 9 | return expect(res).to.be.true; 10 | } 11 | 12 | 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 16 15 | registry-url: https://registry.npmjs.org/ 16 | - run: npm ci 17 | - run: npm test 18 | - run: npm run pub 19 | env: 20 | NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH}} 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | * text=auto 4 | 5 | # For the following file types, normalize line endings to LF on 6 | # checkin and prevent conversion to CRLF when they are checked out 7 | # (this is required in order to prevent newline related issues like, 8 | # for example, after the build script is run) 9 | .* text eol=lf 10 | *.css text eol=lf 11 | *.ejs text eol=lf 12 | *.js text eol=lf 13 | *.md text eol=lf 14 | *.txt text eol=lf 15 | *.json text eol=lf 16 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [16.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install, build, and test 21 | run: | 22 | npm ci 23 | npm run build --if-present 24 | npm test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /src/core/interfaces.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { expect } from 'chai'; 3 | import { 4 | Vector, 5 | } from './interfaces'; 6 | import Point from './point'; 7 | import { distance } from './math'; 8 | 9 | 10 | 11 | describe('interfaces.ts', () => { 12 | it('should be able to use array as well as other types for Vector', () => { 13 | const arr:Vector = [1, 3]; 14 | const point:Vector = new Point(2, -3); 15 | 16 | expect(arr).to.be.instanceof(Array); 17 | expect(point).to.be.instanceof(Point); 18 | 19 | expect(distance(arr, point)).to.be.an('number'); 20 | }); 21 | }); 22 | 23 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "settings": { 5 | "import/resolver": { 6 | "typescript": {} // this loads /tsconfig.json to eslint 7 | } 8 | }, 9 | "plugins": [ 10 | "@typescript-eslint", 11 | "import" 12 | ], 13 | "extends": [ 14 | "eslint:recommended", 15 | "plugin:@typescript-eslint/recommended" 16 | ], 17 | "rules": { 18 | "one-var": "off", 19 | "no-mixed-operators": "off", 20 | "no-restricted-properties": "off", 21 | "no-param-reassign": "off", 22 | "no-plusplus": "off", 23 | "no-underscore-dangle": "off", 24 | "one-var-declaration-per-line": "off", 25 | "import/prefer-default-export": "off", 26 | "import/extensions": [ 27 | "error", 28 | "ignorePackages", 29 | { 30 | "js": "never", 31 | "jsx": "never", 32 | "ts": "never", 33 | "tsx": "never" 34 | } 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import terser from '@rollup/plugin-terser'; 4 | 5 | import pkg from './package.json' assert { type: "json" }; 6 | 7 | export default [ 8 | { 9 | input: 'src/index.ts', 10 | output: [ 11 | { 12 | file: pkg.main, 13 | format: 'cjs', 14 | }, 15 | { 16 | file: pkg.module, 17 | format: 'esm', 18 | }, 19 | ], 20 | external: [ 21 | ...Object.keys(pkg.dependencies || {}), 22 | ], 23 | plugins: [ 24 | typescript(), 25 | terser({ 26 | mangle: false, 27 | }), 28 | ], 29 | }, 30 | { 31 | input: 'src/index.ts', 32 | output: { 33 | name: 'curve-interpolator', 34 | file: pkg.browser, 35 | format: 'umd', 36 | }, 37 | plugins: [ 38 | resolve(), 39 | typescript(), 40 | terser({ 41 | mangle: false, 42 | }), 43 | ], 44 | }, 45 | ]; 46 | -------------------------------------------------------------------------------- /src/core/point.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample class that may be used as Return type. 3 | * @example interp.getPointAt(0.2, new Point()); 4 | * @example interp.getPoints(1000, Point); 5 | */ 6 | export default class Point { 7 | x: number; 8 | y: number; 9 | z?: number; 10 | w?: number; 11 | 12 | constructor(x = 0, y = 0, z:number = null, w:number = null) { 13 | this.x = x; 14 | this.y = y; 15 | this.z = z; 16 | this.w = w; 17 | } 18 | 19 | get 0() { 20 | return this.x; 21 | } 22 | 23 | set 0(x:number) { 24 | this.x = x; 25 | } 26 | 27 | get 1() { 28 | return this.y; 29 | } 30 | 31 | set 1(y:number) { 32 | this.y = y; 33 | } 34 | 35 | get 2() { 36 | return this.z; 37 | } 38 | 39 | set 2(z:number) { 40 | this.z = z; 41 | } 42 | 43 | get 3() { 44 | return this.w; 45 | } 46 | 47 | set 3(w:number) { 48 | this.w = w; 49 | } 50 | 51 | get length() : number { 52 | return Number.isFinite(this.w) ? 4 : Number.isFinite(this.z) ? 3 : 2; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016 Kjerand Pedersen 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/core/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { expect } from 'chai'; 3 | import { binarySearch, clamp, copyValues, fill, map, reduce } from './utils'; 4 | 5 | describe('utils.ts', () => { 6 | it('should be able fill a vector components with a single value', () => { 7 | expect(fill([1, 2, 3], 0)).to.deep.eq([0, 0, 0]); 8 | expect(fill([0, 0, 0, 0], 2)).to.deep.eq([2, 2, 2, 2]); 9 | }); 10 | 11 | it('should be possible to map over the components of a vector and produce a new vector', () => { 12 | expect(map([1,2,3], (c, i) => c + i)).to.deep.eq([1, 3, 5]); 13 | }); 14 | 15 | it('should be possible to reduce a vector to a single value', () => { 16 | expect(reduce([1, 2, 3], (s, c) => s + c)).to.deep.eq(6); 17 | }); 18 | 19 | it('should be able to copy values from one vector to another', () => { 20 | const source = [1, 2, 3]; 21 | const target = copyValues(source); 22 | 23 | expect(target).to.not.be.eq(source); 24 | expect(target).to.deep.eq(source); 25 | }); 26 | 27 | it('should be able to find closest index in an accumulated sum array using binary search', () => { 28 | const arr = [1, 3, 6, 8, 12]; 29 | expect(binarySearch(5, arr)).to.eq(1); 30 | expect(binarySearch(8, arr)).to.eq(3); 31 | expect(binarySearch(7, arr)).to.eq(2); 32 | expect(binarySearch(18, arr)).to.eq(4); 33 | }); 34 | 35 | it('should be able to clamp values', () => { 36 | let result = clamp(-3, 0, 1); 37 | expect(result).to.equal(0); 38 | 39 | result = clamp(3, 0, 1); 40 | expect(result).to.equal(1); 41 | 42 | result = clamp(0.8, 0, 1); 43 | expect(result).to.equal(0.8); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/kjerandp/curve-interpolator/workflows/Node%20CI/badge.svg) 2 | ![](https://img.shields.io/npm/v/curve-interpolator) 3 | # Curve Interpolator 4 | 5 | A lib for interpolating values over a cubic Cardinal/Catmull-Rom spline curve of n-dimenesions. 6 | 7 | ## Installation 8 | ```bash 9 | npm install --save curve-interpolator 10 | ``` 11 | ## Basic usage 12 | Reference the CurveInterpolator class: 13 | ```js 14 | // commonjs 15 | const CurveInterpolator = require('curve-interpolator').CurveInterpolator; 16 | 17 | // es6 18 | import { CurveInterpolator } from 'curve-interpolator'; 19 | 20 | ``` 21 | 22 | Define controlpoints you want the curve to pass through and pass it to the constructor of the CurveInterpolator to create an instance: 23 | 24 | ```js 25 | const points = [ 26 | [0, 4], 27 | [1, 2], 28 | [3, 6.5], 29 | [4, 8], 30 | [5.5, 4], 31 | [7, 3], 32 | [8, 0], 33 | ... 34 | ]; 35 | 36 | const interp = new CurveInterpolator(points, { tension: 0.2, alpha: 0.5 }); 37 | 38 | // get single point 39 | const position = 0.3 // [0 - 1] 40 | const pt = interp.getPointAt(position) 41 | 42 | // get points evently distributed along the curve 43 | const segments = 1000; 44 | const pts = interp.getPoints(segments); 45 | 46 | // lookup values along x and y axises 47 | const axis = 1; 48 | const yintersects = interp.getIntersects(7, axis); 49 | 50 | /* 51 | max number of solutions (0 = all (default), 1 = first, -1 = last) 52 | A negative max value counts solutions from end of curve 53 | */ 54 | const axis = 0; 55 | const max = -1; 56 | const xintersects = interp.getIntersects(3.2, axis, max); 57 | 58 | // get bounding box 59 | const bbox = interp.getBoundingBox(); 60 | ``` 61 | 62 | Online example on ObservableHQ: 63 | - https://observablehq.com/@kjerandp/curve-interpolator-demo 64 | 65 | ## Docs 66 | Docs are generated using typedoc in `./docs`. To create: 67 | ```bash 68 | npm run docs 69 | ``` 70 | Online: https://kjerandp.github.io/curve-interpolator/ 71 | 72 | ## License 73 | MIT 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": false, 3 | "name": "curve-interpolator", 4 | "version": "3.3.1", 5 | "description": "Interpolate values on a Cardinal/Catmull-Rom spline curve", 6 | "repository": "https://github.com/kjerandp/curve-interpolator", 7 | "bugs": { 8 | "url": "https://github.com/kjerandp/curve-interpolator/issues" 9 | }, 10 | "author": "Kjerand Pedersen", 11 | "license": "MIT", 12 | "keywords": [ 13 | "cubic", 14 | "cardinial", 15 | "catmull-rom", 16 | "curve", 17 | "interpolate", 18 | "interpolator", 19 | "spline", 20 | "analysis" 21 | ], 22 | "scripts": { 23 | "prebuild": "rimraf dist", 24 | "build": "rollup -c", 25 | "prepub": "npm run build", 26 | "pub": "npm publish --access=public", 27 | "test": "mocha", 28 | "test:watch": "mocha --reporter min --watch --watch-extensions ts", 29 | "predocs": "rimraf docs", 30 | "docs": "typedoc --out docs src/index.ts", 31 | "postdocs": "copyfiles _config.yml docs", 32 | "lint": "eslint . --ext .ts", 33 | "lint:fix": "eslint . --ext .ts --fix" 34 | }, 35 | "main": "dist/index.cjs.js", 36 | "module": "dist/index.esm.js", 37 | "browser": "dist/index.js", 38 | "devDependencies": { 39 | "@rollup/plugin-node-resolve": "^15.0.1", 40 | "@rollup/plugin-terser": "^0.2.1", 41 | "@rollup/plugin-typescript": "^10.0.1", 42 | "@types/chai": "^4.2.14", 43 | "@types/mocha": "^5.2.7", 44 | "@types/sinon": "^10.0.13", 45 | "@typescript-eslint/eslint-plugin": "^5.48.0", 46 | "@typescript-eslint/parser": "^5.48.0", 47 | "chai": "^4.3.7", 48 | "copyfiles": "^2.4.1", 49 | "eslint": "^8.31.0", 50 | "eslint-import-resolver-typescript": "^3.5.2", 51 | "mocha": "^10.2.0", 52 | "rimraf": "^3.0.2", 53 | "rollup": "^3.9.1", 54 | "sinon": "^15.0.1", 55 | "ts-node": "^10.9.1", 56 | "typedoc": "^0.23.23", 57 | "typedoc-plugin-missing-exports": "^1.0.0", 58 | "typescript": "^4.9.4" 59 | }, 60 | "files": [ 61 | "dist" 62 | ], 63 | "types": "dist/src/index.d.ts", 64 | "moduleFileExtensions": [ 65 | "ts", 66 | "tsx", 67 | "js" 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /src/core/spline-curve.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Vector, 3 | } from './interfaces'; 4 | 5 | /** 6 | * Used to extrapolate points in the beginning and end segments of an open spline chain 7 | * @param u Point bias 1 8 | * @param v Point bias 2 9 | * @returns extrapolated point 10 | */ 11 | export function extrapolateControlPoint(u: Vector, v: Vector) : Vector { 12 | const e = new Array(u.length); 13 | for (let i = 0; i < u.length; i++) { 14 | e[i] = 2 * u[i] - v[i]; 15 | } 16 | return e; 17 | } 18 | 19 | /** 20 | * Get the four control points for a spline segment 21 | * @param idx segment index 22 | * @param points all input control points 23 | * @param closed whether the curve should be closed or open 24 | * @returns array of control points 25 | */ 26 | export function getControlPoints(idx: number, points: Vector[], closed: boolean) : Vector[] { 27 | const maxIndex = points.length - 1; 28 | 29 | let p0: Vector, p1: Vector, p2: Vector, p3: Vector; 30 | 31 | if (closed) { 32 | p0 = points[idx - 1 < 0 ? maxIndex : idx - 1]; 33 | p1 = points[idx % points.length]; 34 | p2 = points[(idx + 1) % points.length]; 35 | p3 = points[(idx + 2) % points.length]; 36 | } else { 37 | if (idx === maxIndex) throw Error('There is no spline segment at this index for a closed curve!'); 38 | p1 = points[idx]; 39 | p2 = points[idx + 1]; 40 | //p2 = idx + 1 <= maxIndex ? points[idx + 1] : extrapolateControlPoint(points[0], p1); 41 | p0 = idx > 0 ? points[idx - 1] : extrapolateControlPoint(p1, p2); 42 | p3 = idx < maxIndex - 1 ? points[idx + 2] : extrapolateControlPoint(p2, p1); 43 | } 44 | 45 | return [p0, p1, p2, p3]; 46 | } 47 | 48 | /** 49 | * Find the spline segment index and the corresponding segment weight/fraction at the provided curve time (ct) 50 | * @param ct non-uniform time along curve (0 - 1) 51 | * @param points set of coordinates/control points making out the curve 52 | * @param options 53 | * @returns segment index and time 54 | */ 55 | export function getSegmentIndexAndT(ct: number, points: Vector[], closed = false) : { index: number, weight: number } { 56 | const nPoints = closed ? points.length : points.length - 1; 57 | if (ct === 1.0) return { index: nPoints - 1, weight: 1.0 }; 58 | 59 | const p = nPoints * ct; 60 | const index = Math.floor(p); 61 | const weight = p - index; 62 | return { index, weight }; 63 | } 64 | 65 | -------------------------------------------------------------------------------- /src/curve-mappers/abstract-curve-mapper.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import * as sinon from 'sinon'; 3 | import { expect } from 'chai'; 4 | import { AbstractCurveMapper } from './abstract-curve-mapper'; 5 | import { points, points3d } from '../../test/test-data'; 6 | 7 | 8 | class TestMapper extends AbstractCurveMapper { 9 | lengthAt(u: number): number { 10 | throw new Error('Method not implemented.'); 11 | } 12 | getT(u: number): number { 13 | throw new Error('Method not implemented.'); 14 | } 15 | getU(t: number): number { 16 | throw new Error('Method not implemented.'); 17 | } 18 | } 19 | 20 | describe('abstract-curve-mapper.ts', () => { 21 | it('should be able to instantiate class', () => { 22 | const mapper = new TestMapper(); 23 | expect(mapper).to.not.be.null; 24 | }); 25 | 26 | it('should be able set parameters', () => { 27 | const mapper = new TestMapper(); 28 | 29 | // test defaults 30 | expect(mapper.alpha).to.equal(0); 31 | expect(mapper.tension).to.equal(0.5); 32 | expect(mapper.closed).to.be.false; 33 | expect(mapper.points).to.be.undefined; 34 | 35 | // should not be allowed to pass less than 3 control points 36 | expect(() => mapper.points = []).to.throw; 37 | 38 | mapper.alpha = 0.5; 39 | mapper.tension = 0; 40 | mapper.closed = true; 41 | mapper.points = [[], [], []]; 42 | 43 | expect(mapper.alpha).to.equal(0.5); 44 | expect(mapper.tension).to.equal(0); 45 | expect(mapper.closed).to.be.true; 46 | expect(mapper.points).to.deep.equal([[], [], []]); 47 | }); 48 | 49 | it('should be able to calculate and cache coefficients and invalidate cache if parameters are changed', () => { 50 | const mapper = new TestMapper(); 51 | mapper.points = points; 52 | const coefficients = mapper.getCoefficients(1); 53 | expect(coefficients).to.be.instanceOf(Array); 54 | expect(mapper._cache['coefficients'].has(1)); 55 | 56 | mapper.alpha = 0; // same as default 57 | expect(mapper._cache['coefficients']).to.not.be.null; 58 | 59 | mapper.alpha = 0.5; 60 | expect(mapper._cache['coefficients']).to.be.null; 61 | }); 62 | 63 | it('should invoke callback when cache is invalidated', () => { 64 | const callback = sinon.spy(); 65 | const mapper = new TestMapper(callback); 66 | mapper.points = [[],[]]; 67 | mapper.alpha = 0.5; 68 | expect(callback.called).to.be.true; 69 | }); 70 | 71 | 72 | }); 73 | -------------------------------------------------------------------------------- /src/core/interfaces.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Array of four number items 3 | */ 4 | export type NumArray4 = [number, number, number, number]; 5 | 6 | /** 7 | * Either a number array or an object implementing the VectorType interface 8 | */ 9 | export type Vector = (number[] | VectorType); 10 | 11 | export type SegmentFunction = (t: number, coefficients: NumArray4) => number; 12 | export interface CurveMapper { 13 | alpha: number, 14 | tension: number, 15 | points: Vector[], 16 | closed: boolean, 17 | 18 | evaluateForT: (func:SegmentFunction, t:number, target?:VectorType) => Vector, 19 | lengthAt: (u: number) => number, 20 | getT: (u: number) => number, 21 | getU: (t: number) => number, 22 | getCoefficients: (idx: number) => NumArray4[], 23 | reset: () => void, 24 | } 25 | 26 | /** 27 | * Any objects that supports indexing values by number may be used as input or return types. 28 | * See the Point class for an example. 29 | */ 30 | export interface VectorType { 31 | 0: number, 32 | 1: number, 33 | 2?: number, 34 | 3?: number, 35 | x?: number, 36 | y?: number, 37 | z?: number, 38 | w?: number, 39 | length: number, 40 | } 41 | 42 | export interface CurveParameters { 43 | /* curve tension (0 = Catmull-Rom curve, 1 = linear curve) */ 44 | tension?: number, 45 | /* curve velocity vector modifier (0 = uniform, 0.5 = centripetal, 1 = chordal */ 46 | alpha?: number, 47 | } 48 | 49 | /** 50 | * Options required to perform calculations on a curve segment. 51 | */ 52 | export interface SplineSegmentOptions extends CurveParameters { 53 | knotSequence?: NumArray4, 54 | target?: Vector, 55 | } 56 | 57 | /** 58 | * Spline Curve characteristics 59 | */ 60 | export interface SplineCurveOptions extends CurveParameters { 61 | /* flag to set if the curve should be closed or not */ 62 | closed?: boolean, 63 | } 64 | 65 | /** 66 | * Used by the valuesLookup function to set axis, tension etc. 67 | */ 68 | export interface LookupOptions extends SplineCurveOptions { 69 | axis?: number, 70 | margin?: number, 71 | max?: number, 72 | processRefAxis?: boolean, 73 | } 74 | 75 | /** 76 | * Used by the positions lookup function 77 | */ 78 | export interface PositionLookupOptions extends SplineCurveOptions { 79 | axis?: number, 80 | margin?: number, 81 | max?: number, 82 | } 83 | 84 | /** 85 | * Bounding box interface 86 | */ 87 | export interface BBox { 88 | min: Vector, 89 | max: Vector, 90 | } 91 | 92 | /** 93 | * Options to control calculation of bounding box 94 | */ 95 | export interface BBoxOptions extends SplineCurveOptions{ 96 | from?: number, 97 | to?: number, 98 | } 99 | -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #795E26; 3 | --dark-hl-0: #DCDCAA; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #A31515; 7 | --dark-hl-2: #CE9178; 8 | --light-hl-3: #0000FF; 9 | --dark-hl-3: #569CD6; 10 | --light-hl-4: #008000; 11 | --dark-hl-4: #6A9955; 12 | --light-hl-5: #0070C1; 13 | --dark-hl-5: #4FC1FF; 14 | --light-hl-6: #001080; 15 | --dark-hl-6: #9CDCFE; 16 | --light-hl-7: #AF00DB; 17 | --dark-hl-7: #C586C0; 18 | --light-hl-8: #098658; 19 | --dark-hl-8: #B5CEA8; 20 | --light-code-background: #FFFFFF; 21 | --dark-code-background: #1E1E1E; 22 | } 23 | 24 | @media (prefers-color-scheme: light) { :root { 25 | --hl-0: var(--light-hl-0); 26 | --hl-1: var(--light-hl-1); 27 | --hl-2: var(--light-hl-2); 28 | --hl-3: var(--light-hl-3); 29 | --hl-4: var(--light-hl-4); 30 | --hl-5: var(--light-hl-5); 31 | --hl-6: var(--light-hl-6); 32 | --hl-7: var(--light-hl-7); 33 | --hl-8: var(--light-hl-8); 34 | --code-background: var(--light-code-background); 35 | } } 36 | 37 | @media (prefers-color-scheme: dark) { :root { 38 | --hl-0: var(--dark-hl-0); 39 | --hl-1: var(--dark-hl-1); 40 | --hl-2: var(--dark-hl-2); 41 | --hl-3: var(--dark-hl-3); 42 | --hl-4: var(--dark-hl-4); 43 | --hl-5: var(--dark-hl-5); 44 | --hl-6: var(--dark-hl-6); 45 | --hl-7: var(--dark-hl-7); 46 | --hl-8: var(--dark-hl-8); 47 | --code-background: var(--dark-code-background); 48 | } } 49 | 50 | :root[data-theme='light'] { 51 | --hl-0: var(--light-hl-0); 52 | --hl-1: var(--light-hl-1); 53 | --hl-2: var(--light-hl-2); 54 | --hl-3: var(--light-hl-3); 55 | --hl-4: var(--light-hl-4); 56 | --hl-5: var(--light-hl-5); 57 | --hl-6: var(--light-hl-6); 58 | --hl-7: var(--light-hl-7); 59 | --hl-8: var(--light-hl-8); 60 | --code-background: var(--light-code-background); 61 | } 62 | 63 | :root[data-theme='dark'] { 64 | --hl-0: var(--dark-hl-0); 65 | --hl-1: var(--dark-hl-1); 66 | --hl-2: var(--dark-hl-2); 67 | --hl-3: var(--dark-hl-3); 68 | --hl-4: var(--dark-hl-4); 69 | --hl-5: var(--dark-hl-5); 70 | --hl-6: var(--dark-hl-6); 71 | --hl-7: var(--dark-hl-7); 72 | --hl-8: var(--dark-hl-8); 73 | --code-background: var(--dark-code-background); 74 | } 75 | 76 | .hl-0 { color: var(--hl-0); } 77 | .hl-1 { color: var(--hl-1); } 78 | .hl-2 { color: var(--hl-2); } 79 | .hl-3 { color: var(--hl-3); } 80 | .hl-4 { color: var(--hl-4); } 81 | .hl-5 { color: var(--hl-5); } 82 | .hl-6 { color: var(--hl-6); } 83 | .hl-7 { color: var(--hl-7); } 84 | .hl-8 { color: var(--hl-8); } 85 | pre, code { background: var(--code-background); } 86 | -------------------------------------------------------------------------------- /src/core/spline-curve.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { expect } from 'chai'; 3 | import { 4 | compareNumArrays, 5 | } from '../../test/test-utils'; 6 | import { points, points3d } from '../../test/test-data'; 7 | import { extrapolateControlPoint, getControlPoints, getSegmentIndexAndT } from './spline-curve'; 8 | 9 | 10 | const EPS = 0.000001; 11 | 12 | describe('spline-curve.ts', () => { 13 | it('should be able to extrapolate a weighted point based on existing adjacent control points', () => { 14 | expect(extrapolateControlPoint([2, 5], [4, 9])).to.deep.eq([0, 1]); 15 | expect(extrapolateControlPoint([4, 9], [2, 5, ])).to.deep.eq([6, 13]); 16 | expect(extrapolateControlPoint([-2, 0], [0, 0])).to.deep.eq([-4, 0]); 17 | }); 18 | 19 | it('should find the associated control points for a spline segment from its index', () => { 20 | const points = [[2, 4], [4, 3], [4, -2], [1, 6]]; 21 | 22 | expect(getControlPoints(0, points, false)).to.deep.eq([ 23 | [0, 5], // extrapolated 24 | [2, 4], 25 | [4, 3], 26 | [4, -2], 27 | ]); 28 | 29 | expect(getControlPoints(1, points, false)).to.deep.eq([ 30 | [2, 4], 31 | [4, 3], 32 | [4, -2], 33 | [1, 6], 34 | ]); 35 | 36 | expect(getControlPoints(2, points, false)).to.deep.eq([ 37 | [4, 3], 38 | [4, -2], 39 | [1, 6], 40 | [-2, 14], // extrapolated 41 | ]); 42 | 43 | // for open curves, there is no curve segment defined for index beyond points.length - 1 44 | expect(() => getControlPoints(3, points, false)).to.throw; 45 | 46 | // closed curve 47 | expect(getControlPoints(0, points, true)).to.deep.eq([ 48 | [1, 6], 49 | [2, 4], 50 | [4, 3], 51 | [4, -2], 52 | ]); 53 | 54 | expect(getControlPoints(1, points, true)).to.deep.eq([ 55 | [2, 4], 56 | [4, 3], 57 | [4, -2], 58 | [1, 6], 59 | ]); 60 | 61 | expect(getControlPoints(2, points, true)).to.deep.eq([ 62 | [4, 3], 63 | [4, -2], 64 | [1, 6], 65 | [2, 4], 66 | ]); 67 | 68 | expect(getControlPoints(3, points, true)).to.deep.eq([ 69 | [4, -2], 70 | [1, 6], 71 | [2, 4], 72 | [4, 3], 73 | ]); 74 | }); 75 | 76 | it('should find the associated segment index and weight (local t) based on a global t', () => { 77 | const points = [[2, 4], [4, 3], [4, -2], [1, 6]]; 78 | 79 | expect(getSegmentIndexAndT(0, points, false)).to.deep.eq({ index: 0, weight: 0 }); 80 | expect(getSegmentIndexAndT(0.5, points, false)).to.deep.eq({ index: 1, weight: 0.5 }); 81 | expect(getSegmentIndexAndT(0.84, points, false)).to.deep.eq({ index: 2, weight: 0.52 }); 82 | expect(getSegmentIndexAndT(1, points, false)).to.deep.eq({ index: 2, weight: 1 }); 83 | 84 | expect(getSegmentIndexAndT(0, points, true)).to.deep.eq({ index: 0, weight: 0 }); 85 | expect(getSegmentIndexAndT(0.5, points, true)).to.deep.eq({ index: 2, weight: 0 }); 86 | expect(getSegmentIndexAndT(0.99, points, true)).to.deep.eq({ index: 3, weight: 0.96 }); 87 | expect(getSegmentIndexAndT(1, points, true)).to.deep.eq({ index: 3, weight: 1 }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/curve-mappers/segmented-curve-mapper.ts: -------------------------------------------------------------------------------- 1 | import { AbstractCurveMapper } from "./abstract-curve-mapper"; 2 | import { Vector } from "../core/interfaces"; 3 | import { distance } from "../core/math"; 4 | import { binarySearch } from "../core/utils"; 5 | import { valueAtT } from "../core/spline-segment"; 6 | 7 | /** 8 | * Approximate spline curve by subdividing it into smaller linear 9 | * line segments. Used to approximate length and mapping between 10 | * uniform (u) and non-uniform (t) time along curve. 11 | */ 12 | export class SegmentedCurveMapper extends AbstractCurveMapper { 13 | 14 | _subDivisions: number; 15 | 16 | /** 17 | * 18 | * @param subDivisions number of sub divisions to use 19 | * @param onInvalidateCache callback function to be invoked when cache is invalidated 20 | */ 21 | constructor(subDivisions = 300, onInvalidateCache: () => void = null) { 22 | super(onInvalidateCache); 23 | this._subDivisions = subDivisions; 24 | } 25 | 26 | get arcLengths() { 27 | if (!this._cache['arcLengths']) { 28 | this._cache['arcLengths'] = this.computeArcLengths(); 29 | } 30 | return this._cache['arcLengths']; 31 | } 32 | 33 | /** 34 | * Clear cache 35 | */ 36 | override _invalidateCache() { 37 | super._invalidateCache(); 38 | this._cache['arcLengths'] = null; 39 | } 40 | 41 | /** 42 | * Break curve into segments and return the curve length at each segment index. 43 | * Used for mapping between t and u along the curve. 44 | */ 45 | computeArcLengths() { 46 | const lengths = []; 47 | let current: Vector, last = this.evaluateForT(valueAtT, 0); 48 | let sum = 0; 49 | 50 | lengths.push(0); 51 | 52 | for (let p = 1; p <= this._subDivisions; p++) { 53 | current = this.evaluateForT(valueAtT, p / this._subDivisions); 54 | sum += distance(current, last); 55 | lengths.push(sum); 56 | last = current; 57 | } 58 | return lengths; 59 | } 60 | 61 | /** 62 | * Get curve length at u 63 | * @param u normalized uniform position along the spline curve 64 | * @returns length in point coordinates 65 | */ 66 | lengthAt(u: number) { 67 | const arcLengths = this.arcLengths; 68 | return u * arcLengths[arcLengths.length - 1]; 69 | } 70 | 71 | /** 72 | * Maps a uniform time along the curve to non-uniform time (t) 73 | * @param u normalized uniform position along the spline curve 74 | * @returns t encoding segment index and local time along curve 75 | */ 76 | getT(u: number) { 77 | const arcLengths = this.arcLengths; 78 | const il = arcLengths.length; 79 | const targetArcLength = u * arcLengths[il - 1]; 80 | 81 | const i = binarySearch(targetArcLength, arcLengths); 82 | if (arcLengths[i] === targetArcLength) { 83 | return i / (il - 1); 84 | } 85 | 86 | // we could get finer grain at lengths, or use simple interpolation between two points 87 | const lengthBefore = arcLengths[i]; 88 | const lengthAfter = arcLengths[i + 1]; 89 | const segmentLength = lengthAfter - lengthBefore; 90 | 91 | // determine where we are between the 'before' and 'after' points 92 | const segmentFraction = (targetArcLength - lengthBefore) / segmentLength; 93 | 94 | // add that fractional amount to t 95 | return (i + segmentFraction) / (il - 1); 96 | } 97 | 98 | /** 99 | * Maps a non-uniform time along the curve to uniform time (u) 100 | * @param t non-uniform time along curve 101 | * @returns uniform time along curve 102 | */ 103 | getU(t: number) { 104 | if (t === 0) return 0; 105 | if (t === 1) return 1; 106 | 107 | const arcLengths = this.arcLengths; 108 | const al = arcLengths.length - 1; 109 | const totalLength = arcLengths[al]; 110 | 111 | // need to denormalize t to find the matching length 112 | const tIdx = t * al; 113 | 114 | const subIdx = Math.floor(tIdx); 115 | const l1 = arcLengths[subIdx]; 116 | 117 | if (tIdx === subIdx) return l1 / totalLength; 118 | 119 | // measure the length between t0 at subIdx and t 120 | const t0 = subIdx / al; 121 | const p0 = this.evaluateForT(valueAtT, t0); 122 | const p1 = this.evaluateForT(valueAtT, t); 123 | const l = l1 + distance(p0, p1); 124 | 125 | //const l2 = arcLengths[subIdx + 1]; 126 | //const l = l1 + (tIdx - subIdx) * (l2 - l1); 127 | 128 | return l / totalLength; 129 | 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/core/utils.ts: -------------------------------------------------------------------------------- 1 | import { Vector } from "./interfaces"; 2 | 3 | /** 4 | * Fill all components of a vector with a value 5 | * @param v vector 6 | * @param val fill value 7 | */ 8 | export function fill(v:Vector, val:number) : Vector { 9 | for (let i = 0; i < v.length; i++) { 10 | v[i] = val; 11 | } 12 | return v; 13 | } 14 | 15 | /** 16 | * Map all components of a vector using provided mapping function. 17 | * @param v vector 18 | * @param func mapping function 19 | */ 20 | export function map(v:Vector, func: (c:number, i:number) => number) : Vector { 21 | for (let i = 0; i < v.length; i++) { 22 | v[i] = func(v[i], i); 23 | } 24 | return v; 25 | } 26 | 27 | /** 28 | * Reduce a vector to a single value using the provided reduce function. 29 | * @param v vector 30 | * @param func reduce function 31 | * @param r initial value 32 | */ 33 | export function reduce(v:Vector, func: (s: number, c:number, i:number) => number, r = 0) : number { 34 | for (let i = 0; i < v.length; i++) { 35 | r = func(r, v[i], i); 36 | } 37 | return r; 38 | } 39 | 40 | /** 41 | * Copy values from one vector to another. If target is not provided it will be created. 42 | * @param source source vector 43 | * @param target target vector 44 | * @returns vector 45 | */ 46 | export function copyValues(source: Vector, target?: Vector) : Vector { 47 | target = target || new Array(source.length); 48 | for (let i = 0; i < source.length; i++) { 49 | target[i] = source[i]; 50 | } 51 | return target; 52 | } 53 | 54 | /** 55 | * Reduce the set of coordinates for a curve by eliminating points that are not 56 | * contributing to the shape of the curve, i.e. multiple points making out a linear 57 | * segment. 58 | * @param inputArr set of coordinates 59 | * @param maxOffset threshold to use for determining if a point is part of a linear line segment 60 | * @param maxDistance points will not be removed if the distance equals or is greater than the given maxDistance 61 | */ 62 | export function simplify2d(inputArr:number[][], maxOffset = 0.001, maxDistance = 10) : number[][] { 63 | if (inputArr.length <= 4) return inputArr; 64 | const [o0, o1] = inputArr[0]; 65 | const arr = inputArr.map(d => [d[0] - o0, d[1] - o1]); 66 | let [a0, a1] = arr[0]; 67 | const sim = [inputArr[0]]; 68 | 69 | for (let i = 1; i + 1 < arr.length; i++) { 70 | const [t0, t1] = arr[i]; 71 | const [b0, b1] = arr[i + 1]; 72 | 73 | if (b0 - t0 !== 0 || b1 - t1 !== 0) { 74 | // Proximity check 75 | const proximity = 76 | Math.abs(a0 * b1 - a1 * b0 + b0 * t1 - b1 * t0 + a1 * t0 - a0 * t1) / 77 | Math.sqrt((b0 - a0) ** 2 + (b1 - a1) ** 2); 78 | 79 | const dir = [a0 - t0, a1 - t1]; 80 | const len = Math.sqrt(dir[0] ** 2 + dir[1] ** 2); 81 | 82 | if (proximity > maxOffset || len >= maxDistance) { 83 | sim.push([t0 + o0, t1 + o1]); 84 | [a0, a1] = [t0, t1]; 85 | } 86 | } 87 | } 88 | const last = arr[arr.length - 1]; 89 | sim.push([last[0] + o0, last[1] + o1]); 90 | 91 | return sim; 92 | } 93 | 94 | /** 95 | * Clamp an input value to min and max 96 | * @param value input value 97 | * @param min min value 98 | * @param max max value 99 | */ 100 | export function clamp(value:number, min = 0, max = 1) : number { 101 | if (value < min) return min; 102 | if (value > max) return max; 103 | return value; 104 | } 105 | 106 | /** 107 | * Finds the index in accumulatedValues of the highest value that is less than or equal to targetValue 108 | * @param targetValue search term 109 | * @param accumulatedValues array of accumulated values to search in 110 | * @returns 111 | */ 112 | export function binarySearch(targetValue: number, accumulatedValues: number[]) { 113 | const min = accumulatedValues[0]; 114 | const max = accumulatedValues[accumulatedValues.length - 1]; 115 | if (targetValue >= max) { 116 | return accumulatedValues.length - 1; 117 | } 118 | 119 | if (targetValue <= min) { 120 | return 0; 121 | } 122 | 123 | let left = 0; 124 | let right = accumulatedValues.length - 1; 125 | 126 | while (left <= right) { 127 | const mid = Math.floor((left + right) / 2); 128 | const lMid = accumulatedValues[mid]; 129 | 130 | if (lMid < targetValue) { 131 | left = mid + 1; 132 | } else if (lMid > targetValue) { 133 | right = mid - 1; 134 | } else { 135 | return mid; 136 | } 137 | } 138 | 139 | return Math.max(0, right); 140 | } 141 | -------------------------------------------------------------------------------- /src/curve-mappers/abstract-curve-mapper.ts: -------------------------------------------------------------------------------- 1 | import { CurveMapper, NumArray4, SegmentFunction, Vector } from "../core/interfaces"; 2 | import { getControlPoints, getSegmentIndexAndT } from "../core/spline-curve"; 3 | import { calculateCoefficients, evaluateForT } from "../core/spline-segment"; 4 | 5 | 6 | /** 7 | * The curve mapper's main responsibility is to map between normalized 8 | * curve position (u) to curve segments and segment position (t). Since 9 | * it requires access to control points and curve parameters, it also keeps 10 | * this data along with an internal cache. For this reason, the common 11 | * functionality has been but into this abstract class definition, so that 12 | * the mapping specific implementation can be held at a minimum by extending 13 | * this class. 14 | */ 15 | export abstract class AbstractCurveMapper implements CurveMapper { 16 | _subDivisions: number; 17 | _cache: object; 18 | _points: Vector[]; 19 | _alpha = 0.0; 20 | _tension = 0.5; 21 | _closed = false; 22 | _onInvalidateCache: () => void = null; 23 | 24 | /** 25 | * AbstractCurveMapper Constructor 26 | * @param onInvalidateCache callback function to be invoked when cache needs to be reset 27 | */ 28 | constructor(onInvalidateCache: () => void = null) { 29 | this._onInvalidateCache = onInvalidateCache; 30 | this._cache = { 31 | arcLengths: null, 32 | coefficients: null, 33 | }; 34 | } 35 | 36 | /** 37 | * Clears cache and invoke callback if provided 38 | * @returns void 39 | */ 40 | protected _invalidateCache() : void { 41 | if (!this.points) return; 42 | this._cache = { 43 | arcLengths: null, 44 | coefficients: null, 45 | }; 46 | if (this._onInvalidateCache) this._onInvalidateCache(); 47 | } 48 | 49 | /** 50 | * Returns the curve length in point coordinates from the global 51 | * curve position u, where u=1 is the full length of the curve. 52 | * @param u normalized position on curve (0..1) 53 | */ 54 | abstract lengthAt(u: number) : number; 55 | abstract getT(u: number) : number; 56 | abstract getU(t: number) : number; 57 | 58 | /** 59 | * Curve alpha parameter (0=uniform, 0.5=centripetal, 1=chordal) 60 | */ 61 | get alpha() { return this._alpha; } 62 | set alpha(alpha: number) { 63 | if (Number.isFinite(alpha) && alpha !== this._alpha) { 64 | this._invalidateCache(); 65 | this._alpha = alpha; 66 | } 67 | } 68 | 69 | /** 70 | * Curve tension (0=Catmull-rom, 1=linear) 71 | */ 72 | get tension() { return this._tension; } 73 | set tension(tension: number) { 74 | if (Number.isFinite(tension) && tension !== this._tension) { 75 | this._invalidateCache(); 76 | this._tension = tension; 77 | } 78 | } 79 | 80 | /** 81 | * Control points for curve 82 | */ 83 | get points() { return this._points; } 84 | set points(points: Vector[]) { 85 | if (!points || points.length < 2) throw Error('At least 2 control points are required!'); 86 | this._points = points; 87 | this._invalidateCache(); 88 | } 89 | 90 | /** 91 | * Determines whether the curve should be a closed curve or not 92 | */ 93 | get closed() { return this._closed; } 94 | set closed(closed: boolean) { 95 | closed = !!closed; 96 | if (this._closed !== closed) { 97 | this._invalidateCache(); 98 | this._closed = closed; 99 | } 100 | } 101 | 102 | reset() { 103 | this._invalidateCache(); 104 | } 105 | 106 | /** 107 | * Evaluate curve segment function at t 108 | * @param t time along full curve (encodes segment index and segment t) 109 | * @param target optional target vector 110 | * @returns vector 111 | */ 112 | evaluateForT(func: SegmentFunction, t:number, target?: Vector) : Vector { 113 | const { index, weight } = getSegmentIndexAndT(t, this.points, this.closed); 114 | const coefficients = this.getCoefficients(index); 115 | return evaluateForT(func, weight, coefficients, target); 116 | } 117 | 118 | /** 119 | * Get the curve function coefficients at the given segment index. The coefficients 120 | * are calculated once per segment and put in cache until it is invalidated. 121 | * @param idx segment index 122 | * @returns coefficients for the curve function at the given segment index 123 | */ 124 | getCoefficients(idx: number) { 125 | if (!this.points) return undefined; 126 | if (!this._cache['coefficients']) { 127 | this._cache['coefficients'] = new Map(); 128 | } 129 | if (!this._cache['coefficients'].has(idx)) { 130 | const [p0, p1, p2, p3] = getControlPoints(idx, this.points, this.closed); 131 | const coefficients = calculateCoefficients(p0, p1, p2, p3, { tension: this.tension, alpha: this.alpha }); 132 | this._cache['coefficients'].set(idx, coefficients); 133 | } 134 | return this._cache['coefficients'].get(idx); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/core/math.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { 3 | getQuadRoots, 4 | getCubicRoots, 5 | distance, 6 | normalize, 7 | orthogonal, 8 | sumOfSquares, 9 | magnitude, 10 | dot, 11 | rotate3d, 12 | rotate2d, 13 | } from './math'; 14 | import { expect } from 'chai'; 15 | import { 16 | compareNumArrays, 17 | compareNumArraysUnordered, 18 | } from '../../test/test-utils'; 19 | 20 | const EPS = 0.000001; 21 | 22 | describe('math.ts', () => { 23 | it('should solve 2nd degree equation', () => { 24 | 25 | // 3x^2 + 2x - 2 26 | let result = getQuadRoots(3, 2, -2); 27 | compareNumArraysUnordered(result, [0.54858, -1.21525]); 28 | 29 | // 2x^2 + 4x - 4 30 | result = getQuadRoots(2, 4, -4); 31 | compareNumArraysUnordered(result, [0.732050, -2.73205]); 32 | }); 33 | 34 | it('should solve 3nd degree equation', () => { 35 | 36 | // 2x^3 + 3x^2 – 11x – 6 37 | let result = getCubicRoots(2, 3, -11, -6); 38 | compareNumArraysUnordered(result, [2, -0.5, -3]); 39 | 40 | // x^3 - 7x^2 + 4x + 12 41 | result = getCubicRoots(1, -7, 4, 12); 42 | compareNumArraysUnordered(result, [-1, 2, 6]); 43 | 44 | // x^3 + 12 45 | result = getCubicRoots(1, 0, 0, 12); 46 | compareNumArraysUnordered(result, [-2.289428]); 47 | 48 | // 2x^2 + 4x - 4 49 | result = getCubicRoots(0, 2, 4, -4); 50 | compareNumArraysUnordered(result, [0.732050, -2.73205]); 51 | 52 | // 2x - 4 53 | result = getCubicRoots(0, 0, 2, -4); 54 | compareNumArraysUnordered(result, [2]); 55 | 56 | // -4 57 | result = getCubicRoots(0, 0, 0, -4); 58 | compareNumArraysUnordered(result, []); 59 | }); 60 | 61 | it('should be able to calculate the distance between two points', () => { 62 | let result = distance([0, 0], [-3, 0]); 63 | expect(result).to.equal(3); 64 | 65 | result = distance([3, 0], [0, 3]); 66 | expect(result).to.be.approximately(4.24264, EPS); 67 | 68 | result = distance([2, 1], [2, 1]); 69 | expect(result).to.equal(0); 70 | 71 | result = distance([2, 1, -3], [2, 1, 8]); 72 | expect(result).to.equal(11); 73 | 74 | result = distance([0, 0, 0], [2, 1, 2]); 75 | expect(result).to.equal(3); 76 | }); 77 | 78 | it('should be able to normalize vectors', () => { 79 | let result = normalize([-3, 0]); 80 | expect(result).to.eql([-1, 0]); 81 | 82 | result = normalize([0, 0]); 83 | expect(result).to.eql([0, 0]); 84 | 85 | result = normalize([3, 0]); 86 | expect(result).to.be.eql([1, 0]); 87 | 88 | result = normalize([2, 2]); 89 | compareNumArrays(result, [0.707106, 0.707106]); 90 | 91 | result = normalize([-2, 4]); 92 | compareNumArrays(result, [-0.447213, 0.89442719]); 93 | 94 | result = normalize([0, 0, 0]); 95 | expect(result).to.eql([0, 0, 0]); 96 | 97 | result = normalize([3, 0, 0]); 98 | expect(result).to.be.eql([1, 0, 0]); 99 | 100 | result = normalize([2, 2, 1]); 101 | compareNumArrays(result, [2/3, 2/3, 1/3]); 102 | 103 | result = normalize([-2, 2, 5]); 104 | compareNumArrays(result, [-0.3481553119, 0.3481553119, 0.87038828]); 105 | }); 106 | 107 | it('should be able to rotate vectors 90 degrees', () => { 108 | let result = orthogonal([-3, 1]); 109 | expect(result).to.eql([-1, -3]); 110 | 111 | result = orthogonal([2, 2]); 112 | expect(result).to.eql([-2, 2]); 113 | 114 | expect(() => orthogonal([1, 2, 3])).to.throw('Only supported for 2d vectors'); 115 | }); 116 | 117 | it('should compute sum of squares (distance squared) between two vectors', () => { 118 | const a = [2, 4]; 119 | const b = [-3, 7]; 120 | const sumSq = sumOfSquares(a, b); 121 | 122 | expect(sumSq).to.eq(34); 123 | }); 124 | 125 | it('should compute the magnitude (absolute value) of a vector', () => { 126 | const a = [2, 4]; 127 | expect(magnitude(a)).to.eq(Math.sqrt(20)); 128 | }); 129 | 130 | it('should compute the dot product between two vectors', () => { 131 | expect(dot([], [])).to.eq(0); 132 | expect(dot([2, -2], [4, 1])).to.eq(6); 133 | expect(dot([0, 0], [2, 1])).to.eq(0); 134 | expect(dot([-1, 3, 6], [2, 6, -3])).to.eq(-2); 135 | }); 136 | 137 | it('should rotate a 2d point at the given angle and anchor point', () => { 138 | compareNumArrays(rotate2d([1, 0], Math.PI / 2), [-0, 1], EPS); 139 | compareNumArrays(rotate2d([1.5, 2], Math.PI / 2), [-2, 1.5], EPS); 140 | compareNumArrays(rotate2d([1, 1], Math.PI), [-1, -1], EPS); 141 | compareNumArrays(rotate2d([1, 1], Math.PI, [0, 1]), [-1, 1], EPS); 142 | compareNumArrays(rotate2d([2, 3], Math.PI / 4, [1, -1]), [-1.12132034, 2.5355339], EPS); 143 | compareNumArrays(rotate2d([2, 3], Math.PI / 4, [2, 3]), [2, 3], EPS); 144 | }); 145 | 146 | it('should rotate a 3d point at a given angle and rotation axis', () => { 147 | compareNumArrays(rotate3d([1, 0, 0], [0, 1, 0], Math.PI), [-1, 0, 0], EPS); 148 | compareNumArrays(rotate3d([1, 5, 0], [0, 1, 0], Math.PI), [-1, 5, 0], EPS); 149 | compareNumArrays(rotate3d([1, -2, 0], [0, 0, 1], Math.PI / 2), [2, 1, 0], EPS); 150 | compareNumArrays(rotate3d([1, -2, 0], [0, 0, 1], Math.PI / 3), [2.23205080, -0.1339746, 0], EPS); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /src/curve-mappers/segmented-curve-mapper.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { expect } from 'chai'; 3 | import { points, points3d } from '../../test/test-data'; 4 | import { SegmentedCurveMapper } from './segmented-curve-mapper'; 5 | import { AbstractCurveMapper } from './abstract-curve-mapper'; 6 | 7 | const EPS = 0.000001; 8 | 9 | describe('segmented-curve-mapper.ts', () => { 10 | it('should be able to instantiate class', () => { 11 | const mapper = new SegmentedCurveMapper(); 12 | expect(mapper).to.not.be.null; 13 | expect(mapper).to.be.instanceOf(AbstractCurveMapper); 14 | }); 15 | 16 | it('should be able to compute arc lengths', () => { 17 | const divisions = 200; 18 | const mapper = new SegmentedCurveMapper(divisions); 19 | 20 | mapper.tension = 0.5; 21 | mapper.points = points; 22 | 23 | expect(mapper.arcLengths.length).to.eq(divisions + 1); 24 | 25 | expect(mapper.arcLengths[0]).to.eq(0); 26 | 27 | const totalLength = mapper.arcLengths[mapper.arcLengths.length - 1]; 28 | expect(totalLength).to.be.closeTo(56.6, 0.1); 29 | expect(mapper.lengthAt(1)).to.eq(totalLength); 30 | expect(mapper.lengthAt(0.25)).to.be.closeTo(totalLength * 0.25, 0.1); 31 | expect(mapper.lengthAt(0.5)).to.be.closeTo(totalLength * 0.5, 0.1); 32 | expect(mapper.lengthAt(0.75)).to.be.closeTo(totalLength * 0.75, 0.1); 33 | 34 | // setting tension to 0 should result in a longer curve, invalidating the existing cached arcLengths 35 | mapper.tension = 0; 36 | expect(mapper.arcLengths[mapper.arcLengths.length - 1]).to.be.greaterThan(totalLength); 37 | }); 38 | 39 | it('should be able to convert between t and u - 2d', () => { 40 | const mapper = new SegmentedCurveMapper(300); 41 | mapper.tension = 0.5; 42 | mapper.points = points; 43 | 44 | expect(mapper.getT(0)).to.eq(0); 45 | expect(mapper.getT(1)).to.eq(1); 46 | expect(mapper.getU(0)).to.eq(0); 47 | expect(mapper.getU(1)).to.eq(1); 48 | 49 | for (let i = 0; i <= 100; i += 1) { 50 | const u = Math.random(); 51 | const t = mapper.getT(u); 52 | expect(mapper.getU(t)).to.be.closeTo(u, 0.001); 53 | } 54 | 55 | }); 56 | 57 | it('should be able to convert between t and u - 3d', () => { 58 | const mapper = new SegmentedCurveMapper(300); 59 | mapper.tension = 0.5; 60 | mapper.points = points3d; 61 | 62 | expect(mapper.getT(0)).to.eq(0); 63 | expect(mapper.getT(1)).to.eq(1); 64 | expect(mapper.getU(0)).to.eq(0); 65 | expect(mapper.getU(1)).to.eq(1); 66 | 67 | for (let i = 0; i <= 100; i += 1) { 68 | const u = Math.random(); 69 | const t = mapper.getT(u); 70 | expect(mapper.getU(t)).to.be.closeTo(u, 0.001); 71 | } 72 | 73 | }); 74 | 75 | it('should be able to divide a curve into segments and estimate each segments length', () => { 76 | const mapper = new SegmentedCurveMapper(300); 77 | mapper.tension = 0; 78 | mapper.points = points; 79 | 80 | const arcLengths = mapper.computeArcLengths(); 81 | 82 | expect(arcLengths.length).to.equal(301); 83 | expect(arcLengths[0]).to.equal(0); 84 | expect(arcLengths[arcLengths.length - 1]).to.be.approximately(57.8, 0.1); 85 | }); 86 | 87 | it('should be able to divide a 3d curve into segments and estimate each segments length', () => { 88 | const mapper = new SegmentedCurveMapper(300); 89 | mapper.tension = 0; 90 | mapper.points = points3d; 91 | 92 | const arcLengths = mapper.computeArcLengths(); 93 | 94 | expect(arcLengths.length).to.equal(301); 95 | expect(arcLengths[0]).to.equal(0); 96 | expect(arcLengths[arcLengths.length - 1]).to.be.approximately(24.17, 0.1); 97 | }); 98 | 99 | it('should be able to map between t and u indexes', () => { 100 | const mapper = new SegmentedCurveMapper(300); 101 | mapper.tension = 0; 102 | mapper.points = points; 103 | 104 | expect(mapper.getT(0)).to.equal(0); 105 | expect(mapper.getT(1)).to.equal(1); 106 | expect(mapper.getT(0.1)).to.approximately(0.065653, EPS); 107 | expect(mapper.getT(0.2)).to.approximately(0.188370, EPS); 108 | expect(mapper.getT(0.3)).to.approximately(0.364322, EPS); 109 | expect(mapper.getT(0.4)).to.approximately(0.544484, EPS); 110 | expect(mapper.getT(0.5)).to.approximately(0.625274, EPS); 111 | expect(mapper.getT(0.6)).to.approximately(0.695089, EPS); 112 | expect(mapper.getT(0.7)).to.approximately(0.758911, EPS); 113 | expect(mapper.getT(0.8)).to.approximately(0.810916, EPS); 114 | expect(mapper.getT(0.9)).to.approximately(0.866147, EPS); 115 | }); 116 | 117 | it('should be able to map between u and t indexes', () => { 118 | const mapper = new SegmentedCurveMapper(300); 119 | mapper.tension = 0; 120 | mapper.points = points; 121 | 122 | expect(mapper.getU(0)).to.equal(0); 123 | expect(mapper.getU(1)).to.equal(1); 124 | expect(mapper.getU(0.1)).to.approximately(0.131273, EPS); 125 | expect(mapper.getU(0.2)).to.approximately(0.206082, EPS); 126 | expect(mapper.getU(0.3)).to.approximately(0.264353, EPS); 127 | expect(mapper.getU(0.4)).to.approximately(0.320257, EPS); 128 | expect(mapper.getU(0.5)).to.approximately(0.360028, EPS); 129 | expect(mapper.getU(0.6)).to.approximately(0.471680, EPS); 130 | expect(mapper.getU(0.7)).to.approximately(0.609124, EPS); 131 | expect(mapper.getU(0.8)).to.approximately(0.771924, EPS); 132 | expect(mapper.getU(0.9)).to.approximately(0.934861, EPS); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /src/core/spline-segment.ts: -------------------------------------------------------------------------------- 1 | import { NumArray4, Vector, CurveParameters, SegmentFunction } from "./interfaces"; 2 | import { EPS, getCubicRoots, sumOfSquares } from "./math"; 3 | import { clamp } from "./utils"; 4 | 5 | 6 | 7 | /** 8 | * This function will calculate the knot sequence, based on a given value for alpha, for a set of 9 | * control points for a curve segment. It is used to calculate the velocity vectors, which 10 | * determines the curvature of the segment. Alpha=0.5 produces a centripetal curve, while 11 | * alpha=1 produces a chordal curve. 12 | * @param p0 First control point 13 | * @param p1 Second control point 14 | * @param p2 Third control point 15 | * @param p3 Fourth control point 16 | * @param alpha alpha value 17 | * @returns calculated knot sequence to use for curve velocity vector calculations 18 | */ 19 | export function calcKnotSequence(p0 : Vector, p1: Vector, p2: Vector, p3: Vector, alpha = 0) : NumArray4 { 20 | if (alpha === 0) return [0, 1, 2, 3]; 21 | 22 | const deltaT = (u: Vector, v: Vector) : number => Math.pow(sumOfSquares(u, v), 0.5 * alpha); 23 | 24 | const t1 = deltaT(p1, p0); 25 | const t2 = deltaT(p2, p1) + t1; 26 | const t3 = deltaT(p3, p2) + t2; 27 | 28 | return [0, t1, t2, t3]; 29 | } 30 | 31 | /** 32 | * Calculate coefficients for a curve segment with specified parameters 33 | * @param p0 control point 1 34 | * @param p1 control point 2 35 | * @param p2 control point 3 36 | * @param p3 control point 4 37 | * @param options curve parameters 38 | * @returns coefficients for curve function 39 | */ 40 | export function calculateCoefficients(p0: Vector, p1:Vector, p2:Vector, p3:Vector, options: CurveParameters) : NumArray4[] { 41 | const tension = Number.isFinite(options.tension) ? options.tension : 0.5; 42 | const alpha = Number.isFinite(options.alpha) ? options.alpha : null; 43 | const knotSequence = alpha > 0 ? calcKnotSequence(p0, p1, p2, p3, alpha) : null; 44 | const coefficientsList = new Array(p0.length); 45 | 46 | for (let k = 0; k < p0.length; k++) { 47 | let u = 0, v = 0; 48 | const v0 = p0[k], v1 = p1[k], v2 = p2[k], v3 = p3[k]; 49 | if (!knotSequence) { 50 | u = (1 - tension) * (v2 - v0) * 0.5; 51 | v = (1 - tension) * (v3 - v1) * 0.5; 52 | } else { 53 | const [t0, t1, t2, t3] = knotSequence; 54 | if (t1 - t2 !== 0) { 55 | if (t0 - t1 !== 0 && t0 - t2 !== 0) { 56 | u = (1 - tension) * (t2 - t1) * ((v0 - v1) / (t0 - t1) - (v0 - v2) / (t0 - t2) + (v1 - v2) / (t1 - t2)); 57 | } 58 | if (t1 - t3 !== 0 && t2 - t3 !== 0) { 59 | v = (1 - tension) * (t2 - t1) * ((v1 - v2) / (t1 - t2) - (v1 - v3) / (t1 - t3) + (v2 - v3) / (t2 - t3)); 60 | } 61 | } 62 | } 63 | 64 | const a = (2 * v1 - 2 * v2 + u + v); 65 | const b = (-3 * v1 + 3 * v2 - 2 * u - v); 66 | const c = u; 67 | const d = v1; 68 | coefficientsList[k] = [a, b, c, d]; 69 | } 70 | return coefficientsList; 71 | } 72 | 73 | /** 74 | * Calculates vector component for a point along the curve segment at time t 75 | * @param t time along curve segment 76 | * @param coefficients coefficients for curve function 77 | * @returns curve value 78 | */ 79 | export function valueAtT(t: number, coefficients: NumArray4) : number { 80 | const t2 = t * t; 81 | const t3 = t * t2; 82 | const [a, b, c, d] = coefficients; 83 | return a * t3 + b * t2 + c * t + d; 84 | } 85 | 86 | /** 87 | * Calculates vector component for the derivative of the curve segment at time t 88 | * @param t time along curve segment 89 | * @param coefficients coefficients for curve function 90 | * @returns derivative (t') 91 | */ 92 | export function derivativeAtT(t: number, coefficients: NumArray4) : number { 93 | const t2 = t * t; 94 | const [a, b, c] = coefficients; 95 | return 3 * a * t2 + 2 * b * t + c; 96 | } 97 | 98 | /** 99 | * Calculates vector component for the second derivative of the curve segment at time t 100 | * @param t time along curve segment 101 | * @param coefficients coefficients for curve function 102 | * @returns second derivative (t'') 103 | */ 104 | export function secondDerivativeAtT(t: number, coefficients: NumArray4) : number { 105 | const [a, b] = coefficients; 106 | return 6 * a * t + 2 * b; 107 | } 108 | 109 | /** 110 | * Solves the cubic spline equation and return t 111 | * @param lookup target lookup value 112 | * @param coefficients lookup axis coefficients 113 | */ 114 | export function findRootsOfT(lookup: number, coefficients: NumArray4): number[] { 115 | const [a, b, c, d] = coefficients; 116 | const x = d - lookup; 117 | if (a === 0 && b === 0 && c === 0 && x === 0) { 118 | return [0]; // whole segment matches - how to deal with this? 119 | } 120 | const roots = getCubicRoots(a, b, c, x); 121 | return roots.filter(t => t > -EPS && t <= 1 + EPS).map(t => clamp(t, 0, 1)); 122 | } 123 | 124 | /** 125 | * Convenience function for evaluating segment functions for all components of a vector 126 | * @param func SegmentFunction to evaluate 127 | * @param t time along curve segment 128 | * @param coefficients coefficients for curve function (for each component) 129 | * @param target target vector 130 | * @returns vector 131 | */ 132 | export function evaluateForT(func: SegmentFunction, t:number, coefficients: NumArray4[], target: Vector = null) : Vector { 133 | target = target || new Array(coefficients.length); 134 | 135 | for (let k = 0; k < coefficients.length; k++) { 136 | target[k] = func(t, coefficients[k]); 137 | } 138 | 139 | return target; 140 | } 141 | 142 | -------------------------------------------------------------------------------- /src/core/spline-segment.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { calculateCoefficients, derivativeAtT, evaluateForT, secondDerivativeAtT, findRootsOfT, valueAtT } from './spline-segment'; 3 | import { expect } from 'chai'; 4 | import { 5 | compareNumArrays, 6 | } from '../../test/test-utils'; 7 | import { points, points3d } from '../../test/test-data'; 8 | 9 | describe('spline-segment.ts', () => { 10 | it('should find curve coefficients', () => { 11 | let result = calculateCoefficients([2], [3], [4], [5], { tension: 0 }); 12 | compareNumArrays(result[0], [0, 0, 1, 3]); 13 | 14 | result = calculateCoefficients([2], [3], [4], [5], { tension: 0.5 }); 15 | compareNumArrays(result[0], [-1, 1.5, 0.5, 3]); 16 | 17 | result = calculateCoefficients([2], [3], [4], [5], { tension: 1 }); 18 | compareNumArrays(result[0], [-2, 3, 0, 3]); 19 | 20 | result = calculateCoefficients([2], [3], [4], [5], { tension: 0.5 }); 21 | result[0][3] -= 4.5; //if we need to include a target value (lookup) 22 | compareNumArrays(result[0], [-1, 1.5, 0.5, -1.5]); 23 | }); 24 | 25 | it('should be possible to calculate value of spline equation given a value for t', () => { 26 | const [coefficients1] = calculateCoefficients([2], [3], [4], [5], { tension: 0.5 }); 27 | const [coefficients2] = calculateCoefficients([2], [3], [4], [5], { tension: 0.0 }); 28 | 29 | let result = valueAtT(0, coefficients1); 30 | expect(result).to.equal(3); 31 | 32 | result = valueAtT(0.25, coefficients1); 33 | expect(result).to.equal(3.203125); 34 | 35 | result = valueAtT(0.5, coefficients1); 36 | expect(result).to.equal(3.5); 37 | 38 | result = valueAtT(0.75, coefficients1); 39 | expect(result).to.equal(3.796875); 40 | 41 | result = valueAtT(0.25, coefficients2); 42 | expect(result).to.equal(3.25); 43 | 44 | result = valueAtT(0.5, coefficients2); 45 | expect(result).to.equal(3.5); 46 | 47 | result = valueAtT(0.75, coefficients2); 48 | expect(result).to.equal(3.75); 49 | 50 | result = valueAtT(1.0, coefficients2); 51 | expect(result).to.equal(4); 52 | }); 53 | 54 | it('should be possible to calculate value of the derivative of a curve equation given a value for t', () => { 55 | const [coefficients1] = calculateCoefficients([2], [3], [4], [5], { tension: 0.5 }); 56 | const [coefficients2] = calculateCoefficients([2], [3], [4], [5], { tension: 0.0 }); 57 | 58 | let result = derivativeAtT(0, coefficients1); 59 | expect(result).to.equal(0.5); 60 | 61 | result = derivativeAtT(0.25, coefficients1); 62 | expect(result).to.equal(1.0625); 63 | 64 | result = derivativeAtT(0.5, coefficients1); 65 | expect(result).to.equal(1.25); 66 | 67 | result = derivativeAtT(0.75, coefficients1); 68 | expect(result).to.equal(1.0625); 69 | 70 | result = derivativeAtT(0.25, coefficients2); 71 | expect(result).to.equal(1); 72 | 73 | result = derivativeAtT(0.5, coefficients2); 74 | expect(result).to.equal(1); 75 | 76 | result = derivativeAtT(0.75, coefficients2); 77 | expect(result).to.equal(1); 78 | 79 | result = derivativeAtT(1.0, coefficients1); 80 | expect(result).to.equal(0.5); 81 | }); 82 | 83 | it('should be possible to calculate value of the second derivative of a curve equation given a value for t', () => { 84 | const [coefficients1] = calculateCoefficients([2], [3], [4], [5], { tension: 0.5 }); 85 | const [coefficients2] = calculateCoefficients([2], [3], [4], [5], { tension: 0.0 }); 86 | 87 | let result = secondDerivativeAtT(0, coefficients1); 88 | expect(result).to.equal(3); 89 | 90 | result = secondDerivativeAtT(0.25, coefficients1); 91 | expect(result).to.equal(1.5); 92 | 93 | result = secondDerivativeAtT(0.5, coefficients1); 94 | expect(result).to.equal(0); 95 | 96 | result = secondDerivativeAtT(0.75, coefficients1); 97 | expect(result).to.equal(-1.5); 98 | 99 | result = secondDerivativeAtT(0.25, coefficients2); 100 | expect(result).to.equal(0); 101 | 102 | result = secondDerivativeAtT(0.5, coefficients2); 103 | expect(result).to.equal(0); 104 | 105 | result = secondDerivativeAtT(0.75, coefficients2); 106 | expect(result).to.equal(0); 107 | 108 | result = secondDerivativeAtT(1.0, coefficients1); 109 | expect(result).to.equal(-3); 110 | }); 111 | 112 | it('should be possible to execute spline functions for multi-dimensional vectors', () => { 113 | const [p0, p1, p2, p3] = points.slice(0, 4); 114 | const [q0, q1, q2, q3] = points3d.slice(0,4); 115 | 116 | const coefficients2d = calculateCoefficients(p0, p1, p2, p3, { tension: 0, alpha: 0.5 }); 117 | const coefficients3d = calculateCoefficients(q0, q1, q2, q3, { tension: 0, alpha: 0.5 }); 118 | 119 | expect(coefficients2d.length).to.eq(2); 120 | expect(coefficients3d.length).to.eq(3); 121 | 122 | let result = evaluateForT(valueAtT, 0.5, coefficients2d); 123 | compareNumArrays(result, [2.20, 11.43], 0.01); 124 | 125 | result = evaluateForT(derivativeAtT, 0.5, coefficients2d); 126 | compareNumArrays(result, [0.34, -2.96], 0.01); 127 | 128 | result = evaluateForT(secondDerivativeAtT, 0.5, coefficients2d); 129 | compareNumArrays(result, [0.40, 0.60], 0.01); 130 | 131 | result = evaluateForT(valueAtT, 0.5, coefficients3d); 132 | compareNumArrays(result, [0.99, -1.51, 1.56], 0.01); 133 | 134 | result = evaluateForT(derivativeAtT, 0.5, coefficients3d); 135 | compareNumArrays(result, [-0.02, -0.97, 1.38], 0.01); 136 | 137 | result = evaluateForT(secondDerivativeAtT, 0.5, coefficients3d); 138 | compareNumArrays(result, [0.10, 0.11, -0.45], 0.01); 139 | }); 140 | 141 | it('should be able to solve cubic equation and return roots', () => { 142 | const [coefficients] = calculateCoefficients([10], [5], [-5], [-8], { tension: 0, alpha: 0.5 }); 143 | 144 | let result = findRootsOfT(0, coefficients); 145 | compareNumArrays(result, [0.4865], 0.001); 146 | 147 | result = findRootsOfT(2.5, coefficients); 148 | compareNumArrays(result, [0.2598], 0.001); 149 | 150 | result = findRootsOfT(6, coefficients); 151 | expect(result).to.deep.eq([]); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /src/curve-mappers/numerical-curve-mapper.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { expect } from 'chai'; 3 | import { points, points3d } from '../../test/test-data'; 4 | import { NumericalCurveMapper } from './numerical-curve-mapper'; 5 | import { AbstractCurveMapper } from './abstract-curve-mapper'; 6 | import { valueAtT } from '../core/spline-segment'; 7 | 8 | const EPS = 0.001; 9 | 10 | describe('numerical-curve-mapper.ts', () => { 11 | it('should be able to instantiate class', () => { 12 | const mapper = new NumericalCurveMapper(); 13 | expect(mapper).to.not.be.null; 14 | expect(mapper).to.be.instanceOf(AbstractCurveMapper); 15 | }); 16 | 17 | it('should be able to instantiate class with parameters', () => { 18 | const mapper = new NumericalCurveMapper(5, 18); 19 | expect(mapper._gauss).to.deep.eq([[-0.906179845938664,0.23692688505618908],[-0.5384693101056831,0.47862867049936647],[0,0.5688888888888889],[0.5384693101056831,0.47862867049936647],[0.906179845938664,0.23692688505618908]]); 20 | expect(mapper._nSamples).to.eq(18); 21 | }); 22 | 23 | it('should be able to compute arc lengths', () => { 24 | const mapper = new NumericalCurveMapper(); 25 | mapper.tension = 0.5; 26 | mapper.points = points; 27 | 28 | const totalLength = mapper.arcLengths[mapper.arcLengths.length - 1]; 29 | expect(totalLength).to.be.closeTo(56.6333, EPS); 30 | expect(mapper.lengthAt(1)).to.eq(totalLength); 31 | expect(mapper.lengthAt(0.25)).to.be.closeTo(totalLength * 0.25, EPS); 32 | expect(mapper.lengthAt(0.5)).to.be.closeTo(totalLength * 0.5, EPS); 33 | expect(mapper.lengthAt(0.75)).to.be.closeTo(totalLength * 0.75, EPS); 34 | 35 | // setting tension to 0 should result in a longer curve, invalidating the existing cached arcLengths 36 | mapper.tension = 0; 37 | expect(mapper.arcLengths[mapper.arcLengths.length - 1]).to.be.greaterThan(totalLength); 38 | }); 39 | 40 | it('should be able to compute samples for inverse function', () => { 41 | const mapper = new NumericalCurveMapper(); 42 | mapper.tension = 0.0; 43 | mapper.alpha = 0.5; 44 | mapper.points = points; 45 | 46 | const [lengths, slopes, cis, dis] = mapper.getSamples(1); 47 | expect(lengths.length).to.eq(mapper._nSamples); 48 | expect(slopes.length).to.eq(mapper._nSamples); 49 | expect(cis.length).to.eq(mapper._nSamples - 1); 50 | expect(dis.length).to.eq(mapper._nSamples - 1); 51 | }); 52 | 53 | it('should be able to convert between t and u - 2d', () => { 54 | const mapper = new NumericalCurveMapper(); 55 | mapper.tension = 0.5; 56 | mapper.points = points; 57 | 58 | expect(mapper.getT(0)).to.eq(0); 59 | expect(mapper.getT(1)).to.eq(1); 60 | expect(mapper.getU(0)).to.eq(0); 61 | expect(mapper.getU(1)).to.eq(1); 62 | 63 | expect(mapper.getT(0.82)).to.not.be.NaN; 64 | for (let i = 0; i <= 100; i += 1) { 65 | const u = Math.random(); 66 | const t = mapper.getT(u); 67 | expect(mapper.getU(t)).to.be.closeTo(u, EPS); 68 | } 69 | 70 | }); 71 | 72 | it('should be able to convert between t and u - 3d', () => { 73 | const mapper = new NumericalCurveMapper(); 74 | mapper.tension = 0.5; 75 | mapper.points = points3d; 76 | 77 | expect(mapper.getT(0)).to.eq(0); 78 | expect(mapper.getT(1)).to.eq(1); 79 | expect(mapper.getU(0)).to.eq(0); 80 | expect(mapper.getU(1)).to.eq(1); 81 | 82 | for (let i = 0; i <= 100; i += 1) { 83 | const u = Math.random(); 84 | const t = mapper.getT(u); 85 | expect(mapper.getU(t)).to.be.closeTo(u, 0.001); 86 | } 87 | 88 | }); 89 | 90 | it('should be able to divide a curve into segments and estimate each segments length', () => { 91 | const mapper = new NumericalCurveMapper(); 92 | mapper.tension = 0; 93 | mapper.points = points; 94 | 95 | const arcLengths = mapper.computeArcLengths(); 96 | 97 | expect(arcLengths.length).to.equal(points.length); 98 | expect(arcLengths[0]).to.equal(0); 99 | expect(arcLengths[arcLengths.length - 1]).to.be.approximately(57.816979, EPS); 100 | }); 101 | 102 | it('should be able to divide a 3d curve into segments and estimate each segments length', () => { 103 | const mapper = new NumericalCurveMapper(); 104 | mapper.tension = 0; 105 | mapper.points = points3d; 106 | 107 | const arcLengths = mapper.computeArcLengths(); 108 | 109 | expect(arcLengths.length).to.equal(points3d.length); 110 | expect(arcLengths[0]).to.equal(0); 111 | expect(arcLengths[arcLengths.length - 1]).to.be.approximately(24.173807, EPS); 112 | }); 113 | 114 | it('should be able to map between t and u indexes', () => { 115 | const mapper =new NumericalCurveMapper(); 116 | mapper.tension = 0; 117 | mapper.points = points; 118 | 119 | expect(mapper.getT(0)).to.equal(0); 120 | expect(mapper.getT(1)).to.equal(1); 121 | expect(mapper.getT(0.1)).to.approximately(0.065657, EPS); 122 | expect(mapper.getT(0.2)).to.approximately(0.188452, EPS); 123 | expect(mapper.getT(0.3)).to.approximately(0.364337, EPS); 124 | expect(mapper.getT(0.4)).to.approximately(0.544511, EPS); 125 | expect(mapper.getT(0.5)).to.approximately(0.625298, EPS); 126 | expect(mapper.getT(0.6)).to.approximately(0.695084, EPS); 127 | expect(mapper.getT(0.7)).to.approximately(0.758899, EPS); 128 | expect(mapper.getT(0.8)).to.approximately(0.810906, EPS); 129 | expect(mapper.getT(0.9)).to.approximately(0.866135, EPS); 130 | }); 131 | 132 | it('should be able to map between u and t indexes', () => { 133 | const mapper =new NumericalCurveMapper(); 134 | mapper.tension = 0; 135 | mapper.points = points; 136 | 137 | expect(mapper.getU(0)).to.equal(0); 138 | expect(mapper.getU(1)).to.equal(1); 139 | expect(mapper.getU(0.1)).to.approximately(0.131242, EPS); 140 | expect(mapper.getU(0.2)).to.approximately(0.206059, EPS); 141 | expect(mapper.getU(0.3)).to.approximately(0.264334, EPS); 142 | expect(mapper.getU(0.4)).to.approximately(0.320241, EPS); 143 | expect(mapper.getU(0.5)).to.approximately(0.360005, EPS); 144 | expect(mapper.getU(0.6)).to.approximately(0.471656, EPS); 145 | expect(mapper.getU(0.7)).to.approximately(0.609148, EPS); 146 | expect(mapper.getU(0.8)).to.approximately(0.771937, EPS); 147 | expect(mapper.getU(0.9)).to.approximately(0.934866, EPS); 148 | }); 149 | 150 | it('should not fail with tension = 1 for dataset 1 (generating sharp slopes at endpoints)', () => { 151 | const points = [[1,6],[2,2],[3.6,2],[4,7],[5.8,7],[6,1],[10,9]]; 152 | const mapper = new NumericalCurveMapper(); 153 | mapper.points = points; 154 | mapper.tension = 1; 155 | 156 | for (let u = 0.01; u < 0.99; u+=0.01) { 157 | const t = mapper.getT(u); 158 | expect(t).to.be.greaterThan(0); 159 | expect(t).to.be.lessThan(1); 160 | } 161 | 162 | for (let i = 0; i < mapper.points.length; i++) { 163 | const t = i / (mapper.points.length - 1); 164 | expect(mapper.evaluateForT(valueAtT, t)).to.deep.eq(mapper.points[i]); 165 | } 166 | }); 167 | 168 | it('should not fail with tension = 1 for dataset 2 (generating sharp slopes at endpoints)', () => { 169 | const points = [[0,10],[2,10],[3,10],[5,10],[6,10],[8,10],[9,10.5],[14.653776978417266,34.25],[12.35161870503597,57.61538461538461],[14,60],[15,85]]; 170 | const mapper = new NumericalCurveMapper(); 171 | mapper.points = points; 172 | mapper.tension = 0.99; 173 | 174 | for (let u = 0.01; u < 0.99; u+=0.01) { 175 | const t = mapper.getT(u); 176 | expect(t).to.be.greaterThan(0); 177 | expect(t).to.be.lessThan(1); 178 | } 179 | 180 | for (let i = 0; i < mapper.points.length; i++) { 181 | const t = i / (mapper.points.length - 1); 182 | expect(mapper.evaluateForT(valueAtT, t)).to.deep.eq(mapper.points[i]); 183 | } 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /src/core/math.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Vector, 3 | } from './interfaces'; 4 | import { reduce, fill, map, copyValues } from './utils'; 5 | 6 | export const EPS = Math.pow(2, -42); 7 | 8 | /** 9 | * Take the cube root of a number 10 | * @param x value to return the cube root of 11 | */ 12 | function cuberoot(x: number) : number { 13 | const y = Math.pow(Math.abs(x), 1/3); 14 | return x < 0 ? -y : y; 15 | } 16 | 17 | /** 18 | * Solve 2nd degree equations 19 | * @param a 2nd degree coefficient 20 | * @param b 1st degree coefficient 21 | * @param c constant coefficient 22 | */ 23 | export function getQuadRoots(a: number, b: number, c: number) : number[] { 24 | if (Math.abs(a) < EPS) { // Linear case, ax+b=0 25 | if (Math.abs(b) < EPS) return []; // Degenerate case 26 | return [-c / b]; 27 | } 28 | const D = b * b - 4 * a * c; 29 | if (Math.abs(D) < EPS) return [-b / (2 * a)]; 30 | 31 | if (D > 0) { 32 | return [(-b + Math.sqrt(D)) / (2 * a), (-b - Math.sqrt(D)) / (2 * a)]; 33 | } 34 | return []; 35 | } 36 | /** 37 | * Solve 3rd degree equations 38 | * @param a 3rd degree coefficient 39 | * @param b 2nd degree coefficient 40 | * @param c 1st degree coefficient 41 | * @param d constant coefficient 42 | */ 43 | export function getCubicRoots(a: number, b: number, c: number, d: number) : number[] { 44 | if (Math.abs(a) < EPS) { // Quadratic case, ax^2+bx+c=0 45 | return getQuadRoots(b, c, d); 46 | } 47 | 48 | // Convert to depressed cubic t^3+pt+q = 0 (subst x = t - b/3a) 49 | const p = (3 * a * c - b * b) / (3 * a * a); 50 | const q = (2 * b * b * b - 9 * a * b * c + 27 * a * a * d) / (27 * a * a * a); 51 | let roots : number[]; 52 | 53 | if (Math.abs(p) < EPS) { // p = 0 -> t^3 = -q -> t = -q^1/3 54 | roots = [cuberoot(-q)]; 55 | } else if (Math.abs(q) < EPS) { // q = 0 -> t^3 + pt = 0 -> t(t^2+p)=0 56 | roots = [0].concat(p < 0 ? [Math.sqrt(-p), -Math.sqrt(-p)] : []); 57 | } else { 58 | const D = q * q / 4 + p * p * p / 27; 59 | if (Math.abs(D) < EPS) { // D = 0 -> two roots 60 | roots = [-1.5 * q / p, 3 * q / p]; 61 | } else if (D > 0) { // Only one real root 62 | const u = cuberoot(-q / 2 - Math.sqrt(D)); 63 | roots = [u - p/(3*u)]; 64 | } else { // D < 0, three roots, but needs to use complex numbers/trigonometric solution 65 | const u = 2 * Math.sqrt(-p / 3); 66 | const t = Math.acos(3 * q / p / u) / 3; // D < 0 implies p < 0 and acos argument in [-1..1] 67 | const k = 2 * Math.PI / 3; 68 | roots = [u * Math.cos(t), u * Math.cos(t-k), u * Math.cos(t - 2 * k)]; 69 | } 70 | } 71 | 72 | // Convert back from depressed cubic 73 | for (let i = 0; i < roots.length; i++) { 74 | roots[i] -= b / (3 * a); 75 | } 76 | 77 | return roots; 78 | } 79 | 80 | /** 81 | * Get the dot product of two vectors 82 | * @param v1 Vector 83 | * @param v2 Vector 84 | * @returns Dot product 85 | */ 86 | export function dot(v1:Vector, v2:Vector) : number { 87 | if (v1.length !== v2.length) throw Error('Vectors must be of equal length!'); 88 | let p = 0; 89 | for (let k = 0; k < v1.length; k++) { 90 | p += v1[k] * v2[k]; 91 | } 92 | return p; 93 | } 94 | 95 | /** 96 | * Get the cross product of two 3d vectors. For 2d vectors, we imply the z-component 97 | * is zero and return a 3d vector. The function returns undefined for dimensions > 3. 98 | * @param v1 Vector 99 | * @param v2 Vector 100 | * @param target optional target 101 | * @returns Vector perpendicular to p1 and p2 102 | */ 103 | export function cross(v1:Vector, v2:Vector, target?:Vector) : Vector { 104 | if (v1.length > 3) return undefined; 105 | target = target || new Array(3); 106 | 107 | const ax = v1[0], ay = v1[1], az = v1[2] || 0; 108 | const bx = v2[0], by = v2[1], bz = v2[2] || 0; 109 | 110 | target[0] = ay * bz - az * by; 111 | target[1] = az * bx - ax * bz; 112 | target[2] = ax * by - ay * bx; 113 | 114 | return target; 115 | } 116 | 117 | /** 118 | * Add two vectors 119 | * @param v1 Vector 120 | * @param v2 Vector 121 | * @param target optional target 122 | * @returns Sum of v1 and v2 123 | */ 124 | export function add(v1:Vector, v2:Vector, target?:Vector) : Vector { 125 | target = target || new Array(v1.length); 126 | 127 | for (let k = 0; k < v1.length; k++) { 128 | target[k] = v1[k] + v2[k]; 129 | } 130 | return target; 131 | } 132 | 133 | /** 134 | * Subtract two vectors 135 | * @param v1 Vector 136 | * @param v2 Vector 137 | * @param target optional target 138 | * @returns Difference of v1 and v2 139 | */ 140 | export function sub(v1:Vector, v2:Vector, target?:Vector) : Vector { 141 | target = target || new Array(v1.length); 142 | 143 | for (let k = 0; k < v1.length; k++) { 144 | target[k] = v1[k] - v2[k]; 145 | } 146 | return target; 147 | } 148 | 149 | /** 150 | * Calculate the sum of squares between two points 151 | * @param v1 coordinates of point 1 152 | * @param v2 coordinates of point 2 153 | */ 154 | export function sumOfSquares(v1:Vector, v2:Vector) : number { 155 | let sumOfSquares = 0; 156 | for (let i = 0; i < v1.length; i++) { 157 | sumOfSquares += (v1[i] - v2[i]) * (v1[i] - v2[i]); 158 | } 159 | return sumOfSquares; 160 | } 161 | 162 | /** 163 | * Calculate the magnitude/length of a vector 164 | * @param v coordinates of the vector 165 | */ 166 | export function magnitude(v:Vector) : number { 167 | let sumOfSquares = 0; 168 | for (let i = 0; i < v.length; i++) { 169 | sumOfSquares += (v[i]) * v[i]; 170 | } 171 | return Math.sqrt(sumOfSquares); 172 | } 173 | 174 | /** 175 | * Calculate the distance between two points 176 | * @param p1 coordinates of point 1 177 | * @param p2 coordinates of point 2 178 | * @returns the distance between p1 and p2 179 | */ 180 | export function distance(p1:Vector, p2:Vector) : number { 181 | const sqrs = sumOfSquares(p1, p2); 182 | return sqrs === 0 ? 0 : Math.sqrt(sqrs); 183 | } 184 | 185 | /** 186 | * Normalizes a vector (mutate input) 187 | * @param v input array/vector to normalize 188 | * @param target optional target 189 | * @return normalized vector v 190 | */ 191 | export function normalize(v:Vector, target?: Vector) : Vector { 192 | const u = target ? copyValues(v, target) : v; 193 | const squared = reduce(u, (s, c) => s + c ** 2); 194 | const l = Math.sqrt(squared); 195 | if (l === 0) return fill(u, 0); 196 | 197 | return map(u, c => c / l); 198 | } 199 | 200 | /** 201 | * Rotates a vector 90 degrees to make it orthogonal (mutates input vector) 202 | * @param v vector to rotate 203 | * @param target optional target 204 | */ 205 | export function orthogonal(v:Vector, target?: Vector) : Vector { 206 | if (v.length > 2) throw Error('Only supported for 2d vectors'); 207 | const u = target ? copyValues(v, target) : v; 208 | const x = -u[1]; 209 | u[1] = u[0]; 210 | u[0] = x; 211 | return u; 212 | } 213 | 214 | /** 215 | * Rotate a 2d point at the specified angle around the anchor point (0,0) 216 | * @param vector vector to rotate 217 | * @param anchor anchor point to rotate around 218 | * @param angle angle of rotation in radians 219 | * @param target optional target 220 | * @returns rotated vector 221 | */ 222 | export function rotate2d(vector:Vector, angle = 0, anchor:Vector = [0, 0], target?: Vector) : Vector { 223 | const c = Math.cos(angle); 224 | const s = Math.sin(angle); 225 | 226 | const vx = vector[0] - anchor[0]; 227 | const vy = vector[1] - anchor[1]; 228 | 229 | target = target || vector; 230 | 231 | target[0] = vx * c - vy * s + anchor[0]; 232 | target[1] = vx * s + vy * c + anchor[1]; 233 | 234 | return target; 235 | } 236 | 237 | /** 238 | * Rotate a 3d point around the given axis and angle 239 | * @param vector vector to rotate 240 | * @param axis vector defining the rotation axis 241 | * @param angle angle of rotation in radians 242 | * @param target optional target 243 | * @returns rotated vector 244 | */ 245 | export function rotate3d(vector:Vector, axis:Vector = [0, 1, 0], angle = 0, target?: Vector) : Vector { 246 | const c = Math.cos(angle); 247 | const s = Math.sin(angle); 248 | 249 | const t = 1 - c; 250 | 251 | const vx = vector[0]; 252 | const vy = vector[1]; 253 | const vz = vector[2]; 254 | 255 | const ax = axis[0]; 256 | const ay = axis[1]; 257 | const az = axis[2]; 258 | 259 | const tx = t * ax, ty = t * ay; 260 | 261 | target = target || vector; 262 | 263 | target[0] = (tx * ax + c) * vx + (tx * ay - s * az) * vy + (tx * az + s * ay) * vz; 264 | target[1] = (tx * ay + s * az) * vx + (ty * ay + c) * vy + (ty * az - s * ax) * vz; 265 | target[2] = (tx * az - s * ay) * vx + (ty * az + s * ax) * vy + (t * az * az + c) * vz; 266 | 267 | return target; 268 | } 269 | -------------------------------------------------------------------------------- /src/curve-mappers/numerical-curve-mapper.ts: -------------------------------------------------------------------------------- 1 | import { AbstractCurveMapper } from "./abstract-curve-mapper"; 2 | import { SplineSegmentOptions } from "../core/interfaces"; 3 | import { getGaussianQuadraturePointsAndWeights } from "./gauss"; 4 | import { derivativeAtT, evaluateForT } from "../core/spline-segment"; 5 | import { magnitude } from "../core/math"; 6 | import { binarySearch, clamp } from "../core/utils"; 7 | 8 | export interface CurveLengthCalculationOptions extends SplineSegmentOptions { 9 | /* Gaussian quadrature weights and abscissae */ 10 | gauss?: [number[], number[]]; 11 | /* from t along arc */ 12 | t0?: number, 13 | /* to t along arc */ 14 | t1?: number, 15 | } 16 | 17 | /** 18 | * This curve mapper implementation uses a numerical integration method (Gauss Legendre) 19 | * in order to approximate curve segment lengths. For re-parameterization of the curve 20 | * function in terms of arc length, a number of precalculated lengths (samples) is used 21 | * to fit a monotone piecewise cubic function using the approach suggested here: 22 | * https://stackoverflow.com/questions/35275073/uniform-discretization-of-bezier-curve 23 | */ 24 | export class NumericalCurveMapper extends AbstractCurveMapper { 25 | _nSamples = 21; 26 | _gauss: number[][]; 27 | 28 | /** 29 | * 30 | * @param onInvalidateCache callback function to be invoked when cache is invalidated 31 | * @param nQuadraturePoints the number of Gauss-Legendre Quadrature points to use for arc length approximation 32 | * @param nInverseSamples the number of arc length samples to use to fit an inverse function for calculating t from arc length 33 | */ 34 | constructor(nQuadraturePoints = 24, nInverseSamples = 21, onInvalidateCache?: () => void) { 35 | super(onInvalidateCache); 36 | this._gauss = getGaussianQuadraturePointsAndWeights(nQuadraturePoints); 37 | this._nSamples = nInverseSamples; 38 | } 39 | 40 | /** 41 | * Clear cache 42 | */ 43 | override _invalidateCache() { 44 | super._invalidateCache(); 45 | this._cache['arcLengths'] = null; 46 | this._cache['samples'] = null; 47 | } 48 | 49 | get arcLengths() { 50 | if (!this._cache['arcLengths']) { 51 | this._cache['arcLengths'] = this.computeArcLengths(); 52 | } 53 | return this._cache['arcLengths']; 54 | } 55 | 56 | /** 57 | * Get samples for inverse function from cache if present, otherwise calculate and put 58 | * in cache for re-use. 59 | * @param idx curve segment index 60 | * @returns Lengths, slopes and coefficients for inverse function 61 | */ 62 | getSamples(idx: number) : [number[], number[], number[], number[]] { 63 | if (!this.points) return undefined; 64 | if (!this._cache['samples']) { 65 | this._cache['samples'] = new Map(); 66 | } 67 | if (!this._cache['samples'].has(idx)) { 68 | const samples = this._nSamples; 69 | const lengths: number[] = [], slopes: number[] = []; 70 | const coefficients = this.getCoefficients(idx); 71 | for (let i = 0; i < samples; ++i) { 72 | const ti = i / (samples - 1); 73 | lengths.push(this.computeArcLength(idx, 0.0, ti)); 74 | const dtln = magnitude(evaluateForT(derivativeAtT, ti, coefficients)); 75 | let slope = dtln === 0 ? 0 : 1 / dtln; 76 | // avoid extreme slopes for near linear curve at the segment endpoints (high tension parameter value) 77 | if (this.tension > 0.95) { 78 | slope = clamp(slope, -1, 1); 79 | } 80 | slopes.push(slope); 81 | } 82 | 83 | // Precalculate the cubic interpolant coefficients 84 | const nCoeff = samples - 1; 85 | const dis = []; // degree 3 coefficients 86 | const cis = []; // degree 2 coefficients 87 | let li_prev = lengths[0]; 88 | let tdi_prev = slopes[0]; 89 | const step = 1.0 / nCoeff; 90 | 91 | for (let i = 0; i < nCoeff; ++i) { 92 | const li = li_prev; 93 | li_prev = lengths[i+1]; 94 | const lDiff = li_prev - li; 95 | const tdi = tdi_prev; 96 | const tdi_next = slopes[i+1]; 97 | tdi_prev = tdi_next; 98 | const si = step / lDiff; 99 | const di = (tdi + tdi_next - 2 * si) / (lDiff * lDiff); 100 | const ci = (3 * si - 2 * tdi - tdi_next) / lDiff; 101 | dis.push(di); 102 | cis.push(ci); 103 | } 104 | 105 | this._cache['samples'].set(idx, [lengths, slopes, cis, dis]); 106 | } 107 | return this._cache['samples'].get(idx); 108 | } 109 | 110 | /** 111 | * Computes the arc length of a curve segment 112 | * @param index index of curve segment 113 | * @param t0 calculate length from t 114 | * @param t1 calculate length to t 115 | * @returns arc length between t0 and t1 116 | */ 117 | computeArcLength(index: number, t0 = 0.0, t1 = 1.0) : number { 118 | if (t0 === t1) return 0; 119 | 120 | const coefficients = this.getCoefficients(index); 121 | const z = (t1 - t0) * 0.5; 122 | 123 | let sum = 0; 124 | for (let i = 0; i < this._gauss.length; i++ ) { 125 | const [T, C] = this._gauss[i]; 126 | const t = z * T + z + t0; 127 | const dtln = magnitude(evaluateForT(derivativeAtT, t, coefficients)); 128 | sum += C * dtln; 129 | } 130 | return z * sum; 131 | } 132 | 133 | /** 134 | * Calculate a running sum of arc length for mapping a position on the curve (u) 135 | * to the position at the corresponding curve segment (t). 136 | * @returns array with accumulated curve segment arc lengths 137 | */ 138 | computeArcLengths() : number[] { 139 | if (!this.points) return undefined; 140 | const lengths = []; 141 | lengths.push(0); 142 | 143 | const nPoints = this.closed ? this.points.length : this.points.length - 1; 144 | let tl = 0; 145 | for (let i = 0; i < nPoints; i++) { 146 | const length = this.computeArcLength(i); 147 | tl += length; 148 | lengths.push(tl); 149 | } 150 | return lengths; 151 | } 152 | 153 | /** 154 | * Calculate t from arc length for a curve segment 155 | * @param idx segment index 156 | * @param len length 157 | * @returns time (t) along curve segment matching the input length 158 | */ 159 | inverse(idx: number, len: number) : number { 160 | const nCoeff = this._nSamples - 1; 161 | const step = 1.0 / nCoeff; 162 | const [lengths, slopes, cis, dis] = this.getSamples(idx); 163 | const length = lengths[lengths.length - 1]; 164 | 165 | if (len >= length) { 166 | return 1.0; 167 | } 168 | 169 | if (len <= 0) { 170 | return 0.0; 171 | } 172 | 173 | // Find the cubic segment which has 'len' 174 | const i = Math.max(0, binarySearch(len, lengths)); 175 | const ti = i * step; 176 | if (lengths[i] === len) { 177 | return ti; 178 | } 179 | const tdi = slopes[i]; 180 | const di = dis[i]; 181 | const ci = cis[i]; 182 | const ld = len - lengths[i]; 183 | 184 | return ((di * ld + ci) * ld + tdi) * ld + ti; 185 | } 186 | 187 | /** 188 | * Get curve length at u 189 | * @param u normalized uniform position along the spline curve 190 | * @returns length in point coordinates 191 | */ 192 | lengthAt(u: number) : number { 193 | return u * this.arcLengths[this.arcLengths.length - 1]; 194 | } 195 | 196 | /** 197 | * Maps a uniform time along the curve to non-uniform time (t) 198 | * @param u normalized uniform position along the spline curve 199 | * @returns t encoding segment index and local time along curve 200 | */ 201 | getT(u: number) : number { 202 | const arcLengths = this.arcLengths; 203 | const il = arcLengths.length; 204 | const targetArcLength = u * arcLengths[il - 1]; 205 | 206 | const i = binarySearch(targetArcLength, arcLengths); 207 | const ti = i / (il - 1); 208 | if (arcLengths[i] === targetArcLength) { 209 | return ti; 210 | } 211 | 212 | const len = targetArcLength - arcLengths[i]; 213 | const fraction = this.inverse(i, len); 214 | return (i + fraction) / (il - 1); 215 | } 216 | 217 | /** 218 | * Maps a non-uniform time along the curve to uniform time (u) 219 | * @param t non-uniform time along curve 220 | * @returns uniform time along curve 221 | */ 222 | getU(t: number) : number { 223 | if (t === 0) return 0; 224 | if (t === 1) return 1; 225 | 226 | const arcLengths = this.arcLengths; 227 | const al = arcLengths.length - 1; 228 | const totalLength = arcLengths[al]; 229 | 230 | // need to de-normalize t to find the matching length 231 | const tIdx = t * al; 232 | 233 | const subIdx = Math.floor(tIdx); 234 | const l1 = arcLengths[subIdx]; 235 | 236 | if (tIdx === subIdx) return l1 / totalLength; 237 | 238 | const t0 = tIdx - subIdx; 239 | const fraction = this.computeArcLength(subIdx, 0, t0); 240 | 241 | return (l1 + fraction) / totalLength; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /docs/types/_internal_.Vector.html: -------------------------------------------------------------------------------- 1 | Vector | curve-interpolator
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Type alias Vector

19 |
Vector: number[] | VectorType
20 |

Either a number array or an object implementing the VectorType interface

21 |
24 |
58 |
59 |

Generated using TypeDoc

60 |
-------------------------------------------------------------------------------- /docs/types/_internal_.NumArray4.html: -------------------------------------------------------------------------------- 1 | NumArray4 | curve-interpolator
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Type alias NumArray4

19 |
NumArray4: [number, number, number, number]
20 |

Array of four number items

21 |
24 |
58 |
59 |

Generated using TypeDoc

60 |
-------------------------------------------------------------------------------- /docs/interfaces/_internal_.BBox.html: -------------------------------------------------------------------------------- 1 | BBox | curve-interpolator
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Interface BBox

19 |
20 |

Bounding box interface

21 |
22 |
23 |

Hierarchy

24 |
    25 |
  • BBox
28 |
29 |
30 |
31 | 32 |
33 |
34 |

Properties

35 |
max 36 | min 37 |
38 |
39 |

Properties

40 |
41 | 42 |
max: Vector
45 |
46 | 47 |
min: Vector
50 |
78 |
79 |

Generated using TypeDoc

80 |
-------------------------------------------------------------------------------- /docs/interfaces/_internal_.CurveParameters.html: -------------------------------------------------------------------------------- 1 | CurveParameters | curve-interpolator
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Interface CurveParameters

19 |
20 |

Hierarchy

21 |
27 |
28 |
29 |
30 | 31 |
32 |
33 |

Properties

34 |
alpha? 35 | tension? 36 |
37 |
38 |

Properties

39 |
40 | 41 |
alpha?: number
44 |
45 | 46 |
tension?: number
49 |
77 |
78 |

Generated using TypeDoc

79 |
-------------------------------------------------------------------------------- /docs/types/_internal_.SegmentFunction.html: -------------------------------------------------------------------------------- 1 | SegmentFunction | curve-interpolator
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Type alias SegmentFunction

19 |
SegmentFunction: ((t: number, coefficients: NumArray4) => number)
20 |
21 |

Type declaration

22 |
    23 |
  • 24 |
      25 |
    • (t: number, coefficients: NumArray4): number
    • 26 |
    • 27 |
      28 |

      Parameters

      29 |
        30 |
      • 31 |
        t: number
      • 32 |
      • 33 |
        coefficients: NumArray4
      34 |

      Returns number

37 |
71 |
72 |

Generated using TypeDoc

73 |
-------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | curve-interpolator
2 |
3 | 10 |
11 |
12 |
13 |
14 |

curve-interpolator

15 |

16 |

17 | 18 | 19 |

Curve Interpolator

20 |
21 |

A lib for interpolating values over a cubic Cardinal/Catmull-Rom spline curve of n-dimenesions.

22 | 23 | 24 |

Installation

25 |
26 |
npm install --save curve-interpolator
27 | 
28 | 29 | 30 |

Basic usage

31 |
32 |

Reference the CurveInterpolator class:

33 |
// commonjs
const CurveInterpolator = require('curve-interpolator').CurveInterpolator;

// es6
import { CurveInterpolator } from 'curve-interpolator';
34 |
35 |

Define controlpoints you want the curve to pass through and pass it to the constructor of the CurveInterpolator to create an instance:

36 |
const points = [
[0, 4],
[1, 2],
[3, 6.5],
[4, 8],
[5.5, 4],
[7, 3],
[8, 0],
...
];

const interp = new CurveInterpolator(points, { tension: 0.2, alpha: 0.5 });

// get single point
const position = 0.3 // [0 - 1]
const pt = interp.getPointAt(position)

// get points evently distributed along the curve
const segments = 1000;
const pts = interp.getPoints(segments);

// lookup values along x and y axises
const axis = 1;
const yintersects = interp.getIntersects(7, axis);

/*
max number of solutions (0 = all (default), 1 = first, -1 = last)
A negative max value counts solutions from end of curve
*/
const axis = 0;
const max = -1;
const xintersects = interp.getIntersects(3.2, axis, max);

// get bounding box
const bbox = interp.getBoundingBox(); 37 |
38 |

Online example on ObservableHQ:

39 | 42 | 43 | 44 |

Docs

45 |
46 |

Docs are generated using typedoc in ./docs. To create:

47 |
npm run docs
48 | 
49 |

Online: https://kjerandp.github.io/curve-interpolator/

50 | 51 | 52 |

License

53 |
54 |

MIT

55 |
56 |
78 |
79 |

Generated using TypeDoc

80 |
-------------------------------------------------------------------------------- /docs/interfaces/_internal_.SplineCurveOptions.html: -------------------------------------------------------------------------------- 1 | SplineCurveOptions | curve-interpolator
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Interface SplineCurveOptions

19 |
20 |

Spline Curve characteristics

21 |
22 |
23 |

Hierarchy

24 |
32 |
33 |
34 |
35 | 36 |
37 |
38 |

Properties

39 |
alpha? 40 | closed? 41 | tension? 42 |
43 |
44 |

Properties

45 |
46 | 47 |
alpha?: number
51 |
52 | 53 |
closed?: boolean
56 |
57 | 58 |
tension?: number
62 |
91 |
92 |

Generated using TypeDoc

93 |
-------------------------------------------------------------------------------- /docs/modules/_internal_.html: -------------------------------------------------------------------------------- 1 | <internal> | curve-interpolator
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Module <internal>

18 |
19 |
20 |
21 |
22 |

Index

23 |
24 |

Classes

25 |
27 |
28 |

Interfaces

29 |
36 |
37 |

Type Aliases

38 |
42 |
76 |
77 |

Generated using TypeDoc

78 |
-------------------------------------------------------------------------------- /docs/variables/EPS.html: -------------------------------------------------------------------------------- 1 | EPS | curve-interpolator
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Variable EPSConst

18 |
EPS: number = ...
21 |
80 |
81 |

Generated using TypeDoc

82 |
-------------------------------------------------------------------------------- /src/curve-mappers/gauss.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Lookup tables for gaussian quadrature 3 | */ 4 | const lut = [[[-0.906179845938664,0.23692688505618908],[-0.5384693101056831,0.47862867049936647],[0,0.5688888888888889],[0.5384693101056831,0.47862867049936647],[0.906179845938664,0.23692688505618908]],[[-0.932469514203152,0.17132449237917036],[-0.6612093864662645,0.3607615730481386],[-0.2386191860831969,0.46791393457269104],[0.2386191860831969,0.46791393457269104],[0.6612093864662645,0.3607615730481386],[0.932469514203152,0.17132449237917036]],[[-0.9491079123427585,0.1294849661688697],[-0.7415311855993945,0.27970539148927664],[-0.4058451513773972,0.3818300505051189],[0,0.4179591836734694],[0.4058451513773972,0.3818300505051189],[0.7415311855993945,0.27970539148927664],[0.9491079123427585,0.1294849661688697]],[[-0.9602898564975363,0.10122853629037626],[-0.7966664774136267,0.22238103445337448],[-0.525532409916329,0.31370664587788727],[-0.1834346424956498,0.362683783378362],[0.1834346424956498,0.362683783378362],[0.525532409916329,0.31370664587788727],[0.7966664774136267,0.22238103445337448],[0.9602898564975363,0.10122853629037626]],[[-0.9681602395076261,0.08127438836157441],[-0.8360311073266358,0.1806481606948574],[-0.6133714327005904,0.26061069640293544],[-0.3242534234038089,0.31234707704000286],[0,0.3302393550012598],[0.3242534234038089,0.31234707704000286],[0.6133714327005904,0.26061069640293544],[0.8360311073266358,0.1806481606948574],[0.9681602395076261,0.08127438836157441]],[[-0.9739065285171717,0.06667134430868814],[-0.8650633666889845,0.1494513491505806],[-0.6794095682990244,0.21908636251598204],[-0.4333953941292472,0.26926671930999635],[-0.14887433898163122,0.29552422471475287],[0.14887433898163122,0.29552422471475287],[0.4333953941292472,0.26926671930999635],[0.6794095682990244,0.21908636251598204],[0.8650633666889845,0.1494513491505806],[0.9739065285171717,0.06667134430868814]],[[-0.978228658146056,0.0556685671161736],[-0.887062599768095,0.125580369464904],[-0.730152005574049,0.186290210927734],[-0.519096129206811,0.23319376459199],[-0.269543155952344,0.262804544510246],[0,0.2729250867779],[0.269543155952344,0.262804544510246],[0.519096129206811,0.23319376459199],[0.730152005574049,0.186290210927734],[0.887062599768095,0.125580369464904],[0.978228658146056,0.0556685671161736]],[[-0.981560634246719,0.0471753363865118],[-0.904117256370474,0.106939325995318],[-0.769902674194304,0.160078328543346],[-0.587317954286617,0.203167426723065],[-0.36783149899818,0.233492536538354],[-0.125233408511468,0.249147045813402],[0.125233408511468,0.249147045813402],[0.36783149899818,0.233492536538354],[0.587317954286617,0.203167426723065],[0.769902674194304,0.160078328543346],[0.904117256370474,0.106939325995318],[0.981560634246719,0.0471753363865118]],[[-0.984183054718588,0.0404840047653158],[-0.917598399222977,0.0921214998377284],[-0.801578090733309,0.138873510219787],[-0.64234933944034,0.178145980761945],[-0.448492751036446,0.207816047536888],[-0.230458315955134,0.226283180262897],[0,0.232551553230873],[0.230458315955134,0.226283180262897],[0.448492751036446,0.207816047536888],[0.64234933944034,0.178145980761945],[0.801578090733309,0.138873510219787],[0.917598399222977,0.0921214998377284],[0.984183054718588,0.0404840047653158]],[[-0.986283808696812,0.0351194603317518],[-0.928434883663573,0.0801580871597602],[-0.827201315069764,0.121518570687903],[-0.687292904811685,0.157203167158193],[-0.515248636358154,0.185538397477937],[-0.319112368927889,0.205198463721295],[-0.108054948707343,0.215263853463157],[0.108054948707343,0.215263853463157],[0.319112368927889,0.205198463721295],[0.515248636358154,0.185538397477937],[0.687292904811685,0.157203167158193],[0.827201315069764,0.121518570687903],[0.928434883663573,0.0801580871597602],[0.986283808696812,0.0351194603317518]],[[-0.987992518020485,0.0307532419961172],[-0.937273392400705,0.0703660474881081],[-0.848206583410427,0.107159220467171],[-0.72441773136017,0.139570677926154],[-0.570972172608538,0.166269205816993],[-0.394151347077563,0.186161000015562],[-0.201194093997434,0.198431485327111],[0,0.202578241925561],[0.201194093997434,0.198431485327111],[0.394151347077563,0.186161000015562],[0.570972172608538,0.166269205816993],[0.72441773136017,0.139570677926154],[0.848206583410427,0.107159220467171],[0.937273392400705,0.0703660474881081],[0.987992518020485,0.0307532419961172]],[[-0.989400934991649,0.027152459411754],[-0.944575023073232,0.0622535239386478],[-0.865631202387831,0.0951585116824927],[-0.755404408355003,0.124628971255533],[-0.617876244402643,0.149595988816576],[-0.458016777657227,0.169156519395002],[-0.281603550779258,0.182603415044923],[-0.0950125098376374,0.189450610455068],[0.0950125098376374,0.189450610455068],[0.281603550779258,0.182603415044923],[0.458016777657227,0.169156519395002],[0.617876244402643,0.149595988816576],[0.755404408355003,0.124628971255533],[0.865631202387831,0.0951585116824927],[0.944575023073232,0.0622535239386478],[0.989400934991649,0.027152459411754]],[[-0.990575475314417,0.0241483028685479],[-0.950675521768767,0.0554595293739872],[-0.880239153726985,0.0850361483171791],[-0.781514003896801,0.111883847193403],[-0.65767115921669,0.135136368468525],[-0.512690537086476,0.15404576107681],[-0.351231763453876,0.16800410215645],[-0.178484181495847,0.176562705366992],[0,0.179446470356206],[0.178484181495847,0.176562705366992],[0.351231763453876,0.16800410215645],[0.512690537086476,0.15404576107681],[0.65767115921669,0.135136368468525],[0.781514003896801,0.111883847193403],[0.880239153726985,0.0850361483171791],[0.950675521768767,0.0554595293739872],[0.990575475314417,0.0241483028685479]],[[-0.99156516842093,0.0216160135264833],[-0.955823949571397,0.0497145488949698],[-0.892602466497555,0.076425730254889],[-0.803704958972523,0.100942044106287],[-0.691687043060353,0.122555206711478],[-0.559770831073947,0.14064291467065],[-0.411751161462842,0.154684675126265],[-0.251886225691505,0.164276483745832],[-0.0847750130417353,0.169142382963143],[0.0847750130417353,0.169142382963143],[0.251886225691505,0.164276483745832],[0.411751161462842,0.154684675126265],[0.559770831073947,0.14064291467065],[0.691687043060353,0.122555206711478],[0.803704958972523,0.100942044106287],[0.892602466497555,0.076425730254889],[0.955823949571397,0.0497145488949697],[0.99156516842093,0.0216160135264833]],[[-0.992406843843584,0.0194617882297264],[-0.96020815213483,0.0448142267656996],[-0.903155903614817,0.0690445427376412],[-0.822714656537142,0.0914900216224499],[-0.720966177335229,0.111566645547333],[-0.600545304661681,0.128753962539336],[-0.46457074137596,0.142606702173606],[-0.316564099963629,0.152766042065859],[-0.160358645640225,0.158968843393954],[0,0.161054449848783],[0.160358645640225,0.158968843393954],[0.316564099963629,0.152766042065859],[0.46457074137596,0.142606702173606],[0.600545304661681,0.128753962539336],[0.720966177335229,0.111566645547333],[0.822714656537142,0.0914900216224499],[0.903155903614817,0.0690445427376412],[0.96020815213483,0.0448142267656996],[0.992406843843584,0.0194617882297264]],[[-0.993128599185094,0.0176140071391521],[-0.963971927277913,0.0406014298003869],[-0.912234428251325,0.062672048334109],[-0.839116971822218,0.0832767415767047],[-0.74633190646015,0.10193011981724],[-0.636053680726515,0.118194531961518],[-0.510867001950827,0.131688638449176],[-0.373706088715419,0.142096109318382],[-0.227785851141645,0.149172986472603],[-0.0765265211334973,0.152753387130725],[0.0765265211334973,0.152753387130725],[0.227785851141645,0.149172986472603],[0.373706088715419,0.142096109318382],[0.510867001950827,0.131688638449176],[0.636053680726515,0.118194531961518],[0.74633190646015,0.10193011981724],[0.839116971822218,0.0832767415767047],[0.912234428251325,0.062672048334109],[0.963971927277913,0.0406014298003869],[0.993128599185094,0.0176140071391521]],[[-0.993752170620389,0.0160172282577743],[-0.967226838566306,0.0369537897708524],[-0.9200993341504,0.0571344254268572],[-0.853363364583317,0.0761001136283793],[-0.768439963475677,0.0934444234560338],[-0.667138804197412,0.108797299167148],[-0.551618835887219,0.121831416053728],[-0.424342120207438,0.132268938633337],[-0.288021316802401,0.139887394791073],[-0.145561854160895,0.14452440398997],[0,0.14608113364969],[0.145561854160895,0.14452440398997],[0.288021316802401,0.139887394791073],[0.424342120207438,0.132268938633337],[0.551618835887219,0.121831416053728],[0.667138804197412,0.108797299167148],[0.768439963475677,0.0934444234560338],[0.853363364583317,0.0761001136283793],[0.9200993341504,0.0571344254268572],[0.967226838566306,0.0369537897708524],[0.993752170620389,0.0160172282577743]],[[-0.994294585482399,0.0146279952982722],[-0.970060497835428,0.0337749015848141],[-0.926956772187174,0.0522933351526832],[-0.8658125777203,0.0697964684245204],[-0.787816805979208,0.0859416062170677],[-0.694487263186682,0.10041414444288],[-0.587640403506911,0.112932296080539],[-0.469355837986757,0.123252376810512],[-0.341935820892084,0.131173504787062],[-0.207860426688221,0.136541498346015],[-0.0697392733197222,0.139251872855631],[0.0697392733197222,0.139251872855631],[0.207860426688221,0.136541498346015],[0.341935820892084,0.131173504787062],[0.469355837986757,0.123252376810512],[0.587640403506911,0.112932296080539],[0.694487263186682,0.10041414444288],[0.787816805979208,0.0859416062170677],[0.8658125777203,0.0697964684245204],[0.926956772187174,0.0522933351526832],[0.970060497835428,0.0337749015848141],[0.994294585482399,0.0146279952982722]],[[-0.994769334997552,0.0134118594871417],[-0.972542471218115,0.0309880058569794],[-0.932971086826016,0.0480376717310846],[-0.876752358270441,0.0642324214085258],[-0.804888401618839,0.0792814117767189],[-0.71866136313195,0.0929157660600351],[-0.619609875763646,0.104892091464541],[-0.509501477846007,0.114996640222411],[-0.39030103803029,0.123049084306729],[-0.264135680970344,0.128905722188082],[-0.133256824298466,0.132462039404696],[0,0.133654572186106],[0.133256824298466,0.132462039404696],[0.264135680970344,0.128905722188082],[0.39030103803029,0.123049084306729],[0.509501477846007,0.114996640222411],[0.619609875763646,0.104892091464541],[0.71866136313195,0.0929157660600351],[0.804888401618839,0.0792814117767189],[0.876752358270441,0.0642324214085258],[0.932971086826016,0.0480376717310846],[0.972542471218115,0.0309880058569794],[0.994769334997552,0.0134118594871417]],[[-0.995187219997021,0.0123412297999872],[-0.974728555971309,0.0285313886289336],[-0.938274552002732,0.0442774388174198],[-0.886415527004401,0.0592985849154367],[-0.820001985973902,0.0733464814110803],[-0.740124191578554,0.0861901615319532],[-0.648093651936975,0.0976186521041138],[-0.545421471388839,0.107444270115965],[-0.433793507626045,0.115505668053725],[-0.315042679696163,0.121670472927803],[-0.191118867473616,0.125837456346828],[-0.0640568928626056,0.127938195346752],[0.0640568928626056,0.127938195346752],[0.191118867473616,0.125837456346828],[0.315042679696163,0.121670472927803],[0.433793507626045,0.115505668053725],[0.545421471388839,0.107444270115965],[0.648093651936975,0.0976186521041138],[0.740124191578554,0.0861901615319532],[0.820001985973902,0.0733464814110803],[0.886415527004401,0.0592985849154367],[0.938274552002732,0.0442774388174198],[0.974728555971309,0.0285313886289336],[0.995187219997021,0.0123412297999872]],[[-0.995556969790498,0.0113937985010262],[-0.976663921459517,0.0263549866150321],[-0.942974571228974,0.0409391567013063],[-0.894991997878275,0.0549046959758351],[-0.833442628760834,0.0680383338123569],[-0.759259263037357,0.080140700335001],[-0.673566368473468,0.0910282619829636],[-0.577662930241222,0.10053594906705],[-0.473002731445714,0.108519624474263],[-0.361172305809387,0.114858259145711],[-0.243866883720988,0.119455763535784],[-0.12286469261071,0.12224244299031],[0,0.123176053726715],[0.12286469261071,0.12224244299031],[0.243866883720988,0.119455763535784],[0.361172305809387,0.114858259145711],[0.473002731445714,0.108519624474263],[0.577662930241222,0.10053594906705],[0.673566368473468,0.0910282619829636],[0.759259263037357,0.080140700335001],[0.833442628760834,0.0680383338123569],[0.894991997878275,0.0549046959758351],[0.942974571228974,0.0409391567013063],[0.976663921459517,0.0263549866150321],[0.995556969790498,0.0113937985010262]],[[-0.995885701145616,0.010551372617343],[-0.97838544595647,0.0244178510926319],[-0.947159066661714,0.0379623832943627],[-0.902637861984307,0.0509758252971478],[-0.845445942788498,0.0632740463295748],[-0.776385948820678,0.0746841497656597],[-0.696427260419957,0.0850458943134852],[-0.606692293017618,0.0942138003559141],[-0.508440714824505,0.102059161094425],[-0.403051755123486,0.108471840528576],[-0.292004839485956,0.113361816546319],[-0.17685882035689,0.116660443485296],[-0.0592300934293132,0.118321415279262],[0.0592300934293132,0.118321415279262],[0.17685882035689,0.116660443485296],[0.292004839485956,0.113361816546319],[0.403051755123486,0.108471840528576],[0.508440714824505,0.102059161094425],[0.606692293017618,0.0942138003559141],[0.696427260419957,0.0850458943134852],[0.776385948820678,0.0746841497656597],[0.845445942788498,0.0632740463295748],[0.902637861984307,0.0509758252971478],[0.947159066661714,0.0379623832943627],[0.97838544595647,0.0244178510926319],[0.995885701145616,0.010551372617343]],[[-0.996179262888988,0.00979899605129436],[-0.979923475961501,0.0226862315961806],[-0.950900557814705,0.0352970537574197],[-0.909482320677491,0.047449412520615],[-0.856207908018294,0.0589835368598335],[-0.791771639070508,0.0697488237662455],[-0.717013473739423,0.0796048677730577],[-0.632907971946495,0.0884231585437569],[-0.540551564579456,0.0960887273700285],[-0.441148251750026,0.102501637817745],[-0.335993903638508,0.107578285788533],[-0.226459365439536,0.111252488356845],[-0.113972585609529,0.113476346108965],[0,0.114220867378956],[0.113972585609529,0.113476346108965],[0.226459365439536,0.111252488356845],[0.335993903638508,0.107578285788533],[0.441148251750026,0.102501637817745],[0.540551564579456,0.0960887273700285],[0.632907971946495,0.0884231585437569],[0.717013473739423,0.0796048677730577],[0.791771639070508,0.0697488237662455],[0.856207908018294,0.0589835368598336],[0.909482320677491,0.047449412520615],[0.950900557814705,0.0352970537574197],[0.979923475961501,0.0226862315961806],[0.996179262888988,0.00979899605129436]],[[-0.996442497573954,0.00912428259309452],[-0.981303165370872,0.0211321125927712],[-0.954259280628938,0.0329014277823043],[-0.915633026392132,0.0442729347590042],[-0.865892522574395,0.0551073456757167],[-0.805641370917179,0.0652729239669995],[-0.735610878013631,0.0746462142345687],[-0.656651094038864,0.0831134172289012],[-0.569720471811401,0.0905717443930328],[-0.475874224955118,0.0969306579979299],[-0.376251516089078,0.10211296757806],[-0.272061627635178,0.106055765922846],[-0.16456928213338,0.108711192258294],[-0.0550792898840342,0.110047013016475],[0.0550792898840342,0.110047013016475],[0.16456928213338,0.108711192258294],[0.272061627635178,0.106055765922846],[0.376251516089078,0.10211296757806],[0.475874224955118,0.0969306579979299],[0.569720471811401,0.0905717443930328],[0.656651094038864,0.0831134172289012],[0.735610878013631,0.0746462142345687],[0.805641370917179,0.0652729239669995],[0.865892522574395,0.0551073456757167],[0.915633026392132,0.0442729347590042],[0.954259280628938,0.0329014277823043],[0.981303165370872,0.0211321125927712],[0.996442497573954,0.00912428259309452]],[[-0.996679442260596,0.00851690387874641],[-0.982545505261413,0.0197320850561227],[-0.957285595778087,0.0307404922020936],[-0.921180232953058,0.0414020625186828],[-0.874637804920102,0.0515948269024979],[-0.818185487615252,0.0612030906570791],[-0.752462851734477,0.0701179332550512],[-0.678214537602686,0.0782383271357637],[-0.596281797138227,0.0854722573661725],[-0.507592955124227,0.0917377571392587],[-0.413152888174008,0.0969638340944086],[-0.314031637867639,0.101091273759914],[-0.211352286166001,0.104073310077729],[-0.106278230132679,0.10587615509732],[0,0.106479381718314],[0.106278230132679,0.10587615509732],[0.211352286166001,0.104073310077729],[0.314031637867639,0.101091273759914],[0.413152888174008,0.0969638340944086],[0.507592955124227,0.0917377571392587],[0.596281797138227,0.0854722573661725],[0.678214537602686,0.0782383271357637],[0.752462851734477,0.0701179332550512],[0.818185487615252,0.0612030906570791],[0.874637804920102,0.0515948269024979],[0.921180232953058,0.0414020625186828],[0.957285595778087,0.0307404922020936],[0.982545505261413,0.0197320850561227],[0.996679442260596,0.00851690387874641]],[[-0.996893484074649,0.0079681924961666],[-0.983668123279747,0.0184664683110909],[-0.960021864968307,0.0287847078833233],[-0.926200047429274,0.038799192569627],[-0.882560535792052,0.048402672830594],[-0.829565762382768,0.057493156217619],[-0.767777432104826,0.0659742298821805],[-0.697850494793315,0.0737559747377052],[-0.620526182989242,0.0807558952294202],[-0.536624148142019,0.0868997872010829],[-0.447033769538089,0.0921225222377861],[-0.352704725530878,0.0963687371746442],[-0.254636926167889,0.0995934205867952],[-0.153869913608583,0.101762389748405],[-0.0514718425553176,0.102852652893558],[0.0514718425553176,0.102852652893558],[0.153869913608583,0.101762389748405],[0.254636926167889,0.0995934205867952],[0.352704725530878,0.0963687371746442],[0.447033769538089,0.0921225222377861],[0.536624148142019,0.0868997872010829],[0.620526182989242,0.0807558952294202],[0.697850494793315,0.0737559747377052],[0.767777432104826,0.0659742298821805],[0.829565762382768,0.057493156217619],[0.882560535792052,0.048402672830594],[0.926200047429274,0.038799192569627],[0.960021864968307,0.0287847078833233],[0.983668123279747,0.0184664683110909],[0.996893484074649,0.0079681924961666]]]; 5 | 6 | const minOrder = 5; 7 | const maxOrder = lut.length + minOrder; 8 | 9 | export function getGaussianQuadraturePointsAndWeights(order: number) : number[][] { 10 | if (order < minOrder || order > maxOrder) throw Error(`Order for Gaussian Quadrature must be in the range of ${minOrder} and ${maxOrder}.`); 11 | return lut[order - minOrder]; 12 | } 13 | --------------------------------------------------------------------------------