Spaces:
Sleeping
Sleeping
File size: 6,594 Bytes
78475cb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | // This creates a WebGL canvas overlay that applies the Trinitron shader to the final scaled output
const vertexShaderSource = `
attribute vec2 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
}
`;
const fragmentShaderSource = `
precision mediump float;
uniform sampler2D u_texture;
uniform vec2 u_resolution;
uniform float u_time;
varying vec2 v_texCoord;
#define PI 3.14159265359
// Random noise function for static
float random(vec2 co) {
return fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453);
}
void main() {
vec2 uv = v_texCoord;
// CRT curvature - subtle but noticeable
vec2 centered = uv - 0.5;
float curvature = 0.06; // Curvature amount (reduced by half from 0.12)
// Apply barrel distortion
float r2 = centered.x * centered.x + centered.y * centered.y;
float distortion = 1.0 + curvature * r2;
vec2 curvedUV = centered * distortion + 0.5;
// Check if we're outside the original screen bounds (black borders)
if (curvedUV.x < 0.0 || curvedUV.x > 1.0 || curvedUV.y < 0.0 || curvedUV.y > 1.0) {
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
return;
}
vec3 color = texture2D(u_texture, curvedUV).rgb;
// Add static noise - using larger blocks for grainier effect
// Divide by 4.0 to make static "pixels" 4x4 screen pixels
vec2 staticCoord = floor(gl_FragCoord.xy / 4.0);
float staticNoise = random(staticCoord + vec2(u_time * 100.0)) * 0.06; // Slightly lower intensity
color += vec3(staticNoise);
// Add flicker (brightness variation over time) - multiple frequencies for realism
float flicker = sin(u_time * 12.0) * 0.015 + sin(u_time * 5.7) * 0.0125 + sin(u_time * 23.3) * 0.0075;
color *= (1.0 + flicker);
// 480i scanline effect - simulating classic CRT TV
float scanline = gl_FragCoord.y;
// Calculate scanline width to achieve ~480 scanlines for current resolution
// For 556px height, we want 480 scanlines: 556/480 ≈ 1.16 pixels per scanline
float scanlineWidth = 2.0;
float scanlineIntensity = 0.7; // How dark the scanlines are
float scanlineMod = mod(scanline, scanlineWidth);
// Make scanline darker for half the pixels
float scanlineFactor = 1.0;
if (scanlineMod < 1.0) {
scanlineFactor = 1.0 - scanlineIntensity;
}
// Apply scanlines
color *= scanlineFactor;
// Slight bloom/glow on bright areas (CRT phosphor persistence)
float brightness = (color.r + color.g + color.b) / 3.0;
color *= 1.0 + (brightness * 0.15);
// Vignette (darker edges like a CRT tube)
float vignette = 1.0 - dot(centered, centered) * 0.4;
color *= vignette;
// Slight color shift for CRT feel
color.r *= 1.02;
color.b *= 0.98;
gl_FragColor = vec4(color, 1.0);
}
`;
export function createShaderOverlay(gameCanvas) {
console.log('Creating shader overlay for canvas:', gameCanvas);
// Create overlay canvas
const overlay = document.createElement('canvas');
overlay.style.position = 'absolute';
overlay.style.pointerEvents = 'none';
overlay.style.zIndex = '1000';
// Position it over the game canvas
const updateOverlayPosition = () => {
const rect = gameCanvas.getBoundingClientRect();
overlay.style.left = rect.left + 'px';
overlay.style.top = rect.top + 'px';
overlay.width = rect.width;
overlay.height = rect.height;
overlay.style.width = rect.width + 'px';
overlay.style.height = rect.height + 'px';
};
document.body.appendChild(overlay);
updateOverlayPosition();
// Update on resize
window.addEventListener('resize', updateOverlayPosition);
const gl = overlay.getContext('webgl') || overlay.getContext('experimental-webgl');
if (!gl) {
console.error('WebGL not supported');
return null;
}
console.log('WebGL context created, overlay size:', overlay.width, 'x', overlay.height);
// Compile shaders
function compileShader(source, type) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('Shader compile error:', gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
const vertexShader = compileShader(vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Program link error:', gl.getProgramInfoLog(program));
return null;
}
gl.useProgram(program);
// Set up geometry (flip Y coordinate for texture)
const positions = new Float32Array([
-1, -1, 0, 1,
1, -1, 1, 1,
-1, 1, 0, 0,
1, 1, 1, 0
]);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
const positionLoc = gl.getAttribLocation(program, 'a_position');
const texCoordLoc = gl.getAttribLocation(program, 'a_texCoord');
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 16, 0);
gl.enableVertexAttribArray(texCoordLoc);
gl.vertexAttribPointer(texCoordLoc, 2, gl.FLOAT, false, 16, 8);
// Create texture from game canvas
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
const resolutionLoc = gl.getUniformLocation(program, 'u_resolution');
const timeLoc = gl.getUniformLocation(program, 'u_time');
const borderWidthLoc = gl.getUniformLocation(program, 'u_borderWidth');
// Render loop
function render() {
updateOverlayPosition();
// Copy game canvas to texture
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, gameCanvas);
// Set uniforms
gl.uniform2f(resolutionLoc, overlay.width, overlay.height);
gl.uniform1f(timeLoc, performance.now() / 1000);
// Draw
gl.viewport(0, 0, overlay.width, overlay.height);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
requestAnimationFrame(render);
}
render();
return overlay;
}
|