using System.Collections; using AibisDream.Utility; using DG.Tweening; using UnityEngine; namespace AibisDream { internal sealed class LargeSpriteEffectsPresenter { private static readonly int BlurAmountId = Shader.PropertyToID("_BlurAmount"); private static readonly int BlurSizeId = Shader.PropertyToID("_BlurSize"); private static readonly int UseSoftFocusId = Shader.PropertyToID("_UseSoftFocus"); private readonly MonoBehaviour _host; private readonly SpriteRenderer _renderer; private readonly Material _focusMaterial; private readonly Vector3 _baseLocalPosition; private readonly Quaternion _baseLocalRotation; private readonly Vector3 _baseLocalScale; private readonly float _baseAlpha; private readonly Material _baseMaterial; private MaterialPropertyBlock _propertyBlock; private Coroutine _animationCoroutine; private Sequence _presentationSequence; private int _blurTweenVersion; private int _presentationTweenVersion; public bool IsAnimationPlaying => _animationCoroutine != null; public string AnimationPrefix { get; private set; } public int AnimationFrameCount { get; private set; } public float AnimationSecondsPerFrame { get; private set; } = 0.2f; public float TargetBlurAmount { get; private set; } public float TargetBlurSize { get; private set; } = 0.012f; public float TargetScaleMultiplier { get; private set; } = 1f; public float TargetAlpha { get; private set; } = 1f; public LargeSpriteEffectsPresenter( MonoBehaviour host, SpriteRenderer renderer, Material focusMaterial) { _host = host; _renderer = renderer; _focusMaterial = focusMaterial; if (_renderer == null) return; var transform = _renderer.transform; _baseLocalPosition = transform.localPosition; _baseLocalRotation = transform.localRotation; _baseLocalScale = transform.localScale; // SpriteShowcase 在自己的 Awake 中会先隐藏 Renderer;D2 的稳定基准仍然是完全可见。 _baseAlpha = 1f; _baseMaterial = _renderer.sharedMaterial; } public IEnumerator ShowAnimation( string picNamePrefix, int frameCount, float secondsPerFrame = 0.2f, float duration = 1f) { if (_renderer == null || _host == null || frameCount <= 0) yield break; ResetEffectsImmediate(); var frames = new Sprite[frameCount]; var normalizedPrefix = Day2SleepSpriteLoader.NormalizeName(picNamePrefix); for (var index = 0; index < frameCount; index++) { Sprite loadedFrame = null; yield return Day2SleepSpriteLoader.Load( $"{normalizedPrefix}{index + 1}", value => loadedFrame = value); if (loadedFrame == null) { ResetEffectsImmediate(); yield break; } frames[index] = loadedFrame; } _renderer.gameObject.SetActive(true); _renderer.sprite = frames[0]; _renderer.SetAlpha(0f); AnimationPrefix = normalizedPrefix; AnimationFrameCount = frameCount; AnimationSecondsPerFrame = Mathf.Max(0.01f, secondsPerFrame); _animationCoroutine = _host.StartCoroutine(LoopAnimation(frames, AnimationSecondsPerFrame)); if (duration <= 0f) _renderer.SetAlpha(1f); else yield return _renderer.DOFade(1f, duration).WaitForCompletion(); TargetAlpha = 1f; } public void SetBlur(float amount, float blurSize = 0.012f) { _blurTweenVersion++; TargetBlurAmount = Mathf.Clamp01(amount); TargetBlurSize = Mathf.Max(0f, blurSize); ApplyBlur(TargetBlurAmount, TargetBlurSize); } public IEnumerator TweenBlur(float targetAmount, float duration, float blurSize = 0.012f) { if (_renderer == null) yield break; var tweenVersion = ++_blurTweenVersion; var startAmount = ReadBlurAmount(); TargetBlurAmount = Mathf.Clamp01(targetAmount); TargetBlurSize = Mathf.Max(0f, blurSize); duration = Mathf.Max(0f, duration); if (duration <= 0f) { ApplyBlur(TargetBlurAmount, TargetBlurSize); yield break; } var elapsed = 0f; while (elapsed < duration) { if (tweenVersion != _blurTweenVersion) yield break; elapsed += Time.deltaTime; var progress = Mathf.Clamp01(elapsed / duration); progress = progress * progress * (3f - 2f * progress); ApplyBlur(Mathf.Lerp(startAmount, TargetBlurAmount, progress), TargetBlurSize); yield return null; } if (tweenVersion != _blurTweenVersion) yield break; ApplyBlur(TargetBlurAmount, TargetBlurSize); } public IEnumerator AnimatePresentation(float scaleMultiplier, float targetAlpha, float duration) { if (_renderer == null) yield break; var tweenVersion = ++_presentationTweenVersion; KillPresentationTweens(); _renderer.gameObject.SetActive(true); TargetScaleMultiplier = Mathf.Max(0.01f, scaleMultiplier); TargetAlpha = Mathf.Clamp01(targetAlpha); duration = Mathf.Max(0f, duration); var targetScale = ScaleFor(TargetScaleMultiplier); if (duration <= 0f) { if (tweenVersion != _presentationTweenVersion) yield break; ApplyPresentationImmediate(TargetScaleMultiplier, TargetAlpha); yield break; } _presentationSequence = DOTween.Sequence(); _presentationSequence.Join( _renderer.transform.DOScale(targetScale, duration).SetEase(Ease.InOutSine)); _presentationSequence.Join( _renderer.transform.DOLocalMove(_baseLocalPosition, duration).SetEase(Ease.InOutSine)); _presentationSequence.Join( _renderer.DOFade(TargetAlpha, duration).SetEase(Ease.InOutSine)); yield return _presentationSequence.WaitForCompletion(); // 被 set_door_closeup / 新 tween 打断后,不要回写 scale。 if (tweenVersion != _presentationTweenVersion) yield break; _presentationSequence = null; var transform = _renderer.transform; transform.localPosition = _baseLocalPosition; transform.localRotation = _baseLocalRotation; transform.localScale = targetScale; _renderer.SetAlpha(TargetAlpha); } public void ApplyPresentationImmediate(float scaleMultiplier, float alpha) { if (_renderer == null) return; _presentationTweenVersion++; KillPresentationTweens(); TargetScaleMultiplier = Mathf.Max(0.01f, scaleMultiplier); TargetAlpha = Mathf.Clamp01(alpha); var transform = _renderer.transform; transform.localPosition = _baseLocalPosition; transform.localRotation = _baseLocalRotation; transform.localScale = ScaleFor(TargetScaleMultiplier); _renderer.SetAlpha(TargetAlpha); } public void SetSpriteImmediate(Sprite sprite) { if (_renderer == null || sprite == null) return; // Null-assign forces URP SpriteRenderer to refresh _MainTex in its property block. _renderer.sprite = null; _renderer.sprite = sprite; } public void StopPresentationAnimation() { _presentationTweenVersion++; KillPresentationTweens(); } public void ResetPresentationImmediate() { if (_renderer == null) return; _presentationTweenVersion++; KillPresentationTweens(); TargetScaleMultiplier = 1f; TargetAlpha = _baseAlpha; var transform = _renderer.transform; transform.localPosition = _baseLocalPosition; transform.localRotation = _baseLocalRotation; transform.localScale = _baseLocalScale; _renderer.SetAlpha(_baseAlpha); } public void HidePresentationImmediate() { if (_renderer == null) return; StopAnimation(hideRenderer: true); ApplyBlur(0f, 0f); ResetPresentationImmediate(); _renderer.SetAlpha(0f); _renderer.gameObject.SetActive(false); } public void ResetEffectsImmediate() { _blurTweenVersion++; var animationWasPlaying = IsAnimationPlaying; StopAnimation(hideRenderer: animationWasPlaying); TargetBlurAmount = 0f; TargetBlurSize = 0.012f; ApplyBlur(0f, 0f); ResetPresentationImmediate(); } private IEnumerator LoopAnimation(Sprite[] frames, float secondsPerFrame) { var index = 0; var wait = new WaitForSeconds(Mathf.Max(0.01f, secondsPerFrame)); while (frames.Length > 0) { if (_renderer != null) _renderer.sprite = frames[index]; index = (index + 1) % frames.Length; yield return wait; } } private void StopAnimation(bool hideRenderer) { if (_animationCoroutine != null && _host != null) { _host.StopCoroutine(_animationCoroutine); _animationCoroutine = null; } AnimationPrefix = null; AnimationFrameCount = 0; if (hideRenderer && _renderer != null) { _renderer.SetAlpha(0f); _renderer.gameObject.SetActive(false); } } private float ReadBlurAmount() { if (_renderer == null || _renderer.sharedMaterial != _focusMaterial) return 0f; _propertyBlock ??= new MaterialPropertyBlock(); _renderer.GetPropertyBlock(_propertyBlock); return _propertyBlock.GetFloat(BlurAmountId); } private void ApplyBlur(float amount, float blurSize) { if (_renderer == null) return; amount = Mathf.Clamp01(amount); if (amount <= 0.001f) { // SetPropertyBlock(null) can drop SpriteRenderer's internal _MainTex on URP. // Clear blur overrides, restore base material, then force a sprite rebind. _renderer.sharedMaterial = _baseMaterial; _propertyBlock ??= new MaterialPropertyBlock(); _renderer.GetPropertyBlock(_propertyBlock); _propertyBlock.SetFloat(BlurAmountId, 0f); _propertyBlock.SetFloat(BlurSizeId, 0f); _propertyBlock.SetFloat(UseSoftFocusId, 0f); _renderer.SetPropertyBlock(_propertyBlock); _renderer.SetPropertyBlock(null); RebindSpriteTexture(_renderer); return; } if (_focusMaterial == null) { Debug.LogWarning("[LargeSpriteEffectsPresenter] 未配置全屏图片对焦材质。"); return; } _renderer.sharedMaterial = _focusMaterial; _propertyBlock ??= new MaterialPropertyBlock(); _renderer.GetPropertyBlock(_propertyBlock); _propertyBlock.SetFloat(BlurAmountId, amount); _propertyBlock.SetFloat(BlurSizeId, Mathf.Max(0f, blurSize)); _propertyBlock.SetFloat(UseSoftFocusId, 1f); // Keep sprite texture in the block so URP SpriteRenderer does not fall back to white. if (_renderer.sprite != null && _renderer.sprite.texture != null) _propertyBlock.SetTexture("_MainTex", _renderer.sprite.texture); _renderer.SetPropertyBlock(_propertyBlock); } private static void RebindSpriteTexture(SpriteRenderer renderer) { if (renderer == null) return; var sprite = renderer.sprite; if (sprite == null) return; renderer.sprite = null; renderer.sprite = sprite; } private Vector3 ScaleFor(float multiplier) { return new Vector3( _baseLocalScale.x * multiplier, _baseLocalScale.y * multiplier, _baseLocalScale.z); } private void KillPresentationTweens() { _presentationSequence?.Kill(); _presentationSequence = null; if (_renderer == null) return; DOTween.Kill(_renderer); DOTween.Kill(_renderer.transform); } } }