#include "animation_preview.hpp" #include #include #include #include #include #include #include #include #include #include "actions.hpp" #include "log.hpp" #include "math.hpp" #include "path.hpp" #include "strings.hpp" #include "toast.hpp" #include "tool.hpp" #include "types.hpp" #include "util/imgui/draw.hpp" #include "util/imgui/input.hpp" #include "util/imgui/layout.hpp" #include "util/imgui/shortcut.hpp" #include "util/imgui/tooltip.hpp" 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 SELECTED_LAYER_BORDER_COLOR = vec4(1.0f, 0.1059f, 0.5882f, 1.0f); constexpr auto TARGET_SIZE = vec2(32, 32); constexpr auto POINT_SIZE = vec2(4, 4); 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); namespace { std::filesystem::path render_destination_directory(const std::filesystem::path& path, int type) { if (type == render::PNGS) return path; auto directory = path.parent_path(); if (directory.empty()) directory = std::filesystem::current_path(); return directory; } std::filesystem::path render_frame_filename(const std::filesystem::path& format, int index) { auto formatString = path::to_utf8(format); try { auto name = std::vformat(formatString, std::make_format_args(index)); auto filename = path::from_utf8(name).filename(); if (filename.empty()) return path::from_utf8(std::format("frame_{:06}.png", index)); if (filename.extension().empty()) filename.replace_extension(render::EXTENSIONS[render::SPRITESHEET]); return filename; } catch (...) { return path::from_utf8(std::format("frame_{:06}.png", index)); } } std::filesystem::path render_temp_directory_create(const std::filesystem::path& directory) { auto timestamp = (uint64_t)std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); for (int suffix = 0; suffix < 1000; ++suffix) { auto tempDirectory = directory / path::from_utf8(std::format(".anm2ed_render_tmp_{}_{}", timestamp, suffix)); std::error_code ec; if (std::filesystem::create_directories(tempDirectory, ec)) return tempDirectory; } return {}; } void render_temp_cleanup(std::filesystem::path& directory, std::vector& frames) { std::error_code ec; if (!directory.empty()) std::filesystem::remove_all(directory, ec); directory.clear(); frames.clear(); } void pixels_unpremultiply_alpha(std::vector& pixels) { for (size_t index = 0; index + 3 < pixels.size(); index += 4) { auto alpha = pixels[index + 3]; if (alpha == 0) { pixels[index + 0] = 0; pixels[index + 1] = 0; pixels[index + 2] = 0; continue; } if (alpha == 255) continue; float alphaUnit = (float)alpha / 255.0f; pixels[index + 0] = (uint8_t)glm::clamp((float)std::round((float)pixels[index + 0] / alphaUnit), 0.0f, 255.0f); pixels[index + 1] = (uint8_t)glm::clamp((float)std::round((float)pixels[index + 1] / alphaUnit), 0.0f, 255.0f); pixels[index + 2] = (uint8_t)glm::clamp((float)std::round((float)pixels[index + 2] / alphaUnit), 0.0f, 255.0f); } } bool render_audio_stream_generate(AudioStream& audioStream, std::map& sounds, const std::vector& frameSoundIDs, int fps) { audioStream.stream.clear(); if (frameSoundIDs.empty() || fps <= 0) return true; SDL_AudioSpec mixSpec = audioStream.spec; mixSpec.format = SDL_AUDIO_F32; auto* mixer = MIX_CreateMixer(&mixSpec); if (!mixer) return false; auto channels = std::max(mixSpec.channels, 1); auto sampleRate = std::max(mixSpec.freq, 1); auto framesPerStep = (double)sampleRate / (double)fps; auto sampleFrameAccumulator = 0.0; auto frameBuffer = std::vector{}; for (auto soundID : frameSoundIDs) { if (soundID != -1 && sounds.contains(soundID)) sounds.at(soundID).play(false, mixer); sampleFrameAccumulator += framesPerStep; auto sampleFramesToGenerate = (int)std::floor(sampleFrameAccumulator); sampleFramesToGenerate = std::max(sampleFramesToGenerate, 1); sampleFrameAccumulator -= (double)sampleFramesToGenerate; frameBuffer.resize((std::size_t)sampleFramesToGenerate * (std::size_t)channels); if (!MIX_Generate(mixer, frameBuffer.data(), (int)(frameBuffer.size() * sizeof(float)))) { for (auto& [_, sound] : sounds) sound.track_detach(mixer); MIX_DestroyMixer(mixer); audioStream.stream.clear(); return false; } audioStream.stream.insert(audioStream.stream.end(), frameBuffer.begin(), frameBuffer.end()); } for (auto& [_, sound] : sounds) sound.track_detach(mixer); MIX_DestroyMixer(mixer); return true; } } AnimationPreview::AnimationPreview() : Canvas(vec2()) {} bool AnimationPreview::is_focused_get() const { return isFocused; } void AnimationPreview::tick(Manager& manager, Settings& settings, float deltaSeconds) { 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; auto stop_all_sounds = [&]() { for (auto& [_, sound] : document.sounds) sound.stop(mixer); }; auto trigger_sound_id_get = [&](Element* animation, float time) { if (!animation) return -1; auto triggers = animation_item_get(*animation, ItemType::TRIGGER); if (!triggers || !triggers->isVisible) return -1; auto trigger = frame_generate(*triggers, time); if (!trigger.isVisible || trigger.soundIds.empty()) return -1; auto soundIndex = trigger.soundIds.size() > 1 ? (size_t)math::random_in_range(0.0f, (float)trigger.soundIds.size()) : (size_t)0; soundIndex = std::min(soundIndex, trigger.soundIds.size() - 1); auto soundID = trigger.soundIds[soundIndex]; return document.sound_get(soundID) ? soundID : -1; }; if (manager.isRecording) { auto& ffmpegPath = settings.renderFFmpegPath; auto& path = settings.renderPath; auto pathString = path::to_utf8(path); auto& type = settings.renderType; auto renderFrameRate = std::max(settings.playbackTickRate, 1); if (playback.time >= (float)end + 1.0f || playback.isFinished) { if (settings.timelineIsSound) audioStream.capture_end(mixer); if (type == render::PNGS) { if (!renderTempFrames.empty()) { toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES), std::make_format_args(pathString))); logger.info(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES, anm2ed::ENGLISH), std::make_format_args(pathString))); } else { toasts.push( std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES_FAILED), std::make_format_args(pathString))); logger.error(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES_FAILED, anm2ed::ENGLISH), std::make_format_args(pathString))); } } else if (type == render::SPRITESHEET) { auto& rows = settings.renderRows; auto& columns = settings.renderColumns; if (renderTempFrames.empty()) { toasts.push(localize.get(TOAST_SPRITESHEET_NO_FRAMES)); logger.warning(localize.get(TOAST_SPRITESHEET_NO_FRAMES, anm2ed::ENGLISH)); } else { auto firstFrame = Texture(renderTempFrames.front()); if (firstFrame.size.x <= 0 || firstFrame.size.y <= 0 || firstFrame.pixels.empty()) { toasts.push(localize.get(TOAST_SPRITESHEET_EMPTY)); logger.error(localize.get(TOAST_SPRITESHEET_EMPTY, anm2ed::ENGLISH)); } 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 < renderTempFrames.size(); ++index) { auto frame = Texture(renderTempFrames[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.push(std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET), std::make_format_args(pathString))); logger.info(std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET, anm2ed::ENGLISH), std::make_format_args(pathString))); } else { toasts.push( std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET_FAILED), std::make_format_args(pathString))); logger.error(std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET_FAILED, anm2ed::ENGLISH), std::make_format_args(pathString))); } } } } else { if (settings.timelineIsSound && type != render::GIF) { if (!render_audio_stream_generate(audioStream, document.sounds, renderFrameSoundIDs, renderFrameRate)) { toasts.push(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED)); logger.error("Failed to generate deterministic render audio stream; exporting without audio."); audioStream.stream.clear(); } } else audioStream.stream.clear(); if (animation_render(ffmpegPath, path, renderTempFrames, renderTempFrameDurations, audioStream, (render::Type)type, renderFrameRate)) { toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION), std::make_format_args(pathString))); logger.info(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION, anm2ed::ENGLISH), std::make_format_args(pathString))); } else { toasts.push( std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED), std::make_format_args(pathString))); logger.error(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED, anm2ed::ENGLISH), std::make_format_args(pathString))); } } if (type == render::PNGS) { renderTempDirectory.clear(); renderTempFrames.clear(); renderTempFrameDurations.clear(); renderFrameSoundIDs.clear(); } else { render_temp_cleanup(renderTempDirectory, renderTempFrames); renderTempFrameDurations.clear(); renderFrameSoundIDs.clear(); } if (settings.renderIsRawAnimation) { settings = savedSettings; pan = savedPan; zoom = savedZoom; overlayIndex = savedOverlayIndex; isSizeTrySet = true; hasPendingZoomPanAdjust = false; isCheckerPanInitialized = false; } playback.isPlaying = false; playback.isFinished = false; manager.isRecording = false; manager.progressPopup.close(); } else { if (settings.timelineIsSound && renderTempFrames.empty()) audioStream.capture_begin(mixer); auto frameSoundID = -1; if (settings.timelineIsSound && !document.sounds.empty()) { auto soundTime = (int)std::floor(playback.time); auto animation = anm2.element_get(ElementType::ANIMATION, document.reference.animationIndex); if (soundTime != renderFrameSoundTimePrev && animation && (!settings.timelineIsOnlyShowLayers || manager.isRecording)) frameSoundID = trigger_sound_id_get(animation, playback.time); renderFrameSoundTimePrev = soundTime; } renderFrameSoundIDs.push_back(frameSoundID); bind(); auto pixels = pixels_get(); if (settings.renderIsRawAnimation) pixels_unpremultiply_alpha(pixels); auto frameIndex = (int)renderTempFrames.size(); auto framePath = renderTempDirectory / render_frame_filename(settings.renderFormat, frameIndex); if (Texture::write_pixels_png(framePath, size, pixels.data())) { renderTempFrames.push_back(framePath); auto fallbackDuration = 1.0 / (double)renderFrameRate; renderTempFrameDurations.push_back(fallbackDuration); } else { toasts.push( std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED), std::make_format_args(pathString))); logger.error(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED, anm2ed::ENGLISH), std::make_format_args(pathString))); if (type != render::PNGS) render_temp_cleanup(renderTempDirectory, renderTempFrames); renderTempFrameDurations.clear(); renderFrameSoundIDs.clear(); playback.isPlaying = false; playback.isFinished = false; manager.isRecording = false; manager.progressPopup.close(); } } } if (playback.isPlaying) { auto animation = anm2.element_get(ElementType::ANIMATION, document.reference.animationIndex); auto& isSound = settings.timelineIsSound; auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; if (!animation) { playback.isPlaying = false; playback.isFinished = false; playback.timing_reset(); } else { if (!manager.isRecording && !document.sounds.empty() && isSound) { if (!isOnlyShowLayers || manager.isRecording) if (auto soundID = trigger_sound_id_get(animation, playback.time); soundID != -1) if (auto sound = document.sound_get(soundID)) sound->play(false, mixer); } auto info = element_first_get(anm2.root, ElementType::INFO); auto fps = std::max(info ? info->fps : 30, 1); playback.tick(fps, animation->frameNum, (animation->isLoop || settings.playbackIsLoop) && !manager.isRecording, deltaSeconds); frameTime = playback.time; } } if (wasPlaybackPlaying && !playback.isPlaying) stop_all_sounds(); wasPlaybackPlaying = playback.isPlaying; } void AnimationPreview::update(Manager& manager, Settings& settings, Resources& resources) { isFocused = false; auto& document = *manager.get(); auto& anm2 = document.anm2; auto& playback = document.playback; auto& reference = document.reference; auto animation = anm2.element_get(ElementType::ANIMATION, reference.animationIndex); auto& pan = document.previewPan; auto& zoom = document.previewZoom; auto& backgroundColor = settings.previewBackgroundColor; auto& isTransparent = settings.animationPreviewTransparent; 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& frames = document.frames.selection; 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(); }; auto fit_view = [&]() { if (animation) set_to_rect(zoom, pan, anm2.animation_rect(*animation, isRootTransform)); }; auto zoom_adjust = [&](float delta) { auto focus = position_translate(zoom, pan, size * 0.5f); auto previousZoom = zoom; zoom_set(zoom, pan, focus, delta); if (zoom != previousZoom) hasPendingZoomPanAdjust = true; }; auto zoom_in = [&]() { zoom_adjust(zoomStep); }; auto zoom_out = [&]() { zoom_adjust(-zoomStep); }; manager.isAbleToRecord = false; if (ImGui::Begin(localize.get(LABEL_ANIMATION_PREVIEW_WINDOW), &settings.windowIsAnimationPreview)) { isFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); manager.isAbleToRecord = true; 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(localize.get(BASIC_GRID), &isGrid); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_GRID_VISIBILITY)); ImGui::SameLine(); ImGui::ColorEdit4(localize.get(BASIC_COLOR), value_ptr(gridColor), ImGuiColorEditFlags_NoInputs); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_GRID_COLOR)); input_int2_range(localize.get(BASIC_SIZE), gridSize, ivec2(GRID_SIZE_MIN), ivec2(GRID_SIZE_MAX)); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_GRID_SIZE)); input_int2_range(localize.get(BASIC_OFFSET), gridOffset, ivec2(GRID_OFFSET_MIN), ivec2(GRID_OFFSET_MAX)); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_GRID_OFFSET)); } ImGui::EndChild(); ImGui::SameLine(); if (ImGui::BeginChild("##View Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar)) { ImGui::InputFloat(localize.get(BASIC_ZOOM), &zoom, zoomStep, zoomStep, "%.0f%%"); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_PREVIEW_ZOOM)); auto widgetSize = widget_size_with_row_get(2); shortcut(manager.chords[SHORTCUT_CENTER_VIEW]); if (ImGui::Button(localize.get(LABEL_CENTER_VIEW), widgetSize)) center_view(); set_item_tooltip_shortcut(localize.get(TOOLTIP_CENTER_VIEW), settings.shortcutCenterView); ImGui::SameLine(); shortcut(manager.chords[SHORTCUT_FIT]); if (ImGui::Button(localize.get(LABEL_FIT), widgetSize)) fit_view(); set_item_tooltip_shortcut(localize.get(TOOLTIP_FIT), settings.shortcutFit); auto readoutSize = ImVec2(row_widget_width_get(2), ImGui::GetTextLineHeightWithSpacing()); auto mousePosInt = ivec2(mousePos); auto positionText = std::vformat(localize.get(FORMAT_POSITION_SPACED), std::make_format_args(mousePosInt.x, mousePosInt.y)); if (ImGui::BeginChild("##Position Readout", readoutSize, false, ImGuiWindowFlags_NoScrollbar)) { ImGui::TextUnformatted(positionText.c_str()); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", positionText.c_str()); } ImGui::EndChild(); ImGui::SameLine(); auto sizeInt = ivec2(size); auto sizeText = std::vformat(localize.get(FORMAT_SIZE_SPACED), std::make_format_args(sizeInt.x, sizeInt.y)); if (ImGui::BeginChild("##Size Readout", readoutSize, false, ImGuiWindowFlags_NoScrollbar)) { ImGui::TextUnformatted(sizeText.c_str()); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", sizeText.c_str()); } ImGui::EndChild(); } ImGui::EndChild(); ImGui::SameLine(); if (ImGui::BeginChild("##Background Child", childSize, true, ImGuiWindowFlags_HorizontalScrollbar)) { ImGui::BeginDisabled(isTransparent); { ImGui::ColorEdit3(localize.get(LABEL_BACKGROUND_COLOR), value_ptr(backgroundColor), ImGuiColorEditFlags_NoInputs); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_BACKGROUND_COLOR)); } ImGui::EndDisabled(); ImGui::SameLine(); ImGui::Checkbox(localize.get(LABEL_AXES), &isAxes); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_AXES)); ImGui::SameLine(); ImGui::ColorEdit4(localize.get(BASIC_COLOR), value_ptr(axesColor), ImGuiColorEditFlags_NoInputs); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_AXES_COLOR)); combo_negative_one_indexed(localize.get(LABEL_OVERLAY), &overlayIndex, document.animation.labels); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_OVERLAY)); ImGui::DragFloat(localize.get(BASIC_ALPHA), &overlayTransparency, DRAG_SPEED, 0, 255, "%.0f"); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_OVERLAY_ALPHA)); } 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(localize.get(LABEL_ROOT_TRANSFORM), &isRootTransform); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ROOT_TRANSFORM)); ImGui::Checkbox(localize.get(LABEL_PIVOTS), &isPivots); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_PIVOTS)); ImGui::Checkbox(localize.get(LABEL_TRANSPARENT), &isTransparent); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_PREVIEW_TRANSPARENT)); } ImGui::EndChild(); ImGui::SameLine(); if (ImGui::BeginChild("##Helpers Child 2", helpersChildSize)) { ImGui::Checkbox(localize.get(LABEL_ALT_ICONS), &isAltIcons); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ALT_ICONS)); ImGui::Checkbox(localize.get(LABEL_BORDER), &isBorder); ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_BORDER)); } 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.renderIsRawAnimation) { savedOverlayIndex = overlayIndex; savedZoom = zoom; savedPan = pan; if (animation) if (auto rect = anm2.animation_rect(*animation, 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; renderTempFrames.clear(); renderTempFrameDurations.clear(); renderFrameSoundIDs.clear(); renderFrameSoundTimePrev = -1; if (settings.renderType == render::PNGS) { renderTempDirectory = settings.renderPath; std::error_code ec; std::filesystem::create_directories(renderTempDirectory, ec); } else { auto destinationDirectory = render_destination_directory(settings.renderPath, settings.renderType); std::error_code ec; std::filesystem::create_directories(destinationDirectory, ec); renderTempDirectory = render_temp_directory_create(destinationDirectory); } if (renderTempDirectory.empty()) { auto pathString = path::to_utf8(settings.renderPath); toasts.push( std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED), std::make_format_args(pathString))); logger.error(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED, anm2ed::ENGLISH), std::make_format_args(pathString))); manager.isRecording = false; manager.progressPopup.close(); playback.isPlaying = false; playback.isFinished = false; return; } playback.isPlaying = true; playback.timing_reset(); playback.time = manager.recordingStart; } if (isSizeTrySet) { auto nextSize = to_vec2(ImGui::GetContentRegionAvail()); bool isCanvasResized = ivec2(nextSize) != ivec2(size); size_set(nextSize); if (isCanvasResized && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { auto resizeSizeInt = ivec2(size); auto resizeSizeText = std::vformat(localize.get(FORMAT_SIZE_SPACED), std::make_format_args(resizeSizeInt.x, resizeSizeInt.y)); ImGui::SetTooltip("%s", resizeSizeText.c_str()); } } bind(); viewport_set(); clear(isTransparent ? vec4(0) : 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 referenceItemType = static_cast(reference.itemType); auto is_layer_animation_selected = [&](int id) { if (reference.animationIndex != -1 && referenceItemType == ItemType::LAYER && reference.itemID == id) return true; for (auto itemReference : document.items.references) if (itemReference.animationIndex == reference.animationIndex && itemReference.itemType == LAYER && itemReference.itemID == id) return true; for (auto frameReference : document.frames.references) if (frameReference.animationIndex == reference.animationIndex && frameReference.itemType == LAYER && frameReference.itemID == id) return true; return document.frames.references.empty() && !frames.empty() && referenceItemType == ItemType::LAYER && reference.itemID == id; }; auto render = [&](Element* animation, float time, vec3 colorOffset = {}, float alphaOffset = {}, const std::vector* layeredOnions = nullptr, bool isIndexMode = false) { auto sample_time_for_item = [&](Element& 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.children.empty()) return std::nullopt; int baseIndex = frame_index_from_time_get(item, frameTime); if (baseIndex < 0) return std::nullopt; int sampleIndex = baseIndex + sample.indexOffset; if (!track_frame_get(item, sampleIndex)) return std::nullopt; return frame_time_from_index_get(item, sampleIndex); }; auto root = animation_item_get(*animation, ItemType::ROOT); auto transform_for_time = [&](float t) { auto sampleTransform = baseTransform; if (isRootTransform && root) { auto rootFrame = frame_generate(*root, t); sampleTransform *= math::quad_model_parent_get(rootFrame.position, {}, math::percent_to_unit(rootFrame.scale), rootFrame.rotation); } return sampleTransform; }; auto transform = transform_for_time(time); auto is_track_group_visible = [](const Element& container, const Element& track) { if (track.groupId == -1) return true; for (const auto& child : container.children) if (child.type == ElementType::GROUP && child.id == track.groupId) return child.isVisible; return true; }; auto draw_root = [&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor, float sampleAlpha, bool isOnion) { if (!root) return; auto rootFrame = frame_generate(*root, sampleTime); if (isOnlyShowLayers || !rootFrame.isVisible || !root->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 && root) for (auto& sample : *layeredOnions) if (auto sampleTime = sample_time_for_item(*root, sample)) { auto sampleTransform = transform_for_time(*sampleTime); draw_root(*sampleTime, sampleTransform, sample.colorOffset, sample.alphaOffset, true); } draw_root(time, transform, {}, 0.0f, false); if (auto layerAnimations = element_child_first_get(*animation, ElementType::LAYER_ANIMATIONS)) { auto layer_animation_draw = [&](auto&& self, Element& layerAnimation, bool isParentVisible = true) -> void { if (layerAnimation.type == ElementType::GROUP) { for (auto& child : layerAnimation.children) self(self, child, isParentVisible && layerAnimation.isVisible); return; } if (layerAnimation.type != ElementType::LAYER_ANIMATION || !isParentVisible || !layerAnimation.isVisible || !is_track_group_visible(*layerAnimations, layerAnimation)) return; auto id = layerAnimation.layerId; auto layer = anm2.element_get(ElementType::LAYER_ELEMENT, id); if (!layer) return; auto spritesheet = anm2.element_get(ElementType::SPRITESHEET, layer->spritesheetId); auto textureInfo = document.texture_get(layer->spritesheetId); if (!spritesheet || !textureInfo || !textureInfo->is_valid()) return; auto draw_layer = [&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor, float sampleAlpha, bool isOnion) { auto frame = frame_generate(layerAnimation, sampleTime); if (!frame.isVisible) return; auto& texture = *textureInfo; auto texSize = vec2(texture.size); if (texSize.x <= 0.0f || texSize.y <= 0.0f) return; frame = anm2.frame_effective(id, frame); auto crop = frame.crop; auto size = frame.size; auto pivot = frame.pivot; auto layerModel = math::quad_model_get(size, frame.position, pivot, math::percent_to_unit(frame.scale), frame.rotation); auto layerTransform = sampleTransform * layerModel; auto uvMin = crop / texSize; auto uvMax = (crop + size) / texSize; vec3 frameColorOffset = frame.colorOffset + colorOffset + sampleColor; vec4 frameTint = frame.tint; if (isRootTransform && root) { auto rootFrame = frame_generate(*root, sampleTime); frameColorOffset += rootFrame.colorOffset; frameTint *= rootFrame.tint; } frameTint.a = std::max(0.0f, frameTint.a - (alphaOffset + sampleAlpha)); 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) : is_layer_animation_selected(id) ? SELECTED_LAYER_BORDER_COLOR : 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(*sampleTime); draw_layer(*sampleTime, sampleTransform, sample.colorOffset, sample.alphaOffset, true); } draw_layer(time, transform, {}, 0.0f, false); }; for (auto& layerAnimation : layerAnimations->children) layer_animation_draw(layer_animation_draw, layerAnimation); } if (auto nullAnimations = element_child_first_get(*animation, ElementType::NULL_ANIMATIONS)) { auto null_animation_draw = [&](auto&& self, Element& nullAnimation, bool isParentVisible = true) -> void { if (nullAnimation.type == ElementType::GROUP) { for (auto& child : nullAnimation.children) self(self, child, isParentVisible && nullAnimation.isVisible); return; } if (nullAnimation.type != ElementType::NULL_ANIMATION || !isParentVisible || !nullAnimation.isVisible || !is_track_group_visible(*nullAnimations, nullAnimation) || isOnlyShowLayers) return; auto id = nullAnimation.nullId; auto nulls = anm2.element_get(ElementType::NULLS); auto nullInfo = nulls ? element_child_id_get(*nulls, ElementType::NULL_ELEMENT, id) : nullptr; if (!nullInfo) return; auto isShowRect = nullInfo->isShowRect; auto draw_null = [&](float sampleTime, const glm::mat4& sampleTransform, vec3 sampleColor, float sampleAlpha, bool isOnion) { auto frame = frame_generate(nullAnimation, sampleTime); if (!frame.isVisible) return; 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 && referenceItemType == ItemType::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(frame.scale, frame.position, frame.scale * 0.5f, vec2(1.0f), 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(*sampleTime); draw_null(*sampleTime, sampleTransform, sample.colorOffset, sample.alphaOffset, true); } draw_null(time, transform, {}, 0.0f, false); }; for (auto& nullAnimation : nullAnimations->children) null_animation_draw(null_animation_draw, nullAnimation); } }; if (animation) { auto layeredOnions = settings.onionskinIsEnabled ? &onionskinSamples : nullptr; render(animation, frameTime, {}, 0.0f, layeredOnions, settings.onionskinMode == (int)OnionskinMode::INDEX); if (auto overlayAnimation = anm2.element_get(ElementType::ANIMATION, overlayIndex)) render(overlayAnimation, frameTime, {}, 1.0f - math::uint8_to_float(overlayTransparency), layeredOnions, settings.onionskinMode == (int)OnionskinMode::INDEX); } unbind(); if (isTransparent) { sync_checker_pan(); render_checker_background(ImGui::GetWindowDrawList(), min, max, -size - checkerPan, CHECKER_SIZE); } ImGui::Image(texture, to_imvec2(size)); isPreviewHovered = ImGui::IsItemHovered(); auto triggers = animation ? animation_item_get(*animation, ItemType::TRIGGER) : nullptr; if (animation && triggers && triggers->isVisible && !isOnlyShowLayers && !manager.isRecording) { if (auto trigger = frame_generate(*triggers, frameTime); 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; auto events = anm2.element_get(ElementType::EVENTS); auto event = events ? element_child_id_get(*events, ElementType::EVENT_ELEMENT, trigger.eventId) : nullptr; if (event) drawList->AddText(textPos, ImGui::GetColorU32(triggerTextColor), event->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 isMouseRightClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Right); auto isMouseRightReleased = ImGui::IsMouseReleased(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 = isFocused && shortcut(manager.chords[SHORTCUT_ZOOM_IN], shortcut::GLOBAL); auto isZoomOut = isFocused && shortcut(manager.chords[SHORTCUT_ZOOM_OUT], shortcut::GLOBAL); auto isMod = ImGui::IsKeyDown(ImGuiMod_Shift); auto useTool = tool; auto step = (float)(isMod ? STEP_FAST : STEP); mousePos = position_translate(zoom, pan, to_vec2(ImGui::GetMousePos()) - to_vec2(cursorScreenPos)); auto is_frame_reference_valid = [&](const Reference& frameReference) { if (frameReference.itemType == NONE || frameReference.itemType == TRIGGER || frameReference.frameIndex < 0) return false; auto itemType = static_cast(frameReference.itemType); auto item = anm2.element_get(frameReference.animationIndex, itemType, frameReference.itemID); return item && track_frame_get(*item, frameReference.frameIndex); }; auto selected_frame_references_get = [&]() { std::set result = document.frames.references; for (auto frameIndex : frames) result.insert({reference.animationIndex, reference.itemType, reference.itemID, frameIndex}); std::erase_if(result, [&](const Reference& frameReference) { return !is_frame_reference_valid(frameReference); }); if (result.empty() && is_frame_reference_valid(reference)) result.insert(reference); return result; }; auto selectedFrameReferences = selected_frame_references_get(); auto editReference = reference; auto editItemType = referenceItemType; if ((referenceItemType == ItemType::TRIGGER || !selectedFrameReferences.contains(reference)) && !selectedFrameReferences.empty()) { editReference = *selectedFrameReferences.begin(); editItemType = static_cast(editReference.itemType); } auto frame = anm2.element_get(editReference.animationIndex, editItemType, editReference.frameIndex, editReference.itemID); auto item = anm2.element_get(editReference.animationIndex, editItemType, editReference.itemID); auto nulls = anm2.element_get(ElementType::NULLS); auto selectedNull = editItemType == ItemType::NULL_ && nulls ? element_child_id_get(*nulls, ElementType::NULL_ELEMENT, editReference.itemID) : nullptr; bool isSelectedNullRect = selectedNull && selectedNull->isShowRect; auto null_rect_top_left = [](const Element& frame) { return frame.position - (frame.scale * 0.5f); }; if (isMouseMiddleDown) useTool = tool::PAN; if (tool == tool::MOVE && isMouseRightDown) useTool = tool::SCALE; if (tool == tool::SCALE && isMouseRightDown) useTool = tool::MOVE; bool isToolMouseClicked = isMouseClicked; bool isToolMouseReleased = isMouseReleased; bool isToolMouseDown = isMouseLeftDown; if ((tool == tool::MOVE && useTool == tool::SCALE) || (tool == tool::SCALE && useTool == tool::MOVE)) { isToolMouseClicked = isMouseRightClicked; isToolMouseReleased = isMouseRightReleased; isToolMouseDown = isMouseRightDown; } auto isToolBegin = isToolMouseClicked || isKeyJustPressed; auto isToolDuring = isToolMouseDown || isKeyDown; auto isToolEnd = isToolMouseReleased || isKeyReleased; auto frame_snapshot = [&](auto message) { manager.command_push({manager.selected, [message](Manager&, Document& document) { document.snapshot(localize.get(message)); }}); }; auto frame_change_apply = [&](FrameChange frameChange, ChangeType changeType = ChangeType::ADJUST) { auto queuedFrameReferences = selectedFrameReferences; manager.command_push({manager.selected, [=](Manager&, Document& document) { std::map> groupedFrames{}; for (auto frameReference : queuedFrameReferences) groupedFrames[{frameReference.animationIndex, frameReference.itemType, frameReference.itemID, -1}] .insert(frameReference.frameIndex); for (auto& [itemReference, itemFrames] : groupedFrames) { auto itemType = static_cast(itemReference.itemType); auto item = document.anm2.element_get(itemReference.animationIndex, itemType, itemReference.itemID); if (!item) continue; frames_change(*item, frameChange, itemType, changeType, itemFrames); } }}); }; auto frame_position_apply = [&](vec2 position) { if (!frame) return; frame_change_apply({.positionX = position.x - frame->position.x, .positionY = position.y - frame->position.y}, ChangeType::ADD); }; auto frame_scale_apply = [&](vec2 scale) { if (!frame) return; frame_change_apply({.scaleX = scale.x - frame->scale.x, .scaleY = scale.y - frame->scale.y}, ChangeType::ADD); }; auto frames_changed = [&]() { manager.command_push( {manager.selected, [](Manager&, Document& document) { document.anm2_change(Document::FRAMES); }}); }; auto null_rect_change = [&](vec2 topLeft, vec2 rectSize) { topLeft = vec2(ivec2(topLeft)); rectSize = vec2(ivec2(rectSize)); frame_change_apply({.positionX = topLeft.x + rectSize.x * 0.5f - frame->position.x, .positionY = topLeft.y + rectSize.y * 0.5f - frame->position.y, .scaleX = rectSize.x - frame->scale.x, .scaleY = rectSize.y - frame->scale.y}, ChangeType::ADD); }; auto& toolInfo = tool::INFO[useTool]; auto& areaType = toolInfo.areaType; bool isAreaAllowed = areaType == tool::ALL || areaType == tool::ANIMATION_PREVIEW; bool isFrameRequired = !(useTool == tool::PAN || useTool == tool::DRAW || useTool == tool::ERASE || useTool == tool::COLOR_PICKER); bool isFrameAvailable = !isFrameRequired || frame; auto cursor = (isAreaAllowed && isFrameAvailable) ? toolInfo.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 (!item || !frame || selectedFrameReferences.empty()) break; if (isToolBegin) { frame_snapshot(EDIT_FRAME_POSITION); if (isToolMouseClicked) { auto origin = isSelectedNullRect ? null_rect_top_left(*frame) : frame->position; moveOffset = settings.inputIsMoveToolSnapToMouse ? vec2() : mousePos - origin; isMoveDragging = true; } } if (isToolMouseDown && isMoveDragging) { auto position = mousePos - moveOffset; if (isSelectedNullRect) frame_position_apply(vec2((float)(int)(position.x + frame->scale.x * 0.5f), (float)(int)(position.y + frame->scale.y * 0.5f))); else frame_position_apply(vec2((float)(int)position.x, (float)(int)position.y)); } if (isLeftPressed) frame_change_apply({.positionX = step}, ChangeType::SUBTRACT); if (isRightPressed) frame_change_apply({.positionX = step}, ChangeType::ADD); if (isUpPressed) frame_change_apply({.positionY = step}, ChangeType::SUBTRACT); if (isDownPressed) frame_change_apply({.positionY = step}, ChangeType::ADD); if (isToolMouseReleased) isMoveDragging = false; if (isToolEnd) frames_changed(); if (isToolDuring) { if (ImGui::BeginTooltip()) { ImGui::TextUnformatted(std::vformat(localize.get(FORMAT_POSITION), std::make_format_args(frame->position.x, frame->position.y)) .c_str()); ImGui::EndTooltip(); } } break; case tool::SCALE: if (!item || !frame || selectedFrameReferences.empty()) break; if (isToolBegin) { frame_snapshot(EDIT_FRAME_SCALE); if (isToolMouseClicked && isSelectedNullRect) nullRectScaleAnchor = null_rect_top_left(*frame); } if (isToolMouseDown) { if (isSelectedNullRect) { auto size = mousePos - nullRectScaleAnchor; if (isMod) { auto squareSize = std::max(std::abs(size.x), std::abs(size.y)); size = {std::copysign(squareSize, size.x), std::copysign(squareSize, size.y)}; } auto minPoint = glm::min(nullRectScaleAnchor, nullRectScaleAnchor + size); auto maxPoint = glm::max(nullRectScaleAnchor, nullRectScaleAnchor + size); minPoint = vec2(ivec2(minPoint)); maxPoint = vec2(ivec2(maxPoint)); null_rect_change(minPoint, maxPoint - minPoint); } else { auto scale = frame->scale + vec2(mouseDelta.x, mouseDelta.y); if (isMod) scale = {scale.x, scale.x}; frame_scale_apply(scale); } } if (isSelectedNullRect) { if (isLeftPressed) frame_change_apply({.positionX = step * 0.5f, .scaleX = step}, ChangeType::SUBTRACT); if (isRightPressed) frame_change_apply({.positionX = step * 0.5f, .scaleX = step}, ChangeType::ADD); if (isUpPressed) frame_change_apply({.positionY = step * 0.5f, .scaleY = step}, ChangeType::SUBTRACT); if (isDownPressed) frame_change_apply({.positionY = step * 0.5f, .scaleY = step}, ChangeType::ADD); } else { if (isLeftPressed) frame_change_apply({.scaleX = step}, ChangeType::SUBTRACT); if (isRightPressed) frame_change_apply({.scaleX = step}, ChangeType::ADD); if (isUpPressed) frame_change_apply({.scaleY = step}, ChangeType::SUBTRACT); if (isDownPressed) frame_change_apply({.scaleY = step}, ChangeType::ADD); } if (isToolDuring) { if (ImGui::BeginTooltip()) { ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_SCALE), std::make_format_args(frame->scale.x, frame->scale.y)) .c_str()); ImGui::EndTooltip(); } } if (isToolEnd) { if (isSelectedNullRect) { auto topLeft = null_rect_top_left(*frame); auto minPoint = glm::min(topLeft, topLeft + frame->scale); auto maxPoint = glm::max(topLeft, topLeft + frame->scale); minPoint = vec2(ivec2(minPoint)); maxPoint = vec2(ivec2(maxPoint)); null_rect_change(minPoint, maxPoint - minPoint); } frames_changed(); } break; case tool::ROTATE: if (!item || !frame || selectedFrameReferences.empty()) break; if (isToolBegin) frame_snapshot(EDIT_FRAME_ROTATION); if (isToolMouseDown) frame_change_apply({.rotation = (float)(int)mouseDelta.x}, ChangeType::ADD); if (isLeftPressed || isDownPressed) frame_change_apply({.rotation = step}, ChangeType::SUBTRACT); if (isUpPressed || isRightPressed) frame_change_apply({.rotation = step}, ChangeType::ADD); if (isToolDuring) { if (ImGui::BeginTooltip()) { ImGui::TextUnformatted( std::vformat(localize.get(FORMAT_ROTATION), std::make_format_args(frame->rotation)).c_str()); ImGui::EndTooltip(); } } if (isToolEnd) frames_changed(); break; default: break; } if ((isMouseDown || isKeyDown) && useTool != tool::PAN) { if (!isAreaAllowed && areaType == tool::SPRITESHEET_EDITOR) { if (ImGui::BeginTooltip()) { ImGui::TextUnformatted(localize.get(TEXT_TOOL_SPRITESHEET_EDITOR)); ImGui::EndTooltip(); } } else if (isFrameRequired && !isFrameAvailable) { if (ImGui::BeginTooltip()) { ImGui::TextUnformatted(localize.get(TEXT_SELECT_FRAME)); ImGui::EndTooltip(); } } } if (mouseWheel != 0 || isZoomIn || isZoomOut) { auto wheelZoomStep = mouseWheel != 0 && ImGui::IsKeyDown(ImGuiMod_Ctrl) ? zoomStep * ZOOM_STEP_FAST_MULTIPLIER : zoomStep; auto previousZoom = zoom; zoom_set(zoom, pan, vec2(mousePos), (mouseWheel > 0 || isZoomIn) ? wheelZoomStep : -wheelZoomStep); if (zoom != previousZoom) hasPendingZoomPanAdjust = true; } } } if (tool == tool::PAN) { Actions actions{}; actions_undo_redo_add(actions, manager, document); actions.separator(); actions.add(ACTION_CENTER_VIEW, []() { return true; }, center_view); actions.add(ACTION_FIT_VIEW, [&]() { return animation; }, fit_view); actions.separator(); actions.add(ACTION_ZOOM_IN, []() { return true; }, zoom_in); actions.add(ACTION_ZOOM_OUT, []() { return true; }, zoom_out); actions_context_window_draw("##Animation Preview Context Menu", actions, settings); } manager.progressPopup.trigger(); if (ImGui::BeginPopupModal(manager.progressPopup.label(), &manager.progressPopup.isOpen, ImGuiWindowFlags_NoResize)) { auto& start = manager.recordingStart; auto& end = manager.recordingEnd; auto progress = (playback.time - start) / (end - start); ImGui::ProgressBar(progress); ImGui::TextUnformatted(localize.get(TEXT_RECORDING_PROGRESS)); shortcut(manager.chords[SHORTCUT_CANCEL]); if (ImGui::Button(localize.get(BASIC_CANCEL), ImVec2(ImGui::GetContentRegionAvail().x, 0))) { if (settings.renderType == render::PNGS) { renderTempDirectory.clear(); renderTempFrames.clear(); renderTempFrameDurations.clear(); } else { render_temp_cleanup(renderTempDirectory, renderTempFrames); renderTempFrameDurations.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; ImGui::End(); } }