Search Unity

How to have a VisualElement locked to aspect ratio?

Discussion in 'UI Toolkit' started by pbhogan, Jul 9, 2021.

  1. pbhogan

    pbhogan

    Joined:
    Aug 17, 2012
    Posts:
    384
    Has anyone figured out how to have a VisualElement fit the bounds of its parent, but maintain a particular aspect ratio?

    The aspect-ratio CSS property and typical CSS hacks (like the vh unit) typically used for this aren't supported in USS as far as I can tell.
     
  2. uMathieu

    uMathieu

    Unity Technologies

    Joined:
    Jun 6, 2017
    Posts:
    398
  3. pbhogan

    pbhogan

    Joined:
    Aug 17, 2012
    Posts:
    384
    Thanks, I saw that. But I should have mentioned that trick only works with fitting to width. I’ve used that on web pages quite a bit. As far as I know it does not work if you need to match the height rather than the width, which is what I need to do. Or more accurately, I need to handle both cases.

    The inner element in my case is taller than it is wide. The outer element could go either way. So I need a true fit to parent while maintaining aspect ratio solution that can handle any parent aspect ratio.
     
    SparkesRS and jwinn like this.
  4. Leslie-Young

    Leslie-Young

    Joined:
    Dec 24, 2008
    Posts:
    1,148
  5. pbhogan

    pbhogan

    Joined:
    Aug 17, 2012
    Posts:
    384
    Excellent! That's what I needed—thank you! Here's my version:

    https://gist.github.com/pbhogan/2094a033c094ddd1b0b8f37a5dffd005

    It lets you set the aspect ratio X and Y which will always be fitted to the parent with absolute positioning and width and height set. There are also balance X and Y attributes (0 to 100 percent) for aligning. 50/50 is centered.
     
  6. Midiphony-panda

    Midiphony-panda

    Joined:
    Feb 10, 2020
    Posts:
    243
    @pbhogan Your implementation works very well :)


    AspectRatioTests.gif


    Have you noticed/profiled a cost to use this kind of element, more-or-less extensively ?
    (or does someone from UITK team think the cost is going to be negligible ?)
     
    Neiist, curbol, fherbst and 2 others like this.
  7. pbhogan

    pbhogan

    Joined:
    Aug 17, 2012
    Posts:
    384
    I haven't profiled it, but it'll probably be okay unless something up the chain is resizing constantly. It only really does anything on a GeometryChangedEvent from the parent. ¯\_(ツ)_/¯
     
    JuliaP_Unity and Midiphony-panda like this.
  8. achimmihca

    achimmihca

    Joined:
    Feb 13, 2016
    Posts:
    283
    I was looking for a way to have an image with preserved aspect ratio.

    For an image, you can use USS property `-unity-background-scale-mode: scale-to-fit;`

    Note however that this affects only how the image is rendered inside the VisualElement.
    The VisualElement will still have a size independently of the aspect ratio.
     
  9. StripeGuy

    StripeGuy

    Joined:
    Dec 30, 2016
    Posts:
    52

    I'm just curious, how do you implement this exactly? Where do you put script?

    Thanks.
     
  10. iperezipina

    iperezipina

    Joined:
    Jun 24, 2021
    Posts:
    26
    Hi I was implementing this script into but after adding to the UI toolkit editor, I get this error, is this some kind of incompatibility with current version? (using unity 2021.2.9f1)
    What I did was adding the script to the editor and then creating a UIDocument throws this exception:

    upload_2022-2-11_13-35-43.png
     
  11. JuliaP_Unity

    JuliaP_Unity

    Unity Technologies

    Joined:
    Mar 26, 2020
    Posts:
    700
    Can you elaborate on what you mean by "adding to the UI toolkit editor"?

    If you mean you installed the package, for Unity 2021.2 you shouldn't use packages (neither for UI Toolkit nor for UI Builder) as all the necessary code is already inside the main Unity product and the packages would actually be outdated in comparison.
     
  12. iperezipina

    iperezipina

    Joined:
    Jun 24, 2021
    Posts:
    26
    Sorry, my english skills betrayed me. I'm using the builtin versions of both UI toolkit and UI builder as you indicate.

    What I did was copy the code into a .cs file, paste it into the "Editor" folder of my UI, and afterwards I tried to create a UXML file to test it out, but I found myself with that error message.
     
  13. JuliaP_Unity

    JuliaP_Unity

    Unity Technologies

    Joined:
    Mar 26, 2020
    Posts:
    700
    Ok thanks for the clarification!

    Assuming you're using the code posted earlier in this thread: https://gist.github.com/pbhogan/2094a033c094ddd1b0b8f37a5dffd005

    There was a fix that was published somewhat recently that addressed what I think is the error you're seeing, because of line 25 in that code (
    get { yield break; }
    ).

    If you're able to update to a more recent version of Unity 2021.2 (2021.2.11f1 is the latest right now), the error shouldn't occur (even though I checked and 2021.2.9f1 looks like it should have the fix). Can you try that out and let us know?
     
  14. iperezipina

    iperezipina

    Joined:
    Jun 24, 2021
    Posts:
    26
  15. iperezipina

    iperezipina

    Joined:
    Jun 24, 2021
    Posts:
    26
    Update: I've actually used the original piece of code, with a simple modification that allows to switch balance of padding between left and right.

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3. using UnityEngine.UIElements;
    4.  
    5. public class AspectRatioPadding : VisualElement
    6. {
    7.     [UnityEngine.Scripting.Preserve]
    8.     public new class UxmlFactory : UxmlFactory<AspectRatioPadding, UxmlTraits> { }
    9.  
    10.     [UnityEngine.Scripting.Preserve]
    11.     public new class UxmlTraits : VisualElement.UxmlTraits
    12.     {
    13.         readonly UxmlIntAttributeDescription RatioWidth = new UxmlIntAttributeDescription { name = "RatioWidth", defaultValue = 16 };
    14.         readonly UxmlIntAttributeDescription RatioHeight = new UxmlIntAttributeDescription { name = "RatioHeight", defaultValue = 9 };
    15.         readonly UxmlIntAttributeDescription balanceLR = new UxmlIntAttributeDescription { name = "balanceLR", defaultValue = 50, restriction = new UxmlValueBounds { min = "0", max = "100" } };
    16.  
    17.         public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
    18.         {
    19.             get { yield break; }
    20.         }
    21.  
    22.         public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
    23.         {
    24.             base.Init(ve, bag, cc);
    25.             AspectRatioPadding ele = ve as AspectRatioPadding;
    26.             ele.RatioWidth = RatioWidth.GetValueFromBag(bag, cc);
    27.             ele.RatioHeight = RatioHeight.GetValueFromBag(bag, cc);
    28.             ele.balanceLR = balanceLR.GetValueFromBag(bag, cc);
    29.         }
    30.     }
    31.  
    32.     public int RatioWidth { get; private set; } = 16;
    33.     public int RatioHeight { get; private set; } = 9;
    34.    
    35.     public int balanceLR { get; private set; } = 50 ;
    36.  
    37.  
    38.     // changed to modifying margin rather than adding new UI elements.
    39.     //private VisualElement leftPadding;
    40.     //private VisualElement rightPadding;
    41.  
    42.     // ------------------------------------------------------------------------------------------------------------
    43.  
    44.     public AspectRatioPadding()
    45.     {
    46.         style.flexDirection = FlexDirection.Row;
    47.         style.flexShrink = 0;
    48.         style.width = Length.Percent(100);
    49.         style.height = Length.Percent(100);
    50.  
    51.         //leftPadding = new VisualElement() { name = "AspectRatioPadding-Left" };
    52.         //rightPadding = new VisualElement() { name = "AspectRatioPadding-Right" };
    53.  
    54.         //Add(leftPadding);
    55.         //Add(rightPadding);
    56.  
    57.         RegisterCallback<GeometryChangedEvent>(OnGeometryChangedEvent);
    58.         RegisterCallback<AttachToPanelEvent>(OnAttachToPanelEvent);
    59.     }
    60.  
    61.     private void OnGeometryChangedEvent(GeometryChangedEvent e)
    62.     {
    63.         UpdateElements();
    64.     }
    65.  
    66.     private void OnAttachToPanelEvent(AttachToPanelEvent e)
    67.     {
    68.         UpdateElements();
    69.     }
    70.  
    71.     public void UpdateElements()
    72.     {
    73.         if (RatioWidth <= 0.0f || RatioHeight <= 0.0f)
    74.         {
    75.             style.paddingLeft = 0f;
    76.             style.paddingRight = 0f;
    77.             Debug.LogError($"[AspectRatioPadding] Invalid width:{RatioWidth} or height:{RatioHeight}");
    78.             return;
    79.         }
    80.  
    81.         if (float.IsNaN(resolvedStyle.width) || float.IsNaN(resolvedStyle.height))
    82.         {
    83.             return;
    84.         }
    85.  
    86.         var designRatio = (float)RatioWidth / RatioHeight;
    87.         var currRatio = resolvedStyle.width / resolvedStyle.height;
    88.         var diff = currRatio - designRatio;
    89.  
    90.         if (diff > 0.01f)
    91.         {
    92.             float balance = (float)(balanceLR / 100.0);
    93.             var w = (resolvedStyle.width - (resolvedStyle.height * designRatio))*1.0f ;
    94.             style.paddingLeft = balance * w;
    95.             style.paddingRight =  (1-balance) *w;
    96.         }
    97.         else
    98.         {
    99.             style.paddingLeft = 0f;
    100.             style.paddingRight = 0f;
    101.         }
    102.  
    103.         //if (RatioWidth <= 0.0f || RatioHeight <= 0.0f)
    104.         //{
    105.         //    leftPadding.style.width = 0;
    106.         //    rightPadding.style.width = 0;
    107.         //    Debug.LogError($"[AspectRatioPadding] Invalid width:{RatioWidth} or height:{RatioHeight}");
    108.         //    return;
    109.         //}
    110.  
    111.         //if (float.IsNaN(resolvedStyle.width) || float.IsNaN(resolvedStyle.height))
    112.         //{
    113.         //    return;
    114.         //}
    115.  
    116.         //if (diff > 0.01f)
    117.         //{
    118.         //    var w = (resolvedStyle.width - (resolvedStyle.height * designRatio)) * 0.5f;
    119.         //    leftPadding.style.width = w;
    120.         //    rightPadding.style.width = w;
    121.         //}
    122.         //else
    123.         //{
    124.         //    leftPadding.style.width = 0;
    125.         //    rightPadding.style.width = 0;
    126.         //}
    127.  
    128.         //// make sure the padding elements are at correct positions in hierarchy
    129.         //leftPadding.SendToBack();
    130.         //rightPadding.BringToFront();
    131.     }
    132.  
    133.     // ============================================================================================================
    134. }
    135.  
     
  16. iperezipina

    iperezipina

    Joined:
    Jun 24, 2021
    Posts:
    26
    Iteration, this time it changes width

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3. using UnityEngine.UIElements;
    4.  
    5. [UnityEngine.Scripting.Preserve]
    6. public class AspectRatioWidth : VisualElement
    7. {
    8.     [UnityEngine.Scripting.Preserve]
    9.     public new class UxmlFactory : UxmlFactory<AspectRatioWidth, UxmlTraits> { }
    10.  
    11.     [UnityEngine.Scripting.Preserve]
    12.     public new class UxmlTraits : VisualElement.UxmlTraits
    13.     {
    14.         readonly UxmlIntAttributeDescription RatioWidth = new UxmlIntAttributeDescription { name = "RatioWidth", defaultValue = 16 };
    15.         readonly UxmlIntAttributeDescription RatioHeight = new UxmlIntAttributeDescription { name = "RatioHeight", defaultValue = 9 };
    16.         readonly UxmlIntAttributeDescription balanceLR = new UxmlIntAttributeDescription { name = "balanceLR", defaultValue = 50, restriction = new UxmlValueBounds { min = "0", max = "100" } };
    17.  
    18.         public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
    19.         {
    20.             get { yield break; }
    21.         }
    22.  
    23.         public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
    24.         {
    25.             base.Init(ve, bag, cc);
    26.             AspectRatioWidth ele = ve as AspectRatioWidth;
    27.             ele.RatioWidth = RatioWidth.GetValueFromBag(bag, cc);
    28.             ele.RatioHeight = RatioHeight.GetValueFromBag(bag, cc);
    29.             ele.balanceLR = balanceLR.GetValueFromBag(bag, cc);
    30.         }
    31.     }
    32.  
    33.     public int RatioWidth { get; private set; } = 16;
    34.     public int RatioHeight { get; private set; } = 9;
    35.    
    36.     public int balanceLR { get; private set; } = 50 ;
    37.  
    38.  
    39.     // changed to modifying margin rather than adding new UI elements.
    40.     //private VisualElement leftPadding;
    41.     //private VisualElement rightPadding;
    42.  
    43.     // ------------------------------------------------------------------------------------------------------------
    44.  
    45.     public AspectRatioWidth()
    46.     {
    47.         style.flexDirection = FlexDirection.Row;
    48.         style.flexShrink = 0;
    49.         //style.width = Length.Percent(100);
    50.         style.height = Length.Percent(100);
    51.  
    52.         //leftPadding = new VisualElement() { name = "AspectRatioWidth-Left" };
    53.         //rightPadding = new VisualElement() { name = "AspectRatioWidth-Right" };
    54.  
    55.         //Add(leftPadding);
    56.         //Add(rightPadding);
    57.  
    58.         RegisterCallback<GeometryChangedEvent>(OnGeometryChangedEvent);
    59.         RegisterCallback<AttachToPanelEvent>(OnAttachToPanelEvent);
    60.     }
    61.  
    62.     private void OnGeometryChangedEvent(GeometryChangedEvent e)
    63.     {
    64.         UpdateElements();
    65.     }
    66.  
    67.     private void OnAttachToPanelEvent(AttachToPanelEvent e)
    68.     {
    69.         UpdateElements();
    70.     }
    71.  
    72.     public void UpdateElements()
    73.     {
    74.         if (RatioWidth <= 0.0f || RatioHeight <= 0.0f)
    75.         {
    76.             style.paddingLeft = 0f;
    77.             style.paddingRight = 0f;
    78.             Debug.LogError($"[AspectRatioWidth] Invalid width:{RatioWidth} or height:{RatioHeight}");
    79.             return;
    80.         }
    81.  
    82.         if (float.IsNaN(resolvedStyle.width) || float.IsNaN(resolvedStyle.height))
    83.         {
    84.             return;
    85.         }
    86.  
    87.         var designRatio = (float)RatioWidth / RatioHeight;
    88.         var currRatio = resolvedStyle.width / resolvedStyle.height;
    89.         var diff = currRatio - designRatio;
    90.  
    91.        
    92.             float balance = (float)(balanceLR / 100.0);
    93.             var w = (resolvedStyle.width - (resolvedStyle.height * designRatio))*1.0f ;
    94.             //style.paddingLeft = balance * w;
    95.             //style.paddingRight =  (1-balance) *w;
    96.             style.width = resolvedStyle.height * designRatio;
    97.        
    98.  
    99.         //if (RatioWidth <= 0.0f || RatioHeight <= 0.0f)
    100.         //{
    101.         //    leftPadding.style.width = 0;
    102.         //    rightPadding.style.width = 0;
    103.         //    Debug.LogError($"[AspectRatioWidth] Invalid width:{RatioWidth} or height:{RatioHeight}");
    104.         //    return;
    105.         //}
    106.  
    107.         //if (float.IsNaN(resolvedStyle.width) || float.IsNaN(resolvedStyle.height))
    108.         //{
    109.         //    return;
    110.         //}
    111.  
    112.         //if (diff > 0.01f)
    113.         //{
    114.         //    var w = (resolvedStyle.width - (resolvedStyle.height * designRatio)) * 0.5f;
    115.         //    leftPadding.style.width = w;
    116.         //    rightPadding.style.width = w;
    117.         //}
    118.         //else
    119.         //{
    120.         //    leftPadding.style.width = 0;
    121.         //    rightPadding.style.width = 0;
    122.         //}
    123.  
    124.         //// make sure the padding elements are at correct positions in hierarchy
    125.         //leftPadding.SendToBack();
    126.         //rightPadding.BringToFront();
    127.     }
    128.  
    129.     // ============================================================================================================
    130. }
    131.  
     
  17. PaulMDev

    PaulMDev

    Joined:
    Feb 19, 2019
    Posts:
    72
    Can something similar be added in the UI Builder ?
    Using custom scripts is great but it would be better to have this by default. Currently you can't make something as simple as hearts to represent the health bar in a game without using custom scripts.
     
    Neiist, jwinn, jowseyy and 2 others like this.
  18. Phenotype

    Phenotype

    Joined:
    Oct 26, 2010
    Posts:
    53
    I get a, "Layout update is struggling to process current layout (consider simplifying to avoid recursive layout)" error when trying this. Changing the style values width and height must retrigger OnGeometryChangedEvent. How to do prevent the recursion?
     
  19. JuliaP_Unity

    JuliaP_Unity

    Unity Technologies

    Joined:
    Mar 26, 2020
    Posts:
    700
    You can unassign the OnGeometryChangedEvent callback while you're setting the values, and then reassign it once you're done with it. Hope this helps!
     
    Phenotype likes this.
  20. jwinn

    jwinn

    Joined:
    Sep 1, 2012
    Posts:
    88
    This. I wanted to follow up about whether there are plans to add this to UI Toolkit / UI Builder? There also is a related thread where someone pointed out that aspect-ratio already exists within Yoga (the layout engine used):

    https://yogalayout.com/docs/aspect-ratio

    I just used UI Builder for the first time over the last few days (I use flexbox daily as a web developer), and the lack of this feature and lack of an image element were the first puzzling barriers I hit. The padding trick workaround is only viable if you're setting a width, and the only other option right now seems to be using a custom script. We need the ability to keep images at their aspect ratios in dynamically sized layouts. And to use specific aspect ratios for other VisualElements.
     
    PaulMDev likes this.
  21. neoRiley

    neoRiley

    Joined:
    Dec 12, 2008
    Posts:
    162
    @pbhogan - thanks for posting your code in Git! really appreciate the jump start.

    I modified it to allow for editing position/alignment in the UIBuilder and added a "Scale" property. This allows me to modify in UIBuilder and I'm off and running ;)

    upload_2023-7-9_17-28-2.png

    Code (CSharp):
    1. /*
    2. This is free and unencumbered software released into the public
    3. domain.
    4.  
    5. Anyone is free to copy, modify, publish, use, compile, sell, or
    6. distribute this software, either in source code form or as a compiled
    7. binary, for any purpose, commercial or non-commercial, and by any
    8. means.
    9.  
    10. In jurisdictions that recognize copyright laws, the author or authors
    11. of this software dedicate any and all copyright interest in the
    12. software to the public domain. We make this dedication for the
    13. benefit of the public at large and to the detriment of our heirs and
    14. successors. We intend this dedication to be an overt act of
    15. relinquishment in perpetuity of all present and future rights to this
    16. software under copyright law.
    17.  
    18. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    19. EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    20. MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    21. NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
    22. CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
    23. TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    24. SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    25. */
    26.  
    27. namespace GallantGames.UI
    28. {
    29.     using System.Collections.Generic;
    30.     using UnityEngine;
    31.     using UnityEngine.UIElements;
    32.  
    33.  
    34.     [UnityEngine.Scripting.Preserve]
    35.     public class AspectRatioPanel : VisualElement
    36.     {
    37.         [UnityEngine.Scripting.Preserve]
    38.         public new class UxmlFactory : UxmlFactory<AspectRatioPanel, UxmlTraits> {}
    39.  
    40.         [UnityEngine.Scripting.Preserve]
    41.         public new class UxmlTraits : VisualElement.UxmlTraits
    42.         {
    43.             readonly UxmlIntAttributeDescription aspectRatioX = new() { name = "aspect-ratio-x", defaultValue = 16, restriction = new UxmlValueBounds { min = "1" } };
    44.             readonly UxmlIntAttributeDescription aspectRatioY = new() { name = "aspect-ratio-y", defaultValue = 9, restriction = new UxmlValueBounds { min = "1" } };
    45.             readonly UxmlFloatAttributeDescription scale = new() { name = "scale", defaultValue = 1f };
    46.  
    47.  
    48.             public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
    49.             {
    50.                 get { yield break; }
    51.             }
    52.  
    53.  
    54.             public override void Init( VisualElement visualElement, IUxmlAttributes attributes, CreationContext creationContext )
    55.             {
    56.                 base.Init( visualElement, attributes, creationContext );
    57.                 var element = visualElement as AspectRatioPanel;
    58.                 if (element != null)
    59.                 {
    60.                     element.AspectRatioX = Mathf.Max( 1, aspectRatioX.GetValueFromBag( attributes, creationContext ) );
    61.                     element.AspectRatioY = Mathf.Max( 1, aspectRatioY.GetValueFromBag( attributes, creationContext ) );
    62.                     element.Scale = scale.GetValueFromBag( attributes, creationContext );
    63.                     element.FitToParent();
    64.                 }
    65.             }
    66.         }
    67.  
    68.  
    69.         public int AspectRatioX { get; private set; } = 16;
    70.         public int AspectRatioY { get; private set; } = 9;
    71.         public float Scale { get; private set; } = 1;
    72.  
    73.  
    74.         public AspectRatioPanel()
    75.         {
    76.             style.position = Position.Relative;
    77.             style.left = StyleKeyword.Auto;
    78.             style.top = StyleKeyword.Auto;
    79.             style.right = StyleKeyword.Auto;
    80.             style.bottom = StyleKeyword.Auto;
    81.             RegisterCallback<AttachToPanelEvent>( OnAttachToPanelEvent );
    82.         }
    83.  
    84.  
    85.         void OnAttachToPanelEvent( AttachToPanelEvent e )
    86.         {
    87.             parent?.RegisterCallback<GeometryChangedEvent>( OnGeometryChangedEvent );
    88.             FitToParent();
    89.         }
    90.  
    91.  
    92.         void OnGeometryChangedEvent( GeometryChangedEvent e )
    93.         {
    94.             FitToParent();
    95.         }
    96.  
    97.  
    98.         void FitToParent()
    99.         {
    100.             if (parent == null) return;
    101.             var parentW = parent.resolvedStyle.width;
    102.             var parentH = parent.resolvedStyle.height;
    103.             if (float.IsNaN( parentW ) || float.IsNaN( parentH )) return;
    104.  
    105.             if (AspectRatioX <= 0.0f || AspectRatioY <= 0.0f)
    106.             {
    107.                 style.width = parentW;
    108.                 style.height = parentH;
    109.                 return;
    110.             }
    111.  
    112.             var ratio = Mathf.Min( parentW / AspectRatioX, parentH / AspectRatioY );
    113.             var targetW = Mathf.Floor( AspectRatioX * ratio );
    114.             var targetH = Mathf.Floor( AspectRatioY * ratio );
    115.             style.width = targetW * Scale;
    116.             style.height = targetH * Scale;
    117.         }
    118.     }
    119. }
     
    SparkesRS likes this.
  22. neoRiley

    neoRiley

    Joined:
    Dec 12, 2008
    Posts:
    162
  23. neoRiley

    neoRiley

    Joined:
    Dec 12, 2008
    Posts:
    162
    Hey Julia, I posted this gist and at one point I thought I took care of that recurssion error, but I saw your post about adding/removing the event listener for geometry changes - could you give me a little more guidance on what you mean and where that should happen? Is it just when a setter is called? or something else?

    Extends VisualElement and provides the ability to maintain an aspect ratio as well as add a Label who's font size is scaled with the AspectRatioPanel (github.com)