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 }