iT邦幫忙

2022 iThome 鐵人賽

DAY 19
0
自我挑戰組

JavaScript 30天挑戰 自學筆記系列 第 19

JS30 自學筆記 Day19_Unreal Webcam Fun

  • 分享至 

  • xImage
  •  

今日任務: 使用getUserMedia API 取得視訊鏡頭,繪製到畫面上,並做出不同的特效

今天這篇比較複雜,因為想寫得清楚一些,所以版面稍長,在這邊先寫下大綱:

  1. 前置作業
    • 視訊鏡頭
    • Browsersync
  2. 請求取得視訊鏡頭
  3. 把MediaStream放進裡面播放
  4. 把影像投到canvas上
  5. 拍照
  6. 取得canvas畫布的像素色彩
  7. 紅色濾鏡效果
  8. 顏色分離效果
  9. 綠幕效果

前置作業

視訊鏡頭

因為使用桌電沒有視訊鏡頭,所以這邊是使用ivcam,將手機轉為電腦鏡頭使用
ivcam官網:將手機轉變成電腦視訊鏡頭的免費軟體 App

瀏覽器

MediaDevices.getUserMedia()需在安全連線(像是https、localhost)下使用

Browsersync

作者有提供一份package.json,裡面內的套件:Browsersync
BrowserSync 能在開發時幫我們建立一個網頁伺服器,並隨檔案變更自動重新整理頁面

先下載node.js
如果不確定有沒有裝過可以在終端機輸入node -v查看版本

下載後在終端機執行

npm install //安裝package.json內的套件(Browsersync)
npm start //啟動localserver(預設port3000)

請求取得視訊鏡頭

navigator物件包含有關使用者的瀏覽器資訊。
MediaDevices.getUserMedia(): 詢問使用者是否同意瀏覽器存取多媒體數據。
使用者按下許可後會產生MediaStream物件,裡面包含了請求存取的多媒體數據,例如視頻軌道或音頻軌道。

function getVideo() {
    navigator.mediaDevices.getUserMedia({ video: true, audio: false })
    .then((localMediaStream) => {
        console.log(localMediaStream);
    });
}
getVideo();

把MediaStream放進<video>裡面播放

localMediaStream目前是一個obj,為了可以讓<video>看得懂
把MediaStream轉為url
作者影片寫法:
window.URL.createObjectURL(): 建立一個帶有 URL 的 DOMString 以代表參數中所傳入的物件

video.src = window.URL.createObjectURL(localMediaStream);
video.play();

出現錯誤

作者有在影片標題寫下:

createObjectURL no longer works with mediaStream objects. Instead, we need to set video.srcObject = localMediaSteam

MDN:createObjectURL()也有寫下:

In older versions of the Media Source specification, attaching a stream to a <video> element required creating an object URL for the MediaStream. This is no longer necessary, and browsers are removing support for doing this.
Warning: If you still have code that relies on createObjectURL() to attach streams to media elements, you need to update your code to set srcObject to the MediaStream directly.

瀏覽器目前正在取消支援createObjectURL(MediaStream),所以我們改用srcObject

改用srcObject

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');

function getVideo() {
    navigator.mediaDevices
        .getUserMedia({ video: true, audio: false })
        .then((localMediaStream) => {
            console.log(localMediaStream);
            video.srcObject = localMediaStream;
            video.play();
        })
}

如果使用者拒絕或發生錯誤,加上catch

function getVideo() {
    navigator.mediaDevices
        .getUserMedia({ video: true, audio: false })
        .then((localMediaStream) => {
            console.log(localMediaStream);
            video.srcObject = localMediaStream;
            video.play();
        })
        .catch((err) => {
            console.log('發生錯誤:', err);
        });
}

把影像投到canvas上

將鏡頭的寬高和canvas一模一樣

HTMLVideoElement.videoWidth: 鏡頭的寬度
HTMLVideoElement.videoHeight: 鏡頭的高度

function paintToCanavas() {
    const width = video.videoWidth;
    const height = video.videoHeight;
    console.log(width, height);

    canvas.width = width;
    canvas.height = height;
}


處理畫面

setInterval(function, milliseconds): 每一段時間(毫秒)執行一次function
drawImage(image, dx, dy, dWidth, dHeight): 在畫布上繪製圖像,詳細參數可看這裡
ctx.drawImage(video, 0, 0, width, height):將鏡頭影像從畫布的(0,0)繪製寬width高height的圖

