在开发H5项目的过程中,通常会遇到自动生成海报图片的需求,这个实现步骤一开始是不容易做的,细细道来会发现,绘制过程不过如此。
准备写一个生成海报的页面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>
有一个引用的模块文件
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){
//...
}
}
接下来,完善所有方法的未实现的逻辑细节,首先是绘制背景图方法
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以上的大图片
到这里就算要写完成了,尝试运行页面index.html,正常的话,生成的海报效果如下图所示,是垂直布局的效果哦,在绘制方法
draw()
传参数那里改一下配置direction:0
为默认水平布局,试运行就是另外一个效果了。如果要导出图片,在手机浏览器上看,直接用拇指长按将图片另存为即可,或者,自己再加一个保存按钮,实现下载图片方法请点此处了解
参考上面源代码,若能看明白的话,就按照自己的实现方式改一改,就讲到这里了,如阅读中有遇到什么问题,请在文章结尾评论处留言,ヾ( ̄▽ ̄)ByeBye