Search Unity

Unity UI scaling RectTransform on mouse position

Discussion in 'UGUI & TextMesh Pro' started by MaskedMouse, Nov 11, 2019.

  1. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    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
    Code (CSharp):
    1. using DG.Tweening;
    2. using UnityEngine;
    3. using UnityEngine.EventSystems;
    4.  
    5. namespace Development
    6. {
    7.     public class ZoomComponent : MonoBehaviour, IScrollHandler
    8.     {
    9.         [SerializeField]
    10.         private RectTransform Target;
    11.  
    12.         [SerializeField]
    13.         private float MinimumScale = 0.5f;
    14.  
    15.         [SerializeField]
    16.         private float MaximumScale = 3f;
    17.  
    18.         [SerializeField]
    19.         private float ScaleStep = 0.25f;
    20.  
    21.         // Current scale using
    22.         private float scale = 1f;
    23.  
    24.         private void Awake()
    25.         {
    26.             if (Target == null) Target = (RectTransform) transform;
    27.             Target.localScale = Vector3.one;
    28.             Target.anchorMin = Vector2.zero;
    29.             Target.anchorMax = Vector2.one;
    30.         }
    31.  
    32.         public void OnScroll(PointerEventData eventData)
    33.         {
    34.             // Set the new scale
    35.             var scrollDeltaY = (eventData.scrollDelta.y * ScaleStep);
    36.             var newScaleValue = scale + scrollDeltaY;
    37.             ApplyScale(newScaleValue, eventData.position);
    38.         }
    39.  
    40.         private void ApplyScale(float newScaleValue, Vector2 position)
    41.         {
    42.             var newScale = Mathf.Clamp(newScaleValue, MinimumScale, MaximumScale);
    43.             if (newScale.Equals(scale)) return;
    44.  
    45.             // Set new pivot
    46.             RectTransformUtility.ScreenPointToLocalPointInRectangle(Target, position, Camera.main, out var point);
    47.             var targetRect = Target.rect;
    48.             var pivotX = (float) ((point.x - (double) targetRect.x) / (targetRect.xMax - (double) targetRect.x));
    49.             var pivotY = (float) ((point.y - (double) targetRect.y) / (targetRect.yMax - (double) targetRect.y));
    50.             var pivot = new Vector2(pivotX, pivotY);
    51.             Target.pivot = pivot;
    52.          
    53.             // Set the new scale
    54.             scale = newScale;
    55.             Target.DOScale(scale, .3f).SetEase(Ease.InOutCubic);
    56.         }
    57.  
    58.         /// <summary>
    59.         /// Applies the scale given
    60.         /// </summary>
    61.         private void ApplyScale(float newScaleValue)
    62.         {
    63.             var newScale = Mathf.Clamp(newScaleValue, MinimumScale, MaximumScale);
    64.             if (newScale.Equals(scale)) return;
    65.  
    66.             scale = newScale;
    67.             Target.DOScale(scale, .3f).SetEase(Ease.InOutCubic);
    68.         }
    69.  
    70.         /// <summary>
    71.         /// Called from Unity UI
    72.         /// </summary>
    73.         public void ZoomIn()
    74.         {
    75.             ApplyScale(scale + ScaleStep);
    76.         }
    77.  
    78.         /// <summary>
    79.         /// Called from Unity UI
    80.         /// </summary>
    81.         public void ZoomOut()
    82.         {
    83.             ApplyScale(scale - ScaleStep);
    84.         }
    85.     }
    86. }
     
  2. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    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.
     
  3. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    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.
     

    Attached Files:

    Last edited: Nov 12, 2019
  4. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    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).
     
    ZJWCY likes this.
  5. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    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:

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.EventSystems;
    3.  
    4. namespace Development
    5. {
    6.     /// <summary>
    7.     /// Zoom component which will handle the scroll wheel events and zooms in on the pointer
    8.     /// </summary>
    9.     public class ZoomComponent : MonoBehaviour, IScrollHandler
    10.     {
    11.         /// <summary>
    12.         /// The Target RectTransform the scale will be applied on.
    13.         /// </summary>
    14.         [SerializeField]
    15.         private RectTransform Target;
    16.  
    17.         /// <summary>
    18.         /// The minimum scale of the RectTransform Target
    19.         /// </summary>
    20.         [SerializeField]
    21.         private float MinimumScale = 0.5f;
    22.  
    23.         /// <summary>
    24.         /// The maximum scale of the RectTransform Target
    25.         /// </summary>
    26.         [SerializeField]
    27.         private float MaximumScale = 3f;
    28.  
    29.         /// <summary>
    30.         /// The scale value it should increase / decrease based on mouse wheel event
    31.         /// </summary>
    32.         [SerializeField]
    33.         private float ScaleStep = 0.25f;
    34.  
    35.         // Used camera for the local point calculation
    36.         [SerializeField]
    37.         private new Camera camera = null;
    38.  
    39.         private Camera Camera
    40.         {
    41.             get
    42.             {
    43.                 if (camera == null) return camera = Target.GetComponentInParent<Canvas>().worldCamera;
    44.                 return camera;
    45.             }
    46.         }
    47.      
    48.         /// <summary>
    49.         /// Current scale which is used to keep track whether it is within boundaries
    50.         /// </summary>
    51.         private float scale = 1f;
    52.  
    53.         private void Awake()
    54.         {
    55.             if (Target == null) Target = (RectTransform) transform;
    56.          
    57.             // Get the current scale
    58.             var targetScale = Target.localScale;
    59.             if(!targetScale.x.Equals(targetScale.y)) Debug.LogWarning("Scale is not uniform.");
    60.             scale = targetScale.x;
    61.          
    62.             // Do a check for the anchors of the target
    63.            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)");
    64.         }
    65.  
    66.         public void OnScroll(PointerEventData eventData)
    67.         {
    68.             // Set the new scale
    69.             var scrollDeltaY = (eventData.scrollDelta.y * ScaleStep);
    70.             var newScaleValue = scale + scrollDeltaY;
    71.             ApplyScale(newScaleValue, eventData.position);
    72.         }
    73.  
    74.         /// <summary>
    75.         /// Applies the scale with the mouse pointer in mind
    76.         /// </summary>
    77.         /// <param name="newScaleValue"></param>
    78.         /// <param name="position"></param>
    79.         private void ApplyScale(float newScaleValue, Vector2 position)
    80.         {
    81.             var newScale = Mathf.Clamp(newScaleValue, MinimumScale, MaximumScale);
    82.             // If the scale did not change, don't do anything
    83.             if (newScale.Equals(scale)) return;
    84.  
    85.             // Calculate the local point in the rectangle using the event position
    86.             RectTransformUtility.ScreenPointToLocalPointInRectangle(Target, position, Camera, out var localPointInRect);
    87.             // Set the pivot based on the local point in the rectangle
    88.             SetPivot(Target, localPointInRect);
    89.  
    90.             // Set the new scale
    91.             scale = newScale;
    92.             // Apply the new scale
    93.             Target.localScale = new Vector3(scale, scale, scale);
    94.         }
    95.  
    96.      
    97.         /// <summary>
    98.        /// Sets the pivot based on the local point of the rectangle <see cref="RectTransformUtility.ScreenPointToLocalPointInRectangle"/>.
    99.         /// Keeps the RectTransform in place when changing the pivot by countering the position change when the pivot is set.
    100.         /// </summary>
    101.         /// <param name="rectTransform">The target RectTransform</param>
    102.         /// <param name="localPoint">The local point of the target RectTransform</param>
    103.         private void SetPivot(RectTransform rectTransform, Vector2 localPoint)
    104.         {
    105.             // Calculate the pivot by normalizing the values
    106.             var targetRect = rectTransform.rect;
    107.             var pivotX = (float) ((localPoint.x - (double) targetRect.x) / (targetRect.xMax - (double) targetRect.x));
    108.             var pivotY = (float) ((localPoint.y - (double) targetRect.y) / (targetRect.yMax - (double) targetRect.y));
    109.             var newPivot = new Vector2(pivotX, pivotY);
    110.  
    111.             // Delta pivot = (current - new) * scale
    112.             var deltaPivot = (rectTransform.pivot - newPivot) * scale;
    113.            // 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
    114.             var rectSize = targetRect.size;
    115.             var deltaPosition = new Vector3(deltaPivot.x * rectSize.x, deltaPivot.y * rectSize.y) * -1f;
    116.  
    117.             // Set the pivot
    118.             rectTransform.pivot = newPivot;
    119.             rectTransform.localPosition += deltaPosition;
    120.         }
    121.     }
    122. }
     
  6. vonSchlank

    vonSchlank

    Joined:
    Jan 5, 2017
    Posts:
    36
  7. hzata

    hzata

    Joined:
    Nov 26, 2020
    Posts:
    1
    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!
     
  8. guodancoder

    guodancoder

    Joined:
    Dec 14, 2017
    Posts:
    4
    Replace MaskedMouse Code: OnScroll with Update.
    Code (CSharp):
    1.    
    2. void Update()
    3.     {
    4.         if (Input.touchCount == 2)
    5.         {
    6.             Touch touch1 = Input.GetTouch(0);
    7.             Touch touch2 = Input.GetTouch(1);
    8.  
    9.             if (touch1.phase == TouchPhase.Began || touch2.phase == TouchPhase.Began)
    10.             {
    11.                 initialDistance = Vector2.Distance(touch1.position, touch2.position);
    12.                 initialScale = transform.localScale;
    13.             }
    14.             else if (touch1.phase == TouchPhase.Moved || touch2.phase == TouchPhase.Moved)
    15.             {
    16.                 float currentDistance = Vector2.Distance(touch1.position, touch2.position);
    17.                 float scaleFactor = currentDistance / initialDistance;
    18.  
    19.                 ApplyScale(scaleFactor * initialScale.x, (touch1.position + touch2.position) * 0.5f);
    20.             }
    21.         }
    22.     }