首页 > 代码库 > 如何实现PS中使用选择框、套索等工具后形成的蚂蚁线效果

如何实现PS中使用选择框、套索等工具后形成的蚂蚁线效果

用过PS的同学都知道使用选择框、套索、魔棒工具选择区域后,边线会有一个黑白条纹交替移动产生的动画,俗称蚂蚁线,作用是更明显的突出选择区域范围。


---原理---

通过观察PS,发现:一、线条可以任意复杂;二、并且不是只往线条指向的一个方向移动;三、不同位置角度的黑白线段长度不同;四、可以跟随图片缩放自然的产生不同效果;五、当生成很多线条时,CPU占用率也很低(比如在复杂图像中使用魔棒时)。

如何做到的呢?一开始我把这些"线"当成一条条黑白相间的线条处理,发现:一、达不到PS的效果;二、当线条复杂时,处理的复杂性大大增加以至于难以完成;三、很难跟随图片缩放自然变化;四、线段多时CPU占用偏高。

继续观察PS,发现在拖动选择区域时,动画效果会暂停,但向不同方向拖动时还是会产生某种动画效果,并且方向不同效果也不同,这让我想起以前看过的一个系列视频——把一块带有特定形状缝隙的板子放到画有明暗条纹背景的平面上,以一定的速度沿着一定方向拖动板子,会形成某种好玩的动画效果。这启发我想到PS会不会也是这样实现的动画效果?——画布是板子,边线是板子上缝隙,画布下面是斜45度黑白条纹排列的平面背景(见[图1]),画布拖不动,反向拖背景效果一样,于是拖动背景产生动画效果。

上述推测对不对呢?单条"缝"太窄看不清,撬开"板子"看看吧——我想到的办法是在PS中左右来回画一些上下距离一个像素的直线,当这些"缝隙"密密麻麻连起来时,也就起到了撬开"板子"的效果。鼠标+直尺?太蛋疼手一抖就连成一片前功尽弃了。想起以前用过一个叫按键精灵的软件,可以通过脚本控制鼠标键盘,正合适在这里用。于是下载安装写脚本保存退出F10运行OK——成功撬开,看看结果(见[图2])——斜45度4个像素宽的黑白相间条纹不停滚动形成动画,证实了我前面的推测是正确的。


---实现---

知道了原理,实现应该就不难了,但如果要像PS里一样高效率的实现还需要一点技巧。我们知道在电脑里不用真的拖动什么东西,只要模拟出相同效果即可。想像一下,要模拟拖动效果,只要在一定时间间隔内用正确的颜色不断填充所有"缝隙",然后不断循环应该就可以达到了。产生循环简单:只要一个定时器即可;重点是如何正确高效的在不同位置计算并显示相应颜色,显示颜色也比较简单可以直接用SetPixel函数,或者使用更快的方法:比如在GDI中直接写DIB内存。

现在问题就剩下——给定任意一个坐标点,如何快速准确的计算该点颜色值。一时没有头绪,画个草图看看(见[图3]同时参考[图1]),首先注意到的是外面的边线(即x/y轴,左上角为原点):0-3是黑色、4-7是白色、8-11黑色、12-15是白色,依此类推可以通过简单的公式计算出边线点是黑色还是白色:((x or y) % 8) < 4 为黑色,否则是白色。边线上点的问题解决了,那其他位置的点有没有简单的计算方法呢?继续观察[图3]:因为所有斜边都是45度,所以和x/y轴组成了一个等腰直角三角形,而等腰直角三角形有如下性质:所有斜边上的点的x坐标加y坐标的和都相等,利用这个性质可以很容易的求出与任意点在同一斜边上的边线点的坐标值,这样结合上面的边线点颜色公式,可以得到任意点颜色公式:((x + y) % 8) < 4 为黑色,否则是白色。

