273 lines
6.6 KiB
TypeScript
273 lines
6.6 KiB
TypeScript
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<ShaderBackgroundProps> = ({
|
|
theme,
|
|
}) => {
|
|
const canvasRef = useRef<HTMLCanvasElement>(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 (
|
|
<canvas
|
|
ref={canvasRef}
|
|
id="blackhole_canvas"
|
|
style={{
|
|
width: "100vw",
|
|
height: "100vh",
|
|
position: "fixed",
|
|
top: 0,
|
|
left: 0,
|
|
zIndex: -1,
|
|
}}
|
|
/>
|
|
);
|
|
};
|