ðŸ”Ū :: Shine On Fish

BamgasiJM·2026년 4ė›” 26ėž

Nannou <Generative Art>

ëŠĐ록 ëģīęļ°
54/55
post-thumbnail

📝 Rust Code

use nannou::prelude::*;
use nannou::noise::{NoiseFn, Perlin, Seedable};

const NUM_PARTICLES: usize = 10_000;
const WIN_W: u32 = 1200;
const WIN_H: u32 = 900;

fn main() {
    nannou::app(model).update(update).run();
}

// ── Data ──────────────────────────────────────────────

struct Particle {
    pos: Vec2,
    vel: Vec2,
    acc: Vec2,
    hue: f32,
    mass: f32,
    trail: Vec<Vec2>,
}

impl Particle {
    fn new(rect: &Rect) -> Self {
        Self {
            pos: vec2(
                random_range(rect.left(), rect.right()),
                random_range(rect.bottom(), rect.top()),
            ),
            vel: Vec2::ZERO,
            acc: Vec2::ZERO,
            hue: random_f32(),
            mass: random_range(0.8, 2.0),
            trail: Vec::new(),
        }
    }

    fn apply_force(&mut self, f: Vec2) {
        self.acc += f / self.mass;
    }

    fn update(&mut self, rect: &Rect, dt: f32) {
        self.vel += self.acc * dt;
        // drag
        self.vel *= 0.96;
        // speed cap
        let max_spd = 300.0;
        if self.vel.length() > max_spd {
            self.vel = self.vel.normalize() * max_spd;
        }
        self.pos += self.vel * dt;
        self.acc = Vec2::ZERO;

        // trail (keep last 20 positions)
        self.trail.push(self.pos);
        if self.trail.len() > 20 {
            self.trail.remove(0);
        }

        // wrap edges
        if self.pos.x < rect.left()   { self.pos.x = rect.right(); self.trail.clear(); }
        if self.pos.x > rect.right()  { self.pos.x = rect.left();  self.trail.clear(); }
        if self.pos.y < rect.bottom() { self.pos.y = rect.top();    self.trail.clear(); }
        if self.pos.y > rect.top()    { self.pos.y = rect.bottom(); self.trail.clear(); }
    }
}

struct Model {
    _window: window::Id,
    particles: Vec<Particle>,
    noise: Perlin,
    noise_z: f64,
    mouse: Vec2,
    mouse_down: bool,
    attractor_pos: Vec2,
}

// ── Setup ─────────────────────────────────────────────

fn model(app: &App) -> Model {
    let _window = app
        .new_window()
        .size(WIN_W, WIN_H)
        .title("Noise Field — Swarm")
        .view(view)
        .mouse_pressed(mouse_pressed)
        .mouse_released(mouse_released)
        .build()
        .unwrap();

    let rect = Rect::from_w_h(WIN_W as f32, WIN_H as f32);
    let particles = (0..NUM_PARTICLES).map(|_| Particle::new(&rect)).collect();
    let noise = Perlin::new().set_seed(42);

    Model {
        _window,
        particles,
        noise,
        noise_z: 0.0,
        mouse: Vec2::ZERO,
        mouse_down: false,
        attractor_pos: vec2(200.0, 150.0),
    }
}

fn mouse_pressed(_app: &App, model: &mut Model, _button: MouseButton) {
    model.mouse_down = true;
}
fn mouse_released(_app: &App, model: &mut Model, _button: MouseButton) {
    model.mouse_down = false;
}

// ── Helpers ───────────────────────────────────────────

/// Gaussian falloff: 1.0 at center → 0.0 far away
fn gaussian(dist: f32, sigma: f32) -> f32 {
    (-0.5 * (dist / sigma).powi(2)).exp()
}

// ── Update ────────────────────────────────────────────

