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