Files
waves/waves.html

351 lines
14 KiB
HTML
Raw Normal View History

2025-12-25 10:08:20 +01:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGL FFT Waves</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
font-family: 'Inter', sans-serif;
background-color: #1a202c; /* dark gray */
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
overflow: hidden;
color: white;
}
#webgl-canvas {
background-color: #2d3748; /* dark blue-gray */
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.25);
width: 100%;
max-width: 800px;
height: 600px;
margin: 1rem;
cursor: grab;
}
#webgl-canvas:active {
cursor: grabbing;
}
</style>
</head>
<body>
<div class="p-6 bg-gray-800 rounded-xl shadow-lg m-4 w-full max-w-2xl text-center">
<h1 class="text-2xl font-bold mb-4 text-white">WebGL FFT-Generated Waves (Wireframe)</h1>
<canvas id="webgl-canvas"></canvas>
<p class="text-sm mt-4 text-gray-400">
Click and drag to orbit the camera, and use the mouse scroll to zoom in and out.
</p>
</div>
<script>
// Get the canvas and WebGL context
const canvas = document.getElementById('webgl-canvas');
const gl = canvas.getContext('webgl');
if (!gl) {
console.error("Unable to initialize WebGL. Your browser may not support it.");
const errorMessage = document.createElement('p');
errorMessage.textContent = 'Error: WebGL is not supported in this browser.';
document.body.appendChild(errorMessage);
throw new Error('WebGL not supported.');
}
// --- Shaders ---
// The vertex shader now sums up multiple sine waves to create the complex wave pattern
const vsSource = `
precision mediump float;
attribute vec4 a_position;
uniform mat4 u_modelViewMatrix;
uniform mat4 u_projectionMatrix;
uniform float u_time;
varying vec3 v_color;
#define NUM_WAVES 10 // Number of sine waves to sum
vec3 calculateWave(vec3 p, vec2 direction, float amplitude, float frequency, float speed, float time) {
float wavePhase = dot(direction, p.xz) * frequency + time * speed;
float waveHeight = amplitude * sin(wavePhase);
return vec3(p.x, p.y + waveHeight, p.z);
}
void main() {
vec4 position = a_position;
// Sum multiple sine waves to simulate a complex FFT-like spectrum
float waveHeight = 0.0;
float x = a_position.x;
float z = a_position.z;
// Manually define wave parameters for the sum, with adjusted frequencies for the larger plane
waveHeight += 0.1 * sin(u_time * 1.5 + x * 2.5);
waveHeight += 0.05 * sin(u_time * 2.0 + z * 4.0);
waveHeight += 0.08 * sin(u_time * 1.8 + x * 2.0 + z * 2.0);
waveHeight += 0.03 * sin(u_time * 2.5 + x * -1.5 + z * 3.0);
waveHeight += 0.06 * sin(u_time * 1.2 + x * 3.5 - z * 1.0);
waveHeight += 0.04 * sin(u_time * 2.1 + x * -2.5 - z * 2.5);
// Added smaller waves with higher frequencies and lower amplitudes
waveHeight += 0.02 * sin(u_time * 3.0 + x * 10.0 + z * 5.0);
waveHeight += 0.015 * sin(u_time * 3.5 + x * -8.0 + z * 8.0);
waveHeight += 0.01 * sin(u_time * 4.0 + x * 12.0 - z * 12.0);
position.y += waveHeight;
gl_Position = u_projectionMatrix * u_modelViewMatrix * position;
// Simple coloring based on the wave height for visual effect
float colorFactor = (position.y / 0.5) + 0.5;
v_color = vec3(0.1, 0.4, 0.8) * colorFactor; // Dark blue
}
`;
// The fragment shader remains simple, just coloring the pixels
const fsSource = `
precision mediump float;
varying vec3 v_color;
void main() {
gl_FragColor = vec4(v_color, 1.0);
}
`;
// --- Shader and Program Initialization Functions ---
function compileShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
function initShaderProgram(gl, vsSource, fsSource) {
const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fsSource);
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
console.error('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram));
return null;
}
return shaderProgram;
}
const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
gl.useProgram(shaderProgram);
const programInfo = {
attribs: {
position: gl.getAttribLocation(shaderProgram, 'a_position'),
},
uniforms: {
projectionMatrix: gl.getUniformLocation(shaderProgram, 'u_projectionMatrix'),
modelViewMatrix: gl.getUniformLocation(shaderProgram, 'u_modelViewMatrix'),
time: gl.getUniformLocation(shaderProgram, 'u_time'),
},
};
// --- Geometry ---
// Increase the GRID_SIZE for a larger and more detailed plane
const GRID_SIZE = 100;
const PLANE_SCALE = 2.0; // The scale of the plane
const vertexData = [];
const indices = [];
// Generate vertices
for (let i = 0; i < GRID_SIZE; i++) {
for (let j = 0; j < GRID_SIZE; j++) {
const x = ((j / (GRID_SIZE - 1)) * 2 - 1) * PLANE_SCALE;
const z = ((i / (GRID_SIZE - 1)) * 2 - 1) * PLANE_SCALE;
const y = 0;
vertexData.push(x, y, z);
}
}
// Generate indices for lines to create a wireframe grid
for (let i = 0; i < GRID_SIZE - 1; i++) {
for (let j = 0; j < GRID_SIZE - 1; j++) {
const i1 = i * GRID_SIZE + j;
const i2 = i * GRID_SIZE + j + 1;
const i3 = (i + 1) * GRID_SIZE + j;
// Horizontal line
indices.push(i1, i2);
// Vertical line
indices.push(i1, i3);
}
}
// Add the last row and column lines
for (let i = 0; i < GRID_SIZE - 1; i++) {
const lastRowIndex = i * GRID_SIZE + (GRID_SIZE - 1);
indices.push(lastRowIndex, (i + 1) * GRID_SIZE + (GRID_SIZE - 1));
const lastColIndex = (GRID_SIZE - 1) * GRID_SIZE + i;
indices.push(lastColIndex, lastColIndex + 1);
}
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexData), gl.STATIC_DRAW);
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(programInfo.attribs.position, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(programInfo.attribs.position);
// --- Camera & Matrix Functions ---
let isDragging = false;
let prevMouseX = 0;
let prevMouseY = 0;
let cameraRadius = 5.0; // Increased radius to view the larger plane
let alpha = Math.PI / 4;
let beta = Math.PI / 4;
canvas.addEventListener('mousedown', (e) => {
isDragging = true;
prevMouseX = e.clientX;
prevMouseY = e.clientY;
});
canvas.addEventListener('mouseup', () => {
isDragging = false;
});
canvas.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const deltaX = e.clientX - prevMouseX;
const deltaY = e.clientY - prevMouseY;
alpha += deltaX * 0.005;
beta += deltaY * 0.005;
beta = Math.max(-Math.PI / 2 + 0.1, Math.min(Math.PI / 2 - 0.1, beta));
prevMouseX = e.clientX;
prevMouseY = e.clientY;
});
// Add a wheel event listener for zooming
canvas.addEventListener('wheel', (e) => {
e.preventDefault(); // Prevent page scroll
const zoomSpeed = 0.1;
if (e.deltaY > 0) {
// Zoom out
cameraRadius += zoomSpeed;
} else {
// Zoom in, but don't go too close
cameraRadius -= zoomSpeed;
cameraRadius = Math.max(1.0, cameraRadius); // Minimum radius of 1.0
}
});
const fieldOfView = 45 * Math.PI / 180;
let aspect = canvas.clientWidth / canvas.clientHeight;
const zNear = 0.1;
const zFar = 100.0;
const projectionMatrix = new Float32Array(16);
function perspective(out, fovy, aspect, near, far) {
const f = 1.0 / Math.tan(fovy / 2);
out[0] = f / aspect; out[1] = 0; out[2] = 0; out[3] = 0;
out[4] = 0; out[5] = f; out[6] = 0; out[7] = 0;
out[8] = 0; out[9] = 0; out[10] = (near + far) / (near - far); out[11] = -1;
out[12] = 0; out[13] = 0; out[14] = (2 * far * near) / (near - far); out[15] = 0;
}
function lookAt(out, eye, center, up) {
let x0, x1, x2, y0, y1, y2, z0, z1, z2, len;
let eyex = eye[0]; let eyey = eye[1]; let eyez = eye[2];
let upx = up[0]; let upy = up[1]; let upz = up[2];
let centerx = center[0]; let centery = center[1]; let centerz = center[2];
z0 = eyex - centerx; z1 = eyey - centery; z2 = eyez - centerz;
len = 1 / Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2);
z0 *= len; z1 *= len; z2 *= len;
x0 = upy * z2 - upz * z1; x1 = upz * z0 - upx * z2; x2 = upx * z1 - upy * z0;
len = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2);
if (!len) {
x0 = 0; x1 = 0; x2 = 0;
} else {
len = 1 / len;
x0 *= len; x1 *= len; x2 *= len;
}
y0 = z1 * x2 - z2 * x1; y1 = z2 * x0 - z0 * x2; y2 = z0 * x1 - z1 * x0;
len = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2);
if (!len) {
y0 = 0; y1 = 0; y2 = 0;
} else {
len = 1 / len;
y0 *= len; y1 *= len; y2 *= len;
}
out[0] = x0; out[1] = y0; out[2] = z0; out[3] = 0;
out[4] = x1; out[5] = y1; out[6] = z1; out[7] = 0;
out[8] = x2; out[9] = y2; out[10] = z2; out[11] = 0;
out[12] = -(x0 * eyex + x1 * eyey + x2 * eyez);
out[13] = -(y0 * eyex + y1 * eyey + y2 * eyez);
out[14] = -(z0 * eyex + z1 * eyey + z2 * eyez);
out[15] = 1;
}
// --- Animation Loop ---
let then = 0;
function render(now) {
now *= 0.001;
const deltaTime = now - then;
then = now;
const displayWidth = canvas.clientWidth;
const displayHeight = canvas.clientHeight;
if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
canvas.width = displayWidth;
canvas.height = displayHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
aspect = canvas.clientWidth / canvas.clientHeight;
perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar);
}
gl.clearColor(0.1, 0.1, 0.1, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.enable(gl.DEPTH_TEST);
// Calculate camera position based on current angles and radius
const eye = [
cameraRadius * Math.sin(beta) * Math.sin(alpha),
cameraRadius * Math.cos(beta),
cameraRadius * Math.sin(beta) * Math.cos(alpha)
];
const center = [0, 0, 0];
const up = [0, 1, 0];
// Create the view matrix using the lookAt function
const modelViewMatrix = new Float32Array(16);
lookAt(modelViewMatrix, eye, center, up);
gl.uniformMatrix4fv(programInfo.uniforms.projectionMatrix, false, projectionMatrix);
gl.uniformMatrix4fv(programInfo.uniforms.modelViewMatrix, false, modelViewMatrix);
gl.uniform1f(programInfo.uniforms.time, now);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.drawElements(gl.LINES, indices.length, gl.UNSIGNED_SHORT, 0);
requestAnimationFrame(render);
}
window.onload = function() {
requestAnimationFrame(render);
};
</script>
</body>
</html>