arona-agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/assets/blue-archive/arona/spine/arona_spr.atlas.txt +1168 -0
- package/assets/blue-archive/arona/spine/arona_spr.png +0 -0
- package/assets/blue-archive/arona/spine/arona_spr.skel +0 -0
- package/assets/blue-archive/arona/voice.mp3 +0 -0
- package/assets/blue-archive/hoshino/hoshino_spr.atlas.txt +174 -0
- package/assets/blue-archive/hoshino/hoshino_spr.json +15385 -0
- package/assets/blue-archive/hoshino/hoshino_spr.png +0 -0
- package/assets/blue-archive/hoshino/hoshino_spr.skel +0 -0
- package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.atlas.txt +111 -0
- package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.png +0 -0
- package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.skel +0 -0
- package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.atlas.txt +251 -0
- package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.png +0 -0
- package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.skel +0 -0
- package/assets/blue-archive/hoshino/voice.mp3 +0 -0
- package/assets/blue-archive/plana/spine/plana_spr.atlas.txt +1266 -0
- package/assets/blue-archive/plana/spine/plana_spr.png +0 -0
- package/assets/blue-archive/plana/spine/plana_spr.skel +0 -0
- package/assets/blue-archive/plana/voice.mp3 +0 -0
- package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.atlas.txt +97 -0
- package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.png +0 -0
- package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.skel +0 -0
- package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.atlas.txt +174 -0
- package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.png +0 -0
- package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.skel +0 -0
- package/assets/blue-archive/shiroko/shiroko_spr.atlas.txt +167 -0
- package/assets/blue-archive/shiroko/shiroko_spr.json +10702 -0
- package/assets/blue-archive/shiroko/shiroko_spr.png +0 -0
- package/assets/blue-archive/shiroko/shiroko_spr.skel +0 -0
- package/assets/blue-archive/shiroko/voice.mp3 +0 -0
- package/bin/arona.mjs +43 -0
- package/package.json +37 -0
- package/pet/agents.cjs +197 -0
- package/pet/main.cjs +462 -0
- package/pet/preload.cjs +21 -0
- package/pet/renderer/fx.html +30 -0
- package/pet/renderer/fx.js +41 -0
- package/pet/renderer/gallery.html +18 -0
- package/pet/renderer/gallery.js +31 -0
- package/pet/renderer/index.html +18 -0
- package/pet/renderer/renderer.js +382 -0
- package/pet/renderer/spine_layer.js +609 -0
- package/pet/renderer/spinetest.html +24 -0
- package/pet/renderer/spinetest.js +295 -0
- package/pet/renderer/style.css +63 -0
- package/pet/tools/gallery_capture.cjs +267 -0
- package/pet/tools/gen_sway.cjs +170 -0
- package/pet/tools/meshify_region.cjs +105 -0
- package/pet/tools/skel_inspect.cjs +112 -0
- package/pet/tools/skel_to_json.cjs +410 -0
- package/pet/tools/spine_node.cjs +69 -0
- package/pet/tools/visual_test.cjs +262 -0
- package/pet/vendor/spine/spine-canvas.js +8472 -0
- package/pet/vendor/spine/spine-webgl.js +10839 -0
- package/python/__pycache__/_i18n.cpython-313.pyc +0 -0
- package/python/__pycache__/_i18n.cpython-314.pyc +0 -0
- package/python/__pycache__/computer_use.cpython-314.pyc +0 -0
- package/python/__pycache__/hotkey.cpython-313.pyc +0 -0
- package/python/__pycache__/hotkey.cpython-314.pyc +0 -0
- package/python/__pycache__/stt.cpython-313.pyc +0 -0
- package/python/__pycache__/stt.cpython-314.pyc +0 -0
- package/python/__pycache__/tts.cpython-313.pyc +0 -0
- package/python/__pycache__/tts.cpython-314.pyc +0 -0
- package/python/__pycache__/tts_stream.cpython-313.pyc +0 -0
- package/python/__pycache__/tts_stream.cpython-314.pyc +0 -0
- package/python/__pycache__/voice_clone.cpython-314.pyc +0 -0
- package/python/_i18n.py +18 -0
- package/python/computer_use.py +190 -0
- package/python/hotkey.py +196 -0
- package/python/stt.py +240 -0
- package/python/tts_say.py +177 -0
- package/python/voice_clone.py +98 -0
- package/requirements.txt +6 -0
- package/src/agent.ts +791 -0
- package/src/agent_registry.ts +109 -0
- package/src/commands.ts +641 -0
- package/src/config.ts +245 -0
- package/src/in_memory_credentials.ts +61 -0
- package/src/index.ts +78 -0
- package/src/locale.ts +131 -0
- package/src/logo.ts +120 -0
- package/src/mcp.ts +192 -0
- package/src/memory.ts +303 -0
- package/src/pet.ts +346 -0
- package/src/renderer.ts +350 -0
- package/src/repl.ts +902 -0
- package/src/setup.ts +371 -0
- package/src/skills.ts +96 -0
- package/src/slash_menu.ts +371 -0
- package/src/slash_registry.ts +53 -0
- package/src/speaker_context.ts +41 -0
- package/src/text_split.ts +79 -0
- package/src/tools/computer_use.ts +142 -0
- package/src/tools/emotion_tool.ts +55 -0
- package/src/tools/keep_silent_tool.ts +26 -0
- package/src/tools/memory_tool.ts +24 -0
- package/src/tools/skill_tools.ts +100 -0
- package/src/tools/tavily_tools.ts +431 -0
- package/src/tools/voice_tools.ts +28 -0
- package/src/tts_stream.ts +260 -0
- package/src/tui_select.ts +146 -0
- package/src/undo.ts +451 -0
- package/src/utils/python.ts +211 -0
- package/src/utils/spawn.ts +56 -0
- package/src/voice.ts +83 -0
- package/src/voice_cli.ts +198 -0
- package/src/voices.ts +135 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
// ARONA 桌宠 Spine 渲染层(spine_layer.js,纯 <script> UMD,无构建)
|
|
2
|
+
// 职责:加载当前 Agent(agents.cjs,经 ?agent= + pet:get-agent-config)骨架并常驻渲染
|
|
3
|
+
// (track0 = Idle_01 全身循环,基底永不隐藏),
|
|
4
|
+
// 提供摸头头部跟随 / 空闲注视 / 眨眼 / 情绪预设四个骨骼接口。
|
|
5
|
+
// 层模型:情绪 = track4 上的数字预设动画(非循环、残留末帧持有),脸+光环瞬时切换;
|
|
6
|
+
// track0 始终保持 Idle_01 循环 → 情绪期间身体继续呼吸摇摆。无 DOM 层、无溶解过渡。
|
|
7
|
+
// 手动骨骼覆盖时机(spike 实证):必须在 state.apply 之后、updateWorldTransform 之前,
|
|
8
|
+
// 否则会被 apply 的 setup 重置(slot 颜色同理,但本模块不覆盖 slot)。
|
|
9
|
+
(function () {
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
// 当前角色 id(main.cjs loadFile 注入 ?agent=;缺省 arona)。实际配置经 IPC 从主进程取,
|
|
13
|
+
// agents.cjs 是 pet 侧唯一事实源。
|
|
14
|
+
let agent = null; // init() 时从 pet:get-agent-config 获取(含 anims/bones/spineBase 等)
|
|
15
|
+
|
|
16
|
+
const TRACK = { main: 0, look: 2, patPose: 3, emotion: 4, blink: 5 };
|
|
17
|
+
// track 布局说明:
|
|
18
|
+
// 0 main Idle_01 循环常驻 / Pat_01_A(摸头)
|
|
19
|
+
// 1 (腾空) 原 blink 已上移到 5(数字预设占 4,预设的 attachment key 会覆盖低 track 的眨眼)
|
|
20
|
+
// 2 look Look_01_M / LookEnd_01_M(空闲注视)
|
|
21
|
+
// 3 patPose Pat_01_M(摸头眉毛)
|
|
22
|
+
// 4 emotion 数字情绪预设(非循环,残留末帧持有;高 track attachment 恒覆盖低 track)
|
|
23
|
+
// 5 blink Eye_Close_01(在预设之上,短暂盖住预设眼部;closedEye 预设期间禁止)
|
|
24
|
+
const DEFAULT_MIX = 0.2; // track 内 crossfade 时长
|
|
25
|
+
|
|
26
|
+
// 摸头/注视参数(初值,可调)
|
|
27
|
+
const PAT_MAX_DEG = 3; // 摸头:Head_Rot 微动幅度(用户:6° 的圆弧状摆动仍诡异,再减半)
|
|
28
|
+
const HEAD_SMOOTH_NEAR = 0.3;
|
|
29
|
+
const HEAD_SMOOTH_FAR = 0.6;
|
|
30
|
+
const HEAD_FAR_DEG = 8; // 超过该角度差用 FAR 系数(远快近慢)
|
|
31
|
+
// 构图微调:情绪 PNG 是 1010×2128 全身图,spine 包围盒 1011×2128,
|
|
32
|
+
// 两者比例一致;若溶解切换时人物轻微跳位,调这三个常量(世界单位 / 倍率)
|
|
33
|
+
const OFFSET_X = 0;
|
|
34
|
+
const OFFSET_Y = 0;
|
|
35
|
+
const EXTRA_SCALE = 1;
|
|
36
|
+
// 眼球跟随:虹膜(含瞳孔)相对眼骨 rest 的最大位移(世界单位,初值可调)
|
|
37
|
+
// 世界→屏幕换算:viewport ≈ 1011 世界单位 对 640 backing px(约 0.63 px/单位),
|
|
38
|
+
// 10 世界单位 ≈ 3 CSS px——虹膜宽约 9 CSS px,位移幅度明显可感。
|
|
39
|
+
const EYE_MAX_OFF_X = 10;
|
|
40
|
+
const EYE_MAX_OFF_Y = 8;
|
|
41
|
+
|
|
42
|
+
let canvas = null;
|
|
43
|
+
let gl = null;
|
|
44
|
+
let ctx2d = null; // Canvas 2D 降级模式上下文(WebGL 不可用时)
|
|
45
|
+
let canvasRenderer2d = null; // spine.canvas.SkeletonRenderer(降级模式)
|
|
46
|
+
let mode = "webgl"; // "webgl" | "canvas2d"(WebGL 创建失败自动降级)
|
|
47
|
+
let cam2d = { scale: 1, cx: 0, cy: 0, cw: 1, ch: 1 }; // 2D 模式投影参数(fitCamera 填充)
|
|
48
|
+
let assetManager = null;
|
|
49
|
+
let skeleton = null;
|
|
50
|
+
let state = null;
|
|
51
|
+
let renderer = null;
|
|
52
|
+
let headBone = null;
|
|
53
|
+
let headRestRot = 0;
|
|
54
|
+
let eyeL = null;
|
|
55
|
+
let eyeR = null;
|
|
56
|
+
let eyeOff = { x: 0, y: 0 };
|
|
57
|
+
let lastT = 0;
|
|
58
|
+
let cursorPos = null;
|
|
59
|
+
let patting = false;
|
|
60
|
+
let gazeEnabled = false;
|
|
61
|
+
let headRotSmoothed = 0;
|
|
62
|
+
let currentPreset = null; // track4 当前情绪预设动画名(null = 无情绪)
|
|
63
|
+
let presetClosedEye = false; // 当前预设是否闭眼(闭眼预设期间禁止眨眼,防 cover 被眨眼末帧置 null 造成睁眼闪)
|
|
64
|
+
let presetGaze = true; // 当前预设是否保留瞳孔跟随(闭眼 / 禁跟随预设为 false)
|
|
65
|
+
// 精灵图角色的摸头:记录进入摸头前的情绪预设,结束摸头时还原(若无则摘除预设)
|
|
66
|
+
let patPrevPreset = null;
|
|
67
|
+
// 精灵图角色摸头微倾骨(如 PC_Layer);骨脸角色为 null
|
|
68
|
+
let tiltBone = null;
|
|
69
|
+
let tiltRestRot = 0;
|
|
70
|
+
// 精灵图角色摸头“仅头动”用的 mesh 槽位:直接旋转脸部/头发 Mesh 顶点,避免整身晃动
|
|
71
|
+
let tiltSlots = []; // { slot, att, base, pivotX, pivotY }
|
|
72
|
+
// 调试用身体冻结:非 null 时每帧把 track0 Idle_01 的 trackTime 钉在固定相位(gallery 截图用,
|
|
73
|
+
// 保证不同批次截图身体姿势完全一致、可直接像素对比;桌宠正常运行不设置,零影响)
|
|
74
|
+
let pinBodyT = null;
|
|
75
|
+
|
|
76
|
+
function clamp(v, lo, hi) {
|
|
77
|
+
return v < lo ? lo : v > hi ? hi : v;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 窗口 CSS px → 骨架世界坐标(与绘制投影严格一致:WebGL 用 runtime 逆投影,
|
|
81
|
+
// canvas2d 用 cam2d 手动画的 ctx 变换之逆——注意 2D 投影含 y 轴翻转)
|
|
82
|
+
function windowToSkeleton(x, y) {
|
|
83
|
+
const cw = canvas.clientWidth || window.innerWidth;
|
|
84
|
+
const ch = canvas.clientHeight || window.innerHeight;
|
|
85
|
+
if (mode === "webgl") {
|
|
86
|
+
const p = new spine.webgl.Vector3(x, y, 0);
|
|
87
|
+
renderer.camera.screenToWorld(p, cw, ch);
|
|
88
|
+
return { x: p.x, y: p.y };
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
x: (x - cam2d.cw / 2) / cam2d.scale + cam2d.cx,
|
|
92
|
+
y: -(y - cam2d.ch / 2) / cam2d.scale + cam2d.cy,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 尺寸拟合:包围盒等比缩放填满窗口(无 pad,构图与 1010×2128 情绪 PNG 对齐)
|
|
97
|
+
// extraScale 按角色参数化(agents.cjs):Arona 1.0;Plana 1.10——其 bounds 比例偏宽、
|
|
98
|
+
// 宽度先顶格导致身高只占窗口 91%,放大后接近 Arona 的满窗高度(数据见 CLAUDE.md A4)
|
|
99
|
+
function fitCamera() {
|
|
100
|
+
const offset = new spine.Vector2();
|
|
101
|
+
const size = new spine.Vector2();
|
|
102
|
+
skeleton.getBounds(offset, size, []);
|
|
103
|
+
const cw = canvas.clientWidth || window.innerWidth;
|
|
104
|
+
const ch = canvas.clientHeight || window.innerHeight;
|
|
105
|
+
const extra = agent && typeof agent.extraScale === "number" ? agent.extraScale : EXTRA_SCALE;
|
|
106
|
+
const scale = Math.min(cw / size.x, ch / size.y) * extra;
|
|
107
|
+
const cx = offset.x + size.x / 2 + OFFSET_X;
|
|
108
|
+
const cy = offset.y + size.y / 2 + OFFSET_Y;
|
|
109
|
+
if (mode === "webgl") {
|
|
110
|
+
renderer.camera.setViewport(cw / scale, ch / scale);
|
|
111
|
+
renderer.camera.position.set(cx, cy, 0);
|
|
112
|
+
} else {
|
|
113
|
+
// canvas2d:记录投影参数,loop 每帧换算成 ctx 变换(含 y 轴翻转)
|
|
114
|
+
cam2d = { scale, cx, cy, cw, ch };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resize() {
|
|
119
|
+
const dpr = window.devicePixelRatio || 1;
|
|
120
|
+
canvas.width = Math.round((canvas.clientWidth || window.innerWidth) * dpr);
|
|
121
|
+
canvas.height = Math.round((canvas.clientHeight || window.innerHeight) * dpr);
|
|
122
|
+
if (mode === "webgl") gl.viewport(0, 0, canvas.width, canvas.height);
|
|
123
|
+
// canvas2d:无需 viewport;loop 每帧按 cam2d + dpr 重设 ctx 变换
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 手动骨骼覆盖(每帧,apply 之后):
|
|
127
|
+
// 1) 眼球跟随(空闲注视):L_Eye_01/R_Eye_01(虹膜+瞳孔所在骨)向光标偏移;
|
|
128
|
+
// 瞳孔是眼骨子骨自动跟随,眼白是独立骨保持不动 → 虹膜在眼白上滑动的 Live2D 效果。
|
|
129
|
+
// **关键坑**:本 rig 的头部骨骼帧在世界空间偏转 ~76°(附件自带 -76° 补偿所以画面正常,
|
|
130
|
+
// 但骨骼 translation 没有补偿)——直接写 local x/y 会把"光标左右"变成"瞳孔上下"。
|
|
131
|
+
// 必须用当前世界矩阵 [[a,b],[c,d]] 的逆把世界偏移转回骨局部帧(每帧自适应)。
|
|
132
|
+
// 2) 摸头 Head_Rot 跟随:仅 patting 时启用(用户确认摸头要"和光标大致方向同步";
|
|
133
|
+
// 空闲注视不歪头)。rotation 是角度量,附件补偿后与屏幕方向一致(实测 +rotation = 向右歪,
|
|
134
|
+
// 光标左应给负 rotation;水平偏移比例映射,勿用 atan2——左侧 ±180° 断点会方向反转)。
|
|
135
|
+
// ---- 精灵图角色“仅头动”辅助:直接旋转脸部/头发 Mesh 顶点 ----
|
|
136
|
+
function refreshTiltSlot(t) {
|
|
137
|
+
const att = t.slot.attachment;
|
|
138
|
+
if (!att || !att.vertices) {
|
|
139
|
+
t.att = null;
|
|
140
|
+
t.base = null;
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
if (t.att === att) return true;
|
|
144
|
+
t.att = att;
|
|
145
|
+
t.base = Array.from(att.vertices);
|
|
146
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
147
|
+
for (let i = 0; i < t.base.length; i += 2) {
|
|
148
|
+
const x = t.base[i], y = t.base[i + 1];
|
|
149
|
+
if (x < minX) minX = x;
|
|
150
|
+
if (y < minY) minY = y;
|
|
151
|
+
if (x > maxX) maxX = x;
|
|
152
|
+
if (y > maxY) maxY = y;
|
|
153
|
+
}
|
|
154
|
+
if (!isFinite(minX) || !isFinite(minY)) return false;
|
|
155
|
+
t.pivotX = (minX + maxX) / 2;
|
|
156
|
+
t.pivotY = (minY + maxY) / 2;
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function prepareTiltSlots() {
|
|
161
|
+
tiltSlots = [];
|
|
162
|
+
if (!skeleton || !agent.pat || agent.pat.type !== "emotion" || !Array.isArray(agent.pat.tiltSlots)) return;
|
|
163
|
+
for (const name of agent.pat.tiltSlots) {
|
|
164
|
+
const slot = skeleton.findSlot(name);
|
|
165
|
+
if (!slot) continue;
|
|
166
|
+
const t = { slotName: name, slot, att: null, base: null, pivotX: 0, pivotY: 0 };
|
|
167
|
+
refreshTiltSlot(t);
|
|
168
|
+
tiltSlots.push(t);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function applySlotTilt(deg) {
|
|
173
|
+
const rad = (deg * Math.PI) / 180;
|
|
174
|
+
const cos = Math.cos(rad);
|
|
175
|
+
const sin = Math.sin(rad);
|
|
176
|
+
for (const t of tiltSlots) {
|
|
177
|
+
if (!refreshTiltSlot(t)) continue;
|
|
178
|
+
const v = t.att.vertices;
|
|
179
|
+
for (let i = 0; i < v.length; i += 2) {
|
|
180
|
+
const x = t.base[i] - t.pivotX;
|
|
181
|
+
const y = t.base[i + 1] - t.pivotY;
|
|
182
|
+
v[i] = t.pivotX + x * cos - y * sin;
|
|
183
|
+
v[i + 1] = t.pivotY + x * sin + y * cos;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function restoreSlotTilt() {
|
|
189
|
+
for (const t of tiltSlots) {
|
|
190
|
+
if (!t.att || !t.base) continue;
|
|
191
|
+
for (let i = 0; i < t.att.vertices.length; i++) t.att.vertices[i] = t.base[i];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function applyManualBones() {
|
|
196
|
+
const cursor = cursorPos;
|
|
197
|
+
if (!skeleton) return;
|
|
198
|
+
// 无头/眼骨的角色(Shiroko/Hoshino 精灵图骨架)直接短路;顶多处理 PC_Layer 微倾或 Mesh 头动
|
|
199
|
+
const hasHead = !!headBone;
|
|
200
|
+
const hasEyes = !!(eyeL && eyeR);
|
|
201
|
+
const hasTilt = !!tiltBone;
|
|
202
|
+
const hasSlotTilt = tiltSlots.length > 0;
|
|
203
|
+
if (!hasHead && !hasEyes && !hasTilt && !hasSlotTilt) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
let halfW = 1;
|
|
207
|
+
let halfH = 1;
|
|
208
|
+
if (mode === "webgl" && renderer) {
|
|
209
|
+
halfW = renderer.camera.viewportWidth / 2;
|
|
210
|
+
halfH = renderer.camera.viewportHeight / 2;
|
|
211
|
+
} else if (cam2d.scale > 0) {
|
|
212
|
+
halfW = cam2d.cw / (2 * cam2d.scale);
|
|
213
|
+
halfH = cam2d.ch / (2 * cam2d.scale);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---- 摸头 Head_Rot 跟随(骨脸角色,仅摸头)----
|
|
217
|
+
// 方向(用户实测定标):光标在头左侧 → 需要正 rotation(+rotation = 头向左微歪),
|
|
218
|
+
// 与数学直觉相反(本 rig 附件旋转补偿的符号约定所致),勿再"修正"符号。
|
|
219
|
+
let target = 0;
|
|
220
|
+
if (cursor && patting && hasHead) {
|
|
221
|
+
const p = windowToSkeleton(cursor.x, cursor.y);
|
|
222
|
+
const dx = p.x - headBone.x;
|
|
223
|
+
target = clamp(-dx / halfW, -1, 1) * PAT_MAX_DEG;
|
|
224
|
+
}
|
|
225
|
+
const d = Math.abs(target - headRotSmoothed);
|
|
226
|
+
const k = d > HEAD_FAR_DEG ? HEAD_SMOOTH_FAR : HEAD_SMOOTH_NEAR;
|
|
227
|
+
headRotSmoothed += (target - headRotSmoothed) * k;
|
|
228
|
+
if (target === 0 && Math.abs(headRotSmoothed) < 0.05) headRotSmoothed = 0;
|
|
229
|
+
if (hasHead) headBone.rotation = headRestRot + headRotSmoothed;
|
|
230
|
+
|
|
231
|
+
// ---- 精灵图角色摸头微倾(仅头动:旋转脸部/头发 Mesh 顶点,不再转 PC_Layer 整身)----
|
|
232
|
+
if (hasSlotTilt) {
|
|
233
|
+
if (patting) {
|
|
234
|
+
const cw = canvas.clientWidth || window.innerWidth;
|
|
235
|
+
const tiltTarget = cursor ? clamp(-(cursor.x - cw / 2) / (cw / 2), -1, 1) * PAT_MAX_DEG : 0;
|
|
236
|
+
applySlotTilt(tiltTarget);
|
|
237
|
+
} else {
|
|
238
|
+
restoreSlotTilt();
|
|
239
|
+
}
|
|
240
|
+
} else if (hasTilt) {
|
|
241
|
+
let tiltTarget = 0;
|
|
242
|
+
if (cursor && patting) {
|
|
243
|
+
const p = windowToSkeleton(cursor.x, cursor.y);
|
|
244
|
+
const dx = p.x - tiltBone.x;
|
|
245
|
+
tiltTarget = clamp(-dx / halfW, -1, 1) * PAT_MAX_DEG;
|
|
246
|
+
}
|
|
247
|
+
tiltBone.rotation = tiltRestRot + tiltTarget;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---- 眼球跟随(仅空闲注视;摸头闭眼时跳过——眼 cover 挂在眉毛骨上,跟眼球位移会露馅)----
|
|
251
|
+
let eyeTX = 0;
|
|
252
|
+
let eyeTY = 0;
|
|
253
|
+
if (cursor && gazeEnabled && !patting && hasEyes && hasHead) {
|
|
254
|
+
const p = windowToSkeleton(cursor.x, cursor.y);
|
|
255
|
+
let nx = (p.x - headBone.x) / halfW;
|
|
256
|
+
let ny = (p.y - headBone.y) / halfH;
|
|
257
|
+
const nd = Math.hypot(nx, ny);
|
|
258
|
+
if (nd > 1) {
|
|
259
|
+
nx /= nd;
|
|
260
|
+
ny /= nd;
|
|
261
|
+
}
|
|
262
|
+
eyeTX = nx * EYE_MAX_OFF_X;
|
|
263
|
+
eyeTY = ny * EYE_MAX_OFF_Y;
|
|
264
|
+
}
|
|
265
|
+
const ed = Math.hypot(eyeTX - eyeOff.x, eyeTY - eyeOff.y);
|
|
266
|
+
const ek = ed > 2 ? HEAD_SMOOTH_FAR : HEAD_SMOOTH_NEAR;
|
|
267
|
+
eyeOff.x += (eyeTX - eyeOff.x) * ek;
|
|
268
|
+
eyeOff.y += (eyeTY - eyeOff.y) * ek;
|
|
269
|
+
if (hasEyes) {
|
|
270
|
+
// 世界偏移 → 骨局部帧:[[a,b],[c,d]] 的逆(含旋转/缩放/镜像,逐帧取当前矩阵)
|
|
271
|
+
const det = eyeL.a * eyeL.d - eyeL.b * eyeL.c;
|
|
272
|
+
if (Math.abs(det) > 1e-6) {
|
|
273
|
+
const lx = (eyeL.d * eyeOff.x - eyeL.b * eyeOff.y) / det;
|
|
274
|
+
const ly = (-eyeL.c * eyeOff.x + eyeL.a * eyeOff.y) / det;
|
|
275
|
+
eyeL.x = eyeL.data.x + lx;
|
|
276
|
+
eyeL.y = eyeL.data.y + ly;
|
|
277
|
+
eyeR.x = eyeR.data.x + lx;
|
|
278
|
+
eyeR.y = eyeR.data.y + ly;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function loop(now) {
|
|
284
|
+
const dt = clamp((now - lastT) / 1000, 0, 0.1); // 防标签页休眠跳变
|
|
285
|
+
lastT = now;
|
|
286
|
+
state.update(dt);
|
|
287
|
+
if (pinBodyT !== null) {
|
|
288
|
+
// 冻结身体相位:update 推进后再钉回,保证 apply 时 track0 姿势确定
|
|
289
|
+
const e = state.getCurrent(TRACK.main);
|
|
290
|
+
if (e) e.trackTime = pinBodyT;
|
|
291
|
+
}
|
|
292
|
+
state.apply(skeleton);
|
|
293
|
+
applyManualBones();
|
|
294
|
+
skeleton.updateWorldTransform();
|
|
295
|
+
if (mode === "webgl") {
|
|
296
|
+
gl.clearColor(0, 0, 0, 0);
|
|
297
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
298
|
+
renderer.begin();
|
|
299
|
+
renderer.drawSkeleton(skeleton, true); // premultiplied
|
|
300
|
+
renderer.end();
|
|
301
|
+
} else {
|
|
302
|
+
// Canvas 2D 降级:手动投影(等价 WebGL camera 的 setViewport+position):
|
|
303
|
+
// setTransform(dpr) 后 translate 到画布中心 → scale(scale, -scale) 缩放并翻转 y
|
|
304
|
+
// (骨架世界坐标 y 向上、canvas 2D y 向下)→ translate 到世界中心。
|
|
305
|
+
const c = ctx2d;
|
|
306
|
+
const dpr = window.devicePixelRatio || 1;
|
|
307
|
+
c.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
308
|
+
c.clearRect(0, 0, cam2d.cw, cam2d.ch);
|
|
309
|
+
c.translate(cam2d.cw / 2, cam2d.ch / 2);
|
|
310
|
+
c.scale(cam2d.scale, -cam2d.scale);
|
|
311
|
+
c.translate(-cam2d.cx, -cam2d.cy);
|
|
312
|
+
canvasRenderer2d.draw(skeleton);
|
|
313
|
+
}
|
|
314
|
+
requestAnimationFrame(loop);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function waitLoad(am) {
|
|
318
|
+
return new Promise((resolve) => {
|
|
319
|
+
const t0 = performance.now();
|
|
320
|
+
const poll = () => {
|
|
321
|
+
if (am.isLoadingComplete()) return resolve(Object.keys(am.errors).length === 0);
|
|
322
|
+
if (performance.now() - t0 > 8000) return resolve(false);
|
|
323
|
+
requestAnimationFrame(poll);
|
|
324
|
+
};
|
|
325
|
+
requestAnimationFrame(poll);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function init(targetCanvas) {
|
|
330
|
+
// 先取当前 Agent 配置(agents.cjs 单一事实源,经主进程 IPC 注入)
|
|
331
|
+
agent = await window.petAPI.getAgentConfig();
|
|
332
|
+
if (!agent) throw new Error("pet:get-agent-config 未返回配置");
|
|
333
|
+
canvas = targetCanvas;
|
|
334
|
+
gl = canvas.getContext("webgl", {
|
|
335
|
+
alpha: true,
|
|
336
|
+
premultipliedAlpha: true,
|
|
337
|
+
antialias: true,
|
|
338
|
+
});
|
|
339
|
+
if (gl) {
|
|
340
|
+
// ---- WebGL 模式(macOS 主路径;Windows 需 enable-unsafe-swiftshader 软渲染)----
|
|
341
|
+
mode = "webgl";
|
|
342
|
+
assetManager = new spine.webgl.AssetManager(gl, agent.spineBase);
|
|
343
|
+
// 纹理上传时预乘(UNPACK_PREMULTIPLY_ALPHA_WEBGL),与 drawSkeleton(skeleton, true) 的
|
|
344
|
+
// premultiplied 混合配套。注意:本 vendored 3.8 构建的 GLTexture 只有 (context, image, useMipMaps)
|
|
345
|
+
// 三个参数、无 premultiply 开关——必须在上传前手动设 pixelStorei,否则边缘 RGB 不预乘
|
|
346
|
+
// 会与 premultiplied 混合冲突,半透明边缘爆白("白边像抠图没抠好"+ 虹膜被冲淡成白)。
|
|
347
|
+
assetManager.textureLoader = (image) => {
|
|
348
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
|
349
|
+
const tex = new spine.webgl.GLTexture(gl, image, false);
|
|
350
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
351
|
+
return tex;
|
|
352
|
+
};
|
|
353
|
+
} else {
|
|
354
|
+
// ---- Canvas 2D 降级(WebGL 不可用时的保险丝,如 Windows 软渲染失败/远程桌面)----
|
|
355
|
+
// getContext("webgl") 返回 null 不锁定 canvas 类型,可再取 2d。
|
|
356
|
+
// canvas 2D 无 premultiplied 管线问题(drawImage 由浏览器按 straight alpha 合成)。
|
|
357
|
+
mode = "canvas2d";
|
|
358
|
+
console.warn("[pet:render] WebGL 不可用,已降级 Canvas 2D 渲染(性能较低,仅兜底)");
|
|
359
|
+
ctx2d = canvas.getContext("2d");
|
|
360
|
+
if (!ctx2d) throw new Error("Canvas 2D 也不可用");
|
|
361
|
+
assetManager = new spine.AssetManager(
|
|
362
|
+
(img) => ({ getImage: () => img }), // 最小 Texture:仅需 getImage()(drawImages 只读图)
|
|
363
|
+
agent.spineBase
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
// skelFile 支持两种格式:.skel 二进制(SkeletonBinary)与 .json(SkeletonJson,
|
|
367
|
+
// 由 pet/tools/skel_to_json.cjs 从 skel 导出,供 gen_sway 等离线工具注入形变动画)
|
|
368
|
+
const isJson = agent.skelFile.endsWith(".json");
|
|
369
|
+
if (isJson) assetManager.loadText(agent.skelFile);
|
|
370
|
+
else assetManager.loadBinary(agent.skelFile);
|
|
371
|
+
assetManager.loadTextureAtlas(agent.atlasFile);
|
|
372
|
+
const ok = await waitLoad(assetManager);
|
|
373
|
+
if (!ok) throw new Error("Spine 资源加载失败: " + JSON.stringify(assetManager.errors));
|
|
374
|
+
|
|
375
|
+
const atlas = assetManager.get(agent.atlasFile);
|
|
376
|
+
const atlasLoader = new spine.AtlasAttachmentLoader(atlas);
|
|
377
|
+
const skelData = isJson
|
|
378
|
+
? new spine.SkeletonJson(atlasLoader).readSkeletonData(
|
|
379
|
+
JSON.parse(assetManager.get(agent.skelFile))
|
|
380
|
+
)
|
|
381
|
+
: new spine.SkeletonBinary(atlasLoader).readSkeletonData(
|
|
382
|
+
assetManager.get(agent.skelFile)
|
|
383
|
+
);
|
|
384
|
+
skeleton = new spine.Skeleton(skelData);
|
|
385
|
+
skeleton.setToSetupPose();
|
|
386
|
+
skeleton.updateWorldTransform();
|
|
387
|
+
|
|
388
|
+
headBone = agent.bones?.head ? skeleton.findBone(agent.bones.head) : null;
|
|
389
|
+
headRestRot = headBone ? headBone.data.rotation : 0;
|
|
390
|
+
eyeL = agent.bones?.eyeL ? skeleton.findBone(agent.bones.eyeL) : null;
|
|
391
|
+
eyeR = agent.bones?.eyeR ? skeleton.findBone(agent.bones.eyeR) : null;
|
|
392
|
+
tiltBone = agent.pat?.tiltBone ? skeleton.findBone(agent.pat.tiltBone) : null;
|
|
393
|
+
tiltRestRot = tiltBone ? tiltBone.data.rotation : 0;
|
|
394
|
+
prepareTiltSlots();
|
|
395
|
+
|
|
396
|
+
const stateData = new spine.AnimationStateData(skelData);
|
|
397
|
+
stateData.defaultMix = DEFAULT_MIX;
|
|
398
|
+
state = new spine.AnimationState(stateData);
|
|
399
|
+
state.setAnimation(TRACK.main, agent.anims.idle, true);
|
|
400
|
+
|
|
401
|
+
if (mode === "webgl") {
|
|
402
|
+
renderer = new spine.webgl.SceneRenderer(canvas, gl, true);
|
|
403
|
+
} else {
|
|
404
|
+
canvasRenderer2d = new spine.canvas.SkeletonRenderer(ctx2d);
|
|
405
|
+
// drawImages 模式会跳过 mesh attachment(骨架网格变形部分不显示),
|
|
406
|
+
// 开 triangleRendering 逐三角形绘制保证骨架完整(性能换完整性,兜底场景可接受)。
|
|
407
|
+
canvasRenderer2d.triangleRendering = true;
|
|
408
|
+
}
|
|
409
|
+
resize();
|
|
410
|
+
fitCamera();
|
|
411
|
+
window.addEventListener("resize", () => {
|
|
412
|
+
resize();
|
|
413
|
+
fitCamera();
|
|
414
|
+
});
|
|
415
|
+
requestAnimationFrame((t) => {
|
|
416
|
+
lastT = t;
|
|
417
|
+
requestAnimationFrame(loop);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 情绪预设内部实现(不检查 patting 守卫;摸头内部路径复用)
|
|
422
|
+
function applyEmotionPreset(name, closedEye, gaze) {
|
|
423
|
+
currentPreset = name;
|
|
424
|
+
presetClosedEye = !!closedEye;
|
|
425
|
+
presetGaze = gaze !== false;
|
|
426
|
+
state.setAnimation(TRACK.emotion, name, false);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ---- 对外 API(renderer.js / visual_test.cjs 使用)----
|
|
430
|
+
window.SpineLayer = {
|
|
431
|
+
init,
|
|
432
|
+
|
|
433
|
+
// 光标(窗口本地 CSS px,可为负/超界;主进程 16ms 轮询发送)
|
|
434
|
+
setCursor(x, y) {
|
|
435
|
+
cursorPos = { x, y };
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
// 空闲注视开关(仅基底稳定态开启;情绪预设显示时关闭)
|
|
439
|
+
// 注视姿态(Look_01_M 单帧 pose)由非循环 track 的"残留末帧"特性天然持有;
|
|
440
|
+
// 回中动画播完显式 clearTrack(同上:防残留 track 干扰其它叠加层)
|
|
441
|
+
// 精灵图角色(无 look 动画)退化为仅记录 gazeEnabled,不播任何动画。
|
|
442
|
+
setGaze(on) {
|
|
443
|
+
gazeEnabled = !!on;
|
|
444
|
+
if (!agent.anims.look || !agent.anims.lookEnd) return;
|
|
445
|
+
if (on) {
|
|
446
|
+
state.setAnimation(TRACK.look, agent.anims.look, false);
|
|
447
|
+
} else {
|
|
448
|
+
const entry = state.setAnimation(TRACK.look, agent.anims.lookEnd, false);
|
|
449
|
+
entry.listener = { complete: () => state.clearTrack(TRACK.look) };
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
// 摸头(摇动触发):
|
|
454
|
+
// - 骨脸角色(Arona/Plana):track0 crossfade 到 Pat_01_A + track3 Pat_01_M,
|
|
455
|
+
// Head_Rot 跟随光标(applyManualBones 内,仅 patting 时)。
|
|
456
|
+
// - 精灵图角色(Shiroko):无 Pat 动画,改走 pat.emotion 情绪预设 + pat.tiltBone 微倾。
|
|
457
|
+
// - pat.type = "none"(Hoshino):不触发摸头。
|
|
458
|
+
// 摸头期间清掉注视 pose(track2),闭眼 cover 不被注视位移干扰。
|
|
459
|
+
startPat() {
|
|
460
|
+
const patType = agent.pat?.type || "anim";
|
|
461
|
+
if (patType === "none") return;
|
|
462
|
+
if (patting) return;
|
|
463
|
+
|
|
464
|
+
if (patType === "emotion") {
|
|
465
|
+
// 记录当前情绪预设,进入"摸头脸"后结束摸头时还原
|
|
466
|
+
patPrevPreset = currentPreset;
|
|
467
|
+
// pat.emotion 是情绪键(如 "enjoy"),需经 agent.emotions 解析成数字预设动画名
|
|
468
|
+
const patEmotionName = agent.pat.emotion;
|
|
469
|
+
const patPreset = (patEmotionName && agent.emotions && agent.emotions[patEmotionName]) || patEmotionName;
|
|
470
|
+
patting = true;
|
|
471
|
+
gazeEnabled = false;
|
|
472
|
+
state.clearTrack(TRACK.look);
|
|
473
|
+
applyEmotionPreset(patPreset, false, false);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
patting = true;
|
|
478
|
+
gazeEnabled = false;
|
|
479
|
+
state.clearTrack(TRACK.look);
|
|
480
|
+
if (agent.anims.pat) state.setAnimation(TRACK.main, agent.anims.pat, false);
|
|
481
|
+
if (agent.anims.patPose) state.setAnimation(TRACK.patPose, agent.anims.patPose, false);
|
|
482
|
+
},
|
|
483
|
+
// 结束摸头:
|
|
484
|
+
// - 骨脸:crossfade 回 Idle_01,恢复空闲注视(按当前预设的 gaze 标志),头部随平滑系数回正
|
|
485
|
+
// - 精灵图:还原摸头前的情绪预设(无则摘除),PC_Layer 随 applyManualBones 归零
|
|
486
|
+
endPat() {
|
|
487
|
+
if (!patting) return;
|
|
488
|
+
patting = false;
|
|
489
|
+
const patType = agent.pat?.type || "anim";
|
|
490
|
+
if (patType === "emotion") {
|
|
491
|
+
if (patPrevPreset !== null) {
|
|
492
|
+
applyEmotionPreset(patPrevPreset, presetClosedEye, presetGaze);
|
|
493
|
+
} else {
|
|
494
|
+
window.SpineLayer.clearEmotionPreset();
|
|
495
|
+
}
|
|
496
|
+
patPrevPreset = null;
|
|
497
|
+
gazeEnabled = presetGaze;
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
gazeEnabled = presetGaze;
|
|
501
|
+
state.clearTrack(TRACK.patPose);
|
|
502
|
+
if (gazeEnabled && agent.anims.look) state.setAnimation(TRACK.look, agent.anims.look, false);
|
|
503
|
+
state.setAnimation(TRACK.main, agent.anims.idle, true);
|
|
504
|
+
},
|
|
505
|
+
|
|
506
|
+
// 眨眼(track5 一次性,预设之上)。忙时返回 false:摸头中 / 闭眼情绪预设中。
|
|
507
|
+
// 注意:本 vendored 3.8.95+ 运行时非循环 track 不会自动清空(trackEnd=MAX_VALUE),
|
|
508
|
+
// 且残留 track 会持续 apply 末帧 → 必须在 complete 里显式 clearTrack(track5 不清理会
|
|
509
|
+
// 把睁眼预设的眼睛盖成闭眼末帧)。
|
|
510
|
+
blink() {
|
|
511
|
+
if (patting) return false;
|
|
512
|
+
if (currentPreset && presetClosedEye) return false;
|
|
513
|
+
const entry = state.setAnimation(TRACK.blink, agent.anims.blink, false);
|
|
514
|
+
entry.listener = { complete: () => state.clearTrack(TRACK.blink) };
|
|
515
|
+
return true;
|
|
516
|
+
},
|
|
517
|
+
|
|
518
|
+
// ---- 情绪预设(track4)----
|
|
519
|
+
// 数字预设 = 游戏原生情绪(脸+光环 attachment 集,或精灵图整脸切换),非循环、残留末帧永久持有,
|
|
520
|
+
// 与 Idle_01 低 track 无竞争(Idle 无 attachment timeline)。瞬时切换,无过渡。
|
|
521
|
+
// closedEye:闭眼预设期间禁止眨眼;gaze:是否保留瞳孔跟随(renderer 从 EMOTION_PRESET 传入)。
|
|
522
|
+
setEmotionPreset(name, closedEye, gaze) {
|
|
523
|
+
if (patting) return; // 摸头中不接受外部情绪切换(内部摸头路径用 applyEmotionPreset)
|
|
524
|
+
applyEmotionPreset(name, closedEye, gaze);
|
|
525
|
+
},
|
|
526
|
+
// 摘除情绪预设(回基底脸)。
|
|
527
|
+
// ⚠️ R2 陷阱(后续维护者勿踩):严禁用 clearTrack 摘除——本运行时 clearTrack 直接摘
|
|
528
|
+
// entry、无 mix-out,被预设 key 过的 slot 会残留预设 attachment 不还原。
|
|
529
|
+
// 必须用 setEmptyAnimation 触发 mix-out 过程的 SETUP 重置(slot 回 setup attachment),
|
|
530
|
+
// empty entry 播完再 clearTrack 防空壳残留。
|
|
531
|
+
clearEmotionPreset() {
|
|
532
|
+
currentPreset = null;
|
|
533
|
+
presetClosedEye = false;
|
|
534
|
+
presetGaze = true;
|
|
535
|
+
const entry = state.setEmptyAnimation(TRACK.emotion, 0.1);
|
|
536
|
+
entry.listener = { complete: () => state.clearTrack(TRACK.emotion) };
|
|
537
|
+
},
|
|
538
|
+
|
|
539
|
+
// 状态内省(visual_test.cjs 断言用);init 完成前 state 为 null,返回空态不抛错
|
|
540
|
+
getBoneNames() {
|
|
541
|
+
if (!skeleton) return [];
|
|
542
|
+
return skeleton.bones.map((b) => b.data.name);
|
|
543
|
+
},
|
|
544
|
+
getBones() {
|
|
545
|
+
if (!skeleton) return [];
|
|
546
|
+
return skeleton.bones.map((b) => ({
|
|
547
|
+
name: b.data.name,
|
|
548
|
+
parent: b.data.parent ? b.data.parent.name : null,
|
|
549
|
+
rotation: +b.data.rotation.toFixed(2),
|
|
550
|
+
x: +b.data.x.toFixed(1),
|
|
551
|
+
y: +b.data.y.toFixed(1),
|
|
552
|
+
length: +b.data.length.toFixed(1),
|
|
553
|
+
}));
|
|
554
|
+
},
|
|
555
|
+
getSlots() {
|
|
556
|
+
if (!skeleton) return [];
|
|
557
|
+
return skeleton.slots.map((s) => {
|
|
558
|
+
const a = s.attachment;
|
|
559
|
+
return {
|
|
560
|
+
name: s.data.name,
|
|
561
|
+
bone: s.data.boneData.name,
|
|
562
|
+
attachment: a ? a.name : null,
|
|
563
|
+
type: a ? a.constructor.name : null,
|
|
564
|
+
x: a && typeof a.x === "number" ? +a.x.toFixed(2) : null,
|
|
565
|
+
y: a && typeof a.y === "number" ? +a.y.toFixed(2) : null,
|
|
566
|
+
rotation: a && typeof a.rotation === "number" ? +a.rotation.toFixed(2) : null,
|
|
567
|
+
scaleX: a && typeof a.scaleX === "number" ? +a.scaleX.toFixed(3) : null,
|
|
568
|
+
scaleY: a && typeof a.scaleY === "number" ? +a.scaleY.toFixed(3) : null,
|
|
569
|
+
};
|
|
570
|
+
});
|
|
571
|
+
},
|
|
572
|
+
getState() {
|
|
573
|
+
if (!state) return { track0: null, track4: null, track5: null, preset: null, patting: false, gaze: false, headRot: 0, eyeOff: { x: 0, y: 0 } };
|
|
574
|
+
const t0 = state.getCurrent(TRACK.main);
|
|
575
|
+
const t4 = state.getCurrent(TRACK.emotion);
|
|
576
|
+
const t5 = state.getCurrent(TRACK.blink);
|
|
577
|
+
return {
|
|
578
|
+
track0: t0 ? t0.animation.name : null,
|
|
579
|
+
track4: t4 ? t4.animation.name : null,
|
|
580
|
+
track5: t5 ? t5.animation.name : null,
|
|
581
|
+
preset: currentPreset,
|
|
582
|
+
patting,
|
|
583
|
+
gaze: gazeEnabled,
|
|
584
|
+
headRot: +headRotSmoothed.toFixed(1),
|
|
585
|
+
eyeOff: { x: +eyeOff.x.toFixed(1), y: +eyeOff.y.toFixed(1) },
|
|
586
|
+
};
|
|
587
|
+
},
|
|
588
|
+
// 调试内省:slot 当前 attachment 名(visual_test 断言摸头闭眼 cover 等)
|
|
589
|
+
getSlotAttachment(name) {
|
|
590
|
+
if (!skeleton) return null;
|
|
591
|
+
const s = skeleton.findSlot(name);
|
|
592
|
+
return s && s.attachment ? s.attachment.name : null;
|
|
593
|
+
},
|
|
594
|
+
// 调试内省:全骨架附件签名(slot=attachment 列表)。比像素更硬的证据——
|
|
595
|
+
// 情绪预设间签名应互不相同(B5 普查);签名不同而截图相同 = capturePage 拿到过期纹理
|
|
596
|
+
getAttachmentSignature() {
|
|
597
|
+
if (!skeleton) return null;
|
|
598
|
+
const parts = [];
|
|
599
|
+
for (const s of skeleton.slots) {
|
|
600
|
+
if (s.attachment) parts.push(s.data.name + "=" + s.attachment.name);
|
|
601
|
+
}
|
|
602
|
+
return parts.join("|");
|
|
603
|
+
},
|
|
604
|
+
// 调试用:冻结 track0 身体相位(gallery 批量截图对比用;null 恢复动画)
|
|
605
|
+
pinBody(t) {
|
|
606
|
+
pinBodyT = t === undefined ? 0 : t;
|
|
607
|
+
},
|
|
608
|
+
};
|
|
609
|
+
})();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'self' file: data: blob:; script-src 'self' file:; style-src 'self' 'unsafe-inline'; img-src 'self' file: data: blob:; font-src 'self' file:; connect-src 'self' file: data: blob:">
|
|
6
|
+
<title>ARONA Spine 调试页</title>
|
|
7
|
+
<style>
|
|
8
|
+
html, body { margin: 0; padding: 0; width: 100%; height: 100%; background: transparent; overflow: hidden; }
|
|
9
|
+
#spine { position: absolute; inset: 0; width: 100%; height: 100%; }
|
|
10
|
+
#hud {
|
|
11
|
+
position: absolute; left: 4px; top: 4px; max-width: 300px;
|
|
12
|
+
background: rgba(0,0,0,.55); color: #8fd3ff;
|
|
13
|
+
font: 10px/1.5 Menlo, monospace; padding: 4px 6px; white-space: pre-wrap;
|
|
14
|
+
pointer-events: none;
|
|
15
|
+
}
|
|
16
|
+
</style>
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<canvas id="spine"></canvas>
|
|
20
|
+
<div id="hud">加载中…</div>
|
|
21
|
+
<script src="../vendor/spine/spine-webgl.js"></script>
|
|
22
|
+
<script src="spinetest.js"></script>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|