// WebGL Newton fractal renderer (z^3 - 1, ocean palette) // Ported from crates/fractal-engine/src/{newton.rs, color.rs} function initFractal(canvasId) { var canvas = document.getElementById(canvasId); if (!canvas) return; var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); if (!gl) { // Fallback: ocean-themed CSS gradient canvas.style.background = 'radial-gradient(ellipse at center, #0d3842 0%, #0c2e58 40%, #060a10 100%)'; return; } // --- Shaders --- var VERT_SRC = [ 'attribute vec2 a_pos;', 'void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }' ].join('\n'); var FRAG_SRC = [ 'precision highp float;', 'uniform float u_time;', 'uniform vec2 u_resolution;', '', 'void main() {', ' vec2 uv = gl_FragCoord.xy / u_resolution;', ' float aspect = u_resolution.x / u_resolution.y;', '', ' // Map to complex plane (view_size=3.0, centered)', ' float zr = (uv.x - 0.5) * 3.0 * aspect;', ' float zi = (uv.y - 0.5) * 3.0;', '', ' // Rotate by time (animation)', ' float ct = cos(u_time);', ' float st = sin(u_time);', ' float tmp = zr * ct - zi * st;', ' zi = zr * st + zi * ct;', ' zr = tmp;', '', ' // Newton iteration for z^3 - 1', ' int rootIndex = 0;', ' int iters = 0;', ' for (int i = 0; i < 32; i++) {', ' iters = i;', ' float zr2 = zr*zr - zi*zi;', ' float zi2 = 2.0*zr*zi;', ' float zr3 = zr*zr2 - zi*zi2;', ' float zi3 = zr*zi2 + zi*zr2;', '', ' float d0 = (zr-1.0)*(zr-1.0) + zi*zi;', ' float d1 = (zr+0.5)*(zr+0.5) + (zi-0.866025)*(zi-0.866025);', ' float d2 = (zr+0.5)*(zr+0.5) + (zi+0.866025)*(zi+0.866025);', '', ' if (d0 < 1e-6) { rootIndex = 0; break; }', ' if (d1 < 1e-6) { rootIndex = 1; break; }', ' if (d2 < 1e-6) { rootIndex = 2; break; }', '', ' float nr = zr3 - 1.0;', ' float ni = zi3;', ' float dr = 3.0*zr2;', ' float di = 3.0*zi2;', ' float denom = dr*dr + di*di;', ' if (denom < 1e-12) break;', ' zr -= (nr*dr + ni*di) / denom;', ' zi -= (ni*dr - nr*di) / denom;', ' }', '', ' // Fallback: closest root', ' float d0 = (zr-1.0)*(zr-1.0) + zi*zi;', ' float d1 = (zr+0.5)*(zr+0.5) + (zi-0.866025)*(zi-0.866025);', ' float d2 = (zr+0.5)*(zr+0.5) + (zi+0.866025)*(zi+0.866025);', ' if (d1 < d0 && d1 < d2) rootIndex = 1;', ' else if (d2 < d0) rootIndex = 2;', '', ' // Ocean palette', ' float brightness = max(1.0 - float(iters)/32.0 * 0.7, 0.3);', ' vec3 color;', ' if (rootIndex == 0) color = vec3(0.05, 0.25, 0.55);', ' else if (rootIndex == 1) color = vec3(0.0, 0.45, 0.50);', ' else color = vec3(0.15, 0.55, 0.65);', '', ' gl_FragColor = vec4(color * brightness, 1.0);', '}' ].join('\n'); // --- Compile helpers --- function compile(type, src) { var s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) { console.error('Shader compile error:', gl.getShaderInfoLog(s)); return null; } return s; } var vs = compile(gl.VERTEX_SHADER, VERT_SRC); var fs = compile(gl.FRAGMENT_SHADER, FRAG_SRC); if (!vs || !fs) return; var prog = gl.createProgram(); gl.attachShader(prog, vs); gl.attachShader(prog, fs); gl.linkProgram(prog); if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { console.error('Program link error:', gl.getProgramInfoLog(prog)); return; } gl.useProgram(prog); // --- Full-screen triangle --- var buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 3,-1, -1,3]), gl.STATIC_DRAW); var aPos = gl.getAttribLocation(prog, 'a_pos'); gl.enableVertexAttribArray(aPos); gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0); var uTime = gl.getUniformLocation(prog, 'u_time'); var uRes = gl.getUniformLocation(prog, 'u_resolution'); // --- Resize --- function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; gl.viewport(0, 0, canvas.width, canvas.height); } resize(); window.addEventListener('resize', resize); // --- Animation loop --- var startTime = performance.now(); var animationSpeed = 0.0001; function frame() { var t = (performance.now() - startTime) * animationSpeed; gl.uniform1f(uTime, t); gl.uniform2f(uRes, canvas.width, canvas.height); gl.drawArrays(gl.TRIANGLES, 0, 3); requestAnimationFrame(frame); } requestAnimationFrame(frame); }