#include "animation_preview.h" #include #include #include #include #include #include "imgui_.h" #include "log.h" #include "math_.h" #include "toast.h" #include "tool.h" #include "types.h" using namespace anm2ed::canvas; using namespace anm2ed::types; using namespace anm2ed::util; using namespace anm2ed::resource; using namespace anm2ed::resource::texture; using namespace glm; namespace anm2ed::imgui { constexpr auto NULL_COLOR = vec4(0.0f, 0.0f, 1.0f, 0.90f); 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_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()) {} void AnimationPreview::tick(Manager& manager, Settings& settings) { auto& document = *manager.get(); auto& anm2 = document.anm2; auto& playback = document.playback; auto& frameTime = document.frameTime; auto& end = manager.recordingEnd; auto& zoom = document.previewZoom; auto& overlayIndex = document.overlayIndex; auto& pan = document.previewPan; if (manager.isRecording) { auto& ffmpegPath = settings.renderFFmpegPath; auto& path = settings.renderPath; auto& type = settings.renderType; if (playback.time > end || playback.isFinished) { if (type == render::PNGS) { auto& format = settings.renderFormat; bool isSuccess{true}; for (auto [i, frame] : std::views::enumerate(renderFrames)) { std::filesystem::path outputPath = std::filesystem::path(path) / std::vformat(format, std::make_format_args(i)); if (!frame.write_png(outputPath)) { isSuccess = false; break; } logger.info(std::format("Saved frame to: {}", outputPath.string())); } if (isSuccess) toasts.info(std::format("Exported rendered frames to: {}", path)); else toasts.warning(std::format("Could not export frames to: {}", path)); } else if (type == render::SPRITESHEET) { auto& rows = settings.renderRows; auto& columns = settings.renderColumns; if (renderFrames.empty()) toasts.warning("No frames captured for spritesheet export."); else { const auto& firstFrame = renderFrames.front(); if (firstFrame.size.x <= 0 || firstFrame.size.y <= 0 || firstFrame.pixels.empty()) toasts.warning("Spritesheet export failed: captured frames are empty."); else { auto frameWidth = firstFrame.size.x; auto frameHeight = firstFrame.size.y; ivec2 spritesheetSize = ivec2(frameWidth * columns, frameHeight * rows); std::vector spritesheet((size_t)(spritesheetSize.x) * spritesheetSize.y * CHANNELS); for (std::size_t index = 0; index < renderFrames.size(); ++index) { const auto& frame = renderFrames[index]; auto row = (int)(index / columns); auto column = (int)(index % columns); if (row >= rows || column >= columns) break; if ((int)frame.pixels.size() < frameWidth * frameHeight * CHANNELS) continue; for (int y = 0; y < frameHeight; ++y) { auto destY = (size_t)(row * frameHeight + y); auto destX = (size_t)(column * frameWidth); auto destOffset = (destY * spritesheetSize.x + destX) * CHANNELS; auto srcOffset = (size_t)(y * frameWidth) * CHANNELS; std::copy_n(frame.pixels.data() + srcOffset, frameWidth * CHANNELS, spritesheet.data() + destOffset); } } Texture spritesheetTexture(spritesheet.data(), spritesheetSize); if (spritesheetTexture.write_png(path)) toasts.info(std::format("Exported spritesheet to: {}", path)); else toasts.warning(std::format("Could not export spritesheet to: {}", path)); } } } else { if (animation_render(ffmpegPath, path, renderFrames, audioStream, (render::Type)type, size, anm2.info.fps)) toasts.info(std::format("Exported rendered animation to: {}", path)); else toasts.warning(std::format("Could not output rendered animation: {}", path)); } renderFrames.clear(); if (settings.renderIsRawAnimation) { settings = savedSettings; pan = savedPan; zoom = savedZoom; overlayIndex = savedOverlayIndex; isSizeTrySet = true; hasPendingZoomPanAdjust = false; isCheckerPanInitialized = false; } if (settings.timelineIsSound) audioStream.capture_end(mixer); playback.isPlaying = false; playback.isFinished = false; manager.isRecording = false; manager.progressPopup.close(); } else { bind(); auto pixels = pixels_get(); renderFrames.push_back(Texture(pixels.data(), size)); } } if (playback.isPlaying) { auto animation = document.animation_get(); auto& isSound = settings.timelineIsSound; auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; if (!anm2.content.sounds.empty() && isSound) { if (auto animation = document.animation_get(); animation && animation->triggers.isVisible && (!isOnlyShowLayers || manager.isRecording)) { if (auto trigger = animation->triggers.frame_generate(playback.time, anm2::TRIGGER); trigger.isVisible) if (anm2.content.sounds.contains(trigger.soundID)) anm2.content.sounds[trigger.soundID].audio.play(false, mixer); } } playback.tick(anm2.info.fps, animation->frameNum, (animation->isLoop || settings.playbackIsLoop) && !manager.isRecording); frameTime = playback.time; } } void AnimationPreview::update(Manager& manager, Settings& settings, Resources& resources) { auto& document = *manager.get(); auto& anm2 = document.anm2; auto& playback = document.playback; auto& reference = document.reference; auto animation = document.animation_get(); auto& pan = document.previewPan; auto& zoom = document.previewZoom; auto& backgroundColor = settings.previewBackgroundColor; auto& axesColor = settings.previewAxesColor; auto& gridColor = settings.previewGridColor; auto& gridSize = settings.previewGridSize; auto& gridOffset = settings.previewGridOffset; auto& zoomStep = settings.inputZoomStep; auto& isGrid = settings.previewIsGrid; auto& overlayTransparency = settings.previewOverlayTransparency; auto& overlayIndex = document.overlayIndex; auto& isRootTransform = settings.previewIsRootTransform; auto& isPivots = settings.previewIsPivots; auto& isAxes = settings.previewIsAxes; auto& isAltIcons = settings.previewIsAltIcons; auto& isBorder = settings.previewIsBorder; 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)) { auto childSize = ImVec2(row_widget_width_get(4), (ImGui::GetTextLineHeightWithSpacing() * 4) + (ImGui::GetStyle().WindowPadding.y * 2)); if (ImGui::BeginChild("##Grid Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar)) { ImGui::Checkbox("Grid", &isGrid); ImGui::SetItemTooltip("Toggle the visibility of the grid."); ImGui::SameLine(); ImGui::ColorEdit4("Color", value_ptr(gridColor), ImGuiColorEditFlags_NoInputs); ImGui::SetItemTooltip("Change the grid's color."); input_int2_range("Size", gridSize, ivec2(GRID_SIZE_MIN), ivec2(GRID_SIZE_MAX)); ImGui::SetItemTooltip("Change the size of all cells in the grid."); input_int2_range("Offset", gridOffset, ivec2(GRID_OFFSET_MIN), ivec2(GRID_OFFSET_MAX)); ImGui::SetItemTooltip("Change the offset of the grid."); } ImGui::EndChild(); ImGui::SameLine(); if (ImGui::BeginChild("##View Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar)) { ImGui::InputFloat("Zoom", &zoom, zoomStep, zoomStep, "%.0f%%"); ImGui::SetItemTooltip("Change the zoom of the preview."); auto widgetSize = widget_size_with_row_get(2); shortcut(manager.chords[SHORTCUT_CENTER_VIEW]); if (ImGui::Button("Center View", widgetSize)) pan = vec2(); set_item_tooltip_shortcut("Centers the view.", settings.shortcutCenterView); ImGui::SameLine(); shortcut(manager.chords[SHORTCUT_FIT]); if (ImGui::Button("Fit", widgetSize)) if (animation) set_to_rect(zoom, pan, animation->rect(isRootTransform)); 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(); ImGui::SameLine(); if (ImGui::BeginChild("##Background Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar)) { ImGui::ColorEdit3("Background", value_ptr(backgroundColor), ImGuiColorEditFlags_NoInputs); ImGui::SetItemTooltip("Change the background color."); ImGui::SameLine(); ImGui::Checkbox("Axes", &isAxes); ImGui::SetItemTooltip("Toggle the axes' visbility."); ImGui::SameLine(); ImGui::ColorEdit4("Color", value_ptr(axesColor), ImGuiColorEditFlags_NoInputs); ImGui::SetItemTooltip("Set the color of the axes."); combo_negative_one_indexed("Overlay", &overlayIndex, document.animation.labels); ImGui::SetItemTooltip("Set an animation to be drawn over the current animation."); ImGui::InputFloat("Alpha", &overlayTransparency, 0, 0, "%.0f"); ImGui::SetItemTooltip("Set the alpha of the overlayed animation."); } ImGui::EndChild(); ImGui::SameLine(); if (ImGui::BeginChild("##Helpers Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar)) { auto helpersChildSize = ImVec2(row_widget_width_get(2), ImGui::GetContentRegionAvail().y); if (ImGui::BeginChild("##Helpers Child 1", helpersChildSize)) { ImGui::Checkbox("Root Transform", &isRootTransform); ImGui::SetItemTooltip("Root frames will transform the rest of the animation."); ImGui::Checkbox("Pivots", &isPivots); ImGui::SetItemTooltip("Toggle the visibility of the animation's pivots."); } ImGui::EndChild(); ImGui::SameLine(); if (ImGui::BeginChild("##Helpers Child 2", helpersChildSize)) { ImGui::Checkbox("Alt Icons", &isAltIcons); ImGui::SetItemTooltip("Toggle a different appearance of the target icons."); ImGui::Checkbox("Border", &isBorder); ImGui::SetItemTooltip("Toggle the visibility of borders around layers."); } ImGui::EndChild(); } ImGui::EndChild(); auto cursorScreenPos = ImGui::GetCursorScreenPos(); auto min = cursorScreenPos; auto max = to_imvec2(to_vec2(min) + size); if (manager.isRecordingStart) { savedSettings = settings; if (settings.timelineIsSound) audioStream.capture_begin(mixer); if (settings.renderIsRawAnimation) { settings.previewBackgroundColor = vec4(); settings.previewIsGrid = false; settings.previewIsAxes = false; settings.previewIsBorder = false; settings.timelineIsOnlyShowLayers = true; settings.onionskinIsEnabled = false; savedOverlayIndex = overlayIndex; savedZoom = zoom; savedPan = pan; if (auto rect = document.animation_get()->rect(isRootTransform); rect != vec4(-1.0f)) { size_set(vec2(rect.z, rect.w) * settings.renderScale); set_to_rect(zoom, pan, rect); } isSizeTrySet = false; } manager.isRecordingStart = false; manager.isRecording = true; playback.isPlaying = true; playback.time = manager.recordingStart; } if (isSizeTrySet) size_set(to_vec2(ImGui::GetContentRegionAvail())); bind(); viewport_set(); clear(manager.isRecording && settings.renderIsRawAnimation ? vec4() : vec4(backgroundColor, 1.0f)); if (isAxes) axes_render(shaderAxes, zoom, pan, axesColor); if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor); auto baseTransform = transform_get(zoom, pan); auto frameTime = document.frameTime > -1 && !playback.isPlaying ? document.frameTime : playback.time; struct OnionskinSample { float time{}; int indexOffset{}; vec3 colorOffset{}; float alphaOffset{}; }; 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); float alphaOffset = (1.0f / (count + 1)) * i; OnionskinSample sample{}; sample.time = useTime; sample.colorOffset = color; sample.alphaOffset = alphaOffset; sample.indexOffset = direction * i; 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, bool isIndexMode = false) { auto sample_time_for_item = [&](anm2::Item& item, const OnionskinSample& sample) -> std::optional { if (!isIndexMode) { if (sample.time < 0.0f || sample.time > animation->frameNum) return std::nullopt; return sample.time; } if (item.frames.empty()) return std::nullopt; int baseIndex = item.frame_index_from_time_get(frameTime); if (baseIndex < 0) return std::nullopt; int sampleIndex = baseIndex + sample.indexOffset; if (sampleIndex < 0 || sampleIndex >= (int)item.frames.size()) return std::nullopt; return item.frame_time_from_index_get(sampleIndex); }; auto transform_for_time = [&](anm2::Animation* anim, float t) { auto sampleTransform = baseTransform; if (isRootTransform) { auto rootFrame = anim->rootAnimation.frame_generate(t, anm2::ROOT); sampleTransform *= math::quad_model_parent_get(rootFrame.position, {}, math::percent_to_unit(rootFrame.scale), rootFrame.rotation); } return sampleTransform; }; auto transform = transform_for_time(animation, time); auto draw_root = [&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor, float sampleAlpha, bool isOnion) { auto rootFrame = animation->rootAnimation.frame_generate(sampleTime, anm2::ROOT); if (isOnlyShowLayers || !rootFrame.isVisible || !animation->rootAnimation.isVisible) return; auto rootModel = isRootTransform ? math::quad_model_get(TARGET_SIZE, {}, TARGET_SIZE * 0.5f) : math::quad_model_get(TARGET_SIZE, rootFrame.position, TARGET_SIZE * 0.5f, math::percent_to_unit(rootFrame.scale), rootFrame.rotation); auto rootTransform = sampleTransform * rootModel; 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) if (auto sampleTime = sample_time_for_item(animation->rootAnimation, sample)) { auto sampleTransform = transform_for_time(animation, *sampleTime); draw_root(*sampleTime, sampleTransform, sample.colorOffset, sample.alphaOffset, true); } draw_root(time, transform, {}, 0.0f, false); for (auto& id : animation->layerOrder) { auto& layerAnimation = animation->layerAnimations[id]; if (!layerAnimation.isVisible) continue; auto& layer = anm2.content.layers.at(id); 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) { if (auto frame = layerAnimation.frame_generate(sampleTime, anm2::LAYER); frame.isVisible) { 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 = 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) if (auto sampleTime = sample_time_for_item(layerAnimation, sample)) { auto sampleTransform = transform_for_time(animation, *sampleTime); draw_layer(*sampleTime, sampleTransform, sample.colorOffset, sample.alphaOffset, true); } draw_layer(time, transform, {}, 0.0f, false); } for (auto& [id, nullAnimation] : animation->nullAnimations) { if (!nullAnimation.isVisible || isOnlyShowLayers) continue; auto& isShowRect = anm2.content.nulls[id].isShowRect; auto draw_null = [&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor, float sampleAlpha, bool isOnion) { if (auto frame = nullAnimation.frame_generate(sampleTime, anm2::NULL_); frame.isVisible) { 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 nullTransform = sampleTransform * nullModel; 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) if (auto sampleTime = sample_time_for_item(nullAnimation, sample)) { auto sampleTransform = transform_for_time(animation, *sampleTime); draw_null(*sampleTime, sampleTransform, sample.colorOffset, sample.alphaOffset, true); } draw_null(time, transform, {}, 0.0f, false); } }; if (animation) { auto layeredOnions = settings.onionskinIsEnabled ? &onionskinSamples : nullptr; render(animation, frameTime, {}, 0.0f, layeredOnions, settings.onionskinMode == static_cast(OnionskinMode::INDEX)); if (auto overlayAnimation = anm2.animation_get(overlayIndex)) render(overlayAnimation, frameTime, {}, 1.0f - math::uint8_to_float(overlayTransparency), layeredOnions, settings.onionskinMode == static_cast(OnionskinMode::INDEX)); } unbind(); sync_checker_pan(); render_checker_background(ImGui::GetWindowDrawList(), min, max, -size - checkerPan, CHECKER_SIZE); ImGui::Image(texture, to_imvec2(size)); isPreviewHovered = ImGui::IsItemHovered(); if (animation && animation->triggers.isVisible && !isOnlyShowLayers && !manager.isRecording) { if (auto trigger = animation->triggers.frame_generate(frameTime, anm2::TRIGGER); trigger.isVisible && trigger.eventID > -1) { auto clipMin = ImGui::GetItemRectMin(); auto clipMax = ImGui::GetItemRectMax(); auto drawList = ImGui::GetWindowDrawList(); auto textPos = to_imvec2(to_vec2(cursorScreenPos) + to_vec2(ImGui::GetStyle().WindowPadding)); drawList->PushClipRect(clipMin, clipMax); ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE_LARGE); 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(); } } if (isPreviewHovered) { auto isMouseClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left); auto isMouseReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left); auto isMouseLeftDown = ImGui::IsMouseDown(ImGuiMouseButton_Left); auto isMouseMiddleDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle); auto isMouseRightDown = ImGui::IsMouseDown(ImGuiMouseButton_Right); auto isMouseDown = isMouseLeftDown || isMouseMiddleDown || isMouseRightDown; auto mouseDelta = to_ivec2(ImGui::GetIO().MouseDelta); auto mouseWheel = ImGui::GetIO().MouseWheel; auto isLeftJustPressed = ImGui::IsKeyPressed(ImGuiKey_LeftArrow, false); auto isRightJustPressed = ImGui::IsKeyPressed(ImGuiKey_RightArrow, false); auto isUpJustPressed = ImGui::IsKeyPressed(ImGuiKey_UpArrow, false); auto isDownJustPressed = ImGui::IsKeyPressed(ImGuiKey_DownArrow, false); auto isLeftPressed = ImGui::IsKeyPressed(ImGuiKey_LeftArrow); auto isRightPressed = ImGui::IsKeyPressed(ImGuiKey_RightArrow); auto isUpPressed = ImGui::IsKeyPressed(ImGuiKey_UpArrow); auto isDownPressed = ImGui::IsKeyPressed(ImGuiKey_DownArrow); auto isLeftDown = ImGui::IsKeyDown(ImGuiKey_LeftArrow); auto isRightDown = ImGui::IsKeyDown(ImGuiKey_RightArrow); auto isUpDown = ImGui::IsKeyDown(ImGuiKey_UpArrow); auto isDownDown = ImGui::IsKeyDown(ImGuiKey_DownArrow); 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 isKeyJustPressed = isLeftJustPressed || isRightJustPressed || isUpJustPressed || isDownJustPressed; auto isKeyDown = isLeftDown || isRightDown || isUpDown || isDownDown; auto isKeyReleased = isLeftReleased || isRightReleased || isUpReleased || isDownReleased; auto isZoomIn = shortcut(manager.chords[SHORTCUT_ZOOM_IN], shortcut::GLOBAL); auto isZoomOut = shortcut(manager.chords[SHORTCUT_ZOOM_OUT], shortcut::GLOBAL); auto isBegin = isMouseClicked || isKeyJustPressed; auto isDuring = isMouseDown || isKeyDown; auto isEnd = isMouseReleased || isKeyReleased; auto isMod = ImGui::IsKeyDown(ImGuiMod_Shift); auto frame = document.frame_get(); auto useTool = tool; auto step = isMod ? canvas::STEP_FAST : canvas::STEP; mousePos = position_translate(zoom, pan, to_vec2(ImGui::GetMousePos()) - to_vec2(cursorScreenPos)); if (isMouseMiddleDown) useTool = tool::PAN; if (tool == tool::MOVE && isMouseRightDown) useTool = tool::SCALE; if (tool == tool::SCALE && isMouseRightDown) useTool = tool::MOVE; auto& areaType = tool::INFO[useTool].areaType; auto cursor = areaType == tool::ANIMATION_PREVIEW || areaType == tool::ALL ? tool::INFO[useTool].cursor : ImGuiMouseCursor_NotAllowed; ImGui::SetMouseCursor(cursor); ImGui::SetKeyboardFocusHere(); if (useTool != tool::MOVE) isMoveDragging = false; switch (useTool) { case tool::PAN: if (isMouseDown || isMouseMiddleDown) pan += vec2(mouseDelta.x, mouseDelta.y); break; case tool::MOVE: if (!frame) break; if (isBegin) { document.snapshot("Frame Position"); if (isMouseClicked) { moveOffset = settings.inputIsMoveToolSnapToMouse ? vec2() : mousePos - frame->position; isMoveDragging = true; } } if (isMouseDown && isMoveDragging) frame->position = ivec2(mousePos - moveOffset); if (isLeftPressed) frame->position.x -= step; if (isRightPressed) frame->position.x += step; if (isUpPressed) frame->position.y -= step; if (isDownPressed) frame->position.y += step; if (isMouseReleased) isMoveDragging = false; if (isEnd) document.change(Document::FRAMES); if (isDuring) { if (ImGui::BeginTooltip()) { auto positionFormat = math::vec2_format_get(frame->position); auto positionString = std::format("Position: ({}, {})", positionFormat, positionFormat); ImGui::Text(positionString.c_str(), frame->position.x, frame->position.y); ImGui::EndTooltip(); } } break; case tool::SCALE: if (!frame) break; if (isBegin) document.snapshot("Frame Scale"); if (isMouseDown) { frame->scale += vec2(mouseDelta.x, mouseDelta.y); if (isMod) frame->scale = {frame->scale.x, frame->scale.x}; } if (isLeftPressed) frame->scale.x -= step; if (isRightPressed) frame->scale.x += step; if (isUpPressed) frame->scale.y -= step; if (isDownPressed) frame->scale.y += step; if (isDuring) { if (ImGui::BeginTooltip()) { auto scaleFormat = math::vec2_format_get(frame->scale); auto scaleString = std::format("Scale: ({}, {})", scaleFormat, scaleFormat); ImGui::Text(scaleString.c_str(), frame->scale.x, frame->scale.y); ImGui::EndTooltip(); } } if (isEnd) document.change(Document::FRAMES); break; case tool::ROTATE: if (!frame) break; if (isBegin) document.snapshot("Frame Rotation"); if (isMouseDown) frame->rotation += mouseDelta.y; if (isLeftPressed || isDownPressed) frame->rotation -= step; if (isUpPressed || isRightPressed) frame->rotation += step; if (isDuring) { if (ImGui::BeginTooltip()) { auto rotationFormat = math::float_format_get(frame->rotation); auto rotationString = std::format("Rotation: {}", rotationFormat); ImGui::Text(rotationString.c_str(), frame->rotation); ImGui::EndTooltip(); } } if (isEnd) document.change(Document::FRAMES); break; default: break; } 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(); manager.progressPopup.trigger(); if (ImGui::BeginPopupModal(manager.progressPopup.label, &manager.progressPopup.isOpen, ImGuiWindowFlags_NoResize)) { if (!animation) return; auto& start = manager.recordingStart; auto& end = manager.recordingEnd; auto progress = (playback.time - start) / (end - start); ImGui::ProgressBar(progress); ImGui::Text("Once recording is complete, rendering may take some time.\nPlease be patient..."); if (ImGui::Button("Cancel", ImVec2(ImGui::GetContentRegionAvail().x, 0))) { renderFrames.clear(); pan = savedPan; zoom = savedZoom; settings = savedSettings; overlayIndex = savedOverlayIndex; isSizeTrySet = true; hasPendingZoomPanAdjust = false; isCheckerPanInitialized = false; if (settings.timelineIsSound) audioStream.capture_end(mixer); playback.isPlaying = false; playback.isFinished = false; manager.isRecording = false; manager.progressPopup.close(); } ImGui::EndPopup(); } if (!document.isAnimationPreviewSet) { center_view(); zoom = settings.previewStartZoom; reset_checker_pan(); document.isAnimationPreviewSet = true; } settings.previewStartZoom = zoom; } }