par-term 0.35.1

Cross-platform GPU-accelerated terminal emulator with inline graphics support (Sixel, iTerm2, Kitty)
/*! par-term shader metadata
name: cursor_orbit
author: null
description: null
version: 1.0.0
defaults:
  animation_speed: null
  brightness: null
  channel0: null
  channel1: null
  channel2: null
  channel3: null
  cubemap: null
  cubemap_enabled: null
  use_background_as_channel0: null
  hides_cursor: true
  glow_radius: null
  glow_intensity: null
  trail_duration: null
  cursor_color: null
*/

// Orbiting Ball Cursor Shader
// A ball that traces the inside edge of the cursor cell with a fading trail
//
// Usage: Set in config.yaml:
//   cursor_shader: "cursor_orbit.glsl"
//   cursor_shader_enabled: true
//   cursor_shader_animation: true
//   cursor_shader_hides_cursor: true  # Recommended: hides default cursor

// Adjustable parameters
const float ORBIT_SPEED = 2.0;        // Speed of orbit (cycles per second)
const float TRAIL_LENGTH = 0.8;       // Trail length (0.0 - 1.0, fraction of orbit)
const float BALL_RADIUS = 2.0;        // Ball radius in pixels
// Keep the orbit tight to the cell edge; inset just enough to avoid clipping with AA.
const float EDGE_INSET = BALL_RADIUS + 0.75; // Distance from cell edge in pixels

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    // Sample terminal content
    vec2 uv = fragCoord / iResolution.xy;
    vec4 terminal = texture(iChannel4, uv);

    // Get cursor info
    vec2 cursorPos = iCurrentCursor.xy;
    vec2 cursorSize = iCurrentCursor.zw;

    // Get cursor color and opacity (includes blink animation)
    vec3 ballColor = iCurrentCursorColor.rgb;
    float cursorOpacity = iCurrentCursorColor.a;

    // Start with terminal content
    vec4 result = terminal;

    // Only draw if cursor is visible
    if (cursorOpacity > 0.01) {
        // Calculate the inset rectangle for the orbit path
        vec2 innerMin = cursorPos + vec2(EDGE_INSET);
        vec2 innerMax = cursorPos + cursorSize - vec2(EDGE_INSET);
        vec2 innerSize = innerMax - innerMin;

        // Calculate perimeter of the orbit path
        float perimeter = 2.0 * (innerSize.x + innerSize.y);

        // Current position along perimeter (0 to 1)
        float t = fract(iTime * ORBIT_SPEED);

        // Convert t to distance along perimeter
        float dist = t * perimeter;

        // Calculate ball position along the rectangular path
        // Path goes: right edge down, bottom edge left, left edge up, top edge right
        vec2 ballPos;

        float rightEdge = innerSize.y;
        float bottomEdge = rightEdge + innerSize.x;
        float leftEdge = bottomEdge + innerSize.y;
        // topEdge = perimeter

        if (dist < rightEdge) {
            // Right edge, going down
            ballPos = vec2(innerMax.x, innerMin.y + dist);
        } else if (dist < bottomEdge) {
            // Bottom edge, going left
            ballPos = vec2(innerMax.x - (dist - rightEdge), innerMax.y);
        } else if (dist < leftEdge) {
            // Left edge, going up
            ballPos = vec2(innerMin.x, innerMax.y - (dist - bottomEdge));
        } else {
            // Top edge, going right
            ballPos = vec2(innerMin.x + (dist - leftEdge), innerMin.y);
        }

        // Function to get position along path for a given t value
        // (inline calculation for trail points)
        #define GET_PATH_POS(tVal) ( \
            (tVal * perimeter < rightEdge) ? vec2(innerMax.x, innerMin.y + tVal * perimeter) : \
            (tVal * perimeter < bottomEdge) ? vec2(innerMax.x - (tVal * perimeter - rightEdge), innerMax.y) : \
            (tVal * perimeter < leftEdge) ? vec2(innerMin.x, innerMax.y - (tVal * perimeter - bottomEdge)) : \
            vec2(innerMin.x + (tVal * perimeter - leftEdge), innerMin.y) \
        )

        // Calculate distance from current fragment to ball
        float distToBall = length(fragCoord - ballPos);

        // Draw ball with anti-aliasing
        float aa = 1.5;
        float ballAlpha = 1.0 - smoothstep(BALL_RADIUS - aa, BALL_RADIUS + aa, distToBall);

        // Calculate trail effect
        float trailAlpha = 0.0;

        // Check multiple points along the trail
        for (float i = 1.0; i <= 20.0; i += 1.0) {
            float trailT = t - (i / 20.0) * TRAIL_LENGTH;
            if (trailT < 0.0) trailT += 1.0;  // Wrap around

            // Calculate trail point position
            float trailDist = trailT * perimeter;
            vec2 trailPos;

            if (trailDist < rightEdge) {
                trailPos = vec2(innerMax.x, innerMin.y + trailDist);
            } else if (trailDist < bottomEdge) {
                trailPos = vec2(innerMax.x - (trailDist - rightEdge), innerMax.y);
            } else if (trailDist < leftEdge) {
                trailPos = vec2(innerMin.x, innerMax.y - (trailDist - bottomEdge));
            } else {
                trailPos = vec2(innerMin.x + (trailDist - leftEdge), innerMin.y);
            }

            // Distance to this trail point
            float distToTrail = length(fragCoord - trailPos);

            // Trail gets thinner and more transparent further back
            float trailFade = 1.0 - (i / 20.0);
            float trailRadius = BALL_RADIUS * trailFade;
            float pointAlpha = (1.0 - smoothstep(trailRadius - aa, trailRadius + aa, distToTrail)) * trailFade;

            trailAlpha = max(trailAlpha, pointAlpha);
        }

        // Combine ball and trail
        float totalAlpha = max(ballAlpha, trailAlpha * 0.6);
        totalAlpha *= cursorOpacity;

        // Apply color
        result = mix(result, vec4(ballColor, 1.0), totalAlpha);
    }

    fragColor = result;
}