Project skyscraper / no man's sky arg

Code with comments (fold it out):
const canvas = document.getElementById('meshCanvas');
const ctx = canvas.getContext('2d', { alpha: true });

let width, height, particles = [];
let mouse = { x: 0, y: 0, active: false };

// --- Particle: a single node in the network ---
class Particle {
  constructor() {
    // Spawn at a random angle around screen center, at radius 160-176px
    const cx = width / 2;
    const cy = height / 2;
    const angle = Math.random() * Math.PI * 2;
    const distance = 160 + Math.random() * 16;

    this.baseX = cx + Math.cos(angle) * distance;
    this.baseY = cy + Math.sin(angle) * distance;
    this.x = this.baseX;
    this.y = this.baseY;
    this.size = Math.random() * 2.6 + 1.5;          // radius: 1.5–4.1px
    this.angle = Math.random() * Math.PI * 2;       // starting orbit phase
    this.orbitSpeed = 0.06 + Math.random() * 0.016; // orbit speed, slightly randomized
  }

  update() {
    // Orbit: rotate around base position (16px wobble)
    this.angle += this.orbitSpeed;
    let tx = this.baseX + Math.cos(this.angle) * 16;
    let ty = this.baseY + Math.sin(this.angle) * 16;

    // Mouse repulsion: if cursor is within 280px, push particle away
    if (mouse.active) {
      const dx = mouse.x - this.x;
      const dy = mouse.y - this.y;
      const dist = Math.hypot(dx, dy);
      if (dist < 280) {
        const force = (1 - dist / 280) * 0.42;  // stronger force when closer
        tx += dx * force;
        ty += dy * force;
      }
    }

    // Smooth interpolation (90% previous + 10% target) for laggy movement
    this.x = this.x * 0.9 + tx * 0.1;
    this.y = this.y * 0.9 + ty * 0.1;
  }

  draw() {
    // Intensity: 1 when cursor is on the node, 0 beyond 260px
    const dist = Math.hypot(this.x - mouse.x, this.y - mouse.y);
    const intensity = Math.max(0, 1 - dist / 260);

    // Red glow when mouse is close, cyan glow otherwise
    if (intensity > 0.25) {
      ctx.fillStyle = '#ff2d2d';
      ctx.shadowBlur = 20;
      ctx.shadowColor = '#ff6666';
    } else {
      ctx.fillStyle = '#00ddff';
      ctx.shadowBlur = 8;
      ctx.shadowColor = '#00ddff';
    }

    ctx.beginPath();
    ctx.arc(this.x, this.y, this.size + intensity * 3.2, 0, Math.PI * 2);
    ctx.fill();
  }
}

// --- Connection lines between nearby particles ---
function connectParticles() {
  ctx.lineWidth = 0.7;
  for (let i = 0; i < particles.length; i++) {
    for (let j = i + 1; j < particles.length; j++) {
      const d = Math.hypot(particles[i].x - particles[j].x, particles[i].y - particles[j].y);
      if (d < 170) {
        // Opacity fades from 1 (touching) to 0 (at 170px apart)
        ctx.strokeStyle = `rgba(0, 221, 255, ${1 - d / 170})`;
        ctx.beginPath();
        ctx.moveTo(particles[i].x, particles[i].y);
        ctx.lineTo(particles[j].x, particles[j].y);
        ctx.stroke();
      }
    }
  }
}

// --- Animation loop ---
function animate() {
  ctx.clearRect(0, 0, width, height);
  particles.forEach(p => { p.update(); p.draw(); });
  connectParticles();
  requestAnimationFrame(animate);  // runs at ~60fps
}

// --- Mouse tracking ---
window.addEventListener('mousemove', (e) => {
  mouse.x = e.clientX;
  mouse.y = e.clientY;
  mouse.active = true;
});
window.addEventListener('mouseleave', () => mouse.active = false);

// --- Initialization ---
function init() {
  width = canvas.width = window.innerWidth;
  height = canvas.height = window.innerHeight;
  particles = [];

  // Particle count scales with day of month: max ~48 on day 28, min 3
  const day = new Date().getDate();
  const count = Math.max(3, Math.round(day * (48 / 28)));
  for (let i = 0; i < count; i++) particles.push(new Particle());

  animate();
}

window.addEventListener('resize', init);
init();

// --- Live Connections polling ---
// Every 15 seconds, re-fetch the page and scrape the "Live Connections"
// counter from the server-rendered HTML. This is how the number updates
// without a full page reload — no WebSocket, no SSE.
setInterval(() => {
  const container = document.getElementById('live-visitors');
  if (!container) return;
  fetch(window.location.href)
    .then(r => r.text())
    .then(html => {
      const temp = document.createElement('div');
      temp.innerHTML = html;
      const newContent = temp.querySelector('#live-visitors');
      if (newContent) container.innerHTML = newContent.innerHTML;
    });
}, 15000);

TL;DR:

  • Nodes orbit in a loose ring around screen center, connected by cyan lines when close.
  • Moving your mouse repels nearby nodes and turns them red.
  • Number of nodes depends on the current day of the month.
  • “Live Connections” counter is just fetch() polling every 15s - nothing real-time.
  • When you grab a new copy via load and a new execution runs, the endpoint returns a value.
  • It runs at z-index: -1 behind all page content - purely decorative.

Also: Project Skyscraper - Frequently Asked Questions
for reference.

2 Likes