Reproduce arcade menu transitions

This commit is contained in:
2026-08-15 16:25:11 +02:00
parent 3090555ab3
commit 72c59bf410
2 changed files with 448 additions and 66 deletions
+434 -60
View File
@@ -17,6 +17,7 @@
#include <cstdint> #include <cstdint>
#include <cstdlib> #include <cstdlib>
#include <fstream> #include <fstream>
#include <initializer_list>
#include <iostream> #include <iostream>
#include <random> #include <random>
#include <unordered_map> #include <unordered_map>
@@ -45,6 +46,67 @@ enum class SelectTask {
Difficulty, Difficulty,
}; };
enum class MenuMotion {
MusicEnter,
Idle,
SongScroll,
SortChange,
MusicDecision,
MusicExit,
ModeEnter,
DifficultyChange,
ModeExitBack,
ModeDecision,
ModeExitConfirm,
};
constexpr float kMenuEnterSeconds = 0.375f;
constexpr float kMenuExitSeconds = 0.375f;
constexpr float kSongScrollSeconds = 0.125f;
constexpr float kMenuChangeSeconds = 0.15f;
constexpr float kDecisionSeconds = 0.25f;
constexpr std::array<int, 12> kCarouselOffsets{
-5, -4, -3, -2, -1, 0, 0, 1, 2, 3, 4, 5
};
constexpr std::array<float, 12> kCarouselRowY{
175, 228, 281, 334, 387, 440, 701, 754, 807, 860, 913, 966
};
constexpr std::array<float, 12> kCarouselIndexY{
189, 242, 295, 348, 401, 454, 715, 768, 821, 874, 927, 980
};
constexpr std::array<float, 12> kCarouselCentersY{
209, 262, 315, 368, 421, 474, 735, 788, 841, 894, 947, 1000
};
constexpr std::array<float, 12> kCarouselAlpha{
0.0f, 0.7f, 0.8f, 0.9f, 1.0f, 0.0f,
0.0f, 1.0f, 0.9f, 0.8f, 0.7f, 0.0f
};
float saturate(float value) {
return std::clamp(value, 0.0f, 1.0f);
}
float lerp(float from, float to, float amount) {
return from + (to - from) * amount;
}
int carouselSlotForOffset(int offset, bool lowerZero = false) {
if (offset < -5 || offset > 5) return -1;
if (offset < 0) return offset + 5;
if (offset > 0) return offset + 6;
return lowerZero ? 6 : 5;
}
int wrappedCarouselDistance(size_t origin, size_t target, size_t count) {
if (count == 0) return 0;
int distance = static_cast<int>(target) - static_cast<int>(origin);
const int wrappedCount = static_cast<int>(count);
if (distance > wrappedCount / 2) distance -= wrappedCount;
if (distance < -wrappedCount / 2) distance += wrappedCount;
return distance;
}
struct Song { struct Song {
gc::StageCatalogEntry catalog; gc::StageCatalogEntry catalog;
fs::path menuTexture; fs::path menuTexture;
@@ -433,9 +495,16 @@ public:
const auto found = textures_.find(draw.imageSymbol); const auto found = textures_.find(draw.imageSymbol);
if (found == textures_.end()) continue; if (found == textures_.end()) continue;
auto corners = draw.corners; auto corners = draw.corners;
float symbolDx = 0.0f;
float symbolDy = 0.0f;
const auto translated = symbolTranslations_.find(draw.imageSymbol);
if (translated != symbolTranslations_.end()) {
symbolDx = translated->second[0];
symbolDy = translated->second[1];
}
for (auto& corner : corners) { for (auto& corner : corners) {
corner[0] += dx; corner[0] += dx + symbolDx;
corner[1] += dy; corner[1] += dy + symbolDy;
} }
ui.textureQuad(found->second, corners, ui.textureQuad(found->second, corners,
glm::vec4(draw.color[0], draw.color[1], draw.color[2], glm::vec4(draw.color[0], draw.color[1], draw.color[2],
@@ -446,13 +515,7 @@ public:
bool ready() const { return !textures_.empty(); } bool ready() const { return !textures_.empty(); }
void translateSymbol(const std::string& symbol, float dx, float dy) { void translateSymbol(const std::string& symbol, float dx, float dy) {
for (gc::RvbImageDraw& draw : draws_) { symbolTranslations_[symbol] = {dx, dy};
if (draw.imageSymbol != symbol) continue;
for (auto& corner : draw.corners) {
corner[0] += dx;
corner[1] += dy;
}
}
} }
void clear() { void clear() {
@@ -460,6 +523,7 @@ public:
if (texture.id != 0) glDeleteTextures(1, &texture.id); if (texture.id != 0) glDeleteTextures(1, &texture.id);
} }
textures_.clear(); textures_.clear();
symbolTranslations_.clear();
draws_.clear(); draws_.clear();
rvbBytes_.clear(); rvbBytes_.clear();
mtxBytes_.clear(); mtxBytes_.clear();
@@ -488,6 +552,7 @@ private:
} }
std::vector<gc::RvbImageDraw> draws_; std::vector<gc::RvbImageDraw> draws_;
std::unordered_map<std::string, DdsTexture> textures_; std::unordered_map<std::string, DdsTexture> textures_;
std::unordered_map<std::string, std::array<float, 2>> symbolTranslations_;
std::vector<uint8_t> rvbBytes_; std::vector<uint8_t> rvbBytes_;
std::vector<uint8_t> mtxBytes_; std::vector<uint8_t> mtxBytes_;
gc::RvbScene scene_; gc::RvbScene scene_;
@@ -563,6 +628,22 @@ gc::RvbSnapshotState selectMusicSceneState(const Song* song = nullptr,
return state; return state;
} }
gc::RvbSnapshotState selectMusicExitSceneState(const Song* song, int activeSort) {
gc::RvbSnapshotState state = selectMusicSceneState(song, activeSort);
auto& frame = state.frameByPath;
frame["/"] = "jf_slmusic_end";
frame["/imc_navi"] = "tg_navi_end";
frame["/imc_title"] = "lf_title_selectmusic_fo";
frame["/imc_focus"] = "jf_focus_fd_out_m";
return state;
}
gc::RvbSnapshotState selectMusicDecisionSceneState(const Song* song, int activeSort) {
gc::RvbSnapshotState state = selectMusicSceneState(song, activeSort);
state.frameByPath["/imc_focus"] = "jf_decision";
return state;
}
gc::RvbSnapshotState selectModeSceneState(const Song* song = nullptr, int difficulty = 0) { gc::RvbSnapshotState selectModeSceneState(const Song* song = nullptr, int difficulty = 0) {
gc::RvbSnapshotState state; gc::RvbSnapshotState state;
auto& frame = state.frameByPath; auto& frame = state.frameByPath;
@@ -604,6 +685,16 @@ gc::RvbSnapshotState selectModeSceneState(const Song* song = nullptr, int diffic
return state; return state;
} }
gc::RvbSnapshotState selectModeExitSceneState(const Song* song, int difficulty) {
gc::RvbSnapshotState state = selectModeSceneState(song, difficulty);
auto& frame = state.frameByPath;
frame["/"] = "jf_slmode_end";
frame["/imc_navi"] = "tg_navi_end";
frame["/imc_title"] = "jf_title_mode_fo";
frame["/imc_slmode"] = "jf_mode_fo";
return state;
}
gc::RvbSnapshotState commonSelectSceneState() { gc::RvbSnapshotState commonSelectSceneState() {
gc::RvbSnapshotState state; gc::RvbSnapshotState state;
auto& frame = state.frameByPath; auto& frame = state.frameByPath;
@@ -623,6 +714,15 @@ gc::RvbSnapshotState commonSelectSceneState() {
return state; return state;
} }
gc::RvbSnapshotState commonSelectExitSceneState() {
gc::RvbSnapshotState state = commonSelectSceneState();
auto& frame = state.frameByPath;
frame["/imc_ctrl_anim"] = "jf_ctrl_end";
frame["/imc_head"] = "jf_head_fo";
frame["/imc_foot"] = "jf_foot_fo";
return state;
}
gc::RvbSnapshotState navigatorSelectSceneState() { gc::RvbSnapshotState navigatorSelectSceneState() {
gc::RvbSnapshotState state; gc::RvbSnapshotState state;
auto& frame = state.frameByPath; auto& frame = state.frameByPath;
@@ -633,6 +733,21 @@ gc::RvbSnapshotState navigatorSelectSceneState() {
return state; return state;
} }
gc::RvbSnapshotState navigatorSelectExitSceneState() {
gc::RvbSnapshotState state = navigatorSelectSceneState();
state.frameByPath["/"] = "jf_ope_end";
return state;
}
gc::RvbSnapshotState animateRvbState(
gc::RvbSnapshotState state, uint32_t animationFrame,
std::initializer_list<const char*> paths) {
for (const char* path : paths) {
state.animationFrameByPath[path] = animationFrame;
}
return state;
}
int genreLabelRow(int genre) { int genreLabelRow(int genre) {
// s_j[_eng].dds is the executable-owned 256x256 genre-label atlas. // s_j[_eng].dds is the executable-owned 256x256 genre-label atlas.
// Each pseudo song ID 50000+n selects one 256x32 row. // Each pseudo song ID 50000+n selects one 256x32 row.
@@ -874,6 +989,14 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
rebuildVisible(); rebuildVisible();
size_t selection = 0; size_t selection = 0;
size_t previousSelection = selection;
int scrollDelta = 0;
MenuMotion motion = MenuMotion::MusicEnter;
Uint64 motionStartTicks = SDL_GetTicks();
const auto startMotion = [&](MenuMotion next) {
motion = next;
motionStartTicks = SDL_GetTicks();
};
int difficulty = firstPlayableDifficulty(songs[visible[selection]]); int difficulty = firstPlayableDifficulty(songs[visible[selection]]);
auto refreshOriginalScenes = [&] { auto refreshOriginalScenes = [&] {
if (visible.empty()) return; if (visible.empty()) return;
@@ -888,12 +1011,15 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
refreshOriginalScenes(); refreshOriginalScenes();
auto changeSong = [&](int delta) { auto changeSong = [&](int delta) {
if (visible.empty()) return; if (visible.empty()) return;
previousSelection = selection;
scrollDelta = delta;
const int count = static_cast<int>(visible.size()); const int count = static_cast<int>(visible.size());
int next = (static_cast<int>(selection) + delta) % count; int next = (static_cast<int>(selection) + delta) % count;
if (next < 0) next += count; if (next < 0) next += count;
selection = static_cast<size_t>(next); selection = static_cast<size_t>(next);
difficulty = firstPlayableDifficulty(songs[visible[selection]], difficulty); difficulty = firstPlayableDifficulty(songs[visible[selection]], difficulty);
refreshOriginalScenes(); refreshOriginalScenes();
startMotion(MenuMotion::SongScroll);
}; };
auto changeSort = [&](int delta) { auto changeSort = [&](int delta) {
const size_t selectedSong = visible.empty() ? 0 : visible[selection]; const size_t selectedSong = visible.empty() ? 0 : visible[selection];
@@ -908,6 +1034,7 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
difficulty = firstPlayableDifficulty(songs[visible[selection]], difficulty); difficulty = firstPlayableDifficulty(songs[visible[selection]], difficulty);
} }
refreshOriginalScenes(); refreshOriginalScenes();
startMotion(MenuMotion::SortChange);
}; };
auto changeDifficulty = [&](int delta) { auto changeDifficulty = [&](int delta) {
const Song& song = songs[visible[selection]]; const Song& song = songs[visible[selection]];
@@ -917,6 +1044,7 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
if (!song.stages[next].empty()) { if (!song.stages[next].empty()) {
difficulty = next; difficulty = next;
refreshOriginalScenes(); refreshOriginalScenes();
startMotion(MenuMotion::DifficultyChange);
return; return;
} }
} }
@@ -927,10 +1055,78 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
SelectTask task = SelectTask::Music; SelectTask task = SelectTask::Music;
bool capturedMusic = false; bool capturedMusic = false;
bool capturedDifficulty = false; bool capturedDifficulty = false;
bool capturedMusicDecision = false;
bool capturedMusicExit = false;
bool capturedModeEnter = false;
const char* capturePrefix = std::getenv("OPENROLLER_CAPTURE_PREFIX"); const char* capturePrefix = std::getenv("OPENROLLER_CAPTURE_PREFIX");
const bool captureBoth = std::getenv("OPENROLLER_CAPTURE_BOTH") != nullptr; const bool captureBoth = std::getenv("OPENROLLER_CAPTURE_BOTH") != nullptr;
const bool captureTransitions =
std::getenv("OPENROLLER_CAPTURE_TRANSITIONS") != nullptr;
const Uint64 menuStartTicks = SDL_GetTicks(); const Uint64 menuStartTicks = SDL_GetTicks();
while (running) { while (running) {
const Uint64 nowTicks = SDL_GetTicks();
const float motionSeconds = static_cast<float>(nowTicks - motionStartTicks) / 1000.0f;
switch (motion) {
case MenuMotion::MusicEnter:
if (motionSeconds >= kMenuEnterSeconds) {
motion = MenuMotion::Idle;
refreshOriginalScenes();
originalCommon.setState(commonSelectSceneState());
originalNavigator.setState(navigatorSelectSceneState());
}
break;
case MenuMotion::SongScroll:
if (motionSeconds >= kSongScrollSeconds) {
motion = MenuMotion::Idle;
refreshOriginalScenes();
}
break;
case MenuMotion::SortChange:
case MenuMotion::DifficultyChange:
if (motionSeconds >= kMenuChangeSeconds) {
motion = MenuMotion::Idle;
refreshOriginalScenes();
}
break;
case MenuMotion::MusicDecision:
if (motionSeconds >= kDecisionSeconds) {
startMotion(MenuMotion::MusicExit);
}
break;
case MenuMotion::MusicExit:
if (motionSeconds >= kMenuExitSeconds) {
task = SelectTask::Difficulty;
startMotion(MenuMotion::ModeEnter);
}
break;
case MenuMotion::ModeEnter:
if (motionSeconds >= kMenuEnterSeconds) {
motion = MenuMotion::Idle;
refreshOriginalScenes();
originalCommon.setState(commonSelectSceneState());
originalNavigator.setState(navigatorSelectSceneState());
}
break;
case MenuMotion::ModeExitBack:
if (motionSeconds >= kMenuExitSeconds) {
task = SelectTask::Music;
startMotion(MenuMotion::MusicEnter);
}
break;
case MenuMotion::ModeDecision:
if (motionSeconds >= kDecisionSeconds) {
startMotion(MenuMotion::ModeExitConfirm);
}
break;
case MenuMotion::ModeExitConfirm:
if (motionSeconds >= kMenuExitSeconds) {
confirmed = true;
running = false;
}
break;
case MenuMotion::Idle:
break;
}
SDL_Event event; SDL_Event event;
while (SDL_PollEvent(&event)) { while (SDL_PollEvent(&event)) {
if (event.type == SDL_EVENT_QUIT) running = false; if (event.type == SDL_EVENT_QUIT) running = false;
@@ -940,6 +1136,7 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
SDL_SetWindowTitle(window, "OpenRoller"); SDL_SetWindowTitle(window, "OpenRoller");
continue; continue;
} }
if (motion != MenuMotion::Idle) continue;
if (task == SelectTask::Music) { if (task == SelectTask::Music) {
switch (event.key.key) { switch (event.key.key) {
case SDLK_ESCAPE: running = false; break; case SDLK_ESCAPE: running = false; break;
@@ -950,14 +1147,14 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
case SDLK_Q: changeSort(-1); break; case SDLK_Q: changeSort(-1); break;
case SDLK_E: case SDLK_TAB: changeSort(1); break; case SDLK_E: case SDLK_TAB: changeSort(1); break;
case SDLK_RETURN: case SDLK_SPACE: case SDLK_RETURN: case SDLK_SPACE:
task = SelectTask::Difficulty; startMotion(MenuMotion::MusicDecision);
break; break;
default: break; default: break;
} }
} else { } else {
switch (event.key.key) { switch (event.key.key) {
case SDLK_ESCAPE: case SDLK_ESCAPE:
task = SelectTask::Music; startMotion(MenuMotion::ModeExitBack);
break; break;
case SDLK_UP: case SDLK_W: case SDLK_LEFT: case SDLK_A: case SDLK_UP: case SDLK_W: case SDLK_LEFT: case SDLK_A:
changeDifficulty(-1); changeDifficulty(-1);
@@ -966,8 +1163,7 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
changeDifficulty(1); changeDifficulty(1);
break; break;
case SDLK_RETURN: case SDLK_SPACE: case SDLK_RETURN: case SDLK_SPACE:
confirmed = true; startMotion(MenuMotion::ModeDecision);
running = false;
break; break;
default: break; default: break;
} }
@@ -976,6 +1172,86 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
if (visible.empty()) continue; if (visible.empty()) continue;
const Song& selected = songs[visible[selection]]; const Song& selected = songs[visible[selection]];
const float activeMotionSeconds =
static_cast<float>(SDL_GetTicks() - motionStartTicks) / 1000.0f;
const uint32_t rvbFrame = static_cast<uint32_t>(activeMotionSeconds * 60.0f);
if (motion == MenuMotion::MusicEnter) {
originalMusic.setState(animateRvbState(
selectMusicSceneState(&selected, static_cast<int>(sortSlot + 1)), rvbFrame,
{"/", "/imc_navi", "/imc_title", "/imc_focus",
"/imc_focus/imc_fd_jacket_anim", "/imc_sort"}));
originalCommon.setState(animateRvbState(
commonSelectSceneState(), rvbFrame,
{"/imc_ctrl_anim", "/imc_head", "/imc_foot"}));
originalNavigator.setState(animateRvbState(
navigatorSelectSceneState(), rvbFrame, {"/", "/imc_ope"}));
} else if (motion == MenuMotion::SongScroll) {
gc::RvbSnapshotState state =
selectMusicSceneState(&selected, static_cast<int>(sortSlot + 1));
state.frameByPath["/imc_focus"] = "jf_focus_fd_move";
originalMusic.setState(animateRvbState(
std::move(state), rvbFrame, {"/imc_focus"}));
} else if (motion == MenuMotion::SortChange) {
originalMusic.setState(animateRvbState(
selectMusicSceneState(&selected, static_cast<int>(sortSlot + 1)), rvbFrame,
{"/imc_sort"}));
} else if (motion == MenuMotion::MusicDecision) {
originalMusic.setState(animateRvbState(
selectMusicDecisionSceneState(&selected, static_cast<int>(sortSlot + 1)),
rvbFrame, {"/imc_focus"}));
} else if (motion == MenuMotion::MusicExit) {
originalMusic.setState(animateRvbState(
selectMusicExitSceneState(&selected, static_cast<int>(sortSlot + 1)), rvbFrame,
{"/", "/imc_navi", "/imc_title", "/imc_focus"}));
originalCommon.setState(animateRvbState(
commonSelectExitSceneState(), rvbFrame,
{"/imc_ctrl_anim", "/imc_head", "/imc_foot"}));
originalNavigator.setState(animateRvbState(
navigatorSelectExitSceneState(), rvbFrame, {"/"}));
} else if (motion == MenuMotion::ModeEnter) {
originalDifficulty.setState(animateRvbState(
selectModeSceneState(&selected, difficulty), rvbFrame,
{"/", "/imc_navi", "/imc_title", "/imc_slmode", "/imc_tab"}));
originalCommon.setState(animateRvbState(
commonSelectSceneState(), rvbFrame,
{"/imc_ctrl_anim", "/imc_head", "/imc_foot"}));
originalNavigator.setState(animateRvbState(
navigatorSelectSceneState(), rvbFrame, {"/", "/imc_ope"}));
} else if (motion == MenuMotion::DifficultyChange) {
originalDifficulty.setState(animateRvbState(
selectModeSceneState(&selected, difficulty), rvbFrame,
{"/imc_slmode/imc_mode/imc_m_smpl",
"/imc_slmode/imc_mode/imc_m_nrml",
"/imc_slmode/imc_mode/imc_m_hard",
"/imc_slmode/imc_mode/imc_m_extra",
"/imc_slmode/imc_mode/imc_tri_set_exoff",
"/imc_slmode/imc_mode/imc_tri_set_exon",
"/imc_tab/imc_info_m_tab"}));
} else if (motion == MenuMotion::ModeDecision) {
gc::RvbSnapshotState state = selectModeSceneState(&selected, difficulty);
static constexpr std::array<const char*, 4> focusPaths{
"/imc_slmode/imc_mode/imc_m_smpl/imc_m_focus",
"/imc_slmode/imc_mode/imc_m_nrml/imc_m_focus",
"/imc_slmode/imc_mode/imc_m_hard/imc_m_focus",
"/imc_slmode/imc_mode/imc_m_extra/imc_m_focus"
};
const char* focusPath = focusPaths[static_cast<size_t>(difficulty)];
state.frameByPath[focusPath] = "jf_mode_decision";
originalDifficulty.setState(animateRvbState(
std::move(state), rvbFrame, {focusPath}));
} else if (motion == MenuMotion::ModeExitBack ||
motion == MenuMotion::ModeExitConfirm) {
gc::RvbSnapshotState state =
selectModeExitSceneState(&selected, difficulty);
originalDifficulty.setState(animateRvbState(
std::move(state), rvbFrame,
{"/", "/imc_navi", "/imc_title", "/imc_slmode"}));
originalCommon.setState(animateRvbState(
commonSelectExitSceneState(), rvbFrame,
{"/imc_ctrl_anim", "/imc_head", "/imc_foot"}));
originalNavigator.setState(animateRvbState(
navigatorSelectExitSceneState(), rvbFrame, {"/"}));
}
SDL_SetWindowTitle(window, ("OpenRoller - " + selected.catalog.imageKey).c_str()); SDL_SetWindowTitle(window, ("OpenRoller - " + selected.catalog.imageKey).c_str());
int pixelWidth = 0, pixelHeight = 0; int pixelWidth = 0, pixelHeight = 0;
SDL_GetWindowSizeInPixels(window, &pixelWidth, &pixelHeight); SDL_GetWindowSizeInPixels(window, &pixelWidth, &pixelHeight);
@@ -1020,39 +1296,90 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
if (logical < 0) logical += count; if (logical < 0) logical += count;
return carousel[static_cast<size_t>(logical)]; return carousel[static_cast<size_t>(logical)];
}; };
size_t previousCarouselEntry = selectedCarouselEntry;
if (motion == MenuMotion::SongScroll && previousSelection < visible.size()) {
const size_t previousSongIndex = visible[previousSelection];
const auto previousEntry = std::find_if(
carousel.begin(), carousel.end(), [previousSongIndex](const CarouselEntry& entry) {
return !entry.category && entry.songIndex == previousSongIndex;
});
if (previousEntry != carousel.end()) {
previousCarouselEntry = static_cast<size_t>(
std::distance(carousel.begin(), previousEntry));
}
}
const float scrollProgress = motion == MenuMotion::SongScroll
? saturate(activeMotionSeconds / kSongScrollSeconds) : 1.0f;
float musicLayerDx = 0.0f;
float musicLayerAlpha = 1.0f;
if (motion == MenuMotion::MusicEnter) {
const float progress = saturate(activeMotionSeconds / kMenuEnterSeconds);
const float remaining = 1.0f - progress;
musicLayerDx = -static_cast<float>(kUiWidth) * remaining * remaining * remaining * remaining;
musicLayerAlpha = progress;
} else if (motion == MenuMotion::MusicExit) {
const float progress = saturate(activeMotionSeconds / kMenuExitSeconds);
const float eased = progress * progress * progress * progress;
musicLayerDx = static_cast<float>(kUiWidth) * eased;
musicLayerAlpha = 1.0f - progress;
}
const auto animatedSlot = [&](size_t targetSlot) {
std::array<float, 4> geometry{
kCarouselRowY[targetSlot], kCarouselIndexY[targetSlot],
kCarouselCentersY[targetSlot], kCarouselAlpha[targetSlot]
};
if (motion != MenuMotion::SongScroll || carousel.empty()) return geometry;
int targetLogical = static_cast<int>(selectedCarouselEntry) +
kCarouselOffsets[targetSlot];
const int count = static_cast<int>(carousel.size());
targetLogical %= count;
if (targetLogical < 0) targetLogical += count;
const int sourceOffset = wrappedCarouselDistance(
previousCarouselEntry, static_cast<size_t>(targetLogical), carousel.size());
const int sourceSlot = carouselSlotForOffset(
sourceOffset, sourceOffset == 0 && scrollDelta < 0);
if (sourceSlot >= 0) {
geometry[0] = lerp(kCarouselRowY[static_cast<size_t>(sourceSlot)],
geometry[0], scrollProgress);
geometry[1] = lerp(kCarouselIndexY[static_cast<size_t>(sourceSlot)],
geometry[1], scrollProgress);
geometry[2] = lerp(kCarouselCentersY[static_cast<size_t>(sourceSlot)],
geometry[2], scrollProgress);
geometry[3] = lerp(kCarouselAlpha[static_cast<size_t>(sourceSlot)],
geometry[3], scrollProgress);
} else {
geometry[3] *= scrollProgress;
}
return geometry;
};
if (task == SelectTask::Music && if (task == SelectTask::Music &&
(originalMusicRow.ready() || originalMusicIndex.ready())) { (originalMusicRow.ready() || originalMusicIndex.ready())) {
// FUN_00447170 supplies the raw MovieClip translation, while // FUN_00447170 supplies the raw MovieClip translation, while
// FUN_00447620 is applied as opacity to these fixed-width rows. // FUN_00447620 is applied as opacity to these fixed-width rows.
// FUN_00446cb0 switches each slot between mc_music_link and // FUN_00446cb0 switches each slot between mc_music_link and
// mc_index_link for pseudo IDs >= 50000. // mc_index_link for pseudo IDs >= 50000.
static constexpr std::array<int, 12> offsets{ for (size_t slot = 0; slot < kCarouselRowY.size(); ++slot) {
-5, -4, -3, -2, -1, 0, 0, 1, 2, 3, 4, 5 if (carousel.empty()) continue;
}; if (kCarouselOffsets[slot] == 0 &&
static constexpr std::array<float, 12> rowY{ ((scrollDelta >= 0 && slot == 5) || (scrollDelta < 0 && slot == 6))) {
175, 228, 281, 334, 387, 440, 701, 754, 807, 860, 913, 966 continue;
}; }
static constexpr std::array<float, 12> indexY{ const auto geometry = animatedSlot(slot);
189, 242, 295, 348, 401, 454, 715, 768, 821, 874, 927, 980 const float alpha = geometry[3] * musicLayerAlpha;
}; if (alpha <= 0.0f) continue;
static constexpr std::array<float, 12> rowAlpha{ const CarouselEntry& entry = carouselAtOffset(kCarouselOffsets[slot]);
0.0f, 0.7f, 0.8f, 0.9f, 1.0f, 0.0f,
0.0f, 1.0f, 0.9f, 0.8f, 0.7f, 0.0f
};
for (size_t slot = 0; slot < rowY.size(); ++slot) {
if (rowAlpha[slot] <= 0.0f || carousel.empty()) continue;
const CarouselEntry& entry = carouselAtOffset(offsets[slot]);
if (!entry.category && originalMusicRow.ready()) { if (!entry.category && originalMusicRow.ready()) {
originalMusicRow.render(ui, 8.0f, rowY[slot], rowAlpha[slot]); originalMusicRow.render(ui, 8.0f + musicLayerDx, geometry[0], alpha);
} else if (entry.category && originalMusicIndex.ready()) { } else if (entry.category && originalMusicIndex.ready()) {
originalMusicIndex.render(ui, 88.0f, indexY[slot], rowAlpha[slot]); originalMusicIndex.render(ui, 88.0f + musicLayerDx, geometry[1], alpha);
if (genreLabels.id != 0) { if (genreLabels.id != 0) {
const float sourceY = 32.0f * genreLabelRow(entry.genre); const float sourceY = 32.0f * genreLabelRow(entry.genre);
// FUN_005aca40: index X/Y plus 53 and 2; FUN_005b3fc0 // FUN_005aca40: index X/Y plus 53 and 2; FUN_005b3fc0
// copies a 256x32 cell from s_j_eng.dds. // copies a 256x32 cell from s_j_eng.dds.
ui.texture(genreLabels, 141.0f, indexY[slot] + 2.0f, ui.texture(genreLabels, 141.0f + musicLayerDx, geometry[1] + 2.0f,
256.0f, 32.0f, 0.0f, sourceY, 256.0f, 32.0f, 256.0f, 32.0f, 0.0f, sourceY, 256.0f, 32.0f,
glm::vec4(1.0f, 1.0f, 1.0f, rowAlpha[slot])); glm::vec4(1.0f, 1.0f, 1.0f, alpha));
} }
} }
} }
@@ -1068,6 +1395,18 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
selectedTexture = textures.emplace(selectedSongIndex, loaded).first; selectedTexture = textures.emplace(selectedSongIndex, loaded).first;
} }
} }
auto previousTexture = textures.end();
if (motion == MenuMotion::SongScroll && previousSelection < visible.size()) {
const size_t previousSongIndex = visible[previousSelection];
previousTexture = textures.find(previousSongIndex);
if (previousTexture == textures.end()) {
DdsTexture loaded;
if (loadSongMenuTexture(songs[previousSongIndex].menuTexture.string(), loaded,
nullptr)) {
previousTexture = textures.emplace(previousSongIndex, loaded).first;
}
}
}
if (!usingOriginal) { if (!usingOriginal) {
const glm::vec4 orange{1.0f, 0.31f, 0.08f, 1.0f}; const glm::vec4 orange{1.0f, 0.31f, 0.08f, 1.0f};
@@ -1165,26 +1504,35 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
if (task == SelectTask::Music) { if (task == SelectTask::Music) {
// FUN_005aca40 + FUN_005b34f0: fixed focus fragments from the // FUN_005aca40 + FUN_005b34f0: fixed focus fragments from the
// selected song's original 512x256 menu atlas. // selected song's original 512x256 menu atlas.
ui.texture(atlas, 39, 497, 196, 196, 1, 1, 196, 196); const auto renderFocus = [&](const DdsTexture& focusAtlas, float alpha) {
ui.texture(atlas, 251, 467, 374, 34, 0, 197, 374, 34); const glm::vec4 tint{1.0f, 1.0f, 1.0f, alpha * musicLayerAlpha};
ui.texture(atlas, 262, 500, 374, 24, 0, 232, 374, 24); ui.texture(focusAtlas, 39 + musicLayerDx, 497, 196, 196,
ui.texture(atlas, 262, 522, 314, 16, 198, 180, 314, 16); 1, 1, 196, 196, tint);
ui.texture(focusAtlas, 251 + musicLayerDx, 467, 374, 34,
0, 197, 374, 34, tint);
ui.texture(focusAtlas, 262 + musicLayerDx, 500, 374, 24,
0, 232, 374, 24, tint);
ui.texture(focusAtlas, 262 + musicLayerDx, 522, 314, 16,
198, 180, 314, 16, tint);
};
if (motion == MenuMotion::SongScroll && previousTexture != textures.end()) {
renderFocus(previousTexture->second, 1.0f - scrollProgress);
renderFocus(atlas, scrollProgress);
} else {
renderFocus(atlas, 1.0f);
}
// FUN_00447170/FUN_00447620: the twelve real carousel slots. // FUN_00447170/FUN_00447620: the twelve real carousel slots.
static constexpr std::array<int, 12> offsets{ for (size_t slot = 0; slot < kCarouselOffsets.size(); ++slot) {
-5, -4, -3, -2, -1, 0, 0, 1, 2, 3, 4, 5 if (kCarouselOffsets[slot] == 0 &&
}; ((scrollDelta >= 0 && slot == 5) || (scrollDelta < 0 && slot == 6))) {
static constexpr std::array<float, 12> centersY{ continue;
209, 262, 315, 368, 421, 474, 735, 788, 841, 894, 947, 1000 }
}; const auto geometry = animatedSlot(slot);
static constexpr std::array<float, 12> scales{ const float scale = geometry[3];
0.0f, 0.7f, 0.8f, 0.9f, 1.0f, 0.0f, const float alpha = scale * musicLayerAlpha;
0.0f, 1.0f, 0.9f, 0.8f, 0.7f, 0.0f
};
for (size_t slot = 0; slot < offsets.size(); ++slot) {
const float scale = scales[slot];
if (scale <= 0.0f || carousel.empty()) continue; if (scale <= 0.0f || carousel.empty()) continue;
const CarouselEntry& entry = carouselAtOffset(offsets[slot]); const CarouselEntry& entry = carouselAtOffset(kCarouselOffsets[slot]);
if (entry.category) continue; if (entry.category) continue;
const size_t songIndex = entry.songIndex; const size_t songIndex = entry.songIndex;
auto texture = textures.find(songIndex); auto texture = textures.find(songIndex);
@@ -1197,19 +1545,28 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
if (texture == textures.end()) continue; if (texture == textures.end()) continue;
const float width = 374.0f * scale; const float width = 374.0f * scale;
const float height = 34.0f * scale; const float height = 34.0f * scale;
ui.texture(texture->second, 201.0f - width * 0.5f, ui.texture(texture->second, 201.0f + musicLayerDx - width * 0.5f,
centersY[slot] - height * 0.5f, width, height, geometry[2] - height * 0.5f, width, height,
0, 197, 374, 34); 0, 197, 374, 34,
glm::vec4(1.0f, 1.0f, 1.0f, alpha));
} }
} else { } else {
// Fixed selected-song fragments in CDifficultyTask's renderer // Fixed selected-song fragments in CDifficultyTask's renderer
// (FUN_005be2d0), using its executable constants. // (FUN_005be2d0), using its executable constants.
// FUN_005b33a0 treats the executable constants as sprite // FUN_005b33a0 treats the executable constants as sprite
// centres and subtracts half of the scaled source size. // centres and subtracts half of the scaled source size.
ui.texture(atlas, 105, 167, 98, 98, 1, 1, 196, 196); float modeAlpha = 1.0f;
ui.texture(atlas, 209, 178, 374, 34, 0, 197, 374, 34); if (motion == MenuMotion::ModeEnter) {
ui.texture(atlas, 220, 214, 374, 24, 0, 232, 374, 24); modeAlpha = saturate(activeMotionSeconds / kMenuEnterSeconds);
ui.texture(atlas, 220, 239, 314, 16, 198, 180, 314, 16); } else if (motion == MenuMotion::ModeExitBack ||
motion == MenuMotion::ModeExitConfirm) {
modeAlpha = 1.0f - saturate(activeMotionSeconds / kMenuExitSeconds);
}
const glm::vec4 tint{1.0f, 1.0f, 1.0f, modeAlpha};
ui.texture(atlas, 105, 167, 98, 98, 1, 1, 196, 196, tint);
ui.texture(atlas, 209, 178, 374, 34, 0, 197, 374, 34, tint);
ui.texture(atlas, 220, 214, 374, 24, 0, 232, 374, 24, tint);
ui.texture(atlas, 220, 239, 314, 16, 198, 180, 314, 16, tint);
} }
} }
// The navigator and common HUD are later compositing passes in // The navigator and common HUD are later compositing passes in
@@ -1218,11 +1575,28 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
if (originalCommon.ready()) originalCommon.render(ui); if (originalCommon.ready()) originalCommon.render(ui);
ui.flush(); ui.flush();
if (capturePrefix && task == SelectTask::Music && !capturedMusic) { if (capturePrefix && captureTransitions && !capturedMusicDecision &&
motion == MenuMotion::MusicDecision &&
activeMotionSeconds >= kDecisionSeconds * 0.5f) {
captureFrameForReverse(capturePrefix, "music-decision", pixelWidth, pixelHeight);
capturedMusicDecision = true;
} else if (capturePrefix && captureTransitions && !capturedMusicExit &&
motion == MenuMotion::MusicExit &&
activeMotionSeconds >= kMenuExitSeconds * 0.5f) {
captureFrameForReverse(capturePrefix, "music-exit", pixelWidth, pixelHeight);
capturedMusicExit = true;
} else if (capturePrefix && captureTransitions && !capturedModeEnter &&
motion == MenuMotion::ModeEnter &&
activeMotionSeconds >= kMenuEnterSeconds * 0.5f) {
captureFrameForReverse(capturePrefix, "mode-enter", pixelWidth, pixelHeight);
capturedModeEnter = true;
} else if (capturePrefix && motion == MenuMotion::Idle &&
task == SelectTask::Music && !capturedMusic) {
captureFrameForReverse(capturePrefix, "music", pixelWidth, pixelHeight); captureFrameForReverse(capturePrefix, "music", pixelWidth, pixelHeight);
capturedMusic = true; capturedMusic = true;
if (captureBoth) task = SelectTask::Difficulty; if (captureBoth) startMotion(MenuMotion::MusicDecision);
} else if (capturePrefix && task == SelectTask::Difficulty && !capturedDifficulty) { } else if (capturePrefix && motion == MenuMotion::Idle &&
task == SelectTask::Difficulty && !capturedDifficulty) {
captureFrameForReverse(capturePrefix, "difficulty", pixelWidth, pixelHeight); captureFrameForReverse(capturePrefix, "difficulty", pixelWidth, pixelHeight);
capturedDifficulty = true; capturedDifficulty = true;
} }
+14 -6
View File
@@ -90,9 +90,11 @@ list deltas rather than complete scenes:
- `COLT` carries RGBA multiplication, including authored visibility fades; - `COLT` carries RGBA multiplication, including authored visibility fades;
- `ASRC` contains the exported `play();`, `stop();`, and target actions. - `ASRC` contains the exported `play();`, `stop();`, and target actions.
`gc::BuildRvbSnapshot` now accumulates those deltas through a selected label `gc::BuildRvbSnapshot` accumulates those deltas through a selected label. Its
and advances `play()` entry frames to their following `stop()` frame. This is snapshot state can either advance a `play()` entry to the following `stop()`
why hidden templates and transition masks no longer appear together. or request a relative animation frame globally or per MovieClip path. Per-path
evaluation is required here because the selected transition must advance while
unrelated child loops remain at their authored steady frames.
MTX starts with `MTX\0`; each payload is a DDS whose first dword was replaced MTX starts with `MTX\0`; each payload is a DDS whose first dword was replaced
by the container. Restoring `DDS ` yields a standard DDS. RVB `ImageN` maps to by the container. Restoring `DDS ` yields a standard DDS. RVB `ImageN` maps to
@@ -137,9 +139,15 @@ actual formulas and spacings:
- `(7 - count) * 0.5 * 68`, with the row anchored at `426 + 16`; - `(7 - count) * 0.5 * 68`, with the row anchored at `426 + 16`;
- `(9 - count) * 0.5 * 52`, with the row anchored at `433 + 16`. - `(9 - count) * 0.5 * 52`, with the row anchored at `433 + 16`.
The remaining visual work is outside this recovered static scene snapshot: `FUN_00447170` also supplies the transition timing used by the executable. Song
the executable-owned player/status HUD in the blank upper band, continuous slot changes interpolate linearly over `0.125` seconds. The list enters/exits
timeline interpolation, and the exact transition timing between task states. over `0.375` seconds and shifts by one viewport width using exponent `4` easing.
The row plates retain their full dimensions; their opacity and the title scale
use the slot values `0.7`, `0.8`, `0.9`, and `1.0`. The desktop selector now
combines these executable-owned values with frame-based RVB entry, exit, sort,
focus, difficulty-change, and decision states. Confirmation is a distinct
`jf_decision`/`jf_mode_decision` phase before the task exit timeline; collapsing
both phases is why an immediate screen switch does not resemble the cabinet.
## Common and navigator layers ## Common and navigator layers