• 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

HEXAGONAL PLANE - 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 — Enhanced 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 {
    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(){
  const PLANE_SIZE   = 40000;
  const SEGMENTS     = 120;
  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 createHexagonalGrid(size, hexRadius, color, opacity) {
    const hexHeight = hexRadius * Math.sqrt(3);
    const hexWidth = hexRadius * 2;
    const horizontalSpacing = hexWidth * 0.75;
    const verticalSpacing = hexHeight;

    const group = new THREE.Group();

    const cols = Math.ceil(size / horizontalSpacing / 2);
    const rows = Math.ceil(size / verticalSpacing / 2);

    for (let row = -rows/2; row < rows/2; row++) {
      for (let col = -cols/2; col < cols/2; col++) {
        const x = col * horizontalSpacing;
        const y = row * verticalSpacing + (col % 2 === 0 ? 0 : verticalSpacing / 2);

        const hexShape = new THREE.Shape();
        for (let i = 0; i < 6; i++) {
          const angle = (Math.PI / 3) * i;
          const px = Math.cos(angle) * hexRadius;
          const py = Math.sin(angle) * hexRadius;
          if (i === 0) hexShape.moveTo(px, py);
          else hexShape.lineTo(px, py);
        }
        hexShape.closePath();

        const hexGeo = new THREE.ShapeGeometry(hexShape);
        const hexMat = new THREE.MeshBasicMaterial({
          color,
          transparent: true,
          opacity: opacity * 0.3,
          side: THREE.DoubleSide
        });
        const hexMesh = new THREE.Mesh(hexGeo, hexMat);
        hexMesh.position.set(x, y, 0);
        hexMesh.userData.baseX = x;
        hexMesh.userData.baseY = y;

        const edgesGeo = new THREE.EdgesGeometry(hexGeo);
        const edgesMat = new THREE.LineBasicMaterial({ color, opacity, transparent: true });
        const edges = new THREE.LineSegments(edgesGeo, edgesMat);
        hexMesh.add(edges);

        group.add(hexMesh);
      }
    }

    return group;
  }

  function makePlane(color, opacity, y) {
    const hexGrid = createHexagonalGrid(PLANE_SIZE, 400, color, opacity);
    hexGrid.rotation.x = -Math.PI / 2;
    hexGrid.position.set(0, y, 0);

    hexGrid.userData.hexagons = [];
    hexGrid.traverse((child) => {
      if (child.isMesh && child.geometry.type === 'ShapeGeometry') {
        hexGrid.userData.hexagons.push(child);
      }
    });

    return hexGrid;
  }

  const plainLayers = [
    { color: 0xff00ff, opacity: 1.0,  y: 0,     wave: 1.0,  name: 'main' },
    { color: 0x00ffff, opacity: 0.4,  y: -800,  wave: 0.7,  name: 'cyan' },
    { color: 0xff6600, opacity: 0.35, y: -1600, wave: 0.5,  name: 'orange' },
    { color: 0xffff00, opacity: 0.3,  y: 600,   wave: 0.6,  name: 'yellow' }
  ];

  const groundPlanes = [];

  plainLayers.forEach(layer => {
    for (let i = 0; i < 3; i++) {
      const p = makePlane(layer.color, layer.opacity, layer.y);
      p.position.z = i * PLANE_SIZE;

      p.userData.layer = layer.name;
      p.userData.waveIntensity = layer.wave;
      p.userData.baseColor = new THREE.Color(layer.color);
      p.userData.baseY = layer.y;

      scene.add(p);
      groundPlanes.push(p);
    }
  });

  const mountainLayers = [
    { color: 0x1a0030, z: -18000, scale: 1.0 },
    { color: 0x2a0050, z: -16000, scale: 0.8 },
    { color: 0x3a0070, z: -14000, scale: 0.6 }
  ];

  mountainLayers.forEach(mLayer => {
    const mtnGeo = new THREE.PlaneGeometry(PLANE_SIZE * 2, 8000 * mLayer.scale, 160, 1);
    const mtnMat = new THREE.MeshBasicMaterial({ color: mLayer.color, transparent: true, opacity: 0.7 });
    const mtn = new THREE.Mesh(mtnGeo, mtnMat);
    mtn.rotation.x = -Math.PI / 2;
    mtn.position.set(0, -200, mLayer.z);
    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);
      const h = Math.sin(nx * 3.1) * 2200 * mLayer.scale
              + Math.sin(nx * 7.3) * 1200 * mLayer.scale
              + Math.sin(nx * 15.7) * 600 * mLayer.scale
              + Math.sin(nx * 31) * 300 * mLayer.scale;
      mtnPos.setZ(i, Math.max(h, -500));
    }
    mtnPos.needsUpdate = true;

    const mtnWireGeo = mtnGeo.clone();
    const wireColor = new THREE.Color(mLayer.color).multiplyScalar(2);
    const mtnWire = new THREE.Mesh(mtnWireGeo, new THREE.MeshBasicMaterial({
      color: wireColor, wireframe: true, transparent: true, opacity: 0.3
    }));
    mtnWire.rotation.x = -Math.PI / 2;
    mtnWire.position.copy(mtn.position);
    mtnWire.userData = { baseZ: mLayer.z };
    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;
    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);
    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 = [];
  const structureColors = [0x00ffff, 0xff00ff, 0xff6600, 0xffff00, 0x9933ff];

  function makeStructure(x, z) {
    const color = structureColors[Math.floor(Math.random() * structureColors.length)];
    const g = new THREE.BoxGeometry(
      200 + Math.random() * 300,
      800 + Math.random() * 2200,
      200 + Math.random() * 300
    );
    const m = new THREE.MeshBasicMaterial({ color, 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 glowColor = new THREE.Color(color).offsetHSL(0.5, 0, 0);
    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: glowColor, transparent: true, opacity: 0.12 });
    mesh.add(new THREE.Mesh(ig, im));

    structures.push({ mesh, baseZ: z, baseColor: new THREE.Color(color) });
  }

  for (let i = 0; i < 12; i++) {
    const side = (i % 2 === 0) ? 1 : -1;
    const x = side * (3500 + Math.random() * 5000);
    const z = i * (PLANE_SIZE / 12);
    makeStructure(x, z);
  }

  const PARTICLE_COUNT = 800;
  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;

    const r = Math.random();
    if (r < 0.25)      { partCol[i*3]=1; partCol[i*3+1]=0.2; partCol[i*3+2]=0.8; }
    else if (r < 0.5)  { partCol[i*3]=0; partCol[i*3+1]=0.9; partCol[i*3+2]=1; }
    else if (r < 0.7)  { partCol[i*3]=1; partCol[i*3+1]=1; partCol[i*3+2]=0; }
    else if (r < 0.85) { partCol[i*3]=1; partCol[i*3+1]=0.4; partCol[i*3+2]=0; }
    else               { partCol[i*3]=0.6; partCol[i*3+1]=0.2; partCol[i*3+2]=1; }
  }

  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 flyingObjects = [];
  const objectShapes = ['sphere', 'cube', 'pyramid', 'torus'];

  function createFlyingObject() {
    let mesh;
    const shape = objectShapes[Math.floor(Math.random() * objectShapes.length)];
    const color = structureColors[Math.floor(Math.random() * structureColors.length)];

    switch(shape) {
      case 'sphere':
        mesh = new THREE.Mesh(
          new THREE.SphereGeometry(150 + Math.random() * 200, 16, 16),
          new THREE.MeshBasicMaterial({ color, wireframe: true, transparent: true, opacity: 0.6 })
        );
        break;
      case 'cube':
        mesh = new THREE.Mesh(
          new THREE.BoxGeometry(200 + Math.random() * 300, 200 + Math.random() * 300, 200 + Math.random() * 300),
          new THREE.MeshBasicMaterial({ color, wireframe: true, transparent: true, opacity: 0.6 })
        );
        break;
      case 'pyramid':
        mesh = new THREE.Mesh(
          new THREE.ConeGeometry(150 + Math.random() * 200, 300 + Math.random() * 400, 4),
          new THREE.MeshBasicMaterial({ color, wireframe: true, transparent: true, opacity: 0.6 })
        );
        break;
      case 'torus':
        mesh = new THREE.Mesh(
          new THREE.TorusGeometry(150 + Math.random() * 100, 40 + Math.random() * 60, 16, 32),
          new THREE.MeshBasicMaterial({ color, wireframe: true, transparent: true, opacity: 0.6 })
        );
        break;
    }

    const side = Math.random() > 0.5 ? 1 : -1;
    mesh.position.set(
      side * (8000 + Math.random() * 6000),
      1000 + Math.random() * 4000,
      -5000 + Math.random() * 30000
    );

    scene.add(mesh);
    flyingObjects.push({
      mesh,
      baseColor: new THREE.Color(color),
      speed: 0.3 + Math.random() * 0.8,
      rotSpeed: { x: (Math.random() - 0.5) * 0.02, y: (Math.random() - 0.5) * 0.02, z: (Math.random() - 0.5) * 0.02 }
    });
  }

  for (let i = 0; i < 8; i++) createFlyingObject();

  const laserBeams = [];

  function createLaserBeam(x, z) {
    const colors = [0xff00ff, 0x00ffff, 0xffff00, 0xff6600];
    const color = colors[Math.floor(Math.random() * colors.length)];

    const geo = new THREE.CylinderGeometry(20, 20, 8000, 8);
    const mat = new THREE.MeshBasicMaterial({
      color,
      transparent: true,
      opacity: 0.15,
      side: THREE.DoubleSide
    });
    const beam = new THREE.Mesh(geo, mat);
    beam.position.set(x, 4000, z);
    scene.add(beam);

    const coreGeo = new THREE.CylinderGeometry(8, 8, 8000, 8);
    const coreMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.4 });
    const core = new THREE.Mesh(coreGeo, coreMat);
    beam.add(core);

    laserBeams.push({
      mesh: beam,
      baseColor: new THREE.Color(color),
      baseZ: z,
      phase: Math.random() * Math.PI * 2
    });
  }

  for (let i = 0; i < 6; i++) {
    const side = i % 2 === 0 ? 1 : -1;
    const x = side * (4000 + Math.random() * 8000);
    const z = i * (PLANE_SIZE / 6);
    createLaserBeam(x, z);
  }

  const energyOrbs = [];

  for (let i = 0; i < 10; i++) {
    const colors = [0xff00ff, 0x00ffff, 0xffff00, 0xff6600, 0x9933ff];
    const color = colors[Math.floor(Math.random() * colors.length)];

    const geo = new THREE.SphereGeometry(80 + Math.random() * 120, 16, 16);
    const mat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.5 });
    const orb = new THREE.Mesh(geo, mat);

    orb.position.set(
      (Math.random() - 0.5) * 15000,
      300 + Math.random() * 600,
      i * (PLANE_SIZE / 10)
    );

    scene.add(orb);
    energyOrbs.push({
      mesh: orb,
      baseColor: new THREE.Color(color),
      baseY: orb.position.y,
      baseZ: orb.position.z,
      floatPhase: Math.random() * Math.PI * 2
    });
  }

  const rings = [];
  const RING_COUNT = 4;
  const ringColors = [0xff00ff, 0x00ffff, 0xffff00, 0xff6600];

  for (let i = 0; i < RING_COUNT; i++) {
    const rGeo = new THREE.RingGeometry(200, 250, 64);
    const color = ringColors[i % ringColors.length];
    const rMat = new THREE.MeshBasicMaterial({ color, 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, baseColor: new THREE.Color(color) });
  }

  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 * (p.userData.waveIntensity * 0.5 + 0.5);
      if (p.position.z < -PLANE_SIZE) p.position.z += PLANE_SIZE * 3;

      const waveInt = p.userData.waveIntensity;
      const hexagons = p.userData.hexagons;

      hexagons.forEach((hex) => {
        const x = hex.userData.baseX;
        const y = hex.userData.baseY;

        const wave = Math.sin(x * 0.0004 + clock.t * waveInt) * WAVE_AMP * waveInt;
        hex.position.z = wave;
      });

      const hueShift = 0.02 * Math.sin(clock.t * 0.3);
      const newColor = p.userData.baseColor.clone();
      newColor.offsetHSL(hueShift, 0, 0);

      hexagons.forEach(hex => {
        hex.material.color.copy(newColor);
        if (hex.children[0]) {
          hex.children[0].material.color.copy(newColor);
        }
      });
    });

    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 pulse = 0.9 + Math.sin(clock.t * 2 + s.mesh.position.x * 0.001) * 0.1;
      const pulsedColor = s.baseColor.clone().multiplyScalar(pulse);
      s.mesh.material.color.copy(pulsedColor);
    });

    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);
      }
      pPos.setY(i, pPos.getY(i) + Math.sin(clock.t * 2 + i) * 0.4);
      pPos.setZ(i, z);
    }
    pPos.needsUpdate = true;

    flyingObjects.forEach(obj => {
      obj.mesh.position.z -= SPEED * obj.speed;
      if (obj.mesh.position.z < -10000) {
        obj.mesh.position.z = 35000;
        const side = Math.random() > 0.5 ? 1 : -1;
        obj.mesh.position.x = side * (8000 + Math.random() * 6000);
        obj.mesh.position.y = 1000 + Math.random() * 4000;
      }

      obj.mesh.rotation.x += obj.rotSpeed.x;
      obj.mesh.rotation.y += obj.rotSpeed.y;
      obj.mesh.rotation.z += obj.rotSpeed.z;

      const pulse = 0.85 + Math.sin(clock.t * 3 + obj.mesh.position.x * 0.001) * 0.15;
      const pulsedColor = obj.baseColor.clone().multiplyScalar(pulse);
      obj.mesh.material.color.copy(pulsedColor);
    });

    laserBeams.forEach(laser => {
      laser.mesh.position.z -= SPEED * 0.3;
      if (laser.mesh.position.z < -PLANE_SIZE * 0.5) {
        laser.mesh.position.z += PLANE_SIZE;
      }

      const pulseIntensity = 0.15 + Math.sin(clock.t * 4 + laser.phase) * 0.1;
      laser.mesh.material.opacity = pulseIntensity;
      laser.mesh.children[0].material.opacity = pulseIntensity * 2;

      const scale = 1 + Math.sin(clock.t * 2 + laser.phase) * 0.2;
      laser.mesh.scale.set(scale, 1, scale);
    });

    energyOrbs.forEach(orb => {
      orb.mesh.position.z -= SPEED * 0.4;
      if (orb.mesh.position.z < -PLANE_SIZE * 0.5) {
        orb.mesh.position.z += PLANE_SIZE;
        orb.mesh.position.x = (Math.random() - 0.5) * 15000;
      }

      orb.mesh.position.y = orb.baseY + Math.sin(clock.t * 1.5 + orb.floatPhase) * 200;

      orb.mesh.rotation.y += dt * 0.5;

      const glowPulse = 0.5 + Math.sin(clock.t * 5 + orb.floatPhase) * 0.3;
      orb.mesh.material.opacity = glowPulse;

      const colorShift = orb.baseColor.clone();
      colorShift.offsetHSL(Math.sin(clock.t * 0.5) * 0.05, 0, 0);
      orb.mesh.material.color.copy(colorShift);
    });

    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);
      const alpha = 1 - Math.abs(progress - 0.5) * 2;
      r.mat.opacity = Math.max(0, alpha * 0.5);

      const shiftedColor = r.baseColor.clone();
      shiftedColor.offsetHSL(Math.sin(clock.t * 0.5) * 0.1, 0, 0);
      r.mat.color.copy(shiftedColor);
    });

    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 glowHue = 0.05 + Math.sin(clock.t * 0.3) * 0.03;
    glowMat.color.setHSL(glowHue, 0.8, 0.6);

    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.