Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| <div class="threejs-galaxy" style="width:100%;margin:10px 0;aspect-ratio:3/1;min-height:260px;"></div> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js", | |
| "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/" | |
| } | |
| } | |
| </script> | |
| <style> | |
| .threejs-galaxy { | |
| overflow: visible; | |
| background: transparent; | |
| } | |
| .threejs-galaxy canvas { | |
| display: block; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .threejs-galaxy .tp-dfwv { | |
| position: absolute ; | |
| top: 16px ; | |
| right: 16px ; | |
| z-index: 100 ; | |
| } | |
| </style> | |
| <script type="module"> | |
| import * as THREE from 'three'; | |
| import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; | |
| import { Pane } from 'https://cdn.jsdelivr.net/npm/tweakpane@4.0.3/dist/tweakpane.js'; | |
| const container = document.querySelector('.threejs-galaxy'); | |
| if (!container || container.dataset.mounted === 'true') { | |
| if (container) console.log('Container already mounted'); | |
| } else { | |
| container.dataset.mounted = 'true'; | |
| // === Scene Setup === | |
| const scene = new THREE.Scene(); | |
| const camera = new THREE.PerspectiveCamera( | |
| 35, | |
| container.clientWidth / Math.max(260, Math.round(container.clientWidth / 3)), | |
| 0.1, | |
| 100 | |
| ); | |
| // Vue du dessus avec angle pour voir la profondeur - plus proche pour remplir l'espace | |
| camera.position.set(-0.03, 1.75, 5.71); | |
| camera.rotation.set(-0.43, -0.01, -0.01); | |
| const renderer = new THREE.WebGLRenderer({ | |
| antialias: true, | |
| alpha: true, | |
| powerPreference: 'high-performance' | |
| }); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| renderer.setClearColor(0x000000, 0); | |
| container.appendChild(renderer.domElement); | |
| // === OrbitControls === | |
| const controls = new OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.05; | |
| controls.autoRotate = true; | |
| controls.autoRotateSpeed = 0.5; | |
| controls.enableZoom = true; | |
| controls.enablePan = true; | |
| controls.panSpeed = 0.5; | |
| controls.minDistance = 3; | |
| controls.maxDistance = 12; | |
| controls.target.set(0.04, -0.75, 0.26); | |
| // Track pane visibility for logging | |
| let paneVisible = false; | |
| // Log camera position and rotation on change (only when pane is visible) | |
| controls.addEventListener('change', () => { | |
| if (paneVisible) { | |
| console.log('Camera Position:', `camera.position.set(${camera.position.x.toFixed(2)}, ${camera.position.y.toFixed(2)}, ${camera.position.z.toFixed(2)});`); | |
| console.log('Camera Rotation:', `camera.rotation.set(${camera.rotation.x.toFixed(2)}, ${camera.rotation.y.toFixed(2)}, ${camera.rotation.z.toFixed(2)});`); | |
| console.log('Target (Center):', `controls.target.set(${controls.target.x.toFixed(2)}, ${controls.target.y.toFixed(2)}, ${controls.target.z.toFixed(2)});`); | |
| console.log('---'); | |
| } | |
| }); | |
| // === Galaxy Parameters === | |
| // Detect current theme | |
| const isDarkMode = () => document.documentElement.getAttribute('data-theme') === 'dark'; | |
| const params = { | |
| count: 12000, | |
| size: 150, | |
| whiteSize: 25, | |
| sizeVariation: 0.8, | |
| radius: 4, | |
| branches: 2, | |
| spin: 3.0, | |
| randomness: 0.3, | |
| randomnessPower: 3, | |
| centerSizeBoost: 1.5, | |
| insideColor: isDarkMode() ? '#ff6030' : '#ff8050', | |
| outsideColor: isDarkMode() ? '#1b3984' : '#3d5fa8', | |
| fov: 35 | |
| }; | |
| // === Tweakpane === | |
| const pane = new Pane({ | |
| container: container, | |
| title: 'Galaxy Controls' | |
| }); | |
| // Hide pane by default | |
| pane.element.style.display = 'none'; | |
| let geometry = null; | |
| let material = null; | |
| let points = null; | |
| let whiteGeometry = null; | |
| let whiteMaterial = null; | |
| let whitePoints = null; | |
| // === Animation Clock === | |
| const clock = new THREE.Clock(); | |
| // === Generate Galaxy Function === | |
| const generateGalaxy = () => { | |
| // Destroy old galaxy | |
| if (points !== null) { | |
| geometry.dispose(); | |
| material.dispose(); | |
| scene.remove(points); | |
| } | |
| // Destroy old white points | |
| if (whitePoints !== null) { | |
| whiteGeometry.dispose(); | |
| whiteMaterial.dispose(); | |
| scene.remove(whitePoints); | |
| } | |
| // New geometry | |
| geometry = new THREE.BufferGeometry(); | |
| const positions = new Float32Array(params.count * 3); | |
| const colors = new Float32Array(params.count * 3); | |
| const scales = new Float32Array(params.count); | |
| const colorInside = new THREE.Color(params.insideColor); | |
| const colorOutside = new THREE.Color(params.outsideColor); | |
| for (let i = 0; i < params.count; i++) { | |
| const i3 = i * 3; | |
| // Position sur le rayon | |
| const radius = Math.random() * params.radius; | |
| const radiusRatio = radius / params.radius; | |
| // Angle de la branche | |
| const branchAngle = (i % params.branches) / params.branches * Math.PI * 2; | |
| // Angle de spin (twist) | |
| const spinAngle = radius * params.spin; | |
| // Randomness | |
| const randomX = Math.pow(Math.random(), params.randomnessPower) * (Math.random() < 0.5 ? 1 : -1) * params.randomness * radius; | |
| const randomY = Math.pow(Math.random(), params.randomnessPower) * (Math.random() < 0.5 ? 1 : -1) * params.randomness * radius * 0.3; | |
| const randomZ = Math.pow(Math.random(), params.randomnessPower) * (Math.random() < 0.5 ? 1 : -1) * params.randomness * radius; | |
| // Position finale en 3D | |
| positions[i3] = Math.cos(branchAngle + spinAngle) * radius + randomX; | |
| positions[i3 + 1] = randomY; | |
| positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * radius + randomZ; | |
| // Couleur | |
| const mixedColor = colorInside.clone(); | |
| mixedColor.lerp(colorOutside, radiusRatio); | |
| colors[i3] = mixedColor.r; | |
| colors[i3 + 1] = mixedColor.g; | |
| colors[i3 + 2] = mixedColor.b; | |
| // Échelle : plus gros au centre, linéairement décroissant vers l'extérieur | |
| const centerScale = (1.0 + params.centerSizeBoost) - radiusRatio * params.centerSizeBoost; | |
| // Variation aléatoire contrôlée par sizeVariation | |
| const randomScale = Math.pow(Math.random(), 2.0) * params.sizeVariation; | |
| scales[i] = randomScale + centerScale; | |
| } | |
| geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); | |
| geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); | |
| geometry.setAttribute('aScale', new THREE.BufferAttribute(scales, 1)); | |
| // === Shader Material === | |
| material = new THREE.ShaderMaterial({ | |
| depthWrite: false, | |
| blending: THREE.AdditiveBlending, | |
| vertexColors: true, | |
| uniforms: { | |
| uTime: { value: 0 }, | |
| uSize: { value: params.size * renderer.getPixelRatio() } | |
| }, | |
| vertexShader: ` | |
| uniform float uTime; | |
| uniform float uSize; | |
| attribute float aScale; | |
| varying vec3 vColor; | |
| void main() { | |
| vec4 modelPosition = modelMatrix * vec4(position, 1.0); | |
| vec4 viewPosition = viewMatrix * modelPosition; | |
| vec4 projectedPosition = projectionMatrix * viewPosition; | |
| gl_Position = projectedPosition; | |
| // Taille des points | |
| gl_PointSize = uSize * aScale; | |
| gl_PointSize *= (1.0 / -viewPosition.z); | |
| vColor = color; | |
| } | |
| `, | |
| fragmentShader: ` | |
| varying vec3 vColor; | |
| void main() { | |
| float distanceToCenter = distance(gl_PointCoord, vec2(0.5)); | |
| // Noyau brillant | |
| float core = 1.0 - smoothstep(0.0, 0.25, distanceToCenter); | |
| core = pow(core, 2.0); | |
| // Halo externe | |
| float halo = 1.0 - smoothstep(0.15, 0.5, distanceToCenter); | |
| halo = pow(halo, 3.0); | |
| // Combinaison | |
| float strength = max(core, halo * 0.3); | |
| // Couleur finale (adaptée au thème) | |
| float coreIntensity = ${isDarkMode() ? '0.8' : '0.7'}; | |
| float haloIntensity = ${isDarkMode() ? '0.4' : '0.35'}; | |
| vec3 coreColor = vColor * coreIntensity; | |
| vec3 haloColor = vColor * haloIntensity; | |
| vec3 finalColor = mix(haloColor, coreColor, core); | |
| // Alpha adapté au thème | |
| float alpha = strength * ${isDarkMode() ? '0.6' : '0.5'}; | |
| gl_FragColor = vec4(finalColor, alpha); | |
| } | |
| ` | |
| }); | |
| // === Points === | |
| points = new THREE.Points(geometry, material); | |
| scene.add(points); | |
| // === White Points (50% random subset) === | |
| const whiteCount = Math.floor(params.count * 0.5); | |
| const whitePositions = new Float32Array(whiteCount * 3); | |
| const whiteScales = new Float32Array(whiteCount); | |
| // Sélectionner aléatoirement 50% des indices | |
| const indices = Array.from({ length: params.count }, (_, i) => i); | |
| // Mélanger les indices (Fisher-Yates shuffle) | |
| for (let i = indices.length - 1; i > 0; i--) { | |
| const j = Math.floor(Math.random() * (i + 1)); | |
| [indices[i], indices[j]] = [indices[j], indices[i]]; | |
| } | |
| const selectedIndices = indices.slice(0, whiteCount); | |
| // Copier les positions sélectionnées et créer des échelles plus petites | |
| for (let i = 0; i < whiteCount; i++) { | |
| const sourceIdx = selectedIndices[i]; | |
| whitePositions[i * 3] = positions[sourceIdx * 3]; | |
| whitePositions[i * 3 + 1] = positions[sourceIdx * 3 + 1]; | |
| whitePositions[i * 3 + 2] = positions[sourceIdx * 3 + 2]; | |
| // Échelles beaucoup plus petites pour les points blancs | |
| whiteScales[i] = (Math.random() * 0.2 + 0.2) / 6; | |
| } | |
| whiteGeometry = new THREE.BufferGeometry(); | |
| whiteGeometry.setAttribute('position', new THREE.BufferAttribute(whitePositions, 3)); | |
| whiteGeometry.setAttribute('aScale', new THREE.BufferAttribute(whiteScales, 1)); | |
| // Matériau pour les points blancs | |
| whiteMaterial = new THREE.ShaderMaterial({ | |
| depthWrite: false, | |
| blending: THREE.AdditiveBlending, | |
| uniforms: { | |
| uTime: { value: 0 }, | |
| uSize: { value: params.whiteSize * renderer.getPixelRatio() } | |
| }, | |
| vertexShader: ` | |
| uniform float uTime; | |
| uniform float uSize; | |
| attribute float aScale; | |
| void main() { | |
| vec4 modelPosition = modelMatrix * vec4(position, 1.0); | |
| vec4 viewPosition = viewMatrix * modelPosition; | |
| vec4 projectedPosition = projectionMatrix * viewPosition; | |
| gl_Position = projectedPosition; | |
| gl_PointSize = uSize * aScale; | |
| gl_PointSize *= (1.0 / -viewPosition.z); | |
| } | |
| `, | |
| fragmentShader: ` | |
| void main() { | |
| float distanceToCenter = distance(gl_PointCoord, vec2(0.5)); | |
| // Créer une boule bien définie | |
| float strength = 1.0 - smoothstep(0.3, 0.5, distanceToCenter); | |
| strength = pow(strength, 2.0); | |
| // Couleur blanche (adaptée au thème) | |
| vec3 whiteColor = vec3(${isDarkMode() ? '1.0, 1.0, 1.0' : '0.95, 0.95, 0.98'}); | |
| gl_FragColor = vec4(whiteColor, strength * ${isDarkMode() ? '0.8' : '0.9'}); | |
| } | |
| ` | |
| }); | |
| whitePoints = new THREE.Points(whiteGeometry, whiteMaterial); | |
| scene.add(whitePoints); | |
| }; | |
| // Generate initial galaxy | |
| generateGalaxy(); | |
| // === Tweakpane Controls === | |
| pane.addBinding(params, 'count', { | |
| label: 'Particles', | |
| min: 1000, | |
| max: 50000, | |
| step: 1000 | |
| }).on('change', () => { | |
| generateGalaxy(); | |
| }); | |
| pane.addBinding(params, 'spin', { | |
| label: 'Twist', | |
| min: 0, | |
| max: 5, | |
| step: 0.1 | |
| }).on('change', () => { | |
| generateGalaxy(); | |
| }); | |
| pane.addBinding(params, 'size', { | |
| label: 'Point Size', | |
| min: 10, | |
| max: 200, | |
| step: 1 | |
| }).on('change', () => { | |
| if (material) { | |
| material.uniforms.uSize.value = params.size * renderer.getPixelRatio(); | |
| } | |
| }); | |
| pane.addBinding(params, 'sizeVariation', { | |
| label: 'Size Variation', | |
| min: 0, | |
| max: 2, | |
| step: 0.05 | |
| }).on('change', () => { | |
| generateGalaxy(); | |
| }); | |
| pane.addBinding(params, 'whiteSize', { | |
| label: 'White Size', | |
| min: 5, | |
| max: 100, | |
| step: 1 | |
| }).on('change', () => { | |
| if (whiteMaterial) { | |
| whiteMaterial.uniforms.uSize.value = params.whiteSize * renderer.getPixelRatio(); | |
| } | |
| }); | |
| pane.addBinding(params, 'centerSizeBoost', { | |
| label: 'Center Boost', | |
| min: 0, | |
| max: 3, | |
| step: 0.1 | |
| }).on('change', () => { | |
| generateGalaxy(); | |
| }); | |
| pane.addBinding(params, 'branches', { | |
| label: 'Branches', | |
| min: 2, | |
| max: 6, | |
| step: 1 | |
| }).on('change', () => { | |
| generateGalaxy(); | |
| }); | |
| pane.addBinding(params, 'fov', { | |
| label: 'FOV (Zoom)', | |
| min: 20, | |
| max: 75, | |
| step: 1 | |
| }).on('change', () => { | |
| camera.fov = params.fov; | |
| camera.updateProjectionMatrix(); | |
| }); | |
| // === Animation === | |
| const animate = () => { | |
| requestAnimationFrame(animate); | |
| const elapsedTime = clock.getElapsedTime(); | |
| if (material) { | |
| material.uniforms.uTime.value = elapsedTime; | |
| } | |
| // Mise à jour des contrôles OrbitControls (gère la rotation automatique) | |
| controls.update(); | |
| renderer.render(scene, camera); | |
| }; | |
| // === Resize === | |
| const onResize = () => { | |
| const width = container.clientWidth; | |
| const height = Math.max(260, Math.round(width / 3)); | |
| camera.aspect = width / height; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(width, height); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| if (material) { | |
| material.uniforms.uSize.value = params.size * renderer.getPixelRatio(); | |
| } | |
| if (whiteMaterial) { | |
| whiteMaterial.uniforms.uSize.value = params.whiteSize * renderer.getPixelRatio(); | |
| } | |
| }; | |
| onResize(); | |
| if (window.ResizeObserver) { | |
| new ResizeObserver(onResize).observe(container); | |
| } else { | |
| window.addEventListener('resize', onResize); | |
| } | |
| // === Theme Support === | |
| const updateTheme = () => { | |
| renderer.setClearColor(0x000000, 0); | |
| // Update colors based on theme | |
| params.insideColor = isDarkMode() ? '#ff6030' : '#ff8050'; | |
| params.outsideColor = isDarkMode() ? '#1b3984' : '#3d5fa8'; | |
| // Regenerate galaxy with new colors | |
| generateGalaxy(); | |
| }; | |
| const observer = new MutationObserver(updateTheme); | |
| observer.observe(document.documentElement, { | |
| attributes: true, | |
| attributeFilter: ['data-theme'] | |
| }); | |
| // === Keyboard Controls === | |
| window.addEventListener('keydown', (event) => { | |
| if (event.key === 'd' || event.key === 'D') { | |
| paneVisible = !paneVisible; | |
| if (paneVisible) { | |
| pane.element.style.display = ''; | |
| } else { | |
| pane.element.style.display = 'none'; | |
| } | |
| } | |
| }); | |
| // === Start === | |
| animate(); | |
| // === Cleanup === | |
| container._cleanup = () => { | |
| observer.disconnect(); | |
| if (geometry) geometry.dispose(); | |
| if (material) material.dispose(); | |
| if (whiteGeometry) whiteGeometry.dispose(); | |
| if (whiteMaterial) whiteMaterial.dispose(); | |
| controls.dispose(); | |
| renderer.dispose(); | |
| pane.dispose(); | |
| }; | |
| } | |
| </script> |