348 lines
12 KiB
C#
348 lines
12 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
|
||
namespace Vanguard.UI
|
||
{
|
||
/// <summary>
|
||
/// Процедурные 9-slice-подобные спрайты для неонового интерфейса.
|
||
/// Свечение запекается в текстуру: пост-обработка (bloom) на Screen Space
|
||
/// Overlay не действует, поэтому glow рисуется в альфе спрайта.
|
||
/// Все текстуры кешируются по ключу и не сохраняются в сцену.
|
||
/// </summary>
|
||
public static class NeonUi
|
||
{
|
||
private static readonly Dictionary<string, Sprite> Cache =
|
||
new Dictionary<string, Sprite>();
|
||
|
||
// ------------------------------------------------------------ панели
|
||
|
||
/// <summary>Заливка со скруглением и рамкой. Размер — точный, без свечения.</summary>
|
||
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;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// Растягиваемая панель: тот же вид, но с 9-slice-рамкой, поэтому
|
||
/// скругление не искажается на больших размерах.
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>Кнопка со срезанными углами и вертикальным градиентом.</summary>
|
||
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);
|
||
});
|
||
}
|
||
|
||
/// <summary>Мягкое свечение вокруг контура; рисуется позади Body.</summary>
|
||
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);
|
||
});
|
||
}
|
||
|
||
/// <summary>Радиальное затемнение по краям экрана.</summary>
|
||
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);
|
||
});
|
||
}
|
||
|
||
// ------------------------------------------------------------ хелперы
|
||
|
||
/// <summary>Полупрозрачная сглаженная кромка в 1 px.</summary>
|
||
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>();
|
||
image.sprite = sprite;
|
||
image.type = Image.Type.Simple;
|
||
image.preserveAspect = true;
|
||
image.raycastTarget = raycast;
|
||
return image;
|
||
}
|
||
|
||
/// <summary>Image с якорем к углу экрана (для тэглайна, копирайта, меню).</summary>
|
||
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>();
|
||
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<float, float, Color> 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");
|
||
}
|
||
}
|
||
}
|