记一次截图+拼图的前端实现

这次主要记录有关前端截图和拼图等处理。

缘由:接了一个活动需求,要求页面打开之后,可以长按触发保存图片,并且图片下方需要带上图和二维码的内容,以方便图片分享到朋友圈后可以长按识别二维码打开页面。(有时候纯分享页面链接到朋友圈,好友未必会点进去,图片的分享方式比点链接高一些吧~)

接到需求后

技术预研

  1. 前端截图可以考虑html2canvas,效果不错,截图必备,其最后会返回一个canvas元素,使用canvas.toDataURL可以获取到图片的dataUrl
  2. 截图完毕后还要拼图,可以考虑直接用canvas来绘图即可
  3. 长按页面触发弹出“保存图片”交互-保存图片要看打开页面的客户端的实现情况,微信和一部分浏览器早已支持长按图片触发是否保存图片的交互,所以结合1和2的图,只要提前将图片覆盖在页面上面,并且设置透明,即可实现长按页面触发保存图片的操作。

实际开发实现

截图 html2canvas

虽然html2canvas好用,但是它还是有一些需要注意到的地方,开发过程中遇到的主要是两个问题:a. 涉及的图片跨域 b. 不支持渐变色字的截图

先说一下html2canvas的基本使用:它接受一个Element参数和Options对象参数,前者指的是将要截图的DOM,后者则是截图的相关配置.可以看这里>>

目前html2canvas方法返回的是一个promise(在前期在线预研的时候,用cdn引了一个比较旧的版本,那时的html2canvas还不够成熟,不会promise,导致用的时候报then is not a function)

实际使用的代码如下,很简单。

const contentDom: HTMLElement | null = document.querySelector('.draw-target');
      if (contentDom) {
        html2canvas(contentDom, {
          x: 0,
          y: 0,
          // 支持跨域访问
          useCORS: true,
          // 移除不需要汇入的元素,越少越好
          ignoreElements: element => {
            // 这里指定了忽略.no-pic的元素
            if (element.className.indexOf('no-pic') !== -1) {
              return true;
            }
            return false;
          }
        })
          .then(canvas => {
            canvas.getContext('2d').imageSmoothingEnabled = false;
            resolve(canvas.toDataURL('image/png'));
          })
          .catch(error => {
            console.log('生成报错', error);
            reject(error);
          });
      } else {
        reject('error');
      }

代码很简单,但是页面涉及到图片资源跨域的时候,就得配合服务端解决图片跨域的问题了,这里要注意:其他域图片虽然能被页面展示出来,但不代表它能被脚本跨域访问。所以考虑截图需求的时候一定要判断好是否有跨域的图片资源,要让图片那边接受js的跨域调用,通常这个只需要nginx上配一下就好了。

拼图实现

首先已知,我们通过上面提到的截图,拿到了图片的data url,然后手里还有一个要拼接上的图片,可以用canvas和它的drawImage搞定。

已知,手里有两个图片的地址,实际上,我们要做的只是创建img并且给img.src给填上图片地址,在img.onload那里把img交给canvas的drawIamge去绘图即可,同样这里绘图也是考虑跨域问题的,不过我的需求里两个图片,第一个本身就是一个data url可以说没跨域了,或者说前面已经解决了,第二个可以考虑就和脚本放一起,那就没有跨域问题啦。

然后拼图要考虑到,图片之间尺寸大小不一,在我的需求里:A图(页面截图) 和 B图(纯图片),最终拼成一张C图,一般C图的宽度就按A图的宽度来用即可,也就是A图为基础图,B图适应A图的宽度来重新调整绘入时的长宽(widthFinalB = widthA base; heightFinalB = widthFinalB / widthB heightB) 这里简单起见,B图在设计稿上base为1.
image.png
先把img赋值部分封装成一个promise

const getImg2Draw = (src): Promise<ImageObj> => {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => {
      resolve({
        img,
        width: img.naturalWidth,
        height: img.naturalHeight
      })
    };
    img.onerror = (err) => {
      reject(err)
    }
    img.src = src;
   })
}

这样最后promise会给我返回图片以及它的实际长宽,方便计算。

然后分别canvas将A图和B图画上canvas

