經過了連續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