【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>

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){
		//...
	}
}

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);
	}
};

还有,实现绘制头像图方法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);
	}
};

还有,实现绘制扫码图方法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);
	}
};

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

const isVerticalDirection = (direction) => {
	switch(typeof direction){
		case 'number':
			return direction==1;
		case 'boolean':
			return direction;
		default:
			return direction=='vertical' || direction.charAt(0)=='v';
	}
};

还有,最后的一个绘制方法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)//如加载图片异常,就抛出给上层调用者处理
	});
}

💡小提示

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

4. 生成海报

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

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

在这里插入图片描述

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

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TA远方

谢谢!收到你的爱╮(╯▽╰)╭

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值