This chapter
is taken from book "Programming Windows Phone 7" by Charles Petzold published by
Microsoft press.
http://www.charlespetzold.com/phone/index.html
The primary means of user input to a Windows Phone 7 application is
touch. A Windows Phone 7 device has a screen that supports at least four
touch points, and applications must be written to accommodate touch in a
way that feels natural and intuitive to the user and the XNA programmers have two basic approaches to
obtaining touch input. With the low-level TouchPanel.GetState method a program can track individual fingers-each
identified by an ID number-as they first touch the screen, move, and
lift off. The TouchPanel.ReadGesture method provides a
somewhat higher-level interface that allows rudimentary handling of
inertia and two-finger manipulation in the form of "pinch" and "stretch"
gestures.
Gestures and Properties
The various gestures supported by the TouchPanel class correspond to members of the GestureType enumeration:
- Tap - quickly touch and lift
- DoubleTap - the second of two successive taps
- Hold - press and hold for one second
- FreeDrag - move finger around the screen
- HorizontalDrag - horizontal component of FreeDrag
- VerticalDrag - vertical component of FreeDrag
- DragComplete - finger lifted from screen
- Flick - single-finger swiping movement
- Pinch - two fingers moving towards each other
or apart
- PinchComplete - fingers lifted from screen
To receive information for particular gestures, the gestures must be
enabled by setting the
TouchPanel.EnabledGestures
property. The program then obtains gestures
during the Update
override of the
Game class in the form of GestureSample structures that include a GestureType property to identify the gesture.
GestureSample also defines four properties of type
Vector2.
None of these properties are valid for the DragComplete and
PinchComplete types. Otherwise:
- Position is valid for all gestures except
Flick.
- Delta is valid for all Drag gestures, Pinch, and Flick.
- Position2 and Delta2 are valid only for Pinch.
The Position property indicates the current position of the finger relative to
the screen. The Delta property indicates the movement of the finger since the
last position. For an object of type GestureSample named
gestureSample,
Vector2 previousPosition = gestureSample.Position - gestureSample.Delta;
The Delta
vector equals zero when the finger first touches the screen or
when the finger is still.
Suppose you're only interested in dragging operations, and you enable the
FreeDrag and
DragComplete gestures. If you need to keep track of
the complete distance a finger travels from the time it touches the screen to
time it lifts, you can use one of two strategies: Either save the Position value from the first occurrence of FreeDrag after a
DragComplete and compare that with the later Position values, or accumulate the Delta
values in a running total.
Let's look at a simple program that lets the user drag a little bitmap around
the screen. In the OneFingerDrag project the
Game1
class has fields to store a Texture2D and maintain its position:
public
class Game1 :
Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D texture;
Vector2 texturePosition =
Vector2.Zero;
public Game1()
{
graphics = new
GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
// Frame rate is 30 fps by default for
Windows Phone.
TargetElapsedTime = TimeSpan.FromTicks(333333);
TouchPanel.EnabledGestures =
GestureType.FreeDrag;
}
....
}
The LoadContent override loads the Texture2D.
protected
override void
LoadContent()
{
// Create a new SpriteBatch, which can be
used to draw textures.
spriteBatch = new
SpriteBatch(GraphicsDevice);
texture = this.Content.Load<Texture2D>("PetzoldTattoo");
}
The Update override handles the FreeDrag gesture simply by adjusting the
texturePosition vector by the Delta property of the GestureSample:
protected
override void
Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back
== ButtonState.Pressed)
this.Exit();
while (TouchPanel.IsGestureAvailable)
{
GestureSample gesture =
TouchPanel.ReadGesture();
if (gesture.GestureType ==
GestureType.FreeDrag)
texturePosition += gesture.Delta;
}
base.Update(gameTime);
}
Although texturePosition is a point and the Delta property of GestureSample
is a vector, they are both Vector2
values so they can be added.
The while
loop might seem a little pointless in this program because we're only interested
in a single gesture type. Couldn't it simply be an if statement? Actually, no.
It is my experience that multiple gestures of the same type can be available
during a single Update call.
The Draw
override simply draws the Texture2D at the updated position:
protected
override void
Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
spriteBatch.Draw(texture, texturePosition,
Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
Initially the Texture2D is parked at the upper-left corner of the screen but
by dragging your finger across the screen you can move it around:
Scale and Rotate
Let's continue examining dragging gestures involving a simple figure,
but using those gestures to implement scaling and rotation rather than
movement. For the next three programs I'll position the Texture2D
in the center of the screen, and it will remain in the
center except that you can scale it or rotate it with a single finger.
The OneFingerScale project has a couple more fields than the previous
program:
public
class Game1 :
Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D texture;
Vector2 screenCenter;
Vector2 textureCenter;
Vector2 textureScale =
Vector2.One;
public Game1()
{
graphics = new
GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
// Frame rate is 30 fps by default for
Windows Phone.
TargetElapsedTime = TimeSpan.FromTicks(333333);
TouchPanel.EnabledGestures =
GestureType.FreeDrag;
}
....
}
The program needs the center of the Texture2D because it uses a long version
of the Draw call to SpriteBatch to include an origin argument. As you'll recall,
the origin argument to Draw is the point in the Texture2D
that is aligned with the position argument, and which also
serves as the center of scaling and rotation.
Notice that the
textureScale field is set to the vector
(1, 1), which
means to multiply the width and height by 1. It's a common mistake to set
scaling to zero, which tends to make graphical objects disappear from the
screen.
All the uninitialized fields are set in the
LoadContent override:
protected
override void
LoadContent()
{
// Create a new SpriteBatch, which can be
used to draw textures.
spriteBatch = new
SpriteBatch(GraphicsDevice);
Viewport viewport =
this.GraphicsDevice.Viewport;
screenCenter = new
Vector2(viewport.Width / 2, viewport.Height /
2);
texture = this.Content.Load<Texture2D>("PetzoldTattoo");
textureCenter = new
Vector2(texture.Width / 2, texture.Height /
2);
}
The handling of the
FreeDrag gesture in the following Update
override doesn't attempt to determine if the
finger is over the bitmap. Because the bitmap is positioned in the center of the
screen and it will be scaled to various degrees, that calculation is a little
more difficult (although certainly not impossible.)
Instead, the
Update override shows how to use the Delta
property to determine the previous position of the finger, which is then used to
calculate how far the finger has moved from the center of the texture (which is
also the center of the screen) during this particular part of the entire
gesture:
protected
override void
Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back
== ButtonState.Pressed)
this.Exit();
while (TouchPanel.IsGestureAvailable)
{
GestureSample gesture =
TouchPanel.ReadGesture();
if (gesture.GestureType ==
GestureType.FreeDrag)
{
Vector2 prevPosition =
gesture.Position - gesture.Delta;
float scaleX = (gesture.Position.X
- screenCenter.X) /
(prevPosition.X - screenCenter.X);
float scaleY = (gesture.Position.Y
- screenCenter.Y) /
(prevPosition.Y - screenCenter.Y);
textureScale.X *= scaleX;
textureScale.Y *= scaleY;
}
}
base.Update(gameTime);
}
For example, the center of the screen is probably the point (400, 240).
Suppose during this particular part of the gesture, the Position property is
(600, 200) and the Delta property is
(20, 10). That means the previous position was (580, 190). In the horizontal
direction, the distance of the finger from the center of the texture increased
from 180 pixels (580 minus 400) to 200 pixels (600 minus 400) for a scaling
factor of 200 divided by 180 or 1.11. In the vertical direction, the distance
from the center decreased from 50 pixels (240 minus 190) to 40 pixels (240 minus
200) for a scaling factor of 40 divided by 80 or 0.80. The image increases in
size by 11% in the horizontal direction and decreases by 20% in the vertical.
Therefore, multiply the X component of the scaling
vector by 1.11 and the Y component by 0.80. As expected, that scaling factor
shows up in the Draw override:
protected
override void
Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
spriteBatch.Draw(texture, screenCenter,
null, Color.White, 0,
textureCenter, textureScale,
SpriteEffects.None, 0);
spriteBatch.End();
base.Draw(gameTime);
}
Probably the most rewarding way to play with this program is to "grab" the
image at one of the corners and move that corner roughly towards or away from
the center:
The Pinch Gesture
Generally you'll want to support both FreeDrag and Pinch
so the user can use one or two fingers. Then
you need to decide whether to restrict scaling to uniform or non-uniform
scaling, and whether rotation should be supported.
The Pinch
gesture, Update breaks down the data
into "old" points and "new" points. When two fingers are both moving relative to
each other, you can determine a composite scaling factor by treating the two
fingers separately. Assume the first finger is fixed in position and the other
is moving relative to it, and then the second finger is fixed in position and
the first finger is moving relative to it. Each represents a separate scaling
operation that you then multiply. In each case, you have a reference point (the
fixed finger) and an old point and a new point (the moving finger).
Let's create a program and call it DragPinchRotate:
namespace
DragPinchRotate
{
public class
Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D texture;
Matrix matrix =
Matrix.Identity;
public Game1()
{
graphics = new
GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
// Frame rate is 30 fps by default for
Windows Phone.
TargetElapsedTime = TimeSpan.FromTicks(333333);
TouchPanel.EnabledGestures =
GestureType.FreeDrag |
GestureType.Pinch;
}
protected
override void Initialize()
{
base.Initialize();
}
protected
override void LoadContent()
{
// Create a new SpriteBatch, which can be
used to draw textures.
spriteBatch = new
SpriteBatch(GraphicsDevice);
texture = this.Content.Load<Texture2D>("PetzoldTattoo");
}
protected
override void UnloadContent()
{
}
protected
override void Update(GameTime
gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back
== ButtonState.Pressed)
this.Exit();
while (TouchPanel.IsGestureAvailable)
{
GestureSample gesture =
TouchPanel.ReadGesture();
switch (gesture.GestureType)
{
case
GestureType.FreeDrag:
Vector2 newPoint = gesture.Position;
Vector2 oldPoint = newPoint - gesture.Delta;
Vector2 textureCenter = new
Vector2(texture.Width / 2, texture.Height /
2);
Vector2 refPoint =
Vector2.Transform(textureCenter, matrix);
matrix *= ComputeRotateAndTranslateMatrix(refPoint,
oldPoint, newPoint);
break;
case
GestureType.Pinch:
Vector2 oldPoint1 =
gesture.Position - gesture.Delta;
Vector2 newPoint1 = gesture.Position;
Vector2 oldPoint2 = gesture.Position2 - gesture.Delta2;
Vector2 newPoint2 = gesture.Position2;
matrix *= ComputeScaleAndRotateMatrix(oldPoint1,
oldPoint2, newPoint2);
matrix *= ComputeScaleAndRotateMatrix(newPoint2,
oldPoint1, newPoint1);
break;
}
}
base.Update(gameTime);
}
Matrix ComputeRotateAndTranslateMatrix(Vector2
refPoint, Vector2 oldPoint,
Vector2 newPoint)
{
Matrix matrix =
Matrix.Identity;
Vector2 delta = newPoint -
oldPoint;
Vector2 oldVector = oldPoint -
refPoint;
Vector2 newVector = newPoint -
refPoint;
// Avoid rotation if fingers are close to
center
if (newVector.Length() > 25
&& oldVector.Length() > 25)
{
// Find angles from center of bitmap
to touch points
float oldAngle = (float)Math.Atan2(oldVector.Y,
oldVector.X);
float newAngle = (float)Math.Atan2(newVector.Y,
newVector.X);
// Calculate rotation matrix
float angle = newAngle -
oldAngle;
matrix *= Matrix.CreateTranslation(-refPoint.X,
-refPoint.Y, 0);
matrix *= Matrix.CreateRotationZ(angle);
matrix *= Matrix.CreateTranslation(refPoint.X,
refPoint.Y, 0);
// Essentially rotate the old vector
oldVector = oldVector.Length() / newVector.Length() *
newVector;
// Re-calculate delta
delta = newVector - oldVector;
}
// Include translation
matrix *= Matrix.CreateTranslation(delta.X,
delta.Y, 0);
return matrix;
}
Matrix ComputeScaleAndRotateMatrix(Vector2
refPoint, Vector2 oldPoint,
Vector2 newPoint)
{
Matrix matrix =
Matrix.Identity;
Vector2 oldVector = oldPoint -
refPoint;
Vector2 newVector = newPoint -
refPoint;
// Find angles from reference point to
touch points
float oldAngle = (float)Math.Atan2(oldVector.Y,
oldVector.X);
float newAngle = (float)Math.Atan2(newVector.Y,
newVector.X);
// Calculate rotation matrix
float angle = newAngle -
oldAngle;
matrix *= Matrix.CreateTranslation(-refPoint.X,
-refPoint.Y, 0);
matrix *= Matrix.CreateRotationZ(angle);
matrix *= Matrix.CreateTranslation(refPoint.X,
refPoint.Y, 0);
// Essentially rotate the old vector
oldVector = oldVector.Length() / newVector.Length() *
newVector;
float scale = 1;
// Determine scaling from dominating delta
if (Math.Abs(newVector.X
- oldVector.X) > Math.Abs(newVector.Y -
oldVector.Y))
scale = newVector.X / oldVector.X;
else
scale = newVector.Y / oldVector.Y;
// Calculation scale matrix
if (!float.IsNaN(scale)
&& !float.IsInfinity(scale) && scale > 0)
{
scale = Math.Min(1.1f, Math.Max(0.9f,
scale));
matrix *= Matrix.CreateTranslation(-refPoint.X,
-refPoint.Y, 0);
matrix *= Matrix.CreateScale(scale,
scale, 1);
matrix *= Matrix.CreateTranslation(refPoint.X,
refPoint.Y, 0);
}
return matrix;
}
protected
override void Draw(GameTime
gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin(SpriteSortMode.Deferred,
null, null,
null, null,
null, matrix);
spriteBatch.Draw(texture, Vector2.Zero,
Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
And now you can perform one-finger translation and
rotation, and two-finger uniform scaling and rotation:
The Mandelbrot Set
In 1980, Benoît Mandelbrot (1924–2010), a Polish-born French and American
mathematician working for IBM, saw for the first time a graphic visualization of
a recursive equation involving complex numbers that had been investigated
earlier in the century. It looked something like this:
Since that time, the Mandelbrot Set (as it is called)
has become a favorite plaything of computer programmers.
The Mandelbrot Set is graphed on the complex plane,
where the horizontal axis represents real numbers (negative at the left and
positive at the right) and the vertical axis represents imaginary numbers
(negative at the bottom and positive at the top). Take any point in the plane
and call it c, and set z
equal to 0:
For some complex numbers (for example, the real number
0) it's very clear that the number belongs to the Mandelbrot Set. For others
(for example, the real number 1) it's very clear that it does not. For many
others, you just have to start cranking out the values. Fortunately, if the
absolute value of z
ever becomes greater than 2 after a finite number of
iterations, you know that c
does not belong to the Mandelbrot Set.
Each number c that does not belong to the Mandelbrot Set
has an associated "iteration" factor, which is the number of iterations
calculating z
that occur before the absolute value becomes greater than 2. Many people who
compute visualizations of the Mandelbrot Set use that iteration factor to select
a color for that point so that areas not in the Mandelbrot Set become rather
more interesting:
The text at the upper-left corner indicates the complex
coordinate associated with that corner, and similarly for the lower-right
corner. The number in the upper-right corner is a global iteration count.
One of the interesting characteristics of the Mandelbrot
Set is that no matter how much you zoom in, the complexity of the image does not
decrease:
That qualifies the Mandelbrot Set as a fractal, a branch
of mathematics that Benoît Mandelbrot pioneered. Considering the simplicity of
the algorithm that produces this image, the results are truly astonishing.
Here is the
PixelInfo structure used to store information for each pixel. The program
retains an array of these structures that parallels the normal pixels array used
for writing data to the Texture2D:
namespace
MandelbrotSet
{
public struct
PixelInfo
{
public static
int pixelWidth;
public static
int pixelHeight;
public static
double xPixelCoordAtComplexOrigin;
public static
double yPixelCoordAtComplexOrigin;
public static
double unitsPerPixel;
public static
bool hasNewColors;
public static
int firstNewIndex;
public static
int lastNewIndex;
public double
cReal;
public double
cImag;
public double
zReal;
public double
zImag;
public int
iteration;
public bool
finished;
public uint
packedColor;
public PixelInfo(int
pixelIndex, uint[] pixels)
{
int x = pixelIndex % pixelWidth;
int y = pixelIndex / pixelWidth;
cReal = (x - xPixelCoordAtComplexOrigin) * unitsPerPixel;
cImag = (yPixelCoordAtComplexOrigin - y) * unitsPerPixel;
zReal = 0;
zImag = 0;
iteration = 0;
finished = false;
packedColor = pixels != null ?
pixels[pixelIndex] : Color.Black.PackedValue;
}
public bool
Iterate()
{
double zImagSquared = zImag * zImag;
zImag = 2 * zReal * zImag + cImag;
zReal = zReal * zReal - zImagSquared + cReal;
if (zReal * zReal + zImag * zImag >=
4.0)
{
finished = true;
return
true;
}
iteration++;
return
false;
}
}
}
With all static fields of PixelInfo, I managed to keep the fields of the Game
derivative down to a reasonable number. You'll see the normal pixels array here
as well as the PixelInfo array. The pixelInfosLock object is used for thread
synchronization:
namespace
MandelbrotSet
{
public class
Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Viewport viewport;
Texture2D texture;
uint[] pixels;
PixelInfo[] pixelInfos;
Matrix drawMatrix =
Matrix.Identity;
int globalIteration = 0;
object pixelInfosLock =
new object();
SpriteFont segoe14;
StringBuilder upperLeftCoordText =
new StringBuilder();
StringBuilder lowerRightCoordText =
new StringBuilder();
StringBuilder upperRightStatusText =
new StringBuilder();
Vector2 lowerRightCoordPosition,
upperRightStatusPosition;
public Game1()
{
graphics = new
GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
// Frame rate is 30 fps by default for
Windows Phone.
TargetElapsedTime = TimeSpan.FromTicks(333333);
// Set full screen & enable gestures
graphics.IsFullScreen = true;
TouchPanel.EnabledGestures =
GestureType.FreeDrag |
GestureType.DragComplete |
GestureType.Pinch
| GestureType.PinchComplete;
}
protected
override void Initialize()
{
base.Initialize();
}
protected
override void LoadContent()
{
// Create a new SpriteBatch, which can be
used to draw textures.
spriteBatch = new
SpriteBatch(GraphicsDevice);
viewport = this.GraphicsDevice.Viewport;
segoe14 = this.Content.Load<SpriteFont>("Segoe14");
}
protected
override void OnActivated(object
sender, EventArgs args)
{
PhoneApplicationService
appService = PhoneApplicationService.Current;
if (appService.State.ContainsKey("xOrigin")
&&
appService.State.ContainsKey("yOrigin")
&&
appService.State.ContainsKey("resolution"))
{
PixelInfo.xPixelCoordAtComplexOrigin
= (double)appService.State["xOrigin"];
PixelInfo.yPixelCoordAtComplexOrigin
= (double)appService.State["yOrigin"];
PixelInfo.unitsPerPixel = (double)appService.State["resolution"];
}
else
{
// Program running from beginning
PixelInfo.xPixelCoordAtComplexOrigin
= 2 * viewport.Width / 3f;
PixelInfo.yPixelCoordAtComplexOrigin
= viewport.Height / 2;
PixelInfo.unitsPerPixel =
Math.Max(2.5 / viewport.Height,
3.0 / viewport.Width);
}
UpdateCoordinateText();
// Restore bitmap from tombstoning or
recreate it
texture = Texture2DExtensions.LoadFromPhoneServiceState(this.GraphicsDevice,
"mandelbrotBitmap");
if (texture ==
null)
texture = new
Texture2D(this.GraphicsDevice,
viewport.Width, viewport.Height);
// Get texture information and pixels
array
PixelInfo.pixelWidth =
texture.Width;
PixelInfo.pixelHeight =
texture.Height;
int numPixels =
PixelInfo.pixelWidth *
PixelInfo.pixelHeight;
pixels = new
uint[numPixels];
texture.GetData<uint>(pixels);
// Create and initialize PixelInfo array
pixelInfos = new
PixelInfo[numPixels];
InitializePixelInfo(pixels);
// Start up the calculation thread
Thread thread =
new Thread(PixelSetterThread);
thread.Start();
base.OnActivated(sender, args);
}
protected
override void OnDeactivated(object
sender, EventArgs args)
{
PhoneApplicationService.Current.State["xOrigin"]
= PixelInfo.xPixelCoordAtComplexOrigin;
PhoneApplicationService.Current.State["yOrigin"]
= PixelInfo.yPixelCoordAtComplexOrigin;
PhoneApplicationService.Current.State["resolution"]
= PixelInfo.unitsPerPixel;
texture.SaveToPhoneServiceState("mandelbrotBitmap");
base.OnDeactivated(sender, args);
}
void InitializePixelInfo(uint[]
pixels)
{
for (int
index = 0; index < pixelInfos.Length; index++)
{
pixelInfos[index] = new
PixelInfo(index, pixels);
}
PixelInfo.hasNewColors =
true;
PixelInfo.firstNewIndex = 0;
PixelInfo.lastNewIndex =
pixelInfos.Length - 1;
}
void PixelSetterThread()
{
int pixelIndex = 0;
while (true)
{
lock (pixelInfosLock)
{
if (!pixelInfos[pixelIndex].finished)
{
if (pixelInfos[pixelIndex].Iterate())
{
int iteration =
pixelInfos[pixelIndex].iteration;
pixelInfos[pixelIndex].packedColor =
GetPixelColor(iteration).PackedValue;
PixelInfo.hasNewColors
= true;
PixelInfo.firstNewIndex
= Math.Min(PixelInfo.firstNewIndex,
pixelIndex);
PixelInfo.lastNewIndex
= Math.Max(PixelInfo.lastNewIndex,
pixelIndex);
}
else
{
// Special case: On scale
up, prevent blocks of color from
// remaining
inside the Mandelbrot Set
if (pixelInfos[pixelIndex].iteration
== 500 &&
pixelInfos[pixelIndex].packedColor !=
Color.Black.PackedValue)
{
pixelInfos[pixelIndex].packedColor =
Color.Black.PackedValue;
PixelInfo.hasNewColors
= true;
PixelInfo.firstNewIndex
=
Math.Min(PixelInfo.firstNewIndex,
pixelIndex);
PixelInfo.lastNewIndex
=
Math.Max(PixelInfo.lastNewIndex,
pixelIndex);
}
}
}
if (++pixelIndex ==
pixelInfos.Length)
{
pixelIndex = 0;
globalIteration++;
}
}
}
}
Color GetPixelColor(int
iteration)
{
float proportion = (iteration / 32f)
% 1;
if (proportion < 0.5)
return
new Color(1 - 2 * proportion, 0, 2 *
proportion);
proportion = 2 * (proportion - 0.5f);
return new
Color(0, proportion, 1 - proportion);
}
protected
override void UnloadContent()
{
}
protected
override void Update(GameTime
gameTime)
{
......
}
PixelInfo[] TranslatePixelInfo(PixelInfo[]
srcPixelInfos, Matrix drawMatrix)
{
int x = (int)(drawMatrix.M41
+ 0.5);
int y = (int)(drawMatrix.M42
+ 0.5);
PixelInfo.xPixelCoordAtComplexOrigin
+= x;
PixelInfo.yPixelCoordAtComplexOrigin
+= y;
PixelInfo[] dstPixelInfos =
new PixelInfo[srcPixelInfos.Length];
for (int
dstY = 0; dstY < PixelInfo.pixelHeight; dstY++)
{
int srcY = dstY - y;
int srcRow = srcY *
PixelInfo.pixelWidth;
int dstRow = dstY *
PixelInfo.pixelWidth;
for (int
dstX = 0; dstX < PixelInfo.pixelWidth; dstX++)
{
int srcX = dstX - x;
int dstIndex = dstRow + dstX;
if (srcX >= 0 && srcX <
PixelInfo.pixelWidth &&
srcY >= 0 && srcY <
PixelInfo.pixelHeight)
{
int srcIndex = srcRow + srcX;
dstPixelInfos[dstIndex] = pixelInfos[srcIndex];
}
else
{
dstPixelInfos[dstIndex] = new
PixelInfo(dstIndex,
null);
}
}
}
return dstPixelInfos;
}
Matrix ComputeScaleMatrix(Vector2
refPoint, Vector2 oldPoint,
Vector2 newPoint,
bool
xDominates)
{
float scale = 1;
if (xDominates)
scale = (newPoint.X - refPoint.X) / (oldPoint.X - refPoint.X);
else
scale = (newPoint.Y - refPoint.Y) / (oldPoint.Y -
refPoint.Y);
if (float.IsNaN(scale)
|| float.IsInfinity(scale) || scale < 0)
{
return
Matrix.Identity;
}
scale = Math.Min(1.1f,
Math.Max(0.9f, scale));
Matrix matrix =
Matrix.CreateTranslation(-refPoint.X, -refPoint.Y,
0);
matrix *= Matrix.CreateScale(scale,
scale, 1);
matrix *= Matrix.CreateTranslation(refPoint.X,
refPoint.Y, 0);
return matrix;
}
uint[] ZoomPixels(uint[]
srcPixels, Matrix matrix)
{
Matrix invMatrix =
Matrix.Invert(matrix);
uint[] dstPixels =
new uint[srcPixels.Length];
for (int
dstY = 0; dstY < PixelInfo.pixelHeight; dstY++)
{
int dstRow = dstY *
PixelInfo.pixelWidth;
for (int
dstX = 0; dstX < PixelInfo.pixelWidth; dstX++)
{
int dstIndex = dstRow + dstX;
Vector2 dst =
new Vector2(dstX,
dstY);
Vector2 src =
Vector2.Transform(dst, invMatrix);
int srcX = (int)(src.X
+ 0.5f);
int srcY = (int)(src.Y
+ 0.5f);
if (srcX >= 0 && srcX <
PixelInfo.pixelWidth &&
srcY >= 0 && srcY <
PixelInfo.pixelHeight)
{
int srcIndex = srcY * PixelInfo.pixelWidth
+ srcX;
dstPixels[dstIndex] = srcPixels[srcIndex];
}
else
{
dstPixels[dstIndex] = Color.Black.PackedValue;
}
}
}
return dstPixels;
}
void UpdateCoordinateText()
{
.....
}
protected
override void Draw(GameTime
gameTime)
{
GraphicsDevice.Clear(Color.Black);
// Draw Mandelbrot Set image
spriteBatch.Begin(SpriteSortMode.Immediate,
null, null,
null, null,
null, drawMatrix);
spriteBatch.Draw(texture, Vector2.Zero,
null, Color.White,
0, Vector2.Zero,
1, SpriteEffects.None, 0);
spriteBatch.End();
// Draw coordinate and status text
spriteBatch.Begin();
spriteBatch.DrawString(segoe14, upperLeftCoordText,
Vector2.Zero,
Color.White);
spriteBatch.DrawString(segoe14, lowerRightCoordText,
lowerRightCoordPosition, Color.White);
spriteBatch.DrawString(segoe14, upperRightStatusText,
upperRightStatusPosition, Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
The most simple Mandelbrot programs I've seen set a maximum for the number of
iterations. (A pseudocode algorithm in the Wikipedia entry on the Mandelbrot Set
sets max_iteration to 1000.) The only place in my implementation where I had to
use an iteration maximum is right in here. As you'll see shortly, when you use a
pair of fingers to zoom in on the viewing area, the program needs to entirely
start from scratch with a new array of PixelInfo structures. But for
visualization purposes it expands the Texture2D to approximate the eventual
image. This expansion often results in some pixels in the Mandelbrot Set being
colored, and the algorithm I'm using here would never restore those pixels to
black. So, if the iteration count on a particular pixel reaches 500, and if the
pixel is not black, it's set to black. That pixel could very well later be set
to some other color, but that's not known at this point.