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