// WaterInterface.h #ifndef WaterInterfaceh #define WaterInterfaceh #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #include "PixelBlock.h" /* CWaterInterface makes the contents of a window appear to be under water. The constant speed is a consequence of the algorithm. Most implementations of this algorithm use large arrays, DirectX or OpenGL... This doesn't need DirectX or OpenGL, all drawing is done using CPixelBlock. The two height Arrays are chars so the overhead is one Byte for every two pixels. The Lighting effect is done by altering the pixel's brighteness using CPixelBlock::BlendPixel(...) to Blend the Pixel with Black or White. To use it, either draw first(and only once!) and call the Draw function afterwards or (preferably) derive a class from it which implements the InitDC and InitPixelBlock functions and put your drawing code in there. //_____________________________________________________________________________ This example uses an Owner-draw Button (IDC_Water) in a Dialog: class CAboutButton : public CButton, public CWaterInterface { LPDRAWITEMSTRUCT pDrawItemStruct; WORD LastX, LastY; public: CAboutButton() : CWaterInterface(false,true) {} virtual ~CAboutButton() {} private: bool InitDC(HDC hDC, WORD Width, WORD Height) { // Draw what you want on the DC and then return true; CRect Rect(0,0,Width,Height); FillRect(hDC, &Rect, CreateSolidBrush(GetSysColor(COLOR_3DFACE))); //Draw in dialogs background colour DrawEdge(hDC, (LPRECT)&Rect, EDGE_RAISED, BF_RECT); CString Title; GetWindowText(Title); DrawText(hDC, Title,-1, (LPRECT)&Rect, DT_SINGLELINE|DT_CENTER|DT_VCENTER); return true; // Always } void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) {Draw(lpDrawItemStruct->hDC);} DECLARE_MESSAGE_MAP() afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnMouseMove (UINT nFlags, CPoint point); }; //>>>>>>>>>>>>>>> AboutButton <<<<<<<<<<<<<<<<<<<<<<< BEGIN_MESSAGE_MAP(CAboutButton, CButton) ON_WM_LBUTTONDOWN() ON_WM_MOUSEMOVE() END_MESSAGE_MAP() void CAboutButton::OnLButtonDown(UINT nFlags, CPoint point) {CButton::OnLButtonDown(nFlags, point); Dip((WORD)point.x, (WORD)point.y, 10, 127);} void CAboutButton::OnMouseMove (UINT nFlags, CPoint point) { if(IsDripping()) Straight(LastX, LastY, (WORD)point.x, (WORD)point.y,1); LastX=(WORD)point.x; LastY=(WORD)point.y; } Then in the Dialog have: CAboutButton About; You need to size the PixelBlock in OnInitDialog: BOOL CPhidippusDlg::OnInitDialog() { CDialog::OnInitDialog(); Water.SubclassDlgItem(IDC_Water, this); CRect Rect; Water.GetClientRect(&Rect); Water.Set(Rect.Width(), Rect.Height()); ... } void CMyDlg::OnBnClickedAbout() { About.StopRaining(); CDialog(IDD_ABOUTBOX).DoModal(); About.StartRaining(m_hWnd); } You can stop flashing as Windows erases control backgrounds by setting the "Clip Children" Property for the Dialog. //_____________________________________________________________________________ You can save all the frames as bitmaps using the Draw function with the SaveFiles parameter set to true. they get saved in the current Directory as t000.bmp upwards. //_____________________________________________________________________________ To Draw Directly on the Dialog Window do the following: Create a Class that is the "paper" you will draw to derived from CWaterInterface: class CMyGame : public CWaterInterface { ... } Declare one in the dialog .cpp file as a global: CMyGame MyGame; void CMyDlg::OnPaint() { CPaintDC dc(this); // device context for painting if(IsIconic()) { CRect Rect; GetClientRect(&Rect); SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0); int cxIcon=GetSystemMetrics(SM_CXICON); int cyIcon=GetSystemMetrics(SM_CYICON); int x=(Rect.Width ()-cxIcon+1)/2; int y=(Rect.Height()-cyIcon+1)/2; dc.DrawIcon(x, y, m_hIcon); }else MyGame.Draw(dc.GetSafeHdc()); // <<<<<<<<<<<< Water Drawn Here! } BOOL CMyDlg::OnEraseBkgnd(CDC* pDC) {return TRUE;} // Don't erase - it flashes! void CMyDlg::OnLButtonDown(UINT nFlags, CPoint point) { MyGame.OnClick((WORD)point.x, (WORD)point.y); CDialog::OnLButtonDown(nFlags, point); } You need to tell the PixelBlock its size which for complete windows is best done in OnSize. For re-sizing windows you need to stop the water before resizing and restart it afterwards: void CMyDlg::OnSize(UINT nType, int cx, int cy) { MyGame.StopRaining(); MyGame.Resample(); // Calls your InitDC and InitPixelBlock again next redraw MyGame.Set(cx,cy); // Resizes the Pixel Block CDialog::OnSize(nType, cx, cy); MyGame.StartRaining(m_hWnd); } If you pop up other dialogs like the about box you will need to stop the water first: void CMyDlg::OnSysCommand(UINT nID, LPARAM lParam) { if((nID & 0xFFF0)==IDM_ABOUTBOX) { MyGame.StopRaining(); CDialog(IDD_ABOUTBOX).DoModal(); MyGame.StartRaining(m_hWnd); }else CDialog::OnSysCommand(nID, lParam); } */ const DWORD MaxDelay=150; class CWaterInterface : public CPixelBlock { bool FirstPass; // true only for the first Draw bool Finished; // true when water is completely still. bool Raining; bool Dripping; int Frame; // Which frame number we're Drawing/Saving DWORD Time; // Delay before collapse DWORD Bg; // Reflection Colour (Defaults to white) int dx; // Height Array Width int dy; // Height Array Height char* New; // New Height Array char* Old; // Old Height Array DWORD* Bmp; public: CWaterInterface(bool Raining=false, bool Dripping=false) : FirstPass(true), Raining(Raining), Dripping(Dripping), Bg(0xFFFFFF), Bmp(0), New(0), Old(0), dx(100), dy(100), Delay(0) {} virtual ~CWaterInterface() {try{DeleteHeightArray();}catch(...){}} void DeleteHeightArray() {delete[] New; delete[] Old; delete[] Bmp; New=Old=0; Bmp=0;} void Set(WORD width, WORD height) { if((width==Width)&&(height==Height)) return; Delay=0; // Start drawing again if stopped because of long Delay between frames DeleteHeightArray(); CPixelBlock::Set(width,height); } void SetRaining (bool Rain=true) {Raining =Rain;} void SetDripping(bool Drip=true) {Dripping=Drip;} bool IsRaining () const {return Raining ;} bool IsDripping() const {return Dripping;} bool IsInside(WORD x, WORD y) const {return (x>2) && (x2) && (yGetWindow())->RedrawWindow(0,0, RDW_INVALIDATE);} // Non-Windows Platforms will need to change this! void StartRaining() { SetRaining(true); Dip(1,1,1,1); Restart(); } void StopRaining() {SetRaining(false);} // Stop the Water button void StartDripping() { SetDripping(true); Dip(1,1,1,1); Restart(); } void StopDripping() {SetDripping(false);} // Stop the Water button void Resample() {FirstPass=true;} void Restart () {Finished=false;} void Draw(HDC hDC, DWORD Delay=0, bool SaveFiles=false) { if((Width==0)||(Height==0)) return; if(FirstPass) { FirstPass=false; Set(Width,Height); dx=(Width>>1)+2; dy=(Height>>1)+2; First(hDC, SaveFiles ? "t0000.bmp" : ""); if(Bmp==0) { delete[] New; New=new char[dx*dy]; if(New==0) return; delete[] Old; Old=new char[dx*dy]; if(Old==0) return; delete[] Bmp; Bmp=new DWORD[Width*Height]; if(Bmp==0) return; memset(New, 0, dx*dy*sizeof(char)); memset(Old, 0, dx*dy*sizeof(char)); } memcpy(Bmp, Bits, Width*Height*sizeof(DWORD)); if(Dripping) Old[((dx+1)*dy)>>1]=1; Time=GetTickCount()+Delay; Frame=1; Finished=false; }else if(!Bmp || !New || !Old) { CPixelBlock::Paint(hDC, 0,0); Redraw(hDC); return; }else if(Finished || (Time>GetTickCount())) { CPixelBlock::Paint(hDC, 0,0); if(!Finished) Redraw(hDC); }else{ if(!IsValid(Width-1,Height-1)) return; CString S; if(SaveFiles) S.Format("t%04i.bmp",Frame++); Next(hDC, S); } } protected: virtual bool InitDC(HDC hDC, WORD Width, WORD Height) {return false;} // Draw what you want on the DC and then return true; virtual void InitPixelBlock() {} // Draw what you want on the PixelBlock virtual void OnStart (HDC hDC, WORD Width, WORD Height) {} // Called when the animation starts virtual bool OnNext (HDC hDC, WORD Width, WORD Height) {return false;} // return true if you make any pixel non-zero; virtual void OnFinish(HDC hDC, WORD Width, WORD Height) {} // Called when the animation finishes void First(HDC hDC, CString Path) { if(InitDC(hDC, Width,Height)) CPixelBlock::Set(hDC); // Draw what you want on the DC then Copy DC to PixelBlock InitPixelBlock(); // Draw what you want on the PixelBlock OnStart(hDC, Width, Height); LastTime=GetTickCount(); CPixelBlock::Paint(hDC, 0,0, Path); Redraw(hDC); } DWORD LastTime; DWORD Delay; void Next(HDC hDC, CString Path) { if(Delay>MaxDelay) { // Don't bother drawing Water at all if the frame time is too long. char* Old1=Old; Old=New; New=Old1; // Swap Buffers CPixelBlock::Paint(hDC, 0,0, Path); DeleteHeightArray(); Redraw(hDC); return; } bool DoneOne=true; Delay=GetTickCount()-LastTime; if(Delay<15) Sleep(15-Delay); // Change the number to alter the frame rate. The number is milliseconds per frame. LastTime=GetTickCount(); DoneOne=OnNext(hDC, Width,Height); if(Raining && (rand()<0xFFF)) Drip(rand() | 0x80); // This loop creates the next frame of ripples to draw: char* Old1=Old; // Read 3 Rows at a time: char* Old2=Old1+dx; // The middle Row is the main one being read. char* Old3=Old2+dx; char* New1=New+dx+1; // Write to the other Buffer for(int y=dy-2; y>0; --y) { register char c11=*Old1++, c12=*Old1++; // We're reading a 3x3 square of Pixels... register char c21=*Old2++, c22=*Old2++; // then moving right a Pixel... register char c31=*Old3++, c32=*Old3++; // so we store two columns to use for the next iteration. for(int x=1; x>2)-*New1; // *New1 is the last value this Pixel had. This algorithm works simply because this happens to represent the velocity of the pixel we are now generating. if(c) { DoneOne=true; // remember that the Water is not flat yet. *New1++=c-(c>>5); // c is the new height at the current velocity, but we need to damp the wave >> can be 1 to 7 (with 7 being very little damping). }else *New1++=0; } New1+=2; // wrap round to point at the next row of pixels (we miss out the last and first columns because they are always 0. } if(!DoneOne && (Raining || Dripping)) { DoneOne=true; // Keep Dripping Drip(dx>>1, dy>>1, 1); } // This loop draws the background texture with the ripples: for(int Ty=Height-6; Ty>0; --Ty) { // Ignore the edge bits (they're always 0) for(int Tx=Width-2; Tx>0; --Tx) { int x=(Tx+1)>>1, y=(Ty+1)>>1; // Scale the Height Grid int xa=(New[ y *dx+x+1]-New[ y *dx+x-1])>>2; // get an idea of the angle of the water at this point... int ya=(New[(y+1)*dx+x ]-New[(y-1)*dx+x ])>>2; // by comparing with the nearest neighbours if((Tx+xa>0) && (Tx+xa0) && (Ty+ya>>>>>>>>>>>>>>>>>>>>>>>>>>>> Draw a Drip <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< void Drip(BYTE Depth) {Drip(rand()%Width+1, rand()%Height+1, Depth);} void Drip(WORD x, WORD y, BYTE Depth) { if(Delay>MaxDelay) return; // Don't bother drawing Water at all if the frame time is too long. x>>=1; y>>=1; // Scale to Height Array if(!IsInside(++x,++y)) return; //++ to ignore the edge bits (they're always 0) Old[y*dx+x]=Depth; } //>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Draw a line of ripples <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< void Swap(WORD& a, WORD& b) {WORD t=a; a=b; b=t;} public:// Based on CLineStencil WU Antialiased line: void Straight(WORD x0, WORD y0, WORD x1, WORD y1, BYTE Depth=8) { if(Delay>MaxDelay) return; // Don't bother drawing Water at all if the frame time is too long. x0>>=1; y0>>=1; x1>>=1; y1>>=1; // Scale to Height Array if(!IsInside(++x0,++y0) || !IsInside(++x1,++y1)) return; //++ to ignore the edge bits (they're always 0) int Dx,Dy, xDir; if(y0>y1) { Swap(y0,y1); Swap(x0,x1); } Dx=x1-x0; Dy=y1-y0; if(Dx>=0) xDir=1; else { xDir=-1; Dx=-Dx; } Old[x0+dx*y0]=Depth; // First and last Pixels always get Set: Old[x1+dx*y1]=Depth; if(Dx==0) { // vertical line if(Dy) for(char* ptr=Old+x0+dx*y0; Dy--; ptr+=dx) *ptr=Depth; return; } if(Dy==0) { // horizontal line if(x0>x1) {Swap(x0,x1);} for(char* ptr=Old+x0+dx*y0; Dx--; *ptr++=Depth); return; } if(Dx==Dy) { // diagonal line. for(char* ptr=Old+x0+dx*y0; Dy--; ptr+=dx+xDir) *ptr=Depth; return; } DWORD ErrorAcc=0; if(Dy>Dx) { // y-major line DWORD ErrorAdj=((DWORD)Dx<<16) / (DWORD)Dy; if(xDir<0) { while(--Dy) { ErrorAcc+=ErrorAdj; ++y0; x1=x0-(WORD)(ErrorAcc>>16); Old[x1 +dx*y0]=Depth; Old[x1-1+dx*y0]=Depth; } }else{ while(--Dy) { ErrorAcc+=ErrorAdj; ++y0; x1=x0+(WORD)(ErrorAcc>>16); Old[x1 +dx*y0]=Depth; Old[x1+xDir+dx*y0]=Depth; } } }else{ // x-major line DWORD ErrorAdj=((DWORD)Dy<<16) / (DWORD)Dx; while(--Dx) { ErrorAcc+=ErrorAdj; x0+=xDir; y1=y0+(WORD)(ErrorAcc>>16); Old[x0+dx*(y1 )]=Depth; Old[x0+dx*(y1+1)]=Depth; } } } //>>>>>>>>>>>>>>>>>>>>> Draw a circle of ripples <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< void Dip(WORD xCenter, WORD yCenter, WORD Radius, BYTE Depth) { // Hemisphere: if(Delay>MaxDelay) return; // Don't bother drawing Water at all if the frame time is too long. if(Radius==0) return; WORD Radius2=Radius*Radius; Drip(xCenter, yCenter, Depth); // Draw the Centre at full depth for(WORD y=Radius; y; --y) { for(WORD x=0; x<=y; ++x) { WORD r2=x*x+y*y; if(r2>=Radius2) continue; BYTE PixelDepth=BYTE((Depth*(Radius2-r2))/Radius2); if(PixelDepth==0) continue; Drip(xCenter+x, yCenter+y, PixelDepth); Drip(xCenter+x, yCenter-y, PixelDepth); if(x) { // don't overdraw vertical line Drip(xCenter-x, yCenter+y, PixelDepth); Drip(xCenter-x, yCenter-y, PixelDepth); } if(x==y) continue; // don't overdraw diagonal line Drip(xCenter+y, yCenter+x, PixelDepth); Drip(xCenter-y, yCenter+x, PixelDepth); if(x==0) continue; // don't overdraw horizontal line Drip(xCenter+y, yCenter-x, PixelDepth); Drip(xCenter-y, yCenter-x, PixelDepth); } } } }; #endif // ndef WaterInterfaceh