新增spine版本支持

This commit is contained in:
tianmo
2026-09-01 17:22:18 +08:00
parent 8802bbe25e
commit 19278dac01
91 changed files with 69095 additions and 5744 deletions
+3 -1
View File
@@ -2,7 +2,9 @@
node_modules/
# Build output (仅忽略根目录的 Vite 构建产物;不要用无锚点的 dist/,否则会误把
# vendor/spine/*/dist 这类必需的第三方运行时文件一并忽略,导致 clone 后项目无法运行)
/dist
/release
/release-tauri
/src-tauri/target
# TypeScript build cache
*.tsbuildinfo
+6
View File
@@ -0,0 +1,6 @@
[
{
"name": "粒子序列帧拖尾",
"file": "粒子序列帧拖尾.json"
}
]
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1238
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 3.1 MiB

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+17
View File
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Spine Particle · 阶段1 Spike</title>
<style>
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; }
</style>
<script type="module" crossorigin src="/assets/index-Dw7eU6re.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BPQ62DCC.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
File diff suppressed because it is too large Load Diff
+286
View File
@@ -0,0 +1,286 @@
spineboy.png
size: 1024,256
format: RGBA8888
filter: Linear,Linear
repeat: none
crosshair
rotate: false
xy: 352, 7
size: 45, 45
orig: 45, 45
offset: 0, 0
index: -1
eye-indifferent
rotate: false
xy: 862, 105
size: 47, 45
orig: 47, 45
offset: 0, 0
index: -1
eye-surprised
rotate: false
xy: 505, 79
size: 47, 45
orig: 47, 45
offset: 0, 0
index: -1
front-bracer
rotate: false
xy: 826, 66
size: 29, 40
orig: 29, 40
offset: 0, 0
index: -1
front-fist-closed
rotate: false
xy: 786, 65
size: 38, 41
orig: 38, 41
offset: 0, 0
index: -1
front-fist-open
rotate: true
xy: 710, 51
size: 43, 44
orig: 43, 44
offset: 0, 0
index: -1
front-foot
rotate: false
xy: 210, 6
size: 63, 35
orig: 63, 35
offset: 0, 0
index: -1
front-shin
rotate: true
xy: 665, 128
size: 41, 92
orig: 41, 92
offset: 0, 0
index: -1
front-thigh
rotate: true
xy: 2, 2
size: 23, 56
orig: 23, 56
offset: 0, 0
index: -1
front-upper-arm
rotate: false
xy: 250, 205
size: 23, 49
orig: 23, 49
offset: 0, 0
index: -1
goggles
rotate: false
xy: 665, 171
size: 131, 83
orig: 131, 83
offset: 0, 0
index: -1
gun
rotate: false
xy: 798, 152
size: 105, 102
orig: 105, 102
offset: 0, 0
index: -1
head
rotate: false
xy: 2, 27
size: 136, 149
orig: 136, 149
offset: 0, 0
index: -1
hoverboard-board
rotate: false
xy: 2, 178
size: 246, 76
orig: 246, 76
offset: 0, 0
index: -1
hoverboard-thruster
rotate: true
xy: 722, 96
size: 30, 32
orig: 30, 32
offset: 0, 0
index: -1
hoverglow-small
rotate: false
xy: 275, 81
size: 137, 38
orig: 137, 38
offset: 0, 0
index: -1
mouth-grind
rotate: false
xy: 614, 97
size: 47, 30
orig: 47, 30
offset: 0, 0
index: -1
mouth-oooo
rotate: false
xy: 612, 65
size: 47, 30
orig: 47, 30
offset: 0, 0
index: -1
mouth-smile
rotate: false
xy: 661, 64
size: 47, 30
orig: 47, 30
offset: 0, 0
index: -1
muzzle-glow
rotate: false
xy: 382, 54
size: 25, 25
orig: 25, 25
offset: 0, 0
index: -1
muzzle-ring
rotate: true
xy: 275, 54
size: 25, 105
orig: 25, 105
offset: 0, 0
index: -1
muzzle01
rotate: true
xy: 911, 95
size: 67, 40
orig: 67, 40
offset: 0, 0
index: -1
muzzle02
rotate: false
xy: 792, 108
size: 68, 42
orig: 68, 42
offset: 0, 0
index: -1
muzzle03
rotate: true
xy: 956, 171
size: 83, 53
orig: 83, 53
offset: 0, 0
index: -1
muzzle04
rotate: false
xy: 275, 7
size: 75, 45
orig: 75, 45
offset: 0, 0
index: -1
muzzle05
rotate: false
xy: 140, 3
size: 68, 38
orig: 68, 38
offset: 0, 0
index: -1
neck
rotate: false
xy: 250, 182
size: 18, 21
orig: 18, 21
offset: 0, 0
index: -1
portal-bg
rotate: false
xy: 140, 43
size: 133, 133
orig: 133, 133
offset: 0, 0
index: -1
portal-flare1
rotate: false
xy: 554, 65
size: 56, 30
orig: 56, 30
offset: 0, 0
index: -1
portal-flare2
rotate: true
xy: 759, 112
size: 57, 31
orig: 57, 31
offset: 0, 0
index: -1
portal-flare3
rotate: false
xy: 554, 97
size: 58, 30
orig: 58, 30
offset: 0, 0
index: -1
portal-shade
rotate: false
xy: 275, 121
size: 133, 133
orig: 133, 133
offset: 0, 0
index: -1
portal-streaks1
rotate: false
xy: 410, 126
size: 126, 128
orig: 126, 128
offset: 0, 0
index: -1
portal-streaks2
rotate: false
xy: 538, 129
size: 125, 125
orig: 125, 125
offset: 0, 0
index: -1
rear-bracer
rotate: false
xy: 857, 67
size: 28, 36
orig: 28, 36
offset: 0, 0
index: -1
rear-foot
rotate: false
xy: 663, 96
size: 57, 30
orig: 57, 30
offset: 0, 0
index: -1
rear-shin
rotate: true
xy: 414, 86
size: 38, 89
orig: 38, 89
offset: 0, 0
index: -1
rear-thigh
rotate: false
xy: 756, 63
size: 28, 47
orig: 28, 47
offset: 0, 0
index: -1
rear-upper-arm
rotate: true
xy: 60, 5
size: 20, 44
orig: 20, 44
offset: 0, 0
index: -1
torso
rotate: false
xy: 905, 164
size: 49, 90
orig: 49, 90
offset: 0, 0
index: -1
Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

File diff suppressed because it is too large Load Diff
+101
View File
@@ -0,0 +1,101 @@
spineboy.png
size: 1024, 256
filter: Linear, Linear
scale: 0.5
crosshair
bounds: 813, 160, 45, 45
eye-indifferent
bounds: 569, 2, 47, 45
eye-surprised
bounds: 643, 7, 47, 45
rotate: 90
front-bracer
bounds: 811, 51, 29, 40
front-fist-closed
bounds: 807, 93, 38, 41
front-fist-open
bounds: 815, 210, 43, 44
front-foot
bounds: 706, 64, 63, 35
rotate: 90
front-shin
bounds: 80, 11, 41, 92
front-thigh
bounds: 754, 12, 23, 56
front-upper-arm
bounds: 618, 5, 23, 49
goggles
bounds: 214, 20, 131, 83
gun
bounds: 347, 14, 105, 102
rotate: 90
head
bounds: 80, 105, 136, 149
hoverboard-board
bounds: 2, 8, 246, 76
rotate: 90
hoverboard-thruster
bounds: 478, 2, 30, 32
hoverglow-small
bounds: 218, 117, 137, 38
rotate: 90
mouth-grind
bounds: 775, 80, 47, 30
rotate: 90
mouth-oooo
bounds: 779, 31, 47, 30
rotate: 90
mouth-smile
bounds: 783, 207, 47, 30
rotate: 90
muzzle-glow
bounds: 779, 4, 25, 25
muzzle-ring
bounds: 451, 14, 25, 105
muzzle01
bounds: 664, 60, 67, 40
rotate: 90
muzzle02
bounds: 580, 56, 68, 42
rotate: 90
muzzle03
bounds: 478, 36, 83, 53
rotate: 90
muzzle04
bounds: 533, 49, 75, 45
rotate: 90
muzzle05
bounds: 624, 56, 68, 38
rotate: 90
neck
bounds: 806, 8, 18, 21
portal-bg
bounds: 258, 121, 133, 133
portal-flare1
bounds: 690, 2, 56, 30
rotate: 90
portal-flare2
bounds: 510, 3, 57, 31
portal-flare3
bounds: 722, 4, 58, 30
rotate: 90
portal-shade
bounds: 393, 121, 133, 133
portal-streaks1
bounds: 528, 126, 126, 128
portal-streaks2
bounds: 656, 129, 125, 125
rear-bracer
bounds: 826, 13, 28, 36
rear-foot
bounds: 743, 70, 57, 30
rotate: 90
rear-shin
bounds: 174, 14, 38, 89
rear-thigh
bounds: 783, 158, 28, 47
rear-upper-arm
bounds: 783, 136, 20, 44
rotate: 90
torso
bounds: 123, 13, 49, 90
Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

File diff suppressed because it is too large Load Diff
+101
View File
@@ -0,0 +1,101 @@
spineboy.png
size: 1024, 256
filter: Linear, Linear
scale: 0.5
crosshair
bounds: 813, 160, 45, 45
eye-indifferent
bounds: 569, 2, 47, 45
eye-surprised
bounds: 643, 7, 47, 45
rotate: 90
front-bracer
bounds: 811, 51, 29, 40
front-fist-closed
bounds: 807, 93, 38, 41
front-fist-open
bounds: 815, 210, 43, 44
front-foot
bounds: 706, 64, 63, 35
rotate: 90
front-shin
bounds: 80, 11, 41, 92
front-thigh
bounds: 754, 12, 23, 56
front-upper-arm
bounds: 618, 5, 23, 49
goggles
bounds: 214, 20, 131, 83
gun
bounds: 347, 14, 105, 102
rotate: 90
head
bounds: 80, 105, 136, 149
hoverboard-board
bounds: 2, 8, 246, 76
rotate: 90
hoverboard-thruster
bounds: 478, 2, 30, 32
hoverglow-small
bounds: 218, 117, 137, 38
rotate: 90
mouth-grind
bounds: 775, 80, 47, 30
rotate: 90
mouth-oooo
bounds: 779, 31, 47, 30
rotate: 90
mouth-smile
bounds: 783, 207, 47, 30
rotate: 90
muzzle-glow
bounds: 779, 4, 25, 25
muzzle-ring
bounds: 451, 14, 25, 105
muzzle01
bounds: 664, 60, 67, 40
rotate: 90
muzzle02
bounds: 580, 56, 68, 42
rotate: 90
muzzle03
bounds: 478, 36, 83, 53
rotate: 90
muzzle04
bounds: 533, 49, 75, 45
rotate: 90
muzzle05
bounds: 624, 56, 68, 38
rotate: 90
neck
bounds: 806, 8, 18, 21
portal-bg
bounds: 258, 121, 133, 133
portal-flare1
bounds: 690, 2, 56, 30
rotate: 90
portal-flare2
bounds: 510, 3, 57, 31
portal-flare3
bounds: 722, 4, 58, 30
rotate: 90
portal-shade
bounds: 393, 121, 133, 133
portal-streaks1
bounds: 528, 126, 126, 128
portal-streaks2
bounds: 656, 129, 125, 125
rear-bracer
bounds: 826, 13, 28, 36
rear-foot
bounds: 743, 70, 57, 30
rotate: 90
rear-shin
bounds: 174, 14, 38, 89
rear-thigh
bounds: 783, 158, 28, 47
rear-upper-arm
bounds: 783, 136, 20, 44
rotate: 90
torso
bounds: 123, 13, 49, 90
Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

File diff suppressed because it is too large Load Diff
+94
View File
@@ -0,0 +1,94 @@
spineboy.png
size: 1024, 256
filter: Linear, Linear
scale: 0.5
crosshair
bounds: 352, 7, 45, 45
eye-indifferent
bounds: 862, 105, 47, 45
eye-surprised
bounds: 505, 79, 47, 45
front-bracer
bounds: 826, 66, 29, 40
front-fist-closed
bounds: 786, 65, 38, 41
front-fist-open
bounds: 710, 51, 43, 44
rotate: 90
front-foot
bounds: 210, 6, 63, 35
front-shin
bounds: 665, 128, 41, 92
rotate: 90
front-thigh
bounds: 2, 2, 23, 56
rotate: 90
front-upper-arm
bounds: 250, 205, 23, 49
goggles
bounds: 665, 171, 131, 83
gun
bounds: 798, 152, 105, 102
head
bounds: 2, 27, 136, 149
hoverboard-board
bounds: 2, 178, 246, 76
hoverboard-thruster
bounds: 722, 96, 30, 32
rotate: 90
hoverglow-small
bounds: 275, 81, 137, 38
mouth-grind
bounds: 614, 97, 47, 30
mouth-oooo
bounds: 612, 65, 47, 30
mouth-smile
bounds: 661, 64, 47, 30
muzzle-glow
bounds: 382, 54, 25, 25
muzzle-ring
bounds: 275, 54, 25, 105
rotate: 90
muzzle01
bounds: 911, 95, 67, 40
rotate: 90
muzzle02
bounds: 792, 108, 68, 42
muzzle03
bounds: 956, 171, 83, 53
rotate: 90
muzzle04
bounds: 275, 7, 75, 45
muzzle05
bounds: 140, 3, 68, 38
neck
bounds: 250, 182, 18, 21
portal-bg
bounds: 140, 43, 133, 133
portal-flare1
bounds: 554, 65, 56, 30
portal-flare2
bounds: 759, 112, 57, 31
rotate: 90
portal-flare3
bounds: 554, 97, 58, 30
portal-shade
bounds: 275, 121, 133, 133
portal-streaks1
bounds: 410, 126, 126, 128
portal-streaks2
bounds: 538, 129, 125, 125
rear-bracer
bounds: 857, 67, 28, 36
rear-foot
bounds: 663, 96, 57, 30
rear-shin
bounds: 414, 86, 38, 89
rotate: 90
rear-thigh
bounds: 756, 63, 28, 47
rear-upper-arm
bounds: 60, 5, 20, 44
rotate: 90
torso
bounds: 905, 164, 49, 90
Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

