动画面板:新增两个动画测试按钮 + 把动画模型 API 全部落进对照表

## 功能(面板源码,重启后仍在)
- 新增 AnimationTools:
  - Animate Random Props:随机骨骼/插槽/附件 → ani_api_test
    旋转 0°→90°、位移 (0,0)→(120,-60)、缩放 (1,1)→(1.6,0.6)、斜切 (0,0)→(25,15)
    插槽颜色 (1,1,1,1)→(1,0.2,0.2,0.25)、附件 显示→隐藏;全部只在 0f/30f
  - Curve Test 6 Modes:curve_test,6 根骨骼各一条旋转轴(0°→45°,0f/30f),
    分别 linear / curve 平滑(0.25,0,0.75,1) / stepped / ease-in(0.42,0,1,1) /
    ease-out(0,0,0.58,1) / 对称自动(0.25,0.25,0.75,0.75)
- ObjectPanel.build 里挂载这两个按钮,复用面板自己的 wire() 点击轮询

## 对照表(tools/deobf/anchors.tsv + 混淆名对照表.md 第十一节)
- GP.I=animations、GP.D:LFT;.D:F=fps、Gl=动画(name/skeleton/timelineGroups/register)
- Gl.D/B/L/I(Gp)=旋转/位移/缩放/斜切;Gl.D/B(Gp,GU)=插槽颜色/附件;Gl.D()=绘制顺序
- FH=组;FI=时间轴(runtimeTimelineClass、keys、findKey、insertKey、invalidateRuntime)
- FN=关键帧(time、curve、c2x/c2y/c3x/c3y、setAngle/setX/setY、Color、Gn)
- FV=曲线枚举(linear/curve/stepped;'自动'=curve+控制值)
- 踩坑记录:
  * 关键帧数值写在 FN.D()/B()/L() 的 float 字段,**不是** FN.D():ga(ga 是 deform 的),
    否则会出现'有帧但运行时不变'
  * 编辑器时间轴刻度就是**帧**,关键帧时间写 0/30,不要按 1 秒=30 帧换算
  * FI.D(float,int) 建帧要求时间轴已有关键帧(先有鸡问题)
  * fI.removeRange 两个 (int,int) 重载都不缩容 → 清空动画直接换新 fI
names.tsv 合并成 380 条,payload-named.zip 重产(7271 处成员引用)。

验证:全部在运行中的编辑器里逐条读回(时间轴/关键帧/曲线枚举/控制值),
重启后 pid 94663 单实例,日志确认 '[animtool] 两个动画按钮已加入'。
This commit is contained in:
tianmo committed 2026-10-17 12:57:17 +08:00
1 parent 8df990a058
commit 6580611012
5 files changed
+583

No files matched your search

