fix(canvas): 字幕预览与 libass 实际压制大小对齐

- 实测对比: libass/FreeType 渲染高度比例 0.768, Canvas 2D/CoreText 比例 0.964
- 添加 CANVAS_FONT_COMPENSATION=0.88 补偿系数用于字幕(PingFang SC)
- 标题(DouyinSansBold)单独使用 1.0 不补偿, 避免偏小
- 扣除 <video controls> 控制条高度 40px 修正 scale
This commit is contained in:
小鱼开发
2026-05-18 15:08:09 +08:00
parent f3fbb267f9
commit 8809684c9d
2 changed files with 26 additions and 10 deletions
@@ -12,6 +12,7 @@ import { useEffect, useRef, useCallback } from 'react';
import type { AssStyle } from '../utils/assGenerator';
import {
PLAY_RES_Y,
CANVAS_FONT_COMPENSATION,
setCanvasFont,
drawTextBlock,
drawTextLine,
@@ -80,9 +81,10 @@ export function useCanvasSubtitleRenderer(options: UseCanvasSubtitleRendererOpti
// 清空画布
ctx.clearRect(0, 0, displayWidth, displayHeight);
// 计算缩放比例:以视频实际分辨率或 ASS PlayResY 为基准,确保预览与实际一致
const playResY = video.videoHeight || PLAY_RES_Y;
const scale = displayHeight / playResY;
// 计算缩放比例(基于高度)
// 扣除 <video controls> 控制条高度(约 40px),确保预览与视频画面比例一致
const VIDEO_CONTROLS_HEIGHT = 40;
const scale = Math.max(1, displayHeight - VIDEO_CONTROLS_HEIGHT) / PLAY_RES_Y;
const currentTimeMs = video.currentTime * 1000;
@@ -90,7 +92,7 @@ export function useCanvasSubtitleRenderer(options: UseCanvasSubtitleRendererOpti
// ASS alignment=8: marginV 是文字块顶部到画面顶部的距离
// Canvas fillText y 是基线,基线 = 顶部 + ascent
if (mainTitle && mainTitleStyle) {
setCanvasFont(ctx, mainTitleStyle, scale);
setCanvasFont(ctx, mainTitleStyle, scale, 1.0);
const marginV = (mainTitleStyle.marginV ?? 360) * scale;
const fontSize = (mainTitleStyle.fontSize ?? 104) * scale;
const metrics = ctx.measureText(mainTitle);
@@ -101,7 +103,7 @@ export function useCanvasSubtitleRenderer(options: UseCanvasSubtitleRendererOpti
// ---- 绘制小标题(全视频时长,在大标题下方,单行截断) ----
if (subTitle && subTitleStyle) {
setCanvasFont(ctx, subTitleStyle, scale);
setCanvasFont(ctx, subTitleStyle, scale, 1.0);
const marginV = (subTitleStyle.marginV ?? 480) * scale;
const fontSize = (subTitleStyle.fontSize ?? 88) * scale;
const metrics = ctx.measureText(subTitle);
@@ -119,7 +121,7 @@ export function useCanvasSubtitleRenderer(options: UseCanvasSubtitleRendererOpti
for (const u of activeUtterances) {
const marginV = (subtitleStyle.marginV ?? 480) * scale;
const fontSize = (subtitleStyle.fontSize ?? 64) * scale;
const fontSize = (subtitleStyle.fontSize ?? 64) * scale * CANVAS_FONT_COMPENSATION;
const maxWidth = displayWidth - 40 * scale;
// 去掉 ASS 换行符,超长截断,只显示单行
+18 -4
View File
@@ -10,6 +10,19 @@ import type { AssStyle } from './assGenerator';
// ASS 基础分辨率(与 generateAssContent 一致)
export const PLAY_RES_Y = 1920;
/**
* Canvas 2D 字体补偿系数
*
* 实测对比(fontSize=56, PingFang SC Medium:
* - libass/FreeType 实际渲染高度: 43px (比例 0.768)
* - Canvas 2D/CoreText 实际渲染高度: 54px (比例 0.964)
* - Canvas 比 libass 大约 26.5%
*
* 原因: 两个引擎对相同 fontSize 的 ascent/descent 计算不同。
* 补偿后确保预览字幕大小与 FFmpeg 压制输出一致。
*/
export const CANVAS_FONT_COMPENSATION = 0.88;
/**
* 将 ASS 颜色 &HAABBGGRR 转为 Canvas 可用的 rgba CSS 字符串
* ASS alpha: 00=不透明, FF=透明(与 CSS 相反)
@@ -61,9 +74,10 @@ function wrapText(
export function setCanvasFont(
ctx: CanvasRenderingContext2D,
style: Partial<AssStyle>,
scale: number
scale: number,
compensation: number = CANVAS_FONT_COMPENSATION
): void {
const fontSize = (style.fontSize ?? 64) * scale;
const fontSize = (style.fontSize ?? 64) * scale * compensation;
const fontName = style.fontName ?? 'DouyinSansBold, PingFang SC, sans-serif';
const weight = (style.bold ?? true) ? 'bold' : 'normal';
const italic = (style.italic ?? false) ? 'italic' : 'normal';
@@ -96,7 +110,7 @@ export function drawTextLine(
if (borderStyle === 3) {
// 背景色块模式 — 绘制贴边直角条(宽度按文字自适应,居中)
const metrics = ctx.measureText(text);
const fontSize = (style.fontSize ?? 64) * scale;
const fontSize = (style.fontSize ?? 64) * scale * CANVAS_FONT_COMPENSATION;
const paddingY = fontSize * 0.15;
const paddingX = fontSize * 0.4;
@@ -164,7 +178,7 @@ export function drawTextBlock(
const marginL = (style.marginL ?? 20) * scale;
const marginR = (style.marginR ?? 20) * scale;
const maxWidth = canvasWidth - marginL - marginR;
const fontSize = (style.fontSize ?? 64) * scale;
const fontSize = (style.fontSize ?? 64) * scale * CANVAS_FONT_COMPENSATION;
const lineHeight = fontSize * 1.2;
let lines: string[];