博客 / 詳情

返回

插件開發實錄:我用Comate在VS Code裏造了一場“能被代碼融化”的初雪

2025年的第一場雪,我是在報錯日誌裏度過的😭

朋友圈在曬雪景,我在盯着 VS Code 萬年不變的界面發呆。

既然錯過了現實裏的初雪,那我為什麼不能在我的 IDE 裏下一場雪?

於是,一個極其離譜又帶感的腦洞誕生了——我想做一個VS Code插件,實現:

  • 落雪:當我停下來思考或者單純摸魚時,讓屏幕飄落初雪,積雪慢慢覆蓋代碼,假裝我也在過冬。
  • 燃火:一旦我開始狂修 Bug,指尖的每一次敲擊都要在屏幕底部點燃烈火。手速越快,火勢越旺,甚至要讓火星子濺滿整個屏幕,主打一個物理取暖和辛勞可視化。
  • 互動:聖誕節快到了,在沒有操作的時候,跳出NPC和我互動,增添聖誕氛圍。

想法很豐滿,現實很骨感。

要在 VS Code 極其受限的 Webview 環境裏,同時跑通物理積雪堆疊算法和 Doom Fire 火焰渲染,還要保證不卡頓,這對我這個只有碎片時間的開發者來説,簡直是降維打擊。

好在,這個冬天,雖然沒有女朋友暖手,但我有隨叫隨到的 文心快碼(Comate) 暖心🐶

01 智能規劃:從一句話需求到 MVP落地

項目啓動階段,我面臨的最大挑戰是架構設計。VS Code 插件開發涉及 Extension 主進程與 Webview 渲染進程的通信,配置繁瑣且容易出錯。這次我沒有直接寫代碼,而是使用了 Comate 的 Spec 模式。

我向 Comate 拋出了我的核心構想:

“我要做一個 VS Code 插件,核心邏輯是監聽鍵盤輸入。輸入頻率高時底部渲染火焰,閒置時頂部下雪並積雪。請幫我設計架構並生成代碼。”

Comate 迅速進入了需求分析模式,幾秒鐘後,它返還了一份結構完整的 FrostFire 插件需求文檔。我仔細審視了這份文檔,發現大框架非常完美,邏輯層和渲染層分得清清楚楚。

插件需求文檔見文章末尾【附錄1】

為了把錯誤攔截在開發之前,我基於這份文檔進行了一些微調:

  • package.json:我補充了 activationEvents 配置,確保插件在打開任何文件時都能自動激活,而不僅僅是特定語言。
  • src/webview/index.html:我特別強調了要配置 CSP 策略,並將背景強制設為純黑,以配合 Canvas 的渲染效果。

確認修改後,Comate 自動根據需求文檔拆解出了具體的開發任務列表(Tasks),從環境配置到核心算法實現,條理清晰。

插件開發任務計劃見文章末尾【附錄2】

隨着一個個 Task 被自動執行,僅僅10分鐘,FrostFire 的 V1.0 版本(MVP)誕生了。

生成完畢後,Comate還給出了貼心的下一步引導,清晰地告訴了我們如何遷移到VS Code裏使用插件⬇️

我打開VS Code,按照它説的一步步做,屏幕上真的燃起了火焰!

👉觀看燃起火焰效果視頻https://mp.weixin.qq.com/s/zDAbDvEc-2-8yKNBEeY86A

對了,VS Code裏也支持文心快碼插件~插件市場搜索文心快碼,即可下載~

文心快碼插件下載下來後,會自動出生在左邊,有點和文件目錄重合了。沒關係,我們可以點擊左側項目欄中文心快碼標識,把它拖到右邊

👉觀看具體操作視頻https://mp.weixin.qq.com/s/zDAbDvEc-2-8yKNBEeY86A

02 核心維穩:用“記憶”根除鬼火Bug

然而,V1 版本很快暴露出了嚴重的穩定性問題——幽靈火。

症狀非常詭異:

  1. 有時候我明明雙手離開了鍵盤,屏幕上的火卻突然燒了起來。
  2. 剛打開 VS Code,還沒開始工作,火焰就直接鋪滿了屏幕。

