using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; namespace Vanguard.UI { /// /// Процедурные 9-slice-подобные спрайты для неонового интерфейса. /// Свечение запекается в текстуру: пост-обработка (bloom) на Screen Space /// Overlay не действует, поэтому glow рисуется в альфе спрайта. /// Все текстуры кешируются по ключу и не сохраняются в сцену. /// public static class NeonUi { private static readonly Dictionary Cache = new Dictionary(); // ------------------------------------------------------------ панели /// Заливка со скруглением и рамкой. Размер — точный, без свечения. public static Sprite Body( int width, int height, int radius, Color fill, Color border, int borderWidth) { var key = string.Format( "body:{0}x{1}:{2}:{3}:{4}:{5}", width, height, radius, Hex(fill), Hex(border), borderWidth); return Get(key, width, height, (dx, dy) => { var halfWidth = width * 0.5f; var halfHeight = height * 0.5f; var distance = RoundedDistance(dx, dy, halfWidth, halfHeight, radius); if (distance > 0f) return Color.clear; if (borderWidth > 0 && distance > -borderWidth) return border; return fill; }); } /// /// Растягиваемая панель: тот же вид, но с 9-slice-рамкой, поэтому /// скругление не искажается на больших размерах. /// public static Sprite BodySliced( int radius, Color fill, Color border, int borderWidth) { var slice = radius + borderWidth + 2; var size = slice * 2 + 8; var key = string.Format( "bodyslice:{0}:{1}:{2}:{3}", radius, Hex(fill), Hex(border), borderWidth); return Get( key, size, size, (dx, dy) => { var half = size * 0.5f; var distance = RoundedDistance(dx, dy, half, half, radius); if (distance > 0f) return Color.clear; if (borderWidth > 0 && distance > -borderWidth) return border; return fill; }, slice); } /// Кнопка со срезанными углами и вертикальным градиентом. public static Sprite Bevel( int width, int height, int cut, Color top, Color bottom, Color border, int borderWidth) { var key = string.Format( "bevel:{0}x{1}:{2}:{3}:{4}:{5}:{6}", width, height, cut, Hex(top), Hex(bottom), Hex(border), borderWidth); return Get(key, width, height, (dx, dy) => { var halfWidth = width * 0.5f; var halfHeight = height * 0.5f; var distance = BevelDistance(dx, dy, halfWidth, halfHeight, cut); if (distance > 0f) return Color.clear; if (borderWidth > 0 && distance > -borderWidth) return border; var t = Mathf.Clamp01((dy + halfHeight) / height); return Color.Lerp(bottom, top, t); }); } /// Мягкое свечение вокруг контура; рисуется позади Body. public static Sprite Glow( int width, int height, int radius, int spread, Color color) { var key = string.Format( "glow:{0}x{1}:{2}:{3}:{4}", width, height, radius, spread, Hex(color)); var innerWidth = Mathf.Max(1f, width * 0.5f - spread); var innerHeight = Mathf.Max(1f, height * 0.5f - spread); return Get(key, width, height, (dx, dy) => { var distance = RoundedDistance(dx, dy, innerWidth, innerHeight, radius); if (distance <= 0f) return Color.clear; var t = 1f - distance / spread; if (t <= 0f) return Color.clear; return new Color(color.r, color.g, color.b, color.a * t * t); }); } /// Радиальное затемнение по краям экрана. public static Sprite Vignette(int width, int height, Color inner, Color outer) { var key = "vignette:" + width + "x" + height + ":" + Hex(inner) + ":" + Hex(outer); var halfWidth = width * 0.5f; var halfHeight = height * 0.5f; var maxDistance = Mathf.Sqrt(halfWidth * halfWidth + halfHeight * halfHeight); return Get(key, width, height, (dx, dy) => { var distance = Mathf.Sqrt(dx * dx + dy * dy) / maxDistance; var t = Mathf.Clamp01((distance - 0.35f) / 0.65f); return Color.Lerp(inner, outer, t * t); }); } // ------------------------------------------------------------ фигуры для иконок public static Sprite Circle(int diameter, Color color) { var key = "circle:" + diameter + ":" + Hex(color); var radius = diameter * 0.5f - 0.5f; return Get(key, diameter, diameter, (dx, dy) => { var distance = Mathf.Sqrt(dx * dx + dy * dy) - radius; return distance > 0f ? Color.clear : Edge(color, distance); }); } public static Sprite Ring(int diameter, float thickness, Color color) { var key = "ring:" + diameter + ":" + thickness + ":" + Hex(color); var radius = diameter * 0.5f - thickness * 0.5f - 0.5f; var half = thickness * 0.5f; return Get(key, diameter, diameter, (dx, dy) => { var distance = Mathf.Abs(Mathf.Sqrt(dx * dx + dy * dy) - radius) - half; return distance > 0f ? Color.clear : Edge(color, distance); }); } public static Sprite HorizontalBar(int width, int height, Color color) { var key = "bar:" + width + "x" + height + ":" + Hex(color); var halfWidth = width * 0.5f; var halfHeight = height * 0.5f; return Get(key, width, height, (dx, dy) => { var distance = Mathf.Max( Mathf.Abs(dx) - halfWidth, Mathf.Abs(dy) - halfHeight); return distance > 0f ? Color.clear : Edge(color, distance); }); } // ------------------------------------------------------------ хелперы /// Полупрозрачная сглаженная кромка в 1 px. private static Color Edge(Color color, float distance) { var alpha = Mathf.Clamp01(-distance); return new Color(color.r, color.g, color.b, color.a * alpha); } public static Image AddImage( Transform parent, string name, Sprite sprite, Vector2 size, Vector2 position, bool raycast = false) { var go = new GameObject(name, typeof(RectTransform), typeof(Image)); var rect = (RectTransform)go.transform; rect.SetParent(parent, false); rect.anchorMin = new Vector2(0.5f, 0.5f); rect.anchorMax = new Vector2(0.5f, 0.5f); rect.pivot = new Vector2(0.5f, 0.5f); rect.sizeDelta = size; rect.anchoredPosition = position; var image = go.GetComponent(); image.sprite = sprite; image.type = Image.Type.Simple; image.preserveAspect = true; image.raycastTarget = raycast; return image; } /// Image с якорем к углу экрана (для тэглайна, копирайта, меню). public static RectTransform AddAnchored( Transform parent, string name, Sprite sprite, Vector2 size, Vector2 anchor, Vector2 pivot, Vector2 offset) { var go = new GameObject(name, typeof(RectTransform), typeof(Image)); var rect = (RectTransform)go.transform; rect.SetParent(parent, false); rect.anchorMin = anchor; rect.anchorMax = anchor; rect.pivot = pivot; rect.sizeDelta = size; rect.anchoredPosition = offset; rect.localScale = Vector3.one; var image = go.GetComponent(); image.sprite = sprite; image.type = Image.Type.Simple; image.preserveAspect = true; image.raycastTarget = false; return rect; } private static Sprite Get( string key, int width, int height, Func shader, int slice = 0) { if (Cache.TryGetValue(key, out var cached) && cached != null) return cached; width = Mathf.Max(2, width); height = Mathf.Max(2, height); var pixels = new Color32[width * height]; var halfWidth = width * 0.5f; var halfHeight = height * 0.5f; for (var y = 0; y < height; y++) { var dy = y + 0.5f - halfHeight; for (var x = 0; x < width; x++) { var dx = x + 0.5f - halfWidth; pixels[y * width + x] = shader(dx, dy); } } var texture = new Texture2D(width, height, TextureFormat.RGBA32, false) { hideFlags = HideFlags.DontSave, wrapMode = TextureWrapMode.Clamp, filterMode = FilterMode.Bilinear, name = "NeonUi " + key, }; texture.SetPixels32(pixels); texture.Apply(false, false); var sprite = Sprite.Create( texture, new Rect(0f, 0f, width, height), new Vector2(0.5f, 0.5f), 100f, 0, SpriteMeshType.FullRect, new Vector4(slice, slice, slice, slice)); sprite.name = key; sprite.hideFlags = HideFlags.DontSave; Cache[key] = sprite; return sprite; } private static float RoundedDistance(float dx, float dy, float halfWidth, float halfHeight, float radius) { radius = Mathf.Clamp(radius, 0f, Mathf.Min(halfWidth, halfHeight) - 1f); var qx = Mathf.Abs(dx) - (halfWidth - radius); var qy = Mathf.Abs(dy) - (halfHeight - radius); var ax = Mathf.Max(qx, 0f); var ay = Mathf.Max(qy, 0f); return Mathf.Sqrt(ax * ax + ay * ay) + Mathf.Min(Mathf.Max(qx, qy), 0f) - radius; } private static float BevelDistance(float dx, float dy, float halfWidth, float halfHeight, float cut) { var axis = Mathf.Max( Mathf.Abs(dx) - halfWidth, Mathf.Abs(dy) - halfHeight); var diagonal = (Mathf.Abs(dx) + Mathf.Abs(dy) - (halfWidth + halfHeight - cut)) * 0.70710678f; return Mathf.Max(axis, diagonal); } private static string Hex(Color color) { return ((int)(color.r * 255)).ToString("X2") + ((int)(color.g * 255)).ToString("X2") + ((int)(color.b * 255)).ToString("X2") + ((int)(color.a * 255)).ToString("X2"); } } }