• 01 - Critter Models 02
  • 02 - VantaJS Shaders
  • 03 - App Bundle 02
  • 04 - Cathedrals
  • 05 - App Bundle 01
  • 06 - Retroscopic
  • 07 - Games
  • 08 - Voronoi
  • 09 - Terrain
  • 10 - Critter Models 01
  • 11 - Chromatophores
  • 12 - Conformal Maps
  • 13 - Echo Logix
  • 14 - Music
  • 15 - Colour Theory
MIKE'S NOTEBOOK
Notes
Number:
Project:
07Games
└ Rockfall└ Speak & Math└ Space Invaders└ Apache└ Asteroids└ Molly Roger└ Carrom└ Chess└ Connect 4└ Anagrams
08Voronoi
└ 001└ 002└ 003└ 004└ 005
09Terrain
└ Simple Polygon└ Simple Flyover└ Infinite Flyover 01└ Infinite Flyover 02└ Hexagonal Plane└ Mountain Ridges
10Critter Models 01
└ Wireframe Spider└ Wireframe Fly└ Wireframe Scorpion└ Wireframe Mosquito└ Wireframe Bee└ Wireframe Butterfly└ Wireframe Locust
11Chromatophores
└ 01└ 02└ 03└ 04
12Conformal Maps
└ 01└ 02└ 03└ 04└ 05
02 / 03

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>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.