Smooth animation using MatrixTransform?

2020-07-17 15:38发布

问题:

I'm trying to do an Matrix animation where I both scale and transpose a canvas at the same time. The only approach I found was using a MatrixTransform and MatrixAnimationUsingKeyFrames. Since there doesnt seem to be any interpolation for matrices built in (only for path/rotate) it seems the only choice is to try and build the interpolation and DiscreteMatrixKeyFrame's yourself.

I did a basic implementation of this but it isnt exactly smooth and I'm not sure if this is the best way and how to handle framerates etc. Anyone have suggestions for improvement? Here's the code:

        MatrixAnimationUsingKeyFrames anim = new MatrixAnimationUsingKeyFrames();
        int duration = 1;
        anim.KeyFrames = Interpolate(new Point(0, 0), centerPoint, 1, factor,100,duration);
        this.matrixTransform.BeginAnimation(MatrixTransform.MatrixProperty, anim,HandoffBehavior.Compose);


public MatrixKeyFrameCollection Interpolate(Point startPoint, Point endPoint, double startScale, double endScale, double framerate,double duration)
    {
        MatrixKeyFrameCollection keyframes = new MatrixKeyFrameCollection();

        double steps = duration * framerate;
        double milliSeconds = 1000 / framerate;
        double timeCounter = 0;



        double diffX = Math.Abs(startPoint.X-  endPoint.X);
        double xStep = diffX / steps;

        double diffY = Math.Abs(startPoint.Y - endPoint.Y);
        double yStep = diffY / steps;

        double diffScale= Math.Abs(startScale- endScale);
        double scaleStep = diffScale / steps;


        if (endPoint.Y < startPoint.Y)
        {
            yStep =  -yStep;
        }

        if (endPoint.X < startPoint.X)
        {
            xStep =  -xStep;
        }


        if (endScale < startScale)
        {
            scaleStep =  -scaleStep;
        }


        Point currentPoint = new Point();
        double currentScale = startScale;

        for (int i = 0; i < steps; i++)
        {
            keyframes.Add(new DiscreteMatrixKeyFrame(new Matrix(currentScale, 0, 0, currentScale, currentPoint.X, currentPoint.Y), KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(timeCounter))));
            currentPoint.X += xStep;
            currentPoint.Y += yStep;
            currentScale += scaleStep;
            timeCounter += milliSeconds;

        }

        keyframes.Add(new DiscreteMatrixKeyFrame(new Matrix(endScale, 0, 0, endScale, endPoint.X, endPoint.Y), KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(0))));

        return keyframes;

    }

回答1:

Try this! As long as you aren't rotating/shearing it will do the trick.

using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace MapControl
{
    public class LinearMatrixAnimation : AnimationTimeline
    {

        public Matrix? From
        {
            set { SetValue(FromProperty, value);}
            get { return (Matrix)GetValue(FromProperty); }
        }
        public static DependencyProperty FromProperty = DependencyProperty.Register("From", typeof(Matrix?), typeof(LinearMatrixAnimation), new PropertyMetadata(null));

        public Matrix? To
        {
            set { SetValue(ToProperty, value); }
            get { return (Matrix)GetValue(ToProperty); }
        }
        public static DependencyProperty ToProperty = DependencyProperty.Register("To", typeof(Matrix?), typeof(LinearMatrixAnimation), new PropertyMetadata(null));

        public LinearMatrixAnimation()
        {            
        }

        public LinearMatrixAnimation(Matrix from, Matrix to, Duration duration)
        {
            Duration = duration;
            From = from;
            To = to;
        }

        public override object GetCurrentValue(object defaultOriginValue, object defaultDestinationValue, AnimationClock animationClock)
        {
            if (animationClock.CurrentProgress == null)
            {
                return null;
            }

            double progress = animationClock.CurrentProgress.Value;
            Matrix from = From ?? (Matrix)defaultOriginValue;

            if (To.HasValue)
            {
                Matrix to = To.Value;
                Matrix newMatrix = new Matrix(((to.M11 - from.M11) * progress)+from.M11, 0, 0, ((to.M22 - from.M22) * progress)+from.M22,
                                              ((to.OffsetX - from.OffsetX) * progress) + from.OffsetX, ((to.OffsetY - from.OffsetY) * progress)+ from.OffsetY);
                return newMatrix;
            }

            return Matrix.Identity;
        }

        protected override System.Windows.Freezable CreateInstanceCore()
        {
            return new LinearMatrixAnimation();
        }

        public override System.Type  TargetPropertyType
        {
            get { return typeof(Matrix); }
        }
    }
}


回答2:

I have implemented MatrixAnimation class which supports smooth translation, scaling and rotation animations. It also supports easing functions! Find it here



回答3:

Well, if you ask that in MSDN

http://msdn.microsoft.com/en-us/library/system.windows.media.animation.discretematrixkeyframe.aspx

you are getting the answer that DiscreteMatrixKeyFrame causes abrupt changes and that you should use LinearDoubleKeyFrame or SplineDoubleKeyFrame together with source code !

EDIT: Ah, I see, Matrix transformations supports only discrete transformations, so you have in fact a problem with jumps. So what I propose is using a RectAnimationUsingKeyFrames

// Create a RectAnimationUsingKeyFrames to
// animate the RectangleGeometry.
RectAnimationUsingKeyFrames rectAnimation = new RectAnimationUsingKeyFrames();
rectAnimation.Duration = TimeSpan.FromSeconds(timeInSeconds);

// Animate position, width, and height in first 2 seconds. LinearRectKeyFrame creates
// a smooth, linear animation between values.
rectAnimation.KeyFrames.Add(
                new LinearRectKeyFrame(
                    new Rect(600,50,200,50), // Target value (KeyValue)
                    KeyTime.FromTimeSpan(TimeSpan.FromSeconds(2))) // KeyTime
                );

