This commit is contained in:
ct
2025-06-17 22:58:51 +08:00
parent ec22cacd6b
commit d36bd4b8bf
3 changed files with 118 additions and 21 deletions

View File

@@ -1,7 +1,9 @@
import { useMitt } from '@/plugins/MittContext'; import { useMitt } from '@/plugins/MittContext';
import useMediaStore from '@/stores/MediaStore';
import useVideoEditorStore from '@/stores/VideoEditorStore'; import useVideoEditorStore from '@/stores/VideoEditorStore';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import sampleTimelineElements from './sample-timeline-data'; import SINGLE_CAPTION_TEMPLATE from '../../templates/single_caption_meme_background.json';
import { generateTimelineFromTemplate } from '../../utils/timeline-template-processor';
import useVideoExport from './video-export'; import useVideoExport from './video-export';
import VideoPreview from './video-preview'; import VideoPreview from './video-preview';
@@ -32,6 +34,7 @@ const VideoEditor = ({ width, height }) => {
const pausedTimeRef = useRef(0); const pausedTimeRef = useRef(0);
const { setVideoIsPlaying } = useVideoEditorStore(); const { setVideoIsPlaying } = useVideoEditorStore();
const { selectedMeme, selectedBackground, currentCaption } = useMediaStore();
const FPS_INTERVAL = 1000 / 30; // 30 FPS const FPS_INTERVAL = 1000 / 30; // 30 FPS
@@ -40,11 +43,18 @@ const VideoEditor = ({ width, height }) => {
timelineElementsRef.current = timelineElements; timelineElementsRef.current = timelineElements;
}, [timelineElements]); }, [timelineElements]);
// Initialize timeline // Initialize timeline on mount
useEffect(() => { useEffect(() => {
initTimeline(); initTimeline();
}, []); }, []);
// Watch MediaStore changes and regenerate timeline
useEffect(() => {
if (selectedMeme || selectedBackground) {
generateAndSetTimeline();
}
}, [selectedMeme, selectedBackground, currentCaption]);
const timelineUpdateResolverRef = useRef(null); const timelineUpdateResolverRef = useRef(null);
const setTimelineElementsAsync = useCallback((newElements) => { const setTimelineElementsAsync = useCallback((newElements) => {
@@ -61,13 +71,34 @@ const VideoEditor = ({ width, height }) => {
} }
}, [timelineElements]); }, [timelineElements]);
const generateAndSetTimeline = () => {
const mediaStoreData = {
selectedMeme,
selectedBackground,
currentCaption: currentCaption || 'Default caption text',
};
const generatedTimeline = generateTimelineFromTemplate(SINGLE_CAPTION_TEMPLATE, mediaStoreData);
if (generatedTimeline.length > 0) {
cleanupVideos(videoElements);
setTimelineElementsAsync(generatedTimeline).then(() => {
showConsoleLogs && console.log('Generated timeline from template:', generatedTimeline);
setupVideos();
setupImages();
});
}
};
const initTimeline = () => { const initTimeline = () => {
cleanupVideos(videoElements); // Try to generate from current MediaStore state first
setTimelineElementsAsync(sampleTimelineElements).then(() => { if (selectedMeme || selectedBackground) {
showConsoleLogs && console.log('Loaded sample timeline'); generateAndSetTimeline();
setupVideos(); } else {
setupImages(); // Set empty timeline and wait for media selections
}); setTimelineElements([]);
setStatus('Select meme and background to start editing');
}
}; };
// Handle element transformations (position, scale, rotation) and text properties // Handle element transformations (position, scale, rotation) and text properties
@@ -134,8 +165,8 @@ const VideoEditor = ({ width, height }) => {
scaledHeight = imgHeight * scale; scaledHeight = imgHeight * scale;
} }
const centeredX = element.x || (maxWidth - scaledWidth) / 2; const centeredX = element.x !== undefined ? element.x : (maxWidth - scaledWidth) / 2;
const centeredY = element.y || (maxHeight - scaledHeight) / 2; const centeredY = element.y !== undefined ? element.y : (maxHeight - scaledHeight) / 2;
setTimelineElements((prev) => setTimelineElements((prev) =>
prev.map((el) => { prev.map((el) => {
@@ -247,8 +278,8 @@ const VideoEditor = ({ width, height }) => {
scaledHeight = posterHeight * scale; scaledHeight = posterHeight * scale;
} }
const centeredX = (maxWidth - scaledWidth) / 2; const centeredX = element.x !== undefined ? element.x : (maxWidth - scaledWidth) / 2;
const centeredY = (maxHeight - scaledHeight) / 2; const centeredY = element.y !== undefined ? element.y : (maxHeight - scaledHeight) / 2;
setTimelineElements((prev) => setTimelineElements((prev) =>
prev.map((el) => { prev.map((el) => {
@@ -331,10 +362,11 @@ const VideoEditor = ({ width, height }) => {
} else if (mediaCount > 0) { } else if (mediaCount > 0) {
setStatus(`Loading media... (${loadedVideos.size}/${mediaCount})`); setStatus(`Loading media... (${loadedVideos.size}/${mediaCount})`);
} else { } else {
setStatus('Ready to play'); setStatus('Select meme and background to start editing');
} }
}; };
// Rest of the component remains the same...
const handlePause = useCallback(() => { const handlePause = useCallback(() => {
if (isPlaying) { if (isPlaying) {
setIsPlaying(false); setIsPlaying(false);

View File

@@ -17,10 +17,10 @@
"layer": 1, "layer": 1,
"inPoint": 0, "inPoint": 0,
"duration": 6, "duration": 6,
"x": 200, "x": 0,
"y": 200, "y": 0,
"width": 280, "width": 720,
"height": 180, "height": 1280,
"rotation": 0 "rotation": 0
}, },
{ {
@@ -34,10 +34,10 @@
"layer": 2, "layer": 2,
"inPoint": 0, "inPoint": 0,
"duration": 6, "duration": 6,
"x": 200, "x": 0,
"y": 200, "y": 0,
"width": 280, "width": 720,
"height": 180, "height": 1280,
"rotation": 0 "rotation": 0
}, },
{ {

View File

@@ -0,0 +1,65 @@
// utils/timeline-template-processor.js
export const generateTimelineFromTemplate = (template, mediaStoreData) => {
const { selectedMeme, selectedBackground, currentCaption } = mediaStoreData;
// If no selections, return empty timeline
if (!selectedMeme && !selectedBackground) {
return [];
}
// Calculate duration based on template config
let maxDuration = 5; // default fallback
if (template.max_duration_based_on === 'memes' && selectedMeme?.duration) {
maxDuration = parseFloat(selectedMeme.duration);
}
// Process each timeline element
const processedTimeline = template.timeline
.map((element) => {
let processedElement = { ...element };
// Update duration for all elements
processedElement.duration = maxDuration;
// Process by element ID/type
switch (element.id) {
case 'background':
if (selectedBackground) {
processedElement.source = selectedBackground.media_url;
processedElement.name = selectedBackground.prompt || 'Background';
} else {
return null; // Skip if no background selected
}
break;
case 'meme':
if (selectedMeme) {
processedElement.source_webm = selectedMeme.webm_url;
processedElement.source_mov = selectedMeme.mov_url;
processedElement.poster = selectedMeme.webp_url;
processedElement.name = selectedMeme.name;
} else {
return null; // Skip if no meme selected
}
break;
case 'caption':
if (currentCaption) {
processedElement.text = currentCaption;
} else {
return null; // Skip if no caption
}
break;
default:
// Keep element as-is for any other types
break;
}
return processedElement;
})
.filter(Boolean); // Remove null elements
return processedTimeline;
};