-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add basic functions for BBox and CubicBezier
- Loading branch information
Showing
8 changed files
with
261 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import {expect} from '@jest/globals' | ||
|
||
beforeEach(() => { | ||
const EPSILON = 1e-6 | ||
|
||
function nearlyEqual(a: number, b: number) { | ||
return ( | ||
Math.abs(a - b) <= EPSILON * Math.max(1, Math.abs(a), Math.abs(b)) || | ||
undefined | ||
) | ||
} | ||
|
||
expect.addEqualityTesters([nearlyEqual]) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import '../jest.config' | ||
|
||
import {BBox, unite} from './BBox' | ||
|
||
describe('unite', () => { | ||
it('should unite multiple bounding boxes correctly', () => { | ||
const bbox1: BBox = [ | ||
[0, 0], | ||
[2, 2], | ||
] | ||
const bbox2: BBox = [ | ||
[1, 1], | ||
[3, 3], | ||
] | ||
const bbox3: BBox = [ | ||
[-1, -1], | ||
[1, 1], | ||
] | ||
const result = unite(bbox1, bbox2, bbox3) | ||
expect(result).toEqual([ | ||
[-1, -1], | ||
[3, 3], | ||
]) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import {vec2} from 'linearly' | ||
|
||
export type BBox = readonly [min: vec2, max: vec2] | ||
|
||
export function unite(...bboxes: BBox[]): BBox { | ||
let minX = Infinity, | ||
minY = Infinity, | ||
maxX = -Infinity, | ||
maxY = -Infinity | ||
|
||
for (const [min, max] of bboxes) { | ||
minX = Math.min(minX, min[0]) | ||
minY = Math.min(minY, min[1]) | ||
maxX = Math.max(maxX, max[0]) | ||
maxY = Math.max(maxY, max[1]) | ||
} | ||
|
||
return [ | ||
[minX, minY], | ||
[maxX, maxY], | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import '../jest.config' | ||
|
||
import {atT, bound, CubicBezier, length, normal} from './CubicBezier' | ||
|
||
describe('CubicBezier', () => { | ||
const a: CubicBezier = [ | ||
[0, 0], | ||
[0, 1], | ||
[1, 1], | ||
[1, 0], | ||
] | ||
const b: CubicBezier = [ | ||
[0, 0], | ||
[2, 0], | ||
[1, 1], | ||
[1, 0], | ||
] | ||
|
||
it('should compute the `length` correctly', () => { | ||
expect(length(a)).toBeCloseTo(2, 10) | ||
expect(length(b)).toBeCloseTo(2, 10) | ||
}) | ||
|
||
it('should compute the `bound` correctly', () => { | ||
expect(bound(a)).toEqual([ | ||
[0, 0], | ||
[1, 0.75], | ||
]) | ||
expect(bound(b)).toEqual([ | ||
[0, 0], | ||
[1.25, 0.444444], | ||
]) | ||
}) | ||
|
||
it('should compute the `atT` correctly', () => { | ||
expect(atT(a, 0)).toEqual([0, 0]) | ||
expect(atT(a, 0.5)).toEqual([0.5, 0.75]) | ||
expect(atT(a, 1)).toEqual([1, 0]) | ||
}) | ||
|
||
it('should compute the `normal` correctly', () => { | ||
expect(normal(a, 0.5)).toEqual([0, 1]) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,135 @@ | ||
import {Bezier as BezierJS, Point} from 'bezier-js' | ||
import {vec2} from 'linearly' | ||
|
||
export type CubicBezier = [ | ||
export type CubicBezier = readonly [ | ||
start: vec2, | ||
control1: vec2, | ||
control2: vec2, | ||
end: vec2, | ||
] | ||
|
||
export const toBezierJS = memoizeCubicBezierFunction( | ||
(bezier: CubicBezier): BezierJS => { | ||
const [start, control1, control2, end] = bezier | ||
return new BezierJS( | ||
start[0], | ||
start[1], | ||
control1[0], | ||
control1[1], | ||
control2[0], | ||
control2[1], | ||
end[0], | ||
end[1] | ||
) | ||
} | ||
) | ||
|
||
function toPoint([x, y]: vec2): Point { | ||
return {x, y} | ||
} | ||
|
||
/** | ||
* Calculates the length of the Bezier curve. Length is calculated using numerical approximation, specifically the Legendre-Gauss quadrature algorithm. | ||
*/ | ||
export const length = memoizeCubicBezierFunction( | ||
(bezier: CubicBezier): number => { | ||
const bezierJS = toBezierJS(bezier) | ||
return bezierJS.length() | ||
} | ||
) | ||
|
||
/** | ||
* Calculates the bounding box of this Bezier curve. | ||
*/ | ||
export const bound = memoizeCubicBezierFunction( | ||
(bezier: CubicBezier): [vec2, vec2] => { | ||
const bezierJS = toBezierJS(bezier) | ||
const {x, y} = bezierJS.bbox() | ||
|
||
return [ | ||
[x.min, y.min], | ||
[x.max, y.max], | ||
] | ||
} | ||
) | ||
|
||
/** | ||
* Calculates the point on the curve at the specified `t` value. | ||
*/ | ||
export function atT(bezier: CubicBezier, t: number): vec2 { | ||
const bezierJS = toBezierJS(bezier) | ||
const {x, y} = bezierJS.get(t) | ||
return [x, y] | ||
} | ||
|
||
/** | ||
* Calculates the curve tangent at the specified `t` value. Note that this yields a not-normalized vector. | ||
*/ | ||
export function derivative(bezier: CubicBezier, t: number): vec2 { | ||
const bezierJS = toBezierJS(bezier) | ||
const {x, y} = bezierJS.derivative(t) | ||
return [x, y] | ||
} | ||
|
||
/** | ||
* Calculates the curve tangent at the specified `t` value. Unlike {@link derivative}, this yields a normalized vector. | ||
*/ | ||
export function tangent(bezier: CubicBezier, t: number): vec2 { | ||
return vec2.normalize(derivative(bezier, t)) | ||
} | ||
|
||
/** | ||
* Calculates the curve normal at the specified `t` value. Note that this yields a normalized vector. | ||
*/ | ||
export function normal(bezier: CubicBezier, t: number): vec2 { | ||
const bezierJS = toBezierJS(bezier) | ||
const {x, y} = bezierJS.normal(t) | ||
return [x, y] | ||
} | ||
|
||
/** | ||
* Finds the on-curve point closest to the specific off-curve point | ||
*/ | ||
export function project( | ||
bezier: CubicBezier, | ||
origin: vec2 | ||
): {position: vec2; t?: number; distance?: number} { | ||
const bezierJS = toBezierJS(bezier) | ||
const {x, y, t, d} = bezierJS.project(toPoint(origin)) | ||
return {position: [x, y], t, distance: d} | ||
} | ||
|
||
function memoizeCubicBezierFunction<T>(f: (bezier: CubicBezier) => T) { | ||
const cache = new WeakMap< | ||
vec2, | ||
WeakMap<vec2, WeakMap<vec2, WeakMap<vec2, T>>> | ||
>() | ||
|
||
return (bezier: CubicBezier): T => { | ||
const [start, control1, control2, end] = bezier | ||
|
||
if (!cache.has(start)) { | ||
cache.set(start, new WeakMap()) | ||
} | ||
|
||
const cache1 = cache.get(start)! | ||
|
||
if (!cache1.has(control1)) { | ||
cache1.set(control1, new WeakMap()) | ||
} | ||
|
||
const cache2 = cache1.get(control1)! | ||
|
||
if (!cache2.has(control2)) { | ||
cache2.set(control2, new WeakMap()) | ||
} | ||
|
||
const cache3 = cache2.get(control2)! | ||
|
||
if (!cache3.has(end)) { | ||
cache3.set(end, f(bezier)) | ||
} | ||
|
||
return cache3.get(end)! | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,4 @@ | ||
export * from './CubicBezier' | ||
export * from './Path' | ||
export type * from './CubicBezier' | ||
export * as CubicBezier from './CubicBezier' | ||
export type * from './Path' | ||
export * as Path from './Path' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters