Skip to main content
Canvas Flowfield new

Flowfield

Perlin noise vector field driving thousands of particles

Statistics
FPS
0
Particles
0
Noise offset
0.00
Particles
Field

Perlin Noise Flowfield

This demo generates a vector field from Perlin noise and uses it to drive thousands of particles across the canvas.

How It Works

  1. Noise field: A 2D grid of angles computed from 3D Perlin noise (x, y, time)
  2. Particles: Each particle reads the angle at its grid cell and steers accordingly
  3. Trails: A semi-transparent background overlay creates fading trails without storing history
  4. Animation: The noise z-offset advances each frame, making the field evolve over time

Perlin Noise

Perlin noise is a gradient noise function that produces smooth, continuous random values. Unlike white noise, neighboring samples are correlated, creating organic-looking patterns.

noise(x, y, z) = interpolate(dot(gradient, distance))

The 3D variant uses the third axis as a time dimension, allowing the field to evolve smoothly.

Mouse Interaction

The mouse applies a radial force that pushes particles outward from the cursor. This perturbs the otherwise smooth flow, creating visible disturbances that propagate through the particle trails.

© 2013 - 2026 Cylian 🤖 Claude
Instructions Claude

Prompt utilisé pour régénérer cette page :

Page: Flowfield
Description: "Perlin noise vector field driving thousands of particles"
Category: canvas
Icon: wind
Tags: generative, particles, noise
Status: new

Front matter (index.md):
  title: "Flowfield"
  description: "Perlin noise vector field driving thousands of particles"
  icon: "wind"
  tags: ["generative", "particles", "noise"]
  status: ["new"]

HTML structure (index.md):
  <section class="container visual size-800 ratio-1-1 canvas-contain">
    <canvas id="flowfield-canvas"></canvas>
  </section>

Widget files:
- _stats.right.md (weight: 10): div.flowfield-stats with:
  ##### Statistics — <dl> with FPS/Particles/Noise offset
  Stat IDs: stat-fps, stat-particles, stat-noise

- _controls.right.md (weight: 20): Contains controls and options:
  div.flowfield-controls with 3 native <button> elements (not shortcode buttons):
    <button id="btn-play" class="item button is-play"> with {{< icon name="play" >}}
    <button id="btn-pause" class="item button is-pause"> with {{< icon name="pause" >}}
    <button id="btn-reset" class="item button"> with {{< icon name="refresh" >}}
  div.flowfield-options with 2 fieldsets:
    Particles fieldset:
      Density: slider-density (200-3000, step 100, value 1500), display span#display-density
      Speed: slider-speed (0.5-5, step 0.1, value 2.0), display span#display-speed
    Field fieldset:
      Noise scale: slider-noise (0.002-0.05, step 0.001, value 0.01), display span#display-noise-scale (4 decimals)
      Trail opacity: slider-trail (0.01-0.2, step 0.01, value 0.05), display span#display-trail (2 decimals)

- _algorithm.after.md (weight: 90): Explains Perlin noise flowfield, noise→angle→particle steering, trail effect via semi-transparent overlay, mouse radial push interaction.

Architecture (single file default.js):
- IIFE, imports panic from /_lib/panic_v3.js
- Contains inline 3D Perlin noise implementation (no external noise library)

SCSS file (default.scss):
- CSS custom property: --flowfield-color-particle (var(--draw-color-primary))
- .flowfield-controls: flex row, .is-play/.is-pause toggled by .is-running, native button reset styling
- .flowfield-options: flex column with fieldset/legend, .option with label+range, label span monospace+primary color
- .flowfield-stats: dl grid (1fr auto), dt secondary color, dd monospace right-aligned

CONFIG defaults:
  canvasSize: 800, cellSize: 20,
  particleCount: 1500, particleSpeed: 2, particleLifespan: 200,
  noiseScale: 0.01, noiseSpeed: 0.0005,
  trailOpacity: 0.05,
  mouseRadius: 100, mouseForce: 0.5

