← Mike Higgins Home
Custom Code Experiment •

Terrain WF-5

Source Code View HTML / CSS / JavaScript
<!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>
  @import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Rajdhani:wght@300;400;600;700&display=swap');

  * { 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; }

  #hud {
    position:absolute; top:0; left:0; width:100%; height:100%;
    pointer-events:none;
    font-family:'Share Tech Mono', monospace;
    color:rgba(255,255,255,0.85);
  }

  .hud-top {
    position:absolute; top:18px; left:0; width:100%;
    display:flex; justify-content:space-between; align-items:flex-start;
    padding:0 28px;
  }
  .hud-title {
    font-family:'Rajdhani', sans-serif;
    font-weight:700; font-size:26px;
    letter-spacing:8px; text-transform:uppercase;
    color:#fff;
    text-shadow: 0 0 12px rgba(255,60,200,0.7), 0 0 40px rgba(255,60,200,0.3);
  }
  .hud-title span { color: rgba(255,100,220,0.6); font-weight:300; }

  .hud-top-right { text-align:right; font-size:11px; line-height:1.8; opacity:0.7; letter-spacing:2px; }

  #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%);
  }

  .hud-bottom {
    position:absolute; bottom:22px; left:0; width:100%;
    display:flex; justify-content:space-between; align-items:flex-end;
    padding:0 28px;
  }
  .hud-stat { font-size:11px; letter-spacing:2px; line-height:1.9; }
  .hud-stat .label { opacity:0.45; text-transform:uppercase; }
  .hud-stat .val { color:#fff; font-size:14px; }
  .hud-stat .val.pink { text-shadow:0 0 8px rgba(255,60,200,0.6); color:#ff5edc; }
  .hud-stat .val.cyan { text-shadow:0 0 8px rgba(0,230,255,0.6); color:#00e6ff; }

  .crosshair {
    position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
    width:30px; height:30px; pointer-events:none; opacity:0.35;
  }
  .crosshair::before, .crosshair::after {
    content:''; position:absolute; background:#fff;
  }
  .crosshair::before { width:100%; height:1px; top:50%; left:0; transform:translateY(-50%); }
  .crosshair::after  { height:100%; width:1px; left:50%; top:0; transform:translateX(-50%); }

  @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 id="hud">
    <div class="hud-top">
      <div class="hud-top-right">
        SYS.ONLINE<br>
        <span id="clock">00:00:00</span>
      </div>
    </div>

    <div class="crosshair"></div>

    <div class="hud-bottom">
      <div class="hud-stat">
        <div class="label">Velocity</div>
        <div class="val pink" id="vel">— — —</div>
      </div>
      <div class="hud-stat" style="text-align:center;">
        <div class="label">Altitude</div>
        <div class="val cyan" id="alt">— — —</div>
      </div>
      <div class="hud-stat" style="text-align:right;">
        <div class="label">Coordinates</div>
        <div class="val" id="coords">— — —</div>
      </div>
    </div>
  </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;

  let clock = { t: 0 };
  let distTravelled = 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) });
  }

  const elVel = document.getElementById('vel');
  const elAlt = document.getElementById('alt');
  const elCoords = document.getElementById('coords');
  const elClock = document.getElementById('clock');
  let hudTick = 0;

  function updateHUD(dt) {
    hudTick += dt;
    if (hudTick < 0.12) return;
    hudTick = 0;

    distTravelled += SPEED;
    const vel = (SPEED * 60 + Math.sin(clock.t * 2.1) * 40).toFixed(0);
    const alt = (4800 + Math.sin(clock.t * 0.7) * 120).toFixed(0);
    const cx = (distTravelled * 0.003).toFixed(2);
    const cz = (Math.sin(clock.t * 0.13) * 9999).toFixed(2);

    elVel.textContent = vel + ' u/s';
    elAlt.textContent = alt + ' m';
    elCoords.textContent = cx + ' , ' + cz;

    const now = new Date();
    elClock.textContent = String(now.getHours()).padStart(2,'0') + ':' +
                          String(now.getMinutes()).padStart(2,'0') + ':' +
                          String(now.getSeconds()).padStart(2,'0');
  }

  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;

    updateHUD(dt);
    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>