# 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);
```

我們改動看看其中一個陣列:
```javascript=
team[0] = "Abc";
console.log("players", players);
console.log("team", team);
```

結果兩個都被改動了,所以我們不能用這種方式複製一個陣列。
1. 使用`Array.prototype.slice()`:
```javascript=
const team2 = players.slice();
team2[0] = "JJJ";
console.log("players", players);
console.log("team2", team2);
```

如此一來就不會改動到原來陣列了。
2. 再來試試看`Array.prototype.concat()`:
```javascript=
const team3 = [].concat(players);
team3[0] = "FFF";
console.log("players", players);
console.log("team3", team3);
```

也不會改動到原來陣列。
使用展開陣列的方式:
```javascript=
const team4 = [...players];
team4[0] = "WWW";
console.log("players", players);
console.log("team4", team4);
```

一樣不會改動原本陣列。
3. 再來使用`Array.from()`:
```javascript=
const team5 = Array.from(players);
team5[0] = "TTT";
console.log("players", players);
console.log("team5", team5);
```

---
再來是物件的複製:
```javascript=
const person = {
name: "Wes Bos",
age: 80,
};
const obj = person;
obj.number = 10;
console.log("person", person);
console.log("obj", obj);
```

一樣用直接賦值的方式會改動到原來的物件。
使用`Object.assign()`:
```javascript
const obj2 = Object.assign({}, person, { number: 10, age: 12 });
console.log("person", person);
console.log("obj2", obj2);
```

如此一來就不會改動原本的物件了。
看看另一個例子:
```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);
```

目前看起來很正常,但當我改動裡面屬性中的物件時:
```javascript
dev.social.twitter = "@cool";
```

被一起改動了,`Object.assign()`這個方法只有第一層的屬性會隔離,進到第二層後還是回有問題,所以就要用JSON方法來先變成字串再轉回物件:
```javascript=
const dev2 = JSON.parse(JSON.stringify(wes));
dev2.social.twitter = "@cool";
console.log("wes", wes);
console.log("dev2", dev2);
```

這樣就不會改動到裡面任何一層的東西了。
# 15-LocalStorage and Event Delegation
這個章節要做一個可以新增且勾選的清單。
首先選取新增按鈕跟即將展示的列表:

```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`裡面,然後清空輸入匡。

現在送出後查看`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("");
}
```

將傳入的`platesList`區塊HTML代碼加入清單中的項目,稍微複查的是我們會先將陣列中的物件全部帶換成用`<li>`包起來的代碼,先是做一個`checkbox`然後放入`data-index`、`id`,然後判斷他的`done`狀態來決定要不要加上`checked`,然後就是放旁邊的文字連結到剛剛的`checkbox`。
這個函式在網頁刷新要執行一次,然後`addItem`裡面也要執行一次

目前的成果已經可以新增項目且勾選他,但只要重整後就會消失,所以上面如果拿掉`e.preventDefault()`就會每次`submit`東西就不見,所以現在我們需要`localStorage`:
```javascript
localStorage.setItem(`items`, items);
```

存進去後發現物件無法被正確解讀,所以需要先用`JSON.stringify()`方法來將它轉成字串:
```javascript
localStorage.setItem(`items`, JSON.stringify(items));
```

這樣就能確保資料正確。
但每次刷新時列表上的清單就會消失,所以我們必須在每次刷新時都把`localStorage`的資料再放到清單中,所以最一開始宣告得空陣列`items`就會變成:
```javascript
const items = JSON.parse(localStorage.getItem("items")) || [];
```

`JSON.parse()`將剛剛存好的`json`格式再轉回物件。
再來要將勾選的狀態也記錄下來,目前的狀態是你勾選以後重整就會回到沒勾選的狀態,所以註冊一個`click`事件來改變`checkbox`的狀態,如果這邊對`checkbox`註冊會有一些問題,因為`checkbox`在每次新增項目都會重新生成(只要執行`populateList`就會重新生成),這樣註冊的事件也會跟著消失,所以我們需要註冊再更外面的元素`itemsList`,不管裡面怎麼改都不會影響到,
```javascript=
function toggleDone(e) {
console.log(e);
}
itemsList.addEventListener("click", toggleDone);
```

這顯然不是我們要的,但打開裡面找到了`target`,看起來就像了:

所以改拿`e.target`:

同時娶到了兩個元素,需要過濾掉不是`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

這個章節要做文字的陰影,並跟著滑鼠的位置調整陰影的方位,首先一樣取元素,然後註冊事件:
```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):

---

但現在遇到的問題是當滑鼠移動到這個`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`;
```

# 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("");
```

非常簡單~
# 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));
```

可以!
再來初步的想法是這樣,感覺有短雜亂:
```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);
```

總之暫時成功了~
影片中的解法是將時間先全部換成秒,再用秒去幾算小時跟分鐘,邏輯上更簡單了:
```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`區塊看到鏡頭畫面了!

---
再來將它投射到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()`:

所以我們就來寫一個`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`是一個快門聲的音檔,所以每次觸發函式時就播放一下:

* 使用`canvas.toDataURL()`方法來建立一個連結通往當下的這張截圖
* 在`document`建立`<a>`標籤並將剛剛的連結設定給他
* 將標籤的屬性`download`設定`screenshot`(設定存檔的檔名)

* 將`<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的值:

所以這裡的`(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);
```

就可以看到畫面變紅了!
做另一個色塊偏移的效果`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;
}
```
原理是將色塊的位置平行移動,結果會像這樣:

最後來做去除某個顏色的效果,(如果背景用綠幕然後去除綠色就能做到去背的效果):
```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`,然後我們就可以調整去背的範圍了~

這邊還有介紹一個效果,調整整個影像的透明度:[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()`來開啟麥克風接收,我們將接收到的東西印出來觀察一下:

可以發現`SpeechRecognitionEvent`裡面的`results`的`transcript`有我們要的結果!!
將他整理一下放到`transcript`印出來看看:
```javascript=
recognition.addEventListener("result", (e) => {
const transcript = Array.from(e.results)
.map((result) => result[0].transcript)
.join("");
console.log(transcript);
});
```

成功了!但是現在的情況會是只接收一次,結束就沒了,所以用:
```javascript
recognition.addEventListener("end", recognition.start);
```
這樣就能在每次接收結束後馬上開啟另一次的接收!
---
接著我們想要放到網頁上:
```javascript
let p = document.createElement("p");
const words = document.querySelector(".words");
words.appendChild(p);
```
在全域中建立`p`並且放到`.words`區塊中:

---
將剛剛事件中拿到的`transcript`加入`p`:
```javascript
recognition.addEventListener("result", (e) => {
const transcript = Array.from(e.results)
.map((result) => result[0].transcript)
.join("");
p.textContent = transcript;
});
```

有成功放入,但每次講話`p`裡面的內容都被取代掉,我們想做的可能是每句話都會新建一個段落,其實之前的物件中有一個屬性可以使用:

`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`裡面:

這樣就做完了~
影片中還介紹我們可以判斷一些關鍵字,讓使用者講出關鍵字後做特定的事:
```javascript
if (transcript.includes("爸爸")) {
console.log("Did you say dad????");
}
```
如果使用者講到『爸爸』,就回應特定句子,當然我們也能做更多應用。
