---
# System prepended metadata

title: J_JS30 11-20
tags: [DOM, javascript]

---

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