svg實戰 - 繪製海報

由於在電商公司,常常作一些h5的活動,須要實現分享海報功能。海報上會有一些我的定製信息,好比得分、評語等,和背景圖合成在一塊兒,作成一張圖用於分享或下載。由於每一個人字數不同,讓內容繪製在合適的位置,就須要一些「手段」。javascript

效果圖
如上圖中,用戶名、已兌換金額是用戶當前活動數據,每一個人不同,字符串長度也就不同。但須要讓它們居中顯示。咱們常常會用到三種方案,各有利弊: 1. 直接使用canvas繪製。須要使用measureText函數得到文本顯示的寬度,而後計算它應該繪製的位置; 2. 使用dom佈局,配合canvas繪製,讓瀏覽器的佈局能力「幫忙」得到字符的合適位置; 3. 使用svg佈局,直接生成圖片。

方案一:純canvas繪製

直接用canvas來合成這張海報,是咱們最熟悉的方案。大體步驟以下:css

  1. 扣出不可變的背景圖,見下面。
  2. 使用measureText得到要繪製內容的寬度,而後計算應該繪製的位置,經過fillText繪製到畫布上
  3. 繪製完全部內容後,使用toDataURL或者toBlob接口,得到圖片內容,上傳到服務器或者傳給接口。
背景圖

核心代碼以下html

async function drawPoster(info){
	// ... 一些準備代碼

	// 1. 得到「已兌換」字符串的長度。注意,須要先設置字體。另外,「元現金」的寬度咱們認爲和它同樣
	ctx.font = '42px "Kaiti SC"';
	let textWidth = ctx.measureText('已兌換').width;
	let spaceWidth = 40;
	// 2. 如法得到金額的寬度
	ctx.font = '140px "Kaiti SC"';
	let moneyWidth = ctx.measureText(info.money).width;
	// 3. 文案的總寬度。兩段普通文案,兩個普通文案和金額之間的空白,加金額的寬度
	let totalWidth = textWidth*2+spaceWidth*2+moneyWidth;
	// 4. 繪製先後兩段普通文案
	ctx.textAlign = 'start';
	ctx.textBaseline = 'alphabetic';
	ctx.font = '42px "Kaiti SC"';
	ctx.fillText('已兌換', (posterWidth-totalWidth)/2, 390);
	ctx.fillText('元現金', posterWidth-(posterWidth-totalWidth)/2-textWidth, 390);
	// 5. 繪製金額
	ctx.font = '140px "Kaiti SC"';
	ctx.fillStyle='#a70322';
	ctx.fillText(info.money, (posterWidth-moneyWidth)/2, 400);

	// ... 其它處理代碼
}
複製代碼

demo地址 查看完整源碼java

上面代碼片段繪製了「已兌換1.50元現金」的文案。爲了讓這段文字居中,就須要用measureText得到每一段文本的長度,而後根據總寬度,當心計算它們應該的渲染位置。例子中的文字比較少,若是多了;或者樣式複雜的時候,使用canvas直接繪製,會讓代碼變得很臃腫。並且,不方便調試,由於不能直接在開發者工具裏修改即所見。git

下面用一種「hack」方式,把佈局工具交給瀏覽器。github

方案二:使用瀏覽器佈局

瀏覽器裏能夠方便的用css控制內容的佈局,對於居中咱們有的是辦法。那咱們索性把這個任務交給它,而後咱們得到每一個文字的位置,直接繪製在畫布中就好了。好比上例中,咱們先用html和css把內容放好。canvas

