使用Javascript制作BadApple字符画视频 | 创作者训练营第二期

简介: 既然有小伙伴提出了这个问题,我就写一个完整教程,下次再有小伙伴问,直接看这篇,保管你用任何语言,任何框架都做得出来。

先拆解需求


  1. 播放视频


  1. 将视频每一帧的画面转为点阵/像素RGB值


  1. 将RGB转灰度值


  1. 按照灰度值填充字符


需求很简单,稍微复杂的部分只有RGB转灰度,那我们直接开撸代码,使用vanilla.js框架(这是个梗,不明白的话自己搜)来完成开发。


1. 播放视频


使用JS创建一个video标签,并为它设置视频源路径


var videoDom = document.createElement("video");
videoDom.src = "./video/badapple.mp4";
videoDom.style.width = "900px";
videoDom.style.height = "675px";


由于我们最终的效果并不需要看到这个视频原画面,所以我们也不用将这个dom添加到网页body当中去。


添加一个控制视频播放和暂停的按钮


var btnPlayAndPause = document.createElement("div");
btnPlayAndPause.style.color = "#fff";
btnPlayAndPause.style.textAlign = "center";
btnPlayAndPause.style.position = "absolute";
btnPlayAndPause.style.top = btnPlayAndPause.style.left = "0px";
btnPlayAndPause.style.width = videoDom.style.width;
btnPlayAndPause.style.height = btnPlayAndPause.style.lineHeight = videoDom.style.height;
btnPlayAndPause.style.cursor = "pointer";
btnPlayAndPause.style.fontSize = "30px";
btnPlayAndPause.style.zIndex = 2;
btnPlayAndPause.innerText = "play";
document.body.appendChild(btnPlayAndPause);


当按钮点击的时候,切换videoDom的播放/暂停状态


btnPlayAndPause.addEventListener("click",function(){
        if(btnPlayAndPause.innerText === "play"){
                videoDom.play();
        }else{
                videoDom.pause();
        }
})


监听videoDomcanplay事件,并渲染第一帧


videoDom.addEventListener('canplay',function(){
    renderVideoFrame(videoDom);
});


监听videoDomplay(播放),pause(暂停),stop(停止)事件在播放时启动字符画面渲染,暂停或停止时也停止掉字符画面渲染。


videoDom.addEventListener('play',function(){
    console.log("开始播放");
    btnPlayAndPause.innerText = "";
    startRender();
});
//监听播放结束
videoDom.addEventListener('pause',function(){
    console.log("播放暂停");
    btnPlayAndPause.innerText = "play";
    stopRender();
}); 
//监听播放结束
videoDom.addEventListener('ended',function(){
    console.log("播放结束");
    btnPlayAndPause.innerText = "play";
    stopRender();
});


画面渲染的绘制频率和浏览器的绘制频率保持一致,这样不会丢掉任何一个画面,但算力消耗会更大。


var timerId;
function startRender() {
        timerId = requestAnimationFrame(updateRender);
}
function updateRender(){
        renderVideoFrame(videoDom);
        timerId = requestAnimationFrame(updateRender);
}
function stopRender(){
        cancelAnimationFrame(timerId);
}


2. 将视频每一帧的画面转为点阵/像素RGB值


这里我们要利用html5canvas标签,首先将视频的画面原封不动的绘制到canvas上。


function renderVideoFrame(videoDom) {
    var videoSize = {width:parseFloat(videoDom.videoWidth),height:parseFloat(videoDom.videoHeight)};
    var canvas = document.querySelector("#canvas");
    if(!canvas){
            canvas = document.createElement("canvas");
            canvas.id = "canvas";
            canvas.style.width = videoDom.style.width;
            canvas.style.height = videoDom.style.height;
            canvas.style.position = "absolute";
            canvas.style.zIndex = 1;
            canvas.style.left = canvas.style.top = "0";
            canvas.width = videoSize.width;
            canvas.height = videoSize.height;
            document.body.appendChild(canvas);
    }
    const ctx = canvas.getContext("2d");
    ctx.drawImage(videoDom, 0, 0, videoSize.width, videoSize.height);
}


注意看我这里做了判断,只在场景上没有指定canvas的时候,才创建它。


接着通过contextdrawImage方法,将视频绘制到场景上,现在我们body虽然没有video标签,但我们也能看到视频了。


image.png


接着我们再通过contextgetImageData方法,获得画布里的全部点阵/像素数据。


var imgData = ctx.getImageData(0, 0, videoSize.width, videoSize.height).data;


这是一个庞大的数组,数组的长度由width*height*4(宽度x高度x4)组成,4代表RGBA四个值。


//如果这个画布是宽2个像素,高1个像素的话,那么getImageData获得数组结构如下
[r,g,b,a,r,g,b,a]


这个理解了之后我们来看如何获得指定位置的RGBA值。


for (var h = 0; h < videoSize.height; h++) {
    for(var w = 0; w < videoSize.width; w++){
            var position = (videoSize.width * h + w) * 4;
            var r = imgData[position], g = imgData[position + 1], b = imgData[position + 2];
    }
}


通过画布宽度和高度的两次for循环,换算得出所有点阵/像素在数组中的起始序号。


  • r = imgData[position]
  • g = imgData[position + 1]
  • b = imgData[position + 2]


大家可以看到,一块200x300的画布=6万个点阵=长度为24万的数组,我们肯定不能按照像素1:1来绘制,这样运算量过大,并且绘制出来的效果也不好,你根本看不清文字内容。


所以我们要加入一个间隔gap,比如1:12,这样运算量大大减少,但是绘制出来的精度也会降低。


image.png


var gap = 6;
for (var h = 0; h < videoSize.height; h+=gap) {
    for(var w = 0; w < videoSize.width; w+=gap){
            var position = (videoSize.width * h + w) * 4;
            var r = imgData[position], g = imgData[position + 1], b = imgData[position + 2];
    }
}


实现这个效果我们不需要alpha,接下来的重头戏是把RGB转为灰度值,灰度值再转化为笔画密度的文字,比如黑色的像素块我们就用这个字来替换。


3. RGB转灰度值



我采用了文中所列的第二种方法


Gray = (R*30 + G*59 + B*11 + 50) / 100 - 0.5


这个值在0.5-255.5之间


4. 按照灰度值填充字符


首先我们要建立一个灰度字符数组,按照笔画密度/视觉灰度(从高到低)排列,最后留一个空白字符去表现纯白色。


var asciiList = ['猿','帅','老','大', ' '];


将灰度值转为字符数组的序号,使用Math.min方法来确保序号不会越界


var i = Math.min(asciiList.length-1,parseInt(gray / (255 / asciiList.length)));


总结


这是一个自我接触计算机以来就知道的特效,我的职业历史上也用各种语言分别实现过。其实拆解需求后,核心就是获取画面点阵信息,RGB数据转灰度或者二值化(仅黑白两色)。再根据灰度信息替换为字符即可。


希望我的这篇教程让你彻底学会,以后不管用什么语言,什么环境,找到对应的API,都能开发出来这个效果。


One More Thing


照我以往的风格,我也将这个特效代码做了个收藏夹的版本,B站任意视频都可以用这个效果来播放~


