# J_JS30 11-20 [TOC] # 11-Custom HTML5 Video Player 這章要製作一個影片播放器,實現一些影片的基本功能,播放暫停、音量大小、速度快慢、快轉、拖拉時間條等功能。 這次他將`<script>`區塊分檔出去了,也將程式碼分成『element』、『function』、『event』三個區塊: ```javascript //Get Element //build function //event listener ``` 所以首先先取即將要用到的元素: ```javascript= //Get Element const player = document.querySelector(".player"); //整個播放器 const video = document.querySelector(".viewer"); //影片元素 const progress = document.querySelector(".progress"); //完整進度條 const progressBar = document.querySelector(".progress__filled"); //目前進度條 const toggle = document.querySelector(".toggle"); //播放與暫停鍵 const ranges = document.querySelectorAll(".player__slider"); //音量與速度鍵 const skipButtons = document.querySelectorAll("[data-skip]"); //快轉與倒退鍵 ``` 再來就是建立`function`給事件呼叫,首先做播放暫停: ```javascript= //build function function togglePlay() { const method = video.paused ? "play" : "pause"; video[method](); } //event listener video.addEventListener("click", togglePlay); toggle.addEventListener("click", togglePlay); ``` 先辨識影片是不是暫停狀態,如果是`method`就給`"play"`,不是就給`"pause"`,然後呼叫`video[method]()`方法來播放或暫停。 --- 再來是播放跟暫停時要改變按鈕的`icon`: ```javascript= //build function function updateButton() { const icon = this.paused ? "►" : "❚ ❚"; toggle.textContent = icon; } //event listener video.addEventListener("play", updateButton); video.addEventListener("pause", updateButton); ``` 一樣辨識影片是否暫停,是`icon`就給`"►"`,不是就給`"❚ ❚"`,然後修改`toggle`的文字內容。 接著做倒退跟快轉: ```javascript= //build function function skip() { video.currentTime += parseFloat(this.dataset.skip); } //event listener skipButtons.forEach((button) => button.addEventListener("click", skip)); ``` 當按下按鈕時將`currenTime`加上按鈕的值。 --- 音量條與速度條: ```javascript= //build function function handleRangeUpdate() { video[this.name] = this.value; } //event listener ranges.forEach((range) => range.addEventListener("input", handleRangeUpdate)); ``` 這邊他介紹的方法是註冊`mousemove`跟`change`兩個事件,之前我有分享過`input`事件可以一次處理完這兩個事件~ 讓目前進度條跟著播放時間走: ```javascript= //build function function handleProgress() { const currentPercent = (video.currentTime / video.duration) * 100; progressBar.style.flexBasis = `${currentPercent}%`; } //event listener video.addEventListener("timeupdate", handleProgress); ``` `video`有`duration`可以查看總秒數,用`(當下秒數/總秒數)*100`就可以得到百分比,再放到`flex-basis`就可以了! 事件的部分有個`timeupdate`可以使用,非常方便,當影片時間在走就觸發事件。 --- 最後要做拉動跟點擊進度條: ```javascript= //build function function scrub(e) { video.currentTime = e.offsetX; } //event listener let mousedown = false; progress.addEventListener("click", scrub); progress.addEventListener("mousedown", () => (mousedown = true)); progress.addEventListener("mouseup", () => (mousedown = false)); progress.addEventListener("mouseout", () => (mousedown = false)); progress.addEventListener("mousemove", (e) => { if (mousedown) { scrub(e); } }); ``` 首先是點擊進度條就觸發`scrub`來更新`currentTime`,然後設置一個`mousedown`來辨識滑鼠有沒有按著,滑鼠點著就`true`,滑數放開就`false`,滑鼠超出範圍也`false`,接著就是當滑鼠移動時辨識`mousedown`與否來決定要不要觸發`scrub`,如此一來就可以拖著進度條來調整時間了! > 題外話:作者好像有講嘗試做全螢幕 > 目前感覺所有網頁的element都可以使用一個叫`requestFullScreen`的方法將這個元素放到全螢幕,然後對`document`使用`exitFullscreen`方法來退出全螢幕: ```javascript= function openFullScreen() { if (!document.fullscreenElement) { player.requestFullscreen(); } } function closeFullScreen() { if (document.fullscreenElement) { document.exitFullscreen(); } } video.addEventListener("dblclick", openFullScreen); video.addEventListener("dblclick", closeFullScreen); ``` `document.fullscreenElement`可以用來找到目前是哪個元素在全螢幕狀態,如果沒有就是`null`,透過這樣來控制事件的執行,然後我設定的是點擊影片兩下觸發事件。 # 12-Key Sequence Detection 這章應該是在做輸入對應的密碼就印出一個圖片,印出圖片的`function`已經幫你做好了,只要判斷輸入的字符合密碼就呼叫`cornify_add()`就可以了。 先設定密碼,然後建立一個陣列來放使用者輸入的內容: ```javascript const magicWord = "jeremy"; const input = []; ``` 然後在`document`建立鍵盤事件來抓取使用者輸入的字: ```javascript document.addEventListener("keyup", (e) => { input.push(e.key); input.splice(0, input.length - magicWord.length); console.log(input); if (input.join("").includes(magicWord)) { cornify_add(); } }); ``` 拿到使用者輸入的字然後加進`input`陣列,當陣列超過密碼的字數時要刪除最前面的字,確保`input`的內容都是最新輸入的: ```javascript input.splice(0, input.length - magicWord.length); ``` ```javascript input.splice(-magicWord.length - 1, input.length - magicWord.length); ``` 下面是影片教的方法,兩個方法用在這結果都會一樣。 再來是判斷密碼的方式,兩個結果也會一樣: ```javascript if (input.join("").includes(magicWord)) { console.log("bingo!"); cornify_add(); } ``` ```javascript if (input.join("") === magicWord) { console.log("bingo!"); cornify_add(); } ``` # 13-Slide In on Scroll 這章目標是當頁面捲到圖片所在位置時圖片滑入,當圖片捲出頁面時就滑出,首先選取所有圖片元素: ```javascript const sliderImages = document.querySelectorAll(".slide-in"); ``` 然後建立一個頁面捲動要觸發的`function`: ```javascript= function checkSlide(e) { sliderImages.forEach((sliderImage) => { const slideTarget = window.scrollY + window.innerHeight - sliderImage.height / 2; const imageButton = sliderImage.offsetTop + sliderImage.height; const isHalfShown = slideTarget > sliderImage.offsetTop; const isNotPassedImage = window.scrollY < imageButton; if (isHalfShown && isNotPassedImage) { sliderImage.classList.add("active"); } else { sliderImage.classList.remove("active"); } }); } ``` 這個函式要做的是捲動頁面時辨識有沒有捲到圖片所在位置,如果中有圖片(這邊只要圖片露出一半就算有出現),圖片就加上`active`屬性來滑入,反之就移除效果。 :question:還不太了解為什麼要用視窗大小扣掉圖片的一半,我可以理解他是想在圖片出現一半的時候觸滑入動畫,但搞得有點複雜,我有改寫成只要圖片一出現就滑入,感覺也沒什麼問題。 > 我的理解也是為了讓使用者先注意到那邊有個空間,然後再做動畫,才會扣掉圖片的一半高,讓使用者要多捲動圖片的一半高,動畫才會動 [name=橘子] ```javascript= sliderImages.forEach((sliderImage) => { const scrollBottom = window.scrollY + window.innerHeight; //差在這邊而已 const imageButton = sliderImage.offsetTop + sliderImage.height; const isShown = scrollBottom > sliderImage.offsetTop; //然後這邊命名就變成有沒有出現 const isNotPassedImage = window.scrollY < imageButton; if (isShown && isNotPassedImage) { sliderImage.classList.add("active"); } else { sliderImage.classList.remove("active"); } }); ``` # 14-Object and Arrays - Reference VS Copy 介紹各種型別的行為 字串、數字、布林值: ```javascript= let name = "Watson"; let name2 = "Jeremy"; console.log(name, name2); //Watson Jeremy name = "Fang"; console.log(name, name2); //Fang Jeremy ``` 改動`name`也不會影響`name2`,同樣的如果是數值及布林值也不會影響。 陣列: ```javascript= const players = ["Wes", "Sarah", "Ryan", "Poppy"]; const team = players; console.log("players", players); console.log("team", team); ``` ![image](https://hackmd.io/_uploads/HkAXkgP5R.png) 我們改動看看其中一個陣列: ```javascript= team[0] = "Abc"; console.log("players", players); console.log("team", team); ``` ![image](https://hackmd.io/_uploads/ry1HJev9C.png) 結果兩個都被改動了,所以我們不能用這種方式複製一個陣列。 1. 使用`Array.prototype.slice()`: ```javascript= const team2 = players.slice(); team2[0] = "JJJ"; console.log("players", players); console.log("team2", team2); ``` ![image](https://hackmd.io/_uploads/B1t7eePqC.png) 如此一來就不會改動到原來陣列了。 2. 再來試試看`Array.prototype.concat()`: ```javascript= const team3 = [].concat(players); team3[0] = "FFF"; console.log("players", players); console.log("team3", team3); ``` ![image](https://hackmd.io/_uploads/S121WxvqA.png) 也不會改動到原來陣列。 使用展開陣列的方式: ```javascript= const team4 = [...players]; team4[0] = "WWW"; console.log("players", players); console.log("team4", team4); ``` ![image](https://hackmd.io/_uploads/HJYHblD5C.png) 一樣不會改動原本陣列。 3. 再來使用`Array.from()`: ```javascript= const team5 = Array.from(players); team5[0] = "TTT"; console.log("players", players); console.log("team5", team5); ``` ![image](https://hackmd.io/_uploads/B1BCblP5A.png) --- 再來是物件的複製: ```javascript= const person = { name: "Wes Bos", age: 80, }; const obj = person; obj.number = 10; console.log("person", person); console.log("obj", obj); ``` ![image](https://hackmd.io/_uploads/rkXyVEvqA.png) 一樣用直接賦值的方式會改動到原來的物件。 使用`Object.assign()`: ```javascript const obj2 = Object.assign({}, person, { number: 10, age: 12 }); console.log("person", person); console.log("obj2", obj2); ``` ![image](https://hackmd.io/_uploads/ryEh4EwqR.png) 如此一來就不會改動原本的物件了。 看看另一個例子: ```javascript= const wes = { name: "wes", age: 20, social: { twitter: "@wesbos", facebook: "wesbos.developer", }, }; const dev = Object.assign({}, wes); dev.name = "Wesley"; console.log("wes", wes); console.log("dev", dev); ``` ![image](https://hackmd.io/_uploads/r1jqjNPq0.png) 目前看起來很正常,但當我改動裡面屬性中的物件時: ```javascript dev.social.twitter = "@cool"; ``` ![image](https://hackmd.io/_uploads/SkemnVv5R.png) 被一起改動了,`Object.assign()`這個方法只有第一層的屬性會隔離,進到第二層後還是回有問題,所以就要用JSON方法來先變成字串再轉回物件: ```javascript= const dev2 = JSON.parse(JSON.stringify(wes)); dev2.social.twitter = "@cool"; console.log("wes", wes); console.log("dev2", dev2); ``` ![image](https://hackmd.io/_uploads/By9yRNw50.png) 這樣就不會改動到裡面任何一層的東西了。 # 15-LocalStorage and Event Delegation 這個章節要做一個可以新增且勾選的清單。 首先選取新增按鈕跟即將展示的列表: ![image](https://hackmd.io/_uploads/S1zT8td5C.png) ```javascript const addItems = document.querySelector(".add-items"); const itemsList = document.querySelector(".plates"); const items = []; ``` 註冊事件: ```javascript= function addItem(e) { e.preventDefault(); const text = this.querySelector("[name=item]").value; const item = { text, done: false, }; items.push(item); this.reset(); } addItems.addEventListener("submit", addItem); ``` 首先我們需要`e.preventDefault()`來停止它重新刷新頁面,不然我們`console.log`任何東西都會馬上被洗掉。 然後建立一個物件來存放使用者在`input`中輸入的內容及勾選狀態(預設是不勾選),最後存進`items`裡面,然後清空輸入匡。 ![image](https://hackmd.io/_uploads/S1HOsFK90.png) 現在送出後查看`items`就可以看到裡面加入了剛剛輸入的內容。 再來要將加入的項目呈現在上方的列表中,寫另一個函式來做這件事: ```javascript= function populateList(plates = [], platesList) { platesList.innerHTML = plates .map((plate, i) => { return ` <li> <input type="checkbox" data-index="${i}" id="item${i}" ${ plate.done ? "checked" : "" } /> <label for="item${i}">${plate.text}</label> </li> `; }) .join(""); } ``` ![image](https://hackmd.io/_uploads/BkALW9Y5R.png) 將傳入的`platesList`區塊HTML代碼加入清單中的項目,稍微複查的是我們會先將陣列中的物件全部帶換成用`<li>`包起來的代碼,先是做一個`checkbox`然後放入`data-index`、`id`,然後判斷他的`done`狀態來決定要不要加上`checked`,然後就是放旁邊的文字連結到剛剛的`checkbox`。 這個函式在網頁刷新要執行一次,然後`addItem`裡面也要執行一次 ![image](https://hackmd.io/_uploads/SkeGz9KqA.png) 目前的成果已經可以新增項目且勾選他,但只要重整後就會消失,所以上面如果拿掉`e.preventDefault()`就會每次`submit`東西就不見,所以現在我們需要`localStorage`: ```javascript localStorage.setItem(`items`, items); ``` ![image](https://hackmd.io/_uploads/S17X45Y9C.png) 存進去後發現物件無法被正確解讀,所以需要先用`JSON.stringify()`方法來將它轉成字串: ```javascript localStorage.setItem(`items`, JSON.stringify(items)); ``` ![image](https://hackmd.io/_uploads/ByoeEcK90.png) 這樣就能確保資料正確。 但每次刷新時列表上的清單就會消失,所以我們必須在每次刷新時都把`localStorage`的資料再放到清單中,所以最一開始宣告得空陣列`items`就會變成: ```javascript const items = JSON.parse(localStorage.getItem("items")) || []; ``` ![image](https://hackmd.io/_uploads/r11AH5t9R.png) `JSON.parse()`將剛剛存好的`json`格式再轉回物件。 再來要將勾選的狀態也記錄下來,目前的狀態是你勾選以後重整就會回到沒勾選的狀態,所以註冊一個`click`事件來改變`checkbox`的狀態,如果這邊對`checkbox`註冊會有一些問題,因為`checkbox`在每次新增項目都會重新生成(只要執行`populateList`就會重新生成),這樣註冊的事件也會跟著消失,所以我們需要註冊再更外面的元素`itemsList`,不管裡面怎麼改都不會影響到, ```javascript= function toggleDone(e) { console.log(e); } itemsList.addEventListener("click", toggleDone); ``` ![image](https://hackmd.io/_uploads/Hk5do9K9A.png) 這顯然不是我們要的,但打開裡面找到了`target`,看起來就像了: ![image](https://hackmd.io/_uploads/rJvqo5FqA.png) 所以改拿`e.target`: ![image](https://hackmd.io/_uploads/H1qTj5YcC.png) 同時娶到了兩個元素,需要過濾掉不是`input`的元素, ```javascript function toggleDone(e) { console.log(e); if (!e.target.matches("input")) return; } ``` 再來就是去改動那個項目的`done`狀態了: ```javascript function toggleDone(e) { console.log(e); if (!e.target.matches("input")) return; const index = e.target.dataset.index; items[index].done = !items[index].done; localStorage.setItem(`items`, JSON.stringify(items)); populateList(items, itemsList); } ``` 一樣每次改完狀態就重新更新一次`localStorage`然後重新編寫一次`itemsList`。 差不多就完成了~ # 16-CSS Text Shadow Mouse Move Effect ![image](https://hackmd.io/_uploads/B1kSTRYcR.png) 這個章節要做文字的陰影,並跟著滑鼠的位置調整陰影的方位,首先一樣取元素,然後註冊事件: ```javascript const hero = document.querySelector(".hero"); const text = hero.querySelector("h1"); function shadow(e) { const { offsetHeight: height, offsetWidth: width } = hero; const { offsetX: x, offsetY: y } = e; const xWalk = x / width; const yWalk = y / height; console.log(xWalk, yWalk); } hero.addEventListener("mousemove", shadow); ``` 因為我們要求的是滑鼠在`hero`範圍內的相對位置,所以用滑鼠的位置除以`hero`的寬高,所以就得到了相對百分比,`hero`的最左上角為(0, 0),最右下角為(1, 1): ![image](https://hackmd.io/_uploads/S1VxgkcqR.png) --- ![image](https://hackmd.io/_uploads/HkmkMJ99C.png) 但現在遇到的問題是當滑鼠移動到這個`h1`區塊,他的`offsetX`跟`offsetY`就會變成在`h1`裡面(可以印出`e.target`來觀察到),所以這時滑鼠在紅匡的左上角就變成(0, 0),右下角(1, 1),為了不讓他有這個斷層,我們需要在滑鼠移動到`h1`區塊裡的時候做一點調整: ```javascript if (this !== e.target) { x = x + e.target.offsetLeft; y = y + e.target.offsetTop; } ``` 因為會改動到x, y,所以上面本來使用`const`宣告要改成`let` 所以移動到`h1`的最左邊時本來`x`會是`0`,我們需要加上`h1`與外匡最左側的距離;`h1`的最上面`y`會是`0`,加上`h1`與外匡頂部的距離。 所以現在函式會是: ```javascript= function shadow(e) { const { offsetHeight: height, offsetWidth: width } = hero; let { offsetX: x, offsetY: y } = e; if (this !== e.target) { x = x + e.target.offsetLeft; y = y + e.target.offsetTop; } const xWalk = x / width; const yWalk = y / height; console.log(xWalk, yWalk); } ``` 解決斷層問題後要來設定陰影的數值了: 先想好想要的最大偏移量是多少,假設是`50px`,那shadow的偏移量應該範圍會在`-50px ~ 50px`,這邊他給的公式是: ```javascript const walk = 100; const xWalk = Math.round((x / width) * walk) - walk / 2; const yWalk = Math.round((y / height) * walk) - walk / 2; ``` 這樣範圍就會落在`-50px ~ 50px`了,`Math.round()`去掉不必要的小數,好難懂:cry: 最後在改動`text`的陰影樣式,所以整個shadow會是: ```javascript= function shadow(e) { const { offsetHeight: height, offsetWidth: width } = hero; let { offsetX: x, offsetY: y } = e; if (this !== e.target) { x = x + e.target.offsetLeft; y = y + e.target.offsetTop; } const xWalk = Math.round((x / width) * walk) - walk / 2; const yWalk = Math.round((y / height) * walk) - walk / 2; text.style.textShadow = `${xWalk}px ${yWalk}px 0 red`; } ``` 就做完了,也可以加上好幾個陰影,然後設定不同方向: ```javascript text.style.textShadow = `${xWalk}px ${yWalk}px 0 red, ${xWalk * -1}px ${yWalk * -1}px 0 blue, ${xWalk}px ${yWalk * -1}px 0 green, ${xWalk * -1}px ${yWalk}px 0 yellow`; ``` ![image](https://hackmd.io/_uploads/SkEjFJ990.png) # 17-Sorting Band Names without articles 這章要將陣列中的字串做重新排序,但必須忽視`articles`(這邊是指`a`、`an`跟`the`等冠詞),然後展示在網頁中。 影片中去除冠詞的方法是用正規表達式,目前我想不出其他方法:cry: ```javascript= function removeRedundantWords(bandName) { return bandName.replace(/^(a |an |the )/i, "").trim(); } const sortedBands = bands.sort((a, b) => removeRedundantWords(a) > removeRedundantWords(b) ? 1 : -1 ); document.querySelector("#bands").innerHTML = sortedBands .map((band) => `<li>${band}</li>`) .join(""); ``` ![image](https://hackmd.io/_uploads/S145bl59C.png) 非常簡單~ # 18-Tally String Times with Reduce 這個章節要將每個`<li>`元素內的時間用`reduce`加總((終於有自己可以做做看的了:cry: 我先確認看看我能不能拿到時間: ```javascript const videos = [...document.querySelectorAll("[data-time]")]; function getTime() { console.log(this.dataset.time); } videos.forEach((video) => video.addEventListener("click", getTime)); ``` ![image](https://hackmd.io/_uploads/r1Sr2kocC.png) 可以! 再來初步的想法是這樣,感覺有短雜亂: ```javascript= const videos = [...document.querySelectorAll("[data-time]")]; const totalTime = videos.reduce( (total, video) => { const timeArray = video.dataset.time .split(":") .map((e) => Number(e)); return total.map((e, i) => e + timeArray[i]); }, [0, 0] ); console.log(totalTime); const time = { hours: Math.floor(totalTime[0] / 60), mins: (totalTime[0] % 60) + Math.floor(totalTime[1] / 60), seconds: totalTime[1] % 60, }; console.log(time); ``` ![image](https://hackmd.io/_uploads/HyVqoys5C.png) 總之暫時成功了~ 影片中的解法是將時間先全部換成秒,再用秒去幾算小時跟分鐘,邏輯上更簡單了: ```javascript= const totalSeconds = videos.reduce((total, video) => { const timeArray = video.dataset.time.split(":").map(parseFloat); const seconds = timeArray[0] * 60 + timeArray[1]; return total + seconds; }, 0); let seconds = totalSeconds; const hours = Math.floor(seconds / 3600); seconds %= 3600; const mins = Math.floor(seconds / 60); seconds %= 60; console.log(hours, mins, seconds); ``` 我沒有完全照抄,但邏輯應該幾乎一樣~ 大功造成! > 告 > [name=橘子] > sorry, 大功告成... > [name=Jermy] # 19-Unreal Webcam Fun 將鏡頭的畫面投射到canvas上,做出截圖、調濾鏡功能。 前面有些他已經幫你取好的元素: ```javascript 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()`來拿到使用者的設備: ```javascript= function getVideo() { navigator.mediaDevices .getUserMedia({ video: true, audio: false }) .then((localMediaStream) => { video.srcObject = localMediaStream; video.play(); }) .catch((err) => { console.error("NO!", err); }); } getVideo(); ``` [Navigator: mediaDevices property](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/mediaDevices) 因為這個方法會得到一個promise,所以我們用`.then`去接他,將它放到video裡面,然後video開始播放,我們就能在右上角的`.player`區塊看到鏡頭畫面了! ![image](https://hackmd.io/_uploads/Bya69GXsR.png) --- 再來將它投射到canvas畫板上: ```javascript= 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); }, 16); } video.addEventListener("canplay", paintToCanvas); ``` 取得影片的長寬,並將canvas的長寬設定跟影片一樣(不懂),接著使用`ctx.drawImage(video, 0, 0, width, height)`將video的畫面投射到畫布`ctx`上,因為投射只會是圖片,所以我們使用`setInterval`設定每16毫秒更新一次,就可以流暢起來了,最後就是在video上註冊事件,當影片可以播放的時候(有接到鏡頭畫面)就觸發`paintToCanvas`。 [CanvasRenderingContext2D.drawImage()](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage) 接著來做截圖功能,我們可以觀察到`html`裡已經有做一個`button`點擊呼叫`takePhoto()`: ![image](https://hackmd.io/_uploads/rJuM17XsR.png) 所以我們就來寫一個`takePhoto`函式: ```javascript= function takePhoto() { snap.currentTime = 0; snap.play(); const data = canvas.toDataURL("image/jpeg"); const link = document.createElement("a"); link.href = data; link.setAttribute("download", "screenshot"); link.innerHTML = `<img src="${data}" alt="screenshot" />`; strip.insertBefore(link, strip.firstChild); } ``` `snap`是一個快門聲的音檔,所以每次觸發函式時就播放一下: ![image](https://hackmd.io/_uploads/H1lo9kQ7jA.png) * 使用`canvas.toDataURL()`方法來建立一個連結通往當下的這張截圖 * 在`document`建立`<a>`標籤並將剛剛的連結設定給他 * 將標籤的屬性`download`設定`screenshot`(設定存檔的檔名) ![image](https://hackmd.io/_uploads/BkJUNfEs0.png) * 將`<img>`放進連結中,連結設定成剛剛的`data` * 插入`strip`區塊中 [insertBefore()](https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore) 做濾鏡,加一些東西到剛剛的`paintToCanvas`: ```javascript= function paintToCanvas() { 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); //先取得畫面每個pixel的數值 let pixels = ctx.getImageData(0, 0, width, height); //將它每個pixel都修改色調 pixels = redEffect(pixels); //再放回畫面上 ctx.putImageData(pixels, 0, 0); }, 16); } function redEffect(pixels) { for (let i = 0; i <= pixels.data.length; i += 4) { pixels.data[i + 0] = pixels.data[i + 0] + 100; //r pixels.data[i + 1] = pixels.data[i + 1] - 50; //g pixels.data[i + 2] = pixels.data[i + 2] * 0.5; //b } return pixels; } ``` --- 先取得畫面的資訊: ```javascript let pixels = ctx.getImageData(0, 0, width, height); console.log(pixels); ``` 印出來可以看到一個超大的陣列,這個陣列就是每一個pixel的rgba的值: ![image](https://hackmd.io/_uploads/SkRmvzEj0.png) 所以這裡的`(24, 22, 18, 255)`是一組(四個一組),代表`(red, green, blue, alpha)` 接著我們用`redEffect()`來調整他的色調: ```javascript pixels = redEffect(pixels); ``` 所以底下的`for`迴圈`i`會用`+4`的方式來循環: ```javascript for (let i = 0;i <= pixels.data.length; i += 4) { pixels.data[i + 0] = pixels.data[i + 0] + 100; //r pixels.data[i + 1] = pixels.data[i + 1] - 50; //g pixels.data[i + 2] = pixels.data[i + 2] * 0.5; //b } return pixels; ``` 這邊是影片調整的數據,紅色加深、綠色藍色減少。 調整完以後再放回`ctx`畫布上: ```javascript ctx.putImageData(pixels, 0, 0); ``` ![image](https://hackmd.io/_uploads/Sky57XVi0.png) 就可以看到畫面變紅了! 做另一個色塊偏移的效果`rgbSplit()`: ```javascript function rgbSplit(pixels) { for (let i = 0; i <= pixels.data.length; i += 4) { pixels.data[i - 100] = pixels.data[i + 0]; //r pixels.data[i + 500] = pixels.data[i + 1]; //g pixels.data[i - 550] = pixels.data[i + 2]; //b } return pixels; } ``` 原理是將色塊的位置平行移動,結果會像這樣: ![image](https://hackmd.io/_uploads/r1GutfVsR.png) 最後來做去除某個顏色的效果,(如果背景用綠幕然後去除綠色就能做到去背的效果): ```javascript function greenScreen(pixels) { const levels = {}; document.querySelectorAll(".rgb input").forEach((input) => { levels[input.name] = input.value; }); for (let 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 && red <= levels.rmax && green >= levels.gmin && green <= levels.gmax && blue >= levels.bmin && blue <= levels.bmax ) { pixels.data[i + 3] = 0; } } return pixels; } ``` 首先要先打開`html`被封印的程式碼(`.rgb`區塊的所有內容,用來調整刻度的)將這些`input`中的值放進`levels`物件中,接著判斷每個pixel的rgb值有沒有在範圍內,在範圍內的`alpha`透明度就設為`0`,然後我們就可以調整去背的範圍了~ ![image](https://hackmd.io/_uploads/BkijzmNiA.png) 這邊還有介紹一個效果,調整整個影像的透明度:[CanvasRenderingContext2D.globalAlpha](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalAlpha) Chris補充的關於圖片檔案格式的細節:[BMP](https://zh.wikipedia.org/zh-tw/BMP) 其中我們前面拿到的`imageData`中的陣列就是連結裡面的『像素陣列』這個部分。 # 20-Native Speech Recognition 透過麥克風將接收到的文字添加到網頁中。 首先要處理SpeechRecognition的相容性問題,如果原來的`window.SpeechRecognition`不存在就用`window.webkitSpeechRecognition`: ```javascript window.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; ``` --- 然後就建構出`recognition`並設置`interimResults = true`: ```javascript const recognition = new SpeechRecognition(); recognition.interimResults = true; ``` 設置這個的用意是他會不斷更新目前結果,不會等到使用者全部講完才一次將結果吐出來。 --- 然後就來嘗試將辨識到的內容加入網頁中: ```javascript= recognition.addEventListener("result", (e) => { console.log(e) }); recognition.start(); ``` 事件`"result"`會在語音辨識成功並轉成文字後觸發,然後我們需要`recognition.start()`來開啟麥克風接收,我們將接收到的東西印出來觀察一下: ![image](https://hackmd.io/_uploads/rJnP2_ViR.png) 可以發現`SpeechRecognitionEvent`裡面的`results`的`transcript`有我們要的結果!! 將他整理一下放到`transcript`印出來看看: ```javascript= recognition.addEventListener("result", (e) => { const transcript = Array.from(e.results) .map((result) => result[0].transcript) .join(""); console.log(transcript); }); ``` ![image](https://hackmd.io/_uploads/By6gwDHo0.png) 成功了!但是現在的情況會是只接收一次,結束就沒了,所以用: ```javascript recognition.addEventListener("end", recognition.start); ``` 這樣就能在每次接收結束後馬上開啟另一次的接收! --- 接著我們想要放到網頁上: ```javascript let p = document.createElement("p"); const words = document.querySelector(".words"); words.appendChild(p); ``` 在全域中建立`p`並且放到`.words`區塊中: ![image](https://hackmd.io/_uploads/Byu7KvSj0.png) --- 將剛剛事件中拿到的`transcript`加入`p`: ```javascript recognition.addEventListener("result", (e) => { const transcript = Array.from(e.results) .map((result) => result[0].transcript) .join(""); p.textContent = transcript; }); ``` ![image](https://hackmd.io/_uploads/B14GcvBjA.png) 有成功放入,但每次講話`p`裡面的內容都被取代掉,我們想做的可能是每句話都會新建一個段落,其實之前的物件中有一個屬性可以使用: ![image](https://hackmd.io/_uploads/H1dWswBsR.png) `isFinal`如果是`false`表示還沒變是完全,當他變`true`表示已經辨識完成,那我們就可以開始下一個`p`的建立,所以變成: ```javascript recognition.addEventListener("result", (e) => { const transcript = Array.from(e.results) .map((result) => result[0].transcript) .join(""); p.textContent = transcript; if (e.results[0].isFinal) { p = document.createElement("p"); words.appendChild(p); } }); ``` 當辨識完成後我們就建立另一個新的`p`然後放進`.words`裡面: ![image](https://hackmd.io/_uploads/rk8qADrjA.png) 這樣就做完了~ 影片中還介紹我們可以判斷一些關鍵字,讓使用者講出關鍵字後做特定的事: ```javascript if (transcript.includes("爸爸")) { console.log("Did you say dad????"); } ``` 如果使用者講到『爸爸』,就回應特定句子,當然我們也能做更多應用。 ![image](https://hackmd.io/_uploads/ByVZJ_rs0.png)