緊急筆記:js控制動畫(AI回答)

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

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

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

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. 帶時間控制 & 進度計算(最推薦實戰寫法)

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. 更乾淨的類別封裝寫法(推薦中大型專案)

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)

範例(逐字顯示):

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 描邊、打字機、拋物線……我可以直接給你對應的完整範例程式碼。

Leave a Comment