← Mike Higgins Home
Custom Code Experiment •

Terrain WF-3

Source Code View HTML / CSS / JavaScript
<!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>
  @import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Rajdhani:wght@300;400;600;700&display=swap');

  * { 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; }

  #hud {
    position:absolute; top:0; left:0; width:100%; height:100%;
    pointer-events:none;
    font-family:'Share Tech Mono', monospace;
    color:rgba(255,255,255,0.85);
  }

  .hud-top {
    position:absolute; top:18px; left:0; width:100%;
    display:flex; justify-content:space-between; align-items:flex-start;
    padding:0 28px;
  }
  .hud-title {
    font-family:'Rajdhani', sans-serif;
    font-weight:700; font-size:26px;
    letter-spacing:8px; text-transform:uppercase;
    color:#fff;
    text-shadow: 0 0 12px rgba(255,60,200,0.7), 0 0 40px rgba(255,60,200,0.3);
  }
  .hud-title span { color: rgba(255,100,220,0.6); font-weight:300; }

  .hud-top-right { text-align:right; font-size:11px; line-height:1.8; opacity:0.7; letter-spacing:2px; }

  #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%);
  }

  .hud-bottom {
    position:absolute; bottom:22px; left:0; width:100%;
    display:flex; justify-content:space-between; align-items:flex-end;
    padding:0 28px;
  }
  .hud-stat { font-size:11px; letter-spacing:2px; line-height:1.9; }
  .hud-stat .label { opacity:0.45; text-transform:uppercase; }
  .hud-stat .val { color:#fff; font-size:14px; }
  .hud-stat .val.pink { text-shadow:0 0 8px rgba(255,60,200,0.6); color:#ff5edc; }
  .hud-stat .val.cyan { text-shadow:0 0 8px rgba(0,230,255,0.6); color:#00e6ff; }

  .crosshair {
    position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
    width:30px; height:30px; pointer-events:none; opacity:0.35;
  }
  .crosshair::before, .crosshair::after {
    content:''; position:absolute; background:#fff;
  }
  .crosshair::before { width:100%; height:1px; top:50%; left:0; transform:translateY(-50%); }
  .crosshair::after  { height:100%; width:1px; left:50%; top:0; transform:translateX(-50%); }

  .speedline {
    position:absolute; top:0; left:50%; width:1px;
    background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.08), transparent);
    pointer-events:none; z-index:2;
  }

  @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 id="hud">
    <div class="hud-top">

      <div class="hud-top-right">
        SYS.ONLINE<br>
        <span id="clock">00:00:00</span>
      </div>
    </div>

    <div class="crosshair"></div>

    <div class="hud-bottom">
      <div class="hud-stat">
        <div class="label">Velocity</div>
        <div class="val pink" id="vel">— — —</div>
      </div>
      <div class="hud-stat" style="text-align:center;">
        <div class="label">Altitude</div>
        <div class="val cyan" id="alt">— — —</div>
      </div>
      <div class="hud-stat" style="text-align:right;">
        <div class="label">Coordinates</div>
        <div class="val" id="coords">— — —</div>
      </div>
    </div>
  </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;

  let clock = { t: 0 };
  let distTravelled = 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 });
  }

  const elVel    = document.getElementById('vel');
  const elAlt    = document.getElementById('alt');
  const elCoords = document.getElementById('coords');
  const elClock  = document.getElementById('clock');
  let hudTick = 0;

  function updateHUD(dt) {
    hudTick += dt;
    if (hudTick < 0.12) return;
    hudTick = 0;

    distTravelled += SPEED;
    const vel = (SPEED * 60 + Math.sin(clock.t * 2.1) * 40).toFixed(0);
    const alt = (4800 + Math.sin(clock.t * 0.7) * 120).toFixed(0);
    const cx  = (distTravelled * 0.003).toFixed(2);
    const cz  = (Math.sin(clock.t * 0.13) * 9999).toFixed(2);

    elVel.textContent    = vel + ' u/s';
    elAlt.textContent    = alt + ' m';
    elCoords.textContent = cx + ' , ' + cz;

    const now = new Date();
    elClock.textContent = String(now.getHours()).padStart(2,'0') + ':' +
                          String(now.getMinutes()).padStart(2,'0') + ':' +
                          String(now.getSeconds()).padStart(2,'0');
  }

  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;

    updateHUD(dt);
    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>