自己動手實現一個html2canvas

前言

昨天寫了新手引導動畫的4種實現方式,
裏面用到了 html2canvas 於是就順便了解了一下實現思路.

大概就是 利用 svgforeignObject 標籤, 嵌入 dom, 最後再利用 canvas 繪製 svg. 從而實現最終目的.

先讓大家看看效果

圖片描述

MDN示例

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

var data = '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">' +
           '<foreignObject width="100%" height="100%">' +
           '<div xmlns="http://www.w3.org/1999/xhtml" style="font-size:40px">' +
             '<em>I</em> like' +
             '<span style="color:white; text-shadow:0 0 2px blue;">' +
             'cheese</span>' +
           '</div>' +
           '</foreignObject>' +
           '</svg>';

var DOMURL = window.URL || window.webkitURL || window;

var img = new Image();
var svg = new Blob([data], {type: 'image/svg+xml;charset=utf-8'});
var url = DOMURL.createObjectURL(svg);

img.onload = function () {
  ctx.drawImage(img, 0, 0);
  DOMURL.revokeObjectURL(url);
}

img.src = url;

MDN示例其實寫的很清楚,不過也相對比較簡單一點, dom 是已經構建好的字符串, 其實我覺得整個過程裏面最麻煩的就是構建 dom. 所以接下來,我們就來看看具體怎麼實現吧

第一步 遍歷目標節點的所有子元素,並構建對應的字符串

/**
 * 遞歸遍歷所有子節點
 * @param element Document Element 要計算的元素
 * @param isTop Boolean 是否是最外層元素
**/
function renderDom (element, isTop) {
    let tag = element.tagName.toLowerCase()
    let str = `<${tag} `
    // 最外層的節點,需要加 xmlns 命名空間
    isTop && (str += `xmlns="http://www.w3.org/1999/xhtml" `)
    str += ` style="${getElementStyles(element)}">\n`

    if (element.children.length) {
        // 遞歸子元素
        for (let el of element.children) {
            str += renderDom(el)
        }
    } else {
        str += element.innerHTML
    }
    str += `</${tag}>\n`
    return str
}
這裏只做了一個最簡單的處理,由於是簡單實現,很多特殊情況沒考慮進去(如:單標籤, img等),有興趣的童鞋可以自己嘗試實現看看.

最外層的元素, 需要加命名空間,否則無法識別

這裏用到的 getElementStyles 就是獲取元素的最終渲染樣式,下一步會實現.

第二步, 獲取元素的最終渲染樣式,並拼接成行內樣式

正常的 dom 元素, 是無法直接放在 foreignObject 裏面準確地渲染的, 因爲還要涉及到父子元素直接的屬性繼承, 元素默認屬性, 非行內樣式無法渲染等問題.
所以我們要獲取每個元素的最終渲染樣式, 然後拼接成行內樣式.

如何獲取元素的最終渲染樣式呢? 剛好,瀏覽器有提供一個 window.getComputedStyle() 方法可以做到.

// 計算每個 dom 的樣式
// 這裏本來應該直接用 Object.keys + forEach 遍歷取出的
// 但是不知道爲什麼,遍歷取出的,會渲染不出來,應該是某些屬性有問題
// 暫時沒空去排查那些有問題,所以目前先把常用的直接寫死.
function getElementStyles (el) {
    let css = window.getComputedStyle(el)
    let style = ''
    // 尺寸相關
    style += `width:${css.width};`
    style += `height: ${css.height};`
    style += `line-height: ${css.lineHeight};`
    style += `max-height: ${css.maxHeight};`
    style += `min-height: ${css.minHeight};`
    style += `max-width: ${css.maxWidth};`
    style += `min-width: ${css.minWidth};`

    style += `font-size: ${css.fontSize};`
    // 顏色相關
    style += `color: ${css.color};`
    style += `background: ${css.background};`
    // 邊框相關
    style += `border: ${css.border};`
    style += `box-sizing: ${css.boxSizing};`
    // 位置相關
    style += `margin: ${css.margin};`
    style += `padding: ${css.padding};`
    style += `position: ${css.position};`
    style += `left: ${css.left};`
    style += `right: ${css.right};`
    style += `top: ${css.top};`
    style += `bottom: ${css.bottom};`
    // 佈局相關
    style += `display: ${css.display};`
    style += `flex: ${css.flex};`
    return style
}

第三步, 渲染 svg

把拼接好的 svg 字符串用 Blob 對象 new 出來(Blob真的是個很強大的對象啊), 然後用 DOMURL.createObjectURL() 轉換爲 url,
有了url, 接下來就看大家自由發揮了. 可以直接下載,也可以在 canvas 裏繪製. 或者當作圖片直接插入到文檔...


// 主入口函數
function shotScreen () {
    let target = document.querySelector('.content')
    let data = getSvgDomString(target)

    let DOMURL = window.URL || window.webkitURL || window;

    let img = new Image();
    let svg = new Blob([data], {type: 'image/svg+xml;charset=utf-8'});
    let url = DOMURL.createObjectURL(svg);

    img.src = url;
    document.body.appendChild(img)
}

// 計算 svg 的字符串
function getSvgDomString (element) {
    return `
    <svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">\n
       <foreignObject width="100%" height="100%">\n
          ${renderDom(element, 1)}
       </foreignObject>\n
   </svg>`
}

這裏順便給個繪製到 canvas 裏的代碼

//  如果想畫到 canvas 裏面
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
let img = new Image();

img.onload = function () {
   ctx.drawImage(img, 0, 0);
   DOMURL.revokeObjectURL(url);
}

最後

參考文檔:

MDN: 將 DOM 對象繪製到 canvas 中

MDN: foreignObject

完整的代碼在這裏,可以直接運行看效果.

本文地址在->個人技術帖合集, 歡迎給個 start 或 follow

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章