use std::hash::{DefaultHasher, Hash, Hasher};
use rand::{Rng, SeedableRng, rngs::StdRng};
use crate::*;
#[derive(Component, Reflect, Clone, PartialEq, Debug)]
enum DurationMode {
CharSpeed(f32),
AnimDuration(f32,)
}
#[derive(Component, Reflect, Clone, Debug)]
#[reflect(from_reflect = false)]
pub struct TextAnimator {
string: String,
#[reflect(ignore)]
function: fn(t: f32, text: &str) -> String,
counter: f32,
mode: DurationMode,
}
impl Default for TextAnimator {
fn default() -> Self {
Self {
string: String::new(),
function: typing_animation_underscore,
counter: 0.0,
mode: DurationMode::AnimDuration(5.0),
}
}
}
impl TextAnimator {
pub fn new(text: impl Into<String>) -> Self {
Self {
string: text.into(),
..Default::default()
}
}
pub fn set_text(&mut self, text: impl Into<String>) {
self.string = text.into();
self.counter = 0.0;
}
pub fn set_text_quiet(&mut self, text: impl Into<String>) {
self.string = text.into();
match self.mode {
DurationMode::AnimDuration(duration) => {
self.counter = duration;
},
DurationMode::CharSpeed(_) => {
self.counter = 1.0 + self.string.trim().chars().count() as f32
}
}
}
pub fn function(mut self, function: fn(t: f32, text: &str) -> String) -> Self {
self.function = function;
self
}
pub fn speed(mut self, speed: f32) -> Self {
self.mode = DurationMode::CharSpeed(speed);
self
}
pub fn duration(mut self, duration: f32) -> Self {
self.mode = DurationMode::AnimDuration(duration);
self
}
pub(crate) fn system_2d(mut query: Query<(&mut Text2d, &mut TextAnimator)>, time: Res<Time>, mut commads: Commands) {
for (mut text, mut animator) in &mut query {
match animator.mode {
DurationMode::CharSpeed(speed) => {
let chars = 1.0 + animator.string.trim().chars().count() as f32;
let mut modified = false;
if animator.counter < chars { animator.counter += time.delta_secs() * speed; modified = true; }
animator.counter = animator.counter.min(chars);
if !modified { continue; }
let new_text = (animator.function)(animator.counter/chars, &animator.string);
if new_text.as_str() != text.as_str() {
text.0 = new_text;
commads.trigger(RecomputeUiLayout);
}
}
DurationMode::AnimDuration(duration) => {
let mut modified = false;
if animator.counter < duration { animator.counter += time.delta_secs(); modified = true; }
animator.counter = animator.counter.min(duration);
if !modified { continue; }
let new_text = (animator.function)(animator.counter/duration, &animator.string);
if new_text.as_str() != text.as_str() {
text.0 = new_text;
commads.trigger(RecomputeUiLayout);
}
},
}
}
}
#[cfg(feature = "text3d")]
pub(crate) fn system_3d(mut query: Query<(&mut Text3d, &mut TextAnimator)>, time: Res<Time>, mut commads: Commands) {
for (mut text, mut animator) in &mut query {
match animator.mode {
DurationMode::CharSpeed(speed) => {
let chars = 1.0 + animator.string.trim().chars().count() as f32;
let mut modified = false;
if animator.counter < chars { animator.counter += time.delta_secs() * speed; modified = true; }
animator.counter = animator.counter.min(chars);
if !modified { continue; }
let new_text = (animator.function)(animator.counter/chars, &animator.string);
if new_text != text.get_single().expect("Multisegment 3D text not supported, make a PR to Lunex if you need it") {
let text = text.get_single_mut().expect("Multisegment 3D text not supported, make a PR to Lunex if you need it");
*text = new_text;
commads.trigger(RecomputeUiLayout);
}
}
DurationMode::AnimDuration(duration) => {
let mut modified = false;
if animator.counter < duration { animator.counter += time.delta_secs(); modified = true; }
animator.counter = animator.counter.min(duration);
if !modified { continue; }
let new_text = (animator.function)(animator.counter/duration, &animator.string);
if new_text != text.get_single().expect("Multisegment 3D text not supported, make a PR to Lunex if you need it") {
let text = text.get_single_mut().expect("Multisegment 3D text not supported, make a PR to Lunex if you need it");
*text = new_text;
commads.trigger(RecomputeUiLayout);
}
},
}
}
}
}
pub fn typing_animation_underscore(t: f32, text: &str) -> String {
let visible_chars = (t * text.len() as f32).floor() as usize;
let visible_chars = visible_chars.min(text.len());
if visible_chars < text.len() {
format!("{}{}", &text[..visible_chars], "_")
} else {
text.to_string()
}
}
pub fn typing_animation_cursor(t: f32, text: &str) -> String {
let visible_chars = (t * text.len() as f32).floor() as usize;
let visible_chars = visible_chars.min(text.len());
if visible_chars < text.len() {
format!("{}{}", &text[..visible_chars], "|")
} else {
text.to_string()
}
}
pub fn decryption_animation(t: f32, text: &str) -> String {
let mut hasher = DefaultHasher::new();
text.hash(&mut hasher);
let seed: u64 = hasher.finish();
let mut rng = StdRng::seed_from_u64(seed + (t*60.0).round() as u64);
let symbols = "!@#$%^&*()_+-=[]{}|;:'\",.<>/?`~";
let mut result = String::with_capacity(text.len());
for (i, c) in text.chars().enumerate() {
let char_progress = (t * text.len() as f32) - i as f32;
if char_progress < 0.0 {
result.push(symbols.chars().nth(rng.random_range(0..symbols.len())).unwrap());
} else if char_progress >= 1.0 {
result.push(c);
} else {
if rng.random::<f32>() < char_progress {
result.push(c);
} else {
result.push(symbols.chars().nth(rng.random_range(0..symbols.len())).unwrap());
}
}
}
result
}
pub fn slide_in_animation(t: f32, text: &str) -> String {
let mut result = String::with_capacity(text.len());
let center = text.len() / 2;
for (i, c) in text.chars().enumerate() {
let distance_from_center = center.abs_diff(i);
let char_progress = t * 2.0 - (distance_from_center as f32 / center as f32);
if char_progress >= 1.0 {
result.push(c);
} else if char_progress > 0.0 {
result.push('_');
} else {
result.push(' ');
}
}
result
}
pub fn scrambled_reveal_animation(t: f32, text: &str) -> String {
let mut indices: Vec<usize> = (0..text.len()).collect();
let seed = 42; let mut rng = StdRng::seed_from_u64(seed);
use rand::seq::SliceRandom;
indices.shuffle(&mut rng);
let chars_to_reveal = (t * text.len() as f32).floor() as usize;
let mut result = vec![' '; text.len()];
for i in indices.iter().take(chars_to_reveal.min(text.len())) {
result[*i] = text.chars().nth(*i).unwrap();
}
result.into_iter().collect()
}
#[derive(Debug, Default, Clone)]
pub struct UiLunexAnimPlugin;
impl Plugin for UiLunexAnimPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, TextAnimator::system_2d);
#[cfg(feature = "text3d")] {
app.add_systems(Update, TextAnimator::system_3d);
}
}
}