iOS特效之仿Mac窗口最小化的神奇效果

點(diǎn)擊獲取本文示例代碼

我希望我可以成為占位圖

前言

這次仿照Mac窗口最小化時(shí)的神奇效果(官方的中文版本是這么叫的卷仑,聽起來(lái)很尷尬)瘸羡,做了一個(gè)iOS版本的。基礎(chǔ)代碼都沿用自iOS特效之破碎的ViewController沛硅。先來(lái)看一下效果圖。

原理

首先要分析一下官方的動(dòng)畫是如何進(jìn)行的,下面是效果的截圖。動(dòng)畫分為兩步消别,先是將圖片扭曲成下面的樣子,然后再吸入到左側(cè)台谢。想要做圖片扭曲寻狂,用一個(gè)nxm的3D網(wǎng)格就可以了。n和m越大朋沮,扭曲后得到的邊緣越平滑蛇券。



在上圖的基礎(chǔ)上加入一個(gè)坐標(biāo)軸,這樣便于觀察規(guī)律樊拓。

在動(dòng)畫執(zhí)行過(guò)程中纠亚,網(wǎng)格上的點(diǎn)會(huì)沿著一個(gè)方向縮放,我們稱縮放的軸為縮放軸骑脱,圖中的縮放軸是y軸菜枷。同時(shí)還需要在縮放軸上指定一個(gè)縮放中心點(diǎn)苍糠。在動(dòng)畫的第二個(gè)階段叁丧,所有點(diǎn)會(huì)沿著一個(gè)方向移動(dòng),我們稱這個(gè)軸為移動(dòng)軸,圖中的移動(dòng)軸是x軸拥娄。

動(dòng)畫第一階段

在動(dòng)畫的第一個(gè)階段中蚊锹,網(wǎng)格上的點(diǎn)只在縮放軸上移動(dòng)。我們假設(shè)一個(gè)點(diǎn)在移動(dòng)軸上的位置為movLoc,那么我們可以使用公式0.5 * 0.98 * cos(3.14 * movLoc + 3.14) + 0.5 + 0.01;計(jì)算出第一階段結(jié)束時(shí)稚瘾,該點(diǎn)需要向縮放中心點(diǎn)縮放的量牡昆。為什么是這個(gè)公式呢,我給大家貼一張圖就清楚了摊欠。是不是和上面的邊緣曲線有點(diǎn)像丢烘。圖我是用Mac自帶的Grapher繪制的。在調(diào)試曲線的過(guò)程中Grapher的確非常好用些椒。公式里的0.98和0.01是相關(guān)的兩個(gè)量播瞳,控制左邊窄口的大小。0.01 = (1 - 0.98) / 2免糕。動(dòng)畫第一階段主要的工作就是根據(jù)當(dāng)前動(dòng)畫的進(jìn)度百分比赢乓,控制點(diǎn)到達(dá)最終縮放量的進(jìn)度即可。

動(dòng)畫第二階段

第二階段主要就是移動(dòng)軸上的移動(dòng)石窑,我們可以根據(jù)最遠(yuǎn)移動(dòng)距離和當(dāng)前的動(dòng)畫進(jìn)度計(jì)算出當(dāng)前點(diǎn)在移動(dòng)軸上的位置牌芋。然后根據(jù)當(dāng)前的位置計(jì)算出縮放軸上需要的縮放量。最遠(yuǎn)距離可以通過(guò)吸入點(diǎn)和另一側(cè)的邊界計(jì)算出來(lái)松逊。

Shader

了解完原理我們來(lái)看Shader代碼吧躺屁。Swift代碼比較簡(jiǎn)單,只是生成了一個(gè)撐滿屏幕的nxm網(wǎng)格棺棵,稍候再說(shuō)楼咳。

傳入Shader的數(shù)據(jù)

