點(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ù)
VertexIn
和VertexOut
很普通,包含頂點(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ù)要做的效果斟酌使用式散。