筆記:js控制動畫

分享和讨论Javascript相关的话题
回复
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4464
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 172 次
被圈友点赞: 200 次
联系:

筆記:js控制動畫

帖子 ejsoon »

代码: 全选

在 JavaScript 中使用**函數遞歸**來控制動畫,最常見且推薦的方式是搭配 **`requestAnimationFrame`** 來實現,而不是用純粹的同步遞歸(因為同步遞歸會造成呼叫堆疊爆炸)。

以下是幾種常見的寫法,從簡單到實用,逐步說明:

### 1. 最經典寫法(推薦大多數情況使用)

```javascript
let isRunning = false;

function animate() {
    // 這裡放每一幀要執行的內容
    console.log("正在動畫中...");

    // 畫面更新邏輯,例如:
    // box.style.transform = `translateX(${progress}px)`;

    if (isRunning) {
        requestAnimationFrame(animate);   // 自己呼叫自己 → 形成遞歸
    }
}

// 啟動動畫
function startAnimation() {
    if (!isRunning) {
        isRunning = true;
        requestAnimationFrame(animate);
    }
}

// 停止動畫
function stopAnimation() {
    isRunning = false;
}

// 使用範例
startAnimation();

// 3秒後停止
setTimeout(stopAnimation, 3000);
```

這是最常見的「**自我遞歸 + 旗標控制**」寫法。

### 2. 帶時間控制 & 進度計算(最推薦實戰寫法)

```javascript
let startTime = null;
const duration = 2000; // 動畫持續 2 秒

function animate(timestamp) {
    // 第一次執行時記錄開始時間
    if (!startTime) startTime = timestamp;
    
    // 計算已經經過的時間比例 (0 ~ 1)
    const elapsed = timestamp - startTime;
    const progress = Math.min(elapsed / duration, 1);  // 不超過1

    // 套用緩動函數(可選)
    // const eased = progress ** 2;           // 二次緩入
    // const eased = 1 - (1 - progress) ** 3; // 強緩出
    const eased = progress;                   // 線性

    // 實際更新畫面
    const moveDistance = 300 * eased;
    box.style.transform = `translateX(${moveDistance}px)`;

    // 還沒結束就繼續下一幀
    if (progress < 1) {
        requestAnimationFrame(animate);
    } else {
        console.log("動畫結束");
        // 可在此呼叫結束回調
    }
}

// 啟動
requestAnimationFrame(animate);
```

### 3. 更乾淨的類別封裝寫法(推薦中大型專案)

```javascript
class Animator {
    constructor(element) {
        this.element = element;
        this.isRunning = false;
        this.startTime = null;
        this.duration = 1500;
        this.from = 0;
        this.to = 300;
    }

    // 緩動函數可自行替換
    ease(t) {
        return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
    }

    animate = (timestamp) => {
        if (!this.startTime) this.startTime = timestamp;

        const t = (timestamp - this.startTime) / this.duration;
        const progress = Math.min(t, 1);
        const eased = this.ease(progress);

        const value = this.from + (this.to - this.from) * eased;
        this.element.style.transform = `translateX(${value}px)`;

        if (progress < 1 && this.isRunning) {
            requestAnimationFrame(this.animate);
        } else if (progress >= 1) {
            console.log("動畫完成");
        }
    }

    start() {
        if (this.isRunning) return;
        this.isRunning = true;
        this.startTime = null;
        requestAnimationFrame(this.animate);
    }

    stop() {
        this.isRunning = false;
    }

    reset() {
        this.stop();
        this.element.style.transform = `translateX(${this.from}px)`;
    }
}

// 使用
const box = document.querySelector('.box');
const animator = new Animator(box);

animator.start();

// 想停止就呼叫
// animator.stop();
```

### 4. 什麼時候才會真的用「純遞歸」而不搭配 rAF?

極少數特殊場景,例如:

- 一次性的階層式展開/收合動畫(樹狀菜單、accordion)
- 逐步顯示的文字逐字動畫(但通常也會搭配 setTimeout)

範例(逐字顯示):

```javascript
function typeText(text, element, speed = 80, i = 0) {
    if (i < text.length) {
        element.textContent += text[i];
        setTimeout(() => {
            typeText(text, element, speed, i + 1);
        }, speed);
    }
}

typeText("Hello! 這是用遞歸實現的打字機效果~", document.querySelector('.text'));
```

### 總結:常見控制動畫的方式對比