总结一下:先得到边线点(x/y轴、直角边)的颜色公式(很直观),再根据等腰直角三角形斜边的性质,将任意点转化成同一斜边上的边线点,使未知的求任意点颜色值的问题简化成已知的求边线点颜色值的问题,最后综合并简化步骤得到任意点的颜色公式。


---代码---

可以写代码了,鉴于黑(0)白(0FFFFFFh)两色的特殊性,最后还可以把颜色判断优化掉
上面啰嗦了那么多,代码其实很简单,用c甚至只要一行:
Color = ((((X + Y - PixShift) & 4) >> 2) - 1) & 0xFFFFFF; // 用定时器让PixShift在0-7之间循环,更简单:PixShift = (++PixShift) & 7;

具体实现看代码中以下两个回调函数:
LineDDProc
Lasso_TimerProc

汇编版的demo中有一些关于原理的演示,点击右键观看。


---参考和学习资料--- (其实写代码没参考到什么,都是写完代码以后,写这篇文章的时候找的,然后中文资料有用的基本没有)

1、wiki http://en.wikipedia.org/wiki/Marching_ants

2、一篇关于GIMP图形处理软件中蚂蚁线简单原理说明以及存在问题的文章 https://banu.com/blog/24/fun-with-marching-ants/

3、 一个JS网页版的实现 http://codepen.io/sstephenson/pen/LrJIG

4、一个C#版的实现 http://www.codeproject.com/Articles/6269/The-Secret-of-Marching-Ants 原理差不多(抽板子),用的是八个8*8pixel生成patterns的方法


试了几个能实现蚂蚁线的图形软件,还是PS实现的好:准确、流畅、不占用资源,就是不知道具体如何实现的。

 

