mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Equidistant binding highlight for diamonds
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
parent
e6b808e86f
commit
4d61040184
4 changed files with 189 additions and 107 deletions
|
@ -14,6 +14,13 @@ import {
|
||||||
pointDistance,
|
pointDistance,
|
||||||
pointFromArray,
|
pointFromArray,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
|
bezierEquation,
|
||||||
|
curve,
|
||||||
|
curveTangent,
|
||||||
|
vectorNormalize,
|
||||||
|
vectorNormal,
|
||||||
|
vectorScale,
|
||||||
|
pointFromVector,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
||||||
|
@ -476,11 +483,7 @@ export const getRectangleBoxAbsoluteCoords = (boxSceneCoords: RectangleBox) => {
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDiamondPoints = (
|
export const getDiamondPoints = (element: ExcalidrawElement) => {
|
||||||
element: ExcalidrawElement,
|
|
||||||
wPadding: number = 0,
|
|
||||||
hPadding: number = 0,
|
|
||||||
) => {
|
|
||||||
// Here we add +1 to avoid these numbers to be 0
|
// Here we add +1 to avoid these numbers to be 0
|
||||||
// otherwise rough.js will throw an error complaining about it
|
// otherwise rough.js will throw an error complaining about it
|
||||||
const topX = Math.floor(element.width / 2) + 1;
|
const topX = Math.floor(element.width / 2) + 1;
|
||||||
|
@ -492,16 +495,7 @@ export const getDiamondPoints = (
|
||||||
const leftX = 0;
|
const leftX = 0;
|
||||||
const leftY = rightY;
|
const leftY = rightY;
|
||||||
|
|
||||||
return [
|
return [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY];
|
||||||
topX,
|
|
||||||
topY - hPadding,
|
|
||||||
rightX + wPadding,
|
|
||||||
rightY,
|
|
||||||
bottomX,
|
|
||||||
bottomY + hPadding,
|
|
||||||
leftX - wPadding,
|
|
||||||
leftY,
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// reference: https://eliot-jones.com/2019/12/cubic-bezier-curve-bounding-boxes
|
// reference: https://eliot-jones.com/2019/12/cubic-bezier-curve-bounding-boxes
|
||||||
|
@ -1159,3 +1153,26 @@ export const doBoundsIntersect = (
|
||||||
|
|
||||||
return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2;
|
return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function offsetBezier(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -51,6 +51,7 @@ import {
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
getDiamondPoints,
|
getDiamondPoints,
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
|
offsetBezier,
|
||||||
} from "@excalidraw/element/bounds";
|
} from "@excalidraw/element/bounds";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -198,10 +199,6 @@ const strokeDiamondWithRotation = (
|
||||||
padding: number,
|
padding: number,
|
||||||
element: ExcalidrawDiamondElement,
|
element: ExcalidrawDiamondElement,
|
||||||
) => {
|
) => {
|
||||||
const { width, height } = element;
|
|
||||||
const side = Math.hypot(width, height);
|
|
||||||
const wPaddingMax = (1.8 * (padding * side)) / height;
|
|
||||||
const hPaddingMax = (1.8 * (padding * side)) / width;
|
|
||||||
const [x, y] = pointRotateRads(
|
const [x, y] = pointRotateRads(
|
||||||
pointFrom<GlobalPoint>(element.x, element.y),
|
pointFrom<GlobalPoint>(element.x, element.y),
|
||||||
elementCenterPoint(element),
|
elementCenterPoint(element),
|
||||||
|
@ -215,56 +212,59 @@ const strokeDiamondWithRotation = (
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
|
|
||||||
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
||||||
getDiamondPoints(element, wPaddingMax, hPaddingMax);
|
getDiamondPoints(element);
|
||||||
if (element.roundness) {
|
if (element.roundness) {
|
||||||
const verticalRadius = getCornerRadius(Math.abs(topX - leftX), element);
|
const verticalRadius = getCornerRadius(Math.abs(topX - leftX), element);
|
||||||
const horizontalRadius = getCornerRadius(
|
const horizontalRadius = getCornerRadius(
|
||||||
Math.abs(rightY - topY),
|
Math.abs(rightY - topY),
|
||||||
element,
|
element,
|
||||||
);
|
);
|
||||||
|
const topApprox = offsetBezier(
|
||||||
|
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
||||||
|
pointFrom(topX, topY),
|
||||||
|
pointFrom(topX, topY),
|
||||||
|
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
||||||
|
padding,
|
||||||
|
);
|
||||||
|
const rightApprox = offsetBezier(
|
||||||
|
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
||||||
|
pointFrom(rightX, rightY),
|
||||||
|
pointFrom(rightX, rightY),
|
||||||
|
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
||||||
|
padding,
|
||||||
|
);
|
||||||
|
const bottomApprox = offsetBezier(
|
||||||
|
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
||||||
|
pointFrom(bottomX, bottomY),
|
||||||
|
pointFrom(bottomX, bottomY),
|
||||||
|
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
||||||
|
padding,
|
||||||
|
);
|
||||||
|
const leftApprox = offsetBezier(
|
||||||
|
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
||||||
|
pointFrom(leftX, leftY),
|
||||||
|
pointFrom(leftX, leftY),
|
||||||
|
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
||||||
|
padding,
|
||||||
|
);
|
||||||
|
|
||||||
context.moveTo(topX + verticalRadius, topY + horizontalRadius);
|
context.moveTo(
|
||||||
context.lineTo(rightX - verticalRadius, rightY - horizontalRadius);
|
topApprox[topApprox.length - 1][0],
|
||||||
context.bezierCurveTo(
|
topApprox[topApprox.length - 1][1],
|
||||||
rightX,
|
|
||||||
rightY,
|
|
||||||
rightX,
|
|
||||||
rightY,
|
|
||||||
rightX - verticalRadius,
|
|
||||||
rightY + horizontalRadius,
|
|
||||||
);
|
|
||||||
context.lineTo(bottomX + verticalRadius, bottomY - horizontalRadius);
|
|
||||||
context.bezierCurveTo(
|
|
||||||
bottomX,
|
|
||||||
bottomY,
|
|
||||||
bottomX,
|
|
||||||
bottomY,
|
|
||||||
bottomX - verticalRadius,
|
|
||||||
bottomY - horizontalRadius,
|
|
||||||
);
|
|
||||||
context.lineTo(leftX + verticalRadius, leftY + horizontalRadius);
|
|
||||||
context.bezierCurveTo(
|
|
||||||
leftX,
|
|
||||||
leftY,
|
|
||||||
leftX,
|
|
||||||
leftY,
|
|
||||||
leftX + verticalRadius,
|
|
||||||
leftY - horizontalRadius,
|
|
||||||
);
|
|
||||||
context.lineTo(topX - verticalRadius, topY + horizontalRadius);
|
|
||||||
context.bezierCurveTo(
|
|
||||||
topX,
|
|
||||||
topY,
|
|
||||||
topX,
|
|
||||||
topY,
|
|
||||||
topX + verticalRadius,
|
|
||||||
topY + horizontalRadius,
|
|
||||||
);
|
);
|
||||||
|
context.lineTo(rightApprox[0][0], rightApprox[0][1]);
|
||||||
|
drawCatmullRom(context, rightApprox);
|
||||||
|
context.lineTo(bottomApprox[0][0], bottomApprox[0][1]);
|
||||||
|
drawCatmullRom(context, bottomApprox);
|
||||||
|
context.lineTo(leftApprox[0][0], leftApprox[0][1]);
|
||||||
|
drawCatmullRom(context, leftApprox);
|
||||||
|
context.lineTo(topApprox[0][0], topApprox[0][1]);
|
||||||
|
drawCatmullRom(context, topApprox);
|
||||||
} else {
|
} else {
|
||||||
context.moveTo(topX, topY);
|
context.moveTo(topX, topY - padding);
|
||||||
context.lineTo(rightX, rightY);
|
context.lineTo(rightX + padding, rightY);
|
||||||
context.lineTo(bottomX, bottomY);
|
context.lineTo(bottomX, bottomY + padding);
|
||||||
context.lineTo(leftX, leftY);
|
context.lineTo(leftX - padding, leftY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -273,62 +273,63 @@ const strokeDiamondWithRotation = (
|
||||||
// sharp inset edges on line joins < 90 degrees.
|
// sharp inset edges on line joins < 90 degrees.
|
||||||
{
|
{
|
||||||
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
||||||
getDiamondPoints(element, 5, 5);
|
getDiamondPoints(element);
|
||||||
if (element.roundness) {
|
if (element.roundness) {
|
||||||
const verticalRadius = getCornerRadius(Math.abs(topX - leftX), element);
|
const verticalRadius = getCornerRadius(Math.abs(topX - leftX), element);
|
||||||
const horizontalRadius = getCornerRadius(
|
const horizontalRadius = getCornerRadius(
|
||||||
Math.abs(rightY - topY),
|
Math.abs(rightY - topY),
|
||||||
element,
|
element,
|
||||||
);
|
);
|
||||||
|
const topApprox = offsetBezier(
|
||||||
|
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
||||||
|
pointFrom(topX, topY),
|
||||||
|
pointFrom(topX, topY),
|
||||||
|
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
||||||
|
-5,
|
||||||
|
);
|
||||||
|
const rightApprox = offsetBezier(
|
||||||
|
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
||||||
|
pointFrom(rightX, rightY),
|
||||||
|
pointFrom(rightX, rightY),
|
||||||
|
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
||||||
|
-5,
|
||||||
|
);
|
||||||
|
const bottomApprox = offsetBezier(
|
||||||
|
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
||||||
|
pointFrom(bottomX, bottomY),
|
||||||
|
pointFrom(bottomX, bottomY),
|
||||||
|
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
||||||
|
-5,
|
||||||
|
);
|
||||||
|
const leftApprox = offsetBezier(
|
||||||
|
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
||||||
|
pointFrom(leftX, leftY),
|
||||||
|
pointFrom(leftX, leftY),
|
||||||
|
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
||||||
|
-5,
|
||||||
|
);
|
||||||
|
|
||||||
context.moveTo(topX - verticalRadius, topY + horizontalRadius);
|
context.moveTo(
|
||||||
context.lineTo(leftX + verticalRadius, leftY - horizontalRadius);
|
topApprox[topApprox.length - 1][0],
|
||||||
context.bezierCurveTo(
|
topApprox[topApprox.length - 1][1],
|
||||||
leftX,
|
|
||||||
leftY,
|
|
||||||
leftX,
|
|
||||||
leftY,
|
|
||||||
leftX + verticalRadius,
|
|
||||||
leftY + horizontalRadius,
|
|
||||||
);
|
|
||||||
context.lineTo(bottomX - verticalRadius, bottomY - horizontalRadius);
|
|
||||||
context.bezierCurveTo(
|
|
||||||
bottomX,
|
|
||||||
bottomY,
|
|
||||||
bottomX,
|
|
||||||
bottomY,
|
|
||||||
bottomX + verticalRadius,
|
|
||||||
bottomY - horizontalRadius,
|
|
||||||
);
|
|
||||||
context.lineTo(rightX - verticalRadius, rightY + horizontalRadius);
|
|
||||||
context.bezierCurveTo(
|
|
||||||
rightX,
|
|
||||||
rightY,
|
|
||||||
rightX,
|
|
||||||
rightY,
|
|
||||||
rightX - verticalRadius,
|
|
||||||
rightY - horizontalRadius,
|
|
||||||
);
|
|
||||||
context.lineTo(topX + verticalRadius, topY + horizontalRadius);
|
|
||||||
context.bezierCurveTo(
|
|
||||||
topX,
|
|
||||||
topY,
|
|
||||||
topX,
|
|
||||||
topY,
|
|
||||||
topX - verticalRadius,
|
|
||||||
topY + horizontalRadius,
|
|
||||||
);
|
);
|
||||||
|
context.lineTo(leftApprox[0][0], leftApprox[0][1]);
|
||||||
|
drawCatmullRom(context, leftApprox);
|
||||||
|
context.lineTo(bottomApprox[0][0], bottomApprox[0][1]);
|
||||||
|
drawCatmullRom(context, bottomApprox);
|
||||||
|
context.lineTo(rightApprox[0][0], rightApprox[0][1]);
|
||||||
|
drawCatmullRom(context, rightApprox);
|
||||||
|
context.lineTo(topApprox[0][0], topApprox[0][1]);
|
||||||
|
drawCatmullRom(context, topApprox);
|
||||||
} else {
|
} else {
|
||||||
context.moveTo(topX, topY);
|
context.moveTo(topX, topY - 5);
|
||||||
context.lineTo(leftX, leftY);
|
context.lineTo(leftX + 5, leftY);
|
||||||
context.lineTo(bottomX, bottomY);
|
context.lineTo(bottomX, bottomY + 5);
|
||||||
context.lineTo(rightX, rightY);
|
context.lineTo(rightX - 5, rightY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context.closePath();
|
context.closePath();
|
||||||
context.fill();
|
context.fill();
|
||||||
}
|
|
||||||
|
|
||||||
context.restore();
|
context.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -388,7 +389,8 @@ const renderBindingHighlightForBindableElement = (
|
||||||
maxBindingGap(element, element.width, element.height, zoom) -
|
maxBindingGap(element, element.width, element.height, zoom) -
|
||||||
BINDING_HIGHLIGHT_OFFSET;
|
BINDING_HIGHLIGHT_OFFSET;
|
||||||
// To ensure the binding highlight doesn't overlap the element itself
|
// To ensure the binding highlight doesn't overlap the element itself
|
||||||
const padding = context.lineWidth / 2 + BINDING_HIGHLIGHT_OFFSET;
|
const padding = maxBindingGap(element, element.width, element.height, zoom);
|
||||||
|
//context.lineWidth / 2 + BINDING_HIGHLIGHT_OFFSET;
|
||||||
|
|
||||||
const radius = getCornerRadius(
|
const radius = getCornerRadius(
|
||||||
Math.min(element.width, element.height),
|
Math.min(element.width, element.height),
|
||||||
|
@ -1354,3 +1356,39 @@ export const renderInteractiveScene = <
|
||||||
renderConfig.callback(ret);
|
renderConfig.callback(ret);
|
||||||
return ret as T extends true ? void : ReturnType<U>;
|
return ret as T extends true ? void : ReturnType<U>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function drawCatmullRom(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
points: GlobalPoint[],
|
||||||
|
segments = 20,
|
||||||
|
) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ import type { Bounds } from "@excalidraw/element/bounds";
|
||||||
import { isPoint, pointDistance, pointFrom } from "./point";
|
import { isPoint, pointDistance, pointFrom } from "./point";
|
||||||
import { rectangle, rectangleIntersectLineSegment } from "./rectangle";
|
import { rectangle, rectangleIntersectLineSegment } from "./rectangle";
|
||||||
|
|
||||||
|
import { vector } from "./vector";
|
||||||
|
|
||||||
import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
|
import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -82,7 +84,7 @@ function solve(
|
||||||
return [t0, s0];
|
return [t0, s0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const bezierEquation = <Point extends GlobalPoint | LocalPoint>(
|
export const bezierEquation = <Point extends GlobalPoint | LocalPoint>(
|
||||||
c: Curve<Point>,
|
c: Curve<Point>,
|
||||||
t: number,
|
t: number,
|
||||||
) =>
|
) =>
|
||||||
|
@ -274,6 +276,26 @@ export function isCurve<P extends GlobalPoint | LocalPoint>(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function curveTangent<Point extends GlobalPoint | LocalPoint>(
|
||||||
|
[p0, p1, p2, p3]: Curve<Point>,
|
||||||
|
t: number,
|
||||||
|
) {
|
||||||
|
return vector(
|
||||||
|
-3 * (1 - t) * (1 - t) * p0[0] +
|
||||||
|
3 * (1 - t) * (1 - t) * p1[0] -
|
||||||
|
6 * t * (1 - t) * p1[0] -
|
||||||
|
3 * t * t * p2[0] +
|
||||||
|
6 * t * (1 - t) * p2[0] +
|
||||||
|
3 * t * t * p3[0],
|
||||||
|
-3 * (1 - t) * (1 - t) * p0[1] +
|
||||||
|
3 * (1 - t) * (1 - t) * p1[1] -
|
||||||
|
6 * t * (1 - t) * p1[1] -
|
||||||
|
3 * t * t * p2[1] +
|
||||||
|
6 * t * (1 - t) * p2[1] +
|
||||||
|
3 * t * t * p3[1],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function curveBounds<Point extends GlobalPoint | LocalPoint>(
|
function curveBounds<Point extends GlobalPoint | LocalPoint>(
|
||||||
c: Curve<Point>,
|
c: Curve<Point>,
|
||||||
): Bounds {
|
): Bounds {
|
||||||
|
|
|
@ -143,3 +143,8 @@ export const vectorNormalize = (v: Vector): Vector => {
|
||||||
|
|
||||||
return vector(v[0] / m, v[1] / m);
|
return vector(v[0] / m, v[1] / m);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the right-hand normal of the vector.
|
||||||
|
*/
|
||||||
|
export const vectorNormal = (v: Vector): Vector => vector(v[1], -v[0]);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue