曲线视觉编辑器,初步

This commit is contained in:
2025-07-01 19:10:31 +08:00
parent 7fee6d651a
commit ffb97c6d28
11 changed files with 3475 additions and 94 deletions

View File

@@ -49,10 +49,10 @@ MonoBehaviour:
m_addressable:
m_atlas:
m_fileSize: 26252
m_assetChangeTS: 1751289904
m_fileInfoReadTS: 1751289905
m_fileWriteTS: 1751289904
m_cachefileWriteTS: 1751289904
m_assetChangeTS: 1751364893
m_fileInfoReadTS: 1751364911
m_fileWriteTS: 1751364893
m_cachefileWriteTS: 1751364893
refreshStamp: 2
UseGUIDsList:
- guid: fc6c02e75b66345c29e8a25e2e2bda9c
@@ -108842,15 +108842,15 @@ MonoBehaviour:
ids: 73000000
- guid: f716a33409746460589eb6305a5ed072
type: 2
m_fileInfoHash: 16659.cs
m_fileInfoHash: 16810.cs
m_assetbundle:
m_addressable:
m_atlas:
m_fileSize: 16659
m_assetChangeTS: 0
m_fileInfoReadTS: 1750510507
m_fileWriteTS: 1745084316
m_cachefileWriteTS: 1745084316
m_fileSize: 16810
m_assetChangeTS: 1751352657
m_fileInfoReadTS: 1751352674
m_fileWriteTS: 1751351894
m_cachefileWriteTS: 1751351894
refreshStamp: 2
UseGUIDsList: []
- guid: f776bdd36b750304c8e0de8ee1f31fc0
@@ -111247,15 +111247,15 @@ MonoBehaviour:
UseGUIDsList: []
- guid: 28c134965d47644a98d3ba8a1343674a
type: 3
m_fileInfoHash: 480091.unity
m_fileInfoHash: 1713382.unity
m_assetbundle:
m_addressable:
m_atlas:
m_fileSize: 480091
m_assetChangeTS: 1750943771
m_fileInfoReadTS: 1750943791
m_fileWriteTS: 1750943771
m_cachefileWriteTS: 1750943771
m_fileSize: 1713382
m_assetChangeTS: 1751364894
m_fileInfoReadTS: 1751364911
m_fileWriteTS: 1751364894
m_cachefileWriteTS: 1751364894
refreshStamp: 2
UseGUIDsList:
- guid: 0000000000000000f000000000000000
@@ -111344,6 +111344,10 @@ MonoBehaviour:
ids: 73000000
- guid: 3312d7739989d2b4e91e6319e9a96d76
ids: 73000000
- guid: 5e4743327f32eb24e86090ec474ac91a
ids: 73000000
- guid: 1344c3c82d62a2a41a3576d8abb8e3ea
ids: 73000000
- guid: 3f216541647c185428ca5581509954d8
ids: e9030000
- guid: 344445a89b4f74a0e9a0a766903df87e
@@ -115557,15 +115561,15 @@ MonoBehaviour:
UseGUIDsList: []
- guid: 7847a689c7721eb4dba4344e727ab715
type: 2
m_fileInfoHash: 3313.cs
m_fileInfoHash: 3863.cs
m_assetbundle:
m_addressable:
m_atlas:
m_fileSize: 3313
m_assetChangeTS: 0
m_fileInfoReadTS: 1750510506
m_fileWriteTS: 1742680961
m_cachefileWriteTS: 1742680961
m_fileSize: 3863
m_assetChangeTS: 1751352657
m_fileInfoReadTS: 1751352674
m_fileWriteTS: 1751351539
m_cachefileWriteTS: 1751351539
refreshStamp: 2
UseGUIDsList: []
- guid: 785830160e033d24280df9c5b4cec368
@@ -143557,15 +143561,15 @@ MonoBehaviour:
UseGUIDsList: []
- guid: 7ab6cd8f9a2e77c49a158e16014f9cec
type: 9
m_fileInfoHash: 2049032.json
m_fileInfoHash: 2049196.json
m_assetbundle:
m_addressable:
m_atlas:
m_fileSize: 2049032
m_assetChangeTS: 1751292309
m_fileInfoReadTS: 1751292326
m_fileWriteTS: 1751292036
m_cachefileWriteTS: 1751292036
m_fileSize: 2049196
m_assetChangeTS: 1751367897
m_fileInfoReadTS: 1751367915
m_fileWriteTS: 1751367843
m_cachefileWriteTS: 1751367843
refreshStamp: 2
UseGUIDsList: []
- guid: 7af6ac3e6b51b8d4aab04adc85b8de2f
@@ -183753,15 +183757,15 @@ MonoBehaviour:
UseGUIDsList: []
- guid: 6d98a93f5b5c14ef5b7b125e407ce17d
type: 5
m_fileInfoHash: 39073.prefab
m_fileInfoHash: 41510.prefab
m_assetbundle:
m_addressable:
m_atlas:
m_fileSize: 39073
m_assetChangeTS: 0
m_fileInfoReadTS: 1750510493
m_fileWriteTS: 1742680962
m_cachefileWriteTS: 1742680962
m_fileSize: 41510
m_assetChangeTS: 1751357984
m_fileInfoReadTS: 1751358658
m_fileWriteTS: 1751357984
m_cachefileWriteTS: 1751357984
refreshStamp: 2
UseGUIDsList:
- guid: f4688fdb7df04437aeb418b961361dc5
@@ -183788,6 +183792,10 @@ MonoBehaviour:
ids: 73000000
- guid: 31a19414c41e5ae4aae2af33fee712f6
ids: 73000000
- guid: 1344c3c82d62a2a41a3576d8abb8e3ea
ids: 73000000
- guid: 5e4743327f32eb24e86090ec474ac91a
ids: 73000000
- guid: 30649d3a9faa99c48a7b1166b86bf2a0
ids: 73000000
- guid: 306cc8c2b49d7114eaa3623786fc2126
@@ -193426,15 +193434,15 @@ MonoBehaviour:
ids: 73000000
- guid: 2e498d1c8094910479dc3e1b768306a4
type: 5
m_fileInfoHash: 9628.asset
m_fileInfoHash: 560939.asset
m_assetbundle:
m_addressable:
m_atlas:
m_fileSize: 9628
m_assetChangeTS: 1751289920
m_fileInfoReadTS: 1751289979
m_fileWriteTS: 1751289920
m_cachefileWriteTS: 1751289920
m_fileSize: 560939
m_assetChangeTS: 1751292440
m_fileInfoReadTS: 1751292443
m_fileWriteTS: 1751292439
m_cachefileWriteTS: 1751292439
refreshStamp: 2
UseGUIDsList:
- guid: fe393ace9b354375a9cb14cdbbc28be4
@@ -218807,6 +218815,32 @@ MonoBehaviour:
m_cachefileWriteTS: 1751289835
refreshStamp: 2
UseGUIDsList: []
- guid: 138c996b947bada4d94d900ca8213f9a
type: 1
m_fileInfoHash:
m_assetbundle:
m_addressable:
m_atlas:
m_fileSize: 0
m_assetChangeTS: 0
m_fileInfoReadTS: 1751296414
m_fileWriteTS: 0
m_cachefileWriteTS: 0
refreshStamp: 2
UseGUIDsList: []
- guid: 5e4743327f32eb24e86090ec474ac91a
type: 2
m_fileInfoHash: 24377.cs
m_assetbundle:
m_addressable:
m_atlas:
m_fileSize: 24377
m_assetChangeTS: 1751367897
m_fileInfoReadTS: 1751367915
m_fileWriteTS: 1751367894
m_cachefileWriteTS: 1751367894
refreshStamp: 2
UseGUIDsList: []
setting:
alternateColor: 1
excludeTypes: 0

