|
1 | | -float ease(float x) { |
2 | | - return pow(1.0 - x, 10.0); |
3 | | -} |
4 | | - |
5 | | -float sdBox(in vec2 p, in vec2 xy, in vec2 b) |
| 1 | +// Cursor Blaze |
| 2 | +// |
| 3 | +// A coronal cursor treatment that complements Stellar Dynamics: |
| 4 | +// - a shrinking white-blue cursor comet |
| 5 | +// - photospheric-white sparks on cursor movement |
| 6 | +// - a cool corona aura and restrained refraction |
| 7 | +// - faint scanlines and a vignette |
| 8 | +// |
| 9 | +// Ghostty exposes the terminal as iChannel0 and supplies cursor uniforms. |
| 10 | + |
| 11 | +#define PI 3.14159265358979323846 |
| 12 | + |
| 13 | +// These are shared in spirit with quiet-star.glsl: a cool white corona |
| 14 | +// surrounding a slightly warmer stellar photosphere. |
| 15 | +const vec3 DEEP_NAVY = vec3(0.012, 0.027, 0.058); |
| 16 | +const vec3 CORONAL_BLUE = vec3(0.320, 0.600, 0.940); |
| 17 | +const vec3 CORONA_WHITE = vec3(0.720, 0.850, 1.000); |
| 18 | +const vec3 PHOTOSPHERE_WHITE = vec3(1.000, 0.940, 0.700); |
| 19 | + |
| 20 | +float saturate(float value) |
6 | 21 | { |
7 | | - vec2 d = abs(p - xy) - b; |
8 | | - return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0); |
| 22 | + return clamp(value, 0.0, 1.0); |
9 | 23 | } |
10 | 24 |
|
11 | | -float getSdfRectangle(in vec2 p, in vec2 xy, in vec2 b) |
| 25 | +float hash11(float value) |
12 | 26 | { |
13 | | - vec2 d = abs(p - xy) - b; |
14 | | - return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0); |
| 27 | + value = fract(value * 0.1031); |
| 28 | + value *= value + 33.33; |
| 29 | + value *= value + value; |
| 30 | + return fract(value); |
15 | 31 | } |
16 | | -// Based on Inigo Quilez's 2D distance functions article: https://iquilezles.org/articles/distfunctions2d/ |
17 | | -// Potencially optimized by eliminating conditionals and loops to enhance performance and reduce branching |
18 | | -float seg(in vec2 p, in vec2 a, in vec2 b, inout float s, float d) { |
19 | | - vec2 e = b - a; |
20 | | - vec2 w = p - a; |
21 | | - vec2 proj = a + e * clamp(dot(w, e) / dot(e, e), 0.0, 1.0); |
22 | | - float segd = dot(p - proj, p - proj); |
23 | | - d = min(d, segd); |
24 | | - |
25 | | - float c0 = step(0.0, p.y - a.y); |
26 | | - float c1 = 1.0 - step(0.0, p.y - b.y); |
27 | | - float c2 = 1.0 - step(0.0, e.x * w.y - e.y * w.x); |
28 | | - float allCond = c0 * c1 * c2; |
29 | | - float noneCond = (1.0 - c0) * (1.0 - c1) * (1.0 - c2); |
30 | | - float flip = mix(1.0, -1.0, step(0.5, allCond + noneCond)); |
31 | | - s *= flip; |
32 | | - return d; |
33 | | -} |
34 | | - |
35 | | -float getSdfParallelogram(in vec2 p, in vec2 v0, in vec2 v1, in vec2 v2, in vec2 v3) { |
36 | | - float s = 1.0; |
37 | | - float d = dot(p - v0, p - v0); |
38 | 32 |
|
39 | | - d = seg(p, v0, v3, s, d); |
40 | | - d = seg(p, v1, v0, s, d); |
41 | | - d = seg(p, v2, v1, s, d); |
42 | | - d = seg(p, v3, v2, s, d); |
43 | | - |
44 | | - return s * sqrt(d); |
| 33 | +float smootherstep(float value) |
| 34 | +{ |
| 35 | + value = saturate(value); |
| 36 | + return value * value * value * (value * (value * 6.0 - 15.0) + 10.0); |
45 | 37 | } |
46 | 38 |
|
47 | | -vec2 normalize(vec2 value, float isPosition) { |
48 | | - return (value * 2.0 - (iResolution.xy * isPosition)) / iResolution.y; |
| 39 | +vec2 cursorCenter(vec4 cursor) |
| 40 | +{ |
| 41 | + return vec2(cursor.x + cursor.z * 0.5, cursor.y - cursor.w * 0.5); |
49 | 42 | } |
50 | 43 |
|
51 | | -float blend(float t) |
| 44 | +float segmentDistance(vec2 point, vec2 start, vec2 end, out float along) |
52 | 45 | { |
53 | | - float sqr = t * t; |
54 | | - return sqr / (2.0 * (sqr - t) + 1.0); |
| 46 | + vec2 segment = end - start; |
| 47 | + along = saturate(dot(point - start, segment) / max(dot(segment, segment), 0.0001)); |
| 48 | + return length(point - (start + segment * along)); |
55 | 49 | } |
56 | 50 |
|
57 | | -float antialising(float distance) { |
58 | | - return 1. - smoothstep(0., normalize(vec2(2., 2.), 0.).x, distance); |
| 51 | +float roundedBoxDistance(vec2 point, vec2 center, vec2 halfSize, float radius) |
| 52 | +{ |
| 53 | + vec2 distanceToEdge = abs(point - center) - halfSize + radius; |
| 54 | + return length(max(distanceToEdge, 0.0)) |
| 55 | + + min(max(distanceToEdge.x, distanceToEdge.y), 0.0) |
| 56 | + - radius; |
59 | 57 | } |
60 | 58 |
|
61 | | -float determineStartVertexFactor(vec2 a, vec2 b) { |
62 | | - // Conditions using step |
63 | | - float condition1 = step(b.x, a.x) * step(a.y, b.y); // a.x < b.x && a.y > b.y |
64 | | - float condition2 = step(a.x, b.x) * step(b.y, a.y); // a.x > b.x && a.y < b.y |
65 | | - |
66 | | - // If neither condition is met, return 1 (else case) |
67 | | - return 1.0 - max(condition1, condition2); |
68 | | -} |
69 | | -vec2 getRectangleCenter(vec4 rectangle) { |
70 | | - return vec2(rectangle.x + (rectangle.z / 2.), rectangle.y - (rectangle.w / 2.)); |
| 59 | +vec3 terminalEmission(vec2 uv, vec2 offset, vec3 background) |
| 60 | +{ |
| 61 | + vec3 sampleColor = texture(iChannel0, clamp(uv + offset, 0.0, 1.0)).rgb; |
| 62 | + float ink = smoothstep(0.08, 0.70, length(sampleColor - background)); |
| 63 | + return max(sampleColor - background, 0.0) * ink; |
71 | 64 | } |
72 | 65 |
|
73 | | -const vec4 TRAIL_COLOR = vec4(0.2, 0.8, 1.0, 1.0); // neon blue |
74 | | -const vec4 CURRENT_CURSOR_COLOR = TRAIL_COLOR; |
75 | | -const vec4 PREVIOUS_CURSOR_COLOR = TRAIL_COLOR; |
76 | | -const vec4 TRAIL_COLOR_ACCENT = vec4(0.0, 0.4, 1.0, 1.0); // deeper blue |
77 | | -const float DURATION = .5; |
78 | | -const float OPACITY = .2; |
79 | | - |
80 | 66 | void mainImage(out vec4 fragColor, in vec2 fragCoord) |
81 | 67 | { |
82 | | - #if !defined(WEB) |
83 | | - fragColor = texture(iChannel0, fragCoord.xy / iResolution.xy); |
84 | | - #endif |
85 | | - //Normalization for fragCoord to a space of -1 to 1; |
86 | | - vec2 vu = normalize(fragCoord, 1.); |
87 | | - vec2 offsetFactor = vec2(-.5, 0.5); |
88 | | - |
89 | | - //Normalization for cursor position and size; |
90 | | - //cursor xy has the postion in a space of -1 to 1; |
91 | | - //zw has the width and height |
92 | | - vec4 currentCursor = vec4(normalize(iCurrentCursor.xy, 1.), normalize(iCurrentCursor.zw, 0.)); |
93 | | - vec4 previousCursor = vec4(normalize(iPreviousCursor.xy, 1.), normalize(iPreviousCursor.zw, 0.)); |
94 | | - |
95 | | - //When drawing a parellelogram between cursors for the trail i need to determine where to start at the top-left or top-right vertex of the cursor |
96 | | - float vertexFactor = determineStartVertexFactor(currentCursor.xy, previousCursor.xy); |
97 | | - float invertedVertexFactor = 1.0 - vertexFactor; |
98 | | - |
99 | | - //Set every vertex of my parellogram |
100 | | - vec2 v0 = vec2(currentCursor.x + currentCursor.z * vertexFactor, currentCursor.y - currentCursor.w); |
101 | | - vec2 v1 = vec2(currentCursor.x + currentCursor.z * invertedVertexFactor, currentCursor.y); |
102 | | - vec2 v2 = vec2(previousCursor.x + currentCursor.z * invertedVertexFactor, previousCursor.y); |
103 | | - vec2 v3 = vec2(previousCursor.x + currentCursor.z * vertexFactor, previousCursor.y - previousCursor.w); |
104 | | - |
105 | | - vec4 newColor = vec4(fragColor); |
106 | | - |
107 | | - float progress = blend(clamp((iTime - iTimeCursorChange) / DURATION, 0.0, 1)); |
108 | | - float easedProgress = ease(progress); |
109 | | - |
110 | | - //Distance between cursors determine the total length of the parallelogram; |
111 | | - vec2 centerCC = getRectangleCenter(currentCursor); |
112 | | - vec2 centerCP = getRectangleCenter(previousCursor); |
113 | | - float lineLength = distance(centerCC, centerCP); |
114 | | - float distanceToEnd = distance(vu.xy, centerCC); |
115 | | - float alphaModifier = distanceToEnd / (lineLength * (easedProgress)); |
116 | | - |
117 | | - if (alphaModifier > 1.0) { // this change fixed it for me. |
118 | | - alphaModifier = 1.0; |
| 68 | + vec2 resolution = iResolution.xy; |
| 69 | + vec2 uv = fragCoord / resolution; |
| 70 | + vec2 pixel = 1.0 / resolution; |
| 71 | + vec4 source = texture(iChannel0, uv); |
| 72 | + vec3 color = source.rgb; |
| 73 | + |
| 74 | + vec2 current = cursorCenter(iCurrentCursor); |
| 75 | + vec2 previous = cursorCenter(iPreviousCursor); |
| 76 | + vec2 movement = current - previous; |
| 77 | + float movementLength = length(movement); |
| 78 | + float cellSize = max(max(iCurrentCursor.z, iCurrentCursor.w), 1.0); |
| 79 | + float cursorAge = max(iTime - iTimeCursorChange, 0.0); |
| 80 | + |
| 81 | + // A subtle phosphor bloom around bright terminal glyphs. Keeping this |
| 82 | + // small preserves sharp text while still giving colours a soft halo. |
| 83 | + vec2 bloomNear = pixel * 1.5; |
| 84 | + vec2 bloomFar = pixel * 3.5; |
| 85 | + vec3 bloom = vec3(0.0); |
| 86 | + bloom += terminalEmission(uv, vec2(bloomNear.x, 0.0), iBackgroundColor); |
| 87 | + bloom += terminalEmission(uv, vec2(-bloomNear.x, 0.0), iBackgroundColor); |
| 88 | + bloom += terminalEmission(uv, vec2(0.0, bloomNear.y), iBackgroundColor); |
| 89 | + bloom += terminalEmission(uv, vec2(0.0, -bloomNear.y), iBackgroundColor); |
| 90 | + bloom += terminalEmission(uv, vec2(bloomFar.x, bloomFar.y), iBackgroundColor); |
| 91 | + bloom += terminalEmission(uv, vec2(-bloomFar.x, bloomFar.y), iBackgroundColor); |
| 92 | + bloom += terminalEmission(uv, vec2(bloomFar.x, -bloomFar.y), iBackgroundColor); |
| 93 | + bloom += terminalEmission(uv, vec2(-bloomFar.x, -bloomFar.y), iBackgroundColor); |
| 94 | + color += bloom * 0.018; |
| 95 | + |
| 96 | + // Pull the old end of the trail towards the new cursor. Short movements |
| 97 | + // disappear quickly, while longer jumps linger just enough to stay |
| 98 | + // readable. |
| 99 | + float movementCells = movementLength / cellSize; |
| 100 | + float trailLifetime = mix(0.30, 0.52, smoothstep(0.5, 10.0, movementCells)); |
| 101 | + float trailProgress = smootherstep(cursorAge / trailLifetime); |
| 102 | + float trailAlive = 1.0 - smoothstep(trailLifetime * 0.58, trailLifetime, cursorAge); |
| 103 | + vec2 trailStart = mix(previous, current, trailProgress); |
| 104 | + float along; |
| 105 | + float trailDistance = segmentDistance(fragCoord, trailStart, current, along); |
| 106 | + float trailWidth = cellSize * mix(0.38, 0.16, trailProgress); |
| 107 | + |
| 108 | + // Taper and dim the old end so the trail reads as a comet rather than a |
| 109 | + // uniformly thick beam. |
| 110 | + float trailTaper = mix(0.28, 1.0, smootherstep(along)); |
| 111 | + float localTrailWidth = trailWidth * trailTaper; |
| 112 | + float tailFade = mix(0.20, 1.0, smootherstep(along)); |
| 113 | + float trailCore = 1.0 |
| 114 | + - smoothstep(localTrailWidth * 0.12, localTrailWidth * 0.55, trailDistance); |
| 115 | + float trailBeam = 1.0 |
| 116 | + - smoothstep(localTrailWidth * 0.45, localTrailWidth * 1.50, trailDistance); |
| 117 | + float trailAura = 1.0 |
| 118 | + - smoothstep(localTrailWidth, localTrailWidth * 4.5, trailDistance); |
| 119 | + float shimmer = 0.90 + 0.10 * sin(along * 18.0 - iTime * 9.0); |
| 120 | + vec3 trailColor = mix(CORONAL_BLUE, CORONA_WHITE, smootherstep(along)); |
| 121 | + trailColor = mix( |
| 122 | + trailColor, |
| 123 | + PHOTOSPHERE_WHITE, |
| 124 | + 0.18 * sin(along * PI) |
| 125 | + ); |
| 126 | + color += trailColor |
| 127 | + * (trailCore * 0.78 + trailBeam * 0.25 + trailAura * 0.10) |
| 128 | + * trailAlive |
| 129 | + * tailFade |
| 130 | + * shimmer; |
| 131 | + |
| 132 | + // A few deterministic particles split gently away from the beam. The |
| 133 | + // cursor-change timestamp acts as a stable random seed for each movement. |
| 134 | + vec2 direction = movement / max(movementLength, 0.0001); |
| 135 | + vec2 normal = vec2(-direction.y, direction.x); |
| 136 | + float movementGate = smoothstep(cellSize * 0.20, cellSize * 0.90, movementLength); |
| 137 | + float sparkLife = 1.0 - smoothstep(0.12, 0.48, cursorAge); |
| 138 | + float eventSeed = floor(iTimeCursorChange * 120.0); |
| 139 | + for (int index = 0; index < 5; index++) { |
| 140 | + float id = float(index); |
| 141 | + float seedA = hash11(eventSeed + id * 17.17); |
| 142 | + float seedB = hash11(eventSeed + id * 41.73 + 9.2); |
| 143 | + float seedC = hash11(eventSeed + id * 73.91 + 2.8); |
| 144 | + vec2 sparkPosition = mix(previous, current, seedA); |
| 145 | + sparkPosition += normal * (seedB - 0.5) * cursorAge * (28.0 + 52.0 * seedC); |
| 146 | + sparkPosition -= direction * cursorAge * (8.0 + 22.0 * seedA); |
| 147 | + float sparkRadius = mix(2.2, 0.45, saturate(cursorAge / 0.48)); |
| 148 | + float spark = 1.0 - smoothstep(sparkRadius * 0.25, sparkRadius, distance(fragCoord, sparkPosition)); |
| 149 | + float twinkle = 0.78 + 0.22 * sin(iTime * 18.0 + id * 3.1); |
| 150 | + color += mix(CORONA_WHITE, PHOTOSPHERE_WHITE, seedB) |
| 151 | + * spark |
| 152 | + * sparkLife |
| 153 | + * movementGate |
| 154 | + * twinkle |
| 155 | + * 0.72; |
119 | 156 | } |
120 | 157 |
|
121 | | - float sdfCursor = getSdfRectangle(vu, currentCursor.xy - (currentCursor.zw * offsetFactor), currentCursor.zw * 0.5); |
122 | | - float sdfTrail = getSdfParallelogram(vu, v0, v1, v2, v3); |
123 | | - |
124 | | - newColor = mix(newColor, TRAIL_COLOR_ACCENT, 1.0 - smoothstep(sdfTrail, -0.01, 0.001)); |
125 | | - newColor = mix(newColor, TRAIL_COLOR, antialising(sdfTrail)); |
126 | | - |
127 | | - newColor = mix(fragColor, newColor, 1.0 - alphaModifier); |
128 | | - fragColor = mix(newColor, fragColor, step(sdfCursor, 0)); |
129 | | - |
| 158 | + // An always-on aura outlines the actual cursor. It follows block, bar, and |
| 159 | + // underline cursors because it uses Ghostty's supplied cursor dimensions. |
| 160 | + vec2 cursorHalfSize = max(iCurrentCursor.zw * 0.5, vec2(0.75)); |
| 161 | + float cursorDistance = roundedBoxDistance( |
| 162 | + fragCoord, |
| 163 | + current, |
| 164 | + cursorHalfSize, |
| 165 | + min(2.5, min(cursorHalfSize.x, cursorHalfSize.y)) |
| 166 | + ); |
| 167 | + float cursorOutside = step(0.0, cursorDistance); |
| 168 | + float cursorAura = (1.0 - smoothstep(0.0, 18.0, cursorDistance)) * cursorOutside; |
| 169 | + float cursorEdge = 1.0 - smoothstep(0.2, 1.8, abs(cursorDistance)); |
| 170 | + float heartbeat = 0.86 + 0.14 * sin(iTime * 4.0); |
| 171 | + color += CORONA_WHITE * cursorAura * 0.11 * heartbeat; |
| 172 | + color += mix(CORONA_WHITE, PHOTOSPHERE_WHITE, 0.45) |
| 173 | + * cursorEdge |
| 174 | + * 0.30; |
| 175 | + |
| 176 | + // Locally split RGB channels near an active trail. The cool coronal |
| 177 | + // fringe stays visible while moving but disappears while reading output. |
| 178 | + float radialDistance = distance(fragCoord, current); |
| 179 | + vec2 radial = (fragCoord - current) / max(radialDistance, 1.0); |
| 180 | + vec2 prismOffset = radial * pixel * 1.35; |
| 181 | + vec3 refracted = color; |
| 182 | + refracted.r = texture(iChannel0, clamp(uv + prismOffset, 0.0, 1.0)).r; |
| 183 | + refracted.b = texture(iChannel0, clamp(uv - prismOffset, 0.0, 1.0)).b; |
| 184 | + float prismMask = trailAlive |
| 185 | + * movementGate |
| 186 | + * (1.0 - smoothstep(20.0, 150.0, radialDistance)) |
| 187 | + * 0.30; |
| 188 | + color = mix(color, refracted + (color - source.rgb), prismMask); |
| 189 | + |
| 190 | + // A nearly invisible energy ripple in background pixels makes the cursor |
| 191 | + // feel luminous without tinting text. |
| 192 | + float backgroundMask = 1.0 - smoothstep(0.025, 0.16, length(source.rgb - iBackgroundColor)); |
| 193 | + float angle = atan(fragCoord.y - current.y, fragCoord.x - current.x); |
| 194 | + float ripple = 0.5 + 0.5 * sin(radialDistance * 0.055 - iTime * 2.2 + angle * 3.0); |
| 195 | + float ambient = (1.0 - smoothstep(20.0, 330.0, radialDistance)) * ripple * backgroundMask; |
| 196 | + color += mix(DEEP_NAVY, CORONAL_BLUE, ripple) * ambient * 0.012; |
| 197 | + |
| 198 | + // Finish with faint moving scanlines and a soft vignette. |
| 199 | + float scanline = 0.5 + 0.5 * sin(fragCoord.y * PI + iTime * 2.0); |
| 200 | + color *= 0.986 + scanline * 0.014; |
| 201 | + vec2 vignetteUv = uv * (1.0 - uv.yx); |
| 202 | + float vignette = pow(saturate(vignetteUv.x * vignetteUv.y * 18.0), 0.10); |
| 203 | + color *= mix(0.90, 1.0, vignette); |
| 204 | + |
| 205 | + fragColor = vec4(clamp(color, 0.0, 1.0), source.a); |
130 | 206 | } |
0 commit comments