6ee33dc561
- 封面使用真实画布尺寸及最终 viewport.x/y/scale。 - 320×240 封面改为居中 cover 裁切,不再从左上角取景。 - 正确绘制画布背景色。 in canva:   in home:   因为原本的布局持久化可能有一些bug,改动不相关,所以我回退到最开始的一版: - 不额外监听画布尺寸变化,给最小尺寸兜底,(大了比小了好) - 封面不要求强一致性,生成/上传时机仍然按照原来随布局持久化 优化了封面生成: - 改用webp格式 - 尽量利用图片缓存 最终生成封面从700ms加速到150ms以内, 加上必须的上传和确认150ms 和 布局持久化的时间相差不多,不是特别离谱的手速都能保证两者一致 --------- Co-authored-by: kdletters <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/111 Co-authored-by: 王德宇 <kvtodev@outlook.com> Co-committed-by: 王德宇 <kvtodev@outlook.com>
132 lines
3.8 KiB
TypeScript
132 lines
3.8 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
|
import {
|
|
buildProjectCoverSnapshotSignature,
|
|
normalizeProjectCoverSnapshotViewport,
|
|
resolveProjectCoverDrawableLayers,
|
|
resolveProjectCoverSnapshotFrame,
|
|
} from './ImageCanvasProjectCoverSnapshotModel';
|
|
|
|
function layer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
|
return {
|
|
id: 'layer-a',
|
|
resourceId: 'resource-a',
|
|
title: '图层',
|
|
src: '/generated/layer-a.png',
|
|
x: 10,
|
|
y: 20,
|
|
width: 320,
|
|
height: 240,
|
|
originalWidth: 320,
|
|
originalHeight: 240,
|
|
zIndex: 2,
|
|
sourceType: 'generated',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('ImageCanvasProjectCoverSnapshotModel', () => {
|
|
it('uses at least a centered 1280 by 960 viewport while preserving larger viewports', () => {
|
|
expect(
|
|
normalizeProjectCoverSnapshotViewport({
|
|
viewport: { x: 12, y: -8, scale: 0.5 },
|
|
viewportSize: { width: 900, height: 640 },
|
|
}),
|
|
).toEqual({
|
|
viewport: { x: 202, y: 152, scale: 0.5 },
|
|
viewportSize: { width: 1280, height: 960 },
|
|
});
|
|
|
|
expect(
|
|
normalizeProjectCoverSnapshotViewport({
|
|
viewport: { x: 12, y: -8, scale: 0.5 },
|
|
viewportSize: { width: 1920, height: 1080 },
|
|
}),
|
|
).toEqual({
|
|
viewport: { x: 12, y: -8, scale: 0.5 },
|
|
viewportSize: { width: 1920, height: 1080 },
|
|
});
|
|
});
|
|
|
|
it('builds a stable cover signature from visible drawable layers', () => {
|
|
const signature = buildProjectCoverSnapshotSignature({
|
|
viewport: { x: 12, y: -8, scale: 0.75 },
|
|
viewportSize: { width: 900, height: 640 },
|
|
backgroundColor: '#f8fafc',
|
|
layers: [
|
|
layer({ id: 'hidden', hidden: true }),
|
|
layer({
|
|
id: 'audio',
|
|
mediaType: 'audio',
|
|
src: '/generated/audio.mp3',
|
|
}),
|
|
layer({
|
|
id: 'front',
|
|
objectKey: 'generated/front.png',
|
|
zIndex: 5,
|
|
}),
|
|
layer({
|
|
id: 'back',
|
|
src: '/generated/back.png',
|
|
zIndex: 1,
|
|
}),
|
|
],
|
|
});
|
|
|
|
expect(signature).toContain('"scale":0.75');
|
|
expect(signature).toContain('"width":900');
|
|
expect(signature).toContain('"backgroundColor":"#f8fafc"');
|
|
expect(signature).toContain('"id":"back"');
|
|
expect(signature).toContain('"id":"front"');
|
|
expect(signature).not.toContain('"id":"hidden"');
|
|
expect(signature).not.toContain('"id":"audio"');
|
|
});
|
|
|
|
it('center-crops the live canvas viewport to fill the 4:3 cover', () => {
|
|
const landscapeFrame = resolveProjectCoverSnapshotFrame({
|
|
viewport: { x: -260, y: 70, scale: 0.5 },
|
|
viewportSize: { width: 900, height: 640 },
|
|
});
|
|
|
|
expect(landscapeFrame.renderScale).toBe(0.5);
|
|
expect(landscapeFrame.frameScale).toBeCloseTo(0.375);
|
|
expect(landscapeFrame.offsetX).toBeCloseTo(-8.75);
|
|
expect(landscapeFrame.offsetY).toBeCloseTo(0);
|
|
|
|
const portraitFrame = resolveProjectCoverSnapshotFrame({
|
|
viewport: { x: 30, y: -40, scale: 1.25 },
|
|
viewportSize: { width: 640, height: 900 },
|
|
});
|
|
|
|
expect(portraitFrame.renderScale).toBe(1.25);
|
|
expect(portraitFrame.frameScale).toBeCloseTo(0.5);
|
|
expect(portraitFrame.offsetX).toBeCloseTo(0);
|
|
expect(portraitFrame.offsetY).toBeCloseTo(-105);
|
|
});
|
|
|
|
it('uses the first image-sequence frame as the drawable source', () => {
|
|
expect(
|
|
resolveProjectCoverDrawableLayers([
|
|
layer({
|
|
mediaType: 'image-sequence',
|
|
imageSequenceFrames: [
|
|
{
|
|
frameIndex: 1,
|
|
imageSrc: '/generated/frame-1.png',
|
|
objectKey: 'generated/frame-1.png',
|
|
width: 320,
|
|
height: 240,
|
|
},
|
|
],
|
|
}),
|
|
]),
|
|
).toMatchObject([
|
|
{
|
|
src: '/generated/frame-1.png',
|
|
objectKey: 'generated/frame-1.png',
|
|
},
|
|
]);
|
|
});
|
|
});
|