插件开发实录:我用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,按照它说的一步步做,屏幕上真的燃起了火焰!

👉观看燃起火焰效果视频mp.weixin.qq.com/s/zDAbDvEc-...

对了,VS Code里也支持文心快码插件~插件市场搜索文心快码,即可下载~

文心快码插件下载下来后,会自动出生在左边,有点和文件目录重合了。没关系,我们可以点击左侧项目栏中文心快码标识,把它拖到右边

👉观看具体操作视频mp.weixin.qq.com/s/zDAbDvEc-...

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。在实现这些新功能时,它依然严格遵守了"安全栓"逻辑,确保雪人和圣诞老人不会因为误触而乱跑。

最后,让我们一起来看看最终版本视频🤩:mp.weixin.qq.com/s/zDAbDvEc-...

04 总结:AI 时代的开发新范式

回看 FrostFire 的开发历程,从解决底层的"幽灵火"逻辑漏洞,到实现细腻的物理积雪,再到充满创意的圣诞彩蛋,文心快码(Comate) 展现出的能力远超一个代码补全工具。

  • Spec 模式 让架构设计不再是难题,将我的抽象想法快速转化为可执行的代码框架。
  • Memory 机制 解决了 AI 容易"遗忘上下文"的痛点,让它成为了一个越用越懂我的专属工程师。
  • 流畅的交互体验(如可编辑输入、一键复制)则极大地降低了沟通成本,让开发过程如丝般顺滑。

现在的 FrostFire,已经不仅仅是一个插件,它是我在 Comate 协助下,送给自己和所有开发者的一份冬日礼物。

在这个项目中,我只需负责构想和决策,而那些繁琐的实现细节,全部交给了 AI。

如果你也想体验这种心想事成的开发快感,不妨试试让文心快码成为你的AI编程助手。或许你的下一个脑洞,就是下一个爆款。

【附录1】插件需求文档

ini 复制代码
# 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】插件开发任务计划

yaml 复制代码
# 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 可运行
有问题吗
相关推荐
嘻哈baby2 小时前
搞了三年运维,这些脚本我天天在用
前端
inCBle2 小时前
vue2 封装一个自动校验是否溢出的 tooltip 自定义指令
前端·javascript·vue.js
austin流川枫2 小时前
深度解析六大Java微服务框架
java·后端·微服务
掘金安东尼2 小时前
⏰前端周刊第444期(2025年12月8日–12月14日)
前端
martin10172 小时前
基于Spring Boot + Thymeleaf + Flying Saucer实现PDF导出功能
后端
weixin_448119942 小时前
Datawhale Hello-Agents入门篇202512第2次作业
java·前端·javascript
程序员爱钓鱼2 小时前
Node.js 编程实战:路由与中间件
前端·后端·node.js
程序员爱钓鱼2 小时前
Node.js 编程实战:Express 基础
前端·后端·node.js
CosMr2 小时前
【QT】【FFmpeg】 Qt 中FFmpeg环境搭建以及D__STDC_FORMAT_MACROS、PRId64解答
后端