經過排查,原來是後端的心跳包和自動保存機制干擾了 idleTime 的計算,導致系統誤判我有輸入。

為了解決這個問題,我與 Comate 進行了多次深度的 Debug 交互。最終,我們共同制定了一套基於趨勢判定的“安全栓”邏輯:只有當監測到閒置時間出現驟降(時間倒流)時,才認定為有效輸入,絕不能僅依賴數值大小來判斷。

這段邏輯非常關鍵,為了防止在未來的迭代中 Comate 忘記這個核心規則,我使用了 Comate 的 Memory(記憶) 功能。

我在 Memory 設置中手動錄入了一條核心指令:

“在 FrostFire 項目中,必須始終保留基於‘安全栓’(如 hasWitnessedDrop 變量)的邏輯:只有當檢測到 idleTime 確實發生下降(current < prev)時才允許解鎖點火功能,絕對不能僅依賴 idleTime的絕對數值來判定打字狀態,以防止‘剛打開或靜止時自動起火’的 Bug 復發。”

這一步操作至關重要。 從此之後,無論我要求 Comate 如何重構代碼,它都會死死守住這條“安全底線”。

當 Comate 生成了最終修復版的代碼後,我點擊了對話框下方的 “贊” 按鈕反饋了滿意度,並使用了 “全文複製”功能,一鍵將這段複雜的邏輯同步到了我的項目中。

03 迭代開發:從“能用”到“驚豔”

搞定了穩定性之後,FrostFire 雖然能跑了,但看起來還很廉價。我決定給它注入靈魂,開啓了三次關鍵的迭代。

迭代一:挑戰“物理積雪”算法

我希望雪花落下時,能像真實的沙堆一樣,形成中間高、兩邊低的自然坡度,而不是像水一樣平鋪。但這需要編寫複雜的“休止角”算法,涉及大量的數據結構計算。

我向 Comate 描述了需求:

“我需要積雪堆疊的感覺,最高堆疊到屏幕最上方。”

Comate 完美執行了我的指令。它在 snowEffect.js 中重構了數據結構,引入了一個雙向平滑算法。 現在的積雪,不僅有自然的起伏,甚至能把代碼編輯器底部的狀態欄慢慢“埋”起來,那種沉浸感簡直絕了。

迭代二:手感調優與温和模式

V1 的火焰太暴躁了,稍微打幾個字就滿屏火光。我希望它能更優雅一些,引入一種温和模式:平時只是小火苗,只有在瘋狂輸出時才會有火星四濺的效果。而且,單純的“不打字下雪,打字起火”太生硬了。我想要一種線性的對抗感。

怕智能體亂改改錯,我換成了Ask智能體,覺得AI説的有道理,就點插入,代碼就自動插入到了我的光標位置,體驗感十分絲滑。

迭代三:節日限定的浪漫

既然是聖誕特供,怎麼能少了節日氣氛?

我給 Comate 下達了新的增量需求:

“我需要增加兩個彩蛋。第一,當積雪變厚時,雪地裏要鑽出一個雪人;一旦我開始打字起火,雪人要表現得很驚恐並逃跑。第二,在火勢旺盛時,偶爾要有聖誕老人坐雪橇飛過,投遞禮物。”

Comate 的表現令人驚喜:

  • 怕熱的雪人:它設計了一個簡易的狀態機。雪人平時在發呆,檢測到熱量上升時,會繪製出流汗和顫抖的動畫,並加速移出屏幕。
  • 聖誕空投:它完美實現了雪橇的飛行軌跡和禮物盒的物理拋物線。

最讓我感動的是,Comate 還記得我之前的 Memory。在實現這些新功能時,它依然嚴格遵守了“安全栓”邏輯,確保雪人和聖誕老人不會因為誤觸而亂跑。

最後,讓我們一起來看看最終版本視頻🤩:https://mp.weixin.qq.com/s/zDAbDvEc-2-8yKNBEeY86A

04 總結:AI 時代的開發新範式