View File

@@ -1,5 +1,6 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
namespace Michsky.MUIP
{
@@ -8,10 +9,11 @@ namespace Michsky.MUIP
[Header("Resources")]
public RectTransform dragArea;
public RectTransform dragObject;
public RectTransform draggerRect;
[Header("Settings")]
public bool topOnDrag = true;
public bool Lock = false;
private Vector2 originalLocalPointerPosition;
private Vector3 originalPanelLocalPosition;
@@ -58,23 +60,29 @@ namespace Michsky.MUIP
public void OnBeginDrag(PointerEventData data)
{
originalPanelLocalPosition = DragObjectInternal.localPosition;
RectTransformUtility.ScreenPointToLocalPointInRectangle(DragAreaInternal, data.position, data.pressEventCamera, out originalLocalPointerPosition);
gameObject.transform.SetAsLastSibling();
if (topOnDrag == true) { dragObject.transform.SetAsLastSibling(); }
if (!Lock && (draggerRect == null || RectTransformUtility.RectangleContainsScreenPoint(GetComponent<RectTransform>(), Mouse.current.position.ReadValue())))
{
originalPanelLocalPosition = DragObjectInternal.localPosition;
RectTransformUtility.ScreenPointToLocalPointInRectangle(DragAreaInternal, data.position, data.pressEventCamera, out originalLocalPointerPosition);
gameObject.transform.SetAsLastSibling();
if (topOnDrag == true) { dragObject.transform.SetAsLastSibling(); }
}
}
public void OnDrag(PointerEventData data)
{
Vector2 localPointerPosition;
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(DragAreaInternal, data.position, data.pressEventCamera, out localPointerPosition))
if (!Lock && (draggerRect == null || RectTransformUtility.RectangleContainsScreenPoint(GetComponent<RectTransform>(), Mouse.current.position.ReadValue())))
{
Vector3 offsetToOriginal = localPointerPosition - originalLocalPointerPosition;
DragObjectInternal.localPosition = originalPanelLocalPosition + offsetToOriginal;
}
Vector2 localPointerPosition;
ClampToArea();
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(DragAreaInternal, data.position, data.pressEventCamera, out localPointerPosition))
{
Vector3 offsetToOriginal = localPointerPosition - originalLocalPointerPosition;
DragObjectInternal.localPosition = originalPanelLocalPosition + offsetToOriginal;
}
ClampToArea();
}
}
private void ClampToArea()

View File

