Files
ichni_Creator_Studio/Assets/Scripts/Editor Tools/KeyframeVisualizer/KeyframeVisualizer.cs
2026-02-09 23:10:55 +08:00

312 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;
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();
}
}
}