Repository: sahaj-b/ghostty-cursor-shaders
Branch: main
Commit: 4faa83e4b930
Files: 9
Total size: 52.4 KB
Directory structure:
gitextract_7ry6z7i4/
├── .gitignore
├── README.md
├── cursor_sweep.glsl
├── cursor_tail.glsl
├── cursor_warp.glsl
├── rectangle_boom_cursor.glsl
├── ripple_cursor.glsl
├── ripple_rectangle_cursor.glsl
└── sonic_boom_cursor.glsl
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
gifs
================================================
FILE: README.md
================================================
# Cursor shaders for ghostty
## WARNING: These are extremely customizable
## Demos
| Effect | Demo |
| -------- | ------ |
| Cursor Warp<br>(Neovide-like) |  |
| Cursor Sweep |  |
| Cursor Tail<br>(Kitty-like) |  |
| Ripple Cursor |  |
| Sonic Boom |  |
| Ripple Rectangle |  |
| Customized<br>(faded warp + ripple) |  |
## Trails
- [cursor_warp.glsl](cursor_warp.glsl): Neovide-like cursor trail, most customizable shader
- [cursor_sweep.glsl](cursor_sweep.glsl): Animated trail that shrinks from previous to current cursor position
- [cursor_tail.glsl](cursor_tail.glsl): Comet-like trail, mimicing kitty terminal's cursor_trail effect
## Pulse/Boom effects
- These trigger on cursor mode changes (block to line or vice versa, looks cool on changing modes in vim)
- [sonic_boom_cursor.glsl](sonic_boom_cursor.glsl): expanding filled circle
- [ripple_cursor.glsl](ripple_cursor.glsl): Expanding circular ring ripple effect
- [rectangle_boom_cursor.glsl](rectangle_boom_cursor.glsl): Same as boom_cursor but rectangular(cursor shape)
- [ripple_rectangle_cursor.glsl](ripple_rectangle_cursor.glsl): Same as ripple_cursor but rectangular(cursor shape)
> [!NOTE]
> If you have the line cursor (default), these effects will trigger and freeze on unfocus(as cursor changes to hollow block). The solution is to add `custom-shader-animation = always` to your ghostty config
## Usage
1. Clone the repo into your ghostty shaders directory:
```bash
git clone https://github.com/sahaj-b/ghostty-cursor-shaders ~/.config/ghostty/shaders
```
2. In your `~/.config/ghostty/config`, add:
```config
custom-shader = shaders/yourshader1.glsl
custom-shader = shaders/yourshader2.glsl
# ...
```
Replace `yourshader` with the name of any shader file (e.g., `cursor_sweep`, `ripple_cursor`, etc.)
## Customization
- All shaders has customizable parameters (like color, duration, size, thickness, etc) etc at the top of each file. You can adjust
- Also, all files has various **Easing Functions** to choose from.
- these function control the animation curve of the effects, you can make them elasitcy, springy, smooth, linear, etc by changing the easing function
- in trail shaders, you can comment/uncomment the easing functions in the code
- in pulse/boom shaders, you can comment/uncomment the lines in the `ANIMATION` section
- you can also add your own easing functions if you want
### Example (faded warp + ripple)
```glsl
// in cursor_warp.glsl
const float DURATION = 0.15;
const float TRAIL_SIZE = 0.8;
const float THRESHOLD_MIN_DISTANCE = 1.0;
const float BLUR = 1.0;
const float TRAIL_THICKNESS = 1.0;
const float TRAIL_THICKNESS_X = 0.9;
const float FADE_ENABLED = 1.0;
const float FADE_EXPONENT = 5.0;
```
```glsl
// in ripple_cursor.glsl
const float DURATION = 0.15;
const float MAX_RADIUS = 0.026;
const float RING_THICKNESS = 0.02;
const float CURSOR_WIDTH_CHANGE_THRESHOLD = 0.5;
vec4 COLOR = vec4(0.35, 0.36, 0.44, 0.8);
const float BLUR = 3.5;
const float ANIMATION_START_OFFSET = 0.01;
```
## Acknowledgements
Inspired by [Neovide](https://neovide.dev/) cursor animations and [KroneCorylus/ghostty-shader-playground](https://github.com/KroneCorylus/ghostty-shader-playground)
## License
MIT
## Why use branching(if/else) instead of branchless math
- coz we are dealing with **uniform branching** here, which has **NO DIVERGENCE**.
- ie, all fragments will take the same branch path, so no performance penalty on modern GPUs
- Branchless math would force GPU to calculate animations every single frame, even when there is no need
================================================
FILE: cursor_sweep.glsl
================================================
// -- CONFIGURATION ---
vec4 TRAIL_COLOR = iCurrentCursorColor; // can change to eg: vec4(0.2, 0.6, 1.0, 0.5);
const float DURATION = 0.2; // in seconds
const float TRAIL_LENGTH = 0.5;
const float BLUR = 2.0; // blur size in pixels (for antialiasing)
// --- CONSTANTS for easing functions ---
const float PI = 3.14159265359;
const float C1_BACK = 1.70158;
const float C2_BACK = C1_BACK * 1.525;
const float C3_BACK = C1_BACK + 1.0;
const float C4_ELASTIC = (2.0 * PI) / 3.0;
const float C5_ELASTIC = (2.0 * PI) / 4.5;
const float SPRING_STIFFNESS = 9.0;
const float SPRING_DAMPING = 0.9;
// --- EASING FUNCTIONS ---
// // Linear
// float ease(float x) {
// return x;
// }
// // EaseOutQuad
// float ease(float x) {
// return 1.0 - (1.0 - x) * (1.0 - x);
// }
// EaseOutCubic
float ease(float x) {
return 1.0 - pow(1.0 - x, 3.0);
}
// // EaseOutQuart
// float ease(float x) {
// return 1.0 - pow(1.0 - x, 4.0);
// }
// // EaseOutQuint
// float ease(float x) {
// return 1.0 - pow(1.0 - x, 5.0);
// }
// EaseOutSine
// float ease(float x) {
// return sin((x * PI) / 2.0);
// }
// // EaseOutExpo
// float ease(float x) {
// return x == 1.0 ? 1.0 : 1.0 - pow(2.0, -10.0 * x);
// }
// // EaseOutCirc
// float ease(float x) {
// return sqrt(1.0 - pow(x - 1.0, 2.0));
// }
// // EaseOutBack
// float ease(float x) {
// return 1.0 + C3_BACK * pow(x - 1.0, 3.0) + C1_BACK * pow(x - 1.0, 2.0);
// }
// // EaseOutElastic
// float ease(float x) {
// return x == 0.0 ? 0.0
// : x == 1.0 ? 1.0
// : pow(2.0, -10.0 * x) * sin((x * 10.0 - 0.75) * C4_ELASTIC) + 1.0;
// }
// Parametric Spring
// float ease(float x) {
// x = clamp(x, 0.0, 1.0);
// float decay = exp(-SPRING_DAMPING * SPRING_STIFFNESS * x);
// float freq = sqrt(SPRING_STIFFNESS * (1.0 - SPRING_DAMPING * SPRING_DAMPING));
// float osc = cos(freq * 6.283185 * x) + (SPRING_DAMPING * sqrt(SPRING_STIFFNESS) / freq) * sin(freq * 6.283185 * x);
// return 1.0 - decay * osc;
// }
float getSdfRectangle(in vec2 point, in vec2 center, in vec2 halfSize)
{
vec2 d = abs(point - center) - halfSize;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
// Based on Inigo Quilez's 2D distance functions article: https://iquilezles.org/articles/distfunctions2d/
// Potencially optimized by eliminating conditionals and loops to enhance performance and reduce branching
float seg(in vec2 p, in vec2 a, in vec2 b, inout float s, float d) {
vec2 e = b - a;
vec2 w = p - a;
vec2 proj = a + e * clamp(dot(w, e) / dot(e, e), 0.0, 1.0);
float segd = dot(p - proj, p - proj);
d = min(d, segd);
float c0 = step(0.0, p.y - a.y);
float c1 = 1.0 - step(0.0, p.y - b.y);
float c2 = 1.0 - step(0.0, e.x * w.y - e.y * w.x);
float allCond = c0 * c1 * c2;
float noneCond = (1.0 - c0) * (1.0 - c1) * (1.0 - c2);
float flip = mix(1.0, -1.0, step(0.5, allCond + noneCond));
s *= flip;
return d;
}
float getSdfParallelogram(in vec2 p, in vec2 v0, in vec2 v1, in vec2 v2, in vec2 v3) {
float s = 1.0;
float d = dot(p - v0, p - v0);
d = seg(p, v0, v3, s, d);
d = seg(p, v1, v0, s, d);
d = seg(p, v2, v1, s, d);
d = seg(p, v3, v2, s, d);
return s * sqrt(d);
}
vec2 normalize(vec2 value, float isPosition) {
return (value * 2.0 - (iResolution.xy * isPosition)) / iResolution.y;
}
float antialising(float distance) {
return 1. - smoothstep(0., normalize(vec2(BLUR, BLUR), 0.).x, distance);
}
float getTopVertexFlag(vec2 a, vec2 b) {
float condition1 = step(b.x, a.x) * step(a.y, b.y); // a.x < b.x && a.y > b.y
float condition2 = step(a.x, b.x) * step(b.y, a.y); // a.x > b.x && a.y < b.y
// if neither condition is met, return 1 (else case)
return 1.0 - max(condition1, condition2);
}
vec2 getRectangleCenter(vec4 rectangle) {
return vec2(rectangle.x + (rectangle.z / 2.), rectangle.y - (rectangle.w / 2.));
}
void mainImage(out vec4 fragColor, in vec2 fragCoord){
#if !defined(WEB)
fragColor = texture(iChannel0, fragCoord.xy / iResolution.xy);
#endif
// normalization & setup(-1, 1 coords)
vec2 vu = normalize(fragCoord, 1.);
vec2 offsetFactor = vec2(-.5, 0.5);
vec4 currentCursor = vec4(normalize(iCurrentCursor.xy, 1.), normalize(iCurrentCursor.zw, 0.));
vec4 previousCursor = vec4(normalize(iPreviousCursor.xy, 1.), normalize(iPreviousCursor.zw, 0.));
vec2 centerCC = currentCursor.xy - (currentCursor.zw * offsetFactor);
vec2 centerCP = previousCursor.xy - (previousCursor.zw * offsetFactor);
float sdfCurrentCursor = getSdfRectangle(vu, centerCC, currentCursor.zw * 0.5);
float lineLength = distance(centerCC, centerCP);
vec4 newColor = vec4(fragColor);
float minDist = currentCursor.w * 1.5;
float progress = clamp((iTime - iTimeCursorChange) / DURATION, 0.0, 1.0);
if (lineLength > minDist) {
// --- Animation Logic ---
float shrinkFactor = ease(progress);
// detect straight moves
vec2 delta = abs(centerCC - centerCP);
float threshold = 0.001;
float isHorizontal = step(delta.y, threshold);
float isVertical = step(delta.x, threshold);
float isStraightMove = max(isHorizontal, isVertical);
// -- Making parallelogram sdf (diagonal moves) ---
float topVertexFlag = getTopVertexFlag(currentCursor.xy, previousCursor.xy);
float bottomVertexFlag = 1.0 - topVertexFlag;
vec2 v0 = vec2(currentCursor.x + currentCursor.z * topVertexFlag, currentCursor.y - currentCursor.w);
vec2 v1 = vec2(currentCursor.x + currentCursor.z * bottomVertexFlag, currentCursor.y);
vec2 v2_full = vec2(previousCursor.x + currentCursor.z * bottomVertexFlag, previousCursor.y);
vec2 v3_full = vec2(previousCursor.x + currentCursor.z * topVertexFlag, previousCursor.y - previousCursor.w);
vec2 v2_start = mix(v1, v2_full, TRAIL_LENGTH);
vec2 v3_start = mix(v0, v3_full, TRAIL_LENGTH);
vec2 v2_anim = mix(v2_start, v1, shrinkFactor);
vec2 v3_anim = mix(v3_start, v0, shrinkFactor);
float sdfTrail_diag = getSdfParallelogram(vu, v0, v1, v2_anim, v3_anim);
// --- Making rectangle sdf (straight moves) ---
vec2 min_center = min(centerCP, centerCC);
vec2 max_center = max(centerCP, centerCC);
vec2 bBoxSize_full = (max_center - min_center) + currentCursor.zw;
vec2 bBoxCenter_full = (min_center + max_center) * 0.5;
vec2 bBoxSize_start = mix(currentCursor.zw, bBoxSize_full, TRAIL_LENGTH);
vec2 bBoxCenter_start = mix(centerCC, bBoxCenter_full, TRAIL_LENGTH);
vec2 animSize = mix(bBoxSize_start, currentCursor.zw, shrinkFactor);
vec2 animCenter = mix(bBoxCenter_start, centerCC, shrinkFactor);
float sdfTrail_rect = getSdfRectangle(vu, animCenter, animSize * 0.5);
// -- Selecting and drawing the trail sdf --
float sdfTrail = mix(sdfTrail_diag, sdfTrail_rect, isStraightMove);
vec4 trail = TRAIL_COLOR;
float trailAlpha = antialising(sdfTrail);
newColor = mix(newColor, trail, trailAlpha);
// Punch hole
newColor = mix(newColor, fragColor, step(sdfCurrentCursor, 0.));
}
fragColor = newColor;
}
================================================
FILE: cursor_tail.glsl
================================================
// -- CONFIGURATION --
vec4 TRAIL_COLOR = iCurrentCursorColor; // can change to eg: vec4(0.2, 0.6, 1.0, 0.5);
const float DURATION = 0.09; // in seconds
const float MAX_TRAIL_LENGTH = 0.2;
const float THRESHOLD_MIN_DISTANCE = 1.5; // min distance to show trail (units of cursor width)
const float BLUR = 2.0; // blur size in pixels (for antialiasing)
// --- CONSTANTS for easing functions ---
const float PI = 3.14159265359;
const float C1_BACK = 1.70158;
const float C2_BACK = C1_BACK * 1.525;
const float C3_BACK = C1_BACK + 1.0;
const float C4_ELASTIC = (2.0 * PI) / 3.0;
const float C5_ELASTIC = (2.0 * PI) / 4.5;
const float SPRING_STIFFNESS = 9.0;
const float SPRING_DAMPING = 0.9;
// --- EASING FUNCTIONS ---
// // Linear
// float ease(float x) {
// return x;
// }
// // EaseOutQuad
// float ease(float x) {
// return 1.0 - (1.0 - x) * (1.0 - x);
// }
// // EaseOutCubic
// float ease(float x) {
// return 1.0 - pow(1.0 - x, 3.0);
// }
// // EaseOutQuart
// float ease(float x) {
// return 1.0 - pow(1.0 - x, 4.0);
// }
// // EaseOutQuint
// float ease(float x) {
// return 1.0 - pow(1.0 - x, 5.0);
// }
// // EaseOutSine
// float ease(float x) {
// return sin((x * PI) / 2.0);
// }
// // EaseOutExpo
// float ease(float x) {
// return x == 1.0 ? 1.0 : 1.0 - pow(2.0, -10.0 * x);
// }
// EaseOutCirc
float ease(float x) {
return sqrt(1.0 - pow(x - 1.0, 2.0));
}
// // EaseOutBack
// float ease(float x) {
// return 1.0 + C3_BACK * pow(x - 1.0, 3.0) + C1_BACK * pow(x - 1.0, 2.0);
// }
// // EaseOutElastic
// float ease(float x) {
// return x == 0.0 ? 0.0
// : x == 1.0 ? 1.0
// : pow(2.0, -10.0 * x) * sin((x * 10.0 - 0.75) * C4_ELASTIC) + 1.0;
// }
// Parametric Spring
// float ease(float x) {
// x = clamp(x, 0.0, 1.0);
// float decay = exp(-SPRING_DAMPING * SPRING_STIFFNESS * x);
// float freq = sqrt(SPRING_STIFFNESS * (1.0 - SPRING_DAMPING * SPRING_DAMPING));
// float osc = cos(freq * 6.283185 * x) + (SPRING_DAMPING * sqrt(SPRING_STIFFNESS) / freq) * sin(freq * 6.283185 * x);
// return 1.0 - decay * osc;
// }
float getSdfRectangle(in vec2 p, in vec2 xy, in vec2 b)
{
vec2 d = abs(p - xy) - b;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
// Based on Inigo Quilez's 2D distance functions article: https://iquilezles.org/articles/distfunctions2d/
// Potencially optimized by eliminating conditionals and loops to enhance performance and reduce branching
float seg(in vec2 p, in vec2 a, in vec2 b, inout float s, float d) {
vec2 e = b - a;
vec2 w = p - a;
vec2 proj = a + e * clamp(dot(w, e) / dot(e, e), 0.0, 1.0);
float segd = dot(p - proj, p - proj);
d = min(d, segd);
float c0 = step(0.0, p.y - a.y);
float c1 = 1.0 - step(0.0, p.y - b.y);
float c2 = 1.0 - step(0.0, e.x * w.y - e.y * w.x);
float allCond = c0 * c1 * c2;
float noneCond = (1.0 - c0) * (1.0 - c1) * (1.0 - c2);
float flip = mix(1.0, -1.0, step(0.5, allCond + noneCond));
s *= flip;
return d;
}
float getSdfParallelogram(in vec2 p, in vec2 v0, in vec2 v1, in vec2 v2, in vec2 v3) {
float s = 1.0;
float d = dot(p - v0, p - v0);
d = seg(p, v0, v3, s, d);
d = seg(p, v1, v0, s, d);
d = seg(p, v2, v1, s, d);
d = seg(p, v3, v2, s, d);
return s * sqrt(d);
}
vec2 normalize(vec2 value, float isPosition) {
return (value * 2.0 - (iResolution.xy * isPosition)) / iResolution.y;
}
float antialising(float distance) {
return 1. - smoothstep(0., normalize(vec2(BLUR, BLUR), 0.).x, distance);
}
float determineIfTopRightIsLeading(vec2 a, vec2 b) {
float condition1 = step(b.x, a.x) * step(a.y, b.y); // a.x < b.x && a.y > b.y
float condition2 = step(a.x, b.x) * step(b.y, a.y); // a.x > b.x && a.y < b.y
// if neither condition is met, return 1 (else case)
return 1.0 - max(condition1, condition2);
}
vec2 getRectangleCenter(vec4 rectangle) {
return vec2(rectangle.x + (rectangle.z / 2.), rectangle.y - (rectangle.w / 2.));
}
void mainImage(out vec4 fragColor, in vec2 fragCoord){
#if !defined(WEB)
fragColor = texture(iChannel0, fragCoord.xy / iResolution.xy);
#endif
// normalization & setup(-1, 1 coords)
vec2 vu = normalize(fragCoord, 1.);
vec2 offsetFactor = vec2(-.5, 0.5);
vec4 currentCursor = vec4(normalize(iCurrentCursor.xy, 1.), normalize(iCurrentCursor.zw, 0.));
vec4 previousCursor = vec4(normalize(iPreviousCursor.xy, 1.), normalize(iPreviousCursor.zw, 0.));
vec2 centerCC = currentCursor.xy - (currentCursor.zw * offsetFactor);
vec2 centerCP = previousCursor.xy - (previousCursor.zw * offsetFactor);
vec2 delta = centerCP - centerCC;
float lineLength = length(delta);
float sdfCurrentCursor = getSdfRectangle(vu, centerCC, currentCursor.zw * 0.5);
vec4 newColor = vec4(fragColor);
float minDist = currentCursor.w * THRESHOLD_MIN_DISTANCE;
float progress = clamp((iTime - iTimeCursorChange) / DURATION, 0.0, 1.0);
if (lineLength > minDist) {
// ANIMATION logic
float head_eased = 0.0;
float tail_eased = 0.0;
float tail_delay_factor = MAX_TRAIL_LENGTH / lineLength;
float isLongMove = step(MAX_TRAIL_LENGTH, lineLength);
float head_eased_short = ease(progress);
float tail_eased_short = ease(smoothstep(tail_delay_factor, 1.0, progress));
float head_eased_long = 1.0;
float tail_eased_long = ease(progress);
head_eased = mix(head_eased_long, head_eased_short, isLongMove);
tail_eased = mix(tail_eased_long, tail_eased_short, isLongMove);
// detect straight moves
vec2 delta_abs = abs(centerCC - centerCP);
float threshold = 0.001;
float isHorizontal = step(delta_abs.y, threshold);
float isVertical = step(delta_abs.x, threshold);
float isStraightMove = max(isHorizontal, isVertical);
// -- Making the parallelogram sdf (diagonal move) --
// animate the TOP-LEFT corners
vec2 head_pos_tl = mix(previousCursor.xy, currentCursor.xy, head_eased);
vec2 tail_pos_tl = mix(previousCursor.xy, currentCursor.xy, tail_eased);
float isTopRightLeading = determineIfTopRightIsLeading(currentCursor.xy, previousCursor.xy);
float isBottomLeftLeading = 1.0 - isTopRightLeading;
// v0, v1 : "front" of the trail (head)
vec2 v0 = vec2(head_pos_tl.x + currentCursor.z * isTopRightLeading, head_pos_tl.y - currentCursor.w);
vec2 v1 = vec2(head_pos_tl.x + currentCursor.z * isBottomLeftLeading, head_pos_tl.y);
// v2, v3: "back" of the trail (tail)
vec2 v2 = vec2(tail_pos_tl.x + currentCursor.z * isBottomLeftLeading, tail_pos_tl.y);
vec2 v3 = vec2(tail_pos_tl.x + currentCursor.z * isTopRightLeading, tail_pos_tl.y - previousCursor.w);
float sdfTrail_diag = getSdfParallelogram(vu, v0, v1, v2, v3);
// -- Making the rectangle sdf (straight move) --
vec2 head_center = mix(centerCP, centerCC, head_eased);
vec2 tail_center = mix(centerCP, centerCC, tail_eased);
vec2 min_center = min(head_center, tail_center);
vec2 max_center = max(head_center, tail_center);
vec2 box_size = (max_center - min_center) + currentCursor.zw;
vec2 box_center = (min_center + max_center) * 0.5;
float sdfTrail_rect = getSdfRectangle(vu, box_center, box_size * 0.5);
// -- FINAL SELECTING AND DRAWING --
float sdfTrail = mix(sdfTrail_diag, sdfTrail_rect, isStraightMove);
vec4 trail = TRAIL_COLOR;
float trailAlpha = antialising(sdfTrail);
newColor = mix(newColor, trail, trailAlpha);
// punch hole
newColor = mix(newColor, fragColor, step(sdfCurrentCursor, 0.));
}
fragColor = newColor;
}
================================================
FILE: cursor_warp.glsl
================================================
// --- CONFIGURATION ---
vec4 TRAIL_COLOR = iCurrentCursorColor; // can change to eg: vec4(0.2, 0.6, 1.0, 0.5);
const float DURATION = 0.2; // total animation time
const float TRAIL_SIZE = 0.8; // 0.0 = all corners move together. 1.0 = max smear (leading corners jump instantly)
const float THRESHOLD_MIN_DISTANCE = 1.5; // min distance to show trail (units of cursor height)
const float BLUR = 1.0; // blur size in pixels (for antialiasing)
const float TRAIL_THICKNESS = 1.0; // 1.0 = full cursor height, 0.0 = zero height, >1.0 = funky aah
const float TRAIL_THICKNESS_X = 0.9;
const float FADE_ENABLED = 0.0; // 1.0 to enable fade gradient along the trail, 0.0 to disable
const float FADE_EXPONENT = 5.0; // exponent for fade gradient along the trail
// --- CONSTANTS for easing functions ---
const float PI = 3.14159265359;
const float C1_BACK = 1.70158;
const float C2_BACK = C1_BACK * 1.525;
const float C3_BACK = C1_BACK + 1.0;
const float C4_ELASTIC = (2.0 * PI) / 3.0;
const float C5_ELASTIC = (2.0 * PI) / 4.5;
const float SPRING_STIFFNESS = 9.0;
const float SPRING_DAMPING = 0.9;
// --- EASING FUNCTIONS ---
// // Linear
// float ease(float x) {
// return x;
// }
// // EaseOutQuad
// float ease(float x) {
// return 1.0 - (1.0 - x) * (1.0 - x);
// }
// // EaseOutCubic
// float ease(float x) {
// return 1.0 - pow(1.0 - x, 3.0);
// }
// // EaseOutQuart
// float ease(float x) {
// return 1.0 - pow(1.0 - x, 4.0);
// }
// // EaseOutQuint
// float ease(float x) {
// return 1.0 - pow(1.0 - x, 5.0);
// }
// // EaseOutSine
// float ease(float x) {
// return sin((x * PI) / 2.0);
// }
// // EaseOutExpo
// float ease(float x) {
// return x == 1.0 ? 1.0 : 1.0 - pow(2.0, -10.0 * x);
// }
// EaseOutCirc
float ease(float x) {
return sqrt(1.0 - pow(x - 1.0, 2.0));
}
// // EaseOutBack
// float ease(float x) {
// return 1.0 + C3_BACK * pow(x - 1.0, 3.0) + C1_BACK * pow(x - 1.0, 2.0);
// }
// // EaseOutElastic
// float ease(float x) {
// return x == 0.0 ? 0.0
// : x == 1.0 ? 1.0
// : pow(2.0, -10.0 * x) * sin((x * 10.0 - 0.75) * C4_ELASTIC) + 1.0;
// }
// // Parametric Spring
// float ease(float x) {
// x = clamp(x, 0.0, 1.0);
// float decay = exp(-SPRING_DAMPING * SPRING_STIFFNESS * x);
// float freq = sqrt(SPRING_STIFFNESS * (1.0 - SPRING_DAMPING * SPRING_DAMPING));
// float osc = cos(freq * 6.283185 * x) + (SPRING_DAMPING * sqrt(SPRING_STIFFNESS) / freq) * sin(freq * 6.283185 * x);
// return 1.0 - decay * osc;
// }
float getSdfRectangle(in vec2 p, in vec2 xy, in vec2 b)
{
vec2 d = abs(p - xy) - b;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
// Based on Inigo Quilez's 2D distance functions article: https://iquilezles.org/articles/distfunctions2d/
// Potencially optimized by eliminating conditionals and loops to enhance performance and reduce branching
float seg(in vec2 p, in vec2 a, in vec2 b, inout float s, float d) {
vec2 e = b - a;
vec2 w = p - a;
vec2 proj = a + e * clamp(dot(w, e) / dot(e, e), 0.0, 1.0);
float segd = dot(p - proj, p - proj);
d = min(d, segd);
float c0 = step(0.0, p.y - a.y);
float c1 = 1.0 - step(0.0, p.y - b.y);
float c2 = 1.0 - step(0.0, e.x * w.y - e.y * w.x);
float allCond = c0 * c1 * c2;
float noneCond = (1.0 - c0) * (1.0 - c1) * (1.0 - c2);
float flip = mix(1.0, -1.0, step(0.5, allCond + noneCond));
s *= flip;
return d;
}
float getSdfConvexQuad(in vec2 p, in vec2 v1, in vec2 v2, in vec2 v3, in vec2 v4) {
float s = 1.0;
float d = dot(p - v1, p - v1);
d = seg(p, v1, v2, s, d);
d = seg(p, v2, v3, s, d);
d = seg(p, v3, v4, s, d);
d = seg(p, v4, v1, s, d);
return s * sqrt(d);
}
vec2 normalize(vec2 value, float isPosition) {
return (value * 2.0 - (iResolution.xy * isPosition)) / iResolution.y;
}
float antialising(float distance, float blurAmount) {
return 1. - smoothstep(0., normalize(vec2(blurAmount, blurAmount), 0.).x, distance);
}
// Determines animation duration based on a corner's alignment with the move direction(dot product)
// dot_val will be in [-2, 2]
// > 0.5 (1 or 2) = Leading
// > -0.5 (0) = Side
// <= -0.5 (-1 or -2) = Trailing
float getDurationFromDot(float dot_val, float DURATION_LEAD, float DURATION_SIDE, float DURATION_TRAIL) {
float isLead = step(0.5, dot_val);
float isSide = step(-0.5, dot_val) * (1.0 - isLead);
// Start with trailing duration
float duration = mix(DURATION_TRAIL, DURATION_SIDE, isSide);
// Mix in leading duration
duration = mix(duration, DURATION_LEAD, isLead);
return duration;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord){
#if !defined(WEB)
fragColor = texture(iChannel0, fragCoord.xy / iResolution.xy);
#endif
// normalization & setup(-1, 1 coords)
vec2 vu = normalize(fragCoord, 1.);
vec2 offsetFactor = vec2(-.5, 0.5);
vec4 currentCursor = vec4(normalize(iCurrentCursor.xy, 1.), normalize(iCurrentCursor.zw, 0.));
vec4 previousCursor = vec4(normalize(iPreviousCursor.xy, 1.), normalize(iPreviousCursor.zw, 0.));
vec2 centerCC = currentCursor.xy - (currentCursor.zw * offsetFactor);
vec2 halfSizeCC = currentCursor.zw * 0.5;
vec2 centerCP = previousCursor.xy - (previousCursor.zw * offsetFactor);
vec2 halfSizeCP = previousCursor.zw * 0.5;
float sdfCurrentCursor = getSdfRectangle(vu, centerCC, halfSizeCC);
float lineLength = distance(centerCC, centerCP);
float minDist = currentCursor.w * THRESHOLD_MIN_DISTANCE;
vec4 newColor = vec4(fragColor);
float baseProgress = iTime - iTimeCursorChange;
if (lineLength > minDist && baseProgress < DURATION - 0.001) {
// defining corners of cursors
// Y (Height) with TRAIL_THICKNESS
float cc_half_height = currentCursor.w * 0.5;
float cc_center_y = currentCursor.y - cc_half_height;
float cc_new_half_height = cc_half_height * TRAIL_THICKNESS;
float cc_new_top_y = cc_center_y + cc_new_half_height;
float cc_new_bottom_y = cc_center_y - cc_new_half_height;
// X (Width) with TRAIL_THICKNESS
float cc_half_width = currentCursor.z * 0.5;
float cc_center_x = currentCursor.x + cc_half_width;
float cc_new_half_width = cc_half_width * TRAIL_THICKNESS_X;
float cc_new_left_x = cc_center_x - cc_new_half_width;
float cc_new_right_x = cc_center_x + cc_new_half_width;
vec2 cc_tl = vec2(cc_new_left_x, cc_new_top_y);
vec2 cc_tr = vec2(cc_new_right_x, cc_new_top_y);
vec2 cc_bl = vec2(cc_new_left_x, cc_new_bottom_y);
vec2 cc_br = vec2(cc_new_right_x, cc_new_bottom_y);
// same thing for previous cursor
float cp_half_height = previousCursor.w * 0.5;
float cp_center_y = previousCursor.y - cp_half_height;
float cp_new_half_height = cp_half_height * TRAIL_THICKNESS;
float cp_new_top_y = cp_center_y + cp_new_half_height;
float cp_new_bottom_y = cp_center_y - cp_new_half_height;
float cp_half_width = previousCursor.z * 0.5;
float cp_center_x = previousCursor.x + cp_half_width;
float cp_new_half_width = cp_half_width * TRAIL_THICKNESS_X;
float cp_new_left_x = cp_center_x - cp_new_half_width;
float cp_new_right_x = cp_center_x + cp_new_half_width;
vec2 cp_tl = vec2(cp_new_left_x, cp_new_top_y);
vec2 cp_tr = vec2(cp_new_right_x, cp_new_top_y);
vec2 cp_bl = vec2(cp_new_left_x, cp_new_bottom_y);
vec2 cp_br = vec2(cp_new_right_x, cp_new_bottom_y);
// calculating durations for every corner
const float DURATION_TRAIL = DURATION;
const float DURATION_LEAD = DURATION * (1.0 - TRAIL_SIZE);
const float DURATION_SIDE = (DURATION_LEAD + DURATION_TRAIL) / 2.0;
vec2 moveVec = centerCC - centerCP;
vec2 s = sign(moveVec);
// dot products for each corner, determining alignment with movement direction
float dot_tl = dot(vec2(-1., 1.), s);
float dot_tr = dot(vec2( 1., 1.), s);
float dot_bl = dot(vec2(-1.,-1.), s);
float dot_br = dot(vec2( 1.,-1.), s);
// assign durations based on dot products
float dur_tl = getDurationFromDot(dot_tl, DURATION_LEAD, DURATION_SIDE, DURATION_TRAIL);
float dur_tr = getDurationFromDot(dot_tr, DURATION_LEAD, DURATION_SIDE, DURATION_TRAIL);
float dur_bl = getDurationFromDot(dot_bl, DURATION_LEAD, DURATION_SIDE, DURATION_TRAIL);
float dur_br = getDurationFromDot(dot_br, DURATION_LEAD, DURATION_SIDE, DURATION_TRAIL);
// check direction of horizontal movement
float isMovingRight = step(0.5, s.x);
float isMovingLeft = step(0.5, -s.x);
// calculate vertical-rail durations
float dot_right_edge = (dot_tr + dot_br) * 0.5;
float dur_right_rail = getDurationFromDot(dot_right_edge, DURATION_LEAD, DURATION_SIDE, DURATION_TRAIL);
float dot_left_edge = (dot_tl + dot_bl) * 0.5;
float dur_left_rail = getDurationFromDot(dot_left_edge, DURATION_LEAD, DURATION_SIDE, DURATION_TRAIL);
float final_dur_tl = mix(dur_tl, dur_left_rail, isMovingLeft);
float final_dur_bl = mix(dur_bl, dur_left_rail, isMovingLeft);
float final_dur_tr = mix(dur_tr, dur_right_rail, isMovingRight);
float final_dur_br = mix(dur_br, dur_right_rail, isMovingRight);
// calculate progress for each corner based on the duration and time since cursor change
float prog_tl = ease(clamp(baseProgress / final_dur_tl, 0.0, 1.0));
float prog_tr = ease(clamp(baseProgress / final_dur_tr, 0.0, 1.0));
float prog_bl = ease(clamp(baseProgress / final_dur_bl, 0.0, 1.0));
float prog_br = ease(clamp(baseProgress / final_dur_br, 0.0, 1.0));
// get the trial corner positions based on progress
vec2 v_tl = mix(cp_tl, cc_tl, prog_tl);
vec2 v_tr = mix(cp_tr, cc_tr, prog_tr);
vec2 v_br = mix(cp_br, cc_br, prog_br);
vec2 v_bl = mix(cp_bl, cc_bl, prog_bl);
// DRAWING THE TRAIL
float sdfTrail = getSdfConvexQuad(vu, v_tl, v_tr, v_br, v_bl);
// --- FADE GRADIENT CALCULATION ---
vec2 fragVec = vu - centerCP;
// project fragment onto movement vector, normalize to [0, 1]
// 0.0 at tail, 1.0 at head
// tiny epsilon to avoid division by zero if moveVec is (0,0)
float fadeProgress = clamp(dot(fragVec, moveVec) / (dot(moveVec, moveVec) + 1e-6), 0.0, 1.0);
vec4 trail = TRAIL_COLOR;
float effectiveBlur = BLUR;
if (BLUR < 2.5) {
// no antialising on horizontal/vertical movement, fixes 'pulse' like thing on end cursor
float isDiagonal = abs(s.x) * abs(s.y); // 1.0 if diagonal, 0.0 if H/V
float effectiveBlur = mix(0.0, BLUR, isDiagonal);
}
float shapeAlpha = antialising(sdfTrail, effectiveBlur); // shape mask
if (FADE_ENABLED > 0.5) {
// apply fade gradient along the trail
// float fadeStart = 0.2;
// float easedProgress = smoothstep(fadeStart, 1.0, fadeProgress);
// easedProgress = pow(2.0, 10.0 * (fadeProgress - 1.0));
float easedProgress = pow(fadeProgress, FADE_EXPONENT);
trail.a *= easedProgress;
}
float finalAlpha = trail.a * shapeAlpha;
// newColor.a to preserve the background alpha.
newColor = mix(newColor, vec4(trail.rgb, newColor.a), finalAlpha);
// punch hole on the trail, so current cursor is drawn on top
newColor = mix(newColor, fragColor, step(sdfCurrentCursor, 0.));
}
fragColor = newColor;
}
================================================
FILE: rectangle_boom_cursor.glsl
================================================
// CONFIGURATION
const float DURATION = 0.15; // How long the ripple animates (seconds)
const float MAX_SIZE = 0.05; // Max radius in normalized coords (0.5 = 1/4 screen height)
const float ANIMATION_START_OFFSET = 0.0; // Start the ripple slightly progressed (0.0 - 1.0)
vec4 COLOR = vec4(0.35, 0.36, 0.44, 1.0); // change to iCurrentCursorColor for your cursor's color
const float CURSOR_WIDTH_CHANGE_THRESHOLD = 0.5; // Triggers ripple if cursor width changes by this fraction
const float BLUR = 3.0; // Blur level in pixels
// Easing functions
float easeOutQuad(float t) {
return 1.0 - (1.0 - t) * (1.0 - t);
}
float easeInOutQuad(float t) {
return t < 0.5 ? 2.0 * t * t : 1.0 - pow(-2.0 * t + 2.0, 2.0) / 2.0;
}
float easeOutCubic(float t) {
return 1.0 - pow(1.0 - t, 3.0);
}
float easeOutQuart(float t) {
return 1.0 - pow(1.0 - t, 4.0);
}
float easeOutQuint(float t) {
return 1.0 - pow(1.0 - t, 5.0);
}
float easeOutExpo(float t) {
return t == 1.0 ? 1.0 : 1.0 - pow(2.0, -10.0 * t);
}
float easeOutCirc(float t) {
return sqrt(1.0 - pow(t - 1.0, 2.0));
}
float easeOutSine(float t) {
return sin((t * 3.1415916) / 2.0);
}
float easeOutElastic(float t) {
const float c4 = (2.0 * 3.1415916) / 3.0;
return t == 0.0 ? 0.0 : t == 1.0 ? 1.0 : pow(2.0, -10.0 * t) * sin((t * 10.0 - 0.75) * c4) + 1.0;
}
float easeOutBounce(float t) {
const float n1 = 7.5625;
const float d1 = 2.75;
if (t < 1.0 / d1) {
return n1 * t * t;
} else if (t < 2.0 / d1) {
return n1 * (t -= 1.5 / d1) * t + 0.75;
} else if (t < 2.5 / d1) {
return n1 * (t -= 2.25 / d1) * t + 0.9375;
} else {
return n1 * (t -= 2.625 / d1) * t + 0.984375;
}
}
float easeOutBack(float t) {
const float c1 = 1.70158;
const float c3 = c1 + 1.0;
return 1.0 + c3 * pow(t - 1.0, 3.0) + c1 * pow(t - 1.0, 2.0);
}
// Pulse fade functions
float smoothstepPulse(float t) {
return 4.0 * t * (1.0 - t);
}
float easeOutPulse(float t) {
return t * (2.0 - t);
}
float powerCurvePulse(float t) {
float x = t * 2.0 - 1.0;
return 1.0 - x * x;
}
float doubleSmoothstepPulse(float t) {
return smoothstep(0.0, 0.5, t) * (1.0 - smoothstep(0.5, 1.0, t));
}
float exponentialDecayPulse(float t) {
return exp(-3.0 * t) * sin(t * 3.1415916);
}
float sinPulse(float t) {
return sin(t * 3.1415916);
}
vec2 normalize(vec2 value, float isPosition) {
return (value * 2.0 - (iResolution.xy * isPosition)) / iResolution.y;
}
float getSdfRectangle(in vec2 p, in vec2 xy, in vec2 b){
vec2 d = abs(p - xy) - b;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord){
#if !defined(WEB)
fragColor = texture(iChannel0, fragCoord.xy / iResolution.xy);
#endif
// Normalization & setup (-1 to 1 coords)
vec2 vu = normalize(fragCoord, 1.);
vec2 offsetFactor = vec2(-.5, 0.5);
vec4 currentCursor = vec4(normalize(iCurrentCursor.xy, 1.), normalize(iCurrentCursor.zw, 0.));
vec4 previousCursor = vec4(normalize(iPreviousCursor.xy, 1.), normalize(iPreviousCursor.zw, 0.));
vec2 centerCC = currentCursor.xy - (currentCursor.zw * offsetFactor);
float cellWidth = max(currentCursor.z, previousCursor.z); // width of the 'block' cursor
// check for significant width change
float widthChange = abs(currentCursor.z - previousCursor.z);
float widthThresholdNorm = cellWidth * CURSOR_WIDTH_CHANGE_THRESHOLD;
float isModeChange = step(widthThresholdNorm, widthChange);
// ANIMATION
float rippleProgress = (iTime - iTimeCursorChange) / DURATION + ANIMATION_START_OFFSET;
// don't clamp yet; we need to know if it's > 1.0 (finished)
float isAnimating = 1.0 - step(1.0, rippleProgress); // progress < 1.0 ? 1.0: 0.0
// WHY NOT BRANCHLESS??? here ya go:
// because we NEVER have divergence, even in this if/else branchfull logic
// why? because its UNIFORM branching (ie, all fragments take the same path) which modern GPUs handles efficiently
// its far more efficient than calculating the ripple EVERY FRAME even when not needed(branchless)
if (isModeChange > 0.0 && isAnimating > 0.0) {
// float easedProgress = rippleProgress;
// float easedProgress = easeOutQuad(rippleProgress);
// float easedProgress = easeInOutQuad(rippleProgress);
// float easedProgress = easeOutCubic(rippleProgress);
// float easedProgress = easeOutQuart(rippleProgress);
// float easedProgress = easeOutQuint(rippleProgress);
// float easedProgress = easeOutExpo(rippleProgress);
float easedProgress = easeOutCirc(rippleProgress);
// float easedProgress = easeOutSine(rippleProgress);
// float easedProgress = easeOutBack(rippleProgress);
// easedProgress = clamp(easedProgress, 0.0, 1.0);
// RIPPLE CALCULATION
float rippleExpansion = easedProgress * MAX_SIZE;
// float fade = 1.0; // no fade
// float fade = 1.0 - easedProgress; // linear fade
// float fade = 1.0 - smoothstepPulse(rippleProgress);
float fade = 1.0 - easeOutPulse(rippleProgress);
// float fade = 1.0 - powerCurvePulse(rippleProgress);
// float fade = doubleSmoothstepPulse(rippleProgress);
// float fade = exponentialDecayPulse(rippleProgress);
// float fade = sinPulse(rippleProgress);
vec2 halfSizeCC = vec2(currentCursor.z, currentCursor.w) * 0.5 + vec2(rippleExpansion);
float sdfRectRing = getSdfRectangle(vu, centerCC, halfSizeCC);
// Antialias (1-pixel width in normalized coords)
float antiAliasSize = normalize(vec2(BLUR, BLUR), 0.0).x;
float ripple = (1.0 - smoothstep(-antiAliasSize, antiAliasSize, sdfRectRing)) * fade;
// Apply ripple effect
fragColor = mix(fragColor, COLOR, ripple * COLOR.a);
}
// else: do nothing, keep original fragColor
}
================================================
FILE: ripple_cursor.glsl
================================================
// CONFIGURATION
const float DURATION = 0.15; // How long the ripple animates (seconds)
const float MAX_RADIUS = 0.05; // Max radius in normalized coords (0.5 = 1/4 screen height)
const float RING_THICKNESS = 0.02; // Ring width in normalized coords
const float CURSOR_WIDTH_CHANGE_THRESHOLD = 0.5; // Triggers ripple if cursor width changes by this fraction
vec4 COLOR = vec4(0.35, 0.36, 0.44, 1.0); // change to iCurrentCursorColor for your cursor's color
const float BLUR = 3.0; // Blur level in pixels
const float ANIMATION_START_OFFSET = 0.0; // Start the ripple slightly progressed (0.0 - 1.0)
// Easing functions
float easeOutQuad(float t) {
return 1.0 - (1.0 - t) * (1.0 - t);
}
float easeInOutQuad(float t) {
return t < 0.5 ? 2.0 * t * t : 1.0 - pow(-2.0 * t + 2.0, 2.0) / 2.0;
}
float easeOutCubic(float t) {
return 1.0 - pow(1.0 - t, 3.0);
}
float easeOutQuart(float t) {
return 1.0 - pow(1.0 - t, 4.0);
}
float easeOutQuint(float t) {
return 1.0 - pow(1.0 - t, 5.0);
}
float easeOutExpo(float t) {
return t == 1.0 ? 1.0 : 1.0 - pow(2.0, -10.0 * t);
}
float easeOutCirc(float t) {
return sqrt(1.0 - pow(t - 1.0, 2.0));
}
float easeOutSine(float t) {
return sin((t * 3.1415916) / 2.0);
}
float easeOutElastic(float t) {
const float c4 = (2.0 * 3.1415916) / 3.0;
return t == 0.0 ? 0.0 : t == 1.0 ? 1.0 : pow(2.0, -10.0 * t) * sin((t * 10.0 - 0.75) * c4) + 1.0;
}
float easeOutBounce(float t) {
const float n1 = 7.5625;
const float d1 = 2.75;
if (t < 1.0 / d1) {
return n1 * t * t;
} else if (t < 2.0 / d1) {
return n1 * (t -= 1.5 / d1) * t + 0.75;
} else if (t < 2.5 / d1) {
return n1 * (t -= 2.25 / d1) * t + 0.9375;
} else {
return n1 * (t -= 2.625 / d1) * t + 0.984375;
}
}
float easeOutBack(float t) {
const float c1 = 1.70158;
const float c3 = c1 + 1.0;
return 1.0 + c3 * pow(t - 1.0, 3.0) + c1 * pow(t - 1.0, 2.0);
}
// Pulse fade functions
float easeOutPulse(float t) {
return t * (2.0 - t);
}
float exponentialDecayPulse(float t) {
return exp(-3.0 * t) * sin(t * 3.1415916);
}
vec2 normalize(vec2 value, float isPosition) {
return (value * 2.0 - (iResolution.xy * isPosition)) / iResolution.y;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord){
#if !defined(WEB)
fragColor = texture(iChannel0, fragCoord.xy / iResolution.xy);
#endif
// Normalization & setup (-1 to 1 coords)
vec2 vu = normalize(fragCoord, 1.);
vec2 offsetFactor = vec2(-.5, 0.5);
vec4 currentCursor = vec4(normalize(iCurrentCursor.xy, 1.), normalize(iCurrentCursor.zw, 0.));
vec4 previousCursor = vec4(normalize(iPreviousCursor.xy, 1.), normalize(iPreviousCursor.zw, 0.));
vec2 centerCC = currentCursor.xy - (currentCursor.zw * offsetFactor);
float cellWidth = max(currentCursor.z, previousCursor.z); // width of the 'block' cursor
// check for significant width change
float widthChange = abs(currentCursor.z - previousCursor.z);
float widthThresholdNorm = cellWidth * CURSOR_WIDTH_CHANGE_THRESHOLD;
float isModeChange = step(widthThresholdNorm, widthChange);
// ANIMATION
float rippleProgress = (iTime - iTimeCursorChange) / DURATION + ANIMATION_START_OFFSET;
// don't clamp yet; we need to know if it's > 1.0 (finished)
float isAnimating = 1.0 - step(1.0, rippleProgress); // progress < 1.0 ? 1.0: 0.0
if (isModeChange > 0.0 && isAnimating > 0.0) {
// Apply easing to progress
// float easedProgress = rippleProgress;
// float easedProgress = easeOutQuad(rippleProgress);
// float easedProgress = easeInOutQuad(rippleProgress);
// float easedProgress = easeOutCubic(rippleProgress);
// float easedProgress = easeOutQuart(rippleProgress);
// float easedProgress = easeOutQuint(rippleProgress);
// float easedProgress = easeOutExpo(rippleProgress);
float easedProgress = easeOutCirc(rippleProgress);
// float easedProgress = easeOutSine(rippleProgress);
// float easedProgress = easeOutBack(rippleProgress);
// RIPPLE CALCULATION
float rippleRadius = easedProgress * MAX_RADIUS;
// float fade = 1.0; // no fade
// float fade = 1.0 - easedProgress; // linear fade
float fade = 1.0 - easeOutPulse(rippleProgress);
// float fade = 1.0 - exponentialDecayPulse(rippleProgress);
// Calculate distance from frag to cursor center
float dist = distance(vu, centerCC);
float sdfRing = abs(dist - rippleRadius) - RING_THICKNESS * 0.5;
// Antialias (1-pixel width in normalized coords)
float antiAliasSize = normalize(vec2(BLUR, BLUR), 0.0).x;
float ripple = (1.0 - smoothstep(-antiAliasSize, antiAliasSize, sdfRing)) * fade;
// Apply ripple effect
fragColor = mix(fragColor, COLOR, ripple * COLOR.a);
}
// else: do nothing, keep original fragColor
}
================================================
FILE: ripple_rectangle_cursor.glsl
================================================
// CONFIGURATION
const float DURATION = 0.15; // How long the ripple animates (seconds)
const float MAX_SIZE = 0.05; // Max radius in normalized coords (0.5 = 1/4 screen height)
const float RING_THICKNESS = 0.02; // Ring width in normalized coords
const float CURSOR_WIDTH_CHANGE_THRESHOLD = 0.5; // Triggers ripple if cursor width changes by this fraction
vec4 COLOR = vec4(0.35, 0.36, 0.44, 1.0); // change to iCurrentCursorColor for your cursor's color
const float BLUR = 1.0; // Blur level in pixels
const float ANIMATION_START_OFFSET = 0.0; // Start the ripple slightly progressed (0.0 - 1.0)
// Easing functions
float easeOutQuad(float t) {
return 1.0 - (1.0 - t) * (1.0 - t);
}
float easeInOutQuad(float t) {
return t < 0.5 ? 2.0 * t * t : 1.0 - pow(-2.0 * t + 2.0, 2.0) / 2.0;
}
float easeOutCubic(float t) {
return 1.0 - pow(1.0 - t, 3.0);
}
float easeOutQuart(float t) {
return 1.0 - pow(1.0 - t, 4.0);
}
float easeOutQuint(float t) {
return 1.0 - pow(1.0 - t, 5.0);
}
float easeOutExpo(float t) {
return t == 1.0 ? 1.0 : 1.0 - pow(2.0, -10.0 * t);
}
float easeOutCirc(float t) {
return sqrt(1.0 - pow(t - 1.0, 2.0));
}
float easeOutSine(float t) {
return sin((t * 3.1415916) / 2.0);
}
float easeOutElastic(float t) {
const float c4 = (2.0 * 3.1415916) / 3.0;
return t == 0.0 ? 0.0 : t == 1.0 ? 1.0 : pow(2.0, -10.0 * t) * sin((t * 10.0 - 0.75) * c4) + 1.0;
}
float easeOutBounce(float t) {
const float n1 = 7.5625;
const float d1 = 2.75;
if (t < 1.0 / d1) {
return n1 * t * t;
} else if (t < 2.0 / d1) {
return n1 * (t -= 1.5 / d1) * t + 0.75;
} else if (t < 2.5 / d1) {
return n1 * (t -= 2.25 / d1) * t + 0.9375;
} else {
return n1 * (t -= 2.625 / d1) * t + 0.984375;
}
}
float easeOutBack(float t) {
const float c1 = 1.70158;
const float c3 = c1 + 1.0;
return 1.0 + c3 * pow(t - 1.0, 3.0) + c1 * pow(t - 1.0, 2.0);
}
// Pulse fade functions
float easeOutPulse(float t) {
return t * (2.0 - t);
}
float exponentialDecayPulse(float t) {
return exp(-3.0 * t) * sin(t * 3.1415916);
}
vec2 normalize(vec2 value, float isPosition) {
return (value * 2.0 - (iResolution.xy * isPosition)) / iResolution.y;
}
float getSdfRectangle(in vec2 p, in vec2 xy, in vec2 b){
vec2 d = abs(p - xy) - b;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord){
#if !defined(WEB)
fragColor = texture(iChannel0, fragCoord.xy / iResolution.xy);
#endif
// Normalization & setup (-1 to 1 coords)
vec2 vu = normalize(fragCoord, 1.);
vec2 offsetFactor = vec2(-.5, 0.5);
vec4 currentCursor = vec4(normalize(iCurrentCursor.xy, 1.), normalize(iCurrentCursor.zw, 0.));
vec4 previousCursor = vec4(normalize(iPreviousCursor.xy, 1.), normalize(iPreviousCursor.zw, 0.));
vec2 centerCC = currentCursor.xy - (currentCursor.zw * offsetFactor);
float cellWidth = max(currentCursor.z, previousCursor.z); // width of the 'block' cursor
// check for significant width change
float widthChange = abs(currentCursor.z - previousCursor.z);
float widthThresholdNorm = cellWidth * CURSOR_WIDTH_CHANGE_THRESHOLD;
float isModeChange = step(widthThresholdNorm, widthChange);
// ANIMATION
float rippleProgress = (iTime - iTimeCursorChange) / DURATION + ANIMATION_START_OFFSET;
// don't clamp yet; we need to know if it's > 1.0 (finished)
float isAnimating = 1.0 - step(1.0, rippleProgress); // progress < 1.0 ? 1.0: 0.0
if (isModeChange > 0.0 && isAnimating > 0.0) {
// Apply easing to progress
// float easedProgress = rippleProgress;
// float easedProgress = easeOutQuad(rippleProgress);
// float easedProgress = easeInOutQuad(rippleProgress);
// float easedProgress = easeOutCubic(rippleProgress);
// float easedProgress = easeOutQuart(rippleProgress);
// float easedProgress = easeOutQuint(rippleProgress);
// float easedProgress = easeOutExpo(rippleProgress);
float easedProgress = easeOutCirc(rippleProgress);
// float easedProgress = easeOutSine(rippleProgress);
// float easedProgress = easeOutBack(rippleProgress);
// RIPPLE CALCULATION
float rippleExpansion = easedProgress * MAX_SIZE;
// float fade = 1.0; // no fade
// float fade = 1.0 - easedProgress; // linear fade
float fade = 1.0 - easeOutPulse(rippleProgress);
// float fade = 1.0 - exponentialDecayPulse(rippleProgress);
// Calculate distance from frag to cursor center
// float dist = distance(vu, centerCC);
// float sdfRing = abs(dist - rippleExpansion) - RING_THICKNESS * 0.5;
vec2 halfSizeCC = vec2(currentCursor.z, currentCursor.w) * 0.5 + vec2(rippleExpansion);
float sdfRectRing = abs(getSdfRectangle(vu, centerCC, halfSizeCC)) - RING_THICKNESS * 0.5;
// Antialias (1-pixel width in normalized coords)
float antiAliasSize = normalize(vec2(BLUR, BLUR), 0.0).x;
float ripple = (1.0 - smoothstep(-antiAliasSize, antiAliasSize, sdfRectRing)) * fade;
// Apply ripple effect
fragColor = mix(fragColor, COLOR, ripple * COLOR.a);
}
// else: do nothing, keep original fragColor
}
================================================
FILE: sonic_boom_cursor.glsl
================================================
// CONFIGURATION
const float DURATION = 0.15; // How long the ripple animates (seconds)
const float MAX_RADIUS = 0.06; // Max radius in normalized coords (0.5 = 1/4 screen height)
const float ANIMATION_START_OFFSET = 0.0; // Start the ripple slightly progressed (0.0 - 1.0)
vec4 COLOR = vec4(0.35, 0.36, 0.44, 1.0); // change to iCurrentCursorColor for your cursor's color
const float CURSOR_WIDTH_CHANGE_THRESHOLD = 0.5; // Triggers ripple if cursor width changes by this fraction
const float BLUR = 3.0; // Blur level in pixels
// Easing functions
float easeOutQuad(float t) {
return 1.0 - (1.0 - t) * (1.0 - t);
}
float easeInOutQuad(float t) {
return t < 0.5 ? 2.0 * t * t : 1.0 - pow(-2.0 * t + 2.0, 2.0) / 2.0;
}
float easeOutCubic(float t) {
return 1.0 - pow(1.0 - t, 3.0);
}
float easeOutQuart(float t) {
return 1.0 - pow(1.0 - t, 4.0);
}
float easeOutQuint(float t) {
return 1.0 - pow(1.0 - t, 5.0);
}
float easeOutExpo(float t) {
return t == 1.0 ? 1.0 : 1.0 - pow(2.0, -10.0 * t);
}
float easeOutCirc(float t) {
return sqrt(1.0 - pow(t - 1.0, 2.0));
}
float easeOutSine(float t) {
return sin((t * 3.1415916) / 2.0);
}
float easeOutElastic(float t) {
const float c4 = (2.0 * 3.1415916) / 3.0;
return t == 0.0 ? 0.0 : t == 1.0 ? 1.0 : pow(2.0, -10.0 * t) * sin((t * 10.0 - 0.75) * c4) + 1.0;
}
float easeOutBounce(float t) {
const float n1 = 7.5625;
const float d1 = 2.75;
if (t < 1.0 / d1) {
return n1 * t * t;
} else if (t < 2.0 / d1) {
return n1 * (t -= 1.5 / d1) * t + 0.75;
} else if (t < 2.5 / d1) {
return n1 * (t -= 2.25 / d1) * t + 0.9375;
} else {
return n1 * (t -= 2.625 / d1) * t + 0.984375;
}
}
float easeOutBack(float t) {
const float c1 = 1.70158;
const float c3 = c1 + 1.0;
return 1.0 + c3 * pow(t - 1.0, 3.0) + c1 * pow(t - 1.0, 2.0);
}
// Pulse fade functions
float smoothstepPulse(float t) {
return 4.0 * t * (1.0 - t);
}
float easeOutPulse(float t) {
return t * (2.0 - t);
}
float powerCurvePulse(float t) {
float x = t * 2.0 - 1.0;
return 1.0 - x * x;
}
float doubleSmoothstepPulse(float t) {
return smoothstep(0.0, 0.5, t) * (1.0 - smoothstep(0.5, 1.0, t));
}
float exponentialDecayPulse(float t) {
return exp(-3.0 * t) * sin(t * 3.1415916);
}
float sinPulse(float t) {
return sin(t * 3.1415916);
}
vec2 normalize(vec2 value, float isPosition) {
return (value * 2.0 - (iResolution.xy * isPosition)) / iResolution.y;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord){
#if !defined(WEB)
fragColor = texture(iChannel0, fragCoord.xy / iResolution.xy);
#endif
// Normalization & setup (-1 to 1 coords)
vec2 vu = normalize(fragCoord, 1.);
vec2 offsetFactor = vec2(-.5, 0.5);
vec4 currentCursor = vec4(normalize(iCurrentCursor.xy, 1.), normalize(iCurrentCursor.zw, 0.));
vec4 previousCursor = vec4(normalize(iPreviousCursor.xy, 1.), normalize(iPreviousCursor.zw, 0.));
vec2 centerCC = currentCursor.xy - (currentCursor.zw * offsetFactor);
float cellWidth = max(currentCursor.z, previousCursor.z); // width of the 'block' cursor
// check for significant width change
float widthChange = abs(currentCursor.z - previousCursor.z);
float widthThresholdNorm = cellWidth * CURSOR_WIDTH_CHANGE_THRESHOLD;
float isModeChange = step(widthThresholdNorm, widthChange);
// ANIMATION
float rippleProgress = (iTime - iTimeCursorChange) / DURATION + ANIMATION_START_OFFSET;
// don't clamp yet; we need to know if it's > 1.0 (finished)
float isAnimating = 1.0 - step(1.0, rippleProgress); // progress < 1.0 ? 1.0: 0.0
if (isModeChange > 0.0 && isAnimating > 0.0) {
// float easedProgress = rippleProgress;
// float easedProgress = easeOutQuad(rippleProgress);
// float easedProgress = easeInOutQuad(rippleProgress);
// float easedProgress = easeOutCubic(rippleProgress);
// float easedProgress = easeOutQuart(rippleProgress);
// float easedProgress = easeOutQuint(rippleProgress);
// float easedProgress = easeOutExpo(rippleProgress);
float easedProgress = easeOutCirc(rippleProgress);
// float easedProgress = easeOutSine(rippleProgress);
// float easedProgress = easeOutBack(rippleProgress);
// easedProgress = clamp(easedProgress, 0.0, 1.0);
// RIPPLE CALCULATION
float rippleRadius = easedProgress * MAX_RADIUS;
// float fade = 1.0; // no fade
// float fade = 1.0 - easedProgress; // linear fade
// float fade = 1.0 - smoothstepPulse(rippleProgress);
float fade = 1.0 - easeOutPulse(rippleProgress);
// float fade = 1.0 - powerCurvePulse(rippleProgress);
// float fade = doubleSmoothstepPulse(rippleProgress);
// float fade = exponentialDecayPulse(rippleProgress);
// float fade = sinPulse(rippleProgress);
// Calculate distance from frag to cursor center
float dist = distance(vu, centerCC);
float sdfCircle = dist - rippleRadius;
// Antialias (1-pixel width in normalized coords)
float antiAliasSize = normalize(vec2(BLUR, BLUR), 0.0).x;
float ripple = (1.0 - smoothstep(-antiAliasSize, antiAliasSize, sdfCircle)) * fade;
// Apply ripple effect
fragColor = mix(fragColor, COLOR, ripple * COLOR.a);
}
}
gitextract_7ry6z7i4/ ├── .gitignore ├── README.md ├── cursor_sweep.glsl ├── cursor_tail.glsl ├── cursor_warp.glsl ├── rectangle_boom_cursor.glsl ├── ripple_cursor.glsl ├── ripple_rectangle_cursor.glsl └── sonic_boom_cursor.glsl
Condensed preview — 9 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (55K chars).
[
{
"path": ".gitignore",
"chars": 5,
"preview": "gifs\n"
},
{
"path": "README.md",
"chars": 4599,
"preview": "# Cursor shaders for ghostty\n## WARNING: These are extremely customizable\n\n## Demos\n\n| Effect "
},
{
"path": "cursor_sweep.glsl",
"chars": 7330,
"preview": "// -- CONFIGURATION ---\nvec4 TRAIL_COLOR = iCurrentCursorColor; // can change to eg: vec4(0.2, 0.6, 1.0, 0.5);\nconst flo"
},
{
"path": "cursor_tail.glsl",
"chars": 7893,
"preview": "// -- CONFIGURATION --\nvec4 TRAIL_COLOR = iCurrentCursorColor; // can change to eg: vec4(0.2, 0.6, 1.0, 0.5);\nconst floa"
},
{
"path": "cursor_warp.glsl",
"chars": 11798,
"preview": "// --- CONFIGURATION ---\nvec4 TRAIL_COLOR = iCurrentCursorColor; // can change to eg: vec4(0.2, 0.6, 1.0, 0.5);\nconst fl"
},
{
"path": "rectangle_boom_cursor.glsl",
"chars": 6035,
"preview": "// CONFIGURATION\nconst float DURATION = 0.15; // How long the ripple animates (seconds)\nconst float MAX_SI"
},
{
"path": "ripple_cursor.glsl",
"chars": 5072,
"preview": "// CONFIGURATION\nconst float DURATION = 0.15; // How long the ripple animates (seconds)\nconst float MAX_RA"
},
{
"path": "ripple_rectangle_cursor.glsl",
"chars": 5410,
"preview": "// CONFIGURATION\nconst float DURATION = 0.15; // How long the ripple animates (seconds)\nconst float MAX_SI"
},
{
"path": "sonic_boom_cursor.glsl",
"chars": 5499,
"preview": "// CONFIGURATION\nconst float DURATION = 0.15; // How long the ripple animates (seconds)\nconst float MAX_RA"
}
]
About this extraction
This page contains the full source code of the sahaj-b/ghostty-cursor-shaders GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 9 files (52.4 KB), approximately 17.0k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.