2 Copyright (C) 2005 - 2025
3 by Guillaume Melquiond <guillaume.melquiond@gmail.com>
4 Copyright (C) 2004 by Philippe Plantier <ayin@anathas.org>
5 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 2 of the License, or
10 (at your option) any later version.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY.
14 See the COPYING file for more details.
19 * Template implementation for frame-based animations.
23 // ============================================================================
25 // ============================================================================
28 const T animated<T>::void_value_ = T();
31 // ============================================================================
33 // ============================================================================
36 inline animated<T>::animated(const std::chrono::milliseconds& start_time)
37 : starting_frame_time_(start_time)
38 , max_animation_time_(0)
40 , current_frame_index_(0)
45 , uses_acceleration_(false)
46 , animation_start_tick_()
54 inline animated<T>::animated(const animated<T>::anim_description& cfg, const std::chrono::milliseconds& start_time, bool force_change)
55 : starting_frame_time_(start_time)
56 , max_animation_time_(0)
58 , current_frame_index_(0)
63 , uses_acceleration_(false)
64 , animation_start_tick_()
69 for(const auto& [duration, value] : cfg) {
70 add_frame(duration, value, force_change);
75 inline void animated<T>::add_frame(const std::chrono::milliseconds& duration, const T& value, bool force_change)
77 // NOTE: We cannot use emplace_back here, because the value may be a reference into the same vector,
78 // which case emplace_back could invalidate it before the new frame is constructed.
80 if(!frames_.empty()) {
81 const auto& last_frame = frames_.back();
82 frames_.push_back(frame{duration, value, last_frame.start_time_ + last_frame.duration_});
85 is_static_ = !force_change;
86 frames_.push_back(frame{duration, value, starting_frame_time_});
91 // ============================================================================
93 // ============================================================================
96 inline void animated<T>::start_animation(const std::chrono::milliseconds& start_time_offset, bool cycles)
102 current_frame_index_ = 0;
103 const auto current_tick = get_current_animation_tick(uses_acceleration_);
104 animation_start_tick_ = current_tick - start_time_offset;
105 if(!frames_.empty()) {
106 next_frame_tick_ = animation_start_tick_ + frames_[0].start_time_ + frames_[0].duration_;
111 inline void animated<T>::pause_animation()
113 if(!started_ || paused_) {
117 pause_tick_ = get_current_animation_tick(uses_acceleration_);
121 inline void animated<T>::resume_animation()
127 const auto current_tick = get_current_animation_tick(uses_acceleration_);
128 const auto pause_duration = current_tick - pause_tick_;
130 // Shift the timeline forward by the pause duration
131 animation_start_tick_ += pause_duration;
132 next_frame_tick_ += pause_duration;
138 inline void animated<T>::advance_to_current_frame()
140 if(frames_.empty()) {
144 const auto current_tick = get_current_animation_tick(uses_acceleration_);
146 // If a cycling animation is more than a whole cycle behind (e.g., it went un-updated
147 // while offscreen), skip forward in whole cycles so the loop below never has to walk
148 // more than one cycle of frames.
150 const std::chrono::milliseconds animation_duration = get_animation_duration();
151 if(animation_duration > std::chrono::milliseconds{0} && current_tick > (next_frame_tick_ + animation_duration)) {
152 const auto behind = current_tick - next_frame_tick_;
153 next_frame_tick_ += (behind / animation_duration) * animation_duration; // The int division truncates to whole cycles.
157 // Advance frame by frame until we reach the frame containing the current time
158 while(current_tick >= next_frame_tick_) {
159 std::size_t next_index = current_frame_index_ + 1;
161 // Non-looping animation finished, stay on last frame
162 if(!cycles_ && next_index == frames_.size()) {
167 // Advance to the next frame, wrapping around when looping
168 current_frame_index_ = next_index % frames_.size();
169 next_frame_tick_ += frames_[current_frame_index_].duration_;
174 // ============================================================================
175 // Playback State Queries
176 // ============================================================================
179 inline bool animated<T>::need_update() const
181 if(is_static_ || !started_ || paused_ || finished_ || frames_.empty()) {
184 return get_current_animation_tick(uses_acceleration_) >= next_frame_tick_;
188 inline bool animated<T>::animation_finished() const
190 return finished_ || frames_.empty() || !started_ || cycles_ || (get_elapsed_time() >= get_end_time());
194 // ============================================================================
195 // Time Queries and Manipulation
196 // ============================================================================
199 inline std::chrono::steady_clock::time_point animated<T>::get_playback_tick() const
201 return paused_ ? pause_tick_ : get_current_animation_tick(uses_acceleration_);
205 inline std::chrono::milliseconds animated<T>::get_elapsed_time() const
208 return starting_frame_time_;
211 // Calculate elapsed time since animation started
212 const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(get_playback_tick() - animation_start_tick_);
214 // Apply maximum time cap if set
215 if(max_animation_time_ > std::chrono::milliseconds{0} && elapsed > max_animation_time_) {
216 return max_animation_time_;
223 inline void animated<T>::apply_time_offset(const std::chrono::milliseconds& time)
225 // Adjust the anchor point to make the current tick correspond to the requested time offset
226 animation_start_tick_ -= time;
228 // Restart animation from beginning; advance_to_current_frame() will sync on next call
229 if(!frames_.empty()) {
230 current_frame_index_ = 0;
232 next_frame_tick_ = animation_start_tick_ + frames_[0].start_time_ + frames_[0].duration_;
237 inline void animated<T>::set_duration_limit(const std::chrono::milliseconds& time)
239 max_animation_time_ = time;
243 inline std::chrono::milliseconds animated<T>::get_animation_duration() const
245 return get_end_time() - get_begin_time();
249 inline std::chrono::milliseconds animated<T>::get_begin_time() const
251 return starting_frame_time_;
255 inline std::chrono::milliseconds animated<T>::get_end_time() const
257 if(frames_.empty()) {
258 return starting_frame_time_;
260 const auto& last_frame = frames_.back();
261 return last_frame.start_time_ + last_frame.duration_;
265 inline void animated<T>::set_begin_time(const std::chrono::milliseconds& new_begin_time)
267 const auto shift = new_begin_time - starting_frame_time_;
268 starting_frame_time_ = new_begin_time;
269 for(auto& frame : frames_) {
270 frame.start_time_ += shift;
274 // ============================================================================
276 // ============================================================================
279 inline const T& animated<T>::get_current_frame() const
281 if(frames_.empty()) {
284 return frames_[current_frame_index_].value_;
288 inline std::chrono::milliseconds animated<T>::get_current_frame_begin_time() const
290 if(frames_.empty()) {
291 return starting_frame_time_;
293 return frames_[current_frame_index_].start_time_;
297 inline std::chrono::milliseconds animated<T>::get_current_frame_end_time() const
299 if(frames_.empty()) {
300 return starting_frame_time_;
302 const auto& frame = frames_[current_frame_index_];
303 return frame.start_time_ + frame.duration_;
307 inline std::chrono::milliseconds animated<T>::get_time_in_current_frame() const
309 if(frames_.empty() || !started_) {
310 return std::chrono::milliseconds{0};
312 const auto current_tick = get_playback_tick();
313 const auto& frame = frames_[current_frame_index_];
315 // How much of this frame has elapsed, clamped to [0, duration].
316 const auto time_until_next = std::chrono::duration_cast<std::chrono::milliseconds>(next_frame_tick_ - current_tick);
317 const auto elapsed_in_frame = frame.duration_ - time_until_next;
318 return std::clamp(elapsed_in_frame, std::chrono::milliseconds{0}, frame.duration_);
322 inline const T& animated<T>::get_first_frame() const
324 if(frames_.empty()) {
327 return frames_[0].value_;
331 inline const T& animated<T>::get_frame(std::size_t n) const
333 if(n >= frames_.size()) {
336 return frames_[n].value_;
340 inline const T& animated<T>::get_last_frame() const
342 if(frames_.empty()) {
345 return frames_.back().value_;
349 inline std::size_t animated<T>::get_frames_count() const
351 return frames_.size();
355 // ============================================================================
356 // Protected Methods (for unit_animation)
357 // ============================================================================
360 void animated<T>::remove_frames_until(const std::chrono::milliseconds& new_starting_time)
362 // Remove frames from the beginning until we reach the target time
363 while(starting_frame_time_ < new_starting_time && !frames_.empty()) {
364 starting_frame_time_ += frames_[0].duration_;
365 frames_.erase(frames_.begin());
370 inline void animated<T>::set_end_time(const std::chrono::milliseconds& new_ending_time)
372 if(frames_.empty()) {
376 // Cut-off before the animation begins: keep nothing but a zero-length first frame
377 if(new_ending_time <= starting_frame_time_) {
378 frames_.erase(std::next(frames_.begin()), frames_.end());
379 frames_.front().duration_ = std::chrono::milliseconds{0};
383 // Cut-off at or past the current end: extend the last frame to reach it
384 const auto current_end_time = get_end_time();
385 if(new_ending_time >= current_end_time) {
386 frames_.back().duration_ += new_ending_time - current_end_time;
390 // The cut-off lands within a frame: walk to it
391 auto current_start_time = starting_frame_time_;
392 auto it = frames_.begin();
394 while(it != frames_.end()) {
395 const auto next_end_time = current_start_time + it->duration_;
397 // Check if the cut-off point is within or at the end of this frame
398 if(next_end_time >= new_ending_time) {
399 // Truncate this frame to time remaining (a no-op if the cut-off is exactly at its end)
400 it->duration_ = new_ending_time - current_start_time;
402 // Erase all SUBSEQUENT frames (keep the current one)
403 frames_.erase(std::next(it), frames_.end());
407 current_start_time = next_end_time;