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 版本很快暴露出了嚴重的穩定性問題——幽靈火。
症狀非常詭異:
- 有時候我明明雙手離開了鍵盤,屏幕上的火卻突然燒了起來。
- 剛打開 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 可運行
有問題嗎