fn update(app: &App, model: &mut Model, _update: Update) {
    let dt = 1.0 / 60.0_f32;
    let rect = app.window_rect();
    model.mouse = app.mouse.position();
    model.noise_z += 0.003;

    // slowly orbit the attractor
    let t = app.time;
    model.attractor_pos = vec2(t.sin() * 250.0, (t * 0.7).cos() * 180.0);

    let noise = &model.noise;
    let nz = model.noise_z;
    let mouse = model.mouse;
    let mouse_down = model.mouse_down;
    let attractor = model.attractor_pos;

    // swarm center of mass (computed once)
    let com: Vec2 = model.particles.iter().map(|p| p.pos).fold(Vec2::ZERO, |a, b| a + b)
        / NUM_PARTICLES as f32;

    // collect positions for repulsion (spatial shortcut: only check neighbors via grid would be
    // ideal, but for 2000 particles brute-force with distance cutoff is fine)
    let positions: Vec<Vec2> = model.particles.iter().map(|p| p.pos).collect();

    for (i, p) in model.particles.iter_mut().enumerate() {
        // ── 1. Noise field ────────────────────────
        let scale = 0.003;
        let nx = noise.get([p.pos.x as f64 * scale, p.pos.y as f64 * scale, nz]) as f32;
        let ny = noise.get([
            p.pos.x as f64 * scale + 100.0,
            p.pos.y as f64 * scale + 100.0,
            nz,
        ]) as f32;
        let noise_force = vec2(nx, ny) * 120.0;
        p.apply_force(noise_force);

        // ── 2. Mouse interaction (Gaussian) ───────
        let to_mouse = mouse - p.pos;
        let dist = to_mouse.length().max(1.0);
        let sigma = 200.0; // influence radius
        let g = gaussian(dist, sigma);

        if mouse_down {
            // attraction toward mouse
            let attract = to_mouse.normalize() * g * 400.0;
            p.apply_force(attract);
        } else {
            // gentle repulsion from mouse
            let repel = -to_mouse.normalize() * g * 250.0;
            p.apply_force(repel);
        }

        // color shift by proximity
        let target_hue = if g > 0.05 {
            map_range(g, 0.05, 1.0, p.hue, 0.0) // shift toward red near cursor
        } else {
            // base hue from noise
            map_range(nx, -1.0, 1.0, 0.45, 0.75) // teal-blue range
        };
        p.hue += (target_hue - p.hue) * 0.05;

        // ── 3. Global attractor ───────────────────
        let to_att = attractor - p.pos;
        let att_dist = to_att.length().max(1.0);
        let att_strength = (80.0 / att_dist).min(1.0) * 60.0;
        p.apply_force(to_att.normalize() * att_strength);

        // ── 4. Swarm cohesion (toward center of mass) ──
        let to_com = com - p.pos;
        p.apply_force(to_com * 0.02);

        // ── 5. Local repulsion (separation) ───────
        let repel_radius = 25.0;
        let mut sep = Vec2::ZERO;
        // sample a few neighbors to keep it fast
        for j in ((i + 1)..NUM_PARTICLES).step_by(8) {
            let diff = p.pos - positions[j];
            let d = diff.length();
            if d > 0.0 && d < repel_radius {
                sep += diff.normalize() / d;
            }
        }
        p.apply_force(sep * 50.0);

        // ── 6. Physics step ───────────────────────
        p.update(&rect, dt);
    }
}

// ── Draw ──────────────────────────────────────────────

fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();

    // semi-transparent background for motion blur
    draw.rect()
        .wh(app.window_rect().wh())
        .color(srgba(0.02, 0.02, 0.05, 0.12));

    let mouse = model.mouse;

    for p in &model.particles {
        let dist_to_mouse = (p.pos - mouse).length();
        let g = gaussian(dist_to_mouse, 200.0);

        // size reacts to mouse proximity
        let size = map_range(g, 0.0, 1.0, 1.5, 5.0) * (1.0 / p.mass);

        // brightness reacts too
        let lum = map_range(g, 0.0, 1.0, 0.4, 0.9);
        let alpha = map_range(g, 0.0, 1.0, 0.5, 1.0);

        // draw trail
        if p.trail.len() > 2 {
            let pts: Vec<(Vec2, Hsla)> = p.trail.iter().enumerate().map(|(ti, &tp)| {
                let t_ratio = ti as f32 / p.trail.len() as f32;
                (tp, hsla(p.hue, 0.6, lum * 0.4, t_ratio * alpha * 0.3))
            }).collect();
            draw.polyline().weight(1.0).points_colored(pts);
        }

        // draw particle
        draw.ellipse()
            .xy(p.pos)
            .w_h(size, size)
            .color(hsla(p.hue, 0.7, lum, alpha));
    }

    // subtle glow at mouse
    for r in (1..=5).rev() {
        let radius = r as f32 * 30.0;
        let a = 0.015 / r as f32;
        draw.ellipse()
            .xy(mouse)
            .w_h(radius, radius)
            .color(hsla(0.6, 0.5, 0.7, a));
    }

    draw.to_frame(app, &frame).unwrap();
}

profile
Coding Art with Blender / oF / Processing / p5.js / nannou

0ę°œė˜ 댓ęļ€