From 3beb65143b0d4cf431ca33122ce0b352923f2bee Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sun, 27 Sep 2026 10:15:08 +0800 Subject: [PATCH] =?UTF-8?q?fix(title):=20=E4=BF=AE=E5=A4=8D=E7=88=86?= =?UTF-8?q?=E6=AC=BE=E9=A2=84=E8=AE=BE=E7=BC=A9=E7=95=A5=E5=9B=BE=E8=89=B2?= =?UTF-8?q?=E5=9D=97/=E7=BC=BA=E5=AD=97=E5=9B=9E=E5=BD=92bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因(PR#2054字体重构引入): 1. Canvas fillStyle泄漏:bgEnabled预设画完背景矩形后未重置fillStyle为文字颜色, fillText用bgColor画字导致黑底/黄底/蓝底预设只显示色块。 2. 字体栈顺序:优设标题黑/抖音美好体把ZCOOL庆科黄油体/快乐体(Google Fonts仅400字重) 排在Noto Sans SC(已加载400/500/700/900)之前,Canvas请求bold时无法合成粗体导致 中文渲染异常(缺字/方框/乱码)。 修复: - TitleMiniPreview:画完bg矩形后显式 ctx.fillStyle = settings.color; 升级Web Font等待为async模式:await fonts.ready + fonts.load(),确保Canvas 绘制时字体已加载到位。 - constants.ts:调整优设标题黑/抖音美好体family顺序,将Noto Sans SC(bold可用) 前置到ZCOOL之前;ZCOOL保留作风格兜底。 验证:tsc/eslint/vitest 620 passed,Chrome headless截图10个预设全部正确渲染'标题'。 --- apps/web/src/components/title/constants.ts | 10 ++-- .../components/title/TitleMiniPreview.tsx | 47 ++++++++++++++----- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/apps/web/src/components/title/constants.ts b/apps/web/src/components/title/constants.ts index 7f4bd24c0..37368e464 100644 --- a/apps/web/src/components/title/constants.ts +++ b/apps/web/src/components/title/constants.ts @@ -20,15 +20,15 @@ export const FONT_OPTIONS: FontOption[] = [ { value: "优设标题黑", label: "优设标题黑", - // 原版"优设标题黑"为商用字体,非开源;这里优先使用本地已安装字体,兜底用 ZCOOL QingKe HuangYou(站酷庆科黄油体,同为厚重黑体/海报风格,可从 Google Fonts 加载) + // 原版"优设标题黑"为商用字体非开源;优先本地已安装字体,兜底用 Noto Sans SC(Google Fonts 已加载 wght@900,保证 bold 字重可用),再用 ZCOOL 庆科黄油体作风格兜底 family: - '"YouSheBiaoTiHei","YouShe Title Black","ZCOOL QingKe HuangYou","Noto Sans SC","PingFang SC","Microsoft YaHei",sans-serif', + '"YouSheBiaoTiHei","YouShe Title Black","Noto Sans SC","ZCOOL QingKe HuangYou","PingFang SC","Microsoft YaHei",sans-serif', tag: "hot", }, { value: "阿里普惠体Bold", label: "阿里普惠体Bold", - // 阿里普惠体需要从阿里官网下载;兜底用 Noto Sans SC 900(同等字重) + // 阿里普惠体需从阿里官网下载;兜底用 Noto Sans SC 900(同等字重,已在 Google Fonts wght@400;500;700;900 加载) family: '"Alibaba PuHuiTi","Alibaba PuHuiTi Bold","Alibaba Sans","Noto Sans SC",system-ui,"PingFang SC","Microsoft YaHei",sans-serif', tag: "hot", @@ -36,9 +36,9 @@ export const FONT_OPTIONS: FontOption[] = [ { value: "抖音美好体", label: "抖音美好体", - // 抖音美好体版权字体兜底 ZCOOL KuaiLe(站酷快乐体,圆润卡通风格近似) + // 抖音美好体为版权字体;兜底用 Noto Sans SC(确保 bold 字重可用),再用 ZCOOL KuaiLe(站酷快乐体,圆润卡通风格近似) family: - '"Douyin Sans","DouyinSans","ZCOOL KuaiLe","Noto Sans SC","PingFang SC","Microsoft YaHei",sans-serif', + '"Douyin Sans","DouyinSans","Noto Sans SC","ZCOOL KuaiLe","PingFang SC","Microsoft YaHei",sans-serif', tag: "hot", }, { diff --git a/apps/web/src/pages/generate/components/title/TitleMiniPreview.tsx b/apps/web/src/pages/generate/components/title/TitleMiniPreview.tsx index 025bd4e84..ee5783b15 100644 --- a/apps/web/src/pages/generate/components/title/TitleMiniPreview.tsx +++ b/apps/web/src/pages/generate/components/title/TitleMiniPreview.tsx @@ -89,9 +89,8 @@ const TitleMiniPreview: React.FC = ({ ctx.fillRect(0, 0, width, h) } - // 分辨率缩放:以 360 宽为基准(对应 720p 的一半) - const scale = width / 360 - const r = (v: number) => Math.round(v * scale) + // 分辨率缩放:以 360 宽为基准(对应 720p 的一半),与外层 previewScale/previewR 保持一致 + const r = previewR // 字体 const size = r(settings.size) @@ -158,6 +157,9 @@ const TitleMiniPreview: React.FC = ({ ctx.fillStyle = settings.bgColor ?? "rgba(0,0,0,0.5)" roundRect(ctx, bx, by, bw, bh, rad) ctx.fill() + // 关键修复:画完背景块后必须把 fillStyle 重置为文字颜色, + // 否则后续 fillText 会用 bgColor 填充文字,导致「文字看不见只剩色块」 + ctx.fillStyle = settings.color // 恢复阴影 if (shadowEnabled) { ctx.shadowColor = settings.shadowColor ?? "rgba(0,0,0,0.8)" @@ -194,15 +196,38 @@ const TitleMiniPreview: React.FC = ({ ctx.shadowOffsetX = prevShadow.ox ctx.shadowOffsetY = prevShadow.oy } - // Web fonts (Google Fonts 等) 加载需要时间,等 fonts.ready 后再画, - // 避免第一次渲染用 sans-serif 画完再切字体造成「字体选择没反应」的错觉 - if (typeof document !== "undefined" && document.fonts && document.fonts.ready) { - document.fonts.ready.then(() => { - if (cancelled) return - draw() - }) + // 计算当前字号(draw() 内部同样逻辑,抽出来供 fontString 复用) + const previewScale = width / 360 + const previewR = (v: number) => Math.round(v * previewScale) + const buildFontString = () => { + const size = previewR(settings.size) + const ff = getFontFamily(settings.font) + const parts: string[] = [] + if (settings.italic) parts.push("italic") + if (settings.bold) parts.push("bold") + parts.push(`${size}px`, ff) + return parts.join(" ") } - draw() + + // Web Font 加载保障: + // 1) 等 document.fonts.ready(CSS @font-face 首次可用) + // 2) 显式 FontFaceSet.load(fontString, text) 触发浏览器真正下载并加载 + // 当前字体到 Canvas 可用,避免首次绘制用 fallback 字体画出错字/色块 + const doDrawWhenReady = async () => { + try { + if (typeof document !== "undefined" && document.fonts) { + await document.fonts.ready + try { + await document.fonts.load(buildFontString(), text) + } catch { + /* ignore */ + } + } + } finally { + if (!cancelled) draw() + } + } + doDrawWhenReady() return () => { cancelled = true }