Complete deployment of unified Light Trinity system: 🔴 RedLight: Template & brand system (18 HTML templates) 💚 GreenLight: Project & collaboration (14 layers, 103 templates) 💛 YellowLight: Infrastructure & deployment 🌈 Trinity: Unified compliance & testing Includes: - 12 documentation files - 8 shell scripts - 18 HTML brand templates - Trinity compliance workflow Built by: Cece + Alexa Date: December 23, 2025 Source: blackroad-os/blackroad-os-infra 🌸✨
1333 lines
49 KiB
HTML
1333 lines
49 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>BlackRoad OS — Living Earth</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
overflow: hidden;
|
|
background: #000;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
#canvas-container {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
/* Logo */
|
|
.logo {
|
|
position: fixed;
|
|
top: 20px;
|
|
left: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
padding: 12px 20px;
|
|
border-radius: 50px;
|
|
backdrop-filter: blur(20px);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
z-index: 100;
|
|
}
|
|
|
|
.logo-icon {
|
|
width: 28px;
|
|
height: 28px;
|
|
background: linear-gradient(135deg, #FF1D6C, #F5A623);
|
|
border-radius: 50%;
|
|
position: relative;
|
|
}
|
|
|
|
.logo-icon::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 10px;
|
|
height: 10px;
|
|
background: #000;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.logo-text {
|
|
color: #fff;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
}
|
|
|
|
/* Stats Bar */
|
|
.stats-bar {
|
|
position: fixed;
|
|
top: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
gap: 24px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
padding: 12px 28px;
|
|
border-radius: 50px;
|
|
backdrop-filter: blur(20px);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
z-index: 100;
|
|
}
|
|
|
|
.stat {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.stat-icon {
|
|
font-size: 16px;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #fff;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 11px;
|
|
color: rgba(255,255,255,0.5);
|
|
margin-left: 2px;
|
|
}
|
|
|
|
/* Info Panel */
|
|
.info-panel {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
padding: 16px 20px;
|
|
border-radius: 16px;
|
|
backdrop-filter: blur(20px);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
z-index: 100;
|
|
min-width: 180px;
|
|
}
|
|
|
|
.info-title {
|
|
font-size: 10px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.12em;
|
|
color: rgba(255,255,255,0.4);
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.info-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 6px 0;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.info-label {
|
|
color: rgba(255,255,255,0.6);
|
|
}
|
|
|
|
.info-value {
|
|
color: #fff;
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Continent Panel */
|
|
.continent-panel {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
left: 20px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
padding: 16px 20px;
|
|
border-radius: 16px;
|
|
backdrop-filter: blur(20px);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
z-index: 100;
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
transition: all 0.3s ease;
|
|
max-width: 300px;
|
|
}
|
|
|
|
.continent-panel.visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.continent-name {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #fff;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.continent-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 12px;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.continent-stat {
|
|
text-align: center;
|
|
}
|
|
|
|
.continent-stat-icon {
|
|
font-size: 18px;
|
|
}
|
|
|
|
.continent-stat-value {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #fff;
|
|
}
|
|
|
|
.continent-stat-label {
|
|
font-size: 9px;
|
|
color: rgba(255,255,255,0.5);
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/* Controls */
|
|
.controls {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
right: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
z-index: 100;
|
|
}
|
|
|
|
.ctrl-btn {
|
|
width: 44px;
|
|
height: 44px;
|
|
border: none;
|
|
border-radius: 50%;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
backdrop-filter: blur(20px);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
color: #fff;
|
|
font-size: 18px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.ctrl-btn:hover {
|
|
background: rgba(255, 29, 108, 0.4);
|
|
border-color: #FF1D6C;
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.ctrl-btn.active {
|
|
background: #FF1D6C;
|
|
border-color: #FF1D6C;
|
|
}
|
|
|
|
/* Loading */
|
|
.loading {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: #000;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
transition: opacity 1s ease, visibility 1s ease;
|
|
}
|
|
|
|
.loading.hidden {
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 80px;
|
|
height: 80px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, #1a5a2e, #2d7a47, #1e5a7f, #0d3a57);
|
|
animation: spin 3s linear infinite;
|
|
position: relative;
|
|
box-shadow: 0 0 40px rgba(100, 180, 255, 0.3);
|
|
}
|
|
|
|
.loading-spinner::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 3px;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.15), transparent 60%);
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.loading-text {
|
|
margin-top: 24px;
|
|
color: rgba(255,255,255,0.8);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.loading-bar {
|
|
width: 180px;
|
|
height: 2px;
|
|
background: rgba(255,255,255,0.1);
|
|
border-radius: 1px;
|
|
margin-top: 16px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.loading-progress {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #FF1D6C, #F5A623);
|
|
width: 0%;
|
|
transition: width 0.2s ease;
|
|
}
|
|
|
|
/* Tooltip */
|
|
.tooltip {
|
|
position: fixed;
|
|
background: rgba(0,0,0,0.85);
|
|
padding: 6px 12px;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
color: #fff;
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
z-index: 200;
|
|
transition: opacity 0.15s ease;
|
|
}
|
|
|
|
.tooltip.visible {
|
|
opacity: 1;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Loading -->
|
|
<div class="loading" id="loading">
|
|
<div class="loading-spinner"></div>
|
|
<div class="loading-text">Building Living Earth...</div>
|
|
<div class="loading-bar">
|
|
<div class="loading-progress" id="loadingProgress"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Canvas -->
|
|
<div id="canvas-container"></div>
|
|
|
|
<!-- Logo -->
|
|
<div class="logo">
|
|
<div class="logo-icon"></div>
|
|
<span class="logo-text">BlackRoad Living Earth</span>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="stats-bar">
|
|
<div class="stat">
|
|
<span class="stat-icon">🌳</span>
|
|
<span class="stat-value" id="treeCount">0</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-icon">🏠</span>
|
|
<span class="stat-value" id="houseCount">0</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-icon">🐰</span>
|
|
<span class="stat-value" id="animalCount">0</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-icon">🤖</span>
|
|
<span class="stat-value" id="agentCount">0</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-icon">🌸</span>
|
|
<span class="stat-value" id="flowerCount">0</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Info -->
|
|
<div class="info-panel">
|
|
<div class="info-title">Earth Status</div>
|
|
<div class="info-row">
|
|
<span class="info-label">Time (UTC)</span>
|
|
<span class="info-value" id="utcTime">00:00</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">Rotation</span>
|
|
<span class="info-value" id="rotation">0.0°</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">Continents</span>
|
|
<span class="info-value">7</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">Villages</span>
|
|
<span class="info-value" id="villageCount">0</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Continent Panel -->
|
|
<div class="continent-panel" id="continentPanel">
|
|
<div class="continent-name" id="contName">North America</div>
|
|
<div class="continent-stats">
|
|
<div class="continent-stat">
|
|
<div class="continent-stat-icon">🌳</div>
|
|
<div class="continent-stat-value" id="contTrees">0</div>
|
|
<div class="continent-stat-label">Trees</div>
|
|
</div>
|
|
<div class="continent-stat">
|
|
<div class="continent-stat-icon">🏠</div>
|
|
<div class="continent-stat-value" id="contHouses">0</div>
|
|
<div class="continent-stat-label">Houses</div>
|
|
</div>
|
|
<div class="continent-stat">
|
|
<div class="continent-stat-icon">🐰</div>
|
|
<div class="continent-stat-value" id="contAnimals">0</div>
|
|
<div class="continent-stat-label">Animals</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Controls -->
|
|
<div class="controls">
|
|
<button class="ctrl-btn active" id="btnRotate" title="Auto Rotate">🔄</button>
|
|
<button class="ctrl-btn active" id="btnClouds" title="Clouds">☁️</button>
|
|
<button class="ctrl-btn active" id="btnTrees" title="Trees">🌳</button>
|
|
<button class="ctrl-btn active" id="btnHouses" title="Houses">🏠</button>
|
|
<button class="ctrl-btn active" id="btnAnimals" title="Animals">🐰</button>
|
|
<button class="ctrl-btn" id="btnNight" title="Night Mode">🌙</button>
|
|
</div>
|
|
|
|
<!-- Tooltip -->
|
|
<div class="tooltip" id="tooltip"></div>
|
|
|
|
<!-- Three.js -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
|
|
<script>
|
|
// ============ CONFIG ============
|
|
const EARTH_RADIUS = 100;
|
|
|
|
const TEXTURES = {
|
|
earth: 'https://unpkg.com/three-globe/example/img/earth-blue-marble.jpg',
|
|
bump: 'https://unpkg.com/three-globe/example/img/earth-topology.png',
|
|
specular: 'https://unpkg.com/three-globe/example/img/earth-water.png',
|
|
clouds: 'https://unpkg.com/three-globe/example/img/earth-clouds.png',
|
|
night: 'https://unpkg.com/three-globe/example/img/earth-night.jpg'
|
|
};
|
|
|
|
// Real continent data with multiple regions for proper coverage
|
|
const CONTINENTS = [
|
|
{
|
|
name: "North America",
|
|
regions: [
|
|
{ lat: 45, lng: -100, radius: 25 }, // Central US
|
|
{ lat: 55, lng: -120, radius: 20 }, // Canada West
|
|
{ lat: 50, lng: -80, radius: 18 }, // Canada East
|
|
{ lat: 35, lng: -90, radius: 15 }, // Southern US
|
|
{ lat: 40, lng: -75, radius: 12 }, // East Coast
|
|
{ lat: 48, lng: -122, radius: 10 }, // Pacific Northwest
|
|
{ lat: 25, lng: -100, radius: 15 }, // Mexico
|
|
{ lat: 65, lng: -150, radius: 15 }, // Alaska
|
|
],
|
|
trees: 400, houses: 80, animals: 120, flowers: 300, agents: 25
|
|
},
|
|
{
|
|
name: "South America",
|
|
regions: [
|
|
{ lat: -15, lng: -60, radius: 25 }, // Brazil Central
|
|
{ lat: -5, lng: -55, radius: 20 }, // Amazon
|
|
{ lat: -23, lng: -45, radius: 15 }, // Southeast Brazil
|
|
{ lat: -35, lng: -65, radius: 18 }, // Argentina
|
|
{ lat: -10, lng: -75, radius: 12 }, // Peru
|
|
{ lat: 5, lng: -75, radius: 12 }, // Colombia
|
|
{ lat: -40, lng: -72, radius: 10 }, // Chile
|
|
],
|
|
trees: 500, houses: 60, animals: 150, flowers: 400, agents: 15
|
|
},
|
|
{
|
|
name: "Europe",
|
|
regions: [
|
|
{ lat: 50, lng: 10, radius: 15 }, // Central Europe
|
|
{ lat: 48, lng: 2, radius: 10 }, // France
|
|
{ lat: 52, lng: -1, radius: 8 }, // UK
|
|
{ lat: 42, lng: 12, radius: 10 }, // Italy
|
|
{ lat: 40, lng: -4, radius: 10 }, // Spain
|
|
{ lat: 55, lng: 20, radius: 12 }, // Poland/Baltic
|
|
{ lat: 60, lng: 10, radius: 12 }, // Scandinavia
|
|
{ lat: 55, lng: 37, radius: 10 }, // Western Russia
|
|
],
|
|
trees: 350, houses: 100, animals: 80, flowers: 350, agents: 30
|
|
},
|
|
{
|
|
name: "Africa",
|
|
regions: [
|
|
{ lat: 10, lng: 20, radius: 25 }, // Central Africa
|
|
{ lat: -5, lng: 25, radius: 20 }, // Congo
|
|
{ lat: 30, lng: 30, radius: 15 }, // North Africa
|
|
{ lat: -25, lng: 25, radius: 18 }, // Southern Africa
|
|
{ lat: 0, lng: 35, radius: 15 }, // East Africa
|
|
{ lat: 10, lng: 0, radius: 15 }, // West Africa
|
|
{ lat: -20, lng: 45, radius: 10 }, // Madagascar
|
|
],
|
|
trees: 450, houses: 70, animals: 200, flowers: 350, agents: 18
|
|
},
|
|
{
|
|
name: "Asia",
|
|
regions: [
|
|
{ lat: 35, lng: 105, radius: 25 }, // China
|
|
{ lat: 25, lng: 80, radius: 20 }, // India
|
|
{ lat: 55, lng: 80, radius: 25 }, // Russia/Siberia
|
|
{ lat: 36, lng: 138, radius: 10 }, // Japan
|
|
{ lat: 37, lng: 127, radius: 8 }, // Korea
|
|
{ lat: 15, lng: 105, radius: 15 }, // Southeast Asia
|
|
{ lat: 30, lng: 70, radius: 12 }, // Pakistan
|
|
{ lat: 40, lng: 60, radius: 15 }, // Central Asia
|
|
{ lat: 25, lng: 55, radius: 10 }, // Middle East
|
|
{ lat: 60, lng: 100, radius: 20 }, // Northern Russia
|
|
],
|
|
trees: 600, houses: 120, animals: 180, flowers: 500, agents: 35
|
|
},
|
|
{
|
|
name: "Oceania",
|
|
regions: [
|
|
{ lat: -25, lng: 135, radius: 25 }, // Australia Central
|
|
{ lat: -35, lng: 145, radius: 15 }, // Southeast Australia
|
|
{ lat: -20, lng: 145, radius: 15 }, // Northeast Australia
|
|
{ lat: -42, lng: 172, radius: 10 }, // New Zealand
|
|
{ lat: -8, lng: 140, radius: 10 }, // Papua New Guinea
|
|
],
|
|
trees: 250, houses: 50, animals: 100, flowers: 200, agents: 12
|
|
},
|
|
{
|
|
name: "Antarctica",
|
|
regions: [
|
|
{ lat: -80, lng: 0, radius: 20 },
|
|
{ lat: -75, lng: 90, radius: 15 },
|
|
{ lat: -75, lng: -90, radius: 15 },
|
|
],
|
|
trees: 0, houses: 5, animals: 30, flowers: 0, agents: 3 // Research stations, penguins
|
|
}
|
|
];
|
|
|
|
// ============ SCENE ============
|
|
let scene, camera, renderer;
|
|
let earth, clouds, nightLights, atmosphere;
|
|
let trees = [], houses = [], animals = [], flowers = [], agents = [];
|
|
let continentGroups = {};
|
|
let starField;
|
|
|
|
let time = 0;
|
|
let autoRotate = true;
|
|
let showClouds = true;
|
|
let showTrees = true;
|
|
let showHouses = true;
|
|
let showAnimals = true;
|
|
let isNight = false;
|
|
|
|
let isDragging = false;
|
|
let previousMouse = { x: 0, y: 0 };
|
|
let spherical = { theta: 0, phi: Math.PI / 3, radius: 280 };
|
|
|
|
// ============ INIT ============
|
|
function init() {
|
|
scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x000008);
|
|
|
|
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 5000);
|
|
updateCamera();
|
|
|
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
renderer.outputEncoding = THREE.sRGBEncoding;
|
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
renderer.toneMappingExposure = 1.1;
|
|
document.getElementById('canvas-container').appendChild(renderer.domElement);
|
|
|
|
loadTextures();
|
|
|
|
// Events
|
|
window.addEventListener('resize', onResize);
|
|
renderer.domElement.addEventListener('mousedown', onMouseDown);
|
|
renderer.domElement.addEventListener('mousemove', onMouseMove);
|
|
renderer.domElement.addEventListener('mouseup', onMouseUp);
|
|
renderer.domElement.addEventListener('wheel', onWheel);
|
|
renderer.domElement.addEventListener('touchstart', onTouchStart, { passive: false });
|
|
renderer.domElement.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
renderer.domElement.addEventListener('touchend', onTouchEnd);
|
|
|
|
// Controls
|
|
document.getElementById('btnRotate').addEventListener('click', () => toggle('rotate'));
|
|
document.getElementById('btnClouds').addEventListener('click', () => toggle('clouds'));
|
|
document.getElementById('btnTrees').addEventListener('click', () => toggle('trees'));
|
|
document.getElementById('btnHouses').addEventListener('click', () => toggle('houses'));
|
|
document.getElementById('btnAnimals').addEventListener('click', () => toggle('animals'));
|
|
document.getElementById('btnNight').addEventListener('click', () => toggle('night'));
|
|
}
|
|
|
|
function loadTextures() {
|
|
const manager = new THREE.LoadingManager();
|
|
|
|
manager.onProgress = (url, loaded, total) => {
|
|
document.getElementById('loadingProgress').style.width = (loaded/total*50) + '%';
|
|
};
|
|
|
|
manager.onLoad = () => {
|
|
createLivingWorld();
|
|
};
|
|
|
|
const loader = new THREE.TextureLoader(manager);
|
|
|
|
const earthTex = loader.load(TEXTURES.earth);
|
|
const bumpTex = loader.load(TEXTURES.bump);
|
|
const specTex = loader.load(TEXTURES.specular);
|
|
const cloudTex = loader.load(TEXTURES.clouds);
|
|
const nightTex = loader.load(TEXTURES.night);
|
|
|
|
[earthTex, bumpTex, specTex, cloudTex, nightTex].forEach(t => {
|
|
t.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
|
});
|
|
|
|
window.textures = { earthTex, bumpTex, specTex, cloudTex, nightTex };
|
|
}
|
|
|
|
function createLivingWorld() {
|
|
createStars();
|
|
createLights();
|
|
createEarth();
|
|
createClouds();
|
|
createAtmosphere();
|
|
|
|
// Create living elements on all continents
|
|
let progress = 50;
|
|
CONTINENTS.forEach((continent, idx) => {
|
|
continentGroups[continent.name] = {
|
|
trees: [],
|
|
houses: [],
|
|
animals: [],
|
|
flowers: [],
|
|
agents: []
|
|
};
|
|
|
|
populateContinent(continent);
|
|
progress = 50 + (idx + 1) / CONTINENTS.length * 50;
|
|
document.getElementById('loadingProgress').style.width = progress + '%';
|
|
});
|
|
|
|
updateCounts();
|
|
|
|
setTimeout(() => {
|
|
document.getElementById('loading').classList.add('hidden');
|
|
animate();
|
|
}, 500);
|
|
}
|
|
|
|
// ============ EARTH ============
|
|
function createEarth() {
|
|
const { earthTex, bumpTex, specTex, nightTex } = window.textures;
|
|
|
|
const geo = new THREE.SphereGeometry(EARTH_RADIUS, 128, 128);
|
|
const mat = new THREE.MeshPhongMaterial({
|
|
map: earthTex,
|
|
bumpMap: bumpTex,
|
|
bumpScale: 0.5,
|
|
specularMap: specTex,
|
|
specular: new THREE.Color(0x222222),
|
|
shininess: 8
|
|
});
|
|
|
|
earth = new THREE.Mesh(geo, mat);
|
|
scene.add(earth);
|
|
|
|
// Night lights
|
|
const nightMat = new THREE.MeshBasicMaterial({
|
|
map: nightTex,
|
|
blending: THREE.AdditiveBlending,
|
|
transparent: true,
|
|
opacity: 0
|
|
});
|
|
nightLights = new THREE.Mesh(geo.clone(), nightMat);
|
|
nightLights.scale.setScalar(1.001);
|
|
scene.add(nightLights);
|
|
}
|
|
|
|
function createClouds() {
|
|
const { cloudTex } = window.textures;
|
|
const geo = new THREE.SphereGeometry(EARTH_RADIUS + 1.5, 64, 64);
|
|
const mat = new THREE.MeshPhongMaterial({
|
|
map: cloudTex,
|
|
transparent: true,
|
|
opacity: 0.35,
|
|
depthWrite: false
|
|
});
|
|
clouds = new THREE.Mesh(geo, mat);
|
|
scene.add(clouds);
|
|
}
|
|
|
|
function createAtmosphere() {
|
|
const geo = new THREE.SphereGeometry(EARTH_RADIUS + 12, 64, 64);
|
|
const mat = 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.65 - dot(vNormal, vec3(0,0,1)), 2.0);
|
|
gl_FragColor = vec4(0.3, 0.6, 1.0, intensity * 0.5);
|
|
}
|
|
`,
|
|
blending: THREE.AdditiveBlending,
|
|
side: THREE.BackSide,
|
|
transparent: true,
|
|
depthWrite: false
|
|
});
|
|
atmosphere = new THREE.Mesh(geo, mat);
|
|
scene.add(atmosphere);
|
|
}
|
|
|
|
function createStars() {
|
|
const count = 8000;
|
|
const geo = new THREE.BufferGeometry();
|
|
const pos = new Float32Array(count * 3);
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const r = 1200 + Math.random() * 2000;
|
|
const theta = Math.random() * Math.PI * 2;
|
|
const phi = Math.acos(Math.random() * 2 - 1);
|
|
pos[i*3] = r * Math.sin(phi) * Math.cos(theta);
|
|
pos[i*3+1] = r * Math.sin(phi) * Math.sin(theta);
|
|
pos[i*3+2] = r * Math.cos(phi);
|
|
}
|
|
|
|
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
|
|
const mat = new THREE.PointsMaterial({ color: 0xffffff, size: 1.2, transparent: true, opacity: 0.9 });
|
|
starField = new THREE.Points(geo, mat);
|
|
scene.add(starField);
|
|
}
|
|
|
|
function createLights() {
|
|
const sun = new THREE.DirectionalLight(0xffffff, 1.4);
|
|
sun.position.set(400, 200, 400);
|
|
scene.add(sun);
|
|
window.sunLight = sun;
|
|
|
|
const ambient = new THREE.AmbientLight(0x333355, 0.4);
|
|
scene.add(ambient);
|
|
window.ambientLight = ambient;
|
|
|
|
const hemi = new THREE.HemisphereLight(0x88aaff, 0x444422, 0.3);
|
|
scene.add(hemi);
|
|
}
|
|
|
|
// ============ POPULATE CONTINENTS ============
|
|
function populateContinent(continent) {
|
|
const group = continentGroups[continent.name];
|
|
|
|
continent.regions.forEach(region => {
|
|
const treesInRegion = Math.floor(continent.trees / continent.regions.length);
|
|
const housesInRegion = Math.floor(continent.houses / continent.regions.length);
|
|
const animalsInRegion = Math.floor(continent.animals / continent.regions.length);
|
|
const flowersInRegion = Math.floor(continent.flowers / continent.regions.length);
|
|
const agentsInRegion = Math.floor(continent.agents / continent.regions.length);
|
|
|
|
// Trees
|
|
for (let i = 0; i < treesInRegion; i++) {
|
|
const tree = createTree(continent.name);
|
|
placeOnRegion(tree, region);
|
|
trees.push(tree);
|
|
group.trees.push(tree);
|
|
scene.add(tree);
|
|
}
|
|
|
|
// Houses
|
|
for (let i = 0; i < housesInRegion; i++) {
|
|
const house = createHouse();
|
|
placeOnRegion(house, region);
|
|
houses.push(house);
|
|
group.houses.push(house);
|
|
scene.add(house);
|
|
}
|
|
|
|
// Animals
|
|
for (let i = 0; i < animalsInRegion; i++) {
|
|
const animal = createAnimal(continent.name);
|
|
placeOnRegion(animal, region);
|
|
animal.userData.region = region;
|
|
animals.push(animal);
|
|
group.animals.push(animal);
|
|
scene.add(animal);
|
|
}
|
|
|
|
// Flowers
|
|
for (let i = 0; i < flowersInRegion; i++) {
|
|
const flower = createFlower();
|
|
placeOnRegion(flower, region);
|
|
flowers.push(flower);
|
|
group.flowers.push(flower);
|
|
scene.add(flower);
|
|
}
|
|
|
|
// Agents
|
|
for (let i = 0; i < agentsInRegion; i++) {
|
|
const agent = createAgent();
|
|
placeOnRegion(agent, region, 3);
|
|
agent.userData.region = region;
|
|
agents.push(agent);
|
|
group.agents.push(agent);
|
|
scene.add(agent);
|
|
}
|
|
});
|
|
}
|
|
|
|
function placeOnRegion(obj, region, heightOffset = 0) {
|
|
// Random position within region
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const dist = Math.random() * region.radius;
|
|
|
|
const lat = region.lat + Math.cos(angle) * dist * 0.5;
|
|
const lng = region.lng + Math.sin(angle) * dist * 0.8;
|
|
|
|
const pos = latLngToVector3(lat, lng, EARTH_RADIUS + 0.5 + heightOffset);
|
|
obj.position.copy(pos);
|
|
|
|
// Orient to surface
|
|
const normal = pos.clone().normalize();
|
|
obj.lookAt(pos.clone().add(normal));
|
|
obj.rotateX(Math.PI / 2);
|
|
obj.rotateY(Math.random() * Math.PI * 2);
|
|
|
|
obj.userData.lat = lat;
|
|
obj.userData.lng = lng;
|
|
}
|
|
|
|
function latLngToVector3(lat, lng, radius) {
|
|
const phi = (90 - lat) * Math.PI / 180;
|
|
const theta = (lng + 180) * Math.PI / 180;
|
|
return new THREE.Vector3(
|
|
radius * Math.sin(phi) * Math.cos(theta),
|
|
radius * Math.cos(phi),
|
|
radius * Math.sin(phi) * Math.sin(theta)
|
|
);
|
|
}
|
|
|
|
// ============ CREATE OBJECTS ============
|
|
function createTree(continent) {
|
|
const group = new THREE.Group();
|
|
|
|
// Trunk
|
|
const trunkGeo = new THREE.CylinderGeometry(0.15, 0.25, 1.5, 6);
|
|
const trunkMat = new THREE.MeshLambertMaterial({ color: 0x4a3728 });
|
|
const trunk = new THREE.Mesh(trunkGeo, trunkMat);
|
|
trunk.position.y = 0.75;
|
|
group.add(trunk);
|
|
|
|
// Determine tree type based on continent
|
|
let leafColor = 0x2d5a27;
|
|
let type = 'normal';
|
|
|
|
if (continent === 'Antarctica') {
|
|
return group; // No trees in Antarctica
|
|
} else if (continent === 'South America' || continent === 'Africa') {
|
|
// Tropical
|
|
type = Math.random() > 0.5 ? 'palm' : 'tropical';
|
|
leafColor = 0x228b22;
|
|
} else if (continent === 'Asia' && Math.random() > 0.7) {
|
|
// Cherry blossom
|
|
type = 'cherry';
|
|
leafColor = 0xffb7c5;
|
|
} else if (continent === 'Europe' && Math.random() > 0.6) {
|
|
// Pine
|
|
type = 'pine';
|
|
leafColor = 0x1a4d1a;
|
|
}
|
|
|
|
if (type === 'pine') {
|
|
for (let i = 0; i < 4; i++) {
|
|
const coneGeo = new THREE.ConeGeometry(1.2 - i * 0.2, 1.2, 6);
|
|
const coneMat = new THREE.MeshLambertMaterial({ color: leafColor });
|
|
const cone = new THREE.Mesh(coneGeo, coneMat);
|
|
cone.position.y = 1.8 + i * 0.8;
|
|
group.add(cone);
|
|
}
|
|
} else if (type === 'palm') {
|
|
trunk.scale.set(0.6, 1.5, 0.6);
|
|
trunk.position.y = 1.2;
|
|
for (let i = 0; i < 5; i++) {
|
|
const frondGeo = new THREE.ConeGeometry(0.15, 2, 3);
|
|
const frondMat = new THREE.MeshLambertMaterial({ color: 0x228b22 });
|
|
const frond = new THREE.Mesh(frondGeo, frondMat);
|
|
frond.position.y = 2.5;
|
|
frond.rotation.z = Math.PI / 3;
|
|
frond.rotation.y = (i / 5) * Math.PI * 2;
|
|
group.add(frond);
|
|
}
|
|
} else {
|
|
// Normal round tree
|
|
const leavesGeo = new THREE.IcosahedronGeometry(1.2, 0);
|
|
const leavesMat = new THREE.MeshLambertMaterial({ color: leafColor });
|
|
const leaves = new THREE.Mesh(leavesGeo, leavesMat);
|
|
leaves.position.y = 2.2;
|
|
leaves.scale.set(1, 0.85, 1);
|
|
group.add(leaves);
|
|
}
|
|
|
|
group.scale.setScalar(0.4 + Math.random() * 0.3);
|
|
group.userData = { type: 'tree', sway: Math.random() * Math.PI * 2 };
|
|
return group;
|
|
}
|
|
|
|
function createHouse() {
|
|
const group = new THREE.Group();
|
|
|
|
const wallColors = [0xfaf8f5, 0xfff5e6, 0xffe8e8, 0xe8f4ff, 0xfff8dc];
|
|
const roofColors = [0xc44, 0x2577b5, 0x3a3, 0xff6b9d, 0xf5a623];
|
|
|
|
// Walls
|
|
const wallGeo = new THREE.BoxGeometry(1.2, 1, 1.2);
|
|
const wallMat = new THREE.MeshLambertMaterial({
|
|
color: wallColors[Math.floor(Math.random() * wallColors.length)]
|
|
});
|
|
const walls = new THREE.Mesh(wallGeo, wallMat);
|
|
walls.position.y = 0.5;
|
|
group.add(walls);
|
|
|
|
// Roof
|
|
const roofGeo = new THREE.ConeGeometry(1, 0.7, 4);
|
|
const roofMat = new THREE.MeshLambertMaterial({
|
|
color: roofColors[Math.floor(Math.random() * roofColors.length)]
|
|
});
|
|
const roof = new THREE.Mesh(roofGeo, roofMat);
|
|
roof.position.y = 1.35;
|
|
roof.rotation.y = Math.PI / 4;
|
|
group.add(roof);
|
|
|
|
// Door
|
|
const doorGeo = new THREE.BoxGeometry(0.25, 0.5, 0.05);
|
|
const doorMat = new THREE.MeshLambertMaterial({ color: 0x5d4037 });
|
|
const door = new THREE.Mesh(doorGeo, doorMat);
|
|
door.position.set(0, 0.3, 0.63);
|
|
group.add(door);
|
|
|
|
// Window
|
|
const winGeo = new THREE.BoxGeometry(0.2, 0.2, 0.05);
|
|
const winMat = new THREE.MeshBasicMaterial({ color: 0x87ceeb });
|
|
const win = new THREE.Mesh(winGeo, winMat);
|
|
win.position.set(0.35, 0.6, 0.63);
|
|
group.add(win);
|
|
|
|
group.scale.setScalar(0.5 + Math.random() * 0.25);
|
|
group.userData = { type: 'house' };
|
|
return group;
|
|
}
|
|
|
|
function createAnimal(continent) {
|
|
const group = new THREE.Group();
|
|
|
|
// Different animals per continent
|
|
let color = 0xffffff;
|
|
let scale = 1;
|
|
|
|
if (continent === 'Antarctica') {
|
|
color = 0x111111; // Penguin
|
|
scale = 0.7;
|
|
} else if (continent === 'Africa') {
|
|
color = [0xd4a574, 0x8b4513, 0x808080, 0xffd700][Math.floor(Math.random() * 4)];
|
|
} else if (continent === 'Oceania') {
|
|
color = [0xa0522d, 0x808080, 0xdeb887][Math.floor(Math.random() * 3)];
|
|
} else {
|
|
color = [0xffffff, 0xa0522d, 0x808080, 0xffa500, 0xf5deb3][Math.floor(Math.random() * 5)];
|
|
}
|
|
|
|
const bodyMat = new THREE.MeshLambertMaterial({ color });
|
|
|
|
// Body
|
|
const bodyGeo = new THREE.SphereGeometry(0.25, 8, 8);
|
|
const body = new THREE.Mesh(bodyGeo, bodyMat);
|
|
body.scale.set(1, 0.7, 1.2);
|
|
group.add(body);
|
|
|
|
// Head
|
|
const headGeo = new THREE.SphereGeometry(0.18, 8, 8);
|
|
const head = new THREE.Mesh(headGeo, bodyMat);
|
|
head.position.set(0, 0.15, 0.25);
|
|
group.add(head);
|
|
|
|
// Eyes
|
|
const eyeGeo = new THREE.SphereGeometry(0.04, 6, 6);
|
|
const eyeMat = new THREE.MeshBasicMaterial({ color: 0x000000 });
|
|
[[-0.06, 0.2, 0.38], [0.06, 0.2, 0.38]].forEach(([x, y, z]) => {
|
|
const eye = new THREE.Mesh(eyeGeo, eyeMat);
|
|
eye.position.set(x, y, z);
|
|
group.add(eye);
|
|
});
|
|
|
|
// Ears
|
|
const earGeo = new THREE.ConeGeometry(0.05, 0.12, 4);
|
|
[[-0.08, 0.32, 0.2], [0.08, 0.32, 0.2]].forEach(([x, y, z]) => {
|
|
const ear = new THREE.Mesh(earGeo, bodyMat);
|
|
ear.position.set(x, y, z);
|
|
group.add(ear);
|
|
});
|
|
|
|
// Legs
|
|
const legGeo = new THREE.CylinderGeometry(0.04, 0.04, 0.15, 4);
|
|
[[-0.1, -0.15, -0.1], [0.1, -0.15, -0.1], [-0.1, -0.15, 0.1], [0.1, -0.15, 0.1]].forEach(([x, y, z]) => {
|
|
const leg = new THREE.Mesh(legGeo, bodyMat);
|
|
leg.position.set(x, y, z);
|
|
group.add(leg);
|
|
});
|
|
|
|
group.scale.setScalar((0.3 + Math.random() * 0.2) * scale);
|
|
group.userData = {
|
|
type: 'animal',
|
|
hop: Math.random() * Math.PI * 2,
|
|
wanderAngle: Math.random() * Math.PI * 2,
|
|
speed: 0.0003 + Math.random() * 0.0002
|
|
};
|
|
return group;
|
|
}
|
|
|
|
function createFlower() {
|
|
const group = new THREE.Group();
|
|
const colors = [0xff69b4, 0xffd700, 0xff6347, 0x9370db, 0x00ced1, 0xffffff, 0xff1d6c];
|
|
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
|
|
// Stem
|
|
const stemGeo = new THREE.CylinderGeometry(0.02, 0.02, 0.4, 4);
|
|
const stemMat = new THREE.MeshLambertMaterial({ color: 0x228b22 });
|
|
const stem = new THREE.Mesh(stemGeo, stemMat);
|
|
stem.position.y = 0.2;
|
|
group.add(stem);
|
|
|
|
// Petals
|
|
const petalGeo = new THREE.SphereGeometry(0.08, 6, 6);
|
|
const petalMat = new THREE.MeshLambertMaterial({ color });
|
|
for (let i = 0; i < 5; i++) {
|
|
const petal = new THREE.Mesh(petalGeo, petalMat);
|
|
petal.scale.set(1, 0.3, 0.5);
|
|
const angle = (i / 5) * Math.PI * 2;
|
|
petal.position.set(Math.cos(angle) * 0.08, 0.4, Math.sin(angle) * 0.08);
|
|
petal.rotation.y = angle;
|
|
petal.rotation.z = 0.3;
|
|
group.add(petal);
|
|
}
|
|
|
|
// Center
|
|
const centerGeo = new THREE.SphereGeometry(0.05, 6, 6);
|
|
const centerMat = new THREE.MeshLambertMaterial({ color: 0xffd700 });
|
|
const center = new THREE.Mesh(centerGeo, centerMat);
|
|
center.position.y = 0.4;
|
|
group.add(center);
|
|
|
|
group.scale.setScalar(0.5 + Math.random() * 0.3);
|
|
group.userData = { type: 'flower', sway: Math.random() * Math.PI * 2 };
|
|
return group;
|
|
}
|
|
|
|
function createAgent() {
|
|
const group = new THREE.Group();
|
|
const colors = [0xff1d6c, 0x2979ff, 0xf5a623, 0x9c27b0];
|
|
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
|
|
// Body
|
|
const bodyGeo = new THREE.CylinderGeometry(0.15, 0.2, 0.4, 8);
|
|
const bodyMat = new THREE.MeshLambertMaterial({ color });
|
|
const body = new THREE.Mesh(bodyGeo, bodyMat);
|
|
body.position.y = 0.2;
|
|
group.add(body);
|
|
|
|
// Head
|
|
const headGeo = new THREE.SphereGeometry(0.18, 12, 12);
|
|
const headMat = new THREE.MeshLambertMaterial({ color: 0xffffff });
|
|
const head = new THREE.Mesh(headGeo, headMat);
|
|
head.position.y = 0.55;
|
|
group.add(head);
|
|
|
|
// Eye
|
|
const eyeGeo = new THREE.CircleGeometry(0.08, 16);
|
|
const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff1d6c });
|
|
const eye = new THREE.Mesh(eyeGeo, eyeMat);
|
|
eye.position.set(0, 0.58, 0.16);
|
|
group.add(eye);
|
|
|
|
// Pupil
|
|
const pupilGeo = new THREE.CircleGeometry(0.03, 12);
|
|
const pupilMat = new THREE.MeshBasicMaterial({ color: 0x000000 });
|
|
const pupil = new THREE.Mesh(pupilGeo, pupilMat);
|
|
pupil.position.set(0, 0.58, 0.17);
|
|
group.add(pupil);
|
|
|
|
// Antenna
|
|
const antGeo = new THREE.CylinderGeometry(0.01, 0.01, 0.15, 4);
|
|
const antMat = new THREE.MeshLambertMaterial({ color: 0x333333 });
|
|
const ant = new THREE.Mesh(antGeo, antMat);
|
|
ant.position.y = 0.8;
|
|
group.add(ant);
|
|
|
|
// Antenna ball
|
|
const ballGeo = new THREE.SphereGeometry(0.04, 8, 8);
|
|
const ballMat = new THREE.MeshBasicMaterial({ color });
|
|
const ball = new THREE.Mesh(ballGeo, ballMat);
|
|
ball.position.y = 0.9;
|
|
group.add(ball);
|
|
|
|
// Glow ring
|
|
const ringGeo = new THREE.TorusGeometry(0.22, 0.02, 6, 24);
|
|
const ringMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.4 });
|
|
const ring = new THREE.Mesh(ringGeo, ringMat);
|
|
ring.rotation.x = Math.PI / 2;
|
|
ring.position.y = 0.05;
|
|
group.add(ring);
|
|
|
|
group.scale.setScalar(0.6);
|
|
group.userData = {
|
|
type: 'agent',
|
|
hover: Math.random() * Math.PI * 2,
|
|
wanderAngle: Math.random() * Math.PI * 2,
|
|
speed: 0.0005 + Math.random() * 0.0003
|
|
};
|
|
return group;
|
|
}
|
|
|
|
// ============ CAMERA ============
|
|
function updateCamera() {
|
|
camera.position.x = spherical.radius * Math.sin(spherical.phi) * Math.sin(spherical.theta);
|
|
camera.position.y = spherical.radius * Math.cos(spherical.phi);
|
|
camera.position.z = spherical.radius * Math.sin(spherical.phi) * Math.cos(spherical.theta);
|
|
camera.lookAt(0, 0, 0);
|
|
}
|
|
|
|
// ============ EVENTS ============
|
|
function onResize() {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
}
|
|
|
|
function onMouseDown(e) {
|
|
isDragging = true;
|
|
previousMouse = { x: e.clientX, y: e.clientY };
|
|
}
|
|
|
|
function onMouseMove(e) {
|
|
if (!isDragging) return;
|
|
const dx = e.clientX - previousMouse.x;
|
|
const dy = e.clientY - previousMouse.y;
|
|
spherical.theta -= dx * 0.005;
|
|
spherical.phi = Math.max(0.2, Math.min(Math.PI - 0.2, spherical.phi + dy * 0.005));
|
|
updateCamera();
|
|
previousMouse = { x: e.clientX, y: e.clientY };
|
|
}
|
|
|
|
function onMouseUp() { isDragging = false; }
|
|
|
|
function onWheel(e) {
|
|
spherical.radius = Math.max(130, Math.min(500, spherical.radius + e.deltaY * 0.25));
|
|
updateCamera();
|
|
}
|
|
|
|
function onTouchStart(e) {
|
|
if (e.touches.length === 1) {
|
|
isDragging = true;
|
|
previousMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
}
|
|
e.preventDefault();
|
|
}
|
|
|
|
function onTouchMove(e) {
|
|
if (!isDragging || e.touches.length !== 1) return;
|
|
const dx = e.touches[0].clientX - previousMouse.x;
|
|
const dy = e.touches[0].clientY - previousMouse.y;
|
|
spherical.theta -= dx * 0.005;
|
|
spherical.phi = Math.max(0.2, Math.min(Math.PI - 0.2, spherical.phi + dy * 0.005));
|
|
updateCamera();
|
|
previousMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
e.preventDefault();
|
|
}
|
|
|
|
function onTouchEnd() { isDragging = false; }
|
|
|
|
// ============ CONTROLS ============
|
|
function toggle(opt) {
|
|
switch(opt) {
|
|
case 'rotate':
|
|
autoRotate = !autoRotate;
|
|
document.getElementById('btnRotate').classList.toggle('active', autoRotate);
|
|
break;
|
|
case 'clouds':
|
|
showClouds = !showClouds;
|
|
clouds.visible = showClouds;
|
|
document.getElementById('btnClouds').classList.toggle('active', showClouds);
|
|
break;
|
|
case 'trees':
|
|
showTrees = !showTrees;
|
|
trees.forEach(t => t.visible = showTrees);
|
|
document.getElementById('btnTrees').classList.toggle('active', showTrees);
|
|
break;
|
|
case 'houses':
|
|
showHouses = !showHouses;
|
|
houses.forEach(h => h.visible = showHouses);
|
|
document.getElementById('btnHouses').classList.toggle('active', showHouses);
|
|
break;
|
|
case 'animals':
|
|
showAnimals = !showAnimals;
|
|
animals.forEach(a => a.visible = showAnimals);
|
|
agents.forEach(a => a.visible = showAnimals);
|
|
document.getElementById('btnAnimals').classList.toggle('active', showAnimals);
|
|
break;
|
|
case 'night':
|
|
isNight = !isNight;
|
|
nightLights.material.opacity = isNight ? 0.9 : 0;
|
|
window.sunLight.intensity = isNight ? 0.15 : 1.4;
|
|
window.ambientLight.intensity = isNight ? 0.15 : 0.4;
|
|
document.getElementById('btnNight').classList.toggle('active', isNight);
|
|
break;
|
|
}
|
|
}
|
|
|
|
function updateCounts() {
|
|
document.getElementById('treeCount').textContent = trees.length.toLocaleString();
|
|
document.getElementById('houseCount').textContent = houses.length.toLocaleString();
|
|
document.getElementById('animalCount').textContent = animals.length.toLocaleString();
|
|
document.getElementById('agentCount').textContent = agents.length.toLocaleString();
|
|
document.getElementById('flowerCount').textContent = flowers.length.toLocaleString();
|
|
document.getElementById('villageCount').textContent = CONTINENTS.reduce((a, c) => a + c.regions.length, 0);
|
|
}
|
|
|
|
// ============ ANIMATION ============
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
time += 0.016;
|
|
|
|
// Auto rotate
|
|
if (autoRotate && !isDragging) {
|
|
spherical.theta += 0.0008;
|
|
updateCamera();
|
|
}
|
|
|
|
// Rotate Earth
|
|
earth.rotation.y += 0.0002;
|
|
nightLights.rotation.y = earth.rotation.y;
|
|
clouds.rotation.y += 0.00025;
|
|
|
|
// Animate trees (sway)
|
|
trees.forEach(tree => {
|
|
tree.userData.sway += 0.015;
|
|
tree.rotation.z = Math.sin(tree.userData.sway) * 0.03;
|
|
});
|
|
|
|
// Animate flowers
|
|
flowers.forEach(flower => {
|
|
flower.userData.sway += 0.02;
|
|
flower.rotation.z = Math.sin(flower.userData.sway) * 0.05;
|
|
});
|
|
|
|
// Animate animals
|
|
animals.forEach(animal => {
|
|
animal.userData.hop += 0.08;
|
|
|
|
// Slight hopping
|
|
const hopHeight = Math.abs(Math.sin(animal.userData.hop)) * 0.15;
|
|
|
|
// Move position slightly
|
|
if (animal.userData.region) {
|
|
animal.userData.wanderAngle += (Math.random() - 0.5) * 0.02;
|
|
|
|
const newLat = animal.userData.lat + Math.cos(animal.userData.wanderAngle) * animal.userData.speed;
|
|
const newLng = animal.userData.lng + Math.sin(animal.userData.wanderAngle) * animal.userData.speed;
|
|
|
|
// Keep within region bounds
|
|
const region = animal.userData.region;
|
|
const distFromCenter = Math.sqrt(
|
|
Math.pow(newLat - region.lat, 2) +
|
|
Math.pow(newLng - region.lng, 2)
|
|
);
|
|
|
|
if (distFromCenter < region.radius * 0.4) {
|
|
animal.userData.lat = newLat;
|
|
animal.userData.lng = newLng;
|
|
|
|
const pos = latLngToVector3(newLat, newLng, EARTH_RADIUS + 0.5 + hopHeight);
|
|
animal.position.copy(pos);
|
|
|
|
const normal = pos.clone().normalize();
|
|
animal.lookAt(pos.clone().add(normal));
|
|
animal.rotateX(Math.PI / 2);
|
|
} else {
|
|
animal.userData.wanderAngle += Math.PI;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Animate agents
|
|
agents.forEach(agent => {
|
|
agent.userData.hover += 0.04;
|
|
const hoverHeight = Math.sin(agent.userData.hover) * 0.5 + 3;
|
|
|
|
if (agent.userData.region) {
|
|
agent.userData.wanderAngle += (Math.random() - 0.5) * 0.015;
|
|
|
|
const newLat = agent.userData.lat + Math.cos(agent.userData.wanderAngle) * agent.userData.speed;
|
|
const newLng = agent.userData.lng + Math.sin(agent.userData.wanderAngle) * agent.userData.speed;
|
|
|
|
const region = agent.userData.region;
|
|
const distFromCenter = Math.sqrt(
|
|
Math.pow(newLat - region.lat, 2) +
|
|
Math.pow(newLng - region.lng, 2)
|
|
);
|
|
|
|
if (distFromCenter < region.radius * 0.35) {
|
|
agent.userData.lat = newLat;
|
|
agent.userData.lng = newLng;
|
|
|
|
const pos = latLngToVector3(newLat, newLng, EARTH_RADIUS + hoverHeight);
|
|
agent.position.copy(pos);
|
|
|
|
const normal = pos.clone().normalize();
|
|
agent.lookAt(pos.clone().add(normal));
|
|
agent.rotateX(Math.PI / 2);
|
|
} else {
|
|
agent.userData.wanderAngle += Math.PI;
|
|
}
|
|
}
|
|
});
|
|
|
|
// UI
|
|
const now = new Date();
|
|
document.getElementById('utcTime').textContent =
|
|
now.toUTCString().split(' ')[4].substring(0, 5);
|
|
document.getElementById('rotation').textContent =
|
|
((earth.rotation.y * 180 / Math.PI) % 360).toFixed(1) + '°';
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
// Start
|
|
init();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|