浅谈html2canvas实现浏览器截图的原理

1,374 阅读5分钟

1、什么是html2canvas

html2canvas可以直接截取特定dom元素内的所有内容并导出图片 官网地址: html2canvas.hertzen.com/

2、使用方式

  • 安装
npm i html2canvas  -S
  • 引入使用
// vue文件
   <!-- 海报html  -->
  <template>
    <div class="c-code" v-if="showOriImg">
        <div class="poster-wrap" ref="box" id="box">
            <img class="bg-poster" :src="qr_code_info.poster_url" />
            <img
                :src="qr_code_info.url"
                alt="二维码"
                class="c-code-img"
            />
            <div>第一份测试文案</div>
            <p style="font-size: 40px">test</p>
            <p>中文</p>
        </div>
    </div>
    <!-- 制作的截图图片 -->
    <div class="c-code" v-if="!showOriImg && posterImg">
        <div class="poster-wrap">
            <img
                class="bg-poster"
                style="width: 100%"
                :src="posterImg"
            />
        </div>
    </div>
</template>
<script>
import html2canvas from 'html2canvas';

methods: {
// 设置背景二维码
     setBgCode() {
          var width = document.getElementById('box').offsetWidth,
              height = document.getElementById('box').offsetHeight,
              scale = window.devicePixelRatio; //放大倍数
          console.log('setBgCode-----------');
          console.log(width);
          console.log(height);
          debugger;
          html2canvas(this.$refs.box, {
              scale,
              width: width,
              heigth: height,
              useCORS: true // 【重要】开启跨域配置
          }).then(canvas => {
              var context = canvas.getContext('2d');
              // 【重要】关闭抗锯齿
              context.mozImageSmoothingEnabled = false;
              context.webkitImageSmoothingEnabled = false;
              context.msImageSmoothingEnabled = false;
              context.imageSmoothingEnabled = false;

              const posterDataUrl = canvas.toDataURL('image/png', 1);
              this.posterImg = posterDataUrl;
              this.showOriImg = false;
             
          });
      }
   }
</script>

大致就是调用 html2canvas()函数 传入 第一个参数dom,第二个参数是配置项, 然后会返回一个promise ,then里面的value是一个canvas,拿到canvas就可以使用toDataURL导出一张base64图片地址: canvas.toDataURL('image/png', 1);

3、原理分析

  1. 确定入口 在这里插入图片描述 在这里插入图片描述
  2. 入口方法
const html2canvas = (element: HTMLElement, options: Partial<Options> = {}): Promise<HTMLCanvasElement> => {
    return renderElement(element, options);
};

入口方法内部执行了 renderElement