VertexInVertexOut很普通,包含頂點(diǎn)位置和紋理坐標(biāo)烛恤。Uniforms里包含了動(dòng)畫相關(guān)的信息母怜,當(dāng)前動(dòng)畫經(jīng)過(guò)的時(shí)間animationElapsedTime,動(dòng)畫總時(shí)間animationTotalTime缚柏,吸入點(diǎn)gatherPoint苹熏。

struct VertexIn
{
    packed_float3  position;
    packed_float2  texcoord;
};

struct VertexOut
{
    float4  position [[position]];
    float2  texcoord;
};

struct Uniforms
{
    float animationElapsedTime;
    float animationTotalTime;
    packed_float3 gatherPoint;
};

動(dòng)畫實(shí)現(xiàn)

動(dòng)畫的實(shí)現(xiàn)都在Vertex Shader里。步驟如下币喧。

  • 計(jì)算并規(guī)范動(dòng)畫進(jìn)度轨域,得到動(dòng)畫進(jìn)度animationPercent
VertexOut outVertex;
VertexIn inVertex = vertexIn[vid];
float animationPercent = uniforms.animationElapsedTime / uniforms.animationTotalTime;
animationPercent = animationPercent > 1.0 ? 1.0 : animationPercent;
  • 求解移動(dòng)軸scaleAxis和縮放軸moveAxis杀餐,以及最遠(yuǎn)移動(dòng)距離干发。我們可以通過(guò)移動(dòng)軸scaleAxis和縮放軸moveAxis獲取點(diǎn)或者向量對(duì)應(yīng)軸的分量。
// 求解縮放軸和移動(dòng)軸
float moveMaxDisplacement = 2.0; // 最遠(yuǎn)移動(dòng)位移史翘,帶符號(hào)
int scaleAxis = 0; // 默認(rèn)縮放軸為X
int moveAxis = 1;   // 默認(rèn)移動(dòng)軸為Y枉长,即沿著y方向吸入的效果
if (uniforms.gatherPoint[0] <= -1 || uniforms.gatherPoint[0] >= 1) {
    scaleAxis = 1;
    moveAxis = 0;
}
if (uniforms.gatherPoint[moveAxis] >= 0) {
    moveMaxDisplacement = uniforms.gatherPoint[moveAxis] + 1;
} else {
    moveMaxDisplacement = uniforms.gatherPoint[moveAxis] - 1;
  • 定義第一階段動(dòng)畫在總動(dòng)畫中的占比冀续。
// 動(dòng)畫第一階段的時(shí)間占比
float animationFirstStagePercent = 0.4;
  • 計(jì)算移動(dòng)軸的動(dòng)畫當(dāng)前執(zhí)行到的進(jìn)度moveAxisAnimationPercent,在第一階段執(zhí)行完之前必峰,這個(gè)值一直是0洪唐。
// 移動(dòng)軸的動(dòng)畫只有在第一階段結(jié)束后才開始進(jìn)行。
float moveAxisAnimationPercent = (animationPercent - animationFirstStagePercent) / (1.0 - animationFirstStagePercent);
moveAxisAnimationPercent = moveAxisAnimationPercent < 0.0 ? 0.0 : moveAxisAnimationPercent;
moveAxisAnimationPercent = moveAxisAnimationPercent > 1.0 ? 1.0 : moveAxisAnimationPercent;
  • 根據(jù)點(diǎn)在移動(dòng)軸上規(guī)范化后的坐標(biāo)計(jì)算縮放量的最終值吼蚁。在第一階段時(shí)凭需,根據(jù)最終縮放量和當(dāng)前動(dòng)畫進(jìn)度計(jì)算當(dāng)前的縮放量scaleAxisCurrentValue。第二階段時(shí)肝匆,直接使用最終縮放量粒蜈,因?yàn)榇藭r(shí)縮放量只和移動(dòng)軸上坐標(biāo)有關(guān)。
