Skip to content

Commit

Permalink
Add basic functions for BBox and CubicBezier
Browse files Browse the repository at this point in the history
  • Loading branch information
baku89 committed Nov 27, 2023
1 parent 0edcd6e commit b3c6f3a
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 3 deletions.
14 changes: 14 additions & 0 deletions jest.test.ts
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])
})
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"homepage": "https://baku89.github.io/pathed",
"devDependencies": {
"@types/jest": "^29.5.4",
"@types/lodash": "^4.14.202",
"@typescript-eslint/eslint-plugin": "^6.7.0",
"@typescript-eslint/parser": "^6.7.0",
"eslint": "^8.49.0",
Expand All @@ -47,6 +48,7 @@
"eslint-plugin-unused-imports": "^3.0.0",
"jest": "^29.7.0",
"jest-runner-eslint": "^2.1.1",
"lodash": "^4.17.21",
"prettier": "^3.0.3",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
Expand All @@ -55,6 +57,8 @@
"typescript": "^5.2.2"
},
"dependencies": {
"@types/bezier-js": "^4.1.3",
"bezier-js": "^6.1.4",
"linearly": "^0.21.0"
}
}
25 changes: 25 additions & 0 deletions src/BBox.test.ts
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],
])
})
})
22 changes: 22 additions & 0 deletions src/BBox.ts
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],
]
}
44 changes: 44 additions & 0 deletions src/CubicBezier.test.ts
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])
})
})
129 changes: 128 additions & 1 deletion src/CubicBezier.ts
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)!
}
}
6 changes: 4 additions & 2 deletions src/index.ts
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'
20 changes: 20 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,11 @@
dependencies:
"@babel/types" "^7.20.7"

"@types/bezier-js@^4.1.3":
version "4.1.3"
resolved "https://registry.yarnpkg.com/@types/bezier-js/-/bezier-js-4.1.3.tgz#237d4fe7e9aae7edd0c27a71f9f236f4ddc1c562"
integrity sha512-FNVVCu5mx/rJCWBxLTcL7oOajmGtWtBTDjq6DSUWUI12GeePivrZZXz+UgE0D6VYsLEjvExRO03z4hVtu3pTEQ==

"@types/graceful-fs@^4.1.3":
version "4.1.6"
resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae"
Expand Down Expand Up @@ -740,6 +745,11 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==

"@types/lodash@^4.14.202":
version "4.14.202"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.202.tgz#f09dbd2fb082d507178b2f2a5c7e74bd72ff98f8"
integrity sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==

"@types/node@*":
version "20.6.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.6.0.tgz#9d7daa855d33d4efec8aea88cd66db1c2f0ebe16"
Expand Down Expand Up @@ -1061,6 +1071,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==

bezier-js@^6.1.4:
version "6.1.4"
resolved "https://registry.yarnpkg.com/bezier-js/-/bezier-js-6.1.4.tgz#c7828f6c8900562b69d5040afb881bcbdad82001"
integrity sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==

brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
Expand Down Expand Up @@ -2396,6 +2411,11 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==

lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==

lru-cache@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
Expand Down

0 comments on commit b3c6f3a

Please sign in to comment.