Compare commits

...
42 Commits
Author SHA1 Message Date
Yeomai 98f44890f8 Revert "编译windows版本连接器"
This reverts commit 97e6713cfd.
2026-11-07 22:52:01 +08:00
Yeomai 97e6713cfd 编译windows版本连接器 2026-11-07 22:49:13 +08:00
tianmo e626e685fc 修正 2026-11-07 15:12:13 +08:00
tianmo b3e9481fe5 开关设置 2026-11-05 18:04:57 +08:00
tianmo 98e5c435c8 更新版本记录 2026-11-05 17:39:39 +08:00
tianmo 8977463861 跟随导出效果 2026-11-04 17:45:21 +08:00
tianmo 517e92a00b 公告栏修正 2026-11-04 11:15:47 +08:00
tianmo 87d6561779 公告栏 2026-11-04 10:55:40 +08:00
tianmo 720e3e0ceb 俄语支持 2026-11-04 10:21:57 +08:00
tianmo 753e3b1e21 网页优化 2026-10-31 15:26:31 +08:00
tianmo ab32146e8d 新增欢迎页 2026-10-31 11:48:29 +08:00
tianmo a11d57f27f 默认序列帧修改 2026-10-30 19:27:27 +08:00
tianmo 5efee6d918 资源整理 2026-10-30 19:10:29 +08:00
tianmo 7062a60c73 显示修正 2026-10-29 18:49:55 +08:00
tianmo bae179eb93 功能修复 2026-10-29 18:43:11 +08:00
tianmo fed1015849 优化碰撞识别,加入暖场帧数 2026-10-29 17:20:05 +08:00
tianmo b8652acce0 增加报表 2026-10-28 16:13:18 +08:00
tianmo ca719ba1d4 加入追踪 2026-10-28 14:32:52 +08:00
tianmo 859c78173a 加入网站追踪 2026-10-28 14:26:04 +08:00
tianmo c93ec671da 混淆
使用 javascript-obfuscator 做标识符重命名、字符串数组编码和常量拆分。
在 vite build 后增加生产环境混淆步骤,开发环境保持源码可读。
关闭 sourcemap,避免直接暴露原始源码。
当前主包约 1.7 MB,建议先拆出第三方依赖,再只混淆应用自身代码,而不是整个 dist。
2026-10-27 16:40:33 +08:00
tianmo 5f7e30063b 位置修正 2026-10-27 16:06:22 +08:00
tianmo 67bf3921cc 收缩按钮 2026-10-27 16:02:18 +08:00
tianmo 0f578d29a4 独立显示修正 2026-10-26 10:26:17 +08:00
tianmo 7069b168de 面板布局更新 2026-10-24 19:30:32 +08:00
tianmo a417c9119d 悬浮导航 2026-10-24 18:52:25 +08:00
tianmo cffbd49eff 体积显示 2026-10-23 11:55:40 +08:00
tianmo 46083c8d2a 完善浏览器配置库与未保存提示
- 新增 IndexedDB 配置保存、加载、删除及批量导入导出

- 配置列表支持选中、双击加载和悬浮操作提示

- 新增未保存提示设置,默认关闭并持久化到浏览器

- 优化配置管理界面并同步九种语言文案

- 更新开发文档与生产构建产物
2026-10-23 11:48:21 +08:00
tianmo 8bd9154414 多语言调整 2026-10-22 18:51:26 +08:00
tianmo 04de5e5a8e 多国语言支持 2026-10-22 18:37:30 +08:00
tianmo b98c0333e6 中英双语 2026-10-22 18:13:43 +08:00
tianmo ea26af26ee 文档完善 2026-10-22 17:23:38 +08:00
tianmo 5d12bf895b 修复导出问题 2026-10-22 17:19:09 +08:00
tianmo d224939efd 子发射器 2026-10-22 16:49:53 +08:00
tianmo d4ebb18777 新编译 2026-10-22 14:14:43 +08:00
tianmo 2b8fd067b3 轴点功能修复 2026-10-22 12:04:29 +08:00
tianmo 4d54c87681 贴图轴点 2026-10-22 11:46:00 +08:00
tianmo c041503004 渲染顺序功能 2026-10-22 11:16:50 +08:00
tianmo aabf4ff693 修改粒子命名规则 2026-10-14 18:35:26 +08:00
tianmo 1ecbb6fc48 飞牛部署文档 2026-10-10 15:35:51 +08:00
tianmo 61b93f590c 新增飞牛 fpk 打包部署文档与 1.0.2 安装包
- Document/飞牛fpk打包与部署.md:记录 fnpack 准备、改版本号、npm run pack:fnos 打包、安装到 fnOS、装完必查项、常见排障与上架前待办;并记下入口形态从 CGI 改为端口服务的原因(CGI 入口在目标设备上桌面图标不注册、「打开」无反应)
- release/fnos/SpineParticle-1.0.2.fpk:端口服务形态安装包,service_port=27850、依赖 nodejs_v22,桌面图标与「打开」正常,包内自带 Windows/macOS 连接器下载物
- 打包用源码暂未入库,仅保留安装包与文档

影响面:飞牛端新增应用包与文档,网页端代码未改动。
2026-10-10 15:27:56 +08:00
tianmo 7745f0de1b Merge branch 'main' of http://gitlab.yeomai.site/Yeomai/SpineParticlesWeb 2026-10-09 17:23:01 +08:00
tianmo 2ba2b9c08a 时间轴交互与预览修正
- 界面文字全局禁止鼠标框选,输入类控件保留可选中
- 拖动时间轴立即暂停自动播放,避免与播放头抢时间轴
- 暂停时补齐未完成的帧缓存,拖动时间轴直接读取已烘焙帧,无需重新触发播放
- 循环持续的烘焙不再依赖从第 0 帧开始录制,避免整段缓存为空
2026-10-09 17:22:36 +08:00
207 changed files with 27431 additions and 2469 deletions

No files matched your search

