Search code examples
c#geometrysystem.drawingaffinetransform

Proportional Translation


I'd like to update a list of points (PointFs) by performing a rotation (around a new origin) and translating each point by an amount that is proportional to its current distance from the origin (so not an absolute translation).

I currently do this for each point in turn but performance is poor when moving more than a handful of points.

I'd like to make the transformation more efficient so wanted to use a matrix. The rotation is no problem, but I don't know how to do the proportional translation.

Can I do this with an affine matrix? Is there some other way to do the transformation more efficiently?

UPDATED

Here's my current code. I've changed it a little so at least it does use a matrix for the rotation. Note the translation is based on a ratio, so points closer to the centre won't move as far as points further away:

    private void DragPointsAroundCentre(PointF centre, PointF priorLocation, PointF newLocation, PointF[] otherPoints)
    {
        // calculate the angle and length of the transformation from the original location
        var priorLength = Maths.Distance(centre, priorLocation);
        var newLength = Maths.Distance(centre, newLocation);

        var lengthRatio = newLength / priorLength;
        var rotationAngle = (float)Maths.Angle(centre, priorLocation, newLocation);

        // apply the rotation to the other points
        Rotate(otherPoints, rotationAngle, centre);

        // apply an equivalent translation to the other points
        for (int i = 0; i < otherPoints.Length ; i++)
        {
            var translation = GetPointOnLine(centre, otherPoints[i], (float) lengthRatio);
            otherPoints[i].X = translation.X;
            otherPoints[i].Y = translation.Y;
        }
    }

    private static void Rotate(PointF[] points, float angle, PointF center)
    {
        using (Matrix m = new Matrix())
        {
            m.RotateAt(angle, center);
            m.TransformPoints(points);
        }
    }

    // gets a point from a relative position on a line using the specified ratio
    private static PointF GetPointOnLine(PointF origin, PointF point, float ratio)
    {
        return new PointF(
            origin.X + (point.X - origin.X) * ratio,
            origin.Y + (point.Y - origin.Y) * ratio);
    }

Solution

  • This is the code I use for transformations. I hope this helps you:

    class Program
    {
        static void Main(string[] args)
        {
            PointF[] points = new PointF[] 
            { 
                new PointF(1, 0), 
                new PointF(0, 1) 
            };
    
            float angle = 90; // in degrees
            PointF center = new PointF(1, 1);
            Rotate(points, angle, center);
    
            float offset = 10;
            PointF vector = new PointF(1, 1);
            Translate(points, offset, vector);
        }
    
        static void Rotate(PointF[] points, float angle, PointF center)
        {
            using (Matrix m = new Matrix())
            {
                m.RotateAt(angle, center);
                m.TransformPoints(points);
            }
        }
    
        // Translates point along the specified vector.
        static void Translate(PointF[] points, float offset, PointF vector)
        {
            float magnitude = (float)Math.Sqrt((vector.X * vector.X) + (vector.Y * vector.Y)); // = length
            vector.X /= magnitude;
            vector.Y /= magnitude;
            PointF translation = new PointF()
            {
                X = offset * vector.X,
                Y = offset * vector.Y
            };
            using (Matrix m = new Matrix())
            {
                m.Translate(translation.X, translation.Y);
                m.TransformPoints(points);
            }
        }
    }
    

    If you need the transformation to be very efficient you can combine both transformation matrices into one and transform all points only once.

    EDIT:

    You can use for example a simple parallel loop to make it a little bit faster. But even for 30.000.000 points the difference is not too big in this case (my case 4 cpu cores). But it depends of course how often do you process them.

    class Program
    {
        static void Main(string[] args)
        {
            int pointCount = 30000000;
            PointF[] otherPoints = new PointF[pointCount];
            Random rnd = new Random();
            for (int i = 0; i < pointCount; i++)
            {
                otherPoints[i] = new Point(rnd.Next(), rnd.Next());
            }
    
            PointF centre = new PointF(3, 3);
            float lengthRatio = 7.3f;
    
            // apply an equivalent translation to the other points
            Stopwatch sw = new Stopwatch();
    
            sw.Start();
            for (int i = 0; i < otherPoints.Length; i++)
            {
                var translation = GetPointOnLine(centre, otherPoints[i], (float)lengthRatio);
                otherPoints[i].X = translation.X;
                otherPoints[i].Y = translation.Y;
            }
            sw.Stop();
            Console.WriteLine("Single thread: {0} sec.", sw.Elapsed.TotalSeconds);
    
            sw.Reset();
            sw.Start();
            Parallel.For(0, pointCount, i =>
            {
                var translation = GetPointOnLine(centre, otherPoints[i], (float)lengthRatio);
                otherPoints[i].X = translation.X;
                otherPoints[i].Y = translation.Y;
    
            });
            sw.Stop();
            Console.WriteLine("Multi thread: {0} sec.", sw.Elapsed.TotalSeconds);
            Console.ReadKey();
        }
    
        // gets a point from a relative position on a line using the specified ratio
        private static PointF GetPointOnLine(PointF origin, PointF point, float ratio)
        {
            return new PointF(
                origin.X + (point.X - origin.X) * ratio,
                origin.Y + (point.Y - origin.Y) * ratio);
        }
    }
    

    EDIT-2:

    I found a transformation that is exacly the same as yours and transforms the points in only one loop using a single matrix. Here's the code for both the old and the new transformation:

    class Program
    {
        static void Main(string[] args)
        {
            PointF[] points1 = new PointF[] 
            { 
                new PointF(1f, 0f),
                new PointF(0f, 1f),
                new PointF(1f, 1f),
                new PointF(2f, 2f),
            };
            PointF[] points2 = new PointF[]
            { 
                new PointF(1f, 0f),
                new PointF(0f, 1f),
                new PointF(1f, 1f),
                new PointF(2f, 2f),
            };
    
            PointF center = new PointF(2f, 2f);
    
            float priorLength = 4f;
            float newLength = 5f;
    
            float lengthRatio = newLength / priorLength;
    
            float rotationAngle = 45f;
    
            Transformation_old(points1, rotationAngle, center, lengthRatio);
            Transformation_new(points2, rotationAngle, center, lengthRatio);
    
            Console.ReadKey();
        }
    
        static void Transformation_old(PointF[] points, float rotationAngle, PointF center, float lengthRatio)
        {
            Rotate(points, rotationAngle, center);
    
            for (int i = 0; i < points.Length; i++)
            {
                var translation = GetPointOnLine(center, points[i], lengthRatio);
                points[i].X = translation.X;
                points[i].Y = translation.Y;
            }
        }
    
        static void Rotate(PointF[] points, float angle, PointF center)
        {
            using (Matrix m = new Matrix())
            {
                m.RotateAt(angle, center);
                m.TransformPoints(points);
            }
        }
    
        private static PointF GetPointOnLine(PointF origin, PointF point, float ratio)
        {
            return new PointF(
                origin.X + (point.X - origin.X) * ratio,
                origin.Y + (point.Y - origin.Y) * ratio);
        }
    
        // Uses only a single matrix and a single transformation:
        static void Transformation_new(PointF[] points, float rotationAngle, PointF center, float lengthRatio)
        {
            using (Matrix m = new Matrix())
            {
                m.RotateAt(rotationAngle, center, MatrixOrder.Prepend);
    
                // Replaces GetPointOnLine
                m.Translate(center.X, center.Y, MatrixOrder.Prepend);
                m.Scale(lengthRatio, lengthRatio, MatrixOrder.Prepend);
                m.Translate(-center.X, -center.Y, MatrixOrder.Prepend);
    
                m.TransformPoints(points);
            }
        }
    }