微信小游戏在启用开放域后,由于安全问题,微信会对主域做出限制,导致无法使用toDataURL、 getlmageData、 readPixels等api
以下是官方提示
而cocos官方示例使用的截图能力实现 刚好使用的是readPixels API
导致readPixels 得到的内容是一个空数据,而截出来的内容也是个完美的黑色
以下是Cocos官方案例中的截图方案
以下提供解决方案和解决思路:
微信小游戏在启用开放域后,由于安全问题,微信会对主域做出限制,导致无法使用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;
}
}
以下是解决思路:
从以上代码,很容易看出,实现方案是轮询node节点及所有子节点中的label和sprite组件,通过canvas 2d的原生api绘制到canvas的离屏画布上。
由于是使用的手动绘制,完美规避了微信的限制。
且支持图集、九宫格图片、bmFont艺术字体集等,基本涵盖截图分享会用到的简单情况了。
且还有一个优点,截图时无需将开放域关闭,两个操作可同时存在。
目前的已知的问题(用不到也懒得做了,大家规避一下就好):
1、不支持mask裁切
2、不支持字体纵向布局,文字无法自动换行
3、文字垂直对齐模式只支持center
4、不支持富文本
5、不支持半透明绘制
若有其他问题或解决思路大佬们在使用过程可以在该帖留言
2.4.3引擎实测通过
大佬,我转到3.7.1不行,还是全黑的