import React, { useMemo } from 'react'; import { useCurrentFrame, useVideoConfig, interpolate, Easing, AbsoluteFill, } from 'remotion'; import * as THREE from 'three'; // ============================================================================= // COMPOSITION CONFIG // ============================================================================= export const compositionConfig = { id: 'SailboatOnCalmWaters', durationInSeconds: 5, fps: 60, width: 1920, height: 1080, }; // ============================================================================= // STYLE CONSTANTS // ============================================================================= const COLORS = { skyTop: '#ffb366', skyHorizon: '#ffecd2', waterDeep: '#1a5276', waterSurface: '#85c1e9', hullWood: '#8b4513', hullDark: '#5d3a1a', deckWood: '#deb887', sailWhite: '#fff8f0', mastWood: '#654321', sunGlow: '#ffdd99', fogColor: '#ffecd2', seagullWhite: '#ffffff', background: '#000000', } as const; const TYPOGRAPHY = { fontFamily: 'Inter, system-ui, sans-serif', } as const; // ============================================================================= // PRE-GENERATED DATA (computed once, NOT during render) // ============================================================================= const seededRandom = (seed: number): number => { const x = Math.sin(seed * 9999) * 10000; return x - Math.floor(x); }; const generateSceneData = () => { const waves = Array.from({ length: 8 }, (_, i) => ({ x: seededRandom(i * 1.1) * 2 - 1, z: seededRandom(i * 2.2) * 2 - 1, amplitude: 0.02 + seededRandom(i * 3.3) * 0.03, frequency: 0.5 + seededRandom(i * 4.4) * 1.5, phase: seededRandom(i * 5.5) * Math.PI * 2, })); const wakeParticles = Array.from({ length: 40 }, (_, i) => ({ offsetX: seededRandom(i * 10) * 8, offsetZ: (seededRandom(i * 20) - 0.5) * 2, size: 0.1 + seededRandom(i * 30) * 0.15, delay: seededRandom(i * 40) * 2, side: seededRandom(i * 50) > 0.5 ? 1 : -1, })); const seagulls = Array.from({ length: 5 }, (_, i) => ({ baseX: -5 + seededRandom(i * 100) * 10, baseY: 8 + seededRandom(i * 200) * 4, baseZ: -8 + seededRandom(i * 300) * 16, orbitRadius: 3 + seededRandom(i * 400) * 4, orbitSpeed: 0.3 + seededRandom(i * 500) * 0.3, flapSpeed: 4 + seededRandom(i * 600) * 2, phase: seededRandom(i * 700) * Math.PI * 2, })); const clouds = Array.from({ length: 8 }, (_, i) => ({ x: seededRandom(i * 1000) * 100 - 50, y: 25 + seededRandom(i * 2000) * 15, z: -40 - seededRandom(i * 3000) * 30, scale: 2 + seededRandom(i * 4000) * 3, speed: 0.5 + seededRandom(i * 5000) * 0.5, })); const glints = Array.from({ length: 30 }, (_, i) => ({ x: seededRandom(i * 1111) * 40 - 20, z: seededRandom(i * 2222) * 30 - 15, phase: seededRandom(i * 3333) * Math.PI * 2, speed: 2 + seededRandom(i * 4444) * 2, })); return { waves, wakeParticles, seagulls, clouds, glints }; }; const SCENE_DATA = generateSceneData(); const EASINGS = { gentle: Easing.bezier(0.25, 0.1, 0.25, 1), smooth: Easing.bezier(0.4, 0, 0.2, 1), }; // ============================================================================= // THREE.JS SCENE COMPONENT // ============================================================================= const ThreeScene: React.FC<{ frame: number; fps: number; durationInFrames: number; }> = ({ frame, fps, durationInFrames }) => { const canvasRef = React.useRef(null); const time = frame / fps; const sceneData = useMemo(() => SCENE_DATA, []); React.useEffect(() => { if (!canvasRef.current) return; const canvas = canvasRef.current; const { width, height } = compositionConfig; // Renderer const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: 'high-performance', }); renderer.setSize(width, height); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.2; // Scene const scene = new THREE.Scene(); scene.fog = new THREE.FogExp2(0xffecd2, 0.012); // Camera const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 500); const boatX = interpolate(time, [0, 5], [-8, 12], { easing: EASINGS.gentle, extrapolateRight: 'clamp', }); const cameraOffsetX = interpolate(time, [0, 5], [8, 6], { easing: EASINGS.smooth, extrapolateRight: 'clamp', }); const cameraHeight = interpolate(time, [0, 2, 5], [4, 3.5, 3.8], { easing: EASINGS.gentle, extrapolateRight: 'clamp', }); const cameraSway = Math.sin(time * 0.8) * 0.15; const cameraRoll = Math.sin(time * 0.5) * 0.02; camera.position.set( boatX + cameraOffsetX, cameraHeight + cameraSway * 0.3, 10 + Math.sin(time * 0.3) * 0.5 ); camera.lookAt(boatX - 2, 1.5 + cameraSway, 0); camera.rotation.z = cameraRoll; // ------------------------------------------------------------------------- // LIGHTING // ------------------------------------------------------------------------- const ambientLight = new THREE.AmbientLight(0xffeedd, 0.4); scene.add(ambientLight); const sunLight = new THREE.DirectionalLight(0xffcc88, 1.4); sunLight.position.set(-50, 15, -30); sunLight.castShadow = true; sunLight.shadow.mapSize.width = 2048; sunLight.shadow.mapSize.height = 2048; sunLight.shadow.camera.near = 1; sunLight.shadow.camera.far = 150; sunLight.shadow.camera.left = -30; sunLight.shadow.camera.right = 30; sunLight.shadow.camera.top = 30; sunLight.shadow.camera.bottom = -30; sunLight.shadow.bias = -0.0002; scene.add(sunLight); const fillLight = new THREE.HemisphereLight(0x87ceeb, 0x3d6b8a, 0.35); scene.add(fillLight); const rimLight = new THREE.DirectionalLight(0xffffff, 0.25); rimLight.position.set(20, 10, 20); scene.add(rimLight); // ------------------------------------------------------------------------- // SKY DOME // ------------------------------------------------------------------------- const skyGeo = new THREE.SphereGeometry(150, 32, 32); const skyMat = new THREE.ShaderMaterial({ uniforms: { topColor: { value: new THREE.Color(0xffb366) }, horizonColor: { value: new THREE.Color(0xffecd2) }, sunPosition: { value: new THREE.Vector3(-50, 15, -30).normalize() }, sunColor: { value: new THREE.Color(0xffdd99) }, }, vertexShader: ` varying vec3 vWorldPosition; void main() { vec4 worldPosition = modelMatrix * vec4(position, 1.0); vWorldPosition = worldPosition.xyz; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform vec3 topColor; uniform vec3 horizonColor; uniform vec3 sunPosition; uniform vec3 sunColor; varying vec3 vWorldPosition; void main() { vec3 dir = normalize(vWorldPosition); float h = dir.y; vec3 skyColor = mix(horizonColor, topColor, pow(max(h, 0.0), 0.4)); float sunDot = max(dot(dir, sunPosition), 0.0); float sunGlow = pow(sunDot, 8.0) * 0.6; float sunCore = pow(sunDot, 64.0) * 1.5; skyColor += sunColor * sunGlow; skyColor += vec3(1.0, 0.95, 0.8) * sunCore; gl_FragColor = vec4(skyColor, 1.0); } `, side: THREE.BackSide, }); scene.add(new THREE.Mesh(skyGeo, skyMat)); // ------------------------------------------------------------------------- // WATER // ------------------------------------------------------------------------- const waterGeometry = new THREE.PlaneGeometry(200, 200, 128, 128); const waterPositions = waterGeometry.attributes.position; for (let i = 0; i < waterPositions.count; i++) { const x = waterPositions.getX(i); const z = waterPositions.getY(i); let waveHeight = 0; sceneData.waves.forEach((wave) => { const dist = Math.sqrt( Math.pow(x * 0.1 - wave.x, 2) + Math.pow(z * 0.1 - wave.z, 2) ); waveHeight += wave.amplitude * Math.sin(dist * wave.frequency * 5 + time * 2 + wave.phase); }); waterPositions.setZ(i, waveHeight); } waterPositions.needsUpdate = true; waterGeometry.computeVertexNormals(); const waterMaterial = new THREE.MeshStandardMaterial({ color: 0x85c1e9, roughness: 0.15, metalness: 0.3, transparent: true, opacity: 0.92, }); const water = new THREE.Mesh(waterGeometry, waterMaterial); water.rotation.x = -Math.PI / 2; water.position.y = 0; water.receiveShadow = true; scene.add(water); const deepWaterGeo = new THREE.PlaneGeometry(200, 200); const deepWaterMat = new THREE.MeshBasicMaterial({ color: 0x1a5276 }); const deepWater = new THREE.Mesh(deepWaterGeo, deepWaterMat); deepWater.rotation.x = -Math.PI / 2; deepWater.position.y = -2; scene.add(deepWater); // ------------------------------------------------------------------------- // SAILBOAT // ------------------------------------------------------------------------- const boatGroup = new THREE.Group(); const rockAngle = Math.sin(time * 1.2) * 0.03; const pitchAngle = Math.sin(time * 0.8 + 0.5) * 0.02; const boatY = Math.sin(time * 1.5) * 0.05; boatGroup.rotation.z = rockAngle; boatGroup.rotation.x = pitchAngle; boatGroup.position.set(boatX, boatY, 0); // Hull const hullShape = new THREE.Shape(); hullShape.moveTo(-2, 0); hullShape.quadraticCurveTo(-2.2, 0.4, -1.8, 0.7); hullShape.lineTo(2.5, 0.7); hullShape.quadraticCurveTo(3, 0.5, 2.8, 0); hullShape.lineTo(-2, 0); const hullGeometry = new THREE.ExtrudeGeometry(hullShape, { steps: 1, depth: 1.2, bevelEnabled: true, bevelThickness: 0.1, bevelSize: 0.1, bevelSegments: 3, }); const hullMaterial = new THREE.MeshStandardMaterial({ color: 0x8b4513, roughness: 0.7, metalness: 0.1, }); const hull = new THREE.Mesh(hullGeometry, hullMaterial); hull.position.set(0, -0.3, -0.6); hull.castShadow = true; hull.receiveShadow = true; boatGroup.add(hull); // Keel const keelGeometry = new THREE.BoxGeometry(4, 0.3, 0.8); const keelMaterial = new THREE.MeshStandardMaterial({ color: 0x5d3a1a, roughness: 0.8, }); const keel = new THREE.Mesh(keelGeometry, keelMaterial); keel.position.set(0.3, -0.4, 0); keel.castShadow = true; boatGroup.add(keel); // Deck const deckGeometry = new THREE.BoxGeometry(4.2, 0.1, 1.3); const deckMaterial = new THREE.MeshStandardMaterial({ color: 0xdeb887, roughness: 0.6, }); const deck = new THREE.Mesh(deckGeometry, deckMaterial); deck.position.set(0.2, 0.45, 0); deck.castShadow = true; deck.receiveShadow = true; boatGroup.add(deck); // Mast const mastGeometry = new THREE.CylinderGeometry(0.06, 0.08, 5, 12); const mastMaterial = new THREE.MeshStandardMaterial({ color: 0x654321, roughness: 0.5, }); const mast = new THREE.Mesh(mastGeometry, mastMaterial); mast.position.set(0, 2.9, 0); mast.castShadow = true; boatGroup.add(mast); // Boom const boomGeometry = new THREE.CylinderGeometry(0.03, 0.04, 2.8, 8); const boom = new THREE.Mesh(boomGeometry, mastMaterial); boom.rotation.z = Math.PI / 2; boom.position.set(1.2, 0.8, 0); boom.castShadow = true; boatGroup.add(boom); // Main Sail const sailBillow = 0.3 + Math.sin(time * 2) * 0.05; const sailGeometry = new THREE.BufferGeometry(); const sailVertices: number[] = []; const sailSegments = 16; for (let i = 0; i <= sailSegments; i++) { for (let j = 0; j <= sailSegments; j++) { const u = i / sailSegments; const v = j / sailSegments; const sailHeight = 4.2 * v; const sailWidth = 2.5 * (1 - v * 0.7); const sx = u * sailWidth; const billowAmount = Math.sin(u * Math.PI) * Math.sin(v * Math.PI * 0.8); const sz = billowAmount * sailBillow * (1 - v * 0.3); sailVertices.push(sx, sailHeight, sz); } } const sailIndices: number[] = []; for (let i = 0; i < sailSegments; i++) { for (let j = 0; j < sailSegments; j++) { const a = i * (sailSegments + 1) + j; const b = a + 1; const c = a + sailSegments + 1; const d = c + 1; sailIndices.push(a, b, c); sailIndices.push(b, d, c); } } sailGeometry.setAttribute( 'position', new THREE.Float32BufferAttribute(sailVertices, 3) ); sailGeometry.setIndex(sailIndices); sailGeometry.computeVertexNormals(); const sailMaterial = new THREE.MeshStandardMaterial({ color: 0xfff8f0, roughness: 0.8, metalness: 0, side: THREE.DoubleSide, }); const sail = new THREE.Mesh(sailGeometry, sailMaterial); sail.position.set(-0.1, 0.7, 0.05); sail.castShadow = true; sail.receiveShadow = true; boatGroup.add(sail); // Jib Sail const jibGeometry = new THREE.BufferGeometry(); const jibVertices = new Float32Array([ 2.3, 0.6, sailBillow * 0.6, 0, 0.6, sailBillow * 0.3, 0, 4.5, 0, ]); jibGeometry.setAttribute('position', new THREE.BufferAttribute(jibVertices, 3)); jibGeometry.computeVertexNormals(); const jib = new THREE.Mesh(jibGeometry, sailMaterial); jib.castShadow = true; boatGroup.add(jib); // Rudder const rudderGeometry = new THREE.BoxGeometry(0.08, 0.8, 0.4); const rudder = new THREE.Mesh(rudderGeometry, hullMaterial); rudder.position.set(-1.9, -0.3, 0); rudder.castShadow = true; boatGroup.add(rudder); // Sailor const sailorGroup = new THREE.Group(); const bodyGeo = new THREE.CapsuleGeometry(0.12, 0.3, 4, 8); const clothMat = new THREE.MeshStandardMaterial({ color: 0x2244aa, roughness: 0.8 }); const body = new THREE.Mesh(bodyGeo, clothMat); body.position.y = 0.25; sailorGroup.add(body); const headGeo = new THREE.SphereGeometry(0.1, 12, 12); const skinMat = new THREE.MeshStandardMaterial({ color: 0xffccaa, roughness: 0.7 }); const head = new THREE.Mesh(headGeo, skinMat); head.position.y = 0.55; sailorGroup.add(head); const hatGeo = new THREE.CylinderGeometry(0.12, 0.14, 0.08, 12); const hatMat = new THREE.MeshStandardMaterial({ color: 0x223344, roughness: 0.6 }); const hat = new THREE.Mesh(hatGeo, hatMat); hat.position.y = 0.65; sailorGroup.add(hat); sailorGroup.position.set(-0.8, 0.5, 0); sailorGroup.rotation.y = -0.3; boatGroup.add(sailorGroup); scene.add(boatGroup); // ------------------------------------------------------------------------- // WAKE // ------------------------------------------------------------------------- const wakeMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, }); sceneData.wakeParticles.forEach((p) => { const age = (time + p.delay) % 3; const spread = age * 1.5; const opacity = Math.max(0, 0.4 - age * 0.15); if (opacity > 0.02) { const wakeGeo = new THREE.RingGeometry( p.size * (1 + age * 2), p.size * (1.3 + age * 2), 16 ); const wakeMat = wakeMaterial.clone(); wakeMat.opacity = opacity; const wake = new THREE.Mesh(wakeGeo, wakeMat); wake.rotation.x = -Math.PI / 2; wake.position.set( boatX - 2.5 - p.offsetX, 0.02, p.side * spread * 0.5 + p.offsetZ ); scene.add(wake); } }); // V-Wake lines for (let i = 0; i < 15; i++) { const wakeAge = i * 0.15; const wakeOpacity = Math.max(0, 0.5 - wakeAge * 0.08); if (wakeOpacity > 0) { const wakeLineGeo = new THREE.PlaneGeometry(0.8 + wakeAge * 0.3, 0.05); const wakeLineMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: wakeOpacity, }); const wakeLeft = new THREE.Mesh(wakeLineGeo, wakeLineMat); wakeLeft.rotation.x = -Math.PI / 2; wakeLeft.rotation.z = 0.3; wakeLeft.position.set(boatX - 2 - wakeAge * 1.5, 0.01, -wakeAge * 0.4); scene.add(wakeLeft); const wakeRight = new THREE.Mesh(wakeLineGeo, wakeLineMat.clone()); wakeRight.rotation.x = -Math.PI / 2; wakeRight.rotation.z = -0.3; wakeRight.position.set(boatX - 2 - wakeAge * 1.5, 0.01, wakeAge * 0.4); scene.add(wakeRight); } } // ------------------------------------------------------------------------- // SEAGULLS // ------------------------------------------------------------------------- sceneData.seagulls.forEach((gull) => { const gullGroup = new THREE.Group(); const gullTime = time * gull.orbitSpeed + gull.phase; const gullX = boatX + gull.baseX + Math.cos(gullTime * 2) * gull.orbitRadius; const gullZ = gull.baseZ + Math.sin(gullTime * 2) * gull.orbitRadius; const gullY = gull.baseY + Math.sin(gullTime * 3) * 0.5; const gullMat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.7 }); const gullBody = new THREE.Mesh( new THREE.CapsuleGeometry(0.08, 0.25, 4, 8), gullMat ); gullBody.rotation.z = Math.PI / 2; gullGroup.add(gullBody); const gullHead = new THREE.Mesh(new THREE.SphereGeometry(0.06, 8, 8), gullMat); gullHead.position.x = 0.2; gullGroup.add(gullHead); const beak = new THREE.Mesh( new THREE.ConeGeometry(0.02, 0.08, 6), new THREE.MeshStandardMaterial({ color: 0xffaa00, roughness: 0.5 }) ); beak.rotation.z = -Math.PI / 2; beak.position.set(0.28, 0, 0); gullGroup.add(beak); const flapAngle = Math.sin(time * gull.flapSpeed) * 0.5; const wingGeo = new THREE.PlaneGeometry(0.4, 0.12); const wingMat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.7, side: THREE.DoubleSide, }); const leftWing = new THREE.Mesh(wingGeo, wingMat); leftWing.position.set(0, 0.02, 0.15); leftWing.rotation.x = flapAngle; gullGroup.add(leftWing); const rightWing = new THREE.Mesh(wingGeo, wingMat); rightWing.position.set(0, 0.02, -0.15); rightWing.rotation.x = -flapAngle; gullGroup.add(rightWing); gullGroup.position.set(gullX, gullY, gullZ); const nextX = boatX + gull.baseX + Math.cos((gullTime + 0.1) * 2) * gull.orbitRadius; const nextZ = gull.baseZ + Math.sin((gullTime + 0.1) * 2) * gull.orbitRadius; gullGroup.lookAt(nextX, gullY, nextZ); gullGroup.castShadow = true; scene.add(gullGroup); }); // ------------------------------------------------------------------------- // CLOUDS // ------------------------------------------------------------------------- const cloudMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 1, transparent: true, opacity: 0.85, }); sceneData.clouds.forEach((c) => { const cloudGroup = new THREE.Group(); const spherePositions = [ [0, 0, 0, 1], [-0.6, 0.1, 0.1, 0.7], [0.7, 0.05, -0.1, 0.75], [0.2, 0.3, 0.15, 0.5], [-0.3, 0.25, -0.1, 0.55], [0.5, 0.2, 0.1, 0.45], ]; spherePositions.forEach(([cx, cy, cz, cr]) => { const sphereGeo = new THREE.SphereGeometry(cr as number, 8, 8); const sphere = new THREE.Mesh(sphereGeo, cloudMaterial); sphere.position.set(cx as number, cy as number, cz as number); cloudGroup.add(sphere); }); cloudGroup.position.set(c.x + time * c.speed, c.y, c.z); cloudGroup.scale.setScalar(c.scale); scene.add(cloudGroup); }); // ------------------------------------------------------------------------- // SUN GLINTS // ------------------------------------------------------------------------- const glintMaterial = new THREE.MeshBasicMaterial({ color: 0xffffee, transparent: true, }); sceneData.glints.forEach((g) => { const glintIntensity = Math.pow(Math.sin(time * g.speed + g.phase), 8) * 0.8; if (glintIntensity > 0.1) { const glintSize = 0.05 + glintIntensity * 0.1; const glintGeo = new THREE.PlaneGeometry(glintSize, glintSize); const glintMat = glintMaterial.clone(); glintMat.opacity = glintIntensity; const glint = new THREE.Mesh(glintGeo, glintMat); glint.rotation.x = -Math.PI / 2; glint.position.set(g.x, 0.03, g.z); scene.add(glint); } }); // ------------------------------------------------------------------------- // DISTANT LAND // ------------------------------------------------------------------------- const landGeometry = new THREE.PlaneGeometry(200, 15, 64, 1); const landPositions = landGeometry.attributes.position; for (let i = 0; i < landPositions.count; i++) { const lx = landPositions.getX(i); const ly = landPositions.getY(i); if (ly > 0) { const hillHeight = Math.sin(lx * 0.08) * 2 + Math.sin(lx * 0.15 + 1) * 1.5 + Math.sin(lx * 0.03) * 3; landPositions.setY(i, ly + hillHeight); } } landPositions.needsUpdate = true; const landMaterial = new THREE.MeshBasicMaterial({ color: 0x2a4a3a, transparent: true, opacity: 0.6, }); const land = new THREE.Mesh(landGeometry, landMaterial); land.position.set(0, 3, -60); scene.add(land); // ------------------------------------------------------------------------- // RENDER // ------------------------------------------------------------------------- renderer.render(scene, camera); return () => { renderer.dispose(); }; }, [frame, fps, durationInFrames, time, sceneData]); return ( ); }; // ============================================================================= // MAIN COMPONENT // ============================================================================= const SailboatOnCalmWaters: React.FC = () => { const frame = useCurrentFrame(); const { fps, durationInFrames, width, height } = useVideoConfig(); return ( ); }; export default SailboatOnCalmWaters;