using System.Collections.Generic; using Ichni.Menu; using UnityEngine; public class SongDotAnimationUI : MonoBehaviour { [Header("引用")] public SongListControllerUI controllerUI; [Header("预制体")] public GameObject DotPfab; [Header("布局参数")] [SerializeField] private float innerOffset = 400f; [SerializeField] private float outerOffset = 600f; [SerializeField] [Min(1f)] private float visibleSongRadius = 2f; [SerializeField] [Min(1)] private int visibleDotCount = 5; [SerializeField] [Range(0f, 1f)] private float outerScale = 0f; [SerializeField] [Range(0f, 1f)] private float midScale = 0.2f; [SerializeField] [Range(0f, 1f)] private float outerAlpha = 0f; [SerializeField] [Range(0f, 1f)] private float midAlpha = 0.45f; private readonly List dots = new(); private readonly List dotCanvasGroups = new(); private int displayedCount = -1; public int Count => controllerUI != null ? controllerUI.SongCount : 0; public float ScrollProgress => controllerUI != null ? controllerUI.ScrollProgress : 0f; void Start() { RebuildDotsIfNeeded(); } void LateUpdate() { RebuildDotsIfNeeded(); if (controllerUI == null || dots.Count == 0) { return; } float centerIndex = controllerUI.ContinuousSongIndex; int firstVisibleSongIndex = Mathf.FloorToInt(centerIndex) - visibleDotCount / 2; for (int i = 0; i < dots.Count; i++) { RectTransform dot = dots[i]; int songIndex = firstVisibleSongIndex + i; if (songIndex < 0 || songIndex >= Count) { SetDotVisible(i, false); continue; } float relativeIndex = songIndex - centerIndex; float absoluteRelativeIndex = Mathf.Abs(relativeIndex); bool isVisible = absoluteRelativeIndex <= visibleSongRadius; SetDotVisible(i, isVisible); if (!isVisible) { continue; } float y = GetDotY(relativeIndex); dots[i].anchoredPosition = new Vector2(0f, y); float scale = GetDotScale(absoluteRelativeIndex); dots[i].localScale = Vector3.one * Mathf.Clamp01(scale); if (i < dotCanvasGroups.Count && dotCanvasGroups[i] != null) { dotCanvasGroups[i].alpha = GetDotAlpha(absoluteRelativeIndex); } } } private void RebuildDotsIfNeeded() { int targetCount = Mathf.Min(Count, visibleDotCount); if (displayedCount == targetCount) { return; } for (int i = dots.Count - 1; i >= 0; i--) { if (dots[i] != null) { Destroy(dots[i].gameObject); } } dots.Clear(); dotCanvasGroups.Clear(); if (DotPfab == null) { displayedCount = -1; return; } for (int i = 0; i < targetCount; i++) { GameObject go = Instantiate(DotPfab, transform); dots.Add(go.GetComponent()); CanvasGroup canvasGroup = go.GetComponent(); if (canvasGroup == null) { canvasGroup = go.AddComponent(); } dotCanvasGroups.Add(canvasGroup); } displayedCount = targetCount; } private void SetDotVisible(int dotIndex, bool isVisible) { RectTransform dot = dots[dotIndex]; if (dot.gameObject.activeSelf != isVisible) { dot.gameObject.SetActive(isVisible); } } private float GetDotY(float relativeIndex) { float absRelativeIndex = Mathf.Abs(relativeIndex); if (absRelativeIndex <= 1f) { return -relativeIndex * innerOffset; } float scaleRange = Mathf.Max(visibleSongRadius - 1f, 0.0001f); float outerT = Mathf.Clamp01((absRelativeIndex - 1f) / scaleRange); float distance = Mathf.Lerp(innerOffset, outerOffset, outerT); return -Mathf.Sign(relativeIndex) * distance; } private float GetDotScale(float absoluteRelativeIndex) { if (absoluteRelativeIndex <= 1f) { return Mathf.Lerp(1f, midScale, absoluteRelativeIndex); } float scaleRange = Mathf.Max(visibleSongRadius - 1f, 0.0001f); float outerT = Mathf.Clamp01((absoluteRelativeIndex - 1f) / scaleRange); return Mathf.Lerp(midScale, outerScale, outerT); } private float GetDotAlpha(float absoluteRelativeIndex) { if (absoluteRelativeIndex <= 1f) { return Mathf.Lerp(1f, midAlpha, absoluteRelativeIndex); } float scaleRange = Mathf.Max(visibleSongRadius - 1f, 0.0001f); float outerT = Mathf.Clamp01((absoluteRelativeIndex - 1f) / scaleRange); return Mathf.Lerp(midAlpha, outerAlpha, outerT); } }