• 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:
01Critter Models 02
└ Locust└ Butterfly└ Cockroach└ Spider└ Scorpion└ Dragon└ Brazilian Treehopper└ Tardigrade└ Diatoms└ Bumblebee└ Wasp└ Hammerhead Worm└ Giraffe Weevil└ Trilobite Beetle
02VantaJS Shaders
└ Clouds└ Waves└ Cells└ Rings└ Halo└ Globe└ Net└ Dots└ Birds└ Trunk└ Topology└ About VantaJS
03App Bundle 02
└ Gutindex└ Insect Builder└ OpenLayers
04Cathedrals
└ Lattice└ Lost└ Lucent└ Lumen└ Glass└ Disco└ Hyperion└ Latent
05App Bundle 01
└ Robohash└ PrismJS└ Horoscope└ Binary Jazz└ Morse Code└ URL Extractor└ Entity Escaper└ Phonetic Alphabet└ Toki Pona└ Semaphore└ Cut Up Machine└ Text Mixer└ Currency Freaks└ Host IO└ CrUX API└ Logo Dev└ Web Speech API└ GEO DB
06Retroscopic
└ Galaxy└ Ocean└ Slime└ 24-Cell└ Unicorn└ Polyp└ Oneiric 1└ Oneiric 2└ Node└ Torus Knot└ GLSL Grid└ Liquid Crystal└ Silky└ Butterfly└ Ocean 2└ Icosahedron└ Luma└ Swarm└ Broken Solid└ Hypersphere└ Constraints└ Psycho Killer
01 / 03

OCEAN 2 - RETROSCOPIC CRT SHADER SOURCE CODE

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Ocean</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
    canvas { width: 100% !important; height: 100% !important; display: block; }
    #stage {
      position: fixed;
      inset: 0;
      width: 100vw;
      height: 100vh;
      filter: grayscale(1) sepia(1) hue-rotate(80deg) saturate(5) brightness(1.15) contrast(1.2);
      z-index: 2;
    }
    #hint {
      position: fixed;
      bottom: 16px;
      right: 20px;
      font-family: Georgia, serif;
      font-size: 13px;
      color: rgba(255,255,255,0.4);
      letter-spacing: 0.04em;
      pointer-events: none;
      z-index: 3;
    }
    /* Retroscopic Overlay */
    #scope {
        position: fixed;
        inset: 0;
        z-index: 4;
        pointer-events: none;
        display: block;
    }
    #scope>div {
        position: absolute;
        inset: 0;
    }
    #scope .glow {
        background: radial-gradient(circle at 50% 46%, rgba(30, 255, 130, .08), rgba(0, 0, 0, 0) 62%);
        animation: nvflick 3.6s steps(24) infinite;
    }
    #scope .scan {
        background: repeating-linear-gradient(to bottom, rgba(0, 0, 0, 0) 0 2px, rgba(0, 25, 8, .45) 2px 3px);
        animation: nvscan 7s linear infinite;
    }
    #scope .vig {
        box-shadow: inset 0 0 200px 50px rgba(0, 0, 0, .8);
    }
    #scope .mask {
        background: radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0) 54%, rgba(0, 0, 0, .5) 72%, rgba(0, 0, 0, .94) 100%);
    }
    @keyframes nvscan {
        from { background-position: 0 0; }
        to { background-position: 0 240px; }
    }
    @keyframes nvflick {
        0%, 100% { opacity: .55; }
        40% { opacity: .9; }
        62% { opacity: .4; }
        80% { opacity: .75; }
    }
  </style>
