/******************************************************************************\
*
* MODULE: PAINT.C
*
* PURPOSE: This is the module responsible for painting the SPINCUBE
* custom control. When Paint() is called we retrieve a
* pointer to a SPINCUBEINFO structure, and then use it's
* current rotation & translation values to transform the
* polyhedron described by gNormalizedVertices & gaiFacets.
* Once we've transformed the vertices, we draw the
* background, which consists of a grey rectangle and a few
* black lines (a crass attempt to render a perspective
* view into a "room"), on the offscreen bitmap associated
* with the control (i.e. pSCI->hbmCompat). Then we walk the
* facet list of the transformed polyhedron (gXformedVertices
* & gaiFacets), drawing only those facets whose outward
* normal faces us (again, drawing on pSCI->hbmCompat).
* Finally, we BitBlt the appropriate rectangle from our
* offscreen bitmap to the screen itself.
*
* Drawing to the offscreen bitmap has two advantages over
* drawing straight to the screen:
*
* 1. The actual drawing the user sees consists of only
* a single BitBlt. Otherwise, the user would see us
* both erase the polyhedron in it's old position and
* draw it in it's new position (alot of flashing- not
* very smooth animation).
*
* 2. When a spincube control with the SS_ERASE style
* is brought to the foreground, all it's contents
* i.e. the cube trails) are saved & can be re-Blted
* to the screen. Otherwise, all this info would be
* lost & there'd be a big blank spot in the middle
* of the control!
*
* Interested persons should consult a text on 3 dimensional
* graphics for more information (i.e. "Computer Graphics:
* Principles and Practice", by Foley & van Dam).
*
* Notes:
*
* - A 3x2 tranformation matrix is used instead of a 3x3
* matrix, since the transformed z-values aren't needed.
* (Normally these would be required for use in depth
* sorting [for hidden surface removal], but since we
* draw only a single convex polyhedron this is not
* necessary.)
*
* - A simplified perspective viewing transformation
* (which also precludes the need for the transformed z
* coordinates). In a nutshell, the perspective scale
* is as follows:
*
* p' = S x p
* per
*
* where:
* S = WindowDepth /
* per (WindowDepth + fCurrentZTranslation)
*
* (WindowDepth is the greater of the control's window
* height or window width.)
*
*
* FUNCTIONS: Paint() - the paint routine
* TransformVertices() - transforms vertices
* ComputeRotationTransformation() - computes xformation
* based on current x, y
* and z rotation angles
*
*
* Microsoft Developer Support
* Copyright 1992 - 1998 Microsoft Corporation
*
\******************************************************************************/
#include <windows.h>
#include <math.h>
#include <stdlib.h>
#include "spincube.h"
#include "paint.h"
/******************************************************************************\
*
* FUNCTION: Paint
*
* INPUTS: hwnd - Handle of the window to draw into.
*
* COMMENTS: Draws window background & a polyhedron in the window.
*
\******************************************************************************/
void Paint (HWND hwnd)
{
PSPINCUBEINFO pSCI;
RECT rect;
int i;
LONG lScaleFactor;
PAINTSTRUCT ps;
HRGN hrgnClip;
HBRUSH hBrush, hBrushSave;
int iX, iY, iCX, iCY;
int facetIndex, numPoints;
POINT polygn[MAX_POINTS_PER_FACET];
POINT vector1, vector2;
COLORREF acrColor[6] = { 0x0000ff, 0x00ff00, 0xff0000,
0x00ffff, 0xff00ff, 0xffff00 };
pSCI = (PSPINCUBEINFO) GetWindowLong (hwnd, GWL_SPINCUBEDATA);
BeginPaint (hwnd, &ps);
if (memcmp((void *)&ps.rcPaint, (void *)&pSCI->rcCubeBoundary, sizeof(RECT))
& !REPAINT_BKGND(pSCI))
{
//
// We're not here because it's time to animate (i.e. this paint isn't
// the result of a WM_TIMER), so just do the Blt & blow out of here...
//
BitBlt (ps.hdc,
ps.rcPaint.left,
ps.rcPaint.top,
ps.rcPaint.right - ps.rcPaint.left,
ps.rcPaint.bottom - ps.rcPaint.top,
pSCI->hdcCompat, ps.rcPaint.left,
ps.rcPaint.top, SRCCOPY);
EndPaint (hwnd, &ps);
return;
}
//
// The rectangle we get back is in Desktop coordinates, so we need to
// modify it to reflect coordinates relative to this window.
//
GetWindowRect (hwnd, &rect);
rect.right -= rect.left;
rect.bottom -= rect.top;
rect.left = rect.top = 0;
//
// Determine a "best fit" scale factor for our polyhedron
//
if (!(lScaleFactor = rect.right > rect.bottom ?
rect.bottom/12 : rect.right/12))
lScaleFactor = 1;
TransformVertices (hwnd, &rect, pSCI, lScaleFactor);
//
// Draw the window frame & background
//
// Note: The chances are that we are coming through here because we
// got a WM_TIMER message & it's time to redraw the cube to simulate
// animation. In that case all we want to erase/redraw is that small
// rectangle which bounded the polyhedron the last time. The less
// drawing that actually gets done the better, since we wnat to
// minimize the flicker on the screen. __BeginPaint__ is perfect for
// this because it causes all drawing outside of the invalid region
// to be "clipped" (no drawing is performed outside of the invalid
// region), and it also validates the invalid region.
//
if (DO_ERASE(hwnd) || REPAINT_BKGND(pSCI))
{
hrgnClip = CreateRectRgnIndirect (&ps.rcPaint);
SelectClipRgn (pSCI->hdcCompat, hrgnClip);
DeleteObject (hrgnClip);
SelectObject (pSCI->hdcCompat, GetStockObject (GRAY_BRUSH));
Rectangle (pSCI->hdcCompat, (int)rect.left, (int)rect.top,
(int)rect.right, (int)rect.bottom);
iX = (rect.right - rect.left) / 4;
iY = (rect.bottom - rect.top ) / 4;
MoveToEx (pSCI->hdcCompat, (int)rect.left, (int)rect.top, NULL);
LineTo (pSCI->hdcCompat, (int)rect.left + iX, (int)rect.top + iY);
LineTo (pSCI->hdcCompat, (int)rect.left + iX, (int)rect.bottom - iY);
LineTo (pSCI->hdcCompat, (int)rect.left, (int)rect.bottom);
MoveToEx (pSCI->hdcCompat, (int)rect.right, (int)rect.top, NULL);
LineTo (pSCI->hdcCompat, (int)rect.right - iX, (int)rect.top + iY);
LineTo (pSCI->hdcCompat, (int)rect.right - iX, (int)rect.bottom- iY);
LineTo (pSCI->hdcCompat, (int)rect.right, (int)rect.bottom);
MoveToEx (pSCI->hdcCompat, (int)rect.left + iX, (int)rect.top + iY, NULL);
LineTo (pSCI->hdcCompat, (int)rect.right - iX, (int)rect.top + iY);
MoveToEx (pSCI->hdcCompat, (int)rect.left + iX, (int)rect.bottom - iY, NULL);
LineTo (pSCI->hdcCompat, (int)rect.right - iX, (int)rect.bottom - iY);
SelectClipRgn (pSCI->hdcCompat, NULL);
pSCI->iOptions &= ~SPINCUBE_REPAINT_BKGND;
}
//
// Draw the polyhedron. We'll walk through the facets list and compute
// the normal for each facet- if the normal has z > 0, then the facet
// faces us and we'll draw it. Note that this algorithim is ONLY valid
// for scenes with a single, convex polyhedron.
//
// Note: Use GetDC here because the above call to BeginPaint will
// probably not give us a DC with access to as much real estate as
// we'd like (we wouldn't be able to draw outside of the invalid
// region). We can party on the entire control window with the DC
// returned by GetDC.
//
for (i = 0, facetIndex = 0; i < NUMFACETS; i++)
{
vector1.x = gXformedVertices[gaiFacets[facetIndex + 1]].x -
gXformedVertices[gaiFacets[facetIndex]].x;
vector1.y = gXformedVertices[gaiFacets[facetIndex + 1]].y -
gXformedVertices[gaiFacets[facetIndex]].y;
vector2.x = gXformedVertices[gaiFacets[facetIndex + 2]].x -
gXformedVertices[gaiFacets[facetIndex + 1]].x;
vector2.y = gXformedVertices[gaiFacets[facetIndex + 2]].y -
gXformedVertices[gaiFacets[facetIndex + 1]].y;
for (numPoints = 0; gaiFacets[facetIndex] != -1; numPoints++, facetIndex++)
{
polygn[numPoints].x = gXformedVertices[gaiFacets[facetIndex]].x;
polygn[numPoints].y = gXformedVertices[gaiFacets[facetIndex]].y;
}
facetIndex++; /* skip over the -1's in the facets list */
if ((vector1.x*vector2.y - vector1.y*vector2.x) > 0)
{
hBrush = CreateSolidBrush (acrColor[i]);
hBrushSave = (HBRUSH) SelectObject (pSCI->hdcCompat, hBrush);
Polygon (pSCI->hdcCompat, &polygn[0], numPoints);
SelectObject (pSCI->hdcCompat, hBrushSave);
DeleteObject (hBrush);
}
}
iX = pSCI->rcCubeBoundary.left < ps.rcPaint.left ?
pSCI->rcCubeBoundary.left : ps.rcPaint.left;
iY = pSCI->rcCubeBoundary.top < ps.rcPaint.top ?
pSCI->rcCubeBoundary.top : ps.rcPaint.top;
iCX = (pSCI->rcCubeBoundary.right > ps.rcPaint.right ?
pSCI->rcCubeBoundary.right : ps.rcPaint.right) - iX;
iCY = (pSCI->rcCubeBoundary.bottom > ps.rcPaint.bottom ?
pSCI->rcCubeBoundary.bottom : ps.rcPaint.bottom) - iY;
EndPaint (hwnd, &ps);
ps.hdc = GetDC (hwnd);
BitBlt (ps.hdc, iX, iY, iCX, iCY, pSCI->hdcCompat, iX, iY, SRCCOPY);
ReleaseDC (hwnd, ps.hdc);
}
/******************************************************************************\
*
* FUNCTION: TransformVertices
*
* INPUTS: hwnd - control window handle
* pWindowRect - pointer to RECT describing control's dimensions
* pSCI - pointer to control's SPINCUBEINFO structure
* fScaleFactor - scale factor for use in this window
*
\******************************************************************************/
void TransformVertices (HWND hwnd, RECT *pWindowRect,
PSPINCUBEINFO pSCI, LONG lScaleFactor)
{
int i;
int iWindowDepth = pWindowRect->right > pWindowRect->bottom ?
pWindowRect->right : pWindowRect->bottom;
RECT WindowRect;
float fDepthScale;
int iNewTranslationInc = (rand() % 10) + 2;
float fNewRotationInc = (float) 0.01 * ((rand() % 30) + 2);
WindowRect.left = - (WindowRect.right = pWindowRect->right >> 1);
WindowRect.top = - (WindowRect.bottom = pWindowRect->bottom >> 1);
//
// Initiailize the bounding rectangle with max/min vals
//
pSCI->rcCubeBoundary.left =
pSCI->rcCubeBoundary.top = 100000; // big positive value
pSCI->rcCubeBoundary.right =
pSCI->rcCubeBoundary.bottom = -100000; // small negative value
//
// First scale, then rotate, then translate each vertex.
// Keep track of the maximum & minimum values bounding the
// vertices in the x,y plane for use later in bounds checking.
//
// Note: we don't bother computing z values after the scale,
// as they are only really necessary for the rotation. If we
// were doing real bounds checking we'd need it, but this code
// simply uses the pSCI->iCurrentZTranslation to determine
// the z-boundaries.
//
for (i = 0; i < NUMVERTICES; i++)
{
LONG tempX;
//
// Copy the static vertices into a temp array
//
gXformedVertices[i] = gNormalizedVertices[i];
//
// The scale...
//
gXformedVertices[i].x *= lScaleFactor;
gXformedVertices[i].y *= lScaleFactor;
gXformedVertices[i].z *= lScaleFactor;
//
// The rotation...
//
ComputeRotationTransformation (pSCI->fCurrentXRotation,
pSCI->fCurrentYRotation,
pSCI->fCurrentZRotation);
tempX = (LONG) (gM[0][0] * gXformedVertices[i].x +
gM[0][1] * gXformedVertices[i].y +
gM[0][2] * gXformedVertices[i].z);
gXformedVertices[i].y = (LONG) (gM[1][0] * gXformedVertices[i].x +
gM[1][1] * gXformedVertices[i].y +
gM[1][2] * gXformedVertices[i].z);
gXformedVertices[i].x = tempX;
//
// The translation...
//
gXformedVertices[i].x += pSCI->iCurrentXTranslation;
gXformedVertices[i].y += pSCI->iCurrentYTranslation;
//
// Check if we have new max or min vals
//
if (pSCI->rcCubeBoundary.left > gXformedVertices[i].x)
pSCI->rcCubeBoundary.left = gXformedVertices[i].x;
if (pSCI->rcCubeBoundary.right < gXformedVertices[i].x)
pSCI->rcCubeBoundary.right = gXformedVertices[i].x;
if (pSCI->rcCubeBoundary.top > gXformedVertices[i].y)
pSCI->rcCubeBoundary.top = gXformedVertices[i].y;
if (pSCI->rcCubeBoundary.bottom < gXformedVertices[i].y)
pSCI->rcCubeBoundary.bottom = gXformedVertices[i].y;
}
//
// Now for some bounds checking, change translation & rotation increments
// if we hit a "wall". Also so the gbHitBoundary flag so we remember
// to flash the cube when we draw it.
//
if (pSCI->rcCubeBoundary.left < WindowRect.left)
{
pSCI->iCurrentXTranslationInc = iNewTranslationInc;
pSCI->fCurrentZRotationInc = fNewRotationInc;
}
else if (pSCI->rcCubeBoundary.right > WindowRect.right)
{
pSCI->iCurrentXTranslationInc = -iNewTranslationInc;
pSCI->fCurrentZRotationInc = -fNewRotationInc;
}
if (pSCI->rcCubeBoundary.top < WindowRect.top)
{
pSCI->iCurrentYTranslationInc = iNewTranslationInc;
pSCI->fCurrentXRotationInc = fNewRotationInc;
}
else if (pSCI->rcCubeBoundary.bottom > WindowRect.bottom)
{
pSCI->iCurrentYTranslationInc = -iNewTranslationInc;
pSCI->fCurrentXRotationInc = -fNewRotationInc;
}
if (pSCI->iCurrentZTranslation < (int) lScaleFactor<<1)
{
pSCI->iCurrentZTranslationInc = iNewTranslationInc;
pSCI->fCurrentYRotationInc = fNewRotationInc;
}
else if (pSCI->iCurrentZTranslation > (iWindowDepth - (int) lScaleFactor))
{
pSCI->iCurrentZTranslationInc = -iNewTranslationInc;
pSCI->fCurrentYRotationInc = -fNewRotationInc;
}
//
// Now a kludgy scale based on depth (iCurrentZTranslation) of the center
// point of the polyhedron
//
fDepthScale = ((float) iWindowDepth) /
((float) (iWindowDepth + pSCI->iCurrentZTranslation));
pSCI->rcCubeBoundary.left = (LONG)(fDepthScale* pSCI->rcCubeBoundary.left );
pSCI->rcCubeBoundary.right = (LONG)(fDepthScale* pSCI->rcCubeBoundary.right );
pSCI->rcCubeBoundary.top = (LONG)(fDepthScale* pSCI->rcCubeBoundary.top );
pSCI->rcCubeBoundary.bottom= (LONG)(fDepthScale* pSCI->rcCubeBoundary.bottom);
for (i = 0; i < NUMVERTICES; i++)
{
gXformedVertices[i].x = (LONG) (fDepthScale * gXformedVertices[i].x);
gXformedVertices[i].y = (LONG) (fDepthScale * gXformedVertices[i].y);
}
//
// If currently in motion then increment the current rotation & tranlation
//
if (IN_MOTION(hwnd))
{
pSCI->fCurrentXRotation += pSCI->fCurrentXRotationInc;
pSCI->fCurrentYRotation += pSCI->fCurrentYRotationInc;
pSCI->fCurrentZRotation += pSCI->fCurrentZRotationInc;
pSCI->iCurrentXTranslation += pSCI->iCurrentXTranslationInc;
pSCI->iCurrentYTranslation += pSCI->iCurrentYTranslationInc;
pSCI->iCurrentZTranslation += pSCI->iCurrentZTranslationInc;
}
//
// Up to this point all coordinates are relative to a window whose
// center is at (0,0). Now we'll translate appropriately...
//
pSCI->rcCubeBoundary.left += pWindowRect->right >> 1;
pSCI->rcCubeBoundary.right += pWindowRect->right >> 1;
pSCI->rcCubeBoundary.top += pWindowRect->bottom >> 1;
pSCI->rcCubeBoundary.bottom += pWindowRect->bottom >> 1;
for (i = 0; i < NUMVERTICES; i++)
{
gXformedVertices[i].x += pWindowRect->right >> 1;
gXformedVertices[i].y += pWindowRect->bottom >> 1;
}
//
// Since FillRect's are inclusive-exclusive (there'll be leftovers
// from the last cube we drew otherwise)...
//
pSCI->rcCubeBoundary.right++;
pSCI->rcCubeBoundary.bottom++;
//
// Finally, adjust the rcCubeBoundary such that it fits entirely within
// the acutal control window. The reason for this is that when calling
// InvalidateRect from SpincubeWndProc\case_WM_TIMER we may get
// a different PAINSTRUCT.rcPaint (since InvalidateRect clips the passed
// in rect to the window bounds) and our abouve test to memcmp() will
// fail.
//
if (pSCI->rcCubeBoundary.left < 0)
pSCI->rcCubeBoundary.left = 0;
if (pSCI->rcCubeBoundary.top < 0)
pSCI->rcCubeBoundary.top = 0;
if (pSCI->rcCubeBoundary.right > pWindowRect->right)
pSCI->rcCubeBoundary.right = pWindowRect->right;
if (pSCI->rcCubeBoundary.bottom > pWindowRect->bottom)
pSCI->rcCubeBoundary.bottom = pWindowRect->bottom;
}
/******************************************************************************\
*
* FUNCTION: ComputeRotationTransformation
*
* INPUTS: fRotationX - Angle to rotate about X axis.
* fRotationY - Angle to rotate about Y axis.
* fRotationZ - Angle to rotate about Z axis.
*
* COMMENTS: Computes a 3x2 tranformation matrix which rotates about
* the Z axis, the Y axis, and the X axis, respectively.
*
\******************************************************************************/
void ComputeRotationTransformation (float fRotationX,
float fRotationY,
float fRotationZ)
{
float sinX, cosX, sinY, cosY, sinZ, cosZ;
sinX = (float) sin ((double) fRotationX);
cosX = (float) cos ((double) fRotationX);
sinY = (float) sin ((double) fRotationY);
cosY = (float) cos ((double) fRotationY);
sinZ = (float) sin ((double) fRotationZ);
cosZ = (float) cos ((double) fRotationZ);
gM[0][0] = cosY*cosZ;
gM[0][1] = -cosY*sinZ;
gM[0][2] = sinY;
gM[1][0] = sinX*sinY*cosZ + cosX*sinZ;
gM[1][1] = -sinX*sinY*sinZ + cosX*cosZ;
gM[1][2] = -sinX*cosY;
}