前言
最近在做魔杖(横刀版)项目,想搞个比较帅的光效。但是我的刀只安装了一条灯带,许多效果就没法实现了,上网查了很多资料也没有相关的文章,问 gpt 更是啥也没有,于是自己写了个思路,仅供参考。
btw,我的灯带是 34 * 1 的,也就是有34个灯,下面叙述的步骤都以34灯为基准
核心思路
将一个视频压缩成34 * 1像素,不就可以搬上去了吗
步骤
-
找到一个雷电效果的视频
-
使用剪映将其处理为 340 * 100px 的视频,并将主元素(闪电)放在正中间,滤色掉无用的地方,并做一些拉伸
- 前置的准备素材到这里就够了,后续的代码直接贴上:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>视频帧分析</title>
<style>
.square {
width: 10px;
height: 10px;
display: inline-block; /* 使用块级元素展示 */
}
</style>
</head>
<body>
<video id="video" width="340" height="100" controls>
<source src="./test.mp4" type="video/mp4" />
您的浏览器不支持视频标签。
</video>
<canvas id="canvas" style="display: none"></canvas>
<canvas id="binaryCanvas" style="display: none"></canvas>
<div id="finalAnimation"></div>
<script>
const video = document.getElementById("video");
const canvas = document.getElementById("canvas");
const binaryCanvas = document.getElementById("binaryCanvas");
const context = canvas.getContext("2d");
const binaryContext = binaryCanvas.getContext("2d");
const finalAnimation = document.getElementById("finalAnimation");
const frameInterval = 1; // 每隔10帧分析一次
const binaryThreshold = 128; // 二值化阈值
const frameResults = []; // 存储每帧分析结果
const colors = []; // 存储颜色等级
// 生成颜色等级
for (let i = 0; i <= 10; i++) {
// 11个等级
const green = 255; // 固定绿色值
const blue = 255 - i * 20; // 蓝色值逐渐降低
colors.push(`rgb(255, ${green}, ${blue})`);
}
video.addEventListener("play", () => {
const fps = 60;
const interval = 1000 / fps; // 计算每帧间隔
frameResults.length = 0; // 清空结果数组
const intervalId = setInterval(() => {
if (video.paused || video.ended) {
clearInterval(intervalId);
displayResults(); // 播放结束时显示结果
return;
}
// 每隔frameInterval帧进行一次分析
if (Math.floor(video.currentTime * fps) % frameInterval === 0) {
analyzeFrame();
}
}, interval);
});
function analyzeFrame() {
if (video.ended) return;
const width = video.videoWidth;
const height = video.videoHeight;
canvas.width = width;
canvas.height = height;
binaryCanvas.width = width;
binaryCanvas.height = height;
context.drawImage(video, 0, 0, width, height); // 绘制当前帧
const imageData = context.getImageData(0, 0, width, height);
const data = imageData.data;
const binaryNonBlackPixels = new Array(34).fill(0); // 记录每个段的非黑色像素数量
// 二值化处理
const binaryImageData = new Uint8ClampedArray(data.length);
for (let i = 0; i < data.length; i += 4) {
const brightness =
0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2];
const binaryValue = brightness > binaryThreshold ? 255 : 0;
binaryImageData[i] = binaryValue;
binaryImageData[i + 1] = binaryValue;
binaryImageData[i + 2] = binaryValue;
binaryImageData[i + 3] = 255; // 设置 alpha 通道
}
binaryContext.putImageData(
new ImageData(binaryImageData, width, height),
0,
0
);
// 统计每个段的非黑色像素数量
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const index = (y * width + x) * 4;
if (binaryImageData[index] === 255) {
const segment = Math.floor(x / (width / 34)); // 根据宽度划分段
binaryNonBlackPixels[segment]++;
}
}
}
// 根据非黑色像素数量进行分类
const maxPixels = Math.max(...binaryNonBlackPixels);
const levelThresholds = Array.from(
{ length: 11 },
(_, i) => (maxPixels / 10) * i
); // 生成等级阈值
const categorizedPixels = binaryNonBlackPixels.map((count) => {
if (count === 0) return 0;
return levelThresholds.findIndex((threshold) => count <= threshold);
});
frameResults.push(categorizedPixels); // 存储当前帧的结果
}
function displayResults() {
// 打印整个结果数组的倒序格式
const reversedFrameResults = frameResults.map((frame) =>
frame.slice().reverse()
); // 倒序每一项,使用 slice() 保持原数组不变
// 转换为 C 语言格式字符串
const cStyleOutput = `uint8_t lightningAnimation[][34] = {\n${reversedFrameResults
.map((frame) => `{ ${frame.join(", ")} }`)
.join(",\n")}\n};`;
console.log(cStyleOutput); // 打印 C 语言格式的结果数组
if (finalAnimation.firstChild) {
finalAnimation.removeChild(finalAnimation.firstChild); // 清除上一次的结果
}
if (frameResults.length > 0) {
const finalSquaresContainer = document.createElement("div");
frameResults[0].forEach(() => {
const square = document.createElement("div");
square.className = "square";
finalSquaresContainer.appendChild(square);
});
finalAnimation.appendChild(finalSquaresContainer); // 添加方块容器
let frameIndex = 0;
const finalAnimationInterval = setInterval(() => {
if (frameIndex < frameResults.length) {
const currentFrame = frameResults[frameIndex];
finalSquaresContainer.childNodes.forEach((square, index) => {
square.style.backgroundColor = colors[currentFrame[index]]; // 更新方块颜色
});
frameIndex++;
} else {
clearInterval(finalAnimationInterval); // 停止动画
}
}, 300);
}
}
// 根据视频播放时间更新方块颜色
video.addEventListener("timeupdate", () => {
if (finalAnimation.firstChild && frameResults.length > 0) {
const frameIndex = Math.floor(
(video.currentTime * 60) / frameInterval
);
if (frameIndex < frameResults.length) {
const currentFrame = frameResults[frameIndex];
finalAnimation.firstChild.childNodes.forEach((square, index) => {
if (index < currentFrame.length) {
square.style.backgroundColor = colors[currentFrame[index]]; // 更新方块颜色
}
});
}
}
});
</script>
</body>
</html>
来稍微讲解一下这段代码做了哪些事情:
- 将视频放入网页中
- 对视频的每一帧,都进行处理
- 二值化,将图片变为黑白方便后续操作
- 二值化后,将图片横向分为10份,每份10px
- 统计每份中,白点像素出现的数量
- 根据白点数量,将其划分为10个等级
- 当处理完所有帧后,我们就得到了一个视频每帧的等级数组
uint8_t lightningAnimation[][34] = {
{ 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 2, 8, 10, 10, 9, 5, 2, 0, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 2, 8, 10, 10, 9, 5, 2, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 3, 7, 10, 9, 9, 7, 8, 7, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 3, 7, 10, 9, 9, 7, 8, 7, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
// ...此处省略若干行
};
- 让这34个灯按数组顺序亮起,并保持切换速度为 1000/60,这样我们的灯带就得到了对应的效果了
// 此处应该有个视频演示,但是掘金视频发不上来,就这样吧