In our work, we will encounter more or less the requirements of fission activities. Fission activities, sharing posters is also an essential part. Therefore, it is necessary to know how to generate posters.

Background image + title + content + exclusive head picture, nickname + drainage TWO-DIMENSIONAL code are the basic elements of poster design. These basic elements are what we need to implement in the code. It can be simply understood as drawing (block, picture) elements, single and multi-line text to Canvas and generating a poster image.

After analyzing the functionality to be implemented, I came up with three ways to implement it:

  1. Get rid of the person who needs it.

    Looked at the product that sandbag big fist, I still temporarily let him go first (absolutely not I am afraid I was solved by him!) .

  2. “Back end band elder brother, help generate a poster in the server side. You finish writing, this week’s white rice I all package!”

    Back end band brother:

    I guess a week of rice isn’t enough, and we’re done.

  3. Implement everything yourself, ensuring complete control of the code.

    Why not use plug-ins? As the saying goes, teaching a man to fish is better than teaching him to fish… Well, actually because I have never touched canvas, I want to learn about it.

Talk is cheap, Show me the demo.

Draws an arc rectangle (block, image) element

The implementation of small programs, and on the Web side of CSS can be very different. The main problems/approaches are the following three:

Draws arc rectangle paths

Canvas does not provide a way to draw rounded rectangles, so we need another way to do it. The core of this method is a method called canvasContext. arcTo.

CanvasRenderingContext2D. ArcTo () is a 2 d Canvas API to draw arc path according to the control point and radius, using the current tracing points (such as a moveTo or lineTo function check point). Draw an arc path between two tangent lines based on the line connecting the current stroke point to the given control point 1, and the line connecting control point 1 to control point 2, as a tangent line to a circle using the specified radius.

You can imagine that a circle ⚪ is pushing hard towards a dead Angle, and when it reaches its limit, that’s the curve we want. The line connecting control point 1 to control point 2 is the tangent of the circle using the specified radius, so this line will also be extended wirelessly. A picture is worth a thousand words:

Then, the rounded corners of a rectangle seem so natural:

// Draw arc rectangle paths
canvasToDrawArcRectPath(ctx, x, y, w, h, r = 0) {
    const [
        topLeftRadius,
        topRightRadius,
        bottomRightRadius,
        BottomLeftRadius
    ] = Array.isArray(r) ? r : [r, r, r, r]
    /** * 1. Move to the beginning of the arc ** 2. Draw a line * 3. Draw the upper right arc * * 4. Draw the right line * 5. Draw the lower right arc * * 6. Draw the lower left arc * * 8. Draw the left line * 9. Draw the upper left arc */
    ctx.beginPath()

    ctx.moveTo(x + topLeftRadius, y)

    / / right
    ctx.lineTo(x + w - topRightRadius, y)
    ctx.arcTo(x + w, y, x + w, y + topRightRadius, topRightRadius)

    / / right
    ctx.lineTo(x + w, y + h - bottomRightRadius)
    ctx.arcTo(
        x + w,
        y + h,
        x + w - bottomRightRadius,
        y + h,
        bottomRightRadius
    )

    / / lower left
    ctx.lineTo(x + BottomLeftRadius, y + h)
    ctx.arcTo(x, y + h, x, y + h - BottomLeftRadius, BottomLeftRadius)

    / / left
    ctx.lineTo(x, y + topLeftRadius)
    ctx.arcTo(x, y, x + topLeftRadius, y, topLeftRadius)

    ctx.closePath()
}
Copy the code

Cut out pictures

When drawing an image, we often need to cut the original image to get the style we want. When we call canvasContext.clip () for clipping, subsequent drawings are restricted to the clipped region (no access to other areas of the canvas). Therefore, the current canvas area is saved by using the Clip method before the clip method, and restored by using the restore method after the clip method.

ctx.save() // Save the canvas area

this.canvasToDrawArcRectPath(ctx, x, y, width, height, radius) // Draw arc rectangle paths

ctx.clip() // Cut to arc rectangular paths

const { path: tempImageUrl } = await this.uniGetImageInfoSync(url)
ctx.drawImage(tempImageUrl, x, y, width, height) // Draw the image after cutting into an arc rectangle path

ctx.restore() // Restore the canvas area
Copy the code

