using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using UnityEngine.UI.Extensions; namespace Ichni.Editor { [RequireComponent(typeof(RectTransform))] public class KeyframeVisualizer : MonoBehaviour { [Header("Settings")] public Vector2Int resolution = new Vector2Int(512, 256); public Color curveColor = Color.green; public Color gridColor = new Color(0.3f, 0.3f, 0.3f, 0.5f); public float pointSize = 15f; public float tangentHandleLength = 40f; public float lineThickness = 2f; [Header("References")] public AnimationCurve curve; public UILineRenderer curveLine; public UILineRenderer gridLines; public UILineRenderer borderLine; // 当用户松开鼠标(编辑结束)时触发,用于同步外部UI public Action OnEditFinished; private List _activePoints = new List(); private void Awake() { // 移除场景/预制体中残留的旧 RawImage 组件 var oldRawImage = GetComponent(); if (oldRawImage != null) Destroy(oldRawImage); SetupUILineRenderers(); } private void SetupUILineRenderers() { if (curveLine == null) curveLine = CreateChildLineRenderer("CurveLine", curveColor); if (gridLines == null) gridLines = CreateChildLineRenderer("GridLines", gridColor); if (borderLine == null) borderLine = CreateChildLineRenderer("BorderLine", Color.white); curveLine.LineThickness = lineThickness; gridLines.LineThickness = 1f; borderLine.LineThickness = 1.5f; } private UILineRenderer CreateChildLineRenderer(string name, Color color) { GameObject go = new GameObject(name, typeof(RectTransform)); go.transform.SetParent(transform, false); RectTransform rt = go.GetComponent(); rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; UILineRenderer line = go.AddComponent(); line.color = color; line.RelativeSize = true; line.raycastTarget = false; return line; } private void OnEnable() { if (curve == null) curve = AnimationCurve.Linear(0, 0, 1, 1); RebuildInteractablePoints(); DrawCurve(); } // === 核心绘制逻辑 (UILineRenderer) === public void DrawCurve() { if (curve == null) return; int sampleCount = Mathf.Max(2, resolution.x); // 1. 绘制曲线 Vector2[] curvePoints = new Vector2[sampleCount]; for (int i = 0; i < sampleCount; i++) { float t = (float)i / (sampleCount - 1); float val = Mathf.Clamp01(curve.Evaluate(t)); curvePoints[i] = new Vector2(t, val); } curveLine.Points = curvePoints; // 2. 绘制网格 (0.25, 0.5, 0.75) float[] gridPositions = { 0.25f, 0.5f, 0.75f }; List segments = new List(); foreach (float p in gridPositions) { segments.Add(new Vector2[] { new Vector2(0, p), new Vector2(1, p) }); // 横线 segments.Add(new Vector2[] { new Vector2(p, 0), new Vector2(p, 1) }); // 竖线 } gridLines.Segments = segments; // 3. 绘制边框 borderLine.Points = new Vector2[] { new Vector2(0, 0), new Vector2(1, 0), new Vector2(1, 1), new Vector2(0, 1), new Vector2(0, 0) }; } // === 交互点生成 === public void RebuildInteractablePoints() { if (curve == null) return; // 清理旧点(保留子UILineRenderer) var toDestroy = new List(); foreach (Transform child in transform) { if (child == curveLine?.transform || child == gridLines?.transform || child == borderLine?.transform) continue; toDestroy.Add(child.gameObject); } foreach (var go in toDestroy) Destroy(go); _activePoints.Clear(); for (int i = 0; i < curve.length; i++) { // 关键帧点 (Key) var keyPoint = CreatePoint(i, PointType.Key, Color.red, pointSize); // 入切线 (In Tangent) - 第一个点通常不需要 if (i > 0) { var inPoint = CreatePoint(i, PointType.InTangent, Color.cyan, pointSize * 0.6f); inPoint.relatedKeyPoint = keyPoint; keyPoint.inHandle = inPoint; } // 出切线 (Out Tangent) - 最后一个点通常不需要 if (i < curve.length - 1) { var outPoint = CreatePoint(i, PointType.OutTangent, Color.cyan, pointSize * 0.6f); outPoint.relatedKeyPoint = keyPoint; keyPoint.outHandle = outPoint; } } RefreshPointsPosition(); } private CurvePoint CreatePoint(int index, PointType type, Color color, float size) { GameObject go = new GameObject($"{type}_{index}"); go.transform.SetParent(transform, false); Image img = go.AddComponent(); img.color = color; RectTransform rt = go.GetComponent(); rt.sizeDelta = Vector2.one * size; rt.anchorMin = rt.anchorMax = Vector2.zero; // 使用绝对坐标定位 CurvePoint cp = go.AddComponent(); cp.Init(this, index, type); _activePoints.Add(cp); return cp; } // === 坐标同步逻辑 === public void RefreshPointsPosition() { Vector2 size = ((RectTransform)transform).rect.size; foreach (var point in _activePoints) { Keyframe key = curve.keys[point.keyIndex]; Vector2 keyNormPos = new Vector2(key.time, key.value); Vector2 keyPixelPos = new Vector2(keyNormPos.x * size.x, keyNormPos.y * size.y); if (point.type == PointType.Key) { point.rectTransform.anchoredPosition = keyPixelPos; } else { float tangent = (point.type == PointType.InTangent) ? key.inTangent : key.outTangent; Vector2 visualDir; if (float.IsInfinity(tangent)) { visualDir = new Vector2(0, tangent > 0 ? 1 : -1); } else { // 斜率 = (y / x) * aspect => y = (tangent / aspect) * x // 令视觉上的 x 为 1 或 -1 float xDir = (point.type == PointType.InTangent) ? -1f : 1f; float yDir = (tangent / (size.x / size.y)) * xDir; visualDir = new Vector2(xDir, yDir).normalized; } // 顺着计算出的向量方向,放置手柄 point.rectTransform.anchoredPosition = keyPixelPos + visualDir * tangentHandleLength; } } } // 处理点被拖拽 public void OnPointDragged(CurvePoint point, Vector2 screenDelta) { Vector2 size = ((RectTransform)transform).rect.size; int index = point.keyIndex; Keyframe key = curve.keys[index]; if (point.type == PointType.Key) { // 关键帧移动逻辑不变 Vector2 deltaNorm = new Vector2(screenDelta.x / size.x, screenDelta.y / size.y); key.time = Mathf.Clamp01(key.time + deltaNorm.x); key.value = Mathf.Clamp01(key.value + deltaNorm.y); } else { // === 切线处理优化:基于向量构建 === // 1. 获取关键帧当前的屏幕坐标 Vector2 keyPos = point.relatedKeyPoint.rectTransform.anchoredPosition; // 2. 获取鼠标当前的目标坐标(当前手柄位置 + 增量) Vector2 mouseTargetPos = point.rectTransform.anchoredPosition + screenDelta; // 3. 计算从关键帧指向鼠标的向量 Vector2 dirVec = mouseTargetPos - keyPos; // 4. 强制方向约束 (InTangent必须在左, OutTangent必须在右) if (point.type == PointType.InTangent) { if (dirVec.x > -1f) dirVec.x = -1f; // 至少向左偏 1 像素,防止除零或方向翻转 } else if (point.type == PointType.OutTangent) { if (dirVec.x < 1f) dirVec.x = 1f; // 至少向右偏 1 像素 } // 5. 计算斜率 (Tangent) float canvasAspect = size.x / size.y; float tangent = (dirVec.y / dirVec.x) * canvasAspect; if (point.type == PointType.InTangent) key.inTangent = tangent; else key.outTangent = tangent; } curve.MoveKey(index, key); DrawCurve(); RefreshPointsPosition(); // 统一刷新位置,确保手柄视觉表现一致 } } public enum PointType { Key, InTangent, OutTangent } // 辅助交互类 public class CurvePoint : MonoBehaviour, IDragHandler, IEndDragHandler { public KeyframeVisualizer visualizer; public int keyIndex; public PointType type; public RectTransform rectTransform; // 引用关联点,方便计算 public CurvePoint relatedKeyPoint; public CurvePoint inHandle; public CurvePoint outHandle; public void Init(KeyframeVisualizer v, int index, PointType t) { visualizer = v; keyIndex = index; type = t; rectTransform = GetComponent(); } public void OnDrag(PointerEventData eventData) { // 实时更新曲线和画面 visualizer.OnPointDragged(this, eventData.delta); } public void OnEndDrag(PointerEventData eventData) { // 拖拽结束后,通知外部同步 (关键一步) visualizer.OnEditFinished?.Invoke(); } } }