HTML5视频转字符动画
什么是视频转字符动画?大至就是
点此在线体验 (纯字符,适合PC) 点此在线体验 (画布,适合PC、移动端)
补充: 字符画进阶实现(后来写的一个更有意思的实现,可以自己手写字符生成图画,比如写个女友的名字。。)


一. 实现原理
将视频拆分成独立的帧(等同于每换一个画面截一张图)
将图转换成黑白图(如何将图片转成黑白自行百度)后,拿到每个像素点的颜色,点的rgb是相同的,是一个0~255的整数,0表示完全黑,255表示完全白
准备一组字符,比如@#&!:,. 这些字符同字号时的表面积应该是递增的且越多越好,对应2中的rgb数值,比如 @对应0,表示黑, .对应255表示白
将2中的黑白图片每个像素点换成3中对应的字符
重复1-4步到视频结束
二. 代码实现
最下面有完整源码,实现部分能理清思路就好。
准备cnavas容器,用来播放字符动画。准备input文件上传入口
<canvas id="textCanvas"></canvas> <input type="file" id="file">
创建一个视频转动画的Dv类,构造中初始化画布
function Dv(){
this.textCanvas = $('textCanvas');
this.textCanvas.width = window.innerWidth;
this.textCanvas.height = window.innerHeight;
this.textCtx = this.textCanvas.getContext('2d');
} 创建一个离屏video容器,用来播放原视频,是字符画的数据来源
Dv.prototype.initVideo = function(src) {
if(!this.video){
this.video = document.createElement('video');
}
if(src){
this.video.src = src;
}
}; 获取并使用FileReader对象做为Blob对象载入
Dv.prototype.initFile = function() {
var file = $('file').files[0];
if(!file){
alert("请选择一个MP4视频文件");
return false;
}
var reader = new FileReader();
var buffer = [];
var that = this;
reader.onload = function(){
var blob = new Blob([reader.result], { type: 'video/mp4'});
that.playFile(reader.result,blob);
}
reader.readAsArrayBuffer(file);
}; 将载入的Blob对象做为ObjectURL赋值给video的src属性,video可以播放视频文件了,下面开始抓取视频帧来生成字符画
Dv.prototype.playFile = function(arrayBuffer,blob) {
var mediaSource = new MediaSource();
src = URL.createObjectURL(blob);
this.initVideo(src);
this.interval();
this.video.play();
};使用requestAnimationFrame动画API开始定时抓取视频单帧图像转换成黑白。ctx是离屏画布的上下文用于临时存放图像,ctx将图像缩小一定比列来减少要处理的像素点。
Dv.prototype.interval = function() {
var that = this;
requestAnimationFrame(function(){
if(!that.video.paused){
that.ctx.drawImage(that.video,0,0,that.width,that.height);
var data = that.loadData();
that.reDraw(data);
that.drawText();
}
that.interval();
});
};
将图像像素点数据,按黑白程度映射成相应的字符,并输出在画布上
Dv.prototype.drawText = function() {
this.textCtx.clearRect(0,0,window.innerWidth,window.innerHeight);
var data = this.data.data;
var points = '.,`":!^|*ITDXUHB%&#@NM'.split('');
for(var i=0,len=data.length;i<len;i+=4){
this.textCtx.fillStyle = '#333';
var xl = (i/4|0)%this.width;
var yl = Math.ceil(i/4/this.width);
var x = xl * this.space;
var y = yl * this.space;
var newData = data[i] | 0;
var plen = Math.ceil(255/points.length);
var point = points[newData/plen | 0]
this.textCtx.font="12px courier";
this.textCtx.fillText(point,x,y);
}
};三. 完整示例,(控制台开启手机模式效果最好)
<!DOCTYPE html>
<html>
<head>
<title>chars video</title>
<style type="text/css">
html,body{ height: 100%; }
html,body,.ctrl{margin: 0;padding: 0;}
#textCanvas{ font-family: 'courier';}
#videoScreen{ height: 100vh;width: 100vw;}
.ctrl{ position: fixed;right: 0px;bottom: 0px;left: 0;z-index: 3; padding: 10px; border-radius: 4px;background-color: #fff;}
input[type=button]{background-color: #1aa988;color: #fff;border-width: 0;padding: 4px 8px;border-radius: 4px;cursor: pointer;}
select{padding : 4px 8px;border-radius: 4px;}
#file{position : absolute;left:-99999px;}
.file{background-color: #1aa988;color: #fff;border-width: 0;padding: 12px 8px;border-radius: 96px;cursor: pointer;display: block;text-align: center;}
#info{position: absolute;text-align: center;padding: 20px;left: 0;right: 0; top: 20%;color: #999;line-height: 2;}
</style>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0,user-scalable=no,minimal-ui">
</head>
<body>
<div class="ctrl" id="ctrl">
<label for="file" class="file"><input type="file" id="file">浏览</label>
</div>
<div id="videoScreen">
<div id="info">点击浏览,选择一个mp4视频文件即可开始<br>点击空白处可隐藏底部按钮</div>
<canvas id="textCanvas"></canvas>
</div>
<script type="text/javascript">
function $(id){ return document.getElementById(id); }
function Dv(){
this.space = 10;
this.width = Math.ceil(window.innerWidth/this.space);
this.height = Math.ceil(window.innerHeight/this.space);
this.data = {};
this.cav = {};
this.ctx = {};
this.playing = false;
this.init();
this.scaleX = window.innerWidth/this.width;
this.textCanvas = $('textCanvas');
this.textCanvas.width = window.innerWidth;
this.textCanvas.height = window.innerHeight;
this.textCtx = this.textCanvas.getContext('2d');
}
Dv.prototype.init = function() {
this.initVideo();
this.initCanvas();
this.cav.width = this.width;
this.cav.height = this.height;
this.initEvent();
};
Dv.prototype.initVideo = function(src) {
if(!this.video){
this.video = document.createElement('video');
//document.body.appendChild(this.video);
}
if(src){
this.video.src = src;
}
};
Dv.prototype.initCanvas = function(video) {
this.cav = document.createElement('canvas');
this.ctx = this.cav.getContext('2d');
};
Dv.prototype.loadData = function() {
return this.ctx.getImageData(0,0,this.width,this.height);
};
Dv.prototype.reDraw = function(data) {
for(var i=0,len=data.data.length;i<len;i+=4){
var r = data.data[i],
g = data.data[i+1],
b = data.data[i+2];
data.data[i] = data.data[i+1] = data.data[i+2] = 255-(r+g+b)/3 | 0;
}
this.data = data
this.ctx.putImageData(data,0,0,0,0,this.width,this.height);
};
Dv.prototype.drawText = function() {
this.textCtx.clearRect(0,0,window.innerWidth,window.innerHeight);
var data = this.data.data;
var points = '.,`":!^|*ITDXUHB%&#@NM'.split('');
for(var i=0,len=data.length;i<len;i+=4){
this.textCtx.fillStyle = '#333';
var xl = (i/4|0)%this.width;
var yl = Math.ceil(i/4/this.width);
var x = xl * this.space;
var y = yl * this.space;
var newData = data[i] | 0;
var plen = Math.ceil(255/points.length);
var point = points[newData/plen | 0]
this.textCtx.font="12px courier";
this.textCtx.fillText(point,x,y);
}
};
Dv.prototype.interval = function() {
var that = this;
requestAnimationFrame(function(){
if(!that.video.paused){
that.ctx.drawImage(that.video,0,0,that.width,that.height);
var data = that.loadData();
that.reDraw(data);
that.drawText();
}
that.interval();
});
};
//以下方法用于本地视频
Dv.prototype.initEvent = function() {
var that = this;
$('file').onchange = function(){
var filename = this.value;
var index = filename.lastIndexOf(".");
var ext = filename.substr(index+1);
if(ext == "mp4"){
that.initFile();
$('info').style.display = 'none';
$('ctrl').style.display = 'none';
}else{
alert("仅支持MP4格式");
}
}
$('videoScreen').onclick = function(){
if($('ctrl').style.display == 'none'){
$('ctrl').style.display = 'block';
}else{
$('ctrl').style.display = 'none';
}
}
};
Dv.prototype.initFile = function() {
var file = $('file').files[0];
if(!file){
alert("请选择一个MP4视频文件");
return false;
}
var reader = new FileReader();
var buffer = [];
var that = this;
reader.onload = function(){
var blob = new Blob([reader.result], { type: 'video/mp4'});
that.playFile(reader.result,blob);
}
reader.readAsArrayBuffer(file);
};
Dv.prototype.playFile = function(arrayBuffer,blob) {
var mediaSource = new MediaSource();
src = URL.createObjectURL(blob);
this.initVideo(src);
this.interval();
this.video.play();
};
var d = new Dv();
</script>
</body>
</html>
2018-08-29 14:18:39
5815
1
参与讨论