掌握这项技能,让你的小程序瞬间爆火——海报生成功能全解析

814 阅读4分钟

引言

最近做了一个关于海报分享的功能,没有用相关的海报插件实现,我将详细用代码展示如何实现这一功能。 如果你们的海报也是前端实现的,希望能帮助到你~

Snipaste_2024-12-26_13-38-37.png
注意事项
  • 绘画时要注意不同手机展示的效果情况,建议可以默认用苹果6的大小来适配所有手机;
  • canvas画布是2倍,所以后面绘画坐标的时候,有些会*2;
  • 要注意绘画的顺序,所以建议绘画时涉及到异步的,都return promise;
  • 针对文案过长时,需要做一步截取分行处理;
  • 生成临时图片路径时,因为绘画需要点时间,建议可以加点loading过度下哦。

1.先定义canvas组件及相关数据

<!-- canvas海报 -->
<canvas id="poster" type="2d" class="cavas-wrapper" style="width: {{postWidth}}px; height: {{postHeight}}px"></canvas>

// 画布大小
const postWidth = 375;
const postHeight = 615;
let dpr = 1; // 分辨率;
// 默认样式
let defaultSyle = {
  avatarSize: 126,
  codeSize: 100,
  top: 136,
  nameSize: 36,
  decSize: 30,
  bgUrl: '/bg-default.png',
  avatarUrl: '/default-avatar.png'
};

let description = '邀请你付费升级会员,尊享更多权益!';
let decWidth = 0; // 描述文案宽度
let nameWidth = 0; // 名字宽度
let customerName = ''; // 会员名字
let fixWidth = 275 * 2; // 文案之间的最大固定宽度,用于文案分行


/*如果想要看到canvas的画出来的实际效果,可以把此css注释*/
.cavas-wrapper {
  position: absolute;
  top: -10000rpx;
}

