DiamondAtmosphere.Transport.cs
using System;

namespace Diamonds;

public sealed partial class DiamondAtmosphere
{
	void TransportDye( float dt, float retention )
	{
		Array.Clear( nextDye );
		float travel = dt * FlowUnit * FlowSpeed / CellSize;
		for ( int y = 0; y < Rows; y++ )
		for ( int x = 0; x < Columns; x++ )
		{
			int own = y * Columns + x;
			if ( wallDistance[own] < 0 ) continue;
			// Like the menu, each destination samples where its fluid came from.
			float tx = x - velocityX[own] * travel, ty = y - velocityY[own] * travel;
			var point = GridToWorld( x, y );
			float worldX = point.x - velocityX[own] * FlowUnit * FlowSpeed * dt;
			float worldY = point.y - velocityY[own] * FlowUnit * FlowSpeed * dt;
			// Reflect crossings of the playfield walls, not stationary samples in
			// the offscreen reservoir. The top stays open for incoming spawn dye.
			bool side = point.x >= 0 && point.x <= DiamondBoard.BoardWidth && (worldX < 0 || worldX > DiamondBoard.BoardWidth);
			bool bottom = point.y <= DiamondBoard.BoardHeight && worldY > DiamondBoard.BoardHeight;
			if ( side || bottom )
				(tx, ty) = WorldToGrid( side ? ReflectX( worldX ) : worldX, bottom ? ReflectBottom( worldY ) : worldY );
			(tx, ty) = TraceFluid( x, y, tx, ty );
			tx = Math.Clamp( tx, 0, Columns - 1 ); ty = Math.Clamp( ty, 0, Rows - 1 );
			int ix = Math.Min( (int)tx, Columns - 2 ), iy = Math.Min( (int)ty, Rows - 2 );
			float fx = tx - ix, fy = ty - iy;
			int a = iy * Columns + ix, b = a + 1, c = a + Columns, d = c + 1;
			float wa = wallDistance[a] < 0 ? 0 : (1 - fx) * (1 - fy);
			float wb = wallDistance[b] < 0 ? 0 : fx * (1 - fy);
			float wc = wallDistance[c] < 0 ? 0 : (1 - fx) * fy;
			float wd = wallDistance[d] < 0 ? 0 : fx * fy;
			float weight = wa + wb + wc + wd;
			float scale = weight > 0 ? retention / weight : 0;
			wa *= scale; wb *= scale; wc *= scale; wd *= scale;
			// Renormalize the open source samples at solid boundaries, as on the menu.
			// Apply the configured fade locally; do not rescale the whole field.
			for ( int color = 0; color < DiamondBoard.ColorCount; color++ )
			{
				nextDye[own * DiamondBoard.ColorCount + color] =
					dye[a * DiamondBoard.ColorCount + color] * wa +
					dye[b * DiamondBoard.ColorCount + color] * wb +
					dye[c * DiamondBoard.ColorCount + color] * wc +
					dye[d * DiamondBoard.ColorCount + color] * wd;
			}
		}
		if ( !dye.AsSpan().SequenceEqual( nextDye ) ) DyeVersion++;
		Array.Copy( nextDye, dye, dye.Length );
	}
}