251 lines
7 KiB
TypeScript
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);
|
|
};
|
|
};
|