@@ -0,0 +1,135 @@
SpineParticle 网站功能介绍
========================
网站地址:
https://spineparticles.yeomai.site/
一句话介绍
----------
SpineParticle 是一款运行在浏览器中的 Spine 粒子动画编辑器。它可以把粒子效果制作、实时预览并烘焙成可继续在 Spine 中编辑的骨骼动画工程。
适合谁使用
----------
- 游戏 UI 和 2D 特效设计人员
- Spine 动画师
- 需要制作技能、爆炸、火花、烟雾、拖尾等效果的美术人员
- 希望减少逐帧图片输出、保留动画可编辑性的团队
- 想在浏览器中快速验证粒子效果的开发者
主要功能
--------
1. 浏览器内粒子编辑
无需安装网页编辑器客户端,打开网站即可开始制作。可以创建粒子系统、碰撞体、贝塞尔路径,并加载 Spine 骨骼动画作为参考或跟随目标。
2. 完整的粒子参数
支持持续发射和爆发发射,可设置发射数量、速率、延迟、发射时长、生命周期、速度、缩放、旋转以及生命周期内变化。大多数参数支持固定、随机和曲线三种方式,并提供可视化曲线编辑。
3. 多种发射区域
可以使用点、圆形环带、矩形环带、锥形和自定义贝塞尔路径作为粒子出生区域。发射方向支持固定角度、随机范围、径向、路径法线和路径切线。
4. 丰富的粒子外观
每颗粒子可以使用单张图片或序列帧,并支持多图片按权重随机。图片可单独设置缩放、贴图轴点、颜色、生命周期渐变、透明度和混合模式。
5. 可视化贴图轴点
图片资源卡可以直接打开贴图轴点编辑器。支持拖动轴点、缩放画布、精确输入坐标、超出图片边界和全屏调整。轴点结果会烘焙到 Spine 附件偏移中。
6. 粒子修改器
提供移动噪声、重力、风力、力场、漩涡、阻尼、吸附、碰撞、拖尾、路径跟随和 Spine 骨骼跟随等效果。多个修改器可以组合使用。
7. 子发射器
粒子可以在出生、死亡或碰撞时触发另一个粒子系统,形成多层粒子效果。支持发射概率、触发延迟、碰撞次数限制,以及颜色、大小、旋转和生命周期继承。
例如,可以让主粒子死亡后生成火花,火花碰撞后再生成更小的碎片。
8. Spine 插槽显示层级
粒子可以在 Spine 的指定插槽图片之上或之下预览,并会跟随动画中的 drawOrder 自动调整。该功能用于编辑器预览,便于检查粒子与角色、武器、UI 元素之间的前后遮挡关系。
9. 时间轴与 AB 循环
底部时间轴可以播放、暂停、逐帧查看完整粒子效果,并自动计算子发射器带来的总时长。
AB 循环是一个独立功能开关。开启后会显示浮动输入窗,可以输入起点 A 和终点 B,单独检查某一段动画;关闭后按完整时间轴播放。
10. Spine 动画导出
粒子最终效果可以烘焙为 Spine 的骨骼、插槽、附件和动画时间轴,而不是导出成不可编辑的逐帧图片。
支持:
- Spine 3.8、4.0、4.1、4.2、4.3 JSON
- 图片资源 ZIP
- 通过本地连接器生成可继续编辑的 .spine 工程包
- 线性或平滑贝塞尔关键帧
- 可调导出 FPS
- 骨骼对象池
- 拖尾加权 Mesh
- 子发射器和碰撞结果烘焙
11. 工程保存与预设
可以保存和加载完整工程配置,也可以使用内置预设快速开始。场景对象支持复制、显隐和撤回。
12. 多语言界面
网站支持简体中文、繁体中文、英语、日语、韩语、法语、德语、葡萄牙语和西班牙语。首次访问会根据浏览器语言自动选择界面语言,也可以随时在画布顶部切换;选择结果会自动保存。
13. 浏览器本地配置库
点击“保存配置”可以为当前工程命名并保存到浏览器。配置列表支持快速加载、删除、单文件下载和全部打包下载。加载前会提示当前场景将被覆盖,避免误操作。
推荐使用流程
------------
1. 打开网站并创建粒子系统。
2. 选择发射模式、发射区域和粒子属性。
3. 添加图片或序列帧,调整颜色、透明度和贴图轴点。
4. 按需要组合重力、风力、碰撞、拖尾、吸附等修改器。
5. 添加子发射器,编排出生、死亡或碰撞后的二次效果。
6. 加载 Spine 资源,检查粒子与插槽的前后层级。
7. 使用时间轴或 AB 循环检查动画衔接。
8. 保存工程配置。
9. 导出 Spine JSON、图片包,或通过连接器生成 .spine 工程。
网站特点
--------
- 浏览器直接使用,工作流轻量
- 参数修改实时预览
- 粒子、碰撞、路径和 Spine 动画统一在一条时间轴中查看
- 支持复杂父子粒子效果
- 导出结果保留骨骼和插槽,可在 Spine 中继续修改
- 提供多版本 Spine 支持
- 配置、预设和导出文件可独立管理
使用说明
--------
编辑器右上角有“使用说明”按钮,包含创建对象、参数模块、时间轴、Spine 资源、连接器和导出流程的快速介绍。
如需生成 .spine 工程包,需要在本机安装并启动 SpineParticle 连接器。只导出 Spine JSON 或图片 ZIP 时,可直接在网页中完成。
简短介绍文案
------------
版本一:
SpineParticle 是一款浏览器端 Spine 粒子动画编辑器,支持粒子设计、子发射器、Spine 插槽层级预览和多版本动画烘焙导出,帮助你把粒子效果转换为可继续编辑的 Spine 工程。
版本二:
在浏览器中完成粒子制作、Spine 效果预览和骨骼动画导出。SpineParticle 支持碰撞、拖尾、路径跟随、父子发射器与 AB 循环检查,适合游戏特效和 Spine 动画工作流。
版本三:
SpineParticle 让粒子特效不再只能输出为逐帧图片。你可以实时编辑粒子、组合次级效果,并将最终动画烘焙为可编辑的 Spine 骨骼、插槽与关键帧数据。
+60
View File
@@ -0,0 +1,60 @@
# 浏览器配置库说明
## 存储方式
浏览器配置库使用 IndexedDB,不占用 `localStorage`。
数据库:
```text
SpineParticle.ProjectLibrary
```
对象仓库:
```text
projects
```
每条配置保存:
- 用户输入的名称;
- 完整编辑器配置 JSON;
- 保存时间;
- 文件大小。
实现位置:
```text
src/editor/projectLibrary.ts
```
## 保存
点击“保存配置”只打开命名弹窗并写入浏览器,不会下载文件。
同名配置已存在时会询问是否覆盖。
## 加载
点击列表中的“加载”后,会先提示:
```text
加载后会覆盖当前场景,未保存的修改将丢失。
```
用户确认后才恢复配置。
## 下载
- “下载当前配置”直接导出当前场景,不需要先保存到列表。
- 单条“下载”导出对应配置的 `.json` 文件。
- “下载所有配置”将列表中全部配置打包为 ZIP。
- 下载不会加载或覆盖当前场景。
## 限制
- IndexedDB 数据属于当前网站域名和当前浏览器。
- 清理浏览器数据、隐私模式或系统空间不足可能移除配置。
- 不同浏览器和设备之间不会同步。
- 下载到磁盘的文件仍应作为正式备份。
+6 -6
View File
@@ -66,9 +66,9 @@
例如: 例如:
```text ```text
diwen_1.png yanhua_1.png
diwen_2.png yanhua_2.png
diwen_10.png yanhua_10.png
``` ```
会按照 `1 → 2 → 10` 排列。 会按照 `1 → 2 → 10` 排列。
@@ -76,13 +76,13 @@ diwen_10.png
界面明确显示实际播放顺序: 界面明确显示实际播放顺序:
```text ```text
已选择 13 帧,按 diwen_000.png-diwen_012.png 顺序播放 已选择 15 帧,按 yanhua_01.png-yanhua_15.png 顺序播放
``` ```
阶段测试资源位于: 阶段测试资源位于:
```text ```text
public/序列帧测试/sg/ public/images/frame-sequence/yanhua/
``` ```
### 2.3 播放模式 ### 2.3 播放模式
@@ -106,7 +106,7 @@ public/序列帧测试/sg/
sequenceFps sequenceFps
``` ```
有效范围为 1~120 FPS,默认 30 FPS。序列帧索引根据粒子实际出生时间计算,不依赖浏览器刷新次数。 有效范围为 1~120 FPS,默认 15 FPS。序列帧索引根据粒子实际出生时间计算,不依赖浏览器刷新次数。
### 2.5 单粒子单骨骼原则 ### 2.5 单粒子单骨骼原则
+482
View File
@@ -0,0 +1,482 @@
# 网页粒子系统 · 阶段十二开发日志
> 阶段定位:**子发射器、Spine 预览层级、贴图轴点、独立 AB 循环与多版本导出完善**
> 完成日期:2026-10-22
> 工程目录:`/Users/tianmokeji/Desktop/SpineParticle`
> 技术栈:Vue 3 + TypeScript + Pinia + PixiJS 7 + Spine Runtime + Vite
---
## 一、阶段结论
阶段十二在阶段十一的粒子编辑与 Spine 导出体系上,补齐了复杂效果编排和实际项目使用中需要的能力。
本阶段重点不再是单一粒子的基础参数,而是粒子系统之间的关系、粒子与 Spine 场景的关系,以及烘焙导出后的可编辑性。完成后的编辑器可以制作父粒子触发的多层子粒子效果,在 Spine 插槽上下预览粒子,对贴图轴点进行可视化调整,并使用独立的 A/B 区间检查循环衔接。
阶段十二主要完成:
1. 新增出生、死亡、碰撞三类子发射器事件;
2. 新增子发射器目标、发射概率、属性继承和碰撞触发上限;
3. 建立父子发射器关系、循环依赖保护和事件链深度限制;
4. 子发射器进入场景树嵌套显示,不再独立自动发射;
5. 时间轴增加子发射器活动区间、启动标记和完整时长计算;
6. AB 循环改为独立开关和上浮输入窗,不再依赖持续发射循环;
7. 新增粒子在 Spine 插槽图片之上、之下的预览层级;
8. 渲染层级支持运行时 drawOrder 变化,并保持世界坐标稳定;
9. 图片资源卡新增可视化贴图轴点编辑器;
10. 贴图轴点支持画布拖动、滚轮缩放、坐标输入和超出图片范围;
11. 贴图轴点写入 Spine 附件偏移,参与最终烘焙导出;
12. Spine JSON 导出扩展为 3.8、4.0、4.1、4.2、4.3;
13. 修复子发射器在导出前暂停补齐时没有进入逐帧事件录制的问题;
14. 修复子发射器因自身生命周期较短而被错误截断帧的问题;
15. 增加子发射器、碰撞次数、AB 循环和层级绑定的配置迁移。
阶段十二完成后,粒子编辑器已经具备从设计、组合、预览、循环检查到 Spine 烘焙导出的完整工作流。
---
## 二、子发射器系统
### 2.1 事件类型
每条规则可选择一种父粒子事件:
| 事件 | 触发时机 |
|---|---|
| 出生 | 父粒子刚生成时触发 |
| 死亡 | 父粒子生命周期结束时触发 |
| 碰撞 | 父粒子接触启用的碰撞对象时触发 |
事件规则包含:
- 目标粒子系统;
- 发射概率;
- 碰撞次数限制;
- 属性继承;
- 颜色继承方式。
### 2.2 目标系统
被任意有效事件规则引用的粒子系统会成为子发射器:
- 不再独立自动发射;
- 只由父粒子事件生成;
- 在场景对象列表中嵌套到父系统下方;
- 在时间轴中使用灰色子发射器轨道;
- 移除引用或关闭“开启子发射器”后恢复普通系统。
子发射器选择列表会排除自身和全部上游父系统,避免形成循环引用。
事件链最多继续两级,防止跨系统事件无限递归。
### 2.3 发射数量与延迟
子发射器仍使用自身的发射模式:
- 爆发模式:每次事件生成“单次数量”颗粒子;
- 持续模式:每次事件生成一颗事件粒子;
- 子发射器 `delay`:父事件之后再等待指定秒数;
- 延迟后的粒子仍会正常参与碰撞、修改器和生命周期更新。
### 2.4 碰撞次数限制
碰撞事件可设置“碰撞次数限制”:
- 默认值:1;
- 整数范围:1~4;
- 按每颗父粒子独立累计;
- 粒子出生时碰撞次数归零;
- 输入超出范围时立即钳制;
- 只在事件类型为“碰撞”时显示。
该参数用于抑制粒子反弹、悬停或反复进出碰撞体导致的同一颗父粒子重复生成子粒子。
### 2.5 属性继承
子发射器支持多选继承:
| 属性 | 当前语义 |
|---|---|
| 颜色 | 按通道相加,或使用父粒子颜色覆盖 |
| 大小 | 子粒子自身缩放 × 父粒子当前渲染缩放 |
| 旋转 | 作为子发射系统整体发射方向和初始朝向偏移 |
| 生命周期 | 覆盖子粒子生命周期,使用父粒子的完整 `maxLife` |
生命周期继承使用父粒子的完整生命周期,不使用剩余寿命,也不与子粒子自身寿命相加。
### 2.6 烘焙要求
子发射器不能脱离父事件单独预热。阶段十二修复了两处烘焙问题:
1. 暂停时为根系统补帧后,旧逻辑会过早将整段动画标记为已烘焙,导致导出跳过事件录制;
2. 导出器按系统自身生命周期截断帧,导致较晚出现的子发射器帧被裁掉。
现在只有所有系统都具备完整帧缓存才可标记烘焙完成;子发射器导出时保留完整录制时间轴。
---
## 三、时间轴与 AB 循环
### 3.1 子发射器时间轴
子发射器在时间轴上显示为灰色条:
- 起点为首次父事件时间加子发射器延迟;
- 末端包含最后一次触发后的粒子生命周期;
- 子发射器圆点标记系统启动时刻,不随子发射器 delay 拖动;
- 拖动灰色条会增加延迟,但不能移动到父事件发生之前;
- 总帧数会递归包含子发射器链;
- 子发射器延长总长后,预览视口会自动扩展;
- 关闭子发射器后,目标系统的独立时长重新计入总帧数。
### 3.2 持续循环
时间轴的持续循环按钮只控制播放是否回到起点,与 AB 区间开关相互独立。
### 3.3 AB 循环
AB 循环现在是一个独立开关:
- 关闭时不显示 A/B 输入框,播放完整时间轴;
- 开启时在时间轴上方浮出输入窗;
- A、B 由用户直接输入;
- 关闭后保留原值,下次开启可以继续使用;
- “持续循环”开启时,AB 区间重复播放;
- “持续循环”关闭时,播放到 B 后停止;
- AB 状态和数值会进入配置文件。
### 3.4 循环衔接原则
循环段仍遵守既有约束:
- A/B 必须是精确的衔接状态;
- 不通过复制首帧数值到尾帧制造表面闭环;
- 粒子位置跳变只允许发生在粒子不可见时;
- 循环开始、循环衔接和结束段需要在衔接帧强制 K 帧;
- 强制 K 帧用于保留当前状态,方便后续在 Spine 中拆分动画;
- 导出数据保留闭合边界,预览可以跳过数学边界帧避免停顿。
---
## 四、Spine 插槽渲染层级
### 4.1 预览层级
粒子系统可选择:
- 最上层;
- Spine 插槽;
- 指定插槽图片之上;
- 指定插槽图片之下。
插槽列表按 drawOrder 排列,并显示附件信息和搜索框。
### 4.2 动画 drawOrder
Spine 动画改变插槽绘制顺序时,编辑器会按当前动画帧的 drawOrder 重新解析实际挂载位置。
粒子容器使用坐标补偿抵消插槽骨骼变换,因此改变显示层级不会改变粒子在场景中的世界位置。
同一插槽可以同时承载多个粒子系统。
### 4.3 导出边界
渲染层级只影响编辑器预览,不写入 Spine 导出数据。
若需要粒子位于 Spine 插槽之间,应在 Spine 编辑器中建立对应层级,再放入烘焙后的粒子骨骼和插槽。
---
## 五、贴图轴点编辑器
图片资源卡增加“贴图轴点”开关。
开启后可以:
- 在图片预览中直接拖动轴点;
- 右键拖动画布;
- 使用滚轮缩放画布;
- 输入精确 X/Y 百分比;
- 将轴点设置在图片范围之外;
- 放大到全屏工作;
- 恢复默认居中;
- 重置当前图片轴点。
轴点十字线保持固定屏幕尺寸,并延伸为横竖无限长参考线。
轴点和开关状态跟随每张图片资源独立保存,不会因修改其他粒子参数而重置。
导出时轴点会转换为 Spine Region 附件的偏移,属于实际烘焙数据。
---
## 六、Spine 资源与多版本导出
### 6.1 Spine 场景对象
支持加载 Spine 的 skeleton、atlas 和图片资源,并提供:
- 动画列表与预览;
- 骨骼层级树;
- 骨骼搜索和选择;
- 插槽列表与附件名称;
- 动画 drawOrder;
- 粒子发射器跟随骨骼;
- 粒子预览插槽层级。
支持 Spine 3.8、4.0、4.1、4.2、4.3 资源预览。
### 6.2 JSON 导出
导出设置支持:
- Spine 3.8 / 4.0 / 4.1 / 4.2 / 4.3;
- 图片路径;
- 线性或平滑贝塞尔;
- 导出 FPS;
- 不写 fps;
- 整数帧对齐;
- 骨骼对象池;
- 排除无内容动画。
Spine 3.8 不支持 4.x 的逐通道数组贝塞尔,导出时会转换为经过容差精简的分段线性关键帧。
### 6.3 图片与工程包
导出入口包含:
- Spine JSON;
- images 图片 ZIP;
- 通过本地连接器生成 `.spine` 工程包。
连接器仅监听本机地址,使用已安装的 Spine 进行转换,并自动匹配项目 JSON 版本。
---
## 七、当前完整功能模块
### 7.1 配置管理
- 保存配置;
- 加载配置;
- 预设;
- 快捷键保存;
- 系统设置与工程配置分离;
- 配置版本校验和旧字段迁移。
- 配置保存到浏览器 IndexedDB,并显示本地配置列表;
- 列表支持加载、删除和单文件下载;
- 支持把全部浏览器配置打包为 ZIP 下载;
- 配置管理标题栏提供下载当前场景的快捷按钮;
- 加载本地配置前提示会覆盖当前场景。
### 7.2 场景对象
- 粒子系统;
- 碰撞体;
- 贝塞尔路径;
- Spine 资源;
- 对象复制;
- 显隐控制;
- 对象树和父子发射器嵌套。
### 7.3 发射模式
- 持续发射;
- 单次爆发;
- 延迟;
- 常规持续;
- 循环持续;
- 循环开始、纯循环、结束三段。
### 7.4 发射器形状
- 点;
- 圆形环带;
- 矩形环带;
- 锥形;
- 路径出生线;
- 固定、随机、径向、路径法线、路径切线角度。
### 7.5 粒子属性
- 生命周期;
- 速度;
- 缩放;
- 初始旋转;
- 生命周期内速度、缩放、旋转变化;
- 固定、随机、曲线取值;
- 多轴分离缩放。
### 7.6 外观与资源
- 单图与序列帧;
- 多图片权重随机;
- 图片缩放和贴图轴点;
- 固定色、随机色、生命周期渐变;
- 每张图片独立透明度;
- 正常、相加、相乘、滤色混合;
- 粒子系统透明度曲线。
### 7.7 修改器
- 移动噪声;
- 重力;
- 风力;
- 全局力场;
- 漩涡;
- 吸附;
- 阻尼;
- 碰撞与碰撞响应;
- 拖尾;
- 路径跟随;
- Spine 骨骼跟随;
- 子发射器。
### 7.8 时间轴
- 播放、暂停和逐帧;
- 持续循环;
- 独立 AB 循环;
- 自动总时长;
- 发射段和粒子存在段;
- Spine 动画条;
- 子发射器灰色条;
- 多动画归属;
- 循环段快捷范围已由独立 AB 输入替代。
### 7.9 导出
- Spine JSON;
- images ZIP;
- `.spine` 工程包;
- 子发射器事件结果烘焙;
- 碰撞结果烘焙;
- 路径和 Spine 跟随结果烘焙;
- 拖尾加权 Mesh;
- 关键帧精简;
- 结构校验。
### 7.10 多语言
- 使用 `vue-i18n` 管理中英文及后续语言;
- 当前支持简体中文、繁体中文、英语、日语、韩语、法语、德语、葡萄牙语和西班牙语;
- 语言下拉框位于画布顶部工具栏、背景图与使用说明之间;
- 首次访问按浏览器语言自动选择,无法匹配时回退英文;
- 自动识别结果和手动选择均写入浏览器缓存;
- 配置字段、Spine 枚举和导出数据保持语言无关,只切换显示文本;
- 页面标题、参数面板、时间轴、使用说明、配置状态和主要错误提示均随语言切换。
---
## 八、数据结构与兼容
阶段十二新增或完善的主要配置字段:
```text
ParticleEventRule.collisionLimit
TimelineState.abLoop
EditorConfigFile.timeline.playbackEndAuto
EditorConfigFile.timeline.abLoop
EmitterConfig.renderLayerMode
EmitterConfig.renderLayerSpineId
EmitterConfig.renderLayerSlotName
EmitterConfig.renderLayerSlotPosition
EmitterConfig.renderLayerFollowAttachment
ParticleImageResource.anchorX
ParticleImageResource.anchorY
```
旧配置处理:
- 缺少碰撞限制时补为 1;
- 缺少 AB 开关时默认关闭;
- 旧 B 帧若原本是完整末帧,加载时迁移到新的完整末帧;
- 无渲染层级字段时恢复“最上层”;
- 无轴点字段时恢复 0.5 / 0.5;
- 关闭“开启子发射器”后,保留规则但不再把目标识别为子发射器。
---
## 九、主要代码落点
| 文件 | 阶段十二职责 |
|---|---|
| `src/core/particleEmitter.ts` | 事件触发、碰撞计数、子粒子生成、属性继承 |
| `src/core/subEmitterIds.ts` | 子发射器引用、上游关系和循环依赖保护 |
| `src/core/subEmitterTiming.ts` | 子发射器触发区间和总时长 |
| `src/views/stage/emitterSync.ts` | 跨系统事件分发、概率、碰撞上限、自动发射切换 |
| `src/views/stage/renderLayer.ts` | 插槽渲染目标解析 |
| `src/views/Stage.vue` | 事件逐帧录制、帧缓存、子发射器烘焙 |
| `src/views/Timeline.vue` | 子发射器条、AB 循环浮窗、启动标记、视图缩放 |
| `src/views/ImageResourceCard.vue` | 图片轴点开关和资源卡入口 |
| `src/views/PivotEditor.vue` | 轴点画布、缩放、拖动和全屏编辑 |
| `src/views/particle-panel/ParticleModifiersSection.vue` | 子发射器卡片、继承和碰撞限制 UI |
| `src/views/particle-panel/ParticleAppearanceSection.vue` | 渲染层级和资源面板 |
| `src/export/spineJsonExporter.ts` | 多版本 JSON、子发射器完整帧导出、轴点偏移 |
| `src/editor/editorConfig.ts` | 配置保存、加载和旧配置迁移 |
| `src/store/particleStore.ts` | 时间轴、独立 AB、总帧数和播放范围 |
---
## 十、验证结果
阶段十二执行并通过:
```text
npm run test:particle-events
npm run test:sub-emitter-timeline
npm run test:timeline
npm run test:loop-seam
npm run test:render-layer
npm run test:spine-json
npm run test:vortex
npx vue-tsc -b
npm run build
```
浏览器实测包括:
- 出生、死亡、碰撞事件均能生成目标子粒子;
- 碰撞次数限制能减少重复触发;
- 关闭子发射器后,对象树、自动发射和时间轴恢复正常;
- Spine 插槽层级支持动画 drawOrder;
- 轴点画布支持拖动、滚轮缩放和超出边界坐标;
- AB 关闭播放完整时间轴,开启后按用户输入区间播放;
- 子发射器在导出前能生成完整帧缓存;
- 导出 JSON 包含一级和二级子发射器轨道;
- 当前测试场景导出骨骼数由错误的 8 根恢复为 140 根;
- `Burst_Collid_0`~`Burst_Collid_89` 已进入 Spine JSON;
- `dist/` 生产构建成功。
---
## 十一、当前边界
- 子发射器事件链最多两级;
- 碰撞次数限制为 1~4;
- 渲染层级仅影响编辑器预览,不参与 Spine 动画数据导出;
- 贴图轴点会烘焙进 Spine 附件偏移;
- `.spine` 工程包需要本机安装并启动连接器;
- Spine 3.8 的贝塞尔表达受旧格式限制,会转换为精简线性关键帧;
- 非常长的子发射器延迟会增加录制与导出时长;
- 预览循环和导出闭环继续遵守“精确衔接、不可见时才允许跳变”的约束。
---
## 十二、阶段十二最终状态
阶段十二完成后,SpineParticle 已具备:
- 多粒子系统父子事件编排;
- 出生、死亡、碰撞三种事件链;
- 子粒子颜色、大小、旋转和生命周期继承;
- 可控的重复碰撞触发;
- Spine 插槽上下预览层级;
- 可视化贴图轴点;
- 独立 AB 循环检查;
- 子发射器完整烘焙;
- Spine 3.8~4.3 JSON 导出;
- JSON、图片包和 `.spine` 工程包输出;
- 配置文件、预设、复制和撤回完整联动。
至此,阶段十二“复杂粒子编排与 Spine 生产链路完善”完成落盘。
+93
View File
@@ -0,0 +1,93 @@
# 预设文件维护说明
## 文件位置
预设配置放在:
```text
public/Preset/
```
预设索引:
```text
public/Preset/index.json
```
## 新增预设
1. 把新的预设配置放入 `public/Preset/`。
2. 重新启动开发服务或执行构建,索引会自动更新。
3. 部署时一起上传 `index.json`、预设文件和 `dist/`。
自动生成示例:
```json
{
"file": "8.烟花爆炸.json",
"name": {
"zh-CN": "8.烟花爆炸",
"zh-TW": "8.烟花爆炸",
"en": "8.烟花爆炸",
"ja": "8.烟花爆炸",
"ko": "8.烟花爆炸",
"fr": "8.烟花爆炸",
"de": "8.烟花爆炸",
"pt-BR": "8.烟花爆炸",
"es": "8.烟花爆炸"
}
}
```
## 规则
- `file` 是实际预设配置文件名,必须与磁盘文件完全一致。
- `name` 是下拉框显示名称,不参与文件加载。
- `name` 可以省略;省略时直接显示去掉 `.json` 后缀的文件名。
- `name` 也兼容旧版字符串格式和多语言对象格式。
- 文件名建议使用稳定、简洁的名称,可使用 ASCII。
- 当前生成器会把所有语言字段都填成文件名本身。
- 自动生成工具可以只输出文件记录:
```json
{
"file": "8.fireworks.json"
}
```
界面会显示为:
```text
8.fireworks
```
- 旧版字符串格式仍然兼容:
```json
{
"file": "8.fireworks.json",
"name": "8.烟花爆炸"
}
```
字符串格式会在所有语言中显示同一个名称。
## 回退顺序
当某种语言的名称缺失时,界面依次使用:
```text
当前语言 → English → 简体中文 → 文件名
```
验证命令:
```bash
npm run test:i18n
```
该命令会检查:
- 每个预设文件是否存在;
- 多语言名称字段格式是否有效;
- 语言包键和插值占位符是否一致。
+585
View File
@@ -0,0 +1,585 @@
# 飞牛 fnOS fpk 打包与部署
把网页粒子编辑器打包成飞牛 fnOS 应用包(`.fpk`),装到 NAS 后从桌面图标直接打开。本文是完整流程,照着做即可重复出包。
## 1. 一次性准备
**打包机**:开发用的 Mac/Windows/Linux 即可,需要 Node 环境(项目本来就有)。
**fnpack**:飞牛官方的打包工具,从 https://developer.fnnas.com/docs/cli/fnpack/ 下载对应平台版本(本项目实测 `fnpack-1.2.3-darwin-arm64`):
```text
chmod +x fnpack-1.2.3-darwin-arm64
mv fnpack-1.2.3-darwin-arm64 /usr/local/bin/fnpack # 或任意位置,后面用 FNPACK 指定
fnpack --help
```
**目标设备**:已初始化的飞牛 fnOS,管理员账号,有可用存储空间,开发机可访问。
## 2. 目录与职责
| 路径 | 作用 |
| --- | --- |
| `src/` | 前端源码,编辑器本体 |
| `dist/` | vite 构建产物,**网页端和飞牛包共用这一份** |
| `packaging/fnos/SpineParticle/` | fnOS 包工程:`manifest`、`cmd/`、`config/`、`app/ui/`、`app/server/`、图标 |
| `scripts/build-fnos-package.mjs` | 把 `dist/` 复制进包的 `app/www/` 并调用 fnpack |
| `release/fnos/` | 打好的 `.fpk`,作为发布物入库(和 `release/connector/` 下的连接器产物一样) |
包工程里 `app/www/` 由脚本生成,不要手改,改了也会被下次打包覆盖。
**注意**:`packaging/`、`scripts/build-fnos-package.mjs`、`src/editor/publicUrl.ts` 以及前端那几处配套改动**没有入库**,仓库里只有本文档和 1.0.2 安装包。第一次打包前按第 10 章重建一次即可,内容与原版一致。
## 3. 打包
(如果 `npm run pack:fnos` 不存在,先完成第 10 章的重建。)
**第一步,改版本号**:编辑 `packaging/fnos/SpineParticle/manifest`,改这两行(飞牛按版本号识别升级,重打包务必递增):
```ini
version=1.0.3
changelog=这次改了什么,面向用户写一句话
```
**第二步,执行打包**:
```text
npm run pack:fnos
```
它等价于 `npm run build && node scripts/build-fnos-package.mjs`:先做 vue-tsc 类型检查和 vite 构建,然后清空 `app/www/`、复制 `dist/`(自动排除 `.DS_Store`)、调用 `fnpack build`,最后把产物移到 `release/fnos/`。
**产物**:`release/fnos/SpineParticle-<version>.fpk`,当前约 2.6MB。
如果 `fnpack` 不在 PATH 里:
```text
FNPACK=/path/to/fnpack npm run pack:fnos
```
## 4. 安装到飞牛
方式一(图形界面):飞牛桌面 → 应用中心 → 左下角「手动安装」→ 选 `.fpk`。
方式二(命令行,需 SSH 到 NAS):
```text
appcenter-cli install-fpk SpineParticle-1.0.3.fpk
```
安装完成后 `cmd/install_callback` 会自动启动内置静态服务;如果应用中心显示未运行,手动点一次「启动」。装好刷新一次飞牛桌面(F5 或重新登录),桌面图标才会出现。
## 5. 装完必须验证的几项
| 检查项 | 预期 |
| --- | --- |
| 桌面图标 | 出现「Spine粒子编辑器」,点开是编辑器界面(`http://<飞牛地址>:27850/`) |
| 应用中心状态 | 显示运行中,可启动/停止 |
| 预设下拉 | 能加载出预设列表(读取 `/Preset/index.json`) |
| 连接器下载 | 帮助弹窗第 9 节两个下载按钮可用,能下到 exe 和 zip |
| 远程备用入口 | `<飞牛地址>/cgi/ThirdParty/SpineParticle/index.cgi/` 也能打开 |
| 连接器联动 | 本机启动连接器后,导出区显示「连接器可用 · Spine 已就绪」 |
## 6. 包形态说明(重要,避免走回头路)
当前用的是**端口服务**形态:
```ini
service_port=27850
install_dep_apps=nodejs_v22
ctl_stop=true
```
`app/server/server.mjs` 是 Node 写的静态服务,把 `app/www` 挂到 27850 端口,`cmd/main` 负责启停,桌面入口配置在 `app/ui/config`,指向该端口。
**为什么不用 CGI 入口**:`/cgi/ThirdParty/{appname}/index.cgi/` 这套(纯静态、不占端口)在开发机上打包校验能通过,页面直连也能打开,但在目标设备上桌面入口始终没注册——表现就是桌面没有图标、应用中心「打开」点了没反应。试过补 `protocol`/`port` 字段也无效,换成端口服务后立即正常。所以后续请沿用端口形态。
`app/ui/index.cgi` 保留着,仅作为远程访问备用入口,和端口走同一份 `www`。
## 7. 常见问题
| 现象 | 处理 |
| --- | --- |
| 桌面没有图标 / 「打开」没反应 | 确认应用状态是运行中;刷新(F5)或重新登录飞牛桌面 |
| 安装报依赖错误 | 确认 `manifest` 里 `install_dep_apps=nodejs_v22`,且设备能安装该运行时 |
| 端口被占用 | 同时改两处并保持一致:`manifest` 的 `service_port`、`app/ui/config` 的 `port`,然后重打包 |
| 外网点图标打不开 | 端口形态需要该端口可达;外网请用 `/cgi/ThirdParty/SpineParticle/index.cgi/` |
| 页面白屏或资源 404 | 前端构建已改为相对路径(vite `base: './'`),若做过路径相关改动,先跑 `npm run build` 再用新 `dist` 打包 |
| 服务起不来 | 看设备上 `/var/apps/SpineParticle/var/app.log` |
| 安装异常 | 看设备上 `/var/log/trim_app_center/error.log` |
## 8. 上架应用中心前待办
- 替换图标:`packaging/fnos/SpineParticle/` 下 `ICON.PNG`(64×64)、`ICON_256.PNG`(256×256),以及 `app/ui/images/icon_64.png`、`icon_256.png`,规格 sRGB、圆角方形、单文件 ≤1MB。目前是脚本生成的占位图。
- 核对 `manifest` 里的 `maintainer`、`maintainer_url`、`distributor`、`os_min_version`(现填 Yeomai / gitlab 地址 / 1.0.0)。
- 准备真实界面截图和测试材料。
- 通过飞牛开发者先锋群 / 开发者后台提交。
## 9. 提交代码时注意
- `dist/` 在版本库里是**跟踪状态**,重新构建会带出 assets 的增删(文件名带哈希),要和源码改动一起提交,否则别人拉下来跑出的页面和打包不一致。
- `release/fnos/*.fpk` 是发布物,按现有惯例入库;`packaging/fnos/SpineParticle/app/www/` 是构建中间产物,重建骨架后如果想让它不出现在 `git status` 里,自行往 `.gitignore` 加三行:
```text
/packaging/fnos/SpineParticle/app/www/
/packaging/fnos/SpineParticle/*.fpk
```
- 包的形态、端口、入口配置一旦上架,跨版本要保持稳定,改端口等于改变用户访问方式。
## 10. 附录:重建打包骨架
仓库里没有打包源码,只有本文档和安装包。按 10.1~10.6 重建一次,`npm run pack:fnos` 就能完整跑通。每个小标题就是文件路径,代码块原样存成该文件即可;下面的内容与打进 1.0.2 安装包的那份一致,重建后打出的包与 `release/fnos/SpineParticle-1.0.2.fpk` 相同(重建时把版本号往上加,见第 3 章)。
### 10.1 建目录
```bash
mkdir -p packaging/fnos/SpineParticle/{app/ui/images,app/server,cmd,config,wizard}
```
### 10.2 fnOS 包工程
#### packaging/fnos/SpineParticle/manifest
```ini
appname=SpineParticle
version=1.0.2
display_name=Spine粒子编辑器
desc=把粒子系统编辑并导出为 Spine 工程的网页编辑器:编辑、预览、导出 Spine Json 与图片包都在浏览器内完成,导出 .spine 工程需自备本机已安装并激活的 Spine,并运行应用内可下载的桌面连接器。本应用不包含 Spine 本体,与 Esoteric Software 无隶属关系。
changelog=改为端口服务形态:安装后自动启动内置静态服务,桌面图标与「打开」可正常使用。
platform=all
source=thirdparty
maintainer=Yeomai
maintainer_url=http://gitlab.yeomai.site/Yeomai/SpineParticlesWeb
distributor=Yeomai
distributor_url=http://gitlab.yeomai.site/Yeomai/SpineParticlesWeb
os_min_version=1.0.0
service_port=27850
install_dep_apps=nodejs_v22
desktop_uidir=ui
desktop_applaunchname=SpineParticle.Application
ctl_stop=true
```
#### packaging/fnos/SpineParticle/config/privilege
```json
{
"defaults": {
"run-as": "package"
},
"username": "spineparticle",
"groupname": "spineparticle"
}
```
#### packaging/fnos/SpineParticle/config/resource
```json
{}
```
#### packaging/fnos/SpineParticle/app/ui/config
```json
{
".url": {
"SpineParticle.Application": {
"title": "Spine粒子编辑器",
"icon": "images/icon_{0}.png",
"type": "url",
"protocol": "http",
"port": "27850",
"url": "/",
"allUsers": true
}
}
}
```
#### packaging/fnos/SpineParticle/app/ui/index.cgi
仅作为远程访问的备用入口,和端口形态共用同一份 `www`。
```bash
#!/bin/bash
# 静态页面入口:把 /cgi/ThirdParty/SpineParticle/index.cgi/ 之后的路径映射到应用目录下的 www。
# 前端由 vite 以相对路径构建(base: './'),所以入口地址必须带结尾斜杠。
BASE_PATH="/var/apps/SpineParticle/target/www"
URI_NO_QUERY="${REQUEST_URI%%\?*}"
case "$URI_NO_QUERY" in
*index.cgi/*) REL_PATH="${URI_NO_QUERY#*index.cgi}" ;;
*index.cgi)
printf 'Status: 301 Moved Permanently\r\nLocation: %s/\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n' "$URI_NO_QUERY"
exit 0
;;
*) REL_PATH="/" ;;
esac
if [ -z "$REL_PATH" ] || [ "$REL_PATH" = "/" ]; then
REL_PATH="/index.html"
fi
TARGET_FILE="${BASE_PATH}${REL_PATH}"
if echo "$TARGET_FILE" | grep -q '\.\.'; then
echo "Status: 400 Bad Request"
echo "Content-Type: text/plain; charset=utf-8"
echo
echo "Bad Request"
exit 0
fi
if [ -d "$TARGET_FILE" ]; then
TARGET_FILE="${TARGET_FILE%/}/index.html"
fi
if [ ! -f "$TARGET_FILE" ]; then
echo "Status: 404 Not Found"
echo "Content-Type: text/plain; charset=utf-8"
echo
echo "Not Found"
exit 0
fi
case "$TARGET_FILE" in
*.html) CONTENT_TYPE="text/html; charset=utf-8" ;;
*.js) CONTENT_TYPE="text/javascript; charset=utf-8" ;;
*.css) CONTENT_TYPE="text/css; charset=utf-8" ;;
*.json) CONTENT_TYPE="application/json; charset=utf-8" ;;
*.png) CONTENT_TYPE="image/png" ;;
*.jpg|*.jpeg) CONTENT_TYPE="image/jpeg" ;;
*.svg) CONTENT_TYPE="image/svg+xml" ;;
*.zip) CONTENT_TYPE="application/zip" ;;
*) CONTENT_TYPE="application/octet-stream" ;;
esac
echo "Content-Type: ${CONTENT_TYPE}"
echo
cat "$TARGET_FILE"
```
#### packaging/fnos/SpineParticle/app/server/server.mjs
```javascript
// 端口服务形态的静态文件服务:把安装目录下的 www 暴露给飞牛桌面入口。
import { createReadStream } from 'node:fs'
import { stat } from 'node:fs/promises'
import { createServer } from 'node:http'
import { extname, resolve, sep } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(fileURLToPath(new URL('../www/', import.meta.url)))
const PORT = Number(process.env.SPINE_PARTICLE_PORT || process.env.TRIM_SERVICE_PORT || 27850)
const TYPES = {
html: 'text/html; charset=utf-8',
js: 'text/javascript; charset=utf-8',
css: 'text/css; charset=utf-8',
json: 'application/json; charset=utf-8',
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
svg: 'image/svg+xml',
zip: 'application/zip',
exe: 'application/octet-stream',
}
async function resolveFile(pathname) {
const target = resolve(ROOT, `.${decodeURIComponent(pathname)}`)
if (target !== ROOT && !target.startsWith(ROOT + sep)) return null
const info = await stat(target).catch(() => null)
if (!info) return null
if (info.isDirectory()) return resolveFile(`${pathname.replace(/\/?$/, '/')}index.html`)
return info.isFile() ? target : null
}
createServer(async (request, response) => {
const file = await resolveFile(new URL(request.url || '/', 'http://localhost').pathname)
if (!file) {
response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' })
response.end('Not Found')
return
}
response.writeHead(200, { 'Content-Type': TYPES[extname(file).slice(1).toLowerCase()] || 'application/octet-stream' })
createReadStream(file).pipe(response)
}).listen(PORT, '0.0.0.0', () => {
console.log(`Spine粒子编辑器已启动:http://0.0.0.0:${PORT}/ ,静态目录 ${ROOT}`)
})
```
#### packaging/fnos/SpineParticle/cmd/main
```bash
#!/bin/bash
# 端口服务:启动内置 Node 静态服务,供桌面入口通过 service_port 访问。
LOG_FILE="${TRIM_PKGVAR}/app.log"
PID_FILE="${TRIM_PKGVAR}/app.pid"
APP_DIR="${TRIM_APPDEST}/server"
PORT="${TRIM_SERVICE_PORT:-27850}"
export PATH=/var/apps/nodejs_v22/target/bin:$PATH
log_msg() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
}
check_process() {
kill -0 "$1" 2>/dev/null
}
status_app() {
if [ -f "$PID_FILE" ]; then
local pid
pid="$(head -n 1 "$PID_FILE" | tr -d '[:space:]')"
if [ -n "$pid" ] && check_process "$pid"; then
return 0
fi
rm -f "$PID_FILE"
fi
return 1
}
start_app() {
if status_app; then
return 0
fi
log_msg "Starting SpineParticle on port ${PORT}..."
cd "$APP_DIR" || exit 1
SPINE_PARTICLE_PORT="$PORT" node server.mjs >> "$LOG_FILE" 2>&1 &
echo "$!" > "$PID_FILE"
}
stop_app() {
log_msg "Stopping SpineParticle..."
if [ ! -f "$PID_FILE" ]; then
return 0
fi
local pid
pid="$(head -n 1 "$PID_FILE" | tr -d '[:space:]')"
if [ -z "$pid" ] || ! check_process "$pid"; then
rm -f "$PID_FILE"
return 0
fi
kill -TERM "$pid" 2>> "$LOG_FILE" || true
local count=0
while check_process "$pid" && [ "$count" -lt 10 ]; do
sleep 1
count=$((count + 1))
done
if check_process "$pid"; then
kill -KILL "$pid" 2>> "$LOG_FILE" || true
fi
rm -f "$PID_FILE"
}
case "$1" in
start)
start_app
;;
stop)
stop_app
;;
status)
if status_app; then
exit 0
fi
exit 3
;;
*)
echo "Unknown command: $1" > "$TRIM_TEMP_LOGFILE"
exit 1
;;
esac
```
#### packaging/fnos/SpineParticle/cmd/install_callback
```bash
#!/bin/bash
# 安装后直接拉起静态服务,省去用户手动点“启动”。
cd "$(dirname "$0")" && ./main start
exit 0
```
#### packaging/fnos/SpineParticle/cmd/uninstall_init
```bash
#!/bin/bash
# 卸载前停掉静态服务。
cd "$(dirname "$0")" && ./main stop
exit 0
```
#### packaging/fnos/SpineParticle/cmd/ 其余六个脚本
`install_init`、`upgrade_init`、`upgrade_callback`、`uninstall_callback`、`config_init`、`config_callback` 内容相同:
```bash
#!/bin/bash
exit 0
```
### 10.3 构建脚本
#### scripts/build-fnos-package.mjs
```javascript
// 把 dist/ 装进飞牛 fpk 项目并调用 fnpack 打包。
import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const projectDir = join(root, 'packaging/fnos/SpineParticle')
const wwwDir = join(projectDir, 'app/www')
const distDir = join(root, 'dist')
if (!existsSync(join(distDir, 'index.html'))) {
console.error('缺少 dist/index.html,请先执行 npm run build')
process.exit(1)
}
if (!wwwDir.endsWith('/app/www')) throw new Error(`拒绝清空的目录:${wwwDir}`)
rmSync(wwwDir, { recursive: true, force: true })
cpSync(distDir, wwwDir, { recursive: true, filter: (source) => !source.endsWith('.DS_Store') })
execFileSync(process.env.FNPACK || 'fnpack', ['build'], { cwd: projectDir, stdio: 'inherit' })
const version = /^version\s*=\s*(.+)$/m.exec(readFileSync(join(projectDir, 'manifest'), 'utf8'))?.[1].trim()
const outDir = join(root, 'release/fnos')
const target = join(outDir, `SpineParticle-${version}.fpk`)
mkdirSync(outDir, { recursive: true })
rmSync(target, { force: true })
renameSync(join(projectDir, 'SpineParticle.fpk'), target)
console.log(`已生成 ${target}`)
```
### 10.4 恢复可执行权限
```bash
chmod +x packaging/fnos/SpineParticle/cmd/* packaging/fnos/SpineParticle/app/ui/index.cgi
```
### 10.5 图标(4 个 PNG)
规格:PNG 或 JPG、sRGB、正方形、单文件 ≤1MB、主体的圆角和留白贴近系统图标。
- `packaging/fnos/SpineParticle/ICON.PNG`(64×64)
- `packaging/fnos/SpineParticle/ICON_256.PNG`(256×256)
- `packaging/fnos/SpineParticle/app/ui/images/icon_64.png`
- `packaging/fnos/SpineParticle/app/ui/images/icon_256.png`
只是跑通流程的话,随便找一张方形图用 macOS 自带的 `sips` 缩放就行(正式上架要换成设计稿):
```bash
sips -z 256 256 源图.png --out packaging/fnos/SpineParticle/ICON_256.PNG
sips -z 64 64 源图.png --out packaging/fnos/SpineParticle/ICON.PNG
cp packaging/fnos/SpineParticle/ICON_256.PNG packaging/fnos/SpineParticle/app/ui/images/icon_256.png
cp packaging/fnos/SpineParticle/ICON.PNG packaging/fnos/SpineParticle/app/ui/images/icon_64.png
```
### 10.6 前端侧配套改动
这几处让前端在「域名根目录」和「子路径」两种部署下都能取到资源;少了 `publicUrl.ts` 会直接导致 `vue-tsc` 编译失败。
#### 新增 src/editor/publicUrl.ts
```typescript
// 应用可能部署在域名根路径,也可能部署在飞牛 fpk 的 /cgi/.../index.cgi/ 子路径下。
// 用文档地址解析相对路径,两种部署方式都能取到 public/ 下的静态资源。
export function publicUrl(path: string) {
return new URL(`${import.meta.env.BASE_URL}${path.replace(/^\/+/, '')}`, document.baseURI).href
}
```
#### vite.config.ts
在 `defineConfig({` 里的 `plugins` 之后加两行:
```typescript
// 相对路径:既要能部署在域名根目录,也要能跑在飞牛 fpk 的 /cgi/ThirdParty/SpineParticle/index.cgi/ 子路径下。
base: './',
```
#### package.json
`scripts` 里加一条:
```json
"pack:fnos": "npm run build && node scripts/build-fnos-package.mjs",
```
#### src/views/particle-panel/useProjectPresets.ts
顶部 import 区加一行:
```typescript
import { publicUrl } from '../../editor/publicUrl'
```
`refreshPresets` 和 `loadSelectedPreset` 里各有一处 fetch,改成用 `publicUrl` 解析:
```typescript
const response = await fetch(`${publicUrl('Preset/index.json')}?t=${Date.now()}`, { cache: 'no-store' })
```
```typescript
const response = await fetch(publicUrl(`Preset/${encodeURIComponent(file)}`), { cache: 'no-store' })
```
#### src/views/HelpDialog.vue
`<script setup>` 顶部加 import:
```typescript
import { publicUrl } from '../editor/publicUrl'
```
「下载」那一项里的两个链接改成用 `publicUrl`(原来是 `/downloads/...` 绝对路径):
```html
<a class="dl-link" :href="publicUrl('downloads/connector/SpineParticleConnector-windows-x64.exe')" download>Windows 版<br><small>SpineParticleConnector-windows-x64.exe</small></a>
<a class="dl-link" :href="publicUrl('downloads/connector/SpineParticleConnector-macos-universal.zip')" download>macOS 版(Universal)<br><small>SpineParticleConnector-macos-universal.zip</small></a>
```
「前置」那一项改写为说明 Spine 前提:
```html
<div><dt>前置</dt><dd>编辑器本身不包含 Spine:导出 <b>.spine</b> 工程需要你电脑上已安装并<b>激活</b>的 Spine(3.8 / 4.0 / 4.1 / 4.2 / 4.3),并启动下面下载的桌面版连接器。其余功能(编辑、预览、导出 Spine Json、导出 images 包)不依赖 Spine,也不需要连接器。</dd></div>
```
在「下载」之后插入「首次放行」一项:
```html
<div><dt>首次放行</dt><dd>连接器未做商业代码签名,系统会拦截一次,属正常提示:<b>Windows</b> 双击 exe,出现“Windows 已保护你的电脑”时点“更多信息”→“仍要运行”;<b>macOS</b> 解压后把 App 拖进“应用程序”,在 Finder 里按住 Control 点图标选“打开”再确认,若仍提示“已损坏”或“无法验证开发者”,到“系统设置 → 隐私与安全性”底部点“仍要打开”,或执行 <code>xattr -dr com.apple.quarantine "Spine粒子连接器.app"</code>。</dd></div>
```
### 10.7 重建后自检
```bash
npm run pack:fnos
```
能打出 `release/fnos/SpineParticle-<version>.fpk` 就说明骨架完整。想先确认前端和服务没问题,可以不依赖飞牛设备本地跑一次:
```bash
cd packaging/fnos/SpineParticle/app/server && SPINE_PARTICLE_PORT=27850 node server.mjs
```
然后浏览 `http://127.0.0.1:27850/`,应能看到编辑器界面。
+1495
View File
File diff suppressed because it is too large. Load diff
+1542
View File
File diff suppressed because it is too large. Load diff
File renamed without changes.
+88 -10
View File
@@ -1,22 +1,100 @@
[ [
{ {
"name": "1.粒子扫过", "file": "1.粒子扫过.json",
"file": "1.粒子扫过.json" "name": {
"zh-CN": "1.粒子扫过",
"zh-TW": "1.粒子扫过",
"en": "1.粒子扫过",
"ja": "1.粒子扫过",
"ko": "1.粒子扫过",
"fr": "1.粒子扫过",
"de": "1.粒子扫过",
"pt-BR": "1.粒子扫过",
"es": "1.粒子扫过"
}
}, },
{ {
"name": "2.长条方块", "file": "2.长条方块.json",
"file": "2.长条方块.json" "name": {
"zh-CN": "2.长条方块",
"zh-TW": "2.长条方块",
"en": "2.长条方块",
"ja": "2.长条方块",
"ko": "2.长条方块",
"fr": "2.长条方块",
"de": "2.长条方块",
"pt-BR": "2.长条方块",
"es": "2.长条方块"
}
}, },
{ {
"name": "3.力场拖尾", "file": "3.力场拖尾.json",
"file": "3.力场拖尾.json" "name": {
"zh-CN": "3.力场拖尾",
"zh-TW": "3.力场拖尾",
"en": "3.力场拖尾",
"ja": "3.力场拖尾",
"ko": "3.力场拖尾",
"fr": "3.力场拖尾",
"de": "3.力场拖尾",
"pt-BR": "3.力场拖尾",
"es": "3.力场拖尾"
}
}, },
{ {
"name": "4.雷电序列帧", "file": "4.雷电序列帧.json",
"file": "4.雷电序列帧.json" "name": {
"zh-CN": "4.雷电序列帧",
"zh-TW": "4.雷电序列帧",
"en": "4.雷电序列帧",
"ja": "4.雷电序列帧",
"ko": "4.雷电序列帧",
"fr": "4.雷电序列帧",
"de": "4.雷电序列帧",
"pt-BR": "4.雷电序列帧",
"es": "4.雷电序列帧"
}
}, },
{ {
"name": "5.SpineBoy", "file": "5.贴图轴点.json",
"file": "5.SpineBoy.json" "name": {
"zh-CN": "5.贴图轴点",
"zh-TW": "5.贴图轴点",
"en": "5.贴图轴点",
"ja": "5.贴图轴点",
"ko": "5.贴图轴点",
"fr": "5.贴图轴点",
"de": "5.贴图轴点",
"pt-BR": "5.贴图轴点",
"es": "5.贴图轴点"
}
},
{
"file": "6.子发射器.json",
"name": {
"zh-CN": "6.子发射器",
"zh-TW": "6.子发射器",
"en": "6.子发射器",
"ja": "6.子发射器",
"ko": "6.子发射器",
"fr": "6.子发射器",
"de": "6.子发射器",
"pt-BR": "6.子发射器",
"es": "6.子发射器"
}
},
{
"file": "7.SpineBoy.json",
"name": {
"zh-CN": "7.SpineBoy",
"zh-TW": "7.SpineBoy",
"en": "7.SpineBoy",
"ja": "7.SpineBoy",
"ko": "7.SpineBoy",
"fr": "7.SpineBoy",
"de": "7.SpineBoy",
"pt-BR": "7.SpineBoy",
"es": "7.SpineBoy"
}
} }
] ]
+16
View File
@@ -0,0 +1,16 @@
{
"enabled": true,
"scrollDurationSeconds": 24,
"messages": {
"zh-CN": "2026-11-04更新:修正路径跟随导出效果。",
"zh-TW": "2026-11-04更新:修正路徑跟隨匯出效果。",
"en": "Updated 2026-11-04: fixed path-following in exports.",
"ja": "2026-11-04 更新:パス追従のエクスポート結果を修正しました。",
"ko": "2026-11-04 업데이트: 경로 추적 내보내기 결과를 수정했습니다.",
"ru": "Обновление от 04.11.2026: исправлен экспорт со следованием по пути.",
"fr": "Mise à jour du 04/11/2026 : correction du suivi de trajectoire lors de l’exportation.",
"de": "Update vom 04.11.2026: Der Export mit Pfadverfolgung wurde korrigiert.",
"pt-BR": "Atualização de 04/11/2026: corrigida a exportação com acompanhamento de trajetória.",
"es": "Actualización del 04/11/2026: corregida la exportación con seguimiento de trayectoria."
}
}
-1
View File
@@ -1 +0,0 @@
.help-overlay[data-v-78204b9d]{position:absolute;top:0;right:0;bottom:0;left:0;z-index:40;display:flex;align-items:center;justify-content:center;padding:72px 24px 28px;box-sizing:border-box;background:#05080fb8;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}.help-dialog[data-v-78204b9d]{width:min(820px,100%);max-height:100%;display:flex;flex-direction:column;overflow:hidden;border:1px solid #3a4864;border-radius:14px;background:#141a27;color:#b8c3d8;box-shadow:0 22px 70px #0000009e}.help-head[data-v-78204b9d]{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;padding:18px 20px 15px;border-bottom:1px solid #2b354a;background:linear-gradient(135deg,#202942,#181e2d)}.help-head h2[data-v-78204b9d]{margin:0;color:#eef2ff;font-size:19px}.help-head p[data-v-78204b9d]{margin:5px 0 0;color:#7786a3;font-size:11px}.help-close[data-v-78204b9d]{width:28px;height:28px;flex:0 0 28px;border:1px solid #43516b;border-radius:6px;background:#273249;color:#ccd7ed;cursor:pointer}.help-close[data-v-78204b9d]:hover{border-color:#7774ff;background:#3b4670;color:#fff}.help-body[data-v-78204b9d]{min-height:0;overflow:auto;padding:18px 20px 21px}.help-section[data-v-78204b9d]{padding:14px 15px;border:1px solid #2c3850;border-radius:10px;background:#181f2e}.help-section h3[data-v-78204b9d]{margin:0 0 11px;color:#9fb2ff;font-size:13px}.help-start ol[data-v-78204b9d]{margin:0;padding-left:21px;color:#aab6cc;font-size:12px;line-height:1.75}.help-start li[data-v-78204b9d]::marker{color:#7774ff;font-weight:700}.help-section b[data-v-78204b9d]{color:#dce4f7}.help-grid[data-v-78204b9d]{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(250px,.85fr);gap:12px;margin-top:12px}.help-list[data-v-78204b9d]{display:grid;gap:0;margin:0}.help-list>div[data-v-78204b9d]{display:grid;grid-template-columns:78px 1fr;gap:9px;padding:7px 0;border-bottom:1px solid #263146}.help-list>div[data-v-78204b9d]:last-child{border-bottom:0}.help-list dt[data-v-78204b9d]{color:#d3dcf0;font-size:11px;font-weight:700}.help-list dd[data-v-78204b9d]{margin:0;color:#8795ae;font-size:11px;line-height:1.5}.shortcut-list[data-v-78204b9d]{display:grid;gap:7px}.shortcut-list>div[data-v-78204b9d]{display:flex;align-items:center;min-height:25px;gap:5px}.shortcut-list kbd[data-v-78204b9d]{min-width:24px;box-sizing:border-box;padding:3px 6px;border:1px solid #465470;border-bottom-width:2px;border-radius:5px;background:#101623;color:#e0e7f6;font:10px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace;text-align:center}.shortcut-list span[data-v-78204b9d]{color:#65728b;font-size:10px}.shortcut-list em[data-v-78204b9d]{margin-left:auto;color:#9ca9c1;font-size:11px;font-style:normal}.help-note[data-v-78204b9d]{margin:11px 0 0;padding-top:9px;border-top:1px solid #2a3549;color:#697791;font-size:10px;line-height:1.55}.help-export[data-v-78204b9d]{margin-top:12px}.help-export p[data-v-78204b9d]{margin:0;color:#8997b0;font-size:11px;line-height:1.65}.help-doc-title[data-v-78204b9d]{margin:16px 2px 2px;color:#9fb2ff;font-size:13px;font-weight:700;letter-spacing:.5px}.help-module[data-v-78204b9d]{margin-top:12px}.help-module-head[data-v-78204b9d]{display:flex;align-items:center;gap:8px;width:100%;padding:0;border:0;background:transparent;color:#9fb2ff;font:inherit;font-size:13px;font-weight:700;text-align:left;cursor:pointer}.help-module-head[data-v-78204b9d]:hover{color:#fff}.help-module-head .mtitle[data-v-78204b9d]{flex:1;min-width:0}.help-module-head .caret[data-v-78204b9d]{flex:0 0 auto;color:#7b86a2;font-size:12px}.help-module.open .help-module-head[data-v-78204b9d]{color:#fff}.help-module.open .help-module-head .caret[data-v-78204b9d]{color:#9fb2ff}.help-module .mnum[data-v-78204b9d]{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:6px;background:linear-gradient(135deg,#4a53b8,#6a5fd0);color:#fff;font-size:12px;font-weight:700}.help-module.help-connector[data-v-78204b9d]{border-color:#4a4f8a;background:linear-gradient(135deg,#1d2442,#181d2f)}.help-module.help-connector .help-module-head[data-v-78204b9d]{color:#b8c2ff}.help-module .connector-intro[data-v-78204b9d]{margin:0 0 10px;color:#9aa8c4;font-size:11px;line-height:1.6}.help-module .connector-intro b[data-v-78204b9d]{color:#dce4f7}.help-module .dl-link[data-v-78204b9d]{display:inline-flex;flex-direction:column;align-items:flex-start;gap:2px;margin:0 10px 6px 0;padding:6px 12px;border:1px solid #5652b5;border-radius:6px;background:#4540a0;color:#fff;font-size:12px;font-weight:700;text-decoration:none}.help-module .dl-link[data-v-78204b9d]:hover{border-color:#8982ff;background:#554dc2}.help-module .dl-link small[data-v-78204b9d]{color:#cfe0ff;font-weight:500;font-size:9px}.help-module-body[data-v-78204b9d]{padding-top:10px}@media(max-width:760px){.help-overlay[data-v-78204b9d]{padding:66px 12px 14px}.help-grid[data-v-78204b9d]{grid-template-columns:1fr}}.topbar[data-v-4749834a]{position:absolute;top:10px;left:10px;right:10px;dispLine truncated
+1
View File
@@ -0,0 +1 @@
const _0x232089=_0x56c4;(function(_0x1758a0,_0x18c8ff){const _0x273c74=_0x56c4,_0x17cd99=_0x1758a0();while(!![]){try{const _0x1f2630=-parseInt(_0x273c74(0x1495))/0x1+-parseInt(_0x273c74(0x53dc))/0x2*(parseInt(_0x273c74(0x35a5))/0x3)+parseInt(_0x273c74(0x4bbd))/0x4*(-parseInt(_0x273c74(0x99e))/0x5)+parseInt(_0x273c74(0x22a8))/0x6+-parseInt(_0x273c74(0x2af5))/0x7+-parseInt(_0x273c74(0x279c))/0x8+parseInt(_0x273c74(0x5120))/0x9;if(_0x1f2630===_0x18c8ff)break;else _0x17cd99['push'](_0x17cd99['shift']());}catch(_0x1428c4){_0x17cd99['push'](_0x17cd99['shift']());}}}(_0x4e85,0xe1874));var ks=Object[_0x232089(0x3ff5)+_0x232089(0x3612)],Cs=(_0x4c33a7,_0x21cab4,_0x4c0c85)=>_0x21cab4 in _0x4c33a7?ks(_0x4c33a7,_0x21cab4,{'enumerable':!0x0,'configurable':!0x0,'writable':!0x0,'value':_0x4c0c85}):_0x4c33a7[_0x21cab4]=_0x4c0c85,ke=(_0x1a32be,_0x1c1a32,_0xff1002)=>Cs(_0x1a32be,typeof _0x1c1a32!=_0x232089(0x16b4)?_0x1c1a32+'':_0x1c1a32,_0xff1002);import{c as _0x20796d,d as _0x59cbd7,u as _0x343082,o as _0x3bfd97,a as _0x3f2cf0,b as _0x323b63,e as _0x55b200,w as _0x4e81f2,f as _0x565f50,n as _0x28b114,g as _0x38be18,t as _0xe258d9,h as _0x7f985c,F as _0xdc2848,r as _0x1e58d6,i as _0x4cd87c,j as _0x39f553,k as _0x176438,l as _0x15f160,m as _0x2ae87f,p as _0x2caeca,q as _0x5b1d4b,s as _0x565f91,v as _0x520e25,T as _0x5b4d33,x as _0x205475,y as _0x3418b2,z as _0xdd80ee,S as _0x5e71bd,C as _0xeb512d,G as _0xb68fcd,A as _0x9686f9,B as _0x1d7cef,D as _0x41cffe,M as _0x275490,E as _0x971725,H as _0x306d98,I as _0x4c30c3,J as _0x4a4ed8,K as _0x6fb7a3,L as _0x5674e1,N as _0x41d1cf,O as _0x429cf5,P as _0x390327,Q as _0x5815e7,R as _0x38c71c,U as _0x3e8c24,V as _0x4af473,W as _0x14c3dc,X as _0x45b326,Y as _0x275a59,Z as _0x1aa4d0,_ as _0xda46aa,$ as _0x155983,a0 as _0x2c0e73,a1 as _0x1cb81d,a2 as _0x3c6f2d,a3 as _0x39d700,a4 as _0x395fb0,a5 as _0x3f85f2,a6 as _0x5a511e,a7 as _0x2f0725,a8 as _0x265878,a9 as _0x540e7a,aa as _0x58c1dc,ab as _0x24512c,ac as _0x32d32c,ad as _0x271745,ae as _0x931123,af as _0x24f48f,ag as _0x5df307,ah as _0x51a88,ai as _0x4401e4}from'./vendor-fsmZiMPK.js';(function(){const _0x29ffba=_0x232089,_0x4da923=document[_0x29ffba(0x12e0)+_0x29ffba(0x44e8)](_0x29ffba(0x1410))[_0x29ffba(0x2485)];if(_0x4da923&&_0x4da923[_0x29ffba(0x4d79)]&&_0x4da923[_0x29ffba(0x4d79)](_0x29ffba(0xa6e)+'eload'))return;for(const _0x4cec16 of document[_0x29ffba(0x51ab)+_0x29ffba(0x320a)]('link[rel'+_0x29ffba(0xe95)+_0x29ffba(0x202d)+']'))_0xdb9d7d(_0x4cec16);new MutationObserver(_0x13c87d=>{const _0x505772=_0x29ffba;for(const _0x4a9859 of _0x13c87d)if(_0x4a9859[_0x505772(0x9ae)]===_0x505772(0x755)+'t'){for(const _0x269f82 of _0x4a9859[_0x505772(0x2c78)+'es'])_0x269f82[_0x505772(0x212b)]===_0x505772(0x405)&&_0x269f82[_0x505772(0x3f14)]===_0x505772(0xa6e)+_0x505772(0x2173)&&_0xdb9d7d(_0x269f82);}})['observe'](document,{'childList':!0x0,'subtree':!0x0});function _0xa837d(_0x22a18d){const _0x475389=_0x29ffba,_0x5c4e5f={};return _0x22a18d[_0x475389(0x2f35)+'y']&&(_0x5c4e5f[_0x475389(0x2f35)+'y']=_0x22a18d[_0x475389(0x2f35)+'y']),_0x22a18d[_0x475389(0x1097)+_0x475389(0x3ddc)]&&(_0x5c4e5f[_0x475389(0x1097)+_0x475389(0x3ddc)]=_0x22a18d[_0x475389(0x1097)+_0x475389(0x3ddc)]),_0x22a18d[_0x475389(0x119e)+'gin']==='use-cred'+'entials'?_0x5c4e5f[_0x475389(0x5c1)+_0x475389(0x1a53)]=_0x475389(0x503d):_0x22a18d['crossOri'+_0x475389(0x3515)]===_0x475389(0x2761)+'s'?_0x5c4e5f['credenti'+_0x475389(0x1a53)]='omit':_0x5c4e5f['credenti'+_0x475389(0x1a53)]=_0x475389(0x49d1)+'gin',_0x5c4e5f;}function _0xdb9d7d(_0xf18e93){const _0x53e9cf=_0x29ffba;if(_0xf18e93['ep'])return;_0xf18e93['ep']=!0x0;const _0x1e9b8e=_0xa837d(_0xf18e93);fetch(_0xf18e93[_0x53e9cf(0x2492)],_0x1e9b8e);}}());const Ki=_0x232089(0x415)+'yeomai-w'+_0x232089(0x263e)+'.png',tr=_0x232089(0x85a)+_0x232089(0x33ec),Zo='/images/'+'eagle-lo'+_0x232089(0x3c09),nr={'resizeTimeline':_0x232089(0x8ae)+_0x232089(0x3c0c)+_0x232089(0x4a20)+'der\x20Zeit'+_0x232089(0x3852)+_0x232089(0x3ef4)+'n','systems':_0x232089(0x4538),'particlesBones':_0x232089(0x35da)+_0x232089(0x2e1e)+'n)','rootBone':_0x232089(0x4ff5)+_0x232089(0x3c5c),'hudTip':_0x232089(0x3fd1)+'·\x20Klicke'+'n\x20Sie\x20mi'+_0x232089(0x4046)+_0x232089(0x235d)+_0x232089(0x31f5)+_0x232089(0x1269)+_0x232089(0x2a2b)+'zu\x20ziehe'+'n','title':_0x232089(0x4f7f)+'rtikeled'+'itor'},or={'documentTitle':'Spine-Pa'+_0x232089(0x51be)+_0x232089(0x197f),'titleLead':_0x232089(0x2ae)+_0x232089(0x50d2)+'eln','titleAccent':_0x232089(0xaac)+_0x232089(0x2cde)+_0x232089(0x3234),'backToTools':_0x232089(0x2314)+'ur\x20Werkz'+_0x232089(0x14a8)+'hl','editor':{'badge':'PARTIKEL'+_0x232089(0x3413),'title':_0x232089(0x35da)+_0x232089(0x336f),'description':_0x232089(0x2665)+_0x232089(0x4027)+_0x232089(0x1cc2)+_0x232089(0xb34)+_0x232089(0x3540)+_0x232089(0x4585)+'\x20Spine-P'+_0x232089(0x29ab)+_0x232089(0x5163)+'ei.','tagPreview':_0x232089(0x53a9)+'schau','tagSequence':_0x232089(0x3f23)+_0x232089(0x283f)+_0x232089(0x4562),'tagExport':'JSON-Exp'+_0x232089(0x4562),'cta':_0x232089(0x35daLine truncated
-2
View File
@@ -1,2 +0,0 @@
var ft=Object.defineProperty;var gt=(W,t,e)=>t in W?ft(W,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):W[t]=e;var T=(W,t,e)=>gt(W,typeof t!="symbol"?t+"":t,e);import{f as ht,R as pt,N as dt,ac as ut,Z as mt,b4 as yt,O as rt,b5 as at,U as D,B as Tt}from"./index-DcKlG1-Y.js";import{$ as Rt,a0 as Yt,a1 as Ft,a2 as Et,y as _t,a3 as Ot,a4 as kt,a5 as Xt,a6 as Bt,a7 as Nt,a8 as Dt,a9 as $t,aa as zt,ab as Wt,ad as qt,H as Gt,ae as Jt,af as Ht,ag as Zt,ah as Kt,ai as Qt,aj as jt,ak as te,al as ee,am as se,an as ne,ao as ie,ap as oe,aq as re,ar as ae,as as le,at as ce,au as he,av as pe,aw as de,ax as ue,ay as fe,az as ge,aA as me,aB as ye,aC as Te,aD as be,aE as we,aF as xe,aG as Ae,aH as Pe,aI as Ie,aJ as Ce,aK as Ve,aL as Le,aM as Se,aN as Me,aO as ve,aP as Ue,aQ as Re,aR as Ye,aS as Fe,aT as Ee,x as _e,aU as Oe,aV as ke,aW as Xe,aX as Be,aY as Ne,aZ as De,a_ as $e,a$ as ze,b0 as We,b1 as qe,b2 as Ge,b3 as Je,b6 as He,b7 as Ze,b8 as Ke,b9 as Qe,ba as je,bb as ts,bc as es,bd as ss,be as ns,bf as is,bg as os,bh as rs,bi as as,bj as ls,bk as cs,bl as hs,bm as ps,bn as ds,bo as us,bp as fs,bq as gs,br as ms,bs as ys,bt as Ts,bu as bs,w as ws,X as xs,J as As,Y as Ps,z as Is,bv as Cs,bw as Vs,bx as Ls,by as Ss,bz as Ms,bA as vs,bB as Us,bC as Rs,bD as Ys,bE as Fs,bF as Es,bG as _s,T as Os,bH as ks,bI as Xs,b as Bs,bJ as Ns,c as Ds,bK as $s,bL as zs,bM as Ws,bN as qs,bO as Gs,bP as Js,bQ as Hs,bR as Zs,bS as Ks,bT as Qs,bU as js,bV as tn,bW as en,bX as sn,bY as nn,bZ as on,b_ as rn,V as an,b$ as ln,c0 as cn,c1 as hn,c2 as pn,c3 as dn}from"./index-DcKlG1-Y.js";class It{constructor(t){T(this,"skeletonData");T(this,"animationToMixTime",{});T(this,"defaultMix",0);if(!t)throw new Error("skeletonData cannot be null.");this.skeletonData=t}setMix(t,e,n){return typeof t=="string"?this.setMix1(t,e,n):this.setMix2(t,e,n)}setMix1(t,e,n){const s=this.skeletonData.findAnimation(t);if(!s)throw new Error(`Animation not found: ${t}`);const i=this.skeletonData.findAnimation(e);if(!i)throw new Error(`Animation not found: ${e}`);this.setMix2(s,i,n)}setMix2(t,e,n){if(!t)throw new Error("from cannot be null.");if(!e)throw new Error("to cannot be null.");const s=`${t.name}.${e.name}`;this.animationToMixTime[s]=n}getMix(t,e){const n=`${t.name}.${e.name}`,s=this.animationToMixTime[n];return s===void 0?this.defaultMix:s}}class Ct{constructor(t,e="",n=new bt,s=new lt){T(this,"textureLoader");T(this,"pathPrefix");T(this,"downloader");T(this,"cache");T(this,"errors",{});T(this,"toLoad",0);T(this,"loaded",0);T(this,"texturePmaInfo",{});this.textureLoader=t,this.pathPrefix=e,this.downloader=n,this.cache=s}start(t){return this.toLoad++,this.pathPrefix+t}success(t,e,n){this.toLoad--,this.loaded++,this.cache.assets[e]=n,this.cache.assetsRefCount[e]=(this.cache.assetsRefCount[e]||0)+1,t&&t(e,n)}error(t,e,n){this.toLoad--,this.loaded++,this.errors[e]=n,t&&t(e,n)}loadAll(){return new Promise((e,n)=>{const s=()=>{if(this.isLoadingComplete()){this.hasErrors()?n(this.errors):e(this);return}requestAnimationFrame(s)};requestAnimationFrame(s)})}setRawDataURI(t,e){this.downloader.rawDataUris[this.pathPrefix+t]=e}loadBinary(t,e=()=>{},n=()=>{}){t=this.start(t),!this.reuseAssets(t,e,n)&&(this.cache.assetsLoaded[t]=new Promise((s,i)=>{this.downloader.downloadBinary(t,o=>{this.success(e,t,o),s(o)},(o,r)=>{const l=`Couldn't load binary ${t}: status ${o}, ${r}`;this.error(n,t,l),i(l)})}))}loadText(t,e=()=>{},n=()=>{}){t=this.start(t),this.downloader.downloadText(t,s=>{this.success(e,t,s)},(s,i)=>{this.error(n,t,`Couldn't load text ${t}: status ${s}, ${i}`)})}loadJson(t,e=()=>{},n=()=>{}){t=this.start(t),!this.reuseAssets(t,e,n)&&(this.cache.assetsLoaded[t]=new Promise((s,i)=>{this.downloader.downloadJson(t,o=>{this.success(e,t,o),s(o)},(o,r)=>{const l=`Couldn't load JSON ${t}: status ${o}, ${r}`;this.error(n,t,l),i(l)})}))}reuseAssets(t,e=()=>{},n=()=>{}){const s=this.cache.getAsset(t),i=s!==void 0;return i&&(this.cache.assetsLoaded[t]=s.then(o=>(o=o instanceof Image||o instanceof ImageBitmap?this.textureLoader(o):o,this.success(e,t,o),o)).catch(o=>{this.error(n,t,o)})),i}loadTexture(t,e=()=>{},n=()=>{}){if(t=this.start(t),this.reuseAssets(t,e,n))return;const s=this.texturePmaInfo[t];this.cache.assetsLoaded[t]=new Promise((i,o)=>{if(!!!(typeof window<"u"&&typeof navigator<"u"&&window.document))fetch(t,{mode:"cors"}).then(c=>{if(c.ok)return c.blob();const a=`Couldn't load image: ${t}`;this.error(n,t,`Couldn't load image: ${t}`),o(a)}).then(c=>c?createImageBitmap(c,{premultiplyAlpha:"none",colorSpaceConversion:"none"}):null).then(c=>{if(c){const a=this.createTexture(t,s,c);this.success(e,t,a),i(a)}});else{const c=new Image;c.crossOrigin="anonymous",c.onload=()=>{const a=this.createTexture(t,s,c);this.success(e,t,a),i(a)},c.onerror=()=>{const a=`Couldn't load image: ${t}`;this.error(n,t,a),o(a)},this.downloader.rawDataUris[t]&&(t=this.downloader.rawDataUris[t]),c.src=t}})}loadTextureAtlas(t,e=()=>{},n=()=>{},s){const i=t.lastIndexOf("/"),o=i>=0?t.substring(0,i+1)Line truncated
${n}`:""}`)}remove(t){t=this.pathPrefix+t;const e=this.cache.assets[t];return e.dispose&&e.dispose(),delete this.cache.assets[t],delete this.cache.assetsRefCount[t],delete this.cache.assetsLoaded[t],e}removeAll(){for(const t in this.cache.assets){const e=this.cache.assets[t];e.dispose&&e.dispose()}this.cache.assets={},this.cache.assetsLoaded={},this.cache.assetsRefCount={}}isLoadingComplete(){return this.toLoad===0}getToLoad(){return this.toLoad}getLoaded(){return this.loaded}dispose(){this.removeAll()}disposeAsset(t){const e=this.cache.assets[t];if(e instanceof ht){e.dispose();return}this.disposeAssetInternal(t)}hasErrors(){return Object.keys(this.errors).length>0}getErrors(){return this.errors}disposeAssetInternal(t){if(this.cache.assetsRefCount[t]>0&&--this.cache.assetsRefCount[t]===0)return this.remove(t)}createTextureAtlas(t,e,n,s){const i=new ht(t);i.dispose=()=>{var o;if(!(this.cache.assetsRefCount[n]<=0)){this.disposeAssetInternal(n);for(const r of i.pages)(o=r.texture)==null||o.dispose()}};for(const o of i.pages){const r=this.texturePath(e,o.name,s);this.texturePmaInfo[this.pathPrefix+r]=o.pma}return i}createTexture(t,e,n){const s=this.textureLoader(n,e),i=s.dispose.bind(s);return s.dispose=()=>{this.disposeAssetInternal(t)&&i()},s}texturePath(t,e,n){return n?n[e]:t+e}}const nt=class nt{constructor(){T(this,"assets",{});T(this,"assetsRefCount",{});T(this,"assetsLoaded",{})}static getCache(t){const e=nt.AVAILABLE_CACHES.get(t);if(e)return e;const n=new nt;return nt.AVAILABLE_CACHES.set(t,n),n}async addAsset(t,e){return this.assetsLoaded[t]=Promise.resolve(e),this.assets[t]=e,e}getAsset(t){return this.assetsLoaded[t]}};T(nt,"AVAILABLE_CACHES",new Map);let lt=nt;class bt{constructor(){T(this,"callbacks",{});T(this,"rawDataUris",{})}dataUriToString(t){if(!t.startsWith("data:"))throw new Error("Not a data URI.");let e=t.indexOf("base64,");return e!==-1?(e+=7,atob(t.substr(e))):t.substr(t.indexOf(",")+1)}base64ToUint8Array(t){var e=window.atob(t),n=e.length,s=new Uint8Array(n);for(let i=0;i<n;i++)s[i]=e.charCodeAt(i);return s}dataUriToUint8Array(t){if(!t.startsWith("data:"))throw new Error("Not a data URI.");let e=t.indexOf("base64,");if(e===-1)throw new Error("Not a binary data URI.");return e+=7,this.base64ToUint8Array(t.substr(e))}downloadText(t,e,n){if(this.start(t,e,n))return;const s=this.rawDataUris[t];if(s&&!s.includes(".")){try{this.finish(t,200,this.dataUriToString(s))}catch(r){this.finish(t,400,JSON.stringify(r))}return}const i=new XMLHttpRequest;i.overrideMimeType("text/html"),i.open("GET",s||t,!0);const o=()=>{this.finish(t,i.status,i.responseText)};i.onload=o,i.onerror=o,i.send()}downloadJson(t,e,n){this.downloadText(t,s=>{e(JSON.parse(s))},n)}downloadBinary(t,e,n){if(this.start(t,e,n))return;const s=this.rawDataUris[t];if(s&&!s.includes(".")){try{this.finish(t,200,this.dataUriToUint8Array(s))}catch(r){this.finish(t,400,JSON.stringify(r))}return}const i=new XMLHttpRequest;i.open("GET",s||t,!0),i.responseType="arraybuffer";const o=()=>{this.finish(t,i.status,i.response)};i.onload=()=>{i.status===200||i.status===0?this.finish(t,200,new Uint8Array(i.response)):o()},i.onerror=o,i.send()}start(t,e,n){let s=this.callbacks[t];try{if(s)return!0;this.callbacks[t]=s=[]}finally{s.push(e,n)}}finish(t,e,n){const s=this.callbacks[t];if(delete this.callbacks[t],e===200||e===0)for(let i=0,o=s.length;i<o;i+=2)s[i](n);else for(let i=1,o=s.length;i<o;i+=2)s[i](e,n)}}class Vt{constructor(t,e=!1){T(this,"atlas");T(this,"allowMissingRegions");this.atlas=t,this.allowMissingRegions=e}findRegions(t,e,n){const s=n.regions;for(let i=0,o=s.length;i<o;i++)s[i]=this.findRegion(t,n.getPath(e,i))}findRegion(t,e){const n=this.atlas.findRegion(e);if(!n&&!this.allowMissingRegions)throw new Error(`Region not found in atlas: ${e} (attachment: ${t})`);return n}newRegionAttachment(t,e,n,s,i){return this.findRegions(n,s,i),new pt(n,i)}newMeshAttachment(t,e,n,s,i){return this.findRegions(n,s,i),new dt(n,i)}newBoundingBoxAttachment(t,e,n){return new ut(n)}newPathAttachment(t,e,n){return new mt(n)}newPointAttachment(t,e,n){return new yt(n)}newClippingAttachment(t,e,n){return new rt(n)}}class Lt{constructor(){T(this,"minX",0);T(this,"minY",0);T(this,"maxX",0);T(this,"maxY",0);T(this,"boundingBoxes",[]);T(this,"polygons",[]);T(this,"polygonPool",new at(()=>D.newFloatArray(16)))}update(t,e){if(!t)throw new Error("skeleton cannot be null.");const n=this.boundingBoxes,s=this.polygons,i=this.polygonPool,o=t.slots,r=o.length;n.length=0,i.freeAll(s),s.length=0;for(let l=0;l<r;l++){const c=o[l];if(!c.bone.active)continue;const a=c.appliedPose.attachment;if(a instanceof ut){n.push(a);let h=i.obtain();h.length!==a.worldVerticesLength&&(h=D.newFloatArray(a.worldVerticesLength)),s.push(h),a.computeWorldVertices(t,c,0,a.worldVerticesLength,h,0,2)}}e?this.aabbCompute():(this.minX=Number.POSITIVE_INFINITY,this.minY=Number.POSITIVE_INFINITY,this.maxX=Number.NEGATIVE_INFINITY,this.maxY=Number.NEGATIVE_INFINITY)}aabbCompute(){let t=Number.POSITIVE_INFLine truncated
+1
View File
@@ -0,0 +1 @@
.language-picker[data-v-811a66dc]{display:flex;height:28px;align-items:center;gap:6px;margin-left:auto;color:#9aa8c1;font-size:11px;white-space:nowrap}.language-dropdown[data-v-811a66dc]{position:relative}.language-trigger[data-v-811a66dc]{display:flex;height:28px;min-width:88px;align-items:center;gap:7px;padding:0 8px;border:1px solid #3a4963;border-radius:6px;outline:none;background:#202a3b;color:#dbe4f5;font:inherit;font-size:11px;cursor:pointer}.language-trigger[data-v-811a66dc]:hover,.language-trigger.open[data-v-811a66dc]{border-color:#5b608c;background:#2b3a5c}.language-trigger[data-v-811a66dc]:focus-visible{border-color:#7774ff}.language-flag-icon[data-v-811a66dc],.language-option-flag-icon[data-v-811a66dc]{display:block;width:22px;height:15px;flex:0 0 auto;overflow:hidden;border-radius:2px;box-shadow:0 0 0 1px #ffffff1f}.language-short[data-v-811a66dc]{min-width:20px;color:#dbe4f5;font-size:11px;font-weight:600}.language-chevron[data-v-811a66dc]{width:10px;height:6px;margin-left:auto;fill:none;stroke:#aab;stroke-width:1.7;transition:transform .15s ease}.language-trigger.open .language-chevron[data-v-811a66dc]{transform:rotate(180deg)}.language-menu[data-v-811a66dc]{position:absolute;right:0;bottom:calc(100% + 6px);z-index:30;width:190px;max-height:min(360px,calc(100vh - 120px));overflow-y:auto;padding:5px;border:1px solid #343b50;border-radius:9px;background:#141420fa;box-shadow:0 8px 24px #0000007a}.menu-down .language-menu[data-v-811a66dc]{top:calc(100% + 6px);bottom:auto}.language-option[data-v-811a66dc]{display:grid;width:100%;min-height:38px;grid-template-columns:24px 1fr 16px;align-items:center;gap:8px;padding:0 7px;border:0;border-radius:6px;background:transparent;color:#aeb8cc;font:inherit;font-size:12px;text-align:left;white-space:nowrap;cursor:pointer}.language-option[data-v-811a66dc]:hover,.language-option[data-v-811a66dc]:focus-visible{outline:none;background:#2b3a5c;color:#fff}.language-option.selected[data-v-811a66dc]{background:#343b63;color:#fff}.language-check[data-v-811a66dc]{justify-self:end;color:#fff;font-size:18px;font-weight:500}.welcome-shell[data-v-1a06af44]{position:fixed;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;overflow:hidden;box-sizing:border-box;padding:clamp(22px,3vw,42px) clamp(22px,4vw,64px) 28px;color:#eef0f5;background:radial-gradient(circle at 17% 0%,rgba(218,45,57,.16),transparent 32%),radial-gradient(circle at 85% 8%,rgba(72,100,170,.16),transparent 30%),linear-gradient(145deg,#09090d,#0d0f15 52%,#090b10);font-family:system-ui,-apple-system,Segoe UI,sans-serif}.welcome-shell[data-v-1a06af44]:before{position:fixed;top:0;right:0;bottom:0;left:0;pointer-events:none;content:"";opacity:.22;background-image:linear-gradient(rgba(255,255,255,.025) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.025) 1px,transparent 1px);background-size:42px 42px;-webkit-mask-image:linear-gradient(to bottom,#000,transparent 70%);mask-image:linear-gradient(to bottom,#000,transparent 70%)}.welcome-header[data-v-1a06af44],.welcome-intro[data-v-1a06af44],.hub-grid[data-v-1a06af44]{position:relative}.welcome-header[data-v-1a06af44]{display:flex;min-height:42px;align-items:center;gap:18px}.welcome-header[data-v-1a06af44]{z-index:40}.brand-link[data-v-1a06af44]{display:inline-flex;border-radius:6px;transition:opacity .16s ease}.brand-link[data-v-1a06af44]:hover{opacity:.78}.brand-link[data-v-1a06af44]:focus-visible{outline:2px solid #ef5147;outline-offset:5px}.brand-logo[data-v-1a06af44]{display:block;width:clamp(128px,10vw,156px);height:auto}.welcome-intro[data-v-1a06af44]{padding:clamp(34px,6vh,64px) 0 clamp(22px,3.5vh,34px);text-align:center}.welcome-intro h1[data-v-1a06af44]{max-width:none;margin:0 auto;color:#f3f3f5;font-size:clamp(20px,2.4vw,40px);line-height:1.1;letter-spacing:-.025em;white-space:nowrap}.welcome-intro h1 span[data-v-1a06af44]{color:#ff4e47}.welcome-intro p[data-v-1a06af44]{max-width:390px;margin:0 0 3px;color:#8c93a3;font-size:13px;line-height:1.75}.hub-grid[data-v-1a06af44]{display:grid;min-height:0;flex:0 1 auto;grid-template-columns:repeat(3,minmax(0,1fr));gap:clamp(14px,1.5vw,22px);width:min(100%,1540px);margin:0 auto}.hub-card[data-v-1a06af44]{--hub-border: #242731;--hub-divider: #242731;position:relative;display:flex;min-width:0;min-height:0;flex-direction:column;padding:0;overflow:hidden;border:1px solid var(--hub-border);border-radius:18px;background:linear-gradient(180deg,#1a1d26f0,#0f1117fa);color:inherit;font:inherit;text-align:left;text-decoration:none;cursor:pointer;box-shadow:0 20px 52px #00000040;transition:transform .18s ease,border-color .18s ease,box-shadow .18s ease}.hub-card[data-v-1a06af44]:hover{transform:translateY(-3px);border-color:var(--hub-border);box-shadow:0 25px 64px #00000061}.hub-card[data-v-1a06af44]:focus-visible{outline:2px solid #ff5b50;outline-offset:2px}.editor-card[data-v-1a06af44]:hover{--hub-border: #706bea}.viewer-card[data-v-1a06af44]:hover,.thumbnail-card[data-v-1a06af44]:hover{--hub-bordeLine truncated
-1239
View File
File diff suppressed because it is too large. Load diff
-71
View File
@@ -1,71 +0,0 @@
var Ae=Object.defineProperty;var Be=(m,o,e)=>o in m?Ae(m,o,{enumerable:!0,configurable:!0,writable:!0,value:e}):m[o]=e;var a=(m,o,e)=>Be(m,typeof o!="symbol"?o+"":o,e);import{T as Fe,a as he,b as F,S as ue,M as pe,c as oe,W as ne,B as H,d as z,L as Te,E as O,e as ce,p as q,f as Ee,g as fe,A as Q,h as me,i as De,j as V,s as re,G as we,k as $,l as Y,m as Le,P as Ie,n as Re,o as Ne,C as X,q as be,r as _e,t as Ue,u as Ye,v as Xe,w as ee,x as j,y as ke,z as Ve,D as J,F as ge,V as Ge,H as ae,I as We,J as $e,U as xe,K as E,R as ve,N as ye,O as Pe,Q as Oe,X as He,Y as ze,Z as qe,_ as Ke}from"./index-DcKlG1-Y.js";import{$ as bt,a0 as kt,a1 as vt,a2 as yt,a3 as Pt,a4 as St,a5 as Mt,a6 as At,a7 as Bt,a8 as Ft,a9 as Et,aa as Dt,ab as Lt,ac as It,ad as Rt,ae as Nt,af as _t,ag as Ut,ah as Yt,ai as Xt,aj as Vt,ak as Gt,al as Wt,am as $t,an as Ot,ao as Ht,ap as zt,aq as qt,ar as Kt,as as Zt,at as Jt,au as Qt,av as jt,aw as es,ax as ts,ay as ss,az as os,aA as ns,aB as as,aC as rs,aD as is,aE as ls,aF as hs,aG as cs,aH as ds,aI as us,aJ as ps,aK as fs,aL as ms,aM as gs,aN as xs,aO as Cs,aP as Ts,aQ as ws,aR as bs,aS as ks,aT as vs,aU as ys,aV as Ps,aW as Ss,aX as Ms,aY as As,aZ as Bs,a_ as Fs,a$ as Es,b0 as Ds,b1 as Ls,b2 as Is,b3 as Rs,b4 as Ns,b5 as _s,b6 as Us,b7 as Ys,b8 as Xs,b9 as Vs,ba as Gs,bb as Ws,bc as $s,bd as Os,be as Hs,bf as zs,bg as qs,bh as Ks,bi as Zs,bj as Js,bk as Qs,bl as js,bm as eo,bn as to,bo as so,bp as oo,bq as no,br as ao,bs as ro,bt as io,bu as lo,bv as ho,bw as co,bx as uo,by as po,bz as fo,bA as mo,bB as go,bC as xo,bD as Co,bE as To,bF as wo,bG as bo,bH as ko,bI as vo,bJ as yo,bK as Po,bL as So,bM as Mo,bN as Ao,bO as Bo,bP as Fo,bQ as Eo,bR as Do,bS as Lo,bT as Io,bU as Ro,bV as No,bW as _o,bX as Uo,bY as Yo,bZ as Xo,b_ as Vo,b$ as Go,c0 as Wo,c1 as $o,c2 as Oo,c3 as Ho}from"./index-DcKlG1-Y.js";import{SkeletonClipping as de,SkeletonPhysicsMovement as Ze,AnimationStateData as Je,AtlasAttachmentLoader as Qe,SkeletonBounds as je}from"./index-D40Mzcwz.js";import{AssetCache as qo,AssetManagerBase as Ko,Downloader as Zo,SkeletonRendererCore as Jo,Triangulator as Qo}from"./index-D40Mzcwz.js";if(typeof window<"u"&&window.PIXI){const m=window.require;window.require=o=>{if(m)return m(o);if(o.startsWith("@pixi/"))return window.PIXI}}const _=class _ extends Fe{constructor(e){super(e.resource.source);a(this,"texture");this.texture=he.from(e)}static from(e){return _.textureMap.has(e)?_.textureMap.get(e):new _(e)}setFilters(e,t){this.texture.baseTexture.scaleMode=_.toPixiTextureFilter(e),this.texture.baseTexture.mipmap=_.toPixiMipMap(e)}setWraps(e,t){this.texture.baseTexture.wrapMode=_.toPixiTextureWrap(e)}dispose(){this.texture.destroy()}static toPixiTextureFilter(e){switch(e){case F.Nearest:case F.MipMapNearestLinear:case F.MipMapNearestNearest:return ue.NEAREST;case F.Linear:case F.MipMapLinearLinear:case F.MipMapLinearNearest:return ue.LINEAR;default:throw new Error(`Unknown texture filter: ${String(e)}`)}}static toPixiMipMap(e){switch(e){case F.Nearest:case F.Linear:return pe.OFF;case F.MipMapNearestLinear:case F.MipMapNearestNearest:case F.MipMapLinearLinear:case F.MipMapLinearNearest:return pe.ON;default:throw new Error(`Unknown texture filter: ${String(e)}`)}}static toPixiTextureWrap(e){switch(e){case oe.ClampToEdge:return ne.CLAMP;case oe.MirroredRepeat:return ne.MIRRORED_REPEAT;case oe.Repeat:return ne.REPEAT;default:throw new Error(`Unknown texture wrap: ${String(e)}`)}}static toPixiBlending(e){switch(e){case H.Normal:return z.NORMAL;case H.Additive:return z.ADD;case H.Multiply:return z.MULTIPLY;case H.Screen:return z.SCREEN;default:throw new Error(`Unknown blendMode: ${String(e)}`)}}};a(_,"textureMap",new Map);let G=_;const K="spineTextureAtlasLoader",et={extension:O.Asset,resolver:{test:m=>V(m,".atlas"),parse:m=>{var e,t;const o=m.split(".");return{resolution:parseFloat(((t=(e=re.RETINA_PREFIX)==null?void 0:e.exec(m))==null?void 0:t[1])??"1"),format:o[o.length-2],src:m}}},loader:{name:K,extension:{type:O.LoadParser,priority:Te.Normal,name:K},test(m){return V(m,".atlas")},async load(m){const o=await re.ADAPTER.fetch(m);if(!o.ok)throw new Error(`[${K}] Failed to fetch ${m}: ${o.status} ${o.statusText}`);return await o.text()},testParse(m,o){const e=V(o.src,".atlas"),t=typeof m=="string",s=o.loadParser===K;return Promise.resolve((e||s)&&t)},unload(m){m.dispose()},async parse(m,o,e){var p;const t=o.data||{};let s=q.dirname(o.src);s&&s.lastIndexOf("/")!==s.length-1&&(s+="/");const n=new Ee(m);if(t.images instanceof fe||typeof t.images=="string"){const h=t.images;t.images={},t.images[n.pages[0].name]=h}const r=[];let l=!0;for(const h of e.parsers)if(h.name==="loadTextures"){l=(p=h.config)==null?void 0:p.preferCreateImageBitmap;break}Q.setPreferences({preferCreateImageBitmap:!1});for(const h of n.pages){const u=h.name,c=t!=null&&t.images?t.images[u]:void 0;if(c instanceof fe)h.setTexture(G.from(c));else{const i=c??q.normalize([...s.split(q.sep),u].join(q.sep)),g={src:De(i,o.src),data:{...t.imageMetadLine truncated
attribute vec2 aVertexPosition;
attribute vec2 aTextureCoord;
uniform mat3 projectionMatrix;
uniform mat3 translationMatrix;
uniform mat3 uTextureMatrix;
varying vec2 vTextureCoord;
void main(void)
{
gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0);
vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy;
}
`,rt=`
varying vec2 vTextureCoord;
uniform vec4 uColor;
uniform vec4 uDarkColor;
uniform sampler2D uSampler;
void main(void)
{
vec4 texColor = texture2D(uSampler, vTextureCoord);
gl_FragColor.a = texColor.a * uColor.a;
gl_FragColor.rgb = ((texColor.a - 1.0) * uDarkColor.a + 1.0 - texColor.rgb) * uDarkColor.rgb + texColor.rgb * uColor.rgb;
}
`;class it extends Le{constructor(e){const t={uSampler:e??he.EMPTY,alpha:1,uTextureMatrix:Re.IDENTITY,uColor:new Float32Array([1,1,1,1]),uDarkColor:new Float32Array([0,0,0,0])},s={tint:16777215,darkTint:0,alpha:1,pluginName:"darkTintBatch"};super(Ie.from(at,rt),t);a(this,"uvMatrix");a(this,"batchable");a(this,"pluginName");a(this,"_tintRGB");a(this,"_darkTintRGB");a(this,"_colorDirty");a(this,"_alpha");a(this,"_tintColor");a(this,"_darkTintColor");this._colorDirty=!1,this.uvMatrix=new Ne(t.uSampler),this.batchable=!0,this.pluginName=s.pluginName,this._tintColor=new X(s.tint),this._darkTintColor=new X(s.darkTint),this._tintRGB=this._tintColor.toLittleEndianNumber(),this._darkTintRGB=this._darkTintColor.toLittleEndianNumber(),this._alpha=s.alpha,this._colorDirty=!0}get texture(){return this.uniforms.uSampler}set texture(e){this.uniforms.uSampler!==e&&(!this.uniforms.uSampler.baseTexture.alphaMode!=!e.baseTexture.alphaMode&&(this._colorDirty=!0),this.uniforms.uSampler=e,this.uvMatrix.texture=e)}set alpha(e){e!==this._alpha&&(this._alpha=e,this._colorDirty=!0)}get alpha(){return this._alpha}set tint(e){e!==this.tint&&(this._tintColor.setValue(e),this._tintRGB=this._tintColor.toLittleEndianNumber(),this._colorDirty=!0)}get tint(){return this._tintColor.value}set darkTint(e){e!==this.darkTint&&(this._darkTintColor.setValue(e),this._darkTintRGB=this._darkTintColor.toLittleEndianNumber(),this._colorDirty=!0)}get darkTint(){return this._darkTintColor.value}get tintValue(){return this._tintColor.toNumber()}get darkTintValue(){return this._darkTintColor.toNumber()}update(){this._colorDirty&&(this._colorDirty=!1,X.shared.setValue(this._tintColor).premultiply(this._alpha,!0).toArray(this.uniforms.uColor),X.shared.setValue(this._darkTintColor).premultiply(this._alpha,!0).premultiply(1,!1).toArray(this.uniforms.uDarkColor)),this.uvMatrix.update()&&(this.uniforms.uTextureMatrix=this.uvMatrix.mapCoord)}}class lt extends be{constructor(e){super(new nt,new it(e),void 0,void 0);a(this,"_darkTintRGB",0)}get darkTint(){return"darkTint"in this.shader?this.shader.darkTint:null}set darkTint(e){this.shader.darkTint=e}get darkTintValue(){return this.shader.darkTintValue}_renderToBatch(e){const t=this.geometry,s=this.shader;s.uvMatrix&&(s.uvMatrix.update(),this.calculateUvs()),this.calculateVertices(),this.indices=t.indexBuffer.data,this._tintRGB=s._tintRGB,this._darkTintRGB=s._darkTintRGB,this._texture=s.texture;const n=this.material.pluginName;e.batch.setObjectRenderer(e.plugins[n]),e.plugins[n].render(this)}}const y=class y extends lt{constructor(){super();a(this,"name","")}updateFromSpineData(e,t,s,n,r,l,p,h){var C,T;this.texture=e.texture;const u=r/(h?12:8)*2,c=this.geometry.getBuffer("aTextureCoord");((C=c.data)==null?void 0:C.length)!==u&&(c.data=new Float32Array(u));const i=this.geometry.getBuffer("aVertexPosition");((T=i.data)==null?void 0:T.length)!==u&&(i.data=new Float32Array(u));let g=0;const x=c.data,d=i.data;for(let k=0;k<r;k+=h?12:8){let b=k;d[g]=n[b++],d[g+1]=n[b++],b+=4,x[g]=n[b++],x[g+1]=n[b++],g+=2}h?(y.auxColor[0]=n[8],y.auxColor[1]=n[9],y.auxColor[2]=n[10],y.auxColor[3]=n[11],this.darkTint=y.auxColor,y.auxColor[0]=n[2],y.auxColor[1]=n[3],y.auxColor[2]=n[4],y.auxColor[3]=n[5],this.tint=y.auxColor):(y.auxColor[0]=n[2],y.auxColor[1]=n[3],y.auxColor[2]=n[4],y.auxColor[3]=n[5],this.tint=y.auxColor),this.blendMode=G.toPixiBlending(t),this.alpha=y.auxColor[3];const f=this.geometry.indexBuffer;if(f.data.length!==l.length)f.data=new Uint32Array(l);else{const k=f.data;for(let b=0;b<p;b++)k[b]=l[b]}this.name=s,c.update(),i.update(),f.update()}};a(y,"auxColor",[0,0,0,0]);let ie=y;class ht extends we{constructor(e=!1){super();a(this,"_buffer");a(this,"_indexBuffer");this._buffer=new $(void 0,e,!1),this._indexBuffer=new $(void 0,e,!0),this.addAttribute("aVertexPosition",this._buffer,2,!1,Y.FLOAT).addAttribute("aTextureCoord",this._buffer,2,!1,Y.FLOAT).addAttribute("aColor",this._buffer,4,!0,Y.UNSIGNED_BYTE).addAttribute("aDarkColor",this._buffer,4,!0,Y.UNSIGNED_BYTE).addAttribute("aTextureId",this._buffer,1,!0,Y.FLOAT).addIndex(this._indexBuffer)}}const ct=`
precision highp float;
attribute vec2 aVertexPosition;
attribute vec2 aTextureCoord;
attribute vec4 aColor;
attribute vec4 aDarkColor;
attribute float aTextureId;
uniform mat3 projectionMatrix;
uniform mat3 translationMatrix;
uniform vec4 tint;
varying vec2 vTextureCoord;
varying vec4 vColor;
varying vec4 vDarkColor;
varying float vTextureId;
void main(void){
gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0);
vTextureCoord = aTextureCoord;
vTextureId = aTextureId;
vColor = aColor * tint;
vDarkColor = aDarkColor * tint;
}
`,dt=`
varying vec2 vTextureCoord;
varying vec4 vColor;
varying vec4 vDarkColor;
varying float vTextureId;
uniform sampler2D uSamplers[%count%];
void main(void){
vec4 color;
%forloop%
gl_FragColor.a = color.a * vColor.a;
gl_FragColor.rgb = ((color.a - 1.0) * vDarkColor.a + 1.0 - color.rgb) * vDarkColor.rgb + color.rgb * vColor.rgb;
}
`;class Se extends _e{constructor(o){super(o),this.shaderGenerator=new Ue(ct,dt),this.geometryClass=ht,this.vertexSize=7}packInterleavedGeometry(o,e,t,s,n){const{uint32View:r,float32View:l}=e,p=s/this.vertexSize,h=o.uvs,u=o.indices,c=o.vertexData,i=o._texture.baseTexture._batchLocation,g=Math.min(o.worldAlpha,1),x=X.shared.setValue(o._tintRGB).toPremultiplied(g,!0),d=X.shared.setValue(o._darkTintRGB).premultiply(g,!0).toPremultiplied(1,!1);for(let f=0;f<c.length;f+=2)l[s++]=c[f],l[s++]=c[f+1],l[s++]=h[f],l[s++]=h[f+1],r[s++]=x,r[s++]=d,l[s++]=i;for(let f=0;f<u.length;f++)t[n++]=p+u[f]}}a(Se,"extension",{name:"darkTintBatch",type:O.RendererPlugin});ce.add(Se);const U=class U extends be{constructor(){const e=new Ye;e.getBuffer("aVertexPosition").static=!1,e.getBuffer("aTextureCoord").static=!1;const t=new Xe(he.EMPTY);super(e,t);a(this,"name","");a(this,"warnedTwoTint",!1)}updateFromSpineData(e,t,s,n,r,l,p,h){var C,T;this.texture=e.texture;const u=r/(h?12:8)*2,c=this.geometry.getBuffer("aTextureCoord");((C=c.data)==null?void 0:C.length)!==u&&(c.data=new Float32Array(u));const i=this.geometry.getBuffer("aVertexPosition");((T=i.data)==null?void 0:T.length)!==u&&(i.data=new Float32Array(u));let g=0;const x=c.data,d=i.data;for(let k=0;k<r;k+=h?12:8){let b=k;d[g]=n[b++],d[g+1]=n[b++],b+=4,x[g]=n[b++],x[g+1]=n[b++],g+=2}h&&!this.warnedTwoTint&&(console.warn("DarkTint is not enabled by default. To enable use a DarkSlotMesh factory while creating the Spine object."),this.warnedTwoTint=!0),U.auxColor[0]=n[2],U.auxColor[1]=n[3],U.auxColor[2]=n[4],U.auxColor[3]=n[5],this.tint=U.auxColor,this.alpha=U.auxColor[3],this.blendMode=G.toPixiBlending(t);const f=this.geometry.indexBuffer;if(f.data.length!==l.length)f.data=new Uint32Array(l);else{const k=f.data;for(let b=0;b<p;b++)k[b]=l[b]}this.name=s,c.update(),i.update(),f.update()}};a(U,"auxColor",[0,0,0,0]);let le=U;class mt{constructor(o,e,t,s){a(this,"x");a(this,"y");a(this,"width");a(this,"height");this.x=o,this.y=e,this.width=t,this.height=s}calculateBounds(){return{x:this.x,y:this.y,width:this.width,height:this.height}}}class gt{constructor(o=!1){a(this,"clipping");this.clipping=o}calculateBounds(o){if(!o.skeleton)return{x:0,y:0,width:0,height:0};const e=new ee(o.skeleton.data);e.setupPose(),e.updateWorldTransform(j.update);const t=e.getBoundsRect(this.clipping?new de:void 0);return t.width===Number.NEGATIVE_INFINITY?{x:0,y:0,width:0,height:0}:t}}class xt{constructor(o,e=[],t=.05,s=!1){a(this,"animation");a(this,"skins");a(this,"timeStep");a(this,"clipping");this.animation=o,this.skins=e,this.timeStep=t,this.clipping=s}calculateBounds(o){if(!o.skeleton||!o.state)return{x:0,y:0,width:0,height:0};const e=new ke(o.state.data),t=new ee(o.skeleton.data),s=this.clipping?new de:void 0,n=t.data;if(this.skins.length>0){const l=new Ve("custom-skin");for(const p of this.skins){const h=n.findSkin(p);h!=null&&l.addSkin(h)}t.setSkin(l)}t.setupPose();const r=this.animation!=null?n.findAnimation(this.animation):null;if(r==null){t.updateWorldTransform(j.update);const l=t.getBoundsRect(s);return l.width===Number.NEGATIVE_INFINITY?{x:0,y:0,width:0,height:0}:l}else{let l=Number.POSITIVE_INFINITY,p=Number.POSITIVE_INFINITY,h=Number.NEGATIVE_INFINITY,u=Number.NEGATIVE_INFINITY;e.clearTracks(),e.setAnimation(0,r,!1);const c=Math.max(r.duration/this.timeStep,1);for(let g=0;g<c;g++){const x=g>0?this.timeStep:0;e.update(x),e.apply(t),t.update(x),t.updateWorldTransform(j.update);const d=t.getBoundsRect(s);l=Math.min(l,d.x),p=Math.min(p,d.y),h=Math.max(h,d.x+d.width),u=Math.max(u,d.y+d.height)}const i={x:l,y:p,width:h-l,height:u-p};return i.width===Number.NEGATIVE_INFINITY?{x:0,y:0,width:0,height:0}:i}}}const w=class w extends J{constructor(e){super();a(this,"skeleton");a(this,"state");a(this,"skeletonPhysics");a(this,"darkTint",!1);a(this,"hasNeverUpdated",!0);a(this,"_debug");a(this,"slotMeshFactory",()=>new le);a(this,"beforeUpdateWorldTransforms",()=>{});a(this,"afterUpdateWorldTransforms",()=>{});a(this,"_autoUpdate",!1);a(this,"_ticker",ge.shared);a(this,"meshesCache",new Map);a(this,"lightColor",new ae);a(this,"darkColor",new ae);a(this,"clippingVertAux",new Float32Array(6));a(this,"_boundsProvider");a(this,"_boundsPoint",new We);a(this,"_boundsSpineID",-1);a(this,"_boundsSpineDirty",!0);a(this,"slotsObject",new Map);a(this,"verticesCache",xe.newFloatArray(1024));a(this,"clippingSlotToPixiMasks",{});e instanceof $e?e={skeletonData:e}:"skeleton"in e&&(e=new.target.createOptions(e));const{autoUpdate:t=!0,boundsProvider:s,darkTint:n,skeletonData:r,ticker:l}=e;this.skeleton=new ee(r),this.skeletonPhysics=new Ze(this.skeleton,{readTransform:(p,h)=>this.readPhysicsTransform(p,h),worldToSkeleton:p=>this.pixiWorldCoordinatesToSkeleton(p)}),this.state=new ke(new Je(r)),l&&(this._ticker=l),this.autoUpdate=t,this.boundsProvider=s,this.darkTint=n===void 0?this.skeleton.slots.some(p=>!!p.data.setupPose.darkColor):n,this.darkTint&&(this.slotMeshFactory=()=>new ie)}get debug(){return this._debug}set debu Line truncated
+1327
View File
File diff suppressed because it is too large. Load diff
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+63
View File
@@ -0,0 +1,63 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="cn" viewBox="0 0 24 16">
<rect width="24" height="16" fill="#de2910"/>
<path fill="#ffde00" d="m5 2.2.7 2.1h2.2L6.1 5.6 6.8 7.7 5 6.4 3.2 7.7l.7-2.1-1.8-1.3h2.2z"/>
<circle cx="9" cy="2.6" r=".55" fill="#ffde00"/>
<circle cx="10.4" cy="4" r=".55" fill="#ffde00"/>
<circle cx="10.4" cy="5.8" r=".55" fill="#ffde00"/>
<circle cx="9" cy="7.2" r=".55" fill="#ffde00"/>
</symbol>
<symbol id="us" viewBox="0 0 24 16">
<rect width="24" height="16" fill="#fff"/>
<g fill="#b22234">
<path d="M0 0h24v1.23H0zm0 2.46h24v1.23H0zm0 2.46h24v1.23H0zm0 2.47h24v1.23H0zm0 2.46h24v1.23H0zm0 2.46h24v1.23H0zm0 2.46h24V16H0z"/>
</g>
<rect width="10" height="8.6" fill="#3c3b6e"/>
<g fill="#fff">
<circle cx="1.7" cy="1.5" r=".35"/><circle cx="4.2" cy="1.5" r=".35"/><circle cx="6.7" cy="1.5" r=".35"/><circle cx="9.2" cy="1.5" r=".35"/>
<circle cx="2.9" cy="2.9" r=".35"/><circle cx="5.4" cy="2.9" r=".35"/><circle cx="7.9" cy="2.9" r=".35"/>
<circle cx="1.7" cy="4.3" r=".35"/><circle cx="4.2" cy="4.3" r=".35"/><circle cx="6.7" cy="4.3" r=".35"/><circle cx="9.2" cy="4.3" r=".35"/>
<circle cx="2.9" cy="5.7" r=".35"/><circle cx="5.4" cy="5.7" r=".35"/><circle cx="7.9" cy="5.7" r=".35"/>
<circle cx="1.7" cy="7.1" r=".35"/><circle cx="4.2" cy="7.1" r=".35"/><circle cx="6.7" cy="7.1" r=".35"/><circle cx="9.2" cy="7.1" r=".35"/>
</g>
</symbol>
<symbol id="jp" viewBox="0 0 24 16">
<rect width="24" height="16" fill="#fff"/>
<circle cx="12" cy="8" r="4.4" fill="#bc002d"/>
</symbol>
<symbol id="kr" viewBox="0 0 24 16">
<rect width="24" height="16" fill="#fff"/>
<path d="M12 3.1a4.9 4.9 0 0 1 0 9.8 2.45 2.45 0 0 1 0-4.9A2.45 2.45 0 0 0 12 3.1z" fill="#cd2e3a"/>
<path d="M12 3.1a4.9 4.9 0 0 0 0 9.8 2.45 2.45 0 0 0 0-4.9 2.45 2.45 0 0 1 0-4.9z" fill="#0047a0"/>
<g fill="#111">
<path d="m3.6 3.7 3.2-1.8.45.8-3.2 1.8zm.8 1.4 3.2-1.8.45.8-3.2 1.8zm16-1.4 3.2 1.8-.45.8-3.2-1.8zm-.8 1.4 3.2 1.8-.45.8-3.2-1.8zM3.6 12.3l3.2 1.8-.45.8-3.2-1.8zm.8-1.4 3.2 1.8-.45.8-3.2-1.8zm16 1.4 3.2-1.8.45.8-3.2 1.8zm-.8-1.4 3.2-1.8.45.8-3.2 1.8z"/>
</g>
</symbol>
<symbol id="ru" viewBox="0 0 24 16">
<rect width="24" height="5.34" fill="#fff"/>
<rect y="5.34" width="24" height="5.33" fill="#0039a6"/>
<rect y="10.67" width="24" height="5.33" fill="#d52b1e"/>
</symbol>
<symbol id="fr" viewBox="0 0 24 16">
<rect width="8" height="16" fill="#0055a4"/>
<rect x="8" width="8" height="16" fill="#fff"/>
<rect x="16" width="8" height="16" fill="#ef4135"/>
</symbol>
<symbol id="de" viewBox="0 0 24 16">
<rect width="24" height="5.34" fill="#111"/>
<rect y="5.34" width="24" height="5.33" fill="#dd0000"/>
<rect y="10.67" width="24" height="5.33" fill="#ffce00"/>
</symbol>
<symbol id="br" viewBox="0 0 24 16">
<rect width="24" height="16" fill="#009c3b"/>
<path d="M12 2.1 22.2 8 12 13.9 1.8 8z" fill="#ffdf00"/>
<circle cx="12" cy="8" r="3.1" fill="#002776"/>
<path d="M9.2 7.1c1.7.8 4.1.6 5.7-.2" fill="none" stroke="#fff" stroke-width=".65"/>
</symbol>
<symbol id="es" viewBox="0 0 24 16">
<rect width="24" height="16" fill="#aa151b"/>
<rect y="3.4" width="24" height="9.2" fill="#f1bf00"/>
<rect x="6.5" y="6.3" width="2.1" height="3.5" rx=".4" fill="#aa151b"/>
<rect x="6.4" y="7.1" width="2.3" height=".6" fill="#f1bf00"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

+15 -2
View File
@@ -3,13 +3,26 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/favicon.png?v=2" sizes="256x256" />
<title>Spine粒子编辑器</title> <title>Spine粒子编辑器</title>
<style> <style>
html, body { margin: 0; padding: 0; background: #111; color: #eee; font-family: system-ui, -apple-system, sans-serif; } html, body { margin: 0; padding: 0; background: #111; color: #eee; font-family: system-ui, -apple-system, sans-serif; }
#app { display: flex; flex-direction: column; height: 100vh; } #app { display: flex; flex-direction: column; height: 100vh; }
</style> </style>
<script type="module" crossorigin src="/assets/index-DcKlG1-Y.js"></script> <script type="module" crossorigin src="/assets/index-CqaZEMQp.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BRJ6rx0Q.css"> <link rel="modulepreload" crossorigin href="/assets/vendor-fsmZiMPK.js">
<link rel="stylesheet" crossorigin href="/assets/index-DI0p1gIE.css">
<script>
var _paq = window._paq = window._paq || [];
_paq.push(['enableLinkTracking']);
(function() {
var u="//web-analysis.yeomai.site/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '3']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
+1
View File
@@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/favicon.png?v=2" sizes="256x256" />
<title>Spine粒子编辑器</title> <title>Spine粒子编辑器</title>
<style> <style>
html, body { margin: 0; padding: 0; background: #111; color: #eee; font-family: system-ui, -apple-system, sans-serif; } html, body { margin: 0; padding: 0; background: #111; color: #eee; font-family: system-ui, -apple-system, sans-serif; }
+1535 -1
View File
File diff suppressed because it is too large. Load diff
+13 -2
View File
@@ -5,7 +5,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc -b && vite build", "build": "vue-tsc -b && vite build && node scripts/obfuscate-build.mjs",
"connector": "node connector/spine-connector.mjs", "connector": "node connector/spine-connector.mjs",
"build:connector": "node scripts/build-connector-app.mjs", "build:connector": "node scripts/build-connector-app.mjs",
"build:connector:legacy-sea": "node scripts/build-connector-sea.mjs", "build:connector:legacy-sea": "node scripts/build-connector-sea.mjs",
@@ -13,6 +13,15 @@
"test:vortex": "node scripts/verify-vortex.mjs", "test:vortex": "node scripts/verify-vortex.mjs",
"test:timeline": "node scripts/verify-timeline.mjs", "test:timeline": "node scripts/verify-timeline.mjs",
"test:spine-json": "node scripts/verify-spine-json.mjs", "test:spine-json": "node scripts/verify-spine-json.mjs",
"test:spine-naming": "node scripts/verify-spine-naming.mjs",
"test:render-layer": "node scripts/verify-render-layer.mjs",
"test:particle-events": "node scripts/verify-particle-events.mjs",
"test:sub-emitter-timeline": "node scripts/verify-sub-emitter-timeline.mjs",
"test:sequence-defaults": "node scripts/verify-sequence-defaults.mjs",
"test:announcement": "node scripts/verify-announcement.mjs",
"test:export-timeline": "node scripts/verify-export-timeline.mjs",
"test:i18n": "node scripts/verify-i18n.mjs",
"i18n:generate": "node scripts/generate-i18n-locales.mjs && node scripts/apply-i18n-overrides.mjs",
"test:connector": "node scripts/verify-spine-connector.mjs", "test:connector": "node scripts/verify-spine-connector.mjs",
"test:connector:spine38-path": "node scripts/verify-spine38-connector-path.mjs", "test:connector:spine38-path": "node scripts/verify-spine38-connector-path.mjs",
"test:connector:sea": "node scripts/verify-connector-sea.mjs", "test:connector:sea": "node scripts/verify-connector-sea.mjs",
@@ -26,11 +35,13 @@
"@spine-particle/spine-pixi-v7-4.3": "file:vendor/spine/spine-pixi-v7-4.3", "@spine-particle/spine-pixi-v7-4.3": "file:vendor/spine/spine-pixi-v7-4.3",
"pinia": "^4.0.3", "pinia": "^4.0.3",
"pixi.js": "^7.4.3", "pixi.js": "^7.4.3",
"vue": "^3.5.13" "vue": "^3.5.13",
"vue-i18n": "^11.4.16"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.19.43", "@types/node": "^20.19.43",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"javascript-obfuscator": "^5.9.0",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vite": "^6.0.5", "vite": "^6.0.5",
"vue-tsc": "^2.2.0" "vue-tsc": "^2.2.0"
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File renamed without changes.
+88 -10
View File
@@ -1,22 +1,100 @@
[ [
{ {
"name": "1.粒子扫过", "file": "1.粒子扫过.json",
"file": "1.粒子扫过.json" "name": {
"zh-CN": "1.粒子扫过",
"zh-TW": "1.粒子扫过",
"en": "1.粒子扫过",
"ja": "1.粒子扫过",
"ko": "1.粒子扫过",
"fr": "1.粒子扫过",
"de": "1.粒子扫过",
"pt-BR": "1.粒子扫过",
"es": "1.粒子扫过"
}
}, },
{ {
"name": "2.长条方块", "file": "2.长条方块.json",
"file": "2.长条方块.json" "name": {
"zh-CN": "2.长条方块",
"zh-TW": "2.长条方块",
"en": "2.长条方块",
"ja": "2.长条方块",
"ko": "2.长条方块",
"fr": "2.长条方块",
"de": "2.长条方块",
"pt-BR": "2.长条方块",
"es": "2.长条方块"
}
}, },
{ {
"name": "3.力场拖尾", "file": "3.力场拖尾.json",
"file": "3.力场拖尾.json" "name": {
"zh-CN": "3.力场拖尾",
"zh-TW": "3.力场拖尾",
"en": "3.力场拖尾",
"ja": "3.力场拖尾",
"ko": "3.力场拖尾",
"fr": "3.力场拖尾",
"de": "3.力场拖尾",
"pt-BR": "3.力场拖尾",
"es": "3.力场拖尾"
}
}, },
{ {
"name": "4.雷电序列帧", "file": "4.雷电序列帧.json",
"file": "4.雷电序列帧.json" "name": {
"zh-CN": "4.雷电序列帧",
"zh-TW": "4.雷电序列帧",
"en": "4.雷电序列帧",
"ja": "4.雷电序列帧",
"ko": "4.雷电序列帧",
"fr": "4.雷电序列帧",
"de": "4.雷电序列帧",
"pt-BR": "4.雷电序列帧",
"es": "4.雷电序列帧"
}
}, },
{ {
"name": "5.SpineBoy", "file": "5.贴图轴点.json",
"file": "5.SpineBoy.json" "name": {
"zh-CN": "5.贴图轴点",
"zh-TW": "5.贴图轴点",
"en": "5.贴图轴点",
"ja": "5.贴图轴点",
"ko": "5.贴图轴点",
"fr": "5.贴图轴点",
"de": "5.贴图轴点",
"pt-BR": "5.贴图轴点",
"es": "5.贴图轴点"
}
},
{
"file": "6.子发射器.json",
"name": {
"zh-CN": "6.子发射器",
"zh-TW": "6.子发射器",
"en": "6.子发射器",
"ja": "6.子发射器",
"ko": "6.子发射器",
"fr": "6.子发射器",
"de": "6.子发射器",
"pt-BR": "6.子发射器",
"es": "6.子发射器"
}
},
{
"file": "7.SpineBoy.json",
"name": {
"zh-CN": "7.SpineBoy",
"zh-TW": "7.SpineBoy",
"en": "7.SpineBoy",
"ja": "7.SpineBoy",
"ko": "7.SpineBoy",
"fr": "7.SpineBoy",
"de": "7.SpineBoy",
"pt-BR": "7.SpineBoy",
"es": "7.SpineBoy"
}
} }
] ]
+16
View File
@@ -0,0 +1,16 @@
{
"enabled": true,
"scrollDurationSeconds": 24,
"messages": {
"zh-CN": "2026-11-04更新:修正路径跟随导出效果。",
"zh-TW": "2026-11-04更新:修正路徑跟隨匯出效果。",
"en": "Updated 2026-11-04: fixed path-following in exports.",
"ja": "2026-11-04 更新:パス追従のエクスポート結果を修正しました。",
"ko": "2026-11-04 업데이트: 경로 추적 내보내기 결과를 수정했습니다.",
"ru": "Обновление от 04.11.2026: исправлен экспорт со следованием по пути.",
"fr": "Mise à jour du 04/11/2026 : correction du suivi de trajectoire lors de l’exportation.",
"de": "Update vom 04.11.2026: Der Export mit Pfadverfolgung wurde korrigiert.",
"pt-BR": "Atualização de 04/11/2026: corrigida a exportação com acompanhamento de trajetória.",
"es": "Actualización del 04/11/2026: corregida la exportación con seguimiento de trayectoria."
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Loaded 100 of 207 files, more files were not shown because too many files have changed in this diff. Show more