/** * RuntimeElement Component * * Renders a single UI element with interactive effects at runtime. * Handles hover, focus, active states and positioning. * Delegates element styling and content to UiElementRenderer. */ import React from 'react'; import UiElementRenderer from './UiElements/UiElementRenderer'; import { useElementEffects } from '../hooks/useElementEffects'; import { buildTransitionStyle, buildAppearAnimationStyle, hasAnyEffects, type ElementEffectProperties, } from '../lib/elementEffects'; import type { CanvasElement } from '../types/constructor'; import type { ResolvedTransitionSettings } from '../types/transition'; import type { PreloadCacheProvider } from '../hooks/video'; interface RuntimeElementProps { element: CanvasElement; onClick: () => void; /** Optional URL resolver for preloaded blob URLs */ resolveUrl?: (url: string | undefined) => string; /** Gallery card click handler */ onGalleryCardClick?: (cardIndex: number) => void; /** Letterbox styles for constraining fullscreen elements to canvas bounds */ letterboxStyles?: React.CSSProperties; /** Page transition settings (for slide transition cascade in carousel/gallery) */ pageTransitionSettings?: ResolvedTransitionSettings; /** Preload cache provider for video elements */ preloadCache?: PreloadCacheProvider; } // Clamp position to canvas bounds (0-100%) const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max); const RuntimeElement: React.FC = ({ element, onClick, resolveUrl, onGalleryCardClick, letterboxStyles, pageTransitionSettings, preloadCache, }) => { // Clamp coordinates to canvas bounds const xPercent = clamp(element.xPercent ?? 50, 0, 100); const yPercent = clamp(element.yPercent ?? 50, 0, 100); const rotation = element.rotation ?? 0; // Extract effect properties from element const effectProperties: Partial = { appearAnimation: element.appearAnimation, appearAnimationDuration: element.appearAnimationDuration, appearAnimationEasing: element.appearAnimationEasing, hoverScale: element.hoverScale, hoverOpacity: element.hoverOpacity, hoverBackgroundColor: element.hoverBackgroundColor, hoverColor: element.hoverColor, hoverBoxShadow: element.hoverBoxShadow, hoverTransitionDuration: element.hoverTransitionDuration, focusScale: element.focusScale, focusOpacity: element.focusOpacity, focusOutline: element.focusOutline, focusBoxShadow: element.focusBoxShadow, activeScale: element.activeScale, activeOpacity: element.activeOpacity, activeBackgroundColor: element.activeBackgroundColor, }; // Use effects hook for interactive states const { effectStyle, eventHandlers } = useElementEffects(effectProperties); // Build base position style let positionStyle: React.CSSProperties = { left: `${xPercent}%`, top: `${yPercent}%`, transform: `translate(-50%, -50%)${rotation ? ` rotate(${rotation}deg)` : ''}`, }; // Merge transform if effect style has transform if (effectStyle.transform) { // Preserve the translate and rotation, add effect transform positionStyle.transform = `translate(-50%, -50%)${rotation ? ` rotate(${rotation}deg)` : ''} ${effectStyle.transform}`; // Remove transform from effectStyle to avoid double application const { transform, ...restEffectStyle } = effectStyle; positionStyle = { ...positionStyle, ...restEffectStyle }; } else { positionStyle = { ...positionStyle, ...effectStyle }; } // Add transition if element has any effects if (hasAnyEffects(effectProperties)) { const transitionStyle = buildTransitionStyle(effectProperties); positionStyle = { ...positionStyle, ...transitionStyle }; } // Add appear animation if configured if (effectProperties.appearAnimation) { const animationStyle = buildAppearAnimationStyle(effectProperties); positionStyle = { ...positionStyle, ...animationStyle }; } return (
); }; export default RuntimeElement;