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,