回看 FrostFire 的開發歷程,從解決底層的“幽靈火”邏輯漏洞,到實現細膩的物理積雪,再到充滿創意的聖誕彩蛋,文心快碼(Comate) 展現出的能力遠超一個代碼補全工具。

  • Spec 模式 讓架構設計不再是難題,將我的抽象想法快速轉化為可執行的代碼框架。
  • Memory 機制 解決了 AI 容易“遺忘上下文”的痛點,讓它成為了一個越用越懂我的專屬工程師。
  • 流暢的交互體驗(如可編輯輸入、一鍵複製)則極大地降低了溝通成本,讓開發過程如絲般順滑。

現在的 FrostFire,已經不僅僅是一個插件,它是我在 Comate 協助下,送給自己和所有開發者的一份冬日禮物。

在這個項目中,我只需負責構想和決策,而那些繁瑣的實現細節,全部交給了 AI。

如果你也想體驗這種心想事成的開發快感,不妨試試讓文心快碼成為你的AI編程助手。或許你的下一個腦洞,就是下一個爆款。

【附錄1】插件需求文檔

# FrostFire VS Code 插件需求文檔

## 1. 項目概述

**FrostFire** 是一個增加編程趣味性的 VS Code 視覺增強插件,通過在編輯器中渲染動態視覺效果(火焰和雪花),讓編程體驗更加生動有趣。

## 2. 核心功能

### 2.1 Fire 狀態(火焰效果)
- **觸發條件**:用户鍵盤輸入頻率(Keystrokes per minute, KPM)較高時觸發
- **視覺效果**:編輯器底部渲染像素風格火焰(基於 Doom Fire Algorithm)
- **動態響應**:輸入越快,火焰越高越旺盛
- **熱度計算**:根據最近一段時間內的按鍵頻率計算"熱度值"

### 2.2 Ice 狀態(雪花效果)
- **觸發條件**:用户停止輸入超過 15 秒
- **視覺效果**:編輯器頂部開始下雪
- **積雪機制**:停止輸入超過 60 秒後,雪花在底部積累形成積雪層
- **遮擋效果**:積雪可輕微遮擋代碼區域,增強沉浸感

### 2.3 狀態切換
- Fire 和 Ice 狀態互斥,不會同時出現
- 用户開始輸入時,雪花效果逐漸消失,火焰效果逐漸出現
- 狀態切換有平滑過渡動畫

## 3. 技術架構

### 3.1 技術棧
- **語言**:TypeScript
- **API**:VS Code Extension API
- **渲染**:HTML5 Canvas(在 Webview 中運行)

### 3.2 項目目錄結構
```
frostfire/
├── .vscode/
│   └── launch.json              # 調試配置
├── src/
│   ├── extension.ts             # 插件入口,註冊命令和監聽器
│   ├── activityTracker.ts       # 用户活躍度追蹤器
│   ├── webviewProvider.ts       # Webview 面板管理
│   └── webview/
│       ├── index.html           # Webview HTML 模板
│       ├── main.js              # Webview 主邏輯
│       ├── fireEffect.js        # Doom Fire 火焰算法實現
│       └── snowEffect.js        # 雪花和積雪效果實現
├── package.json                 # 插件配置清單
├── tsconfig.json                # TypeScript 配置
└── README.md                    # 項目説明
```

### 3.3 架構設計

```
┌─────────────────────────────────────────────────────────────┐
│                     VS Code Extension Host                   │
├─────────────────────────────────────────────────────────────┤
│  extension.ts                                                │
│  ┌─────────────────┐    ┌──────────────────┐                │
│  │ ActivityTracker │───▶│ WebviewProvider  │                │
│  │ - 監聽文檔變化   │    │ - 管理 Webview   │                │
│  │ - 計算熱度值     │    │ - 發送狀態消息    │                │
│  │ - 判斷狀態切換   │    │                  │                │
│  └─────────────────┘    └────────┬─────────┘                │
│                                  │ postMessage               │
└──────────────────────────────────┼──────────────────────────┘
                                   ▼
┌─────────────────────────────────────────────────────────────┐
│                        Webview (Canvas)                      │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────────┐    ┌──────────────────┐                │
│  │   FireEffect    │    │   SnowEffect     │                │
│  │ - Doom Fire算法  │    │ - 雪花粒子系統   │                │
│  │ - 火焰高度控制   │    │ - 積雪累積邏輯   │                │
│  └─────────────────┘    └──────────────────┘                │
│                    main.js (消息調度)                        │
└─────────────────────────────────────────────────────────────┘
```

