11

I'm trying to emulate an animation effect in code (almost any language would do as it appears to be math rather than language). Essentially, it is the emulation of a mass-spring system. I've been looking at WPF/Silverlight's ElasticEase and this appears to be pretty close to what I'm looking for, but not quite.

First of all, here's what I'm looking for - an object, travelling a certain number of seconds, hitting a location and immediately slowing down to ocsillate for a certain number of seconds to rest at the same point where damping was applied. So to visualize this, let's say I have a 600w/900h canvas and I have an square that begins to animate from 900px to 150px in a TranslateTransform.Y. It takes 4 seconds to reach 150px height (187.5px per second), at which stage it immediated gets damped and only travels about 35px more for 0.4 seconds (87.5px per second) to 115px height, then rebounds down for 1 second to 163px height (48px and 48px per second) and then rebounds back up to 146px (17px and 17px per second) and so on until the ocillations slow it to its final resting place of 150px. The ocillation period is 16 seconds.

The example I described above is the top left blue rectangle here: enter image description here

Here's what I will know in advance - the pixel distance and number of seconds it takes to get from point A to point B, the number of seconds for ocillation. Things like mass don't seem to matter.

I've tried ElasticEase and the issue seems to be that I can't get the object to travel with no easing for 4 seconds and then "bounce" for the next 16 seconds. The .Springiness is also always way too much, even if I set it to be a really high number like 20.

ILSpy show's its function as:

protected override double EaseInCore(double normalizedTime)
        {
            double num = Math.Max(0.0, (double)this.Oscillations);
            double num2 = Math.Max(0.0, this.Springiness);
            double num3;
            if (DoubleUtil.IsZero(num2))
            {
                num3 = normalizedTime;
            }
            else
            {
                num3 = (Math.Exp(num2 * normalizedTime) - 1.0) / (Math.Exp(num2) - 1.0);
            }
            return num3 * Math.Sin((6.2831853071795862 * num + 1.5707963267948966) * normalizedTime);
        }

I've included 2 videos and and an Excel file in a zipped folder on DropBox. I guess this question will be more of a work-in-progress as folks ask more clarifying questions.

(DISCLAIMER: I don't know what I'm talking about when it comes to much of this stuff)

Todd Main
  • 28,951
  • 11
  • 82
  • 146
  • Your problem is (as you suspect) a math/physics one. It is a standard first physics course damped oscillations problem, but it will take a few hundred words (and some math formulas, which render pretty bad in SO) to explain fully. The basic problem is that the damping is exponential, so when you discretize it, the tail oscillations are lost. Are you sure you want a full explanation instead of just http://en.wikipedia.org/wiki/Damping? :) – Dr. belisarius Sep 13 '11 at 05:36
  • Thanks @belisarius. The math/physics is really beyond me with this one, especially considering the sample above doesn't seem to take mass into account. Any explaination and/or code samples that you could provide that may help me get started with this with the known variables I have before me would be most helpful. – Todd Main Sep 13 '11 at 06:03
  • I'll try, but as I foresee it will take me a lot of time to do it well, perhaps some physicist/student chasing a bounty could anticipate me :). BTW, the mass IS there, but all the other constants of the problem (elastic constant & damping coeff) are divided by the mass to get a mass independent problem (as the F-m*a == 0 is equaled to zero, you can divide all the terms by whatever constant you want). The F is the composition of two forces, the elastic one, proportional to the elastic constant k and the position, and the damping force, proportional to the velocity and a damping constant. – Dr. belisarius Sep 13 '11 at 06:18
  • Is using a custom subclass of EasingFunctionBase acceptable? – Peter Taylor Sep 13 '11 at 07:41
  • Do you want to understand the physics and model the behaviour physically correct or do you just want your squares (or whatever) to oscillate a little bit? – Marc Sep 13 '11 at 13:21
  • The physics of motion without force considerations is known as "kinematics". http://en.wikipedia.org/wiki/Kinematics – tom10 Sep 13 '11 at 16:29
  • @Peter Taylor - a custom EasingFunction would be ideal, if possible. – Todd Main Sep 13 '11 at 18:36
  • @Marc: I actually just want a way for the squares to oscillate similar to or exactly as above. – Todd Main Sep 13 '11 at 18:37

