mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-21 00:07:01 +08:00
233 lines
5.9 KiB
TypeScript
233 lines
5.9 KiB
TypeScript
interface CornerPathParams {
|
|
a: number;
|
|
b: number;
|
|
c: number;
|
|
d: number;
|
|
p: number;
|
|
cornerRadius: number;
|
|
arcSectionLength: number;
|
|
}
|
|
|
|
interface CornerParams {
|
|
cornerRadius: number;
|
|
cornerSmoothing: number;
|
|
preserveSmoothing: boolean;
|
|
roundingAndSmoothingBudget: number;
|
|
}
|
|
|
|
// The article from figma's blog
|
|
// https://www.figma.com/blog/desperately-seeking-squircles/
|
|
//
|
|
// The original code by MartinRGB
|
|
// https://github.com/MartinRGB/Figma_Squircles_Approximation/blob/bf29714aab58c54329f3ca130ffa16d39a2ff08c/js/rounded-corners.js#L64
|
|
export function getPathParamsForCorner({
|
|
cornerRadius,
|
|
cornerSmoothing,
|
|
preserveSmoothing,
|
|
roundingAndSmoothingBudget,
|
|
}: CornerParams): CornerPathParams {
|
|
// From figure 12.2 in the article
|
|
// p = (1 + cornerSmoothing) * q
|
|
// in this case q = R because theta = 90deg
|
|
let p = (1 + cornerSmoothing) * cornerRadius;
|
|
|
|
// When there's not enough space left (p > roundingAndSmoothingBudget), there are 2 options:
|
|
//
|
|
// 1. What figma's currently doing: limit the smoothing value to make sure p <= roundingAndSmoothingBudget
|
|
// But what this means is that at some point when cornerRadius is large enough,
|
|
// increasing the smoothing value wouldn't do anything
|
|
//
|
|
// 2. Keep the original smoothing value and use it to calculate the bezier curve normally,
|
|
// then adjust the control points to achieve similar curvature profile
|
|
//
|
|
// preserveSmoothing is a new option I added
|
|
//
|
|
// If preserveSmoothing is on then we'll just keep using the original smoothing value
|
|
// and adjust the bezier curve later
|
|
if (!preserveSmoothing) {
|
|
const maxCornerSmoothing = roundingAndSmoothingBudget / cornerRadius - 1;
|
|
cornerSmoothing = Math.min(cornerSmoothing, maxCornerSmoothing);
|
|
p = Math.min(p, roundingAndSmoothingBudget);
|
|
}
|
|
|
|
// In a normal rounded rectangle (cornerSmoothing = 0), this is 90
|
|
// The larger the smoothing, the smaller the arc
|
|
const arcMeasure = 90 * (1 - cornerSmoothing);
|
|
const arcSectionLength =
|
|
Math.sin(toRadians(arcMeasure / 2)) * cornerRadius * Math.sqrt(2);
|
|
|
|
// In the article this is the distance between 2 control points: P3 and P4
|
|
const angleAlpha = (90 - arcMeasure) / 2;
|
|
const p3ToP4Distance = cornerRadius * Math.tan(toRadians(angleAlpha / 2));
|
|
|
|
// a, b, c and d are from figure 11.1 in the article
|
|
const angleBeta = 45 * cornerSmoothing;
|
|
const c = p3ToP4Distance * Math.cos(toRadians(angleBeta));
|
|
const d = c * Math.tan(toRadians(angleBeta));
|
|
|
|
let b = (p - arcSectionLength - c - d) / 3;
|
|
let a = 2 * b;
|
|
|
|
// Adjust the P1 and P2 control points if there's not enough space left
|
|
if (preserveSmoothing && p > roundingAndSmoothingBudget) {
|
|
const p1ToP3MaxDistance =
|
|
roundingAndSmoothingBudget - d - arcSectionLength - c;
|
|
|
|
// Try to maintain some distance between P1 and P2 so the curve wouldn't look weird
|
|
const minA = p1ToP3MaxDistance / 6;
|
|
const maxB = p1ToP3MaxDistance - minA;
|
|
|
|
b = Math.min(b, maxB);
|
|
a = p1ToP3MaxDistance - b;
|
|
p = Math.min(p, roundingAndSmoothingBudget);
|
|
}
|
|
|
|
return {
|
|
a,
|
|
b,
|
|
c,
|
|
d,
|
|
p,
|
|
arcSectionLength,
|
|
cornerRadius,
|
|
};
|
|
}
|
|
|
|
interface SVGPathInput {
|
|
width: number;
|
|
height: number;
|
|
topRightPathParams: CornerPathParams;
|
|
bottomRightPathParams: CornerPathParams;
|
|
bottomLeftPathParams: CornerPathParams;
|
|
topLeftPathParams: CornerPathParams;
|
|
}
|
|
|
|
export function getSVGPathFromPathParams({
|
|
width,
|
|
height,
|
|
topLeftPathParams,
|
|
topRightPathParams,
|
|
bottomLeftPathParams,
|
|
bottomRightPathParams,
|
|
}: SVGPathInput) {
|
|
return `
|
|
M ${width - topRightPathParams.p} 0
|
|
${drawTopRightPath(topRightPathParams)}
|
|
L ${width} ${height - bottomRightPathParams.p}
|
|
${drawBottomRightPath(bottomRightPathParams)}
|
|
L ${bottomLeftPathParams.p} ${height}
|
|
${drawBottomLeftPath(bottomLeftPathParams)}
|
|
L 0 ${topLeftPathParams.p}
|
|
${drawTopLeftPath(topLeftPathParams)}
|
|
Z
|
|
`
|
|
.replace(/[\t\s\n]+/g, ' ')
|
|
.trim();
|
|
}
|
|
|
|
function drawTopRightPath({
|
|
cornerRadius,
|
|
a,
|
|
b,
|
|
c,
|
|
d,
|
|
p,
|
|
arcSectionLength,
|
|
}: CornerPathParams) {
|
|
if (cornerRadius) {
|
|
return rounded`
|
|
c ${a} 0 ${a + b} 0 ${a + b + c} ${d}
|
|
a ${cornerRadius} ${cornerRadius} 0 0 1 ${arcSectionLength} ${arcSectionLength}
|
|
c ${d} ${c}
|
|
${d} ${b + c}
|
|
${d} ${a + b + c}`;
|
|
} else {
|
|
return rounded`l ${p} 0`;
|
|
}
|
|
}
|
|
|
|
function drawBottomRightPath({
|
|
cornerRadius,
|
|
a,
|
|
b,
|
|
c,
|
|
d,
|
|
p,
|
|
arcSectionLength,
|
|
}: CornerPathParams) {
|
|
if (cornerRadius) {
|
|
return rounded`
|
|
c 0 ${a}
|
|
0 ${a + b}
|
|
${-d} ${a + b + c}
|
|
a ${cornerRadius} ${cornerRadius} 0 0 1 -${arcSectionLength} ${arcSectionLength}
|
|
c ${-c} ${d}
|
|
${-(b + c)} ${d}
|
|
${-(a + b + c)} ${d}`;
|
|
} else {
|
|
return rounded`l 0 ${p}`;
|
|
}
|
|
}
|
|
|
|
function drawBottomLeftPath({
|
|
cornerRadius,
|
|
a,
|
|
b,
|
|
c,
|
|
d,
|
|
p,
|
|
arcSectionLength,
|
|
}: CornerPathParams) {
|
|
if (cornerRadius) {
|
|
return rounded`
|
|
c ${-a} 0
|
|
${-(a + b)} 0
|
|
${-(a + b + c)} ${-d}
|
|
a ${cornerRadius} ${cornerRadius} 0 0 1 -${arcSectionLength} -${arcSectionLength}
|
|
c ${-d} ${-c}
|
|
${-d} ${-(b + c)}
|
|
${-d} ${-(a + b + c)}`;
|
|
} else {
|
|
return rounded`l ${-p} 0`;
|
|
}
|
|
}
|
|
|
|
function drawTopLeftPath({
|
|
cornerRadius,
|
|
a,
|
|
b,
|
|
c,
|
|
d,
|
|
p,
|
|
arcSectionLength,
|
|
}: CornerPathParams) {
|
|
if (cornerRadius) {
|
|
return rounded`
|
|
c 0 ${-a}
|
|
0 ${-(a + b)}
|
|
${d} ${-(a + b + c)}
|
|
a ${cornerRadius} ${cornerRadius} 0 0 1 ${arcSectionLength} -${arcSectionLength}
|
|
c ${c} ${-d}
|
|
${b + c} ${-d}
|
|
${a + b + c} ${-d}`;
|
|
} else {
|
|
return rounded`l 0 ${-p}`;
|
|
}
|
|
}
|
|
|
|
function toRadians(degrees: number) {
|
|
return (degrees * Math.PI) / 180;
|
|
}
|
|
|
|
function rounded(strings: TemplateStringsArray, ...values: number[]): string {
|
|
return strings.reduce((acc, str, i) => {
|
|
const value = values[i];
|
|
|
|
if (typeof value === 'number') {
|
|
return acc + str + value.toFixed(4);
|
|
} else {
|
|
return acc + str + (value ?? '');
|
|
}
|
|
}, '');
|
|
}
|