【AE表達(dá)式】createPath創(chuàng)建可控制彈性繩 //用于MG彈跳動(dòng)畫


/*表達(dá)式文末自取*/
關(guān)于彈性運(yùn)動(dòng)網(wǎng)上都有表達(dá)式可以直接拿來用的
本文就不講如何用表達(dá)式實(shí)現(xiàn)彈性運(yùn)動(dòng)了/*主要是我也不會(huì),也許以后會(huì)*/
//其實(shí)文章標(biāo)題應(yīng)該叫做 表達(dá)式控制正弦函數(shù)圖像的各種變化

分析
小球的彈性運(yùn)動(dòng)直接上表達(dá)式:/*反正我也是抄的*/
a=2;//振蕩頻率
b=2;//衰減率
n=0;
if (numKeys>0){
?????? n=nearestKey(time).index;
?????? if (key(n).time>time) n--;}
if (n>0){
?????? t=time-key(n).time;
?????? amp=velocityAtTime(key(n).time-.001);
?????? w=a*Math.PI*2;
?????? value+amp*(Math.sin(t*w)/Math.exp(b*t)/w);}
else{value}
//添加關(guān)鍵幀動(dòng)畫后才有效果
繩子部分使用createPath創(chuàng)建正弦函數(shù)路徑,并使繩子末端跟隨小球運(yùn)動(dòng),頂端不動(dòng)
/*如果你不會(huì)用createPath,我這有個(gè)好康的cv6759435
后面我也會(huì)提一下,就只提一下,不能提多了,畢竟文章中心是控制正弦曲線
總之這個(gè)表達(dá)式非常好玩*/

目標(biāo)
1、 可改變彈性繩的圈數(shù)和寬度/*頻率和振幅*/

2、 通過移動(dòng)兩個(gè)控制點(diǎn)操控彈性繩位置與水平長(zhǎng)度
同時(shí)繩子自身長(zhǎng)度不變//近似不變

3、 曲線折線的切換

4、 彈性繩的相位變化


思路
控制振幅和頻率:建立y=lon*sin(fx) 的函數(shù),lon:振幅,f:頻率
控制位置:
為函數(shù)圖像上所有點(diǎn)加上首點(diǎn)/*第一控制點(diǎn)*/的坐標(biāo)
讀取兩控制點(diǎn)的間距控制圖像水平距離//以便圖像上最后一個(gè)點(diǎn)與末點(diǎn)/*第二控制點(diǎn)*/重合
通過兩點(diǎn)坐標(biāo)差的商求出圖像整體需要旋轉(zhuǎn)的角度
//順手做個(gè)動(dòng)畫以便理解

保持繩長(zhǎng)固定:
通過弧微分公式//你確實(shí)可以這樣做,但之后你會(huì)為了求個(gè)弧長(zhǎng)自學(xué)橢圓積分、階乘Γ函數(shù)、高斯常數(shù)…先不說浪費(fèi)時(shí)間,做個(gè)動(dòng)畫也不至于這么卷
曲線長(zhǎng)難求,可以用直線代替
讓x只取 π/2 倍數(shù),就可以得到這些位置的點(diǎn)

//createPath的畫圖邏輯就是描點(diǎn)連線,所以目前只是折線
再用勾股定理表示一段直線的平方:lon^2+(T/4)^2 確保該式為定值//T為圖像周期水平長(zhǎng)度
曲線/折線轉(zhuǎn)換:無疑就是是否有切線的區(qū)別,使用if判斷語句
相位變化:自變量x后面加參數(shù)c以控制相位

實(shí)現(xiàn)
在一切的開始,請(qǐng)務(wù)必用表達(dá)式把圖層的PSR屬性鎖好,因?yàn)槭髽?biāo)很容易誤操作改變圖層形態(tài),導(dǎo)致函數(shù)圖像位置錯(cuò)誤

//所有表達(dá)式寫在一個(gè)形狀圖層中,這樣可以方便復(fù)制到別的合成里

