JS 30 是由加拿大的全端工程师 Wes Bos 免费提供的 JavaScript 简单应用课程,课程主打 No Frameworks
、No Compilers
、No Libraries
、No Boilerplate
在30天的30部教学影片里,建立30个JavaScript的有趣小东西。
另外,Wes Bos 也很无私地在 Github 上公开了所有 JS 30 课程的程序码,有兴趣的话可以去 fork 或下载。
取得使用者的镜头影像,藉此实作出网页版的相机以及影像滤镜效果。
这次的范例会需要取得镜头的存取权限,而在取得过程必须是透过https
或是localhost
这类的secure origin
才行,以下我们透过npm install
和npm start
架起自己的 little server (localhost)。
如果发现下面的指令无效的话,表示没有安装Node.js,可以点这边下载 LTS 的版本,都用预设的安装就好。
第一步 : 先移动到编辑档案的工作目录,接着输入npm install
,它会帮你安装一些套件
第二步 : 输入npm start
,它会去执行package.json
里的start
,开始运行一个 little server。(红色框是目前的网页位置)
div(.controls
) : 里面放置用来拍照的按钮和调整绿幕效果的按钮。
canvas(.photo
) : 用来放入镜头的影像,之後会搭配一些滤镜。
video(.player
) : 固定在右上角的小影像视窗。
div(.strip
) : 用来放入撷取下来的图片。
audio(.snap
) : 放入按下拍照按钮时要拨放的音效。
<div class="photobooth">
<div class="controls">
<button onClick="takePhoto()">Take Photo</button>
<div class="rgb">
<label for="rmin">Red Min:</label>
<input type="range" min=0 max=255 name="rmin">
<label for="rmax">Red Max:</label>
<input type="range" min=0 max=255 name="rmax">
<br>
<label for="gmin">Green Min:</label>
<input type="range" min=0 max=255 name="gmin">
<label for="gmax">Green Max:</label>
<input type="range" min=0 max=255 name="gmax">
<br>
<label for="bmin">Blue Min:</label>
<input type="range" min=0 max=255 name="bmin">
<label for="bmax">Blue Max:</label>
<input type="range" min=0 max=255 name="bmax">
</div>
</div>
<canvas class="photo"></canvas>
<video class="player"></video>
<div class="strip"></div>
</div>
<audio class="snap" src="./snap.mp3" hidden></audio>
老样子,我们要先取得所有要用到的元素。
.player
是小镜头画面。
canvas
是可以套上滤镜的大镜头画面。
ctx
是canvas
的渲染环境。
strip
是放照片的容器。
snap
是拍照的音效。
const video = document.querySelector('.player');
const canvas = document.querySelector('.photo');
const ctx = canvas.getContext('2d');
const strip = document.querySelector('.strip');
const snap = document.querySelector('.snap');
navigator.mediaDevices.getUserMedia()
,用来取得使用者的媒体装置,因为我们只需要取得影像,所以指定{video:true,audio:false}
不存取音讯,最後回传一个Promise
。
我们用then()
继续进行处理,因为不能直接将取得的MediaStream
指定为video
的来源(它看不懂QQ),还需要透过window.URL.createObjectURL()
把MediaStream
换成video
可以理解的URL
,然後video.play()
开始播放影像。
到这边,我们还需要用catch()
来处理例外发生的状况,当无法顺利取得媒体装置或是媒体装置不存在,就会在 console 上印出错误讯息。
function getVideo(){
navigator.mediaDevices.getUserMedia({video:true,audio:false})
.then(localMediaStream => {
console.log(localMediaStream);
video.src = window.URL.createObjectURL(localMediaStream);
video.play();
})
.catch(err => {
console.log(`OH NO!!!`,err);
});
}
getVideo();
canvas
)上 :为了让画布的大小和取得的影像大小一致,使用video.videoWidth
、video.videoHeight
取得影像的宽、长,然後修改画布的宽(canvas.width
)、长(canvas.height
)。
setInterval()
,设定每隔一段时间就把影像更新到画布(这边是设定16毫秒)。
ctx.drawImage()
,把影像画到画布(canvas)上。
video.addEventListener('canplay',paintToCanvas)
,如果影像现在是可以正常播放的话,就持续将影像输出到画布上。
function paintToCanvas(){
const width = video.videoWidth;
const height = video.videoHeight;
canvas.width = width;
canvas.height = height;
setInterval(() =>{
ctx.drawImage(video,0,0,width,height);
},16);
}
video.addEventListener('canplay',paintToCanvas);
Take Photo
时的快门音效、把撷取下来的图片放入strip
内供下载 :snap.currentTime = 0
,确保每一次都是从头开始播放音效,snap.play()
开始播放。
canvas.toDataURL('image/jpeg')
将canvas
上的影像转换成image/jpeg
格式的URL
档案连结。
const link = document.createElement('a')
,在文件上新增一个<a>
标签。
link.href = data
,将标签连结指定为取得的影像图档连结。
link.setAttribute('download','handsome')
,设定这个连结是可被点击下载,同时下载的档案名称为handsome
。
link.innerHTML
=
<img src="${data}" alt="handsome man" />
,在<a>
内部放入我们取得的图片,现在只要点击图片就会把图片下载下来。
strip.insertBefore(link,strip.firstChild)
,将整个<a><img></a>
(撷取的影像图)插入到.strip
里面并且是第一个位置。
function takePhoto(){
//Play the sound
snap.currentTime = 0;
snap.play();
const data = canvas.toDataURL('image/jpeg');
console.log(data);
const link = document.createElement('a');
link.href = data;
link.setAttribute('download','handsome');
//link.textContent = 'Download Image';
link.innerHTML = `<img src="${data}" alt="handsome man" />`;
strip.insertBefore(link,strip.firstChild);
}
不同的滤镜效果其实只是将CanvasRenderingContext2D.getImageData()
取得的画布像素(pixels)数据以每四个为一组(R-G-B-A)的方式修改,然後再将修改完的像素用CanvasRenderingContext2D.putImageData()
放回画布。
1. 红色滤镜
增强红色并减弱绿、蓝色
function redEffect(pixels){
for(let i=0;i < pixels.data.lenght;i+=4){
pixels.data[i + 0] = pixels.data[i + 0] + 100;// R
pixels.data[i + 1] = pixels.data[i + 0] - 50;// G
pixels.data[i + 2] = pixels.data[i + 0] * 0.5;// B
}
return pixels;
}
2. 色彩分离
实际上是让色板产生位移 (这部分不太好理解@@)
function rgbSplit(pixels){
for(let i=0;i < pixels.data.lenght;i+=4){
pixels.data[i - 150] = pixels.data[i + 0];// R
pixels.data[i + 500] = pixels.data[i + 0];// G
pixels.data[i - 550] = pixels.data[i + 0];// B
}
return pixels;
}
3. 绿幕
让一定数值范围内的 R、G、B 消失。
建立一个空物件levels
,接着放入每一个 range 的名称和数值。
以每四个为一组的方式取得画布像素的 R、G、B 数值,接着把颜色进行比对,举红色为例,如果像素的 R 值处在 rmin 和 rmax 之间,就把该像素的透明度设定为0(在画面上消失)。
function greenScreen(pixels) {
const levels = {};
document.querySelectorAll('.rgb input').forEach((input) => {
levels[input.name] = input.value;
});
for (i = 0; i < pixels.data.length; i = i + 4) {
red = pixels.data[i + 0];
green = pixels.data[i + 1];
blue = pixels.data[i + 2];
alpha = pixels.data[i + 3];
if (red >= levels.rmin
&& green >= levels.gmin
&& blue >= levels.bmin
&& red <= levels.rmax
&& green <= levels.gmax
&& blue <= levels.bmax) {
// take it out!
pixels.data[i + 3] = 0;
}
}
return pixels;
}
(以套用红色滤镜为例,其他滤镜也是同理)
function paintToCanvas(){
const width = video.videoWidth;
const height = video.videoHeight;
canvas.width = width;
canvas.height = height;
return setInterval(() =>{
ctx.drawImage(video,0,0,width,height);
//take the pixels out
let pixels = ctx.getImageData(0,0,width,height);
//mass with them
pixels = redEffect(pixels);
//put them back
ctx.putImageData(pixels,0,0);
},16);
}
这次的练习是目前为止最复杂的,连我自己本身也花了非常多的时间查资料,但仍然没有办法把细节交代清楚。
所以大家可能要多花些精力在学习这次的课程内容上,大家加油~~~
Enabling the Microphone/Camera in Chrome for (Local) Unsecure Origins
Navigator
Navigator.mediaDevices
MediaDevices.getUserMedia()
URL.createObjectURL()
CanvasRenderingContext2D.drawImage()
HTMLCanvasElement.toDataURL()
Document.createElement()
HTMLMediaElement
Element.setAttribute()
Node.insertBefore()
debugger
CanvasRenderingContext2D.putImageData()
CanvasRenderingContext2D.getImageData()
ps. 这次的网页比较特殊,如果打开镜头仍然无法看到效果的话,可能就要自己 fork 程序码到本地端测试~
<<: # Day 10 Cache and TLB Flushing Under Linux (二)
>>: 30-4 之软件架构设计原则 3 - LSP 里氏替换原则
我们透过 LeetCode #412 Fizz Buzz 来实际感受解决问题的过程 ( 题目连结 )...
文章说明 文章分段 文章说明 deeplab的简单介绍、於我的意义 ep.1 tensorflow的...
InputTagHelper: 是针对原生HTML 的封装 新增InputController.cs...
回溯法是暴力破解法的一种,在列出各种可能的组合时,如果遇到不符合条件的就不再继续向下查找,而是回到...
这篇终於要来开始做台股技术面的功能了,对於善於技术分析的投资人来说,看K线是非常基本的事情,因为从技...