# HTML5
## Geolocation
> - 定位
> - PC
> - 一般電腦不具有定位設置, 所以是通過 IP 地址, 導致定位精度非常差
> - 通過 IP 地址來判斷位置時, 就需要一個 IP庫(判斷哪個IP在哪裏)
> - 網路上就有許多 IP 庫可以買
> - CHROME 並不會去買那些庫, 他直接建立在 google.com 的 server,
> 這會導致某些長城國家使用 chrome 會無法拿到數據
> 那些國家使用 IE 可能就沒問題, 因為沒擋微軟
> - 解決這問題就是拿到 IP 後去後端找 IP 庫判斷
> - 移動
> - 使用 GPS 定位, 定位精度高
> #### 查看 geolocation 對象
> - geolocation 在 navigator 裡
```javascript=
console.log(navigator.geolocation);
/*
Geolocation {}
__proto__: Geolocation
getCurrentPosition: ƒ getCurrentPosition()
watchPosition: ƒ watchPosition()
clearWatch: ƒ clearWatch()
constructor: ƒ Geolocation()
Symbol(Symbol.toStringTag): "Geolocation"
__proto__: Object
*/
```
> - 三個方法
> - `getCurrentPosition` : 獲取一次位置
> - `watchPosition` 會不停獲取位置
> - 說白了這就是一個定時器, `clearWatch` 就是把定時器關掉的東西
### gerCurrentPosition()
`getCurrentPosition(成功cb(res), 失敗cb(err), 參數)`
> - 這是以域名為單位, 而非頁面為單位
> 換言之就是問過一次同意或拒絕後, 就不會再問了
> - 既然是以域名為單位, 當然就有跨域的問題
> 亦即網頁必須要有域名, 因為定位是跟著域名走的, 也就是文件域不能用
```shell
$ tree
.
├── http.js # 寫一個非常簡單的服務器, 因為我要有域名
└── www
├── 01.js # 就 01.html 的 js
└── 01_geolocation.html # 只有一個標籤, 就是連那個 js
```
```javascript=
// 開一個服務器
const http = require(`http`);
const fs = require(`fs`);
http.createServer((req, res)=> {
console.log(req.url);
fs.readFile(`www${req.url}`, (err, data)=>{
if (err) {
console.log(err);
res.writeHeader(404);
res.write('Not Found');
} else {
console.log(1);
res.write(data);
}
res.end();
})
}).listen(5566);
```
```htmlmixed=
<!-- 網頁 -->
<head>
<meta charset='utf-8'>
<script src='01.js'></script> <!-- 就幹這事而已-->
</head>
```
```javascript=
// 主角出場
navigator.geolocation.getCurrentPosition(res=>{
console.log(res);
}, err=>{
console.log(err);
})
```
> - 此時開啟服務器, 開啟該網頁後會跳出
> <img src='https://i.imgur.com/U4PWwTU.png' style='width: 250px'/>
> - 點擊 Block 後返回 err 對象, 且右上角會出現<img src='https://i.imgur.com/8hTZlOm.png' style='width: 20px'/>
> ```javascript
> GeolocationPositionError {...}
> code: 1
> message: "User denied Geolocation"
> __proto__: GeolocationPositionError
> ```
> - 拒絕太多次 Chrome 會生氣
> ```
> Geolocation permission has been blocked as the user has dismissed the permission prompt several times. This can be reset in Page Info which can be accessed by clicking the lock icon next to the URL. See https://www.chromestatus.com/features/6443143280984064 for more information.
> ```
> - 點擊 Allow 後返回 res 對象,
> 且右上角會出現<img src='https://i.imgur.com/7LLROCA.png' style='width: 30px'/>
> 點擊可以清掉紀錄, 之後就會再問你要不要記
> <img src='https://i.imgur.com/Ju0BRNB.png' style='width: 250px'/>
> ```javaseript
> GeolocationPosition {...}
> coords: GeolocationCoordinates {...}
> timestamp: 1579234496135
> __proto__: GeolocationPosition
> ```
> - `coords` : 座標
> - `longitude` : 經度
> - `latitude` : 緯度
> - 實際上 PC 有用的只有這兩個
> - `altitude` : 海拔高度, PC 通常為 null, PC 通常沒有辦法測量
> - `accuracy` : 精確度, 不過這在PC是假的, 因為他是透過IP來定位的
> - `altitudeAccuracy` : 海拔高度的精確度
> - `heading` : 朝向哪邊(0~359)
> - `speed` : 測量速度的
### 應用: 嵌入經緯度到 Map API
> - [Google](https://cloud.google.com/maps-platform/) 現在地圖API要錢
```htmlmixed=
<head>
<title>Simple Map</title>
<meta name="viewport" content="initial-scale=1.0">
<meta charset="utf-8">
<style>
/* Always set the map height explicitly to define the size of the div
* element that contains the map. */
#map {
height: 100%;
}
/* Optional: Makes the sample page fill the window. */
html, body {
height: 100%;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
var map;
function initMap() {
// 拿到經緯度
navigator.geolocation.getCurrentPosition(res=>{
console.log(1);
console.log(res);
console.log(res.coords.latitude, res.coords.longitude);
map = new google.maps.Map(document.getElementById('map'), {
center: {lat: res.coords.latitude, lng: res.coords.longitude}, // 寬高經緯度
zoom: 17 // 縮放倍數
});
}, err=>{
console.log(err)
})
}
</script>
<!-- API_KEY 要註冊 -->
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap" async defer></script>
</body>
```
> - [Apple](https://developer.apple.com/maps/web/) 我一直失敗, 改天再來研究
> - [百度](http://lbsyun.baidu.com/index.php?title=首页)的我有成功
> - [地圖生成器](http://api.map.baidu.com/lbsapi/createmap/index.html): 百度地圖開放平台 => 開發文檔 => 地圖生成器 => 獲取代碼
> - 百度的 2.0 需要密鑰, 1.2 的不用, 所以使用的時候最好改成 1.2
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
.box {
height: 600px;
width: 600px;
border: 1px solid red;
}
</style>
<!-- 2.0 要密鑰, 1.2 不用, 所以改成 1.2
<script type="text/javascript" src="http://api.map.baidu.com/api?v=2.0&ak=您的密匙"></script>
-->
<script type="text/javascript" src="http://api.map.baidu.com/api?v=1.2"></script>
<script>
navigator.geolocation.getCurrentPosition(res=>{
console.log(res.coords.latitude, res.coords.longitude);
// 把他提供的API拿到這,
// 當使用者同意給位置時, 就可以調用來畫地圖
//创建和初始化地图函数:
function initMap(){
createMap();//创建地图
setMapEvent();//设置地图事件
addMapControl();//向地图添加控件
addMapOverlay();//向地图添加覆盖物
}
function createMap(){
map = new BMap.Map("map"); // 地圖 ID
map.centerAndZoom(new BMap.Point(res.coords.longitude, res.coords.latitude),17); // 經緯度跟縮放
}
function setMapEvent(){
map.enableScrollWheelZoom();
map.enableKeyboard();
map.enableDragging();
map.enableDoubleClickZoom()
}
function addClickHandler(target,window){
target.addEventListener("click",function(){
target.openInfoWindow(window);
});
}
function addMapOverlay(){
}
//向地图添加控件
function addMapControl(){
var scaleControl = new BMap.ScaleControl({anchor:BMAP_ANCHOR_BOTTOM_LEFT});
scaleControl.setUnit(BMAP_UNIT_IMPERIAL);
map.addControl(scaleControl);
var navControl = new BMap.NavigationControl({anchor:BMAP_ANCHOR_TOP_LEFT,type:BMAP_NAVIGATION_CONTROL_LARGE});
map.addControl(navControl);
var overviewControl = new BMap.OverviewMapControl({anchor:BMAP_ANCHOR_BOTTOM_RIGHT,isOpen:true});
map.addControl(overviewControl);
}
var map;
initMap();
}, err=>{
console.log(err);
})
</script>
</head>
<body>
<div class='box' id='map'></div> <!-- 記得給 id -->
</body>
```
## Video, Audio
## localStorage
> - 用來替代 cookie 的
> - `cookie` :
> - 小 (4K)
> - 瀏覽器與服務器共享(雙方都可控)
> - `localStorage`
> - 大 (5M) / 域名
> - 每個域名 5M, 不是每個頁面 5M
> - 瀏覽器獨享(只存在於客戶端, 服務器無法控制)
> 亦即瀏覽器不會把這個數據發送給服務器
> - 優點: 不用每次請求都要發一次, 每次 5M 也是會受不了的
> - 缺點: 服務器讀不到 localStorage
> - 類似一個大 JSON
> - 永久存儲(不刪就不會不間)
> - `sessionStorage`
> - 5M / 域名
> - 會話存儲(瀏覽器關就沒了)
> - localStorage 跟緩存沒關係,
> - 緩存是瀏覽器自發行為, 控制不了
> - localStorage 是可控的
> - 只支持 String, 非 string 都會被轉成 string,
> 也些會轉的很奇怪, 所以最好自己轉完後再存 `JSON.stringify`
> - 常見用途:
> - 記錄用戶名
> - 紀錄用戶書寫草稿
> #### 寫入數據
> - `localStorage.鍵 = 值`
> - `localStorage.setItem('鍵', 值)`
```html
<head>
<script>
localStorage.a = 100;
</script>
</head>
```
> - 開網頁之後就寫進去瀏覽器了
> 
> #### 拿到數據
> - `localStorage.鍵`
> - `localStorage.getItem('鍵')`
```htmlmixed=
<head>
<script>
// localStorage.a = 100;
console.log(localStorage.a);
</script>
</head>
<!-- 結果
100
-->
```
> #### 遍歷
```javascript=
for (let k in localStorage) {
console.log(`${k}=${localStorage[k]}`);
}
/* 結果: 連原型都遍歷出來了
a=100
length=1
key=function key() { [native code] }
getItem=function getItem() { [native code] }
setItem=function setItem() { [native code] }
removeItem=function removeItem() { [native code] }
clear=function clear() { [native code] }
*/
```
> #### localStorage.key(位置)
> - 返回該位置的 key
```javascript=
// 只遍歷他的值
for (let i = 0; i<localStorage.length; i++) {
let k = localStorage.key(i);
console.log(`${k}: ${localStorage[k]}`);
}
```
> #### 刪除數據
> - `delete localStorage.鍵`
```javascript=
console.log(localStorage.a); // 100
delete localStorage.a;
console.log(localStorage.a); // undefined
```
> 
### 應用: 草稿保存
```htmlmixed=
<head>
<meta charset='utf-8'>
<script>
window.onload = function () {
let t = document.querySelector('#text');
let b = document.querySelector('#btn');
// 每次載入畫面, 都把草稿拿出來用
t.value = localStorage.tmp || '';
// 文本框寫入時, 更新草稿
t.oninput = function () {
localStorage.tmp = t.value;
}
// 送出後刪除草稿
b.onclick = function () {
t.value = '';
delete localStorage.tmp;
}
}
</script>
</head>
<body>
<textarea cols='20' rows='20' id='text'></textarea>
<input type='button' value='send' id='btn'/>
</body>
```
## webworker
> - 幫助前端實現多進程的工作
> - JS 作者為了保持 JS 簡單使用, 沒有考慮多線程
> - 前端的主進程稱為 `UI進程`, 也就是 JS
> 前端的子進程稱為 `工作進程`
> - webWorker 為 工作進程, 所以無法參與UI渲染
> - 子進程不能創造子進程
> - webWorker 基於安全性, 必須有域名(跨域)
> - 多進程可以更充分的利用 CPU 的資源
> 但是前端很少進行計算型任務, 較多為圖形型跟內存型任務
> 所以基本上很少用
### 創建一個 webWorker 對象
`new Worker(<js檔路徑>)`
> - webWorker 必須要有一個 js 檔路徑當參數 (以做數據交互)
> - 必須跨域
```shell
% tree
.
├── server.js
└── www
├── 1.js
└── webWorker.html
```
```htmlmixed=
<head>
<meta charset='utf-8'>
<script>
let wk = new Worker(`1.js`);
console.log(wk);
</script>
</head>
```
```javascript=
// 為了跨域寫的
let http = require(`http`);
let fs = require(`fs`);
http.createServer((req,res)=>{
let rs = fs.createReadStream(`www${req.url}`);
rs.pipe(res);
rs.on(`error`, err=>{
res.writeHead(404);
res.write(`NOT FOUND`);
res.end();
})
}).listen(5566);
```
> - 看對象裡面有啥
> 
```shell
Worker {onmessage: null, onerror: null}
onmessage: null # 接數據事件
onerror: null
__proto__: Worker
onmessage: (...)
onerror: (...)
terminate: ƒ terminate()
postMessage: ƒ postMessage() # 傳數據, 參數只能有一個值, 兩個以上用陣列或JSON
...
```
### 簡單運算
```htmlmixed=
<head>
<meta charset='utf-8'>
<script>
// 創造工作進程對象
let wk = new Worker(`1.js`);
// 收數據事件
wk.onmessage = res=>{
console.log(res.data);
}
// 發數據給 1.js
wk.postMessage({a:1, b:2});
wk.postMessage({method: 'GET'});
wk.postMessage({method: 'POST'});
</script>
</head>
```
```javascript=
// 1.js
this.onmessage = function (res) {
if (res.data.method == 'POST') {
console.log('POST');
} else if (res.data.method == 'GET') {
console.log(`GET`);
} else {
let {a,b} = res.data;
let result = a + b;
this.postMessage(result);
}
}
```
### 注意事項
> - 多線程: 共享同個存儲空間, 進程間傳引用
> - 多進程: 各自獨享存儲空間, 主進程複製一份數據給子進程
> - 所以多進程性能並不高,
> 多進程通常是考量安全性而非性能,
> 多線程才是考量性能的選項
> - 有些對象是禁止被 clone 的, 例如 document (一個頁面只能有一個 DOM)
```htmlmixed=
<head>
<meta charset='utf-8'>
<script>
let wk = new Worker(`1.js`);
wk.postMessage(document);
// webWorker.html:7 Uncaught DOMException: Failed to execute
// 'postMessage' on 'Worker': HTMLDocument object could not be cloned.
</script>
</head>
```
## WebSQL & IndexDB
> - 讓數據庫建置到前端
> - 但是因為有安全疑慮, W3C 把這東西[砍了](https://www.w3.org/TR/webdatabase/)
## 文件操作與拖曳
## manifest 文件
> - 讓前端可以控制緩存
> - 使用者少的原因
> - 緩存是為了優化頁面, 瀏覽器管理緩存已經管理的不錯了, 自己管理不一定管理的比較好
> - 如果頁面真的很大到瀏覽器難以管理而需要自己用時, 通常這種級別的可能較適合做成 APP
>
# CSS3
> - IE10+
> - 圓角: border-radius
> - 陰影: text-shadow & box-shadow
> - 漸變: linear & radial
> - rgba
> - transform
> - 旋轉 rotate
> - 縮放 scale
> - 平移 translate
> - 傾斜 skew
> - 動畫: transition & animation
## border-radius
> `border-radius: 左上x[/左上y 右上x/右上y 右下x/右下y 左下x/左下y]`
> - <img src='https://i.imgur.com/6vY4p1D.png' style='width:200px'/>
```css
div {
height: 200px;
width: 200px;
border: 1px solid black;
border-radius: 20px/50px;
}
```
## text-shadow & box-shadow
> `text-shadow: x偏移 y偏移 陰影大小 陰影顏色`
> `box-shadow: 內陰影 x偏移 y偏移 陰影大小 擴展範圍 陰影顏色`
> - 內陰影: 陰影在裡面
> - 擴展範圍: 在原有物體大小之上, 周圍向外擴展, 擴展後才加陰影
> - <img src='https://i.imgur.com/dcx3DaW.png' style='width:200px'/>
```css
div {
height: 200px;
width: 200px;
margin: 200px auto;
border: 1px solid black;
text-shadow: 10px 10px 2px red;
box-shadow: 10px 10px 2px 20px blue;
}
div:active { // 點擊效果
box-shadow: inset 10px 10px blue;
}
```
## linear & radial
> - 漸變事實上是一種圖片, 所以border沒法用
```txt
# 線性漸變
linear-gradient([ [ [ <angle> | to [top | bottom] || [left | right] ],]? <color-stop>[, <color-stop>]+);
- -webkit-linear-gradient 方向不用 to
- <color-stop> 包含顏色跟開始漸變起始漸變(?
# 圓形漸變(radial-gradient): 暫略, 總之就是一個圓散射漸變, 用到再去MDN看
```
```css
div {
height: 200px;
width: 200px;
margin: 200px auto;
border: 1px solid black;
/* 前30%的紅色為100%, 之後才開始漸變 */
/*background: linear-gradient(to right,red 30%, green, yellow)*/
/*background: -webkit-linear-gradient(left, red, green, yellow);*/
background: -webkit-radial-gradient(right bottom, red, green 20%)
}
```
## transform
> - 使用 transform, 一定要加上初始值, 即使沒有改變也ㄧ樣
```css
div {
height: 200px;
width: 200px;
margin: 200px auto;
border: 1px solid black;
/* 即使是不旋轉, 也要加 0deg */
transform: rotate(0deg);
}
```
## CSS3 性能更高
> - CSS3 的樣式不會改變盒模型(物體佔據空間)
> - DOM 操作
> - 重(新)排(列):
> - 改變盒模型就需要重新計算盒子的大小方位等
> - 這動作非常耗時
> - 如果不改變盒模型, 當然就不用重排, 性能當然就高
> - 重繪
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
* {
padding: 0;
margin: 0;
list-style: none;
}
ul li {
float: left;
background: blue;
height: 200px;
width: 200px;
margin: 10px;
}
ul li:hover {
/* 改變盒模型大小, 當然也擠壓了旁邊盒子
height: 400px;
width: 400px;
*/
transform: scale(2); /* 不會擠壓旁邊盒子 */
}
</style>
</head>
<body>
<ul>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</body>
```
## 其他
### classList
> - 存元素的 class 列表
```txt
__proto__: DOMTokenList
length: (...)
value: (...)
item: ƒ item()
contains: ƒ contains()
add: ƒ add() 增
remove: ƒ remove() 刪
toggle: ƒ toggle()
replace: ƒ replace() 換
supports: ƒ supports()
toString: ƒ toString()
entries: ƒ entries()
forEach: ƒ forEach()
keys: ƒ keys()
values: ƒ values()
```
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
.box {
height: 100px;
width: 100px;
background: gray;
transition: 1s all ease;
}
.slideUp {
height: 0;
}
</style>
<script>
window.onload = function () {
let oBtn = document.querySelector(`#btn`);
let oBox = document.querySelector(`.box`);
oBtn.onclick = function () {
console.log(oBox.classList);
// 如果有這個類
if (Array.from(oBox.classList).includes(`slideUp`)) {
// 刪除類
oBox.classList.remove(`slideUp`);
} else {
// 新增類
oBox.classList.add(`slideUp`)
};
}
}
</script>
</head>
<body>
<input type='button' id='btn'/>
<div class='box'></div>
</body>
```
## canvas
> - 繪圖用的, 畫出來的是點陣圖(位圖), 不能縮放, HTML 的標準
> - vs. `svg` : 矢量圖, 無限縮放, 不是HTML的東西, 是一個獨立的標準
> - ps. `VML` : IE5+的矢量圖, 無限縮放
> - inline-block
> - 應用場景: 只要圖像相關的, 都可以考慮試試看, 最常見於
> - 圖表: [ECHARTS](https://www.echartsjs.com/examples/en/index.html)
> - 遊戲
> - 濾鏡
> - Canvas 不會保留圖形, 即使畫得再屌,對 Canvas 來說也都只是一個像素點
> - 不能修改, 亦即如果話了20x30矩形, 想改成 30x30, 只能刪掉重畫
> - 不能對`圖形`綁定事件, 事件是綁標籤的,只能綁 `<canvas>` 標籤, 不能對個別圖綁標籤
> - 由於他不保留圖形, 執行速度非常快, 因為不需要那些有的沒有的信息(寬高等)
### 渲染環境(rendering context)
`getContext(渲染方式)`
> - `<canvas>` 是一塊畫布
> - 必須使用先拿到渲染環境, 然後在上面畫畫, 然後才會顯示出來
> - 渲染環境有很多種, 例如 `2d` `WebGL`(3D)...
#### 路徑操作
`moveTo(x,y)` `lineTo(x,y)` `closePath(x,y)`
> - 說白了就是選取(區), 沒有真正的幹嘛
> - `moveTo` 移動到某處
> - `lineTo` 劃線選區
> - `closePath` : 閉合選區, 沒寫不會真的閉合, 即使座標看起來閉合
`beginPath()` :
> - 清除之前路徑
> - 最好在一切路徑之前都先清一次
> - 如果沒清路徑的話,
#### 基本操作(邊線, 填充)
> - 要先設定好樣式之後才畫上去( 調用 fn )
> #### 邊線
> `strokeStyle` : 邊線色
> `lineWidth` : 邊線寬
> - 向內向外各半
> - 假設20寬, 內外各畫 10
> `Stroke()` : 畫邊線
> #### 填充
> `fillStyle` : 填充色
> `fill()` : 填充
```htmlmixed=
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<style>
body {
text-align: center;
}
#c1 {
background: skyblue;
}
</style>
<script>
window.onload = function () {
let oC = document.querySelector(`#c1`);
console.dir(oC);
let gC = oC.getContext(`2d`);
console.dir(gC);
// 路徑操作
gC.moveTo(290, 315);
gC.lineTo(400, 234);
gC.lineTo(326, 146);
gC.lineTo(195, 235);
// gC.lineTo(290, 315); // 手動描點閉合不是真的閉合
gC.closePath();
// 邊線
// gC.lineWidth = 20; // 線寬
// gC.strokeStyle = 'red'; // 線色
// gC.stroke();
// 填充
gC.fillStyle = 'green'; // 填充顏色
gC.fill();
}
</script>
</head>
<body>
<canvas id='c1' height='400px' width='600px'></canvas>
</body>
</html>
```
```htmlmixed=
<style>
body {
background: black;
text-align: center;
}
canvas {
background: white;
}
</style>
<script>
window.onload = function () {
let oDiv = document.querySelector(`canvas`);
let gC = oDiv.getContext(`2d`);
gC.moveTo(100,100);
gC.lineTo(200,200);
gC.strokeStyle = 'blue';
gC.stroke();
gC.beginPath(); // 如果沒有清的話, (100,100)=>(200,200) 會畫兩次
gC.moveTo(400, 400);
gC.lineTo(500, 500);
gC.strokeStyle = 'gray';
gC.stroke();
}
</script>
```
### 矩形操作
> - 矩形操作只需要矩形的左上角座標跟寬高
> - `rect(x,y,width,height)` : 路徑操作, 還需要 `stroke()` 或 `fill()` 來畫東西
> - `strokeRect(x,y,width,height)` : 直接圈完畫線
> - `fillRect(x,y,width,height)` : 直接圈完填色
```htmlmixed=
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<style>
body {
background: black;
text-align: center;
}
canvas {
background: white;
}
</style>
<script>
window.onload = function () {
let oDiv = document.querySelector(`canvas`);
let gC = oDiv.getContext(`2d`);
gC.strokeStyle = 'blue';
gC.lineWidth = 20;
// gC.rect(100,100, 300,400);
// gC.stroke();
gC.strokeRect(100,100,300,400);
// gC.fillRect(100,100,300,400);
}
</script>
</head>
<body>
<canvas height='600px' width='600px'></canvas>
</body>
</html>
```
### 清除操作
> `clearRect(x,y,width,height)`
> - 就是橡皮擦
### canvas 動畫
> - 畫完擦掉換位再畫
> - Q. 頻繁的操作畫完擦掉會不會很耗能?
> - 假設我的圖為 500 px * 500 px = 250000 px
> - 去 INTEL 規格查一下, 即使是 i3 第四代最低的基礎頻率也有 1.3ghz (130,000,000)
> - 130,000,000 / 250,000 = 520, 而了不起遊戲一秒頂多也才 60 幀
> - 這還只是我看到最低基頻的一款,一般不超頻平均都 2ghz 超頻都可以到 4ghz 以上了
> - 所以只要圖不是太誇張的大,這都還行
> - Q. 如果卡怎辦?
> - JS 身為一個解釋性語言, 性能本來就不可能多強
> - 所以如果真的卡而想要解決,那就必須使用本地語言來寫
> - 本地語言
> - IOS: OC
> - Andriod: JAVA
> - 用本地語言還能直接調用本地的顯卡編碼器
#### 動畫幀([fps](https://zh.wikipedia.org/wiki/%E5%B8%A7%E7%8E%87))
> - 常見動畫幀為 24 || 30 || 60 fps
> - 如果用定時器給定時,例如 16 ms (1000/16 = 62.5) 約等於 60 幀,
> 可能有些電腦會無法負荷
> - `requestAnimation(function)`
> - 當系統有空時,他就會調用參數函數
> 也就是說他可以依據系統性能來決定幀數
> - 這東西好像會偵測你有沒有正打開這個頁面,開著頁面才會跑
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
body {
background: black;
text-align: center;
}
canvas {
background: white;
}
</style>
<script>
window.onload = function () {
let oDiv = document.querySelector(`canvas`);
let gC = oDiv.getContext(`2d`);
let left = 100;
// setInterval(function() {
// gC.clearRect(0,0,oDiv.width, oDiv.height);
// left+=5;
// gC.strokeRect(left, 100, 100, 100);
// }, 16) // 1000/16 = 62.5 => 常見動畫幀數(fps) 為 60 幀
// 系統決定跑幾幀(根據系統所能負荷的性能為依據)
requestAnimationFrame(next);
function next() {
gC.clearRect(0,0,oDiv.width, oDiv.height);
left +=5;
gC.strokeRect(left, 100, 100, 100);
requestAnimationFrame(next);
}
}
</script>
</head>
<body>
<canvas height='600px' width='600px'></canvas>
</body>
```
### Canvas 事件
> - Canvas 沒有事件,必須自己判斷
#### 滑鼠移到框框內,框框換色
> - 在畫布設定 `onmouse`
> - 判斷滑鼠的座標有沒有在框框裡
> 
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
body {
background: black;
text-align: center;
}
canvas {
background: white;
}
</style>
<script>
window.onload = function () {
let oCanvas = document.querySelector(`canvas`);
let oGC = oCanvas.getContext(`2d`);
oGC.strokeRect(100, 100, 200, 200);
// 1. 把框框在畫布裡的位置記起來
let left = 100, top = 100, right = left + 200, bottom = top + 200;
// 2. 監聽
oCanvas.onmousemove = function (ev) {
// console.log(ev.clientX, oCanvas.offsetLeft, ev.offsetX);
// console.log(ev);
// 3. 拿滑鼠在畫布裡的位置
let x = ev.clientX - oCanvas.offsetLeft,
y = ev.clientY - oCanvas.offsetTop;
// let x = ev.offsetX+1, y = ev.offsetY+1;
// => 滑鼠距離元素上左兩邊的距離, 約等於 ev.clientX - oCanvas.offsetLeft
// => 有支援問題
// => 不知為啥有 1px 差距
// 5. 每次觸發都清掉重劃
oGC.clearRect(0,0,oCanvas.width, oCanvas.height);
// 4. 判斷滑鼠位置有沒有在框框裡
if (left <= x && x <= right && top <= y && y <= bottom) {
oGC.strokeStyle = 'blue';
} else {
oGC.strokeStyle = 'black';
}
oGC.strokeRect(100, 100, 200, 200);
}
}
</script>
</head>
<body>
<canvas height='600px' width='600px'></canvas>
</body>
```
### 圓形操作
`arc(圓心的X, 圓心的Y, 半徑, startRadian, endRadian, 是否逆時針)`
> - `arc()` 只是一個路徑操作
> - 電腦的圖形操作都是弧度計算
> - 弧度1 到 弧度2
> - 零度在三點鐘方向
> - JS 裡所有度數的單位都是弧度
> - 6th 參數代表是否為逆時針,`Boolean`
#### 判斷滑鼠有沒有在圓中
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
body {
background: black;
text-align: center;
}
canvas {
background: white;
}
</style>
<script>
window.onload = function () {
let oCanvas = document.querySelector(`canvas`);
let oGC = oCanvas.getContext(`2d`);
// 角度轉成弧度
function toRadian(angle) {
return angle * Math.PI / 180 // 360:2PI => n:n * PI / 180
}
oGC.beginPath()
oGC.arc(300, 300, 100, toRadian(0), toRadian(360), true);
oGC.stroke();
let cx = 300, cy = 300, r = 100;
oCanvas.onmousemove = function (ev) {
// 1. 算滑鼠在畫布的位置
let x = ev.clientX - oCanvas.offsetLeft,
y = ev.clientY - oCanvas.offsetTop;
// ev.offsetX, ev.offsetY
// 2. 算滑鼠距離圓心的距離
let pos = Math.sqrt(Math.pow(x-cx,2) + Math.pow(y-cy,2));
// console.log(x,y,pos);
// 3. 判斷與畫畫
oGC.clearRect(0,0,oCanvas.width, oCanvas.height);
if (pos <= r) {
oGC.strokeStyle = 'blue';
} else {
oGC.strokeStyle = 'black';
}
oGC.stroke();
}
}
</script>
</head>
<body>
<canvas height='600px' width='600px'></canvas>
</body>
```
#### 製作圓餅圖
> ### 三角函數
> 
> ### 繪畫邏輯
> 
> - 首先我必須畫一條半徑的線, 最大的問題在於必須畫到的那個點座標為多少
> - 已知圓心座標`(cx, cy)`, 半徑`r`, 開始繪畫的角度,
> - 向 0 度線畫一條輔助線, 形成三角形, 寬高`x` `y`
> - `(?, ?)` 應該為 `(cx+x,cy+y)`
> - 根據三角函數公式
> - sin = 對邊 / 斜邊
> - sin = y / r
> - y = sin * r
> - cos = 鄰邊 / 斜邊
> - cos = x / r
> - x = cos * r
> - 所以 `(?, ?)` 應該為 `((cx + cos * r), (cy + sin * r))`
> - 接著畫上一個弧
> - 最後 closePath
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
body {
background: black;
text-align: center;
}
canvas {
background: white;
}
</style>
<script>
window.onload = function () {
let oCanvas = document.querySelector(`canvas`);
let oGC = oCanvas.getContext(`2d`);
function toRadian(angle) {
return angle * Math.PI / 180
}
function circle(startAngle, endAngle, color) {
let startRadian = toRadian(startAngle),
endRadian = toRadian(endAngle);
oGC.beginPath();
// 1. 圓心
oGC.moveTo(cx, cy);
// 2. 畫線
oGC.lineTo(cx+Math.cos(startRadian)*r,cy+Math.sin(startRadian)*r);
// 3. 畫弧
oGC.arc(300, 300, 100, startRadian, endRadian, false);
// 4. 關閉
oGC.closePath();
// 5. 畫上
oGC.fillStyle = color;
oGC.fill();
oGC.beginPath();
}
let cx = 300, cy = 300, r = 100
// - 處理資料
let datas = [
{data: 200, color: 'skyblue'},
{data: 100, color: 'pink'},
{data: 100, color: 'purple'}
]
let sum = 0;
datas.forEach(v=>{
sum += v.data;
})
let now = 30;
// 算比算角
datas.forEach(v=>{
let angle = v.data/sum*360;
circle(now, angle+now, v.color);
now += angle;
})
}
</script>
</head>
<body>
<canvas height='600px' width='600px'></canvas>
</body>
```
### 字體操作
> - `strokeText(字, x, y)` `fillText(字, x, y)`
> - 字體的 xy 並不是字體左上角, 而是以 `baseline` 為基準
> - `font: 'font-style font-weight font-size font-family'`
> - 字體就改這個 key vlaue, 寫法跟 css ㄧ樣
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
body {
background: black;
text-align: center;
}
canvas {
background: white;
}
</style>
<script>
window.onload = function () {
let oCanvas = document.querySelector(`canvas`);
let oGC = oCanvas.getContext(`2d`);
oGC.beginPath();
console.log(oGC)
oGC.font = 'italic 700 30px SimSun';
oGC.strokeText('可黏啊', 100, 100);
oGC.fillText('abc', 200, 200)
}
</script>
</head>
<body>
<canvas height='600px' width='600px'></canvas>
</body>
```
### transform
> - Canvas 的 transform 有三種: `rotate(弧度)` `translate(x,y)` `scale()`
> - Canvas 沒有圖形的概念, 所以不能對畫布上某個東西進行有的沒的, 一切都是像素點
> 也就是說當 Canvas 做 transform 時, 是整個畫布(左上角)進行改變
> - 旋轉前
> <img src='https://i.imgur.com/uJhyF6q.png' style='width: 200px'/>
> - 旋轉後
> <img src='https://i.imgur.com/TCqKAIq.png' style='width: 200px'/>
```javascript=
window.onload = function () {
let oCanvas = document.querySelector(`canvas`);
let oGC = oCanvas.getContext(`2d`);
oGC.rotate(30*Math.PI/180); // 旋轉
oGC.strokeRect(200, 100, 100, 150); // 畫之前轉, 畫完再轉也沒屁用
oGC.fillStyle = 'rgba(100,30,30,.2)';
oGC.fillRect(0,0,oCanvas.width, oCanvas.height);
}
```
> - 所以如果想要向 CSS3 一樣跟著中心點旋轉
> - 首先畫圖的中心點位置必須在旋轉點處,
> 也就是`(-圖寬/2, -圖高/2)`;
> - 接著旋轉, 然後移位到原本想畫的位置(要把`-圖寬高/2`加回來)
> 切記要先寫移位再寫旋轉,跟CSS3一樣,看起來是先做後面再做前面
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
body {
background: black;
text-align: center;
}
canvas {
background: white;
}
</style>
<script>
window.onload = function () {
let oCanvas = document.querySelector(`canvas`);
let oGC = oCanvas.getContext(`2d`);
// 先寫位移,後寫旋轉 => 先旋轉後位移
oGC.translate(250, 175); // (200+50 , 100+75)
oGC.rotate(30*Math.PI/180);
// 圖的'中心'要在旋轉點(畫布 0,0)的位置
oGC.strokeRect(-50, -75, 100, 150); // (-100/2, -150/2)
// oGC.strokeRect(200, 100, 100, 150);
oGC.fillStyle = 'rgba(100,30,30,.2)';
oGC.fillRect(0,0,oCanvas.width, oCanvas.height);
}
</script>
</head>
<body>
<canvas height='600px' width='600px'></canvas>
</body>
```
#### 方塊旋轉起來!!!
> - 筆記:
> - 狀態是會疊加的, 例如
> ```javascript
> oGC.translat(1,1);
> oGC.translat(2,2);
> ```
> 此時並不是位移下右 2px, 而是 3px(1+2);
> - `save()` 可以保存 canvas 當下狀態(圖形並不保存), 例如 style, rotate, ...
> - `restore()`可以讀取 canvas 上次保存的狀態
> - 這兩個跟 `beginPath()` 一樣屬於建議要寫的東西
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
body {
background: black;
text-align: center;
}
canvas {
background: white;
}
</style>
<script>
window.onload = function () {
let oCanvas = document.querySelector(`canvas`);
let oGC = oCanvas.getContext(`2d`);
let r = 0;
oGC.beginPath();
function next() {
oGC.clearRect(0,0,oCanvas.width, oCanvas.height);
oGC.save(); // 保存狀態, 也就是空狀態
oGC.translate(250, 175);
oGC.rotate(r*Math.PI/180);
oGC.strokeRect(-50, -75, 100, 150);
oGC.restore(); // 讀取狀態,也就是回到空狀態
r++;
requestAnimationFrame(next);
}
requestAnimationFrame(next);
}
</script>
</head>
<body>
<canvas height='600px' width='600px'></canvas>
</body>
```
> - 封裝起來 everyBody 轉起來
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
body {
background: black;
text-align: center;
}
canvas {
background: white;
}
</style>
<script>
window.onload = function () {
let oCanvas = document.querySelector(`canvas`);
let oGC = oCanvas.getContext(`2d`);
let pic = [
{x:200, y: 100, w:100, h: 150, r:0},
{x:400, y: 200, w:100, h: 150, r:0},
{x:300, y: 400, w:150, h: 150, r:0},
{x:100, y: 100, w:100, h: 200, r:0}
]
function next() {
oGC.clearRect(0,0,oCanvas.width, oCanvas.height);
pic.forEach((v,i)=>{
setNext(v);
if (i%2==0) {
v.r++
} else {
v.r--
}
})
requestAnimationFrame(next);
}
function setNext(picInfo) {
oGC.save();
oGC.translate(picInfo.x+picInfo.w/2, picInfo.y+picInfo.h);
oGC.rotate(picInfo.r*Math.PI/180);
oGC.strokeRect(-picInfo.w/2, -picInfo.h/2, picInfo.w, picInfo.h);
oGC.restore();
}
requestAnimationFrame(next);
}
</script>
</head>
<body>
<canvas height='600px' width='600px'></canvas>
</body>
```
### 圖片應用
#### 基礎使用
`drawImage(ImageObj, destX, destY)`
> - `1st` 必須是讀好的圖片物件(對象),而非字串
```htmlmixed=
window.onload = function () {
let oCanvas = document.querySelector(`canvas`);
let oGC = oCanvas.getContext(`2d`);
oGC.drawImage(`https://picsum.photos/300/400?random=1`, 100, 100);
}
```
```shell
Failed to execute 'drawImage' on 'CanvasRenderingContext2D':
The provided value is not of type
'(CSSImageValue or HTMLImageElement or SVGImageElement or
HTMLVideoElement or HTMLCanvasElement or ImageBitmap or OffscreenCanvas)'
```
> - 圖片對象可以是報錯訊息的那些東西
> - `<img>`, `ImageObj`, `CanvasObj`, `VideoObj`, `Base64`(CSSImageValue)
> - `Base64` 就是圖片本身了,不用 onload, 那東西太長了,例子先用一般的
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
body {
background: black;
text-align: center;
}
canvas {
background: white;
}
</style>
<script>
window.onload = function () {
let oCanvas = document.querySelector(`canvas`);
let oGC = oCanvas.getContext(`2d`);
let oImg = new Image();
//=> let oImg = document.createElement(`img`); === new Image()
oImg.src = 'https://picsum.photos/300/400?random=1';
// 讀完再畫
oImg.onload = ev=>{
oGC.drawImage(oImg, 100, 100);
}
}
</script>
</head>
<body>
<canvas height='600px' width='600px'></canvas>
</body>
```
#### 全部參數
`drawImage(imgObj, sX, sY, sW, sH, dX, dY, dW, dH)`
> - `sX` `sY` `sW` `sH` :
> - s = source
> - 擷取原圖的座標與大小
> - `dX` `dY` `dW` `dH` :
> - d = dest
> - 放在畫布上的座標與大小(比擷取的大會放大該擷取部分, 反之縮小),
> ps. 放大當然會失真
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
body {
background: black;
text-align: center;
}
canvas {
background: white;
}
</style>
<script>
window.onload = function () {
let oCanvas = document.querySelector(`canvas`);
let oGC = oCanvas.getContext(`2d`);
let oImg = new Image();
oImg.src = 'https://picsum.photos/400/400?random=1';
oImg.onload = ev=>{
oGC.drawImage(oImg, 0, 0); // 截一張完整的比對
oGC.drawImage(
oImg,
200, 200, 200, 100, // 擷取原圖的 (200, 200), 截了寬高 200 x 100
400, 400, 400, 200 // 放在畫布的 (400, 400), 並放大一倍
);
}
}
</script>
</head>
<body>
<canvas height='800px' width='800px'></canvas>
</body>
```
#### 實作: 人物走路

```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
body {
background: black;
text-align: center;
}
canvas {
background: white;
}
</style>
<script>
window.onload = function () {
let oCanvas = document.querySelector(`canvas`);
let oGC = oCanvas.getContext(`2d`);
let oImg = new Image();
oImg.src = 'https://truth.bahamut.com.tw/s01/201205/77388494bff01a80faa65235a5ca089c.PNG';
// - 這張圖 148 x 212 / 4 x 4 個人物 => 37 x 53 / 1個人物
oImg.onload = ev=>{
let step = 0, // 位移量
frameCount = 0, // 幀數
y = 100, // destY
pause = false; // 是否暫停
function next() {
if (!pause) {
oGC.clearRect(0,0,oCanvas.width, oCanvas.height);
oGC.drawImage(
oImg,
37*step, 0, 37, 53,
100, y, 37,53
);
// console.log(step);
// 每十幀走一步
if (frameCount%10==0) {
step++
if (step==4)step=0;
y+=2
}
frameCount++
}
requestAnimationFrame(next);
}
requestAnimationFrame(next);
// 每次點擊都取反
document.documentElement.onclick = function () {
pause = !pause; // 這很酷
}
}
}
</script>
</head>
<body>
<canvas height='800px' width='800px'></canvas>
</body>
```
### 像素應用
> - 性能相對低
> #### `getImageData(dx, dy, dw, dh)`: 獲取圖片資料
> - 這東西有跨域問題, 瀏覽器不允許JS擅自讀取圖片資訊,
> 否則我只要在 src 寫一些本地路徑,用戶打開網頁就會被讀取,
> 說不定路徑檔名被矇中了什麼秘密, 所以使用這功能必須解決跨域問題(向對方服務器要資料, 對方要給同意頭)
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
body {
background: black;
text-align: center;
}
canvas {
background: white;
}
</style>
<script>
window.onload = function () {
let oCanvas = document.querySelector(`canvas`);
let oGC = oCanvas.getContext(`2d`);
let oImg = new Image();
oImg.src = `http://localhost:5566/1.jpg`;
oImg.onload = ev=>{
oGC.drawImage(oImg, 0, 0);
let imgData = oGC.getImageData(0,0,oCanvas.width, oCanvas.height);
console.log(imgData);
}
}
</script>
</head>
<body>
<canvas height='533px' width='800px'></canvas>
</body>
```
```javascript=
let http = require(`http`);
let url = require(`url`);
let fs = require(`fs`);
http.createServer((req, res)=>{
let pathname, query = url.parse(req.url);
console.log(req.url);
pathname = req.url.substr(1);
rs = fs.createReadStream(pathname);
// 給頭
res.setHeader(`Access-Control-Allow-Origin`, `*`);
rs.pipe(res);
rs.on(`error`, err=>{
console.log(err);
res.writeHead(404);
res.write(`Noob`);
res.end();
})
}).listen(5566);
```
```txt
# 拿到一個對象,裡面有寬高跟 1705600 筆的數據
ImageData {}
width: 800
height: 533
data: Uint8ClampedArray(1705600) […]
__proto__: ImageData
```
> #### `Uint8ClampedArray(1705600)`
> - 1705600/4 = 426400 = 800 * 533
> - 1px 占 4 byte => rgba
> - 8 => `8bit`
> - `Uint` => `unsigned int` => `0~255`
> - r : 0~255
> - g : 0~255
> - b : 0~255
> - a : 0~255
> - alpha 是 0~1 => float 至少 32 位
> 但是為了方便而只用 8 位來存
> - 證明: 量第一格的 rgba
>  
```javascript=
console.log(`
r: ${imgData.data[0]},
g: ${imgData.data[1]},
b: ${imgData.data[2]},
a: ${imgData.data[3]}
`);
/*
r: 11,
g: 16,
b: 20,
a: 255
*/
```
#### 實作: 改變照片色調
> <img src='https://i.imgur.com/3m5t5P4.jpg' style='width: 200px'> -> <img src='https://i.imgur.com/32tHlbY.jpg' style='width: 200px'>
> - 黃色 = 紅 + 綠 => 丟掉藍色
> - 灰色 => 三原色數值相等
> - 變亮 => 值越大越亮
> #### `putImageData(imgObj, dx, dy)`: 將 圖資OBJ 畫到 環境 中
```txt
- 假設一張圖片的為 200 x 300
- 第 0 行第 0 列的像素格: 第 0 格
- 第 0 行第 1 列的像素格: 第 1 格
- 第 0 行第 199 列的像素格: 第 199 格 (首行末格)
- 第 1 行第 0 列的像素格: 第 200 格
- 第 y 行第 x 列的像素格: 第 y * width + x 格
- 1px 占 4 Byte (r,g,b,a)
- 第 y * width + x 格 的第 0 位是 r
- 第 y * width + x 格 的第 1 位是 g
- 第 y * width + x 格 的第 2 位是 b
- 第 y * width + x 格 的第 3 位是 a
```
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
body {
background: black;
text-align: center;
}
canvas {
background: white;
}
</style>
<script>
window.onload = function () {
let oCanvas = document.querySelector(`canvas`);
let oGC = oCanvas.getContext(`2d`);
let W = oCanvas.width,
H = oCanvas.height;
let oImg = new Image();
oImg.src = `http://localhost:5566/1.jpg`;
oImg.onload = ev=>{
oGC.drawImage(oImg, 0, 0);
let imgData = oGC.getImageData(0, 0, W, H);
let data = imgData.data;
console.log(imgData);
for (let y = 0; y < H; y++) {
for (let x = 0 ; x < W; x++) {
// r = imgData.data[(y * W + x) * 4 + 0]
// g = imgData.data[(y * W + x) * 4 + 1]
// b = imgData.data[(y * W + x) * 4 + 2]
// a = imgData.data[(y * W + x) * 4 + 3]
// - 讓顏色變黃: 去掉藍色就變黃了
// data[(y*W+x)*4+2] = 0
// - 讓顏色變灰: 三原色比例相同就是灰 => 讓三原色等於三原色的平均即可
// - 讓顏色變亮: 值越大就越亮, 我就乘或加值就好了
let color = (data[(y*W+x)*4+0]+ data[(y*W+x)*4+1]+ data[(y*W+x)*4+2])/3;
data[(y*W+x)*4+0] = data[(y*W+x)*4+1] = data[(y*W+x)*4+2] = color * 1.0;
}
}
oGC.putImageData(imgData, 0, 0);
}
}
</script>
</head>
<body>
<canvas height='533px' width='800px'></canvas>
</body>
```
#### 上傳圖片
> #### `canvasObj.toDataURL()`
> - 這東西會將 canvasObj 轉成一串 base64 來返回
> - 再把這串返回的東西傳回服務器就好了
> - 服務器再寫入文件時,要把 base64 前面那串頭 (`data:image/png;base64,`) 給去掉, 後面才是數據
> - 寫入時要指定一下寫入類型是 base64
```htmlmixed=
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<style>
body {
background: black;
text-align: center;
}
canvas {
background: white;
}
</style>
<script>
window.onload = function () {
let oC = document.querySelector(`#c1`);
let oGD = oC.getContext(`2d`);
let W = oC.width,
H = oC.height;
let oImg = new Image();
oImg.src = 'http://localhost:5566/1.jpg';
// 調色
oImg.onload = function () {
oGD.drawImage(oImg, 0,0);
let imgData = oGD.getImageData(0,0,W,H);
let data = imgData.data;
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
let color = (data[(y*W+x)*4+0] + data[(y*W+x)*4+1] + data[(y*W+x)*4+2])/3;
data[(y*W+x)*4+0] = data[(y*W+x)*4+1] = data[(y*W+x)*4+2] = color * 1.0;
}
}
oGD.putImageData(imgData, 0, 0);
}
// 點擊上傳
let oBtn = document.querySelector(`input[type=button]`);
oBtn.onclick = function () {
// 將數據轉 base64
let base64 = oC.toDataURL();
// 傳回服務器
let xhr = new XMLHttpRequest();
xhr.open(`POST`, `http://localhost:5566/base64`, true);
xhr.send(base64);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
console.log(xhr.responseText);
} else {
console.log(`ERROR`);
}
}
}
}
}
</script>
</head>
<body>
<input type='button'></input>
<canvas width='800px' height='533px' id='c1'></canvas>
</body>
</html>
```
```javascript=
let http = require(`http`);
let fs = require(`fs`);
http.createServer((req, res)=>{
console.log(req.url);
// 判斷是否是上傳
if (req.url == `/base64`) {
// 收數據
let arr = [];
req.on(`data`, data=>{
arr.push(data);
})
req.on(`end`, ()=>{
let buffer = Buffer.concat(arr);
// 寫數據
// - base64 前面有一串 'data: ....;base64,' 的東西
// - 後台寫入文件時,前面沒有那串,所以要先砍掉再寫入
// - 複習: fs.writeFile(file, data[, options], cb(err){})
// - options 裡可以指定寫入的類型
fs.writeFile((Math.floor(Math.random()*99+1))+`.png`, buffer.toString().replace(/^data:[^,]+;base64,/, ''), 'base64', err=>{
if (err) {
console.log(err);
res.end(`Not OK`);
} else {
res.end(`OK`);
}
})
})
} else {
req.url = req.url.substr(1);
let rs = fs.createReadStream(req.url);
rs.pipe(res);
rs.on(`error`, err=>{
res.write(`404`);
res.writeHeader(404);
res.end();
})
}
}).listen(5566);
```
#### 下載圖片
> - 由於 download 屬性兼容性很低, 所以只能用其他辦法
> - 基本方向就是將圖片上傳給服務器,
> 服務器再傳回來給你,並附上一個讓瀏覽器執行下載的[頭](https://blog.csdn.net/wwd0501/article/details/49891023)
> - `Content-Disposition: attachment; filename=檔名`
> - 用 AJAX 遇到問題還解決不了, 所以改用 form
> - 用 form 的邏輯:
> - 1. 案下下載時, 把數據丟到隱藏的 form 表單裡, 然後送出表單
> - 2. 接著服務器對拿來的數據進行處理
> - 2.1 表單送來的POST數據變成 URL 編碼, 所以必須 decode
> - 2.2 表單送來的POST數據會有 name1=value 之類的東西, 要把那東西給砍了
> - 2.3 最後必須把原本 base64 的頭給砍了
> - 3. 把數據存起來後讀取該檔案, 然後把讀到的東西送回瀏覽器
> - 4. 送回去之前要給下載頭
```htmlmixed=
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<style>
body {
background: black;
text-align: center;
}
canvas {
background: white;
}
</style>
<script>
window.onload = function () {
let oC = document.querySelector(`#c1`);
let oGD = oC.getContext(`2d`);
let W = oC.width,
H = oC.height;
let oImg = new Image();
oImg.src = 'http://localhost:5566/1.jpg';
oImg.onload = function () {
oGD.drawImage(oImg, 0,0);
let imgData = oGD.getImageData(0,0,W,H);
let data = imgData.data;
// console.log(data);
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
let color = (data[(y*W+x)*4+0] + data[(y*W+x)*4+1] + data[(y*W+x)*4+2])/3;
data[(y*W+x)*4+0] = data[(y*W+x)*4+1] = data[(y*W+x)*4+2] = color * 1.0;
}
}
oGD.putImageData(imgData, 0, 0);
}
let oBtn = document.querySelector(`input[type=button]`);
oBtn.onclick = function () {
let base64 = oC.toDataURL();
// AJAX 有點問題, 所以改用 form
let oText = document.getElementsByName(`col`)[0].value = base64;
document.getElementById(`form1`).submit();
}
}
</script>
</head>
<body>
<form id='form1' action='http://localhost:5566/base64' method='POST' style='display:none;'>
<textarea name='col' rows='10' cols='100'></textarea>
</form>
<input type='button'></input>
<canvas width='800px' height='533px' id='c1'></canvas>
</body>
</html>
```
```javascript=
let http = require(`http`);
let fs = require(`fs`);
http.createServer((req, res)=>{
console.log(req.url);
if (req.url == `/base64`) {
let arr = [];
req.on(`data`, data=>{
arr.push(data);
})
req.on(`end`, ()=>{
let buffer = Buffer.concat(arr);
/*
// - 版本一:
// 1. 先將檔名存起來, 等等要讀檔傳回去
let fileName = (Math.floor(Math.random()*99+1))+`.png`
// 2. form POST 數據處理
// console.log(buffer.toString().substring(0,100));
// col=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0
// 2.1. 傳進來的是 URL 編碼, 所以要先 decode
// console.log(decodeURIComponent(buffer.toString().substring(0,100)));
// col=data:image/png;base64,iVBORw0KGgoAAAANSU
// 2.2 要把 col= 給砍了 (form 的 name)
// 2.3 要把 base64 的頭給砍了
let dataProcess = decodeURIComponent(buffer.toString().replace(/^col=/, '')).replace(/^data:[^,]+;base64,/, '')
// 3. 把數據存起來
fs.writeFile(fileName, dataProcess, 'base64', err=>{
if (err) {
console.log(`writeFileError: ${err}`);
res.end(`Not OK`);
} else {
// 4. 讀數據傳回去
fs.readFile(fileName, (err,data)=>{
if (err) {
console.log(`readFileError: ${err}`);
res.end(`NOT OK`);
} else {
// 5. 傳回去的時候要給頭
res.setHeader(`Content-Disposition`, `attachment; filename=download.png`);
res.end(data);
}
})
}
})
*/
// 版本二:
// - 把拿到的數據值去頭去一去再用 new Buffer 轉成 base64 傳回去就好了
// - 不用在存起來讀取傳回去
let dataProcess = new Buffer(decodeURIComponent(buffer.toString().replace(/^col=/, '')).replace(/^data:[^,]+;base64,/, ''), 'base64')
res.setHeader(`Content-Disposition`, `attachment; filename=download.png`);
res.end(dataProcess);
})
} else {
req.url = req.url.substr(1);
let rs = fs.createReadStream(req.url);
rs.pipe(res);
rs.on(`error`, err=>{
res.write(`404`);
res.writeHeader(404);
res.end();
})
}
}).listen(5566);
```
#### 用 AJAX 下載時遇到的問題(未找到答案)
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
body {
background: black;
text-align: center;
}
canvas {
background: white;
}
</style>
<script>
window.onload = function () {
let oC = document.querySelector(`#c1`);
let oGD = oC.getContext(`2d`);
let W = oC.width,
H = oC.height;
let oImg = new Image();
oImg.src = 'http://localhost:5566/1.jpg';
oImg.onload = function () {
oGD.drawImage(oImg, 0,0);
let imgData = oGD.getImageData(0,0,W,H);
let data = imgData.data;
// console.log(data);
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
let color = (data[(y*W+x)*4+0] + data[(y*W+x)*4+1] + data[(y*W+x)*4+2])/3;
data[(y*W+x)*4+0] = data[(y*W+x)*4+1] = data[(y*W+x)*4+2] = color * 1.0;
}
}
oGD.putImageData(imgData, 0, 0);
}
let oBtn = document.querySelector(`input[type=button]`);
oBtn.onclick = function () {
let base64 = oC.toDataURL();
// 我把東西送出去, 服務器也的確收到, 而且服務器送回來的圖片我也有收到
let xhr = new XMLHttpRequest();
xhr.open(`POST`, `http://localhost:5566/base64`, true);
xhr.send(base64);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
console.log(`SUCCESS`);
console.log(xhr.response);
} else {
console.log(`ERROR`);
}
}
}
}
}
</script>
</head>
<body>
<input type='button'></input>
<canvas width='800px' height='533px' id='c1'></canvas>
</body>
```
```javascript=
let http = require(`http`);
let fs = require(`fs`);
http.createServer((req, res)=>{
console.log(req.url);
if (req.url == `/base64`) {
let arr = [];
req.on(`data`, data=>{
arr.push(data);
})
req.on(`end`, ()=>{
let buffer = Buffer.concat(arr);
let fileName = (Math.floor(Math.random()*99+1))+`.png`
let dataProcess = buffer.toString().replace(/^data:[^,]+;base64,/, '')
fs.writeFile(fileName, dataProcess, 'base64', err=>{
if (err) {
console.log(`writeFileError: ${err}`);
res.end(`Not OK`);
} else {
fs.readFile(fileName, (err,data)=>{
if (err) {
console.log(`readFileError: ${err}`);
res.end(`NOT OK`);
} else {
// 我給了下載頭, 然後數據不管是二進制還是轉 base64 傳過去,
// 瀏覽器都不屌我設置的頭=.=, AJAX 的確有收到歐, 但就是不屌我
// 不知道是不是因為 AJAX 不跳轉的原因, 導致他沒有跳轉到下載頁面
res.setHeader(`Content-Disposition`, `attachment`, `filename=download.png`);
data = 'data:image/png;base64,' + new Buffer(data).toString('base64');
res.end(data);
}
})
}
})
})
} else {
req.url = req.url.substr(1);
let rs = fs.createReadStream(req.url);
rs.pipe(res);
rs.on(`error`, err=>{
res.write(`404`);
res.writeHeader(404);
res.end();
})
}
}).listen(5566);
```
### 影片操作
> - 影片跟照片的操作一模一樣
> - 頻繁切換照片就是影片, 也就是用到動畫幀的概念
> - 測試用的影片是載這個
> https://www.w3school.com.cn/i/movie.ogg
> - 注意: chrome 的 `<vedio>` 規定, 自動播放需要一些處理, 最簡單的就是靜音
> - https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
body {
background: black;
text-align: center;
}
canvas {
background: white;
}
</style>
<script>
window.onload = function () {
let oV = document.querySelector(`#v1`);
let oC = document.querySelector(`#c1`);
let oGD = oC.getContext(`2d`);
let W = oC.width,
H = oC.height;
// 1. 要一直畫才會有動畫效果
// oGD.drawImage(oV, 0, 0);
// 2. 影片更新時觸發, 但這東西觸發的不是很頻繁, 造成效果很差
// oV.ontimeupdate = function (ev) {
// console.log(oV.currentTime);
// oGD.drawImage(oV, 0, 0);
// }
// 3. 老樣子, 讚
function next() {
oGD.drawImage(oV, 0, 0);
// 3-1. 改色調
let imgData = oGD.getImageData(0,0,W,H);
let data = imgData.data;
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
let color = (data[(y*W+x)*4+0] + data[(y*W+x)*4+1] + data[(y*W+x)*4+2])/3
data[(y*W+x)*4+0] = data[(y*W+x)*4+1] = data[(y*W+x)*4+2] = color * 1.0;
}
}
oGD.putImageData(imgData,0,0);
requestAnimationFrame(next);
}
requestAnimationFrame(next);
}
</script>
</head>
<body>
<video src="1.ogv" muted autoplay loop="-1" id='v1'></video>
<canvas width='320px' height='240px' id='c1'></canvas>
</body>
```
## SVG
`<svg width height></svg>`
> - 矢量
> - 不失真
> - 沒有單位, 他是一個相對值, 簡單說都是倍數
> - Q. 如何自適應不同裝置?
> - 所有點的位置都重算一遍
> - 首先, 那些座標點本來就是從資料庫弄進去的,自己手算會哭
> - 用 div 包, 對 div 做 scale
> - 縮放可能會很怪,就是 4:3 應縮放到 4:6 之類的
> - 性能比一開始全部重算還差,因為每次渲染畫面都要計算一遍
> - 保留圖
> - 事件屬性都有
> - 性能普通
> - 雖然是標籤,但不是HTML標準, 是 SVG 標準 (同一個祖先: XML)
> - SVG 的屬性大致分為兩類
> - 一類是會影響圖形形狀的, 例如座標, 這種只能放屬性(行內)
> - 一類是不影響圖形形狀的, 只會改變視覺效果的屬性, 這種可以放 style
> - 這種的儘量放在 style, 因為屬性的優先級太低了, 比 `*` 還廢
> <img src='https://i.imgur.com/z8TBGnb.png' style='width: 300px'/>
> - 兼容 (Raphael.js)
> - SVG: IE9+, Chrome, FF, ...
> - VML: IE4.0 ~ IE7.0
> - IE8 不能用
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
* {
stroke: red; /* 區區一個 * 就蓋掉左邊那條的顏色了 */
}
</style>
</head>
<body>
<svg width='800' height='600'>
<line x1='100' y1='100' x2='300' y2='300' stroke='black' stroke-width='20'></line>
<line x1='200' y1='100' x2='400' y2='300' style='stroke: blue; stroke-width: 30;'></line>
</svg>
</body>
```
> - 添加事件與修改樣式跟HTML都一樣
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
* {
stroke: red;
stroke-width: 20;
}
div {
height: 100px;
width: 100px;
background: blue;
}
</style>
<script>
window.onload = function () {
let oLine = document.querySelector(`line`);
/*
oLine.onmouseover = function () {
console.log(this);
this.style.stroke = 'green';
}
oLine.onmouseout = function () {
this.style.stroke = 'red';
}
*/
/*
oLine.addEventListener(`mouseover`, function () {
this.style.stroke = 'green';
}, false);
oLine.addEventListener(`mouseout`, function () {
this.style.stroke = 'red';
}, false);
*/
// 事件委託也行
document.body.onmouseover = function (ev) {
if (ev.srcElement == oLine) {
ev.srcElement.style.stroke = 'green';
}
}
document.body.onmouseout = function (ev) {
if (ev.srcElement == oLine) {
ev.srcElement.style.stroke = 'red';
}
}
}
</script>
</head>
<body>
<svg width='800' height='600'>
<!-- 行內添加事件也沒問題
<line x1='100' y1='100' x2='300' y2='300'
onmouseover='this.style.stroke="green";'
onmouseout='this.style.stroke="red";'></line>
-->
<line x1='100' y1='100' x2='300' y2='300'></line>
</svg>
</body>
```
> - 操作屬性的方法跟HTML些微不同
> - 最標準的操作屬性方法本來就是
> `.getAttribute('attr')` `.setAttribute('attr', 'value')`
> - HTML 是可以直接 .attr 來拿來改的
> - SVG 只認最標準的這種
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
* {
stroke: red;
stroke-width: 20;
}
div {
height: 100px;
width: 100px;
background: blue;
}
</style>
<script>
window.onload = function () {
let oLine = document.querySelector(`line`);
oLine.onclick = function (ev) {
console.log(oLine.x1, oLine.getAttribute(`x1`));
// SVGAnimatedLength {...} "100"
// 直接 .xx 不會拿到值, 而是一個 Obj
oLine.setAttribute(`x1`, 200);
}
}
</script>
</head>
<body>
<svg width='800' height='600'>
<line x1='100' y1='100' x2='300' y2='300'></line>
</svg>
</body>
```
### 線
`<line x1 x2 y1 y2></line>`
> - `stroke` : 線色
> - 線默認是 none, 必須填色才有效果
> - `stroke-width` : 線寬
### 矩形
`<rect x y width height rx ry></rect>`
> - `rx` `ry`: 就是 svg 的 border-radius
> - 這兩個可以丟 style
> - 這兩個值就是在四個角畫圓後,四角對於該圓心的距離所畫出的圓而拉出圓角
> - 
> - 當只設置一個值時,默認另外一個值會跟該值相同
```html
<body style="background: yellow">
<svg xmlns="http://www.w3.org/2000/svg" style="height: 800px">
<rect x="50" y="50" width="50" height="50" rx="10" ry="100"></rect>
<rect x="50" y="200" width="50" height="50" rx="10"></rect>
</svg>
</body>
```
> - `fill` : 填色
> - 默認是黑色
> - 注意:
> - SVG 如果沒有上色(value=none),那就是沒有,
> 也就是即使劃定了位置,綁定事件去觸發也都不會有任何反應
> - 如果想要觸發事件又不想要顏色,那就用 rgba 透明就好了
```html
<head>
<meta charset='utf-8'>
<style>
rect {
rx: 10;
ry: 10;
/*fill: none;*/
fill: rgba(0,0,0,0);
stroke: blue;
stroke-width: 10;
}
</style>
<script>
window.onload = function () {
let oRect = document.querySelector(`rect`);
oRect.onclick = function () {
console.log(1);
// 如果沒有填充顏色,是不會觸發事件的
// 也就是說如果我的方形 fill='none';
// 那點擊在國王的新衣上也不會有任何反應
}
}
</script>
</head>
<body>
<svg width='800' height='600'>
<rect x='100' y='100' width='300' height='400'></rect>
</svg>
</body>
```
### 圓形
`<circle cx cy r></circle>`
> - `cx` `cy` : 圓心座標
> - `r` : 半徑
```htmlmixed=
<svg width='800' height='600'>
<circle cx='300' cy='300' r='100'></circle>
</svg>
```
### 橢圓
`<ellipse cs cy rx ry></ellipse>`
> - `rx` `ry` : 半徑 x y
```htmlmixed=
<svg width='800' height='600'>
<ellipse cx='300' cy='300' rx='200' ry='100'></ellipse>
<!-- 200*2 * 100*2 -->
</svg>
```
### [Path](https://www.w3.org/TR/SVG/paths.html)
`<path d ></path>`
> - `d` : 裡面拿來放各種各樣的路徑操作
> - `M` : (x,y)+ MoveTo
> - `L` : (x,y)+ LineTo
```htmlmixed=
<svg width='800' height='600'>
<!-- 這些寫法都可以 -->
<!-- <path d='M 100,100 L 200, 200 L 500, 300 L 400, 400' style='stroke: blue; fill: none'></path> -->
<!-- <path d='M 100 100 L 200 200 L 500 300 L 400 400' style='stroke: blue; fill: none'></path> -->
<path d='M 100 100 L 200 200 500 300 400 400' style='stroke: blue; fill: none'></path>
</svg>
```
> - `a` (elliptical arc)
> - `(rx ry x-axis-rotation large-arc-flag sweep-flag x y)+`
> - `rx` `ry` : 橫縱半徑
> - `x-axis-rotation` : x 軸旋轉
> - `large-arc-flag` : 大弧標誌, 0 (小弧) 1 (大弧)
> - `sweep-flag` : 鏡像標誌, 0 (左) 1 (右)
> 
> - 起點到終點可以畫兩個橢圓, 並從起點到終點畫一條線
> 此時就會產生四個弧(兩大兩小)與兩邊(左右)
> - `large-arc-flag` 就是決定要畫大弧還是小弧
> - `sweep-flag` 則是決定畫左邊還是右邊
> - `x` `y` : 終點座標
> - `Z` (closepath)
#### 實作: 畫一個圓餅小區
> - <img src='https://i.imgur.com/rGClgeO.png' style='width: 200px'>
> - 跟 canvas 一樣, 先畫一條線,再畫弧,最後關路徑
```htmlmixed=
<head>
<meta charset='utf-8'>
<script>
function toRadian(angle) {
return angle * Math.PI / 180
}
window.onload = function () {
let oP = document.getElementById(`p1`);
let cx = 400, cy = 300, r = 200,
ang1 = 20, ang2 = 60;
// 小技巧:
// - d = 'M xx xx L xx xx A xx xx ...'
// - 每次都要 setAttribute 太麻煩了, 先組起來一次改
let arr = [];
function point(angle) {
return {
x: cx + Math.sin(toRadian(angle)) * r,
y: cy - Math.cos(toRadian(angle)) * r
}
}
// 線
// let x1 = cx + Math.sin(toRadian(ang1)) * r,
// y1 = cy - Math.cos(toRadian(ang1)) * r;
let {x:x1, y:y1} = point(ang1);
arr.push(`M ${cx} ${cy} L ${x1} ${y1}`);
// 弧
let {x:x2, y:y2} = point(ang2);
arr.push(`A ${r} ${r} 0 ${Math.abs(ang2-ang1)<180?0:1} ${ang1<ang2?1:0} ${x2} ${y2}`);
// 關
arr.push(`Z`);
oP.setAttribute(`d`, arr.join(` `));
}
</script>
</head>
<body>
<svg width='800' height='600'>
<path id='p1' style='stroke: blue; fill:none'></path>
</svg>
</body>
```
> - `Q (x1 y1 x y)+`(quadratic Bézier curveto)
> - <img src='https://i.imgur.com/6XhMATl.jpg' style='width: 250px'>
> - 將起點到終點的線往一個方向`Q 座標`拉
> - `x1` `y1` 拉力座標
> - `x` `y` 終點座標
```htmlmixed=
<svg width='800' height='600'>
<path d='M 100 300 Q 300 100 400 300' style='stroke:blue;fill:none;'></path>
</svg>
```
### JS 創建 SVG
`createElementNS('http://www.w3.org/2000/svg', 標籤名)`
> - 事實上以前在用的那個 `createElement('標籤名')` 是
> `createElementNS('http://www.w3.org/1999/xhtml', 標籤名)` 的簡寫
> 也就是那東西只能創建 HTML 標籤
> - 而要創造 SVG 標籤,那就要使用最原本的 `createElementNS(命名空間, 標籤名)`
> - NS: Name Space
```htmlmixed=
<head>
<meta charset='utf-8'>
<script>
window.onload = function () {
let oSvg = document.querySelector(`svg`);
let oL1 = document.querySelector(`line`);
let oLine = document.createElementNS(`http://www.w3.org/2000/svg`, `line`);
oLine.setAttribute(`x1`, 100);
oLine.setAttribute(`y1`, 300);
oLine.setAttribute(`x2`, 300);
oLine.setAttribute(`y2`, 500);
oLine.setAttribute(`style`, `stroke: blue;`);
oSvg.appendChild(oLine);
let oLine2 = document.createElement(`line`); //=> 只會創建HTML標籤
alert(oL1); // [object SVGLineElement]
alert(oLine); // [object SVGLineElement]
alert(oLine2); // [object HTMLUnknownElement] //=> HTML 標籤,而且HTML不認識
}
</script>
</head>
<body>
<svg width="800" height="600">
<line x1="100" y1="100" x2="300" y2="300" style="stroke: red;"></line>
</svg>
</body>
```
### SVG 動畫原理
> - 用 `setInterval` 或 `requestAnimationFrame` 來跑
```htmlmixed=
<head>
<meta charset='utf-8'>
<script>
window.onload = function() {
let oLi = document.getElementById(`l1`);
let start = parseInt(oLi.getAttribute(`x1`)); // 起始點
let end = 300; // 終點
let dis = end-start; // 總共走的量
let size = 200; // 走的總步數
let count = 0; // 走第幾次
function next() {
count++;
/* 勻速運動
let a = count / size; //=> 走n步 / 總步數
let cur = start + dis * a;
*/
/* 漸漸加速
let a = count / size;
let cur = start + dis * a * a * a;
*/
// 減速運動
let a = 1 - count / size;
let cur = start + dis * ( 1 - a * a * a);
oLi.setAttribute(`x1`, cur);
if (count<size) {
requestAnimationFrame(next);
}
}
requestAnimationFrame(next);
}
</script>
</head>
<body>
<svg width='800' height='600'>
<line x1='100' y1='100' x2='300' y2='300' style='stroke:blue;stroke-width: 20;' id='l1'></line>
</svg>
</body>
```
#### 實作: 圓餅動畫效果
> - 1. 根據資料動態生成圓餅圖, 並使用隨機色彩填充
> - 2. `mouseover` `mouseout` 改變圓餅圖半徑來進行縮放
> - 3. 動畫效果: 用 `setInterval` 或 `requestAnimationFrame` 來跑
```htmlmixed=
<head>
<meta charset='utf-8'>
<script>
function toRadian(angle) {
return angle * Math.PI / 180
}
window.onload = function () {
let oSVG = document.querySelector(`svg`);
let cx = 300, cy = 300, r = 200;
function pie(ang1, ang2, color) {
let oP = document.createElementNS(`http://www.w3.org/2000/svg`, `path`);
oP.style.fill = color;
oSVG.appendChild(oP);
function calc(r) {
let arr = [];
function point(angle) {
return {
x: cx + Math.sin(toRadian(angle)) * r,
y: cy - Math.cos(toRadian(angle)) * r
}
}
let {x:x1, y:y1} = point(ang1);
arr.push(`M ${cx} ${cy} L ${x1} ${y1}`);
let {x:x2, y:y2} = point(ang2);
arr.push(`A ${r} ${r} 0 ${Math.abs(ang2-ang1)<180?0:1} 1 ${x2} ${y2}`);
arr.push(`Z`);
oP.setAttribute(`d`, arr.join(` `));
}
calc(r);
// 2. 製作動畫:
// - 圓餅圖縮放不外乎就是改變半徑
let curR = r; // 當前半徑
let size = 100; // 最大步數
let fnNext = null;
function move(end) { // end: 最大半徑
let start = curR; // 當前半徑
let dis = end - start; // 須走距離
let count = 0; // 當前走的累積次數
fnNext = function () {
count++;
let a = count / size; // 步數比
curR = start + dis * a; // 當前半徑 = 開始的 + 走了多遠
calc(curR); // 把半徑丟進去改圖
if (count >= size) { // 如果走到了就關掉函數
fnNext = null;
}
}
}
// 2.2 滑鼠經過時, 半徑變成 1.25 倍
oP.onmouseover = function () {
move(r*1.25);
};
// 2.3 滑鼠離開時, 半徑回到 1 倍
oP.onmouseout = function () {
move(r);
};
// 2.1 首先讓動畫不停地轉
// - 為了做出動畫效果, 要馬 setInterval 要馬 requestAnimationFrame
// - 空轉的耗能不會很高
function next() {
fnNext && fnNext();
requestAnimationFrame(next);
}
next();
}
let data = [323,241,228,32];
// 1.1. 根據數據, 生成圓餅圖, 算法跟 canvas 一模一樣
let sum = 0
data.forEach(v=>{sum+=v});
let now = 0;
data.forEach(v=>{
let angle = 360 * v / sum;
// 1.2. 隨機顏色
// 隨機顏色1:
// pie(now, angle+now, `rgb(${Math.floor(Math.random()*256)},${Math.floor(Math.random()*256)},${Math.floor(Math.random()*256)})`);
// 隨機顏色2:
// - #FFFFFF =10=> 16777215
// - Max = 16777215 + 1
// - Number.prototype.toString([radix])
// - Number 的 toString 可以選擇進制
let color = Math.floor(Math.random()*16777216).toString(16);
color = color.length < 6 ? '0'+color : color;
pie(now, angle+now, `#`+color);
now += angle;
})
}
</script>
</head>
<body>
<svg width="800" height="600"></svg>
</body>
```
### VML
> - `<html xmlns:vml="urn:schemas-microsoft-com:vml">`
> - `xmlns` : xml 命名空間, 簡單說就是告訴瀏覽器要遵循哪套標準, 因為以前標準多
> - 簡單說就是為 vml 這個標籤引入 vml ,
> 前面那個 vml 就是標籤名而已, 想叫啥就叫啥
> - `<style>vml\:{behavior: url(#default#VML);}</style>`
> - `vml`: 這名字而已,可以隨便命名, 這邊取啥名, 標籤就叫啥
> - `\:` : `\` 轉譯符, 避免瀏覽器以為這是偽元素
> - `behavior` : IE 瀏覽器特定行為
> - 整句話意思就是 去 #default#VML 加載一個行為給 vml 這個標籤用
> - 創造出來的標籤還是 HTML 標籤, 只是套用了 IE 的樣式模板, 標籤性質跟一般 HTML 一樣
> - `width height`相當於 `<svg width height>` 而不是該圖形的寬高
> - VML 必須要加單位
```htmlmixed=
<!DOCTYPE html>
<html xmlns:vml="urn:schemas-microsoft-com:vml">
<head>
<meta charset='utf-8'>
<style>
vml\:* {
behavior: url(#default#VML);
display: block;
height: 500px;
width:1000px;
}
vml:line {
}
vml:rect {
}
</style>
</head>
<body>
<vml:line from='100, 100' to='300,300'
strokecolor='blue' strokeweight='10'
onclick='this.strokecolor="green"'/>
<vml:rect style='left: 100px; height: 100px'/>
<vml:oval style='width: 400px; height: 400px'/>
<vml:shape path='M 100 100 L 300 300 200 200 X'
style='width: 300px; height: 300px'/>
</body>
</html>
```
## [Raphael.js](https://dmitrybaranovskiy.github.io/raphael/)
> - 一個方便的庫, 好像能讓 SVG 兼容 IE, 不過我一直失敗
> - `Raphael(x,y,w,h)`: 創一個 `paper Obj`
> - `Paper.rect(x,y,w,h)` : 畫布上的 x, y, 寬, 高
> - `Element.attr(k,v)` : 設定屬性
> - 屬性的 path 其實就是 d
> - 如果想一次設置多個 kv, 可以接受 `{"K1":"V1", "K2": "V2"}`
> - `Element.click(fn)` : 點擊事件
> - `Element.animate({param}, ms, 動畫模式)` : 動畫
> - 可以使用鏈式寫法
> - 基本上參數都跟 SVG 差不多
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
</style>
<script src='raphael.js'></script>
<script>
window.onload = function () {
var page = Raphael(0,0,400,400);
var flag = true;
page.rect(100, 100, 200, 200).attr(`fill`, `blue`).attr(`stroke`, `orange`).click(function () {
if (flag) {
this.animate({width: 100, height: 100}, 2000, `ease`);
flag = false;
} else {
this.animate({width: 200, height: 200}, 2000, `ease`);
flag = true;
}
console.log(page);
});
var page2 = Raphael(100, 400 , 300, 300);
page2.path('M 100 100 L 150 200 50 200 Z').attr(`fill`, `blue`).click(function () {
this.animate({path: `M 100 100 L 100 200 100 200 Z`}, 1000, `ease`);
});
var page3 = Raphael(100, 700, 800, 600);
var path3 = page3.path(`M 100 100 L 150 100 M 100 120 L 150 120 M 100 140 L 150 140`).attr(`stroke-width`, 10);
console.log(path3);
path3.click(function () {
path3.animate({path: `M 100 100 L 150 140 M 100 120 L 100 120 M 100 140 L 150 100`}, 1000, `ease`);
})
}
</script>
</head>
```
### 實作: 圓形時鐘
<img src='https://i.imgur.com/EmJihbS.png' style='width: 200px'/>
```htmlmixed=
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<style>
</style>
<script src='raphael.js'></script>
<script>
window.onload = function () {
// 創造畫布
let paper = Raphael(0, 0, 800, 600);
let cx = 300, cy = 300;
// 創建路徑函數
function createPath(r, ang, color) {
let path = paper.path();
path.attr({'stroke-width': 20, 'stroke': color});
// 計算圓弧
function calc(ang, isfalse=false) {
let x = cx + Math.sin(ang*Math.PI/180)*r,
y = cy - Math.cos(ang*Math.PI/180)*r;
let arr = [];
arr.push(`M ${cx} ${cy-r}`);
arr.push(`A ${r} ${r} 0 ${ang > 180? 1:0} ${1} ${x} ${y}`);
// 設置圓弧
// 由於首次設置不需要動畫, 所以用 isfalse 來區別是不是首次設置,
// 沒傳 true 進來就不是首次
if (isfalse) {
path.attr(`path`, arr.join(` `));
} else {
// 0 度時如果做動畫, 原本三百多度的線會有一個縮起來的效果,看起來很詭異, 所以也不做動畫
if (ang == 0) {
path.attr(`path`, arr.join(` `));
} else {
path.animate({path: arr.join(` `)}, 500, `ease`);
}
}
}
// 首次創建時, 塞true進去, 不做動畫
calc(ang, true);
// 重點!!!:
// 把 calc function 存在 pathObj 裡, 讓創建的pathObj可以一直重新計算且重設
path.calc = calc;
return path
}
let paths = []; // 存放所有路徑
function tick() {
let date = new Date();
// 第一次, 創建三條路徑
if (paths.length == 0) {
paths = [
createPath(200, 360 * (date.getHours()>=12?date.getHours()-12:date.getHours()) / 12, `red`),
createPath(150, 360 * date.getMinutes() / 60, `green`),
createPath(100, 360 * date.getSeconds() / 60, `blue`),
]
} else {
// 之後一直重新計算時間並設置值
paths[0].calc(360 * (date.getHours()>=12?date.getHours()-12:date.getHours()) / 12);
paths[1].calc(360 * date.getMinutes() / 60);
paths[2].calc(360 * date.getSeconds() / 60);
}
// console.log(paths);
}
tick();
setInterval(tick, 1000);
}
</script>
</head>
<body>
</body>
</html>
```
### transform
> - 盡量用 attr(`transfrom`, xx) 來設置
> - rotate : `r 旋轉角度`
> - translate : `t x[,y]`
> - scale : `s x[,y]`
> - 寫法跟執行順序(`看起來`是從後面往前執行)都跟 css 一樣
> - 連 animate 都能玩
```javascript=
window.onload = function () {
let paper = Raphael(0, 0, 1000, 1000);
paper.rect(100, 100, 300, 400)
.attr({'transform': `r 20 t 100, 50 s .5, 1`, 'fill': 'green'})
.click(function () {
this.animate({'fill': 'blue', 'transform': 's 1.5, 1'}, 2000, `ease`);
})
}
```
#### 實作: React Logo (尚未理解)
```htmlmixed=
<head>
<meta charset='utf-8'>
<style>
</style>
<script src='raphael.js'></script>
<script>
window.onload = function () {
let paper = Raphael(0, 0, 800, 600);
let cx = 400, cy = 300;
let rx = 150, ry = 50, rs = 20;
// 1. 為了避免起點跟終點重合, 所以將終點 - 0.0001, 既看不見縫縫,又不會重合
paper.path(`M ${cx} ${cy} A ${rx} ${ry} 0 1 1 ${cx} ${cy-0.0001}`)
.attr({'stroke-width': 14, 'stroke': 'blue'});
paper.path(`M ${cx} ${cy} A ${rx} ${ry} 0 1 1 ${cx} ${cy-0.0001}`)
.attr({'stroke-width': 14, 'stroke': 'blue','transform': 'r120'});
paper.path(`M ${cx} ${cy} A ${rx} ${ry} 0 1 1 ${cx} ${cy-0.0001}`)
.attr({'stroke-width': 14, 'stroke': 'blue', 'transform': 'r-120'});
paper.circle(cx/2, cy, rs).attr({'fill': `blue`, 'stroke': 'none'});
paper.circle(cx, cy, rs); // 輔助我理解那神奇的 path 的圓
}
</script>
</head>
<body>
</body>
```