Question

I need to implement a simple animation of a ball moving in uniform circular motion. I've tried several formulas and the following version seems the best so far.However, there are still 2 issues and I really can't figure out what's wrong.

First, a couple of seconds right after the program starts, the ball moves erratically. I think that the values for theta (the angle in radians) are not computed correctly, but I don't know why.

Secondly, the movement becomes more uniform after a while, but it seems to decrease over time.

 The value for 'speed' indicates the number of seconds it takes to do a full revolution.

What I want is an uniform, correct circular movement (according to the value of speed) and without the jerkiness in the beginning.

My code so far:

public partial class ServerForm : Form
{
    Stopwatch watch;

    //Angular velocity
    float angularVelocity;

    //Angle
    float theta = 20;

    //Speed - time to complete a full revolution, in seconds
    private float speed = 3;

    //Circle center
    private int centerX = 250;
    private int centerY = 200;

    //Circle radius
    private float R = 120;

    //Current position
    private LocationData currentLocation;

    public ServerForm()
    {
        InitializeComponent();
    }

    public void UpdateUI()
    {            
        currentLocation.CoordX = (float)(centerX + Math.Cos(theta) * R);
        currentLocation.CoordY = (float)(centerY + Math.Sin(theta) * R);
        currentLocation.Speed = speed;

        try
        {
            this.Invoke(new Action(() => { this.Invalidate(); }));
        }
        catch (Exception ex)
        {
            watch.Stop();
            Application.Exit();
        }

        theta += (float)((angularVelocity * 1000 / watch.ElapsedMilliseconds));            
        //Console.Out.WriteLine("elapsed miliseconds: " + watch.ElapsedMilliseconds + " theta = " + theta);

    }

    protected override void OnPaint(PaintEventArgs e)
    {
        Graphics g = e.Graphics;
        Brush color = new SolidBrush(Color.BlueViolet);

        g.FillEllipse(color, currentLocation.CoordX, currentLocation.CoordY, 30, 30);

        //Draw circle & center
        g.DrawEllipse(new Pen(color), centerX, centerY, 5, 5);

        float x = centerX - R;
        float y = centerY - R;
        float width = 2 * R;
        float height = 2 * R;
        g.DrawEllipse(new Pen(color), x, y, width, height);

        base.OnPaint(e);
    }

    private void button1_Click(object sender, EventArgs e)
    {
        if (!String.IsNullOrEmpty(textSpeed.Text))
        {
            ResetValues(float.Parse(textSpeed.Text));                
        }
    }

    private void ResetValues(float newSpeed)
    {
        speed = newSpeed;
        angularVelocity = (float)(2 * Math.PI / speed);  // radians / sec

        //Start at the top
        currentLocation.CoordX = centerX;
        currentLocation.CoordY = centerY - R;

        theta = 90;

        watch.Restart();            
    }

    private void ServerForm_Load(object sender, EventArgs e)
    {
        watch = new Stopwatch();            

        timer1.Enabled = true;
        timer1.Interval = 100;
        timer1.Tick += timer1_Tick;            

        currentLocation = new LocationData();
        ResetValues(speed);          
    }

    void timer1_Tick(object sender, EventArgs e)
    {
        UpdateUI();
    }

}

LocationData is just a class holding the coordinates & current speed. Are the units for time & angular velocity (and the transformations to use miliseconds) correct?

I changed BackgroundWorker to Timer, but I still get that erratic motion and the movement slows down after a while.

Was it helpful?

Solution

Try using a System.Windows.Forms.Timer instead of a BackgroundWorker. I believe you'll get more consistent results. This is definitely not a good case for using a BackgroundWorker.

Here's a more-or-less complete solution. Note that I'm scaling the swing radius and the ball radius by the size of the Form.

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        SetStyle(ControlStyles.AllPaintingInWmPaint, true);
        SetStyle(ControlStyles.Opaque, true);
        SetStyle(ControlStyles.ResizeRedraw, true);
        SetStyle(ControlStyles.DoubleBuffer, true);
        SetStyle(ControlStyles.UserPaint, true);
    }

    private void Form1_Load(object sender, System.EventArgs e)
    {
        _stopwatch.Start();
    }

    private void Timer1_Tick(object sender, System.EventArgs e)
    {
        Invalidate();
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);

        e.Graphics.Clear(BackColor);

        const float rotationTime = 2000f;

        var elapsedTime = (float) _stopwatch.ElapsedMilliseconds;

        var swingRadius = Math.Min(ClientSize.Width, ClientSize.Height) / 4f;

        var theta = Math.PI * 2f * elapsedTime / rotationTime;

        var ballRadius = Math.Min(ClientSize.Width, ClientSize.Height) / 10f;

        var ballCenterX = (float) ((ClientSize.Width / 2f) + (swingRadius * Math.Cos(theta)));

        var ballCenterY = (float) ((ClientSize.Height / 2f) + (swingRadius * Math.Sin(theta)));

        var ballLeft = ballCenterX - ballRadius;

        var ballTop = ballCenterY - ballRadius;

        var ballWidth = ballRadius * 2f;

        var ballHeight = ballRadius * 2f;

        e.Graphics.FillEllipse(Brushes.Red, ballLeft, ballTop, ballWidth, ballHeight);

        e.Graphics.DrawEllipse(Pens.Black, ballLeft, ballTop, ballWidth, ballHeight);
    }

    private readonly Stopwatch _stopwatch = new Stopwatch();
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top