feat: main page background #1
10 changed files with 352 additions and 23 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,3 +1,5 @@
|
|||
/target
|
||||
/web-build
|
||||
.local
|
||||
.local
|
||||
**.bak
|
||||
**/dist/
|
||||
|
|
|
|||
19
Cargo.lock
generated
19
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"crates/cli",
|
||||
"crates/fractal-engine",
|
||||
"crates/message-tools",
|
||||
"crates/server"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ fn handle_command_status(status_result: std::io::Result<ExitStatus>, 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");
|
||||
|
||||
|
|
|
|||
15
crates/fractal-engine/Cargo.toml
Normal file
15
crates/fractal-engine/Cargo.toml
Normal file
|
|
@ -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"
|
||||
25
crates/fractal-engine/src/color.rs
Normal file
25
crates/fractal-engine/src/color.rs
Normal file
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
5
crates/fractal-engine/src/lib.rs
Normal file
5
crates/fractal-engine/src/lib.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod color;
|
||||
pub mod newton;
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
|
||||
pub mod wasm;
|
||||
76
crates/fractal-engine/src/newton.rs
Normal file
76
crates/fractal-engine/src/newton.rs
Normal file
|
|
@ -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)
|
||||
}
|
||||
106
crates/fractal-engine/src/wasm.rs
Normal file
106
crates/fractal-engine/src/wasm.rs
Normal file
|
|
@ -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::<HtmlCanvasElement>()?;
|
||||
|
||||
let context = canvas
|
||||
.get_context("2d")?
|
||||
.ok_or("no 2d context")?
|
||||
.dyn_into::<CanvasRenderingContext2d>()?;
|
||||
|
||||
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<u8> = 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::<CanvasRenderingContext2d>()?;
|
||||
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<HtmlCanvasElement, JsValue> {
|
||||
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::<HtmlCanvasElement>()?;
|
||||
canvas.set_width(width);
|
||||
canvas.set_height(height);
|
||||
Ok(canvas)
|
||||
}
|
||||
|
|
@ -6,17 +6,39 @@
|
|||
<style>
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
background-color: Cornsilk;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#fractal-canvas {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.content-overlay {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
header h2 {
|
||||
font-weight: normal;
|
||||
font-family: "Gill Sans", sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #fff;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 0.5rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
nav.directory {
|
||||
|
|
@ -31,25 +53,72 @@
|
|||
|
||||
nav.directory dt a {
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
color: #fff;
|
||||
padding: 0.5rem 1rem;
|
||||
display: inline-block;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 4px;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
nav.directory dt a:hover {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h2>Zachery Aaron Shores-Chmielewski</h2>
|
||||
</header>
|
||||
<canvas id="fractal-canvas"></canvas>
|
||||
|
||||
<nav class="directory">
|
||||
<nav>
|
||||
<dl>
|
||||
<dt><a href="./who">Who</a></dt>
|
||||
<!-- <dt><a href="#">Writing</a></dt> -->
|
||||
<dt><a href="./contact">Send me a message</a></dt>
|
||||
</dl>
|
||||
<div class="content-overlay">
|
||||
<header>
|
||||
<h2>Zachery Aaron Shores-Chmielewski</h2>
|
||||
</header>
|
||||
|
||||
<nav class="directory">
|
||||
<nav>
|
||||
<dl>
|
||||
<dt><a href="./who">Who</a></dt>
|
||||
<dt><a href="./contact">Send me a message</a></dt>
|
||||
</dl>
|
||||
</nav>
|
||||
</nav>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<script src="/routes/root/pkg/fractal_engine.js"></script>
|
||||
<script>
|
||||
(async function() {
|
||||
await wasm_bindgen('/routes/root/pkg/fractal_engine_bg.wasm');
|
||||
|
||||
const canvas = document.getElementById('fractal-canvas');
|
||||
|
||||
function resizeCanvas() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
let startTime = performance.now();
|
||||
const animationSpeed = 0.0001;
|
||||
const scale = 4; // Render at 1/4 resolution for performance
|
||||
|
||||
function animate() {
|
||||
const elapsed = performance.now() - startTime;
|
||||
const time = elapsed * animationSpeed;
|
||||
|
||||
try {
|
||||
wasm_bindgen.render_fractal_animated('fractal-canvas', time, scale);
|
||||
} catch (e) {
|
||||
console.error('Fractal render error:', e);
|
||||
}
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
animate();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in a new issue