const renderElement = async (element: HTMLElement, opts: Partial<Options>): Promise<HTMLCanvasElement> => {
    if (!element || typeof element !== 'object') {
        return Promise.reject('Invalid element provided as first argument');
    }
    const ownerDocument = element.ownerDocument;

    if (!ownerDocument) {
        throw new Error(`Element is not attached to a Document`);
    }

    const defaultView = ownerDocument.defaultView;

    if (!defaultView) {
        throw new Error(`Document is not attached to a Window`);
    }

    const resourceOptions = {
        allowTaint: opts.allowTaint ?? false,
        imageTimeout: opts.imageTimeout ?? 15000,
        proxy: opts.proxy,
        useCORS: opts.useCORS ?? false
    };

    const contextOptions = {
        logging: opts.logging ?? true,
        cache: opts.cache,
        ...resourceOptions
    };

    const windowOptions = {
        windowWidth: opts.windowWidth ?? defaultView.innerWidth,
        windowHeight: opts.windowHeight ?? defaultView.innerHeight,
        scrollX: opts.scrollX ?? defaultView.pageXOffset,
        scrollY: opts.scrollY ?? defaultView.pageYOffset
    };

    const windowBounds = new Bounds(
        windowOptions.scrollX,
        windowOptions.scrollY,
        windowOptions.windowWidth,
        windowOptions.windowHeight
    );

    const context = new Context(contextOptions, windowBounds);

    const foreignObjectRendering = opts.foreignObjectRendering ?? false;

    const cloneOptions: CloneConfigurations = {
        allowTaint: opts.allowTaint ?? false,
        onclone: opts.onclone,
        ignoreElements: opts.ignoreElements,
        inlineImages: foreignObjectRendering,
        copyStyles: foreignObjectRendering
    };

    context.logger.debug(
        `Starting document clone with size ${windowBounds.width}x${
            windowBounds.height
        } scrolled to ${-windowBounds.left},${-windowBounds.top}`
    );

    const documentCloner = new DocumentCloner(context, element, cloneOptions);
    const clonedElement = documentCloner.clonedReferenceElement;
    if (!clonedElement) {
        return Promise.reject(`Unable to find element in cloned iframe`);
    }

    const container = await documentCloner.toIFrame(ownerDocument, windowBounds);

    const {width, height, left, top} =
        isBodyElement(clonedElement) || isHTMLElement(clonedElement)
            ? parseDocumentSize(clonedElement.ownerDocument)
            : parseBounds(context, clonedElement);

    const backgroundColor = parseBackgroundColor(context, clonedElement, opts.backgroundColor);

    const renderOptions: RenderConfigurations = {
        canvas: opts.canvas,
        backgroundColor,
        scale: opts.scale ?? defaultView.devicePixelRatio ?? 1,
        x: (opts.x ?? 0) + left,
        y: (opts.y ?? 0) + top,
        width: opts.width ?? Math.ceil(width),
        height: opts.height ?? Math.ceil(height)
    };

    let canvas;

    if (foreignObjectRendering) {
        context.logger.debug(`Document cloned, using foreign object rendering`);
        const renderer = new ForeignObjectRenderer(context, renderOptions);
        canvas = await renderer.render(clonedElement);
    } else {
        context.logger.debug(
            `Document cloned, element located at ${left},${top} with size ${width}x${height} using computed rendering`
        );

        context.logger.debug(`Starting DOM parsing`);
        const root = parseTree(context, clonedElement);

        if (backgroundColor === root.styles.backgroundColor) {
            root.styles.backgroundColor = COLORS.TRANSPARENT;
        }

        context.logger.debug(
            `Starting renderer for element at ${renderOptions.x},${renderOptions.y} with size ${renderOptions.width}x${renderOptions.height}`
        );

        const renderer = new CanvasRenderer(context, renderOptions);
        canvas = await renderer.render(root);
    }

    if (opts.removeContainer ?? true) {
        if (!DocumentCloner.destroy(container)) {
            context.logger.error(`Cannot detach cloned iframe as it is not in the DOM anymore`);
        }
    }

    context.logger.debug(`Finished rendering`);
    return canvas;
};

它主要做了以下事情:

  1. 解析用户传入的options,将其与默认的options合并,得到用于渲染的配置数据renderOptions
  2. 对传入的DOM元素进行解析,取到节点信息和样式信息,这些节点信息会和上一步的renderOptions配置一起传给canvasRenderer实例,用来绘制离屏canvas
  3. canvasRenderer将依据浏览器渲染层叠内容的规则,将用户传入的DOM元素渲染到一个离屏canvas中,这个离屏canvas我们可以在then方法的回调中取到

上面这一步 简化为个步骤

const renderElement = async (element: HTMLElement, opts: Partial<Options>): Promise<HTMLCanvasElement> => {
    const renderOptions = {...defaultOptions, ...opts}; // 合并默认配置和用户配置
    const root = parseTree(element); // 解析用户传入的DOM元素(为了不影响原始的DOM,实际上会克隆一个新的DOM元素),获取节点信息
    const renderer = new CanvasRenderer(renderOptions); // 根据渲染的配置数据生成canvasRenderer实例
    return await renderer.render(root); // canvasRenderer实例会根据解析到的节点信息,依据浏览器渲染层叠内容的规则,将DOM元素内容渲染到离屏canvas中
};

  1. 合并配置的逻辑比较简单,我们直接略过,重点分析下解析节点信息(parseTree)和渲染离屏canvas(renderer.render)两个逻辑。

    3.1 解析节点信息 parseTree

    parseTree的入参就是一个普通的DOM元素,返回值是一个ElementContainer对象,该对象主要包含DOM元素的位置信息(bounds: width|height|left|top)、样式数据、文本节点数据等(只是节点树的相关信息,不包含层叠数据,层叠数据在parseStackingContexts方法中取得)。解析的方法就是递归整个DOM树,并取得每一层节点的数据。

