diff --git a/src/animation_preview.cpp b/src/animation_preview.cpp index 8db80c7..f73ded1 100644 --- a/src/animation_preview.cpp +++ b/src/animation_preview.cpp @@ -19,7 +19,6 @@ namespace anm2ed::animation_preview { constexpr auto NULL_COLOR = vec4(0.0f, 0.0f, 1.0f, 0.90f); constexpr auto TARGET_SIZE = vec2(32, 32); - constexpr auto PIVOT_SIZE = vec2(8, 8); 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); @@ -83,9 +82,17 @@ namespace anm2ed::animation_preview auto widgetSize = imgui::widget_size_with_row_get(2); + imgui::shortcut(settings.shortcutCenterView); if (ImGui::Button("Center View", widgetSize)) pan = vec2(); + imgui::set_item_tooltip_shortcut("Centers the view.", settings.shortcutCenterView); + ImGui::SameLine(); - ImGui::Button("Fit", widgetSize); + + imgui::shortcut(settings.shortcutFit); + if (ImGui::Button("Fit", widgetSize)) + if (animation) set_to_rect(zoom, pan, animation->rect(isRootTransform)); + imgui::set_item_tooltip_shortcut("Set the view to match the extent of the animation.", settings.shortcutFit); + ImGui::TextUnformatted(std::format(POSITION_FORMAT, (int)mousePos.x, (int)mousePos.y).c_str()); } ImGui::EndChild(); @@ -181,9 +188,8 @@ namespace anm2ed::animation_preview auto layerTransform = transform * math::quad_model_get(frame.size, frame.position, frame.pivot, math::percent_to_unit(frame.scale), frame.rotation); - auto inset = 0.5f / vec2(texture.size); // needed to avoid bleed - auto uvMin = frame.crop / vec2(texture.size) + inset; - auto uvMax = (frame.crop + frame.size) / vec2(texture.size) - inset; + auto uvMin = frame.crop / vec2(texture.size); + auto uvMax = (frame.crop + frame.size) / vec2(texture.size); auto vertices = math::uv_vertices_get(uvMin, uvMax); vec3 frameColorOffset = frame.offset + colorOffset; vec4 frameTint = frame.tint; @@ -298,15 +304,24 @@ namespace anm2ed::animation_preview mousePos = position_translate(zoom, pan, to_vec2(ImGui::GetMousePos()) - to_vec2(cursorScreenPos)); - auto isRound = settings.propertiesIsRound; + auto isMouseClick = ImGui::IsMouseClicked(ImGuiMouseButton_Left); + auto isMouseReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left); auto isMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left); auto isMouseMiddleDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle); + auto isLeftPressed = ImGui::IsKeyPressed(ImGuiKey_LeftArrow, false); + auto isRightPressed = ImGui::IsKeyPressed(ImGuiKey_RightArrow, false); + auto isUpPressed = ImGui::IsKeyPressed(ImGuiKey_UpArrow, false); + auto isDownPressed = ImGui::IsKeyPressed(ImGuiKey_DownArrow, false); + auto isLeftReleased = ImGui::IsKeyReleased(ImGuiKey_LeftArrow); + auto isRightReleased = ImGui::IsKeyReleased(ImGuiKey_RightArrow); + auto isUpReleased = ImGui::IsKeyReleased(ImGuiKey_UpArrow); + auto isDownReleased = ImGui::IsKeyReleased(ImGuiKey_DownArrow); auto isLeft = imgui::chord_repeating(ImGuiKey_LeftArrow); auto isRight = imgui::chord_repeating(ImGuiKey_RightArrow); auto isUp = imgui::chord_repeating(ImGuiKey_UpArrow); auto isDown = imgui::chord_repeating(ImGuiKey_DownArrow); auto isMouseRightDown = ImGui::IsMouseDown(ImGuiMouseButton_Right); - auto mouseDelta = to_vec2(ImGui::GetIO().MouseDelta); + auto mouseDelta = to_ivec2(ImGui::GetIO().MouseDelta); auto mouseWheel = ImGui::GetIO().MouseWheel; auto isZoomIn = imgui::chord_repeating(imgui::string_to_chord(settings.shortcutZoomIn)); auto isZoomOut = imgui::chord_repeating(imgui::string_to_chord(settings.shortcutZoomOut)); @@ -314,7 +329,10 @@ namespace anm2ed::animation_preview auto frame = document.frame_get(); auto useTool = tool; auto step = isMod ? step::FAST : step::NORMAL; - auto isClick = isMouseDown; + auto isKeyPressed = isLeftPressed || isRightPressed || isUpPressed || isDownPressed; + auto isKeyReleased = isLeftReleased || isRightReleased || isUpReleased || isDownReleased; + auto isBegin = isMouseClick || isKeyPressed; + auto isEnd = isMouseReleased || isKeyReleased; if (isMouseMiddleDown) useTool = tool::PAN; if (tool == tool::MOVE && isMouseRightDown) useTool = tool::SCALE; @@ -323,30 +341,42 @@ namespace anm2ed::animation_preview switch (useTool) { case tool::PAN: - if (isClick || isMouseMiddleDown) pan += isRound ? vec2(ivec2(mouseDelta)) : mouseDelta; + if (isMouseDown || isMouseMiddleDown) pan += mouseDelta; break; case tool::MOVE: if (!frame) break; - if (isClick) frame->position = isRound ? vec2(ivec2(mousePos)) : mousePos; + if (isBegin) document.snapshot("Frame Position"); + if (isMouseDown) frame->position = mousePos; if (isLeft) frame->position.x -= step; if (isRight) frame->position.x += step; if (isUp) frame->position.y -= step; if (isDown) frame->position.y += step; + if (isEnd) document.change(change::FRAMES); break; case tool::SCALE: if (!frame) break; - if (isClick) frame->scale += isRound ? vec2(ivec2(mouseDelta)) : mouseDelta; + if (isBegin) document.snapshot("Frame Scale"); + if (isMouseDown) frame->scale += mouseDelta; + if (isLeft) frame->scale.x -= step; + if (isRight) frame->scale.x += step; + if (isUp) frame->scale.y -= step; + if (isDown) frame->scale.y += step; + if (isEnd) document.change(change::FRAMES); break; case tool::ROTATE: if (!frame) break; - if (isClick) frame->rotation += isRound ? (int)mouseDelta.y : mouseDelta.y; + if (isBegin) document.snapshot("Frame Rotation"); + if (isMouseDown) frame->rotation += mouseDelta.y; + if (isLeft || isDown) frame->rotation -= step; + if (isUp || isRight) frame->rotation += step; + if (isEnd) document.change(change::FRAMES); break; default: break; } if (mouseWheel != 0 || isZoomIn || isZoomOut) - zoom_set(zoom, pan, mousePos, (mouseWheel > 0 || isZoomIn) ? zoomStep : -zoomStep); + zoom_set(zoom, pan, vec2(mousePos), (mouseWheel > 0 || isZoomIn) ? zoomStep : -zoomStep); } } ImGui::End(); diff --git a/src/animation_preview.h b/src/animation_preview.h index ef490dd..65a1a18 100644 --- a/src/animation_preview.h +++ b/src/animation_preview.h @@ -10,7 +10,7 @@ namespace anm2ed::animation_preview class AnimationPreview : public canvas::Canvas { bool isPreviewHovered{}; - glm::vec2 mousePos{}; + glm::ivec2 mousePos{}; public: AnimationPreview(); diff --git a/src/anm2.cpp b/src/anm2.cpp index 652b8f4..814f0cd 100644 --- a/src/anm2.cpp +++ b/src/anm2.cpp @@ -797,6 +797,50 @@ namespace anm2ed::anm2 return std::string(printer.CStr()); } + vec4 Animation::rect(bool isRootTransform) + { + f32 minX = std::numeric_limits::infinity(); + f32 minY = std::numeric_limits::infinity(); + f32 maxX = -std::numeric_limits::infinity(); + f32 maxY = -std::numeric_limits::infinity(); + bool any = false; + + constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}}; + + for (float t = 0.0f; t < (float)frameNum; t += 1.0f) + { + mat4 transform(1.0f); + + if (isRootTransform) + { + auto root = rootAnimation.frame_generate(t, anm2::ROOT); + transform *= math::quad_model_parent_get(root.position, {}, math::percent_to_unit(root.scale), root.rotation); + } + + for (auto& [id, layerAnimation] : layerAnimations) + { + auto frame = layerAnimation.frame_generate(t, anm2::LAYER); + + if (frame.size == vec2() || !frame.isVisible) continue; + + auto layerTransform = transform * math::quad_model_get(frame.size, frame.position, frame.pivot, + math::percent_to_unit(frame.scale), frame.rotation); + for (auto& corner : CORNERS) + { + vec4 world = layerTransform * vec4(corner, 0.0f, 1.0f); + minX = std::min(minX, world.x); + minY = std::min(minY, world.y); + maxX = std::max(maxX, world.x); + maxY = std::max(maxY, world.y); + any = true; + } + } + } + + if (!any) return vec4(-1.0f); + return {minX, minY, maxX - minX, maxY - minY}; + } + Animations::Animations() = default; Animations::Animations(XMLElement* element) diff --git a/src/anm2.h b/src/anm2.h index 02aaf5a..935f71c 100644 --- a/src/anm2.h +++ b/src/anm2.h @@ -208,6 +208,7 @@ namespace anm2ed::anm2 void item_remove(Type, int = -1); void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*); int length(); + glm::vec4 rect(bool); std::string to_string(); }; diff --git a/src/canvas.cpp b/src/canvas.cpp index 2932a80..526829b 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -3,6 +3,7 @@ #include "math.h" #include #include +#include #include using namespace glm; @@ -183,13 +184,11 @@ namespace anm2ed::canvas void Canvas::grid_render(Shader& shader, float zoom, vec2 pan, ivec2 size, ivec2 offset, vec4 color) { - auto zoomFactor = math::percent_to_unit(zoom); + auto transform = glm::inverse(transform_get(zoom, pan)); glUseProgram(shader.id); - glUniform2f(glGetUniformLocation(shader.id, shader::UNIFORM_VIEW_SIZE), this->size.x, this->size.y); - glUniform2f(glGetUniformLocation(shader.id, shader::UNIFORM_PAN), pan.x, pan.y); - glUniform1f(glGetUniformLocation(shader.id, shader::UNIFORM_ZOOM), zoomFactor); + glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_TRANSFORM), 1, GL_FALSE, value_ptr(transform)); glUniform2f(glGetUniformLocation(shader.id, shader::UNIFORM_SIZE), (float)size.x, (float)size.y); glUniform2f(glGetUniformLocation(shader.id, shader::UNIFORM_OFFSET), (float)offset.x, (float)offset.y); glUniform4f(glGetUniformLocation(shader.id, shader::UNIFORM_COLOR), color.r, color.g, color.b, color.a); @@ -262,7 +261,7 @@ namespace anm2ed::canvas glBindFramebuffer(GL_FRAMEBUFFER, 0); } - void Canvas::zoom_set(float& zoom, vec2& pan, vec2& focus, float step) + void Canvas::zoom_set(float& zoom, vec2& pan, vec2 focus, float step) { auto zoomFactor = math::percent_to_unit(zoom); float newZoom = glm::clamp(math::round_nearest_multiple(zoom + step, step), canvas::ZOOM_MIN, canvas::ZOOM_MAX); @@ -279,4 +278,19 @@ namespace anm2ed::canvas auto zoomFactor = math::percent_to_unit(zoom); return (position - pan - (size * 0.5f)) / zoomFactor; } + + void Canvas::set_to_rect(float& zoom, vec2& pan, vec4 rect) + { + if (rect != vec4(-1.0f) && (rect.z > 0 && rect.w > 0)) + { + f32 scaleX = size.x / rect.z; + f32 scaleY = size.y / rect.w; + f32 fitScale = std::min(scaleX, scaleY); + + zoom = math::unit_to_percent(fitScale); + + vec2 rectCenter = {rect.x + rect.z * 0.5f, rect.y + rect.w * 0.5f}; + pan = -rectCenter * fitScale; + } + } } diff --git a/src/canvas.h b/src/canvas.h index 573d22d..271bc6e 100644 --- a/src/canvas.h +++ b/src/canvas.h @@ -9,6 +9,7 @@ namespace anm2ed::canvas { constexpr float TEXTURE_VERTICES[] = {0, 0, 0.0f, 0.0f, 1, 0, 1.0f, 0.0f, 1, 1, 1.0f, 1.0f, 0, 1, 0.0f, 1.0f}; + constexpr auto PIVOT_SIZE = glm::vec2(8, 8); constexpr auto ZOOM_MIN = 1.0f; constexpr auto ZOOM_MAX = 2000.0f; constexpr auto POSITION_FORMAT = "Position: ({:8} {:8})"; @@ -49,7 +50,8 @@ namespace anm2ed::canvas void clear(glm::vec4&); void bind(); void unbind(); - void zoom_set(float&, glm::vec2&, glm::vec2&, float); + void zoom_set(float&, glm::vec2&, glm::vec2, float); glm::vec2 position_translate(float&, glm::vec2&, glm::vec2); + void set_to_rect(float& zoom, glm::vec2& pan, glm::vec4 rect); }; } diff --git a/src/document.cpp b/src/document.cpp index 7cdf70e..ff46ba3 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -13,6 +13,8 @@ using namespace anm2ed::toast; using namespace anm2ed::types; using namespace anm2ed::util; +using namespace glm; + namespace anm2ed::document { Document::Document(const std::string& path, bool isNew, std::string* errorString) @@ -158,6 +160,110 @@ namespace anm2ed::document change(change::FRAMES); } + void Document::frame_crop_set(anm2::Frame* frame, vec2 crop) + { + if (!frame) return; + snapshot("Frame Crop"); + frame->crop = crop; + change(change::FRAMES); + } + + void Document::frame_size_set(anm2::Frame* frame, vec2 size) + { + if (!frame) return; + snapshot("Frame Size"); + frame->size = size; + change(change::FRAMES); + } + + void Document::frame_position_set(anm2::Frame* frame, vec2 position) + { + if (!frame) return; + snapshot("Frame Position"); + frame->position = position; + change(change::FRAMES); + } + + void Document::frame_pivot_set(anm2::Frame* frame, vec2 pivot) + { + if (!frame) return; + snapshot("Frame Pivot"); + frame->pivot = pivot; + change(change::FRAMES); + } + + void Document::frame_scale_set(anm2::Frame* frame, vec2 scale) + { + if (!frame) return; + snapshot("Frame Scale"); + frame->scale = scale; + change(change::FRAMES); + } + + void Document::frame_rotation_set(anm2::Frame* frame, float rotation) + { + if (!frame) return; + snapshot("Frame Rotation"); + frame->rotation = rotation; + change(change::FRAMES); + } + + void Document::frame_delay_set(anm2::Frame* frame, int delay) + { + if (!frame) return; + snapshot("Frame Delay"); + frame->delay = delay; + change(change::FRAMES); + } + + void Document::frame_tint_set(anm2::Frame* frame, vec4 tint) + { + if (!frame) return; + snapshot("Frame Tint"); + frame->tint = tint; + change(change::FRAMES); + } + + void Document::frame_offset_set(anm2::Frame* frame, vec3 offset) + { + if (!frame) return; + snapshot("Frame Color Offset"); + frame->offset = offset; + change(change::FRAMES); + } + + void Document::frame_is_visible_set(anm2::Frame* frame, bool isVisible) + { + if (!frame) return; + snapshot("Frame Visibility"); + frame->isVisible = isVisible; + change(change::FRAMES); + } + + void Document::frame_is_interpolated_set(anm2::Frame* frame, bool isInterpolated) + { + if (!frame) return; + snapshot("Frame Interpolation"); + frame->isInterpolated = isInterpolated; + change(change::FRAMES); + } + + void Document::frame_flip_x(anm2::Frame* frame) + { + if (!frame) return; + snapshot("Frame Flip X"); + frame->scale.x = -frame->scale.x; + change(change::FRAMES); + } + + void Document::frame_flip_y(anm2::Frame* frame) + { + if (!frame) return; + snapshot("Frame Flip Y"); + frame->scale.y = -frame->scale.y; + change(change::FRAMES); + } + anm2::Item* Document::item_get() { return anm2.item_get(reference); @@ -301,16 +407,6 @@ namespace anm2ed::document toasts.error(std::format("Failed to deserialize event(s): {}", errorString)); } - void Document::item_visible_toggle(anm2::Item* item) - { - if (!item) return; - - snapshot("Item Visible"); - item->isVisible = !item->isVisible; - - change(change::ITEMS); - } - void Document::item_add(anm2::Type type, int id, std::string& name, locale::Type locale, int spritesheetID) { snapshot("Add Item"); @@ -330,15 +426,21 @@ namespace anm2ed::document void Document::item_remove(anm2::Animation* animation) { - snapshot("Remove Item"); - if (!animation) return; - + snapshot("Remove Item"); animation->item_remove(reference.itemType, reference.itemID); reference = {reference.animationIndex}; change(change::ITEMS); } + void Document::item_visible_toggle(anm2::Item* item) + { + if (!item) return; + snapshot("Item Visibility"); + item->isVisible = !item->isVisible; + change(change::ITEMS); + } + anm2::Animation* Document::animation_get() { return anm2.animation_get(reference); diff --git a/src/document.h b/src/document.h index 69b27ff..d7c92c2 100644 --- a/src/document.h +++ b/src/document.h @@ -73,12 +73,27 @@ namespace anm2ed::document anm2::Frame* frame_get(); void frames_add(anm2::Item* item); + void frames_change(); void frames_delete(anm2::Item* item); void frames_bake(int, bool, bool); + void frame_crop_set(anm2::Frame*, glm::vec2); + void frame_size_set(anm2::Frame*, glm::vec2); + void frame_position_set(anm2::Frame*, glm::vec2); + void frame_pivot_set(anm2::Frame*, glm::vec2); + void frame_scale_set(anm2::Frame*, glm::vec2); + void frame_rotation_set(anm2::Frame*, float); + void frame_delay_set(anm2::Frame*, int); + void frame_tint_set(anm2::Frame*, glm::vec4); + void frame_offset_set(anm2::Frame*, glm::vec3); + void frame_is_visible_set(anm2::Frame*, bool); + void frame_is_interpolated_set(anm2::Frame*, bool); + void frame_flip_x(anm2::Frame* frame); + void frame_flip_y(anm2::Frame* frame); anm2::Item* item_get(); void item_add(anm2::Type, int, std::string&, types::locale::Type, int); void item_remove(anm2::Animation* animation); + void item_visible_toggle(anm2::Item*); anm2::Spritesheet* spritesheet_get(); void spritesheet_add(const std::string&); @@ -97,8 +112,6 @@ namespace anm2ed::document void events_remove_unused(); void events_deserialize(const std::string&, types::merge::Type); - void item_visible_toggle(anm2::Item*); - void animation_add(); void animation_duplicate(); void animation_default(); diff --git a/src/frame_properties.cpp b/src/frame_properties.cpp index 57e75f0..2ab5320 100644 --- a/src/frame_properties.cpp +++ b/src/frame_properties.cpp @@ -25,8 +25,8 @@ namespace anm2ed::frame_properties auto& anm2 = document.anm2; auto& reference = document.reference; auto& type = reference.itemType; - auto& isRound = settings.propertiesIsRound; auto frame = document.frame_get(); + auto useFrame = frame ? *frame : anm2::Frame(); ImGui::BeginDisabled(!frame); { @@ -44,77 +44,75 @@ namespace anm2ed::frame_properties } else { - ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_); { - if (ImGui::InputFloat2("Crop", frame ? value_ptr(frame->crop) : &dummy_value(), - frame ? vec2_format_get(frame->crop) : "")) - if (isRound) frame->crop = ivec2(frame->crop); + if (ImGui::InputFloat2("Crop", frame ? value_ptr(useFrame.crop) : &dummy_value(), + frame ? vec2_format_get(useFrame.crop) : "")) + document.frame_crop_set(frame, useFrame.crop); ImGui::SetItemTooltip("%s", "Change the crop position the frame uses."); - if (ImGui::InputFloat2("Size", frame ? value_ptr(frame->size) : &dummy_value(), - frame ? vec2_format_get(frame->size) : "")) - if (isRound) frame->crop = ivec2(frame->size); + if (ImGui::InputFloat2("Size", frame ? value_ptr(useFrame.size) : &dummy_value(), + frame ? vec2_format_get(useFrame.size) : "")) + document.frame_size_set(frame, useFrame.size); ImGui::SetItemTooltip("%s", "Change the size of the crop the frame uses."); } ImGui::EndDisabled(); - if (ImGui::InputFloat2("Position", frame ? value_ptr(frame->position) : &dummy_value(), - frame ? vec2_format_get(frame->position) : "")) - if (isRound) frame->position = ivec2(frame->position); + if (ImGui::InputFloat2("Position", frame ? value_ptr(useFrame.position) : &dummy_value(), + frame ? vec2_format_get(useFrame.position) : "")) + document.frame_position_set(frame, useFrame.position); ImGui::SetItemTooltip("%s", "Change the position of the frame."); ImGui::BeginDisabled(type == anm2::ROOT || type == anm2::NULL_); { - if (ImGui::InputFloat2("Pivot", frame ? value_ptr(frame->pivot) : &dummy_value(), - frame ? vec2_format_get(frame->pivot) : "")) - if (isRound) frame->position = ivec2(frame->position); + if (ImGui::InputFloat2("Pivot", frame ? value_ptr(useFrame.pivot) : &dummy_value(), + frame ? vec2_format_get(useFrame.pivot) : "")) + document.frame_pivot_set(frame, useFrame.pivot); ImGui::SetItemTooltip("%s", "Change the pivot of the frame; i.e., where it is centered."); } ImGui::EndDisabled(); - if (ImGui::InputFloat2("Scale", frame ? value_ptr(frame->scale) : &dummy_value(), - frame ? vec2_format_get(frame->scale) : "")) - if (isRound) frame->position = ivec2(frame->position); + if (ImGui::InputFloat2("Scale", frame ? value_ptr(useFrame.scale) : &dummy_value(), + frame ? vec2_format_get(useFrame.scale) : "")) + document.frame_scale_set(frame, useFrame.scale); ImGui::SetItemTooltip("%s", "Change the scale of the frame, in percent."); - if (ImGui::InputFloat("Rotation", frame ? &frame->rotation : &dummy_value(), step::NORMAL, step::FAST, - frame ? float_format_get(frame->rotation) : "")) - if (isRound) frame->rotation = (int)frame->rotation; + if (ImGui::InputFloat("Rotation", frame ? &useFrame.rotation : &dummy_value(), step::NORMAL, + step::FAST, frame ? float_format_get(useFrame.rotation) : "")) + document.frame_rotation_set(frame, useFrame.rotation); ImGui::SetItemTooltip("%s", "Change the rotation of the frame."); - ImGui::InputInt("Duration", frame ? &frame->delay : &dummy_value(), step::NORMAL, step::FAST, - !frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0); + if (ImGui::InputInt("Duration", frame ? &useFrame.delay : &dummy_value(), step::NORMAL, step::FAST, + !frame ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0)) + document.frame_delay_set(frame, useFrame.delay); ImGui::SetItemTooltip("%s", "Change how long the frame lasts."); - ImGui::ColorEdit4("Tint", frame ? value_ptr(frame->tint) : &dummy_value()); + if (ImGui::ColorEdit4("Tint", frame ? value_ptr(useFrame.tint) : &dummy_value())) + document.frame_tint_set(frame, useFrame.tint); ImGui::SetItemTooltip("%s", "Change the tint of the frame."); - ImGui::ColorEdit3("Color Offset", frame ? value_ptr(frame->offset) : &dummy_value()); + + if (ImGui::ColorEdit3("Color Offset", frame ? value_ptr(useFrame.offset) : &dummy_value())) + document.frame_offset_set(frame, useFrame.offset); ImGui::SetItemTooltip("%s", "Change the color added onto the frame."); - ImGui::Checkbox("Visible", frame ? &frame->isVisible : &dummy_value()); + if (ImGui::Checkbox("Visible", frame ? &useFrame.isVisible : &dummy_value())) + document.frame_is_visible_set(frame, useFrame.isVisible); ImGui::SetItemTooltip("%s", "Toggle the frame's visibility."); + ImGui::SameLine(); - ImGui::Checkbox("Interpolated", frame ? &frame->isInterpolated : &dummy_value()); + + if (ImGui::Checkbox("Interpolated", frame ? &useFrame.isInterpolated : &dummy_value())) + document.frame_is_interpolated_set(frame, useFrame.isInterpolated); ImGui::SetItemTooltip( "%s", "Toggle the frame interpolating; i.e., blending its values into the next frame based on the time."); - ImGui::SameLine(); - ImGui::EndDisabled(); - ImGui::Checkbox("Round", &settings.propertiesIsRound); - ImGui::BeginDisabled(!frame); - ImGui::SetItemTooltip( - "%s", "When toggled, decimal values will be snapped to their nearest whole value when changed."); - auto widgetSize = imgui::widget_size_with_row_get(2); - if (ImGui::Button("Flip X", widgetSize)) - if (frame) frame->scale.x = -frame->scale.x; + if (ImGui::Button("Flip X", widgetSize)) document.frame_flip_x(frame); ImGui::SetItemTooltip("%s", "Flip the horizontal scale of the frame, to cheat mirroring the frame " "horizontally.\n(Note: the format does not support mirroring.)"); ImGui::SameLine(); - if (ImGui::Button("Flip Y", widgetSize)) - if (frame) frame->scale.y = -frame->scale.y; + if (ImGui::Button("Flip Y", widgetSize)) document.frame_flip_y(frame); ImGui::SetItemTooltip("%s", "Flip the vertical scale of the frame, to cheat mirroring the frame " "vertically.\n(Note: the format does not support mirroring.)"); } diff --git a/src/imgui.cpp b/src/imgui.cpp index 883c15d..1224aad 100644 --- a/src/imgui.cpp +++ b/src/imgui.cpp @@ -236,8 +236,8 @@ namespace anm2ed::imgui bool shortcut(std::string string, shortcut::Type type) { if (ImGui::GetTopMostPopupModal() != nullptr) return false; - auto flags = type == shortcut::GLOBAL || type == shortcut::GLOBAL_SET ? ImGuiInputFlags_RouteGlobal - : ImGuiInputFlags_RouteFocused; + int flags = type == shortcut::GLOBAL || type == shortcut::GLOBAL_SET ? ImGuiInputFlags_RouteGlobal + : ImGuiInputFlags_RouteFocused; if (type == shortcut::GLOBAL_SET || type == shortcut::FOCUSED_SET) { ImGui::SetNextItemShortcut(string_to_chord(string), flags); diff --git a/src/shader.h b/src/shader.h index c29db47..be761fc 100644 --- a/src/shader.h +++ b/src/shader.h @@ -66,25 +66,24 @@ namespace anm2ed::shader )"; constexpr auto GRID_VERTEX = R"( -#version 330 core -layout (location = 0) in vec2 i_position; -layout (location = 1) in vec2 i_uv; + #version 330 core + layout (location = 0) in vec2 i_position; + layout (location = 1) in vec2 i_uv; -out vec2 i_uv_out; + out vec2 i_uv_out; -void main() { - i_uv_out = i_position; - gl_Position = vec4(i_position, 0.0, 1.0); -} + void main() + { + i_uv_out = i_position; + gl_Position = vec4(i_position, 0.0, 1.0); + } )"; constexpr auto GRID_FRAGMENT = R"( #version 330 core in vec2 i_uv_out; - uniform vec2 u_view_size; - uniform vec2 u_pan; - uniform float u_zoom; + uniform mat4 u_transform; uniform vec2 u_size; uniform vec2 u_offset; uniform vec4 u_color; @@ -93,18 +92,15 @@ void main() { void main() { - vec2 viewSize = max(u_view_size, vec2(1.0)); - float zoom = max(u_zoom, 1e-6); - vec2 pan = u_pan; - - vec2 world = (i_uv_out - (2.0 * pan / viewSize)) * (viewSize / (2.0 * zoom)); - world += vec2(0.5); // Half pixel nudge + vec4 world4 = u_transform * vec4(i_uv_out, 0.0, 1.0); + vec2 world = world4.xy / world4.w; vec2 cell = max(u_size, vec2(1.0)); vec2 grid = (world - u_offset) / cell; - vec2 d = abs(fract(grid) - 0.5); - float distance = min(d.x, d.y); + vec2 frac = fract(grid); + vec2 distToLine = min(frac, 1.0 - frac); + float distance = min(distToLine.x, distToLine.y); float fw = min(fwidth(grid.x), fwidth(grid.y)); float alpha = 1.0 - smoothstep(0.0, fw, distance); @@ -127,9 +123,6 @@ void main() { constexpr auto UNIFORM_MODEL = "u_model"; constexpr auto UNIFORM_RECT_SIZE = "u_rect_size"; constexpr auto UNIFORM_TEXTURE = "u_texture"; - constexpr auto UNIFORM_VIEW_SIZE = "u_view_size"; - constexpr auto UNIFORM_PAN = "u_pan"; - constexpr auto UNIFORM_ZOOM = "u_zoom"; enum Type { diff --git a/src/spritesheet_editor.cpp b/src/spritesheet_editor.cpp index 81e7141..24c90dc 100644 --- a/src/spritesheet_editor.cpp +++ b/src/spritesheet_editor.cpp @@ -21,6 +21,9 @@ namespace anm2ed::spritesheet_editor void SpritesheetEditor::update(Manager& manager, Settings& settings, Resources& resources) { auto& document = *manager.get(); + auto& anm2 = document.anm2; + auto& reference = document.reference; + auto& referenceSpritesheet = document.referenceSpritesheet; auto& pan = document.editorPan; auto& zoom = document.editorZoom; auto& backgroundColor = settings.editorBackgroundColor; @@ -59,9 +62,16 @@ namespace anm2ed::spritesheet_editor auto widgetSize = ImVec2(imgui::row_widget_width_get(2), 0); - if (ImGui::Button("Center View", widgetSize)) pan = vec2(); + imgui::shortcut(settings.shortcutCenterView); + if (ImGui::Button("Center View", widgetSize)) pan = -size * 0.5f; + imgui::set_item_tooltip_shortcut("Centers the view.", settings.shortcutCenterView); + ImGui::SameLine(); - ImGui::Button("Fit", widgetSize); + + imgui::shortcut(settings.shortcutFit); + if (ImGui::Button("Fit", widgetSize)) + if (spritesheet) set_to_rect(zoom, pan, {0, 0, spritesheet->texture.size.x, spritesheet->texture.size.y}); + imgui::set_item_tooltip_shortcut("Set the view to match the extent of the spritesheet.", settings.shortcutFit); ImGui::TextUnformatted(std::format(POSITION_FORMAT, (int)mousePos.x, (int)mousePos.y).c_str()); } @@ -84,12 +94,27 @@ namespace anm2ed::spritesheet_editor viewport_set(); clear(backgroundColor); + auto frame = document.frame_get(); + if (spritesheet) { auto& texture = spritesheet->texture; - auto transform = transform_get(zoom, pan) * math::quad_model_get(texture.size); - texture_render(shaderTexture, texture.id, transform); - if (isBorder) rect_render(lineShader, transform); + auto transform = transform_get(zoom, pan); + + auto spritesheetTransform = transform * math::quad_model_get(texture.size); + texture_render(shaderTexture, texture.id, spritesheetTransform); + if (isBorder) rect_render(lineShader, spritesheetTransform); + + if (frame && reference.itemID > -1 && + anm2.content.layers.at(reference.itemID).spritesheetID == referenceSpritesheet) + { + auto cropTransform = transform * math::quad_model_get(frame->size, frame->crop); + rect_render(lineShader, cropTransform, color::RED); + + auto pivotTransform = + transform * math::quad_model_get(canvas::PIVOT_SIZE, frame->crop + frame->pivot, PIVOT_SIZE * 0.5f); + texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, color::RED); + } } if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor); @@ -102,19 +127,78 @@ namespace anm2ed::spritesheet_editor { ImGui::SetKeyboardFocusHere(-1); + previousMousePos = mousePos; mousePos = position_translate(zoom, pan, to_vec2(ImGui::GetMousePos()) - to_vec2(cursorScreenPos)); + auto isMouseClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left); auto isMouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left); auto isMouseMiddleDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle); - auto mouseDelta = ImGui::GetIO().MouseDelta; + auto mouseDelta = to_ivec2(ImGui::GetIO().MouseDelta); auto mouseWheel = ImGui::GetIO().MouseWheel; + auto& toolColor = settings.toolColor; auto isZoomIn = imgui::chord_repeating(imgui::string_to_chord(settings.shortcutZoomIn)); auto isZoomOut = imgui::chord_repeating(imgui::string_to_chord(settings.shortcutZoomOut)); + auto isLeft = imgui::chord_repeating(ImGuiKey_LeftArrow); + auto isRight = imgui::chord_repeating(ImGuiKey_RightArrow); + auto isUp = imgui::chord_repeating(ImGuiKey_UpArrow); + auto isDown = imgui::chord_repeating(ImGuiKey_DownArrow); + auto isMod = ImGui::IsKeyDown(ImGuiMod_Shift); + auto step = isMod ? step::FAST : step::NORMAL; + auto useTool = tool; + auto isMouseClick = ImGui::IsMouseClicked(ImGuiMouseButton_Left); + auto isMouseReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left); + auto isLeftPressed = ImGui::IsKeyPressed(ImGuiKey_LeftArrow, false); + auto isRightPressed = ImGui::IsKeyPressed(ImGuiKey_RightArrow, false); + auto isUpPressed = ImGui::IsKeyPressed(ImGuiKey_UpArrow, false); + auto isDownPressed = ImGui::IsKeyPressed(ImGuiKey_DownArrow, false); + auto isLeftReleased = ImGui::IsKeyReleased(ImGuiKey_LeftArrow); + auto isRightReleased = ImGui::IsKeyReleased(ImGuiKey_RightArrow); + auto isUpReleased = ImGui::IsKeyReleased(ImGuiKey_UpArrow); + auto isDownReleased = ImGui::IsKeyReleased(ImGuiKey_DownArrow); + auto frame = document.frame_get(); + auto isKeyPressed = isLeftPressed || isRightPressed || isUpPressed || isDownPressed; + auto isKeyReleased = isLeftReleased || isRightReleased || isUpReleased || isDownReleased; + auto isBegin = isMouseClick || isKeyPressed; + auto isEnd = isMouseReleased || isKeyReleased; - if ((tool == tool::PAN && isMouseDown) || isMouseMiddleDown) pan += vec2(mouseDelta.x, mouseDelta.y); + if (isMouseMiddleDown) useTool = tool::PAN; - switch (tool) + switch (useTool) { + case tool::PAN: + if (isMouseDown || isMouseMiddleDown) pan += mouseDelta; + break; + case tool::MOVE: + if (!frame) break; + if (isBegin) document.snapshot("Frame Pivot"); + if (isMouseDown) frame->pivot = ivec2(mousePos - frame->crop); + if (isLeft) frame->pivot.x -= step; + if (isRight) frame->pivot.x += step; + if (isUp) frame->pivot.y -= step; + if (isDown) frame->pivot.y += step; + if (isEnd) document.change(change::FRAMES); + break; + case tool::CROP: + if (!frame) break; + if (isBegin) document.snapshot(isMod ? "Frame Size" : "Frame Crop"); + if (isMouseClicked) frame->crop = ivec2(mousePos); + if (isMouseDown) frame->size = ivec2(mousePos - frame->crop); + if (isLeft) isMod ? frame->size.x -= step : frame->crop.x -= step; + if (isRight) isMod ? frame->size.x += step : frame->crop.x += step; + if (isUp) isMod ? frame->size.y -= step : frame->crop.y -= step; + if (isDown) isMod ? frame->size.y += step : frame->crop.y += step; + if (isEnd) document.change(change::FRAMES); + break; + case tool::DRAW: + case tool::ERASE: + { + if (!spritesheet) break; + if (isMouseClicked) document.snapshot(tool == tool::DRAW ? "Draw" : "Erase"); + auto color = tool == tool::DRAW ? toolColor : vec4(); + if (isMouseDown) spritesheet->texture.pixel_line(ivec2(previousMousePos), ivec2(mousePos), color); + if (isMouseReleased) document.change(change::FRAMES); + break; + } default: break; } diff --git a/src/spritesheet_editor.h b/src/spritesheet_editor.h index c1b1c00..13fdb13 100644 --- a/src/spritesheet_editor.h +++ b/src/spritesheet_editor.h @@ -10,6 +10,7 @@ namespace anm2ed::spritesheet_editor class SpritesheetEditor : public canvas::Canvas { glm::vec2 mousePos{}; + glm::vec2 previousMousePos{}; public: SpritesheetEditor(); diff --git a/src/texture.cpp b/src/texture.cpp index 76c9761..306889a 100644 --- a/src/texture.cpp +++ b/src/texture.cpp @@ -20,6 +20,9 @@ #pragma GCC diagnostic pop #endif +#include "math.h" + +using namespace anm2ed::math; using namespace glm; namespace anm2ed::texture @@ -140,6 +143,55 @@ namespace anm2ed::texture return stbi_write_png(path.c_str(), size.x, size.y, CHANNELS, this->pixels.data(), size.x * CHANNELS); } + void Texture::pixel_set(ivec2 position, vec4 color) + { + if (position.x < 0 || position.y < 0 || position.x >= size.x || position.y >= size.y) return; + + uint8 rgba8[4] = {(uint8)float_to_uint8(color.r), (uint8)float_to_uint8(color.g), (uint8)float_to_uint8(color.b), + (uint8)float_to_uint8(color.a)}; + + glBindTexture(GL_TEXTURE_2D, id); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexSubImage2D(GL_TEXTURE_2D, 0, position.x, position.y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, rgba8); + glBindTexture(GL_TEXTURE_2D, 0); + } + + void Texture::pixel_line(ivec2 start, ivec2 end, vec4 color) + { + auto plot = [&](ivec2 pos) + { + pixel_set(pos, color); + }; + + int x0 = start.x; + int y0 = start.y; + int x1 = end.x; + int y1 = end.y; + + int dx = std::abs(x1 - x0); + int dy = -std::abs(y1 - y0); + int sx = x0 < x1 ? 1 : -1; + int sy = y0 < y1 ? 1 : -1; + int err = dx + dy; + + while (true) + { + plot({x0, y0}); + if (x0 == x1 && y0 == y1) break; + int e2 = 2 * err; + if (e2 >= dy) + { + err += dy; + x0 += sx; + } + if (e2 <= dx) + { + err += dx; + y0 += sy; + } + } + } + void Texture::bind(GLuint unit) { glActiveTexture(GL_TEXTURE0 + unit); diff --git a/src/texture.h b/src/texture.h index 9fc8076..a5e5b0e 100644 --- a/src/texture.h +++ b/src/texture.h @@ -33,6 +33,8 @@ namespace anm2ed::texture Texture(const char*, size_t, glm::ivec2); Texture(const std::string&); bool write_png(const std::string&); + void pixel_set(glm::ivec2, glm::vec4); + void pixel_line(glm::ivec2, glm::ivec2, glm::vec4); void bind(GLuint = 0); void unbind(GLuint = 0); }; diff --git a/src/timeline.cpp b/src/timeline.cpp index dd47ff6..e0e71f6 100644 --- a/src/timeline.cpp +++ b/src/timeline.cpp @@ -46,8 +46,8 @@ namespace anm2ed::timeline constexpr auto HELP_FORMAT = R"(- Press {} to decrement time. - Press {} to increment time. -- Press {} to extend the selected frame, by one frame. - Press {} to shorten the selected frame, by one frame. +- Press {} to extend the selected frame, by one frame. - Hold Alt while clicking a non-trigger frame to toggle interpolation.)"; void Timeline::item_child(Manager& manager, Document& document, anm2::Animation* animation, Settings& settings, @@ -479,7 +479,8 @@ namespace anm2ed::timeline if (ImGui::Button("##Frame Button", size)) { - if (type != anm2::TRIGGER && ImGui::IsKeyDown(ImGuiMod_Alt)) frame.isInterpolated = !frame.isInterpolated; + if (type != anm2::TRIGGER && ImGui::IsKeyDown(ImGuiMod_Alt)) + document.frame_is_interpolated_set(&frame, !frame.isInterpolated); if (type == anm2::LAYER) { document.referenceSpritesheet = anm2.content.layers[id].spritesheetID;