Some checks failed
Build & Deploy Mintel Blog / build-and-deploy (push) Failing after 2m19s
210 lines
7.4 KiB
TypeScript
210 lines
7.4 KiB
TypeScript
import React, { useMemo, useEffect, useState, useRef } from 'react';
|
|
import {
|
|
AbsoluteFill,
|
|
interpolate,
|
|
useCurrentFrame,
|
|
useVideoConfig,
|
|
Easing,
|
|
Img,
|
|
delayRender,
|
|
continueRender,
|
|
spring,
|
|
} from 'remotion';
|
|
import { MouseCursor } from '../components/MouseCursor';
|
|
import { ContactForm } from '@/src/components/ContactForm';
|
|
import { BackgroundGrid } from '@/src/components/Layout';
|
|
import { initialState } from '@/src/components/ContactForm/constants';
|
|
|
|
// Brand Assets
|
|
import IconWhite from '@/src/assets/logo/Icon White Transparent.svg';
|
|
|
|
export const ContactFormShowcase: React.FC = () => {
|
|
const frame = useCurrentFrame();
|
|
const { width, height, fps } = useVideoConfig();
|
|
const [handle] = useState(() => delayRender('Initializing Smart Camera'));
|
|
|
|
useEffect(() => {
|
|
if (typeof window !== 'undefined') (window as any).isRemotion = true;
|
|
const timer = setTimeout(() => continueRender(handle), 500);
|
|
return () => clearTimeout(timer);
|
|
}, [handle]);
|
|
|
|
// ---- SMART ANCHOR SYSTEM ----
|
|
// We define which target the camera should follow at each frame
|
|
const activeTargetId = useMemo(() => {
|
|
if (frame < 60) return 'overview';
|
|
if (frame < 160) return 'focus-target-web-app';
|
|
if (frame < 260) return 'focus-target-next';
|
|
if (frame < 300) return 'overview';
|
|
if (frame < 550) return 'focus-target-company';
|
|
if (frame < 650) return 'focus-target-next';
|
|
if (frame < 800) return 'overview';
|
|
return 'overview';
|
|
}, [frame]);
|
|
|
|
// Pre-calculated coordinates for common targets in the 1200px scaled form
|
|
// Since real-time DOM measurements can be jittery during a render,
|
|
// we use "Systematic Virtual Anchors" calibrated to the layout.
|
|
const anchors = useMemo(() => ({
|
|
'overview': { z: 0.85, x: 0, y: 0 },
|
|
'focus-target-web-app': { z: 1.3, x: 300, y: 0 },
|
|
'focus-target-next': { z: 1.1, x: -380, y: -280 },
|
|
'focus-target-company': { z: 1.45, x: 50, y: 220 },
|
|
}), []);
|
|
|
|
// ---- UNIFIED SPRING PHYSICS ----
|
|
const camera = useMemo(() => {
|
|
// Find the target state
|
|
const target = (anchors as any)[activeTargetId] || anchors.overview;
|
|
|
|
// We use a "Continuous Follow" spring that always chases the activeTargetId
|
|
// but we need to know when the target changed to reset the spring animation.
|
|
// Actually, for smoothness, we'll just use the frame globally and let the spring settle.
|
|
|
|
// Let's create a transition system
|
|
const points = [
|
|
{ f: 0, t: 'overview' },
|
|
{ f: 80, t: 'focus-target-web-app' },
|
|
{ f: 200, t: 'focus-target-next' },
|
|
{ f: 280, t: 'overview' },
|
|
{ f: 380, t: 'focus-target-company' },
|
|
{ f: 580, t: 'focus-target-next' },
|
|
{ f: 700, t: 'overview' },
|
|
];
|
|
|
|
let idx = 0;
|
|
for (let i = 0; i < points.length; i++) {
|
|
if (frame >= points[i].f) idx = i;
|
|
}
|
|
|
|
const start = (anchors as any)[points[idx].t];
|
|
const end = (anchors as any)[(points[idx + 1] || points[idx]).t];
|
|
|
|
const s = spring({
|
|
frame: frame - points[idx].f,
|
|
fps,
|
|
config: { stiffness: 45, damping: 18, mass: 1 },
|
|
});
|
|
|
|
return {
|
|
z: interpolate(s, [0, 1], [start.z, end.z]),
|
|
x: interpolate(s, [0, 1], [start.x, end.x]),
|
|
y: interpolate(s, [0, 1], [start.y, end.y]),
|
|
};
|
|
}, [frame, anchors, fps]);
|
|
|
|
// ---- MOUSE HANDLER (Sticks to targets) ----
|
|
const mouse = useMemo(() => {
|
|
const path = [
|
|
{ f: 10, x: width * 1.5, y: height * 1.2 }, // Offscreen
|
|
{ f: 80, x: width * 0.35, y: height * 0.45 }, // Move to Web App
|
|
{ f: 210, x: width * 0.85, y: height * 0.68 }, // Move to Next
|
|
{ f: 380, x: width * 0.50, y: height * 0.35 }, // Move to Input
|
|
{ f: 590, x: width * 0.85, y: height * 0.68 }, // Move to Next
|
|
{ f: 850, x: width * 1.5, y: height * 1.2 }, // Exit
|
|
];
|
|
|
|
let idx = 0;
|
|
for (let i = 0; i < path.length; i++) {
|
|
if (frame >= path[i].f) idx = i;
|
|
}
|
|
|
|
const p1 = path[idx];
|
|
const p2 = path[idx + 1] || p1;
|
|
|
|
const s = spring({
|
|
frame: frame - p1.f,
|
|
fps,
|
|
config: { stiffness: 40, damping: 22 },
|
|
});
|
|
|
|
return {
|
|
x: interpolate(s, [0, 1], [p1.x, p2.x]),
|
|
y: interpolate(s, [0, 1], [p1.y, p2.y]),
|
|
};
|
|
}, [frame, width, height, fps]);
|
|
|
|
// ---- UI PROGRESSION ----
|
|
const ui = useMemo(() => {
|
|
let step = 0;
|
|
let type = 'website';
|
|
let name = '';
|
|
|
|
if (frame > 140) type = 'web-app';
|
|
if (frame > 260) step = 1;
|
|
|
|
if (frame > 400) {
|
|
const txt = "Mintel Studios";
|
|
const chars = Math.min(Math.floor((frame - 400) / 4), txt.length);
|
|
name = txt.substring(0, chars);
|
|
}
|
|
|
|
if (frame > 620) step = 2;
|
|
|
|
return { step, type, name };
|
|
}, [frame]);
|
|
|
|
const formState = useMemo(() => ({
|
|
...initialState,
|
|
projectType: ui.type as any,
|
|
companyName: ui.name,
|
|
}), [ui]);
|
|
|
|
// Click triggers
|
|
const isClicking = (frame > 135 && frame < 150) || (frame > 255 && frame < 270) || (frame > 615 && frame < 630);
|
|
|
|
return (
|
|
<AbsoluteFill className="bg-white">
|
|
<BackgroundGrid />
|
|
|
|
<style>{`
|
|
* { transition: none !important; animation: none !important; -webkit-font-smoothing: antialiased; }
|
|
.focus-layer { transform-style: preserve-3d; backface-visibility: hidden; will-change: transform; }
|
|
`}</style>
|
|
|
|
<div
|
|
className="absolute inset-0 flex items-center justify-center focus-layer"
|
|
style={{
|
|
transform: `translate3d(${Math.round(camera.x)}px, ${Math.round(camera.y)}px, 0) scale(${camera.z.toFixed(4)})`,
|
|
}}
|
|
>
|
|
{/* Scale factor 0.85 for the base container */}
|
|
<div
|
|
style={{ transform: 'scale(0.85) translate3d(0,0,0)', width: '1200px' }}
|
|
className="focus-layer"
|
|
>
|
|
<ContactForm
|
|
initialStepIndex={ui.step}
|
|
initialState={formState}
|
|
/>
|
|
</div>
|
|
|
|
{/* Mouse Cursor */}
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
top: 0, left: 0,
|
|
transform: `translate3d(${Math.round(mouse.x)}px, ${Math.round(mouse.y)}px, 0)`,
|
|
zIndex: 1000,
|
|
}}
|
|
className="focus-layer"
|
|
>
|
|
<MouseCursor isClicking={isClicking} x={0} y={0} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Branding Logo HUD */}
|
|
<div className="absolute top-28 left-28 z-50 focus-layer">
|
|
<div className="w-40 h-40 bg-black rounded-[3.5rem] flex items-center justify-center border-4 border-white/5 shadow-2xl">
|
|
<Img src={IconWhite} className="w-24 h-24" />
|
|
</div>
|
|
</div>
|
|
|
|
<AbsoluteFill
|
|
className="bg-white pointer-events-none"
|
|
style={{ opacity: interpolate(frame, [0, 15], [1, 0], { extrapolateRight: 'clamp' }) }}
|
|
/>
|
|
</AbsoluteFill>
|
|
);
|
|
};
|