From 9e6966c9616313413387738e67036a489c844197 Mon Sep 17 00:00:00 2001 From: Wiseguy <68165316+Mr-Wiseguy@users.noreply.github.com> Date: Mon, 12 Jan 2026 17:15:56 -0500 Subject: [PATCH] Launcher background (#58) * Begin implementing launcher background * Add keyframe system for launcher. * Mirror Banjo. * More work on animations. * Update RecompFrontend. * Update RT64. * Fix linux build * adjust launcher anim and launcher --------- Co-authored-by: Dario Co-authored-by: Reonu Co-authored-by: thecozies <79979276+thecozies@users.noreply.github.com> --- CMakeLists.txt | 1 + assets/JiggyColor.svg | 9 + assets/JiggyHole.svg | 7 + assets/JiggyShine.svg | 11 ++ include/banjo_launcher.h | 16 ++ lib/RecompFrontend | 2 +- src/main/launcher_animation.cpp | 308 ++++++++++++++++++++++++++++++++ src/main/main.cpp | 50 ++++-- 8 files changed, 391 insertions(+), 13 deletions(-) create mode 100644 assets/JiggyColor.svg create mode 100644 assets/JiggyHole.svg create mode 100644 assets/JiggyShine.svg create mode 100644 include/banjo_launcher.h create mode 100644 src/main/launcher_animation.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 293cb22..1ab959d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -140,6 +140,7 @@ add_custom_command(OUTPUT add_executable(BanjoRecompiled) set (SOURCES + ${CMAKE_SOURCE_DIR}/src/main/launcher_animation.cpp ${CMAKE_SOURCE_DIR}/src/main/main.cpp ${CMAKE_SOURCE_DIR}/src/main/register_overlays.cpp ${CMAKE_SOURCE_DIR}/src/main/register_patches.cpp diff --git a/assets/JiggyColor.svg b/assets/JiggyColor.svg new file mode 100644 index 0000000..7b2891c --- /dev/null +++ b/assets/JiggyColor.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/JiggyHole.svg b/assets/JiggyHole.svg new file mode 100644 index 0000000..f249d00 --- /dev/null +++ b/assets/JiggyHole.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/JiggyShine.svg b/assets/JiggyShine.svg new file mode 100644 index 0000000..cefee5b --- /dev/null +++ b/assets/JiggyShine.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/include/banjo_launcher.h b/include/banjo_launcher.h new file mode 100644 index 0000000..0a4c330 --- /dev/null +++ b/include/banjo_launcher.h @@ -0,0 +1,16 @@ +#ifndef __BANJO_LAUNCHER_H__ +#define __BANJO_LAUNCHER_H__ + +#include "recompui/recompui.h" + +namespace banjo { + void launcher_animation_setup(recompui::LauncherMenu *menu, recompui::Element *title); + void launcher_animation_update(recompui::LauncherMenu *menu); + + constexpr float launcher_options_right_position_start = 96.0f; + constexpr float launcher_options_right_position_end = 96.0f + 24.0f; + constexpr float launcher_options_top_offset = 96.0f; + constexpr float launcher_options_title_offset = 120.0f; +} + +#endif diff --git a/lib/RecompFrontend b/lib/RecompFrontend index 0ea1e75..62f2f06 160000 --- a/lib/RecompFrontend +++ b/lib/RecompFrontend @@ -1 +1 @@ -Subproject commit 0ea1e75a40c61fd3352fa497bb52ae7e0f56dd0f +Subproject commit 62f2f0652f4cbef87ecabaf9a5af5195beb00d79 diff --git a/src/main/launcher_animation.cpp b/src/main/launcher_animation.cpp new file mode 100644 index 0000000..174de9e --- /dev/null +++ b/src/main/launcher_animation.cpp @@ -0,0 +1,308 @@ +#include "banjo_launcher.h" + +struct KeyframeRot { + float seconds; + float deg; +}; + +struct Keyframe2D { + float seconds; + float x; + float y; +}; + +enum class InterpolationMethod { + Linear, + Smootherstep +}; + +struct AnimationData { + uint32_t keyframe_index = 0; + uint32_t loop_keyframe_index = UINT32_MAX; + float seconds = 0.0f; + InterpolationMethod interpolation_method = InterpolationMethod::Linear; +}; + +struct AnimatedSvg { + recompui::Element *svg = nullptr; + std::vector position_keyframes; + std::vector scale_keyframes; + std::vector rotation_keyframes; + AnimationData position_animation; + AnimationData scale_animation; + AnimationData rotation_animation; + float width = 0; + float height = 0; +}; + +struct LauncherContext { + AnimatedSvg banjo_svg; + AnimatedSvg kazooie_svg; + AnimatedSvg jiggy_color_svg; + AnimatedSvg jiggy_shine_svg; + AnimatedSvg jiggy_hole_svg; + recompui::Element *wrapper; + recompui::Element *title; + std::chrono::steady_clock::time_point last_update_time; + std::chrono::steady_clock::time_point start_time; + bool started = false; +} launcher_context; + +float interpolate_value(float a, float b, float t, InterpolationMethod method) { + switch (method) { + case InterpolationMethod::Smootherstep: + return a + (b - a) * (t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f)); + case InterpolationMethod::Linear: + default: + return a + (b - a) * t; + } +} + +void calculate_rot_from_keyframes(const std::vector &kf, AnimationData &an, float delta_time, float °) { + if (kf.empty()) { + return; + } + + an.seconds += delta_time; + + while ((an.keyframe_index < (kf.size() - 1) && (an.seconds >= kf[an.keyframe_index + 1].seconds))) { + an.keyframe_index++; + } + + if (an.keyframe_index >= (kf.size() - 1)) { + deg = kf[an.keyframe_index].deg; + } + else { + float t = (an.seconds - kf[an.keyframe_index].seconds) / (kf[an.keyframe_index + 1].seconds - kf[an.keyframe_index].seconds); + deg = interpolate_value(kf[an.keyframe_index].deg, kf[an.keyframe_index + 1].deg, t, an.interpolation_method); + } +} + +void calculate_2d_from_keyframes(const std::vector &kf, AnimationData &an, float delta_time, float &x, float &y) { + if (kf.empty()) { + return; + } + + an.seconds += delta_time; + + while ((an.keyframe_index < (kf.size() - 1) && (an.seconds >= kf[an.keyframe_index + 1].seconds))) { + an.keyframe_index++; + } + + if ((an.loop_keyframe_index != UINT32_MAX) && (an.keyframe_index >= (kf.size() - 1))) { + an.seconds = kf[an.loop_keyframe_index].seconds + (an.seconds - kf[an.keyframe_index].seconds); + an.keyframe_index = an.loop_keyframe_index; + } + + if (an.keyframe_index >= (kf.size() - 1)) { + x = kf[an.keyframe_index].x; + y = kf[an.keyframe_index].y; + } + else { + float t = (an.seconds - kf[an.keyframe_index].seconds) / (kf[an.keyframe_index + 1].seconds - kf[an.keyframe_index].seconds); + x = interpolate_value(kf[an.keyframe_index].x, kf[an.keyframe_index + 1].x, t, an.interpolation_method); + y = interpolate_value(kf[an.keyframe_index].y, kf[an.keyframe_index + 1].y, t, an.interpolation_method); + } +} + +AnimatedSvg create_animated_svg(recompui::ContextId context, recompui::Element *parent, const std::string &svg_path, float width, float height) { + AnimatedSvg animated_svg; + animated_svg.width = width; + animated_svg.height = height; + animated_svg.svg = context.create_element(parent, svg_path); + animated_svg.svg->set_position(recompui::Position::Absolute); + animated_svg.svg->set_width(width, recompui::Unit::Dp); + animated_svg.svg->set_height(height, recompui::Unit::Dp); + return animated_svg; +} + +void update_animated_svg(AnimatedSvg &animated_svg, float delta_time, float bg_width, float bg_height) { + float position_x = 0.0f, position_y = 0.0f; + float scale_x = 1.0f, scale_y = 1.0f; + float rotation_degrees = 0.0f; + calculate_2d_from_keyframes(animated_svg.position_keyframes, animated_svg.position_animation, delta_time, position_x, position_y); + calculate_2d_from_keyframes(animated_svg.scale_keyframes, animated_svg.scale_animation, delta_time, scale_x, scale_y); + calculate_rot_from_keyframes(animated_svg.rotation_keyframes, animated_svg.rotation_animation, delta_time, rotation_degrees); + animated_svg.svg->set_translate_2D(position_x + bg_width / 2.0f - animated_svg.width / 2.0f, position_y + bg_height / 2.0f - animated_svg.height / 2.0f); + animated_svg.svg->set_scale_2D(scale_x, scale_y); + animated_svg.svg->set_rotation(rotation_degrees); +} + +const float jiggy_scale_anim_start = 0.4f; +const float jiggy_scale_anim_length = 0.75f; +const float jiggy_scale_anim_end = jiggy_scale_anim_start + jiggy_scale_anim_length; +const float jiggy_move_over_start = jiggy_scale_anim_end + 0.5f; +const float jiggy_move_over_length = 0.75f; +const float jiggy_move_over_end = jiggy_move_over_start + jiggy_move_over_length; +const float jiggy_shine_start = jiggy_move_over_end - 0.5f; +const float jiggy_shine_length = 0.8f; + +void banjo::launcher_animation_setup(recompui::LauncherMenu *menu, recompui::Element *title) { + auto context = recompui::get_current_context(); + recompui::Element *background_container = menu->get_background_container(); + background_container->set_background_color({ 0x1A, 0x56, 0x98, 0xFF }); + + launcher_context.wrapper = context.create_element(background_container, 0); + launcher_context.wrapper->set_position(recompui::Position::Absolute); + launcher_context.wrapper->set_width(100, recompui::Unit::Percent); + launcher_context.wrapper->set_height(100, recompui::Unit::Percent); + launcher_context.wrapper->set_top(0); + + // The creation order of these is important. + launcher_context.jiggy_color_svg = create_animated_svg(context, launcher_context.wrapper, "JiggyColor.svg", 1054.0f, 1044.0f); + launcher_context.jiggy_shine_svg = create_animated_svg(context, launcher_context.wrapper, "JiggyShine.svg", 219.0f, 1080.0f); + launcher_context.jiggy_hole_svg = create_animated_svg(context, launcher_context.wrapper, "JiggyHole.svg", 2180.0f, 2160.0f); + launcher_context.banjo_svg = create_animated_svg(context, launcher_context.wrapper, "Banjo.svg", 649.0f, 622.0f); + launcher_context.kazooie_svg = create_animated_svg(context, launcher_context.wrapper, "Kazooie.svg", 626.0f, 774.0f); + + // Animate the jiggy hole. + launcher_context.jiggy_hole_svg.position_keyframes = { + { 0.0f, 0.0f, 0.0f }, + { 3.0f, 0.0f, -5.0f }, + { 6.0f, 0.0f, 5.0f }, + { 9.0f, 0.0f, -5.0f }, + }; + + launcher_context.jiggy_hole_svg.scale_keyframes = { + { 0.0f, 0.0f, 0.0f }, + { jiggy_scale_anim_start + 0.0f, 0.0f, 0.0f }, + { jiggy_scale_anim_start + jiggy_scale_anim_length, 1.0f, 1.0f }, + }; + + launcher_context.jiggy_hole_svg.rotation_keyframes = { + { 0.0f, -45.0f }, + { jiggy_scale_anim_start + 0.0f, -45.0f }, + { jiggy_scale_anim_start + jiggy_scale_anim_length, 0.0f }, + }; + + launcher_context.jiggy_hole_svg.position_animation.loop_keyframe_index = 1; + launcher_context.jiggy_hole_svg.position_animation.interpolation_method = InterpolationMethod::Smootherstep; + launcher_context.jiggy_hole_svg.scale_animation.interpolation_method = InterpolationMethod::Smootherstep; + launcher_context.jiggy_hole_svg.rotation_animation.interpolation_method = InterpolationMethod::Smootherstep; + + // Copy keyframes from the hole to the color. + launcher_context.jiggy_color_svg.position_keyframes = launcher_context.jiggy_hole_svg.position_keyframes; + launcher_context.jiggy_color_svg.position_animation = launcher_context.jiggy_hole_svg.position_animation; + launcher_context.jiggy_color_svg.scale_keyframes = launcher_context.jiggy_hole_svg.scale_keyframes; + launcher_context.jiggy_color_svg.scale_animation = launcher_context.jiggy_hole_svg.scale_animation; + launcher_context.jiggy_color_svg.rotation_keyframes = launcher_context.jiggy_hole_svg.rotation_keyframes; + launcher_context.jiggy_color_svg.rotation_animation = launcher_context.jiggy_hole_svg.rotation_animation; + + // Animate the jiggy shine. + launcher_context.jiggy_shine_svg.position_keyframes = { + { 0.0f, 700.0f, 0.0f }, + { jiggy_shine_start, 700.0f, 0.0f }, + { jiggy_shine_start + jiggy_shine_length, -700.0f, 0.0f }, + }; + + launcher_context.jiggy_shine_svg.scale_keyframes = { + { 0.0f, 0.0f, 0.0f }, + { jiggy_shine_start, 0.0f, 0.0f }, + { jiggy_shine_start + 0.01f, 1.0f, 1.0f }, + }; + + launcher_context.jiggy_shine_svg.position_animation.interpolation_method = InterpolationMethod::Smootherstep; + + // Animate Banjo. + launcher_context.banjo_svg.position_keyframes = { + { 0.0f, -1200.0f, 0.0f }, + { 1.0f, -220.0f, 0.0f }, + { 2.0f, -220.0f, -5.0f }, + { 5.0f, -220.0f, 5.0f }, + { 8.0f, -220.0f, -5.0f }, + }; + + launcher_context.banjo_svg.scale_keyframes = { + { 0.0f, 0.0f, 0.0f }, + { 0.1f, 1.0f, 1.0f }, + }; + + launcher_context.banjo_svg.rotation_keyframes = { + { 0.0f, -20.0f }, + { 1.0f, 0.0f }, + }; + + launcher_context.banjo_svg.position_animation.loop_keyframe_index = 2; + launcher_context.banjo_svg.position_animation.interpolation_method = InterpolationMethod::Smootherstep; + launcher_context.banjo_svg.rotation_animation.interpolation_method = InterpolationMethod::Smootherstep; + + // Animate Kazooie. Mirror all of Banjo's keyframes except for the scale. + launcher_context.kazooie_svg.position_keyframes = launcher_context.banjo_svg.position_keyframes; + launcher_context.kazooie_svg.position_animation = launcher_context.banjo_svg.position_animation; + launcher_context.kazooie_svg.scale_keyframes = launcher_context.banjo_svg.scale_keyframes; + launcher_context.kazooie_svg.scale_animation = launcher_context.banjo_svg.scale_animation; + launcher_context.kazooie_svg.rotation_keyframes = launcher_context.banjo_svg.rotation_keyframes; + launcher_context.kazooie_svg.rotation_animation = launcher_context.banjo_svg.rotation_animation; + + for (auto &kf : launcher_context.kazooie_svg.position_keyframes) { + kf.x = -kf.x; + } + + launcher_context.kazooie_svg.position_keyframes[2].seconds += 1.5f; + launcher_context.kazooie_svg.position_keyframes[3].seconds += 1.5f; + launcher_context.kazooie_svg.position_keyframes[4].seconds += 1.5f; + + for (auto &kf : launcher_context.kazooie_svg.rotation_keyframes) { + kf.deg = -kf.deg; + } + + launcher_context.start_time = std::chrono::steady_clock::now(); + + launcher_context.title = title; +} + +void banjo::launcher_animation_update(recompui::LauncherMenu *menu) { + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + float delta_time = launcher_context.started ? std::chrono::duration_cast(now - launcher_context.last_update_time).count() / 1000.0f : 0.0f; + launcher_context.last_update_time = now; + launcher_context.started = true; + + recompui::Element *background_container = menu->get_background_container(); + float dp_to_pixel_ratio = background_container->get_dp_to_pixel_ratio(); + float bg_width = background_container->get_client_width() / dp_to_pixel_ratio; + float bg_height = background_container->get_client_height() / dp_to_pixel_ratio; + update_animated_svg(launcher_context.banjo_svg, delta_time, bg_width, bg_height); + update_animated_svg(launcher_context.kazooie_svg, delta_time, bg_width, bg_height); + update_animated_svg(launcher_context.jiggy_color_svg, delta_time, bg_width, bg_height); + update_animated_svg(launcher_context.jiggy_shine_svg, delta_time, bg_width, bg_height); + update_animated_svg(launcher_context.jiggy_hole_svg, delta_time, bg_width, bg_height); + + auto elapsed = now - launcher_context.start_time; + float elapsed_ms = std::chrono::duration_cast(elapsed).count(); + float from_ms = jiggy_move_over_start * 1000.0f; + float to_ms = jiggy_move_over_end * 1000.0f; + if (elapsed_ms > from_ms && elapsed_ms < to_ms) { + float now_ms = elapsed_ms; + float t = (now_ms - from_ms) / (to_ms - from_ms); + float x_translation = interpolate_value(0, 1440 * -0.2f, t, InterpolationMethod::Smootherstep); + launcher_context.wrapper->set_translate_2D(x_translation, 0, recompui::Unit::Dp); + float y_translation = interpolate_value(0, launcher_options_top_offset, t, InterpolationMethod::Smootherstep); + launcher_context.wrapper->set_top(y_translation); + float scale = interpolate_value(1, 0.666f, t, InterpolationMethod::Smootherstep); + launcher_context.wrapper->set_scale_2D(scale, scale); + + float game_option_menu_opacity = interpolate_value(0, 1.0f, t, InterpolationMethod::Smootherstep); + for (auto option : menu->get_game_options_menu()->get_options()) { + option->set_opacity(game_option_menu_opacity); + } + launcher_context.title->set_opacity(game_option_menu_opacity); + + float game_option_menu_right = interpolate_value( + launcher_options_right_position_start, launcher_options_right_position_end, t, InterpolationMethod::Smootherstep); + menu->get_game_options_menu()->set_right(game_option_menu_right); + } + + if (elapsed_ms <= from_ms) { + for (auto option : menu->get_game_options_menu()->get_options()) { + option->set_opacity(0); + } + launcher_context.title->set_opacity(0.0f); + menu->get_game_options_menu()->set_right(launcher_options_right_position_start); + } else if (elapsed_ms >= to_ms) { + for (auto option : menu->get_game_options_menu()->get_options()) { + option->set_opacity(1.0f); + } + launcher_context.title->set_opacity(1.0f); + menu->get_game_options_menu()->set_right(launcher_options_right_position_end); + } +} diff --git a/src/main/main.cpp b/src/main/main.cpp index 0002ad8..a01b642 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -40,6 +40,7 @@ #include "banjo_sound.h" #include "banjo_support.h" #include "banjo_game.h" +#include "banjo_launcher.h" #include "recomp_data.h" #include "ovl_patches.hpp" #include "theme.h" @@ -82,7 +83,7 @@ ultramodern::gfx_callbacks_t::gfx_data_t create_gfx() { SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); - if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) > 0) { + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC) > 0) { exit_error("Failed to initialize SDL2: %s\n", SDL_GetError()); } @@ -151,7 +152,7 @@ ultramodern::renderer::WindowHandle create_window(ultramodern::gfx_callbacks_t:: flags |= SDL_WINDOW_VULKAN; #endif - window = SDL_CreateWindow("Banjo: Recompiled", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1600, 960, flags); + window = SDL_CreateWindow("Banjo: Recompiled", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1600, 900, flags); if (window == nullptr) { exit_error("Failed to create window: %s\n", SDL_GetError()); @@ -546,19 +547,43 @@ void on_launcher_init(recompui::LauncherMenu *menu) { supported_games[0].mod_game_id, recompui::GameOptionsMenuLayout::Center ); + game_options_menu->add_default_options(); - constexpr float banjo_apsect_ratio = 1.0434f; - constexpr float banjo_height = 500.0f; + for (auto option : game_options_menu->get_options()) { + option->set_justify_content(recompui::JustifyContent::FlexEnd); + option->set_border_radius(0); - // TODO: Style launcher and get better background. - auto bg_element = menu->set_launcher_background_svg("Banjo.svg"); - bg_element->set_top(50.0f, recompui::Unit::Percent); - bg_element->set_left(50.0f, recompui::Unit::Percent); - bg_element->set_height(banjo_height, recompui::Unit::Dp); - bg_element->set_width(banjo_height * banjo_apsect_ratio, recompui::Unit::Dp); - bg_element->set_translate_2D(-50.0f, -50.0f, recompui::Unit::Percent); - bg_element->set_opacity(0.25f); + std::vector hover_focus = {&option->hover_style, &option->focus_style}; + for (auto style : hover_focus) { + style->set_background_color(recompui::theme::color::Transparent); + } + } + + recompui::Element *menu_container = menu->get_menu_container(); + menu_container->set_width(1440); + menu_container->unset_left(); + menu_container->set_top(banjo::launcher_options_top_offset); + menu_container->set_bottom(-banjo::launcher_options_top_offset); + menu_container->set_right(50, recompui::Unit::Percent); + menu_container->set_translate_2D(50.0f, 0.0f, recompui::Unit::Percent); + + game_options_menu->unset_left(); + game_options_menu->set_bottom(50.0f, recompui::Unit::Percent); + game_options_menu->set_translate_2D(0.0f, 50.0f, recompui::Unit::Percent); + game_options_menu->set_right(banjo::launcher_options_right_position_start); + + menu->remove_default_title(); + + recompui::ContextId context = recompui::get_current_context(); + recompui::Label* title_label = context.create_element(menu, "Banjo: Recompiled", recompui::theme::Typography::Header1); + title_label->set_position(recompui::Position::Absolute); + title_label->set_top(banjo::launcher_options_title_offset); + title_label->set_right(50.0f, recompui::Unit::Percent); + title_label->set_translate_2D(50.0f, 0.0f, recompui::Unit::Percent); + title_label->set_color(recompui::theme::color::White); + + banjo::launcher_animation_setup(menu, title_label); } #define REGISTER_FUNC(name) recomp::overlays::register_base_export(#name, name) @@ -682,6 +707,7 @@ int main(int argc, char** argv) { banjo::init_config(); recompui::register_launcher_init_callback(on_launcher_init); + recompui::register_launcher_update_callback(banjo::launcher_animation_update); recomp::rsp::callbacks_t rsp_callbacks{ .get_rsp_microcode = get_rsp_microcode,