diff --git a/CMakeLists.txt b/CMakeLists.txt index cb50b102d2..911e7509e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,8 @@ add_library(${PROJECT_NAME} OBJECT src/async_op.h src/algo.h src/algo.cpp + src/animation_helper.cpp + src/animation_helper.h src/attribute.h src/attribute.cpp src/audio.cpp diff --git a/Makefile.am b/Makefile.am index 02992d0f91..73056fbb7a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -14,6 +14,8 @@ libeasyrpg_player_a_SOURCES = \ src/async_op.h \ src/algo.h \ src/algo.cpp \ + src/animation_helper.cpp \ + src/animation_helper.h \ src/attribute.h \ src/attribute.cpp \ src/audio.cpp \ diff --git a/src/animation_helper.cpp b/src/animation_helper.cpp new file mode 100644 index 0000000000..c0c46b9fb3 --- /dev/null +++ b/src/animation_helper.cpp @@ -0,0 +1,253 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#include "animation_helper.h" +#include +#include +#include +#include +#include + +namespace { + constexpr double PI = 3.14159265358979323846; + + // Forward declarations + double HandleElasticEasing(Animation_Helper::EaseType type, double t, double b, double c, double d); + double HandleBounceEasing(Animation_Helper::EaseType type, double t, double b, double c, double d); + + struct EasingData { + std::string_view name; + double x1, y1, x2, y2; + Animation_Helper::EaseType type; + }; + + const EasingData EASING_DATA[] = { + {"linear", 0.250, 0.250, 0.750, 0.750, Animation_Helper::EaseType::Linear}, + {"ease", 0.250, 0.100, 0.250, 1.000, Animation_Helper::EaseType::Ease}, + {"easeIn", 0.420, 0.000, 1.000, 1.000, Animation_Helper::EaseType::EaseIn}, + {"easeOut", 0.000, 0.000, 0.580, 1.000, Animation_Helper::EaseType::EaseOut}, + {"easeInOut", 0.420, 0.000, 0.580, 1.000, Animation_Helper::EaseType::EaseInOut}, + {"quadIn", 0.550, 0.085, 0.680, 0.530, Animation_Helper::EaseType::QuadIn}, + {"quadOut", 0.250, 0.460, 0.450, 0.940, Animation_Helper::EaseType::QuadOut}, + {"quadInOut", 0.455, 0.030, 0.515, 0.955, Animation_Helper::EaseType::QuadInOut}, + {"cubicIn", 0.550, 0.055, 0.675, 0.190, Animation_Helper::EaseType::CubicIn}, + {"cubicOut", 0.215, 0.610, 0.355, 1.000, Animation_Helper::EaseType::CubicOut}, + {"cubicInOut", 0.645, 0.045, 0.355, 1.000, Animation_Helper::EaseType::CubicInOut}, + {"quartIn", 0.895, 0.030, 0.685, 0.220, Animation_Helper::EaseType::QuartIn}, + {"quartOut", 0.165, 0.840, 0.440, 1.000, Animation_Helper::EaseType::QuartOut}, + {"quartInOut", 0.770, 0.000, 0.175, 1.000, Animation_Helper::EaseType::QuartInOut}, + {"quintIn", 0.755, 0.050, 0.855, 0.060, Animation_Helper::EaseType::QuintIn}, + {"quintOut", 0.230, 1.000, 0.320, 1.000, Animation_Helper::EaseType::QuintOut}, + {"quintInOut", 0.860, 0.000, 0.070, 1.000, Animation_Helper::EaseType::QuintInOut}, + {"sineIn", 0.470, 0.000, 0.745, 0.715, Animation_Helper::EaseType::SineIn}, + {"sineOut", 0.390, 0.575, 0.565, 1.000, Animation_Helper::EaseType::SineOut}, + {"sineInOut", 0.445, 0.050, 0.550, 0.950, Animation_Helper::EaseType::SineInOut}, + {"expoIn", 0.950, 0.050, 0.795, 0.035, Animation_Helper::EaseType::ExpoIn}, + {"expoOut", 0.190, 1.000, 0.220, 1.000, Animation_Helper::EaseType::ExpoOut}, + {"expoInOut", 1.000, 0.000, 0.000, 1.000, Animation_Helper::EaseType::ExpoInOut}, + {"circIn", 0.600, 0.040, 0.980, 0.335, Animation_Helper::EaseType::CircIn}, + {"circOut", 0.075, 0.820, 0.165, 1.000, Animation_Helper::EaseType::CircOut}, + {"circInOut", 0.785, 0.135, 0.150, 0.860, Animation_Helper::EaseType::CircInOut}, + {"backIn", 0.600, -0.280, 0.735, 0.045, Animation_Helper::EaseType::BackIn}, + {"backOut", 0.175, 0.885, 0.320, 1.275, Animation_Helper::EaseType::BackOut}, + {"backInOut", 0.680, -0.550, 0.265, 1.550, Animation_Helper::EaseType::BackInOut}, + // Special cases (no bezier points needed) + {"elasticIn", 0, 0, 0, 0, Animation_Helper::EaseType::ElasticIn}, + {"elasticOut", 0, 0, 0, 0, Animation_Helper::EaseType::ElasticOut}, + {"elasticInOut", 0, 0, 0, 0, Animation_Helper::EaseType::ElasticInOut}, + {"bounceIn", 0, 0, 0, 0, Animation_Helper::EaseType::BounceIn}, + {"bounceOut", 0, 0, 0, 0, Animation_Helper::EaseType::BounceOut}, + {"bounceInOut", 0, 0, 0, 0, Animation_Helper::EaseType::BounceInOut} + }; + + double HandleElasticEasing(Animation_Helper::EaseType type, double t, double b, double c, double d) { + constexpr double pi2 = 2.0 * PI; + const double p = d * 0.3; + const double s = p / 4.0; + + if (t <= 0.0) return b; + if (t >= 1.0) return b + c; + + switch (type) { + case Animation_Helper::EaseType::ElasticIn: { + const double post_fix = c * std::pow(2.0, 10.0 * (t - 1.0)); + return -(post_fix * std::sin((t * d - s) * pi2 / p)) + b; + } + case Animation_Helper::EaseType::ElasticOut: { + return (c * std::pow(2.0, -10.0 * t) * std::sin((t * d - s) * pi2 / p) + c + b); + } + case Animation_Helper::EaseType::ElasticInOut: { + if (t < 0.5) { + const double post_fix = c * std::pow(2.0, 10.0 * (2.0 * t - 1.0)); + return -0.5 * (post_fix * std::sin(((2.0 * t - 1.0) * d - s) * pi2 / p)) + b; + } + const double post_fix = c * std::pow(2.0, -10.0 * (2.0 * t - 1.0)); + return post_fix * std::sin(((2.0 * t - 1.0) * d - s) * pi2 / p) * 0.5 + c + b; + } + default: + return b + c * t; + } + } + + double HandleBounceEasing(Animation_Helper::EaseType type, double t, double b, double c, double d) { + switch (type) { + case Animation_Helper::EaseType::BounceIn: + return c - HandleBounceEasing(Animation_Helper::EaseType::BounceOut, 1.0 - t, 0, c, d) + b; + case Animation_Helper::EaseType::BounceOut: + if (t < (1.0 / 2.75)) { + return c * (7.5625 * t * t) + b; + } else if (t < (2.0 / 2.75)) { + t -= (1.5 / 2.75); + return c * (7.5625 * t * t + 0.75) + b; + } else if (t < (2.5 / 2.75)) { + t -= (2.25 / 2.75); + return c * (7.5625 * t * t + 0.9375) + b; + } + t -= (2.625 / 2.75); + return c * (7.5625 * t * t + 0.984375) + b; + case Animation_Helper::EaseType::BounceInOut: + if (t < 0.5) { + return HandleBounceEasing(Animation_Helper::EaseType::BounceIn, t * 2.0, 0, c, d) * 0.5 + b; + } + return HandleBounceEasing(Animation_Helper::EaseType::BounceOut, t * 2.0 - 1.0, 0, c, d) * 0.5 + c * 0.5 + b; + default: + return b + c * t; + } + } +} + +namespace Animation_Helper { + +double CubicBezier(double progress, double x1, double y1, double x2, double y2) { + // This is a cubic Bezier curve for animation timing: + // P₀(0,0) - start point, always fixed + // P₁(x1,y1) - first control point + // P₂(x2,y2) - second control point + // P₃(1,1) - end point, always fixed + // + // x coordinates (x1,x2) control the timing curve's shape + // y coordinates (y1,y2) control the rate of change at those points + + ////progress = std::clamp(progress, 0.0, 1.0); + + // Since we need to find y for a given x (progress), + // we need to solve for t where the curve's x equals our progress + // This is a rough approximation using a few iterations + double t = progress; // Initial guess + + // Newton's method to find better t value + for (int i = 0; i < 5; i++) { // Usually converges in 4-5 iterations + const double currentT = t; + const double oneMinusT = 1.0 - t; + + // Calculate x(t) - current x position on curve + const double x = 3.0 * oneMinusT * oneMinusT * t * x1 + + 3.0 * oneMinusT * t * t * x2 + + t * t * t; + + // If we're close enough to desired x, calculate final y + if (std::abs(x - progress) < 0.001) { + break; + } + + // Calculate x'(t) - derivative of x with respect to t + const double dx = 3.0 * oneMinusT * oneMinusT * x1 + + 6.0 * oneMinusT * t * (x2 - x1) + + 3.0 * t * t * (1 - x2); + + // Avoid division by zero + if (std::abs(dx) < 0.0001) { + break; + } + + // Newton iteration + t = t - (x - progress) / dx; + //// t = std::clamp(t, 0.0, 1.0); + + // If t hasn't changed significantly, we're done + if (std::abs(t - currentT) < 0.0001) { + break; + } + } + + // Calculate final y value using found t + const double oneMinusT = 1.0 - t; + return 3.0 * oneMinusT * oneMinusT * t * y1 + + 3.0 * oneMinusT * t * t * y2 + + t * t * t; +} + +EaseType StringToEaseType(std::string_view type_name) { + for (const auto& data : EASING_DATA) { + if (data.name == type_name) { + return data.type; + } + } + return EaseType::Linear; +} + +double GetEasedTime(EaseType type, double t, double b, double c, double d) { + t = std::clamp(t, 0.0, d); + const double normalized_t = t / d; + + // Handle special cases first + switch (type) { + case EaseType::ElasticIn: + case EaseType::ElasticOut: + case EaseType::ElasticInOut: + return HandleElasticEasing(type, normalized_t, b, c, d); + case EaseType::BounceIn: + case EaseType::BounceOut: + case EaseType::BounceInOut: + return HandleBounceEasing(type, normalized_t, b, c, d); + default: + break; + } + + // Find bezier points for the type + for (const auto& data : EASING_DATA) { + if (data.type == type) { + return b + c * CubicBezier(normalized_t, data.x1, data.y1, data.x2, data.y2); + } + } + + return b + c * normalized_t; // Linear fallback +} + +double GetEasedTime(const std::string& easing_type, double t, double b, double c, double d) { + if (easing_type.substr(0, 6) == "bezier") { + std::vector params; + size_t start = easing_type.find('(') + 1; + size_t end = easing_type.find(')'); + if (start != std::string::npos && end != std::string::npos) { + std::string values = easing_type.substr(start, end - start); + std::istringstream iss(values); + double val; + while (iss >> val) { + params.push_back(val); + iss.ignore(); + } + if (params.size() == 4) { + return b + c * CubicBezier(t/d, params[0], params[1], params[2], params[3]); + } + } + } + + return GetEasedTime(StringToEaseType(easing_type), t, b, c, d); +} + +} // namespace Animation_Helper diff --git a/src/animation_helper.h b/src/animation_helper.h new file mode 100644 index 0000000000..563599a86b --- /dev/null +++ b/src/animation_helper.h @@ -0,0 +1,125 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef ANIMATION_HELPER_H +#define ANIMATION_HELPER_H + +#include +#include + +namespace Animation_Helper { + /** + * Supported easing types for animations. + * These match CSS animation timing functions for familiarity. + */ + enum class EaseType { + Linear, + // Basic + Ease, + EaseIn, + EaseOut, + EaseInOut, + // Quadratic + QuadIn, + QuadOut, + QuadInOut, + // Cubic + CubicIn, + CubicOut, + CubicInOut, + // Quartic + QuartIn, + QuartOut, + QuartInOut, + // Quintic + QuintIn, + QuintOut, + QuintInOut, + // Sinusoidal + SineIn, + SineOut, + SineInOut, + // Exponential + ExpoIn, + ExpoOut, + ExpoInOut, + // Circular + CircIn, + CircOut, + CircInOut, + // Back + BackIn, + BackOut, + BackInOut, + // Elastic + ElasticIn, + ElasticOut, + ElasticInOut, + // Bounce + BounceIn, + BounceOut, + BounceInOut + }; + + /** + * Converts a string to an EaseType enum value + * @param type_name The name of the easing type + * @return The corresponding EaseType enum value + */ + EaseType StringToEaseType(std::string_view type_name); + + /** + * Calculates a point on a cubic Bezier curve for animation timing + * @param progress Current animation progress (0 to 1) + * @param x1 X coordinate of first control point, controls when early changes occur + * @param y1 Y coordinate of first control point, controls magnitude of early changes + * @param x2 X coordinate of second control point, controls when later changes occur + * @param y2 Y coordinate of second control point, controls magnitude of later changes + * @return The calculated animation progress value + * + * Control points affect the animation as follows: + * - x values control timing (when changes happen) + * - y values control magnitude (how much change occurs) + * - Values > current progress create overshoots + * - Values < current progress create undershoots + */ + double CubicBezier(double progress, double x1, double y1, double x2, double y2); + + /** + * Gets the eased time value based on the specified easing type + * @param easing_type Type of easing to apply + * @param t Current time (0 to 1) + * @param b Start value + * @param c Change in value (end - start) + * @param d Duration + * @return The eased value + */ + double GetEasedTime(const std::string& easing_type, double t, double b, double c, double d); + + /** + * Gets the eased time value using enum-based easing type + * @param type The easing type enum + * @param t Current time (0 to 1) + * @param b Start value + * @param c Change in value (end - start) + * @param d Duration + * @return The eased value + */ + double GetEasedTime(EaseType type, double t, double b, double c, double d); +} + +#endif diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index 91821c45e0..4200dc448e 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -26,6 +26,7 @@ #include #include "game_interpreter.h" #include "async_handler.h" +#include "animation_helper.h" #include "audio.h" #include "game_dynrpg.h" #include "filefinder.h" @@ -786,6 +787,8 @@ bool Game_Interpreter::ExecuteCommand(lcf::rpg::EventCommand const& com) { return CommandManiacCallCommand(com); case Cmd::Maniac_GetGameInfo: return CommandManiacGetGameInfo(com); + case Cmd::EasyRpg_AnimateVariable: + return CommandEasyRpgAnimateVariable(com); case Cmd::EasyRpg_SetInterpreterFlag: return CommandEasyRpgSetInterpreterFlag(com); case Cmd::EasyRpg_ProcessJson: @@ -5319,6 +5322,105 @@ bool Game_Interpreter::CommandManiacCallCommand(lcf::rpg::EventCommand const& co return true; } +bool Game_Interpreter::CommandEasyRpgAnimateVariable(lcf::rpg::EventCommand const& com) { + struct AnimationParams { + int32_t target; + int32_t start; + int32_t end; + int32_t duration; + std::string easing_start; + std::string easing_end; + }; + + // Parse command parameters + AnimationParams params{ + ValueOrVariable(com.parameters[0], com.parameters[1]), + ValueOrVariable(com.parameters[2], com.parameters[3]), + ValueOrVariable(com.parameters[4], com.parameters[5]), + ValueOrVariable(com.parameters[6], com.parameters[7]), + ToString(com.string), + "null" + }; + + // Extract dual easing types if present + const auto separator_pos = params.easing_start.find('/'); + if (separator_pos != std::string::npos) { + params.easing_end = params.easing_start.substr(separator_pos + 1); + params.easing_start = params.easing_start.substr(0, separator_pos); + } + + // Prepare animation commands + std::vector cmd_list; + cmd_list.reserve(params.duration * 2); // Pre-allocate for efficiency + + lcf::rpg::EventCommand wait_com; + wait_com.code = static_cast(Cmd::Wait); + + lcf::rpg::EventCommand var_com; + var_com.code = static_cast(Cmd::ControlVars); + std::vector var_params = { + 0, + params.target, + 0, + 0, + 0, + params.end + }; + + const double step_size = 1.0 / params.duration; + const int half_duration = params.duration / 2; + + // Generate animation steps + for (int step = 1; step <= params.duration; ++step) { + const double current_time = step * step_size; + double normalized_time; + const std::string& current_easing = (params.easing_end == "null") + ? params.easing_start + : (step <= half_duration ? params.easing_start : params.easing_end); + + // Calculate normalized time based on single or dual easing + if (params.easing_end == "null") { + normalized_time = current_time; + } + else { + // For dual easing, each half uses its own normalized time from 0 to 1 + normalized_time = step <= half_duration + ? (current_time * 2.0) // First half: 0->1 + : ((current_time - 0.5) * 2.0); // Second half: 0->1 + } + + // Get eased time for current segment + const double eased_time = Animation_Helper::GetEasedTime(current_easing, normalized_time, 0, 1, 1); + + // Calculate interpolated value + double interpolated; + if (params.easing_end == "null") { + // Single easing - interpolate directly from start to end + interpolated = params.start + eased_time * (params.end - params.start); + } + else { + // Dual easing - interpolate each half separately + const double mid_point = (params.start + params.end) * 0.5; + if (step <= half_duration) { + interpolated = params.start + eased_time * (mid_point - params.start); + } + else { + interpolated = mid_point + eased_time * (params.end - mid_point); + } + } + + // Update and push commands + var_params.back() = static_cast(interpolated); + var_com.parameters = lcf::DBArray(var_params.begin(), var_params.end()); + + cmd_list.push_back(var_com); + cmd_list.push_back(wait_com); + } + + Push(cmd_list, 0, false); + return true; +} + bool Game_Interpreter::CommandEasyRpgSetInterpreterFlag(lcf::rpg::EventCommand const& com) { if (!Player::HasEasyRpgExtensions()) { return true; diff --git a/src/game_interpreter.h b/src/game_interpreter.h index 04bf3afda3..d69d9b8315 100644 --- a/src/game_interpreter.h +++ b/src/game_interpreter.h @@ -297,6 +297,7 @@ class Game_Interpreter : public Game_BaseInterpreterContext bool CommandManiacSetGameOption(lcf::rpg::EventCommand const& com); bool CommandManiacControlStrings(lcf::rpg::EventCommand const& com); bool CommandManiacCallCommand(lcf::rpg::EventCommand const& com); + bool CommandEasyRpgAnimateVariable(lcf::rpg::EventCommand const& com); bool CommandEasyRpgSetInterpreterFlag(lcf::rpg::EventCommand const& com); bool CommandEasyRpgProcessJson(lcf::rpg::EventCommand const& com); bool CommandEasyRpgCloneMapEvent(lcf::rpg::EventCommand const& com);