</head>
<body>
  <div id="scope">
    <div class="glow"></div>
    <div class="scan"></div>
    <div class="vig"></div>
    <div class="mask"></div>
  </div>
  <div id="stage">
    <canvas id="c"></canvas>
  </div>
  <div id="hint">drag to orbit</div>

  <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(Math.min(window.devicePixelRatio, 2));
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.toneMapping = THREE.ACESFilmicToneMapping;
      renderer.toneMappingExposure = 0.5;
      renderer.outputEncoding = THREE.sRGBEncoding;

      const scene = new THREE.Scene();
      const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 1, 20000);

      // Orbit controls
      let isDragging = false, lastX = 0, lastY = 0;
      let spherical = { theta: 0.3, phi: 1.1, radius: 110 };

      function updateCamera() {
        camera.position.set(
          spherical.radius * Math.sin(spherical.phi) * Math.sin(spherical.theta),
          spherical.radius * Math.cos(spherical.phi),
          spherical.radius * Math.sin(spherical.phi) * Math.cos(spherical.theta)
        );
        camera.position.y = Math.max(camera.position.y, 5);
        camera.lookAt(0, 10, 0);
      }
      updateCamera();

      canvas.addEventListener('mousedown', e => { isDragging = true; lastX = e.clientX; lastY = e.clientY; });
      canvas.addEventListener('touchstart', e => { isDragging = true; lastX = e.touches[0].clientX; lastY = e.touches[0].clientY; });
      window.addEventListener('mouseup', () => isDragging = false);
      window.addEventListener('touchend', () => isDragging = false);
      window.addEventListener('mousemove', e => {
        if (!isDragging) return;
        spherical.theta -= (e.clientX - lastX) * 0.005;
        spherical.phi = Math.max(0.1, Math.min(Math.PI * 0.48, spherical.phi + (e.clientY - lastY) * 0.005));
        lastX = e.clientX; lastY = e.clientY;
        updateCamera();
      });
      window.addEventListener('touchmove', e => {
        if (!isDragging) return;
        spherical.theta -= (e.touches[0].clientX - lastX) * 0.005;
        spherical.phi = Math.max(0.1, Math.min(Math.PI * 0.48, spherical.phi + (e.touches[0].clientY - lastY) * 0.005));
        lastX = e.touches[0].clientX; lastY = e.touches[0].clientY;
        updateCamera();
      });

      // Sky
      const skyMat = new THREE.ShaderMaterial({
        uniforms: { sunDirection: { value: new THREE.Vector3() } },
        vertexShader: `
          varying vec3 vWorldPos;
          void main() {
            vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
          }`,
        fragmentShader: `
          uniform vec3 sunDirection;
          varying vec3 vWorldPos;
          void main() {
            vec3 dir = normalize(vWorldPos);
            float sunDot = clamp(dot(dir, normalize(sunDirection)), 0.0, 1.0);
            float sundisk = pow(sunDot, 256.0) * 2.0;
            float haze = pow(clamp(1.0 - dir.y, 0.0, 1.0), 4.0) * 0.6;
            vec3 skyColor = mix(vec3(0.1, 0.2, 0.5), vec3(0.6, 0.35, 0.2), haze);
            skyColor += vec3(1.0, 0.85, 0.4) * sundisk;
            gl_FragColor = vec4(skyColor, 1.0);
          }`,
        side: THREE.BackSide,
        depthWrite: false
      });
      scene.add(new THREE.Mesh(new THREE.SphereGeometry(8000, 32, 16), skyMat));

      const theta = Math.PI * (0.49 - 0.5);
      const phi   = 2 * Math.PI * (0.205 - 0.5);
      const sunDir = new THREE.Vector3(
        Math.cos(phi),
        Math.sin(phi) * Math.sin(theta),
        Math.sin(phi) * Math.cos(theta)
      );
      skyMat.uniforms.sunDirection.value.copy(sunDir);

      const sunLight = new THREE.DirectionalLight(0xffcc66, 2.5);
      sunLight.position.copy(sunDir).multiplyScalar(100);
      scene.add(sunLight);
      scene.add(new THREE.AmbientLight(0x334466, 0.8));

      // Water — all wave math in the fragment shader to avoid polygon grid artifacts
      const waterGeo = new THREE.PlaneGeometry(10000, 10000, 1, 1);
      waterGeo.rotateX(-Math.PI / 2);

      const waterMat = new THREE.ShaderMaterial({
        uniforms: {
          time:         { value: 0 },
          sunDirection: { value: sunDir },
          sunColor:     { value: new THREE.Color(0xffcc44) },
          waterColor:   { value: new THREE.Color(0x001e0f) },
          cameraPos:    { value: new THREE.Vector3() },
        },
        vertexShader: `
          varying vec3 vWorldPos;
          varying vec2 vUv;
          void main() {
            vUv = uv;
            vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
          }`,
        fragmentShader: `
          uniform float time;
          uniform vec3  sunDirection;
          uniform vec3  sunColor;
          uniform vec3  waterColor;
          uniform vec3  cameraPos;
          varying vec3  vWorldPos;
          varying vec2  vUv;

          float waveH(float x, float z, float t) {
            float y = 0.0;
            y += 1.20 * sin(0.08 * x + 0.90 * t);
            y += 0.70 * sin(0.13 * z + 1.30 * t + 0.50);
            y += 0.50 * sin(0.10 * x + 0.08 * z + 1.70 * t + 1.20);
            y += 0.30 * sin(0.20 * x - 0.14 * z + 2.30 * t + 2.10);
            y += 0.15 * sin(0.40 * z + 3.10 * t + 0.30);
            y += 0.20 * sin(0.30 * x + 0.15 * z - 2.50 * t + 1.80);
            y += 0.08 * sin(0.60 * x + 0.50 * z + 4.00 * t + 0.70);
            y += 0.05 * sin(0.90 * z - 0.40 * x + 3.50 * t + 2.40);
            return y;
          }

          vec3 waveNormal(float x, float z, float t) {
            float eps = 0.5;
            float h  = waveH(x,       z,       t);
            float hx = waveH(x + eps, z,       t);
            float hz = waveH(x,       z + eps, t);
            return normalize(vec3(-(hx - h) / eps, 1.0, -(hz - h) / eps));
          }

          void main() {
            float x = vWorldPos.x;
            float z = vWorldPos.z;

            vec3 N = waveNormal(x, z, time);
            vec3 V = normalize(cameraPos - vWorldPos);
            vec3 L = normalize(sunDirection);

            float fresnel = pow(1.0 - max(dot(N, V), 0.0), 4.0) * 0.85 + 0.04;
            float spec    = pow(max(dot(reflect(-L, N), V), 0.0), 512.0) * 3.0;

            vec3 shallowColor = vec3(0.0, 0.25, 0.35);
            float depth = clamp(N.y, 0.0, 1.0);
            vec3 waterC = mix(waterColor, shallowColor, depth * 0.4);
            vec3 skyRefl = mix(vec3(0.55, 0.32, 0.12), vec3(0.1, 0.2, 0.5), clamp(N.y * 1.5, 0.0, 1.0));

            vec3 col = mix(waterC, skyRefl, fresnel);
            col += sunColor * spec;

            // Subtle foam on wave crests
            float waveTop = clamp((N.y - 0.85) * 8.0, 0.0, 1.0);
            col = mix(col, vec3(1.0), waveTop * 0.18);

            gl_FragColor = vec4(col, 0.95);
          }`,
        transparent: true,
        side: THREE.FrontSide,
      });

      scene.add(new THREE.Mesh(waterGeo, waterMat));

      function animate() {
        requestAnimationFrame(animate);
        const t = performance.now() * 0.001;
        waterMat.uniforms.time.value = t;
        waterMat.uniforms.cameraPos.value.copy(camera.position);
        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.