1. The background

Due to the recent feedback from many users that there are some minor problems in our shared posters, I have summarized the following problems:

  • The poster is not clear

  • Poster generation is too slow

  • Long-pressing posters cannot be saved

  • Poster text overlap

Therefore, I made an in-depth study and found that it was all related to html2Canvas plug-in (which is a very, very good plug-in). I found that all these problems could be greatly avoided by using native API to draw posters, so I encapsulated a plug-in based on native API, and objToPoster poster generation component was born.

Github address: github.com/6sy/crerate…

Above has the complete demonstration example, likes to think also good remember star once oh ~

2.objToPoster VS html2canvas

2.1 Comparison of generation speed:

Html2canvas plugin effect:

ObjToPoster plugin effects:

Through the generation effect, it is obvious that the speed of our encapsulated component is faster than that of HTML2Canvas, and the specific generation time can be seen from the JS code:

Html2canvas generation time: 2005ms

ObjToPoster generation time: 777ms

2.2 Contrast of clarity:

Html2canvas:

objToPoster:

Put it all together:

3. Explanation of basic drawing function

Before learning about components, we need to learn some basic functions of Canvas.

First, we get the Canvas element, and we get the CTX object.

function initCanvas() {
   canvas = document.getElementsByTagName("canvas");
   ctx = canvas[0].getContext("2d");
   console.log("ctx", ctx);
   canvas[0].style.width=300+'px';
   canvas[0].style.height=300+'px';
}
Copy the code

3.1 Draw the text ctx.filltext ()