Draws an element with both a background and a border and rounded corners

When the image has a background, a border and rounded corners, we need to do it in a clever way: a “pyramid”.

This is because the “layers” of canvas follow the principle of “first come, second come, first come”. The “layers” drawn later will cover the “layers” drawn first. So, in order:

  1. Draw a border and fill it
  2. Draw and fill the background
  3. Draw blocks, picture elements

CanvasContext.strokeRectIt is better for drawing borders, but it cannot set rounded corners, so it is not used.

The drawn effect is shown as follows:

// Draw block elements
canvasToDrawBlock(ctx, params) {
    return new Promise(async (resolve) => {
        const {
            x,
            y,
            url,
            width,
            height,
            radius,
            border,
            borderColor,
            backgroundColor
        } = params

        if (border) {
            ctx.setFillStyle(borderColor ?? '#fff')
            this.canvasToDrawArcRectPath(
                ctx,
                x - border,
                y - border,
                width + border * 2,
                height + border * 2,
                radius
			)
            ctx.fill()
        }

        if (backgroundColor) {
            ctx.setFillStyle(backgroundColor)
            this.canvasToDrawArcRectPath(ctx, x, y, width, height, radius)
            ctx.fill()
        }

        if (url) {
            ctx.save()

            this.canvasToDrawArcRectPath(ctx, x, y, width, height, radius)

            ctx.clip()

            const { path: tempImageUrl } = await this.uniGetImageInfoSync(url)
            ctx.drawImage(tempImageUrl, x, y, width, height)
        }

        ctx.restore()
        resolve()
    })
}
Copy the code

Draw single and multiple lines of text

Typically, posters have multiple lines of text. However, Canvas’s weak support for text typesetting prevents us from using Canvas for text typesetting as happily as CSS typesetting. When a Canvas draws text, it will just keep drawing on a single line without wrapping itself according to the container width.

In the canvas provides CanvasContext. MeasureText (string text) returns the width of the text interface. Therefore, we only need to calculate the width and draw the text one by one, the main steps are as follows:

  1. Calculates the text width of the current text plus the next text
  2. If the text width does not exceed the container width, continue to add the text width of the next text
  3. When the text width is greater than the maximum width, draw the filled text on the canvas
  4. After each line is drawn, the setlineHeightUpdate the Y-axis of the text drawing, reset the current text, and continue with steps 1, 2, and 3.

It should be noted that the text drawn on canvas has its own benchmark rules, which are different in different systems and devices, resulting in different Y-axis positions of the text on different devices. TextBaseline = ‘middle’ and increase the height of the Y-axis by fontSize / 2 to ensure that the Y-axis of the text is consistent with the design on all platforms. This method comes from how 2dunn draws text paragraphs on Canvas.

