Skip to content

Instantly share code, notes, and snippets.

@Phryxia
Last active January 6, 2023 03:00
Show Gist options
  • Save Phryxia/8ba1256b9c0d7cb6588a284032b726fb to your computer and use it in GitHub Desktop.
Save Phryxia/8ba1256b9c0d7cb6588a284032b726fb to your computer and use it in GitHub Desktop.
TypeScript implementation of bezier curve and its derivative.
type Vector = number[]
function add(u: Vector, v: Vector): Vector {
return u.map((x, i) => x + v[i])
}
function sub(u: Vector, v: Vector): Vector {
return u.map((x, i) => x - v[i])
}
function lerp(x: number, y: number, t: number): number {
return (1 - t) * x + t * y
}
function lerpV(x: Vector, y: Vector, t: number): Vector {
return x.map((v, i) => lerp(v, y[i], t))
}
type Interpolator = (t: number) => Vector
export function bezier(points: Vector[]): { b: Interpolator, db_dt: Interpolator } {
if (points.length === 0) throw new Error('no point to interpolate')
function _bezier(s: number, e: number, t: number, cachedValue: Record<number, Vector> = {}): Vector {
return (cachedValue[s * points.length + e] ??=
s === e
? points[s]
: e - s === 1
? lerpV(points[s], points[e], t)
: lerpV(_bezier(s, e - 1, t, cachedValue), _bezier(s + 1, e, t, cachedValue), t))
}
function _bezierPrime(s: number, e: number, t: number, cache: { b: Record<number, Vector>, db_dt: Record<number, Vector> }): Vector {
return (cache.db_dt[s * points.length + e] ??=
s === e
? points[s].map(() => 0)
: e - s === 1
? sub(points[e], points[s])
: add(
lerpV(
_bezierPrime(s, e - 1, t, cache),
_bezierPrime(s + 1, e, t, cache),
t
),
sub(
_bezier(s + 1, e, t, cache.b),
_bezier(s, e - 1, t, cache.b)
)
)
)
}
return {
b: (t: number) => _bezier(0, points.length - 1, t),
db_dt: (t: number) => _bezierPrime(0, points.length - 1, t, { b: {}, db_dt: {} })
}
}
@Phryxia
Copy link
Author

Phryxia commented May 24, 2022

TypeScript implementation of reusable n-th Bezier curve generating function.

n-th Bezier curve is a curve interpolates two points from (n-1)-th Bezier curve recursively, although most of the case 2nd or 3rd is used.

This implementation uses memoization which reduces time complexity of O(2^n) into O(n^2), due to overlapping computations.

image

Derivative of Bezier Curve

This uses same approach with the original bezier curve computation. Note that this doesn't normalize the result. Since the size of the derivative vectors depend on the distances between original points, they are usually very big. You may want to normalize them.

image

Proof

Let p ⊆ ℝ^n be the interpolation points. Bezier curve B is defined as

function B(p, s, e, t):
  let n := e - s + 1
  if n < 1:
    throw error
  if n = 1:
    return p[s]
  if n = 2:
    return lerp(p[s], p[e])
  return lerp(B(p, s, e - 1, t), B(p, s + 1, e, t))

where lerp(u, v, t) = (1 - t) * u + t * v. Note that lerp' = v - u.

Now do derivation for each lines. For the last line, as B is the function of t, apply chain rule to the last line.

lerp(B'(p, s, e - 1, t), B(p, s + 1, e, t)) + lerp'(B(p, s, e - 1, t), B(p, s + 1, e, t))
= lerp(B'(p, s, e - 1, t), B(p, s + 1, e, t)) + B(p, s + 1, e, t) - B(p, s, e - 1, t)

Therefore following function computes B'.

function B'(p, s, e, t):
  let n := e - s + 1
  if n < 1:
    throw error
  if n = 1:
    return 0
  if n = 2:
    return p[e] - p[s]
  return lerp(B'(p, s, e - 1, t), B'(p, s + 1, e, t)) + B(p, s + 1, e, t) - B(p, s, e - 1, t)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment