From a5d56c0bd2aae88c5e14b5c13d21b0b6bdcac30e Mon Sep 17 00:00:00 2001 From: Zachery Aaron Shores-Chmielewski Date: Fri, 6 Feb 2026 11:48:46 +0700 Subject: [PATCH] feat: main page background Render newtons fractal in the background of the main page --- .gitignore | 4 +- Cargo.lock | 19 +++++ Cargo.toml | 1 + crates/cli/src/main.rs | 19 +++-- crates/fractal-engine/Cargo.toml | 15 ++++ crates/fractal-engine/src/color.rs | 25 +++++++ crates/fractal-engine/src/lib.rs | 5 ++ crates/fractal-engine/src/newton.rs | 76 ++++++++++++++++++++ crates/fractal-engine/src/wasm.rs | 106 ++++++++++++++++++++++++++++ routes/root/index.html | 105 ++++++++++++++++++++++----- 10 files changed, 352 insertions(+), 23 deletions(-) create mode 100644 crates/fractal-engine/Cargo.toml create mode 100644 crates/fractal-engine/src/color.rs create mode 100644 crates/fractal-engine/src/lib.rs create mode 100644 crates/fractal-engine/src/newton.rs create mode 100644 crates/fractal-engine/src/wasm.rs diff --git a/.gitignore b/.gitignore index 3f836ad..3e84e65 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /target /web-build -.local \ No newline at end of file +.local +**.bak +**/dist/ diff --git a/Cargo.lock b/Cargo.lock index e380c18..34dbcee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -331,6 +331,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fractal-engine" +version = "0.1.0" +dependencies = [ + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -1104,6 +1113,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "windows-link" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index eb2ac70..a542e61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "crates/cli", + "crates/fractal-engine", "crates/message-tools", "crates/server" ] diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index aebcdbd..7608ee4 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -255,7 +255,7 @@ fn handle_command_status(status_result: std::io::Result, command_nam } fn build() { - println!("Building wasm package..."); + println!("Building wasm package (message-tools)..."); // build the wasm package let status = Command::new("wasm-pack") .arg("build") @@ -265,7 +265,18 @@ fn build() { .arg("--target") .arg("no-modules") .status(); - handle_command_status(status, "wasm-pack build"); + handle_command_status(status, "wasm-pack build message-tools"); + + println!("Building wasm package (fractal-engine)..."); + let status = Command::new("wasm-pack") + .arg("build") + .arg("--out-dir") + .arg("../../routes/root/pkg") + .arg("crates/fractal-engine") + .arg("--target") + .arg("no-modules") + .status(); + handle_command_status(status, "wasm-pack build fractal-engine"); println!("Building server executable..."); // build the server executable @@ -285,10 +296,10 @@ fn build() { handle_command_status(status, "mkdir web-build"); println!("Copying files..."); - // copy relevant files + // copy relevant files (use trailing slash to merge into existing directory) let status = Command::new("cp") .args(["-R", "-f", "routes"]) - .arg(format!("{WEB_BUILD_DIR}/routes")) + .arg(WEB_BUILD_DIR) .status(); handle_command_status(status, "cp routes"); diff --git a/crates/fractal-engine/Cargo.toml b/crates/fractal-engine/Cargo.toml new file mode 100644 index 0000000..123a1cf --- /dev/null +++ b/crates/fractal-engine/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "fractal-engine" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib", "rlib"] + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] +wasm-bindgen = "0.2.105" +web-sys = { version = "0.3", features = [ + "Window", "Document", "HtmlCanvasElement", + "CanvasRenderingContext2d", "ImageData" +]} +js-sys = "0.3" diff --git a/crates/fractal-engine/src/color.rs b/crates/fractal-engine/src/color.rs new file mode 100644 index 0000000..719094d --- /dev/null +++ b/crates/fractal-engine/src/color.rs @@ -0,0 +1,25 @@ +/// Ocean color palette for Newton's fractal + +/// Ocean color palette for three roots +/// root_index: 0, 1, or 2 +/// iterations: number of iterations to converge (affects brightness) +/// max_iterations: maximum iterations (for normalization) +pub fn ocean_palette(root_index: u8, iterations: u32, max_iterations: u32) -> (u8, u8, u8) { + // Brightness based on iteration count - fewer iterations = brighter + let t = (iterations as f64) / (max_iterations as f64); + let brightness = (1.0 - t * 0.7).max(0.3); + + // 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.05, 0.25, 0.55), + }; + + ( + (r * brightness * 255.0) as u8, + (g * brightness * 255.0) as u8, + (b * brightness * 255.0) as u8, + ) +} diff --git a/crates/fractal-engine/src/lib.rs b/crates/fractal-engine/src/lib.rs new file mode 100644 index 0000000..10c428c --- /dev/null +++ b/crates/fractal-engine/src/lib.rs @@ -0,0 +1,5 @@ +pub mod color; +pub mod newton; + +#[cfg(all(target_arch = "wasm32", target_os = "unknown"))] +pub mod wasm; diff --git a/crates/fractal-engine/src/newton.rs b/crates/fractal-engine/src/newton.rs new file mode 100644 index 0000000..8274e0c --- /dev/null +++ b/crates/fractal-engine/src/newton.rs @@ -0,0 +1,76 @@ +/// Newton's method fractal for z^3 - 1 +/// The three roots are at 120 degree intervals on the unit circle + +const MAX_ITERATIONS: u32 = 32; +const TOLERANCE_SQ: f64 = 1e-6; + +/// Compute Newton's method iteration for z^3 - 1 +/// Returns (root_index, iterations) where root_index is 0, 1, or 2 +/// Time parameter rotates the viewing angle for animation +pub fn compute(z_re: f64, z_im: f64, time: f64) -> (u8, u32) { + // Rotate input coordinates to animate the fractal + let cos_t = time.cos(); + let sin_t = time.sin(); + let mut zr = z_re * cos_t - z_im * sin_t; + let mut zi = z_re * sin_t + z_im * cos_t; + + // 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) + ]; + + for iter in 0..MAX_ITERATIONS { + // Compute z^2 + let z2_re = zr * zr - zi * zi; + let z2_im = 2.0 * zr * zi; + + // Compute z^3 + let z3_re = zr * z2_re - zi * z2_im; + let z3_im = zr * z2_im + zi * z2_re; + + // Check distance to each root + for (idx, &(root_re, root_im)) in ROOTS.iter().enumerate() { + let dr = zr - root_re; + let di = zi - root_im; + if dr * dr + di * di < TOLERANCE_SQ { + return (idx as u8, iter); + } + } + + // Newton's method: z = z - (z^3 - 1)/(3z^2) + // Simplifies to: z = (2z^3 + 1)/(3z^2) + + // Denominator: 3z^2 + let denom_re = 3.0 * z2_re; + let denom_im = 3.0 * z2_im; + let denom_mag_sq = denom_re * denom_re + denom_im * denom_im; + + if denom_mag_sq < 1e-12 { + return (0, MAX_ITERATIONS); + } + + // Numerator: 2z^3 + 1 + let num_re = 2.0 * z3_re + 1.0; + let num_im = 2.0 * z3_im; + + // Complex division + zr = (num_re * denom_re + num_im * denom_im) / denom_mag_sq; + zi = (num_im * denom_re - num_re * denom_im) / denom_mag_sq; + } + + // Find closest root + let mut closest = 0u8; + let mut min_dist = f64::MAX; + for (idx, &(root_re, root_im)) in ROOTS.iter().enumerate() { + let dr = zr - root_re; + let di = zi - root_im; + let dist = dr * dr + di * di; + if dist < min_dist { + min_dist = dist; + closest = idx as u8; + } + } + (closest, MAX_ITERATIONS) +} diff --git a/crates/fractal-engine/src/wasm.rs b/crates/fractal-engine/src/wasm.rs new file mode 100644 index 0000000..f49fe17 --- /dev/null +++ b/crates/fractal-engine/src/wasm.rs @@ -0,0 +1,106 @@ +use wasm_bindgen::prelude::*; +use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData}; + +use crate::color::ocean_palette; +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> { + let window = web_sys::window().ok_or("no window")?; + let document = window.document().ok_or("no document")?; + let canvas = document + .get_element_by_id(canvas_id) + .ok_or("no canvas element")? + .dyn_into::()?; + + let context = canvas + .get_context("2d")? + .ok_or("no 2d context")? + .dyn_into::()?; + + Ok((canvas, context)) +} + +/// Render the Newton fractal with animation (time parameter rotates the view) +#[wasm_bindgen] +pub fn render_fractal_animated(canvas_id: &str, time: f64, scale: u32) -> Result<(), JsValue> { + let (canvas, context) = get_canvas_and_context(canvas_id)?; + + let canvas_width = canvas.width() as usize; + let canvas_height = canvas.height() as usize; + + // Render at reduced resolution + let scale = scale.max(1) as usize; + let width = canvas_width / scale; + let height = canvas_height / scale; + + // Create RGBA buffer + let mut pixels: Vec = vec![0; width * height * 4]; + + // Complex plane bounds + let view_size = 3.0; + let aspect = width as f64 / height as f64; + + for y in 0..height { + for x in 0..width { + // Map pixel to complex plane (centered at origin) + let z_re = (x as f64 / width as f64 - 0.5) * view_size * aspect; + let z_im = (y as f64 / height as f64 - 0.5) * view_size; + + // Compute Newton iteration + let (root_index, iterations) = compute(z_re, z_im, time); + + // Get color from ocean palette + let (r, g, b) = ocean_palette(root_index, iterations, MAX_ITERATIONS); + + // Write to pixel buffer (RGBA) + let idx = (y * width + x) * 4; + pixels[idx] = r; + pixels[idx + 1] = g; + pixels[idx + 2] = b; + pixels[idx + 3] = 255; + } + } + + // 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)?; + + // Scale up when drawing to canvas + if scale > 1 { + // Draw to a temporary position then scale + context.put_image_data(&image_data, 0.0, 0.0)?; + + // Use drawImage to scale up + let temp_canvas = document_create_canvas(width as u32, height as u32)?; + let temp_ctx = temp_canvas + .get_context("2d")? + .ok_or("no temp 2d context")? + .dyn_into::()?; + temp_ctx.put_image_data(&image_data, 0.0, 0.0)?; + + context.set_image_smoothing_enabled(false); + 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 + )?; + } else { + context.put_image_data(&image_data, 0.0, 0.0)?; + } + + Ok(()) +} + +fn document_create_canvas(width: u32, height: u32) -> Result { + let window = web_sys::window().ok_or("no window")?; + let document = window.document().ok_or("no document")?; + let canvas = document + .create_element("canvas")? + .dyn_into::()?; + canvas.set_width(width); + canvas.set_height(height); + Ok(canvas) +} diff --git a/routes/root/index.html b/routes/root/index.html index 12d296f..052f0de 100755 --- a/routes/root/index.html +++ b/routes/root/index.html @@ -6,17 +6,39 @@ -
-

Zachery Aaron Shores-Chmielewski

-
+ -