// Real WebGL 3D (never CSS fake 3D). Lighting + materials come from tokens.
import React, {useEffect, useMemo} from 'react';
import {interpolate, useCurrentFrame} from 'remotion';
import {ThreeCanvas} from '@remotion/three';
import {useFrame, useThree} from '@react-three/fiber';
import {EffectComposer} from 'three/examples/jsm/postprocessing/EffectComposer.js';
import {RenderPass} from 'three/examples/jsm/postprocessing/RenderPass.js';
import {BokehPass} from 'three/examples/jsm/postprocessing/BokehPass.js';
import * as THREE from 'three';
import {RoomEnvironment} from 'three/examples/jsm/environments/RoomEnvironment.js';
import {SVGLoader} from 'three/examples/jsm/loaders/SVGLoader.js';
import {RoundedBoxGeometry} from 'three/examples/jsm/geometries/RoundedBoxGeometry.js';
import {S} from '../lib/tokens';
import {cl, spr} from '../lib/anim';
import {beatAccent} from '../lib/beat';
const L = S.three;
const Env: React.FC = () => { const {gl, scene} = useThree(); useEffect(() => { gl.outputColorSpace = THREE.SRGBColorSpace; gl.toneMapping = THREE.NoToneMapping; const pm = new THREE.PMREMGenerator(gl); scene.environment = pm.fromScene(new RoomEnvironment(), 0.04).texture; }, [gl, scene]); return null; };
export const Lights: React.FC = () => <>>;
export type Finish = 'ceramic' | 'glass' | 'metal';
export const Mat: React.FC<{color: string; finish?: Finish}> = ({color, finish = 'ceramic'}) => {
const m = L.finishes[finish];
return ;
};
// Optional depth-aware optical blur. BokehPass renders a depth buffer then
// composites a focus plane; only the 3D canvas is affected, never UI text.
export const DepthOfField:React.FC<{enabled?:boolean}> = ({enabled = L.depthOfField.enabled}) => {
const {gl, scene, camera, size} = useThree();
const config = L.depthOfField;
const composer = useMemo(() => {
if (!enabled) return null;
const c = new EffectComposer(gl);
c.addPass(new RenderPass(scene, camera));
const b = new BokehPass(scene, camera, {focus:config.focus, aperture:config.aperture, maxblur:config.maxBlur});
// Three's stock shader forces output alpha to 1, which turns every
// transparent 3D canvas into a black rectangle over the DOM. Keep sampled
// alpha so the transparent canvas can be composited normally.
b.materialBokeh.fragmentShader=b.materialBokeh.fragmentShader.replace('gl_FragColor.a = 1.0;', 'gl_FragColor.a = col.a / 41.0;');
b.materialBokeh.needsUpdate=true;
b.renderToScreen = true;c.addPass(b);
return c;
}, [gl, scene, camera, enabled]);
useEffect(() => {composer?.setSize(size.width,size.height);return () => composer?.dispose();}, [composer,size.width,size.height]);
useFrame(() => {composer?.render();}, enabled ? 1 : 0);
return null;
};
// Real perspective-camera motion, separate from Stage.Camera's 2D framing.
// Keys are [frame, dolly distance, orbit radians, target x, target y].
export type Camera3DKey = [number, number, number, number, number];
export const camera3DAt = (frame: number, keys: Camera3DKey[]) => {
const sorted = [...keys].sort((a, b) => a[0] - b[0]);
if (!sorted.length) throw new Error('Camera3D needs at least one key');
const a = sorted.findIndex((k) => k[0] >= frame);
if (a === -1) return sorted[sorted.length - 1].slice(1);
if (a === 0) return sorted[0].slice(1);
const lo = sorted[a - 1], hi = sorted[a];
const t = interpolate(frame, [lo[0], hi[0]], [0, 1], {...cl, easing: (x) => x * x * (3 - 2 * x)});
return lo.slice(1).map((v, i) => v + (hi[i + 1] - v) * t);
};
export const Camera3D: React.FC<{keys: Camera3DKey[]}> = ({keys}) => {
const frame = useCurrentFrame(); const {camera} = useThree();
const [distance, orbit, tx, ty] = camera3DAt(frame, keys);
camera.position.set(tx + Math.sin(orbit) * distance, ty, Math.cos(orbit) * distance);
camera.lookAt(tx, ty, 0);
camera.updateProjectionMatrix();
return null;
};
const cameraKeys = (base: number, at: number, move?: Camera3DKey[]): Camera3DKey[] =>
move ?? (S.three.camera.moves as Camera3DKey[]).map(([f, dolly, orbit, x, y]) => [at + f, base * dolly, orbit, x, y]);
// Extruded logo from an SVG path string (use the brand's REAL logo path).
export const Logo3D: React.FC<{svg: string; at: number; size?: number; depth?: number; color?: string; spin?: number; cameraMove?: Camera3DKey[]; finish?: Finish}> = ({svg, at, size = 520, depth = 100, color = S.color.object3d[0], spin = 1, cameraMove, finish = 'ceramic'}) => {
const f = useCurrentFrame(); const s = spr(f, at, S.motion.logo3d.spring);
const geo = useMemo(() => {
const data = new SVGLoader().parse(svg); const shapes = data.paths.flatMap((p) => SVGLoader.createShapes(p));
const g = new THREE.ExtrudeGeometry(shapes, {depth, bevelEnabled: true, bevelThickness: 16, bevelSize: 14, bevelSegments: 9, curveSegments: 24});
g.center(); g.computeBoundingBox(); const bb = g.boundingBox!; const k = 900 / Math.max(bb.max.x - bb.min.x, bb.max.y - bb.min.y); g.scale(k, -k, k); return g;
}, [svg, depth]);
const ry = (1 - s) * Math.PI * 1.2 * spin + Math.sin((f - at) / 40) * 0.12; const fl = Math.sin((f - at) / 26) * 14;
return
;
};
// Mochi mascot: soft white body, glossy eyes, blush. Bounces + blinks. happy = smiling eyes (end card).
const MochiMesh: React.FC<{blink: number; squash: number; happy?: boolean}> = ({blink, squash, happy}) => {
const body = useMemo(() => { const g = new THREE.SphereGeometry(200, 96, 64); const p = g.attributes.position;
for (let i = 0; i < p.count; i++) { const x = p.getX(i); let y = p.getY(i); const z = p.getZ(i); if (y < -110) y = -110 - (y + 110) * 0.35; p.setXYZ(i, x * 1.1, y * 0.92 + 20, z * 0.95); }
g.computeVertexNormals(); return g; }, []);
return
{[-1, 1].map((sd) =>
{happy ?
: }
)}
;
};
export const Mascot: React.FC<{at: number; size?: number; happy?: boolean; cameraMove?: Camera3DKey[]}> = ({at, size = 260, happy, cameraMove}) => {
const f = useCurrentFrame(); const s = spr(f, at, {damping: 9, stiffness: 150, mass: 0.8});
const bounce = Math.abs(Math.sin((f - at) / 11)) * 0.5; const blink = interpolate((f - at) % 75, [60, 63, 66], [0, 1, 0], cl);
return
;
};
// Field of soft pastel objects floating in (hook/opening). Deterministic positions.
export const FloatingShapes: React.FC<{at: number; count?: number; w?: number; h?: number; clearX?: number; clearY?: number; cameraMove?: Camera3DKey[]; finishes?: Finish[]; beats?: number[]}> = ({at, count = 14, w = 1920, h = 1080, clearX = 950, clearY = 430, cameraMove, finishes = ['ceramic', 'metal', 'glass'], beats = []}) => {
const f = useCurrentFrame(); const cols = S.color.object3d;
// Objects stay OUT of the central text area (ellipse clearX x clearY) so they never sit on the headline.
const items = useMemo(() => Array.from({length: count}, (_, i) => { const a = (i * 137.5) * Math.PI / 180, r = 1 + (i % 4) * 0.28;
const x = Math.cos(a) * clearX * r, y = Math.sin(a) * clearY * r; return {x, y, z: -200 - (i % 5) * 120, kind: i % 4, c: cols[i % 4], d: i * 2}; }), [count, cols, clearX, clearY]);
const rb = useMemo(() => new RoundedBoxGeometry(150, 150, 150, 6, 26), []);
return
{items.map((it, i) => { const s = spr(f, at + it.d, {damping: 12, stiffness: 90, mass: 1}); const t = (f - at) / 30;
const finish = finishes[i % finishes.length] ?? 'ceramic';
return
{it.kind === 0 && }
{it.kind === 1 && }
{it.kind === 2 && }
{it.kind === 3 && }
; })}
;
};