在各位眼中肤白貌美大长腿的小姐姐,在我们程序员眼里都长啥样呢,没错就是长封面这样,为什么长这样,下面本小白来带各位看官去一探究竟
整体思路
首先实现这个功能需要准备两个canvas标签和一个video标签,video标签用来播放视频资源,监听video的某个事件,将视频按帧拆为图片,然后逐帧绘制在canvas1,利用canvas像素操作的API获取图片的像素数据,对像素数据进行处理,然后逐帧绘制在canvas2上。
具体实现
video需要做的事情
play事件中去递归调用requestAnimationFrame来逐帧渲染
- player为video标签的dom对象,handleDataCanvas为绘制video图像的canvas,
//监听video的播放事件
player.addEventListener(
"play",
() => {
//根据视频的宽高比按比例缩小计算出canvas1的宽高
handleDataCanvas.width = 50;
handleDataCanvas.height =((50 / 1.5) * player.videoHeight) / player.videoWidth;
const render = () => {
// 自定义事件render
const event = new CustomEvent("render");
// 触发自定义事件
player.dispatchEvent(event);
if (!player.ended && !player.paused) {
//判断video不是结束或者暂停时,递归调用requestAnimationFrame(为了提高性能)
requestAnimationFrame(render);
}
};
requestAnimationFrame(render);
},
false
);
自定义事件中去调用核心方法draw
player.addEventListener(
"render",
() => {
draw();
},
false
);
这里需要注意:
canvas
获取像素数据的API遵循同源策略,如果加载的视频资源涉及跨域问题,需要给video
的crossOrigin
属性设置为''
;
核心代码
在这个方法中,canvas1获取像素数据,然后将获取到的像素数据进行处理,之后绘制到canvas2
处理像素
gray为像素灰度,根据公式R * 0.299 + B * 0.587 + G * 0.114;
,将RGB带入公式计算得来
imgData为像素数据格式为:
{
width:..,
height:...,
data:[],
}
其中width为水平方向像素点个数,height为垂直方向像素点个数,data则是一个一维数组,每三个元素表示一个像素的RGB值,是这些像素从左到右从上到下的一维描述
const pixelsTransform = (imgData) => {
const pixelsData = imgData.data;
const width = imgData.width;
const height = imgData.height;
const charData = [];
// 使用两个循环遍历将一维数组转化为二维数组,这个二维数组中的每一个元素都是一行像素
for (let row = 0; row < height; row++) {
const rowData = []; // 行数据
for (let col = 0; col < width; col++) {
const R = pixelsData[(width * row + col) * 4 + 0];
const G = pixelsData[(width * row + col) * 4 + 1];
const B = pixelsData[(width * row + col) * 4 + 2];
const gray = R * 0.299 + B * 0.587 + G * 0.114; // 计算灰度
rowData.push({ gray, R, G, B }); //点数据装到行数据
}
charData.push(rowData);// 组装为二维数组
}
let map = ["#", "&", "$", "*", "!", ".", " "];
//这里字符排列顺序也有讲究,按照复杂-->简单或者简单-->复杂的书序排列,对应着不同灰度
return charData.map((row) => {
return row.map(({ gray, R, G, B }) => {
let txt = map[Math.floor((gray / 256) * map.length)];// 根据灰度值按比例计算出字符下标
//返回要渲染的字符以及该字符的颜色值
return { txt, R, G, B };
});
});
};
draw方法的实现
displayCtx
为canvas2
的2d上下文对象,通过canvas
dom对象的getContext("2d");
方法获取,handleDataCtx
为canvas1
的2d上下文对象,获取方法同上
const draw = (colorful=false,charColor='#000') => {
// 每次绘制清空画布
displayCtx.clearRect(
0,
0,
displayCanvas.width,
displayCanvas.height
);
// 将video绘制到canvas1
handleDataCtx.drawImage(
player.value,
0,
0,
handleDataCanvas.width,
handleDataCanvas.height
);
//通过canvas1获取图像像素数据
const imgData = handleDataCtx.getImageData(
0,
0,
handleDataCanvas.width,
handleDataCanvas.height
);
//调用处理像素的函数获取到字符数据
const charData = pixelsTransform(imgData);
//遍历字符数据,绘制到canvas2
charData.forEach((row, rowIndex) => {
const fontSize = Math.floor(displayCanvas.value.width / row.length);
// 这里使用画布宽度/每行字符数得到字体大小
const offsetY = Math.floor(
(props.size.height - charData.length * fontSize) / 2
);
/**
* charData.length * fontSize为在画布中绘制的图像高度
* Y轴方向偏移量为 (画布高度-图像高度)/2
*/
displayCtx.font = `${fontSize}px arial`;
// 使用等宽字体 确保上面计算结果在X方向铺满画布,Y方向居中
row.forEach((col, colIndex) => {
const { txt, R, G, B } = col;
// 是否绘制彩色图像
if (colorful) {
displayCtx.fillStyle = `rgb(${R},${G},${B})`;
} else {
//如果不是绘制彩色则看第二参数指定的字符颜色
displayCtx.fillStyle = charColor;
}
displayCtx.fillText(
txt,
fontSize * colIndex,
fontSize * rowIndex + offsetY
);
});
});
};
现成的组件
这是个基于vue写的视频字符化的组件