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;
});
}
/// Кнопка со срезанными углами и вертикальным градиентом.
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)
{
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);
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");
}
}
}