DEV Community

Yusuf İhsan Görgel
Yusuf İhsan Görgel

Posted on • Originally published at github.com

Building a Spatial Grid Particle System in Flutter Web

I recently built my developer portfolio entirely in Flutter Web. Instead of using a template, I wanted something with real engineering depth — cinematic backgrounds, interactive particles, and proper architecture.

The most interesting technical challenge was the particle system. Here's how I solved the performance problem.

The Problem: O(n²) Neighbor Lookups

A constellation particle effect draws connecting lines between nearby particles. The naive approach checks every pair:

// O(n²) — kills performance at 50+ particles
for (var i = 0; i < particles.length; i++) {
  for (var j = i + 1; j < particles.length; j++) {
    if (distance(particles[i], particles[j]) < threshold) {
      drawLine(particles[i], particles[j]);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

With 100 particles, that's 4,950 distance checks per frame. At 60fps, it's choppy.

The Solution: Spatial Grid Hashing

Divide the viewport into cells. Each particle registers in its cell. To find neighbors, only check the 9 surrounding cells:

final class _SpatialGrid {
  final double cellSize;
  final Map<int, List<_Particle>> _cells = {};

  int _key(double x, double y) {
    final cx = (x / cellSize).floor();
    final cy = (y / cellSize).floor();
    return cx * 10000 + cy;
  }

  void insert(_Particle p) {
    (_cells[_key(p.x, p.y)] ??= []).add(p);
  }

  List<_Particle> neighbors(double x, double y) {
    final cx = (x / cellSize).floor();
    final cy = (y / cellSize).floor();
    final result = <_Particle>[];
    for (var dx = -1; dx <= 1; dx++) {
      for (var dy = -1; dy <= 1; dy++) {
        final cell = _cells[(cx + dx) * 10000 + (cy + dy)];
        if (cell != null) result.addAll(cell);
      }
    }
    return result;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now each particle only checks ~9 cells × ~5 particles = ~45 checks instead of 100. O(n) instead of O(n²).

The Full System

The particle system has more going on:

  • Cursor repulsion — particles within 200px of the mouse get pushed away with spring physics
  • Connection lines — drawn between particles within 120px, with opacity based on distance
  • Scene-aware — particle density and speed change based on which section you're scrolling through (controlled by a SceneDirector state machine)
  • Lifecycle-aware — animations pause when the browser tab is hidden
  • RepaintBoundary — isolated from the rest of the widget tree to avoid unnecessary repaints

The Cinematic Background Stack

The portfolio layers 7 visual elements:

  1. Dark base color
  2. Animated mesh gradient (Lissajous-curve blob movement with mouse parallax)
  3. Film grain overlay (pre-rasterized 256×256 texture via toImageSync)
  4. Constellation particles (spatial grid system)
  5. Vignette overlay (per-scene intensity)
  6. Scrollable content
  7. Fixed UI (sidebars, scroll dots, cursor)

Each scene (Hero, About, Experience, Projects, Contact) maps to a movie color palette — Blade Runner 2049, Dune, The Matrix, Spider-Verse, and Interstellar. A SceneDirector controller crossfades between them with 200px blend zones.

Architecture

The project uses Clean Architecture with Dart 3.x patterns:

  • abstract interface class for domain contracts
  • final class on all concrete classes
  • switch expressions and case when pattern guards
  • GetX for reactive state (scroll, scene, language, sound controllers)
  • 185 automated tests

Try It

Live demo: developeryusuf.com

GitHub: Yusufihsangorgel/Flutter-Web-Portfolio

It's fully forkable — change your JSON data, photo, and meta tags. All content is data-driven.

Would love feedback, especially on the particle system performance and the scroll-driven scene architecture.


Built with Flutter 3.41, Dart 3.7, zero external UI dependencies.

Top comments (0)