diff --git a/packages/excalidraw/renderer/helpers.ts b/packages/excalidraw/renderer/helpers.ts index e0397e95d..a06425ea6 100644 --- a/packages/excalidraw/renderer/helpers.ts +++ b/packages/excalidraw/renderer/helpers.ts @@ -5,17 +5,14 @@ import { getDiamondPoints } from "@excalidraw/element"; import { getCornerRadius } from "@excalidraw/element"; import { - bezierEquation, curve, - curveTangent, + curveCatmullRomCubicApproxPoints, + curveCatmullRomQuadraticApproxPoints, + curveOffsetPoints, type GlobalPoint, + offsetPointsForQuadraticBezier, pointFrom, - pointFromVector, pointRotateRads, - vector, - vectorNormal, - vectorNormalize, - vectorScale, } from "@excalidraw/math"; import type { @@ -99,25 +96,14 @@ export const bootstrapCanvas = ({ function drawCatmullRomQuadraticApprox( ctx: CanvasRenderingContext2D, points: GlobalPoint[], - segments = 20, + tension = 0.5, ) { - ctx.lineTo(points[0][0], points[0][1]); + const pointSets = curveCatmullRomQuadraticApproxPoints(points, tension); + if (pointSets) { + for (let i = 0; i < pointSets.length - 1; i++) { + const [[cpX, cpY], [p2X, p2Y]] = pointSets[i]; - for (let i = 0; i < points.length - 1; i++) { - const p0 = points[i - 1 < 0 ? 0 : i - 1]; - const p1 = points[i]; - const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1]; - - for (let t = 0; t <= 1; t += 1 / segments) { - const t2 = t * t; - - const x = - (1 - t) * (1 - t) * p0[0] + 2 * (1 - t) * t * p1[0] + t2 * p2[0]; - - const y = - (1 - t) * (1 - t) * p0[1] + 2 * (1 - t) * t * p1[1] + t2 * p2[1]; - - ctx.lineTo(x, y); + ctx.quadraticCurveTo(cpX, cpY, p2X, p2Y); } } } @@ -125,35 +111,13 @@ function drawCatmullRomQuadraticApprox( function drawCatmullRomCubicApprox( ctx: CanvasRenderingContext2D, points: GlobalPoint[], - segments = 20, + tension = 0.5, ) { - ctx.lineTo(points[0][0], points[0][1]); - - for (let i = 0; i < points.length - 1; i++) { - const p0 = points[i - 1 < 0 ? 0 : i - 1]; - const p1 = points[i]; - const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1]; - const p3 = points[i + 2 >= points.length ? points.length - 1 : i + 2]; - - for (let t = 0; t <= 1; t += 1 / segments) { - const t2 = t * t; - const t3 = t2 * t; - - const x = - 0.5 * - (2 * p1[0] + - (-p0[0] + p2[0]) * t + - (2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 + - (-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3); - - const y = - 0.5 * - (2 * p1[1] + - (-p0[1] + p2[1]) * t + - (2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 + - (-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3); - - ctx.lineTo(x, y); + const pointSets = curveCatmullRomCubicApproxPoints(points, tension); + if (pointSets) { + for (let i = 0; i < pointSets.length; i++) { + const [[cp1x, cp1y], [cp2x, cp2y], [x, y]] = pointSets[i]; + ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); } } } @@ -184,25 +148,25 @@ export const drawHighlightForRectWithRotation = ( context.beginPath(); { - const topLeftApprox = offsetQuadraticBezier( + const topLeftApprox = offsetPointsForQuadraticBezier( pointFrom(0, 0 + radius), pointFrom(0, 0), pointFrom(0 + radius, 0), padding, ); - const topRightApprox = offsetQuadraticBezier( + const topRightApprox = offsetPointsForQuadraticBezier( pointFrom(element.width - radius, 0), pointFrom(element.width, 0), pointFrom(element.width, radius), padding, ); - const bottomRightApprox = offsetQuadraticBezier( + const bottomRightApprox = offsetPointsForQuadraticBezier( pointFrom(element.width, element.height - radius), pointFrom(element.width, element.height), pointFrom(element.width - radius, element.height), padding, ); - const bottomLeftApprox = offsetQuadraticBezier( + const bottomLeftApprox = offsetPointsForQuadraticBezier( pointFrom(radius, element.height), pointFrom(0, element.height), pointFrom(0, element.height - radius), @@ -227,25 +191,25 @@ export const drawHighlightForRectWithRotation = ( // mask" on a filled shape for the diamond highlight, because stroking creates // sharp inset edges on line joins < 90 degrees. { - const topLeftApprox = offsetQuadraticBezier( + const topLeftApprox = offsetPointsForQuadraticBezier( pointFrom(0 + radius, 0), pointFrom(0, 0), pointFrom(0, 0 + radius), -FIXED_BINDING_DISTANCE, ); - const topRightApprox = offsetQuadraticBezier( + const topRightApprox = offsetPointsForQuadraticBezier( pointFrom(element.width, radius), pointFrom(element.width, 0), pointFrom(element.width - radius, 0), -FIXED_BINDING_DISTANCE, ); - const bottomRightApprox = offsetQuadraticBezier( + const bottomRightApprox = offsetPointsForQuadraticBezier( pointFrom(element.width - radius, element.height), pointFrom(element.width, element.height), pointFrom(element.width, element.height - radius), -FIXED_BINDING_DISTANCE, ); - const bottomLeftApprox = offsetQuadraticBezier( + const bottomLeftApprox = offsetPointsForQuadraticBezier( pointFrom(0, element.height - radius), pointFrom(0, element.height), pointFrom(radius, element.height), @@ -340,32 +304,40 @@ export const drawHighlightForDiamondWithRotation = ( const horizontalRadius = element.roundness ? getCornerRadius(Math.abs(rightY - topY), element) : (rightY - topY) * 0.01; - const topApprox = offsetCubicBezier( - pointFrom(topX - verticalRadius, topY + horizontalRadius), - pointFrom(topX, topY), - pointFrom(topX, topY), - pointFrom(topX + verticalRadius, topY + horizontalRadius), + const topApprox = curveOffsetPoints( + curve( + pointFrom(topX - verticalRadius, topY + horizontalRadius), + pointFrom(topX, topY), + pointFrom(topX, topY), + pointFrom(topX + verticalRadius, topY + horizontalRadius), + ), padding, ); - const rightApprox = offsetCubicBezier( - pointFrom(rightX - verticalRadius, rightY - horizontalRadius), - pointFrom(rightX, rightY), - pointFrom(rightX, rightY), - pointFrom(rightX - verticalRadius, rightY + horizontalRadius), + const rightApprox = curveOffsetPoints( + curve( + pointFrom(rightX - verticalRadius, rightY - horizontalRadius), + pointFrom(rightX, rightY), + pointFrom(rightX, rightY), + pointFrom(rightX - verticalRadius, rightY + horizontalRadius), + ), padding, ); - const bottomApprox = offsetCubicBezier( - pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius), - pointFrom(bottomX, bottomY), - pointFrom(bottomX, bottomY), - pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius), + const bottomApprox = curveOffsetPoints( + curve( + pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius), + pointFrom(bottomX, bottomY), + pointFrom(bottomX, bottomY), + pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius), + ), padding, ); - const leftApprox = offsetCubicBezier( - pointFrom(leftX + verticalRadius, leftY + horizontalRadius), - pointFrom(leftX, leftY), - pointFrom(leftX, leftY), - pointFrom(leftX + verticalRadius, leftY - horizontalRadius), + const leftApprox = curveOffsetPoints( + curve( + pointFrom(leftX + verticalRadius, leftY + horizontalRadius), + pointFrom(leftX, leftY), + pointFrom(leftX, leftY), + pointFrom(leftX + verticalRadius, leftY - horizontalRadius), + ), padding, ); @@ -395,32 +367,40 @@ export const drawHighlightForDiamondWithRotation = ( const horizontalRadius = element.roundness ? getCornerRadius(Math.abs(rightY - topY), element) : (rightY - topY) * 0.01; - const topApprox = offsetCubicBezier( - pointFrom(topX + verticalRadius, topY + horizontalRadius), - pointFrom(topX, topY), - pointFrom(topX, topY), - pointFrom(topX - verticalRadius, topY + horizontalRadius), + const topApprox = curveOffsetPoints( + curve( + pointFrom(topX + verticalRadius, topY + horizontalRadius), + pointFrom(topX, topY), + pointFrom(topX, topY), + pointFrom(topX - verticalRadius, topY + horizontalRadius), + ), -FIXED_BINDING_DISTANCE, ); - const rightApprox = offsetCubicBezier( - pointFrom(rightX - verticalRadius, rightY + horizontalRadius), - pointFrom(rightX, rightY), - pointFrom(rightX, rightY), - pointFrom(rightX - verticalRadius, rightY - horizontalRadius), + const rightApprox = curveOffsetPoints( + curve( + pointFrom(rightX - verticalRadius, rightY + horizontalRadius), + pointFrom(rightX, rightY), + pointFrom(rightX, rightY), + pointFrom(rightX - verticalRadius, rightY - horizontalRadius), + ), -FIXED_BINDING_DISTANCE, ); - const bottomApprox = offsetCubicBezier( - pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius), - pointFrom(bottomX, bottomY), - pointFrom(bottomX, bottomY), - pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius), + const bottomApprox = curveOffsetPoints( + curve( + pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius), + pointFrom(bottomX, bottomY), + pointFrom(bottomX, bottomY), + pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius), + ), -FIXED_BINDING_DISTANCE, ); - const leftApprox = offsetCubicBezier( - pointFrom(leftX + verticalRadius, leftY - horizontalRadius), - pointFrom(leftX, leftY), - pointFrom(leftX, leftY), - pointFrom(leftX + verticalRadius, leftY + horizontalRadius), + const leftApprox = curveOffsetPoints( + curve( + pointFrom(leftX + verticalRadius, leftY - horizontalRadius), + pointFrom(leftX, leftY), + pointFrom(leftX, leftY), + pointFrom(leftX + verticalRadius, leftY + horizontalRadius), + ), -FIXED_BINDING_DISTANCE, ); @@ -441,53 +421,3 @@ export const drawHighlightForDiamondWithRotation = ( context.fill(); context.restore(); }; - -function offsetCubicBezier( - p0: GlobalPoint, - p1: GlobalPoint, - p2: GlobalPoint, - p3: GlobalPoint, - offsetDist: number, - steps = 20, -) { - const offsetPoints = []; - - for (let i = 0; i <= steps; i++) { - const t = i / steps; - const c = curve(p0, p1, p2, p3); - const point = bezierEquation(c, t); - const tangent = vectorNormalize(curveTangent(c, t)); - const normal = vectorNormal(tangent); - - offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point)); - } - - return offsetPoints; -} - -function offsetQuadraticBezier( - p0: GlobalPoint, - p1: GlobalPoint, - p2: GlobalPoint, - offsetDist: number, - steps = 20, -) { - const offsetPoints = []; - - for (let i = 0; i <= steps; i++) { - const t = i / steps; - const t1 = 1 - t; - const point = pointFrom( - t1 * t1 * p0[0] + 2 * t1 * t * p1[0] + t * t * p2[0], - t1 * t1 * p0[1] + 2 * t1 * t * p1[1] + t * t * p2[1], - ); - const tangentX = 2 * (1 - t) * (p1[0] - p0[0]) + 2 * t * (p2[0] - p1[0]); - const tangentY = 2 * (1 - t) * (p1[1] - p0[1]) + 2 * t * (p2[1] - p1[1]); - const tangent = vectorNormalize(vector(tangentX, tangentY)); - const normal = vectorNormal(tangent); - - offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point)); - } - - return offsetPoints; -} diff --git a/packages/math/src/curve.ts b/packages/math/src/curve.ts index 359caee09..d7f6a7341 100644 --- a/packages/math/src/curve.ts +++ b/packages/math/src/curve.ts @@ -1,8 +1,8 @@ import type { Bounds } from "@excalidraw/element"; -import { isPoint, pointDistance, pointFrom } from "./point"; +import { isPoint, pointDistance, pointFrom, pointFromVector } from "./point"; import { rectangle, rectangleIntersectLineSegment } from "./rectangle"; -import { vector } from "./vector"; +import { vector, vectorNormal, vectorNormalize, vectorScale } from "./vector"; import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types"; @@ -303,3 +303,106 @@ function curveBounds( const y = [P0[1], P1[1], P2[1], P3[1]]; return [Math.min(...x), Math.min(...y), Math.max(...x), Math.max(...y)]; } + +export function curveCatmullRomQuadraticApproxPoints( + points: GlobalPoint[], + tension = 0.5, +) { + if (points.length < 2) { + return; + } + + const pointSets: [GlobalPoint, GlobalPoint][] = []; + for (let i = 0; i < points.length - 1; i++) { + const p0 = points[i - 1 < 0 ? 0 : i - 1]; + const p1 = points[i]; + const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1]; + const cpX = p1[0] + ((p2[0] - p0[0]) * tension) / 2; + const cpY = p1[1] + ((p2[1] - p0[1]) * tension) / 2; + + pointSets.push([ + pointFrom(cpX, cpY), + pointFrom(p2[0], p2[1]), + ]); + } + + return pointSets; +} + +export function curveCatmullRomCubicApproxPoints( + points: GlobalPoint[], + tension = 0.5, +) { + if (points.length < 2) { + return; + } + + const pointSets: [GlobalPoint, GlobalPoint, GlobalPoint][] = []; + for (let i = 0; i < points.length - 1; i++) { + const p0 = points[i - 1 < 0 ? 0 : i - 1]; + const p1 = points[i]; + const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1]; + const p3 = points[i + 2 >= points.length ? points.length - 1 : i + 2]; + const tangent1 = [(p2[0] - p0[0]) * tension, (p2[1] - p0[1]) * tension]; + const tangent2 = [(p3[0] - p1[0]) * tension, (p3[1] - p1[1]) * tension]; + const cp1x = p1[0] + tangent1[0] / 3; + const cp1y = p1[1] + tangent1[1] / 3; + const cp2x = p2[0] - tangent2[0] / 3; + const cp2y = p2[1] - tangent2[1] / 3; + + pointSets.push([ + pointFrom(cp1x, cp1y), + pointFrom(cp2x, cp2y), + pointFrom(p2[0], p2[1]), + ]); + } + + return pointSets; +} + +export function curveOffsetPoints( + [p0, p1, p2, p3]: Curve, + offsetDist: number, + steps = 50, +) { + const offsetPoints = []; + + for (let i = 0; i <= steps; i++) { + const t = i / steps; + const c = curve(p0, p1, p2, p3); + const point = bezierEquation(c, t); + const tangent = vectorNormalize(curveTangent(c, t)); + const normal = vectorNormal(tangent); + + offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point)); + } + + return offsetPoints; +} + +export function offsetPointsForQuadraticBezier( + p0: GlobalPoint, + p1: GlobalPoint, + p2: GlobalPoint, + offsetDist: number, + steps = 50, +) { + const offsetPoints = []; + + for (let i = 0; i <= steps; i++) { + const t = i / steps; + const t1 = 1 - t; + const point = pointFrom( + t1 * t1 * p0[0] + 2 * t1 * t * p1[0] + t * t * p2[0], + t1 * t1 * p0[1] + 2 * t1 * t * p1[1] + t * t * p2[1], + ); + const tangentX = 2 * (1 - t) * (p1[0] - p0[0]) + 2 * t * (p2[0] - p1[0]); + const tangentY = 2 * (1 - t) * (p1[1] - p0[1]) + 2 * t * (p2[1] - p1[1]); + const tangent = vectorNormalize(vector(tangentX, tangentY)); + const normal = vectorNormal(tangent); + + offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point)); + } + + return offsetPoints; +}