Fix bounding box calculation for bezier lines.

With a bezier line, the client rect was based directly on the positions
of the points as if it were a normal line. This would result in a bounding
box that doesn't correspond to the actual rendered curve, which in turn
causes a Transformer containing the line to look incorrect.

Fix this by calculating the extrema of the bezier curve on each axis and
use those to get the bounds of the actual rendered line.
This commit is contained in:
Daniel Bruce
2025-11-28 10:36:38 +01:00
parent 752cdc3e93
commit 1d41a22856
2 changed files with 107 additions and 0 deletions

View File

@@ -56,6 +56,43 @@ function expandPoints(p, tension) {
return allPoints;
}
function getBezierExtremaPoints(points) {
const axisPoints = [
[points[0], points[2], points[4], points[6]],
[points[1], points[3], points[5], points[7]],
];
const extremaTs: number[] = [];
for (const axis of axisPoints) {
const a = -3 * axis[0] + 9 * axis[1] - 9 * axis[2] + 3 * axis[3];
if (a !== 0) {
const b = 6 * axis[0] - 12 * axis[1] + 6 * axis[2];
const c = -3 * axis[0] + 3 * axis[1];
const discriminant = b * b - 4 * a * c;
if (discriminant >= 0) {
const d = Math.sqrt(discriminant);
extremaTs.push((-b + d) / (2 * a));
extremaTs.push((-b - d) / (2 * a));
}
}
}
return extremaTs
.filter((t) => t > 0 && t < 1)
.flatMap((t) =>
axisPoints.map((axis) => {
const mt = 1 - t;
return (
mt * mt * mt * axis[0] +
3 * mt * mt * t * axis[1] +
3 * mt * t * t * axis[2] +
t * t * t * axis[3]
);
})
);
}
export interface LineConfig extends ShapeConfig {
points?:
| number[]
@@ -259,6 +296,14 @@ export class Line<
points[points.length - 2],
points[points.length - 1],
];
} else if (this.bezier()) {
points = [
points[0],
points[1],
...getBezierExtremaPoints(this.points()),
points[points.length - 2],
points[points.length - 1],
];
} else {
points = this.points();
}

View File

@@ -456,6 +456,68 @@ describe('Line', function () {
assert.equal(client.height, 2, 'check height');
});
it('getClientRect with bezier', function () {
var stage = addStage();
var layer = new Konva.Layer();
stage.add(layer);
var line = new Konva.Line({
x: 0,
y: 0,
points: [25, 5, -47.7791, 20, 107.7837, 35, 25, 50],
bezier: true,
stroke: '#0f0',
});
layer.add(line);
layer.draw();
var client = line.getClientRect();
assert.equal(Math.round(client.x), 4, 'check x');
assert.equal(Math.round(client.y), 4, 'check y');
assert.equal(Math.round(client.width), 47, 'check width');
assert.equal(Math.round(client.height), 47, 'check height');
line.points([5, 25, 20, -47.7791, 35, 107.7837, 50, 25]);
client = line.getClientRect();
assert.equal(Math.round(client.x), 4, 'check x');
assert.equal(Math.round(client.y), 4, 'check y');
assert.equal(Math.round(client.width), 47, 'check width');
assert.equal(Math.round(client.height), 47, 'check height');
});
it('getClientRect with linear bezier', function () {
var stage = addStage();
var layer = new Konva.Layer();
stage.add(layer);
var line = new Konva.Line({
x: 0,
y: 0,
points: [5, 5, 20, 5, 35, 5, 50, 5],
bezier: true,
stroke: '#0f0',
});
layer.add(line);
layer.draw();
var client = line.getClientRect();
assert.equal(Math.round(client.x), 4, 'check x');
assert.equal(Math.round(client.y), 4, 'check y');
assert.equal(Math.round(client.width), 47, 'check width');
assert.equal(Math.round(client.height), 2, 'check height');
line.points([5, 5, 5, 20, 5, 35, 5, 50]);
client = line.getClientRect();
assert.equal(Math.round(client.x), 4, 'check x');
assert.equal(Math.round(client.y), 4, 'check y');
assert.equal(Math.round(client.width), 2, 'check width');
assert.equal(Math.round(client.height), 47, 'check height');
});
it('line caching', function () {
var stage = addStage();
var layer = new Konva.Layer();