针对creator2.4.x 微信小游戏使用开放域后 主域无法截图问题【解决方案】

微信小游戏在启用开放域后,由于安全问题,微信会对主域做出限制,导致无法使用toDataURL、 getlmageData、 readPixels等api
以下是官方提示

而cocos官方示例使用的截图能力实现 刚好使用的是readPixels API
导致readPixels 得到的内容是一个空数据,而截出来的内容也是个完美的黑色

以下是Cocos官方案例中的截图方案

以下提供解决方案和解决思路:

直接上代码,
每一步基本都写了注释,通读一遍很容易就能理解原理
使用方法:将要截图的node节点,传给drawNodeToTempFilePath方法进行调用,即可在回调中获取截图后的临时文件地址,然后就可以用这个地址来做分享或保存到相册了

export default class CanvasDrawManager {
    //预加载图片组件对象池
    private shareImagePool: { src: string, img: HTMLImageElement }[] = [];

    drawNodeToTempFilePath(node: cc.Node, drawOverFn: (path: string) => any) {
        let pnlSnapShotSize = node.getContentSize();
        let width = pnlSnapShotSize.width, height = pnlSnapShotSize.height;
        let shareCanvas = this.getShareCanvas(width, height);
        let ctx = shareCanvas['getContext']('2d');
        //遍历节点绘制到画布
        this.drawToCanvas(node, ctx, () => {
            let path: string = '';
            if (window['wx'] || window['qq']) {
                try {
                    path = shareCanvas['toTempFilePathSync']({
                        "object": { "x": 0, "y": 0, "width": width, "height": height, "destWidth": width, "destHeight": height },
                    });
                } catch (e) {
                    Log.Error('offScreenCanvas to temp file path error!' + e.message);
                }
            } else {
                // web test
                let imgC = new Image();
                imgC.crossOrigin = "anonymous";
                imgC.src = shareCanvas.toDataURL();

                imgC.style.position = 'absolute';
                imgC.style.left = '0';
                imgC.style.top = '0';
                document.body.appendChild(imgC);
            }

            drawOverFn && drawOverFn(path);

            //画完清掉对象,让js自己gc
            this.shareImagePool = [];
            shareCanvas = null;
        });
    }

    private getShareCanvas(width: number, height: number): HTMLCanvasElement {
        let shareCanvas: HTMLCanvasElement = null;
        if (window['wx'] || window['qq']) {
            shareCanvas = window['wx']['createCanvas']();
        } else {
            shareCanvas = document.createElement('canvas');
        }
        shareCanvas['width'] = width;
        shareCanvas['height'] = height;
        return shareCanvas;
    }

    private drawToCanvas(node: cc.Node, ctx: CanvasRenderingContext2D, cb: Function) {
        //获得观测节点,用于坐标系转换
        let observeNode: cc.Node = node.getChildByName("CanvasObserveNode");
        if (!observeNode) {
            observeNode = new cc.Node("CanvasObserveNode");
            node.addChild(observeNode);
            //将观测节点置于截图节点左上角
            let baseNodeContent = node.getContentSize();
            let baseNodeAnchor = node.getAnchorPoint();
            observeNode.setPosition(-baseNodeContent.width * baseNodeAnchor.x, baseNodeContent.height * (1 - baseNodeAnchor.y));
        }

        let allRenderNode: cc.Node[] = this.getAllRenderNode(node);

        this.reLoadImgByNodeList(allRenderNode, () => {
            allRenderNode.forEach(n => {
                this.drawNodeToCanvasCxt(n, observeNode, ctx);
            });
            cb && cb();
        })
    }

