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; 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 }; }