feat(project): optimize image imports and converter

- Optimize imported scene preview images (smart WebP/JPEG/PNG, preserve alpha, keep pixel size)

- Update converter to re-encode existing image assets with same algorithm

- Improve import/export progress overlay and reduce presentation slide stutter

Made-with: Cursor
This commit is contained in:
Ivan Fontosh
2026-04-23 17:59:57 +08:00
parent 1d051f8bf9
commit 8f8eef53c9
33 changed files with 3684 additions and 68 deletions
+32 -6
View File
@@ -35,6 +35,7 @@ export type SceneGraphSceneAudioSummary = {
export type SceneGraphSceneCard = {
title: string;
previewAssetId: AssetId | null;
previewThumbAssetId: AssetId | null;
previewAssetType: 'image' | 'video' | null;
previewVideoAutostart: boolean;
previewRotationDeg: 0 | 90 | 180 | 270;
@@ -69,6 +70,7 @@ type SceneCardData = {
title: string;
active: boolean;
previewAssetId: AssetId | null;
previewThumbAssetId: AssetId | null;
previewAssetType: 'image' | 'video' | null;
previewVideoAutostart: boolean;
previewRotationDeg: 0 | 90 | 180 | 270;
@@ -129,7 +131,8 @@ function IconVideoPreviewAutostart() {
}
function SceneCardNode({ data }: NodeProps<SceneCardData>) {
const url = useAssetUrl(data.previewAssetId);
const thumbUrl = useAssetUrl(data.previewThumbAssetId);
const previewUrl = useAssetUrl(data.previewAssetId);
const cardClass = [styles.card, data.active ? styles.cardActive : ''].filter(Boolean).join(' ');
const showCornerVideo = data.previewIsVideo;
const showCornerAudio = data.hasSceneAudio;
@@ -139,11 +142,11 @@ function SceneCardNode({ data }: NodeProps<SceneCardData>) {
<div className={cardClass}>
<div className={styles.previewShell}>
{data.isStartScene ? <div className={styles.badgeStart}>НАЧАЛО</div> : null}
{url && data.previewAssetType === 'image' ? (
{thumbUrl ? (
<div className={styles.previewFill}>
{data.previewRotationDeg === 0 ? (
<img
src={url}
src={thumbUrl}
alt=""
className={styles.imageCover}
draggable={false}
@@ -152,7 +155,7 @@ function SceneCardNode({ data }: NodeProps<SceneCardData>) {
/>
) : (
<RotatedImage
url={url}
url={thumbUrl}
rotationDeg={data.previewRotationDeg}
mode="cover"
loading="lazy"
@@ -161,9 +164,31 @@ function SceneCardNode({ data }: NodeProps<SceneCardData>) {
/>
)}
</div>
) : url && data.previewAssetType === 'video' ? (
) : previewUrl && data.previewAssetType === 'image' ? (
<div className={styles.previewFill}>
{data.previewRotationDeg === 0 ? (
<img
src={previewUrl}
alt=""
className={styles.imageCover}
draggable={false}
loading="lazy"
decoding="async"
/>
) : (
<RotatedImage
url={previewUrl}
rotationDeg={data.previewRotationDeg}
mode="cover"
loading="lazy"
decoding="async"
style={{ width: '100%', height: '100%' }}
/>
)}
</div>
) : previewUrl && data.previewAssetType === 'video' ? (
<video
src={url}
src={previewUrl}
muted
playsInline
preload="metadata"
@@ -322,6 +347,7 @@ function SceneGraphCanvas({
title: c?.title ?? '',
active,
previewAssetId: c?.previewAssetId ?? null,
previewThumbAssetId: c?.previewThumbAssetId ?? null,
previewAssetType: c?.previewAssetType ?? null,
previewVideoAutostart: c?.previewVideoAutostart ?? false,
previewRotationDeg: c?.previewRotationDeg ?? 0,
@@ -42,6 +42,7 @@ void test('buildNextSceneCardById: does not change refs when irrelevant fields c
connections: [],
layout: { x: 0, y: 0 },
previewAssetId: null,
previewThumbAssetId: null,
previewAssetType: null,
previewVideoAutostart: false,
previewRotationDeg: 0,
@@ -79,6 +80,7 @@ void test('buildNextSceneCardById: changes card when title changes', () => {
connections: [],
layout: { x: 0, y: 0 },
previewAssetId: null,
previewThumbAssetId: null,
previewAssetType: null,
previewVideoAutostart: false,
previewRotationDeg: 0,
@@ -38,6 +38,7 @@ export function buildNextSceneCardById(
if (
prevCard?.title === s.title &&
prevCard.previewAssetId === s.previewAssetId &&
prevCard.previewThumbAssetId === s.previewThumbAssetId &&
prevCard.previewAssetType === s.previewAssetType &&
prevCard.previewVideoAutostart === s.previewVideoAutostart &&
prevCard.previewRotationDeg === s.previewRotationDeg &&
@@ -49,6 +50,7 @@ export function buildNextSceneCardById(
nextMap[id] = {
title: s.title,
previewAssetId: s.previewAssetId,
previewThumbAssetId: s.previewThumbAssetId,
previewAssetType: s.previewAssetType,
previewVideoAutostart: s.previewVideoAutostart,
previewRotationDeg: s.previewRotationDeg,