| 1 | // Vibe coded with AI, manually tuned randomness shader + opacity. |
| 2 | (globalThis as any).canvas_2024 = function(canvas: HTMLCanvasElement) { |
| 3 | const isStandalone = canvas.getAttribute("data-standalone") === "true"; |
| 4 | if (isStandalone) { |
| 5 | canvas.parentElement!.style.backgroundColor = "black"; |
| 6 | } |
| 7 | |
| 8 | const gl = canvas.getContext("webgl", { |
| 9 | alpha: true, |
| 10 | premultipliedAlpha: false, |
| 11 | }); |
| 12 | if (!gl) { |
| 13 | console.error("WebGL not supported"); |
| 14 | return () => {}; |
| 15 | } |
| 16 | |
| 17 | canvas.style.imageRendering = "pixelated"; |
| 18 | canvas.style.opacity = isStandalone ? "0.3" : "0.15"; |
| 19 | |
| 20 | // Resize canvas to match display size |
| 21 | const resize = () => { |
| 22 | const displayWidth = Math.floor( |
| 23 | (canvas.clientWidth || window.innerWidth) / 3, |
| 24 | ); |
| 25 | const displayHeight = Math.floor( |
| 26 | (canvas.clientHeight || window.innerHeight) / 3, |
| 27 | ); |
| 28 | |
| 29 | if (canvas.width !== displayWidth || canvas.height !== displayHeight) { |
| 30 | canvas.width = displayWidth; |
| 31 | canvas.height = displayHeight; |
| 32 | gl.viewport(0, 0, canvas.width, canvas.height); |
| 33 | } |
| 34 | }; |
| 35 | resize(); |
| 36 | |
| 37 | // Vertex shader (just passes coordinates) |
| 38 | const vertexShaderSource = ` |
| 39 | attribute vec2 a_position; |
| 40 | void main() { |
| 41 | gl_Position = vec4(a_position, 0.0, 1.0); |
| 42 | } |
| 43 | `; |
| 44 | |
| 45 | // Fragment shader creates random noise with higher opacity to ensure visibility |
| 46 | const fragmentShaderSource = ` |
| 47 | precision mediump float; |
| 48 | uniform float u_time; |
| 49 | |
| 50 | float noise1(float seed1,float seed2){ |
| 51 | return( |
| 52 | fract(seed1+12.34567* |
| 53 | fract(100.*(abs(seed1*0.91)+seed2+94.68)* |
| 54 | fract((abs(seed2*0.41)+45.46)* |
| 55 | fract((abs(seed2)+757.21)* |
| 56 | fract(seed1*0.0171)))))) |
| 57 | * 1.0038 - 0.00185; |
| 58 | } |
| 59 | |
| 60 | float n(float seed1, float seed2, float seed3){ |
| 61 | float buff1 = abs(seed1+100.81) + 1000.3; |
| 62 | float buff2 = abs(seed2+100.45) + 1000.2; |
| 63 | float buff3 = abs(noise1(seed1, seed2)+seed3) + 1000.1; |
| 64 | buff1 = (buff3*fract(buff2*fract(buff1*fract(buff2*0.146)))); |
| 65 | buff2 = (buff2*fract(buff2*fract(buff1+buff2*fract(buff3*0.52)))); |
| 66 | buff1 = noise1(buff1, buff2); |
| 67 | return(buff1); |
| 68 | } |
| 69 | |
| 70 | void main() { |
| 71 | float noise = n(gl_FragCoord.x, gl_FragCoord.y, u_time); |
| 72 | |
| 73 | gl_FragColor = vec4(1.0, 0.7, 0.7, 0.8*noise); |
| 74 | } |
| 75 | `; |
| 76 | |
| 77 | // Create and compile shaders |
| 78 | const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource); |
| 79 | const fragmentShader = createShader( |
| 80 | gl, |
| 81 | gl.FRAGMENT_SHADER, |
| 82 | fragmentShaderSource, |
| 83 | ); |
| 84 | |
| 85 | // Check if shader creation failed |
| 86 | if (!vertexShader || !fragmentShader) { |
| 87 | console.error("Failed to create shaders"); |
| 88 | return () => {}; |
| 89 | } |
| 90 | |
| 91 | // Create program and link shaders |
| 92 | const program = createProgram(gl, vertexShader, fragmentShader); |
| 93 | |
| 94 | // Check if program creation failed |
| 95 | if (!program) { |
| 96 | console.error("Failed to create program"); |
| 97 | return () => {}; |
| 98 | } |
| 99 | |
| 100 | // Get attribute and uniform locations |
| 101 | const positionAttributeLocation = gl.getAttribLocation(program, "a_position"); |
| 102 | const timeUniformLocation = gl.getUniformLocation(program, "u_time"); |
| 103 | |
| 104 | // Create a position buffer for a rectangle covering the entire canvas |
| 105 | const positionBuffer = gl.createBuffer(); |
| 106 | if (!positionBuffer) { |
| 107 | console.error("Failed to create position buffer"); |
| 108 | return () => {}; |
| 109 | } |
| 110 | |
| 111 | gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); |
| 112 | // Rectangle that covers the entire clip space |
| 113 | const positions = [ |
| 114 | -1.0, |
| 115 | -1.0, // bottom left |
| 116 | 1.0, |
| 117 | -1.0, // bottom right |
| 118 | -1.0, |
| 119 | 1.0, // top left |
| 120 | 1.0, |
| 121 | 1.0, // top right |
| 122 | ]; |
| 123 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); |
| 124 | |
| 125 | // Set up blending |
| 126 | gl.enable(gl.BLEND); |
| 127 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); |
| 128 | |
| 129 | // Fixed 24 FPS timing |
| 130 | const FPS = 24; |
| 131 | const FRAME_TIME = 1000 / FPS; // ms per frame |
| 132 | |
| 133 | // Handle animation |
| 134 | let animationTimerId: number; |
| 135 | let startTime = Date.now(); |
| 136 | let lastFrameTime = 0; |
| 137 | |
| 138 | const render = () => { |
| 139 | // Get current time |
| 140 | const currentTime = Date.now(); |
| 141 | const deltaTime = currentTime - lastFrameTime; |
| 142 | |
| 143 | // Skip frame if it's too early (maintain 24 FPS) |
| 144 | if (deltaTime < FRAME_TIME) { |
| 145 | animationTimerId = window.setTimeout(render, 0); // Check again ASAP but yield to browser |
| 146 | return; |
| 147 | } |
| 148 | |
| 149 | // Update last frame time, accounting for any drift |
| 150 | lastFrameTime = currentTime - (deltaTime % FRAME_TIME); |
| 151 | |
| 152 | // Resize canvas if needed |
| 153 | resize(); |
| 154 | |
| 155 | // Calculate elapsed time in seconds for animation |
| 156 | const elapsedTime = (currentTime - startTime) / 1000; |
| 157 | |
| 158 | // Clear the canvas with transparent black |
| 159 | gl.clearColor(0, 0, 0, 0); |
| 160 | gl.clear(gl.COLOR_BUFFER_BIT); |
| 161 | |
| 162 | // Use our shader program |
| 163 | gl.useProgram(program); |
| 164 | |
| 165 | // Set up the position attribute |
| 166 | gl.enableVertexAttribArray(positionAttributeLocation); |
| 167 | gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); |
| 168 | gl.vertexAttribPointer( |
| 169 | positionAttributeLocation, |
| 170 | 2, // 2 components per vertex |
| 171 | gl.FLOAT, // data type |
| 172 | false, // normalize |
| 173 | 0, // stride (0 = compute from size and type) |
| 174 | 0, // offset |
| 175 | ); |
| 176 | |
| 177 | // Update time uniform for animation |
| 178 | gl.uniform1f(timeUniformLocation, elapsedTime); |
| 179 | |
| 180 | // Draw the rectangle (2 triangles) |
| 181 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); |
| 182 | |
| 183 | // Schedule next frame (aiming for 24 FPS) |
| 184 | const timeToNextFrame = Math.max( |
| 185 | 0, |
| 186 | FRAME_TIME - (Date.now() - currentTime), |
| 187 | ); |
| 188 | animationTimerId = window.setTimeout(render, timeToNextFrame); |
| 189 | }; |
| 190 | |
| 191 | // Helper function to create shaders |
| 192 | function createShader( |
| 193 | gl: WebGLRenderingContext, |
| 194 | type: number, |
| 195 | source: string, |
| 196 | ): WebGLShader | null { |
| 197 | const shader = gl.createShader(type); |
| 198 | if (!shader) { |
| 199 | console.error("Failed to create shader object"); |
| 200 | return null; |
| 201 | } |
| 202 | |
| 203 | gl.shaderSource(shader, source); |
| 204 | gl.compileShader(shader); |
| 205 | |
| 206 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { |
| 207 | console.error("Shader compilation error:", gl.getShaderInfoLog(shader)); |
| 208 | gl.deleteShader(shader); |
| 209 | return null; |
| 210 | } |
| 211 | |
| 212 | return shader; |
| 213 | } |
| 214 | |
| 215 | // Helper function to create program and link shaders |
| 216 | function createProgram( |
| 217 | gl: WebGLRenderingContext, |
| 218 | vertexShader: WebGLShader, |
| 219 | fragmentShader: WebGLShader, |
| 220 | ): WebGLProgram | null { |
| 221 | const program = gl.createProgram(); |
| 222 | if (!program) { |
| 223 | console.error("Failed to create program object"); |
| 224 | return null; |
| 225 | } |
| 226 | |
| 227 | gl.attachShader(program, vertexShader); |
| 228 | gl.attachShader(program, fragmentShader); |
| 229 | gl.linkProgram(program); |
| 230 | |
| 231 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { |
| 232 | console.error("Program linking error:", gl.getProgramInfoLog(program)); |
| 233 | return null; |
| 234 | } |
| 235 | |
| 236 | return program; |
| 237 | } |
| 238 | |
| 239 | // Start the rendering with initial timestamp |
| 240 | lastFrameTime = Date.now(); |
| 241 | render(); |
| 242 | |
| 243 | // Return cleanup function |
| 244 | return () => { |
| 245 | clearTimeout(animationTimerId); |
| 246 | if (program) gl.deleteProgram(program); |
| 247 | if (vertexShader) gl.deleteShader(vertexShader); |
| 248 | if (fragmentShader) gl.deleteShader(fragmentShader); |
| 249 | if (positionBuffer) gl.deleteBuffer(positionBuffer); |
| 250 | }; |
| 251 | }; |