Расчет времени анимации

Здравствуйте.
Подскажите пожалуйста как правильно расчитать анимацию движения с учетом fps?

Вот у меня простой код. Шарик должен двигаться по синусоиде с частотой 1 герц.
То есть за 1 сек нужно сделать оборот.

 thread = new Thread(new ThreadStart(() =>
                {
                        double sin = R * Math.Sin(angle * grad);
                        angle += step;
                        Dispatcher.BeginInvoke(new Action(() =>
                        {
                            Canvas.Info = "Test: " + Hzs[testCnt].ToString() + " Hz, " + secTimes[testCnt] + " sec., TotalTime:  " + stw.Elapsed.Seconds.ToString() + " sec.";
                            if (IsVertical)
                            {
                                Canvas.Markerpos = new Point(Canvas.ActualWidth / 2, Canvas.ActualHeight / 2 + sin);
                            }
                            else
                            {
                                Canvas.Markerpos = new Point(Canvas.ActualWidth / 2 + sin, Canvas.ActualHeight / 2);
                            }
                            Canvas.InvalidateVisual();
                            //Canvas.UpdateLayout();
                        }));
                        Thread.Sleep(threadDelay);
                    }
                    onTestCanceledEvent?.Invoke();
                }));
                thread.Start();

А как мне сделать так чтобы обойтись без Thread.Sleep(threadDelay); и сделать максимально плавную отрисовку с максимальным фпс.
А то сейчас я делю на фиксированное количество шагов и глазом заметно что элемент дергается при подходе к центру.

Умножать на время прошедшее с начала?

Типа как тут https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame#examples

Вот сделал класс анимации:

public class VisualRenderer : FrameworkElement
    {
        private static readonly DependencyPropertyKey MarkerPositionPropertyKey = DependencyProperty.RegisterReadOnly("MarkerPosition", typeof(Point), typeof(VisualRenderer), new FrameworkPropertyMetadata(new Point(100, 100), FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender));

        private static readonly DependencyProperty MarkerPositionProperty = MarkerPositionPropertyKey.DependencyProperty;

        public Point MarkerPosition
        {
            get => (Point)GetValue(MarkerPositionProperty);

            set
            {
                if (Interlocked.Read(ref LockRender) == 0)
                {
                    Dispatcher.Invoke(new Action(() => SetValue(MarkerPositionPropertyKey, value)));
                }
            }
        }



        private static readonly DependencyPropertyKey InfoPropertyKey = DependencyProperty.RegisterReadOnly("Info", typeof(string), typeof(VisualRenderer), new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender));

        private static readonly DependencyProperty InfoProperty = InfoPropertyKey.DependencyProperty;

        public string Info
        {
            get => (string)GetValue(InfoProperty);

            set
            {
                if (Interlocked.Read(ref LockRender) == 0)
                {
                    Dispatcher.Invoke(new Action(() => SetValue(InfoPropertyKey, value)));
                }
            }
        }




        private static readonly DependencyPropertyKey MarkerSizePropertyKey = DependencyProperty.RegisterReadOnly("MarkerSize", typeof(int), typeof(VisualRenderer), new FrameworkPropertyMetadata(25, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender));

        private static readonly DependencyProperty MarkerSizeProperty = MarkerSizePropertyKey.DependencyProperty;

        public int MarkerSize
        {
            get => (int)GetValue(MarkerSizeProperty);

            set => SetValue(MarkerSizePropertyKey, value);
        }




        SolidColorBrush borderBrush;
        SolidColorBrush markerBrush = Brushes.Red;
        SolidColorBrush marker2Brush = Brushes.Blue;
        Pen borderPen;


        public delegate void OnPreviewChanged();
        public event OnPreviewChanged onPreviewChangedEvent;


        /// <summary>
        /// Получает границы рабочей области с учетом размера метки
        /// </summary>
        public Rect MarkerBorders
        {
            get
            {
                return new Rect()
                {
                    X = 0,
                    Y = 0,
                    Width = ActualWidth,
                    Height = ActualHeight
                };
            }
        }

        public VisualRenderer()
        {
            ResetDefaultsParam();
        }

        /// <summary>
        /// Устанавливает параметры по умолчнию
        /// </summary>
        public void ResetDefaultsParam()
        {
            borderBrush = new SolidColorBrush(Color.FromArgb(0xFF, 0xA1, 0xD2, 0x5A));
            borderPen = new Pen(borderBrush, 5);
            MarkerPosition = new Point(RenderSize.Width / 2, RenderSize.Height / 2);
        }

        long LockRender = 0;

        protected override void OnRender(DrawingContext drawingContext)
        {
            if (Interlocked.CompareExchange(ref LockRender, 1, 0) == 0)
            {
                drawingContext.PushClip(new RectangleGeometry(new Rect(RenderSize)));

                drawingContext.DrawRectangle(null, new Pen(borderBrush, 8), new Rect(0, 0, RenderSize.Width, RenderSize.Height));

                drawingContext.DrawRectangle(null, borderPen, new Rect(RenderSize.Width / 2, 0, RenderSize.Width / 2, RenderSize.Height));
                drawingContext.DrawRectangle(null, borderPen, new Rect(0, RenderSize.Height / 2, RenderSize.Width, RenderSize.Height / 2));

                drawingContext.DrawEllipse(markerBrush, new Pen(Brushes.Black, 1), MarkerPosition, MarkerSize, MarkerSize);

                FormattedText formattedText = new FormattedText(Info, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface("Verdana"), 16, Brushes.Black, VisualTreeHelper.GetDpi(this).PixelsPerDip);

                drawingContext.DrawText(formattedText, new Point(RenderSize.Width / 2, 50));
                drawingContext.Pop();             

                onPreviewChangedEvent?.Invoke();
                Interlocked.Exchange(ref LockRender, 0);
            }
            base.OnRender(drawingContext);
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            return base.MeasureOverride(availableSize);
        }

    }

