// CyberQueer glow shader for Ghostty's custom-shader hook. // Real per-pixel bloom (unlike kitty, which can only fake this with a static // background image — see ../../kitty/scanlines.png). Deliberately // color-preserving: it blooms whatever color a glyph already is, so the // suite's violet/crimson/neon ANSI palette blooms in its own hues instead of // a single hardcoded tint. // // Only one custom-shader reliably renders on Linux/OpenGL (stacking multiple // silently drops all but the last), so bloom + scanlines + grain + glitch + // the boot intro all live here as a single pass rather than several files. // // Four effects, composited in order: // 1. occasional glitch/roll burst (analog-signal tear + vertical roll + // chromatic aberration), ported from the technique in // github.com/0xhckr/ghostty-shaders/blob/main/glitchy.glsl // 2. bloom (existing, sampled post-glitch-warp) // 3. scanlines + ambient grain (existing) // 4. a one-time hologram boot intro: the picture resolves out of colored // static via a downward sweep over the first ~1.8s of iTime. // ---------- shared noise helpers (glitch §1 + intro §4) ---------- // SS: a windowed pulse — 0 outside [a,b], rising then falling inside it. #define SS(a, b, x) (smoothstep(a, b, x) * smoothstep(b, a, x)) #define UI0 1597334673u #define UI1 3812015801u #define UI2 uvec2(UI0, UI1) #define UI3 uvec3(UI0, UI1, 2798796415u) #define UIF (1.0 / float(0xffffffffu)) // Hash by David Hoskins — reused from glitchy.glsl for the glitch burst's // analog distortion and the intro's static field. vec3 hash33(vec3 p) { uvec3 q = uvec3(ivec3(p)) * UI3; q = (q.x ^ q.y ^ q.z) * UI3; return -1.0 + 2.0 * vec3(q) * UIF; } // Gradient noise by iq — continuous in space and time, so animating it // reads as organic shimmer rather than a hard per-frame strobe (deliberate: // this drives a full-window effect, so avoiding flicker matters). float gnoise(vec3 x) { vec3 p = floor(x); vec3 w = fract(x); vec3 u = w * w * w * (w * (w * 6.0 - 15.0) + 10.0); vec3 ga = hash33(p + vec3(0.0, 0.0, 0.0)); vec3 gb = hash33(p + vec3(1.0, 0.0, 0.0)); vec3 gc = hash33(p + vec3(0.0, 1.0, 0.0)); vec3 gd = hash33(p + vec3(1.0, 1.0, 0.0)); vec3 ge = hash33(p + vec3(0.0, 0.0, 1.0)); vec3 gf = hash33(p + vec3(1.0, 0.0, 1.0)); vec3 gg = hash33(p + vec3(0.0, 1.0, 1.0)); vec3 gh = hash33(p + vec3(1.0, 1.0, 1.0)); float va = dot(ga, w - vec3(0.0, 0.0, 0.0)); float vb = dot(gb, w - vec3(1.0, 0.0, 0.0)); float vc = dot(gc, w - vec3(0.0, 1.0, 0.0)); float vd = dot(gd, w - vec3(1.0, 1.0, 0.0)); float ve = dot(ge, w - vec3(0.0, 0.0, 1.0)); float vf = dot(gf, w - vec3(1.0, 0.0, 1.0)); float vg = dot(gg, w - vec3(0.0, 1.0, 1.0)); float vh = dot(gh, w - vec3(1.0, 1.0, 1.0)); return 2.0 * (va + u.x * (vb - va) + u.y * (vc - va) + u.z * (ve - va) + u.x * u.y * (va - vb - vc + vd) + u.y * u.z * (va - vc - ve + vg) + u.z * u.x * (va - vb - ve + vf) + u.x * u.y * u.z * (-va + vb + vc - vd + ve - vf - vg + vh)); } float gnoise01(vec3 x) { return 0.5 + 0.5 * gnoise(x); } // Golden-spiral sample offsets + inverse-distance weights for the bloom, // reused verbatim from the community bloom kernel (0xhckr/ghostty-shaders). const vec3[24] samples = vec3[24]( vec3(0.1693761725038636, 0.9855514761735895, 1.0), vec3(-1.333070830962943, 0.4721463328627773, 0.7071067811865475), vec3(-0.8464394909806497, -1.51113870578065, 0.5773502691896258), vec3(1.554155680728463, -1.2588090085709776, 0.5), vec3(1.681364377589461, 1.4741145918052656, 0.4472135954999579), vec3(-1.2795157692199817, 2.088741103228784, 0.4082482904638631), vec3(-2.4575847530631187, -0.9799373355024756, 0.3779644730092272), vec3(0.5874641440200847, -2.7667464429345077, 0.35355339059327373), vec3(2.997715703369726, 0.11704939884745152, 0.3333333333333333), vec3(0.41360842451688395, 3.1351121305574803, 0.31622776601683794), vec3(-3.167149933769243, 0.9844599011770256, 0.30151134457776363), vec3(-1.5736713846521535, -3.0860263079123245, 0.2886751345948129), vec3(2.888202648340422, -2.1583061557896213, 0.2773500981126146), vec3(2.7150778983300325, 2.5745586041105715, 0.2672612419124244), vec3(-2.1504069972377464, 3.2211410627650165, 0.2581988897471611), vec3(-3.6548858794907493, -1.6253643308191343, 0.25), vec3(1.0130775986052671, -3.9967078676335834, 0.24253562503633297), vec3(4.229723673607257, 0.33081361055181563, 0.23570226039551587), vec3(0.40107790291173834, 4.340407413572593, 0.22941573387056174), vec3(-4.319124570236028, 1.159811599693438, 0.22360679774997896), vec3(-1.9209044802827355, -4.160543952132907, 0.2182178902359924), vec3(3.8639122286635708, -2.6589814382925123, 0.21320071635561041), vec3(3.3486228404946234, 3.4331800232609, 0.20851441405707477), vec3(-2.8769733643574344, 3.9652268864187157, 0.20412414523193154) ); float lum(vec4 c) { return 0.299 * c.r + 0.587 * c.g + 0.114 * c.b; } // Cheap 2D hash for the ambient grain speckles — no texture lookup needed. float hash(vec2 p) { p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); } // How often (seconds) an occasional glitch/roll burst fires, and what // fraction of that cycle it's actually active for — "occasional" means // rare and brief, not a constant tic. #define GLITCH_DURATION 18.0 #define GLITCH_AMT 0.045 // How long the hologram takes to resolve out of static when it first boots. #define INTRO_DURATION 1.8 void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = fragCoord.xy / iResolution.xy; float t = iTime; // --- 1. occasional glitch/roll burst --- // glitchAmount is ~0 almost all the time and spikes for under a second // once every GLITCH_DURATION seconds. float glitchAmount = SS(GLITCH_DURATION * 0.002, GLITCH_DURATION * GLITCH_AMT, mod(t, GLITCH_DURATION)); float rowY = fragCoord.y; float tear = gnoise(vec3(0.0, rowY * 0.01, t * 500.0)) * (glitchAmount * 0.03 + 0.0015); float roll = glitchAmount * gnoise(vec3(t * 3.0, 0.0, 0.0)) * 0.05; vec2 guv = uv + vec2(tear, roll); guv.y = fract(guv.y); // vertical roll wraps top/bottom, like a CRT losing vertical hold vec2 caOffset = vec2(2.0 / iResolution.x, 0.0) * (1.0 + glitchAmount * 3.0); vec4 color; color.r = texture(iChannel0, guv + caOffset).r; color.g = texture(iChannel0, guv).g; color.b = texture(iChannel0, guv - caOffset).b; color.a = texture(iChannel0, guv).a; // --- 2. bloom: color-preserving glow around bright glyphs --- vec2 bstep = vec2(1.414) / iResolution.xy; for (int i = 0; i < 24; i++) { vec3 s = samples[i]; vec4 c = texture(iChannel0, guv + s.xy * bstep); float l = lum(c); if (l > 0.25) { color += l * s.z * c * 0.15; } } // burst noise: a brief flash of static riding on top of the tear/roll float burst = hash33(vec3(fragCoord, mod(float(iFrame), 1000.0))).r; color.rgb += burst * glitchAmount * 0.35; // --- 3. scanlines: a hard-edged dark line every 3rd physical row --- float scan = mod(fragCoord.y, 3.0) < 1.0 ? 0.90 : 1.0; color.rgb *= scan; // holographic grain: sparse, low-alpha, slowly time-varying speckles in // the theme's accent/violet/danger hues. Update rate capped at ~8Hz and // intensity kept low deliberately — this sits behind text all day, so it // should read as ambient shimmer, not a strobe. vec2 grainCell = floor(fragCoord.xy / 2.0) + floor(iTime * 8.0); float n = hash(grainCell); if (n > 0.985) { vec3 speck = n > 0.9925 ? vec3(0.894, 0.0, 0.271) // accent E40046 : n > 0.99 ? vec3(0.961, 0.020, 0.020) // danger F50505 : vec3(0.314, 0.094, 0.867); // violet 5018DD color.rgb += speck * 0.10; } // --- 4. hologram boot intro: resolve out of colored static over // INTRO_DURATION seconds via a downward sweep, then never run again for // the life of the shader (t only grows). Uses gnoise (continuous) rather // than a per-frame hash flip, so the static shimmers instead of strobing. float introProgress = clamp(t / INTRO_DURATION, 0.0, 1.0); float sweepFrac = introProgress * 1.15 - 0.05; float resolved = 1.0 - smoothstep(sweepFrac - 0.05, sweepFrac + 0.05, uv.y); float n1 = gnoise01(vec3(fragCoord * 0.08, t * 6.0)); float n2 = gnoise01(vec3(fragCoord * 0.08 + 19.0, t * 6.0)); vec3 violet = vec3(0.314, 0.094, 0.867); vec3 accent = vec3(0.894, 0.0, 0.271); vec3 danger = vec3(0.961, 0.020, 0.020); vec3 introField = mix(violet, accent, n1); introField = mix(introField, danger, smoothstep(0.75, 1.0, n2) * 0.5); introField *= (0.18 + 0.22 * n2); // keep it dim — ambient static, not a flash float sweepBand = 1.0 - smoothstep(0.0, 0.12, abs(uv.y - sweepFrac)); vec3 sweepGlow = violet * sweepBand * 1.2; vec3 finalRGB = mix(introField, color.rgb, resolved) + sweepGlow; fragColor = vec4(finalRGB, color.a); }