Skip to content
SKSuraj Kumar

Generative visual interaction

Glowing Particles

Real-time particle interaction

A reactive particle field running as a GPU simulation, where the whole engineering problem is holding hundreds of thousands of points at a stable frame rate.

  • 3D & Interactive
  • WebGL
  • Shaders
  • Real-time simulation
  • Client project
Role
Frontend engineer. Simulation design, shader work, interaction forces, performance ceiling and the degradation path.
Work type
Client project
Timeline
Paid engagement · 2025
Status
Delivered
Industry
Creative and brand
Client
Gustavo Beltran — USA

Interface built in code for this case study — not a client screen capture.

01Business problem

What was actually going wrong

A visual piece that has to feel alive and respond to the person looking at it, which rules out video and rules out most of what a canvas library can do.

Who feels it

  • Anyone on a phone, where a video hero is the largest thing on the page
  • Visitors who move their pointer and expect the piece to notice
  • People with a reduced-motion preference, usually served nothing at all
  • The client, whose piece has to look distinct rather than stock

Pre-rendered video is the cheap answer to "make it feel alive", and it is immediately identifiable as one. It plays the same way for everybody, it does not respond, and on a phone it is a large download that starts before anyone has decided whether they care.

The alternative most people reach for is a canvas particle library driving a few thousand points from JavaScript. That works, and it caps out early: the position update for every particle runs on the main thread, so the frame rate falls linearly with the particle count, and a few thousand points does not read as a field. It reads as dots.

The interesting count starts around a hundred thousand, and at that point the per-frame arithmetic has to leave the CPU entirely. That is a different kind of build — the simulation state lives on the GPU, the main thread only sends the pointer position, and every decision afterwards is bounded by texture memory and fill rate instead of by JavaScript speed.

02Product overview

What got built

Simulation state stored in floating-point textures and advanced entirely on the GPU, with the CPU doing nothing per frame except handing over the pointer.

Engineering notes

  • Positions and velocities stored in float textures and advanced by a shader, making particle count a resolution parameter with flat main-thread cost.
  • A single instanced draw call for the entire field, with per-point state sampled in the vertex shader.
  • No GPU-to-CPU readback anywhere in the frame, so nothing stalls the pipeline.
  • Render-scale factor decoupled from display density, identified by measuring the blend pass and not the simulation.

Positions and velocities live in textures, not in arrays. Each frame, a fragment shader reads the current state, applies the forces, and writes the next state into a second pair of textures, which are then swapped. The particle count becomes a texture dimension, so going from ten thousand points to two hundred thousand is a resolution change and not a rewrite, and the main thread cost stays flat either way.

Rendering the result uses a single instanced draw call. Every point reads its own position from the state texture in the vertex shader, so there is one geometry upload for the whole field and no per-particle work in JavaScript at any stage.

The forces are deliberately few: a gentle curl noise field for ambient drift, an attraction toward each point’s home position so the field recovers its shape, and a radial force at the pointer. Three forces was not the starting point — earlier versions had six, and the motion read as busy, not alive. Taking forces away improved it; adding more was the instinct I had to argue myself out of.

The additive glow is where the frame budget actually goes. Overlapping translucent points cost fill rate, not arithmetic, so the cost scales with screen area and not with particle count. That meant a resolution ceiling on high-density displays: the simulation renders below native and upscales, which is invisible on a soft glow and is the single change that made it viable on a phone.

Reduced motion is handled by rendering one settled frame of the field; the piece is never removed. The composition is intact and static. Someone who asked not to be moved gets the image, not an empty container.

03Key features

What the software does, feature by feature

