From 528e7ae5a6597d4aeab2c70ede2adf492f5220d1 Mon Sep 17 00:00:00 2001 From: Vlad Date: Sat, 10 Jan 2026 15:06:12 +0600 Subject: [PATCH] add animation buttons --- assets.zip | Bin 17170300 -> 17170469 bytes config/ui.json | 76 ++++++++++- src/Game.cpp | 105 ++++++++++----- src/UiManager.cpp | 330 +++++++++++++++++++++++++++++++++++++++++++++- src/UiManager.h | 66 ++++++++++ 5 files changed, 537 insertions(+), 40 deletions(-) diff --git a/assets.zip b/assets.zip index b8c7ef4fa8d0d57fbc050f73780cfd72de84cf78..1f44300d386d192e724f493a2183b8bc6dc26bca 100644 GIT binary patch delta 2588 zcmXxm3s{V48wc>7rl*;qvki$Xos;BHe64b981K z2}4rCokM&*Ia<;tB_$(M*4l7TO-b_QEHV@(D8))5R%(is9I;YMtfXS4wphs(D|uIx z{83ajA{(Uqt%Rj0F29niN)m5Km!q2OEUCsWl2l_?_zq^mESL>*;Cq-WOEne*++@=o z3W20UF%Ra$0&s(c;4bS>cu;m8Nl(5fc*%P5z4as5I^LK?65bde@C83u3_rk+;15e6 z0G7fs2!!Pj1S?=A`~<6DH3(r1tc74$2S3Al2!Rc-5q^PC*aTq^4kFkLzrq%XfUOV- zQLqi7AqKX?Zx9P{5Dy8E2s>aWh+!8El}xo`}QLmr%flaLPua0*Vt88{2);5=M_LbwQ*;4=IHSKunh;2K_zUjC19%9J;4zfJ6L<>cPyv;oknzS;nXP0y#5Sf% zVjJ@es^M>_k=e#P&+Arwmov)4YyH`nji^>)BdUWJ@Dl2w0UBjCq9&^}Hsz;uis0!uiItZ*B!4O zaD4DU=s5pB=#+Jw|L1KctJJxDkkq+#!AIzp)w%VIGi9AYVUJ``*bARzgTl|T#;h72 z^;r@h^+gsR)wg-Osv_(D*k{aVJ>7vmiS9r@e1!oR+^0K0l0fyMse0iMx$b}#$y?4- zJ@iuX(95_;`J<$YWt2?e;&`f>v_6ND5^3@ul;}vac2dHX&gr0Jh_oe;lCi3D$$85u zNhd7k22m2sW04E&`d0CX`srx3`lxVUjHIq{Ej7|0+ zHL=NBTqISumARC3vv%zeU&nFX>UKpQCGD(TdV-QKEZsaw$s;znKcA97HYk5n zK#73Es*}!A;>Dsg_5vj@S#f0{CCkx#cGyiSjs=2{_5km&I!B$7-T*C^uv1x+4IQ@6vR_oRG;5^&0*}4# zW<2zaJ7t{NdcwHgzQAdm!`#5f3oA~}IqVT`aK<5~p?BW=Z@q25=U=Osz1pr+rbx_7 zT&&&p-tE)-ts8y9N+S(&+IKgbw@z>F`fgsO$inYs^X}(2jK0oXlyBZ@e(O^4qcRC>kuVkWLmA%f+Tu3#P2N(-HGNexr|L z^OWe@QQNq#w@*16z2V=hm0jf$Eu2R)%u4TQ?o_E zsRa(7Gdezfi5rp>Y5j|6-R<>#W<~FNCPrQTn%NU-pS7u>I{N;7KZU36umtOm9Lwul zXLm+B*tuV7IJ$&>%DH+xzpE>|BBax}q_EJ{YeHLWq;{p^>C2@aDMmBuiu(10fx>^z z)V&SnhnoC!AZ&&9KuL_UI7~3m)Y3V^*OSXzAu;S-qp6{x>0~J1gMZwtO*yK)eEBOS wE*$J-c_)$s(Zfa!`v0txq11km66aB>{p#yUwN>8$PyE)6*3g*NOo@iZzsYOgegFUf delta 2408 zcmYM!d0dl68V7KOFgd6YY5`AJtteuxShd!|TC1qEU{=Ih4=6>v77JBWjtUD=3GY%B z!G^=G5Va_1IkZ+^1#7BS>rn*^M*vX>a)_H$ssV~M>@V~FvB~HA=6UCN=6&a#nY_o9 zdD6cuxzfxJMX9p2S$-e0p94Xn0NL7I29<%R47MtRNM*278K}x2Rv9EJgY=w1Hkw*X z_#rBbBP>dg$nHz55vP#Lz#d+MVc-CcAO|OS9frdj;0!L{3L{`7jDpcH2F8LLxWnII z9E^uIVFGx-M3}@YYiMFau`7EO-}Y!+S7? z-{JfIrdsxN59L12A{Lp4`y94qcTv0eaB3G{_&dyn4`3dA2=ie9uXgcUd6`w4bbg#k zw-6S=V(^EL;A7sT3!v;iTwhTjEaCf#6!KWsPwMs$PU^N4f?yd0!*cipR=}sQ5>~-# zSOXyt3ZFq3d=BBT79wCBtcMM-5jMd;Ardyj7Z3%}@Fi@4tq=p-U_0!9ScrpoNPw?k zCnQ1={0ovH1(dK0cEcV>1r?+L2mgk2_!{=YKG+W#Z~!tP3l73JpoVP7fn3OgL+~y9 z2M$9%6o3Yfz)?5`-$5Z9hZ9f)#c&c%!S_%CrBDXta2n3Q4{#QKgmZ8nc(?!;p#m!5 z68r?0K?^^_6}Sr5;1{?KH{e&e3Af-j+=1WVF5H7EsD>K24?14zR_h+dc8FJEE$5Y3 z2ldbZdfqFsvCwS&FXvWoZCb&~CdV{!lVh6U0kptFcm%EdG3VrVk-Nc zGnGB#O=Z13d90<`{|~O&|2ez>GvDmrH+~Gu3`O*DLlONjzz;?Ine4(^l?i`x%7j5) znecL3lC_05zj)~)V>t)QOU}Xa7rX)s4DEHWkON>n*jf)FqIIy?k-`uurX*6mb#`R# z87dpC(XFOrh6LBMwrL(7q2wn`=8u#N)8wa8BGD+KDY4g-8z~v7sYaH);Iz^ZN^;m+ zPkS_!k_}SpIJB}2l*9|+9&`&A!p>Mqb_wC;WJ(H!Zm%mTu?XSt-IP?a7mBtT_q;BI zc^oCr*$itlGAQX5!mqN?Dsy0(j4pY*=h9J$S#8JqPDxk!cg&KDaC3UQ_ z4^bPdY{J7(A#FHHiJ7IFj#1LX(!YL(XBE;lg_Ias`pR)i23d6Z1SNM^;l3hD*04hD zlVVEz*gI0|T|&tc7HX2qDQRJ)VW%kx5%QPLQsN}^s={o$3Ssv-N~W^M*M$8-iJNxT z1xnn7DPN(bq(msQyF$rI;X2kbZ=oRb4ka$a+!f!YWQs6hbvj&92<@6EaUg81ZT2i+ zYkg8;gU>3pm*!?O>LgFh;gQ%MnwL0R%EqjCK*V0YJ#6|9nYa9`f zI`Dg}ZQh)?fu5gp5+79#m8+1FvY^C1=;+Xtj~tf=_YQx><@3Rd zPs>lmJqvteNw(ACsx^VT?&${WmS)u()Em=nQcl#Cmp(Qw{UCN{`^Mnfi$|_c z3+W#ro*!~5D)Q0Ob?a|dv}7GJZN8{GUo)tlG486VrF)*IS`o1DUSak%O4IDT%Bx=F zj?28^arTe(^CNC|-c^29uDGpNP3(!b%Q^m@;o0DMgMQzcd}DRX>U}NFqfTZNHfHW8 z#<2bGq)gyT{oQ<{Q(bawAGgKdH|FmR9Ut5xPn&We_f7Hphu>X3%%0xrtsh)f)*TkQ zqBvA`E6b-cN|!ymzH%sCVS8+FY=^mfr~ZLmzQ@#y&JWVRb&sm(ajgzt)Vni$ZoNV& zbN8M`r{}@<2fo>~!7-yGZQUH7{*A+0 zMzrgH3p-d+QspRaKc95NTz5zBKi{w@zOA%z(!!*uFQUJ+?Cx#sZAyRY9lTvXch>aU z=biGPK#4R;S8<_&*w~DdX=k)k;yFfS-5{YBN_<4vAlgHSrV&T1e(3*8HFZOj%pPss X2tk2VY~5CY_%@HVvDs^&xZ3{!5IC4; diff --git a/config/ui.json b/config/ui.json index c8753cf..974d34a 100644 --- a/config/ui.json +++ b/config/ui.json @@ -31,9 +31,25 @@ "y": 300, "width": 200, "height": 50, + "animations": { + "buttonsExit": { + "repeat": false, + "steps": [ + { + "type": "move", + "to": [ + -400, + 0 + ], + "duration": 1.0, + "easing": "easein" + } + ] + } + }, "textures": { "normal": "./resources/button.png", - "hover": "./resources/button.png", + "hover": "./resources/sand.png", "pressed": "./resources/button.png" } }, @@ -44,9 +60,29 @@ "y": 200, "width": 200, "height": 50, + "animations": { + "buttonsExit": { + "repeat": false, + "steps": [ + { + "type": "wait", + "duration": 0.5 + }, + { + "type": "move", + "to": [ + -400, + 0 + ], + "duration": 1.0, + "easing": "easein" + } + ] + } + }, "textures": { "normal": "./resources/sand.png", - "hover": "./resources/sand.png", + "hover": "./resources/button.png", "pressed": "./resources/sand.png" } }, @@ -57,9 +93,43 @@ "y": 100, "width": 200, "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": { "normal": "./resources/rock.png", - "hover": "./resources/rock.png", + "hover": "./resources/button.png", "pressed": "./resources/rock.png" } } diff --git a/src/Game.cpp b/src/Game.cpp index 115de29..44be6de 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -18,7 +18,8 @@ namespace ZL const char* CONST_ZIP_FILE = "../assets.zip"; #endif - + static bool g_exitBgAnimating = false; + Eigen::Quaternionf generateRandomQuaternion(std::mt19937& gen) { @@ -42,7 +43,7 @@ namespace ZL const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE; const float MIN_COORD = -100.0f; const float MAX_COORD = 100.0f; - const int MAX_ATTEMPTS = 1000; + const int MAX_ATTEMPTS = 1000; std::vector boxCoordsArr; std::random_device rd; @@ -65,7 +66,7 @@ namespace ZL (float)distrib(gen) ); - accepted = true; + accepted = true; for (const auto& existingBox : boxCoordsArr) { @@ -76,7 +77,7 @@ namespace ZL if (distanceSquared < MIN_DISTANCE_SQUARED) { 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("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("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("planetLand", "./shaders/planet_land.vertex", "./shaders/planet_land_desktop.fragment", CONST_ZIP_FILE); @@ -176,37 +177,73 @@ namespace ZL #endif #endif -//#ifndef SIMPLIFIED + //#ifndef SIMPLIFIED 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); projectileEmitter.setEmissionPoints(std::vector()); uiManager.loadFromFile("config/ui.json", renderer, CONST_ZIP_FILE); - uiManager.setButtonCallback("playButton", [this](const std::string& name) { - std::cerr << "Play button pressed: " << name << std::endl; - }); - - uiManager.setButtonCallback("exitButton", [](const std::string& name) { - Environment::exitGameLoop = true; - }); - - uiManager.setButtonCallback("settingsButton", [this](const std::string& name) { + uiManager.loadFromFile("config/ui.json", renderer, CONST_ZIP_FILE); + uiManager.startAnimationOnNode("backgroundNode", "bgScroll"); + static bool isExitButtonAnimating = false; + uiManager.setAnimationCallback("settingsButton", "buttonsExit", [this]() { + std::cerr << "Settings button animation finished -> переход в настройки" << std::endl; if (uiManager.pushMenuFromFile("config/settings.json", this->renderer, CONST_ZIP_FILE)) { - uiManager.setButtonCallback("Opt1", [this](const std::string& n) { std::cerr << "Opt1 pressed: " << n << std::endl; }); - uiManager.setButtonCallback("Opt2", [this](const std::string& n) { std::cerr << "Opt2 pressed: " << n << std::endl; }); - uiManager.setButtonCallback("backButton", [this](const std::string& n) { + uiManager.stopAllAnimations(); uiManager.popMenu(); }); } 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; Environment::shipVelocity = musicVolume * 20.0f; }); -//#endif + //#endif cubemapTexture = std::make_shared( std::array{ @@ -234,8 +271,8 @@ namespace ZL //Load texture spaceshipTexture = std::make_unique(CreateTextureDataFromPng("./resources/DefaultMaterial_BaseColor_shine.png", 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(CreateTextureDataFromPng("./resources/cap_D.png", 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()); @@ -446,11 +483,11 @@ namespace ZL } } -//#ifndef SIMPLIFIED + //#ifndef SIMPLIFIED sparkEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height); -//#endif + //#endif renderer.PopMatrix(); renderer.PopProjectionMatrix(); renderer.DisableVertexAttribArray(vPositionName); @@ -613,10 +650,10 @@ namespace ZL drawShip(); drawBoxes(); -//#ifndef SIMPLIFIED + //#ifndef SIMPLIFIED drawUI(); -//#endif + //#endif CheckGlError(); } @@ -632,10 +669,10 @@ namespace ZL size_t delta = (newTickCount - lastTickCount > CONST_MAX_TIME_INTERVAL) ? CONST_MAX_TIME_INTERVAL : newTickCount - lastTickCount; -//#ifndef SIMPLIFIED - //gameObjects.updateScene(delta); + //#ifndef SIMPLIFIED + //gameObjects.updateScene(delta); sparkEmitter.update(static_cast(delta)); -//#endif + //#endif planetObject.update(static_cast(delta)); if (Environment::tapDownHold) { @@ -670,7 +707,7 @@ namespace ZL Environment::shipPosition = Environment::shipPosition + velocityDirectionAdjusted; } -//#ifndef SIMPLIFIED + //#ifndef SIMPLIFIED for (auto& p : projectiles) { if (p && p->isActive()) { p->update(static_cast(delta), renderer); @@ -705,7 +742,9 @@ namespace ZL sparkEmitter.update(static_cast(delta)); projectileEmitter.update(static_cast(delta)); -//#endif + + uiManager.update(static_cast(delta)); + //#endif lastTickCount = newTickCount; } } @@ -763,7 +802,7 @@ namespace ZL uiManager.onMouseDown(uiX, uiY); bool uiHandled = false; -//#ifndef SIMPLIFIED + //#ifndef SIMPLIFIED if (event.button.button == SDL_BUTTON_LEFT && !uiManager.isUiInteraction()) { uint64_t now = SDL_GetTicks64(); if (now - lastProjectileFireTime >= static_cast(projectileCooldownMs)) { @@ -771,7 +810,7 @@ namespace ZL fireProjectiles(); } } -//#endif + //#endif for (const auto& button : uiManager.findButton("") ? std::vector>{} : std::vector>{}) { (void)button; } diff --git a/src/UiManager.cpp b/src/UiManager.cpp index 33d7a82..5187433 100644 --- a/src/UiManager.cpp +++ b/src/UiManager.cpp @@ -8,6 +8,17 @@ namespace ZL { 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() { mesh.data.PositionData.clear(); mesh.data.TexCoordData.clear(); @@ -52,6 +63,10 @@ namespace ZL { static const std::string vTexCoordName = "vTexCoord"; 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.EnableVertexAttribArray(vPositionName); renderer.EnableVertexAttribArray(vTexCoordName); @@ -61,6 +76,7 @@ namespace ZL { renderer.DisableVertexAttribArray(vPositionName); renderer.DisableVertexAttribArray(vTexCoordName); + renderer.PopMatrix(); } void UiSlider::buildTrackMesh() { @@ -191,6 +207,8 @@ namespace ZL { sliders.clear(); collectButtonsAndSliders(root); + nodeActiveAnims.clear(); + for (auto& b : buttons) { b->buildMesh(); } @@ -278,6 +296,37 @@ namespace ZL { 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(); + 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::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(); + s.toY = step["to"][1].get(); + } + if (step.contains("duration")) { + s.durationMs = step["duration"].get() * 1000.0f; + } + if (step.contains("easing") && step["easing"].is_string()) { + s.easing = step["easing"].get(); + 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()) { for (const auto& ch : j["children"]) { node->children.push_back(parseNode(ch, renderer, zipFile)); @@ -406,13 +455,27 @@ namespace ZL { prev.pressedSlider = pressedSlider; prev.path = ""; + prev.animCallbacks = animCallbacks; + 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); menuStack.push_back(std::move(prev)); return true; } catch (const std::exception& e) { std::cerr << "UiManager: pushMenuFromFile failed to load " << path << " : " << e.what() << std::endl; + animCallbacks = prev.animCallbacks; return false; } } @@ -425,19 +488,32 @@ namespace ZL { auto s = menuStack.back(); menuStack.pop_back(); + nodeActiveAnims.clear(); + root = s.root; buttons = s.buttons; sliders = s.sliders; pressedButton = s.pressedButton; pressedSlider = s.pressedSlider; + animCallbacks = s.animCallbacks; + for (auto& b : buttons) { - if (b) b->buildMesh(); - } - for (auto& sl : sliders) { - if (sl) { sl->buildTrackMesh(); sl->buildKnobMesh(); } + if (b) { + b->animOffsetX = 0.0f; + b->animOffsetY = 0.0f; + b->animScaleX = 1.0f; + b->animScaleY = 1.0f; + b->buildMesh(); + } } + for (auto& sl : sliders) { + if (sl) { + sl->buildTrackMesh(); + sl->buildKnobMesh(); + } + } return true; } @@ -461,6 +537,168 @@ namespace ZL { renderer.PopProjectionMatrix(); } + static std::shared_ptr findNodeByName(const std::shared_ptr& 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, size_t>> animationsToRemove; + std::vector> 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) { for (auto& b : buttons) { if (b->rect.contains((float)x, (float)y)) { @@ -540,4 +778,88 @@ namespace ZL { 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&)> traverse = [&](const std::shared_ptr& 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 cb) { + animCallbacks[{nodeName, animName}] = std::move(cb); + return true; + } + } // namespace ZL \ No newline at end of file diff --git a/src/UiManager.h b/src/UiManager.h index dfee5aa..5891566 100644 --- a/src/UiManager.h +++ b/src/UiManager.h @@ -8,6 +8,7 @@ #include #include #include +#include namespace ZL { @@ -41,6 +42,12 @@ namespace ZL { std::function onClick; + // animation runtime + float animOffsetX = 0.0f; + float animOffsetY = 0.0f; + float animScaleX = 1.0f; + float animScaleY = 1.0f; + void buildMesh(); void draw(Renderer& renderer) const; }; @@ -73,6 +80,19 @@ namespace ZL { std::shared_ptr slider; std::string orientation = "vertical"; 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 steps; + bool repeat = false; + }; + std::map animations; }; class UiManager { @@ -91,6 +111,19 @@ namespace ZL { 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 findButton(const std::string& name); bool setButtonCallback(const std::string& name, std::function cb); @@ -106,15 +139,47 @@ namespace ZL { bool popMenu(); 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 cb); + private: std::shared_ptr parseNode(const json& j, Renderer& renderer, const std::string& zipFile); void layoutNode(const std::shared_ptr& node); void collectButtonsAndSliders(const std::shared_ptr& 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 onComplete; + + float origOffsetX = 0.0f; + float origOffsetY = 0.0f; + float origScaleX = 1.0f; + float origScaleY = 1.0f; + bool stepStarted = false; + }; + std::shared_ptr root; std::vector> buttons; std::vector> sliders; + std::map, std::vector> nodeActiveAnims; + std::map, std::function> animCallbacks; // key: (nodeName, animName) + std::shared_ptr pressedButton; std::shared_ptr pressedSlider; @@ -125,6 +190,7 @@ namespace ZL { std::shared_ptr pressedButton; std::shared_ptr pressedSlider; std::string path; + std::map, std::function> animCallbacks; }; std::vector menuStack;