用过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 // 取得DIB 238 ////////////////////////////////////////////////////////////// 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 // 创建hBmp 254 _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 }