c写的演示程序,VC++6编译

  1 // Windows Header Files:  2 #include <windows.h>  3 #include <windowsx.h>  4   5 // C RunTime Header Files  6 #include "stdio.h"  7   8 // Global Variables:  9 TCHAR szBuffer[256]; 10  11 // Foward declarations of functions included in this code module: 12 LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM); 13  14 int APIENTRY WinMain(HINSTANCE hInstance, 15                      HINSTANCE hPrevInstance, 16                      LPSTR     lpCmdLine, 17                      int       nCmdShow) 18 { 19     WNDCLASSEX wc; 20     MSG msg; 21     HWND hWnd; 22  23     memset(&wc, 0, sizeof(wc)); 24     wc.cbSize      = sizeof(wc); 25     wc.style       = 0; 26     wc.lpfnWndProc = (WNDPROC)WndProc; 27     wc.hInstance   = hInstance; 28     wc.lpszClassName = "Draw"; 29     wc.hCursor       = LoadCursor(NULL, IDC_ARROW); 30  31     RegisterClassEx(&wc); 32  33     hWnd = CreateWindowEx(0, wc.lpszClassName, wc.lpszClassName, WS_OVERLAPPEDWINDOW, 100, 150, 650, 455, NULL, NULL, hInstance, NULL); 34  35     ShowWindow(hWnd, SW_SHOWNORMAL); 36     UpdateWindow(hWnd); 37  38     while (GetMessage(&msg, NULL, 0, 0)) 39     { 40         if (!TranslateAccelerator(msg.hwnd, 0, &msg)) 41         { 42             TranslateMessage(&msg); 43             DispatchMessage(&msg); 44         } 45  46     } 47      48     return msg.wParam; 49 } 50  51 // Global Variables: 52 HDC hDrawDC; 53 HGDIOBJ hBmp; 54 HGDIOBJ hOldBmp; 55 int* lpPt; 56 int nPoint; 57 int nDot; 58 UINT PixShift; 59 UINT* lpBits; 60 BITMAPINFO bmi; 61 int iWidth; 62  63 // Foward declarations of functions included in this code module: 64 void CALLBACK Lasso_TimerProc(HWND, UINT, UINT, DWORD); 65 void SaveDot(int*, int*, POINT*); 66 void ShowLineDot (int*, int); 67 void CALLBACK LineDDAProc(int, int, LPARAM); 68 HGDIOBJ GetDIB (HDC, int, int, void**); 69  70 WINGDIAPI COLORREF WINAPI SetDCBrushColor(HDC, COLORREF); 71 #define DC_BRUSH 18 72 ////////////////////////////////////////////////////////////// 73 //  74 ////////////////////////////////////////////////////////////// 75 LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) 76 { 77     POINT pt; 78     RECT Rect; 79     PAINTSTRUCT ps; 80     HDC hOldDC; 81  82     switch (uMsg) 83     { 84         case WM_CREATE: 85             // 创建兼容DC & 位图 86             hOldDC = GetDC(hWnd); 87             hDrawDC = CreateCompatibleDC(hOldDC); 88  89             GetWindowRect(GetDesktopWindow(), &Rect); 90             hBmp = GetDIB(hDrawDC, Rect.right, Rect.bottom, &lpBits); 91             if (hBmp == 0) 92             { 93                 MessageBox(0, 0, "无法取得设备无关位图", MB_APPLMODAL); 94                 exit(0); 95             } 96             hOldBmp = SelectObject(hDrawDC, hBmp); 97             ReleaseDC(hWnd, hOldDC); 98  99             // 设置背景画刷100             SetDCBrushColor(hDrawDC, RGB(0x5B,0x5B,0x5B));101             SelectObject(hDrawDC, GetStockObject(DC_BRUSH));102 103             lpPt = (int*)malloc(10000*sizeof(POINT));104             if(!lpPt)105             {106                 exit(0);107             }108             nPoint = 0;109             nDot = 0;110             iWidth = bmi.bmiHeader.biWidth * 4;111 112             // 设置定时器113             PixShift = SetTimer(hWnd, 1, 100, Lasso_TimerProc);            114             break;115         case WM_PAINT:116             BeginPaint(hWnd, &ps);117             118             // 画背景119             CopyRect(&Rect, &ps.rcPaint);120             InflateRect(&Rect, 1, 1);121             Rectangle(hDrawDC, Rect.left, Rect.top, Rect.right, Rect.bottom);122             123             // 画点124             nDot = 0;125             ShowLineDot(lpPt, nPoint);126 127             sprintf(szBuffer, "%d : %d", nPoint, nDot);128             TextOut(hDrawDC, 0, 0, szBuffer, lstrlen(szBuffer));129 130             // 翻转到屏幕131             pt.x = ps.rcPaint.right - ps.rcPaint.left;132             pt.y = ps.rcPaint.bottom - ps.rcPaint.top;133             BitBlt(ps.hdc, ps.rcPaint.left, ps.rcPaint.top, pt.x, pt.y, hDrawDC, ps.rcPaint.left, ps.rcPaint.top, SRCCOPY);134 135             EndPaint(hWnd, &ps);136             break;137         case WM_LBUTTONDOWN:138             SetCapture(hWnd);139             nPoint = 0;140             nDot = 0;141             // 保存鼠标位置142             pt.x = GET_X_LPARAM(lParam);143             pt.y = GET_Y_LPARAM(lParam);144             SaveDot(lpPt, &nPoint, &pt);145             break;146         case WM_MOUSEMOVE:147             if (wParam & MK_LBUTTON && GetCapture() == hWnd)148             {149                 // 保存鼠标轨迹150                 pt.x = GET_X_LPARAM(lParam);151                 pt.y = GET_Y_LPARAM(lParam);152                 GetClientRect(hWnd, &Rect);153                 if (PtInRect(&Rect, pt))154                 {155                     SaveDot(lpPt, &nPoint, &pt);156                 }157                 InvalidateRect(hWnd, 0, FALSE);158             }159             break;160         case WM_LBUTTONUP:161             if (GetCapture())162             {163                 // 保存起始位置164                 if (nPoint > 0)165                 {166                     SaveDot(lpPt, &nPoint, (POINT*) lpPt);167                 }168             }169             ReleaseCapture();170             InvalidateRect(hWnd, 0, FALSE);171             break;172         case WM_ERASEBKGND:173             return TRUE;174         case WM_DESTROY:175             KillTimer(hWnd, 1);176             DeleteObject(SelectObject(hDrawDC, hOldBmp));177             DeleteDC(hDrawDC);178             PostQuitMessage(0);179             break;180         default:181             return DefWindowProc(hWnd, uMsg, wParam, lParam);182     }183     return 0;184 }185 186 //////////////////////////////////////////////////////////////187 // 定时器回调188 //////////////////////////////////////////////////////////////189 void CALLBACK Lasso_TimerProc(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime)190 {191     PixShift = (++PixShift) & 7;192     InvalidateRect(hwnd, 0, FALSE);193 }194 //////////////////////////////////////////////////////////////195 // 保存坐标点196 //////////////////////////////////////////////////////////////197 void SaveDot(int* lpPt, int* lpnPoint, POINT* lpstPoint)198 {199     lpPt[*lpnPoint*2] = lpstPoint->x;200     lpPt[*lpnPoint*2+1] = lpstPoint->y;201     (*lpnPoint)++;202 }203 //////////////////////////////////////////////////////////////204 // 线转点并画点205 //////////////////////////////////////////////////////////////206 void ShowLineDot (int* lpPt, int nPoint)207 {208     int i;209     for (i = 1; i < nPoint; i++)210     {211         LineDDA(lpPt[i*2-2], lpPt[i*2-2+1], lpPt[i*2], lpPt[i*2+1], LineDDAProc, 0);212     }213 }214 //////////////////////////////////////////////////////////////215 // 将窗口坐标转换为DIB段内存地址(32bit)216 // 行的长度为四字节的倍数,不足的0补齐217 //////////////////////////////////////////////////////////////218 void SetDIBPixel(HDC hDrawDC, int X, int Y, COLORREF Color)219 {220     lpBits[(X + ((bmi.bmiHeader.biHeight - Y - 1) * bmi.bmiHeader.biWidth))] = Color;221 }222 //////////////////////////////////////////////////////////////223 // 回调函数(画点时)224 //////////////////////////////////////////////////////////////225 void CALLBACK LineDDAProc(int X, int Y, LPARAM lpData)226 {227     UINT Color;228     // 根据位置计算颜色,形成动画效果229     Color = X + Y - PixShift;230     Color = (((Color & 4) >> 2) - 1) & 0xFFFFFF;231     // 计数 & 画点232     nDot++;233     SetDIBPixel(hDrawDC, X, Y, Color);234     //SetPixel(hDrawDC, X, Y, Color);235 }236 //////////////////////////////////////////////////////////////237 // 取得DIB238 //////////////////////////////////////////////////////////////239 HGDIOBJ GetDIB (HDC hDrawDC, int Width, int Height, void** lpBits)240 {241     RECT _Rect;242     HGDIOBJ _hBmp;243 244     SetRect(&_Rect, 0, 0, Width, Height);245     // 填充DIB段结构246     RtlZeroMemory(&bmi, sizeof(BITMAPINFO));247     bmi.bmiHeader.biSize        = sizeof(bmi.bmiHeader);248     bmi.bmiHeader.biHeight      = Height;249     bmi.bmiHeader.biWidth       = Width;250     bmi.bmiHeader.biPlanes      = 1;251     bmi.bmiHeader.biBitCount    = 32;252     bmi.bmiHeader.biCompression = BI_RGB;253     // 创建hBmp254     _hBmp = CreateDIBSection(hDrawDC, &bmi, DIB_RGB_COLORS, lpBits, 0, 0);255     if (_hBmp == 0)256     {257         *lpBits = 0;258         return 0;259     }260     // 取得hBmp大小261     GetDIBits(hDrawDC, _hBmp, 0, 0, 0, &bmi, 0);262     return _hBmp;263 }
View Code

 

如何实现PS中使用选择框、套索等工具后形成的蚂蚁线效果