Files
blackroad-os-web/.trinity/redlight/templates/blackroad-living-earth.html
Alexa Louise f9ec2879ba 🌈 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
🌸
2025-12-23 15:47:25 -06:00

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>