@@ -0,0 +1,312 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e4743327f32eb24e86090ec474ac91a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user