<style> .poster{ position: absolute; width: 721px; height: 920px; left: -10000px; border: 1px solid #000; font-family: "Kaiti SC"; text-align: center; } .uname{ font-size: 36px; padding-top:150px; } /* 其它樣式內容見源碼 */ </style>
<div class="poster">
	<div class="uname"></div>
	<div class="money"></div>
</div>
複製代碼

而後使用js填充內容。瀏覽器

let moneyHtml = '已兌換'.split('').map(word=>`<span>${word}</span>`).join('');
moneyHtml += info.money.split('').map(word=>`<strong>${word}</strong>`).join('');
moneyHtml += '元現金'.split('').map(word=>`<span>${word}</span>`).join('');
document.querySelector('.money').innerHTML = moneyHtml;
複製代碼

注意,上面把文本都用spanstrong標籤分開包裹起來,目的是方便接下來用js單獨得到每一個字符的位置,直接渲染。服務器

而後就是把dom中的內容,「複製」到canvas上。markdown

//繪製金額
ctx.font = '42px "Kaiti SC"';
for(let word of document.querySelectorAll('.money span')){
	let rect = word.getBoundingClientRect();
	ctx.fillText(word.innerHTML, rect.left-left, rect.top-top);
}
ctx.font = '140px "Kaiti SC"';
ctx.fillStyle='#a70322';
for(let word of document.querySelectorAll('.money strong')){
	let rect = word.getBoundingClientRect();
	ctx.fillText(word.innerHTML, rect.left-left, rect.top-top);
}
複製代碼

它獲取每一個字的dom元素,而後挨個兒繪製到canvas上。

這種方案特別適合大量文字的狀況,尤爲是能夠方便解決換行問題。由於canvas裏的文本不會自動換行,要本身算在哪裏換行,太麻煩了。

demo地址 查看完整源碼

方案三:svg繪製

既然svg就是圖片,讓使用它的結構化和css來方便佈局,而後直接把它當圖drawImage行不行?

不行,由於canvas的drawImage接口中,只支持CSSImageValue,HTMLImageElement,SVGImageElement,HTMLVideoElement,HTMLCanvasElement,ImageBitmap或者OffscreenCanvas。這裏面雖然有SVGImageElement,但它不是svg自己,只是svg裏使用元素。

可是,支持base64,svg的內容正好能夠轉化爲base64,一會兒就能夠曲線救國了。

先用svg佈局海報內容,由於svg即支持dom,也支持css,甚至js,用起來實在太方便。

<svg id="poster" viewBox="0 0 721 920">
	<style type="text/css"> .uname{ dominant-baseline: middle; text-anchor: middle; fill: #282521; font: 36px 'Kaiti SC'; } .money{ text-anchor: middle; fill: #282521; font: 42px 'Kaiti SC'; } .money-count{ font-size: 140px; fill: rgb(167, 3, 34); } </style>
	<!-- <image x="0" y="0" width="721" height="920" href="https://inagora.github.io/svg-guide/res/poster-bg.jpg" /> -->
	<text x="360" y="165" class="uname">poker</text>
	<text x="360" y="400" class="money">
		<tspan dominant-baseline="ideographic">已兌換</tspan>
		<tspan dx="40" class="money-count">1.50</tspan>
		<tspan dx="40" dominant-baseline="ideographic">元現金</tspan>
	</text>
</svg>
複製代碼

其中註釋掉的是海報背景圖,在調試時能夠打開,就能夠直接在瀏覽器當作品效果了。注意,上面代碼裏把名字和金額直接寫進去了,方便理解,正式環境中須要js寫進去。

把svg內容轉化成base64代碼以下:

function loadImg(url){
	return new Promise((resolve) => {
		let img = new Image();
		img.setAttribute('crossOrigin', 'Anonymous');
		img.onload = function () {
			resolve(this);
		};
		if(url instanceof window.SVGSVGElement){
			var xml = new XMLSerializer().serializeToString(document.querySelector('#poster'));
			img.src = 'data:image/svg+xml;base64,'+window.btoa(unescape(encodeURIComponent(xml)));
		} else
			img.src = url;
	});
}
複製代碼

它建立了一個Image元素,而後把svg的內容轉成base64代碼,用圖片展現出來。

而後直接把這個圖繪製在canvas上就好了。

let svgImg = await loadImg(document.querySelector('#poster'));
ctx.drawImage(svgImg, 0, 0);
複製代碼

這種方案的好處是即便用svg方便的佈局和調試,而後渲染的代碼又很簡單。

固然,相比於第二種方案,也有一些缺點:

  1. 自動換行須要額外處理;
  2. svg內聯的圖片、字體文件等,這些外聯的文件,都不會加載,須要轉成base64直接寫到svg裏

總結

上面三種方案,各有利弊,svg簡單、代碼也行,很「優雅」;dom方案能夠完成使用瀏覽器的佈局能力,徹底不須要計算,對於特別多文案或內容很複雜的時候,這個方案不錯;純canvas方案的兼容性最好,也不須要額外的dom攙和,「最乾淨」。

相關文章
相關標籤/搜索