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 _activePoints = new List(); // 缓存特定颜色以提高性能 private Color32 _cClear = new Color32(0, 0, 0, 0); private Color32 _cCurve; private Color32 _cGrid; private void Awake() { if (rawImage == null) rawImage = GetComponent(); _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(); img.color = color; RectTransform rt = go.GetComponent(); rt.sizeDelta = Vector2.one * size; rt.anchorMin = rt.anchorMax = Vector2.zero; // 使用绝对坐标定位 CurvePoint cp = go.AddComponent(); 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(); } public void OnDrag(PointerEventData eventData) { // 实时更新曲线和画面 visualizer.OnPointDragged(this, eventData.delta); } public void OnEndDrag(PointerEventData eventData) { // 拖拽结束后,通知外部同步 (关键一步) visualizer.OnEditFinished?.Invoke(); } } }