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

Proposal: Provide new dependency property to re-use a previous animation's progress in duration calculation #10041

Open
SetTrend opened this issue Nov 7, 2024 · 4 comments
Labels
API suggestion Early API idea and discussion, it is NOT ready for implementation

Comments

@SetTrend
Copy link

SetTrend commented Nov 7, 2024

Current Situation

We all know the effect that if an animation is replacing an ongoing animation, the duration time may result in unwanted sluggish animation if the previous animation has just started:

Use previous animation progress in duration

Desired Situation

I'd like to propose a new property to System.Windows.Media.Animation.Timeline. A property that's considering the progress amount of a previously running animation when calculating the first iteration's duration of the subsequent animation.


The calculation is simple:

Just multiply the new, subsequent animation's first iteration's duration with the progress amount of the currently running animation that's being assigned to the same target property of the same target object if no From value is provided in the subsequent animation:

// pseudo code
void OnAnimationIterationStart(Timeline animation)
{
  _iterationDuration =
    !animation.From && RunningStoryboard(sBoard.Target, sBoard.TargetProperty) as Storyboard prevStoryboard
      ? Duration * prevStoryboard.GetCurrentProgress()
      : Duration
      ;
}

The above implementation may even be used for the animation itself when it is repeating: If called at the beginning of a repetition cycle (i.e., "iteration"), prevStoryboard would be the current Storyboard itself and GetCurrentProgress() would return 1.


Proposed Property

Let's call the proposed new property AccelerateByPreviousProperty.

The proposed new dependency property, AccelerateByPrevious, should specify how much of a previous animation is used in the calculation: from none to full consideration.

So, I suggest AccelerateByPreviousProperty to be a double value in the range [0, 1], with 1 being the default (or 0 being the default for backward compatibility).

This would change above calculation from:

Duration * prevStoryboard.GetCurrentProgress()

… to:

Duration * ((1 - AccelerateByPrevious) + prevStoryboard.GetCurrentProgress() * AccelerateByPrevious)

Provided, prevStoryboard.GetCurrentProgress() would be in the range [0, 1] and AccelerateByPrevious would also be in the range [0, 1], the multiplication factor for the Duration calculation would result in:

Previous Progress AccelerateByPrevious Resulting Duration Factor
[0, 1] 0 [1, 1]
[0, 1] 0.5 [.5, 1]
[0, 1] 1 [0, 1]
@lindexi lindexi added API suggestion Early API idea and discussion, it is NOT ready for implementation and removed Untriaged labels Nov 8, 2024
@lindexi
Copy link
Member

lindexi commented Nov 8, 2024

That seems like a good idea.

@MichaeIDietrich
Copy link
Contributor

This is indeed a common limitation in WPF and yes, it usually takes some lines of code to work around this.

If I understand you correctly your proposed property (AccelerateByPrevious) would be used in code behind rather than XAML to adjust the duration of the animation before being started?

While this is probably useful, I think it would be great if this could be easily used in XAML.
So, there is already an IsAdditive property that adds the current value to the animation value, in the same manner we could also have a way to affect the animation duration.

In my opinion it would be great, if there was a property called something like InterpolateDuration that would adjust the duration of an animation depending on the chosen enum value.
There could be an option called ByPreviousAnimation that would adjust the duration in relation of the progress of the previous animation as suggested with AccelerateByPrevious.
There could another option called ByCurrentValue that simply calculates the factor by something like (To - From / Current - From) or so.
And None (the default) to not adjust the duration.

I played a bit that idea and tried to accomplish that behavior somehow with existing functionality in WPF. I took your example code and utilized IsAdditive together with SpeedRatio and came to this result:

<Grid>
    <Button Content="Test" Margin="50" RenderTransformOrigin="0.5,0.5">
        <Button.Resources>
            <local:MathConverter x:Key="MathConverter" Min="1" />
        </Button.Resources>
        <Button.RenderTransform>
            <RotateTransform x:Name="RT" />
        </Button.RenderTransform>
        <Button.Triggers>
            <EventTrigger RoutedEvent="Control.MouseEnter">
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetProperty="RenderTransform.Angle" From="0" To="360" IsAdditive="True" RepeatBehavior="Forever" Duration="0:0:2" />
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
            <EventTrigger RoutedEvent="Control.MouseLeave">
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation SpeedRatio="{Binding ElementName=RT, Path=Angle, Converter={StaticResource MathConverter}, ConverterParameter=360/x}" Storyboard.TargetProperty="RenderTransform.Angle" To="0" Duration="0:0:2" />
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Button.Triggers>
    </Button>
</Grid>

And a converter for calculating the speed ratio from the current angle (360 / angle):

public class MathConverter : IValueConverter
{
    public double Min { get; set; } = double.MinValue;

    public double Max { get; set; } = double.MaxValue;

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var x = System.Convert.ToDouble(value);
        var expression = parameter.ToString()!.Replace("x", x.ToString(CultureInfo.InvariantCulture));
        var result = System.Convert.ToDouble(new DataTable().Compute(expression, ""));

        return Math.Clamp(result, Min, Max);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
RotatingButton.2024-11-08.210701.mp4

The solution is not perfect, but maybe good enough. 😋

@miloush
Copy link
Contributor

miloush commented Nov 8, 2024

I am certainly not a fan of the name, 'offset' would fit better than 'accelerate' (also we have e.g. in color gradients), and previous is also not very descriptive.

The storyboard could find out the source of current value is animation and adjust accordingly automatically. However, I am not sure that is a good direction anyway, because the angle can be set by several other sources, so relying on there being a previous storyboard seems fragile. Also there is acceleration and deceleration ratios and I am not sure how well these would play together.

Let's focus on the intent instead, wouldn't having something like <ReverseStoryboard /> solve the problem?

@SetTrend
Copy link
Author

SetTrend commented Nov 9, 2024

@MichaeIDietrich: Actually, the proposal was considering a dependency property. I.e., this is supposed to be used in XAML, of course:

Image
(cannot highlight code here, hence the image)

@miloush: The name is just a basis for discussion; a token in order to avoid referring to it using half a sentence instead. – If it's called foo in the end, I wouldn't mind 😉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
API suggestion Early API idea and discussion, it is NOT ready for implementation
Projects
None yet
Development

No branches or pull requests

4 participants