diff options
| -rw-r--r-- | ghostty/config | 1 | ||||
| -rw-r--r-- | ghostty/shaders/README.md | 89 | ||||
| -rw-r--r-- | ghostty/shaders/cursor_sweep.glsl | 216 | ||||
| -rw-r--r-- | ghostty/shaders/cursor_tail.glsl | 234 | ||||
| -rw-r--r-- | ghostty/shaders/cursor_warp.glsl | 303 | ||||
| -rw-r--r-- | ghostty/shaders/rectangle_boom_cursor.glsl | 154 | ||||
| -rw-r--r-- | ghostty/shaders/ripple_cursor.glsl | 132 | ||||
| -rw-r--r-- | ghostty/shaders/ripple_rectangle_cursor.glsl | 138 | ||||
| -rw-r--r-- | ghostty/shaders/sonic_boom_cursor.glsl | 148 |
9 files changed, 1415 insertions, 0 deletions
diff --git a/ghostty/config b/ghostty/config index 9bd97b4..b001384 100644 --- a/ghostty/config +++ b/ghostty/config @@ -1,3 +1,4 @@ theme = Gruvbox Dark font-family = JetBrainsMono Nerd Font NL fullscreen +custom-shader = shaders/cursor_sweep.glsl diff --git a/ghostty/shaders/README.md b/ghostty/shaders/README.md new file mode 100644 index 0000000..8f80a89 --- /dev/null +++ b/ghostty/shaders/README.md @@ -0,0 +1,89 @@ +# 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 diff --git a/ghostty/shaders/cursor_sweep.glsl b/ghostty/shaders/cursor_sweep.glsl new file mode 100644 index 0000000..871f3d2 --- /dev/null +++ b/ghostty/shaders/cursor_sweep.glsl @@ -0,0 +1,216 @@ +// -- 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; +} diff --git a/ghostty/shaders/cursor_tail.glsl b/ghostty/shaders/cursor_tail.glsl new file mode 100644 index 0000000..0937138 --- /dev/null +++ b/ghostty/shaders/cursor_tail.glsl @@ -0,0 +1,234 @@ +// -- 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; +} diff --git a/ghostty/shaders/cursor_warp.glsl b/ghostty/shaders/cursor_warp.glsl new file mode 100644 index 0000000..08f3f77 --- /dev/null +++ b/ghostty/shaders/cursor_warp.glsl @@ -0,0 +1,303 @@ +// --- 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; +} diff --git a/ghostty/shaders/rectangle_boom_cursor.glsl b/ghostty/shaders/rectangle_boom_cursor.glsl new file mode 100644 index 0000000..afe26e3 --- /dev/null +++ b/ghostty/shaders/rectangle_boom_cursor.glsl @@ -0,0 +1,154 @@ +// 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 +} diff --git a/ghostty/shaders/ripple_cursor.glsl b/ghostty/shaders/ripple_cursor.glsl new file mode 100644 index 0000000..1ca5a7e --- /dev/null +++ b/ghostty/shaders/ripple_cursor.glsl @@ -0,0 +1,132 @@ +// 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 +} diff --git a/ghostty/shaders/ripple_rectangle_cursor.glsl b/ghostty/shaders/ripple_rectangle_cursor.glsl new file mode 100644 index 0000000..7b1aa34 --- /dev/null +++ b/ghostty/shaders/ripple_rectangle_cursor.glsl @@ -0,0 +1,138 @@ +// 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 +} diff --git a/ghostty/shaders/sonic_boom_cursor.glsl b/ghostty/shaders/sonic_boom_cursor.glsl new file mode 100644 index 0000000..39355d1 --- /dev/null +++ b/ghostty/shaders/sonic_boom_cursor.glsl @@ -0,0 +1,148 @@ +// 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); + } +} |