@@ -784,6 +784,136 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_ShowMaskGraphic: 1
--- !u!1 &4955889325830384991
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 810674083073048004}
- component: {fileID: 8990101195177178229}
- component: {fileID: 4768191335492145245}
- component: {fileID: 1028112064287344483}
m_Layer: 5
m_Name: KeyframeVisualizer
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
--- !u!224 &810674083073048004
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4955889325830384991}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 2, y: 2, z: 2}
m_ConstrainProportionsScale: 1
m_Children: []
m_Father: {fileID: 70126183804589383}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 400, y: 300}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &8990101195177178229
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4955889325830384991}
m_CullTransparentMesh: 1
--- !u!114 &4768191335492145245
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4955889325830384991}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1344c3c82d62a2a41a3576d8abb8e3ea, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 0
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Texture: {fileID: 0}
m_UVRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
--- !u!114 &1028112064287344483
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4955889325830384991}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5e4743327f32eb24e86090ec474ac91a, type: 3}
m_Name:
m_EditorClassIdentifier:
curve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: -0.02690773
outSlope: -0.02690773
tangentMode: 34
weightedMode: 0
inWeight: 0
outWeight: 0.33333334
- serializedVersion: 3
time: 0.49971336
value: 0.98655385
inSlope: -0.0037777494
outSlope: -0.0037777494
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0.9750061
value: 1.0018921
inSlope: 0.032753333
outSlope: 0.032753333
tangentMode: 0
weightedMode: 0
inWeight: 0.43590096
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
timeRange: 10
valueRange: 10
keyframeSize: 0.2
tangentLength: 1
curveColor: {r: 0, g: 1, b: 0, a: 1}
keyframeColor: {r: 1, g: 0, b: 0, a: 1}
tangentColor: {r: 0, g: 0, b: 1, a: 1}
segments: 5
curveRawImage: {fileID: 4768191335492145245}
curveTextureSize: {x: 400, y: 300}
keyframeImages: []
compositeParameterWindow: {fileID: 8591554574042573968}
--- !u!1 &5406300460509605192
GameObject:
m_ObjectHideFlags: 0
@@ -1100,6 +1230,7 @@ RectTransform:
- {fileID: 8670700627722813223}
- {fileID: 4803002250074383695}
- {fileID: 6090386518530065525}
- {fileID: 810674083073048004}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
@@ -1121,7 +1252,9 @@ MonoBehaviour:
m_EditorClassIdentifier:
dragArea: {fileID: 0}
dragObject: {fileID: 70126183804589383}
draggerRect: {fileID: 0}
topOnDrag: 1
Lock: 0
--- !u!114 &8591554574042573968
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -1137,11 +1270,11 @@ MonoBehaviour:
windowRect: {fileID: 1670114893104377247}
closeButton: {fileID: 6180637045729466281}
title: {fileID: 1143112183991551266}
container: {fileID: 0}
addNewUnitButton: {fileID: 3483650628256943973}
unitPrefab: {fileID: 0}
unitList: []
parameterName:
keyframeVisualizer: {fileID: 1028112064287344483}
--- !u!1 &9104419187492598894
GameObject:
m_ObjectHideFlags: 0

File diff suppressed because one or more lines are too long

View File

