1// Cursor trail shader that creates a hexagonal trailing effect
2//
3// The effect works by:
4// 1. Constructing a hexagon from current and previous cursor positions
5// 2. Using signed distance fields (SDF) for smooth antialiased rendering
6// 3. Animating the trailing corner with easing for fluid motion
7// 4. Enhancing color saturation for visual impact
8
9// Process each edge: compute distance and determine if point is inside
10void processEdge(vec2 p, vec2 a, vec2 b, inout float minDist, inout float inside) {
11 vec2 edge = b - a;
12 vec2 pa = p - a;
13 float lenSq = dot(edge, edge);
14 float invLenSq = 1.0 / lenSq;
15
16 float t = clamp(dot(pa, edge) * invLenSq, 0.0, 1.0);
17 vec2 diff = pa - edge * t;
18 minDist = min(minDist, dot(diff, diff));
19
20 float cross = edge.x * pa.y - edge.y * pa.x;
21 inside = min(inside, step(0.0, cross));
22}
23
24// Signed distance field for hexagon (negative inside, positive outside)
25// Vertices must be in counter-clockwise order
26float sdHexagon(in vec2 p, in vec2 v0, in vec2 v1, in vec2 v2, in vec2 v3, in vec2 v4, in vec2 v5) {
27 float minDist = 1e20;
28 float inside = 1.0;
29
30 processEdge(p, v0, v1, minDist, inside);
31 processEdge(p, v1, v2, minDist, inside);
32 processEdge(p, v2, v3, minDist, inside);
33 processEdge(p, v3, v4, minDist, inside);
34 processEdge(p, v4, v5, minDist, inside);
35 processEdge(p, v5, v0, minDist, inside);
36
37 float dist = sqrt(max(minDist, 0.0));
38 return mix(dist, -dist, inside);
39}
40
41// Signed distance field for rectangle (negative inside, positive outside)
42float sdRectangle(in vec2 p, in vec2 center, in vec2 halfSize) {
43 vec2 d = abs(p - center) - halfSize;
44 return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
45}
46
47// Represents cursor as a quad with four corners
48struct Quad {
49 vec2 topLeft;
50 vec2 topRight;
51 vec2 bottomLeft;
52 vec2 bottomRight;
53};
54
55// Construct quad from top-left position and size
56Quad getQuad(vec2 pos, vec2 size) {
57 Quad q;
58 q.topLeft = pos;
59 q.topRight = pos + vec2(size.x, 0.0);
60 q.bottomLeft = pos - vec2(0.0, size.y);
61 q.bottomRight = pos + vec2(size.x, -size.y);
62 return q;
63}
64
65// Select 3 corners from quad based on movement direction
66// sel.x: 0=left, 1=right | sel.y: 0=top, 1=bottom
67// Returns corners in counter-clockwise order for hexagon construction
68void selectTrailCorners(Quad q, vec2 sel, out vec2 p1, out vec2 p2, out vec2 p3) {
69 p1 = mix(mix(q.topRight, q.topLeft, sel.x),
70 mix(q.bottomRight, q.bottomLeft, sel.x),
71 sel.y);
72
73 p2 = mix(mix(q.topLeft, q.bottomLeft, sel.x),
74 mix(q.topRight, q.bottomRight, sel.x),
75 sel.y);
76 p3 = mix(mix(q.bottomRight, q.topRight, sel.x),
77 mix(q.bottomLeft, q.topLeft, sel.x),
78 sel.y);
79}
80
81// Select 4 corners from quad based on movement direction
82// sel.x: 0=left, 1=right | sel.y: 0=top, 1=bottom
83// Returns corners in counter-clockwise order for hexagon construction
84void selectCorners(Quad q, vec2 sel, out vec2 p1, out vec2 p2, out vec2 p3, out vec2 p4) {
85 selectTrailCorners(q, sel, p1, p2, p3);
86
87 p4 = mix(mix(q.bottomLeft, q.bottomRight, sel.x),
88 mix(q.topLeft, q.topRight, sel.x),
89 sel.y);
90}
91
92// Cubic ease-out function for smooth animation (expects clamped input)
93float easeClamped(float x) {
94 float t = 1.0 - x;
95 return 1.0 - t * t * t;
96}
97
98// Trail animation duration in seconds
99const float DURATION = 0.25;
100
101void mainImage(out vec4 fragColor, in vec2 fragCoord) {
102 // Calculate animation progress with easing
103 float baseProgress = clamp((iTime - iTimeCursorChange) / DURATION, 0.0, 1.0);
104
105 vec2 uv = fragCoord / iResolution.xy;
106 vec4 background = texture(iChannel0, uv);
107
108 // Skip further work when animation is complete
109 if (baseProgress >= 1.0) {
110 fragColor = background;
111 return;
112 }
113
114 fragColor = background;
115
116 // Precompute reused values
117 float invResY = 1.0 / iResolution.y;
118 float scale = 2.0 * invResY;
119 float aaWidth = scale;
120 vec2 normOffset = iResolution.xy * invResY;
121
122 // Normalize cursor positions and sizes to screen-independent coordinates
123 vec2 currentPos = iCurrentCursor.xy * scale - normOffset;
124 vec2 previousPos = iPreviousCursor.xy * scale - normOffset;
125 vec2 currentSize = iCurrentCursor.zw * scale;
126 vec2 previousSize = iPreviousCursor.zw * scale;
127
128 // Determine movement direction and construct cursor quads
129 vec2 deltaPos = currentPos - previousPos;
130 Quad currentCursor = getQuad(currentPos, currentSize);
131 Quad previousCursor = getQuad(previousPos, previousSize);
132 vec2 selector = step(vec2(0.0), deltaPos);
133
134 // Select corners based on movement direction
135 vec2 currP1, currP2, currP3, currP4;
136 vec2 prevP1, prevP2, prevP3;
137 selectCorners(currentCursor, selector, currP1, currP2, currP3, currP4);
138 selectTrailCorners(previousCursor, selector, prevP1, prevP2, prevP3);
139
140 float easedProgress = easeClamped(baseProgress);
141 float stretchedProgress = min(baseProgress * 2.0, 1.0);
142 float easedProgressDouble = easeClamped(stretchedProgress);
143
144 // Create trailing effect by moving diagonal point slower
145 vec2 trailP1 = mix(prevP1, currP1, easedProgress);
146 vec2 trailP2 = mix(prevP2, currP2, easedProgressDouble);
147 vec2 trailP3 = mix(prevP3, currP3, easedProgressDouble);
148
149 // Compute hexagon SDF and convert to alpha with antialiasing
150 vec2 normCoord = fragCoord * scale - normOffset;
151 float sdfHex = sdHexagon(normCoord, trailP1, trailP2, currP2, currP4, currP3, trailP3);
152 float alpha = 1.0 - smoothstep(-aaWidth, aaWidth, sdfHex);
153
154 // Compute current cursor SDF
155 vec2 halfCurrentSize = currentSize * 0.5;
156 vec2 currentCenter = currentPos + vec2(halfCurrentSize.x, -halfCurrentSize.y);
157 float sdfCurrentCursor = sdRectangle(normCoord, currentCenter, halfCurrentSize);
158
159 // Enhance color saturation for more vibrant trail effect
160 float gray = dot(iCurrentCursorColor.rgb, vec3(0.299, 0.587, 0.114));
161 const float saturationBoost = 1.8;
162 vec4 enhancedColor = clamp(
163 mix(vec4(vec3(gray), iCurrentCursorColor.a), iCurrentCursorColor, saturationBoost),
164 0.0, 1.0
165 );
166
167 // Blend trail color with background
168 vec4 originalColor = fragColor;
169 fragColor.rgb = mix(fragColor.rgb, enhancedColor.rgb, alpha);
170
171 // Remove trail where it overlaps with current cursor
172 fragColor.rgb = mix(fragColor.rgb, originalColor.rgb, step(sdfCurrentCursor, 0.0));
173}