2 Answers2

9

Skip the physics and just go straight to the equation.

parameters: “Here's what I will know in advance - the pixel distance [D] and number of seconds [T0] it takes to get from point A to point B, the number of seconds for oscillation [T1].” Also, I'll add as free parameters: the maximum size of oscillation, Amax, the damping time constant, Tc, and a frame rate, Rf, that is, at what times does one want a new position value. I assume you don't want to calculate this forever, so I'll just do 10 seconds, Ttotal, but there are a variety of reasonable stop conditions...

code: Here's the code (in Python). The main thing is the equation, found in def Y(t):

from numpy import pi, arange, sin, exp

Ystart, D = 900., 900.-150.  # all time units in seconds, distance in pixels, Rf in frames/second
T0, T1, Tc, Amax, Rf, Ttotal = 5., 2., 2., 90., 30., 10. 

A0 = Amax*(D/T0)*(4./(900-150))  # basically a momentum... scales the size of the oscillation with the speed 

def Y(t):
    if t<T0:  # linear part
        y = Ystart-(D/T0)*t
    else:  # decaying oscillations
        y = Ystart-D-A0*sin((2*pi/T1)*(t-T0))*exp(-abs(T0-t)/Tc)
    return y

y_result = []
for t in arange(0, Ttotal, 1./Rf):  # or one could do "for i in range(int(Ttotal*Rf))" to stick with ints    
    y = Y(t)
    y_result.append(y)

The idea is linear motion up to the point, followed by a decaying oscillation. The oscillation is provided by the sin and the decay by multiplying it by the exp. Of course, change the parameters to get any distance, oscillation size, etc, that you want.

enter image description here

notes:

  1. Most people in the comments are suggesting physics approaches. I didn't use these because if one specifies a certain motion, it is a bit over-doing-it to start with the physics, go to the differential equations, and then calculate the motion, and tweak the parameters to get the final thing. Might as well just go right to the final thing. Unless, that is, one has an intuition for the physics that they want to work from.
  2. Often in problems like this one wants to keep a continuous speed (first derivative), but you say “immediately slows down”, so I didn't do that here.
  3. Note that the period and amplitude of the oscillation won't be exactly as specified when the damping is applied, but that's probably more detailed than you care about.
  4. If you need to express this as a single equation, you can do so using a “Heaviside function”, to turn the contributions on and off.

At the risk of making this too long, I realized I could make a gif in GIMP, so this is what it looks like:

enter image description here

I can post the full code to make the plots if there's interest, but basically I'm just calling Y with different D and T0 values for each timestep. If I were to do this again, I could increase the damping (i.e., decrease Tc), but it's a bit of a hassle so I'm leaving it as is.

tom10
  • 67,082
  • 10
  • 127
  • 137
  • Agreed, you actually don't need mass. Having speed and acceleration is enough. Found that out when doing a spring-graph layout. The naive way would be to do full physic simulation with speed, mass, force etc. Then turns out you're only doing double work with annoying side-effects. – gjvdkamp Sep 13 '11 at 18:33
  • What a brilliant explanation!! Thank you! I'm going to work with this over the next day or two to see if I can get it to work. – Todd Main Sep 13 '11 at 18:37
  • @Otaku Seems the derivative at D is not continuous. You may perceive an abrupt velocity change when the body reaches the resting position for the first time – Dr. belisarius Sep 13 '11 at 22:54
  • It would be easy to fix the abrupt velocity change, but as I say in note in 2, I think it's what the OP wanted, and it also looks to me like there's an abrupt change in the demo as well. Anyway, Otaku, let me know if you want a continuous velocity change since it's very easy to include in the equation. – tom10 Sep 14 '11 at 00:41
  • @Otaku: When you try this out, hopefully it's clear from my answer, but it's worth noting that the main two parameters to change are: D, which will vary *within* a constant color column, and T0, which will vary *between* the columns of your demo. – tom10 Sep 14 '11 at 00:43
  • Great graphic addition! I used PowerPoint->Camtasia->VirtualDub->Camtasia and exported as animated .gif. – Todd Main Sep 14 '11 at 04:40
  • I'm glad to hear that it's working for you. Please post a comment if anything is unclear, like which parameters to set for which effect, etc. – tom10 Sep 18 '11 at 00:03
  • Will do. The bounty was running out, so I wanted to make sure you got it. I will have a few minor questions. – Todd Main Sep 18 '11 at 15:14