8 capabilities, described by what they let someone do rather than by the technology underneath.

  • 01

    GPU-resident simulation state

    Positions and velocities live in floating-point textures, advanced by a shader and swapped each frame. The particle count is a texture dimension, so scaling it costs nothing on the main thread.

  • 02

    One instanced draw call for the whole field

    Every point reads its position from the state texture in the vertex shader. There is no per-particle JavaScript at any point in the frame.

  • 03

    Three forces, chosen by removal

    Ambient curl noise, attraction to a home position, and a radial force at the pointer. Earlier versions had twice as many and the motion read as noise.

  • 04

    Pointer interaction with inertia

    The field is displaced around the pointer and settles back over roughly a second. Without that recovery it would read as a mask following the cursor.

  • 05

    Resolution ceiling on dense displays

    The simulation renders below native resolution and upscales. Imperceptible on a soft additive glow, and the difference between running and not running on a phone.

  • 06

    Adaptive quality from measured frame time

    Sustained slow frames reduce particle count and resolution in steps; the piece never drops to a static image. Degradation is gradual and does not oscillate.

  • 07

    Reduced motion renders a settled frame

    One resolved frame of the field, static. The preference removes the motion, not the composition.

  • 08

    Nothing runs off-screen

    The loop stops when the canvas leaves the viewport or the tab is hidden. A background simulation draining a battery is a bug regardless of how it looks.

04Architecture

How it is put together

The processing path first, then the layers it runs on, then the constraints that shaped both.

Path through the system

5 stages

  1. 01

    Seed

    Home positions generated once into a texture. Every particle keeps its origin, so the field can recover a recognisable shape after being disturbed.

  2. 02

    Simulate

    A fragment shader reads current position and velocity, applies the three forces, and writes to the back buffer. Textures swap; nothing is read back to the CPU.

  3. 03

    Draw

    One instanced draw call. The vertex shader samples the state texture for each instance and the fragment shader applies the additive falloff.

  4. 04

    Composite

    Additive blending at reduced resolution, upscaled. Fill rate is the binding constraint here, not arithmetic.

  5. 05

    Measure

    Frame time sampled over a rolling window. Sustained overruns step quality down; sustained headroom steps it back up, with hysteresis so it cannot flip between levels.

Layers

GPU
State textures with double bufferingSimulation shaderInstanced point renderingAdditive blend pass
CPU
Pointer position uniformFrame-time samplingQuality steppingVisibility handling
Fallbacks
Settled static frame for reduced motionCapability check before context creation

Why it is shaped this way

  • State never returns to the CPU. A single readback per frame stalls the pipeline and undoes the entire reason for the design.
  • Quality stepping uses hysteresis. Without it, a device near the threshold oscillates between levels, which is more noticeable than being on the lower one.
  • Fill rate, not particle count, is the ceiling on glow-heavy rendering. Optimising the wrong one wastes days.
  • The loop is bound to visibility. An animation running behind another tab is invisible and still expensive.

05Technical decisions

The choices that mattered, and what each one cost

Every decision here was contested by a reasonable alternative. The trade-off column is the part usually left out.

01

Hold simulation state in textures rather than in typed arrays

Why

It moves the per-frame arithmetic off the main thread entirely and makes the particle count a resolution parameter. A CPU implementation caps out roughly two orders of magnitude lower.

Trade-off

Debugging is genuinely harder — the state is only inspectable by rendering it — and it depends on float texture support, which needs a capability check and a path for devices without it.

02

Render below native resolution and upscale

Why

On a soft additive glow the difference is imperceptible, and fill rate at native resolution on a high-density phone display is what makes the piece unrunnable.

Trade-off

Any sharp element in the same canvas would be visibly soft, so this composition cannot carry crisp detail. That was accepted before the visual direction was settled, not after.

03

Step quality down instead of switching to a static image

Why

A weaker version of the piece is much better than an abrupt fallback, and it keeps the site coherent on a wide range of hardware.

Trade-off

The lowest quality level is noticeably thinner than the intended look. Worth checking that it still reads as deliberate, which took a couple of passes.

04

Take forces away, do not add them

Why

More forces produced more motion and less legible motion. Cutting to three made the field read as a coherent substance.

Trade-off

Less variety over time, so a visitor who watches for a minute has seen the range of it. Acceptable for a piece nobody watches for a minute.

06Challenges

What was genuinely difficult

Not the setup work. These are the problems where the first implementation was wrong and had to be reconsidered.

01

Frame rate collapsed on a high-density phone display despite plenty of arithmetic headroom.

Approach

Measured passes separately and found the cost in the blend, not the simulation. Introduced a render-scale factor and tuned it against the display density.

Outcome

A stable frame rate at the full particle count. I had spent time optimising the simulation shader, which was never the bottleneck.

02

Pointer interaction felt like a cursor effect, not like disturbing a substance.

Approach

