This commit is contained in:
2026-05-07 00:19:53 -04:00
parent a1bdbeba1c
commit 5c746d99c2
12 changed files with 51 additions and 69 deletions
+2 -2
View File
@@ -2,10 +2,10 @@
namespace anm2ed::imgui
{
void Dockspace::tick(Manager& manager, Settings& settings)
void Dockspace::tick(Manager& manager, Settings& settings, float deltaSeconds)
{
if (auto document = manager.get(); document)
if (settings.windowIsAnimationPreview) animationPreview.tick(manager, settings);
if (settings.windowIsAnimationPreview) animationPreview.tick(manager, settings, deltaSeconds);
}
void Dockspace::update(Taskbar& taskbar, Documents& documents, Manager& manager, Settings& settings,
+1 -1
View File
@@ -37,7 +37,7 @@ namespace anm2ed::imgui
Welcome welcome;
public:
void tick(Manager&, Settings&);
void tick(Manager&, Settings&, float);
void update(Taskbar&, Documents&, Manager&, Settings&, Resources&, Dialog&, Clipboard&);
};
}
+13 -28
View File
@@ -34,8 +34,6 @@ namespace anm2ed::imgui
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);
constexpr auto PLAYBACK_TICK_RATE = 30.0f;
namespace
{
std::filesystem::path render_destination_directory(const std::filesystem::path& path, int type)
@@ -153,7 +151,7 @@ namespace anm2ed::imgui
AnimationPreview::AnimationPreview() : Canvas(vec2()) {}
void AnimationPreview::tick(Manager& manager, Settings& settings)
void AnimationPreview::tick(Manager& manager, Settings& settings, float deltaSeconds)
{
auto& document = *manager.get();
auto& anm2 = document.anm2;
@@ -176,8 +174,9 @@ namespace anm2ed::imgui
auto& path = settings.renderPath;
auto pathString = path::to_utf8(path);
auto& type = settings.renderType;
auto renderFrameRate = std::max(settings.playbackTickRate, 1);
if (playback.time > end || playback.isFinished)
if (playback.time >= (float)end + 1.0f || playback.isFinished)
{
if (settings.timelineIsSound) audioStream.capture_end(mixer);
@@ -262,7 +261,7 @@ namespace anm2ed::imgui
{
if (settings.timelineIsSound && type != render::GIF)
{
if (!render_audio_stream_generate(audioStream, anm2.content.sounds, renderFrameSoundIDs, anm2.info.fps))
if (!render_audio_stream_generate(audioStream, anm2.content.sounds, renderFrameSoundIDs, renderFrameRate))
{
toasts.push(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED));
logger.error("Failed to generate deterministic render audio stream; exporting without audio.");
@@ -273,7 +272,7 @@ namespace anm2ed::imgui
audioStream.stream.clear();
if (animation_render(ffmpegPath, path, renderTempFrames, renderTempFrameDurations, audioStream,
(render::Type)type, anm2.info.fps))
(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),
@@ -325,8 +324,10 @@ namespace anm2ed::imgui
auto frameSoundID = -1;
if (settings.timelineIsSound && !anm2.content.sounds.empty())
{
if (auto animation = document.animation_get();
animation && animation->triggers.isVisible && (!settings.timelineIsOnlyShowLayers || manager.isRecording))
auto soundTime = (int)std::floor(playback.time);
if (auto animation = document.animation_get(); soundTime != renderFrameSoundTimePrev && animation &&
animation->triggers.isVisible &&
(!settings.timelineIsOnlyShowLayers || manager.isRecording))
{
if (auto trigger = animation->triggers.frame_generate(playback.time, anm2::TRIGGER); trigger.isVisible)
{
@@ -341,6 +342,7 @@ namespace anm2ed::imgui
}
}
}
renderFrameSoundTimePrev = soundTime;
}
renderFrameSoundIDs.push_back(frameSoundID);
@@ -352,24 +354,8 @@ namespace anm2ed::imgui
if (Texture::write_pixels_png(framePath, size, pixels.data()))
{
renderTempFrames.push_back(framePath);
auto nowCounter = SDL_GetPerformanceCounter();
auto counterFrequency = SDL_GetPerformanceFrequency();
auto fallbackDuration = 1.0 / (double)std::max(anm2.info.fps, 1);
if (renderTempFrames.size() == 1)
{
renderCaptureCounterPrev = nowCounter;
renderTempFrameDurations.push_back(fallbackDuration);
}
else
{
auto elapsedCounter = nowCounter - renderCaptureCounterPrev;
auto frameDuration = counterFrequency > 0 ? (double)elapsedCounter / (double)counterFrequency : 0.0;
frameDuration = std::max(frameDuration, 1.0 / 1000.0);
renderTempFrameDurations.back() = frameDuration;
renderTempFrameDurations.push_back(frameDuration);
renderCaptureCounterPrev = nowCounter;
}
auto fallbackDuration = 1.0 / (double)renderFrameRate;
renderTempFrameDurations.push_back(fallbackDuration);
}
else
{
@@ -416,7 +402,6 @@ namespace anm2ed::imgui
}
auto fps = std::max(anm2.info.fps, 1);
auto deltaSeconds = manager.isRecording ? (1.0f / (float)fps) : (1.0f / PLAYBACK_TICK_RATE);
playback.tick(fps, animation->frameNum, (animation->isLoop || settings.playbackIsLoop) && !manager.isRecording,
deltaSeconds);
@@ -657,7 +642,7 @@ namespace anm2ed::imgui
renderTempFrames.clear();
renderTempFrameDurations.clear();
renderFrameSoundIDs.clear();
renderCaptureCounterPrev = 0;
renderFrameSoundTimePrev = -1;
if (settings.renderType == render::PNGS)
{
renderTempDirectory = settings.renderPath;
+2 -2
View File
@@ -33,11 +33,11 @@ namespace anm2ed::imgui
std::vector<std::filesystem::path> renderTempFrames{};
std::vector<double> renderTempFrameDurations{};
std::vector<int> renderFrameSoundIDs{};
Uint64 renderCaptureCounterPrev{};
int renderFrameSoundTimePrev{-1};
public:
AnimationPreview();
void tick(Manager&, Settings&);
void tick(Manager&, Settings&, float);
void update(Manager&, Settings&, Resources&);
};
}
+1 -1
View File
@@ -1312,7 +1312,7 @@ namespace anm2ed::imgui
document.frameTime = playback.time;
}
playback.clamp(settings.playbackIsClamp ? length : anm2::FRAME_NUM_MAX);
if (!playback.isPlaying) playback.clamp(settings.playbackIsClamp ? length : anm2::FRAME_NUM_MAX);
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) isDragging = false;
+7
View File
@@ -32,6 +32,13 @@ namespace anm2ed::imgui::wizard
ImGui::Checkbox(localize.get(LABEL_VSYNC), &temporary.isVsync);
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_VSYNC));
ImGui::SeparatorText(localize.get(LABEL_PLAYBACK_MENU));
ImGui::RadioButton("30 Hz", &temporary.playbackTickRate, 30);
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_PLAYBACK_TICK_RATE));
ImGui::SameLine();
ImGui::RadioButton("60 Hz", &temporary.playbackTickRate, 60);
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_PLAYBACK_TICK_RATE));
ImGui::SeparatorText(localize.get(LABEL_LOCALIZATION));
ImGui::Combo(localize.get(LABEL_LANGUAGE), &temporary.language, LANGUAGE_STRINGS, LANGUAGE_COUNT);
ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_LANGUAGE));
+15 -18
View File
@@ -21,30 +21,27 @@ namespace anm2ed
void Playback::tick(int fps, int length, bool isLoop, float deltaSeconds)
{
if (isLoop) isFinished = false;
if (isFinished || !isPlaying || fps <= 0 || length <= 0) return;
if (deltaSeconds <= 0.0f) return;
auto frameDuration = 1.0f / (float)fps;
tickAccumulator += deltaSeconds;
auto steps = (int)std::floor(tickAccumulator / frameDuration);
if (steps <= 0) return;
tickAccumulator -= frameDuration * (float)steps;
time += deltaSeconds * (float)fps;
time += (float)steps;
if (!std::isfinite(time)) time = 0.0f;
if (time > (float)length - 1.0f)
if (isLoop)
{
if (isLoop)
{
time = std::fmod(time, (float)length);
}
else
{
time = (float)length - 1.0f;
isPlaying = false;
isFinished = true;
timing_reset();
}
time = std::fmod(time, (float)length);
if (time < 0.0f) time += (float)length;
return;
}
if (time >= (float)length)
{
time = (float)length - 1.0f;
isPlaying = false;
isFinished = true;
timing_reset();
}
}
+3 -2
View File
@@ -39,7 +39,6 @@ namespace anm2ed
const std::vector<double>& frameDurations, AudioStream& audioStream, render::Type type, int fps)
{
if (framePaths.empty() || ffmpegPath.empty() || path.empty()) return false;
(void)frameDurations;
fps = std::max(fps, 1);
auto pathString = path::to_utf8(path);
@@ -132,6 +131,7 @@ namespace anm2ed
auto framePath = framePaths[index];
auto framePathString = path::to_utf8(framePath);
auto frameDuration = defaultFrameDuration;
if (index < frameDurations.size() && frameDurations[index] > 0.0) frameDuration = frameDurations[index];
framesListFile << "file '" << ffmpeg_concat_escape(framePathString) << "'\n";
framesListFile << "duration " << std::format("{:.9f}", frameDuration) << "\n";
}
@@ -143,11 +143,12 @@ namespace anm2ed
command = std::format("\"{0}\" -y -f concat -safe 0 -i \"{1}\"", ffmpegPathString, framesListPathString);
if (!audioInputArguments.empty()) command += " " + audioInputArguments;
command += std::format(" -fps_mode cfr -r {}", fps);
if (type != render::GIF) command += std::format(" -fps_mode cfr -r {}", fps);
switch (type)
{
case render::GIF:
command += " -fps_mode vfr";
command +=
" -lavfi \"split[s0][s1];[s0]palettegen=stats_mode=full:reserve_transparent=1[p];"
"[s1][p]paletteuse=dither=floyd_steinberg:alpha_threshold=128\""
+1
View File
@@ -606,6 +606,7 @@ namespace anm2ed
X(TOOLTIP_PIVOTS, "Toggle the visibility of the animation's pivots.", "Alterna la visibilidad de los pivotes de la animacion.", "Переключить видимость точек поворота анимации.", "切换动画枢轴是否可见.", "애니메이션의 중심점을 표시하거나 숨깁니다.") \
X(TOOLTIP_PLAYBACK_ALWAYS_LOOP, "Animations will always loop during playback, even if looping isn't set.", "Las animaciones siempre se loopearan durante la reproduccion, incluso si \"looping\" no esta activado.", "Анимации всегда будут циклично воспроизводиться во время воспроизведения, даже если цикличность не установлена.", "动画预放时也会循环, 即使循环并未被设置.", "반복 설정이 켜져 있지 않아도 애니메이션이 항상 반복 재생됩니다.") \
X(TOOLTIP_PLAYBACK_CLAMP, "Operations will always be clamped to within the animation's bounds.\nFor example, dragging the playhead, or triggers.", "Las operaciones siempre se haran Clamp a las normas de la animacion.\nPor ejemplo: Arrastrar el Cabezal, o los triggers.", "Операции всегда будут ограничены границами анимации.\nНапример, перетаскивание воспроизводящей головки или триггеров.", "任何操作都会被限制在动画的区间内.\n比如, 拖拽播放指针或者触发器.", "작업이 항상 애니메이션 범위 내로 제한됩니다.\n예: 플레이헤드를 드래그하거나 트리거를 조작할 때.") \
X(TOOLTIP_PLAYBACK_TICK_RATE, "Set how often animation playback updates while previewing.", "Ajusta con que frecuencia se actualiza la reproduccion de animacion durante la vista previa.", "Установить, как часто обновляется воспроизведение анимации в предпросмотре.", "设置动画预览播放时的更新频率。", "미리보기 중 애니메이션 재생이 업데이트되는 빈도를 설정합니다.") \
X(TOOLTIP_PLAY_ANIMATION, "Play the animation.", "Reproduce la animacion.", "Возпроизвести анимацию.", "播放动画.", "애니메이션을 재생합니다.") \
X(TOOLTIP_POSITION, "Change the position of the frame.", "Cambia la posicion del Frame.", "Изменить позицию кадра.", "更改此帧的位置.", "프레임의 위치를 변경합니다.") \
X(TOOLTIP_PREVIEW_ZOOM, "Change the zoom of the preview.", "Cambia el zoom de la vista previa.", "Изменить масштаб предпросмотра.", "更改预览视图的缩放.", "미리보기의 줌을 변경합니다.") \
+1
View File
@@ -74,6 +74,7 @@ namespace anm2ed
\
X(PLAYBACK_IS_LOOP, playbackIsLoop, STRING_UNDEFINED, BOOL, true) \
X(PLAYBACK_IS_CLAMP, playbackIsClamp, STRING_UNDEFINED, BOOL, true) \
X(PLAYBACK_TICK_RATE, playbackTickRate, STRING_UNDEFINED, INT, 30) \
\
X(CHANGE_IS_CROP_X, changeIsCropX, STRING_UNDEFINED, BOOL, false) \
X(CHANGE_IS_CROP_Y, changeIsCropY, STRING_UNDEFINED, BOOL, false) \
+4 -14
View File
@@ -18,8 +18,6 @@ using namespace anm2ed::types;
namespace anm2ed
{
constexpr auto TICK_RATE = 30;
constexpr auto TICK_INTERVAL = (1000 / TICK_RATE);
constexpr auto UPDATE_RATE = 120;
constexpr auto UPDATE_INTERVAL = (1000 / UPDATE_RATE);
@@ -33,7 +31,7 @@ namespace anm2ed
manager.chords_set(settings);
}
void State::tick(Settings& settings) { dockspace.tick(manager, settings); }
void State::tick(Settings& settings, float deltaSeconds) { dockspace.tick(manager, settings, deltaSeconds); }
void State::update(SDL_Window*& window, Settings& settings)
{
@@ -160,16 +158,8 @@ namespace anm2ed
auto currentTick = SDL_GetTicks();
auto currentUpdate = SDL_GetTicks();
auto isRecording = manager.isRecording;
auto tickIntervalMs = (double)TICK_INTERVAL;
if (isRecording)
{
if (auto document = manager.get())
{
auto fps = std::max(document->anm2.info.fps, 1);
tickIntervalMs = std::max(1.0, 1000.0 / (double)fps);
}
}
auto tickRate = std::max(settings.playbackTickRate, 1);
auto tickIntervalMs = 1000.0 / (double)tickRate;
if (isRecording != wasRecording)
{
@@ -194,7 +184,7 @@ namespace anm2ed
if (tickAccumulatorMs >= tickIntervalMs)
{
tick(settings);
tick(settings, (float)(tickIntervalMs / 1000.0));
tickAccumulatorMs -= tickIntervalMs;
}
+1 -1
View File
@@ -8,7 +8,7 @@ namespace anm2ed
{
class State
{
void tick(Settings&);
void tick(Settings&, float);
void update(SDL_Window*&, Settings&);
void render(SDL_Window*&, Settings&);