Learn what is Gestures to Transforms in Windows Phone 7


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:

211.gif

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:

212.gif

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:

213.gif

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:

214.gif

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:

215.gif

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:

216.gif

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.


Similar Articles