Spaces:
Sleeping
Sleeping
| // 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; | |
| } | |