1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// animation/src/glow.rs
// Component-isolated glow effect built on tairitsu hooks.
use tairitsu_macros::{component, define_props, rsx};
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use tairitsu_vdom::MouseEvent;
use tairitsu_vdom::VNode as Element;
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use wasm_bindgen::JsValue;
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use web_sys::HtmlElement;
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use crate::style::StyleBuilder;
/// Glow component properties
#[define_props]
pub struct GlowProps {
#[default("rgba(255, 255, 255, 0.3)".to_string())]
pub color: String,
#[default(0.3)]
pub intensity: f64,
pub children: Element,
}
/// Glow component with mouse-following effect
///
/// This component provides a glow effect that follows the mouse cursor
/// within the component boundaries. It is fully component-isolated,
/// using tairitsu hooks for state management and lifecycle handling.
///
/// # Example
///
/// ```ignore
/// use tairitsu_macros::rsx;
/// use hikari_animation::Glow;
///
/// rsx! {
/// Glow {
/// color: "rgba(255, 255, 255, 0.3)",
/// intensity: 0.5,
/// Card {
/// "Card with glow effect"
/// }
/// }
/// }
/// ```
#[component]
pub fn Glow(props: GlowProps) -> Element {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
{
use wasm_bindgen::JsCast;
// Clone props for use in event handlers
let color = props.color.clone();
let intensity = props.intensity.to_string();
// Clone for use in rsx! block (color and intensity are moved into onmousemove_handler)
let rsx_color = color.clone();
let rsx_intensity = intensity.clone();
// Mouse move handler - directly update CSS variables without re-render.
// In tairitsu the handler receives the event data directly (no `Event<...>`
// wrapper and no DOM Event object), so we resolve the target element via
// `document.element_from_point` from the reported client coordinates.
let onmousemove_handler = move |event: MouseEvent| {
let client_x = event.client_x as f64;
let client_y = event.client_y as f64;
let Some(document) = (|| web_sys::window()?.document())() else {
return;
};
let Some(target) = document.element_from_point(client_x as f32, client_y as f32) else {
return;
};
let mut current = target;
// Traverse up to find the .hi-glow-wrapper element.
loop {
let class_list = current.class_list();
if class_list.contains("hi-glow-wrapper") {
break;
}
// Move to parent
match current.parent_element() {
Some(parent) => current = parent,
None => return,
}
}
if let Some(html_element) = current.dyn_into::<HtmlElement>().ok() {
// Get element bounding box
let rect = html_element.get_bounding_client_rect();
// Calculate relative position as percentage
let relative_x = client_x - rect.left();
let relative_y = client_y - rect.top();
let width = rect.width();
let height = rect.height();
// Debug logging
let _ = web_sys::console::log_1(&format!(
"Glow: client=({:.0},{:.0}) rect=({:.0},{:.0},{:.0},{:.0}) size=({:.0},{:.0})",
client_x, client_y, rect.left(), rect.top(), rect.right(), rect.bottom(), width, height
).into());
// Handle zero or negative dimensions
if width <= 0.0 || height <= 0.0 {
let _ = web_sys::console::log_1(&JsValue::from(
"Glow: Invalid dimensions, using 50%",
));
StyleBuilder::new(&html_element)
.add_custom("--glow-x", "50%")
.add_custom("--glow-y", "50%")
.add_custom("--glow-color", &color)
.add_custom("--glow-intensity", &intensity.to_string())
.apply();
return;
}
let percent_x = ((relative_x / width) * 100.0).clamp(0.0, 100.0);
let percent_y = ((relative_y / height) * 100.0).clamp(0.0, 100.0);
let _ = web_sys::console::log_1(
&format!("Glow: percent=({:.1}%,{:.1}%)", percent_x, percent_y).into(),
);
// Update CSS variables directly on DOM (no re-render)
let _ = web_sys::console::log_1(
&format!(
"Glow: --glow-x: {:.1}%; --glow-y: {:.1}%; --glow-color: {}; --glow-intensity: {};",
percent_x, percent_y, color, intensity
)
.into(),
);
StyleBuilder::new(&html_element)
.add_custom("--glow-x", &format!("{:.1}%", percent_x))
.add_custom("--glow-y", &format!("{:.1}%", percent_y))
.add_custom("--glow-color", &color)
.add_custom("--glow-intensity", &intensity.to_string())
.apply();
}
};
rsx! {
div {
class: "hi-glow-wrapper",
// Initial style
style: "--glow-x: 50%; --glow-y: 50%; --glow-color: {rsx_color}; --glow-intensity: {rsx_intensity};",
onmousemove: onmousemove_handler,
{ props.children }
}
}
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
{
// SSR fallback: just render children
rsx! {
div { class: "hi-glow-wrapper", { props.children } }
}
}
}