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

Detailed renderings