feat: fractal animation
Add a pleasant fractal animation background to the main page
This commit is contained in:
parent
fbad975386
commit
50e3a49d4f
14 changed files with 2178 additions and 23 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,3 +1,5 @@
|
||||||
/target
|
/target
|
||||||
/web-build
|
/web-build
|
||||||
.local
|
.local
|
||||||
|
.bak
|
||||||
|
*/dist/
|
||||||
|
|
|
||||||
19
Cargo.lock
generated
19
Cargo.lock
generated
|
|
@ -331,6 +331,15 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fractal-engine"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
|
|
@ -1104,6 +1113,16 @@ dependencies = [
|
||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"crates/cli",
|
"crates/cli",
|
||||||
|
"crates/fractal-engine",
|
||||||
"crates/message-tools",
|
"crates/message-tools",
|
||||||
"crates/server"
|
"crates/server"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -255,7 +255,7 @@ fn handle_command_status(status_result: std::io::Result<ExitStatus>, command_nam
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build() {
|
fn build() {
|
||||||
println!("Building wasm package...");
|
println!("Building wasm package (message-tools)...");
|
||||||
// build the wasm package
|
// build the wasm package
|
||||||
let status = Command::new("wasm-pack")
|
let status = Command::new("wasm-pack")
|
||||||
.arg("build")
|
.arg("build")
|
||||||
|
|
@ -265,7 +265,18 @@ fn build() {
|
||||||
.arg("--target")
|
.arg("--target")
|
||||||
.arg("no-modules")
|
.arg("no-modules")
|
||||||
.status();
|
.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...");
|
println!("Building server executable...");
|
||||||
// build the server executable
|
// build the server executable
|
||||||
|
|
@ -285,10 +296,10 @@ fn build() {
|
||||||
handle_command_status(status, "mkdir web-build");
|
handle_command_status(status, "mkdir web-build");
|
||||||
|
|
||||||
println!("Copying files...");
|
println!("Copying files...");
|
||||||
// copy relevant 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(format!("{WEB_BUILD_DIR}/routes"))
|
.arg(WEB_BUILD_DIR)
|
||||||
.status();
|
.status();
|
||||||
handle_command_status(status, "cp routes");
|
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)
|
||||||
|
}
|
||||||
1686
crates/pages/message-box/dist/message-box-ce532c58cb981b7a.js
vendored
Normal file
1686
crates/pages/message-box/dist/message-box-ce532c58cb981b7a.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
BIN
crates/pages/message-box/dist/message-box-ce532c58cb981b7a_bg.wasm
vendored
Normal file
BIN
crates/pages/message-box/dist/message-box-ce532c58cb981b7a_bg.wasm
vendored
Normal file
Binary file not shown.
83
crates/pages/message-box/src/app.rs.bak
Normal file
83
crates/pages/message-box/src/app.rs.bak
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
use pages_common::{Navbar, traits::Module};
|
||||||
|
use crate::modules::PasscodeInfo;
|
||||||
|
|
||||||
|
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
|
||||||
|
#[derive(serde::Deserialize, serde::Serialize, Default)]
|
||||||
|
#[serde(default)] // if we add new fields, give them default values when deserializing old state
|
||||||
|
pub struct MessageBoxApp {
|
||||||
|
navbar: Navbar,
|
||||||
|
|
||||||
|
// Message box app
|
||||||
|
message_box: MessageBox,
|
||||||
|
|
||||||
|
// Displays QR code, password seed, and link information for saving
|
||||||
|
passcode_info: PasscodeInfo,
|
||||||
|
|
||||||
|
// The selected view for display
|
||||||
|
page: PageView,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub enum PageView {
|
||||||
|
#[default]
|
||||||
|
Welcome,
|
||||||
|
MessageBox,
|
||||||
|
PasscodeInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, serde::Deserialize, serde::Serialize)]
|
||||||
|
struct MessageBox(());
|
||||||
|
|
||||||
|
impl MessageBoxApp {
|
||||||
|
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||||
|
// Load previous app state (if any).
|
||||||
|
// Note that you must enable the `persistence` feature for this to work.
|
||||||
|
if let Some(_storage) = cc.storage {
|
||||||
|
let navbar = Navbar::new(cc);
|
||||||
|
let message_box = MessageBox::default();
|
||||||
|
let passcode_info = PasscodeInfo::new(cc);
|
||||||
|
let page = PageView::default();
|
||||||
|
Self {
|
||||||
|
navbar,
|
||||||
|
message_box,
|
||||||
|
passcode_info,
|
||||||
|
page,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn welcome_page(&mut self, ctx: &egui::Context) {
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.heading("Send me a message:");
|
||||||
|
if ui.add(egui::Button::new("Generate New Key")).clicked() {
|
||||||
|
self.page = PageView::PasscodeInfo;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// return home
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.hyperlink_to("back to home", "/");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for MessageBoxApp {
|
||||||
|
/// Called by the framework to save state before shutdown.
|
||||||
|
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||||
|
self.navbar.save(storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
self.navbar.update(ctx);
|
||||||
|
|
||||||
|
match self.page {
|
||||||
|
PageView::Welcome => self.welcome_page(ctx),
|
||||||
|
PageView::PasscodeInfo => self.passcode_info.update(ctx),
|
||||||
|
_ => (), // do nothing...
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
57
crates/pages/message-box/src/modules/passcode_info.rs.bak
Normal file
57
crates/pages/message-box/src/modules/passcode_info.rs.bak
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
use egui::Widget;
|
||||||
|
use egui_qr::QrCodeWidget;
|
||||||
|
use pages_common::traits::Module;
|
||||||
|
use util::crypto::PublicKey;
|
||||||
|
|
||||||
|
const USER_PUBLIC_KEY_STORAGE: &str = "user_pubkey_storage";
|
||||||
|
|
||||||
|
// FIXME: Grab this from env when building
|
||||||
|
const URL: &str = "https://zachery.lol/contact";
|
||||||
|
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct PasscodeInfo {
|
||||||
|
public_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Module for PasscodeInfo {
|
||||||
|
const STORAGE_KEY: &str = "passcode_info_storage_key";
|
||||||
|
fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||||
|
// get or set public key
|
||||||
|
let mut public_key = None;
|
||||||
|
if let Some(storage) = cc.storage {
|
||||||
|
if let Some(pk) = eframe::get_value(storage, USER_PUBLIC_KEY_STORAGE) {
|
||||||
|
public_key = Some(pk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
public_key: public_key.unwrap_or_else(|| {
|
||||||
|
let key: PublicKey = message_tools::gen_keys::Keypair::new_random()
|
||||||
|
.pubkey_bytes()
|
||||||
|
.try_into()
|
||||||
|
.expect("Failed to generate keypair");
|
||||||
|
key.to_string()
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn update(&mut self, ctx: &egui::Context) {
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
let url = format!("{}?pubkey={}", URL, &self.public_key);
|
||||||
|
ui.label(&url);
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.allocate_ui(
|
||||||
|
egui::Vec2 { x: 120.0, y: 120.0 },
|
||||||
|
|ui| match QrCodeWidget::from_data(&url.as_bytes()) {
|
||||||
|
Ok(w) => w.ui(ui),
|
||||||
|
Err(e) => {
|
||||||
|
ui.label(format!("Failed to create QR code display. Error: {e:?}"))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fn save(&self, storage: &mut dyn eframe::Storage) {
|
||||||
|
eframe::set_value(storage, Self::STORAGE_KEY, self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,17 +6,39 @@
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: system-ui, sans-serif;
|
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 {
|
header h2 {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-family: "Gill Sans", sans-serif;
|
font-family: "Gill Sans", sans-serif;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
color: #fff;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
body {
|
background: rgba(0, 0, 0, 0.3);
|
||||||
margin: 0;
|
padding: 0.5rem 1.5rem;
|
||||||
padding: 0;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav.directory {
|
nav.directory {
|
||||||
|
|
@ -31,13 +53,24 @@
|
||||||
|
|
||||||
nav.directory dt a {
|
nav.directory dt a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #333;
|
color: #fff;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
display: inline-block;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<canvas id="fractal-canvas"></canvas>
|
||||||
|
|
||||||
|
<div class="content-overlay">
|
||||||
<header>
|
<header>
|
||||||
<h2>Zachery Aaron Shores-Chmielewski</h2>
|
<h2>Zachery Aaron Shores-Chmielewski</h2>
|
||||||
</header>
|
</header>
|
||||||
|
|
@ -46,10 +79,46 @@
|
||||||
<nav>
|
<nav>
|
||||||
<dl>
|
<dl>
|
||||||
<dt><a href="./who">Who</a></dt>
|
<dt><a href="./who">Who</a></dt>
|
||||||
<!-- <dt><a href="#">Writing</a></dt> -->
|
|
||||||
<dt><a href="./contact">Send me a message</a></dt>
|
<dt><a href="./contact">Send me a message</a></dt>
|
||||||
</dl>
|
</dl>
|
||||||
</nav>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue