什么是视频转字符动画?大至就是
一. 实现原理将视频拆分成独立的帧(等同于每换一个画面截一张图)
将图转换成黑白图(如何将图片转成黑白自行百度)后,拿到每个像素点的颜色,点的rgb是相同的,是一个0~255的整数,0表示完全黑,255表示完全白
准备一组字符,比如@#&!:,. 这些字符同字号时的表面积应该是递增的且越多越好,对应2中的rgb数值,比如 @对应0,表示黑, .对应255表示白
将2中的黑白图片每个像素点换成3中对应的字符
重复1-4步到视频结束
二. 代码实现
最下面有完整源码,实现部分能理清思路就好。
准备cnavas容器,用来播放字符动画。准备input文件上传入口
创建一个视频转动画的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
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);
}
};
三. 完整示例,(控制台开启手机模式效果最好)html>
chars videohtml,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;}
浏览
点击空白处可隐藏底部按钮
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
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
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();