初始設(shè)置
/* createPath(a,b1,b2,c)共四個(gè)參數(shù),使用于路徑中
a為二維數(shù)組(必要),b1出點(diǎn)數(shù)組,b2入點(diǎn)數(shù)組,c為true/false表示是否閉合路徑
a = [ x , y , z …] x y z等 是點(diǎn)的位置/*位置是含有兩個(gè)元素的數(shù)組*/,從左向右讀取連線 */
創(chuàng)建形狀圖層,效果面板里添加并重命名一些表達(dá)式控制//也可以先把名字命名為參數(shù)

鋼筆工具在視圖窗中畫個(gè)點(diǎn),內(nèi)容中就會(huì)出現(xiàn)形狀 1
然后按住Alt點(diǎn)擊路徑前面的圈,打開表達(dá)式編輯框

先定義幾個(gè)變量:
f=effect("頻率")("滑塊");//最重要,不能<=0
p=effect("點(diǎn)密度")("滑塊");//表示單位長(zhǎng)度內(nèi)點(diǎn)的數(shù)量有點(diǎn)雞肋
a=effect("寬度")("滑塊");//用于控制振幅lon
c=effect("相位")("滑塊");
x1=effect("首點(diǎn)")("點(diǎn)")[0];y1=effect("首點(diǎn)")("點(diǎn)")[1];//用于控制位置和方向
x2=effect("末點(diǎn)")("點(diǎn)")[0];y2=effect("末點(diǎn)")("點(diǎn)")[1];
pots=[];ins=[];outs=[];//要傳給createPath的二維數(shù)組
lon=100;hoz=50;m=1;n=10;//先定義幾個(gè)待會(huì)要用的參數(shù)

建立函數(shù)
創(chuàng)建一個(gè)正弦函數(shù):
function fx(t){
?????? x = t;
?????? y = lon * Math.sin( f * t );//函數(shù)lon*sinfx
?????? return [ x , y ]+[ x1 , y1 ];//所有點(diǎn)受首點(diǎn)控制
}
使用時(shí)只需要輸 fx/*方法名,隨便取*/(0),就返回x=0時(shí)點(diǎn)的位置
但一個(gè)一個(gè)輸x值太麻煩,就加一個(gè)for循環(huán)自動(dòng)取點(diǎn):
for (i=0;i<=n;i=i+1){//n表示點(diǎn)數(shù),之后由別的參數(shù)進(jìn)行控制
?????? w = Math.PI/2/f; //只取x = π/2 倍數(shù)的點(diǎn),除以f是因?yàn)閳D像上兩點(diǎn)間距與f成反比
?????? pos = fx(i*w);
?????? pots.push(pos); //每次循環(huán)向數(shù)組pots 添加一個(gè)點(diǎn)的位置
}
最后寫:
if (effect("切線")("復(fù)選框")==0){
?????? ins = [] ; outs = [] ;
}//如果復(fù)選框沒勾選的話,就讓ins和outs成為空數(shù)組,就沒有切線
createPath( pots , ins , outs , 0 )
于是你得到了一條只有聰明人才看得見的sinx函數(shù)曲線

曲線水平方向太小,是因?yàn)辄c(diǎn)的x值 = i/f*w ,這三個(gè)參數(shù)都是個(gè)位數(shù)
所以畫出來的圖像上相鄰兩點(diǎn)間距很短//大概就幾個(gè)像素

放大水平長(zhǎng)度
解決方法就是讓函數(shù)輸出的x值乘一個(gè)參數(shù)hoz:
function fx(t){
?????? x = t;
?????? y = lon * Math.sin( f * t );//函數(shù)lon*sinfx
?????? return [hoz * x , y ] +[ x1 , y1 ];//hoz 控制水平長(zhǎng)度
}

