@@ -57,12 +57,10 @@ const sampleTimelineElements = [
|
||||
startTime: 0,
|
||||
layer: 2,
|
||||
duration: 4,
|
||||
x: 90,
|
||||
y: 180,
|
||||
fontSize: 40,
|
||||
fontFamily: 'Montserrat',
|
||||
fontWeight: 'bold',
|
||||
fontStyle: 'normal',
|
||||
x: 50,
|
||||
y: 600,
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold', // ADD THIS LINE
|
||||
fill: 'white',
|
||||
stroke: 'black',
|
||||
strokeWidth: 1,
|
||||
@@ -78,9 +76,7 @@ const sampleTimelineElements = [
|
||||
x: 50,
|
||||
y: 650,
|
||||
fontSize: 20,
|
||||
fontFamily: 'Montserrat',
|
||||
fontWeight: 'bold',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: 'bold', // ADD THIS LINE
|
||||
fill: 'yellow',
|
||||
stroke: 'red',
|
||||
strokeWidth: 2,
|
||||
|
||||
@@ -70,7 +70,7 @@ const VideoEditor = ({ width, height }) => {
|
||||
});
|
||||
};
|
||||
|
||||
// Handle element transformations (position, scale, rotation)
|
||||
// NEW: 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}
|
||||
onElementUpdate={handleElementUpdate} // NEW: Pass the update handler
|
||||
layerRef={layerRef}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -98,29 +98,16 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
|
||||
showConsoleLogs && console.log('🎵 Audio args:', audioArgs);
|
||||
|
||||
// Process text elements with font family support
|
||||
// Process text elements with centering
|
||||
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=/${fontFile}:text='${escapedText}':x=${centerX}:y=${centerY}:fontsize=${t.fontSize}:fontcolor=${t.fill}:borderw=${t.strokeWidth}:bordercolor=${
|
||||
`[${videoLayer}]drawtext=fontfile=/arial.ttf: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}]`,
|
||||
);
|
||||
@@ -224,25 +211,9 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
showConsoleLogs && console.log('FFmpeg loaded!');
|
||||
setExportProgress(20);
|
||||
|
||||
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!');
|
||||
setExportStatus('Loading font...');
|
||||
await ffmpeg.writeFile('arial.ttf', await fetchFile('https://raw.githubusercontent.com/ffmpegwasm/testdata/master/arial.ttf'));
|
||||
showConsoleLogs && console.log('Font loaded!');
|
||||
setExportProgress(30);
|
||||
|
||||
setExportStatus('Downloading media...');
|
||||
|
||||
@@ -58,10 +58,6 @@ 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;
|
||||
@@ -280,7 +276,7 @@ const VideoPreview = ({
|
||||
[onElementUpdate, timelineElements],
|
||||
);
|
||||
|
||||
// Handle transform events (scale, rotate) with fontSize conversion for text
|
||||
// Handle transform events (scale, rotate) with snapping - USES NATIVE KONVA ROTATION SNAPPING
|
||||
const handleTransform = useCallback(
|
||||
(elementId) => {
|
||||
const node = elementRefs.current[elementId];
|
||||
@@ -295,29 +291,11 @@ const VideoPreview = ({
|
||||
const scaleY = node.scaleY();
|
||||
|
||||
let newWidth, newHeight;
|
||||
let updates = {};
|
||||
|
||||
if (element.type === 'text') {
|
||||
// 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);
|
||||
|
||||
// ✅ FIX: Always get current width/height for text elements too
|
||||
newWidth = node.width();
|
||||
newHeight = node.height();
|
||||
|
||||
updates.fontSize = clampedFontSize;
|
||||
updates.width = newWidth; // ✅ Always include width
|
||||
updates.height = newHeight; // ✅ Always include height
|
||||
|
||||
console.log(`Text transform: scale=${scale.toFixed(2)}, oldFontSize=${element.fontSize}, newFontSize=${clampedFontSize}`);
|
||||
// For text, allow free scaling
|
||||
newWidth = node.width() * scaleX;
|
||||
newHeight = node.height() * scaleY;
|
||||
} else {
|
||||
// For images/videos, maintain aspect ratio by using the larger scale
|
||||
const scale = Math.max(Math.abs(scaleX), Math.abs(scaleY));
|
||||
@@ -333,9 +311,6 @@ 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
|
||||
@@ -345,8 +320,8 @@ const VideoPreview = ({
|
||||
// Convert center position to top-left for snapping
|
||||
const centerX = node.x();
|
||||
const centerY = node.y();
|
||||
topLeftX = centerX - newWidth / 2; // ✅ newWidth is now always defined
|
||||
topLeftY = centerY - newHeight / 2; // ✅ newHeight is now always defined
|
||||
topLeftX = centerX - newWidth / 2;
|
||||
topLeftY = centerY - newHeight / 2;
|
||||
} else {
|
||||
// Use position directly for text
|
||||
topLeftX = node.x();
|
||||
@@ -384,23 +359,18 @@ const VideoPreview = ({
|
||||
});
|
||||
}
|
||||
|
||||
// Always update position and rotation
|
||||
updates.x = topLeftX;
|
||||
updates.y = topLeftY;
|
||||
updates.rotation = rotation;
|
||||
// Update state with the final calculated values
|
||||
const finalTransform = {
|
||||
x: topLeftX,
|
||||
y: topLeftY,
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
rotation: rotation,
|
||||
};
|
||||
|
||||
// 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(elementId, finalTransform);
|
||||
},
|
||||
[onElementUpdate, dimensions.width, dimensions.height, timelineElements, emitter],
|
||||
[onElementUpdate, dimensions.width, dimensions.height, timelineElements],
|
||||
);
|
||||
|
||||
// Update transformer when selection changes
|
||||
@@ -463,10 +433,6 @@ 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}
|
||||
@@ -479,8 +445,8 @@ const VideoPreview = ({
|
||||
x={element.x}
|
||||
y={element.y}
|
||||
fontSize={element.fontSize}
|
||||
fontStyle={`${fontStyle} ${fontWeight}`}
|
||||
fontFamily={element.fontFamily || 'Montserrat'}
|
||||
fontStyle={element.fontWeight === 'bold' || element.fontWeight === 700 ? 'bold' : 'normal'} // ADD THIS LINE
|
||||
fontFamily="Arial"
|
||||
fill={element.fill}
|
||||
stroke={element.stroke}
|
||||
strokeWidth={element.strokeWidth}
|
||||
|
||||
Reference in New Issue
Block a user