diff --git a/tools/deobf/anchors.tsv b/tools/deobf/anchors.tsv index a6798ea..56ca4ef 100644 --- a/tools/deobf/anchors.tsv +++ b/tools/deobf/anchors.tsv @@ -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 # ^ 面板自己的"罗列场景对象 / 删除对象"(走编辑器删除流程,自带撤销)——清理误建对象时用它 +# ============================================================================ +# 【动画模型】(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.)---- +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 的尺寸读取(本次用来把对话框撑到合适大小)---- eX LineNumberTable ()F getMinWidth eX String ()F getMinHeight diff --git a/tools/deobf/names.tsv b/tools/deobf/names.tsv index 1d3498f..3f82dab 100644 --- a/tools/deobf/names.tsv +++ b/tools/deobf/names.tsv @@ -1,5 +1,34 @@ # 合并后的名字映射表:确定(=)指纹 + 手动/实测锚点 # 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 LGU; target GD D Ljava/lang/String; name @@ -25,6 +54,8 @@ GO L F scaleX GO S F rotation GO Z F height GP D (LGp;)V addBone +GP D LFT; projectSettings +GP I LfI animations GP L (LGn;ZZ)V removeAttachment GP L LfI attachments GP Z LfI slots @@ -51,6 +82,17 @@ GX SourceFile F rotation GX StackMapTable F scaleX GX Z F scaleMix 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 Ljava/lang/String; name Gn I ()Ljava/lang/String; type diff --git a/viewpanel/src/com/spineview/AnimationTools.java b/viewpanel/src/com/spineview/AnimationTools.java new file mode 100644 index 0000000..7b99e4d --- /dev/null +++ b/viewpanel/src/com/spineview/AnimationTools.java @@ -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 bones = new ArrayList(), slots = new ArrayList(), + atts = new ArrayList(); + 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 own = new ArrayList(); + 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 bones = new LinkedHashMap(); + 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; + } + } +} diff --git a/viewpanel/src/com/spineview/ObjectPanel.java b/viewpanel/src/com/spineview/ObjectPanel.java index 33a8472..7835c5b 100644 --- a/viewpanel/src/com/spineview/ObjectPanel.java +++ b/viewpanel/src/com/spineview/ObjectPanel.java @@ -138,6 +138,8 @@ public final class ObjectPanel { } // 新按钮:点一下弹名称面板 → 按名字匹配附件 → 在当前选中骨骼下建 15 根骨骼(每根 1 插槽 1 附件) 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); Object nameCell = call(buttons, Obf.TABLE_ADD, new Class[] {actorClass}, cellClass, state.nameField); layoutCell(nameCell, Obf.CELL_LEFT); diff --git a/混淆名对照表.md b/混淆名对照表.md index b21f6c0..afddc52 100644 --- a/混淆名对照表.md +++ b/混淆名对照表.md @@ -531,3 +531,62 @@ macOS 文件系统大小写不敏感,解到目录里会互相覆盖 —— 所 | `eX.LineNumberTable()F` / `eX.String()F` | **getMinWidth / getMinHeight** | 同上(对话框 min=302x166) | | `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 的正中) | + +--- + +## 十一、动画模型(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`(名字取自 ``) + +| 字段 | 真名 | +| --- | --- | +| `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 / 对称自动