位置控制
現(xiàn)在開始進(jìn)入正題
要讓圖像水平長(zhǎng)度跟隨首點(diǎn)和末點(diǎn)的距離變化,就讓這兩個(gè)量進(jìn)行聯(lián)系
讓參數(shù) l 表示兩點(diǎn)間距:
?l=Math.sqrt(Math.pow((x2-x1), 2)+Math.pow((y2-y1), 2));
要使圖像最后一個(gè)點(diǎn)與末點(diǎn)重合,l 就應(yīng)為圖像周期長(zhǎng)度的倍數(shù)
根據(jù)函數(shù)求得周期長(zhǎng)度為 hoz*2π/f
/*代碼函數(shù)本身的周期長(zhǎng)度并不是這個(gè),由于代碼函數(shù)輸出的點(diǎn)為[hoz * x , y ],做出圖像的函數(shù)實(shí)則變了y = lon * sin( f * x / hoz ),該函數(shù)才是真正作圖的函數(shù),知道這個(gè)對(duì)寫導(dǎo)函數(shù)有大用*/

我們還希望頻率f 為1時(shí),首末點(diǎn)間只有一個(gè)周期長(zhǎng)度圖像,為2時(shí)有兩個(gè)
則可以讓l = hoz*2π,只需要在function前面輸:
hoz=l/2/Math.PI;
hoz解決了,但還有點(diǎn)數(shù)n的問題,沒有足夠的n就不能生成足夠長(zhǎng)的線
n應(yīng)與l和f成正比,但n與f的關(guān)系更明顯
f為1時(shí),首末點(diǎn)間只有一個(gè)圖像周期的所有點(diǎn),點(diǎn)數(shù)為5,n=4
//因?yàn)樵趂or循環(huán)取值時(shí)i取0、1、2、3、4剛好5個(gè)點(diǎn)
f為2時(shí),首末點(diǎn)間有兩個(gè)圖像周期的所有點(diǎn),點(diǎn)數(shù)為5+4=9,n=8
很容易看出關(guān)系,把之前的n改為:
n = 4 * f ;

方向控制
但現(xiàn)在還不能讓圖像跟著首末點(diǎn)跑,它還不能旋轉(zhuǎn)
進(jìn)入內(nèi)容-形狀 1-變換-旋轉(zhuǎn)表達(dá)式編輯界面
/*選中圖層按快捷鍵R的是圖層旋轉(zhuǎn)屬性,它應(yīng)該被表達(dá)式鎖住的
這個(gè)是形狀 1的旋轉(zhuǎn)屬性*/

直接輸:
x1=effect("首點(diǎn)")("點(diǎn)")[0];y1=effect("首點(diǎn)")("點(diǎn)")[1];
x2=effect("末點(diǎn)")("點(diǎn)")[0];y2=effect("末點(diǎn)")("點(diǎn)")[1];
dy=y2-y1;dx=x2-x1;//xy坐標(biāo)差求角度
radiansToDegrees(Math.atan2(dy, dx))
//原理上文的動(dòng)圖解釋得很直觀了

錨點(diǎn)跟隨首點(diǎn)
但移動(dòng)控制點(diǎn)時(shí)就發(fā)現(xiàn)圖像并不以控制點(diǎn)為中心旋轉(zhuǎn)
/*這不廢話?圖像肯定繞錨點(diǎn)旋轉(zhuǎn)啊*/
改變錨點(diǎn)的參數(shù)只會(huì)移動(dòng)圖像,改變位置的參數(shù)則錨點(diǎn)和圖像一起動(dòng)
于是在錨點(diǎn)和位置屬性里同時(shí)輸入:
x1=effect("首點(diǎn)")("點(diǎn)")[0];y1=effect("首點(diǎn)")("點(diǎn)")[1];
[x1,y1]//首點(diǎn)控制路徑
讓錨點(diǎn)跟隨首點(diǎn)的同時(shí)不改變位置,現(xiàn)在圖像可以被完全操控了
//如果還不行就重新檢查圖層的位置和錨點(diǎn)屬性是否都為[ 0 , 0 ]

