| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123 |
- '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>
- );
- }
|