🌈 Add Light Trinity system (RedLight + GreenLight + YellowLight)

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
🌸
This commit is contained in:
Alexa Louise
2025-12-23 15:47:25 -06:00
parent 40150e4d58
commit f9ec2879ba
47 changed files with 43897 additions and 0 deletions

View File

@@ -0,0 +1,800 @@
<!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;
}
#canvas-container { position: fixed; inset: 0; }
.logo {
position: fixed;
top: 20px;
left: 20px;
display: flex;
align-items: center;
gap: 10px;
background: rgba(0,0,0,0.75);
padding: 10px 18px;
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 {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 20px;
background: rgba(0,0,0,0.75);
padding: 10px 24px;
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: 6px; }
.stat-icon { font-size: 15px; }
.stat-value { font-size: 13px; font-weight: 600; color: #fff; }
.biome-panel {
position: fixed;
top: 20px;
right: 20px;
background: rgba(0,0,0,0.75);
padding: 14px 18px;
border-radius: 16px;
backdrop-filter: blur(20px);
border: 1px solid rgba(255,255,255,0.1);
z-index: 100;
max-height: 70vh;
overflow-y: auto;
}
.biome-title {
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: rgba(255,255,255,0.4);
margin-bottom: 10px;
}
.biome-list { display: flex; flex-direction: column; gap: 4px; }
.biome-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
color: rgba(255,255,255,0.8);
}
.biome-dot { width: 8px; height: 8px; border-radius: 50%; }
.biome-count { margin-left: auto; color: rgba(255,255,255,0.5); font-size: 10px; }
.info-panel {
position: fixed;
bottom: 20px;
left: 20px;
background: rgba(0,0,0,0.75);
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;
}
.info-panel.visible { opacity: 1; transform: translateY(0); }
.info-biome { font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 4px; }
.info-name { font-size: 18px; font-weight: 600; color: #fff; margin-bottom: 8px; }
.info-desc { font-size: 12px; color: rgba(255,255,255,0.6); line-height: 1.4; }
.info-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(255,255,255,0.1);
}
.info-stat { text-align: center; }
.info-stat-icon { font-size: 14px; }
.info-stat-value { font-size: 13px; font-weight: 600; color: #fff; }
.info-stat-label { font-size: 8px; color: rgba(255,255,255,0.4); text-transform: uppercase; }
.controls {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 6px;
z-index: 100;
}
.ctrl-btn {
width: 42px; height: 42px;
border: none; border-radius: 50%;
background: rgba(0,0,0,0.75);
backdrop-filter: blur(20px);
border: 1px solid rgba(255,255,255,0.1);
color: #fff; font-size: 16px;
cursor: pointer;
transition: all 0.2s ease;
}
.ctrl-btn:hover { background: rgba(255,29,108,0.4); transform: scale(1.1); }
.ctrl-btn.active { background: #FF1D6C; border-color: #FF1D6C; }
.fps {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.6);
padding: 6px 14px;
border-radius: 20px;
font-size: 11px;
color: rgba(255,255,255,0.6);
z-index: 100;
}
.loading {
position: fixed;
inset: 0;
background: #000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
transition: opacity 0.6s ease;
}
.loading.hidden { opacity: 0; pointer-events: none; }
.loading-spinner {
width: 60px; height: 60px;
border: 3px solid rgba(255,255,255,0.1);
border-top-color: #FF1D6C;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading-text { margin-top: 20px; color: rgba(255,255,255,0.7); font-size: 14px; }
</style>
</head>
<body>
<div class="loading" id="loading">
<div class="loading-spinner"></div>
<div class="loading-text">Loading biomes...</div>
</div>
<div id="canvas-container"></div>
<div class="logo">
<div class="logo-icon"></div>
<span class="logo-text">BlackRoad Earth</span>
</div>
<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>
<div class="biome-panel">
<div class="biome-title">Biomes</div>
<div class="biome-list" id="biomeList"></div>
</div>
<div class="info-panel" id="infoPanel">
<div class="info-biome" id="infoBiome">Tropical Rainforest</div>
<div class="info-name" id="infoName">Amazon Basin</div>
<div class="info-desc" id="infoDesc">Description...</div>
<div class="info-stats">
<div class="info-stat">
<div class="info-stat-icon">🌳</div>
<div class="info-stat-value" id="infoTrees">0</div>
<div class="info-stat-label">Trees</div>
</div>
<div class="info-stat">
<div class="info-stat-icon">🐾</div>
<div class="info-stat-value" id="infoAnimals">0</div>
<div class="info-stat-label">Animals</div>
</div>
<div class="info-stat">
<div class="info-stat-icon">🌡️</div>
<div class="info-stat-value" id="infoTemp"></div>
<div class="info-stat-label">Temp</div>
</div>
</div>
</div>
<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="btnLife" title="Life">🌿</button>
<button class="ctrl-btn" id="btnNight" title="Night">🌙</button>
</div>
<div class="fps" id="fps">60 FPS</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// ============ CONFIG ============
const EARTH_RADIUS = 100;
const QUALITY = {
treesPerRegion: 15, // Reduced from 40
animalsPerRegion: 5, // Reduced from 15
agentsPerRegion: 2, // Reduced from 3
housesPerRegion: 3, // Reduced from 8
animateEveryNthFrame: 2 // Only animate every 2nd frame
};
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',
clouds: 'https://unpkg.com/three-globe/example/img/earth-clouds.png',
night: 'https://unpkg.com/three-globe/example/img/earth-night.jpg'
};
// Simplified biomes
const BIOMES = {
TROPICAL_RAINFOREST: { name: "Tropical Rainforest", color: 0x1a5a1a, icon: "🌴", temp: "27°C", treeColor: 0x0d5c0d },
TEMPERATE_FOREST: { name: "Temperate Forest", color: 0x2d5a27, icon: "🌲", temp: "12°C", treeColor: 0x228b22 },
BOREAL_TAIGA: { name: "Boreal Taiga", color: 0x1a4a2a, icon: "🌲", temp: "-5°C", treeColor: 0x0a3a0a },
TUNDRA: { name: "Tundra", color: 0x8fbc8f, icon: "❄️", temp: "-20°C", treeColor: 0x556b2f },
DESERT_HOT: { name: "Hot Desert", color: 0xc2b280, icon: "🏜️", temp: "38°C", treeColor: 0x228b22 },
SAVANNA: { name: "Savanna", color: 0xbdb76b, icon: "🦁", temp: "25°C", treeColor: 0x6b8e23 },
GRASSLAND: { name: "Grassland", color: 0x9acd32, icon: "🌾", temp: "15°C", treeColor: 0x7cba3d },
MOUNTAIN: { name: "Mountain", color: 0x696969, icon: "⛰️", temp: "-8°C", treeColor: 0x1a4a1a },
ICE_SHEET: { name: "Polar Ice", color: 0xf0f8ff, icon: "🧊", temp: "-50°C", treeColor: 0xffffff },
WETLAND: { name: "Wetland", color: 0x2f4f4f, icon: "🐊", temp: "22°C", treeColor: 0x2f4f2f }
};
// Reduced regions - key locations only
const BIOME_REGIONS = [
// Rainforests
{ biome: 'TROPICAL_RAINFOREST', name: "Amazon Basin", lat: -3, lng: -60, radius: 18, desc: "World's largest rainforest" },
{ biome: 'TROPICAL_RAINFOREST', name: "Congo Rainforest", lat: 0, lng: 22, radius: 12, desc: "Africa's largest rainforest" },
{ biome: 'TROPICAL_RAINFOREST', name: "Southeast Asia", lat: 2, lng: 110, radius: 10, desc: "Borneo and Sumatra forests" },
// Temperate
{ biome: 'TEMPERATE_FOREST', name: "Eastern USA", lat: 38, lng: -80, radius: 12, desc: "Appalachian forests" },
{ biome: 'TEMPERATE_FOREST', name: "Western Europe", lat: 48, lng: 8, radius: 10, desc: "European woodlands" },
{ biome: 'TEMPERATE_FOREST', name: "East Asia", lat: 36, lng: 138, radius: 8, desc: "Japanese and Korean forests" },
// Boreal
{ biome: 'BOREAL_TAIGA', name: "Canadian Boreal", lat: 58, lng: -100, radius: 22, desc: "World's largest intact forest" },
{ biome: 'BOREAL_TAIGA', name: "Siberian Taiga", lat: 62, lng: 100, radius: 25, desc: "Largest terrestrial biome" },
// Tundra
{ biome: 'TUNDRA', name: "Arctic Tundra", lat: 72, lng: -100, radius: 18, desc: "Frozen treeless plains" },
{ biome: 'TUNDRA', name: "Siberian Tundra", lat: 72, lng: 130, radius: 16, desc: "Northern Russia permafrost" },
// Deserts
{ biome: 'DESERT_HOT', name: "Sahara Desert", lat: 24, lng: 10, radius: 25, desc: "World's largest hot desert" },
{ biome: 'DESERT_HOT', name: "Arabian Desert", lat: 23, lng: 50, radius: 12, desc: "Arabian Peninsula" },
{ biome: 'DESERT_HOT', name: "Australian Outback", lat: -24, lng: 135, radius: 18, desc: "Red heart of Australia" },
// Savanna
{ biome: 'SAVANNA', name: "Serengeti", lat: -3, lng: 35, radius: 10, desc: "Great migration lands" },
{ biome: 'SAVANNA', name: "Brazilian Cerrado", lat: -15, lng: -48, radius: 12, desc: "South American savanna" },
// Grassland
{ biome: 'GRASSLAND', name: "Great Plains", lat: 42, lng: -100, radius: 15, desc: "North American prairie" },
{ biome: 'GRASSLAND', name: "Eurasian Steppe", lat: 48, lng: 65, radius: 20, desc: "Vast grasslands" },
// Mountains
{ biome: 'MOUNTAIN', name: "Himalayas", lat: 28, lng: 85, radius: 12, desc: "World's highest peaks" },
{ biome: 'MOUNTAIN', name: "Rocky Mountains", lat: 45, lng: -110, radius: 10, desc: "North American spine" },
{ biome: 'MOUNTAIN', name: "Andes", lat: -15, lng: -72, radius: 10, desc: "Longest mountain range" },
{ biome: 'MOUNTAIN', name: "European Alps", lat: 46, lng: 10, radius: 6, desc: "Central European peaks" },
// Ice
{ biome: 'ICE_SHEET', name: "Antarctica", lat: -82, lng: 0, radius: 28, desc: "Frozen continent" },
{ biome: 'ICE_SHEET', name: "Greenland", lat: 75, lng: -42, radius: 15, desc: "Arctic ice sheet" },
// Wetlands
{ biome: 'WETLAND', name: "Pantanal", lat: -18, lng: -57, radius: 8, desc: "World's largest wetland" },
{ biome: 'WETLAND', name: "Everglades", lat: 26, lng: -81, radius: 5, desc: "River of grass" }
];
// ============ GLOBALS ============
let scene, camera, renderer;
let earth, clouds, nightLights;
let lifeGroup;
let regionData = [];
let counts = { trees: 0, houses: 0, animals: 0, agents: 0 };
let frameCount = 0;
let lastTime = performance.now();
let fps = 60;
let autoRotate = true;
let showClouds = true;
let showLife = true;
let isNight = false;
let isDragging = false;
let prevMouse = { x: 0, y: 0 };
let spherical = { theta: 0, phi: Math.PI / 3, radius: 250 };
// ============ INIT ============
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000008);
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000);
updateCamera();
renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); // Cap pixel ratio
document.getElementById('canvas-container').appendChild(renderer.domElement);
// Load and build
loadTextures(() => {
createScene();
populateBiomes();
buildUI();
document.getElementById('loading').classList.add('hidden');
animate();
});
// Events
window.addEventListener('resize', onResize);
renderer.domElement.addEventListener('mousedown', e => { isDragging = true; prevMouse = { x: e.clientX, y: e.clientY }; });
renderer.domElement.addEventListener('mousemove', onDrag);
renderer.domElement.addEventListener('mouseup', () => isDragging = false);
renderer.domElement.addEventListener('mouseleave', () => isDragging = false);
renderer.domElement.addEventListener('wheel', e => {
spherical.radius = Math.max(140, Math.min(400, spherical.radius + e.deltaY * 0.15));
updateCamera();
}, { passive: true });
renderer.domElement.addEventListener('click', onClick);
// Touch
renderer.domElement.addEventListener('touchstart', e => {
if (e.touches.length === 1) { isDragging = true; prevMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY }; }
}, { passive: true });
renderer.domElement.addEventListener('touchmove', e => {
if (!isDragging || e.touches.length !== 1) return;
const dx = e.touches[0].clientX - prevMouse.x;
const dy = e.touches[0].clientY - prevMouse.y;
spherical.theta -= dx * 0.005;
spherical.phi = Math.max(0.3, Math.min(Math.PI - 0.3, spherical.phi + dy * 0.005));
updateCamera();
prevMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
}, { passive: true });
renderer.domElement.addEventListener('touchend', () => isDragging = false);
// Buttons
document.getElementById('btnRotate').onclick = () => { autoRotate = !autoRotate; document.getElementById('btnRotate').classList.toggle('active', autoRotate); };
document.getElementById('btnClouds').onclick = () => { showClouds = !showClouds; clouds.visible = showClouds; document.getElementById('btnClouds').classList.toggle('active', showClouds); };
document.getElementById('btnLife').onclick = () => { showLife = !showLife; lifeGroup.visible = showLife; document.getElementById('btnLife').classList.toggle('active', showLife); };
document.getElementById('btnNight').onclick = () => {
isNight = !isNight;
nightLights.material.opacity = isNight ? 0.8 : 0;
window.sunLight.intensity = isNight ? 0.15 : 1.2;
document.getElementById('btnNight').classList.toggle('active', isNight);
};
}
function loadTextures(callback) {
const loader = new THREE.TextureLoader();
let loaded = 0;
const total = 4;
window.tex = {};
Object.entries(TEXTURES).forEach(([key, url]) => {
loader.load(url, tex => {
window.tex[key] = tex;
loaded++;
if (loaded === total) callback();
});
});
}
function createScene() {
// Stars - simple points
const starGeo = new THREE.BufferGeometry();
const starPos = new Float32Array(3000 * 3);
for (let i = 0; i < 3000; i++) {
const r = 600 + Math.random() * 800;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(Math.random() * 2 - 1);
starPos[i*3] = r * Math.sin(phi) * Math.cos(theta);
starPos[i*3+1] = r * Math.sin(phi) * Math.sin(theta);
starPos[i*3+2] = r * Math.cos(phi);
}
starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
scene.add(new THREE.Points(starGeo, new THREE.PointsMaterial({ color: 0xffffff, size: 0.8 })));
// Lights
window.sunLight = new THREE.DirectionalLight(0xffffff, 1.2);
window.sunLight.position.set(300, 150, 300);
scene.add(window.sunLight);
scene.add(new THREE.AmbientLight(0x404060, 0.35));
// Earth
const earthGeo = new THREE.SphereGeometry(EARTH_RADIUS, 64, 64); // Reduced segments
earth = new THREE.Mesh(earthGeo, new THREE.MeshPhongMaterial({
map: window.tex.earth,
bumpMap: window.tex.bump,
bumpScale: 0.3,
shininess: 3
}));
scene.add(earth);
// Night lights
nightLights = new THREE.Mesh(earthGeo.clone(), new THREE.MeshBasicMaterial({
map: window.tex.night,
blending: THREE.AdditiveBlending,
transparent: true,
opacity: 0
}));
nightLights.scale.setScalar(1.002);
scene.add(nightLights);
// Clouds
clouds = new THREE.Mesh(
new THREE.SphereGeometry(EARTH_RADIUS + 1, 48, 48),
new THREE.MeshPhongMaterial({ map: window.tex.clouds, transparent: true, opacity: 0.25, depthWrite: false })
);
scene.add(clouds);
// Atmosphere
scene.add(new THREE.Mesh(
new THREE.SphereGeometry(EARTH_RADIUS + 8, 32, 32),
new THREE.ShaderMaterial({
vertexShader: `varying vec3 vN; void main() { vN = normalize(normalMatrix * normal); gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
fragmentShader: `varying vec3 vN; void main() { float i = pow(0.6 - dot(vN, vec3(0,0,1)), 2.0); gl_FragColor = vec4(0.3, 0.6, 1.0, i * 0.35); }`,
blending: THREE.AdditiveBlending,
side: THREE.BackSide,
transparent: true,
depthWrite: false
})
));
// Life container
lifeGroup = new THREE.Group();
scene.add(lifeGroup);
}
// ============ POPULATE ============
function populateBiomes() {
BIOME_REGIONS.forEach(region => {
const biome = BIOMES[region.biome];
const scale = region.radius / 12;
const treesCount = Math.max(1, Math.floor(QUALITY.treesPerRegion * scale * (biome.name.includes('Desert') || biome.name.includes('Ice') ? 0.1 : 1)));
const housesCount = Math.max(0, Math.floor(QUALITY.housesPerRegion * scale * (biome.name.includes('Ice') ? 0.1 : 1)));
const animalsCount = Math.max(1, Math.floor(QUALITY.animalsPerRegion * scale));
const agentsCount = Math.floor(QUALITY.agentsPerRegion * scale);
const regionCounts = { trees: 0, animals: 0, houses: 0, agents: 0 };
// Trees - simple merged geometry approach
for (let i = 0; i < treesCount; i++) {
const tree = createSimpleTree(biome);
placeOnGlobe(tree, region);
lifeGroup.add(tree);
regionCounts.trees++;
counts.trees++;
}
// Houses
for (let i = 0; i < housesCount; i++) {
const house = createSimpleHouse(biome);
placeOnGlobe(house, region);
lifeGroup.add(house);
regionCounts.houses++;
counts.houses++;
}
// Animals - very simple
for (let i = 0; i < animalsCount; i++) {
const animal = createSimpleAnimal(biome);
placeOnGlobe(animal, region);
animal.userData.region = region;
lifeGroup.add(animal);
regionCounts.animals++;
counts.animals++;
}
// Agents
for (let i = 0; i < agentsCount; i++) {
const agent = createSimpleAgent();
placeOnGlobe(agent, region, 1.5);
agent.userData.region = region;
lifeGroup.add(agent);
regionCounts.agents++;
counts.agents++;
}
regionData.push({ ...region, biome, counts: regionCounts });
});
// Update UI counts
document.getElementById('treeCount').textContent = counts.trees;
document.getElementById('houseCount').textContent = counts.houses;
document.getElementById('animalCount').textContent = counts.animals;
document.getElementById('agentCount').textContent = counts.agents;
}
function placeOnGlobe(obj, region, heightOffset = 0) {
const angle = Math.random() * Math.PI * 2;
const dist = Math.random() * region.radius * 0.75;
const lat = region.lat + Math.cos(angle) * dist * 0.5;
const lng = region.lng + Math.sin(angle) * dist * 0.7;
const phi = (90 - lat) * Math.PI / 180;
const theta = (lng + 180) * Math.PI / 180;
const r = EARTH_RADIUS + 0.2 + heightOffset;
obj.position.set(
r * Math.sin(phi) * Math.cos(theta),
r * Math.cos(phi),
r * Math.sin(phi) * Math.sin(theta)
);
const normal = obj.position.clone().normalize();
obj.lookAt(obj.position.clone().add(normal));
obj.rotateX(Math.PI / 2);
obj.rotateY(Math.random() * Math.PI * 2);
obj.userData.lat = lat;
obj.userData.lng = lng;
}
// ============ SIMPLE CREATORS ============
function createSimpleTree(biome) {
const group = new THREE.Group();
if (biome.name.includes('Desert')) {
// Cactus
const mat = new THREE.MeshLambertMaterial({ color: 0x228b22 });
const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.1, 0.8, 5), mat);
trunk.position.y = 0.4;
group.add(trunk);
} else if (biome.name.includes('Ice') || biome.name.includes('Tundra')) {
// Shrub
const mat = new THREE.MeshLambertMaterial({ color: 0x556b2f });
const bush = new THREE.Mesh(new THREE.SphereGeometry(0.2, 4, 4), mat);
bush.position.y = 0.15;
group.add(bush);
} else if (biome.name.includes('Savanna')) {
// Acacia
const trunkMat = new THREE.MeshLambertMaterial({ color: 0x4a3728 });
const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.08, 1, 5), trunkMat);
trunk.position.y = 0.5;
group.add(trunk);
const canopy = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.6, 0.15, 6), new THREE.MeshLambertMaterial({ color: biome.treeColor }));
canopy.position.y = 1.1;
group.add(canopy);
} else if (biome.name.includes('Boreal') || biome.name.includes('Mountain')) {
// Pine
const trunkMat = new THREE.MeshLambertMaterial({ color: 0x4a3728 });
const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.08, 0.6, 5), trunkMat);
trunk.position.y = 0.3;
group.add(trunk);
const cone = new THREE.Mesh(new THREE.ConeGeometry(0.35, 1, 5), new THREE.MeshLambertMaterial({ color: biome.treeColor }));
cone.position.y = 0.9;
group.add(cone);
} else {
// Regular tree
const trunkMat = new THREE.MeshLambertMaterial({ color: 0x4a3728 });
const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.1, 0.6, 5), trunkMat);
trunk.position.y = 0.3;
group.add(trunk);
const leaves = new THREE.Mesh(new THREE.IcosahedronGeometry(0.5, 0), new THREE.MeshLambertMaterial({ color: biome.treeColor }));
leaves.position.y = 0.8;
group.add(leaves);
}
group.scale.setScalar(0.4 + Math.random() * 0.2);
return group;
}
function createSimpleHouse(biome) {
const group = new THREE.Group();
const wallColor = biome.name.includes('Desert') ? 0xf5deb3 : 0xfaf8f5;
const roofColor = biome.name.includes('Desert') ? 0xdaa520 : 0xcc4444;
const walls = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.4, 0.5), new THREE.MeshLambertMaterial({ color: wallColor }));
walls.position.y = 0.2;
group.add(walls);
const roof = new THREE.Mesh(new THREE.ConeGeometry(0.45, 0.3, 4), new THREE.MeshLambertMaterial({ color: roofColor }));
roof.position.y = 0.55;
roof.rotation.y = Math.PI / 4;
group.add(roof);
group.scale.setScalar(0.5);
return group;
}
function createSimpleAnimal(biome) {
const group = new THREE.Group();
const color = biome.name.includes('Ice') ? 0xffffff :
biome.name.includes('Savanna') ? 0xd4a574 :
0x8b7355;
const mat = new THREE.MeshLambertMaterial({ color });
const body = new THREE.Mesh(new THREE.SphereGeometry(0.12, 6, 6), mat);
body.scale.set(1, 0.7, 1.2);
group.add(body);
const head = new THREE.Mesh(new THREE.SphereGeometry(0.08, 6, 6), mat);
head.position.set(0, 0.05, 0.12);
group.add(head);
group.scale.setScalar(0.5);
group.userData.hop = Math.random() * Math.PI * 2;
return group;
}
function createSimpleAgent() {
const group = new THREE.Group();
const colors = [0xff1d6c, 0x2979ff, 0xf5a623, 0x9c27b0];
const color = colors[Math.floor(Math.random() * colors.length)];
const body = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.1, 0.25, 6), new THREE.MeshLambertMaterial({ color }));
body.position.y = 0.12;
group.add(body);
const head = new THREE.Mesh(new THREE.SphereGeometry(0.1, 8, 8), new THREE.MeshLambertMaterial({ color: 0xffffff }));
head.position.y = 0.32;
group.add(head);
group.scale.setScalar(0.6);
group.userData.hover = Math.random() * Math.PI * 2;
return group;
}
// ============ UI ============
function buildUI() {
const list = document.getElementById('biomeList');
const biomeCounts = {};
BIOME_REGIONS.forEach(r => { biomeCounts[r.biome] = (biomeCounts[r.biome] || 0) + 1; });
Object.entries(BIOMES).forEach(([key, biome]) => {
if (!biomeCounts[key]) return;
const item = document.createElement('div');
item.className = 'biome-item';
item.innerHTML = `<div class="biome-dot" style="background:#${biome.color.toString(16).padStart(6,'0')}"></div>
<span>${biome.icon} ${biome.name}</span>
<span class="biome-count">${biomeCounts[key]}</span>`;
list.appendChild(item);
});
}
function showInfo(region) {
document.getElementById('infoBiome').textContent = region.biome.icon + ' ' + region.biome.name;
document.getElementById('infoBiome').style.color = '#' + region.biome.color.toString(16).padStart(6, '0');
document.getElementById('infoName').textContent = region.name;
document.getElementById('infoDesc').textContent = region.desc;
document.getElementById('infoTrees').textContent = region.counts.trees;
document.getElementById('infoAnimals').textContent = region.counts.animals;
document.getElementById('infoTemp').textContent = region.biome.temp;
document.getElementById('infoPanel').classList.add('visible');
clearTimeout(window.infoTimeout);
window.infoTimeout = setTimeout(() => document.getElementById('infoPanel').classList.remove('visible'), 4000);
}
// ============ EVENTS ============
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);
}
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function onDrag(e) {
if (!isDragging) return;
const dx = e.clientX - prevMouse.x;
const dy = e.clientY - prevMouse.y;
spherical.theta -= dx * 0.005;
spherical.phi = Math.max(0.3, Math.min(Math.PI - 0.3, spherical.phi + dy * 0.005));
updateCamera();
prevMouse = { x: e.clientX, y: e.clientY };
}
function onClick(e) {
const mouse = new THREE.Vector2(
(e.clientX / window.innerWidth) * 2 - 1,
-(e.clientY / window.innerHeight) * 2 + 1
);
const ray = new THREE.Raycaster();
ray.setFromCamera(mouse, camera);
const hits = ray.intersectObject(earth);
if (hits.length > 0) {
const p = hits[0].point.normalize().multiplyScalar(EARTH_RADIUS);
const lat = 90 - Math.acos(p.y / EARTH_RADIUS) * 180 / Math.PI;
const lng = Math.atan2(p.z, p.x) * 180 / Math.PI - 180;
let nearest = null, minDist = Infinity;
regionData.forEach(r => {
const d = Math.sqrt((lat - r.lat) ** 2 + (lng - r.lng) ** 2);
if (d < r.radius && d < minDist) { minDist = d; nearest = r; }
});
if (nearest) showInfo(nearest);
}
}
// ============ ANIMATION ============
function animate() {
requestAnimationFrame(animate);
frameCount++;
// FPS counter
if (frameCount % 30 === 0) {
const now = performance.now();
fps = Math.round(30000 / (now - lastTime));
lastTime = now;
document.getElementById('fps').textContent = fps + ' FPS';
}
// Rotate earth slowly
earth.rotation.y += 0.0002;
nightLights.rotation.y = earth.rotation.y;
clouds.rotation.y += 0.00025;
lifeGroup.rotation.y = earth.rotation.y;
// Auto rotate camera
if (autoRotate && !isDragging) {
spherical.theta += 0.0008;
updateCamera();
}
// Animate life only every Nth frame
if (frameCount % QUALITY.animateEveryNthFrame === 0) {
lifeGroup.children.forEach(obj => {
if (obj.userData.hop !== undefined) {
obj.userData.hop += 0.1;
}
if (obj.userData.hover !== undefined) {
obj.userData.hover += 0.05;
// Slight bobbing
obj.position.y += Math.sin(obj.userData.hover) * 0.002;
}
});
}
renderer.render(scene, camera);
}
init();
</script>
</body>
</html>