Skip to content

Commit 8723ecf

Browse files
committed
custom cursor shader
1 parent 33e371d commit 8723ecf

3 files changed

Lines changed: 306 additions & 1 deletion

File tree

ghostty/config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
font-family = "FiraMono Nerd Font Mono"
22
font-style = "Medium"
33
theme = hello_flo
4+
custom-shader = shaders/cursor_wrap.glsl

ghostty/shaders/cursor_wrap.glsl

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
// --- CONFIGURATION ---
2+
vec4 TRAIL_COLOR = iCurrentCursorColor; // can change to eg: vec4(0.2, 0.6, 1.0, 0.5);
3+
const float DURATION = 0.2; // total animation time
4+
const float TRAIL_SIZE = 0.8; // 0.0 = all corners move together. 1.0 = max smear (leading corners jump instantly)
5+
const float THRESHOLD_MIN_DISTANCE = 1.5; // min distance to show trail (units of cursor height)
6+
const float BLUR = 1.0; // blur size in pixels (for antialiasing)
7+
const float TRAIL_THICKNESS = 1.0; // 1.0 = full cursor height, 0.0 = zero height, >1.0 = funky aah
8+
const float TRAIL_THICKNESS_X = 0.9;
9+
10+
const float FADE_ENABLED = 0.0; // 1.0 to enable fade gradient along the trail, 0.0 to disable
11+
const float FADE_EXPONENT = 5.0; // exponent for fade gradient along the trail
12+
13+
// --- CONSTANTS for easing functions ---
14+
const float PI = 3.14159265359;
15+
const float C1_BACK = 1.70158;
16+
const float C2_BACK = C1_BACK * 1.525;
17+
const float C3_BACK = C1_BACK + 1.0;
18+
const float C4_ELASTIC = (2.0 * PI) / 3.0;
19+
const float C5_ELASTIC = (2.0 * PI) / 4.5;
20+
const float SPRING_STIFFNESS = 9.0;
21+
const float SPRING_DAMPING = 0.9;
22+
23+
// --- EASING FUNCTIONS ---
24+
25+
// // Linear
26+
// float ease(float x) {
27+
// return x;
28+
// }
29+
30+
// // EaseOutQuad
31+
// float ease(float x) {
32+
// return 1.0 - (1.0 - x) * (1.0 - x);
33+
// }
34+
35+
// // EaseOutCubic
36+
// float ease(float x) {
37+
// return 1.0 - pow(1.0 - x, 3.0);
38+
// }
39+
40+
// // EaseOutQuart
41+
// float ease(float x) {
42+
// return 1.0 - pow(1.0 - x, 4.0);
43+
// }
44+
45+
// // EaseOutQuint
46+
// float ease(float x) {
47+
// return 1.0 - pow(1.0 - x, 5.0);
48+
// }
49+
50+
// // EaseOutSine
51+
// float ease(float x) {
52+
// return sin((x * PI) / 2.0);
53+
// }
54+
55+
// // EaseOutExpo
56+
// float ease(float x) {
57+
// return x == 1.0 ? 1.0 : 1.0 - pow(2.0, -10.0 * x);
58+
// }
59+
60+
// EaseOutCirc
61+
float ease(float x) {
62+
return sqrt(1.0 - pow(x - 1.0, 2.0));
63+
}
64+
65+
// // EaseOutBack
66+
// float ease(float x) {
67+
// return 1.0 + C3_BACK * pow(x - 1.0, 3.0) + C1_BACK * pow(x - 1.0, 2.0);
68+
// }
69+
70+
// // EaseOutElastic
71+
// float ease(float x) {
72+
// return x == 0.0 ? 0.0
73+
// : x == 1.0 ? 1.0
74+
// : pow(2.0, -10.0 * x) * sin((x * 10.0 - 0.75) * C4_ELASTIC) + 1.0;
75+
// }
76+
77+
// // Parametric Spring
78+
// float ease(float x) {
79+
// x = clamp(x, 0.0, 1.0);
80+
// float decay = exp(-SPRING_DAMPING * SPRING_STIFFNESS * x);
81+
// float freq = sqrt(SPRING_STIFFNESS * (1.0 - SPRING_DAMPING * SPRING_DAMPING));
82+
// float osc = cos(freq * 6.283185 * x) + (SPRING_DAMPING * sqrt(SPRING_STIFFNESS) / freq) * sin(freq * 6.283185 * x);
83+
// return 1.0 - decay * osc;
84+
// }
85+
86+
float getSdfRectangle(in vec2 p, in vec2 xy, in vec2 b)
87+
{
88+
vec2 d = abs(p - xy) - b;
89+
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
90+
}
91+
92+
// Based on Inigo Quilez's 2D distance functions article: https://iquilezles.org/articles/distfunctions2d/
93+
// Potencially optimized by eliminating conditionals and loops to enhance performance and reduce branching
94+
float seg(in vec2 p, in vec2 a, in vec2 b, inout float s, float d) {
95+
vec2 e = b - a;
96+
vec2 w = p - a;
97+
vec2 proj = a + e * clamp(dot(w, e) / dot(e, e), 0.0, 1.0);
98+
float segd = dot(p - proj, p - proj);
99+
d = min(d, segd);
100+
101+
float c0 = step(0.0, p.y - a.y);
102+
float c1 = 1.0 - step(0.0, p.y - b.y);
103+
float c2 = 1.0 - step(0.0, e.x * w.y - e.y * w.x);
104+
float allCond = c0 * c1 * c2;
105+
float noneCond = (1.0 - c0) * (1.0 - c1) * (1.0 - c2);
106+
float flip = mix(1.0, -1.0, step(0.5, allCond + noneCond));
107+
s *= flip;
108+
return d;
109+
}
110+
111+
float getSdfConvexQuad(in vec2 p, in vec2 v1, in vec2 v2, in vec2 v3, in vec2 v4) {
112+
float s = 1.0;
113+
float d = dot(p - v1, p - v1);
114+
115+
d = seg(p, v1, v2, s, d);
116+
d = seg(p, v2, v3, s, d);
117+
d = seg(p, v3, v4, s, d);
118+
d = seg(p, v4, v1, s, d);
119+
120+
return s * sqrt(d);
121+
}
122+
123+
vec2 normalize(vec2 value, float isPosition) {
124+
return (value * 2.0 - (iResolution.xy * isPosition)) / iResolution.y;
125+
}
126+
127+
float antialising(float distance, float blurAmount) {
128+
return 1. - smoothstep(0., normalize(vec2(blurAmount, blurAmount), 0.).x, distance);
129+
}
130+
131+
// Determines animation duration based on a corner's alignment with the move direction(dot product)
132+
// dot_val will be in [-2, 2]
133+
// > 0.5 (1 or 2) = Leading
134+
// > -0.5 (0) = Side
135+
// <= -0.5 (-1 or -2) = Trailing
136+
float getDurationFromDot(float dot_val, float DURATION_LEAD, float DURATION_SIDE, float DURATION_TRAIL) {
137+
float isLead = step(0.5, dot_val);
138+
float isSide = step(-0.5, dot_val) * (1.0 - isLead);
139+
140+
// Start with trailing duration
141+
float duration = mix(DURATION_TRAIL, DURATION_SIDE, isSide);
142+
// Mix in leading duration
143+
duration = mix(duration, DURATION_LEAD, isLead);
144+
return duration;
145+
}
146+
147+
void mainImage(out vec4 fragColor, in vec2 fragCoord){
148+
#if !defined(WEB)
149+
fragColor = texture(iChannel0, fragCoord.xy / iResolution.xy);
150+
#endif
151+
152+
// normalization & setup(-1, 1 coords)
153+
vec2 vu = normalize(fragCoord, 1.);
154+
vec2 offsetFactor = vec2(-.5, 0.5);
155+
156+
vec4 currentCursor = vec4(normalize(iCurrentCursor.xy, 1.), normalize(iCurrentCursor.zw, 0.));
157+
vec4 previousCursor = vec4(normalize(iPreviousCursor.xy, 1.), normalize(iPreviousCursor.zw, 0.));
158+
159+
vec2 centerCC = currentCursor.xy - (currentCursor.zw * offsetFactor);
160+
vec2 halfSizeCC = currentCursor.zw * 0.5;
161+
vec2 centerCP = previousCursor.xy - (previousCursor.zw * offsetFactor);
162+
vec2 halfSizeCP = previousCursor.zw * 0.5;
163+
164+
float sdfCurrentCursor = getSdfRectangle(vu, centerCC, halfSizeCC);
165+
166+
float lineLength = distance(centerCC, centerCP);
167+
float minDist = currentCursor.w * THRESHOLD_MIN_DISTANCE;
168+
169+
vec4 newColor = vec4(fragColor);
170+
171+
float baseProgress = iTime - iTimeCursorChange;
172+
173+
if (lineLength > minDist && baseProgress < DURATION - 0.001) {
174+
// defining corners of cursors
175+
176+
// Y (Height) with TRAIL_THICKNESS
177+
float cc_half_height = currentCursor.w * 0.5;
178+
float cc_center_y = currentCursor.y - cc_half_height;
179+
float cc_new_half_height = cc_half_height * TRAIL_THICKNESS;
180+
float cc_new_top_y = cc_center_y + cc_new_half_height;
181+
float cc_new_bottom_y = cc_center_y - cc_new_half_height;
182+
183+
// X (Width) with TRAIL_THICKNESS
184+
float cc_half_width = currentCursor.z * 0.5;
185+
float cc_center_x = currentCursor.x + cc_half_width;
186+
float cc_new_half_width = cc_half_width * TRAIL_THICKNESS_X;
187+
float cc_new_left_x = cc_center_x - cc_new_half_width;
188+
float cc_new_right_x = cc_center_x + cc_new_half_width;
189+
190+
vec2 cc_tl = vec2(cc_new_left_x, cc_new_top_y);
191+
vec2 cc_tr = vec2(cc_new_right_x, cc_new_top_y);
192+
vec2 cc_bl = vec2(cc_new_left_x, cc_new_bottom_y);
193+
vec2 cc_br = vec2(cc_new_right_x, cc_new_bottom_y);
194+
195+
// same thing for previous cursor
196+
float cp_half_height = previousCursor.w * 0.5;
197+
float cp_center_y = previousCursor.y - cp_half_height;
198+
float cp_new_half_height = cp_half_height * TRAIL_THICKNESS;
199+
float cp_new_top_y = cp_center_y + cp_new_half_height;
200+
float cp_new_bottom_y = cp_center_y - cp_new_half_height;
201+
202+
float cp_half_width = previousCursor.z * 0.5;
203+
float cp_center_x = previousCursor.x + cp_half_width;
204+
float cp_new_half_width = cp_half_width * TRAIL_THICKNESS_X;
205+
float cp_new_left_x = cp_center_x - cp_new_half_width;
206+
float cp_new_right_x = cp_center_x + cp_new_half_width;
207+
208+
vec2 cp_tl = vec2(cp_new_left_x, cp_new_top_y);
209+
vec2 cp_tr = vec2(cp_new_right_x, cp_new_top_y);
210+
vec2 cp_bl = vec2(cp_new_left_x, cp_new_bottom_y);
211+
vec2 cp_br = vec2(cp_new_right_x, cp_new_bottom_y);
212+
213+
// calculating durations for every corner
214+
const float DURATION_TRAIL = DURATION;
215+
const float DURATION_LEAD = DURATION * (1.0 - TRAIL_SIZE);
216+
const float DURATION_SIDE = (DURATION_LEAD + DURATION_TRAIL) / 2.0;
217+
218+
vec2 moveVec = centerCC - centerCP;
219+
vec2 s = sign(moveVec);
220+
221+
// dot products for each corner, determining alignment with movement direction
222+
float dot_tl = dot(vec2(-1., 1.), s);
223+
float dot_tr = dot(vec2( 1., 1.), s);
224+
float dot_bl = dot(vec2(-1.,-1.), s);
225+
float dot_br = dot(vec2( 1.,-1.), s);
226+
227+
// assign durations based on dot products
228+
float dur_tl = getDurationFromDot(dot_tl, DURATION_LEAD, DURATION_SIDE, DURATION_TRAIL);
229+
float dur_tr = getDurationFromDot(dot_tr, DURATION_LEAD, DURATION_SIDE, DURATION_TRAIL);
230+
float dur_bl = getDurationFromDot(dot_bl, DURATION_LEAD, DURATION_SIDE, DURATION_TRAIL);
231+
float dur_br = getDurationFromDot(dot_br, DURATION_LEAD, DURATION_SIDE, DURATION_TRAIL);
232+
233+
// check direction of horizontal movement
234+
float isMovingRight = step(0.5, s.x);
235+
float isMovingLeft = step(0.5, -s.x);
236+
237+
// calculate vertical-rail durations
238+
float dot_right_edge = (dot_tr + dot_br) * 0.5;
239+
float dur_right_rail = getDurationFromDot(dot_right_edge, DURATION_LEAD, DURATION_SIDE, DURATION_TRAIL);
240+
241+
float dot_left_edge = (dot_tl + dot_bl) * 0.5;
242+
float dur_left_rail = getDurationFromDot(dot_left_edge, DURATION_LEAD, DURATION_SIDE, DURATION_TRAIL);
243+
244+
float final_dur_tl = mix(dur_tl, dur_left_rail, isMovingLeft);
245+
float final_dur_bl = mix(dur_bl, dur_left_rail, isMovingLeft);
246+
247+
float final_dur_tr = mix(dur_tr, dur_right_rail, isMovingRight);
248+
float final_dur_br = mix(dur_br, dur_right_rail, isMovingRight);
249+
250+
// calculate progress for each corner based on the duration and time since cursor change
251+
float prog_tl = ease(clamp(baseProgress / final_dur_tl, 0.0, 1.0));
252+
float prog_tr = ease(clamp(baseProgress / final_dur_tr, 0.0, 1.0));
253+
float prog_bl = ease(clamp(baseProgress / final_dur_bl, 0.0, 1.0));
254+
float prog_br = ease(clamp(baseProgress / final_dur_br, 0.0, 1.0));
255+
256+
// get the trial corner positions based on progress
257+
vec2 v_tl = mix(cp_tl, cc_tl, prog_tl);
258+
vec2 v_tr = mix(cp_tr, cc_tr, prog_tr);
259+
vec2 v_br = mix(cp_br, cc_br, prog_br);
260+
vec2 v_bl = mix(cp_bl, cc_bl, prog_bl);
261+
262+
// DRAWING THE TRAIL
263+
float sdfTrail = getSdfConvexQuad(vu, v_tl, v_tr, v_br, v_bl);
264+
265+
// --- FADE GRADIENT CALCULATION ---
266+
vec2 fragVec = vu - centerCP;
267+
268+
// project fragment onto movement vector, normalize to [0, 1]
269+
// 0.0 at tail, 1.0 at head
270+
// tiny epsilon to avoid division by zero if moveVec is (0,0)
271+
float fadeProgress = clamp(dot(fragVec, moveVec) / (dot(moveVec, moveVec) + 1e-6), 0.0, 1.0);
272+
273+
vec4 trail = TRAIL_COLOR;
274+
275+
float effectiveBlur = BLUR;
276+
if (BLUR < 2.5) {
277+
// no antialising on horizontal/vertical movement, fixes 'pulse' like thing on end cursor
278+
float isDiagonal = abs(s.x) * abs(s.y); // 1.0 if diagonal, 0.0 if H/V
279+
float effectiveBlur = mix(0.0, BLUR, isDiagonal);
280+
}
281+
float shapeAlpha = antialising(sdfTrail, effectiveBlur); // shape mask
282+
283+
if (FADE_ENABLED > 0.5) {
284+
// apply fade gradient along the trail
285+
// float fadeStart = 0.2;
286+
// float easedProgress = smoothstep(fadeStart, 1.0, fadeProgress);
287+
// easedProgress = pow(2.0, 10.0 * (fadeProgress - 1.0));
288+
float easedProgress = pow(fadeProgress, FADE_EXPONENT);
289+
trail.a *= easedProgress;
290+
}
291+
292+
float finalAlpha = trail.a * shapeAlpha;
293+
294+
// newColor.a to preserve the background alpha.
295+
newColor = mix(newColor, vec4(trail.rgb, newColor.a), finalAlpha);
296+
297+
// punch hole on the trail, so current cursor is drawn on top
298+
newColor = mix(newColor, fragColor, step(sdfCurrentCursor, 0.));
299+
300+
}
301+
302+
fragColor = newColor;
303+
}

install.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ dotfiles() {
4646
done;
4747
mkdir -p ${HOME}/.config/ghostty;
4848
ln -sfn $(pwd)/ghostty/config ${HOME}/.config/ghostty/config
49-
ln -sfn $(pwd)/ghostty/themes ${HOME}/.config/ghostty/themes
49+
ln -sfn $(pwd)/ghostty/themes ${HOME}/.config/ghostty/themes
50+
ln -sfn $(pwd)/ghostty/shaders ${HOME}/.config/ghostty/shaders
5051
}
5152

5253
fix_nu_path() {

0 commit comments

Comments
 (0)