Is there a formula to translate AccelerationRatio or DecelerationRatio to Bezier control points for use in a KeySpline
in a SplineDoubleKeyFrame
? For example, an "Ease Out" may be DecelerationRatio=0.5
, but this doesn't seem equivalent to KeySpline="0.0,0.0 0.5,1.0"
or KeySpline="0.5,0 1,0.5"
.
Would this involve multiple SplineDoubleKeyFrame
to achieve DecelerationRatio=0.5
? Or is their a particular formula that makes them equivalent in a single frame?
Or is this not to be achieved via a SplineDoubleKeyFrame
but a EasingDoubleKeyFrame
instead (if so, what is the EasingFunction/EasingMode/Other Attributes)?
Essentially, I'm trying to achieve <DoubleAnimation Storyboard.TargetName="deceleratedRectangle" Storyboard.TargetProperty="(Rectangle.Width)" DecelerationRatio="0.5" Duration="0:0:10" From="20" To="400" />
with KeyFrames as there will be multiple frames that target the same property path and accelerate/decelerate it.
UPDATE: According to the Microsoft WPF-Silverlight Comparison Whitepaper.pdf on page 7:
The linear interpolation can be modified somewhat by adding AccelerationRatio and DecelerationRatio properties to the animation. These attributes essentially create three linear interpolations for the entire animation in order to modify the starting and stopping speeds. For example, a designer would use these attributes to have an object gradually pick up speed or stop suddenly. Unfortunately, Silverlight does not implement these two attributes, but the effect can be duplicated using keyframe animations with linear interpolation.
So I guess this means it can be done with just 3 key frames, but what the formula is I have no idea.
SOLUTION: For others that may come along that need this, posting the ECMAScript solution crafted by Peter Taylor:
<html>
<head>
<title>Acceleration or deceleration with Bezier splines</title>
<script type="text/javascript"><!--
function calcBezier() {
var res = new Array();
var y0 = +document.getElementById("y0").value;
var y1 = +document.getElementById("y1").value;
var t0 = +document.getElementById("t0").value;
var t1 = +document.getElementById("t1").value;
var ra = +document.getElementById("ra").value;
var rd = +document.getElementById("rd").value;
var ta = t0 + ra * (t1 - t0);
if (ra > 0) res.push("ta = " + ta + "<br />");
var td = t1 - rd * (t1 - t0);
if (rd > 0) res.push("td = " + td + "<br />");
var vm = 2 * (y1 - y0) / (t1 + td - ta - t0);
res.push("vm = " + vm + "<br />");
var ya = y0 + vm * (ta - t0) / 2
if (ra > 0) res.push("ya = " + ya + "<br />");
var yd = y1 - vm * (t1 - td) / 2
if (rd > 0) res.push("yd = " + yd + "<br />");
res.push('<DiscreteDoubleKeyFrame KeyTime="'+t0+'" Value="'+y0+'" /><br />');
if (ra > 0) {
// y - ya = (t - ta) * vm => t = ta + (y - ya) / vm
var p1t = ta - (ya - y0) / vm;
// Scale for spline params: (t0,y0):(0,0) and (ta,ya):(1,1)
p1t = (p1t - t0) / (ta - t0);
// Lift to cubic.
res.push('<SplineDoubleKeyFrame KeyTime="'+ta+'" Value="'+ya+'" KeySpline="'+((2*p1t)/3)+',0 '+((2*p1t+1)/3)+','+(1/3)+'" /><br />');
}
if (ra + rd < 1) {
res.push('<LinearDoubleKeyFrame KeyTime="'+td+'" Value="'+yd+'" /><br />');
}
if (rd > 0) {
var q1y = 1;
var q1t = td + (y1 - yd) / vm;
q1t = (q1t - td) / (t1 - td);
res.push('<SplineDoubleKeyFrame KeyTime="'+t1+'" Value="'+y1+'" KeySpline="'+((2*q1t)/3)+','+(2/3)+' '+((2*q1t+1)/3)+',1" /><br />');
}
document.getElementById("results").innerHTML = res.join("");
}
//-->
</script>
</head>
<body>
<p>Interpolate y from <input id="y0" /> to <input id="y1" />.</p>
<p>Start time: <input id="t0" />; end time: <input id="t1" />.</p>
<p>Acceleration ratio: <input id="ra" />; deceleration ratio: <input id="rd" />.</p>
<p><input type="submit" value="Calculate" onclick="calcBezier();" /></p>
<p id="results"> </p>
</body>
</html>
Update: using the geometric interpretation of calculus as the area under a line, I've worked out how to simplify the derivation a lot.
So we're interpolating from y0 at time t0 to y1 at time t1 with acceleration ratio ra and deceleration ratio rd. The definition of the ratios give us the time at which we stop accelerating, ta = t0 + ra * (t1 - t0), and at which we start decelerating, td = t1 - rd * (t1 - t0).
I understand the documentation you quote to mean that it's constant acceleration from t0 to ta, and constant deceleration from td to t1. We'll take the maximum speed reached to be vm.
Speed | _____________________________________ vm + /| |\ | / \ | / | | \ | / \ | / | | \ | / \ | / | | \ |/ \ +--------+-----------------------------------+--------+---- Time t0 ta td t1
Then the area of the parallelogram is the distance travelled from t0 to t1, which is y1 - y0. The area of a parallelogram is the product of the height with the average of the parallel sides. So
y1 - y0 = vm * ((t1 - t0) + (td - ta)) / 2
or
vm = 2 * (y1 - y0) / (t1 + td - ta - t0)
Using just the area of the triangles at the end, we can find how far we've travelled when we stop accelerating, ya = y(ta), and when we start decelerating, yd = y(td).
ya = y0 + vm * (ta - t0) / 2
yd = y1 - vm * (t1 - td) / 2
Finally we produce a quadratic Bezier for [t0, ta], a straight line (ta, ya) - (td, yd), and a quadratic Bezier for [td, t1].
For the first Bezier, we have the obvious control points P0 = (t0, y0) and P2 = (ta, ya). To find P1 we use the property that P0-P1 is tangent to the curve and P1-P2 is tangent to the curve (in general, for an order-n curve P0-P1 and P(n-1)-Pn are tangent). So P1 is at the intersection of y=y0 and the straight line of the middle segment. Similarly for the other Bezier: Q0 = (td, yd), Q2 = (t1, y1), and Q1 is at the intersection of y=y1 and the straight line of the middle segment.
No fade in (acceleration ratio = 0), deceleration ratio = 0.5, t0 = 0, t1 = 10 (seconds), y0 = 20, y1 = 400. I think this corresponds to your specific question.
ta = 0 (and we can omit the first quadratic Bezier); td = t1 - 0.5 * (t1 - t0) = 5.
vm = 2 * (y1 - y0) / (t1 + td - ta - t0) = 2 * (400 - 20) / (10 + 5 - 0 - 0) = 2 * 380 / 15 = 152 / 3 ~= 50.67.
Ignore ya because we're skipping that Bezier.
yd = y1 - vm * (t1 - td) / 2 = 400 - 152/3 * (10-5)/2 = 400 - 380/3 = 820/3 ~= 273.3
So the straight line goes from (t=0, y=20) to (t=5, y=273.3). The deceleration Bezier has Q0 = (5, 273.3), Q2 = (10, 400).
To find Q1 we extend the straight line to y=400. The line has equation y - 20 = (t - 0) * (273.3 - 20) / (5 - 0), so t = 5 * (400 - 20) / (273.3 - 20) = 7.5.
So we have straight line (0,20)-(5,273.3) and a quadratic Bezier with control points (5,273.3), (7.5,400) and (10,400).
However, there's a slight hitch, which is that Microsoft hasn't deigned to give us quadratic splines. We have to lift the quadratic Q0, Q1, Q2, to the cubic Q0, (Q0 + 2 Q1) /3, (2 Q1 + Q2) / 3, Q2.
We also have to rescale the control points to 0-1. If we apply that rescaling first, we have (0,0)-(0.5,1)-(1,1). So the cubic is (0,0)-(0.333,0.667)-(0.667,1)-(1,1).
I know splines, but not WPF. I think that the following will do what you want:
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="20" /> <LinearDoubleKeyFrame KeyTime="0:0:5" Value="273.333" /> <SplineDoubleKeyFrame KeyTime="0:0:10" Value="400" KeySpline="0.333,0.667,0.667,1"/>
Rescaling the first Bezier, we map (t0, y0) to (0, 0) and (ta, ya) to (1, 1). Therefore we map (P1.t, P1.y) to ((P1.t - t0) / (ta - t0), (P1.y - y0) / (ya - y0)). But P1 is at the intersection of y = y0 with the straight line of gradient vm through (ta, ya), which therefore has equation (y - ya) = (t - ta) * vm. So P1.y = y0 and P1.t = ta + (y0 - ya) / vm = ta - (ya - y0) / vm. Plugging in our identity ya = y0 + vm * (ta - t0) / 2, we get P1.t = ta - (vm * (ta - t0) / 2) / vm = ta - (ta - t0) / 2 = (ta + t0) / 2.
So rescaling we map P1 to (0.5, 0). Therefore when we lift it to a cubic Bezier, the intermediate control points are always at (1/3, 0) and (2/3, 1/3).
Similarly the deceleration spline always has its scaled intermediate points at (1/3, 2/3) and (2/3, 1).
I hope I got your question right.
I tested your example in a WPF project, and I created a similar effect using DoubleAnimationUsingKeyFrames and SplineDoubleKeyFrame. Here is what I did in a Storyboard (this is for deceleration):
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)" Storyboard.TargetName="rectangle">
<SplineDoubleKeyFrame KeyTime="0" Value="1"/>
<SplineDoubleKeyFrame KeyTime="0:0:3" Value="3.45" KeySpline="0,0.52,0.51,1"/>
</DoubleAnimationUsingKeyFrames>
The trick is about setting the Bezier for the SplineDoubleKeyFrame. If you want a smooth acceleration/deceleration, try using a Bezier closer to a linear form.
In addition: the Acceleration/DecelerationRatio is for a whole animation, but the KeySpline is valid for only a KeyFrame. So consider this, if you have an animation consisting of multiple KeyFrames.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With