動效乾貨——貝塞爾曲線插值器原理(進階準備)

之前的文章展示過插值器的一點原理。動效乾貨--交互動效在開發落地中是怎麼實現的?(插值器及動效標註) - 知乎專欄

這次將詳細講解其中提到的「貝塞爾曲線」,它運用非常廣泛。多任務滑動效果就用到貝塞爾曲線原理。在這個例子里練習代碼的基本操作手段,包括 for循環,數組操作,邏輯判斷,函數,自動計數手法等等。

n

cubic-bezier.com 是三階貝塞爾曲線。可以看出來它實際一共有4個控制點,起始點、結束點,以及中間的兩個調節點。但是我們先看一個更簡單的:

二階貝塞爾。

(GIF圖)如果無法播放,可以點擊鏈接 pic2.zhimg.com/v2-e4016

它一共3個控制點。P0是線段起點,P1是中間的調節曲線形態的調節點,P2是終點。

而 t 呢,就是表示整個過程,t 從0逐漸變化到1,就可以得出實際曲線上 P 的所有的位置坐標。

插值器是使用公式來自定義的。貝塞爾曲線就有自己的公式。我們直接去網上拿過來用。這個公式是怎麼發現和證明的我們不要去管。

P = (1-t)2 P0 + 2t(1-t)P1 + t2P2

我們可以把x,y分解出來。

Px = (1-t)2 P0x + 2t(1-t)P1x + t2P2x

n

Py = (1-t)2 P0y + 2t(1-t)P1y + t2P2y

需要把這個運用到 Framer 里。但在這之前需要了解一個概念「細分值」或者說「分段數」「採樣數」。我們得到的都是一個個的點。比如說 t 從0過渡到1,用了20次,每次遞增0.05。那麼「分段數」就是20。可以得到20個點的坐標。我們把20個坐標分別賦予給20個對象,就可以看到曲線形態了。

subdivs = 20 #細分值nn#建立兩個數組,用來存放P的數據nPx = []nPy = []nn#給三個控制點的坐標nP0 = [0,0]nP1 = [100,400]nP2 = [400,400]nn#用準備好的公式計算20次P的值,並且存放到數組裡nfor i in [0...subdivs]nt# t = i/subdivsnt_x = Math.pow(1 - i/subdivs, 2) * P0[0] + 2 * i/subdivs * (1 - i/subdivs) * P1[0] + Math.pow(i/subdivs, 2) * P2[0]nt_y = Math.pow(1 - i/subdivs, 2) * P0[1] + 2 * i/subdivs * (1 - i/subdivs) * P1[1] + Math.pow(i/subdivs, 2) * P2[1]ntPx.push(_x)ntPy.push(_y)nnn#顯示曲線軌跡nfor i in [0...subdivs]nt_point = new Layernttwidth: 10nttheight: 10nttbackgroundColor: HSL(201, 97%, 58%)nttborderRadius: "50%"nttx: Px[i] - 10/2 #從數組裡拿到相應的P點坐標,減去寬度的一半,使之中心點落在曲線上ntty: Py[i] - 10/2 #同樣減去高度的一半n

可以增加細分看看。

subdivs = 60 #細分值n

另外插值器有個慣例:控制點總是設置成在0~1以內。把得到的值再去乘以一個尺寸。

subdivs = 20n_size = 400 #尺寸nnPx = []nPy = []n#設置為 0~1nP0 = [0,0]nP1 = [0.25,1]nP2 = [1,1]nnfor i in [0...subdivs]nt_x = Math.pow(1 - i/subdivs, 2) * P0[0] + 2 * i/subdivs * (1 - i/subdivs) * P1[0] + Math.pow(i/subdivs, 2) * P2[0]nt_y = Math.pow(1 - i/subdivs, 2) * P0[1] + 2 * i/subdivs * (1 - i/subdivs) * P1[1] + Math.pow(i/subdivs, 2) * P2[1]ntPx.push(_x)ntPy.push(_y)nnfor i in [0...subdivs]nt_point = new Layernttwidth: 10nttheight: 10nttbackgroundColor: HSL(201, 97%, 58%)nttborderRadius: "50%"nttx: Px[i]*_size - 10/2 #乘上尺寸ntty: Py[i]*_size - 10/2 #乘上尺寸n

以上就是二階貝塞爾曲線的原理了。

===============================================================

最後介紹一個三階貝塞爾曲線的另類用法:羽化漸變過渡。

我們知道線性漸變,sketch 里的顏色過渡就是線性過渡。但過渡效果其實很不好,很糙!它很生硬,邊緣處有明顯的界限。這個時候我想到「羽化」可以解決這個問題。

左邊是線性,右邊是羽化。

但是我沒有在網路上找到羽化的公式,於是我想用貝塞爾曲線模擬了一個。

我通過手動採樣記錄了 Ae 裡面羽化效果的實際變化表現。並找到了一個三階貝塞爾曲線來擬合這個曲線。

