Click here to Skip to main content
15,891,423 members
Articles / Desktop Programming / Windows Forms

A Panel control that you can flip over

Rate me:
Please Sign up or sign in to vote.
3.84/5 (9 votes)
20 Mar 2007BSD4 min read 74.5K   2.5K   43  
This article shows how to create a 3D style effect using only GDI+ image manipulations
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Data;
using System.Text;
using System.Windows.Forms;

namespace wdi.ui
{
    /// <summary>
    /// WOW!  This is a Panel control that you can put things on the front of as well as the back
    /// of.  Then, you can call it's Flip() method to make it flip over and show what's on the
    /// other side.
    /// </summary>
    public partial class FlipPanel : Panel
    {
        private Bitmap m_PageA;
        private Bitmap m_PageB;
        private int m_X1;
        private int m_X2;
        private float m_Y1;
        private float m_Y2;
        private int m_AngleCounter = 0;
        private int m_XIncrement = 10;

        /// <summary>
        /// The interval between animation ticks in milliseconds
        /// </summary>
        public int TimerInterval
        {
            get { return timer1.Interval; }
            set { timer1.Interval = value; }
        }

        private Control m_Front;
        /// <summary>
        /// The control to put on the front of the container.  This will always be the front, even
        /// when the back control is in the front.
        /// </summary>
        public Control Front
        {
            get { return m_Front; }
            set { this.Controls.Add(value); m_Front = value; }
        }

        private Control m_Back;
        /// <summary>
        /// The control to put on the back of the container.  This will always be called the back
        /// even when it's on the front
        /// </summary>
        public Control Back
        {
            get { return m_Back; }
            set { this.Controls.Add(value);  m_Back = value; }
        }

        private bool m_DoShading;
        /// <summary>
        /// Set this to true if you want the control to calculate shading during the flip animation.
        /// I can't really tell if this actually does anything visually.
        /// </summary>
        public bool DoShading
        {
            get { return m_DoShading; }
            set { m_DoShading = value; }
        }


        /// <summary>
        /// Default constructor.
        /// </summary>
        public FlipPanel()
        {
            InitializeComponent();

            this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint, true);
            this.UpdateStyles();        
        }        

        /// <summary>
        /// Call this to initiate a flip action on the panel
        /// </summary>
        public void Flip()
        {
            this.StartFlip();
            timer1.Enabled = true;
        }

        /// <summary>
        /// For debugging only.  Or, I guess if you want to step through the flip animation...
        /// </summary>
        public void DebugStart()
        {
            this.StartFlip();
        }

        /// <summary>
        /// For debugging only.  Simulates an animation timer tick.
        /// </summary>
        public void DebugStep()
        {
            this.Invalidate();
            this.m_AngleCounter++;
        }

        /// <summary>
        /// This is what starts/resets the variables and timer used to do a flip.
        /// </summary>
        private void StartFlip()
        {
            this.m_X1 = 0;
            this.m_X2 = this.Width;
            this.m_Y1 = 0;
            this.m_Y2 = this.Height;

            m_PageA = new Bitmap(this.Width, this.Height);
            m_Front.DrawToBitmap(m_PageA, new Rectangle(m_Front.Location, m_Front.Size));

            m_PageB = new Bitmap(this.Width, this.Height);
            m_Back.DrawToBitmap(m_PageB, new Rectangle(m_Back.Location, m_Back.Size));
            m_PageB.RotateFlip(RotateFlipType.RotateNoneFlipX);

            // hide the controls while drawing
            this.m_Front.Visible = false;
            this.m_Back.Visible = false;
        }

        /// <summary>
        /// Reset some variables and junk.
        /// </summary>
        private void FlipDone()
        {
            timer1.Enabled = false;
            // Swap front and back controls
            Control temp = m_Front;
            this.m_Front = this.m_Back;
            this.m_Back = temp;
            // Reset the temporary variables
            m_X1 = 0;
            m_X2 = this.Width;
            this.m_Y1 = 0;
            m_AngleCounter = 0;
            // Only show the control that's in front
            this.m_Front.Visible = true;
            this.m_Back.Visible = false;            
        }

        /// <summary>
        /// This is an owner-drawn control, so this is where I draw stuff.
        /// </summary>
        /// <param name="e"></param>
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            if (m_PageA == null || m_PageB == null)
                return;

            e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;                        

            PointF[] destPoints = { new PointF(m_X1, 0), new PointF(m_X2, m_Y1), new PointF(m_X1, this.Height) };

            // this is the current angle of the panel during a flip
            float ang = (float)(m_AngleCounter * (float)(180f / this.Width));

            if (m_X1 < this.Width / 2)
            {
                if(m_DoShading) e.Graphics.DrawImage(ShadeImage(m_PageA, ang), destPoints);
                else e.Graphics.DrawImage(m_PageA, destPoints);
                // this is what creates the perspective effect
                m_Y1 += 0.5f;
            }
            else
            {
                if(m_DoShading) e.Graphics.DrawImage(ShadeImage(m_PageB, ang), destPoints);
                else e.Graphics.DrawImage(m_PageB, destPoints);
                m_Y1 -= 0.5f;
            }

            m_X1 += m_XIncrement;
            m_X2 -= m_XIncrement;
            
            if (m_X2 < -1)
            {                
                FlipDone();                
            }           
        }

        /// <summary>
        /// Here's the exciting part!  This applies sort of a linear gradient over the half of
        /// the image that's moving away from the screen.  See my article for more details if
        /// you're interested.
        /// </summary>
        /// <param name="b">The current bitmap that's being drawn</param>
        /// <param name="angle">The current angle of the flip</param>
        /// <returns>A shaded bitmap, ready to draw</returns>
        private Bitmap ShadeImage(Bitmap b, float angle)
        {            
            Bitmap rval = new Bitmap(b);
            Graphics g = Graphics.FromImage(rval);
            g.DrawImage(b, 0, 0);

            // Convert from radians to degrees
            float ang = angle * (float)(Math.PI / 180);
            float tan = (float)(Math.Tan(ang));
            
            // Start at the middle of the image.  I don't want to shade the part that's moving
            // towards the screen.
            int start = this.Width / 2;
            for (int i = start; i < this.Width; i++)
            {
                float x = i - (this.Width / 2);                
                // light intensity decays in proportion to the square of distance
                // i put a really big fudge factor in, otherwise it drops of really really fast
                float color = ((x * tan) * (x * tan)) / 500;
                // convert to [0...255] integers
                int intColor = (int)(color * 255);
                // clamp to valid integer color values
                if (intColor > 255)
                    intColor = 255;
                if (intColor < 0)
                    intColor = 0;
                // create a pen with the shade found from the trigonometry
                Color shade = Color.FromArgb(intColor, this.BackColor);
                g.DrawLine(new Pen(shade), i, 0, i, this.Height);
            }
            return rval;
        }

        /// <summary>
        /// tick tick tick
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void timer1_Tick(object sender, EventArgs e)
        {
            this.Invalidate();
            //angleCounter++;            
            m_AngleCounter += m_XIncrement;
        }
    }

    
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The BSD License


Written By
United States United States
Whoa! I'm thebeekeeper. I'm from Milwaukee, WI but I'm living in Boston, MA right now. I work for a very large organization doing digital signal processing, but sometimes I write programs for computers!

Go look at my web-site! (thebeekeeper.net)

Comments and Discussions