From 219bb6552edbd4295cd41f8bb6b0416fcb4b2e39 Mon Sep 17 00:00:00 2001 From: Zachery Aaron Shores-Chmielewski Date: Fri, 24 Jul 2026 12:45:02 +0400 Subject: [PATCH] Flesh out about and projects pages Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski --- crates/server/Cargo.toml | 2 +- crates/server/bin/main.rs | 58 ++- routes/about/about-section-skeleton.md | 17 + routes/about/index.html | 152 ++++++++ .../airfrans-neural-cfd-surrogate/index.html | 42 +++ routes/projects/cstat-yoke/index.html | 36 ++ routes/projects/index.html | 156 +++++++++ routes/projects/swactor/index.html | 68 ++++ routes/root/dist-strips.bin | Bin 0 -> 42074 bytes routes/root/fractal-gl.js | 329 +++++++++++++----- routes/root/index.html | 18 +- routes/who/index.html | 122 ------- 12 files changed, 770 insertions(+), 230 deletions(-) create mode 100644 routes/about/about-section-skeleton.md create mode 100644 routes/about/index.html create mode 100644 routes/projects/airfrans-neural-cfd-surrogate/index.html create mode 100644 routes/projects/cstat-yoke/index.html create mode 100644 routes/projects/index.html create mode 100644 routes/projects/swactor/index.html create mode 100644 routes/root/dist-strips.bin delete mode 100755 routes/who/index.html diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index ddc5328..7ced16d 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -10,7 +10,7 @@ path = "bin/main.rs" [dependencies] axum = "0.8.7" tokio = { version = "1.48.0", features = ["full"] } -tower-http = { version = "0.6.6", features = ["fs", "cors", "limit"] } +tower-http = { version = "0.6.6", features = ["fs", "cors", "limit", "set-header"] } serde_json = "1.0.145" message-tools = { path = "../message-tools" } diff --git a/crates/server/bin/main.rs b/crates/server/bin/main.rs index a9fa515..e9be3e0 100644 --- a/crates/server/bin/main.rs +++ b/crates/server/bin/main.rs @@ -1,6 +1,6 @@ use axum::{ Json, Router, - http::{Method, StatusCode}, + http::{HeaderValue, Method, StatusCode, header}, response::Html, routing::{get, post}, }; @@ -13,7 +13,10 @@ use std::{ time::Duration, }; use tokio::io::AsyncWriteExt; -use tower_http::{cors::CorsLayer, limit::RequestBodyLimitLayer, services::ServeDir}; +use tower_http::{ + cors::CorsLayer, limit::RequestBodyLimitLayer, services::ServeDir, + set_header::SetResponseHeaderLayer, +}; const PUBKEY_PATH: &str = "./pubkeys/public_keys.json"; const MESSAGE_STORAGE: &str = "./messages"; @@ -76,18 +79,51 @@ async fn main() -> Result<()> { .allow_methods([Method::GET, Method::POST]) .allow_headers([axum::http::header::CONTENT_TYPE]); + // Cache static assets so navigating between pages reuses the streamed field + // instead of re-downloading it. Scoped to /routes only; HTML routes stay fresh. + let routes_static = Router::new() + .fallback_service(ServeDir::new("./routes")) + .layer(SetResponseHeaderLayer::overriding( + header::CACHE_CONTROL, + HeaderValue::from_static("public, max-age=86400"), + )); + let app = Router::new() .route("/", get(serve_path("./routes/root/index.html")?)) - .route("/who", get(serve_path("./routes/who/index.html")?)) + .route("/about", get(serve_path("./routes/about/index.html")?)) + .route( + "/projects", + get(serve_path("./routes/projects/index.html")?), + ) + .route( + "/projects/swactor", + get(serve_path("./routes/projects/swactor/index.html")?), + ) + .route( + "/projects/airfrans-neural-cfd-surrogate", + get(serve_path( + "./routes/projects/airfrans-neural-cfd-surrogate/index.html", + )?), + ) + .route( + "/projects/cstat-yoke", + get(serve_path("./routes/projects/cstat-yoke/index.html")?), + ) .route("/api/pubkey", get(async || select_key(&PUBLIC_KEYS))) .route("/contact", get(serve_path("./routes/contact/index.html")?)) - .route("/contact/message", get(serve_path("./routes/contact/message/index.html")?)) + .route( + "/contact/message", + get(serve_path("./routes/contact/message/index.html")?), + ) .route( "/api/publish", post(publish_message).layer(RequestBodyLimitLayer::new(64 * 1024)), ) - .nest_service("/gossip-dashboard", ServeDir::new("./routes/gossip-dashboard")) - .nest_service("/routes", ServeDir::new("./routes")) + .nest_service( + "/gossip-dashboard", + ServeDir::new("./routes/gossip-dashboard"), + ) + .nest("/routes", routes_static) .layer(cors); let addr = "0.0.0.0:3000"; @@ -123,9 +159,7 @@ async fn publish_message(Json(msg): Json) -> (StatusCode, Json= MAX_PUBLISHES_PER_MINUTE { return ( StatusCode::TOO_MANY_REQUESTS, - Json(UserCreated { - tag: String::new(), - }), + Json(UserCreated { tag: String::new() }), ); } limiter.1 += 1; @@ -141,11 +175,7 @@ async fn publish_message(Json(msg): Json) -> (StatusCode, Json + + + + + About + + + + + + + +
+
+

Software engineer cum neural network wrangler.

+

Technical interests include systems engineering, distributed compute, physical simulation, and applied machine learning infrastructure.

+ +

Projects

+
    +
  • + swactor + Toolkit for orchestrating somewhat-unreliable heterogeneous GPU nodes over WAN links. +
  • +
  • + AirfRANS / neural CFD surrogate + Machine learning as simulation: neural CFD surrogates, model scaling, and solver-grounded validation. +
  • +
  • + cstat and yoke + Code as data: statistical structure, LLM coding behavior, and agent harnesses. +
  • +
+ +

If any of these interest you, please contact me. I love to talk shop.

+
+ + +
+ + + + diff --git a/routes/projects/airfrans-neural-cfd-surrogate/index.html b/routes/projects/airfrans-neural-cfd-surrogate/index.html new file mode 100644 index 0000000..bcc02d0 --- /dev/null +++ b/routes/projects/airfrans-neural-cfd-surrogate/index.html @@ -0,0 +1,42 @@ + + + + + + AirfRANS / neural CFD surrogate + + + + + + +
+
+

AirfRANS / neural CFD surrogate

+

This fake writeup pretends the goal is simple: make a neural surrogate that behaves enough like a CFD solver to be useful, but not so much like a CFD solver that it takes all afternoon to answer.

+

The model gets geometry, flow conditions, and a stern lecture from validation metrics. It returns fields, uncertainty hints, and the occasional reminder that conservation laws are not optional.

+

The interesting part is the boundary between learned approximation and solver-grounded truth: where the network is fast, where it is wrong, and how to know before a wing falls off in a slide deck.

+
+ +
+ + diff --git a/routes/projects/cstat-yoke/index.html b/routes/projects/cstat-yoke/index.html new file mode 100644 index 0000000..3acb77f --- /dev/null +++ b/routes/projects/cstat-yoke/index.html @@ -0,0 +1,36 @@ + + + + + + cstat and yoke + + + + + + +
+
+

cstat and yoke

+

cstat is the fake microscope; yoke is the fake harness. Together they treat code as data and ask what shape a codebase has before anyone starts arguing about taste.

+

The writeup claims cstat measures local structure, repetition, churn-shaped scars, and the weird fingerprints left by agents that are too confident near edge cases.

+

yoke then turns those observations into experiments: give an agent a task, constrain the harness, watch the trace, and learn whether the model fixed the problem or merely rearranged the furniture.

+
+ +
+ + diff --git a/routes/projects/index.html b/routes/projects/index.html new file mode 100644 index 0000000..60494a1 --- /dev/null +++ b/routes/projects/index.html @@ -0,0 +1,156 @@ + + + + + + Projects + + + + + + + +
+
+

Projects

+
+
+

swactor

+

Toolkit for orchestrating somewhat-unreliable heterogeneous GPU nodes over WAN links.

+
+ + + +
+

cstat and yoke

+

Code as data: statistical structure, LLM coding behavior, and agent harnesses.

+
+
+
+ + +
+ + + + diff --git a/routes/projects/swactor/index.html b/routes/projects/swactor/index.html new file mode 100644 index 0000000..dd0233e --- /dev/null +++ b/routes/projects/swactor/index.html @@ -0,0 +1,68 @@ + + + + + + swactor + + + + + + +
+
+

swactor

+

swactor is the imaginary control plane I would build for a room full of moody GPUs scattered across apartments, closets, and spare cloud instances.

+

The fake writeup version starts with a tiny actor runtime, gives each node a heartbeat, then treats dropped packets and flaky thermal envelopes as normal weather instead of exceptional tragedy.

+

The punchline: jobs migrate toward available memory, checkpoints move over boring protocols, and the scheduler learns which machines are liars before the humans do.

+
+ +
+ + diff --git a/routes/root/dist-strips.bin b/routes/root/dist-strips.bin new file mode 100644 index 0000000000000000000000000000000000000000..73d0fd77c2d96ad930915e16a82f893dc451139d GIT binary patch literal 42074 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 z zkRzbd77dN*Rz;*R#I1QnNCwZmlX4=ILKz6IEtfwB!=$ohsmUsglAS{UxGmcL&yqFQ zMrLiU!>f!Uzik1sbWtdN&KTNT3CWgtZkQQew$}@?h7m2J!Yjy~TTt`nn&PduW0TpR z`R}Z~Ejj%C?rRj!yV;8mzp)D~6HAu^mUqR?J3$Jcf41nwsu2i Canvas2D -> flat dark backdrop. +// +// The 512x512 distance field is precomputed (live per-pixel iteration is too slow). +// It ships as `dist-strips.bin`: full-resolution horizontal bands, each WebP-compressed, +// concatenated in CENTER-OUT order. We stream the file and upload each band to its texture +// sub-rectangle as its bytes arrive. +// +// The DISPLAY FOLLOWS THE STREAM: instead of drawing the whole screen over a half-loaded +// texture (which looked like a "glow"), the shader only reveals the field rows that have +// actually streamed in, blooming outward from the center over a flat dark backdrop. The +// revealed extent eases smoothly, so it reads as an intentional reveal, not jank. +// Palette: BSOD-blue, smooth cosine bands, 1024-entry LUT (Canvas2D path only). + +var FIELD_URL = '/routes/root/dist-strips.bin?v=1'; // streamed WebP bands (bump ?v on regen) +var DIST_URL = '/routes/root/dist.png'; // monolithic source (Canvas2D fallback) +var DARK_HEX = '#060a10'; // backdrop / unloaded tone function initFractal(canvasId) { console.log('[fractal-gl] initFractal:', canvasId); @@ -10,9 +25,8 @@ function initFractal(canvasId) { return; } - // Visible base color even if both render paths fail. - document.body.style.background = - 'radial-gradient(ellipse at center, #142566 0%, #0a103a 60%, #060a10 100%)'; + // Flat dark backdrop: clean start state, and the tone the reveal blooms over. + document.body.style.background = DARK_HEX; var gl = null; try { @@ -30,6 +44,91 @@ function initFractal(canvasId) { } } +// ---------------------------------------------------------------- +// Shared streamed-field loader +// Emits each band as a decoded ImageBitmap the moment its bytes land. +// cb = { onHeader(header), onStrip(bitmap, y, h), onError(err) } +// File: u32(LE) headerLen | JSON header | concatenated band WebPs. +// JSON: { fullW, fullH, bands:[{y,h,off,len}] } in stream (center-out) order. +// ---------------------------------------------------------------- +function streamFieldBands(cb) { + var TD = new TextDecoder(); + + function emitBand(bytes, b) { + // bytes is an independent copy, safe to keep past the next read. + // Disable color management so the grayscale field bytes survive intact. + createImageBitmap(new Blob([bytes], { type: 'image/webp' }), + { colorSpaceConversion: 'none', premultiplyAlpha: 'none' }) + .then(function (bmp) { + try { cb.onStrip(bmp, b.y, b.h); } + catch (e) { console.warn('[fractal-gl] band upload failed', e); } + }) + .catch(function (e) { console.warn('[fractal-gl] band decode failed', e); }); + } + + function emitAll(buf) { + var hlen = new DataView(buf.buffer, buf.byteOffset, 4).getUint32(0, true); + var header = JSON.parse(TD.decode(buf.subarray(4, 4 + hlen))); + var ps = 4 + hlen; + cb.onHeader(header); + header.bands.forEach(function (b) { + emitBand(buf.subarray(ps + b.off, ps + b.off + b.len).slice(), b); + }); + } + + fetch(FIELD_URL).then(function (res) { + if (!res.ok) throw new Error('HTTP ' + res.status); + if (!res.body || !res.body.getReader) { + return res.arrayBuffer().then(function (ab) { emitAll(new Uint8Array(ab)); }); + } + + var reader = res.body.getReader(); + var chunks = []; + var total = 0; + var header = null; + var ps = 0; + var next = 0; + + function assemble() { + var out = new Uint8Array(total); + var o = 0; + for (var i = 0; i < chunks.length; i++) { out.set(chunks[i], o); o += chunks[i].length; } + return out; + } + + function step() { + return reader.read().then(function (r) { + if (r.value) { chunks.push(r.value); total += r.value.length; } + var buf = assemble(); + + if (!header && total >= 4) { + var hlen = new DataView(buf.buffer, 0, 4).getUint32(0, true); + if (total >= 4 + hlen) { + header = JSON.parse(TD.decode(buf.subarray(4, 4 + hlen))); + ps = 4 + hlen; + cb.onHeader(header); + } + } + if (header) { + while (next < header.bands.length) { + var b = header.bands[next]; + var end = ps + b.off + b.len; + if (total < end) break; + emitBand(buf.subarray(ps + b.off, end).slice(), b); + next++; + } + } + if (r.done) return; + return step(); + }); + } + return step(); + }).catch(function (e) { + console.error('[fractal-gl] field stream failed', e); + if (cb.onError) cb.onError(e); + }); +} + function _hslToRgb(h, s, l) { var a = s * Math.min(l, 1 - l); function f(n) { @@ -41,7 +140,6 @@ function _hslToRgb(h, s, l) { // 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); @@ -58,59 +156,68 @@ function _buildPalette32() { } // ---------------------------------------------------------------- -// WebGL path +// WebGL path (default) — streams bands in, reveals only what has loaded // ---------------------------------------------------------------- +var FIELD_VERT_GLSL = [ + 'attribute vec2 a_pos;', + 'void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }' +].join('\n'); + +// Same field-sampling math as before, plus a feathered reveal mask gated on the +// loaded field-row extent [u_loadV0, u_loadV1]. Unloaded rows show the dark backdrop. +var FIELD_FRAG_GLSL = [ + 'precision highp float;', + 'uniform float u_time;', + 'uniform vec2 u_resolution;', + 'uniform sampler2D u_dist;', + 'uniform float u_loadV0;', + 'uniform float u_loadV1;', + 'const vec3 DARK = vec3(0.024, 0.039, 0.063);', + '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;', + ' 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 fv = 1.0 - sy;', // field v-coordinate (texture uploaded natural orientation) + ' // Reveal only loaded field rows; nothing loaded yet => all dark.', + ' if (u_loadV1 - u_loadV0 < 0.001) { gl_FragColor = vec4(DARK, 1.0); return; }', + ' float E = 0.03;', + ' float m = smoothstep(u_loadV0 - E, u_loadV0 + E, fv) *', + ' (1.0 - smoothstep(u_loadV1 - E, u_loadV1 + E, fv));', + ' float v = texture2D(u_dist, vec2(sx, fv)).r * 255.0;', + ' 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(mix(DARK, color, m), 1.0);', + '}' +].join('\n'); + function initWebGL(canvas, gl) { - 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;', - '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;', - ' 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'); - function compile(type, src) { var s = gl.createShader(type); gl.shaderSource(s, src); @@ -122,8 +229,8 @@ function initWebGL(canvas, gl) { return s; } - var vs = compile(gl.VERTEX_SHADER, VERT_SRC); - var fs = compile(gl.FRAGMENT_SHADER, FRAG_SRC); + var vs = compile(gl.VERTEX_SHADER, FIELD_VERT_GLSL); + var fs = compile(gl.FRAGMENT_SHADER, FIELD_FRAG_GLSL); if (!vs || !fs) return initCanvas2D(canvas); var prog = gl.createProgram(); @@ -146,6 +253,8 @@ function initWebGL(canvas, gl) { var uTime = gl.getUniformLocation(prog, 'u_time'); var uRes = gl.getUniformLocation(prog, 'u_resolution'); var uDist = gl.getUniformLocation(prog, 'u_dist'); + var uV0 = gl.getUniformLocation(prog, 'u_loadV0'); + var uV1 = gl.getUniformLocation(prog, 'u_loadV1'); var tex = gl.createTexture(); gl.activeTexture(gl.TEXTURE0); @@ -159,25 +268,69 @@ function initWebGL(canvas, gl) { 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'; + // Drawing the band bitmap to a 2D canvas yields a universally-supported + // TexImageSource (works on WebGL1 and WebGL2 alike). + var scratch = document.createElement('canvas'); + var sctx = scratch.getContext('2d'); + + // ---- reveal state: which field rows have streamed in ---- + var totalH = 1; + var spatial = []; // bands sorted top->bottom: {y, h} + var loadedSlots = []; // parallel booleans + var centerSlot = 0, lastSlot = 0; + var targetV0 = 0.5, targetV1 = 0.5; // loaded extent (v-fraction), grows from center + var dispV0 = 0.5, dispV1 = 0.5; // eased toward target for a smooth bloom + + // Bloom only on the first page of a tab session; on later navigations the field + // is cached, so snap straight to it instead of replaying the ~1s intro. + var revealed = false; + try { revealed = sessionStorage.getItem('fractalRevealed') === '1'; } catch (e) {} + + streamFieldBands({ + onHeader: function (header) { + var w = header.fullW, h = header.fullH; + totalH = h; + spatial = header.bands.slice().sort(function (a, b) { return a.y - b.y; }); + loadedSlots = spatial.map(function () { return false; }); + lastSlot = spatial.length - 1; + centerSlot = 0; + for (var i = 0; i < spatial.length; i++) { + if (h / 2 >= spatial[i].y && h / 2 < spatial[i].y + spatial[i].h) { centerSlot = i; break; } + } + // Allocate the full texture (black; masked until rows load). + var fill = new Uint8Array(w * h * 4); + for (var k = 3; k < fill.length; k += 4) fill[k] = 255; + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, fill); + }, + onStrip: function (bitmap, y, h) { + scratch.width = bitmap.width; + scratch.height = bitmap.height; + sctx.drawImage(bitmap, 0, 0); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); + gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, y, gl.RGBA, gl.UNSIGNED_BYTE, scratch); + + var s = -1; + for (var i = 0; i < spatial.length; i++) { if (spatial[i].y === y) { s = i; break; } } + if (s < 0) return; + loadedSlots[s] = true; + // Grow the contiguous loaded run around the center (guards against a late + // middle band briefly revealing an unloaded gap). + if (loadedSlots[centerSlot]) { + var lo = centerSlot; while (lo - 1 >= 0 && loadedSlots[lo - 1]) lo--; + var hi = centerSlot; while (hi + 1 < spatial.length && loadedSlots[hi + 1]) hi++; + // Push the extent past the field edge once the outermost band is in, so the + // feather never darkens the true field edge in the steady state. + targetV0 = (lo === 0) ? -1.0 : spatial[lo].y / totalH; + targetV1 = (hi === lastSlot) ? 2.0 : (spatial[hi].y + spatial[hi].h) / totalH; + if (lo === 0 && hi === lastSlot) { + try { sessionStorage.setItem('fractalRevealed', '1'); } catch (e) {} + } + } + } + }); function resize() { canvas.width = window.innerWidth; @@ -190,8 +343,15 @@ function initWebGL(canvas, gl) { var startTime = performance.now(); function frame() { var t = (performance.now() - startTime) * 0.001; + // Ease the displayed extent toward the loaded extent for a smooth bloom. + // On a repeat visit (cached field) snap instead of replaying the intro. + var ease = revealed ? 1.0 : 0.12; + dispV0 += (targetV0 - dispV0) * ease; + dispV1 += (targetV1 - dispV1) * ease; gl.uniform1f(uTime, t); gl.uniform2f(uRes, canvas.width, canvas.height); + gl.uniform1f(uV0, dispV0); + gl.uniform1f(uV1, dispV1); gl.drawArrays(gl.TRIANGLES, 0, 3); requestAnimationFrame(frame); } @@ -200,15 +360,16 @@ function initWebGL(canvas, gl) { } // ---------------------------------------------------------------- -// Canvas2D fallback +// Canvas2D fallback (rare; ancient / no-GPU browsers) // - 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) +// Loads the monolithic dist.png and renders once decoded (fades in). // ---------------------------------------------------------------- function initCanvas2D(canvas) { var ctx2d = canvas.getContext('2d'); if (!ctx2d) { - console.error('[fractal-gl] Canvas2D also unavailable; body gradient remains'); + console.error('[fractal-gl] Canvas2D also unavailable; body backdrop remains'); return; } var palette32 = _buildPalette32(); @@ -226,6 +387,10 @@ function initCanvas2D(canvas) { var gray = new Uint8Array(SW * SH); for (var i = 0, j = 0; i < gray.length; i++, j += 4) gray[i] = data[j]; + // Fade the canvas in once the first frame is ready. + canvas.style.transition = 'opacity 400ms ease'; + canvas.style.opacity = '0'; + // 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))); @@ -244,6 +409,7 @@ function initCanvas2D(canvas) { var out32 = new Uint32Array(imgOut.data.buffer); var lastW = canvas.width, lastH = canvas.height; var startTime = performance.now(); + var faded = false; // 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. @@ -314,13 +480,14 @@ function initCanvas2D(canvas) { sy += dys; } ctx2d.putImageData(imgOut, 0, 0); + if (!faded) { faded = true; canvas.style.opacity = '1'; } requestAnimationFrame(frame); } requestAnimationFrame(frame); console.log('[fractal-gl] Canvas2D loop started; internal', canvas.width, 'x', canvas.height); }; - img.src = '/routes/root/dist.png'; + img.src = DIST_URL; } if (document.readyState === 'loading') { diff --git a/routes/root/index.html b/routes/root/index.html index 205e8a8..9bb90bd 100755 --- a/routes/root/index.html +++ b/routes/root/index.html @@ -100,6 +100,8 @@ } } + + @@ -112,23 +114,15 @@ - + + diff --git a/routes/who/index.html b/routes/who/index.html deleted file mode 100755 index 8d69975..0000000 --- a/routes/who/index.html +++ /dev/null @@ -1,122 +0,0 @@ - - - - - - Who - - - - - -
-
-

Who

-

No one in particular :)

-

I'll write more later...

-
- - -
- - - -