290 lines
7.4 KiB
TypeScript
290 lines
7.4 KiB
TypeScript
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 interface PaddingMm {
|
|
top: number;
|
|
right: number;
|
|
bottom: number;
|
|
left: number;
|
|
}
|
|
|
|
export interface BorderSideStyle {
|
|
width: number;
|
|
color: string;
|
|
}
|
|
|
|
export type BorderConfig = Record<ElementSide, BorderSideStyle>;
|
|
|
|
export function resolvePaddingMm(elem: TemplateElement): PaddingMm {
|
|
const base = elem.padding ?? 0;
|
|
const val = (specific?: number) =>
|
|
specific !== undefined && specific !== null ? specific : base;
|
|
return {
|
|
top: val(elem.paddingTop),
|
|
right: val(elem.paddingRight),
|
|
bottom: val(elem.paddingBottom),
|
|
left: val(elem.paddingLeft),
|
|
};
|
|
}
|
|
|
|
export function getPaddedRectPx(
|
|
x: number,
|
|
y: number,
|
|
w: number,
|
|
h: number,
|
|
elem: TemplateElement,
|
|
scale: number
|
|
) {
|
|
const pad = resolvePaddingMm(elem);
|
|
const top = Math.round(pad.top * scale);
|
|
const right = Math.round(pad.right * scale);
|
|
const bottom = Math.round(pad.bottom * scale);
|
|
const left = Math.round(pad.left * scale);
|
|
return {
|
|
x: x + left,
|
|
y: y + top,
|
|
w: Math.max(1, w - left - right),
|
|
h: Math.max(1, h - top - bottom),
|
|
paddingPx: { top, right, bottom, left },
|
|
};
|
|
}
|
|
|
|
export function resolveBorderConfig(elem: TemplateElement): BorderConfig {
|
|
const baseWidth = elem.borderWidth ?? 0;
|
|
const baseColor = elem.borderColor || '#111827';
|
|
const legacy = elem.borderSides ?? {};
|
|
|
|
const resolveSide = (
|
|
side: ElementSide,
|
|
widthOverride: number | undefined,
|
|
colorOverride: string | undefined
|
|
): BorderSideStyle => {
|
|
let width: number;
|
|
if (widthOverride !== undefined && widthOverride !== null) {
|
|
width = widthOverride;
|
|
} else if (baseWidth <= 0 || legacy[side] === false) {
|
|
width = 0;
|
|
} else {
|
|
width = baseWidth;
|
|
}
|
|
return {
|
|
width,
|
|
color: colorOverride?.trim() ? colorOverride : baseColor,
|
|
};
|
|
};
|
|
|
|
return {
|
|
top: resolveSide('top', elem.borderWidthTop, elem.borderColorTop),
|
|
right: resolveSide('right', elem.borderWidthRight, elem.borderColorRight),
|
|
bottom: resolveSide('bottom', elem.borderWidthBottom, elem.borderColorBottom),
|
|
left: resolveSide('left', elem.borderWidthLeft, elem.borderColorLeft),
|
|
};
|
|
}
|
|
|
|
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 cfg = resolveBorderConfig(elem);
|
|
return {
|
|
top: cfg.top.width > 0,
|
|
right: cfg.right.width > 0,
|
|
bottom: cfg.bottom.width > 0,
|
|
left: cfg.left.width > 0,
|
|
};
|
|
}
|
|
|
|
/** 构建顺时针闭合圆角矩形路径 */
|
|
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 border = resolveBorderConfig(elem);
|
|
const { tl, tr, br, bl } = radii;
|
|
|
|
ctx.lineCap = 'butt';
|
|
ctx.lineJoin = 'round';
|
|
|
|
if (sides.top) {
|
|
const lw = Math.max(1, border.top.width * scale);
|
|
const inset = lw / 2;
|
|
ctx.strokeStyle = border.top.color;
|
|
ctx.lineWidth = lw;
|
|
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) {
|
|
const lw = Math.max(1, border.right.width * scale);
|
|
const inset = lw / 2;
|
|
ctx.strokeStyle = border.right.color;
|
|
ctx.lineWidth = lw;
|
|
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) {
|
|
const lw = Math.max(1, border.bottom.width * scale);
|
|
const inset = lw / 2;
|
|
ctx.strokeStyle = border.bottom.color;
|
|
ctx.lineWidth = lw;
|
|
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) {
|
|
const lw = Math.max(1, border.left.width * scale);
|
|
const inset = lw / 2;
|
|
ctx.strokeStyle = border.left.color;
|
|
ctx.lineWidth = lw;
|
|
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 };
|
|
}
|