Merge branch 'spark' of gitea.fishrungames.com:salmon-engine-projects/space-game001 into spark

This commit is contained in:
Vladislav Khorev 2026-01-12 09:44:54 +03:00
commit d610f1dea1
5 changed files with 537 additions and 40 deletions

Binary file not shown.

View File

@ -31,9 +31,25 @@
"y": 300, "y": 300,
"width": 200, "width": 200,
"height": 50, "height": 50,
"animations": {
"buttonsExit": {
"repeat": false,
"steps": [
{
"type": "move",
"to": [
-400,
0
],
"duration": 1.0,
"easing": "easein"
}
]
}
},
"textures": { "textures": {
"normal": "./resources/button.png", "normal": "./resources/button.png",
"hover": "./resources/button.png", "hover": "./resources/sand.png",
"pressed": "./resources/button.png" "pressed": "./resources/button.png"
} }
}, },
@ -44,9 +60,29 @@
"y": 200, "y": 200,
"width": 200, "width": 200,
"height": 50, "height": 50,
"animations": {
"buttonsExit": {
"repeat": false,
"steps": [
{
"type": "wait",
"duration": 0.5
},
{
"type": "move",
"to": [
-400,
0
],
"duration": 1.0,
"easing": "easein"
}
]
}
},
"textures": { "textures": {
"normal": "./resources/sand.png", "normal": "./resources/sand.png",
"hover": "./resources/sand.png", "hover": "./resources/button.png",
"pressed": "./resources/sand.png" "pressed": "./resources/sand.png"
} }
}, },
@ -57,9 +93,43 @@
"y": 100, "y": 100,
"width": 200, "width": 200,
"height": 50, "height": 50,
"animations": {
"buttonsExit": {
"repeat": false,
"steps": [
{
"type": "wait",
"duration": 1.0
},
{
"type": "move",
"to": [
-400,
0
],
"duration": 1.0,
"easing": "easein"
}
]
},
"bgScroll": {
"repeat": true,
"steps": [
{
"type": "move",
"to": [
1280,
0
],
"duration": 5.0,
"easing": "linear"
}
]
}
},
"textures": { "textures": {
"normal": "./resources/rock.png", "normal": "./resources/rock.png",
"hover": "./resources/rock.png", "hover": "./resources/button.png",
"pressed": "./resources/rock.png" "pressed": "./resources/rock.png"
} }
} }

View File