// 用于縮放軸計(jì)算縮放量的因子
float scaleAxisFactor = abs(uniforms.gatherPoint[moveAxis] - (inVertex.position[moveAxis] + moveMaxDisplacement *
moveAxisAnimationPercent)) / abs(moveMaxDisplacement);
float scaleAxisAnimationEndValue = 0.5 * 0.98 * cos(3.14 * scaleAxisFactor + 3.14) + 0.5 + 0.01;
float scaleAxisCurrentValue = 0;
if (animationPercent <= animationFirstStagePercent) {
    scaleAxisCurrentValue = 1 +  (scaleAxisAnimationEndValue - 1) * animationPercent / animationFirstStagePercent;
} else {
    scaleAxisCurrentValue = scaleAxisAnimationEndValue;
}
  • 根據(jù)移動(dòng)軸上動(dòng)畫的進(jìn)度moveAxisAnimationPercent和縮放軸的縮放量scaleAxisCurrentValue計(jì)算最終頂點(diǎn)的位置旗国。
float newMoveAxisValue = inVertex.position[moveAxis] + moveMaxDisplacement * moveAxisAnimationPercent;
float newScaleAxisValue = inVertex.position[scaleAxis] - (inVertex.position[scaleAxis] - uniforms.gatherPoint[scaleAxis]) * (1 - scaleAxisCurrentValue);

float3 newPosition = float3(0, 0, inVertex.position[2]);
newPosition[moveAxis] = newMoveAxisValue;
newPosition[scaleAxis] = newScaleAxisValue;
outVertex.position = float4(newPosition, 1.0);
outVertex.texcoord = inVertex.texcoord;
return outVertex;

Vertex Shader到此就結(jié)束了薪伏,F(xiàn)ragment Shader很簡(jiǎn)單,采樣粗仓,返回顏色嫁怀。

constexpr sampler s(coord::normalized, address::repeat, filter::linear);

fragment float4 passThroughFragment(VertexOut inFrag [[stage_in]],
                                     texture2d<float> diffuse [[ texture(0) ]],
                                    const device Uniforms& uniforms [[ buffer(0) ]])
{
    float4 finalColor = diffuse.sample(s, inFrag.texcoord);
    return finalColor;
};

Swift代碼

Swift代碼里基本重用破碎效果的代碼,在MagicalEffectView.swift中借浊,最核心的代碼也就是構(gòu)建網(wǎng)格這一段了塘淑。

private func buildMesh() -> [Float] {
    let viewWidth: Float = Float(UIScreen.main.bounds.width)
    let viewHeight: Float = Float(UIScreen.main.bounds.height)
    let meshCols: Int = 10;//Int(viewWidth / Float(meshUnitSizeInPixel.width));
    let meshRows: Int = meshCols * Int(viewHeight / viewWidth);//Int(viewHeight / Float(meshUnitSizeInPixel.height));
    let meshUnitSizeInPixel: CGSize = CGSize.init(width: CGFloat(viewWidth / Float(meshCols)), height: CGFloat(viewHeight /
Float(meshRows))) // 每個(gè)mesh單元的大小
    let sizeXInMetalTexcoord = Float(meshUnitSizeInPixel.width) / viewWidth * 2;
    let sizeYInMetalTexcoord = Float(meshUnitSizeInPixel.height) / viewHeight * 2;
    var vertexDataArray: [Float] = []
    for row in 0..<meshRows {
        for col in 0..<meshCols {
            let startX = Float(col) * sizeXInMetalTexcoord - 1.0;
            let startY = Float(row) * sizeYInMetalTexcoord - 1.0;
            let point1: [Float] = [startX, startY, 0.0, Float(col) / Float(meshCols), Float(row) / Float(meshRows)];
            let point2: [Float] = [startX + sizeXInMetalTexcoord, startY, 0.0, Float(col + 1) / Float(meshCols), Float(row) /
Float(meshRows)];
            let point3: [Float] = [startX + sizeXInMetalTexcoord, startY + sizeYInMetalTexcoord, 0.0, Float(col + 1) /
Float(meshCols), Float(row + 1) / Float(meshRows)];
            let point4: [Float] = [startX, startY + sizeYInMetalTexcoord, 0.0, Float(col) / Float(meshCols), Float(row + 1) /
Float(meshRows)];
            
            vertexDataArray.append(contentsOf: point3)
            vertexDataArray.append(contentsOf: point2)
            vertexDataArray.append(contentsOf: point1)
            
            vertexDataArray.append(contentsOf: point3)
            vertexDataArray.append(contentsOf: point1)
            vertexDataArray.append(contentsOf: point4)
        }
    }
    return vertexDataArray
}

