Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Successful in 1m22s
Build & Deploy / 🏗️ Build (push) Failing after 1m0s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Smoke Test (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
- Added 'use client' to not-found.tsx - Refactored RelatedProducts to Server Component to fix 'fs' import error - Created RelatedProductLink for client-side analytics - Fixed lint syntax issues in RecordModeVisuals.tsx - Fixed rule-of-hooks violation in WebsiteVideo.tsx
128 lines
3.7 KiB
TypeScript
128 lines
3.7 KiB
TypeScript
import React, { useMemo } from 'react';
|
|
import {
|
|
AbsoluteFill,
|
|
useVideoConfig,
|
|
useCurrentFrame,
|
|
interpolate,
|
|
spring,
|
|
Easing,
|
|
} from 'remotion';
|
|
import { RecordingSession, RecordEvent } from '../types/record-mode';
|
|
|
|
export const WebsiteVideo: React.FC<{
|
|
session: RecordingSession | null;
|
|
siteUrl: string;
|
|
}> = ({ session, siteUrl }) => {
|
|
const { fps, width, height, durationInFrames } = useVideoConfig();
|
|
const frame = useCurrentFrame();
|
|
|
|
const sortedEvents = useMemo(() => {
|
|
if (!session) return [];
|
|
return [...session.events].sort((a, b) => a.timestamp - b.timestamp);
|
|
}, [session]);
|
|
|
|
if (!session || !session.events.length) {
|
|
return (
|
|
<AbsoluteFill
|
|
style={{
|
|
backgroundColor: 'black',
|
|
color: 'white',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
No session data found.
|
|
</AbsoluteFill>
|
|
);
|
|
}
|
|
|
|
const elapsedTimeMs = (frame / fps) * 1000;
|
|
|
|
// --- Interpolation Logic ---
|
|
|
|
// 1. Find the current window (between which two events are we?)
|
|
const nextEventIndex = sortedEvents.findIndex((e) => e.timestamp > elapsedTimeMs);
|
|
let currentEventIndex;
|
|
|
|
if (nextEventIndex === -1) {
|
|
// We are past the last event, stay at the end
|
|
currentEventIndex = sortedEvents.length - 1;
|
|
} else {
|
|
currentEventIndex = Math.max(0, nextEventIndex - 1);
|
|
}
|
|
|
|
const currentEvent = sortedEvents[currentEventIndex];
|
|
// If there is no next event, we just stay at current (next=current)
|
|
const nextEvent = nextEventIndex !== -1 ? sortedEvents[nextEventIndex] : currentEvent;
|
|
|
|
// 2. Calculate Progress between events
|
|
const gap = nextEvent.timestamp - currentEvent.timestamp;
|
|
const progress = gap > 0 ? (elapsedTimeMs - currentEvent.timestamp) / gap : 1;
|
|
const easedProgress = Easing.cubic(Math.min(Math.max(progress, 0), 1));
|
|
|
|
// 3. Calculate Cursor Position from Rects
|
|
const getCenter = (event: RecordEvent) => {
|
|
if (event.rect) {
|
|
return {
|
|
x: event.rect.x + event.rect.width / 2,
|
|
y: event.rect.y + event.rect.height / 2,
|
|
};
|
|
}
|
|
return { x: width / 2, y: height / 2 };
|
|
};
|
|
|
|
const p1 = getCenter(currentEvent);
|
|
const p2 = getCenter(nextEvent);
|
|
|
|
const cursorX = interpolate(easedProgress, [0, 1], [p1.x, p2.x]);
|
|
const cursorY = interpolate(easedProgress, [0, 1], [p1.y, p2.y]);
|
|
|
|
// 4. Zoom & Blur
|
|
const zoom = interpolate(easedProgress, [0, 1], [currentEvent.zoom || 1, nextEvent.zoom || 1]);
|
|
const isBlurry = currentEvent.motionBlur || nextEvent.motionBlur;
|
|
|
|
return (
|
|
<AbsoluteFill style={{ backgroundColor: '#000' }}>
|
|
<div
|
|
style={{
|
|
width: '100%',
|
|
height: '100%',
|
|
position: 'relative',
|
|
transform: `scale(${zoom})`,
|
|
transformOrigin: `${cursorX}px ${cursorY}px`,
|
|
filter: isBlurry ? 'blur(8px)' : 'none',
|
|
transition: 'filter 0.1s ease-out',
|
|
}}
|
|
>
|
|
<iframe
|
|
src={siteUrl}
|
|
style={{ width: '100%', height: '100%', border: 'none' }}
|
|
title="Website"
|
|
/>
|
|
</div>
|
|
|
|
{/* Visual Cursor */}
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
left: cursorX,
|
|
top: cursorY,
|
|
width: 34,
|
|
height: 34,
|
|
backgroundColor: 'white',
|
|
borderRadius: '50%',
|
|
border: '3px solid black',
|
|
boxShadow: '0 4px 15px rgba(0,0,0,0.4)',
|
|
transform: 'translate(-50%, -50%)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
zIndex: 100,
|
|
}}
|
|
>
|
|
<div style={{ width: 12, height: 12, backgroundColor: '#3b82f6', borderRadius: '50%' }} />
|
|
</div>
|
|
</AbsoluteFill>
|
|
);
|
|
};
|