@flighthq/spritesheet-formats 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/asepriteParse.d.ts +18 -0
- package/dist/asepriteParse.d.ts.map +1 -0
- package/dist/asepriteParse.js +87 -0
- package/dist/asepriteParse.js.map +1 -0
- package/dist/asepriteSchema.d.ts +64 -0
- package/dist/asepriteSchema.d.ts.map +1 -0
- package/dist/asepriteSchema.js +6 -0
- package/dist/asepriteSchema.js.map +1 -0
- package/dist/asepriteSerialize.d.ts +14 -0
- package/dist/asepriteSerialize.d.ts.map +1 -0
- package/dist/asepriteSerialize.js +86 -0
- package/dist/asepriteSerialize.js.map +1 -0
- package/dist/cocosPlistParse.d.ts +15 -0
- package/dist/cocosPlistParse.d.ts.map +1 -0
- package/dist/cocosPlistParse.js +157 -0
- package/dist/cocosPlistParse.js.map +1 -0
- package/dist/cocosPlistSchema.d.ts +31 -0
- package/dist/cocosPlistSchema.d.ts.map +1 -0
- package/dist/cocosPlistSchema.js +6 -0
- package/dist/cocosPlistSchema.js.map +1 -0
- package/dist/cocosPlistSerialize.d.ts +8 -0
- package/dist/cocosPlistSerialize.d.ts.map +1 -0
- package/dist/cocosPlistSerialize.js +86 -0
- package/dist/cocosPlistSerialize.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/libgdxAtlasParse.d.ts +13 -0
- package/dist/libgdxAtlasParse.d.ts.map +1 -0
- package/dist/libgdxAtlasParse.js +209 -0
- package/dist/libgdxAtlasParse.js.map +1 -0
- package/dist/libgdxAtlasSchema.d.ts +38 -0
- package/dist/libgdxAtlasSchema.d.ts.map +1 -0
- package/dist/libgdxAtlasSchema.js +6 -0
- package/dist/libgdxAtlasSchema.js.map +1 -0
- package/dist/spritesheetDetect.d.ts +41 -0
- package/dist/spritesheetDetect.d.ts.map +1 -0
- package/dist/spritesheetDetect.js +101 -0
- package/dist/spritesheetDetect.js.map +1 -0
- package/dist/starlingParse.d.ts +20 -0
- package/dist/starlingParse.d.ts.map +1 -0
- package/dist/starlingParse.js +138 -0
- package/dist/starlingParse.js.map +1 -0
- package/dist/starlingSchema.d.ts +32 -0
- package/dist/starlingSchema.d.ts.map +1 -0
- package/dist/starlingSchema.js +6 -0
- package/dist/starlingSchema.js.map +1 -0
- package/dist/starlingSerialize.d.ts +8 -0
- package/dist/starlingSerialize.d.ts.map +1 -0
- package/dist/starlingSerialize.js +67 -0
- package/dist/starlingSerialize.js.map +1 -0
- package/dist/texturePackerParse.d.ts +15 -0
- package/dist/texturePackerParse.d.ts.map +1 -0
- package/dist/texturePackerParse.js +70 -0
- package/dist/texturePackerParse.js.map +1 -0
- package/dist/texturePackerSchema.d.ts +52 -0
- package/dist/texturePackerSchema.d.ts.map +1 -0
- package/dist/texturePackerSchema.js +5 -0
- package/dist/texturePackerSchema.js.map +1 -0
- package/dist/texturePackerSerialize.d.ts +14 -0
- package/dist/texturePackerSerialize.d.ts.map +1 -0
- package/dist/texturePackerSerialize.js +74 -0
- package/dist/texturePackerSerialize.js.map +1 -0
- package/package.json +41 -0
- package/src/asepriteParse.test.ts +235 -0
- package/src/asepriteSerialize.test.ts +218 -0
- package/src/cocosPlistParse.test.ts +131 -0
- package/src/cocosPlistSerialize.test.ts +56 -0
- package/src/libgdxAtlasParse.test.ts +100 -0
- package/src/spritesheetDetect.test.ts +217 -0
- package/src/starlingParse.test.ts +176 -0
- package/src/starlingSerialize.test.ts +117 -0
- package/src/texturePackerParse.test.ts +237 -0
- package/src/texturePackerSerialize.test.ts +200 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { parseLibgdxAtlasSpritesheet } from './libgdxAtlasParse';
|
|
2
|
+
|
|
3
|
+
const MINIMAL_ATLAS = `
|
|
4
|
+
atlas.png
|
|
5
|
+
size: 128, 64
|
|
6
|
+
format: RGBA8888
|
|
7
|
+
filter: Linear, Linear
|
|
8
|
+
repeat: none
|
|
9
|
+
hero
|
|
10
|
+
rotate: false
|
|
11
|
+
xy: 0, 0
|
|
12
|
+
size: 64, 64
|
|
13
|
+
orig: 64, 64
|
|
14
|
+
offset: 0, 0
|
|
15
|
+
index: -1
|
|
16
|
+
coin
|
|
17
|
+
rotate: false
|
|
18
|
+
xy: 64, 0
|
|
19
|
+
size: 32, 32
|
|
20
|
+
orig: 32, 32
|
|
21
|
+
offset: 0, 0
|
|
22
|
+
index: -1
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
// Animations are inferred from the `baseName_NNN` frame-naming convention.
|
|
26
|
+
const ANIMATED_ATLAS = `
|
|
27
|
+
run.png
|
|
28
|
+
size: 256, 64
|
|
29
|
+
format: RGBA8888
|
|
30
|
+
filter: Nearest, Nearest
|
|
31
|
+
repeat: none
|
|
32
|
+
run_0
|
|
33
|
+
rotate: false
|
|
34
|
+
xy: 0, 0
|
|
35
|
+
size: 64, 64
|
|
36
|
+
orig: 64, 64
|
|
37
|
+
offset: 0, 0
|
|
38
|
+
index: -1
|
|
39
|
+
run_1
|
|
40
|
+
rotate: false
|
|
41
|
+
xy: 64, 0
|
|
42
|
+
size: 64, 64
|
|
43
|
+
orig: 64, 64
|
|
44
|
+
offset: 0, 0
|
|
45
|
+
index: -1
|
|
46
|
+
run_2
|
|
47
|
+
rotate: false
|
|
48
|
+
xy: 128, 0
|
|
49
|
+
size: 64, 64
|
|
50
|
+
orig: 64, 64
|
|
51
|
+
offset: 0, 0
|
|
52
|
+
index: -1
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
describe('parseLibgdxAtlasSpritesheet', () => {
|
|
56
|
+
it('parses frame names', () => {
|
|
57
|
+
const data = parseLibgdxAtlasSpritesheet(MINIMAL_ATLAS);
|
|
58
|
+
expect(data.frames).toHaveLength(2);
|
|
59
|
+
expect(data.frames[0]?.name).toBe('hero');
|
|
60
|
+
expect(data.frames[1]?.name).toBe('coin');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('parses frame positions and dimensions', () => {
|
|
64
|
+
const data = parseLibgdxAtlasSpritesheet(MINIMAL_ATLAS);
|
|
65
|
+
expect(data.frames[0]?.x).toBe(0);
|
|
66
|
+
expect(data.frames[0]?.y).toBe(0);
|
|
67
|
+
expect(data.frames[0]?.width).toBe(64);
|
|
68
|
+
expect(data.frames[0]?.height).toBe(64);
|
|
69
|
+
expect(data.frames[1]?.x).toBe(64);
|
|
70
|
+
expect(data.frames[1]?.y).toBe(0);
|
|
71
|
+
expect(data.frames[1]?.width).toBe(32);
|
|
72
|
+
expect(data.frames[1]?.height).toBe(32);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('parses page image file and dimensions', () => {
|
|
76
|
+
const data = parseLibgdxAtlasSpritesheet(MINIMAL_ATLAS);
|
|
77
|
+
expect(data.imageFile).toBe('atlas.png');
|
|
78
|
+
expect(data.imageWidth).toBe(128);
|
|
79
|
+
expect(data.imageHeight).toBe(64);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('infers animations from baseName_NNN frame names', () => {
|
|
83
|
+
const data = parseLibgdxAtlasSpritesheet(ANIMATED_ATLAS);
|
|
84
|
+
expect(data.frames).toHaveLength(3);
|
|
85
|
+
expect(data.frames[0]?.name).toBe('run_0');
|
|
86
|
+
expect(data.animations).toHaveLength(1);
|
|
87
|
+
expect(data.animations[0]?.name).toBe('run');
|
|
88
|
+
expect(data.animations[0]?.frameNames).toEqual(['run_0', 'run_1', 'run_2']);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('respects custom frameDuration option', () => {
|
|
92
|
+
const data = parseLibgdxAtlasSpritesheet(ANIMATED_ATLAS, { frameDuration: 200 });
|
|
93
|
+
expect(data.animations[0]?.frameDuration).toBe(200);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('sets non-indexed frames without rotation', () => {
|
|
97
|
+
const data = parseLibgdxAtlasSpritesheet(MINIMAL_ATLAS);
|
|
98
|
+
expect(data.frames[0]?.rotated).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SpritesheetFormatKindAseprite,
|
|
3
|
+
SpritesheetFormatKindCocosPlist,
|
|
4
|
+
SpritesheetFormatKindLibgdxAtlas,
|
|
5
|
+
SpritesheetFormatKindStarling,
|
|
6
|
+
SpritesheetFormatKindTexturePacker,
|
|
7
|
+
} from '@flighthq/types';
|
|
8
|
+
|
|
9
|
+
import type { SpritesheetParseOptions } from './spritesheetDetect';
|
|
10
|
+
import {
|
|
11
|
+
detectSpritesheetFormat,
|
|
12
|
+
getSpritesheetFormat,
|
|
13
|
+
parseSpritesheet,
|
|
14
|
+
registerSpritesheetFormat,
|
|
15
|
+
} from './spritesheetDetect';
|
|
16
|
+
|
|
17
|
+
const TEXTURE_PACKER_JSON = JSON.stringify({
|
|
18
|
+
frames: {},
|
|
19
|
+
meta: {
|
|
20
|
+
app: 'https://www.codeandweb.com/texturepacker',
|
|
21
|
+
format: 'RGBA8888',
|
|
22
|
+
image: 'atlas.png',
|
|
23
|
+
scale: 1,
|
|
24
|
+
size: { h: 128, w: 128 },
|
|
25
|
+
version: '1.0',
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const ASEPRITE_JSON = JSON.stringify({
|
|
30
|
+
frames: {},
|
|
31
|
+
meta: {
|
|
32
|
+
app: 'https://www.aseprite.org/',
|
|
33
|
+
format: 'RGBA8888',
|
|
34
|
+
frameTags: [],
|
|
35
|
+
image: 'sprite.png',
|
|
36
|
+
scale: '1',
|
|
37
|
+
size: { h: 64, w: 64 },
|
|
38
|
+
version: '1.3',
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const STARLING_XML = `<?xml version="1.0" encoding="UTF-8"?>
|
|
43
|
+
<TextureAtlas imagePath="atlas.png">
|
|
44
|
+
<SubTexture name="hero" x="0" y="0" width="64" height="64"/>
|
|
45
|
+
</TextureAtlas>`;
|
|
46
|
+
|
|
47
|
+
const LIBGDX_ATLAS = `
|
|
48
|
+
atlas.png
|
|
49
|
+
size: 128, 64
|
|
50
|
+
format: RGBA8888
|
|
51
|
+
filter: Linear, Linear
|
|
52
|
+
repeat: none
|
|
53
|
+
hero
|
|
54
|
+
rotate: false
|
|
55
|
+
xy: 0, 0
|
|
56
|
+
size: 64, 64
|
|
57
|
+
orig: 64, 64
|
|
58
|
+
offset: 0, 0
|
|
59
|
+
index: -1
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
const COCOS_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
|
|
63
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
64
|
+
<plist version="1.0">
|
|
65
|
+
<dict>
|
|
66
|
+
<key>frames</key>
|
|
67
|
+
<dict>
|
|
68
|
+
<key>hero.png</key>
|
|
69
|
+
<dict>
|
|
70
|
+
<key>textureRect</key>
|
|
71
|
+
<string>{{0,0},{64,64}}</string>
|
|
72
|
+
</dict>
|
|
73
|
+
</dict>
|
|
74
|
+
<key>metadata</key>
|
|
75
|
+
<dict>
|
|
76
|
+
<key>textureFileName</key>
|
|
77
|
+
<string>atlas.png</string>
|
|
78
|
+
</dict>
|
|
79
|
+
</dict>
|
|
80
|
+
</plist>`;
|
|
81
|
+
|
|
82
|
+
describe('detectSpritesheetFormat', () => {
|
|
83
|
+
it('detects TexturePacker JSON', () => {
|
|
84
|
+
expect(detectSpritesheetFormat(TEXTURE_PACKER_JSON)).toBe(SpritesheetFormatKindTexturePacker);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('detects Aseprite JSON', () => {
|
|
88
|
+
expect(detectSpritesheetFormat(ASEPRITE_JSON)).toBe(SpritesheetFormatKindAseprite);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('detects Starling XML', () => {
|
|
92
|
+
expect(detectSpritesheetFormat(STARLING_XML)).toBe(SpritesheetFormatKindStarling);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('detects Cocos plist XML', () => {
|
|
96
|
+
expect(detectSpritesheetFormat(COCOS_PLIST)).toBe(SpritesheetFormatKindCocosPlist);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('detects libGDX atlas', () => {
|
|
100
|
+
expect(detectSpritesheetFormat(LIBGDX_ATLAS)).toBe(SpritesheetFormatKindLibgdxAtlas);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('returns null for unknown format', () => {
|
|
104
|
+
expect(detectSpritesheetFormat('completely unknown content')).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('getSpritesheetFormat', () => {
|
|
109
|
+
it('returns the registered entry for a known kind', () => {
|
|
110
|
+
const entry = getSpritesheetFormat(SpritesheetFormatKindTexturePacker);
|
|
111
|
+
expect(entry).not.toBeNull();
|
|
112
|
+
expect(typeof entry?.detect).toBe('function');
|
|
113
|
+
expect(typeof entry?.parse).toBe('function');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('returns null for an unknown kind', () => {
|
|
117
|
+
expect(getSpritesheetFormat('acme.NonExistent')).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('reflects a format registered via registerSpritesheetFormat', () => {
|
|
121
|
+
const kind = 'test.GetFormatTest';
|
|
122
|
+
const detect = (text: string) => text.startsWith('GFT:');
|
|
123
|
+
registerSpritesheetFormat(kind, {
|
|
124
|
+
detect,
|
|
125
|
+
parse: () => ({ animations: [], frames: [], imageFile: '', imageHeight: 0, imageWidth: 0, scale: 1 }),
|
|
126
|
+
});
|
|
127
|
+
const entry = getSpritesheetFormat(kind);
|
|
128
|
+
expect(entry).not.toBeNull();
|
|
129
|
+
expect(entry?.detect).toBe(detect);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('parseSpritesheet', () => {
|
|
134
|
+
it('auto-detects and parses TexturePacker JSON', () => {
|
|
135
|
+
const data = parseSpritesheet(TEXTURE_PACKER_JSON);
|
|
136
|
+
expect(data).not.toBeNull();
|
|
137
|
+
expect(data?.imageFile).toBe('atlas.png');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('auto-detects and parses Aseprite JSON', () => {
|
|
141
|
+
const data = parseSpritesheet(ASEPRITE_JSON);
|
|
142
|
+
expect(data).not.toBeNull();
|
|
143
|
+
expect(data?.imageFile).toBe('sprite.png');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('auto-detects and parses Starling XML', () => {
|
|
147
|
+
const data = parseSpritesheet(STARLING_XML);
|
|
148
|
+
expect(data).not.toBeNull();
|
|
149
|
+
expect(data?.frames[0]?.name).toBe('hero');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('returns null for unknown format', () => {
|
|
153
|
+
const data = parseSpritesheet('unknown format content here');
|
|
154
|
+
expect(data).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('respects explicit formatKind override', () => {
|
|
158
|
+
const data = parseSpritesheet(TEXTURE_PACKER_JSON, SpritesheetFormatKindTexturePacker);
|
|
159
|
+
expect(data).not.toBeNull();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('passes frameDuration option to parsers', () => {
|
|
163
|
+
const json = JSON.stringify({
|
|
164
|
+
frames: {},
|
|
165
|
+
meta: {
|
|
166
|
+
app: 'https://www.codeandweb.com/texturepacker',
|
|
167
|
+
format: 'RGBA8888',
|
|
168
|
+
frameTags: [{ direction: 'forward', from: 0, name: 'run', to: 0 }],
|
|
169
|
+
image: 'atlas.png',
|
|
170
|
+
scale: 1,
|
|
171
|
+
size: { h: 64, w: 64 },
|
|
172
|
+
version: '1.0',
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
const opts: SpritesheetParseOptions = { frameDuration: 250 };
|
|
176
|
+
const data = parseSpritesheet(json, SpritesheetFormatKindTexturePacker, opts);
|
|
177
|
+
// frameTags reference frame index 0 which doesn't exist → animation gets empty frameNames
|
|
178
|
+
// But the option is passed without throwing
|
|
179
|
+
expect(data).not.toBeNull();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('registerSpritesheetFormat', () => {
|
|
184
|
+
it('allows registering a custom format', () => {
|
|
185
|
+
const customKind = 'test.CustomFormat';
|
|
186
|
+
registerSpritesheetFormat(customKind, {
|
|
187
|
+
detect: (text) => text.startsWith('CUSTOM:'),
|
|
188
|
+
parse: () => ({
|
|
189
|
+
animations: [],
|
|
190
|
+
frames: [
|
|
191
|
+
{
|
|
192
|
+
height: 10,
|
|
193
|
+
name: 'custom',
|
|
194
|
+
offsetX: 0,
|
|
195
|
+
offsetY: 0,
|
|
196
|
+
pivotX: null,
|
|
197
|
+
pivotY: null,
|
|
198
|
+
rotated: false,
|
|
199
|
+
sourceHeight: 10,
|
|
200
|
+
sourceWidth: 10,
|
|
201
|
+
width: 10,
|
|
202
|
+
x: 0,
|
|
203
|
+
y: 0,
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
imageFile: 'custom.png',
|
|
207
|
+
imageHeight: 10,
|
|
208
|
+
imageWidth: 10,
|
|
209
|
+
scale: 1,
|
|
210
|
+
}),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(detectSpritesheetFormat('CUSTOM: data here')).toBe(customKind);
|
|
214
|
+
const data = parseSpritesheet('CUSTOM: data here');
|
|
215
|
+
expect(data?.imageFile).toBe('custom.png');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { parseStarlingSpritesheet, parseStarlingSpritesheetDocument } from './starlingParse';
|
|
2
|
+
|
|
3
|
+
// ─── Fixtures ────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
const ATLAS_XML = `<?xml version="1.0" encoding="UTF-8"?>
|
|
6
|
+
<TextureAtlas imagePath="atlas.png">
|
|
7
|
+
<SubTexture name="hero_idle_0001" x="0" y="0" width="60" height="56"
|
|
8
|
+
frameX="-2" frameY="-4" frameWidth="64" frameHeight="64"
|
|
9
|
+
pivotX="32" pivotY="64"/>
|
|
10
|
+
<SubTexture name="hero_idle_0002" x="60" y="0" width="60" height="56"
|
|
11
|
+
frameX="-2" frameY="-4" frameWidth="64" frameHeight="64"
|
|
12
|
+
pivotX="32" pivotY="64"/>
|
|
13
|
+
<SubTexture name="hero_idle_0003" x="120" y="0" width="60" height="56"
|
|
14
|
+
frameX="-2" frameY="-4" frameWidth="64" frameHeight="64"
|
|
15
|
+
pivotX="32" pivotY="64"/>
|
|
16
|
+
<SubTexture name="fx_spark" x="0" y="56" width="16" height="16"/>
|
|
17
|
+
</TextureAtlas>`;
|
|
18
|
+
|
|
19
|
+
const ROTATED_XML = `<?xml version="1.0" encoding="UTF-8"?>
|
|
20
|
+
<TextureAtlas imagePath="sprites.png">
|
|
21
|
+
<SubTexture name="run_001" x="0" y="0" width="32" height="32" rotated="true"/>
|
|
22
|
+
<SubTexture name="run_002" x="32" y="0" width="32" height="32" rotated="true"/>
|
|
23
|
+
</TextureAtlas>`;
|
|
24
|
+
|
|
25
|
+
const MINIMAL_XML = `<TextureAtlas imagePath="mini.png">
|
|
26
|
+
<SubTexture name="tile" x="0" y="0" width="8" height="8"/>
|
|
27
|
+
</TextureAtlas>`;
|
|
28
|
+
|
|
29
|
+
describe('parseStarlingSpritesheet', () => {
|
|
30
|
+
it('returns a SpritesheetData (not a Parsed object)', () => {
|
|
31
|
+
const result = parseStarlingSpritesheet(ATLAS_XML);
|
|
32
|
+
expect(typeof result.frames).toBe('object');
|
|
33
|
+
expect((result as unknown as Record<string, unknown>).document).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('parses all SubTexture elements', () => {
|
|
37
|
+
expect(parseStarlingSpritesheet(ATLAS_XML).frames).toHaveLength(4);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('maps name, x, y, width, height', () => {
|
|
41
|
+
const data = parseStarlingSpritesheet(ATLAS_XML);
|
|
42
|
+
const frame = data.frames[0];
|
|
43
|
+
expect(frame.name).toBe('hero_idle_0001');
|
|
44
|
+
expect(frame.x).toBe(0);
|
|
45
|
+
expect(frame.y).toBe(0);
|
|
46
|
+
expect(frame.width).toBe(60);
|
|
47
|
+
expect(frame.height).toBe(56);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('converts frameX/Y to positive offsetX/Y', () => {
|
|
51
|
+
const data = parseStarlingSpritesheet(ATLAS_XML);
|
|
52
|
+
const frame = data.frames[0];
|
|
53
|
+
expect(frame.offsetX).toBe(2); // -(-2) = 2
|
|
54
|
+
expect(frame.offsetY).toBe(4); // -(-4) = 4
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('maps frameWidth/Height as sourceWidth/Height', () => {
|
|
58
|
+
const data = parseStarlingSpritesheet(ATLAS_XML);
|
|
59
|
+
const frame = data.frames[0];
|
|
60
|
+
expect(frame.sourceWidth).toBe(64);
|
|
61
|
+
expect(frame.sourceHeight).toBe(64);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('falls back to atlas width/height as sourceWidth/Height when no frame attrs', () => {
|
|
65
|
+
const data = parseStarlingSpritesheet(ATLAS_XML);
|
|
66
|
+
const spark = data.frames.find((f) => f.name === 'fx_spark')!;
|
|
67
|
+
expect(spark.sourceWidth).toBe(16);
|
|
68
|
+
expect(spark.sourceHeight).toBe(16);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('normalises pivotX/Y to 0–1 range', () => {
|
|
72
|
+
const data = parseStarlingSpritesheet(ATLAS_XML);
|
|
73
|
+
const frame = data.frames[0]; // pivotX=32, sourceWidth=64 → 0.5
|
|
74
|
+
expect(frame.pivotX).toBeCloseTo(0.5);
|
|
75
|
+
expect(frame.pivotY).toBeCloseTo(1.0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('sets pivot to null when absent', () => {
|
|
79
|
+
const data = parseStarlingSpritesheet(ATLAS_XML);
|
|
80
|
+
const spark = data.frames.find((f) => f.name === 'fx_spark')!;
|
|
81
|
+
expect(spark.pivotX).toBeNull();
|
|
82
|
+
expect(spark.pivotY).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('maps rotated flag', () => {
|
|
86
|
+
const data = parseStarlingSpritesheet(ROTATED_XML);
|
|
87
|
+
expect(data.frames[0].rotated).toBe(true);
|
|
88
|
+
expect(data.frames[1].rotated).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('defaults rotated to false when absent', () => {
|
|
92
|
+
const data = parseStarlingSpritesheet(ATLAS_XML);
|
|
93
|
+
expect(data.frames[0].rotated).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('extracts imagePath as imageFile', () => {
|
|
97
|
+
expect(parseStarlingSpritesheet(ATLAS_XML).imageFile).toBe('atlas.png');
|
|
98
|
+
expect(parseStarlingSpritesheet(MINIMAL_XML).imageFile).toBe('mini.png');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('infers animations from numbered frame names', () => {
|
|
102
|
+
const data = parseStarlingSpritesheet(ATLAS_XML);
|
|
103
|
+
const anim = data.animations.find((a) => a.name === 'hero_idle');
|
|
104
|
+
expect(anim).toBeDefined();
|
|
105
|
+
expect(anim!.frameNames).toHaveLength(3);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('sorts animation frames by numeric suffix', () => {
|
|
109
|
+
const data = parseStarlingSpritesheet(ATLAS_XML);
|
|
110
|
+
const anim = data.animations.find((a) => a.name === 'hero_idle')!;
|
|
111
|
+
expect(anim.frameNames[0]).toBe('hero_idle_0001');
|
|
112
|
+
expect(anim.frameNames[1]).toBe('hero_idle_0002');
|
|
113
|
+
expect(anim.frameNames[2]).toBe('hero_idle_0003');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('does not create animation for singleton frames', () => {
|
|
117
|
+
const data = parseStarlingSpritesheet(ATLAS_XML);
|
|
118
|
+
expect(data.animations.find((a) => a.name === 'fx')).toBeUndefined();
|
|
119
|
+
expect(data.animations.find((a) => a.name === 'fx_spark')).toBeUndefined();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('uses frameDuration option for inferred animations', () => {
|
|
123
|
+
const data = parseStarlingSpritesheet(ATLAS_XML, { frameDuration: 200 });
|
|
124
|
+
const anim = data.animations[0];
|
|
125
|
+
expect(anim.frameDuration).toBe(200);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('defaults frameDuration to 100 when option is absent', () => {
|
|
129
|
+
const data = parseStarlingSpritesheet(ATLAS_XML);
|
|
130
|
+
expect(data.animations[0].frameDuration).toBe(100);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('infers run animation from numbered frames', () => {
|
|
134
|
+
const data = parseStarlingSpritesheet(ROTATED_XML);
|
|
135
|
+
expect(data.animations).toHaveLength(1);
|
|
136
|
+
expect(data.animations[0].name).toBe('run');
|
|
137
|
+
expect(data.animations[0].frameNames).toHaveLength(2);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('handles a single frame with no numbered pattern', () => {
|
|
141
|
+
const data = parseStarlingSpritesheet(MINIMAL_XML);
|
|
142
|
+
expect(data.frames).toHaveLength(1);
|
|
143
|
+
expect(data.animations).toHaveLength(0);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('parseStarlingSpritesheetDocument', () => {
|
|
148
|
+
it('returns the same data as parseStarlingSpritesheet', () => {
|
|
149
|
+
const parsed = parseStarlingSpritesheet(ATLAS_XML);
|
|
150
|
+
const { data } = parseStarlingSpritesheetDocument(ATLAS_XML);
|
|
151
|
+
expect(data.frames.length).toBe(parsed.frames.length);
|
|
152
|
+
expect(data.imageFile).toBe(parsed.imageFile);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('preserves all SubTexture nodes in document', () => {
|
|
156
|
+
const { document } = parseStarlingSpritesheetDocument(ATLAS_XML);
|
|
157
|
+
expect(document.subTextures).toHaveLength(4);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('preserves imagePath in document', () => {
|
|
161
|
+
const { document } = parseStarlingSpritesheetDocument(ATLAS_XML);
|
|
162
|
+
expect(document.imagePath).toBe('atlas.png');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('preserves numeric attributes exactly', () => {
|
|
166
|
+
const { document } = parseStarlingSpritesheetDocument(ATLAS_XML);
|
|
167
|
+
const st = document.subTextures[0];
|
|
168
|
+
expect(st.name).toBe('hero_idle_0001');
|
|
169
|
+
expect(st.x).toBe(0);
|
|
170
|
+
expect(st.width).toBe(60);
|
|
171
|
+
expect(st.frameX).toBe(-2);
|
|
172
|
+
expect(st.frameY).toBe(-4);
|
|
173
|
+
expect(st.frameWidth).toBe(64);
|
|
174
|
+
expect(st.frameHeight).toBe(64);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { parseStarlingSpritesheet, parseStarlingSpritesheetDocument } from './starlingParse';
|
|
2
|
+
import { serializeStarlingSpritesheet } from './starlingSerialize';
|
|
3
|
+
|
|
4
|
+
const XML = `<?xml version="1.0" encoding="UTF-8"?>
|
|
5
|
+
<TextureAtlas imagePath="atlas.png">
|
|
6
|
+
<SubTexture name="hero" x="0" y="0" width="64" height="64"/>
|
|
7
|
+
<SubTexture name="coin" x="64" y="0" width="32" height="32"/>
|
|
8
|
+
</TextureAtlas>`;
|
|
9
|
+
|
|
10
|
+
const ATLAS_XML = `<?xml version="1.0" encoding="UTF-8"?>
|
|
11
|
+
<TextureAtlas imagePath="atlas.png">
|
|
12
|
+
<SubTexture name="hero_idle_0001" x="0" y="0" width="60" height="56"
|
|
13
|
+
frameX="-2" frameY="-4" frameWidth="64" frameHeight="64"
|
|
14
|
+
pivotX="32" pivotY="64"/>
|
|
15
|
+
<SubTexture name="hero_idle_0002" x="60" y="0" width="60" height="56"
|
|
16
|
+
frameX="-2" frameY="-4" frameWidth="64" frameHeight="64"
|
|
17
|
+
pivotX="32" pivotY="64"/>
|
|
18
|
+
<SubTexture name="hero_idle_0003" x="120" y="0" width="60" height="56"
|
|
19
|
+
frameX="-2" frameY="-4" frameWidth="64" frameHeight="64"
|
|
20
|
+
pivotX="32" pivotY="64"/>
|
|
21
|
+
<SubTexture name="fx_spark" x="0" y="56" width="16" height="16"/>
|
|
22
|
+
</TextureAtlas>`;
|
|
23
|
+
|
|
24
|
+
const ROTATED_XML = `<?xml version="1.0" encoding="UTF-8"?>
|
|
25
|
+
<TextureAtlas imagePath="sprites.png">
|
|
26
|
+
<SubTexture name="run_001" x="0" y="0" width="32" height="32" rotated="true"/>
|
|
27
|
+
<SubTexture name="run_002" x="32" y="0" width="32" height="32" rotated="true"/>
|
|
28
|
+
</TextureAtlas>`;
|
|
29
|
+
|
|
30
|
+
const MINIMAL_XML = `<TextureAtlas imagePath="mini.png">
|
|
31
|
+
<SubTexture name="tile" x="0" y="0" width="8" height="8"/>
|
|
32
|
+
</TextureAtlas>`;
|
|
33
|
+
|
|
34
|
+
describe('serializeStarlingSpritesheet', () => {
|
|
35
|
+
it('produces a TextureAtlas XML string', () => {
|
|
36
|
+
const data = parseStarlingSpritesheet(XML);
|
|
37
|
+
const serialized = serializeStarlingSpritesheet(data);
|
|
38
|
+
expect(serialized).toContain('<TextureAtlas');
|
|
39
|
+
expect(serialized).toContain('imagePath="atlas.png"');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('preserves frame count on round-trip', () => {
|
|
43
|
+
const data = parseStarlingSpritesheet(XML);
|
|
44
|
+
const serialized = serializeStarlingSpritesheet(data);
|
|
45
|
+
const reparsed = parseStarlingSpritesheet(serialized);
|
|
46
|
+
expect(reparsed.frames).toHaveLength(data.frames.length);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('round-trips frame names', () => {
|
|
50
|
+
const { data, document } = parseStarlingSpritesheetDocument(ATLAS_XML);
|
|
51
|
+
const data2 = parseStarlingSpritesheet(serializeStarlingSpritesheet(data, document));
|
|
52
|
+
expect(data2.frames.map((f) => f.name)).toEqual(data.frames.map((f) => f.name));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('round-trips atlas positions', () => {
|
|
56
|
+
const { data, document } = parseStarlingSpritesheetDocument(ATLAS_XML);
|
|
57
|
+
const data2 = parseStarlingSpritesheet(serializeStarlingSpritesheet(data, document));
|
|
58
|
+
expect(data2.frames[0].x).toBe(data.frames[0].x);
|
|
59
|
+
expect(data2.frames[0].width).toBe(data.frames[0].width);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('round-trips trim offsets', () => {
|
|
63
|
+
const { data, document } = parseStarlingSpritesheetDocument(ATLAS_XML);
|
|
64
|
+
const data2 = parseStarlingSpritesheet(serializeStarlingSpritesheet(data, document));
|
|
65
|
+
expect(data2.frames[0].offsetX).toBe(data.frames[0].offsetX);
|
|
66
|
+
expect(data2.frames[0].offsetY).toBe(data.frames[0].offsetY);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('round-trips source size', () => {
|
|
70
|
+
const { data, document } = parseStarlingSpritesheetDocument(ATLAS_XML);
|
|
71
|
+
const data2 = parseStarlingSpritesheet(serializeStarlingSpritesheet(data, document));
|
|
72
|
+
expect(data2.frames[0].sourceWidth).toBe(data.frames[0].sourceWidth);
|
|
73
|
+
expect(data2.frames[0].sourceHeight).toBe(data.frames[0].sourceHeight);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('round-trips pivot normalised values', () => {
|
|
77
|
+
const { data, document } = parseStarlingSpritesheetDocument(ATLAS_XML);
|
|
78
|
+
const data2 = parseStarlingSpritesheet(serializeStarlingSpritesheet(data, document));
|
|
79
|
+
expect(data2.frames[0].pivotX).toBeCloseTo(data.frames[0].pivotX!);
|
|
80
|
+
expect(data2.frames[0].pivotY).toBeCloseTo(data.frames[0].pivotY!);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('round-trips rotated flag', () => {
|
|
84
|
+
const { data, document } = parseStarlingSpritesheetDocument(ROTATED_XML);
|
|
85
|
+
const data2 = parseStarlingSpritesheet(serializeStarlingSpritesheet(data, document));
|
|
86
|
+
expect(data2.frames[0].rotated).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('round-trips imagePath', () => {
|
|
90
|
+
const { data, document } = parseStarlingSpritesheetDocument(ATLAS_XML);
|
|
91
|
+
const data2 = parseStarlingSpritesheet(serializeStarlingSpritesheet(data, document));
|
|
92
|
+
expect(data2.imageFile).toBe('atlas.png');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('produces well-formed XML with root element', () => {
|
|
96
|
+
const data = parseStarlingSpritesheet(MINIMAL_XML);
|
|
97
|
+
const xml = serializeStarlingSpritesheet(data);
|
|
98
|
+
expect(xml).toContain('<TextureAtlas');
|
|
99
|
+
expect(xml).toContain('<SubTexture');
|
|
100
|
+
expect(xml).toContain('</TextureAtlas>');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('omits optional attributes when not needed', () => {
|
|
104
|
+
const data = parseStarlingSpritesheet(MINIMAL_XML);
|
|
105
|
+
const xml = serializeStarlingSpritesheet(data);
|
|
106
|
+
expect(xml).not.toContain('frameX');
|
|
107
|
+
expect(xml).not.toContain('pivotX');
|
|
108
|
+
expect(xml).not.toContain('rotated');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('includes frameX/Y when offset is non-zero', () => {
|
|
112
|
+
const { data, document } = parseStarlingSpritesheetDocument(ATLAS_XML);
|
|
113
|
+
const xml = serializeStarlingSpritesheet(data, document);
|
|
114
|
+
expect(xml).toContain('frameX="-2"');
|
|
115
|
+
expect(xml).toContain('frameY="-4"');
|
|
116
|
+
});
|
|
117
|
+
});
|