function paintToCanavas() {
    const width = video.videoWidth;
    const height = video.videoHeight;
    console.log(width, height);
    canvas.width = width;
    canvas.height = height;

    return setInterval(() => {
        ctx.drawImage(video, 0, 0, width, height);
    }, 16);
}

監聽'canplay'事件,繪製鏡頭到畫布上

HTMLMediaElement: canplay event:可以播放媒體時觸發該canplay事件

video.addEventListener('canplay', paintToCanavas);

拍照

將canvas轉為base64位編碼

toDataURL(): 將canvas轉為base64位編碼,預設為 image/png。
base64位編碼: Base64編碼是一種圖片處理格式,通過特定的算法將圖片編碼成一長串字符串,在頁面上顯示的時候,可以用該字符串來代替圖片的url屬性。

function takePhoto() {
    //拍照音效
    snap.currentTime = 0;
    snap.play();
    
    //將canvas轉為base64位編碼
    const data = canvas.toDataURL('image/jpeg');
    console.log(data);
}

base64位編碼長這樣:

做一個下載的超連結

createElement(tagName):可以建立一個新的HTML元素,直到透過 appendChild()insertBefore()replaceChild() 等方法將新元素加入至指定的位置之後才會顯示。
insertBefore(newNode, referenceNode): 將新節點 newNode 插入至指定的 refNode 節點的前面,詳細可以看這裡

setAttribute(name, value):設置指定元素的屬性值。如果屬性已經存在,則更新值
<a>有一個屬性為download,詳細可以看這裡

function takePhoto() {
    ...
    //將canvas轉為base64位編碼
    const data = canvas.toDataURL('image/jpeg');
    //建立一個新的<a>元素
    const link = document.createElement('a');
    //超連結為base64位編碼的canvas圖
    link.href = data;
    //連結點下後會下載base64位編碼的canvas圖,文件名為penguin
    link.setAttribute('download', 'penguin');
    link.textContent = '下載圖片';
    //strip為div,新增的超連結會放到第一個位置,也就是新增的元素會把舊的往後推
    strip.insertBefore(link, strip.firstChild);
}

將超連結文字改為圖片

function takePhoto() {
    ...
    link.innerHTML = ` <img src="${data}" alt="penguin">`;
    strip.insertBefore(link, strip.firstChild);
}

這樣直接點哪張圖就可以下載哪張圖

取得canvas畫布的像素色彩

認識一下ImageData相關定義與用法:
ImageData: 儲存canvas的像素數據。
ImageData.data: 一維數組,其中包含 RGBA 順序的數據,整數值介於0和255(包括)之間。
getImageData(): 取得ImageData數據。
putImageData: 將ImageData數據繪製到畫布上 。

const pixels = ctx.getImageData(0, 0, width, height);
console.log(pixels);

console.log來看一下

將data展開
每4個數字代表圖中每一個像素的顏色,順序為 R(紅色)、G(綠色)、B(藍色)和 A(alpha不透明度),再由這些像素組成一張圖。

下圖來源:這裡,裡面有對canvas ImageData的詳細介紹

紅色濾鏡效果

將每一個像素的紅色增強,藍綠色減弱

因為每個像素由data數組中的四個值組成,因此for循環以4的倍數進行迭代。

function redEffect(pixels) {
    for (let i = 0; i < pixels.data.length; i += 4) {
        pixels.data[i + 0] = pixels.data[i + 0] + 100; //紅色增強
        pixels.data[i + 1] = pixels.data[i + 1] - 50; //綠色減弱
        pixels.data[i + 2] = pixels.data[i + 2] * 0.5; //藍色減弱
    }
    return pixels;
}

將紅色增強數據繪製到畫布上

function paintToCanavas() {
    ...
    return setInterval(() => {
        //繪製畫布
        ctx.drawImage(video, 0, 0, width, height);
        //取出canvas像素資料
        pixels = ctx.getImageData(0, 0, width, height);
        //紅色濾鏡效果
        pixels = redEffect(pixels);
        //將ImageData數據繪製到畫布上 
        ctx.putImageData(pixels,0,0)
    }, 16);
}

紅色效果

以此類推,可以將綠色數值增高就變綠色

pixels.data[i + 1] = pixels.data[i + 1] + 150; 

顏色分離效果