    //预加载图片
    private reLoadImgByNodeList(allRenderNode: cc.Node[], overCb: Function) {
        //清空对象池
        this.shareImagePool = [];

        let loadImgSrcList: string[] = [];
        let loadOverNum: number = 0;
        let cbRunOverFlag: boolean = false;

        let checkLoadOver = () => {
            if (loadOverNum >= loadImgSrcList.length && !cbRunOverFlag) {
                //全部图片加载完成,回调出去,一次预加载只能回调一次
                cbRunOverFlag = true;
                overCb && overCb();
            }
        }

        //所有图片都需要提前加载,推入开始加载
        allRenderNode.forEach(n => {
            let sp = n.getComponent(cc.Sprite);
            if (sp && sp.spriteFrame && sp.spriteFrame.getTexture().url) {
                loadImgSrcList.push(sp.spriteFrame.getTexture().url);
            }
            //bmFont图片
            let lb = n.getComponent(cc.Label)
            if (lb && lb.font && lb.font['spriteFrame'] && lb.font['spriteFrame'].getTexture().url) {
                loadImgSrcList.push(lb.font['spriteFrame'].getTexture().url);
            }
        });

        //图片链接地址去重,加载过的就不需要再加载一遍
        loadImgSrcList = loadImgSrcList.filter((src, index) => loadImgSrcList.findIndex(fSrc => fSrc === src) === index);
        //没有图片要加载就直接cb
        if (loadImgSrcList.length == 0) {
            overCb && overCb();
            return;
        }

        loadImgSrcList.forEach(src => {
            let tmpImg: HTMLImageElement = null;
            if (window['wx'] || window['qq']) {
                tmpImg = window['wx']['createImage']();
            } else {
                tmpImg = document.createElement('img');
            }

            //将.pkm .pvr 改为.png来加载
            let loadTrueUrl: string = src;
            let loadTrueUrlPointList: string[] = loadTrueUrl.split('.');
            let fileType: string = loadTrueUrlPointList[loadTrueUrlPointList.length - 1];
            if (['pkm', 'pvr'].includes(fileType)) {
                loadTrueUrl = loadTrueUrl.slice(0, -3) + 'png';
            }

            tmpImg.src = loadTrueUrl;
            tmpImg.crossOrigin = "anonymous";
            //不论图片加载成功与否,都只加载一次,并推入回调栈
            tmpImg.onload = () => {
                loadOverNum++;
                checkLoadOver();
            };
            tmpImg.onerror = () => {
                loadOverNum++;
                checkLoadOver();
            }

            this.shareImagePool.push({
                src: src,
                img: tmpImg
            })
        })
    }

    private drawNodeToCanvasCxt(node: cc.Node, observeNode: cc.Node, ctx: CanvasRenderingContext2D) {
        //保存当前画布状态
        ctx.save();

        if (node.getComponent(cc.Label)) {
            //在当前帧完成label组件的string填充宽高,以便绘画到正确位置
            node.getComponent(cc.Label)['_forceUpdateRenderData'](true);
        }

        //获得节点相对于左上角观测点的位置
        let worldPos = node.convertToWorldSpaceAR(cc.v2(0, 0)) //得到node的世界坐标。
        let nodePos = observeNode.convertToNodeSpaceAR(worldPos) //得到node在观测节点的相对坐标。
        nodePos.y = -nodePos.y//y轴取反即为canvas的正数
        //先将绘制点放在节点的锚点上做缩放和旋转动作
        ctx.translate(nodePos.x, nodePos.y);

        //获得节点的真实旋转值,旋转画布
        let canvasRotation: number = this.getWorldRotation(node);
        ctx.rotate(canvasRotation * Math.PI / 180);

        //获得节点的真实缩放值,缩放画布
        let nodeWorldScale: cc.Vec2 = this.getWorldScale(node);
        ctx.scale(nodeWorldScale.x, nodeWorldScale.y);

        //缩放和旋转完成后将绘画起点回到canvas 0,0点
        ctx.translate(-nodePos.x, -nodePos.y);

        //将真实绘画点置于节点左上角
        let drawPosX = nodePos.x - (node.width * node.getAnchorPoint().x);
        let drawPosY = nodePos.y - (node.height * (1 - node.getAnchorPoint().y));
        ctx.translate(drawPosX, drawPosY);

        //开始绘画
        if (node.getComponent(cc.Sprite)) {
            this.drawImgToCanvas(node.getComponent(cc.Sprite), ctx);
        } else if (node.getComponent(cc.Label)) {
            if (node.getComponent(cc.Label).font) {
                //bmFont支持
                this.drawBMLabelToCanvas(node.getComponent(cc.Label), ctx);
            } else {
                //普通文本
                this.drawLabelToCanvas(node.getComponent(cc.Label), ctx);
            }
        }

        //绘完毕后前将画布状态恢复到初始状态
        ctx.restore();
    }

    private drawImgToCanvas(sprite: cc.Sprite, ctx: CanvasRenderingContext2D) {
        let spriteFrame: cc.SpriteFrame = sprite.spriteFrame;
        let poolIndex: number = this.shareImagePool.findIndex(p => p.src == spriteFrame.getTexture().url);
        //目前必须提前加载,未加载过的无法绘制
        if (poolIndex == -1) return;

        let imgTmp: HTMLImageElement = this.shareImagePool[poolIndex].img;

        let spRect: cc.Rect = spriteFrame.getRect();
        let drawWidth: number = spRect.width;
        let drawHeight: number = spRect.height;
        drawWidth = sprite.node.getContentSize().width;
        drawHeight = sprite.node.getContentSize().height;

        //就绪,开始绘制 支持九宫格
        this.ctxDrawImg9(imgTmp, spRect, drawWidth, drawHeight, spriteFrame, ctx);
    }

    private ctxDrawImg9(img: HTMLImageElement, spRect: cc.Rect, drawWidth: number, drawHeight: number, spriteFrame: cc.SpriteFrame, ctx: CanvasRenderingContext2D) {
        //判断是否为图集内的旋转图片,进行处理(逆时针转90°)
        if (spriteFrame.isRotated()) {
            //点位移动
            ctx.translate(0, drawHeight);
            //绘制画布旋转
            ctx.rotate(-90 * Math.PI / 180);
            //将图集截取矩形的宽高颠倒
            let tmpNum: number = spRect.width;
            spRect.width = spRect.height;
            spRect.height = tmpNum;
            //绘制宽高颠倒
            tmpNum = drawWidth;
            drawWidth = drawHeight;
            drawHeight = tmpNum;
        }
        
        //九宫格点位数据
        let l = spriteFrame.insetLeft;
        let t = spriteFrame.insetTop;
        let r = spriteFrame.insetRight;
        let b = spriteFrame.insetBottom;
        //图片位于图集左上角的起点坐标
        let x = spRect.x;
        let y = spRect.y;
        //需要绘制的大小
        let dw = drawWidth;
        let dh = drawHeight;
        //图片的真实大小
        let sw = spRect.width;
        let sh = spRect.height;

        //以下绘制大小+1的原因是九宫格绘制后图像之间有分割,额外拉伸1像素填充分割
        //左上角
        if (l > 0 && t > 0) ctx.drawImage(img, x, y, l, t, 0, 0, l, t);
        //顶部中间拉伸部分 宽额外拉伸1
        if (t > 0) ctx.drawImage(img, x + l, y, sw - l - r, t, l, 0, dw - l - r + 1, t);
        //右上角
        if (t > 0 && r > 0) ctx.drawImage(img, x + sw - r, y, r, t, dw - r, 0, r, t);
        //左侧中间拉伸部分 高额外拉伸1
        if (l > 0) ctx.drawImage(img, x, y + t, l, sh - t - b, 0, t, l + 1, dh - t - b + 1);

        //中间拉伸部分,非九宫格类型图片无需拉伸
        let stretchNum: number = 0;
        //九宫格类型图片宽高都拉伸1
        if (l > 0 || r > 0 || t > 0 || b > 0) stretchNum = 1;
        ctx.drawImage(img, x + l, y + t, sw - l - r, sh - t - b, l, t - stretchNum, dw - l - r + stretchNum, dh - t - b + stretchNum);

        //右侧中间拉伸部分 高额外拉伸1
        if (r > 0) ctx.drawImage(img, x + sw - r, y + t, r, sh - t - b, dw - r, t, r, dh - t - b + 1);
        //左下角
        if (l > 0 && b > 0) ctx.drawImage(img, x, y + sh - b, l, b, 0, dh - b, l, b);
        //底部中间拉伸部分 宽额外拉伸1
        if (b > 0) ctx.drawImage(img, x + l, y + sh - b, sw - l - r, b, l, dh - b, dw - l - r + 1, b);
        //右下角
        if (r > 0 && b > 0) ctx.drawImage(img, x + sw - r, y + sh - b, r, b, dw - r, dh - b, r, b);
        
        //恢复旋转图片画布状态
        if (spriteFrame.isRotated()) {
            //绘制画布旋转
            ctx.rotate(90 * Math.PI / 180);
            //点位移动
            ctx.translate(0, -drawHeight);
        }
    }

    private drawLabelToCanvas(label: cc.Label, ctx: CanvasRenderingContext2D) {
        ctx.textAlign = 'left';
        ctx.textBaseline = 'middle';
        let labelColor: cc.Color = label.node.color;
        ctx.fillStyle = `rgba(${labelColor.r},${labelColor.g},${labelColor.b},1)`;
        //斜体支持
        let fontStyle: string = label.enableItalic ? 'italic' : 'normal';
        ctx.font = `${fontStyle} ${label.fontSize}px ${label.fontFamily}`;
        ctx.fillText(label.string, 0, label.node.height / 2);
    }

