HighlightOverlay.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. 'use client';
  2. import { useMemo } from 'react';
  3. import { useSceneSelector } from '@/lib/contexts/scene-context';
  4. import { useCanvasStore } from '@/lib/store/canvas';
  5. import type { SlideContent } from '@/lib/types/stage';
  6. import type { PPTElement } from '@/lib/types/slides';
  7. /**
  8. * Highlight overlay component
  9. *
  10. * Features:
  11. * - Overlays highlight effects on top of elements
  12. * - Does not modify element properties
  13. * - Supports highlighting multiple elements simultaneously
  14. * - Supports animation effects (breathing, blinking, etc.)
  15. *
  16. * Implementation:
  17. * - Creates overlay divs at element positions
  18. * - Uses box-shadow for glow effects
  19. * - Uses CSS animation for animated effects
  20. */
  21. export function HighlightOverlay() {
  22. const highlightedElementIds = useCanvasStore.use.highlightedElementIds();
  23. const highlightOptions = useCanvasStore.use.highlightOptions();
  24. // Get the element list of the current scene
  25. const elements = useSceneSelector<SlideContent, PPTElement[]>(
  26. (content) => content.canvas.elements,
  27. );
  28. // Find all elements to highlight (exclude line elements as they have no height property)
  29. const highlightedElements = useMemo(() => {
  30. if (!highlightedElementIds.length) return [];
  31. return elements.filter((el) => highlightedElementIds.includes(el.id) && el.type !== 'line');
  32. }, [elements, highlightedElementIds]);
  33. // Skip rendering if no highlighted elements
  34. if (!highlightedElements.length || !highlightOptions) {
  35. return null;
  36. }
  37. const { color = '#ff6b6b', opacity = 0.3, borderWidth = 3, animated = true } = highlightOptions;
  38. return (
  39. <>
  40. {highlightedElements.map((element) => {
  41. // Type guard: line elements are already filtered out above
  42. // Use 'in' operator for runtime checks to satisfy TypeScript
  43. const height = 'height' in element ? element.height : 0;
  44. const rotate = 'rotate' in element ? element.rotate : 0;
  45. return (
  46. <div
  47. key={element.id}
  48. className="highlight-overlay absolute pointer-events-none"
  49. style={{
  50. left: `${element.left}px`,
  51. top: `${element.top}px`,
  52. width: `${element.width}px`,
  53. height: `${height}px`,
  54. transform: `rotate(${rotate || 0}deg)`,
  55. transformOrigin: 'center',
  56. zIndex: 999,
  57. transition: 'all 0.3s ease-in-out',
  58. }}
  59. >
  60. {/* Highlight border */}
  61. <div
  62. className={`absolute inset-0 rounded ${animated ? 'animate-pulse' : ''}`}
  63. style={{
  64. border: `${borderWidth}px solid ${color}`,
  65. boxShadow: `
  66. 0 0 ${borderWidth * 3}px ${color},
  67. inset 0 0 ${borderWidth * 2}px rgba(255,255,255,${opacity * 0.5})
  68. `,
  69. backgroundColor: `${color}${Math.round(opacity * 255)
  70. .toString(16)
  71. .padStart(2, '0')}`,
  72. }}
  73. />
  74. {/* Glow effect */}
  75. {animated && (
  76. <div
  77. className="absolute inset-0 rounded animate-ping"
  78. style={{
  79. border: `${borderWidth}px solid ${color}`,
  80. opacity: 0.5,
  81. animationDuration: '2s',
  82. }}
  83. />
  84. )}
  85. </div>
  86. );
  87. })}
  88. {/* CSS animation (breathing light effect) */}
  89. <style jsx>{`
  90. @keyframes breathe {
  91. 0%,
  92. 100% {
  93. opacity: 0.6;
  94. transform: scale(1);
  95. }
  96. 50% {
  97. opacity: 1;
  98. transform: scale(1.02);
  99. }
  100. }
  101. .highlight-overlay.animate-pulse {
  102. animation: breathe 2s ease-in-out infinite;
  103. }
  104. `}</style>
  105. </>
  106. );
  107. }