scaling RectTransform on mouse position

I’m trying to create a script which sets the pivot point at the mouse position and then scales the target RectTransform.

Basically what i’m trying to achieve is a zoom in / out of an image at the mouse position
But what happens is when I set the pivot, Unity compensates the pivot change which causes a displacement. But this only happens when the image is scaled.
How can I achieve the same effect as if I’m dragging the pivot point manually? Because in the Editor that doesn’t move the image.

Here is my current setup:
Canvas (Screenspace Camera, Scale with screen size 1920x1080, Match 1.0)
– Parent ← ZoomComponent (default values)
— Child ← Image

Heres the script I’ve written (Note it requires DoTween):
Use the scroll wheel to zoom in

using DG.Tweening;
using UnityEngine;
using UnityEngine.EventSystems;

namespace Development
{
    public class ZoomComponent : MonoBehaviour, IScrollHandler
    {
        [SerializeField]
        private RectTransform Target;

        [SerializeField]
        private float MinimumScale = 0.5f;

        [SerializeField]
        private float MaximumScale = 3f;

        [SerializeField]
        private float ScaleStep = 0.25f;

        // Current scale using
        private float scale = 1f;

        private void Awake()
        {
            if (Target == null) Target = (RectTransform) transform;
            Target.localScale = Vector3.one;
            Target.anchorMin = Vector2.zero;
            Target.anchorMax = Vector2.one;
        }

        public void OnScroll(PointerEventData eventData)
        {
            // Set the new scale
            var scrollDeltaY = (eventData.scrollDelta.y * ScaleStep);
            var newScaleValue = scale + scrollDeltaY;
            ApplyScale(newScaleValue, eventData.position);
        }

        private void ApplyScale(float newScaleValue, Vector2 position)
        {
            var newScale = Mathf.Clamp(newScaleValue, MinimumScale, MaximumScale);
            if (newScale.Equals(scale)) return;

            // Set new pivot
            RectTransformUtility.ScreenPointToLocalPointInRectangle(Target, position, Camera.main, out var point);
            var targetRect = Target.rect;
            var pivotX = (float) ((point.x - (double) targetRect.x) / (targetRect.xMax - (double) targetRect.x));
            var pivotY = (float) ((point.y - (double) targetRect.y) / (targetRect.yMax - (double) targetRect.y));
            var pivot = new Vector2(pivotX, pivotY);
            Target.pivot = pivot;
          
            // Set the new scale
            scale = newScale;
            Target.DOScale(scale, .3f).SetEase(Ease.InOutCubic);
        }

        /// <summary>
        /// Applies the scale given
        /// </summary>
        private void ApplyScale(float newScaleValue)
        {
            var newScale = Mathf.Clamp(newScaleValue, MinimumScale, MaximumScale);
            if (newScale.Equals(scale)) return;

            scale = newScale;
            Target.DOScale(scale, .3f).SetEase(Ease.InOutCubic);
        }

        /// <summary>
        /// Called from Unity UI
        /// </summary>
        public void ZoomIn()
        {
            ApplyScale(scale + ScaleStep);
        }

        /// <summary>
        /// Called from Unity UI
        /// </summary>
        public void ZoomOut()
        {
            ApplyScale(scale - ScaleStep);
        }
    }
}

If changing the pivot without scaling it does not cause the displacement, I would be suspicious that the scaling function you are calling in DoTween is making some assumption that you are unwittingly breaking. What happens if you just set the scale directly using transform.localScale?

You could also try logging the anchoredPosition and/or sizeDelta from before and after you change the pivot and see if they’re changing.

No, setting the scale directly is giving the same effect.
Right so to get things straight setting the pivot in code with a scaled rect transform i.e not 1,1,1 the Left, Top, Right, Bottom stays 0. When the scale != 1 and when you drag the pivot around in the editor Unity compensates and all values change.

All what I am trying to do is create a zoom function so I can zoom in on where the mouse pointer is targetted.
And I want to achieve that by scaling the parent. But the parent anchors are set to stretch.
It’s the same effect as in draw.io when zooming in on the elements.

Import the package attached (Unity 2019.2.x) and open the Test Scene
It has 4 checker squares. My goal is to zoom in on these squares using the mouse wheel.
So in play mode target the right top square and use the scroll wheel to zoom in. That goes fine right?
Now zoom out and zoom in on the left top square and that won’t work. Setting the pivot while the scale != 1,1,1 and when the Anchors are set to stretch this approach won’t work for me because it doesn’t compensate like it does in the Editor when moving the pivot manually instead of code.

5165642–512444–ZoomingPackage.unitypackage (5.73 KB)

Hm. Well, if pivots work in such a way that the other parameters of the transform need to be adjusted in order to keep the object at the same apparent position, and Unity doesn’t make that adjustment automatically when you set the pivot through script, then you may need to do all of those adjustments yourself from first principles.

Might be easier to instead calculate the required positional offset to keep the same part of the picture underneath the mouse cursor without moving the pivot. I think that would be something along the lines of (pivot - mouse position) * (newScale/oldScale - 1).

1 Like

My colleague helped me a bit out after a while. Seems like I have a solution but it is tied to a certain condition.
I had my RectTransform set to stretch so anchors min 0,0 and max 1,1. We got a new calculation which sets the position right where I want it to be but it requires the anchors to be min 0.5, 0.5 and max 0.5, 0.5

Here’s the change for if anyone is trying to do a similar setup:

using UnityEngine;
using UnityEngine.EventSystems;