繩長(zhǎng)固定
根據(jù)思路用直線代替弧長(zhǎng)
讓圖像中一段直線長(zhǎng)度的平方:lon^2+(T/4)^2成為定值
于是假設(shè)這個(gè)定值為a*k/*k為常數(shù)*/圖像周期長(zhǎng)度T/4為l/(4*f)
則lon^2+( l/(4*f))^2=(a*k)^2 ,于是輸入:
lon=-Math.sqrt(a*100-Math.pow(l/4/f, 2));
//經(jīng)過調(diào)參決定把k設(shè)為100,
//AE中Y軸正方向是向下的,所以lon改為負(fù)值

生成切線
目前為止我們只畫了個(gè)折線,形成曲線得添加切線
切線就是滑桿或手柄//會(huì)用鋼筆工具的你一定知道手柄是什么
手柄有出點(diǎn)和出點(diǎn),這里只需要確定出點(diǎn)的位置然后傳給數(shù)組ins
//“出點(diǎn)的位置”指出點(diǎn)相對(duì)與父點(diǎn)的位置,并非其世界坐標(biāo)

父點(diǎn)就是在圖像上的點(diǎn),我們要基于這些點(diǎn)確定出點(diǎn)的位置
要確定出點(diǎn)位置就應(yīng)先知道切線斜率
切線斜率用導(dǎo)數(shù)公式求,函數(shù)代碼下方輸入導(dǎo)函數(shù):
function dy(t){
?????? k=lon*f*Math.cos(f*t)/hoz;//求斜率
?????? x=-Math.cos(Math.atan(k)); //因?yàn)槭浅鳇c(diǎn),所以為負(fù)
?????? y=-Math.sin(Math.atan(k));
?????? return [x,y]; //輸出出點(diǎn)的位置
}
/*根據(jù)作圖的函數(shù)y=lon*sin(f*x/hoz)求出導(dǎo)函數(shù)lon*f*cosfx /hoz
注意作圖函數(shù)自變量為x/hoz,而導(dǎo)函數(shù)自變量為x,不理解就別理解吧*/
同理,用for循環(huán)把所有出點(diǎn)位置傳給數(shù)組ins:
for (i=0;i<=n;i=i+1){
?????? w=Math.PI/2/f;
?????? pos=fx(i*w);inp=dy(i*w);//給函數(shù)和導(dǎo)函數(shù)的x值一樣
?????? pots.push(pos);ins.push(inp);outs.push(-inp);//入點(diǎn)位置就是出點(diǎn)位置的反方向
}
現(xiàn)在打開效果控件里的切線開關(guān),就可以得到聰明人也看不見的切線了

切線長(zhǎng)度
還是同理,切線長(zhǎng)度只有一個(gè)像素左右大,于是改導(dǎo)函數(shù):
function dy(t){
?????? k=lon*f*Math.cos(f*t)/hoz;//求斜率
?????? x=-Math.cos(Math.atan(k));
?????? y=-Math.sin(Math.atan(k));
?????? return [m*x,m*y]; //m控制切線長(zhǎng)度
}
m應(yīng)隨著l的增長(zhǎng)而增長(zhǎng),隨著f的增加而減短
所以輸入:
m = o * l / f;
//o為常數(shù),建議為參數(shù)o創(chuàng)建一個(gè)滑塊控制,調(diào)節(jié)圖像與由描點(diǎn)連線形成的圖像進(jìn)行契合,找出最佳契合常數(shù),大概是0.1左右吧
你可能會(huì)覺得m應(yīng)該也與振幅控制系數(shù)a有關(guān),但如果不管a的影響的話,契合誤差還是比較小的
/*圖中灰色為由切線作出的圖像,淺綠色是描點(diǎn)連線的圖像
貝塞爾曲線不可能完全契合sinx函數(shù)圖像*/

相位
注意函數(shù)和導(dǎo)函數(shù)都要加相位控制參數(shù)c:
function fx(t){
?????? x=t;
?????? y=lon*Math.sin(f*t+c/*相位*/);
?????? return [hoz*x+x1,y+y1]
}
function dy(t){
?????? k=lon*f*Math.cos(f*t+c/*相位*/)/hoz;
?????? x=-Math.cos(Math.atan(k));
?????? y=-Math.sin(Math.atan(k));
?????? return [m*x,m*y];
}