| 方式                  | 是否推薦     | 優點                              | 缺點                        | 使用場景                  |
|-----------------------|--------------|------------------------------------|-----------------------------|---------------------------|
| requestAnimationFrame + 遞歸   | ★★★★★       | 流暢、省電、跟螢幕刷新率同步      | 需要手動控制開始/停止       | 幾乎所有自訂 JS 動畫      |
| setInterval           | ★★☆☆☆       | 簡單                               | 不跟螢幕同步、容易掉幀      | 簡單計時器、非視覺動畫    |
| setTimeout 遞歸       | ★★★☆☆       | 容易控制間隔                       | 不夠平滑、耗能較高          | 逐字、逐行、延遲觸發      |
| CSS transition/animation      | ★★★★★       | 效能最好、硬體加速                 | 控制彈性較低                | 一般 UI 動畫              |
| Web Animations API    | ★★★★☆       | 原生、精確控制                     | 瀏覽器支援度仍稍差          | 需要精確回調的高級動畫    |

**最推薦的日常寫法就是第2或第3種**(帶時間計算的 rAF 遞歸)。

有沒有具體想做的動畫效果?  
例如:位移、淡入淡出、旋轉、畫布粒子、SVG 描邊、打字機、拋物線……我可以直接給你對應的完整範例程式碼。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4464
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 172 次
被圈友点赞: 200 次
联系:

Re: 筆記:js控制動畫

帖子 ejsoon »

代码: 全选

**cancelAnimationFrame** 是用來「取消」之前用 `requestAnimationFrame` 排程的下一幀執行的 API。

它的主要作用只有一個:

**在你還沒執行到下一幀之前,把它從瀏覽器的待執行清單中移除掉**

### cancelAnimationFrame 的基本用法

```js
let rafId = null;

function animate() {
    // 動畫邏輯...
    rafId = requestAnimationFrame(animate);
}

// 啟動
rafId = requestAnimationFrame(animate);

// 想要停止時
function stop() {
    if (rafId !== null) {
        cancelAnimationFrame(rafId);
        rafId = null;           // 很重要!清空 ID
    }
}
```

### 常見問題:真的有必要每次都呼叫 cancelAnimationFrame 嗎?

實際專案中最常見的兩種寫法對比如下:

| 寫法                          | 是否呼叫 cancelAnimationFrame | 實際行為                              | 推薦程度    | 適用場景                              |
|-------------------------------|--------------------------------|----------------------------------------|-------------|---------------------------------------|
| 用旗標控制(if isRunning)     | 不呼叫                         | 下一幀仍然會執行一次才真正停止         | ★★★★★      | 99% 的日常動畫                        |
| 每次都 cancel + 重新 request   | 每次都呼叫                     | 真正立刻停止,不會有多餘一幀           | ★★☆☆☆      | 對「多餘一幀」極度敏感的場合          |
| 切換頁面/組件卸載時            | 強烈建議呼叫                   | 避免在隱藏的 tab / 已卸載組件繼續跑    | 必須        | React/Vue 組件 unmount、切 tab        |
| 短暫暫停後又要繼續              | 通常呼叫一次就夠               | 避免累積多個 raf 排程                  | ★★★★☆      | 暫停/恢復、拖曳中斷後繼續等           |

### 現實中最常見的結論(2024–2025 年的實務共識)

**大多數情況下,你不需要每次都呼叫 cancelAnimationFrame**  
只要用一個旗標(或狀態)來控制是否繼續遞迴就足夠了。

```js
// 這種寫法在 95%+ 的場景都足夠好
let isActive = true;

function loop() {
    if (!isActive) return;
    
    // 動畫邏輯...
    
    requestAnimationFrame(loop);
}

function stop() {
    isActive = false;
    // 這裡可以選擇不寫 cancelAnimationFrame
}
```

### 什麼時候「真的應該」呼叫 cancelAnimationFrame?

1. 組件即將卸載(React useEffect cleanup、Vue onUnmounted)
2. 切換到另一個畫面/場景,舊動畫完全不需要再跑
3. tab 切走後不想浪費任何性能(雖然瀏覽器會自動節流,但還是會排程)
4. 你正在做「非常精確的時間控制」或「幀級精確同步」的東西
5. 同一個元素/畫布同時有多個動畫在競爭(避免多個 raf 同時跑)

### 快速記憶口訣

- 平常寫動畫 → **旗標控制就夠了,不用 cancel**
- 組件要死掉、畫面要切走 → **一定要 cancelAnimationFrame**
- 對多餘一幀極度敏感(遊戲、音樂視覺化、精確物理模擬) → **考慮 cancel**

簡單一句話總結:

**「一般日常 UI 動畫」不需要每次都 cancelAnimationFrame,用旗標就足夠;  
但「生命週期管理」和「資源清理」的場合,強烈建議搭配使用。**

有沒有正在寫的某個具體動畫場景?可以告訴我,我幫你判斷這個動畫需不需要搭配 cancel。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

正浏览此版面之用户: 没有注册用户 和 0 访客