Skip to content

Commit

Permalink
Add CubicBezier.trim
Browse files Browse the repository at this point in the history
  • Loading branch information
baku89 committed Feb 21, 2024
1 parent 654d377 commit 4511f03
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 18 deletions.
27 changes: 27 additions & 0 deletions src/CubicBezier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,30 @@ test('point', () => {
test('normal', () => {
expect(CubicBezier.normal(a, {time: 0.5})).toEqual([0, 1])
})

describe('trim', () => {
const a: SegmentC = {
start: [0, 0],
command: 'C',
args: [
[1, 0],
[1, 1],
],
point: [0, 1],
}
it('should leave the curve unchanged if the trim range is [0, 1]', () => {
expect(CubicBezier.trim(a, {time: 0}, {time: 1})).toEqual(a)
})

it('should trim the curve if the trim range is [0, 0.5]', () => {
expect(CubicBezier.trim(a, {time: 0}, {time: 0.5})).toEqual(
CubicBezier.of([0, 0], [0.5, 0.0], [0.75, 0.25], [0.75, 0.5])
)
})

it('shoud trim the curve if the trim range is [0.5, 1]', () => {
expect(CubicBezier.trim(a, {time: 0.5}, {time: 1})).toEqual(
CubicBezier.of([0.75, 0.5], [0.75, 0.75], [0.5, 1.0], [0, 1])
)
})
})
118 changes: 100 additions & 18 deletions src/CubicBezier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,9 @@ type SimpleSegmentC = PartialBy<SegmentC, 'command'>
* A collection of functions to handle a cubic bezier represented with {@link SegmentC}.
*/
export namespace CubicBezier {
export const toBezierJS = memoize((bezier: SimpleSegmentC): BezierJS => {
const {
start,
args: [control1, control2],
point,
} = bezier
return new BezierJS(
start[0],
start[1],
control1[0],
control1[1],
control2[0],
control2[1],
point[0],
point[1]
)
})
export function of(start: vec2, control1: vec2, control2: vec2, point: vec2) {
return {command: 'C', start, args: [control1, control2], point}
}

export const toPaperBezier = memoize((beizer: SegmentC) => {
const {
Expand Down Expand Up @@ -154,14 +140,64 @@ export namespace CubicBezier {
* Finds the on-curve point closest to the specific off-curve point
*/
export function project(
bezier: SegmentC,
bezier: SimpleSegmentC,
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}
}

export function trim(
bezier: SegmentC,
start: SegmentLocation,
end: SegmentLocation
): SegmentC {
let startTime = toTime(bezier, start)
let endTime = toTime(bezier, end)

if (startTime === 0 && endTime === 1) {
return bezier
}

// Make sure that startTime < endTime
const shouldFlip = startTime > endTime
if (shouldFlip) {
;[startTime, endTime] = [endTime, startTime]
}

// Trim to [0, 1] -> [startTime, 1]
let newStart: vec2, midC1: vec2, midC2: vec2

if (startTime === 0) {
newStart = bezier.start
;[midC1, midC2] = bezier.args
} else {
;[, , newStart, midC1, midC2] = splitCubicBezierAtTime(bezier, startTime)
}

// Trim to [startTime, 1] -> [startTime, endTime]
let newC1: vec2, newC2: vec2, newEnd: vec2

if (endTime === 1) {
newEnd = bezier.point
;[newC1, newC2] = [midC1, midC2]
} else {
;[newC1, newC2, newEnd] = splitCubicBezierAtTime(
{start: newStart, args: [midC1, midC2], point: bezier.point},
scalar.invlerp(startTime, 1, endTime)
)
}

// Flip back if necessary
if (shouldFlip) {
;[newStart, newEnd] = [newEnd, newStart]
;[newC1, newC2] = [newC2, newC1]
}

return {command: 'C', start: newStart, args: [newC1, newC2], point: newEnd}
}

export function divideAtTimes(segment: SegmentC, times: number[]): VertexC[] {
const bezier = toBezierJS(segment)

Expand Down Expand Up @@ -225,3 +261,49 @@ const getDerivativeFn = memoize((bezier: SimpleSegmentC) => {
return [dx, dy]
}
})

function splitCubicBezierAtTime(
bezier: SimpleSegmentC,
time: number
): [
newControl1: vec2,
inControl: vec2,
midPoint: vec2,
outControl: vec2,
newControl2: vec2,
] {
// Use de Casteljau's algorithm
// https://pomax.github.io/bezierinfo/#splitting

const [c1, c2] = bezier.args

// Find the start point and c1
const newControl1 = vec2.lerp(bezier.start, c1, time)
const p12 = vec2.lerp(c1, c2, time)
const newControl2 = vec2.lerp(c2, bezier.point, time)

const inControl = vec2.lerp(newControl1, p12, time)
const outControl = vec2.lerp(p12, newControl2, time)

const midPoint = vec2.lerp(inControl, outControl, time)

return [newControl1, inControl, midPoint, outControl, newControl2]
}

const toBezierJS = memoize((bezier: SimpleSegmentC): BezierJS => {
const {
start,
args: [control1, control2],
point,
} = bezier
return new BezierJS(
start[0],
start[1],
control1[0],
control1[1],
control2[0],
control2[1],
point[0],
point[1]
)
})

0 comments on commit 4511f03

Please sign in to comment.