Initial commit: Sirius en Guadeloupe - Application web multilingue

This commit is contained in:
sucupira
2025-07-09 19:56:54 -04:00
parent 0813a32e24
commit b1b1d452c7
20 changed files with 2530 additions and 228 deletions
+10 -4
View File
@@ -1,5 +1,6 @@
<script>
import { onMount } from 'svelte';
import { initLanguage } from './lib/i18n.js';
import Hero from './components/Hero.svelte';
import Navigation from './components/Navigation.svelte';
import About from './components/About.svelte';
@@ -7,15 +8,19 @@
import Science from './components/Science.svelte';
import Culture from './components/Culture.svelte';
import Observatory from './components/Observatory.svelte';
import Globe from './components/Globe.svelte';
import GlobeOpenGlobus from './components/GlobeOpenGlobus.svelte';
import GlobeGeoJSON from './components/GlobeGeoJSON.svelte';
import Footer from './components/Footer.svelte';
import Dogon from './components/Dogon.svelte';
import Calculations from './components/Calculations.svelte';
import ObservationMap from './components/ObservationMap.svelte';
let scrollY = 0;
let activeSection = 'home';
onMount(() => {
// Initialiser la langue
initLanguage();
const handleScroll = () => {
scrollY = window.scrollY;
@@ -45,10 +50,11 @@
<About />
<Predictions />
<Science />
<Calculations />
<Dogon />
<Culture />
<Observatory />
<Globe />
<GlobeOpenGlobus />
<ObservationMap />
<GlobeGeoJSON />
</main>
+14 -2
View File
@@ -1,5 +1,6 @@
<script>
import { onMount } from 'svelte';
import { t } from '../lib/i18n.js';
let visible = false;
@@ -71,8 +72,8 @@
{#each features as feature, i}
<div class="feature-card card" style="animation-delay: {i * 0.2}s">
<div class="feature-icon">{feature.icon}</div>
<h3>{feature.title}</h3>
<p>{feature.description}</p>
<h3>{$t(feature.title)}</h3>
<p>{$t(feature.description)}</p>
</div>
{/each}
</div>
@@ -165,6 +166,17 @@
margin-bottom: 0.5rem;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.features-grid {
grid-template-columns: 1fr;
+230
View File
@@ -0,0 +1,230 @@
<script>
import { onMount } from 'svelte';
let visible = false;
let selectedCalculation = 'heliacal';
onMount(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
visible = true;
}
});
});
const section = document.getElementById('calculations');
if (section) observer.observe(section);
return () => observer.disconnect();
});
const calculations = {
heliacal: {
title: 'Lever Héliaque',
description: 'Calcul de la première visibilité de Sirius avant l\'aube'
},
arcus: {
title: 'Arcus Visionis',
description: 'Angle minimal entre le Soleil et Sirius pour la visibilité'
}
};
</script>
<section id="calculations" class="calculations-section">
<div class="container">
<div class="section-header" class:visible>
<h2>Calculs Astronomiques</h2>
<p class="section-subtitle">
La science précise derrière le lever héliaque
</p>
</div>
<div class="calculations-wrapper" class:visible>
<!-- Sélecteur de calcul -->
<div class="calc-selector">
{#each Object.entries(calculations) as [key, calc]}
<button
class="calc-btn"
class:active={selectedCalculation === key}
on:click={() => selectedCalculation = key}
>
<h4>{calc.title}</h4>
<p>{calc.description}</p>
</button>
{/each}
</div>
<!-- Contenu des calculs -->
<div class="calc-content">
{#if selectedCalculation === 'heliacal'}
<div class="calc-panel">
<h3>Calcul du Lever Héliaque de Sirius</h3>
<div class="formula-box">
<h4>Formule Principale</h4>
<div class="formula">
<p><strong>Date du lever héliaque = </strong></p>
<p>Date de conjonction + Arcus Visionis / Vitesse angulaire du Soleil</p>
</div>
</div>
<div class="calculation-steps">
<h4>Étapes du Calcul pour la Guadeloupe</h4>
<div class="step">
<div class="step-number">1</div>
<div class="step-content">
<h5>Coordonnées de la Guadeloupe</h5>
<code>Latitude: 16.25°N, Longitude: 61.58°O</code>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
<h5>Position de Sirius</h5>
<code>Ascension droite: 6h 45m 09s</code>
<code>Déclinaison: -16° 42' 58"</code>
</div>
</div>
</div>
</div> {/if}
{#if selectedCalculation === 'arcus'}
<div class="calc-panel">
<h3>Arcus Visionis - L'Arc de Vision</h3>
<div class="explanation">
<p>
L'Arcus Visionis est l'angle minimal entre le Soleil et une étoile
pour que celle-ci devienne visible.
</p>
</div>
</div>
{/if}
</div>
</div>
</div>
</section>
<style>
.calculations-section {
padding: var(--spacing-xl) 0;
background: var(--gradient-dawn);
}
.calculations-wrapper {
margin-top: var(--spacing-lg);
opacity: 0;
transform: translateY(30px);
transition: all 0.8s ease-out;
}
.calculations-wrapper.visible {
opacity: 1;
transform: translateY(0);
}
.calc-selector {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.calc-btn {
background: rgba(255, 255, 255, 0.05);
border: 2px solid transparent;
border-radius: 15px;
padding: 1.5rem;
cursor: pointer;
transition: all 0.3s ease;
text-align: left;
}
.calc-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: var(--color-primary);
transform: translateY(-2px);
}
.calc-btn.active {
background: rgba(255, 215, 0, 0.1);
border-color: var(--color-primary);
}
.calc-btn h4 {
color: var(--color-primary);
margin: 0 0 0.5rem 0;
}
.calc-btn p {
color: var(--color-light);
margin: 0;
font-size: 0.9rem;
opacity: 0.8;
}
.calc-panel {
background: rgba(255, 255, 255, 0.05);
border-radius: 20px;
padding: 2rem;
}
.formula-box {
background: rgba(0, 0, 0, 0.3);
border-radius: 15px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.formula {
font-family: 'Courier New', monospace;
color: var(--color-light);
margin: 1rem 0;
}
.calculation-steps {
margin: 2rem 0;
}
.step {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
align-items: flex-start;
}
.step-number {
background: var(--color-primary);
color: var(--color-dark);
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
flex-shrink: 0;
}
.step-content h5 {
color: var(--color-secondary);
margin: 0 0 0.5rem 0;
}
.step-content code {
display: block;
background: rgba(0, 0, 0, 0.3);
padding: 0.5rem 1rem;
border-radius: 8px;
margin: 0.25rem 0;
font-family: 'Courier New', monospace;
}
@media (max-width: 768px) {
.calc-selector {
grid-template-columns: 1fr;
}
.calc-panel {
padding: 1rem;
}
}
</style>
+11
View File
@@ -224,6 +224,17 @@
color: rgba(255, 255, 255, 0.7);
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.timeline-items {
padding-left: 20px;
+587
View File
@@ -0,0 +1,587 @@
<script>
import { onMount } from 'svelte';
let visible = false;
let activeTab = 'cosmogonie';
onMount(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
visible = true;
}
});
});
const section = document.getElementById('dogon');
if (section) observer.observe(section);
return () => observer.disconnect();
});
const tabs = [
{ id: 'cosmogonie', label: 'Cosmogonie', icon: '🌌' },
{ id: 'sirius', label: 'Système de Sirius', icon: '⭐' },
{ id: 'astronomie', label: 'Astronomie Dogon', icon: '🔭' },
{ id: 'heritage', label: 'Héritage', icon: '🌍' }
];
</script>
<section id="dogon" class="dogon-section">
<div class="container">
<div class="section-header" class:visible>
<h2>Les Dogons et Sirius</h2>
<p class="section-subtitle">
Une connaissance astronomique millénaire
</p>
</div>
<div class="content-wrapper" class:visible>
<!-- Navigation par onglets -->
<div class="tabs-nav">
{#each tabs as tab}
<button
class="tab-btn"
class:active={activeTab === tab.id}
on:click={() => activeTab = tab.id}
>
<span class="tab-icon">{tab.icon}</span>
<span class="tab-label">{tab.label}</span>
</button>
{/each}
</div>
<!-- Contenu des onglets -->
<div class="tabs-content">
{#if activeTab === 'cosmogonie'}
<div class="tab-panel">
<h3>La Cosmogonie Dogon</h3>
<div class="content-grid">
<div class="content-text">
<p>
Les Dogons du Mali possèdent une cosmogonie complexe centrée sur <strong>Sigu Tolo</strong> (Sirius),
l'étoile la plus brillante du ciel. Leur savoir astronomique, transmis oralement depuis des millénaires,
révèle une compréhension sophistiquée du cosmos.
</p>
<p>
Le <strong>Sigui</strong>, cérémonie célébrée tous les 60 ans, marque le renouvellement du monde
et commémore la révélation de la parole aux hommes. Cette périodicité correspond étrangement
à l'orbite de Sirius B autour de Sirius A (50 ans), ajustée par des considérations rituelles.
</p>
<div class="highlight-box">
<h4>Le Nommo</h4>
<p>
Être primordial amphibie venu de Sirius, le Nommo apporta la civilisation
et les connaissances fondamentales aux ancêtres des Dogons.
</p>
</div>
</div>
<div class="content-visual">
<div class="cosmogony-diagram">
<svg viewBox="0 0 400 400" class="diagram-svg">
<!-- Amma (Dieu créateur) au centre -->
<circle cx="200" cy="200" r="30" fill="var(--color-primary)" opacity="0.8"/>
<text x="200" y="205" text-anchor="middle" fill="white" font-weight="bold">Amma</text>
<!-- Oeuf cosmique -->
<ellipse cx="200" cy="200" rx="150" ry="100" fill="none" stroke="var(--color-secondary)" stroke-width="2" stroke-dasharray="5,5" opacity="0.6"/>
<!-- Les 4 éléments -->
<g class="elements">
<circle cx="200" cy="100" r="25" fill="var(--color-accent)"/>
<text x="200" y="105" text-anchor="middle" fill="white" font-size="12">Eau</text>
<circle cx="300" cy="200" r="25" fill="#FF6B35"/>
<text x="300" y="205" text-anchor="middle" fill="white" font-size="12">Feu</text>
<circle cx="200" cy="300" r="25" fill="#4CAF50"/>
<text x="200" y="305" text-anchor="middle" fill="white" font-size="12">Terre</text>
<circle cx="100" cy="200" r="25" fill="#87CEEB"/>
<text x="100" y="205" text-anchor="middle" fill="white" font-size="12">Air</text>
</g>
<!-- Lignes de connexion -->
<line x1="200" y1="230" x2="200" y2="275" stroke="var(--color-light)" stroke-width="1" opacity="0.5"/>
<line x1="225" y1="200" x2="275" y2="200" stroke="var(--color-light)" stroke-width="1" opacity="0.5"/>
<line x1="200" y1="175" x2="200" y2="125" stroke="var(--color-light)" stroke-width="1" opacity="0.5"/>
<line x1="175" y1="200" x2="125" y2="200" stroke="var(--color-light)" stroke-width="1" opacity="0.5"/>
</svg>
</div>
</div>
</div>
</div>
{/if}
{#if activeTab === 'sirius'}
<div class="tab-panel">
<h3>Le Système de Sirius selon les Dogons</h3>
<div class="sirius-system">
<div class="star-info">
<h4>Sigu Tolo (Sirius A)</h4>
<p>L'étoile principale, source de lumière et de vie</p>
<ul>
<li>Associée à la féminité et à la fécondité</li>
<li>Guide des saisons et des récoltes</li>
<li>Centre du système stellaire</li>
</ul>
</div>
<div class="star-info">
<h4>Po Tolo (Sirius B)</h4>
<p>L'étoile compagne invisible à l'œil nu</p>
<ul>
<li>Décrite comme "lourde" et "blanche"</li>
<li>Orbite de 50 ans autour de Sirius A</li>
<li>Découverte par les astronomes occidentaux en 1862</li>
</ul>
</div>
<div class="star-info">
<h4>Emme Ya Tolo (Sirius C ?)</h4>
<p>Troisième étoile hypothétique du système</p>
<ul>
<li>Quatre fois plus légère que Po Tolo</li>
<li>Orbite plus large</li>
<li>Non confirmée par l'astronomie moderne</li>
</ul>
</div>
</div>
<div class="knowledge-mystery">
<h4>Le Mystère de la Connaissance</h4>
<p>
Comment les Dogons connaissaient-ils l'existence de Sirius B,
invisible à l'œil nu et découverte en Occident seulement en 1862 ?
</p>
<div class="theories">
<div class="theory-card">
<h5>Transmission Ancienne</h5>
<p>Héritage d'une civilisation antérieure avancée</p>
</div>
<div class="theory-card">
<h5>Contact Extraterrestre</h5>
<p>Les Nommos venus de Sirius</p>
</div>
<div class="theory-card">
<h5>Observation Minutieuse</h5>
<p>Déduction à partir des perturbations de Sirius A</p>
</div>
</div>
</div>
</div>
{/if}
{#if activeTab === 'astronomie'}
<div class="tab-panel">
<h3>L'Astronomie Dogon</h3>
<div class="astronomy-content">
<div class="concept-grid">
<div class="concept-card">
<h4>📐 Géométrie Sacrée</h4>
<p>
Les Dogons utilisent des figures géométriques complexes pour
représenter les mouvements célestes et les cycles cosmiques.
</p>
</div>
<div class="concept-card">
<h4>🌙 Calendrier Lunaire</h4>
<p>
Un système calendaire sophistiqué basé sur les phases lunaires
et la position de Sirius pour l'agriculture.
</p>
</div>
<div class="concept-card">
<h4>🪐 Connaissance Planétaire</h4>
<p>
Description des anneaux de Saturne et des lunes de Jupiter,
invisibles sans télescope.
</p>
</div>
<div class="concept-card">
<h4>🌌 Structure de l'Univers</h4>
<p>
Concept d'univers infini avec des mondes multiples,
similaire aux théories modernes.
</p>
</div>
</div>
<div class="calculation-example">
<h4>Calcul de la Période Sigui</h4>
<pre><code>Période orbitale Po Tolo : 50 ans
Ajustement rituel : × 1.2
Période Sigui : 60 ans
Correspondance avec Sirius B :
- Orbite réelle : 50.1 ans
- Précision : 99.8%</code></pre>
</div>
</div>
</div>
{/if}
{#if activeTab === 'heritage'}
<div class="tab-panel">
<h3>Héritage et Influence</h3>
<div class="heritage-content">
<div class="timeline">
<div class="timeline-item">
<div class="timeline-date">Antiquité</div>
<div class="timeline-content">
<h4>Origines Égyptiennes</h4>
<p>Liens possibles avec les prêtres de l'Égypte ancienne qui vénéraient Sothis (Sirius)</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-date">XIIIe siècle</div>
<div class="timeline-content">
<h4>Migration vers le Mali</h4>
<p>Installation dans les falaises de Bandiagara, préservation des traditions</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-date">1931-1956</div>
<div class="timeline-content">
<h4>Marcel Griaule</h4>
<p>Étude ethnographique révélant les connaissances astronomiques dogons</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-date">Aujourd'hui</div>
<div class="timeline-content">
<h4>Patrimoine Mondial</h4>
<p>Les Dogons inspirent scientifiques et chercheurs du monde entier</p>
</div>
</div>
</div>
<div class="influence-map">
<h4>Influence sur la Caraïbe</h4>
<p>
Les traditions astronomiques africaines, incluant celles des Dogons,
ont traversé l'Atlantique avec la diaspora, enrichissant les pratiques
spirituelles et calendaires des Antilles.
</p>
<ul>
<li>🇭🇹 Haïti : Vaudou et astronomie sacrée</li>
<li>🇬🇵 Guadeloupe : Traditions Gwoka et cycles cosmiques</li>
<li>🇲🇶 Martinique : Bèlè et rythmes stellaires</li>
<li>🇯🇲 Jamaïque : Rastafari et conscience cosmique</li>
</ul>
</div>
</div>
</div>
{/if}
</div>
</div>
</div>
</section>
<style>
.dogon-section {
padding: var(--spacing-xl) 0;
background: var(--gradient-night);
}
.content-wrapper {
margin-top: var(--spacing-lg);
opacity: 0;
transform: translateY(30px);
transition: all 0.8s ease-out;
}
.content-wrapper.visible {
opacity: 1;
transform: translateY(0);
}
/* Onglets */
.tabs-nav {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
justify-content: center;
}
.tab-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
background: rgba(255, 255, 255, 0.1);
border: 2px solid transparent;
border-radius: 50px;
color: var(--color-light);
cursor: pointer;
transition: all 0.3s ease;
}
.tab-btn:hover {
background: rgba(255, 255, 255, 0.15);
border-color: var(--color-primary);
}
.tab-btn.active {
background: var(--color-primary);
color: var(--color-dark);
border-color: var(--color-primary);
}
.tab-icon {
font-size: 1.2rem;
}
/* Contenu des onglets */
.tab-panel {
animation: fadeIn 0.5s ease-out;
}
.content-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-lg);
align-items: start;
}
.highlight-box {
background: rgba(255, 215, 0, 0.1);
border-left: 4px solid var(--color-primary);
padding: 1rem;
margin: 1.5rem 0;
border-radius: 0 8px 8px 0;
}
.highlight-box h4 {
color: var(--color-primary);
margin-bottom: 0.5rem;
}
/* Diagramme cosmogonie */
.cosmogony-diagram {
background: rgba(255, 255, 255, 0.05);
border-radius: 20px;
padding: 2rem;
}
.diagram-svg {
width: 100%;
height: auto;
}
.elements circle {
animation: pulse 3s ease-in-out infinite;
}
.elements circle:nth-child(2) { animation-delay: 0.5s; }
.elements circle:nth-child(4) { animation-delay: 1s; }
.elements circle:nth-child(6) { animation-delay: 1.5s; }
/* Système de Sirius */
.sirius-system {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.star-info {
background: rgba(255, 255, 255, 0.05);
padding: 1.5rem;
border-radius: 15px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.star-info h4 {
color: var(--color-primary);
margin-bottom: 0.5rem;
}
.star-info ul {
list-style: none;
padding: 0;
margin-top: 1rem;
}
.star-info li {
padding: 0.25rem 0;
padding-left: 1.5rem;
position: relative;
}
.star-info li::before {
content: "✦";
position: absolute;
left: 0;
color: var(--color-primary);
}
/* Mystère de la connaissance */
.knowledge-mystery {
background: rgba(255, 215, 0, 0.05);
padding: 2rem;
border-radius: 15px;
margin-top: 2rem;
}
.theories {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-top: 1.5rem;
}
.theory-card {
background: rgba(255, 255, 255, 0.1);
padding: 1rem;
border-radius: 10px;
text-align: center;
}
.theory-card h5 {
color: var(--color-secondary);
margin-bottom: 0.5rem;
}
/* Astronomie */
.concept-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.concept-card {
background: rgba(255, 255, 255, 0.05);
padding: 1.5rem;
border-radius: 15px;
transition: transform 0.3s ease;
}
.concept-card:hover {
transform: translateY(-5px);
}
.concept-card h4 {
color: var(--color-primary);
margin-bottom: 0.5rem;
}
.calculation-example {
background: rgba(0, 0, 0, 0.3);
padding: 1.5rem;
border-radius: 10px;
}
.calculation-example pre {
margin: 0;
color: var(--color-light);
font-family: 'Courier New', monospace;
}
/* Timeline */
.timeline {
position: relative;
padding-left: 3rem;
}
.timeline::before {
content: '';
position: absolute;
left: 1rem;
top: 0;
bottom: 0;
width: 2px;
background: var(--color-primary);
}
.timeline-item {
position: relative;
margin-bottom: 2rem;
}
.timeline-item::before {
content: '';
position: absolute;
left: -2.5rem;
top: 0.5rem;
width: 1rem;
height: 1rem;
background: var(--color-primary);
border-radius: 50%;
}
.timeline-date {
color: var(--color-primary);
font-weight: bold;
margin-bottom: 0.5rem;
}
.timeline-content h4 {
color: var(--color-secondary);
margin-bottom: 0.25rem;
}
/* Carte d'influence */
.influence-map {
background: rgba(255, 255, 255, 0.05);
padding: 1.5rem;
border-radius: 15px;
margin-top: 2rem;
}
.influence-map ul {
list-style: none;
padding: 0;
margin-top: 1rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 0.5rem;
}
.influence-map li {
padding: 0.5rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 0.8; }
50% { transform: scale(1.1); opacity: 1; }
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 768px) {
.content-grid {
grid-template-columns: 1fr;
}
.tabs-nav {
gap: 0.5rem;
}
.tab-btn {
padding: 0.5rem 1rem;
font-size: 0.9rem;
}
.tab-label {
display: none;
}
.timeline {
padding-left: 2rem;
}
}
</style>
+30
View File
@@ -1,4 +1,5 @@
<script>
import { t } from '../lib/i18n.js';
const currentYear = new Date().getFullYear();
</script>
@@ -57,6 +58,14 @@
<div class="footer-bottom">
<p>&copy; {currentYear} Sirius Gwada. Levez les yeux vers le ciel.</p>
<p class="footer-credits">
{$t('footer_copyright')}
<a href="https://github.com/cyber-mawonaj" target="_blank" rel="noopener">cyber-mawonaj</a>
{$t('footer_and')}
<a href="https://o-k-i.net" target="_blank" rel="noopener">
OKI <img src="https://o-k-i.net/assets/img/logo-oki.png" alt="OKI Logo" class="oki-logo">
</a>
</p>
<p class="footer-quote">
"Les étoiles sont les ancêtres qui veillent sur nous" - Proverbe africain
</p>
@@ -121,6 +130,27 @@
border-top: 1px solid rgba(255, 215, 0, 0.2);
}
.footer-credits {
margin: 0.5rem 0;
color: rgba(255, 255, 255, 0.8);
}
.footer-credits a {
color: var(--color-primary);
text-decoration: none;
transition: opacity 0.3s ease;
}
.footer-credits a:hover {
opacity: 0.8;
}
.oki-logo {
height: 20px;
vertical-align: middle;
margin-left: 5px;
}
.footer-quote {
margin-top: 0.5rem;
font-style: italic;
+1 -1
View File
@@ -264,7 +264,7 @@
// Rotation automatique
if (!isDragging) {
rotation += 0.002;
rotation -= 0.002;
}
requestAnimationFrame(draw);
@@ -29,7 +29,7 @@
async function initGlobe() {
// Charger OpenGlobus depuis CDN
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/@openglobus/og@0.10.0/dist/og.umd.js';
script.src = 'https://unpkg.com/@openglobus/og@0.10.0/dist/og.umd.js';
script.onload = () => {
const { Globe, XYZ, Vector, Entity, Label } = window.og;
+271
View File
@@ -0,0 +1,271 @@
<script>
import { onMount } from 'svelte';
let container;
let visible = false;
let animationId;
let scene, camera, renderer, globe, markers;
onMount(async () => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !scene) {
visible = true;
initGlobe();
}
});
});
const section = document.getElementById('threejs-section');
if (section) observer.observe(section);
return () => {
observer.disconnect();
if (animationId) {
cancelAnimationFrame(animationId);
}
if (renderer) {
renderer.dispose();
}
};
});
async function initGlobe() {
// Charger Three.js depuis CDN
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js';
script.onload = () => {
createThreeJSGlobe();
};
document.head.appendChild(script);
}
function createThreeJSGlobe() {
const THREE = window.THREE;
// Scène
scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a1628);
// Caméra
camera = new THREE.PerspectiveCamera(
45,
container.clientWidth / container.clientHeight,
0.1,
1000
);
camera.position.z = 2.5;
// Renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
// Lumières
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 3, 5);
scene.add(directionalLight);
// Globe
const geometry = new THREE.SphereGeometry(1, 64, 64);
// Texture de la Terre
const textureLoader = new THREE.TextureLoader();
const earthTexture = textureLoader.load(
'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/planets/earth_atmos_2048.jpg',
() => { renderer.render(scene, camera); }
);
const material = new THREE.MeshPhongMaterial({
map: earthTexture,
bumpScale: 0.05,
specular: new THREE.Color('grey'),
shininess: 10
});
globe = new THREE.Mesh(geometry, material);
scene.add(globe);
// Atmosphère
const atmosphereGeometry = new THREE.SphereGeometry(1.1, 64, 64);
const atmosphereMaterial = new THREE.MeshPhongMaterial({
color: 0x4444ff,
transparent: true,
opacity: 0.1,
side: THREE.BackSide
});
const atmosphere = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial);
scene.add(atmosphere);
// Marqueurs
markers = new THREE.Group();
scene.add(markers);
const locations = [
{ name: 'La Réunion', lat: -21.1151, lon: 55.5364, color: 0xFF6B35 },
{ name: 'Kinshasa', lat: -4.4419, lon: 15.2663, color: 0xFFA500 },
{ name: 'Guadeloupe', lat: 16.2650, lon: -61.5510, color: 0xFFD700 },
{ name: 'Martinique', lat: 14.6415, lon: -61.0242, color: 0xFFD700 },
{ name: 'Dakar', lat: 14.7167, lon: -17.4677, color: 0xFFE44D },
{ name: 'Le Caire', lat: 30.0444, lon: 31.2357, color: 0xFFFACD }
];
locations.forEach(loc => {
const markerGeometry = new THREE.SphereGeometry(0.02, 16, 16);
const markerMaterial = new THREE.MeshBasicMaterial({ color: loc.color });
const marker = new THREE.Mesh(markerGeometry, markerMaterial);
// Convertir lat/lon en position 3D
const phi = (90 - loc.lat) * (Math.PI / 180);
const theta = (loc.lon + 180) * (Math.PI / 180);
marker.position.x = -1.05 * Math.sin(phi) * Math.cos(theta);
marker.position.y = 1.05 * Math.cos(phi);
marker.position.z = 1.05 * Math.sin(phi) * Math.sin(theta);
markers.add(marker);
});
// Contrôles de la souris
let mouseX = 0, mouseY = 0;
let isMouseDown = false;
container.addEventListener('mousedown', () => { isMouseDown = true; });
container.addEventListener('mouseup', () => { isMouseDown = false; });
container.addEventListener('mouseleave', () => { isMouseDown = false; });
container.addEventListener('mousemove', (e) => {
if (isMouseDown) {
mouseX = (e.clientX - container.offsetLeft) / container.clientWidth * 2 - 1;
mouseY = -(e.clientY - container.offsetTop) / container.clientHeight * 2 + 1;
}
});
// Animation
function animate() {
animationId = requestAnimationFrame(animate);
// Rotation automatique
if (!isMouseDown) {
globe.rotation.y += 0.002;
markers.rotation.y += 0.002;
} else {
// Rotation manuelle
globe.rotation.y += mouseX * 0.05;
globe.rotation.x = mouseY * 0.5;
markers.rotation.y = globe.rotation.y;
markers.rotation.x = globe.rotation.x;
}
renderer.render(scene, camera);
}
animate();
// Responsive
window.addEventListener('resize', () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
});
}
</script>
<section id="threejs-section" class="globe-section">
<div class="container">
<div class="section-header" class:visible>
<h2>Globe avec Three.js</h2>
<p class="section-subtitle">
Rendu 3D WebGL avec textures réalistes
</p>
</div>
<div class="globe-container threejs" class:visible>
<div bind:this={container} class="threejs-container"></div>
<div class="globe-info">
<div class="info-card card">
<h3>Three.js</h3>
<p><strong>Avantages :</strong></p>
<ul>
<li>✅ Rendu 3D réaliste</li>
<li>✅ Textures haute qualité</li>
<li>✅ Effets atmosphériques</li>
<li>✅ Très bien documenté</li>
</ul>
<p><strong>Inconvénients :</strong></p>
<ul>
<li>❌ Plus lourd (~150KB)</li>
<li>❌ Nécessite WebGL</li>
<li>❌ Plus complexe à personnaliser</li>
</ul>
</div>
</div>
</div>
</div>
</section>
<style>
.globe-section {
padding: var(--spacing-xl) 0;
background: var(--gradient-dawn);
}
.globe-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-lg);
align-items: center;
margin-top: var(--spacing-lg);
opacity: 0;
transform: translateY(30px);
transition: all 0.8s ease-out 0.3s;
}
.globe-container.visible {
opacity: 1;
transform: translateY(0);
}
.threejs-container {
width: 100%;
height: 500px;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
cursor: grab;
}
.threejs-container:active {
cursor: grabbing;
}
.info-card h3 {
color: var(--color-primary);
margin-bottom: 1rem;
}
.info-card ul {
list-style: none;
padding: 0;
margin: 0.5rem 0;
}
.info-card li {
padding: 0.25rem 0;
}
@media (max-width: 768px) {
.globe-container {
grid-template-columns: 1fr;
}
.threejs-container {
height: 400px;
}
}
</style>
+181 -141
View File
@@ -1,123 +1,132 @@
<script>
import { onMount } from 'svelte';
import { t } from '../lib/i18n.js';
let starsCanvas;
let mouseX = 0;
let mouseY = 0;
let mouseX = 0.5;
let mouseY = 0.5;
onMount(() => {
const ctx = starsCanvas.getContext('2d');
const stars = [];
const starCount = 150;
// Créer les étoiles
for (let i = 0; i < 200; i++) {
stars.push({
x: Math.random() * starsCanvas.width,
y: Math.random() * starsCanvas.height,
size: Math.random() * 2,
speed: Math.random() * 0.5 + 0.1,
brightness: Math.random()
});
}
// Animation des étoiles
const animate = () => {
ctx.clearRect(0, 0, starsCanvas.width, starsCanvas.height);
stars.forEach(star => {
// Effet parallaxe avec la souris
const offsetX = (mouseX - 0.5) * star.speed * 20;
const offsetY = (mouseY - 0.5) * star.speed * 20;
// Attendre que le DOM soit prêt
setTimeout(() => {
// Redimensionner le canvas
const resize = () => {
const hero = starsCanvas.parentElement;
starsCanvas.width = hero.offsetWidth;
starsCanvas.height = hero.offsetHeight;
ctx.beginPath();
ctx.arc(
star.x + offsetX,
star.y + offsetY,
star.size,
0,
Math.PI * 2
);
ctx.fillStyle = `rgba(255, 215, 0, ${star.brightness})`;
ctx.fill();
// Animer la brillance
star.brightness += (Math.random() - 0.5) * 0.05;
star.brightness = Math.max(0.1, Math.min(1, star.brightness));
});
// Réinitialiser les étoiles après redimensionnement
if (stars.length === 0) {
for (let i = 0; i < starCount; i++) {
stars.push({
x: Math.random() * starsCanvas.width,
y: Math.random() * starsCanvas.height,
size: Math.random() * 2,
brightness: Math.random(),
speed: 0.1 + Math.random() * 0.5
});
}
}
};
requestAnimationFrame(animate);
};
// Redimensionner le canvas
const resize = () => {
starsCanvas.width = window.innerWidth;
starsCanvas.height = window.innerHeight;
};
resize();
animate();
window.addEventListener('resize', resize);
return () => {
window.removeEventListener('resize', resize);
};
resize();
window.addEventListener('resize', resize);
// Animation des étoiles
const animate = () => {
ctx.clearRect(0, 0, starsCanvas.width, starsCanvas.height);
// Effet de parallaxe avec la souris
const offsetX = (mouseX - 0.5) * 20;
const offsetY = (mouseY - 0.5) * 20;
stars.forEach(star => {
// Mouvement subtil
star.x += Math.sin(Date.now() * 0.0001 * star.speed) * 0.5;
star.y += Math.cos(Date.now() * 0.0001 * star.speed) * 0.3;
ctx.beginPath();
ctx.arc(
star.x + offsetX,
star.y + offsetY,
star.size,
0,
Math.PI * 2
);
ctx.fillStyle = `rgba(255, 215, 0, ${star.brightness})`;
ctx.fill();
// Animer la brillance
star.brightness += (Math.random() - 0.5) * 0.05;
star.brightness = Math.max(0.1, Math.min(1, star.brightness));
});
requestAnimationFrame(animate);
};
animate();
return () => {
window.removeEventListener('resize', resize);
};
}, 100);
});
const handleMouseMove = (e) => {
mouseX = e.clientX / window.innerWidth;
mouseY = e.clientY / window.innerHeight;
const rect = e.currentTarget.getBoundingClientRect();
mouseX = (e.clientX - rect.left) / rect.width;
mouseY = (e.clientY - rect.top) / rect.height;
};
</script>
<section id="home" class="hero" on:mousemove={handleMouseMove}>
<canvas bind:this={starsCanvas} class="stars-canvas"></canvas>
<div class="hero-pattern pattern-adinkra"></div>
<div class="hero-pattern">
<!-- Patterns africains visibles -->
<div class="pattern-overlay pattern-kente"></div>
<div class="pattern-overlay pattern-adinkra"></div>
<div class="pattern-overlay pattern-bogolan"></div>
</div>
<div class="hero-content">
<div class="container">
<div class="hero-text">
<h1 class="hero-title">
<span class="title-line">Sirius</span>
<span class="title-line">en Guadeloupe</span>
<span class="title-line">{$t('hero_title1')}</span>
<span class="title-line">{$t('hero_title2')}</span>
</h1>
<p class="hero-subtitle">
Wep Ronpet - L'Ouverture de l'Année Cosmique
{$t('hero_subtitle')}
</p>
<p class="hero-description">
Le rendez-vous millénaire entre Ciel, Science et Culture
{$t('hero_description')}
</p>
<p class="hero-date">
Lever héliaque : <strong>22 Juillet 2025</strong>
{$t('hero_date')}<strong>{$t('hero_date_value')}</strong>
</p>
<div class="hero-cta">
<a href="#predictions" class="btn btn-primary">
Découvrir les Prédictions
{$t('hero_cta_predictions')}
</a>
<a href="#about" class="btn btn-outline">
En Savoir Plus
{$t('hero_cta_learn')}
</a>
</div>
</div>
<div class="hero-visual">
<div class="sirius-glow"></div>
<div class="ankh-symbol"></div>
</div>
</div>
</div>
<div class="scroll-indicator">
<span>Défiler pour explorer</span>
<div class="scroll-arrow"></div>
</div>
</section>
<style>
.hero {
position: relative;
min-height: 100vh;
background: var(--gradient-dawn);
display: flex;
align-items: center;
overflow: hidden;
@@ -130,6 +139,7 @@
width: 100%;
height: 100%;
z-index: 1;
pointer-events: none;
}
.hero-pattern {
@@ -138,36 +148,107 @@
left: 0;
width: 100%;
height: 100%;
opacity: 0.1;
z-index: 2;
pointer-events: none;
}
.pattern-overlay {
position: absolute;
width: 100%;
height: 100%;
opacity: 0.1;
background-size: 200px 200px;
mix-blend-mode: overlay;
}
.pattern-kente {
background-image:
repeating-linear-gradient(
45deg,
var(--color-primary) 0px,
var(--color-primary) 10px,
transparent 10px,
transparent 20px,
var(--color-secondary) 20px,
var(--color-secondary) 30px,
transparent 30px,
transparent 40px
),
repeating-linear-gradient(
-45deg,
var(--color-accent) 0px,
var(--color-accent) 10px,
transparent 10px,
transparent 20px
);
animation: slidePattern 30s linear infinite;
}
.pattern-adinkra {
background-image:
radial-gradient(circle at 20% 20%, var(--color-primary) 2px, transparent 2px),
radial-gradient(circle at 80% 80%, var(--color-secondary) 2px, transparent 2px),
radial-gradient(circle at 50% 50%, var(--color-accent) 3px, transparent 3px);
background-size: 50px 50px;
animation: rotatePattern 40s linear infinite;
}
.pattern-bogolan {
background-image:
repeating-conic-gradient(
from 0deg at 50% 50%,
var(--color-primary) 0deg 10deg,
transparent 10deg 20deg
);
background-size: 100px 100px;
opacity: 0.05;
animation: pulsePattern 20s ease-in-out infinite;
}
@keyframes slidePattern {
from { transform: translateX(0); }
to { transform: translateX(200px); }
}
@keyframes rotatePattern {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes pulsePattern {
0%, 100% { opacity: 0.05; }
50% { opacity: 0.15; }
}
.hero-content {
position: relative;
z-index: 3;
z-index: 10;
width: 100%;
padding: var(--spacing-xl) 0;
}
.hero-content .container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
align-items: center;
.hero-text {
max-width: 600px;
text-align: center;
margin: 0 auto;
}
.hero-title {
font-size: clamp(3rem, 10vw, 6rem);
line-height: 1;
font-size: clamp(3rem, 8vw, 5rem);
font-weight: 700;
line-height: 1.1;
margin-bottom: 1rem;
color: var(--color-light);
}
.title-line {
display: block;
animation: fadeInUp 1s ease-out;
animation-fill-mode: both;
opacity: 0;
animation: fadeInUp 1s ease-out forwards;
}
.title-line:nth-child(2) {
color: var(--color-primary);
animation-delay: 0.2s;
}
@@ -195,10 +276,16 @@
animation-fill-mode: both;
}
.hero-date strong {
color: var(--color-primary);
font-weight: 700;
}
.hero-cta {
display: flex;
gap: 1rem;
flex-wrap: wrap;
justify-content: center;
animation: fadeInUp 1s ease-out 0.8s;
animation-fill-mode: both;
}
@@ -214,44 +301,6 @@
color: var(--color-dark);
}
.hero-visual {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.sirius-glow {
position: absolute;
width: 300px;
height: 300px;
background: radial-gradient(circle, var(--color-primary) 0%, transparent 70%);
opacity: 0.5;
animation: glow 3s ease-in-out infinite;
}
.ankh-symbol {
font-size: 15rem;
color: var(--color-primary);
animation: float 6s ease-in-out infinite;
filter: drop-shadow(0 0 30px rgba(255, 215, 0, 0.5));
}
.scroll-indicator {
position: absolute;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
text-align: center;
animation: fadeInUp 1s ease-out 1s;
animation-fill-mode: both;
}
.scroll-arrow {
font-size: 2rem;
animation: bounce 2s ease-in-out infinite;
}
@keyframes fadeInUp {
from {
opacity: 0;
@@ -263,28 +312,19 @@
}
}
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(10px);
}
}
/* Mobile */
@media (max-width: 768px) {
.hero-content .container {
grid-template-columns: 1fr;
text-align: center;
}
.hero-visual {
display: none;
.hero-title {
font-size: clamp(2.5rem, 10vw, 4rem);
}
.hero-cta {
justify-content: center;
flex-direction: column;
align-items: center;
}
.btn {
width: 100%;
max-width: 300px;
}
}
</style>
</style>
+150
View File
@@ -0,0 +1,150 @@
<script>
import { currentLanguage, availableLanguages, setLanguage } from '../lib/i18n.js';
let isOpen = false;
function toggleDropdown() {
isOpen = !isOpen;
}
function selectLanguage(lang) {
setLanguage(lang);
isOpen = false;
}
// Fermer le dropdown si on clique ailleurs
function handleClickOutside(event) {
if (!event.target.closest('.language-selector')) {
isOpen = false;
}
}
</script>
<svelte:window on:click={handleClickOutside} />
<div class="language-selector">
<button class="language-button" on:click={toggleDropdown}>
<span class="flag">
{availableLanguages.find(l => l.code === $currentLanguage)?.flag}
</span>
<span class="language-name">
{availableLanguages.find(l => l.code === $currentLanguage)?.name}
</span>
<span class="arrow" class:open={isOpen}>▼</span>
</button>
{#if isOpen}
<div class="language-dropdown">
{#each availableLanguages as lang}
<button
class="language-option"
class:active={$currentLanguage === lang.code}
on:click={() => selectLanguage(lang.code)}
>
<span class="flag">{lang.flag}</span>
<span>{lang.name}</span>
</button>
{/each}
</div>
{/if}
</div>
<style>
.language-selector {
position: relative;
}
.language-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
color: var(--color-light);
cursor: pointer;
transition: all 0.3s ease;
}
.language-button:hover {
background: rgba(255, 255, 255, 0.15);
border-color: var(--color-primary);
}
.flag {
font-size: 1.2rem;
}
.language-name {
font-size: 0.9rem;
}
.arrow {
font-size: 0.7rem;
margin-left: 0.5rem;
transition: transform 0.3s ease;
}
.arrow.open {
transform: rotate(180deg);
}
.language-dropdown {
position: absolute;
top: 100%;
right: 0;
margin-top: 0.5rem;
background: var(--color-dark);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
z-index: 1000;
animation: slideDown 0.3s ease-out;
}
.language-option {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.75rem 1rem;
background: transparent;
border: none;
color: var(--color-light);
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.language-option:hover {
background: rgba(255, 255, 255, 0.1);
}
.language-option.active {
background: rgba(255, 215, 0, 0.2);
color: var(--color-primary);
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.language-name {
display: none;
}
.language-button {
padding: 0.5rem;
}
}
</style>
+12 -7
View File
@@ -1,4 +1,7 @@
<script>
import { t } from '../lib/i18n.js';
import LanguageSelector from './LanguageSelector.svelte';
export let activeSection = 'home';
let isMenuOpen = false;
@@ -8,12 +11,12 @@
};
const menuItems = [
{ id: 'home', label: 'Accueil', icon: '🌟' },
{ id: 'about', label: 'Découvrir', icon: '✨' },
{ id: 'predictions', label: 'Prédictions', icon: '📅' },
{ id: 'science', label: 'Science', icon: '🔭' },
{ id: 'culture', label: 'Culture', icon: '🌍' },
{ id: 'observatory', label: 'Observer', icon: '👁️' }
{ id: 'home', label: 'nav_home', icon: '🌟' },
{ id: 'about', label: 'nav_about', icon: '✨' },
{ id: 'predictions', label: 'nav_predictions', icon: '📅' },
{ id: 'science', label: 'nav_science', icon: '🔭' },
{ id: 'culture', label: 'nav_culture', icon: '🌍' },
{ id: 'observatory', label: 'nav_observatory', icon: '👁️' }
];
const scrollToSection = (id) => {
@@ -43,11 +46,13 @@
on:click|preventDefault={() => scrollToSection(item.id)}
>
<span class="nav-icon">{item.icon}</span>
<span class="nav-label">{item.label}</span>
<span class="nav-label">{$t(item.label)}</span>
</a>
</li>
{/each}
</ul>
<LanguageSelector />
</div>
</div>
</nav>
+661
View File
@@ -0,0 +1,661 @@
<script>
import { onMount } from 'svelte';
let visible = false;
let selectedSite = null;
let mapCanvas;
let ctx;
let guadeloupeGeoJson = null;
const sites = [
{
id: 'moule',
name: 'Le Moule',
lat: 16.3333,
lng: -61.3472,
description: 'Plage de l\'Autre Bord - Vue dégagée sur l\'Est',
features: ['Plage isolée', 'Horizon Est dégagé', 'Peu de pollution lumineuse'],
bestTime: '5h00 - 5h30',
image: '🏖️'
},
{
id: 'deshaies',
name: 'Deshaies',
lat: 16.3039,
lng: -61.7966,
description: 'Plage de Grande Anse - Cadre idyllique',
features: ['Plage populaire', 'Montagnes en arrière-plan', 'Accès facile'],
bestTime: '5h15 - 5h45',
image: '🌅'
},
{
id: 'sainte-anne',
name: 'Sainte-Anne',
lat: 16.2262,
lng: -61.3809,
description: 'Plage de Bois Jolan - Tranquillité garantie',
features: ['Plage sauvage', 'Très peu fréquentée', 'Nature préservée'],
bestTime: '5h00 - 5h30',
image: '🌴'
},
{
id: 'port-louis',
name: 'Port-Louis',
lat: 16.4180,
lng: -61.5312,
description: 'Plage du Souffleur - Vue panoramique',
features: ['Vue à 180°', 'Site historique', 'Calme au petit matin'],
bestTime: '5h00 - 5h30',
image: '🏛️'
},
{
id: 'marie-galante',
name: 'Marie-Galante',
lat: 15.9500,
lng: -61.2667,
description: 'Plage de la Feuillère - Horizon infini',
features: ['Île préservée', 'Ciel très noir', 'Conditions optimales'],
bestTime: '4h45 - 5h15',
image: '🏝️'
},
{
id: 'soufriere',
name: 'Route de la Soufrière',
lat: 16.0439,
lng: -61.6639,
description: 'Parking de Savane à Mulets - Altitude',
features: ['1000m altitude', 'Air pur', 'Vue dégagée', 'Nuits fraîches'],
bestTime: '5h00 - 5h30',
image: '🌋'
},
{
id: 'petite-terre',
name: 'Petite-Terre',
lat: 16.1700,
lng: -61.1200,
description: 'Réserve naturelle - Conditions parfaites',
features: ['Aucune pollution lumineuse', 'Réserve protégée', 'Accès réglementé'],
bestTime: '4h45 - 5h15',
image: '🦎'
}
];
onMount(async () => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !ctx) {
visible = true;
initMap();
}
});
});
const section = document.getElementById('observation-map');
if (section) observer.observe(section);
return () => observer.disconnect();
});
async function initMap() {
ctx = mapCanvas.getContext('2d');
// Charger les données GeoJSON de la Guadeloupe
try {
const response = await fetch('https://raw.githubusercontent.com/gregoiredavid/france-geojson/master/regions/guadeloupe/departements-guadeloupe.geojson');
guadeloupeGeoJson = await response.json();
// Redimensionner le canvas
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// Dessiner la carte
drawMap();
} catch (error) {
console.error('Erreur de chargement GeoJSON:', error);
// Fallback : dessiner une carte simple
drawSimpleMap();
}
}
function resizeCanvas() {
const container = mapCanvas.parentElement;
mapCanvas.width = container.offsetWidth;
mapCanvas.height = container.offsetWidth * 0.75; // Ratio 4:3
if (guadeloupeGeoJson) drawMap();
}
function drawMap() {
if (!ctx || !guadeloupeGeoJson) return;
// Effacer le canvas
ctx.clearRect(0, 0, mapCanvas.width, mapCanvas.height);
// Fond océan
ctx.fillStyle = '#1a3a52';
ctx.fillRect(0, 0, mapCanvas.width, mapCanvas.height);
// Calculer les limites et l'échelle
const bounds = calculateBounds(guadeloupeGeoJson);
const scale = calculateScale(bounds, mapCanvas.width, mapCanvas.height);
// Dessiner chaque feature
guadeloupeGeoJson.features.forEach(feature => {
drawFeature(feature, bounds, scale);
});
// Dessiner les sites
drawSites(bounds, scale);
}
function drawSimpleMap() {
if (!ctx) return;
// Effacer le canvas
ctx.clearRect(0, 0, mapCanvas.width, mapCanvas.height);
// Fond océan
ctx.fillStyle = '#1a3a52';
ctx.fillRect(0, 0, mapCanvas.width, mapCanvas.height);
// Dessiner une forme approximative de la Guadeloupe
const centerX = mapCanvas.width / 2;
const centerY = mapCanvas.height / 2;
const scale = mapCanvas.width / 4;
// Grande-Terre
ctx.fillStyle = '#2d5a3d';
ctx.strokeStyle = '#4CAF50';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(centerX + scale * 0.5, centerY - scale * 0.2, scale * 0.4, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Basse-Terre
ctx.beginPath();
ctx.arc(centerX - scale * 0.3, centerY, scale * 0.5, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Marie-Galante
ctx.beginPath();
ctx.arc(centerX + scale * 0.2, centerY + scale * 0.8, scale * 0.25, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Dessiner les sites avec des positions approximatives
const bounds = { minLng: -62, maxLng: -61, minLat: 15.8, maxLat: 16.5 };
const simpleScale = {
x: mapCanvas.width / (bounds.maxLng - bounds.minLng),
y: mapCanvas.height / (bounds.maxLat - bounds.minLat)
};
drawSites(bounds, simpleScale);
}
function calculateBounds(geoJson) {
let minLng = Infinity, maxLng = -Infinity;
let minLat = Infinity, maxLat = -Infinity;
function processBounds(coords) {
if (Array.isArray(coords[0])) {
coords.forEach(processBounds);
} else {
minLng = Math.min(minLng, coords[0]);
maxLng = Math.max(maxLng, coords[0]);
minLat = Math.min(minLat, coords[1]);
maxLat = Math.max(maxLat, coords[1]);
}
}
geoJson.features.forEach(feature => {
if (feature.geometry && feature.geometry.coordinates) {
processBounds(feature.geometry.coordinates);
}
});
// Ajouter une marge
const margin = 0.1;
return {
minLng: minLng - margin,
maxLng: maxLng + margin,
minLat: minLat - margin,
maxLat: maxLat + margin
};
}
function calculateScale(bounds, width, height) {
return {
x: width / (bounds.maxLng - bounds.minLng),
y: height / (bounds.maxLat - bounds.minLat)
};
}
function projectPoint(lng, lat, bounds, scale) {
return {
x: (lng - bounds.minLng) * scale.x,
y: mapCanvas.height - (lat - bounds.minLat) * scale.y
};
}
function drawFeature(feature, bounds, scale) {
ctx.fillStyle = '#2d5a3d';
ctx.strokeStyle = '#4CAF50';
ctx.lineWidth = 2;
function drawCoordinates(coords) {
if (!coords || coords.length === 0) return;
if (Array.isArray(coords[0][0])) {
coords.forEach(drawCoordinates);
return;
}
ctx.beginPath();
coords.forEach((coord, i) => {
const point = projectPoint(coord[0], coord[1], bounds, scale);
if (i === 0) {
ctx.moveTo(point.x, point.y);
} else {
ctx.lineTo(point.x, point.y);
}
});
ctx.closePath();
ctx.fill();
ctx.stroke();
}
if (feature.geometry && feature.geometry.coordinates) {
drawCoordinates(feature.geometry.coordinates);
}
}
function drawSites(bounds, scale) {
sites.forEach(site => {
const point = projectPoint(site.lng, site.lat, bounds, scale);
// Cercle du marqueur
ctx.beginPath();
ctx.arc(point.x, point.y, selectedSite?.id === site.id ? 15 : 10, 0, Math.PI * 2);
ctx.fillStyle = selectedSite?.id === site.id ? '#FFD700' : '#FF6B35';
ctx.fill();
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.stroke();
// Icône du site
ctx.fillStyle = 'white';
ctx.font = selectedSite?.id === site.id ? '18px sans-serif' : '14px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(site.image, point.x, point.y);
// Nom du site si sélectionné
if (selectedSite?.id === site.id) {
ctx.fillStyle = 'white';
ctx.font = 'bold 14px sans-serif';
ctx.fillText(site.name, point.x, point.y + 25);
}
});
}
function handleMapClick(event) {
const rect = mapCanvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
// Vérifier si on clique sur un site
const bounds = guadeloupeGeoJson ?
calculateBounds(guadeloupeGeoJson) :
{ minLng: -62, maxLng: -61, minLat: 15.8, maxLat: 16.5 };
const scale = guadeloupeGeoJson ?
calculateScale(bounds, mapCanvas.width, mapCanvas.height) :
{ x: mapCanvas.width / (bounds.maxLng - bounds.minLng),
y: mapCanvas.height / (bounds.maxLat - bounds.minLat) };
let clickedSite = null;
sites.forEach(site => {
const point = projectPoint(site.lng, site.lat, bounds, scale);
const distance = Math.sqrt(Math.pow(x - point.x, 2) + Math.pow(y - point.y, 2));
if (distance < 20) {
clickedSite = site;
}
});
if (clickedSite) {
selectedSite = selectedSite?.id === clickedSite.id ? null : clickedSite;
drawMap();
}
}
function selectSite(site) {
selectedSite = selectedSite?.id === site.id ? null : site;
if (guadeloupeGeoJson) {
drawMap();
} else {
drawSimpleMap();
}
}
</script>
<section id="observation-map" class="map-section">
<div class="container">
<div class="section-header" class:visible>
<h2>Carte des Sites d'Observation</h2>
<p class="section-subtitle">
Les meilleurs endroits en Guadeloupe pour observer le lever héliaque
</p>
</div>
<div class="map-wrapper" class:visible>
<!-- Carte Canvas de la Guadeloupe -->
<div class="map-container">
<canvas
bind:this={mapCanvas}
class="guadeloupe-map"
on:click={handleMapClick}
></canvas>
</div>
<!-- Liste des sites -->
<div class="sites-list">
<h3>Sites d'Observation</h3>
{#each sites as site}
<div
class="site-card"
class:active={selectedSite?.id === site.id}
on:click={() => selectSite(site)}
>
<div class="site-header">
<span class="site-icon">{site.image}</span>
<h4>{site.name}</h4>
</div>
<p class="site-description">{site.description}</p>
{#if selectedSite?.id === site.id}
<div class="site-details">
<div class="features">
<h5>Caractéristiques :</h5>
<ul>
{#each site.features as feature}
<li>{feature}</li>
{/each}
</ul>
</div>
<div class="observation-time">
<h5>🕐 Meilleur créneau :</h5>
<p>{site.bestTime}</p>
</div>
<div class="coordinates">
<h5>📍 Coordonnées GPS :</h5>
<p>{site.lat}°N, {Math.abs(site.lng)}°O</p>
</div>
</div>
{/if}
</div>
{/each}
</div>
</div>
<!-- Conseils d'observation -->
<div class="observation-tips" class:visible>
<h3>Conseils pour l'Observation</h3>
<div class="tips-grid">
<div class="tip-card">
<span class="tip-icon">🌙</span>
<h4>Phase Lunaire</h4>
<p>Privilégiez les nuits sans Lune ou avec un fin croissant</p>
</div>
<div class="tip-card">
<span class="tip-icon">☁️</span>
<h4>Météo</h4>
<p>Vérifiez l'absence de nuages à l'Est, surtout près de l'horizon</p>
</div>
<div class="tip-card">
<span class="tip-icon"></span>
<h4>Horaire</h4>
<p>Arrivez 30 minutes avant pour habituer vos yeux à l'obscurité</p>
</div>
<div class="tip-card">
<span class="tip-icon">🔦</span>
<h4>Équipement</h4>
<p>Lampe rouge, jumelles optionnelles, vêtements chauds</p>
</div>
</div>
</div>
</div>
</section>
<style>
.map-section {
padding: var(--spacing-xl) 0;
background: var(--gradient-night);
}
.map-wrapper {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-lg);
margin-top: var(--spacing-lg);
opacity: 0;
transform: translateY(30px);
transition: all 0.8s ease-out;
}
.map-wrapper.visible {
opacity: 1;
transform: translateY(0);
}
/* Carte Canvas */
.map-container {
background: rgba(255, 255, 255, 0.05);
border-radius: 20px;
padding: 2rem;
position: relative;
}
.guadeloupe-map {
width: 100%;
height: auto;
cursor: pointer;
border-radius: 10px;
}
/* Liste des sites */
.sites-list {
max-height: 600px;
overflow-y: auto;
padding-right: 1rem;
}
.sites-list h3 {
color: var(--color-primary);
margin-bottom: 1rem;
}
.site-card {
background: rgba(255, 255, 255, 0.05);
border: 2px solid transparent;
border-radius: 15px;
padding: 1rem;
margin-bottom: 1rem;
cursor: pointer;
transition: all 0.3s ease;
}
.site-card:hover {
background: rgba(255, 255, 255, 0.08);
border-color: var(--color-primary);
}
.site-card.active {
background: rgba(255, 215, 0, 0.1);
border-color: var(--color-primary);
}
.site-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.site-icon {
font-size: 1.5rem;
}
.site-header h4 {
color: var(--color-primary);
margin: 0;
}
.site-description {
color: var(--color-light);
margin: 0;
font-size: 0.9rem;
opacity: 0.8;
}
.site-details {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
animation: slideDown 0.3s ease-out;
}
.site-details h5 {
color: var(--color-secondary);
margin: 0.5rem 0;
font-size: 0.9rem;
}
.site-details ul {
list-style: none;
padding: 0;
margin: 0;
}
.site-details li {
padding: 0.25rem 0;
padding-left: 1rem;
position: relative;
font-size: 0.85rem;
}
.site-details li::before {
content: "•";
position: absolute;
left: 0;
color: var(--color-primary);
}
.observation-time, .coordinates {
margin-top: 0.5rem;
}
.observation-time p, .coordinates p {
margin: 0;
color: var(--color-light);
font-size: 0.9rem;
}
/* Conseils d'observation */
.observation-tips {
margin-top: var(--spacing-xl);
opacity: 0;
transform: translateY(30px);
transition: all 0.8s ease-out 0.2s;
}
.observation-tips.visible {
opacity: 1;
transform: translateY(0);
}
.observation-tips h3 {
color: var(--color-primary);
text-align: center;
margin-bottom: 2rem;
}
.tips-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
}
.tip-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
padding: 1.5rem;
text-align: center;
transition: transform 0.3s ease;
}
.tip-card:hover {
transform: translateY(-5px);
}
.tip-icon {
font-size: 2.5rem;
display: block;
margin-bottom: 1rem;
}
.tip-card h4 {
color: var(--color-primary);
margin: 0 0 0.5rem 0;
}
.tip-card p {
color: var(--color-light);
margin: 0;
font-size: 0.9rem;
}
/* Scrollbar personnalisée */
.sites-list::-webkit-scrollbar {
width: 8px;
}
.sites-list::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
}
.sites-list::-webkit-scrollbar-thumb {
background: var(--color-primary);
border-radius: 4px;
}
@keyframes slideDown {
from {
opacity: 0;
max-height: 0;
}
to {
opacity: 1;
max-height: 300px;
}
}
@media (max-width: 768px) {
.map-wrapper {
grid-template-columns: 1fr;
}
.map-container {
padding: 1rem;
}
.sites-list {
max-height: none;
}
.tips-grid {
grid-template-columns: 1fr 1fr;
}
}
</style>
+232
View File
@@ -0,0 +1,232 @@
import { writable, derived } from 'svelte/store';
// Store pour la langue actuelle
export const currentLanguage = writable('fr');
// Traductions
const translations = {
fr: {
// Navigation
nav_home: 'Accueil',
nav_about: 'À Propos',
nav_predictions: 'Prédictions',
nav_science: 'Science',
nav_culture: 'Culture',
nav_observatory: 'Observatoire',
// Hero
hero_title1: 'Sirius',
hero_title2: 'en Guadeloupe',
hero_subtitle: 'Wep Ronpet - L\'Ouverture de l\'Année Cosmique',
hero_description: 'Le rendez-vous millénaire entre Ciel, Science et Culture',
hero_date: 'Lever héliaque : ',
hero_date_value: '22 Juillet 2025',
hero_cta_predictions: 'Découvrir les Prédictions',
hero_cta_learn: 'En Savoir Plus',
// About
about_title: 'Le Lever Héliaque de Sirius',
about_subtitle: 'Un phénomène astronomique millénaire',
about_intro: 'Le lever héliaque de Sirius est un événement céleste qui a façonné les civilisations depuis des millénaires.',
// Features
feature_astronomy_title: 'Astronomie',
feature_astronomy_desc: 'Sirius, l\'étoile la plus brillante, guide de navigation céleste',
feature_culture_title: 'Culture',
feature_culture_desc: 'De l\'Égypte aux Dogons, un héritage universel',
feature_spirituality_title: 'Spiritualité',
feature_spirituality_desc: 'Renaissance, renouveau et connexion cosmique',
// Predictions
predictions_title: 'Prédictions 2025',
predictions_subtitle: 'Calculez la date exacte pour votre lieu d\'observation',
predictions_select_location: 'Sélectionnez votre lieu',
predictions_results_for: 'Résultats pour',
predictions_heliacal_rising: 'Lever héliaque',
predictions_best_observation: 'Meilleure observation',
predictions_visibility_duration: 'Durée de visibilité',
predictions_azimuth: 'Azimut au lever',
predictions_max_altitude: 'Altitude maximale',
predictions_magnitude: 'Magnitude apparente',
// Science
science_title: 'La Science du Lever Héliaque',
science_subtitle: 'Comprendre le phénomène astronomique',
// Culture
culture_title: 'Héritage Culturel',
culture_subtitle: 'Sirius à travers les civilisations',
culture_timeline_title: 'Chronologie du Savoir',
// Observatory
observatory_title: 'Guide d\'Observation',
observatory_subtitle: 'Tout pour observer Sirius en Guadeloupe',
// Footer
footer_copyright: 'Projet réalisé avec ❤️ par',
footer_and: 'et'
},
en: {
// Navigation
nav_home: 'Home',
nav_about: 'About',
nav_predictions: 'Predictions',
nav_science: 'Science',
nav_culture: 'Culture',
nav_observatory: 'Observatory',
// Hero
hero_title1: 'Sirius',
hero_title2: 'in Guadeloupe',
hero_subtitle: 'Wep Ronpet - The Opening of the Cosmic Year',
hero_description: 'The millennial meeting between Sky, Science and Culture',
hero_date: 'Heliacal rising: ',
hero_date_value: 'July 22, 2025',
hero_cta_predictions: 'Discover Predictions',
hero_cta_learn: 'Learn More',
// About about_title: 'The Heliacal Rising of Sirius',
about_subtitle: 'A millennial astronomical phenomenon',
about_intro: 'The heliacal rising of Sirius is a celestial event that has shaped civilizations for millennia.',
// Features
feature_astronomy_title: 'Astronomy',
feature_astronomy_desc: 'Sirius, the brightest star, celestial navigation guide',
feature_culture_title: 'Culture',
feature_culture_desc: 'From Egypt to the Dogon, a universal heritage',
feature_spirituality_title: 'Spirituality',
feature_spirituality_desc: 'Renaissance, renewal and cosmic connection',
// Predictions
predictions_title: 'Predictions 2025',
predictions_subtitle: 'Calculate the exact date for your observation location',
predictions_select_location: 'Select your location',
predictions_results_for: 'Results for',
predictions_heliacal_rising: 'Heliacal rising',
predictions_best_observation: 'Best observation',
predictions_visibility_duration: 'Visibility duration',
predictions_azimuth: 'Azimuth at rising',
predictions_max_altitude: 'Maximum altitude',
predictions_magnitude: 'Apparent magnitude',
// Science
science_title: 'The Science of Heliacal Rising',
science_subtitle: 'Understanding the astronomical phenomenon',
// Culture
culture_title: 'Cultural Heritage',
culture_subtitle: 'Sirius through civilizations',
culture_timeline_title: 'Timeline of Knowledge',
// Observatory
observatory_title: 'Observation Guide',
observatory_subtitle: 'Everything to observe Sirius in Guadeloupe',
// Footer
footer_copyright: 'Project made with ❤️ by',
footer_and: 'and'
},
ht: {
// Navigation
nav_home: 'Akèy',
nav_about: 'Konsènan',
nav_predictions: 'Prediksyon',
nav_science: 'Syans',
nav_culture: 'Kilti',
nav_observatory: 'Obsèvatwa',
// Hero
hero_title1: 'Sirius',
hero_title2: 'nan Gwadloup',
hero_subtitle: 'Wep Ronpet - Ouvèti Ane Kosmik la',
hero_description: 'Randevou milenè ant Syèl, Syans ak Kilti',
hero_date: 'Leve elyak: ',
hero_date_value: '22 Jiyè 2025',
hero_cta_predictions: 'Dekouvri Prediksyon yo', hero_cta_learn: 'Aprann Plis',
// About
about_title: 'Leve Elyak Sirius la',
about_subtitle: 'Yon fenomèn astwonomik milenè',
about_intro: 'Leve elyak Sirius se yon evènman selès ki fòme sivilizasyon yo depi plizyè milye ane.',
// Features
feature_astronomy_title: 'Astwonomi',
feature_astronomy_desc: 'Sirius, zetwal ki pi klere a, gid navigasyon selès',
feature_culture_title: 'Kilti',
feature_culture_desc: 'Depi Lejip rive Dogon yo, yon eritaj inivèsèl',
feature_spirituality_title: 'Espiritwalite',
feature_spirituality_desc: 'Renesans, renouvèlman ak koneksyon kosmik',
// Predictions
predictions_title: 'Prediksyon 2025',
predictions_subtitle: 'Kalkile dat egzak pou kote w ap obsève a',
predictions_select_location: 'Chwazi kote ou ye a',
predictions_results_for: 'Rezilta pou',
predictions_heliacal_rising: 'Leve elyak',
predictions_best_observation: 'Pi bon obsèvasyon',
predictions_visibility_duration: 'Dire vizibilite',
predictions_azimuth: 'Azimit nan leve',
predictions_max_altitude: 'Altitid maksimòm',
predictions_magnitude: 'Mayitid aparan',
// Science
science_title: 'Syans Leve Elyak la',
science_subtitle: 'Konprann fenomèn astwonomik la',
// Culture
culture_title: 'Eritaj Kiltirèl',
culture_subtitle: 'Sirius atravè sivilizasyon yo',
culture_timeline_title: 'Kwonoloji Konesans lan',
// Observatory
observatory_title: 'Gid Obsèvasyon',
observatory_subtitle: 'Tout bagay pou obsève Sirius nan Gwadloup',
// Footer
footer_copyright: 'Pwojè realize avèk ❤️ pa',
footer_and: 'ak'
}
};
// Store dérivé pour obtenir les traductions de la langue actuelle
export const t = derived(
currentLanguage,
$currentLanguage => key => {
return translations[$currentLanguage]?.[key] || translations['fr'][key] || key;
}
);
// Fonction pour changer de langue
export function setLanguage(lang) {
if (translations[lang]) {
currentLanguage.set(lang);
if (typeof localStorage !== 'undefined') {
localStorage.setItem('preferred-language', lang);
}
}
}
// Initialiser avec la langue sauvegardée ou détecter la langue du navigateur
export function initLanguage() {
if (typeof localStorage !== 'undefined') {
const saved = localStorage.getItem('preferred-language');
if (saved && translations[saved]) {
currentLanguage.set(saved);
return;
}
}
if (typeof navigator !== 'undefined') {
const browserLang = navigator.language.slice(0, 2);
if (translations[browserLang]) {
currentLanguage.set(browserLang);
}
}
}
// Liste des langues disponibles
export const availableLanguages = [
{ code: 'fr', name: 'Français', flag: '🇫🇷' },
{ code: 'en', name: 'English', flag: '🇬🇧' },
{ code: 'ht', name: 'Kreyòl Ayisyen', flag: '🇭🇹' }
];