using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace RR.Common.UI.FX
{
///
/// Мягкое свечение для кнопок при фокусе с клавиатуры/джойстика или наведении мышью.
///
[DisallowMultipleComponent]
[AddComponentMenu("RR/UI/FX/UI Selectable Focus Glow")]
public sealed class UISelectableFocusGlow : MonoBehaviour, ISelectHandler, IDeselectHandler, IPointerEnterHandler, IPointerExitHandler
{
private const int TextureSize = 96;
private static Sprite _glowSprite;
[SerializeField] private Color glowColor = new(1f, 0.92f, 0.38f, 1f);
[SerializeField] private bool useButtonColor = true;
[SerializeField, Range(0f, 1f)] private float colorLighten = 0.35f;
[SerializeField, Range(0f, 1f)] private float maxAlpha = 0.55f;
[SerializeField, Min(0f)] private float outlineDistance = 3f;
[SerializeField] private bool useTargetSpriteShape = true;
[SerializeField, Range(4, 16)] private int outlineSamples = 8;
[SerializeField, Range(1, 4)] private int blurLayers = 2;
[SerializeField, Min(0f)] private float blurSpread = 1.5f;
[SerializeField, Min(0.01f)] private float fadeSpeed = 12f;
[SerializeField, Min(0f)] private float pulseScale = 0.025f;
[SerializeField, Min(0.01f)] private float pulseDuration = 1.1f;
private Selectable _selectable;
private Image _targetImage;
private Image _glowImage;
private RectTransform _glowRect;
private GameObject _glowObject;
private readonly List _glowLayers = new();
private bool _isPointerInside;
private float _currentAlpha;
private void Awake()
{
ResolveReferences();
EnsureGlowView();
SetGlowAlpha(0f);
}
private void OnEnable()
{
ResolveReferences();
EnsureGlowView();
SyncGlowRect(1f);
}
private void OnDisable()
{
_isPointerInside = false;
SetGlowAlpha(0f);
}
private void Update()
{
EnsureGlowView();
float targetAlpha = IsFocusedNow() ? maxAlpha : 0f;
_currentAlpha = Mathf.MoveTowards(_currentAlpha, targetAlpha, fadeSpeed * Time.unscaledDeltaTime);
SetGlowAlpha(_currentAlpha);
if (_glowRect != null)
{
float wave = (Mathf.Sin(Time.unscaledTime / pulseDuration * Mathf.PI * 2f) + 1f) * 0.5f;
float scale = 1f + wave * pulseScale * Mathf.Clamp01(_currentAlpha / Mathf.Max(0.001f, maxAlpha));
SyncGlowRect(scale);
}
}
public void OnSelect(BaseEventData eventData)
{
_currentAlpha = Mathf.Max(_currentAlpha, maxAlpha * 0.5f);
}
public void OnDeselect(BaseEventData eventData)
{
}
public void OnPointerEnter(PointerEventData eventData)
{
_isPointerInside = true;
_currentAlpha = Mathf.Max(_currentAlpha, maxAlpha * 0.5f);
}
public void OnPointerExit(PointerEventData eventData)
{
_isPointerInside = false;
}
private void ResolveReferences()
{
if (_selectable == null)
{
_selectable = GetComponent();
}
if (_targetImage == null)
{
_targetImage = GetComponent();
}
}
private void EnsureGlowView()
{
if (_glowImage != null)
{
return;
}
Transform glowParent = transform.parent != null ? transform.parent : transform;
_glowObject = new GameObject($"{name} Focus Glow", typeof(RectTransform), typeof(LayoutElement));
_glowObject.transform.SetParent(glowParent, false);
_glowObject.GetComponent().ignoreLayout = true;
_glowRect = _glowObject.GetComponent();
_glowImage = null;
_glowObject.transform.SetSiblingIndex(transform.GetSiblingIndex());
SyncGlowRect(1f);
RebuildGlowLayers();
}
private void OnDestroy()
{
if (_glowObject != null)
{
Destroy(_glowObject);
}
}
private void SyncGlowRect(float scale)
{
if (_glowRect == null)
{
return;
}
RectTransform targetRect = transform as RectTransform;
if (targetRect == null)
{
return;
}
_glowRect.anchorMin = targetRect.anchorMin;
_glowRect.anchorMax = targetRect.anchorMax;
_glowRect.pivot = targetRect.pivot;
_glowRect.anchoredPosition = targetRect.anchoredPosition;
_glowRect.sizeDelta = targetRect.sizeDelta;
_glowRect.localRotation = targetRect.localRotation;
_glowRect.localScale = targetRect.localScale * scale;
if (_glowRect.parent == targetRect.parent)
{
_glowRect.SetSiblingIndex(Mathf.Max(0, targetRect.GetSiblingIndex() - 1));
}
}
private void RebuildGlowLayers()
{
_glowLayers.Clear();
int samples = Mathf.Max(4, outlineSamples);
int layerIndex = 0;
for (int blur = 0; blur < Mathf.Max(1, blurLayers); blur++)
{
float distance = outlineDistance + blur * blurSpread;
for (int sample = 0; sample < samples; sample++)
{
string layerName = $"Focus Outline {layerIndex + 1}";
Transform existingLayer = _glowRect.Find(layerName);
GameObject layerObject = existingLayer != null
? existingLayer.gameObject
: new GameObject(layerName, typeof(RectTransform), typeof(Image));
layerObject.transform.SetParent(_glowRect, false);
layerObject.transform.SetSiblingIndex(layerIndex);
float angle = sample / (float)samples * Mathf.PI * 2f;
Vector2 offset = new(Mathf.Cos(angle) * distance, Mathf.Sin(angle) * distance);
RectTransform layerRect = layerObject.GetComponent();
layerRect.anchorMin = Vector2.zero;
layerRect.anchorMax = Vector2.one;
layerRect.offsetMin = offset;
layerRect.offsetMax = offset;
Image layerImage = layerObject.GetComponent();
ApplyGlowSprite(layerImage);
layerImage.raycastTarget = false;
_glowLayers.Add(layerImage);
if (layerIndex == 0)
{
_glowImage = layerImage;
}
layerIndex++;
}
}
}
private void ApplyGlowSprite(Image image)
{
if (image == null)
{
return;
}
if (_targetImage != null && useTargetSpriteShape && _targetImage.sprite != null)
{
image.sprite = _targetImage.sprite;
image.type = _targetImage.type;
image.preserveAspect = _targetImage.preserveAspect;
image.fillCenter = true;
image.fillMethod = _targetImage.fillMethod;
image.fillOrigin = _targetImage.fillOrigin;
image.fillAmount = _targetImage.fillAmount;
image.fillClockwise = _targetImage.fillClockwise;
image.pixelsPerUnitMultiplier = _targetImage.pixelsPerUnitMultiplier;
return;
}
image.sprite = GetGlowSprite();
image.type = Image.Type.Sliced;
image.preserveAspect = false;
}
private bool IsFocusedNow()
{
if (!isActiveAndEnabled || _selectable == null || !_selectable.IsActive() || !_selectable.IsInteractable())
{
return false;
}
bool isSelected = EventSystem.current != null
&& EventSystem.current.currentSelectedGameObject == gameObject;
return isSelected || _isPointerInside;
}
private void SetGlowAlpha(float alpha)
{
if (_glowLayers.Count == 0)
{
return;
}
for (int i = 0; i < _glowLayers.Count; i++)
{
Image layer = _glowLayers[i];
if (layer == null)
{
continue;
}
int samples = Mathf.Max(4, outlineSamples);
int blurIndex = i / samples;
float layerT = Mathf.Max(1, blurLayers) <= 1 ? 0f : blurIndex / (float)(Mathf.Max(1, blurLayers) - 1);
Color color = ResolveGlowColor();
color.a = alpha * Mathf.Lerp(0.36f, 0.12f, layerT);
layer.color = color;
layer.enabled = alpha > 0.001f;
}
}
private Color ResolveGlowColor()
{
if (!useButtonColor || _targetImage == null)
{
return glowColor;
}
Color color = Color.Lerp(_targetImage.color, Color.white, colorLighten);
color.a = 1f;
return color;
}
private static Sprite GetGlowSprite()
{
if (_glowSprite != null)
{
return _glowSprite;
}
Texture2D texture = new(TextureSize, TextureSize, TextureFormat.RGBA32, false)
{
name = "Runtime UI Focus Glow",
wrapMode = TextureWrapMode.Clamp,
filterMode = FilterMode.Bilinear
};
Vector2 center = new(0.5f, 0.5f);
for (int y = 0; y < TextureSize; y++)
{
for (int x = 0; x < TextureSize; x++)
{
Vector2 uv = new(x / (TextureSize - 1f), y / (TextureSize - 1f));
float edgeDistance = Mathf.Max(Mathf.Abs(uv.x - center.x), Mathf.Abs(uv.y - center.y));
float alpha = Mathf.SmoothStep(0f, 1f, Mathf.InverseLerp(0.25f, 0.5f, edgeDistance));
alpha *= 1f - Mathf.SmoothStep(0.48f, 0.5f, edgeDistance);
texture.SetPixel(x, y, new Color(1f, 1f, 1f, alpha));
}
}
texture.Apply();
_glowSprite = Sprite.Create(
texture,
new Rect(0f, 0f, TextureSize, TextureSize),
new Vector2(0.5f, 0.5f),
100f,
0,
SpriteMeshType.FullRect,
new Vector4(24f, 24f, 24f, 24f));
return _glowSprite;
}
}
}