Animation System Refactors
Closed, ResolvedPublic

Description

A: Why Refactor?

After a few months working with and around Krita's animation systems, @eoinoneill and I want to suggest and discuss a few potential changes.
While it's always possible to just build on top of what's already there, we both think that it will be easier and cleaner in the long to implement things like clones, cycles, animated transform masks, and other new features if we take the extra time now to clean up certain aspects of the Krita's animation system.
We can do this by defining clearer responsibilities and ownership for each piece, improving encapsulation between parts and making sure that interfaces are straight-forward and as easy to use as possible.

B: Suggested Changes

  • Remove subclass-specific interface pollution from KisKeyframe and KisKeyframeChannel base classes.

A number of non-common methods and data have been added to the KisKeyframe and KisKeyframeChannel base classes.
This adds unnecessary clutter to the base interfaces and makes it harder to use and implement the derived classes. For example:

// KisKeyframeChannel.h //

    virtual bool hasScalarValue() const = 0;
    virtual qreal minScalarValue() const;
    virtual qreal maxScalarValue() const;
    virtual qreal scalarValue(const KisKeyframeSP keyframe) const;
    virtual void setScalarValue(KisKeyframeSP keyframe, qreal value, KUndo2Command *parentCommand = 0);

// KisKeyframeChannel.cpp //

qreal KisKeyframeChannel::scalarValue(const KisKeyframeSP keyframe) const
{
    Q_UNUSED(keyframe);

    return 0;
}

void KisKeyframeChannel::setScalarValue(KisKeyframeSP keyframe, qreal value, KUndo2Command *parentCommand)
{
    Q_UNUSED(keyframe);
    Q_UNUSED(value);
    Q_UNUSED(parentCommand);
}

// KisScalarKeyframeChannel.cpp //

qreal KisScalarKeyframeChannel::scalarValue(const KisKeyframeSP keyframe) const
 {
     KisScalarKeyframe *key = dynamic_cast<KisScalarKeyframe*>(keyframe.data());
     Q_ASSERT(key != 0);
     return key->value;
 }

void KisScalarKeyframeChannel::setScalarValue(KisKeyframeSP keyframe, qreal value, KUndo2Command *parentCommand)
 {
     QScopedPointer<KUndo2Command> tempCommand;
     if (!parentCommand) {
         tempCommand.reset(new KUndo2Command());
         parentCommand = tempCommand.data();
     }
 
     qreal oldValue = scalarValue(keyframe);
     KUndo2Command *cmd = new Private::SetValueCommand(this, keyframe, oldValue, value, parentCommand);
     cmd->redo();
 }
  • KisKeyframeChannel should have sole responsibility for mapping "times" to "keyframes" & time state should be removed from KisKeyframe.

Right now, each KisKeyframe maintains an internal "time" state that describes its own location within the channel.
Not only is this duplication of data error prone as care has to be taken to keep things in sync, it implies a one-to-one relationship between virtual KisKeyframes and times, which complicates things clone frames and cycles.
It also creates the temptation to awkwardly use the KisKeyframe's internal time or circumvent the KisKeyframeChannel's interface:

bool KisKeyframeChannel::moveKeyframe(KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand)
 {
     LAZY_INITIALIZE_PARENT_COMMAND(parentCommand);
 
    if (newTime == keyframe->time()) return false;
 
    KisKeyframeSP other = keyframeAt(newTime);
    if (other) {
        deleteKeyframeImpl(other, parentCommand, false);
     }
 
    const int srcTime = keyframe->time();

    KUndo2Command *cmd = new KisMoveFrameCommand(this, keyframe, srcTime, newTime, parentCommand);
     cmd->redo();
 
    if (srcTime == 0) {
        addKeyframe(srcTime, parentCommand);
     }
 
     return true;
}

Because the KisKeyframeChannel is effectively a container of KisKeyframes is shouldn't be necessary for the channel to ask a given keyframe for its internal memory of where it is in time.

  • KisKeyframeChannel interface should operate on "time" indices whenever possible, and should only return KisKeyframes when the value of a keyframe is needed by the caller.

