diff --git a/CMakeLists.txt b/CMakeLists.txt index a559360..46c0297 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,9 +46,6 @@ set(EXTERNAL_SOURCES # ImAnim external/ImAnim/im_anim.cpp - external/ImAnim/im_anim_demo.cpp - external/ImAnim/im_anim_doc.cpp - external/ImAnim/im_anim_usecase.cpp # ImPlot external/implot/implot.cpp diff --git a/src/application.cpp b/src/application.cpp index 98ad4e6..f2b975e 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -807,10 +807,10 @@ void Application::renderMenuBar() { const auto viewport = (ImGuiViewportP*)ImGui::GetMainViewport(); constexpr auto flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar; - constexpr float framePaddingY = 4.0f; - constexpr float itemHeight = 24.0f; - constexpr float barHeight = itemHeight + 2.0f; - constexpr ImVec2 size = { itemHeight, itemHeight }; + const float framePaddingY = 4.0f; + const float itemHeight = ImGui::GetFontSize() + framePaddingY * 2.0f; + const float barHeight = itemHeight + 2.0f; + const ImVec2 size = { itemHeight, itemHeight }; ImGui::PushStyleColor(ImGuiCol_Button, 0x00000000); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, AppColors::DarkGray); @@ -1118,9 +1118,47 @@ void Application::renderPreferences() { void Application::renderPerformanceWindow() { if (ImGui::Begin("Performance", &m_performanceWindowOpen)) { ImGui::SeparatorText("Application"); - ImGui::Text("FPS: %.1f", ImGui::GetIO().Framerate); - ImGui::Text("Delta Time: %.3f ms", m_deltaTime * 1000.0f); - ImGui::Text("Frame Time: %.3f ms", ImGui::GetIO().DeltaTime * 1000.0f); + const auto& io = ImGui::GetIO(); + const auto fps = io.Framerate; + const auto dtMs = m_deltaTime * 1000.0f; + const auto ftMs = io.DeltaTime * 1000.0f; + + ImGui::Text("FPS: %.1f", fps); + ImGui::Text("Delta Time: %.3f ms", dtMs); + ImGui::Text("Frame Time: %.3f ms", ftMs); + + if (m_measurement_accum >= 0.02f) { + m_fps_buffer.push(fps); + m_ft_buffer.push(ftMs); + m_measurement_accum -= 0.02f; + } + m_measurement_accum += m_deltaTime; + + ImGui::SliderFloat("History", &m_history_secs, 1.0f, 30.0f, "%.0f s"); + + const int maxSamples = static_cast(m_history_secs / 0.02f); + + if (ImPlot::BeginPlot("##FPS", { -1, 150 })) { + const int fpsCount = static_cast(m_fps_buffer.size()); + const int fpsOffset = static_cast(m_fps_buffer.offset()); + ImPlot::SetupAxes(nullptr, "FPS", ImPlotAxisFlags_NoTickLabels, ImPlotAxisFlags_AutoFit); + ImPlot::SetupAxisLimits(ImAxis_X1, 0, maxSamples, ImPlotCond_Always); + ImPlotSpec spec; + spec.Offset = fpsOffset; + ImPlot::PlotLine("FPS", m_fps_buffer.data(), fpsCount, 1.0, 0, spec); + ImPlot::EndPlot(); + } + + if (ImPlot::BeginPlot("##FrameTime", {-1, 150})) { + const int ftCount = static_cast(m_ft_buffer.size()); + const int ftOffset = static_cast(m_ft_buffer.offset()); + ImPlot::SetupAxes(nullptr, "ms", ImPlotAxisFlags_NoTickLabels, ImPlotAxisFlags_AutoFit); + ImPlot::SetupAxisLimits(ImAxis_X1, 0, maxSamples, ImPlotCond_Always); + ImPlotSpec spec; + spec.Offset = ftOffset; + ImPlot::PlotLine("Frame Time", m_ft_buffer.data(), ftCount, 1.0, 0, spec); + ImPlot::EndPlot(); + } ImGui::SeparatorText("Current Editor"); m_editor->renderStats(); diff --git a/src/application.h b/src/application.h index 7fe8dbc..6901e4d 100644 --- a/src/application.h +++ b/src/application.h @@ -5,6 +5,7 @@ #include "editor/project_manager.h" #include "gfx/gl_texture.h" #include "version.h" +#include "util/circular_buffer.h" #include #include @@ -253,6 +254,13 @@ class Application { std::string m_extractedBinaryPath; float m_idleAccumulator = 0.0f; // accumulate time to occasionally update particles while unfocused + + // Performance Metrics + float m_history_secs = 10.0f; + float m_last_measurement = 0.0f; + float m_measurement_accum = 0.0f; + CircularBuffer m_fps_buffer; + CircularBuffer m_ft_buffer; }; inline Application* g_application = nullptr; diff --git a/src/editor/editor.cpp b/src/editor/editor.cpp index d619763..581310e 100644 --- a/src/editor/editor.cpp +++ b/src/editor/editor.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -42,7 +43,7 @@ constexpr std::array s_emitterSpawnTypes = { } -Editor::Editor() : m_xAnimBuffer(), m_yAnimBuffer() { +Editor::Editor() : m_xAnimBuffer(), m_yAnimBuffer1() { m_gridRenderer = std::make_shared(s_gridDimensions, s_gridSpacing); m_debugRenderer = std::make_unique(1000); m_collisionGridRenderer = std::make_shared(s_gridDimensions / 2, s_gridSpacing); @@ -275,15 +276,44 @@ void Editor::renderStats() { const auto fraction = static_cast(activeParticles) / maxParticles; const auto particleText = fmt::format("Particles: {}/{}", activeParticles, maxParticles); - constexpr auto colorLow = glm::vec4(0.0f, 1.0f, 0.0f, 1.0f); - constexpr auto colorHigh = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); - const auto color = glm::mix(colorLow, colorHigh, fraction); + constexpr u32 colorLow = IM_COL32(0, 200, 0, 255); + constexpr u32 colorHigh = IM_COL32(220, 0, 0, 255); + ImGui::AnimatedStatBar("##particleBar", fraction, particleText.c_str(), colorLow, colorHigh); - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4{ color.r, color.g, color.b, color.a }); - ImGui::ProgressBar(fraction, ImVec2(0.0f, 0.0f), particleText.c_str()); - ImGui::PopStyleColor(); - - ImGui::Text("Active Emitters: %" PRIu64, system.getEmitters().size()); + const auto emitterCount = system.getEmitters().size(); + const float dt = ImGui::GetIO().DeltaTime; + const ImGuiID flashId = ImGui::GetID("##emitterFlash"); + + if (emitterCount != m_lastEmitterCount) { + m_emitterFlashColor = emitterCount > m_lastEmitterCount ? AppColors::LightGreen : AppColors::LightRed; + iam_tween_float(flashId, 0, 1.0f, 0.0f, iam_ease_preset(iam_ease_linear), iam_policy_cut, dt, 0.0f); + m_lastEmitterCount = emitterCount; + } + + const float flash = iam_tween_float( + flashId, 0, 0.0f, 0.45f, + iam_ease_preset(iam_ease_out_cubic), + iam_policy_crossfade, dt, 0.0f + ); + + const auto emitterText = fmt::format("Active Emitters: {}", emitterCount); + if (flash > 0.001f && m_emitterFlashColor != 0) { + const auto& style = ImGui::GetStyle(); + const ImVec2 p = ImGui::GetCursorScreenPos(); + const ImVec2 ts = ImGui::CalcTextSize(emitterText.c_str()); + const ImVec2 pad = { style.FramePadding.x, style.FramePadding.y * 0.5f }; + + ImVec4 hl = ImGui::ColorConvertU32ToFloat4(m_emitterFlashColor); + hl.w = flash * 0.35f; + ImGui::GetWindowDrawList()->AddRectFilled( + p - pad, + { p.x + ts.x + pad.x, p.y + ts.y + pad.y }, + ImGui::ColorConvertFloat4ToU32(hl), + style.FrameRounding + ); + } + + ImGui::TextUnformatted(emitterText.c_str()); } void Editor::openPicker() { @@ -327,12 +357,14 @@ void Editor::openSettings() { m_settingsOpen = true; ImGui::PushOverrideID(m_settingsWindowId); ImGui::OpenPopup("Settings##Editor"); + ImGui::ResetPopupFade("Settings##Editor"); ImGui::PopID(); } void Editor::openTutorial() { ImGui::PushOverrideID(m_tutorialWindowId); ImGui::OpenPopup("##ViewportTutorial"); + ImGui::ResetPopupFade("##ViewportTutorial"); ImGui::PopID(); } @@ -592,6 +624,11 @@ void Editor::selectResource(u64 editorID, size_t resourceIndex) { void Editor::renderResourcePicker() { if (ImGui::Begin("Resource Picker##Editor", &m_pickerOpen)) { + const auto& style = ImGui::GetStyle(); + constexpr float thumbnailSize = 48.0f; + constexpr ImVec2 thumbnailSizeVec = { thumbnailSize, thumbnailSize }; + const float itemHeight = thumbnailSize + style.FramePadding.y * 2; + const auto& editor = g_projectManager->getActiveEditor(); if (!editor) { ImGui::Text("No editor open"); @@ -618,14 +655,33 @@ void Editor::renderResourcePicker() { const auto contentRegion = ImGui::GetContentRegionAvail(); if (ImGui::BeginListBox("##Resources", contentRegion)) { - const ImGuiStyle& style = ImGui::GetStyle(); const float itemWidth = contentRegion.x - (style.ItemSpacing.x + style.WindowPadding.x) * 1.3f; + const float dt = ImGui::GetIO().DeltaTime; + + // Detect selection changes + const size_t selected = m_selectedResources[id]; + const ImGuiID accentId = ImGui::GetID("##pickerAccent"); + bool selectionChanged = false; + if (m_lastPickerSelection[id] != selected) { + m_lastPickerSelection[id] = selected; + selectionChanged = true; + iam_tween_float(accentId, 0, 0.0f, 0.0f, iam_ease_preset(iam_ease_linear), iam_policy_cut, dt, 0.0f); + } + + const float accentGrow = iam_tween_float( + accentId, 0, + selected != (size_t)-1 ? 1.0f : 0.0f, 0.3f, + iam_ease_preset(iam_ease_out_cubic), + iam_policy_crossfade, dt, 0.0f + ); for (size_t i = 0; i < resources.size(); ++i) { const auto& resource = resources[i]; const auto& texture = textures[resource.header.misc.textureIndex]; ImGui::PushID(i); + const auto itemId = ImGui::GetID("##Resource"); + const auto name = fmt::format("[{}] Tex {}x{}", i, texture.width, texture.height); auto bgColor = m_selectedResources[id] == i @@ -633,13 +689,15 @@ void Editor::renderResourcePicker() { : style.Colors[ImGuiCol_Button]; const auto cursor = ImGui::GetCursorScreenPos(); - if (ImGui::InvisibleButton("##Resource", { itemWidth, 32 })) { + if (ImGui::InvisibleButton("##Resource", { itemWidth, itemHeight })) { m_selectedResources[id] = i; editor->notifyResourceChanged(i); } - if (ImGui::IsItemHovered()) { - bgColor = style.Colors[ImGuiCol_ButtonHovered]; + const bool hovered = ImGui::IsItemHovered(); + const bool pressed = ImGui::IsItemActive(); + + if (hovered) { anyHovered = true; if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { @@ -647,6 +705,22 @@ void Editor::renderResourcePicker() { } } + const auto targetColor = pressed + ? style.Colors[ImGuiCol_SliderGrabActive] + : hovered ? style.Colors[ImGuiCol_ButtonHovered] : bgColor; + + bgColor = iam_tween_color( + itemId, + 1, + targetColor, + 0.2f, + iam_ease_preset(iam_ease_linear), + iam_policy_crossfade, + iam_col_srgb, + dt, + bgColor + ); + const auto bgColor2 = bgColor * ImVec4(0.8f, 0.8f, 0.8f, 1.0f); // Draw a filled rectangle behind the item @@ -654,7 +728,7 @@ void Editor::renderResourcePicker() { const int firstVtx = drawList->VtxBuffer.Size; drawList->AddRectFilled( cursor, - { cursor.x + itemWidth, cursor.y + 32 }, + { cursor.x + itemWidth, cursor.y + itemHeight }, ImGui::ColorConvertFloat4ToU32(bgColor), style.FrameRounding ); @@ -662,19 +736,71 @@ void Editor::renderResourcePicker() { ImGui::ShadeVertsLinearColorGradientKeepAlpha(drawList, firstVtx, lastVtx, - cursor, { cursor.x, cursor.y + 32 }, - ImGui::ColorConvertFloat4ToU32(bgColor), + cursor, { cursor.x, cursor.y + itemHeight }, + ImGui::ColorConvertFloat4ToU32(bgColor), ImGui::ColorConvertFloat4ToU32(bgColor2) ); - ImGui::SetCursorScreenPos(cursor); - ImGui::Image((ImTextureID)(uintptr_t)texture.glTexture->getHandle(), { 32, 32 }); + // Selection accent bar on the left edge, eased in on selection change + if (m_selectedResources[id] == i && accentGrow > 0.001f) { + const float barHeight = itemHeight * accentGrow; + const float barTop = cursor.y + (itemHeight - barHeight) * 0.5f; + ImVec4 accent = ImGui::ColorConvertU32ToFloat4(AppColors::LightBlue); + accent.w = accentGrow; + drawList->AddRectFilled( + { cursor.x, barTop }, + { cursor.x + 3.0f, barTop + barHeight }, + ImGui::ColorConvertFloat4ToU32(accent), + 1.5f + ); + + if (selectionChanged) { + const float itemContentY = (cursor.y - ImGui::GetWindowPos().y) + ImGui::GetScrollY(); + const float targetScroll = itemContentY - ImGui::GetWindowHeight() * 0.5f + itemHeight * 0.5f; + iam_scroll_to_y(targetScroll, 0.25f); + } + } + + const ImVec2 offset = { + iam_tween_float( + itemId, + 0, + hovered ? 8.0f : 0.0f, + 0.25f, + iam_ease_preset(iam_ease_in_out_quad), + iam_policy_crossfade, + dt + ), + 0.0f + }; + + const auto imagePos = cursor + style.FramePadding + offset; + const auto frameMinPos = imagePos - ImVec2(2, 2); + const auto frameMaxPos = imagePos + ImVec2(thumbnailSize + 2, thumbnailSize + 2); + drawList->AddRectFilled( + frameMinPos, + frameMaxPos, + IM_COL32(60, 60, 60, 255), + 2 + ); + + drawList->AddRect( + frameMinPos, + frameMaxPos, + IM_COL32(230, 230, 230, 30), + 2 + ); + + ImGui::SetCursorScreenPos(imagePos); + ImGui::Image(texture.glTexture->getHandle(), thumbnailSizeVec); ImGui::SameLine(); + ImGui::PushFont(nullptr, Application::getFontSizeLarge()); const auto textHeight = ImGui::GetFontSize(); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (32 - textHeight) / 2.0f); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (thumbnailSize - textHeight) / 2.0f); ImGui::TextUnformatted(name.c_str()); + ImGui::PopFont(); if (ImGui::BeginPopup("##ResourcePopup")) { if (ImGui::MenuItemIcon(ICON_FA_COPY, "Copy", "Ctrl+C", false, AppColors::LightGreen3)) { @@ -720,6 +846,9 @@ void Editor::renderResourcePicker() { ImGui::EndPopup(); } + ImGui::SetCursorScreenPos(cursor); + ImGui::Dummy({ itemWidth, itemHeight }); + ImGui::PopID(); } @@ -1237,6 +1366,7 @@ void Editor::renderResourceEditor() { void Editor::renderSettings() { ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); ImGui::PushOverrideID(m_settingsWindowId); + ImGui::BeginPopupFade("Settings##Editor"); bool closedThroughButton = false; if (ImGui::BeginPopupModal("Settings##Editor", &m_settingsOpen)) { @@ -1251,6 +1381,10 @@ void Editor::renderSettings() { ImGuiWindowFlags_AlwaysVerticalScrollbar ); + if (ImGui::IsWindowAppearing()) { + iam_scroll_to_top(); + } + ImGui::SeparatorText("General"); ImGui::InputScalar("Max Particles", ImGuiDataType_U32, &m_settings.maxParticles); ImGui::SameLine(); @@ -1343,6 +1477,7 @@ void Editor::renderSettings() { m_settings = m_settingsBackup; } + ImGui::EndPopupFade(); ImGui::PopID(); ImGui::PopStyleVar(); } @@ -1350,6 +1485,7 @@ void Editor::renderSettings() { void Editor::renderTutorial() { ImGui::PushOverrideID(m_tutorialWindowId); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 16, 16 }); + ImGui::BeginPopupFade("##ViewportTutorial"); ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, { 0.5f, 0.5f }); ImGui::SetNextWindowSize({ 900, 0 }, ImGuiCond_Once); @@ -1476,6 +1612,7 @@ void Editor::renderTutorial() { ImGui::EndPopup(); } + ImGui::EndPopupFade(); ImGui::PopStyleVar(); ImGui::PopID(); } @@ -1847,16 +1984,9 @@ void Editor::renderBehaviorEditor(SPLResource& res) { bool Editor::renderGravityBehaviorEditor(const std::shared_ptr& gravity) { LOCK_EDITOR(); static bool hovered = false; - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - ImGui::BeginChild("##gravityEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); + ImGui::BeginHoverBorderChild("##gravityEditor", hovered, s_hoverAccentColor); ImGui::TextUnformatted("Gravity"); - if (hovered) { - ImGui::PopStyleColor(); - } - NOTIFY(ImGui::DragFloat3("Magnitude", glm::value_ptr(gravity->magnitude), 0.001f)); ImGui::EndChild(); @@ -1868,16 +1998,9 @@ bool Editor::renderGravityBehaviorEditor(const std::shared_ptr& random) { LOCK_EDITOR(); static bool hovered = false; - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - ImGui::BeginChild("##randomEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); + ImGui::BeginHoverBorderChild("##randomEditor", hovered, s_hoverAccentColor); ImGui::TextUnformatted("Random"); - if (hovered) { - ImGui::PopStyleColor(); - } - NOTIFY(ImGui::DragFloat3("Magnitude", glm::value_ptr(random->magnitude), 0.001f)); NOTIFY(ImGui::SliderFloat("Apply Interval", &random->applyInterval, 0, 5, "%.3fs", ImGuiSliderFlags_Logarithmic)); @@ -1890,16 +2013,9 @@ bool Editor::renderRandomBehaviorEditor(const std::shared_ptr bool Editor::renderMagnetBehaviorEditor(const std::shared_ptr& magnet) { LOCK_EDITOR(); static bool hovered = false; - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - ImGui::BeginChild("##magnetEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); + ImGui::BeginHoverBorderChild("##magnetEditor", hovered, s_hoverAccentColor); ImGui::TextUnformatted("Magnet"); - if (hovered) { - ImGui::PopStyleColor(); - } - NOTIFY(ImGui::DragFloat3("Target", glm::value_ptr(magnet->target), 0.05f, -5.0f, 5.0f)); NOTIFY(ImGui::SliderFloat("Force", &magnet->force, 0, 5, "%.3f", ImGuiSliderFlags_Logarithmic)); @@ -1912,16 +2028,9 @@ bool Editor::renderMagnetBehaviorEditor(const std::shared_ptr bool Editor::renderSpinBehaviorEditor(const std::shared_ptr& spin) { LOCK_EDITOR(); static bool hovered = false; - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - ImGui::BeginChild("##spinEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); + ImGui::BeginHoverBorderChild("##spinEditor", hovered, s_hoverAccentColor); ImGui::TextUnformatted("Spin"); - if (hovered) { - ImGui::PopStyleColor(); - } - NOTIFY(ImGui::SliderAngle("Angle", &spin->angle, -180.0f, 180.0f)); ImGui::TextUnformatted("Axis"); ImGui::Indent(); @@ -1939,16 +2048,9 @@ bool Editor::renderSpinBehaviorEditor(const std::shared_ptr& sp bool Editor::renderCollisionPlaneBehaviorEditor(const std::shared_ptr& collisionPlane) { LOCK_EDITOR(); static bool hovered = false; - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - ImGui::BeginChild("##collisionPlaneEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); + ImGui::BeginHoverBorderChild("##collisionPlaneEditor", hovered, s_hoverAccentColor); ImGui::TextUnformatted("Collision Plane"); - if (hovered) { - ImGui::PopStyleColor(); - } - NOTIFY(ImGui::DragFloat("Height", &collisionPlane->y, 0.05f)); NOTIFY(ImGui::SliderFloat("Elasticity", &collisionPlane->elasticity, 0, 2, "%.3f", ImGuiSliderFlags_Logarithmic)); ImGui::TextUnformatted("Collision Type"); @@ -1966,16 +2068,9 @@ bool Editor::renderCollisionPlaneBehaviorEditor(const std::shared_ptr& convergence) { LOCK_EDITOR(); static bool hovered = false; - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - ImGui::BeginChild("##convergenceEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); + ImGui::BeginHoverBorderChild("##convergenceEditor", hovered, s_hoverAccentColor); ImGui::TextUnformatted("Convergence"); - if (hovered) { - ImGui::PopStyleColor(); - } - NOTIFY(ImGui::DragFloat3("Target", glm::value_ptr(convergence->target), 0.05f, -5.0f, 5.0f)); NOTIFY(ImGui::SliderFloat("Force", &convergence->force, -5, 5, "%.3f", ImGuiSliderFlags_Logarithmic)); @@ -2054,15 +2149,7 @@ bool Editor::renderScaleAnimEditor(SPLScaleAnim& res) { return false; } - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - - ImGui::BeginChild("##scaleAnimEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); - - if (hovered) { - ImGui::PopStyleColor(); - } + ImGui::BeginHoverBorderChild("##scaleAnimEditor", hovered, s_hoverAccentColor); NOTIFY(ImGui::SliderFloat("Start Scale", &res.start, 0.01f, 10.0f, "%.3f", ImGuiSliderFlags_Logarithmic)); NOTIFY(ImGui::SliderFloat("Mid Scale", &res.mid, 0.01f, 10.0f, "%.3f", ImGuiSliderFlags_Logarithmic)); @@ -2073,10 +2160,10 @@ bool Editor::renderScaleAnimEditor(SPLScaleAnim& res) { NOTIFY(ImGui::SliderScalar("Out", ImGuiDataType_U8, &res.curve.out, &min, &max, "%u")); NOTIFY(ImGui::Checkbox("Loop", &res.flags.loop)); - res.plot(m_xAnimBuffer, m_yAnimBuffer); + res.plot(m_xAnimBuffer, m_yAnimBuffer1); if (ImPlot::BeginPlot("##scaleAnimPlot", {-1, 0}, ImPlotFlags_CanvasOnly)) { ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_AutoFit, ImPlotAxisFlags_AutoFit); - ImPlot::PlotLine("Scale", m_xAnimBuffer.data(), m_yAnimBuffer.data(), m_xAnimBuffer.size()); + ImPlot::PlotLine("Scale", m_xAnimBuffer.data(), m_yAnimBuffer1.data(), m_xAnimBuffer.size()); ImPlot::EndPlot(); } @@ -2106,15 +2193,7 @@ bool Editor::renderColorAnimEditor(const SPLResource& mainRes, SPLColorAnim& res return false; } - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - - ImGui::BeginChild("##colorAnimEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); - - if (hovered) { - ImGui::PopStyleColor(); - } + ImGui::BeginHoverBorderChild("##colorAnimEditor", hovered, s_hoverAccentColor); NOTIFY(ImGui::ColorEdit3("Start Color", glm::value_ptr(res.start))); NOTIFY(ImGui::ColorEdit3("End Color", glm::value_ptr(res.end))); @@ -2223,15 +2302,7 @@ bool Editor::renderAlphaAnimEditor(SPLAlphaAnim& res) { return false; } - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - - ImGui::BeginChild("##alphaAnimEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); - - if (hovered) { - ImGui::PopStyleColor(); - } + ImGui::BeginHoverBorderChild("##alphaAnimEditor", hovered, s_hoverAccentColor); NOTIFY(ImGui::SliderFloat("Start Alpha", &res.alpha.start, 0, 1)); NOTIFY(ImGui::SliderFloat("Mid Alpha", &res.alpha.mid, 0, 1)); @@ -2251,12 +2322,27 @@ bool Editor::renderAlphaAnimEditor(SPLAlphaAnim& res) { NOTIFY(ImGui::SliderFloat("Random Range", &res.flags.randomRange, 0, 1)); NOTIFY(ImGui::Checkbox("Loop", &res.flags.loop)); - res.plot(m_xAnimBuffer, m_yAnimBuffer); + res.plotWith(m_xAnimBuffer, m_yAnimBuffer1, 0.0f); if (ImPlot::BeginPlot("##alphaAnimPlot", {-1, 0}, ImPlotFlags_CanvasOnly | ImPlotFlags_NoInputs)) { ImPlot::SetupAxisLimits(ImAxis_X1, 0, 1); ImPlot::SetupAxisLimits(ImAxis_Y1, minAlpha, maxAlpha); - ImPlot::PlotLine("Alpha", m_xAnimBuffer.data(), m_yAnimBuffer.data(), m_xAnimBuffer.size()); + + if (res.flags.randomRange > 0) { + res.plotWith(m_xAnimBuffer, m_yAnimBuffer2, res.flags.randomRange, 255); + ImPlot::PlotLine("Alpha Base", m_xAnimBuffer.data(), m_yAnimBuffer1.data(), m_xAnimBuffer.size()); + ImPlot::PlotShaded( + "Alpha Variance", + m_xAnimBuffer.data(), + m_yAnimBuffer2.data(), + m_yAnimBuffer1.data(), + m_xAnimBuffer.size(), + ImPlotSpec{ ImPlotProp_FillAlpha, 0.25f } + ); + } else { + ImPlot::PlotLine("Alpha", m_xAnimBuffer.data(), m_yAnimBuffer1.data(), m_xAnimBuffer.size()); + } + ImPlot::EndPlot(); } @@ -2286,15 +2372,7 @@ bool Editor::renderTexAnimEditor(SPLTexAnim& res) { return false; } - if (hovered) { - ImGui::PushStyleColor(ImGuiCol_Border, s_hoverAccentColor); - } - - ImGui::BeginChild("##texAnimEditor", {}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); - - if (hovered) { - ImGui::PopStyleColor(); - } + ImGui::BeginHoverBorderChild("##texAnimEditor", hovered, s_hoverAccentColor); NOTIFY(ImGui::SliderFloat("Step", &res.param.step, 0.01f, 1.0f)); HELP(texAnimStep); diff --git a/src/editor/editor.h b/src/editor/editor.h index 66fedcb..d51bba5 100644 --- a/src/editor/editor.h +++ b/src/editor/editor.h @@ -151,12 +151,20 @@ class Editor { EmitterSpawnType m_emitterSpawnType = EmitterSpawnType::SingleShot; float m_emitterInterval = 1.0f; // seconds + // Stats panel feedback animation + u32 m_emitterFlashColor = 0; + size_t m_lastEmitterCount = 0; + + // Tracks last selection per editor to animate selection changes + std::unordered_map m_lastPickerSelection; + static inline const u32 s_hoverAccentColor = ImGui::ColorConvertFloat4ToU32({ 0.7f, 0.3f, 0.7f, 1.0f }); static constexpr glm::ivec2 s_gridDimensions = { 20, 20 }; static constexpr glm::vec2 s_gridSpacing = { 1.0f, 1.0f }; std::array m_xAnimBuffer; - std::array m_yAnimBuffer; + std::array m_yAnimBuffer1; + std::array m_yAnimBuffer2; TempTexture* m_tempTexture = nullptr; float m_tempTextureScale = 1.0f; diff --git a/src/imgui/extensions.cpp b/src/imgui/extensions.cpp index d9b3437..24efc9c 100644 --- a/src/imgui/extensions.cpp +++ b/src/imgui/extensions.cpp @@ -1,6 +1,7 @@ #include "extensions.h" #include +#include #include namespace ImGui { @@ -16,7 +17,7 @@ static bool IsRootOfOpenMenuSet() const ImGuiPopupData* upper_popup = &g.OpenPopupStack[g.BeginPopupStack.Size]; if (window->DC.NavLayerCurrent != upper_popup->ParentNavLayer) return false; - return upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu) && ImGui::IsWindowChildOf(upper_popup->Window, window, true, false); + return upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu) && IsWindowChildOf(upper_popup->Window, window, true, false); } static bool MenuItemColor(const char* label, const char* icon, const char* shortcut, ImU32 iconTint, bool selected, bool enabled) { @@ -74,8 +75,10 @@ static bool MenuItemColor(const char* label, const char* icon, const char* short RenderText(pos + ImVec2(offsets->OffsetLabel, 0.0f), label); if (icon_w > 0.0f) { + // Make sure all icons are centered in the icon column + const float icon_offset = (offsets->OffsetLabel - offsets->OffsetIcon - icon_w) * 0.5f; if (iconTint != 0) PushStyleColor(ImGuiCol_Text, iconTint); - RenderText(pos + ImVec2(offsets->OffsetIcon, 0.0f), icon); + RenderText(pos + ImVec2(offsets->OffsetIcon + icon_offset, 0.0f), icon); if (iconTint != 0) PopStyleColor(); } if (shortcut_w > 0.0f) @@ -226,25 +229,85 @@ void ImGui::VerticalSeparator(float height) { } bool ImGui::IconButton(const char* icon, const ImVec2& size, ImU32 tint, bool enabled) { - ImGui::BeginDisabled(!enabled); + BeginDisabled(!enabled); + + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) { + EndDisabled(); + return false; + } + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(icon); + + const ImVec2 icon_size = CalcTextSize(icon); + const ImVec2 pos = window->DC.CursorPos; + const ImVec2 item_size = CalcItemSize(size, icon_size.x + style.FramePadding.x * 2.0f, icon_size.y + style.FramePadding.y * 2.0f); + + const ImRect bb(pos, pos + item_size); + ItemSize(item_size, style.FramePadding.y); + if (!ItemAdd(bb, id)) { + EndDisabled(); + return false; + } + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held); + + // Render button frame + const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + RenderNavCursor(bb, id); + RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding); + + if (g.LogEnabled) + LogSetNextTextDecoration("[", "]"); + + // Animate icon scale on hover/active (scales from center) + const float target_scale = (held && hovered) ? 0.9f : hovered ? 1.2f : 1.0f; + const float icon_scale = iam_tween_float( + id, + ImHashStr("icon_scale"), + target_scale, + 0.07f, + iam_ease_preset(iam_ease_linear), + iam_policy_crossfade, + g.IO.DeltaTime, + 1.0f + ); + + const float scaled_font_size = g.FontSizeBase * icon_scale; + const ImVec2 button_center = ImVec2((bb.Min.x + bb.Max.x) * 0.5f, (bb.Min.y + bb.Max.y) * 0.5f); + + // Measure icon at the actual scaled font size for accurate centering + PushFont(g.Font, scaled_font_size); + + const ImVec2 scaled_icon_size = CalcTextSize(icon); + const ImVec2 icon_pos = ImVec2( + ImFloor(button_center.x - scaled_icon_size.x * 0.5f), + ImFloor(button_center.y - scaled_icon_size.y * 0.5f) + ); + if (tint != 0) { if (enabled) { - ImGui::PushStyleColor(ImGuiCol_Text, tint); + PushStyleColor(ImGuiCol_Text, tint); } else { - const auto disabledTint = ImGui::ColorConvertU32ToFloat4(tint) * ImVec4(0.8f, 0.8f, 0.8f, 1.0f); - ImGui::PushStyleColor(ImGuiCol_TextDisabled, disabledTint); + const auto disabledTint = ColorConvertU32ToFloat4(tint) * ImVec4(0.8f, 0.8f, 0.8f, 1.0f); + PushStyleColor(ImGuiCol_Text, disabledTint); } } - const bool clicked = ImGui::Button(icon, size); + window->DrawList->AddText(icon_pos, GetColorU32(ImGuiCol_Text), icon); + + PopFont(); if (tint != 0) { - ImGui::PopStyleColor(); + PopStyleColor(); } - ImGui::EndDisabled(); + EndDisabled(); - return clicked; + return pressed; } bool ImGui::IconButton(const char* icon, const char* text, ImU32 iconTint, bool enabled) { @@ -307,10 +370,37 @@ bool ImGui::IconButton(const char* icon, const char* text, ImU32 iconTint, bool ImVec2 icon_pos = ImVec2(bb.Min.x + style.FramePadding.x, icon_y); ImVec2 text_pos = ImVec2(icon_pos.x + icon_size.x + spacing, text_y); + // Animate icon scale on hover/active + const float target_scale = (held && hovered) ? 0.9f : hovered ? 1.2f : 1.0f; + const float icon_scale = iam_tween_float( + id, + ImHashStr("icon_scale"), + target_scale, + 0.07f, + iam_ease_preset(iam_ease_linear), + iam_policy_crossfade, + g.IO.DeltaTime, + 1.0f + ); + + const float scaled_font_size = g.FontSizeBase * icon_scale; + const ImVec2 icon_center = ImVec2(icon_pos.x + icon_size.x * 0.5f, icon_pos.y + icon_size.y * 0.5f); + + // Measure icon at the actual scaled font size for accurate centering + PushFont(g.Font, scaled_font_size); + + const ImVec2 scaled_icon_size = CalcTextSize(icon); + const ImVec2 scaled_icon_pos = ImVec2( + ImFloor(icon_center.x - scaled_icon_size.x * 0.5f), + ImFloor(icon_center.y - scaled_icon_size.y * 0.5f) + ); + if (iconTint != 0) PushStyleColor(ImGuiCol_Text, iconTint); - window->DrawList->AddText(icon_pos, GetColorU32(ImGuiCol_Text), icon); + window->DrawList->AddText(scaled_icon_pos, GetColorU32(ImGuiCol_Text), icon); if (iconTint != 0) PopStyleColor(); + PopFont(); + window->DrawList->AddText(text_pos, GetColorU32(ImGuiCol_Text), text); //PopStyleVar(); // FramePadding @@ -320,6 +410,115 @@ bool ImGui::IconButton(const char* icon, const char* text, ImU32 iconTint, bool return pressed; } +bool ImGui::BeginHoverBorderChild(const char* str_id, bool hovered, ImU32 accent, ImGuiChildFlags flags) { + const ImGuiID id = GetID(str_id); + const ImVec4 normal = GetStyleColorVec4(ImGuiCol_Border); + const ImVec4 target = hovered ? ColorConvertU32ToFloat4(accent) : normal; + + const ImVec4 col = iam_tween_color( + id, 0, target, 0.18f, + iam_ease_preset(iam_ease_out_cubic), + iam_policy_crossfade, iam_col_oklab, + GetIO().DeltaTime, normal + ); + + PushStyleColor(ImGuiCol_Border, col); + const bool open = BeginChild(str_id, ImVec2(0, 0), flags); + PopStyleColor(); + return open; +} + +void ImGui::AnimatedStatBar(const char* str_id, float fraction, const char* overlay, ImU32 lowColor, ImU32 highColor) { + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(str_id); + const float dt = g.IO.DeltaTime; + + const ImVec2 pos = window->DC.CursorPos; + const ImVec2 size = CalcItemSize(ImVec2(0.0f, 0.0f), CalcItemWidth(), g.FontSize + style.FramePadding.y * 2.0f); + const ImRect bb(pos, pos + size); + ItemSize(size, style.FramePadding.y); + if (!ItemAdd(bb, id)) + return; + + fraction = ImSaturate(fraction); + + // Ease the displayed fraction so the fill glides instead of snapping each frame + const float shown = ImSaturate(iam_tween_float( + id, 0, fraction, 0.25f, + iam_ease_preset(iam_ease_out_cubic), + iam_policy_crossfade, dt, fraction + )); + + // Blend low -> high in OKLAB based on the eased value + ImVec4 fill = iam_get_blended_color( + ColorConvertU32ToFloat4(lowColor), + ColorConvertU32ToFloat4(highColor), + shown, iam_col_oklab + ); + + // Gentle brightness pulse when near full + if (fraction > 0.9f) { + const float pulse = iam_oscillate(id, 0.10f, 2.0f, iam_wave_sine, 0.0f, dt); + fill.x = ImSaturate(fill.x + pulse); + fill.y = ImSaturate(fill.y + pulse); + fill.z = ImSaturate(fill.z + pulse); + } + + RenderFrame(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); + + const float fillWidth = shown * bb.GetWidth(); + if (fillWidth > 0.0f) { + const ImVec2 fillMax(bb.Min.x + fillWidth, bb.Max.y); + const ImU32 fillTop = ColorConvertFloat4ToU32(fill); + const ImU32 fillBot = ColorConvertFloat4ToU32(fill * ImVec4(0.8f, 0.8f, 0.8f, 1.0f)); + const int v0 = window->DrawList->VtxBuffer.Size; + window->DrawList->AddRectFilled(bb.Min, fillMax, fillTop, style.FrameRounding); + const int v1 = window->DrawList->VtxBuffer.Size; + ShadeVertsLinearColorGradientKeepAlpha(window->DrawList, v0, v1, bb.Min, bb.GetBL(), fillTop, fillBot); + } + + if (overlay && overlay[0]) { + const ImVec2 overlaySize = CalcTextSize(overlay, NULL); + RenderTextClipped(bb.Min, bb.Max, overlay, NULL, &overlaySize, ImVec2(0.5f, 0.5f), &bb); + } +} + +void ImGui::BeginPopupFade(const char* str_id) { + ImGuiContext& g = *GImGui; + const ImGuiID id = GetID(str_id); + const bool open = IsPopupOpen(str_id, ImGuiPopupFlags_None); + + float a = 1.0f; + if (open) { + a = iam_tween_float( + id, 0, 1.0f, 0.15f, + iam_ease_preset(iam_ease_out_cubic), + iam_policy_crossfade, g.IO.DeltaTime, 0.0f + ); + } + + PushStyleVar(ImGuiStyleVar_Alpha, a * g.Style.Alpha); +} + +void ImGui::EndPopupFade() { + PopStyleVar(); +} + +void ImGui::ResetPopupFade(const char* str_id) { + const ImGuiID id = GetID(str_id); + // Snap the channel to 0 so the next BeginPopupFade fades in from fully transparent + iam_tween_float( + id, 0, 0.0f, 0.0f, + iam_ease_preset(iam_ease_linear), + iam_policy_cut, GetIO().DeltaTime, 0.0f + ); +} + bool ImGui::CenteredButton(const char* label, float alignment) { const ImGuiStyle& style = GetStyle(); diff --git a/src/imgui/extensions.h b/src/imgui/extensions.h index d97b941..c51c810 100644 --- a/src/imgui/extensions.h +++ b/src/imgui/extensions.h @@ -5,6 +5,8 @@ namespace ImGui { +inline void PushID(size_t id) { PushID(reinterpret_cast(id)); } + bool GradientButton(const char* label, const ImVec2& size, ImU32 textColor, ImU32 bgColor, ImU32 bgColor2); bool RedButton(const char* label, const ImVec2& size = { 0.0f, 0.0f }); bool GreenButton(const char* label, const ImVec2& size = { 0.0f, 0.0f }); @@ -18,6 +20,14 @@ bool PaddedTreeNode(const char* label, const ImVec2& padding, ImGuiTreeNodeFlags void VerticalSeparator(float height); +bool BeginHoverBorderChild(const char* str_id, bool hovered, ImU32 accent, ImGuiChildFlags flags = ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); + +void AnimatedStatBar(const char* str_id, float fraction, const char* overlay, ImU32 lowColor, ImU32 highColor); + +void BeginPopupFade(const char* str_id); +void EndPopupFade(); +void ResetPopupFade(const char* str_id); + bool IconButton(const char* icon, const ImVec2& size = {}, ImU32 tint = 0, bool enabled = true); bool IconButton(const char* icon, const char* text, ImU32 iconTint = 0, bool enabled = true); diff --git a/src/spl/spl_anim.cpp b/src/spl/spl_anim.cpp index 3ada538..1bb703c 100644 --- a/src/spl/spl_anim.cpp +++ b/src/spl/spl_anim.cpp @@ -108,6 +108,10 @@ void SPLAlphaAnim::apply(SPLParticle& ptcl, const SPLResource& resource, f32 lif } void SPLAlphaAnim::plot(std::span xs, std::span ys) const { + plotWith(xs, ys, flags.randomRange); +} + +void SPLAlphaAnim::plotWith(std::span xs, std::span ys, float variance) const { const size_t samples = std::min(xs.size(), ys.size()); for (size_t i = 0; i < samples; i++) { const f32 lifeRate = (f32)i / (f32)samples; @@ -115,13 +119,35 @@ void SPLAlphaAnim::plot(std::span xs, std::span ys) const { if (lifeRate < curve.getIn()) { ys[i] = glm::mix(alpha.start, alpha.mid, lifeRate / curve.getIn()); - } else if (lifeRate < curve.getOut()) { + } + else if (lifeRate < curve.getOut()) { ys[i] = alpha.mid; - } else { + } + else { + ys[i] = glm::mix(alpha.mid, alpha.end, (lifeRate - curve.getOut()) / (1.0f - curve.getOut())); + } + + ys[i] = SPLRandom::scaledRange(ys[i], variance); + } +} + +void SPLAlphaAnim::plotWith(std::span xs, std::span ys, float variance, u8 factor) const { + const size_t samples = std::min(xs.size(), ys.size()); + for (size_t i = 0; i < samples; i++) { + const f32 lifeRate = (f32)i / (f32)samples; + xs[i] = lifeRate; + + if (lifeRate < curve.getIn()) { + ys[i] = glm::mix(alpha.start, alpha.mid, lifeRate / curve.getIn()); + } + else if (lifeRate < curve.getOut()) { + ys[i] = alpha.mid; + } + else { ys[i] = glm::mix(alpha.mid, alpha.end, (lifeRate - curve.getOut()) / (1.0f - curve.getOut())); } - ys[i] = SPLRandom::scaledRange(ys[i], flags.randomRange); + ys[i] = SPLRandom::scaledRange(ys[i], variance, factor); } } diff --git a/src/spl/spl_random.h b/src/spl/spl_random.h index 812c5cb..1db8864 100644 --- a/src/spl/spl_random.h +++ b/src/spl/spl_random.h @@ -53,10 +53,7 @@ class SPLRandom { // So the value range is [n * (1 - variance / 2), n * (1 + variance / 2)] static f32 scaledRange(f32 n, f32 variance) { #if SPL_ACCURATE_RANDOM - const fx32 nx = FX_F32_TO_FX32(n); - const fx32 range = (fx32)(variance * 255.0f); - const fx32 v = (nx * (255 - ((range * (fx32)nextU32(8)) >> 8))) >> 8; - return FX_FX32_TO_F32(v); + return scaledRange(n, variance, nextU32(8)); #else variance = glm::clamp(variance, 0.0f, 1.0f); const f32 min = n * (1.0f - variance / 2.0f); @@ -68,10 +65,7 @@ class SPLRandom { // Generates a random float in the range [n, n * (1 + variance)] static f32 scaledRange2(f32 n, f32 variance) { #if SPL_ACCURATE_RANDOM - const fx32 nx = FX_F32_TO_FX32(n); - const fx32 range = (fx32)(variance * 255.0f); - const fx32 v = (nx * (255 + range - ((range * (fx32)nextU32(8)) >> 7))) >> 8; - return FX_FX32_TO_F32(v); + return scaledRange2(n, variance, nextU32(8)); #else const f32 min = n; const f32 max = n * (1.0f + variance); @@ -80,6 +74,22 @@ class SPLRandom { #endif } + // Deterministic version of scaledRange + static f32 scaledRange(f32 n, f32 variance, u32 factor) { + const fx32 nx = FX_F32_TO_FX32(n); + const fx32 range = (fx32)(variance * 255.0f); + const fx32 v = (nx * (255 - ((range * (fx32)factor) >> 8))) >> 8; + return FX_FX32_TO_F32(v); + } + + // Deterministic version of scaledRange2 + static f32 scaledRange2(f32 n, f32 variance, u32 factor) { + const fx32 nx = FX_F32_TO_FX32(n); + const fx32 range = (fx32)(variance * 255.0f); + const fx32 v = (nx * (255 + range - ((range * (fx32)factor) >> 7))) >> 8; + return FX_FX32_TO_F32(v); + } + static f32 range(f32 min, f32 max) { return min + nextF32() * (max - min); } diff --git a/src/spl/spl_resource.h b/src/spl/spl_resource.h index c7fb48b..ffbc012 100644 --- a/src/spl/spl_resource.h +++ b/src/spl/spl_resource.h @@ -474,6 +474,8 @@ struct SPLAlphaAnim final : SPLAnim { void apply(SPLParticle& ptcl, const SPLResource& resource, f32 lifeRate) const override; void plot(std::span xs, std::span ys) const; + void plotWith(std::span xs, std::span ys, float variance) const; + void plotWith(std::span xs, std::span ys, float variance, u8 factor) const; static SPLAlphaAnim createDefault() { return SPLAlphaAnim( diff --git a/src/util/circular_buffer.h b/src/util/circular_buffer.h new file mode 100644 index 0000000..d0c0f18 --- /dev/null +++ b/src/util/circular_buffer.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include + +template +class CircularBuffer { +public: + using container_type = std::vector; + using value_type = T; + using size_type = std::size_t; + using difference_type = container_type::difference_type; + using reference = container_type::reference; + using const_reference = container_type::const_reference; + using pointer = container_type::pointer; + using const_pointer = container_type::const_pointer; + + CircularBuffer() = default; + CircularBuffer(std::initializer_list values) : m_data{ std::move(values) } {} + explicit CircularBuffer(size_type count, const_reference value = value_type{}) : m_data(count, value) {} + CircularBuffer(const CircularBuffer&) = default; + CircularBuffer(CircularBuffer&&) = default; + CircularBuffer& operator=(const CircularBuffer&) = default; + CircularBuffer& operator=(CircularBuffer&&) = default; + + void push(const_reference v) { + if (size() < capacity()) { + m_data.push_back(v); + } else { + m_data[m_offset] = v; + m_offset = (m_offset + 1) % capacity(); + } + } + + template + void push(X&& v) { + if (size() < capacity()) { + m_data.emplace_back(std::forward(v)); + } + else { + m_data[m_offset] = v; + m_offset = (m_offset + 1) % capacity(); + } + } + + size_type size() const { + return m_data.size(); + } + + static constexpr size_type capacity() { + return N; + } + + const_pointer data() const { + return m_data.data(); + } + + pointer data() { + return m_data.data(); + } + + size_type offset() const { + return m_offset; + } + +private: + std::vector m_data; + size_type m_offset = 0; +};