The idea of generating a poster by HTML Canvas comes from a personal project called Lyrics-Crawler, which helps Spotify users fetch lyrics easily. To help users sharing pieces of lyrics I decided to generate lyrics card.
Convert DOM elements into SVG or intto Canvas then download as raster (PNG or JPEG) images are the most common methods for poster generating. Js libraries dom-to-image and html2canvas have provided solutions to the above methods. However, the picture generated by these two libraries is unsatisfied. So I start programming my own method.
My idea follows drawing required elements to Canvas then using web API HTMLCanvasElement.toDataURL() save canvas as pictures.
There are two main problems encountered during the implementation process:
1. The generated image and text are blurry
2. HTML5 canvas.fillText() won't do line breaks
Solution will talk later.
The lyrics card is generated dynamically, the width can be set in advance. To get the height of it, using React Refs to get the DOM size after the first render.
import React, { Component } from "react";
class LyricsPic extends Component {
constructor(props) {
super(props);
this.state = {
dimensions: null,
};
}
componentDidMount() {
this.setState({
dimensions: {
width: this.card.offsetWidth,
height: this.card.offsetHeight,
},
});
}
render() {
let card =
<div className='card' id='card' ref={el => { this.card = el }}>
</div>
return (
{card}
<canvas id="canvas" ref={el => { this.canvas = el }} style={{ display: 'none' }}></canvas>
)
}
After getting the dimension information, using it to draw a corresponding Canvas.
// *2 to render high-resolution pic, depends on your needs
this.canvas.width = dimensions.width*2;
this.canvas.height = dimensions.height*2;
this.canvas.style.width = dimensions.width*2 + "px";
this.canvas.style.height = dimensions.height*2 + "px";
var ctx = this.canvas.getContext('2d')
Then load the image and fill text in Canvas. Set image crossOrigin "anonymous" to allow cross-origin downloading. Adjust text position for every line you add.
//load image
var imageObj = new Image();
imageObj.crossOrigin = "anonymous";
imageObj.onload = () => {
ctx.drawImage(imageObj, padding, padding, imageSize, imageSize * imageObj.height / imageObj.width);
}
//fill text
ctx.font = '23px "Roboto Mono"';
ctx.fillText("hello world", padding, currentHeight, width);
Problem 1. Now every element are in its right place, but the Canvas looks blurry. I'm using Mac and 4K screen, to fit the high-resolution requiement, first get the pixel ratio of current device, then using ratio redraw the canvas.
var getPixelRatio = function (context) {
var backingStore = context.backingStorePixelRatio ||
context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1;
return (window.devicePixelRatio || 1) / backingStore;
};
var ratio = getPixelRatio(ctx);
// Canvas zoom ratio
this.canvas.width = dimensions.width * ratio *2;
this.canvas.height = dimensions.height * ratio *2;
this.canvas.style.width = dimensions.width *2+ "px";
this.canvas.style.height = dimensions.height *2+ "px";
// After the canvas is enlarged, the corresponding drawing canvas should also be enlarged
var ctx = this.canvas.getContext('2d')
ctx.scale(ratio, ratio)
Problem 2. Break the line based on the width of your choice, first measure the text wide using ctx.measureText(text).width, then calculate the ratio of text wide and line width, from this get how many words in this line, recursively, break text into different lines.
let lyricsWidth = Math.round(ctx.measureText(item).width);
if (lyricsWidth>=imageSize){
let itemWordNum = item.split(" ").length
while (item!==""){
let wordNum = Math.round(imageSize/lyricsWidth*item.split(" ").length);
if(wordNum >= itemWordNum){
currentHeight = currentHeight + lineheight;
ctx.fillText(item, padding, currentHeight, imageSize);
item = "";
break
}else{
currentHeight = currentHeight + lineheight;
let newline = item.split(" ").slice(0,wordNum).join(" ");
ctx.fillText(newline, padding, currentHeight, imageSize);
item = item.split(" ").slice(wordNum, lyricsWidth).join(" ");
lyricsWidth = Math.round(ctx.measureText(item).width);
}
}
}else{
currentHeight = currentHeight + lineheight;
ctx.fillText(item, padding, currentHeight, imageSize)
}
Now the Canvas is ready! Write a function to make it downloadable.
<a id="download" onClick={el=>this.download_img(el)} >Download Lyrics Pic</a>
download_img = (el) => {
let time = new Date().getTime()
var canvas = document.getElementById('canvas');
var image = canvas.toDataURL("image/png");
el.target.download = name + "_"+ time + ".png"
el.target.href = image;
};
Tada, all done!
Check Lyrics-Crawler here!