Related to the last point, this KisKeyframeChannel has quite a few methods which take in references to Keyframes (that it already owns) even though the operations only require access to time.
Here's a small example:

// Current:
-    bool deleteKeyframe(KisKeyframeSP keyframe, KUndo2Command *parentCommand = 0);
-    bool moveKeyframe(KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand = 0);

// Improved:
+    bool deleteKeyframe(int time, KUndo2Command *parentCommand = 0);
+    bool moveKeyframe(int sourceTime, int targetTime, KUndo2Command *parentCommand = 0);
  • Enforce separation between time, virtual keyframes & physical keyframes.

The KisPaintDevice is responsible for operations of "physical keyframes" using integer keyframeID handles, stored within a KisRasterKeyframe.
The KisKeyframeChannel is responsible for mapping units of time (frames numbers) to "virtual" KisKeyframes.

  • KisRasterKeyframe RAII

A keyframeID is generated by the KisPaintDevice upon construction of a KisRasterKeyframeSP and freed/released upon destruction.

emmetoneill triaged this task as Normal priority.
This comment was removed by emmetoneill.
emmetoneill updated the task description. (Show Details)Jul 10 2020, 10:34 PM
emmetoneill updated the task description. (Show Details)Jul 10 2020, 11:26 PM
emmetoneill updated the task description. (Show Details)Jul 10 2020, 11:32 PM
emmetoneill updated the task description. (Show Details)
emmetoneill updated the task description. (Show Details)Jul 10 2020, 11:35 PM

Remove subclass-specific interface pollution from KisKeyframe and KisKeyframeChannel base classes

I'm totally agree with this step. The interface is used only by KisAnimationCurveDocker for the related functionality. There is no reason to provide that interface in KisRasterKeyframeChannel. Though it means that KisAnimationCurveDocker will have to use dynamic_cast extensively.

KisKeyframeChannel should have sole responsibility for mapping "times" to "keyframes" & time state should be removed from KisKeyframe

KisKeyframeChannel interface should operate on "time" indices whenever possible, and should only return KisKeyframes when the value of a keyframe is needed by the caller.

I'm not sure. There is a problem.

We have three different entities:

  1. frame --- just a timeslot on a timeline
  2. keyframe --- entity represented by KisKeyframe
  3. hold frame --- a frame that shows a different keyframe

Right now all API calls of KisKeyframeChannel operate on keyframes only. And operations's mapping from frame to keyframe happens in KisAnimationUtils.

Your proposal will change operations' space from keyframe to frame. It will basically make it more highlevel. The idea sounds good in general, but it needs experimenting if it is possible at all.

Possible solution:

  1. We either remove the current deleteKeyframe(KisKeyframeSP)/moveKeyframe(KisKeyframeSP) interface or keep it untouched. The choice depends on whether the current GUI code can live without it or not, I'm not sure.
  1. We add a highlevel interface deleteFrames(KisTimeRange)/moveFrames(KisTimeRange), which basically moves code from KisAnimationUtils to the class itself. Note, that the name has changed, since they don't operate on "keyframes" anymore. Instead they operate on a range of frames.
  1. I'm not sure if it would still be possible to remove time from KisKeyframe or not. Perhaps yes, but it needs experimenting.

Enforce separation between time, virtual keyframes & physical keyframes

I guess it is kind of true for KisRanterKeyframeChannel already, so it sounds good.

KisRasterKeyframe RAII

I didn't understand this part

@dkazakov We've posted an initial MR with some refactors. https://invent.kde.org/graphics/krita/-/merge_requests/448

We tried to keep your suggestions in mind here, but keep in mind we were a few days into the refactor by that point. =]

emmetoneill closed this task as Resolved.Sep 28 2020, 11:09 PM
emmetoneill claimed this task.

This is basically finished now, there is more that could potentially be done, but it's not a big deal.