diff --git a/apps/web/src/pages/generate/components/PreviewVideoPanel.tsx b/apps/web/src/pages/generate/components/PreviewVideoPanel.tsx index 8de11fbf7..8409adecb 100644 --- a/apps/web/src/pages/generate/components/PreviewVideoPanel.tsx +++ b/apps/web/src/pages/generate/components/PreviewVideoPanel.tsx @@ -5,6 +5,11 @@ * * 设计说明:标题预览仅在有视频时显示(叠加在视频画面上方)。 * 无视频状态(idle/loading/error)下不再单独显示标题预览,这是有意为之的设计简化。 + * + * Canvas 居中修复说明: + * Canvas 的 CSS 位置和尺寸直接匹配视频实际渲染区域(通过 getBoundingClientRect), + * 绘制坐标系基于 Canvas 自身尺寸,x = w/2 即可实现水平居中, + * 避免容器与视频尺寸不一致时浏览器拉伸 Canvas 导致居中偏移。 */ import React, { useRef, useEffect, useCallback } from "react" import { PlayCircleOutlined, LoadingOutlined } from "@ant-design/icons" @@ -48,32 +53,30 @@ function wrapText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number) /** * 在 canvas 上绘制标题文字(含描边/阴影/多行居中) + * + * Canvas 已通过 CSS 定位到视频实际渲染位置, + * 坐标系基于 Canvas 自身尺寸,居中直接使用 w/2。 + * * @param ctx canvas 上下文 - * @param canvasWidth canvas CSS 宽度(绘制区域宽度) - * @param canvasHeight canvas CSS 高度(绘制区域高度) + * @param w canvas CSS 宽度(= 视频渲染宽度) + * @param h canvas CSS 高度(= 视频渲染高度) * @param text 标题文字 * @param settings 标题样式 * @param paddingX 左右边距(px),与 ASS 的 MarginL/MarginR 对应 * @param position "top" | "center" | "bottom" * @param topOffset 顶部/底部偏移量 - * @param offsetX 绘制区域左边缘相对于 canvas 的偏移(用于 video 居中时补偿) - * @param offsetY 绘制区域上边缘相对于 canvas 的偏移(用于 video 居中时补偿) */ function drawTitleOnCanvas( ctx: CanvasRenderingContext2D, - canvasWidth: number, - canvasHeight: number, + w: number, + h: number, text: string, settings: TitleSettings, paddingX: number, position: string, topOffset: number, - offsetX = 0, - offsetY = 0, ) { const dpr = window.devicePixelRatio || 1 - const w = canvasWidth - const h = canvasHeight // 设置 canvas 物理像素尺寸(高清屏适配) ctx.canvas.width = Math.round(w * dpr) @@ -133,10 +136,10 @@ function drawTitleOnCanvas( break } - // 逐行绘制(offsetX/offsetY 补偿 video 在容器内的居中偏移) - const x = offsetX + w / 2 + // 居中 x = w/2(Canvas 已定位到视频位置,无需额外偏移) + const x = w / 2 lines.forEach((line, i) => { - const y = offsetY + startY + i * lineHeight + const y = startY + i * lineHeight if (settings.stroke) ctx.strokeText(line, x, y) ctx.fillText(line, x, y) }) @@ -203,6 +206,13 @@ export const PreviewVideoPanel: React.FC = ({ offsetY = videoRect.top - containerRect.top } + // 更新 Canvas CSS 位置和尺寸,使其与视频实际渲染区域完全对齐 + canvas.style.left = `${offsetX}px` + canvas.style.top = `${offsetY}px` + canvas.style.width = `${drawW}px` + canvas.style.height = `${drawH}px` + + // 绘制时,坐标系基于 Canvas 自身尺寸,无需额外偏移 drawTitleOnCanvas( ctx, drawW, @@ -212,8 +222,6 @@ export const PreviewVideoPanel: React.FC = ({ 40, titleSettings.position, 60, - offsetX, - offsetY, ) }, [titleText, titleSettings])