根據(jù)網(wǎng)格單元格的大小,構(gòu)建頂點(diǎn)位置和UV數(shù)組蚂斤。還有就是對(duì)Uniforms進(jìn)行了修改存捺。包含動(dòng)畫相關(guān)的信息。

struct Uniforms {
    var animationElapsedTime: Float = 0.0
    var animationTotalTime: Float = 0.6
    var gatherPointX: Float = 0.8
    var gatherPointY: Float = -1.0
    var gatherPointZ: Float = 0.0
    
    func data() -> [Float] {
        return [animationElapsedTime, animationTotalTime, gatherPointX, gatherPointY, gatherPointZ];
    }
    
    static func sizeInBytes() -> Int {
        return 5 * MemoryLayout<Float>.size
    }
}

其他自定義Transition動(dòng)畫的代碼和之前一樣曙蒸,基本沒動(dòng)過(guò)捌治。

總結(jié)

這種看似復(fù)雜的動(dòng)畫,可以把它拆解成幾個(gè)簡(jiǎn)單的階段纽窟,分開處理肖油。對(duì)于每個(gè)階段里復(fù)雜的運(yùn)動(dòng),可以把運(yùn)動(dòng)拆分到不同的軸上臂港,然后為每個(gè)軸上的運(yùn)動(dòng)規(guī)律推導(dǎo)公式森枪。和上學(xué)時(shí)解題的思路還是很像的。使用網(wǎng)格制作動(dòng)畫相對(duì)于之前的點(diǎn)精靈审孽,更加靈活县袱,但是需要的頂點(diǎn)量也偏多∮恿Γ可以根據(jù)要做的效果斟酌使用式散。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市打颤,隨后出現(xiàn)的幾起案子暴拄,更是在濱河造成了極大的恐慌宛畦,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件揍移,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡反肋,警方通過(guò)查閱死者的電腦和手機(jī)那伐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)石蔗,“玉大人罕邀,你說(shuō)我怎么就攤上這事⊙啵” “怎么了诉探?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)棍厌。 經(jīng)常有香客問我肾胯,道長(zhǎng),這世上最難降的妖魔是什么耘纱? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任敬肚,我火速辦了婚禮,結(jié)果婚禮上束析,老公的妹妹穿的比我還像新娘艳馒。我一直安慰自己,他們只是感情好员寇,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布弄慰。 她就那樣靜靜地躺著,像睡著了一般蝶锋。 火紅的嫁衣襯著肌膚如雪陆爽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天扳缕,我揣著相機(jī)與錄音墓陈,去河邊找鬼。 笑死第献,一個(gè)胖子當(dāng)著我的面吹牛贡必,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播庸毫,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼仔拟,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了飒赃?” 一聲冷哼從身側(cè)響起利花,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤科侈,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后炒事,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體臀栈,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年挠乳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了权薯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡睡扬,死狀恐怖盟蚣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情卖怜,我是刑警寧澤屎开,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站马靠,受9級(jí)特大地震影響奄抽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜甩鳄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一如孝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧娩贷,春花似錦第晰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至储笑,卻和暖如春甜熔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背突倍。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工腔稀, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人羽历。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓焊虏,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親秕磷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子诵闭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容