Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

canvas合成图片海报 #72

Open
amandakelake opened this issue May 1, 2019 · 1 comment
Open

canvas合成图片海报 #72

amandakelake opened this issue May 1, 2019 · 1 comment
Labels

Comments

@amandakelake
Copy link
Owner

amandakelake commented May 1, 2019

一、业务场景

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

二、功能以及难点分解

  • 记录用户的自定义图片信息(其实就是状态管理 ,vue/react都很轻易)
  • 用canvas绘制图片、绘制文字,合成图片
    • 图片是否需要预加载,是否需要提前制作离屏canvas,还是实时加载
    • 绘制图片的跨域问题
    • 绘制文字,文字对齐、大小设置,涉及单位转换
    • canvas文字换行,识别换行符
    • canvas绘制的图片质量,跟屏幕dpr有关
  • canvas合成图片并通过toDataURL转成base64格式
  • base64图片上传到后端,后端帮忙上传到服务器,拿回图片在线链接
  • 用户分享图片给好友,分享到whatsapp/FB等的预览图,meta头配置
  • 下载图片(客户端环境/浏览器环境)

三、踩坑

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
Copy link

good!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants