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 版本很快暴露出了严重的稳定性问题------幽灵火。
症状非常诡异:
- 有时候我明明双手离开了键盘,屏幕上的火却突然烧了起来。
- 刚打开 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 可运行
有问题吗









