dotfiles_chezmoi/dot_config/ghostty/shaders/geocities.glsl
2026-07-13 15:12:44 -04:00

202 lines
6.7 KiB
GLSL

// geocities.glsl — a tribute to the golden age of blinking, tiled,
// under-construction personal homepages. Caution-stripe wallpaper,
// a rainbow sparkle field with hard blink (not a gentle twinkle),
// a scrolling pixel-font "UNDER CONSTRUCTION" marquee, and a bouncing
// rotating star badge, DVD-logo style.
// transparent background
const bool transparent = true;
// terminal contents luminance threshold to be considered background (0.0 to 1.0)
const float threshold = 0.02;
// show/hide individual gimmicks
const bool showStripes = true;
const bool showSparkles = true;
const bool showMarquee = true;
const bool showBadge = true;
// caution-stripe wallpaper opacity (0.0 to 1.0)
const float stripeOpacity = 0.02;
// sparkle field brightness
const float sparkleIntensity = 0.1;
// marquee scroll speed, in pixels per second
const float scrollSpeed = 90.0;
// ---------------------------------------------------------------
// helpers
// ---------------------------------------------------------------
float luminance(vec3 color) {
return dot(color, vec3(0.2126, 0.7152, 0.0722));
}
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
float hash21(vec2 p) {
p = fract(p * vec2(233.34, 851.73));
p += dot(p, p + 23.45);
return fract(p.x * p.y);
}
// 0 -> 1 -> 0 triangle wave, used to ping-pong the bouncing badge
float triangleWave(float x, float period) {
float t = fract(x / period);
return 1.0 - abs(2.0 * t - 1.0);
}
// ---------------------------------------------------------------
// tiny 5x7 pixel font, just enough glyphs to spell "UNDER CONSTRUCTION"
// each row is packed as a 5-bit value, bit weight 16 = leftmost column
// ---------------------------------------------------------------
const int FONT_ROWS = 7;
const int FONT[77] = int[77](
0, 0, 0, 0, 0, 0, 0, // 0: space
17, 17, 17, 17, 17, 17, 14, // 1: U
17, 25, 21, 21, 19, 17, 17, // 2: N
30, 17, 17, 17, 17, 17, 30, // 3: D
31, 16, 16, 30, 16, 16, 31, // 4: E
30, 17, 17, 30, 20, 18, 17, // 5: R
15, 16, 16, 16, 16, 16, 15, // 6: C
14, 17, 17, 17, 17, 17, 14, // 7: O
15, 16, 16, 14, 1, 1, 30, // 8: S
31, 4, 4, 4, 4, 4, 4, // 9: T
31, 4, 4, 4, 4, 4, 31 // 10: I
);
// "UNDER CONSTRUCTION" as indices into the glyph table above
const int MSG_LEN = 18;
const int MSG[18] = int[18](1, 2, 3, 4, 5, 0, 6, 7, 2, 8, 9, 5, 1, 6, 9, 10, 7, 2);
float fontBit(int code, int row, int col) {
int rowVal = FONT[code * FONT_ROWS + row];
float shifted = floor(float(rowVal) / pow(2.0, float(4 - col)));
return mod(shifted, 2.0);
}
// ---------------------------------------------------------------
// background gimmicks
// ---------------------------------------------------------------
vec3 hazardStripes(vec2 fragCoord) {
float stripeWidth = 18.0;
float diag = (fragCoord.x + fragCoord.y) + iTime * 40.0;
float m = mod(diag, stripeWidth * 2.0);
vec3 yellow = vec3(1.0, 0.82, 0.0);
vec3 blk = vec3(0.05);
return m < stripeWidth ? yellow : blk;
}
vec3 sparkleStars(vec2 uv, vec2 resolution) {
vec2 gridUV = uv;
gridUV.x *= resolution.x / resolution.y;
gridUV *= 26.0;
vec2 ipos = floor(gridUV);
vec2 fpos = fract(gridUV);
// only a fraction of cells host a star
float rnd = hash21(ipos);
if (rnd > 0.12) return vec3(0.0);
vec2 center = vec2(hash21(ipos + 3.1), hash21(ipos + 7.7));
float d = length(fpos - center);
// hard on/off blink rather than a smooth twinkle, tacky on purpose
float phase = hash21(ipos + 11.0) * 10.0;
float blink = step(0.5, fract(iTime * (1.5 + rnd * 4.0) + phase));
float glow = smoothstep(0.12, 0.0, d);
vec3 hue = hsv2rgb(vec3(fract(rnd * 4.0 + iTime * 0.1), 1.0, 1.0));
return hue * glow * blink;
}
vec3 starBadge(vec2 fragCoord, vec2 resolution) {
float radius = 26.0;
float px = mix(radius, resolution.x - radius, triangleWave(iTime * 0.09, 1.0));
float py = mix(radius, resolution.y - radius, triangleWave(iTime * 0.13 + 0.37, 1.0));
vec2 p = fragCoord - vec2(px, py);
float d = length(p);
float a = atan(p.y, p.x) - iTime * 2.0;
float starR = radius * (0.55 + 0.45 * cos(a * 5.0));
float shape = smoothstep(1.5, -1.5, d - starR);
vec3 hue = hsv2rgb(vec3(fract(iTime * 0.3), 1.0, 1.0));
return hue * shape;
}
// returns rgb + coverage alpha for the scrolling marquee ticker
vec4 marquee(vec2 fragCoord, vec2 resolution) {
float scale = clamp(resolution.y / 220.0, 2.0, 6.0);
float charW = 6.0 * scale; // 5 columns + 1 spacing column
float charH = 8.0 * scale; // 7 rows + 1 spacing row
int gap = 6; // blank characters between loops of the message
int totalChars = MSG_LEN + gap;
// band hugs the bottom edge of the terminal
float bandY0 = resolution.y - charH - scale * 2.0;
float localY = fragCoord.y - bandY0;
if (localY < 0.0 || localY >= charH) return vec4(0.0);
int row = int(floor(localY / scale));
if (row >= FONT_ROWS) return vec4(0.0);
float scrollX = fragCoord.x + iTime * scrollSpeed;
float totalWidth = float(totalChars) * charW;
float wrapped = mod(scrollX, totalWidth);
int charIndex = int(floor(wrapped / charW));
if (charIndex >= MSG_LEN) return vec4(0.0); // in the gap, blank
float localX = mod(wrapped, charW);
int col = int(floor(localX / scale));
if (col >= 5) return vec4(0.0);
int code = MSG[charIndex];
if (fontBit(code, row, col) < 0.5) return vec4(0.0);
vec3 color = hsv2rgb(vec3(fract(float(charIndex) / float(MSG_LEN) + iTime * 0.15), 1.0, 1.0));
return vec4(color, 1.0);
}
// ---------------------------------------------------------------
// main
// ---------------------------------------------------------------
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
vec4 terminalColor = texture(iChannel0, uv);
vec3 deco = vec3(0.0);
if (showStripes) {
deco += hazardStripes(fragCoord) * stripeOpacity;
}
if (showSparkles) {
deco += sparkleStars(uv, iResolution.xy) * sparkleIntensity;
}
if (showBadge) {
deco = max(deco, starBadge(fragCoord, iResolution.xy));
}
if (showMarquee) {
vec4 marq = marquee(fragCoord, iResolution.xy);
deco = mix(deco, marq.rgb, marq.a);
}
if (transparent) {
deco += terminalColor.rgb;
}
// only show the effect where the terminal is background (not text)
float mask = 1.0 - step(threshold, luminance(terminalColor.rgb));
vec3 blended = mix(terminalColor.rgb, deco, mask);
fragColor = vec4(blended, terminalColor.a);
}