Chromatic Drift - Perlin Nebula Gas Clouds

📅 April 23, 2026 🏷️ art
worth-a-look nebula perlin-noise flow-field generative-art gas-clouds particle-system color-gradient emergent
Generated by GridFlow AI | Tags: nebula, perlin-noise, flow-field, generative-art, gas-clouds, particle-system, color-gradient, emergent

💡 AI 提示词

Create a mesmerizing Perlin noise-driven nebula gas cloud visualization with particle systems following flow fields, multiple noise layers for depth, HSB color gradients shifting between cosmic blues and purples, ethereal glow effects, and organic emergent movement patterns that feel alive and volumetric.

🔧 核心算法要点

  1. Use 3D Perlin noise with time as the third dimension to create evolving flow fields that drive particle movement
  2. Implement a particle class with position, velocity, and acceleration vectors following noise-derived forces
  3. Create a dual-layer noise system: coarse noise for large-scale flow direction, fine noise for turbulence
  4. Map noise values to HSB color space, transitioning between deep blues (180 hue) and cosmic purples (300 hue)
  5. Add a separate nebula glow layer that renders soft elliptical blobs where noise exceeds threshold (0.55)
  6. Implement particle lifecycle system with fade-in/fade-out based on noise and position for organic death and rebirth
  7. Use semi-transparent background (alpha 8) for motion trails and ethereal persistence
  8. Apply motion blur effect through line rendering from previous to current position

🎨 原始代码

