Files
ichni_Creator_Studio/Assets/Scripts/Editor Tools/KeyframeVisualizer/KeyframeVisualizer.cs
TRADER_FOER 4b7f25e47a youhua
Signed-off-by: TRADER_FOER <lhf190@outlook.com>
2026-06-12 16:25:52 +08:00

306 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
}
}