# 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.tsexport 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.tsimport * 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.jsclass 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.jsclass 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. ✅ 详细的代码注释,便于理解和扩展
评论