記一次截圖+拼圖的前端實現

此次主要記錄有關前端截圖和拼圖等處理。css

原因:接了一個活動需求,要求頁面打開以後,能夠長按觸發保存圖片,而且圖片下方須要帶上圖和二維碼的內容,以方便圖片分享到朋友圈後能夠長按識別二維碼打開頁面。(有時候純分享頁面連接到朋友圈,好友未必會點進去,圖片的分享方式比點連接高一些吧~)html

接到需求後前端

技術預研

  1. 前端截圖能夠考慮html2canvas,效果不錯,截圖必備,其最後會返回一個canvas元素,使用canvas.toDataURL能夠獲取到圖片的dataUrl
  2. 截圖完畢後還要拼圖,能夠考慮直接用canvas來繪圖便可
  3. 長按頁面觸發彈出「保存圖片」交互-保存圖片要看打開頁面的客戶端的實現狀況,微信和一部分瀏覽器早已支持長按圖片觸發是否保存圖片的交互,因此結合1和2的圖,只要提早將圖片覆蓋在頁面上面,而且設置透明,便可實現長按頁面觸發保存圖片的操做。

實際開發實現

截圖 html2canvas

雖然html2canvas好用,可是它仍是有一些須要注意到的地方,開發過程當中遇到的主要是兩個問題:a. 涉及的圖片跨域 b. 不支持漸變色字的截圖nginx

先說一下html2canvas的基本使用:它接受一個Element參數和Options對象參數,前者指的是將要截圖的DOM,後者則是截圖的相關配置.能夠看這裏>>git

目前html2canvas方法返回的是一個promise(在前期在線預研的時候,用cdn引了一個比較舊的版本,那時的html2canvas還不夠成熟,不會promise,致使用的時候報then is not a function)github

實際使用的代碼以下,很簡單。canvas

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搞定。promise

已知,手裏有兩個圖片的地址,實際上,咱們要作的只是建立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

相關文章
相關標籤/搜索