Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 22 additions & 48 deletions components/slide-renderer/Editor/LaserOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
'use client';

import { motion } from 'motion/react';
import { laserV1 } from '@/lib/choreography';
import type { PercentageGeometry } from '@/lib/types/action';
import { resolveMotionLayer } from './motion-descriptor-adapter';

interface LaserOverlayProps {
geometry: PercentageGeometry;
Expand All @@ -17,66 +19,38 @@ interface LaserOverlayProps {
* - Elegant light dot with soft breathing glow
* - Uses percentage positioning (0-100)
*/
export function LaserOverlay({
geometry,
color = '#ff3b30',
duration: _duration = 3000,
}: LaserOverlayProps) {
export function LaserOverlay({ geometry, color, duration: _duration = 3000 }: LaserOverlayProps) {
const { centerX, centerY } = geometry;

const startPos = {
x: centerX > 50 ? 105 : -5,
y: centerY > 50 ? 105 : -5,
const layerOptions = {
geometry,
units: { left: '%', top: '%' },
...(color !== undefined ? { params: { color } } : {}),
};
const dot = resolveMotionLayer(laserV1, 'dot', layerOptions);
const ring = resolveMotionLayer(laserV1, 'ring', layerOptions);
const core = resolveMotionLayer(laserV1, 'core', layerOptions);

return (
<motion.div
key={`laser-${centerX}-${centerY}`}
initial={{
opacity: 0,
left: `${startPos.x}%`,
top: `${startPos.y}%`,
}}
animate={{
opacity: 1,
left: `${centerX}%`,
top: `${centerY}%`,
}}
exit={{
opacity: 0,
left: `${startPos.x}%`,
top: `${startPos.y}%`,
transition: { duration: 0.25, ease: [0.4, 0, 1, 1] },
}}
transition={{
left: { duration: 0.5, ease: [0.22, 1, 0.36, 1] },
top: { duration: 0.5, ease: [0.22, 1, 0.36, 1] },
opacity: { duration: 0.15 },
}}
className="absolute z-[101] pointer-events-none"
initial={dot.initial}
animate={dot.animate}
exit={dot.exit}
transition={dot.transition}
className="absolute pointer-events-none"
style={{ zIndex: laserV1.zIndex }}
>
<div className="relative -translate-x-1/2 -translate-y-1/2">
<div className="relative" style={dot.staticProps}>
{/* Ring pulse */}
<motion.div
animate={{ scale: [1, 2.8], opacity: [0.6, 0] }}
transition={{
repeat: Infinity,
duration: 1.5,
ease: 'easeOut',
repeatDelay: 0.3,
}}
className="absolute inset-0 rounded-full"
style={{ border: `1.5px solid ${color}` }}
initial={ring.initial}
animate={ring.animate}
transition={ring.transition}
style={ring.staticProps}
/>

{/* Light core */}
<div
className="w-2.5 h-2.5 rounded-full"
style={{
backgroundColor: color,
boxShadow: `0 0 8px 2px ${color}60`,
}}
/>
<div style={core.staticProps} />
</div>
</motion.div>
);
Expand Down
84 changes: 28 additions & 56 deletions components/slide-renderer/Editor/SpotlightOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import { useRef, useState, useLayoutEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { spotlightV1 } from '@/lib/choreography';
import { useSceneSelector } from '@/lib/contexts/scene-context';
import { useCanvasStore } from '@/lib/store/canvas';
import type { SlideContent } from '@/lib/types/stage';
import type { PPTElement } from '@openmaic/dsl';
import { resolveMotionLayer } from './motion-descriptor-adapter';

interface SpotlightRect {
x: number;
Expand Down Expand Up @@ -80,20 +82,31 @@ export function SpotlightOverlay({ domIdPrefix = 'screen-element-' }: SpotlightO
}, [measure, elements]);

const active = !!spotlightElementId && !!spotlightOptions && !!rect;
const dimness = spotlightOptions?.dimness ?? 0.7;
const resolvedLayers = rect
? {
dim: resolveMotionLayer(spotlightV1, 'dim', {
geometry: rect,
params: { dimness: spotlightOptions?.dimness },
}),
cutout: resolveMotionLayer(spotlightV1, 'cutout', { geometry: rect }),
border: resolveMotionLayer(spotlightV1, 'border', { geometry: rect }),
}
: null;

return (
<div
ref={containerRef}
className="absolute inset-0 z-[100] pointer-events-none overflow-hidden"
className="absolute inset-0 pointer-events-none overflow-hidden"
style={{ zIndex: spotlightV1.zIndex }}
>
<AnimatePresence mode="wait">
{active && rect && (
{active && rect && resolvedLayers && (
<motion.div
key={`spotlight-${spotlightElementId}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
initial={resolvedLayers.dim.initial}
animate={resolvedLayers.dim.animate}
exit={resolvedLayers.dim.exit}
transition={resolvedLayers.dim.transition}
className="absolute inset-0"
>
<svg
Expand All @@ -109,25 +122,10 @@ export function SpotlightOverlay({ domIdPrefix = 'screen-element-' }: SpotlightO
<rect x="0" y="0" width="100" height="100" fill="white" />
{/* Black rectangle = hide mask layer (highlighted area / cutout) */}
<motion.rect
fill="black"
initial={{
x: rect.x - 8,
y: rect.y - 8,
width: rect.w + 16,
height: rect.h + 16,
rx: 4,
}}
animate={{
x: rect.x - 0.4,
y: rect.y - 0.6,
width: rect.w + 0.8,
height: rect.h + 1.2,
rx: 1,
}}
transition={{
duration: 0.6,
ease: [0.16, 1, 0.3, 1],
}}
{...resolvedLayers.cutout.staticProps}
initial={resolvedLayers.cutout.initial}
animate={resolvedLayers.cutout.animate}
transition={resolvedLayers.cutout.transition}
/>
</mask>
</defs>
Expand All @@ -138,40 +136,14 @@ export function SpotlightOverlay({ domIdPrefix = 'screen-element-' }: SpotlightO
Tailwind 3 silently dropped `backdrop-blur-[1.5px]` on SVG via
--tw-* variables; Tailwind 4 emits the property directly and
surfaced the bug. */}
<rect
width="100"
height="100"
fill={`rgba(0,0,0,${dimness})`}
mask={`url(#mask-${spotlightElementId})`}
/>
<rect {...resolvedLayers.dim.staticProps} mask={`url(#mask-${spotlightElementId})`} />

{/* THE ONE BORDER - white border */}
<motion.rect
initial={{
x: rect.x - 4,
y: rect.y - 4,
width: rect.w + 8,
height: rect.h + 8,
opacity: 0,
rx: 2,
}}
animate={{
x: rect.x - 0.4,
y: rect.y - 0.6,
width: rect.w + 0.8,
height: rect.h + 1.2,
opacity: 1,
rx: 1,
}}
fill="none"
stroke="rgba(255,255,255,0.7)"
strokeWidth="1.2"
style={{ vectorEffect: 'non-scaling-stroke' } as React.CSSProperties}
transition={{
duration: 0.5,
delay: 0.05,
ease: [0.16, 1, 0.3, 1],
}}
{...resolvedLayers.border.staticProps}
initial={resolvedLayers.border.initial}
animate={resolvedLayers.border.animate}
transition={resolvedLayers.border.transition}
/>
</svg>
</motion.div>
Expand Down
Loading
Loading