這個效果看影片時不太理解,後來自己實作嘗試後有自己的理解想法,記錄下來,希望有幫助到一樣疑惑的人,如有錯誤也歡迎提出!

將顏色往左邊移360像素

function rgbSplit(pixels) {
    for (let i = 0; i < pixels.data.length; i += 4) {
        pixels.data[i + 0 - 360] = pixels.data[i + 0];
        pixels.data[i + 1 - 360] = pixels.data[i + 1];
        pixels.data[i + 2 - 360] = pixels.data[i + 2];
    }
    return pixels;
}

可以看到畫面被移過去

作者的數據

pixels.data[i - 150]= pixels.data[i + 0];
事實上,pixels.data[i + 0]只是一個數值,例如:255、100
pixels.data[i - 150]跑迴圈後,為:
pixels.data[-150],pixels.data[-146],pixels.data[-142]...pixels.data[-2],pixels.data[+2],pixels.data[+6]...
可以從pixels.data[+2]發現是負責處理每個像素藍色部分的地方,
所以pixels.data[i - 150]= pixels.data[i + 0];跑迴圈等於:
像素1控制藍色部分=255,像素2控制藍色部分=100,像素3控制藍色部分=150......
所以可以看到是所有藍色往左邊移動,紅+綠留在原地

因此要看要看前面的數值為處理什麼顏色,數值越大,移動部分越多
以此類推:
//像素控制藍色部分=數值;
pixels.data[i - 150] = pixels.data[i + 0];
//像素控制綠色部分=數值;
pixels.data[i - 151] = pixels.data[i + 0];
//像素控制紅色部分=數值;
pixels.data[i - 152] = pixels.data[i + 0];
//像素控制alpha部分=數值;
pixels.data[i - 153] = pixels.data[i + 0];
//又回到像素控制藍色部分=數值;
pixels.data[i - 154] = pixels.data[i + 0];

綠色沒有控制,被留在原地

紅色和藍色往左移動640像素

function rgbSplit(pixels) {
    for (let i = 0; i < pixels.data.length; i += 4) {
        pixels.data[i + 0 - 640] = pixels.data[i + 0]; //控制紅色(會被下一個控制紅色覆蓋)
        pixels.data[i - 640] = pixels.data[i + 1];//控制紅色
        pixels.data[i + 2 - 640] = pixels.data[i + 2]; //控制藍色
    }
    return pixels;
}

藍色沒有控制,被留在原地

紅色和綠色往左移動640像素

function rgbSplit(pixels) {
    for (let i = 0; i < pixels.data.length; i += 4) {
        pixels.data[i + 0 - 640] = pixels.data[i + 0]; //控制紅色(會被下一個控制紅色覆蓋)
        pixels.data[i + 1 - 640] = pixels.data[i + 1]; //控制綠色
        pixels.data[i - 640] = pixels.data[i + 2]; //控制紅色
    }
    return pixels;
}

藍+綠色沒有控制,被留在原地

紅色往左移動640像素

function rgbSplit(pixels) {
    for (let i = 0; i < pixels.data.length; i += 4) {
        pixels.data[i - 640] = pixels.data[i + 0];//控制紅色(會被下一個控制紅色覆蓋)
        pixels.data[i - 640] = pixels.data[i + 1];//控制紅色(會被下一個控制紅色覆蓋)
        pixels.data[i - 640] = pixels.data[i + 2]; //控制紅色
    }
    return pixels;
}

作者的數據

讓我們回到作者的數據:

pixels.data[i - 150] = pixels.data[i + 0]; // 控制藍色
pixels.data[i + 500] = pixels.data[i + 1]; // 控制紅色
pixels.data[i - 550] = pixels.data[i + 2]; // 控制藍色

發現有兩個控制藍色的部分,代表其實第一個控制藍色會被覆蓋

當我把第一個控制藍色(pixels.data[i - 150] = pixels.data[i + 0];)註解掉:

可以發現兩張圖長得一模一樣

顏色分離效果完整程式碼

function paintToCanavas() {
    ...
    return setInterval(() => {
        ...
        //加入顏色分離效果
        pixels = rgbSplit(pixels);
        ctx.putImageData(pixels, 0, 0);
    }, 16);
}

function rgbSplit(pixels) {
    for (let i = 0; i < pixels.data.length; i += 4) {
        pixels.data[i - 150] = pixels.data[i + 0]; // 控制藍色
        pixels.data[i + 500] = pixels.data[i + 1]; // 控制紅色
        pixels.data[i - 550] = pixels.data[i + 2]; // 控制藍色
    }
    return pixels;
}