// Draw text
canvasToDrawText(ctx, canvasParam) {
    const {
        x,
        y,
        text,
        fontWeight = 'normal',
        fontSize = 40,
        lineHeight,
        maxWidth,
        textAlign = 'left',
        color = '# 323233'
    } = canvasParam

    if (typeoftext ! = ='string') {
        return
    }

    ctx.font = `normal ${fontWeight} ${fontSize}px sans-serif`

    ctx.setFillStyle(color)
    ctx.textBaseline = 'middle'
    ctx.setTextAlign(textAlign)

    function drawLineText(lineText, __y) {
        let __lineText = lineText
        if (__lineText[0= = =' ') {
            __lineText = __lineText.substr(1)
        }
        ctx.fillText(__lineText, x, __y + fontSize / 2)}if (maxWidth) {
        const arrayText = text.split(' ')

        let lineText = ' '
        let __y = y
        for (let index = 0; index < arrayText.length; index++) {
            const aryTextItem = arrayText[index]
            lineText += aryTextItem
            /** * 1. Calculate the width of the current text plus the next text * 2. Draw the filled text on the canvas when the text width is greater than the maximum width * 3. __y + fontSize / 2 problem * 4. Sets the y position of the next line of text, resets the current text information */
            const { width: textMetrics } = ctx.measureText(
                lineText + (arrayText[index + 1]????' '))if (textMetrics > maxWidth) {
                // Draw a line of text, delete it if the first text is a space
                drawLineText(lineText, __y)
                __y += lineHeight ?? fontSize
                lineText = ' '}}// Draw the last line of text, delete it if the first text is a space
            drawLineText(lineText, __y)
            return
        }
        ctx.fillText(text, x, y + fontSize / 2)}Copy the code

Draw the poster and generate the image temporary file address

In the process of drawing posters and generating temporary file addresses of pictures, drawing pictures is an asynchronous process of obtaining picture information. Therefore, to ensure that all required elements are drawn, we need to synchronize the drawing process. Ensure that all elements are drawn before calling the canvasContext.draw method.

After canvasContext.draw is drawn, we call uni.canvasToTempFilePath to export the contents of the specified area of the current canvas to the temporary address of the poster image. Note that under a custom component, you need to bind the this of the current component instance to the third parameter to operate on the canvas component within the component

/ / drawing canvas
canvasToDraw() {
    return new Promise(async (resolve) => {
        const [ctx, canvasId] = this.createCanvasContext()
        const { width, height, backgroundImageUrl, backgroundColor } =
              this.posterParams

        if (backgroundColor) {
            this.canvasToDrawBlock(ctx, {
                x: 0.y: 0,
                width,
                height,
                backgroundColor
            })
        }

        // Draw the background image
        if (backgroundImageUrl) {
            const { path: tempBackgroundImageUrl } =
                  await this.uniGetImageInfoSync(backgroundImageUrl)
            ctx.drawImage(tempBackgroundImageUrl, 0.0, width, height)
        }

        // Draw other elements
        for (const canvasParam of this.posterParams.list) {
            const { type } = canvasParam

            if (type === 'text') {
                this.canvasToDrawText(ctx, canvasParam)
            }

            if (type === 'block') {
                await this.canvasToDrawBlock(ctx, canvasParam)
            }
        }

        ctx.draw(false.async() = > {const { tempFilePath } = await this.canvasToTempFilePath(canvasId, {})
            resolve([canvasId, tempFilePath])
        })
    })
}

// Canvas export image temporary address
canvasToTempFilePath(canvasId, params) {
    return new Promise((resolve, reject) = > {
        uni.canvasToTempFilePath(
            {
                canvasId,
                fileType: 'jpg'. params,success: resolve,
                fail: reject
            },
            this)})}Copy the code

Save temporary files to local and cache

Our posters might not change for months, but now they have to be redrawn with every click. If the poster had been more informative, there would have been a noticeable lag on less powerful models. Although wechat team has carried out a wave of rendering performance improvement on Canvas component of small program in 19 years. But we can not be lazy, the characteristics of the scene is also to do a good job of optimization to improve the user experience.

Since the generated poster may not be changed for several months, we can simply store the file in the user’s local cache after drawing the poster for the first time and use it next time. The specific steps are as follows:

  1. Draw the Canvas andGet temporary address of poster picture.
  2. Poster picture temporary address throughuni.getFileSystemManager().saveFileMethod is saved to the local user and obtainedFile path after storage (local path).
  3. Pass the saved file path (local path)uni.setStorageSyncStore it in the cache to determine whether the poster has been generated.

The steps seemed ideal, but there was a lot less thought. After the poster is generated once, unless the user manually clears the cache, the poster picture will never be updated, the poster parameter Settings are wrong, and the user can only manually clear the cache, otherwise it cannot be updated, causing unnecessary trouble. Therefore, we want the poster diagram to have an expiration time when it is cached, and the interface can decide whether to update the poster directly.

The expiration time is easy to solve. The current timestamp + expiration time is also stored in the cache. The next time you fetch it, check whether the saved timestamp is larger than the current timestamp to know whether it has expired.

const storage = {
  get(key) {
    const { value, expires } = uni.getStorageSync(key)

    if (expires && expires < Date.parse(new Date())) {
      uni.removeStorageSync(key)
      return undefined
    }
    return value
  },
  set(key, value, expires) {
    / / expires
    uni.setStorageSync(key, {
      value,
      expires: expires ? Date.parse(new Date()) + expires * 1000 : undefined}}})export { storage }
Copy the code

It is better to use the interface to decide whether to update the poster directly. The component only needs to receive a disableCache value to determine whether to force the update. Here’s an example:

<poster
	:xxx="xxx"
	:disable-cache="true"
/>

props: {
  // The key for storing the poster in the cache
  cacheKey: {
    type: String.default: 'cache-poster'
  },
  // Whether to disable cache (whether to force a refresh)
  disableCache: {
    type: Boolean.default: false}... }methods: {
  pageInit() {
    const posterImage = storage.get(this.cacheKey)
    // No longer draw Canvas when there are images in the cache and the cache cannot be used (no forced refresh is required)
    if (posterImage && !this.disableCache) {
      this.posterImage = posterImage
      return
    }
    / /... Draw the posters}}Copy the code

Finally, we use the file manager provided by wechat to store the file into the user’s local wx.getFilesystemManager () and store the callback address into the cache through our own storage.set.

const fs = wx.getFileSystemManager()

fs.saveFile({
  tempFilePath: tempCanvasFilePaths, // Pass in a local temporary file path
  success: (res) = > {
    storage.set(this.cacheKey, res.savedFilePath, 86400000)
    this.posterImage = res.savedFilePath
  }
})
Copy the code

Therefore, our complete steps are as follows:

  1. When an image exists in the cache and the cache is not disabled (no forced refresh is required) :
    1. Use the cached poster image address directly.
    2. No longer generate Canvas components.
    3. Do not proceed to the remaining steps.
  2. No image in cache or cache disabled (forced update required) :
    1. Draw the Canvas andGet temporary address of poster picture.
    2. Poster picture temporary address throughuni.getFileSystemManager().saveFileMethod is saved to the local user and obtainedFile path after storage (local path).
    3. Will store after the file path (local path) ownstorage.setStore in the cache and set the expiration time to determine whether the poster has been generated or whether the poster needs to be updated.

Save the poster image to your album

Here, we can directly call the API provided by wechat to save the poster picture to the album. But the premise here is that the user has authorized the permission to save pictures to the system album.

When the user triggers the popup of “save authorization picture to system album” for the first time and clicks “reject authorization”, the next time the user clicks “authorize”, the callback of “Fail” will be directly entered and the “authorize” popup will not be displayed. Although I did not find the explanation of wechat official document, it can also be learned that authorization to save pictures to the system album will only prompt the pop-up window once. Therefore, after the user refuses authorization, we need to manually set up the client applets setting interface uni.openSetting in the fail callback.

Note that calling uni.openSetting directly in the FAIL callback is invalid because wechat requires: Note: from version 2.3.0 onwards, users can only jump to the Settings page and manage authorization information after clicking. For details. To trigger, we need to play a modal dialog box uni.showModal to trigger the user’s clicking behavior, and then call uni.openSetting to open the setting interface. It’s a hassle, but it’s also interactive and logical.

// Save the image to the album
saveImageToPhotosAlbum() {
    uni.saveImageToPhotosAlbum({
        filePath: this.posterImage,
        success: () = > {
            this.$emit('close-overlay')
            uni.showToast({
                title: 'Image saved successfully'.duration: 2000})},fail(err) {
            const { errMsg } = err
            if (errMsg === 'saveImageToPhotosAlbum:fail auth deny') {
                uni.showModal({
                    title: 'Save failed'.content: 'Please grant permission to save pictures to album'.success: (result) = > {
                        const { confirm } = result
                        if (confirm) {
                            uni.openSetting({})
                        }
                    }
                })
            }
        }
    })
}
Copy the code

Write in the last

All roads lead to Rome, if you think the above method is too much trouble. Wxml-to-canvas, the extension component recommended by wechat official, can be used to draw canvas through static templates and styles in the small program and export pictures, which can be used to generate shared images and other scenes.

Of course, you can also choose to transfer the poster generation method to web-view, and then you can do whatever you want with it. The downside is obvious: the Web-view container automatically fills the entire applet page, and personal applets are not currently supported.

These are the functions for now. If you have any important functional requirements, you can raise them in issue. A visual action poster parameter page may also be added later. What time? Next time!

DEMO Github repository address

Blog Blog address

Small procedures sun code (demo demo only)

The resources

  1. Zhang Xinxu realized automatic wrapping, word spacing and vertical arrangement of canvas text drawing
  2. 2dunn draws posters in front more elegantly based on canvas
  3. Fanbox Lucky Draw Koi Applets 2.0 Summary (Poster style)