INFINITE FLYOVER-01 - 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>RETROSCAPE — Infinite Flyover</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
html, body {
width:100%; height:100%;
overflow:hidden;
background:#000;
}
#container { width:100%; height:100vh; position:relative; }
canvas { display:block; }
#scanlines {
position:absolute; top:0; left:0; width:100%; height:100%;
pointer-events:none; z-index:10;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0,0,0,0.15) 2px,
rgba(0,0,0,0.15) 4px
);
}
/* Vignette */
#vignette {
position:absolute; top:0; left:0; width:100%; height:100%;
pointer-events:none; z-index:9;
background: radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,0.65) 100%);
}
@keyframes flicker {
0%,100% { opacity:1; }
92% { opacity:1; }
93% { opacity:0.7; }
94% { opacity:1; }
96% { opacity:0.85; }
97% { opacity:1; }
}
#container { animation: flicker 4s infinite; }
</style>
</head>
<body>
<div id="container">
<div id="scanlines"></div>
<div id="vignette"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(function(){
// ─── CONFIG ───────────────────────────────────
const PLANE_SIZE = 40000;
const SEGMENTS = 80;
const SPEED = 65;
const WAVE_AMP = 1800;
const SUN_RADIUS = 1200;
const container = document.getElementById('container');
const W = window.innerWidth, H = window.innerHeight;
const renderer = new THREE.WebGLRenderer({ antialias: false, alpha: false });
renderer.setSize(W, H);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setClearColor(0x0a0010, 1);
container.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0a0010, 0.000035);
const camera = new THREE.PerspectiveCamera(55, W / H, 1, 120000);
camera.position.set(0, 4800, 0);
camera.rotation.x = -Math.PI / 2.6;
// ---------- 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 = '#0a0010';
if (typeof renderer !== 'undefined' && typeof scene !== 'undefined') {
renderer.setClearColor(0x0a0010, 1);
scene.fog.color.setHex(0x0a0010);
}
}
} 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);
let clock = { t: 0 };
function makePlane(color, opacity, y) {
const geo = new THREE.PlaneGeometry(PLANE_SIZE, PLANE_SIZE, SEGMENTS, SEGMENTS);
const mat = new THREE.MeshBasicMaterial({
color,
wireframe: true,
transparent: opacity < 1,
opacity
});
const mesh = new THREE.Mesh(geo, mat);
mesh.rotation.x = -Math.PI / 2;
mesh.position.set(0, y, 0);
return mesh;
}
const groundColor = 0xff00ff; // magenta
const groundPlanes = [];
for (let i = 0; i < 3; i++) {
const p = makePlane(groundColor, 1, 0);
p.position.z = i * PLANE_SIZE;
scene.add(p);
groundPlanes.push(p);
}
const mtnGeo = new THREE.PlaneGeometry(PLANE_SIZE * 2, 8000, 160, 1);
const mtnMat = new THREE.MeshBasicMaterial({ color: 0x1a0030 });
const mtn = new THREE.Mesh(mtnGeo, mtnMat);
mtn.rotation.x = -Math.PI / 2;
mtn.position.set(0, -200, -18000);
scene.add(mtn);
const mtnPos = mtnGeo.attributes.position;
for (let i = 0; i < mtnPos.count; i++) {
const x = mtnPos.getX(i);
const nx = x / (PLANE_SIZE);
// layered peaks
const h = Math.sin(nx * 3.1) * 2200
+ Math.sin(nx * 7.3) * 1200
+ Math.sin(nx * 15.7) * 600
+ Math.sin(nx * 31) * 300;
mtnPos.setZ(i, Math.max(h, -500));
}
mtnPos.needsUpdate = true;
const mtnWireGeo = mtnGeo.clone();
const mtnWire = new THREE.Mesh(mtnWireGeo, new THREE.MeshBasicMaterial({
color: 0x4400aa, wireframe: true, transparent: true, opacity: 0.35
}));
mtnWire.rotation.x = -Math.PI / 2;
mtnWire.position.copy(mtn.position);
scene.add(mtnWire);
const sunGeo = new THREE.CircleGeometry(SUN_RADIUS, 64);
const sunMat = new THREE.MeshBasicMaterial({ color: 0xffddaa });
const sun = new THREE.Mesh(sunGeo, sunMat);
sun.position.set(0, 2200, -16000);
scene.add(sun);
const stripeCount = 12;
const stripes = [];
for (let i = 0; i < stripeCount; i++) {
const frac = i / stripeCount;
// stripe thickness tapers toward top
const y0 = -SUN_RADIUS + frac * SUN_RADIUS * 1.85;
const y1 = y0 + (SUN_RADIUS * 0.055) * (1 - frac * 0.7);
const halfW = Math.sqrt(Math.max(0, SUN_RADIUS * SUN_RADIUS - y0 * y0));
const sGeo = new THREE.PlaneGeometry(halfW * 2 + 40, y1 - y0);
const sMat = new THREE.MeshBasicMaterial({ color: 0x0a0010 });
const sMesh = new THREE.Mesh(sGeo, sMat);
sMesh.position.set(0, (y0 + y1) / 2, 0.5); // slightly in front of sun
sun.add(sMesh);
stripes.push(sMesh);
}
const glowGeo = new THREE.CircleGeometry(SUN_RADIUS * 1.6, 64);
const glowMat = new THREE.MeshBasicMaterial({ color: 0xff6644, transparent: true, opacity: 0.18 });
const glow = new THREE.Mesh(glowGeo, glowMat);
glow.position.set(0, 2200, -16010);
scene.add(glow);
const structures = [];
function makeStructure(x, z) {
const g = new THREE.BoxGeometry(
200 + Math.random() * 300,
800 + Math.random() * 2200,
200 + Math.random() * 300
);
const m = new THREE.MeshBasicMaterial({ color: 0x00ffff, wireframe: true, transparent: true, opacity: 0.55 });
const mesh = new THREE.Mesh(g, m);
mesh.position.set(x, mesh.geometry.parameters.height / 2 - 400, z);
scene.add(mesh);
const ig = new THREE.BoxGeometry(
mesh.geometry.parameters.width * 0.6,
mesh.geometry.parameters.height * 0.6,
mesh.geometry.parameters.depth * 0.6
);
const im = new THREE.MeshBasicMaterial({ color: 0x8800ff, transparent: true, opacity: 0.12 });
mesh.add(new THREE.Mesh(ig, im));
structures.push({ mesh, baseZ: z });
}
for (let i = 0; i < 20; i++) {
const side = (i % 2 === 0) ? 1 : -1;
const x = side * (3500 + Math.random() * 5000);
const z = i * (PLANE_SIZE / 20);
makeStructure(x, z);
}
const PARTICLE_COUNT = 1200;
const partGeo = new THREE.BufferGeometry();
const partPos = new Float32Array(PARTICLE_COUNT * 3);
const partCol = new Float32Array(PARTICLE_COUNT * 3);
function randParticle(i) {
partPos[i*3+0] = (Math.random()-0.5) * 30000;
partPos[i*3+1] = Math.random() * 8000 + 200;
partPos[i*3+2] = Math.random() * 50000 - 5000;
// pink / cyan / white
const r = Math.random();
if (r < 0.4) { partCol[i*3]=1; partCol[i*3+1]=0.2; partCol[i*3+2]=0.8; } // pink
else if (r < 0.7) { partCol[i*3]=0; partCol[i*3+1]=0.9; partCol[i*3+2]=1; } // cyan
else { partCol[i*3]=1; partCol[i*3+1]=1; partCol[i*3+2]=1; } // white
}
for (let i = 0; i < PARTICLE_COUNT; i++) randParticle(i);
partGeo.setAttribute('position', new THREE.BufferAttribute(partPos, 3));
partGeo.setAttribute('color', new THREE.BufferAttribute(partCol, 3));
const partMat = new THREE.PointsMaterial({ size: 8, vertexColors: true, transparent: true, opacity: 0.9 });
const particles = new THREE.Points(partGeo, partMat);
scene.add(particles);
const rings = [];
const RING_COUNT = 6;
for (let i = 0; i < RING_COUNT; i++) {
const rGeo = new THREE.RingGeometry(200, 250, 64);
const rMat = new THREE.MeshBasicMaterial({ color: 0xff00ff, transparent: true, opacity: 0.0, side: THREE.DoubleSide });
const rMesh = new THREE.Mesh(rGeo, rMat);
rMesh.rotation.x = -Math.PI / 2;
rMesh.position.set(0, 5, 0);
scene.add(rMesh);
rings.push({ mesh: rMesh, mat: rMat, phase: i / RING_COUNT });
}
let lastTime = performance.now();
function animate() {
requestAnimationFrame(animate);
const now = performance.now();
const dt = Math.min((now - lastTime) / 1000, 0.05);
lastTime = now;
clock.t += dt;
groundPlanes.forEach(p => {
p.position.z -= SPEED;
if (p.position.z < -PLANE_SIZE) p.position.z += PLANE_SIZE * 3;
const pos = p.geometry.attributes.position;
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i);
const y = pos.getY(i);
const wave = Math.sin(x * 0.00035 + clock.t * 1.8) * WAVE_AMP * 0.6
+ Math.sin(y * 0.00055 + clock.t * 2.4) * WAVE_AMP * 0.3
+ Math.sin((x+y) * 0.0003 + clock.t * 1.2) * WAVE_AMP * 0.2;
pos.setZ(i, wave);
}
pos.needsUpdate = true;
});
structures.forEach(s => {
s.mesh.position.z -= SPEED;
if (s.mesh.position.z < -PLANE_SIZE * 0.5) {
s.mesh.position.z += PLANE_SIZE;
}
s.mesh.rotation.y += dt * 0.15;
});
const pPos = particles.geometry.attributes.position;
for (let i = 0; i < PARTICLE_COUNT; i++) {
let z = pPos.getZ(i);
z -= SPEED * 0.6;
if (z < -8000) {
z = 42000;
pPos.setX(i, (Math.random()-0.5)*30000);
pPos.setY(i, Math.random()*8000+200);
}
// gentle float
pPos.setY(i, pPos.getY(i) + Math.sin(clock.t * 2 + i) * 0.4);
pPos.setZ(i, z);
}
pPos.needsUpdate = true;
rings.forEach((r, idx) => {
let progress = ((clock.t * 0.35 + r.phase) % 1);
const scale = 10 + progress * 3000;
r.mesh.scale.set(scale, scale, scale);
// fade: visible in middle, transparent at start/end
const alpha = 1 - Math.abs(progress - 0.5) * 2;
r.mat.opacity = Math.max(0, alpha * 0.5);
});
const pulse = 0.96 + Math.sin(clock.t * 3.7) * 0.04;
sunMat.color.setHex(0xffddaa);
sunMat.color.multiplyScalar(pulse);
glowMat.opacity = 0.14 + Math.sin(clock.t * 2.1) * 0.06;
const hueShift = 0.5 + Math.sin(clock.t * 0.25) * 0.15;
const gc = new THREE.Color().setHSL(hueShift, 1, 0.5);
groundPlanes.forEach(p => { p.material.color.copy(gc); });
mtnWire.position.z = mtn.position.z + Math.sin(clock.t * 0.8) * 30;
camera.position.y = 4800 + Math.sin(clock.t * 0.55) * 120;
camera.rotation.z = Math.sin(clock.t * 0.7) * 0.015;
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>