5

I was thinking along the same lines as @tom10. (I also considered an IEasingFunction which took an IList<IEasingFunction>, but it would be tricky to hack the desired behaviour out of the existing ones).

// Based on the example at
// http://msdn.microsoft.com/en-us/library/system.windows.media.animation.easingfunctionbase.aspx
namespace Org.CheddarMonk
{
    public class OtakuEasingFunction : EasingFunctionBase
    {
        // The time proportion at which the cutoff from linear movement to
        // bounce occurs. E.g. for a 4 second movement followed by a 16
        // second bounce this would be 4 / (4 + 16) = 0.2.
        private double _CutoffPoint;
        public double CutoffPoint {
            get { return _CutoffPoint; }
            set {
                if (value <= 0 || value => 1 || double.IsNaN(value)) {
                    throw new ArgumentException();
                }
                _CutoffPoint = value;
            }
        }

        // The size of the initial bounce envelope, as a proportion of the
        // animation distance. E.g. if the animation moves from 900 to 150
        // and you want the maximum bounce to be no more than 35 you would
        // set this to 35 / (900 - 150) ~= 0.0467.
        private double _EnvelopeHeight;
        public double EnvelopeHeight {
            get { return _EnvelopeHeight; }
            set {
                if (value <= 0 || double.IsNaN(value)) {
                    throw new ArgumentException();
                }
                _EnvelopeHeight = value;
            }
        }

        // A parameter controlling how fast the bounce height should decay.
        // The higher the decay, the sooner the bounce becomes negligible.
        private double _EnvelopeDecay;
        public double EnvelopeDecay {
            get { return _EnvelopeDecay; }
            set {
                if (value <= 0 || double.IsNaN(value)) {
                    throw new ArgumentException();
                }
                _EnvelopeDecay = value;
            }
        }

        // The number of half-bounces.
        private int _Oscillations;
        public int Oscillations {
            get { return _Oscillations; }
            set {
                if (value <= 0) {
                    throw new ArgumentException();
                }
                _Oscillations = value;
            }
        }

        public OtakuEasingFunction() {
            // Sensible default values.
            CutoffPoint = 0.7;
            EnvelopeHeight = 0.3;
            EnvelopeDecay = 1;
            Oscillations = 3;
        }

        protected override double EaseInCore(double normalizedTime) {
            // If we get an out-of-bounds value, be nice.
            if (normalizedTime < 0) return 0;
            if (normalizedTime > 1) return 1;

            if (normalizedTime < _CutoffPoint) {
                return normalizedTime / _CutoffPoint;
            }

            // Renormalise the time.
            double t = (normalizedTime - _CutoffPoint) / (1 - _CutoffPoint);
            double envelope = EnvelopeHeight * Math.Exp(-t * EnvelopeDecay);
            double bounce = Math.Sin(t * Oscillations * Math.PI);
            return envelope * bounce;
        }

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

This is untested code, but it shouldn't be too bad to debug if there are problems. I'm not sure what attributes (if any) need to be added to the properties for the XAML editor to handle them properly.

Peter Taylor
  • 4,918
  • 1
  • 34
  • 59
  • Wow Peter! This is amazing! I'll be trying this out over the next day or two and let you know. – Todd Main Sep 14 '11 at 03:11
  • Peter, what you provided is great, equally as good as tom's. I'll open another bounty on this and also give you the points in 2 days' time. – Todd Main Sep 17 '11 at 21:42