CANVAS绘图踩坑记录

最近作了移动端生成图片而且上传的需求,踩了很多坑,这里记录一下。因为本次使用canvas主要功能集中在绘制网络图片以及生成/上传图片,所以本文多为和图片相关的记录。css

绘图部分

onload

最开始使用canvas直接加载网络图片的时候,忘记考虑图片加载的问题了,所以直接上手写image.src = xxx接着就是ctx.drawImage,最后发现网络图片根本没有被绘制上去,这才想起来图片必需要先加载完以后才能使用ctx.drawImage去绘制,不然图片没有加载完,canvas直接绘制一张空的图片。webpack

export class Canvas {
    // code here...
    // 加载图片
    addImage(src: string) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.onload = () => {
                resolve(img);
            }
            img.src = src;
        });
    }
    // code here...
}
复制代码

图片跨域问题

修改完毕以后,原本觉得此次代码能跑起来了,却发现因为访问了CDN图片地址,Image默认不支持访问跨域资源,必需要手动指定crossOrigin属性才能够跨域访问图片。git

export class Canvas {
    // code here...
    // 加载图片
    addImage(src: string) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.crossOrigin = 'anonymous';
            img.onload = () => {
                resolve(img);
            }
            img.src = src;
        });
    }
    // code here...
}
复制代码

渲染的层级关系

图片加载出来以后,原本感受问题已经解决了,可是发现我原本应该渲染的三张图片,最后只出来了一张背景图,另外两个图都不见了,可是在代码里面打印日志是能够看到canvas确实执行了这两张图片的渲染逻辑。github

查了查资料发现canvas只能按照渲染的前后顺序来展现绘图的层级关系,没法手动指定层级,所以咱们想要在背景图上方绘制图片的话,必需要等到背景图绘制完毕以后才能继续执行其余的逻辑。web

export class Canvas {
    canvas: HTMLCanvasElement;
    ctx: CanvasRenderingContext2D;
    constructor() {
        // other code
        this.canvas = document.createElement('canvas');
        this.ctx = this.canvas.getContext('2d')!;
        // other code
    }
    // code here...
    addImage(src: string) {
        // ...
    }
    drawImage() {
        const bg = 'https://xxx';
        this.addImage(bg).then((img: ImageBitmap) => {
            this.ctx.drawImage(img, 0, 0, img.width, img.height);
            // 在这以后才能继续绘制其余图片
            this.addImage(xxx);
        });
    }
}
复制代码

绘制圆形图片

全部绘制都结束以后,产品忽然过来跟我说,但愿在最后面加上一行用户信息的区域,包括用户头像和用户名,原本觉得是很简单的工做,按照上面的逻辑再绘制一张图片和一段文字便可,后来发现用户头像的图片都是方形的,可是产品但愿要一张圆形头像图片,在css中只须要很简单一行border-radius: 50%的东西让我很头疼。typescript

尝试了各类办法都失败以后,上网查了查资料才发现原来context还有保存和还原方法,再配合clip裁切就能够完成一个圆形图片了!canvas

export class Canvas {
    constructor() {
        // ...
    }
    addImage() {
        // ...
    }
    drawImage() {
        // ...
    }
    drawUserInfo() {
        // other code
        const src = 'https://xxx';
         this.addImage(src).then((img: ImageBitmap) => {
            this.ctx.save();
            this.ctx.arc(x, y, r, 0, Math.PI * 2);
            this.ctx.clip();
            this.ctx.drawImage(img, x, y, img.width, img.height);
            this.ctx.restore();
        });
    }
}
复制代码

这样先保存当前画布的状态,而后经过arc在头像图片的位置绘制一个圆形,而后裁切掉多余的部分,接着绘制头像,最后再恢复画布状态便可。后端

优化部分

3x图

以上步骤就进行完以后,我测试了一下绘制图片而且上传CDN的功能,一切正常!跨域

而后满心欢喜的展现这张图片的时候,发如今手机上展现出来的实在太模糊了,甚至连文字都看不清楚。数组

这时我才意识到咱们平时用的图片都是3x或者2x图,如今我按照UI稿的360px宽度绘制的这张图片只是1x图,在咱们高分辨率的手机上展现出来就会很模糊,所以为了让图片不模糊,我也须要将图片变为3x图。

所以将绘图时候全部的宽高及其余数组所有都×3.

this.width = 360 * 3;
this.height = 500 * 3;

this.ctx.drawImage(bg, 0, 0, this.width * 3, this.height * 3);
// 其余改动同理
复制代码

这样改动以后,展现出来的图片就很是清晰了!

缓存图片致使绘制卡主

因为须要加载多张图片,所以我这里须要监听全部图片都加载而且绘制成功以后,才能执行最终的canvas.toBlob()逻辑而且上传图片。

export class Canvas {
    constructor() {
        // ...
        this.loadedImageNumber = 0;
    }
    // code here
    imageOnLoaded() {
        this.loadedImageNumber++;
        if(this.laodedImageNumber >= 3) {
            // upload image
        }
    }
}
复制代码

结果pm和qa同窗测试的时候,常常发现上传过程卡住了,一直处于loading状态,后来通过无数次尝试和排查问题以后,发现由于前面有些图片已经加载过一次了,这里的图片有可能从浏览器的缓存里面获取了,所以根本不会执行onload函数,这样就致使this.loadedImageNumber一直达不成大于等于3的条件,因此就卡在loading状态了。

解决办法是完善一下addImage函数,在监听onload的同时判断一下img的状态,若是是complete的话也执行一遍回调逻辑,顺便也加了一下关于onerror的处理。

export class Canvas {
    // code here...
    // 加载图片
    addImage(src: string) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.onload = () => {
                resolve(img);
            }
            img.onerror = () => {
                // error callback
                reject();
            }
            img.src = src;
            if(img.complete) {
                resolve(img);
            }
        });
    }
    // code here...
}
复制代码

这样测试了一下全部图片均可以正常被加载出来了~

polyfill

上面问题解决了以后,QA同窗有反馈有一些低端手机依然卡在loading状态,我原本还觉得是图片绘制依然有问题,而后我借过手机来调试了一下,发现并非卡在图片绘制过程,而是canvas.toBlob()的时候报错了,因而后面的逻辑都卡住了。

export class Canvas {
    // code here
    imageOnLoaded() {
        this.loadedImageNumber++;
        if(this.laodedImageNumber >= 3) {
            this.canvas.toBlob(blob => {
                // 真正的上传函数
                this.uploadImage(blob);
            },
            'image/jpeg',
            1.0
            )
        }
    }
    // code here
}
复制代码

再次上网查了查资料,发现须要打polyfill才行。

github地址:JavaScript-Canvas-to-Blob

网上不少使用介绍的文章,或者直接看github官网的readme也很容易看懂。

if(__BROWSER__) {
    // import canvas toblob polyfill
    require('blueimp-canvas-to-blob');
}
export class Canvas {
    // ...
}
复制代码

__BROWSER__webpack.DefinePlugin定义的客户端渲染环境。

这下感受应该万事大吉了。

优化图片大小

而后就又被QA同窗找了。。

QA同窗反馈说图片上传太慢了,弱网状况下要loading好久才会结束,或者甚至直接到后端接口返回超时了也尚未结束图片上传。

我抓包看了看图片上传的接口,发现确实有点慢,由于生成的图片体积太大了,足足有2.6M多

问了一下同事,原来是最初图片模糊的时候,我想要提升图片质量,改为3x图的同时又在toBlob()的时候指定图片质量是1.0,因此致使了图片体积过大。

把这里的图片质量指定为0.8左右以后体积一下就降下来了,而且图片质量其实并无改变多少。

export class Canvas {
    // code here
    imageOnLoaded() {
        this.loadedImageNumber++;
        if(this.laodedImageNumber >= 3) {
            this.canvas.toBlob(blob => {
                // 真正的上传函数
                this.uploadImage(blob);
            },
            'image/jpeg',
            0.8
            )
        }
    }
    // code here
}
复制代码

到这里这个功能总算是完成了 OwO

相关文章
相关标签/搜索