## 4. 實現細節

### 4.1 ActivityTracker(活躍度追蹤器)

```typescript
// src/activityTracker.ts
export class ActivityTracker {
    private keystrokeTimestamps: number[] = [];  // 記錄按鍵時間戳
    private readonly WINDOW_SIZE = 60000;         // 統計窗口:60秒
    private readonly IDLE_THRESHOLD = 15000;      // 空閒閾值:15秒
    private readonly SNOW_ACCUMULATE_THRESHOLD = 60000; // 積雪閾值:60秒
    
    // 記錄一次按鍵
    public recordKeystroke(): void {
        const now = Date.now();
        this.keystrokeTimestamps.push(now);
        this.cleanOldTimestamps(now);
    }
    
    // 清理過期的時間戳
    private cleanOldTimestamps(now: number): void {
        this.keystrokeTimestamps = this.keystrokeTimestamps.filter(
            ts => now - ts < this.WINDOW_SIZE
        );
    }
    
    // 計算當前熱度值 (0-100)
    public getHeatLevel(): number {
        const now = Date.now();
        this.cleanOldTimestamps(now);
        // 基於最近10秒的按鍵數計算熱度
        const recentKeystrokes = this.keystrokeTimestamps.filter(
            ts => now - ts < 10000
        ).length;
        // 假設每秒6次按鍵為滿熱度
        return Math.min(100, (recentKeystrokes / 60) * 100);
    }
    
    // 獲取空閒時間(毫秒)
    public getIdleTime(): number {
        if (this.keystrokeTimestamps.length === 0) return Infinity;
        const lastKeystroke = Math.max(...this.keystrokeTimestamps);
        return Date.now() - lastKeystroke;
    }
    
    // 判斷當前狀態
    public getCurrentState(): 'fire' | 'idle' | 'snow' | 'snow_accumulate' {
        const idleTime = this.getIdleTime();
        if (idleTime >= this.SNOW_ACCUMULATE_THRESHOLD) return 'snow_accumulate';
        if (idleTime >= this.IDLE_THRESHOLD) return 'snow';
        if (this.getHeatLevel() > 10) return 'fire';
        return 'idle';
    }
}
```

### 4.2 Extension 核心邏輯

```typescript
// src/extension.ts
import * as vscode from 'vscode';
import { ActivityTracker } from './activityTracker';
import { FrostFireWebviewProvider } from './webviewProvider';

let activityTracker: ActivityTracker;
let webviewProvider: FrostFireWebviewProvider;
let updateInterval: NodeJS.Timeout;

export function activate(context: vscode.ExtensionContext) {
    activityTracker = new ActivityTracker();
    webviewProvider = new FrostFireWebviewProvider(context.extensionUri);
    
    // 註冊 Webview 視圖
    context.subscriptions.push(
        vscode.window.registerWebviewViewProvider(
            'frostfire.effectView',
            webviewProvider
        )
    );
    
    // 註冊啓動命令
    context.subscriptions.push(
        vscode.commands.registerCommand('frostfire.start', () => {
            vscode.commands.executeCommand('frostfire.effectView.focus');
        })
    );
    
    // 監聽文檔變化(用户輸入)
    context.subscriptions.push(
        vscode.workspace.onDidChangeTextDocument((event) => {
            // 只統計實際的文本變化
            if (event.contentChanges.length > 0) {
                activityTracker.recordKeystroke();
            }
        })
    );
    
    // 定時更新狀態到 Webview
    updateInterval = setInterval(() => {
        const state = activityTracker.getCurrentState();
        const heatLevel = activityTracker.getHeatLevel();
        const idleTime = activityTracker.getIdleTime();
        
        webviewProvider.postMessage({
            type: 'stateUpdate',
            state: state,
            heatLevel: heatLevel,
            idleTime: idleTime
        });
    }, 100); // 每100ms更新一次
    
    context.subscriptions.push({
        dispose: () => clearInterval(updateInterval)
    });
}

export function deactivate() {
    if (updateInterval) {
        clearInterval(updateInterval);
    }
}
```

