VanguardReMaster/client/Assets/Vanguard/Runtime/UI/NeonUi.cs

348 lines
12 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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");
}
}
}