MOUNTAIN RIDGES - TERRAIN ANIMATION SOURCE CODE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Circular Terrain</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
width: 100%; height: 100%;
overflow: hidden;
background: #000a14;
}
canvas { display: block; width: 100% !important; height: 100% !important; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(function(){
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(window.devicePixelRatio || 1);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x000a14, 1);
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000a14, 0.0000045);
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 800000);
camera.position.set(0, 28000, 42000);
camera.lookAt(0, 0, 0);
// ---------- THEME SYNC ----------
function syncTheme() {
try {
var isDark = true;
if (window.parent && window.parent.document) {
isDark = window.parent.document.documentElement.classList.contains('dark');
} else {
var theme = localStorage.getItem('theme');
isDark = theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches);
}
if (!isDark) {
document.body.style.backgroundColor = '#f0f4f8';
if (typeof renderer !== 'undefined' && typeof scene !== 'undefined') {
renderer.setClearColor(0xf0f4f8, 1);
scene.fog.color.setHex(0xf0f4f8);
}
} else {
document.body.style.backgroundColor = '#000a14';
if (typeof renderer !== 'undefined' && typeof scene !== 'undefined') {
renderer.setClearColor(0x000a14, 1);
scene.fog.color.setHex(0x000a14);
}
}
} catch (e) {
console.warn("Could not sync theme", e);
}
}
syncTheme();
if (window.parent && window.parent.document) {
try {
var observer = new MutationObserver(syncTheme);
observer.observe(window.parent.document.documentElement, { attributes: true, attributeFilter: ['class'] });
} catch(e) {}
}
window.addEventListener('storage', syncTheme);
scene.add(new THREE.AmbientLight(0x0a1a2e, 1.2));
const sun = new THREE.DirectionalLight(0x7faaff, 1.5);
sun.position.set(30000, 50000, 20000);
scene.add(sun);
const warm = new THREE.PointLight(0xff6030, 2.0, 120000);
warm.position.set(0, 20000, 0);
scene.add(warm);
const radial = 120;
const rings = 60;
const radius = 60000;
const vertH = 10000;
const totalVerts = (rings + 1) * radial;
const positions = new Float32Array(totalVerts * 3);
const colors = new Float32Array(totalVerts * 3);
const baseY = new Float32Array(totalVerts);
for (let r = 0; r <= rings; r++) {
const rr = (r / rings) * radius;
for (let s = 0; s < radial; s++) {
const ang = (s / radial) * Math.PI * 2;
const idx = r * radial + s;
const x = Math.cos(ang) * rr;
const z = Math.sin(ang) * rr;
const y = (Math.random() - 0.5) * vertH * (0.3 + 0.7 * (r / rings));
positions[idx * 3] = x;
positions[idx * 3 + 1] = y;
positions[idx * 3 + 2] = z;
baseY[idx] = y;
}
}
const indices = [];
for (let r = 0; r < rings; r++) {
for (let s = 0; s < radial; s++) {
const v1 = r * radial + s;
const v2 = r * radial + (s + 1) % radial;
const v3 = (r + 1) * radial + s;
const v4 = (r + 1) * radial + (s + 1) % radial;
indices.push(v1, v2, v3);
indices.push(v2, v4, v3);
}
}
const geo = new THREE.BufferGeometry();
geo.setIndex(indices);
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const mat = new THREE.MeshPhongMaterial({
vertexColors: true,
wireframe: true,
shininess: 80,
opacity: 0.85,
transparent: true
});
const mesh = new THREE.Mesh(geo, mat);
scene.add(mesh);
const camR = Math.sqrt(camera.position.x ** 2 + camera.position.z ** 2);
let t = 0;
function updateColors() {
const pos = geo.attributes.position;
const col = geo.attributes.color;
for (let i = 0; i < totalVerts; i++) {
const y = pos.getY(i);
const norm = (y / vertH) * 0.5 + 0.5;
let r, g, b;
if (norm > 0.72) {
const f = (norm - 0.72) / 0.28;
r = 0.9 + f * 0.1; g = 0.55 + f * 0.3; b = 0.1 + f * 0.2;
} else if (norm > 0.5) {
const f = (norm - 0.5) / 0.22;
r = 0.05 + f * 0.15; g = 0.55 + f * 0.25; b = 0.6 + f * 0.25;
} else if (norm > 0.28) {
const f = (norm - 0.28) / 0.22;
r = 0.02 + f * 0.03; g = 0.15 + f * 0.4; b = 0.5 + f * 0.25;
} else {
const f = norm / 0.28;
r = 0.05 + f * 0.05; g = 0.02 + f * 0.13; b = 0.15 + f * 0.35;
}
col.setXYZ(i, r, g, b);
}
col.needsUpdate = true;
}
function animate() {
requestAnimationFrame(animate);
t += 0.012;
const pos = geo.attributes.position;
for (let i = 0; i < totalVerts; i++) {
const x = pos.getX(i);
const z = pos.getZ(i);
const dist = Math.sqrt(x * x + z * z);
const ang = Math.atan2(z, x);
const wave =
Math.sin(dist * 0.00015 - t * 0.6 + ang * 1.5) * 0.6 +
Math.cos(dist * 0.00025 + t * 0.4 - ang * 2.5) * 0.3 +
Math.sin(ang * 4 + t * 0.3) * 0.1;
pos.setY(i, baseY[i] * wave * 1.1);
}
pos.needsUpdate = true;
geo.computeVertexNormals();
updateColors();
const camAng = t * 0.018;
const breathe = 1 + 0.06 * Math.sin(t * 0.25);
camera.position.x = Math.sin(camAng) * camR * breathe;
camera.position.z = Math.cos(camAng) * camR * breathe;
camera.position.y = 26000 + 4000 * Math.sin(t * 0.18);
camera.lookAt(0, 1500, 0);
warm.position.x = Math.cos(t * 0.3) * 35000;
warm.position.z = Math.sin(t * 0.3) * 35000;
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
})();
</script>
</body>
</html>