diff --git a/Cargo.toml b/Cargo.toml index 4075c89..5941ba7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,9 @@ features = [ "DomRect", "NodeList", "DomTokenList", + "HtmlCanvasElement", + "CanvasRenderingContext2d", + "Performance", ] [profile.release] diff --git a/dist/railwayka_landing.d.ts b/dist/railwayka_landing.d.ts index 7accea5..f9fa8af 100644 --- a/dist/railwayka_landing.d.ts +++ b/dist/railwayka_landing.d.ts @@ -12,6 +12,7 @@ export interface InitOutput { readonly __wbindgen_export_2: (a: number, b: number, c: number, d: number) => number; readonly __wbindgen_export_3: WebAssembly.Table; readonly __wbindgen_export_4: (a: number, b: number, c: number, d: number) => void; + readonly __wbindgen_export_5: (a: number, b: number) => void; readonly __wbindgen_start: () => void; } diff --git a/dist/railwayka_landing.js b/dist/railwayka_landing.js index 12d6721..2b765ae 100644 --- a/dist/railwayka_landing.js +++ b/dist/railwayka_landing.js @@ -122,6 +122,71 @@ function getDataViewMemory0() { return cachedDataViewMemory0; } +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches && builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + function dropObject(idx) { if (idx < 132) return; heap[idx] = heap_next; @@ -176,6 +241,10 @@ function __wbg_adapter_4(arg0, arg1, arg2, arg3) { wasm.__wbindgen_export_4(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); } +function __wbg_adapter_9(arg0, arg1) { + wasm.__wbindgen_export_5(arg0, arg1); +} + const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']); async function __wbg_load(module, imports) { @@ -214,6 +283,9 @@ async function __wbg_load(module, imports) { function __wbg_get_imports() { const imports = {}; imports.wbg = {}; + imports.wbg.__wbg_addEventListener_775911544ac9d643 = function() { return handleError(function (arg0, arg1, arg2, arg3) { + getObject(arg0).addEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3)); + }, arguments) }; imports.wbg.__wbg_add_4e0283c00f7ecabe = function() { return handleError(function (arg0, arg1, arg2) { getObject(arg0).add(getStringFromWasm0(arg1, arg2)); }, arguments) }; @@ -221,6 +293,12 @@ function __wbg_get_imports() { const ret = getObject(arg0).appendChild(getObject(arg1)); return addHeapObject(ret); }, arguments) }; + imports.wbg.__wbg_arc_61cbec33cc96a55e = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5) { + getObject(arg0).arc(arg1, arg2, arg3, arg4, arg5); + }, arguments) }; + imports.wbg.__wbg_beginPath_119487ebd04e9e1c = function(arg0) { + getObject(arg0).beginPath(); + }; imports.wbg.__wbg_body_8822ca55cb3730d2 = function(arg0) { const ret = getObject(arg0).body; return isLikeNone(ret) ? 0 : addHeapObject(ret); @@ -241,6 +319,12 @@ function __wbg_get_imports() { const ret = getObject(arg0).document; return isLikeNone(ret) ? 0 : addHeapObject(ret); }; + imports.wbg.__wbg_fillRect_a160edfa11fce49b = function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).fillRect(arg1, arg2, arg3, arg4); + }; + imports.wbg.__wbg_fill_7b331ac62ac7c50b = function(arg0) { + getObject(arg0).fill(); + }; imports.wbg.__wbg_getAttribute_8bfaf67e99ed2ee3 = function(arg0, arg1, arg2, arg3) { const ret = getObject(arg1).getAttribute(getStringFromWasm0(arg2, arg3)); var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export_1, wasm.__wbindgen_export_2); @@ -248,10 +332,32 @@ function __wbg_get_imports() { getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); }; + imports.wbg.__wbg_getContext_15e158d04230a6f6 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).getContext(getStringFromWasm0(arg1, arg2)); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, arguments) }; imports.wbg.__wbg_get_0da715ceaecea5c8 = function(arg0, arg1) { const ret = getObject(arg0)[arg1 >>> 0]; return addHeapObject(ret); }; + imports.wbg.__wbg_innerHeight_eacbddff807274db = function() { return handleError(function (arg0) { + const ret = getObject(arg0).innerHeight; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_innerWidth_dd42bfe6b5e91e59 = function() { return handleError(function (arg0) { + const ret = getObject(arg0).innerWidth; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_instanceof_CanvasRenderingContext2d_8c616198ec03b12f = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof CanvasRenderingContext2D; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; imports.wbg.__wbg_instanceof_Element_162e4334c7d6f450 = function(arg0) { let result; try { @@ -262,6 +368,16 @@ function __wbg_get_imports() { const ret = result; return ret; }; + imports.wbg.__wbg_instanceof_HtmlCanvasElement_299c60950dbb3428 = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof HTMLCanvasElement; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; imports.wbg.__wbg_instanceof_IntersectionObserverEntry_819e56422a481344 = function(arg0) { let result; try { @@ -320,12 +436,29 @@ function __wbg_get_imports() { const ret = getObject(arg0).querySelectorAll(getStringFromWasm0(arg1, arg2)); return addHeapObject(ret); }, arguments) }; + imports.wbg.__wbg_random_7ed63a0b38ee3b75 = function() { + const ret = Math.random(); + return ret; + }; + imports.wbg.__wbg_requestAnimationFrame_ddc84a7def436784 = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).requestAnimationFrame(getObject(arg1)); + return ret; + }, arguments) }; imports.wbg.__wbg_setAttribute_d1baf9023ad5696f = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { getObject(arg0).setAttribute(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); }, arguments) }; imports.wbg.__wbg_setclassName_c8bccad917b973f4 = function(arg0, arg1, arg2) { getObject(arg0).className = getStringFromWasm0(arg1, arg2); }; + imports.wbg.__wbg_setfillStyle_a9ad5b25cf62a5bc = function(arg0, arg1, arg2) { + getObject(arg0).fillStyle = getStringFromWasm0(arg1, arg2); + }; + imports.wbg.__wbg_setheight_4fce583024b2d088 = function(arg0, arg1) { + getObject(arg0).height = arg1 >>> 0; + }; + imports.wbg.__wbg_setid_891db4c7fb5255d1 = function(arg0, arg1, arg2) { + getObject(arg0).id = getStringFromWasm0(arg1, arg2); + }; imports.wbg.__wbg_setinnerHTML_34e240d6b8e8260c = function(arg0, arg1, arg2) { getObject(arg0).innerHTML = getStringFromWasm0(arg1, arg2); }; @@ -335,6 +468,9 @@ function __wbg_get_imports() { imports.wbg.__wbg_setthreshold_7daca4126268ea47 = function(arg0, arg1) { getObject(arg0).threshold = getObject(arg1); }; + imports.wbg.__wbg_setwidth_40a6ed203b92839d = function(arg0, arg1) { + getObject(arg0).width = arg1 >>> 0; + }; imports.wbg.__wbg_static_accessor_GLOBAL_8921f820c2ce3f12 = function() { const ret = typeof global === 'undefined' ? null : global; return isLikeNone(ret) ? 0 : addHeapObject(ret); @@ -364,10 +500,23 @@ function __wbg_get_imports() { const ret = false; return ret; }; + imports.wbg.__wbg_wbindgendebugstring_99ef257a3ddda34d = function(arg0, arg1) { + const ret = debugString(getObject(arg1)); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export_1, wasm.__wbindgen_export_2); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; imports.wbg.__wbg_wbindgenisundefined_c4b71d073b92f3c5 = function(arg0) { const ret = getObject(arg0) === undefined; return ret; }; + imports.wbg.__wbg_wbindgennumberget_f74b4c7525ac05cb = function(arg0, arg1) { + const obj = getObject(arg1); + const ret = typeof(obj) === 'number' ? obj : undefined; + getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); + }; imports.wbg.__wbg_wbindgenrethrow_01815c9239d70cc2 = function(arg0) { throw takeObject(arg0); }; @@ -379,13 +528,18 @@ function __wbg_get_imports() { const ret = getStringFromWasm0(arg0, arg1); return addHeapObject(ret); }; + imports.wbg.__wbindgen_cast_aaa93aae03c115ab = function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { dtor_idx: 1, function: Function { arguments: [], shim_idx: 2, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, 1, __wbg_adapter_9); + return addHeapObject(ret); + }; imports.wbg.__wbindgen_cast_d6cd19b81560fd6e = function(arg0) { // Cast intrinsic for `F64 -> Externref`. const ret = arg0; return addHeapObject(ret); }; - imports.wbg.__wbindgen_cast_fe9164a03cdb0b6b = function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { dtor_idx: 1, function: Function { arguments: [NamedExternref("Array"), NamedExternref("IntersectionObserver")], shim_idx: 2, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + imports.wbg.__wbindgen_cast_e58c2e3d55f4d158 = function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { dtor_idx: 1, function: Function { arguments: [NamedExternref("Array"), NamedExternref("IntersectionObserver")], shim_idx: 4, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, 1, __wbg_adapter_4); return addHeapObject(ret); }; diff --git a/dist/railwayka_landing_bg.wasm b/dist/railwayka_landing_bg.wasm index 238df8f..9c7bc77 100644 Binary files a/dist/railwayka_landing_bg.wasm and b/dist/railwayka_landing_bg.wasm differ diff --git a/dist/railwayka_landing_bg.wasm.d.ts b/dist/railwayka_landing_bg.wasm.d.ts index 396db70..49d0bbf 100644 --- a/dist/railwayka_landing_bg.wasm.d.ts +++ b/dist/railwayka_landing_bg.wasm.d.ts @@ -7,4 +7,5 @@ export const __wbindgen_export_1: (a: number, b: number) => number; export const __wbindgen_export_2: (a: number, b: number, c: number, d: number) => number; export const __wbindgen_export_3: WebAssembly.Table; export const __wbindgen_export_4: (a: number, b: number, c: number, d: number) => void; +export const __wbindgen_export_5: (a: number, b: number) => void; export const __wbindgen_start: () => void; diff --git a/dist/style.css b/dist/style.css index 3ac623f..6d8b6a7 100644 --- a/dist/style.css +++ b/dist/style.css @@ -27,6 +27,17 @@ body { min-height: 100vh; } +/* Particle canvas background */ +#particle-canvas { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 0; + pointer-events: none; +} + /* Loading screen */ #loading { position: fixed; @@ -72,9 +83,9 @@ body { align-items: center; justify-content: center; text-align: center; - background: linear-gradient(135deg, #0a0e17 0%, #1a1f2e 100%); position: relative; overflow: hidden; + z-index: 1; } .hero::before { @@ -130,6 +141,8 @@ body { /* Section Styles */ section { padding: 5rem 0; + position: relative; + z-index: 1; } .section-title { diff --git a/src/lib.rs b/src/lib.rs index fceac10..c16c349 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,154 @@ use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; -use web_sys::{console, Document, Element, HtmlElement, Window, IntersectionObserver, IntersectionObserverEntry, IntersectionObserverInit}; +use web_sys::{ + console, Document, Element, HtmlElement, Window, + IntersectionObserver, IntersectionObserverEntry, IntersectionObserverInit, + HtmlCanvasElement, CanvasRenderingContext2d, +}; +use std::cell::RefCell; +use std::rc::Rc; + +// Particle structure +struct Particle { + x: f64, + y: f64, + vx: f64, + vy: f64, + size: f64, + life: f64, + max_life: f64, + hue: f64, +} + +impl Particle { + fn new(x: f64, y: f64, hue: f64) -> Self { + let angle = js_sys::Math::random() * std::f64::consts::PI * 2.0; + let speed = js_sys::Math::random() * 0.5 + 0.2; + Self { + x, + y, + vx: angle.cos() * speed, + vy: angle.sin() * speed, + size: js_sys::Math::random() * 2.0 + 1.0, + life: 1.0, + max_life: js_sys::Math::random() * 100.0 + 100.0, + hue, + } + } + + fn update(&mut self, width: f64, height: f64, time: f64) { + // Flow field influence using sine waves for fluid motion + let flow_x = ((self.y * 0.01 + time * 0.0005).sin() * 0.5).sin(); + let flow_y = ((self.x * 0.01 + time * 0.0003).cos() * 0.5).cos(); + + self.vx += flow_x * 0.1; + self.vy += flow_y * 0.1; + + // Damping for smooth motion + self.vx *= 0.98; + self.vy *= 0.98; + + self.x += self.vx; + self.y += self.vy; + + self.life -= 1.0; + + // Wrap around edges with smooth transition + if self.x < 0.0 { + self.x = width; + } + if self.x > width { + self.x = 0.0; + } + if self.y < 0.0 { + self.y = height; + } + if self.y > height { + self.y = 0.0; + } + } + + fn is_dead(&self) -> bool { + self.life <= 0.0 + } + + fn draw(&self, ctx: &CanvasRenderingContext2d) { + let alpha = (self.life / self.max_life).min(1.0); + let color = format!("hsla({}, 70%, 50%, {})", self.hue, alpha * 0.8); + + ctx.set_fill_style_str(&color); + ctx.begin_path(); + ctx.arc(self.x, self.y, self.size, 0.0, std::f64::consts::PI * 2.0).ok(); + ctx.fill(); + + // Add glow effect + let glow_color = format!("hsla({}, 70%, 60%, {})", self.hue, alpha * 0.3); + ctx.set_fill_style_str(&glow_color); + ctx.begin_path(); + ctx.arc(self.x, self.y, self.size * 2.0, 0.0, std::f64::consts::PI * 2.0).ok(); + ctx.fill(); + } +} + +// Particle system +struct ParticleSystem { + particles: Vec, + width: f64, + height: f64, + max_particles: usize, + time: f64, +} + +impl ParticleSystem { + fn new(width: f64, height: f64) -> Self { + Self { + particles: Vec::new(), + width, + height, + max_particles: 800, + time: 0.0, + } + } + + fn spawn_particles(&mut self, count: usize) { + for _ in 0..count { + if self.particles.len() < self.max_particles { + let x = js_sys::Math::random() * self.width; + let y = js_sys::Math::random() * self.height; + // Green hues: 120-160 (emerald to lime) + let hue = js_sys::Math::random() * 40.0 + 120.0; + self.particles.push(Particle::new(x, y, hue)); + } + } + } + + fn update(&mut self) { + self.time += 1.0; + + // Update existing particles + for particle in &mut self.particles { + particle.update(self.width, self.height, self.time); + } + + // Remove dead particles + self.particles.retain(|p| !p.is_dead()); + + // Spawn new particles + let spawn_count = (self.max_particles - self.particles.len()).min(5); + self.spawn_particles(spawn_count); + } + + fn draw(&self, ctx: &CanvasRenderingContext2d) { + for particle in &self.particles { + particle.draw(ctx); + } + } + + fn resize(&mut self, width: f64, height: f64) { + self.width = width; + self.height = height; + } +} #[wasm_bindgen(start)] pub fn main() -> Result<(), JsValue> { @@ -12,6 +160,10 @@ pub fn main() -> Result<(), JsValue> { // Clear body and build page if let Some(body) = document.body() { body.set_inner_html(""); + + // Create and setup particle canvas + setup_particle_canvas(&document, &body, &window)?; + build_page(&document, &body)?; setup_scroll_animations(&document)?; } @@ -20,6 +172,85 @@ pub fn main() -> Result<(), JsValue> { Ok(()) } +fn setup_particle_canvas(document: &Document, body: &HtmlElement, window: &Window) -> Result<(), JsValue> { + // Create canvas element + let canvas = document + .create_element("canvas")? + .dyn_into::()?; + canvas.set_id("particle-canvas"); + + // Get window dimensions + let width = window.inner_width()?.as_f64().unwrap_or(800.0); + let height = window.inner_height()?.as_f64().unwrap_or(600.0); + + canvas.set_width(width as u32); + canvas.set_height(height as u32); + + body.append_child(&canvas)?; + + // Get 2D context + let context = canvas + .get_context("2d")? + .unwrap() + .dyn_into::()?; + + // Initialize particle system + let particle_system = Rc::new(RefCell::new(ParticleSystem::new(width, height))); + + // Spawn initial particles + particle_system.borrow_mut().spawn_particles(400); + + // Setup animation loop + let system_clone = particle_system.clone(); + let context_clone = context.clone(); + + let animate_closure = Rc::new(RefCell::new(None::>)); + let animate_clone = animate_closure.clone(); + + *animate_closure.borrow_mut() = Some(Closure::wrap(Box::new(move || { + // Clear canvas with trail effect (don't fully clear for fluid trails) + context_clone.set_fill_style_str("rgba(10, 14, 23, 0.08)"); + context_clone.fill_rect(0.0, 0.0, width, height); + + // Update and draw particles + system_clone.borrow_mut().update(); + system_clone.borrow().draw(&context_clone); + + // Request next frame + if let Some(closure) = animate_clone.borrow().as_ref() { + window + .request_animation_frame(closure.as_ref().unchecked_ref()) + .ok(); + } + }) as Box)); + + // Start animation + if let Some(closure) = animate_closure.borrow().as_ref() { + window.request_animation_frame(closure.as_ref().unchecked_ref())?; + } + + // Setup resize handler + let canvas_clone = canvas.clone(); + let system_resize = particle_system.clone(); + let window_clone = window.clone(); + + let resize_closure = Closure::wrap(Box::new(move || { + let new_width = window_clone.inner_width().unwrap().as_f64().unwrap_or(800.0); + let new_height = window_clone.inner_height().unwrap().as_f64().unwrap_or(600.0); + + canvas_clone.set_width(new_width as u32); + canvas_clone.set_height(new_height as u32); + + system_resize.borrow_mut().resize(new_width, new_height); + }) as Box); + + window.add_event_listener_with_callback("resize", resize_closure.as_ref().unchecked_ref())?; + resize_closure.forget(); + + console::log_1(&"🌊 Particle system initialized!".into()); + Ok(()) +} + fn build_page(document: &Document, body: &HtmlElement) -> Result<(), JsValue> { // Hero Section let hero = create_hero(document)?; @@ -265,7 +496,7 @@ fn setup_scroll_animations(document: &Document) -> Result<(), JsValue> { }) as Box); // Create observer with options - let mut options = IntersectionObserverInit::new(); + let options = IntersectionObserverInit::new(); options.set_threshold(&JsValue::from_f64(0.1)); let observer = IntersectionObserver::new_with_options( diff --git a/style.css b/style.css index 3ac623f..6d8b6a7 100644 --- a/style.css +++ b/style.css @@ -27,6 +27,17 @@ body { min-height: 100vh; } +/* Particle canvas background */ +#particle-canvas { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 0; + pointer-events: none; +} + /* Loading screen */ #loading { position: fixed; @@ -72,9 +83,9 @@ body { align-items: center; justify-content: center; text-align: center; - background: linear-gradient(135deg, #0a0e17 0%, #1a1f2e 100%); position: relative; overflow: hidden; + z-index: 1; } .hero::before { @@ -130,6 +141,8 @@ body { /* Section Styles */ section { padding: 5rem 0; + position: relative; + z-index: 1; } .section-title {