This commit is contained in:
ct
2025-06-17 17:41:40 +08:00
parent 950202c1fb
commit bf5e875ee9
28 changed files with 987 additions and 88 deletions

View File

@@ -57,10 +57,12 @@ const sampleTimelineElements = [
startTime: 0,
layer: 2,
duration: 4,
x: 50,
y: 600,
fontSize: 24,
fontWeight: 'bold', // ADD THIS LINE
x: 90,
y: 180,
fontSize: 40,
fontFamily: 'Montserrat',
fontWeight: 'bold',
fontStyle: 'normal',
fill: 'white',
stroke: 'black',
strokeWidth: 1,
@@ -76,7 +78,9 @@ const sampleTimelineElements = [
x: 50,
y: 650,
fontSize: 20,
fontWeight: 'bold', // ADD THIS LINE
fontFamily: 'Montserrat',
fontWeight: 'bold',
fontStyle: 'normal',
fill: 'yellow',
stroke: 'red',
strokeWidth: 2,

View File

@@ -70,7 +70,7 @@ const VideoEditor = ({ width, height }) => {
});
};
// NEW: Handle element transformations (position, scale, rotation)
// Handle element transformations (position, scale, rotation)
const handleElementUpdate = useCallback(
(elementId, updates) => {
setTimelineElements((prev) =>
@@ -553,7 +553,7 @@ const VideoEditor = ({ width, height }) => {
handleSeek={handleSeek}
copyFFmpegCommand={copyFFmpegCommand}
exportVideo={exportVideo}
onElementUpdate={handleElementUpdate} // NEW: Pass the update handler
onElementUpdate={handleElementUpdate}
layerRef={layerRef}
/>
</div>

View File

@@ -98,16 +98,29 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
showConsoleLogs && console.log('🎵 Audio args:', audioArgs);
// Process text elements with centering
// Process text elements with font family support
texts.forEach((t, i) => {
const escapedText = t.text.replace(/'/g, is_string ? "\\'" : "'").replace(/:/g, '\\:');
// Determine font file based on weight and style
let fontFile = 'Montserrat-Regular.ttf'; // default
const isBold = t.fontWeight === 'bold' || t.fontWeight === 700;
const isItalic = t.fontStyle === 'italic';
if (isBold && isItalic) {
fontFile = 'Montserrat-BoldItalic.ttf';
} else if (isBold) {
fontFile = 'Montserrat-Bold.ttf';
} else if (isItalic) {
fontFile = 'Montserrat-Italic.ttf';
}
// Center the text: x position is the center point, y is adjusted for baseline
const centerX = Math.round(t.x);
const centerY = Math.round(t.y + t.fontSize * 0.3); // Adjust for text baseline
filters.push(
`[${videoLayer}]drawtext=fontfile=/arial.ttf:text='${escapedText}':x=${centerX}:y=${centerY}:fontsize=${t.fontSize}:fontcolor=${t.fill}:borderw=${t.strokeWidth}:bordercolor=${
`[${videoLayer}]drawtext=fontfile=/${fontFile}:text='${escapedText}':x=${centerX}:y=${centerY}:fontsize=${t.fontSize}:fontcolor=${t.fill}:borderw=${t.strokeWidth}:bordercolor=${
t.stroke
}:text_align=center:enable='between(t,${t.startTime},${t.startTime + t.duration})'[t${i}]`,
);
@@ -211,9 +224,25 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
showConsoleLogs && console.log('FFmpeg loaded!');
setExportProgress(20);
setExportStatus('Loading font...');
await ffmpeg.writeFile('arial.ttf', await fetchFile('https://raw.githubusercontent.com/ffmpegwasm/testdata/master/arial.ttf'));
showConsoleLogs && console.log('Font loaded!');
setExportStatus('Loading fonts...');
// Load Montserrat font variants
await ffmpeg.writeFile(
'Montserrat-Regular.ttf',
await fetchFile('https://fonts.gstatic.com/s/montserrat/v26/JTUSjIg1_i6t8kCHKm459Wlhyw.ttf'),
);
await ffmpeg.writeFile(
'Montserrat-Bold.ttf',
await fetchFile('https://fonts.gstatic.com/s/montserrat/v26/JTUSjIg1_i6t8kCHKm459W1hyw.ttf'),
);
await ffmpeg.writeFile(
'Montserrat-Italic.ttf',
await fetchFile('https://fonts.gstatic.com/s/montserrat/v26/JTUSjIg1_i6t8kCHKm459WxhywMDPA.ttf'),
);
await ffmpeg.writeFile(
'Montserrat-BoldItalic.ttf',
await fetchFile('https://fonts.gstatic.com/s/montserrat/v26/JTUSjIg1_i6t8kCHKm459W1hywMDPA.ttf'),
);
showConsoleLogs && console.log('Fonts loaded!');
setExportProgress(30);
setExportStatus('Downloading media...');

View File

@@ -58,6 +58,10 @@ const VideoPreview = ({
// Snap settings
const POSITION_SNAP_THRESHOLD = 10; // Pixels within which to snap to center
// Font size constraints (same as in text-sidebar.jsx)
const MIN_FONT_SIZE = 8;
const MAX_FONT_SIZE = 120;
// Function to determine which image source to use for videos
const getImageSource = (element) => {
const isVideoActive = videoStates[element.id] && isPlaying;
@@ -276,7 +280,7 @@ const VideoPreview = ({
[onElementUpdate, timelineElements],
);
// Handle transform events (scale, rotate) with snapping - USES NATIVE KONVA ROTATION SNAPPING
// Handle transform events (scale, rotate) with fontSize conversion for text
const handleTransform = useCallback(
(elementId) => {
const node = elementRefs.current[elementId];
@@ -290,12 +294,27 @@ const VideoPreview = ({
const scaleX = node.scaleX();
const scaleY = node.scaleY();
let newWidth, newHeight;
let newWidth,
newHeight,
updates = {};
if (element.type === 'text') {
// For text, allow free scaling
newWidth = node.width() * scaleX;
newHeight = node.height() * scaleY;
// OPTION A: Convert scale change to fontSize change
const scale = Math.max(Math.abs(scaleX), Math.abs(scaleY));
const newFontSize = Math.round(element.fontSize * scale);
// Clamp fontSize to valid range
const clampedFontSize = Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, newFontSize));
// Reset scale to 1 since we're converting to fontSize
node.scaleX(1);
node.scaleY(1);
// The width/height will be automatically calculated by Konva based on fontSize
// For text elements, we let Konva handle the natural dimensions
updates.fontSize = clampedFontSize;
console.log(`Text transform: scale=${scale.toFixed(2)}, oldFontSize=${element.fontSize}, newFontSize=${clampedFontSize}`);
} else {
// For images/videos, maintain aspect ratio by using the larger scale
const scale = Math.max(Math.abs(scaleX), Math.abs(scaleY));
@@ -311,6 +330,9 @@ const VideoPreview = ({
// Update offset for center rotation
node.offsetX(newWidth / 2);
node.offsetY(newHeight / 2);
updates.width = newWidth;
updates.height = newHeight;
}
// Calculate position for snapping
@@ -320,8 +342,10 @@ const VideoPreview = ({
// Convert center position to top-left for snapping
const centerX = node.x();
const centerY = node.y();
topLeftX = centerX - newWidth / 2;
topLeftY = centerY - newHeight / 2;
const currentWidth = element.type === 'text' ? node.width() : newWidth;
const currentHeight = element.type === 'text' ? node.height() : newHeight;
topLeftX = centerX - currentWidth / 2;
topLeftY = centerY - currentHeight / 2;
} else {
// Use position directly for text
topLeftX = node.x();
@@ -331,13 +355,15 @@ const VideoPreview = ({
// Check for position snapping during transform (but be less aggressive during rotation)
const isRotating = Math.abs(rotation % 90) > 5; // Not close to perpendicular
if (!isRotating) {
const snapResult = calculateSnapAndGuides(elementId, topLeftX, topLeftY, newWidth, newHeight);
const currentWidth = element.type === 'text' ? node.width() : newWidth;
const currentHeight = element.type === 'text' ? node.height() : newHeight;
const snapResult = calculateSnapAndGuides(elementId, topLeftX, topLeftY, currentWidth, currentHeight);
if (Math.abs(snapResult.x - topLeftX) > 5 || Math.abs(snapResult.y - topLeftY) > 5) {
if (usesCenterPositioning(element.type)) {
// Convert back to center position
const newCenterX = snapResult.x + newWidth / 2;
const newCenterY = snapResult.y + newHeight / 2;
const newCenterX = snapResult.x + currentWidth / 2;
const newCenterY = snapResult.y + currentHeight / 2;
node.x(newCenterX);
node.y(newCenterY);
} else {
@@ -359,18 +385,23 @@ const VideoPreview = ({
});
}
// Update state with the final calculated values
const finalTransform = {
x: topLeftX,
y: topLeftY,
width: newWidth,
height: newHeight,
rotation: rotation,
};
// Always update position and rotation
updates.x = topLeftX;
updates.y = topLeftY;
updates.rotation = rotation;
onElementUpdate(elementId, finalTransform);
// Update state with the calculated values
onElementUpdate(elementId, updates);
// If this is a text element and fontSize changed, emit update for sidebar (without opening it)
if (element.type === 'text' && updates.fontSize && updates.fontSize !== element.fontSize) {
// Small delay to ensure state is updated first
setTimeout(() => {
emitter.emit('text-element-updated', { ...element, ...updates });
}, 50);
}
},
[onElementUpdate, dimensions.width, dimensions.height, timelineElements],
[onElementUpdate, dimensions.width, dimensions.height, timelineElements, emitter],
);
// Update transformer when selection changes
@@ -433,6 +464,10 @@ const VideoPreview = ({
/>
);
} else if (element.type === 'text') {
// Build font style string
const fontWeight = element.fontWeight === 'bold' || element.fontWeight === 700 ? 'bold' : 'normal';
const fontStyle = element.fontStyle === 'italic' ? 'italic' : 'normal';
return (
<Text
key={element.id}
@@ -445,8 +480,8 @@ const VideoPreview = ({
x={element.x}
y={element.y}
fontSize={element.fontSize}
fontStyle={element.fontWeight === 'bold' || element.fontWeight === 700 ? 'bold' : 'normal'} // ADD THIS LINE
fontFamily="Arial"
fontStyle={`${fontStyle} ${fontWeight}`}
fontFamily={element.fontFamily || 'Montserrat'}
fill={element.fill}
stroke={element.stroke}
strokeWidth={element.strokeWidth}