Skip to main content

lepticons_animate/
draw_icon.rs

1use leptos::prelude::*;
2use leptos::text_prop::TextProp;
3use lepticons::{Glyph, LucideGlyph, DEFAULT_SIZE, DEFAULT_FILL, DEFAULT_STROKE, DEFAULT_STROKE_WIDTH};
4use wasm_bindgen::JsCast;
5
6use crate::Easing;
7
8/// Renders an icon with a stroke draw-in animation.
9///
10/// The icon's paths animate from invisible to fully drawn over `duration_ms`.
11///
12/// # Example
13///
14/// ```rust,ignore
15/// use lepticons_animate::{DrawIcon, Easing};
16/// use lepticons::LucideGlyph;
17///
18/// // Default easing (ease-in-out, 600ms)
19/// <DrawIcon glyph=LucideGlyph::Check />
20///
21/// // Custom duration and easing
22/// <DrawIcon glyph=LucideGlyph::Heart duration_ms=800 easing=Easing::EaseOut />
23/// ```
24#[component]
25pub fn DrawIcon(
26    /// The icon to render.
27    #[prop(into)]
28    glyph: Signal<LucideGlyph>,
29    /// Animation duration in milliseconds (default: 600).
30    #[prop(default = 600)]
31    duration_ms: u32,
32    /// Delay before animation starts in milliseconds (default: 0).
33    #[prop(default = 0)]
34    delay_ms: u32,
35    /// Transition timing function (default: `Easing::EaseInOut`).
36    #[prop(default = Easing::EaseInOut)]
37    easing: Easing,
38    /// CSS class for the outer wrapper.
39    #[prop(into, optional)]
40    class: Option<TextProp>,
41    /// Width and height in pixels (default: "24").
42    #[prop(into, optional)]
43    size: Option<TextProp>,
44    /// SVG fill color (default: "none").
45    #[prop(into, optional)]
46    fill: Option<TextProp>,
47    /// SVG stroke color (default: "currentColor").
48    #[prop(into, optional)]
49    stroke: Option<TextProp>,
50    /// SVG stroke width (default: "1.5").
51    #[prop(into, optional)]
52    stroke_width: Option<TextProp>,
53) -> impl IntoView {
54    let size = size.unwrap_or_else(|| DEFAULT_SIZE.into());
55    let size2 = size.clone();
56    let fill = fill.unwrap_or_else(|| DEFAULT_FILL.into());
57    let stroke = stroke.unwrap_or_else(|| DEFAULT_STROKE.into());
58    let stroke_width = stroke_width.unwrap_or_else(|| DEFAULT_STROKE_WIDTH.into());
59    let easing_css = easing.as_css();
60
61    let wrapper_ref = NodeRef::<leptos::html::Div>::new();
62
63    // On mount/glyph change, find all geometry elements and animate stroke-dashoffset.
64    // Deferred to next frame so inner_html has populated the SVG children.
65    Effect::new(move |_| {
66        glyph.get();
67
68        let Some(wrapper) = wrapper_ref.get() else { return };
69        let wrapper_el: web_sys::HtmlElement = (*wrapper).clone();
70
71        // First frame: wait for inner_html to populate SVG children
72        request_animation_frame(move || {
73            let wrapper_as_el: &web_sys::Element = wrapper_el.as_ref();
74            let Some(svg) = wrapper_as_el.first_element_child() else { return };
75            let children = svg.children();
76            let count = children.length();
77
78            for i in 0..count {
79                let Some(child) = children.item(i) else { continue };
80
81                let Ok(geom) = child.clone().dyn_into::<web_sys::SvgGeometryElement>() else {
82                    continue;
83                };
84                let length: f32 = geom.get_total_length();
85                if length <= 0.0 {
86                    continue;
87                }
88
89                let svg_child: web_sys::SvgElement = child.unchecked_into();
90                let s = svg_child.style();
91
92                let len_str = length.to_string();
93                // Set initial state: fully hidden
94                let _ = s.set_property("stroke-dasharray", &len_str);
95                let _ = s.set_property("stroke-dashoffset", &len_str);
96                let _ = s.set_property("transition", "none");
97
98                // Second frame: enable transition and animate to visible
99                let s_clone = s.clone();
100                request_animation_frame(move || {
101                    let _ = s_clone.set_property(
102                        "transition",
103                        &format!(
104                            "stroke-dashoffset {}ms {} {}ms",
105                            duration_ms, easing_css, delay_ms
106                        ),
107                    );
108                    let _ = s_clone.set_property("stroke-dashoffset", "0");
109                });
110            }
111        });
112    });
113
114    view! {
115        <div node_ref=wrapper_ref
116             class=move || class.as_ref().map(|c| c.get().to_string()).unwrap_or_default()
117             style="display:inline-block;line-height:0">
118            <svg
119                xmlns="http://www.w3.org/2000/svg"
120                width=move || size.get()
121                height=move || size2.get()
122                viewBox="0 0 24 24"
123                fill=move || fill.get()
124                stroke=move || stroke.get()
125                stroke-width=move || stroke_width.get()
126                stroke-linecap="round"
127                stroke-linejoin="round"
128                inner_html=move || glyph.get().svg()
129            />
130        </div>
131    }
132}