/** * 🌿 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;