Creating and Drawing
|
This page shows the C++ code to create and display a Plasma.
I've had enough! I want to go home
Take me back to the CDib class
A Plasma is a particular graphical effect where the screen seems to consist of pulsating blobs.
To construct the Plasma, I use a 2-dimensional graph of colour values, where each value is derived from the sum of a number of sine waves.
There are two vertical sine waves and three horizontal sine waves. The sine waves have different frequencies and ranges, and the starting angle of each wave varies for each phase of the plasma.
The colour values are mapped onto a graduated palette. Values near the middle of the range map to black, near the bottom of the range map to white, and near the top of the range map to green.
I derived the Plasma algorithm and the palette from a demo program in an old issue of PC Format.
I set up the palette in the 'OnCreate' method. The colours are hard-coded into an array. (A more sophisticated approach would be to somehow incorporate and use a palette resource).
CDC * pDC ; |
The 'makePalette' method creates a palette based on a bitmap's colour table. The dummy bitmap is created just to contain the colour table with all the hard-coded colours included.
'masterPalette' is a CPalette object I set up as a class variable in the CSaverWnd class.
After the palette is created, it gets converted to an identity palette (if the screen is in 256-colour mode), and a DIB section is created.
There is no need for an identity palette if the screen is in true-colour or high-colour mode, because in these modes there is no system palette. Instead, RGB colours are directly presented to the video card, instead of palette indices.
if ( screenBpp == 8 ) { // convert to an identity palette if in 256-colour mode for ( i = 0 ; i < 256 ; i++ ) remap[i] = 0 ; osb.identityPalette ( &masterPalette , pDC ) ; for ( i = 0 ; i < 256 ; i++ ) remap[i] = masterPalette.GetNearestPaletteIndex(colourTable[i]); } else for ( i = 0 ; i < 256 ; i++ ) remap[i] = i ; // set up the DIB section osb, same size as the full window osb.makeDibSection ( screenWidth , screenHeight , 8 , pDC , &masterPalette , (screenBpp == 8) // palettize if in 256-colour mode ) ; osb.setBgColour ( RGB(0,0,0) , &masterPalette) ; osb.bgFill () ; ReaseDC(pDC) ; |
This code goes straight after the palette creation code, in 'OnCreate'.
Remember that 'palettize' is my name for the process of changing a bitmap to have a 1-to-1 colour table and having the pixels referring directly to a palette rather than the colour table.
'osb' is a CDib object I set up as a class variable.
frameRate = theApp.GetProfileInt("Config","Frame Rate", 10) ; maxPlasma = theApp.GetProfileInt("Config" , "Max Plasma Size" , 200 ) ; minPlasma = theApp.GetProfileInt("Config" , "Min Plasma Size" , 100 ); drift = (float)((int)theApp.GetProfileInt("Config" , "Drift Rate" , 2 ))/10.0 ; plasmaSpeed = (float)(theApp.GetProfileInt("Config" , "Plasma Speed" , 41 )) / 10.0 ; minPlasma = (minPlasma + 3 ) & ~3 ; maxPlasma = (maxPlasma + 3 ) & ~3 ; if ( minPlasma > maxPlasma ) { int save ; |
This code is also in 'OnCreate'. I am reading the configuration values from out of the registry.
The Plasma image is created in a rectangle of a specified size. The rectangle then gets tiled across the screen. If it is a big rectangle, there won't be many copies. If it is a small rectangle, there will be many copies on the screen,
'frameRate' is how fast the animation will go (frames per second).
'minPlasma' is the smallest rectangle size to use for creating the Plasma.
'maxPlasma' is the largest rectangle size to use for creating the Plasma.
The Plasma varies in size from frame to frame, ranging between the 'minPlasma' and 'maxPlasma' values in steps of 4 pixels.
'drift' is the amount by which the Plasma pattern should appear to move diagonally up or down the screen.
'plasmaSpeed' is how much the Plasma pattern should change from one frame to the next.
Note that the 'minPlasma' and 'maxPlasma' values are forced to be multiples of 4. This makes it easier when accessing the pixels (there is no byte-padding on the end of each pixel row).
Also note that you need access to the application object in order to use the 'GetProfileInt' API. So, at the top of the screen saver full-screen window code, you need :
extern CPlasmaApp theApp ; |
This links to the global application object that MFC App Wizard sets up for you in the automatically generated application source code.
I set up a new method to generate and draw the Plasma, called 'drawPlasma'. It relies on the starting angle for each of the five sine waves being saved from one frame to the next. I do this by setting them up as class variables.
Note that the C++ 'sin' function expects angles to be in radians rather than degrees. A full circle in radians is twice pi (6.284), rather than 360 degrees. This is why the code uses to 6.284 such a lot!
void CSaverWnd::drawPlasma ( CDC * pDC , int plasma_size) { int i , j , k ; float value ; int byteWidth ; int * xbuffer , * ybuffer ; xbuffer = (int*)malloc(plasma_size * sizeof(int)) ; ybuffer = (int*)malloc(plasma_size * sizeof(int)) ; angle1bak = angle1 ; angle2bak = angle2 ; angle3bak = angle3 ; // do horizontal graph for ( i = 0 ; i < plasma_size ; i++ ) { value = 32.0*sin(angle1) + 16.0*sin(angle2) + 8.0*sin(angle3) ; // 16,8,4 xbuffer[i] = (int)value ; // * 3)>>1; angle1 += 6.284/plasma_size; angle2 += 2*6.284/plasma_size; angle3 += 4*6.284/plasma_size ; } angle1 = angle1bak - ((1*6.284/320)*plasmaSpeed) ; // 1 angle2 = angle2bak + ((3*6.284/320)*plasmaSpeed) ; // 3 angle3 = angle3bak + ((2*6.284/320)*plasmaSpeed) ; // 2 angle4bak = angle4 ; angle5bak = angle5 ; for ( i = 0 ; i < plasma_size ; i++ ) { value = 32.0*sin(angle4) + 16.0*sin(angle5) ; ybuffer[i] = (int)value ; angle4 += 6.284/plasma_size; angle5 += 2*6.284/plasma_size ; } angle4 = angle1bak + ((3*6.284/320)*plasmaSpeed) ; // 3 angle5 = angle2bak + ((5*6.284/320)*plasmaSpeed) ; // 5 // fill in the plasma ::GdiFlush() ; byteWidth = ( osb.width + 3 ) & ~3 ; BYTE * pPix = osb.pPixels + (osb.height - plasma_size)*byteWidth ; for ( j = 0 ; j < plasma_size ; j++ ) { k = ybuffer[j] ; if ( screenBpp == 8 ) for ( i = 0 ; i < plasma_size ; i++ ) { *(pPix + i + byteWidth*j) = remap[117 + k + xbuffer[i]] ; } else for ( i = 0 ; i < plasma_size ; i++ ) { *(pPix + i + byteWidth*j) = 117 + k + xbuffer[i] ; } } free (xbuffer) ; free (ybuffer) ; // Copy plasma repeatedly in the osb, to tile it. int numAcross = int(osb.width / plasma_size) + 1 ; int numDown = int(osb.height / plasma_size) + 1 ; int vert , horz ; for ( vert = 0 ; vert < numDown ; vert++ ) for ( horz = 0 ; horz < numAcross ; horz++ ) { if ( vert > 0 || horz > 0 ) osb.solidMerge ( horz*plasma_size , vert*plasma_size , osb.pBmpInfoHdr , osb.pPixels , 0 , 0 , plasma_size , plasma_size , false ) ; } CRect fullRect ; fullRect.SetRect ( 0 , 0 , osb.width , osb.height ) ; ::GdiFlush() ; osb.draw ( fullRect , fullRect , pDC , NULL , PALOP_FOREGROUND , &masterPalette ) ; angle1 -= drift ; angle2 -= drift ; angle3 -= drift ; angle4 -= drift ; angle5 -= drift ; if ( angle1 < 0 ) angle1 += 6.284 ; else if ( angle1 > 6.283 ) angle1 -= 6.284 ; if ( angle2 < 0 ) angle2 += 6.284 ; else if ( angle2 > 6.283 ) angle2 -= 6.284 ; if ( angle3 < 0 ) angle3 += 6.284 ; else if ( angle3 > 6.283 ) angle3 -= 6.284 ; if ( angle4 < 0 ) angle4 += 6.284 ; else if ( angle4 > 6.283 ) angle4 -= 6.284 ; if ( angle5 < 0 ) angle5 += 6.284 ; else if ( angle5 > 6.283 ) angle5 -= 6.284 ; } |
The 'OnTimer()' method is where most of the action happens. Each time it is called, the next Plasma frame is generated and displayed.
void CSaverWnd::OnTimer(UINT nIDEvent) { // fill in osb with next frame of plasma if ( minPlasma == maxPlasma ) cycleNum = maxPlasma ; else if ( cycleUp ) { if ( cycleNum >= maxPlasma || cycleNum >= screenWidth ) { cycleUp = false ; cycleNum -= 4 ; } else cycleNum += 4 ; } else { if ( cycleNum <= minPlasma ) { cycleUp = true ; cycleNum += 4 ; } else cycleNum -=4 ; } CDC * pDC = GetDC() ; drawPlasma ( pDC , cycleNum ) ; ReleaseDC (pDC) ; CWnd::OnTimer(nIDEvent); } |
Note that 'cycleNum' contains the size of the Plasma rectangle. This changes from one frame to the next (unless 'minPlasma' and 'maxPlasma' are the same). The 'cycleUp' variable indicates if the size is currently increasing or decreasing.
I put a call to 'drawPlasma' in the 'OnEraseBkgnd' method. This gets called whenever the screen saver starts up. Without the 'drawPlasma' call, the screen would be set to black by 'OnEraseBkgnd', and you would have to wait until the first timer tick before the plasma showed up. This would be visible as a slight flicker.
So, in 'OnEraseBkgnd', insert this code :
drawPlasma ( pDC , cycleNum ) ; |
There are occasions where the screen needs to be repainted, for example after the 'Enter Password' dialog box has obscured part of the screen or changed the palette.
Repainting the screen is easy to do when the screen image is stored in a CDib object. All you need to do is use the 'draw' method on the object.
My repainting code is :
if ( i > 0 ) { // redraw the screen, so that colours are mapped properly CRect fullRect ; fullRect.SetRect ( 0 , 0 , osb.width , osb.height ) ; osb.draw ( fullRect , fullRect , pDC , NULL , PALOP_FOREGROUND , &masterPalette ) ; } |
This code goes straight after the 'RealizePalette()' call.
OnPaint
CPaintDC dc(this); // device context for painting CRect paintRect ( &dc.m_ps.rcPaint ) ; osb.draw ( paintRect , paintRect , &dc , NULL , PALOP_FOREGROUND , &masterPalette ) ; |
Note that we only need to repaint the obscured area (the "invalid rectangle").
The code for the Plasma preview window is very similar to the full-screen window, except on a smaller scale. Because it is so similar, I won't reproduce it here.
You can download all the Plasma source code here. This includes the MSVC project files.