Extract animation operation planning
This commit is contained in:
291
src/app_core/document_animation.h
Normal file
291
src/app_core/document_animation.h
Normal file
@@ -0,0 +1,291 @@
|
||||
#pragma once
|
||||
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
namespace pp::app {
|
||||
|
||||
inline constexpr int document_animation_default_frame_duration = 1;
|
||||
|
||||
enum class DocumentAnimationOperation {
|
||||
add_frame,
|
||||
duplicate_frame,
|
||||
remove_frame,
|
||||
adjust_duration,
|
||||
move_frame,
|
||||
goto_frame,
|
||||
goto_next,
|
||||
goto_previous,
|
||||
set_onion_size,
|
||||
};
|
||||
|
||||
struct DocumentAnimationOperationPlan {
|
||||
DocumentAnimationOperation operation = DocumentAnimationOperation::goto_frame;
|
||||
int frame_count = 1;
|
||||
int current_frame = 0;
|
||||
int selected_frame = 0;
|
||||
int target_frame = 0;
|
||||
int frame_duration = document_animation_default_frame_duration;
|
||||
int duration_delta = 0;
|
||||
int move_offset = 0;
|
||||
int onion_size = 1;
|
||||
bool requires_selected_frame = false;
|
||||
bool mutates_document = false;
|
||||
bool reloads_animation_layers = false;
|
||||
bool updates_canvas_animation = false;
|
||||
bool marks_unsaved = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_count(int frame_count) noexcept
|
||||
{
|
||||
if (frame_count <= 0) {
|
||||
return pp::foundation::Status::invalid_argument("animation layer must contain at least one frame");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_index(
|
||||
int frame_count,
|
||||
int index) noexcept
|
||||
{
|
||||
const auto count_status = validate_animation_frame_count(frame_count);
|
||||
if (!count_status.ok()) {
|
||||
return count_status;
|
||||
}
|
||||
|
||||
if (index < 0 || index >= frame_count) {
|
||||
return pp::foundation::Status::out_of_range("animation frame index is outside the layer");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_duration(int duration) noexcept
|
||||
{
|
||||
if (duration < 1) {
|
||||
return pp::foundation::Status::invalid_argument("animation frame duration must be at least 1");
|
||||
}
|
||||
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_add_frame(
|
||||
int frame_count,
|
||||
int current_frame)
|
||||
{
|
||||
const auto count_status = validate_animation_frame_count(frame_count);
|
||||
if (!count_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(count_status);
|
||||
}
|
||||
|
||||
if (current_frame < 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("current animation frame must not be negative"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::add_frame;
|
||||
plan.frame_count = frame_count;
|
||||
plan.current_frame = current_frame;
|
||||
plan.selected_frame = frame_count;
|
||||
plan.target_frame = current_frame;
|
||||
plan.mutates_document = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.marks_unsaved = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_duplicate_frame(
|
||||
int frame_count,
|
||||
int selected_frame)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::duplicate_frame;
|
||||
plan.frame_count = frame_count;
|
||||
plan.selected_frame = selected_frame;
|
||||
plan.target_frame = selected_frame + 1;
|
||||
plan.requires_selected_frame = true;
|
||||
plan.mutates_document = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.marks_unsaved = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_remove_frame(
|
||||
int frame_count,
|
||||
int selected_frame)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (frame_count <= 1) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation layer must keep at least one frame"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::remove_frame;
|
||||
plan.frame_count = frame_count;
|
||||
plan.selected_frame = selected_frame;
|
||||
plan.target_frame = std::min(selected_frame, frame_count - 2);
|
||||
plan.requires_selected_frame = true;
|
||||
plan.mutates_document = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.marks_unsaved = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_adjust_duration(
|
||||
int frame_count,
|
||||
int selected_frame,
|
||||
int current_duration,
|
||||
int delta)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
const auto duration_status = validate_animation_frame_duration(current_duration);
|
||||
if (!duration_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(duration_status);
|
||||
}
|
||||
|
||||
if (delta == 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation frame duration delta must not be zero"));
|
||||
}
|
||||
|
||||
if (delta > 0 && current_duration > std::numeric_limits<int>::max() - delta) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("animation frame duration would overflow"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::adjust_duration;
|
||||
plan.frame_count = frame_count;
|
||||
plan.selected_frame = selected_frame;
|
||||
plan.target_frame = selected_frame;
|
||||
plan.frame_duration = std::max(current_duration + delta, 1);
|
||||
plan.duration_delta = delta;
|
||||
plan.requires_selected_frame = true;
|
||||
plan.mutates_document = plan.frame_duration != current_duration;
|
||||
plan.reloads_animation_layers = plan.mutates_document;
|
||||
plan.updates_canvas_animation = plan.mutates_document;
|
||||
plan.marks_unsaved = plan.mutates_document;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_move_frame(
|
||||
int frame_count,
|
||||
int selected_frame,
|
||||
int offset)
|
||||
{
|
||||
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
|
||||
if (!index_status.ok()) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
|
||||
}
|
||||
|
||||
if (offset == 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation frame move offset must not be zero"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::move_frame;
|
||||
plan.frame_count = frame_count;
|
||||
plan.selected_frame = selected_frame;
|
||||
const auto unclamped_target = static_cast<std::int64_t>(selected_frame) + static_cast<std::int64_t>(offset);
|
||||
plan.target_frame = static_cast<int>(std::clamp<std::int64_t>(unclamped_target, 0, frame_count - 1));
|
||||
plan.move_offset = offset;
|
||||
plan.requires_selected_frame = true;
|
||||
plan.mutates_document = plan.target_frame != selected_frame;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.marks_unsaved = plan.mutates_document;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_goto_frame(
|
||||
int total_duration,
|
||||
int frame)
|
||||
{
|
||||
if (total_duration <= 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation duration must be greater than zero"));
|
||||
}
|
||||
|
||||
if (frame < 0 || frame >= total_duration) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::out_of_range("animation timeline frame is outside the document"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::goto_frame;
|
||||
plan.frame_count = total_duration;
|
||||
plan.current_frame = frame;
|
||||
plan.target_frame = frame;
|
||||
plan.reloads_animation_layers = true;
|
||||
plan.updates_canvas_animation = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_step_frame(
|
||||
int total_duration,
|
||||
int current_frame,
|
||||
int offset)
|
||||
{
|
||||
const auto current_status = plan_animation_goto_frame(total_duration, current_frame);
|
||||
if (!current_status) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(current_status.status());
|
||||
}
|
||||
|
||||
if (offset == 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation frame step offset must not be zero"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = offset > 0 ? DocumentAnimationOperation::goto_next : DocumentAnimationOperation::goto_previous;
|
||||
plan.frame_count = total_duration;
|
||||
plan.current_frame = current_frame;
|
||||
auto target = (static_cast<std::int64_t>(current_frame) + static_cast<std::int64_t>(offset))
|
||||
% static_cast<std::int64_t>(total_duration);
|
||||
if (target < 0) {
|
||||
target += total_duration;
|
||||
}
|
||||
plan.target_frame = static_cast<int>(target);
|
||||
plan.updates_canvas_animation = true;
|
||||
plan.reloads_animation_layers = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_onion_size(int onion_size)
|
||||
{
|
||||
if (onion_size < 0) {
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
|
||||
pp::foundation::Status::invalid_argument("animation onion size must not be negative"));
|
||||
}
|
||||
|
||||
DocumentAnimationOperationPlan plan;
|
||||
plan.operation = DocumentAnimationOperation::set_onion_size;
|
||||
plan.onion_size = onion_size;
|
||||
plan.updates_canvas_animation = true;
|
||||
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
|
||||
}
|
||||
|
||||
} // namespace pp::app
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "pch.h"
|
||||
#include "node_panel_animation.h"
|
||||
#include "app_core/document_animation.h"
|
||||
#include "node_button.h"
|
||||
#include "node_button_custom.h"
|
||||
#include "renderer_gl/opengl_capabilities.h"
|
||||
@@ -45,62 +46,180 @@ void NodePanelAnimation::init_controls()
|
||||
m_frame_label = find<NodeText>("frame-index");
|
||||
|
||||
btn_add->on_click = [this](Node*) {
|
||||
const auto plan = pp::app::plan_animation_add_frame(
|
||||
Canvas::I->layer().frames_count(),
|
||||
Canvas::I->m_anim_frame);
|
||||
if (!plan)
|
||||
return;
|
||||
Canvas::I->layer().add_frame();
|
||||
load_layers();
|
||||
if (plan.value().marks_unsaved)
|
||||
Canvas::I->m_unsaved = true;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_update();
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
};
|
||||
btn_duplicate->on_click = [this](Node*) {
|
||||
Canvas::I->layer().duplicate_frame(m_selected_frame_index);
|
||||
load_layers();
|
||||
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
|
||||
{
|
||||
const auto plan = pp::app::plan_animation_duplicate_frame(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index);
|
||||
if (!plan)
|
||||
return;
|
||||
layer->duplicate_frame(plan.value().selected_frame);
|
||||
if (plan.value().marks_unsaved)
|
||||
Canvas::I->m_unsaved = true;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_update();
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
}
|
||||
};
|
||||
btn_remove->on_click = [this](Node*) {
|
||||
Canvas::I->layer_with_id(m_selected_frame_layer_id)->remove_frame(m_selected_frame_index);
|
||||
load_layers();
|
||||
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
|
||||
{
|
||||
const auto plan = pp::app::plan_animation_remove_frame(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index);
|
||||
if (!plan)
|
||||
return;
|
||||
layer->remove_frame(plan.value().selected_frame);
|
||||
m_selected_frame_index = plan.value().target_frame;
|
||||
if (plan.value().marks_unsaved)
|
||||
Canvas::I->m_unsaved = true;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_goto_frame(plan.value().target_frame);
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
}
|
||||
};
|
||||
btn_up->on_click = [this](Node*) {
|
||||
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
|
||||
layer->set_frame_duration(m_selected_frame_index, glm::max(layer->frame_duration(m_selected_frame_index) + 1, 1));
|
||||
load_layers();
|
||||
{
|
||||
const auto index_status = pp::app::validate_animation_frame_index(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index);
|
||||
if (!index_status.ok())
|
||||
return;
|
||||
const auto plan = pp::app::plan_animation_adjust_duration(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index,
|
||||
layer->frame_duration(m_selected_frame_index),
|
||||
1);
|
||||
if (!plan)
|
||||
return;
|
||||
layer->set_frame_duration(plan.value().selected_frame, plan.value().frame_duration);
|
||||
if (plan.value().marks_unsaved)
|
||||
Canvas::I->m_unsaved = true;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_update();
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
}
|
||||
};
|
||||
btn_down->on_click = [this](Node*) {
|
||||
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
|
||||
layer->set_frame_duration(m_selected_frame_index, glm::max(layer->frame_duration(m_selected_frame_index) - 1, 1));
|
||||
load_layers();
|
||||
{
|
||||
const auto index_status = pp::app::validate_animation_frame_index(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index);
|
||||
if (!index_status.ok())
|
||||
return;
|
||||
const auto plan = pp::app::plan_animation_adjust_duration(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index,
|
||||
layer->frame_duration(m_selected_frame_index),
|
||||
-1);
|
||||
if (!plan)
|
||||
return;
|
||||
layer->set_frame_duration(plan.value().selected_frame, plan.value().frame_duration);
|
||||
if (plan.value().marks_unsaved)
|
||||
Canvas::I->m_unsaved = true;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_update();
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
}
|
||||
};
|
||||
btn_left->on_click = [this](Node*) {
|
||||
if (!m_selected_frame)
|
||||
return;
|
||||
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
|
||||
m_selected_frame_index = layer->move_frame_offset(m_selected_frame_index, -1);
|
||||
Canvas::I->anim_goto_frame(m_selected_frame_index);
|
||||
load_layers();
|
||||
{
|
||||
const auto plan = pp::app::plan_animation_move_frame(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index,
|
||||
-1);
|
||||
if (!plan)
|
||||
return;
|
||||
m_selected_frame_index = layer->move_frame_offset(plan.value().selected_frame, plan.value().move_offset);
|
||||
if (plan.value().marks_unsaved)
|
||||
Canvas::I->m_unsaved = true;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_goto_frame(m_selected_frame_index);
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
}
|
||||
};
|
||||
btn_right->on_click = [this](Node*) {
|
||||
if (!m_selected_frame)
|
||||
return;
|
||||
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
|
||||
m_selected_frame_index = layer->move_frame_offset(m_selected_frame_index, +1);
|
||||
Canvas::I->anim_goto_frame(m_selected_frame_index);
|
||||
load_layers();
|
||||
{
|
||||
const auto plan = pp::app::plan_animation_move_frame(
|
||||
layer->frames_count(),
|
||||
m_selected_frame_index,
|
||||
1);
|
||||
if (!plan)
|
||||
return;
|
||||
m_selected_frame_index = layer->move_frame_offset(plan.value().selected_frame, plan.value().move_offset);
|
||||
if (plan.value().marks_unsaved)
|
||||
Canvas::I->m_unsaved = true;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_goto_frame(m_selected_frame_index);
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
}
|
||||
};
|
||||
|
||||
m_onion->on_select = [this] (Node* target, int index) {
|
||||
m_timeline->m_onion_size = m_onion->get_int();
|
||||
Canvas::I->anim_update();
|
||||
const auto plan = pp::app::plan_animation_onion_size(m_onion->get_int());
|
||||
if (!plan)
|
||||
return;
|
||||
m_timeline->m_onion_size = plan.value().onion_size;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_update();
|
||||
};
|
||||
|
||||
m_timeline->on_frame_changed = [this] (NodeAnimationTimeline* target, int frame) {
|
||||
LOG("goto frame %d", frame);
|
||||
Canvas::I->anim_goto_frame(frame);
|
||||
load_layers();
|
||||
const auto plan = pp::app::plan_animation_goto_frame(Canvas::I->anim_duration(), frame);
|
||||
if (!plan)
|
||||
return;
|
||||
LOG("goto frame %d", plan.value().target_frame);
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_goto_frame(plan.value().target_frame);
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
};
|
||||
|
||||
btn_next->on_click = [this] (Node* target) {
|
||||
Canvas::I->anim_goto_next();
|
||||
load_layers();
|
||||
const auto plan = pp::app::plan_animation_step_frame(Canvas::I->anim_duration(), Canvas::I->m_anim_frame, 1);
|
||||
if (!plan)
|
||||
return;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_goto_frame(plan.value().target_frame);
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
};
|
||||
btn_prev->on_click = [this](Node* target) {
|
||||
Canvas::I->anim_goto_prev();
|
||||
load_layers();
|
||||
const auto plan = pp::app::plan_animation_step_frame(Canvas::I->anim_duration(), Canvas::I->m_anim_frame, -1);
|
||||
if (!plan)
|
||||
return;
|
||||
if (plan.value().updates_canvas_animation)
|
||||
Canvas::I->anim_goto_frame(plan.value().target_frame);
|
||||
if (plan.value().reloads_animation_layers)
|
||||
load_layers();
|
||||
};
|
||||
btn_play->on_click = [this] (Node* target) {
|
||||
static auto mode = Canvas::I->m_current_mode;
|
||||
@@ -183,9 +302,13 @@ void NodePanelAnimation::on_tick(float dt)
|
||||
if (m_playback_timer > (1.f / m_fps->get_float()))
|
||||
{
|
||||
m_playback_timer = 0;
|
||||
Canvas::I->anim_goto_next();
|
||||
m_timeline->m_frame = Canvas::I->m_anim_frame;
|
||||
update_frames();
|
||||
const auto plan = pp::app::plan_animation_step_frame(Canvas::I->anim_duration(), Canvas::I->m_anim_frame, 1);
|
||||
if (plan)
|
||||
{
|
||||
Canvas::I->anim_goto_frame(plan.value().target_frame);
|
||||
m_timeline->m_frame = Canvas::I->m_anim_frame;
|
||||
update_frames();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user