近来的一个客户项目中,简化的需求是绘制根据行列绘制许多个圆圈。需求看起来不难,上手就可以够做,写两个for轮回。
原始绘制要领
起首定义了许多Circle对象,在遍历轮回中挪用该对象的draw要领。代码以下:
for (var i = 0; i < column; i++) {
for (var j = 0; j < row; j++) {
var circle = new Circle({
x: 8 * i + 3,
y: 8 * j + 3,
radius: 3
})
box.push(circle);
}
}
console.time('time');
for (var c = 0; c < box.length; c++) {
var circle = box[c];
circle.draw(ctx);
}
console.timeEnd('time');
结果绘制出了根据行列排布的许多个圆圈了,以下图所示:
恩,很简朴嘛,可以回家睡觉了。
等等,客户请求绘制的极限是10万个,而且每次绘制不能卡顿。先看下绘制10万个圆圈的时刻是多久,用console.time 统计绘制时刻:
console.time('time');
// 现实绘制的代码
console.timeEnd('time');
时刻显现为几百毫秒(3到4百毫秒),以下图所示:
几百毫秒的绘制时刻,必定是卡顿的。想要流通操纵,一定还的优化。
批量绘制
起首想到的是批量绘制,前面的代码中,每次变量都邑挪用circle.draw(ctx)要领,circle.draw要领代码以下:
draw: function (ctx) {
ctx.save();
ctx.lineWidth=this.lineWidth;
ctx.strokeStyle=this.strokeStyle;
ctx.fillStyle=this.fillStyle;
ctx.beginPath();
this.createPath(ctx);
ctx.stroke();
if(this.isFill){ctx.fill();}
ctx.restore();
},
可以看出 每次遍历都挪用了一次beginPath和stroke要领。为了进步绘制效力,我们可以只挪用beginPath和stroke要领一次,把一切的子途径构造成为一个大的途径,这就是所谓的批量绘制思绪,代码以下:
console.time('time');
ctx.beginPath();
for (var c = 0; c < box.length; c++) {
var circle = box[c];
ctx.moveTo(circle.x + 3, circle.y);
circle.createPath(ctx);
}
ctx.closePath();
ctx.stroke();
console.timeEnd('time');
调试发明,确切效力有了很大的提拔,时刻削减到100毫秒摆布,相当于效力进步了3-4倍摆布,以下图所示:
。
须要注重的是上述代码中的moveTo语句:
ctx.moveTo(circle.x + 3, circle.y);
这是因为: 当运用arc要领给途径中增加子途径的时刻,arc所定义的途径会自动和途径鸠合中的末了一个途径衔接起来,以下图所示:
此处的moveTo就是为了防止这类衔接。
注重:arc 和arcTo都邑有上述题目,然则rect定义的途径却不存在这类题目。
Pattern 体式格局
经由过程以上优化,客户已以为效力挺不错了。 然则手艺研讨没有止境,因为这个散布很规律,总觉得有越发疾速的要领。终究突发灵感想到了一种要领,就是运用canvas 的Pattern功用:
canvas的fillStyle可以指定为一个pattern对象,而pattern可以完成一个简朴图象的平铺。基于这类思绪,我们可以完成以下代码:
var tempCanvas = document.createElement('canvas');
var ctx2 = tempCanvas.getContext('2d');
var w = 5,h = 5;
tempCanvas.width = w;
tempCanvas.height = h;
dpr(tempCanvas);
ctx2.fillStyle = 'red';
ctx2.arc(w/2,h/2,w/2 - 1,0,Math.PI * 2);
ctx2.stroke();
ctx.save();
ctx.beginPath();
var width = tempCanvas.width * 500,height = tempCanvas.height * 200;
var pattern = ctx.createPattern(tempCanvas, 'repeat');
ctx.clearRect(100,100,width,height);
ctx.rect(100,100,width,height);
ctx.fillStyle = pattern;
ctx.fill();
ctx.restore();
代码起首定义一个小的canvas,命名为tempCanvas,在tempCanvas上面绘制一个圆,须要注重的是tempCanvas的尺寸要设置为恰好绘制下这个圆圈。
然后经由过程经由过程tempCanvas建立pattern对象,并把canvas的绘制上下文ctx的fillStyle指定为该pattern对象。
以后经由过程rect要领指定要fill的地区大小,改地区大小应当是一切终究要绘制的圆圈的大小的总和:var width = tempCanvas.width 500,height = tempCanvas.height 200;
末了挪用画笔的fill要领,用tempCanvas添补地区。终究绘制的结果和绘制斲丧的时刻以下图所示:
经由过程上图可以看出,效力极高,可以到达零点几毫秒的级别。
新的需求
假如客户需求只是这么简朴,置信运用canvas pattern对象这类体式格局,效力是最高的。然则,客户的现实需求是,先绘制10万个的圆圈,然后可以用擦除东西,擦除一些地区的圆圈,以下图所示:
原始绘制要领和批量绘制要领如果完成上述结果,都很轻易,只要把不须要绘制圆圈的位置,直接疏忽掉即可以。
比如用一个map纪录须要疏忽的圆圈的坐标,遍历的时刻推断在map纪录中的处所就直接跳过不举行绘制操纵。
canvas pattern + 裁剪
假如是canvas pattern的体式格局,应当怎样完成上图的结果呢? 经由思索发明可以经由过程ctx.clip要领。
clip,裁剪。假如经由过程ctx.clip定义了裁剪地区,绘制的图形只会在裁剪地区的部份显现出来,裁剪地区以外的,则不会显现。
没一个圆圈都邑占用一个矩形地区,本案例中,可以把要显现的的圆圈所占的矩形地区都定义到裁剪地区内里,而不要显现的圆圈的矩形地区则消除到裁剪地区以外,以下图所示,绘制圆圈的矩形地区用实线示意出来,不绘制圆圈的地区用虚线示意:
只须要把一切实线示意的矩形地区都增加到要clip的途径中去,然后挪用fill要领,则只会在完成定义的矩形地区显现出来圆圈。以下是示例代码:
for(var i = 0;i < 400; i ++){
for(var j = 0;j < 400;j ++){
var r = Math.random();
if(r <0.2){
templateMap[i+":" + j] = true;
continue;
}
var x = 10 + j * tempCanvas.width;
var y = 10 + i * tempCanvas.height;
var rect = {
x : x,
y : y,
width : tempCanvas.width,
height:tempCanvas.height
};
ctx.rect(rect.x,rect.y,rext.width,rect.height);
}
ctx.clip();
起首遍历一切的圆圈坐标,为了演示结果,用Math.random为了模仿随机发生一个数,假如这个数小于0.2,则当前圆圈的矩形地区不会被到场裁剪地区,也就是该圆圈不会显现出来。
经由过程上面裁剪操纵后,“擦除后的结果”算是完成了。然则,经由测试,机能却低徊去了,为何,因为增加了许多rect操纵。测试下来,一幁的绘制时刻大概在80多毫秒,比批量绘制照样高一点,然则觉得照样不够好。
Pattern + 兼并裁剪
视察上面 “裁剪地区” 这个图,以第一行动例,第一、第二、第三个矩形地区是连在一块的,完整没有必要挪用三次ctx.rect要领,而是先用算法把三个地区兼并为一个矩形地区,然后挪用一次ctx.rect要领即可,以下图:
下面是兼并裁剪地区的算法,现在只是完成了统一行的兼并,越发优化的兼并算法并没有完成,代码以下:
function calRectMap (tempCanvas){
if(rectMap != null){
return;
}
rectMap = rectMap || [];
for(var i = 0;i < 400; i ++){
for(var j = 0;j < 400;j ++){
var r = Math.random();
if(r <0.2){
templateMap[i+":" + j] = true;
continue;
}
var x = 10 + j * tempCanvas.width;
var y = 10 + i * tempCanvas.height;
var rect = {
x : x,
y : y,
width : tempCanvas.width,
height:tempCanvas.height
};
lineRectMap[i] = lineRectMap[i] || [];
lineRectMap[i][j] = rect;
}
unionLineRects(lineRectMap[i],rectMap);
}
}
function unionLineRect(rect1,rect2){
return {
x: rect1.x,
y : rect1.y,
width:rect1.width + rect2.width,
height:rect1.height
}
}
function unionLineRects(lineRectMap,rectMap){
var lastRect = null,lastNotNullIndex = null;
for(var j = 0;j < 400;j ++){
var currentRect = lineRectMap[j];
if(lastRect == null){
lastRect = currentRect;
}else{
if( lastNotNullIndex == j - 1 && currentRect){
lastRect = unionLineRect(lastRect,currentRect);
}
}
if(currentRect != null){
lastNotNullIndex = j;
}else if (lastRect){
rectMap.push(lastRect);
lastNotNullIndex = null;
lastRect = null;
}
}
if(lastRect){
rectMap.push(lastRect);
}
}
相干兼并的算法,此处不再细致申明。 兼并以后,测试绘制的时刻下降到了10几毫秒,算是比较好的绘制结果了:
webgl绘制
因为笔者本人也历久研讨webgl的手艺,所以尝试着用webgl实线了2d的绘制,相干细节不在此处赘述,背面会写特地的文章如何用webgl绘制2d图形。终究测试的效力不是很抱负,差不多100多毫秒,和上面的批量绘制差不多。 因为用webgl绘制,单次的绘制效力应当不会太差,然则因为须要遍历挪用10万次绘制敕令,必定效力不高。别的webgl绘制的结果现实上是没有2d绘制的结果好的,锯齿严峻。 要完成好的结果,还须要引入去锯齿相干手艺。 绘制的结果以下:
用webgl绘制2d图形的相干主题,转头会别的写一篇文章引见。敬请关注。
webgl2绘制
webgl2 引入了实例化数组,经由过程这个功用,可以完成把许屡次的绘制挪用兼并为一个绘制挪用,这会极大进步绘制效力。
有关实例化数组的功用,参考
https://www.jianshu.com/p/d40…
绘制10万个圆形的效力大概在每帧零点零几毫秒,几乎就是大boss级别的快,以下图:
跋文
经由过程这篇文章,除了想给读者通报相干学问点以外,实在还想表达一个看法:
比拟于学问点,程序员越发须要磨炼的是底层思维才能。在我看来,底层思维才能包含:进修力、创造力、推断力和思索力。而勤于思索的人,不拘泥于屡见不鲜,都可以从一样平常死板的使命中发明许多风趣的东西,启示更多深切的思绪。
勤于思索是很主要的。 学问是死的,人是活的,一样的学问点,在思索力强的人手上,就可以延伸出许多好的处理计划。
这就请求人勤于探究,不要满足于把使命完成,而是要多深切思索,多总结,探究更多的计划和能够性。这自身有助于磨炼思索力和创造力,而思索力和创造力又会反过来协助你处理更多的题目。
实在IT行业的学问更新越来越快,可以以不变应万变的人,就是具有优越的进修力、创造力、推断力和思索力的人。这些才能会让你在变更万千的手艺海洋中,挺立不倒,不被吞没。
固然,标书能够有点好为人师了。 在一样平常的工作中,彪叔更喜好做的事变,就是启示部属的思索,而不仅仅是某个题目的处理计划。这是比进修学问越发主要的素养。彪叔也会在我的其他文章中,分享底层才能的相干认知。有兴致的猿们可以关注彪叔的公号:ITman彪叔
迎接关注民众号: