// 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 && } ; })} ; };