parseTree大致返回 在这里插入图片描述 在这里插入图片描述

里面包含了每一层节点的:

bounds - 位置信息(宽/高、横/纵坐标)
elements - 子元素信息
flags - 用来决定如何渲染的标志
styles - 样式描述信息
textNodes - 文本节点信息

本质上就是把dom元素变成一棵树结构 有父子关系, 然后有bounds信息,列出坐标位置 还有样式,有这些条件,后面就可以做渲染了

3.2 生成渲染器

new CanvasRenderer(renderOptions);

export class CanvasRenderer extends Renderer {
    canvas: HTMLCanvasElement;
    ctx: CanvasRenderingContext2D;
    private readonly _activeEffects: IElementEffect[] = [];
    private readonly fontMetrics: FontMetrics;

    constructor(context: Context, options: RenderConfigurations) {
        super(context, options);
        this.canvas = options.canvas ? options.canvas : document.createElement('canvas');
        this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D;
        if (!options.canvas) {
            this.canvas.width = Math.floor(options.width * options.scale);
            this.canvas.height = Math.floor(options.height * options.scale);
            this.canvas.style.width = `${options.width}px`;
            this.canvas.style.height = `${options.height}px`;
        }
        this.fontMetrics = new FontMetrics(document);
        this.ctx.scale(this.options.scale, this.options.scale);
        this.ctx.translate(-options.x, -options.y);
        this.ctx.textBaseline = 'bottom';
        this._activeEffects = [];
        this.context.logger.debug(
            `Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`
        );
    }
    ....

这一步就是根据配置信息生成一个render渲染器 , 渲染器内部生成一个canvas对象,也是后面peomise返回的cavas对象 , 方便后续调用canvas方法

3.3 渲染离屏canvas , 调用renderer.render

有了节点树信息,就可以用来渲染离屏canvas了,我们来看看渲染的逻辑。

渲染的逻辑在CanvasRenderer类的render方法中,该方法主要用来渲染层叠内容:

render方法的核心代码如下:

  /**
   * StackingContext {
   *   element: ElementPaint {container: ElementContainer, effects: Array(0), curves: BoundCurves}
   *   inlineLevel: []
   *   negativeZIndex: []
   *   nonInlineLevel: [ElementPaint]
   *   nonPositionedFloats: []
   *   nonPositionedInlineLevel: []
   *   positiveZIndex: [StackingContext]
   *   zeroOrAutoZIndexOrTransformedOrOpacity: [StackingContext]
   * }
   */
  