// 获取A图
getContentImg().then(base64 => {
      // 获取截图部分
      Promise.all([getImg2Draw(base64),
        // 获取二维码部分
        getImg2Draw('./public/extra.png')])
        .then((imgs) => {
          const [mainImg, extraImg] = imgs
          const $canvas = document.getElementById('canvas') as HTMLCanvasElement
          // 获取截图宽度,截图是基于html的所以也是实际页面宽度
          const mainWidth = mainImg.width;
          // 获取截图的高度
          const mainHeight = mainImg.height;
          // B的原始宽度
          const extraWidth = extraImg.width;
          // B的原始高度
          const extraHeight = extraImg.height;
          // 设置canvas的宽高
          $canvas.width = mainWidth
          // 计算出基于A图宽的比例B图最终高度
          const appendHeight = mainWidth / extraWidth * extraHeight
          // 计算整体高度
          const allHeight = mainHeight + appendHeight 
          $canvas.height = allHeight
          // 清空旧内容
          canvas.clearRect(0, 0, mainWidth, allHeight)

          // 先把A图怼上去
          canvas.drawImage(mainImg.img, 0, 0, mainWidth, mainHeight)
          // 再把B图部分怼上去
          canvas.drawImage(extraImg.img, 0, mainHeight, mainWidth, appendHeight)
          // 得到最终绘图
          const img = new Image();
          img.src = $canvas.toDataURL('image/png');
          document.body.appendChild(img);
    }).catch(err => {
      console.error(err)
    })

这个时候遇到另外一个问题:拼图的背景色,由于我这边拼图是两块图直接拼在一起,如果各自有各自的背景色倒是没什么关系,拼图的时候各自保留各自背景色即可,但我这次用的是统一背景色,而且还是渐变的背景色。如果两张图各自掌管各自颜色,很容易出现拼接时有一条线,除非画图的时候把两边接触面的背景色计算好,以便无缝衔接。

这里直接由最终绘图的canvas来自行画背景色,两张图处理时都不要自己的背景色,也就是先画布画好背景,然后再两张图往画布贴上即可。

渐变背景的画法:

// 背景色用渐变色自己画
          const gradientBg = canvas.createLinearGradient(0, 0, 0, allHeight)
          gradientBg.addColorStop(0, '#5D79A7')
          gradientBg.addColorStop(1, '#304D7C')
          canvas.fillStyle = gradientBg
          canvas.fillRect(0, 0, mainWidth, allHeight)

把这段画背景色放在绘AB图之前即可,但是可能html2canvas在使用的时候要加一下参数,以及页面的背景色要处理一下。

  1. 保证A图的整块标签元素样式上是没有背景的,子元素有无所谓,这样我才能截一个没底色的图
  2. html2canvas默认底色是白色,所以在使用时要传参:backgroundColor: null

长按图片分享

这块实际上简单化,在页面渲染完后生成图片覆盖在页面上方,用绝对定位处理,并透明展示。

img.style.cssText = 'width:100%;height:100%;position:absolute;top:0;left:0;right:0;bottom:0;opacity:0;z-index:9;';

总结

综上,基本完成了截图+拼图的实现,其实回想起来实现上很简单,就是实现过程要解决各种各样的问题,不过方法总比问题多~

其实期间还遇到过一些坑,以及还没解决但最后和需求无关紧要的问题:

  1. 页面调接口跨域,这个跟截图没啥关联性,只不过以往处理跨域没去理解关于预检请求的响应处理,对这方面缺乏认知,找了其他同事帮忙确认处理,要了解什么情况会出现预检请求options,以及要服务端处理options请求。CORS>>
  2. html2canvas不支持渐变色字体,截成的文字变成了长方形,后来和UI协商以及后续变动,没有了渐变色字体,就暂时不处理了,其实要处理应该也是没问题的,利用canvas的绘图能力来单独绘制,不过就会相当麻烦一些了。
  3. 长按页面是否有优化的手段,提前生成图片后覆盖在页面上方,很容易牺牲掉页面的一些交互互动,不过既然有互动,生成图的时机也就相应会有动态变化了。关于长按页面生成图还是可以想的,但是没想到怎么让生成的图片覆盖在页面上方之后能延续前面的长按响应,实际尝试下,长按页面按的是页面,即便不松手,后来而上的图片也不会鸟你,除非你再一次长按(这次按的是图了)。长按页面生成图片可以参考这里>>

最后补充一下文中没提及的参考资料:
canvas渐变
关于drawImage

阅读 993

推荐阅读
I Dont know
用户专栏

在sf下的小屋~[链接][链接]

6 人关注
13 篇文章
专栏主页