+30
View File
@@ -0,0 +1,30 @@
# Spine 多版本测试资源
这些资源来自 Esoteric Software 官方的
[`spine-runtimes`](https://github.com/EsotericSoftware/spine-runtimes)
仓库,用于测试本项目对 Spine 3.8、4.0、4.1、4.2 JSON 数据的加载、预览与动画联动。
## 目录结构
每个版本目录都包含同一套 Spineboy Pro 示例:
- `spineboy-pro.json`:骨骼与动画数据
- `spineboy.atlas`:纹理图集
- `spineboy.png`:图集贴图
| 目录 | JSON 中的实际 Spine 版本 | 浏览器访问路径 |
| --- | --- | --- |
| `3.8/` | `3.8.55` | `/spine-test/3.8/` |
| `4.0/` | `4.0.31` | `/spine-test/4.0/` |
| `4.1/` | `4.1.17` | `/spine-test/4.1/` |
| `4.2/` | `4.2.22` | `/spine-test/4.2/` |
## 手动测试
1. 新建一个 Spine 场景对象。
2. 依次选择对应版本目录中的 JSON、Atlas 和 PNG 文件。
3. 点击“加载”。
4. 检查资源版本、骨骼层级、动画列表、时间轴与粒子挂载联动。
测试时不要混用不同版本目录中的文件。资源版权和使用条件遵循
[`spine-runtimes` License](https://github.com/EsotericSoftware/spine-runtimes/blob/4.2/LICENSE)。
View File
View File

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Before

Width:  |  Height:  |  Size: 4.7 MiB

After

Width:  |  Height:  |  Size: 4.7 MiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

-492
View File
@@ -1,492 +0,0 @@
{
"format": "SpineParticle.EditorConfig",
"version": 1,
"savedAt": "2026-08-31T09:38:45.071Z",
"editor": {
"viewScale": 6.866040888412048,
"viewX": -86.9344445981659,
"viewY": 14.683070674664151
},
"settings": {
"tickEnabled": false,
"axisFontSize": 12,
"tickWidth": 1,
"showAxisLabels": true,
"axisColor": "#7a86ad",
"undoLimit": 20,
"backgroundX": 0,
"backgroundY": 0,
"backgroundOpacity": 1,
"backgroundImageName": "",
"backgroundImageData": "",
"showEmitter": true,
"showBones": true,
"showSkinMesh": false,
"showGrid": true,
"exportSettings": {
"imagesPath": "./images/",
"spineVersion": "4.2",
"keyframeCurve": "linear",
"fps": 30,
"omitFps": false,
"integerFrameAlignment": true,
"bonePool": true,
"excludeEmptyAnimations": false,
"fileName": "SpineParticle.json"
}
},
"timeline": {
"fps": 30,
"loop": true,
"animations": [
"animation"
]
},
"selection": {
"type": "particle",
"id": 1
},
"scene": {
"systems": [
{
"id": 1,
"visible": true,
"animation": "animation",
"config": {
"imageResources": [
{
"id": 1,
"textureName": "star",
"textureFolder": "",
"previewUrl": "/images/star.png",
"imageMode": "fixed",
"sequencePlayback": "loop-forward",
"sequenceFps": 30,
"sequenceFrames": [],
"weight": 100,
"locked": false,
"scale": 1,
"anchorX": 0.5,
"anchorY": 0.5,
"colorMode": "fixed",
"colorStart": "#ffffff",
"colorEnd": "#ff4d4d",
"colorGradient": [
{
"position": 0,
"color": "#ffffff"
},
{
"position": 1,
"color": "#ff4d4d"
}
],
"blend": "normal",
"independentAlpha": "off",
"alpha": 1,
"alphaCurveMode": "跟随导出",
"alphaCurve": [
{
"x": 0,
"y": 0
},
{
"x": 0.1,
"y": 1
},
{
"x": 0.9,
"y": 1
},
{
"x": 1,
"y": 0
}
]
}
],
"imageWeightVersion": 1,
"name": "ParticleSystem1",
"mode": "burst",
"rate": 20,
"streamBehavior": "normal",
"loopDurationFrames": 30,
"generateLoopStartAnimation": false,
"generateLoopEndAnimation": false,
"burstCount": 1,
"burstLoop": false,
"delay": 0,
"duration": 1,
"shape": "point",
"radius": 30,
"rectW": 60,
"rectH": 40,
"centerX": 0,
"centerY": 0,
"rootRotation": 0,
"rootScaleX": 1,
"rootScaleY": 1,
"direction": 0,
"spread": 360,
"coneAngle": 60,
"lifeMin": 1,
"lifeMax": 1.5,
"lifeMode": "fixed",
"lifeCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"speedMin": 271,
"speedMax": 307,
"speedMode": "random",
"speedCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"scaleMin": 0.3,
"scaleMax": 0.8,
"scaleMode": "random",
"scaleAxisMode": "uniform",
"scaleYMin": 0.3,
"scaleYMax": 0.8,
"scaleCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"scaleYCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"scaleEndRatio": 0.3,
"initialRotationMode": "random",
"initialRotationMin": 0,
"initialRotationMax": 360,
"initialRotationCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"alpha": 0.9,
"alphaEnd": 0,
"rotationSpeedMin": -180,
"rotationSpeedMax": 180,
"speedOverLifeEnabled": false,
"speedOverLifeMode": "fixed",
"speedOverLifeMin": 1,
"speedOverLifeMax": 1,
"speedOverLifeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"scaleOverLifeEnabled": false,
"scaleOverLifeMode": "curve",
"scaleOverLifeAxisMode": "uniform",
"scaleOverLifeMin": 0,
"scaleOverLifeMax": 1,
"scaleYOverLifeMin": 0,
"scaleYOverLifeMax": 1,
"scaleOverLifeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"scaleYOverLifeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"rotationOverLifeEnabled": false,
"rotationOverLifeMode": "random",
"rotationOverLifeCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"gravity": 980,
"gravityMode": "fixed",
"gravityMin": 980,
"gravityMax": 980,
"gravityDirection": -90,
"gravityCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"damping": 0,
"seed": 0,
"colorStart": "#ffffff",
"colorEnd": "#ff4d4d",
"colorOverLife": false,
"blend": "add",
"alphaMode": "curve",
"curveMode": "线性",
"alphaCurve": [
{
"x": 0,
"y": 0
},
{
"x": 0.2,
"y": 1
},
{
"x": 0.8,
"y": 1
},
{
"x": 1,
"y": 0
}
],
"textureName": "star",
"textureFolder": "",
"alphaScale": 1,
"anchorX": 0.5,
"anchorY": 0.5,
"independentAlpha": "off",
"offsetX": 0,
"offsetY": 0,
"noise": false,
"noiseAmt": 30,
"noiseRange": 100,
"noiseSpeed": 1,
"useGravity": true,
"wind": false,
"windX": 0,
"windY": 0,
"windMode": "fixed",
"windMin": 100,
"windMax": 100,
"windDirection": 0,
"windCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"forceField": false,
"forceStrength": 10,
"forceRadius": 100,
"forceCenterX": 0,
"forceCenterY": 0,
"forceFalloff": "linear",
"attraction": false,
"attractionCenterX": 200,
"attractionCenterY": 0,
"attractionRadius": 20,
"attractionDuration": 0.5,
"attractionDelay": 0.1,
"attractionDelayMode": "particle",
"attractionPath": "curve",
"trail": true,
"trailConfigVersion": 3,
"trailResources": [
{
"id": 1,
"textureName": "trail",
"previewUrl": "/images/trail.png",
"weight": 100,
"locked": false,
"rotation": 0,
"colorMode": "particle",
"color": "#ffffff",
"colorGradient": [
{
"position": 0,
"color": "#ffffff"
},
{
"position": 1,
"color": "#ffffff"
}
],
"blend": "normal",
"alphaMode": "particle",
"alpha": 1,
"alphaCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"boneCount": 5,
"length": 100,
"width": 100,
"widthMode": "particle",
"lifeLengthEnabled": false,
"lifeLengthCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"shapeEnabled": true,
"shapeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
]
}
],
"trailResourceVersion": 5,
"trailTextureName": "trail",
"trailPreviewUrl": "/images/trail.png",
"trailColorMode": "particle",
"trailColor": "#ffffff",
"trailColorGradient": [
{
"position": 0,
"color": "#ffffff"
},
{
"position": 1,
"color": "#ffffff"
}
],
"trailAlphaMode": "particle",
"trailAlpha": 1,
"trailAlphaCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"trailBoneCount": 3,
"trailLength": 100,
"trailWidth": 100,
"trailWidthMode": "particle",
"trailLifeLengthEnabled": false,
"trailLifeLengthCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"trailShapeEnabled": false,
"trailShapeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"trailGridRows": 4,
"trailGridCols": 5,
"collision": false,
"pathFollow": false,
"emitterFollow": false,
"emitterFollowMode": "none",
"emitterFollowPathId": 0,
"emitterFollowSpineId": 0,
"emitterFollowBoneName": "",
"emitterFollowBoneOffsetX": 0,
"emitterFollowBoneOffsetY": 0,
"emitterFollowBoneRotation": 0,
"emitterFollowBoneScaleX": 1,
"emitterFollowBoneScaleY": 1,
"emitterAngleMode": "fixed",
"emitterFollowCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"emitterFollowDuration": 2,
"emitterFollowSpace": "world",
"emitterFollowDirection": "forward",
"emitterFollowOffset": 0
}
}
],
"colliders": [],
"paths": [],
"spines": []
}
}
+4 -28
View File
@@ -1,34 +1,10 @@
[
{
"name": "粒子-路径",
"file": "粒子-路径.json"
"name": "粒子-序列帧-拖尾-碰撞-路径-spine",
"file": "粒子-序列帧-拖尾-碰撞-路径-spine.json"
},
{
"name": "粒子-碰撞",
"file": "粒子-碰撞.json"
},
{
"name": "纯粒子-spine",
"file": "纯粒子-spine.json"
},
{
"name": "纯粒子",
"file": "纯粒子.json"
},
{
"name": "空场景",
"file": "空场景.json"
},
{
"name": "默认粒子",
"file": "默认粒子.json"
},
{
"name": "TrailTest",
"file": "TrailTest.json"
},
{
"name": "yanhua",
"file": "yanhua.json"
"name": "粒子序列帧拖尾",
"file": "粒子序列帧拖尾.json"
}
]
File diff suppressed because it is too large Load Diff
-42
View File
@@ -1,42 +0,0 @@
{
"format": "SpineParticle.EditorConfig",
"version": 1,
"savedAt": "2026-08-29T00:00:00.000Z",
"editor": {
"viewScale": 1,
"viewX": 0,
"viewY": 0
},
"settings": {
"tickEnabled": false,
"axisFontSize": 12,
"tickWidth": 1,
"showAxisLabels": true,
"axisColor": "#7a86ad",
"undoLimit": 20,
"backgroundX": 0,
"backgroundY": 0,
"backgroundOpacity": 1,
"backgroundImageName": "",
"backgroundImageData": "",
"showEmitter": true,
"showBones": false,
"showSkinMesh": false,
"showGrid": true
},
"timeline": {
"fps": 30,
"loop": true,
"animations": ["animation"]
},
"selection": {
"type": "particle",
"id": 0
},
"scene": {
"systems": [],
"colliders": [],
"paths": [],
"spines": []
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-514
View File
@@ -1,514 +0,0 @@
{
"format": "SpineParticle.EditorConfig",
"version": 1,
"savedAt": "2026-08-29T16:30:02.631Z",
"editor": {
"viewScale": 0.7971938775510203,
"viewX": -195.50844387755103,
"viewY": -83.49033163265301
},
"settings": {
"tickEnabled": false,
"axisFontSize": 12,
"tickWidth": 1,
"showAxisLabels": true,
"axisColor": "#7a86ad",
"undoLimit": 20,
"backgroundX": 0,
"backgroundY": 0,
"backgroundOpacity": 1,
"backgroundImageName": "",
"backgroundImageData": "",
"showEmitter": true,
"showBones": false,
"showSkinMesh": false,
"showGrid": true
},
"timeline": {
"fps": 30,
"loop": true,
"animations": [
"animation"
]
},
"selection": {
"type": "collision",
"id": 1
},
"scene": {
"systems": [
{
"id": 1,
"visible": true,
"animation": "animation",
"config": {
"imageResources": [
{
"id": 1,
"textureName": "star",
"textureFolder": "",
"previewUrl": "/particles/star.png",
"weight": 100,
"locked": false,
"scale": 2,
"anchorX": 0.5,
"anchorY": 0.5,
"colorMode": "fixed",
"colorStart": "#e83030",
"colorEnd": "#ff4d4d",
"colorGradient": [
{
"position": 0,
"color": "#ffffff"
},
{
"position": 1,
"color": "#ff4d4d"
}
],
"blend": "normal",
"independentAlpha": "off",
"alpha": 1,
"alphaCurveMode": "跟随导出",
"alphaCurve": [
{
"x": 0,
"y": 0
},
{
"x": 0.1,
"y": 1
},
{
"x": 0.9,
"y": 1
},
{
"x": 1,
"y": 0
}
]
}
],
"imageWeightVersion": 1,
"name": "ParticleSystem1",
"mode": "stream",
"rate": 43,
"burstCount": 50,
"burstLoop": false,
"delay": 0.1,
"duration": 12.6333333333333,
"shape": "point",
"radius": 30,
"rectW": 60,
"rectH": 40,
"centerX": 10,
"centerY": -123,
"rootRotation": 46,
"rootScaleX": 1.95,
"rootScaleY": 1.05,
"direction": 7,
"spread": 335,
"coneAngle": 60,
"lifeMin": 1,
"lifeMax": 1,
"lifeMode": "random",
"lifeCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"speedMin": 106,
"speedMax": 156,
"speedMode": "random",
"speedCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"scaleMin": 0.65,
"scaleMax": 1.4,
"scaleMode": "random",
"scaleAxisMode": "uniform",
"scaleYMin": 0.3,
"scaleYMax": 0.8,
"scaleCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"scaleYCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"scaleEndRatio": 0.3,
"initialRotationMode": "random",
"initialRotationMin": 20,
"initialRotationMax": 313,
"initialRotationCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"alpha": 0.9,
"alphaEnd": 0,
"rotationSpeedMin": -310,
"rotationSpeedMax": 370,
"speedOverLifeEnabled": true,
"speedOverLifeMode": "fixed",
"speedOverLifeMin": 0.85,
"speedOverLifeMax": 1,
"speedOverLifeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"scaleOverLifeEnabled": true,
"scaleOverLifeMode": "curve",
"scaleOverLifeAxisMode": "uniform",
"scaleOverLifeMin": 0.15,
"scaleOverLifeMax": 1.95,
"scaleYOverLifeMin": 0,
"scaleYOverLifeMax": 1,
"scaleOverLifeCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"scaleYOverLifeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"rotationOverLifeEnabled": true,
"rotationOverLifeMode": "random",
"rotationOverLifeCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"gravity": 980,
"gravityMode": "fixed",
"gravityMin": 200,
"gravityMax": 980,
"gravityDirection": -90,
"gravityCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"damping": 0,
"seed": 0,
"colorStart": "#ffffff",
"colorEnd": "#ff4d4d",
"colorOverLife": false,
"blend": "add",
"alphaMode": "curve",
"curveMode": "线性",
"alphaCurve": [
{
"x": 0,
"y": 0
},
{
"x": 0.2,
"y": 1
},
{
"x": 0.8,
"y": 1
},
{
"x": 1,
"y": 0
}
],
"textureName": "star",
"textureFolder": "",
"alphaScale": 1,
"anchorX": 0.5,
"anchorY": 0.5,
"independentAlpha": "off",
"offsetX": 15,
"offsetY": -288,
"noise": false,
"noiseAmt": 30,
"noiseRange": 100,
"noiseSpeed": 1,
"useGravity": true,
"wind": true,
"windX": 0,
"windY": 0,
"windMode": "fixed",
"windMin": -200,
"windMax": 100,
"windDirection": 0,
"windCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"forceField": false,
"forceStrength": 10,
"forceRadius": 100,
"forceCenterX": 0,
"forceCenterY": 0,
"forceFalloff": "linear",
"attraction": false,
"attractionCenterX": 200,
"attractionCenterY": 0,
"attractionRadius": 20,
"attractionDuration": 0.5,
"attractionDelay": 0.1,
"attractionDelayMode": "particle",
"attractionPath": "curve",
"trail": false,
"trailConfigVersion": 3,
"trailResources": [
{
"id": 1,
"textureName": "trail",
"previewUrl": "/trails/trail.png",
"weight": 100,
"locked": false,
"rotation": 0,
"colorMode": "particle",
"color": "#ffffff",
"colorGradient": [
{
"position": 0,
"color": "#ffffff"
},
{
"position": 1,
"color": "#ffffff"
}
],
"blend": "normal",
"alphaMode": "particle",
"alpha": 1,
"alphaCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"boneCount": 3,
"length": 100,
"width": 100,
"widthMode": "particle",
"lifeLengthEnabled": false,
"lifeLengthCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"shapeEnabled": false,
"shapeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
]
}
],
"trailResourceVersion": 5,
"trailTextureName": "trail",
"trailPreviewUrl": "/trails/trail.png",
"trailColorMode": "particle",
"trailColor": "#ffffff",
"trailColorGradient": [
{
"position": 0,
"color": "#ffffff"
},
{
"position": 1,
"color": "#ffffff"
}
],
"trailAlphaMode": "particle",
"trailAlpha": 1,
"trailAlphaCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"trailBoneCount": 3,
"trailLength": 100,
"trailWidth": 100,
"trailWidthMode": "particle",
"trailLifeLengthEnabled": false,
"trailLifeLengthCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"trailShapeEnabled": false,
"trailShapeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"trailGridRows": 4,
"trailGridCols": 4,
"collision": false,
"pathFollow": false,
"emitterFollow": false,
"emitterFollowMode": "none",
"emitterFollowPathId": 0,
"emitterFollowSpineId": 0,
"emitterFollowBoneName": "",
"emitterFollowBoneOffsetX": 0,
"emitterFollowBoneOffsetY": 0,
"emitterFollowBoneRotation": 0,
"emitterFollowBoneScaleX": 1,
"emitterFollowBoneScaleY": 1,
"emitterAngleMode": "fixed",
"emitterFollowCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"emitterFollowDuration": 2,
"emitterFollowSpace": "world",
"emitterFollowDirection": "forward",
"emitterFollowOffset": 0
}
}
],
"colliders": [
{
"id": 1,
"name": "Collider1",
"visible": true,
"tags": [
"Default"
],
"selectedTag": "Default",
"shape": "circle",
"enabled": true,
"x": -97.84312343749997,
"y": -448.23894609375,
"rotation": 61,
"scaleX": 1,
"scaleY": 1,
"radius": 227,
"radiusX": 70,
"radiusY": 45,
"width": 120,
"height": 80,
"sides": 6,
"response": "physics",
"elasticity": 0.8,
"friction": 0.3,
"followPath": false,
"followPathId": 0,
"followDuration": 2,
"followPathRotation": false,
"followCurveMode": "跟随导出",
"followCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
]
}
],
"paths": [],
"spines": []
}
}
-522
View File
@@ -1,522 +0,0 @@
{
"format": "SpineParticle.EditorConfig",
"version": 1,
"savedAt": "2026-08-29T16:31:20.445Z",
"editor": {
"viewScale": 0.7971938775510203,
"viewX": -200.17506985411353,
"viewY": -140.15701864437176
},
"settings": {
"tickEnabled": false,
"axisFontSize": 12,
"tickWidth": 1,
"showAxisLabels": true,
"axisColor": "#7a86ad",
"undoLimit": 20,
"backgroundX": 0,
"backgroundY": 0,
"backgroundOpacity": 1,
"backgroundImageName": "",
"backgroundImageData": "",
"showEmitter": true,
"showBones": false,
"showSkinMesh": false,
"showGrid": true
},
"timeline": {
"fps": 30,
"loop": true,
"animations": [
"animation"
]
},
"selection": {
"type": "path",
"id": 1
},
"scene": {
"systems": [
{
"id": 1,
"visible": true,
"animation": "animation",
"config": {
"imageResources": [
{
"id": 1,
"textureName": "star",
"textureFolder": "",
"previewUrl": "/particles/star.png",
"weight": 100,
"locked": false,
"scale": 2,
"anchorX": 0.5,
"anchorY": 0.5,
"colorMode": "fixed",
"colorStart": "#e83030",
"colorEnd": "#ff4d4d",
"colorGradient": [
{
"position": 0,
"color": "#ffffff"
},
{
"position": 1,
"color": "#ff4d4d"
}
],
"blend": "normal",
"independentAlpha": "off",
"alpha": 1,
"alphaCurveMode": "跟随导出",
"alphaCurve": [
{
"x": 0,
"y": 0
},
{
"x": 0.1,
"y": 1
},
{
"x": 0.9,
"y": 1
},
{
"x": 1,
"y": 0
}
]
}
],
"imageWeightVersion": 1,
"name": "ParticleSystem1",
"mode": "stream",
"rate": 43,
"burstCount": 50,
"burstLoop": false,
"delay": 0.1,
"duration": 12.6333333333333,
"shape": "point",
"radius": 30,
"rectW": 60,
"rectH": 40,
"centerX": 10,
"centerY": -123,
"rootRotation": 46,
"rootScaleX": 1.95,
"rootScaleY": 1.05,
"direction": 7,
"spread": 335,
"coneAngle": 60,
"lifeMin": 1,
"lifeMax": 1,
"lifeMode": "random",
"lifeCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"speedMin": 106,
"speedMax": 156,
"speedMode": "random",
"speedCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"scaleMin": 0.65,
"scaleMax": 1.4,
"scaleMode": "random",
"scaleAxisMode": "uniform",
"scaleYMin": 0.3,
"scaleYMax": 0.8,
"scaleCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"scaleYCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"scaleEndRatio": 0.3,
"initialRotationMode": "random",
"initialRotationMin": 20,
"initialRotationMax": 313,
"initialRotationCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"alpha": 0.9,
"alphaEnd": 0,
"rotationSpeedMin": -310,
"rotationSpeedMax": 370,
"speedOverLifeEnabled": true,
"speedOverLifeMode": "fixed",
"speedOverLifeMin": 0.85,
"speedOverLifeMax": 1,
"speedOverLifeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"scaleOverLifeEnabled": true,
"scaleOverLifeMode": "curve",
"scaleOverLifeAxisMode": "uniform",
"scaleOverLifeMin": 0.15,
"scaleOverLifeMax": 1.95,
"scaleYOverLifeMin": 0,
"scaleYOverLifeMax": 1,
"scaleOverLifeCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"scaleYOverLifeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"rotationOverLifeEnabled": true,
"rotationOverLifeMode": "random",
"rotationOverLifeCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"gravity": 980,
"gravityMode": "fixed",
"gravityMin": 200,
"gravityMax": 980,
"gravityDirection": -90,
"gravityCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"damping": 0,
"seed": 0,
"colorStart": "#ffffff",
"colorEnd": "#ff4d4d",
"colorOverLife": false,
"blend": "add",
"alphaMode": "curve",
"curveMode": "线性",
"alphaCurve": [
{
"x": 0,
"y": 0
},
{
"x": 0.2,
"y": 1
},
{
"x": 0.8,
"y": 1
},
{
"x": 1,
"y": 0
}
],
"textureName": "star",
"textureFolder": "",
"alphaScale": 1,
"anchorX": 0.5,
"anchorY": 0.5,
"independentAlpha": "off",
"offsetX": 15,
"offsetY": -288,
"noise": false,
"noiseAmt": 30,
"noiseRange": 100,
"noiseSpeed": 1,
"useGravity": true,
"wind": true,
"windX": 0,
"windY": 0,
"windMode": "fixed",
"windMin": -200,
"windMax": 100,
"windDirection": 0,
"windCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"forceField": false,
"forceStrength": 10,
"forceRadius": 100,
"forceCenterX": 0,
"forceCenterY": 0,
"forceFalloff": "linear",
"attraction": false,
"attractionCenterX": 200,
"attractionCenterY": 0,
"attractionRadius": 20,
"attractionDuration": 0.5,
"attractionDelay": 0.1,
"attractionDelayMode": "particle",
"attractionPath": "curve",
"trail": false,
"trailConfigVersion": 3,
"trailResources": [
{
"id": 1,
"textureName": "trail",
"previewUrl": "/trails/trail.png",
"weight": 100,
"locked": false,
"rotation": 0,
"colorMode": "particle",
"color": "#ffffff",
"colorGradient": [
{
"position": 0,
"color": "#ffffff"
},
{
"position": 1,
"color": "#ffffff"
}
],
"blend": "normal",
"alphaMode": "particle",
"alpha": 1,
"alphaCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"boneCount": 3,
"length": 100,
"width": 100,
"widthMode": "particle",
"lifeLengthEnabled": false,
"lifeLengthCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"shapeEnabled": false,
"shapeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
]
}
],
"trailResourceVersion": 5,
"trailTextureName": "trail",
"trailPreviewUrl": "/trails/trail.png",
"trailColorMode": "particle",
"trailColor": "#ffffff",
"trailColorGradient": [
{
"position": 0,
"color": "#ffffff"
},
{
"position": 1,
"color": "#ffffff"
}
],
"trailAlphaMode": "particle",
"trailAlpha": 1,
"trailAlphaCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"trailBoneCount": 3,
"trailLength": 100,
"trailWidth": 100,
"trailWidthMode": "particle",
"trailLifeLengthEnabled": false,
"trailLifeLengthCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"trailShapeEnabled": false,
"trailShapeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"trailGridRows": 4,
"trailGridCols": 4,
"collision": false,
"pathFollow": false,
"emitterFollow": false,
"emitterFollowMode": "none",
"emitterFollowPathId": 0,
"emitterFollowSpineId": 0,
"emitterFollowBoneName": "",
"emitterFollowBoneOffsetX": 0,
"emitterFollowBoneOffsetY": 0,
"emitterFollowBoneRotation": 0,
"emitterFollowBoneScaleX": 1,
"emitterFollowBoneScaleY": 1,
"emitterAngleMode": "fixed",
"emitterFollowCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"emitterFollowDuration": 2,
"emitterFollowSpace": "world",
"emitterFollowDirection": "forward",
"emitterFollowOffset": 0
}
}
],
"colliders": [],
"paths": [
{
"id": 1,
"name": "Path1",
"visible": true,
"enabled": true,
"x": -320.2901078125,
"y": 86.97174609375001,
"rotation": 55,
"scaleX": 1,
"scaleY": 2,
"closed": true,
"color": "#ff933e",
"preset": "square",
"points": [
{
"x": -100,
"y": -100,
"inX": 0,
"inY": 0,
"outX": 0,
"outY": 0
},
{
"x": 100,
"y": -100,
"inX": 0,
"inY": 0,
"outX": 0,
"outY": 0
},
{
"x": 100,
"y": 100,
"inX": 0,
"inY": 0,
"outX": 0,
"outY": 0
},
{
"x": -100,
"y": 100,
"inX": 0,
"inY": 0,
"outX": 0,
"outY": 0
}
]
}
],
"spines": []
}
}
-473
View File
@@ -1,473 +0,0 @@
{
"format": "SpineParticle.EditorConfig",
"version": 1,
"savedAt": "2026-08-29T16:29:28.329Z",
"editor": {
"viewScale": 0.7971938775510203,
"viewX": -195.50844387755103,
"viewY": -83.49033163265301
},
"settings": {
"tickEnabled": false,
"axisFontSize": 12,
"tickWidth": 1,
"showAxisLabels": true,
"axisColor": "#7a86ad",
"undoLimit": 20,
"backgroundX": 0,
"backgroundY": 0,
"backgroundOpacity": 1,
"backgroundImageName": "",
"backgroundImageData": "",
"showEmitter": true,
"showBones": false,
"showSkinMesh": false,
"showGrid": true
},
"timeline": {
"fps": 30,
"loop": true,
"animations": [
"animation"
]
},
"selection": {
"type": "particle",
"id": 1
},
"scene": {
"systems": [
{
"id": 1,
"visible": true,
"animation": "animation",
"config": {
"imageResources": [
{
"id": 1,
"textureName": "star",
"textureFolder": "",
"previewUrl": "/particles/star.png",
"weight": 100,
"locked": false,
"scale": 2,
"anchorX": 0.5,
"anchorY": 0.5,
"colorMode": "fixed",
"colorStart": "#e83030",
"colorEnd": "#ff4d4d",
"colorGradient": [
{
"position": 0,
"color": "#ffffff"
},
{
"position": 1,
"color": "#ff4d4d"
}
],
"blend": "normal",
"independentAlpha": "off",
"alpha": 1,
"alphaCurveMode": "跟随导出",
"alphaCurve": [
{
"x": 0,
"y": 0
},
{
"x": 0.1,
"y": 1
},
{
"x": 0.9,
"y": 1
},
{
"x": 1,
"y": 0
}
]
}
],
"imageWeightVersion": 1,
"name": "ParticleSystem1",
"mode": "stream",
"rate": 43,
"burstCount": 50,
"burstLoop": false,
"delay": 0.1,
"duration": 12.6333333333333,
"shape": "point",
"radius": 30,
"rectW": 60,
"rectH": 40,
"centerX": 10,
"centerY": -123,
"rootRotation": 46,
"rootScaleX": 1.95,
"rootScaleY": 1.05,
"direction": 7,
"spread": 335,
"coneAngle": 60,
"lifeMin": 1,
"lifeMax": 1,
"lifeMode": "random",
"lifeCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"speedMin": 106,
"speedMax": 156,
"speedMode": "random",
"speedCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"scaleMin": 0.65,
"scaleMax": 1.4,
"scaleMode": "random",
"scaleAxisMode": "uniform",
"scaleYMin": 0.3,
"scaleYMax": 0.8,
"scaleCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"scaleYCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"scaleEndRatio": 0.3,
"initialRotationMode": "random",
"initialRotationMin": 20,
"initialRotationMax": 313,
"initialRotationCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"alpha": 0.9,
"alphaEnd": 0,
"rotationSpeedMin": -310,
"rotationSpeedMax": 370,
"speedOverLifeEnabled": true,
"speedOverLifeMode": "fixed",
"speedOverLifeMin": 0.85,
"speedOverLifeMax": 1,
"speedOverLifeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"scaleOverLifeEnabled": true,
"scaleOverLifeMode": "curve",
"scaleOverLifeAxisMode": "uniform",
"scaleOverLifeMin": 0.15,
"scaleOverLifeMax": 1.95,
"scaleYOverLifeMin": 0,
"scaleYOverLifeMax": 1,
"scaleOverLifeCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"scaleYOverLifeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"rotationOverLifeEnabled": true,
"rotationOverLifeMode": "random",
"rotationOverLifeCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"gravity": 980,
"gravityMode": "fixed",
"gravityMin": 200,
"gravityMax": 980,
"gravityDirection": -90,
"gravityCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"damping": 0,
"seed": 0,
"colorStart": "#ffffff",
"colorEnd": "#ff4d4d",
"colorOverLife": false,
"blend": "add",
"alphaMode": "curve",
"curveMode": "线性",
"alphaCurve": [
{
"x": 0,
"y": 0
},
{
"x": 0.2,
"y": 1
},
{
"x": 0.8,
"y": 1
},
{
"x": 1,
"y": 0
}
],
"textureName": "star",
"textureFolder": "",
"alphaScale": 1,
"anchorX": 0.5,
"anchorY": 0.5,
"independentAlpha": "off",
"offsetX": 15,
"offsetY": -288,
"noise": false,
"noiseAmt": 30,
"noiseRange": 100,
"noiseSpeed": 1,
"useGravity": true,
"wind": true,
"windX": 0,
"windY": 0,
"windMode": "fixed",
"windMin": -200,
"windMax": 100,
"windDirection": 0,
"windCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"forceField": false,
"forceStrength": 10,
"forceRadius": 100,
"forceCenterX": 0,
"forceCenterY": 0,
"forceFalloff": "linear",
"attraction": false,
"attractionCenterX": 200,
"attractionCenterY": 0,
"attractionRadius": 20,
"attractionDuration": 0.5,
"attractionDelay": 0.1,
"attractionDelayMode": "particle",
"attractionPath": "curve",
"trail": false,
"trailConfigVersion": 3,
"trailResources": [
{
"id": 1,
"textureName": "trail",
"previewUrl": "/trails/trail.png",
"weight": 100,
"locked": false,
"rotation": 0,
"colorMode": "particle",
"color": "#ffffff",
"colorGradient": [
{
"position": 0,
"color": "#ffffff"
},
{
"position": 1,
"color": "#ffffff"
}
],
"blend": "normal",
"alphaMode": "particle",
"alpha": 1,
"alphaCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"boneCount": 3,
"length": 100,
"width": 100,
"widthMode": "particle",
"lifeLengthEnabled": false,
"lifeLengthCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"shapeEnabled": false,
"shapeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
]
}
],
"trailResourceVersion": 5,
"trailTextureName": "trail",
"trailPreviewUrl": "/trails/trail.png",
"trailColorMode": "particle",
"trailColor": "#ffffff",
"trailColorGradient": [
{
"position": 0,
"color": "#ffffff"
},
{
"position": 1,
"color": "#ffffff"
}
],
"trailAlphaMode": "particle",
"trailAlpha": 1,
"trailAlphaCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"trailBoneCount": 3,
"trailLength": 100,
"trailWidth": 100,
"trailWidthMode": "particle",
"trailLifeLengthEnabled": false,
"trailLifeLengthCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"trailShapeEnabled": false,
"trailShapeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"trailGridRows": 4,
"trailGridCols": 4,
"collision": false,
"pathFollow": false,
"emitterFollow": false,
"emitterFollowMode": "none",
"emitterFollowPathId": 0,
"emitterFollowSpineId": 0,
"emitterFollowBoneName": "",
"emitterFollowBoneOffsetX": 0,
"emitterFollowBoneOffsetY": 0,
"emitterFollowBoneRotation": 0,
"emitterFollowBoneScaleX": 1,
"emitterFollowBoneScaleY": 1,
"emitterAngleMode": "fixed",
"emitterFollowCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"emitterFollowDuration": 2,
"emitterFollowSpace": "world",
"emitterFollowDirection": "forward",
"emitterFollowOffset": 0
}
}
],
"colliders": [],
"paths": [],
"spines": []
}
}
-473
View File
@@ -1,473 +0,0 @@
{
"format": "SpineParticle.EditorConfig",
"version": 1,
"savedAt": "2026-08-29T14:25:58.695Z",
"editor": {
"viewScale": 1.12,
"viewX": -7.944298618861609,
"viewY": -12.270927472795819
},
"settings": {
"tickEnabled": false,
"axisFontSize": 12,
"tickWidth": 1,
"showAxisLabels": true,
"axisColor": "#7a86ad",
"undoLimit": 20,
"backgroundX": 0,
"backgroundY": 0,
"backgroundOpacity": 1,
"backgroundImageName": "",
"backgroundImageData": "",
"showEmitter": true,
"showBones": false,
"showSkinMesh": false,
"showGrid": true
},
"timeline": {
"fps": 30,
"loop": true,
"animations": [
"animation"
]
},
"selection": {
"type": "particle",
"id": 2
},
"scene": {
"systems": [
{
"id": 2,
"visible": true,
"animation": "animation",
"config": {
"imageResources": [
{
"id": 1,
"textureName": "star",
"textureFolder": "",
"previewUrl": "/particles/star.png",
"weight": 100,
"locked": false,
"scale": 1,
"anchorX": 0.5,
"anchorY": 0.5,
"colorMode": "fixed",
"colorStart": "#ffffff",
"colorEnd": "#ff4d4d",
"colorGradient": [
{
"position": 0,
"color": "#ffffff"
},
{
"position": 1,
"color": "#ff4d4d"
}
],
"blend": "normal",
"independentAlpha": "off",
"alpha": 1,
"alphaCurveMode": "跟随导出",
"alphaCurve": [
{
"x": 0,
"y": 0
},
{
"x": 0.1,
"y": 1
},
{
"x": 0.9,
"y": 1
},
{
"x": 1,
"y": 0
}
]
}
],
"imageWeightVersion": 1,
"name": "ParticleSystem1",
"mode": "stream",
"rate": 20,
"burstCount": 50,
"burstLoop": false,
"delay": 0,
"duration": 1,
"shape": "point",
"radius": 30,
"rectW": 60,
"rectH": 40,
"centerX": 0,
"centerY": 0,
"rootRotation": 0,
"rootScaleX": 1,
"rootScaleY": 1,
"direction": 0,
"spread": 360,
"coneAngle": 60,
"lifeMin": 0.6,
"lifeMax": 1.8,
"lifeMode": "random",
"lifeCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"speedMin": 30,
"speedMax": 140,
"speedMode": "random",
"speedCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"scaleMin": 0.3,
"scaleMax": 0.8,
"scaleMode": "random",
"scaleAxisMode": "uniform",
"scaleYMin": 0.3,
"scaleYMax": 0.8,
"scaleCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"scaleYCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"scaleEndRatio": 0.3,
"initialRotationMode": "random",
"initialRotationMin": 0,
"initialRotationMax": 360,
"initialRotationCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"alpha": 0.9,
"alphaEnd": 0,
"rotationSpeedMin": -180,
"rotationSpeedMax": 180,
"speedOverLifeEnabled": false,
"speedOverLifeMode": "fixed",
"speedOverLifeMin": 1,
"speedOverLifeMax": 1,
"speedOverLifeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"scaleOverLifeEnabled": false,
"scaleOverLifeMode": "curve",
"scaleOverLifeAxisMode": "uniform",
"scaleOverLifeMin": 0,
"scaleOverLifeMax": 1,
"scaleYOverLifeMin": 0,
"scaleYOverLifeMax": 1,
"scaleOverLifeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"scaleYOverLifeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"rotationOverLifeEnabled": false,
"rotationOverLifeMode": "random",
"rotationOverLifeCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"gravity": 980,
"gravityMode": "fixed",
"gravityMin": 980,
"gravityMax": 980,
"gravityDirection": -90,
"gravityCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"damping": 0,
"seed": 0,
"colorStart": "#ffffff",
"colorEnd": "#ff4d4d",
"colorOverLife": false,
"blend": "add",
"alphaMode": "curve",
"curveMode": "线性",
"alphaCurve": [
{
"x": 0,
"y": 0
},
{
"x": 0.2,
"y": 1
},
{
"x": 0.8,
"y": 1
},
{
"x": 1,
"y": 0
}
],
"textureName": "star",
"textureFolder": "",
"alphaScale": 1,
"anchorX": 0.5,
"anchorY": 0.5,
"independentAlpha": "off",
"offsetX": 0,
"offsetY": 0,
"noise": false,
"noiseAmt": 30,
"noiseRange": 100,
"noiseSpeed": 1,
"useGravity": false,
"wind": false,
"windX": 0,
"windY": 0,
"windMode": "fixed",
"windMin": 100,
"windMax": 100,
"windDirection": 0,
"windCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"forceField": false,
"forceStrength": 10,
"forceRadius": 100,
"forceCenterX": 0,
"forceCenterY": 0,
"forceFalloff": "linear",
"attraction": false,
"attractionCenterX": 200,
"attractionCenterY": 0,
"attractionRadius": 20,
"attractionDuration": 0.5,
"attractionDelay": 0.1,
"attractionDelayMode": "particle",
"attractionPath": "curve",
"trail": false,
"trailConfigVersion": 3,
"trailResources": [
{
"id": 1,
"textureName": "trail",
"previewUrl": "/trails/trail.png",
"weight": 100,
"locked": false,
"rotation": 0,
"colorMode": "particle",
"color": "#ffffff",
"colorGradient": [
{
"position": 0,
"color": "#ffffff"
},
{
"position": 1,
"color": "#ffffff"
}
],
"blend": "normal",
"alphaMode": "particle",
"alpha": 1,
"alphaCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"boneCount": 3,
"length": 100,
"width": 100,
"widthMode": "particle",
"lifeLengthEnabled": false,
"lifeLengthCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"shapeEnabled": false,
"shapeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
]
}
],
"trailResourceVersion": 5,
"trailTextureName": "trail",
"trailPreviewUrl": "/trails/trail.png",
"trailColorMode": "particle",
"trailColor": "#ffffff",
"trailColorGradient": [
{
"position": 0,
"color": "#ffffff"
},
{
"position": 1,
"color": "#ffffff"
}
],
"trailAlphaMode": "particle",
"trailAlpha": 1,
"trailAlphaCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"trailBoneCount": 3,
"trailLength": 100,
"trailWidth": 100,
"trailWidthMode": "particle",
"trailLifeLengthEnabled": false,
"trailLifeLengthCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"trailShapeEnabled": false,
"trailShapeCurve": [
{
"x": 0,
"y": 1
},
{
"x": 1,
"y": 1
}
],
"trailGridRows": 4,
"trailGridCols": 4,
"collision": false,
"pathFollow": false,
"emitterFollow": false,
"emitterFollowMode": "none",
"emitterFollowPathId": 0,
"emitterFollowSpineId": 0,
"emitterFollowBoneName": "",
"emitterFollowBoneOffsetX": 0,
"emitterFollowBoneOffsetY": 0,
"emitterFollowBoneRotation": 0,
"emitterFollowBoneScaleX": 1,
"emitterFollowBoneScaleY": 1,
"emitterAngleMode": "fixed",
"emitterFollowCurve": [
{
"x": 0,
"y": 0
},
{
"x": 1,
"y": 1
}
],
"emitterFollowDuration": 2,
"emitterFollowSpace": "world",
"emitterFollowDirection": "forward",
"emitterFollowOffset": 0
}
}
],
"colliders": [],
"paths": [],
"spines": []
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because it is too large Load Diff
+286
View File
@@ -0,0 +1,286 @@
spineboy.png
size: 1024,256
format: RGBA8888
filter: Linear,Linear
repeat: none
crosshair
rotate: false
xy: 352, 7
size: 45, 45
orig: 45, 45
offset: 0, 0
index: -1
eye-indifferent
rotate: false
xy: 862, 105
size: 47, 45
orig: 47, 45
offset: 0, 0
index: -1
eye-surprised
rotate: false
xy: 505, 79
size: 47, 45
orig: 47, 45
offset: 0, 0
index: -1
front-bracer
rotate: false
xy: 826, 66
size: 29, 40
orig: 29, 40
offset: 0, 0
index: -1
front-fist-closed
rotate: false
xy: 786, 65
size: 38, 41
orig: 38, 41
offset: 0, 0
index: -1
front-fist-open
rotate: true
xy: 710, 51
size: 43, 44
orig: 43, 44
offset: 0, 0
index: -1
front-foot
rotate: false
xy: 210, 6
size: 63, 35
orig: 63, 35
offset: 0, 0
index: -1
front-shin
rotate: true
xy: 665, 128
size: 41, 92
orig: 41, 92
offset: 0, 0
index: -1
front-thigh
rotate: true
xy: 2, 2
size: 23, 56
orig: 23, 56
offset: 0, 0
index: -1
front-upper-arm
rotate: false
xy: 250, 205
size: 23, 49
orig: 23, 49
offset: 0, 0
index: -1
goggles
rotate: false
xy: 665, 171
size: 131, 83
orig: 131, 83
offset: 0, 0
index: -1
gun
rotate: false
xy: 798, 152
size: 105, 102
orig: 105, 102
offset: 0, 0
index: -1
head
rotate: false
xy: 2, 27
size: 136, 149
orig: 136, 149
offset: 0, 0
index: -1
hoverboard-board
rotate: false
xy: 2, 178
size: 246, 76
orig: 246, 76
offset: 0, 0
index: -1
hoverboard-thruster
rotate: true
xy: 722, 96
size: 30, 32
orig: 30, 32
offset: 0, 0
index: -1
hoverglow-small
rotate: false
xy: 275, 81
size: 137, 38
orig: 137, 38
offset: 0, 0
index: -1
mouth-grind
rotate: false
xy: 614, 97
size: 47, 30
orig: 47, 30
offset: 0, 0
index: -1
mouth-oooo
rotate: false
xy: 612, 65
size: 47, 30
orig: 47, 30
offset: 0, 0
index: -1
mouth-smile
rotate: false
xy: 661, 64
size: 47, 30
orig: 47, 30
offset: 0, 0
index: -1
muzzle-glow
rotate: false
xy: 382, 54
size: 25, 25
orig: 25, 25
offset: 0, 0
index: -1
muzzle-ring
rotate: true
xy: 275, 54
size: 25, 105
orig: 25, 105
offset: 0, 0
index: -1
muzzle01
rotate: true
xy: 911, 95
size: 67, 40
orig: 67, 40
offset: 0, 0
index: -1
muzzle02
rotate: false
xy: 792, 108
size: 68, 42
orig: 68, 42
offset: 0, 0
index: -1
muzzle03
rotate: true
xy: 956, 171
size: 83, 53
orig: 83, 53
offset: 0, 0
index: -1
muzzle04
rotate: false
xy: 275, 7
size: 75, 45
orig: 75, 45
offset: 0, 0
index: -1
muzzle05
rotate: false
xy: 140, 3
size: 68, 38
orig: 68, 38
offset: 0, 0
index: -1
neck
rotate: false
xy: 250, 182
size: 18, 21
orig: 18, 21
offset: 0, 0
index: -1
portal-bg
rotate: false
xy: 140, 43
size: 133, 133
orig: 133, 133
offset: 0, 0
index: -1
portal-flare1
rotate: false
xy: 554, 65
size: 56, 30
orig: 56, 30
offset: 0, 0
index: -1
portal-flare2
rotate: true
xy: 759, 112
size: 57, 31
orig: 57, 31
offset: 0, 0
index: -1
portal-flare3
rotate: false
xy: 554, 97
size: 58, 30
orig: 58, 30
offset: 0, 0
index: -1
portal-shade
rotate: false
xy: 275, 121
size: 133, 133
orig: 133, 133
offset: 0, 0
index: -1
portal-streaks1
rotate: false
xy: 410, 126
size: 126, 128
orig: 126, 128
offset: 0, 0
index: -1
portal-streaks2
rotate: false
xy: 538, 129
size: 125, 125
orig: 125, 125
offset: 0, 0
index: -1
rear-bracer
rotate: false
xy: 857, 67
size: 28, 36
orig: 28, 36
offset: 0, 0
index: -1
rear-foot
rotate: false
xy: 663, 96
size: 57, 30
orig: 57, 30
offset: 0, 0
index: -1
rear-shin
rotate: true
xy: 414, 86
size: 38, 89
orig: 38, 89
offset: 0, 0
index: -1
rear-thigh
rotate: false
xy: 756, 63
size: 28, 47
orig: 28, 47
offset: 0, 0
index: -1
rear-upper-arm
rotate: true
xy: 60, 5
size: 20, 44
orig: 20, 44
offset: 0, 0
index: -1
torso
rotate: false
xy: 905, 164
size: 49, 90
orig: 49, 90
offset: 0, 0
index: -1
Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

File diff suppressed because it is too large Load Diff
+101
View File
@@ -0,0 +1,101 @@
spineboy.png
size: 1024, 256
filter: Linear, Linear
scale: 0.5
crosshair
bounds: 813, 160, 45, 45
eye-indifferent
bounds: 569, 2, 47, 45
eye-surprised
bounds: 643, 7, 47, 45
rotate: 90
front-bracer
bounds: 811, 51, 29, 40
front-fist-closed
bounds: 807, 93, 38, 41
front-fist-open
bounds: 815, 210, 43, 44
front-foot
bounds: 706, 64, 63, 35
rotate: 90
front-shin
bounds: 80, 11, 41, 92
front-thigh
bounds: 754, 12, 23, 56
front-upper-arm
bounds: 618, 5, 23, 49
goggles
bounds: 214, 20, 131, 83
gun
bounds: 347, 14, 105, 102
rotate: 90
head
bounds: 80, 105, 136, 149
hoverboard-board
bounds: 2, 8, 246, 76
rotate: 90
hoverboard-thruster
bounds: 478, 2, 30, 32
hoverglow-small
bounds: 218, 117, 137, 38
rotate: 90
mouth-grind
bounds: 775, 80, 47, 30
rotate: 90
mouth-oooo
bounds: 779, 31, 47, 30
rotate: 90
mouth-smile
bounds: 783, 207, 47, 30
rotate: 90
muzzle-glow
bounds: 779, 4, 25, 25
muzzle-ring
bounds: 451, 14, 25, 105
muzzle01
bounds: 664, 60, 67, 40
rotate: 90
muzzle02
bounds: 580, 56, 68, 42
rotate: 90
muzzle03
bounds: 478, 36, 83, 53
rotate: 90
muzzle04
bounds: 533, 49, 75, 45
rotate: 90
muzzle05
bounds: 624, 56, 68, 38
rotate: 90
neck
bounds: 806, 8, 18, 21
portal-bg
bounds: 258, 121, 133, 133
portal-flare1
bounds: 690, 2, 56, 30
rotate: 90
portal-flare2
bounds: 510, 3, 57, 31
portal-flare3
bounds: 722, 4, 58, 30
rotate: 90
portal-shade
bounds: 393, 121, 133, 133
portal-streaks1
bounds: 528, 126, 126, 128
portal-streaks2
bounds: 656, 129, 125, 125
rear-bracer
bounds: 826, 13, 28, 36
rear-foot
bounds: 743, 70, 57, 30
rotate: 90
rear-shin
bounds: 174, 14, 38, 89
rear-thigh
bounds: 783, 158, 28, 47
rear-upper-arm
bounds: 783, 136, 20, 44
rotate: 90
torso
bounds: 123, 13, 49, 90
Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

File diff suppressed because it is too large Load Diff
+101
View File
@@ -0,0 +1,101 @@
spineboy.png
size: 1024, 256
filter: Linear, Linear
scale: 0.5
crosshair
bounds: 813, 160, 45, 45
eye-indifferent
bounds: 569, 2, 47, 45
eye-surprised
bounds: 643, 7, 47, 45
rotate: 90
front-bracer
bounds: 811, 51, 29, 40
front-fist-closed
bounds: 807, 93, 38, 41
front-fist-open
bounds: 815, 210, 43, 44
front-foot
bounds: 706, 64, 63, 35
rotate: 90
front-shin
bounds: 80, 11, 41, 92
front-thigh
bounds: 754, 12, 23, 56
front-upper-arm
bounds: 618, 5, 23, 49
goggles
bounds: 214, 20, 131, 83
gun
bounds: 347, 14, 105, 102
rotate: 90
head
bounds: 80, 105, 136, 149
hoverboard-board
bounds: 2, 8, 246, 76
rotate: 90
hoverboard-thruster
bounds: 478, 2, 30, 32
hoverglow-small
bounds: 218, 117, 137, 38
rotate: 90
mouth-grind
bounds: 775, 80, 47, 30
rotate: 90
mouth-oooo
bounds: 779, 31, 47, 30
rotate: 90
mouth-smile
bounds: 783, 207, 47, 30
rotate: 90
muzzle-glow
bounds: 779, 4, 25, 25
muzzle-ring
bounds: 451, 14, 25, 105
muzzle01
bounds: 664, 60, 67, 40
rotate: 90
muzzle02
bounds: 580, 56, 68, 42
rotate: 90
muzzle03
bounds: 478, 36, 83, 53
rotate: 90
muzzle04
bounds: 533, 49, 75, 45
rotate: 90
muzzle05
bounds: 624, 56, 68, 38
rotate: 90
neck
bounds: 806, 8, 18, 21
portal-bg
bounds: 258, 121, 133, 133
portal-flare1
bounds: 690, 2, 56, 30
rotate: 90
portal-flare2
bounds: 510, 3, 57, 31
portal-flare3
bounds: 722, 4, 58, 30
rotate: 90
portal-shade
bounds: 393, 121, 133, 133
portal-streaks1
bounds: 528, 126, 126, 128
portal-streaks2
bounds: 656, 129, 125, 125
rear-bracer
bounds: 826, 13, 28, 36
rear-foot
bounds: 743, 70, 57, 30
rotate: 90
rear-shin
bounds: 174, 14, 38, 89
rear-thigh
bounds: 783, 158, 28, 47
rear-upper-arm
bounds: 783, 136, 20, 44
rotate: 90
torso
bounds: 123, 13, 49, 90
Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

File diff suppressed because it is too large Load Diff
+94
View File
@@ -0,0 +1,94 @@
spineboy.png
size: 1024, 256
filter: Linear, Linear
scale: 0.5
crosshair
bounds: 352, 7, 45, 45
eye-indifferent
bounds: 862, 105, 47, 45
eye-surprised
bounds: 505, 79, 47, 45
front-bracer
bounds: 826, 66, 29, 40
front-fist-closed
bounds: 786, 65, 38, 41
front-fist-open
bounds: 710, 51, 43, 44
rotate: 90
front-foot
bounds: 210, 6, 63, 35
front-shin
bounds: 665, 128, 41, 92
rotate: 90
front-thigh
bounds: 2, 2, 23, 56
rotate: 90
front-upper-arm
bounds: 250, 205, 23, 49
goggles
bounds: 665, 171, 131, 83
gun
bounds: 798, 152, 105, 102
head
bounds: 2, 27, 136, 149
hoverboard-board
bounds: 2, 178, 246, 76
hoverboard-thruster
bounds: 722, 96, 30, 32
rotate: 90
hoverglow-small
bounds: 275, 81, 137, 38
mouth-grind
bounds: 614, 97, 47, 30
mouth-oooo
bounds: 612, 65, 47, 30
mouth-smile
bounds: 661, 64, 47, 30
muzzle-glow
bounds: 382, 54, 25, 25
muzzle-ring
bounds: 275, 54, 25, 105
rotate: 90
muzzle01
bounds: 911, 95, 67, 40
rotate: 90
muzzle02
bounds: 792, 108, 68, 42
muzzle03
bounds: 956, 171, 83, 53
rotate: 90
muzzle04
bounds: 275, 7, 75, 45
muzzle05
bounds: 140, 3, 68, 38
neck
bounds: 250, 182, 18, 21
portal-bg
bounds: 140, 43, 133, 133
portal-flare1
bounds: 554, 65, 56, 30
portal-flare2
bounds: 759, 112, 57, 31
rotate: 90
portal-flare3
bounds: 554, 97, 58, 30
portal-shade
bounds: 275, 121, 133, 133
portal-streaks1
bounds: 410, 126, 126, 128
portal-streaks2
bounds: 538, 129, 125, 125
rear-bracer
bounds: 857, 67, 28, 36
rear-foot
bounds: 663, 96, 57, 30
rear-shin
bounds: 414, 86, 38, 89
rotate: 90
rear-thigh
bounds: 756, 63, 28, 47
rear-upper-arm
bounds: 60, 5, 20, 44
rotate: 90
torso
bounds: 905, 164, 49, 90
Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

+30
View File
@@ -0,0 +1,30 @@
# Spine 多版本测试资源
这些资源来自 Esoteric Software 官方的
[`spine-runtimes`](https://github.com/EsotericSoftware/spine-runtimes)
仓库,用于测试本项目对 Spine 3.8、4.0、4.1、4.2 JSON 数据的加载、预览与动画联动。
## 目录结构
每个版本目录都包含同一套 Spineboy Pro 示例:
- `spineboy-pro.json`:骨骼与动画数据
- `spineboy.atlas`:纹理图集
- `spineboy.png`:图集贴图
| 目录 | JSON 中的实际 Spine 版本 | 浏览器访问路径 |
| --- | --- | --- |
| `3.8/` | `3.8.55` | `/spine-test/3.8/` |
| `4.0/` | `4.0.31` | `/spine-test/4.0/` |
| `4.1/` | `4.1.17` | `/spine-test/4.1/` |
| `4.2/` | `4.2.22` | `/spine-test/4.2/` |
## 手动测试
1. 新建一个 Spine 场景对象。
2. 依次选择对应版本目录中的 JSON、Atlas 和 PNG 文件。
3. 点击“加载”。
4. 检查资源版本、骨骼层级、动画列表、时间轴与粒子挂载联动。
测试时不要混用不同版本目录中的文件。资源版权和使用条件遵循
[`spine-runtimes` License](https://github.com/EsotericSoftware/spine-runtimes/blob/4.2/LICENSE)。
+5 -1
View File
@@ -186,7 +186,10 @@ export async function applyEditorConfig(store: EditorStore, input: EditorConfigF
const colliders = cloneEditorValue(config.scene.colliders) as unknown as CollisionBody[]
const paths = cloneEditorValue(config.scene.paths) as unknown as ScenePath[]
const spines = cloneEditorValue(config.scene.spines) as unknown as SpineSceneObject[]
for (const spine of spines) if (spine.sourceBundle == null) spine.sourceBundle = null
for (const spine of spines) {
if (spine.sourceBundle == null) spine.sourceBundle = null
if (spine.dataVersion == null) spine.dataVersion = ''
}
await hydrateParticleResources(systems)
const parsedSpines = await parseEmbeddedSpines(spines)
@@ -221,6 +224,7 @@ export async function applyEditorConfig(store: EditorStore, input: EditorConfigF
continue
}
registerSpineAsset(spine.id, parsed)
spine.dataVersion = parsed.dataVersion
spine.animations = parsed.animations
spine.bones = parsed.bones
if (!parsed.animations.some((animation) => animation.name === spine.selectedAnimation)) {
+121
View File
@@ -0,0 +1,121 @@
import type { SpineJsonDocument, SpineJsonMap } from './spineJsonTypes'
/** 按编辑器导出设置保留简洁的 Spine 3.8 版本标识。 */
export const SPINE_38_JSON_VERSION = '3.8'
function rename(target: SpineJsonMap, nextName: string, oldName: string) {
if (target[nextName] == null && target[oldName] != null) target[nextName] = target[oldName]
delete target[oldName]
}
function removeArrayCurves(value: unknown) {
if (!value || typeof value !== 'object') return
if (Array.isArray(value)) {
for (const item of value) removeArrayCurves(item)
return
}
const record = value as SpineJsonMap
// Spine 3.8 只有一组归一化贝塞尔控制点,无法表达 4.x 的逐通道绝对控制点。
// 导出器在 3.8 模式下会重新生成线性精简关键帧;这里也为未来新增时间轴兜底。
if (Array.isArray(record.curve)) delete record.curve
for (const child of Object.values(record)) removeArrayCurves(child)
}
function downgradeSetup(document: SpineJsonDocument) {
for (const bone of document.bones) rename(bone, 'transform', 'inherit')
for (const constraint of (document as any).transform || []) {
rename(constraint, 'rotateMix', 'mixRotate')
if (constraint.translateMix == null) constraint.translateMix = constraint.mixX ?? constraint.mixY
if (constraint.scaleMix == null) constraint.scaleMix = constraint.mixScaleX ?? constraint.mixScaleY
rename(constraint, 'shearMix', 'mixShearY')
delete constraint.mixX
delete constraint.mixY
delete constraint.mixScaleX
delete constraint.mixScaleY
}
for (const constraint of (document as any).path || []) {
rename(constraint, 'rotateMix', 'mixRotate')
if (constraint.translateMix == null) constraint.translateMix = constraint.mixX ?? constraint.mixY
delete constraint.mixX
delete constraint.mixY
}
for (const skin of document.skins) {
for (const slot of Object.values<SpineJsonMap>(skin.attachments || {})) {
for (const attachment of Object.values<SpineJsonMap>(slot || {})) {
if (attachment.parent != null) rename(attachment, 'deform', 'timelines')
}
}
}
}
function downgradeSlots(animation: SpineJsonMap) {
for (const slot of Object.values<SpineJsonMap>(animation.slots || {})) {
rename(slot, 'color', 'rgba')
rename(slot, 'twoColor', 'rgba2')
}
}
function downgradeBones(animation: SpineJsonMap) {
for (const bone of Object.values<SpineJsonMap>(animation.bones || {})) {
for (const frame of bone.rotate || []) rename(frame, 'angle', 'value')
}
}
function downgradeTransformConstraints(animation: SpineJsonMap) {
for (const frames of Object.values<SpineJsonMap[]>(animation.transform || {})) {
for (const frame of frames) {
rename(frame, 'rotateMix', 'mixRotate')
if (frame.translateMix == null) frame.translateMix = frame.mixX ?? frame.mixY
if (frame.scaleMix == null) frame.scaleMix = frame.mixScaleX ?? frame.mixScaleY
rename(frame, 'shearMix', 'mixShearY')
delete frame.mixX
delete frame.mixY
delete frame.mixScaleX
delete frame.mixScaleY
}
}
}
function downgradePathConstraints(animation: SpineJsonMap) {
for (const constraint of Object.values<SpineJsonMap>(animation.path || {})) {
for (const timelineName of ['position', 'spacing']) {
for (const frame of constraint[timelineName] || []) rename(frame, timelineName, 'value')
}
for (const frame of constraint.mix || []) {
rename(frame, 'rotateMix', 'mixRotate')
if (frame.translateMix == null) frame.translateMix = frame.mixX ?? frame.mixY
delete frame.mixX
delete frame.mixY
}
}
}
function downgradeDeforms(animation: SpineJsonMap) {
if (!animation.attachments) return
const deform = animation.deform || (animation.deform = {})
for (const [skinName, skin] of Object.entries<SpineJsonMap>(animation.attachments)) {
const targetSkin = deform[skinName] || (deform[skinName] = {})
for (const [slotName, slot] of Object.entries<SpineJsonMap>(skin)) {
const targetSlot = targetSkin[slotName] || (targetSkin[slotName] = {})
for (const [attachmentName, timelines] of Object.entries<SpineJsonMap>(slot)) {
if (timelines.deform) targetSlot[attachmentName] = timelines.deform
}
}
}
delete animation.attachments
}
/** 把编辑器内部生成的 4.x 公共结构降级为 Spine 3.8 软件可导入的 JSON。 */
export function downgradeSpineJsonTo38(document: SpineJsonDocument) {
document.skeleton.spine = SPINE_38_JSON_VERSION
downgradeSetup(document)
for (const animation of Object.values(document.animations)) {
downgradeSlots(animation)
downgradeBones(animation)
downgradeTransformConstraints(animation)
downgradePathConstraints(animation)
downgradeDeforms(animation)
}
removeArrayCurves(document.animations)
return document
}
+4 -2
View File
@@ -1,4 +1,6 @@
export type SpineExportVersion = '4.2'
import { normalizeSpineVersion, type SupportedSpineVersion } from '../spine/spineVersion'
export type SpineExportVersion = SupportedSpineVersion
export type SpineExportKeyframeCurve = 'linear' | 'bezier'
/** Spine JSON 导出的全局参数。实际 JSON 生成逻辑将在后续阶段接入。 */
@@ -32,7 +34,7 @@ export function defaultSpineExportSettings(): SpineExportSettings {
export function ensureSpineExportSettings(value?: Partial<SpineExportSettings> | null): SpineExportSettings {
const settings = Object.assign(defaultSpineExportSettings(), value || {})
settings.imagesPath = String(settings.imagesPath || './images/')
settings.spineVersion = '4.2'
settings.spineVersion = normalizeSpineVersion(settings.spineVersion) || '4.2'
settings.keyframeCurve = settings.keyframeCurve === 'bezier' ? 'bezier' : 'linear'
settings.fps = Math.min(240, Math.max(1, Math.round(Number(settings.fps) || 30)))
settings.omitFps = settings.omitFps === true
+11 -6
View File
@@ -2,6 +2,7 @@ import type { EmitterConfig, ParticleImageResource, ParticleState, TrailImageRes
import type { ParticleSystem, TimelineState } from '../store/particleStore'
import type { SpineExportSettings } from './spineExportSettings'
import type { SpineExportResult, SpineExportWarning, SpineJsonDocument, SpineJsonMap } from './spineJsonTypes'
import { downgradeSpineJsonTo38 } from './spine38JsonExporter'
import { reduceBezierSamples, reduceNumericSamples, roundSpine, unwrapDegrees, type NumericSample } from './spineKeyframeReducer'
import { buildWeightedTrailMesh } from './spineMeshBuilder'
@@ -111,7 +112,7 @@ function resolvedTimelineCurve(mode: '跟随导出' | '线性' | '贝塞尔' | u
function numericTimeline(samples: NumericSample[], fps: number, names: string[], tolerances: number[], settings: SpineExportSettings, forcedFrames: Set<number> = new Set(), curveMode: TimelineCurveMode = settings.keyframeCurve) {
if (!samples.length) return []
const fitted = curveMode === 'bezier'
const fitted = curveMode === 'bezier' && settings.spineVersion !== '3.8'
? reduceBezierSamples(samples, tolerances, forcedFrames)
: { samples: reduceNumericSamples(samples, tolerances, forcedFrames), controls: [] }
const keys = fitted.samples.map((sample) => {
@@ -212,7 +213,7 @@ function rgbaTimeline(colors: number[][], attachments: Array<string | null>, fps
// RGB 保持单通道 1/255,透明度使用 0.01 的视觉容差。
const samples: NumericSample[] = colors.map((values, frame) => ({ frame, values }))
const rgbTolerance = 1 / 255
const fitted = curveMode === 'bezier'
const fitted = curveMode === 'bezier' && settings.spineVersion !== '3.8'
? reduceBezierSamples(samples, [rgbTolerance, rgbTolerance, rgbTolerance, 0.01], forcedFrames)
: { samples: reduceNumericSamples(samples, [rgbTolerance, rgbTolerance, rgbTolerance, 0.01], forcedFrames), controls: [] }
const keys = fitted.samples.map((sample) => ({ time: time(sample.frame, fps), color: `${byte(sample.values[0])}${byte(sample.values[1])}${byte(sample.values[2])}${byte(sample.values[3])}` } as SpineJsonMap))
@@ -232,7 +233,9 @@ function rgbaTimeline(colors: number[][], attachments: Array<string | null>, fps
function addSlotAnimation(animation: SpineJsonMap, slotName: string, attachments: Array<string | null>, colors: number[][], fps: number, settings: SpineExportSettings, curveMode: TimelineCurveMode = settings.keyframeCurve) {
const slot = animation.slots[slotName] ||= {}
const attachment = discreteTimeline(attachments, fps, (name, frame) => name == null ? { time: time(frame, fps) } : { time: time(frame, fps), name })
// Spine 3.8 编辑器要求附件时间轴的每一帧都存在 name;隐藏附件必须显式写 null,
// 省略字段会在导入时触发 “Named value not found: name”。4.x 同样接受该标准写法。
const attachment = discreteTimeline(attachments, fps, (name, frame) => ({ time: time(frame, fps), name: name ?? null }))
if (attachment.length) slot.attachment = attachment
const rgba = rgbaTimeline(colors, attachments, fps, settings, curveMode)
if (rgba.length) slot.rgba = rgba
@@ -247,7 +250,8 @@ export function buildSpineJson(input: ExportInput): SpineExportResult {
const warnings: SpineExportWarning[] = []
const visibleSystems = input.systems.filter((system) => system.visible !== false)
const document: SpineJsonDocument = {
skeleton: { hash: '', spine: '4.2', x: 0, y: 0, width: 0, height: 0, images: settings.imagesPath || './images/' },
// 先生成当前运行时使用的公共结构;3.8 会在最终序列化前降级字段与时间轴格式。
skeleton: { hash: '', spine: settings.spineVersion, x: 0, y: 0, width: 0, height: 0, images: settings.imagesPath || './images/' },
bones: [{ name: 'root' }], slots: [], skins: [{ name: 'default', attachments: {} }], animations: {},
}
if (!settings.omitFps) document.skeleton.fps = targetFps
@@ -426,8 +430,9 @@ export function buildSpineJson(input: ExportInput): SpineExportResult {
}
if (!Object.keys(document.animations).length && !settings.excludeEmptyAnimations) document.animations.animation = { bones: {}, slots: {} }
const text = JSON.stringify(document, null, 2)
return { json: document, text, warnings, particleSystemCount: visibleSystems.length, animationCount: Object.keys(document.animations).length, boneCount: document.bones.length }
const output = settings.spineVersion === '3.8' ? downgradeSpineJsonTo38(document) : document
const text = JSON.stringify(output, null, 2)
return { json: output, text, warnings, particleSystemCount: visibleSystems.length, animationCount: Object.keys(output.animations).length, boneCount: output.bones.length }
}
export function downloadSpineJson(result: SpineExportResult, fileName: string) {
+48 -10
View File
@@ -9,6 +9,9 @@ import {
type AttachmentLoader,
type Skin,
} from '@esotericsoftware/spine-core'
import { normalizeSpine38JsonFor42 } from '../spine/spine38Compatibility'
import { normalizeSpineVersion, supportedSpineVersionsText, type SupportedSpineVersion } from '../spine/spineVersion'
import type { SpineJsonMap } from './spineJsonTypes'
class ExportValidationAttachmentLoader implements AttachmentLoader {
newRegionAttachment(_skin: Skin, name: string, path: string, _sequence: any) { return new RegionAttachment(name, path) }
@@ -59,12 +62,12 @@ function validateMesh(mesh: Record<string, any>, attachmentName: string, boneCou
if (offset !== vertices.length || decodedVertices !== vertexCount) throw new Error(`拖尾网格“${attachmentName}”的顶点数量与 UV 不一致`)
}
function validateEditorImportStructure(root: Record<string, any>) {
function validateEditorImportStructure(root: Record<string, any>, version: SupportedSpineVersion) {
const bones = Array.isArray(root.bones) ? root.bones : []
const boneByName = new Map(bones.map((bone) => [String(bone.name || ''), bone]))
const weightedMeshSlots = new Set<string>()
for (const bone of bones) {
if (bone.inherit != null && !VALID_INHERIT.has(bone.inherit)) throw new Error(`骨骼“${bone.name || ''}”的 inherit 值“${bone.inherit}”不符合 Spine 4.2 JSON 格式`)
if (bone.inherit != null && !VALID_INHERIT.has(bone.inherit)) throw new Error(`骨骼“${bone.name || ''}”的 inherit 值“${bone.inherit}”不符合 Spine ${version} JSON 格式`)
if (/_particle_\d+_trail_/.test(String(bone.name || ''))) {
const parent = boneByName.get(String(bone.parent || ''))
if (!parent || /_particle_\d+_trail_/.test(String(parent.name || ''))) {
@@ -94,12 +97,47 @@ function validateEditorImportStructure(root: Record<string, any>) {
}
}
/** 下载前使用项目内置 Spine 4.2 Runtime 做一次结构解析,提前拦截无效 JSON。 */
export function validateSpineJson(text: string) {
const root = JSON.parse(text)
validateEditorImportStructure(root)
const reader = new SkeletonJson(new ExportValidationAttachmentLoader())
const data = reader.readSkeletonData(root)
if (!data.bones.length) throw new Error('导出结果缺少骨骼')
return { bones: data.bones.length, slots: data.slots.length, animations: data.animations.length }
function validateSpine38Structure(root: Record<string, any>) {
if (String(root?.skeleton?.spine || '') !== '3.8') throw new Error('Spine 3.8 导出的版本标识必须为 3.8')
for (const bone of root.bones || []) {
if (bone.inherit != null) throw new Error(`Spine 3.8 骨骼“${bone.name || ''}”应使用 transform,不能使用 inherit`)
}
for (const animation of Object.values(root.animations || {}) as SpineJsonMap[]) {
for (const slot of Object.values(animation.slots || {}) as SpineJsonMap[]) {
if (slot.rgba || slot.rgba2) throw new Error('Spine 3.8 插槽颜色时间轴必须使用 color / twoColor')
for (const frame of slot.attachment || []) {
if (!Object.prototype.hasOwnProperty.call(frame, 'name')) throw new Error('Spine 3.8 附件时间轴的隐藏帧必须显式写入 name: null')
}
}
for (const bone of Object.values(animation.bones || {}) as SpineJsonMap[]) {
for (const frame of bone.rotate || []) {
if (frame.value != null || frame.angle == null) throw new Error('Spine 3.8 旋转时间轴必须使用 angle')
}
}
const stack: unknown[] = [animation]
while (stack.length) {
const value = stack.pop()
if (!value || typeof value !== 'object') continue
if (!Array.isArray(value) && Array.isArray((value as SpineJsonMap).curve)) throw new Error('Spine 3.8 不支持 4.x 数组曲线格式')
stack.push(...Object.values(value))
}
}
}
/**
* 使 JSON
* skeleton.spine
*/
export function validateSpineJson(text: string, expectedVersion?: SupportedSpineVersion) {
const root = JSON.parse(text)
const version = normalizeSpineVersion(root?.skeleton?.spine)
if (!version) throw new Error(`导出版本不受支持,仅支持 Spine ${supportedSpineVersionsText()}`)
if (expectedVersion && version !== expectedVersion) throw new Error(`导出版本不一致:设置为 Spine ${expectedVersion}JSON 标记为 Spine ${version}`)
validateEditorImportStructure(root, version)
if (version === '3.8') validateSpine38Structure(root)
const runtimeRoot = version === '3.8' ? normalizeSpine38JsonFor42(JSON.parse(JSON.stringify(root))) : root
const reader = new SkeletonJson(new ExportValidationAttachmentLoader())
const data = reader.readSkeletonData(runtimeRoot)
if (!data.bones.length) throw new Error('导出结果缺少骨骼')
return { version, bones: data.bones.length, slots: data.slots.length, animations: data.animations.length }
}
+7 -1
View File
@@ -38,7 +38,10 @@
</button>
<button class="clear" @click="clearBundle">清除</button>
</div>
<div class="runtime-note">Spine 4.2.119 · 官方 PixiJS 7 运行时 · 本地离线</div>
<div class="runtime-note">
支持 Spine 3.8 / 4.0 / 4.1 / 4.2 JSON · 4.2.119 预览运行时 · 本地离线
<span v-if="spineObject.dataVersion"> · 当前资源 {{ spineObject.dataVersion }}</span>
</div>
<div v-if="spineObject.error" class="error">{{ spineObject.error }}</div>
<div class="section-box">
@@ -125,6 +128,7 @@ const textureFiles = ref<File[]>([])
//
if (props.spineObject.selectedBone == null) props.spineObject.selectedBone = ''
if (props.spineObject.sourceBundle == null) props.spineObject.sourceBundle = null
if (props.spineObject.dataVersion == null) props.spineObject.dataVersion = ''
const skeletonName = computed(() => skeletonFile.value?.name || props.spineObject.skeletonFileName)
const atlasName = computed(() => atlasFile.value?.name || props.spineObject.atlasFileName)
@@ -221,6 +225,7 @@ async function loadBundle() {
props.spineObject.skeletonFileName = skeleton.name
props.spineObject.atlasFileName = atlas.name
props.spineObject.textureFileNames = textureFiles.value.map((file) => file.name)
props.spineObject.dataVersion = parsed.dataVersion
props.spineObject.animations = parsed.animations
props.spineObject.bones = parsed.bones
props.spineObject.selectedBone = ''
@@ -257,6 +262,7 @@ function clearBundle() {
props.spineObject.skeletonFileName = ''
props.spineObject.atlasFileName = ''
props.spineObject.textureFileNames = []
props.spineObject.dataVersion = ''
props.spineObject.sourceBundle = null
props.spineObject.animations = []
props.spineObject.bones = []
+219
View File
@@ -0,0 +1,219 @@
type JsonRecord = Record<string, any>
function valueOf(frame: JsonRecord, key: string, fallback: number) {
const value = Number(frame[key])
return Number.isFinite(value) ? value : fallback
}
function setRenamedValue(frame: JsonRecord, target: string, source: string, fallback: number) {
if (frame[target] == null) frame[target] = valueOf(frame, source, fallback)
}
function legacyCurveToArray(
frame: JsonRecord,
next: JsonRecord,
values: Array<[number, number]>,
) {
if (typeof frame.curve !== 'number') return
const time1 = valueOf(frame, 'time', 0)
const time2 = valueOf(next, 'time', 0)
const timeRange = time2 - time1
const x1 = frame.curve
const y1 = valueOf(frame, 'c2', 0)
const x2 = valueOf(frame, 'c3', 1)
const y2 = valueOf(frame, 'c4', 1)
frame.curve = values.flatMap(([start, end]) => [
time1 + timeRange * x1,
start + (end - start) * y1,
time1 + timeRange * x2,
start + (end - start) * y2,
])
delete frame.c2
delete frame.c3
delete frame.c4
}
function convertTimelineCurves(
frames: JsonRecord[] | undefined,
channels: Array<{ key: string; fallback: number }>,
) {
if (!Array.isArray(frames)) return
for (let index = 0; index < frames.length - 1; index++) {
const frame = frames[index]
const next = frames[index + 1]
legacyCurveToArray(frame, next, channels.map(({ key, fallback }) => [
valueOf(frame, key, fallback),
valueOf(next, key, fallback),
]))
}
}
function convertNormalizedTimelineCurves(frames: JsonRecord[] | undefined) {
if (!Array.isArray(frames)) return
for (let index = 0; index < frames.length - 1; index++) {
legacyCurveToArray(frames[index], frames[index + 1], [[0, 1]])
}
}
function colorChannels(value: unknown, includeAlpha: boolean) {
const input = typeof value === 'string' ? value.replace(/^#/, '') : ''
const normalized = input.length === 6 ? `${input}ff` : input.padEnd(8, 'f').slice(0, 8)
const channels = [0, 2, 4, 6].map((offset) => Number.parseInt(normalized.slice(offset, offset + 2), 16) / 255)
return includeAlpha ? channels : channels.slice(0, 3)
}
function convertColorTimelineCurves(frames: JsonRecord[] | undefined, twoColor: boolean) {
if (!Array.isArray(frames)) return
for (let index = 0; index < frames.length - 1; index++) {
const frame = frames[index]
const next = frames[index + 1]
const start = twoColor
? [...colorChannels(frame.light, true), ...colorChannels(frame.dark, false)]
: colorChannels(frame.color, true)
const end = twoColor
? [...colorChannels(next.light, true), ...colorChannels(next.dark, false)]
: colorChannels(next.color, true)
legacyCurveToArray(frame, next, start.map((value, channel) => [value, end[channel]]))
}
}
function normalizeConstraintSetup(root: JsonRecord) {
for (const bone of root.bones || []) {
if (bone.inherit == null && bone.transform != null) bone.inherit = bone.transform
}
for (const constraint of root.transform || []) {
setRenamedValue(constraint, 'mixRotate', 'rotateMix', 1)
setRenamedValue(constraint, 'mixX', 'translateMix', 1)
if (constraint.mixY == null) constraint.mixY = constraint.mixX
setRenamedValue(constraint, 'mixScaleX', 'scaleMix', 1)
if (constraint.mixScaleY == null) constraint.mixScaleY = constraint.mixScaleX
setRenamedValue(constraint, 'mixShearY', 'shearMix', 1)
}
for (const constraint of root.path || []) {
setRenamedValue(constraint, 'mixRotate', 'rotateMix', 1)
setRenamedValue(constraint, 'mixX', 'translateMix', 1)
if (constraint.mixY == null) constraint.mixY = constraint.mixX
}
}
function normalizeLinkedMeshes(root: JsonRecord) {
for (const skin of root.skins || []) {
for (const slot of Object.values<JsonRecord>(skin.attachments || {})) {
for (const attachment of Object.values<JsonRecord>(slot || {})) {
if (attachment.parent != null && attachment.timelines == null && attachment.deform != null) {
attachment.timelines = attachment.deform
}
}
}
}
}
function normalizeSlotTimelines(animation: JsonRecord) {
for (const slot of Object.values<JsonRecord>(animation.slots || {})) {
if (slot.color && !slot.rgba) {
slot.rgba = slot.color
delete slot.color
}
if (slot.twoColor && !slot.rgba2) {
slot.rgba2 = slot.twoColor
delete slot.twoColor
}
convertColorTimelineCurves(slot.rgba, false)
convertColorTimelineCurves(slot.rgba2, true)
}
}
function normalizeBoneTimelines(animation: JsonRecord) {
for (const bone of Object.values<JsonRecord>(animation.bones || {})) {
for (const frame of bone.rotate || []) setRenamedValue(frame, 'value', 'angle', 0)
convertTimelineCurves(bone.rotate, [{ key: 'value', fallback: 0 }])
convertTimelineCurves(bone.translate, [{ key: 'x', fallback: 0 }, { key: 'y', fallback: 0 }])
convertTimelineCurves(bone.scale, [{ key: 'x', fallback: 1 }, { key: 'y', fallback: 1 }])
convertTimelineCurves(bone.shear, [{ key: 'x', fallback: 0 }, { key: 'y', fallback: 0 }])
}
}
function normalizeIkTimelines(animation: JsonRecord) {
for (const frames of Object.values<JsonRecord[]>(animation.ik || {})) {
convertTimelineCurves(frames, [{ key: 'mix', fallback: 1 }, { key: 'softness', fallback: 0 }])
}
}
function normalizeTransformTimelines(animation: JsonRecord) {
for (const frames of Object.values<JsonRecord[]>(animation.transform || {})) {
for (const frame of frames) {
setRenamedValue(frame, 'mixRotate', 'rotateMix', 1)
setRenamedValue(frame, 'mixX', 'translateMix', 1)
if (frame.mixY == null) frame.mixY = frame.mixX
setRenamedValue(frame, 'mixScaleX', 'scaleMix', 1)
if (frame.mixScaleY == null) frame.mixScaleY = frame.mixScaleX
setRenamedValue(frame, 'mixShearY', 'shearMix', 1)
}
convertTimelineCurves(frames, [
{ key: 'mixRotate', fallback: 1 },
{ key: 'mixX', fallback: 1 },
{ key: 'mixY', fallback: 1 },
{ key: 'mixScaleX', fallback: 1 },
{ key: 'mixScaleY', fallback: 1 },
{ key: 'mixShearY', fallback: 1 },
])
}
}
function normalizePathTimelines(animation: JsonRecord) {
for (const constraint of Object.values<JsonRecord>(animation.path || {})) {
for (const timelineName of ['position', 'spacing'] as const) {
const frames = constraint[timelineName] as JsonRecord[] | undefined
for (const frame of frames || []) setRenamedValue(frame, 'value', timelineName, 0)
convertTimelineCurves(frames, [{ key: 'value', fallback: 0 }])
}
const mixFrames = constraint.mix as JsonRecord[] | undefined
for (const frame of mixFrames || []) {
setRenamedValue(frame, 'mixRotate', 'rotateMix', 1)
setRenamedValue(frame, 'mixX', 'translateMix', 1)
if (frame.mixY == null) frame.mixY = frame.mixX
}
convertTimelineCurves(mixFrames, [
{ key: 'mixRotate', fallback: 1 },
{ key: 'mixX', fallback: 1 },
{ key: 'mixY', fallback: 1 },
])
}
}
function normalizeDeformTimelines(animation: JsonRecord) {
if (!animation.deform) return
const attachments = animation.attachments || (animation.attachments = {})
for (const [skinName, skin] of Object.entries<JsonRecord>(animation.deform)) {
const targetSkin = attachments[skinName] || (attachments[skinName] = {})
for (const [slotName, slot] of Object.entries<JsonRecord>(skin)) {
const targetSlot = targetSkin[slotName] || (targetSkin[slotName] = {})
for (const [attachmentName, frames] of Object.entries<JsonRecord[]>(slot)) {
const targetAttachment = targetSlot[attachmentName] || (targetSlot[attachmentName] = {})
targetAttachment.deform = frames
convertNormalizedTimelineCurves(frames)
}
}
}
delete animation.deform
}
/**
* Spine 3.8 线 4.x 使
* Spine 4.2 Runtime 3.8 JSON 4.2 Runtime
* skeleton.spine
*/
export function normalizeSpine38JsonFor42(source: JsonRecord) {
normalizeConstraintSetup(source)
normalizeLinkedMeshes(source)
for (const animation of Object.values<JsonRecord>(source.animations || {})) {
normalizeSlotTimelines(animation)
normalizeBoneTimelines(animation)
normalizeIkTimelines(animation)
normalizeTransformTimelines(animation)
normalizePathTimelines(animation)
normalizeDeformTimelines(animation)
if (!animation.drawOrder && animation.draworder) animation.drawOrder = animation.draworder
}
return source
}
+17 -1
View File
@@ -9,10 +9,13 @@ import { SpineTexture } from '@esotericsoftware/spine-pixi-v7'
import { ALPHA_MODES, Texture } from 'pixi.js'
import type { LoadedSpineAsset } from './spineAssetRegistry'
import type { SpineAnimationInfo, SpineBoneInfo } from './spineTypes'
import { normalizeSpine38JsonFor42 } from './spine38Compatibility'
import { normalizeSpineVersion, supportedSpineVersionsText, type SupportedSpineVersion } from './spineVersion'
export interface ParsedSpineBundle extends LoadedSpineAsset {
animations: SpineAnimationInfo[]
bones: SpineBoneInfo[]
dataVersion: SupportedSpineVersion
}
function basename(path: string) {
@@ -56,17 +59,30 @@ export async function parseSpineBundle(skeletonFile: File, atlasFile: File, text
const attachmentLoader = new AtlasAttachmentLoader(atlas)
let skeletonData: SkeletonData
let dataVersion: SupportedSpineVersion | null = null
if (/\.skel$/i.test(skeletonFile.name)) {
const reader = new SkeletonBinary(attachmentLoader)
skeletonData = reader.readSkeletonData(await skeletonFile.arrayBuffer())
dataVersion = normalizeSpineVersion(skeletonData.version)
} else {
let source: any
try {
source = JSON.parse(await skeletonFile.text())
} catch {
throw new Error('Spine JSON 文件格式无效')
}
dataVersion = normalizeSpineVersion(source?.skeleton?.spine)
if (!dataVersion) throw new Error(`Spine JSON 版本不受支持,仅支持 ${supportedSpineVersionsText()}`)
if (dataVersion === '3.8') normalizeSpine38JsonFor42(source)
const reader = new SkeletonJson(attachmentLoader)
skeletonData = reader.readSkeletonData(await skeletonFile.text())
skeletonData = reader.readSkeletonData(source)
}
if (!dataVersion) throw new Error(`Spine 数据版本不受支持,仅支持 ${supportedSpineVersionsText()}`)
return {
skeletonData,
textures,
dataVersion,
animations: skeletonData.animations.map((animation) => ({ name: animation.name, duration: animation.duration })),
bones: boneMetadata(skeletonData),
}
+3
View File
@@ -1,4 +1,5 @@
import type { SpineSourceBundle } from './spineSourceBundle'
import type { SupportedSpineVersion } from './spineVersion'
export interface SpineAnimationInfo {
name: string
@@ -25,6 +26,8 @@ export interface SpineSceneObject {
skeletonFileName: string
atlasFileName: string
textureFileNames: string[]
/** 当前已加载资源的 Spine 主版本。 */
dataVersion: SupportedSpineVersion | ''
/** 保存配置时使用的可移植原始 Spine 文件。 */
sourceBundle: SpineSourceBundle | null
selectedAnimation: string
+16
View File
@@ -0,0 +1,16 @@
export const SUPPORTED_SPINE_VERSIONS = ['3.8', '4.0', '4.1', '4.2'] as const
export type SupportedSpineVersion = typeof SUPPORTED_SPINE_VERSIONS[number]
export function normalizeSpineVersion(value: unknown): SupportedSpineVersion | null {
const version = String(value || '').trim()
return SUPPORTED_SPINE_VERSIONS.find((candidate) => version === candidate || version.startsWith(`${candidate}.`)) || null
}
export function isSupportedSpineVersion(value: unknown): value is SupportedSpineVersion {
return normalizeSpineVersion(value) !== null
}
export function supportedSpineVersionsText() {
return SUPPORTED_SPINE_VERSIONS.join(' / ')
}
+1
View File
@@ -314,6 +314,7 @@ export const useParticleStore = defineStore('particle', {
skeletonFileName: '',
atlasFileName: '',
textureFileNames: [],
dataVersion: '',
sourceBundle: null,
selectedAnimation: '',
selectedBone: '',
+7 -3
View File
@@ -828,6 +828,9 @@
<label class="export-field">
<span>Spine 版本</span>
<select v-model="exportSettings.spineVersion" class="inp export-input">
<option value="3.8">Spine 3.8</option>
<option value="4.0">Spine 4.0</option>
<option value="4.1">Spine 4.1</option>
<option value="4.2">Spine 4.2</option>
</select>
</label>
@@ -840,6 +843,7 @@
</select>
</label>
<div class="export-help">平滑贝塞尔会在误差允许范围内拟合连续运动出生死亡附件切换和突变仍使用阶跃关键帧</div>
<div v-if="exportSettings.spineVersion === '3.8'" class="export-help">Spine 3.8 不支持 4.x 的逐通道数组贝塞尔导出时会转换为经过容差精简的分段线性关键帧</div>
<div class="export-fps-row">
<label class="export-field export-fps-field">
@@ -869,7 +873,7 @@
</template>
</div>
<button type="button" class="export-spine-json" :disabled="exportBusy" :title="exportBusy ? '正在重新计算并烘焙动画' : '导出可重新导入 Spine 4.2 的 JSON'" @click="exportSpineJson">
<button type="button" class="export-spine-json" :disabled="exportBusy" :title="exportBusy ? '正在重新计算并烘焙动画' : `导出 Spine ${exportSettings.spineVersion} JSON`" @click="exportSpineJson">
{{ exportBusy ? '正在准备导出…' : '导出Spine Json' }}
</button>
<div v-if="exportMessage" class="export-message" :class="{ error: exportError }">{{ exportMessage }}</div>
@@ -1000,10 +1004,10 @@ async function exportSpineJson() {
try {
await prepareExportFrames()
const result = buildSpineJson({ systems: store.systems as unknown as ParticleSystem[], timeline: store.timeline, settings: exportSettings.value })
validateSpineJson(result.text)
validateSpineJson(result.text, exportSettings.value.spineVersion)
downloadSpineJson(result, exportSettings.value.fileName)
const warning = result.warnings.length ? `${result.warnings.length} 条资源提示(缺图仍已导出)` : ''
exportMessage.value = `已导出 ${result.particleSystemCount} 个粒子系统、${result.animationCount} 个动画、${result.boneCount} 根骨骼${warning}`
exportMessage.value = `已导出 Spine ${exportSettings.value.spineVersion}${result.particleSystemCount} 个粒子系统、${result.animationCount} 个动画、${result.boneCount} 根骨骼${warning}`
} catch (error) {
exportError.value = true
exportMessage.value = error instanceof Error ? error.message : String(error)
+1 -1
View File
@@ -125,7 +125,7 @@
<li><b>创建对象</b>在左侧场景对象中添加粒子碰撞路径或 Spine</li>
<li><b>编辑粒子</b>选中粒子系统依次调整发射形状属性外观资源和修改器</li>
<li><b>查看效果</b>使用底部时间轴播放暂停或拖动到指定帧修改参数后会自动重新计算</li>
<li><b>保存成果</b>配置文件用于继续编辑导出 Spine Json用于导入 Spine 4.2</li>
<li><b>保存成果</b>配置文件用于继续编辑导出 Spine Json支持 Spine 3.84.04.1 4.2</li>
</ol>
</section>