Skip to content

Instantly share code, notes, and snippets.

@celsowm
Created February 7, 2026 20:36
Show Gist options
  • Select an option

  • Save celsowm/9345cb404dc08a321e1cb92405cddae8 to your computer and use it in GitHub Desktop.

Select an option

Save celsowm/9345cb404dc08a321e1cb92405cddae8 to your computer and use it in GitHub Desktop.
procedural rio janeiro metal gear vr style background
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Holographic City</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { overflow: hidden; background: #000; cursor: crosshair; } canvas { display: block; } #scanlines { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 11; background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.04) 2px, rgba(0,0,0,0.04) 4px); } .hud-base { position: fixed; color: rgba(60,60,60,0.7); font-family: 'Courier New', monospace; pointer-events: none; z-index: 12; } #hud { top: 20px; left: 20px; font-size: 11px; line-height: 1.8; } #hud-right { top: 20px; right: 20px; font-size: 10px; text-align: right; line-height: 1.8; } #hud-bottom { bottom: 20px; left: 50%; transform: translateX(-50%); color: rgba(80,80,80,0.5); font-size: 9px; letter-spacing: 2px; } </style> </head> <body> <div id="scanlines"></div> <div id="hud" class="hud-base"> <div>SECTOR: <span id="sector">7G-NORTH</span></div> <div>FREQ: <span id="freq">441.2</span> MHz</div> <div>NODES: <span id="nodes">2847</span></div> <div>LATENCY: <span id="latency">12</span>ms</div> <div>──────────────</div> <div>STREAM: <span id="datastream">ACTIVE</span></div> <div>BLOCKS: <span id="blocks">0</span></div> <div>TRAFFIC: <span id="traffic">0</span></div> </div> <div id="hud-right" class="hud-base"> <div><span id="clock">00:00:00</span></div> <div>FRM: <span id="frame">0</span></div> <div id="dataflow">████░░██░░</div> </div> <div id="hud-bottom" class="hud-base">[ DRAG TO ROTATE · SCROLL TO ZOOM · HOLOGRAPHIC URBAN MATRIX v4.2 ]</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(function() {
var renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setClearColor(0xd0d0d0, 1);
renderer.shadowMap.enabled = false;
document.body.appendChild(renderer.domElement);
var scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0xcccccc, 0.005);
var camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.5, 600);
var angleX = 0, angleY = 0.6, radius = 90, targetRadius = 90;
var dragging = false, lastMX = 0, lastMY = 0;
renderer.domElement.addEventListener('mousedown', function(e) {
dragging = true; lastMX = e.clientX; lastMY = e.clientY;
});
window.addEventListener('mouseup', function() { dragging = false; });
window.addEventListener('mousemove', function(e) {
if (!dragging) return;
angleX -= (e.clientX - lastMX) * 0.005;
angleY = Math.max(0.1, Math.min(1.4, angleY + (e.clientY - lastMY) * 0.005));
lastMX = e.clientX; lastMY = e.clientY;
});
window.addEventListener('wheel', function(e) {
targetRadius = Math.max(20, Math.min(180, targetRadius + e.deltaY * 0.08));
});
window.addEventListener('resize', function() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
function rand(a, b) { return a + Math.random() * (b - a); }
function randInt(a, b) { return Math.floor(rand(a, b)); }
function createMat(color, opacity, wireframe, side) {
var opts = { color: color, transparent: true, opacity: opacity };
if (wireframe) opts.wireframe = true;
if (side) opts.side = side;
return new THREE.MeshBasicMaterial(opts);
}
function createLineMat(color, opacity) {
return new THREE.LineBasicMaterial({ color: color, transparent: true, opacity: opacity });
}
var edgeMat = createLineMat(0x222222, 0.6);
var edgeMatDim = createLineMat(0x444444, 0.3);
function makeBuildingMat(opacity) { return createMat(0x888888, opacity); }
function makeWireMat(opacity) { return createMat(0x666666, opacity, true); }
var blinkLights = [], windowPoints = [], cityGroup = new THREE.Group();
var cityExtent = 80;
function generatePositions(start, extent, min, max) {
var positions = [];
var pos = start;
while (pos < extent) { positions.push(pos); pos += rand(min, max); }
positions.push(extent);
return positions;
}
var avenuePositions = generatePositions(-cityExtent, cityExtent, 12, 26);
var streetPositions = generatePositions(-cityExtent, cityExtent, 10, 22);
var majorStreetWidth = 3.5, minorStreetWidth = 2.0, sidewalkWidth = 0.8;
var streetMat = createMat(0x999999, 0.12);
var sidewalkMat = createMat(0x8a8a8a, 0.08);
var roadLineMat = createLineMat(0x666666, 0.35);
var curbMat = createLineMat(0x555555, 0.4);
function createPlane(width, height, material, rotation, position) {
var mesh = new THREE.Mesh(new THREE.PlaneGeometry(width, height), material);
mesh.rotation.x = rotation;
mesh.position.set(position[0], position[1], position[2]);
return mesh;
}
function createLine(points, material) {
var geo = new THREE.BufferGeometry().setFromPoints(points);
return new THREE.Line(geo, material);
}
function drawStreets(positions, isAvenue, majorInterval) {
for (var pi = 0; pi < positions.length; pi++) {
var pos = positions[pi];
var isMajor = pi % majorInterval === 0;
var sw = isMajor ? majorStreetWidth : minorStreetWidth;
var length = cityExtent * 2;
var w, h, x, z;
if (isAvenue) { w = sw; h = length; x = pos; z = 0; }
else { w = length; h = sw; x = 0; z = pos; }
cityGroup.add(createPlane(w, h, streetMat, -Math.PI/2, [x, 0.02, z]));
for (var side = -1; side <= 1; side += 2) {
var offset = side * (sw/2 + sidewalkWidth/2);
var swW, swH, swX, swZ;
if (isAvenue) { swW = sidewalkWidth; swH = length; swX = x + offset; swZ = 0; }
else { swW = length; swH = sidewalkWidth; swX = 0; swZ = z + offset; }
cityGroup.add(createPlane(swW, swH, sidewalkMat, -Math.PI/2, [swX, 0.04, swZ]));
}
if (isMajor) {
for (var d = -cityExtent; d < cityExtent; d += 3) {
var pts;
if (isAvenue) pts = [new THREE.Vector3(pos, 0.06, d), new THREE.Vector3(pos, 0.06, d + 1.5)];
else pts = [new THREE.Vector3(d, 0.06, pos), new THREE.Vector3(d + 1.5, 0.06, pos)];
cityGroup.add(createLine(pts, roadLineMat));
}
if (isAvenue) {
for (var oi = 0; oi < 2; oi++) {
var off = oi === 0 ? -0.15 : 0.15;
cityGroup.add(createLine([
new THREE.Vector3(pos + off, 0.06, -cityExtent),
new THREE.Vector3(pos + off, 0.06, cityExtent)
], createLineMat(0x777755, 0.2)));
}
}
}
for (var side2 = -1; side2 <= 1; side2 += 2) {
var curbPos = pos + side2 * (sw/2);
var cpts;
if (isAvenue) cpts = [new THREE.Vector3(curbPos, 0.05, -cityExtent), new THREE.Vector3(curbPos, 0.05, cityExtent)];
else cpts = [new THREE.Vector3(-cityExtent, 0.05, curbPos), new THREE.Vector3(cityExtent, 0.05, curbPos)];
cityGroup.add(createLine(cpts, curbMat));
}
}
}
drawStreets(avenuePositions, true, 3);
drawStreets(streetPositions, false, 4);
for (var cci = 0; cci < avenuePositions.length; cci++) {
for (var ccj = 0; ccj < streetPositions.length; ccj++) {
if (cci % 3 === 0 || ccj % 4 === 0) {
var csw = (cci % 3 === 0) ? majorStreetWidth : minorStreetWidth;
var csh = (ccj % 4 === 0) ? majorStreetWidth : minorStreetWidth;
var crosswalkMat = createLineMat(0x777777, 0.25);
for (var cs = -csw/2 + 0.3; cs < csw/2; cs += 0.6) {
cityGroup.add(createLine([
new THREE.Vector3(avenuePositions[cci] + cs, 0.07, streetPositions[ccj] - csh/2),
new THREE.Vector3(avenuePositions[cci] + cs, 0.07, streetPositions[ccj] + csh/2)
], crosswalkMat));
}
for (var cs2 = -csh/2 + 0.3; cs2 < csh/2; cs2 += 0.6) {
cityGroup.add(createLine([
new THREE.Vector3(avenuePositions[cci] - csw/2, 0.07, streetPositions[ccj] + cs2),
new THREE.Vector3(avenuePositions[cci] + csw/2, 0.07, streetPositions[ccj] + cs2)
], crosswalkMat));
}
}
}
}
// ==========================================
// BEACH AND SEA - Lowpoly
// ==========================================
var oceanGroup = new THREE.Group();
var waveVertices = [];
var beachCenterX = 85;
var beachStartZ = -80;
var beachEndZ = 80;
var beachWidth = 18;
function createBeach() {
var beachVerts = [];
var beachIdx = [];
var segZ = 40;
var segX = 6;
for (var iz = 0; iz <= segZ; iz++) {
var zt = iz / segZ;
var z = beachStartZ + (beachEndZ - beachStartZ) * zt;
for (var ix = 0; ix <= segX; ix++) {
var xt = ix / segX;
var curveOffset = Math.sin(zt * Math.PI) * 8 + Math.sin(zt * Math.PI * 3) * 2;
var x = cityExtent + xt * beachWidth + curveOffset;
var y = 0.3 - xt * 0.4 + rand(-0.15, 0.15);
y += Math.sin(zt * 12) * 0.08 + Math.cos(xt * 8) * 0.05;
if (y < -0.3) y = -0.3;
beachVerts.push(x, y, z);
}
}
for (var iz2 = 0; iz2 < segZ; iz2++) {
for (var ix2 = 0; ix2 < segX; ix2++) {
var a = iz2 * (segX + 1) + ix2;
var b = a + 1;
var c = a + (segX + 1);
var d = c + 1;
beachIdx.push(a, c, b);
beachIdx.push(b, c, d);
}
}
var beachGeo = new THREE.BufferGeometry();
beachGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(beachVerts), 3));
beachGeo.setIndex(beachIdx);
beachGeo.computeVertexNormals();
var beachFill = new THREE.Mesh(beachGeo, createMat(0xc2b280, 0.15));
oceanGroup.add(beachFill);
var beachWire = new THREE.Mesh(beachGeo, createMat(0xa09060, 0.1, true));
oceanGroup.add(beachWire);
var beachEdges = new THREE.LineSegments(new THREE.EdgesGeometry(beachGeo, 15), createLineMat(0x8a7a50, 0.25));
oceanGroup.add(beachEdges);
for (var smi = 0; smi < 25; smi++) {
var smz = rand(beachStartZ + 5, beachEndZ - 5);
var smzt = (smz - beachStartZ) / (beachEndZ - beachStartZ);
var smCurve = Math.sin(smzt * Math.PI) * 8 + Math.sin(smzt * Math.PI * 3) * 2;
var smx = cityExtent + rand(1, beachWidth * 0.5) + smCurve;
var moundGeo = new THREE.DodecahedronGeometry(rand(0.3, 0.8), 0);
var mound = new THREE.Mesh(moundGeo, createMat(0xb8a870, 0.1));
mound.position.set(smx, rand(-0.1, 0.15), smz);
mound.scale.y = rand(0.2, 0.5);
mound.rotation.y = rand(0, Math.PI);
oceanGroup.add(mound);
var moundEdge = new THREE.LineSegments(new THREE.EdgesGeometry(moundGeo), createLineMat(0x9a8a60, 0.15));
moundEdge.position.copy(mound.position);
moundEdge.scale.copy(mound.scale);
moundEdge.rotation.copy(mound.rotation);
oceanGroup.add(moundEdge);
}
for (var fli = 0; fli < 3; fli++) {
var foamPts = [];
var foamOffset = fli * 1.5 + beachWidth * 0.4;
for (var fpi = 0; fpi <= 60; fpi++) {
var fpt = fpi / 60;
var fpz = beachStartZ + (beachEndZ - beachStartZ) * fpt;
var fpCurve = Math.sin(fpt * Math.PI) * 8 + Math.sin(fpt * Math.PI * 3) * 2;
var fpx = cityExtent + foamOffset + fpCurve + Math.sin(fpt * 20 + fli * 2) * 0.5;
foamPts.push(new THREE.Vector3(fpx, -0.05 + fli * 0.02, fpz));
}
oceanGroup.add(createLine(foamPts, createLineMat(0xaabbcc, 0.15 - fli * 0.03)));
}
}
createBeach();
function createOcean() {
var oceanSegX = 30;
var oceanSegZ = 50;
var oceanWidth = 120;
var oceanStartX = cityExtent + beachWidth - 5;
for (var layer = 0; layer < 3; layer++) {
var oceanVerts = [];
var oceanIdx = [];
var layerY = -0.5 - layer * 0.15;
for (var oz = 0; oz <= oceanSegZ; oz++) {
var ozt = oz / oceanSegZ;
var ozPos = beachStartZ - 20 + (beachEndZ - beachStartZ + 40) * ozt;
for (var ox = 0; ox <= oceanSegX; ox++) {
var oxt = ox / oceanSegX;
var beachCurveAtZ = Math.sin(ozt * Math.PI) * 8 + Math.sin(ozt * Math.PI * 3) * 2;
var oxPos = oceanStartX + beachCurveAtZ + oxt * oceanWidth;
var waveY = layerY;
waveY += Math.sin(oxt * 4 + ozt * 3) * 0.3;
waveY += Math.cos(ozt * 5 + oxt * 2) * 0.2;
waveY += rand(-0.1, 0.1);
oceanVerts.push(oxPos, waveY, ozPos);
}
}
for (var oz2 = 0; oz2 < oceanSegZ; oz2++) {
for (var ox2 = 0; ox2 < oceanSegX; ox2++) {
var oa = oz2 * (oceanSegX + 1) + ox2;
var ob = oa + 1;
var oc = oa + (oceanSegX + 1);
var od = oc + 1;
oceanIdx.push(oa, oc, ob);
oceanIdx.push(ob, oc, od);
}
}
var oceanGeo = new THREE.BufferGeometry();
oceanGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(oceanVerts), 3));
oceanGeo.setIndex(oceanIdx);
oceanGeo.computeVertexNormals();
var oceanColors = [0x5588aa, 0x447799, 0x336688];
var oceanOpacities = [0.12, 0.08, 0.05];
var oceanFill = new THREE.Mesh(oceanGeo, createMat(oceanColors[layer], oceanOpacities[layer]));
oceanGroup.add(oceanFill);
if (layer === 0) {
var oceanWire = new THREE.Mesh(oceanGeo, createMat(0x4488aa, 0.06, true));
oceanGroup.add(oceanWire);
}
var edgeOpacity = [0.2, 0.12, 0.06];
var oceanEdges = new THREE.LineSegments(
new THREE.EdgesGeometry(oceanGeo, 12),
createLineMat(0x4477aa, edgeOpacity[layer])
);
oceanGroup.add(oceanEdges);
waveVertices.push({
geo: oceanGeo,
baseVerts: new Float32Array(oceanVerts),
segX: oceanSegX,
segZ: oceanSegZ,
layer: layer
});
}
var deepGeo = new THREE.PlaneGeometry(200, 200, 8, 8);
var deep = new THREE.Mesh(deepGeo, createMat(0x336688, 0.06));
deep.rotation.x = -Math.PI / 2;
deep.position.set(cityExtent + beachWidth - 5 + 80, -1.2, 0);
oceanGroup.add(deep);
var deepEdge = new THREE.LineSegments(new THREE.EdgesGeometry(deepGeo), createLineMat(0x335577, 0.08));
deepEdge.rotation.x = -Math.PI / 2;
deepEdge.position.copy(deep.position);
oceanGroup.add(deepEdge);
for (var hli = 0; hli < 5; hli++) {
var hx = cityExtent + beachWidth - 5 + 80 + rand(20, 60);
var hz1 = rand(-100, -30);
var hz2 = rand(30, 100);
oceanGroup.add(createLine([
new THREE.Vector3(hx, -0.8 + hli * 0.1, hz1),
new THREE.Vector3(hx + rand(-10, 10), -0.7 + hli * 0.1, (hz1 + hz2) / 2),
new THREE.Vector3(hx + rand(-5, 15), -0.8 + hli * 0.1, hz2)
], createLineMat(0x6699bb, 0.06)));
}
}
createOcean();
var waveLines = [];
for (var wli = 0; wli < 8; wli++) {
var wlPts = [];
var wlBaseOffset = beachWidth * 0.35 + wli * 2.5;
for (var wlp = 0; wlp <= 40; wlp++) {
var wlpt = wlp / 40;
var wlz = beachStartZ + (beachEndZ - beachStartZ) * wlpt;
var wlCurve = Math.sin(wlpt * Math.PI) * 8 + Math.sin(wlpt * Math.PI * 3) * 2;
var wlx = cityExtent + wlBaseOffset + wlCurve;
wlPts.push(new THREE.Vector3(wlx, -0.1, wlz));
}
var wlGeo = new THREE.BufferGeometry().setFromPoints(wlPts);
var wl = new THREE.Line(wlGeo, createLineMat(0x88aacc, 0.15 - wli * 0.015));
wl.userData = {
baseOffset: wlBaseOffset,
speed: 0.4 + wli * 0.08,
phase: wli * 0.8
};
oceanGroup.add(wl);
waveLines.push(wl);
}
for (var bri = 0; bri < 20; bri++) {
var brz = rand(beachStartZ + 3, beachEndZ - 3);
var brzt = (brz - beachStartZ) / (beachEndZ - beachStartZ);
var brCurve = Math.sin(brzt * Math.PI) * 8 + Math.sin(brzt * Math.PI * 3) * 2;
var brx = cityExtent + rand(beachWidth * 0.2, beachWidth * 0.7) + brCurve;
var rockSize = rand(0.4, 1.8);
var brGeo = new THREE.DodecahedronGeometry(rockSize, 0);
var br = new THREE.Mesh(brGeo, createMat(0x777777, 0.1));
br.position.set(brx, rand(-0.2, 0.3), brz);
br.rotation.set(rand(0, 1), rand(0, 1), rand(0, 1));
br.scale.y = rand(0.3, 0.8);
oceanGroup.add(br);
var brEdge = new THREE.LineSegments(new THREE.EdgesGeometry(brGeo), createLineMat(0x555555, 0.2));
brEdge.position.copy(br.position);
brEdge.rotation.copy(br.rotation);
brEdge.scale.copy(br.scale);
oceanGroup.add(brEdge);
}
for (var pti = 0; pti < 15; pti++) {
var ptz = rand(beachStartZ + 8, beachEndZ - 8);
var ptzt = (ptz - beachStartZ) / (beachEndZ - beachStartZ);
var ptCurve = Math.sin(ptzt * Math.PI) * 8 + Math.sin(ptzt * Math.PI * 3) * 2;
var ptx = cityExtent + rand(0.5, 4) + ptCurve;
var ptHeight = rand(3, 6);
var ptLean = rand(-0.3, 0.3);
var trunkPts = [];
for (var tsi = 0; tsi <= 8; tsi++) {
var tst = tsi / 8;
trunkPts.push(new THREE.Vector3(
ptx + Math.sin(tst * 1.5) * ptLean * ptHeight,
tst * ptHeight,
ptz + Math.cos(tst * 2) * ptLean * 0.5
));
}
oceanGroup.add(createLine(trunkPts, createLineMat(0x887755, 0.3)));
var topX = ptx + Math.sin(1.5) * ptLean * ptHeight;
var topZ = ptz + Math.cos(2) * ptLean * 0.5;
for (var fri = 0; fri < 6; fri++) {
var frAngle = (fri / 6) * Math.PI * 2 + rand(-0.2, 0.2);
var frLen = rand(2, 3.5);
var frPts = [];
for (var frj = 0; frj <= 5; frj++) {
var frt = frj / 5;
frPts.push(new THREE.Vector3(
topX + Math.cos(frAngle) * frt * frLen,
ptHeight - frt * frt * 1.5,
topZ + Math.sin(frAngle) * frt * frLen
));
}
oceanGroup.add(createLine(frPts, createLineMat(0x668855, 0.2)));
}
var trunkGeo = new THREE.CylinderGeometry(0.08, 0.15, ptHeight, 5);
var trunk = new THREE.Mesh(trunkGeo, createMat(0x887755, 0.15));
trunk.position.set(ptx + ptLean * ptHeight * 0.3, ptHeight / 2, ptz);
trunk.rotation.z = ptLean * 0.3;
oceanGroup.add(trunk);
}
for (var bti = 0; bti < 6; bti++) {
var btGroup = new THREE.Group();
var btz = rand(beachStartZ + 20, beachEndZ - 20);
var btzt = (btz - beachStartZ) / (beachEndZ - beachStartZ);
var btCurve = Math.sin(btzt * Math.PI) * 8 + Math.sin(btzt * Math.PI * 3) * 2;
var btx = cityExtent + beachWidth + btCurve + rand(10, 50);
var hullVerts = [
-0.8, 0, -0.3, 0.8, 0, -0.3, 0.8, 0, 0.3, -0.8, 0, 0.3,
-1.0, 0.3, -0.4, 1.0, 0.3, -0.4, 1.0, 0.3, 0.4, -1.0, 0.3, 0.4,
1.4, 0.2, 0,
-1.0, 0.2, 0
];
var hullIdx = [
0,1,5, 0,5,4, 1,2,6, 1,6,5, 2,3,7, 2,7,6, 3,0,4, 3,4,7,
4,5,8, 5,6,8, 6,7,9, 7,4,9
];
var hullGeo = new THREE.BufferGeometry();
hullGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(hullVerts), 3));
hullGeo.setIndex(hullIdx);
hullGeo.computeVertexNormals();
var hull = new THREE.Mesh(hullGeo, createMat(0x666666, 0.2));
btGroup.add(hull);
var hullEdge = new THREE.LineSegments(new THREE.EdgesGeometry(hullGeo), createLineMat(0x444444, 0.35));
btGroup.add(hullEdge);
if (Math.random() > 0.4) {
var mastH = rand(1.5, 2.5);
var mast = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, mastH, 4), createMat(0x555555, 0.3));
mast.position.y = 0.3 + mastH / 2;
btGroup.add(mast);
var sailVerts = [0, 0.3 + mastH, 0, 0, 0.5, 0, 0, 0.8, 1.0];
var sailIdx = [0, 1, 2];
var sailGeo = new THREE.BufferGeometry();
sailGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(sailVerts), 3));
sailGeo.setIndex(sailIdx);
var sail = new THREE.Mesh(sailGeo, createMat(0xaaaaaa, 0.1, false, THREE.DoubleSide));
btGroup.add(sail);
var sailEdge = new THREE.LineSegments(new THREE.EdgesGeometry(sailGeo), createLineMat(0x777777, 0.2));
btGroup.add(sailEdge);
}
var boatScale = rand(0.8, 1.8);
btGroup.scale.setScalar(boatScale);
btGroup.position.set(btx, -0.3, btz);
btGroup.rotation.y = rand(0, Math.PI * 2);
btGroup.userData = {
baseY: -0.3,
bobSpeed: rand(0.5, 1.5),
bobAmp: rand(0.1, 0.25),
bobPhase: rand(0, 6.28),
rockSpeed: rand(0.3, 0.8),
rockAmp: rand(0.02, 0.06)
};
oceanGroup.add(btGroup);
waveLines.push(btGroup);
}
for (var wsri = 0; wsri < 4; wsri++) {
var wsrz = rand(beachStartZ + 10, beachEndZ - 10);
var wsrzt = (wsrz - beachStartZ) / (beachEndZ - beachStartZ);
var wsrCurve = Math.sin(wsrzt * Math.PI) * 8 + Math.sin(wsrzt * Math.PI * 3) * 2;
var wsrx = cityExtent + beachWidth + wsrCurve + rand(15, 40);
var wsrR = rand(2, 6);
var wsring = new THREE.Mesh(
new THREE.RingGeometry(wsrR, wsrR + 0.1, 8),
createMat(0x5588aa, 0.06, false, THREE.DoubleSide)
);
wsring.rotation.x = -Math.PI / 2;
wsring.position.set(wsrx, -0.2, wsrz);
oceanGroup.add(wsring);
}
oceanGroup.add(createLine([
new THREE.Vector3(cityExtent + beachWidth + 20, 0, 0),
new THREE.Vector3(cityExtent + beachWidth + 25, 5, 0),
new THREE.Vector3(cityExtent + beachWidth + 35, 5, 0)
], createLineMat(0x5588aa, 0.2)));
var seaLabelDot = new THREE.Mesh(new THREE.SphereGeometry(0.15, 4, 4), createMat(0x5588aa, 0.4));
seaLabelDot.position.set(cityExtent + beachWidth + 20, 0, 0);
oceanGroup.add(seaLabelDot);
scene.add(oceanGroup);
var southBeachGroup = new THREE.Group();
function createSouthBeach() {
var sbVerts = [];
var sbIdx = [];
var sbSegX = 40;
var sbSegZ = 6;
for (var sz = 0; sz <= sbSegZ; sz++) {
var szt = sz / sbSegZ;
for (var sx = 0; sx <= sbSegX; sx++) {
var sxt = sx / sbSegX;
var sxPos = -cityExtent + (cityExtent * 2) * sxt;
var curveOff = Math.sin(sxt * Math.PI) * 6 + Math.sin(sxt * Math.PI * 4) * 1.5;
var szPos = -cityExtent - szt * beachWidth - curveOff;
var sy = 0.3 - szt * 0.4 + rand(-0.12, 0.12);
sy += Math.sin(sxt * 15) * 0.06;
if (sy < -0.3) sy = -0.3;
sbVerts.push(sxPos, sy, szPos);
}
}
for (var sz2 = 0; sz2 < sbSegZ; sz2++) {
for (var sx2 = 0; sx2 < sbSegX; sx2++) {
var sa = sz2 * (sbSegX + 1) + sx2;
var sb = sa + 1;
var sc = sa + (sbSegX + 1);
var sd = sc + 1;
sbIdx.push(sa, sc, sb);
sbIdx.push(sb, sc, sd);
}
}
var sbGeo = new THREE.BufferGeometry();
sbGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(sbVerts), 3));
sbGeo.setIndex(sbIdx);
sbGeo.computeVertexNormals();
southBeachGroup.add(new THREE.Mesh(sbGeo, createMat(0xc2b280, 0.12)));
southBeachGroup.add(new THREE.Mesh(sbGeo, createMat(0xa09060, 0.08, true)));
southBeachGroup.add(new THREE.LineSegments(new THREE.EdgesGeometry(sbGeo, 15), createLineMat(0x8a7a50, 0.2)));
var soVerts = [];
var soIdx = [];
var soSegX = 25;
var soSegZ = 15;
for (var soz = 0; soz <= soSegZ; soz++) {
var sozt = soz / soSegZ;
for (var sox = 0; sox <= soSegX; sox++) {
var soxt = sox / soSegX;
var soxPos = -cityExtent - 10 + (cityExtent * 2 + 20) * soxt;
var soCurve = Math.sin(soxt * Math.PI) * 6 + Math.sin(soxt * Math.PI * 4) * 1.5;
var sozPos = -cityExtent - beachWidth - soCurve - sozt * 60;
var soy = -0.5 + Math.sin(soxt * 3 + sozt * 4) * 0.25 + rand(-0.1, 0.1);
soVerts.push(soxPos, soy, sozPos);
}
}
for (var soz2 = 0; soz2 < soSegZ; soz2++) {
for (var sox2 = 0; sox2 < soSegX; sox2++) {
var soa = soz2 * (soSegX + 1) + sox2;
var sob = soa + 1;
var soc = soa + (soSegX + 1);
var sod = soc + 1;
soIdx.push(soa, soc, sob);
soIdx.push(sob, soc, sod);
}
}
var soGeo = new THREE.BufferGeometry();
soGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(soVerts), 3));
soGeo.setIndex(soIdx);
soGeo.computeVertexNormals();
southBeachGroup.add(new THREE.Mesh(soGeo, createMat(0x5588aa, 0.1)));
southBeachGroup.add(new THREE.Mesh(soGeo, createMat(0x4488aa, 0.05, true)));
southBeachGroup.add(new THREE.LineSegments(new THREE.EdgesGeometry(soGeo, 12), createLineMat(0x4477aa, 0.15)));
waveVertices.push({
geo: soGeo,
baseVerts: new Float32Array(soVerts),
segX: soSegX,
segZ: soSegZ,
layer: 0
});
for (var sfli = 0; sfli < 4; sfli++) {
var sflPts = [];
var sflOff = beachWidth * 0.4 + sfli * 2;
for (var sflp = 0; sflp <= 50; sflp++) {
var sflpt = sflp / 50;
var sflx = -cityExtent + (cityExtent * 2) * sflpt;
var sflCurve = Math.sin(sflpt * Math.PI) * 6 + Math.sin(sflpt * Math.PI * 4) * 1.5;
var sflz = -cityExtent - sflOff - sflCurve + Math.sin(sflpt * 18 + sfli) * 0.4;
sflPts.push(new THREE.Vector3(sflx, -0.08, sflz));
}
southBeachGroup.add(createLine(sflPts, createLineMat(0x88aacc, 0.12 - sfli * 0.02)));
}
for (var spti = 0; spti < 12; spti++) {
var sptx = rand(-cityExtent + 5, cityExtent - 5);
var sptxt = (sptx + cityExtent) / (cityExtent * 2);
var sptCurve = Math.sin(sptxt * Math.PI) * 6 + Math.sin(sptxt * Math.PI * 4) * 1.5;
var sptz = -cityExtent - rand(0.5, 3) - sptCurve;
var sptH = rand(2.5, 5);
var sptLean = rand(-0.3, 0.3);
var stPts = [];
for (var stsi = 0; stsi <= 6; stsi++) {
var stst = stsi / 6;
stPts.push(new THREE.Vector3(
sptx + Math.sin(stst * 1.5) * sptLean * sptH * 0.3,
stst * sptH,
sptz + Math.cos(stst * 1.2) * sptLean * 0.4
));
}
southBeachGroup.add(createLine(stPts, createLineMat(0x887755, 0.25)));
for (var sfri = 0; sfri < 5; sfri++) {
var sfrAngle = (sfri / 5) * Math.PI * 2 + rand(-0.3, 0.3);
var sfrLen = rand(1.5, 3);
var sfrPts = [];
for (var sfrj = 0; sfrj <= 4; sfrj++) {
var sfrt = sfrj / 4;
sfrPts.push(new THREE.Vector3(
sptx + Math.cos(sfrAngle) * sfrt * sfrLen,
sptH - sfrt * sfrt * 1.2,
sptz + Math.sin(sfrAngle) * sfrt * sfrLen
));
}
southBeachGroup.add(createLine(sfrPts, createLineMat(0x668855, 0.18)));
}
var stGeo = new THREE.CylinderGeometry(0.06, 0.12, sptH, 5);
var stTrunk = new THREE.Mesh(stGeo, createMat(0x887755, 0.12));
stTrunk.position.set(sptx, sptH / 2, sptz);
southBeachGroup.add(stTrunk);
}
for (var sbri = 0; sbri < 15; sbri++) {
var sbrx = rand(-cityExtent + 3, cityExtent - 3);
var sbrxt = (sbrx + cityExtent) / (cityExtent * 2);
var sbrCurve = Math.sin(sbrxt * Math.PI) * 6 + Math.sin(sbrxt * Math.PI * 4) * 1.5;
var sbrz = -cityExtent - rand(beachWidth * 0.2, beachWidth * 0.6) - sbrCurve;
var sbrSize = rand(0.3, 1.5);
var sbrGeo = new THREE.DodecahedronGeometry(sbrSize, 0);
var sbr = new THREE.Mesh(sbrGeo, createMat(0x777777, 0.08));
sbr.position.set(sbrx, rand(-0.2, 0.2), sbrz);
sbr.rotation.set(rand(0, 1), rand(0, 1), rand(0, 1));
sbr.scale.y = rand(0.3, 0.7);
southBeachGroup.add(sbr);
var sbrEdge = new THREE.LineSegments(new THREE.EdgesGeometry(sbrGeo), createLineMat(0x555555, 0.18));
sbrEdge.position.copy(sbr.position);
sbrEdge.rotation.copy(sbr.rotation);
sbrEdge.scale.copy(sbr.scale);
southBeachGroup.add(sbrEdge);
}
}
createSouthBeach();
scene.add(southBeachGroup);
// ==========================================
// SUGAR LOAF - Reduced peak height
// ==========================================
function createSugarLoaf() {
var sugarLoafGroup = new THREE.Group();
var pdaX = 62, pdaZ = -62;
var urcaX = pdaX - 16, urcaZ = pdaZ + 12;
var stationX = urcaX - 8, stationZ = urcaZ + 6;
var urcaPeakY = 26.4;
var pdaPeakY = 39; // reduced from 47.5
var waterGeo = new THREE.CircleGeometry(35, 10);
var water = new THREE.Mesh(waterGeo, createMat(0x6688aa, 0.08));
water.rotation.x = -Math.PI / 2;
water.position.set((pdaX + urcaX) / 2, 0.01, (pdaZ + urcaZ) / 2);
sugarLoafGroup.add(water);
var waterEdge = new THREE.LineSegments(new THREE.EdgesGeometry(waterGeo), createLineMat(0x557799, 0.15));
waterEdge.rotation.x = -Math.PI / 2;
waterEdge.position.copy(water.position);
sugarLoafGroup.add(waterEdge);
var slVertices = [];
var slIndices = [];
// Reduced height rings for Pão de Açúcar (scaled from 47.5 to 39)
var rings = [
{ y: 0, r: 6.5 },
{ y: 1.6, r: 6.3 },
{ y: 4.1, r: 6.0 },
{ y: 7.4, r: 5.5 },
{ y: 10.7, r: 5.0 },
{ y: 13.9, r: 4.5 },
{ y: 17.2, r: 4.1 },
{ y: 20.5, r: 3.7 },
{ y: 23.8, r: 3.4 },
{ y: 27.1, r: 3.1 },
{ y: 29.5, r: 2.8 },
{ y: 32.0, r: 2.5 },
{ y: 34.0, r: 2.2 },
{ y: 35.7, r: 1.9 },
{ y: 36.9, r: 1.5 },
{ y: 37.9, r: 1.1 },
{ y: 38.6, r: 0.5 },
{ y: 39.0, r: 0.0 }
];
var segments = 10;
var slOffsets = [];
for (var osi = 0; osi < segments; osi++) {
slOffsets.push(rand(-1, 1));
}
for (var ri = 0; ri < rings.length; ri++) {
var ring = rings[ri];
if (ring.r <= 0.01) {
slVertices.push(0, ring.y, 0);
} else {
for (var sj = 0; sj < segments; sj++) {
var angle = (sj / segments) * Math.PI * 2;
var rx = ring.r * (1.0 + 0.15 * Math.cos(angle * 2));
var rz = ring.r * (1.0 - 0.08 * Math.cos(angle * 2));
var irregFactor = Math.max(0, 1.0 - ring.y / 42) * 0.25;
var irregOffset = slOffsets[sj] * irregFactor;
var vx = Math.cos(angle) * (rx + irregOffset);
var vz = Math.sin(angle) * (rz + irregOffset * 0.7);
slVertices.push(vx, ring.y, vz);
}
}
}
var normalRingCount = rings.length - 1;
var apexIdx = normalRingCount * segments;
for (var ri2 = 0; ri2 < normalRingCount - 1; ri2++) {
for (var sk = 0; sk < segments; sk++) {
var s1 = sk;
var s2 = (sk + 1) % segments;
var i0 = ri2 * segments + s1;
var i1 = ri2 * segments + s2;
var i2 = (ri2 + 1) * segments + s2;
var i3 = (ri2 + 1) * segments + s1;
slIndices.push(i0, i1, i2);
slIndices.push(i0, i2, i3);
}
}
var lastFullRing = (normalRingCount - 1) * segments;
for (var sa = 0; sa < segments; sa++) {
var sa2 = (sa + 1) % segments;
slIndices.push(lastFullRing + sa, lastFullRing + sa2, apexIdx);
}
for (var sb2 = 0; sb2 < segments - 2; sb2++) {
slIndices.push(0, sb2 + 1, sb2 + 2);
}
var slGeo = new THREE.BufferGeometry();
slGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(slVertices), 3));
slGeo.setIndex(slIndices);
slGeo.computeVertexNormals();
var slFill = new THREE.Mesh(slGeo, createMat(0x888888, 0.1));
slFill.position.set(pdaX, 0, pdaZ);
sugarLoafGroup.add(slFill);
var slWire = new THREE.Mesh(slGeo, createMat(0x666666, 0.15, true));
slWire.position.copy(slFill.position);
sugarLoafGroup.add(slWire);
var slEdges = new THREE.LineSegments(new THREE.EdgesGeometry(slGeo, 20), createLineMat(0x222222, 0.5));
slEdges.position.copy(slFill.position);
sugarLoafGroup.add(slEdges);
var urcaRings = [
{ y: 0, r: 5.5 },
{ y: 2, r: 5.3 },
{ y: 5, r: 5.0 },
{ y: 9, r: 4.5 },
{ y: 13, r: 4.0 },
{ y: 17, r: 3.5 },
{ y: 20, r: 3.0 },
{ y: 23, r: 2.6 },
{ y: 25, r: 2.3 },
{ y: 26, r: 2.0 },
{ y: 26.4, r: 1.6 }
];
var urcaSegments = 8;
var urcaVerts = [];
var urcaIdx = [];
var urcaOffsets = [];
for (var uoi = 0; uoi < urcaSegments; uoi++) {
urcaOffsets.push(rand(-1, 1));
}
for (var uri = 0; uri < urcaRings.length; uri++) {
var uring = urcaRings[uri];
for (var usj = 0; usj < urcaSegments; usj++) {
var uAngle = (usj / urcaSegments) * Math.PI * 2;
var urx = uring.r * (1.0 + 0.12 * Math.cos(uAngle * 2 + 0.5));
var urz = uring.r * (1.0 - 0.1 * Math.sin(uAngle * 2));
var uIrreg = Math.max(0, 1.0 - uring.y / 30) * 0.35;
var uOff = urcaOffsets[usj] * uIrreg;
urcaVerts.push(Math.cos(uAngle) * (urx + uOff), uring.y, Math.sin(uAngle) * (urz + uOff * 0.6));
}
}
var urcaTopIdx = urcaRings.length * urcaSegments;
urcaVerts.push(0, 26.4, 0);
for (var uri2 = 0; uri2 < urcaRings.length - 1; uri2++) {
for (var usk = 0; usk < urcaSegments; usk++) {
var us1 = usk, us2 = (usk + 1) % urcaSegments;
var ui0 = uri2 * urcaSegments + us1;
var ui1 = uri2 * urcaSegments + us2;
var ui2 = (uri2 + 1) * urcaSegments + us2;
var ui3 = (uri2 + 1) * urcaSegments + us1;
urcaIdx.push(ui0, ui1, ui2);
urcaIdx.push(ui0, ui2, ui3);
}
}
var uLastRing = (urcaRings.length - 1) * urcaSegments;
for (var utc = 0; utc < urcaSegments; utc++) {
var utc2 = (utc + 1) % urcaSegments;
urcaIdx.push(uLastRing + utc, uLastRing + utc2, urcaTopIdx);
}
for (var ubc = 0; ubc < urcaSegments - 2; ubc++) {
urcaIdx.push(0, ubc + 1, ubc + 2);
}
var urcaGeo = new THREE.BufferGeometry();
urcaGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(urcaVerts), 3));
urcaGeo.setIndex(urcaIdx);
urcaGeo.computeVertexNormals();
var urcaFill = new THREE.Mesh(urcaGeo, createMat(0x777777, 0.12));
urcaFill.position.set(urcaX, 0, urcaZ);
sugarLoafGroup.add(urcaFill);
var urcaWireM = new THREE.Mesh(urcaGeo, createMat(0x666666, 0.15, true));
urcaWireM.position.copy(urcaFill.position);
sugarLoafGroup.add(urcaWireM);
var urcaEdgesM = new THREE.LineSegments(new THREE.EdgesGeometry(urcaGeo, 20), createLineMat(0x333333, 0.45));
urcaEdgesM.position.copy(urcaFill.position);
sugarLoafGroup.add(urcaEdgesM);
var stationY = 1.5;
var stationGeo = new THREE.BoxGeometry(3, 3, 2.5);
var station = new THREE.Mesh(stationGeo, createMat(0x777777, 0.15));
station.position.set(stationX, stationY, stationZ);
sugarLoafGroup.add(station);
var stationEdge = new THREE.LineSegments(new THREE.EdgesGeometry(stationGeo), createLineMat(0x333333, 0.4));
stationEdge.position.copy(station.position);
sugarLoafGroup.add(stationEdge);
var stationTowerH = 6;
var stationTowerGeo = new THREE.BoxGeometry(0.6, stationTowerH, 0.6);
var stationTower = new THREE.Mesh(stationTowerGeo, createMat(0x666666, 0.2));
stationTower.position.set(stationX, stationTowerH / 2, stationZ);
sugarLoafGroup.add(stationTower);
var stationTowerEdge = new THREE.LineSegments(new THREE.EdgesGeometry(stationTowerGeo), createLineMat(0x333333, 0.4));
stationTowerEdge.position.copy(stationTower.position);
sugarLoafGroup.add(stationTowerEdge);
var urcaStationGeo = new THREE.BoxGeometry(2.5, 2, 2);
var urcaStation = new THREE.Mesh(urcaStationGeo, createMat(0x777777, 0.15));
urcaStation.position.set(urcaX, urcaPeakY + 1, urcaZ);
sugarLoafGroup.add(urcaStation);
var urcaStationEdge = new THREE.LineSegments(new THREE.EdgesGeometry(urcaStationGeo), createLineMat(0x333333, 0.4));
urcaStationEdge.position.copy(urcaStation.position);
sugarLoafGroup.add(urcaStationEdge);
var pdaStationGeo = new THREE.BoxGeometry(2, 1.5, 1.8);
var pdaStationY = pdaPeakY - 3;
var pdaStation = new THREE.Mesh(pdaStationGeo, createMat(0x777777, 0.15));
pdaStation.position.set(pdaX, pdaStationY, pdaZ);
sugarLoafGroup.add(pdaStation);
var pdaStationEdge = new THREE.LineSegments(new THREE.EdgesGeometry(pdaStationGeo), createLineMat(0x333333, 0.4));
pdaStationEdge.position.copy(pdaStation.position);
sugarLoafGroup.add(pdaStationEdge);
var cable1StartY = stationTowerH;
var cable1EndY = urcaPeakY + 2;
var cable2StartY = urcaPeakY + 2;
var cable2EndY = pdaStationY + 0.75;
var cable1Points = [];
var c1Start = new THREE.Vector3(stationX, cable1StartY, stationZ);
var c1End = new THREE.Vector3(urcaX, cable1EndY, urcaZ);
for (var c1i = 0; c1i <= 30; c1i++) {
var c1t = c1i / 30;
var c1x = c1Start.x + (c1End.x - c1Start.x) * c1t;
var c1z = c1Start.z + (c1End.z - c1Start.z) * c1t;
var c1y = c1Start.y + (c1End.y - c1Start.y) * c1t;
var c1sag = -Math.sin(c1t * Math.PI) * 3.0;
cable1Points.push(new THREE.Vector3(c1x, c1y + c1sag, c1z));
}
sugarLoafGroup.add(createLine(cable1Points, createLineMat(0x444444, 0.35)));
var cable1bPoints = [];
for (var c1bi = 0; c1bi <= 30; c1bi++) {
var c1bt = c1bi / 30;
var c1bx = c1Start.x + (c1End.x - c1Start.x) * c1bt + 0.3;
var c1bz = c1Start.z + (c1End.z - c1Start.z) * c1bt + 0.3;
var c1by = c1Start.y + (c1End.y - c1Start.y) * c1bt;
var c1bsag = -Math.sin(c1bt * Math.PI) * 3.3;
cable1bPoints.push(new THREE.Vector3(c1bx, c1by + c1bsag, c1bz));
}
sugarLoafGroup.add(createLine(cable1bPoints, createLineMat(0x444444, 0.25)));
var cable2Points = [];
var c2Start = new THREE.Vector3(urcaX, cable2StartY, urcaZ);
var c2End = new THREE.Vector3(pdaX, cable2EndY, pdaZ);
for (var c2i = 0; c2i <= 35; c2i++) {
var c2t = c2i / 35;
var c2x = c2Start.x + (c2End.x - c2Start.x) * c2t;
var c2z = c2Start.z + (c2End.z - c2Start.z) * c2t;
var c2y = c2Start.y + (c2End.y - c2Start.y) * c2t;
var c2sag = -Math.sin(c2t * Math.PI) * 2.5;
cable2Points.push(new THREE.Vector3(c2x, c2y + c2sag, c2z));
}
sugarLoafGroup.add(createLine(cable2Points, createLineMat(0x444444, 0.4)));
var cable2bPoints = [];
for (var c2bi = 0; c2bi <= 35; c2bi++) {
var c2bt = c2bi / 35;
var c2bx = c2Start.x + (c2End.x - c2Start.x) * c2bt + 0.3;
var c2bz = c2Start.z + (c2End.z - c2Start.z) * c2bt + 0.3;
var c2by = c2Start.y + (c2End.y - c2Start.y) * c2bt;
var c2bsag = -Math.sin(c2bt * Math.PI) * 2.8;
cable2bPoints.push(new THREE.Vector3(c2bx, c2by + c2bsag, c2bz));
}
sugarLoafGroup.add(createLine(cable2bPoints, createLineMat(0x444444, 0.3)));
var gondola1 = new THREE.Mesh(new THREE.BoxGeometry(1.4, 1.0, 0.9), createMat(0x555555, 0.5));
gondola1.userData = { cablePoints: cable1Points, t: 0, speed: 0.003, hangOffset: -1.2 };
sugarLoafGroup.add(gondola1);
gondola1.add(new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(1.4, 1.0, 0.9)), createLineMat(0x222222, 0.6)));
var gondola2 = new THREE.Mesh(new THREE.BoxGeometry(1.4, 1.0, 0.9), createMat(0x555555, 0.5));
gondola2.userData = { cablePoints: cable2Points, t: 0.5, speed: 0.002, hangOffset: -1.2 };
sugarLoafGroup.add(gondola2);
gondola2.add(new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(1.4, 1.0, 0.9)), createLineMat(0x222222, 0.6)));
var topBeacon = new THREE.Mesh(new THREE.SphereGeometry(0.35, 6, 6), createMat(0xffffff, 0.8));
topBeacon.position.set(pdaX, pdaPeakY + 1, pdaZ);
topBeacon.userData = { blink: 1.5, offset: 0 };
sugarLoafGroup.add(topBeacon);
blinkLights.push(topBeacon);
var urcaBeacon = new THREE.Mesh(new THREE.SphereGeometry(0.2, 6, 6), createMat(0xffffff, 0.7));
urcaBeacon.position.set(urcaX, urcaPeakY + 0.5, urcaZ);
urcaBeacon.userData = { blink: 2.0, offset: 1.5 };
sugarLoafGroup.add(urcaBeacon);
blinkLights.push(urcaBeacon);
var pdaLightYs = [10, 18, 26, 33];
for (var pli = 0; pli < pdaLightYs.length; pli++) {
var plY = pdaLightYs[pli];
var plight = new THREE.Mesh(new THREE.SphereGeometry(0.12, 4, 4), createMat(0xdddddd, 0.6));
var plAngle = rand(0, Math.PI * 2);
var plR = 3;
for (var plri = 0; plri < rings.length - 1; plri++) {
if (rings[plri].y <= plY && rings[plri + 1].y >= plY) {
var plFrac = (plY - rings[plri].y) / (rings[plri + 1].y - rings[plri].y);
plR = rings[plri].r + (rings[plri + 1].r - rings[plri].r) * plFrac;
break;
}
}
plight.position.set(pdaX + Math.cos(plAngle) * (plR * 0.8), plY, pdaZ + Math.sin(plAngle) * (plR * 0.8));
plight.userData = { blink: rand(0.8, 2.5), offset: rand(0, 6.28) };
sugarLoafGroup.add(plight);
blinkLights.push(plight);
}
var urcaLightYs = [8, 16, 22];
for (var uli = 0; uli < urcaLightYs.length; uli++) {
var ulight = new THREE.Mesh(new THREE.SphereGeometry(0.12, 4, 4), createMat(0xdddddd, 0.5));
var ulAngle = rand(0, Math.PI * 2);
ulight.position.set(urcaX + Math.cos(ulAngle) * 3.0, urcaLightYs[uli], urcaZ + Math.sin(ulAngle) * 3.0);
ulight.userData = { blink: rand(0.8, 2.5), offset: rand(0, 6.28) };
sugarLoafGroup.add(ulight);
blinkLights.push(ulight);
}
for (var rki = 0; rki < 16; rki++) {
var rkAngle = (rki / 16) * Math.PI * 2 + rand(-0.2, 0.2);
var rkCx = (pdaX + urcaX) / 2;
var rkCz = (pdaZ + urcaZ) / 2;
var rkDist = 16 + rand(0, 6);
var rockGeo = new THREE.DodecahedronGeometry(rand(0.8, 2.5), 0);
var rock = new THREE.Mesh(rockGeo, createMat(0x777777, 0.08));
rock.position.set(rkCx + Math.cos(rkAngle) * rkDist, rand(0.2, 1.2), rkCz + Math.sin(rkAngle) * rkDist);
rock.rotation.set(rand(0, 1), rand(0, 1), rand(0, 1));
rock.scale.y = rand(0.4, 1.2);
sugarLoafGroup.add(rock);
var rockEdge = new THREE.LineSegments(new THREE.EdgesGeometry(rockGeo), createLineMat(0x444444, 0.25));
rockEdge.position.copy(rock.position);
rockEdge.rotation.copy(rock.rotation);
rockEdge.scale.copy(rock.scale);
sugarLoafGroup.add(rockEdge);
}
for (var sri = 0; sri < 5; sri++) {
var scanY = 4 + sri * 7;
var scanR = 4;
for (var srri = 0; srri < rings.length - 1; srri++) {
if (rings[srri].y <= scanY && rings[srri + 1].y >= scanY) {
var srFrac = (scanY - rings[srri].y) / (rings[srri + 1].y - rings[srri].y);
scanR = rings[srri].r + (rings[srri + 1].r - rings[srri].r) * srFrac + 1.5;
break;
}
}
var scanRing = new THREE.Mesh(
new THREE.RingGeometry(scanR, scanR + 0.08, 16),
createMat(0x6688aa, 0.06, false, THREE.DoubleSide)
);
scanRing.rotation.x = -Math.PI / 2;
scanRing.position.set(pdaX, scanY, pdaZ);
sugarLoafGroup.add(scanRing);
}
for (var usri = 0; usri < 3; usri++) {
var uScanY = 5 + usri * 8;
var uScanR = 4.5 - usri * 0.8;
var uScanRing = new THREE.Mesh(
new THREE.RingGeometry(uScanR, uScanR + 0.08, 16),
createMat(0x6688aa, 0.05, false, THREE.DoubleSide)
);
uScanRing.rotation.x = -Math.PI / 2;
uScanRing.position.set(urcaX, uScanY, urcaZ);
sugarLoafGroup.add(uScanRing);
}
sugarLoafGroup.add(createLine([
new THREE.Vector3(pdaX + 3, pdaPeakY + 2, pdaZ),
new THREE.Vector3(pdaX + 14, pdaPeakY + 8, pdaZ + 5),
new THREE.Vector3(pdaX + 22, pdaPeakY + 8, pdaZ + 5)
], createLineMat(0x555555, 0.35)));
var pdaLabelDot = new THREE.Mesh(new THREE.SphereGeometry(0.15, 4, 4), createMat(0x666666, 0.5));
pdaLabelDot.position.set(pdaX + 3, pdaPeakY + 2, pdaZ);
sugarLoafGroup.add(pdaLabelDot);
sugarLoafGroup.add(createLine([
new THREE.Vector3(urcaX - 2, urcaPeakY + 2, urcaZ),
new THREE.Vector3(urcaX - 12, urcaPeakY + 8, urcaZ + 6),
new THREE.Vector3(urcaX - 20, urcaPeakY + 8, urcaZ + 6)
], createLineMat(0x555555, 0.3)));
var urcaLabelDot = new THREE.Mesh(new THREE.SphereGeometry(0.12, 4, 4), createMat(0x666666, 0.45));
urcaLabelDot.position.set(urcaX - 2, urcaPeakY + 2, urcaZ);
sugarLoafGroup.add(urcaLabelDot);
var trailPts = [];
for (var tpi = 0; tpi <= 20; tpi++) {
var tpt = tpi / 20;
trailPts.push(new THREE.Vector3(
urcaX + (pdaX - urcaX) * tpt,
0.08,
urcaZ + (pdaZ - urcaZ) * tpt
));
}
sugarLoafGroup.add(createLine(trailPts, createLineMat(0x666666, 0.2)));
return {
group: sugarLoafGroup,
gondolas: [gondola1, gondola2]
};
}
var sugarLoaf = createSugarLoaf();
cityGroup.add(sugarLoaf.group);
var slExcludeX = 58, slExcludeZ = -56, slExcludeR = 28;
function createBuilding(x, z, maxH, footW, footD) {
var dx = x - slExcludeX, dz = z - slExcludeZ;
if (Math.sqrt(dx * dx + dz * dz) < slExcludeR) return new THREE.Group();
if (x > cityExtent - 2) return new THREE.Group();
if (z < -cityExtent + 2) return new THREE.Group();
var group = new THREE.Group();
group.position.set(x, 0, z);
var type = randInt(0, 8);
var w = footW || rand(1.2, 3);
var d = footD || rand(1.2, 3);
var h, geo;
switch(type) {
case 0: h = rand(5, maxH); geo = new THREE.BoxGeometry(w, h, d); break;
case 1: h = rand(10, maxH * 1.3); geo = new THREE.BoxGeometry(w * 0.7, h, d * 0.7); break;
case 2: h = rand(2, 8); geo = new THREE.BoxGeometry(w, h, d); break;
case 3: h = rand(6, maxH); geo = new THREE.CylinderGeometry(Math.min(w,d)*0.4, Math.min(w,d)*0.45, h, 8); break;
case 4: h = rand(12, maxH * 1.2); geo = new THREE.CylinderGeometry(Math.min(w,d)*0.12, Math.min(w,d)*0.45, h, 4); break;
case 5: h = rand(8, maxH); geo = new THREE.CylinderGeometry(Math.min(w,d)*0.45, Math.min(w,d)*0.45, h, 8); break;
case 6: h = rand(5, maxH); geo = new THREE.BoxGeometry(w, h, d); break;
default: h = rand(4, maxH); geo = new THREE.BoxGeometry(w, h, d); break;
}
function addMeshes(geometry, yPos) {
var fill = new THREE.Mesh(geometry, makeBuildingMat(rand(0.06, 0.18)));
fill.position.y = yPos; group.add(fill);
var wire = new THREE.Mesh(geometry, makeWireMat(rand(0.08, 0.2)));
wire.position.y = yPos; group.add(wire);
var edges = new THREE.LineSegments(new THREE.EdgesGeometry(geometry), Math.random() > 0.5 ? edgeMat : edgeMatDim);
edges.position.y = yPos; group.add(edges);
}
addMeshes(geo, h/2);
if (Math.random() > 0.5 && h > 8) {
var curH = h, curW = w, curD = d;
var levels = randInt(1, 3);
for (var lv = 0; lv < levels; lv++) {
curW *= rand(0.5, 0.8); curD *= rand(0.5, 0.8);
var lh = rand(2, 8);
addMeshes(new THREE.BoxGeometry(curW, lh, curD), curH + lh/2);
curH += lh;
}
h = curH;
}
if (type === 6 && Math.random() > 0.3) {
var ew = w * rand(0.3, 0.6), ed = d * rand(0.5, 0.9), eh = h * rand(0.5, 1.0);
var eGeo = new THREE.BoxGeometry(ew, eh, ed);
var eFill = new THREE.Mesh(eGeo, makeBuildingMat(rand(0.06, 0.15)));
eFill.position.set(w/2 + ew/2, eh/2, 0); group.add(eFill);
var eEdge = new THREE.LineSegments(new THREE.EdgesGeometry(eGeo), edgeMatDim);
eEdge.position.copy(eFill.position); group.add(eEdge);
}
if (Math.random() > 0.6) {
var aH = rand(2, 7);
var antenna = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.06, aH, 4), createMat(0x444444, 1));
antenna.position.y = h + aH/2; group.add(antenna);
var beacon = new THREE.Mesh(new THREE.SphereGeometry(0.12, 6, 6), createMat(0xffffff, 0.9));
beacon.position.y = h + aH;
beacon.userData = { blink: rand(1, 4), offset: rand(0, 6.28) };
group.add(beacon); blinkLights.push(beacon);
}
if (h > 5 && Math.random() > 0.2) {
var wPos = [];
var floors = Math.floor(h / 1.5);
var wPerFloor = Math.max(2, Math.floor(w / 0.8));
var ww = w * 0.5, wd = d * 0.5;
for (var f = 1; f < floors; f++) {
var wy = f * 1.5 + rand(-0.1, 0.1);
for (var wi = 0; wi < wPerFloor && wPos.length < randInt(12, 60) * 3; wi++) {
var wside = randInt(0, 4);
var frac = (wi + 0.5) / wPerFloor;
var wx, wz;
if (wside === 0) { wx = ww + 0.05; wz = -wd + frac * 2 * wd; }
else if (wside === 1) { wx = -ww - 0.05; wz = -wd + frac * 2 * wd; }
else if (wside === 2) { wx = -ww + frac * 2 * ww; wz = wd + 0.05; }
else { wx = -ww + frac * 2 * ww; wz = -wd - 0.05; }
wPos.push(wx, wy, wz);
}
}
if (wPos.length > 0) {
var wGeo = new THREE.BufferGeometry();
wGeo.setAttribute('position', new THREE.Float32BufferAttribute(wPos, 3));
var wPts = new THREE.Points(wGeo, new THREE.PointsMaterial({
color: 0x333333, size: rand(0.15, 0.35), transparent: true, opacity: rand(0.3, 0.7)
}));
group.add(wPts); windowPoints.push(wPts);
}
}
return group;
}
var totalBlocks = 0;
var buildingSetback = 0.6;
for (var ai = 0; ai < avenuePositions.length - 1; ai++) {
for (var si2 = 0; si2 < streetPositions.length - 1; si2++) {
var x0 = avenuePositions[ai], x1 = avenuePositions[ai + 1];
var z0 = streetPositions[si2], z1 = streetPositions[si2 + 1];
var sw0 = ((ai % 3 === 0) ? majorStreetWidth : minorStreetWidth) / 2 + sidewalkWidth + buildingSetback;
var sw1 = (((ai+1) % 3 === 0) ? majorStreetWidth : minorStreetWidth) / 2 + sidewalkWidth + buildingSetback;
var sw2 = ((si2 % 4 === 0) ? majorStreetWidth : minorStreetWidth) / 2 + sidewalkWidth + buildingSetback;
var sw3 = (((si2+1) % 4 === 0) ? majorStreetWidth : minorStreetWidth) / 2 + sidewalkWidth + buildingSetback;
var blockLeft = x0 + sw0, blockRight = x1 - sw1;
var blockBottom = z0 + sw2, blockTop = z1 - sw3;
var blockW = blockRight - blockLeft, blockD = blockTop - blockBottom;
if (blockW < 2 || blockD < 2 || Math.random() < 0.2) continue; // increased empty block chance
totalBlocks++;
var blockCX = (blockLeft + blockRight) / 2, blockCZ = (blockBottom + blockTop) / 2;
var bDist = Math.sqrt(blockCX * blockCX + blockCZ * blockCZ);
var baseMaxH = 5 + (1 - Math.min(bDist / cityExtent, 1)) * 45 + rand(0, 10);
var blockType = Math.random();
if (blockType < 0.3 && blockW > 5 && blockD > 5) {
cityGroup.add(createBuilding(blockCX, blockCZ, baseMaxH, blockW * rand(0.7, 0.95), blockD * rand(0.7, 0.95)));
} else if (blockType < 0.6) {
var divisions = Math.max(1, Math.floor(blockW / rand(4, 6.5))); // fewer divisions
var slotW = blockW / divisions;
for (var di = 0; di < divisions; di++) {
var bx = blockLeft + slotW * (di + 0.5);
if (Math.random() > 0.2) { // increased skip chance
var bd1 = Math.min(blockD * 0.4, rand(2, 4));
cityGroup.add(createBuilding(bx, blockBottom + bd1/2 + 0.2, baseMaxH * rand(0.5, 1), slotW * rand(0.75, 0.95), bd1));
}
if (Math.random() > 0.35) { // increased skip chance
var bd2 = Math.min(blockD * 0.4, rand(2, 4));
cityGroup.add(createBuilding(bx, blockTop - bd2/2 - 0.2, baseMaxH * rand(0.4, 0.9), slotW * rand(0.75, 0.95), bd2));
}
}
var sideDivisions = Math.max(1, Math.floor(blockD / rand(4, 6.5))); // fewer divisions
var slotD = blockD / sideDivisions;
for (var di2 = 0; di2 < sideDivisions; di2++) {
var bz = blockBottom + slotD * (di2 + 0.5);
if (Math.random() > 0.45) { // increased skip chance
var bw1 = Math.min(blockW * 0.3, rand(1.5, 3));
cityGroup.add(createBuilding(blockLeft + bw1/2 + 0.2, bz, baseMaxH * rand(0.3, 0.8), bw1, slotD * rand(0.7, 0.9)));
}
if (Math.random() > 0.45) { // increased skip chance
var bw2 = Math.min(blockW * 0.3, rand(1.5, 3));
cityGroup.add(createBuilding(blockRight - bw2/2 - 0.2, bz, baseMaxH * rand(0.3, 0.8), bw2, slotD * rand(0.7, 0.9)));
}
}
} else {
var cols = Math.max(1, Math.floor(blockW / rand(3.5, 5.5))); // fewer cols
var rows = Math.max(1, Math.floor(blockD / rand(3.5, 5.5))); // fewer rows
var cellW = blockW / cols, cellD = blockD / rows;
for (var c = 0; c < cols; c++) {
for (var r = 0; r < rows; r++) {
if (Math.random() < 0.35) continue; // increased skip from 0.15 to 0.35
cityGroup.add(createBuilding(blockLeft + cellW * (c + 0.5), blockBottom + cellD * (r + 0.5),
baseMaxH * rand(0.3, 1), cellW * rand(0.6, 0.9), cellD * rand(0.6, 0.9)));
}
}
}
if (Math.random() < 0.08) { // slightly more parks to fill gaps
var treeCount = randInt(3, 10);
for (var ti = 0; ti < treeCount; ti++) {
var tx = rand(blockLeft + 1, blockRight - 1);
var tz = rand(blockBottom + 1, blockTop - 1);
var tH = rand(1.5, 4);
var trunk2 = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.12, tH, 4), createMat(0x666655, 0.3));
trunk2.position.set(tx, tH/2, tz); cityGroup.add(trunk2);
var canopyGeo = new THREE.SphereGeometry(rand(0.8, 1.8), 6, 6);
var canopy = new THREE.Mesh(canopyGeo, createMat(0x667766, 0.15));
canopy.position.set(tx, tH + 0.5, tz); cityGroup.add(canopy);
var canopyEdge = new THREE.LineSegments(new THREE.EdgesGeometry(canopyGeo), createLineMat(0x556655, 0.2));
canopyEdge.position.copy(canopy.position); cityGroup.add(canopyEdge);
}
}
}
}
// Street lights
for (var sli2 = 0; sli2 < avenuePositions.length; sli2++) {
var slsw = (sli2 % 3 === 0) ? majorStreetWidth : minorStreetWidth;
for (var slz = -cityExtent; slz < cityExtent; slz += rand(6, 12)) {
for (var slside = -1; slside <= 1; slside += 2) {
var lx = avenuePositions[sli2] + slside * (slsw/2 + sidewalkWidth * 0.5);
var pole = new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.06, 3, 4), createMat(0x555555, 0.3));
pole.position.set(lx, 1.5, slz); cityGroup.add(pole);
var slight = new THREE.Mesh(new THREE.SphereGeometry(0.1, 4, 4), createMat(0xeeeecc, 0.6));
slight.position.set(lx, 3.1, slz);
slight.userData = { blink: rand(0.2, 0.8), offset: rand(0, 6.28) };
cityGroup.add(slight); blinkLights.push(slight);
}
}
}
// Traffic - reduced count
var trafficParticles = [];
var trafficCount = 140; // reduced from 200
for (var tri = 0; tri < trafficCount; tri++) {
var trIsAvenue = Math.random() > 0.5;
var trStreetIdx = randInt(0, trIsAvenue ? avenuePositions.length : streetPositions.length);
var trPos, trDir;
if (trIsAvenue) {
trPos = new THREE.Vector3(avenuePositions[trStreetIdx] + rand(-0.8, 0.8), 0.15, rand(-cityExtent, cityExtent));
trDir = new THREE.Vector3(0, 0, Math.random() > 0.5 ? 1 : -1);
} else {
trPos = new THREE.Vector3(rand(-cityExtent, cityExtent), 0.15, streetPositions[trStreetIdx] + rand(-0.8, 0.8));
trDir = new THREE.Vector3(Math.random() > 0.5 ? 1 : -1, 0, 0);
}
var tMesh = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.2, 0.3), createMat(Math.random() > 0.5 ? 0xffffee : 0xffeecc, rand(0.3, 0.6)));
tMesh.position.copy(trPos);
if (trIsAvenue) tMesh.rotation.y = Math.PI / 2;
cityGroup.add(tMesh);
trafficParticles.push({ mesh: tMesh, dir: trDir, speed: rand(0.05, 0.2) });
}
scene.add(cityGroup);
var gridHelper = new THREE.GridHelper(250, 125, 0x999999, 0xbbbbbb);
gridHelper.material.transparent = true;
gridHelper.material.opacity = 0.12;
gridHelper.position.y = 0.005;
scene.add(gridHelper);
var pulseRings = [];
for (var pri = 0; pri < 4; pri++) {
var baseR = 3 + pri * 15;
var pring = new THREE.Mesh(new THREE.RingGeometry(baseR, baseR + 0.15, 64), createMat(0x888888, 0.1, false, THREE.DoubleSide));
pring.rotation.x = -Math.PI / 2;
pring.position.y = 0.05;
pring.userData = { baseRadius: baseR, speed: 0.3 + pri * 0.1 };
scene.add(pring); pulseRings.push(pring);
}
var pCount = 3000;
var pGeo = new THREE.BufferGeometry();
var pPos = new Float32Array(pCount * 3);
var pVel = new Float32Array(pCount);
for (var ppi = 0; ppi < pCount; ppi++) {
pPos[ppi * 3] = rand(-90, 90);
pPos[ppi * 3 + 1] = rand(0, 60);
pPos[ppi * 3 + 2] = rand(-90, 90);
pVel[ppi] = rand(0.03, 0.18);
}
pGeo.setAttribute('position', new THREE.BufferAttribute(pPos, 3));
var pMat = new THREE.PointsMaterial({ color: 0x444444, size: 0.2, transparent: true, opacity: 0.35, sizeAttenuation: true });
var particleSystem = new THREE.Points(pGeo, pMat);
scene.add(particleSystem);
var streams = [];
for (var sti = 0; sti < 40; sti++) {
var stx = rand(-80, 80), stz = rand(-80, 80), sth = rand(15, 50);
var stPoints = [];
for (var stj = 0; stj <= 80; stj++) stPoints.push(new THREE.Vector3(stx, stj * sth / 80, stz));
var sLine = createLine(stPoints, createLineMat(0x555555, 0.08));
sLine.userData = { baseOpacity: rand(0.04, 0.15), speed: rand(0.5, 3), offset: rand(0, 6.28) };
scene.add(sLine); streams.push(sLine);
}
var connections = [];
for (var cni = 0; cni < 30; cni++) {
var cnx1 = rand(-70, 70), cnz1 = rand(-70, 70);
var cnx2 = cnx1 + rand(-25, 25), cnz2 = cnz1 + rand(-25, 25), cny = rand(3, 35);
var cnPts = [];
for (var cnj = 0; cnj <= 30; cnj++) {
var cnt = cnj / 30;
cnPts.push(new THREE.Vector3(cnx1 + (cnx2 - cnx1) * cnt, cny + Math.sin(cnt * Math.PI) * rand(1, 5), cnz1 + (cnz2 - cnz1) * cnt));
}
var cLine = createLine(cnPts, createLineMat(0x666666, 0.06));
cLine.userData = { speed: rand(0.5, 2.5), offset: rand(0, 6.28) };
scene.add(cLine); connections.push(cLine);
}
var packets = [];
for (var pki = 0; pki < 40; pki++) {
var packet = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.3, 0.3), createMat(0x333333, 0.5));
packet.userData = { pathIndex: randInt(0, connections.length), t: Math.random(), speed: rand(0.002, 0.01) };
scene.add(packet); packets.push(packet);
}
var holoCircles = [];
for (var hci = 0; hci < 6; hci++) {
var hcr = rand(1, 4);
var hc = new THREE.Mesh(new THREE.RingGeometry(hcr, hcr + 0.08, 32), createMat(0x777777, rand(0.04, 0.12), false, THREE.DoubleSide));
hc.position.set(rand(-40, 40), rand(10, 40), rand(-40, 40));
hc.userData = { rotSpeed: rand(0.2, 1), bobSpeed: rand(0.3, 1), bobAmp: rand(0.5, 2), baseY: hc.position.y };
scene.add(hc); holoCircles.push(hc);
}
var frame = 0;
var clockObj = new THREE.Clock();
function updateElement(id, value) { document.getElementById(id).textContent = value; }
function animate() {
requestAnimationFrame(animate);
var t = clockObj.getElapsedTime();
frame++;
radius += (targetRadius - radius) * 0.06;
if (!dragging) angleX -= 0.0012;
camera.position.set(Math.sin(angleX) * radius, 8 + angleY * radius * 0.55, Math.cos(angleX) * radius);
camera.lookAt(0, 10, 0);
for (var bli = 0; bli < blinkLights.length; bli++) {
var bl = blinkLights[bli];
var val = Math.abs(Math.sin(t * bl.userData.blink + bl.userData.offset));
bl.material.opacity = 0.2 + 0.8 * val;
bl.scale.setScalar(0.8 + 0.4 * val);
}
for (var wpi = 0; wpi < windowPoints.length; wpi++) {
windowPoints[wpi].material.opacity = 0.2 + 0.4 * Math.abs(Math.sin(t * 1.5 + windowPoints[wpi].id));
}
var posArr = particleSystem.geometry.attributes.position.array;
for (var pai = 0; pai < pCount; pai++) {
posArr[pai * 3 + 1] += pVel[pai];
if (posArr[pai * 3 + 1] > 65) {
posArr[pai * 3 + 1] = 0;
posArr[pai * 3] = rand(-90, 90);
posArr[pai * 3 + 2] = rand(-90, 90);
}
}
particleSystem.geometry.attributes.position.needsUpdate = true;
pMat.opacity = 0.2 + 0.15 * Math.sin(t * 0.5);
for (var sti2 = 0; sti2 < streams.length; sti2++) {
var st = streams[sti2];
st.material.opacity = st.userData.baseOpacity * (0.5 + 0.5 * Math.sin(t * st.userData.speed + st.userData.offset));
}
for (var cni2 = 0; cni2 < connections.length; cni2++) {
var cn = connections[cni2];
cn.material.opacity = 0.03 + 0.1 * Math.abs(Math.sin(t * cn.userData.speed + cn.userData.offset));
}
for (var pki2 = 0; pki2 < packets.length; pki2++) {
var pk = packets[pki2];
pk.userData.t += pk.userData.speed;
if (pk.userData.t > 1) pk.userData.t = 0;
var conn = connections[pk.userData.pathIndex];
if (conn) {
var connPos = conn.geometry.attributes.position.array;
var totalPts = connPos.length / 3;
var idx = Math.floor(pk.userData.t * (totalPts - 1));
var nextIdx = Math.min(idx + 1, totalPts - 1);
var frac2 = (pk.userData.t * (totalPts - 1)) - idx;
pk.position.x = connPos[idx * 3] + (connPos[nextIdx * 3] - connPos[idx * 3]) * frac2;
pk.position.y = connPos[idx * 3 + 1] + (connPos[nextIdx * 3 + 1] - connPos[idx * 3 + 1]) * frac2;
pk.position.z = connPos[idx * 3 + 2] + (connPos[nextIdx * 3 + 2] - connPos[idx * 3 + 2]) * frac2;
}
pk.rotation.x += 0.05; pk.rotation.y += 0.03;
}
for (var pri2 = 0; pri2 < pulseRings.length; pri2++) {
var pr = pulseRings[pri2];
var ps = 1 + 0.3 * Math.sin(t * pr.userData.speed);
pr.scale.set(ps, ps, 1);
pr.material.opacity = 0.04 + 0.08 * Math.abs(Math.sin(t * pr.userData.speed));
}
for (var hci2 = 0; hci2 < holoCircles.length; hci2++) {
var hc2 = holoCircles[hci2];
hc2.rotation.x += hc2.userData.rotSpeed * 0.01;
hc2.rotation.y += hc2.userData.rotSpeed * 0.015;
hc2.position.y = hc2.userData.baseY + Math.sin(t * hc2.userData.bobSpeed) * hc2.userData.bobAmp;
}
for (var tri2 = 0; tri2 < trafficParticles.length; tri2++) {
var tp = trafficParticles[tri2];
tp.mesh.position.x += tp.dir.x * tp.speed;
tp.mesh.position.z += tp.dir.z * tp.speed;
if (tp.mesh.position.x > cityExtent + 5) tp.mesh.position.x = -cityExtent - 5;
if (tp.mesh.position.x < -cityExtent - 5) tp.mesh.position.x = cityExtent + 5;
if (tp.mesh.position.z > cityExtent + 5) tp.mesh.position.z = -cityExtent - 5;
if (tp.mesh.position.z < -cityExtent - 5) tp.mesh.position.z = cityExtent + 5;
}
// Animate gondolas
var gondolas = sugarLoaf.gondolas;
for (var gi = 0; gi < gondolas.length; gi++) {
var gond = gondolas[gi];
gond.userData.t += gond.userData.speed;
if (gond.userData.t > 1) gond.userData.t = 0;
var gCable = gond.userData.cablePoints;
var gIdx = Math.floor(gond.userData.t * (gCable.length - 1));
var gNext = Math.min(gIdx + 1, gCable.length - 1);
var gFrac = (gond.userData.t * (gCable.length - 1)) - gIdx;
gond.position.lerpVectors(gCable[gIdx], gCable[gNext], gFrac);
gond.position.y += gond.userData.hangOffset;
}
// Animate ocean waves
for (var wvi = 0; wvi < waveVertices.length; wvi++) {
var wv = waveVertices[wvi];
var wvPos = wv.geo.attributes.position.array;
var wvBase = wv.baseVerts;
var wvTotal = wvPos.length / 3;
for (var wvj = 0; wvj < wvTotal; wvj++) {
var bx2 = wvBase[wvj * 3];
var by2 = wvBase[wvj * 3 + 1];
var bz2 = wvBase[wvj * 3 + 2];
var waveOffset = Math.sin(bx2 * 0.08 + t * 1.2 + wv.layer) * 0.3
+ Math.cos(bz2 * 0.06 + t * 0.8) * 0.2
+ Math.sin((bx2 + bz2) * 0.04 + t * 0.5) * 0.15;
wvPos[wvj * 3 + 1] = by2 + waveOffset;
}
wv.geo.attributes.position.needsUpdate = true;
wv.geo.computeVertexNormals();
}
// Animate wave shore lines
for (var wli2 = 0; wli2 < waveLines.length; wli2++) {
var wlObj = waveLines[wli2];
if (wlObj.userData && wlObj.userData.baseY !== undefined) {
wlObj.position.y = wlObj.userData.baseY + Math.sin(t * wlObj.userData.bobSpeed + wlObj.userData.bobPhase) * wlObj.userData.bobAmp;
wlObj.rotation.z = Math.sin(t * wlObj.userData.rockSpeed + wlObj.userData.bobPhase) * wlObj.userData.rockAmp;
wlObj.rotation.x = Math.cos(t * wlObj.userData.rockSpeed * 0.7 + wlObj.userData.bobPhase) * wlObj.userData.rockAmp * 0.5;
continue;
}
if (wlObj.geometry && wlObj.userData && wlObj.userData.baseOffset !== undefined) {
var wlPositions = wlObj.geometry.attributes.position.array;
var wlCount = wlPositions.length / 3;
var wlPulse = Math.sin(t * wlObj.userData.speed + wlObj.userData.phase) * 1.5;
for (var wlvi = 0; wlvi < wlCount; wlvi++) {
wlPositions[wlvi * 3 + 1] = -0.1 + Math.sin(t * 2 + wlvi * 0.3 + wlObj.userData.phase) * 0.08;
}
wlObj.geometry.attributes.position.needsUpdate = true;
wlObj.material.opacity = 0.05 + 0.1 * Math.abs(Math.sin(t * wlObj.userData.speed + wlObj.userData.phase));
}
}
if (frame % 20 === 0) {
updateElement('freq', (400 + Math.random() * 100).toFixed(1));
updateElement('nodes', Math.floor(2500 + Math.random() * 500));
updateElement('latency', Math.floor(8 + Math.random() * 15));
updateElement('traffic', Math.floor(trafficCount * (0.8 + Math.random() * 0.4)));
}
if (frame % 120 === 0) {
var sectors = ['7G-NORTH','4A-EAST','9C-SOUTH','2F-WEST','5B-CENTRAL','3D-UPTOWN','8E-HARBOR','PÃO-AÇÚCAR','COPACABANA','IPANEMA'];
updateElement('sector', sectors[randInt(0, sectors.length)]);
var dstreams = ['ACTIVE','SYNC','BURST','PEAK','IDLE','TIDAL'];
updateElement('datastream', dstreams[randInt(0, dstreams.length)]);
}
updateElement('frame', frame);
updateElement('blocks', totalBlocks);
if (frame % 4 === 0) {
var df = '';
for (var dfi = 0; dfi < 14; dfi++) df += Math.random() > 0.5 ? '█' : '░';
updateElement('dataflow', df);
}
var now = new Date();
var hh = now.getHours(), mm = now.getMinutes(), ss = now.getSeconds();
updateElement('clock', (hh < 10 ? '0' : '') + hh + ':' + (mm < 10 ? '0' : '') + mm + ':' + (ss < 10 ? '0' : '') + ss);
renderer.render(scene, camera);
}
animate();
})();
</script>
</body></html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment