Tap and go 加載動畫
2021-08-01 # 教學

上面派錢啦。然而呢,tap and go 的 loading 實在煩人。於是仿著畫一個。

成果圖

起手式

我猜官方應該用的是 css 做的動畫。這裏寫的是用 P5js ,語言是 js 只能說是模仿,不能說一樣。

//p5 js 的起手式
function setup() {}

function draw() {}

分解動作

三顆球

第一印象就是有三顆球在跳。

第二印象是在頂跟底都會變扁。

第三印象是排列位置似乎是打斜一條直線。

基於上述三個印象就有了初步的雛形。

首先先搞三個球。

[PS 背景之所以改成黑色的原因是,現在為標準時間凌晨四點四十。]


const width = 200;
const height = 200;
const padding = 50;
const r = 25;

function setup() {
    createCanvas(width, height);
    fill("orange");
    stroke("orange");
    strokeWeight(3);
}
function draw() {
    background("black");
    ellipse(padding, (height * 2) / 3 , r , r );
    ellipse(width / 2, (height * 2) / 3 , r , r );
    ellipse(width - padding, (height * 2) / 3 , r , r );
}

黑色背景每個 frame 都會把畫板刷黑從而做到動畫效果。至於為什麼這三個球的位置在「這裏」,純粹隨緣而已。

上竄下跳的三顆球

接下來要讓他們動了。基本常識都知道,要讓物件上下跳,就要改變他的 Y 座標值。

考慮到這次有三個球,若每一個球都賦予一個或幾個 variable 的話,相信 code 的長度會爆炸。所以這裏可以用array 來儲存每一個球該有的 variable. 這裡只有三個球,用 OOP 或許有點大材小用了。OOP 適合更多更頻繁生成的環境,例如煙花、花瓣、模擬生活中常見的系統等等。

const width = 200;
const height = 200;
const padding = 50;
const r = 25;
const l = width / 2;
var loopCounters = [r, parseInt((height * 2) / 3), parseInt(((height * 2) / 3 + r) / 2)];
const increment = 3;
var increments = [-increment, increment, increment];

function setup() {
    createCanvas(width, height);
    fill("orange");
    stroke("orange");
    strokeWeight(3);
}
function draw() {
    background("white");
    ellipse(padding, padding + loopCounters[0] - r, r, r);
    ellipse(width / 2, padding + loopCounters[1] - r, r , r);
    ellipse(width - padding, padding + loopCounters[2] - r, r , r);
    for (var i = 0; i < 3; i++) {
        if (loopCounters[i] <= 0) increments[i] = increment;
        if (loopCounters[i] >= parseInt((height * 2) / 3)) increments[i] = -increment;
        loopCounters[i] += increments[i];
    }
}

考慮到之後要做壓扁,有一個 0 -> A -> 0 -> A … 的序列會有所幫助。這裏的做法是球球的x座標不變,而y座標與 loopCounters 成正比關係改變。而球球的上下,取決於 increments 的正負值。increments 的正負值在每一次更新 loopCounters 時做出判定。當球移動出固定範圍(在這裡設定為0 - (height * 2) / 3 之間)時,increments 的正負值做出逆轉動作。

壓扁

這部分有點難,花了點智商與時間。

這裡需要留意的是扁的時機跟扁的比例。

const width = 200;
const height = 200;
const padding = 50;
const r = 25;
var loopCounters = [r, parseInt((height * 2) / 3), parseInt(((height * 2) / 3 + r) / 2)];
const increment = 3;
var increments = [-increment, increment, increment];
var ballVar = [0, 0, 0];
const ballVarMax = 10;

function setup() {
    createCanvas(width, height);
    fill("orange");
    stroke("orange");
    strokeWeight(3);
}
function draw() {
    background("black");
    ellipse(padding, padding + loopCounters[0] - r, r + ballVar[0], r - ballVar[0] / 2);
    ellipse(width / 2, padding + loopCounters[1] - r, r + ballVar[1], r - ballVar[1] / 2);
    ellipse(width - padding, padding + loopCounters[2] - r, r + ballVar[2], r - ballVar[2] / 2);
    for (var i = 0; i < 3; i++) {
        if (loopCounters[i] < ballVarMax) ballVar[i] = ballVarMax - loopCounters[i];
        else if (loopCounters[i] >= parseInt((height * 2) / 3) - ballVarMax) ballVar[i] = loopCounters[i] - parseInt((height * 2) / 3) + ballVarMax;
        else ballVar[i] = 0;
        if (loopCounters[i] <= 0) increments[i] = increment;
        if (loopCounters[i] >= parseInt((height * 2) / 3)) increments[i] = -increment;
        loopCounters[i] += increments[i];
    }
}

