Flow field

← back to next-sketch
500 particles drifting through a Perlin-noise vector field — <CanvasSketch /> plus createNoise2D, next-sketch's own engine end to end.
View the full next-sketch (React) source
'use client';

import { useRef } from 'react';
import { CanvasSketch, createNoise2D, randRange } from 'next-sketch';

const PARTICLE_COUNT = 500;

interface Particle {
  x: number;
  y: number;
}

export default function FlowField() {
  const particles = useRef<Particle[]>([]);
  const noise = useRef(createNoise2D(7));
  const t = useRef(0);

  return (
    <CanvasSketch
      style={{ width: '100%', height: 400 }}
      setup={({ ctx, width, height }) => {
        particles.current = Array.from({ length: PARTICLE_COUNT }, () => ({
          x: randRange(0, width),
          y: randRange(0, height),
        }));
        ctx.fillStyle = '#0f172a';
        ctx.fillRect(0, 0, width, height);
      }}
      draw={({ ctx, width, height, dt }) => {
        t.current += dt;

        ctx.fillStyle = 'rgba(15, 23, 42, 0.12)';
        ctx.fillRect(0, 0, width, height);
        ctx.fillStyle = 'rgba(99, 102, 241, 0.7)';

        for (const p of particles.current) {
          const angle = noise.current(p.x * 0.005 + t.current * 0.05, p.y * 0.005) * Math.PI * 4;
          p.x += Math.cos(angle) * 90 * dt;
          p.y += Math.sin(angle) * 90 * dt;
          if (p.x < 0) p.x = width;
          if (p.x > width) p.x = 0;
          if (p.y < 0) p.y = height;
          if (p.y > height) p.y = 0;

          ctx.beginPath();
          ctx.arc(p.x, p.y, 1.4, 0, Math.PI * 2);
          ctx.fill();
        }
      }}
    />
  );
}