diff --git a/crates/fractal-engine/src/color.rs b/crates/fractal-engine/src/color.rs index 719094d..b289c61 100644 --- a/crates/fractal-engine/src/color.rs +++ b/crates/fractal-engine/src/color.rs @@ -11,9 +11,9 @@ pub fn ocean_palette(root_index: u8, iterations: u32, max_iterations: u32) -> (u // Ocean theme colors for each root basin let (r, g, b) = match root_index { - 0 => (0.05, 0.25, 0.55), // Deep ocean blue - 1 => (0.0, 0.45, 0.50), // Teal - 2 => (0.15, 0.55, 0.65), // Cyan/turquoise + 0 => (0.05, 0.25, 0.55), // Deep ocean blue + 1 => (0.0, 0.45, 0.50), // Teal + 2 => (0.15, 0.55, 0.65), // Cyan/turquoise _ => (0.05, 0.25, 0.55), }; diff --git a/crates/fractal-engine/src/newton.rs b/crates/fractal-engine/src/newton.rs index 8274e0c..c69a4b5 100644 --- a/crates/fractal-engine/src/newton.rs +++ b/crates/fractal-engine/src/newton.rs @@ -16,9 +16,9 @@ pub fn compute(z_re: f64, z_im: f64, time: f64) -> (u8, u32) { // The three roots of z^3 - 1 const ROOTS: [(f64, f64); 3] = [ - (1.0, 0.0), // 1 - (-0.5, 0.866025403784439), // e^(2πi/3) - (-0.5, -0.866025403784439), // e^(4πi/3) + (1.0, 0.0), // 1 + (-0.5, 0.866025403784439), // e^(2πi/3) + (-0.5, -0.866025403784439), // e^(4πi/3) ]; for iter in 0..MAX_ITERATIONS { diff --git a/crates/fractal-engine/src/wasm.rs b/crates/fractal-engine/src/wasm.rs index 2c90318..29d23ea 100644 --- a/crates/fractal-engine/src/wasm.rs +++ b/crates/fractal-engine/src/wasm.rs @@ -7,7 +7,9 @@ use crate::newton::compute; const MAX_ITERATIONS: u32 = 32; /// Get the canvas element and 2D context -fn get_canvas_and_context(canvas_id: &str) -> Result<(HtmlCanvasElement, CanvasRenderingContext2d), JsValue> { +fn get_canvas_and_context( + canvas_id: &str, +) -> Result<(HtmlCanvasElement, CanvasRenderingContext2d), JsValue> { let window = web_sys::window().ok_or("no window")?; let document = window.document().ok_or("no document")?; let canvas = document @@ -66,7 +68,8 @@ pub fn render_fractal_animated(canvas_id: &str, time: f64, scale: u32) -> Result // Create ImageData at reduced size let clamped = wasm_bindgen::Clamped(&pixels[..]); - let image_data = ImageData::new_with_u8_clamped_array_and_sh(clamped, width as u32, height as u32)?; + let image_data = + ImageData::new_with_u8_clamped_array_and_sh(clamped, width as u32, height as u32)?; // Scale up when drawing to canvas if scale > 1 { @@ -89,8 +92,10 @@ pub fn render_fractal_animated(canvas_id: &str, time: f64, scale: u32) -> Result )?; context.draw_image_with_html_canvas_element_and_dw_and_dh( &temp_canvas, - 0.0, 0.0, - canvas_width as f64, canvas_height as f64 + 0.0, + 0.0, + canvas_width as f64, + canvas_height as f64, )?; } else { context.put_image_data(&image_data, 0.0, 0.0)?; diff --git a/forgejo/start-forgejo.sh b/forgejo/start-forgejo.sh index 660147f..a434592 100755 --- a/forgejo/start-forgejo.sh +++ b/forgejo/start-forgejo.sh @@ -2,17 +2,18 @@ WEB_UI_PORT=3001 SSH_PORT=2223 +DATA_DIR=/home/deploy/forgejo/data docker run -d \ --name forgejo \ - -p ${WEB_UI_PORT}:3000 \ + -p 127.0.0.1:${WEB_UI_PORT}:3000 \ -p ${SSH_PORT}:22 \ - -v ~/forgejo/data:/data \ + -v ${DATA_DIR}:/data \ -v /etc/timezone:/etc/timezone:ro \ -v /etc/localtime:/etc/localtime:ro \ - --restart always \ + --restart always \ codeberg.org/forgejo/forgejo:9 echo "Forgejo started on:" -echo " Web UI: http://zachery.lol:${WEB_UI_PORT}" +echo " Web UI: http://127.0.0.1:${WEB_UI_PORT} (proxied at https://zachery.lol/code/)" echo " ssh port: ${SSH_PORT}" diff --git a/nginx-setup/zachery.lol b/nginx-setup/zachery.lol index 51cf40d..fe646ac 100644 --- a/nginx-setup/zachery.lol +++ b/nginx-setup/zachery.lol @@ -33,8 +33,8 @@ server { listen 443 ssl; # managed by Certbot listen [::]:443 ssl ipv6only=on; # managed by Certbot - ssl_certificate /etc/letsencrypt/live/zachery.lol/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/zachery.lol/privkey.pem; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/zachery.lol-0001/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/zachery.lol-0001/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } diff --git a/routes/contact/index.html b/routes/contact/index.html index 0e2d27e..077a289 100644 --- a/routes/contact/index.html +++ b/routes/contact/index.html @@ -117,6 +117,8 @@ } } + + @@ -135,15 +137,7 @@ - + + diff --git a/routes/contact/message/index.html b/routes/contact/message/index.html index 9bd5026..a53c832 100644 --- a/routes/contact/message/index.html +++ b/routes/contact/message/index.html @@ -279,6 +279,8 @@ } } + + @@ -323,15 +325,7 @@ back to contact - + + diff --git a/tools/gen-strips.py b/tools/gen-strips.py new file mode 100644 index 0000000..2fceb25 --- /dev/null +++ b/tools/gen-strips.py @@ -0,0 +1,93 @@ +# /// script +# requires-python = ">=3.9" +# dependencies = ["pillow"] +# /// +""" +Generate routes/root/dist-strips.bin from routes/root/dist.png. + +Splits the 512x512 grayscale Julia distance field into N horizontal bands, +re-encodes each band as its own WebP, and concatenates them in CENTER-OUT order +(the band the pan/zoom animation shows first) so a streaming client can upload +the visible region before the outer bands have finished downloading. WebP q90 is +~half the size of PNG with no visible loss after the color-band mapping. + +File layout (little-endian): + u32 headerLen + bytes headerLen -- UTF-8 JSON: {fullW, fullH, bands:[{y,h,off,len}, ...]} + bytes payload -- concatenated band WebPs in the order listed in `bands` + (off/len are byte ranges within payload, stream order) + +Re-run only when dist.png changes; the output is committed. +Usage: uv run tools/gen-strips.py +""" +import io +import json +import os +import struct +from PIL import Image + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +SRC = os.path.join(ROOT, "routes", "root", "dist.png") +OUT = os.path.join(ROOT, "routes", "root", "dist-strips.bin") +NBANDS = 8 +WEBP_QUALITY = 90 # lossy; ~half of PNG, visually identical. Bump to 95 / use lossless if needed. + + +def center_out(n): + """Band indices ordered from the middle outward: e.g. n=8 -> [4,3,5,2,6,1,7,0].""" + order = [] + lo, hi = n // 2 - 1, n // 2 + while lo >= 0 or hi < n: + if hi < n: + order.append(hi) + hi += 1 + if lo >= 0: + order.append(lo) + lo -= 1 + return order + + +def main(): + img = Image.open(SRC).convert("L") + w, h = img.size + + # Row boundaries (robust if h is not divisible by NBANDS). + bounds = [round(i * h / NBANDS) for i in range(NBANDS + 1)] + bands_by_index = [] + for i in range(NBANDS): + y0, y1 = bounds[i], bounds[i + 1] + crop = img.crop((0, y0, w, y1)) + buf = io.BytesIO() + crop.save(buf, format="WEBP", quality=WEBP_QUALITY, method=6) + bands_by_index.append((y0, y1 - y0, buf.getvalue())) + + payload = bytearray() + meta = [] + order = center_out(NBANDS) + for idx in order: + y, bh, png = bands_by_index[idx] + meta.append({"y": y, "h": bh, "off": len(payload), "len": len(png)}) + payload += png + + header = json.dumps( + {"fullW": w, "fullH": h, "bands": meta}, separators=(",", ":") + ).encode("utf-8") + + with open(OUT, "wb") as f: + f.write(struct.pack(" {NBANDS} bands, center-out order {order}") + print( + f"header {len(header)}B, payload {len(payload)}B, total {total}B " + f"({total / 1024:.1f} KB) vs dist.png {src_size}B ({src_size / 1024:.1f} KB)" + ) + print(f"wrote {OUT}") + + +if __name__ == "__main__": + main()