You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

179 lines
6.4 KiB
Rust

use std::time::Duration;
///
/// Animated Text
///
/// The goal of this code is to get a rudimentary "Text Animation" system in place.
///
/// The use cases are:
/// * Typing Text; ala RPG dialogs like Pokemon
///
/// Eventually adding more would be cool, like movement animations, but those get really
/// complicated really fast...
///
use bevy::prelude::*;
pub struct AnimatedTextPlugin;
impl Plugin for AnimatedTextPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, manage_texts)
.add_systems(Update, animate_texts);
}
}
#[derive(Bundle, Default)]
pub struct AnimatedTextBundle {
pub text_bundle: TextBundle,
pub animated_text: AnimatedText,
}
/// Animated Text Marker
/// Use this to filter out entities managed by Text Animation systems
#[derive(Component, Default, Debug)]
pub struct AnimatedText {
animation_type: Option<TextAnimationType>,
animation_status: TextAnimationStatus,
animation_duration: Option<Duration>,
}
/// Animated Text component
///
/// Handles all of the logistics of running text animation
impl AnimatedText {
pub fn new(animation_type: TextAnimationType) -> Self {
AnimatedText {
animation_type: Some(animation_type),
..default()
}
}
pub fn play(&mut self) {
self.animation_status = TextAnimationStatus::Playing;
self.animation_duration = match self.animation_type {
None => None,
Some(TextAnimationType::Typing(seconds)) => Some(Duration::from_secs_f32(seconds)),
};
}
pub fn stop(&mut self) {
self.animation_status = TextAnimationStatus::Stopped;
self.animation_duration = None;
}
pub fn toggle(&mut self) {
use TextAnimationStatus::*;
self.animation_status = match self.animation_status {
Playing => Stopped,
Stopped => Playing,
}
}
}
#[derive(Debug)]
pub enum TextAnimationType {
Typing(f32), // Typing text out for duration
}
#[derive(Default, Debug)]
enum TextAnimationStatus {
Playing,
#[default]
Stopped,
}
/// Manage individual text entities
///
/// Animated text entities need to conform to different shapes for functional reasons.
/// For example if each letter needs to be a different color, each TextSection must be a single
/// character rather than a word or paragraph.
///
/// Any time a text is updated we need to ensure it conforms to the needs of it's animation
///
/// FIXME: Only update according to animation type
fn manage_texts(mut texts: Query<&mut Text, (Changed<Text>, With<AnimatedText>)>) {
// Check if any Text entities are "dirty"
let dirty = texts
.iter()
.any(|text| text.sections.iter().any(|section| section.value.len() > 1));
// For each text
// For each section
// If section length > 1
// Break into length-1 strings
if dirty {
texts.iter_mut().for_each(|mut text| {
// Replace the existing sections with broken down sections
// Each Text Section is a single character
// On it's own this should not cause the text to look different
// But means we can modify each text section individually in color
text.sections = text
.sections
.iter()
.map(|section| {
section.value.chars().map(|c| TextSection {
value: c.into(),
style: section.style.clone(),
})
})
.flatten()
.collect();
});
}
}
fn animate_texts(mut query: Query<(&mut Text, &mut AnimatedText)>, time: Res<Time>) {
for (mut text, mut animated_text) in query.iter_mut() {
match animated_text.animation_status {
TextAnimationStatus::Stopped => (),
TextAnimationStatus::Playing => match animated_text.animation_type {
None => (),
Some(TextAnimationType::Typing(seconds)) => {
animated_text.animation_duration = match animated_text.animation_duration {
None | Some(Duration::ZERO) => {
// Set sections to alpha 0 before animation begins
text.sections.iter_mut().for_each(|section| {
section.style.color.set_a(0.0);
});
// We have just (re)started the animation, so set the duration to full
Some(Duration::from_secs_f32(seconds))
}
// FIXME: Why can't mutate inner on animation_duration?
Some(inner) => {
{
// how far into the animation are we?
let percentage = 1.0 - (inner.as_secs_f32() / seconds);
// Find the total number of characters to be processed
// let len_total = text
// .sections
// .iter()
// .fold(0, |acc, curr| acc + curr.value.len());
// Backup version:
let len_total = text.sections.len();
// Find the farthest character into the string to show
let target = (len_total as f32 * percentage) as usize;
// Assign all segments an alpha of 0 or 1.
// TODO: Incremental updates only: Start at target, work backward
// until you get to one with alpha 1 and then break
text.sections
.iter_mut()
.take(target)
.rev()
.take_while(|section| section.style.color.a() != 1.0)
.for_each(|section| {
section.style.color.set_a(1.0);
});
}
// We are continuing the animation, so decrement the remaining duration
Some(inner.saturating_sub(time.delta()))
}
}
}
},
}
}
}