網(wǎng)易新聞個(gè)人頁(yè)面的水波效果

網(wǎng)易新聞客戶(hù)端的這個(gè)水波效果出來(lái)很久了,我考慮了很長(zhǎng)時(shí)間該如何實(shí)現(xiàn),但是都沒(méi)有很好的辦法,幸好在一個(gè)動(dòng)畫(huà)牛人daixunry的文章里,知道了實(shí)現(xiàn)這個(gè)動(dòng)畫(huà)最關(guān)鍵的點(diǎn)。他的blog上面有許多優(yōu)秀的動(dòng)畫(huà)案例,非常值的學(xué)習(xí),blog的地址是http://www.itdecent.cn/p/272aa1f26c62

329672-8235dc58632c0963.gif

這個(gè)動(dòng)畫(huà)的關(guān)鍵點(diǎn)就是正余弦函數(shù)。在聽(tīng)到這個(gè)的時(shí)候,我非常的震驚,原因是正余弦我們當(dāng)初在高中的時(shí)候?qū)W習(xí)的知識(shí),不過(guò)從來(lái)沒(méi)有想過(guò)這些高中書(shū)本的知識(shí)竟然運(yùn)用到了實(shí)際,非常的佩服kittenyang,同時(shí)感覺(jué)非常羞愧的,高中的知識(shí)都還給老師了,連正余弦的公式都忘記了。不熟悉的同學(xué)也可以去復(fù)習(xí)一下。

正弦型函數(shù)解析式:y=Asin(ωx+φ)+h
各常數(shù)值對(duì)函數(shù)圖像的影響:
φ(初相位):決定波形與X軸位置關(guān)系或橫向移動(dòng)距離(左加右減)
ω:決定周期(最小正周期T=2π/|ω|)
A:決定峰值(即縱向拉伸壓縮的倍數(shù))
h:表示波形在Y軸的位置關(guān)系或縱向移動(dòng)距離(上加下減)

拆解和分析

好了,我們還是來(lái)拆解一下這個(gè)動(dòng)畫(huà)吧。兩個(gè)波浪是兩個(gè)正弦函數(shù)的效果疊加。首先我們看看該如何繪制一個(gè)波的曲線(xiàn),如下圖:


329672-0cb75e6c6e4f120b.jpeg

我們知道,計(jì)算機(jī)不可能繪制出一條完美的曲線(xiàn),如果放大到像素的級(jí)別,可以看到這些曲線(xiàn)其實(shí)都是柵格的像素點(diǎn)組成。我們只能最大化的接近曲線(xiàn),達(dá)到肉眼無(wú)法分辨的程度。如果想繪制出來(lái)一條正弦函數(shù)曲線(xiàn),可以沿著假想的曲線(xiàn)繪制許多個(gè)點(diǎn),然后把點(diǎn)逐一用直線(xiàn)連在一起,如果點(diǎn)足夠多,就可以得到一條滿(mǎn)足需求的曲線(xiàn),這也是一種微分的思想。而這些點(diǎn)的位置可以通過(guò)正弦函數(shù)的解析式求得。

如果要繪制上面這個(gè)曲線(xiàn),可以觀(guān)察:波的峰值是1,周期是2π,初相位是0,h位移也是0。那么計(jì)算各個(gè)點(diǎn)的坐標(biāo)公式就是y = sin(x);獲得各個(gè)點(diǎn)的坐標(biāo)之后,使用CGPathAddLineToPoint這個(gè)函數(shù),把這些點(diǎn)逐一連成線(xiàn),就可以得到最后的路徑。

接下來(lái)問(wèn)題來(lái)了,我們已經(jīng)繪制了一條靜態(tài)的曲線(xiàn),如何讓它形成一個(gè)流動(dòng)的波呢?

可以這么思考:初始的曲線(xiàn)如上面所示,1s之后,希望曲線(xiàn)能成為下個(gè)形態(tài):

329672-d708b887c2bbf1ee.png

接著,2s、3s...,曲線(xiàn)分別在不停的變化,如下圖:


329672-c5856d9bc788eb54.png

那么隨著時(shí)間的流逝,這個(gè)曲線(xiàn)在不停的起伏變化,就形成了波動(dòng)的效果。我們認(rèn)真的想想,波動(dòng)其實(shí)就是每一個(gè)點(diǎn)的y坐標(biāo)都在不停的做著周期變化,想要實(shí)現(xiàn)上圖1s之后的曲線(xiàn)形態(tài),需要設(shè)置上面公式中的φ常量(初相位),假如φ是π/2,那么y=sin(x+φ)在x=0位置的時(shí)候,y的值就不在是0,而是1,就得到一條變化的曲線(xiàn)。通過(guò)上面的分析,我們知道,需要建立一個(gè)時(shí)間和φ的函數(shù)。

我們可以創(chuàng)建一個(gè)定時(shí)器(當(dāng)然做動(dòng)畫(huà)我們肯定不會(huì)使用計(jì)時(shí)器,這里舉個(gè)例子,下面詳解),假設(shè)每秒讓?duì)兆栽靓?2,這樣第4s的時(shí)候,φ等于2π(一個(gè)周期),y=sin(x+2π)和y=sin(x)等效,又回到了初初始狀態(tài),這樣就完成了一個(gè)波動(dòng)周期,往下繼續(xù)加下去,不停的往復(fù)這個(gè)波動(dòng)周期動(dòng)畫(huà)。

如果我們希望波動(dòng)的非常劇烈,也就是波流速很快,那么我們可以讓初相位隨著時(shí)間的函數(shù)波動(dòng)更快,就可以實(shí)現(xiàn)了。

