Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Properties: Rewrite keyframe processing #3840

Closed
wants to merge 7 commits into from
Closed

Conversation

ferdnyc
Copy link
Contributor

@ferdnyc ferdnyc commented Nov 16, 2020

  • PropertiesModel.value_updated() was an overworked method that performed all of the following functions:

    • Inserting new keyframe points
    • Updating existing keyframe points
    • Updating NON-keyframe property values
    • Changing interpolation types for existing keyframes
    • Handling itemChange signals from the properties table (which are emitted when the user edits a property value by double-clicking and typing, as opposed to dragging the sliders or using the context menus)
    • Converting values produced by the properties model to the proper data type / format for insertion into the project data store.

    As a result, it was a confusing ramble of conditional logic.

  • Yet, color properties (specifically, and only) were handled by a separate (but largely cut-and-paste identical) color_updated() method.

So, with this PR, I have:

  • Broken all of the value_updated() logic apart into separate, purpose-focused methods:
    • update_value() to set/modify properties
    • set_point(), a staticmethod that creates/modifies points in a single keyframe point list (called by update_value() one or multiple times to adjust individual property curves)
    • update_interpolation() to apply keyframe interpolation parameters
    • set_interpolation(), a staticmethod that adjusts interpolation values for a single keyframe point list (called by update_interpolation() one or multiple times to adjust individual property curves)
    • item_changed(), the slot that processes the itemChanged signal from the properties view (by calling update_value() with the necessary parameters)
    • interpret_as_type(), a staticmethod used to convert model data values into project data values before applying them to the project dict.
  • Generalized all of the above to handle color and non-color properties, both keyframe and non-keyframe, supporting all implemented data types. (The TODO about adding QFont/QFontInfo support to the old value_updated() method has been redirected to interpret_as_type(), where that support would now be implemented... but has not yet been.)
  • Rewritten remove_keyframe() to supplement and complement the new methods listed above

WIP because I have about 100 corner cases to test, to ensure that this is fully compatible with the old code (except where it fixes bugs in the old code). And also because there's a lot of ongoing conversation in #3823 that's worth incorporating into these changes. But I wanted to get it out there early.

Ultimately, hopefully: Fixes #3823

Comment on lines 318 to 360
PropertiesModel.set_interpolation(
points,
prop_data.get("previous_point_x"),
prop_data.get("closest_point_x"),
interpolation,
curves,
) for points in points_to_update])
Copy link
Contributor Author

