HTML5 Canvas 如何取消反锯齿绘图(HTML5 Canvas how to turn off anti-aliasing drawing)一、问题的提出
我们都知道反锯齿(anti-aliasing)绘图给我们带来更好的视觉体验,有了这个技术,绘制的图形的边缘再不是以前毛毛躁躁的样子了。这就是采用反锯齿算法的功劳。其实质就是把要绘制的颜色边缘和背景颜色做适当的融合,在人的眼睛看来,有种像雾像雨又像风的感觉。HTML5 Canvas的绘图就是默认anti-aliasing的。其实作为一般的开发者,可以不关心这个东西的存在,好像anti-aliasing是理应如此的。但是,如果我们的用户非要看到non-anti-aliasing的效果呢?
这个类似有了酱油还要吃盐的问题。酱油是好,但是有人就是要吃盐,怎么办?
另外的需求是,即使有了酱油,我还是需要吃盐。为啥,酱油有它的好处,盐有盐的用处。
比如:当我们在Canvas上移动鼠标的时候,我需要知道我的鼠标位置在什么图形上,即著名的点选问题。Canvas以前的绘图软件解决这个问题有标准的方法,就是用图形的ID作为图形的颜色值,绘制在内存当中的后台画布上,当我们移动鼠标在前台显示的画布上,我们可以通过获取后台画布的该点的像素值(ID)来获得图形ID。
这样一切都近乎完美。后台画布与前台画布采用完全一样的绘图机制,不同点是前台画布采用用户看到的实际像素颜色值,而后台画布采用图形的ID作为图形绘制的像素颜色值。这里的前提是,我们能控制这些像素值,以确保它在被绘制到后台画布的时候不被改变,就是我让你画一个像素颜色=1,你别自作聪明搞出个=1.5。遗憾的是目前版本的HTML5 Canvas就是这种自作聪明的家伙。迄今为止,我们没办法控制去掉anti-aliasing这个自作聪明的算法。我试过即使把context.mozImageSmoothingEnabled=false也不行。
如果这儿谁有一句话的方法可以满足我上面讲的需求,那么这篇文章直接就等于是狗屎。我费了很多的努力研究出了这篇狗屎文,在这里以飨读者,包括我自己。
二、解决方法探讨
如何取消(废止)HTML5 Canvas的反锯齿功能,在http://stackoverflow.com上也有很多讨论。如果让HTML5实现者来解决这个问题,几乎就是一句话搞定的事情。然而需要我们一周的时间想各种点子。在HTML5 Canvas本身没解决这个问题之前,如果让有的人来解决,几乎要花掉几个月的时间,好在如果你看到这篇文章,你就可以告诉你的老板,你只需要几个晚上就可以解决了。下面我教你具体办法。
前台画布我不管它,你怎么画是你自己的问题,后台画布和前台一样大小,涂满黑色(#000000)。然后你在前台Canvas上画图形id=1的时候,同时在后台画布上用1为颜色画这个同样的图形。对了,我还没告诉你如何去掉anti-aliasing,如果不去掉anti-aliasing,系统可能给画出来的像素颜色是1.5,这显然不是你想要的,也不是我写这篇狗屎文的目的。
我只好用代码来说明问题。记住这是HTML5的代码,javascript而已。
在HTML5页面中有Canvas:
This browser does not support HTML5 Canvas.
创建前台画布: var canvas = document.getElementById("_canvasView"); var context = canvas.getContext("2d"); 创建后台画布: var backend = document.createElement("canvas"); backend.width = canvas.width; backend.height = canvas.height; var backendContext = backend.getContext("2d"); // 后台画布涂上黑色(我相信anti-aliasing不会把它搞成灰色) backendContext.fillStyle = "#000000"; backendContext.fillRect(0, 0, backend.width, backend.height); // 获取后台画布对应的像素数组 var imageObj = backendContext.getImageData( 0, 0, canvas.width, canvas.height); var imageData = imageObj.data; 如果你想给这个数组里的像素赋予颜色值,可以像下面的代码: function setPixel(imageData, width, height, x, y, red, green, blue) { // i 没做边界条件检查,留给读者自己完成。 var i = (y*width+x)*4; imageData = red; imageData[i+1] = green; imageData[i+2] = blue; // 下面这个是Alpha,我们不用 //DEL: imageData[i+3]=???; }
HTML5声称imageData是可以直接操控的。因此,这是我们良好的机会去直接设置像素值。把一个像素数组设置回绘图上下文和我写累了上厕所拉泡屎一样简单:
backendContext.putImageData(imageData, 0, 0);
接下来是发挥你大学学习计算机图形学的智慧的时候了。
三、真的需要微积分么?微积分大家原理都知道,真正用的时候就发抖了。毕竟我们是写网页脚本的,研究基础代数可不是强项啊。微积分肯定是不需要的,因此计算机图形学也是不需要的。然而,的确有一种解决的办法,就是利用计算机图形学的知识。很底层啊,读者要有心理准备,别雷倒了。
view plainprint? function setPixel(imageData, W, H, x, y, r, g, b) { var i = (y*W+x)*4; imageData = r; imageData[i+1] = g; imageData[i+2] = b; } /** * Bresenham's draw line algorithm */ function Bresenham_drawLine(imageData, width, height, x1, y1, x2, y2, r, g, b) { var dx = Math.abs(x2 - x1), dy = Math.abs(y2 - y1), yy = 0, t; if (dx < dy) { yy = 1; t=x1; x1=y1; y1=t; t=x2; x2=y2; y2=t; t=dx; dx=dy; dy=t; } var ix = (x2 - x1) > 0 ? 1 : -1, iy = (y2 - y1) > 0 ? 1 : -1, cx = x1, cy = y1, n2dy = dy * 2, n2dydx = (dy - dx) * 2, d = dy * 2 - dx; if(yy==1) { while(cx != x2) { if(d < 0) { d += n2dy; } else { cy += iy; d += n2dydx; } setPixel(imageData, width, height, cy, cx, r, g, b); cx += ix; } } else { while(cx != x2) { if(d < 0) { d += n2dy; } else { cy += iy; d += n2dydx; } setPixel(imageData, width, height, cx, cy, r, g, b); cx += ix; } } } /** * Bresenham's draw circle algorithm */ function _draw_circle_8(imageData, W, H, xc, yc, x, y, r, g, b) { setPixel(imageData, W, H, xc+x, yc+y, r, g, b); setPixel(imageData, W, H, xc-x, yc+y, r, g, b); setPixel(imageData, W, H, xc+x, yc-y, r, g, b); setPixel(imageData, W, H, xc-x, yc-y, r, g, b); setPixel(imageData, W, H, xc+y, yc+x, r, g, b); setPixel(imageData, W, H, xc-y, yc+x, r, g, b); setPixel(imageData, W, H, xc+y, yc-x, r, g, b); setPixel(imageData, W, H, xc-y, yc-x, r, g, b); } function Bresenham_drawCircle(imageData, width, height, xc, yc, radius, fill, r, g, b) { var x = 0, y = radius, yi, d = 3 - 2 * radius; if (fill==1) { while (x <= y) { for (yi = x; yi <= y; yi ++) { _draw_circle_8(imageData, width, height, xc, yc, x, yi, r, g, b); } if (d < 0) { d = d + 4 * x + 6; } else { d = d + 4 * (x - y); y--; } x++; } } else { while (x <= y) { _draw_circle_8(imageData, width, height, xc, yc, x, y, r, g, b); if (d < 0) { d = d + 4 * x + 6; } else { d = d + 4 * (x - y); y--; } x++; } } }
有个叫Bresenham的家伙,我们都要感谢他告诉我们怎么把一条线画到屏幕上。就是知道2个点坐标,从而知道如何点亮这条线经过的像素点的问题。我上面贴出来的代码不是卖弄我的图形学理论知识,实话说,代码是google出来的,我只是去伪存真而已。但是按上面的方法的确可以实现我要的去掉反走样,看看我工作的截图:
上面这个图就是带有反走样的前台画布的现实效果。而下面的这个就是后台画布去掉反走样的现实效果。我就是采用上面的Bresenham算法来实现的,运行效率还蛮高的。
其实用这种办法,新带来的问题比原来的更多更复杂:
1)都HTML5时代了,还要Bresenham这个老家伙出来壮场子,当我白痴啊。
2)写脚本的一般都是算法小白,这样会吓傻人的。
3)HTML5 Canvas如果真是这样用,岂不是更大的退步吗?
4)最大的困难是那么多图形类型,每个都要用Bresenham算法搞定,不是太2了么?
四、你可能需要第三块画布第三块画布是个即擦即用的画布,我称它为Swapped Canvas。有了它,Bresenham算法就滚蛋了。过程如下:
1)前台画布每画一个图形G,就在第三块画布上画同样的图形G,前提是第三块画布上每次画图形之前都清空(涂黑)。这就是即擦即用的意思。不要以为我打错字了(不是插),我没那么傻B。
2)然后取得第三块画布的图像像素数组,遍历图形G所在范围矩形内的全部像素p(i,j),如果像素值v=p(i,j)=0为黑,就忽略,否则就到后台画布相应的位置p(i,j)上用G的ID作为颜色填上p(i,j)=ID。
最后我们得到的后台画布一定是没有反锯齿的家伙。好了,anti-aliasing,滚你马蛋吧。
最后整个过程的伪代码如下:
view plainprint? // 遍历全部图形 for (ShapeId=1, 2, 3, ...) { // 取得每个要绘制的图形在画布上的像素坐标范围rect=(x0,y0,w,h) rect = getDrawShapeRect(ShapeId); // 画图形到前台画布上 drawShapeOnForeground(ShapeId); // 设置清除中间画布指定区域的颜色为0(黑色 BlackColor) fillSwappedBlackColor(rect); // 采用白色画笔和刷子绘制图形到中间画布上 drawShapeOnSwappedWithWhiteColor(ShapeId); // 遍历中间画布的rect区域内的所有像素 for (x=x0; x { for (y=y0; y { // 如果像素点有值(不是黑色) if (getSwappedPixelColor(x, y) != BlackColor) { // 在后台画布上设置对应点的像素为当前图形ID setBackendPixelColor(x, y, ShapeId); } } } } // 点选返回图形ID (0:未选中) HitTest(x, y) { return getBackendPixelColor(x, y); }