2018年4月24日 星期二

[APP] 如何撰寫 Typora 中 markdown 的客製化樣式


此文件翻譯自 Write Custom Theme for Typora @ Typora 官網。翻譯時間為 2018-04-24。

總結

如果你想要為 Typora 撰寫客製化的樣式,你需要:
  1. 建立一個新的 css 檔案,這個檔案的名稱不能包含大寫字母或空格,例如,my-typora-theme 就是一個有效的檔案名稱。
  2. 寫 CSS 檔案。
  • 我們準備了一個 toolkit 讓你可以快速開始,並做一些簡單的測試。
  • 如果你是要從開始寫,可以從裡面的 template.less 開始。
  • 如果你想要套用 Wordpress 或 Jekyll 等現有主題的 css 檔案,只要把內容複製下來,加上那些在 css 檔案中沒有涵括到的部分樣式,像是 "toc" 的樣式,或是其他的 UI 元素。
  1. 檢測/除錯你的 CSS 檔案:
  • 將你所建立的 CSS 檔案放入 toolkit/theme/test.css 中,並記得放入你引用到的圖片或字體檔,接著打開在 toolkit/coretoolkit/eletron 內的 HTML 檔案來預覽你的 CSS。若你的作業系統是 Mac 則使用 Safari 來開啟該檔案,若是在 Linux/Windows 下,則使用 Chrome 來開啟。
  • 接著跟著 how to install custom theme 的說明來將你的樣式安裝到 Typora 上作為測試。
  1. 如果你想要分享你的主題,只要 fork 一份,並發送 PR 到 Typora Theme Gallery 上即可。

基本

  1. css 命名的規則:不要使用大寫字母,將空白以 - 替換掉,typora 會將它們轉換成在選單中可讀的名稱。舉例來說,my-first-typora-theme.css 會在 typora 的 "Themes" 選單項目中變成 "My First Typora Theme"
  2. 將預設的字體大小放到 html 中,接著像是 h1p 則使用 rem 作為單位,否則的話,在偏好設定中所設定的客製化字體大小將不會生效。
  3. Typora 是在 macOS 上是透過 Webkit,在 Windows/Linux 上是透過 Chromium,因此需使用 Chrome 或 Safari 所支援的 css 樣式。
  4. 有些 CSS 的調整可能會使得 Typora 產生未預期的情況,例如在 #write 將上 white-space: pre-wrap;,將使得編輯時無法透過 Tab 鍵來插入 \t,因此請盡可能不要複寫預設的 CSS 樣式,並且試試看會不會產生錯誤。

我該用哪個 CSS 選擇器?

一般來說

html:Typora 的視窗內容是一個網頁,因此請把 background, font-family 或起它通用的屬性添加到 html 標籤上。在 Mac 上如果,如果你使用了 seamless window style ,那麼工具列的背景顏色將會套用在 html 上的 background-color 樣式。
#write:寫作的區域是套個 #write,改變它的 width, height, padding 將會調整寫作區域的尺寸。你所設定的屬性,像是添加在 html 上的 color 樣式,會套用在整個 window 內容; UI 也是,像是插入表格時視窗的文字顏色。因此,**如果你只想要改變寫作區域的樣式而不影響到 UI 的部分,則可以把樣式放在 #write 上。
/** example **/
html, body {
  background-color: #fefefe; /*background color of the window and titlebar*/
  font-family: helvetica, sans-serif; /*custom font*/
  ...
}

html {
  font-size: 14px; /*default font size*/
}

#write {
  max-width: 90%; /*adjust size of the wriring area*/
  font-size: 1rem; /*basic font size*/
  color: #555; /*basic font color*/
  ...
}
Typora 會試圖渲染所有 markdown 中的元素,所以段落會被 <p> 標籤包住,清單會被 <ul><ol> 包住,就如同其他 Markdown 處理器一樣,因此你可以透過改變這些 HTML 標籤的樣式來改變它們的外觀。也因此,Wordpress 或其他靜態頁面所使用的 CSS 檔案也會影響 Typora 中大部分的樣式,你可以直接「移植」這些 CSS 規則過來,並添加缺少的樣式或做些調整就好。

Block Elements

如果前面所述,Typora 會是的渲染所有 Markdown 的元素,例如段落用 <p>、表格用 <table>、第一街的標題用 <h1>,等等,你可以改變它們的樣式:
p {...}
h1 {...}
table {...}
table th td {...}
table tr:nth-child(2n) td {...}
...
你可以在這些選擇器前面加上 #write 讓它們只套用於寫作區域而不會影響到其他的控制元件,例如,某些對話方框中的標題是套用 h4 的標籤:
/*this will only aplly to h4 in dialogs popped up by typora (just an example)*/
.dialog h4 {...} 