javascript:!(function(){console.log("badapple effect enabled");function renderVideoFrame(videoDom){var asciiList=['猿','帅','老','大',' '];var scale=parseInt(videoDom.videoHeight/parseFloat($(videoDom).css("height")));var gap=12/scale;console.log(scale);var videoSize={width:parseFloat(videoDom.videoWidth/scale),height:parseFloat(videoDom.videoHeight/scale)};var canvas=document.querySelector("#badapplecanvas");if(!canvas){canvas=document.createElement("canvas");canvas.id="badapplecanvas";canvas.style.width=videoDom.style.width;canvas.style.height=videoDom.style.height;canvas.style.position="absolute";canvas.style.background="#fff";canvas.style.zIndex=999;canvas.style.top="0";canvas.style.left=(parseFloat($(videoDom).css("width"))-videoSize.width)/2+"px";canvas.width=videoSize.width;canvas.height=videoSize.height;videoDom.parentElement.appendChild(canvas)}const ctx=canvas.getContext("2d");ctx.drawImage(videoDom,0,0,videoSize.width,videoSize.height);var imgData=ctx.getImageData(0,0,videoSize.width,videoSize.height).data;ctx.clearRect(0,0,videoSize.width,videoSize.height);ctx.font=gap+"px Verdana";for(var h=0;h<videoSize.height;h+=gap){for(var w=0;w<videoSize.width;w+=gap){var position=(videoSize.width*h+w)*4;var r=imgData[position],g=imgData[position+1],b=imgData[position+2];var gray=(r*30+g*59+b*11+50)/100;var i=Math.min(asciiList.length-1,parseInt(gray/(255/asciiList.length)));ctx.fillText(asciiList[i],w,h)}}}var videoDom=document.querySelector("video");videoDom.style.display="none";videoDom.addEventListener('canplay',function(){renderVideoFrame(videoDom)});videoDom.addEventListener('play',function(){console.log("开始播放");startRender()});videoDom.addEventListener('pause',function(){console.log("播放暂停");stopRender()});videoDom.addEventListener('ended',function(){console.log("播放结束");stopRender()});var timerId;function startRender(){timerId=requestAnimationFrame(updateRender)}function updateRender(){renderVideoFrame(videoDom);timerId=requestAnimationFrame(updateRender)}function stopRender(){cancelAnimationFrame(timerId)}})()


新建一个网页书签,把以上代码复制粘贴到网址中,名称随便取。打开任意B站视频页,点击该书签,即可开启字符画播放模式,快试试吧...


image.png


相关文章
|
6月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的校园竞赛管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的校园竞赛管理系统附带文章源码部署视频讲解等
194 63
|
6月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的小型医院医疗设备管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的小型医院医疗设备管理系统附带文章源码部署视频讲解等
65 6
|
6月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的校园健康驿站管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的校园健康驿站管理系统附带文章源码部署视频讲解等
64 5
|
6月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的箱包存储系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的箱包存储系统附带文章源码部署视频讲解等
50 5
|
6月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的销售项目流程化管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的销售项目流程化管理系统附带文章源码部署视频讲解等
69 3
|
6月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的项目申报管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的项目申报管理系统附带文章源码部署视频讲解等
55 3
|
6月前
|
Web App开发 移动开发 前端开发
技术经验分享:canvas+howler.js解决同页面视频、音频同时播放问题
技术经验分享:canvas+howler.js解决同页面视频、音频同时播放问题
173 0
|
2月前
|
前端开发 JavaScript 安全
前端JS实现密码校验键盘横竖、26字母、相同字母、相同数字、密码包含用户名、数字 字母不能连续 不能相同三个、不能横向 竖向 连续三个 包含字符、不能有中文符号
该 JavaScript 代码实现了一个严格的密码校验功能,确保密码满足多种安全要求,包括长度、字符类型、不包含中文及特殊字符、不与用户名相似等。通过多个辅助函数,如 `validateFormat` 检查密码格式,`isHasChinaCharFun` 检测中文符号,`getCharAll` 生成键盘组合,以及 `checkPasswordFun` 综合验证密码的有效性和安全性。此工具对于提高用户账户的安全性非常有用。
71 0
用html+javascript打造公文一键排版系统14:为半角和全角字符相互转换功能增加英文字母、阿拉伯数字、标点符号、空格选项
用html+javascript打造公文一键排版系统14:为半角和全角字符相互转换功能增加英文字母、阿拉伯数字、标点符号、空格选项
|
3月前
|
存储 文字识别 前端开发
用html+javascript打造公文一键排版系统13:增加半角字符和全角字符的相互转换功能
用html+javascript打造公文一键排版系统13:增加半角字符和全角字符的相互转换功能