用過PS的同學都知道使用選擇框、套索、魔棒工具選擇區域后,邊線會有一個黑白條紋交替移動產生的動畫,俗稱螞蟻線,作用是更明顯的突出選擇區域范圍。
---原理---
通過觀察PS,發現:一、線條可以任意復雜;二、並且不是只往線條指向的一個方向移動;三、不同位置角度的黑白線段長度不同;四、可以跟隨圖片縮放自然的產生不同效果;五、當生成很多線條時,CPU占用率也很低(比如在復雜圖像中使用魔棒時)。
如何做到的呢?一開始我把這些"線"當成一條條黑白相間的線條處理,發現:一、達不到PS的效果;二、當線條復雜時,處理的復雜性大大增加以至於難以完成;三、很難跟隨圖片縮放自然變化;四、線段多時CPU占用偏高。
繼續觀察PS,發現在拖動選擇區域時,動畫效果會暫停,但向不同方向拖動時還是會產生某種動畫效果,並且方向不同效果也不同,這讓我想起以前看過的一個系列視頻——把一塊帶有特定形狀縫隙的板子放到畫有明暗條紋背景的平面上,以一定的速度沿着一定方向拖動板子,會形成某種好玩的動畫效果。這啟發我想到PS會不會也是這樣實現的動畫效果?——畫布是板子,邊線是板子上縫隙,畫布下面是斜45度黑白條紋排列的平面背景(見[圖1]),畫布拖不動,反向拖背景效果一樣,於是拖動背景產生動畫效果。

圖1
上述推測對不對呢?單條"縫"太窄看不清,撬開"板子"看看吧——我想到的辦法是在PS中左右來回畫一些上下距離一個像素的直線,當這些"縫隙"密密麻麻連起來時,也就起到了撬開"板子"的效果。鼠標+直尺?太蛋疼手一抖就連成一片前功盡棄了。想起以前用過一個叫按鍵精靈的軟件,可以通過腳本控制鼠標鍵盤,正合適在這里用。於是下載安裝寫腳本保存退出F10運行OK——成功撬開,看看結果(見[圖2])——斜45度4個像素寬的黑白相間條紋不停滾動形成動畫,證實了我前面的推測是正確的。

                                                   圖2
---實現---
知道了原理,實現應該就不難了,但如果要像PS里一樣高效率的實現還需要一點技巧。我們知道在電腦里不用真的拖動什么東西,只要模擬出相同效果即可。想像一下,要模擬拖動效果,只要在一定時間間隔內用正確的顏色不斷填充所有"縫隙",然后不斷循環應該就可以達到了。產生循環簡單:只要一個定時器即可;重點是如何正確高效的在不同位置計算並顯示相應顏色,顯示顏色也比較簡單可以直接用SetPixel函數,或者使用更快的方法:比如在GDI中直接寫DIB內存。
現在問題就剩下——給定任意一個坐標點,如何快速准確的計算該點顏色值。一時沒有頭緒,畫個草圖看看(見[圖3]同時參考[圖1]),首先注意到的是外面的邊線(即x/y軸,左上角為原點):0-3是黑色、4-7是白色、8-11黑色、12-15是白色,依此類推可以通過簡單的公式計算出邊線點是黑色還是白色:((x or y) % 8) < 4 為黑色,否則是白色。邊線上點的問題解決了,那其他位置的點有沒有簡單的計算方法呢?繼續觀察[圖4]:因為所有斜邊都是45度,所以和x/y軸組成了一個等腰直角三角形,而等腰直角三角形有如下性質:所有斜邊上的點的x坐標加y坐標的和都相等,利用這個性質可以很容易的求出與任意點在同一斜邊上的邊線點的坐標值,這樣結合上面的邊線點顏色公式,可以得到任意點顏色公式:((x + y) % 8) < 4 為黑色,否則是白色。
               
圖3 圖4
總結一下:先得到邊線點(x/y軸、直角邊)的顏色公式(很直觀),再根據等腰直角三角形斜邊的性質,將任意點轉化成同一斜邊上的邊線點,使未知的求任意點顏色值的問題簡化成已知的求邊線點顏色值的問題,最后綜合並簡化步驟得到任意點的顏色公式。
---代碼---
可以寫代碼了,鑒於黑(0)白(0FFFFFFh)兩色的特殊性,最后還可以把顏色判斷優化掉
上面啰嗦了那么多,代碼其實很簡單,用c甚至只要一行:
Color = ((((X + Y - PixShift) & 4) >> 2) - 1) & 0xFFFFFF; // 用定時器讓PixShift在0-7之間循環,更簡單:PixShift = (++PixShift) & 7;
具體實現看代碼中以下兩個回調函數:
LineDDAProc
Lasso_TimerProc
匯編版demo中有一些關於原理的演示,點擊右鍵觀看。(下載地址:https://files.cnblogs.com/files/hhh2000/lasso.zip)
效果圖1:

匯編版效果圖1:

匯編版效果圖2:

---一點感想---
1、使用我的這種方法只要知道點的坐標,然后通過坐標計算出點的顏色再寫入即可,操作簡單。
2、這樣實現速度也比較快,因為操作簡單,指令少了,自然就快了;同時顯示幾千條邊、百萬個點也很流暢,而且有優化余地——保存點坐標等。
3、試了幾個能實現螞蟻線的圖形軟件,還是PS實現的好:准確、流暢、不占用資源,就是不知道具體如何實現的。
---參考和學習資料--- (其實寫代碼沒參考到什么,都是寫完代碼以后,寫這篇文章的時候找的,然后中文資料有用的基本沒有)
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的方法
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 }
https://files.cnblogs.com/files/hhh2000/lasso.zip
