Files
gods-eye-view/src/styles/retro.js
T

179 lines
7.9 KiB
JavaScript

/**
* CRT Terminal — Dynamic CRT Monitor Aesthetic
* Pixelation + Bayer dithering + barrel distortion + scanlines +
* chromatic aberration + vignette + phosphor persistence/ghosting +
* horizontal jitter + flicker + glitch lines + RGB shadow mask subpixels
*
* Exposed uniforms:
* pixelation (1-10) — pixel grid size
* distortion (0-1) — CRT barrel/lens bulge strength
* instability (0-1) — jitter, flicker, glitch frequency
*/
export const retroShader = {
name: 'retro',
uniforms: {
pixelation: { default: 5.0, min: 1, max: 10, label: 'Pixelation' },
distortion: { default: 0, min: 0, max: 1, label: 'Distortion' },
instability: { default: 0.4, min: 0, max: 1, label: 'Instability' },
},
fragmentShader: /* glsl */ `
uniform sampler2D colorTexture;
uniform vec2 colorTextureDimensions;
uniform float intensity;
uniform float pixelation;
uniform float distortion;
uniform float instability;
uniform float time;
in vec2 v_textureCoordinates;
// ── Hash for noise/randomness ─────────────────────────
float hash(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
// 8x8 Bayer dithering matrix (normalized 0-1)
float bayer8(vec2 pos) {
ivec2 p = ivec2(mod(pos, 8.0));
int index = p.x + p.y * 8;
int bayer[64] = int[64](
0, 32, 8, 40, 2, 34, 10, 42,
48, 16, 56, 24, 50, 18, 58, 26,
12, 44, 4, 36, 14, 46, 6, 38,
60, 28, 52, 20, 62, 30, 54, 22,
3, 35, 11, 43, 1, 33, 9, 41,
51, 19, 59, 27, 49, 17, 57, 25,
15, 47, 7, 39, 13, 45, 5, 37,
63, 31, 55, 23, 61, 29, 53, 21
);
return float(bayer[index]) / 64.0;
}
// CRT barrel distortion
vec2 barrelDistort(vec2 uv, float strength) {
vec2 centered = uv * 2.0 - 1.0;
float r2 = dot(centered, centered);
float distort = 1.0 + r2 * strength * 0.4;
centered *= distort;
return centered * 0.5 + 0.5;
}
void main() {
vec2 uv = v_textureCoordinates;
vec2 dims = colorTextureDimensions;
vec2 texel = 1.0 / dims;
// ── Barrel distortion (CRT monitor bulge) ───────────
float dist = distortion * intensity;
vec2 distUV = barrelDistort(uv, dist);
// Black outside the distorted area
if (distUV.x < 0.0 || distUV.x > 1.0 || distUV.y < 0.0 || distUV.y > 1.0) {
out_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
return;
}
// ── Horizontal jitter (random scanline displacement) ──
float lineY = floor(distUV.y * dims.y);
float jitterSeed = hash(vec2(lineY, floor(time * 8.0)));
// Only jitter a few lines at a time (sparse)
float jitterActive = step(0.97 - instability * 0.04, jitterSeed);
float jitterAmount = (hash(vec2(lineY * 7.0, floor(time * 12.0))) - 0.5) *
0.008 * instability * jitterActive * intensity;
vec2 jitteredUV = distUV + vec2(jitterAmount, 0.0);
// ── Chromatic aberration — slight RGB channel offset ──
vec2 centered = jitteredUV - 0.5;
float caStrength = length(centered) * 0.008 * intensity;
float r = texture(colorTexture, jitteredUV + centered * caStrength).r;
float g = texture(colorTexture, jitteredUV).g;
float b = texture(colorTexture, jitteredUV - centered * caStrength).b;
vec4 color = vec4(r, g, b, 1.0);
// ── Pixelation: snap UV to grid ─────────────────────
float pixSize = mix(1.0, pixelation, intensity);
vec2 pixelUV = floor(jitteredUV * dims / pixSize) * pixSize / dims;
vec4 pixelColor = texture(colorTexture, mix(jitteredUV, pixelUV, intensity));
// Blend chromatic aberration with pixelated sample
color = mix(color, pixelColor, 0.7 * intensity);
// ── Bayer dithering before posterization ────────────
vec2 ditherCoord = jitteredUV * dims / pixSize;
float dither = bayer8(ditherCoord) - 0.5;
float ditherAmount = 0.12 * intensity;
vec3 dithered = color.rgb + dither * ditherAmount;
// ── Posterize: reduce color levels ──────────────────
float levels = mix(256.0, 10.0, intensity);
vec3 posterized = floor(dithered * levels + 0.5) / levels;
// ── Slight saturation boost ─────────────────────────
float gray = dot(posterized, vec3(0.299, 0.587, 0.114));
vec3 saturated = mix(vec3(gray), posterized, 1.0 + 0.3 * intensity);
// ── RGB shadow mask subpixel pattern ────────────────
// Each pixel shows faint R/G/B vertical stripes
float subpixelX = mod(distUV.x * dims.x, 3.0);
vec3 subpixelMask = vec3(
smoothstep(0.0, 0.8, 1.0 - abs(subpixelX - 0.5)), // R stripe
smoothstep(0.0, 0.8, 1.0 - abs(subpixelX - 1.5)), // G stripe
smoothstep(0.0, 0.8, 1.0 - abs(subpixelX - 2.5)) // B stripe
);
// Only apply at higher pixelation (visible "pixels")
float subpixelStrength = smoothstep(2.0, 6.0, pixSize) * 0.3 * intensity;
vec3 withSubpixels = mix(saturated, saturated * (subpixelMask * 0.7 + 0.3), subpixelStrength);
// ── Block edge darkening ────────────────────────────
vec2 pixelCenter = fract(distUV * dims / pixSize);
float blockEdge = smoothstep(0.0, 0.08, min(min(pixelCenter.x, 1.0 - pixelCenter.x),
min(pixelCenter.y, 1.0 - pixelCenter.y)));
float edgeFactor = mix(1.0, blockEdge * 0.15 + 0.85, intensity);
vec3 result = withSubpixels * edgeFactor;
// ── Horizontal scanlines — rolling CRT refresh ──────
float scanY = distUV.y * dims.y;
float scanline = sin(scanY * 1.0 + time * 2.5) * 0.5 + 0.5;
scanline = pow(scanline, 1.5);
float scanFade = 0.35 * intensity;
result *= mix(1.0, scanline * scanFade + (1.0 - scanFade), intensity);
// ── Phosphor persistence / ghosting ─────────────────
// Approximate by blurring in a direction (simulates previous frame lingering)
vec3 ghost = texture(colorTexture, jitteredUV - vec2(texel.x * 2.0, 0.0)).rgb;
float ghostGray = dot(ghost, vec3(0.299, 0.587, 0.114));
// Blend a dim ghost of the offset sample
result = mix(result, result + vec3(ghostGray) * 0.08, instability * intensity);
// ── Flicker (overall brightness fluctuation ~50-60Hz) ──
float flicker = sin(time * 188.5) * 0.5 + 0.5; // ~60Hz equivalent
flicker = 1.0 - flicker * 0.03 * instability * intensity; // very subtle
result *= flicker;
// ── Glitch lines (rare horizontal bright bars) ──────
float glitchSeed = hash(vec2(floor(time * 2.0), 0.0));
float glitchLine = step(0.92 - instability * 0.08, glitchSeed);
if (glitchLine > 0.0) {
float glitchY = hash(vec2(floor(time * 2.0), 1.0));
float glitchHit = smoothstep(0.0, 0.003, abs(distUV.y - glitchY));
glitchHit = 1.0 - glitchHit;
result += glitchHit * 0.3 * instability * intensity;
}
// ── Warm phosphor tint (P1 green-amber) ─────────────
vec3 warmTint = result * vec3(1.02, 1.0, 0.94);
result = mix(result, warmTint, 0.4 * intensity);
// ── Edge vignette (darker corners — CRT curvature) ──
vec2 vigUV = distUV * (1.0 - distUV);
float vig = vigUV.x * vigUV.y * 20.0;
vig = clamp(pow(vig, 0.25 + 0.15 * intensity), 0.0, 1.0);
result *= mix(1.0, vig, 0.6 * intensity);
out_FragColor = vec4(mix(texture(colorTexture, uv).rgb, result, intensity), 1.0);
}
`,
};