修复序列帧播放淡入闪烁

序列帧图片不再复用普通图片加载淡入样式

补充画布序列帧无过渡播放回归测试

记录序列帧播放闪烁排障经验
This commit is contained in:
2026-06-25 15:07:23 +08:00
parent 0c2f743010
commit 346146113d
3 changed files with 19 additions and 5 deletions
@@ -79,6 +79,14 @@
- 验证:`ImageCanvasGenerationLayerModel` 应断言动作结果 `src` 为首帧且 `mediaType="image-sequence"`;画布集成测试应出现 `画布序列帧:角色动作` 图片播放器,不应出现角色动作 `<video>`;导出测试应断言角色动作下载和画布素材导出都包含序列帧 ZIP / frames 目录。
- 关联:`src/components/image-editor/ImageCanvasGenerationLayerModel.ts``src/components/image-editor/ImageCanvasWorldView.tsx``src/components/image-editor/ImageCanvasExportModel.ts``server-rs/crates/api-server/src/character_animation_assets.rs``docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md`
## 图片画布序列帧播放不要复用普通图片淡入样式
- 现象:角色动作序列帧播放时看起来像每帧之间在渐变或闪烁。
- 原因:序列帧播放器每帧切换可低至 40ms,默认约 125ms 一帧;如果帧 `<img>` 复用普通图片的 `image-canvas-editor__layer-image--loading/--loaded`,其中 `opacity 180ms ease` 会跨过下一帧切换,形成类似交叉淡入淡出的视觉。
- 处理:`ImageCanvasImageSequenceFrame` 只使用序列帧专属 class,帧显隐用同步 `opacity` 硬切,并显式 `transition: none`;保留“下一帧未加载时继续显示上一帧”的 readiness gate。
- 验证:`ImageCanvasWorldView.test.tsx` 应断言序列帧 `<img>` 不带普通图片 loading/loaded class,且 style 中 `transition``none`
- 关联:`src/components/image-editor/ImageCanvasWorldView.tsx``src/index.css``src/components/image-editor/ImageCanvasWorldView.test.tsx`
## Vidu 文生音频线上网关可能要求 sound 字段
- 现象:画板点击 `生成游戏音效` 后,请求返回 `Failed to deserialize the JSON body into the target type: missing field sound`
@@ -1087,6 +1087,14 @@ describe('ImageCanvasWorldView', () => {
expect(frame.getAttribute('src')).toBe(
'https://oss.example.com/frame01.png?signature=1',
);
expect(frame.className).toBe(
'image-canvas-editor__layer-image-sequence-frame',
);
expect(frame.style.opacity).toBe('0');
expect(frame.style.transition).toBe('none');
fireEvent.load(frame);
expect(frame.style.opacity).toBe('1');
expect(frame.style.transition).toBe('none');
expect(within(layerButton).getByText('1/2')).toBeTruthy();
expect(within(layerButton).getByText('动作')).toBeTruthy();
@@ -755,16 +755,14 @@ function ImageCanvasImageSequenceFrame({
alt={isVisible ? `画布序列帧:${layer.title}` : ''}
aria-hidden={isVisible ? undefined : true}
decoding="async"
className={`image-canvas-editor__layer-image-sequence-frame ${
isVisible && isLoaded
? 'image-canvas-editor__layer-image--loaded'
: 'image-canvas-editor__layer-image--loading'
}`}
className="image-canvas-editor__layer-image-sequence-frame"
style={{
inset: 0,
opacity: isVisible && isLoaded ? 1 : 0,
position: 'absolute',
transition: 'none',
transform: mediaTransform,
zIndex: 1,
}}
onLoad={() => onFrameReady(frameKey)}
onError={() => onFrameReady(frameKey)}