@ -18,7 +18,8 @@ namespace ZL
const char* CONST_ZIP_FILE = "../assets.zip"; const char* CONST_ZIP_FILE = "../assets.zip";
#endif #endif
static bool g_exitBgAnimating = false;
Eigen::Quaternionf generateRandomQuaternion(std::mt19937& gen) Eigen::Quaternionf generateRandomQuaternion(std::mt19937& gen)
{ {
@ -42,7 +43,7 @@ namespace ZL
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE; const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
const float MIN_COORD = -100.0f; const float MIN_COORD = -100.0f;
const float MAX_COORD = 100.0f; const float MAX_COORD = 100.0f;
const int MAX_ATTEMPTS = 1000; const int MAX_ATTEMPTS = 1000;
std::vector<BoxCoords> boxCoordsArr; std::vector<BoxCoords> boxCoordsArr;
std::random_device rd; std::random_device rd;
@ -65,7 +66,7 @@ namespace ZL
(float)distrib(gen) (float)distrib(gen)
); );
accepted = true; accepted = true;
for (const auto& existingBox : boxCoordsArr) for (const auto& existingBox : boxCoordsArr)
{ {
@ -76,7 +77,7 @@ namespace ZL
if (distanceSquared < MIN_DISTANCE_SQUARED) if (distanceSquared < MIN_DISTANCE_SQUARED)
{ {
accepted = false; accepted = false;
break; break;
} }
} }
@ -152,7 +153,7 @@ namespace ZL
renderer.shaderManager.AddShaderFromFiles("default", "./shaders/default.vertex", "./shaders/default_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("default", "./shaders/default.vertex", "./shaders/default_desktop.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("env_sky", "./shaders/env_sky.vertex", "./shaders/env_sky_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("env_sky", "./shaders/env_sky.vertex", "./shaders/env_sky_desktop.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("defaultAtmosphere", "./shaders/defaultAtmosphere.vertex", "./shaders/defaultAtmosphere_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("defaultAtmosphere", "./shaders/defaultAtmosphere.vertex", "./shaders/defaultAtmosphere_desktop.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("planetBake", "./shaders/planet_bake.vertex", "./shaders/planet_bake_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("planetBake", "./shaders/planet_bake.vertex", "./shaders/planet_bake_desktop.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("planetStone", "./shaders/planet_stone.vertex", "./shaders/planet_stone_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("planetStone", "./shaders/planet_stone.vertex", "./shaders/planet_stone_desktop.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("planetLand", "./shaders/planet_land.vertex", "./shaders/planet_land_desktop.fragment", CONST_ZIP_FILE); renderer.shaderManager.AddShaderFromFiles("planetLand", "./shaders/planet_land.vertex", "./shaders/planet_land_desktop.fragment", CONST_ZIP_FILE);
@ -176,37 +177,73 @@ namespace ZL
#endif #endif
#endif #endif
//#ifndef SIMPLIFIED //#ifndef SIMPLIFIED
bool cfgLoaded = sparkEmitter.loadFromJsonFile("config/spark_config.json", renderer, CONST_ZIP_FILE); bool cfgLoaded = sparkEmitter.loadFromJsonFile("config/spark_config.json", renderer, CONST_ZIP_FILE);
bool projCfgLoaded = projectileEmitter.loadFromJsonFile("config/spark_projectile_config.json", renderer, CONST_ZIP_FILE); bool projCfgLoaded = projectileEmitter.loadFromJsonFile("config/spark_projectile_config.json", renderer, CONST_ZIP_FILE);
projectileEmitter.setEmissionPoints(std::vector<Vector3f>()); projectileEmitter.setEmissionPoints(std::vector<Vector3f>());
uiManager.loadFromFile("config/ui.json", renderer, CONST_ZIP_FILE); uiManager.loadFromFile("config/ui.json", renderer, CONST_ZIP_FILE);
uiManager.setButtonCallback("playButton", [this](const std::string& name) { uiManager.loadFromFile("config/ui.json", renderer, CONST_ZIP_FILE);
std::cerr << "Play button pressed: " << name << std::endl; uiManager.startAnimationOnNode("backgroundNode", "bgScroll");
}); static bool isExitButtonAnimating = false;
uiManager.setAnimationCallback("settingsButton", "buttonsExit", [this]() {
uiManager.setButtonCallback("exitButton", [](const std::string& name) { std::cerr << "Settings button animation finished -> переход в настройки" << std::endl;
Environment::exitGameLoop = true;
});
uiManager.setButtonCallback("settingsButton", [this](const std::string& name) {
if (uiManager.pushMenuFromFile("config/settings.json", this->renderer, CONST_ZIP_FILE)) { if (uiManager.pushMenuFromFile("config/settings.json", this->renderer, CONST_ZIP_FILE)) {
uiManager.setButtonCallback("Opt1", [this](const std::string& n) { uiManager.setButtonCallback("Opt1", [this](const std::string& n) {
std::cerr << "Opt1 pressed: " << n << std::endl; std::cerr << "Opt1 pressed: " << n << std::endl;
}); });
uiManager.setButtonCallback("Opt2", [this](const std::string& n) { uiManager.setButtonCallback("Opt2", [this](const std::string& n) {
std::cerr << "Opt2 pressed: " << n << std::endl; std::cerr << "Opt2 pressed: " << n << std::endl;
}); });
uiManager.setButtonCallback("backButton", [this](const std::string& n) { uiManager.setButtonCallback("backButton", [this](const std::string& n) {
uiManager.stopAllAnimations();
uiManager.popMenu(); uiManager.popMenu();
}); });
} }
else { else {
std::cerr << "Failed to open settings menu" << std::endl; std::cerr << "Failed to open settings menu after animations" << std::endl;
}
});
uiManager.setAnimationCallback("exitButton", "bgScroll", []() {
std::cerr << "Exit button bgScroll animation finished" << std::endl;
g_exitBgAnimating = false;
});
// Set UI button callbacks
uiManager.setButtonCallback("playButton", [this](const std::string& name) {
std::cerr << "Play button pressed: " << name << std::endl;
});
uiManager.setButtonCallback("settingsButton", [this](const std::string& name) {
std::cerr << "Settings button pressed: " << name << std::endl;
uiManager.startAnimationOnNode("playButton", "buttonsExit");
uiManager.startAnimationOnNode("settingsButton", "buttonsExit");
uiManager.startAnimationOnNode("exitButton", "buttonsExit");
});
uiManager.setButtonCallback("exitButton", [this](const std::string& name) {
std::cerr << "Exit button pressed: " << name << std::endl;
if (!g_exitBgAnimating) {
std::cerr << "start repeat anim bgScroll on exitButton" << std::endl;
g_exitBgAnimating = true;
uiManager.startAnimationOnNode("exitButton", "bgScroll");
}
else {
std::cerr << "stop repeat anim bgScroll on exitButton" << std::endl;
g_exitBgAnimating = false;
uiManager.stopAnimationOnNode("exitButton", "bgScroll");
auto exitButton = uiManager.findButton("exitButton");
if (exitButton) {
exitButton->animOffsetX = 0.0f;
exitButton->animOffsetY = 0.0f;
exitButton->animScaleX = 1.0f;
exitButton->animScaleY = 1.0f;
exitButton->buildMesh();
}
} }
}); });
@ -215,7 +252,7 @@ namespace ZL
musicVolume = value; musicVolume = value;
Environment::shipVelocity = musicVolume * 20.0f; Environment::shipVelocity = musicVolume * 20.0f;
}); });
//#endif //#endif
cubemapTexture = std::make_shared<Texture>( cubemapTexture = std::make_shared<Texture>(
std::array<TextureDataStruct, 6>{ std::array<TextureDataStruct, 6>{
@ -234,8 +271,8 @@ namespace ZL
//Load texture //Load texture
spaceshipTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/DefaultMaterial_BaseColor_shine.png", CONST_ZIP_FILE)); spaceshipTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/DefaultMaterial_BaseColor_shine.png", CONST_ZIP_FILE));
spaceshipBase = LoadFromTextFile02("./resources/spaceship006.txt", CONST_ZIP_FILE); spaceshipBase = LoadFromTextFile02("./resources/spaceship006.txt", CONST_ZIP_FILE);
spaceshipBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI / 2.0, Eigen::Vector3f::UnitY())).toRotationMatrix());// QuatFromRotateAroundY(M_PI / 2.0).toRotationMatrix()); spaceshipBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI / 2.0, Eigen::Vector3f::UnitY())).toRotationMatrix());// QuatFromRotateAroundY(M_PI / 2.0).toRotationMatrix();
//spaceshipTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/cap_D.png", CONST_ZIP_FILE)); //spaceshipTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/cap_D.png", CONST_ZIP_FILE));
//spaceshipBase = LoadFromTextFile02("./resources/spaceship006x.txt", CONST_ZIP_FILE); //spaceshipBase = LoadFromTextFile02("./resources/spaceship006x.txt", CONST_ZIP_FILE);
//spaceshipBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI / 2.0, Eigen::Vector3f::UnitY())).toRotationMatrix());// QuatFromRotateAroundY(M_PI / 2.0).toRotationMatrix()); //spaceshipBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI / 2.0, Eigen::Vector3f::UnitY())).toRotationMatrix());// QuatFromRotateAroundY(M_PI / 2.0).toRotationMatrix());
@ -448,11 +485,11 @@ namespace ZL
} }
} }
//#ifndef SIMPLIFIED //#ifndef SIMPLIFIED
sparkEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); sparkEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
//#endif //#endif
renderer.PopMatrix(); renderer.PopMatrix();
renderer.PopProjectionMatrix(); renderer.PopProjectionMatrix();
renderer.DisableVertexAttribArray(vPositionName); renderer.DisableVertexAttribArray(vPositionName);
@ -615,10 +652,10 @@ namespace ZL
drawShip(); drawShip();
drawBoxes(); drawBoxes();
//#ifndef SIMPLIFIED //#ifndef SIMPLIFIED
drawUI(); drawUI();
//#endif //#endif
CheckGlError(); CheckGlError();
} }
@ -634,10 +671,10 @@ namespace ZL
size_t delta = (newTickCount - lastTickCount > CONST_MAX_TIME_INTERVAL) ? size_t delta = (newTickCount - lastTickCount > CONST_MAX_TIME_INTERVAL) ?
CONST_MAX_TIME_INTERVAL : newTickCount - lastTickCount; CONST_MAX_TIME_INTERVAL : newTickCount - lastTickCount;
//#ifndef SIMPLIFIED //#ifndef SIMPLIFIED
//gameObjects.updateScene(delta); //gameObjects.updateScene(delta);
sparkEmitter.update(static_cast<float>(delta)); sparkEmitter.update(static_cast<float>(delta));
//#endif //#endif
planetObject.update(static_cast<float>(delta)); planetObject.update(static_cast<float>(delta));
if (Environment::tapDownHold) { if (Environment::tapDownHold) {
@ -672,7 +709,7 @@ namespace ZL
Environment::shipPosition = Environment::shipPosition + velocityDirectionAdjusted; Environment::shipPosition = Environment::shipPosition + velocityDirectionAdjusted;
} }
//#ifndef SIMPLIFIED //#ifndef SIMPLIFIED
for (auto& p : projectiles) { for (auto& p : projectiles) {
if (p && p->isActive()) { if (p && p->isActive()) {
p->update(static_cast<float>(delta), renderer); p->update(static_cast<float>(delta), renderer);
@ -707,7 +744,9 @@ namespace ZL
sparkEmitter.update(static_cast<float>(delta)); sparkEmitter.update(static_cast<float>(delta));
projectileEmitter.update(static_cast<float>(delta)); projectileEmitter.update(static_cast<float>(delta));
//#endif
uiManager.update(static_cast<float>(delta));
//#endif
lastTickCount = newTickCount; lastTickCount = newTickCount;
} }
} }
@ -765,7 +804,7 @@ namespace ZL
uiManager.onMouseDown(uiX, uiY); uiManager.onMouseDown(uiX, uiY);
bool uiHandled = false; bool uiHandled = false;
//#ifndef SIMPLIFIED //#ifndef SIMPLIFIED
if (event.button.button == SDL_BUTTON_LEFT && !uiManager.isUiInteraction()) { if (event.button.button == SDL_BUTTON_LEFT && !uiManager.isUiInteraction()) {
uint64_t now = SDL_GetTicks64(); uint64_t now = SDL_GetTicks64();
if (now - lastProjectileFireTime >= static_cast<uint64_t>(projectileCooldownMs)) { if (now - lastProjectileFireTime >= static_cast<uint64_t>(projectileCooldownMs)) {
@ -773,7 +812,7 @@ namespace ZL
fireProjectiles(); fireProjectiles();
} }
} }
//#endif //#endif
for (const auto& button : uiManager.findButton("") ? std::vector<std::shared_ptr<UiButton>>{} : std::vector<std::shared_ptr<UiButton>>{}) { for (const auto& button : uiManager.findButton("") ? std::vector<std::shared_ptr<UiButton>>{} : std::vector<std::shared_ptr<UiButton>>{}) {
(void)button; (void)button;
} }

