畫布坐標、屏幕坐標的概念
幾何變換:平移、縮放、旋轉
Canvas 中的所有幾何變換針對的不是繪制的圖形,而是針對畫布本身,也就是說,當移動、縮放、旋轉畫布之后,新的坐標系只對新的操作生效
參考:
示例代碼
const canvas = document.getElementById("canvas");
const context = convas.getContext("2d");
context.fillRect(100, 100, 50, 50);
Canvas的繪制和html的繪制是不一樣的,html的繪制是增量的,當變化時,只會重新繪制變化的部分,沒有變化的部分是不會重新繪制的,但是canvas不一樣,每次都是全量繪制的,如果一個canvas里有很多圖形,當改變一個圖形時,需要重新繪制所有圖形才可以(當然,可以用clearRect擦除部分區域,但一般很少這么用)。
了解canvas的繪制規則之后,就很容易發現性能問題,如果canvas上繪制了大量的圖形(成千上萬個),每次重繪就需要很長的時間,如果重繪的頻率很高,那么就會有性能問題
那么如何解決這個問題呢,目前有以下幾種方案
圖層的概念來自于PS,每一個圖層都是一個canvas,既然在一個canvas上繪制太多圖形會有性能問題,那么就分幾個圖層,每次僅重新繪制其中一個圖層,每個圖層的圖形都不會很多,那么即使重繪的頻率很高,也不會有性能問題。圖層的概念圖如下:
這里用背景顏色只是示意,實際上圖層都是透明
代碼實現
用一個父元素作為容器,把所有的元素設置成一樣的寬高并放在里面重疊。
<div class="container">
<canvas width="500" height="500"></canvas>
<canvas width="500" height="500"></canvas>
<canvas width="500" height="500"></canvas>
<canvas width="500" height="500"></canvas>
<canvas width="500" height="500"></canvas>
</div>
繪制是很耗性能的,如果每次都清空畫布然后重新畫一次,那么性能會消耗很大(即使分了幾個圖層),我們應區分“變”與“不變”的部分,只對“變”的部分重新渲染,“不變”的部分不渲染,將經常變化的部分抽離到臨時圖層,這樣僅需要渲染臨時圖層,臨時圖層有幾種實現思路,一種是使用操作圖層(俗稱高性能圖層),一種是使用隱藏圖層(不繪制到界面上的)
高性能圖層
一般高頻(實時響應鼠標、鍵盤等事件)的操作會放在高性能圖層,等操作完成之后,再將最終結果保存到其它圖層,比如繪制、拖拽、縮放一個(或一批)shape
隱藏圖層
有些圖層是不用給用戶看的,這些canvas僅存在于內存中,不會插入html的dom中,用完就銷毀,比如常見的canvas to image。
還有一種實現方式是離屏渲染(OffscreenCanvas),先在一個offCanvas操作,然后再將結果渲染到界面上(有點像虛擬dom操作),一般會結合webworker或webassembly
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
// 繪制圖片,或其它操作
context.drawImage();
// 轉成base64圖片
convas.toDataUrl();
影響canvas性能的除了繪制頻率,還有一個重要的是像素點操作,一般圖像處理會涉及到大量的像素點操作,如果放在主線程計算,那么會卡住其它操作,造成頁面卡頓,特別影響用戶體驗,這些涉及大量計算的一般會單獨開個線程來操作,而在瀏覽器中有這個能力的就只有webworker了。
有了webworker可能還不夠,因為始終是在js上執行,js執行效率天生就比其它語言慢,所以一般的會使用webassembly,執行效率比js快很多,而且還能用到更豐富的圖像處理庫
如果還有更高的性能要求,那么普通的2d canvas可能就無法滿足了,這個時候可以使用webgl,性能更高(當然學習成本也更高),再結合wasm,就可以有無限想象力了,鼎鼎大名的figma就是用webgl + wasm(rust)實現的,另外google doc在線文檔也使用了webgl,飛書文檔將來也會替換成wegbl,基于瀏覽器的渲染始終有諸多限制,一般有能力的都會實現自己的渲染引擎。
假設canvas大小為(867,350)
圖片的大小為(768,576)
將上面這張圖片放到canvas中,圖片貼邊處理,也即圖片太大就縮小,圖片太小就放大。那么我們如何實現這種效果呢?
總結一下,總共分為幾步:
canvas的執行細節如下:
在本文中,了解如何使 HTML 圖像更適合具有響應式尺寸和現代格式的用戶,而不會使您作為開發人員的生活更加困難。
假設我們只有一個從我的域加載圖像的基本網站。代碼可能如下所示:
<!DOCTYPE html><html><正文> <img src="https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png" alt="命令行終端和舊電腦,背景中有靛藍色筆觸。" ></正文></html>具有單個圖像標記且具有 asrc和alt屬性的 HTML 文檔在技術上可以正常工作。它按預期提供圖像,但該圖像存在一些問題。
在大屏幕上,這張圖片會很好,但對于使用小屏幕設備的用戶來說,1200 像素寬的圖片意味著他們必須下載比他們需要的更大的圖片。下載可能需要更長的時間,并且可能會花費他們的數據計劃費用。
這第一步很關鍵,但我認為大多數人都熟悉,所以我不會太深入。
Squoosh是一個非常棒的應用程序,可以手動執行此操作。
如果圖像在您的 GitHub 存儲庫中,您也可以使用imgbot自動完成。
圖像可以有一個srcset屬性,允許我們根據某些設備特征(例如設備寬度)為圖像定義多個來源。
我們可以srcset像這樣向我們的圖像標簽添加一個屬性:
<!DOCTYPE html><html><正文> <img srcset=" https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-1080x540.png 1080w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-480x240.png 480w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-200x100.png 200w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-768x384.png 768w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png 1200w" src="https://cdn.statically.io/img/austingil.com/f=auto%2Cq=70/wp-content/uploads/Command-Line-Blog-Cover.png" alt="命令行終端和舊電腦,背景中有靛藍色筆觸。" ></正文></html>我鏈接到五個不同尺寸的五個不同圖像并定義它們相應的視口寬度。如果我們在瀏覽器中打開我們的圖像,視覺上并沒有真正改變。但在引擎蓋下,有一些改進。
當我們在不同的屏幕上重新加載時,打開開發工具網絡選項卡。
在大屏幕上,我們將看到像以前一樣下載了全尺寸圖像(Command-Line-Blog-Cover.png)。沒什么特別的。
但是,如果我們從 200 像素或更窄的屏幕開始,我們會加載 200x100 像素的圖像(Command-Line-Blog-Cover-200x100.png)。
對于用戶而言,該大小將變得更小更快。
僅發送較小圖像的一個問題是:如果用戶最初在小屏幕上加載頁面,然后將瀏覽器調整為更大的尺寸會發生什么?在大屏幕上拉伸小圖像會使它像素化。
屬性不會發生這種情況srcset(假設我們提供了正確的圖像),因為當從最小屏幕調整到最大屏幕時,會根據它們的斷點下載新圖像。
方便的是,從大屏幕到小屏幕的另一個方向不會發生相同的行為,因為畢竟,您可以在較小的屏幕上提供大圖像,并且可以縮小而不損失質量。隨著屏幕變小,您不想下載額外的圖像,因為那樣會使用更多數據而沒有任何額外的好處。
這是一種非常方便的方法,可以通過節省帶寬來改善用戶體驗,并通過以正確的尺寸提供正確的圖像來提高性能,但仍有改進的空間。
我們正在加載的圖像是 PNG,但現代瀏覽器支持新的圖像格式,例如WebP或AVIF。這些格式提供了更高的壓縮率而沒有明顯的數據丟失,這意味著我們可以以更小的文件大小有效地提供相同的圖像。
再一次,瀏覽器讓我們覆蓋了HTML<picture>元素。
類似于srcset屬性,圖片元素允許我們根據設備特性定義不同的圖像源來服務。
所以我們可以做一些事情,比如根據設備寬度或不同的像素密度提供不同的圖像。但是將圖片元素與屬性區分開來的一件事srcset是我們可以針對不同的 mime 類型。
如果瀏覽器支持,讓我們在示例中添加一個包含 AVIF 和 WebP 格式的圖片元素。當然,我們還希望繼續提供響應式版本。
<!DOCTYPE html><html><正文> <圖片> <來源 類型="圖像/avif" srcset=" https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover.avif 1200w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-1080x540.avif 1080w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-768x384.avif 768w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-480x240.avif 480w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-200x100.avif 200w" > <來源 類型="圖像/webp" srcset=" https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover.webp 1200w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-1080x540.webp 1080w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-768x384.webp 768w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-480x240.webp 480w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-200x100.webp 200w" > <img src="https://cdn.statically.io/img/austingil.com/f=auto%2Cq=70/wp-content/uploads/Command-Line-Blog-Cover.png" alt="命令行終端和舊電腦,背景中有靛藍色筆觸。" > </圖片></正文></html>(我們不需要標簽srcset,<img>因為它可以作為不支持的舊瀏覽器的后備srcset。)
好的,我們有一個圖像,它只會以最小的尺寸為用戶提供最現代的圖像格式。
上面的代碼是不是……很漂亮?
真的讓您想向您的網站添加新圖像,對嗎?
正確的!?!?
不!我們做了什么!?!?
(你好黑暗,我的老朋友……)
好的,既然我們已經解決了這個問題,很明顯,上面的解決方案并不是很好。
對于我們想要定位的每種格式和設備寬度,我們需要不同版本的圖像。在上面的示例中,這是同一圖像的11 個版本(5 個 AVIF、5 個 WebP、1 個 PNG)。
盡管瀏覽器為我們提供了提供更好的圖像和提供更好的用戶體驗的功能,但這是以開發人員體驗為代價的。
(誰愿意付錢?不是我!)
我們可以在上傳圖像以生成不同的格式和大小時自動執行該過程,然后將這些詳細信息存儲在數據庫中。有一些工具可以提供幫助,例如sharp,但這仍然需要大量工作。我很少看到人們這樣做,因為要么工作量太大,要么他們根本不考慮。
我的建議是外包。
今天我將使用 Akamai 圖像和視頻管理器,因為它是我最熟悉的服務,盡管還有其他人做同樣的工作。主要目標是在不讓開發人員發瘋的情況下為用戶提供最佳圖像。
我已經設置了一個圖像管理器的實例。
要從我的域中添加圖像,我只需將“圖像”前綴添加到常規圖像 URL。所以“austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png”變成了“images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png”。
沒什么太有趣的。
但是讓我們回到我們的代碼,去掉<picture>元素,替換舊的 URL,看看我們能做什么:
<!DOCTYPE html><html><正文> <img srcset=" https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover-1080x540.png 1080w, https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover-480x240.png 480w, https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover-200x100.png 200w, https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover-768x384.png 768w, https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png 1200w" data-fr-src="https://cdn.statically.io/img/austingil.com/f=auto%2Cq=70/wp-content/uploads/Command-Line-Blog-Cover.png" alt="命令行終端和舊電腦,背景中有靛藍色筆觸。" ></正文></html><picture>與將元素與 AVIF 和 WebP一起使用相比,我認為沒有人會抱怨這一點。這肯定是少了一些工作,但是結果呢?
如果我重新加載頁面,我們可以看到相同的圖像出現(“Command-Line-Blog-Cover.png”),但如果我們查看響應標題,事情會變得有點有趣。
盡管請求是針對 PNG 圖像的,但響應實際上是發送 WebP 數據。這導致圖像尺寸更小,這意味著只需打開圖像管理器,我就已經為我的用戶節省了帶寬,而無需做任何事情。
我們已經改進了格式,但是響應式圖像大小呢?
我仍然將srcset屬性設置為在小屏幕上提供小圖像,在大屏幕上提供大圖像。但是這個系統仍然依賴于提供五種不同分辨率的五種不同的圖像。
還是很痛的。
僅提供上傳功能就可以成為一項重要功能,更不用說調整大小和存儲不同的圖像了。對我們來說幸運的是,因為我們正在使用圖像管理器,我們實際上可以使這個過程變得更簡單。
我實際上將多次引用同一個圖像,而不是使用五個不同大小的圖像。但我將附加一個查詢字符串參數,該參數明確定義我想要定位的大小。
<!DOCTYPE html><html><正文> <img srcset=" https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png?imwidth=1080 1080w, https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png?imwidth=480 480w, https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png?imwidth=200 200w, https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png?imwidth=768 768w, https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png 1200w" src="https://cdn.statically.io/img/austingil.com/f=auto%2Cq=70/wp-content/uploads/Command-Line-Blog-Cover.png" alt="命令行終端和舊電腦,背景中有靛藍色筆觸。" ></正文></html>請注意,現在每個圖像 URL 都轉到相同的“Command-Line-Blog-Cover.png”文件,但其中一些imwidth附加了一個額外的查詢字符串。
如果我像以前一樣在這個小屏幕上重新加載并調整到更大的屏幕,我們會看到類似的行為。不同尺寸的不同圖像被加載到它們各自的屏幕尺寸中。
但是這里有一個很大的區別:我們不必創建、上傳和管理五張不同大小的不同圖像,我們只需要擔心一張圖像。通過 queer 字符串參數生成不同的大小。
與其他情況相比,它的工作量要少得多,尤其是在您使用基于組件的框架時。
但我們可以做的工作更少!
我們可以回到只使用圖像 URL,并讓圖像管理器為我們選擇合適的尺寸。
為了展示它,我喜歡在新標簽而不是網站中打開圖像。將鼠標懸停在瀏覽器選項卡上會顯示圖像的尺寸,1200x600px。
如果我打開我的開發工具,我可以轉到Elements 選項卡并模擬不同的設備,例如 iPhone SE:
現在,當我重新加載時會發生一些有趣的事情。
盡管 URL 完全相同,但我可以看到圖像大約是一半大小。將鼠標懸停在標簽上可確認尺寸為 640x320 像素。
最好的部分是什么?確定要提供哪個圖像的所有工作都是在我(開發人員)無需做任何事情的情況下進行的。每次請求都會自動發送設備特征,并且 Image Manager 會以更小的圖像進行響應。
老實說,我實際上無法解釋它是如何工作的。這很神奇,而且超級酷,讓我的生活更輕松,讓我的用戶生活得更好。
(如果你真的很好奇它是如何工作的,請在 Twitter 上聯系我,我會為你找出答案。)
如果您是 Akamai 客戶并且您沒有使用此功能,您應該立即使用它!
我還想為非 Akamai 客戶的人們分享一些替代方案。有兩個開源項目看起來提供了類似的功能,盡管我沒有親自使用過它們。
其中之一稱為imgproxy。另一個稱為Thumbor。只要您習慣使用 Docker,它們看起來都很容易安裝。
我真的很喜歡Linode的廉價、強大的 VP。
我是 DIY 方法的忠實擁護者,但我認為它并非沒有缺點(我的意思是,除了設置和維護之外)。我不確定這些服務在哪里托管圖像。如果它們與安裝托管在同一臺服務器上,則可能存在兩個問題。
也就是說,我認為它仍然值得一試。
如果你確實讓它工作并且你喜歡它,請回來告訴我它是怎么回事。什么進展順利,什么沒用,什么很棒,和/或缺少什么?
好吧,這就是我今天為你準備的全部內容。我們介紹了處理圖像的不同選項及其優缺點。我們從<img>一個src屬性開始,添加一個srcset屬性,然后<picture>使用不同的<source>標簽,最終把它全部燒掉,回到只是屬性,但合并了圖像管理器<img>。src
換句話說,這是一個非常迂回的旅程:
<! – 壞 – ><img src="https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png" alt="命令行終端和舊電腦,背景中有靛藍色筆觸。">對此:
<! – 好 – ><img data-fr-src="https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png" alt="命令行終端和舊電腦,背景中有靛藍色筆觸。">多么虎頭蛇尾。
可能本來可以節省時間,然后說“改善圖像的第一步:使用圖像管理器”,但其中的樂趣在哪里?我也喜歡挖掘原因。
我們甚至沒有涉及諸如延遲加載、解碼或獲取優先級之類的內容,但這些內容更具上下文和細微差別,而上面的建議幾乎是全面的。
無論如何,我希望您喜歡它,并且希望您實施其中一些解決方案,因為發送具有現代格式的較小圖像是使互聯網成為更快、更環保的地方的好方法。
下截圖:
點擊文件選擇框,我們不妨選一張尺寸比較大的圖片,例如下面這種2M多的釣魚收獲照:
于是圖片歘歘歘地傳上去了:
此時我們點擊最終上傳完畢的圖片地址,會發現原來2M多3000多像素寬的圖片被限制為400像素寬了:
保存到本地會發現圖片尺寸已經變成只有70K了:
以上就是圖片前端壓縮并上傳demo的完整演示。
二、實現原理
要想使用JS實現圖片的壓縮效果,原理其實很簡單,核心API就是使用canvas的drawImage()方法。
Canvas本質上就是一張位圖,而drawImage()方法可以把一張大大的圖片繪制在小小的Canvas畫布上,不久等同于圖片尺寸壓縮了?
對于本案例的壓縮,使用的5個參數的API方法:
context.drawImage(img, dx, dy, dWidth, dHeight);復制代碼
各參數具體含義可以參見“Canvas API中文文檔-drawImage”,這里不展開。
舉例:
一張圖片(假設圖片對象是img)的原始尺寸是4000*3000,現在需要把尺寸限制為400*300大小,很簡單,原理如下代碼示意:
var canvas = document.createElement('canvas'); var context = canvas.getContext('2d'); canvas.width = 400; canvas.height = 300; // 核心JS就這個 context.drawImage(img,0,0,400,300);復制代碼
把大圖片畫在一張小畫布上,壓縮就這么實現了,是不是簡單的有點超乎想象。
三、如果想要上傳或下載?
如果想要上傳圖片或者下載圖片,可以使用canvas.toDataURL()或者canvas.toBlob()方法先進行轉換。
1. canvas.toDataURL()
語法如下:canvas.toDataURL(mimeType, qualityArgument)復制代碼
可以把畫布轉換成base64格式信息圖像信息,純字符的圖片表示法。
其中:
mimeType表示canvas導出來的base64圖片的類型,默認是png格式,也即是默認值是'image/png',我們也可以指定為jpg格式'image/jpeg'或者webp等格式。file對象中的file.type就是文件的mimeType類型,在轉換時候正好可以直接拿來用(如果有file對象)。
qualityArgument表示導出的圖片質量,只要導出為jpg和webp格式的時候此參數才有效果,默認值是0.92,是一個比較合理的圖片質量輸出參數,通常情況下,我們無需再設定。
更多關于toDataURL()方法的信息可以參見“Canvas API中文文檔-toDataURL()”。
2. canvas.toBlob()方法
語法如下:canvas.toBlob(callback, mimeType, qualityArgument)復制代碼
可以把畫布轉換成Blob文件,通常用在文件上傳中,因為是二進制的,對后端更加友好。
和toDataURL()方法相比,toBlob()方法是異步的,因此多了個callback參數,這個callback回調方法默認的第一個參數就是轉換好的blob文件信息,本文一開始的demo案例中的文件上傳就是將canvas圖片轉換成二進制的blob文件,然后再ajax上傳的,代碼如下:
// canvas轉為blob并上傳 canvas.toBlob(function (blob) { // 圖片ajax上傳 var xhr = new XMLHttpRequest(); // 開始上傳 xhr.open("POST", 'upload.php', true); xhr.send(blob); });復制代碼
更多關于toBlob()方法的信息可以參見“Canvas API中文文檔-toBlob()”。
一旦有了可傳輸的圖像數據,上傳下載就好實現了。例如下載前端壓縮好的圖片,可以參考我上一篇在掘金發布的文章:“純JS生成并下載各種文本文件或圖片”。
四、總結
經過“圖片→canvas壓縮→圖片”三步曲,我們完成了圖片前端壓縮功能。
作者:張鑫旭
鏈接:https://juejin.im/post/5bec3c6cf265da614312a0fa
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。