import { useEffect, useRef } from "react"; import BLACKHOLE_SHADER_CODE from "./assets/blackhole.glsl?raw"; import STAR_SHADER_CODE from "./assets/star.glsl?raw"; import BALL_SHADER_CODE from "./assets/ball.glsl?raw"; import REFLECT_BALL_SHADER_CODE from "./assets/reflect.glsl?raw"; import CLOUDS_SHADER_CODE from "./assets/clouds.glsl?raw"; function buildProgram( ctx: WebGL2RenderingContext, fragShader: WebGLShader, vertexShader: WebGLShader ) { const program = ctx.createProgram(); if (!program) return null; ctx.attachShader(program, fragShader); ctx.attachShader(program, vertexShader); ctx.linkProgram(program); const status = ctx.getProgramParameter(program, ctx.LINK_STATUS); if (status) { return program; } console.error(ctx.getProgramInfoLog(program)); ctx.deleteProgram(program); return null; } function buildShader( ctx: WebGL2RenderingContext, type: number, source: string ) { const shader = ctx.createShader(type); if (!shader) return null; ctx.shaderSource(shader, source); ctx.compileShader(shader); const status = ctx.getShaderParameter(shader, ctx.COMPILE_STATUS); if (status) { return shader; } console.error(ctx.getShaderInfoLog(shader)); ctx.deleteShader(shader); return null; } function loadTexture(gl: WebGL2RenderingContext, url: string) { const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); // Because images have to be downloaded over the internet // they might take a moment until they are ready. // Until then put a single pixel in the texture so we can // use it immediately. When the image has finished downloading // we'll update the texture with the contents of the image. const level = 0; const internalFormat = gl.RGBA; const width = 1; const height = 1; const border = 0; const srcFormat = gl.RGBA; const srcType = gl.UNSIGNED_BYTE; const pixel = new Uint8Array([0, 0, 255, 255]); // opaque blue gl.texImage2D( gl.TEXTURE_2D, level, internalFormat, width, height, border, srcFormat, srcType, pixel ); const image = new Image(); image.onload = () => { gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D( gl.TEXTURE_2D, level, internalFormat, srcFormat, srcType, image ); // WebGL1 has different requirements for power of 2 images // vs. non power of 2 images so check if the image is a // power of 2 in both dimensions. if (isPowerOf2(image.width) && isPowerOf2(image.height)) { // Yes, it's a power of 2. Generate mips. gl.generateMipmap(gl.TEXTURE_2D); } else { // No, it's not a power of 2. Turn off mips and set // wrapping to clamp to edge 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); } }; image.src = url; return texture; } function isPowerOf2(value: number) { return (value & (value - 1)) === 0; } type ShaderBackgroundProps = { theme: string; }; export const ShaderBackground: React.FC = ({ theme, }) => { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const gl = canvas.getContext("webgl2"); if (!gl) return; gl.viewport(0, 0, canvas.width, canvas.height); gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); let shader_code; switch (theme) { case "blackhole": shader_code = BLACKHOLE_SHADER_CODE; break; case "star": shader_code = STAR_SHADER_CODE; break; case "ball": shader_code = BALL_SHADER_CODE; break; case "reflect": shader_code = REFLECT_BALL_SHADER_CODE; break; case "clouds": shader_code = CLOUDS_SHADER_CODE; break; default: return; } const shader = buildShader(gl, gl.FRAGMENT_SHADER, shader_code); const vertexShader = buildShader( gl, gl.VERTEX_SHADER, `#version 300 es precision highp float; in vec2 a_position; void main() { gl_Position = vec4(a_position, 0.0, 1.0); }` ); if (!shader || !vertexShader) return; const finalProgram = buildProgram(gl, shader, vertexShader); if (!finalProgram) return; const posBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer); gl.bufferData( gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW ); gl.useProgram(finalProgram); const loc = gl.getAttribLocation(finalProgram, "a_position"); gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(loc); function setResolution(program: WebGLProgram) { const loc = gl!.getUniformLocation(program, "iResolution"); canvas!.width = window.visualViewport!.width; canvas!.height = window.visualViewport!.height; if (loc) gl!.uniform3fv(loc, [ window.visualViewport!.width, window.visualViewport!.height, 1, ]); } function setTime(program: WebGLProgram, now: number) { const loc = gl!.getUniformLocation(program, "iTime"); if (loc) gl!.uniform1f(loc, now); } const final_iChannel1 = gl.getUniformLocation(finalProgram, "iChannel1"); const ichannel1_texture = loadTexture(gl, "assets/small_noise.png"); gl!.viewport( 0, 0, window.visualViewport!.width, window.visualViewport!.height ); gl!.useProgram(finalProgram); setResolution(finalProgram!); // Bind Texture to Unit 1 gl!.activeTexture(gl!.TEXTURE1); gl!.bindTexture(gl!.TEXTURE_2D, ichannel1_texture); gl!.uniform1i(final_iChannel1, 1); let last_update: number = 0; const compiled_theme = theme; function update(now: number) { // time in seconds const time = now / 1000; if (compiled_theme !== theme) { return; } if (time - last_update < (1 / 30)) { requestAnimationFrame(update); return; }; last_update = time; gl!.clear(gl!.COLOR_BUFFER_BIT); setTime(finalProgram!, time); gl!.drawArrays(gl!.TRIANGLES, 0, 6); requestAnimationFrame(update); } requestAnimationFrame(update); }, [theme]); return ( ); };