View File

@ -8,6 +8,17 @@ namespace ZL {
using json = nlohmann::json; using json = nlohmann::json;
static float applyEasing(const std::string& easing, float t) {
if (easing == "easein") {
return t * t;
}
else if (easing == "easeout") {
float inv = 1.0f - t;
return 1.0f - inv * inv;
}
return t;
}
void UiButton::buildMesh() { void UiButton::buildMesh() {
mesh.data.PositionData.clear(); mesh.data.PositionData.clear();
mesh.data.TexCoordData.clear(); mesh.data.TexCoordData.clear();
@ -52,6 +63,10 @@ namespace ZL {
static const std::string vTexCoordName = "vTexCoord"; static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture"; static const std::string textureUniformName = "Texture";
renderer.PushMatrix();
renderer.TranslateMatrix({ animOffsetX, animOffsetY, 0.0f });
renderer.ScaleMatrix({ animScaleX, animScaleY, 1.0f });
renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform1i(textureUniformName, 0);
renderer.EnableVertexAttribArray(vPositionName); renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName); renderer.EnableVertexAttribArray(vTexCoordName);
@ -61,6 +76,7 @@ namespace ZL {
renderer.DisableVertexAttribArray(vPositionName); renderer.DisableVertexAttribArray(vPositionName);
renderer.DisableVertexAttribArray(vTexCoordName); renderer.DisableVertexAttribArray(vTexCoordName);
renderer.PopMatrix();
} }
void UiSlider::buildTrackMesh() { void UiSlider::buildTrackMesh() {
@ -191,6 +207,8 @@ namespace ZL {
sliders.clear(); sliders.clear();
collectButtonsAndSliders(root); collectButtonsAndSliders(root);
nodeActiveAnims.clear();
for (auto& b : buttons) { for (auto& b : buttons) {
b->buildMesh(); b->buildMesh();
} }
@ -278,6 +296,37 @@ namespace ZL {
node->slider = s; node->slider = s;
} }
if (j.contains("animations") && j["animations"].is_object()) {
for (auto it = j["animations"].begin(); it != j["animations"].end(); ++it) {
std::string animName = it.key();
const auto& animDef = it.value();
UiNode::AnimSequence seq;
if (animDef.contains("repeat") && animDef["repeat"].is_boolean()) seq.repeat = animDef["repeat"].get<bool>();
if (animDef.contains("steps") && animDef["steps"].is_array()) {
for (const auto& step : animDef["steps"]) {
UiNode::AnimStep s;
if (step.contains("type") && step["type"].is_string()) {
s.type = step["type"].get<std::string>();
std::transform(s.type.begin(), s.type.end(), s.type.begin(), ::tolower);
}
if (step.contains("to") && step["to"].is_array() && step["to"].size() >= 2) {
s.toX = step["to"][0].get<float>();
s.toY = step["to"][1].get<float>();
}
if (step.contains("duration")) {
s.durationMs = step["duration"].get<float>() * 1000.0f;
}
if (step.contains("easing") && step["easing"].is_string()) {
s.easing = step["easing"].get<std::string>();
std::transform(s.easing.begin(), s.easing.end(), s.easing.begin(), ::tolower);
}
seq.steps.push_back(s);
}
}
node->animations[animName] = std::move(seq);
}
}
if (j.contains("children") && j["children"].is_array()) { if (j.contains("children") && j["children"].is_array()) {
for (const auto& ch : j["children"]) { for (const auto& ch : j["children"]) {
node->children.push_back(parseNode(ch, renderer, zipFile)); node->children.push_back(parseNode(ch, renderer, zipFile));
@ -406,13 +455,27 @@ namespace ZL {
prev.pressedSlider = pressedSlider; prev.pressedSlider = pressedSlider;
prev.path = ""; prev.path = "";
prev.animCallbacks = animCallbacks;
try { try {
nodeActiveAnims.clear();
animCallbacks.clear();
for (auto& b : buttons) {
if (b) {
b->animOffsetX = 0.0f;
b->animOffsetY = 0.0f;
b->animScaleX = 1.0f;
b->animScaleY = 1.0f;
}
}
loadFromFile(path, renderer, zipFile); loadFromFile(path, renderer, zipFile);
menuStack.push_back(std::move(prev)); menuStack.push_back(std::move(prev));
return true; return true;
} }
catch (const std::exception& e) { catch (const std::exception& e) {
std::cerr << "UiManager: pushMenuFromFile failed to load " << path << " : " << e.what() << std::endl; std::cerr << "UiManager: pushMenuFromFile failed to load " << path << " : " << e.what() << std::endl;
animCallbacks = prev.animCallbacks;
return false; return false;
} }
} }
@ -425,19 +488,32 @@ namespace ZL {
auto s = menuStack.back(); auto s = menuStack.back();
menuStack.pop_back(); menuStack.pop_back();
nodeActiveAnims.clear();
root = s.root; root = s.root;
buttons = s.buttons; buttons = s.buttons;
sliders = s.sliders; sliders = s.sliders;
pressedButton = s.pressedButton; pressedButton = s.pressedButton;
pressedSlider = s.pressedSlider; pressedSlider = s.pressedSlider;
animCallbacks = s.animCallbacks;
for (auto& b : buttons) { for (auto& b : buttons) {
if (b) b->buildMesh(); if (b) {
} b->animOffsetX = 0.0f;
for (auto& sl : sliders) { b->animOffsetY = 0.0f;
if (sl) { sl->buildTrackMesh(); sl->buildKnobMesh(); } b->animScaleX = 1.0f;
b->animScaleY = 1.0f;
b->buildMesh();
}
} }
for (auto& sl : sliders) {
if (sl) {
sl->buildTrackMesh();
sl->buildKnobMesh();
}
}
return true; return true;
} }
@ -461,6 +537,168 @@ namespace ZL {
renderer.PopProjectionMatrix(); renderer.PopProjectionMatrix();
} }
static std::shared_ptr<UiNode> findNodeByName(const std::shared_ptr<UiNode>& node, const std::string& name) {
if (!node) return nullptr;
if (!name.empty() && node->name == name) return node;
for (auto& c : node->children) {
auto r = findNodeByName(c, name);
if (r) return r;
}
return nullptr;
}
void UiManager::update(float deltaMs) {
if (!root) return;
std::vector<std::pair<std::shared_ptr<UiNode>, size_t>> animationsToRemove;
std::vector<std::function<void()>> pendingCallbacks;
for (auto& kv : nodeActiveAnims) {
auto node = kv.first;
auto& activeList = kv.second;
for (size_t i = 0; i < activeList.size(); ++i) {
auto& act = activeList[i];
if (!act.seq) {
animationsToRemove.push_back({ node, i });
continue;
}
const auto& steps = act.seq->steps;
if (act.stepIndex >= steps.size()) {
if (act.repeat) {
if (node->button) {
node->button->animOffsetX = act.origOffsetX;
node->button->animOffsetY = act.origOffsetY;
node->button->animScaleX = act.origScaleX;
node->button->animScaleY = act.origScaleY;
}
act.stepIndex = 0;
act.elapsedMs = 0.0f;
act.stepStarted = false;
}
else {
if (act.onComplete) {
pendingCallbacks.push_back(act.onComplete);
}
animationsToRemove.push_back({ node, i });
}
continue;
}
const auto& step = steps[act.stepIndex];
if (step.durationMs <= 0.0f) {
if (step.type == "move") {
if (node->button) {
node->button->animOffsetX = step.toX;
node->button->animOffsetY = step.toY;
}
}
else if (step.type == "scale") {
if (node->button) {
node->button->animScaleX = step.toX;
node->button->animScaleY = step.toY;
}
}
act.stepIndex++;
act.elapsedMs = 0.0f;
act.stepStarted = false;
continue;
}
if (!act.stepStarted && act.stepIndex == 0 && act.elapsedMs == 0.0f) {
if (node->button) {
act.origOffsetX = node->button->animOffsetX;
act.origOffsetY = node->button->animOffsetY;
act.origScaleX = node->button->animScaleX;
act.origScaleY = node->button->animScaleY;
}
else {
act.origOffsetX = act.origOffsetY = 0.0f;
act.origScaleX = act.origScaleY = 1.0f;
}
}
float prevElapsed = act.elapsedMs;
act.elapsedMs += deltaMs;
if (!act.stepStarted && prevElapsed == 0.0f) {
if (node->button) {
act.startOffsetX = node->button->animOffsetX;
act.startOffsetY = node->button->animOffsetY;
act.startScaleX = node->button->animScaleX;
act.startScaleY = node->button->animScaleY;
}
else {
act.startOffsetX = act.startOffsetY = 0.0f;
act.startScaleX = act.startScaleY = 1.0f;
}
if (step.type == "move") {
act.endOffsetX = step.toX;
act.endOffsetY = step.toY;
}
else if (step.type == "scale") {
act.endScaleX = step.toX;
act.endScaleY = step.toY;
}
act.stepStarted = true;
}
float t = (step.durationMs > 0.0f) ? (act.elapsedMs / step.durationMs) : 1.0f;
if (t > 1.0f) t = 1.0f;
float te = applyEasing(step.easing, t);
if (step.type == "move") {
float nx = act.startOffsetX + (act.endOffsetX - act.startOffsetX) * te;
float ny = act.startOffsetY + (act.endOffsetY - act.startOffsetY) * te;
if (node->button) {
node->button->animOffsetX = nx;
node->button->animOffsetY = ny;
}
}
else if (step.type == "scale") {
float sx = act.startScaleX + (act.endScaleX - act.startScaleX) * te;
float sy = act.startScaleY + (act.endScaleY - act.startScaleY) * te;
if (node->button) {
node->button->animScaleX = sx;
node->button->animScaleY = sy;
}
}
else if (step.type == "wait") {
//wait
}
if (act.elapsedMs >= step.durationMs) {
act.stepIndex++;
act.elapsedMs = 0.0f;
act.stepStarted = false;
}
}
}
for (auto it = animationsToRemove.rbegin(); it != animationsToRemove.rend(); ++it) {
auto& [node, index] = *it;
if (nodeActiveAnims.find(node) != nodeActiveAnims.end()) {
auto& animList = nodeActiveAnims[node];
if (index < animList.size()) {
animList.erase(animList.begin() + index);
}
if (animList.empty()) {
nodeActiveAnims.erase(node);
}
}
}
for (auto& cb : pendingCallbacks) {
try {
cb();
}
catch (...) {
std::cerr << "UiManager: animation onComplete callback threw exception" << std::endl;
}
}
}
void UiManager::onMouseMove(int x, int y) { void UiManager::onMouseMove(int x, int y) {
for (auto& b : buttons) { for (auto& b : buttons) {
if (b->rect.contains((float)x, (float)y)) { if (b->rect.contains((float)x, (float)y)) {
@ -540,4 +778,88 @@ namespace ZL {
return nullptr; return nullptr;
} }
bool UiManager::startAnimationOnNode(const std::string& nodeName, const std::string& animName) {
if (!root) return false;
auto node = findNodeByName(root, nodeName);
if (!node) return false;
auto it = node->animations.find(animName);
if (it == node->animations.end()) return false;
ActiveAnim aa;
aa.name = animName;
aa.seq = &it->second;
aa.stepIndex = 0;
aa.elapsedMs = 0.0f;
aa.repeat = it->second.repeat;
aa.stepStarted = false;
if (node->button) {
aa.origOffsetX = node->button->animOffsetX;
aa.origOffsetY = node->button->animOffsetY;
aa.origScaleX = node->button->animScaleX;
aa.origScaleY = node->button->animScaleY;
}
auto cbIt = animCallbacks.find({ nodeName, animName });
if (cbIt != animCallbacks.end()) aa.onComplete = cbIt->second;
nodeActiveAnims[node].push_back(std::move(aa));
return true;
}
bool UiManager::stopAnimationOnNode(const std::string& nodeName, const std::string& animName) {
if (!root) return false;
auto node = findNodeByName(root, nodeName);
if (!node) return false;
auto it = nodeActiveAnims.find(node);
if (it != nodeActiveAnims.end()) {
auto& animList = it->second;
for (auto animIt = animList.begin(); animIt != animList.end(); ) {
if (animIt->name == animName) {
animIt = animList.erase(animIt);
}
else {
++animIt;
}
}
if (animList.empty()) {
nodeActiveAnims.erase(it);
}
return true;
}
return false;
}
void UiManager::startAnimation(const std::string& animName) {
if (!root) return;
std::function<void(const std::shared_ptr<UiNode>&)> traverse = [&](const std::shared_ptr<UiNode>& n) {
if (!n) return;
auto it = n->animations.find(animName);
if (it != n->animations.end()) {
ActiveAnim aa;
aa.name = animName;
aa.seq = &it->second;
aa.stepIndex = 0;
aa.elapsedMs = 0.0f;
aa.repeat = it->second.repeat;
aa.stepStarted = false;
if (n->button) {
aa.origOffsetX = n->button->animOffsetX;
aa.origOffsetY = n->button->animOffsetY;
aa.origScaleX = n->button->animScaleX;
aa.origScaleY = n->button->animScaleY;
}
auto cbIt = animCallbacks.find({ n->name, animName });
if (cbIt != animCallbacks.end()) aa.onComplete = cbIt->second;
nodeActiveAnims[n].push_back(std::move(aa));
}
for (auto& c : n->children) traverse(c);
};
traverse(root);
}
bool UiManager::setAnimationCallback(const std::string& nodeName, const std::string& animName, std::function<void()> cb) {
animCallbacks[{nodeName, animName}] = std::move(cb);
return true;
}
} // namespace ZL } // namespace ZL

View File

@ -8,6 +8,7 @@
#include <vector> #include <vector>
#include <memory> #include <memory>
#include <functional> #include <functional>
#include <map>
namespace ZL { namespace ZL {
@ -41,6 +42,12 @@ namespace ZL {
std::function<void(const std::string&)> onClick; std::function<void(const std::string&)> onClick;
// animation runtime
float animOffsetX = 0.0f;
float animOffsetY = 0.0f;
float animScaleX = 1.0f;
float animScaleY = 1.0f;
void buildMesh(); void buildMesh();
void draw(Renderer& renderer) const; void draw(Renderer& renderer) const;
}; };
@ -73,6 +80,19 @@ namespace ZL {
std::shared_ptr<UiSlider> slider; std::shared_ptr<UiSlider> slider;
std::string orientation = "vertical"; std::string orientation = "vertical";
float spacing = 0.0f; float spacing = 0.0f;
struct AnimStep {
std::string type;
float toX = 0.0f;
float toY = 0.0f;
float durationMs = 0.0f;
std::string easing = "linear";
};
struct AnimSequence {
std::vector<AnimStep> steps;
bool repeat = false;
};
std::map<std::string, AnimSequence> animations;
}; };
class UiManager { class UiManager {
@ -91,6 +111,19 @@ namespace ZL {
return pressedButton != nullptr || pressedSlider != nullptr; return pressedButton != nullptr || pressedSlider != nullptr;
} }
void stopAllAnimations() {
nodeActiveAnims.clear();
for (auto& b : buttons) {
if (b) {
b->animOffsetX = 0.0f;
b->animOffsetY = 0.0f;
b->animScaleX = 1.0f;
b->animScaleY = 1.0f;
}
}
}
std::shared_ptr<UiButton> findButton(const std::string& name); std::shared_ptr<UiButton> findButton(const std::string& name);
bool setButtonCallback(const std::string& name, std::function<void(const std::string&)> cb); bool setButtonCallback(const std::string& name, std::function<void(const std::string&)> cb);
@ -106,15 +139,47 @@ namespace ZL {
bool popMenu(); bool popMenu();
void clearMenuStack(); void clearMenuStack();
void update(float deltaMs);
void startAnimation(const std::string& animName);
bool startAnimationOnNode(const std::string& nodeName, const std::string& animName);
bool stopAnimationOnNode(const std::string& nodeName, const std::string& animName);
bool setAnimationCallback(const std::string& nodeName, const std::string& animName, std::function<void()> cb);
private: private:
std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile); std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile);
void layoutNode(const std::shared_ptr<UiNode>& node); void layoutNode(const std::shared_ptr<UiNode>& node);
void collectButtonsAndSliders(const std::shared_ptr<UiNode>& node); void collectButtonsAndSliders(const std::shared_ptr<UiNode>& node);
struct ActiveAnim {
std::string name;
const UiNode::AnimSequence* seq = nullptr;
size_t stepIndex = 0;
float elapsedMs = 0.0f;
bool repeat = false;
float startOffsetX = 0.0f;
float startOffsetY = 0.0f;
float endOffsetX = 0.0f;
float endOffsetY = 0.0f;
float startScaleX = 1.0f;
float startScaleY = 1.0f;
float endScaleX = 1.0f;
float endScaleY = 1.0f;
std::function<void()> onComplete;
float origOffsetX = 0.0f;
float origOffsetY = 0.0f;
float origScaleX = 1.0f;
float origScaleY = 1.0f;
bool stepStarted = false;
};
std::shared_ptr<UiNode> root; std::shared_ptr<UiNode> root;
std::vector<std::shared_ptr<UiButton>> buttons; std::vector<std::shared_ptr<UiButton>> buttons;
std::vector<std::shared_ptr<UiSlider>> sliders; std::vector<std::shared_ptr<UiSlider>> sliders;
std::map<std::shared_ptr<UiNode>, std::vector<ActiveAnim>> nodeActiveAnims;
std::map<std::pair<std::string, std::string>, std::function<void()>> animCallbacks; // key: (nodeName, animName)
std::shared_ptr<UiButton> pressedButton; std::shared_ptr<UiButton> pressedButton;
std::shared_ptr<UiSlider> pressedSlider; std::shared_ptr<UiSlider> pressedSlider;
@ -125,6 +190,7 @@ namespace ZL {
std::shared_ptr<UiButton> pressedButton; std::shared_ptr<UiButton> pressedButton;
std::shared_ptr<UiSlider> pressedSlider; std::shared_ptr<UiSlider> pressedSlider;
std::string path; std::string path;
std::map<std::pair<std::string, std::string>, std::function<void()>> animCallbacks;
}; };
std::vector<MenuState> menuStack; std::vector<MenuState> menuStack;