代碼實(shí)現(xiàn)

把上面的原理落實(shí)到我們需要制作的動(dòng)畫(huà)上面。首先要總結(jié)出一個(gè)公式,確定正弦型函數(shù)解析式:y=Asin(ωx+φ)+h中各個(gè)常數(shù)的值。這里需要注意UIKit的坐標(biāo)系統(tǒng)y軸是向下延伸。

    1、我們的容器高度是100,我希望波的整體高度,固定在容器的一個(gè)相對(duì)的位置。
      這里設(shè)置h = 30;也就是說(shuō),當(dāng)Asin(ωx+φ)計(jì)算為0的時(shí)候,這個(gè)時(shí)候y的位置是30;
    2、決定波起伏的高度,我們?cè)O(shè)置波峰是5,波峰越大,曲線(xiàn)越陡峭;
    3、決定波的寬度和周期,比如,我們可以看到上面的例子中是一個(gè)周期的波曲線(xiàn),
      一個(gè)波峰、一個(gè)波谷,如果我們想在0到2π這個(gè)距離顯示2個(gè)完整的波曲線(xiàn),那么周期就是π。
      我們這里設(shè)置波的寬度是容器的寬度_waveWidth,希望能展示2.5個(gè)波曲線(xiàn),周期就是_waveWidth/2.5。
      那么ω常量就可以這樣計(jì)算:2.5*M_PI/_waveWidth。
    4、一共有兩個(gè)波曲線(xiàn),形成一個(gè)落差,也就是設(shè)置不同的φ(初相位),我們這里設(shè)置落差是M_PI/4。
    5、時(shí)間和初相位的函數(shù)關(guān)系:我們?cè)谟?jì)時(shí)器的函數(shù)中一直調(diào)用_offset += _speed;
      可以看到,如果我們?cè)O(shè)置波的速度speed越大,波的震動(dòng)將會(huì)越快。

    最后我們的公式如下:
    CGFloat y = _waveHeight*sinf(2.5*M_PI*i/_waveWidth + 3*_offset*M_PI/_waveWidth + M_PI/4) + _h;
    這些參數(shù)都可以自己調(diào)整,得到一個(gè)符合要求的效果。

現(xiàn)在我們解決了項(xiàng)目中最有難度的問(wèn)題,剩下的事情就非常簡(jiǎn)單了。兩個(gè)波是兩個(gè)CAShapeLayer。我們使用CADisplayLink而不是計(jì)時(shí)器來(lái)驅(qū)動(dòng)動(dòng)畫(huà),因?yàn)镃ADisplayLink觸發(fā)的時(shí)機(jī)是每隔一幀運(yùn)行一次,而NSTimer不是很精確,會(huì)有阻塞的情況,照成動(dòng)畫(huà)卡頓的現(xiàn)象。

- (void)wave
{
    _link = [CADisplayLink displayLinkWithTarget:self selector:@selector(doAni)];
    [_link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)doAni
{
    _offset += _speed;
    //設(shè)置第一條波曲線(xiàn)的路徑
    CGMutablePathRef pathRef = CGPathCreateMutable();
    //起始點(diǎn)
    CGFloat startY = _waveHeight*sinf(_offset*M_PI/_waveWidth);
    CGPathMoveToPoint(pathRef, NULL, 0, startY);
    //第一個(gè)波的公式
    for (CGFloat i = 0.0; i < _waveWidth; i ++) {
        CGFloat y = 1.1*_waveHeight*sinf(2.5*M_PI*i/_waveWidth + _offset*M_PI/_waveWidth) + _h;
        CGPathAddLineToPoint(pathRef, NULL, i, y);
    }
    CGPathAddLineToPoint(pathRef, NULL, _waveWidth, 40);
    CGPathAddLineToPoint(pathRef, NULL, 0, 40);
    CGPathCloseSubpath(pathRef);
    //設(shè)置第一個(gè)波layer的path
    _layer.path = pathRef;
    _layer.fillColor = [UIColor lightGrayColor].CGColor;
    CGPathRelease(pathRef);

    //設(shè)置第二條波曲線(xiàn)的路徑
    CGMutablePathRef pathRef2 = CGPathCreateMutable();
    CGFloat startY2 = _waveHeight*sinf(_offset*M_PI/_waveWidth + M_PI/4);
    CGPathMoveToPoint(pathRef2, NULL, 0, startY2);
    //第二個(gè)波曲線(xiàn)的公式
    for (CGFloat i = 0.0; i < _waveWidth; i ++) {
        CGFloat y = _waveHeight*sinf(2.5*M_PI*i/_waveWidth + 3*_offset*M_PI/_waveWidth + M_PI/4) + _h;
        CGPathAddLineToPoint(pathRef2, NULL, i, y);
    }
    CGPathAddLineToPoint(pathRef2, NULL, _waveWidth, 40);
    CGPathAddLineToPoint(pathRef2, NULL, 0, 40);
    CGPathCloseSubpath(pathRef2);

    _layer2.path = pathRef2;
    _layer2.fillColor = [UIColor lightGrayColor].CGColor;
    CGPathRelease(pathRef2);
}

我們可以看到,兩個(gè)波曲線(xiàn)不但初相位不同,形成一個(gè)落差,而且相位隨著時(shí)間的改變速度也不同,帶來(lái)兩個(gè)波的流速不同的視覺(jué)差異。CADisplayLink每幀都會(huì)調(diào)用wave方法,wave不停的改變著offset的值,也就是改變著初相位,最后形成了波動(dòng)動(dòng)畫(huà)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容