init commit

This commit is contained in:
iqudoo
2026-06-12 19:34:27 +08:00
commit 89b59ecc1b
38 changed files with 17719 additions and 0 deletions

221
src/elementBox.ts Normal file
View File

@@ -0,0 +1,221 @@
import { TemplateElement, ElementSide } from './types';
import { resolveElementBackgroundFill } from './utils';
export interface CornerRadiiPx {
tl: number;
tr: number;
br: number;
bl: number;
}
export interface BorderSides {
top: boolean;
right: boolean;
bottom: boolean;
left: boolean;
}
export function resolveCornerRadiiPx(
elem: TemplateElement,
scale: number,
w: number,
h: number
): CornerRadiiPx {
const maxR = Math.min(w, h) / 2;
const baseMm = elem.borderRadius ?? 0;
const radiusFor = (specificMm?: number): number => {
const mm = specificMm !== undefined && specificMm !== null ? specificMm : baseMm;
if (mm <= 0) return 0;
return Math.min(maxR, mm * scale);
};
return {
tl: radiusFor(elem.borderRadiusTL),
tr: radiusFor(elem.borderRadiusTR),
br: radiusFor(elem.borderRadiusBR),
bl: radiusFor(elem.borderRadiusBL),
};
}
export function resolveBorderSides(elem: TemplateElement): BorderSides {
const width = elem.borderWidth ?? 0;
if (width <= 0) {
return { top: false, right: false, bottom: false, left: false };
}
const sides = elem.borderSides ?? {};
return {
top: sides.top !== false,
right: sides.right !== false,
bottom: sides.bottom !== false,
left: sides.left !== false,
};
}
/** 构建顺时针闭合圆角矩形路径 */
export function traceRoundedRect(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
w: number,
h: number,
r: CornerRadiiPx
) {
const { tl, tr, br, bl } = r;
ctx.beginPath();
ctx.moveTo(x + tl, y);
ctx.lineTo(x + w - tr, y);
if (tr > 0) ctx.arcTo(x + w, y, x + w, y + tr, tr);
else ctx.lineTo(x + w, y);
ctx.lineTo(x + w, y + h - br);
if (br > 0) ctx.arcTo(x + w, y + h, x + w - br, y + h, br);
else ctx.lineTo(x + w, y + h);
ctx.lineTo(x + bl, y + h);
if (bl > 0) ctx.arcTo(x, y + h, x, y + h - bl, bl);
else ctx.lineTo(x, y + h);
ctx.lineTo(x, y + tl);
if (tl > 0) ctx.arcTo(x, y, x + tl, y, tl);
else ctx.lineTo(x, y);
ctx.closePath();
}
export function drawElementFill(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
w: number,
h: number,
elem: TemplateElement,
radii: CornerRadiiPx
) {
const fill = resolveElementBackgroundFill(elem.backgroundColor, elem.backgroundOpacity);
if (!fill) return;
traceRoundedRect(ctx, x, y, w, h, radii);
ctx.fillStyle = fill;
ctx.fill();
}
export function drawElementBorders(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
w: number,
h: number,
radii: CornerRadiiPx,
sides: BorderSides,
elem: TemplateElement,
scale: number
) {
const widthMm = elem.borderWidth ?? 0;
if (widthMm <= 0) return;
const color = elem.borderColor || '#111827';
const lw = Math.max(1, widthMm * scale);
const { tl, tr, br, bl } = radii;
const inset = lw / 2;
ctx.strokeStyle = color;
ctx.lineWidth = lw;
ctx.lineCap = 'butt';
ctx.lineJoin = 'round';
if (sides.top) {
ctx.beginPath();
if (sides.left && tl > 0) {
ctx.arc(x + tl, y + tl, Math.max(0, tl - inset), Math.PI, 1.5 * Math.PI);
} else {
ctx.moveTo(x, y + inset);
}
if (sides.right && tr > 0) {
ctx.lineTo(x + w - tr, y + inset);
ctx.arc(x + w - tr, y + tr, Math.max(0, tr - inset), 1.5 * Math.PI, 2 * Math.PI);
} else {
ctx.lineTo(x + w, y + inset);
}
ctx.stroke();
}
if (sides.right) {
ctx.beginPath();
const topStart = sides.top && tr > 0;
if (topStart) {
ctx.moveTo(x + w - inset, y + tr);
} else {
ctx.moveTo(x + w - inset, y);
}
if (sides.bottom && br > 0) {
ctx.lineTo(x + w - inset, y + h - br);
ctx.arc(x + w - br, y + h - br, Math.max(0, br - inset), 0, 0.5 * Math.PI);
} else {
ctx.lineTo(x + w - inset, y + h);
}
ctx.stroke();
}
if (sides.bottom) {
ctx.beginPath();
if (sides.right && br > 0) {
ctx.moveTo(x + w - inset, y + h - br);
ctx.arc(x + w - br, y + h - br, Math.max(0, br - inset), 0, 0.5 * Math.PI);
} else {
ctx.moveTo(x + w - inset, y + h - inset);
}
if (sides.left && bl > 0) {
ctx.lineTo(x + bl, y + h - inset);
ctx.arc(x + bl, y + h - bl, Math.max(0, bl - inset), 0.5 * Math.PI, Math.PI);
} else {
ctx.lineTo(x, y + h - inset);
}
ctx.stroke();
}
if (sides.left) {
ctx.beginPath();
const bottomStart = sides.bottom && bl > 0;
if (bottomStart) {
ctx.moveTo(x + inset, y + h - bl);
} else {
ctx.moveTo(x + inset, y + h);
}
if (sides.top && tl > 0) {
ctx.lineTo(x + inset, y + tl);
ctx.arc(x + tl, y + tl, Math.max(0, tl - inset), Math.PI, 1.5 * Math.PI);
} else {
ctx.lineTo(x + inset, y);
}
ctx.stroke();
}
}
export function drawElementChrome(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
w: number,
h: number,
elem: TemplateElement,
scale: number
) {
const radii = resolveCornerRadiiPx(elem, scale, w, h);
const sides = resolveBorderSides(elem);
drawElementFill(ctx, x, y, w, h, elem, radii);
drawElementBorders(ctx, x, y, w, h, radii, sides, elem, scale);
return { radii, sides };
}
export function toggleSide(
current: Partial<Record<ElementSide, boolean>> | undefined,
side: ElementSide
): Partial<Record<ElementSide, boolean>> {
const next = { ...current };
const enabled = current?.[side] !== false;
next[side] = !enabled;
return next;
}
export function isSideEnabled(
sides: Partial<Record<ElementSide, boolean>> | undefined,
side: ElementSide
): boolean {
return sides?.[side] !== false;
}