Perlin noise (inline implementation):
- Permutation table: Uint8Array(512), Fisher-Yates shuffle of 0-255, mirrored for overflow-safe indexing
- grad3: 12 gradient vectors [[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],[0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]]
- initNoise(): shuffles permutation table
- fade(t): improved Perlin fade curve 6t^5 - 15t^4 + 10t^3
- lerp(a, b, t): linear interpolation a + t*(b-a)
- noise3D(x, y, z): full 3D Perlin noise returning values in [-1, 1]
  - Grid cell coordinates via Math.floor & 255 bitmask
  - Fade curves on fractional parts
  - Hash cube corners via permutation table (A, AA, AB, B, BA, BB)
  - Trilinear interpolation of gradient dot products using grad3[perm[hash] % 12]

Particle class:
- Properties: x, y, vx, vy, life, maxLife (= CONFIG.particleLifespan), prevX, prevY
- constructor(width, height): calls reset() for random placement
- reset(width, height): random position, zero velocity, random life (0 to maxLife), prevX/prevY = x/y
- update(width, height):
  1. Store prevX/prevY for trail line rendering
  2. Calculate grid cell: col = floor(x/cellSize), row = floor(y/cellSize)
  3. Read flow angle at index = col + row*cols from flowField array
  4. Apply flow force: vx += cos(angle)*0.5, vy += sin(angle)*0.5
  5. Mouse radial push: if within mouseRadius, push outward proportional to (mouseRadius-dist)/mouseRadius * mouseForce
  6. Normalize velocity magnitude then multiply by speed
  7. Move position: x += vx, y += vy
  8. Decrement life
  9. Reset if dead (life <= 0) or out of bounds

Flow field:
- Grid dimensions: cols × rows = ceil(800/20) = 40×40
- flowField[]: flat array of angles (cols*rows elements)
- updateFlowField(): for each cell, sample noise3D(col*noiseScale*cellSize, row*noiseScale*cellSize, noiseOffset)
- Angle = noise value * PI * 4 (multi-revolution range for varied directions)
- noiseOffset advances by CONFIG.noiseSpeed (0.0005) each frame

Rendering:
- HD canvas: 800px × devicePixelRatio, ctx.scale(dpr, dpr)
- Trail effect: NOT a full clear — semi-transparent background overlay:
  ctx.fillStyle = background, ctx.globalAlpha = trailOpacity, ctx.fillRect(...)
- Particles drawn as short lines from (prevX, prevY) to (x, y)
- Per-particle alpha = (life / maxLife) * 0.8 for gradual fade-out
- Line width 1, particle color from cachedColors.particle
- globalAlpha reset to 1 after particle drawing

Mouse interaction:
- mousemove on canvas: track position in canvas logical coordinates
- mouseleave: set mouse position to -1000, -1000 (far offscreen, disables push)
- No click events needed — continuous mouse tracking only

Animation loop:
- animate(timestamp) via requestAnimationFrame
- FPS counter: frameCount tracked, reset every second
- Each frame: advance noiseOffset → updateFlowField → update all particles → draw → updateStats

Controls:
- start(): set isRunning, record lastFpsTime, start rAF loop
- pause(): set !isRunning, cancel rAF
- reset(): pause, reinitialize noise permutation table (initNoise), recreate particles, full clear canvas (globalAlpha=1), update stats
- updateControlState(): toggles .is-running on .flowfield-controls
- bindSlider(sliderId, displayId, setter, decimals): generic helper
- Density slider: changes particleDensity, calls initParticles() immediately to recreate pool

Color caching:
- cachedColors: background, particle
- cacheColors() reads:
  - background: --background-color-surface (fallback #0a0a0a)
  - particle: --flowfield-color-particle or --draw-color-primary (fallback #5eaadd)
- Refreshed on prefers-color-scheme change

Canvas does NOT auto-start. User must click Play button.
Auto-init on DOM ready (DOMContentLoaded or immediate if already loaded).

Page entièrement générée et maintenue par IA, sans intervention humaine.