@@ -23,7 +23,7 @@ namespace Ichni.Editor
this.connectedBaseElement = baseElement;
this.parameterName = parameterName;
unitList = new List<DynamicUICompositeUnit>();
InitializeWindow(titleText, ApplyParameters);
StartCoroutine(WindowAnim.ShowPanelOnScale(gameObject));
@@ -34,7 +34,7 @@ namespace Ichni.Editor
unitList.Remove(unit);
Destroy(unit.gameObject);
}
public CompositeParameterWindow AddListenerFunction(UnityAction action)
{
onQuit = action;
@@ -77,7 +77,7 @@ namespace Ichni.Editor
List<string> stringList = unitList.Select(unit => (unit as DynamicUIInputFieldUnit).GetValue<string>()).ToList();
connectedBaseElement.GetType().GetField(parameterName).SetValue(connectedBaseElement, stringList);
};
return this;
}
@@ -109,7 +109,7 @@ namespace Ichni.Editor
List<float> floatList = unitList.Select(unit => (unit as DynamicUIInputFieldUnit).GetValue<float>()).ToList();
connectedBaseElement.GetType().GetField(parameterName).SetValue(connectedBaseElement, floatList);
};
return this;
}
@@ -146,7 +146,7 @@ namespace Ichni.Editor
}
connectedBaseElement.GetType().GetField(parameterName).SetValue(connectedBaseElement, newFlexibleFloat);
};
return this;
}
@@ -183,7 +183,7 @@ namespace Ichni.Editor
}
connectedBaseElement.GetType().GetField(parameterName).SetValue(connectedBaseElement, newFlexibleInt);
};
return this;
}
@@ -219,12 +219,19 @@ namespace Ichni.Editor
}
connectedBaseElement.GetType().GetField(parameterName).SetValue(connectedBaseElement, newFlexibleBool);
};
return this;
}
public KeyframeVisualizer keyframeVisualizer;
public CompositeParameterWindow SetAsCustomCurve()
{
//生成一个预览器先
keyframeVisualizer.gameObject.SetActive(true);
//
void GenerateUnit(Keyframe content)
{
DynamicUICustomCurveKeyframeUnit unit = Instantiate(unitPrefab, windowRect).GetComponent<DynamicUICustomCurveKeyframeUnit>();
@@ -242,6 +249,11 @@ namespace Ichni.Editor
Instantiate(EditorManager.instance.basePrefabs.customCurveWrapModeUnit, windowRect).GetComponent<DynamicUICustomCurveWrapModeUnit>();
unitList.Add(warpModesUnit);
warpModesUnit.SetUnit(this, warpModes);
//
keyframeVisualizer.curve = curve;
keyframeVisualizer.DrawCurveToRawImage();
keyframeVisualizer.CreateKeyframeImages();
//
foreach (Keyframe keyframe in keyframes)
{
@@ -268,8 +280,8 @@ namespace Ichni.Editor
newCurve.AddKey(unit.GetValue());
}
connectedBaseElement.GetType().GetField(parameterName).SetValue(connectedBaseElement, newCurve);
};
return this;
}
@@ -281,7 +293,7 @@ namespace Ichni.Editor
unitList.Add(unit);
unit.SetUnit(this, content);
}
unitPrefab = EditorManager.instance.basePrefabs.gradientColorKeyUnit;
Gradient gradient = connectedBaseElement.GetType().GetField(parameterName).GetValue(connectedBaseElement) as Gradient;
List<GradientColorKey> colorKeys = gradient.colorKeys.ToList();
@@ -296,7 +308,7 @@ namespace Ichni.Editor
GenerateUnit(new GradientColorKey(Color.white, 1));
addNewUnitButton.GetComponent<RectTransform>().SetAsLastSibling();
});
ApplyParameters = () =>
{
List<GradientColorKey> newColorKeys = new List<GradientColorKey>();
@@ -306,10 +318,10 @@ namespace Ichni.Editor
}
(connectedBaseElement.GetType().GetField(parameterName).GetValue(connectedBaseElement) as Gradient).colorKeys = newColorKeys.ToArray();
};
return this;
}
public CompositeParameterWindow SetAsGradientAlphaKeys()
{
void GenerateUnit(GradientAlphaKey content)
@@ -318,7 +330,7 @@ namespace Ichni.Editor
unitList.Add(unit);
unit.SetUnit(this, content);
}
unitPrefab = EditorManager.instance.basePrefabs.gradientAlphaKeyUnit;
Gradient gradient = connectedBaseElement.GetType().GetField(parameterName).GetValue(connectedBaseElement) as Gradient;
List<GradientAlphaKey> alphaKeys = gradient.alphaKeys.ToList();
@@ -333,7 +345,7 @@ namespace Ichni.Editor
GenerateUnit(new GradientAlphaKey(1, 1));
addNewUnitButton.GetComponent<RectTransform>().SetAsLastSibling();
});
ApplyParameters = () =>
{
List<GradientAlphaKey> newAlphaKeys = new List<GradientAlphaKey>();
@@ -343,10 +355,10 @@ namespace Ichni.Editor
}
(connectedBaseElement.GetType().GetField(parameterName).GetValue(connectedBaseElement) as Gradient).alphaKeys = newAlphaKeys.ToArray();
};
return this;
}
public CompositeParameterWindow SetAsStringIntDictionary()
{
//生成Unit
@@ -358,13 +370,13 @@ namespace Ichni.Editor
}
unitPrefab = EditorManager.instance.basePrefabs.stringIntPairUnit;
Dictionary<string, int> dictionary = connectedBaseElement.GetType().GetField(parameterName).GetValue(connectedBaseElement) as Dictionary<string, int>;
foreach (var pair in dictionary)
{
GenerateUnit(pair);
}
addNewUnitButton.GetComponent<RectTransform>().SetAsLastSibling();
//为添加新的Unit的按钮设置点击事件
@@ -385,7 +397,7 @@ namespace Ichni.Editor
}
connectedBaseElement.GetType().GetField(parameterName).SetValue(connectedBaseElement, dictionaryList);
};
return this;
}
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 138c996b947bada4d94d900ca8213f9a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,587 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Sirenix.OdinInspector;
using System.Linq;
using UnityEngine.InputSystem;
using Michsky.MUIP;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Ichni.Editor
{
[RequireComponent(typeof(RawImage))]
public class KeyframeVisualizer : MonoBehaviour
{
public AnimationCurve curve;
public float timeRange = 10f;
public float valueRange = 10f;
public float keyframeSize = 0.2f;
public float tangentLength = 1f;
public Color curveColor = Color.green;
public Color keyframeColor = Color.red;
public Color tangentColor = Color.blue;
public int segments = 5;
[Header("Curve Preview")]
public RawImage curveRawImage;
public Vector2Int curveTextureSize = new Vector2Int(256, 128);
[System.Serializable]
public struct KeyframeImageInfo
{
public Image keyImage;
public Image inTangentImage;
public Image outTangentImage;
}
public List<KeyframeImageInfo> keyframeImages = new List<KeyframeImageInfo>();
// 合并min/max到类成员便于全局一致使用
private float minTime, maxTime, minValue = 0f, maxValue = 1f;
private void UpdateCurveRange()
{
if (curve == null || curve.length < 2)
{
minTime = maxTime = valueRange = timeRange = 0;
minValue = 0f;
maxValue = 1f;
return;
}
minTime = 0f;
maxTime = 1f;
minValue = 0f; // 固定下界为0
maxValue = 1f; // 固定上界为1
valueRange = Mathf.Max(0.0001f, maxValue - minValue);
timeRange = Mathf.Max(0.0001f, maxTime - minTime);
}
[Button("Draw Curve To RawImage")]
public void DrawCurveToRawImage()
{
UpdateCurveRange();
if (curveRawImage == null || curve == null || curve.length < 2) return;
int texWidth = curveTextureSize.x;
int texHeight = curveTextureSize.y;
Texture2D tex = new Texture2D(texWidth, texHeight, TextureFormat.ARGB32, false);
tex.filterMode = FilterMode.Point;
tex.wrapMode = TextureWrapMode.Clamp;
// 清空
for (int i = 0; i < texWidth; i++)
for (int j = 0; j < texHeight; j++)
tex.SetPixel(i, j, new Color(0, 0, 0, 0));
// === 新增:绘制网格 ===
int gridXCount = 8;
int gridYCount = 4;
Color gridColor = new Color(0.3f, 0.3f, 0.3f, 0.7f);
for (int gx = 1; gx < gridXCount; gx++)
{
int x = gx * texWidth / gridXCount;
DrawVerticalLineOnTexture(tex, x, 0, texHeight - 1, gridColor);
}
for (int gy = 1; gy < gridYCount; gy++)
{
int y = gy * texHeight / gridYCount;
DrawHorizontalLineOnTexture(tex, 0, texWidth - 1, y, gridColor);
}
// === 新增:绘制边框 ===
Color borderColor = Color.white;
DrawHorizontalLineOnTexture(tex, 0, texWidth - 1, 0, borderColor);
DrawHorizontalLineOnTexture(tex, 0, texWidth - 1, texHeight - 1, borderColor);
DrawVerticalLineOnTexture(tex, 0, 0, texHeight - 1, borderColor);
DrawVerticalLineOnTexture(tex, texWidth - 1, 0, texHeight - 1, borderColor);
int lastY = -1;
for (int x = 0; x < texWidth; x++)
{
float t = (float)x / (texWidth - 1);
float time = Mathf.Lerp(minTime, maxTime, t);
float value = curve.Evaluate(time);
float normY = (value - minValue) / valueRange;
int y = (int)(normY * (texHeight - 1));
bool outOfRange = y < 0 || y >= texHeight;
int drawY = y;
Color drawColor = outOfRange
? new Color(1f - curveColor.r, 1f - curveColor.g, 1f - curveColor.b, 1f)
: curveColor;
if (lastY >= 0)
{
int y0 = lastY;
int y1 = y;
int x0 = x - 1;
int x1 = x;
// === 修正:补全断点 ===
if (x0 >= 0 && x0 < texWidth && x1 >= 0 && x1 < texWidth)
{
if (Mathf.Abs(y1 - y0) > 1)
{
int step = y1 > y0 ? 1 : -1;
for (int yyy = y0; yyy != y1; yyy += step)
{
if (yyy >= 0 && yyy < texHeight)
tex.SetPixel(x0, yyy, curveColor);
}
}
}
int dy = Mathf.Abs(y1 - y0);
int sy = y0 < y1 ? 1 : -1;
int err = dy / 2;
int yy = y0;
for (int xx = x0; xx <= x1; xx++)
{
bool segOutOfRange = yy < 0 || yy >= texHeight;
Color segColor = segOutOfRange
? new Color(1f - curveColor.r, 1f - curveColor.g, 1f - curveColor.b, 1f)
: curveColor;
if (xx >= 0 && xx < texWidth)
{
for (int j = lastY; j < yy; j++) tex.SetPixel(x0, j, curveColor);
for (int j = lastY; j > yy; j--) tex.SetPixel(x0, j, curveColor);
tex.SetPixel(xx, yy, segColor);
}
err -= dy;
if (err < 0)
{
yy += sy;
err += (x1 - x0);
}
}
}
if (x >= 0 && x < texWidth)
tex.SetPixel(x, drawY, drawColor);
lastY = y;
}
foreach (var key in curve.keys)
{
float tangentScale = (maxTime - minTime) * 0.08f;
if (!float.IsInfinity(key.inTangent))
{
float t0 = key.time;
float v0 = key.value;
float t1 = t0 - tangentScale;
float v1 = v0 - key.inTangent * tangentScale;
int x0 = (int)(((t0 - minTime) / timeRange) * (texWidth - 1));
int y0 = (int)(((v0 - minValue) / valueRange) * (texHeight - 1));
int x1 = (int)(((t1 - minTime) / timeRange) * (texWidth - 1));
int y1 = (int)(((v1 - minValue) / valueRange) * (texHeight - 1));
DrawLineOnTexture(tex, x0, y0, x1, y1, tangentColor);
}
if (!float.IsInfinity(key.outTangent))
{
float t0 = key.time;
float v0 = key.value;
float t1 = t0 + tangentScale;
float v1 = v0 + key.outTangent * tangentScale;
int x0 = (int)(((t0 - minTime) / timeRange) * (texWidth - 1));
int y0 = (int)(((v0 - minValue) / valueRange) * (texHeight - 1));
int x1 = (int)(((t1 - minTime) / timeRange) * (texWidth - 1));
int y1 = (int)(((v1 - minValue) / valueRange) * (texHeight - 1));
DrawLineOnTexture(tex, x0, y0, x1, y1, tangentColor * 0.8f);
}
}
tex.Apply();
curveRawImage.texture = tex;
}
private void DrawLineOnTexture(Texture2D tex, int x0, int y0, int x1, int y1, Color color)
{
int dx = Mathf.Abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
int dy = Mathf.Abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
int err = (dx > dy ? dx : -dy) / 2, e2;
int lastY = y0;
while (true)
{
for (int j = lastY; j < y0; j++) tex.SetPixel(x0, j, color);
for (int j = lastY; j > y0; j--) tex.SetPixel(x0, j, color);
tex.SetPixel(x0, y0, color);
lastY = y0;
if (x0 == x1 && y0 == y1) break;
e2 = err;
if (e2 > -dx) { err -= dy; x0 += sx; }
if (e2 < dy) { err += dx; y0 += sy; }
}
}
/// <summary>
/// 获取关键帧在RawImage中的localPosition以RawImage中心为原点
/// </summary>
public Vector2 GetKeyframeLocalPositionInRawImage(Keyframe key, float scale = 1f)
{
UpdateCurveRange();
if (curveRawImage == null || curve == null || curve.length < 2)
return Vector2.zero;
// 这里直接使用固定的minValue/maxValue
float normX = (key.time - minTime) / timeRange;
float normY = (key.value - 0f) / Mathf.Max(0.0001f, 1f - 0f);
float px = normX * curveTextureSize.x;
float py = normY * curveTextureSize.y;
Vector2 localPos = new Vector2(
px - curveTextureSize.x * 0.5f,
py - curveTextureSize.y * 0.5f
);
return localPos * scale;
}
[Button("Create Keyframe Images")]
public void CreateKeyframeImages()
{
UpdateCurveRange();
if (curveRawImage == null || curve == null || curve.length < 1) return;
for (int i = curveRawImage.transform.childCount - 1; i >= 0; i--)
{
var child = curveRawImage.transform.GetChild(i);
#if UNITY_EDITOR
if (!Application.isPlaying)
DestroyImmediate(child.gameObject);
else
#endif
Destroy(child.gameObject);
}
keyframeImages.Clear();
for (int i = 0; i < curve.length; i++)
{
Keyframe key = curve.keys[i];
Vector2 localPos = GetKeyframeLocalPositionInRawImage(key);
GameObject go = new GameObject($"KeyframeImage_{i}", typeof(RectTransform), typeof(Image));
go.transform.SetParent(curveRawImage.transform, false);
RectTransform rt = go.GetComponent<RectTransform>();
rt.sizeDelta = Vector2.one * 16f;
rt.anchoredPosition = localPos;
Image img = go.GetComponent<Image>();
img.color = keyframeColor;
img.raycastTarget = false;
PointMover pointMover = go.AddComponent<PointMover>();
pointMover.parent = this;
pointMover.keyIndex = i;
Image inImg = null;
if (!float.IsInfinity(key.inTangent))
{
Vector2 tangentLocalPos = GetTangentLocalPosition(key, true);
GameObject tgo = new GameObject($"TangentInImage_{i}", typeof(RectTransform), typeof(Image));
tgo.transform.SetParent(curveRawImage.transform, false);
RectTransform trt = tgo.GetComponent<RectTransform>();
trt.sizeDelta = Vector2.one * 10f;
trt.anchoredPosition = tangentLocalPos;
inImg = tgo.GetComponent<Image>();
inImg.color = tangentColor;
inImg.raycastTarget = false;
PointMover pointMover1 = tgo.AddComponent<PointMover>();
pointMover1.IO = 1;
pointMover1.parent = this;
pointMover1.keyIndex = i;
pointMover.subpointMover.Add(pointMover1);
}
Image outImg = null;
if (!float.IsInfinity(key.outTangent))
{
Vector2 tangentLocalPos = GetTangentLocalPosition(key, false);
GameObject tgo = new GameObject($"TangentOutImage_{i}", typeof(RectTransform), typeof(Image));
tgo.transform.SetParent(curveRawImage.transform, false);
RectTransform trt = tgo.GetComponent<RectTransform>();
trt.sizeDelta = Vector2.one * 10f;
trt.anchoredPosition = tangentLocalPos;
outImg = tgo.GetComponent<Image>();
outImg.color = tangentColor * 0.8f;
outImg.raycastTarget = false;
PointMover pointMover2 = tgo.AddComponent<PointMover>();
pointMover2.IO = -1;
pointMover2.parent = this;
pointMover2.keyIndex = i;
pointMover.subpointMover.Add(pointMover2);
}
keyframeImages.Add(new KeyframeImageInfo
{
keyImage = img,
inTangentImage = inImg,
outTangentImage = outImg
});
}
}
// 修复后的切线位置计算方法
private Vector2 GetTangentLocalPosition(Keyframe key, bool isIn)
{
Vector2 keyLocalPos = GetKeyframeLocalPositionInRawImage(key);
float tangent = isIn ? key.inTangent : key.outTangent;
// 计算像素/单位转换比例
float pixelsPerTimeUnit = curveTextureSize.x / timeRange;
float pixelsPerValueUnit = curveTextureSize.y / valueRange;
// 处理无穷斜率的情况(垂直切线)
if (float.IsInfinity(tangent))
{
return keyLocalPos + new Vector2(
0,
isIn ? -tangentLength * 20f : tangentLength * 20f
);
}
// 计算方向向量
Vector2 direction = new Vector2(
isIn ? -1f : 1f,
tangent * (pixelsPerValueUnit / pixelsPerTimeUnit)
).normalized;
// 动态计算像素长度
float dynamicLength = tangentLength * Mathf.Min(curveTextureSize.x, curveTextureSize.y) * 0.1f;
return keyLocalPos + direction * dynamicLength;
}
[Button("Update Curve From Images")]
public void UpdateCurveFromImages()
{
UpdateCurveRange();
if (curve == null || keyframeImages == null || keyframeImages.Count < curve.length) return;
Keyframe[] newKeys = new Keyframe[curve.length];
for (int i = 0; i < curve.length; i++)
{
RectTransform rt = keyframeImages[i].keyImage.rectTransform;
Vector2 localPos = rt.anchoredPosition;
float px = localPos.x + curveTextureSize.x * 0.5f;
float py = localPos.y + curveTextureSize.y * 0.5f;
float normX = px / curveTextureSize.x;
float normY = py / curveTextureSize.y;
float time = Mathf.Lerp(minTime, maxTime, normX);
float value = Mathf.Clamp01(0f + normY * Mathf.Max(0.0001f, 1f - 0f));
float inTangent = curve.keys[i].inTangent;
float outTangent = curve.keys[i].outTangent;
float timeScale = timeRange / curveTextureSize.x;
float valueScale = valueRange / curveTextureSize.y;
// 修复切线斜率计算
if (keyframeImages[i].inTangentImage != null)
{
Vector2 inLocalPos = keyframeImages[i].inTangentImage.rectTransform.anchoredPosition;
Vector2 keyLocalPos = keyframeImages[i].keyImage.rectTransform.anchoredPosition;
// 使用像素坐标差计算真实斜率
float dx = (keyLocalPos.x - inLocalPos.x) * timeScale;
float dy = (keyLocalPos.y - inLocalPos.y) * valueScale;
if (Mathf.Abs(dx) > 0.001f)
inTangent = dy / dx;
}
if (keyframeImages[i].outTangentImage != null)
{
Vector2 outLocalPos = keyframeImages[i].outTangentImage.rectTransform.anchoredPosition;
Vector2 keyLocalPos = keyframeImages[i].keyImage.rectTransform.anchoredPosition;
// 使用像素坐标差计算真实斜率
float dx = (outLocalPos.x - keyLocalPos.x) * timeScale;
float dy = (outLocalPos.y - keyLocalPos.y) * valueScale;
if (Mathf.Abs(dx) > 0.001f)
outTangent = dy / dx;
}
Keyframe newKey = new Keyframe(time, value, inTangent, outTangent);
newKeys[i] = newKey;
}
curve.keys = newKeys;
}
[Button("Refresh Keyframe Images Position")]
public void RefreshKeyframeImagesPosition()
{
UpdateCurveRange();
if (curve == null || keyframeImages == null || keyframeImages.Count < curve.length) return;
for (int i = 0; i < curve.length; i++)
{
Keyframe key = curve.keys[i];
var info = keyframeImages[i];
if (info.keyImage != null)
{
Vector2 localPos = GetKeyframeLocalPositionInRawImage(key);
info.keyImage.rectTransform.anchoredPosition = localPos;
}
if (info.inTangentImage != null && !float.IsInfinity(key.inTangent))
{
Vector2 tangentLocalPos = GetTangentLocalPosition(key, true);
info.inTangentImage.rectTransform.anchoredPosition = tangentLocalPos;
}
if (info.outTangentImage != null && !float.IsInfinity(key.outTangent))
{
Vector2 tangentLocalPos = GetTangentLocalPosition(key, false);
info.outTangentImage.rectTransform.anchoredPosition = tangentLocalPos;
}
}
}
public CompositeParameterWindow compositeParameterWindow;
public void UpadteValue()
{
compositeParameterWindow.connectedBaseElement.GetType().GetField(compositeParameterWindow.parameterName).SetValue(compositeParameterWindow.connectedBaseElement, curve);
}
// === 新增:绘制水平线辅助方法 ===
private void DrawHorizontalLineOnTexture(Texture2D tex, int x0, int x1, int y, Color color)
{
int width = tex.width;
int height = tex.height;
if (y < 0 || y >= height) return;
int minX = Mathf.Clamp(Mathf.Min(x0, x1), 0, width - 1);
int maxX = Mathf.Clamp(Mathf.Max(x0, x1), 0, width - 1);
for (int x = minX; x <= maxX; x++)
tex.SetPixel(x, y, color);
}
// === 新增:绘制垂直线辅助方法 ===
private void DrawVerticalLineOnTexture(Texture2D tex, int x, int y0, int y1, Color color)
{
int width = tex.width;
int height = tex.height;
if (x < 0 || x >= width) return;
int minY = Mathf.Clamp(Mathf.Min(y0, y1), 0, height - 1);
int maxY = Mathf.Clamp(Mathf.Max(y0, y1), 0, height - 1);
for (int y = minY; y <= maxY; y++)
tex.SetPixel(x, y, color);
}
}
public class PointMover : MonoBehaviour
{
public RectTransform rectTransform;
public KeyframeVisualizer parent;
public int keyIndex = -1; // 记录所属关键帧索引
public PointMover parentPointMover;
public List<PointMover> subpointMover = new();
public bool Pressed;
public int IO = 0;//0关键帧 1in -1out
private Dictionary<PointMover, Vector2> initialOffsets; // 存储切线点初始偏移
private Vector2 startPosition; // 拖拽开始位置
private void Start()
{
rectTransform = gameObject.GetComponent<RectTransform>();
}
private void Update()
{
if (RectTransformUtility.RectangleContainsScreenPoint(rectTransform, Mouse.current.position.ReadValue()))
{
if (Mouse.current.leftButton.wasPressedThisFrame)
{
StartCoroutine(Moving());
}
}
}
public IEnumerator Moving()
{
var windowDragger = parent.compositeParameterWindow.GetComponent<WindowDragger>();
if (windowDragger != null)
{
windowDragger.Lock = true;
}
// 获取边界尺寸
float halfWidth = parent.curveTextureSize.x * 0.5f;
float halfHeight = parent.curveTextureSize.y * 0.5f;
// 如果是切线点,记录初始位置和方向
Vector2 initialPosition = rectTransform.anchoredPosition;
Vector2 initialDirection = Vector2.zero;
if (IO != 0)
{
PointMover keyPoint = parentPointMover != null ? parentPointMover : this;
Vector2 keyPos = keyPoint.rectTransform.anchoredPosition;
initialDirection = (initialPosition - keyPos).normalized;
}
while (Mouse.current.leftButton.isPressed)
{
Vector2 mouseDelta = Mouse.current.delta.ReadValue();
Vector2 newPos = rectTransform.anchoredPosition + mouseDelta;
// 边界约束
newPos.x = Mathf.Clamp(newPos.x, -halfWidth, halfWidth);
newPos.y = Mathf.Clamp(newPos.y, -halfHeight, halfHeight);
// 如果是切线点,约束移动方向
if (IO != 0 && initialDirection != Vector2.zero)
{
Vector2 keyPos = parentPointMover.rectTransform.anchoredPosition;
Vector2 toNewPos = newPos - keyPos;
// 计算与初始方向的夹角
float angle = Vector2.Angle(initialDirection, toNewPos);
// 如果偏离初始方向超过45度修正方向
if (angle > 45f)
{
// 投影到初始方向
float dot = Vector2.Dot(toNewPos, initialDirection);
newPos = keyPos + initialDirection * Mathf.Sign(dot) * toNewPos.magnitude;
}
}
rectTransform.anchoredPosition = newPos;
// 如果是关键帧点,同时移动切线点
if (IO == 0)
{
foreach (PointMover tangentPoint in subpointMover)
{
if (tangentPoint != null)
{
tangentPoint.rectTransform.anchoredPosition += mouseDelta;
}
}
}
yield return null;
}
if (windowDragger != null)
{
windowDragger.Lock = false;
}
UpdateParentCurve();
}
public void UpdateParentCurve()
{
parent.UpdateCurveFromImages();
parent.DrawCurveToRawImage();
parent.UpadteValue();
parent.CreateKeyframeImages();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5e4743327f32eb24e86090ec474ac91a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -65518,6 +65518,12 @@
"startTime" : 28.8,
"endTime" : 32,
"animationCurveType" : 0
},{
"startValue" : 0,
"endValue" : 1,
"startTime" : 28.8,
"endTime" : 32,
"animationCurveType" : 0
}
]
},
@@ -73541,10 +73547,10 @@
"inTangent" : 0,
"outTangent" : 0
},{
"time" : 0.5,
"time" : 0.185,
"value" : 1,
"inTangent" : 0,
"outTangent" : 0
"outTangent" : -3.43269825
},{
"time" : 1,
"value" : 0,

View File

@@ -65518,6 +65518,12 @@
"startTime" : 28.8,
"endTime" : 32,
"animationCurveType" : 0
},{
"startValue" : 0,
"endValue" : 1,
"startTime" : 28.8,
"endTime" : 32,
"animationCurveType" : 0
}
]
},
@@ -73530,7 +73536,7 @@
"effectTime" : 0
},{
"__type" : "Ichni.RhythmGame.Beatmap.PixelateEffect_BM,Assembly-CSharp",
"duration" : 5,
"duration" : 2.5,
"bottomX" : 3,
"bottomY" : 180,
"intensityCurve" : {
@@ -73541,21 +73547,41 @@
"inTangent" : 0,
"outTangent" : 0
},{
"time" : 0.5,
"value" : 1,
"time" : 0.1225,
"value" : 0.9933333,
"inTangent" : -0.2222226,
"outTangent" : 0.2222226
},{
"time" : 0.1925,
"value" : 0.01,
"inTangent" : 0,
"outTangent" : 0
},{
"time" : 0.3175,
"value" : 0.9833333,
"inTangent" : -0.3555552,
"outTangent" : 0.3555552
},{
"time" : 0.405,
"value" : 0.006666667,
"inTangent" : 0,
"outTangent" : 0
},{
"time" : 0.5175,
"value" : 1,
"inTangent" : -0.2222226,
"outTangent" : -3.592717
},{
"time" : 1,
"value" : 0,
"inTangent" : 0,
"inTangent" : -0.1006292,
"outTangent" : 0
}
],
"preWrapMode" : 8,
"postWrapMode" : 8
},
"effectTime" : 5
"effectTime" : 2.5
}
],"Late":[

File diff suppressed because one or more lines are too long