<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <script src="https://mrdoob.github.io/omggif-example/omggif.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.125.0/build/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.125.0/examples/js/controls/OrbitControls.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/jwagner/simplex-noise.js@latest/simplex-noise.min.js"></script>
    <style>
      body {
        margin: 0;
        background-color: #fafafa;
        font-family: monospace;
        text-align: center;
      }
      #canvas {
        display: block;
        margin: 0 auto;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script>
      // 1) Seeded Random
      function seededRandomGenerator(seed) {
        let s = seed >>> 0;
        return function () {
          s = (s * 16807) % 2147483647;
          return (s - 1) / 2147483646;
        };
      }

      const seed = 4361459;
      const peakExponent = 1;
      const frequency = 0.02;
      const width = 100;
      const height = 100;
      const colorThreshold = 5; // below => forcibly color base
      const dynamicAttr = 16; // amplitude factor for peaks
      const baseAmplitude = 4.0; // baseline amplitude
      const segments = 30; // number of subdivisions
      const lowerBound = -100; // clamp lower Y
      const baseColor = new THREE.Color(0x032273);
      const peakColor = new THREE.Color(0xd22550);

      const mySeededRandom = seededRandomGenerator(seed);

      // 2) Setup Three.js
      const canvas = document.getElementById("canvas");
      const renderer = new THREE.WebGLRenderer({
        canvas: canvas,
        antialias: true,
      });
      // Remove fixed width/height; use viewport size
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.setPixelRatio(window.devicePixelRatio);

      const scene = new THREE.Scene();
      scene.background = new THREE.Color(0xfafafa);

      // Camera
      const camera = new THREE.PerspectiveCamera(
        27, // initial FOV
        window.innerWidth / window.innerHeight,
        0.1,
        1000
      );
      camera.position.set(0, 50, 170);

      // Controls (locked in this example)
      const controls = new THREE.OrbitControls(camera, renderer.domElement);
      controls.enabled = false;

      // Lights
      const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
      scene.add(ambientLight);

      const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
      directionalLight.position.set(10, 30, 10);
      scene.add(directionalLight);

      // 3) Create Terrain Geometry
      const simplex = new SimplexNoise(mySeededRandom);
      const geometry = new THREE.PlaneGeometry(
        width,
        height,
        segments,
        segments
      );
      geometry.rotateX(-Math.PI / 2);

      const positionAttr = geometry.getAttribute("position");
      const vertexCount = positionAttr.count;

      const colorAttr = new THREE.Float32BufferAttribute(
        new Float32Array(vertexCount * 3),
        3
      );
      geometry.setAttribute("color", colorAttr);

      // Initial noise pass
      for (let i = 0; i < vertexCount; i++) {
        const x = positionAttr.getX(i);
        const y = positionAttr.getY(i);
        const z = positionAttr.getZ(i);

        const rawVal = simplex.noise2D(x * frequency, z * frequency);
        const sharpenedVal =
          Math.sign(rawVal) * Math.pow(Math.abs(rawVal), peakExponent);
        const newY = Math.max(y + sharpenedVal * baseAmplitude, lowerBound);
        positionAttr.setY(i, newY);
      }
      positionAttr.needsUpdate = true;
      geometry.computeBoundingBox();
      geometry.center();

      // Store “centered” positions
      const basePositions = [];
      for (let i = 0; i < vertexCount; i++) {
        basePositions.push({
          x: positionAttr.getX(i),
          y: positionAttr.getY(i),
          z: positionAttr.getZ(i),
        });
      }

      const material = new THREE.MeshBasicMaterial({
        wireframe: true,
        vertexColors: true,
      });
      const terrain = new THREE.Mesh(geometry, material);
      scene.add(terrain);

      // Fit camera to geometry’s bounding sphere
      geometry.computeBoundingSphere();
      const { center, radius } = geometry.boundingSphere;
      camera.position.set(center.x, center.y + radius * 1.2, radius * 3);
      camera.lookAt(center);
      camera.updateProjectionMatrix();

      // 4) Animate
      function animate() {
        requestAnimationFrame(animate);

        const time = performance.now() * 0.001;
        const offset = time * 0.2;
        const dynamicAmplitude =
          baseAmplitude + dynamicAttr * Math.sin(((2 * Math.PI) / 3) * time);

        // Recompute Y
        for (let i = 0; i < vertexCount; i++) {
          const { x, y, z } = basePositions[i];

          const rawVal = simplex.noise2D(
            (x + offset) * frequency,
            (z + offset) * frequency
          );
          const sharpenedVal =
            Math.sign(rawVal) * Math.pow(Math.abs(rawVal), peakExponent);
          const newY = Math.max(
            y + sharpenedVal * dynamicAmplitude,
            lowerBound
          );
          positionAttr.setY(i, newY);
        }
        positionAttr.needsUpdate = true;

        // Color each vertex
        let minY = Infinity;
        let maxY = -Infinity;
        for (let i = 0; i < vertexCount; i++) {
          const yVal = positionAttr.getY(i);
          if (yVal < minY) minY = yVal;
          if (yVal > maxY) maxY = yVal;
        }
        const rangeY = maxY - minY || 1;

        for (let i = 0; i < vertexCount; i++) {
          const currentY = positionAttr.getY(i);
          const t = (currentY - minY) / rangeY;
          const color =
            currentY <= colorThreshold
              ? baseColor
              : new THREE.Color().lerpColors(baseColor, peakColor, t);
          colorAttr.setXYZ(i, color.r, color.g, color.b);
        }
        colorAttr.needsUpdate = true;

        // Optional rotation
        terrain.rotation.y += 0.002;

        controls.update();
        renderer.render(scene, camera);
      }
      animate();

      // 5) Listen for browser resize events
      window.addEventListener("resize", onWindowResize, false);

      function onWindowResize() {
        // Adjust camera
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();

        // Adjust renderer
        renderer.setSize(window.innerWidth, window.innerHeight);
      }
    </script>
  </body>
</html>