Files
ichni_Official/Assets/Scripts/UI/SongSelection/SongDotAnimationUI.cs
2026-07-26 12:12:45 +08:00

168 lines
5.0 KiB
C#

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<RectTransform> dots = new();
private readonly List<CanvasGroup> 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<RectTransform>());
CanvasGroup canvasGroup = go.GetComponent<CanvasGroup>();
if (canvasGroup == null)
{
canvasGroup = go.AddComponent<CanvasGroup>();
}
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);
}
}