153 lines
4.7 KiB
JavaScript
153 lines
4.7 KiB
JavaScript
|
|
// 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);
|
||
|
|
}
|