插件开发实录:我用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 可运行
有问题吗
相关推荐
GetcharZp4 小时前
玩转 Linux 机器视觉:手把手带你搞定 Ubuntu 下海康工业相机 C++ SDK
后端
橙子家5 小时前
浏览器缓存之【基础键值存储】:Local storage 和 Session storage
前端
星星在线7 小时前
MusicFree:一个「All in One」的个人音乐服务器,让听歌回归简单
前端·后端
IT_陈寒8 小时前
Redis的SETNX并发问题让我加了三天班
前端·人工智能·后端
demo007x8 小时前
Docling 文档转换以及技术架构分析
前端·后端·程序员
京东云开发者9 小时前
京东市民服务又“上新”!这次是黑龙江“龙易办”
前端
袋鱼不重9 小时前
我的神奇同事,AI 用多了居然写了个 Open In Codex
前端·后端·ai编程
用户83562907805110 小时前
使用 Python 操作 Word 内容控件
后端·python
像我这样帅的人丶你还10 小时前
啥? 前端也要会干Java?🛵🛵🛵
后端
Hommy8810 小时前
【剪映小助手】添加贴纸接口(Add Sticker)
后端·github·剪映小助手·视频剪辑自动化·剪映api