bezier-curve(0.58, 0.09, 0.38, 0.92)』

這次,我不自己去搭建插值器。三階貝塞爾,Framer 已經有了。我直接讓一個對象用這個曲線去做動畫,然後把它做動畫的那個屬性的值,換算投射到另外一些對象的顏色上。就可以得到這些顏色過渡。

feather_curve = bezier-curve(0.58, 0.09, 0.38, 0.92)nsampling_total = 20 #採樣分段數nsampling_points = [1] #建立一個數組,用來存放採樣點的值,先放一個1進去,後面的數將是從1過渡到0,和上面那個例子是反過來的ntime_ = sampling_total*0.1 #採樣動畫的總時長,我設置成每0.1秒採樣一次,總共需要多久,讓程序自己去算吧nnColor_1 = #fffnColor_2 = #000nn#用來移動的對象,設置好動畫,讓屬性值從1過渡到0nmove = new Layerntx: 1ntbackgroundColor: null #我們不需要看見它n#但動畫並不馬上開始,先寫在這nanimation_a = new Animationntlayer: moventproperties: nttx: 0nttime: time_ntcurve: feather_curvenn#三個區域,上下兩個是起始、結束的固定色,中間一個是採樣漸變色nstartColor = new Layerntheight: 100ntwidth: Screen.widthntopacity: 1ntbackgroundColor: Color_1nsampling_layers = new Layernty: 100ntbackgroundColor: nullntwidth: Screen.widthntheight: 200nendColor = new Layernty: 300ntheight: 100ntwidth: Screen.widthntopacity: 1ntbackgroundColor: Color_2nn#創建分段,把它們放到 sampling_layers 的子集里,並垂直排列好nfor i in [0...sampling_total]ntsub = new Layernttparent: sampling_layersnttwidth: sampling_layers.widthnttheight: sampling_layers.height/sampling_totalntty: i * sampling_layers.height/sampling_totalnttopacity: 1nttbackgroundColor: pink #給個原始色nnn# 建立採樣機制nsampler = null #聲明一個對象,作為採樣器使用,但它一開始什麼都不是ncount = 0 #聲明一個變數,用來計數。每採樣一次它增加1,告訴機器什麼時候采夠20個了n#具體的機制nsampler_start = ->nt#在 sampler 里寫一個 interval,其作用是每隔一段時間運行一次下面的命令。這裡時間設置成 time_/sampling_total 意味著在總時長 time_ 里必須采夠 sampling_total 那麼多次,每次的間隔是多久讓程序自己去算吧ntsampler = Utils.interval time_/sampling_total, ->nttpoint_value = move.x #得到曲線動畫實際的坐標值nttsampling_points.push(point_value) #把值放進 sampling_points 數組裡面保存nntt# 這裡是把每個分段的漸變顯示出來。每個分段里使用的還是線性漸變(因為我沒有找到真正的羽化演算法,只能增加線性漸變的分段數來擬合羽化效果)nttsampling_layers.children[count].style = background: "linear-gradient(" + Color.mix(Color_2, Color_1, sampling_points[count]) + " 0%, " + Color.mix(Color_2, Color_1, sampling_points[count+1]) + " 100%)"ntt#linear-gradient(someColor, 0%, otherColor, 100%)是 css 線性過渡的方法。這裡涉及到字元操作,把幾段字元及數據用加號拼接在一起。ntt#Color.mix()是 Framer 內置的方法,按混合的百分比,得到兩個顏色的混合值。我們已經得到了這個混合百分比,就是動畫屬性坐標值,被放在 sampling_points 數組裡,去拿就好了。nttntt#列印出每個採樣點對應的色值nttprint Color.mix(Color_1, Color_2, sampling_points[count]).toHexString()nttntt#讓機器數數nttcount += 1ntt#如果有20次了,停止採樣nttif count >= sampling_totalntttsampler_stop()nn#停止採樣的方法,用 clearInterval 清除 sampler 里的 interval。不要問我為什麼那麼麻煩,我不知道,也不想知道nsampler_stop = ->ntclearInterval(sampler)nnn#以上一切都載入好後,才開始動畫,開始採樣。盡量避免誤差nanimation_a.start()nsampler_start()n

我這樣做的目的主要是檢驗羽化曲線的效果。而列印出每個分段的色值,是以防開發實現不了完整的貝塞爾漸變,可以直接使用20段線性漸變來擬合羽化效果。而且隨便換什麼顏色,這個都能快速給你列印出來。我在這裡公開這個羽化漸變取值小工具,任何人可以隨便使用。

至此,你應該已經掌握了大部分的代碼操作手段。之後會很輕鬆。


推薦閱讀:

那些創意十足的Loading動效原型合集(一鍵復用!)
站在產品設計的角度,我們應該如何思考動效設計?
用PS十倍無損壓縮GIF動效圖

TAG:交互动效 | 前端开发 | Framer |