From aeb8fd6000d00ebbc2ccd20d70d9e33c9ff1da74 Mon Sep 17 00:00:00 2001 From: ct Date: Wed, 18 Jun 2025 11:50:17 +0800 Subject: [PATCH] Update --- .../editor/partials/canvas/video-preview.jsx | 394 ++---------------- .../video-preview-element-selection.js | 38 ++ .../video-preview-element-transform.js | 253 +++++++++++ .../video-preview/video-preview-utils.js | 150 +++++++ .../single_caption_meme_background.json | 8 +- 5 files changed, 476 insertions(+), 367 deletions(-) create mode 100644 resources/js/modules/editor/partials/canvas/video-preview/video-preview-element-selection.js create mode 100644 resources/js/modules/editor/partials/canvas/video-preview/video-preview-element-transform.js create mode 100644 resources/js/modules/editor/partials/canvas/video-preview/video-preview-utils.js diff --git a/resources/js/modules/editor/partials/canvas/video-preview.jsx b/resources/js/modules/editor/partials/canvas/video-preview.jsx index d503b82..edd5ffd 100644 --- a/resources/js/modules/editor/partials/canvas/video-preview.jsx +++ b/resources/js/modules/editor/partials/canvas/video-preview.jsx @@ -1,7 +1,12 @@ import { useMitt } from '@/plugins/MittContext'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useEffect, useRef } from 'react'; import { Image, Layer, Line, Stage, Text, Transformer } from 'react-konva'; +// Import our custom hooks and utilities +import { useElementSelection } from './video-preview/video-preview-element-selection'; +import { useElementTransform } from './video-preview/video-preview-element-transform'; +import { getImageSource, getTextFontStyle } from './video-preview/video-preview-utils'; + const VideoPreview = ({ // Dimensions dimensions, @@ -39,368 +44,33 @@ const VideoPreview = ({ }) => { const emitter = useMitt(); - // Selection state - const [selectedElementId, setSelectedElementId] = useState(null); + // Refs for elements and transformer const transformerRef = useRef(null); const stageRef = useRef(null); - - // Refs for each element to connect with transformer const elementRefs = useRef({}); - // Guide lines state - const [guideLines, setGuideLines] = useState({ - vertical: null, - horizontal: null, - showVertical: false, - showHorizontal: false, - }); + // Use our custom hooks + const { + selectedElementId, + setSelectedElementId, + handleElementSelect, + handleStageClick: baseHandleStageClick, + } = useElementSelection(emitter, timelineElements); - // Snap settings - const POSITION_SNAP_THRESHOLD = 10; // Pixels within which to snap to center - - // Function to determine which image source to use for videos - const getImageSource = (element) => { - const isVideoActive = videoStates[element.id] && isPlaying; - - if (isVideoActive && element.videoElement && element.isVideoReady) { - return element.videoElement; - } else if (element.posterImage && element.isVideoPoster) { - return element.posterImage; - } - - return null; - }; - - // Helper function to get font style for text elements - const getTextFontStyle = (element) => { - const isBold = element.fontWeight === 'bold' || element.fontWeight === 700; - const isItalic = element.fontStyle === 'italic'; - - if (isBold && isItalic) { - return 'bold italic'; - } else if (isBold) { - return 'bold'; - } else if (isItalic) { - return 'italic'; - } else { - return 'normal'; - } - }; - - // Check if element uses center-offset positioning - const usesCenterPositioning = (elementType) => { - return elementType === 'video' || elementType === 'image'; - }; - - // Check if position should snap to center and calculate guide lines - const calculateSnapAndGuides = (elementId, newX, newY, width, height) => { - const centerX = dimensions.width / 2; - const centerY = dimensions.height / 2; - - // Calculate element center - const elementCenterX = newX + width / 2; - const elementCenterY = newY + height / 2; - - let snapX = newX; - let snapY = newY; - let showVertical = false; - let showHorizontal = false; - let verticalLine = null; - let horizontalLine = null; - - // Check vertical center snap - if (Math.abs(elementCenterX - centerX) < POSITION_SNAP_THRESHOLD) { - snapX = centerX - width / 2; - showVertical = true; - verticalLine = centerX; - } - - // Check horizontal center snap - if (Math.abs(elementCenterY - centerY) < POSITION_SNAP_THRESHOLD) { - snapY = centerY - height / 2; - showHorizontal = true; - horizontalLine = centerY; - } - - return { - x: snapX, - y: snapY, - guideLines: { - vertical: verticalLine, - horizontal: horizontalLine, - showVertical, - showHorizontal, - }, - }; - }; - - // Handle element selection - const handleElementSelect = useCallback( - (elementId) => { - setSelectedElementId(elementId); - - // Find the selected element - const element = timelineElements.find((el) => el.id === elementId); - - // If it's a text element, emit text-element-selected event - if (element && element.type === 'text') { - emitter.emit('text-element-selected', element); - } - - // Clear guide lines when selecting - setGuideLines({ - vertical: null, - horizontal: null, - showVertical: false, - showHorizontal: false, - }); - }, - [emitter, timelineElements], + const { guideLines, handleDragMove, handleDragEnd, createDragBoundFunc, handleTransform, clearGuideLines } = useElementTransform( + timelineElements, + dimensions, + onElementUpdate, + elementRefs, ); - // Handle clicking on empty space to deselect - const handleStageClick = useCallback((e) => { - // If clicking on stage (not on an element), deselect + // Enhanced stage click handler that also clears guide lines + const handleStageClick = (e) => { + baseHandleStageClick(e); if (e.target === e.target.getStage()) { - setSelectedElementId(null); - setGuideLines({ - vertical: null, - horizontal: null, - showVertical: false, - showHorizontal: false, - }); + clearGuideLines(); } - }, []); - - // Handle drag events with snapping - const handleDragMove = useCallback( - (elementId, e) => { - const node = e.target; - const element = timelineElements.find((el) => el.id === elementId); - if (!element) return; - - const width = node.width() * node.scaleX(); - const height = node.height() * node.scaleY(); - - let topLeftX, topLeftY; - - if (usesCenterPositioning(element.type)) { - // For center-positioned elements (video/image), convert center to top-left - const elementCenterX = node.x(); - const elementCenterY = node.y(); - topLeftX = elementCenterX - width / 2; - topLeftY = elementCenterY - height / 2; - } else { - // For top-left positioned elements (text) - topLeftX = node.x(); - topLeftY = node.y(); - } - - const snapResult = calculateSnapAndGuides(elementId, topLeftX, topLeftY, width, height); - - // Update guide lines - setGuideLines(snapResult.guideLines); - - // Update state during drag - if (onElementUpdate) { - onElementUpdate(elementId, { - x: snapResult.x, - y: snapResult.y, - }); - } - }, - [onElementUpdate, dimensions.width, dimensions.height, timelineElements], - ); - - // Create drag bound function for real-time snapping - const createDragBoundFunc = useCallback( - (elementId) => { - return (pos) => { - const element = timelineElements.find((el) => el.id === elementId); - if (!element) return pos; - - const node = elementRefs.current[elementId]; - if (!node) return pos; - - const width = node.width() * node.scaleX(); - const height = node.height() * node.scaleY(); - - let topLeftX, topLeftY; - - if (usesCenterPositioning(element.type)) { - // Convert center position to top-left for snapping calculations - topLeftX = pos.x - width / 2; - topLeftY = pos.y - height / 2; - } else { - topLeftX = pos.x; - topLeftY = pos.y; - } - - const snapResult = calculateSnapAndGuides(elementId, topLeftX, topLeftY, width, height); - - if (usesCenterPositioning(element.type)) { - // Convert back to center position - return { - x: snapResult.x + width / 2, - y: snapResult.y + height / 2, - }; - } else { - return { - x: snapResult.x, - y: snapResult.y, - }; - } - }; - }, - [timelineElements, dimensions.width, dimensions.height], - ); - - const handleDragEnd = useCallback( - (elementId, e) => { - const node = e.target; - const element = timelineElements.find((el) => el.id === elementId); - if (!element) return; - - // Clear guide lines when drag ends - setGuideLines({ - vertical: null, - horizontal: null, - showVertical: false, - showHorizontal: false, - }); - - // Final position update - const width = node.width() * node.scaleX(); - const height = node.height() * node.scaleY(); - - let finalX, finalY; - if (usesCenterPositioning(element.type)) { - finalX = node.x() - width / 2; - finalY = node.y() - height / 2; - } else { - finalX = node.x(); - finalY = node.y(); - } - - if (onElementUpdate) { - onElementUpdate(elementId, { - x: finalX, - y: finalY, - }); - } - }, - [onElementUpdate, timelineElements], - ); - - // Handle transform events - TRANSFORMER IS SOURCE OF TRUTH - const handleTransform = useCallback( - (elementId) => { - const node = elementRefs.current[elementId]; - const element = timelineElements.find((el) => el.id === elementId); - if (!node || !onElementUpdate || !element) return; - - // Get rotation - Konva handles snapping automatically with rotationSnaps - const rotation = node.rotation(); - - // Get the scale values from Konva - const scaleX = node.scaleX(); - const scaleY = node.scaleY(); - - let newWidth, newHeight; - let updates = { rotation }; - - if (element.type === 'text') { - // For text: Convert scale to actual fontSize and reset scale to 1 - const avgScale = Math.max(Math.abs(scaleX), Math.abs(scaleY)); - const newFontSize = Math.round(element.fontSize * avgScale); - - // Clamp font size to reasonable limits - const clampedFontSize = Math.max(8, Math.min(120, newFontSize)); - - updates.fontSize = clampedFontSize; - - // Reset scale to 1 since we've applied it to fontSize - node.scaleX(1); - node.scaleY(1); - - // Text dimensions are handled by Konva based on content and fontSize - newWidth = node.width(); - newHeight = node.height(); - } else { - // For images/videos, maintain aspect ratio by using the larger scale - const scale = Math.max(Math.abs(scaleX), Math.abs(scaleY)); - newWidth = node.width() * scale; - newHeight = node.height() * scale; - - // Reset scale to 1 and update dimensions to maintain aspect ratio - node.scaleX(1); - node.scaleY(1); - node.width(newWidth); - node.height(newHeight); - - // Update offset for center rotation - node.offsetX(newWidth / 2); - node.offsetY(newHeight / 2); - - updates.width = newWidth; - updates.height = newHeight; - } - - // Calculate position for snapping - let topLeftX, topLeftY; - - if (usesCenterPositioning(element.type)) { - // Convert center position to top-left for snapping - const centerX = node.x(); - const centerY = node.y(); - topLeftX = centerX - newWidth / 2; - topLeftY = centerY - newHeight / 2; - } else { - // Use position directly for text - topLeftX = node.x(); - topLeftY = node.y(); - } - - // 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); - - 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; - node.x(newCenterX); - node.y(newCenterY); - } else { - // Apply directly for text - node.x(snapResult.x); - node.y(snapResult.y); - } - setGuideLines(snapResult.guideLines); - topLeftX = snapResult.x; - topLeftY = snapResult.y; - } - } else { - // Clear guide lines during rotation - setGuideLines({ - vertical: null, - horizontal: null, - showVertical: false, - showHorizontal: false, - }); - } - - // Add position to updates - updates.x = topLeftX; - updates.y = topLeftY; - - // Update state with all calculated values - onElementUpdate(elementId, updates); - }, - [onElementUpdate, dimensions.width, dimensions.height, timelineElements], - ); + }; // Update transformer when selection changes useEffect(() => { @@ -424,7 +94,7 @@ const VideoPreview = ({ const isSelected = selectedElementId === element.id; if (element.type === 'video') { - const imageSource = getImageSource(element); + const imageSource = getImageSource(element, videoStates, isPlaying); if (!imageSource) { return null; @@ -484,6 +154,7 @@ const VideoPreview = ({ rotation={element.rotation || 0} // Center the text horizontally align="center" + verticalAlign="middle" // Let text have natural width and height for multiline support wrap="word" // Always scale 1 - size changes go through fontSize @@ -496,7 +167,9 @@ const VideoPreview = ({ onDragMove={(e) => handleDragMove(element.id, e)} onDragEnd={(e) => handleDragEnd(element.id, e)} onTransform={() => handleTransform(element.id)} - width={600} + // Apply fixedWidth and offsetX if they exist + width={element.fixedWidth} + offsetX={element.offsetX} // Visual feedback for selection /> ); @@ -583,14 +256,7 @@ const VideoPreview = ({ rotationSnaps={[0, 90, 180, 270]} rotationSnapTolerance={8} // Clear guide lines when transform ends - onTransformEnd={() => { - setGuideLines({ - vertical: null, - horizontal: null, - showVertical: false, - showHorizontal: false, - }); - }} + onTransformEnd={clearGuideLines} // Style the rotation anchor to be circular anchorStyleFunc={(anchor) => { if (anchor.hasName('.rotater')) { diff --git a/resources/js/modules/editor/partials/canvas/video-preview/video-preview-element-selection.js b/resources/js/modules/editor/partials/canvas/video-preview/video-preview-element-selection.js new file mode 100644 index 0000000..8bdc27d --- /dev/null +++ b/resources/js/modules/editor/partials/canvas/video-preview/video-preview-element-selection.js @@ -0,0 +1,38 @@ +// video-preview-element-selection.js +import { useCallback, useState } from 'react'; + +export const useElementSelection = (emitter, timelineElements) => { + // Selection state + const [selectedElementId, setSelectedElementId] = useState(null); + + // Handle element selection + const handleElementSelect = useCallback( + (elementId) => { + setSelectedElementId(elementId); + + // Find the selected element + const element = timelineElements.find((el) => el.id === elementId); + + // If it's a text element, emit text-element-selected event + if (element && element.type === 'text') { + emitter.emit('text-element-selected', element); + } + }, + [emitter, timelineElements], + ); + + // Handle clicking on empty space to deselect + const handleStageClick = useCallback((e) => { + // If clicking on stage (not on an element), deselect + if (e.target === e.target.getStage()) { + setSelectedElementId(null); + } + }, []); + + return { + selectedElementId, + setSelectedElementId, + handleElementSelect, + handleStageClick, + }; +}; diff --git a/resources/js/modules/editor/partials/canvas/video-preview/video-preview-element-transform.js b/resources/js/modules/editor/partials/canvas/video-preview/video-preview-element-transform.js new file mode 100644 index 0000000..0c4187c --- /dev/null +++ b/resources/js/modules/editor/partials/canvas/video-preview/video-preview-element-transform.js @@ -0,0 +1,253 @@ +// video-preview-element-transform.js +import { useCallback, useState } from 'react'; +import { calculateSnapAndGuides, usesCenterPositioning } from './video-preview-utils'; + +export const useElementTransform = (timelineElements, dimensions, onElementUpdate, elementRefs) => { + // Guide lines state + const [guideLines, setGuideLines] = useState({ + vertical: null, + horizontal: null, + showVertical: false, + showHorizontal: false, + }); + + // Handle drag events with snapping + const handleDragMove = useCallback( + (elementId, e) => { + const node = e.target; + const element = timelineElements.find((el) => el.id === elementId); + if (!element) return; + + // Use fixedWidth if available, otherwise use scaled width + const width = element.fixedWidth || node.width() * node.scaleX(); + const height = node.height() * node.scaleY(); + + let positionX = node.x(); + let positionY = node.y(); + + // For center-positioned elements, convert to top-left for consistent snapping + if (usesCenterPositioning(element.type)) { + positionX = node.x() - width / 2; + positionY = node.y() - height / 2; + } + + const snapResult = calculateSnapAndGuides(elementId, positionX, positionY, width, height, timelineElements, dimensions); + + // Update guide lines + setGuideLines(snapResult.guideLines); + + // Update state during drag + if (onElementUpdate) { + onElementUpdate(elementId, { + x: snapResult.x, + y: snapResult.y, + }); + } + }, + [onElementUpdate, dimensions, timelineElements], + ); + + // Create drag bound function for real-time snapping + const createDragBoundFunc = useCallback( + (elementId) => { + return (pos) => { + const element = timelineElements.find((el) => el.id === elementId); + if (!element) return pos; + + const node = elementRefs.current[elementId]; + if (!node) return pos; + + // Use fixedWidth if available, otherwise use scaled width + const width = element.fixedWidth || node.width() * node.scaleX(); + const height = node.height() * node.scaleY(); + + let inputX = pos.x; + let inputY = pos.y; + + // Convert center position to top-left for snapping calculations if needed + if (usesCenterPositioning(element.type)) { + inputX = pos.x - width / 2; + inputY = pos.y - height / 2; + } + + const snapResult = calculateSnapAndGuides(elementId, inputX, inputY, width, height, timelineElements, dimensions); + + // Convert back to the appropriate coordinate system + if (usesCenterPositioning(element.type)) { + return { + x: snapResult.x + width / 2, + y: snapResult.y + height / 2, + }; + } else { + return { + x: snapResult.x, + y: snapResult.y, + }; + } + }; + }, + [timelineElements, dimensions, elementRefs], + ); + + const handleDragEnd = useCallback( + (elementId, e) => { + const node = e.target; + const element = timelineElements.find((el) => el.id === elementId); + if (!element) return; + + // Clear guide lines when drag ends + setGuideLines({ + vertical: null, + horizontal: null, + showVertical: false, + showHorizontal: false, + }); + + // Final position update + const width = element.fixedWidth || node.width() * node.scaleX(); + const height = node.height() * node.scaleY(); + + let finalX = node.x(); + let finalY = node.y(); + + // Convert center position to top-left for consistent storage + if (usesCenterPositioning(element.type)) { + finalX = node.x() - width / 2; + finalY = node.y() - height / 2; + } + + if (onElementUpdate) { + onElementUpdate(elementId, { + x: finalX, + y: finalY, + }); + } + }, + [onElementUpdate, timelineElements], + ); + + // Handle transform events - TRANSFORMER IS SOURCE OF TRUTH + const handleTransform = useCallback( + (elementId) => { + const node = elementRefs.current[elementId]; + const element = timelineElements.find((el) => el.id === elementId); + if (!node || !onElementUpdate || !element) return; + + // Get rotation - Konva handles snapping automatically with rotationSnaps + const rotation = node.rotation(); + + // Get the scale values from Konva + const scaleX = node.scaleX(); + const scaleY = node.scaleY(); + + let newWidth, newHeight; + let updates = { rotation }; + + if (element.type === 'text') { + // For text: Convert scale to actual fontSize and reset scale to 1 + const avgScale = Math.max(Math.abs(scaleX), Math.abs(scaleY)); + const newFontSize = Math.round(element.fontSize * avgScale); + + // Clamp font size to reasonable limits + const clampedFontSize = Math.max(8, Math.min(120, newFontSize)); + + updates.fontSize = clampedFontSize; + + // Reset scale to 1 since we've applied it to fontSize + node.scaleX(1); + node.scaleY(1); + + // Use fixedWidth if available, otherwise use natural width + newWidth = element.fixedWidth || node.width(); + newHeight = node.height(); + } else { + // For images/videos, maintain aspect ratio by using the larger scale + const scale = Math.max(Math.abs(scaleX), Math.abs(scaleY)); + newWidth = node.width() * scale; + newHeight = node.height() * scale; + + // Reset scale to 1 and update dimensions to maintain aspect ratio + node.scaleX(1); + node.scaleY(1); + node.width(newWidth); + node.height(newHeight); + + // Update offset for center rotation + node.offsetX(newWidth / 2); + node.offsetY(newHeight / 2); + + updates.width = newWidth; + updates.height = newHeight; + } + + // Calculate position for snapping + let currentX = node.x(); + let currentY = node.y(); + + // Convert center position to top-left for snapping if needed + if (usesCenterPositioning(element.type)) { + currentX = node.x() - newWidth / 2; + currentY = node.y() - newHeight / 2; + } + + // 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, currentX, currentY, newWidth, newHeight, timelineElements, dimensions); + + if (Math.abs(snapResult.x - currentX) > 5 || Math.abs(snapResult.y - currentY) > 5) { + if (usesCenterPositioning(element.type)) { + // Convert back to center position + const newCenterX = snapResult.x + newWidth / 2; + const newCenterY = snapResult.y + newHeight / 2; + node.x(newCenterX); + node.y(newCenterY); + } else { + // Apply directly for text + node.x(snapResult.x); + node.y(snapResult.y); + } + setGuideLines(snapResult.guideLines); + currentX = snapResult.x; + currentY = snapResult.y; + } + } else { + // Clear guide lines during rotation + setGuideLines({ + vertical: null, + horizontal: null, + showVertical: false, + showHorizontal: false, + }); + } + + // Add position to updates (always store as top-left coordinates) + updates.x = currentX; + updates.y = currentY; + + // Update state with all calculated values + onElementUpdate(elementId, updates); + }, + [onElementUpdate, dimensions, timelineElements, elementRefs], + ); + + // Clear guide lines helper + const clearGuideLines = useCallback(() => { + setGuideLines({ + vertical: null, + horizontal: null, + showVertical: false, + showHorizontal: false, + }); + }, []); + + return { + guideLines, + setGuideLines, + handleDragMove, + handleDragEnd, + createDragBoundFunc, + handleTransform, + clearGuideLines, + }; +}; diff --git a/resources/js/modules/editor/partials/canvas/video-preview/video-preview-utils.js b/resources/js/modules/editor/partials/canvas/video-preview/video-preview-utils.js new file mode 100644 index 0000000..6d0e0a4 --- /dev/null +++ b/resources/js/modules/editor/partials/canvas/video-preview/video-preview-utils.js @@ -0,0 +1,150 @@ +// video-preview-utils.js + +// Snap settings +export const POSITION_SNAP_THRESHOLD = 10; // Pixels within which to snap to center + +// Function to determine which image source to use for videos +export const getImageSource = (element, videoStates, isPlaying) => { + const isVideoActive = videoStates[element.id] && isPlaying; + + if (isVideoActive && element.videoElement && element.isVideoReady) { + return element.videoElement; + } else if (element.posterImage && element.isVideoPoster) { + return element.posterImage; + } + + return null; +}; + +// Helper function to get font style for text elements +export const getTextFontStyle = (element) => { + const isBold = element.fontWeight === 'bold' || element.fontWeight === 700; + const isItalic = element.fontStyle === 'italic'; + + if (isBold && isItalic) { + return 'bold italic'; + } else if (isBold) { + return 'bold'; + } else if (isItalic) { + return 'italic'; + } else { + return 'normal'; + } +}; + +// Check if element uses center-offset positioning +export const usesCenterPositioning = (elementType) => { + return elementType === 'video' || elementType === 'image'; +}; + +// Helper function to get the visual bounds of an element accounting for offsetX +export const getVisualBounds = (element, x, y, width, height) => { + // Check if element has offsetX property + if (element.offsetX !== undefined) { + // For elements with offsetX, calculate the visual position + const visualLeft = x - element.offsetX; + const elementWidth = element.fixedWidth || width; + return { + left: visualLeft, + top: y, + right: visualLeft + elementWidth, + bottom: y + height, + width: elementWidth, + height: height, + centerX: visualLeft + elementWidth / 2, + centerY: y + height / 2, + }; + } else if (usesCenterPositioning(element.type)) { + // For center-positioned elements (video/image) + const left = x - width / 2; + const top = y - height / 2; + return { + left: left, + top: top, + right: left + width, + bottom: top + height, + width: width, + height: height, + centerX: x, + centerY: y, + }; + } else { + // For regular top-left positioned elements + return { + left: x, + top: y, + right: x + width, + bottom: y + height, + width: width, + height: height, + centerX: x + width / 2, + centerY: y + height / 2, + }; + } +}; + +// Check if position should snap to center and calculate guide lines +export const calculateSnapAndGuides = (elementId, newX, newY, width, height, timelineElements, dimensions) => { + const centerX = dimensions.width / 2; + const centerY = dimensions.height / 2; + + const element = timelineElements.find((el) => el.id === elementId); + if (!element) + return { + x: newX, + y: newY, + guideLines: { + vertical: null, + horizontal: null, + showVertical: false, + showHorizontal: false, + }, + }; + + // Get visual bounds accounting for offsetX and fixedWidth + const visualBounds = getVisualBounds(element, newX, newY, width, height); + + let snapX = newX; + let snapY = newY; + let showVertical = false; + let showHorizontal = false; + let verticalLine = null; + let horizontalLine = null; + + // Check vertical center snap using visual center + if (Math.abs(visualBounds.centerX - centerX) < POSITION_SNAP_THRESHOLD) { + if (element.offsetX !== undefined) { + // For elements with offsetX, calculate the x position that will center the visual bounds + const targetVisualLeft = centerX - visualBounds.width / 2; + snapX = targetVisualLeft + element.offsetX; + } else if (usesCenterPositioning(element.type)) { + snapX = centerX; + } else { + snapX = centerX - width / 2; + } + showVertical = true; + verticalLine = centerX; + } + + // Check horizontal center snap using visual center + if (Math.abs(visualBounds.centerY - centerY) < POSITION_SNAP_THRESHOLD) { + if (usesCenterPositioning(element.type)) { + snapY = centerY; + } else { + snapY = centerY - height / 2; + } + showHorizontal = true; + horizontalLine = centerY; + } + + return { + x: snapX, + y: snapY, + guideLines: { + vertical: verticalLine, + horizontal: horizontalLine, + showVertical, + showHorizontal, + }, + }; +}; diff --git a/resources/js/modules/editor/templates/single_caption_meme_background.json b/resources/js/modules/editor/templates/single_caption_meme_background.json index 999aad5..0b614fe 100644 --- a/resources/js/modules/editor/templates/single_caption_meme_background.json +++ b/resources/js/modules/editor/templates/single_caption_meme_background.json @@ -47,8 +47,8 @@ "startTime": 0, "layer": 3, "duration": 6, - "x": 90, - "y": 180, + "x": 0, + "y": 200, "fontSize": 40, "fontWeight": "bold", "fontFamily": "Montserrat", @@ -56,7 +56,9 @@ "fill": "#ffffff", "stroke": "#000000", "strokeWidth": 3, - "rotation": 0 + "rotation": 0, + "fixedWidth": 600, + "offsetX": -60 } ] }