点击关注公众号,“技术干货” 及时达!
《黑神话:悟空》最近很火,明天就要正式上线了!小明我为了提前感受一下这款游戏的激燃氛围,做了个 VSCode 小扩展,让你在写代码时也能体验到《黑神话:悟空》里的打斗快感。
接下来,我会带你一步步实现这个插件,让你的编程旅程不再单调!
原理
这个插件的核心原理是利用 VSCode 的 TextEditorDecorationType
来展示逐帧动画。由于 VSCode 不支持直接播放 MP4 视频,我们需要先将视频转成图片序列,然后通过装饰的方式来显示这些图片。
具体操作是这样的:我们先用 ffmpeg
命令将视频拆解成一帧帧的图片,命令如下:
ffmpeg -i ./wukong.mp4 -vf "fps=10,scale=300:150" -compression_level 9 ./res/frames/frame%03d.png
这个命令会把 wukong.mp4
视频按每秒 10 帧的速度导出成 300x150 像素的图片,存放在 res/frames
文件夹下。然后,我们在 VSCode 中利用这些图片逐帧展示动画,营造出打字时打斗的效果。
接下来,我会带你一步步实现这个有趣的插件,让你的编程过程变得更有意思!
第一步:初始化项目
首先,我们要创建一个新的 VSCode 插件项目。如果你还没有设置好开发环境,可以参考 VSCode 官方文档 来创建你的第一个插件项目。
假设你已经创建了一个插件项目,现在我们直接进入编码阶段。
第二步:预加载动画帧
在展示动画之前,我们需要先加载所有的动画帧。这里假设我们有 100 帧图片,存放在插件的 res/frames
文件夹下。
首先,我们来写一个函数,用来加载这些帧图片并存储到内存中,方便后续使用。
const frameCache: { [key: number]: string } = {}
async function preloadFrames(extensionPath: string) {
for (let i = 1; i <= 100; i++) {
const frameUrl = getFrameUrl(i, extensionPath)
frameCache[i] = frameUrl
}
}
这个函数的逻辑很简单:我们遍历 100 个帧图片,依次读取并存储到 frameCache
对象中。这样在后面播放动画时,就可以直接使用缓存好的图片,而不用每次都去读取文件。
其中的 getFrameUrl
函数负责生成每一帧图片的路径,并将其转换为 Base64 格式的 URL:
function getFrameUrl(frameNumber: number, extensionPath: string): string {
const paddedFrameNumber = frameNumber.toString().padStart(3, '0')
const imagePath = path.join(extensionPath, 'res', 'frames', `frame${paddedFrameNumber}.png`)
try {
const imageBuffer = fs.readFileSync(imagePath)
const base64Image = imageBuffer.toString('base64')
return `data:image/png;base64,${base64Image}`
} catch (error) {
console.error(`读取图片失败: ${imagePath}`, error)
return ''
}
}
这里我们通过 fs.readFileSync
读取每一帧图片,然后将其转换为 Base64 格式,这样就可以直接在 HTML 中使用图片数据。
第三步:监听输入事件
有了预加载的动画帧,现在我们需要监听用户在编辑器中的输入事件,每当用户输入代码时,就触发动画播放。
vscode.workspace.onDidChangeTextDocument(event => {
if (event.contentChanges.length > 0) {
updateAnimation(event.document.uri)
resetInactivityTimer()
incrementComboCount()
}
})
当文档内容发生变化时,我们会调用 updateAnimation
来更新动画。同时,我们还重置了用户不活动计时器,并增加连击计数。
第四步:展示动画
核心部分来了!我们要通过 VSCode 的 TextEditorDecorationType
API 来在编辑器中显示动画。
let animationDecoration: vscode.TextEditorDecorationType | undefined
let currentFrame = 1
async function showNextFrame(editor: vscode.TextEditor) {
if (!isAnimating) return
const frameUrl = frameCache[currentFrame]
if (frameUrl) {
if (animationDecoration) {
animationDecoration.dispose()
}
animationDecoration = vscode.window.createTextEditorDecorationType({
after: {
contentText: comboCount > 0 ? `${comboCount}x` : '',
margin: '0 0 0 1em',
width: '200px',
height: '100px',
textDecoration: `
none;
position: absolute;
left: -0.5rem;
top: 2rem;
z-index: 1;
pointer-events: none;
background-image: url(/cover?u=ZnV4NkdPS3JYK0NmVk1HcEJ3RHpwZz09);
background-size: 100% 100%;
background-repeat: no-repeat;
`
},
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed
})
const lastLine = editor.document.lineAt(editor.document.lineCount - 1)
const range = new vscode.Range(lastLine.range.end, lastLine.range.end)
editor.setDecorations(animationDecoration, [{ range }])
currentFrame = (currentFrame % 100) + 1
}
}
这里的 showNextFrame
函数负责将动画帧显示在编辑器中。通过设置 TextEditorDecorationType
,我们可以自定义每一帧动画的样式和位置。
第五步:处理动画的停止和连击
为了让动画更有趣,我们加入了连击的概念,并且在用户停止输入一段时间后自动停止动画。
let isAnimating = false
let inactivityTimer: NodeJS.Timeout | undefined
let comboCount = 0
function resetInactivityTimer() {
if (inactivityTimer) clearTimeout(inactivityTimer)
inactivityTimer = setTimeout(() => {
stopAnimation()
resetComboCount()
}, 3000) // 3秒后停止动画并重置连击计数
}
function stopAnimation() {
isAnimating = false
if (animationDecoration) {
animationDecoration.dispose()
animationDecoration = undefined
}
}
function incrementComboCount() {
comboCount++
showNextFrame(vscode.window.activeTextEditor!)
}
function resetComboCount() {
comboCount = 0
showNextFrame(vscode.window.activeTextEditor!)
}
resetInactivityTimer
会在用户停止输入超过 3 秒后停止动画,并重置连击计数。这样,我们就不会一直占用资源来播放动画了。
第六步:将插件激活到 VSCode 中
现在,我们的动画逻辑已经写好了。接下来,我们要把它整合到 VSCode 插件的主入口中。
import * as vscode from 'vscode'
import { activateFightAnimation } from './fightAnimation'
export const activate = async (context: vscode.ExtensionContext) => {
try {
console.log('"VSCode Funny" 插件已激活!')
// 激活打斗动画功能
activateFightAnimation(context)
} catch (err) {
console.warn('插件激活失败', err)
}
}
在插件的 activate
函数中,我们调用 activateFightAnimation
来启动我们的打字动画功能。
最后一步:配置和运行插件
至此,插件的代码已经完成。你可以通过以下步骤来运行并测试这个插件:
打开 VSCode,进入插件的项目目录。 运行 npm install
来安装依赖。使用 F5
启动插件调试环境。在调试窗口中输入代码,享受打字战斗动画带来的乐趣!
结语:彩蛋时间 🐣
这个插件不仅能让你在敲代码时体验到《黑神话》的程序吗喽热血,还“故意”留了一个 bug——动画的位置 bug。
如果你对这个扩展感兴趣,或者想亲自体验一下这个插件,可以去我的 GitHub 仓库看看:https://github.com/nicepkg/vscode-funny
点击关注公众号,“技术干货” 及时达!