// In the next half second, change height to 10. 
   rectAnimation.KeyFrames.Add(
                new LinearRectKeyFrame(
                    new Rect(600, 50, 200, 10), // Target value (KeyValue)
                    KeyTime.FromTimeSpan(TimeSpan.FromSeconds(2.5))) // KeyTime
                );

Simply use a Linear or SplineRectKeyFrame, set the duration/Keytime and the values you need. To get the scale, you need to compute the end width/height and set it, but that shouldn't be a problem.



回答4:

I like @LukeN answer. Works nice for simple translate/scale animations.
I added easing to this code (hand made though, not WPF native easing).

private double Sigmoid(double v)
{
    double t = -6 + (v * 12.0);
    return 1.0 / (1.0 + Math.Exp(-t));
}

private double EaseIn(double v)
{
    return 2.0 * Sigmoid(v/2.0);
}

private double EaseOut(double v)
{
    return 2.0 * ( Sigmoid(0.5 + v/2.0) - 0.5);
}

And then in GetCurrentValue do progress = Sigmoid(progress) or EaseIn(progress)...



回答5:

In case of @pwlodek link will not reachable in the future and to not forget his awesome class, I copied into SO:

//http://pwlodek.blogspot.com/2010/12/matrixanimation-for-wpf.html
public class MatrixAnimation : MatrixAnimationBase
{
    public Matrix? From
    {
        set { SetValue(FromProperty, value); }
        get { return (Matrix)GetValue(FromProperty); }
    }

    public static DependencyProperty FromProperty =
        DependencyProperty.Register("From", typeof(Matrix?), typeof(MatrixAnimation),
            new PropertyMetadata(null));

    public Matrix? To
    {
        set { SetValue(ToProperty, value); }
        get { return (Matrix)GetValue(ToProperty); }
    }

    public static DependencyProperty ToProperty =
        DependencyProperty.Register("To", typeof(Matrix?), typeof(MatrixAnimation),
            new PropertyMetadata(null));

    public IEasingFunction EasingFunction
    {
        get { return (IEasingFunction)GetValue(EasingFunctionProperty); }
        set { SetValue(EasingFunctionProperty, value); }
    }

    public static readonly DependencyProperty EasingFunctionProperty =
        DependencyProperty.Register("EasingFunction", typeof(IEasingFunction), typeof(MatrixAnimation),
            new UIPropertyMetadata(null));

    public MatrixAnimation()
    {
    }

    public MatrixAnimation(Matrix toValue, Duration duration)
    {
        To = toValue;
        Duration = duration;
    }

    public MatrixAnimation(Matrix toValue, Duration duration, FillBehavior fillBehavior)
    {
        To = toValue;
        Duration = duration;
        FillBehavior = fillBehavior;
    }

    public MatrixAnimation(Matrix fromValue, Matrix toValue, Duration duration)
    {
        From = fromValue;
        To = toValue;
        Duration = duration;
    }

    public MatrixAnimation(Matrix fromValue, Matrix toValue, Duration duration, FillBehavior fillBehavior)
    {
        From = fromValue;
        To = toValue;
        Duration = duration;
        FillBehavior = fillBehavior;
    }

    protected override Freezable CreateInstanceCore()
    {
        return new MatrixAnimation();
    }

    protected override Matrix GetCurrentValueCore(Matrix defaultOriginValue, Matrix defaultDestinationValue, AnimationClock animationClock)
    {
        if (animationClock.CurrentProgress == null)
        {
            return Matrix.Identity;
        }

        var normalizedTime = animationClock.CurrentProgress.Value;
        if (EasingFunction != null)
        {
            normalizedTime = EasingFunction.Ease(normalizedTime);
        }

        var from = From ?? defaultOriginValue;
        var to = To ?? defaultDestinationValue;

        var newMatrix = new Matrix(
                ((to.M11 - from.M11) * normalizedTime) + from.M11,
                ((to.M12 - from.M12) * normalizedTime) + from.M12,
                ((to.M21 - from.M21) * normalizedTime) + from.M21,
                ((to.M22 - from.M22) * normalizedTime) + from.M22,
                ((to.OffsetX - from.OffsetX) * normalizedTime) + from.OffsetX,
                ((to.OffsetY - from.OffsetY) * normalizedTime) + from.OffsetY);

        return newMatrix;
    }
}

Sample usage for code behind:

    protected void AnimateMatrix(MatrixTransform matrixTransform, Matrix toMatrix, TimeSpan duration)
    {
        if (matrixTransform is MatrixTransform mt && toMatrix is Matrix to && duration is TimeSpan ts)
            AnimateMatrix(mt, mt.Matrix, to, duration);
    }
    protected void AnimateMatrix(MatrixTransform matrixTransform, Matrix? fromMatrix, Matrix? toMatrix, TimeSpan duration)
    {
        if (matrixTransform is MatrixTransform mt)
            mt.BeginAnimation(MatrixTransform.MatrixProperty, GetMatrixAnimation(fromMatrix, toMatrix, duration));
    }
    private MatrixAnimation GetMatrixAnimation(Matrix? fromMatrix, Matrix? toMatrix, TimeSpan duration)
    {
        return new MatrixAnimation(fromMatrix ?? Matrix.Identity, toMatrix ?? Matrix.Identity, new Duration(duration));
    }


回答6:

One way I can think of is to convert the Matrix to a TransformGroup which contains ScaleTransform, RotateTransform, and a TranslateTransform, then animate these using normal animations, then once the animation is complete, create the Matrix back again from the values in each of the Transforms?