### 4.3 Doom Fire 火焰算法

```javascript
// src/webview/fireEffect.js
class FireEffect {
    constructor(canvas) {
        this.canvas = canvas;
        this.ctx = canvas.getContext('2d');
        this.fireWidth = 80;  // 火焰像素寬度
        this.fireHeight = 50; // 火焰像素高度
        this.firePixels = [];
        this.fireColorPalette = this.createPalette();
        this.intensity = 0;   // 火焰強度 0-100
        this.initFire();
    }
    
    // 創建火焰顏色調色板(36色)
    createPalette() {
        return [
            {r:7,g:7,b:7}, {r:31,g:7,b:7}, {r:47,g:15,b:7}, {r:71,g:15,b:7},
            {r:87,g:23,b:7}, {r:103,g:31,b:7}, {r:119,g:31,b:7}, {r:143,g:39,b:7},
            {r:159,g:47,b:7}, {r:175,g:63,b:7}, {r:191,g:71,b:7}, {r:199,g:71,b:7},
            {r:223,g:79,b:7}, {r:223,g:87,b:7}, {r:223,g:87,b:7}, {r:215,g:95,b:7},
            {r:215,g:103,b:15}, {r:207,g:111,b:15}, {r:207,g:119,b:15}, {r:207,g:127,b:15},
            {r:207,g:135,b:23}, {r:199,g:135,b:23}, {r:199,g:143,b:23}, {r:199,g:151,b:31},
            {r:191,g:159,b:31}, {r:191,g:159,b:31}, {r:191,g:167,b:39}, {r:191,g:167,b:39},
            {r:191,g:175,b:47}, {r:183,g:175,b:47}, {r:183,g:183,b:47}, {r:183,g:183,b:55},
            {r:207,g:207,b:111}, {r:223,g:223,b:159}, {r:239,g:239,b:199}, {r:255,g:255,b:255}
        ];
    }
    
    // 初始化火焰數組
    initFire() {
        const totalPixels = this.fireWidth * this.fireHeight;
        this.firePixels = new Array(totalPixels).fill(0);
    }
    
    // 設置火焰強度
    setIntensity(level) {
        this.intensity = Math.max(0, Math.min(100, level));
        // 根據強度設置底部火源
        const maxColorIndex = Math.floor((this.intensity / 100) * 35);
        for (let x = 0; x < this.fireWidth; x++) {
            const bottomIndex = (this.fireHeight - 1) * this.fireWidth + x;
            this.firePixels[bottomIndex] = this.intensity > 5 ? maxColorIndex : 0;
        }
    }
    
    // 火焰傳播算法
    spreadFire() {
        for (let x = 0; x < this.fireWidth; x++) {
            for (let y = 1; y < this.fireHeight; y++) {
                const srcIndex = y * this.fireWidth + x;
                const pixel = this.firePixels[srcIndex];
                
                if (pixel === 0) {
                    this.firePixels[(y - 1) * this.fireWidth + x] = 0;
                } else {
                    // 隨機偏移產生飄動效果
                    const randIdx = Math.floor(Math.random() * 3);
                    const dstX = Math.min(this.fireWidth - 1, Math.max(0, x - randIdx + 1));
                    const dstIndex = (y - 1) * this.fireWidth + dstX;
                    this.firePixels[dstIndex] = Math.max(0, pixel - (randIdx & 1));
                }
            }
        }
    }
    
    // 渲染火焰
    render() {
        this.spreadFire();
        
        const pixelWidth = this.canvas.width / this.fireWidth;
        const pixelHeight = this.canvas.height / this.fireHeight;
        
        for (let y = 0; y < this.fireHeight; y++) {
            for (let x = 0; x < this.fireWidth; x++) {
                const colorIndex = this.firePixels[y * this.fireWidth + x];
                const color = this.fireColorPalette[colorIndex];
                
                if (colorIndex > 0) {
                    this.ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},0.9)`;
                    this.ctx.fillRect(
                        x * pixelWidth,
                        y * pixelHeight,
                        pixelWidth + 1,
                        pixelHeight + 1
                    );
                }
            }
        }
    }
}
```

### 4.4 雪花效果

```javascript
// src/webview/snowEffect.js
class SnowEffect {
    constructor(canvas) {
        this.canvas = canvas;
        this.ctx = canvas.getContext('2d');
        this.snowflakes = [];
        this.snowAccumulation = []; // 積雪層高度數組
        this.maxSnowflakes = 200;
        this.isSnowing = false;
        this.isAccumulating = false;
        this.initAccumulation();
    }
    
