From 344a87c80052691cd5788b3d9deaf1430cc32829 Mon Sep 17 00:00:00 2001 From: SaaS Frontend Agent Date: Fri, 7 Aug 2026 11:34:43 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Canvas=E6=A0=87=E9=A2=98=E5=B1=85?= =?UTF-8?q?=E4=B8=AD=20-=20CSS=E5=B0=BA=E5=AF=B8=E5=8C=B9=E9=85=8D?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E6=B8=B2=E6=9F=93=E5=8C=BA=E5=9F=9F=EF=BC=8C?= =?UTF-8?q?=E6=B6=88=E9=99=A4=E5=AE=B9=E5=99=A8/=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E5=AE=BD=E5=BA=A6=E4=B8=8D=E4=B8=80=E8=87=B4=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E7=9A=84=E5=81=8F=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:Canvas CSS尺寸为100%×100%(占满容器),但物理像素按视频实际渲染尺寸设置。 当容器和视频宽度不一致时浏览器拉伸Canvas,导致x=offsetX+w/2与实际显示位置不匹配。 修复方案: 1. drawVideoTitle: 通过getBoundingClientRect获取视频实际渲染区域, 用canvas.style直接设置CSS位置和尺寸匹配视频(而非容器) 2. drawTitleOnCanvas: 移除offsetX/offsetY参数,坐标系基于Canvas自身, x=w/2即居中 3. 选择直接DOM操作而非state,避免re-render覆盖style的时序问题 --- .../generate/components/PreviewVideoPanel.tsx | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) 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])