diff --git a/routes/contact/index.html b/routes/contact/index.html index 45faaa7..0e2d27e 100644 --- a/routes/contact/index.html +++ b/routes/contact/index.html @@ -20,6 +20,7 @@ } #fractal-canvas { + image-rendering: auto; position: fixed; top: 0; left: 0; @@ -36,7 +37,6 @@ flex-direction: column; align-items: center; justify-content: center; - animation: fadeInUp 800ms ease 300ms both; } .glass-card { @@ -135,7 +135,15 @@ - - + diff --git a/routes/contact/message/index.html b/routes/contact/message/index.html index 568f946..9bd5026 100644 --- a/routes/contact/message/index.html +++ b/routes/contact/message/index.html @@ -23,6 +23,7 @@ } #fractal-canvas { + image-rendering: auto; position: fixed; top: 0; left: 0; @@ -45,14 +46,12 @@ max-width: 560px; min-height: 400px; margin-bottom: 20px; - animation: fadeInUp 800ms ease 300ms both; } .back-link { position: relative; z-index: 1; margin-top: 0.5rem; - animation: fadeInUp 800ms ease 500ms both; } .back-link a { @@ -324,7 +323,15 @@ back to contact - - + diff --git a/routes/root/dist.png b/routes/root/dist.png new file mode 100644 index 0000000..e40d654 Binary files /dev/null and b/routes/root/dist.png differ diff --git a/routes/root/fractal-gl.js b/routes/root/fractal-gl.js index e0ad4fb..4ad4e89 100644 --- a/routes/root/fractal-gl.js +++ b/routes/root/fractal-gl.js @@ -1,20 +1,66 @@ -// WebGL Newton fractal renderer (z^3 - 1, ocean palette) -// Ported from crates/fractal-engine/src/{newton.rs, color.rs} +// Julia distance-isolines background. WebGL preferred, Canvas2D fallback. +// Palette: BSOD-blue, smooth cosine bands, 1024-entry LUT for finer transitions. function initFractal(canvasId) { - var canvas = document.getElementById(canvasId); - if (!canvas) return; + console.log('[fractal-gl] initFractal:', canvasId); - 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%)'; + var canvas = document.getElementById(canvasId); + if (!canvas) { + console.warn('[fractal-gl] canvas not found:', canvasId); return; } - // --- Shaders --- + // Visible base color even if both render paths fail. + document.body.style.background = + 'radial-gradient(ellipse at center, #142566 0%, #0a103a 60%, #060a10 100%)'; + var gl = null; + try { + gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); + } catch (e) { + console.warn('[fractal-gl] WebGL exception:', e); + } + + if (gl) { + console.log('[fractal-gl] using WebGL path'); + initWebGL(canvas, gl); + } else { + console.warn('[fractal-gl] WebGL unavailable; using Canvas2D fallback'); + initCanvas2D(canvas); + } +} + +function _hslToRgb(h, s, l) { + var a = s * Math.min(l, 1 - l); + function f(n) { + var k = (n + h * 12) % 12; + return Math.round((l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1))) * 255); + } + return [f(0), f(8), f(4)]; +} + +// 1024-entry palette: 4x finer than the iteration-count space (0..255), +// which kills visible quantization banding in the band peaks/troughs. +// Cosine band (no abs/pow) → smooth bands; hue pinned ~true blue. +var PALETTE_SIZE = 1024; +function _buildPalette32() { + var p32 = new Uint32Array(PALETTE_SIZE); + for (var i = 0; i < PALETTE_SIZE; i++) { + var f = i / 4; // logical index in [0, 256) + var band = 0.5 + 0.5 * Math.cos(f * 2 * Math.PI / 56); + var h = 0.665 + 0.015 * Math.sin(f * Math.PI / 128); + var s = 0.80; + var l = 0.18 + 0.28 * band; + var rgb = _hslToRgb(h, s, l); + p32[i] = (255 << 24) | (rgb[2] << 16) | (rgb[1] << 8) | rgb[0]; + } + return p32; +} + +// ---------------------------------------------------------------- +// WebGL path +// ---------------------------------------------------------------- +function initWebGL(canvas, gl) { var VERT_SRC = [ 'attribute vec2 a_pos;', 'void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }' @@ -24,76 +70,53 @@ function initFractal(canvasId) { 'precision highp float;', 'uniform float u_time;', 'uniform vec2 u_resolution;', - '', + 'uniform sampler2D u_dist;', + 'vec3 hslToRgb(float h, float s, float l) {', + ' float a = s * min(l, 1.0 - l);', + ' vec3 k = mod(vec3(0.0, 8.0, 4.0) + h * 12.0, 12.0);', + ' vec3 v = max(min(min(k - 3.0, 9.0 - k), vec3(1.0)), vec3(-1.0));', + ' return vec3(l) - a * v;', + '}', + 'vec3 calmBand(float idx) {', + ' float f = mod(idx, 256.0);', + ' float band = 0.5 + 0.5 * cos(f * 6.28318530 / 56.0);', + ' float h = 0.665 + 0.015 * sin(f * 3.14159265 / 128.0);', + ' float s = 0.80;', + ' float l = 0.18 + 0.28 * band;', + ' return hslToRgb(h, s, l);', + '}', '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);', + ' vec2 uv = gl_FragCoord.xy / u_resolution;', + ' float aspect = u_resolution.x / u_resolution.y;', + ' float t = u_time * 0.12;', + ' float maxH = 1.0 / max(aspect, 1.0);', + ' float zoomFrac = 0.65 + 0.15 * sin(t * 0.4);', + ' float halfH = maxH * zoomFrac * 0.5;', + ' float halfW = halfH * aspect;', + ' float fx = 0.5 - halfW;', + ' float fy = 0.5 - halfH;', + ' float cx = 0.5 + fx * 0.5 * cos(t * 0.55);', + ' float cy = 0.5 + fy * 0.5 * sin(t * 0.45);', + ' float sx = cx + (uv.x - 0.5) * 2.0 * halfW;', + ' float sy = cy + (uv.y - 0.5) * 2.0 * halfH;', + ' float v = texture2D(u_dist, vec2(sx, sy)).r * 255.0;', + ' // Dither only in the outer smooth region (v in [180, 240] ramps in;', + ' // inner fractal detail stays crisp).', + ' float ditherMix = clamp((v - 180.0) / 60.0, 0.0, 1.0);', + ' float noise = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) - 0.5;', + ' v += noise * 1.2 * ditherMix;', + ' float offset = u_time * 10.8;', + ' vec3 color = calmBand(v - offset);', + ' gl_FragColor = vec4(color, 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)); + console.error('[fractal-gl] shader compile error:', gl.getShaderInfoLog(s)); return null; } return s; @@ -101,52 +124,207 @@ function initFractal(canvasId) { var vs = compile(gl.VERTEX_SHADER, VERT_SRC); var fs = compile(gl.FRAGMENT_SHADER, FRAG_SRC); - if (!vs || !fs) return; + if (!vs || !fs) return initCanvas2D(canvas); 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; + console.error('[fractal-gl] link error:', gl.getProgramInfoLog(prog)); + return initCanvas2D(canvas); } 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); - + 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'); + var uRes = gl.getUniformLocation(prog, 'u_resolution'); + var uDist = gl.getUniformLocation(prog, 'u_dist'); - // --- Resize --- + var tex = gl.createTexture(); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, + gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 255])); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.uniform1i(uDist, 0); + + var img = new Image(); + img.onload = function () { + var w = img.width, h = img.height; + console.log('[fractal-gl] PNG decoded:', w, 'x', h); + var off = document.createElement('canvas'); + off.width = w; off.height = h; + var octx = off.getContext('2d'); + octx.drawImage(img, 0, 0); + var data = octx.getImageData(0, 0, w, h).data; + var bytes = new Uint8Array(data.length); + bytes.set(data); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); + gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, + gl.RGBA, gl.UNSIGNED_BYTE, bytes); + }; + img.onerror = function (e) { console.error('[fractal-gl] dist.png load failed', e); }; + img.src = '/routes/root/dist.png'; function resize() { - canvas.width = window.innerWidth; + 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; + var t = (performance.now() - startTime) * 0.001; gl.uniform1f(uTime, t); gl.uniform2f(uRes, canvas.width, canvas.height); gl.drawArrays(gl.TRIANGLES, 0, 3); requestAnimationFrame(frame); } requestAnimationFrame(frame); + console.log('[fractal-gl] WebGL loop started'); +} + +// ---------------------------------------------------------------- +// Canvas2D fallback +// - 1024-entry palette (4x finer than the byte-quantized iteration value) +// - bilinear sample of source buffer (no nearest-neighbor jaggies) +// - Uint32 writes (one packed write per pixel instead of four byte writes) +// ---------------------------------------------------------------- +function initCanvas2D(canvas) { + var ctx2d = canvas.getContext('2d'); + if (!ctx2d) { + console.error('[fractal-gl] Canvas2D also unavailable; body gradient remains'); + return; + } + var palette32 = _buildPalette32(); + + var img = new Image(); + img.onerror = function (e) { console.error('[fractal-gl] dist.png load failed (canvas2d)', e); }; + img.onload = function () { + var SW = img.width, SH = img.height; + console.log('[fractal-gl] PNG decoded (canvas2d):', SW, 'x', SH); + var off = document.createElement('canvas'); + off.width = SW; off.height = SH; + var octx = off.getContext('2d'); + octx.drawImage(img, 0, 0); + var data = octx.getImageData(0, 0, SW, SH).data; + var gray = new Uint8Array(SW * SH); + for (var i = 0, j = 0; i < gray.length; i++, j += 4) gray[i] = data[j]; + + // Higher internal resolution = sharper. Capped to keep frame budget reasonable. + function targetSize() { + var h = Math.min(800, Math.max(360, Math.floor(window.innerHeight * 0.7))); + var aspect = window.innerWidth / window.innerHeight; + return { w: Math.max(2, Math.floor(h * aspect)), h: h }; + } + + function resize() { + var s = targetSize(); + canvas.width = s.w; canvas.height = s.h; + } + resize(); + window.addEventListener('resize', resize); + + var imgOut = ctx2d.createImageData(canvas.width, canvas.height); + var out32 = new Uint32Array(imgOut.data.buffer); + var lastW = canvas.width, lastH = canvas.height; + var startTime = performance.now(); + + // 4x4 Bayer matrix, recentered to ±0.6 source units. Only applied where + // v is in the outer-smooth band (ramp 180→240) so inner detail stays crisp. + var BAYER = new Float32Array([0,8,2,10, 12,4,14,6, 3,11,1,9, 15,7,13,5]); + for (var bi = 0; bi < 16; bi++) BAYER[bi] = (BAYER[bi] - 7.5) * 0.6 / 7.5; + + function frame() { + var W = canvas.width, H = canvas.height; + if (W !== lastW || H !== lastH) { + imgOut = ctx2d.createImageData(W, H); + out32 = new Uint32Array(imgOut.data.buffer); + lastW = W; lastH = H; + } + var elapsed = (performance.now() - startTime) * 0.001; + var t = elapsed * 0.12; + var aspect = W / H; + var maxH = 1.0 / Math.max(aspect, 1.0); + var zoomFrac = 0.65 + 0.15 * Math.sin(t * 0.4); + var halfH = maxH * zoomFrac * 0.5; + var halfW = halfH * aspect; + var fxRem = 0.5 - halfW, fyRem = 0.5 - halfH; + var cx = 0.5 + fxRem * 0.5 * Math.cos(t * 0.55); + var cy = 0.5 + fyRem * 0.5 * Math.sin(t * 0.45); + var x0 = (cx - halfW) * SW; + var y0 = (cy - halfH) * SH; + var dxs = (2 * halfW) / W * SW; + var dys = (2 * halfH) / H * SH; + // offset in 1024-entry space (4x granularity) — same cycle period (~24 s) as before + var offsetHD = (elapsed * 10.8 * 4) | 0; + + var SW1 = SW - 1, SH1 = SH - 1; + var idx = 0; + var sy = y0; + for (var y = 0; y < H; y++) { + var sy0i = sy | 0; + if (sy0i < 0) sy0i = 0; else if (sy0i > SH1) sy0i = SH1; + var sy1i = sy0i + 1; + if (sy1i > SH1) sy1i = SH1; + var fy = sy - (sy | 0); + var ify = 1 - fy; + var row0 = sy0i * SW; + var row1 = sy1i * SW; + + var sx = x0; + for (var x = 0; x < W; x++) { + var sx0i = sx | 0; + if (sx0i < 0) sx0i = 0; else if (sx0i > SW1) sx0i = SW1; + var sx1i = sx0i + 1; + if (sx1i > SW1) sx1i = SW1; + var fx = sx - (sx | 0); + var ifx = 1 - fx; + + var a = gray[row0 + sx0i]; + var b = gray[row0 + sx1i]; + var c = gray[row1 + sx0i]; + var d = gray[row1 + sx1i]; + var v = (a * ifx + b * fx) * ify + (c * ifx + d * fx) * fy; + // Selective dither: only the outer smooth region (v 180→240 ramps in). + if (v > 180) { + var mix = v >= 240 ? 1 : (v - 180) / 60; + v += BAYER[(y & 3) * 4 + (x & 3)] * 1.6 * mix; + } + // Multiply by 4 to index into 1024-entry palette, then wrap. + out32[idx++] = palette32[(((v * 4) | 0) - offsetHD) & 1023]; + + sx += dxs; + } + sy += dys; + } + ctx2d.putImageData(imgOut, 0, 0); + requestAnimationFrame(frame); + } + requestAnimationFrame(frame); + console.log('[fractal-gl] Canvas2D loop started; internal', + canvas.width, 'x', canvas.height); + }; + img.src = '/routes/root/dist.png'; +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function () { initFractal('fractal-canvas'); }); +} else { + initFractal('fractal-canvas'); } diff --git a/routes/root/index.html b/routes/root/index.html index 2a43338..205e8a8 100755 --- a/routes/root/index.html +++ b/routes/root/index.html @@ -20,6 +20,7 @@ } #fractal-canvas { + image-rendering: auto; position: fixed; top: 0; left: 0; @@ -36,7 +37,6 @@ flex-direction: column; align-items: center; justify-content: center; - animation: fadeInUp 800ms ease 300ms both; } .glass-card { @@ -119,7 +119,16 @@ - - + diff --git a/routes/who/index.html b/routes/who/index.html index ad2926c..8d69975 100755 --- a/routes/who/index.html +++ b/routes/who/index.html @@ -20,6 +20,7 @@ } #fractal-canvas { + image-rendering: auto; position: fixed; top: 0; left: 0; @@ -36,7 +37,6 @@ flex-direction: column; align-items: center; justify-content: center; - animation: fadeInUp 800ms ease 300ms both; } .glass-card { @@ -108,7 +108,15 @@ - - +