zpsajst's picture
Initial commit with environment variables for API keys
2398be6
raw
history blame
11.2 kB
"use client";
import React, { useRef, useEffect } from 'react';
import { cn } from '@/lib/utils';
interface ShaderBackgroundProps {
children?: React.ReactNode;
blur?: boolean;
overlay?: boolean;
overlayOpacity?: number;
className?: string;
}
// Minimal WebGL renderer wrapper (keeps original behavior but avoids leaking GLSL into TS scope)
class WebGLRenderer {
private canvas: HTMLCanvasElement;
private gl: WebGL2RenderingContext;
private program: WebGLProgram | null = null;
private vs: WebGLShader | null = null;
private fs: WebGLShader | null = null;
private buffer: WebGLBuffer | null = null;
private scale: number;
private shaderSource: string;
private mouseMove: [number, number] = [0, 0];
private mouseCoords: [number, number] = [0, 0];
private pointerCoords: number[] = [0, 0];
private nbrOfPointers = 0;
private vertexSrc = `#version 300 es
precision highp float;
in vec4 position;
void main(){gl_Position=position;}`;
private vertices = [-1, 1, -1, -1, 1, 1, 1, -1];
constructor(canvas: HTMLCanvasElement, scale: number, shaderSource: string) {
this.canvas = canvas;
this.scale = scale;
this.gl = canvas.getContext('webgl2')!;
this.gl.viewport(0, 0, canvas.width * scale, canvas.height * scale);
this.shaderSource = shaderSource;
}
updateShader(source: string) {
this.reset();
this.shaderSource = source;
this.setup();
this.init();
}
updateMove(deltas: [number, number]) {
this.mouseMove = deltas;
}
updateMouse(coords: [number, number]) {
this.mouseCoords = coords;
}
updatePointerCoords(coords: number[]) {
this.pointerCoords = coords;
}
updatePointerCount(nbr: number) {
this.nbrOfPointers = nbr;
}
updateScale(scale: number) {
this.scale = scale;
this.gl.viewport(0, 0, this.canvas.width * scale, this.canvas.height * scale);
}
compile(shader: WebGLShader, source: string) {
const gl = this.gl;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const error = gl.getShaderInfoLog(shader);
console.error('Shader compilation error:', error);
}
}
test(source: string) {
let result = null;
const gl = this.gl;
const shader = gl.createShader(gl.FRAGMENT_SHADER)!;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
result = gl.getShaderInfoLog(shader);
}
gl.deleteShader(shader);
return result;
}
reset() {
const gl = this.gl;
if (this.program && !gl.getProgramParameter(this.program, gl.DELETE_STATUS)) {
if (this.vs) {
gl.detachShader(this.program, this.vs);
gl.deleteShader(this.vs);
}
if (this.fs) {
gl.detachShader(this.program, this.fs);
gl.deleteShader(this.fs);
}
gl.deleteProgram(this.program);
}
}
setup() {
const gl = this.gl;
this.vs = gl.createShader(gl.VERTEX_SHADER)!;
this.fs = gl.createShader(gl.FRAGMENT_SHADER)!;
this.compile(this.vs, this.vertexSrc);
this.compile(this.fs, this.shaderSource);
this.program = gl.createProgram()!;
gl.attachShader(this.program, this.vs);
gl.attachShader(this.program, this.fs);
gl.linkProgram(this.program);
if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(this.program));
}
}
init() {
const gl = this.gl;
const program = this.program!;
this.buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.vertices), gl.STATIC_DRAW);
const position = gl.getAttribLocation(program, 'position');
gl.enableVertexAttribArray(position);
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
(program as any).resolution = gl.getUniformLocation(program, 'resolution');
(program as any).time = gl.getUniformLocation(program, 'time');
(program as any).move = gl.getUniformLocation(program, 'move');
(program as any).touch = gl.getUniformLocation(program, 'touch');
(program as any).pointerCount = gl.getUniformLocation(program, 'pointerCount');
(program as any).pointers = gl.getUniformLocation(program, 'pointers');
}
render(now = 0) {
const gl = this.gl;
const program = this.program;
if (!program || gl.getProgramParameter(program, gl.DELETE_STATUS)) return;
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
gl.uniform2f((program as any).resolution, this.canvas.width, this.canvas.height);
gl.uniform1f((program as any).time, now * 1e-3);
gl.uniform2f((program as any).move, ...this.mouseMove);
gl.uniform2f((program as any).touch, ...this.mouseCoords);
gl.uniform1i((program as any).pointerCount, this.nbrOfPointers);
gl.uniform2fv((program as any).pointers, this.pointerCoords);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
}
// Pointer Handler class
class PointerHandler {
private scale: number;
private active = false;
private pointers = new Map<number, [number, number]>();
private lastCoords: [number, number] = [0, 0];
private moves: [number, number] = [0, 0];
constructor(element: HTMLCanvasElement, scale: number) {
this.scale = scale;
const map = (element: HTMLCanvasElement, scale: number, x: number, y: number): [number, number] =>
[x * scale, element.height - y * scale];
element.addEventListener('pointerdown', (e) => {
this.active = true;
this.pointers.set(e.pointerId, map(element, this.getScale(), e.clientX, e.clientY));
});
element.addEventListener('pointerup', (e) => {
if (this.count === 1) {
this.lastCoords = this.first;
}
this.pointers.delete(e.pointerId);
this.active = this.pointers.size > 0;
});
element.addEventListener('pointerleave', (e) => {
if (this.count === 1) {
this.lastCoords = this.first;
}
this.pointers.delete(e.pointerId);
this.active = this.pointers.size > 0;
});
element.addEventListener('pointermove', (e) => {
if (!this.active) return;
this.lastCoords = [e.clientX, e.clientY];
this.pointers.set(e.pointerId, map(element, this.getScale(), e.clientX, e.clientY));
this.moves = [this.moves[0] + e.movementX, this.moves[1] + e.movementY];
});
}
getScale() {
return this.scale;
}
updateScale(scale: number) {
this.scale = scale;
}
get count() {
return this.pointers.size;
}
get move(): [number, number] {
return this.moves;
}
get coords(): number[] {
return this.pointers.size > 0
? Array.from(this.pointers.values()).flat()
: [0, 0];
}
get first(): [number, number] {
return (this.pointers.values().next().value as [number, number]) || this.lastCoords;
}
}
const defaultShaderSource = `#version 300 es
/*********
* made by Matthias Hurrle (@atzedent)
*/
precision highp float;
out vec4 O;
uniform vec2 resolution;
uniform float time;
#define FC gl_FragCoord.xy
#define T time
#define R resolution
#define MN min(R.x,R.y)
float rnd(vec2 p) {
p=fract(p*vec2(12.9898,78.233));
p+=dot(p,p+34.56);
return fract(p.x*p.y);
}
float noise(in vec2 p) {
vec2 i=floor(p), f=fract(p), u=f*f*(3.-2.*f);
float
a=rnd(i),
b=rnd(i+vec2(1,0)),
c=rnd(i+vec2(0,1)),
d=rnd(i+1.);
return mix(mix(a,b,u.x),mix(c,d,u.x),u.y);
}
float fbm(vec2 p) {
float t=.0, a=1.; mat2 m=mat2(1.,-.5,.2,1.2);
for (int i=0; i<5; i++) {
t+=a*noise(p);
p*=2.*m;
a*=.5;
}
return t;
}
float clouds(vec2 p) {
float d=1., t=.0;
for (float i=.0; i<3.; i++) {
float a=d*fbm(i*10.+p.x*.2+.2*(1.+i)*p.y+d+i*i+p);
t=mix(t,d,a);
d=a;
p*=2./(i+1.);
}
return t;
}
void main(void) {
vec2 uv=(FC-.5*R)/MN,st=uv*vec2(2,1);
vec3 col=vec3(0);
float bg=clouds(vec2(st.x+T*.5,-st.y));
uv*=1.-.3*(sin(T*.2)*.5+.5);
for (float i=1.; i<12.; i++) {
uv+=.1*cos(i*vec2(.1+.01*i, .8)+i*i+T*.5+.1*uv.x);
vec2 p=uv;
float d=length(p);
col+=.00125/d*(cos(sin(i)*vec3(1,2,3))+1.);
float b=noise(i+p+bg*1.731);
col+=.002*b/length(max(p,vec2(b*p.x*.02,p.y)));
col=mix(col,vec3(bg*.25,bg*.137,bg*.05),d);
}
O=vec4(col,1);
}`;
export const ShaderBackground: React.FC<ShaderBackgroundProps> = ({
children,
blur = false,
overlay = false,
overlayOpacity = 0.7,
className = ""
}) => {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const animationFrameRef = useRef<number | null>(null);
const rendererRef = useRef<WebGLRenderer | null>(null);
const pointersRef = useRef<PointerHandler | null>(null);
const resize = () => {
if (!canvasRef.current) return;
const canvas = canvasRef.current;
const dpr = Math.max(1, 0.5 * window.devicePixelRatio);
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
if (rendererRef.current) {
rendererRef.current.updateScale(dpr);
}
};
const loop = (now: number) => {
if (!rendererRef.current || !pointersRef.current) return;
rendererRef.current.updateMouse(pointersRef.current.first);
rendererRef.current.updatePointerCount(pointersRef.current.count);
rendererRef.current.updatePointerCoords(pointersRef.current.coords);
rendererRef.current.updateMove(pointersRef.current.move);
rendererRef.current.render(now);
animationFrameRef.current = requestAnimationFrame(loop);
};
useEffect(() => {
if (!canvasRef.current) return;
const canvas = canvasRef.current;
const dpr = Math.max(1, 0.5 * window.devicePixelRatio);
rendererRef.current = new WebGLRenderer(canvas, dpr, defaultShaderSource);
pointersRef.current = new PointerHandler(canvas, dpr);
rendererRef.current.setup();
rendererRef.current.init();
resize();
if (rendererRef.current.test(defaultShaderSource) === null) {
rendererRef.current.updateShader(defaultShaderSource);
}
loop(0);
window.addEventListener('resize', resize);
return () => {
window.removeEventListener('resize', resize);
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
if (rendererRef.current) {
rendererRef.current.reset();
}
};
}, []);
return (
<div className={cn("relative w-full h-full", className)}>
<canvas
ref={canvasRef}
className={cn(
"absolute inset-0 w-full h-full object-cover touch-none z-0",
blur && "blur-sm"
)}
style={{ background: 'black' }}
/>
{overlay && (
<div
className="absolute inset-0 bg-black/70 backdrop-blur-sm z-10 pointer-events-none"
style={{ opacity: overlayOpacity }}
/>
)}
{children && (
<div className="relative z-20 w-full h-full">
{children}
</div>
)}
</div>
);
};