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
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()))
|
|
}
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|