Spaces:
Running
Running
| /** | |
| * 🌿 Ivy's Creative Studio | |
| * Tab 6: Three.js 3D Renderer | |
| * | |
| * Interactive 3D scenes with Three.js | |
| * Now with 8 scenes, 8 palettes, 6 materials, and effects! | |
| */ | |
| class ThreeJSRenderer { | |
| constructor() { | |
| this.scene = null; | |
| this.camera = null; | |
| this.renderer = null; | |
| this.controls = null; | |
| this.objects = []; | |
| this.animationId = null; | |
| this.isActive = false; | |
| this.clock = new THREE.Clock(); | |
| // Parameters | |
| this.params = { | |
| sceneType: "cubes", | |
| materialType: "standard", | |
| palette: "rainbow", | |
| objectCount: 50, | |
| speed: 1.0, | |
| scale: 1.0, | |
| wireframe: false, | |
| autoRotate: true, | |
| shadows: false, | |
| bloom: false | |
| }; | |
| } | |
| init(canvas) { | |
| this.canvas = canvas; | |
| // Create scene | |
| this.scene = new THREE.Scene(); | |
| this.scene.background = new THREE.Color(0x0a0a0f); | |
| this.scene.fog = new THREE.Fog(0x0a0a0f, 10, 50); | |
| // Use fallback dimensions if canvas is hidden (will be resized properly on start()) | |
| const width = canvas.clientWidth || 800; | |
| const height = canvas.clientHeight || 500; | |
| // Create camera | |
| this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); | |
| this.camera.position.set(0, 5, 15); | |
| // Create renderer | |
| this.renderer = new THREE.WebGLRenderer({ | |
| canvas: canvas, | |
| antialias: true, | |
| alpha: true | |
| }); | |
| this.renderer.setSize(width, height); | |
| this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| // Add lights | |
| this.setupLights(); | |
| // Add controls | |
| this.controls = new THREE.OrbitControls(this.camera, canvas); | |
| this.controls.enableDamping = true; | |
| this.controls.dampingFactor = 0.05; | |
| this.controls.autoRotate = this.params.autoRotate; | |
| this.controls.autoRotateSpeed = 0.5; | |
| // Create initial scene | |
| this.createScene(); | |
| // Handle resize | |
| this.handleResize = this.handleResize.bind(this); | |
| window.addEventListener("resize", this.handleResize); | |
| } | |
| setupLights() { | |
| // Ambient light | |
| const ambient = new THREE.AmbientLight(0x404040, 0.5); | |
| this.scene.add(ambient); | |
| // Directional light | |
| const directional = new THREE.DirectionalLight(0xffffff, 1); | |
| directional.position.set(5, 10, 7); | |
| this.scene.add(directional); | |
| // Point lights for color | |
| const pointLight1 = new THREE.PointLight(0x6366f1, 1, 50); | |
| pointLight1.position.set(-10, 5, 0); | |
| this.scene.add(pointLight1); | |
| const pointLight2 = new THREE.PointLight(0x8b5cf6, 1, 50); | |
| pointLight2.position.set(10, 5, 0); | |
| this.scene.add(pointLight2); | |
| this.lights = { ambient, directional, pointLight1, pointLight2 }; | |
| } | |
| clearScene() { | |
| // Remove all objects except lights and camera | |
| for (const obj of this.objects) { | |
| this.scene.remove(obj); | |
| if (obj.geometry) obj.geometry.dispose(); | |
| if (obj.material) { | |
| if (Array.isArray(obj.material)) { | |
| obj.material.forEach(m => m.dispose()); | |
| } else { | |
| obj.material.dispose(); | |
| } | |
| } | |
| } | |
| this.objects = []; | |
| } | |
| createScene() { | |
| this.clearScene(); | |
| switch (this.params.sceneType) { | |
| case "cubes": | |
| this.createCubesScene(); | |
| break; | |
| case "particles": | |
| this.createParticlesScene(); | |
| break; | |
| case "terrain": | |
| this.createTerrainScene(); | |
| break; | |
| case "galaxy": | |
| this.createGalaxyScene(); | |
| break; | |
| case "ivy": | |
| this.createIvyScene(); | |
| break; | |
| case "torus": | |
| this.createTorusScene(); | |
| break; | |
| case "crystals": | |
| this.createCrystalsScene(); | |
| break; | |
| case "ocean": | |
| this.createOceanScene(); | |
| break; | |
| } | |
| } | |
| getColor(index, total) { | |
| const t = index / total; | |
| switch (this.params.palette) { | |
| case "ivy": | |
| return new THREE.Color().setHSL(0.35 + t * 0.1, 0.7, 0.4 + t * 0.2); | |
| case "rainbow": | |
| return new THREE.Color().setHSL(t, 0.8, 0.5); | |
| case "neon": | |
| const neonHues = [0.85, 0.15, 0.55, 0.75]; | |
| return new THREE.Color().setHSL(neonHues[index % 4], 1.0, 0.5); | |
| case "fire": | |
| return new THREE.Color().setHSL(0.05 + t * 0.08, 1.0, 0.4 + t * 0.2); | |
| case "ocean": | |
| return new THREE.Color().setHSL(0.55 + t * 0.1, 0.7, 0.4 + t * 0.3); | |
| case "pastel": | |
| return new THREE.Color().setHSL(t, 0.5, 0.75); | |
| case "cosmic": | |
| return new THREE.Color().setHSL(0.7 + t * 0.2, 0.8, 0.3 + t * 0.4); | |
| case "monochrome": | |
| return new THREE.Color().setHSL(0.7, 0.0, 0.3 + t * 0.5); | |
| default: | |
| return new THREE.Color().setHSL(t, 0.8, 0.5); | |
| } | |
| } | |
| createMaterial(color) { | |
| const baseProps = { | |
| color: color, | |
| wireframe: this.params.wireframe | |
| }; | |
| switch (this.params.materialType) { | |
| case "standard": | |
| return new THREE.MeshStandardMaterial({ | |
| ...baseProps, | |
| metalness: 0.3, | |
| roughness: 0.6 | |
| }); | |
| case "phong": | |
| return new THREE.MeshPhongMaterial({ | |
| ...baseProps, | |
| shininess: 100, | |
| specular: 0x444444 | |
| }); | |
| case "toon": | |
| return new THREE.MeshToonMaterial(baseProps); | |
| case "glass": | |
| return new THREE.MeshPhysicalMaterial({ | |
| ...baseProps, | |
| metalness: 0.0, | |
| roughness: 0.0, | |
| transmission: 0.9, | |
| thickness: 0.5, | |
| transparent: true, | |
| opacity: 0.8 | |
| }); | |
| case "metal": | |
| return new THREE.MeshStandardMaterial({ | |
| ...baseProps, | |
| metalness: 1.0, | |
| roughness: 0.2 | |
| }); | |
| case "emissive": | |
| return new THREE.MeshStandardMaterial({ | |
| ...baseProps, | |
| metalness: 0.0, | |
| roughness: 0.5, | |
| emissive: color, | |
| emissiveIntensity: 0.5 | |
| }); | |
| default: | |
| return new THREE.MeshStandardMaterial(baseProps); | |
| } | |
| } | |
| createCubesScene() { | |
| const count = this.params.objectCount; | |
| const scale = this.params.scale; | |
| for (let i = 0; i < count; i++) { | |
| const size = (Math.random() * 1 + 0.5) * scale; | |
| const geometry = new THREE.BoxGeometry(size, size, size); | |
| const material = this.createMaterial(this.getColor(i, count)); | |
| const cube = new THREE.Mesh(geometry, material); | |
| // Random position in sphere | |
| const radius = 8 + Math.random() * 8; | |
| const theta = Math.random() * Math.PI * 2; | |
| const phi = Math.random() * Math.PI; | |
| cube.position.set(radius * Math.sin(phi) * Math.cos(theta), radius * Math.cos(phi) - 2, radius * Math.sin(phi) * Math.sin(theta)); | |
| cube.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI); | |
| // Store animation data | |
| cube.userData = { | |
| rotationSpeed: { | |
| x: (Math.random() - 0.5) * 0.02, | |
| y: (Math.random() - 0.5) * 0.02, | |
| z: (Math.random() - 0.5) * 0.02 | |
| }, | |
| floatOffset: Math.random() * Math.PI * 2, | |
| floatSpeed: Math.random() * 0.5 + 0.5, | |
| originalY: cube.position.y | |
| }; | |
| if (this.params.shadows) { | |
| cube.castShadow = true; | |
| cube.receiveShadow = true; | |
| } | |
| this.scene.add(cube); | |
| this.objects.push(cube); | |
| } | |
| } | |
| createParticlesScene() { | |
| const count = this.params.objectCount * 100; | |
| const geometry = new THREE.BufferGeometry(); | |
| const positions = new Float32Array(count * 3); | |
| const colors = new Float32Array(count * 3); | |
| for (let i = 0; i < count; i++) { | |
| const i3 = i * 3; | |
| // Spherical distribution | |
| const radius = 5 + Math.random() * 10; | |
| const theta = Math.random() * Math.PI * 2; | |
| const phi = Math.random() * Math.PI; | |
| positions[i3] = radius * Math.sin(phi) * Math.cos(theta); | |
| positions[i3 + 1] = radius * Math.cos(phi); | |
| positions[i3 + 2] = radius * Math.sin(phi) * Math.sin(theta); | |
| const color = this.getColor(i, count); | |
| colors[i3] = color.r; | |
| colors[i3 + 1] = color.g; | |
| colors[i3 + 2] = color.b; | |
| } | |
| geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); | |
| geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3)); | |
| const material = new THREE.PointsMaterial({ | |
| size: 0.1, | |
| vertexColors: true, | |
| transparent: true, | |
| opacity: 0.8, | |
| blending: THREE.AdditiveBlending | |
| }); | |
| const particles = new THREE.Points(geometry, material); | |
| particles.userData = { isParticles: true }; | |
| this.scene.add(particles); | |
| this.objects.push(particles); | |
| } | |
| createTerrainScene() { | |
| const size = 30; | |
| const segments = this.params.objectCount; | |
| const geometry = new THREE.PlaneGeometry(size, size, segments, segments); | |
| const positions = geometry.attributes.position; | |
| // Generate terrain heights using noise | |
| for (let i = 0; i < positions.count; i++) { | |
| const x = positions.getX(i); | |
| const y = positions.getY(i); | |
| // Simple noise-like height | |
| let height = 0; | |
| height += Math.sin(x * 0.5) * Math.cos(y * 0.5) * 2; | |
| height += Math.sin(x * 0.2 + y * 0.3) * 1.5; | |
| height += Math.random() * 0.2; | |
| positions.setZ(i, height); | |
| } | |
| geometry.computeVertexNormals(); | |
| const material = new THREE.MeshStandardMaterial({ | |
| color: this.getColor(0, 1), | |
| wireframe: this.params.wireframe, | |
| side: THREE.DoubleSide, | |
| flatShading: true | |
| }); | |
| const terrain = new THREE.Mesh(geometry, material); | |
| terrain.rotation.x = -Math.PI / 2; | |
| terrain.position.y = -3; | |
| terrain.userData = { isTerrain: true }; | |
| this.scene.add(terrain); | |
| this.objects.push(terrain); | |
| // Add water plane | |
| const waterGeometry = new THREE.PlaneGeometry(size, size); | |
| const waterMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x1e90ff, | |
| transparent: true, | |
| opacity: 0.6, | |
| metalness: 0.8, | |
| roughness: 0.2 | |
| }); | |
| const water = new THREE.Mesh(waterGeometry, waterMaterial); | |
| water.rotation.x = -Math.PI / 2; | |
| water.position.y = -2; | |
| water.userData = { isWater: true }; | |
| this.scene.add(water); | |
| this.objects.push(water); | |
| } | |
| createGalaxyScene() { | |
| const count = this.params.objectCount * 200; | |
| const branches = 5; | |
| const spin = 2; | |
| const radius = 15; | |
| const geometry = new THREE.BufferGeometry(); | |
| const positions = new Float32Array(count * 3); | |
| const colors = new Float32Array(count * 3); | |
| for (let i = 0; i < count; i++) { | |
| const i3 = i * 3; | |
| const r = Math.random() * radius; | |
| const branchAngle = ((i % branches) / branches) * Math.PI * 2; | |
| const spinAngle = r * spin; | |
| // Add randomness | |
| const randomX = (Math.random() - 0.5) * (radius - r) * 0.3; | |
| const randomY = (Math.random() - 0.5) * (radius - r) * 0.1; | |
| const randomZ = (Math.random() - 0.5) * (radius - r) * 0.3; | |
| positions[i3] = Math.cos(branchAngle + spinAngle) * r + randomX; | |
| positions[i3 + 1] = randomY; | |
| positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * r + randomZ; | |
| // Color gradient from center (white) to edge (colored) | |
| const mixRatio = r / radius; | |
| const branchColor = this.getColor(i % branches, branches); | |
| colors[i3] = 1 - mixRatio * (1 - branchColor.r); | |
| colors[i3 + 1] = 1 - mixRatio * (1 - branchColor.g); | |
| colors[i3 + 2] = 1 - mixRatio * (1 - branchColor.b); | |
| } | |
| geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); | |
| geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3)); | |
| const material = new THREE.PointsMaterial({ | |
| size: 0.05, | |
| vertexColors: true, | |
| transparent: true, | |
| blending: THREE.AdditiveBlending, | |
| depthWrite: false | |
| }); | |
| const galaxy = new THREE.Points(geometry, material); | |
| galaxy.userData = { isGalaxy: true }; | |
| this.scene.add(galaxy); | |
| this.objects.push(galaxy); | |
| // Center glow | |
| const glowGeometry = new THREE.SphereGeometry(0.5, 32, 32); | |
| const glowMaterial = new THREE.MeshBasicMaterial({ | |
| color: 0xffffff, | |
| transparent: true, | |
| opacity: 0.8 | |
| }); | |
| const glow = new THREE.Mesh(glowGeometry, glowMaterial); | |
| this.scene.add(glow); | |
| this.objects.push(glow); | |
| } | |
| // 🌿 IVY SCENE - Organic vines in 3D! | |
| createIvyScene() { | |
| const ivyGreen = new THREE.Color(0x22c55e); | |
| const darkGreen = new THREE.Color(0x166534); | |
| const leafGreen = new THREE.Color(0x4ade80); | |
| // Create multiple vine spirals | |
| const numVines = Math.min(this.params.objectCount, 8); | |
| for (let v = 0; v < numVines; v++) { | |
| const vineAngle = (v / numVines) * Math.PI * 2; | |
| const vineRadius = 3 + Math.random() * 2; | |
| // Vine stem - spiral tube | |
| const curve = new THREE.CatmullRomCurve3([]); | |
| const segments = 50; | |
| for (let i = 0; i < segments; i++) { | |
| const t = i / segments; | |
| const y = t * 15 - 7; | |
| const spiralAngle = vineAngle + t * Math.PI * 4; | |
| const r = vineRadius * (1 - t * 0.3); | |
| curve.points.push(new THREE.Vector3(Math.cos(spiralAngle) * r, y, Math.sin(spiralAngle) * r)); | |
| } | |
| const tubeGeometry = new THREE.TubeGeometry(curve, 64, 0.1, 8, false); | |
| const tubeMaterial = new THREE.MeshStandardMaterial({ | |
| color: darkGreen, | |
| roughness: 0.8, | |
| metalness: 0.1 | |
| }); | |
| const vine = new THREE.Mesh(tubeGeometry, tubeMaterial); | |
| vine.userData = { isVine: true, vineIndex: v }; | |
| this.scene.add(vine); | |
| this.objects.push(vine); | |
| // Add leaves along the vine | |
| const numLeaves = 15 + Math.floor(Math.random() * 10); | |
| for (let l = 0; l < numLeaves; l++) { | |
| const t = (l / numLeaves) * 0.9 + 0.05; | |
| const point = curve.getPoint(t); | |
| // Leaf shape (heart-like) | |
| const leafShape = new THREE.Shape(); | |
| const leafSize = 0.3 + Math.random() * 0.2; | |
| leafShape.moveTo(0, 0); | |
| leafShape.bezierCurveTo(leafSize * 0.5, leafSize * 0.3, leafSize * 0.8, leafSize * 0.8, 0, leafSize * 1.2); | |
| leafShape.bezierCurveTo(-leafSize * 0.8, leafSize * 0.8, -leafSize * 0.5, leafSize * 0.3, 0, 0); | |
| const leafGeometry = new THREE.ShapeGeometry(leafShape); | |
| const leafMaterial = new THREE.MeshStandardMaterial({ | |
| color: leafGreen.clone().lerp(ivyGreen, Math.random()), | |
| side: THREE.DoubleSide, | |
| roughness: 0.6, | |
| metalness: 0.0 | |
| }); | |
| const leaf = new THREE.Mesh(leafGeometry, leafMaterial); | |
| leaf.position.copy(point); | |
| // Random rotation | |
| leaf.rotation.x = Math.random() * Math.PI; | |
| leaf.rotation.y = Math.random() * Math.PI * 2; | |
| leaf.rotation.z = Math.random() * 0.5; | |
| leaf.userData = { isLeaf: true, initialRotation: leaf.rotation.clone() }; | |
| this.scene.add(leaf); | |
| this.objects.push(leaf); | |
| } | |
| } | |
| // Add some floating particles (pollen/sparkles) | |
| const particleCount = 500; | |
| const particleGeometry = new THREE.BufferGeometry(); | |
| const positions = new Float32Array(particleCount * 3); | |
| for (let i = 0; i < particleCount; i++) { | |
| positions[i * 3] = (Math.random() - 0.5) * 20; | |
| positions[i * 3 + 1] = (Math.random() - 0.5) * 20; | |
| positions[i * 3 + 2] = (Math.random() - 0.5) * 20; | |
| } | |
| particleGeometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); | |
| const particleMaterial = new THREE.PointsMaterial({ | |
| size: 0.05, | |
| color: 0xffffff, | |
| transparent: true, | |
| opacity: 0.6, | |
| blending: THREE.AdditiveBlending | |
| }); | |
| const particles = new THREE.Points(particleGeometry, particleMaterial); | |
| particles.userData = { isParticles: true }; | |
| this.scene.add(particles); | |
| this.objects.push(particles); | |
| } | |
| start() { | |
| this.isActive = true; | |
| this.canvas.classList.remove("hidden"); | |
| // Wait for the canvas to be visible and have dimensions before resizing | |
| // Double RAF ensures the browser has completed layout calculations | |
| requestAnimationFrame(() => { | |
| requestAnimationFrame(() => { | |
| this.handleResize(); | |
| this.animate(); | |
| }); | |
| }); | |
| } | |
| stop() { | |
| this.isActive = false; | |
| this.canvas.classList.add("hidden"); | |
| if (this.animationId) { | |
| cancelAnimationFrame(this.animationId); | |
| this.animationId = null; | |
| } | |
| } | |
| reset() { | |
| this.camera.position.set(0, 5, 15); | |
| this.camera.lookAt(0, 0, 0); | |
| this.controls.reset(); | |
| } | |
| setSceneType(type) { | |
| this.params.sceneType = type; | |
| this.createScene(); | |
| } | |
| setMaterialType(type) { | |
| this.params.materialType = type; | |
| this.createScene(); | |
| } | |
| setPalette(palette) { | |
| this.params.palette = palette; | |
| this.createScene(); | |
| } | |
| setObjectCount(count) { | |
| this.params.objectCount = count; | |
| this.createScene(); | |
| } | |
| setSpeed(speed) { | |
| this.params.speed = speed; | |
| } | |
| setScale(scale) { | |
| this.params.scale = scale; | |
| this.createScene(); | |
| } | |
| setWireframe(enabled) { | |
| this.params.wireframe = enabled; | |
| for (const obj of this.objects) { | |
| if (obj.material && !obj.userData.isParticles && !obj.userData.isGalaxy) { | |
| obj.material.wireframe = enabled; | |
| } | |
| } | |
| } | |
| setAutoRotate(enabled) { | |
| this.params.autoRotate = enabled; | |
| if (this.controls) { | |
| this.controls.autoRotate = enabled; | |
| } | |
| } | |
| setShadows(enabled) { | |
| this.params.shadows = enabled; | |
| this.renderer.shadowMap.enabled = enabled; | |
| if (this.lights && this.lights.directional) { | |
| this.lights.directional.castShadow = enabled; | |
| } | |
| this.createScene(); | |
| } | |
| setBloom(enabled) { | |
| this.params.bloom = enabled; | |
| // Note: Full bloom requires post-processing pass | |
| // For now we enhance emissive materials | |
| if (enabled) { | |
| this.scene.background = new THREE.Color(0x050508); | |
| } else { | |
| this.scene.background = new THREE.Color(0x0a0a0f); | |
| } | |
| } | |
| // === NEW SCENES === | |
| createTorusScene() { | |
| const count = Math.floor(this.params.objectCount / 5); | |
| const scale = this.params.scale; | |
| for (let i = 0; i < count; i++) { | |
| const radius = (1 + Math.random() * 0.5) * scale; | |
| const tube = (0.3 + Math.random() * 0.2) * scale; | |
| const geometry = new THREE.TorusKnotGeometry(radius, tube, 100, 16, 2 + (i % 5), 3 + (i % 7)); | |
| const material = this.createMaterial(this.getColor(i, count)); | |
| const torus = new THREE.Mesh(geometry, material); | |
| const angle = (i / count) * Math.PI * 2; | |
| const distance = 5 + Math.random() * 5; | |
| torus.position.set(Math.cos(angle) * distance, (Math.random() - 0.5) * 6, Math.sin(angle) * distance); | |
| torus.userData = { | |
| rotationSpeed: { | |
| x: (Math.random() - 0.5) * 0.01, | |
| y: (Math.random() - 0.5) * 0.02, | |
| z: (Math.random() - 0.5) * 0.01 | |
| } | |
| }; | |
| this.scene.add(torus); | |
| this.objects.push(torus); | |
| } | |
| } | |
| createCrystalsScene() { | |
| const count = this.params.objectCount; | |
| const scale = this.params.scale; | |
| for (let i = 0; i < count; i++) { | |
| // Create crystal-like geometry (octahedron or icosahedron) | |
| const geometryType = | |
| Math.random() > 0.5 | |
| ? new THREE.OctahedronGeometry((0.5 + Math.random()) * scale, 0) | |
| : new THREE.IcosahedronGeometry((0.4 + Math.random() * 0.6) * scale, 0); | |
| const material = this.createMaterial(this.getColor(i, count)); | |
| if (material.transparent === undefined) { | |
| material.transparent = true; | |
| material.opacity = 0.7 + Math.random() * 0.3; | |
| } | |
| const crystal = new THREE.Mesh(geometryType, material); | |
| // Cluster formation | |
| const cluster = Math.floor(i / 10); | |
| const clusterAngle = cluster * 0.8; | |
| const clusterRadius = 3 + cluster * 0.5; | |
| crystal.position.set( | |
| Math.cos(clusterAngle) * clusterRadius + (Math.random() - 0.5) * 3, | |
| (Math.random() - 0.5) * 8 - 2, | |
| Math.sin(clusterAngle) * clusterRadius + (Math.random() - 0.5) * 3 | |
| ); | |
| crystal.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI); | |
| crystal.userData = { | |
| rotationSpeed: { | |
| x: (Math.random() - 0.5) * 0.005, | |
| y: (Math.random() - 0.5) * 0.01, | |
| z: (Math.random() - 0.5) * 0.005 | |
| }, | |
| floatOffset: Math.random() * Math.PI * 2, | |
| floatSpeed: Math.random() * 0.3 + 0.2, | |
| originalY: crystal.position.y | |
| }; | |
| this.scene.add(crystal); | |
| this.objects.push(crystal); | |
| } | |
| } | |
| createOceanScene() { | |
| const scale = this.params.scale; | |
| // Create water plane | |
| const waterGeometry = new THREE.PlaneGeometry(40 * scale, 40 * scale, 128, 128); | |
| const waterMaterial = new THREE.MeshPhongMaterial({ | |
| color: this.getColor(0, 1), | |
| shininess: 100, | |
| transparent: true, | |
| opacity: 0.8, | |
| side: THREE.DoubleSide | |
| }); | |
| const water = new THREE.Mesh(waterGeometry, waterMaterial); | |
| water.rotation.x = -Math.PI / 2; | |
| water.position.y = -2; | |
| water.userData = { isWater: true, geometry: waterGeometry }; | |
| this.scene.add(water); | |
| this.objects.push(water); | |
| // Add floating objects | |
| const floatCount = Math.floor(this.params.objectCount / 3); | |
| for (let i = 0; i < floatCount; i++) { | |
| const size = (0.3 + Math.random() * 0.5) * scale; | |
| const geometry = new THREE.SphereGeometry(size, 16, 16); | |
| const material = this.createMaterial(this.getColor(i, floatCount)); | |
| const sphere = new THREE.Mesh(geometry, material); | |
| sphere.position.set((Math.random() - 0.5) * 30, -1.5 + Math.random() * 0.5, (Math.random() - 0.5) * 30); | |
| sphere.userData = { | |
| floatOffset: Math.random() * Math.PI * 2, | |
| floatSpeed: Math.random() * 0.5 + 0.3, | |
| originalY: sphere.position.y, | |
| waveX: sphere.position.x, | |
| waveZ: sphere.position.z | |
| }; | |
| this.scene.add(sphere); | |
| this.objects.push(sphere); | |
| } | |
| } | |
| handleResize() { | |
| if (!this.canvas || !this.isActive) return; | |
| // Get dimensions from parent container for proper sizing | |
| const container = this.canvas.parentElement; | |
| const width = container?.clientWidth || this.canvas.clientWidth || window.innerWidth; | |
| const height = container?.clientHeight || this.canvas.clientHeight || window.innerHeight; | |
| // Skip if dimensions are still zero | |
| if (width === 0 || height === 0) return; | |
| this.camera.aspect = width / height; | |
| this.camera.updateProjectionMatrix(); | |
| // Set renderer size (this sets canvas width/height attributes) | |
| this.renderer.setSize(width, height, false); | |
| // Force canvas to fill container via CSS (Three.js setSize overrides this) | |
| this.canvas.style.width = "100%"; | |
| this.canvas.style.height = "100%"; | |
| } | |
| animate() { | |
| if (!this.isActive) return; | |
| this.animationId = requestAnimationFrame(() => this.animate()); | |
| const delta = this.clock.getDelta(); | |
| const elapsed = this.clock.getElapsedTime(); | |
| const speed = this.params.speed; | |
| // Update controls | |
| this.controls.update(); | |
| // Animate objects | |
| for (const obj of this.objects) { | |
| if (obj.userData.rotationSpeed) { | |
| obj.rotation.x += obj.userData.rotationSpeed.x * speed; | |
| obj.rotation.y += obj.userData.rotationSpeed.y * speed; | |
| obj.rotation.z += obj.userData.rotationSpeed.z * speed; | |
| // Float animation | |
| if (obj.userData.floatOffset !== undefined) { | |
| obj.position.y = obj.userData.originalY + Math.sin(elapsed * obj.userData.floatSpeed * speed + obj.userData.floatOffset) * 0.5; | |
| } | |
| } | |
| if (obj.userData.isParticles || obj.userData.isGalaxy) { | |
| obj.rotation.y += 0.001 * speed; | |
| } | |
| if (obj.userData.isWater && obj.userData.geometry) { | |
| // Animate water waves | |
| const positions = obj.userData.geometry.attributes.position; | |
| for (let i = 0; i < positions.count; i++) { | |
| const x = positions.getX(i); | |
| const y = positions.getY(i); | |
| const wave = Math.sin(x * 0.5 + elapsed * speed) * 0.3 + Math.sin(y * 0.3 + elapsed * speed * 0.7) * 0.2; | |
| positions.setZ(i, wave); | |
| } | |
| positions.needsUpdate = true; | |
| obj.userData.geometry.computeVertexNormals(); | |
| } | |
| // Floating objects on water | |
| if (obj.userData.waveX !== undefined) { | |
| const wx = obj.userData.waveX; | |
| const wz = obj.userData.waveZ; | |
| const wave = Math.sin(wx * 0.5 + elapsed * speed) * 0.3 + Math.sin(wz * 0.3 + elapsed * speed * 0.7) * 0.2; | |
| obj.position.y = obj.userData.originalY + wave; | |
| obj.rotation.x = Math.sin(elapsed * speed + obj.userData.floatOffset) * 0.1; | |
| obj.rotation.z = Math.cos(elapsed * speed * 0.7 + obj.userData.floatOffset) * 0.1; | |
| } | |
| // 🌿 Ivy leaf animation | |
| if (obj.userData.isLeaf) { | |
| const init = obj.userData.initialRotation; | |
| obj.rotation.x = init.x + Math.sin(elapsed * 0.5 * speed + obj.position.y) * 0.1; | |
| obj.rotation.y = init.y + Math.sin(elapsed * 0.3 * speed + obj.position.x) * 0.15; | |
| obj.rotation.z = init.z + Math.cos(elapsed * 0.4 * speed) * 0.1; | |
| } | |
| } | |
| // Animate point lights | |
| if (this.lights) { | |
| this.lights.pointLight1.position.x = Math.sin(elapsed * 0.5) * 10; | |
| this.lights.pointLight2.position.x = Math.cos(elapsed * 0.5) * 10; | |
| } | |
| this.renderer.render(this.scene, this.camera); | |
| } | |
| dispose() { | |
| this.stop(); | |
| this.clearScene(); | |
| if (this.renderer) { | |
| this.renderer.dispose(); | |
| } | |
| window.removeEventListener("resize", this.handleResize); | |
| } | |
| } | |
| // Export | |
| window.ThreeJSRenderer = ThreeJSRenderer; | |