namespace Development
{
    /// <summary>
    /// Zoom component which will handle the scroll wheel events and zooms in on the pointer
    /// </summary>
    public class ZoomComponent : MonoBehaviour, IScrollHandler
    {
        /// <summary>
        /// The Target RectTransform the scale will be applied on.
        /// </summary>
        [SerializeField]
        private RectTransform Target;

        /// <summary>
        /// The minimum scale of the RectTransform Target
        /// </summary>
        [SerializeField]
        private float MinimumScale = 0.5f;

        /// <summary>
        /// The maximum scale of the RectTransform Target
        /// </summary>
        [SerializeField]
        private float MaximumScale = 3f;

        /// <summary>
        /// The scale value it should increase / decrease based on mouse wheel event
        /// </summary>
        [SerializeField]
        private float ScaleStep = 0.25f;

        // Used camera for the local point calculation
        [SerializeField]
        private new Camera camera = null;

        private Camera Camera
        {
            get
            {
                if (camera == null) return camera = Target.GetComponentInParent<Canvas>().worldCamera;
                return camera;
            }
        }
      
        /// <summary>
        /// Current scale which is used to keep track whether it is within boundaries
        /// </summary>
        private float scale = 1f;

        private void Awake()
        {
            if (Target == null) Target = (RectTransform) transform;
          
            // Get the current scale
            var targetScale = Target.localScale;
            if(!targetScale.x.Equals(targetScale.y)) Debug.LogWarning("Scale is not uniform.");
            scale = targetScale.x;
          
            // Do a check for the anchors of the target
           if(Target.anchorMin != new Vector2(0.5f, 0.5f) || Target.anchorMax != new Vector2(0.5f, 0.5f)) Debug.LogWarning("Anchors are not set to Middle(center)");
        }

        public void OnScroll(PointerEventData eventData)
        {
            // Set the new scale
            var scrollDeltaY = (eventData.scrollDelta.y * ScaleStep);
            var newScaleValue = scale + scrollDeltaY;
            ApplyScale(newScaleValue, eventData.position);
        }

        /// <summary>
        /// Applies the scale with the mouse pointer in mind
        /// </summary>
        /// <param name="newScaleValue"></param>
        /// <param name="position"></param>
        private void ApplyScale(float newScaleValue, Vector2 position)
        {
            var newScale = Mathf.Clamp(newScaleValue, MinimumScale, MaximumScale);
            // If the scale did not change, don't do anything
            if (newScale.Equals(scale)) return;

            // Calculate the local point in the rectangle using the event position
            RectTransformUtility.ScreenPointToLocalPointInRectangle(Target, position, Camera, out var localPointInRect);
            // Set the pivot based on the local point in the rectangle
            SetPivot(Target, localPointInRect);

            // Set the new scale
            scale = newScale;
            // Apply the new scale
            Target.localScale = new Vector3(scale, scale, scale);
        }

      
        /// <summary>
       /// Sets the pivot based on the local point of the rectangle <see cref="RectTransformUtility.ScreenPointToLocalPointInRectangle"/>.
        /// Keeps the RectTransform in place when changing the pivot by countering the position change when the pivot is set.
        /// </summary>
        /// <param name="rectTransform">The target RectTransform</param>
        /// <param name="localPoint">The local point of the target RectTransform</param>
        private void SetPivot(RectTransform rectTransform, Vector2 localPoint)
        {
            // Calculate the pivot by normalizing the values
            var targetRect = rectTransform.rect;
            var pivotX = (float) ((localPoint.x - (double) targetRect.x) / (targetRect.xMax - (double) targetRect.x));
            var pivotY = (float) ((localPoint.y - (double) targetRect.y) / (targetRect.yMax - (double) targetRect.y));
            var newPivot = new Vector2(pivotX, pivotY);

            // Delta pivot = (current - new) * scale
            var deltaPivot = (rectTransform.pivot - newPivot) * scale;
           // The delta position to add after pivot change is the inversion of the delta pivot change * size of the rect * current scale of the rect
            var rectSize = targetRect.size;
            var deltaPosition = new Vector3(deltaPivot.x * rectSize.x, deltaPivot.y * rectSize.y) * -1f;

            // Set the pivot
            rectTransform.pivot = newPivot;
            rectTransform.localPosition += deltaPosition;
        }
    }
}
5 Likes

Necro, i guess, but for normalizing there is API support - spares a few lines:
https://docs.unity3d.com/ScriptReference/Rect.PointToNormalized.html

This worked nicely for zooming in with a mouse. What would the code be if I wanted to have a touch input instead of a mouse, like on touchscreens where you can scale a photo from a point on your phone using two fingers? Any help would be greatly appreciated!

Replace MaskedMouse Code: OnScroll with Update.

void Update()
    {
        if (Input.touchCount == 2)
        {
            Touch touch1 = Input.GetTouch(0);
            Touch touch2 = Input.GetTouch(1);

            if (touch1.phase == TouchPhase.Began || touch2.phase == TouchPhase.Began)
            {
                initialDistance = Vector2.Distance(touch1.position, touch2.position);
                initialScale = transform.localScale;
            }
            else if (touch1.phase == TouchPhase.Moved || touch2.phase == TouchPhase.Moved)
            {
                float currentDistance = Vector2.Distance(touch1.position, touch2.position);
                float scaleFactor = currentDistance / initialDistance;

                ApplyScale(scaleFactor * initialScale.x, (touch1.position + touch2.position) * 0.5f);
            }
        }
    }