ScreenCanvas.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. 'use client';
  2. import { ScreenElement } from './ScreenElement';
  3. import { HighlightOverlay } from './HighlightOverlay';
  4. import { SpotlightOverlay } from './SpotlightOverlay';
  5. import { LaserOverlay } from './LaserOverlay';
  6. import { useSlideBackgroundStyle } from '@/lib/hooks/use-slide-background-style';
  7. import { useCanvasStore } from '@/lib/store';
  8. import { useSceneSelector } from '@/lib/contexts/scene-context';
  9. import { findElementGeometry } from '@/lib/utils/geometry';
  10. import type { SlideContent } from '@/lib/types/stage';
  11. import type { PPTElement, SlideBackground } from '@/lib/types/slides';
  12. import type { PercentageGeometry } from '@/lib/types/action';
  13. import { useViewportSize } from './Canvas/hooks/useViewportSize';
  14. import { useRef, useMemo } from 'react';
  15. import { AnimatePresence } from 'motion/react';
  16. export function ScreenCanvas() {
  17. const canvasScale = useCanvasStore.use.canvasScale();
  18. const elements = useSceneSelector<SlideContent, PPTElement[]>(
  19. (content) => content.canvas.elements,
  20. );
  21. const canvasRef = useRef<HTMLDivElement>(null);
  22. // Viewport size and positioning
  23. const { viewportStyles } = useViewportSize(canvasRef);
  24. // Get background style
  25. const background = useSceneSelector<SlideContent, SlideBackground | undefined>(
  26. (content) => content.canvas.background,
  27. );
  28. const { backgroundStyle } = useSlideBackgroundStyle(background);
  29. // Get visual effect state
  30. const laserElementId = useCanvasStore.use.laserElementId();
  31. const laserOptions = useCanvasStore.use.laserOptions();
  32. const zoomTarget = useCanvasStore.use.zoomTarget();
  33. // Compute laser pointer geometry
  34. const laserGeometry = useMemo<PercentageGeometry | null>(() => {
  35. if (!laserElementId) return null;
  36. const element = elements.find((el) => el.id === laserElementId);
  37. if (!element) return null;
  38. return findElementGeometry(
  39. { type: 'slide', content: { canvas: { elements } } } as Record<string, unknown>,
  40. laserElementId,
  41. );
  42. }, [laserElementId, elements]);
  43. // Compute zoom target geometry
  44. const zoomGeometry = useMemo<PercentageGeometry | null>(() => {
  45. if (!zoomTarget) return null;
  46. const element = elements.find((el) => el.id === zoomTarget.elementId);
  47. if (!element) return null;
  48. return findElementGeometry(
  49. { type: 'slide', content: { canvas: { elements } } } as Record<string, unknown>,
  50. zoomTarget.elementId,
  51. );
  52. }, [zoomTarget, elements]);
  53. return (
  54. <div className="relative h-full w-full overflow-hidden select-none" ref={canvasRef}>
  55. <div
  56. 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"
  57. style={{
  58. width: `${viewportStyles.width * canvasScale}px`,
  59. height: `${viewportStyles.height * canvasScale}px`,
  60. left: `${viewportStyles.left}px`,
  61. top: `${viewportStyles.top}px`,
  62. ...(zoomTarget && zoomGeometry
  63. ? {
  64. transform: `scale(${zoomTarget.scale})`,
  65. transformOrigin: `${zoomGeometry.centerX}% ${zoomGeometry.centerY}%`,
  66. }
  67. : {}),
  68. }}
  69. >
  70. {/* Background layer */}
  71. <div
  72. className="w-full h-full bg-position-center rounded-lg"
  73. style={{ ...backgroundStyle }}
  74. ></div>
  75. {/* Content layer - scaled */}
  76. <div
  77. className="absolute top-0 left-0 origin-top-left"
  78. style={{
  79. width: `${viewportStyles.width}px`,
  80. height: `${viewportStyles.height}px`,
  81. transform: `scale(${canvasScale})`,
  82. }}
  83. >
  84. {elements.map((element, index) => (
  85. <ScreenElement key={element.id} elementInfo={element} elementIndex={index + 1} />
  86. ))}
  87. {/* Highlight overlay - stacked above elements */}
  88. <HighlightOverlay />
  89. </div>
  90. {/* Spotlight overlay - covers the entire slide, positioned via DOM measurement */}
  91. <SpotlightOverlay />
  92. {/* Visual effects layer - outside the scale layer, using percentage coordinates */}
  93. <div className="absolute inset-0 pointer-events-none" style={{ padding: '5%' }}>
  94. <div className="relative w-full h-full">
  95. {/* Laser pointer overlay */}
  96. <AnimatePresence>
  97. {laserElementId && laserGeometry && (
  98. <LaserOverlay
  99. key={`laser-${laserElementId}`}
  100. geometry={laserGeometry}
  101. color={laserOptions?.color}
  102. duration={laserOptions?.duration}
  103. />
  104. )}
  105. </AnimatePresence>
  106. </div>
  107. </div>
  108. </div>
  109. </div>
  110. );
  111. }