2026-02-09 23:10:55 +08:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.UI;
|
|
|
|
|
|
using UnityEngine.EventSystems;
|
2026-06-12 16:25:52 +08:00
|
|
|
|
using UnityEngine.UI.Extensions;
|
2026-02-09 23:10:55 +08:00
|
|
|
|
|
|
|
|
|
|
namespace Ichni.Editor
|
|
|
|
|
|
{
|
2026-06-12 16:25:52 +08:00
|
|
|
|
[RequireComponent(typeof(RectTransform))]
|
2026-02-09 23:10:55 +08:00
|
|
|
|
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;
|
2026-06-12 16:25:52 +08:00
|
|
|
|
public float lineThickness = 2f;
|
2026-02-09 23:10:55 +08:00
|
|
|
|
|
|
|
|
|
|
[Header("References")]
|
|
|
|
|
|
public AnimationCurve curve;
|
2026-06-12 16:25:52 +08:00
|
|
|
|
public UILineRenderer curveLine;
|
|
|
|
|
|
public UILineRenderer gridLines;
|
|
|
|
|
|
public UILineRenderer borderLine;
|
2026-02-09 23:10:55 +08:00
|
|
|
|
|
|
|
|
|
|
// 当用户松开鼠标(编辑结束)时触发,用于同步外部UI
|
|
|
|
|
|
public Action OnEditFinished;
|
|
|
|
|
|
|
|
|
|
|
|
private List<CurvePoint> _activePoints = new List<CurvePoint>();
|
|
|
|
|
|
|
|
|
|
|
|
private void Awake()
|
|
|
|
|
|
{
|
2026-06-12 16:25:52 +08:00
|
|
|
|
// 移除场景/预制体中残留的旧 RawImage 组件
|
|
|
|
|
|
var oldRawImage = GetComponent<RawImage>();
|
|
|
|
|
|
if (oldRawImage != null)
|
|
|
|
|
|
Destroy(oldRawImage);
|
|
|
|
|
|
|
|
|
|
|
|
SetupUILineRenderers();
|
2026-02-09 23:10:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-12 16:25:52 +08:00
|
|
|
|
private void SetupUILineRenderers()
|
2026-02-09 23:10:55 +08:00
|
|
|
|
{
|
2026-06-12 16:25:52 +08:00
|
|
|
|
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;
|
2026-02-09 23:10:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-12 16:25:52 +08:00
|
|
|
|
private UILineRenderer CreateChildLineRenderer(string name, Color color)
|
2026-02-09 23:10:55 +08:00
|
|
|
|
{
|
2026-06-12 16:25:52 +08:00
|
|
|
|
GameObject go = new GameObject(name, typeof(RectTransform));
|
|
|
|
|
|
go.transform.SetParent(transform, false);
|
2026-02-09 23:10:55 +08:00
|
|
|
|
|
2026-06-12 16:25:52 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-02-09 23:10:55 +08:00
|
|
|
|
|
2026-06-12 16:25:52 +08:00
|
|
|
|
private void OnEnable()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (curve == null) curve = AnimationCurve.Linear(0, 0, 1, 1);
|
|
|
|
|
|
RebuildInteractablePoints();
|
|
|
|
|
|
DrawCurve();
|
|
|
|
|
|
}
|
2026-02-09 23:10:55 +08:00
|
|
|
|
|
2026-06-12 16:25:52 +08:00
|
|
|
|
// === 核心绘制逻辑 (UILineRenderer) ===
|
|
|
|
|
|
public void DrawCurve()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (curve == null) return;
|
2026-02-09 23:10:55 +08:00
|
|
|
|
|
2026-06-12 16:25:52 +08:00
|
|
|
|
int sampleCount = Mathf.Max(2, resolution.x);
|
2026-02-09 23:10:55 +08:00
|
|
|
|
|
2026-06-12 16:25:52 +08:00
|
|
|
|
// 1. 绘制曲线
|
|
|
|
|
|
Vector2[] curvePoints = new Vector2[sampleCount];
|
|
|
|
|
|
for (int i = 0; i < sampleCount; i++)
|
2026-02-09 23:10:55 +08:00
|
|
|
|
{
|
2026-06-12 16:25:52 +08:00
|
|
|
|
float t = (float)i / (sampleCount - 1);
|
|
|
|
|
|
float val = Mathf.Clamp01(curve.Evaluate(t));
|
|
|
|
|
|
curvePoints[i] = new Vector2(t, val);
|
2026-02-09 23:10:55 +08:00
|
|
|
|
}
|
2026-06-12 16:25:52 +08:00
|
|
|
|
curveLine.Points = curvePoints;
|
2026-02-09 23:10:55 +08:00
|
|
|
|
|
2026-06-12 16:25:52 +08:00
|
|
|
|
// 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)
|
2026-02-09 23:10:55 +08:00
|
|
|
|
{
|
2026-06-12 16:25:52 +08:00
|
|
|
|
segments.Add(new Vector2[] { new Vector2(0, p), new Vector2(1, p) }); // 横线
|
|
|
|
|
|
segments.Add(new Vector2[] { new Vector2(p, 0), new Vector2(p, 1) }); // 竖线
|
2026-02-09 23:10:55 +08:00
|
|
|
|
}
|
2026-06-12 16:25:52 +08:00
|
|
|
|
gridLines.Segments = segments;
|
2026-02-09 23:10:55 +08:00
|
|
|
|
|
2026-06-12 16:25:52 +08:00
|
|
|
|
// 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)
|
|
|
|
|
|
};
|
2026-02-09 23:10:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// === 交互点生成 ===
|
2026-06-12 16:25:52 +08:00
|
|
|
|
public void RebuildInteractablePoints()
|
2026-02-09 23:10:55 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (curve == null) return;
|
|
|
|
|
|
|
2026-06-12 16:25:52 +08:00
|
|
|
|
// 清理旧点(保留子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);
|
2026-02-09 23:10:55 +08:00
|
|
|
|
_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()
|
|
|
|
|
|
{
|
2026-06-12 16:25:52 +08:00
|
|
|
|
Vector2 size = ((RectTransform)transform).rect.size;
|
2026-02-09 23:10:55 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
2026-06-12 16:25:52 +08:00
|
|
|
|
float yDir = (tangent / (size.x / size.y)) * xDir;
|
2026-02-09 23:10:55 +08:00
|
|
|
|
visualDir = new Vector2(xDir, yDir).normalized;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 顺着计算出的向量方向,放置手柄
|
|
|
|
|
|
point.rectTransform.anchoredPosition = keyPixelPos + visualDir * tangentHandleLength;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理点被拖拽
|
|
|
|
|
|
public void OnPointDragged(CurvePoint point, Vector2 screenDelta)
|
|
|
|
|
|
{
|
2026-06-12 16:25:52 +08:00
|
|
|
|
Vector2 size = ((RectTransform)transform).rect.size;
|
2026-02-09 23:10:55 +08:00
|
|
|
|
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);
|
2026-06-12 16:25:52 +08:00
|
|
|
|
DrawCurve();
|
2026-02-09 23:10:55 +08:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-12 16:25:52 +08:00
|
|
|
|
}
|