Function drawText() {ctx.beginPath(); // Text color ctx.fillStyle = "red"; Font = "22px Arial,sans-serif"; // Ctx. font = "22px Arial,sans-serif"; TextBaseline = "hanging"; // Default to alphabetic, top, hanging, middle, ideographic, bottom ctx.textBaseline = "hanging"; ctx.fillText("hello world", 250, 10, 1300); // Close the path ctx.closepath (); }Copy the code

Effect:

Let’s try increasing the length of the text again:

It will be found that the API for drawing text on canvas does not automatically wrap, and will be intercepted automatically if it goes beyond the scope of canvas. Fortunately, Canvas provides us with another API

let TextMetrics = ctx.measureText('hello world');
console.log(TextMetrics)
Copy the code

Print result:

This width just returns the length of the string I passed in. We can do line breaks in the other direction, just give me the maximum width, and we’ll iterate through all the strings to control that. Here is the function that implements line break, which I first attach to the prototype of CanvasRenderingContext2D:

@param {*}text: the value of the text @param {*}x: the x position of the first word @param {*}y: the y position of the first word @param {*}maxWidth: the maximum number of words displayed on a single line @ param {*} lineHeight: text line height * / CanvasRenderingContext2D prototype. WrapText = function (text, x, y, maxWidth = 300, lineHeight = 12, ) { if (typeof text ! == "string" || typeof x ! == "number" || typeof y ! == "number") { return; } let context = this; Let arrText = text.split(""); // Let lineText = ""; for (let i = 0; i < arrText.length; I ++) {let tempText = lineText + arrText[I]; let tempText = lineText + arrText[I]; let TextMetrics = context.measureText(tempText); If (TextMetrics. Width > maxWidth && I > 0) {context.filltext (lineText, x, y); LineText = arrText[I]; lineText = arrText[I]; lineText = arrText[I]; Y += lineHeight; }else{ lineText = lineText + arrText[i]; } } context.fillText(lineText, x, y) };Copy the code

WrapText: ctx.wraptext: ctx.wraptext: ctx.wraptext

Function drawText() {ctx.beginPath(); // Text color ctx.fillStyle = "red"; Font = "22px Arial,sans-serif"; // Ctx. font = "22px Arial,sans-serif"; TextBaseline = "hanging"; // Default to alphabetic, top, hanging, middle, ideographic, bottom ctx.textBaseline = "hanging"; ctx.fillText("hello world hello world", 250, 10, 1300); let TextMetrics = ctx.measureText("hello world"); Console. log(TextMetrics) // Add ctx.wrapText(" Hello World Hello World ", 10, 10, 120, 23); // Close the path ctx.closepath (); }Copy the code

Effect:

3.2 Draw an image ctx.drawImage

Function drawImage() {let img= new Image(); img.src = "http://wechatapppro-1252524126.file.myqcloud.com/apppcXiaoeT6666/file/YXBwbW9kdWxlZGVmYXVsdC13ZXdvcmstdXNlci1hdmF0YXItd V8xMjM0NTY3ODkxMDExXzEyMzQ1Njc4OTEtZTNVYnJ1YWs.jpg"; Img.onload =function(){// start drawing path ctx.beginPath(); ctx.drawImage(img, 0, 0, 100, 100); // Close the path ctx.closepath (); }}Copy the code

Effect:

We must wait for the picture to be loaded before drawing, so we need to draw the canvas in onload, and the domain name of this picture needs to be configured to be cross-domain.

3.3 Draw states ctx.save() and ctx.restore();

function restoreState() { ctx.beginPath(); ctx.font = "30px Arial,sans-serif"; ctx.fillStyle = "red"; ctx.fillText("123", 30, 30); // Save the current state ctx.save(); ctx.closePath(); // ctx.beginPath(); ctx.fillStyle = "black"; ctx.fillText("456", 65, 65); // Restore the previously saved state ctx.restore(); ctx.fillText("789", 100, 100); ctx.closePath(); }Copy the code

Effect:

When ctx.save() is saved, the color will be red. When we call ctx.resetore(), the color of the current drawing will revert to the previous red, so 123 789 is red and 456 is black.

3.4 cutting CTX. Clip ()

function clipImage(){ ctx.beginPath(); // Draw a clipping range ctx.arc(50, 50, 50, 0, math. PI * 2, true); // Clipping only clipping areas will display ctx.clip(); drawImage(); / / test cutting range / / setTimeout (() = > {/ / CTX beginPath (); // ctx.font = "30px Arial,sans-serif"; // ctx.fillStyle = "red"; // ctx.fillText("1234567", 50, 50); / /}, 1000); }Copy the code

Effect:

Obviously, the square image has been cropped into a circle, so I’m testing the area outside the circle to see if I can draw it.

Function clipImage(){ctx.beginPath(); // Draw a clipping range ctx.arc(50, 50, 50, 0, math. PI * 2, true); // Clipping only clipping areas will display ctx.clip(); drawImage(); // Add test clipping range setTimeout(() => {ctx.beginPath(); ctx.font = "30px Arial,sans-serif"; ctx.fillStyle = "red"; ctx.fillText("1234567", 50, 50); }, 1000); }Copy the code

Effect:

Obviously, the clipped area can only show 123,4567 and beyond the clipped area, it cannot be drawn.

4. Package component ideas and some problems sorting

4.1 Analyze the elements of the poster

First of all, we observed a poster and found that there were few elements in it, only background picture, head picture, nickname, title, TWO-DIMENSIONAL code, etc., and the information to be expressed was only location and size.

From a technical perspective, it is easy to encapsulate these elements into two types: images and text. But as we all know, the first action to draw the poster must be to draw the background image of the poster. In order to better realize the technology, we also encapsulate the background image into a type. So there are three types: background, picture and text.

Depending on the image type, we can set the parameters as follows:

'the resource - img: {type: "img", / SRC/type:' https://wechatapppro-1252524126.file.myqcloud.com/123.jpeg, / / address x: 10, // x axis y:100, // y axis direction width:120, // picture length height:120 // picture width: 60 // cut radius:60 //Copy the code

Text type:

'user-name':{size:10, // text size color:'red', // text type :'sans-serif', // text style x:10, // x direction y:10 // y direction}Copy the code

4.2 Image loading Problem

By drawing the picture above, we know that it must be drawn in img.onload, but there are multiple pictures in the poster that need to be drawn, so we can draw after loading all the pictures successfully, using promise.all.

4.3 Drawing Sequence

The drawing of canvas is actually the same as that of JS, which is single-threaded and can only do one thing at a time. Therefore, it is very important to control the drawing sequence of posters. The first step is to load all the images, and then draw each element in order. The background elements must have the highest priority and must be drawn first. The text is presented on top of the image, so the priority is lowest. The rest is the head two-dimensional code resource pictures, etc., these orders are not exquisite. We can now draw in the order that the user passed in. The general process is as follows:

5. Component explanation

5.1 call

<script> let img=document.querySelector('.img') let obj={ "background-img":{ type:'background-img', src:'https://wechatapppro-1252524126.file.myqcloud.com/appAKLWLitn7978/image/a95789e8626cd3d428ecb85c823d525c.png', x:0, y:0, width:250, height:450, }, 'user-avatar':{ type:'img', src:'https://wechatavator-1252524126.file.myqcloud.com/appAKLWLitn7978/image/compress/u_5f6b0990cac40_vSh2X7BAc2.png', x:88, y:28, width:68, height:68, radius:34, }, 'resource-img':{ type:'img', src:'https://wechatapppro-1252524126.file.myqcloud.com/appAKLWLitn7978/image/b_u_5b2225aa46488_oGKN7IvA/ktb3nze709jx.jpe g?imageView2/2/w/640/h/480/q/100/format/webp', x:10, y:100, width:230, height:120 }, 'qr-code':{ type:'img', src:'data:image/png; base64,iVBOR... ', / / omit x: 100, y: 300, width: 44, height: 44,}, 'the user -name' : {type: 'font' x: 10, y: 20, value: 'good good study, Let DDD =new objToPoster(obj,250,450,10).converttoimg ().then(res=>{console.log(res) // Res is the base64 image that generates the poster img.src=res}); </script>Copy the code

First of all, we know that there are many elements in a poster, so how do we combine them, either through arrays or objects? In this case, I chose to use the object method, because the attribute name makes it easier to mark each element, so the first argument is an obj object.

The second and third parameters are actually the length and width of the box we were editing the poster in. Because all of our x’s and y’s are positioned based on boxes.

The third parameter determines the quality of the poster we generate. First, we draw inside the canvas. The larger the canvas element is, the higher the quality of the drawing will be. This parameter is to control the canvas size, thus controlling the quality of the generated picture.

5.2 What happened to the New objToPoster

constructor(posterObj, imgWidth = 300, imgHeight = 700, proportion = 1) { this.ctx = null; this.canvas = null; // This. Img = null; this.proportion = proportion; This. PosterImages = []; // This. PosterImages = []; // Draw all text this. PosterFonts = []; InitCanvas (imgWidth * proportion, imgHeight * proportion); // Initialize canvas element this.initCanvas(imgWidth * proportion, imgHeight * proportion); // Initialize the incoming object why do this step: the order of drawing this.initPosterobj (posterObj); }Copy the code

Constructor does two main things. First, it initializes the Canvas element using the initCanvas method. The second is to initialize the element objects we pass in and store them in the posterImages and posterFonts arrays respectively. InitCanvas function:

initCanvas(width, height) {
        let canvas = document.createElement("canvas");
        this.canvas = canvas;
        canvas.style["position"] = "absolute";
        canvas.style["z-index"] = "-1";
        canvas.style["top"] = 0;
        canvas.style["left"] = 0;
        canvas.style["display"] = "none";
        canvas.width = width;
        canvas.height = height;
        document.body.appendChild(canvas);
        this.ctx = canvas.getContext("2d");
        console.log("init-canvas is ok");
    }
Copy the code

InitPosterObj function:

InitPosterObj (posterObj) {let objNames = object.keys (posterObj); for (let i = 0; i < objNames.length; I++) {/ / if the background image (posterObj [objNames [I]]. Type = = = TYPES [0]) {this. PosterImages. Unshift (posterObj [objNames [I]]). } if (posterObj[objNames[I]].type === TYPES[1]) {this. PosterObj [objNames[I]]; } // text if (posterObj[objNames[I]].type === TYPES[2]) {this. PosterObj [objNames[I]]; }}}Copy the code

I’m just going to store these elements in an array and then I’m going to loop them out, and there’s a neat thing about this, where ackground-img inserts the array with unshift, so the background is always drawn first.

5.3 Enter the convertToImg method:

convertToImg() { return new Promise(async (res, rej) => { try { await this.initImages(this.posterImages); This.draw (); // load all images this.draw(); // Draw this.img = this.canvas. ToDataURL ("image/jpeg"); // Generate a base64 image this.clearCanvas(); // Clear the canvas dom object res(this.img); } catch (e) {console.error(e); }}); }Copy the code

This function does three things: first initialize all images with await and then draw, and then return the image after drawing.

5.3 initImages Initializes All Images:

InitImages (arr) {let imgLoadPromise = []; for (let j = 0; j < arr.length; j++) { let p = new Promise((resolve, reject) => { let img = new Image(); img.crossOrigin = "anonymous"; img.src = arr[j].src; img.onload = function () { console.log("img-onload"); arr[j].imgOnload = img; resolve(true); }; }); imgLoadPromise.push(p); } return Promise.all(imgLoadPromise); }Copy the code

At this point, the imgOnload of each element object holds the IMG object, so we can iterate through posterImages and posterFonts arrays to draw.

5.4 drawImage Method for drawing an image:

function drawImage(ctx, obj, proportion) { ctx.beginPath(); Ctx.restore (); ctx.save(); // Clipping if (obj.radius) {console.log(obj); If (obj.width === obj.height) {ctx.arc((obj.width * proportion) / 2 + obj.x * proportion, (obj.width * proportion) / 2 + obj.y * proportion, obj.border * proportion, 0, Math.PI * 2, true ); ctx.clip(); ctx.drawImage( obj.imgOnload, obj.x * proportion, obj.y * proportion, obj.width * proportion, obj.height * proportion );  ctx.closePath(); } else {}} else {ctx.drawImage(obj.imgOnload, obj. X * proportion, obJ. Y * proportion, obj. Width * proportion, obJ. obj.height * proportion ); ctx.closePath(); }}Copy the code

We know from the previous example that areas outside the clipped area cannot be drawn, so we call ctx.restore() and ctx.save() every time we draw the image to avoid the problem of not being able to draw after clipping.

5.5 Class objToPoster complete code

// obj img font shape import { drawImage, drawFont } from "./utils/drawImage.js"; let TYPES = ["background-img", "img", "font"]; Class objToPoster {// posterObj- object imgWidth- image width (default px) imgHeight- image length (default px) PROPORTION (image precision - higher accuracy) constructor(posterObj, imgWidth = 300, imgHeight = 700, proportion = 1) { this.ctx = null; this.canvas = null; // This. Img = null; this.proportion = proportion; This. PosterImages = []; // This. PosterImages = []; // Draw all text this. PosterFonts = []; InitCanvas (imgWidth * proportion, imgHeight * proportion); // Initialize canvas element this.initCanvas(imgWidth * proportion, imgHeight * proportion); // Initialize the incoming object why do this step: the order of drawing this.initPosterobj (posterObj); } initCanvas(width, height) { let canvas = document.createElement("canvas"); this.canvas = canvas; canvas.style["position"] = "absolute"; canvas.style["z-index"] = "-1"; canvas.style["top"] = 0; canvas.style["left"] = 0; canvas.style["display"] = "none"; canvas.width = width; canvas.height = height; document.body.appendChild(canvas); this.ctx = canvas.getContext("2d"); console.log("init-canvas is ok"); } initPosterObj(posterObj) {verfiyObjEmpty(posterObj); let objNames = Object.keys(posterObj); for (let i = 0; i < objNames.length; I++) {/ / if the background image (posterObj [objNames [I]]. Type = = = TYPES [0]) {this. PosterImages. Unshift (posterObj [objNames [I]]). } if (posterObj[objNames[I]].type === TYPES[1]) {this. PosterObj [objNames[I]]; } // text if (posterObj[objNames[I]].type === TYPES[2]) {this. PosterObj [objNames[I]]; InitImages (arr) {let imgLoadPromise = []; for (let j = 0; j < arr.length; j++) { let p = new Promise((resolve, reject) => { let img = new Image(); img.crossOrigin = "anonymous"; img.src = arr[j].src; img.onload = function () { console.log("img-onload"); arr[j].imgOnload = img; resolve(true); }; }); imgLoadPromise.push(p); } return Promise.all(imgLoadPromise); } draw(CTX, arr) {this.drawimgs (); this.drawFonts(); } drawImgs() { for (let i = 0; i < this.posterImages.length; i++) { drawImage(this.ctx, this.posterImages[i], this.proportion); } } drawFonts() { for (let i = 0; i < this.posterFonts.length; i++) { drawFont(this.ctx, this.posterFonts[i], this.proportion); } } convertToImg() { return new Promise(async (res, rej) => { try { await this.initImages(this.posterImages); this.draw(); this.img = this.canvas.toDataURL("image/jpeg"); this.clearCanvas(); res(this.img); } catch (e) { console.error(e); }}); } clearCanvas() { this.canvas.remove(); } } export default objToPoster; window.objToPoster = objToPosterCopy the code

5.6 Two methods for drawing:

export function drawImage(ctx, obj, proportion) { ctx.beginPath(); Ctx.restore (); ctx.save(); // Clipping if (obj.radius) {console.log(obj); If (obj.width === obj.height) {ctx.arc((obj.width * proportion) / 2 + obj.x * proportion, (obj.width * proportion) / 2 + obj.y * proportion, obj.radius * proportion, 0, Math.PI * 2, true ); ctx.clip(); ctx.drawImage( obj.imgOnload, obj.x * proportion, obj.y * proportion, obj.width * proportion, obj.height * proportion );  ctx.closePath(); } else {}} else {ctx.drawImage(obj.imgOnload, obj. X * proportion, obJ. Y * proportion, obj. Width * proportion, obJ. obj.height * proportion ); ctx.closePath(); }} export function drawFont (CTX, obj, proportion) {/ / size size = obj. Size | | 10; size = size * proportion + "px"; / / color let color = obj. Color | | "black"; / / style let family = obj. Family | | "Arial, sans-serif"; / / text alignment let textBaseline = obj. TextBaseline | | "hanging"; ctx.beginPath(); ctx.fillStyle = color; ctx.font = `${size} ${family}`; ctx.textBaseline = textBaseline; ctx.fillText(obj.value, obj.x * proportion, obj.y * proportion, 133 * proportion); ctx.closePath(); } export function drawBlobImg(ctx, obj, proportion, Blob) { let img = new Image(); img.src = Blob; obj.imgOnload = img; drawImage(ctx, obj, proportion); }Copy the code

6. Functions of subsequent iterations:

  • Support caption text wrap
  • Support text backgrounds (similar to tags)

7 at the end

Everyone in the use of any problems can be feedback to me ha, there are those deficiencies need to improve the place can be pointed out to correct me, I hope this component can help you ~