• 【JavaScript】使用Canvas组件自动生成海报图片


    在开发H5项目的过程中,通常会遇到自动生成海报图片的需求,这个实现步骤一开始是不容易做的,细细道来会发现,绘制过程不过如此。

    1. 准备素材

    准备写一个生成海报的页面index.html,参考下面页面源代码,自己准备素材,用三个图片文件代替,支持的图片为jpg,png文件格式

    DOCTYPE html>
    <html>
    	<head>
    		<meta charset="utf-8">
    		<meta name="viewport" content="width=device-width, initial-scale=1"/>
    		<title>海报生成title>
    		<style>
    			body{
    				/* margin: 0; */
    				height: calc(100vh - 40px);
    			}
    			img{
    				width: 100%;
    				height: 100%;
    				box-shadow: 1px 1px 1px 1px rgba(0,0,0,0.3);
    			}
    		style>
    	head>
    	<body>
    		<img id="output_img" alt="生成海报..."/>
    		<script type="module">
    			import Poster from './poster.js';//引用 生成海报的 模块
    			
    			window.onload = () => {
    				// 创建 海报功能 对象实例,传入需要的一些参数
    				let p = new Poster({
    					window,
    					id:'output_img',//传入用于显示生成的图片元素id
    				});
    				
    				// p.draw();
    				// 给其传入配置参数,有的默认可不传
    				p.draw({
    					bgImg:'./img/fGGGjP1ob1541164442344compressflag.jpg',//背景图
    					headImg:'./img/see_yuanfang.jpg',//头像
    					scanImg:'./img/my_csdn.png',//扫描图
    					direction: 1,//布局方向:默认0 水平,1 垂直
    					title: '诗和远方',
    					subtitle: '生活不止眼前的苟且',
    					nick: 'TA远方 @CSDN',
    					text: '关注TA,扫一扫',
    				});
    				
    				// 生成图片后,就算完成了,最后将其销毁
    				p.destroy();
    			}
    		script>
    	body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    2. 编写模块

    有一个引用的模块文件poster.js,需要自己创建,先理清楚生成海报的过程,用一个对象Poster类实现,再把自己想到需要调用的逻辑方法一个个列出来,参考代码如下

    export default class Poster {
    	#canvas;
    	#elemImg;
    	
    	#drawBg;
    	#drawScanImg;
    	#drawHeadImg;
    	
    	constructor(e){
    		const { document } = e.window;
    		const elemImg = document.getElementById(e.id);
    				
    		this.#elemImg = elemImg;
    		//创建虚拟DOM...
    		const canvas = document.createElement('canvas');
    		canvas.width = elemImg.width;
    		canvas.height = elemImg.height;
    		this.#canvas = canvas;
    		
    		const ctx = canvas.getContext('2d');
    		const centerX = canvas.width/2;
    		
    		//绘制背景图方法
    		this.#drawBg = (config) => {
    			//...
    		};
    		//绘制头像图方法
    		this.#drawHeadImg = (config) => {
    			//...
    		};
    		
    		//绘制扫码图片方法
    		this.#drawScanImg = (config) => {
    			//...
    		};
    		
    		// this.draw();
    	}
    		
    	/**
    	 * 销毁
    	 */
    	destroy(){
    		this.#canvas.remove();
    	}
    	
    	/**
    	 * 绘制
    	 */
    	draw(config){
    		//...
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    3. 实现方法

    接下来,完善所有方法的未实现的逻辑细节,首先是绘制背景图方法drawBg(),代码如下

    this.#drawBg = (config) => {
    	const imgData = {
    		padding: 60,
    		margin: 180,
    		image: null,
    		title: '诗和远方',
    		subtitle: '生活不止眼前的苟且',
    		font: 20
    	};
    	Object.assign(imgData,config);//默认配置和传入配置合并
    	//有图片就处理显示,没有的话就显示展位区域
    	if (imgData.image) {
    		ctx.drawImage(imgData.image,0,0,canvas.width,canvas.height-imgData.margin);
    		ctx.fillStyle = '#fff';
    	} else {
    		ctx.rect(0,0,canvas.width,canvas.height-imgData.margin);
    		ctx.stroke();
    		ctx.fillStyle = '#000';
    	}
    	
    	ctx.strokeStyle = 'rgba(255,255,255,0.4)';//描边颜色
    	if (imgData.title) {
    		ctx.lineWidth = 4;
    		ctx.font = (imgData.font) + 'px sans-serif';
    		ctx.strokeText(imgData.title,canvas.width/2,imgData.padding);//给字体描边
    		ctx.fillText(imgData.title,canvas.width/2,imgData.padding);
    	}
    	if (imgData.subtitle) {				
    		ctx.lineWidth = 2;
    		ctx.font = imgData.font*0.8 + 'px sans-serif';//副标题 字体相对标题小80%
    		ctx.strokeText(imgData.subtitle,canvas.width/2,imgData.padding*1.6);
    		ctx.fillText(imgData.subtitle,canvas.width/2,imgData.padding*1.6);
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    还有,实现绘制头像图方法drawHeadImg(),代码如下

    this.#drawHeadImg = (config) => {
    	const headData = {
    		size: 75,
    		padding: 4,
    		margin: 50,
    		nick: 'TA远方',
    		font: 18,
    		image: null,
    		direction: 1,
    		height: 180
    	};
    	Object.assign(headData,config);
    	const isVertical = headData.height && isVerticalDirection(headData.direction);
    	headData.r = headData.size/2;
    	//判断布局方向,是否是垂直排列
    	if (isVertical) {
    		headData.x = centerX;
    		headData.y = canvas.height-headData.height;
    		//有头像图的话 就用白色边框
    		ctx.fillStyle = headData.image ? '#fff' : '#000';
    		ctx.beginPath();
    		ctx.arc(headData.x,headData.y,headData.r+headData.padding,0,2*Math.PI);
    		ctx.fill();
    	}else{
    		headData.x = centerX-headData.margin-headData.padding-headData.r;
    		headData.y = canvas.height-headData.margin-headData.padding-headData.r;
    	}
    		
    	if (headData.image) {
    		ctx.save();
    		ctx.beginPath();
    		ctx.arc(headData.x,headData.y,headData.r,0,2*Math.PI);
    		ctx.clip();//在裁剪区域内绘制 这样就有圆角边效果
    		ctx.drawImage(headData.image,headData.x-headData.r,headData.y-headData.r,headData.size,headData.size);
    		ctx.restore();
    	} else {
    		ctx.fillStyle = '#fff';
    		ctx.beginPath();
    		ctx.arc(headData.x,headData.y,headData.r,0,2*Math.PI);
    		ctx.fill();
    	}
    	
    	if (headData.nick) {
    		ctx.strokeStatyle = '#fff';
    		ctx.fillStyle = '#000';
    		ctx.font = headData.font + 'px sans-serif';
    		ctx.fillText(headData.nick,headData.x,headData.y+headData.size,headData.size+headData.padding*2);
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    还有,实现绘制扫码图方法drawScanImg(),代码如下

    //绘制扫码图片方法
    this.#drawScanImg = (config) => {
    	const imgData = {
    		size: 100,
    		padding: 10,
    		margin: 30,
    		text: '关注TA,扫一扫',
    		font: 10,
    		image: null,
    		direction: 1,
    	};
    	Object.assign(imgData,config);
    	//判断布局方向,是否是垂直排列
    	if (isVerticalDirection(imgData.direction)) {
    		imgData.x = centerX-imgData.size/2;
    	}else {
    		imgData.x = canvas.width-imgData.margin-imgData.size;
    	}
    	imgData.y = canvas.height-imgData.margin-imgData.size;
    	
    	if (imgData.image) {
    		ctx.drawImage(imgData.image,imgData.x,imgData.y,imgData.size,imgData.size);
    	}else {
    		ctx.strokeStyle = '#000';
    		ctx.rect(imgData.x,imgData.y,imgData.size,imgData.size);
    		ctx.stroke();
    	}
    	
    	if (imgData.text) {
    		ctx.fillStyle = '#000';
    		ctx.font = imgData.font + 'px sans-serif';
    		ctx.textBaseline = 'top';
    		ctx.fillText(imgData.text,imgData.x+imgData.size/2,canvas.height-imgData.margin+imgData.padding);
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    以上代码中,有用到了isVerticalDirection()方法,如还不知道怎么写,那就贴出来吧,代码如下,用于判断配置的

    const isVerticalDirection = (direction) => {
    	switch(typeof direction){
    		case 'number':
    			return direction==1;
    		case 'boolean':
    			return direction;
    		default:
    			return direction=='vertical' || direction.charAt(0)=='v';
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    还有,最后的一个绘制方法draw(),代码如下,调用此方法时,按照对应的配置传即可,若不传的话,它就用默认的配置参数来绘制

    /**
     * 绘制
     */
    draw(config){
    	const canvas = this.#canvas;
    	const drawData = {
    		bgImg:'',
    		headImg:'',
    		scanImg:'',
    		direction: 0,
    		bgImgHeight: 200,//背景图片默认高度
    	};
    	Object.assign(drawData,config);
    	config.bgImgHeight = Math.min(canvas.height/2,config.bgImgHeight);
    	//异步处理加载所有图片资源
    	Promise.all([drawData.bgImg,drawData.headImg,drawData.scanImg].map((value,index)=>{
    		return new Promise((resolve,reject)=>{
    			if (value) {
    				let img = new Image();
    				img.onload = () => resolve({index,image:img});
    				img.onerror = (err) => reject({
    					errMsg: `index ${index} img src has error`,
    					error: err
    				});
    				img.src = value;
    			}else{
    				resolve({index,image:null});
    			}
    		});
    	})).then((res)=>{
    		//调用到此处,表示所有图片资源加载完成...执行绘制逻辑...
    		const ctx = canvas.getContext('2d');
    		//初始设置
    		ctx.textBaseline = 'alphabetic';
    		ctx.textAlign = 'center';
    		ctx.fillStyle = '#fff';
    		// ctx.clearRect(0,0,canvas.width,canvas.height);
    		ctx.rect(0,0,canvas.width,canvas.height);
    		ctx.fill();//填空白色背景
    		//绘制所有图片资源
    		res.forEach((item)=>{
    			switch(item.index){
    				case 0:
    					this.#drawBg({
    						image:item.image,
    						margin:canvas.height-drawData.bgImgHeight,
    						title:drawData.title,
    						subtitle:drawData.subtitle
    					});
    					break;
    				case 1:
    					this.#drawHeadImg({
    						image:item.image,
    						direction:drawData.direction,
    						height:canvas.height-drawData.bgImgHeight,
    						nick:drawData.nick
    					});
    					break;
    				case 2:
    					this.#drawScanImg({
    						image:item.image,
    						direction:drawData.direction,
    						text:drawData.text
    					});
    					break;
    				default:
    			}
    		});
    		this.#elemImg.src = this.#canvas.toDataURL();//将生成的图片设置图片元素中
    	}).catch((err)=>{
    		throw new Error(err)//如加载图片异常,就抛出给上层调用者处理
    	});
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    💡小提示

    • 会发现文章代码中用到了Promise.all()方法,如果不清楚此Promise用法,可点此前往了解
    • 因为代码中Image.onload() 是异步处理方法,在服务器上加载可能会耗时,也就是说,生成的海报可能要等待图片加载完成才能继续
    • 使用的图片如果放在服务器上,尽量不要放文件大小超过1MB以上的大图片

    4. 生成海报

    到这里就算要写完成了,尝试运行页面index.html,正常的话,生成的海报效果如下图所示,是垂直布局的效果哦,在绘制方法draw()传参数那里改一下配置direction:0为默认水平布局,试运行就是另外一个效果了。

    如果要导出图片,在手机浏览器上看,直接用拇指长按将图片另存为即可,或者,自己再加一个保存按钮,实现下载图片方法请点此处了解

    在这里插入图片描述

    参考上面源代码,若能看明白的话,就按照自己的实现方式改一改,就讲到这里了,如阅读中有遇到什么问题,请在文章结尾评论处留言,ヾ( ̄▽ ̄)ByeBye

  • 相关阅读:
    JAVA:实现使用快速排序算法获取给定数组中的第 k 个最大或第 k 个最小元素算法(附完整源码)
    机器人控制算法九之 位姿描述与空间变换
    Redis 主从搭建和哨兵搭建
    苹果AppleMacOs最新Sonoma系统本地训练和推理GPT-SoVITS模型实践
    C++STL之<set>和<map>
    使用“纯”Servlet做一个单表的CRUD操作
    Linux系统磁盘挂载和卸载教程,详细介绍挂载点、命令及最佳实践
    力扣热题100_普通数组_238_除自身以外数组的乘积
    微信小程序(五十二)开屏页面效果
    UBoot初次编译
  • 原文地址:https://blog.csdn.net/zs1028/article/details/127717851