#include "animate2.hpp" #include #include #include "dbc.hpp" #include "rand.hpp" #include #include #include "sound.hpp" constexpr float SUB_FRAME_SENSITIVITY = 0.999f; namespace animate2 { using namespace std::chrono_literals; std::vector Animate2::calc_frames() { dbc::check(sequence.frames.size() == sequence.durations.size(), "sequence.frames.size() != sequence.durations.size()"); std::vector frames; for(int frame_i : sequence.frames) { dbc::check(frame_i < sheet.frames, "frame index greater than sheet frames"); frames.emplace_back( sf::Vector2i{sheet.frame_width * frame_i, 0}, // NOTE: one row only for now sf::Vector2i{sheet.frame_width, sheet.frame_height}); } return frames; } void Animate2::play() { dbc::check(!playing, "can't call play while playing?"); sequence.current = 0; sequence.subframe = 0.0f; sequence.loop_count = 0; playing = true; sequence.timer.start(); sequence.INVARIANT(); } void Animate2::stop() { playing = false; sequence.timer.reset(); } // need one for each kind of thing to animate // NOTE: possibly find a way to only run apply on frame change? void Animate2::apply(sf::Sprite& sprite) { dbc::check(sequence.current < $frame_rects.size(), "current frame past $frame_rects"); // NOTE: pos is not updated yet auto& rect = $frame_rects.at(sequence.current); sprite.setTextureRect(rect); } void Animate2::motion(sf::View& view_out, sf::Vector2f pos, sf::Vector2f size) { dbc::check(size.x > 1.0f && size.y > 1.0f, "motion size must be above 1.0 since it's not a ratio"); dbc::check(transform.flipped == false, "transform must be false, has no effect on View"); sf::Vector2f scale{transform.min_x, transform.min_y}; transform.apply(sequence, pos, scale); view_out.setCenter(pos); if(transform.scaled) { view_out.setSize({size.x * scale.x, size.y * scale.y}); } else { view_out.setSize(size); } } void Animate2::apply_effect(std::shared_ptr effect) { dbc::check(effect != nullptr, "can't apply null effect"); effect->setUniform("u_time", sequence.timer.getElapsedTime().asSeconds()); sf::Vector2f u_resolution{float(sheet.frame_width), float(sheet.frame_height)}; effect->setUniform("u_resolution", u_resolution); } void Animate2::play_sound() { // BUG: this can be optimized way better if(sounds.contains(form_name)) { for(auto& [at_frame, sound_name] : sounds.at(form_name)) { if(sequence.current == at_frame) { sound::play(sound_name); } } } else { fmt::println("Animation has not sound {}", form_name); } } /* REFACTOR: I believe this is wrong still. If ::commit() determines number of ticks+alpha since last * render then update needs to be called 1/tick. The Timer will keep track of alpha as the error * between commit calls, so this function only really needs to care about ticks. But, I'm still * calling getElapsedTime() when I already did that in commit(), so should I just ignore that and assume * elapsed is DELTA, or use elapsed here? */ void Animate2::update() { dbc::check(playing, "attempt to update animation that's not playing"); sequence.INVARIANT(); auto [ticks, alpha] = sequence.timer.commit(); int duration = sequence.durations.at(sequence.current); sequence.subframe += ticks; sequence.easing_position += ticks; bool frame_change = false; if(sequence.subframe >= duration) { sequence.timer.restart(); sequence.current++; sequence.subframe = 0; frame_change = true; } if(sequence.current >= sequence.frame_count) { sequence.loop_count++; sequence.easing_position = 0; playing = onLoop(sequence, transform); sequence.INVARIANT(); } if(frame_change) play_sound(); if(frame_change && onFrame != nullptr) onFrame(); } void Animate2::motion(sf::Transformable& sprite, sf::Vector2f pos, sf::Vector2f scale) { sequence.INVARIANT(); transform.apply(sequence, pos, scale); if(transform.flipped) { scale.x *= -1; } sprite.setPosition(pos); if(transform.scaled) { sprite.setScale(scale); } } void Timer::start() { clock.start(); prev_time = clock.getElapsedTime().asSeconds(); } void Timer::reset() { elapsed_ticks = 0; clock.reset(); } void Timer::restart() { elapsed_ticks = 0; clock.restart(); prev_time = clock.getElapsedTime().asSeconds(); } sf::Time Timer::getElapsedTime() { return clock.getElapsedTime(); } std::pair Timer::commit() { // determine frame duration based on previous time current_time = clock.getElapsedTime().asSeconds(); frame_duration = current_time - prev_time; // update prev_time for the next call prev_time = current_time; // update accumulator, retaining previous errors accumulator += frame_duration; // find the tick count based on DELTA double tick_count = floor(accumulator / DELTA); // reduce accumulator by the number of DELTAS accumulator -= tick_count * DELTA; // that leaves the remaining errors for next loop elapsed_ticks += tick_count; // alpha is then what we lerp...but WHY?! alpha = accumulator / DELTA; // return the number of even DELTA ticks and the alpha return {int(tick_count), alpha}; } void Transform::apply(Sequence& seq, sf::Vector2f& pos_out, sf::Vector2f& scale_out) { float tick = easing_func(seq.easing_position / seq.easing_duration); motion_func(*this, pos_out, scale_out, tick, relative); } bool Animate2::has_form(const std::string& as_form) { return forms.contains(as_form); } void Animate2::set_form(const std::string& as_form) { dbc::check(forms.contains(as_form), fmt::format("form {} does not exist in animation", as_form)); stop(); const auto& [seq_name, tr_name] = forms.at(as_form); dbc::check(sequences.contains(seq_name), fmt::format("sequences do NOT have \"{}\" name", seq_name)); dbc::check(transforms.contains(tr_name), fmt::format("transforms do NOT have \"{}\" name", tr_name)); // everything good, do the update form_name = as_form; sequence_name = seq_name; transform_name = tr_name; sequence = sequences.at(seq_name); transform = transforms.at(tr_name); sequence.frame_count = sequence.frames.size(); // BUG: should this be configurable instead? for(auto duration : sequence.durations) { sequence.easing_duration += float(duration); } dbc::check(sequence.easing_duration > 0.0, "bad easing duration"); $frame_rects = calc_frames(); transform.easing_func = ease2::get_easing(transform.easing); transform.motion_func = ease2::get_motion(transform.motion); sequence.INVARIANT(); } Animate2 load(const std::string &file, const std::string &anim_name) { using nlohmann::json; std::ifstream infile(file); auto data = json::parse(infile); Animate2 anim; animate2::from_json(data[anim_name], anim); dbc::check(anim.forms.contains("idle"), fmt::format("animation {} must have 'idle' form", anim_name)); anim.set_form("idle"); return anim; } void Sequence::INVARIANT(const std::source_location location) { dbc::check(frames.size() == durations.size(), fmt::format("frames.size={} doesn't match durations.size={}", frames.size(), durations.size()), location); dbc::check(easing_duration > 0.0, fmt::format("bad easing duration: {}", easing_duration), location); dbc::check(frame_count == frames.size(), fmt::format("frame_count={} doesn't match frames.size={}", frame_count, frames.size()), location); dbc::check(frame_count == durations.size(), fmt::format("frame_count={} doesn't match durations.size={}", frame_count, durations.size()), location); dbc::check(current < durations.size(), fmt::format("current={} went past end of fame durations.size={}", current, durations.size()), location); } }