+77
View File
@@ -339,6 +339,83 @@ com.spineview.SceneObjects listAllNodes (Ljava/lang/ClassLoader;)Ljava/util/List
com.spineview.SceneActions delete (Ljava/lang/ClassLoader;Ljava/lang/Object;)Z delete com.spineview.SceneActions delete (Ljava/lang/ClassLoader;Ljava/lang/Object;)Z delete
# ^ 面板自己的"罗列场景对象 / 删除对象"(走编辑器删除流程,自带撤销)——清理误建对象时用它 # ^ 面板自己的"罗列场景对象 / 删除对象"(走编辑器删除流程,自带撤销)——清理误建对象时用它
# ============================================================================
# 【动画模型】(2026-10-17 实测:新建 Gl / 4 条骨骼轴 / 2 条插槽轴 / 关键帧 / 6 种曲线均跑通并读回验证)
# ============================================================================
# ---- 骨架上的动画表与 fps ----
GP I LfI animations
# ^ 骨架的动画表(fI,元素是 Gl)。新建动画 = new Gl() → 设 D:LGP、D:String → Gl.D() 注册 → animations.add(gl)
GP D LFT; projectSettings
FT D F fps
# ^ 本项目 fps=30(编辑器时间轴刻度直接用帧:关键帧时间写 0 和 30,不要换算成秒)
# ---- Gl = 动画 ----
Gl D LGP; skeleton
Gl D Ljava/lang/String; name
Gl D LfI timelineGroups
# ^ 时间轴组表(FH)。清空动画:直接换一个新的空 fI(removeRange 两个重载实测都不缩容)
Gl D ()V register
# ^ 把动画注册进骨架的动画列表模型(编辑器 New Animation 命令里就是这么做的)
Gl D (LGp;)LFI; rotateTimeline
Gl B (LGp;)LFI; translateTimeline
Gl L (LGp;)LFI; scaleTimeline
Gl I (LGp;)LFI; shearTimeline
# ^ 骨骼四条轴(取或建):运行时类分别是 ih / io / ii / ij
Gl D (LGp;LGU;)LFI; slotColorTimeline
Gl B (LGp;LGU;)LFI; slotAttachmentTimeline
# ^ 插槽两条轴:hU / ip
Gl D ()LFI; drawOrderTimeline
# ^ 骨架级绘制顺序轴(hZ)
# ---- FH = 时间轴组(按对象分组)----
FH D LGl; animation
FH D LGp; bone
FH D LfI timelines
# ---- FI = 时间轴 ----
FI D Ljava/lang/Class; runtimeTimelineClass
# ^ 运行时轴类:ih=Rotate(旋转) io=Translate(位移) ii=Scale(缩放) ij=Shear(斜切)
# hU=SlotColor(插槽颜色) ip=SlotAttachment(附件切换) hZ=DrawOrder(绘制顺序)
FI D LfI keys
FI D (F)LFN; findKey
# ^ 按时间精确找关键帧;找不到返回 null
FI D (LFN;Z)V insertKey
# ^ 插入关键帧(第二参=是否记撤销)
FI L ()V invalidateRuntime
# ^ 标脏运行时轴(改完关键帧调它,让编辑器重建 il)
FI D (FI;LFH;)V copyFrom
# ---- FN = 关键帧(数值字段以动画 JSON 导出器为准)----
FN D F time
FN D LFV; curve
# ^ 曲线类型;FV 枚举见下
FN B F curveC2x
FN L F curveC2y
FN I F curveC3x
FN S F curveC3y
# ^ 贝塞尔四个控制值(新建关键帧默认 0.25 / 0 / 0.75 / 1)
FN D (F)V setAngle
FN D ()F getAngle
# ^ 旋转轴的 angle(**不是** FN.D():ga!ga 是 deform 的数值数组)
FN B (F)V setX
FN B ()F getX
FN L (F)V setY
FN L ()F getY
# ^ 位移/缩放/斜切轴的 x / y
FN D Lcom/badlogic/gdx/graphics/Color; color
FN D LGn; attachment
# ^ 插槽颜色 / 挂哪个附件(null = 隐藏)
FN D LOr; valueSchema
FN D ()Lga; values
# ^ deform 用的数值数组(旋转/位移/缩放/斜切**不要**写这里)
# ---- FV = 曲线枚举(名字来自 FV.<clinit>)----
FV D LFV; linear
FV B LFV; curve
FV L LFV; stepped
# ^ 依次是 linear / curve(贝塞尔) / stepped;"自动"= curve + 一组控制值(如 0.25,0.25,0.75,0.75)
# ---- WidgetGroup 的尺寸读取(本次用来把对话框撑到合适大小)---- # ---- WidgetGroup 的尺寸读取(本次用来把对话框撑到合适大小)----
eX LineNumberTable ()F getMinWidth eX LineNumberTable ()F getMinWidth
eX String ()F getMinHeight eX String ()F getMinHeight
Can't render this file because it contains an unexpected character in line 41 and column 97.
+42
View File
@@ -1,5 +1,34 @@
# 合并后的名字映射表:确定(=)指纹 + 手动/实测锚点 # 合并后的名字映射表:确定(=)指纹 + 手动/实测锚点
# owner name desc newName # owner name desc newName
FH D LGl; animation
FH D LGp; bone
FH D LfI timelines
FI D (F)LFN; findKey
FI D (FI;LFH;)V copyFrom
FI D (LFN;Z)V insertKey
FI D LfI keys
FI D Ljava/lang/Class; runtimeTimelineClass
FI L ()V invalidateRuntime
FN B ()F getX
FN B (F)V setX
FN B F curveC2x
FN D ()F getAngle
FN D ()Lga; values
FN D (F)V setAngle
FN D F time
FN D LFV; curve
FN D LGn; attachment
FN D LOr; valueSchema
FN D Lcom/badlogic/gdx/graphics/Color; color
FN I F curveC3x
FN L ()F getY
FN L (F)V setY
FN L F curveC2y
FN S F curveC3y
FT D F fps
FV B LFV; curve
FV D LFV; linear
FV L LFV; stepped
GD D F position GD D F position
GD D LGU; target GD D LGU; target
GD D Ljava/lang/String; name GD D Ljava/lang/String; name
@@ -25,6 +54,8 @@ GO L F scaleX
GO S F rotation GO S F rotation
GO Z F height GO Z F height
GP D (LGp;)V addBone GP D (LGp;)V addBone
GP D LFT; projectSettings
GP I LfI animations
GP L (LGn;ZZ)V removeAttachment GP L (LGn;ZZ)V removeAttachment
GP L LfI attachments GP L LfI attachments
GP Z LfI slots GP Z LfI slots
@@ -51,6 +82,17 @@ GX SourceFile F rotation
GX StackMapTable F scaleX GX StackMapTable F scaleX
GX Z F scaleMix GX Z F scaleMix
GX init F scaleY GX init F scaleY
Gl B (LGp;)LFI; translateTimeline
Gl B (LGp;LGU;)LFI; slotAttachmentTimeline
Gl D ()LFI; drawOrderTimeline
Gl D ()V register
Gl D (LGp;)LFI; rotateTimeline
Gl D (LGp;LGU;)LFI; slotColorTimeline
Gl D LGP; skeleton
Gl D LfI timelineGroups
Gl D Ljava/lang/String; name
Gl I (LGp;)LFI; shearTimeline
Gl L (LGp;)LFI; scaleTimeline
Gn D LHi; placeholderRef Gn D LHi; placeholderRef
Gn D Ljava/lang/String; name Gn D Ljava/lang/String; name
Gn I ()Ljava/lang/String; type Gn I ()Ljava/lang/String; type
1 # 合并后的名字映射表:确定(=)指纹 + 手动/实测锚点
2 # owner
3 FH
4 FH
5 FH
6 FI
7 FI
8 FI
9 FI
10 FI
11 FI
12 FN
13 FN
14 FN
15 FN
16 FN
17 FN
18 FN
19 FN
20 FN
21 FN
22 FN
23 FN
24 FN
25 FN
26 FN
27 FN
28 FT
29 FV
30 FV
31 FV
32 GD
33 GD
34 GD
54 GO
55 GO
56 GP
57 GP
58 GP
59 GP
60 GP
61 GP
82 GX
83 GX
84 GX
85 Gl
86 Gl
87 Gl
88 Gl
89 Gl
90 Gl
91 Gl
92 Gl
93 Gl
94 Gl
95 Gl
96 Gn
97 Gn
98 Gn
@@ -0,0 +1,403 @@
package com.spineview;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
/**
* 动画面板工具(正式按钮,重启后仍在):
* - "Animate Random Props":随机骨骼/插槽/附件,把所有能 K 帧的属性做成 0f/30f 两个关键帧(值变化明显)
* - "Curve Test 6 Modes":6 根骨骼各一条旋转轴(0°→45°,0f/30f),分别用 linear / curve 平滑 /
* stepped / ease-in / ease-out / 对称自动 六种曲线
*
* 动画模型(全部来自字节码,见 混淆名对照表.md 第十一节):
* GP.I:fI = 动画表;Gl = 动画(D:LGP 骨架、D:String 名字、D:fI 时间轴组)
* Gl.D(Gp)=旋转、Gl.B(Gp)=位移、Gl.L(Gp)=缩放、Gl.I(Gp)=斜切
* Gl.D(Gp,GU)=插槽颜色、Gl.B(Gp,GU)=附件切换
* FH = 组(D:Gl、D:Gp、D:fI 时间轴);FI = 时间轴(D:Class 运行时类、D:fI 关键帧、L() 重建)
* FN = 关键帧(D:F 时间、D:FV 曲线、B/L/I/S 贝塞尔控制值;
* D(float)=angle、B(float)=x、L(float)=y、D:Color 颜色、D:Gn 附件)
* FV = 曲线枚举(D=linear、B=curve、L=stepped)
*/
final class AnimationTools {
private static boolean installed;
private AnimationTools() {
}
/** 由 ObjectPanel.build 调用:加两个按钮并接上点击。 */
static void install(final ClassLoader loader, Object skin, Object buttonsTable,
Class<?> tableClass, final Class<?> cellClass, final Class<?> actorClass) {
if (installed) return;
try {
addButton(loader, skin, buttonsTable, cellClass, actorClass, "Animate Random Props",
new Runnable() {
@Override public void run() {
final ClassLoader classLoader = loader;
Thread thread = new Thread(new Runnable() {
@Override public void run() {
try { animateRandom(classLoader); } catch (Throwable error) {
System.err.println("[animtool] 随机动画失败: " + error);
error.printStackTrace(System.err);
}
}
}, "spineview-animate-random");
thread.setDaemon(true);
thread.start();
}
});
addButton(loader, skin, buttonsTable, cellClass, actorClass, "Curve Test 6 Modes",
new Runnable() {
@Override public void run() {
final ClassLoader classLoader = loader;
Thread thread = new Thread(new Runnable() {
@Override public void run() {
try { curveTest(classLoader); } catch (Throwable error) {
System.err.println("[animtool] 曲线测试失败: " + error);
error.printStackTrace(System.err);
}
}
}, "spineview-curve-test");
thread.setDaemon(true);
thread.start();
}
});
installed = true;
System.err.println("[animtool] 两个动画按钮已加入");
} catch (Throwable error) {
System.err.println("[animtool] 加按钮失败: " + error);
}
}
private static void addButton(ClassLoader loader, Object skin, Object buttonsTable,
Class<?> cellClass, Class<?> actorClass, String text, Runnable action) throws Exception {
Class<?> textButton = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.TextButton", true, loader);
Class<?> skinClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.Skin", true, loader);
final Object button = textButton.getConstructor(String.class, skinClass).newInstance(text, skin);
Object cell = find(buttonsTable.getClass(), "D", new Class<?>[] {actorClass}, cellClass)
.invoke(buttonsTable, button);
find(cellClass, "C", new Class<?>[0], cellClass).invoke(cell); // Cell.left()
find(buttonsTable.getClass(), "I", new Class<?>[0], cellClass).invoke(buttonsTable);
ObjectPanel.wire(loader, button, action);
}
// ==================== 按钮 1:随机对象的所有可 K 属性 ====================
static void animateRandom(ClassLoader loader) throws Exception {
final ClassLoader L = loader;
final Class<?> gp = Class.forName("GP", true, L);
final Class<?> boneC = Class.forName("Gp", true, L);
final Class<?> slotC = Class.forName("GU", true, L);
final Class<?> attC = Class.forName("Gn", true, L);
final Class<?> glC = Class.forName("Gl", true, L);
final Class<?> fiC = Class.forName("FI", true, L);
final Class<?> fnC = Class.forName("FN", true, L);
final Class<?> colorC = Class.forName("com.badlogic.gdx.graphics.Color", true, L);
SceneActions.onEditorThread(L, new Runnable() {
@Override public void run() {
try {
Object skeleton = null;
List<Object> bones = new ArrayList<Object>(), slots = new ArrayList<Object>(),
atts = new ArrayList<Object>();
for (Object o : SceneObjects.listAllNodes(L)) {
if (o == null) continue;
if (skeleton == null && gp.isInstance(o)) skeleton = o;
if (boneC.isInstance(o)) bones.add(o);
if (slotC.isInstance(o)) slots.add(o);
if (attC.isInstance(o)) atts.add(o);
}
if (skeleton == null || bones.isEmpty() || slots.isEmpty() || atts.isEmpty()) {
System.err.println("[animtool] 场景对象不够");
return;
}
Random random = new Random();
Object bone = bones.get(random.nextInt(bones.size()));
Object slot = slots.get(random.nextInt(slots.size()));
Object slotBone = fieldOfType(slotC, slot, boneC);
List<Object> own = new ArrayList<Object>();
for (Object a : atts) if (fieldOfType(attC, a, slotC) == slot) own.add(a);
Object attachment = own.isEmpty() ? atts.get(random.nextInt(atts.size()))
: own.get(random.nextInt(own.size()));
System.err.println("[animtool] 随机选中:骨骼=" + SceneObjects.nameOf(bone)
+ " 插槽=" + SceneObjects.nameOf(slot) + " 附件=" + SceneObjects.nameOf(attachment));
Object anim = animation(L, skeleton, "ani_api_test");
set(anim, "D", Class.forName("fI", true, L),
Class.forName("fI", true, L).getConstructor().newInstance());
Object rotate = find(glC, "D", new Class<?>[] {boneC}, fiC).invoke(anim, bone);
scalarPair(L, rotate, fiC, fnC, 0f, "D", 0f);
scalarPair(L, rotate, fiC, fnC, 30f, "D", 90f);
Object translate = find(glC, "B", new Class<?>[] {boneC}, fiC).invoke(anim, bone);
xy(L, translate, fiC, fnC, 0f, 0f, 0f);
xy(L, translate, fiC, fnC, 30f, 120f, -60f);
Object scale = find(glC, "L", new Class<?>[] {boneC}, fiC).invoke(anim, bone);
xy(L, scale, fiC, fnC, 0f, 1f, 1f);
xy(L, scale, fiC, fnC, 30f, 1.6f, 0.6f);
Object shear = find(glC, "I", new Class<?>[] {boneC}, fiC).invoke(anim, bone);
xy(L, shear, fiC, fnC, 0f, 0f, 0f);
xy(L, shear, fiC, fnC, 30f, 25f, 15f);
Object color = find(glC, "D", new Class<?>[] {boneC, slotC}, fiC).invoke(anim, slotBone, slot);
color(L, color, fiC, fnC, colorC, 0f, 1f, 1f, 1f, 1f);
color(L, color, fiC, fnC, colorC, 30f, 1f, 0.2f, 0.2f, 0.25f);
Object att = find(glC, "B", new Class<?>[] {boneC, slotC}, fiC).invoke(anim, slotBone, slot);
attachment(L, att, fiC, fnC, attC, 0f, attachment);
attachment(L, att, fiC, fnC, attC, 30f, null);
refresh(L, skeleton);
System.err.println("[animtool] ani_api_test 已重建:6 条时间轴(0f/30f)");
} catch (Throwable error) {
System.err.println("[animtool] 随机动画失败: " + error);
error.printStackTrace(System.err);
}
}
});
}
// ==================== 按钮 2:六种曲线 ====================
static void curveTest(ClassLoader loader) throws Exception {
final ClassLoader L = loader;
final Class<?> gp = Class.forName("GP", true, L);
final Class<?> boneC = Class.forName("Gp", true, L);
final Class<?> glC = Class.forName("Gl", true, L);
final Class<?> fiC = Class.forName("FI", true, L);
final Class<?> fnC = Class.forName("FN", true, L);
final Class<?> fvC = Class.forName("FV", true, L);
SceneActions.onEditorThread(L, new Runnable() {
@Override public void run() {
try {
Object skeleton = null;
Map<String, Object> bones = new LinkedHashMap<String, Object>();
for (Object o : SceneObjects.listAllNodes(L)) {
if (o == null) continue;
if (skeleton == null && gp.isInstance(o)) skeleton = o;
if (boneC.isInstance(o)) bones.put(SceneObjects.nameOf(o), o);
}
if (skeleton == null) return;
String[] order = {"root", "bone", "bone2", "bone3", "bone5", "lizi"};
String[] labels = {"linear", "curve smooth", "stepped", "ease-in", "ease-out", "symmetric auto"};
Object linear = fvC.getField("D").get(null);
Object curve = fvC.getField("B").get(null);
Object stepped = fvC.getField("L").get(null);
Object[] modes = {linear, curve, stepped, curve, curve, curve};
float[][] controls = {{0, 0, 0, 0}, {0.25f, 0f, 0.75f, 1f}, {0, 0, 0, 0},
{0.42f, 0f, 1f, 1f}, {0f, 0f, 0.58f, 1f}, {0.25f, 0.25f, 0.75f, 0.75f}};
Object anim = animation(L, skeleton, "curve_test");
set(anim, "D", Class.forName("fI", true, L),
Class.forName("fI", true, L).getConstructor().newInstance());
int made = 0;
for (int i = 0; i < order.length; i++) {
Object bone = bones.get(order[i]);
if (bone == null) continue;
Object tl = find(glC, "D", new Class<?>[] {boneC}, fiC).invoke(anim, bone);
Object k0 = key(L, tl, fiC, fnC, 0f);
Object k1 = key(L, tl, fiC, fnC, 30f);
setFloatMethod(fnC, k0, "D", 0f); // angle 0
setFloatMethod(fnC, k1, "D", 45f); // angle 45
set(k0, "D", fvC, modes[i]);
set(k0, "B", float.class, Float.valueOf(controls[i][0]));
set(k0, "L", float.class, Float.valueOf(controls[i][1]));
set(k0, "I", float.class, Float.valueOf(controls[i][2]));
set(k0, "S", float.class, Float.valueOf(controls[i][3]));
rebuild(fiC, tl);
System.err.println("[animtool] curve_test " + order[i] + " = " + labels[i]);
made++;
}
refresh(L, skeleton);
System.err.println("[animtool] curve_test 已重建:共 " + made + " 条旋转轴(0f/30f,0°→45°)");
} catch (Throwable error) {
System.err.println("[animtool] 曲线测试失败: " + error);
error.printStackTrace(System.err);
}
}
});
}
// ==================== 时间轴/关键帧 ====================
/** 取或建动画(并按编辑器自己的 Gl.D() 注册)。 */
private static Object animation(ClassLoader L, Object skeleton, String name) throws Exception {
Class<?> gp = Class.forName("GP", true, L);
Class<?> glC = Class.forName("Gl", true, L);
Class<?> fI = Class.forName("fI", true, L);
Field animsF = gp.getDeclaredField("I");
animsF.setAccessible(true);
Object anims = animsF.get(skeleton);
for (int i = 0; i < size(fI, anims); i++) {
Object a = get(fI, anims, i);
if (a != null && name.equals(fieldOfType(glC, a, String.class))) return a;
}
Object anim = glC.getConstructor().newInstance();
set(anim, "D", gp, skeleton);
set(anim, "D", String.class, name);
for (Method m : glC.getDeclaredMethods()) {
if (m.getName().equals("D") && m.getParameterTypes().length == 0
&& m.getReturnType() == void.class) { m.setAccessible(true); m.invoke(anim); }
}
add(fI, anims, anim);
System.err.println("[animtool] 新建动画 " + name);
return anim;
}
private static Object key(ClassLoader L, Object tl, Class<?> fiC, Class<?> fnC, float time)
throws Exception {
Method get = find(fiC, "D", new Class<?>[] {float.class}, fnC);
Object k = get.invoke(tl, Float.valueOf(time));
if (k != null) return k;
k = fnC.getConstructor(fiC).newInstance(tl);
set(k, "D", float.class, Float.valueOf(time));
find(fiC, "D", new Class<?>[] {fnC, boolean.class}, void.class)
.invoke(tl, k, Boolean.FALSE);
return k;
}
private static void scalarPair(ClassLoader L, Object tl, Class<?> fiC, Class<?> fnC,
float time, String setter, float value) throws Exception {
Object k = key(L, tl, fiC, fnC, time);
setFloatMethod(fnC, k, setter, value);
rebuild(fiC, tl);
}
private static void xy(ClassLoader L, Object tl, Class<?> fiC, Class<?> fnC,
float time, float x, float y) throws Exception {
Object k = key(L, tl, fiC, fnC, time);
setFloatMethod(fnC, k, "B", x);
setFloatMethod(fnC, k, "L", y);
rebuild(fiC, tl);
}
private static void color(ClassLoader L, Object tl, Class<?> fiC, Class<?> fnC, Class<?> colorC,
float time, float r, float g, float b, float a) throws Exception {
Object k = key(L, tl, fiC, fnC, time);
Object color = fieldOfType(fnC, k, colorC);
if (color == null) color = colorC.getConstructor().newInstance();
colorC.getField("r").setFloat(color, r);
colorC.getField("g").setFloat(color, g);
colorC.getField("b").setFloat(color, b);
colorC.getField("a").setFloat(color, a);
set(k, "D", colorC, color);
rebuild(fiC, tl);
}
private static void attachment(ClassLoader L, Object tl, Class<?> fiC, Class<?> fnC,
Class<?> attC, float time, Object attachment) throws Exception {
Object k = key(L, tl, fiC, fnC, time);
set(k, "D", attC, attachment);
rebuild(fiC, tl);
}
private static void rebuild(Class<?> fiC, Object tl) throws Exception {
find(fiC, "L", new Class<?>[0], void.class).invoke(tl);
}
private static void refresh(ClassLoader L, Object skeleton) throws Exception {
Class<?> gp = Class.forName("GP", true, L);
Class<?> vz = Class.forName("VZ", true, L);
Class<?> og = Class.forName("OG", true, L);
Object tree = staticField(og, vz);
Method refresh = find(vz, "D", new Class<?>[] {gp, boolean.class, boolean.class}, void.class);
if (refresh != null) refresh.invoke(tree, skeleton, Boolean.TRUE, Boolean.TRUE);
}
// ==================== 反射工具 ====================
private static void setFloatMethod(Class<?> owner, Object target, String name, float value)
throws Exception {
for (Class<?> c = owner; c != null && c != Object.class; c = c.getSuperclass()) {
for (Method m : c.getDeclaredMethods()) {
if (!m.getName().equals(name)) continue;
Class<?>[] types = m.getParameterTypes();
if (types.length != 1 || types[0] != float.class) continue;
m.setAccessible(true);
m.invoke(target, Float.valueOf(value));
return;
}
}
throw new NoSuchMethodException("float " + name);
}
private static Object fieldOfType(Class<?> owner, Object target, Class<?> type) {
for (Class<?> c = owner; c != null && c != Object.class; c = c.getSuperclass()) {
for (Field f : c.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers()) || f.getType() != type) continue;
f.setAccessible(true);
try {
Object v = f.get(target);
if (v != null) return v;
} catch (Throwable ignored) {
// 下一个
}
}
}
return null;
}
private static Object staticField(Class<?> owner, Class<?> type) {
for (Field f : owner.getDeclaredFields()) {
if (!Modifier.isStatic(f.getModifiers()) || f.getType() != type) continue;
f.setAccessible(true);
try {
Object v = f.get(null);
if (v != null) return v;
} catch (Throwable ignored) {
// 下一个
}
}
return null;
}
private static void set(Object target, String name, Class<?> type, Object value) throws Exception {
for (Class<?> c = target.getClass(); c != null && c != Object.class; c = c.getSuperclass()) {
for (Field f : c.getDeclaredFields()) {
if (!f.getName().equals(name) || f.getType() != type) continue;
f.setAccessible(true);
f.set(target, value);
return;
}
}
throw new NoSuchFieldException(name);
}
private static Method find(Class<?> owner, String name, Class<?>[] params, Class<?> ret) {
for (Class<?> c = owner; c != null && c != Object.class; c = c.getSuperclass()) {
for (Method m : c.getDeclaredMethods()) {
if (!m.getName().equals(name)) continue;
if (!Arrays.equals(m.getParameterTypes(), params)) continue;
if (ret != null && m.getReturnType() != ret) continue;
m.setAccessible(true);
return m;
}
}
throw new IllegalStateException(owner.getName() + "." + name);
}
private static void add(Class<?> arrayClass, Object array, Object item) throws Exception {
find(arrayClass, "D", new Class<?>[] {Object.class}, void.class).invoke(array, item);
}
private static int size(Class<?> arrayClass, Object array) {
if (array == null) return 0;
for (Class<?> c = arrayClass; c != null && c != Object.class; c = c.getSuperclass()) {
for (Field f : c.getDeclaredFields()) {
if (f.getType() != int.class) continue;
f.setAccessible(true);
try { return f.getInt(array); } catch (Throwable ignored) { }
}
}
return 0;
}
private static Object get(Class<?> arrayClass, Object array, int index) {
try {
return find(arrayClass, "D", new Class<?>[] {int.class}, Object.class)
.invoke(array, Integer.valueOf(index));
} catch (Throwable ignored) {
return null;
}
}
}
@@ -138,6 +138,8 @@ public final class ObjectPanel {
} }
// 新按钮:点一下弹名称面板 → 按名字匹配附件 → 在当前选中骨骼下建 15 根骨骼(每根 1 插槽 1 附件) // 新按钮:点一下弹名称面板 → 按名字匹配附件 → 在当前选中骨骼下建 15 根骨骼(每根 1 插槽 1 附件)
NewFifteen.install(loader, skin, buttons, tableClass, cellClass, actorClass); NewFifteen.install(loader, skin, buttons, tableClass, cellClass, actorClass);
// 动画工具按钮:随机对象 6 属性动画 / 六种曲线测试
AnimationTools.install(loader, skin, buttons, tableClass, cellClass, actorClass);
call(buttons, Obf.TABLE_ROW, new Class<?>[0], cellClass); call(buttons, Obf.TABLE_ROW, new Class<?>[0], cellClass);
Object nameCell = call(buttons, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, state.nameField); Object nameCell = call(buttons, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, state.nameField);
layoutCell(nameCell, Obf.CELL_LEFT); layoutCell(nameCell, Obf.CELL_LEFT);
+59
View File
@@ -531,3 +531,62 @@ macOS 文件系统大小写不敏感,解到目录里会互相覆盖 —— 所
| `eX.LineNumberTable()F` / `eX.String()F` | **getMinWidth / getMinHeight** | 同上(对话框 min=302x166) | | `eX.LineNumberTable()F` / `eX.String()F` | **getMinWidth / getMinHeight** | 同上(对话框 min=302x166) |
| `eX.b_()V` | **invalidate** | 让内容按新尺寸重新布局(和 `dl.b_()Z`=remove 同名不同描述符) | | `eX.b_()V` | **invalidate** | 让内容按新尺寸重新布局(和 `dl.b_()Z`=remove 同名不同描述符) |
| `dl.D(FFI)V` | **setPosition(x,y,alignment)** | **align=0 → 以 (x,y) 为中心**。"先出现在左下角、再跳到中间"的原因就是:先 `addActor`(默认 (0,0))渲染了一帧,350ms 后才居中。修法:**尺寸 + 居中都在同一次渲染回调里做完**(本次实测 399x170 @ (1665.5,937) → 中心正好 (1865,1022) = 3730x2044 的正中) | | `dl.D(FFI)V` | **setPosition(x,y,alignment)** | **align=0 → 以 (x,y) 为中心**。"先出现在左下角、再跳到中间"的原因就是:先 `addActor`(默认 (0,0))渲染了一帧,350ms 后才居中。修法:**尺寸 + 居中都在同一次渲染回调里做完**(本次实测 399x170 @ (1665.5,937) → 中心正好 (1865,1022) = 3730x2044 的正中) |
---
## 十一、动画模型(2026-10-17 实测:新建动画 / 4 条骨骼轴 / 2 条插槽轴 / 关键帧 / 6 种曲线全部跑通)
这一节是"在动画面板下用编辑器自身 API 建 30 帧动画"时确认的整套模型。每条都真跑过并读回验证。
### 骨架 / 动画
| 载荷签名 | 真名 | 依据 / 坑 |
| --- | --- | --- |
| `GP.I:LfI` | **animations(动画表)** | 元素是 `Gl`;新建动画后 `animations.add(gl)` |
| `GP.D:LFT;` → `FT.D:F` | **projectSettings.fps** | 本项目 fps=**30**。⚠️ 编辑器时间轴刻度直接用**帧**:关键帧时间写 0 和 30,不要换算成秒 |
| `Gl.D:LGP;` / `Gl.D:Ljava/lang/String;` | skeleton / name | |
| `Gl.D:LfI` | **timelineGroups(时间轴组表)** | 元素是 `FH`。清空动画最稳的做法:直接换一个**新的空 fI**(`removeRange` 的两个 `(int,int)` 重载实测都不缩容) |
| `Gl.D()V` | **register(注册到动画列表模型)** | 编辑器 New Animation 命令里就是这么做的 |
| `Gl.D(Gp)FI` / `Gl.B(Gp)FI` / `Gl.L(Gp)FI` / `Gl.I(Gp)FI` | **旋转 / 位移 / 缩放 / 斜切轴** | 运行时类依次 `ih` / `io` / `ii` / `ij` |
| `Gl.D(Gp,GU)FI` / `Gl.B(Gp,GU)FI` | **插槽颜色 / 附件切换轴** | 运行时类 `hU` / `ip` |
| `Gl.D()FI` | **绘制顺序轴** | 运行时类 `hZ` |
### 时间轴 / 关键帧
| 载荷签名 | 真名 | 依据 / 坑 |
| --- | --- | --- |
| `FH.D:Gl` / `FH.D:Gp` / `FH.D:LfI` | animation / bone / timelines | `FH` 是按"骨骼或插槽"分组的一层 |
| `FI.D:Ljava/lang/Class;` | **runtimeTimelineClass** | `ih`=Rotate、`io`=Translate、`ii`=Scale、`ij`=Shear、`hU`=SlotColor、`ip`=SlotAttachment、`hZ`=DrawOrder |
| `FI.D:LfI` | **keys(关键帧表)** | 元素是 `FN` |
| `FI.D(float)FN` | **findKey** | 按时间精确找关键帧(找不到返回 null) |
| `FI.D(FN,boolean)V` | **insertKey** | 插入关键帧(第二参=是否记撤销) |
| `FI.L()V` | **invalidateRuntime** | 标脏运行时轴(改完关键帧调它重建) |
| `FI.D(float,int)Z` | **addKeyFromPose** | 按当前姿态在指定时间建关键帧;⚠️ 时间轴上**必须已经有关键帧**,否则抛 `frameCount must be > 0` |
| `FN` 字段/方法 | 真名 | 依据 / 坑 |
| --- | --- | --- |
| `D:F` | **time** | 关键帧时间(帧) |
| `D:FV` | **curve** | 曲线类型,见下 |
| `B/L/I/S:F` | **curveC2x / c2y / c3x / c3y** | 贝塞尔四个控制值(新建关键帧默认 0.25 / 0 / 0.75 / 1) |
| `D(float)` / `D()F` | **setAngle / getAngle** | 旋转轴的角度。⚠️ **不是** `FN.D():ga`! |
| `B(float)` / `B()F` | **setX / getX** | 位移/缩放/斜切的 x |
| `L(float)` / `L()F` | **setY / getY** | 位移/缩放/斜切的 y |
| `D:Color` | **color** | 插槽颜色关键帧 |
| `D:Gn` | **attachment** | 附件切换关键帧(null = 隐藏) |
| `D:Or` / `D():ga` | **valueSchema / values** | **deform** 用的数值数组(旋转/位移/缩放/斜切不要写这里——曾因此"帧有值但不生效") |
### 曲线枚举 `FV`(名字取自 `<clinit>`)
| 字段 | 真名 |
| --- | --- |
| `FV.D` | **linear(线性)** |
| `FV.B` | **curve(贝塞尔)** |
| `FV.L` | **stepped(阶跃)** |
"自动"不是单独的枚举值,而是 **curve + 一组控制值**(本次实测过 0.25,0.25,0.75,0.75 的对称自动;ease-in 用 0.42,0,1,1;ease-out 用 0,0,0.58,1)。
### 面板按钮(正式源码)
- `viewpanel/src/com/spineview/AnimationTools.java`
- **Animate Random Props**:随机骨骼/插槽/附件 → `ani_api_test`:旋转 0°→90°、位移 (0,0)→(120,-60)、缩放 (1,1)→(1.6,0.6)、斜切 (0,0)→(25,15)、插槽颜色 (1,1,1,1)→(1,0.2,0.2,0.25)、附件 显示→隐藏;全部只在 0f/30f
- **Curve Test 6 Modes**:`curve_test`:6 根骨骼各一条旋转轴(0°→45°,0f/30f),分别 linear / curve 平滑 / stepped / ease-in / ease-out / 对称自动