    // 初始化積雪層
    initAccumulation() {
        const segments = Math.floor(this.canvas.width / 5);
        this.snowAccumulation = new Array(segments).fill(0);
    }
    
    // 創建雪花
    createSnowflake() {
        return {
            x: Math.random() * this.canvas.width,
            y: -10,
            radius: Math.random() * 3 + 1,
            speed: Math.random() * 1 + 0.5,
            wind: Math.random() * 0.5 - 0.25,
            opacity: Math.random() * 0.5 + 0.5
        };
    }
    
    // 開始下雪
    startSnow(accumulate = false) {
        this.isSnowing = true;
        this.isAccumulating = accumulate;
    }
    
    // 停止下雪
    stopSnow() {
        this.isSnowing = false;
        this.isAccumulating = false;
    }
    
    // 清除效果
    clear() {
        this.snowflakes = [];
        this.initAccumulation();
        this.isSnowing = false;
        this.isAccumulating = false;
    }
    
    // 更新雪花位置
    update() {
        // 添加新雪花
        if (this.isSnowing && this.snowflakes.length < this.maxSnowflakes) {
            if (Math.random() < 0.3) {
                this.snowflakes.push(this.createSnowflake());
            }
        }
        
        // 更新雪花位置
        for (let i = this.snowflakes.length - 1; i >= 0; i--) {
            const flake = this.snowflakes[i];
            flake.y += flake.speed;
            flake.x += flake.wind;
            
            // 檢查是否落到底部或積雪上
            const segmentIndex = Math.floor(flake.x / 5);
            const groundLevel = this.canvas.height - (this.snowAccumulation[segmentIndex] || 0);
            
            if (flake.y >= groundLevel) {
                // 積雪
                if (this.isAccumulating && segmentIndex >= 0 && segmentIndex < this.snowAccumulation.length) {
                    this.snowAccumulation[segmentIndex] = Math.min(
                        this.snowAccumulation[segmentIndex] + 0.2,
                        this.canvas.height * 0.3 // 最大積雪高度30%
                    );
                }
                this.snowflakes.splice(i, 1);
            } else if (flake.x < 0 || flake.x > this.canvas.width) {
                this.snowflakes.splice(i, 1);
            }
        }
    }
    
