← sketches

starry field

Perlin noise flow field with swirling, layered strokes.

p5.js

  import p5 from 'p5';

  new p5((p: p5) => {
    interface Particle {
      x: number; y: number;
      px: number; py: number;
      hue: number;
      nOff: number;
      age: number;
      maxAge: number;
    }

    let particles: Particle[] = [];

    function spawn(): Particle {
      const x = p.random(p.width);
      const y = p.random(p.height);
      const hue = p.constrain(p.map(p.noise(x * 0.003, y * 0.003, p.frameCount * 0.0005), 0.25, 0.75, 0, 360), 0, 360);
      return { x, y, px: x, py: y, hue, nOff: p.random(3), age: 0, maxAge: Math.floor(p.random(c.length * 0.6, c.length * 1.4)) };
    }

    function initParticles() {
      particles = [];
      const count = c.lines;
      const cols = Math.ceil(Math.sqrt(count * (p.width / p.height)));
      const rows = Math.ceil(count / cols);
      const cellW = p.width / cols;
      const cellH = p.height / rows;
      for (let i = 0; i < count; i++) {
        const col = i % cols;
        const row = Math.floor(i / cols);
        const x = col * cellW + p.random(cellW);
        const y = row * cellH + p.random(cellH);
        const hue = p.constrain(p.map(p.noise(x * 0.003, y * 0.003), 0.25, 0.75, 0, 360), 0, 360);
        particles.push({ x, y, px: x, py: y, hue, nOff: p.random(3), age: Math.floor(p.random(c.length)), maxAge: Math.floor(p.random(c.length * 0.6, c.length * 1.4)) });
      }
    }

    const c = controls({
      lines: { label: 'lines', min: 500, max: 5000, value: 2500, step: 100 },
      length: { label: 'length', min: 50, max: 1000, value: 500, step: 10 },
      curl: { label: 'curl', min: 1, max: 30, rawValue: 15, value: 0.15, parse: (r) => parseInt(r) / 100, format: (v) => v.toFixed(2) },
      scale: { label: 'scale', min: 50, max: 600, value: 200 },
      speed: { label: 'speed', min: 1, max: 20, rawValue: 3, value: 0.3, parse: (r) => parseInt(r) / 10, format: (v) => v.toFixed(1) },
      weight: { label: 'weight', min: 5, max: 75, rawValue: 40, value: 4.0, parse: (r) => parseInt(r) / 10, format: (v) => v.toFixed(1) },
    }, { onRestart: () => { p.background(230, 80, 16); initParticles(); },
         onReset: () => { p.background(230, 80, 16); initParticles(); } });

    p.setup = () => {
      const el = document.getElementById('canvas')!.parentElement!;
      const cv = p.createCanvas(el.clientWidth, el.clientHeight);
      cv.parent(el);
      document.getElementById('canvas')?.remove();
      p.colorMode(p.HSB, 360, 100, 100, 100);
      p.background(230, 80, 16);
      initParticles();
    };

    const basePalette = [
      { h: 230, s: 85, b: 30 },  // deep navy
      { h: 220, s: 75, b: 50 },  // royal blue
      { h: 215, s: 65, b: 65 },  // mid blue
      { h: 210, s: 55, b: 75 },  // sky blue
      { h: 200, s: 50, b: 80 },  // light blue
      { h: 195, s: 45, b: 75 },  // cyan-blue
      { h: 235, s: 70, b: 45 },  // slate blue
      { h: 205, s: 60, b: 60 },  // steel blue
      { h: 190, s: 40, b: 70 },  // teal
      { h: 50,  s: 80, b: 90 },  // warm yellow
      { h: 55,  s: 65, b: 80 },  // soft yellow
    ];

    p.draw = () => {
      const t = p.frameCount * 0.001;
      const noiseScale = 1 / c.scale;

      for (let i = 0; i < particles.length; i++) {
        const pt = particles[i];

        // Smooth flow noise with slight per-particle divergence
        const nx = pt.x * noiseScale;
        const ny = pt.y * noiseScale;
        const n1 = p.noise(nx, ny, t);
        const n2 = p.noise(nx * 2 + 50, ny * 2 + 50, t * 1.2);
        const base = (n1 + n2 * 0.3) * Math.PI * 2 * (1.5 + c.curl * 12);
        const angle = base + pt.nOff * 0.15;

        pt.px = pt.x;
        pt.py = pt.y;
        pt.x += Math.cos(angle) * c.speed;
        pt.y += Math.sin(angle) * c.speed;
        pt.age++;

        const palIdx = (pt.hue / 360) * basePalette.length;
        const pA = basePalette[Math.floor(palIdx) % basePalette.length];
        const pB = basePalette[Math.ceil(palIdx) % basePalette.length];
        const mix = palIdx % 1;

        const h = pA.h + (pB.h - pA.h) * mix;
        const s = pA.s + (pB.s - pA.s) * mix;
        const b = pA.b + (pB.b - pA.b) * mix + n1 * 20;

        p.stroke(h, s, Math.min(100, b), 25);
        p.strokeWeight(c.weight * (0.5 + n1));
        p.line(pt.px, pt.py, pt.x, pt.y);

        // Respawn when off screen or lifespan exceeded
        if (pt.age >= pt.maxAge || pt.x < 0 || pt.x > p.width || pt.y < 0 || pt.y > p.height) {
          particles[i] = spawn();
        }
      }
    };
  });