diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..46f3536 --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 07f120a4700a2d2468e29d1ec6cac28f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/GradientImageEditor.cs b/Editor/GradientImageEditor.cs new file mode 100644 index 0000000..8f65459 --- /dev/null +++ b/Editor/GradientImageEditor.cs @@ -0,0 +1,163 @@ +using System.Linq; +using UnityEditor; +using UnityEditor.AnimatedValues; +using UnityEngine; +using UnityEngine.UI; + +namespace Gilzoide.GradientRect.Editor +{ + [CustomEditor(typeof(GradientImage))] + public class GradientImageEditor : GradientRectEditor + { + private SerializedProperty _sprite; + private SerializedProperty _type; + private SerializedProperty _fillCenter; + private SerializedProperty _pixelsPerUnitMultiplier; + + private AnimBool m_ShowSlicedOrTiled; + private AnimBool m_ShowSliced; + private AnimBool m_ShowTiled; + private AnimBool m_ShowFilled; + + private AnimBool m_ShowType; + + protected override void OnEnable() + { + base.OnEnable(); + + _sprite = serializedObject.FindProperty("_sprite"); + _type = serializedObject.FindProperty("_type"); + _fillCenter = serializedObject.FindProperty("_fillCenter"); + _pixelsPerUnitMultiplier = serializedObject.FindProperty("_pixelsPerUnitMultiplier"); + + Image.Type enumValueIndex = (Image.Type)_type.enumValueIndex; + + m_ShowSlicedOrTiled = new AnimBool(!_type.hasMultipleDifferentValues && enumValueIndex == Image.Type.Sliced); + m_ShowSlicedOrTiled.valueChanged.AddListener(Repaint); + + m_ShowSliced = new AnimBool(!_type.hasMultipleDifferentValues && enumValueIndex == Image.Type.Sliced); + m_ShowSliced.valueChanged.AddListener(Repaint); + + m_ShowTiled = new AnimBool(!_type.hasMultipleDifferentValues && enumValueIndex == Image.Type.Tiled); + m_ShowTiled.valueChanged.AddListener(Repaint); + + m_ShowFilled = new AnimBool(!_type.hasMultipleDifferentValues && enumValueIndex == Image.Type.Filled); + m_ShowFilled.valueChanged.AddListener(Repaint); + + m_ShowType = new AnimBool(_sprite.objectReferenceValue != null); + m_ShowType.valueChanged.AddListener(Repaint); + } + + protected override void OnDisable() + { + base.OnDisable(); + + m_ShowType.valueChanged.RemoveListener(Repaint); + m_ShowSlicedOrTiled.valueChanged.RemoveListener(Repaint); + m_ShowSliced.valueChanged.RemoveListener(Repaint); + + m_ShowTiled.valueChanged.RemoveListener(Repaint); + m_ShowFilled.valueChanged.RemoveListener(Repaint); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + SpriteGUI(); + + AppearanceControlsGUI(); + DrawGradientPropertys(); + RaycastControlsGUI(); + MaskableControlsGUI(); + + m_ShowType.target = _sprite.objectReferenceValue != null; + if (EditorGUILayout.BeginFadeGroup(m_ShowType.faded)) + { + TypeGUI(); + } + EditorGUILayout.EndFadeGroup(); + + serializedObject.ApplyModifiedProperties(); + } + + protected void SpriteGUI() + { + EditorGUI.BeginChangeCheck(); + EditorGUILayout.PropertyField(_sprite); + if (!EditorGUI.EndChangeCheck()) + { + return; + } + + Sprite sprite = _sprite.objectReferenceValue as Sprite; + if (sprite) + { + Image.Type enumValueIndex = (Image.Type)_type.enumValueIndex; + if (sprite.border.SqrMagnitude() > 0f) + { + _type.enumValueIndex = 1; + } + else if (enumValueIndex == Image.Type.Sliced) + { + _type.enumValueIndex = 0; + } + } + } + + protected void TypeGUI() + { + EditorGUILayout.PropertyField(_type, EditorGUIUtility.TrTextContent("Image Type")); + EditorGUI.indentLevel++; + Image.Type enumValueIndex = (Image.Type)_type.enumValueIndex; + bool flag = !_type.hasMultipleDifferentValues && (enumValueIndex == Image.Type.Sliced || enumValueIndex == Image.Type.Tiled); + if (flag && targets.Length > 1) + { + flag = targets.Select((Object obj) => obj as Image).All((Image img) => img.hasBorder); + } + + m_ShowSlicedOrTiled.target = flag; + m_ShowSliced.target = flag && !_type.hasMultipleDifferentValues && enumValueIndex == Image.Type.Sliced; + m_ShowTiled.target = flag && !_type.hasMultipleDifferentValues && enumValueIndex == Image.Type.Tiled; + m_ShowFilled.target = !_type.hasMultipleDifferentValues && enumValueIndex == Image.Type.Filled; + + GradientImage image = target as GradientImage; + + if (EditorGUILayout.BeginFadeGroup(m_ShowSliced.faded)) + //if (EditorGUILayout.BeginFadeGroup(m_ShowSlicedOrTiled.faded)) + { + if (image.HasBorder) + { + EditorGUILayout.PropertyField(_fillCenter); + } + + EditorGUILayout.PropertyField(_pixelsPerUnitMultiplier); + + _pixelsPerUnitMultiplier.floatValue = Mathf.Max(0.01f, _pixelsPerUnitMultiplier.floatValue); + } + EditorGUILayout.EndFadeGroup(); + + if (EditorGUILayout.BeginFadeGroup(m_ShowSliced.faded) && image.Sprite != null && !image.HasBorder) + { + EditorGUILayout.HelpBox("This Image doesn't have a border.", MessageType.Warning); + } + EditorGUILayout.EndFadeGroup(); + + if (EditorGUILayout.BeginFadeGroup(m_ShowTiled.faded) && image.Sprite != null && !image.HasBorder && ((image.Sprite.texture != null && image.Sprite.texture.wrapMode != TextureWrapMode.Repeat) || image.Sprite.packed)) + { + EditorGUILayout.HelpBox("This type unsoported to paint image.", MessageType.Error); + EditorGUILayout.HelpBox("It looks like you want to tile a sprite with no border. It would be more efficient to modify the Sprite properties, clear the Packing tag and set the Wrap mode to Repeat.", MessageType.Warning); + } + EditorGUILayout.EndFadeGroup(); + + if (EditorGUILayout.BeginFadeGroup(m_ShowFilled.faded)) + { + // TODO: code for editor for filled type image + EditorGUILayout.HelpBox("This type unsoported to paint image.", MessageType.Error); + } + EditorGUILayout.EndFadeGroup(); + + EditorGUI.indentLevel--; + } + } +} \ No newline at end of file diff --git a/Editor/GradientImageEditor.cs.meta b/Editor/GradientImageEditor.cs.meta new file mode 100644 index 0000000..c6dbbcb --- /dev/null +++ b/Editor/GradientImageEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ae45e5b4c3948ba45bada172ba0843d8 \ No newline at end of file diff --git a/Editor/GradientRectEditor.cs b/Editor/GradientRectEditor.cs new file mode 100644 index 0000000..5c540c2 --- /dev/null +++ b/Editor/GradientRectEditor.cs @@ -0,0 +1,38 @@ +using UnityEditor; +using UnityEditor.UI; + +namespace Gilzoide.GradientRect.Editor +{ + [CustomEditor(typeof(GradientRect))] + public class GradientRectEditor : GraphicEditor + { + private SerializedProperty _gradient; + private SerializedProperty _direction; + + protected override void OnEnable() + { + base.OnEnable(); + _gradient = serializedObject.FindProperty("_gradient"); + _direction = serializedObject.FindProperty("_direction"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + AppearanceControlsGUI(); + DrawGradientPropertys(); + RaycastControlsGUI(); + MaskableControlsGUI(); + + serializedObject.ApplyModifiedProperties(); + } + + + protected virtual void DrawGradientPropertys() + { + EditorGUILayout.PropertyField(_gradient); + EditorGUILayout.PropertyField(_direction); + } + } +} \ No newline at end of file diff --git a/Editor/GradientRectEditor.cs.meta b/Editor/GradientRectEditor.cs.meta new file mode 100644 index 0000000..4483d70 --- /dev/null +++ b/Editor/GradientRectEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d3867f895b9c4e74aa88c7b3ba321b44 \ No newline at end of file diff --git a/README.md b/README.md index 5d0c634..be8679c 100644 --- a/README.md +++ b/README.md @@ -33,4 +33,4 @@ Either: - [GradientTexture](Runtime/GradientTexture.cs): texture with configurable UV, tinted with Gradient colors. - [GradientImage](Runtime/GradientImage.cs): sprite tinted with Gradient colors. UVs are automatically fetched from sprite data. - Only simple filling is supported, no slicing nor tiling. \ No newline at end of file + Only simple filling and slicing is supported, not tiling. \ No newline at end of file diff --git a/Runtime/GradientImage.cs b/Runtime/GradientImage.cs index df6dc8e..ab12524 100644 --- a/Runtime/GradientImage.cs +++ b/Runtime/GradientImage.cs @@ -1,24 +1,76 @@ using UnityEngine; +using UnityEngine.Sprites; +using UnityEngine.UI; namespace Gilzoide.GradientRect { [RequireComponent(typeof(CanvasRenderer))] public class GradientImage : GradientRect { - [Header("Texture")] [SerializeField] protected Sprite _sprite; - /// Sprite used to draw Image. - /// For now, the only fill supported is simple mode, no slicing nor tiling. + [SerializeField] private Image.Type _type = Image.Type.Simple; + [SerializeField] private bool _fillCenter = true; + [SerializeField] private float _pixelsPerUnitMultiplier = 1f; + + private static readonly Vector2[] s_VertScratch = new Vector2[4]; + private static readonly Vector2[] s_UVScratch = new Vector2[4]; + + public float PixelsPerUnitMultiplier + { + get => _pixelsPerUnitMultiplier; + set + { + _pixelsPerUnitMultiplier = Mathf.Max(0.01f, value); + SetVerticesDirty(); + } + } + + public Image.Type Type + { + get => _type; + set + { + if (_type == value) + return; + + _type = value; + SetVerticesDirty(); + } + } + + public bool FillCenter + { + get => _fillCenter; + set + { + if (_fillCenter == value) + return; + + _fillCenter = value; + SetVerticesDirty(); + } + } + + public bool HasBorder + { + get + { + if (_sprite == null) + return false; + + Vector4 v = _sprite.border; + return v.sqrMagnitude > 0f; + } + } + public Sprite Sprite { get => _sprite; set { if (_sprite == value) - { return; - } _sprite = value; SetVerticesDirty(); @@ -30,16 +82,169 @@ public override Texture mainTexture { get { - if (_sprite == null) + if (_sprite != null) + return _sprite.texture; + + if (material != null && material.mainTexture != null) + return material.mainTexture; + + return s_WhiteTexture; + } + } + + protected override void OnPopulateMesh(VertexHelper vh) + { + switch (_type) + { + case Image.Type.Simple: + base.OnPopulateMesh(vh); + break; + case Image.Type.Sliced: + GenerateSlicedSprite(vh); + break; + default: + vh.Clear(); + Debug.LogWarning($"Unsupported image type: {_type}"); + break; + } + } + + private void GenerateSlicedSprite(VertexHelper vh) + { + if (!HasBorder) + { + base.OnPopulateMesh(vh); + return; + } + + Vector4 outer, inner, padding, border; + + if (_sprite != null) + { + outer = DataUtility.GetOuterUV(_sprite); + inner = DataUtility.GetInnerUV(_sprite); + padding = DataUtility.GetPadding(_sprite); + border = _sprite.border; + } + else + { + outer = Vector4.zero; + inner = Vector4.zero; + padding = Vector4.zero; + border = Vector4.zero; + } + + Rect rect = GetPixelAdjustedRect(); + + Vector4 adjustedBorders = GetAdjustedBorders(border / _pixelsPerUnitMultiplier, rect); + padding = padding / _pixelsPerUnitMultiplier; + + s_VertScratch[0] = new Vector2(padding.x, padding.y); + s_VertScratch[3] = new Vector2(rect.width - padding.z, rect.height - padding.w); + + s_VertScratch[1].x = adjustedBorders.x; + s_VertScratch[1].y = adjustedBorders.y; + + s_VertScratch[2].x = rect.width - adjustedBorders.z; + s_VertScratch[2].y = rect.height - adjustedBorders.w; + + for (int i = 0; i < 4; ++i) + { + s_VertScratch[i].x += rect.x; + s_VertScratch[i].y += rect.y; + } + + s_UVScratch[0] = new Vector2(outer.x, outer.y); + s_UVScratch[1] = new Vector2(inner.x, inner.y); + s_UVScratch[2] = new Vector2(inner.z, inner.w); + s_UVScratch[3] = new Vector2(outer.z, outer.w); + + vh.Clear(); + + for (int x = 0; x < 3; ++x) + { + int x2 = x + 1; + + for (int y = 0; y < 3; ++y) { - if (material != null && material.mainTexture != null) - { - return material.mainTexture; - } - return s_WhiteTexture; + if (!_fillCenter && x == 1 && y == 1) + continue; + + int y2 = y + 1; + + if ((s_VertScratch[x2].x - s_VertScratch[x].x <= 0) || (s_VertScratch[y2].y - s_VertScratch[y].y <= 0)) + continue; + + Vector2 bottomLeft = new Vector2(s_VertScratch[x].x, s_VertScratch[y].y); + Vector2 topRight = new Vector2(s_VertScratch[x2].x, s_VertScratch[y2].y); + + Vector2 normalizedBottomLeft = new Vector2( + (bottomLeft.x - rect.x) / rect.width, + (bottomLeft.y - rect.y) / rect.height + ); + Vector2 normalizedTopLeft = new Vector2( + (bottomLeft.x - rect.x) / rect.width, + (topRight.y - rect.y) / rect.height + ); + Vector2 normalizedTopRight = new Vector2( + (topRight.x - rect.x) / rect.width, + (topRight.y - rect.y) / rect.height + ); + Vector2 normalizedBottomRight = new Vector2( + (topRight.x - rect.x) / rect.width, + (bottomLeft.y - rect.y) / rect.height + ); + + Color colorBL = _gradient.Evaluate(GetGradientTime(normalizedBottomLeft)) * color; + Color colorTL = _gradient.Evaluate(GetGradientTime(normalizedTopLeft)) * color; + Color colorTR = _gradient.Evaluate(GetGradientTime(normalizedTopRight)) * color; + Color colorBR = _gradient.Evaluate(GetGradientTime(normalizedBottomRight)) * color; + + AddQuad(vh, + bottomLeft, + topRight, + colorBL, colorTL, colorTR, colorBR, + new Vector2(s_UVScratch[x].x, s_UVScratch[y].y), + new Vector2(s_UVScratch[x2].x, s_UVScratch[y2].y)); + } + } + } + + private Vector4 GetAdjustedBorders(Vector4 border, Rect rect) + { + Rect originalRect = rectTransform.rect; + + for (int axis = 0; axis <= 1; axis++) + { + float borderScaleRatio; + + if (originalRect.size[axis] != 0) + { + borderScaleRatio = rect.size[axis] / originalRect.size[axis]; + border[axis] *= borderScaleRatio; + border[axis + 2] *= borderScaleRatio; } - return _sprite.texture; + float combinedBorders = border[axis] + border[axis + 2]; + if (rect.size[axis] < combinedBorders && combinedBorders != 0) + { + borderScaleRatio = rect.size[axis] / combinedBorders; + border[axis] *= borderScaleRatio; + border[axis + 2] *= borderScaleRatio; + } + } + return border; + } + + private float GetGradientTime(Vector2 normalizedPos) + { + switch (_direction) + { + case GradientDirection.LeftToRight: return normalizedPos.x; + case GradientDirection.RightToLeft: return 1 - normalizedPos.x; + case GradientDirection.BottomToTop: return normalizedPos.y; + case GradientDirection.TopToBottom: return 1 - normalizedPos.y; + default: return normalizedPos.x; } } @@ -47,7 +252,7 @@ protected override Vector2 GetUVForNormalizedPosition(Vector2 position) { if (_sprite) { - Vector4 outerUV = UnityEngine.Sprites.DataUtility.GetOuterUV(_sprite); + Vector4 outerUV = DataUtility.GetOuterUV(_sprite); return new Vector2( Mathf.Lerp(outerUV.x, outerUV.z, position.x), Mathf.Lerp(outerUV.y, outerUV.w, position.y) @@ -58,5 +263,20 @@ protected override Vector2 GetUVForNormalizedPosition(Vector2 position) return position; } } + + private static void AddQuad(VertexHelper vertexHelper, Vector2 posMin, Vector2 posMax, + Color colorBottomLeft, Color colorTopLeft, Color colorTopRight, Color colorBottomRight, + Vector2 uvMin, Vector2 uvMax) + { + int startIndex = vertexHelper.currentVertCount; + + vertexHelper.AddVert(new Vector3(posMin.x, posMin.y, 0), colorBottomLeft, new Vector2(uvMin.x, uvMin.y)); + vertexHelper.AddVert(new Vector3(posMin.x, posMax.y, 0), colorTopLeft, new Vector2(uvMin.x, uvMax.y)); + vertexHelper.AddVert(new Vector3(posMax.x, posMax.y, 0), colorTopRight, new Vector2(uvMax.x, uvMax.y)); + vertexHelper.AddVert(new Vector3(posMax.x, posMin.y, 0), colorBottomRight, new Vector2(uvMax.x, uvMin.y)); + + vertexHelper.AddTriangle(startIndex, startIndex + 1, startIndex + 2); + vertexHelper.AddTriangle(startIndex + 2, startIndex + 3, startIndex); + } } } diff --git a/Runtime/GradientRect.cs b/Runtime/GradientRect.cs index 52d92a3..96d7fac 100644 --- a/Runtime/GradientRect.cs +++ b/Runtime/GradientRect.cs @@ -8,6 +8,7 @@ namespace Gilzoide.GradientRect [RequireComponent(typeof(CanvasRenderer))] public class GradientRect : MaskableGraphic { + [Serializable] public enum GradientDirection { LeftToRight, @@ -16,7 +17,6 @@ public enum GradientDirection TopToBottom, } - [Header("Gradient")] [SerializeField] protected Gradient _gradient; [SerializeField] protected GradientDirection _direction; @@ -79,7 +79,7 @@ protected override void OnPopulateMesh(VertexHelper vh) c1 = c2 = color1 * tint; c3 = c4 = color2 * tint; break; - + case GradientDirection.RightToLeft: v1 = new Vector2(1 - time2, 0); v2 = new Vector2(1 - time2, 1); @@ -88,7 +88,7 @@ protected override void OnPopulateMesh(VertexHelper vh) c1 = c2 = color2 * tint; c3 = c4 = color1 * tint; break; - + case GradientDirection.BottomToTop: v1 = new Vector2(0, time1); v2 = new Vector2(0, time2); @@ -106,7 +106,7 @@ protected override void OnPopulateMesh(VertexHelper vh) c1 = c4 = color2 * tint; c2 = c3 = color1 * tint; break; - + default: throw new ArgumentOutOfRangeException(nameof(_direction)); }