dotnet / wpf

WPF is a .NET Core UI framework for building Windows desktop applications.
MIT License
7.11k stars 1.17k forks source link

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

Open SetTrend opened 3 weeks ago

SetTrend commented 3 weeks ago

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 commented 3 weeks ago

That seems like a good idea.

MichaeIDietrich commented 3 weeks ago

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();
    }
}

https://github.com/user-attachments/assets/5cc0d8d2-ba80-4378-9ec6-5de2ac4e7278

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

miloush commented 3 weeks ago

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 commented 3 weeks ago

@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 😉