@ganziliang/desktop-pet 0.1.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 +444 -0
- package/assets/awaken/README.md +82 -0
- package/assets/awaken/astral-burst.png +0 -0
- package/assets/awaken/astral-charge.png +0 -0
- package/assets/awaken/nocturne-burst.png +0 -0
- package/assets/awaken/nocturne-charge.png +0 -0
- package/assets/awaken/seraph-burst.png +0 -0
- package/assets/awaken/seraph-charge.png +0 -0
- package/assets/awaken/thunder-burst.png +0 -0
- package/assets/awaken/thunder-charge.png +0 -0
- package/assets/outfits.json +18 -0
- package/assets/pet-base.png +0 -0
- package/assets/pet-blink.png +0 -0
- package/assets/pet-idle-blink.png +0 -0
- package/assets/pet-idle.png +0 -0
- package/assets/pet-run.png +0 -0
- package/assets/pet-swim.png +0 -0
- package/assets/pet-walk.png +0 -0
- package/assets/sprites.json +132 -0
- package/bin/desktop-pet.js +43 -0
- package/examples/pi-pet-bridge.ts +101 -0
- package/main.js +1143 -0
- package/package.json +58 -0
- package/preload.js +29 -0
- package/renderer/app.js +1219 -0
- package/renderer/awaken.js +775 -0
- package/renderer/fx.js +1057 -0
- package/renderer/index.html +43 -0
- package/renderer/style.css +599 -0
- package/scripts/debug-run.ps1 +6 -0
- package/scripts/drop-probe.ps1 +120 -0
- package/scripts/hold-probe.ps1 +86 -0
- package/scripts/knockout.py +98 -0
- package/scripts/roam-probe.ps1 +95 -0
- package/scripts/sim-drag.ps1 +107 -0
- package/scripts/spritesheet.py +425 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ganziliang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
# 桌面动漫角色小控件(desktop-pet)
|
|
2
|
+
|
|
3
|
+
Electron 做的桌面桌宠:透明无边框置顶窗 + 真实像素级命中测试 + 本地消息接口。
|
|
4
|
+
|
|
5
|
+
## 快速开始
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd desktop-pet
|
|
9
|
+
npm install
|
|
10
|
+
npm start
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
启动后角色会出现在屏幕右下角。首次启动会自己打个招呼。
|
|
14
|
+
|
|
15
|
+
### 从 npm 安装(不用 clone 仓库)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# 直接跑,不长期安装
|
|
19
|
+
npx @ganziliang/desktop-pet
|
|
20
|
+
|
|
21
|
+
# 或者装成全局命令
|
|
22
|
+
npm install -g @ganziliang/desktop-pet
|
|
23
|
+
desktop-pet
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
包内自带 electron 依赖,命令行参数原样透传给主进程(`--snapshot`、`--anim=<walk|run|swim|awaken:seraph>`、`--no-auto`)。
|
|
27
|
+
只发布运行需要的素材,`assets/source/`(生图原始图,约 39MB)与 `character_green*.png` 不进包。
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## 交互
|
|
31
|
+
|
|
32
|
+
| 操作 | 反应 |
|
|
33
|
+
|---|---|
|
|
34
|
+
| 按住拖动 | 窗口跟着走,被甩得越快身体摆得越狠,松手落地弹一下。**拖到哪就留在哪** |
|
|
35
|
+
| 单击 | 跳一下 + 甜妹语气台词 + 飘爱心(按下后漂移 < 12px 也算单击) |
|
|
36
|
+
| 双击 | 转圈跳舞 |
|
|
37
|
+
| 鼠标悬停 | 眼神/身体轻微跟随光标,高亮发光 |
|
|
38
|
+
| 75 秒无操作 | 打瞌睡(Zzz…),鼠标靠近立刻醒并吐槽一句 |
|
|
39
|
+
| 右键 | 原生菜单:动作 / 走动 / 穿透 / 置顶 / 缩放 / 退出 |
|
|
40
|
+
| 鼠标移到透明区域 | 自动穿透,不挡桌面图标(点得到下面的东西) |
|
|
41
|
+
| 托盘图标 | 显示隐藏、穿透、置顶、缩放、自动活动、开机自启、退出 |
|
|
42
|
+
|
|
43
|
+
## 觉醒技能(战斗服装 + 多帧演出)
|
|
44
|
+
|
|
45
|
+
右键菜单 / 托盘 → 「✨ 觉醒技能」,四套服装各有自己的觉醒:
|
|
46
|
+
|
|
47
|
+
| 服装 | 觉醒 | 特效 |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| 圣光 · 炽天使 | 圣裁之光 | 金白光柱 + 光翼 + 圆形符文阵 + 羽毛飘落 |
|
|
50
|
+
| 暗影 · 血月狂想 | 夜狱降临 | 血月升起 + 蝠影 + 血色残雷 + 血晶炸裂 |
|
|
51
|
+
| 雷霆 · 天罚 | 雷神裁决 | 雷云 + 连锁闪电 + 持续落雷 + 电磁环 |
|
|
52
|
+
| 幻梦 · 星海 | 星海咏叹 | 星尘漩涡 + 星河光带 + 水晶碎片 + 星屑喷泉 |
|
|
53
|
+
|
|
54
|
+
快捷键 `Ctrl+Shift+A` 随机来一个。演出约 5.7 秒,期间鼠标穿透,用户点一下可随时叫停。
|
|
55
|
+
|
|
56
|
+
### 演出是一条时间线,角色本身也是真序列帧
|
|
57
|
+
|
|
58
|
+
每套觉醒是**两张 4 帧的横向精灵图**(蓄力 / 爆发),帧号写在 def 的 `sheet` 里,
|
|
59
|
+
和特效事件共用同一条时间轴,所以「动作推到哪一帧」和「特效炸在哪一刻」是对死的:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
0ms 黑屏切入(canvas 暗幕,全黑 0.94)
|
|
63
|
+
180ms 窗口放大(藏在黑幕里,看不见窗口在变形)
|
|
64
|
+
560ms 暗幕提到 0.6 —— 角色以「蓄力」序列帧亮相
|
|
65
|
+
蓄力 4 帧 × 360ms:合手聚气 → 抬手 → 上举 → 蓄力顶点
|
|
66
|
+
同时跑魔法阵、向心汇聚、脚下螺旋
|
|
67
|
+
2000ms 爆发(revealAt):白闪 + 抖屏 + 冲击波,切到「爆发」序列帧
|
|
68
|
+
爆发 4 帧 × 300ms:张臂 → 光爆 → 释放 → 收势
|
|
69
|
+
2200ms 光柱冲天 + 主爆星屑
|
|
70
|
+
2650ms 技能名落字
|
|
71
|
+
4400ms 暗幕再进 + 窗口收回(同样藏在黑幕里)
|
|
72
|
+
5700ms 暗幕退掉,回到站立
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
暗幕在 `charge.at` 就从全黑提到 0.6,所以**蓄力动作是真的看得见的** ——
|
|
76
|
+
不是一层黑幕盖满五秒再炸一下。
|
|
77
|
+
|
|
78
|
+
### 素材是两步,不是一步
|
|
79
|
+
|
|
80
|
+
每套服装两张图:`assets/awaken/<id>-charge.png`、`assets/awaken/<id>-burst.png`,
|
|
81
|
+
各 4 帧,过 `spritesheet.py --frames 4 --norm-axis h --size 620`(**和 idle 同一套参数**)。
|
|
82
|
+
|
|
83
|
+
**管线命令必须带 `--manifest assets/sprites.json`**:它的默认值是「输出目录/sprites.json」,
|
|
84
|
+
输出目录是 `assets/awaken/`,漏传就会把清单写进 `assets/awaken/sprites.json`,
|
|
85
|
+
而主进程只读 `assets/sprites.json` —— 现象是**素材明明在、游戏里却没换衣服**,而且是静默失败。
|
|
86
|
+
(`PET_DEBUG=1` 启动时如果发现那个文件存在,日志里会告警。)
|
|
87
|
+
|
|
88
|
+
生图提示词的必备句式、帧节拍、加第五套的步骤全写在 `assets/awaken/README.md`。
|
|
89
|
+
素材缺哪个阶段就退到下一档(爆发 → 蓄力 → 单帧立绘 → idle 帧),**光效照跑,只是不换衣服**。
|
|
90
|
+
|
|
91
|
+
### 两个技术决定
|
|
92
|
+
|
|
93
|
+
**窗口是「屏幕的百分之几」而不是全屏**:默认 `46% × 72%` 工作区大小(改 `assets/outfits.json`
|
|
94
|
+
的 `stage` 即可)。而且只向上、向两侧长,保住角色脚下那条线和水平中心,所以窗口变大的时候
|
|
95
|
+
角色在屏幕上的位置几乎不动。
|
|
96
|
+
|
|
97
|
+
**特效是 canvas 不是 DOM**:觉醒爆发一帧要几百个粒子,DOM 粒子(一个粒子一个元素 +
|
|
98
|
+
一条 CSS 动画)会直接把合成层压垮。`renderer/fx.js` 是一层铺满窗口的透明 canvas,
|
|
99
|
+
预渲染贴图 + 对象池 + `lighter` 加色混合。
|
|
100
|
+
|
|
101
|
+
两个坑写在注释里了,这里也记一下:
|
|
102
|
+
|
|
103
|
+
- **加色混合画不出黑色**(黑加什么都是原色),所以蝠影 / 乌云这类「暗的东西」走
|
|
104
|
+
`norm: true` 的普通混合通道,在亮色光效之前先画
|
|
105
|
+
- **亮色桌面上光效看不清**(白底加白光还是白),所以亮相之后垫一层 `soft` 暗域:
|
|
106
|
+
中心浓、边缘透明 —— 金光 / 雷光才真的亮得起来,又不会把桌面整个盖住
|
|
107
|
+
|
|
108
|
+
### 逐帧播放为什么不用 CSS `steps()`
|
|
109
|
+
|
|
110
|
+
走跑游泳那种循环动作用 `steps()` 最省事,但觉醒不行:一套觉醒要「蓄力慢、爆发快」两种节奏,
|
|
111
|
+
`steps()` 一条动画只能一个速度;而且帧必须和特效事件对死。
|
|
112
|
+
所以觉醒的帧由渲染层直接写 `backgroundPositionX` 推(`awakenFrame()`)。
|
|
113
|
+
|
|
114
|
+
## 自动活动(走 / 跑 / 游 / 觉醒)
|
|
115
|
+
|
|
116
|
+
不用管它的时候,角色会自己决定在桌面上活动一下。行为由渲染层调度(默认每 6~15 秒挑一个),
|
|
117
|
+
**位移由主进程驱动**:主进程用梯形速度曲线(加速 → 匀速 → 减速)推窗口位置,
|
|
118
|
+
所以起步和到位都是平滑的,而不是线性插值那种「啪」地弹出去、「撞墙」一样急停。
|
|
119
|
+
渲染层只负责把对应的序列帧播出来,两边用同一份 plan 对齐朝向和时长。
|
|
120
|
+
|
|
121
|
+
平时动作:
|
|
122
|
+
|
|
123
|
+
- **走一走** —— 慢慢踱到附近某个位置
|
|
124
|
+
- **跑一跑** —— 快速跑一小段或横跨半屏,带扬尘
|
|
125
|
+
- **游一游** —— 屏幕底部浮出水面,角色**切换成横躺俯卧的泳姿**骑在水线上,手臂做蛙泳划水,带气泡与涟漪
|
|
126
|
+
|
|
127
|
+
### 动作怎么挑:洗牌发牌
|
|
128
|
+
|
|
129
|
+
不是每次独立抛骰子,而是「洗牌发牌」—— 把一轮的牌洗乱后一张张发,发完再洗新一轮。
|
|
130
|
+
纯随机会出现「连走三次都不游」这种长连击,而用户又看不出来那是随机;发牌则是
|
|
131
|
+
「每 N 次里必定各来一次」,顺序仍然随机,不会显得机械。
|
|
132
|
+
|
|
133
|
+
**觉醒技能也在这沓牌里。** 一轮 32 张:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
// renderer/app.js —— 数字 = 一轮发牌里出现几张;'awaken:*' 写的是「每套觉醒各几张」
|
|
137
|
+
const ROAM_WEIGHTS = { walk: 4, run: 4, swim: 4, 'awaken:*': 5 };
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
也就是 **单个觉醒 5/32 ≈ 15.6%,单个平时动作 4/32 = 12.5%** —— 觉醒比平时动作略容易抽到
|
|
141
|
+
一点点(1.25 倍)。抽到哪张就放哪套觉醒,牌是 `awaken:seraph` 这种字符串。
|
|
142
|
+
|
|
143
|
+
想调密度就改这一个数:
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
const ROAM_WEIGHTS = { walk: 4, run: 4, swim: 4, 'awaken:*': 1 }; // 觉醒稀有(每套 1/16 ≈ 6%)
|
|
147
|
+
const ROAM_WEIGHTS = { walk: 4, run: 4, swim: 4, 'awaken:*': 5 }; // 默认
|
|
148
|
+
const ROAM_WEIGHTS = { walk: 4, run: 4, swim: 4, 'awaken:*': 10 }; // 觉醒很爱出
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**觉醒之后会歇一段**(`AWAKEN_REST_MS = [24, 52]` 秒)再考虑下一次活动。
|
|
152
|
+
两个原因:觉醒占屏 5.7 秒还是放大窗口的演出,刚完就又接一次很像卡住;
|
|
153
|
+
而且发牌只决定「这张是什么」,没有这个停顿的话连着抽到两张觉醒会真的背靠背放两次。
|
|
154
|
+
只有**自主抽到**的觉醒才歇这么久,你从菜单/HTTP 手动点的就正常续轮。
|
|
155
|
+
|
|
156
|
+
### 调参 / 自动化
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
PET_DEBUG=1 PET_ROAM_GAP_MS=900 npx electron .
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`PET_ROAM_GAP_MS` 把「两次活动的间隔」和「觉醒后的停顿」两个随机区间都压成固定值,
|
|
163
|
+
几十秒就能把一整轮 32 张牌看完(配合 `PET_DEBUG=1` 看 `roam-bag` 那行日志里的构成)。
|
|
164
|
+
|
|
165
|
+
### 细节
|
|
166
|
+
|
|
167
|
+
- 活动范围夹在当前显示器的 `workArea` 里,走到边缘自动掉头;多屏时跟着窗口所在的那块屏走
|
|
168
|
+
- 用户一按住角色,巡游立刻让位
|
|
169
|
+
- **角色留在你放它的位置**,巡游也在那个高度上水平走(游泳的水面画在窗口内部,窗口在哪水就在哪)。
|
|
170
|
+
想要 Shimeji 那种「松手掉回屏幕底部」的效果,勾上菜单里的「拖完松手后落回屏幕底部(重力)」。
|
|
171
|
+
勾上之后:巡游统一在屏幕底部那条地面线上走,松手会落回底部
|
|
172
|
+
- 该睡觉的时候会先停下脚步再睡,不会边睡边走
|
|
173
|
+
- 说话 / 跳舞 / 打瞌睡时不会插队开始活动,等回到站立再走;觉醒也一样
|
|
174
|
+
- 菜单关掉「自动活动」只是停掉自主调度,手动触发(菜单 / HTTP)仍然有效
|
|
175
|
+
|
|
176
|
+
全局快捷键:
|
|
177
|
+
|
|
178
|
+
- `Ctrl+Shift+P` 切换鼠标穿透模式
|
|
179
|
+
- `Ctrl+Shift+D` 显示 / 隐藏
|
|
180
|
+
- `Ctrl+Shift+A` 随机放一个觉醒技能
|
|
181
|
+
- `Esc` 恢复默认大小
|
|
182
|
+
|
|
183
|
+
## 消息接口(让角色替你说话)
|
|
184
|
+
|
|
185
|
+
主进程内置本地 HTTP 服务,默认 `127.0.0.1:8520`:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# 让角色说话(气泡 + 说话动画)
|
|
189
|
+
curl -X POST http://127.0.0.1:8520/say \
|
|
190
|
+
-H "Content-Type: application/json" \
|
|
191
|
+
-d "{\"text\":\"构建成功啦,好厉害~\"}"
|
|
192
|
+
|
|
193
|
+
# 触发动作:greet | dance | sleep | walk | run | swim | stop
|
|
194
|
+
curl -X POST http://127.0.0.1:8520/action -d "{\"name\":\"dance\"}"
|
|
195
|
+
|
|
196
|
+
# 放个觉醒技能(id: seraph | nocturne | thunder | astral | random)
|
|
197
|
+
curl -X POST http://127.0.0.1:8520/awaken -d "{\"id\":\"seraph\"}"
|
|
198
|
+
curl -X POST http://127.0.0.1:8520/awaken # 不带 id = 随机
|
|
199
|
+
curl -X POST http://127.0.0.1:8520/awaken/stop # 演出中途叫停
|
|
200
|
+
curl http://127.0.0.1:8520/outfits # 列出可用服装 / 觉醒 + 演出窗口规模
|
|
201
|
+
|
|
202
|
+
# 让它走 / 跑 / 游一段
|
|
203
|
+
curl -X POST http://127.0.0.1:8520/roam -d "{\"mode\":\"run\"}"
|
|
204
|
+
|
|
205
|
+
# 停下当前动作 / 开关自动活动
|
|
206
|
+
curl -X POST http://127.0.0.1:8520/halt
|
|
207
|
+
curl -X POST http://127.0.0.1:8520/auto -d "{\"enabled\":false}"
|
|
208
|
+
|
|
209
|
+
curl -X POST http://127.0.0.1:8520/show
|
|
210
|
+
curl -X POST http://127.0.0.1:8520/hide
|
|
211
|
+
curl http://127.0.0.1:8520/health
|
|
212
|
+
# -> {"ok":true,"app":"desktop-pet","visible":true,"hwnd":"1314584"} (hwnd 给自动化测试用)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
托盘菜单里点「消息接口」会把 curl 示例复制到剪贴板。
|
|
216
|
+
|
|
217
|
+
配合 pi agent 使用:把 `examples/pi-pet-bridge.ts` 复制到 `~/.pi/agent/extensions/`,
|
|
218
|
+
再用上 `sweet-tone` 语气扩展,agent 的回复就会以甜妹语气从角色气泡里说出来。
|
|
219
|
+
|
|
220
|
+
## 素材
|
|
221
|
+
|
|
222
|
+
角色是多帧精灵图。运行时按「角色身高」自适应缩放,命中测试按基准帧的 alpha 算。
|
|
223
|
+
|
|
224
|
+
| 文件 | 用途 |
|
|
225
|
+
|---|---|
|
|
226
|
+
| `pet-idle.png` | 站立基准帧(也是命中测试、以及各动作尺寸归一化的基准) |
|
|
227
|
+
| `pet-idle-blink.png` | 眨眼帧(与 idle 同尺寸同构图,直接换背景图就是眨眼) |
|
|
228
|
+
| `pet-walk.png` | 走路:4 帧 × 1 行,竖构图 |
|
|
229
|
+
| `pet-run.png` | 跑步:4 帧 × 1 行,竖构图 |
|
|
230
|
+
| `pet-swim.png` | 游泳:6 帧 × 1 行,**横构图**(身体水平俯卧,骑在水线上) |
|
|
231
|
+
| `sprites.json` | 精灵图清单:帧数 / 帧尺寸 / 角色参考尺寸,主进程启动时读它 |
|
|
232
|
+
| `pet-base.png` / `pet-blink.png` | 原始立绘,作为生图参考与 `pet-idle*.png` 的来源 |
|
|
233
|
+
| `source/` | 生图原始产物与绿幕图,不参与运行 |
|
|
234
|
+
| `tray.png` | 可选托盘图标(缺省时自动裁角色头部) |
|
|
235
|
+
|
|
236
|
+
`sprites.json` 是关键:帧数和「角色在这张图里有多大」都从它读,所以**加动作 / 改帧数只需要重跑脚本,不用改代码**。
|
|
237
|
+
(清单缺失时才退回 `main.js` 里的兜底常量。)
|
|
238
|
+
|
|
239
|
+
缺图不会崩:`pet-walk.png` 之类丢了就退回用 idle 帧走位;连 `pet-idle.png` 也没有就直接用 `pet-base.png`。
|
|
240
|
+
|
|
241
|
+
### 换角色 / 加动作
|
|
242
|
+
|
|
243
|
+
动作素材是生图出来的横向序列图,但**直接切不能用**:各帧身高站位有细微差异、底色也不一定干净,
|
|
244
|
+
直接切会抖、会左右滑步、会挂一圈绿边。`scripts/spritesheet.py` 专门治这三件事。
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
# 1) 生图:以 pet-base.png 为角色参考,一次出一整行 N 帧
|
|
248
|
+
# 提示词里必须写清:「N frames in a single horizontal row」+「flat pure green #00FF00
|
|
249
|
+
# background」+「no shadow / no text」。游泳这类动作还要额外写明身体是水平的,
|
|
250
|
+
# 否则模型会画成「站在原地摆手臂」—— 它默认就爱画站姿。
|
|
251
|
+
|
|
252
|
+
# 2) 竖构图动作(站立 / 走路 / 跑步):切帧 + 抠背景 + 对齐 + 按身高归一化
|
|
253
|
+
python scripts/spritesheet.py assets/source/walk-sheet-raw.png assets/pet-walk.png --frames 4 --norm-axis h --size 620 --key walk
|
|
254
|
+
|
|
255
|
+
# 3) 横构图动作(游泳):身体横躺,改按身长归一化,并且关掉轮廓对齐
|
|
256
|
+
python scripts/spritesheet.py assets/source/swim-prone.png assets/pet-swim.png --frames 6 --norm-axis w --align center --size 420 --key swim
|
|
257
|
+
|
|
258
|
+
# 4) idle / blink 也要用同一套参数归一化,否则从站立切到走路会跳位置、变大小
|
|
259
|
+
python scripts/spritesheet.py assets/pet-base.png assets/pet-idle.png --frames 1 --norm-axis h --size 620 --key idle
|
|
260
|
+
python scripts/spritesheet.py assets/pet-blink.png assets/pet-idle-blink.png --frames 1 --norm-axis h --size 620 --key idle-blink
|
|
261
|
+
```
|
|
262
|
+
(`--key` 可以省略,默认从输出文件名推导。)
|
|
263
|
+
|
|
264
|
+
常用参数:
|
|
265
|
+
|
|
266
|
+
- `--size 620` 归一化后角色的特征尺寸,渲染层按它换算屏幕尺寸。改这个值 ≈ 改角色默认大小
|
|
267
|
+
- `--norm-axis h|w` 按哪个轴归一化。**`h` = 身高**(站立 / 走路 / 跑步这类竖着的动作),
|
|
268
|
+
**`w` = 身长**(游泳这类横躺的动作)。横躺的角色按身高归一化会被放大好几倍,尺寸和立绘对不上
|
|
269
|
+
- `--align iou|center` 帧间水平对齐方式。`iou`(默认)用上半身轮廓做互相关,适合竖构图;
|
|
270
|
+
横躺的动作用 `center` —— 手臂前伸本身就是动画内容,按轮廓重合去对齐会把「前伸」这个动作抵消掉
|
|
271
|
+
- `--key` / `--manifest` 写进清单的键名与清单路径(默认 `<输出目录>/sprites.json`)
|
|
272
|
+
- `--bg RRGGBB` 手动指定底色(默认自动从画布四边取中位数)
|
|
273
|
+
- `--debug --dump-frames` 打印每帧包围盒与对齐偏移,并额外导出单帧 + 竖排预览图,肉眼查抖不抖最快
|
|
274
|
+
- 源图自带透明通道时直接用它、不再猜底色(生图有时直接给透明底,比后处理抠图干净)
|
|
275
|
+
|
|
276
|
+
脚本内部做了四件事(坑都踩过了,注释里写了原因):
|
|
277
|
+
|
|
278
|
+
1. **抠背景**:从画布边框做连通域判定,只删和边缘相连的背景。
|
|
279
|
+
这样白底图里角色身上的白袜子/白鞋不会被一起删掉。
|
|
280
|
+
绿幕另外按「绿度」全局判一次 —— 双马尾和身体之间那种被压暗的绿,颜色距离早就超阈值了,
|
|
281
|
+
而且常被头发围成闭合区域、根本连不到边框,只靠连通域判定会留一块绿斑。
|
|
282
|
+
2. **统一缩放**:全部帧共用一个缩放系数(取身高中位数),消掉忽大忽小;
|
|
283
|
+
但纵向保留各自的站位,所以走路该有的起伏不会被抹平。
|
|
284
|
+
3. **水平对齐**:用上半身的 alpha 掩膜做互相关(IoU)找最优偏移。
|
|
285
|
+
只拿上半身参与,是因为腿脚每帧摆幅最大,放进来会把对齐带偏,反而左右滑步。
|
|
286
|
+
4. **预乘 alpha 再缩放**:直接对 RGBA 做 LANCZOS,会把「完全透明像素的 RGB」也蹭进边缘 ——
|
|
287
|
+
抠完背景的透明区往往还留着绿/白,缩完就挂一圈彩边。
|
|
288
|
+
5. **写清单**:把帧数、帧尺寸、角色参考尺寸(`refPx`)和归一化轴落到 `sprites.json`,
|
|
289
|
+
渲染层照它算显示尺寸。这样换素材、加动作、改帧数都不用动代码。
|
|
290
|
+
|
|
291
|
+
### 素材规格
|
|
292
|
+
|
|
293
|
+
- 竖构图(站立/走/跑)归一化后角色身高 620px;横构图(游泳)按身长归一化到 420px
|
|
294
|
+
- 两者最终都会被换算成同一个屏幕尺寸(`main.js` 的 `BASE_FIGURE_H = 378`),所以来回切不会忽大忽小
|
|
295
|
+
- 每种动作的**步频**(一轮帧循环时长)= 帧数 × 每帧停留时长(`renderer/app.js` 的 `PER_FRAME_MS`),
|
|
296
|
+
所以帧数改了步频自动跟着变,不用改常数:
|
|
297
|
+
|
|
298
|
+
| 动作 | 帧数 | 每帧 | 一轮 |
|
|
299
|
+
|---|---|---|---|
|
|
300
|
+
| 走路 | 4 | 150ms | 600ms |
|
|
301
|
+
| 跑步 | 4 | 75ms | 300ms |
|
|
302
|
+
| 游泳 | 6 | 200ms | 1200ms |
|
|
303
|
+
|
|
304
|
+
- **游泳是横构图,定位规则和别的动作不一样**:竖动作按「脚踩屏幕底部」定位,
|
|
305
|
+
游泳按「身体骑在水线上」定位(水线切在身体自下往上 42% 处)。
|
|
306
|
+
这个偏移由渲染层按帧尺寸和 `padY` 现算(`swimSink()`),改素材不用手调数值
|
|
307
|
+
|
|
308
|
+
## 已验证 / 已知边界
|
|
309
|
+
|
|
310
|
+
### 自动化验证(不靠人手点)
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
# 1) 渲染自检:渲染一帧存 assets/_snapshot.png 后退出
|
|
314
|
+
npx electron . --snapshot
|
|
315
|
+
# --anim=<walk|run|swim> 先把角色摆到某个动作再截,用来查动作素材
|
|
316
|
+
npx electron . --snapshot --anim=swim
|
|
317
|
+
# 觉醒也能截:--anim=awaken:<id>;换 PET_SNAPSHOT_MS 就能逐帧看整场演出
|
|
318
|
+
PET_SNAPSHOT_MS=1200 npx electron . --snapshot --anim=awaken:seraph # 蓄力:黑屏 + 魔法阵
|
|
319
|
+
PET_SNAPSHOT_MS=3400 npx electron . --snapshot --anim=awaken:seraph # 亮相 + 技能名
|
|
320
|
+
PET_SNAPSHOT_MS=6500 npx electron . --snapshot --anim=awaken:seraph # 收招:窗口应该已还原
|
|
321
|
+
# PET_SNAPSHOT_MS 改截帧时刻。用两个不同值各截一张做差分,就能证明逐帧动画真的在推进:
|
|
322
|
+
# 实测 300ms 一轮的跑步动画,相隔 80ms 两张图有 20%~35% 的像素不一样
|
|
323
|
+
PET_SNAPSHOT_MS=1500 PET_SNAPSHOT_OUT=f1.png npx electron . --snapshot --anim=run
|
|
324
|
+
PET_SNAPSHOT_MS=1580 PET_SNAPSHOT_OUT=f2.png npx electron . --snapshot --anim=run
|
|
325
|
+
|
|
326
|
+
# 2) 带调试日志启动(写 pet-debug.log)
|
|
327
|
+
powershell -ExecutionPolicy Bypass -File scripts/debug-run.ps1
|
|
328
|
+
|
|
329
|
+
# 3) 真实鼠标事件端到端拖拽测试:找窗口 -> 按住角色拖动 -> 比对窗口坐标
|
|
330
|
+
# 无调试模式也能跑(从 http://127.0.0.1:8520/health 拿 HWND)
|
|
331
|
+
powershell -ExecutionPolicy Bypass -File scripts/sim-drag.ps1
|
|
332
|
+
# 期望输出:RESULT: PASS - drag works
|
|
333
|
+
|
|
334
|
+
# 4) 按住不放 / 抖动拖拽压测:每 200~600ms 采样窗口矩形,抓“拖动时窗口自己变大”这类问题
|
|
335
|
+
# -JitterPx 30 原地抖,-JitterPx 300 会跨到副屏;期望全程尺寸不变
|
|
336
|
+
powershell -ExecutionPolicy Bypass -File scripts/hold-probe.ps1 -HoldMs 4000 -StepMs 400 -JitterPx 30
|
|
337
|
+
powershell -ExecutionPolicy Bypass -File scripts/hold-probe.ps1 -NoPress -HoldMs 1000 # 只量尺寸,不按键
|
|
338
|
+
|
|
339
|
+
# 5) 拖拽落点:拖到半空松手,看它是留在原地还是被弹回屏幕底部
|
|
340
|
+
# 默认(重力关)应当停在半空;勾上重力后应当落回工作区底边
|
|
341
|
+
powershell -ExecutionPolicy Bypass -File scripts/drop-probe.ps1 -Expect stay
|
|
342
|
+
powershell -ExecutionPolicy Bypass -File scripts/drop-probe.ps1 -Expect fall # 先勾上重力
|
|
343
|
+
|
|
344
|
+
# 6) 自动巡游端到端:先关掉自主调度,再触发走/跑/游,边动边采样窗口矩形
|
|
345
|
+
# 判定三件事:真的位移了(>90px)、尺寸全程不漂(混合 DPI 回归)、最后落在地面线上
|
|
346
|
+
powershell -ExecutionPolicy Bypass -File scripts/roam-probe.ps1 -Mode run
|
|
347
|
+
# 期望输出:RESULT: PASS - roaming works
|
|
348
|
+
|
|
349
|
+
# 开启精细日志(写 pet-debug.log,含 sheet 切换、loco-start/end、对齐与命中快照)
|
|
350
|
+
PET_DEBUG=1 npx electron .
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
`GET /health` 除了探活还带诊断字段:`bounds`(窗口 DIP 尺寸)、`scale`、`displays`(每块屏的 `scaleFactor` / 边界)。
|
|
354
|
+
混合 DPI 的问题看这几个值最快。
|
|
355
|
+
|
|
356
|
+
实测输出(125% DPI 屏):
|
|
357
|
+
|
|
358
|
+
```
|
|
359
|
+
before: (796,555)-(1171,1080) size 375x525
|
|
360
|
+
press point: (984,860)
|
|
361
|
+
after : (578,555) delta=(-218,0) <- dy=0 是松手后落回地面线的结果
|
|
362
|
+
RESULT: PASS - drag works
|
|
363
|
+
|
|
364
|
+
--- roam-probe -Mode swim ---
|
|
365
|
+
start : x=1086 y=444 size=421x420
|
|
366
|
+
end : x=826 y=444 size=421x420
|
|
367
|
+
travel: 260 px (samples moved across 244 px, min=826 max=1070)
|
|
368
|
+
size : drift=1 px (slack 4)
|
|
369
|
+
RESULT: PASS - roaming works
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
调试日志里能看到完整链路(`PET_DEBUG=1` 时开启,含命中测试的 alpha / rect / canvas 快照):
|
|
373
|
+
|
|
374
|
+
```
|
|
375
|
+
renderer: pointerdown hit=true client=153,244 screen=1328.8,628
|
|
376
|
+
renderer: drag-start screen=1318.4,634.4
|
|
377
|
+
drag-start cursor=1318,634 win=1176,384
|
|
378
|
+
drag-move cursor=1220,690 target=1080,439 actual=1080,439 ← actual==target 才算真动了
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### 踩过的坑(都已在代码里修掉)
|
|
382
|
+
|
|
383
|
+
1. **拖不动的根因是极性反了**:渲染层传的是「光标是否在角色不透明像素上」,主进程却把它当成 `ignoreMouseEvents` 的值,结果光标一压到角色上就切进穿透模式,点击直接穿到桌面。现在参数语义明确为 `interactive`,主进程自己做 `!interactive` 转换。
|
|
384
|
+
2. **穿透状态下不能只靠 mousemove 唤醒**:光标停着不动就不会有事件,按下的那一刻可能还在穿透。现在主进程在穿透状态下每 220ms 轮询光标位置、交给渲染层做像素命中,唤醒不依赖鼠标移动。
|
|
385
|
+
3. **手抖式点击**:按下后漂移几像素会被判成拖拽,导致「点了没反应」。现在松手时总位移 `< 12px` 仍按单击处理。
|
|
386
|
+
4. **DPI 陷阱**:系统 125% 缩放时,`GetWindowRect` 拿到的是物理像素(378×528),Electron 的 `getBounds()` 是 DIP(303×423),写自动化脚本必须先 `SetProcessDPIAware()`,否则按下的点根本不在角色上。
|
|
387
|
+
5. **PowerShell 5.1 按 ANSI 读 .ps1**:脚本里写中文注释会直接把语法解析搞坏(`UnexpectedToken }`),所以 `scripts/*.ps1` 一律保持纯 ASCII。这条被踩过两次了,新写脚本时直接用下面的命令自检:
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
python -c "import io;s=io.open('scripts/x.ps1',encoding='utf-8').read();print([i+1 for i,l in enumerate(s.splitlines()) if any(ord(c)>127 for c in l)])"
|
|
391
|
+
```
|
|
392
|
+
6. **文件夹锁**:素材图被系统看图器打开时会锁文件,`Pillow` 保存同路径会报 `OSError: [Errno 22] Invalid argument`,所以抠图输出用了新文件名 `pet-base.png`。
|
|
393
|
+
7. **拖动时窗口自己一路变大(Windows 混合 DPI)**:125% 屏 + 100% 屏并排时,`win.setPosition()`(拖拽里每个 mousemove 都会调)会让无边框窗每次 `SetWindowPos` 都漂 ~1px,几秒钟窗口就能涨到原来的两三倍——现象就是「按住鼠标不放,控件不断放大」。这是 Electron 在 Windows 上的老问题([#20423](https://github.com/electron/electron/issues/20423) 在 >100% 缩放的屏上移动窗口尺寸每次 ±1px、[#20683](https://github.com/electron/electron/issues/20683) `resizable:false` 的窗口在缩放不同的屏上尺寸算错、[#51572](https://github.com/electron/electron/issues/51572) 无边框窗在非 100% 缩放下尺寸本身就差 1~3px)。
|
|
394
|
+
修法两层:
|
|
395
|
+
- 拖拽改用 `win.setBounds({ x, y, ...targetSize() })`,每次移动都显式带上目标尺寸,源头不再漂;
|
|
396
|
+
- `pinSize()` 挂到 `resize` / `display-metrics-changed` 上做兜底:内容尺寸偏离目标超过 4px 就拉回来(判据用 `getContentSize()` 而不是 `getBounds()`——混合 DPI 下后者会把物理像素当成 DIP 报出来,照它纠正反而打架)。
|
|
397
|
+
|
|
398
|
+
8. **巡游的窗口位移和渲染层的动画必须各管一头**:一开始想在渲染层用 `requestAnimationFrame` 每帧发 IPC 让主进程挪窗口,60fps 的 IPC 会把主线程淹掉、抖动还大。现在位移完全在主进程算(主进程持有 plan:起点/终点/时长/朝向),渲染层拿同一份 plan 只负责播帧,两边不会漂。
|
|
399
|
+
9. **启动时的问候语会把动作顶掉**:`say()` 会把状态机切到 `talk`,之后 `talk` 的定时器又恢复成 `idle`,正好盖掉刚起的动作。现在 `setAnim('idle')` 在巡游期间被显式忽略;截帧自检(`--anim=`)里也会先取消开场问候,否则截到的永远是说话的姿势。
|
|
400
|
+
10. **`ImageDraw.floodfill` 不适合这个尺寸**:在 2000×700 的掩膜上实测会出现「第一次调用一个像素都不改、第二次换个填充值就正常」的行为,而且比 `scipy.ndimage.label` 慢几个数量级。抠图已全部换成 `ndimage`。
|
|
401
|
+
11. **`binary_closing` 默认把画布外当背景**:边缘一圈会被侵蚀掉,于是画布边框上的像素永远拿不到背景连通域标签,抠图 0% 命中。必须先 `np.pad(..., constant_values=True)` 再闭运算。
|
|
402
|
+
12. **互相关对齐要先粗对齐再精调**:用整张画布宽的掩膜直接做 IoU,各帧角色分别站在自己的槽位里、重叠面积恒为 0,argmax 会一路滑到搜索边界(表现为每帧都被平移了「搜索半径」那么多)。必须先用包围盒中心算粗偏移,再在附近 ±20px 找精调量。
|
|
403
|
+
13. **贴图坐标 ≠ 平移量**:`paste` 只做平移、不改变内容自身坐标,所以「把对齐后的中心放到帧中心」和「按对齐偏移平移内容」是两个不同的公式,混用会把后面的帧直接推到画布外(现象:一张 4 帧图里只有前两帧有角色)。
|
|
404
|
+
14. **生图模型默认爱画站姿**:想让角色游泳,提示词只写 "treading water" 是没用的,它会画成「站在原地摆手臂」。必须明确写 `body is HORIZONTAL, PRONE and LYING FLAT`、`rotated 90 degrees from standing`、`DO NOT draw her standing upright`,并逐帧描述手臂前后划水的幅度。同理,正面视角游泳时手臂的前后摆动会被透视完全压缩掉,**侧视才是可读的**。
|
|
405
|
+
15. **横构图素材不能复用竖构图的归一化和对齐参数**:横躺的角色按身高归一化会被放大数倍(它只有几百像素高),按轮廓做互相关对齐又会把「手臂前伸」这个动作本身抵消掉;而且游泳不能按「脚踩地面」定位,得按「身体骑在水线」定位。这三件事现在分别由 `--norm-axis w`、`--align center` 和渲染层的 `swimSink()` 处理。
|
|
406
|
+
16. **`spritesheet.py` 的 `--manifest` 默认落在「输出目录」**:正常给角色出图时输出目录就是 `assets/`,所以一直没暴露;给觉醒序列帧出图时输出目录是 `assets/awaken/`,不写 `--manifest assets/sprites.json` 清单就会掉到 `assets/awaken/sprites.json`,而主进程只读 `assets/sprites.json` —— **素材文件明明在,游戏里却完全没换衣服,而且一句报错都没有**。现在 `PET_DEBUG=1` 启动时会检查这个幽灵文件并告警,另外写 `assets/awaken/README.md` 时把 `--manifest` 标成了必写项。
|
|
407
|
+
17. **觉醒的帧不能靠 CSS `steps()` 推**:`steps()` 一条动画只能一个速度,而一套觉醒要「蓄力慢 + 爆发快」两种节奏,而且帧得和特效事件对死(不然动作和光柱各跑各的)。现在渲染层直接写 `backgroundPositionX` 逐帧推。副作用是:换 sheet 时必须把内联的偏移清掉(`useSheet` 里那句),否则新表一上来就偏几帧。
|
|
408
|
+
|
|
409
|
+
### 其它边界
|
|
410
|
+
|
|
411
|
+
- 全局快捷键可能和别的软件冲突,冲突就在 `main.js` 里改 `globalShortcut.register`。
|
|
412
|
+
- 未做打包(无 electron-builder 配置)。要出安装包加:
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
npm i -D electron-builder
|
|
416
|
+
npx electron-builder --win portable
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## 目录
|
|
420
|
+
|
|
421
|
+
```
|
|
422
|
+
desktop-pet/
|
|
423
|
+
├─ main.js 主进程:窗口/托盘/菜单/HTTP/IPC/巡游位移引擎/觉醒放大窗口
|
|
424
|
+
├─ preload.js contextBridge 白名单 API
|
|
425
|
+
├─ renderer/
|
|
426
|
+
│ ├─ index.html
|
|
427
|
+
│ ├─ style.css 状态机动画 + 精灵图逐帧播放 + 水面/粒子 + 觉醒态
|
|
428
|
+
│ ├─ fx.js 特效引擎:粒子池 + 预渲染贴图 + 几何特效 + 暗幕 + 时间轴
|
|
429
|
+
│ ├─ awaken.js 四套觉醒的特效编排 + 序列帧节拍
|
|
430
|
+
│ └─ app.js 精灵图调度、命中测试、拖拽、台词、自主活动、觉醒状态机
|
|
431
|
+
├─ scripts/
|
|
432
|
+
│ ├─ spritesheet.py 生图序列图 -> 抠背景/切帧/对齐/归一化的精灵图
|
|
433
|
+
│ ├─ knockout.py 早期单图绿幕抠图(保留,简单场景够用)
|
|
434
|
+
│ ├─ debug-run.ps1 带 PET_DEBUG=1 启动,写 pet-debug.log
|
|
435
|
+
│ ├─ sim-drag.ps1 真实鼠标事件端到端拖拽/单击测试
|
|
436
|
+
│ ├─ hold-probe.ps1 按住 / 抖动拖拽压测,采样窗口尺寸(抓窗口自己变大)
|
|
437
|
+
│ ├─ roam-probe.ps1 自动巡游端到端:位移距离 + 尺寸不漂 + 落回地面线
|
|
438
|
+
│ └─ drop-probe.ps1 拖拽落点:留在原地 vs 落回屏幕底部
|
|
439
|
+
├─ examples/pi-pet-bridge.ts pi 扩展示例:把回复推给桌宠
|
|
440
|
+
└─ assets/ 角色精灵图 + 生图原始产物(source/)
|
|
441
|
+
├─ sprites.json 精灵图清单(帧数/帧尺寸/参考尺寸),主进程启动时读
|
|
442
|
+
├─ outfits.json 服装 / 觉醒清单 + 演出窗口规模
|
|
443
|
+
└─ awaken/<id>-<阶段>.png 觉醒序列帧(蓄力/爆发各 4 帧,可选,缺了退回 idle 帧)
|
|
444
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# 战斗服装 / 觉醒序列帧
|
|
2
|
+
|
|
3
|
+
觉醒演出是**轻量档**设计:战斗服只在觉醒那几秒里出现,不做常驻换装。
|
|
4
|
+
但它是**真正的多帧动画**,不是一张定格立绘 —— 每套服装两张横向序列图:
|
|
5
|
+
|
|
6
|
+
| 阶段 | 文件 | 清单 key | 内容 |
|
|
7
|
+
|---|---|---|---|
|
|
8
|
+
| 蓄力 | `assets/awaken/<id>-charge.png` | `awaken-<id>-charge` | 4 帧:合手聚气 → 抬手 → 上举 → 蓄力顶点 |
|
|
9
|
+
| 爆发 | `assets/awaken/<id>-burst.png` | `awaken-<id>-burst` | 4 帧:张臂 → 光翼/光柱 → 释放 → 收势 |
|
|
10
|
+
|
|
11
|
+
旧的单帧立绘 `assets/awaken/<id>.png`(key `awaken-<id>`)仍然兼容,会当作 `still` 阶段使用。
|
|
12
|
+
三个阶段缺哪个就退到下一档,全缺就用 idle 帧 —— **光效照跑,只是不换衣服**。
|
|
13
|
+
|
|
14
|
+
## 帧节拍
|
|
15
|
+
|
|
16
|
+
帧号写死在 `renderer/awaken.js` 每个 def 的 `sheet` 里,和特效时间轴对死:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
sheet: { charge: { at: 560, step: 360 }, burst: { at: 2000, step: 300 } },
|
|
20
|
+
revealAt: 2000, // = burst.at,爆发那一刻(白闪 + 抖屏)
|
|
21
|
+
closeAt: 4400, // 暗幕再进 + 窗口收回
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
蓄力 4 帧 × 360ms(560→1640),爆发 4 帧 × 300ms(2000→2900),之后停在最后一帧收招式。
|
|
25
|
+
暗幕在 `charge.at` 从全黑提到 0.6,所以**蓄力动作是真的看得见的**,不是一层黑幕盖完五秒。
|
|
26
|
+
|
|
27
|
+
## 怎么加一张
|
|
28
|
+
|
|
29
|
+
1. 生图:以 `assets/pet-base.png` 为角色参考(layout_reference),保证是同一个人。
|
|
30
|
+
提示词里必须写清楚:
|
|
31
|
+
|
|
32
|
+
- `N frames in a single horizontal row, evenly spaced, equal frame width, no overlap`
|
|
33
|
+
- `flat pure green #00FF00 background`
|
|
34
|
+
- `no shadow, no text, no watermark, no border, no extra characters`
|
|
35
|
+
- `full body visible in every frame, the character is exactly the same height in all N frames`
|
|
36
|
+
- `each frame is a DIFFERENT pose of the same continuous motion, NOT N copies of the same standing pose`
|
|
37
|
+
- 结尾再强调一次 `Exactly N frames in one row.`
|
|
38
|
+
|
|
39
|
+
模型默认爱画站姿摆拍,也常常只画 1 帧 —— 这两句必须写,而且要逐帧把姿势描述出来。
|
|
40
|
+
|
|
41
|
+
爆发那一张建议把**本套服装的蓄力图**也作为 ref 传进去(edit_target),
|
|
42
|
+
这样两阶段的服装款式才是一致的。
|
|
43
|
+
|
|
44
|
+
2. 抠背景 + 切帧 + 对齐 + 归一化。**必须**和 idle 用同一套参数(`--size 620 --norm-axis h`),
|
|
45
|
+
否则换上战斗服的那一帧会忽大忽小、位置也会跳:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python scripts/spritesheet.py assets/source/awaken-seraph-charge-raw.png \
|
|
49
|
+
assets/awaken/seraph-charge.png \
|
|
50
|
+
--frames 4 --norm-axis h --size 620 \
|
|
51
|
+
--key awaken-seraph-charge \
|
|
52
|
+
--manifest assets/sprites.json --debug
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
3. 重启即生效:右键菜单 → ✨ 觉醒技能 → 对应那一项。
|
|
56
|
+
|
|
57
|
+
## ⚠️ 必踩的坑:`--manifest` 一定要写
|
|
58
|
+
|
|
59
|
+
`spritesheet.py` 的 `--manifest` 默认是「**输出目录**/sprites.json」。
|
|
60
|
+
输出目录是 `assets/awaken/`,所以不写就会把清单写进 `assets/awaken/sprites.json`,
|
|
61
|
+
而主进程只读 `assets/sprites.json` —— 现象是**素材文件明明在,游戏里却完全没换衣服**,
|
|
62
|
+
而且是静默失败,没有任何报错。
|
|
63
|
+
|
|
64
|
+
每条命令都要带 `--manifest assets/sprites.json`。
|
|
65
|
+
(`main.js` 启动时如果发现 `assets/awaken/sprites.json` 存在,会在 `PET_DEBUG=1` 的日志里告警。)
|
|
66
|
+
|
|
67
|
+
`--debug --dump-frames` 会产生 `<name>-f0.png` 和 `<name>-preview.png`,
|
|
68
|
+
**它们是调试图不是交付物**,检查完记得删掉。
|
|
69
|
+
|
|
70
|
+
## 现有的四套
|
|
71
|
+
|
|
72
|
+
| id | 服装 | 觉醒 | 素材 |
|
|
73
|
+
|---|---|---|---|
|
|
74
|
+
| `seraph` | 圣光 · 炽天使 | 圣裁之光 | `seraph-charge.png` / `seraph-burst.png` |
|
|
75
|
+
| `nocturne` | 暗影 · 血月狂想 | 夜狱降临 | `nocturne-charge.png` / `nocturne-burst.png` |
|
|
76
|
+
| `thunder` | 雷霆 · 天罚 | 雷神裁决 | `thunder-charge.png` / `thunder-burst.png` |
|
|
77
|
+
| `astral` | 幻梦 · 星海 | 星海咏叹 | `astral-charge.png` / `astral-burst.png` |
|
|
78
|
+
|
|
79
|
+
加第五套:在 `assets/outfits.json` 的 `list` 里加一条 `{ "id", "name", "skill" }`,
|
|
80
|
+
在 `renderer/awaken.js` 里加一个同名对象(照着现有的抄,`palette` 换配色、
|
|
81
|
+
`build()` 换特效编排、`sheet` 换帧节拍),放好两张序列图即可。
|
|
82
|
+
菜单和 HTTP 接口都是从 `outfits.json` 读的,不用改 `main.js`。
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"comment": "战斗服装 / 觉醒技能清单。渲染层按 id 找 assets/awaken/<id>.png 作为觉醒立绘;素材缺失时用 idle 帧 + 纯特效兜底,不会崩。",
|
|
3
|
+
"stage": {
|
|
4
|
+
"widthRatio": 0.46,
|
|
5
|
+
"heightRatio": 0.72,
|
|
6
|
+
"maxWidth": 1280,
|
|
7
|
+
"maxHeight": 900,
|
|
8
|
+
"minWidth": 560,
|
|
9
|
+
"minHeight": 520,
|
|
10
|
+
"zoom": 1.5
|
|
11
|
+
},
|
|
12
|
+
"list": [
|
|
13
|
+
{ "id": "seraph", "name": "圣光 · 炽天使", "skill": "圣裁之光", "hint": "金白光柱 / 羽翼 / 圆形符文阵" },
|
|
14
|
+
{ "id": "nocturne", "name": "暗影 · 血月狂想", "skill": "夜狱降临", "hint": "血月升起 / 蝠影 / 黑雾" },
|
|
15
|
+
{ "id": "thunder", "name": "雷霆 · 天罚", "skill": "雷神裁决", "hint": "雷云 / 连锁闪电 / 电磁环" },
|
|
16
|
+
{ "id": "astral", "name": "幻梦 · 星海", "skill": "星海咏叹", "hint": "星尘漩涡 / 星河光带 / 水晶碎片" }
|
|
17
|
+
]
|
|
18
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|