312 lines
11 KiB
C#
312 lines
11 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using UnityEngine.EventSystems;
|
||
|
||
namespace Ichni.Editor
|
||
{
|
||
[RequireComponent(typeof(RawImage))]
|
||
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;
|
||
|
||
[Header("References")]
|
||
public AnimationCurve curve;
|
||
public RawImage rawImage;
|
||
|
||
// 当用户松开鼠标(编辑结束)时触发,用于同步外部UI
|
||
public Action OnEditFinished;
|
||
|
||
private Texture2D _texture;
|
||
private Color32[] _buffer;
|
||
private List<CurvePoint> _activePoints = new List<CurvePoint>();
|
||
|
||
// 缓存特定颜色以提高性能
|
||
private Color32 _cClear = new Color32(0, 0, 0, 0);
|
||
private Color32 _cCurve;
|
||
private Color32 _cGrid;
|
||
|
||
private void Awake()
|
||
{
|
||
if (rawImage == null) rawImage = GetComponent<RawImage>();
|
||
_cCurve = curveColor;
|
||
_cGrid = gridColor;
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
if (curve == null) curve = AnimationCurve.Linear(0, 0, 1, 1);
|
||
InitTexture();
|
||
RebuildInteractablePoints();
|
||
DrawCurveToRawImage();
|
||
}
|
||
|
||
// === 核心绘制逻辑 (高性能) ===
|
||
public void DrawCurveToRawImage()
|
||
{
|
||
if (curve == null || _texture == null) return;
|
||
|
||
// 1. 清屏
|
||
int len = _buffer.Length;
|
||
for (int i = 0; i < len; i++) _buffer[i] = _cClear;
|
||
|
||
int w = resolution.x;
|
||
int h = resolution.y;
|
||
|
||
// 2. 绘制网格 (0.25, 0.5, 0.75)
|
||
DrawGridLine(w, h, 0.25f);
|
||
DrawGridLine(w, h, 0.5f);
|
||
DrawGridLine(w, h, 0.75f);
|
||
|
||
// 绘制边框
|
||
DrawRect(0, 0, w - 1, h - 1, Color.white);
|
||
|
||
// 3. 绘制曲线
|
||
// 限制范围在 0-1
|
||
int prevY = -1;
|
||
for (int x = 0; x < w; x++)
|
||
{
|
||
float t = (float)x / (w - 1);
|
||
float val = curve.Evaluate(t);
|
||
|
||
// 映射到像素高度,并Clap防止数组越界
|
||
int y = Mathf.FloorToInt(Mathf.Clamp01(val) * (h - 1));
|
||
|
||
// 绘制点
|
||
int idx = y * w + x;
|
||
if (idx >= 0 && idx < len) _buffer[idx] = _cCurve;
|
||
|
||
// 垂直补间(防止曲线断裂)
|
||
if (prevY != -1 && Mathf.Abs(y - prevY) > 1)
|
||
{
|
||
int step = y > prevY ? 1 : -1;
|
||
for (int k = prevY + step; k != y; k += step)
|
||
{
|
||
int fillIdx = k * w + x;
|
||
if (fillIdx >= 0 && fillIdx < len) _buffer[fillIdx] = _cCurve;
|
||
}
|
||
}
|
||
prevY = y;
|
||
}
|
||
|
||
_texture.SetPixels32(_buffer);
|
||
_texture.Apply();
|
||
rawImage.texture = _texture;
|
||
}
|
||
|
||
private void InitTexture()
|
||
{
|
||
if (_texture == null || _texture.width != resolution.x || _texture.height != resolution.y)
|
||
{
|
||
_texture = new Texture2D(resolution.x, resolution.y, TextureFormat.ARGB32, false);
|
||
_texture.filterMode = FilterMode.Bilinear;
|
||
_buffer = new Color32[resolution.x * resolution.y];
|
||
}
|
||
}
|
||
|
||
private void DrawGridLine(int w, int h, float percent)
|
||
{
|
||
int x = (int)(w * percent);
|
||
int y = (int)(h * percent);
|
||
for (int i = 0; i < h; i++) _buffer[i * w + x] = _cGrid; // 竖线
|
||
for (int i = 0; i < w; i++) _buffer[y * w + i] = _cGrid; // 横线
|
||
}
|
||
|
||
private void DrawRect(int x1, int y1, int x2, int y2, Color32 c)
|
||
{
|
||
int w = resolution.x;
|
||
for (int x = x1; x <= x2; x++) { _buffer[y1 * w + x] = c; _buffer[y2 * w + x] = c; }
|
||
for (int y = y1; y <= y2; y++) { _buffer[y * w + x1] = c; _buffer[y * w + x2] = c; }
|
||
}
|
||
|
||
// === 交互点生成 ===
|
||
public void RebuildInteractablePoints() // 原名 CreateKeyframeImages
|
||
{
|
||
if (curve == null) return;
|
||
|
||
// 清理旧点
|
||
foreach (Transform child in transform) Destroy(child.gameObject);
|
||
_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 = rawImage.rectTransform.rect.size;
|
||
float canvasAspect = size.x / size.y;
|
||
|
||
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 / canvasAspect) * xDir;
|
||
visualDir = new Vector2(xDir, yDir).normalized;
|
||
}
|
||
|
||
// 顺着计算出的向量方向,放置手柄
|
||
point.rectTransform.anchoredPosition = keyPixelPos + visualDir * tangentHandleLength;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理点被拖拽
|
||
// 在 KeyframeVisualizer 类中修改 OnPointDragged 方法
|
||
public void OnPointDragged(CurvePoint point, Vector2 screenDelta)
|
||
{
|
||
Vector2 size = rawImage.rectTransform.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)
|
||
// 物理斜率 = (DeltaValue / DeltaTime)
|
||
// 对应 UI = (dirVec.y / size.y) / (dirVec.x / size.x)
|
||
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;
|
||
|
||
// 6. 更新手柄位置:让手柄视觉上严格对齐鼠标方向,但保持固定长度(可选)
|
||
// 如果你不希望手柄被拉长,可以将 dirVec 归一化再乘上固定长度
|
||
// point.rectTransform.anchoredPosition = keyPos + dirVec.normalized * tangentHandleLength;
|
||
}
|
||
|
||
curve.MoveKey(index, key);
|
||
DrawCurveToRawImage();
|
||
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();
|
||
}
|
||
}
|
||
} |