fix: Canvas标题居中修复+字体加载等待
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 47s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 57s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m11s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m27s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m43s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 54s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m4s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m23s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m38s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Failing after 989h9m23s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 989h9m24s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 989h10m19s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 989h10m19s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 989h10m21s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 989h10m23s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 989h10m23s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 989h10m23s
CI/CD Pipeline / PR Build API Image (pull_request) Failing after 989h42m41s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 989h43m37s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 47s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 57s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m11s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m27s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m43s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 54s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m4s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m23s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m38s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Failing after 989h9m23s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 989h9m24s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 989h10m19s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 989h10m19s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 989h10m21s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 989h10m23s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 989h10m23s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 989h10m23s
CI/CD Pipeline / PR Build API Image (pull_request) Failing after 989h42m41s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 989h43m37s
- 使用video元素实际渲染尺寸(clientWidth/clientHeight)替代容器尺寸 - 添加document.fonts.ready等待字体加载后再绘制 - 字体加载前跳过绘制避免measureText用fallback字体计算
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
* Step3 生成预览后常驻显示预览视频
|
||||
* Step4+ 用 Canvas 绘制标题预览(替代 CSS overlay,与 ASS 渲染行为一致)
|
||||
*/
|
||||
import React, { useRef, useEffect, useCallback } from "react"
|
||||
import React, { useRef, useEffect, useCallback, useState } from "react"
|
||||
import { PlayCircleOutlined, LoadingOutlined } from "@ant-design/icons"
|
||||
import type { PreviewResult, PreviewStatus } from "../hooks/useStep3Preview"
|
||||
import type { TitleSettings } from "../types"
|
||||
@@ -46,8 +46,8 @@ function wrapText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number)
|
||||
/**
|
||||
* 在 canvas 上绘制标题文字(含描边/阴影/多行居中)
|
||||
* @param ctx canvas 上下文
|
||||
* @param canvasWidth canvas CSS 宽度
|
||||
* @param canvasHeight canvas CSS 高度
|
||||
* @param canvasWidth canvas CSS 宽度(绘制区域宽度)
|
||||
* @param canvasHeight canvas CSS 高度(绘制区域高度)
|
||||
* @param text 标题文字
|
||||
* @param settings 标题样式
|
||||
* @param paddingX 左右边距(px),与 ASS 的 MarginL/MarginR 对应
|
||||
@@ -128,7 +128,7 @@ function drawTitleOnCanvas(
|
||||
break
|
||||
}
|
||||
|
||||
// 逐行绘制
|
||||
// 逐行绘制(水平居中于绘制区域)
|
||||
const x = w / 2
|
||||
lines.forEach((line, i) => {
|
||||
const y = startY + i * lineHeight
|
||||
@@ -162,13 +162,17 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
|
||||
const isError = previewStatus === "error"
|
||||
const showTitlePreview = !!titleSettings
|
||||
|
||||
// video 模式 canvas ref
|
||||
// video 模式 refs
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
// no-video 模式 canvas ref
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
// no-video 模式 refs
|
||||
const noVideoCanvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const noVideoContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 字体加载状态
|
||||
const [fontLoaded, setFontLoaded] = useState(false)
|
||||
|
||||
/** 在 video canvas 上绘制标题 */
|
||||
const drawVideoTitle = useCallback(() => {
|
||||
const canvas = canvasRef.current
|
||||
@@ -180,17 +184,30 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
|
||||
const rect = container.getBoundingClientRect()
|
||||
if (rect.width <= 0 || rect.height <= 0) return
|
||||
|
||||
// 字体加载前跳过(等 fontLoaded 触发重绘)
|
||||
if (!fontLoaded) return
|
||||
|
||||
// 优先使用 video 元素实际渲染尺寸,确保标题居中于视频画面
|
||||
// 而非容器尺寸(容器可能比 video 元素更宽/更高)
|
||||
const videoEl = videoRef.current
|
||||
let drawW = rect.width
|
||||
let drawH = rect.height
|
||||
if (videoEl && videoEl.clientWidth > 0 && videoEl.clientHeight > 0) {
|
||||
drawW = videoEl.clientWidth
|
||||
drawH = videoEl.clientHeight
|
||||
}
|
||||
|
||||
drawTitleOnCanvas(
|
||||
ctx,
|
||||
rect.width,
|
||||
rect.height,
|
||||
drawW,
|
||||
drawH,
|
||||
titleText || "",
|
||||
titleSettings,
|
||||
40,
|
||||
titleSettings.position,
|
||||
60,
|
||||
)
|
||||
}, [titleText, titleSettings])
|
||||
}, [titleText, titleSettings, fontLoaded])
|
||||
|
||||
/** 在 no-video canvas 上绘制标题 */
|
||||
const drawNoVideoTitle = useCallback(() => {
|
||||
@@ -203,6 +220,9 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
|
||||
const rect = container.getBoundingClientRect()
|
||||
if (rect.width <= 0 || rect.height <= 0) return
|
||||
|
||||
// 字体加载前跳过
|
||||
if (!fontLoaded) return
|
||||
|
||||
// no-video 容器较窄,字号缩小 + 减少边距
|
||||
drawTitleOnCanvas(
|
||||
ctx,
|
||||
@@ -215,7 +235,7 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
|
||||
30,
|
||||
0.75,
|
||||
)
|
||||
}, [titleText, titleSettings])
|
||||
}, [titleText, titleSettings, fontLoaded])
|
||||
|
||||
// video 模式:ResizeObserver 监听容器尺寸变化 → 重绘
|
||||
useEffect(() => {
|
||||
@@ -227,7 +247,6 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
|
||||
drawVideoTitle()
|
||||
})
|
||||
observer.observe(container)
|
||||
// 初始绘制(延迟一帧等容器渲染完成)
|
||||
requestAnimationFrame(drawVideoTitle)
|
||||
|
||||
return () => observer.disconnect()
|
||||
@@ -248,6 +267,22 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
|
||||
return () => observer.disconnect()
|
||||
}, [showTitlePreview, hasPreview, isError, drawNoVideoTitle])
|
||||
|
||||
// 字体加载完成后重绘(Canvas measureText 依赖字体已加载)
|
||||
useEffect(() => {
|
||||
if (!showTitlePreview) return
|
||||
let cancelled = false
|
||||
document.fonts.ready.then(() => {
|
||||
if (cancelled) return
|
||||
// 短暂延迟确保字体完全可用
|
||||
setTimeout(() => {
|
||||
if (!cancelled) setFontLoaded(true)
|
||||
}, 50)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [showTitlePreview])
|
||||
|
||||
// video 加载完成后重绘
|
||||
const handleVideoLoaded = useCallback(() => {
|
||||
if (showTitlePreview) {
|
||||
@@ -285,7 +320,10 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="xx-preview-progress-bar-wrap">
|
||||
<div className="xx-preview-progress-fill" style={{ width: `${progress}%` }} />
|
||||
<div
|
||||
className="xx-preview-progress-fill"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -310,6 +348,7 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
|
||||
<div ref={containerRef} style={{ position: "relative" }}>
|
||||
<div className="xx-preview-video">
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={previewResult.videoUrl}
|
||||
controls
|
||||
preload="metadata"
|
||||
|
||||
Reference in New Issue
Block a user