railwayka-landing/src/lib.rs

590 lines
19 KiB
Rust

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<Particle>,
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::<HtmlCanvasElement>()?;
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::<CanvasRenderingContext2d>()?;
// 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::<Closure<dyn FnMut()>>));
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<dyn FnMut()>));
// 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<dyn FnMut()>);
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<Element, JsValue> {
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<Element, JsValue> {
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<Element, JsValue> {
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<Element, JsValue> {
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<Element, JsValue> {
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<Element, JsValue> {
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 <span class=\"rust-love\">🦀 Rust</span> + 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::<IntersectionObserverEntry>() {
if entry.is_intersecting() {
if let Some(target) = entry.target().dyn_ref::<Element>() {
let delay = target.get_attribute("data-delay")
.and_then(|d| d.parse::<u32>().ok())
.unwrap_or(0);
target.class_list().add_1("animate-in").ok();
target.set_attribute("style", &format!("animation-delay: {}ms", delay)).ok();
}
}
}
}
}) as Box<dyn FnMut(js_sys::Array, IntersectionObserver)>);
// 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::<Element>() {
observer.observe(element);
}
}
}
// Prevent callback from being dropped
callback.forget();
Ok(())
}