    private drawBMLabelToCanvas(label: cc.Label, ctx: CanvasRenderingContext2D) {
        let font: cc.Font = label.font;
        let fontSpriteFrame: cc.SpriteFrame = font['spriteFrame'];
        let poolIndex: number = this.shareImagePool.findIndex(p => p.src == fontSpriteFrame.getTexture().url);
        //目前必须提前加载,未加载过的无法绘制
        if (poolIndex == -1) return;

        let imgTmp: HTMLImageElement = this.shareImagePool[poolIndex].img;
        //设计字体字号
        let orgSize = font['_fntConfig'].fontSize;
        //真实字体字号缩放值
        let scaleSize = label.fontSize / orgSize;
        //获取字体集数据源
        let fontDefDictionary = font['_fntConfig'].fontDefDictionary;

        //移动绘制点到节点左侧中部
        ctx.translate(0, label.node.height / 2);
        //缩放画布到文字大小倍数后绘制
        ctx.scale(scaleSize, scaleSize);

        for (let index = 0; index < label.string.length; index++) {
            let str = label.string[index];
            let strAdvance: number = str.charCodeAt(0);
            let dictionary: { rect: cc.Rect, xAdvance: number, xOffset: number, yOffset: number } = fontDefDictionary[strAdvance.toString()];
            if (!dictionary) continue;
            //移动画笔到偏移点
            ctx.translate(dictionary.xOffset, dictionary.yOffset - orgSize / 2);
            //计算字体图集在外层图集内的真实偏移值
            let trueRect = new cc.Rect();
            if (fontSpriteFrame.isRotated()) {
                trueRect.x = fontSpriteFrame.getRect().x + fontSpriteFrame.getRect().width - dictionary.rect.y - dictionary.rect.height;
                trueRect.y = fontSpriteFrame.getRect().y + dictionary.rect.x;
            } else {
                trueRect.x = fontSpriteFrame.getRect().x + dictionary.rect.x;
                trueRect.y = fontSpriteFrame.getRect().y + dictionary.rect.y;
            }
            trueRect.width = dictionary.rect.width;
            trueRect.height = dictionary.rect.height;
            //开始绘制
            this.ctxDrawImg9(imgTmp, trueRect, dictionary.rect.width, dictionary.rect.height, fontSpriteFrame, ctx);
            //恢复画笔偏移点
            ctx.translate(-dictionary.xOffset, -(dictionary.yOffset - orgSize / 2));
            //移动绘制点到下一个文字位置,只支持横轴位移
            let advanceNum: number = (dictionary.xAdvance - dictionary.rect.width) / 2;
            if (advanceNum < 0) advanceNum = 0;
            ctx.translate(dictionary.rect.width + advanceNum + label.spacingX, 0);
        }
    }

    /**
     * 获得node在世界坐标系下的旋转值
     * @param {cc.Node} node 
     * @return {float}
     */
    private getWorldRotation(node): number {
        let rot = node.rotation;
        let parent = node.parent;
        //循环累加旋转值
        while (parent){
            rot += parent.rotation;
            parent = parent.parent;
        }
        return rot;
    }

    /**
     * 获得node在世界坐标下的缩放值
     * @param {cc.Node} node 
     * @returns {cc.Vec2}
     */
    private getWorldScale(node): cc.Vec2 {
        let scaleX = node.scaleX;
        let scaleY = node.scaleY;
        let parent = node.parent;
        //循环累乘缩放值
        while (parent){
            scaleX *= parent.scaleX;
            scaleY *= parent.scaleY;
            parent = parent.parent;
        }
        return new cc.Vec2(scaleX, scaleY);
    }

    //深度优先递归遍历所有需要绘制的子节点
    private getAllRenderNode(node: cc.Node, nodeList: cc.Node[] = []) {
        if (node !== null) {
            //暂只支持绘制图片和文本,过滤出需要绘制的节点,渲染节点内容空则不渲染
            if (node.active && node.opacity > 0) {
                if ((node.getComponent(cc.Sprite) && node.getComponent(cc.Sprite).spriteFrame)
                    || (node.getComponent(cc.Label) && node.getComponent(cc.Label).string.length)
                ) {
                    nodeList.push(node);
                }
            }
            //cocos特性,父级节点被隐藏了  所有子节点也会被隐藏
            if (node.active && node.opacity > 0 && node.children && node.children.length > 0) {
                let children = node.children
                for (let i = 0; i < children.length; i++) {
                    this.getAllRenderNode(children[i], nodeList);
                }
            }
        }
        return nodeList;
    }
}

以下是解决思路:

1赞

从以上代码,很容易看出,实现方案是轮询node节点及所有子节点中的label和sprite组件,通过canvas 2d的原生api绘制到canvas的离屏画布上。
由于是使用的手动绘制,完美规避了微信的限制。
且支持图集、九宫格图片、bmFont艺术字体集等,基本涵盖截图分享会用到的简单情况了。
且还有一个优点,截图时无需将开放域关闭,两个操作可同时存在。

目前的已知的问题(用不到也懒得做了,大家规避一下就好):
1、不支持mask裁切
2、不支持字体纵向布局,文字无法自动换行
3、文字垂直对齐模式只支持center
4、不支持富文本
5、不支持半透明绘制

若有其他问题或解决思路大佬们在使用过程可以在该帖留言
2.4.3引擎实测通过

大佬,我转到3.7.1不行,还是全黑的