建立點(diǎn)密度/*可略過*/
點(diǎn)密度p就是單位長(zhǎng)度內(nèi)點(diǎn)的數(shù)量,在一些極端情況/*比如振幅極大時(shí)*/可以通過增加點(diǎn)密度讓曲線生成的圖像更像sinx函數(shù)圖像

原理就是控制圖像上相鄰兩點(diǎn)水平間距:
for (i=0;i<=n;i=i+1){
?????? w=Math.PI/2/f/p;//使p增大時(shí)兩點(diǎn)間距減小
?????? pos=fx(i*w);inp=dy(i*w);
?????? pots.push(pos);ins.push(inp);outs.push(-inp);
}
圖像上兩點(diǎn)間距減小,那點(diǎn)數(shù)n也就要增加:
n=f*p*4;
點(diǎn)數(shù)n多了,切線長(zhǎng)度m就要減小:
m=o*l/f/p;//o為由你調(diào)出的常數(shù)
/*點(diǎn)密度p可能有點(diǎn)雞肋,但還是建議加上它,說不定就用上了*/

參數(shù)控制
做到這里基本結(jié)束了,接下來優(yōu)化代碼,處理一些報(bào)錯(cuò)的情況

心血來潮把點(diǎn)密度p調(diào)得過大導(dǎo)致點(diǎn)數(shù)過多,使循環(huán)次數(shù)過多導(dǎo)致電腦爆炸
同理f也是,使用clamp限制其數(shù)值:
p=clamp(p,20,0.1);
f=Math.round(clamp(f,20,1));
//f取整使首末點(diǎn)始終位于圖像中間,f必須>0
心血來潮讓首末點(diǎn)間距過長(zhǎng)則發(fā)生報(bào)錯(cuò),因?yàn)槭啄c(diǎn)間距過長(zhǎng)時(shí)
振幅公式lon=-Math.sqrt(a*100-Math.pow(l/4/f, 2));
中的a*100-Math.pow(l/4/f, 2)成為負(fù)數(shù),則Math.sqrt()不能計(jì)算
用if判斷語句解決:
if (a*100 < Math.pow(l/4/f, 2)){
lon=0;
}
else{
lon=-Math.sqrt(a*100-Math.pow(l/4/f, 2));
}
使當(dāng)l過長(zhǎng)時(shí)振幅lon為0,形成一條直線

總結(jié)
后來覺得可以改進(jìn)繩長(zhǎng)固定的代碼,于是用微分的方法把弧長(zhǎng)確定到了個(gè)位
但算出了弧長(zhǎng)后不知道怎么根據(jù)弧長(zhǎng)控制振幅lon,就傻了

發(fā)現(xiàn)自己花了一堆時(shí)間整出個(gè)沒用的sin弧長(zhǎng)計(jì)算代碼
//本來連圖都畫出來了的……

前天看了大佬的三篇文章cv6759435,從而了解createPath的使用方法
于是前天晚上就有一個(gè)這樣的想法,當(dāng)天半夜寫出了個(gè)大概,昨天進(jìn)行代碼各種功能的完善,下午加晚上寫文章和做gif動(dòng)圖,今天用一堆時(shí)間整弧長(zhǎng)
/*總之createPath這個(gè)表達(dá)式非常好玩*/
?

感覺這些表達(dá)式可以用在很多mg動(dòng)畫的方面,不僅僅是彈跳動(dòng)畫
之后會(huì)稍微研究一下下面的兩種彈性表達(dá)式

