整合營(yíng)銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          Linux 一鍵安裝腳本集(2)

          Linux 一鍵安裝腳本集(2)

          1.LAMP一鍵安裝腳本


          https://teddysun.com/410.html


          12.Ubuntu/Debian/Centos一鍵安裝FTP腳本


          http://vpsxyz.com/archives/21


          13.網(wǎng)速測(cè)試


          主要包括國(guó)內(nèi)的一些節(jié)點(diǎn)進(jìn)行專項(xiàng)測(cè)試,電信、聯(lián)通、移動(dòng)


          wget -N –no-check-certificate “https://raw.githubusercontent.com/WhosYourFathe/UsefulScripts/master/superspeed/superspeed.sh“;chmod +x superspeed.sh;sudo bash ./superspeed.sh


          14.html5網(wǎng)速測(cè)試


          https://down.vpsmm.com/php/speedtest.zip


          需要php環(huán)境,下載后解壓即可運(yùn)行,無(wú)須安裝


          15.SS一鍵包4版本


          一鍵安裝ss服務(wù)器端4合1版本,自主選擇:python版、R版(推薦)、go版、libev版(省內(nèi)存)。


          wget -N –no-check-certificate “https://raw.githubusercontent.com/WhosYourFathe/UsefulScripts/master/ss/ss_all.sh“;chmod +x ss_all.sh;sudo bash ./ss_all.sh


          16.銳速一鍵包


          一鍵安裝銳速破解全功能版,不支持openvz架構(gòu),來(lái)自91yun


          wget -N –no-check-certificate “https://raw.githubusercontent.com/WhosYourFathe/UsefulScripts/master/serverspeeder/serverspeeder.sh“;chmod +x serverspeeder.sh;sudo bash ./serverspeeder.sh


          另一位愛(ài)好者開(kāi)發(fā)的銳速一鍵安裝腳本,開(kāi)發(fā):https://github.com/0oVicero0/serverSpeeser_Install


          wget -N –no-check-certificate “https://raw.githubusercontent.com/WhosYourFathe/UsefulScripts/master/serverspeeder/serverspeeder_appex.sh“;chmod +x serverspeeder_appex.sh;sudo bash ./serverspeeder_appex.sh


          17.FS/finalspeed一鍵包


          一鍵安裝finalspeed功能,支持全系架構(gòu)


          wget -N –no-check-certificate “https://raw.githubusercontent.com/WhosYourFathe/UsefulScripts/master/finalspeed/finalspeed.sh“;chmod +x finalspeed.sh;sudo bash ./finalspeed.sh


          18.mysql一鍵備份腳本


          每天自動(dòng)備份MYSQL及打包網(wǎng)站目錄


          wget -N –no-check-certificate “https://raw.githubusercontent.com/WhosYourFathe/UsefulScripts/master/AutuBackupToFtp/AutoBackupToFtp.sh“;chmod +x AutoBackupToFtp.sh;sudo bash ./AutoBackupToFtp.sh


          19.transfer.sh 中文一鍵安裝腳本


          wget -N –no-check-certificate “https://raw.githubusercontent.com/WhosYourFathe/UsefulScripts/master/transfer/transfer.sh“;chmod +x transfer.sh;sudo bash ./transfer.sh

          我們以Java Web為例,來(lái)搭建一個(gè)簡(jiǎn)單的電商系統(tǒng),看看這個(gè)系統(tǒng)可以如何一步步演變。

          該系統(tǒng)具備的功能:

          • 用戶模塊:用戶注冊(cè)和管理
          • 商品模塊:商品展示和管理
          • 交易模塊:創(chuàng)建交易和管理

          正文


          階段一、單機(jī)構(gòu)建網(wǎng)站

          網(wǎng)站的初期,我們經(jīng)常會(huì)在單機(jī)上跑我們所有的程序和軟件。此時(shí)我們使用一個(gè)容器,如Tomcat、Jetty、Jboss,然后直接使用JSP/Servlet技術(shù),或者使用一些開(kāi)源的框架如Maven + Spring + Struts + Hibernate、Maven + Spring + Spring MVC + Mybatis。最后再選擇一個(gè)數(shù)據(jù)庫(kù)管理系統(tǒng)來(lái)存儲(chǔ)數(shù)據(jù),如MySQL、SqlServer、Oracle,然后通過(guò)JDBC進(jìn)行數(shù)據(jù)庫(kù)的連接和操作。

          把以上的所有軟件包括數(shù)據(jù)庫(kù)、應(yīng)用程序都裝載同一臺(tái)機(jī)器上,應(yīng)用跑起來(lái)了,也算是一個(gè)小系統(tǒng)了。此時(shí)系統(tǒng)結(jié)果如下:

          階段二、應(yīng)用服務(wù)器與數(shù)據(jù)庫(kù)分離

          隨著網(wǎng)站的上線,訪問(wèn)量逐步上升,服務(wù)器的負(fù)載慢慢提高,在服務(wù)器還沒(méi)有超載的時(shí)候,我們應(yīng)該就要做好準(zhǔn)備,提升網(wǎng)站的負(fù)載能力。假如我們代碼層面已難以優(yōu)化,在不提高單臺(tái)機(jī)器的性能的情況下,采用增加機(jī)器是一個(gè)不錯(cuò)的方式,不僅可以有效地提高系統(tǒng)的負(fù)載能力,而且性價(jià)比高。

          增加的機(jī)器用來(lái)做什么呢?此時(shí)我們可以把數(shù)據(jù)庫(kù)服務(wù)器Web服務(wù)器拆分開(kāi)來(lái),這樣不僅提高了單臺(tái)機(jī)器的負(fù)載能力,也提高了容災(zāi)能力。

          應(yīng)用服務(wù)器與數(shù)據(jù)庫(kù)分開(kāi)后的架構(gòu)如下圖所示:

          階段三、應(yīng)用服務(wù)器集群

          隨著訪問(wèn)量繼續(xù)增加,單臺(tái)應(yīng)用服務(wù)器已經(jīng)無(wú)法滿足需求了。在假設(shè)數(shù)據(jù)庫(kù)服務(wù)器沒(méi)有壓力的情況下,我們可以把應(yīng)用服務(wù)器從一臺(tái)變成了兩臺(tái)甚至多臺(tái),把用戶的請(qǐng)求分散到不同的服務(wù)器中,從而提高負(fù)載能力。而多臺(tái)應(yīng)用服務(wù)器之間沒(méi)有直接的交互,他們都是依賴數(shù)據(jù)庫(kù)各自對(duì)外提供服務(wù)。著名的做故障切換的軟件有KeepAlived,KeepAlived是一個(gè)類似于Layer3、4、7交換機(jī)制的軟件,他不是某個(gè)具體軟件故障切換的專屬品,而是可以適用于各種軟件的一款產(chǎn)品。KeepAlived配合上ipvsadm又可以做負(fù)載均衡,可謂是神器。

          我們以增加了一臺(tái)應(yīng)用服務(wù)器為例,增加后的系統(tǒng)結(jié)構(gòu)圖如下:

          系統(tǒng)演變到這里,將會(huì)出現(xiàn)下面四個(gè)問(wèn)題

          1. 用戶的請(qǐng)求由誰(shuí)來(lái)轉(zhuǎn)發(fā)到到具體的應(yīng)用服務(wù)器?
          2. 有那些轉(zhuǎn)發(fā)的算法和策略可以使用?
          3. 應(yīng)用服務(wù)器如何返回用戶的請(qǐng)求?
          4. 用戶如果每次訪問(wèn)到的服務(wù)器不一樣,那么如何維護(hù)session的一致性?

          針對(duì)以上問(wèn)題,常用的解決方案如下:

          1、負(fù)載均衡的問(wèn)題

          一般以下有5種解決方案:

          1、HTTP重定向

          HTTP重定向就是應(yīng)用層的請(qǐng)求轉(zhuǎn)發(fā)。用戶的請(qǐng)求其實(shí)已經(jīng)到了HTTP重定向負(fù)載均衡服務(wù)器,服務(wù)器根據(jù)算法要求用戶重定向,用戶收到重定向請(qǐng)求后,再次請(qǐng)求真正的集群
          • 優(yōu)點(diǎn):簡(jiǎn)單易用;
          • 缺點(diǎn):性能較差。

          2、DNS域名解析負(fù)載均衡

          DNS域名解析負(fù)載均衡就是在用戶請(qǐng)求DNS服務(wù)器,獲取域名對(duì)應(yīng)的IP地址時(shí),DNS服務(wù)器直接給出負(fù)載均衡后的服務(wù)器IP。
          • 優(yōu)點(diǎn):交給DNS,不用我們?nèi)ゾS護(hù)負(fù)載均衡服務(wù)器;
          • 缺點(diǎn):當(dāng)一個(gè)應(yīng)用服務(wù)器掛了,不能及時(shí)通知DNS,而且DNS負(fù)載均衡的控制權(quán)在域名服務(wù)商那里,網(wǎng)站無(wú)法做更多的改善和更強(qiáng)大的管理。

          3、反向代理服務(wù)器

          在用戶的請(qǐng)求到達(dá)反向代理服務(wù)器時(shí)(已經(jīng)到達(dá)網(wǎng)站機(jī)房),由反向代理服務(wù)器根據(jù)算法轉(zhuǎn)發(fā)到具體的服務(wù)器。常用的Apache,Nginx都可以充當(dāng)反向代理服務(wù)器。
          • 優(yōu)點(diǎn):部署簡(jiǎn)單;
          • 缺點(diǎn):代理服務(wù)器可能成為性能的瓶頸,特別是一次上傳大文件。

          4、IP層負(fù)載均衡

          在請(qǐng)求到達(dá)負(fù)載均衡器后,負(fù)載均衡器通過(guò)修改請(qǐng)求的目的IP地址,從而實(shí)現(xiàn)請(qǐng)求的轉(zhuǎn)發(fā),做到負(fù)載均衡。
          • 優(yōu)點(diǎn):性能更好;
          • 缺點(diǎn):負(fù)載均衡器的寬帶成為瓶頸。

          5、數(shù)據(jù)鏈路層負(fù)載均衡

          在請(qǐng)求到達(dá)負(fù)載均衡器后,負(fù)載均衡器通過(guò)修改請(qǐng)求的MAC地址,從而做到負(fù)載均衡,與IP負(fù)載均衡不一樣的是,當(dāng)請(qǐng)求訪問(wèn)完服務(wù)器之后,直接返回客戶。而無(wú)需再經(jīng)過(guò)負(fù)載均衡器。

          2、集群調(diào)度轉(zhuǎn)發(fā)算法

          1、rr輪詢調(diào)度算法

          顧名思義,輪詢分發(fā)請(qǐng)求。
          • 優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單
          • 缺點(diǎn):不考慮每臺(tái)服務(wù)器的處理能力

          2、wrr加權(quán)調(diào)度算法

          我們給每個(gè)服務(wù)器設(shè)置權(quán)值Weight,負(fù)載均衡調(diào)度器根據(jù)權(quán)值調(diào)度服務(wù)器,服務(wù)器被調(diào)用的次數(shù)跟權(quán)值成正比。
          • 優(yōu)點(diǎn):考慮了服務(wù)器處理能力的不同

          3、sh原地址散列算法

          提取用戶IP,根據(jù)散列函數(shù)得出一個(gè)key,再根據(jù)靜態(tài)映射表,查處對(duì)應(yīng)的value,即目標(biāo)服務(wù)器IP。過(guò)目標(biāo)機(jī)器超負(fù)荷,則返回空。
          • 優(yōu)點(diǎn):實(shí)現(xiàn)同一個(gè)用戶訪問(wèn)同一個(gè)服務(wù)器。

          4、dh目標(biāo)地址散列算法

          原理同上,只是現(xiàn)在提取的是目標(biāo)地址的IP來(lái)做哈希。
          • 優(yōu)點(diǎn):實(shí)現(xiàn)同一個(gè)用戶訪問(wèn)同一個(gè)服務(wù)器。

          5、lc最少連接算法

          優(yōu)先把請(qǐng)求轉(zhuǎn)發(fā)給連接數(shù)少的服務(wù)器。
          • 優(yōu)點(diǎn):使得集群中各個(gè)服務(wù)器的負(fù)載更加均勻。

          6、wlc加權(quán)最少連接算法

          在lc的基礎(chǔ)上,為每臺(tái)服務(wù)器加上權(quán)值。算法為:(活動(dòng)連接數(shù) * 256 + 非活動(dòng)連接數(shù)) ÷ 權(quán)重,計(jì)算出來(lái)的值小的服務(wù)器優(yōu)先被選擇。
          • 優(yōu)點(diǎn):可以根據(jù)服務(wù)器的能力分配請(qǐng)求。

          7、sed最短期望延遲算法

          其實(shí)sed跟wlc類似,區(qū)別是不考慮非活動(dòng)連接數(shù)。算法為:(活動(dòng)連接數(shù) +1 ) * 256 ÷ 權(quán)重,同樣計(jì)算出來(lái)的值小的服務(wù)器優(yōu)先被選擇。

          8、nq永不排隊(duì)算法

          改進(jìn)的sed算法。我們想一下什么情況下才能“永不排隊(duì)”,那就是服務(wù)器的連接數(shù)為0的時(shí)候,那么假如有服務(wù)器連接數(shù)為0,均衡器直接把請(qǐng)求轉(zhuǎn)發(fā)給它,無(wú)需經(jīng)過(guò)sed的計(jì)算。

          9、LBLC基于局部性最少連接算法

          負(fù)載均衡器根據(jù)請(qǐng)求的目的IP地址,找出該IP地址最近被使用的服務(wù)器,把請(qǐng)求轉(zhuǎn)發(fā)之。若該服務(wù)器超載,最采用最少連接數(shù)算法。

          10、LBLCR帶復(fù)制的基于局部性最少連接算法

          負(fù)載均衡器根據(jù)請(qǐng)求的目的IP地址,找出該IP地址最近使用的“服務(wù)器組”,注意,并不是具體某個(gè)服務(wù)器,然后采用最少連接數(shù)從該組中挑出具體的某臺(tái)服務(wù)器出來(lái),把請(qǐng)求轉(zhuǎn)發(fā)之。若該服務(wù)器超載,那么根據(jù)最少連接數(shù)算法,在集群的非本服務(wù)器組的服務(wù)器中,找出一臺(tái)服務(wù)器出來(lái),加入本服務(wù)器組,然后把請(qǐng)求轉(zhuǎn)發(fā)。

          3、集群請(qǐng)求返回模式問(wèn)題

          1、NAT

          負(fù)載均衡器接收用戶的請(qǐng)求,轉(zhuǎn)發(fā)給具體服務(wù)器,服務(wù)器處理完請(qǐng)求返回給均衡器,均衡器再重新返回給用戶。

          2、DR

          負(fù)載均衡器接收用戶的請(qǐng)求,轉(zhuǎn)發(fā)給具體服務(wù)器,服務(wù)器出來(lái)玩請(qǐng)求后直接返回給用戶。需要系統(tǒng)支持IP Tunneling協(xié)議,難以跨平臺(tái)。

          3、TUN

          同上,但無(wú)需IP Tunneling協(xié)議,跨平臺(tái)性好,大部分系統(tǒng)都可以支持。

          4、集群Session一致性問(wèn)題

          1、Session Sticky

          Session sticky就是把同一個(gè)用戶在某一個(gè)會(huì)話中的請(qǐng)求,都分配到固定的某一臺(tái)服務(wù)器中,這樣我們就不需要解決跨服務(wù)器的session問(wèn)題了,常見(jiàn)的算法有ip_hash算法,即上面提到的兩種散列算法。
          • 優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單;
          • 缺點(diǎn):應(yīng)用服務(wù)器重啟則session消失。

          2、Session Replication

          Session replication就是在集群中復(fù)制session,使得每個(gè)服務(wù)器都保存有全部用戶的session數(shù)據(jù)。
          • 優(yōu)點(diǎn):減輕負(fù)載均衡服務(wù)器的壓力,不需要要實(shí)現(xiàn)ip_hasp算法來(lái)轉(zhuǎn)發(fā)請(qǐng)求;
          • 缺點(diǎn):復(fù)制時(shí)網(wǎng)絡(luò)帶寬開(kāi)銷大,訪問(wèn)量大的話Session占用內(nèi)存大且浪費(fèi)。

          3、Session數(shù)據(jù)集中存儲(chǔ)

          Session數(shù)據(jù)集中存儲(chǔ)就是利用數(shù)據(jù)庫(kù)來(lái)存儲(chǔ)session數(shù)據(jù),實(shí)現(xiàn)了session和應(yīng)用服務(wù)器的解耦。
          • 優(yōu)點(diǎn):相比Session replication的方案,集群間對(duì)于寬帶和內(nèi)存的壓力大幅減少;
          • 缺點(diǎn):需要維護(hù)存儲(chǔ)Session的數(shù)據(jù)庫(kù)。

          4、Cookie Base

          Cookie base就是把Session存在Cookie中,由瀏覽器來(lái)告訴應(yīng)用服務(wù)器我的session是什么,同樣實(shí)現(xiàn)了session和應(yīng)用服務(wù)器的解耦。
          • 優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,基本免維護(hù)。
          • 缺點(diǎn):cookie長(zhǎng)度限制,安全性低,帶寬消耗。

          值得一提的是:

          • Nginx目前支持的負(fù)載均衡算法有wrr、sh(支持一致性哈希)、fair(lc)。但Nginx作為均衡器的話,還可以一同作為靜態(tài)資源服務(wù)器
          • Keepalived + ipvsadm比較強(qiáng)大,目前支持的算法有:rr、wrr、lc、wlc、lblc、sh、dh
          • Keepalived支持集群模式有:NAT、DR、TUN
          • Nginx本身并沒(méi)有提供session同步的解決方案,而Apache則提供了session共享的支持。

          解決了以上的問(wèn)題之后,系統(tǒng)的結(jié)構(gòu)如下:

          階段四、數(shù)據(jù)庫(kù)讀寫(xiě)分離化

          上面我們總是假設(shè)數(shù)據(jù)庫(kù)負(fù)載正常,但隨著訪問(wèn)量的的提高,數(shù)據(jù)庫(kù)的負(fù)載也在慢慢增大。那么可能有人馬上就想到跟應(yīng)用服務(wù)器一樣,把數(shù)據(jù)庫(kù)一份為二再負(fù)載均衡即可。

          但對(duì)于數(shù)據(jù)庫(kù)來(lái)說(shuō),并沒(méi)有那么簡(jiǎn)單。假如我們簡(jiǎn)單的把數(shù)據(jù)庫(kù)一分為二,然后對(duì)于數(shù)據(jù)庫(kù)的請(qǐng)求,分別負(fù)載到A機(jī)器和B機(jī)器,那么顯而易見(jiàn)會(huì)造成兩臺(tái)數(shù)據(jù)庫(kù)數(shù)據(jù)不統(tǒng)一的問(wèn)題。那么對(duì)于這種情況,我們可以先考慮使用讀寫(xiě)分離主從復(fù)制的方式。

          讀寫(xiě)分離后的系統(tǒng)結(jié)構(gòu)如下:

          這個(gè)結(jié)構(gòu)變化后也會(huì)帶來(lái)兩個(gè)問(wèn)題:

          • 主從數(shù)據(jù)庫(kù)之間數(shù)據(jù)同步問(wèn)題。
          • 應(yīng)用對(duì)于數(shù)據(jù)源的選擇問(wèn)題。

          解決方案:

          • 使用MySQL自帶的Master + Slave的方式實(shí)現(xiàn)主從復(fù)制
          • 采用第三方數(shù)據(jù)庫(kù)中間件,例如MyCat。MyCat是從Cobar發(fā)展而來(lái)的,而Cobar是阿里開(kāi)源的數(shù)據(jù)庫(kù)中間件,后來(lái)停止開(kāi)發(fā)。MyCat是國(guó)內(nèi)比較好的MySql開(kāi)源數(shù)據(jù)庫(kù)分庫(kù)分表中間件。

          階段五、用搜索引擎緩解讀庫(kù)的壓力

          數(shù)據(jù)庫(kù)做讀庫(kù)的話,常常對(duì)模糊查找力不從心,即使做了讀寫(xiě)分離,這個(gè)問(wèn)題還未能解決。以我們所舉的交易網(wǎng)站為例,發(fā)布的商品存儲(chǔ)在數(shù)據(jù)庫(kù)中,用戶最常使用的功能就是查找商品,尤其是根據(jù)商品的標(biāo)題來(lái)查找對(duì)應(yīng)的商品。對(duì)于這種需求,一般我們都是通過(guò)like功能來(lái)實(shí)現(xiàn)的,但是這種方式的代價(jià)非常大,而且結(jié)果非常不準(zhǔn)確。此時(shí)我們可以使用搜索引擎倒排索引來(lái)完成。

          搜索引擎具有的優(yōu)點(diǎn):它能夠大大提高查詢速度和搜索準(zhǔn)確性。

          引入搜索引擎的開(kāi)銷

          • 帶來(lái)大量的維護(hù)工作,我們需要自己實(shí)現(xiàn)索引的構(gòu)建過(guò)程,設(shè)計(jì)全量/增加的構(gòu)建方式來(lái)應(yīng)對(duì)非實(shí)時(shí)與實(shí)時(shí)的查詢需求。
          • 需要維護(hù)搜索引擎集群
          搜索引擎并不能替代數(shù)據(jù)庫(kù),它解決了某些場(chǎng)景下的精準(zhǔn)、快速、高效的“讀”操作,是否引入搜索引擎,需要綜合考慮整個(gè)系統(tǒng)的需求。

          引入搜索引擎后的系統(tǒng)結(jié)構(gòu)如下:

          階段六、用緩存緩解讀庫(kù)的壓力

          常用的緩存機(jī)制包括頁(yè)面級(jí)緩存、應(yīng)用數(shù)據(jù)緩存和數(shù)據(jù)庫(kù)緩存。

          應(yīng)用層和數(shù)據(jù)庫(kù)層的緩存

          隨著訪問(wèn)量的增加,逐漸出現(xiàn)了許多用戶訪問(wèn)同一部分熱門(mén)內(nèi)容的情況,對(duì)于這些比較熱門(mén)的內(nèi)容,沒(méi)必要每次都從數(shù)據(jù)庫(kù)讀取。我們可以使用緩存技術(shù),例如可以使用Google的開(kāi)源緩存技術(shù)Guava或者使用Memecahed作為應(yīng)用層的緩存,也可以使用Redis作為數(shù)據(jù)庫(kù)層的緩存。

          另外,在某些場(chǎng)景下,關(guān)系型數(shù)據(jù)庫(kù)并不是很適合,例如我想做一個(gè)“每日輸入密碼錯(cuò)誤次數(shù)限制”的功能,思路大概是在用戶登錄時(shí),如果登錄錯(cuò)誤,則記錄下該用戶的IP和錯(cuò)誤次數(shù),那么這個(gè)數(shù)據(jù)要放在哪里呢?假如放在內(nèi)存中,那么顯然會(huì)占用太大的內(nèi)容;假如放在關(guān)系型數(shù)據(jù)庫(kù)中,那么既要建立數(shù)據(jù)庫(kù)表,還要簡(jiǎn)歷對(duì)應(yīng)的Java bean,還要寫(xiě)SQL等等。而分析一下我們要存儲(chǔ)的數(shù)據(jù),無(wú)非就是類似{ip:errorNumber}這樣的key:value數(shù)據(jù)。對(duì)于這種數(shù)據(jù),我們可以用NOSQL數(shù)據(jù)庫(kù)來(lái)代替?zhèn)鹘y(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)。

          頁(yè)面緩存

          除了數(shù)據(jù)緩存,還有頁(yè)面緩存。比如使用HTML5的localstroage或者Cookie。除了頁(yè)面緩存帶來(lái)的性能提升外,對(duì)于并發(fā)訪問(wèn)且頁(yè)面置換頻率小的頁(yè)面,應(yīng)盡量使用頁(yè)面靜態(tài)化技術(shù)。

          • 優(yōu)點(diǎn):減輕數(shù)據(jù)庫(kù)的壓力, 大幅度提高訪問(wèn)速度;
          • 缺點(diǎn):需要維護(hù)緩存服務(wù)器,提高了編碼的復(fù)雜性。

          值得一提的是:

          緩存集群的調(diào)度算法不同與上面提到的應(yīng)用服務(wù)器和數(shù)據(jù)庫(kù)。最好采用一致性哈希算,這樣才能提高命中率

          加入緩存后的系統(tǒng)結(jié)構(gòu)如下:

          階段七、數(shù)據(jù)庫(kù)水平拆分與垂直拆分

          我們的網(wǎng)站演進(jìn)到現(xiàn)在,交易、商品、用戶的數(shù)據(jù)都還在同一個(gè)數(shù)據(jù)庫(kù)中。盡管采取了增加緩存讀寫(xiě)分離的方式,但隨著數(shù)據(jù)庫(kù)的壓力繼續(xù)增加,數(shù)據(jù)庫(kù)數(shù)據(jù)量的瓶頸越來(lái)越突出,此時(shí),我們可以有數(shù)據(jù)垂直拆分水平拆分兩種選擇。

          數(shù)據(jù)垂直拆分

          垂直拆分的意思是把數(shù)據(jù)庫(kù)中不同的業(yè)務(wù)數(shù)據(jù)拆分到不同的數(shù)據(jù)庫(kù)中,結(jié)合現(xiàn)在的例子,就是把交易、商品、用戶的數(shù)據(jù)分開(kāi)。

          優(yōu)點(diǎn):

          • 解決了原來(lái)把所有業(yè)務(wù)放在一個(gè)數(shù)據(jù)庫(kù)中的壓力問(wèn)題;
          • 可以根據(jù)業(yè)務(wù)的特點(diǎn)進(jìn)行更多的優(yōu)化。

          缺點(diǎn):

          • 需要維護(hù)多個(gè)數(shù)據(jù)庫(kù)的狀態(tài)一致性和數(shù)據(jù)同步。

          問(wèn)題:

          • 需要考慮原來(lái)跨業(yè)務(wù)的事務(wù);
          • 跨數(shù)據(jù)庫(kù)的Join。

          解決問(wèn)題方案:

          • 應(yīng)該在應(yīng)用層盡量避免跨數(shù)據(jù)庫(kù)的分布式事務(wù),如果非要跨數(shù)據(jù)庫(kù),盡量在代碼中控制。
          • 通過(guò)第三方中間件來(lái)解決,如上面提到的MyCat,MyCat提供了豐富的跨庫(kù)Join方案,詳情可參考MyCat官方文檔。

          數(shù)據(jù)垂直拆分后的結(jié)構(gòu)如下:

          數(shù)據(jù)水平拆分

          數(shù)據(jù)水平拆分就是把同一個(gè)表中的數(shù)據(jù)拆分到兩個(gè)甚至多個(gè)數(shù)據(jù)庫(kù)中。產(chǎn)生數(shù)據(jù)水平拆分的原因是某個(gè)業(yè)務(wù)的數(shù)據(jù)量或者更新量到達(dá)了單個(gè)數(shù)據(jù)庫(kù)的瓶頸,這時(shí)就可以把這個(gè)表拆分到兩個(gè)或更多個(gè)數(shù)據(jù)庫(kù)中。

          優(yōu)點(diǎn):

          • 如果能克服以上問(wèn)題,那么我們將能夠很好地對(duì)數(shù)據(jù)量及寫(xiě)入量增長(zhǎng)的情況。

          問(wèn)題:

          • 訪問(wèn)用戶信息的應(yīng)用系統(tǒng)需要解決SQL路由的問(wèn)題,因?yàn)楝F(xiàn)在用戶信息分在了兩個(gè)數(shù)據(jù)庫(kù)中,需要在進(jìn)行數(shù)據(jù)操作時(shí)了解需要操作的數(shù)據(jù)在哪里。
          • 主鍵 的處理也變得不同,例如原來(lái)自增字段,現(xiàn)在不能簡(jiǎn)單地繼續(xù)使用。
          • 如果需要分頁(yè)查詢,那就更加麻煩。

          解決問(wèn)題方案:

          • 我們還是可以通過(guò)可以解決第三方中間件,如MyCat。MyCat可以通過(guò)SQL解析模塊對(duì)我們的SQL進(jìn)行解析,再根據(jù)我們的配置,把請(qǐng)求轉(zhuǎn)發(fā)到具體的某個(gè)數(shù)據(jù)庫(kù)。
          • 我們可以通過(guò)UUID保證唯一或自定義ID方案來(lái)解決。
          • MyCat也提供了豐富的分頁(yè)查詢方案,比如先從每個(gè)數(shù)據(jù)庫(kù)做分頁(yè)查詢,再合并數(shù)據(jù)做一次分頁(yè)查詢等等。

          數(shù)據(jù)水平拆分后的結(jié)構(gòu)如下:

          階段八、應(yīng)用的拆分

          按微服務(wù)拆分應(yīng)用

          隨著業(yè)務(wù)的發(fā)展,業(yè)務(wù)越來(lái)越多,應(yīng)用越來(lái)越大。我們需要考慮如何避免讓?xiě)?yīng)用越來(lái)越臃腫。這就需要把應(yīng)用拆開(kāi),從一個(gè)應(yīng)用變?yōu)閭z個(gè)甚至更多。還是以我們上面的例子,我們可以把用戶、商品、交易拆分開(kāi)。變成“用戶、商品”和“用戶,交易”兩個(gè)子系統(tǒng)。

          拆分后的結(jié)構(gòu):

          問(wèn)題:

          這樣拆分后,可能會(huì)有一些相同的代碼,如用戶相關(guān)的代碼,商品和交易都需要用戶信息,所以在兩個(gè)系統(tǒng)中都保留差不多的操作用戶信息的代碼。如何保證這些代碼可以復(fù)用是一個(gè)需要解決的問(wèn)題。

          解決問(wèn)題:

          通過(guò)走服務(wù)化SOA的路線來(lái)解決頻繁公共的服務(wù)。

          走SOA服務(wù)化治理道路

          為了解決上面拆分應(yīng)用后所出現(xiàn)的問(wèn)題,我們把公共的服務(wù)拆分出來(lái),形成一種服務(wù)化的模式,簡(jiǎn)稱SOA。

          采用服務(wù)化之后的系統(tǒng)結(jié)構(gòu):

          優(yōu)點(diǎn):

          • 相同的代碼不會(huì)散落在不同的應(yīng)用中了,這些實(shí)現(xiàn)放在了各個(gè)服務(wù)中心,使代碼得到更好的維護(hù)。
          • 我們把對(duì)數(shù)據(jù)庫(kù)的交互業(yè)務(wù)放在了各個(gè)服務(wù)中心,讓前端的Web應(yīng)用更注重與瀏覽器交互的工作。

          問(wèn)題:

          如何進(jìn)行遠(yuǎn)程的服務(wù)調(diào)用?

          解決方法:

          可以通過(guò)下面的引入消息中間件來(lái)解決。

          階段九、引入消息中間件

          隨著網(wǎng)站的繼續(xù)發(fā)展,的系統(tǒng)中可能出現(xiàn)不同語(yǔ)言開(kāi)發(fā)的子模塊和部署在不同平臺(tái)的子系統(tǒng)。此時(shí)我們需要一個(gè)平臺(tái)來(lái)傳遞可靠的,與平臺(tái)和語(yǔ)言無(wú)關(guān)的數(shù)據(jù),并且能夠把負(fù)載均衡透明化,能在調(diào)用過(guò)程中收集分析調(diào)用數(shù)據(jù),推測(cè)出網(wǎng)站的訪問(wèn)增長(zhǎng)率等等一系列需求,對(duì)于網(wǎng)站應(yīng)該如何成長(zhǎng)做出預(yù)測(cè)。開(kāi)源消息中間件有阿里的Dubbo,可以搭配Google開(kāi)源的分布式程序協(xié)調(diào)服務(wù)Zookeeper實(shí)現(xiàn)服務(wù)器的注冊(cè)發(fā)現(xiàn)。

          引入消息中間件后的結(jié)構(gòu):

          總結(jié)

          以上的演變過(guò)程只是一個(gè)例子,并不適合所有的網(wǎng)站,實(shí)際中網(wǎng)站演進(jìn)過(guò)程與自身業(yè)務(wù)和不同遇到的問(wèn)題有密切的關(guān)系,沒(méi)有固定的模式。只有認(rèn)真的分析和不斷地探究,才能發(fā)現(xiàn)適合自己網(wǎng)站的架構(gòu)。

          上節(jié)

          繪制文本:
          可以在Canvas畫(huà)布中進(jìn)行文本的繪制,同時(shí)也可以指定繪制文本的字體、大小、對(duì)齊方式等,還可以進(jìn)行文字的紋理填充等;

          繪制文本涉及兩個(gè)方法,分別為:
          fillText(text,x,y,[maxwidth])方法:用填充方式繪制字符串;
          strokeText(text,x,y,[maxwidth])方法:用輪廓方式繪制字符串;
          這兩個(gè)方法都接收4個(gè)參數(shù):要繪制的文本字符串、x和y坐標(biāo)、以及一個(gè)可選的maxwidth參數(shù),表示顯示文字時(shí)最大的寬度,可以防止文字溢出;

          fillText()方法使用fillStyle屬性繪制文本,而strokeText()以strokeStyle屬性為文本描邊;如:

          context.fillText('零點(diǎn)程序員', 0, 50);
          context.strokeText('零點(diǎn)程序員', 0, 100);
          context.fillText('零點(diǎn)程序員', 0, 150, 30);
          context.fillStyle="#00f";
          context.fillText('零點(diǎn)程序員', 0, 200);
          context.strokeStyle="#f00";
          context.strokeText('零點(diǎn)程序員', 0, 250);
          context.strokeText('零點(diǎn)程序員', 0, 250, 30);

          示例:繪制包含數(shù)據(jù)說(shuō)明的柱狀圖

          context.fillStyle="white";
          context.fillRect(0,0,canvas.width,canvas.height);
          var data=[100, 50, 20, 30, 100];
          var colors=[ "red","orange", "yellow","green", "blue"];
          for(var i=0; i<data.length; i++){
          var dt=data[i];
          context.fillStyle=colors[i];
          context.fillRect(25+i*50, 280-dt*2, 50, dt*2);
          }
          context.fillStyle="black";
          context.lineWidth=2;
          context.beginPath();
          context.moveTo(25,10);
          context.lineTo(25,280);
          context.lineTo(290,280);
          context.stroke();
          for(var i=0; i<6; i++){
          context.fillText((5-i)*20 + "", 4, i*40+80);
          context.beginPath();
          context.moveTo(25, i*40+80);
          context.lineTo(30, i*40+80);
          context.stroke();
          }
          var labels=["JAN","FEB","MAR","APR","MAY"];
          for(var i=0; i<5; i++)
          context.fillText(labels[i], 40+ i*50, 300);

          在進(jìn)行文字繪制之前,可以先對(duì)該對(duì)象的有關(guān)文字繪制的屬性進(jìn)行設(shè)置,如:

          • font屬性:設(shè)置字體、大小和樣式,與CSS中font屬性一致;
          • textAlign:設(shè)置水平對(duì)齊方式,值有:start、end、left、right、center;建議使用start和end,不要使用left和right,因?yàn)閟tart和end更加的穩(wěn)妥,能同時(shí)適合從左到右和從右到左顯示(閱讀)的語(yǔ)言;
          • textBaesline:設(shè)置文本基線,值有:top、hanging、middle、alphabetic、ideographic(表意的)、bottom,默認(rèn)值是alphabetic;

          這三個(gè)屬性都有默認(rèn)值,因此也不是必須顯式去設(shè)置它們;

          // 使用字體樣式
          context.font='italic bold 30px sans-serif';
          context.textAlign="end";
          context.textBaseline='middle';
          context.fillText('零點(diǎn)網(wǎng)絡(luò)', 150 , 300);
          context.font="bold 14px Arial";
          context.textAlign="center";
          context.textBaseline="middle";
          context.fillText("12", 100, 20);

          再議textAlign屬性,當(dāng)值為start時(shí),則x坐標(biāo)表示的是文本左端的位置,如果設(shè)置為end,則x坐標(biāo)表示的是文本右端的位置,如果設(shè)置為center,則x坐標(biāo)表示的是文本的中間的位置,如:

          // 默認(rèn)為start
          context.fillText('零點(diǎn)程序員', 100 , 200);
          context.textAlign="start"; // 起點(diǎn)對(duì)齊
          context.fillText('零點(diǎn)程序員', 100 , 220);
          context.textAlign="center"; // 中間對(duì)齊
          context.fillText('零點(diǎn)程序員', 100 , 240);
          context.textAlign="end"; // 終點(diǎn)對(duì)齊
          context.fillText('零點(diǎn)程序員', 100 , 260);

          獲取文字寬度:measureText(text)該方法使用要繪制的文本作為參數(shù),返回一個(gè)TextMetrics對(duì)象,該對(duì)象有個(gè)最重要的width屬性,表示使用當(dāng)前指定的字體后,text參數(shù)中指定的文字的總文字寬度,如:

          var txt="零點(diǎn)程序員";
          var tm=context.measureText(txt);
          console.log(tm); // TextMetrics
          console.log(tm.width); // 50
          measureText()方法利用font、textAlign和textBaseline的當(dāng)前值計(jì)算指定文本的大小,如:
          var txt="零點(diǎn)程序員";
          context.fillText(txt, 10, 50);
          var tm=context.measureText(txt);
          console.log(tm); // TextMetrics
          console.log(tm.width); // 50
          context.fillStyle="#00f";
          context.font="italic 20px san-serif";
          var tm=context.measureText(txt);
          console.log(tm.width); // 100
          context.fillText(txt, 10, 100);
          context.fillText(tm.width , tm.width + 10, 100);
          context.font="bold 32px san-serif";
          var tm2=context.measureText(txt);
          console.log(tm2.width); // 160
          context.fillStyle="purple";
          context.fillText(txt, 10, 150);
          context.fillText(tm2.width, tm2.width + 10, 150);

          如:使用適當(dāng)?shù)拇笮±L制文本

          var txt="零點(diǎn)程序員";
          var fontSize=100;
          context.font=fontSize + "px Arial";
          while(context.measureText(txt).width > 140){
          fontSize--;
          context.font=fontSize + "px Arial";
          }
          context.fillText(txt, 50, 50);
          context.fillText("字體大小是:" + fontSize + "px", 50, 100);

          direction屬性:用來(lái)在繪制文本時(shí),描述當(dāng)前文本方向的屬性,可能的值:

          • ltr:文本方向從左向右;
          • rtl:文本方向從右向左;
          • inherit:繼承,默認(rèn)值;
          context.font='48px serif';
          context.fillText('zero!', 200, 50);
          // context.direction='rtl';
          context.fillText('zero!', 200, 130);

          filter屬性:濾鏡,提供模糊、灰度等過(guò)濾效果的屬性,類似于CSS filter屬性,并且接受相同的函數(shù);

          context.filter="blur(5px)";
          context.font="48px serif";
          context.strokeText("大師哥王唯", 100, 100);

          Chrome還定義了多個(gè)有關(guān)字體的屬性:

          • fontKerning屬性:對(duì)應(yīng)CSS的font-kerning,設(shè)置是否使用字體中儲(chǔ)存的字距信息;
          • fontStretch屬性:對(duì)應(yīng)CSS的font-stretch,為字體定義一個(gè)正?;蚪?jīng)過(guò)伸縮變形的字體外觀,這個(gè)屬性并不會(huì)通過(guò)伸展/縮小而改變字體的幾何外形;
          • fontVariantCaps屬性:對(duì)應(yīng)CSS的font-variant-caps,使用不同的大寫(xiě)形式;
          • letterSpacing屬性:對(duì)應(yīng)CSS的letter-spacing,設(shè)置字間距;
          • wordSpacing屬性:對(duì)應(yīng)CSS的word-spacing,設(shè)置詞間距;
          • textRendering屬性:對(duì)應(yīng)CSS的text-rendering,如何渲染字體;

          示例:繪制帶有文字的餅形圖

          function PieChart (context){
          this.context=context || document.getElementById("canvas").getContext("2d");
          this.x=this.context.canvas.width/2 - 30;
          this.y=this.context.canvas.height/2;
          this.r=120;
          this.outLine=20;
          this.dataList=null;
          }
          PieChart.prototype={
          constructor:PieChart,
          init:function(dataList){
          this.dataList=dataList || [{title:"默認(rèn)",value:100}];
          
          this.transformAngle();
          this.drawPie();
          },
          drawPie:function(){
          var startAngle=0,endAngle;
          for(var i=0 ; i < this.dataList.length ; i++){
          var item=this.dataList[i];
          endAngle=startAngle + item.angle;
          this.context.beginPath();
          this.context.moveTo(this.x,this.y);
          this.context.arc(this.x,this.y,this.r,startAngle,endAngle,false);
          var color=this.context.strokeStyle=this.context.fillStyle=this.getRandomColor();
          this.context.stroke();
          this.context.fill();
          this.drawPieTitle(startAngle,item.angle,color,item.title)
          this.drawPieLegend(i,item.title);
          startAngle=endAngle;
          }
          },
          drawPieTitle:function(startAngle,angle,color,title){
          var edge=this.r + this.outLine;
          var edgeX=Math.cos(startAngle + angle / 2) * edge;
          var edgeY=Math.sin(startAngle + angle / 2) * edge;
          var outX=this.x + edgeX;
          var outY=this.y + edgeY;
          this.context.beginPath();
          this.context.moveTo(this.x,this.y);
          this.context.lineTo(outX,outY);
          this.context.strokeStyle=color;
          this.context.stroke();
          var textWidth=this.context.measureText(title).width + 5;
          var lineX=outX > this.x ? outX + textWidth : outX - textWidth;
          this.context.lineTo(lineX,outY);
          this.context.stroke();
          this.context.font="15px KaiTi";
          this.context.textAlign=outX > this.x ? "left" : "right";
          this.context.textBaseline="bottom";
          this.context.fillText(title,outX,outY);
          },
          drawPieLegend:function(index,title){
          var space=10;
          var rectW=40;
          var rectH=20;
          var rectX=this.x + this.r + 80;
          var rectY=this.y + (index * 30);
          this.context.fillRect(rectX,rectY,rectW,rectH);
          // this.context.beginPath();
          this.context.textAlign='left';
          this.context.textBaseline='top';
          this.context.fillStyle="#000";
          this.context.fillText(title,rectX + rectW + space,rectY);
          },
          getRandomColor:function(){
          var r=Math.floor(Math.random() * 256);
          var g=Math.floor(Math.random() * 256);
          var b=Math.floor(Math.random() * 256);
          return 'rgb('+r+','+g+','+b+')';
          },
          transformAngle:function(){
          var self=this;
          var total=0;
          this.dataList.forEach(function(item,i){
          total +=item.value;
          })
          this.dataList.forEach(function(item,i){
          self.dataList[i].angle=2 * Math.PI * item.value/total;
          })
          },
          }
          var data=[{value:20,title:"UI"},{value:26,title:"java"},
          {value:20,title:"iOS"},{value:63,title:"H5"},{value:25,title:"Node"}]
          var pie=new PieChart().init(data);

          裁切路徑:
          在繪制圖形的時(shí)候,如果只保留圖形的一部分,可以使用裁切路徑;
          使用clip()方法,可以將當(dāng)前創(chuàng)建的路徑設(shè)置為當(dāng)前剪切路徑;
          使用原理是:首先在畫(huà)布內(nèi)使用路徑,只繪制該路徑所包括區(qū)域內(nèi)的圖像;再使用clip()方法,該方法創(chuàng)建一個(gè)裁切路徑,使用該路徑對(duì)canvas畫(huà)布設(shè)置一個(gè)裁剪區(qū)域;如:

          context.arc(100, 100, 75, 0, Math.PI*2, false);
          context.clip();
          context.fillRect(0, 0, 100,100);

          默認(rèn)情況下,canvas 有一個(gè)與它自身一樣大的裁切路徑;

          示例:

          context.font="bold 60pt sans-serif";
          context.lineWidth=2;
          context.strokeStyle="#F00";
          context.strokeText("零點(diǎn)程序員", 15, 330);
          context.strokeRect(175,25,50,350);
          context.beginPath();
          context.moveTo(200, 50);
          context.lineTo(350, 350);
          context.lineTo(50, 350);
          context.closePath();
          context.clip();
          context.lineWidth=10;
          context.stroke();
          context.fillStyle="#aaa";
          context.fillRect(175,25,50,350);
          context.fillStyle="#888";
          context.fillText("零點(diǎn)程序員", 15, 330);

          示例:繪制一個(gè)五角形裁切路徑

          var image=new Image();
          image.src="images/1.png";
          image.onload=function(){
          createStar(context);
          context.drawImage(image,-50,-150,300,300);
          }
          function createStar(context){
          var dx=100, dy=0, s=150, dig=Math.PI / 5 * 4;
          context.beginPath();
          context.translate(100, 150);
          for(var i=0; i<5; i++){
          var x=Math.sin(i*dig);
          var y=Math.cos(i*dig);
          context.lineTo(dx+x*s, dy+y*s);
          }
          context.clip();
          }

          裁剪區(qū)域一旦設(shè)置好后,后續(xù)繪制的所有圖形都使用這個(gè)裁切區(qū)域;如果要取消這個(gè)已經(jīng)設(shè)置好的裁剪區(qū)域,由于沒(méi)有重置裁切路徑的方法,所以,需要使用繪制狀態(tài)的保存與恢復(fù)功能;即通過(guò)save()和restore(),對(duì)之后繪制的圖像取消裁剪區(qū)域;

          示例:探照燈效果

          <canvas id="myCanvas" width="400" height="400" style="border:3px double #996633;"></canvas>
          <script>
          var rot=10;
          var canvas=document.getElementById('myCanvas');
          var context=canvas.getContext('2d');
          setInterval("draw()",100);
          function draw(){
          context.clearRect(0,0,400,400);
          context.save();
          context.fillStyle="black";
          context.fillRect(0,0,400,400);
          context.beginPath();
          context.arc(rot, 200, 40, 0, Math.PI*2, true);
          context.closePath();
          context.fillStyle="white";
          context.fill();
          context.clip();
          context.font="bold 45px 隸書(shū)";
          context.textAlign="center";
          context.textBaseline="middle";
          context.fillStyle="#FF0000";
          context.fillText("零點(diǎn)程序員大師哥",200,200);
          context.restore();
          rot=rot+10;
          if (rot>400) rot=10;
          }
          </script>

          示例:隨機(jī)星星

          context.fillRect(0,0,150,150);
          context.translate(75,75);
          context.beginPath();
          context.arc(0,0,60,0,Math.PI*2,true);
          context.clip();
          var lingrad=context.createLinearGradient(0,-75,0,75);
          lingrad.addColorStop(0, '#232256');
          lingrad.addColorStop(1, '#143778');
          context.fillStyle=lingrad;
          context.fillRect(-75,-75,150,150);
          for (var j=1; j<50; j++){
          context.save();
          context.fillStyle='#fff';
          context.translate(75-Math.floor(Math.random()*150),
          75-Math.floor(Math.random()*150));
          drawStar(context, Math.floor(Math.random()*4)+2);
          context.restore();
          }
          function drawStar(context, r){
          context.save();
          context.beginPath()
          context.moveTo(r, 0);
          for (var i=0; i<9; i++){
          context.rotate(Math.PI/5); // 36度
          if(i%2==0) {
          context.lineTo((r/0.525731)*0.200811, 0);
          } else {
          context.lineTo(r, 0);
          }
          }
          context.closePath();
          context.fill();
          context.restore();
          }

          繪制圖像:
          2D上下文內(nèi)置了對(duì)圖像(位圖)的支持,可以讀取本地以及網(wǎng)絡(luò)中的圖片,再將該圖像的像素內(nèi)容繪制(復(fù)制)在畫(huà)布中;必要的時(shí)候,還可以對(duì)圖片進(jìn)行縮放和旋轉(zhuǎn);

          使用drawImage()方法即可繪制圖像;
          drawImage(image,x,y):繪制圖像,參數(shù)image為一個(gè)將要被繪制到畫(huà)布上的源圖片,x和y指定了待繪制圖片的左上角的坐標(biāo);繪制的圖像和原圖大小相同;
          繪制圖像時(shí),首先需要一個(gè)Image對(duì)象或一個(gè)<img>元素,如:

          var image=document.images[0];
          context.drawImage(image, 50, 50);

          如果使用Image(),設(shè)定好image的src屬性后,并不一定立刻就能把圖像繪制完畢,如有時(shí)該圖像來(lái)自網(wǎng)絡(luò)的比較大的圖像文件,就需要完全下載后才能繪制,所以需要在image的onload事件進(jìn)行處理,此時(shí)就可以一邊裝載一邊繪制了,如:

          image=new Image();
          image.src="images/1.jpg";
          image.onload=function(){
          drawImg(context,image);
          }
          function drawImg(context,image){
          for(var i=0;i<7;i++){
          context.drawImage(image,i*50,i*25,100,100);
          }
          }

          示例:一個(gè)簡(jiǎn)單的線圖

          var img=new Image();
          img.onload=function(){
          context.drawImage(img,0,0);
          context.beginPath();
          context.moveTo(30,96);
          context.lineTo(70,66);
          context.lineTo(103,76);
          context.lineTo(170,15);
          context.stroke();
          }
          img.src='images/backdrop.png';

          drawImage(image, x, y, w, h):使用w和h設(shè)置繪制的圖像的大小,可以用來(lái)圖像縮放;會(huì)繪制整個(gè)源圖像;如:

          context.drawImage(image, 50, 50, 200, 160);

          按比例指定大小,如:

          var image=document.images[0];
          var w=image.width,
          h=image.height;
          var ratio=w / h;
          var nw=200,
          nh=nw / ratio;
          context.drawImage(image, 50, 50, nw, nh);

          示例:平鋪圖像:

          var img=new Image();
          img.onload=function(){
          for (var i=0; i<4; i++){
          for (var j=0; j<3; j++){
          context.drawImage(img, j*50, i*38, 50, 38);
          }
          }
          };
          img.src='images/1.jpg';

          drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)方法:可以將畫(huà)布中已繪制好的圖像的全部或者局部區(qū)域復(fù)制到畫(huà)布中的另一個(gè)位置上;sx和sy表示源圖像的被復(fù)制區(qū)域的坐標(biāo);sw和sh表示源圖像的被復(fù)制區(qū)域的寬和高;dx和dy表示復(fù)制后的目標(biāo)圖像的坐標(biāo);dw和dh表示復(fù)制后的目標(biāo)圖像的寬和高;
          該方法可以只復(fù)制圖像的局部,只要將sx和sy設(shè)為局部區(qū)域的起始坐標(biāo),將sw與sh設(shè)為局部區(qū)域的寬和高即可;該方法也可以用來(lái)縮放源圖像,只要將dw和dh設(shè)為縮放后的寬和高即可;

          如:局部放大復(fù)制到另外一個(gè)位置:

          context.drawImage(image, 100, 100, 150, 100, 10, 10, 100, 50);

          示例:相框:

          context.drawImage(document.getElementById('source'),
          33,71,104,124,21,20,87,104); // 圖片
          context.drawImage(document.getElementById('frame'),0,0);

          示例:畫(huà)廊

          window.onload=function(){
          document.body.style.backgroundColor="#4f191A";
          var canvas, context;
          var frameImg=new Image();
          frameImg.src="images/picture_frame.png";
          frameImg.onload=function(){
          var imgsArr=["images/1.jpg","images/2.jpg","images/3.jpg","images/4.jpg","images/5.jpg","images/6.jpg","images/7.jpg","images/8.jpg",]
          for (i=0; i<imgsArr.length; i++){
          (function(i){
          var img=new Image();
          img.src=imgsArr[i];
          img.onload=function(){
          canvas=document.createElement('canvas');
          canvas.setAttribute('width',132);
          canvas.setAttribute('height',150);
          
          document.body.appendChild(canvas);
          
          context=canvas.getContext('2d');
          context.drawImage(img,15,20,102,110);
          context.drawImage(frameImg,0,0);
          }
          })(i);
          }
          }
          }

          除了使用image作為源圖像之外,也可以傳入一個(gè)<canvas>元素,這樣,就可以把另一個(gè)畫(huà)布內(nèi)容繪制到當(dāng)前畫(huà)布上;除此之外,還可以是一個(gè)Video元素、ImageBitmap對(duì)象或使用dataURL嵌入的圖像;

          如,使用視頻幀:

          <canvas id="canvas" width="600" height="400"></canvas>
          <video width="600" height="400" src="media/video.mp4" controls></video>
          <script>
          var video=document.querySelector("video");
          var canvas=document.getElementById("canvas");
          var interId;
          var context=canvas.getContext("2d");
          video.onplay=function(){
          interId=setInterval(function(){
          context.clearRect(0,0,600,400);
          context.fillRect(0,0,600,400);
          context.drawImage(video, 0, 70, 600, 440);
          context.font="20px 微軟雅黑";
          context.strokeStyle="#999";
          context.strokeText("零點(diǎn)程序員", 50, 50);
          },16);
          }
          video.onpause=function(){
          clearInterval(interId);
          }
          </script>

          圖像平鋪:
          所謂的平鋪就是按一定比例縮小后的圖像填滿畫(huà)布,有兩種方法,一是使用drawImage方法:

          var image=new Image();
          image.src="images/1.jpg";
          image.onload=function(){
          drawImage(canvas,context,image);
          }
          function drawImage(canvas,context,image){
          var scale=20;
          var w=image.width / scale; var h=image.height / scale;
          var numX=canvas.width / w; var numY=canvas.height / h;
          for(var i=0;i<numX;i++){
          for(var j=0;j<numY;j++){
          context.drawImage(image,i*w,j*h,w,h);
          }
          }
          }

          第二種方法:可以使用context的createPattern(image, type)方法,參數(shù)type指定了重復(fù)的類型,其可能的值:no-repeat、repeat-x、repeat-y、repeat;創(chuàng)建完createPattern對(duì)象后,再賦給fillStyle即可;

          var image=new Image();
          image.src="images/1.jpg";
          image.onload=function(){
          var pattern=context.createPattern(image,"repeat");
          context.fillStyle=pattern;
          context.fillRect(0,0,400,300);
          }

          圖形、圖像的混合與組合(composite):

          混合(合成)圖像:
          所謂圖像混合(合成),是指使用某種數(shù)學(xué)公式將兩幅圖像混合在一起;從這個(gè)角度說(shuō),圖像混合有些類似于對(duì)一幅圖像使用一定的透明度后將其放置在另一幅圖像上,但是事實(shí)上圖像混合技術(shù)能夠?qū)崿F(xiàn)比透明度更好的混合效果;

          在混合圖像時(shí),將疊放在一起的兩幅圖像進(jìn)行逐像素顏色比較,放置于底層的圖像像素顏色稱為基色,放置于上層的圖像像素顏色稱為混合色,將這兩種像素顏色按一定的計(jì)算公式計(jì)算后得到的像素顏色稱為結(jié)果色,最后對(duì)合成后的圖像像素應(yīng)用結(jié)果色;

          為了使用混合技術(shù),需要使用context的屬性:globalCompositeOperation;

          globalCompositeOperation屬性:
          表示后繪制的圖形怎樣與先繪制的圖形結(jié)合,其可能的值如下:

          • normal:默認(rèn)值,不混合;
          • darken:變暗模式,保留兩個(gè)圖層中最暗的像素;
          • lighten:變亮模式,保留兩個(gè)圖層中最亮的像素;
          • multiply:正片疊底,像素被倒轉(zhuǎn),相乘,再倒轉(zhuǎn),結(jié)果是一幅更明亮的圖片;
          • screen:濾色模式,像素被倒轉(zhuǎn),相乘,再倒轉(zhuǎn),結(jié)果是一幅更明亮的圖片;
          • color-burn:顏色加深模式,將反置的底層除以頂層,然后將結(jié)果反過(guò)來(lái);
          • color-dodge:顏色減淡模式,將底層除以頂層的反置;
          • hard-light:強(qiáng)光模式,屏幕相乘,類似于疊加,但上下圖層互換了;
          • soft-light:柔光模式,用頂層減去底層或者相反來(lái)得到一個(gè)正值;
          • overlay:疊加模式,multiply 和 screen 的結(jié)合,原本暗的地方更暗,原本亮的地方更亮;
          • difference:差值模式,一個(gè)柔和版本的強(qiáng)光(hard-light);純黑或純白不會(huì)導(dǎo)致純黑或純白;
          • exclusion:排除模式,和 difference 相似,但對(duì)比度較低;
          • hue:保留了底層的亮度(luma)和色度(chroma),同時(shí)采用了頂層的色調(diào)(hue);
          • saturation:保留底層的亮度(luma)和色調(diào)(hue),同時(shí)采用頂層的色度(chroma);
          • color:保留了底層的亮度(luma),同時(shí)采用了頂層的色調(diào) (hue) 和色度 (chroma);
          • luminosity:保持底層的色調(diào)(hue)和色度(chroma),同時(shí)采用頂層的亮度(luma);
          context.fillStyle="#ff0000";
          context.fillRect(50,50,100,100);
          context.globalCompositeOperation="screen";
          context.fillStyle="rgba(0,0,255,1)";
          context.fillRect(50,100,100,100);
          context.globalCompositeOperation="darken";
          var image=new Image();
          image.src="images/s.png";
          image.onload=function(){
          context.drawImage(image,0,0);
          var image2=new Image();
          image2.src="images/2.png";
          image2.onload=function(){
          context.drawImage(image2,0,0);
          }
          }

          有的時(shí)候,并不希望進(jìn)行合成;比如:已經(jīng)使用半透明像素在畫(huà)布中繪制了內(nèi)容,這個(gè)時(shí)候,想要進(jìn)行臨時(shí)切換,然后再恢復(fù)到原先的狀態(tài);這個(gè)時(shí)候最簡(jiǎn)單的方法就是:將使用drawImage()方法將畫(huà)布內(nèi)容(或一部分內(nèi)容)復(fù)制到一張屏幕外畫(huà)布中;但是,保存的像素都是半透明的,這個(gè)時(shí)候合成是開(kāi)啟的,它們并不會(huì)完全抹除臨時(shí)繪制的內(nèi)容;因此,在這種情況下,就需要一種方式將合成關(guān)閉:不論像素是否透明,都會(huì)繪制源像素并忽略目標(biāo)像素;

          組合圖像:
          在繪制多個(gè)圖形的時(shí)候,會(huì)出現(xiàn)重疊的現(xiàn)象;此時(shí)可以將多塊圖形進(jìn)行組合,也是使用globalCompositeOperation屬性去指定組合方式,其可能的值如下:

          • source-over:默認(rèn)值,表示將新圖形繪制在源圖形上,對(duì)于半透明的源圖形就直接合并,這是默認(rèn)值;
          • source-in:新圖形與源圖形做in運(yùn)算,只顯示新圖形中與源圖形相重疊的部分,新圖形與源圖形的其他部分均變成透明;
          • source-out:新圖形與源圖形做out運(yùn)算,只顯示新圖形中與源圖形不重疊的部分,新圖形與源圖形的其他部分均變成透明;
          • source-atop:只繪制新圖形中與源圖形相重疊的部分以及未被重疊覆蓋的源圖形,新圖形的其他部分變成透明;
          • destination-over:表示在源圖形之下繪制新圖形;
          • destination-in:源圖形與新圖形做in運(yùn)算,只顯示源圖形中與新圖形相重疊的部分,新圖形與源圖形的其他部分均變成透明;
          • destination-out:新圖形與源圖形做out運(yùn)算,只顯示源圖形中與新圖形不重疊的部分,新圖形與源圖形的其他部分均變成透明;
          • destination-atop:只繪制源圖形中被新圖形重疊覆蓋的部分及新圖形的其他部分,源圖形的其他部分變成透明,且新圖形在源圖形后面繪制;
          • lighter:源圖形與新圖形均繪制,重疊部分做加色處理;
          • xor:只繪制新圖形中與源圖形不重疊的部分,重疊部分變成透明;
          • copy:只繪制新圖形,源圖形中未與新圖形重疊的部分變成透明;
          • source-over、destination-over和copy是三種常用的合成類型;另外,如果指定的值不在這幾個(gè)字符串中,則按默認(rèn)的方式組合圖形;
          context.fillStyle="#F00";
          context.fillRect(50,50,200,200);
          // context.globalCompositeOperation="source-over";
          // context.globalCompositeOperation="source-atop";
          // context.globalCompositeOperation="source-in";
          // context.globalCompositeOperation="source-out";
          // context.globalCompositeOperation="destination-over";
          // context.globalCompositeOperation="destination-atop";
          // context.globalCompositeOperation="destination-out";
          // context.globalCompositeOperation="copy";
          // context.globalCompositeOperation="lighter";
          context.globalCompositeOperation="xor";
          // context.fillStyle="#0F0"; // 硬透明度
          var g=context.createRadialGradient(200,200,20, 200,200,120);// 軟透明度
          g.addColorStop(0.0, "#0F0");
          g.addColorStop(1.0, "#00F");
          context.fillStyle=g;
          context.arc(200,200,120,0,Math.PI*2);
          context.fill();

          觀察不同值的狀態(tài):

          <input id="changeBtn" type="button" value="下一個(gè)" >
          <canvas id="canvas" width="1000" height="600"></canvas>
          <script>
          var canvas=document.getElementsByTagName("canvas")[0];
          var arr=new Array("source-over","source-in","source-out","source-atop","destination-over","destination-in","destination-out","destination-atop","lighter","xor","copy");
          var i=0;
          drawComposite(i);
          var changeBtn=document.getElementById("changeBtn");
          changeBtn.addEventListener("click", function(){
          if(i++==arr.length - 1)
          i=0;
          drawComposite(i);
          })
          function drawComposite(i){
          var context=canvas.getContext("2d");
          context.save();
          context.clearRect(0, 0, canvas.width, canvas.height);
          context.fillStyle="blue";
          context.fillRect(10,10,100,100);
          context.globalCompositeOperation=arr[i];
          context.beginPath();
          context.fillStyle="red";
          context.arc(100,100,60,0,Math.PI*2);
          context.closePath();
          context.fill();
          context.restore();
          context.font="24px 微軟雅黑";
          context.fillText(i + ": " + arr[i], 0, 200);
          }
          </script>

          組合合成示例:

          <script>
          // 定義了一些全局變量
          var canvas1=document.createElement("canvas");
          var canvas2=document.createElement("canvas");
          var gco=[ 'source-over','source-in','source-out','source-atop',
          'destination-over','destination-in','destination-out','destination-atop',
          'lighter', 'copy','xor',
          'multiply', 'screen', 'overlay', 'darken',
          'lighten', 'color-dodge', 'color-burn', 'hard-light', 'soft-light',
          'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity'
          ].reverse();
          var gcoText=[
          '這是默認(rèn)設(shè)置,并在現(xiàn)有畫(huà)布上下文之上繪制新圖形。',
          '新圖形只在新圖形和目標(biāo)畫(huà)布重疊的地方繪制。其他的都是透明的。',
          '在不與現(xiàn)有畫(huà)布內(nèi)容重疊的地方繪制新圖形。',
          '新圖形只在與現(xiàn)有畫(huà)布內(nèi)容重疊的地方繪制。',
          '在現(xiàn)有的畫(huà)布內(nèi)容后面繪制新的圖形。',
          '現(xiàn)有的畫(huà)布內(nèi)容保持在新圖形和現(xiàn)有畫(huà)布內(nèi)容重疊的位置。其他的都是透明的。',
          '現(xiàn)有內(nèi)容保持在新圖形不重疊的地方。',
          '現(xiàn)有的畫(huà)布只保留與新圖形重疊的部分,新的圖形是在畫(huà)布內(nèi)容后面繪制的。',
          '兩個(gè)重疊圖形的顏色是通過(guò)顏色值相加來(lái)確定的。',
          '只顯示新圖形。',
          '圖像中,那些重疊和正常繪制之外的其他地方是透明的。',
          '將頂層像素與底層相應(yīng)像素相乘,結(jié)果是一幅更黑暗的圖片。',
          '像素被倒轉(zhuǎn),相乘,再倒轉(zhuǎn),結(jié)果是一幅更明亮的圖片。',
          'multiply 和 screen 的結(jié)合,原本暗的地方更暗,原本亮的地方更亮。',
          '保留兩個(gè)圖層中最暗的像素。',
          '保留兩個(gè)圖層中最亮的像素。',
          '將底層除以頂層的反置。',
          '將反置的底層除以頂層,然后將結(jié)果反過(guò)來(lái)。',
          '屏幕相乘(A combination of multiply and screen)類似于疊加,但上下圖層互換了。',
          '用頂層減去底層或者相反來(lái)得到一個(gè)正值。',
          '一個(gè)柔和版本的強(qiáng)光(hard-light)。純黑或純白不會(huì)導(dǎo)致純黑或純白。',
          '和 difference 相似,但對(duì)比度較低。',
          '保留了底層的亮度(luma)和色度(chroma),同時(shí)采用了頂層的色調(diào)(hue)。',
          '保留底層的亮度(luma)和色調(diào)(hue),同時(shí)采用頂層的色度(chroma)。',
          '保留了底層的亮度(luma),同時(shí)采用了頂層的色調(diào) (hue) 和色度 (chroma)。',
          '保持底層的色調(diào)(hue)和色度(chroma),同時(shí)采用頂層的亮度(luma)。'
          ].reverse();
          var width=320;
          var height=340;
          window.onload=function() {
          var lum={
          r: 0.33,
          g: 0.33,
          b: 0.33
          };
          canvas1.width=width;
          canvas1.height=height;
          canvas2.width=width;
          canvas2.height=height;
          colorSphere();
          lightMix()
          runComposite();
          return;
          };
          var colorSphere=function(element) {
          var ctx=canvas1.getContext("2d");
          var width=360;
          var halfWidth=width / 2;
          var rotate=(1 / 360) * Math.PI * 2; // per degree
          var offset=0; // scrollbar offset
          var oleft=-20;
          var otop=-20;
          for (var n=0; n <=359; n ++) {
          var gradient=ctx.createLinearGradient(oleft + halfWidth, otop, oleft + halfWidth, otop + halfWidth);
          var color=Color.HSV_RGB({ H: (n + 300) % 360, S: 100, V: 100 });
          gradient.addColorStop(0, "rgba(0,0,0,0)");
          gradient.addColorStop(0.7, "rgba("+color.R+","+color.G+","+color.B+",1)");
          gradient.addColorStop(1, "rgba(255,255,255,1)");
          ctx.beginPath();
          ctx.moveTo(oleft + halfWidth, otop);
          ctx.lineTo(oleft + halfWidth, otop + halfWidth);
          ctx.lineTo(oleft + halfWidth + 6, otop);
          ctx.fillStyle=gradient;
          ctx.fill();
          ctx.translate(oleft + halfWidth, otop + halfWidth);
          ctx.rotate(rotate);
          ctx.translate(-(oleft + halfWidth), -(otop + halfWidth));
          }
          ctx.beginPath();
          ctx.fillStyle="#00f";
          ctx.fillRect(15,15,30,30)
          ctx.fill();
          return ctx.canvas;
          };
          var lightMix=function() {
          var ctx=canvas2.getContext("2d");
          ctx.save();
          ctx.globalCompositeOperation="lighter";
          ctx.beginPath();
          ctx.fillStyle="rgba(255,0,0,1)";
          ctx.arc(100, 200, 100, Math.PI*2, 0, false);
          ctx.fill()
          ctx.beginPath();
          ctx.fillStyle="rgba(0,0,255,1)";
          ctx.arc(220, 200, 100, Math.PI*2, 0, false);
          ctx.fill()
          ctx.beginPath();
          ctx.fillStyle="rgba(0,255,0,1)";
          ctx.arc(160, 100, 100, Math.PI*2, 0, false);
          ctx.fill();
          ctx.restore();
          ctx.beginPath();
          ctx.fillStyle="#f00";
          ctx.fillRect(0,0,30,30)
          ctx.fill();
          };
          function createCanvas() {
          var canvas=document.createElement("canvas");
          canvas.style.background="url("+op_8x8.data+")";
          canvas.style.border="1px solid #000";
          canvas.style.margin="5px";
          canvas.width=width/2;
          canvas.height=height/2;
          return canvas;
          }
          function runComposite() {
          var dl=document.createElement("dl");
          document.body.appendChild(dl);
          while(gco.length) {
          var pop=gco.pop();
          var dt=document.createElement("dt");
          dt.textContent=pop;
          dl.appendChild(dt);
          var dd=document.createElement("dd");
          var p=document.createElement("p");
          p.textContent=gcoText.pop();
          dd.appendChild(p);
          var canvasToDrawOn=createCanvas();
          var canvasToDrawFrom=createCanvas();
          var canvasToDrawResult=createCanvas();
          var ctx=canvasToDrawOn.getContext('2d');
          ctx.clearRect(0, 0, width, height)
          ctx.save();
          ctx.drawImage(canvas1, 0, 0, width/2, height/2);
          ctx.fillStyle="rgba(0,0,0,0.8)";
          ctx.fillRect(0, height/2 - 20, width/2, 20);
          ctx.fillStyle="#FFF";
          ctx.font="14px arial"; // [?ɡ?z?st??] 現(xiàn)存的,存在的
          ctx.fillText('existing content', 5, height/2 - 5);
          ctx.restore();
          var ctx=canvasToDrawFrom.getContext('2d');
          ctx.clearRect(0, 0, width, height)
          ctx.save();
          ctx.drawImage(canvas2, 0, 0, width/2, height/2);
          ctx.fillStyle="rgba(0,0,0,0.8)";
          ctx.fillRect(0, height/2 - 20, width/2, 20);
          ctx.fillStyle="#FFF";
          ctx.font="14px arial";
          ctx.fillText('new content', 5, height/2 - 5);
          ctx.restore();
          var ctx=canvasToDrawResult.getContext('2d');
          ctx.clearRect(0, 0, width, height)
          ctx.save();
          ctx.drawImage(canvas1, 0, 0, width/2, height/2);
          ctx.globalCompositeOperation=pop;
          ctx.drawImage(canvas2, 0, 0, width/2, height/2);
          ctx.globalCompositeOperation="source-over";
          ctx.fillStyle="rgba(0,0,0,0.8)";
          ctx.fillRect(0, height/2 - 20, width/2, 20);
          ctx.fillStyle="#FFF";
          ctx.font="14px arial";
          ctx.fillText(pop, 5, height/2 - 5);
          ctx.restore();
          dd.appendChild(canvasToDrawOn);
          dd.appendChild(canvasToDrawFrom);
          dd.appendChild(canvasToDrawResult);
          dl.appendChild(dd);
          }
          };
          // HSV (1978)=H: Hue / S: Saturation / V: Value
          Color={};
          Color.HSV_RGB=function (o) {
          var H=o.H / 360,
          S=o.S / 100,
          V=o.V / 100,
          R, G, B;
          var A, B, C, D;
          if (S==0) {
          R=G=B=Math.round(V * 255);
          } else {
          if (H >=1) H=0;
          H=6 * H;
          D=H - Math.floor(H);
          A=Math.round(255 * V * (1 - S));
          B=Math.round(255 * V * (1 - (S * D)));
          C=Math.round(255 * V * (1 - (S * (1 - D))));
          V=Math.round(255 * V);
          switch (Math.floor(H)) {
          case 0:
          R=V;
          G=C;
          B=A;
          break;
          case 1:
          R=B;
          G=V;
          B=A;
          break;
          case 2:
          R=A;
          G=V;
          B=C;
          break;
          case 3:
          R=A;
          G=B;
          B=V;
          break;
          case 4:
          R=C;
          G=A;
          B=V;
          break;
          case 5:
          R=V;
          G=A;
          B=B;
          break;
          }
          }
          return {
          R: R,
          G: G,
          B: B
          };
          };
          var createInterlace=function (size, color1, color2) {
          var proto=document.createElement("canvas").getContext("2d");
          proto.canvas.width=size * 2;
          proto.canvas.height=size * 2;
          proto.fillStyle=color1; // top-left
          proto.fillRect(0, 0, size, size);
          proto.fillStyle=color2; // top-right
          proto.fillRect(size, 0, size, size);
          proto.fillStyle=color2; // bottom-left
          proto.fillRect(0, size, size, size);
          proto.fillStyle=color1; // bottom-right
          proto.fillRect(size, size, size, size);
          var pattern=proto.createPattern(proto.canvas, "repeat");
          pattern.data=proto.canvas.toDataURL();
          return pattern;
          };
          var op_8x8=createInterlace(8, "#FFF", "#eee");
          </script>

          像素處理;
          使用canvas API能夠獲取圖像中的每個(gè)像素,并且能夠得到該像素的顏色的RGB值或RGBA值;具體就是使用context的getImageData(x, y, width, height)方法來(lái)獲取原始圖像數(shù)據(jù),參數(shù)x、y表示所獲取區(qū)域的起始坐標(biāo),width、height表示所獲取區(qū)域的寬和高;

          var image=document.getElementsByTagName("img")[0];
          context.drawImage(image,0,0);
          var imagedata=context.getImageData(0, 0, image.width, image.height);
          console.log(imagedata); // ImageData

          該方法返回一個(gè)ImageData類型的對(duì)象,表示畫(huà)布矩形區(qū)域中的原始像素信息,通過(guò)該對(duì)象可以操縱像素?cái)?shù)據(jù)、直接讀取或?qū)?shù)據(jù)數(shù)組寫(xiě)入該對(duì)象中,其具有width、height、colorSpace、data等屬性;其中,data屬性是一個(gè)保存像素的Uint8ClampedArray類型化數(shù)組視圖,內(nèi)容類似[r1,g1,b1,a1,r2,g2,b2,a2,..],其中每4個(gè)元素表示一個(gè)像素信息,即為R、G、B和A分量,也就是r1、g1、b1、a1為一個(gè)像素的紅、綠、藍(lán)與透明度的值;data.length為所取得像素的數(shù)量的4倍;

          某個(gè)像素的索引位置(n為第n個(gè)像素)為:(n-1)*4+0(R)、(n-1)*4+1(G)、(n-1)*4+2(B)、(n-1)*4+3(A),分別為第n個(gè)像素的 R/G/B/A 分量值;

          // 取得第一個(gè)像素的值
          var red=imagedata.data[0],
          green=imagedata.data[1],
          blue=imagedata.data[2],
          alpha=imagedata.data[3];
          var color=[red, green, blue, alpha];
          console.log(color);
          var red=imagedata.data[4],
          green=imagedata.data[5],
          blue=imagedata.data[6],
          alpha=imagedata.data[7];
          var color=[red, green, blue, alpha];
          console.log(color); // [57, 133, 254, 255]
          var n=9;
          var red=imagedata.data[(n-1)*4+0],
          green=imagedata.data[(n-1)*4+1],
          blue=imagedata.data[(n-1)*4+2],
          alpha=imagedata.data[(n-1)*4+3];
          var color=[red, green, blue, alpha];
          console.log(color);

          按行和列獲取某個(gè)像素RGBA分量值:row * imageData.width * 4 + col * 4 + 0|1|2|3或(row * imageData.width + col) * 4 + 0|1|2|3;

          // 讀取圖片中位于索引為50行、索引為200列的像素的藍(lán)色
          var bluePixel=imagedata.data[50 * imagedata.width * 4 + 200 * 4 + 2];
          console.log(bluePixel); // 254
          // 封裝一個(gè)函數(shù)
          function getPixel(row, col){
          var pixels=[];
          for(var i=0; i<4; i++){
          pixels.push(imagedata.data[(row * imagedata.width + 200) * 4 + i]);
          }
          return pixels;
          }
          var pixels=getPixel(50, 200);
          console.log(pixels);

          任何在畫(huà)布以外的元素都會(huì)被返回成一個(gè)透明黑的 ImageData 對(duì)像;

          var imagedata=context.getImageData(image.width, 0, image.width, image.height);
          console.log(imagedata);

          通過(guò)類型為Uint8ClampedArray的data屬性,不僅能直接訪問(wèn)到原始圖像像素?cái)?shù)據(jù),還能夠以各種方式來(lái)操作這些數(shù)據(jù),例如,可以修改圖像像素?cái)?shù)據(jù),創(chuàng)建一個(gè)簡(jiǎn)單的灰階過(guò)濾器,如:

          var red, green, blue, alpha;
          var average;
          var data=imagedata.data;
          for(var i=0, len=data.length; i<len; i+=4){
          red=data[i];
          green=data[i+1];
          blue=data[i+2];
          alpha=data[i+3];
          average=Math.floor((red + green + blue) / 3);
          data[i]=data[i+1]=data[i+2]=average;
          }
          imagedata.data=data;
          context.putImageData(imagedata, 0, 0);

          用getImageData方法獲取圖片信息時(shí),該圖像不能跨域;

          img元素中的crossorigin屬性,該屬性是HTML5新增的屬性,以腳本中,也有同名的屬性,其決定了圖片獲取過(guò)程中是否開(kāi)啟CORS功能;有兩個(gè)可能值:

          • anonymous:當(dāng)使用這個(gè)值時(shí),就會(huì)在請(qǐng)求中的header中的帶上Origin屬性,但請(qǐng)求不會(huì)帶上cookie和其他的一些認(rèn)證信息;
          • use-credentials:當(dāng)使用這個(gè)值時(shí),會(huì)同時(shí)在跨域請(qǐng)求中帶上cookie和其他的一些認(rèn)證信息;

          默認(rèn)情況下,如果沒(méi)有使用這個(gè)屬性,說(shuō)明沒(méi)有開(kāi)啟CORS,且值為null;

          示例:顏色選擇器

          <style>
          .container{
          display: flex;
          }
          .container div{width:200px; border:1px solid;}
          </style>
          <div class="container">
          <canvas id="drawing" width="300" height="200"></canvas>
          <div id="hovered-color"></div>
          <div id="selected-color"></div>
          </div>
          <script>
          window.onload=function(){
          var img=new Image();
          img.crossOrigin='anonymous';
          img.src='images/1.jpg';
          var canvas=document.getElementById('drawing');
          var context=canvas.getContext('2d');
          img.onload=function() {
          context.drawImage(img, 0, 0);
          img.style.display='none';
          };
          var hoveredColor=document.getElementById('hovered-color');
          var selectedColor=document.getElementById('selected-color');
          canvas.addEventListener('mousemove', function(event) {
          pick(event, hoveredColor);
          });
          canvas.addEventListener('click', function(event) {
          pick(event, selectedColor);
          });
          function pick(event, destination) {
          var x=event.layerX;
          var y=event.layerY;
          var pixel=context.getImageData(x, y, 1, 1);
          var data=pixel.data;
          var r=data[0], g=data[1], b=data[2], a=data[3] / 255;
          var rgba="rgba(" + r + ", "+ g +", "+ b +", " + a + ")";
          destination.style.background=rgba;
          destination.textContent=rgba;
          return rgba;
          }
          }
          </script>

          取得像素后,就可以對(duì)這些像素進(jìn)行處理,如蒙版處理、面部識(shí)別等較復(fù)雜的圖像處理操作;

          使用putImageData(imagedata, dx, dy[, dirtyX, dirtyY, dirtyWidth, dirtyHeight])方法可以將一個(gè)已有的ImageData對(duì)象繪制到畫(huà)布上,參數(shù)dx和dy表示重繪圖像的起點(diǎn)坐標(biāo),dirtyX、dirtyY、dirtyWidth、dirtyHeight為可選,它們給出一個(gè)矩形的起點(diǎn)坐標(biāo)及寬高,如果使用這4個(gè)參數(shù),則只繪制像素?cái)?shù)組中在這個(gè)矩形范圍內(nèi)的圖像;

          context.drawImage(image, 0, 0);
          var imagedata=context.getImageData(0,0,image.width,image.height);
          context.clearRect(0, 0, 800, 600);
          context.putImageData(imagedata, 0, 0, 200, 200, 200, 100);

          也可以寫(xiě)入到另外一個(gè)canvas中,如:

          // ...
          var canvas1=document.getElementById("canvas1");
          var ctx=canvas1.getContext("2d");
          ctx.putImageData(imagedata, 0, 0, 200, 200, 200, 100);
          context.drawImage(image, 0, 0);
          var imagedata=context.getImageData(0,0,image.width,image.height);
          for(var i=0, len=imagedata.data.length; i<len; i++){
          imagedata.data[i+0]=255 - imagedata.data[i+0]; // red
          imagedata.data[i+1]=255 - imagedata.data[i+2]; // green
          imagedata.data[i+2]=255 - imagedata.data[i+1]; // blue
          }
          context.putImageData(imagedata, 0, 0);

          putImageData()會(huì)按照默認(rèn)的坐標(biāo)系來(lái)處理,不受畫(huà)布變換矩陣的影響,而且,會(huì)忽略所有的圖形屬性;不會(huì)進(jìn)行任何合成操作,也不會(huì)用globalAlpha乘以像素來(lái)顯示,更不會(huì)繪制陰影;

          putImageData()方法的原理:

          function putImageData(context, imageData, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight) {
          var data=imageData.data;
          var width=imageData.width;
          var height=imageData.height;
          dirtyX=dirtyX || 0;
          dirtyY=dirtyY || 0;
          dirtyWidth=dirtyWidth !==undefined ? dirtyWidth : width;
          dirtyHeight=dirtyHeight !==undefined ? dirtyHeight : height;
          var limitBottom=dirtyY + dirtyHeight;
          var limitRight=dirtyX + dirtyWidth;
          
          for (var y=dirtyY; y < limitBottom; y++) {
          for (var x=dirtyX; x < limitRight; x++) {
          var pos=y * width + x;
          context.fillStyle='rgba(' + data[pos*4+0]
          + ',' + data[pos*4+1]
          + ',' + data[pos*4+2]
          + ',' + (data[pos*4+3]/255) + ')';
          context.fillRect(x + dx, y + dy, 1, 1);
          }
          }
          }
          context.fillRect(0,0,100,100);
          var imagedata=context.getImageData(0,0,100,100);
          putImageData(context, imagedata, 150, 0, 50, 50, 25, 25);

          示例:圖片灰度和反相顏色

          <canvas id="canvas" width="300" height="200"></canvas>
          <p>
          <input type="radio" id="original" name="color" value="original" checked>
          <label for="original">Original</label>
          <input type="radio" id="grayscale" name="color" value="grayscale">
          <label for="grayscale">Grayscale</label>
          <input type="radio" id="inverted" name="color" value="inverted">
          <label for="inverted">Inverted</label>
          </p>
          <script>
          var img=new Image();
          img.crossOrigin='anonymous';
          img.src='images/2.jpg';
          var canvas=document.getElementById('canvas');
          var context=canvas.getContext('2d');
          img.onload=function() {
          context.drawImage(img, 0, 0);
          };
          var original=function() {
          context.drawImage(img, 0, 0);
          };
          var grayscale=function() {
          context.drawImage(img, 0, 0);
          var imageData=context.getImageData(0, 0, canvas.width, canvas.height);
          var data=imageData.data;
          for (var i=0; i < data.length; i +=4) {
          var avg=(data[i] + data[i + 1] + data[i + 2]) / 3;
          data[i]=avg; // red
          data[i + 1]=avg; // green
          data[i + 2]=avg; // blue
          }
          context.putImageData(imageData, 0, 0);
          };
          var invert=function() {
          context.drawImage(img, 0, 0);
          var imageData=context.getImageData(0, 0, canvas.width, canvas.height);
          var data=imageData.data;
          for (var i=0; i < data.length; i +=4) {
          data[i]=255 - data[i]; // red
          data[i + 1]=255 - data[i + 1]; // green
          data[i + 2]=255 - data[i + 2]; // blue
          }
          context.putImageData(imageData, 0, 0);
          };
          var inputs=document.querySelectorAll('[name=color]');
          for (var input of inputs) {
          input.addEventListener("change", function(evt) {
          switch (evt.target.value) {
          case "inverted":
          return invert();
          case "grayscale":
          return grayscale();
          default:
          return original();
          }
          });
          }
          </script>

          createImageData(width, height | imagedata)方法:
          可以創(chuàng)建一個(gè)空的ImageData對(duì)象,該對(duì)象中的像素是可寫(xiě)的,因此,可以對(duì)它們進(jìn)行設(shè)置;
          參數(shù)width和height為新對(duì)象的寬和高;imagedata為一個(gè)已有的ImageData對(duì)象,即復(fù)制一個(gè)和它具有相同的高和寬的對(duì)象,而圖像自身不被復(fù)制;

          context.rect(10, 10, 100, 100);
          context.fill();
          var imgdata=context.createImageData(100, 100);
          console.log(imgdata); // ImageData
          console.log(context.createImageData(imgdata)); // ImageData

          默認(rèn)情況下,這個(gè)空的ImageData對(duì)象的像素全部被預(yù)設(shè)為透明黑;如果width和height指定為負(fù)值,會(huì)被處理成相應(yīng)的正值:

          有了ImageData對(duì)象后,再通過(guò)putImageData()方法將這些像素復(fù)制回畫(huà)布中;如:

          var imagedata=context.getImageData(0, 0, canvas.width, canvas.height);
          var data=context.createImageData(imagedata);
          console.log(imagedata);
          for(var i=0; i<imagedata.data.length; i++){
          data.data[i]=imagedata.data[i];
          }
          console.log(data);
          var canvas1=document.getElementById("canvas1");
          var ctx=canvas1.getContext("2d");
          ctx.putImageData(data, 0, 0);

          示例:在一個(gè)畫(huà)布中的圖形要?jiǎng)?chuàng)建一種簡(jiǎn)單的動(dòng)態(tài)模糊或“涂抺”效果;

          function smear(c, n, x, y, w, h){
          var pixels=c.getImageData(x,y,w,h);
          var width=pixels.width, height=pixels.height;
          var data=pixels.data;
          var m=n - 1;
          for(var row=0; row<height; row++){
          var i=row * width * 4 + 4;
          for(var col=1; col<width; col++, i+=4){
          data[i]=(data[i] + data[i-4]*m) / n;
          data[i+1]=(data[i+1] + data[i-3]*m) / n;
          data[i+2]=(data[i+2] + data[i-2]*m) / n;
          data[i+3]=(data[i]+3 + data[i-1]*m) / n;
          }
          }
          c.putImageData(pixels, x, y);
          }
          var image=new Image();
          image.src="images/1.jpg";
          image.onload=function(){
          context.drawImage(image,50,50);
          smear(context, 50, 100, 100, 100, 100);
          };

          縮放和反鋸齒:
          過(guò)度縮放圖像可能會(huì)導(dǎo)致圖像模糊或像素化;可以通過(guò)使用2D上下文的imageSmoothingEnabled屬性來(lái)控制是否在縮放圖像時(shí)使用平滑算法;默認(rèn)值為true,即啟用平滑縮放,也可以禁用此功能,如:

          context.imageSmoothingEnabled=false;
          context.mozImageSmoothingEnabled=false;
          context.webkitImageSmoothingEnabled=false;
          context.msImageSmoothingEnabled=false;
          context.imageSmoothingEnabled=false;
          示例:zoom
          <canvas id="canvas" width="300" height="227"></canvas>
          <canvas id="zoom" width="300" height="227"></canvas>
          <div>
          <label for="smoothbtn">
          <input type="checkbox" name="smoothbtn" checked="checked" id="smoothbtn">
          Enable image smoothing
          </label>
          </div>
          <script>
          window.onload=function(){
          var img=new Image();
          img.src='images/3.jpg';
          img.onload=function() {
          draw(this);
          };
          function draw(img) {
          var canvas=document.getElementById('canvas');
          var context=canvas.getContext('2d');
          context.drawImage(img, 0, 0);
          img.style.display='none';
          var zoomctx=document.getElementById('zoom').getContext('2d');
          var smoothbtn=document.getElementById('smoothbtn');
          var toggleSmoothing=function(event) {
          zoomctx.imageSmoothingEnabled=this.checked;
          zoomctx.mozImageSmoothingEnabled=this.checked;
          zoomctx.webkitImageSmoothingEnabled=this.checked;
          zoomctx.msImageSmoothingEnabled=this.checked;
          };
          smoothbtn.addEventListener('change', toggleSmoothing);
          var zoom=function(event) {
          var x=event.layerX;
          var y=event.layerY;
          zoomctx.drawImage(canvas,
          Math.abs(x - 5),
          Math.abs(y - 5),
          10, 10,
          0, 0,
          200, 200);
          };
          canvas.addEventListener('mousemove', zoom);
          }
          }
          </script>

          imageSmoothingQuality屬性,用于設(shè)置圖像平滑度的屬性,一般配合imageSmoothingEnabled屬性使用;其可能的值為:"low","medium","high";

          context.imageSmoothingQuality="Medium";

          命中檢測(cè):
          在2D繪圖上下文中,路徑是一種主要的繪圖方式,因?yàn)槁窂侥転橐L制的圖形提供更多控制,由于路徑的使用很頻繁,所以就有了一個(gè)名為isPointInPath(x, y[, fillRule])的方法,該方法接收x和y坐標(biāo)作為參數(shù),用于確定畫(huà)布上的某一點(diǎn)是否位于當(dāng)前路徑上(內(nèi)),該坐標(biāo)是在默認(rèn)坐標(biāo)系中而不是在變換過(guò)的坐標(biāo)系中;如:

          context.rect(100, 100, 200, 100);
          context.stroke();
          if(context.isPointInPath(100, 100)){
          alert("點(diǎn)(100,100)位于路徑內(nèi)");
          }

          可選的參數(shù)fillRule,用來(lái)決定點(diǎn)在路徑內(nèi)還是在路徑外的算法,允許的值:"nonzero": 非零環(huán)繞規(guī)則 ,默認(rèn)的規(guī)則;"evenodd": 奇偶環(huán)繞原則;

          isPointInPath()還有另外一種形式:isPointInPath(path, x, y[, fillRule]);參數(shù)path為一個(gè)Path2D路徑對(duì)象;
          該方法用于命中檢測(cè)(hit detection):檢測(cè)鼠標(biāo)單擊事件是否發(fā)生在特定的形狀上;但是,不是將MouseEvent對(duì)象的clientX和clientY屬性直接傳遞給isPointInPath()方法;首先,必須要將鼠標(biāo)事件的坐標(biāo)轉(zhuǎn)換成相應(yīng)的畫(huà)布坐標(biāo);其次,如果畫(huà)布在屏幕上顯示的尺寸和實(shí)際尺寸不同,鼠標(biāo)事件坐標(biāo)必須要進(jìn)行適當(dāng)?shù)目s放,如

          function hitpath(context, event){
          var canvas=context.canvas;
          var rect=canvas.getBoundingClientRect();
          var x=(event.clientX - rect.left) * (canvas.width / rect.width);
          var y=(event.clientY - rect.top) * (canvas.height / rect.height);
          return context.isPointInPath(x, y);
          }
          canvas.onclick=function(event){
          if(hitpath(this.getContext("2d"),event)){
          alert("Hit");
          }
          }

          除了進(jìn)行基于路徑的命中檢測(cè)之外,還可以使用getImageData()方法來(lái)檢測(cè)鼠標(biāo)點(diǎn)下的像素是否已經(jīng)繪制過(guò)了;如果返回的像素(單個(gè)或多個(gè))是完全透明的,則表示該像素上沒(méi)有繪制任何內(nèi)容,或者認(rèn)為鼠標(biāo)點(diǎn)空了,如:

          function hitpaint(context, event){
          var canvas=context.canvas;
          var rect=canvas.getBoundingClientRect();
          var x=(event.clientX - rect.left) * (canvas.width / rect.width);
          var y=(event.clientY - rect.top) * (canvas.height / rect.height);
          var pixels=context.getImageData(x, y, 1, 1);
          for(var i=3; i<pixels.data.length; i+=4){
          if(pixels.data[i] !==0)
          return true;
          }
          return false;
          }

          isPointInStroke([path,] x, y)方法:
          用于檢測(cè)某點(diǎn)是否在路徑的描邊線上;參數(shù):x、y為檢測(cè)點(diǎn)的 X 坐標(biāo)Y 坐標(biāo),path為Path2D 路徑;當(dāng)這個(gè)點(diǎn)在路徑的描邊線上,則返回 true,否則返回 false;

          context.rect(10, 10, 100, 100);
          context.lineWidth=10;
          context.stroke();
          console.log(context.isPointInStroke(10, 10)); // true
          console.log(context.isPointInStroke(12, 12)); // true

          保存文件;
          在畫(huà)布中繪制完成一幅圖形或圖像后,可以將該圖像或圖形保存到文件中;例如,用戶直接可以在畫(huà)布上右擊,把canvas繪圖保存為一個(gè)圖像文件,默認(rèn)為PNG格式;
          另外,也可以轉(zhuǎn)換為data URL,此時(shí)是把當(dāng)前的繪畫(huà)狀態(tài)輸出到一個(gè)data URL地址所指向的數(shù)據(jù)中;

          toDataURL(type [, quality])方法:可以把canvas上的繪制的圖像導(dǎo)出,參數(shù)type,為輸出數(shù)據(jù)的MIME類型;參數(shù)quality為圖像質(zhì)量,值為從0到1,1 表示最好品質(zhì),0 基本不被辨析,但文件更?。?/p>

          var imgURI=drawing.toDataURL("image/png");
          var image=document.createElement("img");
          image.src=imgURI;
          document.body.appendChild(image);

          默認(rèn)情況下,瀏覽器會(huì)將圖像編碼為PNG格式,且分辨率為96dpi;

          轉(zhuǎn)換為Blob對(duì)象:
          也可以使用canvas對(duì)象的canvas.toBlob(callback, [mimeType[, qualityArgument]]);方法將canvas元素中的圖像直接轉(zhuǎn)換為一個(gè)Blob對(duì)象,參數(shù)mimeType指定圖像的MIME類型,當(dāng)它參數(shù)值為“image/jpeg”或“image/webp”時(shí),可以使用qualityArgument參數(shù)指定圖像質(zhì)量,參數(shù)值為一個(gè)允許小數(shù)的數(shù)值,值范圍為0到1之間;當(dāng)轉(zhuǎn)換成功時(shí),會(huì)執(zhí)行回調(diào)函數(shù),其參數(shù)即為轉(zhuǎn)換成功的Blob對(duì)象,如:

          <canvas id="canvas" width="400" height="300"></canvas><br/>
          <input type="button" id="btnSave" value="保存圖像">
          <script>
          window.onload=function(){
          draw('canvas');
          var btnSave=document.getElementById("btnSave");
          btnSave.addEventListener("click", savePic)
          };
          function draw(id){
          var img=new Image();
          img.src="images/2.jpg";
          img.onload=function(){
          var canvas=document.getElementById(id);
          if (canvas==null)
          return false;
          var context=canvas.getContext("2d");
          context.drawImage(img, 0, 0, canvas.width, canvas.height);
          }
          }
          function savePic(){
          canvas.toBlob(function(blob){
          var a=document.createElement("a");
          a.textContent="打開(kāi)圖像";
          document.body.appendChild(a);
          a.style.display="block";
          a.href=URL.createObjectURL(blob);
          },"image/png",0.95);
          }
          </script>

          canvas元素的toBlob方法是非常重要的,因?yàn)槿绻褂肂lob對(duì)象,可以使用Blob對(duì)象的size屬性獲取輸出后的文件尺寸;在將canvas元素中的圖像進(jìn)行輸出或?qū)⑵涮峤坏椒?wù)器端時(shí),如果使用Data URL,由于圖像數(shù)據(jù)為文本數(shù)據(jù),對(duì)于大數(shù)據(jù)圖像,將大幅度增加瀏覽器端的負(fù)擔(dān),如果使用Blob對(duì)象,由于瀏覽器內(nèi)部使用二進(jìn)制數(shù)據(jù),會(huì)大幅度減輕瀏覽器的負(fù)擔(dān);

          解碼圖像:
          針對(duì)一個(gè)cavnas元素來(lái)說(shuō),無(wú)論是修改、剪切或縮放其中的圖像后再利用時(shí),首先要做的一件事情是解碼其中的圖像;問(wèn)題在于當(dāng)對(duì)canvas元素中的圖像進(jìn)行解碼時(shí),可能需要耗費(fèi)較大的CPU資源;

          使用window對(duì)象的createImageBitmap方法,可用于后臺(tái)解碼圖像,其返回一個(gè)ImageBitmap對(duì)象,開(kāi)發(fā)者可以將該對(duì)象中存儲(chǔ)的圖像繪制到一個(gè)canvas元素中;

          createImageBitmap(image[, sx, sy, sw, sh]).then(function(response) {…}); 參數(shù)image用于指定圖像來(lái)源,其可以為一個(gè)img元素、video元素、canvas、Blob、ImageData、ImageBitmap等;參數(shù)sx, sy, sw, sh分別用于指定被復(fù)制區(qū)域的起始坐標(biāo)及寬和高;
          該方法返回一個(gè)以一個(gè)ImageBitmap對(duì)象為結(jié)果的Promise對(duì)象,該對(duì)象中包含了指定區(qū)域的圖像;

          var image=document.getElementsByTagName("img")[0];
          image.onload=function(){
          // var imageBitmap=window.createImageBitmap(image);
          var imageBitmap=window.createImageBitmap(image, 50, 50, 200, 100);
          console.log(imageBitmap); // Promise
          }

          例如,繪制一個(gè)圖像到畫(huà)布中

          function draw(id){
          fetch("images/1.jpg")
          .then(response=> response.blob())
          .catch(error=> console.error("Error:",error))
          .then(response=> {
          let canvas=document.getElementById(id);
          let context=canvas.getContext("2d");
          createImageBitmap(response,50,50,400,300).then(
          imageBitmap=> context.drawImage(imageBitmap,0,0));
          });
          }
          draw("canvas");

          ImageBitmap對(duì)象:
          ImageBitmap 接口表示能夠被繪制到 <canvas> 上的位圖圖像,具有低延遲的特性;一般由createImageBitmap()方法返回,并且它可以從多種源中生成,如img、canvas、video等;
          ImageBitmap提供了一種異步且高資源利用率的方式來(lái)為WebGL的渲染準(zhǔn)備基礎(chǔ)結(jié)構(gòu);

          屬性:
          width:只讀,無(wú)符號(hào)長(zhǎng)整型數(shù)值,表示ImageData對(duì)象的寬度,單位為像素;
          height:只讀,無(wú)符號(hào)長(zhǎng)整型數(shù)值,表示ImageData對(duì)象的高度;

          方法:
          close():釋放ImageBitmap所相關(guān)聯(lián)的所有圖形資源;

          可以在Web Worker中使用createImageBitmap方法;如果有許多圖像需要解碼,可以將URL傳遞給Web Worker,在其中下載并解碼圖像,然后將解碼結(jié)果傳遞給主線程以便將其繪制到canvas中;
          worker.js代碼:

          onmessage=function(event){
          fetch(event.data).then(response=> response.blob())
          .catch(error=> self.postMessage(error))
          .then(response=> {
          createImageBitmap(response,23,5,57,80)
          .then(imageBitmap=> self.postMessage({imageBitmap:imageBitmap});)
          });
          }

          js代碼:

          function draw(id){
          let canvas=document.getElementById(id);
          let context=canvas.getContext("2d");
          let worker=new Worker("worker.js");
          worker.postMessage("images/1.jpg");
          worker.onmessage=(evt)=> {
          if(evt.data.err)
          console.log(evt.data.message);
          context.drawImage(evt.data.imageBitmap,0,0);
          };
          }

          createImageBitmap方法還有一個(gè)可選的options參數(shù),其為一個(gè)設(shè)置選項(xiàng)的對(duì)象;可用的選項(xiàng)為:

          • imageOrientation:指示圖像是按原樣呈現(xiàn)還是垂直翻轉(zhuǎn),值為none不翻轉(zhuǎn)或flipY翻轉(zhuǎn);
          • premultiplyAlpha:指示位圖顏色通道由alpha通道預(yù)乘,值為none、premultiply或default;
          • colorSpaceConversion:指示圖像是否使用色彩空間轉(zhuǎn)換進(jìn)行解碼,值為none或default;
          • resizeWidth:指定新寬度;
          • resizeHeight:指示新高度;
          • resizeQuality:指定圖像質(zhì)量,值為:pixelated、low (默認(rèn))、medium或high;
          // ...
          createImageBitmap(response,50,50,400,300, {
          imageOrientation:"flipY",
          premultiplyAlpha:"premultiply",
          colorSpaceConversion:"none",
          resizeWidth:200,
          resizeHeight:150,
          resizeQuality:"medium"
          }).then(
          imageBitmap=> context.drawImage(imageBitmap,0,0));

          動(dòng)畫(huà)的制作:
          在Canvas畫(huà)布中制作動(dòng)畫(huà)相對(duì)來(lái)說(shuō)比較簡(jiǎn)單,實(shí)際上就是一個(gè)不斷擦除、重繪、擦除、重繪的過(guò)程;

          基本步驟:

          • 1)清空canvas:預(yù)先編寫(xiě)好用來(lái)繪圖的函數(shù),在該函數(shù)中先用clearRect方法將畫(huà)布整體或局部擦除;
          • 2)保存狀態(tài):如果要改變一些會(huì)改變 canvas 狀態(tài)的設(shè)置(樣式,變形之類的),又要在每畫(huà)一幀的時(shí)候都是原始狀態(tài)的話,需要先保存一下;
          • 3)繪制動(dòng)畫(huà)圖形:使用setInterval方法設(shè)置動(dòng)畫(huà)的間隔時(shí)間;
          • 4)恢復(fù)狀態(tài);

          操控動(dòng)畫(huà):
          在繪圖圖形圖像時(shí),僅僅在腳本執(zhí)行結(jié)束后才能看見(jiàn)結(jié)果,所以,在類似for循環(huán)體里實(shí)現(xiàn)動(dòng)畫(huà)是不太可能的;

          因此,為了實(shí)現(xiàn)動(dòng)畫(huà),需要一些可以定時(shí)執(zhí)行重繪的方法;即可以通過(guò) setInterval、setTimeout 和window.requestAnimationFrame()方法來(lái)控制在設(shè)定的時(shí)間點(diǎn)上執(zhí)行重繪,從而操控動(dòng)畫(huà);

          如果并不需要與用戶互動(dòng),可以使用 setInterval() 方法;如果需要做一個(gè)游戲,可以使用鍵盤(pán)或者鼠標(biāo)事件配合上setTimeout()方法來(lái)實(shí)現(xiàn),通過(guò)設(shè)置事件監(jiān)聽(tīng),可以捕捉用戶的交互,并執(zhí)行相應(yīng)的動(dòng)作;

          如:一個(gè)走動(dòng)的小方塊

          var context;
          var w,h,i;
          var timer=null;
          function draw(id){
          var canvas=document.getElementById(id);
          if(canvas==null){
          return false;
          }
          context=canvas.getContext("2d");
          context.fillStyle="#EEE";
          context.fillRect(0,0,400,300);
          w=canvas.width;
          h=canvas.height;
          i=0;
          timer=setInterval(rotate,100);
          }
          function rotate(){
          if(i>=w -20)
          clearInterval(timer);
          
          context.clearRect(0,0,w,h);
          context.fillStyle="red";
          context.fillRect(i,0,20,20);
          i=i + 20;
          }
          draw("canvas");

          示例:圖形組合變換

          var globalId;
          var i=0;
          function draw(id){
          globalId=id;
          setInterval(Composite,1000);
          }
          function Composite(){
          var canvas=document.getElementById(globalId);
          if(canvas==null)
          return false;
          var context=canvas.getContext("2d");
          var arr=new Array("source-atop","source_in","source-out","source-over","destination-atop","destination-in","destination-out","destination-over","lighter","copy","xor");
          if(i>10) i=0;
          context.clearRect(0,0,canvas.width,canvas.height);
          context.save();
          context.fillStyle="blue";
          context.fillRect(10,10,60,60);
          context.globalCompositeOperation=arr[i];
          context.beginPath();
          context.fillStyle="red";
          context.arc(60,60,30,0,Math.PI*2,false);
          context.fill();
          context.restore();
          i=i+1;
          }
          draw("canvas");

          示例:太陽(yáng)系的動(dòng)畫(huà)

          <canvas id="canvas" width="600" height="600"></canvas>
          <script>
          var sun=new Image();
          var moon=new Image();
          var earth=new Image();
          function init(){
          sun.src='images/sun.png';
          moon.src='images/moon.png';
          earth.src='images/earth.png';
          window.requestAnimationFrame(draw);
          }
          function draw() {
          var ctx=document.getElementById('canvas').getContext('2d');
          ctx.globalCompositeOperation='destination-over';
          ctx.clearRect(0,0,300,300); // clear canvas
          ctx.fillStyle='rgba(0,0,0,0.4)';
          ctx.strokeStyle='rgba(0,153,255,0.4)';
          ctx.save();
          ctx.translate(150,150);
          // Earth
          var time=new Date();
          ctx.rotate( ((2*Math.PI)/60)*time.getSeconds() + ((2*Math.PI)/60000)*time.getMilliseconds() );
          ctx.translate(105,0);
          ctx.fillRect(0,-12,50,24); // Shadow
          ctx.drawImage(earth,-12,-12);
          // Moon
          ctx.save();
          ctx.rotate( ((2*Math.PI)/6)*time.getSeconds() + ((2*Math.PI)/6000)*time.getMilliseconds() );
          ctx.translate(0,28.5);
          ctx.drawImage(moon,-3.5,-3.5);
          ctx.restore();
          ctx.restore();
          ctx.beginPath();
          ctx.arc(150,150,105,0,Math.PI*2,false); // Earth orbit
          ctx.stroke();
          ctx.drawImage(sun,0,0,300,300);
          window.requestAnimationFrame(draw);
          }
          init();
          </script>

          示例:動(dòng)畫(huà)時(shí)鐘

          <canvas id="canvas" width="600" height="600"></canvas>
          <script>
          function clock(){
          var context=document.getElementById('canvas').getContext('2d');
          context.save();
          context.clearRect(0,0,150,150);
          context.translate(75,75);
          context.scale(0.4,0.4);
          context.rotate(-Math.PI/2);
          context.strokeStyle="black";
          context.lineWidth=8;
          context.lineCap="round";
          // 小時(shí)刻度
          context.save();
          for (var i=0;i<12;i++){
          context.beginPath();
          context.rotate(Math.PI/6);
          context.moveTo(100,0);
          context.lineTo(120,0);
          context.stroke();
          }
          context.restore();
          context.save();
          context.lineWidth=5;
          for (i=0;i<60;i++){
          if (i%5 !=0) {
          context.beginPath();
          context.moveTo(117,0);
          context.lineTo(120,0);
          context.stroke();
          }
          context.rotate(Math.PI/30);
          }
          context.restore();
          var now=new Date();
          var sec=now.getSeconds();
          var min=now.getMinutes();
          var hr=now.getHours();
          hr=hr>=12 ? hr-12 : hr;
          context.fillStyle="black";
          context.save();
          context.rotate(hr*(Math.PI/6) + (Math.PI/360)*min + (Math.PI/21600)*sec)
          context.lineWidth=14;
          context.beginPath();
          context.moveTo(-20,0);
          context.lineTo(80,0);
          context.stroke();
          context.restore();
          context.save();
          context.rotate((Math.PI/30)*min + (Math.PI/1800)*sec)
          context.lineWidth=10;
          context.beginPath();
          context.moveTo(-28,0);
          context.lineTo(112,0);
          context.stroke();
          context.restore();
          context.save();
          context.rotate(sec * Math.PI/30);
          context.strokeStyle="#D40000";
          context.fillStyle="#D40000";
          context.lineWidth=6;
          context.beginPath();
          context.moveTo(-30,0);
          context.lineTo(83,0);
          context.stroke();
          context.beginPath();
          context.arc(0, 0, 10, 0, Math.PI*2, true);
          context.fill();
          context.beginPath();
          context.arc(95, 0, 10, 0, Math.PI*2, true);
          context.stroke();
          context.beginPath();
          context.fillStyle="rgba(0,0,0,1)";
          context.arc(0,0,3,0,Math.PI*2,true);
          context.fill();
          context.restore();
          context.beginPath();
          context.lineWidth=14;
          context.strokeStyle='#325FA2';
          context.arc(0, 0, 142, 0, Math.PI*2, true);
          context.stroke();
          context.restore();
          window.requestAnimationFrame(clock);
          }
          window.requestAnimationFrame(clock);
          </script>

          示例:循環(huán)全景照片

          <canvas id="canvas" width="800" height="200"></canvas>
          <script>
          var img=new Image();
          img.src='images/park.jpg';
          var CanvasXSize=800, CanvasYSize=200;
          var speed=30;
          var scale=1.05;
          var y=-4.5;
          // 主程序
          var dx=0.75;
          var imgW, imgH;
          var x=0;
          var clearX, clearY;
          var ctx;
          img.onload=function() {
          imgW=img.width * scale;
          imgH=img.height * scale;
          if (imgW > CanvasXSize) {
          x=CanvasXSize - imgW; // -275.2
          clearX=imgW;
          } else {
          clearX=CanvasXSize;
          }
          if (imgH > CanvasYSize) {
          clearY=imgH;
          } else {
          clearY=CanvasYSize;
          }
          ctx=document.getElementById('canvas').getContext('2d');
          setInterval(draw, speed);
          }
          function draw() {
          ctx.clearRect(0, 0, clearX, clearY);
          if (imgW <=CanvasXSize) {
          if (x > CanvasXSize) {
          x=-imgW + x;
          }
          if (x > 0) {
          ctx.drawImage(img, -imgW + x, y, imgW, imgH);
          }
          if (x - imgW > 0) {
          ctx.drawImage(img, -imgW * 2 + x, y, imgW, imgH);
          }
          } else {
          if (x > CanvasXSize) {
          x=CanvasXSize - imgW;
          }
          if (x > (CanvasXSize - imgW)) {
          ctx.drawImage(img, x - imgW + 1, y, imgW, imgH);
          }
          }
          ctx.drawImage(img, x, y, imgW, imgH);
          x +=dx;
          }
          </script>

          示例:鼠標(biāo)追蹤動(dòng)畫(huà)

          <style>
          #cw {position: fixed; z-index: -1;}
          body {margin: 0; padding: 0; background-color: rgba(0,0,0,0.05);}
          </style>
          <canvas id="cw"></canvas>
          <script>
          var cn;
          //=document.getElementById('cw');
          var c;
          var u=10;
          const m={
          x: innerWidth / 2,
          y: innerHeight / 2
          };
          window.onmousemove=function(e) {
          m.x=e.clientX;
          m.y=e.clientY;
          }
          function gc() {
          var s="0123456789ABCDEF";
          var c="#";
          for (var i=0; i < 6; i++) {
          c +=s[Math.ceil(Math.random() * 15)]
          }
          return c
          }
          var a=[];
          window.onload=function myfunction() {
          cn=document.getElementById('cw');
          c=cn.getContext('2d');
          for (var i=0; i < 10; i++) {
          var r=30;
          var x=Math.random() * (innerWidth - 2 * r) + r;
          var y=Math.random() * (innerHeight - 2 * r) + r;
          var t=new ob(innerWidth / 2,innerHeight / 2,5,"red",Math.random() * 200 + 20,2);
          a.push(t);
          }
          //cn.style.backgroundColor="#700bc8";
          c.lineWidth="2";
          c.globalAlpha=0.5;
          resize();
          anim()
          }
          window.onresize=function() {
          resize();
          }
          function resize() {
          cn.height=innerHeight;
          cn.width=innerWidth;
          for (var i=0; i < 101; i++) {
          var r=30;
          var x=Math.random() * (innerWidth - 2 * r) + r;
          var y=Math.random() * (innerHeight - 2 * r) + r;
          a[i]=new ob(innerWidth / 2,innerHeight / 2,4,gc(),Math.random() * 200 + 20,0.02);
          }
          // a[0]=new ob(innerWidth / 2, innerHeight / 2, 40, "red", 0.05, 0.05);
          //a[0].dr();
          }
          function ob(x, y, r, cc, o, s) {
          this.x=x;
          this.y=y;
          this.r=r;
          this.cc=cc;
          this.theta=Math.random() * Math.PI * 2;
          this.s=s;
          this.o=o;
          this.t=Math.random() * 150;
          this.o=o;
          this.dr=function() {
          const ls={
          x: this.x,
          y: this.y
          };
          this.theta +=this.s;
          this.x=m.x + Math.cos(this.theta) * this.t;
          this.y=m.y + Math.sin(this.theta) * this.t;
          c.beginPath();
          c.lineWidth=this.r;
          c.strokeStyle=this.cc;
          c.moveTo(ls.x, ls.y);
          c.lineTo(this.x, this.y);
          c.stroke();
          c.closePath();
          }
          }
          function anim() {
          requestAnimationFrame(anim);
          c.fillStyle="rgba(0,0,0,0.05)";
          c.fillRect(0, 0, cn.width, cn.height);
          a.forEach(function(e, i) {
          e.dr();
          });
          }
          </script>

          常規(guī)動(dòng)畫(huà)設(shè)計(jì):
          繪制小球:

          <canvas id="canvas" width="600" height="300"></canvas>
          <script>
          var canvas=document.getElementById('canvas');
          var ctx=canvas.getContext('2d');
          var ball={
          x: 100,
          y: 100,
          radius: 25,
          color: 'blue',
          draw: function() {
          ctx.beginPath();
          ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
          ctx.closePath();
          ctx.fillStyle=this.color;
          ctx.fill();
          }
          };
          ball.draw();
          </script>

          添加速率并實(shí)現(xiàn)動(dòng)畫(huà):

          <canvas id="canvas" width="600" height="300"></canvas>
          <script>
          var canvas=document.getElementById('canvas');
          var ctx=canvas.getContext('2d');
          var rafID;
          var ball={
          x: 100,
          y: 100,
          vx: 5,
          vy: 2,
          radius: 25,
          color: 'blue',
          draw: function() {
          ctx.beginPath();
          ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
          ctx.closePath();
          ctx.fillStyle=this.color;
          ctx.fill();
          }
          };
          function drawAnimation() {
          ctx.clearRect(0,0, canvas.width, canvas.height);
          ball.draw();
          ball.x +=ball.vx;
          ball.y +=ball.vy;
          rafID=window.requestAnimationFrame(drawAnimation);
          }
          canvas.addEventListener('mouseover', function(e){
          rafID=window.requestAnimationFrame(drawAnimation);
          });
          canvas.addEventListener('mouseout', function(e){
          window.cancelAnimationFrame(rafID);
          });
          ball.draw();
          </script>

          運(yùn)動(dòng)邊界:

          // 在drawAnimation()函數(shù)中添加
          if (ball.y + ball.vy > canvas.height || ball.y + ball.vy < 0) {
          ball.vy=-ball.vy;
          }
          if (ball.x + ball.vx > canvas.width || ball.x + ball.vx < 0) {
          ball.vx=-ball.vx;
          }
          rafID=window.requestAnimationFrame(drawAnimation);

          設(shè)置加速度:

          ball.vy *=.99;
          ball.vy +=.25;
          // 添加到drawAnimation()函數(shù)中
          ball.vy *=.99;
          ball.vy +=.25;
          ball.x +=ball.vx;
          ball.y +=ball.vy;

          長(zhǎng)尾效果:

          // ctx.clearRect(0,0, canvas.width, canvas.height);
          // 如:在drawAnimation中用以下代碼替代掉上方的clearRect()方法
          ctx.fillStyle='rgba(255,255,255,0.3)';
          ctx.fillRect(0,0,canvas.width,canvas.height);
          ball.draw();

          添加鼠標(biāo)控制:

          // ...
          // 添加一個(gè)全局變量,用于判斷是否正在運(yùn)動(dòng)
          var running=false;
          // ...
          function clear() {
          ctx.fillStyle='rgba(255,255,255,0.3)';
          ctx.fillRect(0,0,canvas.width,canvas.height);
          }
          canvas.addEventListener('mousemove', function(e){
          if(!running){
          clear();
          ball.x=e.offsetX;
          ball.y=e.offsetY;
          ball.draw();
          }
          });
          canvas.addEventListener('mouseout', function(e){
          window.cancelAnimationFrame(rafID);
          running=false;
          });
          canvas.addEventListener('click',function(e){
          if (!running) {
          rafID=window.requestAnimationFrame(drawAnimation);
          running=true;
          }
          });
          // ...

          使用 canvas 處理視頻:
          通過(guò)canvas和video,可以實(shí)時(shí)地操縱視頻數(shù)據(jù)來(lái)合成各種視覺(jué)特效,并把結(jié)果呈現(xiàn)到視頻畫(huà)面中;
          例如:色度鍵控(也被稱為“綠屏效果”)

          <style>
          body {background: black; color:#CCCCCC;}
          #c2 {background-image: url(url("images/logo.png")); background-repeat: no-repeat;}
          div {float: left; border :1px solid #444444;
          padding:10px; margin: 10px; background:#3B3B3B;}
          </style>
          <div>
          <video id="video" src="images/video.ogv" controls="true"/>
          </div>
          <div>
          <canvas id="c1" width="160" height="96"></canvas>
          <canvas id="c2" width="160" height="96"></canvas>
          </div>

          JavaScript:main.js

          var processor={};
          processor.doLoad=function doLoad() {
          this.video=document.getElementById('video');
          this.c1=document.getElementById('c1');
          this.ctx1=this.c1.getContext('2d');
          this.c2=document.getElementById('c2');
          this.ctx2=this.c2.getContext('2d');
          var self=this;
          this.video.addEventListener('play', function() {
          self.width=self.video.videoWidth / 2;
          self.height=self.video.videoHeight / 2;
          self.timerCallback();
          }, false);
          },

          主頁(yè)面:

          <script src="video_main.js"></script>
          <script>
          window.onload=processor.doLoad();
          </script>

          實(shí)現(xiàn)計(jì)時(shí)器回調(diào)timerCallback()方法:

          processor.timerCallback=function timerCallback() {
          if (this.video.paused || this.video.ended) {
          return;
          }
          this.computeFrame();
          var self=this;
          setTimeout(function() {
          self.timerCallback();
          }, 0);
          },

          實(shí)現(xiàn)computeFrame()方法用來(lái)操作視頻幀數(shù)據(jù):

          processor.computeFrame=function computeFrame() {
          this.ctx1.drawImage(this.video, 0, 0, this.width, this.height);
          var frame=this.ctx1.getImageData(0, 0, this.width, this.height);
          var l=frame.data.length / 4;
          for (var i=0; i < l; i++) {
          var r=frame.data[i * 4 + 0];
          var g=frame.data[i * 4 + 1];
          var b=frame.data[i * 4 + 2];
          if (g > 100 && r > 100 && b < 43)
          frame.data[i * 4 + 3]=0;
          }
          this.ctx2.putImageData(frame, 0, 0);
          return;
          }
          canvas 的優(yōu)化:
          避免浮點(diǎn)數(shù)的坐標(biāo)點(diǎn),應(yīng)該使用整數(shù):
          當(dāng)在繪制一個(gè)沒(méi)有整數(shù)坐標(biāo)點(diǎn)的圖形時(shí)會(huì)發(fā)生子像素渲染;
          ctx.drawImage(myImage, 0.3, 0.5);

          瀏覽器為了達(dá)到抗鋸齒的效果會(huì)做額外的運(yùn)算;為了避免這種情況,應(yīng)該用Math.floor()函數(shù)對(duì)所有的坐標(biāo)點(diǎn)取整;

          盡量不要在用drawImage時(shí)縮放圖像:
          因?yàn)闉g覽器一樣需要額外的運(yùn)算,去處理縮放后的圖像;

          避免使用多層畫(huà)布去畫(huà)一個(gè)復(fù)雜的場(chǎng)景:
          在有些應(yīng)用中,可能某些圖形需要經(jīng)常移動(dòng)或更改,而其他對(duì)象則保持相對(duì)靜態(tài);在這種情況下,可以使用多個(gè)<canvas>元素對(duì)項(xiàng)目進(jìn)行分層;

          <style>
          #stage {
          width: 480px; height: 320px;
          position: relative; border: 2px solid black
          }
          canvas { position: absolute; }
          #ui-layer { z-index: 3 }
          #game-layer { z-index: 2 }
          #background-layer { z-index: 1 }
          </style>
          <div id="stage">
          <canvas id="ui-layer" width="480" height="320"></canvas>
          <canvas id="game-layer" width="480" height="320"></canvas>
          <canvas id="background-layer" width="480" height="320"></canvas>
          </div>

          用CSS設(shè)置大的背景圖:
          可以避免在每一幀在畫(huà)布上繪制大圖;

          用CSS transforms特性縮放畫(huà)布:
          因?yàn)镃SS transforms使用 GPU,速度很快,所以最好的情況是不直接縮放畫(huà)布;

          var scaleX=window.innerWidth / canvas.width;
          var scaleY=window.innerHeight / canvas.height;
          var scaleToFit=Math.min(scaleX, scaleY);
          var scaleToCover=Math.max(scaleX, scaleY);
          stage.style.transformOrigin='0 0';
          stage.style.transform='scale(' + scaleToFit + ')';

          關(guān)閉透明度:
          如果畫(huà)布不需要透明,當(dāng)使用getContext()方法創(chuàng)建一個(gè)繪圖上下文時(shí)把 alpha 選項(xiàng)設(shè)置為 false,這個(gè)選項(xiàng)可以幫助瀏覽器進(jìn)行內(nèi)部?jī)?yōu)化;

          var ctx=canvas.getContext('2d', { alpha: false });

          其它建議:

          • 將畫(huà)布的函數(shù)調(diào)用集合到一起;
          • 避免不必要的畫(huà)布狀態(tài)改變;
          • 渲染畫(huà)布中的不同點(diǎn),而非整個(gè)新?tīng)顟B(tài);
          • 盡可能避免使用shadowBlur;
          • 盡可能避免使用text rendering
          • 有動(dòng)畫(huà),盡量使用requestAnimationFrame()而非setInterval();

          主站蜘蛛池模板: 亚洲乱码一区二区三区在线观看 | 国产成人精品一区二区三在线观看| 日本精品一区二区三区四区| 波多野结衣一区视频在线| 无码人妻精品一区二区三区夜夜嗨| 精品在线一区二区| 一区二区国产精品| 成人精品一区二区三区中文字幕| 国产成人av一区二区三区不卡 | 丰满爆乳一区二区三区| 韩日午夜在线资源一区二区| 国产精品一区二区久久精品无码| 国产成人无码精品一区在线观看| av无码精品一区二区三区四区| 亚洲国模精品一区| 亚洲乱码一区二区三区在线观看| 久久99国产精品一区二区| 精品国产一区二区三区久久久狼| 国产日韩精品视频一区二区三区| 国产一区二区久久久| 精品一区二区三区在线观看l| 亚洲熟妇AV一区二区三区宅男| AV天堂午夜精品一区二区三区| 三级韩国一区久久二区综合| 国产激情一区二区三区 | 日本不卡在线一区二区三区视频| 亚洲视频一区在线| 精品一区二区三区中文| 久久99精品国产一区二区三区 | 日韩在线一区二区三区免费视频| 国产激情无码一区二区三区| 久久久久人妻一区二区三区| 国产精品一区二区在线观看| 熟女少妇丰满一区二区| 人妻av综合天堂一区| 狠狠色婷婷久久一区二区三区| 精品国产鲁一鲁一区二区| 一区二区三区在线观看免费| 国产对白精品刺激一区二区| 午夜无码视频一区二区三区| 91在线一区二区三区|