amandakelake / blog

think more!learn more!
2.25k stars 183 forks source link

canvas合成图片海报 #72

Open amandakelake opened 5 years ago

amandakelake commented 5 years ago

一、业务场景

用户自行选择背景图、头像,手动输入昵称、文案 根据以上元素合成图片,上传到后端,拿到图片在线链接并分享出去 保存图片到本地

二、功能以及难点分解

三、踩坑

1、移动端暂不支持webp格式图片,仅仅后缀名不代表真正的格式,可以通过网络请求看header里面的Content-Type 2、用户选择图片的时候,加载过的url,后面canvas绘制同一张图片的url时,因为缓存,不会重新load,会导致绘制失败,解决办法是绘制时给图片链接加时间戳,破坏缓存 3、分享链接出去给用户时,制作短链,短链本身为一个中间页,利用php动态添加meta头,配置og:image og:title og:description等属性,分享可以动态生成预览信息 分享到FB twitter 4、H5本地下载图片

四、难点伪代码

1、计算绘制canvas时的具体位置

this.dpr = window.devicePixelRatio ? window.devicePixelRatio : 2;
// 这里的计算方法根据具体项目的px/rem/em等进行转换
calcuSize(px) {
  // 计算的时候   需要算上dpr  屏幕分辨率
  if (!this.winWidth) {
    const winWidth = window.innerWidth;
    this.winWidth = winWidth;
    return ((winWidth * px) / 750) * this.dpr;
  } else {
    return ((this.winWidth * px) / 750) * this.dpr;
  }
},

2、绘制一张图片

drawImage(ctx, url, width, height, posX, posY) {
  return new Promise((resolve, reject) => {
    const image = new Image();
    // 这行是canvas绘制图片的跨域关键
    image.setAttribute("crossOrigin", "anonymous");
    image.onload = () => {
      ctx.drawImage(
        image,
        this.calcuSize(posX),
        this.calcuSize(posY),
        this.calcuSize(width),
        this.calcuSize(height)
      );
      // 貌似ctx.drawImage也是异步的,暂时找不到回调,暂等一个循环
      setTimeout(() => {
        resolve(true);
      }, 0);
    };
    image.onerror = () => {
      reject("image load fail");
    };
    // 加时间戳  破坏缓存
    image.src = `${url}?time=${new Date().getTime()}`;
  });
},

3、绘制文案

设置文案大小,也是根据各自的css单位换算规则+dpr

  const htmlFS = document.querySelector("html").style.fontSize;
  const htmlFSPx = htmlFS.split("px")[0];
  ctx.font = `${0.22 * this.dpr * htmlFSPx}px serif`;
drawName(ctx) {
  const htmlFS = document.querySelector("html").style.fontSize;
  const htmlFSPx = htmlFS.split("px")[0];
  ctx.font = `${0.22 * this.dpr * htmlFSPx}px serif`;
  return new Promise(resolve => {
    if (this.username) {
      // 画背景,用很粗的线条
      const nameWidth = ctx.measureText(this.username).width;
      const x = (this.calcuSize(696) - nameWidth) / 2;
      const y = this.calcuSize(576);
      ctx.fillStyle = "rgba(73,107,193,0.8)";
      ctx.strokeStyle = "rgba(73,107,193,0.8)"; // 线条颜色
      ctx.lineCap = "round"; // 线条圆角端点
      ctx.lineWidth = this.calcuSize(37);
      ctx.beginPath();
      ctx.moveTo(x, y + this.calcuSize(20));
      ctx.lineTo(x + nameWidth + this.calcuSize(0), y + this.calcuSize(20));
      ctx.stroke();

      // 绘制文案
      // 这里的居中,是指根据当前x位置两边分布
      ctx.textAlign = "center";
      ctx.fillStyle = "#fff";
      ctx.fillText(
        this.username,
        this.calcuSize(348), // 这里可以直接用canvas宽度的一半
        this.calcuSize(601),
        this.calcuSize(600)
      );
      setTimeout(() => {
        resolve(true);
      }, 0);
    } else {
      resolve(true);
    }
  });
},

4、文案换行

利用ctx.measureText(str)方法计算文案宽度,可以实现换行 但要预先设置好文案的lineHeight,这样才知道下一行文字的y轴位置

canvasTextAutoLine(str, ctx, initX, initY, lineHeight, totalWidth) {
  let lineWidth = 0;
  let canvasWidth = totalWidth;
  let lastSubStrIndex = 0;
  for (let i = 0; i < str.length; i++) {
    lineWidth += ctx.measureText(str[i]).width;
    if (lineWidth > canvasWidth) {
      //减去initX,防止边界出现的问题
      ctx.fillText(str.substring(lastSubStrIndex, i), initX, initY);
      initY += lineHeight;
      lineWidth = 0;
      lastSubStrIndex = i;
    }
    if (i == str.length - 1) {
      ctx.fillText(str.substring(lastSubStrIndex, i + 1), initX, initY);
    }
  }
},

5、canvas转base64图片

const base64Image = canvasEle.toDataURL("image/png", 1);

6、汇总:异步合成图片,并上传

// 利用async/await 写出比较好看的异步流程代码
    async dynamicCreateImage() {
      try {
    // 先动态创建canvas
        let canvasEle = document.createElement("canvas");
        let ctx = canvasEle.getContext("2d");
        canvasEle.width = this.calcuSize(696);
        canvasEle.height = this.calcuSize(750);

    // 绘制图片
        await this.drawBgImg(ctx);
        // 画用户名字、名字背景
        await this.drawName(ctx);
        // 用户头像
        await this.drawHeadImg(ctx);

        // canvas转base64上传图片+分享
    // 上传图片和分享功能,各自实现就好
        const base64Image = canvasEle.toDataURL("image/png", 1);
        const imageOnlineUrl = await this.uploadImage(base64Image);
        this.shareImage(imageOnlineUrl);

        canvasEle = null;
        ctx = null;
      } catch (error) {
    // ... 错误处理
      }
    },

参考

javascript - 如何通过js实现canvas保存图片为png格式并下载到本地! - SegmentFault 思否 在浏览器端用JS创建和下载文件 | AlloyTeam javascript - Capture HTML Canvas as gif/jpg/png/pdf? - Stack Overflow canvas 微信海报分享(个人爬坑) - 掘金 这个很多踩坑经验 canvas文本绘制自动换行、字间距、竖排等实现 « 张鑫旭-鑫空间-鑫生活

wensiyuanseven commented 3 years ago

good!!!