?代碼/*與上文有一點(diǎn)出入*/
地面反彈
e=effect("彈力系數(shù)")("滑塊");
g=10*effect("重力 *10")("滑塊");
nMax=effect("最大反彈次數(shù)")("滑塊");
n=10;
if(numKeys>0){
?????? n=nearestKey(time).index;
?????? if(key(n).time>time)n--;}
if(n>0){
?????? t=time-key(n).time;
?????? v=-velocityAtTime(key(n).time-.001)*e;
?????? vl=length(v);
?????? if(value instanceof Array){
????????????? vu=(vl>0)? normalize(v):[0,0,0];}
?????? else{vu=(v<0)?-1:1;}
tCur=0;
segDur=2*vl/g;
tNext=segDur;
nb=1;//Number of bounces
while(tNext < t && nb <= nMax){
?????? vl*=e;
?????? segDur*=e;
?????? tCur=tNext;
?????? tNext+=segDur;
?????? nb++}
if(nb<=nMax){
?????? delta=t-tCur;
?????? value+vu*delta*(vl-g*delta/2);}
else{value}
}
else{value}
彈性振蕩
a=effect("振蕩頻率")("滑塊");
b=effect("衰減率")("滑塊");
n=0;
if (numKeys>0){
?????? n=nearestKey(time).index;
?????? if (key(n).time>time) n--;}
if (n>0){
?????? t=time-key(n).time;
?????? amp=velocityAtTime(key(n).time-.001);
?????? w=a*Math.PI*2;
?????? value+amp*(Math.sin(t*w)/Math.exp(b*t)/w);}
else{value}
/*兩個(gè)彈性表達(dá)式都是網(wǎng)上找到的其中一種,都需要添加關(guān)鍵幀動(dòng)畫*/
彈性繩路徑
f=effect("頻率")("滑塊");
p=effect("點(diǎn)密度")("滑塊");
a=effect("寬度")("滑塊");c=effect("相位")("滑塊");
x1=effect("首點(diǎn)")("點(diǎn)")[0];y1=effect("首點(diǎn)")("點(diǎn)")[1];
x2=effect("末點(diǎn)")("點(diǎn)")[0];y2=effect("末點(diǎn)")("點(diǎn)")[1];
pots=[];ins=[];outs=[];
?
/*設(shè)置參數(shù)閾值,避免報(bào)錯(cuò)*/
p=clamp(p,20,0.1);f=Math.round(clamp(f,20,1));
?
/*參數(shù)控制*/
n=f*p*4;//點(diǎn)數(shù)自動(dòng)改變
l=Math.sqrt(Math.pow((x2-x1), 2)+Math.pow((y2-y1), 2));//首末點(diǎn)距離
hoz=l/2/Math.PI;//水平距離由l控制
?????? if (a*100 < Math.pow(l/4/f, 2)){lon=0;}//防止因l過長(zhǎng)的報(bào)錯(cuò)
?????? else{lon=-Math.sqrt(a*100-Math.pow(l/4/f, 2));}//振幅控制,使線段總長(zhǎng)一致
m=-0.1001*l/f/p;//切線長(zhǎng)度調(diào)節(jié)
?
/*函數(shù)部分*/
function fx(t){
?????? x=t;
?????? y=lon*Math.sin(f*t+c/*相位*/);//函數(shù)lon*sinfx
?????? return [hoz*x+x1,y+y1]//曲線從首點(diǎn)出發(fā),用hoz放大水平長(zhǎng)度
}
function dy(t){
?????? k=lon*f*Math.cos(f*t+c)/hoz;//導(dǎo)數(shù)lon*f*cosfx,得出切線斜率
?????? x=Math.cos(Math.atan(k));
?????? y=Math.sin(Math.atan(k));//返回入點(diǎn)位置
?????? return [m*x,m*y];//m調(diào)節(jié)切線長(zhǎng)度
}
?
/*通過循環(huán)獲取點(diǎn)*/
for (i=0;i<=n;i=i+1){
?????? w=Math.PI/2/p/f;//兩點(diǎn)間距
?????? pos=fx(i*w);inp=dy(i*w);
?????? pots.push(pos);ins.push(inp);outs.push(-inp);
}
?
/*切線控制*/
if (effect("切線")("復(fù)選框")==0){
?????? ins=[];outs=[];
}
?
/*創(chuàng)建路徑*/
createPath(pots,ins,outs,0)
//關(guān)于形狀屬性的表達(dá)式詳見方向控制

2022.6.15 寫了腳本,粘貼到記事本里保存,后綴改成.jsx就能用了