use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; 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 { Self { x, y, vx: 0.0, vy: 0.0, size: js_sys::Math::random() * 2.0 + 2.0, life: 1.0, max_life: js_sys::Math::random() * 500.0 + 500.0, hue, } } fn update(&mut self, width: f64, height: f64, time: f64) { // Multiple layered flow fields for complex fluid motion let scale1 = 0.003; let scale2 = 0.006; let scale3 = 0.001; // Layer 1: Large sweeping currents let angle1 = (self.x * scale1 + time * 0.0002).sin() * 3.0 + (self.y * scale1 + time * 0.0001).cos() * 3.0; // Layer 2: Medium turbulence let angle2 = (self.x * scale2 - time * 0.0003).cos() * 2.0 + (self.y * scale2 + time * 0.0002).sin() * 2.0; // Layer 3: Fine detail let angle3 = ((self.x * scale3 + time * 0.0001).sin() + (self.y * scale3 - time * 0.00015).cos()) * 1.5; // Combine flow fields let angle = angle1 + angle2 + angle3; // Convert to force let force = 0.15; let fx = angle.cos() * force; let fy = angle.sin() * force; // Apply force with gentle acceleration self.vx += fx; self.vy += fy; // Smooth damping for fluid motion self.vx *= 0.95; self.vy *= 0.95; // Limit maximum velocity for smooth flow let max_speed = 2.0; let speed = (self.vx * self.vx + self.vy * self.vy).sqrt(); if speed > max_speed { self.vx = (self.vx / speed) * max_speed; self.vy = (self.vy / speed) * max_speed; } // Update position self.x += self.vx; self.y += self.vy; self.life -= 1.0; // Wrap around edges smoothly if self.x < -10.0 { self.x = width + 10.0; } if self.x > width + 10.0 { self.x = -10.0; } if self.y < -10.0 { self.y = height + 10.0; } if self.y > height + 10.0 { self.y = -10.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); // Glow effect let glow_gradient = ctx.create_radial_gradient( self.x, self.y, 0.0, self.x, self.y, self.size * 3.0 ).ok(); if let Some(gradient) = glow_gradient { let alpha_hex = format!("{:02x}", (alpha * 60.0) as u8); gradient.add_color_stop(0.0, &format!("#{}", alpha_hex.repeat(3))).ok(); gradient.add_color_stop(0.5, &format!("rgba(50, 255, 150, {})", alpha * 0.3)).ok(); gradient.add_color_stop(1.0, "rgba(50, 255, 150, 0)").ok(); ctx.set_fill_style(&JsValue::from(gradient)); ctx.begin_path(); let _ = ctx.arc(self.x, self.y, self.size * 3.0, 0.0, std::f64::consts::PI * 2.0); ctx.fill(); } // Core particle let color = format!("rgba(100, 255, 180, {})", alpha * 0.8); ctx.set_fill_style_str(&color); ctx.begin_path(); let _ = ctx.arc(self.x, self.y, self.size, 0.0, std::f64::consts::PI * 2.0); 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: 1200, 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> { console::log_1(&"🚀 railwayka.ru WASM landing page initializing...".into()); let window = web_sys::window().expect("no global window exists"); let document = window.document().expect("should have a document on window"); // 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)?; } console::log_1(&"✅ Landing page ready!".into()); Ok(()) } fn setup_particle_canvas(document: &Document, body: &HtmlElement, window: &Window) -> Result<(), JsValue> { console::log_1(&"🎨 Setting up particle canvas...".into()); // 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); console::log_1(&format!("📐 Canvas size: {}x{}", width, height).into()); canvas.set_width(width as u32); canvas.set_height(height as u32); body.append_child(&canvas)?; console::log_1(&"✅ Canvas appended to body".into()); // 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(600); console::log_1(&format!("🌟 Spawned {} initial particles", particle_system.borrow().particles.len()).into()); // Setup animation loop let system_clone = particle_system.clone(); let context_clone = context.clone(); let frame_count = Rc::new(RefCell::new(0u32)); let window_clone = window.clone(); let animate_closure = Rc::new(RefCell::new(None::>)); let animate_clone = animate_closure.clone(); let frame_clone = frame_count.clone(); *animate_closure.borrow_mut() = Some(Closure::wrap(Box::new(move || { *frame_clone.borrow_mut() += 1; if *frame_clone.borrow() == 1 { console::log_1(&"🎬 Animation loop started!".into()); } if *frame_clone.borrow() % 60 == 0 { let sys = system_clone.borrow(); console::log_1(&format!("🔄 Frame {}, {} particles", *frame_clone.borrow(), sys.particles.len()).into()); if let Some(p) = sys.particles.first() { console::log_1(&format!("🔍 First particle: x={:.1}, y={:.1}, vx={:.2}, vy={:.2}, size={:.1}, life={:.1}", p.x, p.y, p.vx, p.vy, p.size, p.life).into()); } } // Semi-transparent clear for fluid trails context_clone.set_fill_style_str("rgba(10, 14, 23, 0.12)"); 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_clone .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)?; body.append_child(&hero)?; // Services Section let services = create_services_section(document)?; body.append_child(&services)?; // Tech Stack Section let tech = create_tech_section(document)?; body.append_child(&tech)?; // Footer let footer = create_footer(document)?; body.append_child(&footer)?; Ok(()) } fn create_hero(document: &Document) -> Result { let section = document.create_element("section")?; section.set_class_name("hero"); let container = document.create_element("div")?; container.set_class_name("container"); let title = document.create_element("h1")?; title.set_text_content(Some("railwayka.ru")); title.set_class_name("hero-title"); let subtitle = document.create_element("p")?; subtitle.set_text_content(Some("Modern self-hosted infrastructure powered by GitOps")); subtitle.set_class_name("hero-subtitle"); let badge = document.create_element("div")?; badge.set_class_name("hero-badge"); badge.set_text_content(Some("🦀 Powered by Rust + WebAssembly")); container.append_child(&title)?; container.append_child(&subtitle)?; container.append_child(&badge)?; section.append_child(&container)?; Ok(section) } fn create_services_section(document: &Document) -> Result { let section = document.create_element("section")?; section.set_class_name("services"); let container = document.create_element("div")?; container.set_class_name("container"); let heading = document.create_element("h2")?; heading.set_text_content(Some("Services")); heading.set_class_name("section-title"); container.append_child(&heading)?; let grid = document.create_element("div")?; grid.set_class_name("services-grid"); // Live services let services_live = vec![ ("Git Repository", "Gitea - Self-hosted Git server with web UI", "🔴", "https://git.dc.railwayka.ru"), ]; for (i, (name, desc, status, url)) in services_live.iter().enumerate() { let card = create_service_card(document, name, desc, status, Some(url))?; card.set_attribute("data-animate", "fade-up")?; card.set_attribute("data-delay", &format!("{}", i * 100))?; grid.append_child(&card)?; } // Planned services let services_planned = vec![ ("Matrix Server", "Decentralized messaging and communication", "🔵"), ("Blog", "Technical writing and knowledge sharing", "🔵"), ("Password Manager", "Vaultwarden - Self-hosted password vault", "🔵"), ("Cloud Storage", "Nextcloud - File sync and sharing", "🔵"), ("Monitoring", "Grafana + Prometheus - Infrastructure monitoring", "🔵"), ]; for (i, (name, desc, status)) in services_planned.iter().enumerate() { let card = create_service_card(document, name, desc, status, None)?; card.set_attribute("data-animate", "fade-up")?; card.set_attribute("data-delay", &format!("{}", (i + services_live.len()) * 100))?; grid.append_child(&card)?; } container.append_child(&grid)?; section.append_child(&container)?; Ok(section) } fn create_service_card( document: &Document, name: &str, description: &str, status: &str, url: Option<&str>, ) -> Result { let card = if let Some(link) = url { let a = document.create_element("a")?; a.set_attribute("href", link)?; a.set_attribute("target", "_blank")?; a.set_attribute("rel", "noopener noreferrer")?; a } else { document.create_element("div")? }; card.set_class_name("service-card"); let header = document.create_element("div")?; header.set_class_name("service-header"); let title = document.create_element("h3")?; title.set_text_content(Some(name)); let badge = document.create_element("span")?; badge.set_class_name("status-badge"); badge.set_text_content(Some(status)); header.append_child(&title)?; header.append_child(&badge)?; let desc = document.create_element("p")?; desc.set_text_content(Some(description)); desc.set_class_name("service-description"); card.append_child(&header)?; card.append_child(&desc)?; Ok(card) } fn create_tech_section(document: &Document) -> Result { let section = document.create_element("section")?; section.set_class_name("tech"); let container = document.create_element("div")?; container.set_class_name("container"); let heading = document.create_element("h2")?; heading.set_text_content(Some("Technology Stack")); heading.set_class_name("section-title"); container.append_child(&heading)?; let grid = document.create_element("div")?; grid.set_class_name("tech-grid"); let technologies = vec![ ("Dcape", "GitOps platform for containerized applications"), ("Docker", "Containerization and orchestration"), ("Traefik", "Automatic HTTPS and reverse proxy"), ("PowerDNS", "Authoritative DNS server"), ("PostgreSQL", "Shared database infrastructure"), ("Rust + WASM", "High-performance web applications"), ]; for (i, (name, desc)) in technologies.iter().enumerate() { let item = create_tech_item(document, name, desc)?; item.set_attribute("data-animate", "fade-up")?; item.set_attribute("data-delay", &format!("{}", i * 80))?; grid.append_child(&item)?; } container.append_child(&grid)?; section.append_child(&container)?; Ok(section) } fn create_tech_item(document: &Document, name: &str, description: &str) -> Result { let item = document.create_element("div")?; item.set_class_name("tech-item"); let title = document.create_element("h3")?; title.set_text_content(Some(name)); let desc = document.create_element("p")?; desc.set_text_content(Some(description)); item.append_child(&title)?; item.append_child(&desc)?; Ok(item) } fn create_footer(document: &Document) -> Result { let footer = document.create_element("footer")?; footer.set_class_name("footer"); let container = document.create_element("div")?; container.set_class_name("container"); let text = document.create_element("p")?; text.set_inner_html("Built with 🦀 Rust + WebAssembly • Deployed via GitOps"); let links = document.create_element("div")?; links.set_class_name("footer-links"); let link_data = vec![ ("Traefik Dashboard", "https://dc.railwayka.ru/dashboard/"), ("Gitea", "https://git.dc.railwayka.ru"), ("Woodpecker CI", "https://cicd.dc.railwayka.ru"), ]; for (name, url) in link_data { let link = document.create_element("a")?; link.set_attribute("href", url)?; link.set_attribute("target", "_blank")?; link.set_attribute("rel", "noopener noreferrer")?; link.set_text_content(Some(name)); links.append_child(&link)?; } container.append_child(&text)?; container.append_child(&links)?; footer.append_child(&container)?; Ok(footer) } fn setup_scroll_animations(document: &Document) -> Result<(), JsValue> { // Create intersection observer callback let callback = Closure::wrap(Box::new(move |entries: js_sys::Array, _observer: IntersectionObserver| { for entry in entries.iter() { if let Ok(entry) = entry.dyn_into::() { if entry.is_intersecting() { if let Some(target) = entry.target().dyn_ref::() { let delay = target.get_attribute("data-delay") .and_then(|d| d.parse::().ok()) .unwrap_or(0); target.class_list().add_1("animate-in").ok(); target.set_attribute("style", &format!("animation-delay: {}ms", delay)).ok(); } } } } }) as Box); // Create observer with options let options = IntersectionObserverInit::new(); options.set_threshold(&JsValue::from_f64(0.1)); let observer = IntersectionObserver::new_with_options( callback.as_ref().unchecked_ref(), &options, )?; // Observe all elements with data-animate attribute let elements = document.query_selector_all("[data-animate]")?; for i in 0..elements.length() { if let Some(node) = elements.item(i) { if let Some(element) = node.dyn_ref::() { observer.observe(element); } } } // Prevent callback from being dropped callback.forget(); Ok(()) }