use crate::easing::Easing;
use crate::timeline::{stagger, Timeline};
use crate::tween::Tween;
use crate::traits::Animatable;
#[derive(Debug, Clone)]
pub struct SplitChar {
pub ch: char,
pub index: usize,
pub word_index: usize,
}
#[derive(Debug, Clone)]
pub struct SplitWord {
pub text: String,
pub index: usize,
pub char_start: usize,
pub char_end: usize,
}
#[derive(Debug, Clone)]
pub struct SplitText {
chars: Vec<SplitChar>,
words: Vec<SplitWord>,
original: String,
}
impl SplitText {
pub fn from_str(text: &str) -> Self {
let mut chars = Vec::new();
let mut words = Vec::new();
let mut word_index = 0;
let mut char_index = 0;
for word_str in text.split_whitespace() {
let char_start = text.find(word_str).unwrap_or(char_index);
while char_index < char_start {
if let Some(ch) = text.chars().nth(char_index) {
chars.push(SplitChar {
ch,
index: char_index,
word_index: if word_index > 0 { word_index - 1 } else { 0 },
});
}
char_index += 1;
}
let word_char_start = char_index;
for ch in word_str.chars() {
chars.push(SplitChar {
ch,
index: char_index,
word_index,
});
char_index += 1;
}
words.push(SplitWord {
text: word_str.to_string(),
index: word_index,
char_start: word_char_start,
char_end: char_index,
});
word_index += 1;
}
while char_index < text.len() {
if let Some(ch) = text.chars().nth(char_index) {
chars.push(SplitChar {
ch,
index: char_index,
word_index: if word_index > 0 { word_index - 1 } else { 0 },
});
}
char_index += 1;
}
Self {
chars,
words,
original: text.to_string(),
}
}
pub fn chars(&self) -> &[SplitChar] {
&self.chars
}
pub fn words(&self) -> &[SplitWord] {
&self.words
}
pub fn char_count(&self) -> usize {
self.chars.len()
}
pub fn word_count(&self) -> usize {
self.words.len()
}
pub fn original(&self) -> &str {
&self.original
}
pub fn stagger_chars<T: Animatable + Clone>(
&self,
from: T,
to: T,
duration: f32,
delay: f32,
easing: Easing,
) -> Timeline {
let tweens: Vec<_> = (0..self.chars.len())
.map(|_| {
let t = Tween::new(from.clone(), to.clone())
.duration(duration)
.easing(easing.clone())
.build();
(t, duration)
})
.collect();
stagger(tweens, delay)
}
pub fn stagger_words<T: Animatable + Clone>(
&self,
from: T,
to: T,
duration: f32,
delay: f32,
easing: Easing,
) -> Timeline {
let tweens: Vec<_> = (0..self.words.len())
.map(|_| {
let t = Tween::new(from.clone(), to.clone())
.duration(duration)
.easing(easing.clone())
.build();
(t, duration)
})
.collect();
stagger(tweens, delay)
}
#[cfg(feature = "wasm-dom")]
pub fn inject_chars(&self, parent: &web_sys::Element) {
parent.set_inner_html("");
let doc = web_sys::window().unwrap().document().unwrap();
for sc in &self.chars {
let span = doc.create_element("span").unwrap();
let _ = span.set_attribute("style", "display:inline-block");
let _ = span.set_attribute("data-char-index", &sc.index.to_string());
if sc.ch == ' ' {
span.set_inner_html(" ");
} else {
span.set_text_content(Some(&sc.ch.to_string()));
}
let _ = parent.append_child(&span);
}
}
#[cfg(feature = "wasm-dom")]
pub fn inject_words(&self, parent: &web_sys::Element) {
parent.set_inner_html("");
let doc = web_sys::window().unwrap().document().unwrap();
for (i, sw) in self.words.iter().enumerate() {
if i > 0 {
let space = doc.create_text_node(" ");
let _ = parent.append_child(&space);
}
let span = doc.create_element("span").unwrap();
let _ = span.set_attribute("style", "display:inline-block");
let _ = span.set_attribute("data-word-index", &sw.index.to_string());
span.set_text_content(Some(&sw.text));
let _ = parent.append_child(&span);
}
}
#[cfg(feature = "wasm-dom")]
pub fn detect_lines(container: &web_sys::Element) -> Vec<Vec<usize>> {
let spans = container.children();
let mut lines: Vec<Vec<usize>> = Vec::new();
let mut current_top: Option<f32> = None;
for i in 0..spans.length() {
if let Some(span) = spans.item(i) {
let rect = span.get_bounding_client_rect();
let top = rect.top() as f32;
match current_top {
Some(ct) if (top - ct).abs() < 2.0 => {
if let Some(line) = lines.last_mut() {
line.push(i as usize);
}
}
_ => {
current_top = Some(top);
lines.push(vec![i as usize]);
}
}
}
}
lines
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::Update;
#[test]
fn split_basic() {
let split = SplitText::from_str("Hello world");
assert_eq!(split.word_count(), 2);
assert_eq!(split.words()[0].text, "Hello");
assert_eq!(split.words()[1].text, "world");
}
#[test]
fn split_chars_count() {
let split = SplitText::from_str("Hi");
assert_eq!(split.char_count(), 2);
assert_eq!(split.chars()[0].ch, 'H');
assert_eq!(split.chars()[1].ch, 'i');
}
#[test]
fn split_empty_string() {
let split = SplitText::from_str("");
assert_eq!(split.char_count(), 0);
assert_eq!(split.word_count(), 0);
}
#[test]
fn split_single_word() {
let split = SplitText::from_str("Rust");
assert_eq!(split.word_count(), 1);
assert_eq!(split.chars()[0].word_index, 0);
}
#[test]
fn stagger_chars_creates_timeline() {
let split = SplitText::from_str("ABC");
let mut timeline = split.stagger_chars(0.0_f32, 1.0, 0.5, 0.1, Easing::Linear);
timeline.play();
timeline.update(0.1);
}
#[test]
fn stagger_words_creates_timeline() {
let split = SplitText::from_str("One Two Three");
let mut timeline = split.stagger_words(0.0_f32, 1.0, 0.5, 0.2, Easing::Linear);
timeline.play();
timeline.update(0.3);
}
}