| Ease In to Linear (range mapping problem)• | Ease In to Linear (range mapping problem)
on Jul 22, 2019 at 11:09:21 am |
Hi,
I wrote this post in Cinema 4D forum because I want an Xpresso setup, but it could be done with AE expressions:
I want to write an Xpresso function to Ease In values at the beginning until normal linear interpolation, the very same way a car accelerates until it reaches constant speed, or the way you constraint 2 objects with a Spring Constraint in C4D.
Spline graph in Range Mapper (or Ease In in AE) doesn't work because at the end of spline mapping range there's always an angle.
In below picture I want to write an expression to get the blue curve from the green one (red one is range mapped)
Any ideas?
Thanks
Corrado • | Re: Ease In to Linear (range mapping problem) on Jul 22, 2019 at 3:30:10 pm |
Fun thought experiment, lets talk it through a bit.
Well, first let's think how we would draw this out in bezier curves.
It would be 3 points to create the blue line; one (A) at the origin, one (C) at the destination, and one stuck in the middle at the inflection point (B). I would want no out-tangent on the origin, and an in-tangent on the inflection point (B1) , and no point at the out point.
The in-tangent location (B1) would be on a straight line extended between (B-C), at an yet-undetermined distance.
For clarification, if we were going to decelerate, it would be another inflection point at the beginning of the deceleration, but it would have an out-tangent parallel to the slope of the (B-C).
to parse data like this, I would probably cannibalize a cubic Bezier function, https://cubic-bezier.com/#.17,.67,.83,.67.
Luckily for you, I've already rewritten this as a function for my own needs:
"cubicBezier":function(t, tMin, tMax, value1, value2, handles){
aX = thisLayer.linear(t,tMin,tMax,0,1);
try{
mX1 = thisLayer.clamp(handles[0],0,1);
mX2 = thisLayer.clamp(handles[2],0,1);
mY1 = handles[1]; mY2 = handles[3];
}catch(e){mX1 = .89; mY1 = -0.03; mX2 = 0; mY2 = .98;}
function A(aA1,aA2){return 1.0-3.0*aA2+3.0*aA1;}
function B(aA1,aA2){return 3.0*aA2-6.0*aA1;}
function C(aA1){return 3.0*aA1;}
function CalcBezier(aT,aA1,aA2){return((A(aA1,aA2)*aT+B(aA1,aA2))*aT+C(aA1))*aT;}
function GetSlope(aT,aA1,aA2){return 3.0*A(aA1,aA2)*aT*aT+2.0*B(aA1,aA2)*aT+C(aA1);}
function GetTForX(aX){
var aGuessT=aX;
for(var i=0;i<6;++i){
var currentSlope=GetSlope(aGuessT,mX1,mX2);
if(currentSlope==0.0)returnaGuessT;
var currentX=CalcBezier(aGuessT,mX1,mX2)-aX;
aGuessT-=currentX/currentSlope;
}return aGuessT;}
if (mX1 != mY1 && mX2 != mY2) aX = CalcBezier(GetTForX(aX), mY1, mY2);
return tMin < tMax ? thisLayer.linear(aX,0,1,value1,value2) : thisLayer.linear(aX,0,1,value2,value1);
},
This works just like linear, where you parse (t,tMin,tMax,value1,value2), but then at the end we add an additional cubic-bezier handle [outTangent-X, outTangent-Y, inTangent-X, inTangent-Y].
So, what you really need to do now is make two sliders: one (Slider 1 or S1) that chooses the time (X-value) of the inflection point (B) relative to the position between A and C. This will be equivalent to your image's "range mapper" limit.
And then another slider that adjusts the acceleration intensity (which will adjust the inflection point's value (Y-value), C's value (Y-value), and also the distance between B1 and B on B-C. This will be a 'percentage' indicator.
So, where do we put B?
first, lets create our points:
A = [key[1].time, key[1].value];
C = [key[numKeys].time,key[numKeys].value];
B will be at:
B = linear(S1, 0,100,A,C);
Now, where do we put B1? Well, if we wanted maximum accelleration, B1 would be found at A. If we wanted minimum acceleration, B1 would be found at B. Neither of this are what you really want; you want something in-between.
So what we can do is this:
B1 = linear(S2,0,100,A,B);
o = linear(S2,0,100,0,B[1]-A[1]); //sets the offset from accellerating
B[1]-= o;
B1[1]-=o;
C[1]-= o;
Now, to parse our handle through the cubic bezier function we need to normalize it from a factor of 0-1. So let's do this:
xs = 1/(B[0]-A[0]);
ys = 1/(B[1]-A[1]);
B1[0] = (B1[0]-A[0]) * xs;
B1[1] = (B1[1]-A[1])*sy;
finally, we just need to determine what part gets parsed through the bezier-curve equation and what doesn't.
if(time >= B[0]){
linear(time,B[0],C[0],B[1],C[1]);
}
or parse that through the cubic bezier function:
else cubicBezier(time, A[0], B[0], A[1], B[1], [ 0, 0, B1[0], B1[1] ])
I think that should do it. There are likely a ton of typos and maybe even a wrong thing or two, but this ought to be 80% of it. No idea how to do multiple keys off the top of my head, but it will likely be else/if functions and checking if things are linear. This is where I would start though. There's a chance you'll need to get B1 closer to B, I would just linearize it again prior to offsetting it if need be: B1 = linear(z,0,1,B1,B);
Note: none of this code was tested.
Alex Printz
Mograph Designer • | Re: Ease In to Linear (range mapping problem) on Jul 22, 2019 at 6:52:53 pm |
Wow, thank you for your reply, but I'll need "some" time to dig it.
I thought this had to be a very simple problem, given that this is how cars speed up and objects fall down.
Meanwhile I came up with a simple and empirical solution which I don't understand very well too:
First part of the curve is just a parabola, so Y.blue is Y.green dived by 10 and squared (1->0,01 2->0,04 3->0,09 4->0,16 ...); when Y.green reaches 50 Y.blue is 25 (50/10*50/10): at this point the blue parabola is tangent to the green line(!), so from now on Y.blue is just Y.green - 25.
This is working but don't know why... What do you think about this?
Corrado
• | Re: Ease In to Linear (range mapping problem) on Jul 22, 2019 at 7:37:44 pm |
That's an interesting approach, there's lots of ways to deal with stuff like this I guess is the answer. Here is my working code that I've taken a stab at after lunch. The biggest change was I was having problems with the matrix math, I'm sure there are more appropriate ways to deal with this either as for loops or whatnot, but here it is:
function cubicBezier(t, tMin, tMax, value1, value2, handles){
aX = thisLayer.linear(t,tMin,tMax,0,1);
try{
mX1 = thisLayer.clamp(handles[0],0,1);
mX2 = thisLayer.clamp(handles[2],0,1);
mY1 = handles[1]; mY2 = handles[3];
}catch(e){mX1 = .89; mY1 = -0.03; mX2 = 0; mY2 = .98;}
function A(aA1,aA2){return 1.0-3.0*aA2+3.0*aA1;}
function B(aA1,aA2){return 3.0*aA2-6.0*aA1;}
function C(aA1){return 3.0*aA1;}
function CalcBezier(aT,aA1,aA2){
return((A(aA1,aA2)*aT+B(aA1,aA2))*aT+C(aA1))*aT;
}
function GetSlope(aT,aA1,aA2){
return 3.0*A(aA1,aA2)*aT*aT+2.0*B(aA1,aA2)*aT+C(aA1);
}
function GetTForX(aX){
var aGuessT=aX;
for(var i=0;i<6;++i){
var currentSlope=GetSlope(aGuessT,mX1,mX2);
if(currentSlope==0.0)returnaGuessT;
var currentX=CalcBezier(aGuessT,mX1,mX2)-aX;
aGuessT-=currentX/currentSlope;
}return aGuessT;
}
if (mX1 != mY1 && mX2 != mY2) aX = CalcBezier(GetTForX(aX), mY1, mY2);
return tMin < tMax ? thisLayer.linear(aX,0,1,value1,value2) : thisLayer.linear(aX,0,1,value2,value1);
}
L = thisComp.layer("Shape Layer 1");
S1 = L.effect("S1")(1).value; //slider for '% length until reached velocity'
S2 = L.effect("S2")(1).value; //slider for ' rate of accelleration increase'
AP = L.position.key(1).value; //point A position
AT = L.position.key(1).time; //point A time
CP = L.position.key(numKeys).value; //point C position
CT = L.position.key(numKeys).time; //point C time
BP = linear(S1,0,100,AP,CP); //point B position
BT = linear(S1,0,100,AT,CT); //point B time
XP = linear(S2,0,100,BP,AP); //handle B1(X) position
XT = linear(S2,0,100,AT,BT); //handle B1(X) time
O = linear(S2,0,100,0,BP-AP); //sets the offset from accellerating;
BP = [BP[0]-O[0],BP[1]-O[1]]; // offsets B position caused my accelleration
CP = [CP[0]-O[0],CP[1]-O[1]]; // offsets C position caused my accelleration
XP = [XP[0]-O[0],XP[1]-O[1]]; // offsets B1(X) position caused my accelleration
if(time >= BT){ //if we are after the accelleration
linear(time,BT,CT,BP,CP); //linears between B and C (full accleration)
}else{
XP[0] = linear(XP[0],AP[0],BP[0],0,.1); // linearizes B1(X) X-position
XP[1] = linear(XP[1],AP[1],BP[1],0,.1); // linearizes B1(X) Y-position
XT = linear(XT,AT,BT,0,1); // linearizes B1 time
//creates bezier curves between point A and B with B1(X) as handle
FX = cubicBezier(time, AT, BT, AP[0], BP[0], [ .01, 0, XT, XP[0] ]);
FY = cubicBezier(time, AT, BT, AP[1], BP[1], [ .01, 0, XT, XP[1] ]);
[FX,FY]
}
Alex Printz
Mograph Designer • | Re: Ease In to Linear (range mapping problem) on Jul 29, 2019 at 9:47:32 am |
Thank you very much for your code, but I don't even understand where I'm supposed to use it ☺
I see I need a "Shape layer 1" with 2 sliders S1, S2 but then I don't know where I should specify point A B C position etc.
Can you clarify a bit further please?
Thanks
Corrado • | Re: Ease In to Linear (range mapping problem) on Aug 6, 2019 at 2:40:06 pm |
Set two keyframes, your beginning and end points. These will be A and C.
then have to sliders, "S1" and "S2" (rename them however you want.
S1 should be the percentage of length of time it takes to accelerate up to full velocity.
S2 should be rate that the acceleration happens (is it really smooth? Is it really abrupt?).
B will be calculated automatically based on S1 and S2.
Alex Printz
Mograph Designer
• | Re: Ease In to Linear (range mapping problem) on Aug 6, 2019 at 2:42:25 pm |
whoops, I think L should be set to 'thisLayer'; I had this code pasted on a text object for debugging and it was referencing the animated layer.
Alex Printz
Mograph Designer • | Re: Ease In to Linear (range mapping problem) on Aug 6, 2019 at 5:33:07 pm |
This one is pretty plug and play. Just need to make your 2 sliders, and 2 keys.
function cubicBezier(t, tMin, tMax, value1, value2, handles){
aX = thisLayer.linear(t,tMin,tMax,0,1);
try{
mX1 = thisLayer.clamp(handles[0],0,1);
mX2 = thisLayer.clamp(handles[2],0,1);
mY1 = handles[1]; mY2 = handles[3];
}catch(e){mX1 = .89; mY1 = -0.03; mX2 = 0; mY2 = .98;}
function A(aA1,aA2){return 1-3*aA2+3*aA1;}
function B(aA1,aA2){return 3*aA2-6*aA1;}
function C(aA1){return 3*aA1;}
function CalcBezier(aT,aA1,aA2){
return((A(aA1,aA2)*aT+B(aA1,aA2))*aT+C(aA1))*aT;
}
function GetSlope(aT,aA1,aA2){
return 3*A(aA1,aA2)*aT*aT+2*B(aA1,aA2)*aT+C(aA1);
}
function GetTForX(aX){
var aGuessT=aX;
for(var i=0;i<6;++i){
var currentSlope=GetSlope(aGuessT,mX1,mX2);
if(currentSlope==0)returnaGuessT;
var currentX=CalcBezier(aGuessT,mX1,mX2)-aX;
aGuessT-=currentX/currentSlope;
}return aGuessT;
}
if (mX1 != mY1 && mX2 != mY2) aX = CalcBezier(GetTForX(aX), mY1, mY2);
return tMin < tMax ? thisLayer.linear(aX,0,1,value1,value2) : thisLayer.linear(aX,0,1,value2,value1);
}
V = value.length;
S1 = effect("S1")(1).value; //slider for '% length until reached velocity'
S2 = effect("S2")(1).value; //slider for ' rate of accelleration increase'
AP = key(1).value; //point A position
AT = key(1).time; //point A time
CP = key(numKeys).value; //point C position
CT = key(numKeys).time; //point C time
BP = linear(S1,0,100,AP,CP); //point B position
BT = linear(S1,0,100,AT,CT); //point B time
XP = linear(S2,0,100,BP,AP); //handle B1(X) position
XT = linear(S2,0,100,AT,BT); //handle B1(X) time
O = linear(S2,0,100,0,BP-AP); //sets the offset from accellerating;
if(V.length > 1){
for(i=0;i= BT){ //if we are after the accelleration
linear(time,BT,CT,BP,CP); //linears between B and C (full accleration)
}else{
if(V > 1) for(i=0;i 1){
F=[];
for(i=0;i<V;i++) F[i] = cubicBezier(time, AT, BT, AP[i], BP[i], [ .01, 0, XT, XP[i] ]); // linearizes B1(X) X-position
}else F = cubicBezier(time, AT, BT, AP, BP, [ .01, 0, XT, XP ])
F
}
Alex Printz
Mograph Designer • | Re: Ease In to Linear (range mapping problem) on Aug 23, 2019 at 12:25:33 am |
Thank you very much, I think i'll still need some time to figure out how is working.
now it's giving me an "error at line 52 expected: ;., an expression was disabled..." which I can't find.
I'll try to fix it
(About my first parabola approach, it's x it's going up with constant acceleration (y= x*x) til it reach the parabola's focus at 1/4 y, where the tangent is y = x, constant velocity)
| |