preface
The thing is, recently I have been working on the development of wechat mini program. In the process of development, I met a demand to generate a poster, so that other users can enter the mini program through the poster. OK, look up the information on the Internet soon solved the problem.
Then things came, the demand changed, this small program is not a poster, posters are not static content, each poster needs to be displayed dynamically according to the content of the page, and two-dimensional code needs to carry certain information.
For example: in the mall applet, you can share different theme posters of applet and generate different commodity posters in different commodity pages. Posters are required to carry user information for point accumulation and page hopping.
B: well.. After searching the Internet.. Should come or want to come, that oneself encapsulate a bar!
Need to sort out
Basic requirements: Users can select a picture and generate corresponding poster content according to the picture. They can add text, picture and small program TWO-DIMENSIONAL code by themselves, and finally save the generated poster content to the local album.
Implementation idea: All the small programs in the company are compiled and developed with UNI, so I choose to use UNI this time. The poster generation part draws the poster on the canvas and caches the image locally with canvasToTempFilePath. Preview the drawn picture with the image label. If the user thinks OK, click the save button to authorize the album to save.
The overall implementation
The functions of encapsulated components are as follows:
1. Preview the base picture of the poster 2. Draw the poster according to the configuration parameters of the poster 3. Small program QR code can be configured (back-end support is required) 5. Can be quickly saved to the local album 6. Poster. Js function is independently separable and can be used to draw Canvas 7. Two slots header and Save are provided. Customize the title and saveCopy the code
<template>
<view class="poster_wrapper">
<slot name="header"></slot>
<! To generate the poster image -->
<image :src="imageUrl" mode="aspectFill" :style="{width:imageWidth + 'rpx',height:imageHeight + 'rpx'}" @click="click"></image>
<! -- Move the canvas out of the screen, remove the positioning if you need to see the canvas -->
<! -- position:'fixed',left:'9999px',top:'0' -->
<canvas :style="{width:canvasWidth + 'px',height:canvasHeight + 'px',position:'fixed',left:'9999px',top:'0'}"
canvas-id="myCanvas" id="myCanvas" class="canvas"></canvas>
<! -- Mask layer -->
<view class="mask" v-if="showMask" @click="hideMask">
<! -- Generated poster image -->
<image :style="posterSize" :src="lastPoster" :mode="config.imageMode" @click.stop=""></image>
<view class="btn_wrapper" @click.stop>
<slot name="save">
<button type="primary" @click="saveToAlbum">Save to album</button>
</slot>
</view>
</view>
</view>
</template>
<script>
import {
loadImage,
createPoster,
canvasToTempFilePath,
saveImageToPhotosAlbum
} from '@u/poster.js';
import {
getWechatCode
} from "@u/appletCode.js";
export default {
props: {
// Display the width of the image in RPX
imageWidth: {
type: [String.Number].default: 550
},
// Show the height of the image in RPX
imageHeight: {
type: [String.Number].default: 980
},
// Display the url of the image
imageUrl: {
type: String.default: ' '.required: true
},
// Data parameters for drawing posters
drawData: {
type: Array.default: () = > ([]),
required: true
},
// Poster configuration parameters
config: {
type: Object.default: () = > ({
imageMode: 'aspectFit'.posterHeight: '80%',})},// Do you need a small program qr code
wechatCode: {
type: Boolean.default: false
},
// The configuration parameters of the small program QR code
wechatCodeConfig: {
type: Object.default: () = > ({
serverUrl: ' '.scene: ' '.config: {
x: 0.y: 0.w: 100.h: 100}})}},data() {
return {
// Indicates whether the resource is successfully loaded
readyed: false.// Draw parameters after converting network images into static images
imageMap: [].// The local cache address of the last generated poster
lastPoster: ' '.// Whether to display masks
showMask: false.// Whether to load the resource flag
loadingShow: false.// Whether a poster can be created
disableCreatePoster:false,}},computed: {
// The size of the generated poster diagram
posterSize() {
let str = ' ';
this.config.posterWidth && (str += `width:The ${this.config.posterWidth}; `);
this.config.posterHeight && (str += `height:The ${this.config.posterHeight}; `);
return str
},
// The width of the canvas is preferred. If not, the width of the image is used by default
// The main thing is that canvas and image have different units, but it doesn't matter
// When drawing (drawData is configured), it is ok to draw with PX as the benchmark. The reason for using PX is to prevent the specific width and height of the canvas from being determined due to the different Dpr of different devices, so that the final image may have a white edge
canvasWidth(){
return this.config.canvasWidth ? this.config.canvasWidth : this.imageWidth
},
// The height of the canvas is preferred. If not, the height of the image is used by default
canvasHeight(){
return this.config.convasHeight ? this.config.convasHeight : this.imageHeight
}
},
watch: {
// Listen for changes in external draw parameters to reload resources
drawData(newVlaue) {
this.loadingResources(newVlaue)
},
// Listen for readyed changes
readyed(newVlaue) {
// When the user clicks generate poster and the resource is not loaded, the poster will be generated when the resource is loaded
if (newVlaue == true && this.loadingShow == true) {
uni.hideLoading()
this.loadingShow = false;
this.disableCreatePoster = false;
this.createImage(); }}// There are asynchrony problems, which have not been solved yet.
1. Change scene 2 before drawing. After changing the scene, call this.loadingResources manually, but the resources are reloaded
// "wechatCodeConfig.scene":function (newVlaue){
// console.log('wechatCodeConfig.scene',this.imageMap)
// this.loadingWechatCode(this.imageMap)
// }
},
created() {
this.loadingResources(this.drawData)
},
methods: {
// Load a static resource to create or update a collection of downloaded local images within the component
async loadingResources(drawData) {
this.readyed = false;
if(! drawData.length || drawData.length <=0) return;
// Load a static image and replace the network address of all images with the local cache address
const tempMap = [];
for (let i = 0; i < drawData.length; i++) {
let temp
if (drawData[i].type === "image") {
temp = awaitloadImage(drawData[i].config.url); drawData[i].config.url = temp; } tempMap.push({ ... drawData[i],url: temp
})
}
// Load the small program qr code
await this.loadingWechatCode(tempMap);
// Assign the value to imageMap
this.imageMap = tempMap;
setTimeout(() = > {
this.readyed = true;
}, 100)},// Draw the poster diagram
async createImage() {
// Disable poster generation and return directly
if(this.disableCreatePoster) return
this.disableCreatePoster = true;
try {
if (!this.readyed) {
uni.showLoading({
title: 'Static resource loading... '
});
this.loadingShow = true;
this.$emit('loading')
return
}
// Get the context object. This must be passed inside the component
const ctx = uni.createCanvasContext('myCanvas'.this);
await createPoster(ctx, this.imageMap);
this.lastPoster = await canvasToTempFilePath('myCanvas'.this);
this.showMask = true;
this.disableCreatePoster = false;
// Create success function
this.$emit('success')}catch (e) {
// Create a failed function
this.disableCreatePoster = false;
this.$emit('fail', e)
}
},
// Load or update the applet QR code
async loadingWechatCode(tempMap) {
if (this.wechatCode) {
if (this.wechatCodeConfig.serverUrl) {
const code = await getWechatCode(this.wechatCodeConfig.serverUrl, this.wechatCodeConfig.scene || ' ');
// Replace the index. If there is no index, replace the length bit
let targetIndex = tempMap.length;
for (let i = 0; i < tempMap.length; i++) {
if (tempMap[i].wechatCode) targetIndex = i;
}
tempMap.splice(targetIndex, 1, {
type: 'image'.url: code.path,
// The tag is a small program qr code
wechatCode: true.config: this.wechatCodeConfig.config,
})
} else {
throw new Error('serverUrl request QR code server address cannot be empty ')}}return tempMap
},
// Save to album
saveToAlbum() {
saveImageToPhotosAlbum(this.lastPoster).then(res= > {
this.showMask = false;
uni.showToast({
icon: 'none'.title: 'Saved successfully'
})
}).catch(err= >{})},click() {
this.$emit('click')},hideMask(){
this.showMask = false;
this.$emit('hidemask')}}}</script>
<style scoped>
.poster_wrapper {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.canvas {
border: 1px solid # 333333;
}
.mask {
width: 100vw;
height: 100vh;
position: fixed;
background-color: rgba(0.0.0.4);
left: 0;
top: 0;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
}
</style>
Copy the code
Draw the function
Poster.js provides functions related to posters, including parsing configuration parameters, drawing canvas according to configuration parameters, caching canvas as local pictures, saving local pictures to mobile phone albums, etc. In order to see where the configuration parameters are problematic during drawing, a check function is used to quickly locate the problem. The overall implementation is as follows:
// Error collection
const errMsgMap = {
'arc': {'x':'Please specify the starting position of the circle x'.'y':'Please specify the starting position of the circle y'.'r':'Please specify the radius of the circle r'..'sAngle':'Please specify the starting radian of the circle sAngle'.'eAngle':'Please specify the ending radian of the circle eAngle',},'rect': {'x':'Please specify the starting position of the rectangle x'.'y':'Please specify the starting position of the rectangle y'.'w':'Please specify the width of the rectangle W'.'h':'Please specify the height of the rectangle h',},'stroke_rect': {'x':'Please specify the starting position of the rectangle border x'.'y':'Please specify the starting position of the rectangle border y'.'w':'Please specify the width of the rectangle border w'.'h':'Please specify the height of the rectangle border h',},'text': {'x':'Please specify the starting position of the text x'.'y':'Please specify the starting position of the text y'.'text':'Please specify the content of the text text'
},
'image': {'x':'Please specify the starting position of the picture x'.'y':'Please specify the starting position of the picture y'.'w':'Please specify the width of the picture w'.'h':'Please specify the height of the picture h'.'url':'Please specify the path URL of the image'
},
'line': {'path':'Please specify the path of the line'
},
'points': {'points':'Please specify collection points'}};// Draw a collection of methods
const DrawFuncMap = {
drawLine(ctx,config,i){
// Check the required parameters
checkNecessaryParam(config,'line',i,'path');
// Each path describes the beginning and end of a set of lines. This set of lines does not have to be contiguous
// Their shape is the same (line thickness, color), the shape is different and not necessarily continuous
for(let j = 0; j < config.path.length; j++){
const path = config.path[j];
checkNecessaryParam(path,'points'.`${i}-${j}`.'points');
const lineWidth = path.lineWidth || 1;
const lineJoin = path.lineJoin || 'round';
const lineCap = path.lineCap || 'round';
ctx.beginPath();
// Set the color
ctx.setStrokeStyle(path.strokeStyle || '# 333333');
// Set the thickness
ctx.setLineWidth(lineWidth);
// Set the line intersection style
ctx.setLineJoin(lineJoin);
// Set the breakpoint style for the line
ctx.setLineCap(lineCap);
// Walk over the set of points of the line, drawing the line according to the different properties of each point
for(let k = 0; k < path.points.length; k++){
// Get each point
const pointSet = path.points[k];
// If the point is an array collection, the point type is handled directly by lineTo
if(Object.prototype.toString.call(pointSet) === "[object Array]") {if(k === 0) ctx.moveTo(... pointSet);elsectx.lineTo(... pointSet); }else{
// By default, the first dot must be the starting point, and ctx.moveTo moves the brush if the point type is moveTo
if(k === 0 || pointSet.type === 'moveTo'){ ctx.moveTo(... pointSet.point);// Point with type lineTo or no type attribute defaults to lineTo to ctx.lineTo connection
}else if(pointSet.type === 'lineTo' || pointSet.type === undefined){ ctx.lineTo(... pointSet.point); }else if(pointSet.type === 'bezierCurveTo') {constP2 = pointSet.P2 ? pointSet.P2 : pointSet.P1; ctx.bezierCurveTo(... pointSet.P1,... P2,... pointSet.point); }}}// Each set of points (path) ends strokectx.stroke(); }},// Draw a picture
drawImage(ctx,config,i){
checkNecessaryParam(config,'image',i,'x'.'y'.'w'.'h'.'url');
ctx.drawImage(config.url, config.x, config.y, config.w, config.h);
},
/ / draw circle
drawArc(ctx,config,i){
checkNecessaryParam(config,'arc',i,'x'.'y'.'r'.'sAngle'.'eAngle');
ctx.beginPath();
ctx.arc(config.x, config.y, config.r, config.sAngle, config.eAngle);
ctx.setFillStyle(config.fillStyle || '# 333333');
ctx.fill();
ctx.setLineWidth(config.lineWidth || 1);
ctx.setStrokeStyle(config.strokeStyle || '# 333333');
ctx.stroke();
},
// Draw text
drawText(ctx,config,i){
checkNecessaryParam(config,'text',i,'x'.'y'.'text');
ctx.font = config.font || '10px sans-serif';
ctx.setFillStyle(config.color || '# 333333');
ctx.setTextAlign(config.textAlign || 'center');
ctx.fillText(config.text, config.x, config.y);
ctx.stroke();
},
// Draw a rectangle
drawRect(ctx,config,i){
checkNecessaryParam(config,'rect',i,'x'.'y'.'w'.'h');
ctx.beginPath();
ctx.rect(config.x, config.y, config.w, config.h);
ctx.setFillStyle(config.fillStyle || '# 333333');
ctx.fill();
ctx.setLineWidth(config.lineWidth || 1);
ctx.setStrokeStyle(config.strokeStyle || '# 333333');
ctx.stroke();
},
// Draw a non-filled rectangle
drawStrokeRect(ctx,config,i){
checkNecessaryParam(config,'stroke_rect',i,'x'.'y'.'w'.'h');
ctx.beginPath();
ctx.setStrokeStyle(config.strokeStyle || '# 333333');
ctx.setLineWidth(config.lineWidth || 1); ctx.strokeRect(config.x, config.y, config.w, config.h); ctx.stroke(); }},/** * Check the necessary attributes of the draw *@param {Object} ConfigObj Configobject *@param {String} Type Indicates the verification type *@param {String|Number} Index the current error position starts from 0 and corresponds to the index in the drawData configuration. * If it is a String, the index will be separated by a '-'. For example, 1-2 indicates that the second child of the first configuration object in the drawData configuration is faulty, and so on@param {Array} KeyArr collects the key name that needs to be checked **/
function checkNecessaryParam (configObj,type,index,... keyArr){
ErrMsgMap [type] is used as a traversal object for comparison
for(let prop in errMsgMap[type]){
if(configObj[prop] === undefined) {throw new Error(The first `${index}The sequence:${errMsgMap[type][prop]}`)}}}// Obtain the image information, here is the main image cache address
export function loadImage(url) {
return new Promise((resolve, reject) = > {
wx.getImageInfo({
src: url,
success(res) {
resolve(res.path)
},
fail(err) {
reject('Poster resource failed to load')}})})}// Parse the poster object and draw the Canvas poster
export function createPoster(ctx, posterItemList) {
return new Promise((resolve,reject) = >{
try{
for (let i = 0; i < posterItemList.length; i++) {
const temp = posterItemList[i];
if (temp.type === 'image') {
DrawFuncMap.drawImage(ctx,temp.config,i);
} else if (temp.type === 'text') {
DrawFuncMap.drawText(ctx,temp.config,i);
} else if ( temp.type === 'arc' ){
DrawFuncMap.drawArc(ctx,temp.config,i);
} else if (temp.type === 'rect'){
DrawFuncMap.drawRect(ctx,temp.config,i);
} else if (temp.type === 'stroke_rect'){
DrawFuncMap.drawStrokeRect(ctx,temp.config,i);
} else if (temp.type === 'line'){
DrawFuncMap.drawLine(ctx,temp.config,i)
}
}
ctx.draw();
resolve({result:'ok'.msg:'Drawn successfully'})}catch(e){
console.error(e)
reject({result:'fail'.msg:e})
}
})
}
// canvas to image
export function canvasToTempFilePath(canvasId, vm,delay=50) {
return new Promise((resolve, reject) = > {
// It takes a certain amount of time to save the cache after the canvas is drawn. The value is set to 50 milliseconds
setTimeout(() = >{
uni.canvasToTempFilePath({
canvasId: canvasId,
success(res) {
if (res.errMsg && res.errMsg.indexOf('ok') != -1) resolve(res.tempFilePath);
else reject(res)
},
fail(err) {
reject(err)
}
}, vm);
},delay)
})
}
// Save the image to the album
export function saveImageToPhotosAlbum(imagePath) {
return new Promise((resolve, reject) = > {
uni.saveImageToPhotosAlbum({
filePath: imagePath,
success(res) {
resolve(res)
},
fail(err){
reject(err)
}
})
})
}
Copy the code
The open source project
The overall functionality is outlined, and specific use examples can be found in my open source repository or directly in the UNI plug-in market.
This is my second open source, if there are written deficiencies still hope forgive me. If you find any problems or areas that need improvement, feel free to leave a comment below.
Future plans:
1. Users can choose a favorite poster from multiple pictures 2. Expand canvas drawing function on existing basis 3. 4. Solve the problem that the two-dimensional code picture on the poster cannot be updated in time when the two-dimensional code carrying parameters of the mini program changeCopy the code
Uni Plug-in Market
Gitee warehouse