circles/src/app/src/components/network_animation_view.rs
timurgordon 0fdc6518c0 cleanup
2025-06-19 11:34:22 +03:00

275 lines
9.2 KiB
Rust

use common_models::CircleData;
use gloo_timers::callback::{Interval, Timeout};
use rand::seq::SliceRandom;
use rand::Rng;
use std::collections::HashMap;
use std::rc::Rc;
use yew::prelude::*;
#[derive(Clone, Debug, PartialEq)]
struct ServerNode {
x: f32,
y: f32,
name: String,
id: u32,
is_active: bool,
}
#[derive(Clone, Debug, PartialEq)]
struct DataTransmission {
id: usize,
from_node: u32,
to_node: u32,
progress: f32,
transmission_type: TransmissionType,
}
#[derive(Clone, Debug, PartialEq)]
enum TransmissionType {
Data,
Sync,
Heartbeat,
}
#[derive(Properties, Clone, PartialEq)]
pub struct NetworkAnimationViewProps {
pub all_circles: Rc<HashMap<u32, CircleData>>,
}
pub enum Msg {
StartTransmission,
UpdateTransmissions,
RemoveTransmission(usize),
PulseNode(u32),
}
pub struct NetworkAnimationView {
server_nodes: Rc<HashMap<u32, ServerNode>>,
active_transmissions: Vec<DataTransmission>,
next_transmission_id: usize,
_transmission_interval: Option<Interval>,
_update_interval: Option<Interval>,
}
impl NetworkAnimationView {
fn calculate_server_positions(
all_circles: &Rc<HashMap<u32, CircleData>>,
) -> Rc<HashMap<u32, ServerNode>> {
let mut nodes = HashMap::new();
// Predefined realistic server locations on the world map (coordinates scaled to viewBox 783.086 x 400.649)
let server_positions = vec![
(180.0, 150.0, "North America"), // USA/Canada
(420.0, 130.0, "Europe"), // Central Europe
(580.0, 160.0, "Asia"), // East Asia
(220.0, 280.0, "South America"), // Brazil/Argentina
(450.0, 220.0, "Africa"), // Central Africa
(650.0, 320.0, "Oceania"), // Australia
(400.0, 90.0, "Nordic"), // Scandinavia
(520.0, 200.0, "Middle East"), // Middle East
];
for (i, (id, circle_data)) in all_circles.iter().enumerate() {
if let Some((x, y, _region)) = server_positions.get(i % server_positions.len()) {
nodes.insert(
*id,
ServerNode {
x: *x,
y: *y,
name: format!("{}", circle_data.name),
id: *id,
is_active: true,
},
);
}
}
Rc::new(nodes)
}
fn create_transmission(
&mut self,
from_id: u32,
to_id: u32,
transmission_type: TransmissionType,
) -> usize {
let id = self.next_transmission_id;
self.next_transmission_id += 1;
self.active_transmissions.push(DataTransmission {
id,
from_node: from_id,
to_node: to_id,
progress: 0.0,
transmission_type,
});
id
}
}
impl Component for NetworkAnimationView {
type Message = Msg;
type Properties = NetworkAnimationViewProps;
fn create(ctx: &Context<Self>) -> Self {
let server_nodes = Self::calculate_server_positions(&ctx.props().all_circles);
let link = ctx.link().clone();
let transmission_interval = Interval::new(3000, move || {
link.send_message(Msg::StartTransmission);
});
let link2 = ctx.link().clone();
let update_interval = Interval::new(50, move || {
link2.send_message(Msg::UpdateTransmissions);
});
Self {
server_nodes,
active_transmissions: Vec::new(),
next_transmission_id: 0,
_transmission_interval: Some(transmission_interval),
_update_interval: Some(update_interval),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::StartTransmission => {
if self.server_nodes.len() < 2 {
return false;
}
let mut rng = rand::thread_rng();
let node_ids: Vec<u32> = self.server_nodes.keys().cloned().collect();
if let (Some(&from_id), Some(&to_id)) =
(node_ids.choose(&mut rng), node_ids.choose(&mut rng))
{
if from_id != to_id {
let transmission_type = match rng.gen_range(0..3) {
0 => TransmissionType::Data,
1 => TransmissionType::Sync,
_ => TransmissionType::Heartbeat,
};
let transmission_id =
self.create_transmission(from_id, to_id, transmission_type);
// Pulse the source node
ctx.link().send_message(Msg::PulseNode(from_id));
// Remove transmission after completion
let link = ctx.link().clone();
let timeout = Timeout::new(2000, move || {
link.send_message(Msg::RemoveTransmission(transmission_id));
});
timeout.forget();
return true;
}
}
false
}
Msg::UpdateTransmissions => {
let mut updated = false;
for transmission in &mut self.active_transmissions {
if transmission.progress < 1.0 {
transmission.progress += 0.02; // 2% per update (50ms * 50 = 2.5s total)
updated = true;
}
}
updated
}
Msg::RemoveTransmission(id) => {
let initial_len = self.active_transmissions.len();
self.active_transmissions.retain(|t| t.id != id);
self.active_transmissions.len() != initial_len
}
Msg::PulseNode(_node_id) => {
// This will trigger a re-render for node pulse animation
true
}
}
}
fn view(&self, _ctx: &Context<Self>) -> Html {
let server_pins = self.server_nodes.iter().map(|(_id, node)| {
html! {
<g class="server-node" transform={format!("translate({}, {})", node.x, node.y)}>
// Subtle glow background
<circle r="6" class="node-glow" />
// Main white pin
<circle r="3" class="node-pin" />
// Ultra-subtle breathing effect
<circle r="4" class="node-pulse" />
// Clean label
<text x="0" y="16" class="node-label" text-anchor="middle">{&node.name}</text>
</g>
}
});
let transmissions = self
.active_transmissions
.iter()
.map(|transmission| {
if let (Some(from_node), Some(to_node)) = (
self.server_nodes.get(&transmission.from_node),
self.server_nodes.get(&transmission.to_node),
) {
html! {
<g class="transmission-group">
// Simple connection line with subtle animation
<line
x1={from_node.x.to_string()}
y1={from_node.y.to_string()}
x2={to_node.x.to_string()}
y2={to_node.y.to_string()}
class="transmission-line"
/>
</g>
}
} else {
html! {}
}
})
.collect::<Html>();
html! {
<div class="network-animation-overlay">
<svg
viewBox="0 0 783.086 400.649"
class="network-overlay-svg"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;"
>
<defs>
// Minimal gradient for node glow
<@{"radialGradient"} id="nodeGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" style="stop-color: var(--primary-color, #007bff); stop-opacity: 0.3" />
<stop offset="100%" style="stop-color: var(--primary-color, #007bff); stop-opacity: 0" />
</@>
</defs>
<g class="server-nodes">
{ for server_pins }
</g>
<g class="transmissions">
{ transmissions }
</g>
</svg>
</div>
}
}
fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
if ctx.props().all_circles != old_props.all_circles {
self.server_nodes = Self::calculate_server_positions(&ctx.props().all_circles);
self.active_transmissions.clear();
return true;
}
false
}
}