, 64, 64); break; case 'photos': // Camera icon ctx.beginPath(); ctx.arc(64, 50, 25, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.fillRect(50, 30, 28, 20); ctx.strokeRect(50, 30, 28, 20); ctx.fillStyle = '#000000'; ctx.beginPath(); ctx.arc(64, 50, 10, 0, Math.PI * 2); ctx.fill(); break; case 'chat': // Chat bubble ctx.beginPath(); ctx.ellipse(64, 50, 30, 25, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.moveTo(50, 75); ctx.lineTo(64, 65); ctx.lineTo(78, 75); ctx.closePath(); ctx.fill(); ctx.stroke(); break; } const texture = new THREE.CanvasTexture(canvas); const material = new THREE.MeshBasicMaterial({ map: texture, transparent: true, opacity: 0.9, side: THREE.DoubleSide }); const geometry = new THREE.PlaneGeometry(0.8, 0.8); const logoMesh = new THREE.Mesh(geometry, material); // Position logos fixed around the sphere logoMesh.position.copy(positions[index]); logoMesh.userData = { id: logo.id, label: logo.label }; // Make logos always face the camera logoMesh.lookAt(camera.position); scene.add(logoMesh); logos.push(logoMesh); }); } function animate() { requestAnimationFrame(animate); const delta = clock.getDelta(); if (isAnimating) { // Blink animation blinkTime += delta; if (blinkTime >= 1/Config.sphere.blinkFreq) { blinkTime = 0; triggerBlink(); } // Pulse animation pulseTime += delta; const pulseScale = 1 + 0.05 * Math.sin(pulseTime * (2 * Math.PI / Config.sphere.pulsePeriod)); sphere.scale.set(pulseScale, pulseScale, pulseScale); } // Rotate sphere slowly sphere.rotation.y += 0.002; sphere.rotation.x += 0.001; // Update logo positions to stay fixed logos.forEach(logo => { logo.lookAt(camera.position); }); renderer.render(scene, camera); } function triggerBlink() { // Quick emissive pulse sphere.material.emissive = new THREE.Color(0xffffff); sphere.material.emissiveIntensity = 1.0; setTimeout(() => { sphere.material.emissiveIntensity = 0.2; sphere.material.emissive = new THREE.Color(0x000033); }, 100); } function onMouseMove(event) { // Calculate mouse position in normalized device coordinates mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; // Update raycaster raycaster.setFromCamera(mouse, camera); // Check intersection with sphere const intersects = raycaster.intersectObject(sphere); if (intersects.length > 0) { // Show spotlight at intersection point spotLight.position.copy(intersects[0].point.clone().add( intersects[0].face.normal.clone().multiplyScalar(0.1) )); spotLight.target.position.copy(intersects[0].point); spotLight.visible = true; // Update highlight point highlightPoint.copy(intersects[0].point); } else { // Hide spotlight when not over sphere spotLight.visible = false; highlightPoint.set(0, 0, 0); } // Check logo hover updateLogoHoverEffects(); } function updateLogoHoverEffects() { raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(logos); // Reset all logo highlights logos.forEach(logo => { logo.material.opacity = 0.8; logo.scale.set(1, 1, 1); }); // Highlight hovered logo if (intersects.length > 0) { const logo = intersects[0].object; logo.material.opacity = 1.0; logo.scale.set(1.2, 1.2, 1.2); } } function onSphereClick(event) { toggleControls(); } function toggleControls() { const controls = document.getElementById('controls'); controls.style.display = controls.style.display === 'none' ? 'block' : 'none'; } function toggleAnimations() { isAnimating = !isAnimating; document.getElementById('play-pause').textContent = isAnimating ? '⏸ Pause' : '▶ Play'; } function resetDefaults() { Config.sphere.blinkFreq = 1.5; Config.sphere.pulsePeriod = 2.5; Config.sphere.highlightIntensity = 0.7; document.getElementById('blink-frequency').value = Config.sphere.blinkFreq; document.getElementById('pulse-frequency').value = 60 / Config.sphere.pulsePeriod; document.getElementById('highlight-intensity').value = Config.sphere.highlightIntensity; updateSliderValues(); } function updateBlinkFrequency(e) { Config.sphere.blinkFreq = parseFloat(e.target.value); document.getElementById('blink-value').textContent = Config.sphere.blinkFreq.toFixed(1); } function updatePulseFrequency(e) { const bpm = parseInt(e.target.value); Config.sphere.pulsePeriod = 60 / bpm; document.getElementById('pulse-value').textContent = bpm; } function updateHighlightIntensity(e) { Config.sphere.highlightIntensity = parseFloat(e.target.value); document.getElementById('highlight-value').textContent = Config.sphere.highlightIntensity.toFixed(1); // Update spotlight intensity spotLight.intensity = Config.sphere.highlightIntensity; } function initLogoControls() { const container = document.getElementById('logo-toggles'); Config.logos.forEach(logo => { const div = document.createElement('div'); div.className = 'logo-toggle'; div.innerHTML = ` `; container.appendChild(div); document.getElementById(`toggle-${logo.id}`).addEventListener('change', (e) => { toggleLogo(logo.id, e.target.checked); }); }); } function toggleLogo(id, visible) { const logo = logos.find(l => l.userData.id === id); if (logo) { logo.visible = visible; } } function updateSliderValues() { document.getElementById('blink-value').textContent = Config.sphere.blinkFreq.toFixed(1); document.getElementById('pulse-value').textContent = Math.round(60 / Config.sphere.pulsePeriod); document.getElementById('highlight-value').textContent = Config.sphere.highlightIntensity.toFixed(1); } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } // Initialize when page loads window.addEventListener('load', initScene);