From be772481f63ec523d4791afa6c9eafe846f38366 Mon Sep 17 00:00:00 2001 From: shweet Date: Tue, 18 Nov 2025 00:56:32 -0500 Subject: [PATCH] ....that! --- src/anm2/frame.h | 2 +- src/clipboard.cpp | 12 +- src/clipboard.h | 1 + src/imgui/documents.cpp | 8 + src/imgui/imgui_.cpp | 34 +++ src/imgui/taskbar.cpp | 10 +- src/imgui/window/animation_preview.cpp | 274 ++++++++++++++++-------- src/imgui/window/animation_preview.h | 5 + src/imgui/window/onionskin.cpp | 9 - src/imgui/window/spritesheet_editor.cpp | 41 +++- src/imgui/window/spritesheet_editor.h | 5 + src/imgui/window/timeline.cpp | 46 ++-- src/imgui/window/tools.cpp | 6 +- src/loader.cpp | 7 +- src/settings.h | 2 +- src/snapshots.cpp | 48 ++++- src/snapshots.h | 17 +- src/types.h | 9 - 18 files changed, 374 insertions(+), 162 deletions(-) diff --git a/src/anm2/frame.h b/src/anm2/frame.h index db3d262..9319113 100644 --- a/src/anm2/frame.h +++ b/src/anm2/frame.h @@ -10,7 +10,7 @@ namespace anm2ed::anm2 { constexpr auto FRAME_DURATION_MIN = 1; - constexpr auto FRAME_DURATION_MAX = 100000; + constexpr auto FRAME_DURATION_MAX = 1000000; #define MEMBERS \ X(isVisible, bool, true) \ diff --git a/src/clipboard.cpp b/src/clipboard.cpp index e66150d..4aa232d 100644 --- a/src/clipboard.cpp +++ b/src/clipboard.cpp @@ -4,6 +4,8 @@ namespace anm2ed { + Clipboard::Clipboard() { set(""); } + std::string Clipboard::get() { auto text = SDL_GetClipboardText(); @@ -13,13 +15,7 @@ namespace anm2ed return string; } - bool Clipboard::is_empty() - { - return get().empty(); - } + bool Clipboard::is_empty() { return get().empty(); } - void Clipboard::set(const std::string& string) - { - SDL_SetClipboardText(string.data()); - } + void Clipboard::set(const std::string& string) { SDL_SetClipboardText(string.data()); } } \ No newline at end of file diff --git a/src/clipboard.h b/src/clipboard.h index 9e8ec8b..12a7065 100644 --- a/src/clipboard.h +++ b/src/clipboard.h @@ -7,6 +7,7 @@ namespace anm2ed class Clipboard { public: + Clipboard(); bool is_empty(); std::string get(); void set(const std::string&); diff --git a/src/imgui/documents.cpp b/src/imgui/documents.cpp index 6af9d40..748fab4 100644 --- a/src/imgui/documents.cpp +++ b/src/imgui/documents.cpp @@ -14,10 +14,17 @@ namespace anm2ed::imgui { auto viewport = ImGui::GetMainViewport(); auto windowHeight = ImGui::GetFrameHeightWithSpacing(); + bool isLightTheme = settings.theme == theme::LIGHT; + bool pushedStyle = false; ImGui::SetNextWindowViewport(viewport->ID); ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + taskbar.height)); ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, windowHeight)); + if (isLightTheme) + { + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImGui::GetStyleColorVec4(ImGuiCol_TitleBgActive)); + pushedStyle = true; + } for (auto& document : manager.documents) { @@ -159,6 +166,7 @@ namespace anm2ed::imgui } ImGui::End(); + if (pushedStyle) ImGui::PopStyleColor(); if (manager.isAnm2DragDrop) { diff --git a/src/imgui/imgui_.cpp b/src/imgui/imgui_.cpp index ebfd1e4..ee84858 100644 --- a/src/imgui/imgui_.cpp +++ b/src/imgui/imgui_.cpp @@ -12,6 +12,20 @@ using namespace glm; namespace anm2ed::imgui { + constexpr ImVec4 COLOR_LIGHT_BUTTON{0.98f, 0.98f, 0.98f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TITLE_BG{0.78f, 0.78f, 0.78f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TITLE_BG_ACTIVE{0.64f, 0.64f, 0.64f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TITLE_BG_COLLAPSED{0.74f, 0.74f, 0.74f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TABLE_HEADER{0.78f, 0.78f, 0.78f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TAB{0.74f, 0.74f, 0.74f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TAB_HOVERED{0.82f, 0.82f, 0.82f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TAB_SELECTED{0.92f, 0.92f, 0.92f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TAB_DIMMED{0.70f, 0.70f, 0.70f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TAB_DIMMED_SELECTED{0.86f, 0.86f, 0.86f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TAB_OVERLINE{0.55f, 0.55f, 0.55f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_TAB_DIMMED_OVERLINE{0.50f, 0.50f, 0.50f, 1.0f}; + constexpr ImVec4 COLOR_LIGHT_CHECK_MARK{0.0f, 0.0f, 0.0f, 1.0f}; + constexpr auto FRAME_BORDER_SIZE = 1.0f; void theme_set(theme::Type theme) { @@ -28,6 +42,26 @@ namespace anm2ed::imgui ImGui::StyleColorsClassic(); break; } + auto& style = ImGui::GetStyle(); + style.FrameBorderSize = FRAME_BORDER_SIZE; + + if (theme == theme::LIGHT) + { + auto& colors = style.Colors; + colors[ImGuiCol_Button] = COLOR_LIGHT_BUTTON; + colors[ImGuiCol_TitleBg] = COLOR_LIGHT_TITLE_BG; + colors[ImGuiCol_TitleBgActive] = COLOR_LIGHT_TITLE_BG_ACTIVE; + colors[ImGuiCol_TitleBgCollapsed] = COLOR_LIGHT_TITLE_BG_COLLAPSED; + colors[ImGuiCol_TableHeaderBg] = COLOR_LIGHT_TABLE_HEADER; + colors[ImGuiCol_Tab] = COLOR_LIGHT_TAB; + colors[ImGuiCol_TabHovered] = COLOR_LIGHT_TAB_HOVERED; + colors[ImGuiCol_TabSelected] = COLOR_LIGHT_TAB_SELECTED; + colors[ImGuiCol_TabSelectedOverline] = COLOR_LIGHT_TAB_OVERLINE; + colors[ImGuiCol_TabDimmed] = COLOR_LIGHT_TAB_DIMMED; + colors[ImGuiCol_TabDimmedSelected] = COLOR_LIGHT_TAB_DIMMED_SELECTED; + colors[ImGuiCol_TabDimmedSelectedOverline] = COLOR_LIGHT_TAB_DIMMED_OVERLINE; + colors[ImGuiCol_CheckMark] = COLOR_LIGHT_CHECK_MARK; + } } int input_text_callback(ImGuiInputTextCallbackData* data) diff --git a/src/imgui/taskbar.cpp b/src/imgui/taskbar.cpp index ec38441..f5e14af 100644 --- a/src/imgui/taskbar.cpp +++ b/src/imgui/taskbar.cpp @@ -16,6 +16,7 @@ #include "math_.h" #include "render.h" #include "shader.h" +#include "snapshots.h" #include "types.h" #include "icon.h" @@ -372,7 +373,6 @@ namespace anm2ed::imgui for (int i = 0; i < theme::COUNT; i++) { - if (i == theme::LIGHT) continue; // TODO; light mode is jank rn so i am soft disabling it ImGui::RadioButton(theme::STRINGS[i], &editSettings.theme, i); ImGui::SameLine(); } @@ -400,6 +400,11 @@ namespace anm2ed::imgui ImGui::Checkbox("Overwrite Warning", &editSettings.fileIsWarnOverwrite); ImGui::SetItemTooltip("A warning will be shown when saving a file."); + + ImGui::SeparatorText("Snapshots"); + input_int_range("Stack Size", editSettings.fileSnapshotStackSize, 0, 1000); + ImGui::SetItemTooltip("Set the maximum snapshot stack size of a document\n(i.e., how many undo/redos are " + "preserved at a time)."); } ImGui::EndChild(); @@ -503,6 +508,9 @@ namespace anm2ed::imgui settings = editSettings; imgui::theme_set((theme::Type)editSettings.theme); manager.chords_set(settings); + SnapshotStack::max_size_set(settings.fileSnapshotStackSize); + for (auto& document : manager.documents) + document.snapshots.apply_limit(); configurePopup.close(); } ImGui::SetItemTooltip("Use the configured settings."); diff --git a/src/imgui/window/animation_preview.cpp b/src/imgui/window/animation_preview.cpp index 353cee6..3f23c24 100644 --- a/src/imgui/window/animation_preview.cpp +++ b/src/imgui/window/animation_preview.cpp @@ -26,7 +26,8 @@ namespace anm2ed::imgui constexpr auto TARGET_SIZE = vec2(32, 32); constexpr auto POINT_SIZE = vec2(4, 4); constexpr auto NULL_RECT_SIZE = vec2(100); - constexpr auto TRIGGER_TEXT_COLOR = ImVec4(1.0f, 1.0f, 1.0f, 0.5f); + constexpr auto TRIGGER_TEXT_COLOR_DARK = ImVec4(1.0f, 1.0f, 1.0f, 0.5f); + constexpr auto TRIGGER_TEXT_COLOR_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 0.5f); AnimationPreview::AnimationPreview() : Canvas(vec2()) {} @@ -136,6 +137,8 @@ namespace anm2ed::imgui zoom = savedZoom; overlayIndex = savedOverlayIndex; isSizeTrySet = true; + hasPendingZoomPanAdjust = false; + isCheckerPanInitialized = false; } if (settings.timelineIsSound) audioStream.capture_end(mixer); @@ -204,10 +207,38 @@ namespace anm2ed::imgui auto& tool = settings.tool; auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; auto& shaderLine = resources.shaders[shader::LINE]; + bool isLightTheme = settings.theme == theme::LIGHT; auto& shaderAxes = resources.shaders[shader::AXIS]; auto& shaderGrid = resources.shaders[shader::GRID]; auto& shaderTexture = resources.shaders[shader::TEXTURE]; + auto reset_checker_pan = [&]() + { + checkerPan = pan; + checkerSyncPan = pan; + checkerSyncZoom = zoom; + isCheckerPanInitialized = true; + hasPendingZoomPanAdjust = false; + }; + + auto sync_checker_pan = [&]() + { + if (!isCheckerPanInitialized) + { + reset_checker_pan(); + return; + } + + if (pan != checkerSyncPan || zoom != checkerSyncZoom) + { + bool ignorePanDelta = hasPendingZoomPanAdjust && zoom != checkerSyncZoom; + if (!ignorePanDelta) checkerPan += pan - checkerSyncPan; + checkerSyncPan = pan; + checkerSyncZoom = zoom; + if (ignorePanDelta) hasPendingZoomPanAdjust = false; + } + }; + auto center_view = [&]() { pan = vec2(); }; if (ImGui::Begin("Animation Preview", &settings.windowIsAnimationPreview)) @@ -351,28 +382,77 @@ namespace anm2ed::imgui if (isAxes) axes_render(shaderAxes, zoom, pan, axesColor); if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor); - auto render = [&](anm2::Animation* animation, float time, vec3 colorOffset = {}, float alphaOffset = {}, - bool isOnionskin = false) + auto baseTransform = transform_get(zoom, pan); + auto frameTime = document.frameTime > -1 && !playback.isPlaying ? document.frameTime : playback.time; + + struct OnionskinSample + { + float time{}; + vec3 colorOffset{}; + float alphaOffset{}; + glm::mat4 transform{1.0f}; + anm2::Frame root{}; + }; + + std::vector onionskinSamples; + + if (animation && settings.onionskinIsEnabled) + { + auto add_samples = [&](int count, int direction, vec3 color) + { + for (int i = 1; i <= count; ++i) + { + float useTime = frameTime + (float)(direction * i); + if (useTime < 0.0f || useTime > animation->frameNum) continue; + + float alphaOffset = (1.0f / (count + 1)) * i; + OnionskinSample sample{}; + sample.time = useTime; + sample.colorOffset = color; + sample.alphaOffset = alphaOffset; + sample.root = animation->rootAnimation.frame_generate(sample.time, anm2::ROOT); + sample.transform = baseTransform; + if (isRootTransform) + sample.transform *= math::quad_model_parent_get( + sample.root.position, {}, math::percent_to_unit(sample.root.scale), sample.root.rotation); + onionskinSamples.push_back(sample); + } + }; + + add_samples(settings.onionskinBeforeCount, -1, settings.onionskinBeforeColor); + add_samples(settings.onionskinAfterCount, 1, settings.onionskinAfterColor); + } + + auto render = [&](anm2::Animation* animation, float time, vec3 colorOffset = {}, float alphaOffset = {}, + const std::vector* layeredOnions = nullptr) { - auto baseTransform = transform_get(zoom, pan); auto transform = baseTransform; auto root = animation->rootAnimation.frame_generate(time, anm2::ROOT); if (isRootTransform) transform *= math::quad_model_parent_get(root.position, {}, math::percent_to_unit(root.scale), root.rotation); - if (!isOnlyShowLayers && root.isVisible && animation->rootAnimation.isVisible) + auto draw_root = [&](const anm2::Frame& rootFrame, vec3 sampleColor, float sampleAlpha, bool isOnion) { - auto rootTransform = - isRootTransform ? baseTransform * math::quad_model_get(TARGET_SIZE, root.position, TARGET_SIZE * 0.5f, - math::percent_to_unit(root.scale), root.rotation) - : baseTransform * math::quad_model_get(TARGET_SIZE, {}, TARGET_SIZE * 0.5f); + if (isOnlyShowLayers || !rootFrame.isVisible || !animation->rootAnimation.isVisible) return; - vec4 color = isOnionskin ? vec4(colorOffset, alphaOffset) : color::GREEN; + auto rootTransform = + isRootTransform + ? baseTransform * math::quad_model_get(TARGET_SIZE, rootFrame.position, TARGET_SIZE * 0.5f, + math::percent_to_unit(rootFrame.scale), rootFrame.rotation) + : baseTransform * math::quad_model_get(TARGET_SIZE, {}, TARGET_SIZE * 0.5f); + + vec4 color = isOnion ? vec4(sampleColor, sampleAlpha) : color::GREEN; auto icon = isAltIcons ? icon::TARGET_ALT : icon::TARGET; texture_render(shaderTexture, resources.icons[icon].id, rootTransform, color); - } + }; + + if (layeredOnions) + for (auto& sample : *layeredOnions) + draw_root(sample.root, sample.colorOffset, sample.alphaOffset, true); + + draw_root(root, {}, 0.0f, false); for (auto& id : animation->layerOrder) { @@ -381,46 +461,56 @@ namespace anm2ed::imgui auto& layer = anm2.content.layers.at(id); - if (auto frame = layerAnimation.frame_generate(time, anm2::LAYER); frame.isVisible) + auto spritesheet = anm2.spritesheet_get(layer.spritesheetID); + if (!spritesheet || !spritesheet->is_valid()) continue; + + auto draw_layer = + [&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor, float sampleAlpha, bool isOnion) { - auto spritesheet = anm2.spritesheet_get(layer.spritesheetID); - if (!spritesheet || !spritesheet->is_valid()) continue; - - auto& texture = spritesheet->texture; - - auto layerModel = math::quad_model_get(frame.size, frame.position, frame.pivot, - math::percent_to_unit(frame.scale), frame.rotation); - auto layerTransform = transform * layerModel; - - auto texSize = vec2(texture.size); - if (texSize.x <= 0.0f || texSize.y <= 0.0f) continue; - - auto uvMin = frame.crop / texSize; - auto uvMax = (frame.crop + frame.size) / texSize; - vec3 frameColorOffset = frame.colorOffset + colorOffset; - vec4 frameTint = frame.tint; - frameTint.a = std::max(0.0f, frameTint.a - alphaOffset); - - auto inset = vec2(0.5f) / texSize; - uvMin += inset; - uvMax -= inset; - auto vertices = math::uv_vertices_get(uvMin, uvMax); - - texture_render(shaderTexture, texture.id, layerTransform, frameTint, frameColorOffset, vertices.data()); - - auto color = isOnionskin ? vec4(colorOffset, 1.0f - alphaOffset) : color::RED; - - if (isBorder) rect_render(shaderLine, layerTransform, layerModel, color); - - if (isPivots) + if (auto frame = layerAnimation.frame_generate(sampleTime, anm2::LAYER); frame.isVisible) { - auto pivotModel = math::quad_model_get(PIVOT_SIZE, frame.position, PIVOT_SIZE * 0.5f, - math::percent_to_unit(frame.scale), frame.rotation); - auto pivotTransform = transform * pivotModel; + auto& texture = spritesheet->texture; - texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, color); + auto layerModel = math::quad_model_get(frame.size, frame.position, frame.pivot, + math::percent_to_unit(frame.scale), frame.rotation); + auto layerTransform = sampleTransform * layerModel; + + auto texSize = vec2(texture.size); + if (texSize.x <= 0.0f || texSize.y <= 0.0f) return; + + auto uvMin = frame.crop / texSize; + auto uvMax = (frame.crop + frame.size) / texSize; + vec3 frameColorOffset = frame.colorOffset + colorOffset + sampleColor; + vec4 frameTint = frame.tint; + frameTint.a = std::max(0.0f, frameTint.a - (alphaOffset + sampleAlpha)); + + auto inset = vec2(0.5f) / texSize; + uvMin += inset; + uvMax -= inset; + auto vertices = math::uv_vertices_get(uvMin, uvMax); + + texture_render(shaderTexture, texture.id, layerTransform, frameTint, frameColorOffset, vertices.data()); + + auto color = isOnion ? vec4(sampleColor, 1.0f - sampleAlpha) : color::RED; + + if (isBorder) rect_render(shaderLine, layerTransform, layerModel, color); + + if (isPivots) + { + auto pivotModel = math::quad_model_get(PIVOT_SIZE, frame.position, PIVOT_SIZE * 0.5f, + math::percent_to_unit(frame.scale), frame.rotation); + auto pivotTransform = sampleTransform * pivotModel; + + texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, color); + } } - } + }; + + if (layeredOnions) + for (auto& sample : *layeredOnions) + draw_layer(sample.time, sample.transform, sample.colorOffset, sample.alphaOffset, true); + + draw_layer(time, transform, {}, 0.0f, false); } for (auto& [id, nullAnimation] : animation->nullAnimations) @@ -429,71 +519,57 @@ namespace anm2ed::imgui auto& isShowRect = anm2.content.nulls[id].isShowRect; - if (auto frame = nullAnimation.frame_generate(time, anm2::NULL_); frame.isVisible) + auto draw_null = + [&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor, float sampleAlpha, bool isOnion) { - auto icon = isShowRect ? icon::POINT : isAltIcons ? icon::TARGET_ALT : icon::TARGET; - - auto& size = isShowRect ? POINT_SIZE : TARGET_SIZE; - auto color = isOnionskin ? vec4(colorOffset, 1.0f - alphaOffset) - : id == reference.itemID && reference.itemType == anm2::NULL_ ? color::RED - : NULL_COLOR; - - auto nullModel = math::quad_model_get(size, frame.position, size * 0.5f, math::percent_to_unit(frame.scale), - frame.rotation); - auto nullTransform = transform * nullModel; - - texture_render(shaderTexture, resources.icons[icon].id, nullTransform, color); - - if (isShowRect) + if (auto frame = nullAnimation.frame_generate(sampleTime, anm2::NULL_); frame.isVisible) { - auto rectModel = math::quad_model_get(NULL_RECT_SIZE, frame.position, NULL_RECT_SIZE * 0.5f, + auto icon = isShowRect ? icon::POINT : isAltIcons ? icon::TARGET_ALT : icon::TARGET; + + auto& size = isShowRect ? POINT_SIZE : TARGET_SIZE; + auto color = isOnion ? vec4(sampleColor, 1.0f - sampleAlpha) + : id == reference.itemID && reference.itemType == anm2::NULL_ ? color::RED + : NULL_COLOR; + + auto nullModel = math::quad_model_get(size, frame.position, size * 0.5f, math::percent_to_unit(frame.scale), frame.rotation); - auto rectTransform = transform * rectModel; + auto nullTransform = sampleTransform * nullModel; - rect_render(shaderLine, rectTransform, rectModel, color); + texture_render(shaderTexture, resources.icons[icon].id, nullTransform, color); + + if (isShowRect) + { + auto rectModel = math::quad_model_get(NULL_RECT_SIZE, frame.position, NULL_RECT_SIZE * 0.5f, + math::percent_to_unit(frame.scale), frame.rotation); + auto rectTransform = sampleTransform * rectModel; + + rect_render(shaderLine, rectTransform, rectModel, color); + } } - } + }; + + if (layeredOnions) + for (auto& sample : *layeredOnions) + draw_null(sample.time, sample.transform, sample.colorOffset, sample.alphaOffset, true); + + draw_null(time, transform, {}, 0.0f, false); } }; - auto onionskin_render = [&](float time, int count, int direction, vec3 color) - { - for (int i = 1; i <= count; i++) - { - float useTime = time + (float)(direction * i); - if (useTime < 0.0f || useTime > animation->frameNum) continue; - - float alphaOffset = (1.0f / (count + 1)) * i; - render(animation, useTime, color, alphaOffset, true); - } - }; - - auto onionskins_render = [&](float time) - { - onionskin_render(time, settings.onionskinBeforeCount, -1, settings.onionskinBeforeColor); - onionskin_render(time, settings.onionskinAfterCount, 1, settings.onionskinAfterColor); - }; - - auto frameTime = document.frameTime > -1 && !playback.isPlaying ? document.frameTime : playback.time; - if (animation) { - auto& drawOrder = settings.onionskinDrawOrder; - auto& isEnabled = settings.onionskinIsEnabled; + auto layeredOnions = settings.onionskinIsEnabled ? &onionskinSamples : nullptr; - if (drawOrder == draw_order::BELOW && isEnabled) onionskins_render(frameTime); - - render(animation, frameTime); + render(animation, frameTime, {}, 0.0f, layeredOnions); if (auto overlayAnimation = anm2.animation_get(overlayIndex)) render(overlayAnimation, frameTime, {}, 1.0f - math::uint8_to_float(overlayTransparency)); - - if (drawOrder == draw_order::ABOVE && isEnabled) onionskins_render(frameTime); } unbind(); - render_checker_background(ImGui::GetWindowDrawList(), min, max, -size - pan, CHECKER_SIZE); + sync_checker_pan(); + render_checker_background(ImGui::GetWindowDrawList(), min, max, -size - checkerPan, CHECKER_SIZE); ImGui::Image(texture, to_imvec2(size)); isPreviewHovered = ImGui::IsItemHovered(); @@ -510,7 +586,8 @@ namespace anm2ed::imgui drawList->PushClipRect(clipMin, clipMax); ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE_LARGE); - drawList->AddText(textPos, ImGui::GetColorU32(TRIGGER_TEXT_COLOR), + auto triggerTextColor = isLightTheme ? TRIGGER_TEXT_COLOR_LIGHT : TRIGGER_TEXT_COLOR_DARK; + drawList->AddText(textPos, ImGui::GetColorU32(triggerTextColor), anm2.content.events.at(trigger.eventID).name.c_str()); ImGui::PopFont(); drawList->PopClipRect(); @@ -648,8 +725,12 @@ namespace anm2ed::imgui } if (mouseWheel != 0 || isZoomIn || isZoomOut) + { + auto previousZoom = zoom; zoom_set(zoom, pan, mouseWheel != 0 ? vec2(mousePos) : vec2(), (mouseWheel > 0 || isZoomIn) ? zoomStep : -zoomStep); + if (zoom != previousZoom) hasPendingZoomPanAdjust = true; + } } } ImGui::End(); @@ -677,6 +758,8 @@ namespace anm2ed::imgui settings = savedSettings; overlayIndex = savedOverlayIndex; isSizeTrySet = true; + hasPendingZoomPanAdjust = false; + isCheckerPanInitialized = false; if (settings.timelineIsSound) audioStream.capture_end(mixer); @@ -693,6 +776,7 @@ namespace anm2ed::imgui { center_view(); zoom = settings.previewStartZoom; + reset_checker_pan(); document.isAnimationPreviewSet = true; } diff --git a/src/imgui/window/animation_preview.h b/src/imgui/window/animation_preview.h index c2b1fdf..cc1fa68 100644 --- a/src/imgui/window/animation_preview.h +++ b/src/imgui/window/animation_preview.h @@ -19,6 +19,11 @@ namespace anm2ed::imgui glm::vec2 savedPan{}; int savedOverlayIndex{}; glm::ivec2 mousePos{}; + glm::vec2 checkerPan{}; + glm::vec2 checkerSyncPan{}; + float checkerSyncZoom{}; + bool isCheckerPanInitialized{}; + bool hasPendingZoomPanAdjust{}; std::vector renderFrames{}; public: diff --git a/src/imgui/window/onionskin.cpp b/src/imgui/window/onionskin.cpp index 76704f8..ff7f876 100644 --- a/src/imgui/window/onionskin.cpp +++ b/src/imgui/window/onionskin.cpp @@ -18,7 +18,6 @@ namespace anm2ed::imgui auto& beforeColor = settings.onionskinBeforeColor; auto& afterCount = settings.onionskinAfterCount; auto& afterColor = settings.onionskinAfterColor; - auto& drawOrder = settings.onionskinDrawOrder; if (ImGui::Begin("Onionskin", &settings.windowIsOnionskin)) { @@ -38,14 +37,6 @@ namespace anm2ed::imgui configure_widgets("Before", beforeCount, beforeColor); configure_widgets("After", afterCount, afterColor); - - ImGui::Text("Draw Order"); - ImGui::SameLine(); - ImGui::RadioButton("Below", &drawOrder, draw_order::BELOW); - ImGui::SetItemTooltip("The onionskin frames will draw below the original frames."); - ImGui::SameLine(); - ImGui::RadioButton("Above", &drawOrder, draw_order::ABOVE); - ImGui::SetItemTooltip("The onionskin frames will draw above the original frames."); } ImGui::End(); diff --git a/src/imgui/window/spritesheet_editor.cpp b/src/imgui/window/spritesheet_editor.cpp index 57f985e..0f2077c 100644 --- a/src/imgui/window/spritesheet_editor.cpp +++ b/src/imgui/window/spritesheet_editor.cpp @@ -50,6 +50,33 @@ namespace anm2ed::imgui auto& shaderTexture = resources.shaders[shader::TEXTURE]; auto& dashedShader = resources.shaders[shader::DASHED]; + auto reset_checker_pan = [&]() + { + checkerPan = pan; + checkerSyncPan = pan; + checkerSyncZoom = zoom; + isCheckerPanInitialized = true; + hasPendingZoomPanAdjust = false; + }; + + auto sync_checker_pan = [&]() + { + if (!isCheckerPanInitialized) + { + reset_checker_pan(); + return; + } + + if (pan != checkerSyncPan || zoom != checkerSyncZoom) + { + bool ignorePanDelta = hasPendingZoomPanAdjust && zoom != checkerSyncZoom; + if (!ignorePanDelta) checkerPan += pan - checkerSyncPan; + checkerSyncPan = pan; + checkerSyncZoom = zoom; + if (ignorePanDelta) hasPendingZoomPanAdjust = false; + } + }; + auto center_view = [&]() { pan = -size * 0.5f; }; if (ImGui::Begin("Spritesheet Editor", &settings.windowIsSpritesheetEditor)) @@ -173,7 +200,8 @@ namespace anm2ed::imgui unbind(); - render_checker_background(drawList, min, max, -size * 0.5f - pan, CHECKER_SIZE); + sync_checker_pan(); + render_checker_background(drawList, min, max, -size * 0.5f - checkerPan, CHECKER_SIZE); if (!isTransparent) drawList->AddRectFilled(min, max, ImGui::GetColorU32(to_imvec4(vec4(backgroundColor, 1.0f)))); drawList->AddImage(texture, min, max); ImGui::InvisibleButton("##Spritesheet Editor", to_imvec2(size)); @@ -236,15 +264,15 @@ namespace anm2ed::imgui { if (gridSize.x != 0) { - auto offsetX = static_cast(gridOffset.x); - auto sizeX = static_cast(gridSize.x); + auto offsetX = (float)(gridOffset.x); + auto sizeX = (float)(gridSize.x); minPoint.x = std::floor((minPoint.x - offsetX) / sizeX) * sizeX + offsetX; maxPoint.x = std::ceil((maxPoint.x - offsetX) / sizeX) * sizeX + offsetX; } if (gridSize.y != 0) { - auto offsetY = static_cast(gridOffset.y); - auto sizeY = static_cast(gridSize.y); + auto offsetY = (float)(gridOffset.y); + auto sizeY = (float)(gridSize.y); minPoint.y = std::floor((minPoint.y - offsetY) / sizeY) * sizeY + offsetY; maxPoint.y = std::ceil((maxPoint.y - offsetY) / sizeY) * sizeY + offsetY; } @@ -380,7 +408,9 @@ namespace anm2ed::imgui if (auto spritesheet = document.spritesheet_get(); spritesheet && mouseWheel == 0) focus = spritesheet->texture.size / 2; + auto previousZoom = zoom; zoom_set(zoom, pan, focus, (mouseWheel > 0 || isZoomIn) ? zoomStep : -zoomStep); + if (zoom != previousZoom) hasPendingZoomPanAdjust = true; } } } @@ -392,6 +422,7 @@ namespace anm2ed::imgui zoom = settings.editorStartZoom; set(); center_view(); + reset_checker_pan(); document.isSpritesheetEditorSet = true; } diff --git a/src/imgui/window/spritesheet_editor.h b/src/imgui/window/spritesheet_editor.h index 8c992fe..0829acf 100644 --- a/src/imgui/window/spritesheet_editor.h +++ b/src/imgui/window/spritesheet_editor.h @@ -12,6 +12,11 @@ namespace anm2ed::imgui glm::vec2 mousePos{}; glm::vec2 previousMousePos{}; glm::vec2 cropAnchor{}; + glm::vec2 checkerPan{}; + glm::vec2 checkerSyncPan{}; + float checkerSyncZoom{}; + bool isCheckerPanInitialized{}; + bool hasPendingZoomPanAdjust{}; public: SpritesheetEditor(); diff --git a/src/imgui/window/timeline.cpp b/src/imgui/window/timeline.cpp index 9b34d46..0cb80fb 100644 --- a/src/imgui/window/timeline.cpp +++ b/src/imgui/window/timeline.cpp @@ -1,6 +1,7 @@ #include "timeline.h" #include +#include #include #include @@ -40,6 +41,7 @@ namespace anm2ed::imgui constexpr auto TIMELINE_PLAYHEAD_RECT_COLOR_DARK = ImVec4(0.60f, 0.45f, 0.30f, 1.0f); constexpr auto TIMELINE_PLAYHEAD_RECT_COLOR_LIGHT = ImVec4(0.8353f, 0.8353f, 0.7294f, 1.0f); constexpr auto TIMELINE_TICK_COLOR_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); + constexpr auto TIMELINE_CHILD_BG_COLOR_LIGHT = ImVec4(0.5490f, 0.5490f, 0.5882f, 1.0f); constexpr auto TIMELINE_TEXT_COLOR_LIGHT = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); constexpr glm::vec4 FRAME_COLOR_LIGHT_BASE[] = {{0.80f, 0.80f, 0.80f, 1.0f}, @@ -48,18 +50,18 @@ namespace anm2ed::imgui {0.6157f, 1.0f, 0.5882f, 1.0f}, {1.0f, 0.5882f, 0.8314f, 1.0f}}; constexpr glm::vec4 FRAME_COLOR_LIGHT_ACTIVE[] = {{0.74f, 0.74f, 0.74f, 1.0f}, - {0.4700f, 0.6830f, 0.95f, 1.0f}, - {0.96f, 0.956f, 0.548f, 1.0f}, - {0.5757f, 0.95f, 0.5482f, 1.0f}, - {0.94f, 0.5482f, 0.7814f, 1.0f}}; + {0.0980f, 0.3765f, 0.6431f, 1.0f}, + {1.0f, 0.5255f, 0.3333f, 1.0f}, + {0.3686f, 0.5765f, 0.2353f, 1.0f}, + {0.6118f, 0.2039f, 0.2745f, 1.0f}}; constexpr glm::vec4 FRAME_COLOR_LIGHT_HOVERED[] = {{0.84f, 0.84f, 0.84f, 1.0f}, - {0.5616f, 0.7733f, 1.0f, 1.0f}, - {1.0f, 0.9361f, 0.5482f, 1.0f}, - {0.6557f, 1.0f, 0.6282f, 1.0f}, - {1.0f, 0.6282f, 0.8714f, 1.0f}}; + {0.0752f, 0.2887f, 0.4931f, 1.0f}, + {0.85f, 0.4467f, 0.2833f, 1.0f}, + {0.2727f, 0.4265f, 0.1741f, 1.0f}, + {0.4618f, 0.1539f, 0.2072f, 1.0f}}; constexpr glm::vec4 ITEM_COLOR_LIGHT_BASE[] = {{0.3059f, 0.3255f, 0.5412f, 1.0f}, {0.3333f, 0.5725f, 0.8392f, 1.0f}, - {0.8706f, 0.4549f, 0.2353f, 1.0f}, + {1.0f, 0.5412f, 0.3412f, 1.0f}, {0.5255f, 0.8471f, 0.4588f, 1.0f}, {0.7961f, 0.3882f, 0.5412f, 1.0f}}; constexpr glm::vec4 ITEM_COLOR_LIGHT_ACTIVE[] = {{0.3459f, 0.3655f, 0.5812f, 1.0f}, @@ -67,6 +69,11 @@ namespace anm2ed::imgui {0.9106f, 0.4949f, 0.2753f, 1.0f}, {0.5655f, 0.8871f, 0.4988f, 1.0f}, {0.8361f, 0.4282f, 0.5812f, 1.0f}}; + constexpr glm::vec4 ITEM_COLOR_LIGHT_SELECTED[] = {{0.74f, 0.74f, 0.74f, 1.0f}, + {0.2039f, 0.4549f, 0.7176f, 1.0f}, + {0.8745f, 0.4392f, 0.2275f, 1.0f}, + {0.3765f, 0.6784f, 0.2980f, 1.0f}, + {0.6353f, 0.2235f, 0.3647f, 1.0f}}; constexpr auto FRAME_MULTIPLE = 5; constexpr auto FRAME_DRAG_PAYLOAD_ID = "Frame Drag Drop"; @@ -302,7 +309,15 @@ namespace anm2ed::imgui auto iconTintCurrent = isLightTheme && type == anm2::NONE ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : itemIconTint; auto baseColorVec = item_color_vec(type); auto activeColorVec = item_color_active_vec(type); - auto colorVec = isActive ? activeColorVec : baseColorVec; + bool isTypeNone = type == anm2::NONE; + auto colorVec = baseColorVec; + if (isActive && !isTypeNone) + { + if (isLightTheme) + colorVec = ITEM_COLOR_LIGHT_SELECTED[type_index(type)]; + else + colorVec = activeColorVec; + } auto color = to_imvec4(colorVec); color = !isVisible ? to_imvec4(colorVec * COLOR_HIDDEN_MULTIPLIER) : color; ImGui::PushStyleColor(ImGuiCol_ChildBg, color); @@ -319,7 +334,7 @@ namespace anm2ed::imgui auto cursorPos = ImGui::GetCursorPos(); - if (type != anm2::NONE) + if (!isTypeNone) { ImGui::SetCursorPos(to_imvec2(to_vec2(cursorPos) - to_vec2(style.ItemSpacing))); @@ -618,6 +633,8 @@ namespace anm2ed::imgui ImGui::PushID(index); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); + bool isDefaultChild = type == anm2::NONE; + if (isLightTheme && isDefaultChild) ImGui::PushStyleColor(ImGuiCol_ChildBg, TIMELINE_CHILD_BG_COLOR_LIGHT); if (ImGui::BeginChild("##Frames Child", childSize, ImGuiChildFlags_Borders)) { @@ -628,7 +645,7 @@ namespace anm2ed::imgui auto framesSize = ImVec2(frameSize.x * length, frameSize.y); auto cursorPos = ImGui::GetCursorPos(); auto cursorScreenPos = ImGui::GetCursorScreenPos(); - auto border = ImGui::GetStyle().FrameBorderSize; + auto border = glm::max(0.5f, ImGui::GetStyle().FrameBorderSize * 0.5f); auto borderLineLength = frameSize.y / 5; auto frameMin = std::max(0, (int)std::floor(scroll.x / frameSize.x) - 1); auto frameMax = std::min(anm2::FRAME_NUM_MAX, (int)std::ceil((scroll.x + clipMax.x) / frameSize.x) + 1); @@ -657,11 +674,11 @@ namespace anm2ed::imgui auto frameScreenPos = ImVec2(cursorScreenPos.x + frameSize.x * (float)i, cursorScreenPos.y); drawList->AddRect(frameScreenPos, ImVec2(frameScreenPos.x + border, frameScreenPos.y + borderLineLength), - ImGui::GetColorU32(timelineTickColor)); + ImGui::GetColorU32(timelineTickColor), 0, 0, 0.5f); drawList->AddRect(ImVec2(frameScreenPos.x, frameScreenPos.y + frameSize.y - borderLineLength), ImVec2(frameScreenPos.x + border, frameScreenPos.y + frameSize.y), - ImGui::GetColorU32(timelineTickColor)); + ImGui::GetColorU32(timelineTickColor), 0, 0, 0.5); if (i % FRAME_MULTIPLE == 0) { @@ -1021,6 +1038,7 @@ namespace anm2ed::imgui context_menu(); ImGui::EndChild(); + if (isLightTheme && isDefaultChild) ImGui::PopStyleColor(); ImGui::PopStyleVar(); index++; diff --git a/src/imgui/window/tools.cpp b/src/imgui/window/tools.cpp index 3c47745..5aadfbb 100644 --- a/src/imgui/window/tools.cpp +++ b/src/imgui/window/tools.cpp @@ -43,6 +43,8 @@ namespace anm2ed::imgui } }; + auto iconTint = settings.theme == theme::LIGHT ? ImVec4(0.0f, 0.0f, 0.0f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + for (int i = 0; i < tool::COUNT; i++) { auto& info = tool::INFO[i]; @@ -65,7 +67,9 @@ namespace anm2ed::imgui { if (i == tool::UNDO) ImGui::BeginDisabled(!document.is_able_to_undo()); if (i == tool::REDO) ImGui::BeginDisabled(!document.is_able_to_redo()); - if (ImGui::ImageButton(info.label, resources.icons[info.icon].id, to_imvec2(size))) tool_use((tool::Type)i); + if (ImGui::ImageButton(info.label, resources.icons[info.icon].id, to_imvec2(size), ImVec2(0, 0), ImVec2(1, 1), + ImVec4(0, 0, 0, 0), iconTint)) + tool_use((tool::Type)i); if (i == tool::UNDO) ImGui::EndDisabled(); if (i == tool::REDO) ImGui::EndDisabled(); } diff --git a/src/loader.cpp b/src/loader.cpp index ed1d641..ab517c1 100644 --- a/src/loader.cpp +++ b/src/loader.cpp @@ -12,6 +12,7 @@ #include "imgui_.h" +#include "snapshots.h" #include "socket.h" using namespace anm2ed::types; @@ -110,6 +111,7 @@ namespace anm2ed } settings = Settings(settings_path()); + SnapshotStack::max_size_set(settings.fileSnapshotStackSize); if (!SDL_Init(SDL_INIT_VIDEO)) { @@ -184,11 +186,6 @@ namespace anm2ed imgui::theme_set((theme::Type)settings.theme); - if (settings.theme == theme::DARK) - ImGui::StyleColorsDark(); - else if (settings.theme == theme::LIGHT) - ImGui::StyleColorsClassic(); - ImGui_ImplSDL3_InitForOpenGL(window, glContext); ImGui_ImplOpenGL3_Init("#version 330"); diff --git a/src/settings.h b/src/settings.h index 5b4dcf2..a284caf 100644 --- a/src/settings.h +++ b/src/settings.h @@ -49,6 +49,7 @@ namespace anm2ed X(FILE_IS_AUTOSAVE, fileIsAutosave, "Autosave", BOOL, true) \ X(FILE_AUTOSAVE_TIME, fileAutosaveTime, "Autosave Time", INT, 1) \ X(FILE_IS_WARN_OVERWRITE, fileIsWarnOverwrite, "Warn on Overwrite", BOOL, true) \ + X(FILE_SNAPSHOT_STACK_SIZE, fileSnapshotStackSize, "Snapshot Stack Size", INT, 50) \ \ X(KEYBOARD_REPEAT_DELAY, keyboardRepeatDelay, "Repeat Delay", FLOAT, 0.300f) \ X(KEYBOARD_REPEAT_RATE, keyboardRepeatRate, "Repeat Rate", FLOAT, 0.050f) \ @@ -136,7 +137,6 @@ namespace anm2ed X(TIMELINE_IS_SOUND, timelineIsSound, "Sound", BOOL, true) \ \ X(ONIONSKIN_IS_ENABLED, onionskinIsEnabled, "Enabled", BOOL, false) \ - X(ONIONSKIN_DRAW_ORDER, onionskinDrawOrder, "Draw Order", INT, 0) \ X(ONIONSKIN_BEFORE_COUNT, onionskinBeforeCount, "Frames", INT, 0) \ X(ONIONSKIN_AFTER_COUNT, onionskinAfterCount, "Frames", INT, 0) \ X(ONIONSKIN_BEFORE_COLOR, onionskinBeforeColor, "Color", VEC3, types::color::RED) \ diff --git a/src/snapshots.cpp b/src/snapshots.cpp index c9df201..08c304d 100644 --- a/src/snapshots.cpp +++ b/src/snapshots.cpp @@ -1,29 +1,51 @@ #include "snapshots.h" +#include + using namespace anm2ed::snapshots; namespace anm2ed { - bool SnapshotStack::is_empty() { return top == 0; } + int SnapshotStack::maxSize = snapshots::MAX; + + bool SnapshotStack::is_empty() { return stack.empty(); } void SnapshotStack::push(const Snapshot& snapshot) { - if (top >= MAX) + if (maxSize <= 0) { - for (int i = 0; i < MAX - 1; i++) - snapshots[i] = snapshots[i + 1]; - top = MAX - 1; + stack.clear(); + return; } - snapshots[top++] = snapshot; + if ((int)stack.size() >= maxSize) stack.pop_front(); + stack.push_back(snapshot); } - Snapshot* SnapshotStack::pop() + std::optional SnapshotStack::pop() { - if (is_empty()) return nullptr; - return &snapshots[--top]; + if (is_empty()) return std::nullopt; + auto snapshot = stack.back(); + stack.pop_back(); + return snapshot; } - void SnapshotStack::clear() { top = 0; } + void SnapshotStack::clear() { stack.clear(); } + + void SnapshotStack::trim_to_limit() + { + if (maxSize <= 0) + { + clear(); + return; + } + + while ((int)stack.size() > maxSize) + stack.pop_front(); + } + + void SnapshotStack::max_size_set(int value) { maxSize = std::max(0, value); } + + int SnapshotStack::max_size_get() { return maxSize; } void Snapshots::push(const Snapshot& snapshot) { @@ -54,4 +76,10 @@ namespace anm2ed undoStack.clear(); redoStack.clear(); } + + void Snapshots::apply_limit() + { + undoStack.trim_to_limit(); + redoStack.trim_to_limit(); + } }; diff --git a/src/snapshots.h b/src/snapshots.h index 587886b..b33a4bd 100644 --- a/src/snapshots.h +++ b/src/snapshots.h @@ -1,5 +1,8 @@ #pragma once +#include +#include + #include "anm2/anm2.h" #include "playback.h" #include "storage.h" @@ -34,13 +37,20 @@ namespace anm2ed class SnapshotStack { public: - Snapshot snapshots[snapshots::MAX]; - int top{}; + SnapshotStack() = default; bool is_empty(); void push(const Snapshot&); - Snapshot* pop(); + std::optional pop(); void clear(); + void trim_to_limit(); + + static void max_size_set(int); + static int max_size_get(); + + private: + static int maxSize; + std::deque stack; }; class Snapshots @@ -55,5 +65,6 @@ namespace anm2ed void undo(); void redo(); void reset(); + void apply_limit(); }; } diff --git a/src/types.h b/src/types.h index 40ba506..1c6d891 100644 --- a/src/types.h +++ b/src/types.h @@ -4,15 +4,6 @@ #include #include -namespace anm2ed::types::draw_order -{ - enum Type - { - BELOW, - ABOVE - }; -} - namespace anm2ed::types::theme { #define THEMES \