"use strict"; function main() { // Get a WebGL2 context /** @type {HTMLCanvasElement} */ const canvas = document.querySelector("#canvas"); const gl = canvas.getContext("webgl2"); if (!gl) { return; } //──────────────────────────────────────────────────────────── // Vertex Shader // Simply passes along vertex positions. const vs = `#version 300 es in vec4 a_position; void main() { gl_Position = a_position; } `; //──────────────────────────────────────────────────────────── // Fragment Shader // This shader ray–marches a sphere whose radius is perturbed by multiple // cymatic (sine–based) modes. Its surface is lit with diffuse and specular // shading and decorated with intricate stripe textures and a wild color palette. const fs = `#version 300 es precision highp float; uniform vec2 iResolution; uniform vec2 iMouse; uniform float iTime; out vec4 outColor; // A wild palette function that returns vivid colors. vec3 wildPalette(float t) { vec3 a = vec3(0.5, 0.5, 0.5); vec3 b = vec3(0.5, 0.5, 0.5); vec3 c = vec3(1.0, 1.0, 1.0); vec3 d = vec3(0.0, 0.33, 0.67); return a + b * cos(6.28318 * (c * t + d)); } // The sphere’s surface is modulated by several sine–based modes. // We compute a “displacement” that is added to the base radius. float sphereDisplacement(vec3 p, float t) { float r = length(p); float theta = acos(p.y / r); float phi = atan(p.z, p.x); // Three modes for a rich, cymatic effect: float d1 = sin(3.0 * theta + t) * sin(4.0 * phi + 1.3 * t); float d2 = cos(5.0 * theta - 0.7 * t) * sin(2.0 * phi + 1.1 * t); float d3 = sin(7.0 * theta + 3.0 * t) * cos(6.0 * phi - 2.0 * t); return 0.2 * (d1 + d2 + d3); } // Signed distance function for the deformed (cymatic) sphere. // Base sphere has radius 1.0; its radius is perturbed by sphereDisplacement. float sdCymaticSphere(vec3 p, float t) { return length(p) - (1.0 + sphereDisplacement(p, t)); } // Compute the normal at point p via finite differences of the SDF. vec3 calcNormal(vec3 p, float t) { float eps = 0.001; vec3 n; n.x = sdCymaticSphere(p + vec3(eps, 0.0, 0.0), t) - sdCymaticSphere(p - vec3(eps, 0.0, 0.0), t); n.y = sdCymaticSphere(p + vec3(0.0, eps, 0.0), t) - sdCymaticSphere(p - vec3(0.0, eps, 0.0), t); n.z = sdCymaticSphere(p + vec3(0.0, 0.0, eps), t) - sdCymaticSphere(p - vec3(0.0, 0.0, eps), t); return normalize(n); } // Ray–marching routine to find the intersection of a ray with the deformed sphere. float raymarch(vec3 ro, vec3 rd, float t, out vec3 pos) { float depth = 0.0; for (int i = 0; i < 100; i++) { pos = ro + rd * depth; float dist = sdCymaticSphere(pos, t); if (abs(dist) < 0.001) { return depth; } depth += dist; if (depth >= 20.0) break; } return -1.0; } void main() { // Normalized pixel coordinates (centered on zero) vec2 uv = (gl_FragCoord.xy - 0.5 * iResolution.xy) / iResolution.y; // Use mouse position to control the camera’s azimuth and pitch. float angle = iMouse.x / iResolution.x * 6.28318; float pitch = mix(0.3, 1.2, iMouse.y / iResolution.y); float radius = 4.0; vec3 ro = vec3( radius * cos(pitch) * cos(angle), radius * sin(pitch), radius * cos(pitch) * sin(angle) ); vec3 target = vec3(0.0); // Build a simple camera coordinate system. vec3 forward = normalize(target - ro); vec3 right = normalize(cross(forward, vec3(0.0, 1.0, 0.0))); vec3 up = cross(right, forward); // Compute the ray direction. vec3 rd = normalize(forward + uv.x * right + uv.y * up); // Ray–march the scene. vec3 pos; float d = raymarch(ro, rd, iTime, pos); vec3 color; if (d > 0.0) { // Surface hit: compute normal for lighting. vec3 normal = calcNormal(pos, iTime); vec3 lightDir = normalize(vec3(0.8, 1.0, 0.6)); float diff = max(dot(normal, lightDir), 0.0); vec3 viewDir = normalize(ro - pos); vec3 halfDir = normalize(lightDir + viewDir); float spec = pow(max(dot(normal, halfDir), 0.0), 32.0); // Compute spherical coordinates for the hit point. float rPos = length(pos); float theta = acos(pos.y / rPos); float phi = atan(pos.z, pos.x); // Use the cymatic displacement to drive the wild color palette. float sp = sphereDisplacement(pos, iTime); float factor = sp * 5.0; vec3 baseColor = wildPalette(factor + sin(iTime)); // Add intricate stripe textures via high–frequency sine patterns. float stripes = sin(10.0 * phi + iTime) * sin(10.0 * theta + iTime); baseColor *= 0.5 + 0.5 * stripes; // Combine the base color with lighting. color = baseColor * diff + vec3(0.2) * spec; color = mix(color, baseColor, 0.3); } else { // No hit: use a subtle background gradient. color = mix(vec3(0.0, 0.0, 0.1), vec3(0.0), uv.y + 0.5); } outColor = vec4(color, 1.0); } `; //──────────────────────────────────────────────────────────── // Create and compile the shader program using webgl-utils. const program = webglUtils.createProgramFromSources(gl, [vs, fs]); // Look up attribute and uniform locations. const positionAttributeLocation = gl.getAttribLocation(program, "a_position"); const resolutionLocation = gl.getUniformLocation(program, "iResolution"); const mouseLocation = gl.getUniformLocation(program, "iMouse"); const timeLocation = gl.getUniformLocation(program, "iTime"); // Create a vertex array object (VAO) and bind it. const vao = gl.createVertexArray(); gl.bindVertexArray(vao); // Create a buffer and put a full–screen quad in it. const positionBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bufferData( gl.ARRAY_BUFFER, new Float32Array([ -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1, ]), gl.STATIC_DRAW ); // Enable the attribute. gl.enableVertexAttribArray(positionAttributeLocation); gl.vertexAttribPointer( positionAttributeLocation, 2, // 2 components per vertex gl.FLOAT, // data type is float false, 0, 0 ); //──────────────────────────────────────────────────────────── // Set up mouse/touch interactions. const playpauseElem = document.querySelector(".playpause"); const inputElem = document.querySelector(".divcanvas"); inputElem.addEventListener("mouseover", requestFrame); inputElem.addEventListener("mouseout", cancelFrame); let mouseX = 0; let mouseY = 0; function setMousePosition(e) { const rect = inputElem.getBoundingClientRect(); mouseX = e.clientX - rect.left; mouseY = rect.height - (e.clientY - rect.top) - 1; } inputElem.addEventListener("mousemove", setMousePosition); inputElem.addEventListener("touchstart", (e) => { e.preventDefault(); playpauseElem.classList.add("playpausehide"); requestFrame(); }, { passive: false }); inputElem.addEventListener("touchmove", (e) => { e.preventDefault(); setMousePosition(e.touches[0]); }, { passive: false }); inputElem.addEventListener("touchend", (e) => { e.preventDefault(); playpauseElem.classList.remove("playpausehide"); cancelFrame(); }, { passive: false }); //──────────────────────────────────────────────────────────── // Animation loop. let requestId; function requestFrame() { if (!requestId) { requestId = requestAnimationFrame(render); } } function cancelFrame() { if (requestId) { cancelAnimationFrame(requestId); requestId = undefined; } } let then = 0; let time = 0; function render(now) { requestId = undefined; now *= 0.001; // Convert to seconds. const elapsedTime = Math.min(now - then, 0.1); time += elapsedTime; then = now; webglUtils.resizeCanvasToDisplaySize(gl.canvas); gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); gl.useProgram(program); gl.bindVertexArray(vao); // Set uniforms. gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height); gl.uniform2f(mouseLocation, mouseX, mouseY); gl.uniform1f(timeLocation, time); gl.drawArrays(gl.TRIANGLES, 0, 6); requestFrame(); } requestFrame(); requestAnimationFrame(cancelFrame); } main();