fix: fractal animation on webgl, targeted builds

Fractal animation (Newton's fractal) now animates smoothly. Rebuilds now can be targeted based on which module was changed.
This commit is contained in:
Zachery Aaron Shores-Chmielewski 2026-02-15 12:39:10 +07:00
parent 4200ca77df
commit d10f539e06
7 changed files with 574 additions and 273 deletions

View file

@ -9,6 +9,15 @@ use std::{
const WEB_BUILD_DIR: &str = "web-build"; const WEB_BUILD_DIR: &str = "web-build";
#[derive(clap::ValueEnum, Clone, Debug, PartialEq)]
enum BuildTarget {
Messages,
Wasm,
Server,
Routes,
Keys,
}
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
struct Cli { struct Cli {
@ -19,7 +28,11 @@ struct Cli {
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
enum Commands { enum Commands {
/// Builds the website and server, including wasm files /// Builds the website and server, including wasm files
Build, Build {
/// Build only specific targets (comma-separated). Omit for full build.
#[arg(long, value_delimiter = ',')]
only: Vec<BuildTarget>,
},
/// Runs the server locally /// Runs the server locally
Start { Start {
@ -64,16 +77,20 @@ fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
match &cli.command { match &cli.command {
Commands::Build => { Commands::Build { only } => {
println!("Executing build command..."); println!("Executing build command...");
build(); if only.is_empty() {
build_full();
} else {
build_targeted(only);
}
} }
Commands::Start { rebuild } => { Commands::Start { rebuild } => {
println!("Starting server..."); println!("Starting server...");
if *rebuild { if *rebuild {
println!("Rebuilding web pages..."); println!("Rebuilding web pages...");
build(); build_full();
} // Change the current working directory of the running Rust program } // Change the current working directory of the running Rust program
println!("Changing directory to {WEB_BUILD_DIR}..."); println!("Changing directory to {WEB_BUILD_DIR}...");
let cd_result = std::env::set_current_dir(WEB_BUILD_DIR); let cd_result = std::env::set_current_dir(WEB_BUILD_DIR);
@ -254,9 +271,15 @@ fn handle_command_status(status_result: std::io::Result<ExitStatus>, command_nam
} }
} }
fn build() { // --- Build steps ---
fn ensure_web_build_dir() {
let status = Command::new("mkdir").args(["-p", WEB_BUILD_DIR]).status();
handle_command_status(status, "mkdir web-build");
}
fn build_message_tools_wasm() {
println!("Building wasm package (message-tools)..."); println!("Building wasm package (message-tools)...");
// build the wasm package
let status = Command::new("wasm-pack") let status = Command::new("wasm-pack")
.arg("build") .arg("build")
.arg("--out-dir") .arg("--out-dir")
@ -266,20 +289,10 @@ fn build() {
.arg("no-modules") .arg("no-modules")
.status(); .status();
handle_command_status(status, "wasm-pack build message-tools"); handle_command_status(status, "wasm-pack build message-tools");
}
println!("Building wasm package (fractal-engine)..."); fn build_server() {
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..."); println!("Building server executable...");
// build the server executable
let status = Command::new("cargo") let status = Command::new("cargo")
.arg("build") .arg("build")
.arg("--release") .arg("--release")
@ -287,28 +300,34 @@ fn build() {
.args(["--target", "x86_64-unknown-linux-musl"]) .args(["--target", "x86_64-unknown-linux-musl"])
.status(); .status();
handle_command_status(status, "cargo build server"); handle_command_status(status, "cargo build server");
}
let server_bin = "target/x86_64-unknown-linux-musl/release/server"; fn copy_routes() {
ensure_web_build_dir();
println!("Creating build directory..."); println!("Copying routes...");
// make webpage build directory if it doesn't already exist
let status = Command::new("mkdir").args(["-p", WEB_BUILD_DIR]).status();
handle_command_status(status, "mkdir web-build");
println!("Copying files...");
// copy relevant files (use trailing slash to merge into existing directory)
let status = Command::new("cp") let status = Command::new("cp")
.args(["-R", "-f", "routes"]) .args(["-R", "-f", "routes"])
.arg(WEB_BUILD_DIR) .arg(WEB_BUILD_DIR)
.status(); .status();
handle_command_status(status, "cp routes"); handle_command_status(status, "cp routes");
}
fn copy_server_binary() {
ensure_web_build_dir();
let server_bin = "target/x86_64-unknown-linux-musl/release/server";
println!("Copying server binary...");
let status = Command::new("cp") let status = Command::new("cp")
.arg("-f") .arg("-f")
.arg(server_bin) .arg(server_bin)
.arg(format!("{WEB_BUILD_DIR}")) .arg(format!("{WEB_BUILD_DIR}"))
.status(); .status();
handle_command_status(status, "cp server bin"); handle_command_status(status, "cp server bin");
}
fn ensure_keys_and_copy() {
ensure_web_build_dir();
let pubkey_file = "target/public_keys.json"; let pubkey_file = "target/public_keys.json";
@ -333,7 +352,7 @@ fn build() {
let status = Command::new("mkdir") let status = Command::new("mkdir")
.args(["-p", &format!("{WEB_BUILD_DIR}/pubkeys")]) .args(["-p", &format!("{WEB_BUILD_DIR}/pubkeys")])
.status(); .status();
handle_command_status(status, "mkdir web-build"); handle_command_status(status, "mkdir pubkeys");
let status = Command::new("cp") let status = Command::new("cp")
.arg("-f") .arg("-f")
@ -341,6 +360,49 @@ fn build() {
.arg(format!("{WEB_BUILD_DIR}/pubkeys/")) .arg(format!("{WEB_BUILD_DIR}/pubkeys/"))
.status(); .status();
handle_command_status(status, "cp pubkeys"); handle_command_status(status, "cp pubkeys");
}
// --- Build orchestration ---
fn build_full() {
build_message_tools_wasm();
build_server();
copy_routes();
copy_server_binary();
ensure_keys_and_copy();
println!("\nBuild successful!"); println!("\nBuild successful!");
} }
fn build_targeted(targets: &[BuildTarget]) {
let mut did_wasm = false;
for target in targets {
match target {
BuildTarget::Messages => {
build_message_tools_wasm();
did_wasm = true;
}
BuildTarget::Wasm => {
build_message_tools_wasm();
did_wasm = true;
}
BuildTarget::Server => {
build_server();
copy_server_binary();
}
BuildTarget::Routes => {
copy_routes();
}
BuildTarget::Keys => {
ensure_keys_and_copy();
}
}
}
// WASM output lands inside routes/, so always copy routes after any WASM build
if did_wasm {
copy_routes();
}
println!("\nTargeted build successful!");
}

View file

@ -81,7 +81,12 @@ pub fn render_fractal_animated(canvas_id: &str, time: f64, scale: u32) -> Result
.dyn_into::<CanvasRenderingContext2d>()?; .dyn_into::<CanvasRenderingContext2d>()?;
temp_ctx.put_image_data(&image_data, 0.0, 0.0)?; temp_ctx.put_image_data(&image_data, 0.0, 0.0)?;
context.set_image_smoothing_enabled(false); context.set_image_smoothing_enabled(true);
js_sys::Reflect::set(
context.as_ref(),
&JsValue::from_str("imageSmoothingQuality"),
&JsValue::from_str("high"),
)?;
context.draw_image_with_html_canvas_element_and_dw_and_dh( context.draw_image_with_html_canvas_element_and_dw_and_dh(
&temp_canvas, &temp_canvas,
0.0, 0.0, 0.0, 0.0,

View file

@ -5,11 +5,18 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Contact</title> <title>Contact</title>
<style> <style>
*,
*::before,
*::after {
box-sizing: border-box;
}
body { body {
font-family: system-ui, sans-serif; font-family: system-ui, -apple-system, sans-serif;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
background: #060a10;
} }
#fractal-canvas { #fractal-canvas {
@ -29,22 +36,27 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
animation: fadeInUp 800ms ease 300ms both;
} }
.contact-box { .glass-card {
background: rgba(0, 0, 0, 0.65); background: rgba(10, 14, 20, 0.55);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 2.5rem 3rem; padding: 2.5rem 3rem;
border-radius: 10px;
text-align: center; text-align: center;
min-width: 250px; min-width: 250px;
} }
.contact-box h2 { .glass-card h2 {
color: #fff; color: #fff;
margin: 0 0 1.5rem 0; margin: 0 0 1.5rem 0;
font-weight: normal; font-weight: 300;
font-family: "Gill Sans", sans-serif; font-family: system-ui, -apple-system, sans-serif;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); font-size: 1.6rem;
letter-spacing: 0.01em;
} }
.contact-list { .contact-list {
@ -54,42 +66,55 @@
} }
.contact-list li { .contact-list li {
margin: 1rem 0; margin: 0.75rem 0;
} }
.contact-list li a { .contact-list li a {
text-decoration: none; text-decoration: none;
color: #fff; color: rgba(255, 255, 255, 0.75);
padding: 0.6rem 1.5rem; padding: 0.6rem 1.5rem;
display: inline-block; display: inline-block;
background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 4px; border-radius: 8px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); font-size: 0.85rem;
transition: background 0.2s; font-weight: 400;
font-size: 1.1rem; letter-spacing: 0.06em;
text-transform: uppercase;
transition: all 250ms ease;
} }
.contact-list li a:hover { .contact-list li a:hover {
background: rgba(255, 255, 255, 0.2); color: #fff;
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
} }
nav.back-link { .back-link {
margin-top: 2rem; margin-top: 2rem;
} }
nav.back-link a { .back-link a {
text-decoration: none; text-decoration: none;
color: #fff; color: rgba(255, 255, 255, 0.4);
padding: 0.5rem 1rem; font-size: 0.8rem;
display: inline-block; letter-spacing: 0.04em;
background: rgba(0, 0, 0, 0.3); transition: color 250ms ease;
border-radius: 4px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
transition: background 0.2s;
} }
nav.back-link a:hover { .back-link a:hover {
background: rgba(0, 0, 0, 0.5); color: rgba(255, 255, 255, 0.7);
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
</style> </style>
</head> </head>
@ -97,7 +122,7 @@
<canvas id="fractal-canvas"></canvas> <canvas id="fractal-canvas"></canvas>
<div class="content-overlay"> <div class="content-overlay">
<div class="contact-box"> <div class="glass-card">
<h2>Contact</h2> <h2>Contact</h2>
<ul class="contact-list"> <ul class="contact-list">
<li><a href="mailto:">Email me</a></li> <li><a href="mailto:">Email me</a></li>
@ -110,40 +135,7 @@
</nav> </nav>
</div> </div>
<script src="/routes/root/pkg/fractal_engine.js"></script> <script src="/routes/root/fractal-gl.js"></script>
<script> <script>initFractal('fractal-canvas');</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;
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> </body>
</html> </html>

View file

@ -5,6 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Direct Message</title> <title>Direct Message</title>
<style> <style>
*,
*::before,
*::after {
box-sizing: border-box;
}
body { body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -12,8 +18,8 @@
padding-top: 50px; padding-top: 50px;
min-height: 120vh; min-height: 120vh;
margin: 0; margin: 0;
font-family: Arial, sans-serif; font-family: system-ui, -apple-system, sans-serif;
background-color: #111; background: #060a10;
} }
#fractal-canvas { #fractal-canvas {
@ -28,40 +34,37 @@
.form-container { .form-container {
position: relative; position: relative;
z-index: 1; z-index: 1;
background-color: rgba(0, 0, 0, 0.7); background: rgba(10, 14, 20, 0.55);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
color: #fff; color: #fff;
padding: 30px; padding: 2.5rem 3rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
width: 90%; width: 90%;
max-width: 800px; max-width: 560px;
min-height: 400px; min-height: 400px;
max-height: 1000px;
margin-bottom: 20px; margin-bottom: 20px;
animation: fadeInUp 800ms ease 300ms both;
} }
nav.directory { .back-link {
position: relative; position: relative;
z-index: 1; z-index: 1;
width: 90%; margin-top: 0.5rem;
max-width: 800px; animation: fadeInUp 800ms ease 500ms both;
margin: 0 auto;
text-align: center;
} }
nav.directory dt a { .back-link a {
text-decoration: none; text-decoration: none;
color: #fff; color: rgba(255, 255, 255, 0.4);
padding: 0.5rem 1rem; font-size: 0.8rem;
display: inline-block; letter-spacing: 0.04em;
background: rgba(0, 0, 0, 0.3); transition: color 250ms ease;
border-radius: 4px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
transition: background 0.2s;
} }
nav.directory dt a:hover { .back-link a:hover {
background: rgba(0, 0, 0, 0.5); color: rgba(255, 255, 255, 0.7);
} }
.input-group { .input-group {
@ -71,46 +74,115 @@
label { label {
display: block; display: block;
margin-bottom: 5px; margin-bottom: 5px;
font-weight: bold; font-weight: 300;
font-size: 0.85rem;
letter-spacing: 0.04em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.6);
} }
input[type="text"], input[type="text"],
input[type="password"] { input[type="password"] {
width: 100%; width: 100%;
padding: 8px; padding: 10px 12px;
border: 1px solid #555; border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px; border-radius: 8px;
box-sizing: border-box; background: rgba(255, 255, 255, 0.06);
background: rgba(255, 255, 255, 0.1);
color: #fff; color: #fff;
font-family: inherit;
font-size: 0.95rem;
transition: border-color 250ms ease, box-shadow 250ms ease;
}
input[type="text"]:focus,
input[type="password"]:focus {
outline: none;
border-color: rgba(120, 200, 220, 0.3);
box-shadow: 0 0 0 3px rgba(120, 200, 220, 0.1);
} }
textarea { textarea {
width: 100%; width: 100%;
height: 120px; height: 120px;
padding: 8px; padding: 10px 12px;
border: 1px solid #555; border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px; border-radius: 8px;
resize: vertical; resize: vertical;
box-sizing: border-box; background: rgba(255, 255, 255, 0.06);
background: rgba(255, 255, 255, 0.1);
color: #fff; color: #fff;
font-family: inherit;
font-size: 0.95rem;
transition: border-color 250ms ease, box-shadow 250ms ease;
}
textarea:focus {
outline: none;
border-color: rgba(120, 200, 220, 0.3);
box-shadow: 0 0 0 3px rgba(120, 200, 220, 0.1);
}
button[type="submit"] {
width: 100%;
padding: 10px;
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.75);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
cursor: pointer;
font-family: inherit;
font-size: 0.85rem;
font-weight: 400;
letter-spacing: 0.06em;
text-transform: uppercase;
transition: all 250ms ease;
}
button[type="submit"]:hover {
color: #fff;
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
} }
.expandable-section { .expandable-section {
margin-top: 20px; margin-top: 20px;
} }
.expandable-header { .expandable-header {
cursor: pointer; cursor: pointer;
padding: 10px; padding: 10px;
background-color: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.06);
border-radius: 4px; border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
color: rgba(255, 255, 255, 0.5);
font-size: 0.85rem;
transition: all 250ms ease;
} }
.expandable-header:hover {
color: rgba(255, 255, 255, 0.7);
border-color: rgba(255, 255, 255, 0.15);
}
.expandable-content { .expandable-content {
display: none; display: none;
padding: 10px; padding: 10px;
background-color: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.03);
border-radius: 0 0 4px 4px; border-radius: 0 0 8px 8px;
color: rgba(255, 255, 255, 0.6);
font-size: 0.9rem;
line-height: 1.5;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
</style> </style>
<script src="/routes/contact/pkg/message_tools.js"></script> <script src="/routes/contact/pkg/message_tools.js"></script>
@ -230,20 +302,7 @@
</div> </div>
<div class="input-group"> <div class="input-group">
<button <button type="submit">Submit</button>
type="submit"
style="
width: 100%;
padding: 10px;
background-color: rgba(255, 255, 255, 0.15);
color: #fff;
border: 1px solid #555;
border-radius: 4px;
cursor: pointer;
"
>
Submit
</button>
</div> </div>
</form> </form>
@ -261,44 +320,11 @@
</div> </div>
</div> </div>
<nav class="directory"> <nav class="back-link">
<dt><a href="/contact">back to contact</a></dt> <a href="/contact">back to contact</a>
</nav> </nav>
<script src="/routes/root/pkg/fractal_engine.js"></script> <script src="/routes/root/fractal-gl.js"></script>
<script> <script>initFractal('fractal-canvas');</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;
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> </body>
</html> </html>

152
routes/root/fractal-gl.js Normal file
View file

@ -0,0 +1,152 @@
// WebGL Newton fractal renderer (z^3 - 1, ocean palette)
// Ported from crates/fractal-engine/src/{newton.rs, color.rs}
function initFractal(canvasId) {
var canvas = document.getElementById(canvasId);
if (!canvas) return;
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%)';
return;
}
// --- Shaders ---
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;',
'',
'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);',
'}'
].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));
return null;
}
return s;
}
var vs = compile(gl.VERTEX_SHADER, VERT_SRC);
var fs = compile(gl.FRAGMENT_SHADER, FRAG_SRC);
if (!vs || !fs) return;
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;
}
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);
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');
// --- Resize ---
function resize() {
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;
gl.uniform1f(uTime, t);
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}

View file

@ -2,13 +2,21 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Zach's page</title> <title>Zach's page</title>
<style> <style>
*,
*::before,
*::after {
box-sizing: border-box;
}
body { body {
font-family: system-ui, sans-serif; font-family: system-ui, -apple-system, sans-serif;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
background: #060a10;
} }
#fractal-canvas { #fractal-canvas {
@ -28,42 +36,68 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
animation: fadeInUp 800ms ease 300ms both;
}
.glass-card {
background: rgba(10, 14, 20, 0.55);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 2.5rem 3rem;
text-align: center;
} }
header h2 { header h2 {
font-weight: normal; font-weight: 300;
font-family: "Gill Sans", sans-serif; font-family: system-ui, -apple-system, sans-serif;
text-align: center; text-align: center;
color: #fff; color: #fff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); margin: 0 0 1.5rem 0;
background: rgba(0, 0, 0, 0.3); font-size: 1.6rem;
padding: 0.5rem 1.5rem; letter-spacing: 0.01em;
border-radius: 8px;
} }
nav.directory { .nav-links {
text-align: center; display: flex;
padding: 1rem 0; gap: 1rem;
justify-content: center;
list-style: none;
padding: 0;
margin: 0;
} }
nav.directory dt { .nav-links a {
margin: 1.2rem 0;
font-size: 1.1rem;
}
nav.directory dt a {
text-decoration: none; text-decoration: none;
color: #fff; color: rgba(255, 255, 255, 0.75);
padding: 0.5rem 1rem; padding: 0.5rem 1.5rem;
display: inline-block; display: inline-block;
background: rgba(0, 0, 0, 0.3); border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 4px; border-radius: 8px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); font-size: 0.85rem;
transition: background 0.2s; font-weight: 400;
letter-spacing: 0.06em;
text-transform: uppercase;
transition: all 250ms ease;
} }
nav.directory dt a:hover { .nav-links a:hover {
background: rgba(0, 0, 0, 0.5); color: #fff;
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
</style> </style>
</head> </head>
@ -71,54 +105,21 @@
<canvas id="fractal-canvas"></canvas> <canvas id="fractal-canvas"></canvas>
<div class="content-overlay"> <div class="content-overlay">
<header> <div class="glass-card">
<h2>Zachery Aaron Shores-Chmielewski</h2> <header>
</header> <h2>Zachery Aaron Shores-Chmielewski</h2>
</header>
<nav class="directory">
<nav> <nav>
<dl> <div class="nav-links">
<dt><a href="./who">Who</a></dt> <a href="./who">Who</a>
<dt><a href="./contact">Contact</a></dt> <a href="./contact">Contact</a>
</dl> </div>
</nav> </nav>
</nav> </div>
</div> </div>
<script src="/routes/root/pkg/fractal_engine.js"></script> <script src="/routes/root/fractal-gl.js"></script>
<script> <script>initFractal('fractal-canvas');</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> </body>
</html> </html>

View file

@ -2,50 +2,113 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Zach's page</title> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Who</title>
<style> <style>
body { *,
font-family: system-ui, sans-serif; *::before,
background-color: Cornsilk; *::after {
} box-sizing: border-box;
header h2 {
font-weight: normal;
font-family: "Gill Sans", sans-serif;
text-align: center;
} }
body { body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: hidden;
background: #060a10;
}
#fractal-canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.content-overlay {
position: relative;
z-index: 1;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
animation: fadeInUp 800ms ease 300ms both;
}
.glass-card {
background: rgba(10, 14, 20, 0.55);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 2.5rem 3rem;
text-align: center; text-align: center;
min-width: 250px;
color: rgba(255, 255, 255, 0.7);
line-height: 1.6;
} }
nav.directory { .glass-card h2 {
text-align: center; color: #fff;
padding: 1rem 0; margin: 0 0 1rem 0;
font-weight: 300;
font-size: 1.6rem;
letter-spacing: 0.01em;
} }
nav.directory dt { .glass-card p {
margin: 1.2rem 0; margin: 0.5rem 0;
font-size: 1.1rem; font-size: 1rem;
} }
nav.directory dt a { .back-link {
margin-top: 2rem;
}
.back-link a {
text-decoration: none; text-decoration: none;
color: #333; color: rgba(255, 255, 255, 0.4);
padding: 0.5rem 1rem; font-size: 0.8rem;
display: inline-block; letter-spacing: 0.04em;
transition: color 250ms ease;
}
.back-link a:hover {
color: rgba(255, 255, 255, 0.7);
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
</style> </style>
</head> </head>
<body> <body>
<div> <canvas id="fractal-canvas"></canvas>
<p>No one in particular :)</p>
<p>I'll write more later...</p> <div class="content-overlay">
<div class="glass-card">
<h2>Who</h2>
<p>No one in particular :)</p>
<p>I'll write more later...</p>
</div>
<nav class="back-link">
<a href="/">back to home</a>
</nav>
</div> </div>
<nav class="directory"> <script src="/routes/root/fractal-gl.js"></script>
<dt><a href="/">back to home</a></dt> <script>initFractal('fractal-canvas');</script>
</nav>
</body> </body>
</html> </html>