    // 渲染雪花和積雪
    render() {
        this.update();
        
        // 繪製雪花
        this.ctx.fillStyle = 'white';
        for (const flake of this.snowflakes) {
            this.ctx.globalAlpha = flake.opacity;
            this.ctx.beginPath();
            this.ctx.arc(flake.x, flake.y, flake.radius, 0, Math.PI * 2);
            this.ctx.fill();
        }
        this.ctx.globalAlpha = 1;
        
        // 繪製積雪
        if (this.isAccumulating) {
            this.ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
            this.ctx.beginPath();
            this.ctx.moveTo(0, this.canvas.height);
            
            for (let i = 0; i < this.snowAccumulation.length; i++) {
                const x = i * 5;
                const y = this.canvas.height - this.snowAccumulation[i];
                this.ctx.lineTo(x, y);
            }
            
            this.ctx.lineTo(this.canvas.width, this.canvas.height);
            this.ctx.closePath();
            this.ctx.fill();
        }
    }
}
```

### 4.5 Webview 主邏輯

```javascript
// src/webview/main.js
(function() {
    const vscode = acquireVsCodeApi();
    const canvas = document.getElementById('effectCanvas');
    const ctx = canvas.getContext('2d');
    
    let fireEffect, snowEffect;
    let currentState = 'idle';
    let animationId;
    
    // 初始化
    function init() {
        resizeCanvas();
        fireEffect = new FireEffect(canvas);
        snowEffect = new SnowEffect(canvas);
        animate();
    }
    
    // 調整畫布大小
    function resizeCanvas() {
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
    }
    
    // 動畫循環
    function animate() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        
        if (currentState === 'fire') {
            fireEffect.render();
        } else if (currentState === 'snow' || currentState === 'snow_accumulate') {
            snowEffect.render();
        }
        
        animationId = requestAnimationFrame(animate);
    }
    
    // 處理來自擴展的消息
    window.addEventListener('message', event => {
        const message = event.data;
        
        if (message.type === 'stateUpdate') {
            handleStateUpdate(message);
        }
    });
    
    // 處理狀態更新
    function handleStateUpdate(message) {
        const newState = message.state;
        
        if (newState !== currentState) {
            // 狀態切換
            if (newState === 'fire') {
                snowEffect.clear();
            } else if (newState === 'snow' || newState === 'snow_accumulate') {
                fireEffect.setIntensity(0);
            }
            currentState = newState;
        }
        
        // 更新效果參數
        if (currentState === 'fire') {
            fireEffect.setIntensity(message.heatLevel);
        } else if (currentState === 'snow') {
            snowEffect.startSnow(false);
        } else if (currentState === 'snow_accumulate') {
            snowEffect.startSnow(true);
        } else {
            fireEffect.setIntensity(0);
            snowEffect.stopSnow();
        }
    }
    
    // 窗口大小改變時重新調整
    window.addEventListener('resize', () => {
        resizeCanvas();
        if (snowEffect) snowEffect.initAccumulation();
    });
    
    // 啓動
    init();
})();
```

## 5. 邊界條件與異常處理

### 5.1 輸入邊界
- 熱度值範圍:0-100,超出範圍自動截斷
- 空閒時間:使用 Infinity 表示從未輸入
- 按鍵時間戳數組:定期清理過期數據,避免內存泄漏

### 5.2 狀態切換
- 狀態切換時清理前一狀態的視覺殘留
- 使用平滑過渡避免突兀感

### 5.3 性能優化
- 火焰像素矩陣使用固定大小(80x50),通過縮放渲染到實際尺寸
- 雪花數量上限 200 個,避免性能問題
- 使用 requestAnimationFrame 保證動畫流暢

### 5.4 Webview 生命週期
- Webview 隱藏時暫停動畫
- Webview 銷燬時清理資源
- 重新顯示時恢復狀態

## 6. 數據流動路徑

```
用户輸入 → onDidChangeTextDocument → ActivityTracker.recordKeystroke()
                                            ↓
                                    計算熱度值/空閒時間
                                            ↓
                              定時器(100ms) → getCurrentState()
                                            ↓
                              WebviewProvider.postMessage()
                                            ↓
                              Webview 接收消息 → handleStateUpdate()
                                            ↓
                              更新 FireEffect/SnowEffect 參數
                                            ↓
                              requestAnimationFrame → render()