前一步埋下的伏筆終於要派上用場了。

在前一步裡,loopCounters 是以 0 -> A -> 0 -> A … 的序列存在的。 於是乎便有了兩個臨界點,一個是 0 一個是 A 。而這裡的做法是,ballVarMax -> 0 -> ballVarMax 這段序列跟 (height * 2) / 3) - ballVarMax -> (height * 2) / 3) -> (height * 2) / 3) - ballVarMax 這段序列形成了逐漸加大/減小的樣式。

接下來就看怎麼利用這兩條序列了。

一般情況下,我們想要球球是圓的,所以設定ballVar為0。而經過算式,最後得到的是0 -> ballVarMax -> 0 序列儲存在 ballVar 裡。

如此一來就可以塞進畫球的指令裡。

留意此處的第三第四個項目分別代表長寬,按照比例帶入就可以得到壓扁效果。

飄移進度條

這裡需要單獨把進度條跟中間的球球抽出來講。仔細觀察後發現,原版的進度條只在中間的球球下降的時候出現。

const width = 200;
const height = 200;
const padding = 50;
const r = 25;
var loopCounters = [r, parseInt((height * 2) / 3), parseInt(((height * 2) / 3 + r) / 2)];
const increment = 3;
var increments = [-increment, increment, increment];
var ballVar = [0, 0, 0];
const ballVarMax = 10;

function setup() {
    createCanvas(width, height);
    fill("orange");
    stroke("orange");
    strokeWeight(3);
}
function draw() {
    background("black");
    ellipse(width / 2, padding + loopCounters[1] - r, r + ballVar[1], r - ballVar[1] / 2);
    if (loopCounters[1] < ballVarMax) ballVar[1] = ballVarMax - loopCounters[1];
    else if (loopCounters[1] >= parseInt((height * 2) / 3) - ballVarMax) ballVar[1] = loopCounters[1] - parseInt((height * 2) / 3) + ballVarMax;
    else ballVar[1] = 0;
    if (loopCounters[1] <= 0) increments[1] = increment;
    if (loopCounters[1] >= parseInt((height * 2) / 3)) increments[1] = -increment;
    loopCounters[1] += increments[1];
    if (increments[1] == increment) {
        const offset = map(loopCounters[1], 0, parseInt((height * 2) / 3), -width, width);
        line(offset, height - 5, width / 2 + offset, height - 5);
    }
}

有鑑於此,線/「進度條」只需要在當increments為正時出現並更新。而移動範圍橫跨整個畫面。

完整代碼

const width = 200;
const height = 200;
const padding = 50;
const r = 25;
var loopCounters = [r, parseInt((height * 2) / 3), parseInt(((height * 2) / 3 + r) / 2)];
const increment = 3;
var increments = [-increment, increment, increment];
var ballVar = [0, 0, 0];
const ballVarMax = 10;

function setup() {
    createCanvas(width, height);
    fill("orange");
    stroke("orange");
    strokeWeight(3);
}
function draw() {
    background("white");
    ellipse(padding, padding + loopCounters[0] - r, r + ballVar[0], r - ballVar[0] / 2);
    ellipse(width / 2, padding + loopCounters[1] - r, r + ballVar[1], r - ballVar[1] / 2);
    ellipse(width - padding, padding + loopCounters[2] - r, r + ballVar[2], r - ballVar[2] / 2);
    for (var i = 0; i < 3; i++) {
        if (loopCounters[i] < ballVarMax) ballVar[i] = ballVarMax - loopCounters[i];
        else if (loopCounters[i] >= parseInt((height * 2) / 3) - ballVarMax) ballVar[i] = loopCounters[i] - parseInt((height * 2) / 3) + ballVarMax;
        else ballVar[i] = 0;
        if (loopCounters[i] <= 0) increments[i] = increment;
        if (loopCounters[i] >= parseInt((height * 2) / 3)) increments[i] = -increment;
        loopCounters[i] += increments[i];
    }
    if (increments[1] == increment) {
        const offset = map(loopCounters[1], 0, parseInt((height * 2) / 3), -width, width);
        line(offset, height - 5, width / 2 + offset, height - 5);
    }
}

結語

上面派的錢也收到了,tap and go 的咖啡也喝夠了。手癢癢就寫了這麼個動畫。現在時間,五點三十,早上。不知道一覺醒來還買不買得到switch。

[當天下午更新]

買到了:) 開森。某air 藍芽好用,低延遲。