    async render(element: ElementContainer): Promise<HTMLCanvasElement> {
        if (this.options.backgroundColor) {
            this.ctx.fillStyle = asString(this.options.backgroundColor);
            this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
        }

        const stack = parseStackingContexts(element);

        await this.renderStack(stack);
        this.applyEffects([]);
        return this.canvas;
    }
  1. 这个render函数 传入的element 就是3.1拿到的root元素
  2. 然后根据配置信息,填充canvas的背景
  3. 然后根据element 生成stack

parseStackingContexts(element) 实现如下:

export const parseStackingContexts = (container: ElementContainer): StackingContext => {
    const paintContainer = new ElementPaint(container, null);
    const root = new StackingContext(paintContainer);
    const listItems: ElementPaint[] = [];
    parseStackTree(paintContainer, root, root, listItems);
    processListItems(paintContainer.container, listItems);
    return root;
};

返回的stack 就是 下面的对象 本质就是把上么的elementContainer根据dom实际情况分门别类 放进不同数组 在这里插入图片描述

其中的 inlineLevel - 内联元素 negativeZIndex - zIndex为负的元素 nonInlineLevel - 非内联元素 nonPositionedFloats - 未定位的浮动元素 nonPositionedInlineLevel - 内联的非定位元素,包含内联表和内联块 positiveZIndex - z-index大于等于1的元素 zeroOrAutoZIndexOrTransformedOrOpacity - 所有有层叠上下文的(z-index: auto|0)、透明度小于1的(opacity小于1)或变换的(transform不为none)元素 代表的是层叠信息,渲染层叠内容时会根据这些层叠信息来决定渲染的顺序,一层一层有序进行渲染。

parseStackingContexts解析层叠信息的方式和parseTree解析节点信息的方式类似,都是递归整棵树,收集树的每一层的信息,形成一颗包含层叠信息的层叠树。

而渲染层叠内容的renderStack方式实际上调用的是renderStackContent方法,该方法是整个渲染流程中最为关键的方法.

parseStackTree 就是根据整个elements数遍历 递归处理, 把符合的类型分门别类放在各个数组, 比如根据inline 根据opacity 、position等样式决定。

const parseStackTree = (
    parent: ElementPaint,
    stackingContext: StackingContext,
    realStackingContext: StackingContext,
    listItems: ElementPaint[]
) => {
    parent.container.elements.forEach((child) => {
        const treatAsRealStackingContext = contains(child.flags, FLAGS.CREATES_REAL_STACKING_CONTEXT);
        const createsStackingContext = contains(child.flags, FLAGS.CREATES_STACKING_CONTEXT);
        const paintContainer = new ElementPaint(child, parent);
        if (contains(child.styles.display, DISPLAY.LIST_ITEM)) {
            listItems.push(paintContainer);
        }

        const listOwnerItems = contains(child.flags, FLAGS.IS_LIST_OWNER) ? [] : listItems;

        if (treatAsRealStackingContext || createsStackingContext) {
            const parentStack =
                treatAsRealStackingContext || child.styles.isPositioned() ? realStackingContext : stackingContext;

            const stack = new StackingContext(paintContainer);

            if (child.styles.isPositioned() || child.styles.opacity < 1 || child.styles.isTransformed()) {
                const order = child.styles.zIndex.order;
                if (order < 0) {
                    let index = 0;

                    parentStack.negativeZIndex.some((current, i) => {
                        if (order > current.element.container.styles.zIndex.order) {
                            index = i;
                            return false;
                        } else if (index > 0) {
                            return true;
                        }

                        return false;
                    });
                    parentStack.negativeZIndex.splice(index, 0, stack);
                } else if (order > 0) {
                    let index = 0;
                    parentStack.positiveZIndex.some((current, i) => {
                        if (order >= current.element.container.styles.zIndex.order) {
                            index = i + 1;
                            return false;
                        } else if (index > 0) {
                            return true;
                        }

                        return false;
                    });
                    parentStack.positiveZIndex.splice(index, 0, stack);
                } else {
                    parentStack.zeroOrAutoZIndexOrTransformedOrOpacity.push(stack);
                }
            } else {
                if (child.styles.isFloating()) {
                    parentStack.nonPositionedFloats.push(stack);
                } else {
                    parentStack.nonPositionedInlineLevel.push(stack);
                }
            }

            parseStackTree(
                paintContainer,
                stack,
                treatAsRealStackingContext ? stack : realStackingContext,
                listOwnerItems
            );
        } else {
            if (child.styles.isInlineLevel()) {
                stackingContext.inlineLevel.push(paintContainer);
            } else {
                stackingContext.nonInlineLevel.push(paintContainer);
            }

            parseStackTree(paintContainer, stackingContext, realStackingContext, listOwnerItems);
        }

        if (contains(child.flags, FLAGS.IS_LIST_OWNER)) {
            processListItems(child, listOwnerItems);
        }
    });
};

parent.container.elements就是一开始3.1获取dom树 在这里插入图片描述 stackingContext 就是整个对象 然后里面包含各种类别的情况, 把element单独塞进去 最后再分别渲染出来。 在这里插入图片描述 在这里插入图片描述