```

## 7. 預期成果

完成後的 MVP 應具備:
1. ✅ 一鍵啓動視覺效果面板
2. ✅ 實時響應用户輸入,渲染火焰效果
3. ✅ 空閒時自動切換到雪花效果
4. ✅ 長時間空閒產生積雪效果
5. ✅ 流暢的動畫和狀態切換
6. ✅ 詳細的代碼註釋,便於理解和擴展

【附錄2】插件開發任務計劃

# FrostFire VS Code 插件開發任務計劃

- [ ] 任務 1:初始化項目結構與配置文件
    - 1.1: 創建 `package.json`,配置插件元信息、activationEvents、commands 和 views(側邊欄註冊)
    - 1.2: 創建 `tsconfig.json`,配置 TypeScript 編譯選項
    - 1.3: 創建 `.vscode/launch.json`,配置調試啓動項
    - 1.4: 創建 `.vscode/tasks.json`,配置 watch 編譯任務
    - 1.5: 創建 `README.md`,説明項目用途和使用方法

- [ ] 任務 2:實現 ActivityTracker 活躍度追蹤模塊
    - 2.1: 創建 `src/activityTracker.ts`,定義 ActivityTracker 類
    - 2.2: 實現 `recordKeystroke()` 方法,記錄按鍵時間戳
    - 2.3: 實現 `getHeatLevel()` 方法,計算熱度值(0-100)
    - 2.4: 實現 `getIdleTime()` 方法,計算空閒時間
    - 2.5: 實現 `getCurrentState()` 方法,判斷當前狀態(fire/idle/snow/snow_accumulate)

- [ ] 任務 3:實現 WebviewProvider 面板管理模塊
    - 3.1: 創建 `src/webviewProvider.ts`,定義 FrostFireWebviewProvider 類
    - 3.2: 實現 `resolveWebviewView()` 方法,創建並配置 Webview
    - 3.3: 實現 `getHtmlContent()` 方法,生成包含 Canvas 和腳本的 HTML
    - 3.4: 實現 `postMessage()` 方法,向 Webview 發送狀態更新消息

- [ ] 任務 4:實現插件主入口 extension.ts
    - 4.1: 創建 `src/extension.ts`,實現 `activate()` 函數
    - 4.2: 註冊 WebviewViewProvider 到 frostfire.effectView
    - 4.3: 註冊 frostfire.start 和 frostfire.stop 命令
    - 4.4: 添加 `onDidChangeTextDocument` 監聽器,記錄用户輸入
    - 4.5: 設置定時器(100ms),定期向 Webview 發送狀態更新
    - 4.6: 實現 `deactivate()` 函數,清理資源

- [ ] 任務 5:實現 Webview 前端效果(火焰效果)
    - 5.1: 創建 `src/webview/fireEffect.js`,定義 FireEffect 類
    - 5.2: 實現 `createPalette()` 方法,創建 36 色火焰調色板
    - 5.3: 實現 `setIntensity()` 方法,根據熱度設置火焰強度
    - 5.4: 實現 `spreadFire()` 方法,Doom Fire 傳播算法
    - 5.5: 實現 `render()` 方法,渲染火焰到 Canvas

- [ ] 任務 6:實現 Webview 前端效果(雪花效果)
    - 6.1: 創建 `src/webview/snowEffect.js`,定義 SnowEffect 類
    - 6.2: 實現 `createSnowflake()` 方法,生成隨機雪花粒子
    - 6.3: 實現 `startSnow()` / `stopSnow()` / `clear()` 控制方法
    - 6.4: 實現 `update()` 方法,更新雪花位置和積雪層
    - 6.5: 實現 `render()` 方法,渲染雪花和積雪到 Canvas

- [ ] 任務 7:實現 Webview 主邏輯與 HTML 模板
    - 7.1: 創建 `src/webview/main.js`,實現消息監聽和狀態調度
    - 7.2: 實現 `handleStateUpdate()` 方法,根據狀態切換效果
    - 7.3: 實現動畫循環 `animate()`,使用 requestAnimationFrame
    - 7.4: 創建 `src/webview/index.html`,純黑背景 Canvas 模板

- [ ] 任務 8:安裝依賴並編譯驗證
    - 8.1: 執行 `npm install` 安裝 TypeScript 和 VS Code 類型定義
    - 8.2: 執行 `npm run compile` 編譯 TypeScript
    - 8.3: 檢查編譯輸出,確保無報錯
    - 8.4: 提供調試啓動指南,確認 MVP 可運行
有問題嗎
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.