/*this will only apply to h4 inside writing area, which is generated after user input "#### " */
#write h4 {...} 
此外,所有的區塊元素都有 mdtype 這個屬性,舉例來說,你可以透過 [mdtype="heading"] 來選到標題,其他的類型像是 paragraph, heading, blockquote, fences, hr, def_link, def_footnote, table, meta_block, math_block, list, toc, list_item, table_row, table_cell, line但大部分的情況下,使用 HTML 標籤就已經非常足夠
mdtype Output Css Selector Explanation
paragraph p
line .md-line A paragraph can contain one or more .md-line
heading h1~h6
blockquote blockquote
list (unordered list) ul li
list (ordered list) ol li
list (task) ul.task-list li. task-list-item
toc .md-toc Also refer to [this doc][toc]
fences (before codemirror is initialized) pre.md-fences.mock-cm
fences pre.md-fences please refer to “Code Fences” section
diagrams pre[lang=’sequence’], pre[lang=’flow’], pre[lang=’mermaid’] They are special code fences with certain code language.
hr hr
def_link .md-def-link with children .md-def-name, .md-def-content, .md-def-title
def_footnote .md-def-footnote with children .md-def-name, .md-def-content
meta_block pre.md-meta-block content for YAML front matters
math_block [mdtype=”math_block”] preview part is .mathjax-block, html content is generated via MathJax. TeX editor is powered by CodeMirror, please refer to “Code Fences” section
table table thead tbody th tr td

Lines

Typroa 會渲染如實的渲染斷行,因此,一個段落中會透過 \n 來包含許多行,而 .md-line 就是用來選擇 <p> 中的每一行。

Code Fences(程式碼區塊)

程式碼高亮的效果是透過 CodeMirror 的功能來達到,因此可以參 這份文件 來檢視更多細節。

Mermaid(流程圖)

Markdown 中的流程圖式透過 Mermaid 完成。

Inline Elements

行內元素也會如同多數的 markdown 解析器一樣的被渲染,因此你可以使用:
strong {
  font-weight: bold;
}
em {..}
code {..}
a {..}
img {..}
mark {..} /*highlight*/
行內元素通常會被 span 、meta syntax 或最後輸出的行內元素所包住,例如,**strong** 會被渲染成:
<!--wrapper for strong element-->
<span md-inline="strong" class=""> 
  
  <!--meta syntax for strong element-->
  <span class="md-meta md-before">**</span> 
  
    <!--output for strong element-->
    <strong>
      <!--inner output-->
      <span md-inline="plain">strong</span> 
    </strong>
  
   <!--meta syntax for strong element-->
  <span class="md-meta md-after">**</span>