Added velocity-dependent displacement and a slow return toward home positions, so the field carries momentum and recovers gradually instead of snapping back.

Outcome

The interaction reads as physical. The recovery time matters more than the displacement strength, which was the opposite of my initial assumption.

03

Adaptive quality flickered between levels on borderline devices.

Approach

Widened the measurement window and separated the step-down and step-up thresholds so a device cannot satisfy both.

Outcome

Quality settles at a level and stays there. Visible switching had been worse than simply running at the lower setting.

07Business value

What it changes for the business

Stated qualitatively on purpose. Invented percentages are the easiest thing to put on a portfolio and the easiest thing to see through.

Operational effect

  • The piece responds to the visitor, which video cannot do at any bitrate.
  • It runs on a phone at full particle count, so the mobile visitor sees the intended work and not a still.
  • It looks specific to this client instead of like a library demo with the colours changed.
  • A reduced-motion preference still gets the composition, so the accessibility path is not an empty space.
  • Nothing runs off-screen, so the visual cost is bounded to the time someone is actually looking at it.

What would change at scale

  • The simulation is a single field. Multiple interacting fields would need separate state textures and a combining pass, which changes the frame budget substantially.
  • Quality levels are hand-tuned against a small device set. Wider coverage would mean deriving the steps from measured throughput.
  • Float texture support is checked but the no-support path is a static frame. A reduced integer-encoded simulation would serve those devices better and has not been built.

08Interface

The screens where the work happens

Built as a coded interface for this write-up rather than a screen capture, so it stays sharp at any zoom and no client content is republished.

Simulation pipeline

State textures, the swap each frame, and where the pointer uniform enters.

Forces

The three that survived, with the weight each one carries.

Quality ladder

The levels, their thresholds, and the gap between stepping down and stepping back up.

Render scale

Why the lever is screen area rather than particle count.

A diagram I drew from the implementation, showing the components that exist and how they call each other. It is evidence of how the system is put together. It is not a capture of a running system, and it does not by itself establish that the system shipped or was used by anyone.

09Technologies

What it is built with

Chosen for the shape of the problem, not for novelty. Anything unusual is justified in the decisions section above.

Rendering
  • WebGL2
  • GLSL fragment and vertex shaders
  • Instanced rendering
  • Float textures with double buffering
Simulation
  • Curl noise field
  • Home-position attraction
  • Radial pointer force
Frontend
  • TypeScript
  • Visibility-bound render loop
  • Rolling frame-time sampling
Accessibility
  • prefers-reduced-motion settled frame
  • Capability check before context creation

Implementation detail

  • Positions and velocities stored in float textures and advanced by a shader, making particle count a resolution parameter with flat main-thread cost.
  • A single instanced draw call for the entire field, with per-point state sampled in the vertex shader.
  • No GPU-to-CPU readback anywhere in the frame, so nothing stalls the pipeline.
  • Render-scale factor decoupled from display density, identified by measuring the blend pass and not the simulation.
  • Velocity-dependent displacement with a slow return to home positions, giving the interaction weight.
  • Adaptive quality with a rolling window and separated thresholds, so levels cannot oscillate.
  • Force count reduced from six to three; fewer forces produced more legible motion.
  • Render loop bound to canvas visibility and tab state.

10Technical preview

Where the source for this one sits

This build was delivered to a client, so the source belongs to them and is not republished here in any form — not as excerpts and not as a repository link.

Delivered work

A walkthrough instead of a code dump

For private client work, contact me for a walkthrough. On a call I can screen-share the build, go through the component structure, the decisions behind the interaction work and the parts that needed rewriting — the same ground a technical preview covers, without republishing a client's property to do it.

Request a walkthrough

Or email directly: surajk86808@gmail.com

What is not published here

  • The live URL — the deployment belongs to the client, not to this portfolio.
  • The repository, in whole or in excerpt.
  • Client content, contact records, pricing or anything else from the running site.
  • Anything beyond what was agreed: Gustavo Beltran — USA.

Next step

Need something along these lines?

Send the process, the constraints and the deadline. You will get an honest scope, an architecture sketch and a timeline before any commitment.

Email
surajk86808@gmail.com
Based in
Bengaluru, India
Working hours
IST (UTC+5:30)
Availability
Taking new engagements