using System; using System.Collections.Generic; using System.Threading; using Cysharp.Threading.Tasks; using PrimeTween; using TMPro; using UnityEngine; using UnityEngine.UI; namespace BriarQueen.Framework.Effects { [ExecuteAlways] public class UIEdgeDarken : MonoBehaviour { private static readonly int _amountId = Shader.PropertyToID("_Amount"); private static readonly int _centerDarknessId = Shader.PropertyToID("_CenterDarkness"); private static readonly int _colorId = Shader.PropertyToID("_Color"); private static readonly int _edgeDarknessId = Shader.PropertyToID("_EdgeDarkness"); private static readonly int _edgeWidthId = Shader.PropertyToID("_EdgeWidth"); private static readonly int _rectPivotId = Shader.PropertyToID("_RectPivot"); private static readonly int _rectSizeId = Shader.PropertyToID("_RectSize"); private static readonly int _softnessId = Shader.PropertyToID("_Softness"); [Header("References")] [SerializeField] private Graphic _graphic; [SerializeField] private CanvasGroup _canvasGroup; [SerializeField] private Material _edgeDarkenMaterialTemplate; [Header("Targeting")] [SerializeField] private bool _targetChildGraphics; [SerializeField] private bool _includeInactiveChildGraphics = true; [SerializeField] private bool _includeTextGraphics; [Header("Darken")] [SerializeField] [Range(0f, 1f)] private float _amount = 1f; [SerializeField] private Color _color = new(0f, 0f, 0f, 0.65f); [SerializeField] [Range(0f, 1f)] private float _edgeDarkness = 1f; [SerializeField] [Range(0f, 1f)] private float _centerDarkness; [SerializeField] [Range(0.001f, 0.5f)] private float _edgeWidth = 0.22f; [SerializeField] [Range(0.001f, 0.5f)] private float _softness = 0.18f; [Header("Tween")] [SerializeField] private float _duration = 0.35f; [SerializeField] private Ease _ease = Ease.InOutSine; [SerializeField] private bool _useUnscaledTime = true; [Header("Editor Preview")] [SerializeField] private bool _previewInEditMode; [SerializeField] [Range(0f, 1f)] private float _previewAmount = 1f; private readonly List _targetGraphics = new(); private readonly List _originalMaterials = new(); private readonly List _runtimeMaterials = new(); private CancellationTokenSource _darkenCts; private bool _hasOriginalMaterials; private bool _isPreviewMaterial; private Sequence _darkenSequence; public float Amount { get => _amount; set => SetAmount(value); } private void Awake() { ResolveReferences(); if (Application.isPlaying) { CreateRuntimeMaterial(false); ApplyMaterialProperties(_amount); return; } #if UNITY_EDITOR RefreshEditModePreview(); #endif } private void OnEnable() { #if UNITY_EDITOR if (!Application.isPlaying) { RefreshEditModePreview(); } #endif } private void OnDisable() { #if UNITY_EDITOR if (!Application.isPlaying) { RestoreEditModeMaterial(); } #endif } private void OnDestroy() { CancelTween(); if (Application.isPlaying) { DestroyRuntimeMaterial(false); return; } #if UNITY_EDITOR RestoreEditModeMaterial(); #endif } private void OnValidate() { ResolveReferences(); if (Application.isPlaying) { ApplyMaterialProperties(_amount); return; } #if UNITY_EDITOR RestoreEditModeMaterial(); RefreshEditModePreview(); #endif } public UniTask DarkenIn() { return TweenAmount(0f, 1f, _duration); } public UniTask DarkenOut() { return TweenAmount(1f, 0f, _duration); } public async UniTask TweenAmount(float from, float to, float duration) { if (_runtimeMaterials.Count == 0) { CreateRuntimeMaterial(false); } CancelTween(); _darkenCts = new CancellationTokenSource(); SetAmount(from); _darkenSequence = Sequence.Create(useUnscaledTime: _useUnscaledTime) .Group(Tween.Custom( from, to, Mathf.Max(0f, duration), SetAmount, _ease, useUnscaledTime: _useUnscaledTime)); try { await _darkenSequence.ToUniTask(cancellationToken: _darkenCts.Token); SetAmount(to); } catch (OperationCanceledException) { // Interrupted by another darken request or object destruction. } finally { _darkenSequence = default; if (_darkenCts != null) { _darkenCts.Dispose(); _darkenCts = null; } } } public void SetAmount(float amount) { _amount = Mathf.Clamp01(amount); ApplyMaterialProperties(_amount); } public void CancelTween() { if (_darkenSequence.isAlive) { _darkenSequence.Stop(); _darkenSequence = default; } if (_darkenCts != null) { _darkenCts.Cancel(); _darkenCts.Dispose(); _darkenCts = null; } } private void ResolveReferences() { if (_graphic == null) { _graphic = GetComponent(); } if (_canvasGroup == null) { _canvasGroup = GetComponent(); } } private void ResolveTargetGraphics() { ResolveReferences(); _targetGraphics.Clear(); if (_targetChildGraphics) { var root = _canvasGroup != null ? _canvasGroup.transform : transform; var graphics = root.GetComponentsInChildren(_includeInactiveChildGraphics); foreach (var graphic in graphics) { if (IsValidTargetGraphic(graphic)) { _targetGraphics.Add(graphic); } } return; } if (_graphic != null) { _targetGraphics.Add(_graphic); } } private bool IsValidTargetGraphic(Graphic graphic) { if (graphic == null) { return false; } // TMP needs a TMP-compatible shader; replacing its SDF material breaks text rendering. if (!_includeTextGraphics && graphic is TMP_Text) { return false; } return true; } private void CreateRuntimeMaterial(bool isPreviewMaterial) { ResolveTargetGraphics(); if (_targetGraphics.Count == 0) { return; } var sourceMaterial = _edgeDarkenMaterialTemplate != null ? _edgeDarkenMaterialTemplate : _targetGraphics[0].material; if (sourceMaterial == null) { return; } if (_runtimeMaterials.Count > 0) { ApplyRuntimeMaterialToTargets(); return; } SaveOriginalMaterials(); _isPreviewMaterial = isPreviewMaterial; foreach (var graphic in _targetGraphics) { if (graphic == null) { _runtimeMaterials.Add(null); continue; } var runtimeMaterial = Instantiate(sourceMaterial); runtimeMaterial.name = isPreviewMaterial ? $"{nameof(UIEdgeDarken)} Preview Material" : $"{nameof(UIEdgeDarken)} Runtime Material"; if (isPreviewMaterial) { runtimeMaterial.hideFlags = HideFlags.DontSaveInEditor; } _runtimeMaterials.Add(runtimeMaterial); } ApplyRuntimeMaterialToTargets(); } private void ApplyRuntimeMaterialToTargets() { var count = Mathf.Min(_targetGraphics.Count, _runtimeMaterials.Count); for (var i = 0; i < count; i++) { var graphic = _targetGraphics[i]; if (graphic == null) { continue; } graphic.material = _runtimeMaterials[i]; graphic.SetMaterialDirty(); } } private void ApplyMaterialProperties(float amount) { if (_runtimeMaterials.Count == 0) { return; } var count = Mathf.Min(_targetGraphics.Count, _runtimeMaterials.Count); for (var i = 0; i < count; i++) { var material = _runtimeMaterials[i]; var graphic = _targetGraphics[i]; if (material == null || graphic == null) { continue; } var rectTransform = graphic.rectTransform; material.SetFloat(_amountId, Mathf.Clamp01(amount)); material.SetColor(_colorId, _color); material.SetFloat(_edgeDarknessId, _edgeDarkness); material.SetFloat(_centerDarknessId, _centerDarkness); material.SetFloat(_edgeWidthId, _edgeWidth); material.SetFloat(_softnessId, _softness); material.SetVector(_rectSizeId, rectTransform.rect.size); material.SetVector(_rectPivotId, rectTransform.pivot); } SetTargetsMaterialDirty(); } private void SaveOriginalMaterials() { _originalMaterials.Clear(); foreach (var graphic in _targetGraphics) { _originalMaterials.Add(graphic != null ? graphic.material : null); } _hasOriginalMaterials = true; } private void RestoreOriginalMaterials() { if (!_hasOriginalMaterials) { return; } var count = Mathf.Min(_targetGraphics.Count, _originalMaterials.Count); for (var i = 0; i < count; i++) { var graphic = _targetGraphics[i]; if (graphic == null) { continue; } graphic.material = _originalMaterials[i]; graphic.SetMaterialDirty(); } _originalMaterials.Clear(); _hasOriginalMaterials = false; } private void SetTargetsMaterialDirty() { foreach (var graphic in _targetGraphics) { if (graphic != null) { graphic.SetMaterialDirty(); } } } private void DestroyRuntimeMaterial(bool immediate) { if (_runtimeMaterials.Count == 0) { return; } RestoreOriginalMaterials(); foreach (var runtimeMaterial in _runtimeMaterials) { if (runtimeMaterial == null) { continue; } if (immediate) { DestroyImmediate(runtimeMaterial); } else { Destroy(runtimeMaterial); } } _runtimeMaterials.Clear(); _isPreviewMaterial = false; } #if UNITY_EDITOR private void RefreshEditModePreview() { if (Application.isPlaying) { return; } if (!_previewInEditMode) { RestoreEditModeMaterial(); return; } CreateRuntimeMaterial(true); ApplyMaterialProperties(_previewAmount); } private void RestoreEditModeMaterial() { if (!_isPreviewMaterial) { return; } DestroyRuntimeMaterial(true); } #endif } }