</span>
如你所見,整個行內元素被帶有 md-inline 屬性的 span 所包住,用來指稱解析後的結果,其他可能的屬性包含(有些行內元素需要在偏好設定中開啟):
md-inline syntax Output Tag
plain plain span
strong **strong** strong
em *em* em
code code code
underline <u>underline</u> u
escape \( span
tag <button>
del ~~del~~ del
footnote ^1 sup
emoji :smile: span
inline_math $x^2$ span
subscript ~sub~ sub
superscript ^sup^ sup
linebreak (two whitespace at end of a line)
highlight ==highlight== mark
url http://typora.io a
autolink <http://typora.io> a
link [link](href) a
reflink [link][ref] a
image ![img](src) img
refimg ![img][ref] img
下面會說明 Typora 如何為行內的 markdown 語法添加樣式,像是 *_ ,這些通常會在 Typroa 中被隱藏起來,而你通常也不需要個別為它們設定 CSS 規則。
大部分的語法像是 **== 會在你將 markdown 轉換成 HTML 後消失,因此他們被 md-meta 的 class 所包住,並且預設會帶有 display:none ,一些像是 markdown 中圖片的語法預設會被隱藏,並且被以 md-content 的 class 所包住。當你的游標在這個行內元素時,被關注(focus)的那個將會被 md-expand class 所包住,接著 .md-meta.md-content 會變成可見,所以如果你想改變它們的外觀,將樣式套用在 .md-meta.md-content

原始碼模式(Source Code Mode)

原始碼模式(SourceCode Mode)是透過 CodeMirror 的加持,所以它們所使用的語法高亮和程式碼區塊的樣式是一樣的(檢視更多細節)。需要留意的是,**程式碼區塊使用 codemirror 主題的 .cm-s-inner ,但在程式碼模式下,codemirror 的主題是使用 .cm-s-typora-default ,所以 CSS 像這樣:
.cm-s-typora-default .cm-header {
  /*styles for h1~h6 in source code mode*/
}

專注模式(Focus Mode)

關於這個主題,可以參考這份文件

客製化字體(Custom Font)

關於這個主題,可以參考這份文件(目前暫無連結)。

背景(Background)

關於這個主題,可以參考 這份文件

控制 UI(Controller UI)

大部分的 UI 元件包括提示項目(tooltip)、對話框(dialog)和按鈕都是透過 HTML 所繪製。當你完成上面調整樣式的步驟後,如果發現這些 UI 和你的主題不搭時,你可以改變這些部分。在 toolkit 中的 HTML 檔案包含了大部分常用的 UI 元件,方便你可以除錯。

適用於 Windows/Linux 的其他 UI

相較於 macOS 的版本,Windows/Linux 版本的 Typora 使用了更多 HTML 的元素,包含清單(context menu)、偏好設定(preference panel)、甚至是視窗的外框(如果你在 Windows 上使用的是 "unibody" 的 window style)。
toolkit 中的 HTML 檔案包含了大部分常用的 UI 元件,方便你可以除錯。

列印(Print)

在下面的區塊中所撰寫的 CSS 將只會套用到列印或匯出成 PDF 時套用:
@media print {
    /* for example: */
    .typora-export * {
        -webkit-print-color-adjust: exact;
    }
    /* add styles here */
}

除錯和測試(Debug and Test)

在瀏覽器中測試

我們在 toolkithtml-preview 資料夾內提供的 HTML 檔案讓你可以透過 Safari 或 Chrome 來預覽你的主題。使用它們時,重新命名你的檔案並將 CSS 檔案放在 html-preview/theme/test.css 內。

在 Typora 中測試

跟著這份文件可以學習如何將主題安裝到 Typora 上。
對於 Mac 的使用者,在工具列「說明」的選單中可以勾選 enable debug mode,接著在內容上可以點選「檢閱元件(Inspect Element)」來跳出開發者工具。
對於 Windows/Linux 的使用者,你可以使用從「檢視」中切換 Toggle DevTools 來開關開發者工具。

客製化樣式的技巧和參考文件

相關的文件列在這裡
如果你有好的點子或用法想要分享,可以到 typora-wiki-site 上發送 PR。
Share:

2018年2月5日 星期一

[JS] 用 JavaScript 打造視訊錄影 APP(MediaRecorder API)

keywords: mediaRecorder, mediaStream, mediaDevices, Blob
最近剛好有需要透過瀏覽器來做錄影的操作,於是便和 AndyyouCalvert 一塊研究開始試著實作,這個範例將示範如何透過瀏覽器內建的 MediaStream API 來打造影音錄影的功能。
專案 下載位置 @ Github
做出來的效果類似這樣:
Imgur

閱讀建議

在閱讀這篇文章前,建議已經知道如何透過 JavaScript querySelector() 來選取 DOM 元素;對於 JavaScript 的 addEventListenerPromise 有最基本的觀念即可(知道 .then() 的使用)。
由於需要透過瀏覽器取得系統的攝影機,在 Chrome 的瀏覽器操作下,這個 API 只能在安全的來源(secure origins only)下才能使用,也就是只能在網址來自 HTTPSlocalhost 下才能使用,因此建議直接 clone 專案下來執行(貼到 codepen 或 JSFiddle 可能無法正常執行)。
將程式碼拉下來執行後,可先簡單閱讀程式碼 main.js ,接著選擇依照本文內容逐步了解各 API 用法,或針對不懂的部分進行搜尋。最後重構成自己理解的版本。

環境建置

由於需要透過瀏覽器取得系統的攝影機,在 Chrome 的瀏覽器操作下,這個 API 只能在安全的來源(secure origins only)下才能使用,也就是只能在網址來自 HTTPSlocalhost 下才能使用,因此我們先透過 gulp 建立一個 local server 。
因此你可以先把這個專案下載或 clone 下來,接著執行:
$ npm install
$ npm start
應該就可以把這個專案 run 起來了,預設當 ./src 資料夾內的檔案有變動的時候會自動更新瀏覽器:
Imgur

版面配置

接著簡單做一下 HTML 和 CSS 的 View,主要套用了 Bootstrap 4 使用,但這比較不是這篇的重點,可以直接參考專案中的 ./src/views/index.html./src/sass/style.scss
套用好的版面大概會長的像這樣子:
Imgur
有一些元素是之後會在 JS 中選取的使用的,可以留意下面提到的這幾個元素。
透過 HTML5 的 <video> 元素來帶入影片,分別取 id 為 #inputVideo#outputVideo ,前者是用來顯示攝影機即時的影像,後者會顯示錄好的影像,之後我們會在 JS 中選取這兩個元素。另外有個 .is-recording 的 div 是用來顯示錄影中的提示:
<!-- 透過 <video> 代入影像 -->
<div class="row mb-5 justify-content-center align-items-center">
  <div class="col-md-6 text-center">
    <div class="video-container d-flex align-items-center justify-content-center">
      <div class="is-recording"></div>
      <video id="inputVideo" alt="在這裡錄影" muted>Video stream not available.</video>
    </div>
  </div>

  <div class="col-md-6 text-center">
    <div class="video-container d-flex align-items-center justify-content-center">
      <video id="outputVideo" alt="錄好的畫面將會出現在這" muted>Video stream not available.</video>
    </div>
  </div>
</div>
再來是按鈕的部分可以分成開始錄影 #startBtn, 結束錄影 #stopBtn重新啟動錄影機 #resetBtn 這三個元素:
<!-- 操作按鍵 -->
<div class="row mb-4 justify-content-center align-items-center">
  <div class="col-4 d-flex justify-content-center align-items-center">
    <button id="startBtn" class="btn btn-sm btn-outline-primary">Start Recording</button>
    <button id="stopBtn" class="btn btn-sm btn-outline-danger" style="display:none">Stop Recording</button>
    <button id="resetBtn" class="btn btn-sm btn-outline-info" style="display:none">Restart Recorder</button>
  </div>
</div>
最後有一個 #errorMsg 是當有錯誤訊息產生時放置的地方:
<!-- 顯示錯誤訊息 -->
<div class="row">
  <div class="col-12">
    <div role="alert" id="errorMsg"></div>
  </div>
</div>

透過 Web APIs 錄製視訊畫面

在來就要透過 JS 搭配瀏覽器的 APIs 來實做錄製視訊畫面。這部分讀起來難度雖然不高,但就是很多名詞需要理解,讓我們一步一步看下去,在這篇文章中,我們先大概瞭解每個 API 的用法,最後再把每個範例完整的拼起來。
錄製視訊螢幕的流程大概是這樣的:
  1. 取得使用者視訊鏡頭權限-MediaDevices API
  2. 將取得視訊鏡頭的影音串流即時播放於瀏覽器-HTML5 Video Element
  3. 錄製視訊鏡頭所取得的影音串流-MediaRecorder API
  4. 將錄製的影音串流顯示於瀏覽器-Blob 物件
  5. 關閉視訊鏡頭-MediaStream API
  6. 重新啟動視訊鏡頭與釋放記憶體
提醒:要取得使用者視訊鏡頭,並需是安全來源(secure origin),因此網址需為 https 或 localhost。

1. 取得視訊鏡頭權限-MediaDevices API

MediaDevices API 讓網頁能夠存取與系統連接的媒體裝置,例如相機、麥克風、甚至分享螢幕。我們透過 MediaDevices.getUserMedia() 可以請求並取得使用者的視訊鏡頭。
這個 API 的基本用法是這樣:
  • 透過 constraints 定義想要取得的影音來源
  • 透過 navigator.mediaDevices.getUserMedia(constraints) 來請求影音權限,並且回傳一個 Promise
  • 當取得影音來源時,會透過 .then() 來回傳 MediaStreams(影音串流) ,因此我們可以透過在 function 中代入變數 stream (可自取變數名稱,慣例上用 stream)來取得影音串流的內容,在這裡影音串流的內容其實就是當前透過攝影機和麥克風取得的影音內容。
// 定義要取得的影音內容,包含影像和聲音
let constraints = {
  audio: true,
  video: true
}

// 請求開啟影音裝置
navigator.mediaDevices.getUserMedia(constraints)
  .then(function (stream) {
    // 取得當前裝置的影音串流(stream)
  })
  .catch(function (error) {
  // 如果有錯誤發生
  });
當執行這段語法後,瀏覽去就會去請求使用者的麥克風和相機的權限:
Imgur

2. 將取得視訊鏡頭的影音串流即時播放於瀏覽器-HTML5 Video Element

接著要把透過 MediaDevices.getUserMedia() 將取得的**串流(Stream)**播放於瀏覽器上。我們先選取 HTML 的 <video> 元素:
// <video> element
let inputVideo = document.querySelector('#inputVideo')
要把取得的串流放到 <video> 中有兩種作法,一種是透過 HTMLMediaElement.srcObject 的方式,一種是透過 URL.createObjectURL() 的方式,這兩種方法都可以把串流的內容放到 <video> 中進行播放,但前者並不會在 <video> 上出現 src 的屬性即可播放;後者則是利用在 <video> 上的 src 屬性來讀取影音串流。
由於 HTMLMediaElement.srcObject 目前僅是實驗性的方法,因此如果要用的話,MDN 上建議還是要使用 try ...catch,進行瀏覽器不支援時的處理。因此在這裡我們還是先用 URL.createObjectURL() 的方式將影音串流放到 <video> 中:
let inputVideo = document.querySelector('#inputVideo')

navigator.mediaDevices.getUserMedia(constraints)
  .then(function (stream) {
    inputVideoURL = URL.createObjectURL(stream)
    inputVideo.src = inputVideoURL
    inputVideo.controls = false       // 要不要顯示播放控制器
  })
  .catch(function (error) {
    console.warn('some error occurred' + error)
  });
這時候你會發現瀏覽器已經可以讀取視訊鏡頭上的串流了:
Imgur
但你可能會發現視訊的畫面有些卡頓,這是因為我們沒有讓這個 video 元素播放,所以只有在當瀏覽器畫面重新渲染時(例如捲動 scrollbar 時),才會更新視訊畫面,因此我們可以針對 HTML Video element 監聽 loadedmetadata 這個事件,它會在當媒體檔的 metadata 完成載入時被觸發,這時候在來透過 video.play() 播放:
/* 當媒體的 metadata 載入後即播放媒體 */
inputVideo.addEventListener('loadedmetadata', e => {
  inputVideo.play()
})
這樣就不會有畫面卡頓的問題了。

3. 錄製視訊鏡頭所取得的影音串流-MediaRecorder

要對串流進行錄製的動作,需要使用到 Media Recorder APIMedia Recorder 基本的使用方式是先透過 MediaRecorder() 建構式,給它要錄製的 MediaStream ,即可以建立 MediaRecorder 物件:
/* 建立 mediaRecorder 物件 */
mediaRecorder = new MediaRecorder(stream)
接著 mediaRecorder 有一些方法可以使用,基本的像是:mediaRecorder.start()mediaRecorder.stop() 來開啟和結束錄製串流:
mediaRecorder = new MediaRecorder(stream)
mediaRecorder.start()   // 可使錄製影音串流
mediaRecorder.stop()    // 結束錄製影音串流
預設沒有帶參數的情況下,當停止錄製時,mediaRecorder 會一次丟整包錄製好的檔案回來;如果我們希望每次丟一點丟一點,則可以在 mediaRecorder.start() 中代入參數,它會根據你給予的時間一次回拋一段:
/* 每秒回拋一次錄製的串流 */
mediaRecorder.start(1000)
這裡可以整理出幾個 function:
let startBtn = document.querySelector('#startBtn')
let stopBtn = document.querySelector('#stopBtn')

startBtn.addEventListener('click', onStartRecording)
stopBtn.addEventListener('click', onStopRecording)

// Start Recording: mediaRecorder.start()
function onStartRecording (e) {
  mediaRecorder.start()
}

// Stop Recording: mediaRecorder.stop()
function onStopRecording (e) {
  mediaRecorder.stop()
}
另外,MediaRecorder 也提供一些事件讓我們可以監聽,比較重要的是 dataavailable 這個事件,這個事件會在 MediaRecorder 傳送媒體資料到應用程式以供使用時促發,data 會是包含媒體資料。簡單來說,當有可用資料傳入時,就可以在 dataavailable 事件取得:
/* 監聽 dataavailable 事件,可以用 ondataavailable 或透過 addEventListener 均可 */

MediaRecorder.addEventListener('dataavailable', function(e) {
  e.data    // 取得資料
})
如同前面所述的,如果我們開始錄製時的 mediaRecorder.start() 沒有代入參數,它會在停止錄製時一次丟整包錄製好的檔案回來,這個檔案可以在 dataavailable 事件中取得:
Imgur
但若我們在 mediaRecorder.start(1000) 中代入參數 1000ms ,那麼開始錄製後每隔 1 秒就會回傳一次資料,像下面這樣:
Imgur

4. 將錄製的影音串流顯示於瀏覽器 - Blob 物件

到目前為止簡單整理一下:
  • 首先透過 MediaDevices 物件的方法 MediaDevices.getUserMedia() 可以請求使用者影音裝置的權限,並獲得影音裝置傳回來的串流(stream)
  • 接著透過 MeidaRecorder 物件的方法 mediaRecorder.start()mediaRecorder.stop() 可以開始和結束錄製透過影音裝置傳來的串流;
  • 最後監聽事件 mediaRecorder.ondataavailable 可以在觸發該事件時,透過 function (e) {e.data} 來取得資料。
接著要把透過 dataavailalbe 取得的資料放到瀏覽器的 <video> 上面,從剛剛的說明可以知道 dataavailable 有可能是透過 mediaRecorder.start() 只促發一次(一次送回整包資料),或者透過 mediaRecorder.start(1000) 每隔一定時間回傳一包資料,因此作法上會先建立一個名為 chunks 的陣列,在把取得的資料推進這個陣列中(如果是一次送回整包資料的這種,可以不用 chunks):
let chunks = []
mediaRecorder.addEventListener('dataavailable', mediaRecorderOnDataAvailable)

function mediaRecorderOnDataAvailable (e) {
  chunks.push(e.data)
}
接著把這個 chunks 陣列變成 Blob 物件,Blob 物件可以簡單想成是一種相當於檔案的物件,透過 Blob() 建構式 代入資料和編碼的方式,即可建立 Blob 物件,這裡我們把它編碼成 video/webm ,這個檔案格式可以透過 chrome 瀏覽器打開。接著透過前面提過的 URL.createObjectURL(blob) 的方式,再把影音連結丟給 video 元素:
// 將由 dataavailable 取得的資料代入 Bl產生() 建構式中,產生 Blob 物件
var blob = new Blob(chunks, { 'type': 'video/webm; codecs=vp9' })

// 把 blob 物件透過 URL.createObjectURL() 代入 src 內
outputVideoURL = URL.createObjectURL(blob)
outputVideo.src = outputVideoURL
這時候,錄製好的影像就可以呈現於右邊 outputVideo 這個元素上。
這裡可以把 function 整理成:
let chunks = []

navigator.mediaDevices.getUserMedia(constraints)
  .then(function (stream) {

    mediaRecorder = new MediaRecorder(stream)
    mediaRecorder.addEventListener('dataavailable', mediaRecorderOnDataAvailable)   // 資料傳入時觸發
    mediaRecorder.addEventListener('stop', mediaRecorderOnStop)                     // 停止錄影時觸發

    function mediaRecorderOnDataAvailable(e) {
      chunks.push(e.data)
    }

    function mediaRecorderOnStop(e) {
      var blob = new Blob(chunks, { 'type': 'video/webm; codecs=vp9' })
      chunks = []       // 清空 chunks
      // 將錄製好的影片接到 <video> 上
      outputVideoURL = URL.createObjectURL(blob)
      outputVideo.src = outputVideoURL
    }
  })
  .catch(err => {...})

5. 關閉視訊鏡頭 - MediaStream API

這時候錄製和播放雖然不會有什麼問題,但會發現即使錄影結束了,筆電上方的攝影鏡頭卻還亮著沒有關閉,左邊的 inputVideo 元素也繼續播放透過攝像鏡頭取得的影像:
Imgur
因此,(若有需要)可以在結束錄影的時候把攝影鏡頭關閉。透過 MediaStram.getTracks() 可以取得當前正在串流的影音裝置,每個影音裝置都是一個軌(track),在取得影音裝置後可以透過 track.stop() 把該裝置關閉:
 navigator.mediaDevices.getUserMedia(constraints)
   .then(function (stream) {

     // 取得所有串流的裝置,並全部關閉
     stream.getTracks().forEach(function (track) {
       track.stop()
     })

   })
  .catch(...)

6. 重新啟動視訊鏡頭與釋放記憶體

最後,既然在上一步的時候關閉了視訊鏡頭,如果要再次錄製就需要重新啟動它,因此我們會把整個啟動攝影機到錄製的步驟(步驟 1 ~ 步驟 5)包在一個叫做 mediaRecorderSetup 的函式中,在重新啟動視訊鏡頭的時候再去呼叫這個 function 來啟動視訊鏡頭。
除此之外,由於透過 URL.createObjectURL() 的內容會佔用在瀏覽器的記憶體中,雖然瀏覽器有自動清除的機制,但 MDN 還是建議手動清除它,所以在重新啟動視訊鏡頭的過程中,我們順便把記憶體釋放掉:
function onReset (e) {
  // 釋放記憶體
  URL.revokeObjectURL(inputVideoURL)
  URL.revokeObjectURL(outputVideoURL)
  outputVideo.src = ''
  outputVideo.controls = false
  inputVideo.src = ''

  // 重新啟動攝影機
  mediaRecorderSetup()
}

其他函式

在這段程式碼中有一些和錄製視訊比較沒這麼相關的函式簡單說明一下。

自動下載

在預設的情況下,當錄製好的視訊影片載入完畢後,影片控制欄就會出現下載的符號可供下載:
Imgur
但如果希望視訊影片結束錄製時可以自動跳出下載視窗,可以透過建立一個連結並自動點擊來達到自動下載的方法:
function saveData (dataURL) {
  var fileName = 'my-download-' + Date.now() + '.webm'
  var a = document.createElement('a')
  document.body.appendChild(a)
  a.style = 'display: none'
  a.href = dataURL
  a.download = fileName
  a.click()
}

顯示錯誤訊息

function errorMsg (msg, error) {
  console.log('errorElement', errorElement)
  errorElement.classList.add('alert', 'alert-warning')
  errorElement.innerHTML += msg
  if (typeof error !== 'undefined') {
    console.error(error)
  }
}
這個 function 主要用來顯示錯誤訊息,例如當使用者未提供視訊裝置的權限時,會顯示:
Imgur

切換按鈕

這個 function 只是用來切換要顯示的按鈕:
function isRecordingBtn (recordBtnState) {
  startBtn.style.display = 'none'
  stopBtn.style.display = 'none'
  resetBtn.style.display = 'none'
  isRecordingIcon.style.display = 'none'
  switch (recordBtnState) {
    case 'start':
      startBtn.style.display = 'block'         // show startBtn
      break;
    case 'stop':
      stopBtn.style.display = 'block'          // show stopBtn
      isRecordingIcon.style.display = 'block'
      break;
    case 'reset':
      resetBtn.style.display = 'block'         // show resetBtn
      break;
    default:
      console.warn('isRecordingBtn error')
  }
}

統整 JavaScript 程式碼

最後,就可以把整個程式碼統整起來了,完整的程式碼放置於 Github

參考

Share:

2017年12月8日 星期五

5 分鐘快速了解 FontAwesome 5

keywords: icon, logo, fa
Imgur
FontAwesome 正式釋出第五版,在 FontAwesome 5 中,除了主色系從綠色變成藍色之外,究竟第 5 版中還多了哪些新功能呢?讓我們用 5 分鐘快速了解 FontAwesome 5 帶來什麼新功能吧!
Share:

2017年12月7日 星期四

[JS] 談談 JavaScript 中的錯誤處理 Error Handling

圖片來源:Proper Error Handling in JavaScript @ Scotch
keywords: exception handling, javascript, 錯誤處理, 例外處理
本文主要內容翻譯自 Exceptional Exception Handling in JavaScript @ SitePoint
在撰寫程式的過程中發生錯誤(error)或出現例外情況(exception)是經常出現的情況,一般我們會把「錯誤」稱作「例外」,兩者可以交替著使用。
當 JavaScript 程式執行的過程中發生錯誤時,它會丟出例外狀況(throw an exception),JS 並不會繼續往下走,而是尋找有沒有任何程式碼能夠處理這些錯誤(exception handling code),如果沒有找到任何可以處理錯誤狀況的程式碼,則它會從丟出例外狀況的函式中跳出(return),就這樣重複尋找錯誤處理的程式碼、跳出,直到觸及到最外層的函式(top level function)後終止。

錯誤類型

當例外產生時,會產生一個用來代表錯誤的物件。在 JS 中內建了 7 種錯誤類型的物件:

1. Error

Error 這個類型用來代表一般的錯誤情況,最常用在客製化例外情況。透過下面的指令可以產生一個錯誤物件的實例:
var error = new Error("error message");
console.log(error)          // Error: error message
這個 Error 物件包含兩個屬性-namemessage
  • name: 用來說明錯誤的類型(這裡就是 Error
  • message: 提供更多關於此例外情況的描述。

2. RangeError

RangeError 的例外情況會發生在當數值落在特定的區間外時,例如,透過 toFixed() 方法時,它可以接受介於 0 ~ 20 的參數來說明要顯示到小數後第幾位,當這個參數超過這個區間時,就會拋出 RangeError
var pi = 3.14159;
pi.toFixed(100000);  // RangeError: toFixed() digits argument must be between 0 and 100

3. ReferenceError(找不到變數:拼錯字)

當試圖存取一個不存在的變數時,會拋出 ReferenceError,這個錯誤經常發生在拼錯字的情況。
console.log(bar);    // ReferenceError: bar is not defined

4. SyntaxError(語法錯誤)

當有程式碼違反 JavaScript 的語法規則時,會拋出 SyntaxError 的錯誤。熟悉 C 或 Java 的通常是在編譯的過程中(compulation process)遇到語法錯誤;但 JavaScript 是直譯式語言(interpreted language),因此當程式碼被執行到時,才會辨認到語法錯誤。這種錯誤類型是所有例外狀況中唯一不能被修復的
if (false) {    // SyntaxError: Unexpected token (3:0)
  // 缺少結束的大括號

5. TypeError(找不到函式)

如果某一變項的型別和所期待的操作不同時,會拋出 TypeError這經常發生在去呼叫執行一個不存在的函式時
/**
 * 由於 foo 當中並不包含 bar 這個函式,因此會拋出 TypeError 錯誤
 **/
var foo = {};
foo.bar();     // TypeError: foo.bar is not a function

6. URIError

當使用 encodeURI()decodeURI() 的方法,但確有給了不合法的 URI 時會拋出這個錯誤:
/**
 * "%" 表示的是 URI 中的跳脫片段,但在下面的例子中 "%" 後沒有接任和字串,因此是不合法的跳脫片段
 **/
decodeURIComponent("%");   // URIError: URI malformed

7. EvalError

eval() 這個函式不恰當的使用時,會拋出 EvalError 這個錯誤。這個錯誤不再被當前的 ECMAScript 規範所採用。

處理錯誤(Handling Exceptions)

在 JavaScript 中如果碰到錯誤或例外情況時,如果沒有找到錯誤處理的程式,它會直接在那裡炸掉。那麼要如何避面錯誤產生時讓我們的程式直接炸掉呢?在 JavaScript 中,可以使用 try...catch...finally 語句。
try {
  // attempt to execute this code
} catch (exception) {
  // this code handles exceptions
} finally {
  // this code always gets executed
}

try

我們預期在 try 區塊內的程式碼會成功執行,但當有 try 區塊中有任何錯誤發生時,會立即進入 catch 的區塊;如果沒有錯誤發生,則會跳過 catch 區塊。finally 則是會在 try...catch... 之後先被執行。

catch

catch 區塊中可以指定一個參數,這個參數通常稱作 exceptioncatchID。在 catch 區塊中可以辨認這個參數,但離開這個區塊後就無法取得這個變數。透過 catch 可以阻止例外情況繼續向外冒泡,讓整個程式可以繼續執行。
try {
  foo++;              // ReferenceError
} catch (exception) {
  //  ReferenceError: foo is not defined
  console.log(`${exception.name}: ${exception.message}`)
}

finally

finally 區塊中的程式碼會在 try, catch 後被執行,不論有沒有例外情況產生,因此 finally 區塊通常用來包含清除的程式碼(例如,closing files)。
但是如果 finally 區塊中有 return 值的話,這個值會是最後整個 try-catch-finally 回傳的結果,即時在 trycatch 中有 return 或其他的 throw 都會被忽略。
function f() {
  try {
    console.log(0);
    throw 'bogus';    // 進入 catch
  } catch(e) {
    console.log(1);
    return true;      // 這個回傳值會被終止,直到整個 finally block 完成之後
    console.log(2);   // 執行不到
  } finally {
    console.log(3);
    return false;     // finally 中的 return 會覆蓋掉 try/catch 中的 return 或 throw
    console.log(4);   // 執行不到
  }
  // 執行 finally 中的 "return false"
  console.log(5);     // 執行不到
}
f();          //  0, 1, 3; returns false

客製化錯誤(Throwing Exceptions)

JavaScript 允許開發者透過 throw 來拋出客製化的例外情形:
throw expression;
透過 throw 可以丟出幾乎任何型別的錯誤,但一般還是建議使用內建的例外類型(瀏覽器會提供比較多資訊)。

客製化錯誤訊息(message)

例如,當我們透過除法卻不小心除了分母為 0 的數值時,可以使用 throw 來拋出例外情況:
let denominator = 0

// RangeError: Attempted division by zero!
try {
  if (denominator === 0) {
    throw new RangeError("Attempted division by zero!");
  }
} catch (e) {
  console.log(e.name + ': ' + e.message)
}

客製化錯誤名稱(name)

透過上面的方式我們可以根據原生的那 7 種錯誤類型(e.name)給予想要的錯誤訊息(e.message),但如果我們想要客製化錯誤名稱的話可以這麼做:
/**
 * 客製化錯誤類型
 **/
function DivisionByZeroError(message) {
  this.name = 'DivisionByZeroError';
  this.message = message;
}

// 繼承 Error 物件
DivisionByZeroError.prototype = new Error();
DivisionByZeroError.prototype.constructor = DivisionByZeroError;

// 建立 showError 的方法
DivisionByZeroError.prototype.showError = function() {
  return this.name + ': "' + this.message + '"';
}
使用客製化錯誤類型:
let denominator = 0

try {
  if (denominator === 0) {
    throw new DivisionByZeroError("Attempted division by zero!");
  }
}
catch (e) {
  console.log(e.showError())  // DivisionByZeroError: "Attempted division by zero!"
}

其他範例

透過 instanceof 處理多種不同的錯誤類型

利用 instanceof 可以根據不同的錯誤類型執行不同的 catch
try {
    // assume an exception occurs
} catch (exception) {
  if (exception instanceof TypeError) {
      // Handle TypeError exceptions
  } else if (exception instanceof ReferenceError) {
      // Handle ReferenceError exceptions
  } else {
      // Handle all other types of exceptions
  }
}

參考

Share:

2017年11月20日 星期一

[生產力] 目標設定後卻總是沒動機去做?透過這個 APP 提升自己的動力吧!

圖片來源:Projecturf
keywords: 目標設定, 任務, 時間管理, 獎勵, 酬賞, 行為改變, 教養, 快樂生活
記得小時候只要做了一些大人所認為或所定義「好的行為」之後,就可以得到一張好寶寶貼紙或印章,集滿了一定的張數之後就可以換取自己想要的禮物。
這樣的做法看起來好像有點幼稚,但實際上這招在行為改變、、習慣養成、甚至是成癮戒治上非常有效。看看各家便利商店的集點制度、銀行信用卡提出的紅利回饋等等,其實都是利用的類似的概念-「完成一個預期的行為、做完之後就給你獎賞」,只是長大之後我們想要的東西不同罷了。
過去曾經分享過一篇萬事起頭難,也許你需要的是習慣培養小利器,事隔多年之後,現在已經是手機發達的時代了,最近一直想要找一套可以用來設定工作任務,然後完成該任務之後可以透過類似集點的方式換取禮物的 APP,市面上雖然乍看有許多類似的 App,但是大部分要嘛比較偏向 Todo List、待辦清單這種(例如,TodoistTickTickWunderlistAny.do);要嘛則比較偏向習慣培養,每天記錄自己是不是有完成某件事,連續持續好幾天達標(例如,Loop 習以為常Habit TrackerHabitHubThirty;要嘛則缺少了我想要的獎勵酬賞制度。
主要的需求是希望能夠「設定任務」、「設定獎賞」、「任務完成後可以換取代幣」、「代幣可以兌換設定的獎賞」,另外希望功能不用太過複雜,簡單方便操作就好。
後來搜尋了一下找到幾款看起來比較合適的 APP ,先說結論:實際操作之後我比較喜歡使用 Incentive 、和 Agoal - 私人任務助理 這兩套,因為它們的操作簡單、設定不複雜;其他可以完成同樣需求的像是 LifeRPGHabiticaDo It Now 也都是不錯的選擇,但是設定上稍微比較複雜一些(或許比較有趣?),因為還帶入了遊戲化像是升級或經驗值的概念,喜歡這類的朋友們也可以參考看看。
下面則列出這些 App 簡單的說明和介紹。

Incentive

先來分享這套我個人最喜歡的 App ,它的設定非常的簡單,進入之後透過右下角的「+」就可以新增目標(Goal)、任務(Tasks)和獎勵(Rewards)。
Imgur

目標(Goals)

基本上目標比較像是一個大方向,裡面可以設定許多任務來完成這個目標,例如我希望可以成為網頁工程師,目標完成時可以設定會獲得多少的代幣(Points)。
Imgur

任務(Tasks)

接下來就可以設定各種想要完成的任務來達到自己的目標,其中可以選擇相對應的目標、喜歡的 ICON、截止日期和完成後可以獲得多少代幣。
Imgur
另外,非常好用的地方是在 Task Type 的地方它可以設定任務類型是屬於 每天/週/月 的或 重複性的(Repeatable) 任務,所以像是如果每天要運動、睡眠等等就可以設成 Daily,如果是可以重複很多次的像是喝水這種,就可以設 Repeatable,另外一樣可以設定完成時可以獲得多少代幣:
Imgur

酬賞(Rewards)

最後是設定獎勵的部分,獎勵的部分很棒的是也分成單次的獎勵和可重複的獎勵,所以像是買個喜歡的禮物給自己、出國遊玩這種就可以算是單次的;如果是吃大餐或小點心這種就可以算是重複的,接著就是設定需要設定多少代幣才可以兌換:
Imgur

實際操作

設定好之後畫面的右上角會有你目前有的代幣數,當完成一個任務後,就可以點選右邊的「勾勾」,右上角的代幣數目就會增加了。如果是單次的任務完成後就會進入封存(Archived)、如果是重複性的任務則會可以重複點擊(一直點錢就會一直增加XDD)。
Imgur
接著存到一定的代幣後,就可以到獎勵區(Rewards)去兌換自己想要的獎品,如果錢夠的話點了「勾勾」右上角的金額就會扣除;如果錢不夠的話當然就沒辦法兌換拉。同樣的,對於單次的獎勵換完了就會進到封存,重複性的獎勵則可以重複兌換:
Imgur
另外也提供簡單的統計資訊,也可以在封存中(Archives)看到已經完成的任務:
Imgur

使用經驗

在設定的時候有一些經驗可以簡單分享一下,就是不要用什麼代幣點數了,直接用現金吧XD,簡單來說,如果我的獎品是 iPhoneX,就先設定對應的金額要是 $35,900 ,然後直接設定完成某項任務可以獲得多少錢,這樣做起來實在很有動力啊!例如我每天只要完成 「12 點前就寢」,就可以馬上獲得 $10,每喝一次水就可以馬上獲得 $1 ,做起來真是非常有感覺啊!
Incentive @ Google Play

Agoal - 私人任務助理

Imgur
這是一款非常簡單易用的 App,任務部分可以設定完成時限、完成可以獲得多少獎勵、沒有完成的處罰;獎勵部分可以設定獎勵類型、需要多少點可以購買該獎勵、購買之後不一定要馬上兌換。唯一美中不足的是如果你有一項任務是每天都要做的習慣(例如,運動或早睡早起)那麼你就必須每天重複設定
Imgur

其他相關 App

其他類似的 App 像是 LifeRPGHabiticaDo It Now 則融入了更多遊戲化(Gamification)的特性,多了像是技能、經驗值、任務向度等等的設定,但對於我的需求來說就顯得不夠簡潔,提供給其他朋友做個參考:

Do it Now

畫面和設計風格蠻好看的:
Imgur
Do It Now @ Google Play

Life RPG

點數是寶石,可以設定任務的困難度、急迫度、和恐懼度:
Imgur
Life RPG @ Google Play

Habitica

畫面設計的很像 RPG 風格的圖案,內建了一些和遊戲有關的獎品:
Imgur
Habitica @ Google Play
Share: