Files
panopainter/src/app_core/document_animation.h

621 lines
24 KiB
C++

#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,
select_frame,
goto_frame,
goto_next,
goto_previous,
playback_step,
toggle_playback,
set_onion_size,
};
enum class DocumentAnimationPanelAction {
goto_frame,
next_frame,
previous_frame,
playback_step,
toggle_playback,
};
struct DocumentAnimationPanelState {
int total_duration = 1;
int current_frame = 0;
bool playback_active = false;
};
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;
int layer_index = 0;
std::uint32_t layer_id = 0;
int playback_idle_ms = 100;
bool requires_selected_frame = false;
bool mutates_document = false;
bool reloads_animation_layers = false;
bool updates_canvas_animation = false;
bool marks_unsaved = false;
bool playback_was_active = false;
bool playback_active = false;
bool resets_playback_timer = false;
};
class DocumentAnimationServices {
public:
virtual ~DocumentAnimationServices() = default;
virtual void add_frame() = 0;
virtual void duplicate_frame(int selected_frame) = 0;
virtual void remove_frame(int selected_frame, int target_frame) = 0;
virtual void set_frame_duration(int selected_frame, int duration) = 0;
virtual int move_frame(int selected_frame, int move_offset) = 0;
virtual void select_frame(std::uint32_t layer_id, int layer_index, int selected_frame) = 0;
virtual void select_layer(int layer_index) = 0;
virtual void goto_frame(int target_frame) = 0;
virtual void set_timeline_frame(int target_frame) = 0;
virtual void set_onion_size(int onion_size) = 0;
virtual void capture_playback_restore_mode() = 0;
virtual void enter_playback_camera_mode() = 0;
virtual void restore_playback_canvas_mode() = 0;
virtual void set_playback_active(bool active) = 0;
virtual void reset_playback_timer() = 0;
virtual void set_playback_idle_ms(int idle_ms) = 0;
virtual void update_canvas_animation() = 0;
virtual void update_frame_status() = 0;
virtual void reload_animation_layers() = 0;
virtual void mark_unsaved() = 0;
};
[[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_select_frame(
int frame_count,
int layer_index,
std::uint32_t layer_id,
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 (layer_index < 0) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::out_of_range("animation layer index must not be negative"));
}
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::select_frame;
plan.frame_count = frame_count;
plan.selected_frame = selected_frame;
plan.target_frame = selected_frame;
plan.layer_index = layer_index;
plan.layer_id = layer_id;
plan.requires_selected_frame = true;
plan.updates_canvas_animation = true;
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_playback_step(
int total_duration,
int current_frame,
int offset)
{
const auto step = plan_animation_step_frame(total_duration, current_frame, offset);
if (!step) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(step.status());
}
auto plan = step.value();
plan.operation = DocumentAnimationOperation::playback_step;
plan.move_offset = offset;
plan.reloads_animation_layers = false;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_playback_toggle(
bool playback_active)
{
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::toggle_playback;
plan.playback_was_active = playback_active;
plan.playback_active = !playback_active;
plan.playback_idle_ms = playback_active ? 100 : 10;
plan.resets_playback_timer = !playback_active;
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);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_panel_action(
DocumentAnimationPanelAction action,
const DocumentAnimationPanelState& state,
int target_frame = 0)
{
switch (action) {
case DocumentAnimationPanelAction::goto_frame:
return plan_animation_goto_frame(state.total_duration, target_frame);
case DocumentAnimationPanelAction::next_frame:
return plan_animation_step_frame(state.total_duration, state.current_frame, 1);
case DocumentAnimationPanelAction::previous_frame:
return plan_animation_step_frame(state.total_duration, state.current_frame, -1);
case DocumentAnimationPanelAction::playback_step:
return plan_animation_playback_step(state.total_duration, state.current_frame, 1);
case DocumentAnimationPanelAction::toggle_playback:
return plan_animation_playback_toggle(state.playback_active);
}
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::invalid_argument("unknown animation panel action"));
}
[[nodiscard]] inline pp::foundation::Status validate_animation_operation_plan(
const DocumentAnimationOperationPlan& plan) noexcept
{
switch (plan.operation) {
case DocumentAnimationOperation::add_frame:
if (!plan.mutates_document || !plan.marks_unsaved) {
return pp::foundation::Status::invalid_argument("animation add plan must mutate the document");
}
return validate_animation_frame_count(plan.frame_count);
case DocumentAnimationOperation::duplicate_frame:
case DocumentAnimationOperation::remove_frame:
if (!plan.requires_selected_frame || !plan.mutates_document || !plan.marks_unsaved) {
return pp::foundation::Status::invalid_argument("animation selected-frame plan must mutate the document");
}
if (plan.operation == DocumentAnimationOperation::remove_frame && plan.frame_count <= 1) {
return pp::foundation::Status::invalid_argument("animation layer must keep at least one frame");
}
return validate_animation_frame_index(plan.frame_count, plan.selected_frame);
case DocumentAnimationOperation::adjust_duration:
if (!plan.requires_selected_frame) {
return pp::foundation::Status::invalid_argument("animation duration plan must require a selected frame");
}
{
const auto index_status = validate_animation_frame_index(plan.frame_count, plan.selected_frame);
if (!index_status.ok()) {
return index_status;
}
}
return validate_animation_frame_duration(plan.frame_duration);
case DocumentAnimationOperation::move_frame:
if (!plan.requires_selected_frame || plan.move_offset == 0) {
return pp::foundation::Status::invalid_argument("animation move plan must require selected frame and non-zero offset");
}
return validate_animation_frame_index(plan.frame_count, plan.selected_frame);
case DocumentAnimationOperation::select_frame:
if (!plan.requires_selected_frame || !plan.updates_canvas_animation || plan.layer_index < 0) {
return pp::foundation::Status::invalid_argument("animation frame select plan has invalid state");
}
{
const auto index_status = validate_animation_frame_index(plan.frame_count, plan.selected_frame);
if (!index_status.ok()) {
return index_status;
}
}
return validate_animation_frame_index(plan.frame_count, plan.target_frame);
case DocumentAnimationOperation::goto_frame:
case DocumentAnimationOperation::goto_next:
case DocumentAnimationOperation::goto_previous:
case DocumentAnimationOperation::playback_step:
if (!plan.updates_canvas_animation) {
return pp::foundation::Status::invalid_argument("animation goto plan must update canvas animation");
}
if (plan.operation == DocumentAnimationOperation::playback_step && plan.move_offset == 0) {
return pp::foundation::Status::invalid_argument("animation playback step offset must not be zero");
}
return validate_animation_frame_index(plan.frame_count, plan.target_frame);
case DocumentAnimationOperation::toggle_playback:
if (plan.playback_active == plan.playback_was_active) {
return pp::foundation::Status::invalid_argument("animation playback toggle must change state");
}
if (plan.playback_idle_ms <= 0) {
return pp::foundation::Status::invalid_argument("animation playback idle interval must be positive");
}
if (plan.playback_active && !plan.resets_playback_timer) {
return pp::foundation::Status::invalid_argument("animation playback start must reset timer");
}
return pp::foundation::Status::success();
case DocumentAnimationOperation::set_onion_size:
if (plan.onion_size < 0) {
return pp::foundation::Status::invalid_argument("animation onion size must not be negative");
}
if (!plan.updates_canvas_animation) {
return pp::foundation::Status::invalid_argument("animation onion plan must update canvas animation");
}
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown animation operation");
}
[[nodiscard]] inline pp::foundation::Status execute_animation_operation_plan(
const DocumentAnimationOperationPlan& plan,
DocumentAnimationServices& services)
{
const auto validation = validate_animation_operation_plan(plan);
if (!validation.ok()) {
return validation;
}
switch (plan.operation) {
case DocumentAnimationOperation::add_frame:
services.add_frame();
services.mark_unsaved();
services.update_canvas_animation();
services.reload_animation_layers();
return pp::foundation::Status::success();
case DocumentAnimationOperation::duplicate_frame:
services.duplicate_frame(plan.selected_frame);
services.mark_unsaved();
services.update_canvas_animation();
services.reload_animation_layers();
return pp::foundation::Status::success();
case DocumentAnimationOperation::remove_frame:
services.remove_frame(plan.selected_frame, plan.target_frame);
services.mark_unsaved();
if (plan.updates_canvas_animation) {
services.goto_frame(plan.target_frame);
}
if (plan.reloads_animation_layers) {
services.reload_animation_layers();
}
return pp::foundation::Status::success();
case DocumentAnimationOperation::adjust_duration:
if (plan.mutates_document) {
services.set_frame_duration(plan.selected_frame, plan.frame_duration);
services.mark_unsaved();
if (plan.updates_canvas_animation) {
services.update_canvas_animation();
}
if (plan.reloads_animation_layers) {
services.reload_animation_layers();
}
}
return pp::foundation::Status::success();
case DocumentAnimationOperation::move_frame:
{
const auto actual_target_frame = services.move_frame(plan.selected_frame, plan.move_offset);
if (plan.marks_unsaved) {
services.mark_unsaved();
}
if (plan.updates_canvas_animation) {
services.goto_frame(actual_target_frame);
}
if (plan.reloads_animation_layers) {
services.reload_animation_layers();
}
return pp::foundation::Status::success();
}
case DocumentAnimationOperation::goto_frame:
case DocumentAnimationOperation::goto_next:
case DocumentAnimationOperation::goto_previous:
services.goto_frame(plan.target_frame);
if (plan.reloads_animation_layers) {
services.reload_animation_layers();
}
return pp::foundation::Status::success();
case DocumentAnimationOperation::select_frame:
services.select_frame(plan.layer_id, plan.layer_index, plan.selected_frame);
if (plan.updates_canvas_animation) {
services.goto_frame(plan.target_frame);
}
services.select_layer(plan.layer_index);
return pp::foundation::Status::success();
case DocumentAnimationOperation::playback_step:
services.goto_frame(plan.target_frame);
services.set_timeline_frame(plan.target_frame);
services.update_frame_status();
return pp::foundation::Status::success();
case DocumentAnimationOperation::toggle_playback:
if (plan.playback_active) {
services.capture_playback_restore_mode();
services.enter_playback_camera_mode();
if (plan.resets_playback_timer) {
services.reset_playback_timer();
}
} else {
services.restore_playback_canvas_mode();
}
services.set_playback_active(plan.playback_active);
services.set_playback_idle_ms(plan.playback_idle_ms);
return pp::foundation::Status::success();
case DocumentAnimationOperation::set_onion_size:
services.set_onion_size(plan.onion_size);
if (plan.updates_canvas_animation) {
services.update_canvas_animation();
}
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown animation operation");
}
} // namespace pp::app