什么是视频转字符动画?大至就是
点此在线体验 (纯字符,适合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>