Repository navigation
@remotion/paths: Fix getBoundingBox() for curves starting with a flat tangent - #12687
Conversation
…flat tangent `minmaxQ()` treated a control value equal to the start value as an extremum inside the curve and returned [min, E] with E equal to the start, dropping the end of the curve from the box. This happens for `Q` and `C` curves whose first control point is level with the start point in x or y, and for `T` after a non-quadratic command. Co-Authored-By: Claude Opus 5.5 <[email protected]>
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
minmaxQ()extremum test (get-bounding-box.ts:20): replaces the branchyA[1] > A[0] ? A[2] >= A[1] : A[2] <= A[1]check with(A[1] - A[0]) * (A[2] - A[1]) >= 0. Since the derivative of a quadratic Bézier is linear, an interior extremum exists iff the derivative flips sign, i.e.d0 = A[1] - A[0]andd1 = A[2] - A[1]have strictly opposite signs — the new condition is exact. The>= 0(rather than> 0) is essential: it makes the flat-tangent case (d0 === 0ord1 === 0) return early instead of computingE = A[0], which is what dropped the curve's end.- Regression test (
get-bounding-box.test.ts:29): covers theQ,C(near-quadratic,K ≈ 0reduction path), andT-after-line cases.
I verified the algebra independently: the new predicate is strictly more correct than the old one. It only computes E when d0 * d1 < 0, which guarantees a genuine interior extremum, and never divides by zero (d0 * d1 < 0 implies A[0] - 2*A[1] + A[2] = d1 - d0 ≠ 0). The old check also mis-handled symmetric humps like A = [0, 5, 0] (returned [0, 0] instead of [0, 2.5]), which this fixes too. bun test src passes locally (51 tests).
The separate T-after-T control-point issue the author mentions in getLength() / reduceInstructions() is a pre-existing bug outside this diff, so it is correctly out of scope for this PR.
deepseek-v4.1-flash | 𝕏
|
Thank you! |

getBoundingBox()leaves out the end of a curve when its first control point is level with the start point. For examplegetBoundingBox('M 0 0 Q 0 10 10 10')returns a box that is 0 wide instead of 10. This also affectsCcurves whose first control point is the start point, andTafter a line (its control point is the current point).centerPath()uses the same box, so it shifts these paths by the wrong amount.The cause is in
minmaxQ(). WhenA[1] === A[0]and the curve rises, the "no extremum in ]0,1[" check does not return early. The extremum formula then givesA[0]and[min, E]drops the maximum. The check now tests whetherA[1] - A[0]andA[2] - A[1]have opposite signs, which is the only case where the extremum is strictly inside the curve.Before (
main4725fad, box values rounded to 3 decimals):After (same rounding):
I added a test to
get-bounding-box.test.ts. It fails onmain(Expected: 10, Received: 0) and passes with the change.bun test srcinpackages/pathspasses (51 tests), andoxfmt --checkis clean on both files.I found this by checking
getBoundingBox()against points sampled withgetPointAtLength()on random paths. The same check also shows that aTfollowing anotherTthat came after a non-quadratic command (e.g.M 0 0 H 10 T 20 10 T 30 0) gets the wrong control point in bothgetLength()andreduceInstructions(). I can send that separately if you want it.I used Claude Code to write this change.