2.我这边主要绘画的有:背景图,头像,文案,太阳码

  onLoad(options) {
    this.handleCanvas();
  },
  
  //获取canvas实例
  async handleCanvas() {
    const query = wx.createSelectorQuery();
    query.select('#poster')
      .fields({ node: true, size: true })
      .exec(async (res) => {
        canvas = res[0].node;
        ctx = canvas.getContext('2d');

        // 初始化画布大小
        canvas.width = postWidth * dpr;
        canvas.height = postHeight * dpr;
        ctx.scale(dpr / 2, dpr / 2);

        let { userInfo, posterDetail } = this.data;
        let { decSize, nameSize } = defaultSyle;
        
        // 文案宽度 
        ctx.font = `${decSize}px sans-serif`;
        decWidth = ctx.measureText(description).width;
        console.log('decWidth===', decWidth);
        // 会员名字宽度
        ctx.font = `bold ${nameSize}px sans-serif`;
        customerName = userInfo.customer_name;
        nameWidth = ctx.measureText(customerName).width;
       
        // 绘画背景图
        await this.handleDrawBg();
        // 绘制太阳码
        await this.handleDrawCode();
        // 绘制头像
        await this.handleAvatar();
        // 绘制姓名
        this.handleCustomerName();
        // 绘制宣传文案
        this.handleDec();
        }

      });
  },
  
  // 绘制背景图
  handleDrawBg() {
    console.log('开始绘画背景-------')
    const image = canvas.createImage();
    image.src = defaultSyle.bgUrl;
    
    return new Promise((resolve, reject) => {
      // 图片加载完成回调
      image.onload = () => {
        // 开始处理背景图实现cover效果,这是避免如果用户没有按照一定的比例上传图片,背景图不会铺满
        let sx, sy, sw, sh, imgRatio, canvasRatio;
        canvasRatio = postWidth / postHeight
        imgRatio = image.width / image.height;
        // 图片宽高比 <= 画布宽高比时
        if (imgRatio <= canvasRatio) {
          sw = image.width
          sh = sw / canvasRatio
          sx = 0
          sy = (image.height - sh) / 2
        }
        // 当图片宽高比 >= 画布宽高比时
        else {
          sh = image.height
          sw = sh * canvasRatio
          sx = (image.width - sw) / 2
          sy = 0
        };
        // // 绘制背景图,因为画布他是默认2倍的,所以画在画布时,都要*2
        ctx.drawImage(image, sx, sy, sw, sh, 0, 0, postWidth * 2, postHeight * 2)
        console.log('背景画图完毕');
        resolve();
      };
    })
  },
  
  // 绘制头像
  handleAvatar() {
    console.log('开始绘画头像-------');

    const image = canvas.createImage();
    let { userInfo } = this.data;
    image.src = userInfo.head_image;

    let avatarSize = defaultSyle.avatarSize; // 头像大小
    
    // 计算圆的绘画位置,这里大家可以自行去调试,我这边的数据基本都是以居中为主。
    let x1 = postWidth;
    let y1 = defaultSyle.top + (avatarSize / 2) + 1;

    // 计算头像的绘画位置
    let x2 = x1 - (avatarSize / 2);
    let y2 = y1 - (avatarSize / 2);

    ctx.save();
    ctx.beginPath();
    ctx.arc(x1, y1, avatarSize / 2, 0, 2 * Math.PI); // 绘画圆形
    ctx.clip();

    return new Promise((resolve, reject) => {
      // 图片加载完成回调
      image.onload = () => {
        ctx.drawImage(image, x2, y2, avatarSize, avatarSize);
        ctx.restore();
        console.log('头像画图完毕');
        resolve();
      };
    })
  },
  
    // 绘制会员姓名
  handleCustomerName() {
   let { nameSize } = defaultSyle;
    ctx.font = `bold ${nameSize}px sans-serif`;
    ctx.fillStyle = '#222';

    let textLen = 0; // 记录字符长度
    if (nameWidth > fixWidth) {
      console.log('名字超字符===');
      // 开始处理字符截取
      let overWidth = nameWidth - fixWidth;
      textLen = Math.ceil(overWidth / nameSize);
      let line1 = customerName.slice(0, customerName.length - textLen - 1);

      let x = postWidth - (ctx.measureText(line1).width / 2);
      let y = defaultSyle.top + defaultSyle.avatarSize + 70;
      ctx.fillText(`${line1}...`, x, y);
    } else {
      console.log('名字不超字符===');
      let x = postWidth - (nameWidth / 2);
      let y = defaultSyle.top + defaultSyle.avatarSize + 70;
      ctx.fillText(customerName, x, y);

    }
  },
  
    // 绘制宣传文案
  handleDec() {
    let { decSize } = defaultSyle;
    ctx.font = `${decSize}px sans-serif`;
    ctx.fillStyle = '#6F6F6F';
    let str = description;

    let textLen = 0; // 记录字符长度
    if (decWidth > fixWidth) {
      console.log('宣传超出字符=====');
      // 开始处理字符截取
      let overWidth = decWidth - fixWidth;
      textLen = Math.ceil(overWidth / decSize);
      let line1 = str.slice(0, str.length - textLen);
      let line2 = str.slice(str.length - textLen);
      
      // 计算第一行文字的绘画位置
      let x1 = postWidth - (ctx.measureText(line1).width / 2);
      let y1 = defaultSyle.top + defaultSyle.avatarSize + 60 + 36 + 30;
      
      // 计算第二行文字的绘画位置
      let x2 = postWidth - (ctx.measureText(line2).width / 2);
      let y2 = y1 + 40;

      ctx.fillText(line1, x1, y1);
      ctx.fillText(line2, x2, y2);
    } else {
      console.log('宣传不超出字符');
      let x = postWidth - (decWidth / 2);
      let y = defaultSyle.top + defaultSyle.avatarSize + 60 + 36 + 30;
      ctx.fillText(str, x, y);
    }
  },
  
  // 绘制太阳码
  handleDrawCode() {
    console.log('开始绘画码-------');
     let {codeSize} = defaultSyle;
     
    let codeUrl = ‘/code.png’;
    const image = canvas.createImage();
    image.src = codeUrl;

    // 计算圆的绘画位置
    let x1 = postWidth + (codeSize * 2)
    let y1 = postHeight - 60 + (codeSize * 2);
    
    // 计算头像绘画的位置
    let x2 = x1 - codeSize;
    let y2 = y1 - codeSize;

    ctx.save();
    ctx.beginPath();
    ctx.fillStyle = '#fff';

    ctx.arc(x1, y1, codeSize, 0, Math.PI * 2, false); // 绘画圆形
    ctx.clip();
    ctx.fill();

    return new Promise((resolve, reject) => {
      // 图片加载完成回调
      image.onload = () => {
        ctx.drawImage(image, x2, y2, codeSize * 2, codeSize * 2);
        console.log('码画图完毕');
        ctx.restore();
        resolve();
      };
    })
  },
  

3.获取canvas生成的临时图片路径

  // 获取临时图片路径
  hanldeGetCanvasTempFilePath(cb) {
    wx.showLoading({
      title: '图片生成中',
      mask: true,
    });

    wx.canvasToTempFilePath({
      x: 0,
      y: 0,
      width: postWidth,
      height: postHeight,
      destWidth: canvas.width,
      destHeight: canvas.height,
      canvas,
      success: (res) => {
        console.log('res.tempFilePath=====', res.tempFilePath)
        setTimeout(() => {
          wx.hideLoading();
        }, 1000)
      },
      fail: (err) => {
        console.log('canvas生成图片失败===', err);
        wx.showToast({
          title: '图片生成失败',
          icon: 'none',
          duration: 1500,
        })

      },
    });
  },

End~