var sketch = function(p) {
    let particles = [];
    let numParticles = 800;
    let noiseScale = 0.0008;
    let timeScale = 0.0003;
    let t = 0;
    let cols, rows;
    let resolution = 20;
    
    p.setup = function() {
        let container = document.getElementById('p5-wrapper');
        p.createCanvas(container.offsetWidth, container.offsetHeight).parent(container);
        p.colorMode(p.HSB, 360, 100, 100, 100);
        p.background(0, 0, 5);
        
        cols = p.ceil(p.width / resolution) + 1;
        rows = p.ceil(p.height / resolution) + 1;
        
        for (let i = 0; i < numParticles; i++) {
            particles.push(new Particle(p.random(p.width), p.random(p.height)));
        }
    };
    
    p.draw = function() {
        p.background(0, 0, 5, 8);
        
        t += timeScale;
        
        for (let particle of particles) {
            particle.update(t);
            particle.display();
        }
        
        drawFlowField();
        addNebulaGlow();
    };
    
    function drawFlowField() {
        p.noiseDetail(4, 0.5);
        
        for (let x = 0; x < cols; x++) {
            for (let y = 0; y < rows; y++) {
                let noiseVal = p.noise(x * noiseScale, y * noiseScale, t);
                let noiseVal2 = p.noise(x * noiseScale * 2, y * noiseScale * 2, t * 1.5 + 100);
                
                let angle = noiseVal * p.TWO_PI * 3;
                angle += (noiseVal2 - 0.5) * 0.5;
                
                let vx = p.cos(angle) * 2;
                let vy = p.sin(angle) * 2;
                
                let px = x * resolution;
                let py = y * resolution;
                
                let hue = p.map(noiseVal, 0, 1, 180, 300);
                let sat = p.map(noiseVal2, 0, 1, 40, 80);
                let bri = p.map(noiseVal, 0, 1, 10, 30);
                
                p.stroke(hue, sat, bri, 15);
                p.strokeWeight(0.5);
                p.line(px, py, px + vx * 5, py + vy * 5);
            }
        }
    }
    
    function addNebulaGlow() {
        p.noiseDetail(3, 0.6);
        let scale = 0.001;
        
        for (let i = 0; i < 3; i++) {
            p.loadPixels();
            let d = p.pixelDensity();
            let step = 4;
            
            for (let x = 0; x < p.width; x += step) {
                for (let y = 0; y < p.height; y += step) {
                    let noiseVal = p.noise(x * scale + i * 50, y * scale + i * 50, t + i * 20);
                    
                    if (noiseVal > 0.55) {
                        let hue = p.map(noiseVal, 0.55, 1, 200, 280);
                        let sat = p.map(noiseVal, 0.55, 1, 30, 60);
                        let bri = p.map(noiseVal, 0.55, 1, 5, 20);
                        
                        p.fill(hue, sat, bri, 12);
                        p.noStroke();
                        p.ellipse(x, y, step * 3, step * 3);
                    }
                }
            }
        }
    }
    
    class Particle {
        constructor(x, y) {
            this.pos = p.createVector(x, y);
            this.vel = p.createVector(0, 0);
            this.acc = p.createVector(0, 0);
            this.maxSpeed = 1.5;
            this.prevPos = this.pos.copy();
            this.life = p.random(100, 300);
            this.maxLife = this.life;
            this.baseHue = p.random(180, 300);
        }
        
        update(time) {
            this.prevPos = this.pos.copy();
            
            let noiseVal = p.noise(this.pos.x * noiseScale, this.pos.y * noiseScale, time);
            let angle = noiseVal * p.TWO_PI * 4;
            
            let force = p5.Vector.fromAngle(angle);
            force.mult(0.1);
            
            this.acc.add(force);
            this.vel.add(this.acc);
            this.vel.limit(this.maxSpeed);
            this.pos.add(this.vel);
            this.acc.mult(0);
            
            this.life -= 0.5;
            
            if (this.life < 0 || this.isOutOfBounds()) {
                this.pos.x = p.random(p.width);
                this.pos.y = p.random(p.height);
                this.life = p.random(100, 300);
                this.maxLife = this.life;
                this.vel.mult(0);
            }
        }
        
        isOutOfBounds() {
            return this.pos.x < -10 || this.pos.x > p.width + 10 || 
                   this.pos.y < -10 || this.pos.y > p.height + 10;
        }
        
        display() {
            let lifeRatio = this.life / this.maxLife;
            let hue = this.baseHue + p.map(lifeRatio, 0, 1, 0, 30);
            let sat = p.map(lifeRatio, 0, 1, 20, 60);
            let bri = p.map(lifeRatio, 0, 1, 40, 90);
            let alpha = p.map(lifeRatio, 0, 1, 20, 60);
            
            p.stroke(hue % 360, sat, bri, alpha);
            p.strokeWeight(p.map(lifeRatio, 0, 1, 0.5, 2));
            p.line(this.prevPos.x, this.prevPos.y, this.pos.x, this.pos.y);
        }
    }
    
    p.windowResized = function() {
        let container = document.getElementById('p5-wrapper');
        p.resizeCanvas(container.offsetWidth, container.offsetHeight);
        cols = p.ceil(p.width / resolution) + 1;
        rows = p.ceil(p.height / resolution) + 1;
    };
};
// p5 init stripped

✨ AI 艺术解读

This piece captures the infinite, breathing nature of cosmic gas clouds through layered Perlin noise fields. The particles dance through invisible currents, their trails revealing the hidden structure of space itself. The color palette shifts from warm nebula pinks to cold cosmic blues, suggesting the vast temperature gradients found in stellar nurseries. Each viewing reveals different emergent patterns as the noise evolves, never repeating yet always harmonious. The slow, hypnotic movement invites contemplation of our place within these cosmic phenomena.

📝 补充说明

  • Adjust noiseDetail() parameters (octaves and falloff) to control flow field complexity - higher octaves create more turbulent motion
  • The particle lifecycle system prevents clustering and ensures even distribution across the canvas over time
  • Combine multiple noise frequencies (scale * 1, scale * 2) to achieve both macro flow patterns and fine detail
  • Use extremely low background alpha (5-15) for ghostly trail effects that build up over time
  • Resize event recalculates grid dimensions to maintain visual density across different viewport sizes