/** * 标题样式工具(#2001 / 模板系统 #2003) * * - snake_case TitleStyleConfig ↔ camelCase TitleSettings 互转 * - preset 归一化预览(修复"标题"两字大小不一) */ import type { TitleStyleConfig } from "./types" import type { TitleSettings } from "../../pages/generate/types" import { TITLE_PRESETS } from "./constants" import { DEFAULT_TITLE_SETTINGS_FULL } from "../../pages/generate/types" import type { TitleTemplate } from "./template-types" /** snake_case TitleStyleConfig → camelCase TitleSettings(仅覆盖已知字段) */ export function titleStyleConfigToCamel(s: Partial): Partial { const out: Partial = {} if (s.font != null) out.font = s.font if (s.size != null) out.size = s.size if (s.color != null) out.color = s.color if (s.bold != null) out.bold = s.bold if (s.italic != null) out.italic = s.italic if (s.position != null) out.position = s.position as TitleSettings["position"] if (s.pos_x != null) out.posX = s.pos_x if (s.pos_y != null) out.posY = s.pos_y if (s.line_height != null) out.lineHeight = s.line_height if (s.margin_top != null) out.marginTop = s.margin_top if (s.max_chars_per_line != null) out.maxCharsPerLine = s.max_chars_per_line if (s.stroke != null) out.stroke = s.stroke if (s.stroke_width != null) out.strokeWidth = s.stroke_width if (s.stroke_color != null) out.strokeColor = s.stroke_color if (s.shadow != null) out.shadow = s.shadow if (s.shadow_offset_x != null) out.shadowOffsetX = s.shadow_offset_x if (s.shadow_offset_y != null) out.shadowOffsetY = s.shadow_offset_y if (s.shadow_blur != null) out.shadowBlur = s.shadow_blur if (s.shadow_color != null) out.shadowColor = s.shadow_color if (s.bg_enabled != null) out.bgEnabled = s.bg_enabled if (s.bg_color != null) out.bgColor = s.bg_color if (s.bg_padding != null) out.bgPadding = s.bg_padding if (s.bg_radius != null) out.bgRadius = s.bg_radius if (s.line_overrides != null) out.lineOverrides = s.line_overrides return out } /** camelCase TitleSettings patch → snake_case TitleStyleConfig patch */ export function camelToTitleStyleConfig(p: Partial): Partial { const out: Partial = {} if (p.font != null) out.font = p.font if (p.size != null) out.size = p.size if (p.color != null) out.color = p.color if (p.bold != null) out.bold = p.bold if (p.italic != null) out.italic = p.italic if (p.position != null) out.position = p.position as TitleStyleConfig["position"] if (p.posX != null) out.pos_x = p.posX if (p.posY != null) out.pos_y = p.posY if (p.lineHeight != null) out.line_height = p.lineHeight if (p.marginTop != null) out.margin_top = p.marginTop if (p.maxCharsPerLine != null) out.max_chars_per_line = p.maxCharsPerLine if (p.stroke != null) out.stroke = p.stroke if (p.strokeWidth != null) out.stroke_width = p.strokeWidth if (p.strokeColor != null) out.stroke_color = p.strokeColor if (p.shadow != null) out.shadow = p.shadow if (p.shadowOffsetX != null) out.shadow_offset_x = p.shadowOffsetX if (p.shadowOffsetY != null) out.shadow_offset_y = p.shadowOffsetY if (p.shadowBlur != null) out.shadow_blur = p.shadowBlur if (p.shadowColor != null) out.shadow_color = p.shadowColor if (p.bgEnabled != null) out.bg_enabled = p.bgEnabled if (p.bgColor != null) out.bg_color = p.bgColor if (p.bgPadding != null) out.bg_padding = p.bgPadding if (p.bgRadius != null) out.bg_radius = p.bgRadius if (p.lineOverrides != null) out.line_overrides = p.lineOverrides return out } /** * 把 preset style(snake_case)归一化为固定字号的 TitleSettings, * 用于"预设卡片"缩略预览——所有卡片视觉上"标题"两字大小一致,便于辨识。 * 描边/阴影/背景padding 按 fixedSize / 原始 size 比例缩放,避免粗描边爆框。 */ export function buildPresetPreviewSettings( base: TitleSettings, presetKey: string, fixedSize = 56, ): TitleSettings { const preset = TITLE_PRESETS.find((p) => p.key === presetKey) if (!preset) return base const origSize = preset.style.size ?? fixedSize const ratio = fixedSize / origSize const scale = (v: number | undefined, fallback: number): number => v != null ? Math.round(v * ratio) : fallback return { ...base, ...titleStyleConfigToCamel(preset.style), size: fixedSize, strokeWidth: scale(preset.style.stroke_width, base.strokeWidth) ?? base.strokeWidth, shadowOffsetX: scale(preset.style.shadow_offset_x, base.shadowOffsetX) ?? base.shadowOffsetX, shadowOffsetY: scale(preset.style.shadow_offset_y, base.shadowOffsetY) ?? base.shadowOffsetY, shadowBlur: scale(preset.style.shadow_blur, base.shadowBlur) ?? base.shadowBlur, bgPadding: scale(preset.style.bg_padding, base.bgPadding) ?? base.bgPadding, lineOverrides: [], } } /** * 把 TitleTemplate 渲染为完整 TitleSettings(带默认值),用于卡片预览。 * 与模板选择器中保持一致,抽出共用。 */ export function templateToPreviewSettings(t: TitleTemplate, fixedSize = 48): TitleSettings { const base: TitleSettings = { ...DEFAULT_TITLE_SETTINGS_FULL, ...titleStyleConfigToCamel(t.style), } // 预览时用固定字号保证所有卡片字大小一致;描边/阴影/padding按比例缩放 const origSize = t.style.size ?? base.size if (origSize && origSize !== fixedSize) { const ratio = fixedSize / origSize base.size = fixedSize if (base.strokeWidth != null) base.strokeWidth = Math.max(1, Math.round(base.strokeWidth * ratio)) if (base.shadowOffsetX != null) base.shadowOffsetX = Math.round(base.shadowOffsetX * ratio) if (base.shadowOffsetY != null) base.shadowOffsetY = Math.round(base.shadowOffsetY * ratio) if (base.shadowBlur != null) base.shadowBlur = Math.round(base.shadowBlur * ratio) if (base.bgPadding != null) base.bgPadding = Math.round(base.bgPadding * ratio) } base.lineOverrides = [] return base }