transformers / app /src /content /embeds /banner-threejs-galaxy.html
tfrere's picture
tfrere HF Staff
Clean repository - remove missing LFS files
6afedde
raw
history blame
18.8 kB
<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 !important;
top: 16px !important;
right: 16px !important;
z-index: 100 !important;
}
</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>