@ferdnyc ferdnyc Nov 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Among the untested and/or unproven assumptions in the new code that really need to be tested before we can even think about merging this: The assumption here is that the current keyframe curve segment's endpoints are always located at previous_point_x and closest_point_x.

  • Is that always correct? Is it ever really correct, even?
  • Is it (still?) correct when the playhead is positioned at a keyframe point?
  • Is it (still?) correct when the playhead is positioned beyond the last defined keyframe point?
  • Is it (still?) correct when the playhead is positioned beyond the end of the selected timeline item entirely?
  • (What will closest_point_x be, in the previous three cases? The same as self.frame_number? Which for the last case, would be equal to the clip/transition's end frame.)
  • Are there ever cases where we can't rely on closest_point_x, and have to look at self.frame_number instead, when setting interpolation parameters?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Does set_interpolation() ever need to also insert a value at closest_point_x? (Meaning, does closest_point_x ever represent a position that doesn't already hold an existing keyframe point, but one that needs to be created? Perhaps when the playhead is positioned out beyond the farthest point in the existing list?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Tangentially, I'm now wondering if I should re-rename curves (which I renamed from the supremely unwieldy interpolation_details), and instead call it handles?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to all be OK, except for a special case when the playhead is positioned at frame 1 of the clip. In those cases, closest_point_x == previous_point_x == 1, which confuses the set_interpolation() points-loop because looks for previous_point_x, finds it and sets handle_right, then does a continue and starts looking for closest_point_x — which it's already passed. So, I just have to special-case the two endpoints being the same.

(In which case, trying to create a curve at all is nonsensical, so I think set_interpolation can just bail immediately.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prev_x == closest_x fixed with warning log and early return.

And I did rename curves to handles, which is more descriptive.

Copy link
Contributor Author

@ferdnyc ferdnyc Nov 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • (What will closest_point_x be, in the previous three cases? The same as self.frame_number? Which for the last case, would be equal to the clip/transition's end frame.)
  • Are there ever cases where we can't rely on closest_point_x, and have to look at self.frame_number instead, when setting interpolation parameters?

To answer my own questions, when out past the last keyframe point, closest_point_x is always equal to the position of that point, and previous_point_x is always the previous point. Which is why you need at least 2 keyframe points before interpolation modes become visible/active in the UI.

The good

closest_point_x will never point to a non-keyframe point. No worries about that.

The ugly-ish

It's possible to choose an interpolation mode with the playhead positioned beyond the last keyframe, and that will (somewhat-surprisingly) change the mode of the previous segment, the one between previous_point_x and closest_point_x, even though the playhead is not positioned between those two points.

(In any other situation, setting interpolation mode changes the interpolation of the curve segment the playhead is currently positioned in — that is, the interpolation mode between previous_point_x < self.frame_number <= closest_point_x. But after the last keyframe, when self.frame_number becomes greater than closest_point_x, that value and the value of previous_point_x do not change, so any keyframe/interpolation operations will still affect the previous keyframe curve segment, not the unbounded one in which the playhead is positioned.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should disable/hide interpolation mode once
self.frame_number > ...let's call it last_point_x? (Which we can easily check because it's the only condition in which self.frame_number > closest_point_x.)

The user can still use Insert keyframe, or they can still adjust values to implicitly create a new keyframe, but they can't view or change the interpolation mode of the current curve segment because it doesn't have one.

@ferdnyc
Copy link
Contributor Author

ferdnyc commented Nov 17, 2020

It strikes me that @musteresel's input would be especially helpful here, since ultimately this code is deeply intertwined with the libopenshot keyframe implementation.

@ferdnyc ferdnyc added needs testing Issue or PR needs some help testing from the community or other developers project data Issues or PRs involving .OSP project file handling labels Nov 17, 2020
@ferdnyc
Copy link
Contributor Author

ferdnyc commented Nov 18, 2020

As I've mentioned in the past, most recently in #3823 (comment), currently if "Remove keyframe" is used on a keyframe list which contains only one point, that point is removed and the property promptly breaks because it no longer has any value at all.

(Initial values are set via a keyframe at co.X=1, with that point deleted the property's value just collapses to 0 even if that doesn't make sense as a value for that property — think scale_x and scale_y, for example, which can't usefully be 0.)

Worse, once a keyframe list is broken (emptied) the interface can no longer set values for that property anymore, Which means the property is not just broken, it's unfixable (at least from within OpenShot itself).

This PR's code now fixes that long-standing bug, by simply refusing to delete the last point in a keyframe list. Doesn't matter where it's located, doesn't matter what its value is, every list of points is prohibited from having a length < 1.

This implies that it's still permissible to:

  1. Change a property's value at co.X = 1 to co.Y = 0.5
  2. Move to co.X = 300
  3. Modify the value at co.X = 300 to co.Y = 8.5
  4. Move back to the first frame and Remove keyframe

The property will now have the value 8.5 for the entire length of the clip, as set by the only remaining keyframe point at co.X = 300.

That has the potential to be confusing, so it's probably a bad idea and best avoided unless there's a pretty good reason to do it. But there's really no reason to prohibit this sort of setup, since everything will still function just the way it's supposed to.

@rexdk
Copy link

rexdk commented Nov 18, 2020

My goodness, I'm struggling to keep up here... :)

The case where the x=0 keyframe is deleted: just thinking about the prospect of having a no-animation setting (which we've touched on, and I reckon would help many users). I'd vaguely thought that with 'no-animation' the x=0 keyframe would store the value set. But it can already be deleted in the current build, so I guess nothing has changed in that respect.

There is some arcane logic behind that x=0 keyframe, which is a mystery to me; sometimes it is visible to the user and sometimes not - but why? Possibly because there is no no-animation facility?

There is another small issue that I've not brought up yet, but it might help to mention it while this is hot: the property label helpfully goes blue to tell the dude that interpolation is happening. But after the final keyframe, if no end keyframe is set, the property label is still blue - even though the value is static and not interpolating. This was more of an issue before we could navigate between keyframes, but it's still confusing and difficult to document. I guess it's part of the arcane logic, mysterious.

It may well be better as a separate issue, but possibly helpful to mention now as the relevant code is presumably nearby.

I'm finding it hard to get to grips with the code - I'm sure understanding will come slowly, but there are lots of hurdles - like why is there a closest_point_x - rather than actual_point_x ? Drilling down through the many layers is a slow process. Not asking for help - there are better uses for your time, just explaining why I'm not more incisive

@ferdnyc
Copy link
Contributor Author

ferdnyc commented Nov 18, 2020

@rexdk

The case where the x=0 keyframe is deleted: just thinking about the prospect of having a no-animation setting (which we've touched on, and I reckon would help many users). I'd vaguely thought that with 'no-animation' the x=0 keyframe would store the value set. But it can already be deleted in the current build, so I guess nothing has changed in that respect.

Well, there is no x = 0, frame numbering in OpenShot starts at 1 so the first frame is x = 1. We've occasionally talked about using an x = 0 keyframe to hold the default value for a property, with the advantage being that the user would have no way to either change or delete that point (since they can't seek to frame 0). But, honestly, I think if we were going to go down that road I'd rather use a separate list of default values and no keyframe points, instead of forcing an x = 0 keyframe on every property the same way we force an x = 1 keyframe today.

There is some arcane logic behind that x=0 (@ferdnyc edit:) x=1 keyframe, which is a mystery to me; sometimes it is visible to the user and sometimes not - but why? Possibly because there is no no-animation facility?

That's primarily a rendering bug. At some point the keyframe marks slipped a bit left of where they should be. So right now if a clip is positioned at the first frame of the Timeline, the green tick for all of its x=1 keyframe points is drawn beyond the left edge of the canvas (which makes it invisible). My #3842 will fix that, and green ticks will be visible on all clips again.

I'm finding it hard to get to grips with the code - I'm sure understanding will come slowly, but there are lots of hurdles - like why is there a closest_point_x - rather than actual_point_x ? Drilling down through the many layers is a slow process. Not asking for help - there are better uses for your time, just explaining why I'm not more incisive

I added some followups to my review comment above which hopefully shed some light on that. But in short, actual_point_x is self.frame_number, meaning the current "effective position" of the playhead. (Effective because, if the playhead is positioned outside of the clip, then self.frame_number for that clip is equal to either the first frame, or the last frame — depending which side of the clip the playhead is on.)

So, self.frame_number may or may not be a keyframe point. If it is, then closest_point_x is equal to it, since obviously the closest keyframe to the one we're currently positioned at is that one. If self.frame_number is not a keyframe point, then closest_point_x may be greater than OR less than self.frame_number, again depending on their relative position. (It will be greater than self.frame_number in every case except when self.frame_number is past the last keyframe point.)

previous_point_x will always be the keyframe point immediately before closest_point_x, meaning the other end of its curve segment. AKA its "Bezier interpolation buddy", the point that supplies the starting previous_point_x.handle_right for the curve segment ending at closest_point_x.handle_left, when Bezier is enabled at closest_point_x.

the property label helpfully goes blue to tell the dude that interpolation is happening. But after the final keyframe, if no end keyframe is set, the property label is still blue - even though the value is static and not interpolating.

That's kind of a definitional / semantic question, I suppose — in OpenShot's current logic, that value is still interpolated, because it's computed based on keyframe points and will be non-static across the entire clip. A value that isn't changing, but previously did change, can still be interpolated — the same way, when you have a Constant segment between points A and B (where B.co.Y = 0.5) all of the 0.5 values at A < X < B are interpolated values. For a keyframe list with four points A, B, C, D, if D.co.Y = 0.8, then the 0.8 value for every frame past D (X > D.co.X) is an interpolated value (the way it's currently defined) based on D. If the value at D is modified, all of those interpolated X > D.co.X values will also change.

Effectively, you could say that for every property with a keyframe list, the value is interpolated for every frame X > 1. Technically, that's correct, we just hide the interpolation mode and visible decoration for frames properties with only a single keyframe. In part that's to avoid showing almost the entire property list with a blue background, but more importantly it's meant to keep focus on the values that will change somewhere along the clip's runtime.

If the value is always the same for the entire clip, the property isn't colored. If it changes at any point, it's colored green at keyframe points, blue otherwise — including the frames beyond the last keyframe point.

@ferdnyc
Copy link
Contributor Author

ferdnyc commented Nov 18, 2020

@rexdk

P.S> "closest_point_x" is somewhat of a misnomer, no dispute there, since given two keyframe points A.co.X = 5 and B.co.X = 20, positioning the playhead at X = 6 will still result in closest_point_x = 20 and previous_point_x = 5. So it's really the "next point x" whenever there is a next point, whether or not it's the closest.

@rexdk
Copy link

rexdk commented Nov 19, 2020

I wonder if we should disable/hide interpolation mode once
self.frame_number > ...let's call it last_point_x?

I wondered if there was anything else that would set the last point when the playhead is past it. I can't find it, but managed to generate a few more keyframes for different properties at the end. So now changing the interpolation type near the end affects a keyframe several back. That's not great, and I'm inclined to agree we should hide interpolation setting here as you say.

There's still the situation where we adjust location-x interp type between 2 location-x keyframes say, but there are other location-y say keyframes in between. But that's logical enough I think.

While on the topic, a keyframe at the very start shows an interpolation type, but that's not used of course. It can actually be set. Same thing for your case where a keyframe B is set in the middle somewhere, and the keyframe at the start is deleted. Should we
a) hide the interp type setting for the first keyframe?
b) hide the interp type icon for the first keyframe?

@ferdnyc
Copy link
Contributor Author

ferdnyc commented Nov 19, 2020

@rexdk

I wonder if we should disable/hide interpolation mode once
self.frame_number > ...let's call it last_point_x?

I wondered if there was anything else that would set the last point when the playhead is past it. I can't find it, but managed to generate a few more keyframes for different properties at the end. So now changing the interpolation type near the end affects a keyframe several back. That's not great, and I'm inclined to agree we should hide interpolation setting here as you say.

I'll have to look into that, TBH I'm not sure if we can. It depends if that part of the code has access to both the playhead position and the X location of the last keyframe point for the property. If so, we can check for that condition no problem. But if it isn't easily detected, then we may have to live with how things are currently.

There's still the situation where we adjust location-x interp type between 2 location-x keyframes say, but there are other location-y say keyframes in between. But that's logical enough I think.

Well, the thing to keep in mind there, though it's admittedly a behind-the-scenes sort of detail, is that keyframes for any other property aren't really "in between", since each property's keyframes are kept on a separate list. In terms of how they're displayed on the Timeline, all of the lists are merged together, but each property only has access to its own keyframes. Any others, it doesn't even know are there.

That's an area where the active-property stuff I mentioned in #3823 (comment) might help, since it would differentiate between keyframe points that belong to one property and any other points that belong to other properties. ...It might help, or OTOH maybe it would just make things even more confusing for the users, because they'd have to keep changing their property selection when viewing / seeking to / modifying keyframe points.

While on the topic, a keyframe at the very start shows an interpolation type, but that's not used of course. It can actually be set. Same thing for your case where a keyframe B is set in the middle somewhere, and the keyframe at the start is deleted. Should we
a) hide the interp type setting for the first keyframe?
b) hide the interp type icon for the first keyframe?

Mmm, in both of those cases, the interpolation type shouldn't show unless there are more points for that property. Like I said, we special-case for properties that only have one point, and already hide the interpolation menu choices and icon. That's true even if the single point is somewhere other than X=1 — as soon as you delete all of the other points for that property (including X=1), the property should revert to showing no icon, and the menu options should be hidden. They won't reappear until you add a second point for that property.

When there are two or more points, it's true that the first frame shows interpolation options, but as of the new code in this PR you won't be able to set the interpolation mode — if you try, you'll trigger the new check I added that will abort set_interpolation if previous_point_x == closest_point_x, which it always does for X=1. So attempting to make changes there will just have no effect.

I suppose we could see about checking when the playhead is at the first frame, and special-case the view so it doesn't display the icon or the interpolation menu options. I'm just a little worried that might be confusing.

Of course, this is only a problem because of the bugfix. In the old code, the interpolation mode at X=1 would show the interpolation mode for the first curve segment because of the N - 1 interpolation mode bug. Whatever the second keyframe was set to, that mode would've also bled over to X = 1.

I kind of wish that's what it did show for the first frame, since the interpolation mode of the first curve is really the only one that matters from that point — it's how all of the values to the right of that frame will be computed, which is all you care about in terms of interpolation. The interpolation mode set or not set on the point X=1 itself only has relevance when you start computing values for points left of that point, and there aren't and can never be any of those.

@rexdk
Copy link

rexdk commented Nov 19, 2020

I suppose we could see about checking when the playhead is at the first frame, and special-case the view so it doesn't display the icon or the interpolation menu options. I'm just a little worried that might be confusing.

Ah, but it's not exactly clear at the moment :) . I actually think it's OK if it doesn't show at x=1, but then I'm comfortable with having the curve set at the second keyframe. Another option would be to partially reverse what you've just been correcting, and copy the interp type only if previous_point is at x=1. Not sure I like it much, but you might :)

That's an area where the active-property stuff I mentioned in #3823 (comment) might help, since it would differentiate between keyframe points that belong to one property and any other points that belong to other properties. ...It might help, or OTOH maybe it would just make things even more confusing for the users, because they'd have to keep changing their property selection when viewing / seeking to / modifying keyframe points.

There's certainly scope for confusion - they might want to focus on both locaton x and location y for example if they're moving a clip, so they probably want "active properties" (plural). How on earth do you activate that without too many options? Well possibly by allowing them to tick mutliple properties I guess. But it's getting complex - and most users wil only have a minimum number of keyframes, if any. Anyone with a lot of keyframes is probably a power user, and can cope.

P.S> "closest_point_x" is somewhat of a misnomer,

Arf - yes indeed. Thanks for all the explanations - very helpful

@ferdnyc ferdnyc mentioned this pull request Nov 25, 2020
@github-actions
Copy link

Merge conflicts have been detected on this PR, please resolve.

@github-actions github-actions bot added the conflicts A PR with unresolved merge conflicts label Jan 29, 2021
@github-actions github-actions bot removed the conflicts A PR with unresolved merge conflicts label Mar 31, 2021
@github-actions github-actions bot added the conflicts A PR with unresolved merge conflicts label May 5, 2021
@github-actions
Copy link

github-actions bot commented May 5, 2021

Merge conflicts have been detected on this PR, please resolve.

@ferdnyc ferdnyc mentioned this pull request May 19, 2021
@github-actions github-actions bot removed the conflicts A PR with unresolved merge conflicts label May 21, 2021
@github-actions
Copy link

github-actions bot commented Jun 1, 2021

Merge conflicts have been detected on this PR, please resolve.

@github-actions github-actions bot added the conflicts A PR with unresolved merge conflicts label Jun 1, 2021
- PropertiesModel.value_updated() was an overloaded method that
  performed all of the following functions:
  * Inserting new keyframe points
  * Updating existing keyframe points
  * Updating NON-keyframe property values
  * Changing interpolation types for existing keyframes
  As a result, it was a confusing ramble of conditional logic.
- Yet, color properties (specifically, and only) were handled by a
  separate (but largely cut-and-paste identical) color_updated()
  method.
- Broke logic apart into separate, purpose-focused methods:
  * update_value() to set/modify properties
  * set_point() to create/modify single keyframe points
  * update_interpolation() to apply keyframe interpolation types
  * set_interpolation() to adjust interpolation values for single
    keyframe segments
  * item_changed(), the slot that processes the itemChanged signal
    from the properties view
  * interpret_as_type(), to convert model data into project data
- All of the above handles both color and non-color properties, of
  all supported data types
- A rewritten remove_keyframe() supplements and complements the
  new methods listed above
- Rename 'curves' list argument to 'handles'
- Bail (early return) for special case where prev_x == closest_x
- Don't use mutable types (lists) as default arguments
- Don't place keyword args before *args
@github-actions github-actions bot removed the conflicts A PR with unresolved merge conflicts label Jul 6, 2021
@github-actions github-actions bot added the conflicts A PR with unresolved merge conflicts label Jul 16, 2021
@github-actions
Copy link

Merge conflicts have been detected on this PR, please resolve.

@ferdnyc ferdnyc closed this Jul 7, 2022
@jonoomph jonoomph deleted the keyframe-interp branch November 30, 2022 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
conflicts A PR with unresolved merge conflicts needs testing Issue or PR needs some help testing from the community or other developers project data Issues or PRs involving .OSP project file handling
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Changing keyframe interpolation type incorrectly changes other keyframe interpolation types
2 participants