import React, { useMemo } from 'react'; import { useCurrentFrame, useVideoConfig, interpolate, Easing, AbsoluteFill, } from 'remotion'; import * as THREE from 'three'; // ============================================================================= // COMPOSITION CONFIG // ============================================================================= export const compositionConfig = { id: 'VintageRecordPlayer', durationInSeconds: 15, fps: 60, width: 1920, height: 1080, }; // ============================================================================= // STYLE CONSTANTS // ============================================================================= const COLORS = { walnut: 0x5d4037, walnutDark: 0x3e2723, walnutLight: 0x795548, walnutGrain: 0x4e342e, vinyl: 0x0d0d0d, vinylSheen: 0x1a1a1a, labelRed: 0xb71c1c, labelGold: 0xffd54f, chrome: 0xe8e8e8, chromeDark: 0x9e9e9e, brass: 0xd4a84b, brassDark: 0x8d6e2c, rubber: 0x212121, feltGreen: 0x1b5e20, warmLight: 0xffe8d6, sunlight: 0xfff3e0, } as const; const EASINGS = { gentle: Easing.bezier(0.25, 0.1, 0.25, 1), easeOut: Easing.bezier(0.33, 1, 0.68, 1), easeInOut: Easing.bezier(0.37, 0, 0.63, 1), smooth: Easing.bezier(0.4, 0, 0.2, 1), }; // ============================================================================= // SEEDED RANDOM // ============================================================================= const seededRandom = (seed: number): number => { const x = Math.sin(seed * 9999) * 10000; return x - Math.floor(x); }; // ============================================================================= // TYPE DEFINITIONS // ============================================================================= interface DustParticle { x: number; y: number; z: number; size: number; driftSpeed: number; floatSpeed: number; phase: number; brightness: number; } interface WoodGrain { x: number; z: number; width: number; length: number; rotation: number; darkness: number; } // ============================================================================= // 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 totalDuration = durationInFrames / fps; const sceneData = useMemo(() => { // Dust particles floating in sunlight const dustParticles: DustParticle[] = []; for (let i = 0; i < 200; i++) { dustParticles.push({ x: seededRandom(i * 1.1) * 6 - 2, y: seededRandom(i * 2.2) * 4 + 0.5, z: seededRandom(i * 3.3) * 5 - 2.5, size: 0.004 + seededRandom(i * 4.4) * 0.012, driftSpeed: 0.1 + seededRandom(i * 5.5) * 0.2, floatSpeed: 0.3 + seededRandom(i * 6.6) * 0.4, phase: seededRandom(i * 7.7) * Math.PI * 2, brightness: 0.4 + seededRandom(i * 8.8) * 0.6, }); } // Wood grain lines for realism const woodGrains: WoodGrain[] = []; for (let i = 0; i < 40; i++) { woodGrains.push({ x: seededRandom(i * 11.1) * 3 - 1.5, z: seededRandom(i * 12.2) * 2.4 - 1.2, width: 0.003 + seededRandom(i * 13.3) * 0.006, length: 0.3 + seededRandom(i * 14.4) * 0.8, rotation: (seededRandom(i * 15.5) - 0.5) * 0.3, darkness: 0.7 + seededRandom(i * 16.6) * 0.3, }); } // Vinyl grooves - more detailed const grooveRadii: number[] = []; for (let i = 0; i < 60; i++) { grooveRadii.push(0.22 + i * 0.013); } return { dustParticles, woodGrains, grooveRadii }; }, []); React.useEffect(() => { if (!canvasRef.current) return; const canvas = canvasRef.current; const width = 1920; const height = 1080; 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.1; renderer.outputColorSpace = THREE.SRGBColorSpace; const scene = new THREE.Scene(); scene.fog = new THREE.FogExp2(0x1a1209, 0.06); const camera = new THREE.PerspectiveCamera(40, width / height, 0.1, 100); // ========================================================================= // LIGHTING - Warm afternoon sunlight through window // ========================================================================= const ambientLight = new THREE.AmbientLight(0xffeedd, 0.25); scene.add(ambientLight); // Main sunlight streaming through window const sunLight = new THREE.DirectionalLight(COLORS.sunlight, 2.2); sunLight.position.set(6, 10, 4); sunLight.castShadow = true; sunLight.shadow.mapSize.width = 2048; sunLight.shadow.mapSize.height = 2048; sunLight.shadow.camera.near = 1; sunLight.shadow.camera.far = 30; sunLight.shadow.camera.left = -6; sunLight.shadow.camera.right = 6; sunLight.shadow.camera.top = 6; sunLight.shadow.camera.bottom = -6; sunLight.shadow.bias = -0.0001; sunLight.shadow.normalBias = 0.02; scene.add(sunLight); // Warm fill from room const fillLight = new THREE.DirectionalLight(0xffd9b3, 0.5); fillLight.position.set(-4, 3, -3); scene.add(fillLight); // Subtle rim light const rimLight = new THREE.PointLight(0xffcc99, 0.4, 8); rimLight.position.set(-2, 2.5, 3); scene.add(rimLight); // Bounce light from table const bounceLight = new THREE.DirectionalLight(0xd4a574, 0.2); bounceLight.position.set(0, -2, 0); scene.add(bounceLight); // ========================================================================= // ENVIRONMENT - Warm interior // ========================================================================= // Back wall with subtle texture const wallGeo = new THREE.PlaneGeometry(16, 12); const wallMat = new THREE.MeshStandardMaterial({ color: 0x4a3728, roughness: 0.92, }); const wall = new THREE.Mesh(wallGeo, wallMat); wall.position.set(0, 3, -4); wall.receiveShadow = true; scene.add(wall); // Side wall (window wall - brighter) const sideWall = new THREE.Mesh( new THREE.PlaneGeometry(10, 12), new THREE.MeshStandardMaterial({ color: 0x5d4a3a, roughness: 0.9, }) ); sideWall.position.set(6, 3, 0); sideWall.rotation.y = -Math.PI / 2; sideWall.receiveShadow = true; scene.add(sideWall); // Window frame suggestion (bright rectangle) const windowLight = new THREE.Mesh( new THREE.PlaneGeometry(2, 3), new THREE.MeshBasicMaterial({ color: 0xfffef5, transparent: true, opacity: 0.95, }) ); windowLight.position.set(5.98, 3.5, 1); windowLight.rotation.y = -Math.PI / 2; scene.add(windowLight); // ========================================================================= // TABLE - Rich wood surface // ========================================================================= const tableGeo = new THREE.BoxGeometry(6, 0.12, 5); const tableMat = new THREE.MeshStandardMaterial({ color: 0x5d4037, roughness: 0.55, metalness: 0.05, }); const table = new THREE.Mesh(tableGeo, tableMat); table.position.set(0, -0.45, 0); table.receiveShadow = true; table.castShadow = true; scene.add(table); // ========================================================================= // RECORD PLAYER CABINET - Detailed walnut construction // ========================================================================= const cabinetGroup = new THREE.Group(); // Main cabinet body const cabinetMat = new THREE.MeshStandardMaterial({ color: COLORS.walnut, roughness: 0.45, metalness: 0.02, }); // Base with beveled edges simulation const cabinetBase = new THREE.Mesh( new THREE.BoxGeometry(2.6, 0.18, 2.0), cabinetMat ); cabinetBase.position.y = 0.09; cabinetBase.castShadow = true; cabinetBase.receiveShadow = true; cabinetGroup.add(cabinetBase); // Top plate - slightly lighter const topPlateMat = new THREE.MeshStandardMaterial({ color: COLORS.walnutLight, roughness: 0.4, metalness: 0.03, }); const topPlate = new THREE.Mesh( new THREE.BoxGeometry(2.5, 0.05, 1.9), topPlateMat ); topPlate.position.y = 0.205; topPlate.castShadow = true; topPlate.receiveShadow = true; cabinetGroup.add(topPlate); // Inset panel for platter area const insetMat = new THREE.MeshStandardMaterial({ color: COLORS.walnutDark, roughness: 0.5, }); const inset = new THREE.Mesh( new THREE.CylinderGeometry(1.15, 1.15, 0.02, 48), insetMat ); inset.position.set(-0.25, 0.19, 0); cabinetGroup.add(inset); // Wood grain details sceneData.woodGrains.forEach((grain) => { const grainMesh = new THREE.Mesh( new THREE.BoxGeometry(grain.width, 0.002, grain.length), new THREE.MeshStandardMaterial({ color: new THREE.Color(COLORS.walnutGrain).multiplyScalar(grain.darkness), roughness: 0.5, transparent: true, opacity: 0.4, }) ); grainMesh.position.set(grain.x, 0.232, grain.z); grainMesh.rotation.y = grain.rotation; cabinetGroup.add(grainMesh); }); // Metal corner accents const cornerMat = new THREE.MeshStandardMaterial({ color: COLORS.brass, roughness: 0.25, metalness: 0.85, }); const cornerPositions = [ [-1.22, -0.92], [1.22, -0.92], [-1.22, 0.92], [1.22, 0.92], ]; cornerPositions.forEach(([x, z]) => { const corner = new THREE.Mesh( new THREE.CylinderGeometry(0.035, 0.035, 0.2, 12), cornerMat ); corner.position.set(x, 0.09, z); corner.castShadow = true; cabinetGroup.add(corner); // Corner cap const cap = new THREE.Mesh( new THREE.SphereGeometry(0.04, 12, 8, 0, Math.PI * 2, 0, Math.PI / 2), cornerMat ); cap.position.set(x, 0.19, z); cabinetGroup.add(cap); }); // Front decorative strip const frontStrip = new THREE.Mesh( new THREE.BoxGeometry(2.2, 0.025, 0.015), cornerMat ); frontStrip.position.set(0, 0.09, 0.95); cabinetGroup.add(frontStrip); scene.add(cabinetGroup); // ========================================================================= // TURNTABLE PLATTER AND VINYL - Main rotating elements // ========================================================================= const platterGroup = new THREE.Group(); // Metal platter const platterMat = new THREE.MeshStandardMaterial({ color: 0x37474f, roughness: 0.35, metalness: 0.7, }); const platter = new THREE.Mesh( new THREE.CylinderGeometry(1.08, 1.08, 0.04, 64), platterMat ); platter.position.y = 0.25; platter.castShadow = true; platter.receiveShadow = true; platterGroup.add(platter); // Platter edge detail const platterEdge = new THREE.Mesh( new THREE.TorusGeometry(1.08, 0.015, 8, 64), new THREE.MeshStandardMaterial({ color: 0x263238, roughness: 0.3, metalness: 0.8, }) ); platterEdge.rotation.x = Math.PI / 2; platterEdge.position.y = 0.27; platterGroup.add(platterEdge); // Rubber mat const matTexture = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.9, metalness: 0, }); const rubberMat = new THREE.Mesh( new THREE.CylinderGeometry(1.02, 1.02, 0.015, 64), matTexture ); rubberMat.position.y = 0.285; platterGroup.add(rubberMat); // Center spindle const spindle = new THREE.Mesh( new THREE.CylinderGeometry(0.02, 0.02, 0.12, 16), new THREE.MeshStandardMaterial({ color: COLORS.chrome, roughness: 0.15, metalness: 0.9, }) ); spindle.position.y = 0.35; platterGroup.add(spindle); // ========================================================================= // VINYL RECORD - Detailed with visible grooves // ========================================================================= const vinylGroup = new THREE.Group(); // Main vinyl body const vinylMat = new THREE.MeshStandardMaterial({ color: COLORS.vinyl, roughness: 0.15, metalness: 0.05, }); const vinyl = new THREE.Mesh( new THREE.CylinderGeometry(0.95, 0.95, 0.012, 64), vinylMat ); vinyl.position.y = 0.3; vinyl.castShadow = true; vinylGroup.add(vinyl); // Vinyl edge - slightly reflective const vinylEdge = new THREE.Mesh( new THREE.TorusGeometry(0.95, 0.006, 8, 64), new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.1, metalness: 0.2, }) ); vinylEdge.rotation.x = Math.PI / 2; vinylEdge.position.y = 0.306; vinylGroup.add(vinylEdge); // Record label - classic red with gold const labelMat = new THREE.MeshStandardMaterial({ color: COLORS.labelRed, roughness: 0.6, }); const label = new THREE.Mesh( new THREE.CylinderGeometry(0.15, 0.15, 0.014, 32), labelMat ); label.position.y = 0.307; vinylGroup.add(label); // Label center ring const labelRing = new THREE.Mesh( new THREE.TorusGeometry(0.12, 0.008, 8, 32), new THREE.MeshStandardMaterial({ color: COLORS.labelGold, roughness: 0.4, metalness: 0.6, }) ); labelRing.rotation.x = Math.PI / 2; labelRing.position.y = 0.314; vinylGroup.add(labelRing); // Center hole area const centerHole = new THREE.Mesh( new THREE.CylinderGeometry(0.022, 0.022, 0.016, 16), new THREE.MeshStandardMaterial({ color: 0x0a0a0a, roughness: 0.3, }) ); centerHole.position.y = 0.307; vinylGroup.add(centerHole); // Vinyl grooves - visible spinning detail sceneData.grooveRadii.forEach((radius, i) => { const grooveDepth = i % 3 === 0 ? 0.004 : 0.002; const groove = new THREE.Mesh( new THREE.TorusGeometry(radius, grooveDepth, 4, 96), new THREE.MeshStandardMaterial({ color: i % 5 === 0 ? 0x151515 : 0x0a0a0a, roughness: 0.08, metalness: 0.1, }) ); groove.rotation.x = Math.PI / 2; groove.position.y = 0.307; vinylGroup.add(groove); }); // Vinyl surface sheen - catches light during rotation const sheenMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.05, metalness: 0.15, transparent: true, opacity: 0.3, }); const sheen = new THREE.Mesh( new THREE.CylinderGeometry(0.94, 0.94, 0.001, 64), sheenMat ); sheen.position.y = 0.308; vinylGroup.add(sheen); platterGroup.add(vinylGroup); // ========================================================================= // RECORD ROTATION - 33 1/3 RPM realistic speed // ========================================================================= // 33.33 RPM = 0.555 rotations per second = 3.49 radians per second const RPM = 33.333; const radiansPerSecond = (RPM / 60) * Math.PI * 2; // Start spinning after needle drops const spinStartTime = 2.0; const spinUpDuration = 1.5; const spinProgress = interpolate( time, [spinStartTime, spinStartTime + spinUpDuration], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASINGS.easeOut } ); const currentSpeed = spinProgress * radiansPerSecond; const totalRotation = time > spinStartTime ? (time - spinStartTime - spinUpDuration * (1 - spinProgress) / 2) * currentSpeed : 0; platterGroup.rotation.y = totalRotation; platterGroup.position.set(-0.25, 0, 0); scene.add(platterGroup); // ========================================================================= // TONEARM - Detailed with realistic mechanics // ========================================================================= const tonearmGroup = new THREE.Group(); // Arm rest const armRestBase = new THREE.Mesh( new THREE.BoxGeometry(0.08, 0.06, 0.15), new THREE.MeshStandardMaterial({ color: COLORS.walnutDark, roughness: 0.5, }) ); armRestBase.position.set(0.15, 0.26, 0.35); tonearmGroup.add(armRestBase); const armRestCradle = new THREE.Mesh( new THREE.BoxGeometry(0.03, 0.03, 0.12), new THREE.MeshStandardMaterial({ color: COLORS.chrome, roughness: 0.2, metalness: 0.8, }) ); armRestCradle.position.set(0.15, 0.3, 0.35); tonearmGroup.add(armRestCradle); // Arm base/pivot housing const armBaseMat = new THREE.MeshStandardMaterial({ color: COLORS.chromeDark, roughness: 0.3, metalness: 0.7, }); const armBase = new THREE.Mesh( new THREE.CylinderGeometry(0.07, 0.08, 0.1, 20), armBaseMat ); armBase.position.y = 0.28; tonearmGroup.add(armBase); const armBaseTop = new THREE.Mesh( new THREE.CylinderGeometry(0.05, 0.07, 0.03, 20), new THREE.MeshStandardMaterial({ color: COLORS.chrome, roughness: 0.15, metalness: 0.9, }) ); armBaseTop.position.y = 0.345; tonearmGroup.add(armBaseTop); // Pivot point const pivot = new THREE.Mesh( new THREE.SphereGeometry(0.025, 16, 12), new THREE.MeshStandardMaterial({ color: COLORS.chrome, roughness: 0.1, metalness: 0.95, }) ); pivot.position.y = 0.37; tonearmGroup.add(pivot); // Arm assembly (rotates) const armAssembly = new THREE.Group(); armAssembly.position.y = 0.37; // Main arm tube const armTube = new THREE.Mesh( new THREE.CylinderGeometry(0.008, 0.012, 0.85, 12), new THREE.MeshStandardMaterial({ color: COLORS.chrome, roughness: 0.12, metalness: 0.9, }) ); armTube.rotation.z = Math.PI / 2; armTube.position.x = -0.38; armAssembly.add(armTube); // Headshell mount const headshellMount = new THREE.Mesh( new THREE.CylinderGeometry(0.015, 0.012, 0.06, 10), new THREE.MeshStandardMaterial({ color: COLORS.chromeDark, roughness: 0.25, metalness: 0.8, }) ); headshellMount.rotation.z = Math.PI / 2; headshellMount.position.set(-0.82, 0, 0); armAssembly.add(headshellMount); // Headshell const headshell = new THREE.Mesh( new THREE.BoxGeometry(0.07, 0.018, 0.035), new THREE.MeshStandardMaterial({ color: 0x2a2a2a, roughness: 0.5, metalness: 0.3, }) ); headshell.position.set(-0.87, -0.008, 0); armAssembly.add(headshell); // Cartridge body const cartridge = new THREE.Mesh( new THREE.BoxGeometry(0.045, 0.03, 0.02), new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.6, }) ); cartridge.position.set(-0.885, -0.03, 0); armAssembly.add(cartridge); // Stylus/needle const stylus = new THREE.Mesh( new THREE.ConeGeometry(0.002, 0.018, 6), new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 0.15, metalness: 0.9, }) ); stylus.rotation.x = Math.PI; stylus.position.set(-0.885, -0.055, 0); armAssembly.add(stylus); // Stylus cantilever const cantilever = new THREE.Mesh( new THREE.CylinderGeometry(0.001, 0.001, 0.015, 6), new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.3, metalness: 0.7, }) ); cantilever.rotation.z = Math.PI / 2 + 0.3; cantilever.position.set(-0.89, -0.048, 0); armAssembly.add(cantilever); // Counterweight const counterweight = new THREE.Mesh( new THREE.CylinderGeometry(0.04, 0.04, 0.05, 16), new THREE.MeshStandardMaterial({ color: COLORS.chromeDark, roughness: 0.25, metalness: 0.75, }) ); counterweight.rotation.z = Math.PI / 2; counterweight.position.set(0.1, 0, 0); armAssembly.add(counterweight); // Anti-skate weight const antiSkate = new THREE.Mesh( new THREE.SphereGeometry(0.015, 10, 10), new THREE.MeshStandardMaterial({ color: COLORS.chrome, roughness: 0.2, metalness: 0.85, }) ); antiSkate.position.set(0.02, 0.02, 0.04); armAssembly.add(antiSkate); // ========================================================================= // TONEARM ANIMATION - Smooth needle drop // ========================================================================= const needleDropStart = 0.8; const needleDropEnd = 2.0; const dropProgress = interpolate( time, [needleDropStart, needleDropEnd], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASINGS.smooth } ); // Arm swings from rest position to record const armSwing = interpolate(dropProgress, [0, 1], [0.35, 0.02]); // Arm lowers onto record const armHeight = interpolate( dropProgress, [0, 0.6, 1], [0.06, 0.06, 0], { easing: EASINGS.gentle } ); // Subtle tracking movement as record plays const trackingProgress = time > needleDropEnd ? interpolate(time, [needleDropEnd, totalDuration], [0, 0.15], { extrapolateRight: 'clamp' }) : 0; armAssembly.rotation.y = armSwing - trackingProgress; armAssembly.rotation.z = -0.04; armAssembly.position.y = 0.37 + armHeight; tonearmGroup.add(armAssembly); tonearmGroup.position.set(0.85, 0, 0.55); scene.add(tonearmGroup); // ========================================================================= // CONTROLS - Speed selector, power, cueing lever // ========================================================================= // Speed selector knob const speedKnob = new THREE.Mesh( new THREE.CylinderGeometry(0.05, 0.055, 0.03, 20), new THREE.MeshStandardMaterial({ color: 0x303030, roughness: 0.5, metalness: 0.3, }) ); speedKnob.position.set(1.0, 0.24, -0.65); scene.add(speedKnob); // Speed indicator line const speedLine = new THREE.Mesh( new THREE.BoxGeometry(0.006, 0.004, 0.03), new THREE.MeshStandardMaterial({ color: 0xeeeeee }) ); speedLine.position.set(1.0, 0.258, -0.63); scene.add(speedLine); // Speed markings plate const speedPlate = new THREE.Mesh( new THREE.BoxGeometry(0.12, 0.002, 0.04), new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.7, }) ); speedPlate.position.set(1.0, 0.22, -0.55); scene.add(speedPlate); // Power switch const powerSwitch = new THREE.Mesh( new THREE.BoxGeometry(0.04, 0.015, 0.025), new THREE.MeshStandardMaterial({ color: 0x404040, roughness: 0.4, }) ); powerSwitch.position.set(1.0, 0.24, -0.82); scene.add(powerSwitch); // Power indicator LED const powerLed = new THREE.Mesh( new THREE.SphereGeometry(0.008, 8, 8), new THREE.MeshBasicMaterial({ color: time > 0.3 ? 0x00ff44 : 0x004400, }) ); powerLed.position.set(0.92, 0.235, -0.82); scene.add(powerLed); // Cueing lever const cueBase = new THREE.Mesh( new THREE.CylinderGeometry(0.025, 0.03, 0.02, 12), new THREE.MeshStandardMaterial({ color: COLORS.chromeDark, roughness: 0.3, metalness: 0.7, }) ); cueBase.position.set(0.95, 0.24, 0.2); scene.add(cueBase); const cueLever = new THREE.Mesh( new THREE.CylinderGeometry(0.008, 0.008, 0.06, 8), new THREE.MeshStandardMaterial({ color: COLORS.chrome, roughness: 0.2, metalness: 0.85, }) ); const cueAngle = dropProgress > 0.5 ? 0.3 : -0.3; cueLever.rotation.z = cueAngle; cueLever.position.set(0.95, 0.28, 0.2); scene.add(cueLever); // ========================================================================= // DUST PARTICLES - Floating in sunbeam // ========================================================================= const dustGeo = new THREE.SphereGeometry(1, 6, 6); sceneData.dustParticles.forEach((p) => { const drift = time * p.driftSpeed; const floatY = Math.sin(time * p.floatSpeed + p.phase) * 0.15; const floatX = Math.sin(time * 0.3 + p.phase * 1.5) * 0.1; const floatZ = Math.cos(time * 0.25 + p.phase * 0.8) * 0.08; const px = p.x + floatX + drift * 0.3; const py = ((p.y + floatY + drift * 0.1) % 4.5) + 0.3; const pz = p.z + floatZ; // Check if in light beam (coming from window) const inLightBeam = px > 0.5 && px < 5 && py > 0 && py < 5 && pz > -1 && pz < 2.5; // Shimmer effect const shimmer = Math.sin(time * 4 + p.phase * 3) * 0.4 + 0.6; const baseOpacity = inLightBeam ? p.brightness * 0.8 : p.brightness * 0.15; const finalOpacity = baseOpacity * shimmer; if (finalOpacity > 0.03) { const dustMat = new THREE.MeshBasicMaterial({ color: inLightBeam ? 0xfff8e8 : 0xccccbb, transparent: true, opacity: Math.min(finalOpacity, 0.7), }); const dust = new THREE.Mesh(dustGeo, dustMat); dust.position.set(px, py, pz); dust.scale.setScalar(p.size * (inLightBeam ? 1.3 : 1)); scene.add(dust); } }); // ========================================================================= // LIGHT BEAM VOLUME - Subtle atmospheric effect // ========================================================================= const beamGeo = new THREE.CylinderGeometry(0.8, 2.5, 7, 20, 1, true); const beamMat = new THREE.MeshBasicMaterial({ color: 0xfffae8, transparent: true, opacity: 0.025, side: THREE.DoubleSide, }); const beam = new THREE.Mesh(beamGeo, beamMat); beam.position.set(4, 3.5, 1); beam.rotation.z = -0.5; beam.rotation.x = 0.15; scene.add(beam); // Secondary beam layer for depth const beam2 = new THREE.Mesh( new THREE.CylinderGeometry(0.5, 1.8, 6, 16, 1, true), new THREE.MeshBasicMaterial({ color: 0xfff5e0, transparent: true, opacity: 0.02, side: THREE.DoubleSide, }) ); beam2.position.set(3.8, 3.2, 0.8); beam2.rotation.z = -0.45; beam2.rotation.x = 0.1; scene.add(beam2); // ========================================================================= // DECORATIVE ELEMENTS - Adding life to the scene // ========================================================================= // Stack of vinyl records const recordStackGroup = new THREE.Group(); for (let i = 0; i < 5; i++) { const stackedVinyl = new THREE.Mesh( new THREE.CylinderGeometry(0.95, 0.95, 0.01, 32), new THREE.MeshStandardMaterial({ color: 0x0d0d0d, roughness: 0.2, }) ); stackedVinyl.position.y = i * 0.012; stackedVinyl.rotation.y = seededRandom(i * 50) * 0.2; stackedVinyl.castShadow = true; recordStackGroup.add(stackedVinyl); } recordStackGroup.position.set(-2.3, -0.33, -0.8); recordStackGroup.rotation.x = 0.02; scene.add(recordStackGroup); // Album cover leaning const albumCover = new THREE.Mesh( new THREE.BoxGeometry(0.02, 0.95, 0.95), new THREE.MeshStandardMaterial({ color: 0x2c1810, roughness: 0.7, }) ); albumCover.position.set(-2.6, 0.1, -0.5); albumCover.rotation.z = -0.15; albumCover.rotation.y = 0.3; albumCover.castShadow = true; scene.add(albumCover); // Coffee mug const mugGroup = new THREE.Group(); const mugBody = new THREE.Mesh( new THREE.CylinderGeometry(0.1, 0.085, 0.22, 20), new THREE.MeshStandardMaterial({ color: 0xf5f0e6, roughness: 0.6, }) ); mugBody.castShadow = true; mugGroup.add(mugBody); const mugHandle = new THREE.Mesh( new THREE.TorusGeometry(0.045, 0.012, 8, 12, Math.PI), new THREE.MeshStandardMaterial({ color: 0xf5f0e6, roughness: 0.6, }) ); mugHandle.rotation.y = Math.PI / 2; mugHandle.rotation.x = Math.PI / 2; mugHandle.position.set(0.115, 0.02, 0); mugGroup.add(mugHandle); // Coffee surface const coffee = new THREE.Mesh( new THREE.CylinderGeometry(0.09, 0.09, 0.01, 20), new THREE.MeshStandardMaterial({ color: 0x3e2723, roughness: 0.3, }) ); coffee.position.y = 0.1; mugGroup.add(coffee); mugGroup.position.set(2.0, -0.28, 0.2); scene.add(mugGroup); // Small plant const potGroup = new THREE.Group(); const pot = new THREE.Mesh( new THREE.CylinderGeometry(0.12, 0.09, 0.15, 16), new THREE.MeshStandardMaterial({ color: 0xb87333, roughness: 0.7, }) ); pot.castShadow = true; potGroup.add(pot); // Soil const soil = new THREE.Mesh( new THREE.CylinderGeometry(0.11, 0.11, 0.02, 16), new THREE.MeshStandardMaterial({ color: 0x3e2723, roughness: 0.95, }) ); soil.position.y = 0.07; potGroup.add(soil); // Simple plant leaves const leafMat = new THREE.MeshStandardMaterial({ color: 0x4a7c59, roughness: 0.7, side: THREE.DoubleSide, }); for (let i = 0; i < 5; i++) { const leaf = new THREE.Mesh( new THREE.SphereGeometry(0.06, 8, 6), leafMat ); leaf.scale.set(1, 0.15, 2); const angle = (i / 5) * Math.PI * 2 + seededRandom(i * 30) * 0.5; const tilt = 0.4 + seededRandom(i * 31) * 0.3; leaf.position.set( Math.cos(angle) * 0.06, 0.12 + i * 0.03, Math.sin(angle) * 0.06 ); leaf.rotation.x = tilt; leaf.rotation.y = angle; potGroup.add(leaf); } potGroup.position.set(2.3, -0.31, -0.9); scene.add(potGroup); // ========================================================================= // CAMERA MOVEMENT - Subtle, cinematic // ========================================================================= const camProgress = interpolate(time, [0, totalDuration], [0, 1], { easing: EASINGS.gentle, }); // Gentle dolly and pan const camX = interpolate(camProgress, [0, 0.3, 0.7, 1], [2.8, 2.2, 1.8, 2.0]); const camY = interpolate(camProgress, [0, 0.5, 1], [2.0, 1.7, 1.9]); const camZ = interpolate(camProgress, [0, 0.4, 1], [3.2, 2.8, 3.0]); // Breathing motion const breatheX = Math.sin(time * 0.4) * 0.015; const breatheY = Math.sin(time * 0.3) * 0.01; const breatheZ = Math.sin(time * 0.35) * 0.012; camera.position.set( camX + breatheX, camY + breatheY, camZ + breatheZ ); const lookX = interpolate(camProgress, [0, 1], [-0.2, -0.15]); const lookY = interpolate(camProgress, [0, 0.5, 1], [0.35, 0.3, 0.32]); camera.lookAt(lookX, lookY, 0); // ========================================================================= // RENDER // ========================================================================= renderer.render(scene, camera); return () => { renderer.dispose(); }; }, [frame, fps, durationInFrames, time, totalDuration, sceneData]); return ( ); }; // ============================================================================= // MAIN COMPONENT // ============================================================================= const VintageRecordPlayer: React.FC = () => { const frame = useCurrentFrame(); const { durationInFrames, fps } = useVideoConfig(); return ( ); }; export default VintageRecordPlayer;