What you are looking at
The trail is not a list of past positions. Nothing here remembers where the pointer has been.
There are two textures. Each frame reads one, dims it slightly, stamps the segment the pointer has just travelled on top, and writes the result to the other. Then they swap. The fading tail is simply what is left of every previous stamp after being multiplied by the decay a few hundred times.
Why a shader and not a 2D canvas
Painting translucent black over a 2D canvas every frame does approximately the same thing, and is the usual way to get this. It has two problems.
The first is that it is a full-surface read-modify-write on the CPU, every frame. The second one you can see: the channels are 8-bit and the operation clamps, so once a pixel reaches the background colour it stops changing, and long tails band into visible steps.
On the GPU the decay is a floating-point multiply across every texel at once, and the tail dissolves to nothing.
The brush is a segment
Between two frames the pointer has moved several pixels. Stamping a disc at its position leaves a dotted line as soon as you move your hand quickly.
float segmentDistance(vec2 p, vec2 a, vec2 b) {
vec2 pa = p - a;
vec2 ba = b - a;
float h = clamp(dot(pa, ba) / max(dot(ba, ba), 0.0001), 0.0, 1.0);
return length(pa - ba * h);
}
The brush is the distance to the whole segment travelled since the previous frame. That is what turns a fast gesture into a continuous stroke.
The tail, in frames
The decay control reads better as a duration: a trail keeps roughly 1 / (1 - decay) frames of history.
decay 0.90 -> 10 frames
decay 0.96 -> 25 frames
decay 0.99 -> 100 frames
That is why the panel shows that number next to the value: 0.96 means nothing on its own, and "25 frames" does.