sitegen/src/file-viewer/scripts/canvas_2024.client.ts

251 lines
7 KiB
TypeScript

// Vibe coded with AI, manually tuned randomness shader + opacity.
(globalThis as any).canvas_2024 = function (canvas: HTMLCanvasElement) {
const isStandalone = canvas.getAttribute("data-standalone") === "true";
if (isStandalone) {
canvas.parentElement!.style.backgroundColor = "black";
}
const gl = canvas.getContext("webgl", {
alpha: true,
premultipliedAlpha: false,
});
if (!gl) {
console.error("WebGL not supported");
return () => {};
}
canvas.style.imageRendering = "pixelated";
canvas.style.opacity = isStandalone ? "0.3" : "0.15";
// Resize canvas to match display size
const resize = () => {
const displayWidth = Math.floor(
(canvas.clientWidth || window.innerWidth) / 3,
);
const displayHeight = Math.floor(
(canvas.clientHeight || window.innerHeight) / 3,
);
if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
canvas.width = displayWidth;
canvas.height = displayHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
}
};
resize();
// Vertex shader (just passes coordinates)
const vertexShaderSource = `
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}
`;
// Fragment shader creates random noise with higher opacity to ensure visibility
const fragmentShaderSource = `
precision mediump float;
uniform float u_time;
float noise1(float seed1,float seed2){
return(
fract(seed1+12.34567*
fract(100.*(abs(seed1*0.91)+seed2+94.68)*
fract((abs(seed2*0.41)+45.46)*
fract((abs(seed2)+757.21)*
fract(seed1*0.0171))))))
* 1.0038 - 0.00185;
}
float n(float seed1, float seed2, float seed3){
float buff1 = abs(seed1+100.81) + 1000.3;
float buff2 = abs(seed2+100.45) + 1000.2;
float buff3 = abs(noise1(seed1, seed2)+seed3) + 1000.1;
buff1 = (buff3*fract(buff2*fract(buff1*fract(buff2*0.146))));
buff2 = (buff2*fract(buff2*fract(buff1+buff2*fract(buff3*0.52))));
buff1 = noise1(buff1, buff2);
return(buff1);
}
void main() {
float noise = n(gl_FragCoord.x, gl_FragCoord.y, u_time);
gl_FragColor = vec4(1.0, 0.7, 0.7, 0.8*noise);
}
`;
// Create and compile shaders
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(
gl,
gl.FRAGMENT_SHADER,
fragmentShaderSource,
);
// Check if shader creation failed
if (!vertexShader || !fragmentShader) {
console.error("Failed to create shaders");
return () => {};
}
// Create program and link shaders
const program = createProgram(gl, vertexShader, fragmentShader);
// Check if program creation failed
if (!program) {
console.error("Failed to create program");
return () => {};
}
// Get attribute and uniform locations
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
const timeUniformLocation = gl.getUniformLocation(program, "u_time");
// Create a position buffer for a rectangle covering the entire canvas
const positionBuffer = gl.createBuffer();
if (!positionBuffer) {
console.error("Failed to create position buffer");
return () => {};
}
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// Rectangle that covers the entire clip space
const positions = [
-1.0,
-1.0, // bottom left
1.0,
-1.0, // bottom right
-1.0,
1.0, // top left
1.0,
1.0, // top right
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
// Set up blending
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// Fixed 24 FPS timing
const FPS = 24;
const FRAME_TIME = 1000 / FPS; // ms per frame
// Handle animation
let animationTimerId: number;
let startTime = Date.now();
let lastFrameTime = 0;
const render = () => {
// Get current time
const currentTime = Date.now();
const deltaTime = currentTime - lastFrameTime;
// Skip frame if it's too early (maintain 24 FPS)
if (deltaTime < FRAME_TIME) {
animationTimerId = window.setTimeout(render, 0); // Check again ASAP but yield to browser
return;
}
// Update last frame time, accounting for any drift
lastFrameTime = currentTime - (deltaTime % FRAME_TIME);
// Resize canvas if needed
resize();
// Calculate elapsed time in seconds for animation
const elapsedTime = (currentTime - startTime) / 1000;
// Clear the canvas with transparent black
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
// Use our shader program
gl.useProgram(program);
// Set up the position attribute
gl.enableVertexAttribArray(positionAttributeLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(
positionAttributeLocation,
2, // 2 components per vertex
gl.FLOAT, // data type
false, // normalize
0, // stride (0 = compute from size and type)
0, // offset
);
// Update time uniform for animation
gl.uniform1f(timeUniformLocation, elapsedTime);
// Draw the rectangle (2 triangles)
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
// Schedule next frame (aiming for 24 FPS)
const timeToNextFrame = Math.max(
0,
FRAME_TIME - (Date.now() - currentTime),
);
animationTimerId = window.setTimeout(render, timeToNextFrame);
};
// Helper function to create shaders
function createShader(
gl: WebGLRenderingContext,
type: number,
source: string,
): WebGLShader | null {
const shader = gl.createShader(type);
if (!shader) {
console.error("Failed to create shader object");
return null;
}
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error("Shader compilation error:", gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
// Helper function to create program and link shaders
function createProgram(
gl: WebGLRenderingContext,
vertexShader: WebGLShader,
fragmentShader: WebGLShader,
): WebGLProgram | null {
const program = gl.createProgram();
if (!program) {
console.error("Failed to create program object");
return null;
}
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error("Program linking error:", gl.getProgramInfoLog(program));
return null;
}
return program;
}
// Start the rendering with initial timestamp
lastFrameTime = Date.now();
render();
// Return cleanup function
return () => {
clearTimeout(animationTimerId);
if (program) gl.deleteProgram(program);
if (vertexShader) gl.deleteShader(vertexShader);
if (fragmentShader) gl.deleteShader(fragmentShader);
if (positionBuffer) gl.deleteBuffer(positionBuffer);
};
};