• 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

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