635 lines
20 KiB
HTML
635 lines
20 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="fr">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
|
<title>Vague Planétaire de Sirius - Lever Héliaque 2025</title>
|
||
|
|
<style>
|
||
|
|
* {
|
||
|
|
margin: 0;
|
||
|
|
padding: 0;
|
||
|
|
box-sizing: border-box;
|
||
|
|
}
|
||
|
|
|
||
|
|
body {
|
||
|
|
overflow: hidden;
|
||
|
|
background: radial-gradient(circle at center, #0a192f 0%, #020c1b 100%);
|
||
|
|
font-family: 'Arial', sans-serif;
|
||
|
|
color: #ccd6f6;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Contrôles */
|
||
|
|
.controls {
|
||
|
|
position: fixed;
|
||
|
|
top: 1rem;
|
||
|
|
right: 1rem;
|
||
|
|
z-index: 10;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn {
|
||
|
|
padding: 0.75rem 1.5rem;
|
||
|
|
background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
|
||
|
|
color: #0a192f;
|
||
|
|
border: none;
|
||
|
|
border-radius: 25px;
|
||
|
|
cursor: pointer;
|
||
|
|
font-weight: bold;
|
||
|
|
transition: all 0.3s ease;
|
||
|
|
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn:hover {
|
||
|
|
transform: translateY(-2px);
|
||
|
|
box-shadow: 0 6px 20px rgba(255, 215, 0, 0.5);
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn.secondary {
|
||
|
|
background: linear-gradient(135deg, #64ffda 0%, #64b5f6 100%);
|
||
|
|
box-shadow: 0 4px 15px rgba(100, 255, 218, 0.3);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Tooltip amélioré */
|
||
|
|
#tooltip {
|
||
|
|
position: absolute;
|
||
|
|
padding: 12px 16px;
|
||
|
|
background: rgba(10, 25, 47, 0.95);
|
||
|
|
color: #ccd6f6;
|
||
|
|
border-radius: 8px;
|
||
|
|
pointer-events: none;
|
||
|
|
display: none;
|
||
|
|
z-index: 100;
|
||
|
|
font-size: 0.9rem;
|
||
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||
|
|
border: 1px solid rgba(255, 215, 0, 0.3);
|
||
|
|
backdrop-filter: blur(10px);
|
||
|
|
}
|
||
|
|
|
||
|
|
#tooltip strong {
|
||
|
|
color: #ffd700;
|
||
|
|
font-size: 1.1rem;
|
||
|
|
display: block;
|
||
|
|
margin-bottom: 4px;
|
||
|
|
}
|
||
|
|
|
||
|
|
#tooltip .date {
|
||
|
|
color: #64ffda;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Info Panel */
|
||
|
|
.info-panel {
|
||
|
|
position: fixed;
|
||
|
|
bottom: 1rem;
|
||
|
|
left: 1rem;
|
||
|
|
background: rgba(10, 25, 47, 0.9);
|
||
|
|
padding: 1.5rem;
|
||
|
|
border-radius: 12px;
|
||
|
|
backdrop-filter: blur(10px);
|
||
|
|
border: 1px solid rgba(255, 215, 0, 0.2);
|
||
|
|
max-width: 300px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-panel h2 {
|
||
|
|
color: #ffd700;
|
||
|
|
font-size: 1.2rem;
|
||
|
|
margin-bottom: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-panel p {
|
||
|
|
font-size: 0.9rem;
|
||
|
|
line-height: 1.5;
|
||
|
|
color: #8892b0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Loading */
|
||
|
|
.loading {
|
||
|
|
position: fixed;
|
||
|
|
top: 50%;
|
||
|
|
left: 50%;
|
||
|
|
transform: translate(-50%, -50%);
|
||
|
|
text-align: center;
|
||
|
|
z-index: 1000;
|
||
|
|
}
|
||
|
|
|
||
|
|
.loading-spinner {
|
||
|
|
width: 50px;
|
||
|
|
height: 50px;
|
||
|
|
border: 3px solid rgba(255, 215, 0, 0.3);
|
||
|
|
border-top-color: #ffd700;
|
||
|
|
border-radius: 50%;
|
||
|
|
animation: spin 1s linear infinite;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes spin {
|
||
|
|
to { transform: rotate(360deg); }
|
||
|
|
}
|
||
|
|
|
||
|
|
.loading-text {
|
||
|
|
margin-top: 1rem;
|
||
|
|
color: #ffd700;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Légende */
|
||
|
|
.legend {
|
||
|
|
position: fixed;
|
||
|
|
top: 1rem;
|
||
|
|
left: 1rem;
|
||
|
|
background: rgba(10, 25, 47, 0.9);
|
||
|
|
padding: 1rem;
|
||
|
|
border-radius: 8px;
|
||
|
|
border: 1px solid rgba(255, 215, 0, 0.2);
|
||
|
|
backdrop-filter: blur(10px);
|
||
|
|
}
|
||
|
|
|
||
|
|
.legend h3 {
|
||
|
|
color: #ffd700;
|
||
|
|
font-size: 1rem;
|
||
|
|
margin-bottom: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.legend-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.5rem;
|
||
|
|
margin: 0.3rem 0;
|
||
|
|
font-size: 0.85rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.legend-icon {
|
||
|
|
width: 20px;
|
||
|
|
height: 20px;
|
||
|
|
border-radius: 50%;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Mobile responsive */
|
||
|
|
@media (max-width: 768px) {
|
||
|
|
.info-panel {
|
||
|
|
max-width: 200px;
|
||
|
|
padding: 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.legend {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.controls {
|
||
|
|
top: 0.5rem;
|
||
|
|
right: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn {
|
||
|
|
padding: 0.5rem 1rem;
|
||
|
|
font-size: 0.9rem;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<!-- Loading Screen -->
|
||
|
|
<div class="loading" id="loading">
|
||
|
|
<div class="loading-spinner"></div>
|
||
|
|
<div class="loading-text">Chargement de la carte stellaire...</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Controls -->
|
||
|
|
<div class="controls">
|
||
|
|
<button class="btn" id="modeBtn">Mode Manuel</button>
|
||
|
|
<button class="btn secondary" id="waveBtn">Afficher Vague</button>
|
||
|
|
<button class="btn secondary" id="resetBtn">Réinitialiser</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Tooltip -->
|
||
|
|
<div id="tooltip"></div>
|
||
|
|
|
||
|
|
<!-- Legend -->
|
||
|
|
<div class="legend">
|
||
|
|
<h3>Lever Héliaque de Sirius</h3>
|
||
|
|
<div class="legend-item">
|
||
|
|
<div class="legend-icon" style="background: #ffd700;"></div>
|
||
|
|
<span>Sites d'observation</span>
|
||
|
|
</div>
|
||
|
|
<div class="legend-item">
|
||
|
|
<div class="legend-icon" style="background: #64ffda;"></div>
|
||
|
|
<span>Vague de Sirius</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Info Panel -->
|
||
|
|
<div class="info-panel">
|
||
|
|
<h2>Wep Ronpet 2025</h2>
|
||
|
|
<p>Le lever héliaque de Sirius marque le Nouvel An égyptien. Cette visualisation montre la progression de ce phénomène à travers le monde.</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Three.js -->
|
||
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||
|
|
<script>
|
||
|
|
// Configuration
|
||
|
|
const config = {
|
||
|
|
globeRadius: 1,
|
||
|
|
markerSize: 0.08,
|
||
|
|
waveSpeed: 0.001,
|
||
|
|
autoRotateSpeed: 0.0005,
|
||
|
|
cameraDistance: 3.2,
|
||
|
|
colors: {
|
||
|
|
marker: 0xffd700,
|
||
|
|
wave: 0x64ffda,
|
||
|
|
glow: 0xffd700,
|
||
|
|
atmosphere: 0x64b5f6
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Initialize Three.js
|
||
|
|
const renderer = new THREE.WebGLRenderer({
|
||
|
|
antialias: true,
|
||
|
|
alpha: true,
|
||
|
|
powerPreference: "high-performance"
|
||
|
|
});
|
||
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||
|
|
renderer.shadowMap.enabled = true;
|
||
|
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||
|
|
document.body.appendChild(renderer.domElement);
|
||
|
|
|
||
|
|
// Scene setup
|
||
|
|
const scene = new THREE.Scene();
|
||
|
|
scene.fog = new THREE.Fog(0x020c1b, 8, 20);
|
||
|
|
|
||
|
|
// Camera
|
||
|
|
const camera = new THREE.PerspectiveCamera(
|
||
|
|
45,
|
||
|
|
window.innerWidth / window.innerHeight,
|
||
|
|
0.1,
|
||
|
|
1000
|
||
|
|
);
|
||
|
|
camera.position.z = config.cameraDistance;
|
||
|
|
|
||
|
|
// Lights
|
||
|
|
const ambientLight = new THREE.AmbientLight(0x404040, 1.5);
|
||
|
|
scene.add(ambientLight);
|
||
|
|
|
||
|
|
const sunLight = new THREE.DirectionalLight(0xffffff, 1);
|
||
|
|
sunLight.position.set(5, 3, 5);
|
||
|
|
sunLight.castShadow = true;
|
||
|
|
scene.add(sunLight);
|
||
|
|
|
||
|
|
const backLight = new THREE.DirectionalLight(0x64b5f6, 0.3);
|
||
|
|
backLight.position.set(-5, -3, -5);
|
||
|
|
scene.add(backLight);
|
||
|
|
|
||
|
|
// Stars background
|
||
|
|
const starsGeometry = new THREE.BufferGeometry();
|
||
|
|
const starsCount = 5000;
|
||
|
|
const starsPositions = new Float32Array(starsCount * 3);
|
||
|
|
|
||
|
|
for(let i = 0; i < starsCount * 3; i += 3) {
|
||
|
|
const radius = 50 + Math.random() * 450;
|
||
|
|
const theta = Math.random() * Math.PI * 2;
|
||
|
|
const phi = Math.acos(Math.random() * 2 - 1);
|
||
|
|
|
||
|
|
starsPositions[i] = radius * Math.sin(phi) * Math.cos(theta);
|
||
|
|
starsPositions[i + 1] = radius * Math.sin(phi) * Math.sin(theta);
|
||
|
|
starsPositions[i + 2] = radius * Math.cos(phi);
|
||
|
|
}
|
||
|
|
|
||
|
|
starsGeometry.setAttribute('position', new THREE.BufferAttribute(starsPositions, 3));
|
||
|
|
|
||
|
|
const starsMaterial = new THREE.PointsMaterial({
|
||
|
|
color: 0xffffff,
|
||
|
|
size: 0.5,
|
||
|
|
transparent: true,
|
||
|
|
opacity: 0.8,
|
||
|
|
blending: THREE.AdditiveBlending
|
||
|
|
});
|
||
|
|
|
||
|
|
const stars = new THREE.Points(starsGeometry, starsMaterial);
|
||
|
|
scene.add(stars);
|
||
|
|
|
||
|
|
// Globe group
|
||
|
|
const globeGroup = new THREE.Group();
|
||
|
|
scene.add(globeGroup);
|
||
|
|
|
||
|
|
// Texture loader with error handling
|
||
|
|
const loader = new THREE.TextureLoader();
|
||
|
|
const textureUrls = {
|
||
|
|
earth: 'https://raw.githubusercontent.com/jeromeetienne/threex.planets/master/images/earthmap1k.jpg',
|
||
|
|
earthBump: 'https://raw.githubusercontent.com/jeromeetienne/threex.planets/master/images/earthbump1k.jpg',
|
||
|
|
earthSpec: 'https://raw.githubusercontent.com/jeromeetienne/threex.planets/master/images/earthspec1k.jpg'
|
||
|
|
};
|
||
|
|
|
||
|
|
// Sites data with accurate coordinates
|
||
|
|
const sites = [
|
||
|
|
{name: 'La Réunion', lat: -21.1151, lon: 55.5364, date: '27 Juin', time: '04:50', order: 1},
|
||
|
|
{name: 'Kinshasa', lat: -4.4419, lon: 15.2663, date: '16 Juillet', time: '05:20', order: 2},
|
||
|
|
{name: 'Guadeloupe', lat: 16.2650, lon: -61.5510, date: '22 Juillet', time: '05:10', order: 3},
|
||
|
|
{name: 'Martinique', lat: 14.6415, lon: -61.0242, date: '23 Juillet', time: '05:08', order: 4},
|
||
|
|
{name: 'Dakar', lat: 14.7167, lon: -17.4677, date: '23 Juillet', time: '05:15', order: 5},
|
||
|
|
{name: 'Abidjan', lat: 5.3600, lon: -4.0083, date: '28 Juillet', time: '05:25', order: 6},
|
||
|
|
{name: 'Lomé', lat: 6.1375, lon: 1.2123, date: '28 Juillet', time: '05:22', order: 7},
|
||
|
|
{name: 'Accra', lat: 5.6037, lon: -0.1870, date: '28 Juillet', time: '05:23', order: 8},
|
||
|
|
{name: 'Cotonou', lat: 6.3703, lon: 2.3912, date: '28 Juillet', time: '05:24', order: 9},
|
||
|
|
{name: 'Lagos', lat: 6.5244, lon: 3.3792, date: '28 Juillet', time: '05:26', order: 10},
|
||
|
|
{name: 'Yaoundé', lat: 3.8480, lon: 11.5021, date: '29 Juillet', time: '05:28', order: 11},
|
||
|
|
{name: 'Libreville', lat: 0.4162, lon: 9.4673, date: '29 Juillet', time: '05:30', order: 12},
|
||
|
|
{name: 'Brazzaville', lat: -4.2634, lon: 15.2429, date: '29 Juillet', time: '05:32', order: 13},
|
||
|
|
{name: 'Kampala', lat: 0.3476, lon: 32.5825, date: '29 Juillet', time: '05:35', order: 14},
|
||
|
|
{name: 'Nairobi', lat: -1.2921, lon: 36.8219, date: '30 Juillet', time: '05:37', order: 15},
|
||
|
|
{name: 'Addis-Abeba', lat: 9.0320, lon: 38.7466, date: '26 Juillet', time: '05:18', order: 16},
|
||
|
|
{name: 'Le Caire', lat: 30.0444, lon: 31.2357, date: '3 Août', time: '05:00', order: 17},
|
||
|
|
{name: 'Paris', lat: 48.8566, lon: 2.3522, date: '12 Août', time: '05:45', order: 18}
|
||
|
|
];
|
||
|
|
|
||
|
|
// Load textures and create globe
|
||
|
|
Promise.all([
|
||
|
|
new Promise((resolve, reject) => {
|
||
|
|
loader.load(textureUrls.earth, resolve, undefined, reject);
|
||
|
|
}),
|
||
|
|
new Promise((resolve, reject) => {
|
||
|
|
loader.load(textureUrls.earthBump, resolve, undefined, reject);
|
||
|
|
}),
|
||
|
|
new Promise((resolve, reject) => {
|
||
|
|
loader.load(textureUrls.earthSpec, resolve, undefined, reject);
|
||
|
|
})
|
||
|
|
]).then(([earthMap, earthBump, earthSpec]) => {
|
||
|
|
// Hide loading
|
||
|
|
document.getElementById('loading').style.display = 'none';
|
||
|
|
|
||
|
|
// Create globe
|
||
|
|
const globeGeometry = new THREE.SphereGeometry(config.globeRadius, 64, 64);
|
||
|
|
const globeMaterial = new THREE.MeshPhongMaterial({
|
||
|
|
map: earthMap,
|
||
|
|
bumpMap: earthBump,
|
||
|
|
bumpScale: 0.05,
|
||
|
|
specularMap: earthSpec,
|
||
|
|
specular: new THREE.Color(0x444444),
|
||
|
|
shininess: 20
|
||
|
|
});
|
||
|
|
|
||
|
|
const globe = new THREE.Mesh(globeGeometry, globeMaterial);
|
||
|
|
globe.castShadow = true;
|
||
|
|
globe.receiveShadow = true;
|
||
|
|
globeGroup.add(globe);
|
||
|
|
|
||
|
|
// Atmosphere
|
||
|
|
const atmosphereGeometry = new THREE.SphereGeometry(config.globeRadius * 1.1, 32, 32);
|
||
|
|
const atmosphereMaterial = new THREE.ShaderMaterial({
|
||
|
|
vertexShader: `
|
||
|
|
varying vec3 vNormal;
|
||
|
|
void main() {
|
||
|
|
vNormal = normalize(normalMatrix * normal);
|
||
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||
|
|
}
|
||
|
|
`,
|
||
|
|
fragmentShader: `
|
||
|
|
varying vec3 vNormal;
|
||
|
|
void main() {
|
||
|
|
float intensity = pow(0.7 - dot(vNormal, vec3(0.0, 0.0, 1.0)), 2.0);
|
||
|
|
gl_FragColor = vec4(0.3, 0.6, 1.0, 1.0) * intensity;
|
||
|
|
}
|
||
|
|
`,
|
||
|
|
blending: THREE.AdditiveBlending,
|
||
|
|
side: THREE.BackSide
|
||
|
|
});
|
||
|
|
|
||
|
|
const atmosphere = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial);
|
||
|
|
globeGroup.add(atmosphere);
|
||
|
|
|
||
|
|
// Wave system
|
||
|
|
const waveGroup = new THREE.Group();
|
||
|
|
globeGroup.add(waveGroup);
|
||
|
|
|
||
|
|
// Create wave rings
|
||
|
|
const waveRings = [];
|
||
|
|
for(let i = 0; i < 3; i++) {
|
||
|
|
const radius = 1.01 + (i * 0.02); // Légèrement au-dessus du globe
|
||
|
|
const waveGeometry = new THREE.TorusGeometry(radius, 0.05, 16, 100);
|
||
|
|
waveGeometry.rotateX(Math.PI / 2); // Rotation pour que le torus soit horizontal
|
||
|
|
|
||
|
|
const waveMaterial = new THREE.MeshBasicMaterial({
|
||
|
|
color: config.colors.wave,
|
||
|
|
transparent: true,
|
||
|
|
opacity: 0.4 - (i * 0.1),
|
||
|
|
side: THREE.DoubleSide
|
||
|
|
});
|
||
|
|
|
||
|
|
const wave = new THREE.Mesh(waveGeometry, waveMaterial);
|
||
|
|
waveRings.push(wave);
|
||
|
|
waveGroup.add(wave);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Markers
|
||
|
|
const markersGroup = new THREE.Group();
|
||
|
|
globeGroup.add(markersGroup);
|
||
|
|
|
||
|
|
// Create custom marker geometry (simplified ankh shape)
|
||
|
|
const markerShape = new THREE.Shape();
|
||
|
|
markerShape.moveTo(0, -0.5);
|
||
|
|
markerShape.lineTo(0, 0.2);
|
||
|
|
markerShape.moveTo(-0.3, 0);
|
||
|
|
markerShape.lineTo(0.3, 0);
|
||
|
|
markerShape.moveTo(0, 0.2);
|
||
|
|
markerShape.absarc(0, 0.3, 0.2, Math.PI, 0, true);
|
||
|
|
|
||
|
|
const markerGeometry = new THREE.ShapeGeometry(markerShape);
|
||
|
|
|
||
|
|
// Add markers for each site
|
||
|
|
sites.forEach(site => {
|
||
|
|
// Conversion identique à l'exemple
|
||
|
|
const latRad = site.lat * (Math.PI / 180);
|
||
|
|
const lonRad = -site.lon * (Math.PI / 180);
|
||
|
|
|
||
|
|
const x = Math.cos(latRad) * Math.cos(lonRad);
|
||
|
|
const y = Math.sin(latRad);
|
||
|
|
const z = Math.cos(latRad) * Math.sin(lonRad);
|
||
|
|
|
||
|
|
// Marker group
|
||
|
|
const markerGroup = new THREE.Group();
|
||
|
|
|
||
|
|
// Simple sphere marker comme dans l'exemple
|
||
|
|
const markerGeometry = new THREE.SphereGeometry(0.015, 16, 16);
|
||
|
|
const markerMaterial = new THREE.MeshBasicMaterial({
|
||
|
|
color: config.colors.marker,
|
||
|
|
emissive: 0x000000
|
||
|
|
});
|
||
|
|
const marker = new THREE.Mesh(markerGeometry, markerMaterial);
|
||
|
|
|
||
|
|
// Glow effect
|
||
|
|
const glowGeometry = new THREE.SphereGeometry(0.025, 16, 16);
|
||
|
|
const glowMaterial = new THREE.MeshBasicMaterial({
|
||
|
|
color: config.colors.glow,
|
||
|
|
transparent: true,
|
||
|
|
opacity: 0.3,
|
||
|
|
blending: THREE.AdditiveBlending
|
||
|
|
});
|
||
|
|
const glow = new THREE.Mesh(glowGeometry, glowMaterial);
|
||
|
|
|
||
|
|
markerGroup.add(marker);
|
||
|
|
markerGroup.add(glow);
|
||
|
|
|
||
|
|
// Position directement sur le globe (rayon = 1)
|
||
|
|
markerGroup.position.set(x, y, z);
|
||
|
|
|
||
|
|
markerGroup.userData = site;
|
||
|
|
markersGroup.add(markerGroup);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Animation variables
|
||
|
|
let autoRotate = true;
|
||
|
|
let showWave = true;
|
||
|
|
let mouseX = 0, mouseY = 0;
|
||
|
|
let targetRotationX = 0, targetRotationY = 0;
|
||
|
|
|
||
|
|
// Controls
|
||
|
|
const modeBtn = document.getElementById('modeBtn');
|
||
|
|
const waveBtn = document.getElementById('waveBtn');
|
||
|
|
const resetBtn = document.getElementById('resetBtn');
|
||
|
|
|
||
|
|
modeBtn.addEventListener('click', () => {
|
||
|
|
autoRotate = !autoRotate;
|
||
|
|
modeBtn.textContent = autoRotate ? 'Mode Manuel' : 'Mode Auto';
|
||
|
|
});
|
||
|
|
|
||
|
|
waveBtn.addEventListener('click', () => {
|
||
|
|
showWave = !showWave;
|
||
|
|
waveGroup.visible = showWave;
|
||
|
|
waveBtn.textContent = showWave ? 'Masquer Vague' : 'Afficher Vague';
|
||
|
|
});
|
||
|
|
|
||
|
|
resetBtn.addEventListener('click', () => {
|
||
|
|
targetRotationX = 0;
|
||
|
|
targetRotationY = 0;
|
||
|
|
camera.position.z = config.cameraDistance;
|
||
|
|
});
|
||
|
|
|
||
|
|
// Mouse interaction
|
||
|
|
let isDragging = false;
|
||
|
|
const raycaster = new THREE.Raycaster();
|
||
|
|
const mouse = new THREE.Vector2();
|
||
|
|
const tooltip = document.getElementById('tooltip');
|
||
|
|
|
||
|
|
renderer.domElement.addEventListener('mousedown', () => {
|
||
|
|
isDragging = true;
|
||
|
|
renderer.domElement.style.cursor = 'grabbing';
|
||
|
|
});
|
||
|
|
|
||
|
|
window.addEventListener('mouseup', () => {
|
||
|
|
isDragging = false;
|
||
|
|
renderer.domElement.style.cursor = 'grab';
|
||
|
|
});
|
||
|
|
|
||
|
|
window.addEventListener('mousemove', (event) => {
|
||
|
|
if (isDragging && !autoRotate) {
|
||
|
|
targetRotationY += event.movementX * 0.005;
|
||
|
|
targetRotationX += event.movementY * 0.005;
|
||
|
|
targetRotationX = Math.max(-Math.PI/2, Math.min(Math.PI/2, targetRotationX));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update mouse position for raycasting
|
||
|
|
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
||
|
|
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
||
|
|
});
|
||
|
|
|
||
|
|
// Click detection
|
||
|
|
renderer.domElement.addEventListener('click', (event) => {
|
||
|
|
raycaster.setFromCamera(mouse, camera);
|
||
|
|
const intersects = raycaster.intersectObjects(markersGroup.children, true);
|
||
|
|
|
||
|
|
if (intersects.length > 0) {
|
||
|
|
const marker = intersects[0].object.parent;
|
||
|
|
if (marker.userData && marker.userData.name) {
|
||
|
|
const data = marker.userData;
|
||
|
|
tooltip.innerHTML = `
|
||
|
|
<strong>${data.name}</strong>
|
||
|
|
<span class="date">${data.date} à ${data.time}</span>
|
||
|
|
<br><small>Ordre: ${data.order}/${sites.length}</small>
|
||
|
|
`;
|
||
|
|
tooltip.style.left = event.clientX + 10 + 'px';
|
||
|
|
tooltip.style.top = event.clientY - 30 + 'px';
|
||
|
|
tooltip.style.display = 'block';
|
||
|
|
|
||
|
|
setTimeout(() => {
|
||
|
|
tooltip.style.display = 'none';
|
||
|
|
}, 3000);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Zoom with mouse wheel
|
||
|
|
renderer.domElement.addEventListener('wheel', (event) => {
|
||
|
|
event.preventDefault();
|
||
|
|
camera.position.z += event.deltaY * 0.002;
|
||
|
|
camera.position.z = Math.max(2, Math.min(5, camera.position.z));
|
||
|
|
});
|
||
|
|
|
||
|
|
// Animation loop
|
||
|
|
function animate() {
|
||
|
|
requestAnimationFrame(animate);
|
||
|
|
|
||
|
|
// Auto-rotate
|
||
|
|
if (autoRotate) {
|
||
|
|
targetRotationY += config.autoRotateSpeed;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Smooth rotation
|
||
|
|
globeGroup.rotation.y += (targetRotationY - globeGroup.rotation.y) * 0.1;
|
||
|
|
globeGroup.rotation.x += (targetRotationX - globeGroup.rotation.x) * 0.1;
|
||
|
|
|
||
|
|
// Animate wave
|
||
|
|
if (showWave) {
|
||
|
|
const time = Date.now() * 0.0002; // Même vitesse que l'exemple
|
||
|
|
|
||
|
|
// Oscillation entre sud et nord comme dans l'exemple
|
||
|
|
const latAngle = Math.sin(time) * (Math.PI / 2);
|
||
|
|
|
||
|
|
waveRings.forEach((ring, i) => {
|
||
|
|
const offset = i * 0.05; // Petit décalage entre les anneaux
|
||
|
|
|
||
|
|
// Position Y basée sur le sinus de l'angle de latitude
|
||
|
|
ring.position.y = Math.sin(latAngle + offset);
|
||
|
|
|
||
|
|
// Scale basé sur le cosinus pour maintenir la forme circulaire
|
||
|
|
const scale = Math.cos(latAngle + offset);
|
||
|
|
ring.scale.set(scale, 1, scale);
|
||
|
|
|
||
|
|
// Opacité proportionnelle à l'échelle
|
||
|
|
ring.material.opacity = (0.4 - (i * 0.1)) * Math.max(0.2, Math.abs(scale));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Rotate stars slowly
|
||
|
|
stars.rotation.y += 0.0001;
|
||
|
|
|
||
|
|
// Animate markers
|
||
|
|
markersGroup.children.forEach((marker, i) => {
|
||
|
|
const time = Date.now() * 0.001;
|
||
|
|
const offset = i * 0.2;
|
||
|
|
const scale = 1 + Math.sin(time + offset) * 0.1;
|
||
|
|
marker.scale.setScalar(scale);
|
||
|
|
});
|
||
|
|
|
||
|
|
renderer.render(scene, camera);
|
||
|
|
}
|
||
|
|
|
||
|
|
animate();
|
||
|
|
|
||
|
|
}).catch(error => {
|
||
|
|
console.error('Error loading textures:', error);
|
||
|
|
document.getElementById('loading').innerHTML = `
|
||
|
|
<div style="color: #ff6b6b;">Erreur de chargement</div>
|
||
|
|
<div style="color: #8892b0; margin-top: 10px;">Vérifiez votre connexion internet</div>
|
||
|
|
`;
|
||
|
|
});
|
||
|
|
|
||
|
|
// Handle window resize
|
||
|
|
window.addEventListener('resize', () => {
|
||
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
||
|
|
camera.updateProjectionMatrix();
|
||
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Initial cursor style
|
||
|
|
renderer.domElement.style.cursor = 'grab';
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|