globalAlpha

ctx.globalAlpha:在將形狀和圖像繪製到畫布上之前應用於這個屬性的 alpha(不透明度)值。

function paintToCanavas() {
    ...
    return setInterval(() => {
        ...
        pixels = rgbSplit(pixels);
        ctx.globalAlpha = 0.1
        ctx.putImageData(pixels, 0, 0);
    }, 16);
}

綠幕效果

在html加上range bar

<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>

獲取數值

const controllers = document.querySelectorAll('.rgb input');

function greenScreen() {
    const levels = {};
    controllers.forEach((controller) => {
        levels[controller.name] = controller.value;
        console.log(levels);
    });
}

綠幕效果

如果最小值 <= 顏色 <= 最大值,將它設為透明

function paintToCanavas() {
    ...
    return setInterval(() => {
        ...
        //加入綠幕效果
        pixels = greenScreen(pixels);
        //輸出
        ctx.putImageData(pixels, 0, 0);
    }, 16);
}

function greenScreen(pixels) {
    const levels = {};
    controllers.forEach((controller) => {
        levels[controller.name] = controller.value;
    });

    for (i = 0; i < pixels.data.length; 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
        ) {
            //透明
            pixels.data[i + 3] = 0;
        }
    }

    return pixels;
}

今天的比較複雜一點,以上皆可以再改數值玩出不同效果,
避免板面太長偏離主題,所以有些知識點只有稍微介紹過去,有興趣的話可以再點進相關連結詳細了解。

今日學習到的:

  • BrowserSync: 能在開發時幫我們建立一個網頁伺服器,並隨檔案變更自動重新整理頁面
  • navigator物件: 包含有關使用者的瀏覽器資訊。
  • MediaDevices.getUserMedia(): 詢問使用者是否同意瀏覽器存取多媒體數據。
  • MediaStream物件: 裡面包含了請求存取的多媒體數據,例如視頻軌道或音頻軌道。
  • 把MediaStream放進<video>裡面播放:
    • 瀏覽器目前正在取消支援createObjectURL(MediaStream),改用video.srcObject = localMediaStream;
  • HTMLVideoElement.videoWidth: 鏡頭的寬度
  • HTMLVideoElement.videoHeight: 鏡頭的高度
  • setInterval(function, milliseconds): 每一段時間(毫秒)執行一次function
  • drawImage(): 在畫布上繪製圖像
  • canplay event:可以播放媒體時觸發該canplay事件
  • toDataURL(): 將canvas轉為base64位編碼,預設為 image/png。
  • base64位編碼: Base64編碼是一種圖片處理格式,通過特定的算法將圖片編碼成一長串字符串,在頁面上顯示的時候,可以用該字符串來代替圖片的url屬性。
  • createElement(tagName):可以建立一個新的HTML元素,將新元素加入至指定的位置之後才會顯示。
  • insertBefore(newNode, referenceNode): 將新節點 newNode 插入至指定的 refNode 節點的前面。
  • setAttribute(name, value):設置指定元素的屬性值。如果屬性已經存在,則更新值
  • <a>有一個屬性為download
  • ImageData: 儲存canvas的像素數據。
  • ImageData.data: 一維數組,每4個數字代表圖中每一個像素的顏色,順序為 R(紅色)、G(綠色)、B(藍色)和 A(alpha不透明度),整數值介於0和255(包括)之間,再由這些像素組成一張圖。
  • getImageData(): 取得ImageData數據。
  • putImageData(): 將ImageData數據繪製到畫布上 。
  • ctx.globalAlpha:在將形狀和圖像繪製到畫布上之前應用於這個屬性的 alpha(不透明度)值。

效果連結:連結(有加上效果按鈕可切換)

參考連結:
MDN: MediaDevices.getUserMedia()
安全連線
MDN: URL.createObjectURL()
MDN: HTMLMediaElement.srcObject
MDN: Canvas.drawImage()
Base64編碼
MDN: a屬性
重新認識 JavaScript: Day 13 DOM Node 的建立、刪除與修改
MDN: ImageData.data
Canvas ImageData 對象


上一篇
JS30 自學筆記 Day18_Tally String Times with Reduce
下一篇
JS30 自學筆記 Day20_Native Speech Recognition
系列文
JavaScript 30天挑戰 自學筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言