// 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; }