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();
}
}
};
});