这能叫例行更新吗

Signed-off-by: TRAfoer <lhf190@outlook.com>
This commit is contained in:
2026-02-09 23:10:55 +08:00
parent 77726bcb6c
commit a76f650998
40 changed files with 1323 additions and 866 deletions

View File

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