Spaces:
No application file
No application file
| 'use client'; | |
| import { ScreenElement } from './ScreenElement'; | |
| import { HighlightOverlay } from './HighlightOverlay'; | |
| import { SpotlightOverlay } from './SpotlightOverlay'; | |
| import { LaserOverlay } from './LaserOverlay'; | |
| import { useSlideBackgroundStyle } from '@/lib/hooks/use-slide-background-style'; | |
| import { useCanvasStore } from '@/lib/store'; | |
| import { useSceneSelector } from '@/lib/contexts/scene-context'; | |
| import { findElementGeometry } from '@/lib/utils/geometry'; | |
| import type { SlideContent } from '@/lib/types/stage'; | |
| import type { PPTElement, SlideBackground } from '@/lib/types/slides'; | |
| import type { PercentageGeometry } from '@/lib/types/action'; | |
| import { useViewportSize } from './Canvas/hooks/useViewportSize'; | |
| import { useRef, useMemo } from 'react'; | |
| import { AnimatePresence } from 'motion/react'; | |
| export function ScreenCanvas() { | |
| const canvasScale = useCanvasStore.use.canvasScale(); | |
| const elements = useSceneSelector<SlideContent, PPTElement[]>( | |
| (content) => content.canvas.elements, | |
| ); | |
| const canvasRef = useRef<HTMLDivElement>(null); | |
| // Viewport size and positioning | |
| const { viewportStyles } = useViewportSize(canvasRef); | |
| // Get background style | |
| const background = useSceneSelector<SlideContent, SlideBackground | undefined>( | |
| (content) => content.canvas.background, | |
| ); | |
| const { backgroundStyle } = useSlideBackgroundStyle(background); | |
| // Get visual effect state | |
| const laserElementId = useCanvasStore.use.laserElementId(); | |
| const laserOptions = useCanvasStore.use.laserOptions(); | |
| const zoomTarget = useCanvasStore.use.zoomTarget(); | |
| // Compute laser pointer geometry | |
| const laserGeometry = useMemo<PercentageGeometry | null>(() => { | |
| if (!laserElementId) return null; | |
| const element = elements.find((el) => el.id === laserElementId); | |
| if (!element) return null; | |
| return findElementGeometry( | |
| { type: 'slide', content: { canvas: { elements } } } as Record<string, unknown>, | |
| laserElementId, | |
| ); | |
| }, [laserElementId, elements]); | |
| // Compute zoom target geometry | |
| const zoomGeometry = useMemo<PercentageGeometry | null>(() => { | |
| if (!zoomTarget) return null; | |
| const element = elements.find((el) => el.id === zoomTarget.elementId); | |
| if (!element) return null; | |
| return findElementGeometry( | |
| { type: 'slide', content: { canvas: { elements } } } as Record<string, unknown>, | |
| zoomTarget.elementId, | |
| ); | |
| }, [zoomTarget, elements]); | |
| return ( | |
| <div className="relative h-full w-full overflow-hidden select-none" ref={canvasRef}> | |
| <div | |
| className="absolute shadow-[0_0_0_1px_rgba(0,0,0,0.01),0_0_12px_0_rgba(0,0,0,0.1)] rounded-lg overflow-hidden transition-transform duration-700" | |
| style={{ | |
| width: `${viewportStyles.width * canvasScale}px`, | |
| height: `${viewportStyles.height * canvasScale}px`, | |
| left: `${viewportStyles.left}px`, | |
| top: `${viewportStyles.top}px`, | |
| ...(zoomTarget && zoomGeometry | |
| ? { | |
| transform: `scale(${zoomTarget.scale})`, | |
| transformOrigin: `${zoomGeometry.centerX}% ${zoomGeometry.centerY}%`, | |
| } | |
| : {}), | |
| }} | |
| > | |
| {/* Background layer */} | |
| <div | |
| className="w-full h-full bg-position-center rounded-lg" | |
| style={{ ...backgroundStyle }} | |
| ></div> | |
| {/* Content layer - scaled */} | |
| <div | |
| className="absolute top-0 left-0 origin-top-left" | |
| style={{ | |
| width: `${viewportStyles.width}px`, | |
| height: `${viewportStyles.height}px`, | |
| transform: `scale(${canvasScale})`, | |
| }} | |
| > | |
| {elements.map((element, index) => ( | |
| <ScreenElement key={element.id} elementInfo={element} elementIndex={index + 1} /> | |
| ))} | |
| {/* Highlight overlay - stacked above elements */} | |
| <HighlightOverlay /> | |
| </div> | |
| {/* Spotlight overlay - covers the entire slide, positioned via DOM measurement */} | |
| <SpotlightOverlay /> | |
| {/* Visual effects layer - outside the scale layer, using percentage coordinates */} | |
| <div className="absolute inset-0 pointer-events-none" style={{ padding: '5%' }}> | |
| <div className="relative w-full h-full"> | |
| {/* Laser pointer overlay */} | |
| <AnimatePresence> | |
| {laserElementId && laserGeometry && ( | |
| <LaserOverlay | |
| key={`laser-${laserElementId}`} | |
| geometry={laserGeometry} | |
| color={laserOptions?.color} | |
| duration={laserOptions?.duration} | |
| /> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |