306 lines
11 KiB
C#
306 lines
11 KiB
C#
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<CurvePoint> _activePoints = new List<CurvePoint>();
|
||
|
||
private void Awake()
|
||
{
|
||
// 移除场景/预制体中残留的旧 RawImage 组件
|
||
var oldRawImage = GetComponent<RawImage>();
|
||
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<RectTransform>();
|
||
rt.anchorMin = Vector2.zero;
|
||
rt.anchorMax = Vector2.one;
|
||
rt.offsetMin = Vector2.zero;
|
||
rt.offsetMax = Vector2.zero;
|
||
|
||
UILineRenderer line = go.AddComponent<UILineRenderer>();
|
||
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<Vector2[]> segments = new List<Vector2[]>();
|
||
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<GameObject>();
|
||
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<Image>();
|
||
img.color = color;
|
||
|
||
RectTransform rt = go.GetComponent<RectTransform>();
|
||
rt.sizeDelta = Vector2.one * size;
|
||
rt.anchorMin = rt.anchorMax = Vector2.zero; // 使用绝对坐标定位
|
||
|
||
CurvePoint cp = go.AddComponent<CurvePoint>();
|
||
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<RectTransform>();
|
||
}
|
||
|
||
public void OnDrag(PointerEventData eventData)
|
||
{
|
||
// 实时更新曲线和画面
|
||
visualizer.OnPointDragged(this, eventData.delta);
|
||
}
|
||
|
||
public void OnEndDrag(PointerEventData eventData)
|
||
{
|
||
// 拖拽结束后,通知外部同步 (关键一步)
|
||
visualizer.OnEditFinished?.Invoke();
|
||
}
|
||
}
|
||
}
|