經過了連續5篇複雜度略高的物理模擬系列,我在想看官們多少會有點疲乏~
所以我在規劃了幾篇『中場休息』系列科普文,用來穿插在主要的chapter之間,
休息是為了走更長遠的路,還有看更多的物理模擬(X
主要內容會講一些比較容易理解~一篇之內就可以講完的案例。
這篇文是『中場休息』系列的第一篇文,我們這次會講講怎麼樣在Canvas上實作 html 轉圖像的功能。
大部分人提到htmlToCanvas的實作,應該會直接想到html2Canvas這個著名的NPM包,但是我們秉持著NINJA精神當然不能光會用別人寫的包,所以我們就來看看這個案例的實作原理。
其實htmlToCanvas的實作在這一篇MDN的舊版文章(已經被封存)裡面有提到過做法。
這邊直接把源碼貼上來:
<canvas id="canvas" style="border:2px solid black;" width="200" height="200"></canvas>
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var data = '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">' +
'<foreignObject width="100%" height="100%">' +
'<div xmlns="http://www.w3.org/1999/xhtml" style="font-size:40px">' +
'<em>I</em> like <span style="color:white; text-shadow:0 0 2px blue;">cheese</span>' +
'</div>' +
'</foreignObject>' +
'</svg>';
var DOMURL = window.URL || window.webkitURL || window;// 這是一個防呆,因為不同瀏覽器的createObjectURL方法可能存在於不同對象底下。
var img = new Image();
var svg = new Blob([data], {type: 'image/svg+xml;charset=utf-8'});
var url = DOMURL.createObjectURL(svg);
img.onload = function () {
ctx.drawImage(img, 0, 0);
DOMURL.revokeObjectURL(url);//這個api是用來銷毀已經用不到的URL,避免記憶體消耗
}
img.src = url;
codepen連結: https://codepen.io/mizok_contest/pen/RwgdpgR
在上面這個案例我們可以看到,html to canvas的實作流程大致如下:
foreignObject tag 塞到svg內部Blob類的建構式去把svg字串轉換成Blob物件URL.createObjectURL(Blob) 去取得轉化出來的Blob物件的URL
這邊我們就每個步驟稍微說明一下~
首先我們來講講foreignObject 是什麼。
有自己寫過svg的同學應該很清楚,svg的原生元素是沒有辦法做文字段落自動換行的,一般要換行的話,我們只能透過把斷行的部分寫成多個<tspan>,然後手動指定tspan的座標值,讓他看起來像換到下一行
(聽起來很笨,但是確實就是這樣)
延伸閱讀:tspan 在MDN上的介紹頁面: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/tspan
而foreignObject的存在意義其實就是可以讓我們在svg的XML namespace(命名空間)底下去把不同namespace的結構語言給渲染出來,像這樣透過使用foreignObject,我們就可以輕鬆地在svg內部實現文字換行~
XML 指的是結構性語言,html/svg都是一種XML,但是他們只能在特定的namespace底下被渲染,不同的namespace底下會有不同的渲染邏輯~
透過使用foreignObject把html的內容埋進去svg裡面,這樣我們就得到了一張具有html外觀的svg了~大概可以這樣理解。
Blob其實是一種 類檔案(File-like)的物件。
舉個例來說~我們常常看到有些網頁會有利用<input type="file">做檔案上傳的功能,
這些input在on change時接到的東西其實就是Blob的一種。
而我們這邊則是透過手動把svg字串傳到Blob類的建構式,來建立一個全新的Blob實例。
new Blob(array, options);
Blob 的第一個參數固定要傳一個陣列,而陣列的內容可以允許傳入字串(也可以傳入其他的東西,可以讀MDN上的解釋)
延伸閱讀: MDN 上關於 Blob()的頁面:https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob
之所以用Blob 是因為後面需要用createObjectURL(Blob) 來取得Blob實例的URL。
延伸閱讀: MDN 上關於 createObjectURL(Blob)的頁面:https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
如果不想要用Blob來取得image src url,其實也是可以直接把埋入foreignObject的svg string直接寫入<img>的src attribute,就像這樣:
<img width="600" height="450" src='data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg"><foreignObject width="100%" height="100%"><body xmlns="http://www.w3.org/1999/xhtml"><span style="color:red">123123123</span></body></foreignObject></svg>'>
其實ctx.drawImage我們之前有提到過(在像素操作概論的篇章)
可以看這邊
他就是2DContext所提供的一個用來把圖片畫到Canvas 的api
而這邊就是把前面拿到的Blob url 去賦予給 Image.src,然後再把這張圖片給畫出來~
到這邊為止我們大概可以理解htmlToCanvas的實作,但是以實際場景來講其實很多時候不會像我們上面給的範例一樣這麼簡單。
打個比方,例如典型的跨域問題,導致我們在程序中無法順利取得正確的圖片/樣式表,除此之外,因為為了要防堵資安漏洞,利用foreignObject 去取得html的渲染畫面這一操作其實有很多限制,例如:
foreignObject中引入js文件,這意味著有些透過js生成的樣式變成需要手動賦予到foreignObject的html元素上而為了應對各種複雜的截圖需求,才會有了html2Canvas這樣的插件,這個插件實際上是用了很多比較tricky的方法去繞過防堵機制來達成部分因為上述限制而難以實行的問題,但是也因為這樣這個插件當然也就會有被認定為bug的狀況。
到這邊為止是這次的html2Canvas實作介紹~希望大家喜歡 :D