  1. 渲染stack
    调用this.renderStack(stack);
 async renderStack(stack: StackingContext): Promise<void> {
        const styles = stack.element.container.styles;
        if (styles.isVisible()) {
            await this.renderStackContent(stack);
        }
    }
    
async renderStackContent(stack: StackingContext): Promise<void> {
        if (contains(stack.element.container.flags, FLAGS.DEBUG_RENDER)) {
            debugger;
        }
        // https://www.w3.org/TR/css-position-3/#painting-order
        // 1. the background and borders of the element forming the stacking context.
        await this.renderNodeBackgroundAndBorders(stack.element);
        // 2. the child stacking contexts with negative stack levels (most negative first).
        for (const child of stack.negativeZIndex) {
            await this.renderStack(child);
        }
        // 3. For all its in-flow, non-positioned, block-level descendants in tree order:
        await this.renderNodeContent(stack.element);

        for (const child of stack.nonInlineLevel) {
            await this.renderNode(child);
        }
        // 4. All non-positioned floating descendants, in tree order. For each one of these,
        // treat the element as if it created a new stacking context, but any positioned descendants and descendants
        // which actually create a new stacking context should be considered part of the parent stacking context,
        // not this new one.
        for (const child of stack.nonPositionedFloats) {
            await this.renderStack(child);
        }
        // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
        for (const child of stack.nonPositionedInlineLevel) {
            await this.renderStack(child);
        }
        for (const child of stack.inlineLevel) {
            await this.renderNode(child);
        }
        // 6. All positioned, opacity or transform descendants, in tree order that fall into the following categories:
        //  All positioned descendants with 'z-index: auto' or 'z-index: 0', in tree order.
        //  For those with 'z-index: auto', treat the element as if it created a new stacking context,
        //  but any positioned descendants and descendants which actually create a new stacking context should be
        //  considered part of the parent stacking context, not this new one. For those with 'z-index: 0',
        //  treat the stacking context generated atomically.
        //
        //  All opacity descendants with opacity less than 1
        //
        //  All transform descendants with transform other than none
        for (const child of stack.zeroOrAutoZIndexOrTransformedOrOpacity) {
            await this.renderStack(child);
        }
        // 7. Stacking contexts formed by positioned descendants with z-indices greater than or equal to 1 in z-index
        // order (smallest first) then tree order.
        for (const child of stack.positiveZIndex) {
            await this.renderStack(child);
        }
    }

↑ renderstackContent 就是按类别渲染 在这里插入图片描述 按照上面这种顺序渲染

核心渲染方法:

async renderNodeContent(paint: ElementPaint): Promise<void> {
        this.applyEffects(paint.getEffects(EffectTarget.CONTENT));
        const container = paint.container;
        const curves = paint.curves;
        const styles = container.styles;
        for (const child of container.textNodes) {
            await this.renderTextNode(child, styles);
        }

        if (container instanceof ImageElementContainer) {
            try {
                const image = await this.context.cache.match(container.src);
                this.renderReplacedElement(container, curves, image);
            } catch (e) {
                this.context.logger.error(`Error loading image ${container.src}`);
            }
        }

        if (container instanceof CanvasElementContainer) {
            this.renderReplacedElement(container, curves, container.canvas);
        }

        if (container instanceof SVGElementContainer) {
            try {
                const image = await this.context.cache.match(container.svg);
                this.renderReplacedElement(container, curves, image);
            } catch (e) {
                this.context.logger.error(`Error loading svg ${container.svg.substring(0, 255)}`);
            }
        }

        if (container instanceof IFrameElementContainer && container.tree) {
            const iframeRenderer = new CanvasRenderer(this.context, {
                scale: this.options.scale,
                backgroundColor: container.backgroundColor,
                x: 0,
                y: 0,
                width: container.width,
                height: container.height
            });

            const canvas = await iframeRenderer.render(container.tree);
            if (container.width && container.height) {
                this.ctx.drawImage(
                    canvas,
                    0,
                    0,
                    container.width,
                    container.height,
                    container.bounds.left,
                    container.bounds.top,
                    container.bounds.width,
                    container.bounds.height
                );
            }
        }

        if (container instanceof InputElementContainer) {
            const size = Math.min(container.bounds.width, container.bounds.height);

            if (container.type === CHECKBOX) {
                if (container.checked) {
                    this.ctx.save();
                    this.path([
                        new Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79),
                        new Vector(container.bounds.left + size * 0.16, container.bounds.top + size * 0.5549),
                        new Vector(container.bounds.left + size * 0.27347, container.bounds.top + size * 0.44071),
                        new Vector(container.bounds.left + size * 0.39694, container.bounds.top + size * 0.5649),
                        new Vector(container.bounds.left + size * 0.72983, container.bounds.top + size * 0.23),
                        new Vector(container.bounds.left + size * 0.84, container.bounds.top + size * 0.34085),
                        new Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79)
                    ]);

                    this.ctx.fillStyle = asString(INPUT_COLOR);
                    this.ctx.fill();
                    this.ctx.restore();
                }
            } else if (container.type === RADIO) {
                if (container.checked) {
                    this.ctx.save();
                    this.ctx.beginPath();
                    this.ctx.arc(
                        container.bounds.left + size / 2,
                        container.bounds.top + size / 2,
                        size / 4,
                        0,
                        Math.PI * 2,
                        true
                    );
                    this.ctx.fillStyle = asString(INPUT_COLOR);
                    this.ctx.fill();
                    this.ctx.restore();
                }
            }
        }

        if (isTextInputElement(container) && container.value.length) {
            const [fontFamily, fontSize] = this.createFontStyle(styles);
            const {baseline} = this.fontMetrics.getMetrics(fontFamily, fontSize);

            this.ctx.font = fontFamily;
            this.ctx.fillStyle = asString(styles.color);

            this.ctx.textBaseline = 'alphabetic';
            this.ctx.textAlign = canvasTextAlign(container.styles.textAlign);

            const bounds = contentBox(container);

            let x = 0;

            switch (container.styles.textAlign) {
                case TEXT_ALIGN.CENTER:
                    x += bounds.width / 2;
                    break;
                case TEXT_ALIGN.RIGHT:
                    x += bounds.width;
                    break;
            }

            const textBounds = bounds.add(x, 0, 0, -bounds.height / 2 + 1);

            this.ctx.save();
            this.path([
                new Vector(bounds.left, bounds.top),
                new Vector(bounds.left + bounds.width, bounds.top),
                new Vector(bounds.left + bounds.width, bounds.top + bounds.height),
                new Vector(bounds.left, bounds.top + bounds.height)
            ]);

            this.ctx.clip();
            this.renderTextWithLetterSpacing(
                new TextBounds(container.value, textBounds),
                styles.letterSpacing,
                baseline
            );
            this.ctx.restore();
            this.ctx.textBaseline = 'alphabetic';
            this.ctx.textAlign = 'left';
        }

        if (contains(container.styles.display, DISPLAY.LIST_ITEM)) {
            if (container.styles.listStyleImage !== null) {
                const img = container.styles.listStyleImage;
                if (img.type === CSSImageType.URL) {
                    let image;
                    const url = (img as CSSURLImage).url;
                    try {
                        image = await this.context.cache.match(url);
                        this.ctx.drawImage(image, container.bounds.left - (image.width + 10), container.bounds.top);
                    } catch (e) {
                        this.context.logger.error(`Error loading list-style-image ${url}`);
                    }
                }
            } else if (paint.listValue && container.styles.listStyleType !== LIST_STYLE_TYPE.NONE) {
                const [fontFamily] = this.createFontStyle(styles);

                this.ctx.font = fontFamily;
                this.ctx.fillStyle = asString(styles.color);

                this.ctx.textBaseline = 'middle';
                this.ctx.textAlign = 'right';
                const bounds = new Bounds(
                    container.bounds.left,
                    container.bounds.top + getAbsoluteValue(container.styles.paddingTop, container.bounds.width),
                    container.bounds.width,
                    computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 1
                );

                this.renderTextWithLetterSpacing(
                    new TextBounds(paint.listValue, bounds),
                    styles.letterSpacing,
                    computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 2
                );
                this.ctx.textBaseline = 'bottom';
                this.ctx.textAlign = 'left';
            }
        }
    }

本质上 渲染的时候就会根据实际样式情况 图片就 this.ctx.drawImage 文案就 this.ctx.fillText 等等操作 就能渲染好canvas了。

最后返回这个渲染好的canvas就可以了。

4、总结

这个图是借鉴别人的 很清晰列出从入口到最终渲染的方法: 在这里插入图片描述

核心关键 1、是先解析dom元素,抽取关键信息,如样式,位置信息等,形成父子关系的树 2、遍历第一步的树,形成一个stack 里面分别装着7种情况的dom树 3、生成一个render渲染器, 根据stack 7种情况,用canvas去渲染这7种情况 4、返回这个离屏canvas即可。