Вот поток который выполняет обновление положения метки по синусоиды с разным значением частоты:

if (thread == null)
            {
                angle = 0;
                double Hz = 1.0;

                double[] Hzs = new double[] { 0.1, 0.2, 0.3, 0.4, 0.5 };
                int[] secTimes = new int[] { 10, 10, 5, 5, 5 };
                int[] defAngle = new int[] { 0, 0, 0, 180, 180 };
                int testCnt = 0;

                R = GetMaxBorder();

                // initializition
                Hz = Hzs[testCnt];
                cancel = false;
                System.Diagnostics.Stopwatch stw = new System.Diagnostics.Stopwatch();
                TimeSpan lasttime = stw.Elapsed;
                thread = new Thread(new ThreadStart(() =>
                {
                    stw.Restart();
                    lasttime = stw.Elapsed;
                    while (!cancel)
                    {
                        var delta = stw.Elapsed - lasttime;
                        lasttime = stw.Elapsed;
                        double step = (360.0 * Hz) * (double)delta.TotalSeconds;
                        if (stw.Elapsed.TotalSeconds >= secTimes[testCnt])
                        {
                            if (testCnt < Hzs.Length - 1)
                            {
                                testCnt++;
                                Hz = Hzs[testCnt];
                                stw.Restart();
                                lasttime = stw.Elapsed;
                                angle = defAngle[testCnt];
                                step = 0;
                            }
                            else
                            {
                                stw.Stop();
                                lasttime = stw.Elapsed;
                                cancel = true;
                            }
                        }

                        angle += step; 
                        string info = "Test: " + Hzs[testCnt].ToString() + " Hz, " + secTimes[testCnt] + " sec., TotalTime:  " + stw.Elapsed.Seconds.ToString() + " sec.";

                        SetMarkerPos(new Point(angle, 0), info);

                        Thread.Sleep(5);
                    }

                    SetMarkerPos(new Point(), "Complete");
                }));
                thread.Start();
            }
            else
            {
                cancel = true;
                thread = null;
            }

Но все равно вроде бы как при приближении метки к центру наблюдаются какие то рывки. Нету плавности движения.
Почему?? что тут еще то можно сделать итак ведь частота обновления метки почти 200 герц.

WpfApp1.7z (7.6 КБ)